Bootstrap

Service Workers 初体验

Service Workers 初体验

【https://www.w3ctrain.com/2016/09/17/service-workers-note/】

在开始和 Service Workers 打招呼之前,我们先来对比一下 Native App 和 Web 在性能和可及性之间的差异。

差异

差异

为了结合两者的优点,各大厂商推出了各自的解决方案,Hibrid、Weex、Reative Native 等等。
Google 团队推出了 Progressive Web Apps。

Progressive Web Apps 能做什么

  • Notification
  • Add To Home Screen
  • Instant Loading
  • Fast
  • Responsive
  • Secure

Anyways,这是理想状态,在中国难免会遇上障碍,我们还是必须走中国特色 ** 道路。

  • Notification 依赖于 Google Push API,除非你能让用户都翻过 GFW。
  • 国内 Android 自定义程度极高,大部分厂商自定义桌面,自带浏览器也没有 Add To Home Screen 选项。

上面这两个功能都没法用在正式环境上,不过还好对注重用户体验的我们来说,它们并非必要。接下来看 PWA 的核心,也是这篇文章的重点,Service Worker。

初识 Service Worker
  • Service Worker 是运行在浏览器后台的脚本,运行环境与普通的脚本不同,它不能直接参与页面交互。
  • Service Worker 类似于网络代理,可以用来请求转发,伪造响应。
  • Service Worker 可以用来拦截网络请求,处理推送消息以及执行其他任务。
  • Service worker 权限很高,所以需要运行在 HTTPS 上,防止被人从中攻击,Github Page 支持 HTTPS,是块极好的试验田。

网络代理

网络代理

Service Worker 生命周期

生命周期

生命周期

Service Worker 基于事件监听机制,在没有接受到事件时,它几乎不占内存。Service Worker 可接收的事件主要有下面几个:

install 事件通常在 ServiceWorker 首次运行的时候被激活,通常在这个时间点缓存资源,以便于用户在下次访问时能提速。
activate 事件在一次成功的 install 之后被触发。
fetch 事件能截取当前 ServiceWorker 作用域下发出的请求,这时候我们可以拦截请求并作出响应。
message 事件在接收到消息的时候执行。

Register

使用 Service worker 需要现在页面脚本上注册它

1
2
3
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/serviceworker.js');
}

不必担心,如果客户端浏览器不支持 ServiceWorker 也不会报错。
值得一提的是,ServiceWorker 能处理的请求范围是有限的,通常它只能处理子路径下的请求,比方说,/js/serviceworker.js 这个路径只能够拦截 /js/ 域下的请求。我们通常是把 js 文件统一放在静态文件目录下,但是使用 ServiceWorkder,或许你得想办法把 ServiceWorker 放在根目录下。

Install
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
var version = 'v1::';

/*
self 指的是 ServiceWorkerGlobalScope 物件,也就是 service worker
*/
self.addEventListener("install", function(event) {
console.log('WORKER: install event in progress.');
/*
waitUtil 接受一个 Promise 对象,直到 resolve 之后才会继续执行。
*/
event.waitUntil(
caches
/* 缓存通过名字进行索引,使用这个名字,我们可以对缓存进行增删改。
*/
.open(version + 'fundamentals')
.then(function(cache) {
/* 打开缓存之后,指定需要缓存的文件路径,SW 会自动发出 HTTP 请求,并缓存。
这个过程中如果有任意一个文件 请求或缓存失败,那么 SW 不会被安装成功,不会触发 activate 事件。
*/
return cache.addAll([
'/',
'/css/global.css',
'/js/global.js'
]);
})
.then(function() {
console.log('WORKER: install completed');
})
);
});

Activate

Activate 会在 Service Worker 被成功 install 之后执行,这个时间点我们可以删除旧版本的的缓存。

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
self.addEventListener("activate", function(event) {
console.log('WORKER: activate event in progress.');

event.waitUntil(
caches
.keys()
.then(function (keys) {
return Promise.all(
keys
.filter(function (key) {
// 过滤过期缓存
return !key.startsWith(version);
})
.map(function (key) {
/* 删除所有过期缓存
*/
return caches.delete(key);
})
);
})
.then(function() {
console.log('WORKER: activate completed.');
})
);
});

Fetch

