遷移至 Service Worker

將背景或事件頁面替換為 Service Worker

Service Worker 會取代擴充功能的背景或事件頁面,確保背景程式碼不會留在主執行緒上。這樣一來,擴充功能只會在需要時執行,節省資源。

背景網頁自推出以來,一直是擴充功能的基本元件。簡單來說,背景網頁提供的環境獨立於任何其他視窗或分頁。這可讓擴充功能觀察事件並採取因應措施。

本頁說明將背景網頁轉換為擴充功能 Service Worker 的工作。如要進一步瞭解擴充功能 Service Worker,請參閱「使用 Service Worker 處理事件」教學課程和「關於擴充功能 Service Worker」一節。

背景指令碼和擴充功能 Service Worker 的差異

在某些情況下,擴充功能 Service Worker 會稱為「背景指令碼」。雖然擴充功能 Service Worker 確實會在背景執行,但將其稱為背景指令碼,可能會讓人誤以為兩者功能相同。差異如下。

背景網頁異動

Service Worker 與背景網頁有許多不同之處。

  • 它們在主執行緒外運作,因此不會干擾擴充功能內容。
  • Service Worker 具備特殊功能,例如攔截擴充功能來源的擷取事件,像是工具列彈出式視窗的事件。
  • 他們可以透過用戶端介面與其他情境通訊及互動。

需要進行的變更

您需要調整部分程式碼,以因應背景指令碼和 Service Worker 運作方式的差異。首先,在資訊清單檔案中指定 Service Worker 的方式與指定背景指令碼的方式不同。此外:

  • 由於這類指令碼無法存取 DOM 或 window 介面,因此您必須將這類呼叫移至其他 API 或螢幕外文件。
  • 請勿註冊事件監聽器來回應傳回的 Promise,或在事件回呼中註冊事件監聽器。
  • 由於這些方法與 XMLHttpRequest() 不回溯相容,因此您必須將對這個介面的呼叫替換為對 fetch() 的呼叫。
  • 由於函式會在不使用時終止,因此您必須保留應用程式狀態,而非依賴全域變數。終止 Service Worker 也可能導致計時器在完成前結束。請改用鬧鐘。

本頁面將詳細說明這些工作。

更新資訊清單中的「background」欄位

在 Manifest V3 中,背景頁面會由 Service Worker 取代。資訊清單變更如下。

  • 將 manifest.json 中的 "background.scripts" 替換為 "background.service_worker"。請注意,"service_worker" 欄位接受字串,而非字串陣列。
  • 從 manifest.json 移除 "background.persistent"。
Manifest V2
{
  ...
  "background": {
    "scripts": [
      "backgroundContextMenus.js",
      "backgroundOauth.js"
    ],
    "persistent": false
  },
  ...
}
Manifest V3
{
  ...
  "background": {
    "service_worker": "service_worker.js",
    "type": "module"
  }
  ...
}

"service_worker" 欄位接受單一字串。只有在使用 ES 模組 (使用 import 關鍵字) 時,才需要 "type" 欄位。其值一律為 "module"。詳情請參閱「擴充功能 Service Worker 基本概念」。

將 DOM 和視窗呼叫移至螢幕外文件

部分擴充功能需要存取 DOM 和視窗物件,但不會實際開啟新視窗或分頁。Offscreen API 支援這些用途,可開啟及關閉與擴充功能封裝在一起的未顯示文件,而不會干擾使用者體驗。除了訊息傳遞之外,螢幕外文件不會與其他擴充功能情境共用 API,但會做為完整的網頁,供擴充功能互動。

如要使用 Offscreen API,請從 Service Worker 建立螢幕外文件。

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

在螢幕外文件中,執行您先前在背景指令碼中執行的任何動作。例如,您可以複製在主機頁面上選取的文字。

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

使用訊息傳遞,在螢幕外文件和擴充功能 Service Worker 之間通訊。

將 localStorage 轉換為其他類型

網路平台的 Storage 介面 (可透過 window.localStorage 存取) 無法在 Service Worker 中使用。如要解決這個問題,請採取下列任一做法:首先,您可以將其替換為對其他儲存機制的呼叫。browser.storage.local 命名空間適用於大多數用途,但您也可以選擇其他選項。

您也可以將呼叫移至螢幕外文件。舉例來說,如要將先前儲存在 localStorage 中的資料遷移至其他機制:

  1. 使用轉換常式和 runtime.onMessage 處理常式建立螢幕外文件。
  2. 在螢幕外文件中新增轉換常式。
  3. 在擴充功能 Service Worker 中,檢查 browser.storage 是否有資料。
  4. 如果找不到資料,請建立螢幕外文件,並呼叫 runtime.sendMessage() 啟動轉換常式。
  5. 在新增至離螢幕文件的 runtime.onMessage 處理常式中,呼叫轉換常式。

