Cuộc sống của nhân viên dịch vụ

Rất khó để biết trình chạy dịch vụ đang làm gì nếu không hiểu được vòng đời của trình chạy dịch vụ. Cách hoạt động bên trong của các lớp này sẽ có vẻ không rõ ràng, thậm chí là tuỳ ý. Hãy nhớ rằng – giống như mọi API trình duyệt khác – hành vi của worker dịch vụ được xác định rõ ràng, được chỉ định và cho phép các ứng dụng ngoại tuyến, đồng thời tạo điều kiện cho các bản cập nhật mà không làm gián đoạn trải nghiệm người dùng.

Trước khi tìm hiểu sâu về Workbox, bạn cần hiểu rõ vòng đời của worker dịch vụ để biết Workbox hoạt động như thế nào.

Xác định các thuật ngữ

Trước khi tìm hiểu về vòng đời của worker dịch vụ, bạn nên xác định một số thuật ngữ liên quan đến cách hoạt động của vòng đời đó.

Kiểm soát và phạm vi

Ý tưởng về quyền kiểm soát là yếu tố quan trọng để hiểu cách hoạt động của worker dịch vụ. Trang được mô tả là do trình chạy dịch vụ kiểm soát là trang cho phép trình chạy dịch vụ thay mặt chặn các yêu cầu mạng. Trình chạy dịch vụ hiện diện có thể thực hiện công việc cho trang trong một phạm vi nhất định.

Phạm vi

Phạm vi của worker dịch vụ được xác định theo vị trí của worker đó trên máy chủ web. Nếu một trình chạy dịch vụ chạy trên một trang nằm ở /subdir/index.html và nằm ở /subdir/sw.js, thì phạm vi của trình chạy dịch vụ là /subdir/. Để xem khái niệm về phạm vi trong thực tế, hãy xem ví dụ sau:

  1. Truy cập vào https://service-worker-scope-viewer.glitch.me/subdir/index.html. Một thông báo sẽ xuất hiện cho biết không có trình chạy dịch vụ nào đang kiểm soát trang. Tuy nhiên, trang đó đăng ký một trình chạy dịch vụ từ https://service-worker-scope-viewer.glitch.me/subdir/sw.js.
  2. Tải lại trang. Vì trình chạy dịch vụ đã được đăng ký và hiện đang hoạt động, nên trình chạy dịch vụ này sẽ kiểm soát trang. Một biểu mẫu chứa phạm vi, trạng thái hiện tại và URL của worker dịch vụ sẽ hiển thị. Lưu ý: việc phải tải lại trang không liên quan gì đến phạm vi mà là vòng đời của trình chạy dịch vụ. Chúng ta sẽ giải thích về vấn đề này sau.
  3. Bây giờ, hãy truy cập vào https://service-worker-scope-viewer.glitch.me/index.html. Mặc dù đã đăng ký trình chạy dịch vụ trên nguồn gốc này, nhưng vẫn có thông báo cho biết hiện không có trình chạy dịch vụ nào. Đó là do trang này không thuộc phạm vi của trình chạy dịch vụ đã đăng ký.

Phạm vi giới hạn những trang mà trình chạy dịch vụ kiểm soát. Trong ví dụ này, điều đó có nghĩa là trình chạy dịch vụ được tải từ /subdir/sw.js chỉ có thể kiểm soát các trang nằm trong /subdir/ hoặc cây con của trang đó.

Trên đây là cách hoạt động của tính năng xác định phạm vi theo mặc định, nhưng bạn có thể ghi đè phạm vi tối đa được phép bằng cách đặt tiêu đề phản hồi Service-Worker-Allowed, cũng như truyền tuỳ chọn scope đến phương thức register.

Trừ phi có lý do chính đáng để giới hạn phạm vi của trình chạy dịch vụ ở một tập hợp con của một nguồn gốc, hãy tải trình chạy dịch vụ từ thư mục gốc của máy chủ web để phạm vi của trình chạy dịch vụ rộng nhất có thể và đừng lo lắng về tiêu đề Service-Worker-Allowed. Việc này sẽ đơn giản hơn rất nhiều cho mọi người.

Khách hàng

Khi nói rằng trình chạy dịch vụ đang kiểm soát một trang, thực ra trình chạy dịch vụ đang kiểm soát một ứng dụng. Ứng dụng là bất kỳ trang mở nào có URL nằm trong phạm vi của worker dịch vụ đó. Cụ thể, đây là các thực thể của WindowClient.

