Service Worker入門

發表於2015-03-26

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

Service Worker 是什麼?

一個 service worker 是一段執行在瀏覽器後臺程式裡的指令碼,它獨立於當前頁面,提供了那些不需要與web頁面互動的功能在網頁背後悄悄執行的能力。在將來,基於它可以實現訊息推送,靜默更新以及地理圍欄等服務,但是目前它首先要具備的功能是攔截和處理網路請求,包括可程式設計的響應快取管理。

為什麼說這個API是一個非常棒的API呢?因為它使得開發者可以支援非常好的離線體驗,它給予開發者完全控制離線資料的能力。

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

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

  • 它是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頁面的生命週期。

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

在安裝過程中,瀏覽器會載入並快取一些靜態資源。如果所有的檔案被快取成功,service worker就安裝成功了。如果有任何檔案載入或快取失敗,那麼安裝過程就會失敗,service worker就不能被啟用(也即沒能安裝成功)。如果發生這樣的問題,別擔心,它會在下次再嘗試安裝。

當安裝完成後,service worker的下一步是啟用,在這一階段,你還可以升級一個service worker的版本,具體內容我們會在後面講到。

在啟用之後,service worker將接管所有在自己管轄域範圍內的頁面,但是如果一個頁面是剛剛註冊了service worker,那麼它這一次不會被接管,到下一次載入頁面的時候,service worker才會生效。

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

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

在我們開始寫碼之前

從這個專案地址拿到chaches polyfill。

這個polyfill支援CacheStorate.match,Cache.add和Cache.addAll,而現在Chrome M40實現的Cache API還沒有支援這些方法。

將dist/serviceworker-cache-polyfill.js放到你的網站中,在service worker中通過importScripts載入進來。被service worker載入的指令碼檔案會被自動快取。

需要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指令碼在哪裡。

上面的程式碼檢查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是否對你的網站啟用了。

當service worker第一版被實現的時候,你也可以在chrome://serviceworker-internals中檢視,它很有用,通過它可以最直觀地熟悉service worker的生命週期,不過這個功能很快就會被移到chrome://inspect/#service-workers中。

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

Service Worker的安裝步驟

在頁面上完成註冊步驟之後,讓我們把注意力轉到service worker的指令碼里來,在這裡面,我們要完成它的安裝步驟。

在最基本的例子中,你需要為install事件定義一個callback,並決定哪些檔案你想要快取。

在我們的install callback中,我們需要執行以下步驟:

  1. 開啟一個快取
  2. 快取我們的檔案
  3. 決定是否所有的資源是否要被快取

上面的程式碼中,我們通過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事件。下面是一個例子:

上面的程式碼裡我們定義了fetch事件,在event.respondWith裡,我們傳入了一個由caches.match產生的promise.caches.match 查詢request中被service worker快取命中的response。

如果我們有一個命中的response,我們返回被快取的值,否則我們返回一個實時從網路請求fetch的結果。這是一個非常簡單的例子,使用所有在install步驟下被快取的資源。

如果我們想要增量地快取新的請求,我們可以通過處理fetch請求的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-v1和blog-posts-cache-v1,在activite步驟裡,我們可以刪除舊的my-site-cache-v1。

下面的程式碼能夠迴圈所有的快取,刪除掉所有不在白名單中的快取。

處理邊界和填坑

這一節內容比較新,有很多待定細節。希望這一節很快就不需要講了(因為標準會處理這些問題——譯者注),但是現在,這些內容還是應該被提一下。

如果安裝失敗了,沒有很優雅的方式獲得通知

如果一個worker被註冊了,但是沒有出現在chrome://inspect/#service-workers或chrome://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等憑據,要想帶上的話,需要:

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

Non-CORS預設不支援

預設情況下,從第三方URL跨域得到一個資源將會失敗,除非對方支援了CORS。你可以新增一個non-CORS選項到Request去避免失敗。代價是這麼做會返回一個“不透明”的response,意味著你不能得知這個請求究竟是成功了還是失敗了。

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程式碼如下:

如果我們在一個2x的顯示模式下,瀏覽器會下載image-2x.png,如果我們離線,你可以讀取之前快取並返回image-src.png替代,如果之前它已經被快取過。儘管如此,由於現在的模式是2x,瀏覽器會把400X400的圖片顯示成200X200,要避免這個問題就要在圖片的樣式上設定寬高。

<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’標籤,以便於我們及時跟進和儘可能幫助你解決問題。

相關文章