跳到主要内容

设计前端灰度发布系统

问题

如何设计一个完整的前端灰度发布系统?从 Feature Flag 管理、用户分桶策略、SDK 设计到 A/B 测试与数据分析,请详细说明核心模块的设计思路与关键技术实现。

答案

灰度发布系统是前端工程化的核心基础设施,它让团队能够在不重新部署的情况下安全、可控地发布新功能。一个完整的灰度发布系统涉及 Feature Flag 配置管理、用户分桶与流量分配、前端 SDK 设计、A/B 测试与数据分析、实时配置推送、回滚机制等多个关键模块。其核心价值在于将代码部署与功能发布解耦,实现"Deploy ≠ Release"。


一、需求分析

1.1 核心概念

在深入架构设计之前,先理清灰度发布领域的核心概念:

概念英文说明典型场景
Feature FlagFeature Flag / Feature Toggle通过配置开关控制功能的开启/关闭新功能开关、运营活动控制
灰度发布Gradual Rollout将新功能逐步放量给部分用户从 1% → 10% → 50% → 100%
A/B 测试A/B Testing对比实验组和对照组的指标差异按钮颜色 A vs B 的转化率对比
金丝雀发布Canary Release先向小部分用户发布,观察是否异常新版本先给 1% 用户,无异常再扩大
蓝绿部署Blue-Green Deployment两套环境交替上线,一套运行旧版一套运行新版零停机切换
核心理念

Deploy ≠ Release。代码部署到生产环境不等于功能对用户可见。通过 Feature Flag,团队可以随时控制功能的可见性,降低发布风险。

1.2 功能需求

模块功能点
Flag 管理创建/编辑/删除 Flag、多种 Flag 类型、生命周期管理
用户分桶基于用户 ID Hash、白名单、地域、设备等多维度分桶
流量控制百分比放量、定向投放、互斥/正交实验分组
A/B 测试创建实验、分组、指标采集、统计分析、P 值显著性检验
实时更新配置变更实时推送到前端 SDK,无需重新部署
回滚机制一键关闭功能、自动回滚(基于异常指标)
管理后台可视化配置、流量分配、数据看板、审计日志

1.3 非功能需求

指标目标
高可用配置服务 99.99% 可用,SDK 降级不影响主业务
低延迟Flag 求值 < 1ms,配置更新延迟 < 5s
一致性同一用户多次访问命中相同分桶(分桶一致性)
可扩展支持百万级用户、千级 Flag 同时运行
安全性配置变更需审批、操作审计、敏感 Flag 加密
关键约束

灰度发布系统必须保证分桶一致性:同一用户在同一实验中,无论何时何地访问,都应命中相同的分组。这是 A/B 测试结果可信的前提。


二、整体架构

2.1 系统架构全景

2.2 数据流转过程

架构要点
  • 管理后台配置中心分离,管理后台只负责 CRUD,配置中心负责存储、缓存和推送
  • SDK 内置本地缓存和降级策略,即使配置中心不可用也能正常工作
  • 数据分析平台独立部署,不影响 Flag 求值的性能

三、核心模块设计

3.1 Feature Flag 类型设计

Feature Flag 不仅仅是布尔开关,还需要支持多种类型以应对不同场景:

types/feature-flag.ts
/** Flag 值类型 */
type FlagValueType = 'boolean' | 'string' | 'number' | 'json';

/** Flag 基础配置 */
interface FeatureFlag {
/** Flag 唯一标识 */
key: string;
/** Flag 名称 */
name: string;
/** 描述信息 */
description: string;
/** 值类型 */
valueType: FlagValueType;
/** 默认值(Flag 未命中任何规则时返回) */
defaultValue: unknown;
/** 是否启用 */
enabled: boolean;
/** 定向规则列表(按优先级排序) */
rules: TargetingRule[];
/** 百分比放量配置 */
rollout?: RolloutConfig;
/** 所属环境 */
environment: 'development' | 'staging' | 'production';
/** 标签 */
tags: string[];
/** 创建/更新时间 */
createdAt: number;
updatedAt: number;
}

/** 定向规则 */
interface TargetingRule {
/** 规则 ID */
id: string;
/** 规则名称 */
name: string;
/** 匹配条件(AND 关系) */
conditions: Condition[];
/** 命中后返回的值 */
value: unknown;
/** 优先级(数字越小越优先) */
priority: number;
}

/** 匹配条件 */
interface Condition {
/** 用户属性名 */
attribute: string;
/** 操作符 */
operator: ConditionOperator;
/** 目标值 */
values: string[];
}

type ConditionOperator =
| 'equals' // 等于
| 'not_equals' // 不等于
| 'contains' // 包含
| 'not_contains' // 不包含
| 'in' // 在列表中
| 'not_in' // 不在列表中
| 'gt' // 大于
| 'lt' // 小于
| 'regex' // 正则匹配
| 'semver_gt' // 语义化版本大于
| 'semver_lt'; // 语义化版本小于

/** 百分比放量配置 */
interface RolloutConfig {
/** 放量百分比 0-100 */
percentage: number;
/** 分桶 key(通常是 userId) */
bucketBy: string;
/** 放量时返回的值 */
value: unknown;
}

3.2 Flag 类型使用场景

Flag 类型值类型使用场景示例
布尔开关boolean功能开/关是否展示新版导航栏
多值 FlagstringA/B/C 多组实验按钮文案:"立即购买" / "加入购物车" / "马上下单"
数值 Flagnumber参数调优列表每页展示条数:10 / 20 / 50
JSON Flagjson复杂配置新版首页布局配置对象

3.3 Flag 求值流程

Flag 求值是整个系统的核心逻辑,需要按优先级依次匹配规则:

