随着时代的发展,用户对应用体验的要求愈加苛刻,我们虽然可以通过各种优化手段来减少页面加载时间,但当用户处于移动状态,潜在的网络切换很可能导致短暂的离线,如果用户在此时进行事务处理,那么此刻应用的不可用很可能导致用户流失。也许正是由于 Web 应用在离线处理上的弱势,才最终导致其地位在移动时代不如原生应用这一局面。
那又该如何破局,将本机原生应用所具有的离线处理能力植入到 Web 应用中去呢?接下来我们要讨论的 Service Worker 便可解决这一难题。
# Service Worker 与 Web Worker
- 首次看到
Service Worker,我想大家可能会跟我一样都有这东西跟 Web Worker 有什么联系之类的疑问,带着这个疑问让我们来梳理下两者的差异。 Web Worker是现代浏览器提供的一个JavaScript多线程解决方案,我们可以将一些复杂、耗时的运算交给 Web Worker 执行以达到释放主线程的目的;Service Worker则是建立在Web Worker之上,旨在通过请求代理、本地缓存、后台同步等机制来提供离线处理能力。两者的主要异同点如下:
相同点
- 都独立于主线程,以单独线程的形式运行。
- 都不能直接访问并操作
DOM、window对象。 - 都是通过
postMessage接口与主线程进行交互。
不同点
Service Worker内部大部分为基于Promise的异步操作。Service Worker必须运行在HTTPS环境下以避免中间人攻击。Service Worker的生命周期完全独立于网页,且可在不用时被中止、在下次有需要时重启。
# 生命周期
上一节我们对
Service Worker及Web Worker进行了简单对比,下面我们将深入了解 Service Worker 的生命周期。
# 注册
要使用 Service Worker,需通过以下方式在页面中对其进行注册:
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('./sw.js', { scope: './' }).then(function(registration) {
// do domething...
}).catch(function(err) {
// do domething...
});
});
}
示例首先检测
Service Worker是否可用,如果可用,则在页面加载完后注册位于./sw.js的Service Worker。代码非常简单,但需注意以下两点:
- 注册成功仅仅表明指定脚本已成功解析,并不意味着 Service Worker 已经安装或处于激活状态。
register方法中的 scope 参数指定了 Service Worker 可接收 fetch 事件的作用域,比如 scope 的值为 /mobile,那么 Service Worker 便只能接收 path 以/mobile开头的 fetch 事件,默认值为sw.js所在路径。
# 安装
注册完成后,浏览器便会立即尝试安装并进入安装状态,此时将触发
Service Worker的install事件,在该事件中我们经常对静态资源进行缓存处理,比如:
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('sw-cache').then(function(cache) {
return cache.addAll([
'/',
'/index.html',
'/main.css',
'/main.js',
'/image.jpg'
]);
})
);
});
示例中通过 Cache API 对静态资源进行了缓存处理,其中方法
event.waitUntil的参数是一个Promise对象,并且:
- 等待直到参数为
resolve状态时,Service Worker才会进入下一个生命周期。 - 如果最终参数为
reject状态,Service Worker安装失败,我们无需为此做特殊的处理,因为在下次进行注册时,会重新进行安装尝试。
需要注意的是,并不是每次注册成功后都会进入安装状态并触发 Service Worker 的 install 事件,其需要满足以下两个条件中的任意一个:
- 页面中尚未安装
Service Worker。 Service Worker已安装,并且从服务器获取的sw.js文件与本地版本存在差异
# 等待
安装成功后,如果已经存在一个版本的 Service Worker 且有页面正在使用该版本,新版 Service Worker 便会进入等待状态,当 Service Worker 处于该阶段时,由于它必须等正在运行旧版本 Service Worker 的页面全部关闭后才会获得控制权,因此如果我们需要所有页面能够及时得到更新,可在 install 中通过 self.skipWaiting 来强制跳过该阶段:
self.addEventListener('install', function(event) {
self.skipWaiting();
//……
});
# 激活
当满足以下任一条件,Service Worker 便可进入该阶段:
self.skipWaiting方法被调用。- 安装完成后,不存在旧版本的 Service Worker 或无页面使用此版本。
- 等待状态下正在运行旧版本 Service Worker 的页面被全部关闭(页面刷新或切换无法使 Service Worker 从等待进入激活状态,这是由于当页面刷新或切换时,浏览器需要等到新页面渲染完成之后才会销毁旧页面,即新旧两个页面存在共同的交叉时间)。
- 进入该状态后,
activate事件将会被触发,我们常通过订阅该事件对缓存进行更新或删除,比如
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.filter(function(cacheName) {
return cacheName != 'sw-cache';
}).map(function(cacheName) {
return caches.delete(cacheName);
})
);
})
);
});
当 Service Worker 被首次注册时,已打开的页面只有在刷新后才会接受 Service Worker 的控制,如果想要 Service Worker 在激活后尽快掌握这些页面的控制权,可在 activate 中调用
self.clients.claim方法来实现:
self.addEventListener('activate', function(event) {
self.clients.claim()
//……
});
# 已激活
到了这一阶段,便可通过监听
fetch、push、sync、message等事件来为应用提供丰富的离线处理能力。
# 注销
用户可通过点击调试面板中的
unregister(如下图)来注销 Service Worker,但有些时候我们可能需要通过编程的方式来进行注销,其实现代码如下:

