使用服務工作者取代背景或事件頁面
服務工作站會取代擴充功能的背景或事件頁面,確保背景程式碼不會影響主執行緒。這樣一來,擴充功能就只會在需要時執行,可節省資源。
自推出以來,擴充功能就一直有背景頁面這項基本元素。簡單來說,背景頁面提供的環境與其他視窗或分頁無關。這樣一來,擴充功能就能觀察事件並採取行動。
本頁說明將背景頁面轉換為擴充功能服務 worker 的工作。如要進一步瞭解擴充功能服務 worker 的一般資訊,請參閱「使用服務 worker 處理事件」教學課程,以及「關於擴充功能服務 worker」一節。
背景指令碼和擴充功能服務工作者之間的差異
在某些情況下,您會看到稱為「背景指令碼」的擴充功能服務 worker。雖然擴充功能 Service Worker 確實會在背景執行,但將其稱為背景指令碼會造成誤解,因為這會暗示兩者具有相同的功能。以下說明兩者的差異。
來自背景頁面的變更
服務工作站與背景頁面有許多不同之處。
- 這些函式會在主執行緒外運作,因此不會干擾擴充功能內容。
- 它們具有特殊功能,例如在擴充功能的來源中攔截擷取事件,例如工具列彈出式視窗中的事件。
- 他們可以透過用戶端介面與其他情境進行通訊和互動。
需要進行的變更
您需要調整部分程式碼,以便考量背景指令碼和服務工作程式函式之間的差異。首先,在資訊清單檔案中指定 Service Worker 的方式,與指定背景指令碼的方式不同。此外:
- 由於這類呼叫無法存取 DOM 或
window
介面,因此您必須將這些呼叫移至其他 API 或螢幕外文件。 - 事件監聽器不應註冊回應傳回的承諾,或在事件回呼中註冊。
- 由於這些方法與
XMLHttpRequest()
不相容,因此您必須將對此介面的呼叫,改為對fetch()
的呼叫。 - 由於全域變數在未使用時會終止,因此您需要保留應用程式狀態,而非依賴全域變數。終止服務工作站也可以在計時器完成前結束計時器。請改用鬧鐘。
本頁面將詳細說明這些工作。
更新資訊清單中的「背景」欄位
在資訊清單 V3 中,背景頁面會由服務工作者取代。以下列出資訊清單變更項目。
- 在
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"
。詳情請參閱「擴充功能服務 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
中的資料遷移至其他機制:
- 使用轉換常式和
runtime.onMessage
處理常式建立螢幕外文件。 - 在離線文件中新增轉換例行程序。
- 在擴充功能服務 worker 中,檢查
chrome.storage
是否有資料。 - 如果找不到資料,請create離線文件,然後呼叫
runtime.sendMessage()
來啟動轉換例行作業。 - 在您新增至螢幕外文件的
runtime.onMessage
處理常式中,呼叫轉換例行程序。
網路儲存空間 API 在擴充功能中的運作方式也有所不同。詳情請參閱「儲存空間和 Cookie」。
同步註冊事件監聽器
非同步註冊事件監聽器 (例如在承諾或回呼中),不保證可在 Manifest V3 中運作。請參考以下程式碼。
chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
chrome.browserAction.setBadgeText({ text: badgeText });
chrome.browserAction.onClicked.addListener(handleActionClick);
});
這項功能適用於持續性背景頁面,因為該頁面會持續執行,且不會重新初始化。在 Manifest V3 中,服務工作者會在事件調度時重新初始化。也就是說,事件觸發時,系統不會註冊事件監聽器 (因為事件監聽器是異步新增的),因此會錯過事件。
請改為將事件監聽器註冊作業移至指令碼的頂層。這樣一來,即使擴充功能尚未完成執行啟動邏輯,Chrome 也能立即找到並叫用動作的點擊處理程序。
chrome.action.onClicked.addListener(handleActionClick);
chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
chrome.action.setBadgeText({ text: badgeText });
});
將 XMLHttpRequest() 替換為全域 fetch()
XMLHttpRequest()
無法從服務工作者、擴充功能或其他方式呼叫。將背景指令碼對 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);
保留狀態
服務工作者是暫時性的,也就是說,在使用者的瀏覽器工作階段中,服務工作者很可能會重複啟動、執行和終止。這也表示,由於先前的內容已解構,因此全域變數無法立即取得資料。如要解決這個問題,請使用儲存空間 API 做為可靠資料來源。以下範例說明如何執行這項操作。
以下範例使用全域變數來儲存名稱。在服務工作者中,這個變數可能會在使用者瀏覽器工作階段期間重設多次。
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 的呼叫。
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(), }); });
讓服務工作程式保持運作
服務工作者是事件導向的,並會在閒置時終止。這樣一來,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'];
}