Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

嵌套路由

Nested Routing

我们刚刚定义了下面这组路由:

We just defined the following set of routes:

<Routes fallback=|| "Not found.">
  <Route path=path!("/") view=Home/>
  <Route path=path!("/users") view=Users/>
  <Route path=path!("/users/:id") view=UserProfile/>
  <Route path=path!("/*any") view=|| view! { <h1>"Not Found"</h1> }/>
</Routes>

这里存在一定程度的重复:/users/users/:id。对于小型应用来说这没问题,但你可能已经预见到它的扩展性并不好。如果我们能嵌套这些路由,岂不是更好?

There’s a certain amount of duplication here: /users and /users/:id. This is fine for a small app, but you can probably already tell it won’t scale well. Wouldn’t it be nice if we could nest these routes?

事实上……你可以!

Well... you can!

<Routes fallback=|| "Not found.">
  <Route path=path!("/") view=Home/>
  <ParentRoute path=path!("/users") view=Users>
    <Route path=path!(":id") view=UserProfile/>
  </ParentRoute>
  <Route path=path!("/*any") view=|| view! { <h1>"Not Found"</h1> }/>
</Routes>

你可以将一个 <Route/> 嵌套在一个 <ParentRoute/> 内部。看起来很直观。

You can nest a <Route/> inside a <ParentRoute/>. Seems straightforward.

但等等。我们刚刚微妙地改变了应用程序的行为。

But wait. We’ve just subtly changed what our application does.

接下来的部分是本指南整个路由章节中最重要的部分之一。请仔细阅读,如果有任何不理解的地方,欢迎随时提问。

The next section is one of the most important in this entire routing section of the guide. Read it carefully, and feel free to ask questions if there’s anything you don’t understand.

嵌套路由即布局

Nested Routes as Layout

嵌套路由是一种布局形式,而不仅仅是定义路由的方法。

Nested routes are a form of layout, not a method of route definition.

换句话说:定义嵌套路由的主要目的并不是为了在输入路由定义的路径时避免重复。它的实际作用是告诉路由管理器在页面上同时并排显示多个 <Route/>

Let me put that another way: The goal of defining nested routes is not primarily to avoid repeating yourself when typing out the paths in your route definitions. It is actually to tell the router to display multiple <Route/>s on the page at the same time, side by side.

让我们回顾一下之前的实际例子。

Let’s look back at our practical example.

<Routes fallback=|| "Not found.">
  <Route path=path!("/users") view=Users/>
  <Route path=path!("/users/:id") view=UserProfile/>
</Routes>

这意味着:

This means:

  • 如果我访问 /users,我会得到 <Users/> 组件。

  • If I go to /users, I get the <Users/> component.

  • 如果我访问 /users/3,我会得到 <UserProfile/> 组件(参数 id 被设置为 3;稍后会详细介绍)。

  • If I go to /users/3, I get the <UserProfile/> component (with the parameter id set to 3; more on that later)

假设我改用嵌套路由:

Let’s say I use nested routes instead:

<Routes fallback=|| "Not found.">
  <ParentRoute path=path!("/users") view=Users>
    <Route path=path!(":id") view=UserProfile/>
  </ParentRoute>
</Routes>

这意味着:

This means:

  • 如果我访问 /users/3,路径会匹配两个 <Route/><Users/><UserProfile/>

  • If I go to /users/3, the path matches two <Route/>s: <Users/> and <UserProfile/>.

  • 如果我访问 /users,路径将无法匹配。

  • If I go to /users, the path is not matched.

我实际上需要添加一个回退路由:

I actually need to add a fallback route

<Routes>
  <ParentRoute path=path!("/users") view=Users>
    <Route path=path!(":id") view=UserProfile/>
    <Route path=path!("") view=NoUser/>
  </ParentRoute>
</Routes>

现在:

Now:

  • 如果我访问 /users/3,路径匹配 <Users/><UserProfile/>

  • If I go to /users/3, the path matches <Users/> and <UserProfile/>.

  • 如果我访问 /users,路径匹配 <Users/><NoUser/>

  • If I go to /users, the path matches <Users/> and <NoUser/>.

换句话说,当我使用嵌套路由时,每个 路径(path) 可以匹配多个 路由(routes):每个 URL 可以在同一页面的同一时间渲染由多个 <Route/> 组件提供的视图。

