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:556history.rs:44 的 hydration 数据处理逻辑,怀疑是模板替换过程中只处理了 {INNER} 之前的插入点,未正确移除占位符文本节点。

临时修复

inline.jsDOMContentLoaded 回调中手动清理:

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="..."> 原生链接后正常。

复现步骤

  1. 使用 dx serve --platform server --port 5051 启动 fullstack 应用
  2. 导航到 /admin/settings
  3. 点击侧边栏中站点设置下的子菜单项(如 SEO、第三方服务)
  4. 期望:nav.push("/admin/settings/seo") 被执行,URL 跳转
  5. 实际:无任何反应,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:

  1. 新增路由变体 AdminSettingsTab { tab: String }(路径 /admin/settings/:tab
  2. Settings 组件通过 use_route::<Route>() 读取当前 tab
  3. 侧边栏子菜单改为 <a href="/admin/settings/seo"> 链接
  4. 高亮状态通过匹配当前路由判断
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

提交的 Issues

dioxusrustssrhydrationbug