中间件
中间件允许您在请求完成之前运行代码。然后, 基于传入的请求, 您能够通过重写、重定向、修改请求或响应头或者直接响应来修改响应。
中间件在缓存内容和路由匹配之前运行。参阅匹配路径获取更多信息。
用例
将中间件集成到应用程序中可以显著提高性能、安全和用户体验。 有些特别有效的中间件场景,包括:
- 身份验证和授权: 在授予对特别的页面或 API 路由访问之前,确保用户身份和核对 session。
- 服务器端重定向: 基于某些条件在服务器级别重定向用户,(例如:区域设置、用户角色)。
- 路径重写: 基于请求属性,通过对 API 路由或页面的动态重写路径,支持 A/B 测试、功能发布或遗留路径。
- 爬虫程序检查: 通过检测和拦截爬虫程序来保护您的资源。
- 日志和分析: 在页面或 API 进行处理之前,捕获和分析请求数据以获得观点。
- 功能标记: 动态启用或禁用功能,以实现无缝功能推出或测试。
认识到中间件可能不是最佳方法同样重要。以下是一些需要注意的场景:
- 复杂的数据获取和操作: 中间件不是为直接数据读取或操作而设计, 其应该在路由处理程序或服务器端工具内完成。
- 繁重的计算任务: 中间件应该是轻量级的并且快速响应的,否则其可能造成页面延迟加载。应该在专门的路由处理程序中完成繁重的计算任务或长时间运行的进行。
- 大量的 Session 管理: 尽管中间件能够操纵简单的 session 任务, 但是大量的 session 管理应该由专门的身份验证服务或在路由处理程序中操作。
- 直接操作数据库: 不推荐在中间件中直接执行数据库操作。应该在路由处理程序或服务器端的工具内完成数据库交互。
公约
在项目根目录中使用middleware.ts (或 .js)文件来定义中间件。例如,和pages 或 app同级或在src目录内。
注意: 虽然每个项目只支持一个
middleware.ts文件, 但是您仍然可以模块化地管理您的中间件逻辑。将中间件功能分解为单独的.ts或.js文件,并将它们导入到您的主middleware.ts文件中。这样可以更清晰地管理特定于路由的中间件, 为了集中控制而聚集在middleware.ts中。通过强制使用单个中间件文件,它简化了配置,防止了潜在的冲突,并通过避免多个中间件层来优化性能。
例子
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL("/home", request.url));
}
// See "Matching Paths" below to learn more
export const config = {
matcher: "/about/:path*",
};
import { NextResponse } from "next/server";
// This function can be marked `async` if using `await` inside
export function middleware(request) {
return NextResponse.redirect(new URL("/home", request.url));
}
// See "Matching Paths" below to learn more
export const config = {
matcher: "/about/:path*",
};
Matching Paths
中间件将被项目中的每个路由调用。鉴于此,使用匹配器精确地定位或排除特定的路由是非常重要的。以下是执行顺序:
headersfromnext.config.jsredirectsfromnext.config.js- Middleware (
rewrites,redirects, etc.) beforeFiles(rewrites) fromnext.config.js- Filesystem routes (
public/,_next/static/,pages/,app/, etc.) afterFiles(rewrites) fromnext.config.js- Dynamic Routes (
/blog/[slug]) fallback(rewrites) fromnext.config.js
有两种方法定义中间件在哪些路径上运行:
Matcher
'matcher'允许您过滤中间件,以便在特定路径上运行
export const config = {
matcher: "/about/:path*",
};
你能够使用数组语法匹配单个或多个路径:
export const config = {
matcher: ["/about/:path*", "/dashboard/:path*"],
};
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 (favicon file)
*/
"/((?!api|_next/static|_next/image|favicon.ico).*)",
],
};
您也可以通过使用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 (favicon file)
*/
{
source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
missing: [
{ type: "header", key: "next-router-prefetch" },
{ type: "header", key: "purpose", value: "prefetch" },
],
},
{
source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
has: [
{ type: "header", key: "next-router-prefetch" },
{ type: "header", key: "purpose", value: "prefetch" },
],
},
{
source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
has: [{ type: "header", key: "x-present" }],
missing: [{ type: "header", key: "x-missing", value: "prefetch" }],
},
],
};
您需要知道:
matcher的值需要是常量,以便在构建时可以对其进行静态分析。动态值将会被忽略(如变量)。
匹配器配置:
- 必须以
/开始 - 可以包含命名参数:
/about/:path匹配/about/a和/about/b,但是不能匹配/about/a/c - 可以在命名参数上设置修饰符 (以
:开始):/about/:path*匹配/about/a/b/c,因为*是 0 或多个,?是 0 或 1 个* ,+是一个或多个* - 可以使用圆括号包裹正则表达式:
/about/(.*)和/about/:path*一样
在path-to-regexp 文档上阅读更多详情。
您需要知道: 为了向后兼容, Next.js 始终将
/public视为/public/index。因此,/public/:path的匹配器将会匹配。
条件语句
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));
}
}
NextResponse
NextResponse API 允许您:
- 将传入的请求
重定向到另一个 URL - 通过展示指定的 URL
重写响应 - 为 API Routes,
getServerSideProps, 和rewrite目标设置 headers - 设置响应 cookies
- 设置响应 headers
要从中间件生成响应, 您可以:
重写到生成响应的路由(Page 或 Route Handler)- 直接返回
NextResponse。 参阅Producing a Response
使用 Cookies
Cookies 是常规的 headers。在请求中, 它们被存储在Cookie header 中。在响 应中,它们位于Set-Cookie header 中. Next.js 通过在NextRequest 和 NextResponse上扩展 cookies,提供了获取和操作这些 cookies 的便捷方式。
- 对于传入的请求,
cookies带有以下的方法:get,getAll,set, 和deletecookies. 您可以使用has核对 cookie 的存在或者使用clear移除所有 cookies。 - 对于传出的响应,
cookies有下列方法:get,getAll,set, 和delete。
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(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;
}
import { NextResponse } from "next/server";
export function middleware(request) {
// 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=/test` header.
return response;
}
设置 Headers
您能够使用NextResponse API 设置请求和响应 headers (自 Next.js v13.0.0 起,可以设置请求头)。
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
// Clone the request headers and set a new header `x-hello-from-middleware1`
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-hello-from-middleware1", "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-middleware2`
response.headers.set("x-hello-from-middleware2", "hello");
return response;
}
import { NextResponse } from "next/server";
export function middleware(request) {
// Clone the request headers and set a new header `x-hello-from-middleware1`
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-hello-from-middleware1", "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-middleware2`
response.headers.set("x-hello-from-middleware2", "hello");
return response;
}
您需要知道: 避免设置大的 headers,因为这可能导致431 请求头字段太大 错误,这取决于您的后端 web 服务器配置。
CORS
您可以在中间件中设置 CORS header 来允许跨域请求, 包括 simple 和 preflighted 请求。
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) {
// 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*",
};
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) {
// 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 headers。
生成响应
您可以通过返回一个Response or NextResponse实例直接从中间件进行响应。((自 Next.js v13.1.0) 起可用))
import type { NextRequest } from "next/server";
import { isAuthenticated } from "@lib/auth";
// Limit the middleware to paths starting with `/api/`
export const config = {
matcher: "/api/:function*",
};
export function middleware(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 }
);
}
}
import { isAuthenticated } from "@lib/auth";
// Limit the middleware to paths starting with `/api/`
export const config = {
matcher: "/api/:function*",
};
export function middleware(request) {
// 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 }
);
}
}
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();
}
Advanced Middleware Flags
Next.jsv13.1为中间件引入了skipMiddlewareUrlNormalize 和 skipTrailingSlashRedirect两个额外的标志,它们用来处理高级用例。
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();
}
// 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 middleware(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
}
运行
中间件目前只支持Edge 运行。不支持 Node.js 运行。
版本历史
| Version | Changes |
|---|---|
v13.1.0 | 添加了高级中间件标 志 |
v13.0.0 | 中间件可以修改请求头、响应头和发送响应 |
v12.2.0 | 中间件稳定, 请参阅 升级指导 |
v12.0.9 | 在 Edge 运行时强制绝对 url (PR) |
v12.0.0 | 添加了中间件(测试版) |