service worker 实现离线缓存

阅读数:118 2019 年 9 月 25 日 23:48

service worker 实现离线缓存

近两年前端比较热的话题之一就是 PWA(Progressive Web APP)——渐进式网页;致力于实现与原生 APP 相似的交互体验。

最早听说 pwa,是 2017 年年底的时候,看到朋友圈里的前端大神分享相关文章、应用。今年大概 3 月份的时候,看到 ios 从 11.3 开始支持后,就开始认真了解相关实现。

了解后发现,这一技术涉及到的内容很多。总结下来,pwa 的实现其实主要依赖于以下三点:

1)manifest 实现手机主界面的 web app 图标、标题、开屏 icon 等;
2)service worker 实现离线缓存请求、更新缓存、删除缓存;iOS safari 在 11.3 系统已支持;
3)push/notification 实现消息推送及接收。

其中 service worker 是离线应用的关键,我们主要来说说 service worker(以下简称 SW)。

1service worker 是什么

在说 SW 之前,先来回顾下 js 的单线程问题。

众所周知,js 在浏览器中是单线程运行的;主要是为了准确操作 DOM。但单线程存在的问题是,UI 线程和 js 线程需要抢占资源,在 js 执行耗时逻辑时,容易造成页面假死,用户体验较差。

由此出现了异步操作,可不影响主界面的响应。如 ajax、promise 等。

后来 html5 开放的 web worker 可以在浏览器后台挂载新线程。它无法直接操作 DOM,无法访问 window、document、parent 对象。

SW 是 web worker 的一种,也是挂载在浏览器后台运行的线程。主要用于代理网页请求,可缓存请求结果;可实现离线缓存功能。也拥有单独的作用域范围和运行环境

1.1 SW 使用限制

SW 除了 work 线程的限制外,由于可拦截页面请求,为了保证页面安全,浏览器端对 sw 的使用限制也不少。

1)无法直接操作 DOM 对象,也无法访问 window、document、parent 对象。可以访问 location、navigator;

2)可代理的页面作用域限制。默认是 sw.js 所在文件目录及子目录的请求可代理,可在注册时手动设置作用域范围;

3)必须在 https 中使用,允许在开发调试的 localhost 使用。

1.2 SW 兼容性

目前移动端 chrome 及 ios safari 11.3 以上已支持,pc 端火狐、谷歌、欧朋等支持,总体来看移动端及 pc 端都可以尝试使用。

service worker 实现离线缓存

不过在 chrome 里调试是最方便的,可以直观看到当前 SW 的状态、使用页面:

service worker 实现离线缓存

在 cacheStorage 查看缓存空间的存储信息:

service worker 实现离线缓存

1.3 service worker 可以解决的问题

1)用户多次访问网站时加快访问速度;

2)离线缓存接口请求及文件,更新、清除缓存内容;

3)可以在客户端通过 indexedDB API 保存持久化信息。

2 生命周期

生命周期有 5 步:注册、安装成功(安装失败)、激活、运行、销毁。

事件:install、activate、message、fetch、push、async。

由于是离线缓存,所以在首次注册、二次访问、服务脚本更新等会有不同的生命周期。

2.1 首次注册流程

从主线程注册后,会下载服务工作线程的 js。
安装:执行过程即是 installing 过程,此时会触发 install 事件,并执行 installEvent 的 waitUtil 方法。执行完毕后,当前状态是 installed。
激活:立即进入 activating 状态;并触发 activate 事件,处理相关事件内容。执行完成后,变成 activated 状态。
销毁: 当安装失败或进程被关闭时。

service worker 实现离线缓存

2.2 二次访问

在上一次服务未销毁时,二次访问页面,直接停留在激活运行状态:

service worker 实现离线缓存

2.3 服务脚本更新

