第五章:列表渲染与条件渲染
博客v1.0系列教程(Dioxus)博客 v1.0 系列教程 (Dioxus)
第五章:列表渲染与条件渲染
1. 列表渲染
1.1 for 循环
Dioxus 推荐在 RSX 中使用 for 循环来渲染列表,而不是使用 .map() 迭代器:
#[component]
fn ArticleList() -> Element {
let articles = vec![
ArticleSummary { title: "Dioxus 入门".into(), slug: "dioxus-intro".into() },
ArticleSummary { title: "Rust 基础".into(), slug: "rust-basics".into() },
];
rsx! {
div { class: "space-y-4",
for article in &articles {
ArticleCard { summary: article.clone() }
}
}
}
}
1.2 带索引的循环
rsx! {
ol { class: "list-decimal pl-6",
for (index, item) in items.iter().enumerate() {
li { key: "{index}", "{index + 1}. {item}" }
}
}
}
1.3 迭代器方式
// for 循环是首选,也可以用迭代器
rsx! {
ul {
{items.iter().map(|item| rsx! {
li { "{item}" }
})}
}
}
2. Key 属性的重要性
Key 帮助 Dioxus 识别列表中的每个元素,优化重渲染:
// ✅ 推荐:使用稳定唯一的 ID
for article in &articles {
ArticleCard { key: "{article.slug}", summary: article.clone() }
}
// ❌ 不推荐:使用索引(列表变化时会导致问题)
for (i, article) in articles.iter().enumerate() {
ArticleCard { key: "{i}", summary: article.clone() }
}
// ❌ 错误:不使用 key(每次全部重渲染)
for article in &articles {
ArticleCard { summary: article.clone() }
}
key 的最佳实践:
- 使用数据本身的唯一标识符(ID、slug、UUID)
- 在同级兄弟间唯一即可
- 稳定不变,不要在每次渲染时生成新 key
3. 条件渲染
3.1 if 表达式
rsx! {
div {
// 简单的条件
if is_loading() {
Spinner {}
}
// if-else
if let Some(user) = current_user() {
UserProfile { user: user.clone() }
} else {
LoginPrompt {}
}
}
}
3.2 match 多分支
enum PageState {
Loading,
Loaded(Vec<Article>),
Error(String),
Empty,
}
rsx! {
div {
match state() {
PageState::Loading => rsx! {
div { class: "text-center py-8", "加载中..." }
},
PageState::Loaded(articles) => rsx! {
for a in &articles {
ArticleCard { summary: a.clone() }
}
},
PageState::Error(msg) => rsx! {
div { class: "text-red-500", "错误: {msg}" }
},
PageState::Empty => rsx! {
div { class: "text-gray-400 text-center py-8", "暂无文章" }
},
}
}
}
3.3 短路渲染
// 仅在条件满足时渲染该项
rsx! {
div { class: "article-meta",
if let Some(ref series) = article.series {
span { class: "badge", "{series}" }
}
if let Some(ref cat) = article.category {
span { class: "badge", "{cat}" }
}
if !article.tags.is_empty() {
div { class: "tags",
for tag in &article.tags {
span { class: "tag", "{tag}" }
}
}
}
}
}
4. 空状态处理
#[component]
fn ArticleListView(articles: Vec<ArticleSummary>) -> Element {
rsx! {
div { class: "space-y-4",
if articles.is_empty() {
// 空状态展示
div { class: "text-center py-16",
div { class: "text-5xl mb-4", "📭" }
h3 { class: "font-semibold mb-2", "暂无文章" }
p { class: "text-sm", style: "color: var(--tertiary);",
"还没有发布任何文章"
}
a {
class: "mt-4 inline-block px-4 py-2 rounded-lg text-white",
style: "background: var(--primary);",
href: "/write",
"写第一篇文章"
}
}
} else {
for article in &articles {
ArticleCard { key: "{article.slug}", summary: article.clone() }
}
}
}
}
}
// 更加通用的空状态组件
#[component]
fn EmptyState(
icon: String,
title: String,
description: String,
action: Option<(String, String)>, // (按钮文字, 链接)
) -> Element {
rsx! {
div { class: "text-center py-16",
div { class: "text-5xl mb-4", "{icon}" }
h3 { class: "font-semibold mb-2", "{title}" }
p { class: "text-sm", style: "color: var(--tertiary);", "{description}" }
if let Some((label, href)) = action {
a {
class: "mt-4 inline-block px-4 py-2 rounded-lg text-white",
style: "background: var(--primary);",
href: "{href}",
"{label}"
}
}
}
}
}
5. 实战:文章列表页
综合运用条件渲染和列表渲染:
#[component]
fn BlogPage() -> Element {
let articles = use_signal(|| Vec::<ArticleSummary>::new());
let loading = use_signal(|| true);
let error = use_signal(|| Option::<String>::None);
// 加载数据(简化)
use_effect(move || {
loading.set(true);
spawn(async move {
match fetch_articles().await {
Ok(list) => {
articles.set(list);
loading.set(false);
}
Err(e) => {
error.set(Some(e));
loading.set(false);
}
}
});
});
rsx! {
div { class: "max-w-4xl mx-auto",
h1 { class: "text-2xl font-bold mb-6", "文章列表" }
// 三种状态:加载中 / 错误 / 正常
if loading() {
div { class: "space-y-4",
// 骨架屏
for _ in 0..3 {
SkeletonCard {}
}
}
} else if let Some(ref msg) = error() {
EmptyState {
icon: "😵".to_string(),
title: "加载失败".to_string(),
description: msg.clone(),
action: Some(("重试".to_string(), "#".to_string())),
}
} else if articles().is_empty() {
EmptyState {
icon: "📭".to_string(),
title: "暂无文章".to_string(),
description: "还没有发布任何文章".to_string(),
action: None,
}
} else {
div { class: "space-y-4",
for article in articles.read().iter() {
ArticleCard { key: "{article.slug}", summary: article.clone() }
}
// 文章计数
p { class: "text-sm text-center",
style: "color: var(--tertiary);",
"共 {articles.read().len()} 篇"
}
}
}
}
}
}
6. 常见陷阱
// ❌ 错误:条件中调用 Hook
if condition {
let data = use_resource(|| ...); // Hook 不能在条件中
}
// ✅ 正确:Hook 始终在最外层
let data = use_resource(|| ...);
if condition {
// 使用 data
}
// ❌ 错误:循环中创建组件
for i in 0..10 {
#[component] // 不能在循环中定义组件
fn Item() -> Element { ... }
}
// ✅ 正确:组件在顶层定义
// for 循环只使用组件
7. 小结
- 使用
for循环渲染列表,比迭代器更直观 - 始终使用稳定的
key属性,帮助 Diff 算法优化 if/match实现条件渲染,覆盖所有 UI 状态- 空状态、加载态、错误态、正常态四种状态都要处理
- 骨架屏(Skeleton)提升加载体验
- 下一章将学习路由与导航
dioxuslistforifmatchkey