之前的文章:
本章主要介绍 rust 在浏览器中以 WebAssembly 形式运行的基本方法和编程技巧。
编译到 WebAssembly
rust 支持以 WebAssembly 作为编译目标,从而在浏览器中运行。这样,可以使用 rust 来编写一些逻辑复杂的模块,代替性能和底层编程体验不佳的 JavaScript 。
想要在浏览器中运行 rust 代码,首先需要安装 wasm-pack 工具:
https://rustwasm.github.io/wasm-pack/
然后在 Cargo.toml 中加入 cdylib 编译目标和几个常用的依赖项:
[lib]crate-type = ["rlib", "cdylib"] # 必须包含 cdylib[dependencies]wasm-bindgen = "0.2" # WebAssembly 辅助工具js-sys = "0.3" # JavaScript 接口定义console_error_panic_hook = "0.1" # 错误输出辅助工具log = "0.4"console_log = "0.2" # 将日志输出到 console 的工具# Web 接口定义[dependencies.web-sys]version = "0.3"features = [ # 必须罗列出需要用到的 Web 对象"Window","Document","HtmlElement","Element",]
这个 crate 本质上是一个 lib crate ,所以入口文件是 src/lib.rs ,在其中可以有一个起始函数,例如:
#[macro_use] extern crate log;use wasm_bindgen::prelude::*;// 这个函数会在初始化时执行#[wasm_bindgen(start)]pub fn wasm_main() {// 初始化错误输出模块console_error_panic_hook::set_once();// 初始化日志模块console_log::init_with_level(log::Level::Trace).unwrap();// 输出一句日志(显示在浏览器 console 中)debug!("Initialized!");}
编译这个模块时,通常可以使用 wasm-pack build 命令,不过这样编译出来的模块还需要使用 webpack 等 web 打包工具来进一步处理。如果只需要简单测试的话,可以使用这个命令来快速生成可直接在 HTML 中调用的文件:
wasm-pack build --target no-modules
它会在代码 pkg 子目录下生成编译结果。
加载 WebAssembly 模块
生成的编译结果,最主要的是一个 js 文件和一个 wasm 文件。它们需要使用一个 HTML 文件来引导启动(其中需要正确指定这两个文件的路径),例如 index.html :
<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body></body><!-- my_wasm 是 crate 名称 --><script src="pkg/my_wasm.js"></script><script>wasm_bindgen('pkg/my_wasm_bg.wasm')</script></html>
然后,启动一个 HTTP 服务器使这些文件能被正确加载。如果本地没有易用的 HTTP 服务器,可以安装 basic-http-server 工具:
cargo install basic-http-server
然后在 HTML 文件所在的目录中启动它:
basic-http-server -x
在浏览器中打开 http://127.0.0.1:4000/ 就可以载入这个 HTML 文件了。
这个 HTML 载入时,会同时加载上面编译好的 rust 模块并执行初始化函数、在浏览器的 F12 console 中输出日志。
使用 web 接口
需要注意的是,部分标准库接口不能在浏览器环境中使用,如 println! 和多线程。但 web 接口是可用的。
标准的 web 接口可以使用 web_sys 来引用,例如:
#[macro_use] extern crate log;use wasm_bindgen::prelude::*;#[wasm_bindgen(start)]pub fn wasm_main() {console_error_panic_hook::set_once();console_log::init_with_level(log::Level::Trace).unwrap();debug!("Initialized!");// 获取浏览器 window 对象let window = web_sys::window().unwrap();// 获取浏览器 document 对象let document = window.document().unwrap();// 获取 document.bodylet body = document.body().unwrap();// 写 body.innerHTMLbody.set_inner_html("Hello world!");}
web_sys 中的各个接口名称与对应的 web 接口相仿,不过有更为明确的类型限制。具体接口定义可检索它的文档。
https://rustwasm.github.io/wasm-bindgen/api/web_sys/index.html
如果调用的是 JavaScript 的内置对象,如 Date 对象、 Math 对象,则需要使用 js_sys ,例如:
#[macro_use] extern crate log;use wasm_bindgen::prelude::*;#[wasm_bindgen(start)]pub fn wasm_main() {console_error_panic_hook::set_once();console_log::init_with_level(log::Level::Trace).unwrap();// 获取当前时间戳let timestamp = js_sys::Date::now();debug!("{}", timestamp);}
导入自定义接口
如果想要使用 JavaScript 编写的接口,可以在 rust 中添加接口定义后使用。例如编写了一个 JavaScript aPlusB 函数:
<script src="pkg/my_wasm.js"></script><script>function aPlusB(a, b) {return a + b}wasm_bindgen('pkg/my_wasm_bg.wasm')</script>
可以在 rust 中声明这个函数,然后调用它:
#[macro_use] extern crate log;use wasm_bindgen::prelude::*;#[wasm_bindgen]extern {// js 函数定义#[wasm_bindgen(js_name = "aPlusB")]fn a_plus_b(a: i32, b: i32) -> i32;}#[wasm_bindgen(start)]pub fn wasm_main() {console_error_panic_hook::set_once();console_log::init_with_level(log::Level::Trace).unwrap();// 调用 js 函数let sum = a_plus_b(2, 3);debug!("{}", sum);}
导出接口
rust 也可以提供接口给 JavaScript 调用。例如:
#[macro_use] extern crate log;use wasm_bindgen::prelude::*;// 导出一个 struct#[wasm_bindgen]struct Adder {sum: i32,}#[wasm_bindgen]impl Adder {// 构造器#[wasm_bindgen(constructor)]pub fn new() -> Self {Self {sum: 0,}}// 导出一个函数#[wasm_bindgen]pub fn add(&mut self, n: i32) -> i32 {self.sum += n;self.sum}}#[wasm_bindgen(start)]pub fn wasm_main() {console_error_panic_hook::set_once();console_log::init_with_level(log::Level::Trace).unwrap();}
上面定义的接口就可以在 JavaScript 中调用:
<script src="pkg/my_wasm.js"></script><script>wasm_bindgen('pkg/my_wasm_bg.wasm').then(() => {// 加载完成后可以调用 rust 导出的接口const adder = new wasm_bindgen.Adder()adder.add(2)console.log(adder.add(3)) // 输出 5})</script>
rust 可以实现浏览器和服务器端的代码复用。不过,浏览器环境下编程依然是一个不同的编程领域,有很多不一样的问题和挑战。关于 rust 的浏览器环境编程相关内容,后面的文章将有更多细节介绍。




