第 12 章:Middleware 中间件(路由守卫与请求拦截)

深入理解 Next.js Middleware 的工作原理与高级用法——从路由守卫、国际化、A/B 测试,到请求重写、日志监控,以及 Edge Runtime 的限制与最佳实践。

本章目标:全面掌握 Next.js Middleware——理解其执行时机、Edge Runtime 限制,学会实现路由守卫、国际化、A/B 测试、请求重写等高级功能,并掌握性能优化与调试技巧。


12.1 Middleware 概述

什么是 Middleware?

Middleware 是 Next.js 中在请求到达页面之前执行的代码层。它可以在 Edge Runtime 上运行,具有以下能力:

  • 重写(Rewrite)请求路径
  • 重定向(Redirect)到其他 URL
  • 修改请求/响应 Headers
  • 拦截并返回自定义响应
  • 执行认证检查
请求流程:

Client Request
    ↓
Next.js Middleware(Edge Runtime)
    ↓ 可以:重写、重定向、修改 Headers、拦截
Page / API Route(Node.js Runtime)
    ↓
Response

文件位置

// middleware.ts(项目根目录,与 app/ 同级)

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

export function middleware(request: NextRequest) {
  // 你的中间件逻辑
  return NextResponse.next();
}

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

执行时机

请求生命周期:

1. Client 发起请求
2. Middleware 执行(Edge Runtime)
   - 可以读取 / 修改 request
   - 可以读取 / 修改 headers
   - 可以返回重定向或自定义响应
3. 如果 Middleware 调用 NextResponse.next():
   - 请求继续到 Page / API Route
4. Page / API Route 执行(Node.js Runtime)
5. Response 返回给 Client

12.2 Edge Runtime 限制

Middleware 运行在 Edge Runtime 上,这意味着:

可用的 API

// ✅ 可用:Web 标准 API
- Request / Response / Headers
- URL / URLSearchParams
- fetch()
- ReadableStream / WritableStream
- TextEncoder / TextDecoder
- crypto.subtle (Web Crypto API)
- setTimeout(仅 Vercel Edge

不可用的 API

// ❌ 不可用:Node.js 特有 API
- fs / path(文件系统)
- child_process(子进程)
- net / http(原生网络)
- Prisma(需要 Node.js
- bcrypt(需要 Node.js
- mongoose(需要 Node.js

替代方案

// ❌ 不能用 Prisma
import { prisma } from '@/lib/prisma';
const user = await prisma.user.findUnique({ where: { id } });

// ✅ 改用 fetch 调用 API
const res = await fetch(`${request.nextUrl.origin}/api/users/${id}`);
const user = await res.json();

// ❌ 不能用 bcrypt
import { compare } from 'bcryptjs';
const valid = await compare(password, hash);

// ✅ 改用 Web Crypto API
const encoder = new TextEncoder();
const data = encoder.encode(password);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);

12.3 基础用法

请求日志

// middleware.ts

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

export function middleware(request: NextRequest) {
  const start = Date.now();

  // 记录请求信息
  console.log(`[${new Date().toISOString()}] ${request.method} ${request.url}`);

  const response = NextResponse.next();

  // 添加自定义 Header
  response.headers.set('X-Request-Id', crypto.randomUUID());
  response.headers.set('X-Response-Time', `${Date.now() - start}ms`);

  return response;
}

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

IP 地理位置

// middleware.ts

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

export function middleware(request: NextRequest) {
  // Vercel 自动注入地理位置信息
  const country = request.geo?.country || 'Unknown';
  const city = request.geo?.city || 'Unknown';
  const region = request.geo?.region || 'Unknown';

  // 将地理信息传递给页面
  const response = NextResponse.next();
  response.headers.set('X-User-Country', country);
  response.headers.set('X-User-City', city);
  response.headers.set('X-User-Region', region);

  // 根据地理位置重定向
  if (country === 'CN') {
    return NextResponse.redirect(new URL('/zh', request.url));
  }

  return response;
}

请求重写(Rewrite)

// middleware.ts

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

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

  // 将 /old-path 重写到 /new-path(浏览器地址栏不变)
  if (pathname === '/old-path') {
    return NextResponse.rewrite(new URL('/new-path', request.url));
  }

  // 将 /blog/:slug 重写到 /articles/:slug
  if (pathname.startsWith('/blog/')) {
    const slug = pathname.replace('/blog/', '');
    return NextResponse.rewrite(new URL(`/articles/${slug}`, request.url));
  }

  return NextResponse.next();
}

条件重定向

// middleware.ts

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

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

  // 维护模式:将所有请求重定向到维护页面
  if (process.env.MAINTENANCE_MODE === 'true' && pathname !== '/maintenance') {
    return NextResponse.redirect(new URL('/maintenance', request.url));
  }

  // 旧链接重定向
  if (pathname === '/about-us') {
    return NextResponse.redirect(new URL('/about', request.url), 301); // 永久重定向
  }

  return NextResponse.next();
}

12.4 路由守卫(认证保护)

基础认证守卫

// middleware.ts

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getToken } from 'next-auth/jwt';

