中间件
中间件(Middleware),一个听起来就很高级、很强大的功能。实际上也确实如此。使用中间件,你可以拦截并控制应用里的所有请求和响应。
比如你可以基于传入的请求,重写、重定向、修改请求或响应头、甚至直接响应内容。一个比较常见的应用就是鉴权,在打开页面渲染具体的内容前,先判断用户是否登录,如果未登录,则跳转到登录页面。
定义
写中间件,你需要在项目的根目录定义一个名为 middleware.js
的文件:
- 这里的根目录就是与
package.json
同级别 不是app
下面 目录如下:
├── app
│ ├── about
│ │ └── page.js
│ ├── home
│ │ └── page.js
├── middleware.js
├── package.json
└── tsconfig.json
- 举例如下
// middleware.js
import { NextResponse } from "next/server";
// 中间件可以是 async 函数,如果使用了 await
export function middleware(request) {
return NextResponse.redirect(new URL("/home", request.url));
}
// 设置匹配路径
export const config = {
matcher: "/about/:path*",
};
在这个例子中,我们通过 config.matcher
设置中间件生效的路径,在 middleware
函数中设置中间件的逻辑,作用是将 /about
、/about/xxx
、/about/xxx/xxx
这样的的地址统一重定向到 /home
matcher 配置
单独
export const config = {
matcher: "/about/:path*",
};
matcher
不仅支持字符串形式,也支持数组形式,用于匹配多个路径:
多个
export const config = {
matcher: ["/about/:path*", "/dashboard/:path*"],
};
正则
命名参数可以使用修饰符,其中 * 表示 0 个或 1 个或多个,?表示 0 个或 1 个,+表示 1 个或多个,比如
/about/:path*
匹配/about
、/about/xxx
、/about/xxx/xxx
/about/:path?
匹配/about
、/about/xxx
/about/:path+
匹配/about/xxx
、/about/xxx/xxx
也可以在圆括号中使用标准的正则表达式,比如/about/(.*)
等同于 /about/:path*
,比如 /(about|settings)
匹配 /about
和 /settings
,不匹配其他的地址。/user-(ya|yu)
匹配 /user-ya
和 /user-yu
。
一个复杂常用的例子:
export const config = {
matcher: [
/*
* 匹配所有的路径除了以这些作为开头的:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
"/((?!api|_next/static|_next/image|favicon.ico).*)",
],
};
除此之外,还要注意,路径必须以 /
开头。matcher
的值必须是常量,这样可以在构建的时候被静态分析。使用变量之类的动态值会被忽略。
路径参数
export const config = {
matcher: [
{
source: "/api/*",
has: [
{ type: "header", key: "Authorization", value: "Bearer Token" },
{ type: "query", key: "userId", value: "123" },
],
missing: [{ type: "cookie", key: "session", value: "active" }],
},
],
};
在这个例子中,不仅匹配了路由地址,还要求 header
的 Authorization
必须是 Bearer Token
,查询参数的 userId
为 123,且 cookie
里的 session
值不是 active
。
条件语句
第二种方法是使用条件语句:
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));
}
}
中间件逻辑
export function middleware(request) {
// 如何读取和设置 cookies ?
// 如何读取 headers ?
// 如何直接响应?
}
如何读取和设置 cookies?
用法跟路由处理程序一致,使用 NextRequest 和 NextResponse 快捷读取和设置 cookies。
对于传入的请求,NextRequest 提供了 get
、getAll
、set
和 delete
方法处理 cookies
,你也可以用 has 检查 cookie 或者 clear 删除所有的 cookies。
对于返回的响应,NextResponse 同样提供了 get、getAll、set 和 delete 方法处理 cookies。示例代码如下:
import { NextResponse } from "next/server";
export function middleware(request) {
// 假设传入的请求 header 里 "Cookie:nextjs=fast"
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
// 设置 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: '/' }
// 响应 header 为 `Set-Cookie:vercel=fast;path=/test`
return response;
}
在这个例子中,我们调用了 NextResponse.next()
这个方法,这个方法专门用在 middleware
中,毕竟我们写的是中间件,中间件进行一层处理后,返回的结果还要在下一个逻辑中继续使用,此时就需要返回 NextResponse.next()
。当然如果不需要再走下一个逻辑了,可以直接返回一个 Response
实例,接下来的例子中会演示其写法。
如何读取和设置 headers?
用法跟路由处理程序一致,使用 NextRequest
和 NextResponse
快捷读取和设置 headers
。示例代码如下:
// middleware.js
import { NextResponse } from "next/server";
export function middleware(request) {
// clone 请求标头
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-hello-from-middleware1", "hello");
// 你也可以在 NextResponse.rewrite 中设置请求标头
const response = NextResponse.next({
request: {
// 设置新请求标头
headers: requestHeaders,
},
});
// 设置新响应标头 `x-hello-from-middleware2`
response.headers.set("x-hello-from-middleware2", "hello");
return response;
}
这个例子比较特殊的地方在于调用 NextResponse.next 的时候传入了一个对象用于转发 headers
CORS
这是一个在实际开发中会用到的设置 CORS 的例子:
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*",
};
如何直接响应?
用法跟路由处理程序一致,使用 NextResponse 设置返回的 Response。示例代码如下:
import { NextResponse } from "next/server";
import { isAuthenticated } from "@lib/auth";
export const config = {
matcher: "/api/:function*",
};
export function middleware(request) {
// 鉴权判断
if (!isAuthenticated(request)) {
// 返回错误信息
return new NextResponse(
JSON.stringify({ success: false, message: "authentication failed" }),
{ status: 401, headers: { "content-type": "application/json" } }
);
}
}
执行顺序
在 Next.js 中,有很多地方都可以设置路由的响应,比如 next.config.js 中可以设置,中间件中可以设置,具体的路由中可以设置,所以要注意它们的执行顺序:
注意
- headers(next.config.js)
- redirects(next.config.js)
- 中间件 (rewrites, redirects 等)
- beforeFiles (next.config.js 中的 rewrites)
- 基于文件系统的路由 (public/, _next/static/, pages/, app/ 等)
- afterFiles (next.config.js 中的 rewrites)
- 动态路由 (/blog/[slug])
- fallback 中的 (next.config.js 中的 rewrites)
注: beforeFiles
顾名思义,在基于文件系统的路由之前,afterFiles
顾名思义,在基于文件系统的路由之后,fallback
顾名思义,垫底执行。
中间件相关配置
skipTrailingSlashRedirect
skipTrailingSlashRedirect
顾名思义,跳过尾部斜杠重定向,当
你设置 skipTrailingSlashRedirect
为 true 后
假设再次访问 /about/
,URL 依然会是 /about/
。
// next.config.js
module.exports = {
skipTrailingSlashRedirect: true,
};
skipMiddlewareUrlNormalize
可以获取路由原始的地址,常用于国际化场景中。
// 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);
// 如果设置为 true,值为:/_next/data/build-id/hello.json
// 如果没有配置,值为: /hello
}
注意
注意
这意味着写 中间价 的时候,尽可能使用 Web API,避免使用 Node.js API
中间件分割
工具函数代码如下
- 有的时候中间件太多了 不好维护 需要拆分
import { NextResponse } from "next/server";
function chain(functions, index = 0) {
const current = functions[index];
if (current) {
const next = chain(functions, index + 1);
return current(next);
}
return () => NextResponse.next();
}
function withMiddleware1(middleware) {
return async (request) => {
console.log("middleware1 " + request.url);
return middleware(request);
};
}
function withMiddleware2(middleware) {
return async (request) => {
console.log("middleware2 " + request.url);
return middleware(request);
};
}
export default chain([withMiddleware1, withMiddleware2]);
export const config = {
matcher: "/api/:path*",
};
调用
import { chain } from "@/lib/utils";
import { withHeaders } from "@/middlewares/withHeaders";
import { withLogging } from "@/middlewares/withLogging";
export default chain([withLogging, withHeaders]);
export const config = {
matcher: "/api/:path*",
};
具体每一个中间件
export const withHeaders = (next) => {
return async (request) => {
// ...
return next(request);
};
};