中间件 (Middleware)

中间件 (Middleware) 允许您在请求完成前运行代码。然后根据传入的请求,您可以通过重写、重定向、修改请求或响应头,或直接响应来修改响应。

中间件 (Middleware) 会在缓存内容和路由匹配之前运行。详见路径匹配

使用场景

中间件 (Middleware) 特别适用的常见场景包括:

  • 读取部分传入请求后快速重定向
  • 基于 A/B 测试或实验重写到不同页面
  • 修改所有页面或部分页面的头部信息

中间件 (Middleware) 适合以下场景:

  • 缓慢的数据获取
  • 会话管理

约定

在项目根目录下使用 middleware.ts(或 .js)文件定义中间件 (Middleware)。例如,与 pagesapp 同级,或在 src 目录内(如适用)。

注意:虽然每个项目仅支持一个 middleware.ts 文件,但您仍可以模块化组织中间件逻辑。将中间件功能拆分为单独的 .ts.js 文件,然后导入到主 middleware.ts 文件中。这样可以更清晰地管理特定路由的中间件,同时在 middleware.ts 中集中控制。通过强制使用单一中间件文件,可以简化配置、避免潜在冲突,并通过避免多层中间件来优化性能。

示例

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// 如果内部使用 `await`,此函数可标记为 `async`
export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}

// 详见下方的"路径匹配"部分
export const config = {
  matcher: '/about/:path*',
}
import { NextResponse } from 'next/server'

// 如果内部使用 `await`,此函数可标记为 `async`
export function middleware(request) {
  return NextResponse.redirect(new URL('/home', request.url))
}

// 详见下方的"路径匹配"部分
export const config = {
  matcher: '/about/:path*',
}

路径匹配

中间件 (Middleware) 会为项目中的每个路由调用。因此,使用匹配器 (matcher) 精确指定或排除特定路由至关重要。以下是执行顺序:

  1. next.config.js 中的 headers
  2. next.config.js 中的 redirects
  3. 中间件 (Middleware)(rewritesredirects 等)
  4. next.config.js 中的 beforeFilesrewrites
  5. 文件系统路由(public/_next/static/pages/app/ 等)
  6. next.config.js 中的 afterFilesrewrites
  7. 动态路由(/blog/[slug]
  8. next.config.js 中的 fallbackrewrites

有两种方式定义中间件 (Middleware) 运行的路径:

  1. 自定义匹配器配置
  2. 条件语句

匹配器 (Matcher)

matcher 允许您筛选中间件 (Middleware) 在特定路径上运行。

middleware.js
export const config = {
  matcher: '/about/:path*',
}

您可以使用数组语法匹配单个路径或多个路径:

middleware.js
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}

matcher 配置支持完整正则表达式,因此支持负向先行断言或字符匹配等操作。以下是匹配除特定路径外的所有路径的负向先行断言示例:

middleware.js
export const config = {
  matcher: [
    /*
     * 匹配所有请求路径,除了以以下内容开头的路径:
     * - api(API 路由)
     * - _next/static(静态文件)
     * - _next/image(图片优化文件)
     * - favicon.ico、sitemap.xml、robots.txt(元数据文件)
     */
    '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
  ],
}

您还可以使用 missinghas 数组,或两者的组合,来绕过某些请求的中间件 (Middleware):

middleware.js
export const config = {
  matcher: [
    /*
     * 匹配所有请求路径,除了以以下内容开头的路径:
     * - api(API 路由)
     * - _next/static(静态文件)
     * - _next/image(图片优化文件)
     * - favicon.ico、sitemap.xml、robots.txt(元数据文件)
     */
    {
      source:
        '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
      missing: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },

    {
      source:
        '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
      has: [
        { type: 'header', key: 'next-router-prefetch' },
        { type: 'header', key: 'purpose', value: 'prefetch' },
      ],
    },

    {
      source:
        '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
      has: [{ type: 'header', key: 'x-present' }],
      missing: [{ type: 'header', key: 'x-missing', value: 'prefetch' }],
    },
  ],
}

须知matcher 值必须是常量,以便在构建时进行静态分析。动态值(如变量)将被忽略。