// 公开路由白名单
const publicRoutes = [
  '/',
  '/login',
  '/register',
  '/articles',
  '/about',
  '/api/auth',
];

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

  // 跳过静态资源
  if (
    pathname.startsWith('/_next') ||
    pathname.startsWith('/api/auth') ||
    pathname.includes('.')
  ) {
    return NextResponse.next();
  }

  // 检查是否是公开路由
  const isPublicRoute = publicRoutes.some(
    (route) => pathname === route || pathname.startsWith(route + '/')
  );

  if (isPublicRoute) {
    return NextResponse.next();
  }

  // 验证 JWT Token
  const token = await getToken({
    req: request,
    secret: process.env.AUTH_SECRET,
  });

  if (!token) {
    // 未登录,重定向到登录页
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('callbackUrl', pathname);
    return NextResponse.redirect(loginUrl);
  }

  // 已登录,继续
  return NextResponse.next();
}

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

角色权限守卫

// middleware.ts

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getToken } from 'next-auth/jwt';

// 路由权限配置
const routePermissions: Record<string, string[]> = {
  '/admin': ['admin'],
  '/admin/users': ['admin'],
  '/admin/settings': ['admin'],
  '/dashboard': ['admin', 'editor', 'user'],
  '/dashboard/articles': ['admin', 'editor'],
};

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

  // 查找匹配的权限规则
  const requiredRoles = Object.entries(routePermissions)
    .filter(([route]) => pathname === route || pathname.startsWith(route + '/'))
    .map(([, roles]) => roles);

  // 如果没有权限规则,放行
  if (requiredRoles.length === 0) {
    return NextResponse.next();
  }

  // 验证 Token
  const token = await getToken({
    req: request,
    secret: process.env.AUTH_SECRET,
  });

  if (!token) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('callbackUrl', pathname);
    return NextResponse.redirect(loginUrl);
  }

  // 检查角色
  const userRole = token.role as string;
  const hasAccess = requiredRoles.some((roles) => roles.includes(userRole));

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

  return NextResponse.next();
}

12.5 国际化(i18n)

检测用户语言

// middleware.ts

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

const locales = ['en', 'zh', 'ja', 'ko'];
const defaultLocale = 'en';

// 从 Accept-Language Header 解析首选语言
function getPreferredLocale(request: NextRequest): string {
  const acceptLanguage = request.headers.get('Accept-Language');
  if (!acceptLanguage) return defaultLocale;

  // 解析 "zh-CN,zh;q=0.9,en;q=0.8"
  const languages = acceptLanguage
    .split(',')
    .map((lang) => {
      const [code, priority] = lang.trim().split(';q=');
      return {
        code: code.split('-')[0], // "zh-CN" → "zh"
        priority: priority ? parseFloat(priority) : 1,
      };
    })
    .sort((a, b) => b.priority - a.priority);

  // 找到第一个支持的语言
  for (const { code } of languages) {
    if (locales.includes(code)) {
      return code;
    }
  }

  return defaultLocale;
}

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

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

  if (hasLocale) {
    return NextResponse.next();
  }

  // 检查 Cookie 中是否有保存的语言偏好
  const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
  const locale = cookieLocale && locales.includes(cookieLocale)
    ? cookieLocale
    : getPreferredLocale(request);

  // 重定向到带语言前缀的路径
  const url = new URL(`/${locale}${pathname}`, request.url);
  url.search = request.nextUrl.search;

  return NextResponse.redirect(url);
}

export const config = {
  matcher: [
    // 排除 API、静态文件、_next
    '/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)',
  ],
};

语言切换

// app/components/LanguageSwitcher.tsx
'use client';

import { usePathname, useRouter } from 'next/navigation';

const locales = [
  { code: 'en', name: 'English' },
  { code: 'zh', name: '中文' },
  { code: 'ja', name: '日本語' },
];

