[譯] Service workers:PWA背後的英雄

白吟靈發表於2019-02-01

原文地址:medium.freecodecamp.org/service-wor…
作者:Flavio Copes
摘要:這篇文章簡述service worker作為PWA核心技術如何實現資源快取和訊息推送的功能,還幫助讀者理解service worker的生命週期。

Service worker是漸進式網路應用(Progressive Web Apps)的核心。它們幫助我們實現原本是原生app才有資源快取和訊息推送兩大特性。

Service worker是你的網頁與網路間的代理,它能夠攔截和快取來往的網路請求。這可以幫助你的應用創造一個離線環境下也能良好訪問的使用者體驗。

首先介紹一下web worker的概念。它是一個與指定網頁相關聯的JS檔案,獨立與主執行緒執行在一個特定的上下文環境中,這樣就不會為了計算資料去犧牲UI的效能,從而避免了阻塞的情況。而service worker是一種特殊的web worker。

而正是由於它是一個子執行緒,所以無法操作DOM。同樣也無法訪問Local Storage API和XHR API。它只能通過Channel Messaging API和主執行緒通訊。

Service Worker能夠與下面幾個API合作:

  • Promises
  • Fetch API
  • Cache API

只有在HTTPS協議下的網頁裡它們才會起作用。(不過不包括本地的網路請求,因為它們不需要保持安全連線。這樣也方便我們除錯。)

後臺程式

Service worker能夠獨立於與它關聯的應用程式執行,並且在這些程式處於非活躍狀態下仍可以接收訊息。

讓我來舉幾個場景:

  • app處於後臺非活躍狀態下執行;
  • app被關閉;
  • 呈現你網頁的瀏覽器被關閉;

那麼service worker將不受影響地繼續工作。

Service worker的有用之處在於:

  • 它們可以當作緩衝層,處理網路請求和快取離線所需的資源;
  • 它們可以用來推送訊息。

Service worker只在需要的時候執行,其他情況下都會停止工作。

支援離線

對於傳統網頁,離線情況下的使用者體驗非常糟糕。如果使用者沒有聯網,移動端的web應用一般是直接停止工作。反觀原生應用,會展示給使用者一些友好的提示資訊。

下面這張圖是Chrome瀏覽器中離線網頁顯示的內容,顯然這並不算是一個友好的提示資訊:

chrome離線

也許唯一不錯的地方是你可以通過點選恐龍來免費玩一個遊戲,不過相信你很快就會變得不耐煩了。

無聊的遊戲

不久之前,HTML5標準下的AppCache可以讓web應用快取離線資源,但是它缺乏靈活性並且有一些令人困惑的行為,這也說明它無法勝任支援離線這項工作。

而現在,service worker成了離線快取的新標準。

那麼,它實現哪些快取呢?

安裝時的預快取

像圖片、CSS檔案和JS檔案都會在app的使用過程中重複用到。這些資源可以在app開啟的第一時間快取好。

這也是所謂的APP殼架構( App Shell architecture)的基礎。

快取網路請求

我們使用Fetch API可以對伺服器返回的響應報文進行編輯,根據伺服器是否可達來決定是否使用快取中的響應報文代替。

生命週期

一個service worker在啟動前經歷了三步:

  • 註冊(Registration)
  • 安裝(Installation)
  • 啟用(Activation)

註冊

註冊階段是通知瀏覽器service worker的存在,並且在後臺開始安裝。

下面是寫在worker.js中註冊一個service worker的程式碼:

if (`serviceWorker` in navigator) { 
  window.addEventListener(`load`, () => {   
    navigator.serviceWorker.register(`/worker.js`) 
    .then((registration) => { 
      console.log(`Service Worker registration completed with scope: `, registration.scope) 
    }, (err) => { 
      console.log(`Service Worker registration failed`, err)
    })
  })
} else { 
  console.log(`Service Workers not supported`) 
}
複製程式碼

無論這段程式碼被呼叫多少次,瀏覽器始終只會在service worker之前沒有註冊過或是需要更新的情況下進入註冊階段。

Scope

register函式需要一個scope引數來指明你的web應用被該service worker管理的檔案所在路徑。

這個引數的預設值是所有檔案以及service worker檔案父級目錄下的所有子資料夾。所以如果你把service worker檔案放在根目錄下,它會管理整個web應用。而如果在某個子資料夾中,它只會管理該路徑能夠訪問的網頁。

下面這個例子通過指定scope引數為/notifications/目錄來註冊一個service worker。

navigator.serviceWorker.register(`/worker.js`, { 
  scope: `/notifications/` 
})
複製程式碼

結尾的/非常重要,可以避免/notification頁面觸發service worker。而如果寫成下面這樣:

{ scope: `/notifications` }
複製程式碼

