Dioxus 渲染器架构解析:WriteMutations _trait_、Sledgehammer 二进制协议与 Web/Desktop/Native/LiveView 四大渲染后端
Dioxus 通过一套基于 trait 的公共抽象(WriteMutations / HtmlEventConverter)让同一个组件树能够渲染到浏览器、Wry WebView、自研 Native 管线乃至纯服务端 LiveView 之上。本文以仓库中 06-RENDERERS.md 为骨架,逐节还原其核心抽象与各渲染后端的实现细节,并结合 dioxus-core、dioxus-interpreter-js、dioxus-web、dioxus-desktop、dioxus-liveview 的实际源码,讲清楚“VirtualDOM 的 diff 结果如何变成真实的画面”,以及事件如何反向回流触发重渲染。读完后你可以理解每个渲染器(Renderer)的职责边界、批处理/序列化策略的差异,并知道如何为 Dioxus 接入一个全新的渲染后端。
核心抽象:WriteMutations —— VirtualDOM 与真实 DOM 之间的唯一桥梁
架构文档首先点明:Dioxus 支持多种渲染后端,它们都通过同一套基于 trait 的抽象来表达变更。整个体系的核心是定义在 packages/core/src/mutations.rs 中的 WriteMutations trait,其源码注释原话是:“Mutations are the only link between the RealDOM and the VirtualDOM”(Mutations 是真实 DOM 与虚拟 DOM 之间唯一的纽带)。
文档中的方法清单
架构文档列出了该抽象的核心方法语义:
append_children(id, count)—— 向元素添加 N 个子节点assign_node_id(path, id)—— 标记模板路径处的元素create_placeholder(id)—— 创建占位(marker)节点create_text_node(value, id)—— 创建文本节点load_template(template, index, id)—— 从模板缓存克隆replace_node_with(id, count)—— 替换元素set_attribute(name, ns, value, id)—— 更新属性create_event_listener(name, id)—— 注册事件监听器remove_node(id)—— 删除元素
当前代码中的堆栈机模型
对照当前工作区源码可以看到,这一抽象已演进为一台“堆栈机”(stack machine):调用方维护一个节点栈,diff 引擎只发出相对操作,渲染器负责把它们落到具体后端。WriteMutations 的当前方法集为:
// packages/core/src/mutations.rs(L12-L89 摘录)
pub trait WriteMutations {
fn can_cache_template_roots(&mut self) -> bool { true }
fn push_id(&mut self, id: ElementId); // 将已注册节点压栈
fn child(&mut self, index: usize); // 栈顶替换为其第 index 个子节点
fn pop(&mut self); // 弹出并丢弃栈顶
fn create_element(&mut self, tag: &str, ns: Option<&str>);
fn create_text(&mut self, value: &str);
fn clone(&mut self); // 栈顶替换为自身深拷贝(模板复用)
fn append_children(&mut self, m: usize); // 将栈顶 m 个节点挂到父节点
fn replace_with(&mut self, m: usize);
fn insert_after(&mut self, m: usize);
fn insert_before(&mut self, m: usize);
fn set_attribute(&mut self, name: &str, ns: Option<&str>, value: &AttributeValue);
fn set_text(&mut self, value: &str);
fn add_event_listener(&mut self, name: &str);
fn remove_event_listener(&mut self, name: &str);
fn remove(&mut self);
fn set_id(&mut self, id: ElementId); // 将栈顶节点注册到 id(替代旧 pop_id)
}
几个值得注意的设计点(均有源码依据):
can_cache_template_roots(mutations.rs#L21-L23):默认返回true。注释明确警告——如果某个 writer 忽略 mutations、或不把模板原型节点保留到本地节点映射中,必须返回false,否则 VirtualDom 侧会为根本不存在于渲染器里的节点创建模板缓存条目。这解释了 SSR/测试用 writer 与真实渲染器之间的行为差异。set_id取代pop_id:源码注释说明旧pop_id是“注册并弹出”二合一,新 API 把注册与弹出拆开,调用方若想让节点离开栈可显式跟随一个pop。&mut W转发实现(mutations.rs#L127-L133):通过对可变引用转发所有方法,让 writer 泛型代码可以直接以&mut dyn WriteMutations实例化,方便 diff 过程内部以动态分发驱动各后端。with_id辅助函数(mutations.rs#L140-L149):封装“push_id → 在节点上操作 → pop”的不变式——闭包执行完时,被压入的节点必须仍位于渲染器栈顶;No-op writer 虽然不应用变更,但仍会收到这些栈操作,diff 流程保持单一控制流。
HtmlEventConverter 与事件流模式
事件侧的对称抽象是 HtmlEventConverter trait(当前实现在 packages/html/src/events/generated.rs#L389 生成),每个渲染器提供自己的实现,负责把平台特定的 PlatformEventData 转换为类型化的 Dioxus 事件数据,从而实现跨平台的事件多态。架构文档给出的事件流模式如下:
平台事件 → 被渲染器捕获
→ 经 HtmlEventConverter 转换
→ runtime.handle_event(name, event, element_id)
→ VirtualDOM 的 handler 被调用
→ 状态变更 → 重新渲染
→ WriteMutations 被应用
Web 渲染器中的 mounted 事件是这条链路的一个具体例证:DOM 节点真正落入页面后,WebsysDom::flush_edits 会遍历排队的元素,构造 Event::new(...) 并以名字 "mounted" 调用 self.runtime.handle_event(name, event, id)(见 packages/web/src/mutations.rs#L16-L31),完全印证了文档中的事件流闭环。
Web 渲染器(dioxus-web):WebsysDom 与 Hydration
WebsysDom 结构
架构文档给出的结构图与 packages/web/src/dom.rs 对应:
WebsysDom
├── interpreter: Sledgehammer JS 解释器
├── document: web_sys::Document 引用
├── root: 根 DOM 节点
├── templates: HashMap<Template, u16>
├── runtime: Rc<Runtime>
└── (hydration 相关): skip_mutations, suspense_hydration_ids
Mutation 实现
Web 端的 WriteMutations for WebsysDom(packages/web/src/mutations.rs#L39)几乎逐方法直接委托给 Sledgehammer JS 解释器——例如 push_id 调用 self.interpreter.push_id(id.raw() as u32),create_element 调用 self.interpreter.create_element_top(tag, ns.unwrap_or_default())。其要点:
- 通过 wasm-bindgen 直接委托到 JavaScript 解释器;
- 模板只序列化一次、存放在 JS 侧,之后按引用实例化;
- Sledgehammer 解释器内部维护“正在构建的节点”栈,与 Rust 侧
WriteMutations的堆栈机语义严格对应; flush_edits(mutations.rs#L8-L14)调用self.interpreter.flush()把攒下的 edits 批量落入真实 DOM,随后在mountedfeature 下再派发挂载事件。
事件处理
- 根元素上只挂一个委托监听器,事件发生后沿 DOM 树向上查找
data-dioxus-id属性定位目标元素; WebEventConverter负责把web_sys事件转换为 Dioxus 类型;- 支持所有标准 DOM 事件(鼠标、键盘、触摸等)。
Hydration 系统
文档描述的五步水合流程(packages/web/src/hydration/ 目录承载其实现):
- SSR 服务端渲染出带
dio_el数据属性的 HTML(SSR 实现在 packages/ssr/src/); - 客户端接收 base64 编码的 hydration 上下文;
- VirtualDOM 以
skip_mutations = true重建,即不产生 DOM 写入; - 客户端遍历预渲染的 DOM,为节点分配 element id;
- 对 suspense 边界通过
rehydrate_streaming()做流式水合。
Launch 流程
launch(root_component, contexts, config)
→ 创建 VirtualDom
→ 创建 WebsysDom 包装器
→ 若为 hydrate:反序列化数据,以 skip_mutations 重建
→ 否则:vdom.rebuild(&mut websys_dom)
→ 主循环:wait_for_work() → render_immediate() → flush_edits()
该流程的入口在 packages/web/src/launch.rs。
Desktop 渲染器(dioxus-desktop):Wry WebView 之上的原生壳
Desktop 渲染器基于 Wry webview 库 + Tao 窗口管理,源码位于 packages/desktop/src/。
App 结构
架构文档描述的 App 结构对应 packages/desktop/src/app.rs:
App
├── unmounted_dom: Cell<Option<VirtualDom>>
├── webviews: HashMap<WindowId, WebviewInstance>
├── shared: Rc<SharedContext>
│ ├── event_handlers: WindowEventHandlers
│ ├── pending_webviews: Vec<PendingWebview>
│ ├── shortcut_manager: ShortcutRegistry
│ └── websocket: EditWebsocket
└── control_flow: ControlFlow
WryQueue:基于 WebSocket 的 Mutation 传输
这是 Desktop 渲染器中最有工程含金量的一块。packages/desktop/src/edits.rs 顶部有一段极有价值的模块注释,交代了技术选型的历史与权衡:
- 最初用 Wry 自定义协议上的 long-polling 向 webview 推送 edits,但受 Wry 在 Android 上的 bug 影响,改为由 webview 主动连接的 WebSocket;
- 使用 Sledgehammer(interpreter)构建 edits 批次后经 WebSocket 发送,二进制帧高效且规避了普通请求/响应协议的诸多问题;
- WebSocket 最大帧尺寸极大,因此可以无压力地发送大批次 mutations;
- 由于自行承担了传输通道,安全与 CSP 也要自己处理:代码会生成一个随机 key,webview 必须携带它才能连上 WebSocket;并通过 initialization script API 建立连接,避免把 key 泄漏到可能包含不可信内容的 webview 中;
- 在 iOS 等设备上,系统休眠可能杀掉 WebSocket 连接,此时会自动切换新端口并通知 webview 新地址与新 key,webview 重连后继续接收 edits。
WryQueue 本身(edits.rs#L41-L68)注册为该窗口 RenderTargetId 对应的 writer,diff 直接写入其内部的 MutationState;is_touched/clear_touched 标记用于在 send_edits 后判定下一渲染轮是否存在新批次。文档中“WebviewEdits 实现 WriteMutations、委托给管理 mutation 批次的 WryQueue、随机端口上的 WebSocket 服务器、Sledgehammer 二进制协议”的描述与此完全一致。
IPC(进程间通信)
浏览器事件 → JavaScript → window.postMessage()
→ Wry 拦截请求
→ 提取 dioxus-data 头(base64 JSON)
→ IpcMessage { method, params }
→ 分派:UserEvent、Query、BrowserOpen、Initialize
该链路的 Rust 侧实现在 packages/desktop/src/ipc.rs。
Protocol Handler
dioxus://自定义协议用于资产(assets)分发;- 处理
__events路径完成事件接入; __file_dialog处理文件选择;- 支持用户自定义 handler 命名空间;
- 查询
dioxus_asset_resolver获取打包资产。
实现见 packages/desktop/src/protocol.rs 与 packages/desktop/src/assets.rs。
Native 能力与配置
原生特性方面:通过 muda 集成菜单、trayicon 系统托盘、global_hotkey 全局热键、文件对话框与拖拽支持,以及供自动化测试使用的 headless 模式(对应 packages/desktop/src/menubar.rs、trayicon.rs、shortcut.rs、file_upload.rs)。配置面(packages/desktop/src/config.rs)覆盖:
Config
├── WindowBuilder 定制
├── 自定义事件循环
├── 同步/异步 Protocols
├── 预渲染 HTML 模板
├── 禁用上下文菜单标志
├── 背景色(RGBA)
└── Devtools 支持开关
Native 渲染器(dioxus-native):不经过浏览器引擎的自研管线
Native 渲染器与 Web/Desktop 有本质区别——从文档与 packages/native/src/ 的源码结构看,它不是浏览器引擎,而是一条自研的原生渲染管线:
- Blitz:负责 CSS 布局的布局引擎;
- Vello:GPU 加速的矢量渲染;
- Winit:跨平台窗口管理。
DioxusNativeWindowRenderer 包装 anyrender-vello、实现 WindowRenderer trait、可配置 GPU 特性并支持自定义绘制。完整管线为:
VirtualDOM 组件
→ DioxusNativeDOM
→ 带 CSS 的 Blitz DOM 树
→ Blitz 布局引擎
→ Vello 渲染器
→ GPU 渲染
布局与 CSS 能力上,文档明确了边界:CSS 2.1+ 解析器(并非完整 CSS 3)、基于 Flexbox 的布局模型、计算样式附加在元素节点上、布局在 mutation 期间自底向上计算。Winit 侧的事件循环处理模式:
Event::NewEvents(StartCause::Init) → 创建初始窗口 + 带 VirtualDOM 的 DioxusDocument
Resumed → Renderer.resume()
WindowEvent::RedrawRequested → VirtualDOM.render_immediate()
→ Mutations 应用到 Blitz DOM → 计算布局 → 渲染一帧
WindowEvent::Resized → 排队重绘
Native DOM 节点与样式相关的基础设施位于 packages/native-dom/src/。
LiveView 渲染器(dioxus-liveview):VirtualDOM 跑在服务端
LiveView 的架构反转了 Web 的模式:浏览器只做“哑终端”,VirtualDOM 运行在服务端,二者通过 WebSocket 上的二进制协议同步。
客户端(带 WebSocket 的浏览器)
↓ events(上行)
服务端(VirtualDOM 在这里运行)
↓ mutations(二进制协议)
客户端接收 mutations → Sledgehammer 应用
LiveViewPool
packages/liveview/src/pool.rs#L17-L40 证实了文档描述:LiveViewPool 持有 tokio_util::task::LocalPoolHandle,new() 时按 std::thread::available_parallelism() 的核数建池,并在此处全局设置事件转换器 SerializedHtmlEventConverter。每个客户端通过 spawn_pinned 获得一个绑定到线程的 pinned task,VirtualDOM 便运行在该 task 的 executor 上,池子因此可承载多个并发客户端。LiveViewSocket 是一个 auto trait——只要类型实现 Stream + Sink 即可作为通信端使用,packages/liveview/src/adapters/ 提供了各 HTTP 框架的现成适配。
每客户端生命周期
LiveViewPool::launch_virtualdom(ws, || VirtualDom::new())
→ 创建 MutationState、QueryEngine
→ vdom.rebuild() → 发送初始 HTML
→ 循环:
tokio::select! {
ws.next() → 处理事件/query
vdom.wait_for_work() → 有待处理工作
query_rx.recv() → JS 查询
hot_reload_rx.recv() → 热重载
}
render_immediate() → 发送 mutations
二进制协议与消息类型
mutation 通过 MutationState::write_memory_into(&mut bytes) 导出——packages/interpreter/src/write_native_mutations.rs#L19-L22 中该方法把 channel 内存追加到字节缓冲后 reset(),随后经 WebSocket 传输,客户端以 window.interpreter.handleEdits(bytes) 应用。消息分三类:二进制帧(mutations)、文本帧(查询/元数据)、上行事件(用户交互)。
Mounted 元素查询
LiveviewElement 提供一组映射到 JS 的查询方法(packages/liveview/src/element.rs):
scroll_offset() → JS: getScrollLeft/Top()
scroll_size() → JS: getScrollWidth/Height()
client_rect() → JS: getBoundingClientRect()
scroll_to(options) → JS: element.scrollTo()
Interpreter 包(dioxus-interpreter-js):共享的二进制协议
packages/interpreter/ 是整套体系的“传输编码层”,架构文档称 Sledgehammer 为“跨渲染器共享的超紧凑 DOM mutation 二进制协议”。
三个核心 JS 组件
- INTERPRETER_JS —— 基础解释器类;
- NATIVE_JS —— 平台特定扩展;
- SLEDGEHAMMER_JS —— 由 Rust 生成绑定的编码实现。
MutationState:为二进制序列化服务的 writer
MutationState(packages/interpreter/src/write_native_mutations.rs#L5-L25)实现 WriteMutations,把每条 mutation 记录进 channel,并可随时 export_memory() / write_memory_into(buffer) 导出为字节流。它的 set_attribute 实现还展示了 AttributeValue 枚举的分发:Text/Float/Int/Bool 转为字符串属性,None 则调用 remove_current_attribute 删除属性,非字符串类属性会触发 unreachable!(源码注释注明当前渲染器不支持 Any 属性)。其 add_event_listener 特意使用 foreign listener 而非 native 变体,因为“native 方法假设能直接访问 DOM,而我们没有”(write_native_mutations.rs#L100-L105)。Desktop 的 WryQueue 与 LiveView 的 MutationState 正是复用了同一个 writer,这就是“同一份协议贯穿所有非直连渲染器”的实现基础。
堆栈机操作对照
文档列出的堆栈机操作与 WriteMutations 的一一映射:
create_text_node(value, id) → 压入文本节点
create_placeholder(id) → 压入注释节点
append_children(parent_id, count) → 弹出并追加
replace_with(id, count) → 替换元素
set_attribute(id, name, value, ns) → 设置 DOM 属性
new_event_listener(name, id, bubbles) → 注册监听器
在 MutationState 中这些操作分别落到 create_text、append_children_top、replace_top_with、set_current_attribute、foreign_top_event_listener(带 event_bubbles(name) 计算冒泡标志)等 channel 指令上。
公共模式:四种渲染器的对比与通用约定
1. WriteMutations 实现策略对比
每个渲染器以不同方式落地同一 trait:
| 渲染器 | mutation 落点 | 传输/应用方式 |
|---|---|---|
| Web | 直接委托 Sledgehammer JS(wasm-bindgen) | 本地 JS 解释器,flush_edits 批量落 DOM |
| Desktop | 累积进 MutationState(经 WryQueue) |
随机端口的 WebSocket 二进制帧 |
| Native | 直接应用到 Blitz DOM | 进程内同步应用 + 重算布局 |
| LiveView | 累积进 MutationState |
WebSocket 二进制帧,客户端 Sledgehammer 应用 |
2. 基于 Box<dyn Any> 的配置模式
渲染器用 Box<dyn Any> 实现可扩展配置:launch(app, configs: Vec<Box<dyn Any>>),对每个 config 尝试 downcast 到期望类型。这使调用方可以传入任意数量的可选配置(窗口参数、协议、模板等)而无需为每种组合提供重载 API。
3. 懒初始化
多数渲染器把上下文设置推迟到第一个窗口/请求出现时,避免启动期做无用工作。
4. 事件循环集成
- Web:WASM bindgen + 浏览器事件循环;
- Desktop:Tao 事件循环,承载
UserWindowEvent; - Native:Winit
ApplicationHandler; - LiveView:
tokio::select!多路等待(见上文每客户端生命周期)。
5. Mutation 批处理
所有非 Web 直连渲染器都遵循“先累积、后冲刷”的批处理:mutation 累积在临时结构(MutationState/WryQueue)中,定期 flush 到传输层。Web 端同样有 interpreter.flush() 的批量落 DOM 语义,只是“传输层”换成了本地 JS 解释器。
如何为 Dioxus 新增一个渲染器
架构文档最后给出了接入新后端的五步清单,结合前文源码可以落到实处:
- 实现
WriteMutations:定义平台如何应用类 DOM 的 mutations、处理模板系统(注意can_cache_template_roots的语义)、维护堆栈机状态;可参考 packages/web/src/mutations.rs(直连式)或 packages/interpreter/src/write_native_mutations.rs(序列化式)两种范式。 - 实现
HtmlEventConverter:把平台事件映射到 Dioxus 事件类型,需要序列化时参考 LiveView 的SerializedHtmlEventConverter(packages/liveview/src/events.rs)。 - 创建 Launch 函数:构造 VirtualDOM、初始化渲染器结构、进入平台事件循环,并在有工作时调用
vdom.render_immediate(&mut mutations)。 - 定义配置:创建 Config 结构体,支持
Box<dyn Any>downcast。 - 可选增强:自定义 mount 事件数据、Document 集成、History 支持、资产分发(参考 packages/desktop/src/protocol.rs 的
dioxus://协议实现)。
小结:一张表看懂四大渲染器
| 维度 | Web | Desktop | Native | LiveView |
|---|---|---|---|---|
| 底层技术 | wasm-bindgen + Sledgehammer JS | Wry/Tao + WebSocket | Blitz + Vello + Winit | 服务端 VirtualDOM + WebSocket |
WriteMutations 实现 |
WebsysDom 直委解释器 |
WryQueue → MutationState |
直接写入 Blitz DOM | MutationState |
| 事件回程 | 根节点委托监听 + data-dioxus-id |
postMessage → IPC |
Winit 窗口事件 | 上行 WebSocket 事件帧 |
| 特色能力 | Hydration/流式水合 | 自定义协议、托盘、热键 | 无浏览器引擎的自绘管线 | 多客户端连接池、元素查询 |
从源码结构看,Dioxus 渲染器架构的关键取舍在于:把“diff 输出什么”(VirtualDOM 侧、与平台无关)和“如何应用到平台”(WriteMutations 实现侧)彻底解耦,再用 Sledgehammer 二进制协议统一所有需要跨进程/跨线程传输 mutation 的场景(Desktop 的 WebSocket、LiveView 的 WebSocket),最终使得同一段组件代码可以在四类后端上以完全一致的状态模型运行。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00