export function LanguageSwitcher() {
  const pathname = usePathname();
  const router = useRouter();

  function switchLocale(locale: string) {
    // 移除当前语言前缀
    const pathWithoutLocale = pathname.replace(/^\/[a-z]{2}/, '');

    // 设置 Cookie
    document.cookie = `NEXT_LOCALE=${locale};path=/;max-age=31536000`;

    // 重定向
    router.push(`/${locale}${pathWithoutLocale}`);
  }

  return (
    <select
      onChange={(e) => switchLocale(e.target.value)}
      className="px-2 py-1 border rounded text-sm"
    >
      {locales.map(({ code, name }) => (
        <option key={code} value={code}>
          {name}
        </option>
      ))}
    </select>
  );
}

翻译字典

// lib/i18n/dictionaries.ts

const dictionaries = {
  en: () => import('./dictionaries/en.json').then((m) => m.default),
  zh: () => import('./dictionaries/zh.json').then((m) => m.default),
  ja: () => import('./dictionaries/ja.json').then((m) => m.default),
};

export const getDictionary = async (locale: string) => {
  const loader = dictionaries[locale as keyof typeof dictionaries];
  return loader ? loader() : dictionaries.en();
};
// lib/i18n/dictionaries/zh.json
{
  "common": {
    "home": "首页",
    "about": "关于",
    "login": "登录",
    "logout": "退出"
  },
  "article": {
    "readMore": "阅读更多",
    "publishedAt": "发布于",
    "author": "作者"
  }
}

使用方式:

// app/[locale]/page.tsx

import { getDictionary } from '@/lib/i18n/dictionaries';

export default async function HomePage({
  params,
}: {
  params: { locale: string };
}) {
  const dict = await getDictionary(params.locale);

  return (
    <div>
      <h1>{dict.common.home}</h1>
      <a href={`/${params.locale}/about`}>{dict.common.about}</a>
    </div>
  );
}

12.6 A/B 测试

// middleware.ts

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

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

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

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

  if (!variant) {
    // 随机分配 A / B 组
    variant = Math.random() > 0.5 ? 'A' : 'B';

    const response = NextResponse.next();
    response.cookies.set('ab-test-variant', variant, {
      path: '/',
      maxAge: 60 * 60 * 24 * 30, // 30 天
      sameSite: 'lax',
    });

    // 将分组信息传递给页面
    response.headers.set('X-AB-Variant', variant);

    return response;
  }

  // 已有分组,直接传递
  const response = NextResponse.next();
  response.headers.set('X-AB-Variant', variant);

  return response;
}

根据分组渲染不同内容

// app/page.tsx

import { headers } from 'next/headers';

export default async function HomePage() {
  const headersList = await headers();
  const variant = headersList.get('X-AB-Variant') || 'A';

  return (
    <div>
      {variant === 'A' ? (
        // 版本 A:强调功能
        <section className="bg-blue-600 text-white py-20">
          <h1 className="text-4xl font-bold">强大的 Next.js 教程</h1>
          <p className="mt-4 text-xl">从零到生产,一站式学习</p>
          <button className="mt-8 px-6 py-3 bg-white text-blue-600 rounded-lg font-semibold">
            立即开始
          </button>
        </section>
      ) : (
        // 版本 B:强调社区
        <section className="bg-gradient-to-r from-purple-600 to-pink-600 text-white py-20">
          <h1 className="text-4xl font-bold">加入 10,000+ 开发者社区</h1>
          <p className="mt-4 text-xl">一起学习 Next.js,共同成长</p>
          <button className="mt-8 px-6 py-3 bg-white text-purple-600 rounded-lg font-semibold">
            免费加入
          </button>
        </section>
      )}
    </div>
  );
}

基于路径的 A/B 测试(Rewrite)

// middleware.ts

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

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

  if (pathname !== '/pricing') {
    return NextResponse.next();
  }

  // 检查分组
  let variant = request.cookies.get('pricing-test')?.value;

  if (!variant) {
    variant = Math.random() > 0.5 ? 'simple' : 'detailed';

    const response = NextResponse.next();
    response.cookies.set('pricing-test', variant, {
      path: '/',
      maxAge: 60 * 60 * 24 * 30,
    });

    // 重写到不同的页面
    return NextResponse.rewrite(
      new URL(`/pricing/${variant}`, request.url),
      { headers: { 'X-AB-Variant': variant } }
    );
  }

  return NextResponse.rewrite(
    new URL(`/pricing/${variant}`, request.url),
    { headers: { 'X-AB-Variant': variant } }
  );
}

