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

Jake Archibald
Jake Archibald

Vòng đời của worker dịch vụ là phần phức tạp nhất. Nếu bạn không biết công cụ này đang cố gắng làm gì và mang lại lợi ích gì, thì có thể bạn sẽ cảm thấy công cụ này đang chống lại bạn. Tuy nhiên, khi đã biết cách hoạt động của tính năng này, bạn có thể cung cấp cho người dùng các bản cập nhật liền mạch và không gây phiền toái, kết hợp những ưu điểm tốt nhất của các mẫu web và mẫu gốc.

Đây là một bài viết chuyên sâu, nhưng các dấu đầu dòng ở đầu mỗi phần sẽ trình bày hầu hết những điều bạn cần biết.

Ý định

Mục đích của vòng đời là:

  • Ưu tiên chế độ ngoại tuyến
  • Cho phép trình chạy dịch vụ mới tự chuẩn bị mà không làm gián đoạn trình chạy dịch vụ hiện tại.
  • Đảm bảo một trang trong phạm vi được kiểm soát bởi cùng một trình chạy dịch vụ (hoặc không có trình chạy dịch vụ nào) xuyên suốt.
  • Đảm bảo chỉ có một phiên bản trang web đang chạy cùng một lúc.

Bước cuối cùng là khá quan trọng. Nếu không có trình chạy dịch vụ, người dùng có thể tải một thẻ vào trang web của bạn, sau đó mở một thẻ khác. Điều này có thể khiến hai phiên bản trang web của bạn chạy cùng lúc. Đôi khi, điều này là bình thường, nhưng nếu đang xử lý bộ nhớ, bạn có thể dễ dàng kết thúc bằng hai thẻ có ý kiến rất khác nhau về cách quản lý bộ nhớ dùng chung của chúng. Điều này có thể dẫn đến lỗi hoặc tệ hơn là mất dữ liệu.

Trình chạy dịch vụ đầu tiên

Tóm lại:

  • Sự kiện install là sự kiện đầu tiên mà một trình chạy dịch vụ nhận được và chỉ xảy ra một lần.
  • Lời hứa được truyền đến installEvent.waitUntil() sẽ báo hiệu thời lượng và trạng thái thành công hoặc không thành công của quá trình cài đặt.
  • Một trình chạy dịch vụ sẽ không nhận được các sự kiện như fetchpush cho đến khi hoàn tất quá trình cài đặt và có trạng thái "đang hoạt động".
  • Theo mặc định, các lần tìm nạp của trang sẽ không thông qua trình chạy dịch vụ trừ khi yêu cầu của trang đó đã trải qua một trình chạy dịch vụ. Vì vậy, bạn cần làm mới trang để xem các hiệu ứng của service worker.
  • clients.claim() có thể ghi đè giá trị mặc định này và kiểm soát các trang không được kiểm soát.

Lấy đoạn mã HTML sau:

<!DOCTYPE html>
An image will appear here in 3 seconds:
<script>
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW registered!', reg))
    .catch(err => console.log('Boo!', err));

  setTimeout(() => {
    const img = new Image();
    img.src = '/dog.svg';
    document.body.appendChild(img);
  }, 3000);
</script>

Phương thức này đăng ký một worker dịch vụ và thêm hình ảnh một chú chó sau 3 giây.

Dưới đây là trình chạy dịch vụ sw.js:

self.addEventListener('install', event => {
  console.log('V1 installing…');

  // cache a cat SVG
  event.waitUntil(
    caches.open('static-v1').then(cache => cache.add('/cat.svg'))
  );
});

self.addEventListener('activate', event => {
  console.log('V1 now ready to handle fetches!');
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the cat SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/cat.svg'));
  }
});

Công cụ này lưu hình ảnh một con mèo vào bộ nhớ đệm và phân phát hình ảnh này bất cứ khi nào có yêu cầu đối với /dog.svg. Tuy nhiên, nếu chạy ví dụ trên, bạn sẽ thấy một chú chó trong lần đầu tiên tải trang. Nhấn làm mới và bạn sẽ thấy chú mèo.

