首页
/ Dioxus 渲染器架构解析:WriteMutations _trait_、Sledgehammer 二进制协议与 Web/Desktop/Native/LiveView 四大渲染后端

Dioxus 渲染器架构解析:WriteMutations _trait_、Sledgehammer 二进制协议与 Web/Desktop/Native/LiveView 四大渲染后端

2026-09-05 12:32:29作者:尤辰城Agatha

Dioxus 通过一套基于 trait 的公共抽象(WriteMutations / HtmlEventConverter)让同一个组件树能够渲染到浏览器、Wry WebView、自研 Native 管线乃至纯服务端 LiveView 之上。本文以仓库中 06-RENDERERS.md 为骨架,逐节还原其核心抽象与各渲染后端的实现细节,并结合 dioxus-coredioxus-interpreter-jsdioxus-webdioxus-desktopdioxus-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)
}

几个值得注意的设计点(均有源码依据):

  1. can_cache_template_rootsmutations.rs#L21-L23):默认返回 true。注释明确警告——如果某个 writer 忽略 mutations、或不把模板原型节点保留到本地节点映射中,必须返回 false,否则 VirtualDom 侧会为根本不存在于渲染器里的节点创建模板缓存条目。这解释了 SSR/测试用 writer 与真实渲染器之间的行为差异。
  2. set_id 取代 pop_id:源码注释说明旧 pop_id 是“注册并弹出”二合一,新 API 把注册与弹出拆开,调用方若想让节点离开栈可显式跟随一个 pop
  3. &mut W 转发实现mutations.rs#L127-L133):通过对可变引用转发所有方法,让 writer 泛型代码可以直接以 &mut dyn WriteMutations 实例化,方便 diff 过程内部以动态分发驱动各后端。
  4. 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 WebsysDompackages/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_editsmutations.rs#L8-L14)调用 self.interpreter.flush() 把攒下的 edits 批量落入真实 DOM,随后在 mounted feature 下再派发挂载事件。

事件处理

  • 根元素上只挂一个委托监听器,事件发生后沿 DOM 树向上查找 data-dioxus-id 属性定位目标元素;
  • WebEventConverter 负责把 web_sys 事件转换为 Dioxus 类型;
  • 支持所有标准 DOM 事件(鼠标、键盘、触摸等)。

Hydration 系统

文档描述的五步水合流程(packages/web/src/hydration/ 目录承载其实现):

  1. SSR 服务端渲染出带 dio_el 数据属性的 HTML(SSR 实现在 packages/ssr/src/);
  2. 客户端接收 base64 编码的 hydration 上下文;
  3. VirtualDOM 以 skip_mutations = true 重建,即不产生 DOM 写入;
  4. 客户端遍历预渲染的 DOM,为节点分配 element id;
  5. 对 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 直接写入其内部的 MutationStateis_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.rspackages/desktop/src/assets.rs

Native 能力与配置

原生特性方面:通过 muda 集成菜单、trayicon 系统托盘、global_hotkey 全局热键、文件对话框与拖拽支持,以及供自动化测试使用的 headless 模式(对应 packages/desktop/src/menubar.rstrayicon.rsshortcut.rsfile_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::LocalPoolHandlenew() 时按 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 组件

  1. INTERPRETER_JS —— 基础解释器类;
  2. NATIVE_JS —— 平台特定扩展;
  3. SLEDGEHAMMER_JS —— 由 Rust 生成绑定的编码实现。

MutationState:为二进制序列化服务的 writer

MutationStatepackages/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_textappend_children_topreplace_top_withset_current_attributeforeign_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
  • LiveViewtokio::select! 多路等待(见上文每客户端生命周期)。

5. Mutation 批处理

所有非 Web 直连渲染器都遵循“先累积、后冲刷”的批处理:mutation 累积在临时结构(MutationState/WryQueue)中,定期 flush 到传输层。Web 端同样有 interpreter.flush() 的批量落 DOM 语义,只是“传输层”换成了本地 JS 解释器。

如何为 Dioxus 新增一个渲染器

架构文档最后给出了接入新后端的五步清单,结合前文源码可以落到实处:

  1. 实现 WriteMutations:定义平台如何应用类 DOM 的 mutations、处理模板系统(注意 can_cache_template_roots 的语义)、维护堆栈机状态;可参考 packages/web/src/mutations.rs(直连式)或 packages/interpreter/src/write_native_mutations.rs(序列化式)两种范式。
  2. 实现 HtmlEventConverter:把平台事件映射到 Dioxus 事件类型,需要序列化时参考 LiveView 的 SerializedHtmlEventConverterpackages/liveview/src/events.rs)。
  3. 创建 Launch 函数:构造 VirtualDOM、初始化渲染器结构、进入平台事件循环,并在有工作时调用 vdom.render_immediate(&mut mutations)
  4. 定义配置:创建 Config 结构体,支持 Box<dyn Any> downcast。
  5. 可选增强:自定义 mount 事件数据、Document 集成、History 支持、资产分发(参考 packages/desktop/src/protocol.rsdioxus:// 协议实现)。

小结:一张表看懂四大渲染器

维度 Web Desktop Native LiveView
底层技术 wasm-bindgen + Sledgehammer JS Wry/Tao + WebSocket Blitz + Vello + Winit 服务端 VirtualDOM + WebSocket
WriteMutations 实现 WebsysDom 直委解释器 WryQueueMutationState 直接写入 Blitz DOM MutationState
事件回程 根节点委托监听 + data-dioxus-id postMessage → IPC Winit 窗口事件 上行 WebSocket 事件帧
特色能力 Hydration/流式水合 自定义协议、托盘、热键 无浏览器引擎的自绘管线 多客户端连接池、元素查询

从源码结构看,Dioxus 渲染器架构的关键取舍在于:把“diff 输出什么”(VirtualDOM 侧、与平台无关)和“如何应用到平台”(WriteMutations 实现侧)彻底解耦,再用 Sledgehammer 二进制协议统一所有需要跨进程/跨线程传输 mutation 的场景(Desktop 的 WebSocket、LiveView 的 WebSocket),最终使得同一段组件代码可以在四类后端上以完全一致的状态模型运行。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
983
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384