第9章 9.2 WebAssembly 入门:用 Rust 写代码,浏览器里跑

上一章我们学会了 Web Components,把自定义 HTML 元素封装成可复用的「积木块」。但光有 UI 组件还不够——如果你想在浏览器里跑一段高性能计算(比如视频解码、图片处理、游戏逻辑),JavaScript 跑起来就有点力不从心了。

这一章,我们来学一个让浏览器「快到飞起」的技术——WebAssembly,简称 Wasm。


🎯 开场 3 分钟:为什么要学这个?

真实场景:浏览器里跑「性能怪兽」

想象你是一个独立游戏开发者,你用 Rust 写了一个跑得飞快的游戏引擎。现在你想把这个引擎搬到浏览器里,让玩家直接打开网页就能玩。

问题来了:
- JavaScript 跑你的游戏,卡成 PPT
- 浏览器不认识 Rust 代码
- 怎么办?

答案就是 WebAssembly。

简单来说,WebAssembly 是一种可以让浏览器运行 C/C++/Rust 等语言编译出来的代码的技术。它不是要替代\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\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) = 987fibonacci(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

延伸资源

  1. 官方文档Rust and WebAssembly Book — Rust 官方出品的 Wasm 教程
  2. 在线练习WasmFiddle — 浏览器里写 Wasm
  3. 生态库awesome-rust wasm — Wasm 相关 Rust 生态汇总

互动钩子

你是做什么场景的?游戏、图像处理、数据分析?评论区聊聊,老粉优先回复!


下一章预告:

上一章我们学了 Web Components(UI 复用),这一章学了 WebAssembly(性能加持)。但浏览器里还有个问题没解决——JavaScript 是单线程的,如果一段代码计算量很大,页面还是会卡。

下一章我们要学:Web Worker 与多线程——让 JavaScript「分身术」,一边计算一边响应用户操作!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。