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

Jake Archibald
Jake Archibald

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

Sau đây là thông tin chi tiết, nhưng các gạch đầu dòng ở đầu mỗi phần sẽ cung cấp 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 một trình chạy dịch vụ mới sẵn sàng 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 chạy đồng thời.

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 nhận được hai thẻ có quan điểm rất khác nhau về cách quản lý bộ nhớ dùng chung của chúng. Việc này có thể dẫn đến lỗi hoặc nghiêm trọng 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 chuyển đến installEvent.waitUntil() cho biết thời lượng và cài đặt thành công hay không thành công.
  • 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 trải qua một 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 thuộc quyền kiểm soát.

Lấy HTML này:

<!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>

Thư viện này đăng ký một trình chạy dịch vụ và thêm hình ảnh của một chú chó sau 3 giây.

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

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 đó 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ó ở 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à phạm vi kiểm soát

Phạm vi mặc định của việc đăng ký trình chạy dịch vụ là ./ so với URL của tập lệnh. Tức 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 ứng dụng được "kiểm soát", các lượt tìm nạp của ứng dụng đó sẽ trải qua trình chạy 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 hoặc gửi ra lỗi trong lần thực thi ban đầu, thì lời hứa đăng ký sẽ bị từ chối và trình chạy 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à một trình chạy dịch vụ nhận được là install. Lệnh 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 dịch vụ. 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ề 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 báo hiệu việc cài đặt không thành công và trình duyệt sẽ loại bỏ worker dịch vụ đó. Quyền này sẽ không bao giờ kiểm soát khách hàng. Điều này nghĩa là chúng ta có thể dựa vào cat.svg trong bộ nhớ đệm đối với các sự kiện fetch. Đó là phần phụ thuộc.

Ứng dụng

Khi trình chạy 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. Nhưng điều đó không có nghĩa là trang có tên là .register() sẽ bị kiểm soát.

Vào lần đầu tiên bạn tải bản minh hoạ, mặc dù dog.svg được yêu cầu rất lâu sau khi trình chạy dịch vụ kích hoạt, nhưng trình chạy dịch vụ này sẽ không xử lý yêu cầu và bạn vẫn thấy hình ảnh của 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ó trình chạy dịch vụ, thì các tài nguyên phụ của trang cũng vậy. Nếu bạn tải bản minh hoạ lần thứ hai (nói cách khác là làm mới trang), thì quá trình này 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 máy khách 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, gọi clients.claim() trong sự kiện activate. Bạn sẽ thấy một chú mèo vào lần đầu tiên. Tôi nói "nên", vì đây là yếu tố nhạy cảm về thời gian. 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 trình chạy dịch vụ của mình để tải các trang khác với cách chúng tải qua mạng, thì clients.claim() có thể gặp rắc rối, vì trình chạy dịch vụ của bạn sẽ kiểm soát một số ứng dụng được tải mà không cần trình chạy đó.

Đang cập nhật trình chạy dịch vụ

Tóm lại:

  • Quá trình cập nhật sẽ được kích hoạt nếu bất kỳ trường hợp nào sau đây xảy ra:
    • Điều hướng đến một trang thuộc phạm vi chi phối.
    • Một sự kiện hoạt động như pushsync, trừ phi có lượt kiểm tra bản cập nhật trong vòng 24 giờ qua.
    • Chỉ gọi .register() khi URL trình chạy 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, mặc định bỏ qua tiêu đề lưu vào bộ nhớ đệm khi kiểm tra các bản cập nhật của tập lệnh trình chạy dịch vụ đã đăng ký. Chúng vẫn tuân thủ tiêu đề lưu vào bộ nhớ đệm khi tìm nạp tài nguyên được tải bên trong một trình chạy dịch vụ 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 sẽ mở rộng phạm vi để bao gồm cả các tập lệnh/mô-đun được nhập.)
  • Trình chạy dịch vụ đã cập nhật được khởi chạy cùng với trình chạy 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 cú pháp, báo 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 chồng chéo nhau trong quá trình làm mới.)
  • self.skipWaiting() ngăn việc chờ đợi, nghĩa là trình chạy dịch vụ sẽ kích hoạt ngay sau khi cài đặt xong.

Giả sử chúng ta đã thay đổi tập lệnh của nhân viên 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

Xin 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 bộ nhớ đệm 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ó cá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, trình chạy dịch vụ đã cập nhật sẽ trì hoãn việc kích hoạt cho đến khi trình chạy dịch vụ hiện tại không còn kiểm soát ứng dụng nữa. Trạng thái này được gọi là "waiting" (đang chờ) và đây là cách trình duyệt đảm bảo rằng tại một thời điểm chỉ có một phiên bản của service worker 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 trình chạy dịch vụ mới đang chờ trong thẻ "Ứng dụng" của Công cụ cho nhà phát triển:

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. Thậm chí trang hiện tại có thể vẫn ở lại nếu câu trả lời có tiêu đề Content-Disposition. Do sự chồng chéo 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 điều hướng khỏi tất cả các thẻ bằng trình chạy dịch vụ hiện tại. Sau đó, khi di chuyển đến bản minh hoạ, bạn sẽ thấy chú 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.

Ứng dụng

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 làm được khi vẫn sử dụng worker cũ, 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ì tính năng này sẽ lưu vào vùng đệm các sự kiện chức năng (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().

Việc này khiến trình chạy dịch vụ loại bỏ worker đang hoạt động và tự kích hoạt ngay khi nó chuyển sang giai đoạn chờ (hoặc ngay lập tức nếu đã ở trong giai đoạn chờ). Việc này không khiến worker bỏ qua quá trình cài đặt mà chỉ chờ.

Việc bạn gọi skipWaiting() không thực sự quan trọng, miễn là thao tác 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ụ. Như trong trường hợp, bạn muốn skipWaiting() sau một tương tác của người dùng.

Đây là bản minh hoạ sử dụng skipWaiting(). Bạn sẽ thấy hình ảnh một con bò mà không phải rời đi. Giống như clients.claim(), đây là một cuộc đua, vì vậy bạn sẽ chỉ thấy cow nếu worker mới của dịch vụ 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ập nhật thủ công

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

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

Nếu muốn người dùng sử dụng trang web của bạn trong một thời gian dài mà không tải lại, bạn nên gọi update() trong 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 cho mỗi phiên bản trình chạy dịch vụ một URL duy nhất. Đừng làm việc này! Đây thường là phương pháp không phù hợp với trình chạy dịch vụ, 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 một sự cố như sau:

  1. index.html đăng ký sw-v1.js làm một 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 của mình.

Nếu bạn làm như vậy, 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 qua bộ nhớ đệm. Bạn đã tự đặt mình vào vị trí mà bạn cần cập nhật trình chạy dịch vụ của mình để cập nhật trình chạy dịch vụ của mình. Kinh

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

Giúp phát triển dễ dàng

Vòng đời của trình chạy dịch vụ được xây dựng có lưu ý đến người dùng, nhưng trong quá trình phát triển thì có 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à trò chơi tôi thích nhất.

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

Thao tác này sẽ thay đổi vòng đời để phù hợp với nhà phát triển. Mỗi thao tác đ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. Di chuyển trên 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 điều hướng (bao gồm cả làm mới) mà không phải 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".

Tải lại Shift

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ụ. Chế độ này sẽ không kiểm soát được. 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ý nội dung 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. Và 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

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 nhân viên dịch vụ.