When I use nested routes, in other words, each path can match multiple routes: each URL can render the views provided by multiple <Route/> components, at the same time, on the same page.

这可能违反直觉,但它非常强大,原因你很快就会看到。

This may be counter-intuitive, but it’s very powerful, for reasons you’ll hopefully see in a few minutes.

为什么要使用嵌套路由?

Why Nested Routing?

为什么要费心使用它呢?

Why bother with this?

大多数 Web 应用程序都包含对应于布局不同部分的导航层级。例如,在一个邮件应用中,你可能有一个像 /contacts/greg 这样的 URL,它在屏幕左侧显示联系人列表,在屏幕右侧显示 Greg 的联系人详情。联系人列表和联系人详情应该始终同时出现在屏幕上。如果没有选择联系人,也许你想显示一些说明文字。

Most web applications contain levels of navigation that correspond to different parts of the layout. For example, in an email app you might have a URL like /contacts/greg, which shows a list of contacts on the left of the screen, and contact details for Greg on the right of the screen. The contact list and the contact details should always appear on the screen at the same time. If there’s no contact selected, maybe you want to show a little instructional text.

你可以使用嵌套路由轻松定义这一点:

You can easily define this with nested routes

<Routes fallback=|| "Not found.">
  <ParentRoute path=path!("/contacts") view=ContactList>
    <Route path=path!(":id") view=ContactInfo/>
    <Route path=path!("") view=|| view! {
      <p>"Select a contact to view more info."</p>
    }/>
  </ParentRoute>
</Routes>

你甚至可以嵌套得更深。假设你想为每个联系人的地址、电子邮件/电话以及你与他们的对话设置标签页(tabs)。你可以在 :id 内部添加 另一组 嵌套路由:

You can go even deeper. Say you want to have tabs for each contact’s address, email/phone, and your conversations with them. You can add another set of nested routes inside :id:

<Routes fallback=|| "Not found.">
  <ParentRoute path=path!("/contacts") view=ContactList>
    <ParentRoute path=path!(":id") view=ContactInfo>
      <Route path=path!("") view=EmailAndPhone/>
      <Route path=path!("address") view=Address/>
      <Route path=path!("messages") view=Messages/>
    </ParentRoute>
    <Route path=path!("") view=|| view! {
      <p>"Select a contact to view more info."</p>
    }/>
  </ParentRoute>
</Routes>

Remix 网站(由 React Router 的创建者开发的 React 框架)的主页在你向下滚动时有一个很好的视觉示例,展示了三层嵌套路由:Sales > Invoices > an invoice。

The main page of the Remix website, a React framework from the creators of React Router, has a great visual example if you scroll down, with three levels of nested routing: Sales > Invoices > an invoice.

<Outlet/>

父路由不会自动渲染它们的嵌套路由。毕竟,它们只是组件;它们不知道应该在哪里渲染它们的子路由,“直接把它塞到父组件的末尾”并不是一个好的答案。

Parent routes do not automatically render their nested routes. After all, they are just components; they don’t know exactly where they should render their children, and “just stick it at the end of the parent component” is not a great answer.

相反,你需要通过 <Outlet/> 组件告诉父组件在哪里渲染任何嵌套组件。<Outlet/> 简单地渲染以下两件事之一:

Instead, you tell a parent component where to render any nested components with an <Outlet/> component. The <Outlet/> simply renders one of two things:

  • 如果没有匹配到嵌套路由,它什么也不显示。

  • if there is no nested route that has been matched, it shows nothing

  • 如果匹配到了嵌套路由,它显示该路由的 view

  • if there is a nested route that has been matched, it shows its view

就这么多!但这非常重要,务必记住,因为它是“为什么这不起作用?”这类挫败感的常见来源。如果你不提供 <Outlet/>,嵌套路由就不会被显示。

That’s all! But it’s important to know and to remember, because it’s a common source of “Why isn’t this working?” frustration. If you don’t provide an <Outlet/>, the nested route won’t be displayed.

