Featured image of post Feature Flags 运行时开关实践

Feature Flags 运行时开关实践

为什么需要 Feature Flags

Feature Flags 解决几个问题:

  1. 环境隔离:开发环境不需要 Turnstile 验证码,生产环境需要
  2. 渐进发布:新功能先对部分用户开放
  3. 快速回滚:出问题时关闭 Flag,不需要回滚代码

实现方式

RuiTool AI 的 Feature Flags 基于环境变量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/flags.ts

import "server-only";
import { cache } from "react";
import { isTestMode } from "@/utils/is-test-mode";

export async function isGoogleSSOEnabled() {
  return Boolean(
    process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
  );
}

export async function isTurnstileEnabled() {
  if (isTestMode()) {
    return false;
  }

  return Boolean(process.env.TURNSTILE_SECRET_KEY);
}

// 集中导出的配置对象
export const getConfig = cache(async () => {
  return {
    isGoogleSSOEnabled: await isGoogleSSOEnabled(),
    isTurnstileEnabled: await isTurnstileEnabled(),
  };
});

设计原则:

  • 每个 Flag 一个函数:语义清晰,调用方一眼就知道在检查什么
  • test mode 自动跳过:测试环境不触发外部依赖的 Flag
  • React cache:同一请求内多次调用只执行一次

项目中的 Flags

Flag用途条件
isTurnstileEnabled验证码开关TURNSTILE_SECRET_KEY 且非测试模式
isGoogleSSOEnabledGoogle 登录GOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRET
DISABLE_CREDIT_BILLING_SYSTEM积分系统配置项,可关闭整个积分/计费模块

使用示例

1
2
3
4
5
6
7
// 在 Server Action 中
if (await isTurnstileEnabled()) {
  const success = await validateTurnstileToken(input.captchaToken);
  if (!success) {
    throw new ActionError("INPUT_PARSE_ERROR", "Please complete the captcha");
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 在前端组件中
export async function SignInPage() {
  const config = await getConfig();

  return (
    <div>
      {/* 密码登录 */}
      <SignInForm />

      {/* 只在启用 Google SSO 时显示 */}
      {config.isGoogleSSOEnabled && <GoogleSignInButton />}
    </div>
  );
}

配置缓存

getConfigReact.cache() 包裹,同一请求内多次调用不会重复读取环境变量:

1
2
3
4
5
6
export const getConfig = cache(async () => {
  return {
    isGoogleSSOEnabled: await isGoogleSSOEnabled(),
    isTurnstileEnabled: await isTurnstileEnabled(),
  };
});

在 Server Component 中,可以在页面顶部获取一次 config,然后传给子组件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
export default async function Page() {
  const config = await getConfig();

  return (
    <>
      <Header config={config} />
      <Content config={config} />
    </>
  );
}

总结

  • 基于环境变量的 Feature Flags,简单可靠
  • 每个 Flag 独立函数,语义清晰
  • React.cache() 避免重复读取
  • 测试模式自动跳过外部依赖的 Flag
使用 Hugo 构建
主题 StackJimmy 设计