proxy.js
注意:
middleware文件约定已废弃并已重命名为proxy。有关更多详细信息,请参阅迁移到代理。
proxy.js|ts 文件用于编写 代理 并在请求完成之前在服务器上运行代码。然后,根据传入请求,您可以通过重写、重定向、修改请求或响应头,或直接响应来修改响应。
代理在路由渲染之前执行。它对于实现自定义服务器端逻辑(如身份验证、日志记录或处理重定向)特别有用。
须知:
代理旨在独立于您的渲染代码调用,并且在优化情况下部署到您的 CDN 以实现快速重定向/重写处理,您不应尝试依赖共享模块或全局变量。
在项目根目录或 src 目录中(如果适用)创建一个 proxy.ts(或 .js)文件,使其与 pages 或 app 位于同一级别。
如果您已自定义 pageExtensions,例如将其设置为 .page.ts 或 .page.js,则相应地将您的文件命名为 proxy.page.ts 或 proxy.page.js。
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。请注意,不支持来自同一文件的多个代理。
// Example of default export
export default function proxy(request) {
// Proxy logic
}配置对象(可选)
可选地,配置对象可以与代理函数一起导出。此对象包含匹配器以指定代理适用的路径。
匹配器
matcher 选项允许您指定代理运行的特定路径。您可以通过多种方式指定这些路径
- 对于单个路径:直接使用字符串定义路径,例如
'/about'。 - 对于多个路径:使用数组列出多个路径,例如
matcher: ['/about', '/contact'],它将代理应用于/about和/contact。
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)不存在的条件。
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' }],
},
],
}配置的匹配器
- 必须以
/开头 - 可以包含命名参数:
/about/:path匹配/about/a和/about/b,但不匹配/about/a/c - 可以对命名参数(以
:开头)使用修饰符:/about/:path*匹配/about/a/b/c,因为*表示 零个或多个。?表示 零个或一个,+表示 一个或多个 - 可以使用括号括起来的正则表达式:
/about/(.*)与/about/:path*相同
阅读更多关于 path-to-regexp 文档的详细信息。
须知:
matcher值必须是常量,以便在构建时进行静态分析。动态值(例如变量)将被忽略。- 为了向后兼容,Next.js 始终将
/public视为/public/index。因此,/public/:path的匹配器将匹配。
参数
request
定义代理时,默认导出函数接受一个参数 request。此参数是 NextRequest 的实例,表示传入的 HTTP 请求。
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 路由、
getServerSideProps和rewrite目的地设置请求头 - 设置响应 Cookie
- 设置响应头
要从代理生成响应,您可以
须知:对于重定向,您也可以使用
Response.redirect而不是NextResponse.redirect。
执行顺序
代理将针对**项目中的每个路由**调用。鉴于此,使用匹配器精确地定位或排除特定路由至关重要。以下是执行顺序
next.config.js中的headersnext.config.js中的redirects- 代理(
rewrites、redirects等) next.config.js中的beforeFiles(rewrites)- 文件系统路由(
public/、_next/static/、pages/、app/等) next.config.js中的afterFiles(rewrites)- 动态路由(
/blog/[slug]) next.config.js中的fallback(rewrites)
运行时
代理默认使用 Node.js 运行时。runtime 配置选项在代理文件中不可用。在代理中设置 runtime 配置选项将抛出错误。
高级代理标志
在 Next.js v13.1 中,为代理引入了两个额外的标志,skipMiddlewareUrlNormalize 和 skipTrailingSlashRedirect,以处理高级用例。
skipTrailingSlashRedirect 禁用 Next.js 添加或删除尾部斜杠的重定向。这允许在代理内部进行自定义处理,以保持某些路径的尾部斜杠而不保持其他路径,这可以使增量迁移更容易。
module.exports = {
skipTrailingSlashRedirect: true,
}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 提供完全控制。
module.exports = {
skipMiddlewareUrlNormalize: true,
}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
}示例
条件语句
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))
}
}使用 Cookies
Cookies 是常规的请求头。在 Request 上,它们存储在 Cookie 请求头中。在 Response 上,它们存储在 Set-Cookie 请求头中。Next.js 提供了一种便捷的方式,通过 NextRequest 和 NextResponse 上的 cookies 扩展来访问和操作这些 Cookie。
- 对于传入请求,
cookies附带以下方法:get、getAll、set和deletecookies。您可以使用has检查 Cookie 是否存在,或使用clear删除所有 Cookie。 - 对于传出响应,
cookies具有以下方法:get、getAll、set和delete。
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
}设置 Headers
您可以使用 NextResponse API 设置请求和响应头(从 Next.js v13.0.0 开始支持设置*请求*头)。
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 请求头中了解更多。
须知:避免设置过大的请求头,否则可能会根据您的后端 Web 服务器配置导致 431 请求头字段过大 错误。
CORS
您可以在代理中设置 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 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*',
}须知: 您可以在路由处理程序中为单个路由配置 CORS 头。
生成响应
您可以直接从代理返回 Response 或 NextResponse 实例来响应。(此功能自 Next.js v13.1.0 起可用)
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 配置允许使用完整的正则表达式,因此支持负向前瞻或字符匹配等匹配。负向前瞻的示例(匹配除特定路径之外的所有路径)可见此处
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).*)',
],
}您还可以通过使用 missing 或 has 数组,或两者的组合来绕过某些请求的代理
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' }],
},
],
}waitUntil 和 NextFetchEvent
NextFetchEvent 对象扩展了原生的 FetchEvent 对象,并包含 waitUntil() 方法。
waitUntil() 方法接受一个 Promise 作为参数,并延长代理的生命周期,直到 Promise 解决。这对于在后台执行工作非常有用。
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 的原因。
为什么叫“代理”
“代理”这个名称阐明了中间件的功能。“代理”一词意味着它在应用程序前面有一个网络边界,这正是中间件的行为。此外,中间件默认在边缘运行时运行,它可以更接近客户端运行,与应用程序的区域分离。这些行为与“代理”一词更好地契合,并提供了更清晰的功能目的。
如何迁移
我们建议用户避免依赖中间件,除非别无选择。我们的目标是为他们提供具有更好人体工程学的 API,以便他们可以在没有中间件的情况下实现目标。
“中间件”一词经常让用户与 Express.js 中间件混淆,这可能会鼓励滥用。为了澄清我们的方向,我们将文件约定重命名为“代理”。这突出表明我们正在摆脱中间件,分解其重载功能,并使代理的目的明确。
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() {版本历史
这有帮助吗?