跳到内容

代理

注意middleware 文件约定已弃用,并已更名为 proxy。有关更多详细信息,请参阅迁移到代理

proxy.js|ts 文件用于编写 代理 并在请求完成之前在服务器上运行代码。然后,根据传入请求,您可以通过重写、重定向、修改请求或响应头,或直接响应来修改响应。

代理在路由渲染之前执行。它对于实现自定义服务器端逻辑(如身份验证、日志记录或处理重定向)特别有用。

须知:

代理旨在独立于您的渲染代码调用,并在优化情况下部署到您的 CDN 以实现快速重定向/重写处理,您不应尝试依赖共享模块或全局变量。

要将信息从代理传递到您的应用程序,请使用请求头Cookie重写重定向或 URL。

在项目根目录或 src 目录中(如果适用)创建一个 proxy.ts(或 .js)文件,使其与 pagesapp 位于同一级别。

如果您自定义了pageExtensions,例如将其设为.page.ts.page.js,请相应地将您的文件命名为 proxy.page.tsproxy.page.js

proxy.ts
import { NextResponse, NextRequest } from 'next/server'
 
// This function can be marked `async` if using `await` inside
export function proxy(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}
 
export const config = {
  matcher: '/about/:path*',
}

导出

代理函数

文件必须导出一个单独的函数,无论是作为默认导出还是命名为 proxy。请注意,同一文件中的多个代理不受支持。

proxy.js
// Example of default export
export default function proxy(request) {
  // Proxy logic
}

配置对象(可选)

可选地,可以与代理函数一起导出一个配置对象。此对象包含 匹配器,用于指定代理适用的路径。

匹配器

matcher 选项允许您指定代理运行的特定路径。您可以通过以下几种方式指定这些路径:

  • 对于单个路径:直接使用字符串定义路径,例如 '/about'
  • 对于多个路径:使用数组列出多个路径,例如 matcher: ['/about', '/contact'],它会将代理应用于 /about/contact
proxy.js
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}

此外,matcher 选项支持通过正则表达式进行复杂的路径规范,例如 matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],从而精确控制要包含或排除的路径。

matcher 选项接受一个包含以下键的对象数组:

  • source:用于匹配请求路径的路径或模式。它可以是用于直接路径匹配的字符串,也可以是用于更复杂匹配的模式。
  • regexp(可选):一个正则表达式字符串,用于根据源对匹配进行微调。它提供了对包含或排除哪些路径的额外控制。
  • locale(可选):一个布尔值,当设置为 false 时,会在路径匹配中忽略基于语言环境的路由。
  • has(可选):根据特定请求元素(如请求头、查询参数或 Cookie)的存在来指定条件。
  • missing(可选):侧重于缺少某些请求元素(如缺少请求头或 Cookie)的条件。
proxy.js
export const config = {
  matcher: [
    {
      source: '/api/*',
      regexp: '^/api/(.*)',
      locale: false,
      has: [
        { type: 'header', key: 'Authorization', value: 'Bearer Token' },
        { type: 'query', key: 'userId', value: '123' },
      ],
      missing: [{ type: 'cookie', key: 'session', value: 'active' }],
    },
  ],
}

配置的匹配器

  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 文档的更多详细信息。

须知:

  • matcher 值必须是常量,以便在构建时进行静态分析。变量等动态值将被忽略。
  • 为了向后兼容,Next.js 始终将 /public 视为 /public/index。因此,/public/:path 的匹配器将匹配。

参数

请求

定义代理时,默认导出函数接受一个参数 request。此参数是 NextRequest 的实例,它表示传入的 HTTP 请求。

proxy.ts
import type { NextRequest } from 'next/server'
 
export function proxy(request: NextRequest) {
  // Proxy logic goes here
}

须知:

  • NextRequest 是一种表示 Next.js 代理中传入 HTTP 请求的类型,而 NextResponse 是一个用于操作和发送 HTTP 响应的类。

