Menu

布局和模板

特殊文件 layout.jstemplate.js 允许你创建在多个 路由 之间共享的 UI。本页将指导你如何以及何时使用这些特殊文件。

布局

布局是在多个路由之间共享的 UI。在导航时,布局会保持状态,保持交互性,并且不会重新渲染。布局也可以 嵌套

你可以通过从 layout.js 文件中默认导出一个 React 组件来定义布局。该组件应接受一个 children 属性,在渲染过程中,这个属性将被填充为子布局(如果存在)或页面。

例如,该布局将与 /dashboard/dashboard/settings 页面共享:

layout.js 特殊文件
app/dashboard/layout.tsx
TypeScript
export default function DashboardLayout({
  children, // 将是一个页面或嵌套布局
}: {
  children: React.ReactNode
}) {
  return (
    <section>
      {/* 在此包含共享的 UI,例如页眉或侧边栏 */}
      <nav></nav>
 
      {children}
    </section>
  )
}

根布局(必需)

根布局定义在 app 目录的顶层,适用于所有路由。这个布局是必需的,必须包含 htmlbody 标签,允许你修改从服务器返回的初始 HTML。

app/layout.tsx
TypeScript
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {/* 布局 UI */}
        <main>{children}</main>
      </body>
    </html>
  )
}

嵌套布局

默认情况下,文件夹层次结构中的布局是嵌套的,这意味着它们通过 children 属性包裹子布局。你可以通过在特定路由段(文件夹)内添加 layout.js 来嵌套布局。

例如,要为 /dashboard 路由创建一个布局,在 dashboard 文件夹中添加一个新的 layout.js 文件:

嵌套布局
app/dashboard/layout.tsx
TypeScript
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return <section>{children}</section>
}

如果你要组合上面的两个布局,根布局(app/layout.js)将包裹仪表板布局(app/dashboard/layout.js),后者将包裹 app/dashboard/* 内的路由段。

这两个布局将如下嵌套:

嵌套布局

值得注意的是

  • 布局可以使用 .js.jsx.tsx 文件扩展名。
  • 只有根布局可以包含 <html><body> 标签。
  • 当在同一文件夹中定义了 layout.jspage.js 文件时,布局将包裹该页面。
  • 布局默认是 服务器组件,但可以设置为 客户端组件
  • 布局可以获取数据。查看 数据获取 部分了解更多信息。
  • 在父布局和其子组件之间传递数据是不可能的。但是,你可以在一个路由中多次获取相同的数据,React 将 自动删除重复请求,而不会影响性能。
  • 布局无法访问 pathname ( 了解更多 )。但是,导入的客户端组件可以使用 usePathname hook 访问 pathname。
  • 布局无法访问其下方的路由段。要访问所有路由段,你可以在客户端组件中使用 useSelectedLayoutSegmentuseSelectedLayoutSegments
  • 你可以使用 路由组 来选择特定路由段加入或退出共享布局。
  • 你可以使用 路由组 来创建多个根布局。查看 这里的示例
  • pages 目录迁移: 根布局替代了 _app.js_document.js 文件。 查看迁移指南

模板

模板与布局类似,都是包裹子布局或页面。但与在路由间保持状态的布局不同,模板在导航时会为其每个子组件创建一个新实例。这意味着当用户在共享模板的路由之间导航时,会挂载子组件的新实例,重新创建 DOM 元素,客户端组件中的状态不会保留,并且会重新同步 effects。

在某些情况下,你可能需要这些特定的行为,此时模板会是比布局更合适的选择。例如:

  • 在导航时重新同步 useEffect
  • 在导航时重置子客户端组件的状态。

可以通过从 template.js 文件中导出一个默认的 React 组件来定义模板。该组件应接受一个 children 属性。

template.js 特殊文件
app/template.tsx
TypeScript
export default function Template({ children }: { children: React.ReactNode }) {
  return <div>{children}</div>
}

在嵌套方面,template.js 在布局和其子组件之间渲染。以下是一个简化的输出:

output
<Layout>
  {/* 注意,模板被赋予了一个唯一的键。 */}
  <Template key={routeParam}>{children}</Template>
</Layout>

示例

元数据

你可以使用 元数据 API 修改 <head> HTML 元素,如 titlemeta

元数据可以通过在 layout.jspage.js 文件中导出 metadata 对象generateMetadata 函数 来定义。

app/page.tsx
TypeScript
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: 'Next.js',
}
 
export default function Page() {
  return '...'
}

值得注意的是:你不应在根布局中手动添加 <head> 标签,如 <title><meta>。相反,应该使用 元数据 API,它可以自动处理高级需求,如流式传输和去重 <head> 元素。

API 参考 中了解更多可用的元数据选项。

激活的导航链接

你可以使用 usePathname() hook 来确定导航链接是否处于激活状态。

由于 usePathname() 是一个客户端 hook,你需要将导航链接提取到一个客户端组件中,该组件可以导入到你的布局或模板中:

app/ui/nav-links.tsx
TypeScript
'use client'
 
import { usePathname } from 'next/navigation'
import Link from 'next/link'
 
export function NavLinks() {
  const pathname = usePathname()
 
  return (
    <nav>
      <Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
        首页
      </Link>
 
      <Link
        className={`link ${pathname === '/about' ? 'active' : ''}`}
        href="/about"
      >
        关于
      </Link>
    </nav>
  )
}
app/layout.tsx
TypeScript
import { NavLinks } from '@/app/ui/nav-links'
 
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <NavLinks />
        <main>{children}</main>
      </body>
    </html>
  )
}