You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 03:23:34