NextResponse

NextResponse API 允许您:

  • 将传入请求 redirect(重定向)到不同的 URL
  • 通过显示给定 URL 来 rewrite(重写)响应
  • 为 API 路由、getServerSidePropsrewrite 目的地设置请求头
  • 设置响应 Cookie
  • 设置响应头

要从代理生成响应,您可以:

  1. rewrite(重写)到一个生成响应的路由(页面Edge API 路由
  2. 直接返回 NextResponse。请参阅生成响应

执行顺序

代理将为**项目中的每个路由**调用。因此,使用 匹配器 精确地定位或排除特定路由至关重要。以下是执行顺序:

  1. next.config.js 中的 headers
  2. next.config.js 中的 redirects
  3. 代理(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

运行时

代理默认为使用 Node.js 运行时。runtime 配置选项 在代理文件中不可用。在代理中设置 runtime 配置选项将引发错误。

高级代理标志

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

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

next.config.js
module.exports = {
  skipTrailingSlashRedirect: true,
}
proxy.js
const legacyPrefixes = ['/docs', '/blog']
 
export default async function proxy(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)
    )
  }
}

skipMiddlewareUrlNormalize 允许禁用 Next.js 中的 URL 规范化,以使直接访问和客户端转换相同。在某些高级情况下,此选项通过使用原始 URL 提供完全控制。

next.config.js
module.exports = {
  skipMiddlewareUrlNormalize: true,
}
proxy.js
export default async function proxy(req) {
  const { pathname } = req.nextUrl
 
  // GET /_next/data/build-id/hello.json
 
  console.log(pathname)
  // with the flag this now /_next/data/build-id/hello.json
  // without the flag this would be normalized to /hello
}

示例

条件语句

proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function proxy(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))
  }
}

使用 Cookie

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

  1. 对于传入请求,cookies 具有以下方法:getgetAllsetdelete Cookie。您可以使用 has 检查 Cookie 是否存在,或使用 clear 删除所有 Cookie。
  2. 对于传出响应,cookies 具有以下方法:getgetAllsetdelete
proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function proxy(request: NextRequest) {
  // Assume a "Cookie:nextjs=fast" header to be present on the incoming request
  // Getting cookies from the request using the `RequestCookies` API
  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
 
  // Setting cookies on the response using the `ResponseCookies` API
  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: '/' }
  // The outgoing response will have a `Set-Cookie:vercel=fast;path=/` header.
 
  return response
}

设置请求头

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

proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function proxy(request: NextRequest) {
  // Clone the request headers and set a new header `x-hello-from-proxy1`
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-hello-from-proxy1', 'hello')
 
  // You can also set request headers in NextResponse.next
  const response = NextResponse.next({
    request: {
      // New request headers
      headers: requestHeaders,
    },
  })
 
  // Set a new response header `x-hello-from-proxy2`
  response.headers.set('x-hello-from-proxy2', 'hello')
  return response
}

请注意,此代码片段使用

  • NextResponse.next({ request: { headers: requestHeaders } }) 使 requestHeaders 可用于上游
  • 而不是 NextResponse.next({ headers: requestHeaders }),后者使 requestHeaders 可用于客户端

代理中的 NextResponse 请求头中了解更多。

须知:避免设置过大的请求头,否则可能会导致 431 请求头字段过大 错误,具体取决于您的后端 Web 服务器配置。

CORS

您可以在代理中设置 CORS 头部以允许跨域请求,包括简单请求预检请求

proxy.ts
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 proxy(request: NextRequest) {
  // Check the origin from the request
  const origin = request.headers.get('origin') ?? ''
  const isAllowedOrigin = allowedOrigins.includes(origin)
 
  // Handle preflighted requests
  const isPreflight = request.method === 'OPTIONS'
 
  if (isPreflight) {
    const preflightHeaders = {
      ...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
      ...corsOptions,
    }
    return NextResponse.json({}, { headers: preflightHeaders })
  }
 
  // Handle simple requests
  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*',
}

