Middleware

Middleware 允许在请求完成之前执行代码,然后,基于即将到来的请求,你可以修改 response、重定向、修改 request 或者 response headers,或者直接响应。

中间件在缓存内容和路由匹配之前运行。

约定

在项目的根目录下使用middleware.ts来定义中间件。需要跟 app 或者 pages 同一层级。

例子

js
1// middleware.ts
2import { NextResponse } from 'next/server'
3import type { NextRequest } from 'next/server'
4
5// This function can be marked `async` if using `await` inside
6export function middleware(request: NextRequest) {
7  return NextResponse.redirect(new URL('/home', request.url))
8}
9
10// See "Matching Paths" below to learn more
11export const config = {
12  matcher: '/about/:path*',
13}

匹配 path

有两种方式可以规定哪些 paths 会调用 Middleware:

  1. Matcher
  2. Conditional statements

Matcher

使用 Matcher 让 Middleware 只在指定的 path 执行:

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

你可以指定单个或者多个 path:

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

matcher 设置允许使用正则表达式匹配:

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

matcher 值必须是常量,这样才能在构建时对其进行静态分析

配置 matcher 的规则:

  1. 必须以/开头
  2. 可以包含参数,比如:/about/:path会匹配/about/a和/about/b但是不匹配/about/a/c
  3. 可以用修饰符,比如/about/:path*匹配/about/a/b/c,*表示 0 或更多,?表示0或 1,+表示 1 或更多。
  4. 可以用括号包住常用的正则表达式来匹配,/about/(.*)的效果跟/about/:path*是一样的。

条件声明

js
1import { NextResponse } from 'next/server'
2import type { NextRequest } from 'next/server'
3
4export function middleware(request: NextRequest) {
5  if (request.nextUrl.pathname.startsWith('/about')) {
6    return NextResponse.rewrite(new URL('/about-2', request.url))
7  }
8
9  if (request.nextUrl.pathname.startsWith('/dashboard')) {
10    return NextResponse.rewrite(new URL('/dashboard/user', request.url))
11  }
12}

NextResponse

NextResponse允许我们做:

  1. 重定向
  2. 重写 response
  3. 设置 request headers
  4. 设置 response cookies
  5. 设置 response headers

要从中间件生成 response,你可以:

  1. 重写一个生成响应的路由(page 或 路由 handler)
  2. 直接 return NextResponse

使用 cookies

Cookies 是常用的 header。在 Request 中,他们被存到 header 的 Cookie 字段,在 Response 中,他们在Set-Cookie中。

Nextjs 提供了一个方便的方法,可以通过 NextRequest 和 NextResponse 上的 cookie 扩展来访问和操作这些 cookie。

  1. 对于 requests,cookie 具有以下方法:get、getAll、set 和 delete cookie。您可以使用 has 检查是否存在 cookie,也可以使用 clear 删除所有 cookie。
  2. 对于响应,cookies 也有下面的方法:get, getAll, set, and delete.
js
1import { NextResponse } from 'next/server'
2import type { NextRequest } from 'next/server'
3
4export function middleware(request: NextRequest) {
5  // Assume a "Cookie:nextjs=fast" header to be present on the incoming request
6  // Getting cookies from the request using the `RequestCookies` API
7  let cookie = request.cookies.get('nextjs')
8  console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
9  const allCookies = request.cookies.getAll()
10  console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]
11
12  request.cookies.has('nextjs') // => true
13  request.cookies.delete('nextjs')
14  request.cookies.has('nextjs') // => false
15
16  // Setting cookies on the response using the `ResponseCookies` API
17  const response = NextResponse.next()
18  response.cookies.set('vercel', 'fast')
19  response.cookies.set({
20    name: 'vercel',
21    value: 'fast',
22    path: '/',
23  })
24  cookie = response.cookies.get('vercel')
25  console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
26  // The outgoing response will have a `Set-Cookie:vercel=fast;path=/test` header.
27
28  return response
29}

设置 Headers

你可以使用NextResponse来设置 request 或者 response 的 headers。(自 13.0.0 开始,支持设置 request headers)。

js
1import { NextResponse } from 'next/server'
2import type { NextRequest } from 'next/server'
3
4export function middleware(request: NextRequest) {
5  // Clone the request headers and set a new header `x-hello-from-middleware1`
6  const requestHeaders = new Headers(request.headers)
7  requestHeaders.set('x-hello-from-middleware1', 'hello')
8
9  // You can also set request headers in NextResponse.rewrite
10  const response = NextResponse.next({
11    request: {
12      // New request headers
13      headers: requestHeaders,
14    },
15  })
16
17  // Set a new response header `x-hello-from-middleware2`
18  response.headers.set('x-hello-from-middleware2', 'hello')
19  return response
20}

生成 Response

你可以直接通过中间件来返回response,或者返回NextResponse实例。

js
1import { NextRequest, NextResponse } from 'next/server'
2import { isAuthenticated } from '@lib/auth'
3
4// Limit the middleware to paths starting with `/api/`
5export const config = {
6  matcher: '/api/:function*',
7}
8
9export function middleware(request: NextRequest) {
10  // Call our authentication function to check the request
11  if (!isAuthenticated(request)) {
12    // Respond with JSON indicating an error message
13    return new NextResponse(JSON.stringify({ success: false, message: 'authentication failed' }), {
14      status: 401,
15      headers: { 'content-type': 'application/json' },
16    })
17  }
18}

高级中间件标志

在 Next.js 的 v13.1 版本中,引入了两个额外的中间件标志,分别是 skipMiddlewareUrlNormalize 和 skipTrailingSlashRedirect,用于处理高级用例。

skipTrailingSlashRedirect 允许禁用 Next.js 的默认重定向,以便在中间件内部进行自定义处理,从而允许保留一些路径的尾随斜杠,而对其他路径不做处理,使得渐进式迁移变得更加容易。

js
1module.exports = {
2  skipTrailingSlashRedirect: true,
3}
js
1const legacyPrefixes = ['/docs', '/blog']
2
3export default async function middleware(req) {
4  const { pathname } = req.nextUrl
5
6  if (legacyPrefixes.some((prefix) => pathname.startsWith(prefix))) {
7    return NextResponse.next()
8  }
9
10  // apply trailing slash handling
11  if (!pathname.endsWith('/') && !pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)) {
12    req.nextUrl.pathname += '/'
13    return NextResponse.redirect(req.nextUrl)
14  }
15}

skipMiddlewareUrlNormalize 允许禁用 Next.js 对 URL 进行的规范化处理,以便处理直接访问和客户端过渡的方式保持一致。在一些高级情况下,你可能需要完全控制使用原始 URL,而这个功能可以实现这一点。

js
1module.exports = {
2  skipMiddlewareUrlNormalize: true,
3}
js
1export default async function middleware(req) {
2  const { pathname } = req.nextUrl
3
4  // GET /_next/data/build-id/hello.json
5
6  console.log(pathname)
7  // with the flag this now /_next/data/build-id/hello.json
8  // without the flag this would be normalized to /hello
9}