使用服務工作程處理事件

涵蓋擴充功能 Service Worker 概念的教學課程

總覽

本教學課程將介紹 Chrome 擴充功能服務工作人員。在本教學課程中,您將建構擴充功能,讓使用者透過多功能方塊快速前往 Chrome API 參考頁面。您將學習下列內容:

  • 註冊 Service Worker 並匯入模組。
  • 對擴充功能 Service Worker 進行偵錯。
  • 管理狀態及處理事件。
  • 觸發週期性事件。
  • 與內容指令碼通訊。

事前準備

本指南假設您具備基本的網頁開發經驗。建議您先參閱「擴充功能 101」和「Hello World」,瞭解如何開發擴充功能。

建立擴充功能

首先,請建立名為 quick-api-reference 的新目錄,用於存放擴充功能檔案,或從 GitHub 範例存放區下載原始碼。

步驟 1:註冊 Service Worker

在專案根目錄中建立 manifest 檔案,並加入下列程式碼:

manifest.json:

{
  "manifest_version": 3,
  "name": "Open extension API reference",
  "version": "1.0.0",
  "icons": {
    "16": "images/icon-16.png",
    "128": "images/icon-128.png"
  },
  "background": {
    "service_worker": "service-worker.js"
  }
}

擴充功能會在資訊清單中註冊 Service Worker,而資訊清單只會採用單一 JavaScript 檔案。您不需要像在網頁中一樣呼叫 navigator.serviceWorker.register()

建立 images 資料夾,然後將圖示下載到該資料夾

請參閱「閱讀時間」教學課程的第一步,進一步瞭解資訊清單中的擴充功能中繼資料圖示

步驟 2:匯入多個 Service Worker 模組

我們的 Service Worker 實作了兩項功能。為方便維護,我們將在個別模組中實作各項功能。首先,我們需要在資訊清單中將 Service Worker 宣告為 ES 模組,這樣才能在 Service Worker 中匯入模組:

manifest.json:

{
 "background": {
    "service_worker": "service-worker.js",
    "type": "module"
  },
}

建立 service-worker.js 檔案並匯入兩個模組:

import './sw-omnibox.js';
import './sw-tips.js';

建立這些檔案,並在每個檔案中加入控制台記錄。

sw-omnibox.js:

console.log("sw-omnibox.js");

sw-tips.js:

console.log("sw-tips.js");

如要瞭解在 Service Worker 中匯入多個檔案的其他方式,請參閱「匯入指令碼」。

選用:偵錯 Service Worker

我將說明如何找出 Service Worker 記錄,以及如何得知 Service Worker 何時終止。首先,請按照操作說明載入未封裝的擴充功能

30 秒後,您會看到「Service Worker (閒置)」,表示 Service Worker 已終止。按一下「service worker (inactive)」連結即可檢查。請參閱下方動畫。

您是否注意到檢查 Service Worker 會喚醒 Service Worker?在開發人員工具中開啟 Service Worker,即可保持啟用狀態。如要確保擴充功能在 Service Worker 終止時正常運作,請記得關閉開發人員工具。

現在請中斷擴充功能,瞭解如何找出錯誤。其中一個方法是從 service-worker.js 檔案的 './sw-omnibox.js' 匯入項目中刪除「.js」。Chrome 將無法註冊 service worker。

返回 chrome://extensions 並重新整理擴充功能。您會看到兩項錯誤:

Service worker registration failed. Status code: 3.

An unknown error occurred when fetching the script.

如要進一步瞭解如何對擴充功能 Service Worker 進行偵錯,請參閱「對擴充功能進行偵錯」。

步驟 4:初始化狀態

如果不需要服務工作人員,Chrome 會關閉。我們使用 browser.storage API,在 Service Worker 工作階段中保留狀態。如要存取儲存空間,我們需要在資訊清單中要求權限:

manifest.json:

{
  ...
  "permissions": ["storage"],
}

首先,請將預設建議儲存至儲存空間。我們可以監聽 runtime.onInstalled() 事件,在擴充功能首次安裝時初始化狀態:

sw-omnibox.js:

