Langfuse React 组件清理实战:基于 Component Cleanup Todo-List 的六步重构工作流
【免费下载链接】langfuse🪢 Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. 🍊YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse
本篇指南以 Langfuse 仓库中.agents/skills/react-component-cleaner技能所配套的 component-cleanup-todolist.md 为核心骨架,完整讲解如何对 React 组件执行一次"严格化接口 → 清洗冗余 → 收敛复合组件 → 复核语义 → 补充 Storybook → 输出报告"的全链路清理。读者将掌握冻结 props 类型、使用sweepyCLI 完成narrow-props、把 className/style 中的颜色与尺寸类收敛为 cva 变体、以Object.assign重写复合组件等一整套可落地的非破坏性重构手法,并理解 Langfuse 前端设计系统(web/src/components/design-system)对组件接口的硬性规范。
一、Todo-List 的定位与工作纪律
Component Cleanup Todo-List 是react-component-cleaner技能(SKILL.md)执行时的操作手册:技能本体规定"做什么、按什么原则做",Todo-List 规定"按什么顺序做、每一步做完后做什么"。
1.1 前置审计与工具链
在进入 Todo-List 之前,技能要求先完整阅读 react-component-guidelines 对目标组件做一次"基线审计",审计对象仅限组件本身,不包括调用点(callsite 会在后续步骤中逐个检查)。该指南明确了组件接口的验收标准:
- 最小接口(Minimal Interface):无未使用 props;除非带来显著人体工学收益,否则不设默认值;避免可选 props;不允许存在语义冲突的 props(如同时存在
onClick与onSelect); - 显式状态(Explicit States):优先用
Pick<>而非Omit<>;用可辨识联合(discriminated union)表达互斥状态,让"不可能状态"在类型层面无法表达; - 封装(Encapsulation):除非组件本身是无样式的 headless 组件,否则不应暴露
className/styleprops; - 确定性样式(Deterministic Styling):用 cva、条件或查找表显式表达变体,避免依赖
tailwind-merge的覆盖顺序去"碰运气"。
清理全程依赖sweepyCLI(技能中固定安装v0.1.0,并锁定到指定 commit)。该 CLI 默认是交互模式,自动化场景可加--yes自动接受全部改动,加--dry-run先预览再落地——这保证了每一步修改都是可审阅、可回退的。
1.2 COMMIT 标记与格式化纪律
Todo-List 在关键步骤后都标注了COMMIT,含义是"在创建 git commit 之前,必须先跑完格式化与 lint 工具链"。需要特别遵守的纪律包括:
- 只做指令内的修改,不做任何额外编辑;拿不准时向用户确认;
- 格式化 / lint 出现问题不要手改代码,一律使用命令的 fix 变体;
- commit message 不做特殊格式要求(最终提交会被用户 squash 并审计);
- todo 文件本身不要提交,它只是执行参照物。
二、Step 1:把接口变成"严格类型"
2.1 删除未使用的 props
第一步检查组件的 props 是否全部被使用。任何未使用的 prop 直接删除并清理相关代码。
2.2 冻结宽松的字符串 props
核心检查项是:className、style、size这类被定义为string的 prop,是否可能被冻结为字符串字面量联合类型(union of string literals),且不影响现有所有调用点:
- 可以冻结:直接用
sweepy冻结该 prop,然后COMMIT; - 不可以冻结:先检查"先冻结调用点、再冻结 prop"是否可行,逐个调用点递归处理,每执行一次冻结命令就
COMMIT一次。
这一步的价值在于:把"任意字符串都能传"的宽接口收窄为"只有这几个合法值能传"的严格接口,从类型系统层面阻止非法样式值的扩散。Langfuse 设计系统对这一点有硬性要求:prop 值永远不能等于 Tailwind 类名,例如size="md"合法而size="w-5 h-5"非法(见 design-system/README.md)。
三、Step 2:清洗收窄后的接口
3.1 默认值与可选 props 的清理
在接口已收紧的基础上:
- 默认值:凡不带来"显著人体工学收益"的默认值一律删除,并更新调用点,
COMMIT; - 可选 props:凡可改为必填且不牺牲明显易用性的,改为必填并更新调用点,
COMMIT。
Langfuse 设计系统把"无默认值 / 无可选 props"列为组件最小接口的标准(react-component-guidelines/SKILL.md),这一步正是把该标准落到具体组件上。
3.2 审计 className 与 style:把"裸类"升级为"变体"
这是整个清理中最核心的样式治理环节,按顺序执行:
- 条件类上收:检查是否存在"条件性 className / style"其实应该属于组件默认类。若是,移入默认类并更新调用点,
COMMIT; - 颜色类 → 颜色变体:与颜色相关的类应定义为组件变体,命名优先选用
variant、type或level(自行判断)。把颜色类从 className / style 移入变体并更新调用点,COMMIT; - 尺寸类 → 尺寸变体:与尺寸相关的类应定义为
size变体,同样移入并更新调用点,COMMIT; - 合并重复 props:若出现两个表达同一语义的 props(如同时有
size与small),删除冗余 prop 并更新调用点,优先用sweepyCLI 替换 prop 值,COMMIT; - 上提残留类:若仅剩的违规项是"本应属于父组件"的 className / style,用
sweepy将违规类上提(lift)到父组件,COMMIT。
关于变体的关键约束(见 SKILL.md):把类移入变体时,变体必须完整拥有它改变的每一个 CSS 属性的全部类——即先移除基础类中的对应属性类,再保证每个属性恰好由一个变体分支提供,不得依赖cn、tailwind-merge、CSS 顺序或优先级来解决类冲突,优先使用穷举查找表或cva变体。
3.3 用可辨识联合合并依赖 props
检查是否存在"相互依赖的 props"可以合并为可辨识联合(discriminated union)。若可以,则合并——这一步不应导致调用点改动,也不应引发 lint 问题,COMMIT。
这与组件指南中"用可辨识联合表达意图,而非依赖可空 / 可选""让不可能状态在类型系统中无法表达"的原则(react-component-guidelines/SKILL.md)完全一致。
3.4 最后一步:narrow-props
收尾使用sweepy的narrow-props命令对接口做最终收窄,COMMIT。
四、Step 3:审计复合组件(Composite API)
4.1 先清洗成员,再评估是否折叠
如果组件属于复合 API(例如Avatar、AvatarImage、AvatarFallback这种多成员集合),必须先识别该 API 的所有公开导出成员,然后:
- 保持现有的导出与组合语法不变;
- 对每个成员依次完成 Step 1 和 Step 2,顺序为从叶子组件到根组件;
- 在进入下一个成员前,先审计并更新当前成员的调用点;
- 在所有成员都清洗完成之前,不要评估是否折叠 API。
4.2 折叠 vs 保留的决策标准
全部成员清洗完毕后,审计所有调用点,判断组合是否有意义:
应当折叠(替换为单一组件并更新全部调用点,COMMIT),当且仅当同时满足:
- 成员始终表达一个固定的领域概念;
- 调用方无法有意义地控制成员的结构、顺序或生命周期;
- 使用差异可以通过少量语义化的父级 props清晰表达;
- 折叠后仍保留行为、语义、无障碍(accessibility)、事件处理与 ref 访问。
应当保留复合 API,当调用方确实需要:
- 对 children 进行重排、省略、重复或插入;
- 配置子组件特有行为;
- 为单个成员挂接 handler 或 ref;
- 把成员当作扩展点使用;
- 折叠会导致大量使用 slots 或 render props。
4.3 单组件导出 + Object.assign
保留复合 API 时,必须遵守"一个文件只导出一个组件"的规范:通过Object.assign把子组件挂到导出组件上,使用Alert.Title、Alert.Description语法,而不是分别导出AlertTitle、AlertDescription。更新全部调用点后COMMIT。
Langfuse 设计系统的这一规范在 design-system/README.md 中有完整示例,源码中也有大量落地实现,例如 Alert.tsx 的Object.assign(AlertRoot, { Title: AlertTitle, Description: AlertDescription })、Accordion.tsx、Tabs.tsx、RadioGroup.tsx。每个文件都遵守"单文件单组件导出"的目录结构约定(文件夹名 = 组件名,见 README.md)。
五、Step 4:复核改动(位置与 HTML 语义)
审计所有改动,确认更新后的调用点在定位(positioning)与 HTML 语义上依然成立。如果发现问题,把解决方案选项呈现给用户,由用户决策,不要擅自处理。常见的复核点包括:折叠后的组件是否丢失了原有的role语义、事件冒泡行为、以及 ref 转发能力。
六、Step 5:为组件补充 Storybook 文档
若目标组件尚不存在 Storybook story,则创建之;创建前先在仓库中查找相关的技能或文档指引(Langfuse 设计系统约定每个组件目录下放置Button.stories.tsx,见 design-system/README.md)。完成后COMMIT。
七、Step 6:最终报告
全部步骤完成后,用 react-component-guidelines 对组件做二次复检,并向用户输出报告,内容包括:
- 本次所做的全部修改;
- 仍然存在的违规项(如有);
- 对每一个被更新的调用点,给出在应用中如何查看改动的详细指引(组件现在接受哪些 prop、调用处应如何书写)。
八、从 Todo-List 反观 Langfuse 设计系统源码
Todo-List 描述的"目标状态"在 Langfuse 设计系统中已经大面积落地,可作为最佳实践参照:
- 变体治理:几乎所有基础组件都用
cva定义变体,例如 Avatar.tsx 将size(sm/md/lg)与shape(circle/rounded)定义为显式变体并使用defaultVariants,Alert.tsx 的variant支持default/destructive/info/warning,size支持default/sm,并定义了actionPosition、hasIcon等派生变体; - Pick 优先:Alert.tsx 用
Pick<VariantProps<typeof alertVariants>, "actionPosition" | "size" | "variant">从 cva 变体类型中挑选公开 props,与指南中"始终优先Pick<>"的要求一致; - 复合组件 Object.assign:Alert、Accordion、Tabs、RadioGroup 全部采用"根组件 +
Object.assign挂子组件"的写法; - 不暴露 className/style:设计系统规则明确禁止
className/styleprops,禁止任意值(如#fff、12px),布尔 props 必须用is/should前缀(isLoading、shouldTruncate),布局(margin)由父组件负责、根元素不含 margin——这些正是 Todo-List Step 2 期望达成的终态。
换言之,这份 Todo-List 与其说是一次性的清理清单,不如说是"把新组件打磨到 Langfuse 设计系统水准"的验收流水线:从类型收窄、变体收敛、联合类型重构,到复合组件折叠决策与 Storybook 补全,每一步都有明确的 COMMIT 检查点,最终通过narrow-props收口,再以 react-component-guidelines 复检闭环。
【免费下载链接】langfuse🪢 Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. 🍊YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考