切换语言
切换主题

Next.js Middleware 实战指南:路径匹配、Edge Runtime 限制与常见陷阱

凌晨两点,我盯着 Vercel 的错误日志发呆。

刚上线的后台管理系统,明明本地测试时所有 /dashboard 路由都被正确保护了——未登录用户会被重定向到登录页。可现在生产环境,用户直接访问 /dashboard/settings/profile 居然绕过了验证,直接看到了敏感数据。

我马上打开代码,Middleware 文件好好地躺在那里,逻辑也没问题。那会是什么?

翻了半小时 Next.js 文档,终于在 matcher 配置那一小节找到答案:我写的是 /dashboard/:path,但它只匹配 /dashboard/settings 这种一层路径,多层路径就失效了。正确写法应该是 /dashboard/:path*,那个小小的星号,差点让我背锅。

说白了,这不是我第一次在 Middleware 上栽跟头了。Edge Runtime 不支持某个库、matcher 配置不生效、无限重定向循环……这些坑,我几乎都踩过一遍。

如果你也在用 Next.js Middleware,或者准备用它来做身份验证、国际化、A/B 测试这些事,那这篇文章可能帮得上忙。我会把那些容易踩的坑、难懂的配置规则、以及三个完整的实战案例都拆开来讲。

不扯那些”在当今Web开发中”的套话。就聊实际问题,怎么写,怎么避坑,怎么让 Middleware 真正起作用。

Middleware 到底是什么?为什么要用它?

简单来说,Middleware 就是一道”关卡”。

用户请求到达你的页面或 API 之前,它会先经过这道关卡。你可以在这里检查用户身份、修改请求内容、甚至直接返回响应——比如把未登录用户踢到登录页,或者根据用户地区跳转到不同语言版本。

它运行在 Edge Runtime,这个很关键。Edge Runtime 不是跑在你的服务器上,而是部署在靠近用户的边缘节点(CDN)。距离近,延迟低,冷启动接近零。你可以理解为:Middleware 是最接近用户的那层代码。

Edge Runtime 和 Node.js Runtime 有什么区别?

特性Edge RuntimeNode.js Runtime
启动速度接近零冷启动需要几百毫秒
运行位置全球边缘节点特定服务器
API支持Web 标准 API完整 Node.js API
适用场景轻量逻辑、快速响应复杂计算、数据库操作

简单讲:速度快,但能力有限。你没法用 Node.js 那些模块(fspath 这些),也连不了大部分数据库。这也是后面最容易踩坑的地方。

什么时候该用 Middleware?

不是所有逻辑都适合塞进 Middleware。我总结了几个最常见的使用场景:

1. 身份验证(Auth Gate)
最经典的用法。检查用户是否登录,没登录就重定向到登录页。比服务端组件里判断要快,因为请求都还没到服务器。

2. 国际化(i18n)
根据用户语言偏好(可能来自 URL、Cookie 或浏览器设置),自动跳转到对应语言版本。//zh/en

3. A/B 测试
随机把用户分成两组,展示不同版本的页面。用 Cookie 保持分组一致,避免用户刷新后看到不一样的内容。

4. Bot 检测与限流
拦截爬虫或恶意请求,或者对某些 IP 做访问频率限制。

5. 日志与数据统计
记录每个请求的基本信息(路径、来源、UA),发送到分析服务。

6. 内容改写(Rewrite)
把用户访问的 URL 内部映射到另一个路径,但浏览器地址栏不变。这个在做动态路由或 A/B 测试时特别有用。

为什么不直接在页面组件里做这些?

可以,但会慢。服务端组件或客户端组件的逻辑,都在请求到达服务器、甚至渲染页面之后才执行。而 Middleware 在边缘就拦住了,响应更快,体验更好。

还有一点:集中管理。你不想在每个需要保护的页面里都写一遍身份验证逻辑吧?Middleware 让你在一个地方搞定。

不过也别什么都往 Middleware 里塞。复杂的业务逻辑、数据库查询、大量计算——这些还是交给 API 路由或服务端组件。Middleware 应该保持轻量、快速。

Middleware 基础配置与文件结构

文件放哪儿?

Next.js 对 Middleware 的位置要求很严格:必须放在项目根目录或 src 目录下,文件名必须是 middleware.ts(或 .js)。

项目根目录/
├── app/
├── middleware.ts    ← 放这里
├── package.json

或者如果你用 src 目录:

项目根目录/
├── src/
│   ├── app/
│   ├── middleware.ts    ← 放这里
├── package.json

注意:一个项目只能有一个 middleware.ts 文件。不能在 app 目录下或其他地方创建多个中间件文件。这个设计挺合理的,因为 Middleware 本来就该是全局的”守门员”。

最简单的 Middleware 长什么样?

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  console.log('有请求进来了:', request.url);
  return NextResponse.next(); // 放行,继续执行
}

