第五章:列表渲染与条件渲染

博客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