[譯] JavaScript 是如何工作的:Service Worker 的生命週期與使用場景

LeviDing發表於2018-05-31

這是專門探索 JavaScript 及其構建元件的系列的第八個。在識別和描述核心元素的過程中,我們也分享了一些我們在構建 SessionStack 時的最佳實踐。SessionStack 是一個強大且效能卓越的 JavaScript 應用程式,可以向你實時顯示使用者在 Web 應用程式中遇到技術問題或使用者體驗問題時的具體情況。

如果你沒看過之前的章節,你可以在這裡看到:

  1. [譯] JavaScript 是如何工作的:對引擎、執行時、呼叫堆疊的概述
  2. [譯] JavaScript 是如何工作的:在 V8 引擎裡 5 個優化程式碼的技巧
  3. [譯] JavaScript 是如何工作的:記憶體管理 + 處理常見的4種記憶體洩漏
  4. [譯] JavaScript 是如何工作的: 事件迴圈和非同步程式設計的崛起 + 5個如何更好的使用 async/await 編碼的技巧
  5. [譯] JavaScript 是如何工作的:深入剖析 WebSockets 和擁有 SSE 技術 的 HTTP/2,以及如何在二者中做出正確的選擇
  6. [譯] JavaScript 是如何工作的:與 WebAssembly 一較高下 + 為何 WebAssembly 在某些情況下比 JavaScript 更為適用
  7. [譯] JavaScript 是如何工作的:Web Worker 的內部構造以及 5 種你應當使用它的場景

你可能已經知道了漸進式 Web 應用只會越來越受歡迎,因為它們旨在使 Web 應用的使用者體驗更加流暢,提供原生應用體驗而不是瀏覽器的外觀和感覺。

構建漸進式 Web 應用程式的主要要求之一是使其在網路和載入方面非常可靠 —— 它應該可用於不確定或不可用的網路條件。

在這篇文章中,我們將深入探討 Service Worker:它們如何運作以及開發者應該關心什麼。最後,我們還列出了開發者應該利用的 Service Worker 的一些獨特優勢,並在 SessionStack 中分享我們自己團隊的經驗。

概覽

如果你想了解 Service Worker 的一切內容,你應該從閱讀我們部落格上,關於 Web Workers 的文章開始。

基本上,Service Worker 是 Web Worker 的一個型別,更具體地說,它像 Shared Worker

  • Service Worker 在其自己的全域性上下文中執行
  • 它沒有繫結到特定的網頁
  • 它不能訪問到 DOM

Service Worker API 令人興奮的主要原因之一是它可以讓你的網路應用程式支援離線體驗,從而使開發人員能夠完全控制流程。

Service Worker 的生命週期

Service Worker 的生命週期與你的網頁是完全分開的,它由以下幾個階段組成:

  • 下載
  • 安裝
  • 啟用

下載

這是瀏覽器下載包含 Service Worker 的 .js 檔案的時候。

安裝

要為你的Web應用程式安裝 Service Worker,你必須先註冊它,你可以在 JavaScript 程式碼中進行註冊。當註冊 Service Worker 時,它會提示瀏覽器在後臺啟動 Service Worker 安裝步驟。

通過註冊 Service Worker,你可以告訴瀏覽器你的 Service Worker 的 JavaScript 檔案在哪裡。我們來看下面的程式碼:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/sw.js').then(function(registration) {
      // Registration was successful
      console.log('ServiceWorker registration successful');
    }, function(err) {
      // Registration failed
      console.log('ServiceWorker registration failed: ', err);
    });
  });
}
複製程式碼

該程式碼檢查當前環境中是否支援Service Worker API。如果是,則 /sw.js 這個 Service Worker 就被註冊了。

每次頁面載入時都可以呼叫 register() 方法,瀏覽器會判斷 Service Worker 是否已經註冊,並且會正確處理。

