# @focus-reactive/payload-plugin-ab 中文文档

规范地址：https://npmcn.com/package/%40focus-reactive/payload-plugin-ab/

版本：2\.10\.1
npm 元数据更新时间：2026-10-27T09:21:10.548Z
中文译文更新时间：2026-11-15T04:36:39.475Z
软件包许可：MIT

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40focus-reactive/payload-plugin-ab
原文对照：https://npmcn.com/package/%40focus-reactive/payload-plugin-ab/?lang=original
项目主页：https://github.com/focusreactive/payload-plugins#readme
源码仓库：https://github.com/focusreactive/payload-plugins

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：185 kB，25 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- 运行依赖：0 个
- peer 依赖：next ^14\.0\.0 \|\| ^15\.0\.0 \|\| ^16\.0\.0，react ^18\.0\.0 \|\| ^19\.0\.0，payload ^3\.0\.0，@payloadcms/ui ^3\.0\.0
- 首次发布：2026-03-02T07:31:40.473Z
- 最新版本发布：2026-10-06T08:35:01.359Z
- 近 30 天下载量：2217（2026-10-15 至 2026-11-13，npm 官方下载统计）
- 已发布版本数：24（近 12 个月 24 个）

## 中文简介

有效载荷 CMS 的 A/B 测试插件

## 安装

```sh
npm install @focus-reactive/payload-plugin-ab
```

也可以使用：`pnpm add @focus-reactive/payload-plugin-ab`、`yarn add @focus-reactive/payload-plugin-ab`、`bun add @focus-reactive/payload-plugin-ab`

## README

# @ 聚焦- reactive/ payload- plugin- ab