12.7 请求修改与 Headers

添加安全 Headers

// middleware.ts

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

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

  // 安全 Headers
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  response.headers.set('X-XSS-Protection', '1; mode=block');

  // Content Security Policy
  response.headers.set(
    'Content-Security-Policy',
    [
      "default-src 'self'",
      "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
      "font-src 'self' data:",
      "connect-src 'self' https://api.example.com",
    ].join('; ')
  );

  return response;
}

CORS 预检处理

// middleware.ts

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

const ALLOWED_ORIGINS = [
  'http://localhost:3000',
  'https://yourdomain.com',
];

export function middleware(request: NextRequest) {
  const origin = request.headers.get('origin');
  const isAllowed = origin && ALLOWED_ORIGINS.includes(origin);

  // 处理 OPTIONS 预检请求
  if (request.method === 'OPTIONS') {
    return new NextResponse(null, {
      status: 204,
      headers: {
        'Access-Control-Allow-Origin': isAllowed ? origin : ALLOWED_ORIGINS[0],
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
        'Access-Control-Max-Age': '86400',
      },
    });
  }

  // 其他请求添加 CORS Headers
  const response = NextResponse.next();
  response.headers.set(
    'Access-Control-Allow-Origin',
    isAllowed ? origin : ALLOWED_ORIGINS[0]
  );

  return response;
}

export const config = {
  matcher: '/api/:path*',
};

请求追踪

// middleware.ts

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

export function middleware(request: NextRequest) {
  const requestId = crypto.randomUUID();
  const startTime = Date.now();

  // 将 Request ID 传递给后续处理
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('X-Request-Id', requestId);

  const response = NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  });

  // 在响应中也添加追踪信息
  response.headers.set('X-Request-Id', requestId);
  response.headers.set('X-Response-Time', `${Date.now() - startTime}ms`);

  return response;
}

12.8 Matcher 配置详解

基础 Matcher

export const config = {
  // 匹配单个路径
  matcher: '/dashboard',

  // 匹配路径及其子路径
  matcher: '/dashboard/:path*',

  // 匹配多个路径
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],

  // 排除特定路径
  matcher: '/((?!api|_next/static|_next/image).*)',
};

高级 Matcher(正则表达式)

export const config = {
  matcher: [
    // 匹配所有 /articles/:slug 路径
    '/articles/:slug*',

    // 匹配所有带语言前缀的路径
    '/:locale(en|zh|ja)/:path*',

    // 排除静态文件
    '/((?!.*\\..*|_next).*)',
  ],
};

使用 next.config.js 中的 matcher

// next.config.js

module.exports = {
  experimental: {
    middlewareClientAuth: true, // 允许 Middleware 访问客户端认证
  },
};

12.9 性能优化

减少 Middleware 执行时间

// middleware.ts

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

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

  // ❌ 避免:对所有请求执行复杂逻辑
  // const user = await fetchUserFromDatabase(request);

  // ✅ 推荐:只对需要的路径执行
  if (!pathname.startsWith('/dashboard')) {
    return NextResponse.next();
  }

  // 只在需要时执行认证检查
  const token = request.cookies.get('auth-token');
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

缓存策略

// middleware.ts

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

// 简单的内存缓存(Edge Runtime 中每个实例独立)
const cache = new Map<string, { data: any; expires: number }>();

async function getCachedData(key: string, fetcher: () => Promise<any>, ttlMs: number) {
  const cached = cache.get(key);
  if (cached && cached.expires > Date.now()) {
    return cached.data;
  }

  const data = await fetcher();
  cache.set(key, { data, expires: Date.now() + ttlMs });

  return data;
}

export async function middleware(request: NextRequest) {
  // 使用缓存减少重复请求
  const config = await getCachedData(
    'site-config',
    async () => {
      const res = await fetch(`${request.nextUrl.origin}/api/config`);
      return res.json();
    },
    60000 // 1 分钟缓存
  );

  // 使用配置...

  return NextResponse.next();
}

12.10 调试与测试

本地调试

# 启动开发服务器
npm run dev

# Middleware 日志会输出到终端
# [middleware] GET /dashboard - 200 (15ms)

使用 console.log

// middleware.ts

export function middleware(request: NextRequest) {
  console.log('=== Middleware Debug ===');
  console.log('URL:', request.url);
  console.log('Method:', request.method);
  console.log('Headers:', Object.fromEntries(request.headers));
  console.log('Cookies:', request.cookies.getAll());
  console.log('Geo:', request.geo);
  console.log('========================');

  return NextResponse.next();
}

