WASM 开发指南:Rust 与 JavaScript
1. WASM 是什么
WASM,全称 WebAssembly,是一种可以在浏览器、Node.js、边缘运行时等环境中执行的二进制指令格式。
它本身不是一种业务开发语言,而是一种编译目标。常见用法是:
- 用 Rust、C、C++、AssemblyScript 等语言编写高性能模块。
- 编译成
.wasm文件。 - 由 JavaScript 加载
.wasm,再在业务项目中调用导出的函数。
在前端工程里,WASM 通常不直接替代 JavaScript,而是承担计算密集型、可复用、跨语言的模块能力。
常见适用场景:
- 图片、音视频、压缩、加密、哈希等计算密集任务。
- 游戏引擎、物理引擎、地图渲染、CAD、图形处理。
- 将已有 Rust/C/C++ 算法复用到 Web 项目。
- 在浏览器中运行接近原生性能的核心逻辑。
不适合优先使用 WASM 的场景:
- 普通 DOM 操作。
- 简单表单、列表、请求封装。
- 大量依赖浏览器 API 的业务逻辑。
- 本身性能瓶颈不在计算层的页面。
2. Rust 开发 WASM
Rust 是目前开发 WASM 最常见、工程体验较成熟的语言之一。通常会配合 wasm-bindgen 和 wasm-pack 使用。
2.1 推荐工具链
需要安装:
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
常用依赖:
[dependencies]
wasm-bindgen = "0.2"
wasm-bindgen 的作用是让 Rust 函数、结构体、类型可以更方便地暴露给 JavaScript。
wasm-pack 的作用是把 Rust crate 编译成可以被 JavaScript 项目消费的 npm 包结构。
2.2 目录结构
一个典型 Rust WASM 项目结构如下:
my-wasm-lib/
├── Cargo.toml
├── src/
│ ├── lib.rs
│ └── utils.rs
├── tests/
│ └── web.rs
├── pkg/
└── README.md
各目录与文件作用:
| 路径 | 作用 |
|---|---|
Cargo.toml | Rust 项目配置文件,声明包名、版本、依赖、crate 类型。 |
src/lib.rs | WASM 模块入口。需要暴露给 JS 的函数通常写在这里,或从这里导出。 |
src/utils.rs | 普通 Rust 模块,用于拆分内部逻辑。是否存在取决于项目复杂度。 |
tests/ | 测试目录,可以放 Rust 测试或浏览器环境测试。 |
pkg/ | wasm-pack build 后生成的打包产物目录。不要手写。 |
README.md | 给当前 WASM 包使用者看的说明文档。 |
最小 Cargo.toml 示例:
[package]
name = "my-wasm-lib"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
关键点:
crate-type = ["cdylib", "rlib"]表示这个 crate 可以被编译成动态库形式,供 WASM 使用。cdylib是生成.wasm所需要的关键配置。rlib方便 Rust 内部测试或被其他 Rust crate 复用。
2.3 Rust 代码示例
src/lib.rs:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
说明:
#[wasm_bindgen]表示这个函数需要暴露给 JavaScript。- 基础数值类型可以直接传递。
- 字符串、数组、对象等复杂类型会通过
wasm-bindgen生成的 JS 胶水代码处理。
2.4 打包命令
面向浏览器或现代前端构建工具:
wasm-pack build --target bundler
面向直接在浏览器中通过 ES Module 使用:
wasm-pack build --target web
面向 Node.js:
wasm-pack build --target nodejs
常见 target 区别:
| target | 适用场景 | 说明 |
|---|---|---|
bundler | Vite、Webpack、Rollup 等工程 | 交给打包器处理 .wasm 加载。前端项目最常用。 |
web | 原生浏览器 ES Module | 可以直接在浏览器中 import init from "./pkg/xxx.js"。 |
nodejs | Node.js 项目 | 生成适合 Node.js require 或 import 使用的产物。 |
no-modules | 老浏览器或非模块脚本 | 较少使用。 |
2.5 Rust WASM 打包产物
执行:
wasm-pack build --target bundler
会生成类似目录:
pkg/
├── my_wasm_lib_bg.wasm
├── my_wasm_lib.js
├── my_wasm_lib.d.ts
├── package.json
└── README.md
各产物作用:
| 文件 | 作用 |
|---|---|
my_wasm_lib_bg.wasm | 核心 WASM 二进制文件,真正执行 Rust 编译后的逻辑。 |
my_wasm_lib.js | JS 胶水代码,负责加载 .wasm、做类型转换、暴露 JS 可调用 API。 |
my_wasm_lib.d.ts | TypeScript 类型声明,给 TS 项目提供类型提示。 |
package.json | npm 包描述文件,声明入口、类型文件、包名、版本等。 |
README.md | 从项目 README 复制或生成的说明文档。 |
重点理解:
.wasm是核心逻辑。.js是连接 JavaScript 和 WASM 的桥。.d.ts是 TypeScript 类型提示。package.json让pkg/可以像一个 npm 包一样被其他项目安装或引用。
3. JavaScript 使用 WASM
JavaScript 使用 WASM 有两种主流方式:
- 直接加载
.wasm文件。 - 使用 Rust WASM 打包出来的 npm 包。
实际工程中,更推荐第二种,因为 wasm-pack 已经帮你处理了加载、初始化、类型转换和包结构。
3.1 方式一:直接加载 .wasm
目录结构示例:
web-app/
├── index.html
├── src/
│ └── main.js
└── public/
└── add.wasm
各目录与文件作用:
| 路径 | 作用 |
|---|---|
index.html | 页面入口。 |
src/main.js | JS 业务入口,负责加载并调用 WASM。 |
public/add.wasm | WASM 二进制文件,通常会被静态服务原样输出。 |
示例代码:
const response = await fetch("/add.wasm");
const bytes = await response.arrayBuffer();
const result = await WebAssembly.instantiate(bytes);
const { add } = result.instance.exports;
console.log(add(1, 2));
这种方式的特点:
- 优点是直接、依赖少。
- 缺点是需要自己处理加载、初始化、内存、字符串、复杂类型转换。
- 更适合非常简单的 WASM 模块,或学习 WASM 底层机制。
如果 WASM 导出的函数只处理整数、浮点数,这种方式还能接受;如果需要传字符串、数组、对象,手动处理会很麻烦。
3.2 方式二:使用 Rust WASM 的 pkg 包
Rust WASM 项目打包后会生成 pkg/ 目录。这个目录可以被其他项目当作 npm 包使用。
常见目录关系:
workspace/
├── my-wasm-lib/
│ ├── Cargo.toml
│ ├── src/
│ └── pkg/
└── web-app/
├── package.json
└── src/
└── main.js
my-wasm-lib/pkg 是产物包。
web-app 是使用 WASM 的业务项目。
3.3 在 Vite 项目中使用
在业务项目中安装本地 WASM 包:
npm install ../my-wasm-lib/pkg
或使用 pnpm:
pnpm add ../my-wasm-lib/pkg
在代码中使用:
import init, { add, greet } from "my-wasm-lib";
await init();
console.log(add(1, 2));
console.log(greet("WASM"));
说明:
init()用于初始化 WASM 模块。- 初始化完成后,才能稳定调用
add、greet等导出函数。 - 包名通常来自
pkg/package.json中的name字段。
如果使用 TypeScript:
import init, { add, greet } from "my-wasm-lib";
async function main() {
await init();
const value: number = add(1, 2);
const message: string = greet("WASM");
console.log(value, message);
}
main();
3.4 在 Webpack 项目中使用
Webpack 5 对 WASM 支持较好,但通常需要开启异步 WASM 支持。
示例配置:
module.exports = {
experiments: {
asyncWebAssembly: true,
},
};
使用方式与 Vite 类似:
import init, { add } from "my-wasm-lib";
async function bootstrap() {
await init();
console.log(add(1, 2));
}
bootstrap();
如果项目打包时报 .wasm 解析错误,需要检查:
- Webpack 版本是否为 5。
- 是否开启
experiments.asyncWebAssembly。 - WASM 包是否使用了适合 bundler 的构建方式。
推荐 Rust 包使用:
wasm-pack build --target bundler
3.5 在 Node.js 中使用
Rust WASM 包面向 Node.js 构建:
wasm-pack build --target nodejs
在 Node.js 项目中安装:
npm install ../my-wasm-lib/pkg
使用:
const wasm = require("my-wasm-lib");
console.log(wasm.add(1, 2));
如果项目使用 ESM:
import { add } from "my-wasm-lib";
console.log(add(1, 2));
Node.js target 的特点:
- 通常不需要手动
await init()。 - 产物加载方式更贴近 Node.js 模块系统。
- 不适合直接拿到浏览器项目中使用。
4. 打包产物如何作用在别的项目
可以把 Rust WASM 的 pkg/ 理解成一个已经准备好的 npm 包。
它对其他项目提供三层能力:
业务项目
↓ import
pkg/my_wasm_lib.js
↓ 加载和初始化
pkg/my_wasm_lib_bg.wasm
↓ 执行
Rust 编译后的核心逻辑
4.1 本地路径安装
适合开发阶段。
npm install ../my-wasm-lib/pkg
优点:
- 简单直接。
- 不需要发布 npm。
- 适合本地联调。
缺点:
- 路径依赖不适合长期跨仓库协作。
- CI 环境需要保证路径存在。
4.2 npm link
适合频繁本地调试。
在 WASM 产物目录:
cd my-wasm-lib/pkg
npm link
在业务项目:
npm link my-wasm-lib
优点:
- 修改 WASM 包后可以快速联调。
- 适合本机多项目开发。
缺点:
- link 状态只存在本机。
- 团队协作和 CI 不应依赖
npm link。
4.3 发布到 npm 私有源或公共源
适合正式复用。
cd my-wasm-lib/pkg
npm publish
业务项目安装:
npm install my-wasm-lib
优点:
- 版本清晰。
- CI/CD 友好。
- 多项目复用方便。
缺点:
- 需要维护版本号。
- 需要考虑 npm registry 权限。
4.4 monorepo workspace 引用
适合同一个仓库中同时维护 WASM 包和业务项目。
目录结构:
repo/
├── packages/
│ └── my-wasm-lib/
│ ├── Cargo.toml
│ ├── src/
│ └── pkg/
├── apps/
│ └── web-app/
│ ├── package.json
│ └── src/
└── package.json
根 package.json:
{
"workspaces": [
"packages/my-wasm-lib/pkg",
"apps/web-app"
]
}
业务项目中安装:
npm install my-wasm-lib --workspace apps/web-app
这种方式适合长期维护,但需要把 Rust 构建流程接入 monorepo 的构建脚本。
5. JavaScript 侧目录结构建议
如果业务项目只是消费 WASM 包,推荐结构:
web-app/
├── package.json
├── vite.config.js
├── src/
│ ├── main.js
│ ├── wasm/
│ │ └── index.js
│ └── features/
│ └── image-process.js
└── public/
各目录作用:
| 路径 | 作用 |
|---|---|
src/main.js | 应用入口。不要在这里堆大量 WASM 业务逻辑。 |
src/wasm/index.js | WASM 初始化和导出封装层。建议统一管理。 |
src/features/ | 具体业务功能。通过 src/wasm/index.js 调用 WASM。 |
public/ | 放静态资源。如果直接加载 .wasm,可以放这里。 |
推荐封装方式:
import init, { add, greet } from "my-wasm-lib";
let initialized = false;
export async function initWasm() {
if (initialized) {
return;
}
await init();
initialized = true;
}
export async function wasmAdd(a, b) {
await initWasm();
return add(a, b);
}
export async function wasmGreet(name) {
await initWasm();
return greet(name);
}
业务代码:
import { wasmAdd, wasmGreet } from "../wasm";
const value = await wasmAdd(1, 2);
const message = await wasmGreet("WASM");
console.log(value, message);
这样做的好处:
- 业务代码不需要关心 WASM 初始化细节。
- 后续替换 WASM 包或调整加载方式时影响范围小。
- 可以避免多个地方重复调用初始化逻辑。
6. Rust WASM 与 JavaScript 使用方式结合
| 维度 | Rust 开发 WASM | JavaScript 使用 WASM |
|---|---|---|
| 主要职责 | 编写高性能核心逻辑,并编译成 .wasm。 | 加载、初始化、调用 WASM,并接入业务流程。 |
| 核心目录 | src/、Cargo.toml、pkg/。 | src/wasm/、业务模块、构建配置。 |
| 核心产物 | .wasm、JS 胶水代码、.d.ts、package.json。 | 最终业务 bundle,包含或引用 WASM 产物。 |
| 常用工具 | Rust、Cargo、wasm-bindgen、wasm-pack。 | Vite、Webpack、Node.js、npm/pnpm。 |
| 关注点 | 类型边界、内存、性能、导出 API。 | 初始化时机、异步加载、打包配置、业务封装。 |
7. 推荐工程实践
7.1 WASM API 设计要简单
不要把大量复杂对象直接跨 JS 和 WASM 传来传去。
推荐:
- 传数字、布尔值、字符串等简单类型。
- 大数组用
Uint8Array、Float32Array等 TypedArray。 - 复杂业务对象尽量在 JS 侧组织,WASM 只处理核心计算。
避免:
- 频繁跨边界调用小函数。
- 把 UI 状态塞进 WASM。
- 让 WASM 直接管理 DOM。
7.2 初始化集中管理
不要在多个业务文件里到处写:
await init();
更推荐建立 src/wasm/index.js:
import init from "my-wasm-lib";
let initPromise = null;
export function ensureWasmReady() {
if (!initPromise) {
initPromise = init();
}
return initPromise;
}
业务模块需要调用 WASM 前统一:
import { ensureWasmReady } from "./wasm";
await ensureWasmReady();
7.3 区分开发产物和发布产物
Rust 项目源码目录:
my-wasm-lib/
├── Cargo.toml
├── src/
└── pkg/
真正给其他项目用的是:
my-wasm-lib/pkg/
不要让业务项目直接依赖 Rust 的 src/。
业务项目应该依赖 pkg/、npm 包或 workspace 包。
7.4 构建脚本建议
Rust WASM 项目可以在 package.json 中加脚本:
{
"scripts": {
"build:wasm": "wasm-pack build --target bundler",
"build:wasm:web": "wasm-pack build --target web",
"build:wasm:node": "wasm-pack build --target nodejs"
}
}
如果没有 Node.js 包管理需求,也可以只在文档中约定命令:
wasm-pack build --target bundler
保持简单即可,不要为了一个小 WASM 模块引入复杂构建系统。
8. 常见问题
8.1 为什么有 .wasm 还会有 .js?
因为 JavaScript 不能直接像普通函数一样调用 .wasm 文件。
.js 胶水代码负责:
- 加载
.wasm。 - 初始化 WASM 实例。
- 处理字符串、数组、内存等类型转换。
- 把 Rust 导出的函数包装成 JS 可以调用的函数。
8.2 为什么调用前要 await init()?
浏览器加载 WASM 是异步过程。
await init() 确保:
.wasm文件已经加载完成。- WASM 实例已经初始化。
- 导出函数已经可以被调用。
如果没有初始化就调用导出函数,可能会出现运行时报错。
8.3 为什么字符串传递比数字复杂?
WASM 的底层内存接近线性内存模型。
数字可以直接传递,但字符串需要:
- 编码成 UTF-8。
- 写入 WASM 内存。
- 把指针和长度传给 WASM。
- 从 WASM 内存读回结果。
wasm-bindgen 生成的 JS 胶水代码会帮你处理这些细节。
8.4 为什么不直接全部用 WASM 写?
WASM 适合计算,不适合直接管理 Web UI。
JavaScript 仍然更适合:
- DOM 操作。
- 网络请求。
- 前端状态管理。
- 与浏览器 API 交互。
- 与现有前端生态集成。
更合理的分工是:
- Rust/WASM:核心计算、算法、性能敏感逻辑。
- JavaScript:业务编排、UI、数据请求、工程集成。
8.5 打包后业务项目找不到 .wasm 怎么办?
优先检查:
- Rust 包是否使用了正确 target,例如前端项目使用
--target bundler。 - 业务项目构建工具是否支持 WASM。
.wasm文件是否被发布或复制到了 npm 包中。- import 的包名是否和
pkg/package.json中的name一致。 - 部署服务器是否正确返回
.wasm文件。
服务器最好返回正确 MIME:
application/wasm
大多数现代静态服务和托管平台已经支持,但旧服务可能需要手动配置。
9. 最小完整流程
9.1 创建 Rust WASM 包
cargo new my-wasm-lib --lib
cd my-wasm-lib
cargo add wasm-bindgen
修改 Cargo.toml:
[package]
name = "my-wasm-lib"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
编写 src/lib.rs:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
构建:
wasm-pack build --target bundler
得到:
pkg/
├── my_wasm_lib_bg.wasm
├── my_wasm_lib.js
├── my_wasm_lib.d.ts
└── package.json
9.2 在 Vite 项目中使用
npm create vite@latest web-app
cd web-app
npm install
npm install ../my-wasm-lib/pkg
src/main.js:
import init, { add } from "my-wasm-lib";
async function main() {
await init();
console.log(add(1, 2));
}
main();
运行:
npm run dev
浏览器控制台应该输出:
3
10. 总结
Rust 负责把高性能逻辑编译成 WASM,JavaScript 负责加载 WASM 并接入业务项目。
最重要的工程边界是:
- Rust 项目的源码在
src/。 - Rust WASM 的可分发产物在
pkg/。 - 其他项目应该消费
pkg/或发布后的 npm 包。 - JavaScript 项目最好用一个
src/wasm/封装层统一管理初始化和调用。
如果只是学习 WASM,可以直接加载 .wasm。
如果是工程项目,推荐使用:
wasm-pack build --target bundler
然后在业务项目中:
npm install ../my-wasm-lib/pkg
再通过:
import init, { add } from "my-wasm-lib";
await init();
console.log(add(1, 2));
完成接入。