Carbon React Feature Flags 全面指南:从FeatureFlags.js到 v12 渐进式升级
【免费下载链接】carbonA design system built by IBM项目地址: https://gitcode.com/GitHub_Trending/carbo/carbon
本指南以 packages/react/src/internal/README.md 中关于编译期特性开关(Feature Flags)的说明为核心骨架,结合
@carbon/react与@carbon/feature-flags的源码实现,系统讲解 Carbon 特性开关的默认值定义、组件内条件渲染用法、运行时开关组件,以及贯穿 React / Sass / Web Components 三端的 v12 渐进式升级路径。读完本文,你将掌握如何基于useFeatureFlag与FeatureFlags组件在项目中有序地开启 v12 新行为,并在升级前利用 Codemod 自动化迁移。
一、Feature Flags 是什么
Carbon(IBM 开源设计系统)在@carbon/react中内置了一套Feature Flags(特性开关)机制。它的作用是让使用方在仍然停留在当前主版本的前提下,逐步、按需地启用新的组件行为与样式,而不是一次性承受整个大版本升级带来的破坏性变更。
在 React 包的内部目录下,FeatureFlags.js维护着全部编译期特性开关的默认值清单;组件源码在编译/渲染时读取这些开关,决定走旧路径还是新路径。当某个新特性开关被引入时,默认一律为false(关闭),从而保证向后兼容——这一点从源码与配置中可以明确印证:
packages/feature-flags/feature-flags.yml中除enable-v11-release外,其余开关(如enable-v12-release、enable-dialog-element、enable-presence等)默认值均为false;- 汇总各端开关清单的权威文档位于 docs/feature-flags.md,其中明确说明"除非特别指定,所有开关默认关闭"。
从底层实现看,开关的运行时载体是@carbon/feature-flags包(packages/feature-flags),它在启动时从./generated/feature-flags读取由feature-flags.yml生成的全部开关信息,并构建一个默认作用域(见 packages/feature-flags/src/index.ts):
// packages/feature-flags/src/index.ts(节选) const createDefaultScope = () => { const scope = createScope(); for (const featureFlag of featureFlagInfo) { scope.add(featureFlag.name, featureFlag.enabled); } return scope; }; export const FeatureFlags = createDefaultScope();二、最简用法:根据开关渲染不同内容
关联文档给出的核心示例,演示了如何在 React 组件中读取某个开关并做条件渲染——这是@carbon/react特性开关最直接的使用形态:
import { aFeatureFlag } from '/path/to/FeatureFlags'; // ... const MyComponent = (props) => ( <div {...props}>{aFeatureFlag ? 'foo' : 'bar'}</div> );在这个示例中,aFeatureFlag即代表 packages/react/src/internal/FeatureFlags.js 中定义的某个编译期开关常量;当它为true时渲染foo,否则渲染bar。
需要指出的是,这是文档展示的最原始形态(直接导入默认值常量)。在当前的@carbon/react架构中,更推荐、也更普遍的做法是通过useFeatureFlag(flagName)Hook 在运行时读取开关,例如 packages/react/src/components/ComboBox/ComboBox.tsx 中的实际调用:
// packages/react/src/components/ComboBox/ComboBox.tsx(节选) useFeatureFlag('enable-v12-dynamic-floating-styles') || autoAlign;useFeatureFlag由 packages/react/src/components/FeatureFlags/index.tsx 导出,它从FeatureFlagContext中取出当前作用域并查询开关状态,同时会在非生产环境下调用notifyAvailableFlag给出开发提示:
export const useFeatureFlag = (flag: string) => { const scope = useContext(FeatureFlagContext); const enabled = scope.enabled(flag); if (process.env.NODE_ENV !== 'production') { notifyAvailableFlag(flag, enabled); } return enabled; };三、运行时开关组件:FeatureFlags与作用域合并
仅靠编译期默认值,开关的粒度只能停留在"全包级"。为了让不同子树使用不同的开关组合,@carbon/react提供了FeatureFlags组件(定义于 packages/react/src/components/FeatureFlags/index.tsx),它可以包裹一段 React 子树,通过 Context 为其提供独立的开关作用域。
其使用方式如下:
import { FeatureFlags } from '@carbon/react'; <FeatureFlags enablePresence enableV12Overflowmenu> {/* 该子树内 enable-presence 与 enable-v12-overflowmenu 被打开 */} <MyApp /> </FeatureFlags>从源码结构可以梳理出以下几点关键机制:
- Prop 到开关名的映射:
PROP_TO_FLAG常量把驼峰命名的 Prop(如enableV12Release)映射为 kebab-case 的开关名(如enable-v12-release)。 - 只合并显式传入的开关:组件通过
useMemo遍历映射表,仅把值为undefined之外的 Prop 写入新作用域,避免未指定的 Prop 覆盖父级作用域的设置——这是嵌套FeatureFlags作用域能正确生效的前提。 - 作用域合并:新建作用域后调用
scope.mergeWithScope(parentScope),保证子作用域继承父级已有的开关值,子级显式指定的开关优先(见 packages/feature-flags/src/FeatureFlagScope.ts 中mergeWithScope的实现:子作用域已有的 key 不会被父级覆盖)。 flagsProp 已废弃:早期版本通过flags={{ 'enable-presence': true }}传入对象,现在该 Prop 已被标记为废弃(propTypes 中通过deprecate包装并提示运行 Codemod),应改用上述独立布尔 Prop。
配套的 Hook 有两个:
useFeatureFlag(flag):读取单个开关是否启用;useFeatureFlags():直接获取当前FeatureFlagContext的整个作用域对象,便于批量查询。
四、作用域底层:FeatureFlagScope的实现
无论是编译期默认作用域还是FeatureFlags组件创建的运行时作用域,最终都落到 packages/feature-flags/src/FeatureFlagScope.ts 的FeatureFlagScope类上。该类以Map<string, boolean>存储开关状态,并暴露以下核心方法:
| 方法 | 作用 | 关键行为 |
|---|---|---|
add(name, enabled) | 注册一个新开关 | 同名重复注册会抛出异常 |
enable(name)/disable(name) | 打开 / 关闭指定开关 | 开关不存在时抛出异常(checkForFlag) |
enabled(name) | 查询开关是否启用 | 见下方 v12 特殊逻辑 |
merge(flags) | 合并一组开关 | 同名 key 以传入值为准 |
mergeWithScope(scope) | 合并另一个作用域 | 子作用域已有 key 优先,父级不覆盖 |
值得注意的细节是enabled()中针对 v12 的特殊逻辑:
enabled(name: string) { this.checkForFlag(name); if (isV12Flag(name) && this.flags.get(v12ReleaseFlag) === true) { return true; } return this.flags.get(name) ?? false; }即:当enable-v12-release被打开时,所有enable-v12-*前缀的开关(以及enable-focus-wrap-without-sentinels这一特例)都会被自动视为开启,无需逐个设置。isV12Flag的判定规则也定义在同一文件:开关名以enable-v12-开头,或命中unprefixedV12Flags集合。
五、开关命名规范:enable-*与enable-v#-*
根据 docs/feature-flags.md 中"Feature flag naming convention"一节的说明,Carbon 的开关命名遵循严格的前缀约定,前缀本身即表明该开关所处的生命周期阶段:
enable-*:试验性开关
- 包含希望消费方测试并反馈的新特性;
- 总体稳定、不太可能变动,但可能根据反馈调整;
- 可能需要使用方做少量手动迁移或代码改动;
- 已收录进 Storybook 文档;
- 需要用户反馈以确保特性覆盖所有关切点。
enable-v#-*:已承诺给未来主版本的开关
- 当某个开关被广泛采用、或该特性被判定为高优先级时,Carbon 会将其"承诺"给某个未来主版本,并重命名为
enable-v#-*(例如enable-v12-some-feature); - 此时该开关背后的 API 或功能已冻结、不再变动;
- 计划在名称所指示的主版本中默认开启;
- 所有破坏性变更都会以
enable-v12-*开关形式在当前主版本(v11)中提供,让项目可以提前、按自己的节奏启用破坏性变更,避免升级到 v12 时一次性面对巨大变更集——理论上,如果项目在 v12 发布前已开启全部enable-v12-*开关,升级到 v12 时受影响组件无需再做任何改动。
一个开关要被"承诺"并重命名为enable-v#-*,必须满足:经过早期采用者测试、单元/AVT/VRT 测试全覆盖、Storybook 与官网文档齐备,并尽可能提供自动化迁移脚本(Codemod)。
六、v12 开关全景与逐项说明
下表汇总当前仓库中与 v12 相关的开关(数据来自 packages/feature-flags/feature-flags.yml,可用性与 Codemod 信息来自 docs/feature-flags.md):
| 开关 | 说明 | 可用端 |
|---|---|---|
enable-v12-release | 一键开启全部 v12 特性开关(并隐含开启enable-focus-wrap-without-sentinels) | React、Sass、Web Components |
enable-v12-tile-default-icons | Tile 组件渲染默认图标 | React、Web Components |
enable-v12-tile-radio-icons | RadioTile组件渲染新的单选图标 | React、Sass、Web Components |
enable-v12-overflowmenu | 使用基于Menu子组件的 v12OverflowMenu | React、Web Components |
enable-v12-dynamic-floating-styles | 为Popover、Tooltip等组件动态设置浮动样式 | React、Web Components |
enable-v12-structured-list-visible-icons | StructuredList中的图标组件始终可见 | Sass |
enable-v12-toggle-reduced-label-spacing | 缩减 Toggle 控件与标签之间的间距 | Sass、Web Components |
enable-dialog-element | 组件使用原生dialog元素 | React、Sass |
enable-enhanced-file-uploader | FileUploader提供更丰富的回调数据与更广的触发事件 | React |
enable-focus-wrap-without-sentinels | 不依赖哨兵节点的新焦点循环行为 | React |
enable-presence | 组件在关闭状态下保持卸载、打开时才挂载(Presence 机制) | React、Sass |
enable-treeview-controllable | 新的TreeView可控 API | React |
enable-tile-contrast | 改进的 Tile 对比度样式 | Sass |
两个已废弃开关需注意:enable-experimental-tile-contrast(改用enable-tile-contrast)与enable-experimental-focus-wrap-without-sentinels(改用enable-focus-wrap-without-sentinels)。
在@carbon/react中,这些开关在 packages/react/src/components/FeatureFlags/index.tsx 的FeatureFlagsProps中均有对应的独立布尔 Prop(如enableV12TileDefaultIcons、enablePresence、enableEnhancedFileUploader等),可逐项开启。真实组件代码中随处可见此类消费,例如 packages/react/src/components/ComposedModal/ComposedModal.tsx 同时查询enable-presence、enable-dialog-element与焦点循环相关开关来决定自身的渲染与行为。
七、开启 v12 后的迁移:Codemod 与注意事项
使用 Codemod 自动化迁移
Codemod 是@carbon/upgrade提供的代码自动改写脚本,可以显著降低启用新特性时的迁移成本。在干净的工作目录下,进入你的项目目录执行:
npx @carbon/upgrade migrate <codemod-name> --write例如为OverflowMenu启用 v12 行为:
npx @carbon/upgrade migrate enable-v12-overflowmenu --write执行后可通过git中的未暂存文件变更来审阅改动。v12 相关的 Codemod 默认针对React 源码,Web Components 与 Sass 的迁移目前需要手动完成。
并非所有开关都配有 Codemod,常见原因包括:尚未编写、开关仅作用于.scss文件(目前不提供此类 Codemod)、或开关保护的新行为本就不需要重构代码。
迁移评估与文档指引
开启enable-v12-release后,建议结合 docs/migration/v12.md 检查各包的变更范围、跨包关系,以及从 v11 迁移应用时需要重点审视的区域。此外,部分组件在 Storybook 中设有独立的Feature flags文件夹(如 FileUploader.featureflag.mdx、ComposedModal.featureflag.mdx、Toggletip.featureflag.stories.js 等),这些页面演示了开关开启前后的行为差异,可作为直观参考;但开关的清单、可用端与 Codemod 关联,仍以 docs/feature-flags.md 为权威来源。
另需注意:一批源自carbon-for-ibm-products的常用组件正在并入@carbon/react(v12 计划的一部分),它们不会在发布的 v11 包中提供,开启enable-v12-release也不会提前暴露它们——这些组件将在 v12 发布时成为@carbon/react公共 API 的一部分。
八、写在最后
Carbon 的 Feature Flags 机制是一条设计精良的渐进式升级通道:从 packages/react/src/internal/README.md 中"按开关条件渲染"的最小用法出发,向上是useFeatureFlag/FeatureFlags组件构成的运行时作用域体系(packages/react/src/components/FeatureFlags/index.tsx),向下是@carbon/feature-flags中以Map为核心的FeatureFlagScope(packages/feature-flags/src/FeatureFlagScope.ts),最终由feature-flags.yml统一登记并生成默认值(packages/feature-flags/feature-flags.yml)。
对实际项目而言,推荐的接入路径是:先在 Storybook 对应组件的 Feature flags 页面观察开关效果,再通过FeatureFlags组件或独立 Prop 在局部开启验证,确认无回归后用 Codemod 完成代码迁移,最后借助enable-v12-release做一次全量开关验证,平滑过渡到 v12。
【免费下载链接】carbonA design system built by IBM项目地址: https://gitcode.com/GitHub_Trending/carbo/carbon
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考