跳至内容
返回博客

2022年5月23日,星期一

布局 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 使用文件系统将 页面 目录中的各个文件夹和文件映射到可通过 URL 访问的路由。每个页面文件都导出一个 React 组件,并根据其文件名具有关联的路由。例如

引入 app 目录

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

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

定义路由

您可以使用 app 内部的文件夹层次结构来定义路由。路由是从根文件夹到最终叶子文件夹的嵌套文件夹的单个路径。

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

注意

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

路由段

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

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

  • / 根段
  • dashboard
  • settings

注意:选择名称路由段是为了与围绕URL 路径的现有术语相匹配。

布局

新的文件约定:layout.js

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

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

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

有两种类型的布局

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

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

根布局

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

注意

常规布局

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

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

嵌套布局

布局默认情况下是嵌套的

例如,如果我们要组合以上两个布局。根布局(app/layout.js)将应用于 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 属性。

当渲染布局组件时,children 属性将填充子布局(如果它存在于 子树 中)或页面。

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

示例

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 功能,例如流式传输、Suspense 和过渡。这些是 React 服务器端组件的基础构建块。

服务器端组件作为默认值

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

这将允许您在从 pages 迁移到 app 时自动采用 React 服务器端组件。

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

客户端和服务器端组件约定

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

关于如何定义客户端组件和服务器端组件的约定,目前正在进行讨论。我们将遵循讨论的结果。

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

注意

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

钩子

我们将添加客户端和服务器端组件钩子,允许您访问标头对象、Cookie、路径名、搜索参数等。将来,我们将提供包含更多信息的文档。

渲染环境

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

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

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

在 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 属性一起应用,因此您无需创建额外的包装器组件。

例如,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 属性的原因之一。

数据获取

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

在布局中获取数据

您可以通过使用 Next.js 数据获取方法 getStaticPropsgetServerSidePropslayout.js 文件中获取数据。

例如,博客布局可以使用 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 并加快水合速度。

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

行为和优先级

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

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

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

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

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

服务器端路由、React 服务器端组件、Suspense 和流式传输的组合对 Next.js 中的数据获取和渲染有一些影响。

并行数据获取

Next.js 将积极地并行启动数据获取以最大程度地减少瀑布效应。例如,如果数据获取是顺序的,则路由中的每个嵌套段都无法在先前段完成之前开始获取数据。但是,使用并行获取,每个段可以同时积极地开始数据获取。

由于渲染可能取决于上下文,因此每个段的渲染将在其数据获取完成且其父元素完成渲染后开始。

未来,借助 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 边界将通过一个名为 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>
</>

这将导致文件夹中的所有段都包装在 Suspense 边界中。默认骨架将在第一次加载布局以及在兄弟页面之间导航时使用。

错误处理

错误边界 是 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(例如每个页面的反馈表单)的功能
  • 更改默认框架行为。例如,仅在第一次加载布局时,布局内的悬念边界才会显示回退,而切换页面时不会显示。对于模板,在每次导航时都会显示回退。

例如,考虑一个嵌套布局的设计,该布局具有一个应围绕每个子页面包装的带边框的容器。

您可以将容器放在父布局中(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 元素将被重新创建,状态将不会保留,并且效果将被重新同步。

高级路由模式

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

拦截路由

有时,从其他路由中拦截路由段可能很有用。在导航时,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 都作为 prop 传递给分析布局。

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

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 时,您可能希望页面在打开模态及其后面的上下文时加载,或者您可能希望页面在没有模态的情况下加载内容。

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

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

考虑以下文件夹结构

使用此模式

  • /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 讨论