Vòng đời của trình chạy dịch vụ mới

Để một worker dịch vụ có thể kiểm soát một trang, trước tiên, worker đó phải được tạo. Hãy bắt đầu với những gì xảy ra khi một worker dịch vụ hoàn toàn mới được triển khai cho một trang web không có worker dịch vụ đang hoạt động.

Đăng ký

Đăng ký là bước đầu tiên của vòng đời của trình chạy dịch vụ:

<!-- In index.html, for example: -->
<script>
  // Don't register the service worker
  // until the page has fully loaded
  window.addEventListener('load', () => {
    // Is service worker available?
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/sw.js').then(() => {
        console.log('Service worker registered!');
      }).catch((error) => {
        console.warn('Error registering service worker:');
        console.warn(error);
      });
    }
  });
</script>

Mã này chạy trên luồng chính và thực hiện những việc sau:

  1. Vì lần truy cập đầu tiên của người dùng vào trang web không có worker dịch vụ đã đăng ký, nên hãy đợi cho đến khi trang tải xong trước khi đăng ký worker dịch vụ. Điều này giúp tránh tình trạng tranh chấp băng thông nếu worker dịch vụ lưu trước bất kỳ nội dung nào.
  2. Mặc dù trình chạy dịch vụ được hỗ trợ tốt, nhưng một bước kiểm tra nhanh sẽ giúp tránh lỗi trong các trình duyệt không hỗ trợ trình chạy dịch vụ.
  3. Khi trang tải xong và nếu trình chạy dịch vụ được hỗ trợ, hãy đăng ký /sw.js.

Sau đây là một số điều quan trọng cần nắm được:

  • Trình chạy dịch vụ chỉ có trên HTTPS hoặc máy chủ cục bộ.
  • Nếu nội dung của trình chạy dịch vụ chứa lỗi cú pháp, thì quá trình đăng ký sẽ không thành công và trình chạy dịch vụ sẽ bị loại bỏ.
  • Lưu ý: trình chạy dịch vụ hoạt động trong một phạm vi. Ở đây, phạm vi là toàn bộ nguồn gốc, vì phạm vi này được tải từ thư mục gốc.
  • Khi quá trình đăng ký bắt đầu, trạng thái của trình chạy dịch vụ được đặt thành 'installing'.

Sau khi đăng ký xong, quá trình cài đặt sẽ bắt đầu.

Cài đặt

Sau khi đăng ký, worker dịch vụ sẽ kích hoạt sự kiện install. install chỉ được gọi một lần cho mỗi worker và sẽ không kích hoạt lại cho đến khi được cập nhật. Bạn có thể đăng ký lệnh gọi lại cho sự kiện install trong phạm vi của worker bằng addEventListener:

// /sw.js
self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v1';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v1'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.bc7b80b7.css',
      '/css/home.fe5d0b23.css',
      '/js/home.d3cc4ba4.js',
      '/js/jquery.43ca4933.js'
    ]);
  }));
});

Thao tác này sẽ tạo một thực thể Cache mới và lưu các thành phần vào bộ nhớ đệm trước. Chúng ta sẽ có nhiều cơ hội để thảo luận về việc lưu vào bộ nhớ đệm trước sau này, vì vậy, hãy tập trung vào vai trò của event.waitUntil. event.waitUntil chấp nhận một lời hứa và đợi cho đến khi lời hứa đó được giải quyết. Trong ví dụ này, lời hứa đó thực hiện hai việc không đồng bộ:

  1. Tạo một thực thể Cache mới có tên là 'MyFancyCache_v1'.
  2. Sau khi bộ nhớ đệm được tạo, một mảng URL thành phần sẽ được lưu vào bộ nhớ đệm trước bằng cách sử dụng phương thức addAll không đồng bộ.

Quá trình cài đặt sẽ không thành công nếu(các) lời hứa được truyền đến event.waitUntil bị từ chối. Nếu điều này xảy ra, worker dịch vụ sẽ bị loại bỏ.

Nếu các lời hứa giải quyết, quá trình cài đặt sẽ thành công và trạng thái của worker dịch vụ sẽ thay đổi thành 'installed', sau đó sẽ kích hoạt.

Cách triển khai

