Menu

中间件

中间件允许你在请求完成之前运行代码。然后,基于传入的请求,你可以通过重写、重定向、修改请求或响应头,或直接响应来修改响应。

中间件在缓存内容和路由匹配之前运行。有关更多详细信息,请参阅 匹配路径

用例

将中间件集成到你的应用程序中可以显著提高性能、安全性和用户体验。以下是中间件特别有效的一些常见场景:

  • 身份验证和授权: 在授予对特定页面或 API 路由的访问权限之前,确保用户身份并检查会话 cookie。
  • 服务器端重定向: 根据某些条件 (例如语言环境、用户角色) 在服务器级别重定向用户。
  • 路径重写: 通过根据请求属性动态重写 API 路由或页面的路径,支持 A/B 测试、功能推出或旧路径。
  • 机器人检测: 通过检测和阻止机器人流量来保护你的资源。
  • 日志和分析: 在页面或 API 处理之前捕获和分析请求数据以获取洞察。
  • 功能标记: 动态启用或禁用功能,以实现无缝功能推出或测试。

认识到中间件可能不是最佳方法的情况同样重要。以下是一些需要注意的场景:

  • 复杂的数据获取和操作: 中间件并非设计用于直接数据获取或操作,这应该在路由处理程序或服务器端实用程序中完成。
  • 繁重的计算任务: 中间件应该轻量级并快速响应,否则可能导致页面加载延迟。繁重的计算任务或长时间运行的进程应该在专用的路由处理程序中完成。
  • 广泛的会话管理: 虽然中间件可以管理基本的会话任务,但广泛的会话管理应由专门的身份验证服务或路由处理程序管理。
  • 直接数据库操作: 不建议在中间件中执行直接数据库操作。数据库交互应在路由处理程序或服务器端实用程序中完成。

约定

在项目的根目录中使用文件 middleware.ts (或 .js) 来定义中间件。例如,与 pagesapp 在同一级别,或者在适用的情况下放在 src 内。

值得注意的是: 虽然每个项目只支持一个 middleware.ts 文件,但你仍然可以模块化地组织你的中间件逻辑。将中间件功能分解为单独的 .ts.js 文件,并将它们导入到主 middleware.ts 文件中。这允许更清晰地管理特定路由的中间件,并在 middleware.ts 中集中控制。通过强制使用单个中间件文件,可以简化配置,防止潜在冲突,并通过避免多层中间件来优化性能。

示例

middleware.ts
import { NextResponse } from "next/server";
import type { 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*",
};
middleware.js
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*",
};

匹配路径

中间件将对你项目中的 每个路由 调用。考虑到这一点,使用匹配器精确定位或排除特定路由至关重要。以下是执行顺序:

  1. 来自 next.config.jsheaders
  2. 来自 next.config.jsredirects
  3. 中间件 (rewritesredirects 等)
  4. 来自 next.config.jsbeforeFiles (rewrites)
  5. 文件系统路由 (public/_next/static/pages/app/ 等)
  6. 来自 next.config.jsafterFiles (rewrites)
  7. 动态路由 (/blog/[slug])
  8. 来自 next.config.jsfallback (rewrites)

有两种方法可以定义中间件将在哪些路径上运行:

  1. 自定义匹配器配置
  2. 条件语句

匹配器

matcher 允许你过滤中间件以在特定路径上运行。

middleware.js
export const config = {
  matcher: "/about/:path*",
};

你可以使用数组语法匹配单个路径或多个路径:

middleware.js
export const config = {
  matcher: ["/about/:path*", "/dashboard/:path*"],
};

matcher 配置允许使用完整的正则表达式,因此支持匹配负向前瞻或字符匹配。这里是一个使用负向前瞻来匹配除特定路径之外的所有路径的例子:

middleware.js
export const config = {
  matcher: [
    /*
     * 匹配所有请求路径,除了以下开头的路径:
     * - api (API 路由)
     * - _next/static (静态文件)
     * - _next/image (图像优化文件)
     * - favicon.ico (网站图标文件)
     */
    "/((?!api|_next/static|_next/image|favicon.ico).*)",
  ],
};

你也可以使用 missinghas 数组,或两者的组合来绕过某些请求的中间件:

middleware.js
export const config = {
  matcher: [
    /*
     * 匹配所有请求路径,除了以下开头的路径:
     * - api (API 路由)
     * - _next/static (静态文件)
     * - _next/image (图像优化文件)
     * - favicon.ico (网站图标文件)
     */
    {
      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 值需要是常量,以便可以在构建时进行静态分析。动态值 (如变量) 将被忽略。

配置的匹配器:

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

值得注意的是: 为了向后兼容,Next.js 始终将 /public 视为 /public/index。因此,匹配器 /public/:path 将匹配。

条件语句

middleware.ts
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));
  }
}
middleware.js
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 允许你:

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

要从中间件生成响应,你可以:

  1. rewrite 到生成响应的路由 (页面路由处理程序)
  2. 直接返回 NextResponse。请参阅 生成响应

使用 Cookies

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

  1. 对于传入的请求,cookies 具有以下方法: getgetAllsetdelete cookies。你可以使用 has 检查 cookie 是否存在,或使用 clear 删除所有 cookies。
  2. 对于传出的响应,cookies 具有以下方法: getgetAllsetdelete
middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
 
export function middleware (request: NextRequest) {
  // 假设传入请求的头部中存在 "Cookie:nextjs=fast"
  // 使用 `RequestCookies` API 从请求中获取 cookies
  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 在响应上设置 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: '/' }
  // 传出的响应将具有 `Set-Cookie:vercel=fast;path=/` 头部。
 
  return response;
}
middleware.js
import { NextResponse } from "next/server";
 
export function middleware (request) {
  // 假设传入请求的头部中存在 "Cookie:nextjs=fast"
  // 使用 `RequestCookies` API 从请求中获取 cookies
  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 在响应上设置 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: '/'继续翻译:
 
  // 传出的响应将具有 `Set-Cookie:vercel=fast;path=/test` 头部。
 
  return response;
}

设置头部

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

middleware.ts
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;
}
middleware.js
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;
}

值得注意的是: 避免设置大型头部,因为这可能会导致 431 请求头字段太大 错误,具体取决于你的后端 Web 服务器配置。

CORS

你可以在中间件中设置 CORS 头部以允许跨域请求,包括 简单预检 请求。

middleware.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 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*",
};
middleware.js
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 头部。

生成响应

你可以通过返回 ResponseNextResponse 实例直接从中间件响应。(这从 Next.js v13.1.0 开始可用)

middleware.ts
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 }
    );
  }
}
middleware.js
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 }
    );
  }
}

waitUntilNextFetchEvent

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

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

middleware.ts
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 的 v13.1 中引入了两个额外的中间件标志,skipMiddlewareUrlNormalizeskipTrailingSlashRedirect,用于处理高级用例。

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

next.config.js
module.exports = {
  skipTrailingSlashRedirect: true,
};
middleware.js
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.url)
    );
  }
}

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

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);
  // 使用该标志现在是 /_next/data/build-id/hello.json
  // 不使用该标志会被规范化为 /hello
}

运行时

中间件目前只支持 Edge 运行时。不能使用 Node.js 运行时。

版本历史

版本变更
v13.1.0添加高级中间件标志
v13.0.0中间件可以修改请求头部、响应头部,并发送响应
v12.2.0中间件稳定,请参阅 升级指南
v12.0.9在 Edge 运行时强制使用绝对 URL (PR)
v12.0.0添加中间件 (Beta)