您的第二个扩展程序

如果你已经读过你的第一个扩展这篇文章,你已经了解了如何编写一个扩展。在这篇文章中,你将编写一个稍微复杂一点的扩展,它将展示更多的 API。

这个扩展会在 Firefox 工具栏中添加一个新按钮。当用户点击这个按钮时,我们会显示一个弹出窗口,让他们选择一种动物。一旦他们选择了一种动物,我们就会用所选动物的图片替换当前页面的内容。

为了实现这一点,我们将

  • 定义一个浏览器动作,它是一个附加到 Firefox 工具栏的按钮。对于这个按钮,我们将提供

    • 一个图标,名为 "beasts-32.png"
    • 一个在按钮按下时打开的弹出窗口。弹出窗口将包含 HTML、CSS 和 JavaScript。
  • 为扩展定义一个图标,名为 "beasts-48.png"。它将显示在附加组件管理器中。

  • 编写一个内容脚本 "beastify.js",它将被注入到网页中。这是实际修改页面的代码。

  • 打包一些动物图片,以替换网页中的图片。我们将这些图片设置为“网络可访问资源”,以便网页可以引用它们。

你可以像这样可视化扩展的整体结构

The manifest.json file includes icons, browser actions, including popups, and web accessible resources. The choose beast JavaScript popup resource calls in the beastify script.

这是一个简单的扩展,但展示了 WebExtensions API 的许多基本概念

  • 向工具栏添加按钮
  • 使用 HTML、CSS 和 JavaScript 定义弹出面板
  • 将内容脚本注入网页
  • 在内容脚本和扩展的其余部分之间进行通信
  • 将网页可以使用的资源与你的扩展一起打包

该扩展的完整源代码可以在 GitHub 上找到

编写扩展

创建一个新目录并导航到它

bash
mkdir beastify
cd beastify

manifest.json

现在创建一个名为 "manifest.json" 的新文件,并为其提供以下内容

json
{
  "manifest_version": 2,
  "name": "Beastify",
  "version": "1.0",

  "description": "Adds a browser action icon to the toolbar. Click the button to choose a beast. The active tab's body content is then replaced with a picture of the chosen beast. See https://mdn.org.cn/en-US/Add-ons/WebExtensions/Examples#beastify",
  "homepage_url": "https://github.com/mdn/webextensions-examples/tree/main/beastify",
  "icons": {
    "48": "icons/beasts-48.png"
  },

  "permissions": ["activeTab"],

  "browser_action": {
    "default_icon": "icons/beasts-32.png",
    "default_title": "Beastify",
    "default_popup": "popup/choose_beast.html"
  },

  "web_accessible_resources": [
    "beasts/frog.jpg",
    "beasts/turtle.jpg",
    "beasts/snake.jpg"
  ]
}
  • 前三个键:manifest_versionnameversion 是必填项,包含扩展的基本元数据。

  • descriptionhomepage_url 是可选的,但建议填写:它们提供有关扩展的有用信息。

  • icons 是可选的,但建议填写:它允许你为扩展指定一个图标,该图标将显示在附加组件管理器中。

  • permissions 列出了扩展所需的权限。我们这里只请求 activeTab 权限

  • browser_action 指定了工具栏按钮。我们在这里提供三条信息

    • default_icon 是必填项,指向按钮的图标
    • default_title 是可选的,将显示在工具提示中
    • default_popup 用于在用户点击按钮时显示弹出窗口。我们确实需要,所以我们包含了这个键,并让它指向扩展中包含的 HTML 文件。
  • web_accessible_resources 列出了我们希望对网页可访问的文件。由于扩展用我们与扩展一起打包的图片替换了页面中的内容,因此我们需要使这些图片对页面可访问。

请注意,所有给定的路径都是相对于 manifest.json 本身。

图标

