遷移至 Service Worker

將背景或活動頁面換成服務工作處理程序

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

自推出以來,擴充功能就一直有背景頁面這項基本元素。簡單來說,背景頁面提供的環境與其他視窗或分頁無關。這樣一來,擴充功能就能觀察事件並採取行動。

本頁說明將背景頁面轉換為擴充功能服務 worker 的工作。如要進一步瞭解擴充功能服務 worker 的一般資訊,請參閱「使用服務 worker 處理事件」教學課程,以及「關於擴充功能服務 worker」一節。

背景指令碼和擴充功能服務工作者之間的差異

在某些情況下,您會看見名為「背景指令碼」的擴充功能 Service Worker。雖然擴充功能 Service Worker 確實會在背景執行,但將其稱為背景指令碼會造成誤解,因為這會讓人誤以為兩者具有相同的功能。請參閱下方說明。

來自背景頁面的變更

服務工作站與背景頁面有許多不同之處。

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

需要進行的變更

您需要調整部分程式碼,以便考量背景指令碼和服務工作程式函式之間的差異。首先,在資訊清單檔案中指定 Service Worker 的方式,與指定背景指令碼的方式不同。此外:

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

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

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

在資訊清單 V3 中,背景頁面會由服務工作者取代。以下列出資訊清單變更。

  • manifest.json 中以 "background.service_worker" 取代 "background.scripts"。請注意,"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,請透過服務工作者建立離線文件。

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

在離線文件中執行先前在背景指令碼中執行的任何動作。舉例來說,您可以複製主機頁面上選取的文字。

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

使用訊息傳遞功能,在螢幕外文件和擴充功能服務工作站之間進行通訊。

將 localStorage 轉換為其他類型

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

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

  1. 使用轉換常式和 runtime.onMessage 處理常式建立螢幕外文件。
  2. 在離線文件中新增轉換例行程序。
  3. 在擴充功能服務 worker 中,檢查 chrome.storage 是否有資料。
  4. 如果找不到資料,請create離線文件,然後呼叫 runtime.sendMessage() 來啟動轉換例行程序。
  5. 在您新增至螢幕外文件的 runtime.onMessage 處理常式中,呼叫轉換處理常式。

網路儲存空間 API 在擴充功能中的運作方式也有所不同。詳情請參閱儲存空間和 Cookie

同步註冊事件監聽器

非同步註冊事件監聽器 (例如在承諾或回呼中),不保證可在 Manifest V3 中運作。請參考以下程式碼。

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

這個做法適用於持續性的背景頁面,因為網頁會持續執行,並且從未重新初始化。在 Manifest V3 中,系統會在調度事件時重新初始化 Service Worker。也就是說,事件觸發時,系統不會註冊事件監聽器 (因為事件監聽器是異步新增的),因此會錯過事件。

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

chrome.action.onClicked.addListener(handleActionClick);

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

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

XMLHttpRequest() 無法從服務工作者、擴充功能或其他方式呼叫。將背景指令碼對 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);
擷取
const response = await fetch('https://www.example.com/greeting.json'')
console.log(response.statusText);

保留狀態

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

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

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

針對資訊清單 V3,請將全域變數替換為對 Storage API 的呼叫。

Manifest V3 Service Worker
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 });
});

將計時器轉換為鬧鐘

常見的做法是透過 setTimeout()setInterval() 方法使用延遲或定期作業。但這些 API 可能會在服務工作站中失敗,因為計時器會在服務工作站終止時取消。

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

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

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

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

讓 Service Worker 保持運作

服務工作者是根據事件驅動,並會在閒置時終止。這樣一來,Chrome 才能最佳化擴充功能的效能和記憶體用量。詳情請參閱服務工作站生命週期說明文件。在特殊情況下,您可能需要採取額外措施,確保服務工作程式可維持更長的時間。

在長時間執行的作業完成前,請讓服務工作者保持運作

在長時間執行的服務 worker 作業中,如果未呼叫擴充功能 API,服務 worker 可能會在作業中途關閉。例如:

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

如要延長服務工作程生命週期,您可以定期呼叫簡單的擴充 API 來重設逾時計數器。請注意,這種做法僅適用於特殊情況,在大部分的情況下,通常都採取更理想、更慣用的平台,能夠達到相同結果。

以下範例顯示 waitUntil() 輔助函式,可在指定的承諾解決前維持服務工作程式運作:

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

waitUntil(someExpensiveCalculation());

持續讓服務工作者保持運作

在極少數情況下,您可能需要無限期延長生命週期。我們認為企業和教育機構是最大的用途,因此特別允許這類用途,但一般情況下並不支援。在這些特殊情況下,您可以定期呼叫簡單的擴充功能 API,讓服務工作程保持運作。請注意,這項建議僅適用於在企業或教育用途受管理裝置上執行的擴充功能。但 Chrome 擴充功能團隊則保留日後針對這些擴充功能採取行動的權利,

使用下列程式碼片段,讓服務工作者持續運作:

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