news 2026/9/9 13:13:14

使用 Dioxus eval 在 Rust 中执行 JavaScript:跨平台双向通信完全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 Dioxus eval 在 Rust 中执行 JavaScript:跨平台双向通信完全指南

使用 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并不是一个把字符串简单交给解释器的黑盒,而是一套分层的设计:

  1. document::eval(script)会调用当前渲染器通过上下文注入的Documenttrait 实现(见 Document trait 定义);
  2. 返回的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 callingeval。(见 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" } } }

要点说明:

  • documentdioxus::prelude中导出的模块别名,对应 dioxus_document 中的dioxus_document
  • 被执行的 JS 通过return返回serde_json::Value,Rust 端await后可直接解包、反序列化;
  • Eval实现了IntoFuture,因此result.await是合法写法。在 packages/document/src/eval.rs 中可以看到IntoFutureOutput被定义为Result<serde_json::Value, EvalError>

向 JavaScript 发送数据:格式化拼接与 send 通道

当你执行一段 JS 时,有两种方式把数据传给它:

  1. 启动前静态注入:把值format!进 JS 字符串里,适合初始参数;
  2. 运行时动态发送:借助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反序列化成你声明的具体类型(i32StringVec<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_effectspawneval组合在一起: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 Webpackages/web/src/document.rsJSChannel+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
Liveviewpackages/liveview/src/document.rs由 Liveview 文档实现提供——
纯 Native(无 WebView)NoOpDocument(packages/document/src/document.rs)任何操作都返回EvalError::Unsupported

需要注意:dioxus 的 re-export 结构里,dioxus::mobiledioxus::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 侧通过GenerationalBoxtry_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)漏洞。

落地到工程实践,需要注意几点:

  1. 不要拼接不可信输入:用户输入、网络响应中的字符串如果被format!进 JS 字符串,等同于把执行权交给了攻击者;
  2. 对外部数据先净化:即使只是"放进 JS 字符串传参",也应做转义与白名单校验,而不是直接拼入;
  3. 最小化暴露面:eval 只开放给可信内部逻辑,JS 里不要顺手把敏感数据写入全局对象;
  4. 注意平台差异:桌面/移动 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 13:11:57

三张大头如何做到风格统一?批量稿件全流程拆解与实操指南

做稿件的同学应该都有这个感受&#xff1a;单张“大头”不算难画&#xff0c;真正让人头疼的是三张放在一起时&#xff0c;看起来不像同一批稿件。角色脸型跑偏、颜色冷暖不一致、背景光源方向不统一&#xff0c;这些在单张审核时很难发现&#xff0c;等三张拼在一起就特别明显…

作者头像 李华
网站建设 2026/9/9 13:10:25

iOS审核4.3a拒信全解析:从判定逻辑到差异化改造与申诉实操

做iOS开发最难熬的环节是什么&#xff1f;不是写代码&#xff0c;不是调BUG&#xff0c;是提审上架。尤其是当你收到一封以“4.3a”开头的拒信时&#xff0c;心里基本就凉了一半——审核团队直接认定你的App属于重复内容、垃圾应用&#xff0c;甚至懒得跟你多解释。这篇文章我结…

作者头像 李华
网站建设 2026/9/9 13:10:21

基于YOLO-Pose的实时坐姿检测系统:从关键点识别到工程落地

简介&#xff1a;这是一套基于Python编程与YOLO算法的学生坐姿检测系统&#xff0c;面向AI视觉方向学习者及教育信息化项目开发者&#xff0c;可实时统计课堂上错误坐姿人数&#xff0c;并通过MQTT协议将数据上传至阿里云平台&#xff0c;实现远程监控与数据可视化。系统以Maix…

作者头像 李华
网站建设 2026/9/9 13:09:49

nhdeep电子档案长期保存系统

nhdeep电子档案长期保存系统&#xff0c;用于导入管理系统中的著录项信息&#xff0c;并安装档案相关规范&#xff0c;转换为适合长期保存的电子文件格式和封装包结构&#xff0c;进行管理和存储。著录信息列表页面&#xff0c;用于导入著录项&#xff0c;挂接原文文件&#xf…

作者头像 李华
网站建设 2026/9/9 13:09:36

软件测试五道关:从需求澄清到测试报告的完整指南

前几天有位同事甩了个文档给我&#xff0c;标题写着“测试文章标题01”&#xff0c;正文是空的&#xff0c;就一行占位符。他说组长让他牵头整理一份测试团队的能力清单&#xff0c;模板建好了&#xff0c;自己却对着空白页发了半天呆。我说这太正常了&#xff0c;测试这行看着…

作者头像 李华
网站建设 2026/9/9 13:09:33

PaddleOCR数据智能分割工具:基于多维指标的可视化数据集拆分方案

先放结论&#xff1a;这个工具要解决的&#xff0c;不是"把数据按8:2随机切两堆"这么简单的事。PaddleOCR训练最让人头疼的&#xff0c;是数据集中混着模糊图、竖排文本、超长表格、高密度小字——这些样本如果不提前分桶&#xff0c;训练前你会花一整晚调参&#xf…

作者头像 李华