如何在next-intl中间件中结合实验需求重写URL?
解决next-intl + Next.js 实验分组URL重写方案
核心思路
先通过next-intl中间件处理语言前缀,再基于用户实验分组重写URL,同时避免循环重写、确保重写路径对应真实页面,解决404问题。
步骤1:编写中间件逻辑(middleware.ts)
import createMiddleware from 'next-intl/middleware'; import { NextRequest, NextResponse } from 'next/server'; import { getExperimentBucket } from '@/utils/experiment'; // 初始化next-intl语言中间件 const intlMiddleware = createMiddleware({ locales: ['en', 'es'], defaultLocale: 'en' }); export default async function middleware(request: NextRequest) { // 先让next-intl处理语言路由,拿到基础响应 const baseResponse = intlMiddleware(request); // 跳过已包含实验分组的路径,避免循环重写 if (/\/experiment\/(control|test)$/.test(request.nextUrl.pathname)) { return baseResponse; } // 匹配两种路径:不带语言的/experiment、带语言的/[lang]/experiment const pathMatch = request.nextUrl.pathname.match(/^\/(?:([a-z]{2})\/)?experiment$/); if (pathMatch) { const userLocale = pathMatch[1] || 'en'; // 获取用户的实验分组(control/test) const bucket = await getExperimentBucket(request); // 构造重写后的完整路径 const rewrittenPath = `/${userLocale}/experiment/${bucket}`; const targetUrl = new URL(rewrittenPath, request.url); // 保留原请求的查询参数 targetUrl.search = request.nextUrl.search; // 返回重写响应,继承next-intl的响应头信息 return NextResponse.rewrite(targetUrl, { headers: baseResponse.headers }); } return baseResponse; } // 配置中间件匹配的路径 export const config = { matcher: ['/', '/(es|en)/:path*', '/experiment'] };
步骤2:封装实验分组获取逻辑(utils/experiment.ts)
import { NextRequest } from 'next/server'; import Statsig from 'statsig-node'; // 初始化Statsig(建议在项目启动时全局初始化,这里简化处理) async function initStatsig() { if (!Statsig.getClient()) { await Statsig.initialize(process.env.STATSIG_SERVER_KEY!); } } export async function getExperimentBucket(request: NextRequest) { await initStatsig(); // 从cookie获取用户ID,无则生成匿名ID并写入cookie let userId = request.cookies.get('experiment_user_id')?.value; if (!userId) { userId = crypto.randomUUID(); } // 获取实验分组(假设实验名称为"experiment_page") const experiment = await Statsig.getExperiment(userId, 'experiment_page'); const bucket = experiment.get('bucket') as 'control' | 'test' || 'control'; // 写入用户ID到cookie,保证分组一致性 const response = new NextResponse(); response.cookies.set('experiment_user_id', userId, { httpOnly: true, secure: process.env.NODE_ENV === 'production', maxAge: 30 * 24 * 60 * 60 // 30天有效期 }); return bucket; }
步骤3:创建对应页面避免404
基于App Router结构,创建匹配重写路径的页面:
app/ [locale]/ layout.tsx // next-intl的语言布局 experiment/ [bucket]/ page.tsx // 实验页面组件,接收bucket参数
页面示例(app/[locale]/experiment/[bucket]/page.tsx):
interface PageProps { params: { locale: string; bucket: 'control' | 'test'; }; } export default function ExperimentPage({ params }: PageProps) { return ( <div> <h1>实验页面 - {params.bucket}组</h1> <p>当前语言:{params.locale}</p> </div> ); }
关键注意事项
- 避免循环重写:必须判断路径是否已包含实验分组,防止中间件反复触发。
- 分组一致性:通过cookie保存用户ID,确保同一用户每次请求拿到相同分组。
- 页面路径匹配:重写后的路径必须对应真实存在的页面组件,否则会出现404。
内容的提问来源于stack exchange,提问作者Alex Carpenter
相关产品推荐
相关产品推荐

