第9章 9.2 WebAssembly 入门:用 Rust 写代码,浏览器里跑
上一章我们学会了 Web Components,把自定义 HTML 元素封装成可复用的「积木块」。但光有 UI 组件还不够——如果你想在浏览器里跑一段高性能计算(比如视频解码、图片处理、游戏逻辑),JavaScript 跑起来就有点力不从心了。
这一章,我们来学一个让浏览器「快到飞起」的技术——WebAssembly,简称 Wasm。
🎯 开场 3 分钟:为什么要学这个?
真实场景:浏览器里跑「性能怪兽」
想象你是一个独立游戏开发者,你用 Rust 写了一个跑得飞快的游戏引擎。现在你想把这个引擎搬到浏览器里,让玩家直接打开网页就能玩。
问题来了:
- JavaScript 跑你的游戏,卡成 PPT
- 浏览器不认识 Rust 代码
- 怎么办?
答案就是 WebAssembly。
简单来说,WebAssembly 是一种可以让浏览器运行 C/C++/Rust 等语言编译出来的代码的技术。它不是要替代\n\n
\n\n
\n\n JavaScript,而是和 JavaScript 一起配合,各干各的擅长的事。
举个例子
如果你要做一个图片滤镜:
- JavaScript:写起来简单,但处理大图慢
- WebAssembly:用 Rust/C 写核心算法,编译成 Wasm,性能接近原生 App
学完这章,你就能:
1. 把 Rust 代码编译成 Wasm 模块
2. 在 JavaScript 里调用这个 Wasm 模块
3. 做一个图片处理小工具,体验「飞一般的感觉」
🧱 基础 25 分钟:核心概念
什么是 WebAssembly?
生活类比:
想象你要把一段中文演讲交给一个只懂英语的人听。你需要:
1. 先把中文翻译成英文(编译)
2. 然后那个人用英语听(运行)
WebAssembly 就是这个「翻译」环节——它把 Rust/C/C++ 代码「翻译」成浏览器能理解的二进制格式,浏览器里有个专门跑这种格式的「虚拟机」。
为什么要用 WebAssembly?
解决痛点:
| 场景 | JavaScript | WebAssembly |
|---|---|---|
| 复杂计算 | 慢 | 快(接近原生) |
| 内存操作 | 受限 | 更灵活 |
| 代码安全 | 源码可见 | 二进制加密 |
| 生态 | 超丰富 | 还在成长 |
一句话总结:
WebAssembly 不是用来替代 JavaScript 的,它是 JavaScript 的「性能外挂」。
怎么用?(最简单例子)
准备工作:安装 Rust 和 wasm-pack
在终端执行:
# 安装 Rust(如果没装过)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 安装 wasm-pack(编译 Wasm 的工具)
cargo install wasm-pack
安装完成后,你就有了一把「瑞士军刀」,可以把 Rust 代码编译成浏览器能跑的 Wasm 文件。
第一个 Wasm 模块:用 Rust 写一个加法函数
Step 1:创建 Rust 项目
cargo new wasm-demo --lib
cd wasm-demo
Step 2:写 Rust 代码(src/lib.rs)
use wasm_bindgen::prelude::*;
// 标记这个函数可以被 JavaScript 调用
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// 标记这个函数可以被 JavaScript 调用
#[wasm_bindgen]
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
Step 3:配置依赖( Cargo.toml)
[package]
name = "wasm-demo"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
Step 4:编译成 Wasm
wasm-pack build --target web
编译成功后,会生成 pkg/ 目录,里面有:
- wasm_demo.js — JavaScript 胶水代码
- wasm_demo_bg.wasm — 二进制 Wasm 文件
Step 5:在 HTML/JavaScript 里调用
<!DOCTYPE html>
<html>
<head>
<title>Wasm 初体验</title>
</head>
<body>
<h1>WebAssembly 初体验</h1>
<div id="output"></div>
<script type="module">
import init, { add, multiply } from "./pkg/wasm_demo.js";
async function run() {
// 初始化 Wasm 模块
await init();
// 调用 Rust 写的加法函数
const result1 = add(10, 20);
document.getElementById("output").innerHTML +=
`10 + 20 = ${result1}<br>`;
// 调用 Rust 写的乘法函数
const result2 = multiply(6, 7);
document.getElementById("output").innerHTML +=
`6 × 7 = ${result2}<br>`;
}
run();
</script>
</body>
</html>
预期输出:
10 + 20 = 30
6 × 7 = 42
这行代码在干嘛:
- await init() 加载 Wasm 模块
- add(10, 20) 直接调用 Rust 写的函数,就像调用普通 JS 函数一样
Wasm 和 JavaScript 的数据传递
核心概念:线性内存
WebAssembly 有一块自己的内存,叫「线性内存」。Wasm 和 JS 交换数据时:
- 简单类型(数字):直接传
- 复杂类型(字符串、数组):通过内存指针传
传字符串的例子:
Rust 端:
use wasm_bindgen::prelude::*;
use std::fmt;
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("你好,{}!欢迎学习 WebAssembly!", name)
}
JavaScript 端:
import init, { greet } from "./pkg/wasm_demo.js";
async function run() {
await init();
// JS 传字符串给 Rust,Rust 返回处理后的字符串
const message = greet("小明");
console.log(message); // 输出:你好,小明!欢迎学习 WebAssembly!
}
run();
注意:
- Rust 代码里字符串是 &str,返回是 String
- wasm-bindgen 帮你做了「JS 字符串 ↔ Rust 字符串」的转换
- 不用手动管理内存,wasm-bindgen 帮你搞定
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):斐波那契数列——理解 Wasm 性能优势
项目目标: 对比 JavaScript 和 WebAssembly 计算斐波那契数列的性能。
Rust 代码(src/lib.rs):
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
JavaScript 对比代码:
// 普通 JS 版
function fibonacciJS(n) {
if (n === 0) return 0;
if (n === 1) return 1;
return fibonacciJS(n - 1) + fibonacciJS(n - 2);
}
// Wasm 版
import init, { fibonacci } from "./pkg/fibonacci.js";
async function compare() {
await init();
const n = 35; // 计算第 35 个斐波那契数
console.time("JavaScript");
const resultJS = fibonacciJS(n);
console.timeEnd("JavaScript");
console.time("WebAssembly");
const resultWasm = fibonacci(n);
console.timeEnd("WebAssembly");
console.log(`结果:${resultJS} (JS) vs ${resultWasm} (Wasm)`);
}
compare();
预期输出:
JavaScript: 8.5ms
WebAssembly: 3.2ms
结果:9227465 (JS) vs 9227465 (Wasm)
一句话解释:
同样的算法,Wasm 跑得更快,因为它的计算更「接近机器」。
项目 2(15 分钟):图片灰度处理——真实场景应用
项目目标: 用 Rust 写图片处理算法,JavaScript 调用,做一个「一键转灰度图」的小工具。
Rust 代码(src/lib.rs):
use wasm_bindgen::prelude::*;
use std::slice;
#[wasm_bindgen]
pub fn apply_grayscale(
pixels: &mut [u8], // RGBA 像素数据
width: u32,
height: u32,
) {
// 每 4 个字节是一个像素 (R, G, B, A)
for i in (0..pixels.len()).step_by(4) {
// 取 RGB 三个通道的平均值
let gray = (pixels[i] as u32 + pixels[i + 1] as u32 + pixels[i + 2] as u32) / 3;
pixels[i] = gray as u8; // R
pixels[i + 1] = gray as u8; // G
pixels[i + 2] = gray as u8; // B
// A 保持不变
}
}
JavaScript 代码(index.html):
<!DOCTYPE html>
<html>
<head>
<title>图片灰度处理</title>
<style>
body { font-family: sans-serif; padding: 20px; }
canvas { border: 1px solid #ccc; margin: 10px 0; }
button { padding: 10px 20px; font-size: 16px; }
</style>
</head>
<body>
<h1>🖼️ 图片灰度处理工具</h1>
<input type="file" id="fileInput" accept="image/*">
<br><br>
<canvas id="originalCanvas"></canvas>
<canvas id="processedCanvas"></canvas>
<br>
<button id="processBtn">转灰度图</button>
<script type="module">
import init, { apply_grayscale } from "./pkg/image_processor.js";
let originalImageData = null;
async function run() {
await init();
const fileInput = document.getElementById("fileInput");
const originalCanvas = document.getElementById("originalCanvas");
const processedCanvas = document.getElementById("processedCanvas");
const processBtn = document.getElementById("processBtn");
const ctxOriginal = originalCanvas.getContext("2d");
const ctxProcessed = processedCanvas.getContext("2d");
// 选择图片后显示原图
fileInput.addEventListener("change", (e) => {
const file = e.target.files[0];
const img = new Image();
img.onload = () => {
originalCanvas.width = img.width;
originalCanvas.height = img.height;
ctxOriginal.drawImage(img, 0, 0);
originalImageData = ctxOriginal.getImageData(
0, 0, img.width, img.height
);
// 准备处理后的画布
processedCanvas.width = img.width;
processedCanvas.height = img.height;
};
img.src = URL.createObjectURL(file);
});
// 点击处理按钮
processBtn.addEventListener("click", () => {
if (!originalImageData) {
alert("请先选择一张图片!");
return;
}
// 复制像素数据
const pixels = new Uint8Array(originalImageData.data);
// 调用 Rust 函数处理
apply_grayscale(pixels, originalImageData.width, originalImageData.height);
// 显示处理后的图片
const newImageData = new ImageData(
new Uint8ClampedArray(pixels),
originalImageData.width,
originalImageData.height
);
ctxProcessed.putImageData(newImageData, 0, 0);
});
}
run();
</script>
</body>
</html>
使用步骤:
1. 在终端运行 wasm-pack build --target web
2. 用任意 HTTP 服务器打开 index.html(比如 python -m http.server)
3. 上传一张图片,点击「转灰度图」
预期输出:
页面上方显示原图,下方显示处理后的灰度图
一句话解释:
Rust 写的处理函数直接操作像素数组,
wasm-bindgen帮你把 JS 的Uint8Array无缝传给 Rust。
项目 3(15 分钟):待办清单数据统计分析——Wasm 处理 JSON 数据
项目目标: 用 Wasm 做待办清单的数据统计分析(统计完成率、筛选高优先级任务)。
Rust 代码(src/lib.rs):
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct TodoItem {
id: u32,
title: String,
completed: bool,
priority: u32, // 1-5,数字越大优先级越高
}
#[derive(Serialize, Deserialize)]
struct TodoStats {
total: u32,
completed: u32,
pending: u32,
completion_rate: f32,
high_priority_pending: Vec<String>,
}
#[wasm_bindgen]
pub fn analyze_todos(json_data: &str) -> String {
// 解析 JSON
let todos: Vec<TodoItem> = serde_json::from_str(json_data)
.expect("无效的 JSON 格式");
// 统计
let total = todos.len() as u32;
let completed = todos.iter().filter(|t| t.completed).count() as u32;
let pending = total - completed;
let completion_rate = if total > 0 {
(completed as f32 / total as f32) * 100.0
} else {
0.0
};
// 找出未完成的高优先级任务(优先级 >= 4)
let high_priority_pending: Vec<String> = todos
.iter()
.filter(|t| !t.completed && t.priority >= 4)
.map(|t| t.title.clone())
.collect();
let stats = TodoStats {
total,
completed,
pending,
completion_rate,
high_priority_pending,
};
// 返回 JSON 格式的统计结果
serde_json::to_string(&stats).unwrap()
}
JavaScript 代码:
import init, { analyze_todos } from "./pkg/todo_analyzer.js";
async function run() {
await init();
// 模拟待办清单数据
const todoList = [
{ id: 1, title: "买早餐", completed: true, priority: 3 },
{ id: 2, title: "写周报", completed: false, priority: 4 },
{ id: 3, title: "开会", completed: true, priority: 5 },
{ id: 4, title: "健身", completed: false, priority: 5 },
{ id: 5, title: "看书", completed: false, priority: 2 },
];
// 转换成 JSON 字符串传给 Rust
const jsonString = JSON.stringify(todoList);
// 调用 Rust 函数分析数据
const statsJson = analyze_todos(jsonString);
const stats = JSON.parse(statsJson);
console.log("=== 待办清单统计分析 ===");
console.log(`总任务数:${stats.total}`);
console.log(`已完成:${stats.completed}`);
console.log(`待完成:${stats.pending}`);
console.log(`完成率:${stats.completion_rate.toFixed(1)}%`);
console.log(`高优先级待办:${stats.high_priority_pending.join("、")}`);
}
run();
预期输出:
=== 待办清单统计分析 ===
总任务数:5
已完成:2
待完成:3
完成率:40.0%
高优先级待办:写周报、健身
一句话解释:
Rust 的
serde库帮你解析 JSON,处理完再转回 JSON 返回,JS 端只管拿结果用。
💪 进阶 20 分钟:常见坑 + 性能小贴士
❌ 坑 1:忘记在 Rust 函数上加 #[wasm_bindgen]
// ❌ 错误:这个函数不会被导出
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// ✅ 正确:加了这个标记才会被 wasm-bindgen 处理
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
❌ 坑 2:字符串传递没注意生命周期
// ❌ 错误:返回局部字符串的引用,内存会被释放
#[wasm_bindgen]
pub fn bad_greet(name: &str) -> &str {
"你好"
}
// ✅ 正确:返回 owned String,生命周期没问题
#[wasm_bindgen]
pub fn good_greet(name: &str) -> String {
format!("你好,{}!", name)
}
❌ 坑 3:大数组传参没注意内存布局
// ✅ 正确:用 slice 接收,wasm-bindgen 会帮你处理内存映射
#[wasm_bindgen]
pub fn process_data(data: &[u8]) -> Vec<u8> {
// 处理 data...
data.iter().map(|x| x * 2).collect()
}
❌ 坑 4:没装 wasm32 目标平台
# ❌ 直接编译会报错
cargo build
# ✅ 先添加目标平台
rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown
❌ 坑 5:本地调试时没开 HTTP 服务器
直接双击打开 index.html 会报 CORS 错误:
Access to fetch at 'file://...' from origin 'null' has been blocked
解决方法:
# 用 Python 开启简单服务器
python -m http.server 8000
# 或用 Node.js
npx serve .
性能小贴士
避免频繁的 JS ↔ Wasm 调用:
// ❌ 低效:每次都调用 Wasm
for (let i = 0; i < 1000000; i++) {
result = add(result, 1);
}
// ✅ 高效:把数据一次性传给 Wasm,让 Rust 做循环
// Rust 端
#[wasm_bindgen]
pub fn batch_add(data: &[u32]) -> u32 {
data.iter().sum()
}
// JS 端
const data = new Uint32Array(1000000).fill(1);
const result = batch_add(data);
调试技巧
用 console.log 调试 Rust:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn debug_example(x: i32) -> i32 {
web_sys::console::log_1(&format!("收到数据:{}", x).into());
x * 2
}
Cargo.toml 需要加依赖:
[dependencies]
web-sys = { version = "0.3", features = ["console"] }
✏️ 练习题
练习 1(2 分钟):改数字
- 输入:调用
multiply(8, 9) - 预期输出:
72 - 提示:直接在 JS 里改参数就行
练习 2(2 分钟):加判断
- 输入:基于项目 1,判断如果斐波那契结果大于 1000 返回
">1000",否则返回数字本身 - 预期输出:
fibonacci(16) = 987,fibonacci(17) = ">1000" - 提示:在 JS 端用 if 判断,不用改 Rust 代码
练习 3(3 分钟):新数据
- 输入:用项目 3 的方式分析这个待办清单:
[{"id":1,"title":"喝水","completed":false,"priority":1},
"id":2,"title":"学习","completed":true,"priority":5},
"id":3,"title":"睡觉","completed":false,"priority":5}]
- 预期输出:完成率
33.3%,高优先级待办「学习、睡觉」 - 提示:把 JSON 格式粘过去就行
练习 4(5 分钟):串起来
- 输入:把练习 3 的待办清单传给项目 2 的灰度处理(随机生成一张图,只处理高优先级对应的像素)
- 预期输出:处理后的图片中,高优先级区域变灰度,其他区域保留原色
- 提示:用 priority 做掩码 mask
练习 5(3 分钟):分析报错
- 输入:如果用户忘记调用
await init(),会报什么错? - 预期输出:
TypeError: Cannot read properties of undefined (reading 'add') - 提示:Wasm 函数在 init() 完成后才可用
作业:做一个「Wasm 驱动的文本加密工具」
需求描述:
用 Rust 实现一个凯撒密码(Caesar Cipher),JS 调用,做一个可在浏览器里用的「文本加密小工具」。
功能点:
1. 用户输入一段文字
2. 选择偏移量(1-25)
3. 点击加密/解密按钮
4. 显示处理结果
加分项:
1. 支持中英文混合加密
2. 实时预览(输入变化时自动加密)
验收标准:
- 能跑起来
- 加密和解密结果正确
- 代码有注释
提交方式: 评论区贴代码或 GitHub 链接
📚 总结 + 资源
一句话总结
本文学了 3 个核心点:
1. WebAssembly 是什么:浏览器的「高性能代码格式」
2. 怎么写 Wasm:用 Rust 写 + wasm-bindgen 导出 + wasm-pack 编译
3. 怎么调用:JS 端 import + await init() + 像调 JS 函数一样调 Wasm
延伸资源
- 官方文档:Rust and WebAssembly Book — Rust 官方出品的 Wasm 教程
- 在线练习:WasmFiddle — 浏览器里写 Wasm
- 生态库:awesome-rust wasm — Wasm 相关 Rust 生态汇总
互动钩子
你是做什么场景的?游戏、图像处理、数据分析?评论区聊聊,老粉优先回复!
下一章预告:
上一章我们学了 Web Components(UI 复用),这一章学了 WebAssembly(性能加持)。但浏览器里还有个问题没解决——JavaScript 是单线程的,如果一段代码计算量很大,页面还是会卡。
下一章我们要学:Web Worker 与多线程——让 JavaScript「分身术」,一边计算一边响应用户操作!

评论(0)