使用 Dioxus eval 在 Rust 中执行 JavaScript:跨平台双向通信完全指南
【免费下载链接】dioxusFullstack app framework for web, desktop, and mobile.项目地址: https://gitcode.com/GitHub_Trending/di/dioxus
在 Dioxus 桌面、移动端、Web 与 Liveview 渲染器中,eval允许你以 Rust 代码直接执行一段 JavaScript 逻辑,并通过异步通道把数据从 Rust 发送到 JavaScript、或从 JavaScript 接收回 Rust。本篇指南以 packages/document/docs/eval.md 为核心,结合 dioxus_document 的源码实现,完整讲解document::eval的四种典型用法——执行与取值、向 JS 发送数据、从 JS 接收数据、与已挂载 DOM 交互——并深入剖析其底层信道机制、各渲染器的实现差异与安全注意事项。读完本文,你可以在自己的 Dioxus 应用中安全、高效地桥接 Rust 与 JavaScript 生态。
eval 是什么:跨渲染器的 JavaScript 执行入口
eval在 Dioxus 中是一个"全局可用"的实用函数。无论是桌面端(基于系统 WebView)、移动端、Web(WASM)还是 Liveview 渲染器,只要渲染器本身具备执行 JavaScript 的能力,就可以调用document::eval运行一段代码。它接收一段字符串形式的 JavaScript(允许包含async/await异步逻辑),并返回一个Eval对象。你通过该对象既可以等待 JS 代码执行完毕拿到返回值,也可以在 JS 运行期间与它双向交换数据。
在 Rust 侧,公开 API 的形态如下(摘自 packages/document/src/lib.rs):
/// Evaluate some javascript in the current document pub fn eval(script: &str) -> Eval { document().eval(script.to_string()) }从源码结构看,eval并不是一个把字符串简单交给解释器的黑盒,而是一套分层的设计:
document::eval(script)会调用当前渲染器通过上下文注入的Documenttrait 实现(见 Document trait 定义);- 返回的
Eval内部封装了由GenerationalBox托管的Box<dyn Evaluator>,真正"跑 JS 的人"是各渲染器提供的Evaluator实现(见 packages/document/src/eval.rs)。
这也是为什么当某个渲染器不支持 JS 时(例如纯 Native 渲染器),Eval上的操作会返回EvalError::Unsupported。这一点在仓库示例的文档注释中有明确说明:
Eval will only work with renderers that support javascript - so currently only the web and desktop/mobile renderers that use a webview. Native renderers will throw "unsupported" errors when calling
eval。(见 examples/08-apis/eval.rs)
最简单的例子:执行并取回结果
下面是官方文档给出的最小示例(节选自 packages/document/docs/eval.md),点击按钮时在渲染器里执行一段 JS:
use dioxus::prelude::*; fn App() -> Element { rsx! { button { onclick: move |_| async move { // Eval 是 Dioxus 内部的全局函数,在任何位置都可以调用,它会执行给定的 JavaScript 代码。 let result = document::eval(r#"console.log("Hello World"); return "Hello World";"#); // 使用 `await` 关键字等待 JavaScript 代码返回结果。 println!("{:?}", result.await); }, "Log Hello World" } } }要点说明:
document是dioxus::prelude中导出的模块别名,对应 dioxus_document 中的dioxus_document;- 被执行的 JS 通过
return返回serde_json::Value,Rust 端await后可直接解包、反序列化; Eval实现了IntoFuture,因此result.await是合法写法。在 packages/document/src/eval.rs 中可以看到IntoFuture的Output被定义为Result<serde_json::Value, EvalError>。
向 JavaScript 发送数据:格式化拼接与 send 通道
当你执行一段 JS 时,有两种方式把数据传给它:
- 启动前静态注入:把值
format!进 JS 字符串里,适合初始参数; - 运行时动态发送:借助
Eval::send,在 JS 执行期间把 Rust 数据推送到 JS 侧。
文档给出的"计数循环"示例把两种方式结合使用(节选自 packages/document/docs/eval.md):
use dioxus::prelude::*; fn app() -> Element { rsx! { button { onclick: move |_| { // 方式一:通过格式化把初始数据注入 JS 代码字符串。 const LOOP_COUNT: usize = 10; let eval = document::eval(&format!(r#"for(let i = 0; i < {LOOP_COUNT}; i++) {{ // 在 JS 侧使用 `await dioxus.recv()` 异步接收 Rust 发来的值。 let value = await dioxus.recv(); console.log("Received", value); }}"#)); // 方式二:在 Rust 侧用返回对象上的 `send` 方法把数据发给 JS。 for i in 0..LOOP_COUNT { eval.send(i).unwrap(); } }, "Log Count" } } }这里的 JS 侧 APIdioxus.recv()与 Rust 侧Eval::send是一一对应的:Rust 每调用一次send,JS 的await dioxus.recv()就消费一个值。也就是说,二者之间建立的是一条有先后顺序的消息队列而不是"共享内存"。
从底层实现看,Eval::send会把 Rust 数据先serde_json::to_value序列化,再交给平台Evaluator(见 packages/document/src/eval.rs);序列化失败时返回EvalError::Serialization。
从 JavaScript 接收数据:Eval::recv 与 dioxus.send
反过来,JS 侧也可以主动向 Rust 发送数据。通信方向与上一节正好对调:JS 使用dioxus.send(),Rust 使用Eval::recv()(注意recv需要可变借用&mut self,且是异步的,因为它要等待 JS 真正投递消息)。
官方文档的示例在 JS 里循环dioxus.send(i)发送 10 次,Rust 端循环等待接收(节选自 packages/document/docs/eval.md):
use dioxus::prelude::*; fn app() -> Element { rsx! { button { onclick: move |_| async move { // Rust → JS:使用 eval 返回对象上的 `send` 方法。 let mut eval = document::eval(r#"for(let i = 0; i < 10; i++) { // JS → Rust:使用 `dioxus.send()` 方法异步发送。 dioxus.send(i); }"#); // Rust 侧用 `recv` 方法接收 JS 发来的值。 for _ in 0..10 { let value: i32 = eval.recv().await.unwrap(); println!("Received {}", value); } }, "Log Count" } } }Eval::recv会把收到的serde_json::Value反序列化成你声明的具体类型(i32、String、Vec<T>等,任何实现了DeserializeOwned的类型均可),见 packages/document/src/eval.rs。仓库中 packages/desktop/headless_tests/eval.rs 有一段无头测试恰好验证了这种"你发我收、再翻倍回发"的全双工链路:
let mut eval = document::eval( r#"for (let i = 0; i < 100; i++) { let value = await dioxus.recv(); dioxus.send(value*2); }"#, ); for i in 0..100 { eval.send(i).unwrap(); let value: i32 = eval.recv().await.unwrap(); assert_eq!(value, i * 2); EVALS_RECEIVED.with_mut(|x| *x += 1); }这段测试同时证明了:往返 100 次消息全部按序到达,且返回值类型(此处是放大两倍后的i32)可以无缝反序列化。
双向信道的底层机制
为什么 JS 里会出现一个dioxus全局对象,而且它还有send/recv/close等方法?这与渲染器注入代码有关。以 Web 渲染器为例,Rust 侧在创建WebEvaluator时会把用户 JS 包进一个 Promise 包装器(见 packages/web/src/document.rs):
/// Required to avoid blocking the Rust WASM thread. const PROMISE_WRAPPER: &str = r#" return (async function(){ {JS_CODE} dioxus.close(); })(); "#;随后通过Function::new_with_args("dioxus", &code)构造一个接收dioxus参数并立即执行的 JS 函数,从而把真正的 JS 运行放到了异步 Promise 里,避免阻塞 Rust 的 WASM 线程;用户代码返回后自动调用dioxus.close()关闭信道。
通信所用的Channel本质是两条"带缓存 + 等待队列"的队列(见 packages/document/src/ts/eval.ts):
send(data):若有正在等待的resolve回调,立即唤醒;否则把数据压入pending;recv():若pending有数据,立即 resolve;否则把resolve挂进waiting等待。
Rust 与 JS 之间通过rustSend/rustRecv(Rust → JS / JS → Rust)与send/recv(JS ↔ JS 内部)组合实现全双工。桌面端则使用同一套 JS 信道脚本、但消息通过 WebView 的 IPC 传输(见 packages/desktop/src/document.rs 与 native_eval.js)。
序列化细节与返回数据类型
Eval各方法的返回值类型要求贯穿始终:
Eval::join<T: DeserializeOwned>:等待 JS 任务结束并把最终return值反序列化为T(packages/document/src/eval.rs);Eval::send(data: impl Serialize):Rust → JS,任意可序列化值;Eval::recv<T: DeserializeOwned>:JS → Rust,收任意可反序列化值。
而无头测试里还覆盖了一个"边角情况"——JS 代码没有任何return时,eval.await也能正常结束(return;返回undefined会被解析为空值而非报错),见 packages/desktop/headless_tests/eval.rs;而return [1, 2, 3]这种数组则可以被反序列化到Vec<i32>(同一文件 L43-L53)。
与已挂载 DOM 交互:eval + use_effect
eval另一大典型用途是读写 DOM。需要特别强调的是执行时机:只有在组件挂载之后 DOM 才真实存在,因此在组件函数体里直接调用eval是错误用法。官方文档(packages/document/docs/eval.md)给出的正确姿势是把它放进use_effect或事件处理器中:
use dioxus::prelude::*; const SCRIPT: &str = r#" let element = document.getElementById("my-element"); element.innerHTML = "Hello World"; return element.getAttribute("data-count"); "#; fn app() -> Element { // ❌ 不要在组件函数体里直接调用 eval。 // 这会在组件挂载之前执行,此时 DOM 尚不存在。 // document::eval(SCRIPT); // ✅ 应该把 eval 放进 effect 或事件里执行,确保组件已经挂载。 use_effect(move || { spawn(async { let count = document::eval(SCRIPT).await; println!("Count is {:?}", count); }); }); rsx! { div { id: "my-element", "data-count": "123", } } }这个例子把use_effect、spawn与eval组合在一起:effect 在挂载后触发,spawn把异步任务派生到后台,await等待 JS 对 DOM 的读操作返回。执行后控制台会打印Count is Ok("123")——Rust 侧读取到了 JS 从 DOM 属性data-count取回的值。
关于为什么不能直接在组件体内执行,源码中也有印证:Document::create_head_element的默认实现注释明确指出"元素只应在 effect 内创建,不能在组件挂起(suspended)时调用"(见 packages/document/src/document.rs),DOM 相关操作同理受挂载生命周期约束。
平台支持矩阵与渲染器差异
官方文档声明eval适用于 desktop、mobile、web 与 liveview 渲染器。结合源码,各端实现要点归纳如下:
| 渲染器 | Evaluator 实现位置 | 消息传输方式 | 备注 |
|---|---|---|---|
| Web / Fullstack Web | packages/web/src/document.rs | JSChannel+WebDioxusChannel,JS 以 Promise 包装执行 | 用户 JS 注入async function(dioxus),结束后dioxus.close() |
| Desktop(WebView) | packages/desktop/src/document.rs | 复用 JS 信道脚本,消息经window.ipc的 WebView IPC 传递 | JS 侧用window.ipc.postMessage发回 Rust |
| Liveview | packages/liveview/src/document.rs | 由 Liveview 文档实现提供 | —— |
| 纯 Native(无 WebView) | NoOpDocument(packages/document/src/document.rs) | 无 | 任何操作都返回EvalError::Unsupported |
需要注意:dioxus 的 re-export 结构里,dioxus::mobile与dioxus::desktop指向同一套实现(见 packages/dioxus/src/lib.rs),因此移动端走的是同一套基于 WebView 的信道方案。
另外,如果你的应用中没有渲染器注入Document(例如某些 SSR 或纯服务端上下文),document()会 fallback 到NoOpDocument并打印一条错误日志,此时调用eval得到的同样是EvalError::Unsupported(见 packages/document/src/lib.rs)。也就是说:eval 的有效性取决于运行时是否提供了支持 JS 的 Document 实现。
错误处理:EvalError 的五个分支
所有Eval方法都会返回Result<_, EvalError>。完整的错误类型定义在 packages/document/src/error.rs:
pub enum EvalError { /// 当前平台不支持执行 JavaScript。 Unsupported, /// 该段 JavaScript 已经被执行过了。 Finished, /// 提供的 JavaScript 非法、无法执行。 InvalidJs(String), /// Rust 与 JavaScript 通信期间发生错误。 Communication(String), /// 反序列化/序列化 eval 结果失败。 Serialization(serde_json::Error), }实践建议:
- 代码书写阶段最容易遇到的是
InvalidJs(JS 语法错误),它会在 JS 被编译时立刻暴露(见 packages/web/src/document.rs,该错误由Function::new的构造失败产生); - 跨端运行时,若目标渲染器不支持 JS(纯 Native、或误在无 Document 的 SSR 上下文调用),会得到
Unsupported; Finished出现在对已经结束的 eval 继续send/join/recv时——Rust 侧通过GenerationalBox的try_read/try_write检测 JS 任务是否已结束(packages/document/src/eval.rs);- 消息往返失败多为
Communication;类型不匹配则表现为Serialization。
因此调用方应尽量对Unsupported分支做降级处理(例如给出"当前平台不支持 eval"的提示),而不要假设所有目标都能执行 JS。
安全警告:XSS 边界
由于被执行的 JS 运行在渲染器环境中——尤其对 Web 目标,JS 上下文几乎可以访问你应用的绝大部分数据——只应执行你完全信任的代码。官方文档在 packages/document/docs/eval.md 中专门设置了 Safety 警告区块:如果执行不可信代码,可能引入跨站脚本(XSS)漏洞。
落地到工程实践,需要注意几点:
- 不要拼接不可信输入:用户输入、网络响应中的字符串如果被
format!进 JS 字符串,等同于把执行权交给了攻击者; - 对外部数据先净化:即使只是"放进 JS 字符串传参",也应做转义与白名单校验,而不是直接拼入;
- 最小化暴露面:eval 只开放给可信内部逻辑,JS 里不要顺手把敏感数据写入全局对象;
- 注意平台差异:桌面/移动 WebView 与 Web 的安全边界不同,Web 端 JS 拿到的是整个页面上下文,风险面最大。
更完整的实战:仓库示例 eval.rs
把上述 API 串起来的完整可运行示例位于 examples/08-apis/eval.rs。它在一个use_resource中演示了完整闭环:
// eval 需要等待 WebView/网页环境就绪 let mut eval = document::eval( r#" dioxus.send("Hi from JS!"); let msg = await dioxus.recv(); console.log(msg); return "hi from JS!"; "#, ); // 把消息发给 JS 侧(JS 里 await dioxus.recv() 会收到它) eval.send("Hi from Rust!").unwrap(); // 等待 JS 的 return 值 let res: String = eval.recv().await.unwrap(); println!("{:?}", eval.await);运行该示例后,渲染器控制台会出现两端互相问候的日志,页面则展示 JS 的返回字符串。你可以把它作为在 Web / Desktop / 移动端验证 eval 行为的起点。
小结
document::eval是 Dioxus 跨平台应用接入 JavaScript 生态的通用后门:
- 单向取值:
document::eval(js).await等待 JSreturn,拿到serde_json::Value; - Rust → JS:启动参数用
format!注入;运行期用Eval::send+ JS 侧await dioxus.recv(); - JS → Rust:JS 侧
dioxus.send()+ Rust 侧Eval::recv().await; - 操作 DOM:必须在
use_effect或事件处理器内、组件挂载完成后执行; - 跨端一致性:web、desktop、mobile、liveview 渲染器均可用;纯 Native 或无 Document 上下文时返回
EvalError::Unsupported; - 安全红线:只执行可信代码,Web 端尤其要警惕 XSS。
如果你想深入了解实现细节,推荐按如下路径阅读当前仓库源码:文档正文 → API 定义与 Evaluator trait → Document trait → Web 端实现 → 桌面端实现 → 桌面无头双向通信测试。
【免费下载链接】dioxusFullstack app framework for web, desktop, and mobile.项目地址: https://gitcode.com/GitHub_Trending/di/dioxus
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考