就这么简单。NextResponse.next() 表示”没问题,继续”,请求会正常到达目标页面或 API。

核心 API:NextRequest 和 NextResponse

NextRequest 是对标准 Web Request 的扩展,提供了一些方便的属性:

  • request.nextUrl:解析后的 URL 对象,可以直接拿到 pathname、search 等
  • request.cookies:读写 Cookie 更方便
  • request.geo:用户地理位置信息(需要部署平台支持,比如 Vercel)

NextResponse 提供了几种不同的返回方式:

1. 放行(继续执行)

return NextResponse.next();

2. 重定向(跳转到新 URL)

return NextResponse.redirect(new URL('/login', request.url));

用户会看到地址栏变化。

3. 改写(内部重定向,URL 不变)

return NextResponse.rewrite(new URL('/dashboard/v2', request.url));

用户访问 /dashboard,实际返回 /dashboard/v2 的内容,但地址栏还是显示 /dashboard。这个在做 A/B 测试或版本切换时特别有用。

4. 直接返回响应

return new NextResponse('访问被拒绝', { status: 403 });

不继续处理了,直接给用户返回内容。

一个稍微实用点的例子:添加自定义 Header

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // 给所有响应添加一个自定义 header
  response.headers.set('x-custom-header', 'my-value');

  return response;
}

这个挺常用的,比如你想在所有响应里加个时间戳,或者标记请求来源,就可以这么搞。

版本说明(重要!)

如果你用的是 Next.js 15,要注意官方把 middleware.ts 重命名为 proxy.ts 了。虽然 middleware.ts 还能用(向下兼容),但新项目建议用新名字。本文的代码基于 Next.js 14/15,两个版本都适用,主要概念和 API 没变。

路径匹配(Matcher):最容易踩坑的地方

老实讲,matcher 这块我踩的坑最多。

为什么需要 matcher?

如果不配置 matcher,你的 Middleware 会对每一个请求执行——包括 CSS、JS、图片、字体这些静态资源。想象一下,用户加载一个页面,浏览器请求了 20 个静态文件,你的 Middleware 就执行了 20 次。浪费资源不说,还可能拖慢响应速度。

matcher 的作用就是告诉 Next.js:“只在这些路径上执行 Middleware,其他的别管。“

基础语法

middleware.ts 文件里导出一个 config 对象:

export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*']
}

这表示:只有访问 /dashboard/api 开头的路径时,Middleware 才会执行。

修饰符:*+? 都是什么意思?

这些符号控制路径匹配的”贪婪”程度:

*(零个或多个)
/dashboard/:path* 会匹配:

  • /dashboard
  • /dashboard/settings
  • /dashboard/settings/profile

+(一个或多个)
/dashboard/:path+ 会匹配:

  • /dashboard
  • /dashboard/settings
  • /dashboard/settings/profile

?(零个或一个)
/dashboard/:path? 会匹配:

  • /dashboard
  • /dashboard/settings
  • /dashboard/settings/profile

大部分情况下,用 * 就够了。

常见陷阱与解决方案(重点!)

这个表格是我用血泪换来的,建议收藏:

问题错误写法正确写法原因
动态路由多层路径不匹配/dashboard/:path/dashboard/:path*不加 * 只匹配一层,多层路径会失效
根路径被遗漏matcher: ['/dashboard/:path*']matcher: ['/', '/dashboard/:path*']根路径 / 不会被自动包含,需显式添加
静态资源被拦截matcher: ['/:path*']matcher: ['/((?!_next|favicon.ico).*)']需要用正则排除 _next 等内部路径
API 路由没保护到matcher: ['/api/users']matcher: ['/api/:path*']具体路径只匹配那一个,用通配符覆盖所有 API

最容易中招的:静态资源陷阱

你写了这样的 matcher:

export const config = {
  matcher: ['/:path*'] // 想匹配所有路径
}

结果 Next.js 内部的 _next/static 路径也被匹配了,Middleware 疯狂执行,页面加载巨慢。

正确做法:用负向预查排除

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}

这个正则的意思是:“匹配所有路径,除了 api_next/static_next/imagefavicon.ico。”

说白了,这个正则挺绕的,我也是抄的官方示例。你直接复制用就行,不用深究原理。

另一个坑:不能用动态值

const lang = 'zh'; // 这是个变量
export const config = {
  matcher: [`/${lang}/:path*`] // ❌ 不行!
}

matcher 必须是静态的、编译时就能确定的值。你不能用模板字符串插入变量,也不能在运行时动态生成。

如果需要动态判断,把逻辑放在 middleware 函数里:

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 在这里动态判断
  if (pathname.startsWith('/zh') || pathname.startsWith('/en')) {
    // 处理逻辑
  }

  return NextResponse.next();
}

// matcher 保持静态
export const config = {
  matcher: ['/:locale/:path*']
}

我推荐的 matcher 模板(直接抄作业)

保护特定路由(如后台管理):

