什么是 Web 扩展?
Web 扩展社区小组于 2021 年在 W3C 成立,由苹果、谷歌、微软和 Mozilla 发起,旨在就浏览器扩展的共同愿景达成一致,并努力实现未来的标准化。
扩展的核心是manifest.json文件,这是任何 Web 扩展中唯一必须存在的文件。清单文件包含有关扩展的元数据,以及指向构成扩展的其他文件的指针。
这些其他文件包括后台脚本、侧边栏、弹出窗口和选项页面、扩展页面、内容脚本和 Web 可访问资源。
这个manifest.json文件有特定的语法,截至撰写本文时,最新语法应该是 v3。如果你没有一直关注 Web 扩展的发展,可能不知道 v3 引起了不少争议。
我不会详述所有细节,但总结一下主要批评:v3 将 Web 扩展从被视为拥有自己持久执行环境的一等应用程序,降级为具有有限权限和反应式执行能力的配件。这主要是由于强制使用服务工作者并移除"阻塞式 webRequest"机制的结果。
如果你感兴趣,可以自行谷歌更多信息。
manifest.json
这是一个什么都不做,只展示浏览器识别扩展存在的最低要求的示例:
{
"manifest_version": 3,
"name": "AE1",
"version": "1.0",
"description": "这个扩展实际上什么都不做",
"icons": {
"32": "icons/icon32.png",
"48": "icons/icon48.png"
},
"action": {
"default_popup": "nothing.html"
}
}
完整代码见:github.com/huijing/sli…
manifest.json必须有 3 个必填键:manifest_version、name和version。示例中还包含了description、icons和action键。
虽然description和icons是可选的,但如果你想在 Chrome 网上应用店发布扩展,它们是必需的。它们也能提升用户体验,因为给你的扩展一个漂亮的图标和适当的描述。
action决定了扩展在浏览器工具栏中的外观,以及点击它时的行为。popup可以包含任何 HTML 内容,窗口会自动调整大小以适应内容。
<html>
<body>
<h1>什么都没有</h1>
</body>
</html>
要为扩展添加一些样式和交互性,可以通过 link 或 style 和 script 元素包含 CSS 和 JavaScript。
<html>
<body>
<h1>什么都没有</h1>
<button>有点东西</button>
</body>
<style>
body {
text-align: center;
}
</style>
<script src="nothing.js"></script>
</html>
document.querySelector("button").addEventListener("click", () => {
document.querySelector("h1").style.color = "tomato";
});
弹出窗口运行的环境与浏览器加载的网页内容是隔离的。在这个例子中,nothing.js针对的是非常通用的button和h1标签元素,但上面的代码只影响弹出窗口中的元素,而不影响网页上的任何内容。
如果你像我一样是个前端开发者,可能更喜欢看到代码执行后的一些视觉反馈。
要在 Firefox 中加载扩展,你必须将其作为临时附加组件加载。在你修改源代码后重新加载扩展的选项也是可用的。
对于 Chrome,你必须先启用开发者模式,然后才能加载解压的扩展。相同的源代码在两个浏览器中都能工作。
内容脚本
当我们想让扩展实际对网页内容做些事情时,就需要使用内容脚本,它在浏览器加载的网页上下文中运行。这是我们能从扩展访问页面内容的唯一方式。
有 3 种方法可以将扩展的内容脚本加载到网页中:静态声明、动态声明或以编程方式。这些不同的方法允许最广泛的使用场景,无论你是想让扩展默认就修改体验,还是基于特定触发器。
内容脚本是隔离的,意思是它可以对自己的 JavaScript 环境进行更改,而不会与页面或其他扩展的内容脚本冲突。尽管内容脚本可以访问和修改 DOM,但它们只能看到未被任何 JavaScript 修改过的"干净"版本的 DOM。
Firefox 和 Chrome 处理这种隔离行为的方式不同。在 Firefox 中,这个概念被称为Xray Vision,内容脚本可能会遇到来自自己全局作用域的 JavaScript 对象或来自网页的 Xray Vision包装版本。
而在 Chrome 中,存在 3 种世界的概念:主世界、隔离世界和工作世界。每个世界都有自己的上下文、自己的全局变量作用域和原型链。
内容脚本可以直接访问一组特定且有限的扩展 API,但对于其他 API,需要某种形式的消息交换,我们将在下一个示例中简要提及。
这个扩展包含一个按钮,将接管整个网页并用来自互联网的一些像素艺术覆盖它。完整代码见:github.com/huijing/sli…
{
"manifest_version": 3,
"name": "AE2",
"version": "1.0",
"description": "激活像素艺术",
"icons": {
"32": "icons/icon32.png",
"48": "icons/icon48.png"
},
"permissions": ["activeTab", "scripting"],
"action": {
"default_popup": "pixel.html"
},
"web_accessible_resources": [
{
"resources": [
"images/pixel-adventure-time.png",
"images/pixel-cat.jpg",
"images/pixel-city.png",
"images/pixel-zen-garden.png"
],
"extension_ids": ["*"],
"matches": ["*://*/*"]
}
]
}
为了让扩展识别和加载图像,它们需要在manifest.json文件中用web_accessible_resources键声明。扩展还需要activeTab权限,这仅在用户交互发生时为活动标签页授予扩展额外权限,以及脚本权限,这是在内容脚本中调用脚本 API 方法所必需的。
let id;
browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
id = tabs[0].id;
browser.scripting.executeScript({
target: { tabId: tabs[0].id },
files: ["content.js"],
});
browser.scripting.insertCSS({
target: { tabId: tabs[0].id },
files: ["content.css"],
});
});
document.getElementById("pixelate").addEventListener("click", () => {
browser.tabs.sendMessage(id, { message: "pixelate" });
});
document.getElementById("reset").addEventListener("click", () => {
browser.tabs.sendMessage(id, { message: "reset" });
});
browser.tabs.query()用于获取我们想要定位的标签页信息,特别是标签页 ID,因为脚本方法需要传入一个标签页 ID,这很合理,因为我们必须指定我们想要注入脚本的目标,对吧?
注入 CSS 也是一样的道理。是否可以通过 JavaScript 来样式化被注入的内容?当然可以。我们应该那样做吗?这完全取决于你。我个人更喜欢把样式放在 CSS 文件中,仅此而已。
那么这个sendMessage()方法又是什么呢?因为内容脚本在网页的上下文中运行,而不是扩展本身,这是内容脚本与扩展通信的方式。扩展和内容脚本将监听彼此的消息,并在同一通道上响应。
要访问扩展提供的图像,需要使用runtime.getURL()方法,它将图像的相对路径转换为浏览器可以正确渲染的完全限定 URL。最后,使用runtime.onMessage()事件来监听消息。
后台脚本
Web 扩展中另一种常见的脚本是后台脚本。它们旨在监视浏览器中的事件并相应地做出反应。例如,如果我们想为扩展实现键盘快捷键,可以利用后台脚本监听特定命令并触发某些操作。
下一个示例将通过使用后台脚本为扩展添加一些键盘快捷键。完整代码见:github.com/huijing/sli…。
需要对manifest.json文件进行一些添加才能使功能正常工作。
{
"background": {
"service_worker": "background.js", // v3语法
"scripts": ["background.js"] // v2语法
},
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Ctrl+Shift+Y"
}
},
"pixelate": {
"suggested_key": {
"default": "Alt+A"
},
"description": "向扩展发送'pixelate'事件"
},
"reset": {
"suggested_key": {
"default": "Ctrl+Shift+E"
},
"description": "向扩展发送'reset'事件"
}
}
}
Commands API 让我们可以定义命令并将它们绑定到特定的按键组合。这些命令必须首先在manifest.json文件中声明为属性。然后我们使用后台脚本监听 onCommand 事件的触发,并在按下正确的按键组合时运行我们想要运行的任何内容。
有 4 个特殊快捷键具有默认操作,onCommand事件不会触发,_execute_action就是其中之一。这个快捷键的行为就像用户点击了工具栏中的扩展图标一样。
在background.js文件中,我们使用onCommand.addListener为清单中列出的每个命令绑定一个处理程序。如前所述,_execute_action命令不会触发事件,所以我们不需要为它准备处理程序。
总结
这涵盖了开始 Web 扩展开发所需的所有基本信息。扩展可以简单到改变页面上文本的颜色,也可以复杂到一个完整的应用程序(是的,如果你愿意,可以使用 React 来构建你的扩展)。
我想如果你对可疑的扩展商店下载心存警惕,你完全可以构建自己的扩展来做你想让它做的任何事情,为什么不呢?