Di chuyển sang một trình chạy dịch vụ

Thay thế trang nền hoặc trang sự kiện bằng trình chạy dịch vụ

Trình chạy dịch vụ sẽ thay thế trang sự kiện hoặc nền của tiện ích để đảm bảo mã nền không nằm trong luồng chính. Điều này cho phép các tiện ích chỉ chạy khi cần, tiết kiệm tài nguyên.

Trang nền là một thành phần cơ bản của các tiện ích kể từ khi được giới thiệu. Nói một cách đơn giản, các trang ở chế độ nền cung cấp một môi trường độc lập với mọi cửa sổ hoặc thẻ khác. Điều này cho phép các tiện ích quan sát và hành động để phản hồi các sự kiện.

Trang này mô tả các tác vụ để chuyển đổi các trang ở chế độ nền thành worker dịch vụ tiện ích. Để biết thêm thông tin chung về trình chạy dịch vụ tiện ích, hãy xem hướng dẫn Xử lý sự kiện bằng trình chạy dịch vụ và phần Giới thiệu về trình chạy dịch vụ tiện ích.

Sự khác biệt giữa tập lệnh trong nền và worker dịch vụ tiện ích

Trong một số ngữ cảnh, bạn sẽ thấy các worker dịch vụ tiện ích được gọi là "tập lệnh trong nền". Mặc dù trình chạy dịch vụ của tiện ích chạy ở chế độ nền, nhưng việc gọi chúng là tập lệnh nền có phần gây hiểu lầm vì ngụ ý các chức năng giống hệt nhau. Các điểm khác biệt được mô tả dưới đây.

Thay đổi từ các trang ở chế độ nền

Trình chạy dịch vụ có một số điểm khác biệt với trang ở chế độ nền.

  • Các luồng này hoạt động ngoài luồng chính, nghĩa là không can thiệp vào nội dung tiện ích.
  • Các trình bổ trợ này có các chức năng đặc biệt như chặn các sự kiện tìm nạp trên nguồn gốc của tiện ích, chẳng hạn như các sự kiện từ cửa sổ bật lên của thanh công cụ.
  • Các thành phần này có thể giao tiếp và tương tác với các ngữ cảnh khác thông qua Giao diện ứng dụng.

Những thay đổi bạn cần thực hiện

Bạn cần điều chỉnh một vài mã để tính đến sự khác biệt giữa cách hoạt động của tập lệnh ở chế độ nền và trình chạy dịch vụ. Trước tiên, cách chỉ định trình chạy dịch vụ trong tệp kê khai khác với cách chỉ định tập lệnh trong nền. Ngoài ra:

  • Vì không thể truy cập vào DOM hoặc giao diện window, nên bạn cần chuyển các lệnh gọi đó sang một API khác hoặc vào một tài liệu ngoài màn hình.
  • Không được đăng ký trình nghe sự kiện để phản hồi các lời hứa được trả về hoặc bên trong lệnh gọi lại sự kiện.
  • Vì các phương thức này không tương thích ngược với XMLHttpRequest(), nên bạn cần thay thế các lệnh gọi đến giao diện này bằng các lệnh gọi đến fetch().
  • Vì các phiên bản này sẽ chấm dứt khi không được sử dụng, nên bạn cần duy trì trạng thái ứng dụng thay vì dựa vào các biến toàn cục. Việc chấm dứt trình chạy dịch vụ cũng có thể kết thúc bộ hẹn giờ trước khi bộ hẹn giờ hoàn tất. Bạn cần thay thế các thông báo đó bằng chuông báo.

Trang này mô tả chi tiết những việc cần làm này.

Cập nhật trường "background" (nền) trong tệp kê khai

Trong tệp kê khai Manifest V3, các trang trong nền được thay thế bằng trình chạy dịch vụ. Các thay đổi về tệp kê khai được liệt kê bên dưới.

  • Thay thế "background.scripts" bằng "background.service_worker" trong manifest.json. Xin lưu ý rằng trường "service_worker" nhận một chuỗi, chứ không phải một mảng chuỗi.
  • Xoá "background.persistent" khỏi manifest.json.
