將背景或事件頁面替換為 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"。
{ ... "background": { "scripts": [ "backgroundContextMenus.js", "backgroundOauth.js" ], "persistent": false }, ... }
{ ... "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 中的資料遷移至其他機制:
- 使用轉換常式和
runtime.onMessage處理常式建立螢幕外文件。 - 在螢幕外文件中新增轉換常式。
- 在擴充功能 Service Worker 中,檢查
browser.storage是否有資料。 - 如果找不到資料,請建立螢幕外文件,並呼叫
runtime.sendMessage()啟動轉換常式。 - 在新增至離螢幕文件的
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() 的呼叫。
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);
保留狀態
Service Worker 是暫時性的,也就是說,在使用者瀏覽器工作階段期間,Service Worker 可能會重複啟動、執行和終止。這也表示,由於先前的結構定義已拆除,因此全域變數中的資料無法立即使用。如要解決這個問題,請使用儲存空間 API 做為可靠資料來源。以下將舉例說明如何執行這項操作。
以下範例使用全域變數來儲存名稱。在 Service Worker 中,這個變數可能會在使用者瀏覽器工作階段期間重設多次。
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 的呼叫。
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 終止,計時器就會取消。
// 3 minutes in milliseconds const TIMEOUT = 3 * 60 * 1000; setTimeout(() => { browser.action.setIcon({ path: getRandomIconPath(), }); }, TIMEOUT);
請改用 Alarms API。與其他事件監聽器一樣,鬧鐘事件監聽器應在指令碼的頂層註冊。
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'];
}