Service Worker trong TypeScript
Bài viết này giải thích về Service Worker trong TypeScript.
Chúng tôi sẽ giải thích về Service Worker trong TypeScript, bao gồm cả ví dụ thực tế.
YouTube Video
Service Worker trong TypeScript
Service Worker là một 'proxy yêu cầu' nằm giữa trình duyệt và mạng. Nó cho phép chặn fetch, kiểm soát bộ nhớ đệm, hỗ trợ offline và xử lý nền (đồng bộ, push). Sử dụng TypeScript giúp đảm bảo an toàn kiểu và tăng khả năng bảo trì.
Thiết lập TypeScript
tsconfig.json
(Bật các loại WebWorker)
Hãy xem ví dụ về cách bật loại WebWorker trong tsconfig.json
.
1{
2 "compilerOptions": {
3 "target": "ES2020",
4 "module": "ES2020",
5 "lib": ["ES2020", "WebWorker"],
6 "moduleResolution": "Bundler",
7 "strict": true,
8 "noEmitOnError": true,
9 "outDir": "out",
10 "skipLibCheck": true
11 },
12 "include": ["sw.ts"]
13}
- Bằng cách thêm
WebWorker
vào mảnglib
, bạn có thể sử dụng các kiểu nhưServiceWorkerGlobalScope
. DOM
vàWebWorker
có các kiểu dữ liệu khác nhau, vì vậy việc tách riêng cấu hìnhtsconfig.json
cho trình duyệt (ứng dụng chính) vàService Worker
là thông lệ phổ biến.- Các tệp
Service Worker
cuối cùng sẽ được xuất ra đường dẫn khớp với phạm vi (scope) (thường là gốc trang web/sw.js
). - Vì lý do bảo mật, Service Worker chỉ chạy trên HTTPS (hoặc
localhost
).
Mã đăng ký bên phía trình duyệt
register-sw.ts
1// register-sw.ts
2async function registerServiceWorker() {
3 if (!('serviceWorker' in navigator)) return;
4
5 try {
6 const registration = await navigator.serviceWorker.register(
7 '/sw.js', { scope: '/' }
8 );
- Quá trình này sẽ đăng ký một
Service Worker
.scope
đề cập đến phạm vi các đường dẫn màService Worker
có thể kiểm soát. Ví dụ, nếu bạn đặt/sw.js
trực tiếp dưới thư mục gốc và đặtscope
là thư mục gốc (/
), bạn có thể kiểm soát tất cả tài nguyên của toàn bộ trang web. Ngược lại, nếu bạn chỉ định một thư mục cụ thể như/app/
, chỉ những nội dung trong thư mục đó sẽ được kiểm soát.
1 // If there's a waiting worker, notify the user.
2 if (registration.waiting) {
3 promptUserToUpdate(registration);
4 }
waiting
biểu thị trạng thái khi một Service Worker mới đã được cài đặt và đang chờ kích hoạt. Ở giai đoạn này, các trang hiện tại vẫn được kiểm soát bởiService Worker
cũ, nên thường sẽ hỏi ý kiến xác nhận người dùng, và sau khi được chấp thuận, gọiskipWaiting()
để ngay lập tức kích hoạt Service Worker mới. Điều này cho phép bạn cập nhật các thay đổi mới nhất mà không cần chờ tải lại trang kế tiếp.
1 // When a new SW is installing, monitor its state changes
2 registration.addEventListener('updatefound', () => {
3 const newWorker = registration.installing;
4 if (!newWorker) return;
5 newWorker.addEventListener('statechange', () => {
6 if (newWorker.state === 'installed' &&
7 navigator.serviceWorker.controller) {
8 // New content available, prompt the user
9 promptUserToUpdate(registration);
10 }
11 });
12 });
updatefound
được kích hoạt khi quá trình cài đặt một Service Worker mới bắt đầu. Khi sự kiện này xảy ra, một worker mới sẽ được đặt trongregistration.installing
, vì vậy bạn có thể giám sát sự thay đổi trạng thái (statechange
) để phát hiện khi cài đặt đã hoàn tất (installed
). Hơn nữa, nếunavigator.serviceWorker.controller
tồn tại, điều đó có nghĩa là một Service Worker cũ đã kiểm soát trang, vì vậy đây là cơ hội để thông báo cho người dùng về sự tồn tại của phiên bản mới.
1 // When the active worker changes (e.g., after skipWaiting), reload if desired
2 navigator.serviceWorker.addEventListener('controllerchange', () => {
3 // Optionally reload to let the new SW take over
4 window.location.reload();
5 });
6 } catch (err) {
7 console.error('Service Worker registration failed: ', err);
8 }
9}
- Sự kiện
controllerchange
sẽ được gọi tại thời điểm Service Worker mới bắt đầu kiểm soát trang hiện tại. Tải lại trang vào thời điểm này sẽ áp dụng ngay các quy trình và chiến lược bộ nhớ cache mới. Tuy nhiên, việc tự động tải lại có thể làm giảm trải nghiệm người dùng, vì vậy chỉ nên tải lại sau khi đã được sự đồng ý của người dùng.
1function promptUserToUpdate(reg: ServiceWorkerRegistration) {
2 // Show UI to user. If user accepts:
3 if (reg.waiting) {
4 reg.waiting.postMessage({ type: 'SKIP_WAITING' });
5 }
6}
7
8registerServiceWorker();
- Bằng cách cho
Service Worker
nhậnpostMessage({ type: 'SKIP_WAITING' })
từ phía client và sau đó gọiself.skipWaiting()
, bạn có thể kích hoạt việc cập nhật.
Khai báo Scope trong sw.ts
Tiếp theo, hãy xem ví dụ về Service Worker thực hiện bộ nhớ đệm app shell điển hình.
Khi sử dụng Service Worker trong TypeScript, việc gán đúng kiểu cho self
là rất hữu ích.
1// sw.ts
2export default null;
3declare const self: ServiceWorkerGlobalScope;
- Trong TypeScript,
self
mặc định được xem là kiểuany
, do đó nếu không khai báo kiểu bổ sung, bạn sẽ không có tính năng tự động gợi ý hoặc kiểm tra kiểu cho các API riêng của Service Worker nhưskipWaiting()
hoặcclients
. - Chỉ định
ServiceWorkerGlobalScope
giúp có gợi ý tự động, ngăn sử dụng sai và cho phép phát triển an toàn hơn tách biệt với script DOM thông thường.
Service Worker cơ bản (Cài đặt/Kích hoạt/Lấy dữ liệu)
Nó minh họa quản lý phiên bản cache đơn giản, precache khi cài đặt, xóa cache cũ khi kích hoạt và các chiến lược cache khi lấy dữ liệu (ưu tiên cache cho tài nguyên tĩnh, ưu tiên mạng cho API).
sw.ts
(Thiết lập tối thiểu + Bộ khung Cache)
1const CACHE_NAME = 'app-shell-v1';
2const STATIC_ASSETS = [
3 '/',
4 '/index.html',
5 '/styles.css',
6 '/main.js',
7 '/fallback.png'
8];
9
10self.addEventListener('install', (event: ExtendableEvent) => {
11 // Pre-cache application shell
12 event.waitUntil(
13 caches.open(CACHE_NAME)
14 .then(cache => cache.addAll(STATIC_ASSETS))
15 // Activate immediately (optional: coordinate with client)
16 .then(() => self.skipWaiting())
17 );
18});
- Trong sự kiện
install
, tài nguyên tĩnh (App Shell) của ứng dụng sẽ được lưu vào bộ nhớ đệm trước. Bằng cách gọiself.skipWaiting()
,Service Worker
mới sẽ được kích hoạt ngay lập tức, cho phép sử dụng bộ nhớ đệm mới nhất mà không cần chờ truy cập tiếp theo.
1self.addEventListener('activate', (event: ExtendableEvent) => {
2 // Clean up old caches and take control of clients immediately
3 event.waitUntil(
4 caches.keys().then(keys =>
5 Promise.all(keys
6 .filter(key => key !== CACHE_NAME)
7 .map(key => caches.delete(key)))
8 ).then(() => self.clients.claim())
9 );
10});
- Trong sự kiện
activate
, các phiên bản bộ nhớ đệm cũ sẽ bị xóa vàService Worker
luôn được cập nhật mới nhất. Ngoài ra, bằng cách gọiself.clients.claim()
,Service Worker
mới có thể kiểm soát tất cả các client mà không cần đợi trang tải lại.
1self.addEventListener('fetch', (event: FetchEvent) => {
2 const request = event.request;
3 const url = new URL(request.url);
4
5 // Navigation requests (SPA) -> network-first with fallback to cached index.html
6 if (request.mode === 'navigate') {
7 event.respondWith(
8 fetch(request).catch(() => caches.match('/index.html') as Promise<Response>)
9 );
10 return;
11 }
12
13 // Simple API routing: network-first for /api/
14 if (url.pathname.startsWith('/api/')) {
15 event.respondWith(networkFirst(request));
16 return;
17 }
18
19 // Static assets: cache-first
20 event.respondWith(cacheFirst(request));
21});
- Trong
fetch
, bạn có thể chặn yêu cầu và kiểm soát phản hồi. Bạn có thể thực hiện các chiến lược như ưu tiên cache hoặc ưu tiên mạng, hữu ích cho hỗ trợ offline và hiệu suất.
1self.addEventListener('message', (event: ExtendableMessageEvent) => {
2 const data = (event as any).data;
3 if (!data) return;
4
5 if (data.type === 'SKIP_WAITING') {
6 // Force the waiting service worker to become active
7 self.skipWaiting();
8 }
9});
- Nếu nhận được
SKIP_WAITING
, gọiself.skipWaiting()
sẽ kích hoạt ngay lập tức Service Worker đang chờ. Kết quả là phiên bản mới sẽ được áp dụng từ lần request tiếp theo mà không cần tải lại trang.
Tổng quan các chiến lược cache thực tế
cache-first
Cache-first kiểm tra cache trước và trả về phản hồi ngay nếu có. Nếu không có, nó lấy dữ liệu từ mạng và lưu vào cache. Điều này phù hợp với các tệp tĩnh.
1async function cacheFirst(request: Request): Promise<Response> {
2 const cache = await caches.open(CACHE_NAME);
3 const cached = await cache.match(request);
4 if (cached) {
5 return cached;
6 }
7 const response = await fetch(request);
8 if (response && response.ok) {
9 cache.put(request, response.clone());
10 }
11 return response;
12}
- Đoạn mã này minh họa cách triển khai cache-first. Nếu có cache, nó trả về; nếu không, sẽ lấy từ mạng và lưu vào cache. Phù hợp với tài nguyên tĩnh ít thay đổi như hình ảnh hoặc CSS.
network-first
Network-first ưu tiên mạng trước, nếu thất bại sẽ quay về cache. Phù hợp cho API nơi độ mới dữ liệu rất quan trọng.
1async function networkFirst(request: Request): Promise<Response> {
2 const cache = await caches.open(CACHE_NAME);
3 try {
4 const response = await fetch(request);
5 if (response && response.ok) {
6 cache.put(request, response.clone());
7 }
8 return response;
9 } catch (err) {
10 const cached = await cache.match(request);
11 if (cached) return cached;
12 return new Response(JSON.stringify({ error: 'offline' }), {
13 status: 503,
14 headers: { 'Content-Type': 'application/json' }
15 });
16 }
17}
- Đoạn mã này minh họa cách triển khai network-first. Nếu nhận được phản hồi mạng, nó lưu vào cache; nếu thất bại, trả về bản đã lưu trong cache. Phù hợp cho tài nguyên cần dữ liệu mới như bài báo hoặc phản hồi API.
stale-while-revalidate
stale-while-revalidate trả về dữ liệu từ bộ nhớ đệm trước và đồng thời cập nhật nó từ mạng trong nền. Điều này cân bằng giữa tốc độ phản hồi và độ mới.
1async function staleWhileRevalidate(request: Request, cacheName = CACHE_NAME): Promise<Response> {
2 const cache = await caches.open(cacheName);
3 const cachedResponse = await cache.match(request);
4 const networkFetch = fetch(request).then(networkResponse => {
5 if (networkResponse && networkResponse.ok) {
6 cache.put(request, networkResponse.clone());
7 }
8 return networkResponse;
9 }).catch(() => undefined);
10
11 // Return cached immediately if exists, otherwise wait network
12 return cachedResponse || (await networkFetch) || new Response('offline', { status: 503 });
13}
- Đoạn mã này trả về cache ngay nếu có, đồng thời lấy dữ liệu mới từ mạng ở chế độ nền để cập nhật cache. Mang lại phản hồi nhanh cho người dùng và sử dụng nội dung cập nhật cho lần truy cập tiếp theo, phù hợp với UI hoặc truyền dữ liệu nhẹ.
Tối ưu hóa luồng cập nhật (Thông báo và tải lại an toàn)
Các cập nhật của Service Worker
không xảy ra ngay lập tức; phiên bản mới sẽ chờ đến khi tất cả tab hiện tại được đóng lại.
Ở đây, chúng ta xây dựng hệ thống thông báo cho client khi bản mới đã sẵn sàng và tải lại trang an toàn dựa trên thao tác người dùng.
Thông báo cho client từ phía Service Worker
khi phiên bản mới đã sẵn sàng.
1// In sw.ts: after 'activate' or when new version is ready, broadcast a message
2async function notifyClientsUpdated() {
3 const all = await self.clients.matchAll({ type: 'window' });
4 for (const client of all) {
5 client.postMessage({ type: 'SW_UPDATED' });
6 }
7}
8
9// e.g., call this at the end of 'activate'
10self.addEventListener('activate', (event) => {
11 event.waitUntil((async () => {
12 if ('navigationPreload' in self.registration) {
13 await self.registration.navigationPreload.enable();
14 }
15 // cache cleanup
16 const cacheNames = await caches.keys();
17 await Promise.all(
18 cacheNames.map((name) => {
19 if (name !== CACHE_NAME) {
20 return caches.delete(name);
21 }
22 })
23 );
24
25 await self.clients.claim();
26 await notifyClientsUpdated();
27 })());
28});
- Trong đoạn mã này,
notifyClientsUpdated
được gọi vào cuối sự kiệnactivate
để thông báo cho tất cả client đã kết nối rằng phiên bản mới đã sẵn sàng.clients.claim()
là một phương thức giúp đưa các trang (client) đang mở vào sự kiểm soát của Service Worker mới kích hoạt ngay lập tức. Thông thường,Service Worker
chỉ bắt đầu kiểm soát trang ở lần tải lại kế tiếp, nhưng vớiclients.claim()
, bạn có thể kiểm soát lập tức mà không cần tải lại.
Hiển thị giao diện cập nhật trên client, và reload nhờ thao tác của người dùng
1// in app startup
2navigator.serviceWorker.addEventListener('message', (e) => {
3 if (e.data?.type === 'SW_UPDATED') {
4 // Show a non-intrusive toast or banner: "New version available"
5 // When user clicks "Reload", call:
6 window.location.reload();
7 }
8});
- Client nhận
SW_UPDATED
qua sự kiệnmessage
và hiển thị thông báo cập nhật trên UI. Khi người dùng chọn tải lại,window.location.reload()
sẽ được thực thi, cập nhật HTML, CSS và các tài nguyên khác trên trang lên phiên bản mới nhất. Điều này đảm bảo rằng bộ nhớ đệm và quyền kiểm soát củaService Worker
(sau khi dùngclients.claim()
) sẽ được áp dụng cho toàn bộ trang.
Dự phòng khi offline
Chuẩn bị /offline.html
cho điều hướng quan trọng và cung cấp giao diện tối giản vẫn truyền tải được ý nghĩa dù không có ảnh hoặc font. Nếu gọi API thất bại, hãy hiển thị trạng thái cache cuối cùng nếu có thể và thử refetch ở chế độ nền để cải thiện UX.
Ví dụ triển khai
1// sw.ts
2const CACHE_NAME = 'app-cache-v1';
3
4// Cache offline.html during install
5self.addEventListener('install', (event) => {
6 event.waitUntil((async () => {
7 const cache = await caches.open(CACHE_NAME);
8 await cache.addAll(['/offline.html']);
9 })());
10});
11
12// Handle fetch requests
13self.addEventListener('fetch', (event) => {
14 const request = event.request;
15
16 // Navigation requests (e.g., page transitions)
17 if (request.mode === 'navigate') {
18 event.respondWith((async () => {
19 try {
20 // Try to fetch from the network as usual
21 return await fetch(request);
22 } catch (err) {
23 // On failure, return offline fallback page
24 const cache = await caches.open(CACHE_NAME);
25 return await cache.match('/offline.html') as Response;
26 }
27 })());
28 }
29});
- Precache
/offline.html
trong sự kiệninstall
để luôn trả về một trang tối thiểu khi không có mạng. - Trong sự kiện
fetch
, bạn có thể theo dõi các yêu cầu chuyển hướng vớirequest.mode === 'navigate'
và đặc biệt nhắm mục tiêu tới các lần chuyển trang. - Chuyển sang
/offline.html
khi mạng thất bại, đảm bảo vẫn hiển thị nội dung khi offline.
Giao tiếp giữa client và Service Worker
.
Vì Service Worker
hoạt động độc lập với vòng đời của trang, nên nhắn tin hai chiều rất quan trọng để thông báo trạng thái và thực thi các lệnh. Việc xác định kiểu cho các tin nhắn giúp tránh gửi sai tin nhắn, hỗ trợ hoàn thiện mã và khiến việc triển khai của bạn chắc chắn hơn.
Ví dụ mã
- Định nghĩa các loại tin nhắn
1type SwToClient =
2 | { type: 'SW_READY' }
3 | { type: 'SW_UPDATED' }
4 | { type: 'CACHE_CLEARED' }
5 | { type: 'PING'; ts: number };
6
7type ClientToSw =
8 | { type: 'CLEAR_CACHE' }
9 | { type: 'PING'; ts: number };
SwToClient
là loại tin nhắn gửi từ Service Worker đến client.ClientToSw
là loại tin nhắn gửi từ client đến Service Worker.- Điều này giúp bạn làm rõ các loại sự kiện có thể trao đổi qua giao tiếp hai chiều.
- Xử lý phía Service Worker
1self.addEventListener('message', (event) => {
2 const data = event.data as ClientToSw;
3 if (data?.type === 'CLEAR_CACHE') {
4 event.waitUntil((async () => {
5 const keys = await caches.keys();
6 await Promise.all(keys.map((k) => caches.delete(k)));
7 await broadcast({ type: 'CACHE_CLEARED' });
8 })());
9 } else if (data?.type === 'PING') {
10 event.source?.postMessage({ type: 'PING', ts: data.ts } as SwToClient);
11 }
12});
- Service Worker nhận tin nhắn từ client và xử lý tương ứng theo từng loại.
- Với
CLEAR_CACHE
, nó xóa cache và thông báo cho tất cả client vớiCACHE_CLEARED
. - Với
PING
, nó trả lời cho client gốc bằng tin nhắnPING
kèm timestamp.
- Thông báo cho tất cả client từ Service Worker
1async function broadcast(msg: SwToClient) {
2 const clients = await self.clients.matchAll({ includeUncontrolled: true });
3 for (const c of clients) c.postMessage(msg);
4}
- Dùng
clients.matchAll
để lấy tất cả tab cửa sổ. - Bằng cách gửi
postMessage
cho từng tab, bạn có thể phát broadcast tin nhắn. - Điều này có thể dùng để thông báo cập nhật (như
SW_UPDATED
) và thông báo lỗi.
- Xử lý phía client
1navigator.serviceWorker.controller?.postMessage({
2 type: 'PING',
3 ts: Date.now()
4} as ClientToSw);
- Bằng cách gửi
PING
từ client và nhận phản hồi từService Worker
, bạn có thể xác minh rằng giao tiếp hai chiều hoạt động đúng. Điều này giúp kiểm tra trạng thái kết nối và xử lý tin nhắn dễ dàng hơn.
1navigator.serviceWorker.addEventListener('message', (e) => {
2 const msg = e.data as SwToClient;
3 switch (msg.type) {
4 case 'SW_READY':
5 console.log('Service Worker is ready');
6 // Example: hide loading spinner or enable offline UI
7 break;
8 case 'SW_UPDATED':
9 console.log('A new version of the Service Worker is available');
10 // Example: show update notification or reload prompt
11 const shouldReload = confirm('A new version is available. Reload now?');
12 if (shouldReload) {
13 window.location.reload();
14 }
15 break;
16 case 'CACHE_CLEARED':
17 console.log('Cache cleared');
18 // Example: show confirmation message to user
19 alert('Cache has been successfully cleared.');
20 break;
21 case 'PING':
22 console.log(`Received PING response, ts=${msg.ts}`);
23 break;
24 }
25});
- {^ i18n_speak
クライアント側では
Service Worker
から送信されるメッセージを受信し、種類に応じて処理を分岐します。SW_READY
は初期化完了、SW_UPDATED
は新バージョン検出、CACHE_CLEARED
はキャッシュ削除完了、PING
は通信確認を示します。各メッセージに応じて、UI の更新やリロード、通知表示などを行います。^}
Lợi ích của giao tiếp có kiểu
- Sử dụng tin nhắn có kiểu giúp làm rõ loại tin nhắn có thể gửi và nhận, đồng thời gợi ý tự động và kiểm tra kiểu tăng độ an toàn.
postMessage
cho phép giao tiếp một-một, vàbroadcast
cho phép một-nhiều.- Bạn dễ dàng triển khai các tính năng quan trọng như thông báo cập nhật (
SW_UPDATED
), quản lý cache (CACHE_CLEARED
), kiểm tra sức khỏe kết nối (PING
).
Tóm tắt
- Sử dụng TypeScript mang lại an toàn kiểu cho các lệnh API và giao tiếp của Service Worker, cải thiện hiệu quả và khả năng bảo trì.
- Hiểu rõ các sự kiện vòng đời
install
,activate
,fetch
và chọn chiến lược cache phù hợp (như cache-first hoặc network-first) cho từng tình huống giúp nâng cao trải nghiệm người dùng. - Đối với vận hành, việc hiểu về quản lý version cache và luồng cập nhật (
updatefound
,waiting
,SKIP_WAITING
, v.v.) là rất quan trọng. - Bằng cách áp dụng nhắn tin có kiểu cho giao tiếp giữa client và
Service Worker
, bạn có thể ngăn ngừa việc thực hiện sai và thiết lập một hệ thống dễ mở rộng, bảo trì lâu dài.
Bạn có thể làm theo bài viết trên bằng cách sử dụng Visual Studio Code trên kênh YouTube của chúng tôi. Vui lòng ghé thăm kênh YouTube.