WASM 开发指南:Rust 与 JavaScript

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-bindgenwasm-pack 使用。

2.1 推荐工具链

需要安装:

sql 复制代码
rustup target add wasm32-unknown-unknown
cargo install wasm-pack

常用依赖:

ini 复制代码
[dependencies]
wasm-bindgen = "0.2"

wasm-bindgen 的作用是让 Rust 函数、结构体、类型可以更方便地暴露给 JavaScript。

wasm-pack 的作用是把 Rust crate 编译成可以被 JavaScript 项目消费的 npm 包结构。

2.2 目录结构

一个典型 Rust WASM 项目结构如下:

vbnet 复制代码
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 示例:

ini 复制代码
[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

rust 复制代码
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 打包命令

面向浏览器或现代前端构建工具:

css 复制代码
wasm-pack build --target bundler

面向直接在浏览器中通过 ES Module 使用:

css 复制代码
wasm-pack build --target web

面向 Node.js:

css 复制代码
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 打包产物

执行:

css 复制代码
wasm-pack build --target bundler

会生成类似目录:

go 复制代码
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.jsonpkg/ 可以像一个 npm 包一样被其他项目安装或引用。

3. JavaScript 使用 WASM

JavaScript 使用 WASM 有两种主流方式:

  1. 直接加载 .wasm 文件。
  2. 使用 Rust WASM 打包出来的 npm 包。

实际工程中,更推荐第二种,因为 wasm-pack 已经帮你处理了加载、初始化、类型转换和包结构。

3.1 方式一:直接加载 .wasm

目录结构示例:

csharp 复制代码
web-app/
├── index.html
├── src/
│   └── main.js
└── public/
    └── add.wasm

各目录与文件作用:

路径 作用
index.html 页面入口。
src/main.js JS 业务入口,负责加载并调用 WASM。
public/add.wasm WASM 二进制文件,通常会被静态服务原样输出。

示例代码:

ini 复制代码
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 包使用。

常见目录关系:

css 复制代码
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 包:

bash 复制代码
npm install ../my-wasm-lib/pkg

或使用 pnpm:

bash 复制代码
pnpm add ../my-wasm-lib/pkg

在代码中使用:

csharp 复制代码
import init, { add, greet } from "my-wasm-lib";

await init();

console.log(add(1, 2));
console.log(greet("WASM"));

说明:

  • init() 用于初始化 WASM 模块。
  • 初始化完成后,才能稳定调用 addgreet 等导出函数。
  • 包名通常来自 pkg/package.json 中的 name 字段。

如果使用 TypeScript:

csharp 复制代码
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 支持。

示例配置:

ini 复制代码
module.exports = {
  experiments: {
    asyncWebAssembly: true,
  },
};

使用方式与 Vite 类似:

csharp 复制代码
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 包使用:

css 复制代码
wasm-pack build --target bundler

3.5 在 Node.js 中使用

Rust WASM 包面向 Node.js 构建:

css 复制代码
wasm-pack build --target nodejs

在 Node.js 项目中安装:

bash 复制代码
npm install ../my-wasm-lib/pkg

使用:

ini 复制代码
const wasm = require("my-wasm-lib");

console.log(wasm.add(1, 2));

如果项目使用 ESM:

sql 复制代码
import { add } from "my-wasm-lib";

console.log(add(1, 2));

Node.js target 的特点:

  • 通常不需要手动 await init()
  • 产物加载方式更贴近 Node.js 模块系统。
  • 不适合直接拿到浏览器项目中使用。

4. 打包产物如何作用在别的项目

可以把 Rust WASM 的 pkg/ 理解成一个已经准备好的 npm 包。

它对其他项目提供三层能力:

bash 复制代码
业务项目
  ↓ import
pkg/my_wasm_lib.js
  ↓ 加载和初始化
pkg/my_wasm_lib_bg.wasm
  ↓ 执行
Rust 编译后的核心逻辑

4.1 本地路径安装

适合开发阶段。

bash 复制代码
npm install ../my-wasm-lib/pkg

优点:

  • 简单直接。
  • 不需要发布 npm。
  • 适合本地联调。

缺点:

  • 路径依赖不适合长期跨仓库协作。
  • CI 环境需要保证路径存在。

适合频繁本地调试。

在 WASM 产物目录:

bash 复制代码
cd my-wasm-lib/pkg
npm link

在业务项目:

perl 复制代码
npm link my-wasm-lib

优点:

  • 修改 WASM 包后可以快速联调。
  • 适合本机多项目开发。

缺点:

  • link 状态只存在本机。
  • 团队协作和 CI 不应依赖 npm link

4.3 发布到 npm 私有源或公共源

适合正式复用。

bash 复制代码
cd my-wasm-lib/pkg
npm publish

业务项目安装:

perl 复制代码
npm install my-wasm-lib

优点:

  • 版本清晰。
  • CI/CD 友好。
  • 多项目复用方便。

缺点:

  • 需要维护版本号。
  • 需要考虑 npm registry 权限。

4.4 monorepo workspace 引用

适合同一个仓库中同时维护 WASM 包和业务项目。

目录结构:

css 复制代码
repo/
├── packages/
│   └── my-wasm-lib/
│       ├── Cargo.toml
│       ├── src/
│       └── pkg/
├── apps/
│   └── web-app/
│       ├── package.json
│       └── src/
└── package.json

package.json

json 复制代码
{
  "workspaces": [
    "packages/my-wasm-lib/pkg",
    "apps/web-app"
  ]
}

业务项目中安装:

bash 复制代码
npm install my-wasm-lib --workspace apps/web-app

这种方式适合长期维护,但需要把 Rust 构建流程接入 monorepo 的构建脚本。

5. JavaScript 侧目录结构建议

如果业务项目只是消费 WASM 包,推荐结构:

arduino 复制代码
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,可以放这里。

推荐封装方式:

javascript 复制代码
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);
}