Phạm vi và quyền kiểm soát

Phạm vi mặc định của một lượt đăng ký trình chạy dịch vụ là ./ so với URL tập lệnh. Điều này có nghĩa là nếu bạn đăng ký một trình chạy dịch vụ tại //example.com/foo/bar.js, thì trình chạy dịch vụ đó sẽ có phạm vi mặc định là //example.com/foo/.

Chúng tôi gọi các trang, worker và worker dùng chung là clients. Trình chạy dịch vụ của bạn chỉ có thể kiểm soát các ứng dụng nằm trong phạm vi. Sau khi một ứng dụng khách được "kiểm soát", các lệnh tìm nạp của ứng dụng đó sẽ đi qua worker dịch vụ trong phạm vi. Bạn có thể phát hiện xem một ứng dụng có được kiểm soát qua navigator.serviceWorker.controller hay không (có giá trị rỗng) hoặc một thực thể của trình chạy dịch vụ.

Tải xuống, phân tích cú pháp và thực thi

Trình chạy dịch vụ đầu tiên của bạn sẽ tải xuống khi bạn gọi .register(). Nếu tập lệnh của bạn không tải xuống, phân tích cú pháp hoặc gửi lỗi trong lần thực thi đầu tiên, thì lời hứa đăng ký sẽ từ chối và worker dịch vụ sẽ bị loại bỏ.

Công cụ cho nhà phát triển của Chrome hiển thị lỗi trong bảng điều khiển và trong phần trình chạy dịch vụ của thẻ ứng dụng:

Lỗi xuất hiện trong thẻ Công cụ cho nhà phát triển của trình chạy dịch vụ

Cài đặt

Sự kiện đầu tiên mà worker dịch vụ nhận được là install. Phương thức này được kích hoạt ngay khi worker thực thi và chỉ được gọi một lần cho mỗi worker. Nếu bạn thay đổi tập lệnh trình chạy dịch vụ, trình duyệt sẽ coi đó là một trình chạy dịch vụ khác và sẽ nhận được sự kiện install riêng. Tôi sẽ trình bày chi tiết về các nội dung cập nhật sau.

Sự kiện install là cơ hội để bạn lưu mọi thứ mình cần vào bộ nhớ đệm trước khi có thể kiểm soát ứng dụng. Lời hứa bạn chuyển đến event.waitUntil() cho trình duyệt biết khi nào quá trình cài đặt của bạn hoàn tất và liệu có thành công hay không.

Nếu lời hứa của bạn bị từ chối, điều này cho biết quá trình cài đặt không thành công và trình duyệt sẽ loại bỏ worker dịch vụ. Ứng dụng này sẽ không bao giờ kiểm soát ứng dụng khách. Điều này có nghĩa là chúng ta có thể dựa vào cat.svg có trong bộ nhớ đệm trong các sự kiện fetch. Đây là một phần phụ thuộc.

Kích hoạt

Khi worker dịch vụ của bạn đã sẵn sàng để kiểm soát ứng dụng và xử lý các sự kiện chức năng như pushsync, bạn sẽ nhận được một sự kiện activate. Tuy nhiên, điều đó không có nghĩa là trang gọi .register() sẽ được kiểm soát.

Lần đầu tiên bạn tải bản minh hoạ, mặc dù dog.svg được yêu cầu sau khi worker dịch vụ kích hoạt, nhưng worker dịch vụ không xử lý yêu cầu này và bạn vẫn thấy hình ảnh chú chó. Chế độ mặc định là tính nhất quán, nếu trang của bạn tải mà không có worker dịch vụ, thì các tài nguyên phụ của trang cũng sẽ không tải. Nếu bạn tải bản minh hoạ lần thứ hai (tức là làm mới trang), thì bản minh hoạ đó sẽ được kiểm soát. Cả trang và hình ảnh sẽ trải qua các sự kiện fetch và thay vào đó, bạn sẽ thấy một con mèo.

clients.claim