export const config = {
  matcher: ['/dashboard/:path*', '/admin/:path*']
}

匹配所有路由但排除静态资源:

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.png$).*)',
  ],
}

保护所有 API 路由:

export const config = {
  matcher: ['/api/:path*']
}

不知道你有没有注意到,matcher 这块 Next.js 文档写得挺简略的,细节全靠自己踩坑总结。希望这个表格能帮你少走弯路。

Edge Runtime 限制与规避方案

这章讲的问题,我第一次遇到的时候真的很懵。

当时想在 Middleware 里验证用户身份,打算查询数据库确认 token 是否有效。代码写完,本地一运行——报错:Native Node.js APIs are not supported in the Edge Runtime

啥意思?我就连个数据库,怎么就不支持了?

后来才搞明白:Edge Runtime 不是完整的 Node.js 环境,很多常见的 API 和库都用不了。

Edge Runtime 不支持什么?

功能类别不支持的 API/模块影响
文件系统fs, path无法读写本地文件
子进程child_process无法执行外部命令
加密部分 crypto API需用 Web Crypto API 替代
数据库MongoDB、MySQL 原生驱动大部分传统数据库驱动不可用
其他process.emit, setImmediate一些底层 Node.js API

实际影响是什么?

最直接的影响:

  1. 不能直接连数据库验证用户身份
  2. 不能读配置文件,比如从 config.json 读取设置
  3. 不能用依赖 Node.js API 的第三方库

这听起来限制很大,但其实有办法绕过。

规避方案:把 Edge Runtime 当成”前哨”

关键思路:Middleware 只做轻量判断,复杂逻辑交给后面的环节。

需求❌ Edge Runtime 限制✅ 解决方案
验证用户身份无法查询数据库使用 JWT 本地验证,或调用 API 路由
加密解密部分 crypto 不可用使用 Web Crypto API
读取配置无法访问文件系统使用环境变量(process.env)或 API
日志记录无法写入本地文件发送到第三方日志服务(如 Logtail)
数据库操作传统驱动不支持使用支持 Edge 的数据库(Vercel Postgres、Supabase)

实战示例:JWT 验证(推荐方案)

JWT(JSON Web Token)是最适合 Middleware 的身份验证方式,因为它是无状态的——token 本身包含所有信息,不需要查询数据库。

import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose'; // 这个库支持 Edge Runtime

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value;

  // 没有 token,重定向到登录页
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  try {
    // 验证 JWT(使用 jose 库,支持 Edge Runtime)
    const secret = new TextEncoder().encode(process.env.JWT_SECRET);
    const { payload } = await jwtVerify(token, secret);

    // 验证通过,把用户信息添加到 header(可选)
    const response = NextResponse.next();
    response.headers.set('x-user-id', payload.userId as string);

    return response;
  } catch (error) {
    // token 无效,清除 cookie 并重定向
    const response = NextResponse.redirect(new URL('/login', request.url));
    response.cookies.delete('auth-token');
    return response;
  }
}

export const config = {
  matcher: ['/dashboard/:path*']
}

关键点:

  • 使用 jose 库而不是 jsonwebtoken,因为后者依赖 Node.js 的 crypto 模块
  • JWT secret 从环境变量读取(process.env 在 Edge Runtime 中可用)
  • 验证失败时清除 cookie,避免后续重复失败

如果必须调用数据库怎么办?

有些场景确实需要查数据库,比如检查用户是否被封禁。这时可以在 Middleware 里调用一个 API 路由:

export async function middleware(request: NextRequest) {
  const userId = request.cookies.get('user-id')?.value;

  if (!userId) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // 调用 API 路由验证用户状态
  const apiUrl = new URL('/api/check-user-status', request.url);
  const response = await fetch(apiUrl, {
    headers: { 'x-user-id': userId }
  });

  const { isActive } = await response.json();

  if (!isActive) {
    return NextResponse.redirect(new URL('/account-suspended', request.url));
  }

  return NextResponse.next();
}

API 路由跑在 Node.js Runtime,可以随便连数据库。但这样会增加延迟,所以只在必要时使用。

关于 Web Crypto API

如果需要加密解密,用浏览器原生的 Web Crypto API:

// 生成哈希
const data = new TextEncoder().encode('hello world');
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');

坦白说,这个 API 用起来比 Node.js 的 crypto 麻烦,但在 Edge Runtime 里只能这样。

怎么知道某个库支不支持 Edge Runtime?

看文档或者直接试。如果报 Native Node.js APIs are not supported,那就是不支持。

有些流行库专门提供了 Edge 版本,比如:

  • JWT:用 jose 而不是 jsonwebtoken
  • 数据库:Vercel Postgres、Supabase、Prisma(部分支持)
  • 日志:Logtail、Axiom

我的建议:别在 Middleware 里干太复杂的事。它就该快进快出,重活留给 API 路由。

实战案例:三大核心场景完整实现

