之前的文章:
本章主要介绍 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.body
let body = document.body().unwrap();
// 写 body.innerHTML
body.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 的浏览器环境编程相关内容,后面的文章将有更多细节介绍。