生成响应

您可以通过返回 ResponseNextResponse 实例直接从代理响应。(此功能自 Next.js v13.1.0 起可用)

proxy.ts
import type { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'
 
// Limit the proxy to paths starting with `/api/`
export const config = {
  matcher: '/api/:function*',
}
 
export function proxy(request: NextRequest) {
  // Call our authentication function to check the request
  if (!isAuthenticated(request)) {
    // Respond with JSON indicating an error message
    return Response.json(
      { success: false, message: 'authentication failed' },
      { status: 401 }
    )
  }
}

负匹配

matcher 配置允许完整的正则表达式,因此支持负向先行断言或字符匹配等匹配。负向先行断言匹配所有路径但排除特定路径的示例可见此处

proxy.js
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, sitemap.xml, robots.txt (metadata files)
     */
    '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
  ],
}

您还可以通过使用 missinghas 数组,或两者的组合来绕过某些请求的代理

proxy.js
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, sitemap.xml, robots.txt (metadata files)
     */
    {
      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' }],
    },
  ],
}

waitUntilNextFetchEvent

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

waitUntil() 方法接受一个 Promise 作为参数,并延长代理的生命周期直到 Promise 解决。这对于在后台执行工作很有用。

proxy.ts
import { NextResponse } from 'next/server'
import type { NextFetchEvent, NextRequest } from 'next/server'
 
export function proxy(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 15.1 开始,next/experimental/testing/server 包包含用于帮助单元测试代理文件的实用程序。单元测试代理有助于确保它仅在所需的路径上运行,并且自定义路由逻辑在代码投入生产之前按预期工作。

unstable_doesProxyMatch 函数可用于断言代理是否将针对提供的 URL、请求头和 Cookie 运行。

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

整个代理函数也可以进行测试。

import { isRewrite, getRewrittenUrl } from 'next/experimental/testing/server'
 
const request = new NextRequest('https://nextjs.net.cn/docs')
const response = await proxy(request)
expect(isRewrite(response)).toEqual(true)
expect(getRewrittenUrl(response)).toEqual('https://other-domain.com/docs')
// getRedirectUrl could also be used if the response were a redirect

平台支持

部署选项支持
Node.js 服务器
Docker 容器
静态导出
适配器平台特定

了解如何配置代理,当自托管 Next.js 时。

迁移到代理

为什么会更改

middleware 更名的原因是,“中间件”一词常常与 Express.js 中间件混淆,导致对其用途的误解。此外,中间件功能强大,可能会鼓励过度使用;然而,建议将此功能作为最后的手段。

Next.js 正在向前发展,提供具有更好人体工程学的 API,以便开发人员无需中间件即可实现其目标。这就是 middleware 更名的原因。

为什么叫“代理”

“代理”这个名称澄清了中间件的功能。“代理”一词暗示应用程序前面有一个网络边界,这正是中间件的行为。此外,中间件默认在Edge Runtime上运行,可以更接近客户端运行,与应用程序区域分离。这些行为与“代理”一词更吻合,并提供了该功能更清晰的用途。

如何迁移

我们建议用户除非别无选择,否则避免依赖中间件。我们的目标是为他们提供具有更好人体工程学的 API,以便他们可以在没有中间件的情况下实现其目标。

“中间件”一词经常让用户与 Express.js 中间件混淆,这可能会鼓励滥用。为了明确我们的方向,我们将文件约定重命名为“proxy”。这突出表明我们正在摆脱中间件,打破其过载的功能,并使代理的用途明确。

Next.js 提供了一个 codemod,可以将 middleware.ts 迁移到 proxy.ts。您可以运行以下命令进行迁移:

npx @next/codemod@canary middleware-to-proxy .

codemod 会将文件名和函数名从 middleware 重命名为 proxy

// middleware.ts -> proxy.ts
 
- export function middleware() {
+ export function proxy() {

版本历史

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