Service Worker 入門 - PWA 強依賴於 Service Worker


https://www.w3ctech.com/topic/866javascript


原文:http://www.html5rocks.com/en/tutorials/service-worker/introduction/css

原生App擁有Web應用一般所不具有的富離線體驗,定時的靜默更新,消息通知推送等功能。而新的Service workers標準讓在Web App上擁有這些功能成爲可能。html

Service Worker 是什麼?

一個 service worker 是一段運行在瀏覽器後臺進程裏的腳本,它獨立於當前頁面,提供了那些不須要與web頁面交互的功能在網頁背後悄悄執行的能力。在未來,基於它能夠實現消息推送,靜默更新以及地理圍欄等服務,可是目前它首先要具有的功能是攔截和處理網絡請求,包括可編程的響應緩存管理。html5

爲何說這個API是一個很是棒的API呢?由於它使得開發者能夠支持很是好的離線體驗,它給予開發者徹底控制離線數據的能力。java

在service worker提出以前,另一個提供開發者離線體驗的API叫作App Cache。然而App Cache有些侷限性,例如它能夠很容易地解決單頁應用的問題,可是在多頁應用上會很麻煩,而Service workers的出現正是爲了解決App Cache的痛點。git

下面詳細說一下service worker有哪些須要注意的地方:es6

  • 它是JavaScript Worker,因此它不能直接操做DOM。可是service worker能夠經過postMessage與頁面之間通訊,把消息通知給頁面,若是須要的話,讓頁面本身去操做DOM。
  • Service worker是一個可編程的網絡代理,容許開發者控制頁面上處理的網絡請求。
  • 在不被使用的時候,它會本身終止,而當它再次被用到的時候,會被從新激活,因此你不能依賴於service worker的onfecth和onmessage的處理函數中的全局狀態。若是你想要保存一些持久化的信息,你能夠在service worker裏使用IndexedDB API。
  • Service worker大量使用promise,因此若是你不瞭解什麼是promise,那你須要先閱讀這篇文章。

Service Worker的生命週期

Service worker擁有一個徹底獨立於Web頁面的生命週期。github

要讓一個service worker在你的網站上生效,你須要先在你的網頁中註冊它。註冊一個service worker以後,瀏覽器會在後臺默默啓動一個service worker的安裝過程。web

在安裝過程當中,瀏覽器會加載並緩存一些靜態資源。若是全部的文件被緩存成功,service worker就安裝成功了。若是有任何文件加載或緩存失敗,那麼安裝過程就會失敗,service worker就不能被激活(也即沒能安裝成功)。若是發生這樣的問題,別擔憂,它會在下次再嘗試安裝。chrome

當安裝完成後,service worker的下一步是激活,在這一階段,你還能夠升級一個service worker的版本,具體內容咱們會在後面講到。

在激活以後,service worker將接管全部在本身管轄域範圍內的頁面,可是若是一個頁面是剛剛註冊了service worker,那麼它這一次不會被接管,到下一次加載頁面的時候,service worker纔會生效。

當service worker接管了頁面以後,它可能有兩種狀態:要麼被終止以節省內存,要麼會處理fetch和message事件,這兩個事件分別產生於一個網絡請求出現或者頁面上發送了一個消息。

下圖是一個簡化了的service worker初次安裝的生命週期:

lifecycle

在咱們開始寫碼以前

從這個項目地址拿到chaches polyfill。

這個polyfill支持CacheStorate.match,Cache.add和Cache.addAll,而如今Chrome M40實現的Cache API尚未支持這些方法。

dist/serviceworker-cache-polyfill.js放到你的網站中,在service worker中經過importScripts加載進來。被service worker加載的腳本文件會被自動緩存。

importScripts('serviceworker-cache-polyfill.js');

須要HTTPS

在開發階段,你能夠經過localhost使用service worker,可是一旦上線,就須要你的server支持HTTPS。