业务代码:

ini 复制代码
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.tomlpkg/ src/wasm/、业务模块、构建配置。
核心产物 .wasm、JS 胶水代码、.d.tspackage.json 最终业务 bundle,包含或引用 WASM 产物。
常用工具 Rust、Cargo、wasm-bindgen、wasm-pack。 Vite、Webpack、Node.js、npm/pnpm。
关注点 类型边界、内存、性能、导出 API。 初始化时机、异步加载、打包配置、业务封装。

7. 推荐工程实践

7.1 WASM API 设计要简单

不要把大量复杂对象直接跨 JS 和 WASM 传来传去。

推荐:

  • 传数字、布尔值、字符串等简单类型。
  • 大数组用 Uint8ArrayFloat32Array 等 TypedArray。
  • 复杂业务对象尽量在 JS 侧组织,WASM 只处理核心计算。

避免:

  • 频繁跨边界调用小函数。
  • 把 UI 状态塞进 WASM。
  • 让 WASM 直接管理 DOM。

7.2 初始化集中管理

不要在多个业务文件里到处写:

swift 复制代码
await init();

更推荐建立 src/wasm/index.js

csharp 复制代码
import init from "my-wasm-lib";

let initPromise = null;

export function ensureWasmReady() {
  if (!initPromise) {
    initPromise = init();
  }

  return initPromise;
}

业务模块需要调用 WASM 前统一:

javascript 复制代码
import { ensureWasmReady } from "./wasm";

await ensureWasmReady();

7.3 区分开发产物和发布产物

Rust 项目源码目录:

css 复制代码
my-wasm-lib/
├── Cargo.toml
├── src/
└── pkg/

真正给其他项目用的是:

bash 复制代码
my-wasm-lib/pkg/

不要让业务项目直接依赖 Rust 的 src/

业务项目应该依赖 pkg/、npm 包或 workspace 包。

7.4 构建脚本建议

Rust WASM 项目可以在 package.json 中加脚本:

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 包管理需求,也可以只在文档中约定命令:

css 复制代码
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:

bash 复制代码
application/wasm

大多数现代静态服务和托管平台已经支持,但旧服务可能需要手动配置。

9. 最小完整流程

9.1 创建 Rust WASM 包

vbnet 复制代码
cargo new my-wasm-lib --lib
cd my-wasm-lib
cargo add wasm-bindgen

修改 Cargo.toml

ini 复制代码
[package]
name = "my-wasm-lib"
version = "0.1.0"
edition = "2021"

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

[dependencies]
wasm-bindgen = "0.2"

编写 src/lib.rs

rust 复制代码
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

构建:

css 复制代码
wasm-pack build --target bundler

得到:

go 复制代码
pkg/
├── my_wasm_lib_bg.wasm
├── my_wasm_lib.js
├── my_wasm_lib.d.ts
└── package.json

9.2 在 Vite 项目中使用

bash 复制代码
npm create vite@latest web-app
cd web-app
npm install
npm install ../my-wasm-lib/pkg

src/main.js

csharp 复制代码
import init, { add } from "my-wasm-lib";

async function main() {
  await init();
  console.log(add(1, 2));
}

main();

运行:

arduino 复制代码
npm run dev

浏览器控制台应该输出:

复制代码
3

10. 总结

Rust 负责把高性能逻辑编译成 WASM,JavaScript 负责加载 WASM 并接入业务项目。

最重要的工程边界是:

  • Rust 项目的源码在 src/
  • Rust WASM 的可分发产物在 pkg/
  • 其他项目应该消费 pkg/ 或发布后的 npm 包。
  • JavaScript 项目最好用一个 src/wasm/ 封装层统一管理初始化和调用。

如果只是学习 WASM,可以直接加载 .wasm

如果是工程项目,推荐使用:

css 复制代码
wasm-pack build --target bundler

然后在业务项目中:

bash 复制代码
npm install ../my-wasm-lib/pkg

再通过:

csharp 复制代码
import init, { add } from "my-wasm-lib";

await init();
console.log(add(1, 2));

完成接入。

相关推荐
comerzhang65512 天前
Web 性能的架构边界:跨线程信令通道的确定性分析
javascript·webassembly
李剑一1 个月前
前端必懂!一文搞懂 WebAssembly:Web/Electron/RN 全通用,你天天用的软件,底层都靠它
前端·webassembly
七夜zippoe1 个月前
WebAssembly与Python:在浏览器中运行Python
开发语言·python·wasm·webassembly·pyscript
码路飞1 个月前
不会 Rust 也能玩 WebAssembly:3 个 npm install 就能用的 WASM 神器
前端·javascript·webassembly
雅乐橙1 个月前
WebAssembly 代码保护实战:Seed 芥子安装与使用完全指南
webassembly
bluceli2 个月前
WebAssembly实战指南:将高性能计算带入浏览器
前端·webassembly
BigByte2 个月前
我用 6 个 WASM 编码器干掉了 Canvas.toBlob(),图片压缩率直接提升 15%
性能优化·webassembly·图片资源
Tlink2 个月前
WebAssembly:十年磨一剑,这些实践案例让我看到了它的真面目
webassembly·webassembly实践