中间件(middleware.js)
middleware.js|ts
文件用于编写中间件并在请求完成之前在服务器上运行代码。然后,基于传入的请求,你可以通过重写、重定向、修改请求或响应头,或直接响应来修改响应。
中间件在路由渲染之前执行。它对 于实现自定义服务器端逻辑(如身份验证、日志记录或处理重定向)特别有用。
在项目根目录中使用文件 middleware.ts
(或 .js)来定义中间件。例如,与 app
或 pages
同级,或在适用的情况下在 src
内。
- TypeScript
- JavaScript
import { NextResponse, 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
。注意,不支持来自同一文件的多个中间件。
// 默认导出示例
export default function middleware(request) {
// 中间件逻辑
}
配置对象 (可选)
可以选择性地与中间件函数一起导出配置对象。此对象包括 matcher 来指定中间件应用的路径。
匹配器
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 请求。
- TypeScript
- JavaScript
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// 中间件逻辑在这里
}
export function middleware(request) {
// 中间件逻辑在这里
}
提示:
NextRequest
是表示 Next.js 中间件中传入 HTTP 请求的类型,而NextResponse
是用于操作和发送回 HTTP 响应的类。
NextResponse
NextResponse
API 允许你:
redirect
将传入请求重定向到不同的 URLrewrite
通过显示给定 URL 来重写响应- 为 API 路由、
getServerSideProps
和rewrite
目标设置请求头 - 设置响应 cookie
- 设置响应头
要从中间件产生响应,你可以:
提示:对于重定向,你也可以使用
Response.redirect
而不是NextResponse.redirect
。
要从中间件产生响应,你可以:
执行顺序
中间件将为项目中的每个路由调用 。考虑到这一点,使用 matchers 精确地定位或排除特定路由至关重要。以下是执行顺序:
next.config.js
中的headers
next.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
)
运行时
中间件默认使用边缘运行时。从 v15.2 (canary) 开始,我们实验性地支持使用 Node.js 运行时。要启用,请在 next.config
文件中添加标志:
- TypeScript
- JavaScript
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
nodeMiddleware: true,
},
}
export default nextConfig
const nextConfig = {
experimental: {
nodeMiddleware: true,
},
}
export default nextConfig
然后在你的中间件文件中,在 config
对象中将运行时设置为 nodejs
:
- JavaScript
- TypeScript
export const config = {
runtime: 'nodejs',
}
export const config = {
runtime: 'nodejs',
}
注意:此功能尚未推荐用于生产环境。因此,Next.js 将抛出错误,除非你使用 next@canary 版本而不是稳定版本。
高级中间件标志
在 Next.js 的 v13.1
中引入了两个额外的中间件标志,skipMiddlewareUrlNormalize
和 skipTrailingSlashRedirect
来处理高级用例。
skipTrailingSlashRedirect
禁用 Next.js 添加或删除尾随斜杠的重定向。这允许在中间件内进行自定义处理,为某些路径保持尾随斜杠,但不为其他路径保持,这可以使增量迁移更容易。
module.exports = {
skipTrailingSlashRedirect: true,
}
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 提供完全控制。
module.exports = {
skipMiddlewareUrlNormalize: true,
}
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
}
示例
条件语句
- TypeScript
- JavaScript
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))
}
}
使用 Cookie
Cookie 是常规头。在 Request
上,它们存储在 Cookie
头中。在 Response
上,它们在 Set-Cookie
头中。Next.js 通过 NextRequest
和 NextResponse
上的 cookies
扩展提供了一种方便的方式来访问和操作这些 cookie。
- 对于传入请求,
cookies
带有以下方法:get
、getAll
、set
和delete
cookie。你可以使用has
检查 cookie 的存在,或使用clear
删除所有 cookie。 - 对于传出响应,
cookies
有以下方法get
、getAll
、set
和delete
。
- TypeScript
- JavaScript
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 开始可用)。
- TypeScript
- JavaScript
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 Request Header Fields Too Large 错误。
CORS
你可以在中间件中设置 CORS 头以允许跨域请求,包括简单和预检请求。
- TypeScript
- JavaScript
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 头。
产生响应
你可以通过返回 Response
或 NextResponse
实例直接从中间件响应。(这从 Next.js v13.1.0 开始可用)
- TypeScript
- JavaScript
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 }
)
}
}
负匹配
matcher
配置允许完整的正则表达式,因此支持负前瞻或字符匹配等匹配。这里可以看到负前瞻的示例,匹配除特定路径之外的所有路径:
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).*)',
],
}
你还可以通过使用 missing
或 has
数组,或两者的组合来绕过某些请求的中间件:
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' }],
},
],
}
waitUntil
和 NextFetchEvent
NextFetchEvent
对象扩展了原生 FetchEvent
对象,并包括 waitUntil()
方法。
waitUntil()
方法接受一个 Promise 作为参数,并扩展中间件的生命周期直到 Promise 解决。这对于在后台执行工作很有用。
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 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
平台支持
部署选项 | 支持 |
---|---|
Node.js 服务器 | 是 |
Docker 容器 | 是 |
静态导出 | 否 |
适配器 | 平台特定 |
了解自托管 Next.js 时如何配置中间件。
版本历史
版本 | 变更 |
---|---|
v15.2.0 | 中间件现在可以使用 Node.js 运行时(实验性) |
v13.1.0 | 添加高级中间件标志 |
v13.0.0 | 中间件可以修改请求头、响应头并发送响应 |
v12.2.0 | 中间件稳定,请参阅升级指南 |
v12.0.9 | 在边缘运行时中强制绝对 URL (PR) |
v12.0.0 | 添加中间件(测试版) |