Fetch 会在被 Service Worker 控制的页面请求资源的时候触发。它几乎包括所有的请求,ajax,js, css, fonts, image 等等。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
self.addEventListener("fetch", function(event) {
console.log('WORKER: fetch event in progress.');

// 只缓存 GET 请求,其他请求交给后端
if (event.request.method !== 'GET') {
console.log('WORKER: fetch event ignored.', event.request.method, event.request.url);
return;
}
/* 响应
*/
event.respondWith(
caches
/* 匹配请求
*/
.match(event.request)
.then(function(cached) {
var networked = fetch(event.request)
.then(fetchedFromNetwork, unableToResolve)
.catch(unableToResolve);

console.log('WORKER: fetch event', cached ? '(cached)' : '(network)', event.request.url);
return cached || networked;

function fetchedFromNetwork(response) {
/* response 只能用一次,clone 一份用于缓存。
*/
var cacheCopy = response.clone();

console.log('WORKER: fetch response from network.', event.request.url);

caches
.open(version + 'pages')
.then(function add(cache) {
/* 再次打开缓存,将本次请求响应缓存起来。
*/
cache.put(event.request, cacheCopy);
})
.then(function() {
console.log('WORKER: fetch response stored in cache.', event.request.url);
});

return response;
}

function unableToResolve () {
/*
当代码执行到这里,说明请求无论是从缓存还是走网络,都无法得到答复,这个时机,我们可以返回一个相对友好的页面,告诉用户,你可能离线了。
*/

console.log('WORKER: fetch request failed in both cache and network.');

return new Response('<h1>Service Unavailable</h1>', {
status: 503,
statusText: 'Service Unavailable',
headers: new Headers({
'Content-Type': 'text/html'
})
});
}
})
);
});

缓存策略

我们只截取 Get 请求,其他请求交给后端。
例子中,我们尝试返回缓存版本的同时,请求多一次后端,保证缓存版本没有过期。

待续

信息传递

Service Worker 和页面之间通过 PostMessage 进行通信,这种通信是双向的,类似于与 iframe 之间的通信。
做个 Demo 页

index.html

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Service Worker Demo</title>
</head>
<body>
<ul id='messageList'>
</ul>
<input type="text" name="message" value="" id='textInput'>
<button id='sendBtn'>发送</button>
<script>
(function() {
if ('serviceWorker' in navigator) {
var messageList = document.getElementById('messageList');
var textInput = document.getElementById('textInput');
var sendBtn = document.getElementById('sendBtn');

navigator.serviceWorker.register('serviceworker.js');

navigator.serviceWorker.addEventListener('message', function (event) {
messageList.innerHTML = messageList.innerHTML + '<li>' + event.data.message + '</li>';
});

sendBtn.addEventListener('click', function() {
if (textInput.value) {
navigator.serviceWorker.controller.postMessage(textInput.value);
textInput.value = '';
}
})
}
})();
</script>
</body>
</html>

代码里判断浏览器是否支持 Service Worker, 注册 Service Worker 并监听 message 与按钮点击事件。

serviceWorker.js

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
self.addEventListener('message', function(event) {
var promise = self.clients.matchAll()
.then(function(clientList) {
var senderId = event.source ? event.source.id: 'unknown';
if (!event.source) {
console.log('event.source is null; we don\'t know the sender of the ' +
'message');
}
clientList.forEach(function(client) {
if (client.id === senderId) {
return;
}

client.postMessage({
client: senderId,
message: event.data
})
})
})
if (event.waitUntil) {
event.waitUntil(promise);
}
});


self.addEventListener('activate', function(event) {
event.waitUntil(self.clients.claim());
})

serviceWorker.js 代码里同样监听 message 事件,广播消息发送给非传播源。

其他说明
  • Service Worker 权限很高,只能在 HTTPS 环境下运行,localhost 是个例外~
  • Service Worker 中的 Javascript 代码必须是非阻塞的,所以你不应该在 Service Worker 代码中是用 localStorage,因为 localStorage 是阻塞性的。
  • 支持 Service Worker 的浏览器也支持 ES6 箭头函数和模板语法。
  • 打开 chrome://inspect/#service-workers 你可以看到所有运行着的 Service Worker
  • response 只能用一次,需要用多次的话,需要执行 response.clone()
实例演示

访问 https://helkyle.github.io/service-worker-practice/step-2/index.html,Service Worker 会在另外的线程去缓存图片,等图片缓存完之后,再次刷新页面,就能看到它的威力。
From Service Worker

From Service Worker

待页面缓存完之后,再做个小实验,关掉 Wifi, 随便访问一个该 Service Worker 作用域下的其他页面,比如 https://helkyle.github.io/service-worker-practice/step-2/tttt.html,(注意需要离线访问)。


;