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à lợi ích của công cụ này là 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 đ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.

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

  • Cho phép sử dụng khi không có mạng.
  • 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 rằng một trang thuộc 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ụ) trong suốt quá trình.
  • Đảm bảo chỉ có một phiên bản trang web đang chạy cùng một lúc.

Điều cuối cùng đó khá quan trọng. Nếu không có worker 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à worker dịch vụ nhận được và sự kiện này 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.
  • Worker dịch vụ sẽ không nhận được các sự kiện như fetchpush cho đến khi cài đặt xong và trở thành "đang hoạt động".
  • Theo mặc định, các lệnh tìm nạp của trang sẽ không đi qua trình chạy dịch vụ trừ phi chính yêu cầu trang đã đi qua trình chạy dịch vụ. Vì vậy, bạn cần làm mới trang để xem hiệu quả của worker dịch vụ.
  • 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 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'));
  }
});

Phương thứ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 đó bất cứ khi nào có yêu cầu về /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. Worker 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 khách có được kiểm soát thông qua navigator.serviceWorker.controller (sẽ là giá trị rỗng) hay một thực thể trình chạy dịch vụ hay không.

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ụ của Chrome cho nhà phát triển cho thấy lỗi trong bảng điều khiển và trong phần worker dịch vụ của thẻ ứng dụng:

Lỗi hiển thị trong thẻ DevTools 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 worker của mình, trình duyệt sẽ coi đó là một worker 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ứ bạn cần vào bộ nhớ đệm trước khi có thể kiểm soát ứng dụng. Lời hứa mà bạn truyền đến event.waitUntil() cho trình duyệt biết thời điểm cài đặt hoàn tất và liệu quá trình cài đặt 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

Sau 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à 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 worker dịch vụ sau khi worker 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 con mèo nếu worker dịch vụ 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 khách đã 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 của worker.
  • 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 nội dung 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ý worker dịch vụ.
  • Trình chạy dịch vụ của bạn được coi là đã cập nhật nếu khác với trình chạy dịch vụ mà 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ập nhật sẽ wait cho đến khi worker hiện tại không kiểm soát được ứng dụng nào. (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 vẫn sẽ thấy hình ảnh một con mèo. Lý do là…

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 ghi đè các mục trong bộ nhớ đệm hiện tại mà worker dịch vụ cũ vẫn đang sử dụng.

Mẫu này tạo bộ nhớ đệm dành riêng cho phiên bản, tương tự như các thành phần mà ứng dụng gốc sẽ đóng gói với 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ập nhật, bạn vẫn sẽ thấy hình ảnh một con mèo vì worker V2 chưa được 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ẻ cho bản minh hoạ, 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 thao tác điều hướng trong 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.

Mẫu này tương tự như cách Chrome cập nhật. Bản cập nhật cho Chrome sẽ tải xuống trong 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 đề khó khăn trong quá trình phát triển, nhưng DevTools có các cách giúp bạn dễ dàng hơn. Tôi sẽ đề cập đến vấn đề 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 sau khi worker cũ biến mất và worker mới có thể kiểm soát các ứng dụ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 đó và trong sự kiện activate, tôi loại bỏ mọi bộ nhớ đệm khác, giúp 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 sẽ hoàn tất.

Bỏ qua giai đoạn chờ

Giai đoạn chờ nghĩa là bạn chỉ chạy một phiên bản 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 worker 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 bỏ qua việc cài đặt, mà chỉ chờ đợi.

Thời điểm bạn gọi skipWaiting() không thực sự quan trọng, miễn là trong hoặc trước khi chờ. Thông thường, bạn sẽ gọi phương thức này 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 phương thức này dưới dạng kết quả của postMessage() cho worker 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 worker 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 tốt đối với worker dịch vụ, bạn chỉ cần cập nhật tập lệnh tại vị trí hiện tại.

Điều này có thể khiến bạn gặp phải vấn đề 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 để hoạt động ngoại tuyến trước.
  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ụ. Vì vậy, để 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, có một số 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. Cài đặt ứng dụng này dưới dạng phiên bản mới ngay cả khi ứng dụng này giống hệt nhau theo byte, nghĩa là sự kiện install của bạn 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 "skip waiting" (bỏ qua trạng thái chờ) trong DevTools để ngay lập tức chuyển worker đó sang trạng thái "active" (đang hoạt động).

Shift-reload

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

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. Ý tưởng 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 các nhà phát triển web. Do đó, chúng tôi không nên cung cấp các API cấp cao 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 ta thích, mà thay vào đó, chúng tôi sẽ cấp cho bạn quyền truy cập vào phần cốt lõi của trình duyệt và cho phép bạn thực hiện 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, để bật nhiều mẫu nhất có thể, toàn bộ chu kỳ cập nhật đều có thể quan sát được:

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 rõ vòng đời của worker dịch vụ. Khi hiểu rõ điều đó, hành vi của worker dịch vụ sẽ có vẻ hợp lý hơn và í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 worker dịch vụ.