Service Worker挺有意思的,前段时间看了相关的资料,本身动手调了调demo,记录一下学习过程。文中不只会介绍Service Worker的使用,对fetch
、push
、cache
等Service Worker配套的API都会涉及,毕竟Service Worker与这些API配合使用才能发挥出真正的威力css
Chrome对Service Worker的开发者支持较好,Dev tools里能够简单的调试,Firefox还未提供调试用的工具,可是对API的支持更好。建议开发和测试的话,在Chrome上进行html
文中有把Service Worker简写SW,不要以为奇怪~前端
Service workers essentially act as proxy servers that sit between web applications, and the browser and network (when available). They are intended to (amongst other things) enable the creation of effective offline experiences, intercepting network requests and taking appropriate action based on whether the network is available and updated assets reside on the server. They will also allow access to push notifications and background sync APIs.node
一个ServiceWorker从被加载到生效,有这么几个生命周期:git
Installing 这个阶段能够监听install
事件,并使用event.waitUtil
来作Install完成前的准备,好比cache一些数据之类的,另外还有self.skipWaiting
在serviceworker被跳过install过程时触发github
> for example by creating a cache using the built in storage API, and placing assets inside it that you'll want for running your app offline.
Installed 加载完成,等待被激活,也就是新的serverworker替换旧的web
Activating 也可使用event.waitUtil
事件,和self.clients.clainm
ajax
> If there is an **existing** service worker available, the new version is installed in the background, but not yet **activated** — at this point it is called the worker in waiting. **It is only activated when there are no longer any pages loaded that are still using the old service worker**. As soon as there are no more such pages still loaded, the new service worker activates (becoming the active worker). **这说明serviceWorker被替换是有条件的,即便有新的serviceworker,也得等旧的没有被使用才能替换**。最明显的体现是,刷新页面并不必定能加载到新闻serviceworker
Activated 文章上的解释是the service worker can now handle functional eventschrome
Redundant 被替换,即被销毁npm
fetch
是新的Ajax
标准接口,已经有不少浏览器原生支持了,用来代替繁琐的XMLHttpRequest
和jQuery.ajax
再好不过了。对于还未支持的浏览器,能够用isomorphic-fetch polyfill。
fetch的API很简洁,这篇文档讲的很清晰。下面记录一下以前被我忽略的2个API
Response
写个fetch的栗子
fetch('/style.css') // 这里的response,就是一个Response实例 .then(response => response.text()) .then(text => { console.log(text); });
Response的API,列几个比较经常使用的:
Response.clone()
Creates a clone of a Response object. 这个常常用在cache直接缓存返回结果的场景
Body.blob()
这里写的是Body
,其实调用接口仍是用response
,这里取Blob
数据的数据流。MDN是这么说的:
> Response implements Body, so it also has the following methods available to it:
Body.json()
Body.text()
Body.formData()
Takes a Response stream and reads it to completion. It returns a promise that resolves with a FormData object.
Request
应该不会单独new
出来使用,由于不少Request相关的参数,在Request的实例中都是只读的,而真正能够配置Request属性的地方,是fetch
的第二个参数:
// fetch的第一个参数是URI路径,第二个参数则是生成Request的配置, // 而若是直接传给fetch一个request对象,其实只有URI是可配置的, // 由于其余的配置如headers都是readonly,不能直接从Request处配置 let request = new Request('./style.css'); request.method = 'POST'; // Uncaught TypeError: Cannot set property method of #<Request> which has only a getter fetch(request).then(response => response.text()) .then(text => { console.log(text); });
Cache是Service Worker衍生出来的API,配合Service Worker实现对资源请求的缓存。
有意思的是cache并不直接缓存字符串(想一想localstorage),而是直接缓存资源请求(css、js、html等)。cache也是key-value
形式,通常来讲key就是request,value就是response
caches.open(cacheName)
打开一个cache,caches
是global对象,返回一个带有cache返回值的Promise
cache.keys()
遍历cache中全部键,获得value的集合
caches.open('v1').then(cache => { // responses为value的数组 cache.keys().then(responses => { responses.forEach((res, index) => { console.log(res); }); }); });
cache.match(Request|url)
在cache中匹配传入的request,返回Promise
;cache.matchAll
只有第一个参数与match不一样,须要一个request的数组,固然返回的结果也是response的数组
cache.add(Request|url)
并非单纯的add,由于传入的是request或者url,在cache.add内部会自动去调用fetch取回request的请求结果,而后才是把response存入cache;cache.addAll
相似,一般在sw
install的时候用cache.addAll
把全部须要缓存的文件都请求一遍
cache.put(Request, Response)
这个至关于cache.add
的第二步,即fetch到response后存入cache
cache.delete(Request|url)
删除缓存
Note: Cache.put, Cache.add, and Cache.addAll only allow GET requests to be stored in the cache.
As of Chrome 46, the Cache API will only store requests from secure origins, meaning those served over HTTPS.
Service Worker是worker
的一种,跟Web Worker
同样,不在浏览器的主线程里运行,于是和Web Worker
同样,有跟主线程通讯的能力。
window.postMessage(message, target[, transfer])
这个API以前也用过,在iframe
之间通讯(onmessage
接收信息)。简单记下参数:
message 能够是字符串,或者是JSON序列化后的字符串,在接收端保存在event.data
里
target 须要传输的URL域,具体看API文档
transfer 用mdn的说法,是一个transferable
的对象,好比MessagePort
、ArrayBuffer
另外说明一点,postMessage的调用者是被push数据一方的引用,即我要向sw post数据,就须要sw的引用
注意,上面的postMessage是在document中使用的。在sw的context里使用略有不一样:没有target参数。具体看这个API文档
先看个栗子:
// main thread if (serviceWorker) { // 建立信道 var channel = new MessageChannel(); // port1留给本身 channel.port1.onmessage = e => { console.log('main thread receive message...'); console.log(e); } // port2给对方 serviceWorker.postMessage('hello world!', [channel.port2]); serviceWorker.addEventListener('statechange', function (e) { // logState(e.target.state); }); } // sw self.addEventListener('message', ev => { console.log('sw receive message..'); console.log(ev); // 取main thread传来的port2 ev.ports[0].postMessage('Hi, hello too'); });
在sw里须要传递MessagePort
,这个是由MessageChannel
生成的通讯的两端,在己方的一端为channel.port1
,使用channel.port1.onmessage
便可监遵从另外一端返回的信息。而须要在postMessage里传的是channel.port2
,给另外一端postMessage使用。在sw端经过监听message
事件就能够监听到主线程的postMessage,在message
的event.ports[0]
里便可找到主线程传过来的port,以后就能够用event.ports[0].postMessage
来向主线程发送信息了。
这里用到了MessageChannel
。这是一个很简单的APi,完成在两个不一样的cotext中通讯的功能。
在上面已经提到了,MessageChannel在一端建立,而后用channel.port1.onmesssage
监听另外一端post的message,而将channel.port2
经过postMessage的第二个参数(transfer)传给另外一端,让另外一端也能用MessagePort
作一样的操做。
须要注意的是channel
的port1和port2的区别:port1是new
MessageChannel的一方须要使用的,port2是另外一方使用的
若是说fetch
事件是sw拦截客户端请求的能力,那么push
事件就是sw拦截服务端“请求”的能力。这里的“请求”打了引号,你能够把Push当成WebSocket
,也就是服务端能够主动推送消息到客户端。
与WebSocket不一样的是,服务端的消息在到达客户端以前会被sw拦截,要不要给浏览器,给什么,能够在sw里控制,这就是Push API的做用。
MDN上有个push-api-demo,是个简易聊天器。具体搭建的方法在这个repo上有,再也不赘述。由于有些Push API只有Firefox Nightly版本支持,因此demo也只能跑在这个浏览器上,我还没下好,没跑起来,等明天看吧~
记几个Push API:
ServiceWorkerRegistration.showNotification(title, options)
这个能够理解成alert
的升级版,网页版的wechat的通知就是这个。
Notification.requestPermission()
提示用户是否容许浏览器通知
PushManager
Push API的核心对象,注册Push API从这里开始,放在 ServiceWorkerRegistration
里
PushManager.subscribe
返回一个带有PushSubscription的Promise,经过PushSubscription对象才能生成公钥(PushSubscription.getKey()
,这个方法只有firefox有,这也是chrome不能执行的缘由),获取endpoint
PushManager.getSubscription()
获取当前注册好的PushSubscription对象
atob()
和btob()
意外捡到两个API,用于浏览器编码、解码base64
仍是看个栗子:
// 浏览器端的main.js, 代码来自push-api-demo navigator.serviceWorker.ready.then(function(reg) { // 注册push reg.pushManager.subscribe({userVisibleOnly: true}) // 获得PushSubscription对象 .then(function(subscription) { // The subscription was successful isPushEnabled = true; subBtn.textContent = 'Unsubscribe from Push Messaging'; subBtn.disabled = false; // Update status to subscribe current user on server, and to let // other users know this user has subscribed var endpoint = subscription.endpoint; // 生成公钥 var key = subscription.getKey('p256dh'); // 这一步是个ajax,把公钥和endpoint传给server,由于是https因此不怕公钥泄露 updateStatus(endpoint,key,'subscribe'); }) }); // 服务端 server.js,接收并存下公钥、endpoint ... } else if(obj.statusType === 'subscribe') { // bodyArray里是ajax传上来的key和endpoint fs.appendFile('endpoint.txt', bodyArray + '\n', function (err) { if (err) throw err; fs.readFile("endpoint.txt", function (err, buffer) { var string = buffer.toString(); var array = string.split('\n'); for(i = 0; i < (array.length-1); i++) { var subscriber = array[i].split(','); webPush.sendNotification(subscriber[2], 200, obj.key, JSON.stringify({ action: 'subscribe', name: subscriber[1] })); }; }); }); } ... // 仍是服务端 server.js,推送信息到service worker if(obj.statusType === 'chatMsg') { // 取出客户端传来的公钥和endpoint fs.readFile("endpoint.txt", function (err, buffer) { var string = buffer.toString(); var array = string.split('\n'); for(i = 0; i < (array.length-1); i++) { var subscriber = array[i].split(','); // 这里用了web-push这个node的库,sendNotification里有key,说明对信息加密了 webPush.sendNotification(subscriber[2], 200, obj.key, JSON.stringify({ action: 'chatMsg', name: obj.name, msg: obj.msg })); }; }); }
进入页面后先注册ServiceWorker
,而后subscribe PushManager
,把公钥和endpoint传给Server端(ajax)保存下来,便于以后的通讯(都是加密的)
而后建立一个MessageChannel
与ServiceWorker
通讯
准备工做到这里就作完了。Client与Server端的通讯仍是ajax,聊天室嘛就是传用户发送的消息。ServiceWorker
去监听push
事件接住Server端push来的数据,在这个demo里都是Server端接到Client的ajax请求的响应,固然也能够又Server端主动发起一个push。当同时有两个以上的Client都与这个Server通讯,那么这几个Client能看到全部与Server的消息,这才是聊天室嘛,不过要验证至少须要两台机器
一个HTTPS服务,加了Web-Push
这个module,这里面确定有用公钥和endpoint给push信息加密的功能。webPush.sendNotification
这个API能把Server端的push消息广播到全部的Client端
Web-push这个库还得看看
MDN上有一个完整的使用Service Worker的Demo,一个简易的聊天室,能够本身玩玩儿。
这个demo的思路是:install
时fetch
须要缓存的文件,用cache.addAll
缓存到cacheStorage
里。在fetch
事件触发时,先cache.match
这些缓存,若存在则直接返回,若不存在则用fetch
抓这个request,而后在cache.put
进缓存。
Chrome has chrome://inspect/#service-workers, which shows current service worker activity and storage on a device, and chrome://serviceworker-internals, which shows more detail and allows you to start/stop/debug the worker process. In the future they will have throttling/offline modes to simulate bad or non-existent connections, which will be a really good thing.
最新的Chrome
版本,Dev tools
的Resource选项卡里已经添加了Service Workers
,能够查看当前页面是否有使用Service Worker,和它当前的生命周期
service worker
很顽强,一个新的service worker
install以后不能直接active
,须要等到全部使用这个service worker的页面都卸载以后能替换,不利于调试。今天试出来一个100%能卸载的方法:
chrome://inspect/#service-workers
中terminate相应的service worker
chrome://serviceworker-internals/
中unregister相应的service worker
关闭调试页面,再打开
调试service worker能够在chrome://inspect/#service-workers
里inspect相应的Devtool
若是在缓存中找不到对应的资源,把拦截的请求发回原来的流程
If a match wasn’t found in the cache, you could tell the browser to simply fetch the default network request for that resource, to get the new resource from the network if it is available:
fetch(event.request)
复制response的返回结果,下次直接从cache里取出来用
this.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request).catch(function() { return fetch(event.request).then(function(response) { return caches.open('v1').then(function(cache) { cache.put(event.request, response.clone()); return response; }); }); }) );
cache未命中且网络不可用的状况,这里Promise
用了两次catch
,第一次还报错的话第二次catch才会执行
this.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request).catch(function() { return fetch(event.request).then(function(response) { return caches.open('v1').then(function(cache) { cache.put(event.request, response.clone()); return response; }); }); }).catch(function() { return caches.match('/sw-test/gallery/myLittleVader.jpg'); }) );
activated
以前清除不须要的缓存
this.addEventListener('activate', function(event) { var cacheWhitelist = ['v2']; event.waitUntil( caches.keys().then(function(keyList) { return Promise.all(keyList.map(function(key) { if (cacheWhitelist.indexOf(key) === -1) { return caches.delete(key); } })); }) ); });
伪造Response
// service-worker.js self.addEventListener('fetch', ev => { var reqUrl = ev.request.url; console.log('hijack request: ' + reqUrl); console.log(ev.request); // 如果text.css的请求被拦截,返回伪造信息 if (reqUrl.indexOf('test.css') > -1) { console.log('hijack text.css'); ev.respondWith( new Response('hahah', { headers: {'Content-Type': 'text/css'} }) ); } // 继续请求 else { ev.respondWith(fetch(ev.request)); } });
// app.js window.onload = () => { // 请求test.css fetch('/service-worker-demo/test.css') .then(response => { return response.text(); }) .then(text => { console.log('text.css: ' + text); // 在service worker install时返回真实的文本,在sw active时返回hahah,即伪造的文本 return text; });
## 未解之谜 1. `serviceworker.register(url, { scope: 'xxx' })`,这里的`scope`彷佛没用。在这个scope上级的静态资源请求也会被`fetch`拦截,在`HTTPS`上也无效,能够看看[这个demo](https://ydss.github.io/service-worker-demo/) ## Reference - [Using Service Workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers) - [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) - [Using the Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API/Using_the_Push_API) - [PushManager](https://developer.mozilla.org/en-US/docs/Web/API/PushManager) - [Notifications API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API) - [Service Worker MDN demo](https://github.com/mdn/sw-test/) - [当前端也拥有 Server 的能力](http://www.barretlee.com/blog/2016/02/16/when-fe-has-the-power-of-server)