该扩展应有一个图标。它将显示在附加组件管理器中扩展列表的旁边(你可以通过访问 URL "about:addons" 打开它)。我们的 manifest.json 承诺我们会在 "icons/beasts-48.png" 处有一个工具栏图标。

创建 "icons" 目录并将一个名为 "beasts-48.png" 的图标保存在那里。你可以使用我们示例中的图标,该图标取自 Aha-Soft 的免费 Retina 图标集,并在其许可条款下使用。

如果你选择提供自己的图标,它应该是 48x48 像素。你也可以提供一个 96x96 像素的图标,用于高分辨率显示器,如果你这样做,它将在 manifest.json 中指定为 icons 对象的 96 属性。

json
"icons": {
  "48": "icons/beasts-48.png",
  "96": "icons/beasts-96.png"
}

工具栏按钮

工具栏按钮也需要一个图标,我们的 manifest.json 承诺我们会在 "icons/beasts-32.png" 处有一个工具栏图标。

将一个名为 "beasts-32.png" 的图标保存在 "icons" 目录中。你可以使用我们示例中的图标,该图标取自 IconBeast Lite 图标集,并在其许可条款下使用。

如果你不提供弹出窗口,那么当用户点击按钮时,会向你的扩展分派一个点击事件。如果你提供了弹出窗口,则不会分派点击事件,而是打开弹出窗口。我们希望有一个弹出窗口,所以接下来让我们创建它。

弹出窗口

弹出窗口的功能是让用户选择三种动物中的一种。

在扩展根目录下创建一个名为 "popup" 的新目录。我们将在这里存放弹出窗口的代码。弹出窗口将由三个文件组成

  • choose_beast.html 定义面板的内容
  • choose_beast.css 为内容设置样式
  • choose_beast.js 通过在活动选项卡中运行内容脚本来处理用户的选择
bash
mkdir popup
cd popup
touch choose_beast.html choose_beast.css choose_beast.js

choose_beast.html

HTML 文件如下所示

html
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="choose_beast.css" />
  </head>

  <body>
    <div id="popup-content">
      <button>Frog</button>
      <button>Turtle</button>
      <button>Snake</button>
      <button type="reset">Reset</button>
    </div>
    <div id="error-content" class="hidden">
      <p>Can't beastify this web page.</p>
      <p>Try a different page.</p>
    </div>
    <script src="choose_beast.js"></script>
  </body>
</html>

我们有一个 ID 为 "popup-content"<div> 元素,其中包含每个动物选择的按钮和一个重置按钮。我们还有另一个 ID 为 "error-content" 且类为 "hidden"<div>。我们将用它来处理弹出窗口初始化时可能出现的问题。

请注意,我们从这个文件中包含 CSS 和 JS 文件,就像网页一样。

choose_beast.css

CSS 固定了弹出窗口的大小,确保三个选项填满空间,并为它们提供了一些基本样式。它还隐藏了带有 class="hidden" 的元素:这意味着我们的 <div id="error-content"... 元素将默认隐藏。

css
html,
body {
  width: 100px;
}

.hidden {
  display: none;
}

button {
  border: none;
  width: 100%;
  margin: 3% auto;
  padding: 4px;
  text-align: center;
  font-size: 1.5em;
  cursor: pointer;
  background-color: #e5f2f2;
}

button:hover {
  background-color: #cff2f2;
}

button[type="reset"] {
  background-color: #fbfbc9;
}

button[type="reset"]:hover {
  background-color: #eaea9d;
}

choose_beast.js

这是弹出窗口的 JavaScript

js
/**
 * CSS to hide everything on the page,
 * except for elements that have the "beastify-image" class.
 */
const hidePage = `body > :not(.beastify-image) {
                    display: none;
                  }`;

/**
 * Listen for clicks on the buttons, and send the appropriate message to
 * the content script in the page.
 */
