修改网页

扩展最常见的用例之一就是修改网页。例如,一个扩展可能希望更改应用于页面的样式、隐藏特定的 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 和 title(将在上下文菜单中显示的文本)。然后,我们设置了一个事件监听器,以便当用户单击上下文菜单项时,我们检查它是否是我们 eat-page 项。如果是,我们使用 tabs.executeScript() API 将“page-eater.js”注入当前标签页。此 API 可选地接受标签页 ID 作为参数:我们省略了标签页 ID,这意味着脚本被注入到当前活动标签页中。

此时,扩展应该如下所示:

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

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

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。

了解更多