Next 路由篇

路由导航

基本使用

1
2
3
4
5
import Link from "next/link";

export default async function Index() {
return <Link href="/about">To About</Link>
}

useRouter

1
2
3
4
5
6
7
8
9
10
'use client'
import {useRouter} from 'next/navigation'

export default function Index() {
const router = useRouter();
const toAbout = () => {
router.push('/about')
}
return <button onClick={toAbout}>To About</button>
}

使用 useRouter 必须在客户端组件,也就是在文件顶部加上 'use client' 还有就是 useRouter 是 “next/navigation” 导入的,不是 “next/router”。

redirect

1
2
3
export default function Index() {
redirect('/about')
}

服务端组件使用 redirect 进行重定向跳转。

history

history 是浏览器提供的原生跳转方式,有 pushState 和 replaceState 两种方式,前者是向路由栈推一条新的路由,后者是直接将当前条替换掉

跳转行为

在 App Router 下跳转后默认滚动到顶部,使用导航的前进后退会保持原来的滚动距离

1
2
3
4
5
import Link from "next/link";

export default async function Index() {
return <Link href="/about" scroll={false}>To About</Link>
}

通过给 Link 标签添加 scroll 属性,或者使用 router 跳转时第二个参数对象加上 scroll: false 禁用行为。

动态路由

动态路由需要用[]将文件夹名包住,例如新建如下结构目录:

src/app/dynamic-routing/[id]/page.js 就是一个 id 的动态路由

1
2
3
4
export default (props) => {
const { params } = props;
return <>{params.id}</>
}

这时访问 /dynamic-routing/111 页面现实的结果就是 111

但如果有多个参数的情况以上方法就不好用了,这时只需要将文件夹的名改成 [...params],声明接收所有参数

1
2
3
4
export default (props) => {
const params = JSON.stringify(props.params.params)
return <>{params}</>
}

这时我们访问 /dynamic-routing/name/age/hobby 来看下效果

可以看到的是参数是以数组的形式传递进来的。

路由组

通过使用 () 包裹文件夹名创建分组,通过 () 创建的文件夹不会出现在路由的路径中,例如下图 (group1)(group2) 并不会对路由产生影响,也正因如此,即使在不同分组下,也不能创建同名的文件夹

路由组

多个根布局

使用路由组后,可以给每个组单独设置 layout,也就是在每个分组下新建 layout.js,如果这样做的话,就需要删除掉 app 下的 layout.js,并且因为是根布局,每个分组下的 layout 都需要有 和 标签。
只是删除 app 下的 layout.js 的话,还是会报错,因为还需要将 app 下的 page.js 放入到某一个分组下,需要注意的是,放在某个分组下就会使用这个分组的 layout。

平行路由

当一个页面布局需要多个页面显示在一起的时候可以使用平行路由,通过 @ 作为文件夹的开头声明这是一个平行路由,在平行路由的 layout 中通过 props 拿到并渲染

目录结构

layout.js

1
2
3
4
5
6
7
8
9
10
11
export default function RootLayout({ children, address, contact }) {
return (
<html lang="en">
<body>
{children}
{address}
{contact}
</body>
</html>
)
}

并且平行路由还可以嵌套子路由,例如在 @address 下新增 detail/page.js ,然后在 layout 中加入跳转 <Link href="/detail">to detail</Link>

效果图

可以看到当进行跳转时,只有 address 部分进行了更新,然后我们刷新页面会发现页面404了

为什么会出现以上的情况呢?简单来说我们给 @address 加了 detail 的子路由,而 @contact 没有,当通过 Link 软导航跳转时,Next 进行了部分渲染,也就是对匹配上路由的 @address 进行更新,而 @contact 匹配不上,保留了原来的状态,当我们刷新浏览器进行硬导航时,Next 没办法判断不匹配路由,也就是 @contact 的状态没办法进行渲染,所以 404。

想要解决 404 的问题,我们需要给不匹配的路由增加 default.js 在不匹配时显示

效果

路由拦截

Next 提供路由拦截功能,当从一个路由跳转到新的路由可以进行拦截渲染。

实现方式:在文件夹名前加上 (.) 声明拦截路由,按照官网说的不同数量的 . 代表拦截不同层级

  • (.)来匹配同一层级
  • (..)来匹配上一层级
  • (..)(..)来匹配上两层
  • (…)来匹配应用程序根目录

我们新建一个 product ,这里希望使用 (.)detail 拦截 '/product/detail'

目录结构

1
2
3
4
// (.)detail/page.js
export default () => {
return<>拦截 Product Detail</>
}

剩下的文件随便写点内容即可,我们看下效果

可以看到当我们从 /product 跳转到 /product/detail 时被拦截了,而我们硬导航刷新页面则不会被拦截。

路由处理程序