如上图,sw.js 中,每次访问都会被下载一次。并且至少每 24 小时会被下载一次。为的是避免错误代码一直被运行。
下载后,会比对是否更新,如果更新,就会重新注册安装 serivceworker,安装成功后会处于 waiting 状态。
当 clients 都被关闭后,再次打开,才会激活最新的代码。
当然,也有方法可以跳过等待,比方说在安装完成后,执行 installEvent.skipWaiting()

service worker 实现离线缓存
service worker 实现离线缓存

3SW 的实际使用

3.1 注册

在主线程 main.js 中调起注册方法 register,register 只会被执行一次。

复制代码
1// 主线程的 main.js
2// 先判断浏览器是否支持
3if('serviceWorker' in navigator){
4 navigator.serviceWorker
5
6 // scope 是自定义 sw 的作用域范围为根目录,默认作用域为当前 sw.js 所在目录的页面
7 .register('./sw.js', {scope: '/'})
8
9 // 注册成功后会返回 reg 对象,指代当前服务线程实例
10 .then(reg => {
11 console.log('注册成功')
12 })
13 .catch(error => {
14 console.log('注册失败')
15 })
16}else{
17 console.log('当前浏览器不支持 SW')
18}

3.2 安装

在服务线程中添加安装监听及对应需缓存的资源文件:

复制代码
1// 在 sw.js 中监听对应的安装事件,并预处理需要缓存的文件
2// 该部分内容涉及到 cacheStorage API
3
4// 定义缓存空间名称
5const CACHE_NAME = 'sylvia_cache'
6
7// 定义需要缓存的文件目录
8let filesToCache = [
9 '/src/css/test.css',
10 '/src/js/test.js'
11]
12
13// 监听安装事件,返回 installEvent 对象
14self.addEventListener('install', function(event){
15
16 // waitUntil 方法执行缓存方法
17 event.waitUntil(
18
19 // cacheStorage API 可直接用 caches 来替代
20 // open 方法创建 / 打开缓存空间,并会返回 promise 实例
21 // then 来接收返回的 cache 对象索引
22 caches.open(CACHE_NAME).then(cache => {
23
24 // cache 对象 addAll 方法解析(同 fetch)并缓存所有的文件
25 cache.addAll(filesToCache)
26 })
27 )
28})

3.3 激活

第一次注册并安装成功后,会触发 activate 事件:

复制代码
1self.addEventListener('activate', event => {
2 console.log('安装成功,即将监听作用域下的所有页面')
3})

在有 sw 脚本更新时,在后台默默注册安装新的脚本文件,安装成功后进入 waiting 状态。当前所有老版本控制的页面关闭后,再次打开时,新版本的脚本触发 activate 事件,此时可清除旧缓存,当前是修改 CACHE_NAME 的值来实现清除之前的缓存。

复制代码
1// 监听激活事件
2self.addEventListener('activate', event => {
3 // 获取所有的缓存 key 值,将需要被缓存的路径加载放到缓存空间下
4 var cacheDeletePromise = caches.keys().then(keyList => {
5 Promise.all(keyList.map(key => {
6 if(key !== CACHE_NAME){
7 var deletePromise = caches.delete(key)
8 return deletePromise
9 }else{
10 Promise.resolve()
11 }
12 }))
13 })
14 // 等待所有的缓存都被清除后,直接启动新的缓存机制
15 event.waitUtil(
16 Promise.all([cacheDeletePromise]).then(res => {
17 this.clients.claim()
18 })
19 )
20})

3.4 运行

安装并激活成功后,就可以对页面实行 fetch 监控啦。

复制代码
1// 可以过滤使用已缓存的请求,若无缓存,由 fetch 发起新的请求,并返回给页面
2self.addEventListener('fetch', event => {
3 event.respondWith(
4 caches.match(event.request).then(res => {
5 if(res){
6 return res
7 }else{
8 return fetch(event.request)
9 }
10 })
11 )
12})
13
14// 也可连续将请求缓存到内存里
15self.addEventListener('fetch', event => {
16 event.respondWith(
17 caches.match(event.request).then(response => {
18 if(response){
19 return response
20 }
21 let requestClone = event.request.clone()
22 return fetch(requestClone).then(res => {
23 if(!res || res.status !== 200){
24 return res
25 }
26 let resClone = res.clone()
27 caches.open(CACHE_NAME).then(cache => {
28 cache.put(event.request, resClone)
29 })
30 return res
31 })
32 })
33 )
34})

