`Service Worker` trong JavaScript

`Service Worker` trong JavaScript

Bài viết này giải thích khái niệm về Service Worker trong JavaScript.

Chúng tôi sẽ giải thích từng bước từ căn bản về Service Worker đến việc kiểm soát bộ nhớ đệm thực tế.

YouTube Video

offline.html
 1<!DOCTYPE html>
 2<html lang="en">
 3<head>
 4  <meta charset="UTF-8">
 5  <title>Offline</title>
 6</head>
 7<body>
 8  <h1>You are offline</h1>
 9  <p>This is the offline fallback page.</p>
10</body>
11</html>
style.css
1body {
2  font-family: sans-serif;
3  background-color: #f0f0f0;
4  padding: 20px;
5}
6h1 {
7  color: #333;
8}
javascript-service-worker.html
  1<!DOCTYPE html>
  2<html lang="en">
  3<head>
  4  <meta charset="UTF-8">
  5  <title>JavaScript &amp; HTML</title>
  6  <style>
  7    * {
  8        box-sizing: border-box;
  9    }
 10
 11    body {
 12        margin: 0;
 13        padding: 1em;
 14        padding-bottom: 10em;
 15        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
 16        background-color: #f7f9fc;
 17        color: #333;
 18        line-height: 1.6;
 19    }
 20
 21    .container {
 22        max-width: 800px;
 23        margin: 0 auto;
 24        padding: 1em;
 25        background-color: #ffffff;
 26        border: 1px solid #ccc;
 27        border-radius: 10px;
 28        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 29    }
 30
 31    .container-flex {
 32        display: flex;
 33        flex-wrap: wrap;
 34        gap: 2em;
 35        max-width: 1000px;
 36        margin: 0 auto;
 37        padding: 1em;
 38        background-color: #ffffff;
 39        border: 1px solid #ccc;
 40        border-radius: 10px;
 41        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 42    }
 43
 44    .left-column, .right-column {
 45        flex: 1 1 200px;
 46        min-width: 200px;
 47    }
 48
 49    h1, h2 {
 50        font-size: 1.2rem;
 51        color: #007bff;
 52        margin-top: 0.5em;
 53        margin-bottom: 0.5em;
 54        border-left: 5px solid #007bff;
 55        padding-left: 0.6em;
 56        background-color: #e9f2ff;
 57    }
 58
 59    button {
 60        display: block;
 61        margin: 1em auto;
 62        padding: 0.75em 1.5em;
 63        font-size: 1rem;
 64        background-color: #007bff;
 65        color: white;
 66        border: none;
 67        border-radius: 6px;
 68        cursor: pointer;
 69        transition: background-color 0.3s ease;
 70    }
 71
 72    button:hover {
 73        background-color: #0056b3;
 74    }
 75
 76    #output {
 77        margin-top: 1em;
 78        background-color: #1e1e1e;
 79        color: #0f0;
 80        padding: 1em;
 81        border-radius: 8px;
 82        min-height: 200px;
 83        font-family: Consolas, monospace;
 84        font-size: 0.95rem;
 85        overflow-y: auto;
 86        white-space: pre-wrap;
 87    }
 88
 89    .highlight {
 90        outline: 3px solid #ffc107; /* yellow border */
 91        background-color: #fff8e1;  /* soft yellow background */
 92        transition: background-color 0.3s ease, outline 0.3s ease;
 93    }
 94
 95    .active {
 96        background-color: #28a745; /* green background */
 97        color: #fff;
 98        box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
 99        transition: background-color 0.3s ease, box-shadow 0.3s ease;
100    }
101  </style>
102</head>
103<body>
104    <div class="container">
105        <h1>JavaScript Console</h1>
106        <button id="executeBtn">Execute</button>
107        <div id="output"></div>
108    </div>
109
110    <div class="container">
111        <h2>HTML Sample</h2>
112        <button id="fetchBtn">Fetch Test</button>
113    </div>
114
115    <script>
116        // Override console.log to display messages in the #output element
117        (function () {
118            // Override console.log
119            const originalLog = console.log;
120            console.log = function (...args) {
121                originalLog.apply(console, args);
122                const message = document.createElement('div');
123                message.textContent = args.map(String).join(' ');
124                output.appendChild(message);
125            };
126
127            // Override console.error
128            const originalError = console.error;
129            console.error = function (...args) {
130                originalError.apply(console, args);
131                const message = document.createElement('div');
132                message.textContent = args.map(String).join(' ');
133                message.style.color = 'red'; // Color error messages red
134                output.appendChild(message);
135            };
136        })();
137
138        document.getElementById('executeBtn').addEventListener('click', () => {
139            // Prevent multiple loads
140            if (document.getElementById('externalScript')) return;
141
142            const script = document.createElement('script');
143            script.src = 'javascript-service-worker.js';
144            script.id = 'externalScript';
145            //script.onload = () => console.log('javascript-service-worker.js loaded and executed.');
146            //script.onerror = () => console.log('Failed to load javascript-service-worker.js.');
147            document.body.appendChild(script);
148        });
149    </script>
150</body>
151</html>

Service Worker trong JavaScript

