Dioxus 0.7 SSR/Hydration 踩坑记录
Dioxus 0.7 SSR/Hydration 踩坑记录
背景
项目 rabitlogic-blog 使用 Dioxus 0.7 fullstack 模式构建,包含前台博客展示和后台管理面板。在开发过程中遇到了三个影响较大的问题,均已确认复现。
{INNER} id="bug-1-ssr-模板-占位符残留">Bug 1: SSR 模板 占位符残留
现象
assets/index.html 中的 SSR 占位符 {INNER} 在服务端渲染后未能被替换,作为纯文本节点残留在 DOM 中:
<div id="main">
<!-- Dioxus SSR 注入的内容 -->
<script>window.initial_dioxus_hydration_data="...";</script>
{INNER} ← 未被替换!
</div>
在浏览器中肉眼不可见(因被其他元素遮挡或无样式),但在辅助功能树和 DOM 中确实存在。
分析
Dioxus 0.7 fullstack 的 SSR 引擎读取 assets/index.html,查找 {INNER} 占位符并应将其替换为渲染后的 HTML 内容。但实际行为是:渲染内容被追加到 {INNER} 之前,而 {INNER} 本身作为文本节点保留。
查看 Dioxus Server 源码 ssr.rs:556 和 history.rs:44 的 hydration 数据处理逻辑,怀疑是模板替换过程中只处理了 {INNER} 之前的插入点,未正确移除占位符文本节点。
临时修复
在 inline.js 的 DOMContentLoaded 回调中手动清理:
document.querySelectorAll("#main").forEach(function (el) {
el.childNodes.forEach(function (node) {
if (node.nodeType === 3 && node.textContent.trim() === "{INNER}") {
node.remove();
}
});
});
预期行为
SSR 完成后,{INNER} 占位符应被完全替换,不留任何文本节点残留。
<div onclick> id="bug-2-hydration-模式下-事件不触发">Bug 2: Hydration 模式下 事件不触发
现象
在 AdminShell 侧边栏中使用 <div onclick={...}> 注册的点击事件,在 hydration 完成后不触发。改用 <a href="..."> 原生链接后正常。
复现步骤
- 使用
dx serve --platform server --port 5051启动 fullstack 应用 - 导航到
/admin/settings - 点击侧边栏中站点设置下的子菜单项(如 SEO、第三方服务)
- 期望:
nav.push("/admin/settings/seo")被执行,URL 跳转 - 实际:无任何反应,URL 不变
代码示例
不工作(div + onclick):
rsx! {
div {
class: "...",
onclick: move |_| {
nav.push(format!("/admin/settings/{tab}"));
},
span { "{icon}" }
span { "{label}" }
}
}
工作(<a> + href):
rsx! {
a {
class: "...",
href: "{href}",
span { "{icon}" }
span { "{label}" }
}
}
分析
查看渲染后的 HTML,Dioxus 0.7 使用事件委托机制(hydration marker data-node-hydration="21,click:1")。事件监听器注册在根元素上,通过事件冒泡分发。
但在 fullstack 模式下,hydration 过程可能未能正确附加这些委托事件处理器。表现为:
- 通过 Playwright
page.click()、element.dispatchEvent('click')均无效 - 控制台无 JavaScript 错误
- 普通的
<a>链接点击正常(由浏览器原生处理)
影响范围
所有通过 rsx! { div { onclick: ... } } 注册的点击处理器在 SSR hydration 后均可能不触发。这会影响:
- 自定义导航菜单
- Tab 切换
- Dropdown 等交互组件
临时修复
将交互元素改为 <a> 标签 + href,利用 Dioxus Router 的链接拦截机制,或浏览器原生导航(fallback)。
childrenOutlet id="bug-3-context-无法通过-prop-穿透">Bug 3: Context 无法通过 prop 穿透
现象
在 AdminShell 中通过 use_context_provider(|| settings_tab) 提供 Signal<String> context,但在 Settings 组件(位于 Outlet<Route> 内部)中通过 use_context::<Signal<String>>() 无法获取到该 context。
架构示意图
AppShell (route layout)
└─ AdminShell ← 此处 use_context_provider(|| signal)
└─ children = Outlet<Route> ← 在 AppShell 中创建,作为 children 传入
└─ AdminSettings
└─ Settings ← 此处 use_context() 找不到 signal
分析
Outlet::<Route> {} 是在 AppShell 中创建的 VNode,然后通过 AdminShell { Outlet::<Route> {} } 作为 children prop 传入 AdminShell。
在 Dioxus 0.7 中,context 的传播依赖于 VNode 创建时的组件层次,而不是渲染时的 DOM 树层次。因为 Outlet 是在 AppShell 的作用域中创建的,即使 AdminShell 渲染了 {children},Outlet 内部组件的 context 查找仍然回溯到 AppShell 的上下文链。
这导致 AdminShell 提供的 context 对 Settings 不可见。
临时修复
改用 URL 路径 传递状态,完全绕过 context:
- 新增路由变体
AdminSettingsTab { tab: String }(路径/admin/settings/:tab) Settings组件通过use_route::<Route>()读取当前 tab- 侧边栏子菜单改为
<a href="/admin/settings/seo">链接 - 高亮状态通过匹配当前路由判断
let active = match &route {
Route::AdminSettingsTab { tab: t } => t == &tab,
Route::AdminSettings => tab == "basic",
_ => false,
};
预期行为
use_context_provider 提供的 context 应对所有 DOM 后代可见,包括通过 children prop 传入的 VNode 内部组件。
环境信息
- Dioxus 版本: 0.7.1 (crates.io)
- Dioxus Server 版本: 0.7.9
- Dioxus Fullstack Core 版本: 0.7.9
- 平台: Windows (dx serve --platform server)
- 浏览器: Chromium (Playwright 测试 + 手动测试)
- 构建工具: dx CLI