Next 中命名为 route.js 的文件路由处理程序,也就是负责请求的文件,该文件必须在 app 目录下,并且不能和 page.js 同级。因为在同级的情况下,请求和页面路径相同产生冲突。

我们新增 api/test/route.js 文件

1
2
3
export async function GET() {
return await fetch("xxxxxxxxx")
}

这里写了一个最简单的 GET 请求,通过 /api/test 即可访问接口,除了 GET 请求还有 POST、PUT、PATCH、DELETE、HEAD 和 OPTIONS ,另外每个请求都有两个参数 request 和 context。

1
2
3
export async function GET(request, context){}
export async function POST(request, context){}
// ...

request

request 是对 Web Request API 的扩展

request

通过 request 获取 search 参数

1
2
3
4
export async function GET(request) {
const field = request.nextUrl.searchParams.get("name")
// ....
}

通过 request.nextUrl.searchParams.get() 的方法来获取 search 参数

通过 request 获取 body 参数

1
2
3
4
5
export async function POST(request) {
const res = await request.json()
console.log(res)
return Response.json({ res })
}

这里我们写一个 POST 请求,然后随便传递参数进来看打印结果。

context

context 只有一个 params,这里我们新建目录来测试

我们让接口地址传入一个 name 参数进来,看看 context 会打印出什么

我接口调用 /api/jack 可以看到 context 输出了 name为 jack 的对象,如果有多个参数需要传递,也可以使用上文说到的 [...params] 的方式命名文件夹,参数会以数组的形式传递进来。

缓存

NextResponse

NextResponse 是 Next 提供的网络响应 API,他是对Response的扩展,在生产环境下,使用 Response 或者 NextResponse 的 GET 请求会被缓存。

1
2
3
export async function GET() {
return NextResponse.json({ data: new Date().toLocaleTimeString() })
}

我们写一行这样一段 api 然后 执行 npm run build 打包,再执行 npm run start 构建

这时我们访问这个接口发现日期是不会更新的

退出缓存的方式

  1. GET 请求使用 Request 对象
    1
    2
    3
    4
    export async function GET(request) {
    const params = request.nextUrl.searchParams
    return NextResponse.json({ data: new Date().toLocaleTimeString(), params: params.toString() })
    }
  2. 添加其他的请求,如 POST
  3. 手动声明为动态模式
    1
    2
    3
    4
    5
    6
    7
    import { NextResponse } from 'next/server'

    export const dynamic = 'force-dynamic'

    export async function GET() {
    return NextResponse.json({ data: new Date().toLocaleTimeString() })
    }
  4. 设置缓存失效
    1
    2
    3
    4
    5
    6
    // 设置重新验证频率为 10s 就是十秒后第一次请求还是会拿到缓存值,再次请求才会拿到新的值
    export const revalidate = 10

    export async function GET() {
    return NextResponse.json({ data: new Date().toLocaleTimeString() })
    }

路由中间件

中间件一般用来拦截应用的请求和响应,一般写在 src 文件夹下,也就是存放路由页面的根目录(当然可以不是 src)。

目录结构

官网给了一个中间件的例子,功能就是拦截一个路由并且进行重定向。这里可以根据自己的例子修改下路由即可。

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

export function middleware(request) {
return NextResponse.redirect(new URL('/', request.url))
}

export const config = {
matcher: '/product/:path*',
}

效果图

效果图

可以看到当访问 /product/detail 时,被重定向到 / 说明我们写的 middleware 生效了。

matcher

matcher 就是用来选中拦截的路由,只有一个时用字符串即可,就像上面的例子一样,如果是多个情况,也可以使用数组。还可以用()包裹正则表达式进行匹配。

官网提供了一个例子,匹配了除特定路径之外的所有路径。

1
2
3
4
5
6
7
8
9
10
11
12
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}

matcher 还可以是一个对象数组,通过对象属性进一步严格匹配,has 匹配有该属性和值,missing 匹配不存在该属性和值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export const config = {
matcher: [
{
source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
has: [
{ type: 'header', key: 'Authorization', value: 'Bearer Token' }
],
},
],
}

就像上面例子,missing 规定 header 不能存在 next-router-prefetchpurpose 值不为 prefetch,has 规定 headerAuthorization 的值需要为 Bearer Token 的。

middleware

匹配路由除了使用 config 的 matcher 还可以直接在 middleware 中进行判断,效果和上面是一样的。

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