讲了这么多理论,该上代码了。这三个案例都是我在实际项目中用过的,直接复制就能跑。

案例 1:身份验证与路由保护

场景:你有一个后台管理系统,所有 /dashboard 下的页面都需要登录才能访问。

完整代码:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 获取 token
  const token = request.cookies.get('auth-token')?.value;

  // 没登录,重定向到登录页,并记录原始 URL
  if (!token) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname); // 登录后跳回来
    return NextResponse.redirect(loginUrl);
  }

  try {
    // 验证 JWT
    const secret = new TextEncoder().encode(process.env.JWT_SECRET);
    const { payload } = await jwtVerify(token, secret);

    // 可选:检查 token 是否快过期,自动续期
    const expiresAt = payload.exp as number;
    const now = Math.floor(Date.now() / 1000);
    const shouldRefresh = expiresAt - now < 3600; // 小于1小时就刷新

    const response = NextResponse.next();

    if (shouldRefresh) {
      // 这里可以调用 API 路由刷新 token
      // 为简化示例,这里省略
      response.headers.set('x-token-refresh-needed', 'true');
    }

    // 把用户信息传给页面(可选)
    response.headers.set('x-user-id', payload.userId as string);
    response.headers.set('x-user-role', payload.role as string);

    return response;
  } catch (error) {
    // token 无效或过期,清除并重定向
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('from', pathname);
    loginUrl.searchParams.set('reason', 'expired');

    const response = NextResponse.redirect(loginUrl);
    response.cookies.delete('auth-token');

    return response;
  }
}

export const config = {
  matcher: ['/dashboard/:path*']
}

关键点:

  1. from 参数记录用户原本要去哪儿,登录后可以跳回去
  2. 检查 token 是否快过期,提前刷新,避免用户操作到一半突然掉登录
  3. 把用户信息放到 header,页面组件可以直接读取(可选)

测试方法:

  • 清除浏览器 cookie,访问 /dashboard → 应该跳转到 /login?from=/dashboard
  • 登录后设置 cookie,再访问 → 应该正常显示

常见问题:

  • 问题:本地测试正常,部署后不工作
    原因:环境变量 JWT_SECRET 没在生产环境配置
    解决:在 Vercel/Netlify 等平台的设置里添加环境变量

案例 2:国际化(i18n)路由重定向

场景:你的网站支持中英文,用户访问 / 时根据语言偏好自动跳转到 /zh/en

完整代码:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

const supportedLocales = ['en', 'zh', 'ja'];
const defaultLocale = 'en';

function getPreferredLocale(request: NextRequest): string {
  // 优先级 1:URL 参数(用于手动切换)
  const urlLocale = request.nextUrl.searchParams.get('lang');
  if (urlLocale && supportedLocales.includes(urlLocale)) {
    return urlLocale;
  }

  // 优先级 2:Cookie(用户上次选择)
  const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
  if (cookieLocale && supportedLocales.includes(cookieLocale)) {
    return cookieLocale;
  }

  // 优先级 3:浏览器语言(Accept-Language header)
  const acceptLanguage = request.headers.get('accept-language');
  if (acceptLanguage) {
    // 简单解析 "zh-CN,zh;q=0.9,en;q=0.8"
    const browserLang = acceptLanguage.split(',')[0].split('-')[0];
    if (supportedLocales.includes(browserLang)) {
      return browserLang;
    }
  }

  return defaultLocale;
}

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 检查路径是否已经包含语言前缀
  const pathnameHasLocale = supportedLocales.some(
    locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (!pathnameHasLocale) {
    // 没有语言前缀,重定向到带语言的路径
    const locale = getPreferredLocale(request);
    const newUrl = new URL(`/${locale}${pathname}`, request.url);

    // 保留查询参数
    newUrl.search = request.nextUrl.search;

    const response = NextResponse.redirect(newUrl);

    // 设置 cookie 记住用户选择(30天)
    response.cookies.set('NEXT_LOCALE', locale, {
      maxAge: 60 * 60 * 24 * 30,
      path: '/'
    });

    return response;
  }

  return NextResponse.next();
}

export const config = {
  matcher: [
    // 匹配所有路径,但排除静态资源和 API
    '/((?!api|_next/static|_next/image|favicon.ico|.*\\.).*)'
  ]
}

关键点:

  1. 三层语言检测:URL 参数 > Cookie > 浏览器设置
  2. 用 Cookie 记住用户选择,下次访问直接用上次的语言
  3. matcher 排除了静态资源,避免图片也被重定向

与 next-intl 库集成:

如果你用 next-intl,可以简化很多:

import { createI18nMiddleware } from 'next-intl/middleware';

export default createI18nMiddleware({
  locales: ['en', 'zh', 'ja'],
  defaultLocale: 'en'
});

export const config = {
  matcher: ['/((?!api|_next|.*\\.).)']
};

next-intl 会自动处理语言检测、Cookie 管理这些事,省心很多。

