聊聊Service Worker

Web Workers

web worker: 爲Web內容在後臺線程中運行腳本提供了一種簡單的方法,線程可以執行任務而不干擾用戶界面,即:運行在後臺的 JavaScript

瀏覽器一般有三類 web Worker:
- Worker:專用的 worker,只能被創建它的 JS 訪問,生命週期到創建它的頁面關閉時結束。
- SharedWorker:共享的 worker,可以被好幾個 JS 訪問,生命週期到關聯的頁面都關閉時結束。
ServiceWorker:一個特殊的 worker,生命週期與頁面無關,關聯頁面未關閉時,它也可以退出,沒有關聯頁面時,它也可以啓動,嗯總之就是一個神奇的worker

對於workers來說,它運行的上下文不同於當前的window對象所在的上下文,在專用worker的情況下,DedicatedWorkerGlobalScope 對象代表了worker的上下文;在共享worker的情況下,SharedWorkerGlobalScope對象代表了共享worker的上下文。

因此,在worker線程中運行的代碼是有一些特殊情況的,比如,不能直接操作DOM以及使用window對象的一些默認屬性和方法。但是在worker裏,還是有很多window對象之下的東西是可以使用的,具體就要查閱文檔了 https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Functions_and_classes_available_to_workers

worker線程和主線程各自都使用postMessage()發送消息和和onmessage事件來響應對方發送的消息,傳遞的信息包含在 Message 這個事件的data屬性內裏,數據傳遞的是副本。

當然一個 worker 可以生成另外的新的 worker,這些 worker 的宿主和它們父頁面的宿主相同。

Service Worker

前兩種worker主要是爲了解決js執行耗時操作時影響UI響應的問題,而之所以說service worker是一種特殊的worker,是因爲它想要把一個web APP變得更像native APP,可以支持離線訪問。在service worker之前,離線緩存使用AppCache來做,從Firefox44起,當使用 AppCache 來提供離線頁面支持時,會提示建議開發者使用 service workers 來實現離線頁面。

  • 它是一種 JavaScript 工作線程,無法直接訪問 DOM。 service worker通過響應 postMessage 接口發送的消息來與其控制的頁面通信,頁面可在必要時對 DOM 執行操作。
  • 它是一種可編程網絡代理,能夠控制並處理頁面所發送的網絡請求。
  • 它在不用時會被中止,並在下次有需要時重啓,因此不能依賴於service worker的
    onfetch事件onmessage事件 處理程序中的全局狀態。如果存在需要持續保存並在重啓後加以重用的信息,service worker可以訪問 IndexedDB API以及FireFox OS專用的Data Store API等數據存儲機制。
  • service worker 廣泛利用了 promise
  • service Workers 要求要在必須在 HTTPS 下才能運行,爲了便於本地開發,localhost 也被瀏覽器認爲是安全源。

生命週期

Service worker的生命週期完全獨立於網頁

  1. 如果要使用service worker,首先要在js中進行註冊,註冊的動作會讓瀏覽器在後臺啓動service worker的安裝步驟
  2. service worker獲取的第一個事件爲 install。該事件在工作線程執行時立即觸發,並且只能被每個service worker調用一次。 如果更改service worker的代碼,則瀏覽器將其視爲一個不同的service worker,並且它將獲得自己的 install 事件。在安裝的過程中,如果所有需要離線緩存的靜態資源都已經成功緩存,那麼service worker就安裝完成進入激活步驟,如果有文件下載失敗或緩存失敗,service worker就無法完成安裝過程。
  3. 安裝之後進入激活步驟,可以對舊的緩存進行管理
  4. 激活之後,service worker開始施展身手,對它作用域內的所有頁面進行控制,首次註冊service worker的頁面需要再次加載時纔會受控制。激活之後,service worker將處於以下兩種狀態之一:終止或處理onfetch和onmessage事件,從頁面發出網絡請求或消息後將會出現後一種狀態。

如果 service worker 腳本版本處於 ACTIVATED 狀態,功能事件處理完之後,service worker 線程會被終止,當再次有功能事件時,service worker 線程又會被啓動,啓動完成後 service worker 就可以立即進入 ACTIVATED 狀態。

生命週期

瀏覽器內核會管理三種 service worker 腳本版本:

  • installing_version:處於 INSTALLING 狀態的版本
  • waiting_version:處於 INSTALLED 狀態的版本
  • active_version:處於 ACTIVATED 狀態的版本

installing_version 一般是在 service worker 線程啓動後的版本,這是一箇中間版本,在正確安裝完成後會轉入 waiting_version。

waiting_version 一般在註冊信息已被存儲的版本狀態,或者在再次打開 service worker 頁面時,檢查到 service worker 腳本版本的狀態爲 INSTALLED,也會進入此版本狀態。waiting_version 的存在確保了當前 scope 下只有一個生效的 service worker。