...
// Save default API suggestions
browser.runtime.onInstalled.addListener(({ reason }) => {
  if (reason === 'install') {
    browser.storage.local.set({
      apiSuggestions: ['tabs', 'storage', 'scripting']
    });
  }
});

服務工作人員無法直接存取 window 物件,因此無法使用 window.localStorage 儲存值。此外,服務工作人員是生命週期短暫的執行環境,會在使用者瀏覽器工作階段中重複終止,因此不相容於全域變數。請改用 browser.storage.local,將資料儲存在本機。

如要瞭解擴充功能服務工作人員的其他儲存選項,請參閱「保留資料,而非使用全域變數」。

步驟 5:註冊事件

所有事件監聽器都必須在 Service Worker 的全球範圍內靜態註冊。換句話說,事件監聽器不應巢狀內嵌在非同步函式中。這樣一來,Chrome 就能確保在服務工作站重新啟動時,所有事件處理常式都會還原。

在本範例中,我們將使用 browser.omnibox API,但首先必須在資訊清單中宣告網址列關鍵字觸發條件:

manifest.json:

{
  ...
  "minimum_chrome_version": "102",
  "omnibox": {
    "keyword": "api"
  },
}

現在,請在指令碼的頂層註冊多功能方塊事件監聽器。使用者在網址列中輸入多功能方塊關鍵字 (api),然後按下 Tab 鍵或空格鍵時,Chrome 會根據儲存空間中的關鍵字顯示建議清單。onInputChanged() 事件會接收目前的使用者輸入內容和 suggestResult 物件,負責填入這些建議。

sw-omnibox.js:

...
const URL_CHROME_EXTENSIONS_DOC =
  'https://developer.chrome.com/docs/extensions/reference/';
const NUMBER_OF_PREVIOUS_SEARCHES = 4;

// Display the suggestions after user starts typing
browser.omnibox.onInputChanged.addListener(async (input, suggest) => {
  await browser.omnibox.setDefaultSuggestion({
    description: 'Enter a Chrome API or choose from past searches'
  });
  const { apiSuggestions } = await browser.storage.local.get('apiSuggestions');
  const suggestions = apiSuggestions.map((api) => {
    return { content: api, description: `Open browser.${api} API` };
  });
  suggest(suggestions);
});

使用者選取建議後,onInputEntered() 會開啟對應的 Chrome API 參考資料頁面。

sw-omnibox.js:

...
// Open the reference page of the chosen API
browser.omnibox.onInputEntered.addListener((input) => {
  browser.tabs.create({ url: URL_CHROME_EXTENSIONS_DOC + input });
  // Save the latest keyword
  updateHistory(input);
});

updateHistory() 函式會接收網址列輸入內容,並儲存至 storage.local。這樣一來,系統之後就能將最近的搜尋字詞做為網址列建議。

sw-omnibox.js:

...
async function updateHistory(input) {
  const { apiSuggestions } = await browser.storage.local.get('apiSuggestions');
  apiSuggestions.unshift(input);
  apiSuggestions.splice(NUMBER_OF_PREVIOUS_SEARCHES);
  return browser.storage.local.set({ apiSuggestions });
}

步驟 6:設定週期性活動

setTimeout()setInterval() 方法通常用於執行延遲或週期性工作。不過,由於排程器會在服務工作人員終止時取消計時器,因此這些 API 可能會失敗。擴充功能可以改用 browser.alarms API。

請先在資訊清單中要求 "alarms" 權限:

manifest.json:

{
  ...
  "permissions": ["storage"],
  "permissions": ["storage", "alarms"],
}

擴充功能會擷取所有提示、隨機挑選一個並儲存至儲存空間。我們會建立每天觸發一次的鬧鐘,以便更新提示。關閉 Chrome 時,系統不會儲存鬧鐘。因此我們需要檢查鬧鐘是否存在,如果不存在則建立鬧鐘。

sw-tips.js:

// Fetch tip & save in storage
const updateTip = async () => {
  const response = await fetch('https://chrome.dev/f/extension_tips/');
  const tips = await response.json();
  const randomIndex = Math.floor(Math.random() * tips.length);
  return browser.storage.local.set({ tip: tips[randomIndex] });
};

const ALARM_NAME = 'tip';