Tệp kê khai V2
{
  ...
  "background": {
    "scripts": [
      "backgroundContextMenus.js",
      "backgroundOauth.js"
    ],
    "persistent": false
  },
  ...
}
Tệp kê khai V3
{
  ...
  "background": {
    "service_worker": "service_worker.js",
    "type": "module"
  }
  ...
}

Trường "service_worker" nhận một chuỗi duy nhất. Bạn chỉ cần trường "type" nếu sử dụng mô-đun ES (sử dụng từ khoá import). Giá trị của thuộc tính này sẽ luôn là "module". Để biết thêm thông tin, hãy xem phần Kiến thức cơ bản về trình chạy dịch vụ tiện ích

Di chuyển các lệnh gọi DOM và cửa sổ sang một tài liệu ngoài màn hình

Một số tiện ích cần quyền truy cập vào DOM và các đối tượng cửa sổ mà không cần mở cửa sổ hoặc thẻ mới. API Ngoài màn hình hỗ trợ các trường hợp sử dụng này bằng cách mở và đóng các tài liệu không hiển thị được đóng gói bằng tiện ích mà không làm gián đoạn trải nghiệm người dùng. Ngoại trừ việc truyền thông báo, tài liệu ngoài màn hình không chia sẻ API với các ngữ cảnh tiện ích khác, mà hoạt động như các trang web đầy đủ để các tiện ích tương tác.

Để sử dụng API Ngoài màn hình, hãy tạo một tài liệu ngoài màn hình từ worker dịch vụ.

chrome.offscreen.createDocument({
  url: chrome.runtime.getURL('offscreen.html'),
  reasons: ['CLIPBOARD'],
  justification: 'testing the offscreen API',
});

Trong tài liệu ngoài màn hình, hãy thực hiện bất kỳ hành động nào mà trước đây bạn đã chạy trong tập lệnh ở chế độ nền. Ví dụ: bạn có thể sao chép văn bản đã chọn trên trang lưu trữ.

let textEl = document.querySelector('#text');
textEl.value = data;
textEl.select();
document.execCommand('copy');

Giao tiếp giữa các tài liệu ngoài màn hình và worker dịch vụ tiện ích bằng cách chuyển tin nhắn.

Chuyển đổi localStorage sang một loại khác

Không thể sử dụng giao diện Storage của nền tảng web (có thể truy cập từ window.localStorage) trong trình chạy dịch vụ. Để giải quyết vấn đề này, hãy làm theo một trong hai cách sau. Trước tiên, bạn có thể thay thế phương thức này bằng các lệnh gọi đến một cơ chế lưu trữ khác. Không gian tên chrome.storage.local sẽ phục vụ hầu hết các trường hợp sử dụng, nhưng bạn cũng có thể sử dụng các tuỳ chọn khác.

Bạn cũng có thể di chuyển các lệnh gọi của tài liệu này sang một tài liệu ngoài màn hình. Ví dụ: để di chuyển dữ liệu trước đây được lưu trữ trong localStorage sang một cơ chế khác:

  1. Tạo một tài liệu ngoài màn hình bằng một quy trình chuyển đổi và trình xử lý runtime.onMessage.
  2. Thêm một quy trình chuyển đổi vào tài liệu ngoài màn hình.
  3. Trong worker dịch vụ tiện ích, hãy kiểm tra chrome.storage để biết dữ liệu của bạn.
  4. Nếu không tìm thấy dữ liệu, hãy create một tài liệu ngoài màn hình và gọi runtime.sendMessage() để bắt đầu quy trình chuyển đổi.
  5. Trong trình xử lý runtime.onMessage mà bạn đã thêm vào tài liệu ngoài màn hình, hãy gọi quy trình chuyển đổi.

Ngoài ra, còn có một số điểm khác biệt về cách hoạt động của API bộ nhớ web trong các tiện ích. Tìm hiểu thêm trong phần Bộ nhớ và cookie.

Đăng ký trình nghe một cách đồng bộ

Việc đăng ký trình nghe không đồng bộ (ví dụ: bên trong một lời hứa hoặc lệnh gọi lại) không được đảm bảo sẽ hoạt động trong Tệp kê khai V3. Hãy xem xét mã sau.

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.browserAction.setBadgeText({ text: badgeText });
  chrome.browserAction.onClicked.addListener(handleActionClick);
});

