一个 bug 引发的思考 -- Service Worker

1,443 阅读6分钟

在一个风和日丽的下午,我正在埋头苦(mo)干(yu)的时候,QA 同学急冲冲的跑了过来跟我说:你这网页怎么又挂了呀?这我必须给你提个 P0 的 bug 了!

我本能的脱口而出:不可能,在我本地明明是好的!

随后我拿 QA 手机一看:嗨呀,这不是没联网吗,当然刷不出网页啦!这是正常功能,不是 bug

善良的 QA 同学没有给我提 bug,但我也看出了她眼中带着一丝不满与一丝疑惑。

这不禁引发了我的思考:

就算没联网,那是不是可以给用户呈现之前的展示内容呢?

我激动的一拍大腿,那不就是 PWA 嘛!

PWA

传统 Web App:

  • 网页资源需要下载,因此弱网 / 无网条件下可能会出现白屏
  • 依赖浏览器作为入口,用户需要打开浏览器才能访问

Progressive Web Apps(渐进式增强 WEB APP):

  • 渐进增强:向上兼容
  • 可以在弱网 / 无网 条件下运行
  • 更安全: 必须是 https
  • 可安装:通过 manifest.json 配置,可以让用户将 web app “安装” 到桌面上
  • 支持消息推送:用户留存

今天重点介绍的是 PWA 的核心技术: Service Worker

Service Worker 是什么

Service worker 是一个运行在浏览器后台的 web worker 线程,是独立于网页运行的脚本。

【核心功能】

拦截和处理 scope 下面的所有请求,而且支持离线。相当于网页与服务端之间的一层可编程网络代理。

【特点】

  • 非侵入:无法操作DOM,但是可以通过 navigator.serviceWorker.controller.postMessage 与网页进行通信
  • 完全异步:基于 Promise
  • 安全性:localhost or https (可利用 github pages)
  • 一旦安装成功就会一直存在,除非手动 unregister
  • 在访问页面时会进入 activate 状态,在浏览器或者当前tab页关闭时会自动休眠,减少资源损耗
  • 支持离线缓存,消息推送等功能

使用 Service Worker

注册

使用 Service Worker 很简单,只要在你的 Web 项目里进行注册即可。

image.png

注册时机

需保证首屏加载不受影响:用户首次访问我们网页时,会下载 JS 等资源,然后浏览器进程中会执行 JS 线程、GUI 线程等渲染页面,如果这时候再启动一个 worker 线程,那必定会造成 CPU 和内存的争用,反而降低了首屏加载速度

所以,建议在页面加载完成后再注册

image.png

作用域

Service Worker 拥有自己的作用域,能够控制作用域范围内的所有页面。

【默认作用域】

Service Worker 的默认作用域是 sw 文件所在的目录:

  • 比如你的 sw.js 文件是在项目根目录下,那么默认作用域就是根目录;
  • 如果 sw.js 文件是在 /a 目录下,那么默认作用域就是 /a

image.png

【指定作用域】

Service Worker 也可以指定自己的作用域,只需要在注册时指定 scope 属性即可

image.png

【最大作用域】

Service Worker 还有一个概念叫做最大作用域,最大作用域不可超过默认作用域。

即假设你的默认作用域是 /a,那么你的最大作用域就是 /a

如果你指定的作用域超过了最大作用域,则会报错。

image.png image.png

【作用域污染】

作用域污染指的就是一个页面被多个 Service Worker 所控制。

比如,在 /a/index.html 注册了 /a/a-sw.js , 同时在 /index.html 注册了 /sw.js, 那么 /a/index.html 就同时被两个 Service Worker 所控制。

那么如何避免呢?

可以通过 navigator.serviceWorker.getRegistrations 获取当前所有已注册的 Service Worker,再通过 unregister 注销非当前作用域的 Service Worker

image.png

Service Worker 生命周期

Service Worker 的生命周期主要有几个阶段:

  • Install
  • Activate
  • Idle
  • Waiting
  • Redundant

我用下面这张图来概括它的生命周期流程:

image.png

Service Worker 缓存策略

Service Worker 的主要工作就是离线缓存,它的缓存策略有下面这几种:

Cache On Install

在 Install 生命周期就开始进行缓存。适用于:

  • 缓存静态资源
  • waitUntil 失败时,install 就会失败,因此尽量避免在 waitUntil 中缓存过多文件,降低失败概率

image.png

Clear On Activate

从生命周期流程可知,更新后的 Service Worker 进入 Activate 后,旧版本的 Service Worker 就废弃了。

而我们无法在 SW 中监听废弃状态,因此这里是处理旧版本缓存的最佳时机。

在 Activate 期间,fetch 等事件已经进入队列,因此这里的逻辑需要简洁,否则可能会影响页面加载

image.png

On User Interaction

可以在页面上渲染一个缓存按钮,让用户自己决定是否需要缓存

image.png

On Network Response

适用缓存动态资源及更新较快的场景:比如动态获取的图像资源,以及文章类,新闻类的请求

response.clone():由于响应只能被消费一次,因此需要先 clone 再放入缓存中

image.png

Cache, Faling Back to Network

顾名思义,有缓存优先使用缓存,无缓存时则 fetch。

前者适用于所缓存的动态资源,后者适用于大多数网络请求(比如非 GET 请求)。

如果二者都失败,则要考虑降级处理

image.png

Service Worker 其他应用

基于 Service Worker 的能力,我们还可以做很多事情。比如可以用来模拟心跳机制,从而检测页面是否 crash

  1. 页面加载后,间隔 5s 向 sw 发送一个心跳表示还在线,sw 会更新该网页的最新在线时间
  2. 在页面正常退出(beforeunload)时,通知 sw 当前页面已正常退出,sw 会清空该网页的记录
  3. 如果页面在运行过程中 crash,那么记录在 sw 中的最新在线时间就不会更新了
  4. sw 每隔一段时间 (10s)就去检测当前网页最新在线时间,如果已经超过一定时间没更新(15s),就判定为 crash

image.png

SW 核心代码:

image.png

Service Worker 存在问题

【兼容性问题】

Service Worker 目前最大的问题还是兼容性问题,可以看到在移动设备上需要比较新的版本才能很好的支持。

image.png

【无法检查外部引用更新】

由于浏览器是通过字节长度判断 Service Worker 是否有更新,所以如果通过 importScripts () 引入一个外部依赖,则当外部依赖内容改变时,浏览器无法识别当前 Service Worker 有更新

参考资料

web.dev/offline-coo… developers.google.com/web/fundame… w3c.github.io/ServiceWork…