使用 Service Worker 处理事件

介绍 Extensions Service Worker 概念的教程

概览

本教程介绍了 Chrome 扩展程序 Service Worker。在本教程中,您将构建一个扩展程序,让用户能够使用万能搜索框快速前往 Chrome API 参考页面。您将学习如何:

  • 注册您的 Service Worker 并导入模块。
  • 调试扩展程序 Service Worker。
  • 管理状态和处理事件。
  • 触发周期性事件。
  • 与内容脚本通信。

前期准备

本指南假定您具备基本的 Web 开发经验。建议您查看扩展程序 101Hello 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"
  }
}

扩展程序会在清单中注册其服务工作线程,而清单只包含一个 JavaScript 文件。无需像在网页中一样调用 navigator.serviceWorker.register()

创建一个 images 文件夹,然后将下载的图标下载到该文件夹中。

请参阅“阅读时间”教程的第一个步骤,详细了解清单中的扩展程序元数据图标

第 2 步:导入多个服务工件模块

我们的 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 日志并了解其终止时间。首先,按照说明加载已解压缩的扩展程序

30 秒后,您会看到“service worker (inactive)”(服务工件 [无效]),这表示服务工件已终止。点击“service worker(inactive)”链接以进行检查。下图展示了这一点。

您是否注意到,检查 Service Worker 会唤醒它?在开发者工具中打开服务工作线程会使其保持活跃状态。为确保您的扩展程序在 Service Worker 终止后正常运行,请务必关闭 DevTools。

现在,请中断扩展程序,了解错误所在的位置。实现此目的的方法之一是从 service-worker.js 文件中的 './sw-omnibox.js' 导入中删除“.js”。Chrome 将无法注册服务工作线程。

返回 chrome://extensions,然后刷新扩展程序。您会看到两个错误:

Service worker registration failed. Status code: 3.

An unknown error occurred when fetching the script.

如需了解调试扩展程序服务工作器的更多方法,请参阅调试扩展程序

第 4 步:初始化状态

如果不需要服务工件,Chrome 会将其关闭。我们使用 chrome.storage API 跨 Service Worker 会话保留状态。如需存储空间访问权限,我们需要在清单中请求权限:

manifest.json

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

首先,将默认建议保存到存储空间。我们可以通过监听 runtime.onInstalled() 事件,在扩展程序首次安装时初始化状态:

sw-omnibox.js

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

服务工件无法直接访问 window 对象,因此无法使用 window.localStorage 存储值。此外,Service Worker 是短期的执行环境;它们会在用户的浏览器会话中反复终止,这使其与全局变量不兼容。请改用 chrome.storage.local,将数据存储在本地机器上。

如需了解扩展程序服务工件的其他存储选项,请参阅保留数据,而不是使用全局变量

第 5 步:注册事件

所有事件监听器都需要在 Service Worker 的全局范围内进行静态注册。换句话说,事件监听器不应嵌套在异步函数中。这样,Chrome 就可以确保在服务工作器重启时恢复所有事件处理脚本。

在此示例中,我们将使用 chrome.omnibox API,但首先必须在清单中声明全局搜索框关键字触发器:

manifest.json

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

现在,在脚本的顶级注册 Omnibox 事件监听器。当用户在地址栏中输入多功能框关键字 (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
chrome.omnibox.onInputChanged.addListener(async (input, suggest) => {
  await chrome.omnibox.setDefaultSuggestion({
    description: 'Enter a Chrome API or choose from past searches'
  });
  const { apiSuggestions } = await chrome.storage.local.get('apiSuggestions');
  const suggestions = apiSuggestions.map((api) => {
    return { content: api, description: `Open chrome.${api} API` };
  });
  suggest(suggestions);
});

用户选择某个建议后,onInputEntered() 将打开相应的 Chrome API 参考文档页面。

sw-omnibox.js

...
// Open the reference page of the chosen API
chrome.omnibox.onInputEntered.addListener((input) => {
  chrome.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 chrome.storage.local.get('apiSuggestions');
  apiSuggestions.unshift(input);
  apiSuggestions.splice(NUMBER_OF_PREVIOUS_SEARCHES);
  return chrome.storage.local.set({ apiSuggestions });
}

第 6 步:设置周期性活动

setTimeout()setInterval() 方法通常用于执行延迟或定期任务。不过,这些 API 可能会失败,因为调度程序会在服务 worker 终止时取消计时器。扩展程序可以改用 chrome.alarms API。

首先,在清单中请求 "alarms" 权限。此外,如需从远程托管位置提取扩展程序提示,您需要请求主机权限

manifest.json

{
  ...
  "permissions": ["storage"],
  "permissions": ["storage", "alarms"],
  "host_permissions": ["https://extension-tips.glitch.me/*"],
}

该扩展程序会提取所有提示,随机选择一个,然后将其保存到存储空间。我们将创建一个每天触发一次的闹钟,用于更新小费。关闭 Chrome 后,系统不会保存闹钟。因此,我们需要检查闹钟是否存在,如果不存在,则创建一个。

sw-tips.js

// Fetch tip & save in storage
const updateTip = async () => {
  const response = await fetch('https://extension-tips.glitch.me/tips.json');
  const tips = await response.json();
  const randomIndex = Math.floor(Math.random() * tips.length);
  return chrome.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 chrome.alarms.get(ALARM_NAME);
  if (typeof alarm === 'undefined') {
    chrome.alarms.create(ALARM_NAME, {
      delayInMinutes: 1,
      periodInMinutes: 1440
    });
    updateTip();
  }
}

createAlarm();

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

第 7 步:与其他上下文通信

扩展程序使用内容脚本来读取和修改网页内容。当用户访问 Chrome API 参考页面时,扩展程序的内容脚本会将当天的提示更新到该页面。它发送消息,请求从服务工件获取当天的小费。

首先,在清单中声明内容脚本,并添加与 Chrome API 参考文档对应的匹配模式。

manifest.json

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

创建新的内容文件。以下代码会向服务工件发送消息,请求获取小费。然后,添加一个按钮,用于打开包含扩展程序提示的弹出式窗口。此代码使用新的网站平台 Popover API

content.js:

(async () => {
  // Sends a message to the service worker and receives a tip in response
  const { tip } = await chrome.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;
}

最后一步是向我们的 Service Worker 添加消息处理程序,以便向内容脚本发送包含每日提示的回复。

sw-tips.js

...
// Send tip to content script via messaging
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.greeting === 'tip') {
    chrome.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 参考文档
用于打开 Runtime API 的 Quick API 扩展程序。

打开今日提示

点击导航栏上的“提示”按钮,打开扩展程序提示。

在
用于打开当天提示的快速 API 扩展程序。

🎯? 提升潜力

根据您今天学到的知识,尝试完成以下任一操作:

  • 探索实现万能搜索框建议的另一种方法。
  • 创建您自己的自定义模态,以显示扩展程序提示。
  • 打开指向 MDN 的 Web Extensions 参考 API 页面的另一个页面。

继续构建!

恭喜您完成本教程 🎉?。请继续完成其他适合新手的教程,不断提升您的技能:

扩展程序 学习内容
阅读时间 自动在特定网页中插入元素。
标签页管理器 创建用于管理浏览器标签页的弹出式窗口。
对焦模式 在点击扩展程序操作后,在当前网页上运行代码。

继续探索

如需继续学习扩展程序服务 worker,建议您浏览以下文章: