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

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

Một trình chạy dịch vụ thay thế trang sự kiện hoặc nền của tiện ích để đảm bảo rằng mã nền luôn nằm ngoài luồng chính. Nhờ đó, tiện ích chỉ chạy khi cần và tiết kiệm tài nguyên.

Kể từ khi ra mắt, trang nền đã là một thành phần cơ bản của tiện ích. Nói một cách đơn giản, trang nền cung cấp một môi trường hoạt động độc lập với bất kỳ cửa sổ hoặc thẻ nào 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 thao tác để chuyển đổi trang nền thành trình chạy dịch vụ tiện ích. Để biết thêm thông tin về trình chạy dịch vụ tiện ích nói chung, 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 nền và trình chạy dịch vụ tiện ích

Trong một số ngữ cảnh, bạn sẽ thấy trình chạy dịch vụ tiện ích được gọi là "tập lệnh nền". Mặc dù trình chạy dịch vụ tiện ích có 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 khi ngụ ý các khả năng giống nhau. Sự khác biệt được mô tả dưới đây.

Thay đổi từ trang nền

Service worker có một số khác biệt với trang nền.

  • Các tiện ích này hoạt động bên ngoài luồng chính, nghĩa là không can thiệp vào nội dung tiện ích.
  • Chúng có các tính năng đặc biệt, chẳng hạn 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 trong cửa sổ bật lên trên thanh công cụ.
  • Ứng dụng 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 sẽ cần thực hiện một vài điều chỉnh mã để tính đến sự khác biệt giữa cách hoạt động của tập lệnh nền và trình chạy dịch vụ. Để bắt đầu, cách chỉ định một trình chạy dịch vụ trong tệp kê khai sẽ khác với cách chỉ định tập lệnh nền. Ngoài ra:

  • Vì các lệnh gọi này không thể truy cập vào DOM hoặc giao diện window, nên bạn sẽ phải chuyển những lệnh gọi như vậy sang một API khác hoặc vào một tài liệu ngoài màn hình.
  • Bạn không được đăng ký trình nghe sự kiện để phản hồi lời hứa được trả về hoặc lệnh gọi lại sự kiện bên trong.
  • Vì các lệnh gọi 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 biến này 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 đồng hồ hẹn giờ trước khi hoàn tất. Bạn cần thay thế các chuông báo này 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" trong tệp kê khai

Trong Manifest V3, các trang nền được thay thế bằng một service worker. Những thay đổi đối với tệp kê khai được liệt kê dưới đây.

  • Thay thế "background.scripts" bằng "background.service_worker" trong manifest.json. Lưu ý 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.
Manifest V2
{
  ...
  "background": {
    "scripts": [
      "backgroundContextMenus.js",
      "backgroundOauth.js"
    ],
    "persistent": false
  },
  ...
}
Manifest V3
{
  ...
  "background": {
    "service_worker": "service_worker.js",
    "type": "module"
  }
  ...
}

Trường "service_worker" nhận một chuỗi duy nhất. Bạn sẽ chỉ cần trường "type" nếu sử dụng các mô-đun ES (bằ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 Thông tin cơ bản về trình chạy dịch vụ tiện ích

Di chuyển lệnh gọi DOM và lệnh gọi cửa sổ sang 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à đối tượng cửa sổ mà không cần mở cửa sổ hoặc thẻ mới một cách trực quan. Offscreen API hỗ trợ những 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ị đi kèm với 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 nhưng có chức năng như trang web đầy đủ để 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ừ trình chạy 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 đó bạn đã chạy trong tập lệnh 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 tài liệu ngoài màn hình và trình chạy dịch vụ tiện ích bằng cách sử dụng tính năng truyền tin nhắn.

Chuyển đổi localStorage sang 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 một trong hai việc sau. Trước tiên, bạn có thể thay thế cơ chế 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ó thể dùng các tuỳ chọn khác.

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

  1. Tạo tài liệu ngoài màn hình có 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 trình chạy dịch vụ tiện ích, hãy kiểm tra chrome.storage để xem dữ liệu của bạn.
  4. Nếu bạn không tìm thấy dữ liệu của mình, hãy tạo 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ố khác biệt về cách hoạt động của API lưu trữ web trong 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 Manifest 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 một trang nền cố định vì trang này liên tục chạy và không bao giờ khởi chạy lại. Trong Manifest V3, service worker sẽ được khởi động lại khi sự kiện được gửi đ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ỏ qua.

Thay vào đó, hãy chuyển đăng ký trình nghe sự kiện lên cấp cao nhất trong tập lệnh của bạn. Điều này đảm bảo rằng Chrome có thể tìm và gọi ngay trình xử lý lượt nhấp của hành động, ngay cả khi tiện ích 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 phương thức tìm nạp chung()

Không thể gọi XMLHttpRequest() từ một trình chạy dịch vụ, tiện ích hoặc các trường hợp khác. Thay thế các lệnh gọi từ tập lệnh nền tới XMLHttpRequest() bằng các lệnh gọi đến fetch() chung.

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

Trạng thái cố định

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

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

Tập lệnh nền 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 Manifest V3, hãy thay thế biến toàn cục bằng lệnh gọi đến Storage API.

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ì bạn sẽ dùng các toán tử 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ộ tính 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 nền 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 API Chuông báo. 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 trong tập lệnh của bạn.

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(),
  });
});

Bảo vệ trình chạy dịch vụ

Theo định nghĩa, trình chạy dịch vụ sẽ hoạt động theo sự kiện và sẽ chấm dứt khi không hoạt động. Nhờ đó, 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 trình chạy dịch vụ của chúng tôi. Các trường hợp ngoại lệ có thể yêu cầu các biện pháp bổ sung để đảm bảo một trình chạy dịch vụ tồn tại trong thời gian dài hơn.

Duy trì một trình chạy dịch vụ cho đến khi một thao tác diễn ra trong thời gian dài hoàn tất

Trong các hoạt động của trình chạy dịch vụ lâu dài mà không gọi API tiện ích, trình chạy dịch vụ này có thể tắt trong quá trình hoạt động. Ví dụ:

  • Yêu cầu fetch() có thể mất hơn 5 phút (ví dụ: tải một tệp lớn xuống khi 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 vòng đời của trình chạy 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 hết thời gian chờ. Xin lưu ý rằng cách này chỉ dành cho các trường hợp đặc biệt và trong hầu hết các trường hợp, thường có một cách đặc trưng và phù hợp với nền tảng hơn để đạt được kết quả tương tự.

Ví dụ sau đây cho thấy hàm trợ giúp waitUntil() giúp trình chạy dịch vụ của bạn hoạt động 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());

Liên tục giúp một trình chạy dịch vụ hoạt động

Trong một số ít trường hợp, cần phải kéo dài thời gian tồn tại vô thời hạn. Chúng tôi xác định rằng doanh nghiệp và giáo dục là những trường hợp sử dụng chủ yếu nhất, và đặc biệt là cho phép việc này, nhưng nói chung chúng tôi không hỗ trợ việc này. Trong những trường hợp đặc biệt này, bạn có thể duy trì một trình chạy dịch vụ bằng cách gọi định kỳ một API tiện ích đơn giản. 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 của doanh nghiệp hoặc giáo dục. Tiện ích này không được phép trong các trường hợp khác và nhóm tiện ích của Chrome giữ quyền thực hiện hành động đối với các tiện ích đó trong tương lai.

Sử dụng đoạn mã sau đây để giữ cho trình chạy dịch vụ của bạn tiếp tục hoạt động:

/**
 * 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'];
}