Service Worker là một tính năng của JavaScript nằm giữa trình duyệt và mạng, cho phép lưu bộ nhớ đệm các yêu cầu và hỗ trợ hoạt động ngoại tuyến. Nó là công nghệ cốt lõi của PWA (Ứng dụng Web Tiến Tiến) và mang lại trải nghiệm giống ứng dụng gốc cho các ứng dụng web.

Service Worker là gì?

Service Worker là một tệp JavaScript chạy trên luồng nền của trình duyệt. Nó chạy trên một luồng riêng biệt với trang web, không thể truy cập giao diện người dùng, nhưng có thể chặn các yêu cầu mạng, quản lý bộ nhớ đệm và xử lý thông báo đẩy.

Các tính năng chính của Service Worker bao gồm:.

  • Nó chỉ hoạt động với HTTPS, trừ localhost.
  • Nó sử dụng API bất đồng bộ dựa trên Promise.
  • Nó dựa trên sự kiện, sử dụng các sự kiện như install, activate, fetchpush.

Đăng ký một Service Worker

Trước tiên, hãy viết mã để đăng ký Service Worker trong trình duyệt.

 1if ('serviceWorker' in navigator) {
 2    window.addEventListener('load', () => {
 3        navigator.serviceWorker.register('/sw.js')
 4        .then(registration => {
 5            console.log(
 6                'Service Worker registered with scope:',
 7                registration.scope
 8            );
 9        })
10        .catch(error => {
11            console.error('Service Worker registration failed:', error);
12        });
13    });
14}

Giải thích

  • Dùng navigator.serviceWorker.register() để đăng ký /sw.js (tệp Service Worker).
  • Bạn có thể dùng then để xử lý thành công và catch để xử lý lỗi trong quá trình đăng ký.
  • registration.scope đại diện cho phạm vi đường dẫn (scope) bị ảnh hưởng bởi Service Worker.
  • Mặc định, phạm vi là thư mục nơi tệp đã được đăng ký (trong trường hợp này là /sw.js) và các thư mục con của nó.

Phạm vi của Service Worker

Nếu bạn muốn giới hạn phạm vi, bạn có thể chỉ định scope bằng cách sử dụng đối số thứ hai của register.

1navigator.serviceWorker.register('/sw.js', { scope: '/app/' })
2.then(registration => {
3    console.log(
4        'Service Worker registered with scope:',
5        registration.scope
6    );
7});

Giải thích

  • Với thiết lập này, chỉ các trang nằm dưới /app/ mới được kiểm soát bởi Service Worker.

Tạo tệp Service Worker

Tiếp theo, tạo một tệp tên là sw.js và triển khai các sự kiện cơ bản.

1// sw.js
2const CACHE_NAME = 'my-cache-v1';
3const urlsToCache = [
4    '/',
5    '/index.html',
6    '/styles.css',
7    '/script.js',
8    '/offline.html'
9];

Đoạn mã này định nghĩa một danh sách các tài nguyên sẽ được lưu vào bộ nhớ đệm.

Vai trò và cơ chế của từng sự kiện

install

 1// Install event (initial caching)
 2self.addEventListener('install', event => {
 3    console.log('[ServiceWorker] Install');
 4    event.waitUntil(
 5        caches.open(CACHE_NAME).then(cache => {
 6            console.log('[ServiceWorker] Caching app shell');
 7            return cache.addAll(urlsToCache);
 8        })
 9    );
10});
  • self.addEventListener('install') được kích hoạt khi Service Worker được đăng ký lần đầu tiên. Ở giai đoạn này, các tệp cần thiết sẽ được lưu vào bộ nhớ đệm trước.

activate

 1// Activation event (delete old caches)
 2self.addEventListener('activate', event => {
 3    console.log('[ServiceWorker] Activate');
 4    event.waitUntil(
 5        caches.keys().then(keyList => {
 6            return Promise.all(keyList.map(key => {
 7                if (key !== CACHE_NAME) {
 8                    console.log('[ServiceWorker] Removing old cache:', key);
 9                    return caches.delete(key);
10                }
11            }));
12        })
13    );
14    return self.clients.claim();
15});
  • Trong sự kiện activate, các bộ nhớ đệm cũ sẽ bị xóa để tối ưu hóa lưu trữ. Chỉ bộ nhớ đệm của phiên bản mới được giữ lại.

fetch

1// Fetch event (cache-first strategy)
2self.addEventListener('fetch', event => {
3  console.log('[ServiceWorker] Fetch', event.request.url);
4    event.respondWith(
5        caches.match(event.request).then(response => {
6            return response || fetch(event.request).catch(() => caches.match('/offline.html'));
7        })
8    );
9});

Tất cả các yêu cầu HTTP đều được chặn—nếu có phiên bản trong bộ nhớ đệm thì trả về; nếu không, sẽ lấy từ mạng. Khi ngoại tuyến, một trang thay thế (ví dụ: offline.html) sẽ được trả về.

Xác nhận thao tác

