Sponsor
ntab.devntab.dev 提升效率的新标签页组件
点击查看
Menu

Middleware

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

Middleware 在缓存内容和路由匹配之前运行。有关更多详细信息,请参阅匹配路径

使用场景

将 Middleware 集成到你的应用程序中可以带来显著的性能、安全性和用户体验的改进。Middleware 特别有效的一些常见场景包括:

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

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

  • 复杂的数据获取和操作:Middleware 不是为直接数据获取或操作而设计的,这应该在 Route Handlers 或服务器端实用程序中完成。
  • 繁重的计算任务:Middleware 应当轻量化并快速响应,否则可能导致页面加载延迟。繁重的计算任务或长时间运行的进程应该在专用的 Route Handlers 中完成。
  • 广泛的会话管理:虽然 Middleware 可以管理基本的会话任务,但广泛的会话管理应该由专用的身份验证服务或在 Route Handlers 中管理。
  • 直接数据库操作:不建议在 Middleware 中执行直接的数据库操作。数据库交互应该在 Route Handlers 或服务器端实用程序中完成。

约定

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

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

示例

middleware.ts
TypeScript
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 将被调用用于你项目中的每个路由。鉴于此,使用匹配器精确定位或排除特定路由至关重要。以下是执行顺序:

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

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

条件语句

middleware.ts
TypeScript
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))
  }
}

NextResponse

NextResponse API 允许你:

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

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

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

使用 Cookies

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

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

设置头信息

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

middleware.ts
TypeScript
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
}

值得注意的是:避免设置大型头信息,因为根据你的后端 Web 服务器配置,可能会导致 431 Request Header Fields Too Large 错误。

CORS

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

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) {
  // 检查请求中的源
  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 实例直接从 Middleware 响应。(这在 Next.js v13.1.0 之后可用)

middleware.ts
TypeScript
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 }
    )
  }
}

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
}

单元测试(实验性)

从 Next.js 15.1 开始,next/experimental/testing/server 包包含了帮助单元测试 middleware 文件的实用工具。单元测试 middleware 可以帮助确保它只在所需的路径上运行,并且自定义路由逻辑在代码到达生产环境之前按预期工作。

unstable_doesMiddlewareMatch 函数可用于断言 middleware 是否将为提供的 URL、头信息和 cookies 运行。

import { unstable_doesMiddlewareMatch } from 'next/experimental/testing/server'
 
expect(
  unstable_doesMiddlewareMatch({
    config,
    nextConfig,
    url: '/test',
  })
).toEqual(false)

整个 middleware 函数也可以被测试。

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

运行时

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

next.config.ts
TypeScript
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  experimental: {
    nodeMiddleware: true,
  },
}
 
export default nextConfig

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

middleware.ts
TypeScript
export const config = {
  runtime: 'nodejs',
}

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

版本历史

版本更改
v15.2.0Middleware 现在可以使用 Node.js 运行时(实验性)
v13.1.0添加了高级 Middleware 标志
v13.0.0Middleware 可以修改请求头、响应头并发送响应
v12.2.0Middleware 已稳定,请参阅升级指南
v12.0.9在 Edge Runtime 中强制使用绝对 URL(PR
v12.0.0添加了 Middleware(测试版)