Service Worker 简介

  • 丰富的离线体验
  • 定期的后台同步
  • 推送通知

以上功能通常需要原生应用程序的支持, Service Worker 提供这些功能所依赖的技术基础。


什么是 Service Worker

Service Worker 是浏览器在后台运行的脚本,它独立于网页。通常一个网页的打开,从输入一个 url 开始, dns 解析等等一系列操作,然后最终渲染界面,用户可以看到前端界面,但一旦处于离线状态,用户再次打开该网页就会访问失败,这种体验还是比较差的。

为了解决这类问题,我们想到的是能不能将一些静态资源等进行缓存,这样就算用户在弱网或者无网的情况下,还是能继续看到上次缓存的内容,比如博客、图片等。 Service Worker 的出现,让我们可以支持离线体验,让开发能够全面控制这一体验。

Service Worker 出现前,存在能够在网络上为用户提供离线体验的另一个 API,称为 AppCache),但存在许多问题,而在设计 Service Worker 时已经避免这些问题。

关于使用 Service Worker 需要注意的几点:

  1. Service Worker 是一个 JavaScript Worker ),无法直接访问页面的 DOMService Worker 通过响应 postMessage 接口发送的消息来与其控制的页面通信,页面可以在必要时对 DOM 进行操作;
  2. Service Worker是一个可编程网络代理,允许你控制如何处理来自页面的网络请求;
  3. Service Worker在不用时会被终止,并在下次需要时重启,因此,不能依赖 Service Workeronfetchonmessage 处理程序中的全局状态。 如果存在你需要持续保存并在重启后加以重用的信息, Service Worker 可以访问 IndexedDB API )。
  4. Service Worker 广泛使用了 promise ,所以需要熟悉 promise

Service Worker 的生命周期

Service Worker 的生命周期完全独立于网页。

为网站安装 Service Worker ,需要现在页面的 JavaScript 进行注册,注册后将会通知浏览器在后台启动 Service Worker 安装步骤。

安装过程,通常需要缓存某些静态资源,当所有需要缓存的文件已成功缓存,那代表 Service Worker 安装完毕。如果存在任何文件下载失败或者缓存失败,那就会安装失败, Service Worker 无法激活,发生这种情况,它下次会再次尝试。

安装成功后,进入激活步骤,这是管理旧缓存的好方式,后续部分会介绍。激活之后, Service Worker 将会对其作用域内的所有页面进行控制,不过,首次注册该 Service Worker 的页面需要再次加载才会受其控制。当 Service Worker 对页面进行控制后,一般会处于以下两种状态之一:

  1. Terminated,终止以节省内存;
  2. Fetch / Message,处理 fetch 和消息事件

以下是 Service Worker 初始化安装时的简化生命周期:


先决条件 [需要满足什么条件才能使用]

  1. 浏览器支持。现在基本不部分主流的浏览器已经支持使用 Service Worker ,所有浏览器的支持情况可以参考该链接:https://jakearchibald.github.io/isserviceworkerready/
  2. 需要 使用 HTTPS ,开发过程中可以通过 [localhost](http://localhost) 使用 Service Worker ,但如果需要在网站上部署 Service Worker ,需要在服务器上设置 HTTPS

注册 Service Worker

在安装 Service Worker 前,需要在页面中对它进行注册,然后启动安装,这一点上述步骤也提过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function registerServiceWorker() {
if ('serviceWorker' in navigator) {
var isSupportEle = document.getElementById('is-support');
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then(function (registration) {
updateServiceWorker(registration);
console.log('success: ', registration.scope);
isSupportEle.innerHTML = 'Good, your browser supports serviceWorker.';
})
.catch(function (err) {
console.log('failed: ', err);
isSupportEle.innerHTML = "Sorry, your browser doesn't support serviceWorker.";
});
});
}
}