core/evaluator.ts
/** Flag 求值引擎 */
class FlagEvaluator {
/**
* 求值入口:获取 Flag 的最终值
*/
evaluate(flag: FeatureFlag, user: UserContext): EvaluationResult {
// 1. Flag 未启用,直接返回默认值
if (!flag.enabled) {
return { value: flag.defaultValue, reason: 'FLAG_DISABLED' };
}

// 2. 按优先级遍历定向规则
const sortedRules = [...flag.rules].sort(
(a, b) => a.priority - b.priority
);

for (const rule of sortedRules) {
if (this.matchRule(rule, user)) {
return {
value: rule.value,
reason: 'RULE_MATCH',
ruleId: rule.id,
};
}
}

// 3. 检查百分比放量
if (flag.rollout) {
const bucketValue = user[flag.rollout.bucketBy] as string;
if (bucketValue && this.isInRollout(flag.key, bucketValue, flag.rollout.percentage)) {
return {
value: flag.rollout.value,
reason: 'ROLLOUT',
percentage: flag.rollout.percentage,
};
}
}

// 4. 兜底返回默认值
return { value: flag.defaultValue, reason: 'DEFAULT' };
}

/** 匹配单条规则(所有条件 AND 关系) */
private matchRule(rule: TargetingRule, user: UserContext): boolean {
return rule.conditions.every((cond) =>
this.matchCondition(cond, user)
);
}

/** 匹配单个条件 */
private matchCondition(cond: Condition, user: UserContext): boolean {
const userValue = String(user[cond.attribute] ?? '');

switch (cond.operator) {
case 'equals':
return cond.values.includes(userValue);
case 'not_equals':
return !cond.values.includes(userValue);
case 'contains':
return cond.values.some((v) => userValue.includes(v));
case 'in':
return cond.values.includes(userValue);
case 'not_in':
return !cond.values.includes(userValue);
case 'gt':
return Number(userValue) > Number(cond.values[0]);
case 'lt':
return Number(userValue) < Number(cond.values[0]);
case 'regex':
return new RegExp(cond.values[0]).test(userValue);
default:
return false;
}
}

/**
* 百分比放量判断
* 使用 MurmurHash 保证分桶一致性
*/
private isInRollout(flagKey: string, bucketValue: string, percentage: number): boolean {
const hashInput = `${flagKey}:${bucketValue}`;
const hashValue = this.murmurHash(hashInput);
// 将 hash 值映射到 0-99 的范围
const bucket = hashValue % 100;
return bucket < percentage;
}

/** MurmurHash3 - 32bit 实现 */
private murmurHash(key: string): number {
let h = 0x811c9dc5;
for (let i = 0; i < key.length; i++) {
h ^= key.charCodeAt(i);
h = Math.imul(h, 0x01000193);
}
return h >>> 0; // 转为无符号 32 位整数
}
}

/** 求值结果 */
interface EvaluationResult {
value: unknown;
reason: 'FLAG_DISABLED' | 'RULE_MATCH' | 'ROLLOUT' | 'DEFAULT';
ruleId?: string;
percentage?: number;
}

/** 用户上下文 */
interface UserContext {
userId: string;
[key: string]: string | number | boolean | undefined;
}

四、用户分桶策略

用户分桶是灰度发布系统中最关键的技术环节,直接决定了实验结果的可信度。

4.1 分桶策略对比

策略原理优点缺点适用场景
userId Hash对 userId 做 Hash 取模分布均匀、一致性好需要用户登录登录态功能灰度
白名单指定用户 ID 列表精确控制无法大规模放量内测、VIP 体验
IP 段基于客户端 IP 地址不需要登录IP 不稳定、精度差区域限制功能
地域基于 GPS/IP 定位地域满足区域化需求定位精度有限城市级灰度
设备指纹基于设备特征生成唯一 ID匿名用户可用精度和隐私问题未登录场景
Cookie/UUID分配随机 UUID 存入 Cookie简单可靠清除 Cookie 后失效通用匿名场景

4.2 一致性 Hash 分桶实现

关键要求

分桶一致性是灰度发布系统的基石。必须保证:

  1. 确定性:同一用户 + 同一实验 → 永远命中同一分桶
  2. 均匀性:用户在各分桶中的分布接近均匀
  3. 独立性:不同实验的分桶结果相互独立
core/bucket.ts
/**
* 用户分桶引擎
* 核心思路:flagKey + userId → Hash → bucket (0-9999)
* 使用万分位精度,支持 0.01% 的粒度控制
*/
class BucketEngine {
private readonly BUCKET_SIZE = 10000; // 万分位

/**
* 计算用户所在分桶
* @param flagKey - Flag 唯一标识(保证不同实验独立)
* @param bucketKey - 分桶依据(通常是 userId)
* @returns 0 到 9999 之间的整数
*/
getBucket(flagKey: string, bucketKey: string): number {
const input = `${flagKey}:${bucketKey}`;
const hash = this.murmurHash3(input);
return hash % this.BUCKET_SIZE;
}

/**
* 判断用户是否在放量范围内
* @param percentage - 放量百分比 (0-100)
*/
isInPercentage(flagKey: string, bucketKey: string, percentage: number): boolean {
const bucket = this.getBucket(flagKey, bucketKey);
// 百分比转万分位:10% → 1000
return bucket < percentage * 100;
}

/**
* A/B 测试分组
* 将用户分配到多个实验组中的一个
*/
assignGroup(
experimentKey: string,
bucketKey: string,
groups: ExperimentGroup[]
): ExperimentGroup | null {
const bucket = this.getBucket(experimentKey, bucketKey);

let accumulated = 0;
for (const group of groups) {
accumulated += group.weight * 100; // weight 是百分比
if (bucket < accumulated) {
return group;
}
}
return null; // 未分配(剩余流量)
}

/**
* MurmurHash3 - 32bit
* 选择 MurmurHash 的原因:
* 1. 分布均匀性好
* 2. 计算速度快
* 3. 碰撞率低
*/
private murmurHash3(key: string, seed: number = 0): number {
let h = seed;
const len = key.length;
const nblocks = Math.floor(len / 4);

for (let i = 0; i < nblocks; i++) {
let k =
(key.charCodeAt(i * 4) & 0xff) |
((key.charCodeAt(i * 4 + 1) & 0xff) << 8) |
((key.charCodeAt(i * 4 + 2) & 0xff) << 16) |
((key.charCodeAt(i * 4 + 3) & 0xff) << 24);

k = Math.imul(k, 0xcc9e2d51);
k = (k << 15) | (k >>> 17);
k = Math.imul(k, 0x1b873593);

h ^= k;
h = (h << 13) | (h >>> 19);
h = Math.imul(h, 5) + 0xe6546b64;
}

let k = 0;
const tailStart = nblocks * 4;
switch (len & 3) {
case 3:
k ^= (key.charCodeAt(tailStart + 2) & 0xff) << 16;
// falls through
case 2:
k ^= (key.charCodeAt(tailStart + 1) & 0xff) << 8;
// falls through
case 1:
k ^= key.charCodeAt(tailStart) & 0xff;
k = Math.imul(k, 0xcc9e2d51);
k = (k << 15) | (k >>> 17);
k = Math.imul(k, 0x1b873593);
h ^= k;
}

h ^= len;
h ^= h >>> 16;
h = Math.imul(h, 0x85ebca6b);
h ^= h >>> 13;
h = Math.imul(h, 0xc2b2ae35);
h ^= h >>> 16;

return h >>> 0;
}
}