#[component]
pub fn ContactList() -> impl IntoView {
  let contacts = todo!();

  view! {
    <div style="display: flex">
      // 联系人列表
      // the contact list
      <For each=contacts
        key=|contact| contact.id
        children=|contact| todo!()
      />
      // 嵌套子路由(如果有),
      // 别忘了这个!
      // the nested child, if any
      // don’t forget this!
      <Outlet/>
    </div>
  }
}

重构路由定义

Refactoring Route Definitions

如果你愿意,不需要在一个地方定义所有的路由。你可以将任何 <Route/> 及其子路由重构为单独的组件。

You don’t need to define all your routes in one place if you don’t want to. You can refactor any <Route/> and its children out into a separate component.

例如,你可以将上面的示例重构为使用两个独立的组件:

For example, you can refactor the example above to use two separate components:

#[component]
pub fn App() -> impl IntoView {
    view! {
      <Router>
        <Routes fallback=|| "Not found.">
          <ParentRoute path=path!("/contacts") view=ContactList>
            <ContactInfoRoutes/>
            <Route path=path!("") view=|| view! {
              <p>"Select a contact to view more info."</p>
            }/>
          </ParentRoute>
        </Routes>
      </Router>
    }
}

#[component(transparent)]
fn ContactInfoRoutes() -> impl MatchNestedRoutes + Clone {
    view! {
      <ParentRoute path=path!(":id") view=ContactInfo>
        <Route path=path!("") view=EmailAndPhone/>
        <Route path=path!("address") view=Address/>
        <Route path=path!("messages") view=Messages/>
      </ParentRoute>
    }
    .into_inner()
    .into_any_nested_route()
}

第二个组件是 #[component(transparent)],这意味着它只返回数据,而不是视图;同样,它使用 .into_inner() 来移除 view 宏添加的一些调试信息,并仅返回由 <ParentRoute/> 创建的路由定义。

This second component is a #[component(transparent)], meaning it just returns its data, not a view; likewise, it uses .into_inner() to remove some debug info added by the view macro and just return the route definitions created by <ParentRoute/>.

嵌套路由与性能

Nested Routing and Performance

从概念上讲,所有这些都很棒,但再次强调——这有什么大不了的?

All of this is nice, conceptually, but again—what’s the big deal?

性能。

Performance.

在像 Leptos 这样细粒度响应式的库中,尽可能减少渲染工作量始终很重要。因为我们处理的是真实的 DOM 节点,而不是对虚拟 DOM 进行 diff,所以我们希望尽可能少地“重新渲染”组件。嵌套路由使这一点变得极其容易。

In a fine-grained reactive library like Leptos, it’s always important to do the least amount of rendering work you can. Because we’re working with real DOM nodes and not diffing a virtual DOM, we want to “rerender” components as infrequently as possible. Nested routing makes this extremely easy.

想象一下我的联系人列表例子。如果我从 Greg 导航到 Alice 再到 Bob,最后回到 Greg,联系人信息在每次导航时都需要更改。但是 <ContactList/> 永远不应该被重新渲染。这不仅节省了渲染性能,还保持了 UI 中的状态。例如,如果我在 <ContactList/> 的顶部有一个搜索栏,从 Greg 导航到 Alice 再到 Bob 不会清除搜索内容。

Imagine my contact list example. If I navigate from Greg to Alice to Bob and back to Greg, the contact information needs to change on each navigation. But the <ContactList/> should never be rerendered. Not only does this save on rendering performance, it also maintains state in the UI. For example, if I have a search bar at the top of <ContactList/>, navigating from Greg to Alice to Bob won’t clear the search.

事实上,在这种情况下,在联系人之间移动时,我们甚至不需要重新渲染 <Contact/> 组件。路由管理器只会在我们导航时响应式地更新 :id 参数,从而允许我们进行细粒度的更新。当我们在联系人之间导航时,我们将更新单个文本节点来更改联系人的姓名、地址等,而无需进行 任何 额外的重新渲染。

In fact, in this case, we don’t even need to rerender the <Contact/> component when moving between contacts. The router will just reactively update the :id parameter as we navigate, allowing us to make fine-grained updates. As we navigate between contacts, we’ll update single text nodes to change the contact’s name, address, and so on, without doing any additional rerendering.

