ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

dioxus-web 完全指南:用 Rust 在浏览器中通过 WebAssembly 渲染 Dioxus 应用

dioxus-web 完全指南:用 Rust 在浏览器中通过 WebAssembly 渲染 Dioxus 应用 dioxus-web 完全指南用 Rust 在浏览器中通过 WebAssembly 渲染 Dioxus 应用【免费下载链接】dioxusFullstack app framework for web, desktop, and mobile.项目地址: https://gitcode.com/GitHub_Trending/di/dioxusdioxus-web 是 Dioxus 生态中负责浏览器端渲染的核心 crate它将 Dioxus 的响应式 VirtualDOM 编译为 WebAssembly并借助 web-sys 直接操作真实 DOM让一套 Rust 代码即可跑在浏览器中。本文围绕仓库内 packages/web/README.md 展开结合源码讲解其启动流程、Config配置项、事件与 DOM 更新机制、SSR 水合、路由历史集成以及体积控制读完你将掌握如何在自己的项目里直接使用 dioxus-web 这一渲染器。文档说了什么dioxus-web 的定位packages/web/README.md对 dioxus-web 的定位描述非常精炼一句话概括就是Run Dioxus in the browser using WebAssembly。它同时明确列出三个核心事实这也是我们理解这个 crate 的起点依赖sledgehammer-bindgen和web-sys完成 DOM 的修改与事件桥接通过Dioxus CLI支持即时热重载instant hot reloading打包后体积约60k gzippedREADME 声明值实际与启用的 feature 相关。从 Cargo.toml 中 crate 的元数据可以看到它的正式描述“Web-sys renderer for Dioxus: Build fullstack web, desktop, and mobile apps with a single codebase.” 这说明 dioxus-web 在架构上的职责非常单一且明确它是 Dioxus 面向Web 目标wasm32的渲染器实现负责把核心层算出的变更序列落到浏览器 DOM 上。所谓“单一代码库多端复用”正是因为 Dioxus 上层 API 与目标无关真正按平台分叉的渲染器之一是它。渲染器在 Dioxus 架构中的角色Dioxus 采用“核心无关渲染器”的分层设计dioxus-core维护 VirtualDOM、作用域、信号与调度而具体的界面落地工作交给平台渲染器。dioxus-web 与 dioxus-desktop基于 WebView、dioxus-mobile 等并列都是dioxus-core的“消费端”。从 packages/web/Cargo.toml 的依赖清单可以清晰看出渲染器与核心、平台能力的边界dioxus-core、dioxus-core-types、dioxus-html、dioxus-signals提供组件模型、事件语义与响应式状态dioxus-cli-config启用webfeature读取 CLI 生成的 web 配置例如编译期 build iddioxus-interpreter-js启用minimal_bindings、webonlydioxus-web 不再用 Rust 逐条翻译 DOM 操作而是把核心产生的“变更指令流”编码后交给一个极小的 JS 解释器执行web-syswasm-bindgenRust 与浏览器 API 之间的 FFI 层dioxus-document、dioxus-devtools、dioxus-history与dioxus-fullstack-corefeature 门控分别承担文档元信息、开发工具、路由历史与全栈水合。而lib.rs顶层导出也印证了这种模块划分pub mod launch启动入口、WebsysDomDOM 渲染器实现、Config平台配置、WebDocument文档实现documentfeature 下、HashHistory/WebHistory历史实现documentfeature 下以及大量*Event类型的事件转换器源码见 packages/web/src/lib.rs。启动应用从 dioxus::launch 到 WebSys 运行时推荐方式dioxus::launch日常开发中大多数项目并不直接调用 dioxus-web 的 API而是通过dioxus聚合 crateuse dioxus::prelude::*; fn App() - Element { rsx! { div { Hello from WebAssembly! } } } fn main() { dioxus::launch(App); }在 packages/dioxus/src/launch.rs第 355 行附近中LaunchBuilder会根据当前编译目标把调用分派给对应渲染器其中就包含dioxus_web::launch::launch(app, contexts, configs)而 packages/dioxus/src/lib.rs 也直接以pub use dioxus_web as web导出别名。因此如果你在dioxuscrate 中看到dioxus::web::Config之类路径本质上就是这里的类型。底层启动函数族在 packages/web/src/launch.rs 中dioxus-web 提供了三个层层递进的启动入口// 最常用root 组件 若干根上下文 平台配置 pub fn launch( root: fn() - Element, contexts: VecBoxdyn Fn() - Boxdyn Any Send Sync, platform_config: VecBoxdyn Any, ) // 直接接管一个已经构造好的 VirtualDom pub fn launch_virtual_dom(vdom: VirtualDom, platform_config: Config) // 简写root 组件 单个 Config pub fn launch_cfg(root: fn() - Element, platform_config: Config)它们最终都会执行wasm_bindgen_futures::spawn_local(...)把crate::run(vdom, platform_config)包装成的主循环 Future 投递到浏览器主线程事件循环上。主循环与“无卡顿渲染”的取舍核心主循环位于 packages/web/src/lib.rs 的pub async fn run(mut virtual_dom: VirtualDom, web_config: Config) - !。流程大致是devtools debug 构建下初始化热重载通道并判断是否启用should_hydrate web_config.hydrate || cfg!(feature hydrate)构造WebsysDom::new(cfg, runtime)完成根节点查找与事件监听器注册若水合开启从window.initial_dioxus_hydration_data取回序列化的水合数据并执行rehydrate否则直接virtual_dom.rebuild(mut websys_dom)完成首次渲染并flush_edits()进入无限loopwait_for_work()等待有可用变更随后render_immediate完成 diff 并通过flush_edits把变更一次性刷到 DOM如此循环驱动整个应用的响应式更新。值得留意的是源码中保留了一段被注释掉的“Jank free rendering”设计等待浏览器 idle 时段分片 diff、按 rAF 节奏批量打补丁并注明“目前禁用是因为它对事件响应时延有负面影响未来可能只对任务task重新启用”。这为我们理解 dioxus-web 的渲染时机取舍提供了直接依据——当前实现是“有工作就立即渲染一帧并刷新”在事件交互上更及时。平台配置 Config挂载根、水合与历史Config源码 packages/web/src/cfg.rs是 dioxus-web 暴露给调用方最主要的平台配置结构。它实现dioxus_core::LaunchConfigtrait可用链式 builder 方式构造。方法总览方法作用默认值/说明Config::new()构造默认配置等价于Config::default().rootname(name)按 id 指定挂载根元素默认main即在document.getElementById(main)上渲染找不到时打印错误并退回挂载到body.rootelement(elem)直接指定一个web_sys::Element作为根适用于当前document之外的宿主如弹窗、iframe.rootnode(node)直接指定一个web_sys::Node作为根同rootelement接受任意 Node.hydrate(bool)开启 SSR 水合需hydratefeature默认false.history(Rcdyn History)注入自定义历史提供者需documentfeature不设置时内部默认使用WebHistory见后文其中.hydrate的文档值得逐字理解水合会完全跳过所有异步工作与 suspended 节点Dioxus 在页面加载时会把预渲染 HTML 中带标记的元素装载进内存再绑定事件与状态。一个直接使用 dioxus-web 的最小示例dioxus_web::launch_cfg(App, Config::new().rootname(app));Config::default()的实现细节packages/web/src/cfg.rs为hydrate: false、root: ConfigRoot::RootName(main.to_string())、history: None以及一个panic_hook: true字段。从源码结构看panic_hook目前只有默认值、没有公开 builder 方法被标记allow(dead_code)说明其行为在现阶段是固定开启的。根节点查找的兜底策略WebsysDom::newpackages/web/src/dom.rs中按RootName查找时若get_element_by_id返回空会在浏览器控制台输出element #xxx not found. mounting to the body.错误并退回document.body。因此如果你的 HTML 模板里没有与 rootname 匹配的挂载节点页面元素依然会被创建只是会挂在 body 下。DOM 操作与事件系统解释器 事件委托dioxus-web 的 DOM 落地不是逐条调用 web-sys而是通过dioxus-interpreter-js提供的最小 JS 运行时完成Cargo.toml 中dioxus-interpreter-js { features [minimal_bindings, webonly] }。变更写入WriteMutations → interpreterWebsysDom实现了dioxus_core::WriteMutationspackages/web/src/mutations.rs把虚拟层的标准操作翻译为解释器指令例如create_element/create_text创建元素与文本节点child/pop在模板树中游走定位当前节点set_attribute把AttributeValueText/Float/Int/Bool/None统一序列化为字符串后交给set_current_attribute其中AttributeValue::None会转为移除当前属性add_event_listener除了mounted走特殊通道外都会调用new_top_event_listener(name, event_bubbles(name))第二个参数标明该事件是否冒泡交给解释器做事件委托。flush_edits()则调用interpreter.flush()一次性把缓存的批量 DOM 操作提交并在mountedfeature 下触发排队中的mounted事件回调。事件委托与 target 回溯浏览器事件并非给每个元素单独挂监听而是在根节点上注册统一的委托回调。在 packages/web/src/dom.rs 中可以看到这个关键的Closuredyn Fn(Event)取出事件的type_()如click、keydown用walk_event_for_id从事件target开始向上回溯直到找到带data-dioxus-id属性的元素得到对应的ElementId通过virtual_event_from_websys_event把浏览器事件转换成 Dioxus 平台事件交给runtime.handle_event(name, event, element)进入响应式调度若用户在事件上调用了prevent_default即事件默认行为被禁用则在此执行web_sys_event.prevent_default()。注释中还特意说明事件回调必须使用Closuredyn Fn而非FnMut因为一个事件可能递归触发另一个事件例如 focus 另一个元素触发对方的 focus 事件。这是从真实 issue 中沉淀下来的实现约束。为了让这些事件类型可用packages/web/Cargo.toml 的web-sys依赖开启了大量 featureMouseEvent、PointerEvent、KeyboardEvent、TouchEvent、DragEvent、FocusEvent、WheelEvent、ClipboardEvent、CompositionEvent、TransitionEvent、AnimationEvent、InputEvent、File/FileList/FileReader、IntersectionObserverEntry、ResizeObserverEntry等。而 events 目录下的模块mouse.rs、keyboard.rs、pointer.rs、mounted.rs、visible.rs、scroll.rs等见 packages/web/src/events分别实现了对应的 web 事件转换器。开启hydrate与服务端渲染的对接要让 SSR 产出的 HTML 在浏览器端“复活”成可交互应用需要同时满足两个条件编译时启用hydratefeature并且运行期把Config::hydrate(true)打开。hydratefeature 在 Cargo.toml 中同时引入web-sys/Comment、serde与dioxus-fullstack-core。结合 packages/web/src/lib.rs 的水合分支可以看到完整的数据流通过内联 JS 从window.initial_dioxus_hydration_data读取服务端序列化的 Base64 数据debug 下还额外读取initial_dioxus_hydration_debug_types与initial_dioxus_hydration_debug_locations用于更好的报错信息HydrationContext::from_serialized反序列化若服务端在根 Suspense 边界序列化了错误则直接抛给根作用域在序列化数据的上下文中virtual_dom.rebuild_in_place()不产生任何 DOM 变更因为 SSR DOM 已在页面中随后websys_dom.rehydrate把预渲染节点逐个绑定hydratefeature 下还支持流式水合Suspense 边界在异步数据就绪后通过rehydrate_streaming增量接管新增内容对应hydration/suspense.rs。水合还应用了一个值得注意的细节在 debug devtools 构建下启动时会先短暂等待约 100ms观察是否有热重载消息到达——这样水合会使用与服务端序列化时相同的函数实现避免因热重载代码不一致导致绑定错位。路由与历史WebHistory 与 HashHistory浏览器端的 URL 路由依赖dioxus-historytrait。dioxus-web 在documentfeature 下提供了两个开箱即用的实现源码 packages/web/src/history.rs通过Config::history(...)注入。WebHistory基于 History APIWebHistory使用浏览器history.pushState/replaceStateURL 形如/users/42。其Default::new()等价于WebHistory::new(None, true)。两个值得注意的能力prefix基路径支持适用于部署在域名子路径的应用。优先使用new(Some(prefix), ...)传入否则回退读取dioxus_cli_config::web_base_path()前缀会 trim 掉首尾斜杠后规范化为/xxx形式滚动恢复scroll restorationdo_scroll_restorationtrue时会把浏览器scroll_restoration设为manual将[scrollX, scrollY]存入 history state监听popstate后在下一个 rAF 恢复滚动位置每次push前也会先把当前滚动位置写入 state。HashHistory基于 URL 片段HashHistory把所有路由塞进#片段形如/path#/users/42因此只需要单一路径即可承载任意路由非常适合无法配置服务端重写、需要以单个 HTML 文件分发或托管在对象存储的场景。注意历史上 router 相关代码如 packages/router 的 web 适配与这两个实现配合紧密仓库在 examples/06-routing/hash_fragment_state.rs 中提供了 hash 片段路由状态恢复的完整示例在 examples/06-routing/router.rs 与 examples/06-routing/flat_router.rs 中可以看到常规 Router 的用法其中默认即走WebHistory。Feature 配置与包体控制dioxus-web 的编译期能力完全由 feature 控制直接决定最终 wasm 的体积与可用 APIFeature默认作用mounted✅支持onmounted回调元素挂载后触发同时开启web-sys/Element、DomRect、HtmlElement等能力devtools✅启用与 Dioxus Devtools 的 WebSocket 通道、热重载消息与serde序列化仅在 debug 构建生效document✅提供WebDocument、WebHistory、HashHistory等文档与历史实现hydrate❌支持 SSR 水合与流式水合引入dioxus-fullstack-core与serdeREADME 中“约 60k gzipped”的体量正是在默认 feature 集下可达到的目标实际体积会随你启用的功能尤其是是否带完整事件、文档能力浮动。如果你构建纯客户端 SPA默认配置即可如果要跑 SSR 水合/全栈应用则需要追加hydrate。开发体验CLI 驱动的即时热重载README 特别强调“Supports instant hot reloading via the Dioxus CLI”。dioxus-web 在 debug 构建下会通过 packages/web/src/devtools.rs 建立与 Dioxus CLI 的连接其核心链路在 packages/web/src/lib.rs 的主循环中清晰可见devtools::init启动热重载接收端主循环select!同时监听“虚拟 DOM 有新工作”“Devtools 热重载消息”“流式水合数据”三路信号收到热重载消息时调用dioxus_devtools::apply_changes(virtual_dom, msg)原地替换模板若消息还携带资源变更则调用invalidate_browser_asset_cache()让浏览器缓存失效热补丁成功后会在页面右下角弹出Hot-patch success!toast并显示App successfully patched in {ms} ms的耗时。这意味着你可以在保持浏览器页面与应用状态不丢失的情况下实时编辑 rsx 模板并立即看到效果这也是 dioxus-web 面向交互式开发体验设计的直接证据。从哪些代码继续深入如果希望进一步验证或学习 dioxus-web 的行为仓库内可直接研读的参考包括渲染器核心packages/web/src/dom.rs事件委托与根节点解析、packages/web/src/mutations.rs变更写入、packages/web/src/lib.rs主循环与水合配置与启动packages/web/src/cfg.rs、packages/web/src/launch.rs浏览器端端到端示例examples/06-routing/web_router.rs 同目录下有水合游走实现仓库根 examples/01-app-demos 下的多个 demo 以及 examples/07-fullstack/hello-world、examples/07-fullstack/router 展示了真实可运行的全栈/水合工程浏览器行为验证playwright-tests/web、playwright-tests/web-hash-routing等 Playwright 工程见 packages/playwright-tests覆盖了 web 目标下的行为断言渲染对比桌面端的dioxus-desktop走 WebViewnative 目标走dioxus-native而浏览器端唯一的渲染器就是本文的 dioxus-web。小结回到 README 那句高度浓缩的 Overviewdioxus-web 的全部技术栈可以拆解为三句话用 WebAssembly 承载 Dioxus VirtualDOM用 web-sys FFI 与解释器落地 DOM用 Dioxus CLI 提供即时热重载开发体验。通过本文你可以清楚地看到一个dioxus::launch(App)背后是 Config 解析、事件委托、变更批处理、SSR 水合与历史路由这一整套精心设计的渲染管线——理解这一层也就理解了 Dioxus 在浏览器端的一切行为边界与性能特性。【免费下载链接】dioxusFullstack app framework for web, desktop, and mobile.项目地址: https://gitcode.com/GitHub_Trending/di/dioxus创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进