function listenForClicks() {
  document.addEventListener("click", (e) => {
    /**
     * Given the name of a beast, get the URL to the corresponding image.
     */
    function beastNameToURL(beastName) {
      switch (beastName) {
        case "Frog":
          return browser.runtime.getURL("beasts/frog.jpg");
        case "Snake":
          return browser.runtime.getURL("beasts/snake.jpg");
        case "Turtle":
          return browser.runtime.getURL("beasts/turtle.jpg");
      }
    }

    /**
     * Insert the page-hiding CSS into the active tab,
     * then get the beast URL and
     * send a "beastify" message to the content script in the active tab.
     */
    function beastify(tabs) {
      browser.tabs.insertCSS({ code: hidePage }).then(() => {
        const url = beastNameToURL(e.target.textContent);
        browser.tabs.sendMessage(tabs[0].id, {
          command: "beastify",
          beastURL: url,
        });
      });
    }

    /**
     * Remove the page-hiding CSS from the active tab,
     * send a "reset" message to the content script in the active tab.
     */
    function reset(tabs) {
      browser.tabs.removeCSS({ code: hidePage }).then(() => {
        browser.tabs.sendMessage(tabs[0].id, {
          command: "reset",
        });
      });
    }

    /**
     * Just log the error to the console.
     */
    function reportError(error) {
      console.error(`Could not beastify: ${error}`);
    }

    /**
     * Get the active tab,
     * then call "beastify()" or "reset()" as appropriate.
     */
    if (e.target.tagName !== "BUTTON" || !e.target.closest("#popup-content")) {
      // Ignore when click is not on a button within <div id="popup-content">.
      return;
    }
    if (e.target.type === "reset") {
      browser.tabs
        .query({ active: true, currentWindow: true })
        .then(reset)
        .catch(reportError);
    } else {
      browser.tabs
        .query({ active: true, currentWindow: true })
        .then(beastify)
        .catch(reportError);
    }
  });
}

/**
 * There was an error executing the script.
 * Display the popup's error message, and hide the normal UI.
 */
function reportExecuteScriptError(error) {
  document.querySelector("#popup-content").classList.add("hidden");
  document.querySelector("#error-content").classList.remove("hidden");
  console.error(`Failed to execute beastify content script: ${error.message}`);
}

/**
 * When the popup loads, inject a content script into the active tab,
 * and add a click handler.
 * If we couldn't inject the script, handle the error.
 */
browser.tabs
  .executeScript({ file: "/content_scripts/beastify.js" })
  .then(listenForClicks)
  .catch(reportExecuteScriptError);

这里要从第 99 行开始。弹出脚本使用 browser.tabs.executeScript() API,在弹出窗口加载后立即在活动选项卡中执行内容脚本。如果执行内容脚本成功,则内容脚本将保留在页面中,直到选项卡关闭或用户导航到其他页面。

browser.tabs.executeScript() 调用失败的一个常见原因是您无法在所有页面中执行内容脚本。例如,您不能在 about:debugging 等特权浏览器页面中执行它们,也不能在 addons.mozilla.org 域中的页面上执行它们。如果失败,reportExecuteScriptError() 将隐藏 <div id="popup-content"> 元素,显示 <div id="error-content"... 元素,并将错误记录到控制台

如果执行内容脚本成功,我们将调用 listenForClicks()。它会监听弹出窗口上的点击事件。

  • 如果点击未发生在弹出窗口中的按钮上,我们将忽略它并什么都不做。
  • 如果点击发生在 type="reset" 的按钮上,那么我们调用 reset()
  • 如果点击发生在任何其他按钮(即动物按钮)上,那么我们调用 beastify()

beastify() 函数执行三件事

  • 将点击的按钮映射到指向特定动物图像的 URL
  • 使用 browser.tabs.insertCSS() API 注入一些 CSS,隐藏页面的全部内容
  • 使用 browser.tabs.sendMessage() API 向内容脚本发送一个 "beastify" 消息,请求它美化页面,并向它传递动物图像的 URL。

