跳到内容
返回博客

2022 年 5 月 23 日,星期一

Layouts RFC

发布者

此 RFC(征求意见稿)概述了自 2016 年推出以来 Next.js 最大的更新

  • 嵌套布局: 使用嵌套路由构建复杂的应用程序。
  • 专为服务器组件设计: 针对子树导航进行了优化。
  • 改进的数据获取: 在布局中获取数据,同时避免瀑布流。
  • 使用 React 18 功能: 流式处理、过渡和 Suspense。
  • 客户端和服务器端路由: 以服务器为中心的路由,具有 类似 SPA 的行为。
  • 100% 可逐步采用: 无重大更改,因此您可以逐步采用。
  • 高级路由模式: 并行路由、拦截路由等。

新的 Next.js 路由器将构建在 最近发布的 React 18 功能之上。我们计划引入默认设置和约定,使您能够轻松采用这些新功能,并利用它们解锁的优势。

此 RFC 的工作正在进行中,我们将在新功能可用时发布公告。要提供反馈,请加入 Github 讨论 中的对话。

目录

动机

我们一直在从 GitHub、Discord、Reddit 和我们的开发者调查中收集社区反馈,了解 Next.js 中当前路由的局限性。我们发现:

  • 创建布局的开发者体验可以改进。应该可以轻松创建可以嵌套、跨路由共享并在导航时保持其状态的布局。
  • 许多 Next.js 应用程序都是仪表板或控制台,这将受益于更高级的路由解决方案。

虽然当前的路由系统自 Next.js 诞生以来一直运行良好,但我们希望让开发者更容易构建性能更高、功能更丰富的 Web 应用程序。

作为框架维护者,我们也希望构建一个向后兼容并与 React 未来发展方向一致的路由系统。

注意: 一些路由约定受到了 Meta 中基于 Relay 的路由器的启发,服务器组件的某些功能最初是在那里开发的,以及客户端路由器,如 React Router 和 Ember.js。layout.js 文件约定受到了 SvelteKit 中完成的工作的启发。我们还要感谢 Cassidy 开启了 关于布局的早期 RFC

术语

此 RFC 引入了新的路由约定和语法。术语基于 React 和标准 Web 平台术语。在整个 RFC 中,您将看到这些术语链接回下面的定义。

  • 树: 一种可视化分层结构的约定。例如,具有父组件和子组件的组件树、文件夹结构等。
  • 子树 树的一部分,从根(第一个)开始,到叶(最后一个)结束。
  • URL 路径: URL 中域名之后的部分。
  • URL 段: URL 路径中由斜杠分隔的部分。

当前路由的工作方式

目前,Next.js 使用文件系统将 Pages 目录中的各个文件夹和文件映射到可通过 URL 访问的路由。每个页面文件导出一个 React 组件,并具有基于其文件名的关联路由。例如:

引入 app 目录

为了确保这些新改进可以逐步采用并避免重大更改,我们建议使用一个名为 app 的新目录。

app 目录将与 pages 目录协同工作。您可以逐步将应用程序的部分内容迁移到新的 app 目录,以利用新功能。为了向后兼容,pages 目录的行为将保持不变,并继续受到支持。

定义路由

您可以使用 app 目录内的文件夹层级结构来定义路由。**路由**是嵌套文件夹的单个路径,遵循从**根文件夹**到最终**叶子文件夹**的层级结构。

例如,您可以通过在 app 目录中嵌套两个新文件夹来添加一个新的 /dashboard/settings 路由。

注意

  • 使用此系统,您将使用文件夹来定义路由,并使用文件来定义 UI(使用新的文件约定,例如 layout.jspage.js,以及 RFC 的第二部分中的 loading.js)。
  • 这允许您将自己的项目文件(UI 组件、测试文件、stories 等)与 app 目录放在一起。目前,这仅在使用 pageExtensions 配置时才有可能。

路由段

子树中的每个文件夹代表一个**路由段**。每个路由段都映射到 **URL 路径**中的相应**段**。

例如,/dashboard/settings 路由由 3 个段组成

  • / 根段
  • dashboard
  • settings

注意:选择名称**路由段**是为了与围绕 URL 路径的现有术语保持一致。

布局

新的文件约定: layout.js

到目前为止,我们已经使用文件夹来定义应用程序的路由。但是空文件夹本身不会做任何事情。让我们讨论如何使用新的文件约定来定义将为这些路由渲染的 UI。

