跳到主要内容

实战案例

本章通过完整的实战案例,展示 WebAssembly 在实际项目中的应用。

案例1:图像处理

使用 WebAssembly 实现高性能图像处理。

Rust 实现

use wasm_bindgen::prelude::*;
use js_sys::Uint8ClampedArray;

#[wasm_bindgen]
pub fn grayscale(data: &mut [u8]) {
for i in (0..data.len()).step_by(4) {
let r = data[i] as f32;
let g = data[i + 1] as f32;
let b = data[i + 2] as f32;
let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
data[i] = gray;
data[i + 1] = gray;
data[i + 2] = gray;
}
}

#[wasm_bindgen]
pub fn invert(data: &mut [u8]) {
for i in (0..data.len()).step_by(4) {
data[i] = 255 - data[i];
data[i + 1] = 255 - data[i + 1];
data[i + 2] = 255 - data[i + 2];
}
}

#[wasm_bindgen]
pub fn brightness(data: &mut [u8], value: i16) {
for i in (0..data.len()).step_by(4) {
data[i] = (data[i] as i16 + value).clamp(0, 255) as u8;
data[i + 1] = (data[i + 1] as i16 + value).clamp(0, 255) as u8;
data[i + 2] = (data[i + 2] as i16 + value).clamp(0, 255) as u8;
}
}

#[wasm_bindgen]
pub fn contrast(data: &mut [u8], factor: f32) {
let factor = (259.0 * (factor + 255.0)) / (255.0 * (259.0 - factor));
for i in (0..data.len()).step_by(4) {
data[i] = ((factor * (data[i] as f32 - 128.0) + 128.0).clamp(0.0, 255.0)) as u8;
data[i + 1] = ((factor * (data[i + 1] as f32 - 128.0) + 128.0).clamp(0.0, 255.0)) as u8;
data[i + 2] = ((factor * (data[i + 2] as f32 - 128.0) + 128.0).clamp(0.0, 255.0)) as u8;
}
}

#[wasm_bindgen]
pub fn blur(data: &mut [u8], width: u32, height: u32, radius: u32) {
let mut temp = data.to_vec();
let width = width as usize;
let height = height as usize;
let radius = radius as usize;

// 水平模糊
for y in 0..height {
for x in 0..width {
let mut r = 0u32;
let mut g = 0u32;
let mut b = 0u32;
let mut count = 0u32;

for dx in -(radius as i32)..=(radius as i32) {
let nx = (x as i32 + dx).clamp(0, (width - 1) as i32) as usize;
let idx = (y * width + nx) * 4;
r += data[idx] as u32;
g += data[idx + 1] as u32;
b += data[idx + 2] as u32;
count += 1;
}

let idx = (y * width + x) * 4;
temp[idx] = (r / count) as u8;
temp[idx + 1] = (g / count) as u8;
temp[idx + 2] = (b / count) as u8;
}
}

// 垂直模糊
for y in 0..height {
for x in 0..width {
let mut r = 0u32;
let mut g = 0u32;
let mut b = 0u32;
let mut count = 0u32;

for dy in -(radius as i32)..=(radius as i32) {
let ny = (y as i32 + dy).clamp(0, (height - 1) as i32) as usize;
let idx = (ny * width + x) * 4;
r += temp[idx] as u32;
g += temp[idx + 1] as u32;
b += temp[idx + 2] as u32;
count += 1;
}

let idx = (y * width + x) * 4;
data[idx] = (r / count) as u8;
data[idx + 1] = (g / count) as u8;
data[idx + 2] = (b / count) as u8;
}
}
}

JavaScript 集成

<!DOCTYPE html>
<html>
<head>
<title>图像处理</title>
</head>
<body>
<canvas id="canvas"></canvas>
<input type="file" id="input" accept="image/*">
<button id="grayscale">灰度</button>
<button id="invert">反色</button>
<button id="brightness">增加亮度</button>
<button id="contrast">增加对比度</button>
<button id="blur">模糊</button>

<script type="module">
import init, { grayscale, invert, brightness, contrast, blur } from './pkg/image_processor.js';

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let imageData = null;

document.getElementById('input').addEventListener('change', async (e) => {
const file = e.target.files[0];
const img = new Image();
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
imageData = ctx.getImageData(0, 0, img.width, img.height);
};
img.src = URL.createObjectURL(file);
});

document.getElementById('grayscale').addEventListener('click', () => {
if (!imageData) return;
grayscale(imageData.data);
ctx.putImageData(imageData, 0, 0);
});

document.getElementById('invert').addEventListener('click', () => {
if (!imageData) return;
invert(imageData.data);
ctx.putImageData(imageData, 0, 0);
});

