使用 Service Worker 替換背景或事件網頁
Service Worker 會取代擴充功能的背景或事件頁面,確保背景程式碼不會占用主執行緒。這樣一來,擴充功能只會在需要時執行,節省資源。
自擴充功能推出以來,背景網頁一直是基本元件。簡單來說,背景網頁提供的環境獨立於任何其他視窗或分頁。擴充功能可藉此觀察事件並採取相應行動。
本頁說明將背景網頁轉換為擴充功能服務工作站的工作。如要進一步瞭解擴充功能 Service Worker,請參閱「使用 Service Worker 處理事件」教學課程和「關於擴充功能 Service Worker」一節。
背景指令碼與擴充功能服務工作人員的差異
在某些情況下,您會看到稱為「背景指令碼」的擴充功能服務工作人員。雖然擴充功能 Service Worker 會在背景執行,但稱呼為背景指令碼有點誤導,因為這類指令碼的功能並不相同。詳情請參閱下文。
背景網頁的變更
服務工作人員與背景網頁有許多不同之處。
- 這些函式會在主執行緒外運作,因此不會干擾擴充功能內容。
- 這類指令碼具有特殊功能,例如攔截擴充功能來源的擷取事件,像是工具列彈出式視窗的事件。
- 這些內容可透過用戶端介面與其他內容通訊及互動。
需要進行的變更
您需要調整一些程式碼,以因應背景指令碼和 Service Worker 的運作方式差異。首先,資訊清單檔案中指定 Service Worker 的方式,與指定背景指令碼的方式不同。此外:
- 由於這些呼叫無法存取 DOM 或
window介面,您必須將這類呼叫移至其他 API 或螢幕外文件。 - 事件監聽器不應註冊來回應傳回的 Promise,也不應註冊在事件回呼內。
- 由於這些介面不與
XMLHttpRequest()向下相容,您需要將對這個介面的呼叫替換為對fetch()的呼叫。 - 由於這些變數會在閒置時終止,因此您需要保留應用程式狀態,而不是依賴全域變數。終止服務工作站也會在計時器完成前結束計時。請改用鬧鐘。
這個頁面會詳細說明這些工作。
更新資訊清單中的「background」欄位
在 Manifest V3 中,背景網頁會由Service Worker取代。以下列出資訊清單變更。
- 在
manifest.json中,以"background.service_worker"取代"background.scripts"。請注意,"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 建立螢幕外文件。
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 的資料遷移至其他機制,請按照下列步驟操作:
- 使用轉換常式和
runtime.onMessage處理常式,建立螢幕外文件。 - 在螢幕外文件中新增轉換常式。
- 在擴充功能 Service Worker 檢查
chrome.storage中查看資料。 - 如果找不到資料,請建立螢幕外文件,並呼叫
runtime.sendMessage()來啟動轉換常式。 - 在您新增至螢幕外文件的
runtime.onMessage處理常式中,呼叫轉換常式。
此外,網頁儲存空間 API 在擴充功能中的運作方式也有些細微差異。詳情請參閱「儲存空間和 Cookie」。
同步註冊事件監聽器
在 Manifest V3 中,非同步註冊監聽器 (例如在 Promise 或回呼內) 不保證能正常運作。請參考下列程式碼。
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()
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);
保留狀態
服務工作人員是暫時性的,也就是說,在使用者瀏覽器工作階段期間,服務工作人員可能會重複啟動、執行及終止。這也表示由於先前的環境已終止,因此全域變數中不會立即提供資料。如要解決這個問題,請使用 Storage API 做為可靠資料來源。我們會以範例說明如何執行這項操作。
以下範例使用全域變數儲存名稱。在 Service Worker 中,這項變數可能會在使用者瀏覽器工作階段期間多次重設。
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 的呼叫。
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 可能會在 Service Worker 中失敗,因為只要終止 Service Worker,計時器就會取消。
// 3 minutes in milliseconds const TIMEOUT = 3 * 60 * 1000; setTimeout(() => { chrome.action.setIcon({ path: getRandomIconPath(), }); }, TIMEOUT);
請改用 Alarms API。與其他監聽器一樣,鬧鐘監聽器應在指令碼的頂層註冊。
async function startAlarm(name, duration) { await chrome.alarms.create(name, { delayInMinutes: 3 }); } chrome.alarms.onAlarm.addListener(() => { chrome.action.setIcon({ path: getRandomIconPath(), }); });
讓 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(chrome.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 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'];
}