**布局**是在 子树中的路由段之间共享的 UI。当用户在兄弟段之间导航时,布局不会影响 URL 路径,也不会重新渲染(React 状态会被保留)。

可以通过从 layout.js 文件默认导出一个 React 组件来定义布局。该组件应接受一个 children prop,它将被填充布局正在包裹的段。

有 2 种类型的布局

  • **根布局:** 应用于所有路由
  • **常规布局:** 应用于特定路由

您可以将两个或多个布局嵌套在一起以形成**嵌套布局**。

根布局

您可以通过在 app 文件夹内添加一个 layout.js 文件来创建一个根布局,该布局将应用于应用程序的所有路由。

注意

  • 根布局取代了对 自定义 App (_app.js)自定义 Document (_document.js) 的需求,因为它应用于所有路由。
  • 您将能够使用根布局来自定义初始文档 shell(例如,<html><body> 标签)。
  • 您将能够在根布局(和其他布局)中获取数据。

常规布局

您还可以通过在特定文件夹内添加一个 layout.js 文件来创建一个仅应用于应用程序一部分的布局。

例如,您可以在 dashboard 文件夹内创建一个 layout.js 文件,该文件将仅应用于 dashboard 内的路由段。

嵌套布局

布局默认是**嵌套**的。

例如,如果我们结合上面的两个布局。根布局 (app/layout.js) 将应用于 dashboard 布局,而 dashboard 布局也将应用于 dashboard/* 内的所有路由段。

页面

新的文件约定: page.js

页面是路由段独有的 UI。您可以通过在文件夹内添加一个 page.js 文件来创建一个页面。

例如,要为 /dashboard/* 路由创建页面,您可以在每个文件夹内添加一个 page.js 文件。当用户访问 /dashboard/settings 时,Next.js 将渲染 settings 文件夹的 page.js 文件,并将其包裹在 子树中更上层的任何布局中。

您可以直接在 dashboard 文件夹内创建一个 page.js 文件来匹配 /dashboard 路由。dashboard 布局也将应用于此页面。

此路由由 2 个段组成

  • / 根段
  • dashboard

注意

  • 为了使路由有效,它需要在其叶子段中有一个页面。如果它没有,路由将抛出一个错误。

布局和页面行为

  • 文件扩展名 js|jsx|ts|tsx 可用于页面和布局。
  • 页面组件是 page.js 的默认导出。
  • 布局组件是 layout.js 的默认导出。
  • 布局组件**必须**接受一个 children prop。

当渲染布局组件时,children prop 将被填充一个子布局(如果它存在于 子树的更下层)或一个页面。

可以更容易地将其可视化为一个布局 ,其中父布局将选择最近的子布局,直到它到达一个页面。

示例

app/layout.js
// Root layout
// - Applies to all routes
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Header />
        {children}
        <Footer />
      </body>
    </html>
  );
}
app/dashboard/layout.js
// Regular layout
// - Applies to route segments in app/dashboard/*
export default function DashboardLayout({ children }) {
  return (
    <>
      <DashboardSidebar />
      {children}
    </>
  );
}
app/dashboard/analytics/page.js
// Page Component
// - The UI for the `app/dashboard/analytics` segment
// - Matches the `acme.com/dashboard/analytics` URL path
export default function AnalyticsPage() {
  return <main>...</main>;
}

上述布局和页面的组合将渲染以下组件层级结构

组件层级结构
<RootLayout>
  <Header />
  <DashboardLayout>
    <DashboardSidebar />
    <AnalyticsPage>
      <main>...</main>
    </AnalyticsPage>
  </DashboardLayout>
  <Footer />
</RootLayout>

React 服务端组件

注意: React 引入了新的组件类型:服务端、客户端(传统的 React 组件)和共享组件。要了解有关这些新类型的更多信息,我们建议阅读 React 服务端组件 RFC

通过此 RFC,您可以开始使用 React 功能,并将 React 服务端组件逐步引入到您的 Next.js 应用程序中。

新路由系统的内部机制还将利用最近发布的 React 功能,例如 Streaming、Suspense 和 Transitions。这些是 React 服务端组件的构建块。

默认使用服务端组件

pagesapp 目录之间最大的变化之一是,默认情况下,**app 目录内的文件将在服务器上渲染为 React 服务端组件。**

这将使您在从 pages 迁移到 app 时能够自动采用 React 服务端组件。

注意: 服务端组件可以在 app 文件夹或您自己的文件夹中使用,但为了向后兼容,不能在 pages 目录中使用。

客户端和服务端组件约定

app 文件夹将支持服务端、客户端和共享组件,并且您将能够 在树中交错使用这些组件

关于客户端组件和服务端组件的具体定义约定,目前正在进行 讨论。我们将关注此讨论的最终结果。

  • 目前,服务端组件可以通过在文件名后附加 .server.js 来定义。例如:layout.server.js
  • 客户端组件可以通过在文件名后附加 .client.js 来定义。例如:page.client.js
  • .js 文件被视为共享组件。由于它们可以在服务端和客户端渲染,因此它们需要遵守每个上下文的约束。

注意

  • 客户端和服务端组件具有需要遵守的 约束。在决定使用客户端组件还是服务端组件时,我们建议使用服务端组件(默认),直到您需要使用客户端组件为止。

Hooks

我们将添加客户端和服务端组件的 hooks,这将允许您访问 headers 对象、cookies、pathnames、search params 等。未来,我们将提供包含更多信息的文档。

渲染环境

使用客户端和服务端组件约定,您将可以精细地控制哪些组件将包含在客户端 JavaScript bundle 中。

默认情况下,app 中的路由将使用静态生成,当路由段使用需要请求上下文的 服务端 hooks 时,将切换到动态渲染。

在路由中交错使用客户端和服务端组件

在 React 中,在客户端组件内部导入服务端组件存在限制,因为服务端组件可能具有仅限服务端的代码(例如,数据库或文件系统实用程序)。

例如,导入服务端组件将不起作用

ClientComponent.js
import ServerComponent from './ServerComponent.js';
 
export default function ClientComponent() {
  return (
    <>
      <ServerComponent />
    </>
  );
}

但是,服务端组件可以作为客户端组件的子组件传递。您可以通过将它们**包裹**在另一个服务端组件中来实现这一点。例如

ClientComponent.js
export default function ClientComponent({ children }) {
  return (
    <>
      <h1>Client Component</h1>
      {children}
    </>
  );
}
 
// ServerComponent.js
export default function ServerComponent() {
  return (
    <>
      <h1>Server Component</h1>
    </>
  );
}
 
// page.js
// It's possible to import Client and Server components inside Server Components
// because this component is rendered on the server
import ClientComponent from "./ClientComponent.js";
import ServerComponent from "./ServerComponent.js";
 
export default function ServerComponentPage() {
  return (
    <>
      <ClientComponent>
        <ServerComponent />
      </ClientComponent>
    </>
  );
}

通过这种模式,React 将知道它需要在服务器上渲染 ServerComponent,然后再将结果(不包含任何仅限服务端的代码)发送到客户端。从客户端组件的角度来看,它的子组件已经被渲染。

在布局中,此模式通过 children prop 应用,因此您无需创建额外的包装组件。

例如,ClientLayout 组件将接受 ServerPage 组件作为其子组件

app/dashboard/layout.js
// The Dashboard Layout is a Client Component
export default function ClientLayout({ children }) {
  // Can use useState / useEffect here
  return (
    <>
      <h1>Layout</h1>
      {children}
    </>
  );
}
 
// The Page is a Server Component that will be passed to Dashboard Layout
// app/dashboard/settings/page.js
export default function ServerPage() {
  return (
    <>
      <h1>Page</h1>
    </>
  );
}

注意: 这种组合风格是在客户端组件内部渲染服务器组件的重要模式。它为学习一种模式设定了优先级,也是我们决定使用 children prop 的原因之一。

数据获取

在路由中的多个段中获取数据是可能的。这与 pages 目录不同,在 pages 目录中,数据获取仅限于页面级别。

布局中的数据获取

您可以在 layout.js 文件中使用 Next.js 数据获取方法 getStaticPropsgetServerSideProps 来获取数据。

例如,博客布局可以使用 getStaticProps 从 CMS 获取类别,这些类别可用于填充侧边栏组件

app/blog/layout.js
export async function getStaticProps() {
  const categories = await getCategoriesFromCMS();
 
  return {
    props: { categories },
  };
}
 
export default function BlogLayout({ categories, children }) {
  return (
    <>
      <BlogSidebar categories={categories} />
      {children}
    </>
  );
}

路由中的多个数据获取方法

您还可以在路由的多个段中获取数据。例如,获取数据的 layout 也可以包裹一个获取自身数据的 page

使用上面的博客示例,单个帖子页面可以使用 getStaticPropsgetStaticPaths 从 CMS 获取帖子数据

app/blog/[slug]/page.js
export async function getStaticPaths() {
  const posts = await getPostSlugsFromCMS();
 
  return {
    paths: posts.map((post) => ({
      params: { slug: post.slug },
    })),
  };
}
 
export async function getStaticProps({ params }) {
  const post = await getPostFromCMS(params.slug);
 
  return {
    props: { post },
  };
}
 
export default function BlogPostPage({ post }) {
  return <Post post={post} />;
}

由于 app/blog/layout.jsapp/blog/[slug]/page.js 都使用 getStaticProps,Next.js 将在构建时静态生成整个 /blog/[slug] 路由作为 React 服务器组件 - 从而减少客户端 JavaScript 并加快 hydration。

静态生成的路由进一步改善了这一点,因为客户端导航重用缓存(服务器组件数据)并且不重新计算工作,从而减少了 CPU 时间,因为您正在渲染服务器组件的快照。

行为和优先级

Next.js 数据获取方法(getServerSidePropsgetStaticProps)只能在 app 文件夹中的服务器组件中使用。单个路由中跨段的不同数据获取方法会相互影响。

在一个段中使用 getServerSideProps 将会影响其他段中的 getStaticProps。由于请求已经必须为了 getServerSideProps 段而访问服务器,服务器也将渲染任何 getStaticProps 段。它将重用构建时获取的 props,因此数据仍然是静态的,渲染发生在每个请求的按需渲染,并使用在 next build 期间生成的 props。

在一个段中使用带有 revalidate (ISR)getStaticProps 将会影响其他段中带有 revalidategetStaticProps。如果一个路由中有两个 revalidate 周期,则较短的重新验证周期将优先。

注意: 未来,这可能会被优化,以允许在路由中实现完整的数据获取粒度。

使用 React 服务器组件进行数据获取

服务器端路由、React 服务器组件、Suspense 和 Streaming 的组合对 Next.js 中的数据获取和渲染产生了一些影响

并行数据获取

Next.js 将积极主动地并行启动数据获取,以最大限度地减少瀑布流。例如,如果数据获取是顺序的,则路由中的每个嵌套段都必须在前一个段完成之后才能开始获取数据。但是,通过并行获取,每个段都可以同时积极主动地开始数据获取。

由于渲染可能依赖于 Context,因此每个段的渲染将在其数据被获取并且其父级完成渲染后开始。

未来,借助 Suspense,渲染也可以立即开始 - 即使数据尚未完全加载。如果在数据可用之前读取数据,则会触发 Suspense。React 将乐观地开始渲染服务器组件,在请求完成之前,并在请求解决时插入结果。

部分获取和渲染

当在同级路由段之间导航时,Next.js 将仅从该段向下获取和渲染。它不需要重新获取或重新渲染任何上层内容。这意味着在共享布局的页面中,当用户在同级页面之间导航时,布局将被保留,并且 Next.js 将仅从该段向下获取和渲染。

这对于 React 服务器组件尤其有用,因为否则每次导航都会导致整个页面在服务器上重新渲染,而不是仅在服务器上渲染页面的更改部分。这减少了数据传输量和执行时间,从而提高了性能。

例如,如果用户在 /analytics/settings 页面之间导航,React 将重新渲染页面段,但保留布局

注意: 将有可能强制重新获取 结构中更高层的数据。我们仍在讨论如何实现此功能的细节,并将更新 RFC。

路由组

app 文件夹的层次结构直接映射到 URL 路径。但是,可以通过创建路由组来打破此模式。路由组可用于

  • 组织路由,而不影响 URL 结构。
  • 将路由段选择退出布局。
  • 通过拆分应用程序来创建多个根布局。

约定

可以通过将文件夹名称括在括号中来创建路由组:(folderName)

注意: 路由组的命名仅用于组织目的,因为它们不影响 URL 路径。

示例:将路由选择退出布局

要将路由选择退出布局,请创建一个新的路由组(例如 (shop)),并将共享相同布局的路由移动到该组中(例如 accountcart)。组外的路由将不共享布局(例如 checkout)。

之前

之后

示例:组织路由,而不影响 URL 路径

同样,要组织路由,请创建一个组以将相关的路由放在一起。括号中的文件夹将从 URL 中省略(例如 (marketing)(shop))。

示例:创建多个根布局

要创建多个根布局,请在 app 目录的顶层创建两个或多个路由组。这对于将应用程序划分为具有完全不同的 UI 或体验的部分非常有用。可以分别自定义每个根布局的 <html><body><head> 标签。

以服务器为中心的路由

目前,Next.js 使用客户端路由。在初始加载和后续导航时,会向服务器发出请求以获取新页面的资源。这包括每个组件的 JavaScript(包括仅在特定条件下显示的组件)及其 props(来自 getServerSidePropsgetStaticProps 的 JSON 数据)。一旦从服务器加载了 JavaScript 和数据,React 就会在客户端渲染组件。

在这个新模型中,Next.js 将使用以服务器为中心的路由,同时保持客户端过渡。这与在服务器上评估的 服务器组件 相一致。

在导航时,会获取数据,并且 React 会在服务器端渲染组件。来自服务器的输出是用于客户端 React 更新 DOM 的特殊指令(不是 HTML 或 JSON)。这些指令包含渲染的服务器组件的结果,这意味着无需在浏览器中加载该组件的 JavaScript 即可渲染结果。

这与当前默认的客户端组件形成对比,客户端组件需要将组件 JavaScript 发送到浏览器以在客户端渲染。

使用 React 服务器组件的以服务器为中心的路由的一些优点包括

  • 路由使用与服务器组件相同的请求(不进行额外的服务器请求)。
  • 服务器端完成的工作较少,因为在路由之间导航仅获取和渲染更改的段。
  • 当客户端导航且未使用新的客户端组件时,不会在浏览器中加载额外的 JavaScript。
  • 路由器利用新的流协议,以便可以在所有数据加载之前开始渲染。

当用户在应用程序中导航时,路由器会将 React 服务器组件有效负载的结果存储在内存中的客户端缓存中。缓存按路由段拆分,这允许在任何级别进行失效,并确保跨并发渲染的一致性。这意味着在某些情况下,可以重用先前获取的段的缓存。

注意

  • 静态生成和服务器端缓存可用于优化数据获取。
  • 以上信息描述了后续导航的行为。初始加载是一个不同的过程,涉及服务器端渲染以生成 HTML。
  • 虽然客户端路由在 Next.js 中运行良好,但当潜在路由数量很大时,它的扩展性较差,因为客户端必须下载路由图
  • 总的来说,通过使用 React 服务器组件,客户端导航速度更快,因为我们在浏览器中加载和渲染的组件更少。

即时加载状态

使用服务器端路由,导航发生在数据获取和渲染之后,因此在数据正在获取时显示加载 UI 非常重要,否则应用程序会显得无响应。

新的路由器将使用 Suspense 以实现即时加载状态和默认骨架屏。这意味着在加载新段的内容时,可以立即显示加载 UI。一旦服务器端渲染完成,新内容就会被替换进去。

在渲染发生时

  • 到新路由的导航将是即时的。
  • 共享布局将在新的路由段加载时保持交互性。
  • 导航将是可中断的 - 这意味着用户可以在一个路由的内容正在加载时在路由之间导航。

默认加载骨架屏

Suspense boundaries 将会在幕后自动处理,通过一种名为 loading.js 的新文件约定。

示例

您将可以通过在文件夹内添加 loading.js 文件来创建一个默认的加载骨架屏。

loading.js 应该导出一个 React 组件

loading.js
export default function Loading() {
  return <YourSkeleton />
}
 
// layout.js
export default function Layout({children}) {
  return (
    <>
      <Sidebar />
      {children}
    </>
  )
}
 
// Output
<>
  <Sidebar />
  <Suspense fallback={<Loading />}>{children}</Suspense>
</>

这将导致文件夹中的所有段(segments)都被包裹在一个 suspense boundary 中。默认的骨架屏将在布局首次加载以及在兄弟页面之间导航时使用。

错误处理

错误边界 是 React 组件,可以捕获其子组件树中任何地方的 JavaScript 错误。

约定

您将可以通过添加一个 error.js 文件并默认导出一个 React 组件来创建一个错误边界,该错误边界将捕获子树内的错误。

如果在该子树中抛出错误,该组件将显示为回退内容。此组件可用于记录错误、显示有关错误的有用信息以及尝试从错误中恢复的功能。

由于段和布局的嵌套特性,创建错误边界允许您将错误隔离到 UI 的这些部分。在发生错误期间,边界上方的布局将保持交互性,并且其状态将被保留。

error.js
export default function Error({ error, reset }) {
  return (
    <>
      An error occurred: {error.message}
      <button onClick={() => reset()}>Try again</button>
    </>
  );
}
 
// layout.js
export default function Layout({children}) {
  return (
    <>
      <Sidebar />
      {children}
    </>
  )
}
 
// Output
<>
  <Sidebar />
  <ErrorBoundary fallback={<Error />}>{children}</ErrorBoundary>
</>

注意

  • error.js 位于同一段中的 layout.js 文件内部的错误将不会被捕获,因为自动错误边界包裹的是布局的子元素,而不是布局本身。

模板

模板与布局类似,因为它们包裹着每个子布局或页面。

与跨路由持久存在并保持状态的布局不同,模板为其每个子元素创建一个新实例。这意味着,当用户在共享模板的路由段之间导航时,组件的新实例将被挂载。

注意: 除非您有使用模板的特定理由,否则我们建议使用布局。

约定

可以通过从 template.js 文件导出一个默认的 React 组件来定义模板。该组件应接受一个 children prop,该 prop 将填充嵌套的段。

示例

template.js
export default function Template({ children }) {
  return <Container>{children}</Container>;
}

具有布局和模板的路由段的渲染输出将如下所示

<Layout>
  {/* Note that the template is given a unique key. */}
  <Template key={routeParam}>{children}</Template>
