什么是 Service Worker
Service Worker(后面简称SW) 是 Web Worker
的一种增强版,在后台独立于网页运行,和普通脚本拥有不同的运行容器,其中运行的代码不会被普通的JS阻塞,同时也不会阻塞其它页面的JS文件中的代码。常见于 PWA 中,用于拦截全局的 fetch 事件,实现离线缓存、后台同步等操作。
SW主要包含以下特性:
- 有自己的上下文,独立于当前网页进程
- 一旦安装,永远存在,需要手动卸载
- 事件型驱动,使用时自动唤醒,不使用时自动休眠
- 必须允许在 https 或者 localhost(127.0.0.1)下,也不能运行在浏览器的无痕模式下
浏览器支持情况
当前主流浏览器都已经支持,实时查看
相关接口介绍
SW API 主要包含四个接口:
- ServiceWorkerContainer:用于注册、注销 SW 线程的容器
- ServiceWorkerRegistration:对 SW 注册实例的引用,可用于注册同步消息、推送消息、通知等
- ServiceWorker:提供了对 SW 线程的引用,可用于获取线程信息和向线程发送消息。状态包含以下几种:
parsed
installing
installed(Waiting)
activating
activated
redundant
- ServiceWorkerGlobalScope:代表一个SW的全局执行上下文,使用 self 标识。包含很多属性和事件,后续很多事件的操作都是对这个接口的操作。
ServiceWorker生命周期
为了方便理解,可以从两个角度来看待SW的生命周期:脚本自身的生命周期 和 SW线程的生命周期。
脚本的生命周期
我们在上面介绍相关接口的时候,提到 ServiceWorker 包含6个状态,这其实就是脚本从注册到销毁的整个生命周期。
- 解析(Parsed):当
ServiceWorkerContainer.register
执行成功后,注册的 Service Worker 文件解析完成 - 安装(Installing):注册完成后,SW线程会转入 installing 状态,每个SW 知会调用一次安装事件。
ServiceWorkerGlobalScope.oninstall
事件会被触发,可以再这个事件中做一些静态资源的缓存等操作。在这个阶段,可以通过event.waitUtil()
的 Promise 来告诉浏览器什么时候安装完成。 - 已安装(Installed):
ServiceWorkerGlobalScope.oninstall
中处理完成后,状态即为 installed。此时新的 SW 线程处于等待状态,可以手动调用self.skipWaiting
或者重新打开网页来进行激活。 - 激活中(activating):激活中状态下会触发
ServiceWorkerGlobalScope.onactivate
事件,可以再这个事件里处理一些旧版本的资源删除操作。在此状态下手动调用self.clients.claim()
,相关页面会立刻被新的 SW 线程控制,并触发ServiceWorkerContainer.oncontrollerchange
事件 - 已激活(activated):
ServiceWorkerGlobalScope.onactivate
事件中的处理逻辑完成后,更新状态为已激活 - 废弃(redundant):安装失败、激活失败会导致当前注册的 SW 线程废弃。
线程的生命周期
伴随着脚本的注册实例化,会实例化出一个SW线程,这个线程是属于事件驱动的,空闲时关闭线程进行休眠,fetch/sync/push等事件触发时开启线程进行工作。
使用
举个栗子
navigator.serviceWorker // ServiceWorkerContainer 对象: window环境下用于注册、注销 Service Worker 线程的容器
.register('sw.js', {
scope: '.',
updateViaCache: 'none'
})
.then((swReg) => {
console.log('sw.js 注册成功');
swReg.onupdatefound = () => {
const workerInstalling = swReg.installing; // 返回 ServiceWorker 对象, 对 SW 线程的引用,可用于获取线程信息和向线程发送消息。
console.log(workerInstalling);
if (!workerInstalling) {
return;
}
// 监听状态
workerInstalling.onstatechange = () => {
console.log(`====> worker 状态 ${workerInstalling.state}`);
};
};
})
.catch((e) => {
console.log('sw.js 注册失败', e);
});
document.getElementById('unReg').onclick = () => {
navigator.serviceWorker
.getRegistration()
.then((swReg) => {
if (!swReg) {
return Promise.resolve(true);
}
return swReg.unregister();
})
.then((ret) => {
if (ret) {
console.log('卸载成功');
}
});
};
const CACHE_NAME = 'pwa'; // 定义缓存名称
// SW安装成功后开始缓存所需的资源
self.addEventListener('install', (e) => {
console.log('监听 install 事件:', e);
// self.skipWaiting(); // 跳过等待
e.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
cache.addAll([
// 在 SW 安装时缓存相关资源
'images/face.jpg',
'custom404.html',
'/',
'index.html'
]);
})
);
});
// 激活阶段,说明上一个SW已经失效,可以在这里删除上个SW的缓存
self.addEventListener('activate', (e) => {
console.log('监听 active 事件:', e);
clients.claim(); // 立即受控
});
// 拦截代理所有指定的请求,并进行对应的操作
self.addEventListener('fetch', (e) => {
console.log('监听 fetch 事件:', e);
return e.respondWith(
fetch(e.request)
.then((res) => {
if (e.request.mode == 'navigate' && res.status == 404) {
return fetch('custom404.html');
}
return res;
})
.catch(() => {
return caches.open(CACHE_NAME).then((cache) => {
return cache.match(e.request).then((res) => {
if (res) {
return res;
}
return cache.match('custom404.html');
});
});
})
);
});
调试
-
可以通过 Application 的页签查看注册的 SW
-
可以通过NetWork查看部分文件 from Service Worker
-
通过 Cache Storage 查看缓存
-
断点调试,注意非 main 线程
常见问题
页面受控指什么?
指某个页面被注册的SW线程控制,即监听这个页面所有的动作,比如fetch、push等事件
注册 SW 时传入的 Scope 怎么理解?
即SW 注册的作用域,默认是与脚本网址相对的 ./
。SW会监听和代理所有作用域下的请求。
首次注册激活的 SW 为什么需要再次加载来生效?
主要是首次注册之后不会立即受控这个网页,需要二次加载。
此时可以通过调用clients.claim()
让SW立马获得该网页的控制权。
// sw.js
self.addEventListener('activate', event => {
event.waitUntil(clients.claim());
console.log('Now ready to handle fetches!');
});