A/B 测试插件 [有效载荷CMS](https://payloadcms.com/) v3 + 下一个.js. 直接在有效载荷管理员内部创建页面变体,分配流量百分数,并设置边缘的路由用户——没有额外的收藏,没有手动接线.

变体与原页的同名相活. 该插件注入了管理员UI,悬钩,以及一个显示Next.js中件读取到路由流量的变体而无需数据库回转.

* * *

## AI 整合提示

> 复制并粘贴此提示到您的 AI 助手( Cursor, Claude等) 中, 将插件整合到已有的 Biveload + Next.js 项目中 .

```
I want to add A/B testing to my Payload CMS v3 + Next.js project using @focus-reactive/payload-plugin-ab.

## How it works

Variants are regular documents in the same collection as the original page, distinguished by an
injected `_abVariantOf` relationship field. The plugin auto-injects:
- A Variants panel (sidebar) on original pages — lists variants, traffic % inputs, "+ Add new" button
- A "Variant of" badge (sidebar) on variant pages — links back to the original
- Hooks that maintain a variant manifest (a path-keyed JSON map) in a storage adapter
- A `POST /api/_ab/duplicate` endpoint that clones a page with slug `original--{hash}`

The manifest is read by Next.js middleware at the edge to rewrite requests to the correct variant.

## Installation

pnpm add @focus-reactive/payload-plugin-ab

## Step 1 — Register the plugin in payload.config.ts

import { abTestingPlugin } from '@focus-reactive/payload-plugin-ab'
import { payloadGlobalAdapter } from '@focus-reactive/payload-plugin-ab/adapters/payload-global'

const abAdapter = payloadGlobalAdapter({
  serverURL: process.env.NEXT_PUBLIC_SERVER_URL ?? '',
})

// Inside buildConfig({ plugins: [...] })
abTestingPlugin({
  storage: abAdapter,
  collections: {
    // Key = slug of any collection you want to A/B test
    pages: {
      generatePath: ({ doc, locale }) => {
        const slug = doc.slug as string | undefined
        if (!slug) return null
        return locale ? `/${locale}/${slug}` : `/${slug}`
      },
      // generateVariantData is optional — auto-generates:
      // { bucket: variantSlug, rewritePath: generatePath(variantDoc), passPercentage }
    },
  },
})

## Step 2 — Add Next.js middleware

import { createResolveAbRewrite } from '@focus-reactive/payload-plugin-ab/middleware'
import { payloadGlobalAdapter } from '@focus-reactive/payload-plugin-ab/adapters/payload-global'

const storage = payloadGlobalAdapter({ serverURL: process.env.NEXT_PUBLIC_SERVER_URL ?? '' })

const resolveAbRewrite = createResolveAbRewrite({
  storage,
  getBucket: (v) => v.bucket,
  getRewritePath: (v) => v.rewritePath,
  getPassPercentage: (v) => v.passPercentage,
})

export async function middleware(request) {
  const { pathname } = request.nextUrl
  const result = await resolveAbRewrite(request, pathname, pathname, pathname)
  return result ?? NextResponse.next()
}

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

## Important notes

- generatePath must return a non-null path for variant slugs (e.g. "about--4ji9") since it
  is also used to compute the variant's rewrite path when generateVariantData is omitted.
- Variant documents are hidden from the collection list view automatically.
- The slug field (and tenantField if configured) is read-only on variant documents.
- Percentage validation: the sum of all variant passPercentage values for one page cannot exceed 100.
  A Payload ValidationError is thrown on save if it would.
- For Vercel deployments, swap payloadGlobalAdapter for vercelEdgeAdapter for sub-millisecond
  manifest reads from Vercel Edge Config.
- If the project uses Payload localization, locale-aware manifest entries are written automatically
  — no extra config needed.
```

* * *

## 如何运作

变体存在 **在与原始页面相同的收藏中**数字 :

```
pages collection
├── original   slug: "about"       _abVariantOf: null
├── variant A  slug: "about--4ji9" _abVariantOf: <original id>
└── variant B  slug: "about--x2k1" _abVariantOf: <original id>
```

在每次保存或删除时,会重算 **变量显示** ——由 URL 路径键入的平版 JSON 对象——并写入存储适配器. Next.js 中间软件在边缘读取运算表,以便在任何渲染发生之前重写请求.

```
Payload Admin  →  afterChange / afterDelete hooks  →  Storage Adapter
                                                              ↓
                                         { "/about": [variantA, variantB] }
                                                              ↓
                                              Next.js Middleware (edge)
                                                              ↓
                                           Route user to original or variant
```

* * *

## 安装

```bash
pnpm add @focus-reactive/payload-plugin-ab
```

对于Vercel Edge适配器,也安装:

```bash
pnpm add @vercel/edge-config
```

**同伴依赖性 :** `payload ^3.0.0`。 。 。 . `next ^14 || ^15` 财务报告和审定财务报表 `react ^18 || ^19` 是可选的,只要求中件和分析.

* * *

## 快速启动

### 步骤1——注册插件

```ts
// payload.config.ts
import { buildConfig } from "payload";
import { abTestingPlugin } from "@focus-reactive/payload-plugin-ab";
import { payloadGlobalAdapter } from "@focus-reactive/payload-plugin-ab/adapters/payload-global";

const abAdapter = payloadGlobalAdapter({
  serverURL: process.env.NEXT_PUBLIC_SERVER_URL ?? "",
});

export default buildConfig({
  plugins: [
    abTestingPlugin({
      storage: abAdapter,
      collections: {
        pages: {
          generatePath: ({ doc, locale }) => {
            const slug = doc.slug as string | undefined;
            if (!slug) return null;
            return locale ? `/${locale}/${slug}` : `/${slug}`;
          },
        },
      },
    }),
  ],
});
```

插件注入每个配置的收藏 :

-   **备选面板** (原页边栏) — 列出输入% 的变体, 添加, 删除
-   **"变相"** 只读关系( 变体页上的边栏)
-   `_abVariantOf` 财务报告和审定财务报表 `_abPassPercentage` 字段( 隐藏在编辑器中)
-   `admin.baseListFilter` 从列表视图中隐藏变量文档
-   `beforeChange` 编号 : `afterChange` 编号 : `afterDelete` 钩子

### 交通百分比

每个变体都有自己的 `_abPassPercentage`,与变体文档一起版本: 版本草稿中包含您正在编辑的值, 已出版的版本中包含该值 显示服务。 原始页面没有存储副本.

备选案文小组的编辑百分比 `_abPendingPercentages` 虚拟 字段,没有数据库列,随原保存发送 :

-   **保存草稿** 每个变体草案的新百分比。 现场直播 流量继续使用之前发布的分数.
-   **发布** 原文中将其应用于已公布的变体。 只有百分比为 发布——一个变体的未发布内容保持未发布,并在发布时进行直播 那变体本身.
-   从未出版的变体,无论百分比如何,都不使用流量.

可变百分比必须总计为99%或更低,使原来的百分比至少为1%.

### 第2步——接通中间软件

```ts
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { createResolveAbRewrite } from "@focus-reactive/payload-plugin-ab/middleware";
import { payloadGlobalAdapter } from "@focus-reactive/payload-plugin-ab/adapters/payload-global";

const storage = payloadGlobalAdapter({
  serverURL: process.env.NEXT_PUBLIC_SERVER_URL ?? "",
});

const resolveAbRewrite = createResolveAbRewrite({
  storage,
  getBucket: (v) => v.bucket,
  getRewritePath: (v) => v.rewritePath,
  getPassPercentage: (v) => v.passPercentage,
});

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const result = await resolveAbRewrite(request, pathname, pathname, pathname);
  return result ?? NextResponse.next();
}

export const config = {
  matcher: ["/((?!_next|api|favicon.ico).*)"],
};
```

* * *

## 配置参考

### 插件选项

```ts
interface AbTestingPluginConfig<TVariantData extends object> {
  storage: StorageAdapter<TVariantData>;
  collections: Record<string, CollectionABConfig<TVariantData>>;
  enabled?: boolean; // default: true
  debug?: boolean; // show manifest Global in Payload admin. default: false
}
```

### 收藏AB 配置

```ts
interface CollectionABConfig<TVariantData = DefaultVariantData> {
  /**
   * Slug field name used to generate variant slugs: `{original}--{hash}`.
   * Default: 'slug'
   */
  slugField?: string;

  /**
   * Dot-notation path to the tenant field.
   * Scopes percentage validation per tenant; locks the field read-only on variants.
   */
  tenantField?: string;

  /**
   * Returns the URL path used as the manifest key.
   * Return null to skip this document.
   * Called once per locale when localization is enabled.
   *
   * Must also return a valid path for variant slugs (e.g. 'about--4ji9')
   * since it is used to compute rewritePath when generateVariantData is omitted.
   */
  generatePath: (args: { doc: Record<string, unknown>; locale: string | undefined }) => string | null;

  /**
   * Builds the data stored per variant in the manifest.
   * When omitted, auto-generates: { bucket: variantSlug, rewritePath, passPercentage }
   */
  generateVariantData?: (args: {
    doc: Record<string, unknown>; // parent document
    variantDoc: Record<string, unknown>; // variant document
    locale: string | undefined;
  }) => TVariantData;
}

// Default shape when generateVariantData is omitted:
type DefaultVariantData = {
  bucket: string; // variant's slug, e.g. 'about--4ji9'
  rewritePath: string; // result of generatePath(variantDoc, locale)
  passPercentage: number; // _abPassPercentage field value
};
```

### 存储加速

```ts
interface StorageAdapter<TVariantData extends object> {
  write(path: string, variants: TVariantData[], payload: Payload): Promise<void>;
  read(path: string): Promise<TVariantData[] | null>; // must be edge-compatible
  clear(path: string, payload: Payload): Promise<void>;
  createGlobal?(debug: boolean): GlobalConfig; // optional, used by payloadGlobalAdapter
}
```

执行此接口以使用任何后端( Redis, Upstash, Vercel KV等) .

* * *

## 存储适配器

### 全球绘图器

将清单存储在有效载荷 Global 中作为 JSON 字段。 中间软件通过有效载荷 REST API获取.

```ts
import { payloadGlobalAdapter } from "@focus-reactive/payload-plugin-ab/adapters/payload-global";

const storage = payloadGlobalAdapter({
  serverURL: "https://cms.example.com", // required for middleware reads
  globalSlug: "_abManifest", // default
  apiRoute: "/api", // default
});
```

Global默认隐藏于管理员. 设定 `debug: true` 在插件选项中以显示 **系统** 组合.

### 缩写

将列表存储在 Vercel Edge Config 中,用于分毫秒读取边缘.

```ts
import { vercelEdgeAdapter } from "@focus-reactive/payload-plugin-ab/adapters/vercel-edge";

const storage = vercelEdgeAdapter({
  configID: process.env.EDGE_CONFIG_ID!,
  configURL: process.env.EDGE_CONFIG!,
  vercelRestAPIAccessToken: process.env.VERCEL_REST_API_ACCESS_TOKEN!,
  teamID: process.env.VERCEL_TEAM_ID, // optional
  manifestKey: "ab-testing", // default
});
```

**必需的 vars :** `EDGE_CONFIG`, (中文(简体) ), `EDGE_CONFIG_ID`, (中文(简体) ), `VERCEL_REST_API_ACCESS_TOKEN`。 。 。 . 在 Vercel 仪表板中创建边框配置商店并安装 `@vercel/edge-config`。 。 。 .

* * *

## 中间软件

### 创建 Solve 重写

```ts
import { createResolveAbRewrite } from "@focus-reactive/payload-plugin-ab/middleware";

const resolveAbRewrite = createResolveAbRewrite({
  storage, // same adapter as the plugin
  getBucket: (v) => v.bucket, // extract bucket key
  getRewritePath: (v) => v.rewritePath, // extract internal rewrite URL
  getPassPercentage: (v) => v.passPercentage, // enables weighted routing
});

// In middleware:
const result = await resolveAbRewrite(
  request,
  pathname, // visiblePathname — used as the per-path cookie key
  pathname, // manifestKey — key to look up in the manifest
  pathname, // originalRewritePath — where to send 'original' bucket users
);
```

三个路径参数允许您解开用户所看到的,显示的密钥,以及内部重写目标——对本地语前缀有用:

```ts
const internalPath = `/${locale}${pathname}`;

await resolveAbRewrite(request, pathname, internalPath, internalPath);
```

**跑道:** 何时 `getPassPercentage` 提供,每个变体都得到其流量的百分比;其余为原始。 插件在保存上验证总和从未超过100% 。 何时 `getPassPercentage` 被省略,所有变体和原共享的概率相等.

**粘性任务 :** Bucket任务被存储在一种活于浏览器会话的pro-path cookie中,因此返回的用户总是看到相同的变体. 如果桶饼干不见了,中间的软件会从实验饼干中恢复分配;再取出一个其变体已不存在的桶.

### Cookie 系统

| 饼干 | 默认名称 | 终身 | 目的 |
| --- | --- | --- | --- |
| 弹夹 | `payload_ab_bucket_{path}` | 90天时间 | 此用户所在的桶 |
| 访客身份证 | `ab_visitor_id` | 365天(1天) | 分析的持续访问者识别符 |
| 实验 | `exp_{path}` | 90天时间 | Bucket 名称可读客户端用分析钩 |

取代名称和寿命( A)`visitorIdMaxAge`, (中文(简体) ), `expCookieMaxAge`, (中文(简体) ), `bucketCookieMaxAge`(以秒计) `cookies` 配置为 `createResolveAbRewrite`数字 :

```ts
import type { AbCookieConfig } from "@focus-reactive/payload-plugin-ab/middleware";

// Define once, share between middleware and analytics
export const abCookies: AbCookieConfig = {
  visitorIdCookieName: "my_visitor_id",
  getExpCookieName: (key) => `ab_exp_${key}`,
};
```

使用 `resolveAbCookieNames(config, experimentId)` 在服务器组件中以纯字符串方式获取 cookie 名称以传递给客户端组件 :

```ts
import { resolveAbCookieNames } from "@focus-reactive/payload-plugin-ab/middleware";

const { variantCookieName, visitorCookieName } = resolveAbCookieNames(abCookies, "/en/about");
```

* * *

## 分析一体化

挂载 `<ExperimentTracker experimentId={manifestKey} />` 每个变式 页 次 它在以后的每一个事件上 都标出三个GA4的自定义尺寸:

-   `fr_ab_experiment` ——实验id(明显密钥等. `/en/about`(中文(简体) )
-   `fr_ab_variant` - 指定桶(`original` 或变种涕丸)
-   `fr_ab_visitor_id` - 那个 `ab_visitor_id` 饼干价值

在您的 GA4 属性中注册这三个维度 。 这个 `@focus-reactive/payload-plugin-analytics` A/B 标签读取以计算 接触、转化率、升力、意义和SRM。 转换是 分析插件已存在 `lead_action` 实验页上的事件——那里 没有单独的AB转换事件.

插件自动创建 `ab-experiments` 收藏(隐藏,除非 `debug: true`(中文(简体) ) 记录每个实验 `startedAt` 在第一个变体发布.

### 使用实验跟踪器

```tsx
// app/[locale]/[slug]/page.tsx  (Server Component)
import { ExperimentTracker } from "@focus-reactive/payload-plugin-ab/analytics/client";
import { resolveAbCookieNames } from "@focus-reactive/payload-plugin-ab/middleware";

export default async function Page({ params }) {
  const experimentId = `/${params.locale}/${params.slug}`;
  const { variantCookieName, visitorCookieName } = resolveAbCookieNames(undefined, experimentId);

  return (
    <>
      {/* page content */}
      <ExperimentTracker
        experimentId={experimentId}
        variantCookieName={variantCookieName}
        visitorCookieName={visitorCookieName}
      />
    </>
  );
}
```

`ExperimentTracker` 是客户组件。 道具 :

| Prop | 类型 | 需求 | 说明 |
| --- | --- | --- | --- |
| `experimentId` | `string` | 对 | 突出密钥( URL 路径, 例如 ). `/en/about`(中文(简体) ) |
| `variantCookieName` | `string` | 没有 | 覆盖变体桶的饼干名称( 使用) `resolveAbCookieNames`(中文(简体) ) |
| `visitorCookieName` | `string` | 没有 | 覆盖访问者身份的 cookie 名称( 使用) `resolveAbCookieNames`(中文(简体) ) |

* * *

## 多用途支助

```ts
abTestingPlugin({
  storage,
  collections: {
    pages: {
      tenantField: 'tenant',  // dot-notation path to the tenant field
      generatePath: ...,
    },
  },
})
```

设定时,百分数验证为每个租户范围,租户田被锁定在变体页上只读.

* * *

## 本地化支助

不需要额外的配置 。 何时 `payload.config.localization` 设置时,插件将每个地方的每个保存上显示每个地方,并每个地方写一个单独的显示条目. `locale` 实值 `undefined` 未配置本地化时.

```ts
generatePath: ({ doc, locale }) => {
  const slug = doc.slug as string | undefined;
  if (!slug) return null;
  return locale ? `/${locale}/${slug}` : `/${slug}`; // '/en/about', '/fr/about'
};
```

* * *

## 参考文献

| 导入路径 | 出口 |
| --- | --- |
| `@focus-reactive/payload-plugin-ab` | `abTestingPlugin`, (中文(简体) ), `AbTestingPluginConfig`, (中文(简体) ), `CollectionABConfig`, (中文(简体) ), `StorageAdapter`, (中文(简体) ), `AbCookieConfig`, (中文(简体) ), `resolveAbCookieNames`, (中文(简体) ), `ResolvedAbCookieNames`, (中文(简体) ), `AB_EXPERIMENTS_SLUG`, (中文(简体) ), `AB_DIMENSION_PARAMS` |
| `@focus-reactive/payload-plugin-ab/adapters/payload-global` | `payloadGlobalAdapter` |
| `@focus-reactive/payload-plugin-ab/adapters/vercel-edge` | `vercelEdgeAdapter` |
| `@focus-reactive/payload-plugin-ab/middleware` | `createResolveAbRewrite`, (中文(简体) ), `resolveAbCookieNames`, (中文(简体) ), `AbCookieConfig`, (中文(简体) ), `ResolveAbRewriteConfig` |
| `@focus-reactive/payload-plugin-ab/analytics` | `ExperimentTracker`, (中文(简体) ), `ExperimentTrackerProps`, (中文(简体) ), `AbCookieConfig`, (中文(简体) ), `resolveAbCookieNames`, (中文(简体) ), `ResolvedAbCookieNames` |
| `@focus-reactive/payload-plugin-ab/analytics/client` | `ExperimentTracker`, (中文(简体) ), `ExperimentTrackerProps`, (中文(简体) ), `AbCookieConfig`, (中文(简体) ), `resolveAbCookieNames`, (中文(简体) ), `ResolvedAbCookieNames` |

## 常见问题

### @focus-reactive/payload-plugin-ab 是什么？

有效载荷 CMS 的 A/B 测试插件

### 如何安装 @focus-reactive/payload-plugin-ab？

在项目目录运行 npm install @focus-reactive/payload-plugin-ab，或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖：payload（^3\.0\.0）、@payloadcms/ui（^3\.0\.0）。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖，Yarn 需要手动安装。

### @focus-reactive/payload-plugin-ab 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### @focus-reactive/payload-plugin-ab 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### @focus-reactive/payload-plugin-ab 可以商用吗？

@focus-reactive/payload-plugin-ab 使用 MIT 许可。宽松许可：允许商业使用、修改和再分发，通常只需在分发时保留原版权与许可声明。以上为概括，不构成法律意见，请以许可原文为准。

### @focus-reactive/payload-plugin-ab 的下载量有多少？

根据 npm 官方下载统计，@focus-reactive/payload-plugin-ab 在 2026/10/15 – 2026/11/13 这 30 天内被下载 2,217 次，日均约 74 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入，适合横向比较热度，不能直接等同于使用人数。

### @focus-reactive/payload-plugin-ab 还在维护吗？

最新版本 2\.10\.1 发布于 2026/10/06（1 个月前）；至今共发布 24 个版本，其中过去 12 个月发布了 24 个；首次发布于 2026/03/02。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

