工具与框架
现代浏览器扩展很少是从零开始编写的。开发者可利用大量工具,高效地在所选的 JavaScript 框架中构建和测试扩展,并轻松将其发布至多个扩展市场。
使用 React 构建扩展
对于希望使用 JavaScript 框架构建浏览器扩展的开发者而言,React 是最常见的选择。你最终如何将 React 集成到扩展中,在很大程度上取决于应用的范围和复杂性。以下是一些需要考虑的因素:
• 你的扩展有一个还是多个入口点? 此处,“入口点”既可指将加载的 HTML 文件(如 popup.html 或 options.html),也可指通过内容脚本渲染的 UI。单个 HTML 入口点将大大简化扩展的架构。
• 你的扩展 UI 是否需要在不同视图之间共享状态? 扩展仅提供一种异步共享存储机制,这使得使用 Redux 等工具变得更加复杂。
• 你的扩展是否需要在内容脚本中包含一个 React UI? 在页面中挂载单页应用程序与在受控的 HTML 文件中渲染它有所不同。
• 你的单页应用程序将如何使用路由? 内容脚本、选项页面和弹出页面都有不同的路由考虑因素,可能会影响你部署 React 应用的方式。
单入口点 React 扩展
如果你的扩展仅需要一个入口点(如弹出窗口或选项页面),那么使用由 create-react-app
(CRA)生成的应用程序会是一个不错的选择。在本节中,我们将讨论如何使用 CRA 创建一个非常简单的扩展,同时也会介绍使用此策略的一些缺点。首先,我们使用 TypeScript 生成一个 CRA 应用程序:
$ npx create-react-app mvx-react --template typescript
这将生成一个为网页设计的基本应用程序结构。我们需要对其进行自定义,以便在扩展中使用。首先,添加 WebExtensions API 的类型定义:
$ yarn add @types/chrome -D
CRA 会生成一个针对渐进式 Web 应用程序的 manifest.json
。它已经位于 public
目录中,因此非常适合你将其重新用于扩展清单。使用以下内容更新该文件:
manifest.json
{
"name": "MVX React",
"description": "Minimum Viable Extension - React",
"version": "0.0.1",
"manifest_version": 3,
"action": {
"default_popup": "index.html"
},
"icons": {
"16": "logo192.png",
"48": "logo192.png",
"128": "logo192.png"
}
}
最后,还需要进行一项小的 CSS 调整。由于这是在弹出窗口容器中渲染的,因此我们需要强制设置最小尺寸,否则浏览器会将容器折叠为内容的大小。将以下规则添加到 index.css
中:
index.css
body {
...
width: 400px;
min-height: 400px;
}
目前它还无法运行。在构建应用程序时,CRA 会尝试将 JavaScript 代码内联到页面中。而清单 v3 扩展明确禁止这样做,因此你需要配置 CRA 构建过程,使其仅从 URL 加载脚本。你可以通过将环境变量 INLINE_RUNTIME_CHUNK
设置为 false
来防止 CRA 内联脚本。
有多种方法可以实现这一点,但最简单的方法就是在 package.json
命令前加上该赋值:
package.json
{
"scripts": {
"start": "INLINE_RUNTIME_CHUNK=false react-scripts start",
"build": "INLINE_RUNTIME_CHUNK=false react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
...
}
该应用程序已准备好作为扩展进行安装。如果我们将其作为网站运行,我们会执行 npm run start
来启动开发服务器。然而,由于清单 v3 禁止从远程源加载代码(特别是运行在 localhost:3000
上的服务器),因此这样做将无法工作。相反,你需要执行 npm run build
并将构建目录加载到浏览器中。构建成功后,将扩展加载到浏览器中并打开弹出窗口。你应该会看到 CRA 的默认页面(图 16-1)。
提示: 使用 npm run build
是小型项目的合适解决方案,但它并非可扩展的解决方案。请继续阅读本章,以了解更好的构建工具。
多入口点 React 扩展
如果你的应用程序有多个入口点,那么 create-react-app
可能不适合你。你需要一个支持多个独立入口点(包括独立的 JS 和 CSS 包)的开发配置。请继续阅读本章,以了解几种优秀的解决方案。
响应式状态管理
在管理复杂单页应用程序的状态时,大多数开发者会选择响应式状态管理容器,如 Redux。持久化和重新加载状态通常是有利的,这可以使应用程序在重新加载时保持一致。在浏览器扩展的上下文中,有两个主要挑战:
- 我们通常使用的存储 API(如
localStorage
或IndexedDB
)在扩展的不同部分之间并不共享。例如,一个写入localStorage
的弹出窗口将与一个写入localStorage
的内容脚本隔离,因为它们的脚本运行在不同的源上! - 唯一的共享存储机制是
chrome.storage
,且它是异步的。
通常,你可能只需要在多个扩展视图之间共享一小部分状态,如身份验证数据。像路由状态、服务器加载的数据和其他视图中心信息很可能绑定到特定的扩展视图,因此将其持久化在类似 localStorage
的地方可能是可以接受的。例如,一个从服务器加载用户偏好的弹出窗口,如果这些数据不会在其他地方显示,那么将其本地存储是安全的。
在必须在组件之间共享状态的情况下,可以配置 Redux 使用 chrome.storage
作为异步存储。chrome.storage.onChanged
事件意味着每个视图都可以对其他视图改变存储做出反应。有两个流行的 GitHub 仓库实现了这一点:
- https://github.com/ssorallen/redux-persist-webextension-storage
- https://github.com/robinmalburn/redux-persist-chrome-storage
提示:如果你不熟悉 Redux,请在此处阅读 React Redux 库的文档:https://react-redux.js.org/。
路由
在管理复杂单页应用程序的视图状态时,大多数开发者会选择路由解决方案,如 React Router。在浏览器扩展的上下文中,有两个主要挑战:
- 在弹出窗口和选项页面等视图中,浏览器直接加载 HTML 文件,因此使用现成的路由解决方案将导致无效路径(如
index.html/foo/bar
),这些路径在重新加载时会出错。 - 在通过内容脚本渲染的视图中,你不应修改主机页面的 URL。假设主机页面将完全使用 URL 哈希或查询字符串,这可能会破坏你附加到它的任何路由值。
幸运的是,这两个问题都有直接的解决方案。
扩展视图和 HashRouter
在所有浏览器中,扩展视图都有类似于以下的 URL 结构:
extension-protocol://path/to/file.html
所有主要的单页应用程序路由器都支持某种形式的 URL 哈希路由,这不会干扰扩展路径,并且可以在页面重新加载时生存:
extension-protocol://path/to/file.html#/your/app/route
例如,React Router 可以使用 HashRouter
实现此路由策略。
提示:HashRouter
的文档可以在此处找到:https://v5.reactrouter.com/web/api/HashRouter。
如果你希望弹出窗口始终打开最近的 URL,可以通过 chrome.action.setPopup()
动态更新你的弹出窗口 URL,并包含哈希路由。
内容脚本和 MemoryRouter
对于通过内容脚本渲染的视图,你可能有一个复杂的用户界面,可以从路由中受益,但修改页面 URL 栏不是一个选项。所有主要的单页应用程序路由器都支持某种形式的内存中路由。这些路由器通常是为没有 URL 的原生应用程序设计的,但它们同样适用于扩展内容脚本视图。React Router 可以使用 MemoryRouter
实现此路由策略。
如果你需要路由器状态在页面重新加载时生存,可以将路由器状态持久化到应用程序状态中。请注意,你可能需要处理不同标签页之间的多个路由状态。
提示:MemoryRouter
的文档可以在此处找到:https://v5.reactrouter.com/web/api/MemoryRouter。
Mozilla 工具
Mozilla 维护了一套用于开发浏览器扩展的工具。在某种程度上,它们是为开发 Firefox 而设计的,但一般来说,它们可以有效地用于为任何浏览器开发。
web-ext
https://github.com/mozilla/web-ext
web-ext
项目是一套用于开发和发布浏览器扩展的 CLI 工具。它具有以下命令:
web-ext build
从源代码创建扩展包web-ext sign
签署扩展,以便可以在 Firefox 中安装web-ext run
运行扩展web-ext lint
验证扩展源代码web-ext docs
在浏览器中打开web-ext
文档
还有一个 Webpack 插件包装了 web-ext
(不是由 Mozilla 维护的):
https://github.com/hiikezoe/web-ext-webpack-plugin
单独运行 Webpack 将构建扩展,实际上是在 Webpack 构建的输出上运行 web-ext build
。
WebExtension Polyfill
https://github.com/mozilla/webextension-polyfill
浏览器正在逐步将 WebExtensions API 迁移到支持 async/await
的格式,但这一过渡尚未完成。Mozilla 维护了这个 polyfill 库,使整个 API 返回 promise,从而消除了对回调的需求。
打包工具和 CLI 工具
在开发浏览器扩展时,我强烈建议使用某种形式的软件来协助构建和打包扩展,特别是如果你正在使用像 React 这样的 JavaScript 框架,或者你的扩展有多个入口点。由于扩展和框架空间目前正在发生重大变化,因此有很多工具和仓库已经过时:它们要么只针对 manifest v2,要么不支持框架的较新版本。在这个选择中,我精心挑选了一些积极维护的项目,可以支持浏览器扩展的开发。
警告:打包工具的一个主要特性是热模块替换(HMR),即能够在开发者更新源文件时热替换应用程序的部分内容。对于 manifest v3 扩展开发,由于脚本限制,这与 HMR 严格不兼容;你需要禁用传统的 HMR。一些打包工具通过将其更改为以编程方式重新加载扩展来重新设计 HMR。
Parcel
如果你正在为浏览器扩展开发选择一个开源工具,Parcel 绝对是我的首选。它对 manifest v3、多个入口点、TypeScript、React、Vue 和 Sass 都有出色的支持。
对于从 Webpack 世界过来的开发者来说,Parcel 一开始可能会有点让人困惑。Webpack 使用一个密集的配置文件来组织它如何构建应用程序,而 Parcel 则是无配置的。代替冗长的配置文件,它使用预配置的 NPM 包,使 Parcel 能够隐式地理解如何构建和打包应用程序。
你可以安装 @parcel/config-webextension
NPM 包(https://parceljs.org/recipes/web-extension/)来启用 Parcel 构建你的 Web 扩展。使用这个配方,Parcel 可以解析你的 manifest.json
文件以获取入口点。然后,它将解析这些 HTML 文件,以了解它应该如何为每个单独的入口点构建 JS 和 CSS 包。
未编译的文件扩展名(如 TypeScript(.ts
)、React(.jsx
/.tsx
)、Vue(.vue
)和 Sass(.scss
)文件)的转换是自动的,这意味着你可以直接在 manifest 和 HTML 入口点中使用这些文件,Parcel 将为你处理编译和文件路径替换。以下是一些示例:
manifest.json
{
...
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content-script.tsx"],
"css": ["content-script.scss"]
}
],
...
}
popup.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="popup.scss" rel="stylesheet" />
</head>
<body>
<div id="app"></div>
<script type="module" src="popup.tsx"></script>
</body>
</html>
注意:请参阅 Parcel 文档以获取有关如何设置新的浏览器扩展项目的说明,以及其他功能的覆盖:https://parceljs.org/recipes/web-extension/。
Webpack
Webpack 通常是网站开发的首选,对于浏览器扩展开发来说,它仍然是一个合理的选择。与 Parcel 不同,Webpack 喜欢在配置文件中显式定义行为。对于特别复杂的扩展,能够显式指示 Webpack 如何编译和打包你的扩展文件可能对你有利。Webpack 也有一个更大的社区支持,这可能是长期支持的重要考虑因素。
对于希望从针对构建 manifest v3 浏览器扩展的 Webpack 配置文件开始的开发者,我推荐以下两个仓库之一:
- https://github.com/lxieyang/chrome-extension-boilerplate-react
- https://github.com/sszczep/chromeextension-webpack
警告:GitHub 上有相对较多的仓库包含过时的 Webpack 浏览器扩展模板。这些模板针对的是旧版本的 Webpack,或者严格限于 manifest v2。在选择入门仓库时要小心,从它迁移出去可能会非常痛苦。
Plasmo
Plasmo 的网站说得最好:
Plasmo 是一个用于开发浏览器扩展的平台。
像发布网站一样快速发布扩展
浏览器扩展的开发和部署过程有很多令人痛苦的地方,而 Plasmo 的设计旨在隐藏所有这些不愉快的部分。正如你将在本节中看到的,它具有一套强大的功能,可以隐藏生成多个 manifest 版本、支持不同浏览器以及部署到所有扩展市场的不愉快之处。
提示:由于其易用性、活跃的维护者、活跃的开发者社区和强大的功能集,我建议使用 Plasmo 来开发浏览器扩展,而不是其他任何工具。
高层次概述
Plasmo 框架在 Parcel 之上构建了有主见的抽象,极大地改善了扩展开发体验。
与 Parcel 一样,Plasmo 具有约定优于配置的设计理念。例如,如果你想向扩展添加一个选项页面,只需在项目的根目录中创建一个 options.tsx
React 组件,Plasmo 就会完成其余的工作:自动将该文件编译为 JS/CSS/HTML 资产,生成一个新的 manifest.json
,其中包含新的 HTML 文件作为选项页面,并重新加载扩展。
提示:Plasmo 非常容易上手。请查看他们的入门指南:https://docs.plasmo.com/#getting-started
JavaScript 框架
Plasmo 在 JavaScript 框架方面没有主见。它支持 React、Vue 和 Svelte。默认情况下,CLI init 命令将生成一个 React 应用程序,但切换到不同的框架很简单:
- 要使用 Vue,请安装
vue
包并将文件更改为使用.vue
扩展名(例如,popup.vue
)。 - 要使用 Svelte,请安装
svelte
和svelte-preprocess
,初始化你的svelte.config.js
文件,并将文件更改为使用.svelte
扩展名(例如,popup.svelte
)。
文档和示例
长期以来,糟糕的文档一直是扩展开发的一个痛点。Manifest 版本不一致,API 覆盖不完整或不清楚,示例也不一致。Plasmo 具有非常强大和完整的文档:
Plasmo 的文档还包括一系列“快速入门”,详细说明了如何集成流行库。这对像我这样的开发者特别有用,因为我在新项目中的前两件事就是安装和设置 Tailwind 和 Redux。
https://docs.plasmo.com/quickstarts
对于喜欢通过示例学习的开发者,Plasmo GitHub 账户上有一系列广泛的示例,解释了如何完成常见任务:
https://github.com/PlasmoHQ/examples
差异构建输出
Plasmo 具有一些非常有用的 CLI 功能,用于生成应用程序构建。它能够使用命令行标志生成 manifest v2 和 manifest v3 输出,如下所示:
# 生成 build/chrome-dev-mv3/ 目录
pnpm dev --target=chrome-mv3
# 生成 build/firefox-dev-mv2/ 目录
pnpm dev --target=firefox-mv2
# 生成 build/chrome-prod-mv3/ 目录
pnpm build --target=chrome-mv3
# 生成 build/firefox-prod-mv2/ 目录
pnpm build --target=firefox-mv2
注意:chrome-mv3
是默认目标
此外,Plasmo 可以创建这些构建的 zip 文件,这些文件可以直接提交给扩展市场。这是使用 --zip
标志完成的:
# 生成 build/chrome-prod-mv3.zip
pnpm build --zip
# 生成 build/firefox-prod-mv2.zip
pnpm build --target=firefox-mv2 --zip
自动生成 Manifest
与其自己编写 manifest.json
文件,Plasmo 会根据你的源文件和从代码中导出的配置生成 manifest——类似于 Next.js 如何通过文件系统和页面组件抽象页面路由和 SSG。
项目 package.json
中的值通常与扩展 manifest 中的相应值重复。Plasmo 将自动将 package.json
中的值复制到 manifest.json
中:
packageJson.version
->manifest.version
packageJson.displayName
->manifest.name
packageJson.description
->manifest.description
packageJson.author
->manifest.author
packageJson.homepage
->manifest.homepage_url
图标生成
浏览器扩展在整个浏览器中显示其图标,并且这些图标必须在 manifest 中的多个地方列出。如下所示,Plasmo 将从 assets/
目录中的单个源 512x512 图标生成多个调整大小的图标,并自动将它们包含在 manifest 中需要的地方(图 16-2)。
打包远程代码
在 Manifest V3 中,严格禁止加载和执行远程代码。为了解决这一问题,Plasmo 会解析你的导入语句以识别远程托管的资源,自动获取这些资源并将其打包到扩展程序中。在以下截图中,我们可以看到 popup.tsx
正在导入远程的 Google Tag Manager JavaScript 文件,而该文件已自动包含在 build/chrome-mv3-dev/
目录中(图 16-3)。
环境变量
Plasmo 提供了一种基于 dotenv
NPM 包的直观环境变量解决方案。你可以将环境变量放置在项目根目录的 .env
文件中,它们将通过以下方式可用:
- 通过
process.env
命名空间在 JavaScript 中使用 - 在导入的 URL 中使用
- 在
package.json
的清单覆盖中使用
以下代码片段展示了每种用法:
.env
PLASMO_PUBLIC_FOO=foo
PLASMO_PUBLIC_GTAG_ID=123456789
PLASMO_PUBLIC_CRX_PUBLIC_KEY=asdf-1234-asdf-1234
popup.tsx
import `https://www.googletagmanager.com/gtag/js?id=${PLASMO_PUBLIC_GTAG_ID}`;
function IndexPopup() {
return <div>{process.env.PLASMO_PUBLIC_FOO}</div>;
}
export default IndexPopup;
manifest.json
{
"key": "$CRX_PUBLIC_KEY"
}
注意:Plasmo 遵循 Next.js 的 .env
文件约定,支持 .local
、.production
和 .development
后缀规则。更多信息请访问:https://docs.plasmo.com/workflows/env。
内容脚本挂载
在内容脚本中注入用户界面通常需要一些引导操作,而 Plasmo 会作为内容脚本 UI 自动为你完成这些操作。你可以在 content.*
文件中使用 PlasmoContentScript
定义来定义组件。Plasmo 将自动在你的清单中添加相应的 content_scripts
条目。以下是一个示例:
content.tsx
import type { PlasmoContentScript } from "plasmo";
const config: PlasmoContentScript = {
matches: ["<all_urls>"]
};
function IndexContentUI() {
return (
<h1
style={{
backgroundColor: "blue",
color: "white",
padding: "12rem"
}}
>
I'm the content script widget!
</h1>
);
}
export default IndexContentUI;
此组件将被注入到每个网页中。请注意,Plasmo 会将组件注入到 Shadow DOM 容器中,以防止 CSS 干扰(图 16-4)。
面向扩展开发的热模块替换(HMR)友好方案
Manifest V3 禁止传统的热模块替换(HMR),但 Plasmo 提供了一种类似的解决方案。在开发扩展程序时,Plasmo 会向扩展程序的开发版本注入一个 WebSocket 监听器。每当打包文件发生变化时,Plasmo 会发送刷新消息,并根据上下文执行 chrome.runtime.reload()
或 location.reload()
。虽然这与传统 HMR 不完全相同,但它仍然允许你实时查看代码更新。
使用浏览器平台发布(Browser Platform Publish)进行自动化部署
Plasmo 附带了一个名为 Browser Platform Publish(BPP) 的 GitHub 操作,可以自动将扩展程序的更新发布到所有支持的浏览器扩展市场。设置 BPP 的步骤如下:
首次发布扩展程序:
- 在每个市场上手动发布扩展程序的初始版本。在使用 BPP 发布更新之前,每个市场都需要有一个现有的扩展程序。
为每个市场生成凭证:
- 浏览器扩展市场允许你生成身份验证令牌,用于自动签名和发布更新。你需要为每个市场执行此操作。
将凭证提供给 BPP:
- 获取每个市场的凭证后,将其作为加密的仓库密钥提供给 BPP GitHub 操作。
完成上述一次性设置后,你将能够无缝地将更新同时推送到所有扩展市场。以下链接提供了有关如何设置和使用 BPP 的更多详细信息:
- 凭证检索详情:https://github.com/PlasmoHQ/bms/blob/main/tokens.md
- BPP GitHub 操作页面:https://github.com/marketplace/actions/browser-platform-publisher
- BPP GitHub 仓库:https://github.com/PlasmoHQ/bpp
- BPP 使用入门:https://docs.plasmo.com/workflows/submit
有用的网站
https://buildingbrowserextensions.com: 本书的配套网站,包含指向 Browser Extension Explorer 的链接。这是一个 Chrome 扩展程序,包含每个扩展 API 的开源演示。
https://developer.chrome.com/: Chrome 的官方网站,帮助你构建扩展程序、在 Chrome 网上应用店发布、优化网站等。
https://extensionworkshop.com/: 帮助创建和发布 Firefox 插件,使浏览更智能、更安全、更快速。无论你是刚开始扩展开发、准备发布创新,还是开发定制企业解决方案,这里都有你需要的资源。
https://webext.eu/: 只需几次点击即可快速生成浏览器扩展模板。该网站由 Mozilla 维护,非常适合快速原型设计扩展程序。
总结
在本章中,我们讨论了各种自动化工具,这些工具可以加速和简化扩展程序的开发。首先,我们讨论了如何配置 React 以完美适配扩展程序项目,以及如何集成一些流行的 React 库。接下来,我们介绍了一些流行的开源构建工具,这些工具可用于管理浏览器扩展程序的固有复杂性。最后,我们介绍了 Plasmo 平台提供的所有不同功能。