</Layout>

行为

在某些情况下,您可能需要挂载和卸载共享 UI,而模板将是更合适的选择。例如

  • 使用 CSS 或动画库的进入/退出动画
  • 依赖于 useEffect(例如,记录页面浏览量)和 useState(例如,每页反馈表单)的功能
  • 更改默认框架行为。例如,布局内的 suspense boundaries 仅在首次加载布局时显示回退内容,而不是在切换页面时。对于模板,每次导航都会显示回退内容。

例如,考虑嵌套布局的设计,其中包含一个带边框的容器,该容器应包裹每个子页面。

您可以将容器放在父布局(shop/layout.js)中

shop/layout.js
export default function Layout({ children }) {
  return <div className="container">{children}</div>;
}
 
// shop/page.js
export default function Page() {
  return <div>...</div>;
}
 
// shop/categories/layout.js
export default function CategoryLayout({ children }) {
  return <div>{children}</div>;
}

但是,当切换页面时,任何进入/退出动画都不会播放,因为共享的父布局不会重新渲染。

您可以将容器放在每个嵌套布局或页面中

shop/layout.js
export default function Layout({ children }) {
  return <div>{children}</div>;
}
 
// shop/page.js
export default function Page() {
  return <div className="container">...</div>;
}
 
// shop/categories/layout.js
export default function CategoryLayout({ children }) {
  return <div className="container">{children}</div>;
}