单元测试

// __tests__/middleware.test.ts

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

describe('Middleware', () => {
  it('should redirect unauthenticated users to login', async () => {
    const request = new NextRequest('http://localhost:3000/dashboard');
    const response = await middleware(request);

    expect(response.status).toBe(307);
    expect(response.headers.get('Location')).toContain('/login');
  });

  it('should allow authenticated users', async () => {
    const request = new NextRequest('http://localhost:3000/dashboard', {
      headers: {
        cookie: 'auth-token=valid-token',
      },
    });

    const response = await middleware(request);

    expect(response.status).toBe(200);
  });
});

12.11 实战:完整的 Middleware 示例

// middleware.ts(完整版)

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getToken } from 'next-auth/jwt';

// ============ 配置 ============

const LOCALES = ['en', 'zh'];
const DEFAULT_LOCALE = 'en';

const PUBLIC_ROUTES = [
  '/',
  '/login',
  '/register',
  '/articles',
  '/about',
  '/pricing',
];

const ADMIN_ROUTES = ['/admin'];

// ============ 辅助函数 ============

function getLocale(request: NextRequest): string {
  const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
  if (cookieLocale && LOCALES.includes(cookieLocale)) {
    return cookieLocale;
  }

  const acceptLanguage = request.headers.get('Accept-Language');
  if (acceptLanguage) {
    const preferred = acceptLanguage.split(',')[0].split('-')[0];
    if (LOCALES.includes(preferred)) {
      return preferred;
    }
  }

  return DEFAULT_LOCALE;
}

function isPublicRoute(pathname: string): boolean {
  return PUBLIC_ROUTES.some(
    (route) => pathname === route || pathname.startsWith(route + '/')
  );
}

function isAdminRoute(pathname: string): boolean {
  return ADMIN_ROUTES.some(
    (route) => pathname === route || pathname.startsWith(route + '/')
  );
}

// ============ 主中间件 ============

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

  // 1. 跳过静态资源和 Next.js 内部路由
  if (
    pathname.startsWith('/_next') ||
    pathname.startsWith('/api/auth') ||
    pathname.includes('.')
  ) {
    return NextResponse.next();
  }

  // 2. 国际化处理
  const hasLocale = LOCALES.some(
    (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (!hasLocale && isPublicRoute(pathname)) {
    const locale = getLocale(request);
    const url = new URL(`/${locale}${pathname}`, request.url);
    url.search = request.nextUrl.search;
    return NextResponse.redirect(url);
  }

  // 3. 安全 Headers
  const response = NextResponse.next();
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');

  // 4. 请求追踪
  const requestId = crypto.randomUUID();
  response.headers.set('X-Request-Id', requestId);

  // 5. 认证检查(仅对非公开路由)
  if (!isPublicRoute(pathname)) {
    const token = await getToken({
      req: request,
      secret: process.env.AUTH_SECRET,
    });

    if (!token) {
      const loginUrl = new URL('/login', request.url);
      loginUrl.searchParams.set('callbackUrl', pathname);
      return NextResponse.redirect(loginUrl);
    }

    // 6. 管理员权限检查
    if (isAdminRoute(pathname)) {
      if (token.role !== 'admin') {
        return NextResponse.redirect(new URL('/403', request.url));
      }
    }
  }

  return response;
}

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

本章小结

Key Takeaways

  1. Middleware 在 Edge Runtime 上运行:不能使用 Node.js 特有 API(fs、Prisma、bcrypt)
  2. 执行时机在请求到达页面之前:适合做路由守卫、国际化、A/B 测试
  3. Matcher 配置决定哪些路由触发 Middleware:精确配置避免不必要的执行
  4. 认证守卫应该结合 getToken 使用:不要在 Middleware 中直接查数据库
  5. 性能是关键:Middleware 在每个请求上都会执行,保持逻辑简洁
  6. 可以修改请求和响应 Headers:用于安全、追踪、CORS 等

下一步

Phase 4(数据库、认证与中间件)到此全部完成!🎉

下一卷 卷 V:UI 工程化 将涵盖:

  • 第 13 章:Tailwind CSS 深度集成(主题定制、插件、暗色模式)
  • 第 14 章:组件库选型与实战(shadcn/ui、MUI、Ant Design)
  • 第 15 章:状态管理(Zustand、Jotai、Redux Toolkit)
  • 第 16 章:表单与验证(React Hook Form + Zod)

参考资料

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页