通过上面代码可以分析,首先要去判断该浏览器是否支持 serviceWorker ,在页面加载后注册位于根目录 sw.js 文件,从而注册 Service Worker。每次页面加载后,都会调用 register() ,浏览器将会判断 Service Worker 是否已注册并做出相应的处理。以上代码中 sw.js 文件位于根目录,所以它的作用域将是整个来源,即 Service Worker 将接收此网站上所有事项的 fetch 事件。如果我们在 example/sw.js 处注册 Service Worker 文件,则 Service Worker 将只能看到网址以 /example/ 开头(即 /example/page1//example/page2/)页面的 fetch 事件。

完成以上操作后,可以通过 chrome://inspect/#service-workers 检查 Service Worker 是否已启用。

可以通过 chrome://serviceworker-internals/ 查看所有 Service Worker 详情。


安装 Service Worker

install 定义回调,并缓存需要的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var CACHE_NAME = 'my-first-cache';
var urlToCache = [
'/',
'/index.css',
'/index.html',
'/index.js'
];
this.addEventListener('install', function (event) {
event.waitUntil(
caches.open('my-first-cache').then(function (cache) {
console.log('Opened cache', cache);
return cache.addAll(urlToCache);
})
);
});

缓存和返回请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request)
.then(function (response) {
// 如果存在该资源,直接返回缓存的 response
console.log({ response });
if (response) {
return response;
}
// 否则请求数据
console.log({ request: event.request });
return fetch(event.request);
}
)
);
});

上述代码不难理解,如果存在有缓存数据,那就直接返回数据,不需要重新 fetch 请求,否则需要走网络请求获取对应数据。在离线情况下,比如已经缓存下来的文件(如图片等)可以正常展示,那没被缓存的文件会直接请求失败,所以可以在有网情况下请求到数据后,并对数据进行相应的缓存。

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
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request)
.then(function (response) {
// Cache hit - return response
console.log({ response });
if (response) {
return response;
}
// IMPORTANT:Clone the request. A request is a stream and
// can only be consumed once. Since we are consuming this
// once by cache and once by the browser for fetch, we need
// to clone the response.
var fetchRequest = event.request.clone();
console.log({ request: event.request });
return fetch(fetchRequest).then(function (response) {
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function (cache) {
cache.put(event.request, responseToCache);
});

return response;
});
}
)
);
});

/*
为啥需要克隆,该响应是数据流, 因此主体只能使用一次。
由于我们想要返回能被浏览器使用的响应,并将其传递到缓存以供使用,因此需要克隆一份副本。
我们将一份发送给浏览器,另一份则保留在缓存.
*/

更新 Service Worker

Service Worker 文件与其当前所用文件(如 sw.js)存在字节差异,则会被视为新 Service Worker 。用户进入该网站后,新的 sw.js 文件将启动,且触发 install 事件,但是旧的 sw.js 仍然会控制着当前页面,所以新的 sw.js 将进入 waiting 状态。当关闭该标签界面后,旧的 sw.js 终止,新的 sw.js 获得控制权,再次打开该界面,将会触发 activate 事件进行激活,可以查看上面的简化生命周期图。

出现在 activate 回调中的一个常见任务就是缓存管理,比如清除旧的缓存数据等,然后缓存新的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
self.addEventListener('activate', function (event) {
console.log("activate");
event.waitUntil(
Promise.all([
// 清理旧版本
caches.keys().then(function (cacheList) {
console.log("当前缓存列表:", cacheList);
return Promise.all(
cacheList.map(function (cacheName) {
if (cacheName === 'my-first-cache') {
console.log("清除当前缓存列表:", cacheList);
return caches.delete(cacheName);
}
})
);
})
]),
// 缓存新版本
caches.open('my-second-cache').then(function (cache) {
return cache.addAll(cacheNames);
})
);
});

参考链接:

https://developers.google.com/web/fundamentals/primers/service-workers/

demo:

https://github.com/LCINA/service-worker-example