使用Service Worker构建离线web应用

Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步API。 – MDN

都2019了,得益于web的跨平台、开发周期短等优势以及硬件设备、网络环境等的不断升级,web app成为了每一个产品优先考虑的实现方案。Service Worker的出现更是让web app的使用体验更靠近native app了,随着17年年底Apple Safari也开始支持Service Worker,可见PWA即是未来。

今天就使用Service Worker完成一个简单的,可离线访问的网页。

基本页面

首先是一个基本的html,里面挂载了我们需要的各种静态资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!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">
<link rel="stylesheet" href="./index.css">
<title>sw-demo</title>
</head>
<body>
<h1>Service Worker离线应用demo</h1>
<img src="./demo.jpg" alt="demo">
<p></p>
<p></p>
<script src="index.js"></script>
</body>
</html>

注册Service Worker

这里的index.js即为基本的注册逻辑, service-worker.js是service worker的具体逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
const tipContainer = document.getElementsByTagName('p')[0]

if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./service-worker.js')
.then(() => {
const content = 'ServiceWorker注册成功'
console.log(content)
tipContainer.innerHTML = content
})
.catch(() => {
tipContainer.innerHTML = 'ServiceWorker注册失败,请更换新版浏览器后重试'
})
}

编写Service Worker的缓存逻辑

Service Worker生命周期 & 缓存静态资源

在这之前我们要先简单了解Service Worker的生命周期。

从上图可知Service Worker载入成功后由安装(install)到激活(activate),我们在开始安装缓存指定的静态资源,缓存用到了浏览器的Cache接口。

1
2
3
4
5
6
7
8
9
10
11
12
const cacheKey = 'v1'
const cacheList = ['/pwa/', '/pwa/demo.jpg', '/pwa/index.css', '/pwa/index.js']

this.addEventListener('install', (event) => {
event.waitUntil(
caches.open(cacheKey)
.then((cache) => cache.addAll(cacheList))
.then(() => {
console.log('installed')
})
)
})

代码中的event.waitUntil扩展了事件的生命周期,只有当其传递的Promise被resolve,才会继续执行对应的事件。在这里之所以要用waitUntil,个人的理解是为了 保证在该生命周期中的代码能够成功完整的执行。比如我们如果没有将缓存逻辑写在waitUntil里面,在添加缓存的过程中部分缓存添加失败了,然而Service Worker的install事件在脚本内容不变的情况下,之后只会执行一次,那么我们后面在activate事件读取的缓存都是残缺的。所以将代码写在waitUntil之中即可保证我们所需要业务逻辑是完整可靠的执行下来,如果失败则在下次install安装事件中继续尝试。

读取静态资源

上面将静态资源缓存起来了,那么有存必有取,要怎样读取缓存的静态资源呢?

先来看看service worker支持的事件。

上图可以看到service worker可以监听fetch事件,然后用respondWith来劫持请求响应即可实现缓存的读取,如果匹配到了缓存则读取缓存,否则继续发起请求。

1
2
3
4
5
6
this.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
)
})

如何更新

因为现在的缓存都交给service worker,所以我们不能够缓存service worker脚本,然后通过更新缓存中的缓存名(cacheKey)和缓存列表(cacheList)在激活事件中即可通过匹配cacheKey来进行旧缓存的清除。比如将上面的cacheKey = v1 改为 cacheKey = v2。更多更新细节可参考谨慎处理 Service Worker 的更新

1
2
3
4
5
6
7
8
9
10
11
12
this.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys()
.then((keyList) => {
return Promise.all(keyList.map((key) => {
if (key !== cacheKey) {
return caches.delete(key)
}
}))
})
)
})

到这里一个简单的离线应用就完成了。

完整的源码https://github.com/reesexu/sw-demo

预览地址https://demo.xuwenchao.site/pwa/

参考

  1. 使用 Service Workers
  2. Cache