NEXT.JS从入门到放弃

包学包会!看完你也会

NEXT.JS 官方英文文档:https://nextjs.org/

中文文档不建议看,落后太多!

1. 初始篇

1.1 创建项目

使用 create-next-app 脚手架

1
2
npx
create - next - app

如果你不使用 npx,也支持使用 yarn、pnpm、bunx

1
2
3
4
5
6
7
8
9
10
yarn
create
next - app

pnpm
create
next - app

bunx
create - next - app

1.2 运行项目

查看项目根目录 package.json 文件的代码:

1
2
3
4
5
6
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}

开发的时候使用 npm run dev。部署的时候先使用 npm run build构建生产代码,再执行 npm run start
运行生产项目。运行 npm run lint则会执行 ESLint 语法检查。

2. 路由篇(App Router)

Next.js 目前有两套路由解决方案,之前的方案称之为“Pages Router”,目前的方案称之为“App Router”,两套方案是兼容的,都可以在
Next.js 中使用。

目前,我们主要学习“App Router”。

1. 文件系统

Next.js 的路由基于的是文件系统,也就是说,一个文件就可以是一个路由。举个例子,你在 app/pages 目录下创建一个 index.js
文件,它会直接映射到 / 路由地址;在 app/pages 目录下创建一个 about.js 文件,它会直接映射到 /about 路由地址。

2. App Router

2.1 定义路由

文件夹被用来定义路由。

app/dashboard/settings目录对应的路由地址就是 /dashboard/settings

2.2 定义页面(Pages)

你需要创建一个特殊的名为 page.js 的文件,这是一种约定和规范

目录结构

在上图这个例子中:

  • app/page.js 对应路由 /
  • app/dashboard/page.js 对应路由 /dashboard
  • app/dashboard/settings/page.js 对应路由/dashboard/settings
  • analytics 目录下因为没有 page.js 文件,所以没有对应的路由。这个文件可以被用于存放组件、样式表、图片或者其他文件。

page.js 的代码如下:

1
2
3
4
// app/page.js
export default function Page() {
return <h1>Hello, Next.js!</h1>
}

2.3 定义布局(Layouts)

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

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

目录结构

1
2
3
4
5
6
7
8
9
10
11
// app/dashboard/layout.js
export default function DashboardLayout({
children,
}) {
return (
<section>
<nav>nav</nav>
{children}
</section>
)
}
1
2
3
4
// app/dashboard/page.js
export default function Page() {
return <h1>Hello, Dashboard!</h1>
}

显示

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

根布局(Root Layout)

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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标签,其他布局不能包含这些标签。但如果你要更改这些标签,不推荐直接修改,Next.js
    提供内置工具帮助你管理诸如 <title /> 这样的 HTML 元素。
  • 你可以使用路由组创建多个根布局。
  • 默认根布局是服务端组件,且不能设置为客户端组件。

2.4 定义模板(Templates)

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

模板在路由切换时会为每一个 children 创建一个实例。这就意味着当用户在共享一个模板的路由间跳转的时候,将会重新挂载组件实例,重新创建
DOM 元素,不保留状态。

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

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

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

模板会比布局更适合以下场景:

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

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

2.5 定义加载页面(Loading UI)

App Router 提供了用于展示加载界面的 loading.js

这个功能的实现借助了 React 的Suspense API。关于 Suspense
的用法,可以查看 《React 之 Suspense》。它实现的效果就是当发生路由变化的时候,立刻展示
fallback UI,等加载完成后,展示数据。

1
2
3
4
// 在 ProfilePage 组件处于加载阶段时显示 Spinner
<Suspense fallback={<Spinner/>}>
<ProfilePage/>
</Suspense>

新建一个loading.js 文件

1
2
3
4
// app/dashboard/loading.js
export default function DashboardLoading() {
return <>Loading dashboard...</>
}

同级的page.js文件

1
2
3
4
5
6
7
8
9
10
11
12
// app/dashboard/page.js
async function getData() {
await new Promise((resolve) => setTimeout(resolve, 3000))
return {
message: 'Hello, Dashboard!',
}
}

export default async function DashboardPage(props) {
const {message} = await getData()
return <h1>{message}</h1>
}

这样页面的加载loading效果就出现了。

其关键在于 page.js导出了一个 async 函数。

loading.js 的实现原理是将 page.js和下面的 children 用 <Suspense> 包裹。因为page.js导出一个 async 函数,Suspense
得以捕获数据加载的 promise,借此实现了 loading 组件的关闭。