const serviceWorker = navigator.serviceWorker;
if (typeof serviceWorker.getRegistrations === 'function') {
serviceWorker.getRegistrations().then(function(registrations) {
registrations.forEach(function(registration) {
registration.unregister();
});
});
} else if (typeof serviceWorker.getRegistration === 'function') {
serviceWorker.getRegistration().then(function(registration) {
registration.unregister();
})
}
需要注意的是,无论通过何种方式注销,本地缓存都不会自动清除,需手动调用 Cache API、IndexedDB API 等其他离线存储 API 进行清理操作。
# 废弃
该阶段表示一个 Service Worker 的生命周期已结束;进入该阶段的条件可为以下任意一个:
- 安装失败。
- 激活失败。
- 用户执行了注销操作。
- 新版本的 Service Worker 替换了它并成为激活状态。
# 状态监听
注册成功后,我们可通过回调中的 registration 参数来获取以下状态的 ServiceWorker 实例:
- 安装:通过
registration.installing获取,如属性值为非空,则表示 Service Worker 正处于安装状态。 - 等待:通过
registration.waiting获取,如果属性值为非空,则表示 Service Worker 正处于等待状态。 - 激活:通过
registration.active获取,如果属性值为非空,则表示 Service Worker 已被激活。
需要注意的是,在 Service Worker 新旧版本切换的时候,会同时存在安装(等待)及激活状态实例,这是因为新的 Service Worker 还没有完全取得所有页面的控制权。
通过以上方式得到 ServiceWorker 实例后,我们可通过监听该实例的 statechange 事件来获得其最新状态,比如:
navigator.serviceWorker.register('./sw.js').then(function(registration) {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', function() {
console.log(newWorker.state);
});
});
同时也可通过
registration的updatefound事件来监听Service Worker的更新,该事件将在registration.installing的值发生变化时触发:
navigator.serviceWorker.register('./sw.js').then(function(registration) {
registration.addEventListener('updatefound', function() {
});
});
如果我们想要在新的 Service Worker 取得页面控制权后执行一些逻辑(比如给予用户提示),可通过订阅
navigator.serviceWorker的controllerchange事件来实现:
navigator.serviceWorker.addEventListener('controllerchange', function() {
});
# 事件
上一节我们对 Service Worker 的生命周期进行了详细说明,下面我们了解下 Service Worker 所支持的常用事件:
install:安装事件,一般对静态资源文件进行缓存处理。activate:激活事件,一般用于更新或删除旧版本的缓存。fetch:接收 Service Worker 作用域下的fetch事件,在该事件中可以做各种缓存代理的事情。sync:后台同步事件,由BackgroundSync API发出。message:由于 Service Worker 以独立线程运行,通过该事件可以实现与主进程的交互。push:响应来自系统的推送消息。notificationclick:推送通知点击事件,一般用来处理通知与用户的交互。
# 总结
本章我们通过与 Web Worker 的对比、生命周期以及常用事件三个方面对 Service Worker 进行了系统学习,这些机制为 Web 应用的离线处理、系统交互提供了可能,然而仅仅拥有机制还远远不够,它还需要其他技术的配合才能真正发挥其威力,因此在接下来的几个章节中,我们将对为其提供底层服务的技术一一进行讲解。