Bạn có thể kiểm soát các ứng dụng không được kiểm soát bằng cách gọi clients.claim() trong trình chạy dịch vụ của bạn sau khi trình chạy dịch vụ này được kích hoạt.

Dưới đây là một biến thể của bản minh hoạ ở trên. Biến thể này gọi clients.claim() trong sự kiện activate. Bạn sẽ thấy một con mèo trong lần đầu tiên. Tôi nói "nên" vì đây là thời điểm nhạy cảm. Bạn sẽ chỉ thấy một chú mèo nếu service worker kích hoạt và clients.claim() có hiệu lực trước khi hình ảnh cố gắng tải.

Nếu bạn sử dụng worker dịch vụ để tải các trang theo cách khác với cách tải qua mạng, thì clients.claim() có thể gây rắc rối vì worker dịch vụ của bạn sẽ kiểm soát một số ứng dụng đã tải mà không cần đến clients.claim().

Cập nhật trình chạy dịch vụ

Tóm lại:

  • Hệ thống sẽ kích hoạt bản cập nhật nếu xảy ra một trong những trường hợp sau:
    • Một đường liên kết đến một trang thuộc phạm vi.
    • Sự kiện chức năng như pushsync, trừ phi đã có một lần kiểm tra cập nhật trong vòng 24 giờ trước đó.
    • Chỉ gọi .register() nếu URL của worker dịch vụ đã thay đổi. Tuy nhiên, bạn nên tránh thay đổi URL trình chạy.
  • Hầu hết các trình duyệt, bao gồm cả Chrome 68 trở lên, đều mặc định bỏ qua các tiêu đề lưu vào bộ nhớ đệm khi kiểm tra bản cập nhật của tập lệnh worker dịch vụ đã đăng ký. Các trình này vẫn tuân thủ các tiêu đề lưu vào bộ nhớ đệm khi tìm nạp tài nguyên được tải bên trong trình chạy dịch vụ thông qua importScripts(). Bạn có thể ghi đè hành vi mặc định này bằng cách đặt tuỳ chọn updateViaCache khi đăng ký trình chạy dịch vụ.
  • Service worker của bạn được coi là đã cập nhật nếu nó khác byte với trình duyệt đã có. (Chúng tôi cũng đang mở rộng phạm vi này để bao gồm cả tập lệnh/mô-đun đã nhập.)
  • Worker dịch vụ đã cập nhật được chạy cùng với worker dịch vụ hiện có và nhận sự kiện install riêng.
  • Nếu worker mới có mã trạng thái không ổn (ví dụ: 404), không phân tích cú pháp, gửi lỗi trong quá trình thực thi hoặc từ chối trong quá trình cài đặt, thì worker mới sẽ bị loại bỏ nhưng worker hiện tại vẫn hoạt động.
  • Sau khi cài đặt thành công, worker được cập nhật sẽ wait cho đến khi worker hiện tại đang kiểm soát 0 ứng dụng. (Lưu ý rằng các ứng dụng sẽ chồng chéo nhau trong quá trình làm mới.)
  • self.skipWaiting() ngăn việc chờ đợi, nghĩa là worker dịch vụ sẽ kích hoạt ngay khi cài đặt xong.

Giả sử chúng ta đã thay đổi tập lệnh của worker dịch vụ để phản hồi bằng hình ảnh một con ngựa thay vì một con mèo:

const expectedCaches = ['static-v2'];

self.addEventListener('install', event => {
  console.log('V2 installing…');

  // cache a horse SVG into a new cache, static-v2
  event.waitUntil(
    caches.open('static-v2').then(cache => cache.add('/horse.svg'))
  );
});

self.addEventListener('activate', event => {
  // delete any caches that aren't in expectedCaches
  // which will get rid of static-v1
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.map(key => {
        if (!expectedCaches.includes(key)) {
          return caches.delete(key);
        }
      })
    )).then(() => {
      console.log('V2 now ready to handle fetches!');
    })
  );
});

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  // serve the horse SVG from the cache if the request is
  // same-origin and the path is '/dog.svg'
  if (url.origin == location.origin && url.pathname == '/dog.svg') {
    event.respondWith(caches.match('/horse.svg'));
  }
});