document.getElementById('brightness').addEventListener('click', () => {
if (!imageData) return;
brightness(imageData.data, 30);
ctx.putImageData(imageData, 0, 0);
});

document.getElementById('contrast').addEventListener('click', () => {
if (!imageData) return;
contrast(imageData.data, 50);
ctx.putImageData(imageData, 0, 0);
});

document.getElementById('blur').addEventListener('click', () => {
if (!imageData) return;
blur(imageData.data, canvas.width, canvas.height, 5);
ctx.putImageData(imageData, 0, 0);
});

await init();
</script>
</body>
</html>

案例2:计算密集型任务

实现斐波那契数列和质数计算。

Rust 实现

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u64 {
if n <= 1 {
return n as u64;
}

let mut a = 0u64;
let mut b = 1u64;

for _ in 2..=n {
let temp = a + b;
a = b;
b = temp;
}

b
}

#[wasm_bindgen]
pub fn is_prime(n: u64) -> bool {
if n < 2 {
return false;
}
if n == 2 {
return true;
}
if n % 2 == 0 {
return false;
}

let sqrt = (n as f64).sqrt() as u64;
for i in (3..=sqrt).step_by(2) {
if n % i == 0 {
return false;
}
}
true
}

#[wasm_bindgen]
pub fn count_primes(start: u64, end: u64) -> u32 {
let mut count = 0;
for n in start..=end {
if is_prime(n) {
count += 1;
}
}
count
}

#[wasm_bindgen]
pub fn find_primes(start: u64, end: u64) -> Vec<u64> {
(start..=end).filter(|&n| is_prime(n)).collect()
}

#[wasm_bindgen]
pub fn matrix_multiply(a: &[f64], b: &[f64], n: usize) -> Vec<f64> {
let mut result = vec![0.0; n * n];

for i in 0..n {
for j in 0..n {
let mut sum = 0.0;
for k in 0..n {
sum += a[i * n + k] * b[k * n + j];
}
result[i * n + j] = sum;
}
}

result
}

性能对比

<!DOCTYPE html>
<html>
<head>
<title>性能对比</title>
</head>
<body>
<h1>WebAssembly vs JavaScript 性能对比</h1>
<button id="fib">斐波那契(40)</button>
<button id="prime">质数计数(1-1000000)</button>
<button id="matrix">矩阵乘法(100x100)</button>
<div id="results"></div>

<script type="module">
import init, { fibonacci, count_primes, matrix_multiply } from './pkg/compute.js';

function jsFibonacci(n) {
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b];
}
return b;
}

function jsIsPrime(n) {
if (n < 2) return false;
if (n === 2) return true;
if (n % 2 === 0) return false;
const sqrt = Math.sqrt(n);
for (let i = 3; i <= sqrt; i += 2) {
if (n % i === 0) return false;
}
return true;
}

function jsCountPrimes(start, end) {
let count = 0;
for (let n = start; n <= end; n++) {
if (jsIsPrime(n)) count++;
}
return count;
}

function jsMatrixMultiply(a, b, n) {
const result = new Array(n * n).fill(0);
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
let sum = 0;
for (let k = 0; k < n; k++) {
sum += a[i * n + k] * b[k * n + j];
}
result[i * n + j] = sum;
}
}
return result;
}

function benchmark(name, wasmFn, jsFn, ...args) {
const wasmStart = performance.now();
const wasmResult = wasmFn(...args);
const wasmTime = performance.now() - wasmStart;

const jsStart = performance.now();
const jsResult = jsFn(...args);
const jsTime = performance.now() - jsStart;

const speedup = jsTime / wasmTime;

document.getElementById('results').innerHTML += `
<p><strong>${name}</strong></p>
<p>WASM: ${wasmTime.toFixed(2)}ms</p>
<p>JS: ${jsTime.toFixed(2)}ms</p>
<p>加速比: ${speedup.toFixed(2)}x</p>
<hr>
`;
}

document.getElementById('fib').addEventListener('click', () => {
benchmark('斐波那契(40)', fibonacci, jsFibonacci, 40);
});

document.getElementById('prime').addEventListener('click', () => {
benchmark('质数计数(1-1000000)', count_primes, jsCountPrimes, 1, 1000000);
});

document.getElementById('matrix').addEventListener('click', () => {
const n = 100;
const a = Array.from({ length: n * n }, () => Math.random());
const b = Array.from({ length: n * n }, () => Math.random());
benchmark('矩阵乘法(100x100)',
(a, b) => matrix_multiply(a, b, n),
(a, b) => jsMatrixMultiply(a, b, n),
a, b
);
});

await init();
</script>
</body>
</html>

