Rust + WebAssembly 新手完全入门指南
这篇文章面向前端、Rust 开发者,只要跟着步骤就能跑通你的第一个 WebAssembly 前端组件。
WebAssembly 是什么
WebAssembly(简称 Wasm)是一种可在现代浏览器中运行的低级、紧凑、高效的二进制指令格式。它的出现主要是用来解决 JavaScript 天生的性能瓶颈,尤其是计算密集型场景下,WebAssembly 能达到接近原生的执行性能。
可以说,WebAssembly 的出现,使得在网页上运行高级、复杂的客户端应用(如图像编辑、Web AI 推理、复杂的游戏引擎)成为可能。
通过 caniuse,我们可以看到 WebAssembly 在主流浏览器中已经广泛可用,如下图所示:
环境准备
首先,确保你已经安装好 Nodejs 和 Rust 环境。这里我们会用到 wasm-pack,wasm-pack是 Rust Wasm 开发的核心工具,负责构建、打包、测试 Wasm 项目。
# 安装 wasm-packcargoinstallwasm-pack接下来,我们使用wasm-pack初始化项目:
# 初始化项目wasm-pack new wasm-examplecdwasm-examplewasm-pack会帮我们生成如下的目录结构:
.├── Cargo.toml ├── LICENSE_APACHE ├── LICENSE_MIT ├── README.md ├── src │ ├── lib.rs │ └── utils.rs └── tests └── web.rswasm-pack初始化项目时默认使用的是 wasm-pack-template 这个模板,由于缺少维护,所以默认使用的Edition、wasm-bindgen 等的版本都比较低,你可以手动调整下,但是在这个案例中,我们就暂时不动它们了。
在以后的开发中,你可以维护一个自己的模板,然后通过如下命令使用模板:
wasm-pack new<name>--template<template>实现一个简单 Wasm 组件
我们首先实现一个简单 Wasm 组件,我们将会用到 js-sys 这个库,它包含 JavaScript 语言本身的 API,如数组、字符串、console、Promise 等。
# 添加 js-syscargoaddjs-sys现在我们直接修改src/lib.rs文件:
# 默认模板里有 src/utils.rs,这里引入了它modutils;usewasm_bindgen::prelude::*;#[wasm_bindgen]pubfnsum_array(arr:&js_sys::Array)->u32{letmutsum=0;foriin0..arr.length(){letval=arr.get(i).as_f64().unwrap_or(0.0)asu32;sum+=val;}sum}#[wasm_bindgen]属性宏声明了将导出sum_array函数以供 JS 调用,这就是我们这个简单组件提供的能力。接下来,我们进行编译:
wasm-pack build--targetweb编译完成后,我们可以在pkg目录中看到编译结果:
pkg ├── package.json ├── README.md ├── wasm_example_bg.js ├── wasm_example_bg.wasm ├── wasm_example_bg.wasm.d.ts ├── wasm_example.d.ts └── wasm_example.jswasm_example_bg.wasm是核心的 Wasm 二进制文件,包含我们编写的 Rust 逻辑;wasm_example.js是 JS 胶水代码,负责 Wasm 的加载、类型转换,把 Rust 函数封装成 JS 可直接调用的方法;wasm_example.d.ts是 TypeScript 类型定义,用于支持 TS 项目。
这里的--target web表示我们的目标编译平台是 web,这样方便在 HTML 文件中引入。在根目录创建index.html并编辑:
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Rust + WebAssembly</title></head><body><scripttype="module">importinit,{sum_array}from'./pkg/wasm_example.js';awaitinit();console.log("求和结果为:",sum_array([10,20,30]));</script></body></html>由于浏览器的安全策略不允许加载本地 Wasm 模块,所以无法使用file://协议打开 HTML 文件,我们需要一个本地 HTTP 服务器来启动项目。
npx serve.这时候,打开浏览器的控制台就能够看到打印出来的求和结果了。
使用 Rust 操作 DOM
借助 web-sys,我们可以直接用 Rust 操作 DOM。与js-sys不同,web-sys包含浏览器环境提供的 Web API。
cargoaddweb-sys--features"Window,Document,HtmlElement"这里需要注意的是,web-sys必须指定启用的feature,不然编译不通过。
现在我们通过web-sys来修改网页标题,编辑src/lib.rs,添加:
#[wasm_bindgen]pubfnset_title(title:&str){// 1. 获取 window 对象letwin=matchweb_sys::window(){Some(w)=>w,None=>return,};// 获取 document 对象letdoc=matchwin.document(){Some(d)=>d,None=>return,};// 修改网页标题doc.set_title(title);}重新编译完成后,我们修改index.html,添加:
<scripttype="module">importinit,{set_title,sum_array}from'./pkg/wasm_example.js';awaitinit();console.log("求和结果为:",sum_array([10,20,30]));set_title("Hello world");</script>刷新网页后,我们就能够看到网页的标题被修改为Hello world了。
接入 Vite
在实际开发中,我们很少直接写原生 HTML,基本上是基于 Vite、Webpack 等构建工具进行开发,这里以最常用的 Vite 为例,演示如何集成 Rust Wasm。
npmcreate vite@latest wasm-vite-example ----templatevanilla-tscdwasm-vite-example把之前的wasm-example项目整个拷贝到wasm-vite-example的根目录。在实际开发中,这种时候肯定是要用 monorepo 的,但在这个案例中就不这么弄了,一切从简。
.├── .gitignore ├── index.html ├── node_modules ├── package-lock.json ├── package.json ├── public ├── src ├── tsconfig.json └── wasm-example安装插件:
npminstall-Dvite-plugin-wasm-pack创建vite.config.ts并编辑:
import{defineConfig}from'vite';importwasmPackfrom'vite-plugin-wasm-pack';exportdefaultdefineConfig({plugins:[wasmPack('./wasm-example')]});接下来,在src/main.ts中添加上我们之前写的业务逻辑,如下:
importinit,{set_title,sum_array}from'wasm_example'asyncfunctioninitWasm(){awaitinit();console.log("求和结果为:",sum_array([10,20,30]));set_title("Hello world");}initWasm();启动开发服务:
npmrun dev打开浏览器就能看到 Wasm 函数的执行结果了。
结尾
至此,我们就成功实现了第一个 WebAssembly 前端组件,虽然它很简陋,没有涉及到:
- 测试与调试,如:用
wasm-pack test进行单元测试、使用浏览器 DevTools 调试 Wasm 代码; - Wasm 高级特性,如共享内存、多线程、SIMD 指令等;
- 性能优化与踩坑。
不过没有关系,这些内容在后续的文章更新中可能会涉及到。现在,我们需要做的是动手实现一遍这个简单的 Wasm 组件。毕竟对于新手而言,最好的学习方式就是动手实践。