interface ExperimentGroup {
id: string;
name: string;
/** 流量权重百分比 */
weight: number;
/** 该组对应的 Flag 值 */
value: unknown;
}

4.3 正交与互斥实验

当系统同时运行多个实验时,需要考虑实验之间的关系:

类型说明实现方式适用场景
互斥实验同一用户只能参与一个实验共享同一个流量层,按范围划分同一功能的不同方案对比
正交实验同一用户可参与多个实验不同实验使用不同的 Hash 种子不相关功能的独立测试
core/experiment-layer.ts
/**
* 实验流量层管理
* 互斥实验共享同一个层的流量,正交实验位于不同层
*/
interface TrafficLayer {
id: string;
name: string;
/** 该层下的互斥实验 */
experiments: Experiment[];
}

interface Experiment {
id: string;
name: string;
/** 在所属 layer 中的流量范围 [start, end),万分位 */
trafficRange: [number, number];
groups: ExperimentGroup[];
}

class ExperimentManager {
private layers: Map<string, TrafficLayer> = new Map();
private bucketEngine = new BucketEngine();

/**
* 分配用户到实验组
* 1. 用 layerId + userId 计算在该层的分桶
* 2. 检查分桶是否落在某个实验的流量范围内
* 3. 用 experimentId + userId 计算组内分组
*/
assignUser(layerId: string, userId: string): AssignmentResult | null {
const layer = this.layers.get(layerId);
if (!layer) return null;

// 步骤 1:在该层中计算用户分桶
const layerBucket = this.bucketEngine.getBucket(layerId, userId);

// 步骤 2:找到命中的实验
const experiment = layer.experiments.find(
(exp) =>
layerBucket >= exp.trafficRange[0] &&
layerBucket < exp.trafficRange[1]
);

if (!experiment) return null;

// 步骤 3:在实验内部分组(使用不同的 key 保证独立性)
const group = this.bucketEngine.assignGroup(
experiment.id,
userId,
experiment.groups
);

return group ? { experimentId: experiment.id, group } : null;
}
}

interface AssignmentResult {
experimentId: string;
group: ExperimentGroup;
}

五、SDK 设计

5.1 SDK 核心架构

5.2 SDK 核心实现

sdk/ff-client.ts
interface FFClientConfig {
/** 服务端 API 地址 */
serverUrl: string;
/** SDK Key(区分环境) */
sdkKey: string;
/** 用户上下文 */
user: UserContext;
/** 是否启用实时更新 */
realtime?: boolean;
/** 配置刷新间隔(毫秒),默认 60s */
refreshInterval?: number;
/** 初始化超时(毫秒),默认 5s */
initTimeout?: number;
/** 离线模式下使用的默认配置 */
defaults?: Record<string, unknown>;
/** 配置变更回调 */
onConfigChange?: (changes: FlagChange[]) => void;
}

interface FlagChange {
key: string;
oldValue: unknown;
newValue: unknown;
}

class FFClient {
private config: FFClientConfig;
private flags: Map<string, FeatureFlag> = new Map();
private evaluator: FlagEvaluator;
private cache: CacheManager;
private reporter: EventReporter;
private updater: RealtimeUpdater | null = null;
private ready: boolean = false;
private readyPromise: Promise<void>;
private listeners: Map<string, Set<(value: unknown) => void>> = new Map();

constructor(config: FFClientConfig) {
this.config = config;
this.evaluator = new FlagEvaluator();
this.cache = new CacheManager(config.sdkKey);
this.reporter = new EventReporter(config.serverUrl, config.sdkKey);

// 初始化流程
this.readyPromise = this.initialize();
}

/**
* SDK 初始化流程
* 1. 先尝试从本地缓存加载(保证离线可用)
* 2. 再从服务端拉取最新配置
* 3. 启动实时更新通道
*/
private async initialize(): Promise<void> {
// 步骤 1:加载本地缓存(毫秒级完成)
const cached = this.cache.load();
if (cached) {
this.flags = new Map(Object.entries(cached));
}

// 步骤 2:从服务端拉取最新配置(有超时保护)
try {
const remote = await this.fetchWithTimeout(
this.config.initTimeout ?? 5000
);
this.updateFlags(remote);
} catch (error) {
console.warn('[FFClient] 初始化拉取配置失败,使用缓存:', error);
// 降级使用缓存或默认值,不阻塞业务
}

// 步骤 3:启动实时更新
if (this.config.realtime !== false) {
this.updater = new RealtimeUpdater(
this.config.serverUrl,
this.config.sdkKey,
(flags) => this.updateFlags(flags)
);
this.updater.connect();
}

// 步骤 4:启动定时刷新(作为实时更新的兜底)
this.startPolling();

this.ready = true;
}

/** 等待 SDK 初始化完成 */
waitUntilReady(): Promise<void> {
return this.readyPromise;
}

/**
* 获取 Flag 布尔值
* 这是最常用的 API
*/
getBooleanFlag(key: string, defaultValue: boolean = false): boolean {
return this.getFlag(key, defaultValue) as boolean;
}

/** 获取 Flag 字符串值 */
getStringFlag(key: string, defaultValue: string = ''): string {
return this.getFlag(key, defaultValue) as string;
}

/** 获取 Flag 数值 */
getNumberFlag(key: string, defaultValue: number = 0): number {
return this.getFlag(key, defaultValue) as number;
}

/** 获取 Flag JSON 值 */
getJsonFlag<T>(key: string, defaultValue: T): T {
return this.getFlag(key, defaultValue) as T;
}

/**
* 获取 Flag 值(通用方法)
* 优先级:规则匹配 > 百分比放量 > 默认值 > 传入的 fallback
*/
private getFlag(key: string, defaultValue: unknown): unknown {
const flag = this.flags.get(key);
if (!flag) {
// Flag 不存在,使用配置的默认值或传入的默认值
return this.config.defaults?.[key] ?? defaultValue;
}

const result = this.evaluator.evaluate(flag, this.config.user);

// 上报曝光事件(异步,不阻塞)
this.reporter.trackExposure(key, result);

return result.value;
}

/** 监听特定 Flag 的变化 */
onFlagChange(key: string, callback: (value: unknown) => void): () => void {
if (!this.listeners.has(key)) {
this.listeners.set(key, new Set());
}
this.listeners.get(key)!.add(callback);

// 返回取消监听函数
return () => {
this.listeners.get(key)?.delete(callback);
};
}

/** 更新用户上下文(例如用户登录后) */
identify(user: UserContext): void {
this.config.user = { ...this.config.user, ...user };
// 用户变更后需要重新求值所有 Flag,通知监听器
this.notifyAllListeners();
}

/** 更新 Flag 配置并通知变更 */
private updateFlags(remote: Record<string, FeatureFlag>): void {
const changes: FlagChange[] = [];

for (const [key, newFlag] of Object.entries(remote)) {
const oldFlag = this.flags.get(key);
const oldValue = oldFlag
? this.evaluator.evaluate(oldFlag, this.config.user).value
: undefined;
const newValue = this.evaluator.evaluate(newFlag, this.config.user).value;

if (oldValue !== newValue) {
changes.push({ key, oldValue, newValue });
}
this.flags.set(key, newFlag);
}

// 持久化到本地缓存
this.cache.save(Object.fromEntries(this.flags));

// 通知变更
if (changes.length > 0) {
this.config.onConfigChange?.(changes);
for (const change of changes) {
this.listeners.get(change.key)?.forEach((cb) => cb(change.newValue));
}
}
}

/** 带超时的配置拉取 */
private async fetchWithTimeout(timeout: number): Promise<Record<string, FeatureFlag>> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);

