[FE] Service Worker、Web Worker 和 Shared Worker

Todd
發佈於FE
2024/02/29閱讀時間約 21 分鐘
Service Worker、Web Worker 和 Shared Worker

Service 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));
});

Life Cycle

  • install 每個 Service Worker 只會執行一次,直到 Service Worker 更新
  • activate Service Worker 準備好控制 Client Site
  • activated完成 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。

Update event

Service Worker 會在以下情況觸發 update 事件。

  • 使用者瀏覽的 url 在 Service Worker scope 內
  • 更改 scope 或是 Service Worker 的路徑 (不推薦)
  • 在過去 24H 內有觸發過 sync  或是 push  事件
  • 手動觸發
    • navigator.serviceWorker.ready.then((registration) => {
      registration.update();
      });

觸發更新後,Service Worker 將會下載新版本的 Service Worker 但並不會立刻激活,直到沒有打開的頁面是由舊的 Service Worker 控制時才會被激活。

Cache

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。


Cache Only

raw-image

所有的 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;
}
});

Network Only

raw-image

與 Cache Only 完全相反,沒有 Cache 只向 Network 獲取 Resource。

Cache First, falling back to network

raw-image

如果 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 First, falling back to cache

raw-image

優先向 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。

State-while-revalidate

優先考量獲取資源的速度,並在後台進行更新。

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;
});
})
);
});

適合用於需要保持資源狀態在最新,但並不是最重要的資料。

其他的用法

Web Push API

由 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:[email protected]",
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());
});


Background Sync

當使用者網路不穩或是在 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 的頻率。

Web Worker

從瀏覽器建立多個 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 後才會關閉。瀏覽器中則會直接中斷關閉。

Shared Worker

Web Worker 的一種,可以在不同的頁面相同 Domain 分享訊息(某一頁面登入,其餘頁面皆改變狀態),或是節省資源(一個 Domain 只建立一個 Websocket 連線)。


0會員
2內容數
FE Developer
留言0
查看全部
發表第一個留言支持創作者!