active_version 一般在 activate 事件處理完成後,就會處於此版本狀態,同一 scope 下只有一個 active Service Worker。需要特別注意的是,當前頁面已有 active worker 控制,刷新頁面時,新版本 Waiting(Installed) 狀態的 service worker 並不能轉入 active 狀態。

Service worker 可以從 waiting_version 轉入 active_version 的條件:

  • 當前 scope 下沒有 active service worker 在運行。
  • 頁面 JS 調用 self.skipWaiting 跳過 waiting 狀態。
  • 用戶關閉頁面,釋放了當前處於 active 狀態的 service worker。
  • 瀏覽器週期性檢測,發現 active service worker 處於 idle 狀態,就會釋放當前處於 active 狀態的 service worker。

其中,INSTALLED和ACTIVATED是穩定狀態,可以對緩存之類的資源進行管理。

E.G.

1. 註冊

service worker需要在頁面中進行註冊才能啓動安裝步驟,註冊時需要告訴在service worker中執行的代碼的地址

if ('serviceWorker' in navigator) {
    window.addEventListener('load', function() {
        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 是否可用,如果可用,則在頁面加載後註冊位於 /sw.js 的service worker。
每次頁面加載無誤時,即可調用 register();瀏覽器將會判斷service worker是否已註冊並做出相應的處理。

register() 方法的精妙之處在於service worker文件的位置。 本例中service worker文件位於根目錄。 這意味着service worker的作用域將是整個來源。 換句話說,service worker可以接收此網域上所有的 fetch 事件。 如果在 /example/sw.js 處註冊service worker文件,則service worker將只能看到URL以 /example/ 開頭(即 /example/page1/、/example/page2/)的頁面的 fetch 事件。

chrome://inspect/#service-workers 可以看到目前運行中的service worker

這是Google的一個例子
https://cdn.rawgit.com/jakearchibald/80368b84ac1ae8e229fc90b3fe826301/raw/ad55049bee9b11d47f1f7d19a73bf3306d156f43/

2. 安裝

啓動註冊後,需要爲安裝事件定義一個回調,決定哪些資源需要被緩存,在回調的內部,需要執行以下步驟:
1. 打開緩存
2. 緩存資源文件
3. 確定所有需要緩存的資源是否已經緩存完畢

在 install 事件中也可以執行其他任務,甚至不設置 install 事件偵聽器也可以。

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);
      })
  );
});

3.開始工作

刷新當前頁面後,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。 此方法會檢車這個請求,並從service worker創建的緩存中查找緩存的結果。

如果發現有匹配的結果,則返回命中的值,否則調用 fetch 發出網絡請求,並將從服務器請求到的數據作爲結果返回。

4. 更新service worker

  1. 更新您的service worker JavaScript 文件。用戶訪問更新了之後的站點時,瀏覽器會嘗試在後臺重新下載service worker的腳本文件。如果service worker文件與其當前所用文件存在字節差異,則將其視爲“新service worker”。
  2. 新service worker將會啓動,且觸發 install 事件。
  3. 此時,舊service worker仍控制着當前頁面,新service worker將進入 waiting 狀態。
  4. 當網站上當前打開的頁面關閉時,舊service worker將會被終止,新service worker將會取得控制權。
  5. 新service worker取得控制權後,觸發其 activate 事件。(舊service worker退出時將觸發 新service worker的Activate,新service worker將能夠控制客戶端。 可以執行在仍使用舊工作線程時無法執行的操作,如遷移數據庫和清除緩存。)
    出現在 activate 回調中的一個常見任務是緩存管理,但緩存管理通常不在這個新service worker的install回調裏面做,因爲它installed的時候舊的service worker還在工作中。

比如說我們有一個名爲 ‘my-site-cache-v1’ 的緩存,我們想要將該緩存拆分爲一個頁面緩存和一個博文緩存。這就意味着在安裝步驟中我們創建了兩個緩存:’pages-cache-v1’ 和 ‘blog-posts-cache-v1’,且在激活步驟中我們要刪除舊的 ‘my-site-cache-v1’。

具體做法爲:遍歷service worker中的所有緩存,並刪除未在緩存白名單中定義的任何緩存。

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);
          }
        })
      );
    })
  );
});

等待階段表示每次只能運行一個版本的service worker,但可以通過調用 self.skipWaiting() 用新的service worker把舊的那個逐出去,並在進入等待階段時儘快激活自己(或立即激活,前提是已經處於等待階段)。
skipWaiting() 在等待期間調用還是在之前調用並沒有什麼不同。 一般情況下是在 install 事件中調用它:

self.addEventListener('install', event => {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章