try {
const response = await fetch(
`${this.config.serverUrl}/api/flags`,
{
headers: {
'Authorization': `Bearer ${this.config.sdkKey}`,
'X-User-Context': JSON.stringify(this.config.user),
},
signal: controller.signal,
}
);
return response.json();
} finally {
clearTimeout(timer);
}
}

/** 定时轮询(兜底机制) */
private startPolling(): void {
const interval = this.config.refreshInterval ?? 60000;
setInterval(async () => {
try {
const remote = await this.fetchWithTimeout(10000);
this.updateFlags(remote);
} catch {
// 轮询失败静默处理
}
}, interval);
}

/** 通知所有监听器重新求值 */
private notifyAllListeners(): void {
for (const [key, callbacks] of this.listeners) {
const flag = this.flags.get(key);
if (flag) {
const value = this.evaluator.evaluate(flag, this.config.user).value;
callbacks.forEach((cb) => cb(value));
}
}
}

/** 销毁 SDK,清理资源 */
destroy(): void {
this.updater?.disconnect();
this.reporter.flush();
this.listeners.clear();
}
}

5.3 缓存管理

sdk/cache-manager.ts
/**
* 本地缓存管理
* 保证 SDK 在网络不可用时仍能正常工作
*/
class CacheManager {
private storageKey: string;

constructor(sdkKey: string) {
this.storageKey = `ff_cache_${sdkKey}`;
}

save(flags: Record<string, FeatureFlag>): void {
try {
const data: CacheData = {
flags,
timestamp: Date.now(),
version: this.generateVersion(flags),
};
localStorage.setItem(this.storageKey, JSON.stringify(data));
} catch {
// localStorage 写入失败(容量满等),静默处理
}
}

load(): Record<string, FeatureFlag> | null {
try {
const raw = localStorage.getItem(this.storageKey);
if (!raw) return null;

const data: CacheData = JSON.parse(raw);

// 缓存有效期 24 小时,过期后丢弃
const isExpired = Date.now() - data.timestamp > 24 * 60 * 60 * 1000;
if (isExpired) {
localStorage.removeItem(this.storageKey);
return null;
}

return data.flags;
} catch {
return null;
}
}

/** 生成配置版本号,用于增量更新 */
private generateVersion(flags: Record<string, FeatureFlag>): string {
const content = JSON.stringify(flags);
let hash = 0;
for (let i = 0; i < content.length; i++) {
hash = ((hash << 5) - hash + content.charCodeAt(i)) | 0;
}
return hash.toString(36);
}
}

interface CacheData {
flags: Record<string, FeatureFlag>;
timestamp: number;
version: string;
}

5.4 实时更新

sdk/realtime-sse.ts
/**
* 基于 SSE(Server-Sent Events)的实时更新
* 优势:原生支持、自动重连、轻量
*/
class RealtimeUpdater {
private eventSource: EventSource | null = null;
private reconnectDelay = 1000;
private maxReconnectDelay = 30000;

constructor(
private serverUrl: string,
private sdkKey: string,
private onUpdate: (flags: Record<string, FeatureFlag>) => void
) {}

connect(): void {
const url = `${this.serverUrl}/api/flags/stream?sdkKey=${this.sdkKey}`;
this.eventSource = new EventSource(url);

this.eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.onUpdate(data.flags);
// 连接成功,重置重连延迟
this.reconnectDelay = 1000;
} catch (error) {
console.error('[FFClient] 解析 SSE 数据失败:', error);
}
};

this.eventSource.onerror = () => {
this.eventSource?.close();
// 指数退避重连
setTimeout(() => this.connect(), this.reconnectDelay);
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxReconnectDelay
);
};
}

disconnect(): void {
this.eventSource?.close();
this.eventSource = null;
}
}

六、前端集成

6.1 React Hook 封装

react/use-feature-flag.ts
import { useState, useEffect, useContext, createContext, useMemo } from 'react';

/** SDK Context */
const FFContext = createContext<FFClient | null>(null);

/** Provider 组件 */
function FFProvider({
client,
children,
}: {
client: FFClient;
children: React.ReactNode;
}) {
const [ready, setReady] = useState(false);

useEffect(() => {
client.waitUntilReady().then(() => setReady(true));
return () => client.destroy();
}, [client]);

if (!ready) return null; // 或展示 loading

return (
&lt;FFContext.Provider value={client}&gt;
{children}
&lt;/FFContext.Provider&gt;
);
}

/** 获取布尔 Flag */
function useFeatureFlag(key: string, defaultValue: boolean = false): boolean {
const client = useContext(FFContext);
const [value, setValue] = useState<boolean>(() =>
client?.getBooleanFlag(key, defaultValue) ?? defaultValue
);

useEffect(() => {
if (!client) return;
// 监听 Flag 变更,实现实时更新 UI
const unsubscribe = client.onFlagChange(key, (newValue) => {
setValue(newValue as boolean);
});
return unsubscribe;
}, [client, key]);

return value;
}