你能夠經過service worker劫持鏈接,僞造和過濾響應,很是逆天。即便你能夠約束本身不幹壞事,也會有人想幹壞事。因此爲了防止別人使壞,你只能在HTTPS的網頁上註冊service workers,這樣咱們才能夠防止加載service worker的時候不被壞人篡改。(由於service worker權限很大,因此要防止它自己被壞人篡改利用——譯者注)

Github Pages正好是HTTPS的,因此它是一個理想的自然實驗田。

若是你想要讓你的server支持HTTPS,你須要爲你的server得到一個TLS證書。不一樣的server安裝方法不一樣,閱讀幫助文檔並經過Mozilla's SSL config generator瞭解最佳實踐。

使用Service Worker

如今咱們有了polyfill,而且搞定了HTTPS,讓咱們看看究竟怎麼用service worker。

如何註冊和安裝service worker

要安裝service worker,你須要在你的頁面上註冊它。這個步驟告訴瀏覽器你的service worker腳本在哪裏。

if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').then(function(registration) { // Registration was successful console.log('ServiceWorker registration successful with scope: ', registration.scope); }).catch(function(err) { // registration failed :( console.log('ServiceWorker registration failed: ', err); }); }

上面的代碼檢查service worker API是否可用,若是可用,service worker /sw.js 被註冊。

若是這個service worker已經被註冊過,瀏覽器會自動忽略上面的代碼。

有一個須要特別說明的是service worker文件的路徑,你必定注意到了在這個例子中,service worker文件被放在這個域的根目錄下,這意味着service worker和網站同源。換句話說,這個service work將會收到這個域下的全部fetch事件。若是我將service worker文件註冊爲/example/sw.js,那麼,service worker只能收到/example/路徑下的fetch事件(例如: /example/page1/, /example/page2/)。

如今你能夠到 chrome://inspect/#service-workers 檢查service worker是否對你的網站啓用了。

sw-chrome-inspect

當service worker初版被實現的時候,你也能夠在chrome://serviceworker-internals中查看,它頗有用,經過它能夠最直觀地熟悉service worker的生命週期,不過這個功能很快就會被移到chrome://inspect/#service-workers中。

你會發現這個功能可以很方便地在一個模擬窗口中測試你的service worker,這樣你能夠關閉和從新打開它,而不會影響到你的新窗口。任何建立在模擬窗口中的註冊服務和緩存在窗口被關閉時都將消失。

Service Worker的安裝步驟

在頁面上完成註冊步驟以後,讓咱們把注意力轉到service worker的腳本里來,在這裏面,咱們要完成它的安裝步驟。

在最基本的例子中,你須要爲install事件定義一個callback,並決定哪些文件你想要緩存。

// The files we want to cache var urlsToCache = [ '/', '/styles/main.css', '/script/main.js' ]; // Set the callback for the install step self.addEventListener('install', function(event) { // Perform install steps });

在咱們的install callback中,咱們須要執行如下步驟:

  1. 開啓一個緩存
  2. 緩存咱們的文件
  3. 決定是否全部的資源是否要被緩存
