NextJS学习【路由篇】

CLI、App Router路由、后端路由处理程序、中间件

构建工具:Turbopack

React Compiler

Next CLI

next dev

自动具有热加载、错误报告

next build

alt text
Size:导航到该路由时下载的资源大小,每个路由的大小只包括它自己的依赖项
First Load JS:加载该页面时下载的资源大小
First load JS shared by all:所有路由共享的 JS 大小会被单独列出来

生产环境开启 React profiler:next build –profile

next build –debug

next start

启动生产环境服务器

Next路由模式 App Router

alt text

定义路由(Routes)

定义页面(Pages)

定义布局(Layouts)

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

同一文件夹下如果有 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
20
// 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标签,其他布局不能包含这些标签。如果你要更改这些标签,不推荐直接修改
你可以使用路由组创建多个根布局。
默认根布局是服务端组件,且不能设置为客户端组件。

定义模版(Templates)

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

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

template.js 代码如下:

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

Layouts和Templates最大的区别就是状态的保持。如果同一目录下既有 template.js 也有 layout.js,最后的输出效果如下:

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

layout 会包裹 template,template 又会包裹 page。

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

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

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

Layout VS Template
布局和模板的特点就是:

  • 布局嵌套:支持多层布局嵌套,构建复杂的页面结构
  • 状态管理:布局会在页面切换时保持状态,而模板会重新渲染
  • 根布局:app/layout.tsx 是必须存在的根布局文件
  • 渲染顺序:当布局和模板同时存在时,渲染顺序为 layout → template → page

定义加载界面(Loading UI)

实现Loading UI的几种方式
  1. 使用 page.js导出一个 async 函数
  2. React use函数

定义错误处理(Error Handling)

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

定义 404 页面(不存在的路由)

关于 app/not-found.js 一定要说明一点的是,它只能由两种情况触发:

  1. 当组件抛出了 notFound 函数的时候
  2. 当路由地址不匹配的时候

只需要在 app 目录下新建一个 not-found.js

链接和导航

在 Next.js 中,有 4 种方式可以实现路由导航:

  1. 使用 组件
  2. 使用 useRouter Hook(客户端组件)
  3. 使用 redirect 函数(服务端组件)
  4. 使用浏览器原生 History API

导航行为设置

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

如果你想要禁用这个行为,你可以给 组件传递一个 scroll={false}属性,或者在使用 router.push和 router.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 })

动态路由

使用动态路由,需要将文件夹的名字用方括号括住,比如 [id]、[slug]。这个路由的名字会作为 params prop 传给布局、 页面、 路由处理程序 以及 generateMetadata 函数。

[folderName]

当你访问 /blog/a的时候,params 的值为 { slug: ‘a’ }。

当你访问 /blog/yayu的时候,params 的值为 { slug: ‘yayu’ }。

[…folderName]

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

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

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

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

[[…folderName]]

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

路由组

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

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

  1. 按站点、意图、团队等将路由分组
  2. 在同一层级中创建多个布局,甚至是创建多个根布局
    那么该如何标记呢?把文件夹用括号括住就可以了,就比如 (dashboard)。

按逻辑分组

alt text

创建不同布局

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

创建多个根布局

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

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

平行路由

类似Vue的插槽功能
平行路由跟路由组一样,不会影响 URL

独立路由处理

  1. 使用平行路由可以将单个布局拆分为多个插槽,使代码更易于管理,尤其适用于团队协作的时候
  2. 每个插槽都可以定义自己的加载界面和错误状态,比如某个插槽加载速度比较慢,那就可以加一个加载效果,加载期间,也不会影响其他插槽的渲染和交互。当出现错误的时候,也只会在具体的插槽上出现错误提示,而不会影响页面其他部分,有效改善用户体验
  3. 每个插槽都可以有自己独立的导航和状态管理,这使得插槽的功能更加丰富,比如在上面的例子中,我们在 @analytics 插槽下又建了查看页面 PV 的 /page-views、查看访客的 /visitors,使得同一个插槽区域可以根据路由显示不同的内容

用途1:条件渲染

用途2:子导航

default.js

为了解决这个问题,Next.js 提供了 default.js。当发生硬导航的时候,Next.js 会为不匹配的插槽呈现 default.js 中定义的内容,如果 default.js 没有定义,再渲染 404 错误。

硬导航影响,软导航不变

拦截路由

https://nextjs-app-route-interception.vercel.app/

同样一个路由地址,却展示了不同的内容。这就是拦截路由的效果。如果你在 dribbble.com 想要访问 dribbble.com/shots/xxxxx,此时会拦截 dribbble.com/shots/xxxxx 这个路由地址,以 Modal 的形式展现。而当直接访问 dribbble.com/shots/xxxxx 时,则是原本的样式。