配置的匹配器:

  1. 必须以 / 开头
  2. 可以包含命名参数:/about/:path 匹配 /about/a/about/b,但不匹配 /about/a/c
  3. 可以在命名参数上使用修饰符(以 : 开头):/about/:path* 匹配 /about/a/b/c,因为 * 表示 零个或多个? 表示 零个或一个+ 表示 一个或多个
  4. 可以使用括号括起来的正则表达式:/about/(.*) 等同于 /about/:path*

更多详情请参阅 path-to-regexp 文档。

须知:为了向后兼容,Next.js 始终将 /public 视为 /public/index。因此,/public/:path 的匹配器会匹配。

条件语句

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  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))
  }
}
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))
  }
}

NextResponse

NextResponse API 允许您:

  • 将传入请求 redirect 到不同的 URL
  • 通过显示给定 URL 来 rewrite 响应
  • 为 API 路由、getServerSidePropsrewrite 目标设置请求头
  • 设置响应 cookie
  • 设置响应头

要从中间件 (Middleware) 生成响应,您可以:

  1. rewrite 到生成响应的路由(页面路由处理器
  2. 直接返回 NextResponse。参见生成响应

Cookie 是常规的头部信息。在 Request 中,它们存储在 Cookie 头中。在 Response 中,它们存储在 Set-Cookie 头中。Next.js 通过 NextRequestNextResponse 上的 cookies 扩展提供了便捷的访问和操作方式。

  1. 对于传入请求,cookies 提供以下方法:getgetAllsetdelete cookie。您可以使用 has 检查 cookie 是否存在,或使用 clear 删除所有 cookie。
  2. 对于传出响应,cookies 提供以下方法:getgetAllsetdelete
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // 假设传入请求中存在 "Cookie:nextjs=fast" 头
  // 使用 `RequestCookies` API 从请求中获取 cookie
  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

  // 使用 `ResponseCookies` API 在响应上设置 cookie
  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: '/' }
  // 传出响应将包含 `Set-Cookie:vercel=fast;path=/` 头。

  return response
}
import { NextResponse } from 'next/server'

export function middleware(request) {
  // 假设传入请求中存在 "Cookie:nextjs=fast" 头
  // 使用 `RequestCookies` API 从请求中获取 cookie
  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

  // 使用 `ResponseCookies` API 在响应上设置 cookie
  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: '/' }
  // 传出响应将包含 `Set-Cookie:vercel=fast;path=/test` 头。

  return response
}

设置头部

您可以使用 NextResponse API 设置请求和响应头(自 Next.js v13.0.0 起支持设置 请求 头)。

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

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

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

  // 设置新响应头 `x-hello-from-middleware2`
  response.headers.set('x-hello-from-middleware2', 'hello')
  return response
}
import { NextResponse } from 'next/server'

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

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

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

须知:避免设置过大的头部,因为这可能会根据后端 Web 服务器配置导致 431 请求头字段过大 错误。

CORS

您可以在中间件 (Middleware) 中设置 CORS 头以允许跨域请求,包括简单请求预检请求

import { NextRequest, 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: NextRequest) {
  // 检查请求中的来源
  const origin = request.headers.get('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*',
}
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') ?? ''
  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*',
}

须知:您可以在路由处理器中为单个路由配置 CORS 头。

生成响应

您可以直接从中间件返回一个 ResponseNextResponse 实例来生成响应。(此功能自 Next.js v13.1.0 起可用)

