您的第二个扩展

如果您已经阅读了您的第一个扩展文章,那么您已经了解了如何编写扩展。在本文中,您将编写一个稍微复杂一点的扩展,该扩展演示了更多 API。

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

为了实现这一点,我们将

  • 定义一个浏览器操作,它是一个附加到 Firefox 工具栏的按钮。对于该按钮,我们将提供
    • 一个名为“beasts-32.png”的图标
    • 一个在按下按钮时打开的弹出窗口。弹出窗口将包含 HTML、CSS 和 JavaScript。
  • 为扩展定义一个图标,名为“beasts-48.png”。这将在附加组件管理器中显示。
  • 编写一个内容脚本“beastify.js”,该脚本将注入网页。这实际上是修改页面的代码。
  • 打包一些动物的图像,以替换网页中的图像。我们将使这些图像成为“Web 可访问资源”,以便网页可以引用它们。

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

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 的免费视网膜图标集,并在其许可条款下使用。

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

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

工具栏按钮

工具栏按钮也需要一个图标,并且我们的 manifest.json 承诺我们将有一个位于“icons/beasts-32.png”的工具栏图标。

在“icons”目录中保存一个名为“beasts-32.png”的图标。您可以使用我们示例中的图标,该图标取自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>
  <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
  • 通过注入一些 CSS 来隐藏页面的全部内容,使用browser.tabs.insertCSS() API
  • 使用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”,点击“加载临时加载项”,然后选择您的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

接下来是什么?