但是,这样您就必须手动将其放置在每个嵌套布局或页面中,这在更复杂的应用程序中可能很繁琐且容易出错。

通过此约定,您可以在跨路由共享模板,这些模板在导航时创建新实例。这意味着 DOM 元素将被重新创建,状态将不会被保留,并且 effects 将被重新同步。

高级路由模式

我们计划引入约定来涵盖边缘情况,并允许您实现更高级的路由模式。以下是我们一直在积极思考的一些示例

拦截路由

有时,从其他路由内部拦截路由段可能很有用。在导航时,URL 将像往常一样更新,但被拦截的段将显示在当前路由的布局中。

示例

之前: 点击图片会跳转到一个新路由,该路由有自己的布局。

之后: 通过拦截路由,现在点击图片会在当前路由的布局内加载该段。例如,作为模态框。

要从 /[username] 段中拦截 /photo/[id] 路由,请在 /[username] 文件夹内创建一个重复的 /photo/[id] 文件夹,并使用 (..) 约定作为前缀。

约定

  • (..) - 将匹配高一级别的路由段(父目录的兄弟目录)。类似于相对路径中的 ../
  • (..)(..) - 将匹配高两级的路由段。类似于相对路径中的 ../../
  • (...) - 将匹配根目录中的路由段。

注意: 刷新或共享页面将加载具有其默认布局的路由。