/** 获取字符串 Flag(A/B 测试多变体) */
function useStringFlag(key: string, defaultValue: string = ''): string {
const client = useContext(FFContext);
const [value, setValue] = useState<string>(() =>
client?.getStringFlag(key, defaultValue) ?? defaultValue
);

useEffect(() => {
if (!client) return;
const unsubscribe = client.onFlagChange(key, (newValue) => {
setValue(newValue as string);
});
return unsubscribe;
}, [client, key]);

return value;
}

/** 获取 JSON Flag */
function useJsonFlag<T>(key: string, defaultValue: T): T {
const client = useContext(FFContext);
const [value, setValue] = useState<T>(() =>
client?.getJsonFlag<T>(key, defaultValue) ?? defaultValue
);

useEffect(() => {
if (!client) return;
const unsubscribe = client.onFlagChange(key, (newValue) => {
setValue(newValue as T);
});
return unsubscribe;
}, [client, key]);

return value;
}

6.2 业务集成示例

components/Navbar.tsx
function Navbar() {
const showNewNavbar = useFeatureFlag('new-navbar-v2', false);

if (showNewNavbar) {
return &lt;NewNavbar /&gt;;
}

return &lt;OldNavbar /&gt;;
}

6.3 应用初始化

App.tsx
import { FFProvider, FFClient } from '@myorg/feature-flag-sdk';

const ffClient = new FFClient({
serverUrl: 'https://ff.example.com',
sdkKey: 'sdk-prod-xxx',
user: {
userId: getCurrentUserId(),
platform: 'web',
country: 'CN',
appVersion: '2.1.0',
},
realtime: true,
refreshInterval: 60000,
defaults: {
'new-navbar-v2': false,
'checkout-btn-text': 'buy',
},
onConfigChange: (changes) => {
console.log('[FeatureFlag] 配置变更:', changes);
},
});

function App() {
return (
&lt;FFProvider client={ffClient}&gt;
&lt;Router&gt;
&lt;Routes /&gt;
&lt;/Router&gt;
&lt;/FFProvider&gt;
);
}

七、A/B 测试

7.1 实验设计流程

7.2 数据采集与事件上报

sdk/event-reporter.ts
/** 事件类型 */
interface FFEvent {
/** 事件类型 */
type: 'exposure' | 'conversion' | 'custom';
/** Flag key */
flagKey: string;
/** 用户 ID */
userId: string;
/** Flag 求值结果 */
flagValue: unknown;
/** 求值原因 */
reason: string;
/** 实验组 ID */
groupId?: string;
/** 自定义指标名 */
metricName?: string;
/** 自定义指标值 */
metricValue?: number;
/** 时间戳 */
timestamp: number;
}

/**
* 事件上报器
* 使用批量上报 + 本地队列,减少网络请求
*/
class EventReporter {
private queue: FFEvent[] = [];
private flushTimer: ReturnType<typeof setTimeout> | null = null;
private readonly BATCH_SIZE = 50;
private readonly FLUSH_INTERVAL = 10000; // 10s
private exposureCache: Set<string> = new Set();

constructor(
private serverUrl: string,
private sdkKey: string
) {
// 页面关闭前发送剩余事件
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', () => this.flush());
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.flush();
}
});
}
}

/**
* 上报曝光事件
* 同一用户 + 同一 Flag 只上报一次(去重)
*/
trackExposure(flagKey: string, result: EvaluationResult): void {
const dedupeKey = `${flagKey}:${result.value}`;
if (this.exposureCache.has(dedupeKey)) return;
this.exposureCache.add(dedupeKey);

this.enqueue({
type: 'exposure',
flagKey,
userId: '', // 由 SDK 层填充
flagValue: result.value,
reason: result.reason,
timestamp: Date.now(),
});
}

/** 上报转化事件 */
trackConversion(flagKey: string, metricName: string, metricValue: number = 1): void {
this.enqueue({
type: 'conversion',
flagKey,
userId: '',
flagValue: null,
reason: 'CONVERSION',
metricName,
metricValue,
timestamp: Date.now(),
});
}

/** 入队列 */
private enqueue(event: FFEvent): void {
this.queue.push(event);

if (this.queue.length >= this.BATCH_SIZE) {
this.flush();
} else if (!this.flushTimer) {
this.flushTimer = setTimeout(() => this.flush(), this.FLUSH_INTERVAL);
}
}

/** 批量发送 */
flush(): void {
if (this.queue.length === 0) return;

const events = [...this.queue];
this.queue = [];

if (this.flushTimer) {
clearTimeout(this.flushTimer);
this.flushTimer = null;
}

// 使用 sendBeacon 保证页面关闭时也能发送
if (typeof navigator?.sendBeacon === 'function') {
navigator.sendBeacon(
`${this.serverUrl}/api/events`,
JSON.stringify({ events, sdkKey: this.sdkKey })
);
} else {
fetch(`${this.serverUrl}/api/events`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ events, sdkKey: this.sdkKey }),
keepalive: true,
}).catch(() => {
// 发送失败,放回队列
this.queue.unshift(...events);
});
}
}
}

7.3 统计分析

A/B 测试的核心是假设检验,判断实验组与对照组之间的指标差异是否具有统计学意义。

P 值与显著性
  • P 值:在原假设(两组无差异)成立的前提下,观察到当前或更极端结果的概率
  • P < 0.05:拒绝原假设,认为差异具有统计学显著性
  • 置信区间:真实效果大小以 95% 概率落在的范围
analytics/statistics.ts
/**
* A/B 测试统计分析
* 使用 Z 检验(大样本比例检验)
*/
interface ExperimentMetrics {
/** 样本量 */
sampleSize: number;
/** 转化数 */
conversions: number;
/** 转化率 */
conversionRate: number;
}

interface AnalysisResult {
/** 对照组指标 */
control: ExperimentMetrics;
/** 实验组指标 */
treatment: ExperimentMetrics;
/** 相对提升 */
relativeUplift: number;
/** P 值 */
pValue: number;
/** 是否显著 (P < 0.05) */
isSignificant: boolean;
/** 95% 置信区间 */
confidenceInterval: [number, number];
/** 统计功效 */
power: number;
}

