偶然一次机会,接触了Rust的代码。当时想给团队小伙伴做演示,发现自己并不能在移动端按照文档生成演示demo。我就想,要是Rust代码能转化成JavaScript就好了。结果一搜,还真有。
下面整理成文档,分享给大家。为大家解决问题,多提供一种思路、方式、方法。
一、分享的目的:
由 Rust、WebAssembly、JavaScript、HTML 和 CSS 开发多语言程序的工作流程。
如何设计 API 以最大限度地利用 Rust 和 WebAssembly 的优势以及 JavaScript 的优势。
如何调试从 Rust 编译的 WebAssembly 模块。
二、什么是WebAssembly?
WebAssembly (wasm) 是一种具有广泛规范的简单机器模型和可执行格式。它被设计为可移植、紧凑并以本机速度或接近本机速度执行。
.wat文本格式(称为wat“WebAssemblyText”)使用S表达式,与 Scheme 和 Clojure等Lisp 语言家族有相似之处。https://en.wikipedia.org/wiki/S-expression
.wasm是较低级别的,旨在供 wasm 虚拟机直接使用。它在概念上类似于 ELF 和 Mach-O。
.wat文本格式到.wasm二进制格式的转换。
三、环境准备:
rustup、rustc和cargo。
四、学习网站:
五、练习演示:
wasm-pack是一个帮助你构建和打包Rust代码到WebAssembly的工具。
cargo install wasm-pack
cargo new --lib my_democd my_demo
Cargo.toml文件中添加wasm-bindgen和web-sys依赖项。
[package]authors = ["The wasm-demo Developers"]edition = "2024"name = "wasm-in-web-worker"publish = falseversion = "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特性。更多配置,参考学习文档。
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 函数。
wasm-pack构建项目,生成可以在Web环境中运行的WebAssembly包。
wasm-pack build --target no-modules
.wasm文件。
<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();
python3 -m http.server
http://localhost:8000,你应该能看到【Rust 和 WebAssembly 与现有的 JavaScript 工具集成】的网站。
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 等。
七、应用场景:
八、小结:
-
性能提升:Rust 编译到 WebAssembly 可以提供接近原生的性能,特别是在计算密集型任务中,这通常比 JavaScript 执行得更快。 -
类型安全:Rust 是一种静态类型语言,提供了编译时类型检查,这有助于减少运行时错误。 -
内存安全:Rust 的所有权和借用机制确保了内存安全,没有垃圾收集器的开销,这在 WebAssembly 中同样适用。 -
并发编程:Rust 的并发编程模型比 JavaScript 的并发模型(基于事件循环和回调)更为强大和灵活。 -
现代工具链:Rust 的`cargo`工具链和`wasm-pack`等工具提供了强大的依赖管理和构建工具。 -
生态系统:Rust 的生态系统正在快速增长,提供了大量的库和框架。 -
跨平台兼容性:WebAssembly 是跨平台的,可以在所有主流浏览器上运行。
-
学习曲线:对于熟悉 JavaScript 的开发者来说,Rust 的学习曲线可能会比较陡峭。 -
工具集成:尽管 Rust 和 WebAssembly 的工具正在改进,但它们与现有的 JavaScript 工具和生态系统(如 npm, webpack 等)的集成可能不如纯 JavaScript 项目那样无缝。 -
启动时间和文件大小:WebAssembly 模块可能需要额外的加载时间,尤其是当模块很大时。虽然 Wasm 文件通常比等效的 JavaScript 文件小,但是需要额外的解析和编译时间。 -
DOM 和 Web API 交互:直接从 Rust/WebAssembly 与 DOM 进行交互比从 JavaScript 进行交互更复杂,通常需要通过 JavaScript 中间层或使用像`web-sys`这样的库。 -
调试支持:虽然 WebAssembly 的调试工具在不断改进,但它们通常不如 JavaScript 的调试工具成熟和易用。 -
社区和资源:JavaScript 拥有一个庞大的社区和大量的资源,而 Rust 和 WebAssembly 相对较新,社区和资源可能没有那么丰富。 -
浏览器兼容性:虽然 WebAssembly 在所有现代浏览器上都得到了支持,但在一些旧的浏览器或者某些移动设备上可能不被支持。 总的来说,Rust 和 WebAssembly 在性能和安全性方面提供了显著的优势,但在易用性、工具集成和社区支持方面可能存在一些挑战。对于需要高性能计算的应用程序,或者那些对安全性有严格要求的项目,使用Rust和 WebAssembly 可能是一个很好的选择。然而,对于需要快速开发和广泛社区支持的项目,纯JavaScript 解决方案可能更加合适。
扫一扫,加入技术交流群


