Service Worker學習與實踐
Service Worker
本質上充當Web應用程式與瀏覽器之間的代理伺服器,也可以在網路可用時作為瀏覽器和網路間的代理。它們旨在(除其他之外)使得能夠建立有效的離線體驗,攔截網路請求並基於網路是否可用以及更新的資源是否駐留在伺服器上來採取適當的動作。他們還允許訪問推送通知和後臺同步 API
。
-
Service Worker
的本質是一個Web Worker
,它獨立於JavaScript
主執行緒,因此它不能直接訪問DOM
,也不能直接訪問window
物件,但是,Service Worker
可以訪問navigator
物件,也可以通過訊息傳遞的方式(postMessage)與JavaScript
主執行緒進行通訊。 -
Service Worker
是一個網路代理,它可以控制Web
頁面的所有網路請求。 -
Service Worker
具有自身的生命週期,使用好Service Worker
的關鍵是靈活控制其生命週期。
Service Worker
的作用
Web APP
Service Worker
相容性
Service Worker
是現代瀏覽器的一個高階特性,它依賴於 fetch API
、 Cache Storage
、 Promise
等,其中, Cache
提供了 Request / Response
物件對的儲存機制, Cache Storage
儲存多個 Cache
。

示例
在瞭解 Service Worker
的原理之前,先來看一段 Service Worker
的示例:
self.importScripts('./serviceworker-cache-polyfill.js'); var urlsToCache = [ '/', '/index.js', '/style.css', '/favicon.ico', ]; var CACHE_NAME = 'counterxing'; self.addEventListener('install', function(event) { self.skipWaiting(); event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { return cache.addAll(urlsToCache); }) ); }); self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { if (response) { return response; } return fetch(event.request); }) ); }); self.addEventListener('activate', function(event) { var cacheWhitelist = ['counterxing']; 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
的神祕面紗:
polyfill
首先看第一行: self.importScripts('./serviceworker-cache-polyfill.js');
,這裡引入了Cache API的一個 ofollow,noindex">polyfill ,這個 polyfill
支援使得在較低版本的瀏覽器下也可以使用 Cache Storage API
。想要實現 Service Worker
的功能,一般都需要搭配 Cache API
代理網路請求到快取中。
在 Service Worker
執行緒中,使用 importScripts
引入 polyfill
指令碼,目的是對低版本瀏覽器的相容。
Cache Resources List
And Cache Name
之後,使用一個 urlsToCache
列表來宣告需要快取的靜態資源,再使用一個變數 CACHE_NAME
來確定當前快取的 Cache Storage Name
,這裡可以理解成 Cache Storage
是一個 DB
,而 CACHE_NAME
則是 DB
名:
var urlsToCache = [ '/', '/index.js', '/style.css', '/favicon.ico', ]; var CACHE_NAME = 'counterxing'; 複製程式碼
Lifecycle
Service Worker
獨立於瀏覽器 JavaScript
主執行緒,有它自己獨立的生命週期。
如果需要在網站上安裝 Service Worker
,則需要在 JavaScript
主執行緒中使用以下程式碼引入 Service Worker
。
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').then(function(registration) { console.log('成功安裝', registration.scope); }).catch(function(err) { console.log(err); }); } 複製程式碼
此處,一定要注意 sw.js
檔案的路徑,在我的示例中,處於當前域根目錄下,這意味著, Service Worker
和網站是同源的,可以為當前網站的所有請求做代理,如果 Service Worker
被註冊到 /imaging/sw.js
下,那隻能代理 /imaging
下的網路請求。
可以使用 Chrome
控制檯,檢視當前頁面的 Service Worker
情況:

安裝完成後, Service Worker
會經歷以下生命週期:
download install activate
-
使用者首次訪問
Service Worker
控制的網站或頁面時,Service Worker
會立刻被下載。之後至少每24
小時它會被下載一次。它可能被更頻繁地下載,不過每24
小時一定會被下載一次,以避免不良指令碼長時間生效。 -
在下載完成後,開始安裝
Service Worker
,在安裝階段,通常需要快取一些我們預先宣告的靜態資源,在我們的示例中,通過urlsToCache
預先宣告。 -
在安裝完成後,會開始進行啟用,瀏覽器會嘗試下載
Service Worker
指令碼檔案,下載成功後,會與前一次已快取的Service Worker
指令碼檔案做對比,如果與前一次的Service Worker
指令碼檔案不同,證明Service Worker
已經更新,會觸發activate
事件。完成啟用。
如圖所示,為 Service Worker
大致的生命週期:

install
在安裝完成後,嘗試快取一些靜態資源:
self.addEventListener('install', function(event) { self.skipWaiting(); event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { return cache.addAll(urlsToCache); }) ); }); 複製程式碼
首先, self.skipWaiting()
執行,告知瀏覽器直接跳過等待階段,淘汰過期的 sw.js
的 Service Worker
指令碼,直接開始嘗試啟用新的 Service Worker
。
然後使用 caches.open
開啟一個 Cache
,開啟後,通過 cache.addAll
嘗試快取我們預先宣告的靜態檔案。
監聽 fetch
,代理網路請求
頁面的所有網路請求,都會通過 Service Worker
的 fetch
事件觸發, Service Worker
通過 caches.match
嘗試從 Cache
中查詢快取,快取如果命中,則直接返回快取中的 response
,否則,建立一個真實的網路請求。
self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { if (response) { return response; } return fetch(event.request); }) ); }); 複製程式碼
如果我們需要在請求過程中,再向 Cache Storage
中新增新的快取,可以通過 cache.put
方法新增,看以下例子:
self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { // 快取命中 if (response) { return response; } // 注意,這裡必須使用clone方法克隆這個請求 // 原因是response是一個Stream,為了讓瀏覽器跟快取都使用這個response // 必須克隆這個response,一份到瀏覽器,一份到快取中快取。 // 只能被消費一次,想要再次消費,必須clone一次 var fetchRequest = event.request.clone(); return fetch(fetchRequest).then( function(response) { // 必須是有效請求,必須是同源響應,第三方的請求,因為不可控,最好不要快取 if (!response || response.status !== 200 || response.type !== 'basic') { return response; } // 消費過一次,又需要再克隆一次 var responseToCache = response.clone(); caches.open(CACHE_NAME) .then(function(cache) { cache.put(event.request, responseToCache); }); return response; } ); }) ); }); 複製程式碼
在專案中,一定要注意控制快取,介面請求一般是不推薦快取的。所以在我自己的專案中,並沒有在這裡做動態的快取方案。
activate
Service Worker
總有需要更新的一天,隨著版本迭代,某一天,我們需要把新版本的功能釋出上線,此時需要淘汰掉舊的快取,舊的 Service Worker
和 Cache Storage
如何淘汰呢?
self.addEventListener('activate', function(event) { var cacheWhitelist = ['counterxing']; event.waitUntil( caches.keys().then(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName); } }) ); }) ); }); 複製程式碼
- 首先有一個白名單,白名單中的
Cache
是不被淘汰的。 - 之後通過
caches.keys()
拿到所有的Cache Storage
,把不在白名單中的Cache
淘汰。 - 淘汰使用
caches.delete()
方法。它接收cacheName
作為引數,刪除該cacheName
所有快取。
sw-precache-webpack-plugin
sw-precache-webpack-plugin 是一個 webpack plugin
,可以通過配置的方式在 webpack
打包時生成我們想要的 sw.js
的 Service Worker
指令碼。
一個最簡單的配置如下:
var path = require('path'); var SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin'); const PUBLIC_PATH = 'https://www.my-project-name.com/';// webpack needs the trailing slash for output.publicPath module.exports = { entry: { main: path.resolve(__dirname, 'src/index'), }, output: { path: path.resolve(__dirname, 'src/bundles/'), filename: '[name]-[hash].js', publicPath: PUBLIC_PATH, }, plugins: [ new SWPrecacheWebpackPlugin( { cacheId: 'my-project-name', dontCacheBustUrlsMatching: /\.\w{8}\./, filename: 'service-worker.js', minify: true, navigateFallback: PUBLIC_PATH + 'index.html', staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/], } ), ], } 複製程式碼
在執行 webpack
打包後,會生成一個名為 service-worker.js
檔案,用於快取 webpack
打包後的靜態檔案。
一個最簡單的 示例 。
Service Worker Cache
VS Http Cache
對比起 Http Header
快取, Service Worker
配合 Cache Storage
也有自己的優勢:
- 快取與更新並存:每次更新版本,藉助
Service Worker
可以立馬使用快取返回,但與此同時可以發起請求,校驗是否有新版本更新。 - 無侵入式:
hash
值實在是太難看了。 - 不易被沖掉:
Http
快取容易被沖掉,也容易過期,而Cache Storage
則不容易被沖掉。也沒有過期時間的說法。 - 離線:藉助
Service Worker
可以實現離線訪問應用。
但是缺點是,由於 Service Worker
依賴於 fetch API
、依賴於 Promise
、 Cache Storage
等,相容性不太好。
後話
本文只是簡單總結了 Service Worker
的基本使用和使用 Service Worker
做客戶端快取的簡單方式,然而, Service Worker
的作用遠不止於此,例如:藉助 Service Worker
做離線應用、用於做網路應用的推送(可參考push-notifications)等。
甚至可以藉助 Service Worker
,對介面進行快取,在我所在的專案中,其實並不會做的這麼複雜。不過做介面快取的好處是支援離線訪問,對離線狀態下也能正常訪問我們的 Web
應用。
Cache Storage
和 Service Worker
總是分不開的。 Service Worker
的最佳用法其實就是配合 Cache Storage
做離線快取。藉助於 Service Worker
,可以輕鬆實現對網路請求的控制,對於不同的網路請求,採取不同的策略。例如對於 Cache
的策略,其實也是存在多種情況。例如可以優先使用網路請求,在網路請求失敗時再使用快取、亦可以同時使用快取和網路請求,一方面檢查請求,一方面有檢查快取,然後看兩個誰快,就用誰。