
让 Rust 跑在浏览器里:wasm-bindgen 和前端交互
你有没有想过,用 Rust 写前端代码?
不是开玩笑,Rust 真的可以编译成 WebAssembly (WASM),在浏览器里运行!而且性能接近原生代码。
WASM 能做什么?
今天咱们就聊聊怎么用 Rust 写 WASM,和 JavaScript 交互,让 Rust 跑在浏览器里。
WASM 是一种二进制指令格式,可以在浏览器里运行。它的特点:
生活化类比:
想象浏览器是个厨房:
工具 | 作用 |
|---|---|
wasm-pack | 构建和发布 WASM 包 |
wasm-bindgen | Rust 和 JS 互操作 |
wasm-opt | 优化 WASM 二进制 |
cargo-generate | 快速创建项目模板 |
use wasm_bindgen::prelude::*;
// 导出给 JavaScript 用
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// 使用 JavaScript 的 API
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
log(&format!("Hello, {}!", name));
}
# 安装 wasm-pack
cargo install wasm-pack
# 创建项目
cargo generate --git https://github.com/rustwasm/wasm-pack-template
# 项目名称:my-wasm-app
# 构建
wasm-pack build --target web
项目结构:
my-wasm-app/
├── Cargo.toml
├── src/
│ └── lib.rs
├── pkg/ # 构建输出
│ ├── my_wasm_app.js
│ ├── my_wasm_app_bg.wasm
│ └── package.json
└── index.html
// src/lib.rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
=> ,
=> ,
_ => {
let mut a = ;
let mut b = ;
for _ in ..=n {
let temp = a + b;
a = b;
b = temp;
}
b
}
}
}
#[wasm_bindgen]
pub fn factorial(n: u32) -> u32 {
(..=n).product()
}
使用(HTML + JavaScript):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Rust WASM Demo</title>
</head>
<body>
<script type="module">
import init, { add, fibonacci, factorial } from './pkg/my_wasm_app.js';
async function run() {
await init();
console.log('2 + 3 =', add(, ));
console.log('fibonacci(10) =', fibonacci());
console.log('factorial(5) =', factorial());
}
run();
</script>
</body>
</html>
// Cargo.toml
[dependencies]
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ["Document", "Element", "HtmlElement", "Window"] }
// src/lib.rs
use wasm_bindgen::prelude::*;
use web_sys::window;
#[wasm_bindgen]
pub fn change_background_color(color: &str) {
let document = window()
.expect("没有 window")
.document()
.expect("没有 document");
let body = document.body().expect("没有 body");
body.set_attribute("style", &format!("background-color: {}", color))
.expect("设置样式失败");
}
#[wasm_bindgen]
pub fn alert_message(message: &str) {
if let Some(window) = window() {
window.alert_with_message(message).ok();
}
}
// src/lib.rs
use wasm_bindgen::prelude::*;
// 导入 JavaScript 函数
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = Math)]
fn random() -> f64;
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
pub fn random_number() -> f64 {
random()
}
#[wasm_bindgen]
pub fn log_to_console(message: &str) {
log(message);
}
// 回调函数
#[wasm_bindgen]
pub fn call_js_callback(callback: &js_sys::Function, value: i32) -> Result<(), JsValue> {
callback.call1(&JsValue::NULL, &JsValue::from(value))
}
JavaScript 端:
import init, { call_js_callback } from './pkg/my_wasm_app.js';
async function run() {
await init();
// Rust 调用 JS 回调
const myCallback = (value) => {
console.log('从 Rust 收到的值:', value);
};
call_js_callback(myCallback, );
}
// src/lib.rs
use wasm_bindgen::prelude::*;
use js_sys::Array;
#[wasm_bindgen]
pub fn reverse_string(s: &str) -> String {
s.chars().rev().collect()
}
#[wasm_bindgen]
pub fn sort_numbers(numbers: Vec<i32>) -> Vec<i32> {
let mut sorted = numbers;
sorted.sort();
sorted
}
#[wasm_bindgen]
pub fn process_array(arr: &[i32]) -> Vec<i32> {
arr.iter().map(|&x| x * ).collect()
}
// 返回复杂数据
#[wasm_bindgen]
pub fn get_user_info() -> JsValue {
let obj = js_sys::Object::new();
js_sys::Reflect::set(&obj, &JsValue::from("name"), &JsValue::from("Alice")).unwrap();
js_sys::Reflect::set(&obj, &JsValue::from("age"), &JsValue::from()).unwrap();
obj
}
// Cargo.toml
[dependencies]
wasm-bindgen = "0.2"
image = "0.24"
// src/lib.rs
use wasm_bindgen::prelude::*;
use image::{ImageBuffer, Rgb};
#[wasm_bindgen]
pub fn create_grayscale_image(width: u32, height: u32, data: &[u8]) -> Vec<u8> {
let mut img = ImageBuffer::<Rgb<u8>, Vec<u8>>::new(width, height);
for (x, y, pixel) in img.enumerate_pixels_mut() {
let index = ((y * width + x) * ) as usize;
let r = data[index];
let g = data[index + ];
let b = data[index + ];
// 转灰度
let gray = (r as u32 * + g as u32 * + b as u32 * ) / ;
*pixel = Rgb([gray as u8, gray as u8, gray as u8]);
}
img.into_raw()
}
#[wasm_bindgen]
pub fn apply_sepia(data: &[u8], width: u32, height: u32) -> Vec<u8> {
let mut result = Vec::with_capacity(data.len());
for i in (..data.len()).step_by() {
let r = data[i] as f32;
let g = data[i + ] as f32;
let b = data[i + ] as f32;
let new_r = (0.393 * r + 0.769 * g + 0.189 * b).min(255.0) as u8;
let new_g = (0.349 * r + 0.686 * g + 0.168 * b).min(255.0) as u8;
let new_b = (0.272 * r + 0.534 * g + 0.131 * b).min(255.0) as u8;
result.push(new_r);
result.push(new_g);
result.push(new_b);
}
result
}
// ❌ 直接返回 &str 不行
#[wasm_bindgen]
pub fn get_string() -> &str {
"hello"// 编译错误
}
// ✅ 返回 String
#[wasm_bindgen]
pub fn get_string() -> String {
String::from("hello")
}
// ❌ Result 需要特殊处理
#[wasm_bindgen]
pub fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == {
Err("除数不能为零".to_string())
} else {
Ok(a / b)
}
}
// JavaScript 端需要 try-catch
try {
const result = divide(, );
} catch (e) {
console.error(e);
}
// ❌ 忘记释放
#[wasm_bindgen]
pub fn process_data(data: &[u8]) -> Vec<u8> {
// 大量分配
}
// ✅ 使用 Box 和手动释放
#[wasm_bindgen]
pub struct DataProcessor {
data: Vec<u8>,
}
#[wasm_bindgen]
impl DataProcessor {
pub fn new() -> DataProcessor {
DataProcessor { data: Vec::new() }
}
pub fn process(&mut self, input: &[u8]) {
self.data = input.to_vec();
// 处理...
}
pub fn free(self) {
// 显式释放
}
}
// ❌ 早期 wasm-bindgen 不支持 async
// ✅ 现在支持了
#[wasm_bindgen]
pub async fn fetch_data(url: &str) -> Result<JsValue, JsValue> {
// 使用 web-sys 的 fetch API
let window = web_sys::window().unwrap();
let opts = web_sys::RequestInit::new();
// ...
}
// Cargo.toml
[dependencies]
wasm-bindgen = "0.2"
pulldown-cmark = "0.9"
// src/lib.rs
use wasm_bindgen::prelude::*;
use pulldown_cmark::{Parser, Options, html};
#[wasm_bindgen]
pub fn markdown_to_html(markdown: &str) -> String {
let mut options = Options::empty();
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TABLES);
let parser = Parser::new_ext(markdown, options);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
html_output
}
// Cargo.toml
[dependencies]
wasm-bindgen = "0.2"
rand = "0.8"
getrandom = { version = "0.2", features = ["js"] }
// src/lib.rs
use wasm_bindgen::prelude::*;
use rand::Rng;
#[wasm_bindgen]
pub fn generate_password(length: usize, use_uppercase: bool, use_numbers: bool, use_symbols: bool) -> String {
let mut chars = String::from("abcdefghijklmnopqrstuvwxyz");
if use_uppercase {
chars.push_str("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
}
if use_numbers {
chars.push_str("0123456789");
}
if use_symbols {
chars.push_str("!@#$%^&*()_+-=[]{}|;:,.<>?");
}
let mut rng = rand::thread_rng();
(..length)
.map(|_| {
let idx = rng.gen_range(..chars.len());
chars.chars().nth(idx).unwrap()
})
.collect()
}

金句回顾:
下篇预告:
性能优化是每个程序员都要面对的课题。下篇聊聊Rust 性能优化,看看怎么用基准测试、profiling、零成本抽象,让你的代码飞起来!