Service Worker 主要作用是用来拦截请求,可以让我们拦截并修改资源请求,精准地缓存资源。
特性:
- 不能访问DOM和window对象
- 运行于其他进程,不会阻塞主线程
- 无法使用同步API (XHR,localStorage)
- 只能HTTPS (
localhost也可以) - 拦截资源请求
安装(注册)
navigator.serviceWorker.register('./sw.js')
对于浏览器兼容性问题可以使用
if('serviceWorker' in navigator)来进行判断
Service Worker会根据其所在目录的不同而拦截不同范围(scope)的请求, 比如
navigator.serviceWorker.register('./sw.js').then(r => {
console.log('Service Worker registration successful with scope: ', r.scope)
// http://localhost:8000/demo/
})
navigator.serviceWorker.register('./subpages/sw.js').then(r => {
console.log('Service Worker registration successful with scope: ', r.scope)
// http://localhost:8000/demo/subpages/
})
我们可以通过传入 scope参数来规定 Service Worker 的拦截范围(Service Worker文件所在目录为默认以及可指定的最大scope)
navigator.serviceWorker.register('./sw.js', {
scope: './subpages'
})
无法指定ServiceWorker所在路径的上级路径, 比如
{ scope: '/' }, 将会抛出错误
然后打开我们的浏览器控制台, 在应用程序(application)面板中选中 Service Workers 栏就可以看见我们注册成功的 Service Worker 了
Service Worker 注册激活后会在下次页面加载的时候启用, 可以使用 clients.claim() 立即启用
self.addEventListener('activate', event => {
event.waitUntil(clients.claim());
})
事件及方法
事件
我们可以使用 self.addEventListener('xxx')来为Service Worker添加事件监听
| 事件类型 | 生命周期 | 说明 | 常见用途 |
|---|---|---|---|
生命周期 Lifecycle | install | ServiceWorker 安装触发 | 操作IndexDB、缓存站点资源 |
| activate | ServiceWorker 激活触发 | 清除旧缓存 | |
| message | ServiceWorker 接受信息触发 | ||
Functional Events | fetch | 请求 | 拦截请求 |
| sync | 同步 | ||
| push | 推送 |
生命周期
- 在调用 register 注册 Service Worker 之后会触发它的安装(
install)事件,可以用来操作IndexDB和缓存站点资源 - 在Service Worker安装完毕后,就会触发激活事件(
activate)