var CACHE_NAME = 'my-site-cache-v1'; var urlsToCache = [ '/', '/styles/main.css', '/script/main.js' ]; self.addEventListener('install', function(event) { // Perform install steps event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { console.log('Opened cache'); return cache.addAll(urlsToCache); }) ); });

上面的代碼中,咱們經過caches.open打開咱們指定的cache文件名,而後咱們調用cache.addAll並傳入咱們的文件數組。這是經過一連串promise(caches.open 和 cache.addAll)完成的。event.waitUntil拿到一個promise並使用它來得到安裝耗費的時間以及是否安裝成功。

若是全部的文件都被緩存成功了,那麼service worker就安裝成功了。若是任何一個文件下載失敗,那麼安裝步驟就會失敗。這個方式容許你依賴於你本身指定的全部資源,可是這意味着你須要很是謹慎地決定哪些文件須要在安裝步驟中被緩存。指定了太多的文件的話,就會增長安裝失敗率。

上面只是一個簡單的例子,你能夠在install事件中執行其餘操做或者甚至忽略install事件。

怎樣緩存和返回Request

你已經安裝了service worker,你如今能夠返回你緩存的請求了。

當service worker被安裝成功而且用戶瀏覽了另外一個頁面或者刷新了當前的頁面,service worker將開始接收到fetch事件。下面是一個例子:

self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { // Cache hit - return response if (response) { return response; } return fetch(event.request); } ) ); });

上面的代碼裏咱們定義了fetch事件,在event.respondWith裏,咱們傳入了一個由caches.match產生的promise.caches.match 查找request中被service worker緩存命中的response。

若是咱們有一個命中的response,咱們返回被緩存的值,不然咱們返回一個實時從網絡請求fetch的結果。這是一個很是簡單的例子,使用全部在install步驟下被緩存的資源。

若是咱們想要增量地緩存新的請求,咱們能夠經過處理fetch請求的response而且添加它們到緩存中來實現,例如:

self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { // Cache hit - return response if (response) { return response; } // IMPORTANT: Clone the request. A request is a stream and // can only be consumed once. Since we are consuming this // once by cache and once by the browser for fetch, we need // to clone the response var fetchRequest = event.request.clone(); return fetch(fetchRequest).then( function(response) { // Check if we received a valid response if(!response || response.status !== 200 || response.type !== 'basic') { return response; } // IMPORTANT: Clone the response. A response is a stream // and because we want the browser to consume the response // as well as the cache consuming the response, we need // to clone it so we have 2 stream. var responseToCache = response.clone(); caches.open(CACHE_NAME) .then(function(cache) { cache.put(event.request, responseToCache); }); return response; } ); }) ); });

代碼裏咱們所作事情包括:

  1. 添加一個callback到fetch請求的 .then 方法中
  2. 一旦咱們得到了一個response,咱們進行以下的檢查:

    1. 確保response是有效的
    2. 檢查response的狀態是不是200
    3. 保證response的類型是basic,這表示請求自己是同源的,非同源(即跨域)的請求也不能被緩存。
  3. 若是咱們經過了檢查,clone這個請求。這麼作的緣由是若是response是一個Stream,那麼它的body只能被讀取一次,因此咱們得將它克隆出來,一份發給瀏覽器,一份發給緩存。

如何更新一個Service Worker

你的service worker總有須要更新的那一天。當那一天到來的時候,你須要按照以下步驟來更新:

  1. 更新你的service worker的JavaScript文件

    1. 當用戶瀏覽你的網站,瀏覽器嘗試在後臺下載service worker的腳本文件。只要服務器上的文件和本地文件有一個字節不一樣,它們就被斷定爲須要更新。
  2. 更新後的service worker將開始運做,install event被從新觸發。

  3. 在這個時間節點上,當前頁面生效的依然是老版本的service worker,新的servicer worker將進入"waiting"狀態。
  4. 當前頁面被關閉以後,老的service worker進程被殺死,新的servicer worker正式生效。
  5. 一旦新的service worker生效,它的activate事件被觸發。

代碼更新後,一般須要在activate的callback中執行一個管理cache的操做。由於你會須要清除掉以前舊的數據。咱們在activate而不是install的時候執行這個操做是由於若是咱們在install的時候立馬執行它,那麼依然在運行的舊版本的數據就壞了。

以前咱們只使用了一個緩存,叫作my-site-cache-v1,其實咱們也可使用多個緩存的,例如一個給頁面使用,一個給blog的內容提交使用。這意味着,在install步驟裏,咱們能夠建立兩個緩存,pages-cache-v1blog-posts-cache-v1,在activite步驟裏,咱們能夠刪除舊的my-site-cache-v1

下面的代碼可以循環全部的緩存,刪除掉全部不在白名單中的緩存。

self.addEventListener('activate', function(event) { var cacheWhitelist = ['pages-cache-v1', 'blog-posts-cache-v1']; event.waitUntil( caches.keys().then(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName); } }) ); }) ); });

處理邊界和填坑

這一節內容比較新,有不少待定細節。但願這一節很快就不須要講了(由於標準會處理這些問題——譯者注),可是如今,這些內容仍是應該被提一下。

