From b95777e58d422b5b7b9fb13b90fa5f729bf8095f Mon Sep 17 00:00:00 2001 From: xfy Date: Wed, 8 Jul 2026 10:56:35 +0800 Subject: [PATCH] =?UTF-8?q?fix(editor):=20=E4=BF=AE=E5=A4=8D=20SSR=20?= =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E8=AE=BF=E9=97=AE=E6=97=B6=E5=8F=AF=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E4=BB=A3=E7=A0=81=E5=9D=97=E4=B8=8D=E6=8C=82=E8=BD=BD?= =?UTF-8?q?=20CodeMirror?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRunner 原用 use_hook(|| format!("code-runner-{}", now_pseudo_unique())) 生成容器 id,后缀依赖 now_millis() 时间戳。Dioxus hydration 不传递 use_hook 状态——SSR 渲染与客户端 hydration 各执行一次 use_hook,时间戳 (以及顺带验证过的 ScopeId)两端不同,导致 CodeMirror create() 在 hydration 时用新 id 去 getElementById,找不到 SSR 渲染的容器(id 是旧值),返回 null 不挂载。路由跳转是纯客户端渲染,use_hook 只跑一次 id 一致,故正常。 改为由父组件传入确定性的 instance_id(PostContent 的片段索引 i,来自纯函数 split_content_fragments 对同一 content_html 的解析,SSR 与 hydration 一致), container_id 直接派生自该 prop,不进 use_hook。admin 试运行页单实例固定 0。 Playwright(release 构建)验证:SSR 直接访问与路由跳转 .cm-editor 均为 1。 --- .gitignore | 2 ++ src/components/code_runner/runner.rs | 22 ++++++++++------------ src/components/post/post_content.rs | 4 ++++ src/pages/admin/runner.rs | 3 ++- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 4c1ed60..7b05711 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ profile.json.gz # FreeBSD cross-compile sysroot (extracted from base.txz, machine-local) .freebsd-sysroot/ + +.zcode/ diff --git a/src/components/code_runner/runner.rs b/src/components/code_runner/runner.rs index 1df9b01..d2bb5d0 100644 --- a/src/components/code_runner/runner.rs +++ b/src/components/code_runner/runner.rs @@ -24,6 +24,11 @@ const MAX_POLLS: u32 = 240; // 500ms * 240 = 120s 上限 /// - `source`:初始源码(首次挂载用于初始化编辑器;之后编辑器内容是唯一真源)。 /// - `language`:语言标识(python / node 等)。 /// - `overrides`:可选资源限制覆盖。 +/// - `instance_id`:实例在父级片段序列中的索引,用作 CodeMirror 容器 id 后缀。 +/// 必须是 **SSR/hydration 确定性**的值(如父组件 `for (i, ..)` 的索引 `i`)—— +/// Dioxus hydration 不传递 use_hook 状态,任何在 use_hook 内基于运行时状态 +/// (时间戳 / 随机 / ScopeId)生成的 id,在 SSR 与 hydration 两端会不一致, +/// 导致 CodeMirror `create()` 在 hydration 时找不到 SSR 渲染的容器元素。 /// /// `mut` 信号仅在 WASM 的 spawn 闭包内被 `.set()`,server 构建会触发 unused_mut, /// 故按项目惯例加 `cfg_attr` 放行(参见 AGENTS.md「mut bindings needed only on WASM」)。 @@ -33,6 +38,7 @@ pub fn CodeRunner( source: String, language: String, overrides: Option, + instance_id: usize, ) -> Element { let mut running = use_signal(|| false); let mut stage = use_signal(String::new); @@ -43,9 +49,10 @@ pub fn CodeRunner( // 编辑器内容的唯一真源;初始化为 prop 值。 let mut source_signal = use_signal(|| source.clone()); - // 为每个实例生成稳定的容器 id(CodeMirror 容器,由本组件在 WASM 端挂载)。 - // use_hook 保证只算一次;后缀用时间戳+原子计数避免同页多实例 id 冲突。 - let container_id = use_hook(|| format!("code-runner-{}", now_pseudo_unique())); + // CodeMirror 容器 id:直接由确定性 prop 派生(不进 use_hook)。 + // instance_id 由父组件从纯函数片段解析的索引传入,SSR 与 hydration 同一 content_html + // → 同一片段序列 → 同一索引 → 同一 id,故 hydration 时 create() 能找到 SSR 渲染的容器。 + let container_id = format!("code-runner-{instance_id}"); // —— CodeMirror 挂载(仅 WASM)—— // 范式镜像 src/pages/admin/system.rs 的 SQL 控制台与 src/pages/admin/write.rs 的 Tiptap。 @@ -282,12 +289,3 @@ fn status_label(status: &ExecStatus) -> String { ExecStatus::RateLimited => "请求过频".to_string(), } } - -/// 生成一个伪唯一后缀(基于时间戳 + 计数器),用于容器 id。 -/// 非安全用途,仅避免同页多实例 id 冲突。 -fn now_pseudo_unique() -> String { - use std::sync::atomic::{AtomicU64, Ordering}; - static COUNTER: AtomicU64 = AtomicU64::new(0); - let n = COUNTER.fetch_add(1, Ordering::Relaxed); - format!("{}-{}", crate::utils::time::now_millis(), n) -} diff --git a/src/components/post/post_content.rs b/src/components/post/post_content.rs index 5f749d3..2456932 100644 --- a/src/components/post/post_content.rs +++ b/src/components/post/post_content.rs @@ -162,6 +162,10 @@ pub fn PostContent(content_html: String) -> Element { source: source.clone(), language: lang.clone(), overrides: overrides.clone(), + // i 是片段序列中的确定性索引(来自纯函数 split_content_fragments + // 对同一 content_html 的解析),SSR 与 hydration 一致,用作容器 + // id 后缀保证 hydration 时 CodeMirror 能找到 SSR 渲染的容器。 + instance_id: i, } }, }} diff --git a/src/pages/admin/runner.rs b/src/pages/admin/runner.rs index 0386271..4838459 100644 --- a/src/pages/admin/runner.rs +++ b/src/pages/admin/runner.rs @@ -113,11 +113,12 @@ pub fn Runner() -> Element { } } - // 运行器 + // 运行器(admin 试运行页单实例、纯客户端渲染,instance_id 固定 0 即可) CodeRunner { source: source(), language: lang(), overrides: overrides, + instance_id: 0, } } }