Menu

中间件(Middleware)

Middleware 允许你在请求完成之前运行代码。然后,基于传入的请求,你可以通过重写、重定向、修改请求或响应 header,或直接响应来修改响应。

Middleware 在缓存内容和路由匹配之前运行。更多详情请参阅 匹配路径

使用场景

将 Middleware 集成到你的应用程序中可以显著提升性能、安全性和用户体验。以下是 Middleware 特别有效的一些常见场景:

  • 身份验证和授权:在授予对特定页面或 API 路由的访问权限之前,确认用户身份并检查会话 cookie。
  • 服务器端重定向:基于某些条件 (例如:地区、用户角色) 在服务器级别重定向用户。
  • 路径重写:根据请求属性动态重写 API 路由或页面的路径,支持 A/B 测试、功能发布或保留旧路径。
  • 机器人检测:通过检测和阻止机器人流量来保护你的资源。
  • 日志和分析:在页面或 API 处理之前捕获和分析请求数据以获得洞察。
  • 功能标记:动态启用或禁用功能,实现无缝功能发布或测试。

认识到 middleware 可能不是最佳方案的情况同样重要。以下是需要注意的一些场景:

  • 复杂的数据获取和处理:Middleware 不适合直接获取或处理数据,这些应该在 Route Handler 或服务器端工具中完成。
  • 重计算任务:Middleware 应该轻量且快速响应,否则会导致页面加载延迟。重计算任务或长时间运行的进程应该在专门的 Route Handler 中完成。
  • 大量会话管理:虽然 Middleware 可以管理基本的会话任务,但大量的会话管理应该由专门的认证服务或在 Route Handler 中管理。
  • 直接数据库操作:不建议在 Middleware 中执行直接的数据库操作。数据库交互应该在 Route Handler 或服务器端工具中完成。

约定

在项目根目录使用 middleware.ts (或 .js) 文件来定义 Middleware。例如,与 pagesapp 目录同级,或者如果适用的话放在 src 目录内。

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

示例

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*',
}
middleware.js
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 中的 beforeFiles (rewrites)
  5. 文件系统路由 (public/_next/static/pages/app/ 等)
  6. next.config.js 中的 afterFiles (rewrites)
  7. 动态路由 (/blog/[slug])
  8. next.config.js 中的 fallback (rewrites)

有两种方式来定义 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 的 matcher 将匹配。

条件语句

middleware.ts
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))
  }
}
middleware.js
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
  • rewrite 通过显示给定的 URL 来重写响应
  • 为 API 路由、getServerSidePropsrewrite 目标设置请求 header
  • 设置响应 cookie
  • 设置响应 header

要从 Middleware 产生响应,你可以:

  1. rewrite 到一个产生响应的路由 (页面路由处理程序)
  2. 直接返回一个 NextResponse。参见 产生响应

使用 Cookies

Cookie 是常规的 header。在 Request 中,它们存储在 Cookie header 中。在 Response 中,它们在 Set-Cookie header 中。Next.js 通过 NextRequestNextResponse 上的 cookies 扩展提供了一种便捷的方式来访问和操作这些 cookie。

  1. 对于传入的请求,cookies 提供以下方法:getgetAllsetdelete cookie。你可以用 has 检查 cookie 是否存在,或用 clear 删除所有 cookie。
  2. 对于传出的响应,cookies 有以下方法:getgetAllsetdelete
middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  // 假设传入请求中存在 "Cookie:nextjs=fast" header
  // 使用 `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=/` header
 
  return response
}
middleware.js
import { NextResponse } from 'next/server'
 
export function middleware(request) {
  // 假设传入请求中存在 "Cookie:nextjs=fast" header
  // 使用 `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` header
 
  return response
}

设置 Headers

你可以使用 NextResponse API 设置请求和响应 header (设置请求 header 从 Next.js v13.0.0 开始可用)。

middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  // 克隆请求 header 并设置新的 header `x-hello-from-middleware1`
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-hello-from-middleware1', 'hello')
 
  // 你也可以在 NextResponse.next 中设置请求 header
  const response = NextResponse.next({
    request: {
      // 新的请求 header
      headers: requestHeaders,
    },
  })
 
  // 设置新的响应 header `x-hello-from-middleware2`
  response.headers.set('x-hello-from-middleware2', 'hello')
  return response
}
middleware.js
import { NextResponse } from 'next/server'
 
export function middleware(request) {
  // 克隆请求 header 并设置新的 header `x-hello-from-middleware1`
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-hello-from-middleware1', 'hello')
 
  // 你也可以在 NextResponse.next 中设置请求 header
  const response = NextResponse.next({
    request: {
      // 新的请求 header
      headers: requestHeaders,
    },
  })
 
  // 设置新的响应 header `x-hello-from-middleware2`
  response.headers.set('x-hello-from-middleware2', 'hello')
  return response
}

值得注意的是:避免设置大型 header,因为这可能会导致 431 请求 Header 字段过大 错误,具体取决于你的后端 web 服务器配置。

CORS

你可以在 Middleware 中设置 CORS header 以允许跨源请求,包括 简单请求预检请求

middleware.ts
TypeScript
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) {
  // 检查请求中的 origin
  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 header。

产生响应

你可以通过返回一个 ResponseNextResponse 实例从 Middleware 直接响应。(这个功能从 Next.js v13.1.0 开始可用)

middleware.ts
import type { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'
 
// 将 middleware 限制在以 `/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 }
    )
  }
}
middleware.js
import { isAuthenticated } from '@lib/auth'
 
// 将 middleware 限制在以 `/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 作为参数,并延长 Middleware 的生命周期直到 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()
}

高级 Middleware 标志

在 Next.js 的 v13.1 中引入了两个额外的 middleware 标志,skipMiddlewareUrlNormalizeskipTrailingSlashRedirect,用于处理高级用例。

skipTrailingSlashRedirect 禁用 Next.js 添加或删除尾部斜杠的重定向。这允许在 middleware 中自定义处理,为某些路径保留尾部斜杠但其他路径不保留,这可以使增量迁移更容易。

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)
  // 有标志时现在是 /_next/data/build-id/hello.json
  // 没有标志时会被规范化为 /hello
}

运行时

Middleware 目前只支持与 Edge 运行时 兼容的 API。Node.js 独有的 API 不支持

版本历史

版本变更
v13.1.0添加高级 Middleware 标志
v13.0.0Middleware 可以修改请求 header、响应 header,并发送响应
v12.2.0Middleware 稳定版,请参阅 升级指南
v12.0.9在 Edge Runtime 中强制使用绝对 URL (PR)
v12.0.0添加 Middleware (测试版)