案例 3:A/B 测试与特性开关

场景:你改版了首页,想让 50% 用户看到新版本,收集数据后再决定是否全量上线。

完整代码:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 只在首页做 A/B 测试
  if (pathname !== '/') {
    return NextResponse.next();
  }

  // 检查用户是否已被分组
  let variant = request.cookies.get('ab-test-homepage')?.value;

  if (!variant) {
    // 新用户,随机分配到 A 或 B 组
    variant = Math.random() < 0.5 ? 'A' : 'B';
  }

  let response: NextResponse;

  if (variant === 'B') {
    // B 组:改写到新版本页面(URL 不变)
    response = NextResponse.rewrite(new URL('/homepage-v2', request.url));
  } else {
    // A 组:使用原版本
    response = NextResponse.next();
  }

  // 设置 cookie,确保用户分组一致(7天)
  response.cookies.set('ab-test-homepage', variant, {
    maxAge: 60 * 60 * 24 * 7,
    path: '/'
  });

  // 添加 header 标记用户所属分组(用于数据分析)
  response.headers.set('x-ab-variant', variant);

  return response;
}

export const config = {
  matcher: ['/']
}

关键点:

  1. rewrite 而不是 redirect,用户看到的 URL 还是 /,体验更好
  2. Cookie 保证用户分组一致,不会刷新后看到不同版本
  3. 通过 header 标记分组,方便数据分析平台识别

数据埋点建议:

在页面组件里读取 header:

// app/page.tsx
import { headers } from 'next/headers';

export default function HomePage() {
  const headersList = headers();
  const abVariant = headersList.get('x-ab-variant');

  // 发送埋点数据
  useEffect(() => {
    analytics.track('page_view', {
      page: 'homepage',
      variant: abVariant
    });
  }, [abVariant]);

  return <div>...</div>;
}

这样你就能在分析后台看到 A/B 两组的转化率差异。

进阶:按用户 ID 分组(而不是随机)

如果你想让同一用户在不同设备上看到相同版本:

const userId = request.cookies.get('user-id')?.value;

if (userId) {
  // 根据用户 ID 哈希分配分组(稳定映射)
  const hash = simpleHash(userId);
  variant = hash % 2 === 0 ? 'A' : 'B';
} else {
  // 未登录用户用 cookie
  variant = request.cookies.get('ab-test-homepage')?.value ||
            (Math.random() < 0.5 ? 'A' : 'B');
}

// 简单哈希函数
function simpleHash(str: string): number {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = ((hash << 5) - hash) + str.charCodeAt(i);
    hash |= 0;
  }
  return Math.abs(hash);
}

这三个案例基本覆盖了 Middleware 最常见的用法。你可以把它们组合起来,比如在身份验证的基础上再加国际化。

性能优化与最佳实践

写 Middleware 很容易,写好就需要点技巧了。

模块化组织中间件逻辑

项目变大后,所有逻辑塞在一个 middleware.ts 里会乱成一团。虽然 Next.js 只允许一个 middleware 文件,但你可以把逻辑拆分成多个函数模块。

推荐的目录结构:

项目根目录/
├── middleware/
│   ├── auth.ts        # 身份验证逻辑
│   ├── i18n.ts        # 国际化逻辑
│   ├── ab-test.ts     # A/B 测试逻辑
│   └── rate-limit.ts  # 限流逻辑
├── middleware.ts      # 入口文件

入口文件示例:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { checkAuth } from './middleware/auth';
import { handleI18n } from './middleware/i18n';
import { handleABTest } from './middleware/ab-test';

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // 1. 先处理国际化
  const i18nResponse = handleI18n(request);
  if (i18nResponse) return i18nResponse;

  // 2. 再检查身份验证
  if (pathname.startsWith('/dashboard')) {
    const authResponse = await checkAuth(request);
    if (authResponse) return authResponse;
  }

  // 3. 最后处理 A/B 测试
  if (pathname === '/') {
    return handleABTest(request);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
}

auth.ts 示例:

// middleware/auth.ts
import { NextRequest, NextResponse } from 'next/server';
import { jwtVerify } from 'jose';