// Check if alarm exists to avoid resetting the timer.
// The alarm might be removed when the browser session restarts.
async function createAlarm() {
  const alarm = await browser.alarms.get(ALARM_NAME);
  if (typeof alarm === 'undefined') {
    browser.alarms.create(ALARM_NAME, {
      delayInMinutes: 1,
      periodInMinutes: 1440
    });
    updateTip();
  }
}

createAlarm();

// Update tip once a day
browser.alarms.onAlarm.addListener(updateTip);

步驟 7:與其他環境通訊

擴充功能會使用內容指令碼讀取及修改網頁內容。使用者造訪 Chrome API 參考資料頁面時,擴充功能的內容指令碼會更新頁面,顯示每日提示。這會傳送訊息,向 Service Worker 要求每日提示。

首先,請在資訊清單中宣告內容指令碼,並新增與 Chrome API 參考說明文件對應的相符模式。

manifest.json:

{
  ...
  "content_scripts": [
    {
      "matches": ["https://developer.chrome.com/docs/extensions/reference/*"],
      "js": ["content.js"]
    }
  ]
}

建立新的內容檔案。下列程式碼會將訊息傳送至 Service Worker,要求提供提示。然後新增按鈕,開啟含有擴充功能提示的彈出式視窗。這段程式碼使用新的網頁平台 Popover API

content.js:

(async () => {
  // Sends a message to the service worker and receives a tip in response
  const { tip } = await browser.runtime.sendMessage({ greeting: 'tip' });

  const nav = document.querySelector('.upper-tabs > nav');
  
  const tipWidget = createDomElement(`
    <button type="button" popovertarget="tip-popover" popovertargetaction="show" style="padding: 0 12px; height: 36px;">
      <span style="display: block; font: var(--devsite-link-font,500 14px/20px var(--devsite-primary-font-family));">Tip</span>
    </button>
  `);

  const popover = createDomElement(
    `<div id='tip-popover' popover style="margin: auto;">${tip}</div>`
  );

  document.body.append(popover);
  nav.append(tipWidget);
})();

function createDomElement(html) {
  const dom = new DOMParser().parseFromString(html, 'text/html');
  return dom.body.firstElementChild;
}

最後一個步驟是在服務工作站中新增訊息處理常式,將每日提示回覆給內容指令碼。

sw-tips.js:

...
// Send tip to content script via messaging
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.greeting === 'tip') {
    browser.storage.local.get('tip').then(sendResponse);
    return true;
  }
});

測試是否正常運作

確認專案的檔案結構如下所示:

擴充功能資料夾的內容:images 資料夾、manifest.json、service-worker.js、sw-omnibox.js、sw-tips.js 和 content.js

在本機載入擴充功能

如要在開發人員模式中載入未封裝的擴充功能,請按照「Hello world」一文的步驟操作。

開啟參考頁面

  1. 在瀏覽器網址列中輸入關鍵字「api」。
  2. 按下「Tab」鍵或「空格」鍵。
  3. 輸入 API 的完整名稱。
    • 或從過去的搜尋記錄清單中選擇
  4. 系統會開啟新頁面,顯示 Chrome API 參考資料頁面。

內容應該會類似這樣:

快速 API 參考資料,開啟執行階段 API 參考資料
快速 API 擴充功能,可開啟 Runtime API。

開啟每日提示

按一下導覽列上的「提示」按鈕,開啟擴充功能提示。

在
快速 API 擴充功能,可開啟每日提示。

🎯 潛在強化功能

根據今天所學內容,嘗試完成下列任一操作:

  • 探索實作網址列建議的其他方式。
  • 建立自訂強制回應,顯示擴充功能提示。
  • 開啟 MDN 的 Web Extensions 參考 API 頁面。

繼續加油!

恭喜完成本教學課程 🎉。如要繼續提升技能,請完成其他初學者教學課程:

額外資訊 學習目標
閱讀時間 自動在特定網頁集插入元素。
分頁管理工具 建立用於管理瀏覽器分頁的彈出式視窗。
專注模式 點選擴充功能動作後,在目前頁面執行程式碼。

繼續探索

如要繼續學習擴充功能 Service Worker 的學習路徑,建議您參考下列文章: