Service Worker是什么
service worker 是独立于当前页面的一段运行在浏览器后台进程里的脚本。它的特性将包括推送消息,背景后台同步, geofencing(地理围栏定位),拦截和处理网络请求。
这个 API 会让人兴奋的原因是,它可以使你的应用先访问本地缓存资源,所以在离线状态时,在没有通过网络接收到更多的数据前,仍可以提供基本的功能(一般称之为 Offline First)。
在 service worker 之前,另一个叫做 APP Cache 的 api 也可以提供离线体验。APP Cache 的的主要问题是坑比较多,而且其被设计为只适合于单页 web 应用程序,对于传统的多页网站则不适合。service worker 的设计规避了这些痛点。
关于 service worker 的一些注意点:
- service worker 是一个JavaScript worker ,所以它不能直接访问 DOM 。但 service worker 可以通过postMessage 接口与跟其相关的页面进行通信,发送消息,从而让这些页面在有需要的时候去操纵 DOM 。
- Service worker 是一个可编程的网络代理,允许你去控制如何处理页面的网络请求, 可以处理fetch请求。
- Service worker 在不使用时将被终止,并会在需要的时候重新启动,因此你不能把onfetch 和 onmessage事件来作为全局依赖处理程序。如果你需要持久话一些信息并在重新启动Service worker后使用他,可以使用 IndexedDBAPI ,service worker 支持。
- Service Worker 的缓存机制是依赖 Cache API 实现的
- Service worker 广泛使用了 promise。
- Service worker依赖 HTML5 fetch API
- Service Workers 要求必须在 HTTPS 下才能运行
Service Worker生命周期
注册service worker,在网页上生效
安装成功,激活 或者 安装失败(下次加载会尝试重新安装)
激活后,在sw的作用域下作用所有的页面,首次控制sw不会生效,下次加载页面才会生效。
sw作用页面后,处理fetch(网络请求)和message(页面消息)事件 或者 被终止(节省内存)。
需要提前掌握的API
- Cache API基本使用
(1)检测api是否存在
if('caches' in window) { // Has support! }
(2)caches.open,创建缓存总对象。如下创建名为 test-cache 的缓存。
caches.open('test-cache').then(function(cache) { // Cache is created and accessible });
(3)cache.add和cache.addAll,添加缓存内容。其中cache.add只添加一个,cache.addAll可以添加多个。
caches.open('test-cache').then(function(cache) { cache.addAll(['/', '/images/logo.png']) .then(function() { // Cached! // or use cache.add cache.add('/page/1'); // "/page/1" URL will be fetched and cached! }); });
(4)cache.keys(),查看已经缓存的数据
caches.open('test-cache').then(function(cache) { cache.keys().then(function(cachedRequests) { console.log(cachedRequests); // [Request, Request] }); });
(5)cache.match和cache.matchAll,匹配缓存文件路径
caches.open('test-cache').then(function(cache) { cache.match('/page/1').then(function(matchedResponse) { console.log(matchedResponse); }); });
(6)cache.delete,删除缓存。
caches.open('test-cache').then(function(cache) { cache.delete('/page/1'); });
- Fetch API基本使用
// url (required), options (optional) fetch('https://davidwalsh.name/some/url', { method: 'get' }).then(function(response) { }).catch(function(err) { // Error :( });
其中options对象包含以下属性:
- method - GET, POST, PUT, DELETE, HEAD
- url - 请求的链接
- headers - 请求的header对象
- referrer - 请求的referrer对象
- mode - cors, no-cors, same-origin
- credentials - 设置请求可不可以携带cookie
- redirect - follow, error, manual
- integrity - 子资源完整值
- cache - 缓存模式 (default, reload, no-cache)
可以在fetch中传入Request对象实例:
var request = new Request('https://davidwalsh.name/users.json', { method: 'POST', mode: 'cors', redirect: 'follow', headers: new Headers({ 'Content-Type': 'text/plain' }) }); // Now use it! fetch(request).then(function() { /* handle response */ });
(3)可以自定义返回的Response对象实例,其中的options有:
- type - basic, cors
- url
- useFinalURL - 上面的url参数是不是最终的URL
- status - 状态码(ex: 200, 404, etc.)
- ok - 是否成功响应 (范围在 200-299)
- statusText - 状态码 (ex: OK)
- headers - 响应的headers对象
另外Response的实例还具备以下方法:
- clone() - 创建Response对象的克隆。
- error() - 返回与网络错误关联的新Response对象。
- redirect() - 使用不同的URL创建新响应。
- arrayBuffer() - 返回使用ArrayBuffer解析的promise。
- blob() - 返回使用Blob解析的promise。
- formData() - 返回使用FormData对象解析的promise。
- json() - 返回使用JSON对象解析的promise。
- text() - 返回使用USVString(文本)解析的promise。
// Create your own response for service worker testing // new Response(BODY, OPTIONS) var response = new Response('.....', { ok: false, status: 404, url: '/' }); // The fetch's `then` gets a Response instance back fetch('https://davidwalsh.name/') .then(function(responseObj) { console.log('status: ', responseObj.status); });
Service Worker的使用
- 兼容低版本,注入Cache API的一个polyfill,Service Worker需要依赖Cache API:
self.importScripts('./serviceworker-cache-polyfill.js');
- 注册service worker:
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').then(function(registration) { // Registration was successful console.log('ServiceWorker registration successful with scope: ', registration.scope); }).catch(function(err) { // registration failed :( console.log('ServiceWorker registration failed: ', err); }); }
上面的代码检查 service worker API 是否可用,如果可用, /sw.js 这个文件将会作为 service worker 被注册。
如果这个 service worker 已经被注册过,浏览器会自动忽略上面的代码。
有一个特别要注意是 service worker 文件的路径。你一定注意到,在这个例子中,service worker 文件被放在这个域的根目录下,这意味着 service worker是跟网站同源的。换句话说,这个 service worker 将会获取到这个域下的所有 fetch 事件。如果 service worker文件注册到/example/sw.js ,那么 service worker 只能收到 /example/ 路径下的 fetch 事件(比如: /example/page1/, /example/page2/)。
- 安装service worker:
var CACHE_NAME = 'my-site-cache-v1'; var urlsToCache = [ '/', '/styles/main.css', '/script/main.js' ]; self.addEventListener('install', function(event) { // Perform install steps event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { console.log('Opened cache'); return cache.addAll(urlsToCache); }) ); });
上面代码声明了需要缓存的内容,如果所有的文件都缓存成功,service worker 就安装成功了。如果任何一个文件下载失败,那么安装步骤就会失败。这个方式依赖于你自己指定的资源,但这意味着,你需要非常仔细地确定哪些文件需要被缓存。指定了太多文件的话,会增加失败率。
- 缓存和返回请求
self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { // Cache hit - return response if (response) { return response; } // IMPORTANT: Clone the request. A request is a stream and // can only be consumed once. Since we are consuming this // once by cache and once by the browser for fetch, we need // to clone the response var fetchRequest = event.request.clone(); return fetch(fetchRequest).then( function(response) { // Check if we received a valid response if(!response || response.status !== 200 || response.type !== 'basic') { return response; } // IMPORTANT: Clone the response. A response is a stream // and because we want the browser to consume the response // as well as the cache consuming the response, we need // to clone it so we have 2 stream. var responseToCache = response.clone(); caches.open(CACHE_NAME) .then(function(cache) { cache.put(event.request, responseToCache); }); return response; } ); }) );
如果我们想在缓存中添加新的请求缓存,可以通过处理fetch请求的response,将其添加到缓存中即可。代码里我们做了以下事情:
添加一个 callback 到 fetch 请求的 .then 方法中。 一旦我们获得一个 response,我们进行如下的检查:
- 确保 response 有效
- 检查 response 的状态是200
- 确保 response 的类型是 basic 类型的,这说明请求是同源的,这意味着第三方的请求不能被缓存。
如果检查通过会clone 这个请求。这么做的原因是如果 response 是一个 Stream,那么它的 body 只能被消费一次。所以为了让浏览器跟缓存都使用这个body,我们必须克隆这个 body,一份到浏览器,一份到缓存中缓存。
- 重新激活
你的 service worker 总会有要更新的时候。在那时,你需要按照以下步骤来更新:
更新你 service worker 的 JavaScript 文件 当用户浏览你的网站时,浏览器尝试在后台重新下载 service worker 的脚本文件。经过对比,只要服务器上的文件和本地文件有一个字节不同,这个文件就认为是新的。
之后更新后的 service worker 启动并触发 install 事件。
此时,当前页面生效的依然是老版本的 service worker,新的 service worker 会进入 “waiting” 状态。
当页面关闭之后,老的 service worker 会被干掉,新的 servicer worker 接管页面 一旦新的 service worker 生效后会触发 activate 事件。 通常来讲,需要在 activate 的 callback 中进行 cache 管理,来清理老的 cache。我们在 activate 而不是 install 的时候进行的原因,是如果我们在 install 的时候进行清理,那么老的 service worker 仍然在控制页面,他们依赖的缓存就失效了,因此就会突然被停止。
之前我们使用的缓存可以叫 my-site-cache-v1 ,我们想把这个拆封到多个缓存,一份给页面使用,一份给博客文章使用。这意味着,install 步骤里,我们要创建两个缓存: pages-cache-v1 和 blog-posts-cache-v1。在 activite 步骤里,我们需要删除旧的 my-site-cache-v1。
下面的代码会遍历所有的缓存,并删除掉不在 cacheWhitelist 数组(我们定义的缓存白名单)中的缓存。
self.addEventListener('activate', function(event) { var cacheWhitelist = ['pages-cache-v1', 'blog-posts-cache-v1']; event.waitUntil( caches.keys().then(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName); } }) ); }) ); });
参考文章
http://kailian.github.io/2017/03/01/service-worker
https://juejin.im/post/5ba0fe356fb9a05d2c43a25c
https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API/Using_Service_Workers
https://lavas.baidu.com/doc/offline-and-cache-loading/service-worker/how-to-use-service-worker
https://zhuanlan.zhihu.com/p/20040372
https://zhuanlan.zhihu.com/p/28161855
https://www.villainhr.com/page/2017/01/08/Service Worker 全面进阶