reset() 函数本质上是撤销一个美化操作

  • 使用 browser.tabs.removeCSS() API 移除我们添加的 CSS
  • 向内容脚本发送一个 "reset" 消息,要求它重置页面。

内容脚本

在扩展根目录下创建一个名为 "content_scripts" 的新目录,并在其中创建一个名为 "beastify.js" 的新文件,内容如下

js
(() => {
  /**
   * Check and set a global guard variable.
   * If this content script is injected into the same page again,
   * it will do nothing next time.
   */
  if (window.hasRun) {
    return;
  }
  window.hasRun = true;

  /**
   * Given a URL to a beast image, remove all existing beasts, then
   * create and style an IMG node pointing to
   * that image, then insert the node into the document.
   */
  function insertBeast(beastURL) {
    removeExistingBeasts();
    const beastImage = document.createElement("img");
    beastImage.setAttribute("src", beastURL);
    beastImage.style.height = "100vh";
    beastImage.className = "beastify-image";
    document.body.appendChild(beastImage);
  }

  /**
   * Remove every beast from the page.
   */
  function removeExistingBeasts() {
    const existingBeasts = document.querySelectorAll(".beastify-image");
    for (const beast of existingBeasts) {
      beast.remove();
    }
  }

  /**
   * Listen for messages from the background script.
   * Call "insertBeast()" or "removeExistingBeasts()".
   */
  browser.runtime.onMessage.addListener((message) => {
    if (message.command === "beastify") {
      insertBeast(message.beastURL);
    } else if (message.command === "reset") {
      removeExistingBeasts();
    }
  });
})();

内容脚本做的第一件事是检查全局变量 window.hasRun:如果它已设置,脚本会提前返回,否则它会设置 window.hasRun 并继续。我们这样做是因为每次用户打开弹出窗口时,弹出窗口都会在活动选项卡中执行一个内容脚本,因此我们可能在单个选项卡中运行多个脚本实例。如果发生这种情况,我们需要确保只有第一个实例会实际执行任何操作。

之后,从第 40 行开始,内容脚本使用 browser.runtime.onMessage API 监听来自弹出窗口的消息。我们上面看到弹出脚本可以发送两种不同类型的消息:“beastify”和“reset”。

  • 如果消息是 "beastify",我们期望它包含一个指向野兽图片的 URL。我们移除任何可能由之前的 "beastify" 调用添加的野兽,然后构造并附加一个 <img> 元素,其 src 属性设置为野兽 URL。
  • 如果消息是 "reset",我们只需移除可能已添加的任何野兽。

野兽们

最后,我们需要包含野兽的图像。

创建一个名为 "beasts" 的新目录,并将这三张图片添加到该目录中,并使用适当的名称。您可以从GitHub 仓库或此处获取图片

A brown frog.

An emerald tree boa with white stripes.

A red-eared slider turtle.

测试它

首先,仔细检查你是否在正确的位置拥有正确的文件

beastify/

    beasts/
        frog.jpg
        snake.jpg
        turtle.jpg

    content_scripts/
        beastify.js

    icons/
        beasts-32.png
        beasts-48.png

    popup/
        choose_beast.css
        choose_beast.html
        choose_beast.js

    manifest.json

现在将扩展作为临时附加组件加载。在 Firefox 中打开 "about:debugging",点击 "Load Temporary Add-on"(加载临时附加组件),然后选择你的 manifest.json 文件。然后你应该会在 Firefox 工具栏中看到扩展的图标出现

The beastify icon in the Firefox toolbar

打开一个网页,点击图标,选择一种野兽,然后查看网页的变化

A page replaced with the image of a turtle

从命令行开发

你可以使用 web-ext 工具自动化临时安装步骤。试试这个

bash
cd beastify
web-ext run

下一步是什么?

现在你已经为 Firefox 创建了一个更高级的 WebExtension