ServiceWorker 也是一种 worker,他的功能偏向代理或者缓存,应该算是功能最强大的一类 worker 了,PWA的实现就离不开ServiceWorker。
ServiceWorker API 大部分浏览器都已经支持了,可以看到 Can I Use 网站给出的数据如下
注册
在使用ServiceWorker之前要先对其进行注册,通过 navigator.serviceWorker.register 来注册一个ServiceWorker
if ('serviceWorker' in navigator) { // 当浏览器支持ServiceWorker时才进行注册 window.addEventListener('load', function() { navigator.serviceWorker.register('/service-worker.js'); }); } 复制代码
多个页面共用一个ServiceWorker,但一个页面只能使用一个ServiceWorker,同样的ServiceWorker也要遵循同源和 HTTPS的约束。某种角度来说ServiceWorker可以完成 SharedWorker 能做的事情,but 有点大材小用。
注册方法返回一个 Promise 对象,注册成功的状态是 resolved 失败则为 rejected,按理来说一般不会失败。
ServiceWorker的生效范围是注册路径的上一级路径,比如引入的ServiceWorker路径为/app/sw.js
,那么就会对/app
下的页面生效,而在/dev
等目录下则不会生效。
此时如果想要对其他作用域生效,可以显式地指定作用域
navigator.serviceWorker.register( '/app/service-worker.js', { scope: '/' } ) 复制代码
这样就可以在根目录下的所有页面生效。
通信
ServiceWorker 和主进程之间的通信也是基于事件驱动的,ServiceWorker支持了以下事件
生命周期
其中 install 和 activate 是受生命周期控制的,分别会在安装和激活时触发,message 则会在接收到主进程的 message 时触发;右边 3 个时间是功能性事件,从左到右分别会在请求、后台同步、推送通知时触发。
如果新版本的 ServiceWorker 出现报错,会继续使用旧版文件
生命周期:
installing:正在安装,此时会触发 install 事件
↓ 当 event.waitUntil 执行完成进入 installed;调用 self.skipWaiting 时可以跳过等待直接接管网页
installed:完成安装,此时并不会正常工作,而是进入 waiting 状态
↓ 等页面刷新之后激活
activating:正在激活,此时触发 activate 事件,可以通过调用self.clients.claim来控制未受控的客户端
↓ 当 event.waitUntil 执行完成进入 activated
activated:激活完成,此时可以正常监听功能性事件
↓
redundant:被替换
- install:ServiceWorker安装时触发,通常在这个时机缓存文件。
- activate:ServiceWorker激活时触发,通常在这个时机做一些重置的操作,例如处理旧版本ServiceWorker的缓存。
例如在 install 事件缓存文件
self.addEventListener('install', (e) => { let CACHE_NAME = 'app-1' let urls = ['/', '/index.js', '/style.css'] e.waitUntil( caches.open(CACHE_NAME). then(cache => cache.addAll(urls)) ) }) 复制代码
在 activate 事件清理无用的缓存
self.addEventListener('activate', (e) => { let cacheWhitelist = ['v2']; // 新版本文件白名单 e.waitUntil( Promise.all([ self.clients.claim(), caches.keys().then(function (keyList) { return Promise.all(keyList.map(function (key) { if (cacheWhitelist.indexOf(key) === -1) { return caches.delete(key); } })); }]) ); }) 复制代码
在 ServiceWorker 和主进程之间互发消息和之前的 worker 不一样,我们需要通过 navigator.serviceWorker.controller获取ServiceWorker实例,然后调用 postMessage 发送消息
主进程向 worker 发送(install完成之前 controller 获取不到,所以可以用另一种方式来发送)
// client if ('serviceWorker' in navigator) { window.addEventListener('load', function () { navigator.serviceWorker.register('/service-worker.js').then((registration) => { // navigator.serviceWorker.controller.postMessage('msg from client') let serviceWorker if (registration.installing) { serviceWorker = registration.installing; } else if (registration.waiting) { serviceWorker = registration.waiting; } else if (registration.active) { serviceWorker = registration.active; } if (serviceWorker) { serviceWorker.postMessage({ data: 'test msg' }) } }); }); } // ServiceWorker self.addEventListener('message', (e) => { console.log(e.data); }) 复制代码
如果要从 worker 中向主进程中发送消息则需要借助 MessageChannel来完成
// client function sendMessage(message) { return new Promise((resolve, reject) => { let messageChannel = new MessageChannel(); messageChannel.port1.onmessage = function (event) { if (event.data.error) { reject(event.data.error); } else { resolve(event.data); } }; navigator.serviceWorker.controller.postMessage(message, [messageChannel.port2]); }); } if ('serviceWorker' in navigator) { window.addEventListener('load', function () { navigator.serviceWorker.register('/service-worker.js').then((registration) => { // navigator.serviceWorker.controller.postMessage('msg from client') let serviceWorker if (registration.installing) { serviceWorker = registration.installing; } else if (registration.waiting) { serviceWorker = registration.waiting; } else if (registration.active) { serviceWorker = registration.active; } if (serviceWorker) { sendMessage({ data: 'msg from client' }).then(data => { console.log(data); }) } }); }); } // ServiceWorker self.addEventListener('message', (e) => { console.log(e.data); e.ports[0].postMessage({ msg: 'received msg', source: 'service-worker' }) }) 复制代码
功能事件
最常用的功能莫过于监听 fetch
事件来处理缓存了,主要是通过 fetch 和 cache 的配合来实现缓存或者代理等功能。
📢:fetch 会受 scope 限制,如果 scope 是/assets,则/api下的请求将不会触发 fetch 事件
self.addEventListener('fetch', (fetchEvent) => { if (!fetchEvent.request.url.startsWith(fetchEvent.request.referrer)) { // 不是本页面发出的请求不进行处理 return } fetchEvent.respondWith( // 缓存中匹配当前请求 caches.match(fetchEvent.request) .then(res => { // 存在缓存直接返回 if (res) { return res } else { // 否则重新请求并缓存 return fetch(fetchEvent.request).then((response) => { caches.open('app-1') .then(cache => cache.put(fetchEvent.request, response) ); return response.clone(); }) } }) ); }) 复制代码
通过 ServiceWorker 获取到缓存的响应
此时甚至可以断开网络访问页面
在缓存这里存在几种策略,上面代码属于缓存优先,除此之外还有网络优先、仅限缓存、仅限网络、异步缓存更新。
异步缓存更新就是从缓存返回结果的同时去网络刷新缓存,会在下一次请求时获取到最新数据。其他几个就跟字面意思一致了,通过 fetch 和 cache 相互配合即可完成。
调试
可以在 chrmoe 的chrome://serviceworker-internals/?devtools
中看到ServiceWorker的相关信息
点击 inspect 可以进入开发者工具