Service Worker初體驗
在2014年,W3C公布了service worker的草案,service worker提供了很多新的能力,使得web app擁有與native app相同的離線體驗、消息推送體驗。service worker是一段腳本,與web worker一樣,也是在后臺運行。作為一個獨立的線程,運行環境與普通腳本不同,所以不能直接參與web交互行為。native app可以做到離線使用、消息推送、后臺自動更新,service worker的出現是正是為了使得web app也可以具有類似的能力。
service worker可以:
- 后臺消息傳遞
-
網絡代理,轉發請求,偽造響應
-
離線緩存
-
消息推送
-
… …
本文以資源緩存為例,說明一下service worker是如何工作的。
生命周期
先來看一下一個service worker的運行周期
上圖是service worker生命周期,出處 http://www.html5rocks.com/en/tutorials/service-worker/introduction/
圖中可以看到,一個service worker要經歷以下過程:
1. 安裝
2. 激活,激活成功之后,打開chrome://inspect/#service-workers可以查看到當前運行的service worker
3. 監聽fetch和message事件,下面兩種事件會進行簡要描述
4. 銷毀,是否銷毀由瀏覽器決定,如果一個service worker長期不使用或者機器內存有限,則可能會銷毀這個worker
fetch事件
在頁面發起http請求時,service worker可以通過fetch事件攔截請求,并且給出自己的響應。w3c提供了一個新的fetch api,用于取代XMLHttpRequest,與XMLHttpRequest最大不同有兩點:
1. fetch()方法返回的是Promise對象,通過then方法進行連續調用,減少嵌套。ES6的Promise在成為標準之后,會越來越方便開發人員。
2. 提供了Request、Response對象,如果做過后端開發,對Request、Response應該比較熟悉。前端要發起請求可以通過url發起,也可以使用Request對象發起,而且Request可以復用。但是Response用在哪里呢?在service worker出現之前,前端確實不會自己給自己發消息,但是有了service worker,就可以在攔截請求之后根據需要發回自己的響應,對頁面而言,這個普通的請求結果并沒有區別,這是Response的一處應用。
下面是在 http://www.sitepoint.com/introduction-to-the-fetch-api/ 中,作者利用fetch api通過fliker的公開api獲取圖片的例子,注釋中詳細解釋了每一步的作用:
/* 由于是get請求,直接把參數作為query string傳遞了 */ var URL = 'https://api.flickr.com/services/rest/?method=flickr.photos.search&api_key=your_api_key&format=json&nojsoncallback=1&tags=penguins'; function fetchDemo() { // fetch(url, option)支持兩個參數,option中可以設置header、body、method信息 fetch(URL).then(function(response) { // 通過promise 對象獲得相應內容,并且將響應內容按照json格式轉成對象,json()方法調用之后返回的依然是promise對象 // 也可以把內容轉化成arraybuffer、blob對象 return response.json(); }).then(function(json) { // 渲染頁面 insertPhotos(json); }); } fetchDemo();
fetch api與XMLHttpRequest相比,更加簡潔,并且提供的功能更全面,資源獲取方式比ajax更優雅。兼容性方面:chrome 42開始支持,對于舊瀏覽器,可以通過官方維護的polyfill支持。
message事件
頁面和serviceWorker之間可以通過posetMessage()方法發送消息,發送的消息可以通過message事件接收到。
這是一個雙向的過程,頁面可以發消息給service worker,service worker也可以發送消息給頁面,由于這個特性,可以將service worker作為中間紐帶,使得一個域名或者子域名下的多個頁面可以自由通信。
這里是一個小的頁面之間通信demo https://nzv3tos3n.qnssl.com/message/msg-demo.html
利用service workder緩存文件
下面介紹一個利用service worker緩存離線文件的例子準備index.js,用于注冊service-worker
if (navigator.serviceWorker) { navigator.serviceWorker.register('service-worker.js').then(function(registration) { console.log('service worker 注冊成功'); }).catch(function (err) { console.log('servcie worker 注冊失敗') }); }
在上述代碼中,注冊了service-worker.js作為當前路徑下的service worker。由于service worker的權限很高,所有的代碼都需要是安全可靠的,所以只有https站點才可以使用service worker,當然localhost是一個特例。
注冊完畢,現在開始寫service-worker.js代碼。
根據前面的生命周期圖,在一個新的service worker被注冊以后,首先會觸發install事件,在service-workder.js中,可以通過監聽install事件進行一些初始化工作,或者什么也不做。
因為我們是要緩存離線文件,所以可以在install事件中開始緩存,但是只是將文件加到caches緩存中,真正想讓瀏覽器使用緩存文件需要在fetch事件中攔截
var cacheFiles = [ 'about.js', 'blog.js' ]; self.addEventListener('install', function (evt) { evt.waitUntil( caches.open('my-test-cahce-v1').then(function (cache) { return cache.addAll(cacheFiles); }) ); });
首先定義了需要緩存的文件數組cacheFile,然后在install事件中,緩存這些文件。
evt是一個InstallEvent對象,繼承自ExtendableEvent,其中的waitUntil()方法接收一個promise對象,直到這個promise對象成功resolve之后,才會繼續運行service-worker.js。
caches是一個CacheStorage對象,使用open()方法打開一個緩存,緩存通過名稱進行區分。
獲得cache實例之后,調用addAll()方法緩存文件。
這樣就將文件添加到caches緩存中了,想讓瀏覽器使用緩存,還需要攔截fetch事件
// 緩存圖片 self.addEventListener('fetch', function (evt) { evt.respondWith( caches.match(evt.request).then(function(response) { if (response) { return response; } var request = evt.request.clone(); return fetch(request).then(function (response) { if (!response && response.status !== 200 && !response.headers.get('Content-type').match(/image/)) { return response; } var responseClone = response.clone(); caches.open('my-test-cache-v1').then(function (cache) { cache.put(evt.request, responseClone); }); return response; }); }) ) });
通過監聽fetch事件,service worker可以返回自己的響應。
首先檢緩存中是否已經緩存了這個請求,如果有,就直接返回響應,就減少了一次網絡請求。否則由service workder發起請求,這時的service workder起到了一個中間代理的作用。
service worker請求的過程通過fetch api完成,得到response對象以后進行過濾,查看是否是圖片文件,如果不是,就直接返回請求,不會緩存。
如果是圖片,要先復制一份response,原因是request或者response對象屬于stream,只能使用一次,之后一份存入緩存,另一份發送給頁面。這就是service worker的強大之處:攔截請求,偽造響應。fetch api在這里也起到了很大的作用。
service worker的更新很簡單,只要service-worker.js的文件內容有更新,就會使用新的腳本。但是有一點要注意:舊緩存文件的清除、新文件的緩存要在activate事件中進行,因為可能舊的頁面還在使用之前的緩存文件,清除之后會失去作用。
在初次使用service worker的過程中,也遇到了一些問題,下面是其中兩個
問題1. 運行時間
service worker并不是一直在后臺運行的。在頁面關閉后,瀏覽器可以繼續保持service worker運行,也可以關閉service worker,這取決與瀏覽器自己的行為。所以不要定義一些全局變量,例如下面的代碼(來自 https://jakearchibald.com/2014/service-worker-first-draft/ ):
var hitCounter = 0; this.addEventListener('fetch', function(event) { hitCounter++; event.respondWith( new Response('Hit number ' + hitCounter) ); });
返回的結果可能是沒有規律的:1,2,1,2,1,1,2….,原因是hitCounter并沒有一直存在,如果瀏覽器關閉了它,下次啟動的時候hitCounter就賦值為0了這樣的事情導致調試代碼困難,當你更新一個service worker以后,只有在打開新頁面以后才可能使用新的service worker,在調試過程中經常等上一兩分鐘才會使用新的,比較抓狂。
問題2. 權限太大
當service worker監聽fetch事件以后,對應的請求都會經過service worker。通過chrome的network工具,可以看到此類請求會標注:from service worker。如果service worker中出現了問題,會導致所有請求失敗,包括普通的html文件。所以service worker的代碼質量、容錯性一定要很好才能保證web app正常運行。
參考文章:
1. http://www.html5rocks.com/en/tutorials/service-worker/introduction/
2. http://www.sitepoint.com/introduction-to-the-fetch-api/
3. https://developer.mozilla.org/en-US/docs/Web/API/InstallEvent
4. https://developer.mozilla.org/en-US/docs/Web/API/ExtendableEvent
5. https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage