Service Worker、Web Worker 和 Shared Worker
可以當作 Client Side 的攔截器,獲取網站內容的請求判斷(像是 axios 的 interceptor 或是 nginx 的 proxy),依據條件進行動作,可以使用 Cache Storage 和 IndexDB,基於事件驅動,並有自己的生命週期。註冊 Service Worker
'serviceWorker' in navigator &&
window.addEventListener('load', () => {
navigator.serviceWorker.register(new URL('./sw.js', import.meta.url));
});
install
每個 Service Worker 只會執行一次,直到 Service Worker 更新activate
Service Worker 準備好控制 Client Siteactivated
完成 activate
後觸發的 event透過 Life Cycle 進行 precaching 的範例
self.addEventListener('install', (event) => {
const cacheKey = '...';
event.waitUntil(caches.open(cacheKey).then((cache) => {
return cache.addAll([
...
]);
}));
});
在 install 時,先進行 precaching 並透過 event.waitUntil
處理非同步的 function。
self.addEventListener('activate', (event) => {
const cacheAllowList = [...];
event.waitUntil(caches.keys().then((keys) => {
return Promise.all(keys.map((key) => {
if (!cacheAllowList.includes(key)) {
return caches.delete(key);
}
}));
}));
});
在 activate 時刪除(修改) cache。
Service Worker 會在以下情況觸發 update
事件。
sync
或是 push
事件navigator.serviceWorker.ready.then((registration) => {
registration.update();
});
觸發更新後,Service Worker 將會下載新版本的 Service Worker 但並不會立刻激活,直到沒有打開的頁面是由舊的 Service Worker 控制時才會被激活。
Service Worker 可以搭配 JS 的 Cache Interface 建立 Cache,與 HTTP 的 Cache Header 並不相同,JS 的 Cache Interface 在較高的層級,完全獨立於 HTTP Cache。例如建立圖片的 Cache
self.addEventListener("fetch", (event) => {
if (event.request.destination === "image") {
event.responseWith(
cache.open(cacheName).then((cache) => {
return cache.match(event.request).then((cacheResponse) => {
if (cacheResponse) {
return cacheResponse;
}
return fetch(event.request.url).then((fetchResponse) => {
cache.put(event.request, fetchResponse.clone());
return fetchResponse;
});
});
})
);
}
});
Service Worker 搭配 Cache Interface 可以實現不同的 Cache Strategies。
所有的 Resource 只透過 Cache 獲得,一開始進行資源的 Precaching ,直到 Service Worker 更新後才會進行更新。
// asset request url
const precachedAssets = [
'/pic1.jpg',
'/pic2.jpg',
'/pic3.jpg'
];
self.addEventListener('install', (event) => {
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) {
event.responseWith(cache.open(cacheName).then(cache => {
return cache.match(event.request.url);
}));
} else {
// go to the network
return;
}
});
與 Cache Only 完全相反,沒有 Cache 只向 Network 獲取 Resource。
如果 Cache 沒有我們要的資源則從 Network 獲取,有的話優先從 Cache 獲得。
self.addEventListener("fetch", (event) => {
event.reponseWith(
caches.open(cacheName).then((cacheResponse) => {
if (cacheResponse) {
return cacheResponse;
}
return fetch(event.request).then((fetchResponse) => {
cache.put(event.request, fetchResponse.clone());
return fetchResponse;
});
})
);
});
適合用於 CSS, JavaScript, Image, Font 會經過 hash 的靜態資源。
優先向 Network 獲取資源,並將獲得的 Response 儲存至 Cache,如果是在 Offline 的情況下,則透過 Cache 獲得資料。
self.addEventListener("fetch", (event) => {
event.responseWith(
caches.open(cacheName).then((cache) => {
return fetch(event.request.url)
.then((fetchResponse) => {
cache.put(event.request, fetchResponse.clone());
return fetchResponse;
})
.cache(() => {
return cache.match(event.request.url);
});
})
);
});
適合希望獲取最新的資料,但若在 Offline 的時候可以獲取最新的 Cache,例如 HTML 或是 API。
優先考量獲取資源的速度,並在後台進行更新。
self.addEventListener("fetch", (event) => {
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;
});
})
);
});
適合用於需要保持資源狀態在最新,但並不是最重要的資料。
由 Client Side 向第三方 Server(Mozila, Google, Apple 等) 進行 Web Push 的註冊,Client Side 會帶著 VAPID Public Key 傳送給 Server,之後將獲取到的訂閱訊息傳送給 Bacdend。
async () => {
const notificationPermission = await Notification.requestPermission();
if (notificationPermission === "granted") {
const registration = await navigator.serviceWorker.ready;
const notificationSub = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey:
"...", // VAPID public key
});
await fetch("BE Server", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(notificationSub),
});
}
})();
Backend 透過獲取到的訂閱資訊和 VAPID Private Key 發送 Request。
const vapidKeys = {
privateKey: "VAPID Private Key",
publicKey: "VAPID Public Key",
};
webPush.setVapidDetails(
"mailto:test@localhost.com",
vapidKeys.publicKey,
vapidKeys.privateKey
);
webPush.sendNotification(
req.body,
JSON.stringify({ title: "...", data: "..." })
);
第三方的 Web Push Server 會發送 Notification 給 Service Worker,由 Service Worker 觸發顯示 Notification。
self.addEventListener("push", (event) => {
const data = event.data.json();
const showNotification = async () => {
await self.registration.showNotification(data.title, { body: data.data });
console.log("showNotification");
};
event.waitUntil(showNotification());
});
當使用者網路不穩或是在 Offline 的環境時,將資料進行緩存,等待到有網路的時候在發送請求。
<form id="form">
<input name="name" /> <input name="message" />{" "}
<button type="submit">Send</button>
</form>;
透過 Cache Interface 進行儲存,並註冊 sync
事件。
document.getElementById("form").addEventListener("submit", async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = { name: formData.get("name"), message: formData.get("message") };
const formCache = await caches.open("form");
await formCache.put("/data", new Response(JSON.stringify(data)));
if ("SyncManager" in window) {
const registration = await navigator.serviceWorker.ready;
registration.sync.register("form-sync");
}
});
在 Service Worker 中進行監聽並發送 Request。
self.addEventListener("sync", (event) => {
switch (event.tag) {
case "form-sync": {
async function submitFormData() {
const formCache = await caches.open("form");
const formDataRes = await formCache.match("/data");
const body = await formDataRes.json();
const resp = await fetch("http://localhost:3000/form-sync", {
method: "POST",
body: JSON.stringify(body),
headers: { "Content-Type": "application/json" },
});
const data = await resp.json();
console.log(data);
}
event.waitUntil(submitFormData());
break;
}
}
});
Period Background Sync
Interval 的 Background Sync,需要權限(User 有安裝 PWA)才可以進行操作,並依據 Site Engagement Score 來決定 sync 的頻率。
從瀏覽器建立多個 thread 處理任務,主要用於處理可能會使用到大量運算且並不希望影響到使用者的瀏覽體驗。建立 Web Worker。
const worker = new Worker(new URL('./workerPath.js', import.meta.url));
透過 postMessage
進行溝通。
// browser
worker.postMessage({ data: "from app.js" });
worker.addEventListener("message", (event) => {
console.log(event.data);
});
// worker
self.postMessage({ data: "hello from worker.js" });
self.addEventListener("message", (event) => {
console.log(event.data);
});
關閉 Worker。
// browser
worker.terminate();
//worker
self.close();
在 Worker 中關閉的話, Worker 會執行完 Event Loop 後才會關閉。瀏覽器中則會直接中斷關閉。
Web Worker 的一種,可以在不同的頁面相同 Domain 分享訊息(某一頁面登入,其餘頁面皆改變狀態),或是節省資源(一個 Domain 只建立一個 Websocket 連線)。