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)