export async function checkAuth(request: NextRequest): Promise<NextResponse | null> {
  const token = request.cookies.get('auth-token')?.value;

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  try {
    const secret = new TextEncoder().encode(process.env.JWT_SECRET);
    await jwtVerify(token, secret);
    return null; // 验证通过,返回 null 表示继续
  } catch {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

这样每个模块职责清晰,改起来也方便。

缓存策略:减少重复计算

有些逻辑可以缓存,避免每次请求都重新计算。比如用户权限检查,token 在有效期内其实不用每次都验证。

使用 Vercel Edge Config 缓存配置:

import { get } from '@vercel/edge-config';

export async function middleware(request: NextRequest) {
  // 从 Edge Config 读取特性开关(已缓存)
  const featureFlags = await get('feature-flags');

  if (featureFlags?.newDashboard) {
    return NextResponse.rewrite(new URL('/dashboard-v2', request.url));
  }

  return NextResponse.next();
}

Edge Config 是 Vercel 提供的全球分布式键值存储,读取速度极快,非常适合存放不常变的配置。

避免过大的 Header

Middleware 里设置的 header 会附加到响应里。如果 header 太多或太大,可能导致 431 Request Header Fields Too Large 错误。

建议:

  • Header 总大小控制在 8KB 以内
  • 只传必要的信息,别把整个用户对象塞进去
  • 如果需要传大量数据,考虑用加密后的 token

错误示例:

// ❌ 不要这样
response.headers.set('x-user-data', JSON.stringify(userData)); // 可能很大

正确做法:

// ✅ 只传关键信息
response.headers.set('x-user-id', user.id);
response.headers.set('x-user-role', user.role);

Matcher 优化:精确匹配优于通配符

matcher 越精确,Next.js 执行效率越高。

不够好的写法:

export const config = {
  matcher: ['/:path*'] // 匹配所有路径
}

更好的写法:

export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'] // 只匹配需要的
}

如果你的 Middleware 只需要保护几个特定路由,就别用通配符匹配所有。

监控与调试

开发环境下的调试:

export function middleware(request: NextRequest) {
  if (process.env.NODE_ENV === 'development') {
    console.log('Middleware 执行:', {
      path: request.nextUrl.pathname,
      method: request.method,
      cookies: request.cookies.getAll()
    });
  }

  // 你的逻辑...
}

生产环境的日志:

Edge Runtime 里的 console.log 会输出到平台日志(如 Vercel 的 Edge Function Logs)。但别打太多日志,会增加执行时间。

推荐的日志方案:发送到第三方服务

import { Logger } from '@logtail/edge';

const logger = new Logger(process.env.LOGTAIL_TOKEN);

export async function middleware(request: NextRequest) {
  try {
    // 你的逻辑...
  } catch (error) {
    // 只在出错时记录日志
    await logger.error('Middleware error', {
      path: request.nextUrl.pathname,
      error: error.message
    });
    throw error;
  }
}

最佳实践清单

总结一下这些年踩坑总结的经验:

✅ 应该做的:

  • 保持 Middleware 逻辑轻量,复杂逻辑交给 API 路由
  • 使用 matcher 精确匹配需要处理的路径
  • 用 JWT 做身份验证,避免数据库查询
  • 模块化组织代码,按功能拆分文件
  • 在开发环境打印调试信息
  • 设置合理的 Cookie 过期时间

❌ 不应该做的:

  • 在 Middleware 里做大量计算或数据库操作
  • 不配置 matcher,让所有请求都执行
  • 使用依赖 Node.js API 的第三方库
  • 设置过大的 header(>8KB)
  • 在生产环境打大量日志
  • 创建无限重定向循环(检查逻辑避免递归)

遵循这些原则,你的 Middleware 应该不会有太大问题。

常见错误与调试技巧

最后聊聊那些让人抓狂的错误。这些我都遇到过,有些还折腾了挺久。

错误 1:Middleware 根本不执行

症状:写了 Middleware,但好像没起作用,请求正常通过。

可能的原因与解决方案:

原因检查方法解决方案
文件位置错误确认 middleware.ts 在根目录或 src 目录移动到正确位置
matcher 没覆盖当前路径打印 request.nextUrl.pathname 看路径调整 matcher 配置
语法错误查看控制台是否有编译错误修复语法问题
没有正确导出函数确认使用 export function middleware检查导出语法
缓存问题清除 .next 目录运行 rm -rf .next && npm run dev

调试技巧:

在 Middleware 最开头加一行日志:

export function middleware(request: NextRequest) {
  console.log('🔥 Middleware 执行了!路径:', request.nextUrl.pathname);
  // 其他逻辑...
}

如果这行日志都没打印,说明 Middleware 没执行,检查上面的原因。

错误 2:Native Node.js APIs are not supported in the Edge Runtime

症状:运行时报这个错,指出某个 API 或模块不支持。

定位问题:

看报错堆栈,找出是哪个库或代码调用了 Node.js API。常见的罪魁祸首:

  • fspath 等文件系统模块
  • jsonwebtoken(用 jose 替代)
  • MongoDB、MySQL 原生驱动(用支持 Edge 的库)

解决方案:

  1. 找替代库:查看库的文档,看是否有 Edge Runtime 兼容版本
  2. 把逻辑移到 API 路由:如果必须用 Node.js API,就别放在 Middleware 里
  3. 使用 Web 标准 API:比如用 Web Crypto API 替代 Node.js 的 crypto

示例:

// ❌ 不行
import jwt from 'jsonwebtoken';

// ✅ 用这个
import { jwtVerify } from 'jose';

错误 3:Invalid middleware found

症状:启动项目时报这个错。

可能的原因:

1. matcher 是空数组

// ❌ 不行
export const config = {
  matcher: []
}

2. 没有正确导出 middleware 函数

// ❌ 不行
const middleware = (request: NextRequest) => { ... }

// ✅ 要这样
export function middleware(request: NextRequest) { ... }

3. middleware 函数没有返回值

// ❌ 不行
export function middleware(request: NextRequest) {
  console.log('做点啥');
  // 忘记 return 了
}

// ✅ 必须有返回值
export function middleware(request: NextRequest) {
  return NextResponse.next();
}

错误 4:无限重定向循环

症状:浏览器报 ERR_TOO_MANY_REDIRECTS,页面打不开。

原因:重定向的目标 URL 又触发了 Middleware,导致循环。

常见场景:

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token');

  if (!token) {
    // ❌ 问题:重定向到 /login,但 /login 也会触发这个 Middleware
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/:path*'] // 匹配所有路径,包括 /login
}

解决方案:排除登录页或其他公共页面

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // ✅ 先排除公共页面
  if (pathname === '/login' || pathname === '/') {
    return NextResponse.next();
  }

  const token = request.cookies.get('auth-token');

  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

或者调整 matcher:

export const config = {
  matcher: ['/dashboard/:path*'] // 只保护需要登录的路径
}

错误 5:环境变量读不到

症状:process.env.XXXundefined

原因:

  1. .env.local 文件没创建或变量名拼错
  2. 部署平台没配置环境变量(如 Vercel、Netlify)
  3. 变量名不符合规范(Next.js 有些限制)

解决方案:

本地开发:

// .env.local
JWT_SECRET=your-secret-here

生产环境:
在 Vercel/Netlify 的项目设置里添加环境变量,重新部署。

注意:

  • 环境变量修改后需要重启开发服务器
  • 部署平台的环境变量只在服务端和 Edge Runtime 可用,客户端需要 NEXT_PUBLIC_ 前缀

调试技巧总结

1. 使用 x-middleware-next header 追踪

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // 标记这个请求经过了 Middleware
  response.headers.set('x-middleware-executed', 'true');
  response.headers.set('x-middleware-path', request.nextUrl.pathname);

  return response;
}

