Service Worker 缓存静态资源
最近有用到 Service Worker
和 CacheStorage
做离线缓存, 一方面是因为项目资源包着实大, 另一方面也是因为之前没有用缓存, 这也是需要的优化。
相比起 Http
缓存或者 manifest
文件管理缓存(如果还有其他缓存方法, 欢迎提出来讨论!), Service Worker
完全由前端维护, 让前端对资源拥有了更大的控制权, 能自由的对资源进行增删改, 另一方面也是因为自己没用过。也碰到了一些问题, 查阅了一些资料, 希望对你有帮助。
Service Worker
附上 MDN 文档 ,还有一个 Demo 除了 MDN, Google 的文档也是很有帮助的
Service Worker
的特点
- 是一个浏览器与网络之间的拦截器, 通过
Service Worker
你可以拦截任何网络请求 - 必须在 Https 下运行, 但 localhost 作为开发环境也可以
- 拥有自己的 Worker 上下文 ServiceWorkerGlobalScope (继承于 WorkerGlobalScope,而 WorkerGlobalScope 继承于 EventTarget), 与主线程 (原有的浏览器上下文) 互不干扰,
ServiceWorkerGlobalScope
包含的一些属性- Caches,这个后面会提到
- Clients,
- 在主线程中需要使用
navigator.serviceWorker
, 该对象的原型是ServiceWorkerContainer
, 包含注册,删除,更新Service Worker
以及与Service Worker
通信的方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14if ("serviceWorker" in navigator) {
try {
const registration = await navigator.serviceWorker.register("/worker.js?v=" + version)
if (registration.installing) {
console.log("Service worker installing")
} else if (registration.waiting) {
console.log("Service worker installed")
} else if (registration.active) {
console.log("Service worker active")
}
} catch (error) {
console.error(`Registration failed with ${error}`)
}
} - 如同页面有
load
unload
等生命周期事件一样,Service Worker
也拥有自己的生命周期, 下图来自 MDN, 描绘了Service Worker
的生命周期
安装中 => 安装后 => 激活中 => 激活后 => 结束
用户第一次进入网页,只是安装 woker,那么第二次进入网站才会激活 worker,激活后才会开始响应各类事件 - 相关事件,可以使用
addEventListener
进行监听- install, 生命周期事件
- activate,生命周期事件
- fetch,当浏览器发起请求获取资源时,可在此监听和拦截
1
2
3
4
5
6
7
8
9
10
11
12
13
14self.addEventListener("fetch", function (event) {
const { request, currentTarget } = event
event.respondWith(
caches.match(_name).then((cachedResponse) => {
if (cachedResponse) return cachedResponse
return fetch(request).then((response) => {
return caches.open(cacheName).then(function (cache) {
cache.put(_name, response.clone())
return response
})
})
})
)
}) - push,
- sync
caches
&& cache
上面提到了 Service Worker
可以拦截请求 => 拦截到静态资源的请求 => 获得 Response
=> 存储 Response
, caches
登场。
caches
被定义为 ServiceWorkerGlobalScope
的一部分, 但也被暴露在主线程的作用域下, ServiceWorkerGlobalScope.caches
和 Window.caches
都可以调用, 所以 caches
并不一定只能和 Worker
搭配使用。
注意:这里有两个重要的 api, caches
和 cahce
, 字面意思理解, 一个单数一个复数。
cache
, 一个存储区域, 以Request / Response
作为key / value
的模式来进行存储, 包含add
addAll
keys
delete
match
matchAll
put
等一些 对数据进行增删查改 的 apicaches
, 接口模型是CacheStorage
, 是一个cache
实例的集合, 包含keys
open
delete
has
match
等一些 对cache
增删查改 的 api
以上所有方法都是以 promise
的形式返回结果
如果我们需要操作数据, 那么需要先使用 open
方法获取 cache
实例, 如果该 cache
不存在就会新建一个
1 | caches.open(cacheName).then(function (cache) { |
存储限制
W3C 描绘了两种存储类型 Temporary
和 Persistent
。参考 W3C 文档 Temporary vs Persistent
CacheStorage
属于 Temporary
, 是作为类似 temp/
中的临时数据, 当浏览器存储到达了极限,那么会根据 LRU(least recently used) policy
来删除 Temporary
类型的数据,使存储再次回到限制内。(参考 MDN 文档 Browser storage limits and eviction criteria)
参考代码
我们可以在前端资源包中添加一个
Worker.json
的文件1
2
3
4
5{
useWorker: true, // 是否启用 `Worker`,根据该属性来注销和注册 `Worker`
version: 1.0 // 项目版本号
swVersion: 2.0 // worker 的版本号
}在应用初始化时以接口的方式请求该
Worker.json
文件, 对比返回的版本和当前的版本来决定是否对资源进行增删改通过修改
swVersion
版本号,也可以动态更新Worker.js
文件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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44const sw = {
registration: async (version) => {
if ("serviceWorker" in navigator) {
try {
const registration = await navigator.serviceWorker.register("/worker.js?v=" + version)
if (registration.installing) {
console.log("Service worker installing")
} else if (registration.waiting) {
console.log("Service worker installed")
} else if (registration.active) {
console.log("Service worker active")
}
} catch (error) {
console.error(`Registration failed with ${error}`)
}
}
},
unregistration: () => {
navigator.serviceWorker.getRegistrations().then((registrations) => {
for (let registration of registrations) {
registration.unregister()
sw.cleanRes()
}
})
},
cleanRes: () => {
const cacheName = "myResource"
caches.delete(cacheName).then((res) => {
res && window.location.reload()
})
},
}
api.system.getFrontendVersion().then((res) => {
if (!res.useWorker) {
Sw.unregistration()
return
}
Sw.registration(res.swVersion)
const _new = `${res.version}`,
_old = `${Settings.version}`
if (_new !== _old) {
Sw.cleanRes()
}
})Worker.js
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
26const resPath = "static",
cacheName = "myResource",
exclude = ["version.json"]
self.addEventListener("fetch", function (event) {
const { request, currentTarget } = event
const _name = isCacheable(request.url, currentTarget.registration.scope)
if (_name)
event.respondWith(
caches.match(_name).then((cachedResponse) => {
if (cachedResponse) return cachedResponse
return fetch(request).then((response) => {
return caches.open(cacheName).then(function (cache) {
cache.put(_name, response.clone())
return response
})
})
})
)
})
const isCacheable = (url, scope) => {
const _i = url.match(new RegExp(`${scope}${resPath}\/(?<name>.*)`))
if (!_i) return false
const name = _i.groups.name
if (exclude.some((el) => name.indexOf(el) > -1)) return false
return name
}
问题集锦
fetch event 不生效
Q: Worker 注册后, 调用了 install event, 调用了 activate event, 但却一个 fetch event 都没有调用, 为什么? fetch event 不生效?
A: 在实际场景里, 服务器路径由后端配置, 而静态资源通常是单独配置了路径。那如果我们需要监听所有的静态资源请求, 是否把 work 文件放在静态资源的目录下就可以了?
scope 这个参数在 MDN 中描绘得并不详细, Google 文档中的 scope 描述会更生动些。
A
Service Worker
‘s scope is determined by its location on a web server. If aService Worker
runs on a page located at /subdir/index.html, and is located at /subdir/sw.js, theService Worker
‘s scope is /subdir/.
所以我理解 Worker 文件需要和 Html 文件在同一级下, Html 产生的请求才会被捕获。所以不妨再请求下后端同学支持, 再为 Worker 配置一个路径, 与 html 同级。
Cannot construct a Request with a Request object that has already been used
request
has already consumed by the fetch()
, and request
can’t use twice. so clone request
before using it
an opaque request
it‘s not allowed to access response body from an opaque request
. cors-origin
request with mode: no-cors
and their response cannot be intercept by service worker, but change CORS-safelisted request headers
.