Xem bản minh hoạ ở trên. Bạn sẽ vẫn thấy hình ảnh một chú mèo. Đây là lý do...

Cài đặt

Lưu ý rằng tôi đã thay đổi tên bộ nhớ đệm từ static-v1 thành static-v2. Điều này có nghĩa là tôi có thể thiết lập bộ nhớ đệm mới mà không cần ghi đè lên bộ nhớ đệm hiện tại mà trình chạy dịch vụ cũ vẫn đang sử dụng.

Mẫu này tạo ra bộ nhớ đệm dành riêng cho từng phiên bản, giống như các thành phần mà một ứng dụng gốc sẽ gói kèm theo tệp thực thi. Bạn cũng có thể có bộ nhớ đệm không dành riêng cho phiên bản, chẳng hạn như avatars.

Đang đợi

Sau khi cài đặt thành công, worker dịch vụ đã cập nhật sẽ trì hoãn việc kích hoạt cho đến khi worker dịch vụ hiện tại không còn kiểm soát ứng dụng. Trạng thái này được gọi là "đang chờ" và là cách trình duyệt đảm bảo rằng mỗi lần chỉ có một phiên bản của worker dịch vụ đang chạy.

Nếu chạy bản minh hoạ được cập nhật, bạn vẫn sẽ thấy hình ảnh một chú mèo vì worker V2 chưa kích hoạt. Bạn có thể thấy worker dịch vụ mới đang chờ trong thẻ "Application" (Ứng dụng) của DevTools:

Công cụ cho nhà phát triển hiển thị trình chạy dịch vụ mới đang chờ

Ngay cả khi bạn chỉ mở một thẻ để xem bản minh hoạ, thì việc làm mới trang vẫn chưa đủ để phiên bản mới thay thế. Điều này là do cách hoạt động của các thao tác điều hướng trên trình duyệt. Khi bạn điều hướng, trang hiện tại sẽ không biến mất cho đến khi nhận được tiêu đề phản hồi. Ngay cả khi đó, trang hiện tại vẫn có thể tồn tại nếu phản hồi có tiêu đề Content-Disposition. Do sự trùng lặp này, trình chạy dịch vụ hiện tại luôn kiểm soát ứng dụng trong quá trình làm mới.

Để nhận bản cập nhật, hãy đóng hoặc rời khỏi tất cả các thẻ bằng trình chạy dịch vụ hiện tại. Sau đó, khi quay lại trang minh hoạ, bạn sẽ thấy hình ảnh con ngựa.

Hình này tương tự như cách Chrome cập nhật. Các bản cập nhật cho Chrome tải xuống ở chế độ nền nhưng không áp dụng cho đến khi Chrome khởi động lại. Trong thời gian chờ đợi, bạn có thể tiếp tục sử dụng phiên bản hiện tại mà không bị gián đoạn. Tuy nhiên, đây là một vấn đề trong quá trình phát triển, nhưng Công cụ cho nhà phát triển có nhiều cách để giúp bạn thực hiện việc này dễ dàng hơn. Tôi sẽ đề cập đến điều này ở phần sau của bài viết này.

Kích hoạt

Lệnh này sẽ kích hoạt khi worker cũ không còn hoạt động và nhân viên dịch vụ mới có thể kiểm soát khách hàng. Đây là thời điểm lý tưởng để làm những việc mà bạn không thể làm khi worker cũ vẫn đang hoạt động, chẳng hạn như di chuyển cơ sở dữ liệu và xoá bộ nhớ đệm.

Trong bản minh hoạ ở trên, tôi duy trì danh sách bộ nhớ đệm mà tôi dự kiến sẽ có ở đó. Trong sự kiện activate, tôi sẽ loại bỏ mọi bộ nhớ đệm khác, thao tác này sẽ xoá bộ nhớ đệm static-v1 cũ.

