「前置知識點」,只是做一個概念的介紹,不會做深度解釋。因為,這些概念在下面文章中會有出現(xiàn),為了讓行文更加的順暢,所以將本該在文內(nèi)的概念解釋放到前面來。「如果大家對這些概念熟悉,可以直接忽略」同時,由于閱讀我文章的群體有很多,所以有些知識點可能「我視之若珍寶,爾視只如草芥,棄之如敝履」。以下知識點,請「酌情使用」。

創(chuàng)新互聯(lián)是一家專注于成都網(wǎng)站建設(shè)、成都做網(wǎng)站與策劃設(shè)計,孝昌網(wǎng)站建設(shè)哪家好?創(chuàng)新互聯(lián)做網(wǎng)站,專注于網(wǎng)站建設(shè)十年,網(wǎng)設(shè)計領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:孝昌等地區(qū)。孝昌做網(wǎng)站價格咨詢:18982081108
要查看正在運(yùn)行的Service workers列表,我們可以在Chrome/Chromium中地址欄中輸入chrome://serviceworker-internals/。
圖片
chrome://xx 包含了很多內(nèi)置的功能,這塊也是有很大的說道的。后期,會單獨有一個專題來講。(已經(jīng)在籌劃準(zhǔn)備中....)
Cache API為緩存的 Request / Response 對象對提供存儲機(jī)制。例如,作為ServiceWorker 生命周期的一部分
Cache API像 workers 一樣,是暴露在 window 作用域下的。盡管它被定義在 service worker 的標(biāo)準(zhǔn)中,但是它不必一定要配合 service worker 使用。
「一個域可以有多個命名 Cache 對象」。我們需要在腳本 (例如,在 ServiceWorker 中) 中處理緩存更新的方式。
緩存配額使用估算值,可以使用 StorageEstimate API 獲得。
瀏覽器盡其所能去管理磁盤空間,但它有可能刪除一個域下的緩存數(shù)據(jù)。
瀏覽器要么自動刪除特定域的全部緩存,要么全部保留。
一些圍繞service worker緩存的重要 API 方法包括:
Cache.put, Cache.add和Cache.addAll只能在GET請求下使用。
更多詳情可以參考MDN-Cache[1]
如果我們以前沒有使用過Cache接口,可能會認(rèn)為它與 HTTP 緩存相同,或者至少與 HTTP 緩存相關(guān)。但實際情況并非如此。
可以將瀏覽器緩存看作是「分層的」。
Service workers是JavaScript層面的 API,「充當(dāng) Web 瀏覽器和 Web 服務(wù)器之間的代理」。它們的目標(biāo)是通過提供離線訪問以及提升頁面性能來提高可靠性。
Service workers是對現(xiàn)有網(wǎng)站的增強(qiáng)。這意味著如果使用Service workers的網(wǎng)站的用戶使用不支持Service workers的瀏覽器訪問網(wǎng)站,基本功能不會受到破壞。它是向下兼容的。
Service workers通過類似于桌面應(yīng)用程序的生命周期逐漸增強(qiáng)網(wǎng)站。想象一下當(dāng)從應(yīng)用商城安裝APP時會發(fā)生流程:
Service worker也采用類似的生命周期,但采用「漸進(jìn)增強(qiáng)」的方法。
Service worker技術(shù)中不可或缺的一部分是Cache API,這是一種「完全獨立于 HTTP 緩存的緩存機(jī)制」。Cache API可以在Service worker作用域內(nèi)和「主線程」作用域內(nèi)訪問。該特性為用戶操作與 Cache 實例的交互提供了許多可能性。
這意味著可以根據(jù)網(wǎng)站的特有的邏輯來緩存網(wǎng)絡(luò)請求的響應(yīng)。例如:
這些都是緩存策略的應(yīng)用方向。緩存策略使離線體驗成為可能,并「通過繞過 HTTP 緩存觸發(fā)的高延遲重新驗證檢查提供更好的性能」。
在「網(wǎng)絡(luò)上傳輸數(shù)據(jù)本質(zhì)上是異步的」。請求資產(chǎn)、服務(wù)器響應(yīng)請求以及下載響應(yīng)都需要時間。所涉及的時間是多樣且不確定的。Service workers通過「事件驅(qū)動」的 API 來適應(yīng)這種異步性,「使用回調(diào)處理事件」,例如:
都可以使用addEventListener API 注冊事件。所有這些事件都可以與Cache API進(jìn)行交互。特別是在網(wǎng)絡(luò)請求是離散的,運(yùn)行回調(diào)的能力對于「提供所期望的可靠性和速度」至關(guān)重要。
在JavaScript中進(jìn)行異步工作涉及使用Promises。因為Promises也支持async和await,這些JavaScript特性也可用于簡化Service worker代碼,從而提供更好的開發(fā)者體驗。
Service worker與Cache實例之間的交互涉及兩個不同的緩存概念:
預(yù)緩存是需要提前緩存資源的過程,通常在Service worker「安裝期間」進(jìn)行。通過預(yù)緩存,「關(guān)鍵的靜態(tài)資產(chǎn)和離線訪問所需的材料可以被下載并存儲在 Cache 實例中」。這種類型的緩存還可以提高需要預(yù)緩存資源的后續(xù)頁面的頁面速度。
運(yùn)行時緩存是指在運(yùn)行時從網(wǎng)絡(luò)請求資源時應(yīng)用緩存策略。這種類型的緩存非常有用,因為它保證了用戶已經(jīng)訪問過的頁面和資源的離線訪問。
當(dāng)在Service worker中使用這些方法時,可以為用戶體驗提供巨大的好處,并為普通的網(wǎng)頁提供類似應(yīng)用程序的行為。
Service workers與Web workers類似,它們的「所有工作都在自己的線程上進(jìn)行」。這意味著Service workers的任務(wù)不會與主線程上的其他任務(wù)競爭。
我們就以Web Worker為例子,做一個簡單的演示 在JavaScript中創(chuàng)建Web Worker并不是一項復(fù)雜的任務(wù)。
創(chuàng)建一個新的JavaScript文件,其中包含我們希望在工作線程中運(yùn)行的代碼。此文件不應(yīng)包含對DOM的任何引用,因為它將無法訪問DOM。
在我們的主JavaScript文件中,使用Worker構(gòu)造函數(shù)創(chuàng)建一個新的Worker對象。此構(gòu)造函數(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)在讓我們運(yùn)行Web應(yīng)用程序并測試Worker。我們應(yīng)該在控制臺中看到打印的消息,指示主線程和工作線程之間已發(fā)送和接收消息。
圖片
在深入了解service worker的生命周期之前,我們先來了解一下與生命周期運(yùn)作相關(guān)的「術(shù)語」(黑話)
了解service worker運(yùn)作方式的關(guān)鍵在于理解「控制」(control)。
一個service worker的作用域由其「在 Web 服務(wù)器上的位置確定」。如果一個service worker在位于/A/index.html的頁面上運(yùn)行,并且位于/A/sw.js上,那么該service worker的作用域就是/A/。
作用域限制了service worker控制的頁面。在上面的例子中,這意味著從/subdir/sw.js加載的service worker只能「控制位于/subdir/或其子頁面中」。
控制頁面的service worker仍然可以「攔截任何網(wǎng)絡(luò)請求」,包括跨域資源的請求。作用域限制了由service worker控制的頁面。
上述是默認(rèn)情況下作用域工作的方式,但可以通過設(shè)置Service-Worker-Allowed響應(yīng)頭,以及通過向register方法傳遞作用域選項來進(jìn)行覆蓋。
除非有很好的理由將service worker的作用域限制為origin的子集,否則應(yīng)「從 Web 服務(wù)器的根目錄加載service worker,以便其作用域盡可能廣泛」,不必?fù)?dān)心Service-Worker-Allowed頭部。
當(dāng)說一個service worker正在控制一個頁面時,實際上「是在控制一個客戶端」??蛻舳耸侵窾RL位于該service worker作用域內(nèi)的「任何打開的頁面」。具體來說,這些是WindowClient的實例。
圖片
為了使service worker能夠控制頁面,首先必須將其部署。
讓我們看看一個沒有service worker的網(wǎng)站到部署全新service worker時,中間發(fā)生了啥?
注冊是service worker生命周期的「初始步驟」:
此代碼在「主線程」上運(yùn)行,并執(zhí)行以下操作:
還有一些關(guān)鍵要點:
一旦注冊完成,「安裝」就開始了。
service worker在注冊后觸發(fā)其install事件。install「只會在每個service worker中調(diào)用一次,直到它被更新才會再次觸發(fā)」。可以使用addEventListener在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)進(jìn)行「預(yù)緩存」。其中有一個event.waitUntil。event.waitUntil接受一個Promise,并等待該P(yáng)romise被解決。
在這個示例中,這個Promise執(zhí)行兩個異步操作:
如果傳遞給event.waitUntil的Promise被「拒絕,安裝將失敗」。如果發(fā)生這種情況,service worker將被「丟棄」。
如果Promise被解決,安裝成功,service worker的狀態(tài)將更改為installed,然后進(jìn)入「激活」階段。
如果注冊和安裝成功,service worker將被「激活」,其狀態(tài)將變?yōu)閍ctivating。在service worker的activate事件中可以進(jìn)行激活期間的工作。在此事件中的一個典型任務(wù)是「清理舊緩存」,但對于「全新 service worker」,目前還不相關(guān)。
對于新的service worker,「安裝成功后,激活會立即觸發(fā)」。一旦激活完成,service worker的狀態(tài)將變?yōu)閍ctivated。
默認(rèn)情況下,新的service worker直到「下一次導(dǎo)航或頁面刷新之前才會開始控制頁面」。
一旦部署了第一個service worker,它很可能需要在以后進(jìn)行更新。例如,如果請求處理或預(yù)緩存邏輯發(fā)生了變化,就可能需要進(jìn)行更新。
瀏覽器會在以下情況下檢查service worker的更新:
了解瀏覽器何時更新service worker很重要,但“如何”也很重要。假設(shè)service worker的URL或作用域未更改,「只有在其內(nèi)容發(fā)生變化時,當(dāng)前安裝的service worker才會更新到新版本」。
瀏覽器以幾種方式檢測變化:
為確保瀏覽器能夠可靠地檢測service worker內(nèi)容的變化,「不要使用 HTTP 緩存保留它,也不要更改其文件名」。當(dāng)導(dǎo)航到service worker作用域內(nèi)的新頁面時,瀏覽器會自動執(zhí)行更新檢查。
關(guān)于更新,注冊邏輯通常不應(yīng)更改。然而,一個例外情況可能是「網(wǎng)站上的會話持續(xù)時間很長」。這可能在「單頁應(yīng)用程序」中發(fā)生,因為導(dǎo)航請求通常很少,應(yīng)用程序通常在應(yīng)用程序生命周期的開始遇到一個導(dǎo)航請求。在這種情況下,可以在「主線程上手動觸發(fā)更新」:
navigator.serviceWorker.ready.then((registration) => {
registration.update();
});對于傳統(tǒng)的網(wǎng)站,或者在用戶會話不持續(xù)很長時間的任何情況下,手動更新可能不是必要的。
當(dāng)使用打包工具生成「靜態(tài)資源」時,這些資源的「名稱中會包含哈希值」,例如framework.3defa9d2.js。假設(shè)其中一些資源被預(yù)緩存以供以后離線訪問,這將需要對service worker進(jìn)行更新以預(yù)緩存新的資源:
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事件示例有兩個方面不同:
更新后的service worker會與先前的service worker并存。這意味著舊的service worker仍然控制著任何打開的頁面。剛才安裝的新的service worker進(jìn)入等待狀態(tài),直到被激活。
默認(rèn)情況下,新的service worker將在「沒有任何客戶端由舊的service worker控制時激活」。這發(fā)生在相關(guān)網(wǎng)站的所有打開標(biāo)簽都關(guān)閉時。
當(dāng)安裝了新的service worker并結(jié)束了等待階段時,它會被激活,并丟棄舊的service worker。在更新后的service worker的activate事件中執(zhí)行的常見任務(wù)是「清理舊緩存」。通過使用caches.keys獲取所有打開的 Cache 實例的key,并使用caches.delete刪除不在允許列表中的所有舊緩存:
self.addEventListener("activate", (event) => {
// 指定允許的緩存密鑰
const cacheAllowList = ["前端柒八九_v2"];
// 獲取當(dāng)前活動的所有`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!
要有效使用service worker,有必要采用一個或多個緩存策略,這需要對Cache API有一定的了解。
緩存策略是service worker的fetch事件與Cache API之間的交互。如何編寫緩存策略取決于不同情況。
緩存策略的另一個重要的用途就是與service worker的fetch事件配合使用。我們已經(jīng)聽說過一些關(guān)于「攔截網(wǎng)絡(luò)請求」的內(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) => {
// 從緩存中響應(yīng)圖片,如果緩存中沒有,就從網(wǎng)絡(luò)獲取圖片
return cache.match(event.request).then((cachedResponse) => {
return (
cachedResponse ||
fetch(event.request.url).then((fetchedResponse) => {
// 將網(wǎng)絡(luò)響應(yīng)添加到緩存以供將來訪問。
// 注意:我們需要復(fù)制響應(yīng)以保存在緩存中,同時使用原始響應(yīng)作為請求的響應(yīng)。
cache.put(event.request, fetchedResponse.clone());
// 返回網(wǎng)絡(luò)響應(yīng)
return fetchedResponse;
})
);
});
})
);
} else {
return;
}
});上面的代碼執(zhí)行以下操作:
fetch事件的事件對象包含一個request屬性,其中包含一些有用的信息,可幫助我們識別每個請求的類型:
「異步操作是關(guān)鍵」。我們還記得install事件提供了一個event.waitUntil方法,它接受一個promise,并在激活之前等待其解析。fetch事件提供了類似的event.respondWith方法,我們可以使用它來返回異步fetch請求的結(jié)果或Cache接口的match方法返回的響應(yīng)。
展示了從頁面到service worker到緩存的流程。
「僅緩存」運(yùn)作方式:當(dāng)service worker控制頁面時,「匹配的請求只會進(jìn)入緩存」。這意味著為了使該模式有效,「任何緩存的資源都需要在安裝時進(jìn)行預(yù)緩存」,而「這些資源在service worker更新之前將不會在緩存中進(jìn)行更新」。
// 建立緩存名稱
const cacheName = "前端柒八九_v1";
// 要預(yù)緩存的資產(chǎn)
const preCachedAssets = ["/A.jpg", "/B.jpg", "/C.jpg", "/D.jpg"];
self.addEventListener("install", (event) => {
// 在安裝時預(yù)緩存資產(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) {
// 從緩存中獲取預(yù)緩存的資產(chǎn)
event.respondWith(
caches.open(cacheName).then((cache) => {
return cache.match(event.request.url);
})
);
} else {
// 轉(zhuǎn)到網(wǎng)絡(luò)
return;
}
});在上面的示例中,數(shù)組中的資產(chǎn)在安裝時被預(yù)緩存。當(dāng)service worker處理fetch請求時,我們「檢查fetch事件處理的請求 URL 是否在預(yù)緩存資產(chǎn)的數(shù)組中」。
圖片
「僅網(wǎng)絡(luò)」的策略與「僅緩存」相反,它將請求通過service worker傳遞到網(wǎng)絡(luò),而「不與 service worker 緩存進(jìn)行任何交互」。這是一種「確保內(nèi)容新鮮度」的好策略,但其權(quán)衡是「當(dāng)用戶離線時將無法正常工作」。
要確保請求直接通過到網(wǎng)絡(luò),只需「不對匹配的請求調(diào)用 event.respondWith」。如果我們想更明確,可以在要傳遞到網(wǎng)絡(luò)的請求的fetch事件回調(diào)中加入一個空的return;。這就是「僅緩存」策略演示中對于未經(jīng)預(yù)緩存的請求所發(fā)生的情況。
圖片
對于「匹配的請求」,流程如下:
// 建立緩存名稱
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) => {
// 如果我們有緩存的響應(yīng),則返回緩存的響應(yīng)
if (cachedResponse) {
return cachedResponse;
}
// 否則,訪問網(wǎng)絡(luò)
return fetch(event.request).then((fetchedResponse) => {
// 將網(wǎng)絡(luò)響應(yīng)添加到緩存以供以后訪問
cache.put(event.request, fetchedResponse.clone());
// 返回網(wǎng)絡(luò)響應(yīng)
return fetchedResponse;
});
});
})
);
} else {
return;
}
});盡管這個示例只涵蓋了圖像,但這是一個很好的范例,「適用于所有靜態(tài)資產(chǎn)」(如CSS、JavaScript、圖像和字體),「尤其是哈希版本的資產(chǎn)」。它「通過跳過 HTTP 緩存可能啟動的任何與服務(wù)器的內(nèi)容新鮮度檢查,為不可變資產(chǎn)提供了速度提升」。更重要的是,「任何緩存的資產(chǎn)都將在離線時可用」。
它的含義就是:
這種策略對于HTML或 API 請求非常有用,當(dāng)在線時,我們希望獲取資源的最新版本,但希望在離線時能夠訪問最新可用的版本。
// 建立緩存名稱
const cacheName = "前端柒八九_v1";
self.addEventListener("fetch", (event) => {
// 檢查這是否是導(dǎo)航請求
if (event.request.mode === "navigate") {
// 打開緩存
event.respondWith(
caches.open(cacheName).then((cache) => {
// 首先通過網(wǎng)絡(luò)請求
return fetch(event.request.url)
.then((fetchedResponse) => {
cache.put(event.request, fetchedResponse.clone());
return fetchedResponse;
})
.catch(() => {
// 如果網(wǎng)絡(luò)不可用,從緩存中獲取
return cache.match(event.request.url);
});
})
);
} else {
return;
}
});在需要重視離線功能,但又需要平衡該功能與獲取一些標(biāo)記或 API 數(shù)據(jù)的最新版本的情況下,「網(wǎng)絡(luò)優(yōu)先,備用緩存」是一種實現(xiàn)這一目標(biāo)的可靠策略。
圖片
「陳舊時重新驗證」策略是其中最復(fù)雜的。該策略的過程「優(yōu)先考慮了資源的訪問速度」,同時在后臺保持其更新。該策略的工作流程如下:
這是一個適用于「需要保持更新但不是絕對必要的資源」的策略,比如網(wǎng)站的頭像。它們會在用戶愿意更新時進(jìn)行更新,但不一定需要在每次請求時獲取最新版本。
// 建立緩存名稱
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;
}
});如果將預(yù)緩存「應(yīng)用于太多的資產(chǎn)」,或者如果Service Worker在頁面「完成加載關(guān)鍵資產(chǎn)之前」就注冊了,那么可能會遇到問題。
當(dāng)Service Worker在「安裝期間預(yù)緩存資產(chǎn)時,將同時發(fā)起一個或多個網(wǎng)絡(luò)請求」。如果時機(jī)不合適,這可能會對用戶體驗產(chǎn)生問題。即使時機(jī)剛剛好,如果未對預(yù)緩存資產(chǎn)的「數(shù)量進(jìn)行限制」,仍可能會浪費數(shù)據(jù)。
如果Service Worker預(yù)緩存任何內(nèi)容,那么它的注冊時機(jī)很重要。Service Worker通常使用內(nèi)聯(lián)的