发布于 2026-01-06 6 阅读
0

使用 Rust 和 WASM 的 Chrome 扩展示例

使用 Rust 和 WASM 的 Chrome 扩展示例

本文解释了使用 Rust 构建并编译成 WASM 的 Chrome 扩展程序的内部工作原理。

目标受众:对使用 WASM 构建跨浏览器扩展程序感兴趣的 Rust 程序员。

关于此扩展程序: GitHubChrome 网上应用商店Mozilla 网上应用商店

你将学到:

  • 工具链
  • 扩展的架构
  • WASM、背景脚本和内容脚本如何相互通信
  • 拦截会话令牌以冒充用户
  • 调试
  • 使其适用于 Chrome 和 Firefox
  • 在 Google 和 Mozilla 插件商店中列出

如果你一边查看源代码一边阅读这篇文章,你的时间将得到最大程度的利用。

工具链

构建、测试和发布 WASM 的主要工具:https://rustwasm.github.io/docs/wasm-pack/

cargo install wasm-pack
Enter fullscreen mode Exit fullscreen mode

有关构建命令的示例,请参阅build.sh

cargo会在首次编译时安装wasm-bindgen,以促进 wasm 模块和 JavaScript 之间的高级交互。

参考

扩展的架构

该扩展程序由多个逻辑模块组成:

  • manifest 文件——浏览器扩展的定义
  • 后台脚本——一组JS脚本和WASM代码,它们在独立的浏览器进程中运行,不受当前网页的影响。
  • 内容脚本- 扩展程序中在当前网页内运行的部分,但此扩展程序未使用它。
  • 弹出窗口- 当用户点击工具栏中的扩展程序按钮时,浏览器显示的窗口

文件夹

解决方案文件夹截图

  • 扩展程序:指加载到浏览器中的扩展程序代码,包括 JS 和 WASM。
  • 媒体:用于发布扩展名的各种媒体文件,并非必需。
  • wasm_mod:要编译成 WASM 的 Rust 代码
  • wasm_mod/samples:Spotify 请求/响应捕获,非必需

清单 V3

由于不同浏览器的功能不兼容,我们需要为不同的浏览器使用不同的 manifest V3 文件:

  • extension/manifest_cr.json:Chrome 版本
  • extension/manifest_ff.json:Firefox 版本

如本文档的调试打包部分所述,build.sh会将浏览器特定的清单文件重命名为manifest.json

需要注意的清单属性列表:

  • action/show_matches - 浏览器何时激活扩展程序按钮
  • action/default_popup - 当用户点击扩展按钮时应该发生的情况
  • background/service_worker - 要作为后台服务工作线程运行的脚本的名称
  • content_security_policy - 声明扩展程序可以执行的操作,例如加载脚本或 WASM。

其他属性要么不言自明,要么不太重要。

Manifest V3 文档:MDN / Chrome

背景脚本

extension/js/background.js充当 Service Worker 的角色。“后台脚本”这个名称是历史遗留问题,可以与“Service Worker”互换使用。

