第六章:路由与导航

博客v1.0系列教程(Dioxus)博客 v1.0 系列教程 (Dioxus)

第六章:路由与导航

1. 为什么需要路由

单页应用(SPA)的所有代码在同一个页面加载,通过路由控制显示哪个"页面"组件:

/          → 首页
/blog      → 文章列表
/article/1 → 文章详情
/login     → 登录页

路由的核心能力:

  • URL 与组件一一对应
  • 浏览器前进/后退正常工作
  • 支持链接和程序化导航

2. 定义路由

2.1 路由枚举

use dioxus::prelude::*;

#[derive(Routable, Clone, PartialEq)]
enum Route {
    #[route("/")]
    Home {},

    #[route("/blog")]
    BlogList {},

    #[route("/article/:slug")]
    BlogPost { slug: String },

    #[route("/login")]
    Login {},
}

2.2 路由组件与 App

// 每个页面组件
#[component]
fn Home() -> Element {
    rsx! { h1 { "首页" } }
}

#[component]
fn BlogList() -> Element {
    rsx! { h1 { "文章列表" } }
}

#[component]
fn BlogPost(slug: String) -> Element {
    rsx! { h1 { "文章: {slug}" } }
}

#[component]
fn Login() -> Element {
    rsx! { h1 { "登录" } }
}

// App 中挂载路由
#[component]
fn App() -> Element {
    rsx! {
        Router::<Route> {}
    }
}

2.3 Cargo.toml

[dependencies]
dioxus = { version = "0.7", features = ["router"] }

3. 页面导航

use dioxus::prelude::*;

#[component]
fn NavBar() -> Element {
    rsx! {
        nav { class: "flex gap-4 p-4",
            Link { to: Route::Home {}, "首页" }
            Link { to: Route::BlogList {}, "文章" }
            Link { to: Route::Login {}, "登录" }
        }
    }
}

3.2 use_navigator 编程式导航

#[component]
fn LoginPage() -> Element {
    let navigator = use_navigator();

    let do_login = move |_| {
        // 登录成功后跳转
        navigator.push(Route::Home {});
    };

    rsx! {
        button { onclick: do_login, "登录" }
    }
}

// 更多导航方式
navigator.push(Route::Home {});        // 推入新页面
navigator.replace(Route::Home {});     // 替换当前页面
navigator.go_back();                    // 后退

4. 路由参数

4.1 动态路径参数

#[derive(Routable, Clone, PartialEq)]
enum Route {
    #[route("/article/:slug")]
    BlogPost { slug: String },

    #[route("/category/:id")]
    Category { id: u32 },

    #[route("/user/:user_id/post/:post_id")]  // 多参数
    UserPost { user_id: String, post_id: String },
}

// 在组件中接收参数
#[component]
fn BlogPost(slug: String) -> Element {
    // slug 自动从 URL 中提取
    rsx! { h1 { "查看文章: {slug}" } }
}

4.2 查询参数

use serde::Deserialize;

#[derive(Deserialize)]
struct SearchParams {
    q: String,
    page: Option<u32>,
}

#[component]
fn SearchPage() -> Element {
    // 从 URL 查询参数中读取 ?q=xxx&page=1
    let params = use_route().query::<SearchParams>();

    match params {
        Ok(p) => rsx! {
            div { "搜索: {p.q}, 页码: {p.page.unwrap_or(1)}" }
        },
        Err(_) => rsx! {
            div { "请在 URL 中提供 ?q=xxx 参数" }
        },
    }
}

5. 嵌套路由与布局

5.1 布局组件

#[derive(Routable, Clone, PartialEq)]
enum Route {
    #[layout(DefaultLayout)]   // 此类路由共享一个布局
        #[route("/")]
        Home {},

        #[route("/blog")]
        BlogList {},
    #[end_layout]

    #[route("/login")]
    Login {},                    // 没有布局包裹
}

#[component]
fn DefaultLayout() -> Element {
    rsx! {
        div { class: "min-h-screen",
            NavBar {}           // 所有页面共享导航栏
            main { class: "p-6",
                Outlet::<Route> {}    // 子页面内容在此渲染
            }
            Footer {}
        }
    }
}

5.2 嵌套路由

#[derive(Routable, Clone, PartialEq)]
enum Route {
    #[nest("/admin")]
    #[layout(AdminLayout)]
        #[route("/admin")]
        AdminDashboard {},
        #[route("/admin/articles")]
        ArticleManager {},
        #[route("/admin/settings")]
        Settings {},
    #[end_nest]
}

6. 404 页面

#[derive(Routable, Clone, PartialEq)]
enum Route {
    // ... 其他路由

    #[route("/:404")]
    NotFound {},
}

#[component]
fn NotFound() -> Element {
    rsx! {
        div { class: "text-center py-16",
            h1 { class: "text-6xl font-bold mb-4", "404" }
            p { class: "text-lg mb-6", "页面未找到" }
            Link { to: Route::Home {}, "返回首页" }
        }
    }
}

7. 链接高亮

use dioxus::prelude::*;

#[component]
fn NavLink(to: Route, label: String) -> Element {
    let current = use_route::<Route>();
    let is_active = current == to;

    rsx! {
        Link {
            class: "px-4 py-2 rounded-lg text-sm transition-colors {
                if is_active { \"bg-blue-500 text-white\" } else { \"text-gray-600\" }
            }",
            to,
            "{label}"
        }
    }
}

// 使用
NavLink { to: Route::Home {}, label: "首页" }
NavLink { to: Route::BlogList {}, label: "文章" }

8. 路由守卫

// 需要登录才能访问的路由
#[component]
fn ProtectedRoute(children: Element) -> Element {
    let user = use_context::<Signal<Option<User>>>();

    if user.read().is_none() {
        return rsx! {
            Redirect { to: Route::Login {} }
        };
    }

    rsx! { {children} }
}

// 在路由中使用
#[derive(Routable, Clone, PartialEq)]
enum Route {
    #[layout(ProtectedRoute)]
        #[route("/admin")]
        Admin {},
    #[end_layout]
}

9. 小结

  • #[derive(Routable)] 枚举定义所有路由
  • Router::<Route> {} 渲染根路由
  • Link 组件声明式导航,use_navigator 编程式导航
  • :param 动态路径参数,?query= 查询参数
  • #[layout]#[nest] 实现布局共享和嵌套路由
  • /:404 捕获未匹配路由
  • 下一章将学习样式与 Tailwind CSS 集成
dioxusrouternavigationroutablelink