2.6 定义错误处理(Error Handling)

error.js。顾名思义,用来创建发生错误时的展示 UI。

其实现借助了 React
Error Boundary
功能。简单来说,就是给 page.js 和 children 包了一层 ErrorBoundary

新建一个 error.js 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
'use client' // 错误组件必须是客户端组件
// dashboard/error.js
import {useEffect} from 'react'

export default function Error({error, reset}) {
useEffect(() => {
console.error(error)
}, [error])

return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// 尝试恢复
() => reset()
}
>
Try again
</button>
</div>
)
}

同级 page.js 的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"use client";
// dashboard/page.js
import React from "react";

export default function Page() {
const [error, setError] = React.useState(false);

const handleGetError = () => {
setError(true);
};

return (
<>{error ? Error() : <button onClick={handleGetError}>Get Error</button>}</>
);
}

当前这些特殊文件的层级关系如下图:
层级关系

因为 LayoutTemplateErrorBoundary 外面,这说明错误边界不能捕获同级的 layout.js 或者 template.js
中的错误。如果你想捕获特定布局或者模板中的错误,那就在父级的 error.js 里进行捕获。

那问题来了,如果已经到了顶层,就比如根布局中的错误如何捕获呢?为了解决这个问题,Next.js 提供了 global-error.js文件,使用它时,将其放在
app 目录下。

global-error.js会包裹整个应用,而且当它触发的时候,它会替换掉根布局的内容。所以,global-error.js 中也要定义 <html>
<body>
标签。

1
2
3
4
5
6
7
8
9
10
11
12
'use client'
// app/global-error.js
export default function GlobalError({error, reset}) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}

注:global-error.js 用来处理根布局和根模板中的错误,app/error.js 建议还是要写的

2.7 定义404页面

not-found.js。顾名思义,当该路由不存在的时候展示的内容

需要在 app 目录下新建一个 not-found.js,代码示例如下

1
2
3
4
5
6
7
8
9
10
11
import Link from 'next/link'

export default function NotFound() {
return (
<div>
<h2>Not Found</h2>
<p>Could not find requested resource</p>
<Link href="/">Return Home</Link>
</div>
)
}

当我们请求一个用户的数据时或是请求一篇文章的数据时,如果该数据不存在,就可以直接丢出 notFound
函数,渲染自定义的not-found
界面。一个示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// app/dashboard/blog/[id]/page.js
import {notFound} from 'next/navigation'

async function fetchUser(id) {
const res = await fetch('https://...')
if (!res.ok) return undefined
return res.json()
}

export default async function Profile({params}) {
const user = await fetchUser(params.id)

if (!user) {
notFound()
}

// ...
}

链接与导航

知道了如何定义路由,最后我们再讲讲如何在 Next.js 中实现链接和导航。Next.js 提供了两种方式:

  1. 使用 <Link> 组件
  2. 使用 useRouter Hook

使用方式:

1
2
3
4
5
import Link from 'next/link'

export default function Page() {
return <Link href="/dashboard">Dashboard</Link>
}

如果需要对当前链接进行判断,你可以使用 usePathname() 这个方法,它会读取当前 URL 的路径名(pathname),这是一段示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
'use client'

import {usePathname} from 'next/navigation'
import Link from 'next/link'

export function Navigation({navLinks}) {
const pathname = usePathname()

return (
<>
{navLinks.map((link) => {
const isActive = pathname === link.href
return (
<Link
className={isActive ? 'text-blue' : 'text-black'}
href={link.href}
key={link.name}
>
{link.name}
</Link>
)
})}
</>
)
}

App Router 的默认行为是滚动到新路由的顶部,或者在前进后退导航时维持之前的滚动距离。

如果你想要禁用这个行为,你可以给 <Link> 组件传递一个 scroll={false},或者在使用 router.pushrouter.replace的时候,设置
scroll: false

示例代码如下:

1
2
3
4
// next/link
<Link href="/dashboard" scroll={false}>
Dashboard
</Link>

或者:

1
2
3
4
5
6
// useRouter
import {useRouter} from 'next/navigation'

const router = useRouter()

router.push('/dashboard', {scroll: false})

useRouter

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
'use client'

import {useRouter} from 'next/navigation'

export default function Page() {
const router = useRouter()

return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
)
}

注意使用该 hook 需要在客户端组件中。(顶层的 use client 就是声明这是客户端组件)

