1. 先确定:你的库可能已经能跑在 Wasm 上!
在做任何改造前,先来一次“体检”:
rustup target add wasm32-unknown-unknown # 只需一次
cargo check --target wasm32-unknown-unknown
如果 编译直接通过,恭喜!
说明你的 crate 没有文件 I/O、线程、系统/C ABI 依赖,也不会同步阻塞 —— 几乎可以直接发布带 Wasm 支持的版本。
若出现编译错误,接下来就针对常见“拦路虎”逐个解决。
2. 重构要点:避开 Wasm 的“红线”
场景 | 原因 | 迁移策略 |
---|---|---|
文件系统 I/O | 浏览器沙盒无真实 FS | 把读写逻辑“外提”: 由使用者读文件后把 &[u8] / &str 传进来 |
同步阻塞 I/O | Web 只有异步 fetch 、事件循环 | 使用 Future / async + wasm-bindgen-futures ,或定义 trait 让调用方注入 async read/write |
线程 / Rayon | std::thread::spawn 会 panic | 用 cfg(target_arch) 区分实现;或暴露“任务接口”,让上层自行并行化 |
C / 系统库绑定 | 浏览器无 libc /动态链接 | 关闭 *-sys 默认特性;或用纯 Rust 实现/JS API 替代 |
2.1 示例:将同步文件读取改写为“纯数据解析”
// ❌ 原实现:直接读取文件
pub fn parse_thing(path: &Path) -> Result<Thing, Error> {let bytes = std::fs::read(path)?;parse_thing_from_slice(&bytes)
}// ✅ 改进:只负责解析
pub fn parse_thing_from_slice(bytes: &[u8]) -> Result<Thing, Error> {// ...
}
这样一来,在浏览器侧可以用 fetch
获取文件后把 Uint8Array
交给 Wasm;在原生侧仍可照常 fs::read()
。
2.2 示例:平台差异用 cfg
隔离
#[cfg(target_arch = "wasm32")]
fn do_work() {// 单线程逻辑
}#[cfg(not(target_arch = "wasm32"))]
fn do_work() {std::thread::spawn(|| heavy_compute());
}
3. 引入 wasm-bindgen
& 生态依赖(仅 Wasm 目标启用)
在 Cargo.toml
中加一段 按目标架构生效 的依赖:
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3" # JS 原生对象
web-sys = { version = "0.3", features = ["Window", "Response"] }
wasm-bindgen
:在 Rust ↔ JS 之间导入/导出函数、结构体。js-sys
/web-sys
:绑定所有 ECMAScript & Web API(DOM/Fetch 等)。- 仅在
wasm32
目标启用,不会污染其它平台的依赖树。
4. 让异步真正异步:futures
+ wasm-bindgen-futures
如果你的库 必须 自己发起 HTTP 请求/数据库调用,浏览器里只能走 异步。做法:
- 对外暴露
async fn
/ 返回impl Future<Output=T>
- 依赖注入:让调用者提供具体 Future,或用 trait+泛型。
- 在 Wasm 目标用
wasm-bindgen-futures::JsFuture
把 JSPromise
转换为 RustFuture
。
// library.rs
pub async fn download_json<F>(fetch: F) -> Result<Data>
whereF: Future<Output = Result<Vec<u8>>>,
{let bytes = fetch.await?;parse_json(&bytes)
}
浏览器端示例(TS/JS):
import init, { download_json } from "my_crate";await init(); // 初始化 Wasm
const promise = fetch("/data.json").then(r => r.arrayBuffer()).then(buf => new Uint8Array(buf));
await download_json(promise); // 传入 Promise
5. 持续集成:保证以后也不会“突然炸锅”
5.1 CI 配置示例(GitHub Actions)
name: wasm-check
on: [push, pull_request]jobs:wasm:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- uses: actions-rs/toolchain@v1with:toolchain: stableoverride: truetargets: wasm32-unknown-unknown- run: cargo check --target wasm32-unknown-unknown --no-default-features
cargo check
比build
快得多,只检查能否通过编译。- 如需落地逻辑测试,可配合
wasm-bindgen-test
+wasm-pack test --node
或 headless Chrome。
5.2 在测试里覆盖 Wasm 目标
#[cfg(target_arch = "wasm32")]
use wasm_bindgen_test::wasm_bindgen_test as my_test;#[cfg(not(target_arch = "wasm32"))]
use std::prelude::v1::test as my_test;#[my_test]
fn roundtrip() {// 同一份断言,跑在两端
}
6. 发布:声明支持 & 文档提示
Cargo.toml
:无需额外字段,只要能编译即可。- README / docs.rs:注明“已通过
wasm32-unknown-unknown
目标测试”。 - 上传到 crates.io 后,
wasm-pack
用户可直接安装并在前端使用。
7. 小结 · 一条清晰路线
- 先用
cargo check --target wasm32
测一测:能编译就离成功不远。 - 把文件 I/O、线程、阻塞操作“外提”,让使用者注入或用异步替代。
- 按目标添加
wasm-bindgen
依赖,利用js-sys/web-sys
连接浏览器 API。 - 保持
async/await
泛化,跨平台共享同一接口。 - CI 持续检查 + (可选)
wasm-bindgen-test
真机跑用例。
只要遵循以上步骤,你的通用 Rust 库就能 零崩溃 地兼容 Wasm,进入浏览器和其他 Wasm 运行时的广阔天地。祝迁移顺利,欢迎把更多高质量 crate 带到 WebAssembly 生态!