Dioxus SSR 文章排序 Bug 排查与修复记录
Dioxus SSR 文章排序 Bug 排查与修复记录
1. 问题现象
在博客首页和系列文章列表中,本应按照 order 字段升序排列的系列教程文章,显示顺序却是乱的。例如 Dioxus 系列 30 篇文章的 slug 顺序为:
dioxus-22, dioxus-13, dioxus-03, dioxus-12, ... // ❌ 乱序
而期望的顺序是:
dioxus-01, dioxus-02, dioxus-03, ..., dioxus-30 // ✅ 按 order 升序
2. 排查过程
2.1 确认数据层排序正确
首先检查文章加载的核心逻辑 content.rs,排序代码看起来是合理的:
self.by_slug.sort_by(|a, b| match (a.series.as_ref(), b.series.as_ref()) {
(Some(a_series), Some(b_series)) if a_series == b_series => {
match (a.order, b.order) {
(Some(a_order), Some(b_order)) => a_order.cmp(&b_order),
// ...
}
}
_ => b.date.cmp(&a.date),
});
为了验证,我们在 rebuild() 方法中添加了调试输出,打印排序后的文章列表。
2.2 发现问题 1:调试代码导致崩溃
启动服务器后,页面反而打不开了!查看日志发现:
thread panicked at src/content.rs:165:31:
end byte index 28 is not a char boundary; it is inside '南' (bytes 27..30)
原因:调试代码中使用 &article.title[..28] 来截取标题前 28 个字符显示,但 Rust 的字符串切片是按 字节 操作的。中文标题"项目部署与..."的第 28 个字节正好在一个 3 字节中文字符的中间,导致运行时 panic。
这个 panic 发生在 rebuild() 的 println! 中——它获取了 RwLock 的写锁,然后在持有锁的情况下 panic 了。Rust 的 std::sync::RwLock 在 panic 后会进入毒化(poisoned) 状态,所有后续尝试获取该锁的操作都会返回 PoisonError。
2.3 发现问题 2:RwLock 毒化导致连锁崩溃
由于 REGISTRY 这个 OnceLock<RwLock<ArticleRegistry>> 被毒化,所有后续页面请求在调用 load_all_articles() 时都会 panic:
let mut guard = reg.write().unwrap(); // panic! PoisonError
这造成了连锁反应——首页、文章列表页、系列页全部报错,看起来像是"排序不生效",实际是根本没有正确返回文章数据。
2.4 发现问题 3:前端渲染层未按 order 排序
修复 panic 后重新观察排序输出,确认 by_slug 中排序正确。但页面上显示仍然不对——问题的根因不在数据层,而在前端渲染层。
查看 blog_list.rs,代码按年份分组后没有对组内文章重新排序:
// 年份分组
let mut year_map: std::collections::BTreeMap<String, Vec<&ArticleSummary>> = ...;
for a in &articles {
let year = a.date.get(..4).unwrap_or("0000").to_string();
year_map.entry(year).or_default().push(a); // ❌ 未排序
}
虽然 content.rs 中的全局排序保证了某些顺序,但同一系列的文章可能分散在多年份中,且同一年的文章顺序在放入 Vec 时只取决于 articles 的原始遍历顺序。
而 home.rs 首页在提取系列文章时,也只做了筛选 .filter(),没有对筛选结果排序。
3. 解决方案
3.1 修复 UTF-8 字符安全截断
// ❌ 字节截断,遇到中文会 panic
&article.title[..std::cmp::min(article.title.len(), 28)]
// ✅ 字符截断,安全处理中文
let preview: String = article.title.chars().take(28).collect();
3.2 修复 RwLock 毒化
// ❌ unwrap 会在锁毒化时 panic
let mut guard = reg.write().unwrap();
// ✅ 从毒化状态恢复
let mut guard = reg.write().unwrap_or_else(|e| e.into_inner());
into_inner() 会获取被毒化锁的内部数据,使得后续操作可以继续进行。这对于非关键数据的缓存场景是合理的——一次渲染 panic 不应该导致整个应用不可用。
3.3 前端渲染层增加排序
在 blog_list.rs 的年份分组中,对每个年份组内的文章增加排序逻辑:
for items in year_map.values_mut() {
items.sort_by(|a, b| match (a.series.as_ref(), b.series.as_ref()) {
(Some(a_series), Some(b_series)) if a_series == b_series => {
match (a.order, b.order) {
(Some(a_order), Some(b_order)) => a_order.cmp(&b_order),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => b.date.cmp(&a.date),
}
}
_ => b.date.cmp(&a.date),
});
}
在 home.rs 的系列文章筛选中同样增加按 order 排序:
items.sort_by(|a, b| match (a.order, b.order) {
(Some(a_order), Some(b_order)) => a_order.cmp(&b_order),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => b.date.cmp(&a.date),
});
4. 经验教训
4.1 不要在持有锁的上下文中做可能 panic 的操作
println! 中的字符串切片看似无害,但遇到非 ASCII 字符就会出问题。持有锁的代码路径必须格外小心,任何 panic 都会污染共享状态。
4.2 数据层排序 ≠ 渲染层排序
content.rs 中的全局排序不能保证前端渲染时保持。特别是当数据经过分组、筛选等变换操作后,原始顺序可能丢失。前端渲染层也需要独立的排序逻辑。
4.3 Rust 字符串切片与 UTF-8
Rust 的 str 是 UTF-8 编码的,string[index] 形式的索引是不允许的(编译时拒绝),但 &string[..n] 的切片操作是允许的——它会在运行时检查边界是否在字符边界上。对于包含中文的文本,应当使用 .chars() 迭代器来安全处理。