fix(post): 修复上/下一篇导航后文章内容不更新

点击文章底部的上一篇/下一篇后,URL 正确变化为 /post/<new-slug>,但页面
内容仍停留在旧文章,只有刷新浏览器才生效。

根因: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::<Route>() 读取当前
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 变,标题随之变为新文章
This commit is contained in:
xfy 2026-07-09 15:34:25 +08:00
parent 6a3ac038da
commit 79978aa4a4

View File

@ -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::<Route>()` 读取当前 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 再手动 setrender 期 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 || {
// 在闭包内读取当前 slugcurrent() 内部会 subscribe_to_current_context()
// 把订阅注册到 use_server_future 的 ReactiveContext路由变化即重跑。
let current_slug = match router.current::<Route>() {
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 {