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.addEventListener("install", function(event) { console.log('WORKER: install event in progress.');
event.waitUntil( caches
.open(version + 'fundamentals') .then(function(cache) {
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.');
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) {
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
待页面缓存完之后,再做个小实验,关掉 Wifi, 随便访问一个该 Service Worker 作用域下的其他页面,比如 https://helkyle.github.io/service-worker-practice/step-2/tttt.html,(注意需要离线访问)。