它的工作原理background.js是:

  • 加载并初始化 WASM 模块
  • 监听来自 WASM 和弹出页面(extension/js/popup.html)的消息
  • 向弹出页面(extension/js/popup.js)发送错误消息
  • 捕获会话令牌(captureSessionToken()
  • 获取用户详细信息(fetchUserDetails()
  • 从当前标签页 URL 中提取当前播放列表 ID ( getPlaylistIdFromCurrentTabUrl())
  • 响应用户操作而调用 WASM 函数(add_random_tracks()
  • 控制工具栏中的扩展程序图标(toggleToolbarBadge()

background.js浏览器启动时加载并执行以下操作:

  • WASM模块初始化
  • 添加消息传递和令牌捕获监听器

一旦运行,background.js其 WASM 部分会继续独立于启动它的页面运行。即使用户导航到其他页面或关闭启动它的标签页,Service Worker 也会继续运行,因为Service Worker 的生命周期与文档的生命周期是相互独立的。

关于 Service Worker 的更多信息:MDN / Chrome

弹出页面

清单文件指示浏览器在用户激活扩展程序时(例如,单击工具栏中的扩展程序图标)打开一个弹出窗口。

弹出窗口激活

清单文件中的这一行定义了当用户点击工具栏按钮时要打开的弹出窗口:

"default_popup": "js/popup.html"
Enter fullscreen mode Exit fullscreen mode

弹出窗口仅在显示期间存在。它无法运行任何长时间运行的进程,也不会在两次激活之间保留状态。DOMContentLoaded每次激活时都会触发所有事件。

它的工作原理popup.js是:

  • 将事件处理程序附加到按钮和链接上
  • 处理链接的点击事件,因为浏览器不会从弹出窗口中打开链接 URL(参见chrome.tabs.create())。
  • 监听来自background.jsWASM 的消息(参见chrome.runtime.onMessage.addListener()
  • 显示从 WASM 和background.js接收的消息的活动日志。

popup.html中的任何内联 JavaScript 代码都会被忽略。所有 JavaScript 代码或事件处理程序都必须从外部加载,<script type="module" src="popup.js">因为清单文件中只允许使用src属性。content_security_policy/extension_pages

内容脚本

内容脚本与用户界面和 DOM 进行交互。它们与页面在同一上下文中运行,并以Window对象作为其主要浏览器上下文。

此扩展程序不与 Spotify 标签页交互,也不需要内容脚本。

脚本间的消息传递

不同脚本(后台脚本、弹出窗口脚本、WASM 脚本)之间的通信通过异步消息传递实现。脚本 A 向共享消息池发送消息,希望目标接收者正在监听并能够理解该消息。这意味着如果有多个监听者,它们都会收到消息通知。发送者不会收到其发送的消息的通知。

可以在同一扩展程序中调用不同脚本中的函数,但该脚本将在调用者的上下文中运行。例如,popup.js可以在用户点击弹出窗口中的按钮时调用一个background.js函数。该调用将在弹出窗口的上下文中执行,并在弹出窗口关闭后立即终止。

另一方面,background.js这是一个长时间运行的进程。它会监听其他脚本发送给它的消息。例如,该chrome.runtime.onMessage.addListener()函数会检查消息的有效负载,并根据消息内容采取相应的操作。

此扩展程序依赖于消息传递,而不是直接调用。

关键信息传递概念:

  • 消息是对象
  • 邮件中唯一附加的元数据是发件人的详细信息。
  • 构建消息结构,以包含任何其他元数据,例如有效载荷的类型。
  • 务必捕获chrome.runtime.sendMessage()错误,因为无法保证交付成功,而未处理的错误会导致整个脚本运行失败。
  • 如果发送消息时没有活动的监听器,例如,您期望一个弹出窗口监听,但用户已将其关闭,则始终会引发错误。
  • 消息发送者无法接收自己的消息,因此,如果您在同一个脚本中发送和监听消息,则不会收到消息通知,只会通知其他监听者。

有关不同脚本之间消息传递的更多信息:https://developer.chrome.com/docs/extensions/develop/concepts/messaging

消息传递示例

从 background.js 到 popup.js

background.js它会向所有监听者发送错误信息。对于未送达的错误,它会直接忽略。

popup.js监听并显示弹出窗口(如果popup.html已打开)。如果弹出窗口未打开,则没有监听器,发送方会收到错误。为了使发送方脚本的其余部分正常运行,必须处理这些错误。

chrome.runtime.sendMessage("Already running. Restart the browser if stuck on this message.").then(onSuccess, onError);
Enter fullscreen mode Exit fullscreen mode

其中两者onSuccessonError没有采取任何措施来阻止错误向上冒泡。

从 popup.js 到 background.js

popup.js当用户点击“添加曲目”按钮时,发送一条消息。

background.js监听并调用 WASM 来处理用户请求。

弹出窗口脚本可以调用后台脚本中的函数来调用 WASM,甚至可以直接调用 WASM,但弹出窗口仅在打开时存在。此外,它也无法访问存储在长时间运行background.js进程上下文中的令牌。

因此,我们有一个长时间运行的后台脚本,它独立于标签页或弹出窗口运行。当弹出窗口收到消息时,background.js会调用 WASM,即使调用者已不存在,它也会继续运行。

从 WASM 向 JS 脚本发送消息

report_progress()WASM 通过脚本中的函数发送消息wasm_mod/src/progress.js。该函数被导入到wasm_mod/src/lib.rsas 中。

#[wasm_bindgen(module = "/src/progress.js")]
extern "C" {
    pub fn report_progress(msg: &str);
}
Enter fullscreen mode Exit fullscreen mode

并且可以从其他 Rust 函数中作为原生 Rust 函数调用。

WASM 进程持续运行时,WASM 会将进度报告近乎实时地发送到弹出窗口。

WASM

wasm_mod文件夹包含扩展程序的 WASM 部分。

货物内部.toml

货物文件:wasm_mod/Cargo.toml

crate-type = ["cdylib", "rlib"]

  • cdylib:用于创建动态系统库,例如 .so 或 .dll 文件,但对于 WebAssembly 目标,它会创建一个没有 start 函数的 *.wasm 文件。
  • rlib:可选,用于创建用于单元测试的中间“Rust 库”。wasm-pack test

关于目标的更多信息:https://doc.rust-lang.org/reference/linkage.html

依赖关系

  • wasm-bindgen#[wasm_bindgen] :包含对属性、JsValue接口和其他 JS 绑定的运行时支持( crate 文档
  • js-sys:绑定到 JS 类型,例如 Array、Date 和 Promise(crate 文档
  • web-sys:Web API 的原始 API 绑定,1:1 绑定,例如浏览器窗口对象是 web_sys::Window,它具有所有可通过 JS 访问的方法、属性和事件(crate 文档)。

请记住在 Cargo.toml 文件中将 WebAPI 类和接口作为web-sys功能添加进去。例如,如果您想在 Rust 代码中使用Window类,则必须先将其添加到web-sys功能中。

关于 Rust 如何绑定浏览器和 JS API 的更多信息:https://rustwasm.github.io/wasm-pack/book/tutorials/npm-browser-packages/template-deep-dive/cargo-toml.html

从 JS 调用 WASM 示例

background.js会调用lib.rshello_wasm()中的函数,在浏览器控制台中记录一条问候语以作演示之用。

hello_wasm()序列图

hello_wasm() 调用堆栈

Rust 是如何console.log()调用它的?

为了将日志记录到浏览器控制台,我们的 Rust 代码需要访问浏览器提供的 WebAPI 日志记录函数。lib.rs
导入了 WebAPIconsole.log()函数,并使其在 Rust 中可用:

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace=console)]
    fn log(s: &str);
}
Enter fullscreen mode Exit fullscreen mode

底层绑定由web-sysjs-sys crates 借助wasm-bindgenwasm-pack完成。

Rust 如何hello_wasm()从 JS 调用

要从 JS 调用 Rust/WASM 函数,我们需要使用#[wasm_bindgen].

lib.rs导出hello_wasm()Rust 函数并使其可供以下对象使用background.js

#[wasm_bindgen]
pub fn hello_wasm() {
    log("Hello from WASM!");
}
Enter fullscreen mode Exit fullscreen mode

wasm-bindgenwasm-pack每次运行构建时都会生成wasm_mod.js文件。该文件是 WASM 和 JS 之间的粘合剂。它的一些主要特性如下:

  • 初始化 WASM 模块
  • background.js的导出wasm.hello_wasm()功能
  • 类型和参数检查

WASM是如何初始化的

每次浏览器激活扩展程序时,都会运行background.js中的以下代码

import initWasmModule, { hello_wasm } from './wasm/wasm_mod.js';
(async () => {
    await initWasmModule();
    hello_wasm(); 
})();
Enter fullscreen mode Exit fullscreen mode

wasm-bindgenwasm-pack生成初始化例程,并将其作为默认导出项放置在wasm_mod.js文件中。

lib.rs

lib.rs是我们 WASM 模块的顶层文件。它暴露了 Rust 函数并绑定到 JS 接口。

主要入口

向当前播放列表添加曲目的主要入口点是pub async fn add_random_tracks(...)

浏览器上下文

代码需要访问一些全局函数,这些函数只能通过浏览器上下文或运行时实例访问。实现此目的的两个主要类是WorkerGlobalScopeWindow

两者之间最重要的区别在于:

  • WorkerGlobalScope可供 Service Worker(独立于网页运行的后台脚本)使用
  • Window可供与网页在同一进程中运行的内容脚本使用

这两个类非常相似,但属性和方法集略有不同。

lib.rs获取对正确浏览器运行时环境的引用async fn get_runtime(),并将其传递给其他函数。

get_runtime()首先尝试获取对某个对象的引用WorkerGlobalScope。如果失败,则尝试获取对Window另一个对象的引用。一种方法在 Chrome 浏览器中有效,另一种方法在 Firefox 浏览器中有效。

将进度报告反馈给用户界面。

我们的WASM代码可能需要几分钟才能运行完毕。它会通过浏览器消息传递API将运行进度和失败情况报告给用户界面。

pub fn report_progress(msg: &str)lib.rs中的代理是progress.jsreport_progress()的代理 它在我们的 Rust 代码中被多个位置调用,用于向 progress.js 发送进度和错误消息
popup.js

为了让 Rust 代码能够使用自定义的 JS 函数,我们创建了wasm_mod/src/progress.js 文件,并添加了以下导出:

export function report_progress(msg) {
  chrome.runtime.sendMessage(msg).then(handleResponse, handleError);
}
Enter fullscreen mode Exit fullscreen mode

并通过在lib.rs中创建以下导入语句来匹配它

#[wasm_bindgen(module = "/src/progress.js")]
extern "C" {
    pub fn report_progress(msg: &str);
}
Enter fullscreen mode Exit fullscreen mode

wasm-bindgenwasm-pack生成必要的绑定并将progress.js复制到extension/js/wasm/snippets/wasm_mod-bc4ca452742d1bb1/src/progress.js

其他 .rs 文件

其余的 .rs 文件包含可以编译成 WASM 的纯 Rust 代码。

  • client.rs——一个将所有内容整合在一起的高级函数。
  • api_wrappers.rs - Spotify API 的封装库,由 client.rs 调用。
  • constants.rs - 共享常量、实用函数
  • models.rs - 用于 Spotify 请求和响应的 Rust 结构体

令牌捕获

该扩展程序需要某种凭证才能代表用户与 Spotify 通信。一种无需用户授权即可实现此目的的方法是获取当前会话令牌。为此,用户必须访问 Spotify 网页并登录。

经常使用 Spotify 的用户应该已经登录,因为 Spotify 在所有 *.spotify.com 页面上都会保持一个活跃的会话。

此扩展程序会捕获发送到https://api-partner.spotify.com/pathfinder/v1/query端点的所有请求标头,包括令牌。这些标头存储在本地变量中,并复制到扩展程序发出的请求中,以模拟 Spotify 应用的行为。

captureSessionToken()background.js中的函数会在onBeforeSendHeaders事件触发时执行标头提取:

chrome.webRequest.onBeforeSednHeaders.addListener(captureSessionToken, { urls: ['https://api-partner.spotify.com/pathfinder/v1/query*'] }, ["requestHeaders"])
Enter fullscreen mode Exit fullscreen mode
  • onBeforeSendHeaders监听器读取,但不修改标头https://api-partner.spotify.com/pathfinder/v1/query*
  • URL 末尾的逗号*是该模式生效所必需的。
  • ["requestHeaders"]该参数指示浏览器将所有请求头包含在传递给captureSessionToken处理程序的请求详细信息对象中。如果省略该参数,则仅包含 URL 和一些常用请求头。
  • host_permissions": ["*://*.spotify.com/*"]manifest.json需要设置onBeforeSendHeaders才能使其正常工作。

所有提取的请求头都存储在background.js 文件headers中的变量中。令牌存储在变量中。请求头和令牌都不会持久化存储。authtoken

这些令牌作为函数参数传递给 WASM:

#[wasm_bindgen]
pub async fn add_random_tracks(
    auth_header_value: &str,
    token_header_value: &str,
    playlist_id: &str,
    user_uri: &str,
)
Enter fullscreen mode Exit fullscreen mode

调试

在 Firefox 和 Chrome 中,可以通过源代码加载扩展程序进行测试和调试。

火狐浏览器

  1. 运行. build.sh以构建 WASM 代码
  2. 前往about:debugging#/runtime/this-firefox
  3. 点击“加载临时插件”按钮
  4. Firefox 会弹出文件选择器窗口,要求选择manifest.json文件。请记住将manifest_ff.json重命名为manifest.json

如果扩展程序已正确加载,其详细信息应显示在同一页面的“临时扩展程序”部分中。

如果你总是找不到这个晦涩难懂的about:debugging#/runtime/this-firefox URL,还有另一种方法可以访问 Firefox 中的调试页面:

  • 点击工具栏中的扩展程序图标。
  • 点击“管理扩展程序”
  • 点击页面右上角的齿轮状设置图标
  • 点击“调试插件”

铬合金

Chrome 的流程与 Firefox 的流程非常相似。

  1. 运行. build.sh以构建 WASM 代码
  2. 前往chrome://extensions/
  3. 打开页面右上角的开发者模式开关。
  4. 点击“加载解压后的文件” ,然后选择包含manifest.json的文件夹。请记住将manifest_cr.json重命名为manifest.json

如果扩展程序加载成功,其详细信息应该会显示在同一页面的扩展程序列表中。更多信息请参阅Chrome 文档。

如果您忘记了直接 URL(chrome://extensions/),还有另一种方法可以访问 Chrome 中的调试页面:

  • 点击...Chrome 工具栏中的图标即可打开 Chrome 选项菜单
  • 点击“扩展程序/管理扩展程序”
  • Chrome 将打开 chrome://extensions/ 页面

进行更改并重新加载

对wasm_mod/src文件夹中的代码进行更改需要运行build.sh脚本来重新构建 WASM 代码。

对扩展程序文件夹内的 JS、CSS 或资源文件进行更改不需要重新构建,当您重新加载扩展程序时,浏览器会自动应用这些更改。

点击扩展程序的重新加载图标,加载最新更改。

例如,修改lib.rsprogress.js需要重新构建并重新加载。修改popup.js只需要重新加载。

查看日志

不同的模块会将日志消息记录到不同的控制台。如果您看不到所需的日志消息,则可能是您查看的控制台不正确。

内容脚本会将日志消息(例如通过 `<script> ` 标签console.log())输出到与网页相同的日志中。您可以在网页的开发者工具/控制台选项卡中查看这些消息。

后台脚本(包括 WASM)会将消息发送到单独的控制台日志。

  • 在 Chrome 浏览器中:点击扩展程序详细信息面板中的“检查视图”链接(例如“检查视图服务工作线程(已停用) ”)。

Chrome开发者工具

  • 在 Firefox 中:点击扩展程序详细信息面板中的“检查”按钮

Firefox 开发者工具

如果按照上述步骤操作,Firefox 应该会打开一个新的开发者工具窗口,其中包含控制台、网络和其他选项卡。

Firefox 中的弹出窗口(例如popup.html)会将消息记录到与后台脚本相同的控制台中。

Chrome 中的弹出窗口也会登录到与后台脚本相同的控制台,但您必须通过单独的步骤启用它。

右键单击打开的弹出窗口,然后选择“检查”。这将打开一个新的开发者工具窗口,其中包含后台日志和弹出窗口日志。关闭弹出窗口也会关闭开发者工具窗口。

弹出式开发者工具

background.js和 WASM的网络请求和响应会显示在后台 DevTools 窗口中。

跨浏览器兼容性

大部分扩展代码在 Firefox 和 Chrome 中都能运行。只有少数细微差别需要分开处理:清单文件全局上下文主机权限

manifest.json

background.js以不同的清单属性名称存在:

  • Firefox:背景/脚本
  • Chrome:background/service_worker

Firefox 会抱怨minimum_chrome_version,offline_enabledshow_matches

Chrome拒绝browser_specific_settingsFirefox所需的功能。

使用以下 CLI 命令查看 Firefox 和 Chrome 之间清单文件差异的完整列表:

git diff --no-index --word-diff extension/manifest_cr.json extension/manifest_ff.json
Enter fullscreen mode Exit fullscreen mode

此项目配置了两个清单文件,分别位于两个单独的文件中:manifest_cr.jsonmanifest_ff.json。为了进行本地调试,请手动将其中一个重命名为manifest.json,完成后再恢复为浏览器特定的文件。build.sh脚本会在打包过程中动态地将它们重命名为manifest.json 。

如果一个软件包没有manifest.json 文件,浏览器将无法将其识别为扩展程序。

全球背景

Firefox 和 Chrome 使用不同的全局上下文类名来访问方法,例如fetch()

  • WorkerGlobalScope
  • Window

这两个类都是标准化 WebAPI 的一部分。区别在于它们在内容脚本和后台脚本中的使用方式。

铬合金

  • web_sys::window()该函数对内容脚本返回Some(Window) ,对 Service Workers返回None。
  • js_sys::global().dyn_into::<WorkerGlobalScope>()对于 Service Worker,返回Ok(WorkerGlobalScope) ;对于内容脚本,返回 Err。

火狐浏览器

  • web_sys::window()该函数对上下文和 Service Worker 脚本均返回Some(Window)。
  • js_sys::global().dyn_into::<WorkerGlobalScope>()始终返回None

看起来 Firefox 不符合标准,甚至不符合 MDN 的规定,但也许是我遗漏了什么,导致这部分理解有误。

请参阅lib.rsget_runtime()中的函数以了解更多实现细节。另请参阅MDN 上的WindowWorkerGlobalScope文档。

主机权限

Chrome 和 Firefox 的清单文件中都有这条记录:

"host_permissions": [
    "*://*.spotify.com/*"
]
Enter fullscreen mode Exit fullscreen mode

格式相同,但 Chrome 会在安装时授予此权限,而 Firefox 则将其视为运行时必须请求的可选权限。

扩展代码能够优雅地处理这种差异,但代价是增加了一些复杂性。更多信息:

在 Google 和 Mozilla 插件商店中列出 WASM 扩展程序

包含 WASM 代码的扩展程序没有特殊的上市要求。

Google 和 Mozilla 都可能要求提供 WASM 部分的源代码和构建说明,这可能会延迟审核流程。此扩展程序完全开源,并且通常在 24 小时内获得批准。

包装

build.sh脚本会将扩展程序打包成chrome.zipfirefox.zip文件。打包步骤包括:

  • 使用wasm-pack构建 WASM 代码
  • 删除不必要的文件
  • 将浏览器特定的清单文件重命名为manifest.json
  • 将扩展程序文件夹压缩成chrome.zipfirefox.zip,并附上正确的清单文件。

wasm-pack会将wasm_mod/src文件夹中的 .js 文件复制extension/js/wasm/snippets/wasm_mod/src 文件夹中(如果这些文件用于绑定到 Rust)。例如,wasm_mod/src/progress.js会被复制到extension/js/wasm/snippets/wasm_mod-bc4ca452742d1bb1/src/progress.js 文件夹中

有关更多信息,请参阅FirefoxChrome的详细包装说明。

列表

列表表单中没有针对 WASM 的特定问题或选项。请确保清单中没有任何未使用的权限,并清楚地解释您请求的权限的原因。

Chrome 权限

实用链接

火狐浏览器

铬合金

上市流程

  • 注册、创建新列表、描述扩展程序并上传.zip文件以供审核。
  • 上传.zip文件后,请检查商店网站生成的任何错误和警告。
  • 根据我的个人经验,两家商店的审批时间大约都是 24 小时。
  • 任何代码或商品信息的更改都需要经过店铺审核才能发布。

这两个商店都允许添加商品扩展程序,但这些扩展程序不会出现在公共目录中,而拥有扩展程序 URL 的用户仍然可以访问它们。

文章来源:https://dev.to/rimutaka/chrome-extension-with-rust-and-wasm-by-example-5cbh