然后在浏览器的开发者工具里查看响应 header,确认 Middleware 是否执行了。

2. 分段注释,定位问题

如果不确定哪行代码导致错误,把 Middleware 逻辑逐步注释掉:

export function middleware(request: NextRequest) {
  console.log('步骤 1');
  // ... 一些逻辑

  console.log('步骤 2');
  // ... 更多逻辑

  console.log('步骤 3');
  return NextResponse.next();
}

看日志打印到哪一步,就知道问题出在哪。

3. 查看 Vercel 的 Edge Function Logs

如果部署到 Vercel,在项目的 Functions 标签页可以看到 Edge Function 的日志,包括 console.log 输出和错误信息。

4. 本地测试时使用 next dev --turbo

Next.js 15+ 支持 Turbo 模式,启动更快,错误提示也更清晰:

npm run dev -- --turbo

遇到问题别慌,按这些方法一步步排查,总能找到原因。实在搞不定,去 Next.js 的 GitHub Discussions 或 Stack Overflow 搜搜,很多坑别人都踩过了。

总结

说了这么多,总结成三句话:

1. 明确 Middleware 的适用边界

别什么都往里塞。它适合做轻量级的判断和转发——身份验证、路由重定向、A/B 测试这些。复杂的业务逻辑、数据库查询、大量计算,交给 API 路由或服务端组件。记住:Middleware 应该快进快出,像个称职的门卫,而不是管家。

2. matcher 配置是重中之重

这块最容易踩坑,也是我在文章里反复强调的。动态路由记得加 *,静态资源记得排除,公共页面(如登录页)别拦截。实在搞不清楚,就用我提供的模板,或者在开发环境打日志看路径匹配情况。

3. 理解并接受 Edge Runtime 的限制

Edge Runtime 不是完整的 Node.js,这是设计取舍的结果——牺牲能力换取速度和全球分布。知道哪些 API 不能用,知道怎么绕过这些限制(JWT、Web Crypto API、API 路由调用),你就能用好它。

Next.js Middleware 并不复杂,就是细节多。matcher 规则、Edge Runtime 限制、无限重定向陷阱——这些坑我都踩过,写这篇文章就是想让你少走弯路。

如果你正在做需要全局拦截的功能,试试 Middleware 吧。代码跑起来后,再慢慢优化。遇到问题翻回来看看错误排查那一章,基本都能搞定。

最后,如果这篇文章帮到了你,欢迎分享给同样在用 Next.js 的朋友。下一篇我打算写 Server Actions,也是个坑挺多的话题。

加油,祝你的 Middleware 一次跑通。

Next.js Middleware 配置完整流程

从创建Middleware文件到实现路由保护、国际化、A/B测试三大场景