此外,網頁儲存空間 API 在擴充功能中的運作方式也有一些細微差異。詳情請參閱「儲存空間和 Cookie」。

同步註冊事件監聽器

在 Manifest V3 中,非同步註冊事件監聽器 (例如在 Promise 或回呼中) 不一定能正常運作。請參考下列程式碼。

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

這適用於永久背景頁面,因為頁面會持續執行,且不會重新初始化。在 Manifest V3 中,系統會在事件調度時重新初始化 Service Worker。也就是說,事件觸發時,監聽器不會註冊 (因為是以非同步方式新增),因此會遺漏事件。

請改為將事件監聽器註冊移至指令碼的頂層。這樣一來,即使擴充功能尚未完成啟動邏輯,Chrome 也能立即找到並叫用動作的點擊處理常式。

browser.action.onClicked.addListener(handleActionClick);

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

將 XMLHttpRequest() 替換為全域 fetch()

XMLHttpRequest() 無法從 Service Worker、擴充功能或其他位置呼叫。將背景指令碼對 XMLHttpRequest() 的呼叫,替換為對全域 fetch() 的呼叫。

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

保留狀態

Service Worker 是暫時性的,也就是說,在使用者瀏覽器工作階段期間,Service Worker 可能會重複啟動、執行和終止。這也表示,由於先前的結構定義已拆除,因此全域變數中的資料無法立即使用。如要解決這個問題,請使用儲存空間 API 做為可靠資料來源。以下將舉例說明如何執行這項操作。

以下範例使用全域變數來儲存名稱。在 Service Worker 中,這個變數可能會在使用者瀏覽器工作階段期間重設多次。

Manifest V2 背景指令碼
let savedName = undefined;

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

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

如果是 Manifest V3,請將全域變數替換為對 Storage API 的呼叫。

Manifest V3 Service Worker
browser.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    browser.storage.local.set({ name });
  }
});

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

將計時器轉換為鬧鐘

使用 setTimeout() 或 setInterval() 方法執行延遲或週期性作業,是常見的做法。不過,這些 API 在 Service Worker 中可能會失敗,因為只要 Service Worker 終止,計時器就會取消。

Manifest V2 背景指令碼
// 3 minutes in milliseconds
const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  browser.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

請改用 Alarms API。與其他事件監聽器一樣,鬧鐘事件監聽器應在指令碼的頂層註冊。

Manifest V3 Service Worker
async function startAlarm(name, duration) {
  await browser.alarms.create(name, { delayInMinutes: 3 });
}

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

讓 Service Worker 保持運作

根據定義,Service Worker 是事件導向,會在閒置時終止。這樣 Chrome 就能最佳化擴充功能的效能和記憶體用量。詳情請參閱 Service Worker 生命週期說明文件。在特殊情況下,可能需要採取額外措施,確保 Service Worker 保持運作更長時間。

讓 Service Worker 保持運作,直到長時間執行的作業完成

如果 Service Worker 執行長時間作業,但未呼叫擴充功能 API,則 Service Worker 可能會在作業期間關閉。例如:

  • fetch() 要求可能需要超過五分鐘 (例如在連線品質不佳的情況下下載大型檔案)。
  • 複雜的非同步計算,耗時超過 30 秒。

在這種情況下,如要延長 Service Worker 的生命週期,可以定期呼叫簡單的擴充功能 API,重設逾時計數器。請注意,這項功能僅適用於例外情況,在大多數情況下,通常有更符合平台慣例的方法可以達到相同結果。

以下範例顯示 waitUntil() 輔助函式,可讓 Service Worker 保持運作,直到特定 Promise 解析為止:

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

waitUntil(someExpensiveCalculation());

讓 Service Worker 持續運作

在極少數情況下,必須無限期延長生命週期。我們發現企業和教育機構是最大的用途,因此特別允許在這兩個領域使用,但一般情況下不支援。在這些例外情況下,您可以定期呼叫簡單的擴充功能 API,讓 Service Worker 保持運作。請注意,這項建議僅適用於在受管理裝置上執行的擴充功能,且僅限企業或教育用途。其他情況則不允許,Chrome 擴充功能團隊保留日後對這些擴充功能採取行動的權利。

請使用下列程式碼片段,讓 Service Worker 保持運作:

/**
 * 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 browser.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 browser.storage.local.get('last-heartbeat'))['last-heartbeat'];
}