Service Workers 与离线缓存

系列文章:css

  1. Service Workers 和离线缓存 (本文)git

  2. Notification with Service Workers push eventsgithub

  3. PWA:添加应用至桌面及分享web

第一次听到 Service Workers 这个词仍是在去年 Google 来安利 Angular 2 的时候,那时就以为很惊艳,想搞一搞,可是由于没把网站升级成 https 一直拖到如今。不久前,把网站升级成了 https,终于能够搞一发了。chrome

本篇主要包含如下内容:docker

固然,仍是先来看看 Service Workers 到底是什么?

What's Service Workers?

Service Workers 是谷歌 chrome 团队提出并大力推广的一项 web 技术。在 2015 年,它加入到 W3C 标准,进入草案阶段。W3C 标准中对 Service Workers 的解释太细致,相对而言,我更喜欢 MDN 上的解释,更简练,更易于理解。

Service workers essentially act as proxy servers that sit between web applications, and the browser and network (when available). They are intended to (amongst other things) enable the creation of effective offline experiences, intercepting network requests and taking appropriate action based on whether the network is available and updated assets reside on the server. They will also allow access to push notifications and background sync APIs. - MDN

简单翻译一下:Service workers 基本上充当应用同服务器之间的代理服务器,能够用于拦截请求,也就意味着能够在离线环境下响应请求,从而提供更好的离线体验。同时,它还能够接收服务器推送和后台同步 API。

那么,这项技术的浏览器支持状况是什么样,仍是来看一眼 Can I use?

能够从看到,Chrome 和 Firefox, Opera 都已经支持 Service Workers,底下的备注也写到 Edge 在开发中,Safari 也考虑支持。至于 IE,船长都跳船了。看了 PC 端,再来看看移动端。移动端的支持率并不尽如人意,不过在安卓 4.4 以后,安卓原生浏览器,以及安卓版的 Chrome 都已经开始支持 Service Workers。

说句题外话,忽然发如今 Can I use 中选择导入我国数据时,竟出现了 UC 和 QQ 浏览器的支持状况,口以口以?...

