遷移至 Service Worker

使用 Service Worker 取代背景或活動頁面

Service Worker 會取代擴充功能的背景或事件頁面,確保背景程式碼並未顯示在主執行緒中。這可讓擴充功能只在必要時執行,節省資源。

自推出以來,背景頁面一直是擴充功能的基本要素。簡單來說,背景頁面提供的環境不受其他視窗或分頁影響。如此一來,擴充功能就能觀察事件並據此採取行動。

本頁說明將背景頁面轉換為擴充功能 Service Worker 的工作。如要進一步瞭解擴充功能 Service Worker 的一般資訊,請參閱「透過 Service Worker 處理事件」及「關於擴充功能 Service Worker」一節。

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

在某些情況下,您會看到「背景指令碼」的擴充功能服務 Worker。雖然擴充功能服務工作處理程序是在背景執行,但呼叫背景指令碼時,暗中功能也是稍微誤導。說明差異如下。

背景頁面的變更

Service Worker 與背景頁面有許多差異。

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

您需要進行的變更

為因應背景指令碼和 Service Worker 功能之間的差異,您需要做出幾項程式碼調整。首先,在資訊清單檔案中指定服務工作站的方式,與背景指令碼的指定方式不同。此外:

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

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

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

在 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,請向服務工作團隊建立文件外文件。

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. 在擴充功能服務工作站中,檢查 chrome.storage 是否有您的資料。
  4. 如果找不到資料,請建立畫面外文件,並呼叫 runtime.sendMessage() 來啟動轉換日常安排。
  5. 在新增至畫面外文件的 runtime.onMessage 處理常式中,呼叫轉換處理常式。

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

同步註冊事件監聽器

以非同步方式註冊事件監聽器 (例如在 promise 或回呼中) 並不保證可在 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() 替換為全域 fetch()

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

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 是暫時性的,因此可能會在使用者的瀏覽器工作階段期間重複啟動、執行及終止。這也表示由於前一個結構定義遭到關閉,全域變數中無法立即取得資料。如要解決這個問題,請使用 Storage API 做為可靠資料來源。相關做法範例。

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

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

在 Manifest 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 保持運作

Service Worker 是以定義事件為準,會在閒置時終止。這樣一來,Chrome 就能最佳化擴充功能的效能和記憶體用量。詳情請參閱服務 Worker 生命週期說明文件。在特殊情況下,您可能需要採取額外措施,確保服務工作處理程序維持較長時間。

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

在非呼叫擴充功能 API 的長時間執行 Service Worker 作業中,Service Worker 可能會於作業中途關閉。例如:

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

如要在這類情況下延長 Service Worker 的生命週期,您可以定期呼叫基本擴充功能 API 來重設逾時計數器。請注意,這個做法只適用於特殊情況,在大多數情況下,通常比較適合透過平台慣用的方式達到相同結果。

以下範例顯示 waitUntil() 輔助函式,可讓 Service Worker 持續運作,直到指定的 promise 解決為止:

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

waitUntil(someExpensiveCalculation());

讓 Service Worker 持續運作

在極少數情況下,您必須無限期延長生命週期。我們已經認定企業和教育是最大的用途,並明確允許上述做法,但一般不會提供相關支援。在這類特殊情況下,可以定期呼叫簡易的擴充功能 API,藉此維持服務 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 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'];
}