export function middleware(request) {
if (request.nextUrl.pathname.startsWith('/product/')) {
return NextResponse.rewrite(new URL('/', request.url))
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function middleware(request) {
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

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: '/' }

return response
}

通过 request 的 get 和 getAll 方法读取 cookie 的值,通过 has 查看是否存在某值,通过 delete 方法删除 cookie 的值。

NextResponse.next() 执行该方法会返回一个新的 Response 对象,通过 set 方法设置 cookie 的值。

读取和设置 header

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { NextResponse } from 'next/server'

export function middleware(request) {
// 克隆请求头
const requestHeaders = new Headers(request.headers)
// 在请求前设置请求头
requestHeaders.set('x-hello-from-middleware1', 'hello')

// You can also set request headers in NextResponse.next
const response = NextResponse.next({
request: {
// New request headers
headers: requestHeaders,
},
})

// 设置响应头
response.headers.set('x-hello-from-middleware2', 'hello')
return response
}

响应头加上了 x-hello-from-middleware2

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) {
// 获取请求源
const origin = request.headers.get('origin') ?? ''
console.log("origin:", origin)
// 是否在允许源的白名单中
const isAllowedOrigin = allowedOrigins.includes(origin)

// 处理预请求
const isPreflight = request.method === 'OPTIONS'
if (isPreflight) {
const preflightHeaders = {
...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
...corsOptions,
}
return NextResponse.json({}, { headers: preflightHeaders })
}

// 处理简单请求
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*',
}

生成响应

通过 Response 或 NextResponse 直接返回响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {NextResponse} from "next/server";

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

export function middleware(request) {
// return Response.json(
// { success: false, message: 'authentication failed --Response' },
// { status: 401 }
// )
return new NextResponse(
JSON.stringify({ success: false, message: 'authentication failed --NextResponse' }),
{ status: 401 }
)
}

高级中间件标志

skipMiddlewareUrlNormalize

跳过尾部斜杠重定向

1
2
3
4
5
// next.config.js
module.exports = {
// 跳过尾部斜杠重定向
skipTrailingSlashRedirect: true,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// middleware.js
import {NextResponse} from "next/server";

const legacyPrefixes = ['/product']
export async function middleware(req) {
const { pathname } = req.nextUrl

if (legacyPrefixes.some((prefix) => pathname.startsWith(prefix))) {
return NextResponse.next()
}

// apply trailing slash handling
if (
!pathname.endsWith('/') &&
!pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)
) {
return NextResponse.redirect(
new URL(`${req.nextUrl.pathname}/`, req.nextUrl)
)
}
}

以上代码实现除了 /product 为前缀的路由外,其他路由加上尾斜杠

skipMiddlewareUrlNormalize

开启时在 middleware 中获取的 req.nextUrl.pathname 为原始的地址。

Runtime

middleware(截至目前 Next 最新版本为 14.2.5) 只支持 Edge runtime,不支持 node runtime,所以写 middleware 时尽量使用 web API 不要使用 node API。

路由段配置

路由段指的是斜杠分割的路径,比如 /api/user 中 / 是根段,api 是段 user 是叶段。

配置项

1
2
3
4
5
6
7
8
// layout.js | page.js | route.js
export const dynamic = 'auto'
export const dynamicParams = true
export const revalidate = false
export const fetchCache = 'auto'
export const runtime = 'nodejs'
export const preferredRegion = 'auto'
export const maxDuration = 5

dynamic

更改布局或者页面的动态行为

可选值:'auto'(默认) | 'force-dynamic' | 'error' | 'force-static'

  • auto: 自动判断
  • force-dynamic: 强制使用动态渲染,退出所有 fetch 请求缓存
  • error:强制静态渲染并且缓存数据,如果有组件使用动态函数或者不缓存数据请求,会报错
  • force-static:强制静态渲染并且缓存数据,同时 cookie() headers() useSearchParams() 返回空值。

dynamicParams

可选值:true(默认) | false

  • true: 按需生成
  • false: 返回 404

revalidate

设置重验证的时间间隔,单位为秒。此设置不会覆盖单个 fetch 请求设置的 revalidate 的值。

可选值:false | ‘force-cache’ | 0 | number

  • false: 不重新验证
  • 0:总是动态渲染
  • number:重新验证的时间间隔

fetchCache

可选值:'auto' | 'default-cache' | 'only-cache' | 'force-cache' | 'force-no-store' | 'default-no-store' | 'only-no-store'

  • auto: 自动判断
  • default-cache: 默认缓存
  • only-cache:如果开发者未设置 cache 选项,默认设置为 force-cache,如果有请求设置成 cache: ‘no-store’,则会导致报错
  • force-cache:将所有请求的 cache 选项设置为 force-cache 。
  • default-no-store:开发者可以自由设置 cache 选项,但如果开发者未设置 cache 选项,默认设置为 no-store,这意味着即使是在动态函数之前的请求,也会被视为动态。
  • only-no-store:如果开发者未设置 cache 选项,默认设置为 no-store,如果有请求设置成 cache: ‘force-cache’,则会导致报错
  • force-no-store:将所有请求的 cache 选项设置为 no-store 。

runtime

运行时环境设置
可选值:'edge' | 'nodejs'


Next 路由篇
https://l1ushun.github.io/2024/08/12/next-02/
作者
liu shun
发布于
2024年8月12日