若是安裝失敗了,沒有很優雅的方式得到通知

若是一個worker被註冊了,可是沒有出如今chrome://inspect/#service-workerschrome://serviceworker-internals,那麼極可能由於異常而安裝失敗了,或者是產生了一個被拒絕的的promise給event.waitUtil。

要解決這類問題,首先到 chrome://serviceworker-internals檢查。打開開發者工具窗口準備調試,而後在你的install event代碼中添加debugger;語句。這樣,經過斷點調試你更容易找到問題。

fetch()目前僅支持Service Workers

fetch立刻支持在頁面上使用了,可是目前的Chrome實現,它還只支持service worker。cache API也即將在頁面上被支持,可是目前爲止,cache也還只能在service worker中用。

fetch()的默認參數

當你使用fetch,缺省地,請求不會帶上cookies等憑據,要想帶上的話,須要:

fetch(url, { credentials: 'include' })

這樣設計是有理由的,它比XHR的在同源下默認發送憑據,但跨域時丟棄憑據的規則要來得好。fetch的行爲更像其餘的CORS請求,例如<img crossorigin>,它默認不發送cookies,除非你指定了<img crossorigin="use-credentials">.

Non-CORS默認不支持

默認狀況下,從第三方URL跨域獲得一個資源將會失敗,除非對方支持了CORS。你能夠添加一個non-CORS選項到Request去避免失敗。代價是這麼作會返回一個「不透明」的response,意味着你不能得知這個請求到底是成功了仍是失敗了。

cache.addAll(urlsToPrefetch.map(function(urlToPrefetch) { return new Request(urlToPrefetch, { mode: 'no-cors' }); })).then(function() { console.log('All resources have been fetched and cached.'); });

fetch()不遵循30x重定向規範

不幸,重定向在fetch()中不會被觸發,這是當前版本的bug;

處理響應式圖片

img的srcset屬性或者<picture>標籤會根據狀況從瀏覽器或者網絡上選擇最合適尺寸的圖片。

在service worker中,你想要在install步驟緩存一個圖片,你有如下幾種選擇:

  1. 安裝全部的<picture>元素或者將被請求的srcset屬性。
  2. 安裝單一的low-res版本圖片
  3. 安裝單一的high-res版本圖片

比較好的方案是2或3,由於若是把全部的圖片都給下載下來存着有點浪費內存。

假設你將low-res版本在install的時候緩存了,而後在頁面加載的時候你想要嘗試從網絡上下載high-res的版本,可是若是high-res版本下載失敗的話,就依然用low-res版本。這個想法很好也值得去作,可是有一個問題:

若是咱們有下面兩種圖片:

Screen Density Width Height
1x 400 400
2x 800 800

HTML代碼以下:

<img src="image-src.png" srcset="image-src.png 1x, image-2x.png 2x" />

若是咱們在一個2x的顯示模式下,瀏覽器會下載image-2x.png,若是咱們離線,你能夠讀取以前緩存並返回image-src.png替代,若是以前它已經被緩存過。儘管如此,因爲如今的模式是2x,瀏覽器會把400X400的圖片顯示成200X200,要避免這個問題就要在圖片的樣式上設置寬高。

<img src="image-src.png" srcset="image-src.png 1x, image-2x.png 2x" style="width:400px; height: 400px;" />

img

<picture>標籤狀況更復雜一些,難度取決於你是如何建立和使用的,可是能夠經過與srcset相似的思路去解決。

改變URL Hash的Bug

在M40版本中存在一個bug,它會讓頁面在改變hash的時候致使service worker中止工做。

你能夠在這裏找到更多相關的信息: https://code.google.com/p/chromium/issues/detail?id=433708

更多內容

這裏有一些相關的文檔能夠參考:https://jakearchibald.github.io/isserviceworkerready/resources.html

得到幫助

若是你遇到麻煩,請在Stackoverflow上發帖詢問,使用'service-worker'標籤,以便於咱們及時跟進和儘量幫助你解決問題。

相關文章
相關標籤/搜索