Nếu bạn truyền một lời hứa đến event.waitUntil(), thì lời hứa đó sẽ lưu các sự kiện chức năng vào vùng đệm (fetch, push, sync, v.v.) cho đến khi lời hứa được giải quyết. Vì vậy, khi sự kiện fetch của bạn kích hoạt, quá trình kích hoạt đã hoàn tất.

Bỏ qua giai đoạn chờ

Giai đoạn chờ có nghĩa là bạn chỉ chạy một phiên bản của trang web cùng một lúc, nhưng nếu không cần tính năng đó, bạn có thể kích hoạt trình chạy dịch vụ mới sớm hơn bằng cách gọi self.skipWaiting().

Điều này khiến worker dịch vụ của bạn loại bỏ worker đang hoạt động hiện tại và tự kích hoạt ngay khi worker này chuyển sang giai đoạn chờ (hoặc ngay lập tức nếu worker này đã ở giai đoạn chờ). Điều này không khiến worker của bạn bỏ qua việc cài đặt, mà chỉ chờ đợi.

Việc bạn gọi skipWaiting() không thực sự quan trọng, miễn là lệnh này được thực hiện trong hoặc trước khi chờ. Thường gọi nó trong sự kiện install:

self.addEventListener('install', event => {
  self.skipWaiting();

  event.waitUntil(
    // caching etc
  );
});

Tuy nhiên, bạn có thể muốn gọi sự kiện đó dưới dạng kết quả của postMessage() đối với trình chạy dịch vụ. Tức là bạn muốn skipWaiting() sau khi người dùng tương tác.

Sau đây là một bản minh hoạ sử dụng skipWaiting(). Bạn sẽ thấy hình ảnh một con bò mà không cần phải rời khỏi trang. Giống như clients.claim(), đây là một cuộc đua, vì vậy, bạn sẽ chỉ thấy bò nếu trình chạy dịch vụ mới tìm nạp, cài đặt và kích hoạt trước khi trang cố gắng tải hình ảnh.

Các bản cập nhật thủ công

Như đã đề cập trước đó, trình duyệt sẽ tự động kiểm tra nội dung cập nhật sau các thao tác điều hướng và sự kiện chức năng, nhưng bạn cũng có thể kích hoạt các nội dung cập nhật đó theo cách thủ công:

navigator.serviceWorker.register('/sw.js').then(reg => {
  // sometime later…
  reg.update();
});

Nếu dự kiến người dùng sẽ sử dụng trang web của bạn trong một thời gian dài mà không cần tải lại, bạn nên gọi update() theo một khoảng thời gian (chẳng hạn như hằng giờ).

Tránh thay đổi URL của tập lệnh trình chạy dịch vụ

Nếu đã đọc bài đăng của tôi về các phương pháp hay nhất để lưu vào bộ nhớ đệm, bạn có thể cân nhắc việc cung cấp một URL riêng biệt cho mỗi phiên bản của worker dịch vụ. Đừng làm việc này! Đây thường là phương pháp không phù hợp với các trình chạy dịch vụ. Bạn chỉ cần cập nhật tập lệnh ở vị trí hiện tại của tập lệnh.

Ứng dụng này có thể khiến bạn gặp sự cố như sau:

  1. index.html đăng ký sw-v1.js làm trình chạy dịch vụ.
  2. sw-v1.js lưu vào bộ nhớ đệm và phân phát index.html để thiết bị hoạt động ở chế độ ngoại tuyến.
  3. Bạn cập nhật index.html để đăng ký sw-v2.js mới và sáng bóng.

Nếu bạn làm như trên, người dùng sẽ không bao giờ nhận được sw-v2.jssw-v1.js đang phân phát phiên bản cũ của index.html từ bộ nhớ đệm. Bạn đang ở vị trí cần cập nhật worker dịch vụ để cập nhật worker dịch vụ. Kinh.

Tuy nhiên, đối với bản minh hoạ ở trên, tôi đã thay đổi URL của worker dịch vụ. Do đó, để minh hoạ, bạn có thể chuyển đổi giữa các phiên bản. Đây không phải là điều tôi sẽ làm trong quá trình phát hành chính thức.

