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 không biết chiến dịch đang cố gắng làm gì và lợi ích gì, thì bạn sẽ cảm thấy như đang đối đầu với mình. Nhưng khi hiểu rõ cách hoạt động của nền tảng này, bạn có thể cung cấp các 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 gì tốt nhất giữa web và mẫu gốc.

Đây là thông tin chi tiết, nhưng phần gạch đầ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à:

  • Bật chế độ ưu tiên 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 hiện tại.
  • Đảm bảo trang trong phạm vi chi phối đượ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 cùng một lúc chỉ có một phiên bản trang web chạy.

Điều cuối cùng đó 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, rồi mở một thẻ khác. Do đó, có thể hai phiên bản trang web của bạn chạy cùng một lúc. Đôi khi việc này cũng không sao cả, nhưng nếu đang giải quyết vấn đề bộ nhớ, bạn có thể dễ dàng có hai thẻ có ý kiến rất khác nhau về cách quản lý bộ nhớ dùng chung. 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() báo hiệu thời lượng và thành công hay không thành công của lượt 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 trình chạy này hoàn tất quá trình cài đặt và chuyển sang trạng thái "đang hoạt động".
  • Theo mặc định, hoạt động tìm nạp của một trang sẽ không trải qua trình chạy dịch vụ trừ phi yêu cầu trang đó được thực hiện thông qua một trình chạy dịch vụ. Vì vậy, bạn cần làm mới trang để xem tác động của trình chạy dịch vụ.
  • clients.claim() có thể ghi đè chế độ 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 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>

Dịch vụ này đăng ký một nhân viên 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:

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'));
  }
});

Tệp 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ó trong lần đầu tiên tải trang. Hãy nhấn vào làm mới và bạn sẽ thấy con mèo.

Phạm vi và khả năng kiểm soát

Phạm vi mặc định của đăng ký trình chạy dịch vụ là ./ tương ứng với URL của 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ì phạm vi mặc định là //example.com/foo/.

Chúng ta gọi trang, trình thực thi và trình thực thi 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 trong phạm vi chi phối của chính sách. Sau khi một ứng dụng được "kiểm soát", các lượt tìm nạp của ứng dụng đó sẽ chuyển 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 thông qua navigator.serviceWorker.controller hay không. Đây là giá trị rỗng hoặc là 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 thể tải xuống, phân tích cú pháp hoặc báo lỗi trong lần thực thi đầu tiên, lời hứa đăng ký sẽ 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 hiển thị 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à 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 trình chạy dịch vụ. Nếu bạn thay đổi tập lệnh của trình chạy dịch vụ, trình duyệt sẽ coi đây là một trình chạy dịch vụ khác và sẽ nhận được sự kiện install của riêng mình. Tôi sẽ nói 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ứ cần thiết vào bộ nhớ đệm trước khi có thể kiểm soát các ứng dụng. Lời hứa bạn truyề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 nó 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ẽ ném worker đi. Nó sẽ không bao giờ kiểm soát khách hàng. Tức là chúng ta không thể dựa vào việc cat.svg hiện diện trong bộ nhớ đệm trong các sự kiện fetch. Đó là phần phụ thuộc.

Ứng dụng

Sau 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 sự kiện activate. Nhưng điều đó không có nghĩa là trang được 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 rất lâu sau khi trình chạy dịch vụ kích hoạt, nhưng nó không xử lý yêu cầu và bạn vẫn thấy hình ảnh của chú chó. Tuỳ chọn mặc định là tính nhất quán. Nếu trang của bạn tải mà không cần trình chạy dịch vụ, thì 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ì tính năng này sẽ được kiểm soát. Cả trang và hình ảnh đều sẽ trải qua fetch sự kiện 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 trình chạy dịch vụ sau khi trình chạy này được kích hoạt.

Đây là biến thể của bản minh hoạ ở trên gọi clients.claim() trong sự kiện activate. Bạn sẽ nhìn thấy một chú mèo vào lần đầu tiên. Tôi nói "nên" vì vấn đề này có giới hạn về thời gian. Bạn sẽ chỉ nhìn thấy một con 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ụ để tải trang khác với cách tải trang thông qua mạng, thì clients.claim() có thể gây ra 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 dịch vụ đó.

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 một trong những trường hợp sau đây xảy ra:
    • Thao tác điều hướng đến một trang thuộc phạm vi.
    • Các sự kiện hoạt động như pushsync, trừ phi có bước kiểm tra bản cập nhật trong vòng 24 giờ trước.
    • Chỉ gọi .register() khi URL của trình chạy dịch vụ đã thay đổi. Tuy nhiên, bạn nên tránh thay đổi URL trình thực thi.
  • Hầu hết các trình duyệt, bao gồm Chrome 68 trở lên, mặc định bỏ qua tiêu đề lưu vào bộ nhớ đệm khi kiểm tra xem có bản cập nhật của tập lệnh trình chạy dịch vụ đã đăng ký hay không. Các tiêu đề này vẫn tuân theo tiêu đề lưu vào bộ nhớ đệm khi tìm nạp các 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ụ.
  • Trình chạy dịch vụ của bạn được coi là được cập nhật nếu trình chạy này khác với byte mà trình duyệt đã có. (Chúng tôi sẽ mở rộng phạm vi này để bao gồm cả các tập lệnh/mô-đun đã nhập.)
  • Trình chạy dịch vụ đã cập nhật được chạy cùng với trình chạy hiện có và nhận được sự kiện install riêng.
  • Nếu worker mới có mã trạng thái không OK (ví dụ: 404), không thể phân tích cú pháp, lỗi trong quá trình thực thi hoặc từ chối worker trong quá trình cài đặt, 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 không có ứng dụng khách. (Lưu ý rằng các ứng dụng trùng lặp trong quá trình làm mới.)
  • self.skipWaiting() giúp tránh phải 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 của 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ạ nội dung trên. Bạn vẫn sẽ thấy hình ảnh một con mèo. Sau đây là lý do...

