来自 「何天翔」 同学的内部分享。
Service Worker 简介
Service Workers 本质上是一种能在浏览器后台运行的独立线程,它能够在网页关闭后持续运行,能够拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源,从而实现拦截和加工网络请求、消息推送、静默更新、事件同步等一系列功能,是 PWA 应用的核心技术之一。
与普通 JS 运行环境相比,Service Workers 有如下特点:
无法直接访问 DOM , 可通过 postMessage 发送消息与页面通信;
能够控制页面发送网络请求;
必须在 HTTPS 协议下运行;
开发过程中可以通过 locakhost 使用 service worker。
![525508a63bf8ea9dab0b337eaf2f7962.png](https://img-blog.csdnimg.cn/img_convert/525508a63bf8ea9dab0b337eaf2f7962.png)
本文将从应用角度,简单汇总下 Service Workers 几个核心概念,包括:API、生命周期、waitUntil 机制、调试等。
生命周期
Service Worker 的生命周期完全独立于网页。生命周期 (install -> waiting -> activate -> fetch):
![c894ac7dcd5fb0092ec94313d5842d0d.png](https://img-blog.csdnimg.cn/img_convert/c894ac7dcd5fb0092ec94313d5842d0d.png)
其中, install
事件是 Service Worker 获取的第一个事件,并且只发生一次。
主要逻辑 & API
register
install
activate
fetch
skipWaiting
// register
if ('serviceWorker' in navigator) {
// 由于 127.0.0.1:8000 是所有测试 Demo 的 host
// 为了防止作用域污染,将安装前注销所有已生效的 Service Worker
navigator.serviceWorker.getRegistrations()
.then(regs => {
for (let reg of regs) {
reg.unregister()
}
navigator.serviceWorker.register('./sw.js')
})
}
// sw.js
console.log('service worker 注册成功')
self.addEventListener('install', () => {
// 安装回调的逻辑处理
console.log('service worker 安装成功')
})
self.addEventListener('activate', () => {
// 激活回调的逻辑处理
console.log('service worker 激活成功')
})
self.addEventListener('fetch', event => {
console.log('service worker 抓取请求成功: ' + event.request.url)
})
「waitUntil 机制」
参考:https://developer.mozilla.org/zh-CN/docs/Web/API/ExtendableEvent/waitUntil
ExtendableEvent.waitUntil()
方法告诉事件分发器该事件仍在进行。这个方法也可以用于检测进行的任务是否成功。在服务工作线程中,这个方法告诉浏览器事件一直进行,直至 promise resolve,浏览器不应该在事件中的异步操作完成之前终止服务工作线程。
「skipWaiting」
Service Worker 一旦更新,需要等所有的终端都关闭之后,再重新打开页面才能激活新的 Service Worker,这个过程太复杂了。通常情况下,开发者希望当 Service Worker 一检测到更新就直接激活新的 Service Worker。如果不想等所有的终端都关闭再打开的话,只能通过 skipWaiting 的方法了。
Service Worker 在全局提供了一个 skipWaiting()
方法,skipWaiting()
在 waiting 期间调用还是在之前调用并没有什么不同。一般情况下是在 install 事件中调用它。
「clients.claim() 方法」
如果使用了 skipWaiting 的方式跳过 waiting 状态,直接激活了 Service Worker,可能会出现其他终端还没有受当前终端激活的 Service Worker 控制的情况,切回其他终端之后,Service Worker 控制页面的效果可能不符合预期,尤其是如果 Service Worker 需要动态拦截第三方请求的时候。
为了保证 Service Worker 激活之后能够马上作用于所有的终端,通常在激活 Service Worker 后,通过在其中调用 self.clients.claim()
方法控制未受控制的客户端。self.clients.claim()
方法返回一个 Promise,可以直接在 waitUntil()
方法中调用,如下代码所示:
self.addEventListener('activate', event => {
event.waitUntil(
self.clients.claim()
.then(() => {
// 返回处理缓存更新的相关事情的 Promise
})
)
})
如何处理 Service Worker 的更新
如果目前尚未有活跃的 SW ,那就直接安装并激活。
如果已有 SW 安装着,向新的
swUrl
发起请求,获取内容和和已有的 SW 比较。如没有差别,则结束安装。如有差别,则安装新版本的 SW(执行install
阶段),之后令其等待(进入waiting
阶段)如果老的 SW 控制的所有页面 「全部关闭」,则老的 SW 结束运行,转而激活新的 SW(执行
activated
阶段),使之接管页面。
![be2060157b4f00e7b07d6e6cec4dca3e.png](https://img-blog.csdnimg.cn/img_convert/be2060157b4f00e7b07d6e6cec4dca3e.png)
![a441d4a16da08e5407ab2b0f84e7049a.png](https://img-blog.csdnimg.cn/img_convert/a441d4a16da08e5407ab2b0f84e7049a.png)
![27f25da028669a111f85b041045a9dfc.png](https://img-blog.csdnimg.cn/img_convert/27f25da028669a111f85b041045a9dfc.png)
方法一:skipWaiting
问题:同一个页面,前半部分的请求是由 sw.v1.js
控制,而后半部分是由 sw.v2.js
控制。这两者的不一致性很容易导致问题,甚至网页报错崩溃
方法二:skipWaiting + 刷新
let refreshing = false
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) {
return
}
refreshing = true;
window.location.reload();
});
问题:毫无征兆的刷新页面的确不可接受,影响用户体验
方法三:给用户一个提示
大致的流程是:
浏览器检测到存在新的(不同的)SW 时,安装并让它等待,同时触发
updatefound
事件我们监听事件,弹出一个提示条,询问用户是不是要更新 SW
如果用户确认,则向处在等待的 SW 发送消息,要求其执行
skipWaiting
并取得控制权因为 SW 的变化触发
controllerchange
事件,我们在这个事件的回调中刷新页面即可
问题:
弊端一:过于复杂
弊端二:刷新逻辑的实现必须通过 JS 完成更新
如何调试
![d50035ef52baeaf94776bc77050ad932.png](https://img-blog.csdnimg.cn/img_convert/d50035ef52baeaf94776bc77050ad932.png)
为了更熟练的运用 Chrome Devtools 调试 Service Worker,首先需要熟悉以下这些选项:
「Offline」:复选框可以将 DevTools 切换至离线模式。它等同于 Network 窗格中的离线模式。
「Update on reload」:复选框可以强制 Service Worker 线程在每次页面加载时更新。
「Bypass for network」:复选框可以绕过 Service Worker 线程并强制浏览器转至网络寻找请求的资源。
「Update」:按钮可以对指定的 Service Worker 线程执行一次性更新。
「Push」:按钮可以在没有负载的情况下模拟推送通知。
「Sync」:按钮可以模拟后台同步事件。
「Unregister」:按钮可以注销指定的 Service Worker 线程。
「Source」:告诉当前正在运行的 Service Worker 线程的安装时间,链接是 Service Worker 线程源文件的名称。点击链接会将定向并跳转至 Service Worker 线程来源。
「Status」:告诉 Service Worker 线程的状态。此行上的数字指示 Service Worker 线程已被更新的次数。如果启用
update on reload
复选框,接下来会注意到每次页面加载时此数字都会增大。在状态旁边会看到start
按钮(如果 Service Worker 线程已停止)或stop
按钮(如果 Service Worker 线程正在运行)。Service Worker 线程设计为可由浏览器随时停止和启动。使用 stop 按钮明确停止 Service Worker 线程可以模拟这一点。停止 Service Worker 线程是测试 Service Worker 线程再次重新启动时的代码行为方式的绝佳方法。它通常可以揭示由于对持续全局状态的不完善假设而引发的错误。「Clients」:告诉 Service Worker 线程作用域的原点。如果已启用
show all
复选框,focus
按钮将非常实用。在此复选框启用时,系统会列出所有注册的 Service Worker 线程。如果这时候点击正在不同标签中运行的 Service Worker 线程旁的focus
按钮,Chrome 会聚焦到该标签。
总结
完整流程
![2e8f25392288b37638d46dcd8ea0cc79.png](https://img-blog.csdnimg.cn/img_convert/2e8f25392288b37638d46dcd8ea0cc79.png)
应用场景
基于service worker 可以实现拦截和处理网络请求、消息推送、静默更新、事件同步等服务。
离线缓存:配合 CacheStorage 可以将应用中不变化的资源或者很少变化的资源长久的存储在用户端,提升加载速度、降低流量消耗、降低服务器压力,提高请求速度,让用户体验更加丝滑
消息推送:激活沉睡的用户,推送即时消息、公告通知,激发更新等。如web资讯客户端、web即时通讯工具、h5游戏等运营产品。
事件同步:确保web端产生的任务即使在用户关闭了web页面也可以顺利完成。如web邮件客户端、web即时通讯工具等。
定时同步:周期性的触发Service Worker脚本中的定时同步事件,可借助它提前刷新缓存内容
结合CacheStorage、 Push API 和 Notification API
参考链接:
https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
https://juejin.cn/post/6844903792522035208
https://www.simicart.com/blog/pwa-service-worker/