案例3:解析器

实现一个简单的 JSON 解析器。

Rust 实现

use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};
use js_sys::Object;

#[derive(Serialize, Deserialize)]
pub struct Person {
name: String,
age: u32,
email: String,
}

#[wasm_bindgen]
pub fn parse_person(json: &str) -> Result<Person, JsValue> {
serde_json::from_str(json)
.map_err(|e| JsValue::from_str(&format!("Parse error: {}", e)))
}

#[wasm_bindgen]
pub fn serialize_person(person: &Person) -> Result<String, JsValue> {
serde_json::to_string(person)
.map_err(|e| JsValue::from_str(&format!("Serialize error: {}", e)))
}

#[wasm_bindgen]
pub struct JsonParser {
input: String,
pos: usize,
}

#[wasm_bindgen]
impl JsonParser {
#[wasm_bindgen(constructor)]
pub fn new(input: &str) -> Self {
JsonParser {
input: input.to_string(),
pos: 0,
}
}

pub fn parse_value(&mut self) -> Result<JsValue, JsValue> {
self.skip_whitespace();

let ch = self.peek().ok_or_else(|| JsValue::from_str("Unexpected end"))?;

match ch {
'"' => self.parse_string(),
'0'..='9' | '-' => self.parse_number(),
't' | 'f' => self.parse_boolean(),
'n' => self.parse_null(),
'[' => self.parse_array(),
'{' => self.parse_object(),
_ => Err(JsValue::from_str(&format!("Unexpected character: {}", ch))),
}
}

fn peek(&self) -> Option<char> {
self.input.chars().nth(self.pos)
}

fn skip_whitespace(&mut self) {
while let Some(ch) = self.peek() {
if ch.is_whitespace() {
self.pos += 1;
} else {
break;
}
}
}

fn parse_string(&mut self) -> Result<JsValue, JsValue> {
self.pos += 1; // skip opening quote
let mut result = String::new();

while let Some(ch) = self.peek() {
if ch == '"' {
self.pos += 1;
return Ok(JsValue::from_str(&result));
}
result.push(ch);
self.pos += 1;
}

Err(JsValue::from_str("Unterminated string"))
}

fn parse_number(&mut self) -> Result<JsValue, JsValue> {
let start = self.pos;

if self.peek() == Some('-') {
self.pos += 1;
}

while let Some(ch) = self.peek() {
if ch.is_ascii_digit() || ch == '.' {
self.pos += 1;
} else {
break;
}
}

let num_str: String = self.input.chars().skip(start).take(self.pos - start).collect();
let num: f64 = num_str.parse().map_err(|_| JsValue::from_str("Invalid number"))?;

Ok(JsValue::from_f64(num))
}

fn parse_boolean(&mut self) -> Result<JsValue, JsValue> {
if self.input[self.pos..].starts_with("true") {
self.pos += 4;
Ok(JsValue::TRUE)
} else if self.input[self.pos..].starts_with("false") {
self.pos += 5;
Ok(JsValue::FALSE)
} else {
Err(JsValue::from_str("Invalid boolean"))
}
}

fn parse_null(&mut self) -> Result<JsValue, JsValue> {
if self.input[self.pos..].starts_with("null") {
self.pos += 4;
Ok(JsValue::NULL)
} else {
Err(JsValue::from_str("Invalid null"))
}
}

fn parse_array(&mut self) -> Result<JsValue, JsValue> {
self.pos += 1; // skip '['
let mut arr = vec![];

self.skip_whitespace();
if self.peek() == Some(']') {
self.pos += 1;
return Ok(JsValue::from(js_sys::Array::new()));
}

loop {
let value = self.parse_value()?;
arr.push(value);

self.skip_whitespace();
match self.peek() {
Some(',') => self.pos += 1,
Some(']') => {
self.pos += 1;
break;
}
_ => return Err(JsValue::from_str("Expected ',' or ']'")),
}
}

Ok(js_sys::Array::from_iter(arr).into())
}

fn parse_object(&mut self) -> Result<JsValue, JsValue> {
self.pos += 1; // skip '{'
let obj = Object::new();

self.skip_whitespace();
if self.peek() == Some('}') {
self.pos += 1;
return Ok(obj.into());
}

loop {
self.skip_whitespace();
if self.peek() != Some('"') {
return Err(JsValue::from_str("Expected string key"));
}

let key = self.parse_string()?;
let key_str = key.as_string().unwrap();

self.skip_whitespace();
if self.peek() != Some(':') {
return Err(JsValue::from_str("Expected ':'"));
}
self.pos += 1;

let value = self.parse_value()?;
js_sys::Reflect::set(&obj, &JsValue::from_str(&key_str), &value);

self.skip_whitespace();
match self.peek() {
Some(',') => self.pos += 1,
Some('}') => {
self.pos += 1;
break;
}
_ => return Err(JsValue::from_str("Expected ',' or '}'")),
}
}

Ok(obj.into())
}
}