Cài đặt

Xin lưu ý rằng tôi đã đổ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 đè nội dung trong 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 dành riêng cho phiên bản, gần giống với các nội dung mà ứng dụng gốc sẽ gói với tệp thực thi của ứng dụng đó. Có thể bạn cũng 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 quá trình 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 khách nữa. Trạng thái này được gọi là "waiting" (đ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 dịch vụ của bạn chạy.

Nếu chạy bản minh hoạ cập nhật, bạn vẫn sẽ thấy hình ảnh con 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ỉ có một thẻ mở bản minh hoạ, việc làm mới trang vẫn chưa đủ để phiên bản mới chiếm ưu thế. Lý do là cách thức 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, và 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ự 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.

Để cập nhật, hãy đóng hoặc di chuyển khỏi tất cả các thẻ bằng trình chạy dịch vụ hiện tại. Sau đó, khi điều hướng đến bản minh hoạ một lần nữa, bạn sẽ thấy con ngựa.

Mẫu này tương tự như cách Chrome cập nhật. Các bản cập nhật đối với 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à khó khăn trong quá trình phát triển nhưng Công cụ cho nhà phát triển có một số 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 phần sau của bài viết này.

Ứng dụng

Thao tác này sẽ kích hoạt sau khi trình chạy dịch vụ cũ không còn nữa và trình chạy dịch vụ mới của bạn có thể kiểm soát ứng dụng. Đây là thời điểm lý tưởng để thực hiện những việc mà bạn không thể làm khi worker cũ vẫn được sử dụ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ẽ xuất hiện, và trong trường hợp activate, tôi xoá mọi bộ nhớ đệm còn lại. Thao tác này sẽ xoá bộ nhớ đệm cũ của static-v1.

Nếu bạn truyền một lời hứa đến event.waitUntil(), thì các sự kiện chức năng đó sẽ lưu 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 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ờ có 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 trình chạy dịch vụ mới sớm hơn bằng cách gọi self.skipWaiting().

Thao tác này sẽ khiến worker đang hoạt động bị loại ra 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 nó đã trong giai đoạn chờ). Điều này không khiến worker của bạn bỏ qua bước cài đặt mà chỉ đang chờ.

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ì bạn nên gọi lệnh này trong sự kiện install:

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

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

Tuy nhiên, bạn nên gọi đây là kết quả của postMessage() cho trình chạy dịch vụ. Như trong, 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 của một con bò mà không cần phải rời đi. Giống như clients.claim(), đó là một cuộc đua, vì vậy bạn sẽ chỉ nhìn thấy bò này 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ậ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 chức nă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 mình trong một thời gian dài mà không tải lại, thì bạn nên gọi update() theo 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 về lưu vào bộ nhớ đệm, bạn có thể cân nhắc cung cấp một URL riêng biệt cho mỗi phiên bản trình chạy dịch vụ của mình. Đừng làm thế! Đây thường là một phương pháp không hợp lệ đối với trình chạy dịch vụ, chỉ cần cập nhật tập lệnh tại vị trí hiện tại của tập lệnh.

Việc này có thể khiến bạn gặp phải những 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 trước tiên khi không có mạng.
  3. Bạn cập nhật index.html để đăng ký sw-v2.js mới và nổi bật.

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 đã 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ập nhật trình chạy dịch vụ của mình. Tập

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. Đây không phải là việc tôi sẽ làm trong quá trình sản xuất.

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 dành cho người dùng, nhưng có một chút khó khăn trong quá trình phát triển. Rất may là có một vài công cụ sẽ giúp bạn:

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

Đây là câu hỏi mà tôi yêu thích.

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 để trở nên thân thiện với nhà phát triển. Mỗi cách di chuyển 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 mã giống hệt nhau, 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ờ để kích hoạt trình chạy dịch vụ mới.
  4. Di chuyển trên trang.

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

Bỏ qua bước chờ

Công cụ cho nhà phát triển hiển thị trạng thái &quot;đang chờ bỏ qua&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 chuyển nó lên trạng thái "đang hoạt động".

Shift-load

Nếu bạn buộc tải lại trang (shift-reload), trang này sẽ bỏ qua trình chạy dịch vụ hoàn toàn. Nó sẽ không được kiểm soát. Tính năng này nằm trong phần thông số kỹ thuật nên hoạt động trong các trình duyệt hỗ trợ service-worker 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. Ý 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 làm tốt hơn nhà phát triển web so với nhà phát triển web. Và 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 tôi thích và thay vào đó cung cấp cho bạn quyền truy cập vào các phần nội dung 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 cho người dùng của bạn.

Vì vậy, để có thể ghi nhận được nhiều mẫu nhất có thể, bạn có thể quan sát được 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 cứ tiếp diễn

Như bạn có thể thấy, việc hiểu được vòng đời của trình chạy dịch vụ là cần thiết — và với sự hiểu biết đó, hành vi của trình chạy dịch vụ có vẻ logic 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 trình chạy dịch vụ.