Giúp việc phát triển trở nên dễ dàng

Vòng đời của trình chạy dịch vụ được xây dựng để phục vụ người dùng, nhưng trong quá trình phát triển, bạn sẽ gặp một chút khó khăn. Rất may là có một vài công cụ có thể giúp bạn:

Cập nhật khi tải lại

Đây là bức ảnh tôi yêu thích nhất.

Công cụ cho nhà phát triển hiển thị &quot;cập nhật khi tải lại&quot;

Điều này giúp vòng đời trở nên thân thiện với nhà phát triển. Mỗi thành phần điều hướng sẽ:

  1. Tìm nạp lại trình chạy dịch vụ.
  2. Hãy cài đặt phiên bản này dưới dạng một phiên bản mới ngay cả khi phiên bản giống hệt byte, nghĩa là sự kiện install sẽ chạy và bộ nhớ đệm sẽ cập nhật.
  3. Bỏ qua giai đoạn chờ để trình chạy dịch vụ mới kích hoạt.
  4. Điều hướng trang.

Điều này có nghĩa là bạn sẽ nhận được thông tin cập nhật trên mỗi thao tác điều hướng (bao gồm cả thao tác làm mới) mà không cần tải lại hai lần hoặc đóng thẻ.

Bỏ qua thời gian chờ

Công cụ cho nhà phát triển hiển thị thông báo &quot;bỏ qua thời gian chờ&quot;

Nếu có một worker đang chờ, bạn có thể nhấn vào "bỏ qua đang chờ" trong Công cụ cho nhà phát triển để ngay lập tức thăng cấp worker thành "đang hoạt động".

Shift-reload

Nếu bạn buộc tải lại trang (shift-reload), nó sẽ hoàn toàn bỏ qua trình chạy dịch vụ. Quá trình này sẽ không được kiểm soát. Tính năng này nằm trong quy cách, vì vậy sẽ hoạt động trong các trình duyệt hỗ trợ trình dịch vụ khác.

Xử lý bản cập nhật

Trình chạy dịch vụ được thiết kế như một phần của web có thể mở rộng. Mục đích là chúng tôi, với tư cách là nhà phát triển trình duyệt, thừa nhận rằng chúng tôi không giỏi phát triển web hơn nhà phát triển web. Do đó, chúng ta không nên cung cấp các API cấp cao và hẹp để giải quyết một vấn đề cụ thể bằng cách sử dụng các mẫu chúng tôi thích, mà thay vào đó hãy cho phép bạn truy cập vào phần chính của trình duyệt và cho phép bạn làm theo cách bạn muốn, theo cách phù hợp nhất với người dùng của bạn.

Vì vậy, để cho phép nhiều mẫu nhất có thể, chúng ta có thể quan sát toàn bộ chu kỳ cập nhật:

navigator.serviceWorker.register('/sw.js').then(reg => {
  reg.installing; // the installing worker, or undefined
  reg.waiting; // the waiting worker, or undefined
  reg.active; // the active worker, or undefined

  reg.addEventListener('updatefound', () => {
    // A wild service worker has appeared in reg.installing!
    const newWorker = reg.installing;

    newWorker.state;
    // "installing" - the install event has fired, but not yet complete
    // "installed"  - install complete
    // "activating" - the activate event has fired, but not yet complete
    // "activated"  - fully active
    // "redundant"  - discarded. Either failed install, or it's been
    //                replaced by a newer version

    newWorker.addEventListener('statechange', () => {
      // newWorker.state has changed
    });
  });
});

navigator.serviceWorker.addEventListener('controllerchange', () => {
  // This fires when the service worker controlling this page
  // changes, eg a new worker has skipped waiting and become
  // the new active worker.
});

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

Như bạn có thể thấy, bạn nên hiểu vòng đời của trình chạy dịch vụ và với sự hiểu biết đó, hành vi của trình chạy dịch vụ sẽ có vẻ logic hơn và bớt bí ẩn hơn. Kiến thức đó sẽ giúp bạn tự tin hơn khi triển khai và cập nhật trình chạy dịch vụ.