修改网页

扩展最常见的用例之一是修改网页。例如,扩展可能希望更改应用于页面的样式、隐藏特定的 DOM 节点或将额外的 DOM 节点注入页面。

有两种方法可以使用 WebExtensions API 来做到这一点

  • 声明式:定义一个匹配一组 URL 的模式,并将一组脚本加载到 URL 匹配该模式的页面中。
  • 以编程方式:使用 JavaScript API,将脚本加载到特定选项卡中托管的页面中。

无论哪种方式,这些脚本都称为内容脚本,并且与构成扩展的其他脚本不同

  • 它们只能访问 WebExtension API 的一小部分。
  • 它们可以直接访问加载它们的网页。
  • 它们使用消息传递 API 与扩展的其余部分通信。

在本文中,我们将介绍这两种加载脚本的方法。

修改与 URL 模式匹配的页面

首先,创建一个名为“modify-page”的新目录。在该目录中,创建一个名为“manifest.json”的文件,其内容如下

json
{
  "manifest_version": 2,
  "name": "modify-page",
  "version": "1.0",

  "content_scripts": [
    {
      "matches": ["https://mdn.org.cn/*"],
      "js": ["page-eater.js"]
    }
  ]
}

content_scripts 键是将脚本加载到匹配 URL 模式的页面中的方法。在本例中,content_scripts 指示浏览器将名为“page-eater.js”的脚本加载到 https://mdn.org.cn/ 下的所有页面中。

注意:由于 content_scripts"js" 属性是一个数组,因此您可以使用它将多个脚本注入匹配的页面。如果您这样做,页面将共享相同的范围,就像页面加载的多个脚本一样,并且它们将按照数组中列出的顺序加载。

注意:content_scripts 键还有一个 "css" 属性,您可以使用它来注入 CSS 样式表。

接下来,在“modify-page”目录中创建一个名为“page-eater.js”的文件,并赋予它以下内容

js
document.body.textContent = "";

let header = document.createElement("h1");
header.textContent = "This page has been eaten";
document.body.appendChild(header);

现在 安装扩展,然后访问 https://mdn.org.cn/。页面应如下所示

developer.mozilla.org page "eaten" by the script

以编程方式修改页面

如果您仍然想要“吃掉”页面,但只有在用户要求您这样做时才这样做怎么办?让我们更新此示例,以便在用户点击上下文菜单项时注入内容脚本。

首先,更新“manifest.json”,使其具有以下内容

json
{
  "manifest_version": 2,
  "name": "modify-page",
  "version": "1.0",

  "permissions": ["activeTab", "contextMenus"],

  "background": {
    "scripts": ["background.js"]
  }
}

在这里,我们删除了 content_scripts 键,并添加了两个新键

  • permissions:要将脚本注入页面,我们需要修改页面的权限。activeTab 权限 是一种临时获取当前活动选项卡权限的方法。我们还需要 contextMenus 权限才能添加上下文菜单项。
  • background:我们使用它来加载一个名为 background.js 的持久 “后台脚本”,我们将在其中设置上下文菜单并注入内容脚本。

让我们创建此文件。在 modify-page 目录中创建一个名为 background.js 的新文件,并赋予它以下内容

js
browser.contextMenus.create({
  id: "eat-page",
  title: "Eat this page",
});

browser.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === "eat-page") {
    browser.tabs.executeScript({
      file: "page-eater.js",
    });
  }
});

在此脚本中,我们创建了一个 上下文菜单项,为其指定一个特定的 ID 和标题(要在上下文菜单中显示的文本)。然后,我们设置一个事件侦听器,以便当用户点击上下文菜单项时,我们检查它是否是我们 eat-page 项。如果是,我们使用 tabs.executeScript() API 将“page-eater.js”注入到当前选项卡中。此 API 可选择将选项卡 ID 作为参数:我们省略了选项卡 ID,这意味着脚本将注入到当前活动选项卡中。

此时,扩展应如下所示

modify-page/
    background.js
    manifest.json
    page-eater.js

现在 重新加载扩展,打开一个页面(这次是任何页面),激活上下文菜单,然后选择“吃掉此页面”

Option to eat a page on the context menu

消息传递

内容脚本和后台脚本无法直接访问彼此的状态。但是,它们可以通过发送消息来通信。下表总结了每一方涉及的 API

在内容脚本中 在后台脚本中
发送消息 browser.runtime.sendMessage() browser.tabs.sendMessage()
接收消息 browser.runtime.onMessage browser.runtime.onMessage

注意:除了这种发送一次性消息的通信方法外,您还可以使用 基于连接的方法来交换消息。有关选择选项的建议,请参阅 选择一次性消息和基于连接的消息传递

让我们更新我们的示例以显示如何从后台脚本发送消息。

首先,编辑 background.js,使其具有以下内容

js
browser.contextMenus.create({
  id: "eat-page",
  title: "Eat this page",
});

function messageTab(tabs) {
  browser.tabs.sendMessage(tabs[0].id, {
    replacement: "Message from the extension!",
  });
}

function onExecuted(result) {
  let querying = browser.tabs.query({
    active: true,
    currentWindow: true,
  });
  querying.then(messageTab);
}

browser.contextMenus.onClicked.addListener((info, tab) => {
  if (info.menuItemId === "eat-page") {
    let executing = browser.tabs.executeScript({
      file: "page-eater.js",
    });
    executing.then(onExecuted);
  }
});

现在,在注入 page-eater.js 后,我们使用 tabs.query() 获取当前活动选项卡,然后使用 tabs.sendMessage() 向加载到该选项卡中的内容脚本发送消息。该消息的有效负载为 {replacement: "Message from the extension!"}

接下来,像这样更新 page-eater.js

js
function eatPageReceiver(request, sender, sendResponse) {
  document.body.textContent = "";
  let header = document.createElement("h1");
  header.textContent = request.replacement;
  document.body.appendChild(header);
}
browser.runtime.onMessage.addListener(eatPageReceiver);

现在,内容脚本不再立即“吃掉”页面,而是使用 runtime.onMessage 侦听消息。当收到消息时,内容脚本运行与之前基本相同的代码,只是替换文本取自 request.replacement

由于 tabs.executeScript() 是一个异步函数,为了确保我们仅在 page-eater.js 中添加侦听器后才发送消息,我们使用 onExecuted(),它将在 page-eater.js 执行后被调用。

注意:Ctrl+Shift+J(或 macOS 上的 Cmd+Shift+J)或 web-ext run --bc 打开 浏览器控制台 以查看后台脚本中的 console.log

或者,使用 附加组件调试器,它允许您设置断点。目前没有办法 直接从 web-ext 启动附加组件调试器

如果我们想从内容脚本向背景页面发送消息,我们会使用runtime.sendMessage()而不是tabs.sendMessage(),例如。

js
browser.runtime.sendMessage({
  title: "from page-eater.js",
});

注意:这些示例都注入 JavaScript;您也可以使用tabs.insertCSS()函数以编程方式注入 CSS。

了解更多