这个沙盒包含本节和前一节讨论的一些特性(如嵌套路由),以及本章其余部分将涵盖的一些特性。路由是一个集成度很高的系统,因此提供一个完整的示例是有意义的,所以如果有什么你不理解的地方,不要感到惊讶。

This sandbox includes a couple features (like nested routing) discussed in this section and the previous one, and a couple we’ll cover in the rest of this chapter. The router is such an integrated system that it makes sense to provide a single example, so don’t be surprised if there’s anything you don’t understand.

:::admonish sandbox title="在线示例" collapsible=true

点击打开 CodeSandbox。

Click to open CodeSandbox.

:::

CodeSandbox Source
use leptos::prelude::*;
use leptos_router::components::{Outlet, ParentRoute, Route, Router, Routes, A};
use leptos_router::hooks::use_params_map;
use leptos_router::path;

#[component]
pub fn App() -> impl IntoView {
    view! {
        <Router>
            <h1>"Contact App"</h1>
            // 这个 <nav> 将显示在所有路由上,
            // 因为它在 <Routes/> 之外。
            // 注意:我们可以直接使用普通的 <a> 标签,
            // 路由管理器将使用客户端导航。
            // this <nav> will show on every routes,
            // because it's outside the <Routes/>
            // note: we can just use normal <a> tags
            // and the router will use client-side navigation
            <nav>
                <a href="/">"Home"</a>
                <a href="/contacts">"Contacts"</a>
            </nav>
            <main>
                <Routes fallback=|| "Not found.">
                    // / 只有一个未嵌套的 "Home"
                    // / just has an un-nested "Home"
                    <Route path=path!("/") view=|| view! {
                        <h3>"Home"</h3>
                    }/>
                    // /contacts 有嵌套路由
                    // /contacts has nested routes
                    <ParentRoute
                        path=path!("/contacts")
                        view=ContactList
                      >
                        // 如果指定了 id,匹配对应的嵌套路由
                        // if id specified, matches nested routes
                        <ParentRoute path=path!(":id") view=ContactInfo>
                            <Route path=path!("") view=|| view! {
                                <div class="tab">
                                    "(Contact Info)"
                                </div>
                            }/>
                            <Route path=path!("conversations") view=|| view! {
                                <div class="tab">
                                    "(Conversations)"
                                </div>
                            }/>
                        </ParentRoute>
                        // 如果没有指定 id,回退到此
                        // if no id specified, fall back
                        <Route path=path!("") view=|| view! {
                            <div class="select-user">
                                "Select a user to view contact info."
                            </div>
                        }/>
                    </ParentRoute>
                </Routes>
            </main>
        </Router>
    }
}

#[component]
fn ContactList() -> impl IntoView {
    view! {
        <div class="contact-list">
            // 这里是我们联系人列表组件本身
            // here's our contact list component itself
            <h3>"Contacts"</h3>
            <div class="contact-list-contacts">
                <A href="alice">"Alice"</A>
                <A href="bob">"Bob"</A>
                <A href="steve">"Steve"</A>
            </div>

            // <Outlet/> 将显示嵌套的子路由。
            // 我们可以将此 outlet 放置在布局中的任何位置。
            // <Outlet/> will show the nested child route
            // we can position this outlet wherever we want
            // within the layout
            <Outlet/>
        </div>
    }
}

#[component]
fn ContactInfo() -> impl IntoView {
    // 我们可以使用 `use_params_map` 响应式地访问 :id 参数
    // we can access the :id param reactively with `use_params_map`
    let params = use_params_map();
    let id = move || params.read().get("id").unwrap_or_default();

    // 假设我们在这里从 API 加载数据
    // imagine we're loading data from an API here
    let name = move || match id().as_str() {
        "alice" => "Alice",
        "bob" => "Bob",
        "steve" => "Steve",
        _ => "User not found.",
    };

    view! {
        <h4>{name}</h4>
        <div class="contact-info">
            <div class="tabs">
                <A href="" exact=true>"Contact Info"</A>
                <A href="conversations">"Conversations"</A>
            </div>

            // 这里的 <Outlet/> 是嵌套在 /contacts/:id 路由下的标签页
            // <Outlet/> here is the tabs that are nested
            // underneath the /contacts/:id route
            <Outlet/>
        </div>
    }
}

fn main() {
    leptos::mount::mount_to_body(App)
}