那麼service worker就將同樣作用於/notification頁面。

注意:service worker無法控制自身所在目錄以外的檔案。也就是說,如果service worker檔案被放在/notification資料夾下,它無法控制根目錄/或其他不屬於/notification的檔案。

安裝

如果瀏覽器發現一個service worker過期或之前沒有註冊過,那麼它將安裝這個service worker。

self.addEventListener(`install`, (event) => { 
  //... 
});
複製程式碼

這是使用service worker初始化快取,然後利用Cache API來快取APP shell和靜態資源的好時機。

啟用

一旦service worker註冊並安裝成功後 ,我們來到了第三階段:啟用。

這時,service worker能夠在載入新頁面時開始工作。

它不能作用於啟用前已經載入過的頁面,所以只有重啟app或是重新整理已載入頁面兩種方式來使它工作。

self.addEventListener(`activate`, (event) => { 
  //... 
});
複製程式碼

監聽這個事件可以用來清除舊快取或者是刪除新版service worker不需要的舊資源。

更新

你僅僅是修改一位元組的檔案就需要更新一次service worker。它會在註冊的程式碼再次執行時被更新。

更新後的service worker只有在所有頁面都被關閉後才開始代替之前的service worker工作。如果僅僅是重新整理頁面是不會起作用的,因為之前的service worker仍然在執行且沒有被刪除。

這種機制保證了更新不會讓之前在執行的app或網頁崩潰。

Fetch事件

當瀏覽器傳送網路請求時就會觸發Fetch事件。

我們藉此可以在請求傳送時檢查快取中是否已經儲存所需資源。

舉個例子,下面的程式碼使用了Cache API來檢查請求的URL是否已經被快取。如果是,那麼返回快取中的響應資料,否則會傳送請求然後返回響應資料。

self.addEventListener(`fetch`, (event) => {
  event.respondWith( 
    caches.match(event.request) 
      .then((response) => { 
        if (response) { 
          //entry found in cache 
          return response 
        } 
        return fetch(event.request) 
      } 
    ) 
  ) 
})
複製程式碼

Background Sync

當使用者在離線狀態下傳送網路請求時,Background Sync這個API將延遲該請求直到使用者脫離離線狀態。

這保證了使用者在離線狀態下仍然可以使用並操作app,這些離線操作會儲存在佇列中,以便在連線網路後向服務端發出響應請求。(是不是比展示一個無休止的loading圖示要好多了?)

navigator.serviceWorker.ready.then((swRegistration) => { 
    //註冊一個事件event1
  return swRegistration.sync.register(`event1`) 
});
複製程式碼

下面的程式碼是在service worker中監聽這個事件:

self.addEventListener(`sync`, (event) => { 
  if (event.tag == `event1`) { 
    event.waitUntil(doSomething()) 
  } 
})
複製程式碼

doSomething()返回一個promise。如果返回的promise被拒,另一個同步事件被自動地開始重試操作,直到返回一個成功狀態的promise。

這也使得app能夠在聯網時立刻更新伺服器發來的資料。

訊息推送

Service worker使得web應用可以像原生一樣推送訊息給使用者。

事實上,推送和訊息通知是兩個不同的概念,它們組合而成的技術才是我們熟悉的訊息推送。推送機制使得伺服器能夠向service worker傳送資訊,然後service worker將資訊展示給使用者才是訊息通知。

由於service worker可以在app關閉後繼續執行,所以它們能夠一直監聽推送事件。然後它們可以傳送訊息通知,或者是更新app的狀態。

推送事件會在後端通過瀏覽器推送服務後啟動,比如說Firebase的推送服務。

下面的程式碼演示了web worker如何監聽push事件:

self.addEventListener(`push`, (event) => { 
  console.log(`Received a push event`, event) 
  const options = { 
    title: `I got a message for you!`, 
    body: `Here is the body of the message`, 
    icon: `/img/icon-192x192.png`, 
    tag: `tag-for-this-notification`, 
  } 
  event.waitUntil( 
    self.registration.showNotification(title, options) 
  ) 
})
複製程式碼

關於console.log

如果你的程式碼中包含console.log或是其他類似的控制檯輸出語句,務必開啟Chrome開發工具中的Preserve log功能。

否則的話,因為service worker在網頁載入前就開始執行,而此時控制檯會被清空,所以你無法看到任何日誌輸出。

作者總結

非常感謝閱讀本篇教程,事實上,關於PWA還有很多要學習的知識。如果您有什麼見解歡迎在下面評論。

譯者注:主流瀏覽器開始逐漸支援service worker,以後PWA是否會真的與原生平分秋色呢?未來如何,我想現在多瞭解一點PWA的知識總不會壞事。
專案地址:github.com/WhiteYin/tr…

相關文章