2019-04-02 | UNLOCK

Service Worker离线缓存实战

背景介绍

最近实战了Service Worker(以下简称“sw”)来进行网站缓存,以实现离线状态下,网站仍然可以正常使用。

尤其对于个人博客这种以内容为主体的静态网站,离线访问和缓存优化尤其重要;并且Ajax交互较少,离线访问和缓存优化的实现壁垒因此较低。

环境准备

虽然sw要求必须在https环境下才可以使用,但是为了方便开发者,通过localhost或者127.0.0.1也可以正常加载和使用。

利用cnpm下载http-servernpm install http-server -g

进入存放示例代码的文件目录,启动静态服务器:http-server -p 80

最后,准备下html代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<img src="./image.png" height="300" width="300"/>
<img src="https://user-gold-cdn.xitu.io/2017/10/4/50e8f96bbcb3bc644a083a409ce0ce2d?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" />
<h3>一些提示信息sdfsf</h3>
<ul>
<li>浏览器是否支持:<span id="isSupport"></span></li>
<li>service worker是否注册成功:<span id="isSuccess"></span></li>
<li>当前注册状态:<span id="state"></span></li>
<li>当前service worker状态:<span id="swState"></span></li>
</ul>
<script src="/script.js"></script>
</body>
</html>

注册Service Worker

我们通过script.js来判断浏览器是否支持serviceWorker,并且加载对应的代码。script.js内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
window.addEventListener('load', event => {
// 判断浏览器是否支持
if('serviceWorker' in navigator) {
console.log('支持')
window.navigator.serviceWorker
.register('/sw.js', {
scope: '/'
})
.then(registration => {
console.log('注册成功')
})
.catch(error => {
console.log('注册失败', error.message)
})
} else {
console.log('不支持')
}
})

注册时机

如上所示,最好在页面资源加载完成的事件(window.onload)之后注册serviceWorker线程。因为serviceWorker也会浪费资源和网络IO,不能因为它而影响正常情况下(网络信号ok的情况)的使用体验。

拦截作用域

之后,我们需要用serviceWorker线程来拦截资源请求,但不是所有的资源都能被拦截,这主要是看serviceWorker的作用域:它只管理其路由和子路由下的资源文件

例如上面代码中,/sw.js是serviceWorker脚本,它拦截根路径下的所有静态资源。如果是/static/sw.js,就只拦截/static/下的静态资源。

开发者也可以通过传递scope参数,来指定作用域。

Service Worker最佳实践

笔者爬了很久的坑,中途看了很多人的博客,包括张鑫旭老师的文章。但是实践的时候都出现了问题,直到读到了百度团队的文章才豁然开朗。

为了让sw.js的逻辑更清晰,这里仅仅展示最后总结出来的最优代码。如果想了解更多,可以跳到本章最后一个部分《参考链接》。

sw的生命周期

对于sw,它的生命周期有3个部分组成:install -> waiting -> activate。开发者常监听的生命周期是install 和 activate。

这里需要注意的是:两个事件的回调监听函数的参数上都有waitUntil函数。开发者传递到它的promise可以让浏览器了解什么时候此状态完成

如果难理解,可以看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const VERSION = 'v1'

self.addEventListener('install', event => {
// ServiceWoker注册后,立即添加缓存文件,
// 当缓存文件被添加完后,才从install -> waiting
event.waitUntil(
caches.open(VERSION).then(cache => {
return cache.addAll([
'./index.html',
'./image.png'
])
})
)
})

更新Service Worker代码

对于缓存的更新,可以通过定义版本号的方式来标识,例如上方代码中的VERSION变量。但对于ServiceWorker本身的代码更新,需要别的机制。

简单来说,分为以下两步:

  1. 在install阶段,调用 self.skipWaiting() 跳过waiting阶段,直接进入activate阶段
  2. 在activate阶段,调用 self.clients.claim() 更新客户端ServiceWorker

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const VERSION = 'v1'

// 添加缓存
self.addEventListener('install', event => {
// 跳过 waiting 状态,然后会直接进入 activate 阶段
event.waitUntil(self.skipWaiting())
})

// 缓存更新
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all([
// 更新所有客户端 Service Worker
self.clients.claim(),

// 清理旧版本
cacheNames.map(cacheName => {
// 如果当前版本和缓存版本不一样
if(cacheName !== VERSION) {
return caches.delete(cacheName)
}
})
])
})
)
})

再探更新

上一部分说了更新sw的2个步骤,但是为什么这么做呢?

因为对于同一个sw.js文件,浏览器可以检测到它已经更新(假设旧代码是sw1,新代码是sw2)。由于sw1还在运行,以及默认只运行一个同名的sw代码,所以sw2处于waiting状态。所以需要强制跳过waiting状态

进入activate后,还需要取得“控制权”,并且弃用旧代码sw1。上方的代码顺便清理了旧版本的缓存。

资源拦截

在代码的最后,需要监听 fetch 事件,并且进行拦截。如果命中,返回缓存;如果未命中,放通请求,并且将请求后的资源缓存下来。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
// 如果 Service Workder 有自己的返回
if(response) {
return response
}

let request = event.request.clone()
return fetch(request).then(httpRes => {
// http请求的返回已被抓到,可以处置了。

// 请求失败了,直接返回失败的结果就好了。。
if (!httpRes || httpRes.status !== 200) {
return httpRes
}

// 请求成功的话,将请求缓存起来。
let responseClone = httpRes.clone()
caches.open(VERSION).then(cache => {
cache.put(event.request, responseClone)
})

return httpRes
})
})
)
})

效果测试

启动服务后,进入 localhost ,打开devtools面板。可以看到资源都通过ServiceWorker缓存加载进来了。

image.png

现在,我们打开离线模式,

image.png

离线模式下照样可以访问:

image.png

最后,我们修改一下html的代码,并且更新一下sw.js中标识缓存版本的变量VERSION:

image.png

在第2次刷新后,通过上图可以看到,缓存版本内容已更新到v2,并且左侧内容区已经被改变。

参考链接

请针对 Disqus 开启代理