function analyzeExperiment(
control: ExperimentMetrics,
treatment: ExperimentMetrics
): AnalysisResult {
const p1 = control.conversionRate;
const p2 = treatment.conversionRate;
const n1 = control.sampleSize;
const n2 = treatment.sampleSize;

// 合并比例
const pPooled = (control.conversions + treatment.conversions) / (n1 + n2);

// Z 统计量
const se = Math.sqrt(pPooled * (1 - pPooled) * (1 / n1 + 1 / n2));
const z = (p2 - p1) / se;

// P 值(双尾检验)
const pValue = 2 * (1 - normalCDF(Math.abs(z)));

// 相对提升
const relativeUplift = p1 > 0 ? (p2 - p1) / p1 : 0;

// 95% 置信区间
const seDiff = Math.sqrt((p1 * (1 - p1)) / n1 + (p2 * (1 - p2)) / n2);
const ci: [number, number] = [
(p2 - p1) - 1.96 * seDiff,
(p2 - p1) + 1.96 * seDiff,
];

return {
control,
treatment,
relativeUplift,
pValue,
isSignificant: pValue < 0.05,
confidenceInterval: ci,
power: calculatePower(p1, p2, n1, n2),
};
}

/** 标准正态分布 CDF 近似计算 */
function normalCDF(x: number): number {
const a1 = 0.254829592;
const a2 = -0.284496736;
const a3 = 1.421413741;
const a4 = -1.453152027;
const a5 = 1.061405429;
const p = 0.3275911;

const sign = x < 0 ? -1 : 1;
x = Math.abs(x) / Math.SQRT2;

const t = 1.0 / (1.0 + p * x);
const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);

return 0.5 * (1.0 + sign * y);
}

/** 统计功效计算 */
function calculatePower(
p1: number,
p2: number,
n1: number,
n2: number,
alpha: number = 0.05
): number {
const se = Math.sqrt((p1 * (1 - p1)) / n1 + (p2 * (1 - p2)) / n2);
const zAlpha = 1.96; // 对应 alpha = 0.05
const effectSize = Math.abs(p2 - p1) / se;
return 1 - normalCDF(zAlpha - effectSize);
}

7.4 最小样本量计算

在启动实验前,需要预估所需的最小样本量,以保证实验结果的可信度:

analytics/sample-size.ts
/**
* 计算 A/B 测试最小样本量
* @param baselineRate - 当前转化率(对照组预期转化率)
* @param mde - 最小可检测效应(Minimum Detectable Effect)
* @param alpha - 显著性水平,默认 0.05
* @param power - 统计功效,默认 0.8
* @returns 每组所需最小样本量
*/
function calculateMinSampleSize(
baselineRate: number,
mde: number,
alpha: number = 0.05,
power: number = 0.8
): number {
const p1 = baselineRate;
const p2 = baselineRate * (1 + mde); // 例如提升 5%

const zAlpha = getZScore(1 - alpha / 2); // 1.96
const zBeta = getZScore(power); // 0.842

const numerator = (zAlpha * Math.sqrt(2 * p1 * (1 - p1)) +
zBeta * Math.sqrt(p1 * (1 - p1) + p2 * (1 - p2))) ** 2;
const denominator = (p2 - p1) ** 2;

return Math.ceil(numerator / denominator);
}

function getZScore(p: number): number {
// 使用近似公式
if (p <= 0 || p >= 1) throw new Error('p must be between 0 and 1');
if (p === 0.5) return 0;

const t = Math.sqrt(-2 * Math.log(p < 0.5 ? p : 1 - p));
const c0 = 2.515517;
const c1 = 0.802853;
const c2 = 0.010328;
const d1 = 1.432788;
const d2 = 0.189269;
const d3 = 0.001308;

let z = t - (c0 + c1 * t + c2 * t * t) / (1 + d1 * t + d2 * t * t + d3 * t * t * t);
return p < 0.5 ? -z : z;
}

// 使用示例:
// 当前转化率 5%,希望检测出 10% 的提升
// calculateMinSampleSize(0.05, 0.10) → 每组约 31,234 个样本

八、配置管理后台

8.1 核心功能模块

8.2 Flag 生命周期管理

一个 Feature Flag 从创建到清理,经历完整的生命周期:

技术债务

Feature Flag 如果不及时清理,会导致代码中充斥大量条件判断,增加维护成本。建议:

  • 全量发布后 2 周内完成 Flag 代码清理
  • 在管理后台设置 Flag 过期提醒
  • 定期进行 Flag 审计,清理过期和废弃的 Flag

8.3 变更审批与审计

admin/audit.ts
/** 审计日志 */
interface AuditLog {
id: string;
/** 操作人 */
operator: string;
/** 操作类型 */
action: AuditAction;
/** 目标 Flag */
flagKey: string;
/** 变更前 */
before: Partial<FeatureFlag> | null;
/** 变更后 */
after: Partial<FeatureFlag> | null;
/** 操作环境 */
environment: string;
/** 操作时间 */
timestamp: number;
/** 审批人 */
approver?: string;
/** 备注 */
comment?: string;
}

type AuditAction =
| 'flag.created'
| 'flag.updated'
| 'flag.enabled'
| 'flag.disabled'
| 'flag.deleted'
| 'rollout.changed'
| 'experiment.started'
| 'experiment.stopped';

/**
* 生产环境变更需要审批
* 审批流程:
* 1. 开发者提交变更请求
* 2. 审批人(Tech Lead / 产品)审批
* 3. 审批通过后自动生效
*/
interface ChangeRequest {
id: string;
flagKey: string;
environment: 'production';
changes: Partial<FeatureFlag>;
requester: string;
status: 'pending' | 'approved' | 'rejected';
approver?: string;
createdAt: number;
resolvedAt?: number;
}

九、发布策略与回滚

9.1 常见发布策略对比

策略说明优点缺点适用场景
金丝雀发布先发给 1% 用户,逐步扩大风险最低、可及时发现问题发布周期长核心功能变更
滚动发布按批次逐步替换实例零停机、过程平滑短时间存在新旧版本共存服务端部署
蓝绿部署两套环境瞬间切换切换快、回滚快资源成本翻倍重大版本升级
Feature Flag代码已部署,功能按需开放最灵活、秒级回滚需要 Flag 管理和清理前端功能灰度

9.2 金丝雀发布流程

9.3 回滚机制