动态并行路由

有时,在同一视图中显示两个或多个叶子段 (page.js) 可能很有用,这些叶子段可以独立导航。

以同一仪表板中的两个或多个选项卡组为例。导航一个选项卡组不应影响另一个选项卡组。当向后和向前导航时,选项卡的组合也应正确恢复。

约定

默认情况下,布局接受一个名为 children 的 prop,它将包含嵌套的布局或页面。您可以通过创建一个命名的“插槽”(包含 @ 前缀的文件夹)并在其中嵌套段来重命名该 prop。

更改后,布局将收到名为 customProp 的 prop,而不是 children

analytics/layout.js
export default function Layout({ customProp }) {
  return <>{customProp}</>;
}

您可以通过在同一级别添加多个命名插槽来创建并行路由。在下面的示例中,@views@audience 都作为 props 传递给分析布局。

您可以使用命名插槽同时显示叶子段。

analytics/layout.js
export default function Layout({ views, audience }) {
  return (
    <>
      <div>
        <ViewsNav />
        {views}
      </div>
      <div>
        <AudienceNav />
        {audience}
      </div>
    </>
  );
}

当用户首次导航到 /analytics 时,将显示每个文件夹(@views@audience)中的 page.js 段。

导航到 /analytics/subscribers 时,仅更新 @audience。同样,导航到 /analytics/impressions 时,仅更新 @views