Nếu quá trình đăng ký và cài đặt thành công, thì worker dịch vụ sẽ kích hoạt và trạng thái của worker dịch vụ sẽ trở thành 'activating'. Bạn có thể thực hiện công việc trong quá trình kích hoạt trong sự kiện activate của worker dịch vụ. Một tác vụ thông thường trong sự kiện này là cắt bớt bộ nhớ đệm cũ, nhưng đối với một worker dịch vụ hoàn toàn mới, việc này hiện không liên quan và sẽ được mở rộng khi chúng ta thảo luận về các bản cập nhật worker dịch vụ.

Đối với worker dịch vụ mới, activate sẽ kích hoạt ngay sau khi install thành công. Sau khi quá trình kích hoạt hoàn tất, trạng thái của worker dịch vụ sẽ trở thành 'activated'. Xin lưu ý rằng theo mặc định, trình chạy dịch vụ mới sẽ không bắt đầu kiểm soát trang cho đến khi thao tác điều hướng hoặc làm mới trang tiếp theo diễn ra.

Xử lý bản cập nhật của worker dịch vụ

Sau khi triển khai worker dịch vụ đầu tiên, bạn có thể sẽ phải cập nhật worker này sau. Ví dụ: bạn có thể cần cập nhật nếu có thay đổi trong việc xử lý yêu cầu hoặc logic lưu vào bộ nhớ đệm trước.

Thời điểm cập nhật

Trình duyệt sẽ kiểm tra bản cập nhật cho worker dịch vụ khi:

  • Người dùng chuyển đến một trang trong phạm vi của trình chạy dịch vụ.
  • navigator.serviceWorker.register() được gọi bằng một URL khác với trình chạy dịch vụ hiện đã cài đặt –nhưng đừng thay đổi URL của trình chạy dịch vụ!
  • navigator.serviceWorker.register() được gọi bằng cùng một URL với worker dịch vụ đã cài đặt, nhưng có phạm vi khác. Xin nhắc lại, hãy tránh điều này bằng cách giữ phạm vi ở gốc của một nguồn gốc nếu có thể.
  • Khi các sự kiện như 'push' hoặc 'sync' được kích hoạt trong vòng 24 giờ qua. Tuy nhiên, bạn đừng lo lắng về những sự kiện này.

Cách thức cập nhật

Việc biết thời điểm trình duyệt cập nhật một worker dịch vụ là rất quan trọng, nhưng "cách thức" cũng quan trọng không kém. Giả sử URL hoặc phạm vi của worker dịch vụ không thay đổi, worker dịch vụ hiện đã cài đặt sẽ chỉ cập nhật lên phiên bản mới nếu nội dung của worker đó đã thay đổi.

Trình duyệt phát hiện các thay đổi theo một số cách:

  • Mọi thay đổi byte-for-byte đối với tập lệnh do importScripts yêu cầu (nếu có).
  • Mọi thay đổi trong mã cấp cao nhất của worker dịch vụ sẽ ảnh hưởng đến vân tay mà trình duyệt đã tạo.

Trình duyệt thực hiện nhiều thao tác nặng ở đây. Để đảm bảo trình duyệt có mọi thứ cần thiết để phát hiện đáng tin cậy các thay đổi đối với nội dung của worker dịch vụ, hãy không yêu cầu bộ nhớ đệm HTTP giữ lại nội dung đó và không thay đổi tên tệp của nội dung đó. Trình duyệt sẽ tự động kiểm tra bản cập nhật khi có thao tác điều hướng đến một trang mới trong phạm vi của worker dịch vụ.

Kích hoạt quy trình kiểm tra bản cập nhật theo cách thủ công

Liên quan đến nội dung cập nhật, logic đăng ký thường không thay đổi. Tuy nhiên, có một ngoại lệ là nếu các phiên trên trang web có thời lượng dài. Điều này có thể xảy ra trong các ứng dụng một trang, trong đó yêu cầu điều hướng hiếm khi xảy ra, vì ứng dụng thường gặp một yêu cầu điều hướng ở đầu vòng đời của ứng dụng. Trong những trường hợp như vậy, bạn có thể kích hoạt một bản cập nhật thủ công trên luồng chính:

navigator.serviceWorker.ready.then((registration) => {
  registration.update();
});

Đối với các trang web truyền thống hoặc trong mọi trường hợp mà phiên hoạt động của người dùng không kéo dài, có thể bạn không cần kích hoạt tính năng cập nhật thủ công.

Cài đặt