案例4:游戏物理引擎

实现简单的 2D 物理模拟。

Rust 实现

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
#[derive(Clone, Copy)]
pub struct Vector2 {
pub x: f64,
pub y: f64,
}

#[wasm_bindgen]
impl Vector2 {
#[wasm_bindgen(constructor)]
pub fn new(x: f64, y: f64) -> Self {
Vector2 { x, y }
}

pub fn add(&self, other: &Vector2) -> Vector2 {
Vector2 { x: self.x + other.x, y: self.y + other.y }
}

pub fn sub(&self, other: &Vector2) -> Vector2 {
Vector2 { x: self.x - other.x, y: self.y - other.y }
}

pub fn scale(&self, s: f64) -> Vector2 {
Vector2 { x: self.x * s, y: self.y * s }
}

pub fn length(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}

pub fn normalize(&self) -> Vector2 {
let len = self.length();
if len > 0.0 {
Vector2 { x: self.x / len, y: self.y / len }
} else {
*self
}
}
}

#[wasm_bindgen]
pub struct Particle {
position: Vector2,
velocity: Vector2,
acceleration: Vector2,
mass: f64,
}

#[wasm_bindgen]
impl Particle {
#[wasm_bindgen(constructor)]
pub fn new(x: f64, y: f64, mass: f64) -> Self {
Particle {
position: Vector2::new(x, y),
velocity: Vector2::new(0.0, 0.0),
acceleration: Vector2::new(0.0, 0.0),
mass,
}
}

pub fn apply_force(&mut self, force: &Vector2) {
let a = force.scale(1.0 / self.mass);
self.acceleration = self.acceleration.add(&a);
}

pub fn update(&mut self, dt: f64) {
self.velocity = self.velocity.add(&self.acceleration.scale(dt));
self.position = self.position.add(&self.velocity.scale(dt));
self.acceleration = Vector2::new(0.0, 0.0);
}

pub fn get_x(&self) -> f64 { self.position.x }
pub fn get_y(&self) -> f64 { self.position.y }
}

#[wasm_bindgen]
pub struct PhysicsWorld {
particles: Vec<Particle>,
gravity: Vector2,
}

#[wasm_bindgen]
impl PhysicsWorld {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
PhysicsWorld {
particles: Vec::new(),
gravity: Vector2::new(0.0, 9.8),
}
}

pub fn add_particle(&mut self, x: f64, y: f64, mass: f64) -> usize {
let particle = Particle::new(x, y, mass);
self.particles.push(particle);
self.particles.len() - 1
}

pub fn step(&mut self, dt: f64) {
for particle in &mut self.particles {
particle.apply_force(&self.gravity.scale(particle.mass));
particle.update(dt);
}
}

pub fn get_particle_x(&self, index: usize) -> f64 {
self.particles[index].get_x()
}

pub fn get_particle_y(&self, index: usize) -> f64 {
self.particles[index].get_y()
}

pub fn get_particle_count(&self) -> usize {
self.particles.len()
}
}

Canvas 渲染

<!DOCTYPE html>
<html>
<head>
<title>物理引擎</title>
<style>
canvas { border: 1px solid black; }
</style>
</head>
<body>
<canvas id="canvas" width="800" height="600"></canvas>
<button id="add">添加粒子</button>

<script type="module">
import init, { PhysicsWorld } from './pkg/physics.js';

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

let world = null;

function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);

const count = world.get_particle_count();
for (let i = 0; i < count; i++) {
const x = world.get_particle_x(i);
const y = world.get_particle_y(i);

ctx.beginPath();
ctx.arc(x, y, 10, 0, Math.PI * 2);
ctx.fillStyle = 'blue';
ctx.fill();
}
}

function gameLoop() {
world.step(0.016);
render();
requestAnimationFrame(gameLoop);
}

document.getElementById('add').addEventListener('click', () => {
const x = Math.random() * canvas.width;
const y = 50;
world.add_particle(x, y, 1.0);
});

async function main() {
await init();
world = new PhysicsWorld();

// 添加初始粒子
for (let i = 0; i < 10; i++) {
world.add_particle(
100 + i * 60,
50,
1.0
);
}

gameLoop();
}

main();
</script>
</body>
</html>

下一步

通过这些实战案例,你应该对 WebAssembly 的应用场景有了更深入的了解。继续探索:

  • 速查表 - WebAssembly 常用语法和 API 速查