在一个风和日丽的下午,我正在埋头苦(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 项目里进行注册即可。
注册时机
需保证首屏加载不受影响:用户首次访问我们网页时,会下载 JS 等资源,然后浏览器进程中会执行 JS 线程、GUI 线程等渲染页面,如果这时候再启动一个 worker 线程,那必定会造成 CPU 和内存的争用,反而降低了首屏加载速度
所以,建议在页面加载完成后再注册
作用域
Service Worker 拥有自己的作用域,能够控制作用域范围内的所有页面。
【默认作用域】
Service Worker 的默认作用域是 sw 文件所在的目录:
- 比如你的 sw.js 文件是在项目根目录下,那么默认作用域就是根目录;
- 如果 sw.js 文件是在
/a
目录下,那么默认作用域就是/a
【指定作用域】
Service Worker 也可以指定自己的作用域,只需要在注册时指定 scope
属性即可
【最大作用域】
Service Worker 还有一个概念叫做最大作用域,最大作用域不可超过默认作用域。
即假设你的默认作用域是 /a
,那么你的最大作用域就是 /a
。
如果你指定的作用域超过了最大作用域,则会报错。
【作用域污染】
作用域污染指的就是一个页面被多个 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
Service Worker 生命周期
Service Worker 的生命周期主要有几个阶段:
- Install
- Activate
- Idle
- Waiting
- Redundant
我用下面这张图来概括它的生命周期流程:
Service Worker 缓存策略
Service Worker 的主要工作就是离线缓存,它的缓存策略有下面这几种:
Cache On Install
在 Install 生命周期就开始进行缓存。适用于:
- 缓存静态资源
- waitUntil 失败时,install 就会失败,因此尽量避免在 waitUntil 中缓存过多文件,降低失败概率
Clear On Activate
从生命周期流程可知,更新后的 Service Worker 进入 Activate 后,旧版本的 Service Worker 就废弃了。
而我们无法在 SW 中监听废弃状态,因此这里是处理旧版本缓存的最佳时机。
在 Activate 期间,fetch 等事件已经进入队列,因此这里的逻辑需要简洁,否则可能会影响页面加载
On User Interaction
可以在页面上渲染一个缓存按钮,让用户自己决定是否需要缓存
On Network Response
适用缓存动态资源及更新较快的场景:比如动态获取的图像资源,以及文章类,新闻类的请求
response.clone():由于响应只能被消费一次,因此需要先 clone 再放入缓存中
Cache, Faling Back to Network
顾名思义,有缓存优先使用缓存,无缓存时则 fetch。
前者适用于所缓存的动态资源,后者适用于大多数网络请求(比如非 GET 请求)。
如果二者都失败,则要考虑降级处理
Service Worker 其他应用
基于 Service Worker 的能力,我们还可以做很多事情。比如可以用来模拟心跳机制,从而检测页面是否 crash
- 页面加载后,间隔 5s 向 sw 发送一个心跳表示还在线,sw 会更新该网页的最新在线时间
- 在页面正常退出(beforeunload)时,通知 sw 当前页面已正常退出,sw 会清空该网页的记录
- 如果页面在运行过程中 crash,那么记录在 sw 中的最新在线时间就不会更新了
- sw 每隔一段时间 (10s)就去检测当前网页最新在线时间,如果已经超过一定时间没更新(15s),就判定为 crash
SW 核心代码:
Service Worker 存在问题
【兼容性问题】
Service Worker 目前最大的问题还是兼容性问题,可以看到在移动设备上需要比较新的版本才能很好的支持。
【无法检查外部引用更新】
由于浏览器是通过字节长度判断 Service Worker 是否有更新,所以如果通过 importScripts () 引入一个外部依赖,则当外部依赖内容改变时,浏览器无法识别当前 Service Worker 有更新
参考资料
web.dev/offline-coo… developers.google.com/web/fundame… w3c.github.io/ServiceWork…