core/rollback.ts
/**
* 自动回滚机制
* 监控关键指标,异常时自动关闭 Flag
*/
interface RollbackConfig {
/** 要监控的 Flag */
flagKey: string;
/** 监控指标 */
metrics: RollbackMetric[];
/** 检查间隔(秒) */
checkInterval: number;
/** 是否启用自动回滚 */
autoRollback: boolean;
/** 回滚通知 */
notifyChannels: ('email' | 'slack' | 'webhook')[];
}

interface RollbackMetric {
/** 指标名称 */
name: string;
/** 指标类型 */
type: 'error_rate' | 'latency_p99' | 'conversion_rate' | 'custom';
/** 阈值 */
threshold: number;
/** 比较方式 */
comparator: 'gt' | 'lt'; // gt: 大于阈值触发回滚, lt: 小于阈值触发回滚
}

class AutoRollbackMonitor {
private timers: Map<string, ReturnType<typeof setInterval>> = new Map();

startMonitoring(config: RollbackConfig): void {
const timer = setInterval(async () => {
const shouldRollback = await this.checkMetrics(config);

if (shouldRollback) {
// 1. 立即关闭 Flag
await this.disableFlag(config.flagKey);
// 2. 发送告警通知
await this.notify(config);
// 3. 停止监控
this.stopMonitoring(config.flagKey);
// 4. 记录审计日志
await this.logRollback(config);
}
}, config.checkInterval * 1000);

this.timers.set(config.flagKey, timer);
}

private async checkMetrics(config: RollbackConfig): Promise<boolean> {
for (const metric of config.metrics) {
const currentValue = await this.fetchMetricValue(
config.flagKey,
metric.name
);

const triggered =
metric.comparator === 'gt'
? currentValue > metric.threshold
: currentValue < metric.threshold;

if (triggered) {
console.error(
`[AutoRollback] ${config.flagKey} 指标异常: ` +
`${metric.name}=${currentValue}, 阈值=${metric.threshold}`
);
return true;
}
}
return false;
}

/** 关闭 Flag(一键回滚) */
private async disableFlag(flagKey: string): Promise<void> {
await fetch(`/api/flags/${flagKey}/disable`, { method: 'POST' });
// 配置中心会通过 SSE 推送变更到所有前端 SDK
}

private async notify(config: RollbackConfig): Promise<void> {
for (const channel of config.notifyChannels) {
// 发送告警到对应渠道
await fetch('/api/notifications', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
channel,
message: `[自动回滚] Flag "${config.flagKey}" 因指标异常已自动关闭`,
}),
});
}
}

stopMonitoring(flagKey: string): void {
const timer = this.timers.get(flagKey);
if (timer) {
clearInterval(timer);
this.timers.delete(flagKey);
}
}

private async fetchMetricValue(flagKey: string, metricName: string): Promise<number> {
const response = await fetch(
`/api/metrics?flagKey=${flagKey}&metric=${metricName}`
);
const data = await response.json();
return data.value;
}

private async logRollback(config: RollbackConfig): Promise<void> {
await fetch('/api/audit-logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'flag.disabled',
flagKey: config.flagKey,
operator: 'system:auto-rollback',
comment: '指标异常触发自动回滚',
}),
});
}
}

十、性能优化

10.1 SDK 性能优化策略

优化点策略效果
Flag 求值预计算 + 缓存结果,避免重复求值求值耗时 < 0.1ms
网络请求初始化加载全量,后续增量更新减少 90% 网络传输
本地缓存localStorage 持久化,内存缓存加速离线可用,启动 < 5ms
事件上报批量上报 + 去重 + sendBeacon减少 HTTP 请求数
包体积Tree Shaking + 核心/插件分离Core SDK < 5KB gzipped
实时更新SSE 长连接 + 轮询兜底配置更新延迟 < 3s

10.2 求值缓存优化

core/evaluation-cache.ts
/**
* Flag 求值缓存
* 避免同一用户上下文下重复计算
*/
class EvaluationCache {
private cache: Map<string, { value: unknown; timestamp: number }> = new Map();
private readonly TTL = 5000; // 缓存 5 秒

get(flagKey: string, userHash: string): unknown | undefined {
const key = `${flagKey}:${userHash}`;
const entry = this.cache.get(key);
if (!entry) return undefined;

if (Date.now() - entry.timestamp > this.TTL) {
this.cache.delete(key);
return undefined;
}
return entry.value;
}

set(flagKey: string, userHash: string, value: unknown): void {
const key = `${flagKey}:${userHash}`;
this.cache.set(key, { value, timestamp: Date.now() });
}

/** 配置更新时清空缓存 */
invalidate(flagKey?: string): void {
if (flagKey) {
for (const key of this.cache.keys()) {
if (key.startsWith(`${flagKey}:`)) {
this.cache.delete(key);
}
}
} else {
this.cache.clear();
}
}
}

10.3 增量更新

core/incremental-update.ts
/**
* 增量更新策略
* 只传输变更的 Flag,减少网络传输
*/
interface IncrementalUpdate {
/** 服务端配置版本号 */
version: number;
/** 变更的 Flag 列表 */
changes: FlagChange[];
/** 删除的 Flag key 列表 */
deleted: string[];
}

class IncrementalUpdateManager {
private currentVersion: number = 0;

async fetchUpdates(): Promise<IncrementalUpdate | null> {
const response = await fetch(
`/api/flags/updates?since=${this.currentVersion}`
);

if (response.status === 304) {
// 无变更
return null;
}

const update: IncrementalUpdate = await response.json();
this.currentVersion = update.version;
return update;
}

applyUpdates(
currentFlags: Map<string, FeatureFlag>,
update: IncrementalUpdate
): Map<string, FeatureFlag> {
// 应用变更
for (const change of update.changes) {
currentFlags.set(change.key, change.newValue as FeatureFlag);
}

// 删除已移除的 Flag
for (const key of update.deleted) {
currentFlags.delete(key);
}

return currentFlags;
}
}

十一、主流方案对比

维度LaunchDarklyUnleashFlagsmith自建方案
类型商业 SaaS开源 + 商业开源 + 商业内部研发
价格按 MAU 计费,较贵开源免费,企业版收费开源免费,云版收费人力成本
SDK 支持25+ 语言/框架15+ 语言/框架10+ 语言/框架按需开发
实时更新SSE 流式推送轮询 + Webhook轮询 + SSE自行实现
A/B 测试内置完善基础支持基础支持自行实现
数据看板功能强大基础中等自行实现
私有部署企业版支持支持支持天然私有
适用场景大型企业、预算充足中大型企业、注重开源中小型企业定制化需求强
方案选择建议
  • 初创团队:直接用 LaunchDarkly 或 Flagsmith 云版,快速上线
  • 中型团队:部署 Unleash 开源版,按需扩展
  • 大型团队:自建系统或基于 Unleash 二次开发,满足定制化需求

