如何使用 ServiceWorker 增强应用
- 发布于
目录
ServiceWorker 是什么
ServiceWorker 是专门的 JavaScript 资源,用作网络浏览器和网络服务器之间的代理。类似于 Web Worker,所有工作都独立于主线程执行。
结合 Cache API,在 ServiceWorker 中拦截应用中所有请求并添加相应的缓存策略, 就可以在离线场景下,使用本地缓存降级支持所有请求响应,从而实现网络应用的离线使用。
请注意以下两点
- 使用 ServiceWorker 属于渐进式增强,也就是如果不支持 ServiceWorker 或者在 ServiceWorker 中工作出错,应用仅是退化成普通的 Web 应用,基本功能不会中断支持。
- 为确保安全 ServiceWorker 仅 https 和 localhost 可启用
1
ServiceWorker 的生命周期一个网页如果添加了 ServiceWorker 支持的话,首次访问网页时,页面的加载和 ServiceWorker 下载安装同时进行,页面正常提供基本功能。
在主线程注册 ServiceWorker
window.addEventListener('load', () => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js').then(() => {
console.log('Service worker registered!')
})
}
})
ServiceWorker 安装
ServiceWorker 会在注册后触发其 install 事件。每个 ServiceWorker 只能调用 install 一次,并且在更新前不会再次触发。
self.addEventListener('install', (event) => { })
ServiceWorker 激活 activated
如果注册和安装成功,ServiceWorker 就会激活,并且其状态会变为 'activating'。可以在此时开始工作。
self.addEventListener('activate', (event) => {
event.waitUntil(/** task() */)
})
在安装并激活 ServiceWorker 后,它将接管页面,开始工作,提高可靠性和速度。
更新
在执行导航和功能事件后,浏览器会自动检查更新,也可以手动触发更新:
navigator.serviceWorker.register('/service_worker.js').then(reg => {
// sometime later…
reg.update()
})
拦截网络请求
ServiceWorker 支持 fetch 事件,监听主线程发出的任何请求,先忽略这里 caches.open()
的调用,下文再介绍
const cacheName = 'ImageCacheName'
self.addEventListener('fetch', async (event) => {
if (event.request.destination === 'image') {
// 拦截所有图片请求,有缓存则直接返回,无则请求后缓存并返回
const cacheStore = await caches.open(cacheName)
const cachedImage = await cacheStore.match(event.request)
if (cachedImage) {
return cachedImage
}
const response = await fetch(event.request.url)
cacheStore.put(event.request, fetchedResponse.clone())
return response
}
})
event.request
类型声明
url
当前网络请求的网址method
HTTP methodmode
请求模式 值 'navigate' 通常用于区分对 HTML 文档的请求与其他请求destination
请求的内容类型,并避免使用所请求资源的文件扩展名
此时,network 面板可以看到,响应图片请求的不再是 server,而是 ServiceWorker(需要后面刷新页面才能看到)
Cache API
上面在 ServiceWorker 中缓存的使用与 HTTP 缓存并不冲突,Cache 接口是一种完全独立于 HTTP 缓存的缓存机制。可以认为是比 HTTP 应用层缓存更高一层的 JavaScript API 级别的磁盘缓存(不同于代码级别实现的运行时内存缓存)
Cache API 为 Request / Response 对象提供缓存机制,它与 ServiceWorker 的生命周期 API 的一部分提供,但它同时也是暴露在 window 作用域下的,不必一定要配合 ServiceWorker 使用。
关于 ServiceWorker 缓存的一些重要 API 方法包括:
CacheStorage.open
创建新的 Cache 实例,或者通过Window.caches
或WorkerGlobalScope.caches
访问Cache.add
和Cache.put
将网络响应存储在缓存中。Cache.match
在 Cache 实例中查找缓存的响应。Cache.delete
从 Cache 实例中移除缓存的响应。
缓存策略
下面介绍两种常用的缓存策略以支持应用不同资源的处理
缓存优先,回退到网络
上面示例采用的是缓存优先策略,有缓存则优先使用缓存,无缓存才发起请求。
这种策略适用于所有静态资源,CSS、JavaScript、图片、字体等等,虽然此类资源一般也会应用 HTTP 缓存策略,但在 ServiceWorker 中缓存后,可以进一步提高速度,以及获得离线使用能力。
网络优先,回退到缓存
此种策略下,资源请求一律在 ServiceWorker 转发为网络请求并更新到缓存响应,一但网络离线时,便直接使用缓存,这是实现应用离线使用的关键。
这种策略很适合 html 文档请求或者资源类 API
仅缓存
这种策略会将所有请求都导向缓存,就需要预缓存所有资源。
self.addEventListener('install', (event) => {
event.waitUntil(caches.open('StaticCacheStore').then((cache) => {
return cache.addAll([
'/1.js',
'/2.css',
'/3.jpg'
])
}))
})
组合不同的缓存策略可以为应用实现丝滑的在线和离线使用支持,比如这个博客中的静态资源采用缓存优先,页面文档等采用网络优先,可以关闭网络后尝试刷新,页面仍可以正常加载查看
Workerbox
Workerbox 是 Chrome 提供的一组 ServiceWorker 生命周期和缓存策略的封装,简化应用的 ServiceWorker 增强使用
- workbox-routing,用于请求匹配。
- workbox-strategies,适用于缓存策略。
- workbox-precaching(用于预缓存)。
- workbox-expiration,用于管理缓存。
- workbox-window,用于注册 Service Worker 并处理更新。
以及 worker-build 直接在开发构建过程中生成 ServiceWorker,如你使用 webpack 作为开发构建工具,还可直接选用已提供的 workbox-webpack-plugin GenerateSW
和 InjectManifest
示例:NextJS
import { precacheAndRoute } from 'workbox-precaching'
import { registerRoute } from 'workbox-routing'
import { StaleWhileRevalidate } from 'workbox-strategies'
declare const self: ServiceWorkerGlobalScope
// Precache files
precacheAndRoute(self.__WB_MANIFEST)
// Cache pages
registerRoute(
({ request }) => request.mode === 'navigate',
new StaleWhileRevalidate({
cacheName: 'pages-cache',
})
)
// Cache static resources
registerRoute(
({ request }) =>
request.destination === 'style' ||
request.destination === 'script' ||
request.destination === 'image',
new StaleWhileRevalidate({
cacheName: 'static-resources-cache',
})
)
const { InjectManifest } = require("workbox-webpack-plugin")
/**
* @type {import('next/dist/next-server/server/config').NextConfig}
**/
module.exports = {
// ...
webpack: (config, options) => {
config.plugin.push(new InjectManifest({
swSrc: './worker/service_worker.ts',
swDest: '/service_worker.js',
exclude: [
/^build-manifest\.json$/i,
/^react-loadable-manifest\.json$/i,
/\/_error\.js$/i,
/\.js\.map$/i,
/\.map$/, /_next\/static\/.*/
],
include: [/_next\/app-build-manifest\.json/],
}))
return config
},
}
最后可以看到,网络处于关闭状态,此时刷新页面,网络请求全部由 ServiceWorker 代理发起,虽然全部失败,但可以回退到缓存, ServiceWorker 仍能给到主线程响应,页面也得以正常加载展示,真正实现了离线使用
Footnotes
- 最后更新于