言归正传,在真正开始使用 Service Workers 以前,还有几点要注意:

  1. Service Workers 基于 Https,这是硬性条件(如何升级 https 能够参考上一篇文章

  2. 每一个 Service Worker 都有本身的做用域,它只会处理本身做用域下的请求,而 Service Worker 的存放位置就是它的最大做用域

  3. Service Workder 是 Web Worker 的一种,它不可以直接操做 DOM

Github 上有一个很是棒的资源,它用图片的方式展现了 Servic Workers 的一些核心要点。

搞定这些基础就能够正式开搞了...

小试 Service Workers

和其余 worker 同样,service worker 有一个独自的文件。因为以前所提到的 service worker 只能做用在本身存放位置之下的文件,因此,通常在应用根目录下存放 service worker 文件。

首先,先写一个最简单的来看看浏览器是否是支持,以及可否正确地安装并运行 service worker。

// service-worker.js
const _self = this;

console.log('In service worker.');

_self.addEventListener('install', function () {
    console.log('Install success');
});

_self.addEventListener('activate', function () {
    console.log('Activated');
});

虽然,service worker 是 web worker 其中的一种,但它有些不一样,它有本身的注册方式。

// ServiceWorkerService.js
const SERVICE_WORKER_API = 'serviceWorker';
const SERVICE_WORKER_FILE_PATH = 'service-worker.js';

const isSupportServiceWorker = () => SERVICE_WORKER_API in navigator;

if (isSupportServiceWorker()) {
    navigator
        .serviceWorker
        .register(SERVICE_WORKER_FILE_PATH)
        .then(() => console.log('Load service worker Success.'))
        .catch(() => console.error('Load service worker fail'));
} else {
    console.info('Browser not support Service Worker.');
}

重启程序以后,你应该就能在控制台中看到 Load service worker Success.。然而,却没有另两句的输出,难道加载失败了?可是,控制台不是显示加载成功了么?不要担忧,程序没有出错,只是 service worker 中的日志信息有它本身的输出位置,而并不是输出在主日志之中。

接下去,先来看看如何调试 service worker。

调试 Service Workers

在 Chrome 中,service worker 的信息显示在 Application -> Service Workers 中,就像这样

里面会显示注册的 service worker,以及它当前的状态。还能经过切换最上面的选项来模拟不一样的网络环境,测试在不一样环境下 service worker 的响应,它们分别是:

  • Offline: 离线

  • Update on reload: 加载时更新

  • Bypass for network: 使用网络内容

回到以前的问题,如何查看 service worker 之中的日志哪?只需点击图中的 inspect 连接,它会弹出另外一个开发者窗口,在里面能够查看 service worker 的日志。是否是以为须要那么多步有点麻烦,别担忧,Chrome 已经替咱们解决了这个烦恼。从新刷新页面后,Chrome 的开发者工具中已经可以查看 service workers 的信息了,好比:在 console 选项卡勾选 Show all messages 就能显示 service workers 中控制台的信息;在 source 选项卡也能看到 service workers 的代码,固然也能够打断点啦~

在 firefox 中,默认会将 service worker 中的日志输出到主控制台中,但要打开 service worker 的调试器就有点麻烦了。有两种方法查看,一个是在地址栏中输入 about:debugging#workers,另外一种就是经过菜单栏中选择 Tools -> Web Developer -> Service Workers

更多关于在 firefox 中调试 service workers 的信息能够点此查看

虽然,已经将日志输出到主控制台了,可这里就有个疑问了,主页能不能获取 service workers 中的信息哪?答案是确定的,那就是经过 postMessage

经过 postMessage 与主窗口通讯

和 web worker 同样,service worker 与主窗口通信也须要经过 postMessage,但它的语法又有些许不一样。

首先,是主页面给 service worker 发消息。

// ServiceWorkerService.js
const sendMessageToSW = msg => navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage(msg);

if (isSupportServiceWorker()) {
    const sw = navigator.serviceWorker;

    sw.register(SERVICE_WORKER_FILE_PATH)
        .then(() => console.log('Load service worker Success.'))
        .catch(() => console.error('Load service worker fail'))
        .then(() => sendMessageToSW('Hello, service worker.'))
        .catch(() => console.error('Send message error.'));
} else {
    console.info('Browser not support Service Worker.');
}

能够看到,postMessage 方法并不在 worker 实例下,而是在 serviceWorker 下的 controller 对象下。这里须要注意一下,当 service worker 尚未注册成功时,navigator.serviceWorker.controller 对象的值是 null,因此,在调用 postMessage 以前须要确保 controller 对象已经存在。在 service worker 这边就没有什么区别了

// service-worker.js
_self.addEventListener('message', function(event) {
    console.log(event.data);
});

是否是很简单?不过,反过来 service worker 给主页面发消息就要复杂一点了。在 service worker 里发送信息须要经过 Client 对象的 postMessage 方法。获取 Client 的方法有不少,好比,刚从主页面发来的消息,事件的来源就是一个 Client 对象,即 event.source。不过,这只能向来源发消息,但若是你开了几个网页,或者不是经过主页消息发来的该怎么办哪?方法仍是有的,在 service workers 中能够经过 clients 来获取全部的页面对象或其余的 service workers。

// service-worker.js
_self.clients.matchAll().then(function(clients) {
    clients.forEach(function(client) {
        client.postMessage('Service worker attached.');
    })
});

不过,若是你发出一个消息须要等到另外一方的返回的消息作处理,上述的办法就作不到了。这时就须要创建一个通道来处理了,修改一下以前的 sendMessageToSW 方法。

// ServiceWorkerService.js
const sendMessageToSW = msg => new Promise((resolve, reject) => {
    const messageChannel = new MessageChannel();
    messageChannel.port1.onmessage = event => {
        if (event.data.error) {
            reject(event.data.error);
        } else {
            resolve(event.data);
        }
    };

    navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage(msg, [messageChannel.port2]);
});

这样信息发送出去后会返回一个 promise,而后就能够优雅地链式调用了。

// ServiceWorkerService.js
if (isSupportServiceWorker()) {
    const sw = navigator.serviceWorker;

    sw.register(SERVICE_WORKER_FILE_PATH)
        .then(() => console.log('Load service worker Success.'))
        .catch(() => console.error('Load service worker fail'))
        .then(() => sendMessageToSW('Hello, service worker.'))
        .then(console.log)
        .catch(() => console.error('Send message error.'));
} else {
    console.info('Browser not support Service Worker.');
}

了解了如何在浏览器中调试 service workers 和与主页面通讯这些基础以后,就能够搞一些正真功能性的东西,好比创造 service workers 最初的动机——提供更好的离线体验。

为应用添加离线缓存

为应用添加缓存的方式有不少,但可以提供离线缓存的,据我所知,那就只有 service workers 一家了。这就比如已经安装了的应用,不管是否有网络链接均可以随时打开使用(google 所推的 PWA 最终目的就是这个)。你可能会怀疑,听起来这么高大上实现起来会不会很复杂?然而并无,使用 service workers 为应用添加离线缓存仍是至关简单的。

就如同文章开头 MDN 中所提到的,service workers 能够充当应用与服务器以前的代理服务器,它经过监听 fetch 事件来捕捉本身做用域下发出的网络请求,并经过 event.respondWith 来返回请求结果,过程当中能够对返回结果作任何的修改(因此必须 https 啊)。

// service-worker.js
const handleFetchRequest = function(request) {
    return fetch(request);
};

const onFetch = function(event) {
    event.respondWith(handleFetchRequest(event.request));
};

_self.addEventListener('fetch', onFetch);

上面这段代码就是捕获请求最基本的方式,而后直接将请求发送出去,并将请求的结果返回,没有作其余额外的操做。若是,你这时观察控制台的网络请求,会发现全部请求的 size 都再也不是原先的文件大小或来自缓存,而是 from ServiceWorker

接下去,就来给应用添加离线缓存。既然,全部的请求都是手动发出的,并且可以拿到返回的结果,那么,缓存这些结果就变得垂手可得了。

不过,这里要先讲另外一个知识点——Cache Storage。它做为 service worker 的一部分写在草案中。经过它,咱们能够方便地把请求,以及请求结果一同缓存起来。了解了 Cache Storage,那就把上面的代码改一下,让它可以缓存请求。

// service-worker.js
const handleFetchRequest = function(request) {
    return caches.match(request)
        .then(function(response) {
            return response || fetch(request)
                    .then(function(response) {
                        const clonedResponse = response.clone();

                        caches.open(CACHE_NAME)
                            .then(function(cache) {
                                cache.put(request, clonedResponse);
                            });

                        return response;
                    });
        });
};

这里主要修改了如何处理请求的方法,先判断这个请求是否已经被缓存过了,缓存过了就直接返回结果,没有的话就去请求,并把结果添加到缓存中,以便下次请求来时能够直接返回。

离线缓存就这样添加好了,来看看效果怎么样。这就要用到以前调试时所提到的模拟不一样环境,不记得的童鞋能够往上翻一翻。(提示关键词:控制台, Application, Service Workers, Offline)这里模拟离线环境,设置好后再刷新页面。

Awesome~?

虽然已实现了离线缓存,可是,使用 Cache Storage 还须要注意如下几点:

  1. 它只能缓存 GET 请求;

  2. 每一个站点只能缓存属于本身域下的请求,同时也能缓存跨域的请求,好比 CDN,不过没法对跨域请求的请求头和内容进行修改

  3. 缓存的更新须要自行实现;

  4. 缓存不会过时,除非将缓存删除,而浏览器对每一个网站 Cache Storage 的大小有硬性的限制,因此须要清理没必要要的缓存。

上面的代码并无作缓存的清除和更新,因此,还要更新一下。同时,经过给跨域请求添加 {mode: 'cors'} 属性来使请求支持跨域,从而拿到响应头信息。

const HOST_NAME = location.host;
const VERSION_NAME = 'CACHE-v1';
const CACHE_NAME = HOST_NAME + '-' + VERSION_NAME;
const CACHE_HOST = [HOST_NAME, 'cdn.bootcss.com'];

const isNeedCache = function(url) {
    return CACHE_HOST.some(function(host) {
        return url.search(host) !== -1;
    });
};

const isCORSRequest = function(url, host) {
    return url.search(host) === -1;
};

const isValidResponse = function(response) {
    return response && response.status >= 200 && response.status < 400;
};

const handleFetchRequest = function(req) {
    if (isNeedCache(req.url)) {
        const request = isCORSRequest(req.url, HOST_NAME) ? new Request(req.url, {mode: 'cors'}) : req;
        return caches.match(request)
            .then(function(response) {
                // Cache hit - return response directly
                if (response) {
                    // Update Cache for next time enter
                    fetch(request)
                        .then(function(response) {

                            // Check a valid response
                            if(isValidResponse(response)) {
                                caches
                                    .open(CACHE_NAME)
                                    .then(function (cache) {
                                        cache.put(request, response);
                                    });
                            } else {
                                sentMessage('Update cache ' + request.url + ' fail: ' + response.message);
                            }
                        })
                        .catch(function(err) {
                            sentMessage('Update cache ' + request.url + ' fail: ' + err.message);
                        });
                    return response;
                }

                // Return fetch response
                return fetch(request)
                    .then(function(response) {
                        // Check if we received an unvalid response
                        if(!isValidResponse(response)) {
                            return response;
                        }

                        const clonedResponse = response.clone();

                        caches
                            .open(CACHE_NAME)
                            .then(function(cache) {
                                cache.put(request, clonedResponse);
                            });

                        return response;
                    });
            });
    } else {
        return fetch(req);
    }
};

升级以后,仍是有缓存先拿缓存,这样比较快,但依旧会在后台发出请求,若是返回合法的请求,就更新 cache 中的值,那么,下次访问时就是此次访问返回的结果了。

service worker 的 installactivite 事件对象都包含一个 waitUntil 方法,方法接受一个 promise,当 promise 被 resolve 后才会继续执行到下一个状态。若是,想要强制更新缓存,就能够经过这个方法在 service worker 激活时除旧版本缓存。

// service-worker.js
const onActive = function(event) {
    event.waitUntil(
        caches
            .keys()
            .then(function(cacheNames) {
                return Promise.all(
                    cacheNames.map(function(cacheName) {
                        // Remove expired cache response
                        if (CACHE_NAME.indexOf(cacheName) === -1) {
                            return caches.delete(cacheName);
                        }
                    })
                );
            })
    );
};

_self.addEventListener('activate', onActive);

这样请求的缓存就能随时更新了,不过,你可能会和我有一样的疑问——那 service workers 怎么更新呢?

Service workers 的生命周期与更新

事实上,service workers 的更新并不须要咱们操心,只要 service workers 文件有任何一点的修改,浏览器就会当即装载它。然而,它仍是有须要注意的地方,否则也就不值一提了。

虽然,浏览器当即装载它,但它并无当即生效,这和它的生命周期有关。下面这张图来自 Service Workers 101,很是形象地展现了 service workers 的生命周期。

先看图的右边,它展现了 service workers 的 3 种状态:Installing, WaitingActive;左边是 service workers 的生命周期,二者结合在一块儿,直观地展示了在 service workers 不一样的生命周期时,service workers 所处的状态。能够看到,installactivate 2 个时间中间,service workers 是处于 Waiting 的状态。

回到刚才提到的 service workers 更新,浏览器虽然会当即装载最新的 service workers,但只是让它 install,并进入 Waiting 的状态,而并无当即 activate。只有当用户将浏览器关闭后,从新打开页面时,旧的 service workers 才会被新的 service workers 替换。不过,图中也有提到,能够在 install 事件中 self.skipWaiting 方法来跳过等待,直接进入 activate 状态。一样的,能够在 activate 事件中调用 self.clients.claim 方法来更新全部客户端上的 service works。

为 service workers 添加上述两个方法就能较好地处理更新问题。代码改动很小,这里就再也不重复贴了,全部的代码都已上传 Github

下次准备捣鼓 service workers 相关的服务器推送,敬请关注...?

相关文章
相关标签/搜索