register() 方法的一個重要細節是 Service Worker 檔案的位置。在這種情況下,你可以看到 Service Worker 檔案位於域的根目錄。這意味著 Service Worker 的範圍將是整個網站。換句話說,這個 Service Worker 將會收到這個域的所有內容的 fetch 事件(我們將在後面討論)。如果我們在 /example/sw.js 註冊 Service Worker 檔案,那麼 Service Worker 只會看到以 /example/ 開頭的頁面的 fetch 事件(例如 /example/page1//example/page2/)。

在安裝階段,最好載入和快取一些靜態資源。資源成功快取後,Service Worker 安裝完成。如果沒有成功(載入失敗)—— Service Worker 將重試。一旦安裝成功,靜態資源就已經在快取中了。

如果註冊需要在載入事件之後發生,這就解答了你“註冊是否需要在載入事件之後發生”的疑惑。這不是必要的,但絕對是推薦的。

為什麼這樣呢?讓我們考慮使用者第一次訪問網路應用程式的情況。當前還沒有 Service Worker,瀏覽器無法事先知道最終是否會安裝 Service Worker。如果安裝了 Service Worker,則瀏覽器需要為這個額外的執行緒承擔額外的 CPU 和記憶體開銷,否則瀏覽器會將計算資源用於渲染網頁上。

最重要的是,如果在頁面上安裝一個 Service Worker,就可能會有延遲載入和渲染的風險 —— 而不是儘快讓你的使用者可以使用該頁面。

請注意,這種情況僅僅是在第一次訪問頁面時很重要。後續頁面訪問不受 Service Worker 安裝的影響。一旦在第一次訪問頁面時啟用 Service Worker,它可以處理載入、快取事件,以便隨後訪問 Web 應用程式。這一切都是有意義的,因為它需要準備好處理受限的的網路連線。

啟用

安裝 Service Worker 之後,下一步是將它啟用。這一步是管理之前快取內容的好機會。

一旦啟用,Service Worker 將開始控制所有屬於其範圍的頁面。一個有趣的事實是:首次註冊 Service Worker 的頁面將不會被控制,直到該頁面再次被載入。一旦 Service Worker 處於控制之下,它將處於以下狀態之一:

  • 它將處理當頁面發出網路請求或訊息時發生的 fetch 和 message 事件
  • 它將被終止以節省記憶體

以下是生命週期的示意圖:

[譯] JavaScript 是如何工作的:Service Worker 的生命週期與使用場景

處理 Service Worker 內部的裝置

在頁面處理註冊過程之後,讓我們看看 Service Worker 指令碼中發生了什麼,它通過向 Service Worker 例項新增事件監聽來處理 install 事件。

這些是處理 install 事件時需要採取的步驟:

  • 開啟一個快取
  • 快取我們的檔案
  • 確認是否快取了所有必需的資源

在 Service Worker 內部的一個簡單裝置可能會看起來像這樣:

var CACHE_NAME = 'my-web-app-cache';
var urlsToCache = [
  '/',
  '/styles/main.css',
  '/scripts/app.js',
  '/scripts/lib.js'
];

self.addEventListener('install', function(event) {
  // event.waitUntil takes a promise to know how
  // long the installation takes, and whether it 
  // succeeded or not.
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});
複製程式碼

如果所有檔案都成功快取,則將安裝 Service Worker。如果任何一個檔案都無法下載,則安裝步驟將失敗。所以要小心你放在那裡的檔案。

處理 install 事件完全是可選的,你可以避免它,在這種情況下,你不需要執行這裡的任何步驟。

執行時快取請求

這部分是貨真價實的內容。你將看到如何攔截請求並返回建立的快取(以及建立新快取)的位置。

在安裝 Service Worker 後,使用者進入了新的頁面,或者重新整理當前頁面後,Service Worker 將收到 fetch 事件。 下面是一個演示如何返回快取資源,或傳送新請求後快取結果的示例:

self.addEventListener('fetch', function(event) {
  event.respondWith(
    // This method looks at the request and
    // finds any cached results from any of the
    // caches that the Service Worker has created.
    caches.match(event.request)
      .then(function(response) {
        // If a cache is hit, we can return thre response.
        if (response) {
          return response;
        }

        // 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 request.
        var fetchRequest = event.request.clone();
        
        // A cache hasn't been hit so we need to perform a fetch,
        // which makes a network request and returns the data if
        // anything can be retrieved from the network.
        return fetch(fetchRequest).then(
          function(response) {
            // Check if we received a valid response
            if(!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }

            // Cloning the response since it's a stream as well.
            // 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 two streams.
            var responseToCache = response.clone();

            caches.open(CACHE_NAME)
              .then(function(cache) {
                // Add the request to the cache for future queries.
                cache.put(event.request, responseToCache);
              });

            return response;
          }
        );
      })
    );
});
複製程式碼

概括地說這其中發生了什麼:

  • event.respondWith() 將決定我們如何迴應 fetch 事件。我們傳遞來自 caches.match() 的一個 promise,它檢查請求並查詢是否有已經建立的快取結果。
  • 如果在快取中,響應內容就被恢復了。
  • 否則,將會執行 fetch
  • 檢查狀態碼是不是 200,同時檢查響應型別是 basic,表明響應來自我們最初的請求。在這種情況下,不會快取對第三方資源的請求。
  • 響應被快取下來

請求和響應必須被複制,因為它們是。流的 body 只能被使用一次。並且由於我們想消費它們,而瀏覽器也必須消費它們,因此我們便需要克隆它們。

上傳 Service Worker

當有一個使用者訪問你的 web 應用,瀏覽器將嘗試重新下載包含了 Service Worker 的 .js 檔案。這將在後臺執行。

如果與當前 Service Worker 的檔案相比,新下載的 Service Worker 檔案中存在哪怕一個位元組的差異,則瀏覽器將會認為有變更,且必須啟動新的 Service Worker。

新的 Service Worker 將啟動並且安裝事件將被移除。然而,在這一點上,舊的 Service Worker 仍在控制你的 web 應用的頁面,這意味著新的 Service Worker 將進入 waiting 狀態。

一旦你的 web 應用程式當前開啟的頁面都被關掉,舊的 Service Worker 就會被瀏覽器幹掉,西南裝的 Service Worker 將完全掌控應用。這就是它啟用的事件將被幹掉的時候。

為什麼需要這些?為了避免兩個版本的 Web 應用程式同時執行在不同的 tab 上 —— 這在網路上實際上非常常見,並且可能會產生非常糟糕的錯誤(例如,在瀏覽器中本地儲存資料時,會有不同的 schema)。

從快取中刪除資料

activate 回撥中最常見的步驟是快取管理。我們應該現在做這件事,因為如果你在安裝步驟中清除了所有舊快取,舊的 Service Worker 將突然停止提供快取中的檔案。

這裡提供了一個如何從快取中刪除一些不在白名單中的檔案的例子(在本例中,有 page-1page-2 兩個實體):

self.addEventListener('activate', function(event) {

  var cacheWhitelist = ['page-1', 'page-2'];

  event.waitUntil(
    // Retrieving all the keys from the cache.
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        // Looping through all the cached files.
        cacheNames.map(function(cacheName) {
          // If the file in the cache is not in the whitelist
          // it should be deleted.
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});
複製程式碼

HTTPS 要求

在構建 Web 應用程式時,開發者可以通過本地主機使用 Service Worker,但是一旦將其部署到生產環境中,則需要準備好 HTTPS(這是擁有 HTTPS 的最後一個原因)。

使用 Service Worker,你可以劫持連線並偽造響應。若不使用 HTTPS,你的 web 應用程式變得容易發生中間人攻擊

為了更安全,你需要在通過 HTTPS 提供的頁面上註冊 Service Worker,以便知道瀏覽器接收的 Service Worker 在通過網路傳輸時未被修改。

瀏覽器支援

瀏覽器對 Service Worker 的支援正在變得越來越好:

[譯] JavaScript 是如何工作的:Service Worker 的生命週期與使用場景

你可以在這個網站上追蹤所有瀏覽器的適配程式 —— jakearchibald.github.io/isservicewo…

Service Workers 正在開啟美好特性的大門

Service Worker 提供的一些獨一無二的特性:

  • 推送通知 —— 允許使用者選擇從 web 應用程式及時獲取通知。
  • 後臺同步 —— 在使用者網路不穩定時,允許開發者推遲操作,直到使用者具有穩定的連線。這樣,就可以確保無論使用者想要傳送什麼資料,都可以發出去。
  • 定時同步(未來支援)—— 提供管理定期後臺同步功能的 API。
  • 地理圍欄(未來支援)—— 開發者可以自定義引數,建立感興趣區域的地理圍欄。當裝置跨越地理圍欄時,Web 應用程式會收到通知,這可以讓開發者根據使用者的地理位置提供有效服務。

這些將在本系列未來的部落格文章中詳細討論。

我們一直致力於使 SessionStack 的使用者體驗儘可能流暢,優化頁面載入和響應時間。

當你在 SessionStack(或實時觀看)中重播使用者會話時,SessionStack 前端將不斷從我們的伺服器提取資料,以便無縫地建立緩衝區,像你剛才,同本文中一樣的經歷。為了提供一些上下文 —— 一旦你將 SessionStack 的庫整合到 Web 應用程式中,它將不斷收集諸如 DOM 更改,使用者互動,網路請求,未處理的異常和除錯訊息等資料。

當會話正在重播或實時流式傳輸時,SessionStack 會提供所有資料,讓開發者可以在視覺和技術上檢視使用者在自己的瀏覽器中體驗到的所有內容。這一切都需要快速實現,因為我們不想讓使用者等待。

由於資料是由我們的前端提取的,因此這是一個很好的地方,可以利用 Service Worker 來重新載入我們的播放器,以及重新傳輸資料流等情況。處理較慢的網路連線也非常重要。

如果你想嘗試 SessionStack,這有個免費的計劃

[譯] JavaScript 是如何工作的:Service Worker 的生命週期與使用場景

參考資料


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章