JavaScript 与 Rust 和 WebAssembly 集成
偶然一次机会,接触了Rust的代码。当时想给团队小伙伴做演示,发现自己并不能在移动端按照文档生成演示demo。我就想,要是Rust代码能转化成JavaScript就好了。结果一搜,还真有。
下面整理成文档,分享给大家。为大家解决问题,多提供一种思路、方式、方法。
一、分享的目的:
二、什么是WebAssembly?
WebAssembly (wasm) 是一种具有广泛规范的简单机器模型和可执行格式。它被设计为可移植、紧凑并以本机速度或接近本机速度执行。
作为一种编程语言,WebAssembly 由两种表示相同结构的格式组成,尽管方式不同:
.wat
文本格式(称为wat
“WebAssemblyText”)使用S 表达式,与 Scheme 和 Clojure等Lisp 语言家族有相似之处。https://en.wikipedia.org/wiki/S-expression.wasm
是较低级别的,旨在供 wasm 虚拟机直接使用。它在概念上类似于 ELF 和 Mach-O。 有工具,可以从.wat
文本格式到.wasm
二进制格式的转换。
三、环境准备:
需要标准 Rust 工具链,包括rustup
、rustc
和cargo
。
安装参考:https://www.rust-lang.org/tools/install
四、学习网站:
Rust 和 WebAssembly
wasm-bindgen官网地址
五、练习演示:
下面这段代码项目是用 Rust + JavaScript 编写的,用于 WebAssembly (Wasm) 项目,它与 Web Workers 和 Web 页面交互。代码的主要功能是判断用户输入的数字是否为偶数,并将结果显示在网页上。
1、安装wasm-pack: wasm-pack
是一个帮助你构建和打包Rust代码到WebAssembly的工具。
cargo install wasm-pack
2、创建一个新的Rust库项目:
cargo new --lib my_demo
cd my_demo
此时,生成文件目录:

