1.定义布局

布局是指多个页面共享的 UI。在导航的时候,布局会保留状态、保持可交互性并且不会重新渲染,比如用来实现后台管理系统的侧边导航栏。

定义一个布局,你需要新建一个名为 layout.js的文件,该文件默认导出一个 React 组件,该组件应接收一个 children prop,chidren 表示子布局(如果有的话)或者子页面。

举个例子,我们新建目录和文件如下图所示:

Next App Router(中)-LMLPHP

// app/dashboard/layout.js
export default function DashboardLayout({
  children,
}) {
  return (
    <section>
      <nav>nav</nav>
      {children}
    </section>
  )
}
// app/dashboard/page.js
export default function Page() {
  return <h1>Hello, Dashboard!</h1>
}

当访问 /dashboard的时候,效果如下:

Next App Router(中)-LMLPHP

其中,nav 来自于 app/dashboard/layout.jsHello, Dashboard! 来自于 app/dashboard/page.js

你可以发现:同一文件夹下如果有 layout.js 和 page.js,page 会作为 children 参数传入 layout。换句话说,layout 会包裹同层级的 page。

app/dashboard/settings/page.js 代码如下:

// app/dashboard/settings/page.js
export default function Page() {
  return <h1>Hello, Settings!</h1>
}

当访问 /dashboard/settings的时候,效果如下:

Next App Router(中)-LMLPHP

其中,nav 来自于 app/dashboard/layout.jsHello, Settings! 来自于 app/dashboard/settings/page.js

你可以发现:布局是支持嵌套的app/dashboard/settings/page.js 会使用 app/layout.js 和 app/dashboard/layout.js 两个布局中的内容,不过因为我们没有在 app/layout.js 写入可以展示的内容,所以图中没有体现出来。

2、根布局

布局支持嵌套,最顶层的布局我们称之为根布局(Root Layout),也就是 app/layout.js。它会应用于所有的路由。除此之外,这个布局还有点特殊。

使用 create-next-app 默认创建的 layout.js 代码如下:

// app/layout.js
import './globals.css'
import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  )
}

其中:

  1. app 目录必须包含根布局,也就是 app/layout.js 这个文件是必需的。
  2. 根布局必须包含 html 和 body标签,其他布局不能包含这些标签。如果你要更改这些标签,不推荐直接修改,
  3. 你可以使路由组创建多个根布局。
  4. 默认根布局是服务端组件,且不能设置为客户端组件。

3、 定义模板(Templates)

模板类似于布局,它也会传入每个子布局或者页面。但不会像布局那样维持状态。

模板在路由切换时会为每一个 children 创建一个实例。这就意味着当用户在共享一个模板的路由间跳转的时候,将会重新挂载组件实例,重新创建 DOM 元素,不保留状态。这听起来有点抽象,没有关系,我们先看看模板的写法,再写个 demo 你就明白了。

定义一个模板,你需要新建一个名为 template.js 的文件,该文件默认导出一个 React 组件,该组件接收一个 children prop。我们写个示例代码。

在 app目录下新建一个 template.js文件:

Next App Router(中)-LMLPHP

template.js 代码如下

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

你会发现,这用法跟布局一模一样。它们最大的区别就是状态的保持。如果同一目录下既有 template.js 也有 layout.js,最后的输出效果如下:

<Layout>
  {/* 模板需要给一个唯一的 key */}
  <Template key={routeParam}>{children}</Template>
</Layout>

也就是说 layout 会包裹 templatetemplate 又会包裹 page

某些情况下,模板会比布局更适合:

  • 依赖于 useEffect 和 useState 的功能,比如记录页面访问数(维持状态就不会在路由切换时记录访问数了)、用户反馈表单(每次重新填写)等

  • 更改框架的默认行为,举个例子,布局内的 Suspense 只会在布局加载的时候展示一次 fallback UI,当切换页面的时候不会展示。但是使用模板,fallback 会在每次路由切换的时候展示

4、布局 VS 模板

为了帮助大家更好的理解布局和模板,我们写一个 demo,展示下两者的特性。

app
└─ dashboard
   ├─ layout.js
   ├─ page.js
   ├─ template.js
   ├─ about
   │  └─ page.js
   └─ settings
      └─ page.js

其中 dashboard/layout.js 代码如下:

'use client'

import { useState } from 'react'
import Link from 'next/link'

export default function Layout({ children }) {
  const [count, setCount] = useState(0)
  return (
    <>
      <div>
        <Link href="/dashboard/about">About</Link>
        <br/>
        <Link href="/dashboard/settings">Settings</Link>
      </div>
      <h1>Layout {count}</h1>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      {children}
    </>
  )
}

dashboard/template.js 代码如下:

'use client'

import { useState } from 'react'

export default function Template({ children }) {
  const [count, setCount] = useState(0)
  return (
    <>
      <h1>Template {count}</h1>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
      {children}
    </>
  )
}

dashboard/page.js代码如下:

export default function Page() {
  return <h1>Hello, Dashboard!</h1>
}

dashboard/about/page.js代码如下:

export default function Page() {
  return <h1>Hello, About!</h1>
}

dashboard/settings/page.js代码如下:

export default function Page() {
  return <h1>Hello, Settings!</h1>
}

最终展示效果如下(为了方便区分,做了部分样式处理):

Next App Router(中)-LMLPHP

现在点击两个 Increment 按钮,会开始计数。随便点击下数字,然后再点击 About或者 Settings切换路由,你会发现,Layout 后的数字没有发生变化,Template 后的数字重置为 0。

这就是所谓的状态保持。

注:当然如果刷新页面,Layout 和 Template 后的数字肯定都重置为 0。

04-20 04:34