3. 路由篇(动态路由、路由组、平行路由、拦截路由)

1. 动态路由

有的时候,你并不能提前知道路由的地址,就比如根据 URL 中的 id 参数展示该 id 对应的文章内容,文章那么多,我们不可能一一定义路由,这个时候就需要用到动态路由。

1.1 [folderName]

使用动态路由,你需要将文件夹的名字用方括号括住,比如 [id][slug]。这个路由的名字会作为 param prop 传给布局(layout)页面(page)路由处理程序(route)以及 generateMetadata(用于生成页面元数据) 函数。

举个例子,我们在 app/blog 目录下新建一个名为[slug]的文件夹,在该文件夹新建一个 page.js 文件,代码如下:

1
2
3
4
// app/blog/[slug]/page.js
export default function Page({ params }) {
return <div>My Post: {params.slug}</div>
}

页面效果 :

Alt text

1.2 […folderName]

如果你在方括号内添加省略号,比如 [...folderName],这表示捕获所有后面所有的路由片段。

也就是说,app/shop/[...slug]/page.js会匹配 /shop/clothes,也会匹配 /shop/clothes/tops/shop/clothes/tops/t-shirts等等。

1
2
3
4
// app/shop/[...slug]/page.js
export default function Page({ params }) {
return <div>My Shop: {JSON.stringify(params)}</div>
}

页面效果:
Alt text

当你访问 /shop/a的时候,params 的值为 { slug: ['a'] }

当你访问 /shop/a/b的时候,params 的值为 { slug: ['a', 'b'] }

当你访问 /shop/a/b/c的时候,params 的值为 { slug: ['a', 'b', 'c'] }

1.3 [[…folderName]]

在命名文件夹的时候,如果你在双方括号内添加省略号,比如 [[…folderName]],这表示可选的捕获所有后面所有的路由片段。

也就是说,app/shop/[[...slug]]/page.js会匹配 /shop,也会匹配 /shop/clothes/shop/clothes/tops/shop/clothes/tops/t-shirts等等。

它与上一种的区别就在于,不带参数的路由也会被匹配(就比如 /shop

2. 路由组(Route groups)

app目录下,文件夹名称通常会被映射到 URL 中,但你可以将文件夹标记为路由组,阻止文件夹名称被映射到 URL 中。

使用路由组,你可以将路由和项目文件按照逻辑进行分组,但不会影响 URL 路径结构。路由组可用于比如:

按站点、意图、团队等将路由分组

  1. 在同一层级中创建多个布局,甚至是创建多个根布局
  2. 那么该如何标记呢?把文件夹用括号括住就可以了,就比如 (dashboard)

2.1 按逻辑分组

按逻辑分组

最终的url中省略了带括号的文件夹

2.2 创建不同布局

借助路由组,即便在同一层级,也可以创建不同的布局:

创建不同的布局

在这个例子中,/account/cart/checkout 都在同一层级。但是 /account/cart使用的是 /app/(shop)/layout.js布局和app/layout.js布局,/checkout使用的是 app/layout.js

2.3 创建多个根布局

创建多个根布局

创建多个根布局,你需要删除掉 app/layout.js 文件,然后在每组都创建一个 layout.js文件。创建的时候要注意,因为是根布局,所以要有 <html> <body> 标签。

这个功能很实用,比如你将前台购买页面和后台管理页面都放在一个项目里,一个 C 端,一个 B 端,两个项目的布局肯定不一样,借助路由组,就可以轻松实现区分。

路由组tips:

  1. 路由组的命名除了用于组织之外并无特殊意义。它们不会影响 URL 路径。
  2. 注意不要解析为相同的 URL 路径。举个例子,因为路由组不影响 URL 路径,所以 (marketing)/about/page.js(shop)/about/page.js都会解析为 /about,这会导致报错。
  3. 创建多个根布局的时候,因为删除了顶层的 app/layout.js文件,访问 /会报错,所以app/page.js需要定义在其中一个路由组中。
  4. 跨根布局导航会导致页面完全重新加载,就比如使用 app/(shop)/layout.js根布局的 /cart 跳转到使用 app/(marketing)/layout.js根布局的 /blog 会导致页面重新加载(full page load)

NEXT.JS从入门到放弃
https://zouhualu.github.io/20240130/NEXT-JS从入门到放弃/
作者
花鹿
发布于
2024年1月30日
许可协议