常见面试问题

Q1: Feature Flag 的核心原理是什么?它解决了什么问题?

答案

Feature Flag(功能开关)的核心原理是将代码部署与功能发布解耦。通过在代码中嵌入条件判断,配合远程配置中心动态控制功能的开启/关闭,实现"Deploy ≠ Release"。

核心工作流程:

feature-flag-principle.ts
// 传统方式:部署即发布,无法控制
function renderPage() {
return &lt;NewFeature /&gt;; // 部署后所有用户立即可见
}

// Feature Flag 方式:部署和发布解耦
function renderPage() {
const showNewFeature = featureFlag.getBooleanFlag('new-feature', false);

if (showNewFeature) {
return &lt;NewFeature /&gt;; // 只有 Flag 开启时才可见
}
return &lt;OldFeature /&gt;; // Flag 关闭时展示旧版
}

Feature Flag 解决的核心问题:

问题传统方式Feature Flag 方式
发布风险全量发布,出问题影响所有用户灰度放量,逐步验证
回滚速度需要重新部署,分钟级关闭 Flag,秒级生效
功能测试只能在测试环境验证生产环境定向给内测用户
A/B 测试需要额外系统支持天然支持多变体分组
长周期开发功能分支合并困难主干开发 + Flag 控制

Feature Flag 的四种用途类型:

  1. Release Flag(发布开关):控制新功能上线节奏,短期使用
  2. Experiment Flag(实验开关):A/B 测试,中期使用
  3. Ops Flag(运维开关):运行时调整系统行为(如降级开关),长期使用
  4. Permission Flag(权限开关):控制特定用户群体的功能可见性,长期使用

Q2: 如何保证分桶的一致性?为什么不能用 Math.random()

答案

分桶一致性是指同一用户在同一实验中,无论何时何地访问,都应命中相同的分组。这是 A/B 测试可信的前提。

为什么不能用 Math.random()

bucket-consistency.ts
// 错误:使用 Math.random()
function assignGroupWrong(userId: string): 'A' | 'B' {
return Math.random() < 0.5 ? 'A' : 'B'; // 每次结果不同!
}

// 用户第一次访问可能分到 A 组,刷新页面可能分到 B 组
// 导致用户看到的功能反复变化(闪烁),且统计数据不可信

// 正确:使用确定性 Hash
function assignGroupCorrect(userId: string, experimentKey: string): 'A' | 'B' {
const hash = murmurHash3(`${experimentKey}:${userId}`);
return (hash % 100) < 50 ? 'A' : 'B'; // 同一用户永远返回相同结果
}

保证分桶一致性的三个核心原则:

原则说明实现方式
确定性相同输入永远产生相同输出Hash 函数(MurmurHash/MD5)
均匀性用户在各分桶中分布均匀选择高质量 Hash 算法
独立性不同实验的分桶互不影响Hash 输入包含 experimentKey

Hash 函数选择对比:

Hash 算法均匀性性能适用场景
MurmurHash3优秀极快首选,前端分桶标准方案
MD5优秀较慢服务端分桶
SHA-256优秀最慢安全场景
简单取模最快不推荐,分布不均匀

Q3: 灰度发布和 A/B 测试有什么区别?

答案

虽然灰度发布和 A/B 测试都涉及流量分配,但它们的目的、方法和决策依据完全不同:

维度灰度发布A/B 测试
目的安全地发布新功能,降低风险对比方案优劣,数据驱动决策
分组通常只有两组:灰度组 + 对照组可以有多个实验组(A/B/C/...)
放量方式逐步扩大(1% → 10% → 100%)固定比例,直到样本量足够
关注点错误率、崩溃率、性能指标业务指标(转化率、留存率等)
决策依据"有没有问题"(故障检测)"哪个更好"(统计显著性)
持续时间短期(确认稳定后即全量)中期(需要足够样本量)
结束方式全量或回滚基于 P 值判断后选择最优方案

实际中两者通常结合使用:

Q4: 如何设计实时配置更新?SSE 和 WebSocket 如何选择?

答案

实时配置更新的目标是让管理后台的配置变更秒级生效到所有前端客户端。主要有三种方案:

方案原理延迟可靠性实现复杂度
轮询客户端定时拉取高(取决于间隔)
SSE服务端单向推送低(秒级)中(自动重连)
WebSocket双向通信低(秒级)中(需心跳保活)

推荐方案:SSE + 轮询兜底

原因分析:

  1. Feature Flag 配置更新是典型的"服务端向客户端推送"场景,不需要双向通信,SSE 天然匹配
  2. SSE 基于 HTTP,天然支持重连,浏览器内置 EventSource API 自动处理断线重连
  3. SSE 资源消耗远低于 WebSocket,更适合大量客户端的场景
  4. 轮询作为兜底,当 SSE 不可用时(如某些代理环境),保证配置仍能更新
realtime-strategy.ts
/**
* 多层更新策略
* 优先级:SSE 实时推送 > 轮询兜底 > 本地缓存
*/
class MultiLayerUpdater {
private sseConnected = false;

constructor(
private serverUrl: string,
private sdkKey: string,
private onUpdate: (flags: Record<string, FeatureFlag>) => void
) {}

start(): void {
// 层级 1:SSE 实时推送
this.connectSSE();

// 层级 2:轮询兜底(SSE 断开时启用)
setInterval(async () => {
if (!this.sseConnected) {
const flags = await this.fetchFlags();
if (flags) this.onUpdate(flags);
}
}, 30000);
}

private connectSSE(): void {
const es = new EventSource(
`${this.serverUrl}/api/flags/stream?key=${this.sdkKey}`
);

es.onopen = () => {
this.sseConnected = true;
};

es.onmessage = (event) => {
const data = JSON.parse(event.data);
this.onUpdate(data.flags);
};

es.onerror = () => {
this.sseConnected = false;
// EventSource 会自动重连
};
}

private async fetchFlags(): Promise<Record<string, FeatureFlag> | null> {
try {
const res = await fetch(`${this.serverUrl}/api/flags`, {
headers: { Authorization: `Bearer ${this.sdkKey}` },
});
return res.json();
} catch {
return null;
}
}
}

SSE vs WebSocket 选择决策树:


相关链接