向后和向前导航将恢复并行路由的正确组合。

组合拦截路由和并行路由

您可以组合拦截路由和并行路由,以在您的应用程序中实现特定的路由行为。

示例

例如,在创建模态框时,您通常需要注意一些常见的挑战,例如

  • 模态框无法通过 URL 访问。
  • 刷新页面时模态框关闭。
  • 后退导航跳转到上一个路由,而不是模态框后面的路由。
  • 前进导航不重新打开模态框。

您可能希望模态框在打开时更新 URL,并且后退/前进导航可以打开和关闭模态框。此外,在共享 URL 时,您可能希望页面加载时模态框打开并显示其后的上下文,或者您可能希望页面加载时不显示模态框的内容。

社交媒体网站上的照片就是一个很好的例子。通常,照片可以从用户的 feed 或个人资料中的模态框中访问。但是,当共享照片时,它们会直接在自己的页面上显示。

通过使用约定,我们可以使模态框行为默认映射到路由行为。

考虑以下文件夹结构

使用此模式

  • /photo/[id] 的内容可以通过其自身上下文中的 URL 访问。它也可以从 /[username] 路由内的模态框中访问。
  • 使用客户端导航向后和向前导航应关闭并重新打开模态框。
  • 刷新页面(服务器端导航)应将用户带到原始的 /photo/id 路由,而不是显示模态框。

