From 79978aa4a46a05cbb5d7e7e36add0c5be2441eb4 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 9 Jul 2026 15:34:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(post):=20=E4=BF=AE=E5=A4=8D=E4=B8=8A/?= =?UTF-8?q?=E4=B8=8B=E4=B8=80=E7=AF=87=E5=AF=BC=E8=88=AA=E5=90=8E=E6=96=87?= =?UTF-8?q?=E7=AB=A0=E5=86=85=E5=AE=B9=E4=B8=8D=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 点击文章底部的上一篇/下一篇后,URL 正确变化为 /post/,但页面 内容仍停留在旧文章,只有刷新浏览器才生效。 根因:use_server_future(内部即 use_resource)依赖 ReactiveContext 追踪 闭包内读取的 signal 来决定重跑时机。但 PostDetail 的 slug 是路由宏注入的 普通 String prop,move 进闭包后成为冻结快照,读取它不建立任何反应式订阅。 同一 Route::PostDetail 变体间的导航(/post/a -> /post/b)会复用组件实例、 仅更新 props,而 use_server_future 的 task 只在首次 hook 时创建一次,因此 永远不会重跑。 此前 commit 225bb24 把 slug_signal 镜像 + render 期 set 改成直接读 prop, 消除了反模式,但也丢掉了唯一能触发重跑的订阅源(slug_signal 的读取), 反而引入了这个 bug。 修复:在闭包内通过 dioxus::router::router().current::() 读取当前 slug。current() 内部调用 subscribe_to_current_context(),在 use_server_future 的 ReactiveContext 中注册订阅,路由变化即触发重跑。render body 保持纯净。 验证(headless chromium,本地需设 APP_BASE_URL 绕过 CSRF 403): - 修复前:点 Next 后 URL 变 /post/1783580058,标题仍为旧文章(复现 bug) - 修复后:URL 变,标题随之变为新文章 --- src/pages/post_detail.rs | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/pages/post_detail.rs b/src/pages/post_detail.rs index 9a5052f..30e8199 100644 --- a/src/pages/post_detail.rs +++ b/src/pages/post_detail.rs @@ -4,7 +4,18 @@ //! //! 数据获取:通过 `use_server_future` 调用 `get_post_by_slug` server function, //! 根据 URL 中的 slug 获取单篇文章详情(含正文 HTML、目录、封面及上下篇导航)。 -//! 路由参数 slug 变化时,Dioxus 会自动重跑该 future,无需镜像成 signal 再手动同步。 +//! +//! # 反应式取数的关键 +//! Dioxus 0.7 的 `use_server_future`(内部即 `use_resource`)只在闭包内读取的 +//! **signal** 变化时才会重跑 future——它通过 `ReactiveContext` 追踪闭包执行期间的 +//! 订阅。但本组件的 `slug` 是路由宏注入的普通 `String` prop,被 `move` 进闭包后 +//! 成了冻结快照,读取它不会建立订阅。因此上/下一篇导航(同一路由变体间的 slug +//! 变化)会复用组件实例、更新 props,却无法触发 future 重跑——表现为「URL 变了 +//! 但内容不变,刷新才生效」。 +//! +//! 修复:在闭包内通过 `router().current::()` 读取当前 slug。`current()` +//! 内部调用 `subscribe_to_current_context()`,在 `use_server_future` 的 +//! ReactiveContext 中注册订阅;路由变化时订阅触发,future 自动重跑。 //! 在 `wasm32` 目标下,server function 的函数体被替换为向服务端端点发起 HTTP POST 请求的客户端存根; //! 实际的数据库访问逻辑仅在 `feature = "server"` 启用时运行。 @@ -27,9 +38,22 @@ use crate::router::Route; /// 若文章不存在或加载失败,则展示对应的提示页面。 #[component] pub fn PostDetail(slug: String) -> Element { - // 直接读取 slug prop:路由参数变化时 Dioxus 会自动重跑该 server future, - // 无需镜像成 signal 再手动 set(render 期 set signal 是 Dioxus 反模式)。 - let post = use_server_future(move || get_post_by_slug(slug.clone()))?; + // 取得路由上下文句柄(不订阅组件层渲染,仅在闭包内按需订阅)。 + // 见模块文档:必须在闭包内读取路由状态才能建立反应式订阅,future 才会在 + // slug 变化(上/下一篇导航)时重跑。`slug` prop 本身是冻结的 String 快照, + // 不能作为依赖。 + let router = dioxus::router::router(); + + let post = use_server_future(move || { + // 在闭包内读取当前 slug:current() 内部会 subscribe_to_current_context(), + // 把订阅注册到 use_server_future 的 ReactiveContext,路由变化即重跑。 + let current_slug = match router.current::() { + Route::PostDetail { slug } => slug, + // 组件卸载/路由切走的瞬间可能命中其它变体,退回用 prop 值兜底。 + _ => slug.clone(), + }; + get_post_by_slug(current_slug) + })?; // 将结果映射为更直观的 Ok(post) / Err("not_found") / Err("error") 三种状态。 let post_data = post.read().as_ref().map(|r| match r {