1.定义布局
布局是指多个页面共享的 UI。在导航的时候,布局会保留状态、保持可交互性并且不会重新渲染,比如用来实现后台管理系统的侧边导航栏。
定义一个布局,你需要新建一个名为 layout.js
的文件,该文件默认导出一个 React 组件,该组件应接收一个 children
prop,chidren
表示子布局(如果有的话)或者子页面。
举个例子,我们新建目录和文件如下图所示:
// 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
的时候,效果如下:
其中,nav
来自于 app/dashboard/layout.js
,Hello, 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
的时候,效果如下:
其中,nav
来自于 app/dashboard/layout.js
,Hello, 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>
)
}
其中:
app
目录必须包含根布局,也就是app/layout.js
这个文件是必需的。- 根布局必须包含
html
和body
标签,其他布局不能包含这些标签。如果你要更改这些标签,不推荐直接修改, - 你可以使路由组创建多个根布局。
- 默认根布局是服务端组件,且不能设置为客户端组件。
3、 定义模板(Templates)
模板类似于布局,它也会传入每个子布局或者页面。但不会像布局那样维持状态。
模板在路由切换时会为每一个 children 创建一个实例。这就意味着当用户在共享一个模板的路由间跳转的时候,将会重新挂载组件实例,重新创建 DOM 元素,不保留状态。这听起来有点抽象,没有关系,我们先看看模板的写法,再写个 demo 你就明白了。
定义一个模板,你需要新建一个名为 template.js
的文件,该文件默认导出一个 React 组件,该组件接收一个 children
prop。我们写个示例代码。
在 app
目录下新建一个 template.js
文件:
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
会包裹 template
,template
又会包裹 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>
}
最终展示效果如下(为了方便区分,做了部分样式处理):
现在点击两个 Increment
按钮,会开始计数。随便点击下数字,然后再点击 About
或者 Settings
切换路由,你会发现,Layout 后的数字没有发生变化,Template 后的数字重置为 0。
这就是所谓的状态保持。
注:当然如果刷新页面,Layout 和 Template 后的数字肯定都重置为 0。