/@modal/(..)photo/[id]/page.js 中,您可以返回包裹在模态框组件中的页面内容。

/@modal/(..)photo/[id]/page.js
export default function PhotoPage() {
  const router = useRouter();
 
  return (
    <Modal
      // the modal should always be shown on page load
      isOpen={true}
      // closing the modal should take user back to the previous page
      onClose={() => router.back()}
    >
      {/* Page Content */}
    </Modal>
  );
}

注意: 此解决方案不是在 Next.js 中创建模态框的唯一方法,但旨在展示如何组合约定来实现更复杂的路由行为。

条件路由

有时,您可能需要动态信息(如数据或上下文)来确定要显示的路由。您可以使用并行路由来有条件地加载一个路由或另一个路由。

示例

layout.js
export async function getServerSideProps({ params }) {
  const { accountType } = await fetchAccount(params.slug);
  return { props: { isUser: accountType === 'user' } };
}
 
export default function UserOrTeamLayout({ isUser, user, team }) {
  return <>{isUser ? user : team}</>;
}

在上面的示例中,您可以根据 slug 返回 userteam 路由。这允许您有条件地加载数据,并将子路由与一个或另一个选项进行匹配。

结论

我们对 Next.js 中布局、路由和 React 18 的未来感到兴奋。实现工作已经开始,我们将在功能可用后立即宣布。

请留下评论并加入 GitHub Discussions 上的对话