更新
一个 Service Worker 在安装并激活后,如果它有新的版本, 则会在后台安装并进入等待模式, 不会激活(worker in waiting时序),直到已加载页面不再使用旧的worker才会激活。self.skipWaiting()就是用来跳过该等待阶段,直接对现有worker进行更新.
我们可以通过以下方法来对ServiceWorker进行更新
- 手动调用
**update()**进行更新 (逐字节匹配,需要Service Worker文件与历史文件不同) - 自动更新(无操作后24小时)
我们可以监听 updatefound来知道 Service Worker 是否有更新来决定是否立即更新Worker
navigator.serviceWorker.register('./sw.js').then(function(registration) {
registration.addEventListener('updatefound', function() {
const installingWorker = registration.installing
console.log('A new service worker is being installed:', installingWorker)
const res = confirm('发现新的worker, 是否更新')
res && registration.update().then(() => {
console.log('worker update success!')
location.reload()
})
});
}).catch(function(err) {
console.log('ServiceWorker registration failed: ', err);
})
比如我们在看Vue等技术的官方文档的时候,右下角可能会提示”页面内容有更新”,让我们点击更新对内容进行更新。可以在加载页面的时候请求最新的 Service Worker 版本,与 localstorage 中的版本号进行对比,如果不同则弹框让用户选择更新或者自动调用更新方法进行更新。
const latestVersion = await request('xxxx')
navigator.serviceWorker.register('./sw.js').then(function(registration) {
const v = localStorage.getItem(KEY_SERVICE_WORKER)
if (v !==latestVersion) {
registration.update().then(_ => {
localStorage.setItem(KEY_SERVICE_WORKER, WorkerVersion)
})
}
})
使用场景
缓存站点资源文件
比如我们想要缓存index.html所引用的main.css样式文件
// 待缓存资源列表
const cacheUrls = [
'./theme/default.css'
]
const CACHE_NAME = 'theme-v1'
// 1. 监听 Service Worker 安装事件
self.addEventListener('install', e => {
self.skipWaiting()
console.log('main worker installed!', e)
// 2. 确保 Service Worker 不会在 waitUntil() 里面的代码执行完毕之前安装完成
e.waitUntil(
// 3. 指定缓存名称
caches.open(CACHE_NAME).then(cache => cache.addAll(cacheUrls))
)
})
// 4. 监听请求
self.addEventListener('fetch', e => {
e.respondWith(
// 5.使用缓存来匹配当前请求的URL
caches.match(e.request).then(res => {
// 6. 如果有对于缓存, 直接返回缓存, 否则请求资源
return res || fetch(e.request)
})
)
})
- 监听
install事件 - 使用
waitUntil来确保 Service Worker 不会在 waitUntil() 里面的代码执行完毕之前安装完成 - 使用
Cache API来创建并缓存指定 url 资源caches.open(缓存名称)用来创建指定名称缓存addAll(urls)用来指定缓存资源列表
- 监听
fetch事件(拦截ServiceWorker指定scope下的浏览器请求) - 匹配缓存资源(通过 url 和 vary header进行)
- 无匹配的缓存资源, 则恢复请求
我们还可以通过配置自定义 request header 来让 Service Worker 动态缓存请求资源
(改造上面步骤6)
function handleRequest(request) {
// 有自定义缓存头 offline: 1, 则缓存资源
if (request.headers.get('offline') === '1') {
console.log('cache request:', e.request.url)
return fetch(request).then(res => {
return caches.open(CACHE_NAME).then(cache => {
cache.put(request, res.clone())
return res
})
})
} else {
return fetch(request)
}
}
const cacheUrls = [
'theme/theme-default.css',
]
const CACHE_NAME = 'style-v2'
self.addEventListener('install', e => {
self.skipWaiting()
console.log('main worker installed!', e)
e.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(cacheUrls))
)
})
self.addEventListener('fetch', e => {
e.respondWith(
// 5.使用缓存来匹配当前请求的URL
caches.match(e.request).then(res => {
// 6. 如果有对于缓存, 直接返回缓存, 否则请求资源
return res || handleRequest(e.request)
})
// 没有资源或者网络不可用的回退方案
.cache(() => {
return caches.match('/theme/theme-default.css')
})
)
})
// 注册
navigator.serviceWorker.register('./sw.js').then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope, registration);
}).catch(function(err) {
console.log('ServiceWorker registration failed: ', err);
});
// 添加测试按钮
const btn = document.createElement('button')
btn.innerHTML = '改变主题(缓存)'
document.body.appendChild(btn)
// 添加按钮事件
btn.addEventListener('click', () => {
// 自定义请求头
const headers = new Headers()
headers.set('offline', '1')
// 请求资源
fetch('./theme/theme-dark.css', {
headers
}).then(async res => {
const fileContent = await res.text()
console.log('改变主题', 'dark')
const styleElement = document.createElement('style')
styleElement.innerHTML = fileContent
styleElement.setAttribute('rel', 'stylesheet')
document.head.appendChild(styleElement)
})
})
打开devtools后,我们点击按钮进行测试,可以在网络面板看到,请求先访问了ServiceWorker,发现没有缓存后ServiceWorker进行资源请求,所以会有两次请求
我们再次点击按钮请求资源, 发现只多了一个请求,为ServiceWorker从缓存中读取
具体的缓存记录我们可以在 应用程序(application) 面板中的 缓存 菜单查看