Cách này hoạt động với trang nền ổn định vì trang này liên tục chạy và không bao giờ được khởi tạo lại. Trong Tệp kê khai V3, trình chạy dịch vụ sẽ được khởi tạo lại khi sự kiện được gửi. Điều này có nghĩa là khi sự kiện kích hoạt, trình nghe sẽ không được đăng ký (vì chúng được thêm không đồng bộ) và sự kiện sẽ bị bỏ lỡ.

Thay vào đó, hãy di chuyển phần đăng ký trình nghe sự kiện lên cấp cao nhất của tập lệnh. Điều này đảm bảo rằng Chrome có thể tìm thấy và gọi ngay trình xử lý lượt nhấp của hành động, ngay cả khi tiện ích của bạn chưa hoàn tất việc thực thi logic khởi động.

chrome.action.onClicked.addListener(handleActionClick);

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.action.setBadgeText({ text: badgeText });
});

Thay thế XMLHttpRequest() bằng fetch() chung

Không thể gọi XMLHttpRequest() từ một worker dịch vụ, tiện ích hoặc bất kỳ cách nào khác. Thay thế các lệnh gọi từ tập lệnh trong nền đến XMLHttpRequest() bằng các lệnh gọi đến fetch() toàn cục.

XMLHttpRequest()
const xhr = new XMLHttpRequest();
console.log('UNSENT', xhr.readyState);

xhr.open('GET', '/api', true);
console.log('OPENED', xhr.readyState);

xhr.onload = () => {
    console.log('DONE', xhr.readyState);
};
xhr.send(null);
fetch()
const response = await fetch('https://www.example.com/greeting.json'')
console.log(response.statusText);

Duy trì trạng thái

Worker dịch vụ là tạm thời, tức là chúng có thể bắt đầu, chạy và chấm dứt liên tục trong phiên trình duyệt của người dùng. Điều này cũng có nghĩa là dữ liệu không có sẵn ngay lập tức trong các biến toàn cục vì ngữ cảnh trước đó đã bị phá bỏ. Để giải quyết vấn đề này, hãy sử dụng API bộ nhớ làm nguồn đáng tin cậy. Sau đây là ví dụ về cách thực hiện việc này.

Ví dụ sau đây sử dụng một biến toàn cục để lưu trữ tên. Trong một worker dịch vụ, biến này có thể được đặt lại nhiều lần trong phiên trình duyệt của người dùng.

Tập lệnh trong nền của Manifest V2
let savedName = undefined;

chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    savedName = name;
  }
});

chrome.browserAction.onClicked.addListener((tab) => {
  chrome.tabs.sendMessage(tab.id, { name: savedName });
});

Đối với Tệp kê khai V3, hãy thay thế biến toàn cục bằng lệnh gọi đến API bộ nhớ.

Trình chạy dịch vụ Manifest V3
chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    chrome.storage.local.set({ name });
  }
});

chrome.action.onClicked.addListener(async (tab) => {
  const { name } = await chrome.storage.local.get(["name"]);
  chrome.tabs.sendMessage(tab.id, { name });
});

Chuyển đổi đồng hồ hẹn giờ thành chuông báo

Thông thường, bạn có thể sử dụng các thao tác bị trì hoãn hoặc định kỳ bằng phương thức setTimeout() hoặc setInterval(). Tuy nhiên, các API này có thể không hoạt động trong trình chạy dịch vụ vì bộ hẹn giờ bị huỷ bất cứ khi nào trình chạy dịch vụ bị chấm dứt.

Tập lệnh trong nền của Manifest V2
// 3 minutes in milliseconds
const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

Thay vào đó, hãy sử dụng Alarms API. Giống như các trình nghe khác, trình nghe chuông báo phải được đăng ký ở cấp cao nhất của tập lệnh.

Trình chạy dịch vụ Manifest V3
async function startAlarm(name, duration) {
  await chrome.alarms.create(name, { delayInMinutes: 3 });
}

chrome.alarms.onAlarm.addListener(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
});

Duy trì trình chạy dịch vụ