import type { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'

// 将中间件限制为以 `/api/` 开头的路径
export const config = {
  matcher: '/api/:function*',
}

export function middleware(request: NextRequest) {
  // 调用身份验证函数检查请求
  if (!isAuthenticated(request)) {
    // 返回包含错误信息的 JSON 响应
    return Response.json(
      { success: false, message: 'authentication failed' },
      { status: 401 }
    )
  }
}
import { isAuthenticated } from '@lib/auth'

// 将中间件限制为以 `/api/` 开头的路径
export const config = {
  matcher: '/api/:function*',
}

export function middleware(request) {
  // 调用身份验证函数检查请求
  if (!isAuthenticated(request)) {
    // 返回包含错误信息的 JSON 响应
    return Response.json(
      { success: false, message: 'authentication failed' },
      { status: 401 }
    )
  }
}

waitUntilNextFetchEvent

NextFetchEvent 对象扩展了原生的 FetchEvent 对象,并包含 waitUntil() 方法。

waitUntil() 方法接收一个 Promise 作为参数,并延长中间件的生命周期直到该 Promise 完成。这对于在后台执行工作非常有用。

middleware.ts
import { NextResponse } from 'next/server'
import type { NextFetchEvent, NextRequest } from 'next/server'

export function middleware(req: NextRequest, event: NextFetchEvent) {
  event.waitUntil(
    fetch('https://my-analytics-platform.com', {
      method: 'POST',
      body: JSON.stringify({ pathname: req.nextUrl.pathname }),
    })
  )

  return NextResponse.next()
}

高级中间件标志

在 Next.js 的 v13.1 版本中,引入了两个额外的中间件标志 skipMiddlewareUrlNormalizeskipTrailingSlashRedirect 来处理高级用例。

skipTrailingSlashRedirect 禁用了 Next.js 用于添加或移除尾部斜杠的重定向。这允许在中间件内进行自定义处理,为某些路径保留尾部斜杠而不为其他路径保留,从而使得增量迁移更加容易。

next.config.js
module.exports = {
  skipTrailingSlashRedirect: true,
}
middleware.js
const legacyPrefixes = ['/docs', '/blog']

export default async function middleware(req) {
  const { pathname } = req.nextUrl

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

  // 应用尾部斜杠处理
  if (
    !pathname.endsWith('/') &&
    !pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)
  ) {
    return NextResponse.redirect(
      new URL(`${req.nextUrl.pathname}/`, req.nextUrl)
    )
  }
}

skipMiddlewareUrlNormalize 允许禁用 Next.js 中的 URL 规范化,使得直接访问和客户端跳转的处理方式一致。在某些高级用例中,此选项通过使用原始 URL 提供了完全控制。

next.config.js
module.exports = {
  skipMiddlewareUrlNormalize: true,
}
middleware.js
export default async function middleware(req) {
  const { pathname } = req.nextUrl

  // GET /_next/data/build-id/hello.json

  console.log(pathname)
  // 启用标志后,pathname 为 /_next/data/build-id/hello.json
  // 未启用标志时,pathname 会被规范化为 /hello
}

单元测试(实验性功能)

从 Next.js 15.1 开始,next/experimental/testing/server 包提供了用于单元测试中间件文件的工具。单元测试中间件有助于确保它仅在所需的路径上运行,并且自定义的路由逻辑在生产环境之前按预期工作。

unstable_doesMiddlewareMatch 函数可用于断言中间件是否会针对提供的 URL、标头和 Cookie 运行。

import { unstable_doesMiddlewareMatch } from 'next/experimental/testing/server'

expect(
  unstable_doesMiddlewareMatch({
    config,
    nextConfig,
    url: '/test',
  })
).toEqual(false)

也可以测试整个中间件函数。

import { isRewrite, getRewrittenUrl } from 'next/experimental/testing/server'

const request = new NextRequest('https://nextjs.org/docs')
const response = await middleware(request)
expect(isRewrite(response)).toEqual(true)
expect(getRewrittenUrl(response)).toEqual('https://other-domain.com/docs')
// 如果响应是重定向,也可以使用 getRedirectUrl

运行时

中间件默认使用 Edge 运行时。从 v15.2(canary 版本)开始,我们实验性地支持使用 Node.js 运行时。要启用此功能,请在 next.config 文件中添加标志:

import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    nodeMiddleware: true,
  },
}

export default nextConfig
const nextConfig = {
  experimental: {
    nodeMiddleware: true,
  },
}

export default nextConfig

然后在中间件文件中,将 config 对象中的运行时设置为 nodejs

export const config = {
  runtime: 'nodejs',
}
export const config = {
  runtime: 'nodejs',
}

注意:此功能尚不建议在生产环境中使用。因此,除非您使用的是 next@canary 版本而非稳定版本,否则 Next.js 会抛出错误。

平台支持

部署选项是否支持
Node.js 服务器
Docker 容器
静态导出
适配器取决于具体平台

了解如何配置中间件以自托管 Next.js。

版本历史

版本变更
v15.2.0中间件现在可以使用 Node.js 运行时(实验性功能)
v13.1.0新增高级中间件标志
v13.0.0中间件可以修改请求标头、响应标头并发送响应
v12.2.0中间件功能稳定,请参阅升级指南
v12.0.9在 Edge 运行时中强制使用绝对 URL (PR)
v12.0.0新增中间件(Beta 版)