使用 Service Worker 讓首頁秒開
我們可以用 Stale-While-Revalidate 加速頁面訪問,策略分 3 步
-
在收到頁面請求時首先檢查緩存,如果命中緩存就直接從緩存中返回給用戶
-
將緩存返回用戶的同時,在後臺異步發起網絡請求,嘗試獲取資源的最新版本
-
獲取成功後更新緩存,下次使用
而這一切的幕後功臣便是 Service Worker,作爲一個後臺代理在網絡與緩存之間搭建橋樑,提供了豐富的緩存管理和資源控制能力,從而實現這一高效策略
爲了實現這一策略,需要首先了解一下 Service Worker 的核心 API
Service 基礎概念
Service Worker 基礎概念可以在這裏瞭解
此處爲語雀內容卡片,點擊鏈接查看:www.yuque.com/sunluyong/f…[1]
攔截修改 Response 對象
使用 event.respondWith
可以在 fetch 事件中攔截網絡請求並提供自定義響應,一旦調用瀏覽器會等待提供的 Promise 解析,並將其結果作爲響應返回給發起請求的代碼
self.addEventListener('fetch', event => {
event.respondWith(
/* 自定義響應邏輯 */
);
});
比如實現攔截特定請求,可以首先嚐試從緩存中獲取資源,如果緩存命中則返回緩存內容,否則從網絡獲取資源並緩存
self.addEventListener('fetch', event => {
// 過濾非頁面請求
const url = new URL(event.request.url);
if (!url.pathname.startsWith('/page/')) return;
event.respondWith(
caches.match(event.request) // 嘗試匹配緩存
.then(cachedResponse => {
if (cachedResponse) {
return cachedResponse; // 緩存命中,返回緩存內容
}
// 緩存未命中,從網絡獲取
return fetch(event.request);
})
);
});
必須在 fetch 事件監聽器內部的第一時間調用 event.respondWith
,否則瀏覽器將繼續使用默認的網絡請求處理方式
clone Response 對象緩存
在 Service Worker 中處理網絡請求和緩存時,經常會遇到需要 clone 響應對象
const responseToCache = networkResponse.clone();
這是由於 Response
對象是一個可讀流,而流具有以下特性
-
單次消費:Streams 在被消費後就會關閉,不能重新讀取
-
節省資源:適合處理大型數據,如視頻流、文件下載等
當讀取 Response 的 body 返回給瀏覽器後,Stream 會被讀取並關閉,之後無法再次讀取用於緩存。通過 clone Response 對象,可以創建一個獨立的副本,確保每個副本的 Stream 都可單獨消費
fetch(event.request).then(networkResponse => {
// 克隆響應用於緩存
const responseToCache = networkResponse.clone();
// 返回給客戶端
event.respondWith(networkResponse);
// 緩存克隆的響應
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseToCache);
});
});
event.waitUntil 確保異步任務完成
Service Worker 事件都是異步的,瀏覽器可能在這些異步操作完成之前終止 Service Worker,導致關鍵任務(如緩存資源或清理舊緩存)無法正確完成
通過調用 event.waitUntil(promise)
,可以告訴瀏覽器要 “等待” 某個 Promise
完成之後,才認爲事件處理完成,這確保了瀏覽器不會在關鍵異步操作完成之前終止 Service Worker
比如在激活階段,通常需要清理舊的緩存
self.addEventListener('activate', event => {
console.log('[Service Worker] Activate Event');
const cacheWhitelist = ['my-cache-v2'];
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (!cacheWhitelist.includes(cacheName)) {
console.log(`[Service Worker] Deleting old cache: ${cacheName}`);
return caches.delete(cacheName);
}
})
);
})
});
瀏覽器可能在異步緩存清理任務完成之前終止激活過程,導致舊緩存可能未被正確刪除,使用 event.waitUntil 可以確保所有清理操作完成
self.addEventListener('activate', event => {
console.log('[Service Worker] Activate Event');
const cacheWhitelist = ['my-cache-v2'];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (!cacheWhitelist.includes(cacheName)) {
console.log(`[Service Worker] Deleting old cache: ${cacheName}`);
return caches.delete(cacheName);
}
})
);
})
);
});
Stale-While-Revalidate 實現
1. 創建目錄結構
.
├── app
│ └── index.js
├── package.json
└── public
├── favicon.ico
├── index.html
└── sw.js
因爲 Service Worker 需要服務端配合,爲了簡單使用 express 演示
npm install --save express
2. 提供 web 服務
修改 app/index.js,public 目錄對外服務,爲了演示緩存更新效果,添加了一個帶有頁面版本號的自定義響應頭 x-page-version
const express = require('express');
const path = require('path');
const app = express();
const port = 3000;
app.use(express.static(path.join(__dirname, '../public'), {
setHeaders: (res) => {
// mock,每隔 5s version 就發生變化
res.set('x-page-version', Math.ceil(Date.now() / 5000));
}
}));
// 啓動服務器
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
3. Service Worker 實現
首先是最基礎的安裝、激活,代碼量並不大,主要是添加了很多 log 方便觀測 Service Worker 執行過程
const CACHE_NAME = 'HOMEPAGE_CACHE_v1'; // 緩存 key,sw.js 更新了可以升級版本
// 配置需要緩存的資源,demo 中只緩存主文檔,靜態資源瀏覽器自己就會緩存
const urlsToCache = [
'/',
];
// 安裝事件:預緩存一些關鍵資源
self.addEventListener('install', (event) => {
console.log('[Service Worker] Install Event');
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('[Service Worker] Caching pre-defined resources');
return cache.addAll(urlsToCache);
}).catch((error) => {
console.error('[Service Worker] Failed to cache resources during install:', error);
})
);
});
// 激活事件:清理舊版本的緩存
self.addEventListener('activate', (event) => {
console.log('[Service Worker] Activate Event');
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (!cacheWhitelist.includes(cacheName)) {
console.log(`[Service Worker] Deleting old cache: ${cacheName}`);
return caches.delete(cacheName);
}
})
);
}).then(() => self.clients.claim()) // 確保 SW 控制所有客戶端
);
});
4. 劫持頁面請求
// 獲取事件:實現 "Stale-While-Revalidate" 策略
self.addEventListener('fetch', (event) => {
const requestUrl = new URL(event.request.url);
// 僅處理需要緩存的請求
if (!urlsToCache.includes(requestUrl.pathname)) return;
// 處理 fetch 事件
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
// 如果緩存存在,立即返回緩存內容
console.log(`[Service Worker] Serving from cache: ${event.request.url}`);
// 後臺發起網絡請求以更新緩存
event.waitUntil(
fetch(event.request).then((networkResponse) => {
if (networkResponse && networkResponse.status === 200) {
return caches.open(CACHE_NAME).then((cache) => {
// 緩存最新內容,下次使用
cache.put(event.request, networkResponse.clone());
console.log(`[Service Worker] Fetched and cached (background): ${event.request.url}`);
});
}
}).catch((error) => {
console.error(`[Service Worker] Background fetch failed for: ${event.request.url}`, error);
})
);
return cachedResponse; // 立即返回緩存內容
}
// 如果緩存不存在,從網絡獲取最新資源
return fetch(event.request).catch((error) => {
console.error(`[Service Worker] Fetch failed for: ${event.request.url}`, error);
});
})
);
});
這樣就基本實現了 Stale-While-Revalidate
5. 註冊 Service Worker
在主線程激活 Service Worker
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js").then(registration => {
console.log(`Service Worker registered with scope: ${registration.scope}`);
}).catch(error => {
console.log(`Service Worker registration failed: ${error}`);
});
}
更進一步
可以對上面 demo 改進一下,當獲取到最新版本頁面後和緩存對比,如果發現頁面版本已更新,可以給主線程發送通知,讓頁面重新發請求,獲取最新版本的緩存
更新 fetch 事件處理
// 獲取事件:實現 "Stale-While-Revalidate" 策略
self.addEventListener('fetch', (event) => {
const requestUrl = new URL(event.request.url);
// 僅處理需要緩存的請求
if (!urlsToCache.includes(requestUrl.pathname)) return;
// 處理 fetch 事件
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
// 如果緩存存在,立即返回緩存內容
console.log(`[Service Worker] Serving from cache: ${event.request.url}`);
// 後臺發起網絡請求以更新緩存
event.waitUntil(
fetch(event.request).then((networkResponse) => {
if (networkResponse && networkResponse.status === 200 && networkResponse.type === 'basic') {
// 獲取緩存響應中的版本
const cachedVersion = cachedResponse.headers.get('x-page-version');
// 獲取網絡響應中的版本
const networkVersion = networkResponse.headers.get('x-page-version');
console.log(`[Service Worker] Cached Version: ${cachedVersion}`);
console.log(`[Service Worker] Network Version: ${networkVersion}`);
// 如果頁面版本已更新
if (networkVersion !== cachedVersion) {
return caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, networkResponse.clone());
console.log(`[Service Worker] Fetched and cached (background): ${event.request.url}`);
// 通知客戶端刷新,展示最新內容
return sendMessage({
version: networkVersion,
action: 'update',
url: event.request.url,
});
});
}
}
}).catch((error) => {
console.error(`[Service Worker] Background fetch failed for: ${event.request.url}`, error);
})
);
return cachedResponse; // 立即返回緩存內容
}
// 如果緩存不存在,從網絡獲取最新資源
return fetch(event.request).catch((error) => {
console.error(`[Service Worker] Fetch failed for: ${event.request.url}`, error);
});
})
);
});
// 輔助函數:發送消息給客戶端
function sendMessage(data) {
return self.clients.matchAll().then((clients) => {
clients.forEach((client) => {
client.postMessage(data);
});
});
}
更新主線程,添加接收來自 Service Worker 消息事件
navigator.serviceWorker.addEventListener("message", event => {
console.log('Received a message from Service Worker:', event.data);
if (event.data.action === "update") {
if (event.data.url === window.location.href ) {
console.log('load lasted version');
location.href = event.data.url;
}
}
});
這就是 alibaba.com[2] 秒開的祕籍
作者:謙行
https://juejin.cn/post/7443159206318686246
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/oa92SRlPNXqbpp57pAXwJw