Theo định nghĩa, trình chạy dịch vụ là do sự kiện điều khiển và sẽ chấm dứt khi không hoạt động. Bằng cách này, Chrome có thể tối ưu hoá hiệu suất và mức sử dụng bộ nhớ của tiện ích. Tìm hiểu thêm trong tài liệu về vòng đời của worker dịch vụ. Trong một số trường hợp ngoại lệ, bạn có thể phải áp dụng thêm các biện pháp để đảm bảo rằng worker dịch vụ vẫn hoạt động trong thời gian dài hơn.

Duy trì hoạt động của worker cho đến khi một thao tác chạy trong thời gian dài hoàn tất

Trong quá trình thực hiện các thao tác lâu dài của worker dịch vụ không gọi API tiện ích, worker dịch vụ có thể ngừng hoạt động giữa chừng. Ví dụ:

  • Yêu cầu fetch() có thể mất nhiều hơn 5 phút (ví dụ: tải xuống một tệp lớn trên kết nối có thể kém).
  • Một phép tính không đồng bộ phức tạp mất hơn 30 giây.

Để kéo dài thời gian hoạt động của worker dịch vụ trong những trường hợp này, bạn có thể định kỳ gọi một API tiện ích nhỏ để đặt lại bộ đếm thời gian chờ. Xin lưu ý rằng cách này chỉ dành cho các trường hợp ngoại lệ và trong hầu hết các trường hợp, thường có một cách tốt hơn, phù hợp với nền tảng để đạt được kết quả tương tự.

Ví dụ sau đây cho thấy một hàm trợ giúp waitUntil() giúp duy trì trình chạy dịch vụ cho đến khi một lời hứa nhất định được giải quyết:

async function waitUntil(promise) = {
  const keepAlive = setInterval(chrome.runtime.getPlatformInfo, 25 * 1000);
  try {
    await promise;
  } finally {
    clearInterval(keepAlive);
  }
}

waitUntil(someExpensiveCalculation());

Duy trì liên tục một worker dịch vụ

Trong một số ít trường hợp, bạn cần phải kéo dài thời gian hoạt động vô thời hạn. Chúng tôi nhận thấy doanh nghiệp và giáo dục là những trường hợp sử dụng lớn nhất, nên chúng tôi cho phép việc này ở đó, nhưng nói chung thì chúng tôi không hỗ trợ việc này. Trong những trường hợp ngoại lệ này, bạn có thể duy trì hoạt động của worker dịch vụ bằng cách định kỳ gọi một API tiện ích không quan trọng. Xin lưu ý rằng đề xuất này chỉ áp dụng cho các tiện ích chạy trên thiết bị được quản lý cho trường hợp sử dụng doanh nghiệp hoặc giáo dục. Trong các trường hợp khác, việc này không được phép và nhóm tiện ích Chrome có quyền áp dụng biện pháp xử lý đối với những tiện ích đó trong tương lai.

Sử dụng đoạn mã sau để duy trì worker dịch vụ:

/**
 * Tracks when a service worker was last alive and extends the service worker
 * lifetime by writing the current time to extension storage every 20 seconds.
 * You should still prepare for unexpected termination - for example, if the
 * extension process crashes or your extension is manually stopped at
 * chrome://serviceworker-internals. 
 */
let heartbeatInterval;

async function runHeartbeat() {
  await chrome.storage.local.set({ 'last-heartbeat': new Date().getTime() });
}

/**
 * Starts the heartbeat interval which keeps the service worker alive. Call
 * this sparingly when you are doing work which requires persistence, and call
 * stopHeartbeat once that work is complete.
 */
async function startHeartbeat() {
  // Run the heartbeat once at service worker startup.
  runHeartbeat().then(() => {
    // Then again every 20 seconds.
    heartbeatInterval = setInterval(runHeartbeat, 20 * 1000);
  });
}

async function stopHeartbeat() {
  clearInterval(heartbeatInterval);
}

/**
 * Returns the last heartbeat stored in extension storage, or undefined if
 * the heartbeat has never run before.
 */
async function getLastHeartbeat() {
  return (await chrome.storage.local.get('last-heartbeat'))['last-heartbeat'];
}