您的第二个扩展程序
如果你已经读过你的第一个扩展这篇文章,你已经了解了如何编写一个扩展。在这篇文章中,你将编写一个稍微复杂一点的扩展,它将展示更多的 API。
这个扩展会在 Firefox 工具栏中添加一个新按钮。当用户点击这个按钮时,我们会显示一个弹出窗口,让他们选择一种动物。一旦他们选择了一种动物,我们就会用所选动物的图片替换当前页面的内容。
为了实现这一点,我们将
-
定义一个浏览器动作,它是一个附加到 Firefox 工具栏的按钮。对于这个按钮,我们将提供
- 一个图标,名为 "beasts-32.png"
- 一个在按钮按下时打开的弹出窗口。弹出窗口将包含 HTML、CSS 和 JavaScript。
-
为扩展定义一个图标,名为 "beasts-48.png"。它将显示在附加组件管理器中。
-
编写一个内容脚本 "beastify.js",它将被注入到网页中。这是实际修改页面的代码。
-
打包一些动物图片,以替换网页中的图片。我们将这些图片设置为“网络可访问资源”,以便网页可以引用它们。
你可以像这样可视化扩展的整体结构

这是一个简单的扩展,但展示了 WebExtensions API 的许多基本概念
- 向工具栏添加按钮
- 使用 HTML、CSS 和 JavaScript 定义弹出面板
- 将内容脚本注入网页
- 在内容脚本和扩展的其余部分之间进行通信
- 将网页可以使用的资源与你的扩展一起打包
编写扩展
创建一个新目录并导航到它
mkdir beastify
cd beastify
manifest.json
现在创建一个名为 "manifest.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_version、name和version是必填项,包含扩展的基本元数据。 -
description和homepage_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 属性。
"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通过在活动选项卡中运行内容脚本来处理用户的选择
mkdir popup
cd popup
touch choose_beast.html choose_beast.css choose_beast.js
choose_beast.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"... 元素将默认隐藏。
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
/**
* 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" 的新文件,内容如下
(() => {
/**
* 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 仓库或此处获取图片



测试它
首先,仔细检查你是否在正确的位置拥有正确的文件
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 工具栏中看到扩展的图标出现
![]()
打开一个网页,点击图标,选择一种野兽,然后查看网页的变化

从命令行开发
你可以使用 web-ext 工具自动化临时安装步骤。试试这个
cd beastify
web-ext run
下一步是什么?
现在你已经为 Firefox 创建了一个更高级的 WebExtension