Hãy cùng kiểm tra thực tế cách hoạt động của Service Worker.

 1document.getElementById('fetchBtn').addEventListener('click', () => {
 2    fetch('/style.css')
 3        .then(response => response.text())
 4        .then(data => {
 5            console.log('Fetched data:', data);
 6        })
 7        .catch(error => {
 8            console.error('Fetch failed:', error);
 9        });
10});
  • Tại đây, chúng tôi kiểm tra việc đăng ký Service Worker và hành vi truy xuất tài nguyên bằng cách nhấn nút kiểm tra.

Ví dụ về các chiến lược lưu bộ nhớ đệm

Dưới đây là các chiến lược lưu bộ nhớ đệm phổ biến:.

Ưu tiên Bộ nhớ đệm (Cache First)

Dưới đây là một ví dụ triển khai cho chiến lược Cache First:.

1self.addEventListener('fetch', event => {
2    event.respondWith(
3        caches.match(event.request).then(response => {
4            return response || fetch(event.request);
5        })
6    );
7});
  • Đoạn mã này triển khai chiến lược ưu tiên bộ nhớ đệm (cache-first), trong đó tài nguyên được yêu cầu sẽ được lấy từ bộ nhớ đệm nếu có; nếu không có thì sẽ lấy từ mạng.

Ưu tiên Mạng (Network First)

Dưới đây là một ví dụ triển khai cho chiến lược Network First:.

 1self.addEventListener('fetch', event => {
 2    event.respondWith(
 3        fetch(event.request)
 4            .then(response => {
 5                return caches.open(CACHE_NAME).then(cache => {
 6                    cache.put(event.request, response.clone());
 7                    return response;
 8                });
 9            })
10            .catch(() => caches.match(event.request))
11    );
12});
  • Đoạn mã này triển khai chiến lược ưu tiên mạng (network-first), trong đó tài nguyên được yêu cầu sẽ được lấy từ mạng trước; nếu thất bại thì sẽ lấy từ bộ nhớ đệm.

Chỉ lưu bộ nhớ đệm các tệp style và JavaScript, truy cập API theo thời gian thực

Dưới đây là ví dụ triển khai khi chỉ lưu bộ nhớ đệm các tệp style và JavaScript, còn API được truy cập theo thời gian thực:.

 1self.addEventListener('fetch', event => {
 2    if (event.request.url.includes('/api/')) {
 3        // Fetch API responses in real-time without caching
 4        return;
 5    }
 6
 7    // Use cache-first strategy for static files
 8    event.respondWith(
 9        caches.match(event.request).then(response => {
10            return response || fetch(event.request);
11        })
12    );
13});
  • Đoạn mã này luôn truy cập các yêu cầu API theo thời gian thực và áp dụng chiến lược ưu tiên bộ nhớ đệm cho các tệp tĩnh như stylesheet và JavaScript.

Quy trình cập nhật

Quy trình cập nhật của Service Worker như sau:.

  1. Một tệp sw.js mới được phát hiện.
  2. Sự kiện install được kích hoạt.
  3. Đợi cho đến khi Service Worker trước đó trở nên không hoạt động.
  4. Sự kiện activate được kích hoạt.
  5. Chuyển sang Service Worker mới.
  6. Sự kiện controllerchange được kích hoạt.

Phát hiện cập nhật

Một khi Service Worker đã được cài đặt, phiên bản cũ vẫn tiếp tục được sử dụng cho đến lần truy cập tiếp theo. Để áp dụng cập nhật, thông thường sử dụng mã phát hiện cập nhật và tải lại trang.

1navigator.serviceWorker.addEventListener('controllerchange', () => {
2    window.location.reload();
3});
  • Sự kiện controllerchange được kích hoạt khi bộ điều khiển (controller) của Service Worker, tức là Service Worker đang kiểm soát trang hiện tại, thay đổi.
  • Các trang đã mở vẫn tiếp tục sử dụng Service Worker hiện tại, và Service Worker mới được cài đặt sẽ không có hiệu lực ngay lập tức trên những trang đó. Do đó, một kỹ thuật được sử dụng là tận dụng sự kiện controllerchange để phát hiện khi bộ điều khiển mới trở nên hoạt động và sau đó tải lại trang để áp dụng cập nhật ngay lập tức.

Lưu ý và Thực hành tốt nhất

Khi sử dụng Service Worker, hãy lưu ý các điểm sau:.

  • Yêu cầu HTTPS Do giới hạn bảo mật, nó không hoạt động trên http:// trừ khi ở localhost.

  • Tên tệp được băm (Hashed File Names) Tên bộ nhớ đệm có thể bao gồm tên tệp, URL và thông tin phiên bản.

  • Giao tiếp với Client Sử dụng postMessage để giao tiếp giữa Service Worker và JavaScript của trang.

Tóm tắt

Service Worker là một công nghệ thiết yếu để hỗ trợ hoạt động ngoại tuyến và cải thiện hiệu suất cho các ứng dụng web. Bằng cách hiểu quy trình cơ bản về cài đặt, kích hoạt, xử lý truy xuất dữ liệu và triển khai chiến lược lưu bộ nhớ đệm phù hợp, bạn có thể xây dựng các ứng dụng web chất lượng cao hơn.

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.

YouTube Video