简单的来说,就是希望用户继续停留在重要的页面上。比如上述例子中的图片流页面,开发者肯定是希望用户能够持续在图片流页面浏览,如果点击一张图片就跳转出去,会打断用户的浏览体验,如果点击只展示一个 Modal,分享操作又会变得麻烦一点。拦截路由正好可以实现这样一种平衡。又比如任务列表页面,点击其中一项任务,弹出 Modal 让你能够编辑此任务,同时又可以方便的分享任务内容。

实现方式

  1. (.) 表示匹配同一层级
  2. (..) 表示匹配上一层级
  3. (..)(..) 表示匹配上上层级。
  4. (…) 表示匹配根目录

路由处理程序(网络请求)

使用next框架做接口开发 – route handler

定义路由处理程序

route.js
alt text

该文件必须在 app目录下,可以在 app 嵌套的文件夹下,但是要注意 page.js和 route.js不能在同一层级同时存在。

想想也能理解,page.js和 route.js本质上都是对路由的响应。page.js主要负责渲染 UI,route.js主要负责处理请求。如果同时存在,Next.js 就不知道用谁的逻辑了。

支持的请求方法

Next.js 支持 GET、POST、PUT、PATCH、DELETE、HEAD 和 OPTIONS 这些 HTTP 请求方法。如果传入了不支持的请求方法,Next.js 会返回 405 Method Not Allowed。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// route.js
export async function GET(request) {}

export async function HEAD(request) {}

export async function POST(request) {}

export async function PUT(request) {}

export async function DELETE(request) {}

export async function PATCH(request) {}

// 如果 `OPTIONS` 没有定义, Next.js 会自动实现 `OPTIONS`
export async function OPTIONS(request) {}

实现一个Get请求

1
2
3
4
5
6
7
8
import { NextResponse } from 'next/server'

export async function GET() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts')
const data = await res.json()

return NextResponse.json({ data })
}

实现一个Post请求

1
2
3
4
5
6
7
8
export async function POST(request) {
const article = await request.json()

return NextResponse.json({
id: Math.random().toString(36).slice(-8),
data: article
}, { status: 201 })
}

传入参数

每个请求方法的处理函数会被传入两个参数,一个 request,一个 context 。两个参数都是可选的:

  1. request (optional)
    使用request对象,可以快捷的读取 cookies 和处理 URL。
1
2
3
4
5
6
export async function GET(request, context) {
// 访问 /home, pathname 的值为 /home
const pathname = request.nextUrl.pathname
// 访问 /home?name=lee, searchParams 的值为 { 'name': 'lee' }
const searchParams = request.nextUrl.searchParams
}
  1. context (optional)
    context 只有一个值就是 params,它是一个包含当前动态路由参数的对象。
1
2
3
4
// app/dashboard/[team]/route.js
export async function GET(request, { params }) {
const team = params.team
}

当访问 /dashboard/1 时,params 的值为 { team: ‘1’ }

app/shop/[tag]/[item]/route.js
/shop/1/2
{ tag: ‘1’, item: ‘2’ }

app/blog/[…slug]/route.js
/blog/1/2
{ slug: [‘1’, ‘2’] }

缓存行为

默认缓存
退出缓存

这些情况都会导致退出缓存:

  1. GET 请求使用 Request 对象
  2. 使用其他 HTTP 方法,比如 POST
  3. 路由段配置项手动声明为动态模式
1
2
3
4
5
export const dynamic = 'force-dynamic'

export async function GET() {
return Response.json({ data: new Date().toLocaleTimeString() })
}
设置缓存

除了退出缓存,也可以设置缓存的时效,适用于一些重要性低、时效性低的页面。

有两种常用的方案,一种是使用路由段配置项。

1
2
3
4
5
6
7
export const revalidate = 10

export async function GET() {
return Response.json({ data: new Date().toLocaleTimeString() })
}
export const revalidate = 10 表示设置重新验证频率为 10s,但是要注意:

这句代码的效果并不是设置服务器每 10s 会自动更新一次 /api/time。而是最少 10s 后才重新验证。

举个例子,假设现在访问了 /api/time,此时时间设为 0s,10s 内持续访问,/api/time返回的都是之前缓存的结果。当 10s 过后,假设你第 12s 又访问了一次 /api/time,此时虽然超过了 10s,但依然会返回之前缓存的结果,但同时会触发服务器更新缓存,当你第 13s 再次访问的时候,就是更新后的结果。

简单来说,超过 revalidate 设置时间的首次访问会触发缓存更新,如果更新成功,后续的返回就都是新的内容,直到下一次触发缓存更新。

中间件

使用中间件,可以拦截并控制应用里的所有请求和响应。

比如可以基于传入的请求,重写、重定向、修改请求或响应头、甚至直接响应内容。一个比较常见的应用就是鉴权,在打开页面渲染具体的内容前,先判断用户是否登录,如果未登录,则跳转到登录页面。

中间件示例

写中间件,你需要在项目的根目录定义一个名为 middleware.js的文件:

1
2
3
4
5
6
7
8
9
10
11
12
// middleware.js
import { NextResponse } from 'next/server'

// 中间件可以是 async 函数,如果使用了 await
export function middleware(request) {
return NextResponse.redirect(new URL('/home', request.url))
}

// 设置匹配路径
export const config = {
matcher: '/about/:path*',
}

设置匹配路径

matcher配置项

第一种是使用 matcher配置项,示例代码如下:

1
2
3
export const config = {
matcher: '/about/:path*',
}

matcher 不仅支持字符串形式,也支持数组形式,用于匹配多个路径:

1
2
3
export const config = {
matcher: ['/about/:path*', '/dashboard/:path*'],
}

matcher 还可以判断查询参数、cookies、headers

1
2
3
4
5
6
7
8
9
10
11
12
export const config = {
matcher: [
{
source: '/api/*',
has: [
{ type: 'header', key: 'Authorization', value: 'Bearer Token' },
{ type: 'query', key: 'userId', value: '123' },
],
missing: [{ type: 'cookie', key: 'session', value: 'active' }],
},
],
}
条件语句
1
2
3
4
5
6
7
8
9
10
11
import { NextResponse } from 'next/server'

export function middleware(request) {
if (request.nextUrl.pathname.startsWith('/about')) {
return NextResponse.rewrite(new URL('/about-2', request.url))
}

if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}
}

中间件逻辑

设置和读取cookies

用法跟路由处理程序一致,使用 NextRequest 和 NextResponse 快捷读取和设置 cookies。

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
26
27
import { NextResponse } from 'next/server'

export function middleware(request) {
// 假设传入的请求 header 里 "Cookie:nextjs=fast"
let cookie = request.cookies.get('nextjs')
console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
const allCookies = request.cookies.getAll()
console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]

request.cookies.has('nextjs') // => true
request.cookies.delete('nextjs')
request.cookies.has('nextjs') // => false

// 设置 cookies
const response = NextResponse.next()
response.cookies.set('vercel', 'fast')
response.cookies.set({
name: 'vercel',
value: 'fast',
path: '/',
})
cookie = response.cookies.get('vercel')
console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }

// 响应 header 为 `Set-Cookie:vercel=fast;path=/test`
return response
}

这里调用了 NextResponse.next() 这个方法,这个方法专门用在 middleware 中,因为中间件进行一层处理后,返回的结果还要在下一个逻辑中继续使用,此时就需要返回 NextResponse.next()

设置和读取headers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// middleware.js 
import { NextResponse } from 'next/server'

export function middleware(request) {
// clone 请求标头
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-hello-from-middleware1', 'hello')

// 你也可以在 NextResponse.rewrite 中设置请求标头
const response = NextResponse.next({
request: {
// 设置新请求标头
headers: requestHeaders,
},
})

// 设置新响应标头 `x-hello-from-middleware2`
response.headers.set('x-hello-from-middleware2', 'hello')
return response
}
设置Cors
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { NextResponse } from 'next/server'

const allowedOrigins = ['https://acme.com', 'https://my-app.org']

const corsOptions = {
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}

export function middleware(request) {
// Check the origin from the request
const origin = request.headers.get('origin') ?? ''
const isAllowedOrigin = allowedOrigins.includes(origin)

// Handle preflighted requests
const isPreflight = request.method === 'OPTIONS'

if (isPreflight) {
const preflightHeaders = {
...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
...corsOptions,
}
return NextResponse.json({}, { headers: preflightHeaders })
}

// Handle simple requests
const response = NextResponse.next()

if (isAllowedOrigin) {
response.headers.set('Access-Control-Allow-Origin', origin)
}

Object.entries(corsOptions).forEach(([key, value]) => {
response.headers.set(key, value)
})

return response
}

export const config = {
matcher: '/api/:path*',
}
直接响应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { NextResponse } from 'next/server'
import { isAuthenticated } from '@lib/auth'

export const config = {
matcher: '/api/:function*',
}

export function middleware(request) {
// 鉴权判断
if (!isAuthenticated(request)) {
// 返回错误信息
return new NextResponse(
JSON.stringify({ success: false, message: 'authentication failed' }),
{ status: 401, headers: { 'content-type': 'application/json' } }
)
}
}

执行顺序

  1. headers(next.config.js)
  2. redirects(next.config.js)
  3. 中间件 (rewrites, redirects 等)
  4. beforeFiles (next.config.js中的rewrites)
  5. 基于文件系统的路由 (public/, _next/static/, pages/, app/ 等)
  6. afterFiles(next.config.js中的rewrites)
  7. 动态路由 (/blog/[slug])
  8. fallback中的 (next.config.js中的rewrites)

注: beforeFiles 顾名思义,在基于文件系统的路由之前,afterFiles顾名思义,在基于文件系统的路由之后,fallback顾名思义,垫底执行。