3.5 进程销毁

1)当安装失败时会被浏览器丢弃该工作线程

2)浏览器后台启动 SW 时,会消耗性能,所以在不需要使用缓存时,可销毁

self.terminate()

4 应用框架 workbox

目前 chrome 有出一套完整的 SW 实用框架,可以较低成本的实现离线缓存

并提前封装好了对应所需的 API。

4.1 使用方法

在 sw.js 的文件中直接引入 workbox 的 cdn 上的文件:

复制代码
1importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.0.0-alpha.3/workbox-sw.js')
2if(workbox){
3 console.log('your workbox is working now')
4}else{
5 console.log('it can't work!')
6}
7

如果浏览器支持,可以直接引用 API 接口:

1)precaching,可以在注册成功后直接缓存的文件;

2)routing,匹配符合规则的 url,与 strategies 合作来完成文件的缓存。

示例:

复制代码
1// 注册完成后,即缓存对应的文件列表
2workbox.precaching.precacheAndRoute([
3 '/src/static/js/index.js',
4 '/src/static/css/index/css',
5 {url: '/src/static/img/logo.png', revision" }
6])
7
8// routing 方法匹配请求文件路径,strategies 用来存储对应文件
9workbox.routing.registerRoute(
10 matchFunction, // 字符串或者是正则表达式
11 handler // 可以使用 workbox.strategies 缓存策略来缓存
12)

workbox.strategies 缓存策略有:

1)staleWhileRevalidate 使用已有的缓存,然后发起请求,用请求结果来更新缓存;

2)networkFirst 先发起请求,请求成功后会缓存结果。如果失败,则使用最新的缓存;

3)cacheFirst 总是先使用缓存,如果无匹配的缓存,则发起网络请求并缓存结果;

4)networkOnly 强制发起请求;

5)cacheOnly 强制使用缓存。

示例:

复制代码
1 // 缓存使用方法
2workbox.routing.registerRoute(
3 '/src/index.js',
4 workbox.strategies.staleWhileRevalidate()
5)

5 总结

以上可以实现简单的离线缓存。

使用场景
1)web 页面可将较少变动的静态资源放到客户端缓存中;

2)将一些基本数据缓存后,可以让用户在无网、弱网环境下,也能正常访问页面;

3)sw 其他的类似消息推送的功能,主要用在多页面同步消息,比如:邮件、即时通讯等。

但在实际使用中,一般离线内容有: 数据请求类、静态资源类、html 页面类等。
1)数据请求类:主要可缓存一些重要数据,需要注意数据量,一般浏览器分配给 sw 的空间是有限的,需要及时更新及删除无效数据;

2)静态资源类:一般是放在 cdn 上的文件,这时需要处理一些跨域缓存问题;

3)html 页面类:页面的缓存需要谨慎处理,因为如果注册 sw 的代码写在页面上的话,注意采用第二种缓存更新方法。

缓存更新,大多是两种:
1)引用新的 sw.js 的文件,即修改注册时的文件名;

2)修改 cacheStorage 中的缓存对象名称(推荐这种)。

回到 html 的缓存问题。如果缓存了注册 sw 的页面文件,那么访问时,总会从缓存中取页面内容。此时若采用第一种缓存方式,那么会出现 sw 永远无法更新的问题。

目前来看还是比较推荐使用 workbox 来接入,实现比较方便,缓存策略也比较实用。

作者介绍:
西薇亚(企业代号名),贝壳找房租赁平台前端工程师。

本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。

原文链接:

https://mp.weixin.qq.com/s/aboA9dtCK6t0fzs0JU58Iw

评论

发布