WorkBox 之底層邏輯 Service Worker
1. 前置知識點
「前置知識點」,只是做一個概念的介紹,不會做深度解釋。因為,這些概念在下面文章中會有出現(xiàn),為了讓行文更加的順暢,所以將本該在文內(nèi)的概念解釋放到前面來?!溉绻蠹覍@些概念熟悉,可以直接忽略」同時,由于閱讀我文章的群體有很多,所以有些知識點可能「我視之若珍寶,爾視只如草芥,棄之如敝履」。以下知識點,請「酌情使用」。
如何查看Service Worker
要查看正在運行的Service workers列表,我們可以在Chrome/Chromium中地址欄中輸入chrome://serviceworker-internals/。
圖片
chrome://xx 包含了很多內(nèi)置的功能,這塊也是有很大的說道的。后期,會單獨有一個專題來講。(已經(jīng)在籌劃準備中....)
Cache API
Cache API為緩存的 Request / Response 對象對提供存儲機制。例如,作為ServiceWorker 生命周期的一部分
Cache API像 workers 一樣,是暴露在 window 作用域下的。盡管它被定義在 service worker 的標準中,但是它不必一定要配合 service worker 使用。
「一個域可以有多個命名 Cache 對象」。我們需要在腳本 (例如,在 ServiceWorker 中) 中處理緩存更新的方式。
- 除非明確地更新緩存,否則緩存將不會被更新;
- 除非刪除,否則緩存數(shù)據(jù)不會過期
- 使用 CacheStorage.open(cacheName) 打開一個 Cache 對象,再使用 Cache 對象的方法去處理緩存。
- 需要定期地清理緩存條目,因為每個瀏覽器都硬性限制了一個域下緩存數(shù)據(jù)的大小。
緩存配額使用估算值,可以使用 StorageEstimate API 獲得。
瀏覽器盡其所能去管理磁盤空間,但它有可能刪除一個域下的緩存數(shù)據(jù)。
瀏覽器要么自動刪除特定域的全部緩存,要么全部保留。
一些圍繞service worker緩存的重要 API 方法包括:
- CacheStorage.open用于創(chuàng)建新的 Cache 實例。
- Cache.add和Cache.put用于將「網(wǎng)絡響應」存儲在service worker緩存中。
- Cache.match用于查找 Cache 實例中的緩存響應。
- Cache.delete用于從 Cache 實例中刪除緩存響應。
- .....
Cache.put, Cache.add和Cache.addAll只能在GET請求下使用。
更多詳情可以參考MDN-Cache[1]
Cache API 與 HTTP 緩存的區(qū)別
如果我們以前沒有使用過Cache接口,可能會認為它與 HTTP 緩存相同,或者至少與 HTTP 緩存相關。但實際情況并非如此。
- Cache接口是一個「完全獨立于」HTTP 緩存的緩存機制
- 用于影響HTTP緩存的任何Cache-Control配置對存儲在Cache接口中的資源沒有影響。
可以將瀏覽器緩存看作是「分層的」。
- HTTP緩存是一個由「鍵-值對驅(qū)動」的「低級緩存」,其中的指令在HTTP Header中表示。
- Cache接口是由「JavaScript API 驅(qū)動」的「高級緩存」。這比使用相對簡單的HTTP鍵-值對具有更大的靈活性。
2. Service Workers 能為我們帶來什么
Service workers是JavaScript層面的 API,「充當 Web 瀏覽器和 Web 服務器之間的代理」。它們的目標是通過提供離線訪問以及提升頁面性能來提高可靠性。
漸進增強,類似應用程序生命周期
Service workers是對現(xiàn)有網(wǎng)站的增強。這意味著如果使用Service workers的網(wǎng)站的用戶使用不支持Service workers的瀏覽器訪問網(wǎng)站,基本功能不會受到破壞。它是向下兼容的。
Service workers通過類似于桌面應用程序的生命周期逐漸增強網(wǎng)站。想象一下當從應用商城安裝APP時會發(fā)生流程:
- 發(fā)出下載APP的請求。
- APP下載并安裝。
- APP準備好使用并可以啟動。
- APP進行新版本的更新。
Service worker也采用類似的生命周期,但采用「漸進增強」的方法。
- 在首次訪問安裝了新Service worker的網(wǎng)頁時,初始訪問提供網(wǎng)站的基本功能,同時Service worker開始「下載」。
- 「安裝」和「激活」Service worker后,它將控制頁面以提供更高的可靠性和速度。
采用 JavaScript 驅(qū)動的 Cache API
Service worker技術中不可或缺的一部分是Cache API,這是一種「完全獨立于 HTTP 緩存的緩存機制」。Cache API可以在Service worker作用域內(nèi)和「主線程」作用域內(nèi)訪問。該特性為用戶操作與 Cache 實例的交互提供了許多可能性。
- HTTP緩存是通過HTTP Header中指定的「緩存指令」來影響的
- Cache API可以「通過 JavaScript 進行編程」
這意味著可以根據(jù)網(wǎng)站的特有的邏輯來緩存網(wǎng)絡請求的響應。例如:
- 在「首次請求靜態(tài)資源時」將其存儲在緩存中,然后在「后續(xù)請求中從緩存中獲取」。
- 將頁面結構存儲在緩存中,但在「離線情況下」從緩存中獲取。
- 對于一些「非緊急的資源」,先從緩存中獲取,然后在后臺中通過網(wǎng)絡再更新它。下次再獲取該資源時候,就認為是最新的
- 網(wǎng)絡采用「流式傳輸」處理部分內(nèi)容,并與緩存中的應用程序攔截層組合以改善感知性能。
這些都是緩存策略的應用方向。緩存策略使離線體驗成為可能,并「通過繞過 HTTP 緩存觸發(fā)的高延遲重新驗證檢查提供更好的性能」。
異步和事件驅(qū)動的 API
在「網(wǎng)絡上傳輸數(shù)據(jù)本質(zhì)上是異步的」。請求資產(chǎn)、服務器響應請求以及下載響應都需要時間。所涉及的時間是多樣且不確定的。Service workers通過「事件驅(qū)動」的 API 來適應這種異步性,「使用回調(diào)處理事件」,例如:
- 當Service worker正在「安裝」時。
- 當Service worker正在「激活」時。
- 當Service worker檢測到網(wǎng)絡請求時。
都可以使用addEventListener API 注冊事件。所有這些事件都可以與Cache API進行交互。特別是在網(wǎng)絡請求是離散的,運行回調(diào)的能力對于「提供所期望的可靠性和速度」至關重要。
在JavaScript中進行異步工作涉及使用Promises。因為Promises也支持async和await,這些JavaScript特性也可用于簡化Service worker代碼,從而提供更好的開發(fā)者體驗。
預緩存和運行時緩存
Service worker與Cache實例之間的交互涉及兩個不同的緩存概念:
- 「預緩存」(Precaching caching)
- 「運行時緩存」(Runtime caching)
預緩存是需要提前緩存資源的過程,通常在Service worker「安裝期間」進行。通過預緩存,「關鍵的靜態(tài)資產(chǎn)和離線訪問所需的材料可以被下載并存儲在 Cache 實例中」。這種類型的緩存還可以提高需要預緩存資源的后續(xù)頁面的頁面速度。
運行時緩存是指在運行時從網(wǎng)絡請求資源時應用緩存策略。這種類型的緩存非常有用,因為它保證了用戶已經(jīng)訪問過的頁面和資源的離線訪問。
當在Service worker中使用這些方法時,可以為用戶體驗提供巨大的好處,并為普通的網(wǎng)頁提供類似應用程序的行為。
與主線程隔離
Service workers與Web workers類似,它們的「所有工作都在自己的線程上進行」。這意味著Service workers的任務不會與主線程上的其他任務競爭。
我們就以Web Worker為例子,做一個簡單的演示 在JavaScript中創(chuàng)建Web Worker并不是一項復雜的任務。
創(chuàng)建一個新的JavaScript文件,其中包含我們希望在工作線程中運行的代碼。此文件不應包含對DOM的任何引用,因為它將無法訪問DOM。
在我們的主JavaScript文件中,使用Worker構造函數(shù)創(chuàng)建一個新的Worker對象。此構造函數(shù)接受一個參數(shù),即我們在第1步中創(chuàng)建的JavaScript文件的URL。
const worker = new Worker('worker.js');
為Worker對象添加事件偵聽器,以處理主線程和工作線程之間發(fā)送的消息。onmessage事件處理程序用于處理從工作線程發(fā)送的消息,而postMessage方法用于向工作線程發(fā)送消息。
worker.onmessage = function(event) {
console.log('Worker said: ' + event.data);
};
worker.postMessage('Hello, worker!');
在我們的工作線程JavaScript文件中,添加一個事件偵聽器,以處理從主線程發(fā)送的消息,使用self對象的onmessage屬性。我們可以使用event.data屬性訪問消息中發(fā)送的數(shù)據(jù)。
self.onmessage = function(event) {
console.log('Main thread said: ' + event.data);
self.postMessage('Hello, main thread!');
};
現(xiàn)在讓我們運行Web應用程序并測試Worker。我們應該在控制臺中看到打印的消息,指示主線程和工作線程之間已發(fā)送和接收消息。
圖片
3. Service worker 的生命周期
定義術語
在深入了解service worker的生命周期之前,我們先來了解一下與生命周期運作相關的「術語」(黑話)
控制和作用域
了解service worker運作方式的關鍵在于理解「控制」(control)。
- 由service worker控制的頁面允許service worker代表該頁面進行攔截網(wǎng)絡請求。
- 在給定的「作用域」(scope)內(nèi),service worker能夠為頁面執(zhí)行處理資源的相關工作。
作用域
一個service worker的作用域由其「在 Web 服務器上的位置確定」。如果一個service worker在位于/A/index.html的頁面上運行,并且位于/A/sw.js上,那么該service worker的作用域就是/A/。
- 打開https://service-worker-scope-viewer.glitch.me/subdir/index.html。將顯示一條消息,說明沒有service worker正在「控制」該頁面。但是,該頁面從https://service-worker-scope-viewer.glitch.me/subdir/sw.js注冊了一個service worker。
- 「重新加載頁面」。因為service worker「已經(jīng)注冊并處于活動狀態(tài)」,它正在「控制」頁面。將顯示一個包含service worker作用域、當前狀態(tài)和其 URL 的表單。
- 現(xiàn)在打開https://service-worker-scope-viewer.glitch.me/index.html。盡管在此origin上注冊了一個service worker,但仍然會顯示一條消息,說明沒有當前的service worker。這是因為此頁面不在已注冊service worker的作用域內(nèi)。
作用域限制了service worker控制的頁面。在上面的例子中,這意味著從/subdir/sw.js加載的service worker只能「控制位于/subdir/或其子頁面中」。
控制頁面的service worker仍然可以「攔截任何網(wǎng)絡請求」,包括跨域資源的請求。作用域限制了由service worker控制的頁面。
上述是默認情況下作用域工作的方式,但可以通過設置Service-Worker-Allowed響應頭,以及通過向register方法傳遞作用域選項來進行覆蓋。
除非有很好的理由將service worker的作用域限制為origin的子集,否則應「從 Web 服務器的根目錄加載service worker,以便其作用域盡可能廣泛」,不必擔心Service-Worker-Allowed頭部。
客戶端
當說一個service worker正在控制一個頁面時,實際上「是在控制一個客戶端」??蛻舳耸侵窾RL位于該service worker作用域內(nèi)的「任何打開的頁面」。具體來說,這些是WindowClient的實例。
圖片
3.1 Service worker 在初始化時的生命周期
為了使service worker能夠控制頁面,首先必須將其部署。
讓我們看看一個沒有service worker的網(wǎng)站到部署全新service worker時,中間發(fā)生了啥?
1. 注冊(Registration)
注冊是service worker生命周期的「初始步驟」:
<script>
// 直到頁面完全加載后再注冊service worker
window.addEventListener("load", () => {
// 檢查service worker是否可用
if ("serviceWorker" in navigator) {
navigator.serviceWorker
.register("/sw.js")
.then(() => {
console.log("Service worker 注冊成功!");
})
.catch((error) => {
console.warn("注冊service worker時發(fā)生錯誤:");
console.warn(error);
});
}
});
</script>
此代碼在「主線程」上運行,并執(zhí)行以下操作:
- 因為用戶「首次訪問網(wǎng)站時」沒有注冊service worker,所以等待「頁面完全加載后」再注冊一個。這樣可以避免在service worker預緩存任何內(nèi)容時出現(xiàn)「帶寬爭用」。
- 盡管service worker得到了廣泛支持,但進行「特性檢查」可以避免在不支持它的瀏覽器中出現(xiàn)錯誤。
- 當頁面完全加載后,如果支持service worker,則注冊/sw.js。
還有一些關鍵要點:
- Service worker僅在HTTPS或localhost上可用。
- 如果service worker的內(nèi)容包含「語法錯誤」,注冊會失敗,并丟棄service worker。
- service worker在一個作用域內(nèi)運行。在這里,作用域是整個origin,因為它是從根目錄加載的。
- 當注冊開始時,service worker的狀態(tài)被設置為installing。
一旦注冊完成,「安裝」就開始了。
2. 安裝(Installation)
service worker在注冊后觸發(fā)其install事件。install「只會在每個service worker中調(diào)用一次,直到它被更新才會再次觸發(fā)」??梢允褂胊ddEventListener在worker的作用域內(nèi)注冊install事件的回調(diào):
// /sw.js
self.addEventListener("install", (event) => {
const cacheKey = "前端柒八九_v1";
event.waitUntil(
caches.open(cacheKey).then((cache) => {
// 將數(shù)組中的所有資產(chǎn)添加到'前端柒八九_v1'的`Cache`實例中以供以后使用。
return cache.addAll([
"/css/global.bc7b80b7.css",
"/css/home.fe5d0b23.css",
"/js/home.d3cc4ba4.js",
"/js/A.43ca4933.js",
]);
})
);
});
這會創(chuàng)建一個新的Cache實例并對資產(chǎn)進行「預緩存」。其中有一個event.waitUntil。event.waitUntil接受一個Promise,并等待該Promise被解決。
在這個示例中,這個Promise執(zhí)行兩個異步操作:
- 創(chuàng)建一個名為前端柒八九_v1的新Cache實例。
- 在創(chuàng)建緩存之后,使用其異步的addAll方法「預緩存」一個資源URL數(shù)組。
如果傳遞給event.waitUntil的Promise被「拒絕,安裝將失敗」。如果發(fā)生這種情況,service worker將被「丟棄」。
如果Promise被解決,安裝成功,service worker的狀態(tài)將更改為installed,然后進入「激活」階段。
3. 激活(Activation)
如果注冊和安裝成功,service worker將被「激活」,其狀態(tài)將變?yōu)閍ctivating。在service worker的activate事件中可以進行激活期間的工作。在此事件中的一個典型任務是「清理舊緩存」,但對于「全新 service worker」,目前還不相關。
對于新的service worker,「安裝成功后,激活會立即觸發(fā)」。一旦激活完成,service worker的狀態(tài)將變?yōu)閍ctivated。
默認情況下,新的service worker直到「下一次導航或頁面刷新之前才會開始控制頁面」。
3.2 處理 service worker 的更新
一旦部署了第一個service worker,它很可能需要在以后進行更新。例如,如果請求處理或預緩存邏輯發(fā)生了變化,就可能需要進行更新。
更新發(fā)生的時機
瀏覽器會在以下情況下檢查service worker的更新:
- 用戶導航到service worker作用域內(nèi)的頁面。
- 調(diào)用navigator.serviceWorker.register()并「傳入與當前安裝的 service worker 不同的 URL」
- 調(diào)用navigator.serviceWorker.register()并「傳入與已安裝的 service worker 相同的 URL」,但具有「不同的作用域」。
更新的方式
了解瀏覽器何時更新service worker很重要,但“如何”也很重要。假設service worker的URL或作用域未更改,「只有在其內(nèi)容發(fā)生變化時,當前安裝的service worker才會更新到新版本」。
瀏覽器以幾種方式檢測變化:
- importScripts請求的腳本的「字節(jié)級更改」。
- service worker的「頂級代碼的任何更改」,這會影響瀏覽器生成的指紋。
為確保瀏覽器能夠可靠地檢測service worker內(nèi)容的變化,「不要使用 HTTP 緩存保留它,也不要更改其文件名」。當導航到service worker作用域內(nèi)的新頁面時,瀏覽器會自動執(zhí)行更新檢查。
手動觸發(fā)更新檢查
關于更新,注冊邏輯通常不應更改。然而,一個例外情況可能是「網(wǎng)站上的會話持續(xù)時間很長」。這可能在「單頁應用程序」中發(fā)生,因為導航請求通常很少,應用程序通常在應用程序生命周期的開始遇到一個導航請求。在這種情況下,可以在「主線程上手動觸發(fā)更新」:
navigator.serviceWorker.ready.then((registration) => {
registration.update();
});
對于傳統(tǒng)的網(wǎng)站,或者在用戶會話不持續(xù)很長時間的任何情況下,手動更新可能不是必要的。
安裝(Installation)
當使用打包工具生成「靜態(tài)資源」時,這些資源的「名稱中會包含哈希值」,例如framework.3defa9d2.js。假設其中一些資源被預緩存以供以后離線訪問,這將需要對service worker進行更新以預緩存新的資源:
self.addEventListener("install", (event) => {
const cacheKey = "前端柒八九_v2";
event.waitUntil(
caches.open(cacheKey).then((cache) => {
// 將數(shù)組中的所有資產(chǎn)添加到'前端柒八九_v2'的`Cache`實例中以供以后使用。
return cache.addAll([
"/css/global.ced4aef2.css",
"/css/home.cbe409ad.css",
"/js/home.109defa4.js",
"/js/A.38caf32d.js",
]);
})
);
});
與之前的install事件示例有兩個方面不同:
- 創(chuàng)建了一個具有 key 為前端柒八九_v2的「新 Cache 實例」。
- 預緩存資源的名稱已更改。(/css/global.bc7b80b7.css變?yōu)?css/global.ced4aef2.css)
更新后的service worker會與先前的service worker并存。這意味著舊的service worker仍然控制著任何打開的頁面。剛才安裝的新的service worker進入等待狀態(tài),直到被激活。
默認情況下,新的service worker將在「沒有任何客戶端由舊的service worker控制時激活」。這發(fā)生在相關網(wǎng)站的所有打開標簽都關閉時。
激活(Activation)
當安裝了新的service worker并結束了等待階段時,它會被激活,并丟棄舊的service worker。在更新后的service worker的activate事件中執(zhí)行的常見任務是「清理舊緩存」。通過使用caches.keys獲取所有打開的 Cache 實例的key,并使用caches.delete刪除不在允許列表中的所有舊緩存:
self.addEventListener("activate", (event) => {
// 指定允許的緩存密鑰
const cacheAllowList = ["前端柒八九_v2"];
// 獲取當前活動的所有`Cache`實例。
event.waitUntil(
caches.keys().then((keys) => {
// 刪除不在允許列表中的所有緩存:
return Promise.all(
keys.map((key) => {
if (!cacheAllowList.includes(key)) {
return caches.delete(key);
}
})
);
})
);
});
舊的緩存不會自動清理。我們需要自己來做,否則可能會超過存儲配額。
由于第一個service worker中的前端柒八九_v1已經(jīng)過時,緩存允許列表已更新為指定前端柒八九_v2,這將刪除具有不同名稱的緩存。
「激活事件在舊緩存被刪除后完成」。此時,新的service worker將控制頁面,最終替代舊的service worker!
4. Service worker 緩存策略
要有效使用service worker,有必要采用一個或多個緩存策略,這需要對Cache API有一定的了解。
緩存策略是service worker的fetch事件與Cache API之間的交互。如何編寫緩存策略取決于不同情況。
普通的 Fetch 事件
緩存策略的另一個重要的用途就是與service worker的fetch事件配合使用。我們已經(jīng)聽說過一些關于「攔截網(wǎng)絡請求」的內(nèi)容,而service worker內(nèi)部的fetch事件就是處理這種情況的:
// 建立緩存名稱
const cacheName = "前端柒八九_v1";
self.addEventListener("install", (event) => {
event.waitUntil(caches.open(cacheName));
});
self.addEventListener("fetch", async (event) => {
// 這是一個圖片請求
if (event.request.destination === "image") {
// 打開緩存
event.respondWith(
caches.open(cacheName).then((cache) => {
// 從緩存中響應圖片,如果緩存中沒有,就從網(wǎng)絡獲取圖片
return cache.match(event.request).then((cachedResponse) => {
return (
cachedResponse ||
fetch(event.request.url).then((fetchedResponse) => {
// 將網(wǎng)絡響應添加到緩存以供將來訪問。
// 注意:我們需要復制響應以保存在緩存中,同時使用原始響應作為請求的響應。
cache.put(event.request, fetchedResponse.clone());
// 返回網(wǎng)絡響應
return fetchedResponse;
})
);
});
})
);
} else {
return;
}
});
上面的代碼執(zhí)行以下操作:
- 檢查請求的destination屬性,以查看是否是圖像請求。
- 如果圖像在service worker緩存中,則從緩存中提供它。如果沒有,從網(wǎng)絡獲取圖像,將響應存儲在緩存中,并返回網(wǎng)絡響應。
- 所有其他請求都會通過service worker,不與緩存互動。
fetch事件的事件對象包含一個request屬性,其中包含一些有用的信息,可幫助我們識別每個請求的類型:
- url,表示當前由 fetch 事件處理的網(wǎng)絡請求的 URL。
- method,表示請求方法(例如GET或POST)。
- mode,描述請求的模式。通常使用值navigate來區(qū)分對 HTML 文檔的請求與其他請求。
- destination,以一種避免使用所請求資產(chǎn)的文件擴展名的方式描述所請求內(nèi)容的類型。
「異步操作是關鍵」。我們還記得install事件提供了一個event.waitUntil方法,它接受一個promise,并在激活之前等待其解析。fetch事件提供了類似的event.respondWith方法,我們可以使用它來返回異步fetch請求的結果或Cache接口的match方法返回的響應。
緩存策略
1. 僅緩存(Cache only)
展示了從頁面到service worker到緩存的流程。
「僅緩存」運作方式:當service worker控制頁面時,「匹配的請求只會進入緩存」。這意味著為了使該模式有效,「任何緩存的資源都需要在安裝時進行預緩存」,而「這些資源在service worker更新之前將不會在緩存中進行更新」。
// 建立緩存名稱
const cacheName = "前端柒八九_v1";
// 要預緩存的資產(chǎn)
const preCachedAssets = ["/A.jpg", "/B.jpg", "/C.jpg", "/D.jpg"];
self.addEventListener("install", (event) => {
// 在安裝時預緩存資產(chǎn)
event.waitUntil(
caches.open(cacheName).then((cache) => {
return cache.addAll(preCachedAssets);
})
);
});
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
const isPrecachedRequest = preCachedAssets.includes(url.pathname);
if (isPrecachedRequest) {
// 從緩存中獲取預緩存的資產(chǎn)
event.respondWith(
caches.open(cacheName).then((cache) => {
return cache.match(event.request.url);
})
);
} else {
// 轉(zhuǎn)到網(wǎng)絡
return;
}
});
在上面的示例中,數(shù)組中的資產(chǎn)在安裝時被預緩存。當service worker處理fetch請求時,我們「檢查fetch事件處理的請求 URL 是否在預緩存資產(chǎn)的數(shù)組中」。
- 如果是,我們從緩存中獲取資源,并跳過網(wǎng)絡。
- 其他請求將通過網(wǎng)絡傳遞,只經(jīng)過網(wǎng)絡。
2. 僅網(wǎng)絡(Network only)
圖片
「僅網(wǎng)絡」的策略與「僅緩存」相反,它將請求通過service worker傳遞到網(wǎng)絡,而「不與 service worker 緩存進行任何交互」。這是一種「確保內(nèi)容新鮮度」的好策略,但其權衡是「當用戶離線時將無法正常工作」。
要確保請求直接通過到網(wǎng)絡,只需「不對匹配的請求調(diào)用 event.respondWith」。如果我們想更明確,可以在要傳遞到網(wǎng)絡的請求的fetch事件回調(diào)中加入一個空的return;。這就是「僅緩存」策略演示中對于未經(jīng)預緩存的請求所發(fā)生的情況。
3. 緩存優(yōu)先,備用網(wǎng)絡(Cache first, falling back to network)
圖片
對于「匹配的請求」,流程如下:
- 請求到達緩存。如果資產(chǎn)在緩存中,就從緩存中提供。
- 如果請求不在緩存中,去訪問網(wǎng)絡。
- 一旦網(wǎng)絡請求完成,將其添加到緩存,然后返回網(wǎng)絡響應。
// 建立緩存名稱
const cacheName = "前端柒八九_v1";
self.addEventListener("fetch", (event) => {
// 檢查這是否是一個圖像請求
if (event.request.destination === "image") {
event.respondWith(
caches.open(cacheName).then((cache) => {
// 首先從緩存中獲取
return cache.match(event.request.url).then((cachedResponse) => {
// 如果我們有緩存的響應,則返回緩存的響應
if (cachedResponse) {
return cachedResponse;
}
// 否則,訪問網(wǎng)絡
return fetch(event.request).then((fetchedResponse) => {
// 將網(wǎng)絡響應添加到緩存以供以后訪問
cache.put(event.request, fetchedResponse.clone());
// 返回網(wǎng)絡響應
return fetchedResponse;
});
});
})
);
} else {
return;
}
});
盡管這個示例只涵蓋了圖像,但這是一個很好的范例,「適用于所有靜態(tài)資產(chǎn)」(如CSS、JavaScript、圖像和字體),「尤其是哈希版本的資產(chǎn)」。它「通過跳過 HTTP 緩存可能啟動的任何與服務器的內(nèi)容新鮮度檢查,為不可變資產(chǎn)提供了速度提升」。更重要的是,「任何緩存的資產(chǎn)都將在離線時可用」。
4. 網(wǎng)絡優(yōu)先,備用緩存(Network first, falling back to cache)
它的含義就是:
- 首先通過網(wǎng)絡請求資源,然后將響應放入緩存。
- 如果以后「離線了,就回退到緩存中的最新版本的響應」。
這種策略對于HTML或 API 請求非常有用,當在線時,我們希望獲取資源的最新版本,但希望在離線時能夠訪問最新可用的版本。
// 建立緩存名稱
const cacheName = "前端柒八九_v1";
self.addEventListener("fetch", (event) => {
// 檢查這是否是導航請求
if (event.request.mode === "navigate") {
// 打開緩存
event.respondWith(
caches.open(cacheName).then((cache) => {
// 首先通過網(wǎng)絡請求
return fetch(event.request.url)
.then((fetchedResponse) => {
cache.put(event.request, fetchedResponse.clone());
return fetchedResponse;
})
.catch(() => {
// 如果網(wǎng)絡不可用,從緩存中獲取
return cache.match(event.request.url);
});
})
);
} else {
return;
}
});
- 首先,訪問頁面??赡苄枰趯?nbsp;HTML 響應放入緩存之前重新加載。
- 然后在開發(fā)者工具中,模擬離線連接,然后重新加載。
- 最后一個可用版本將立即從緩存中提供。
在需要重視離線功能,但又需要平衡該功能與獲取一些標記或 API 數(shù)據(jù)的最新版本的情況下,「網(wǎng)絡優(yōu)先,備用緩存」是一種實現(xiàn)這一目標的可靠策略。
5. 陳舊時重新驗(Stale-while-revalidate)
圖片
「陳舊時重新驗證」策略是其中最復雜的。該策略的過程「優(yōu)先考慮了資源的訪問速度」,同時在后臺保持其更新。該策略的工作流程如下:
- 對于首次請求的資源,從網(wǎng)絡獲取,將其放入緩存,并返回網(wǎng)絡響應。
- 對于后續(xù)請求,首先從緩存中提供資源,然后在后臺重新從網(wǎng)絡請求并更新資源的緩存條目。
- 對于以后的請求,我們將收到從網(wǎng)絡獲取并在前一步放入緩存的最新版本。
這是一個適用于「需要保持更新但不是絕對必要的資源」的策略,比如網(wǎng)站的頭像。它們會在用戶愿意更新時進行更新,但不一定需要在每次請求時獲取最新版本。
// 建立緩存名稱
const cacheName = "前端柒八九_v1";
self.addEventListener("fetch", (event) => {
if (event.request.destination === "image") {
event.respondWith(
caches.open(cacheName).then((cache) => {
return cache.match(event.request).then((cachedResponse) => {
const fetchedResponse = fetch(event.request).then(
(networkResponse) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
}
);
return cachedResponse || fetchedResponse;
});
})
);
} else {
return;
}
});
5. Service Worker 預緩存的陷阱
如果將預緩存「應用于太多的資產(chǎn)」,或者如果Service Worker在頁面「完成加載關鍵資產(chǎn)之前」就注冊了,那么可能會遇到問題。
當Service Worker在「安裝期間預緩存資產(chǎn)時,將同時發(fā)起一個或多個網(wǎng)絡請求」。如果時機不合適,這可能會對用戶體驗產(chǎn)生問題。即使時機剛剛好,如果未對預緩存資產(chǎn)的「數(shù)量進行限制」,仍可能會浪費數(shù)據(jù)。
一切都取決于時機
如果Service Worker預緩存任何內(nèi)容,那么它的注冊時機很重要。Service Worker通常使用內(nèi)聯(lián)的<script>元素注冊。這意味著 HTML 解析器可能在頁面的關鍵資產(chǎn)加載完成之前就發(fā)現(xiàn)了Service Worker的注冊代碼。
這是一個問題。Service Worker在最壞的情況下應該對性能沒有不利影響,而不是使性能變差。為用戶著想,應該在「頁面加載事件」觸發(fā)時注冊Service Worker。這減少了預緩存可能干擾加載頁面的關鍵資產(chǎn)的機會,從而意味著頁面可以更快地實現(xiàn)交互,而無需處理后來可能不需要的資產(chǎn)的網(wǎng)絡請求。
if ("serviceWorker" in navigator) {
window.addEventListener("load", function () {
navigator.serviceWorker.register("/service-worker.js");
});
}
考慮數(shù)據(jù)使用
無論時機如何,「預緩存都涉及發(fā)送網(wǎng)絡請求」。如果不謹慎地選擇要預緩存的資產(chǎn)清單,結果可能會浪費一些數(shù)據(jù)。
「浪費數(shù)據(jù)是預緩存的一個潛在代價」,但并非每個人都可以訪問快速的互聯(lián)網(wǎng)或無限的數(shù)據(jù)計劃!「在預緩存時,應考慮刪除特別大的資產(chǎn),并依賴于運行時緩存來捕捉它們」,而不是進行假設用戶都需要這些資源,從而全部都進行緩存。
6. 改進Service Worker開發(fā)體驗
雖然Service Worker生命周期確保了可預測的安裝和更新過程,但它可能使本地開發(fā)與常規(guī)開發(fā)有些不同。
本地開發(fā)的異常情況
通常情況下,Service WorkerAPI 僅在通過 HTTPS 提供的頁面上可用,但是我們平時開發(fā)中,經(jīng)常是通過 localhost 提供的頁面進行嚴重。
此時,我們可以通過 chrome://flags/#unsafely-treat-insecure-origin-as-secure,并指定要將不安全的起源視為安全起源。
Service Worker開發(fā)輔助工具
迄今為止,測試Service Worker的最有效方法是依賴于無痕窗口,例如 Chrome 中的無痕窗口。每次打開無痕窗口時,我們都是從頭開始的。沒有活動的Service Worker,也沒有打開的緩存實例。這種測試的常規(guī)流程如下:
- 打開一個無痕瀏覽窗口。
- 轉(zhuǎn)到注冊了Service Worker的頁面。
- 驗證Service Worker是否按我們的預期工作。
- 關閉無痕窗口。
- 重復。
通過這個過程,我們模擬了Service Worker的生命周期。
Chrome DevTools 應用程序面板中提供的其他測試工具也可以幫助,盡管它們可能在某些方面修改了Service Worker的生命周期。
圖片
應用程序面板有一個名為Service Workers的面板,顯示了當前頁面的活動Service Worker。每個活動Service Worker都可以手動更新,甚至完全注銷。面板頂部還有三個開關按鈕,有助于開發(fā)。
- Offline(離線):模擬離線條件。這有助于測試當前是否有活動Service Worker提供脫機內(nèi)容。
- Update on reload(重新加載時更新):當切換開啟時,每次重新加載頁面時都會重新獲取并替換當前的Service Worker。
- Bypass for network(繞過網(wǎng)絡):切換開啟時,會繞過Service Worker的 fetch 事件中的任何代碼,并始終從網(wǎng)絡獲取內(nèi)容。
這些開關非常有幫助,特別是Bypass for network,當我們正在開發(fā)一個具有活動Service Worker的項目時,同時還希望確保體驗在沒有Service Worker的情況下也能按預期工作。
強制刷新
當在本地開發(fā)中使用活動的Service Worker,而不需要更新后刷新或繞過網(wǎng)絡功能時,按住 Shift 鍵并單擊刷新按鈕也非常有用。
這個操作的鍵盤變體涉及在 macOS 計算機上按住 Shift、Cmd 和 R 鍵。
這被稱為「強制刷新」,它繞過 HTTP 緩存以獲取網(wǎng)絡數(shù)據(jù)。當Service Worker處于活動狀態(tài)時,強制刷新也將完全繞過Service Worker。
如果不確定特定緩存策略是否按預期工作,或者希望從網(wǎng)絡獲取所有內(nèi)容以比較有Service Worker和無Service Worker時的行為,這個功能非常有用。更好的是,這是一個規(guī)定的行為,因此所有支持Service Worker的瀏覽器都會觀察到它。
檢查緩存內(nèi)容
如果無法檢查緩存,就很難確定緩存策略是否按預期工作。Chrome DevTools 的應用程序面板提供了一個子面板,用于檢查緩存實例的內(nèi)容。
在DevTools中檢查緩存
這個子面板通過提供以下功能來使Service Worker開發(fā)變得更容易:
- 查看緩存實例的名稱。
- 檢查緩存資產(chǎn)的響應正文以及它們關聯(lián)的響應標頭。
- 從緩存中清除一個或多個項目,甚至刪除整個緩存實例。
這個圖形用戶界面使檢查Service Worker緩存更容易,以查看項目是否已添加、更新或從Service Worker緩存中完全刪除。
模擬存儲配額
在擁有大量大型靜態(tài)資產(chǎn)(如高分辨率圖像)的網(wǎng)站中,可能會觸及存儲配額。當這種情況發(fā)生時,瀏覽器將從緩存中驅(qū)逐它認為過時或值得犧牲以騰出空間以容納新資產(chǎn)的項目。
處理存儲配額應該是Service Worker開發(fā)的一部分,而 Workbox 使這個過程比自行管理更簡單。不管是否使用 Workbox,模擬自定義存儲配額以測試緩存管理邏輯可能是一個不錯的主意。
存儲使用查看器
Chrome DevTools 的 Application 面板中的存儲使用查看器。在這里,正在設置自定義存儲配額。
Chrome DevTools 的 Application 面板有一個存儲子面板,提供了有關頁面使用的當前存儲配額的信息。它還允許指定以兆字節(jié)為單位的自定義配額。一旦生效,Chrome 將執(zhí)行自定義存儲配額以進行測試。
這個子面板還包含一個清除站點數(shù)據(jù)按鈕以及一整套相關的復選框,用于在單擊按鈕時清除哪些內(nèi)容。其中包括任何打開的緩存實例,以及注銷控制頁面的任何活動Service Worker的能力。