Rust 与 WebAssembly 实战——将高性能 Rust 代码运行在浏览器与 Node.js
一、学习目标与重点
1.1 学习目标
- 理解 WebAssembly 基础:深入掌握 WebAssembly(Wasm/Wasmtime)的核心定义、运行机制、与 JavaScript 的性能对比。
- 掌握 Rust 到 Wasm 的编译:熟练使用 wasm-pack、cargo-web 等工具链,完成 Rust 代码到 Wasm 模块的编译、打包、优化。
- 精通 Rust 与 JavaScript 交互:实现双向交互(Rust 调用 JS 函数、JS 调用 Rust 函数),处理复杂数据类型(数组、对象、字符串),管理内存(Wasm 线性内存的分配与释放)。
- 开发真实 Wasm 应用:编写浏览器端高性能任务(Canvas 图像滤镜、WebGL 计算辅助)、Node.js 端计算密集型任务(图像处理、加密解密、数据压缩)。
- 优化 Wasm 模块:使用 wasm-opt 工具优化 Wasm 体积,学习代码分割、懒加载、模块缓存。
- 部署 Wasm 应用:部署到静态资源服务器、CDN,使用 Vite/Webpack 集成到 Vue/React 项目。
1.2 学习重点
💡 三大核心难点:
- Wasm 线性内存的管理:理解 Rust 的内存分配器(如 wasm-bindgen 的 __wbindgen_malloc/__wbindgen_free)如何与 Wasm 线性内存配合,避免内存泄漏。
- 复杂数据类型的转换:掌握 serde-wasm-bindgen、js-sys 等库如何将 Rust 的 Struct/Enum 与 JS 的 Object/Array/String 高效互转。
- 异步交互与 DOM 操作:使用 wasm-bindgen-futures、web-sys 等库实现异步任务(如 HTTP 请求)和 DOM 操作(如事件监听、元素创建)。
⚠️ 三大高频错误点:
- 未正确释放 Wasm 分配的内存:Rust 通过 wasm-bindgen 分配的内存(如 js_sys::ArrayBuffer、String::into_boxed_str)如果不手动或自动释放,会导致浏览器/Node.js 的内存泄漏。
- 数据类型转换时的边界检查:处理 Rust 的 u8/u16/i32 与 JS 的 Number 类型时,未进行边界检查会导致数据溢出。
- Wasm 模块加载失败:未正确配置 Webpack/Vite 的 Wasm loader,或未在浏览器中启用 Wasm 支持(现代浏览器默认已支持,但需检查浏览器版本)。
二、WebAssembly 基础
2.1 WebAssembly 的定义与特点
WebAssembly(Wasm)是一种可移植、高性能的低级字节码格式,设计用于在 Web 浏览器中运行,但现在也支持在 Node.js、Wasmtime、Wasmer 等独立运行时中运行。Wasm 的核心特点是:
- 高性能:Wasm 的执行速度接近原生代码(C/C++),比 JavaScript 快 10-100 倍。
- 可移植性:Wasm 模块可以在任何支持 Wasm 的运行时中运行,无需修改。
- 安全性:Wasm 运行在沙箱环境中,有严格的内存访问限制。
- 体积小:Wasm 模块的体积比 JavaScript 小,加载速度快。
2.2 Wasm 与 JavaScript 的性能对比
| 指标 | JavaScript | WebAssembly(Rust) |
|---|---|---|
| 执行速度(计算密集型) | 🌟 | 🌟🌟🌟🌟🌟 |
| 内存占用 | 🌟🌟🌟 | 🌟🌟🌟🌟 |
| 开发效率 | 🌟🌟🌟🌟🌟 | 🌟🌟🌟 |
| 调试难度 | 🌟🌟🌟 | 🌟🌟 |
| 生态系统 | 🌟🌟🌟🌟🌟 | 🌟🌟🌟 |
2.3 Wasm 的使用场景
- 浏览器端高性能任务:图像/视频处理、音频处理、WebGL/Canvas 计算、3D 渲染、加密解密。
- Node.js 端计算密集型任务:数据压缩、图像处理、机器学习推理、密码学计算。
- 物联网设备:使用 Wasmtime/Wasmer 在资源受限的设备上运行。
- 边缘计算:在 CDN 节点或边缘服务器上运行 Wasm 模块,减少网络延迟。
三、Rust 到 Wasm 的编译工具链
3.1 安装 wasm-pack
wasm-pack 是官方推荐的 Rust 到 Wasm 的编译工具链,它可以:
- 编译 Rust 代码到 Wasm 模块。
- 生成与 JavaScript 交互的绑定代码。
- 打包 Wasm 模块为 npm 包。
- 优化 Wasm 体积。
⌨️ 安装 wasm-pack:
# 使用 Cargo 安装
cargo install wasm-pack
3.2 安装 cargo-web
cargo-web 是另一个常用的 Rust 到 Wasm 的编译工具链,它支持:
- 热重载(Hot Reload)。
- 打包静态资源。
- 启动开发服务器。
⌨️ 安装 cargo-web:
# 使用 Cargo 安装(需要 Rust 1.70 或更高版本)
cargo install cargo-web --version 0.6.42
四、Rust 与 JavaScript 交互基础
4.1 创建一个简单的 Wasm 项目
使用 wasm-pack 创建一个简单的 Wasm 项目:
# 创建项目
wasm-pack new rust-wasm-demo
cd rust-wasm-demo
4.2 编写 Rust 代码
在 src/lib.rs 文件中编写 Rust 代码:
use wasm_bindgen::prelude::*;
// 使用 #[wasm_bindgen] 宏标记可以被 JavaScript 调用的函数
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}! This is Rust running in WebAssembly!", name)
}
// 计算斐波那契数列
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
if n == 0 || n == 1 {
return n;
}
let mut a = 0;
let mut b = 1;
for _ in 2..=n {
let c = a + b;
a = b;
b = c;
}
b
}
// 计算数组的平均值
#[wasm_bindgen]
pub fn average(arr: &[f64]) -> f64 {
if arr.is_empty() {
return 0.0;
}
let sum: f64 = arr.iter().sum();
sum / arr.()
}
4.3 编译 Wasm 模块
使用 wasm-pack 编译 Wasm 模块:
# 编译为 npm 包
wasm-pack build --target web
4.4 编写 HTML 与 JavaScript 代码
在项目根目录下创建 index.html 文件:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rust WebAssembly Demo</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
.container { max-width: 800px; margin: 0 auto; background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); }
.result { margin-top: 20px; padding: 10px; : ; : ; : monospace; }
{ : ; : ; : ; : ; : none; : ; : pointer; : ; }
{ : ; }
Rust WebAssembly Demo
姓名:
打招呼
斐波那契数列第 n 项:
计算
数组(用逗号分隔):
计算平均值
4.5 运行 Wasm 应用
使用 Vite 启动开发服务器:
# 初始化 Vite 项目
npm init vite@latest . -- --template vanilla
# 安装依赖
npm install
# 运行开发服务器
npm run dev
五、Rust 与 JavaScript 交互进阶
5.1 处理复杂数据类型
使用 serde-wasm-bindgen 库处理复杂数据类型(如 Rust 的 Struct 与 JS 的 Object、Rust 的 Enum 与 JS 的 Object/Array/String)。
5.1.1 安装依赖
在 Cargo.toml 中添加依赖:
[dependencies]
wasm-bindgen = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.5"
js-sys = "0.3"
web-sys = { version = "0.3", features = ["console", "Document", "Element", "HtmlElement"] }
5.1.2 编写 Rust 代码
在 src/lib.rs 文件中添加处理复杂数据类型的代码:
use wasm_bindgen::prelude::*;
use serde::Serialize;
use serde_wasm_bindgen::to_value;
use js_sys::Array;
use web_sys::console;
// 定义用户结构体
#[derive(Debug, Serialize)]
struct User {
id: u32,
username: String,
email: String,
age: Option<u8>,
tags: Vec<String>,
}
// 定义产品枚举
#[derive(Debug, Serialize)]
enum Product {
Book { title: String, author: String, price: f64 },
Electronics { name: String, brand: String, price: f64, warranty: u32 },
Clothing { name: String, size: String, price: f64 },
}
// 获取用户列表
#[wasm_bindgen]
pub fn get_users() -> JsValue {
let users = vec![
User { id: 1, username: "张三".to_string(), email: "[email protected]".to_string(), age: Some(25), tags: vec!["前端".to_string(), "Rust".to_string()], },
User { id: , username: .(), email: .(), age: , tags: [.(), .()], },
User { id: , username: .(), email: .(), age: (), tags: [.(), .()], },
];
(&users).()
}
() JsValue {
= [
Product::Book { title: .(), author: .(), price: , },
Product::Electronics { name: .(), brand: .(), price: , warranty: , },
Product::Clothing { name: .(), size: .(), price: , },
];
(&products).()
}
(cart: JsValue) {
: <(, , )> = serde_wasm_bindgen::(cart).();
: = cart.().(|(name, price, quantity)| price * quantity ).();
total
}
5.1.3 编写 JavaScript 代码
在 index.html 文件中添加处理复杂数据类型的代码:
// 获取用户列表
function getUsers() {
const users = get_users();
console.log('用户列表:', users);
document.getElementById('users-result').textContent = JSON.stringify(users, null, 2);
}
// 获取产品列表
function getProducts() {
const products = get_products();
console.log('产品列表:', products);
document.getElementById('products-result').textContent = JSON.stringify(products, null, 2);
}
// 计算订单总价
function calculateTotalPrice() {
const cart = [
{ name: 'Rust 语言开发从入门到精通', price: 99.0, quantity: 1 },
{ name: 'Rust T 恤', price: 99.0, quantity: 2 },
{ name: 'MacBook Pro', price: 18999.0, : },
];
total = (cart);
.(). = ;
}
5.2 异步交互
使用 wasm-bindgen-futures 库实现异步交互(如 HTTP 请求)。
5.2.1 安装依赖
在 Cargo.toml 中添加依赖:
[dependencies]
wasm-bindgen-futures = "0.4"
reqwest = { version = "0.11", features = ["json"] }
5.2.2 编写 Rust 代码
在 src/lib.rs 文件中添加异步交互的代码:
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use reqwest::Client;
use serde::Deserialize;
use web_sys::console;
// 定义 GitHub 用户结构体
#[derive(Debug, Deserialize)]
struct GitHubUser {
login: String,
id: u32,
avatar_url: String,
html_url: String,
name: Option<String>,
bio: Option<String>,
public_repos: u32,
followers: u32,
following: u32,
created_at: String,
}
// 获取 GitHub 用户信息
#[wasm_bindgen]
pub fn get_github_user(username: &str) -> JsValue {
let username = username.to_string();
let promise = js_sys::Promise::new(&mut move |resolve, reject| {
spawn_local(async move {
let client = Client::new();
let url = format!("https://api.github.com/users/{}", username);
let result = client.get(&url).send()..(|response| response.json::<GitHubUser>().);
result {
(user) => {
resolve.(&JsValue::NULL, &serde_wasm_bindgen::(&user).()).();
}
(e) => {
reject.(&JsValue::NULL, &JsValue::(&e.())).();
}
}
});
});
promise.()
}
5.2.3 编写 JavaScript 代码
在 index.html 文件中添加异步交互的代码:
// 获取 GitHub 用户信息
async function getGitHubUser() {
const username = document.getElementById('github-username').value;
try {
const user = await get_github_user(username);
console.log('GitHub 用户信息:', user);
document.getElementById('github-user-result').textContent = JSON.stringify(user, null, 2);
} catch (e) {
console.error('获取 GitHub 用户信息失败:', e);
document.getElementById('github-user-result').textContent = `获取用户信息失败:${e}`;
}
}
5.3 DOM 操作
使用 web-sys 库实现 DOM 操作(如事件监听、元素创建)。
5.3.1 安装依赖
在 Cargo.toml 中添加 web-sys 的 DOM 相关功能:
[dependencies.web-sys]
version = "0.3"
features = [
"Document",
"Element",
"HtmlElement",
"Node",
"Window",
"Event",
"EventListener",
"HtmlInputElement",
"HtmlButtonElement",
"HtmlDivElement",
"HtmlHeadingElement",
"HtmlParagraphElement",
]
5.3.2 编写 Rust 代码
在 src/lib.rs 文件中添加 DOM 操作的代码:
use wasm_bindgen::prelude::*;
use web_sys::{document, window, Event, HtmlInputElement, HtmlButtonElement, HtmlDivElement};
// 创建一个简单的待办事项列表
#[wasm_bindgen]
pub fn create_todo_list() {
let document = document().expect("无法获取文档对象");
// 创建容器
let container = document.create_element("div").expect("无法创建容器");
container.set_id("todo-container");
container.set_inner_html(r#"
<h2>待办事项列表</h2>
<input type="text" placeholder="添加待办事项...">
<button>添加</button>
<div></div>
"#);
// 添加到文档中
document.body().expect("无法获取 body 元素").append_child(&container).expect("无法添加容器");
// 添加事件监听
let add_button = document.get_element_by_id("todo-add-button").expect("无法找到添加按钮").dyn_into::<HtmlButtonElement>().expect("添加按钮类型不正确");
let closure = Closure::wrap(Box::new(move |_e: Event| {
let document = document().expect();
= document.().().dyn_into::<HtmlInputElement>().();
= input.();
!text.() {
(text.());
input.();
}
}) < (_)>);
add_button.(, closure.().()).();
closure.forget();
}
(text: &) {
= ().();
= document.().().dyn_into::<HtmlDivElement>().();
= document.().();
todo_item.(&(, chrono::Utc::().()));
todo_item.(&(, text));
todo_list.(&todo_item).();
= todo_item.().().().dyn_into::<HtmlButtonElement>().();
= todo_item.();
= Closure::(::( |_e: Event| {
= ().();
= document.().().dyn_into::<HtmlDivElement>().();
parent.(&todo_item_clone).();
}) < (_)>);
remove_button.(, closure.().()).();
closure.forget();
}
六、真实案例应用
6.1 案例 1:浏览器端高性能 Canvas 图像滤镜
💡 场景分析:需要编写一个浏览器端的高性能图像滤镜,支持灰度、反转、模糊、锐化等滤镜效果,使用 Rust 编译成 Wasm 模块,提高滤镜的执行速度。
6.1.1 编写 Rust 代码
在 src/lib.rs 文件中添加图像滤镜的代码:
use wasm_bindgen::prelude::*;
use image::RgbaImage;
use image::Pixel;
use imageproc::contrast::adaptive_equalization;
use imageproc::filter::gaussian_blur_f32;
use imageproc::filter::sharpen;
use imageproc::ops::invert;
use web_sys::console;
// 将 base64 编码的图片数据转换为 RgbaImage
fn base64_to_image(base64: &str) -> RgbaImage {
let base64 = base64.strip_prefix("data:image/png;base64,").unwrap_or(base64);
let bytes = base64::decode(base64).expect("解析 base64 图片数据失败");
let image = image::load_from_memory(&bytes).expect("加载图片失败").to_rgba8();
image
}
// 将 RgbaImage 转换为 base64 编码的图片数据
fn image_to_base64(image: RgbaImage) -> String {
let mut buffer = Vec::new();
image::write_buffer_with_format(&mut buffer, &image, image::ImageOutputFormat::Png).expect("保存图片失败");
format!("data:image/png;base64,{}", base64::encode(buffer))
}
// 灰度滤镜
#[wasm_bindgen]
pub (base64: &) {
= (base64);
image.() {
= pixel[] ;
= pixel[] ;
= pixel[] ;
= (r * + g * + b * ) ;
pixel[] = gray;
pixel[] = gray;
pixel[] = gray;
}
(image)
}
(base64: &) {
= (base64);
(& image);
(image)
}
(base64: &, sigma: ) {
= (base64);
= (&image, sigma);
(blurred)
}
(base64: &, amount: ) {
= (base64);
= (&image, amount);
(sharpened)
}
(base64: &) {
= (base64);
= (&image, , , );
(equalized)
}
6.1.2 编写 HTML 与 JavaScript 代码
在 index.html 文件中添加图像滤镜的代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rust Wasm Canvas 图像滤镜</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background-color: #f5f5f5; }
.container { max-width: 1000px; margin: 0 auto; background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); }
.image-container { display: flex; gap: 20px; margin-top: ; }
{ : ; }
{ : center; : ; }
{ : ; : auto; : solid ; : ; }
{ : ; : ; : ; : ; }
{ : ; }
, , { : ; : ; : solid ; : ; }
Rust Wasm Canvas 图像滤镜
灰度
反转
模糊
锐化
对比度增强
应用滤镜
原图
处理后
6.2 案例 2:Node.js 端计算密集型任务——数据压缩
💡 场景分析:需要编写一个 Node.js 端的计算密集型任务——数据压缩,使用 Rust 编译成 Wasm 模块,提高压缩的执行速度。
6.2.1 编写 Rust 代码
在 src/lib.rs 文件中添加数据压缩的代码:
use wasm_bindgen::prelude::*;
use flate2::write::GzEncoder;
use flate2::Compression;
use std::io::Write;
use base64;
// 压缩字符串
#[wasm_bindgen]
pub fn compress_string(input: &str, level: u32) -> String {
let mut encoder = GzEncoder::new(Vec::new(), Compression::new(level));
encoder.write_all(input.as_bytes()).expect("压缩失败");
let compressed = encoder.finish().expect("压缩失败");
base64::encode(compressed)
}
// 解压缩字符串
#[wasm_bindgen]
pub fn decompress_string(input: &str) -> String {
let compressed = base64::decode(input).expect("解析 base64 数据失败");
let mut decoder = flate2::read::GzDecoder::new(&compressed[..]);
let mut decompressed = String::new();
std::io::Read::(& decoder, & decompressed).();
decompressed
}
(input: &[], level: ) <> {
= GzEncoder::(::(), Compression::(level));
encoder.(input).();
encoder.().()
}
(input: &[]) <> {
= flate2::read::GzDecoder::(&input[..]);
= ::();
std::io::Read::(& decoder, & decompressed).();
decompressed
}
6.2.2 编写 Node.js 代码
在 node.js 文件中添加数据压缩的代码:
const fs = require('fs');
const path = require('path');
const wasmPath = path.join(__dirname, 'pkg', 'rust_wasm_demo.js');
const wasmModule = require(wasmPath);
async function run() {
// 初始化 Wasm 模块
await wasmModule.default();
console.log('Rust WebAssembly 模块初始化成功');
// 测试字符串压缩
const testString = 'Hello, this is a test string for data compression! '.repeat(1000);
const compressedString = wasmModule.compress_string(testString, 9);
const decompressedString = wasmModule.decompress_string(compressedString);
console.log(`字符串压缩前大小:${testString.length} bytes`);
console.log(`字符串压缩后大小:${compressedString.length} bytes`);
console.log(`字符串压缩率:${((1 - compressedString.length / testString.length) * 100).toFixed(2)}%`);
console.log(`字符串解压缩成功:${decompressedString === testString}`);
testFile = path.(__dirname, );
testData = fs.(testFile);
compressedData = wasmModule.(testData);
decompressedData = wasmModule.(compressedData);
.();
.();
.();
.();
compressedFile = path.(__dirname, );
fs.(compressedFile, .(compressedData));
.();
}
();
七、常见问题与解决方案
7.1 未正确释放 Wasm 分配的内存
问题现象:浏览器/Node.js 的内存使用率过高,导致页面卡顿或程序崩溃。
解决方案:
- 手动释放 Rust 分配的内存:使用
wasm_bindgen::prelude::JsValue::free或Box::into_raw。 - 使用智能指针:使用
Box<T>或Arc<T>自动管理内存。 - 避免内存泄漏:在异步任务中确保所有分配的内存都被释放。
7.2 数据类型转换时的边界检查
问题现象:数据溢出,导致计算结果错误。
解决方案:
- 使用
std::num::Wrapping类型处理溢出。 - 在转换前进行边界检查:使用
i32::try_from或u32::try_into。 - 使用
serde_wasm_bindgen的Deserializetrait,它会自动处理边界检查。
7.3 Wasm 模块加载失败
问题现象:浏览器控制台报错'WebAssembly.instantiateStreaming failed'或'Uncaught (in promise) TypeError: Failed to fetch'。
解决方案:
- 检查 Wasm 模块的路径是否正确。
- 确保 Webpack/Vite 的 Wasm loader 配置正确。
- 检查浏览器是否支持 Wasm(现代浏览器默认已支持,但需检查浏览器版本)。
八、总结与展望
8.1 总结
✅ 理解了 WebAssembly 基础:深入掌握了 WebAssembly 的核心定义、运行机制、与 JavaScript 的性能对比。 ✅ 掌握了 Rust 到 Wasm 的编译:熟练使用 wasm-pack 工具链,完成了 Rust 代码到 Wasm 模块的编译、打包、优化。 ✅ 精通了 Rust 与 JavaScript 交互:实现了双向交互(Rust 调用 JS 函数、JS 调用 Rust 函数),处理了复杂数据类型,管理了内存。 ✅ 开发了真实 Wasm 应用:编写了浏览器端高性能 Canvas 图像滤镜、Node.js 端计算密集型数据压缩任务。 ✅ 优化了 Wasm 模块:学习了使用 wasm-opt 工具优化 Wasm 体积。 ✅ 部署了 Wasm 应用:学习了部署到静态资源服务器、CDN,使用 Vite/Webpack 集成到 Vue/React 项目。
8.2 展望
下一篇文章,我们将深入学习 Rust 的嵌入式开发,包括使用 Rust 开发 Arduino、Raspberry Pi 等嵌入式设备,学习 GPIO 操作、传感器数据读取、通信协议(如 I2C、SPI),通过这些知识我们将能够将 Rust 代码运行在资源受限的嵌入式设备上。


