您的第二个扩展
如果您已经阅读了您的第一个扩展文章,那么您已经了解了如何编写扩展。在本文中,您将编写一个稍微复杂一点的扩展,该扩展演示了更多 API。
该扩展向 Firefox 工具栏添加了一个新按钮。当用户点击该按钮时,我们将显示一个弹出窗口,允许他们选择一种动物。一旦他们选择了一种动物,我们将用所选动物的图片替换当前页面的内容。
为了实现这一点,我们将
-
定义一个浏览器操作,它是一个附加到 Firefox 工具栏的按钮。对于该按钮,我们将提供
- 一个名为“beasts-32.png”的图标
- 一个在按下按钮时打开的弹出窗口。弹出窗口将包含 HTML、CSS 和 JavaScript。
- 为扩展定义一个图标,名为“beasts-48.png”。这将在附加组件管理器中显示。
- 编写一个内容脚本“beastify.js”,该脚本将注入网页。这实际上是修改页面的代码。
- 打包一些动物的图像,以替换网页中的图像。我们将使这些图像成为“Web 可访问资源”,以便网页可以引用它们。
您可以这样可视化扩展的整体结构
这是一个简单的扩展,但展示了许多 WebExtensions API 的基本概念
- 向工具栏添加按钮
- 使用 HTML、CSS 和 JavaScript 定义弹出面板
- 将内容脚本注入网页
- 在内容脚本和扩展的其余部分之间进行通信
- 打包扩展程序中可由网页使用的资源
您可以在GitHub 上找到扩展的完整源代码。
编写扩展
创建一个新目录并导航到该目录
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 的免费视网膜图标集,并在其许可条款下使用。
如果您选择提供自己的图标,它应该是 48x48 像素。您还可以提供一个 96x96 像素的图标,用于高分辨率显示器,如果您这样做,它将被指定为 manifest.json 中 icons
对象的 96
属性
"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
通过在活动选项卡中运行内容脚本来处理用户的选择
mkdir popup
cd popup
touch choose_beast.html choose_beast.css choose_beast.js
choose_beast.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"...
元素默认情况下将处于隐藏状态。
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
- 通过注入一些 CSS 来隐藏页面的全部内容,使用
browser.tabs.insertCSS()
API - 使用
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”,我们只需删除可能添加的任何野兽。
野兽
测试
首先,仔细检查您是否在正确的位置拥有正确的文件
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 工具栏中
打开一个网页,点击图标,选择一个野兽,然后查看网页的变化
从命令行开发
您可以使用web-ext 工具自动执行临时安装步骤。试试这个
cd beastify
web-ext run
接下来是什么?
既然您已经为 Firefox 创建了一个更高级的 WebExtension