⏱️ 预计耗时: 3 小时

  1. 1

    步骤1: 创建Middleware文件

    创建文件:
    • 位置:middleware.ts(项目根目录)
    • 导出config对象配置matcher
    • 导出middleware函数处理请求

    基础结构:
    export const config = {
    matcher: '/dashboard/:path*'
    }

    export function middleware(request: NextRequest) {
    // 处理逻辑
    }
  2. 2

    步骤2: 配置matcher规则

    匹配规则:
    • 单个路径:'/dashboard'
    • 动态路由:'/dashboard/:path*'(注意星号匹配多层)
    • 多个路径:['/dashboard/:path*', '/admin/:path*']
    • 排除路径:使用负向前瞻,如'/((?!api|_next/static|_next/image|favicon.ico).*)'

    注意事项:
    • 动态路由必须加*才能匹配多层
    • 静态资源要排除(_next/static、_next/image等)
    • 公共页面(登录页)不要拦截
  3. 3

    步骤3: 实现路由保护(身份验证)

    步骤:
    1. 从cookie中读取token
    2. 验证token有效性(可以用JWT或调用API)
    3. 未登录用户重定向到登录页
    4. 已登录用户继续访问

    代码要点:
    • 使用NextRequest.cookies获取cookie
    • 使用NextResponse.redirect重定向
    • 使用NextResponse.next继续请求
    • 注意避免无限重定向循环
  4. 4

    步骤4: 实现国际化(语言切换)

    步骤:
    1. 检测用户语言偏好(cookie、header、默认值)
    2. 根据路径判断是否需要重定向
    3. 添加语言前缀到URL
    4. 设置语言cookie

    代码要点:
    • 使用request.headers.get('accept-language')
    • 使用request.nextUrl.pathname获取路径
    • 使用NextResponse.rewrite重写URL
    • 支持语言切换而不改变URL结构
  5. 5

    步骤5: 处理Edge Runtime限制

    限制和解决方案:
    • 不支持Node.js API → 使用Web标准API
    • 不支持文件系统 → 使用环境变量或API调用
    • 不支持某些npm包 → 检查包是否支持Edge Runtime
    • 需要JWT验证 → 使用Web Crypto API或调用API路由

    调试技巧:
    • 使用console.log输出调试信息
    • 检查Vercel Edge Functions日志
    • 使用try-catch捕获错误
  6. 6

    步骤6: 测试和调试

    测试要点:
    • 测试所有匹配的路径
    • 测试未匹配的路径(确保不拦截)
    • 测试重定向逻辑
    • 测试Edge Runtime兼容性

    调试方法:
    • 在middleware中添加console.log
    • 检查浏览器Network tab
    • 查看Vercel Edge Functions日志
    • 使用Next.js开发模式查看警告

常见问题

Middleware 的 matcher 配置不生效怎么办?
检查:
1) matcher路径是否正确(动态路由需要加*)
2) 是否排除了静态资源
3) 路径格式是否正确(不要用正则表达式,用Next.js支持的格式)

可以在middleware中添加console.log查看哪些路径被匹配。
为什么多层路径匹配不到?
动态路由必须使用`:path*`(注意星号)才能匹配多层路径。例如`/dashboard/:path*`可以匹配`/dashboard/settings/profile`,而`/dashboard/:path`只能匹配`/dashboard/settings`这种一层路径。
Edge Runtime 不支持某个库怎么办?
Edge Runtime不是完整的Node.js,不支持所有npm包。

解决方案:
1) 检查包是否支持Edge Runtime
2) 使用Web标准API替代
3) 将复杂逻辑移到API路由或Server Component
4) 使用支持Edge Runtime的替代库
如何避免无限重定向循环?
确保重定向目标不在matcher匹配范围内。例如,如果重定向到`/login`,matcher应该排除`/login`路径。可以使用负向前瞻排除:`'/((?!login|api).*)'`。
Middleware 中可以访问数据库吗?
不建议。Middleware在Edge Runtime运行,应该保持轻量级。

如果需要数据库查询,应该:
1) 在Middleware中调用API路由
2) 使用环境变量存储配置
3) 将复杂逻辑移到API路由或Server Component
如何调试 Middleware?
方法:
1) 在middleware函数中添加console.log输出调试信息
2) 检查浏览器Network tab查看请求和响应
3) 查看Vercel Edge Functions日志
4) 使用Next.js开发模式查看警告和错误信息
Middleware 和 API 路由有什么区别?
Middleware:
• 在Edge Runtime运行
• 在请求到达页面或API路由之前执行
• 适合轻量级的拦截和转发

API路由:
• 在Node.js运行时运行
• 可以访问完整的Node.js API和数据库
• 适合复杂的业务逻辑

20 分钟阅读 · 发布于: 2025年12月25日 · 修改于: 2026年1月22日

评论

使用 GitHub 账号登录后即可评论

相关文章