Khi sử dụng trình tạo gói để tạo tài sản tĩnh, các tài sản đó sẽ chứa hàm băm trong tên, chẳng hạn như framework.3defa9d2.js. Giả sử một số thành phần đó được lưu vào bộ nhớ đệm trước để truy cập khi không có mạng sau này. Điều này đòi hỏi bạn phải cập nhật trình chạy dịch vụ để lưu các thành phần đã cập nhật vào bộ nhớ đệm trước:

self.addEventListener('install', (event) => {
  const cacheKey = 'MyFancyCacheName_v2';

  event.waitUntil(caches.open(cacheKey).then((cache) => {
    // Add all the assets in the array to the 'MyFancyCacheName_v2'
    // `Cache` instance for later use.
    return cache.addAll([
      '/css/global.ced4aef2.css',
      '/css/home.cbe409ad.css',
      '/js/home.109defa4.js',
      '/js/jquery.38caf32d.js'
    ]);
  }));
});

Có hai điểm khác biệt so với ví dụ về sự kiện install đầu tiên ở trên:

  1. Một thực thể Cache mới có khoá là 'MyFancyCacheName_v2' sẽ được tạo.
  2. Tên thành phần được lưu vào bộ nhớ đệm trước đã thay đổi.

Một điều cần lưu ý là trình chạy dịch vụ đã cập nhật sẽ được cài đặt cùng với trình chạy dịch vụ trước đó. Điều này có nghĩa là worker dịch vụ cũ vẫn kiểm soát mọi trang đang mở và sau khi cài đặt, worker mới sẽ chuyển sang trạng thái chờ cho đến khi được kích hoạt.

Theo mặc định, một worker dịch vụ mới sẽ kích hoạt khi không có ứng dụng nào đang được worker cũ kiểm soát. Điều này xảy ra khi bạn đóng tất cả các thẻ đang mở của trang web có liên quan.

Cách triển khai

Khi một trình chạy dịch vụ đã cập nhật được cài đặt và giai đoạn chờ kết thúc, trình chạy dịch vụ đó sẽ kích hoạt và trình chạy dịch vụ cũ sẽ bị loại bỏ. Một nhiệm vụ phổ biến cần thực hiện trong sự kiện activate của trình chạy dịch vụ đã cập nhật là loại bỏ bộ nhớ đệm cũ. Xoá bộ nhớ đệm cũ bằng cách lấy khoá cho tất cả các thực thể Cache đang mở bằng caches.keys và xoá bộ nhớ đệm không có trong danh sách cho phép đã xác định bằng caches.delete:

self.addEventListener('activate', (event) => {
  // Specify allowed cache keys
  const cacheAllowList = ['MyFancyCacheName_v2'];

  // Get all the currently active `Cache` instances.
  event.waitUntil(caches.keys().then((keys) => {
    // Delete all caches that aren't in the allow list:
    return Promise.all(keys.map((key) => {
      if (!cacheAllowList.includes(key)) {
        return caches.delete(key);
      }
    }));
  }));
});

Bộ nhớ đệm cũ không tự dọn dẹp. Chúng ta cần tự làm việc đó nếu không muốn vượt quá hạn mức bộ nhớ. Vì 'MyFancyCacheName_v1' từ worker dịch vụ đầu tiên đã lỗi thời, nên danh sách cho phép bộ nhớ đệm được cập nhật để chỉ định 'MyFancyCacheName_v2', giúp xoá bộ nhớ đệm có tên khác.

Sự kiện activate sẽ kết thúc sau khi xoá bộ nhớ đệm cũ. Tại thời điểm này, trình chạy dịch vụ mới sẽ kiểm soát trang, cuối cùng thay thế trình chạy dịch vụ cũ!

Vòng đời không ngừng diễn ra

Cho dù bạn sử dụng Workbox để xử lý việc triển khai và cập nhật worker dịch vụ hay sử dụng trực tiếp API Worker dịch vụ, bạn cũng nên tìm hiểu vòng đời của worker dịch vụ. Với sự hiểu biết đó, hành vi của worker dịch vụ sẽ có vẻ hợp lý hơn là bí ẩn.

Đối với những người muốn tìm hiểu sâu hơn về chủ đề này, bạn nên tham khảo bài viết này của Jake Archibald. Có rất nhiều sắc thái trong cách toàn bộ quá trình diễn ra xung quanh vòng đời dịch vụ, nhưng bạn có thể biết được và kiến thức đó sẽ giúp ích rất nhiều khi sử dụng Workbox.