3、配置Cargo.toml: 在Cargo.toml
文件中添加wasm-bindgen
和web-sys
依赖项。
[package]
authors = ["The wasm-demo Developers"]
edition = "2024"
name = "wasm-in-web-worker"
publish = false
version = "0.0.0"
[lib]
crate-type = ["cdylib"]
[dependencies]
console_error_panic_hook = { version = "0.1.6", optional = true }
wasm-bindgen = "0.2"
web-sys = { version = "0.3", features = ['console',
'Document',
'HtmlElement',
'HtmlInputElement',
'MessageEvent',
'Window',
'Worker'] }
在features
中,你可以根据需要启用web-sys
的特定Web API特性。更多配置,参考学习文档。
4、编写Rust代码: 在src/lib.rs
中使用web-sys
。
// 代码首先导入了一些 Rust 标准库和 wasm_bindgen 相关的模块,这些模块用于在 Rust 和 JavaScript 之间建立桥梁,以及操作 Web API。
use std::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use web_sys::{console, HtmlElement, HtmlInputElement, MessageEvent, Worker};
// 定义 NumberEval 结构体
// NumberEval 结构体用于存储一个整数,并提供方法来判断该整数是否为偶数。
// new 方法创建 NumberEval 的新实例,初始数字为 0。
// is_even 方法接受一个整数参数,将其存储在结构体中,并返回该数字是否为偶数。
// get_last_number 方法返回结构体中存储的最后一个数字
#[wasm_bindgen]
pub struct NumberEval {
number: i32,
}
#[wasm_bindgen]
impl NumberEval {
// Create new instance.
pub fn new() -> NumberEval {
NumberEval { number: 0 }
}
pub fn is_even(&mut self, number: i32) -> bool {
self.number = number;
self.number % 2 == 0
}
pub fn get_last_number(&self) -> i32 {
self.number
}
}
// startup 函数是在 Wasm 模块加载时调用的入口点。它创建了一个 Web Worker 实例,并设置了一个输入框的 oninput 事件回调。
#[wasm_bindgen]
pub fn startup() {
// 创建Web Worker实例
let worker_handle = Rc::new(RefCell::new(Worker::new("./worker.js").unwrap()));
console::log_1(&"Created a new worker from within Wasm".into());
setup_input_oninput_callback(worker_handle);
}
// 定义 setup_input_oninput_callback 函数
// 这个函数设置了一个回调函数,当用户在输入框中输入时触发。它读取输入框的值,尝试将其解析为整数,并将该整数发送到 Web Worker。
// 如果解析失败,它会清空结果显示字段。
fn setup_input_oninput_callback(worker: Rc<RefCell<web_sys::Worker>>) {
let document = web_sys::window().unwrap().document().unwrap();
// #[allow(unused_assignments)] 属性被用来告诉编译器忽略未使用的赋值警告。这样,即使value变量被赋值后没有被使用,编译器也不会发出警告。
#[allow(unused_assignments)]
let mut persistent_callback_handle = get_on_msg_callback();
let callback = Closure::new(move || {
console::log_1(&"oninput callback triggered".into());
let document = web_sys::window().unwrap().document().unwrap();
let input_field = document
.get_element_by_id("inputNumber")
.expect("#inputNumber should exist");
let input_field = input_field
.dyn_ref::<HtmlInputElement>()
.expect("#inputNumber should be a HtmlInputElement");
match input_field.value().parse::<i32>() {
Ok(number) => {
// 代码中的 Web Worker 交互包括创建 Worker 实例、发送消息给 Worker (post_message),以及设置 Worker 的 onmessage 事件处理器来接收 Worker 的响应。
let worker_handle = &*worker.borrow();
let _ = worker_handle.post_message(&number.into());
persistent_callback_handle = get_on_msg_callback();
worker_handle
.set_onmessage(Some(persistent_callback_handle.as_ref().unchecked_ref()));
}
Err(_) => {
document
.get_element_by_id("resultField")
.expect("#resultField should exist")
.dyn_ref::<HtmlElement>()
.expect("#resultField should be a HtmlInputElement")
.set_inner_text("");
}
}
});
document
.get_element_by_id("inputNumber")
.expect("#inputNumber should exist")
.dyn_ref::<HtmlInputElement>()
.expect("#inputNumber should be a HtmlInputElement")
.set_oninput(Some(callback.as_ref().unchecked_ref()));
// forget 方法用于防止 Rust 清理闭包,因为闭包将由 JavaScript 管理。
callback.forget();
}
// 定义 get_on_msg_callback 函数
// 这个函数创建了一个闭包,用于处理从 Web Worker 返回的消息。
// 它接收一个 MessageEvent,从中提取数据,并根据数据是 true 还是 false 来更新页面上的结果显示字段,显示 "even" 或 "odd"。
fn get_on_msg_callback() -> Closure<dyn FnMut(MessageEvent)> {
Closure::new(move |event: MessageEvent| {
console::log_2(&"Received response: ".into(), &event.data());
let result = match event.data().as_bool().unwrap() {
true => "even",
false => "odd",
};
let document = web_sys::window().unwrap().document().unwrap();
document
.get_element_by_id("resultField")
.expect("#resultField should exist")
.dyn_ref::<HtmlElement>()
.expect("#resultField should be a HtmlInputElement")
.set_inner_text(result);
})
}
注意事项:
Closure::new
和 Closure::forget
用于创建和管理 Rust 和 JavaScript 之间的闭包。Rc<RefCell<>>
用于共享对 Worker 的可变引用,允许在多个地方修改 Worker 的状态。wasm_bindgen
宏用于将 Rust 代码暴露给 JavaScript,使得 JavaScript 可以调用 Rust 函数。
5、构建项目: 使用wasm-pack
构建项目,生成可以在Web环境中运行的WebAssembly包。
wasm-pack build --target no-modules
--target 后面可以跟的参数,如下图

6、在Web页面中使用: 创建一个HTML文件,并在其中引入生成的.wasm
文件。
index.html如下:
<html>
<head>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="wrapper">
<h1>与Wasm Web Worker 交互</h1>
<input type="text" id="inputNumber">
<div id="resultField"></div>
</div>
<script src='./pkg/wasm_in_web_worker.js'></script>
<script src="./index.js"></script>
</body>
</html>
// index.js
// `#[wasm_bindgen]`
const {startup} = wasm_bindgen;
async function run_wasm() {
// 加载 Wasm 文件
// 在`index.html`里导入了`wasm_bindgen`
await wasm_bindgen();
console.log('index.js loaded');
// 运行入口方法
// 创建worker实例
startup();
}
run_wasm();
// worker.js
// 这段代码包含 Web Worker 的实现细节, worker.js 接收到数字后,会判断它是否为偶数,并将结果发送回主线程。
importScripts('./pkg/wasm_in_web_worker.js');
console.log('Initializing worker')
// In the worker, we have a different struct that we want to use as in
// `index.js`.
const {NumberEval} = wasm_bindgen;
async function init_wasm_in_worker() {
// Load the Wasm file by awaiting the Promise returned by `wasm_bindgen`.
await wasm_bindgen('./pkg/wasm_in_web_worker_bg.wasm');
// Create a new object of the `NumberEval` struct.
var num_eval = NumberEval.new();
// Set callback to handle messages passed to the worker.
self.onmessage = async event => {
// By using methods of a struct as reaction to messages passed to the
// worker, we can preserve our state between messages.
var worker_result = num_eval.is_even(event.data);
// Send response back to be handled by callback in main thread.
self.postMessage(worker_result);
};
};
init_wasm_in_worker();
7、启动一个HTTP服务器: 你可以使用任何HTTP服务器来提供你的页面和WebAssembly模块。例如,如果你已经安装了Python,可以使用以下命令:
python3 -m http.server
然后在浏览器中打开http://localhost:8000
,你应该能看到【Rust 和 WebAssembly 与现有的 JavaScript 工具集成】的网站。

8、整体文件目录如下:

六、其他相关工具:
1、wasm-bindgen
wasm-bindgen
促进 Rust 和 JavaScript 之间的高级交互。它允许将 JavaScript 内容导入 Rust 并将 Rust 内容导出到 JavaScript。
2、wasm-bindgen-futures
wasm-bindgen-futuresPromise
是连接 JavaScript和 Rust 的桥梁Future
。它可以双向转换,在 Rust 中处理异步任务时非常有用,并允许与 DOM 事件和 I/O 操作进行交互。
3、js-sys
所有 JavaScript 全局类型和方法的原始wasm-bindgen
导入,例如Object
、等。这些 APIFunction
可eval
在所有标准 ECMAScript 环境中移植,而不仅仅是 Web,例如 Node.js。
4、web-sys
wasm-bindgen
所有 Web API 的原始导入,例如 DOM 操作setTimeout
、Web GL、Web Audio 等。
七、应用场景:
JavaScript 与 Rust 和 WebAssembly (Wasm) 的集成可以应用于多种场景,特别是在需要高性能和/或低级系统访问的情况下。以下是一些具体的应用场景:
在集成 Rust 和 WebAssembly 到 JavaScript 项目中时,通常会使用 JavaScript 作为“胶水代码”,处理 DOM 操作、网络请求等,而将计算密集型或需要优化性能的部分交给 Rust 编写的 WebAssembly 模块处理。这种方式可以结合 Rust 的性能和安全性以及 JavaScript 的灵活性和生态系统。
八、小结:
1、主要是为大家解决问题,多提供一种思路、方式、方法;
2、Rust 和 WebAssembly与JavaScript集成优势:
3、Rust 和 WebAssembly与JavaScript集成劣势:
总的来说,Rust 和 WebAssembly 在性能和安全性方面提供了显著的优势,但在易用性、工具集成和社区支持方面可能存在一些挑战。对于需要高性能计算的应用程序,或者那些对安全性有严格要求的项目,使用 Rust 和 WebAssembly 可能是一个很好的选择。然而,对于需要快速开发和广泛社区支持的项目,纯JavaScript 解决方案可能更加合适。
欢迎大家留言或私信我,我们共同探讨、学习,并且相互提供宝贵的反馈和建议。