关于 componentize
componentize 是用于编写新 UI 前检查代码库复用机会的技能,面向需要减少重复并建立可复用组件的开发者。它强调先盘点现有组件和项目边界,再决定扩展、迁移或提取组件。
主要功能
- 01检查仓库结构与 UI 技术栈
- 02搜索重复组件和标记
- 03优先适配现有组件
- 04规划组件 API 与迁移调用点
- 05兼顾服务端与客户端边界
适用场景
- 01审计多个页面中的重复 UI
- 02实现前确定共享组件的归属与 API
- 03拆分交互组件以保持服务端边界
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ howells/ skills --skill componentize安装后在 Claude Code 中输入/componentize确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlreferences/collection-migration.mdpackage-extraction.mdscripts/find-god-files.py详细介绍
组件化
在编写新的 UI 之前,使用此技能检查代码库中可复用的机会。目标是减少重复,建立规范的共享组件,并通过 props、插槽、组合和清晰的包边界,让应用代码使用这些组件。除非用户明确要求构建设计系统项目,否则不要将整个 UI 集中管理。
开始
调用时:
- 说明你正在使用
componentize技能。 - 在编辑前确认仓库结构:
- 包管理器和工作区配置
- 应用和包
- 是否存在
turbo.json - 是否已有共享 UI 包,常见位置包括
packages/ui、packages/design-system、packages/components或libs/ui - UI 框架、样式系统、图标库、测试设置、Storybook 或示例
- 如果用户根据位置、外观或交互提及某个可见目标,先确认该目标,再进行广泛搜索。
- 创建任何内容之前先搜索。使用
rg、rg --files和导入关系,查找现有组件、重复标记、重复的 Tailwind 类字符串、复制的表单控件、重复的外壳和变体差异。 - 如果用户要求实现,先说明提取目标、规范归属位置、公共 API、需要迁移的调用点以及验证计划。然后进行有限范围的编辑并验证。如果用户要求分析,则返回一份按优先级排列的提取候选项审查报告。
核心原则
优先改造,而非新增:
- 原样复用现有组件。
- 用已有且负责该模式的组件替换重复标记或一次性本地组件。
- 为现有组件添加 prop、变体、插槽、渲染 prop 或组合扩展点。
- 从两个或更多本地实现中提取共享基础组件。
- 仅当无法合理改造任何现有组件时,才创建新的共享组件。
- 仅当复用跨越应用或包的边界,且候选 API 与领域无关时,才创建新的共享 UI 包。
不要创建仅在文案、间距、图标、颜色或细微布局上有所不同的平行组件。应将这些差异纳入同一个 API,除非这样会使组件含糊不清或过度臃肿。 如果现有组件已经符合相应的行为和无障碍约定,就将调用点迁移到该组件,而不是再提取一个组件。
原子组件标准
以最小且实用的组件边界为目标。先提取基础组件,再提取组合式产品 UI;组合组件应依赖基础组件,而不是重复其标记或类字符串。
优先采用以下层级:
- 令牌或类名辅助工具:共享间距、排版、颜色或焦点环规则
- 基础组件:Button、Input、Badge、TooltipTrigger、DialogContent
- 复合基础组件:Field、Menu、Tabs、Select、DataList
- 组合界面:Toolbar、EmptyState、PageHeader、FilterBar
- 特定应用的组合:与路由相关的外壳、业务工作流、文案繁多的面板
原子化并不意味着为了拆分而创建微型文件。组件具备单一且稳定的职责、明确的无障碍约定、不包含隐藏的领域行为,并且其 API 可复用而不会将产品假设带入其他调用点时,它就是原子的。
变体与组合约定
引入新约定前,先遵循仓库中已有的组件约定:
- 如果仓库使用 shadcn 风格的原语,语义变体和尺寸优先使用
class-variance-authority(cva)和VariantProps;合并使用者的className时使用cn/twMerge;对于按钮、链接、触发器和菜单项等多态叶组件,使用 RadixSlot/asChild。 - 如果仓库使用来自
@base-ui-components/react的 Base UI,优先采用其render属性组合模型。对于需要相同多态能力的自定义原语,使用 Base UI 的useRender和mergeProps模式,而不要自行实现asChild克隆。 - 如果仓库使用较旧的 MUI Base 风格组件,请遵循其
slots和slotProps自定义模型,不要混用 RadixSlot或 Base UIrender模式。 - 如果仓库使用
tailwind-variants、Stitches、Vanilla Extract、CSS modules、Panda 或其他成熟的变体系统,应扩展该系统,而不是只为一次提取引入cva。 - 如果没有既定模式,仅当组件在多个调用位置中确实存在语义变体或尺寸时才使用
cva。对于没有变体矩阵的一次性原语,使用简单的cn(...)类名合并。
对于多态组件:
- 仅当实现使用兼容 Slot 的库,且组件能够将属性、事件、引用、无障碍属性和数据属性安全地传递给唯一一个子元素时,才公开
asChild - 仅当仓库使用
@base-ui-components/react约定,或组件确实需要 render 属性组合时,才公开 Base UI 风格的render - 对于具有必需的原生元素专用属性、内部 DOM 结构或无障碍语义(在任意元素上都会失效)的组件,避免使用多态
- 在组件 API 中记录或标注默认元素及必需的子元素/引用行为
服务端与客户端边界
在 Next.js App Router 等 React Server Component 框架中,默认优先使用 Server Components,并将客户端边界控制在交互所需的最小范围内。
在支持 RSC 的应用中将界面组件化时:
- 找出带有
'use client'标记的文件,并在提取或移动界面前检查其导入树 - 不要仅因为某个嵌套控件需要状态、效果、浏览器 API、上下文或事件处理程序,就将大型路由、布局、外壳、表格、表单或面板转为 Client Component
- 将交互部分提取为尽可能小的 Client Component,并在由 Server Component 中渲染;Server Component 负责数据获取、仅限服务端的模块、机密信息、路由参数、静态标记和非交互式布局
- 将可序列化数据从 Server Component 传入 Client Component;允许传入受支持的 Server Function 引用,但不要跨边界传递普通回调、不受支持的类实例、仅限服务端的对象或其他不可序列化数据
- 如果交互仅涉及模态框、折叠面板、标签页状态或弹出框等外壳,可使用
children或显式插槽,将服务端渲染的界面视觉上嵌套在小型客户端包装器中 - 尽可能将提供程序和第三方纯客户端包装器放在组件树深处,而不要默认用它们包裹整个应用、布局或页面
- 保留现有的
server-only和client-only守卫导入;只有在仓库已有此约定或边界错误确实存在风险时才添加
如果服务端文件无法导入纯客户端组件而导致代码重复,不要通过将所有内容改为客户端来解决。尽可能将共用的展示原语拆分为与运行环境无关的组件,然后在其上包裹一个处理交互行为的小型 Client Component。
代码勘察
阅读代码,而不只是文件名。
视觉目标定位
当用户说“把右边那个组件化”“左侧面板”“那个标题栏”“卡片网格”或“侧边栏”之类的话时:
- 如果应用正在运行,请进行检查。优先使用环境提供的浏览器或截图工具。
- 根据屏幕位置、附近文本、角色、地标、路由或交互来识别可见区域。
- 尽可能通过检查 DOM 标签和类名,将该区域映射回源码;然后搜索特征文本、aria 标签、测试 ID、组件名、路由文件和附近的图标名。
- 如果有多个区域都可能符合,请在编辑前提出一个简短的澄清问题。
- 映射完成后,在审查或实施检查点中明确指出目标:
- 可见目标:“右侧检查器面板”
- 源码归属:
path/to/file.tsx - 候选抽取项:
InspectorPanel - 找到的消费者/调用点
不要仅凭布局方向猜测。抽取前,必须根据源码证据确认屏幕目标。
检查:
package.json、工作区文件、turbo.json、tsconfig*、构建脚本、lint 脚本、测试脚本- 应用路由/页面和功能目录
components/、ui/、design-system/、shared/、lib/和包入口- 对本地组件、包组件、shadcn/radix/headless 库、图标库和 CSS 工具的导入
- 重复的类名字符串和重复的 JSX 结构
- 现有组件的 props 和无障碍契约是否已涵盖重复的本地代码
- 组件是否接受
className、children、asChild、变体 props、尺寸 props、状态 props 和无障碍标签 - 现有技术栈是否使用
cva、tailwind-variants、RadixSlot/asChild、Base UIrender/useRender、MUI Baseslots/slotProps、CSS modules,或其他变体/组合约定 - 框架是否使用 Server Components、
'use client'指令、server-only/client-only防护导入、服务端操作、路由处理程序,或可序列化的 Server-to-Client props
实用搜索:
rg --files -g '*.{tsx,jsx,ts,js}' | rg '(^|/)(components|ui|design-system|shared|app|pages|src)/'
rg "className=['\"][^'\"]{60,}['\"]" -g '*.{tsx,jsx}'
rg 'className=\{|cn\(|clsx\(|cva\(|tv\(|data-slot=' -g '*.{tsx,jsx}'
rg '(Button|Card|Modal|Dialog|Input|Select|Badge|Avatar|Tabs|Table|Toast|Tooltip)' -g '*.{tsx,jsx}'
rg '@radix-ui|@base-ui|@mui/base|class-variance-authority|tailwind-variants|Slot|asChild|useRender|slotProps|variant' -g '*.{tsx,jsx,ts,js}'
rg "['\"]use client['\"]|['\"]use server['\"]|server-only|client-only|useState\\(|useEffect\\(|window\\.|document\\.|localStorage" -g '*.{tsx,jsx,ts,js}'
rg --files -g '*.{tsx,jsx}' | sed 's#.*/##' | sort | uniq -d
运行指定路径的搜索前,先找出源码根目录。不要假设源码目录固定不变;检查仓库,并且只搜索实际存在的目录,例如 app、apps、src、packages、libs、frontend 或 web。
要检查导入关系,先从文本搜索入手:
rg "^import .* from ['\"](\.|@|~|[A-Za-z])" -g '*.{ts,tsx,js,jsx}'
rg 'export .* from|export \{' -g 'index.{ts,tsx,js,jsx}' -g 'package.json'
仅当 madge、dependency-cruiser 或包边界检查器等图工具已安装或明显属于仓库的一部分时,才使用它们。除非用户提出要求,否则不要仅为完成审查而添加新的图工具。
Turborepo 决策
如果项目使用 Turborepo:
- 如果已有 UI 包且其用途与当前工作相符,请使用该包。
- 如果没有既定约定,优先将
packages/ui作为标准归属位置。 - 保持包导出范围精简且有明确意图。
- 让使用方应用通过包的公共 API 导入。像
@repo/ui/button这样的明确子路径导出是可以的;直接导入私有源文件则不可以。 - 如果导入跨越包边界,请补充缺失的包依赖。
创建或更改 UI 包时,请检查:
package.json中面向 JS 和类型的exports入口- 必须保留 CSS 导入时的 CSS/样式导出和
sideEffects - 使用方是否需要转译源代码、包构建产物,或直接导出 TypeScript
- Tailwind v4 源码扫描;必要时包含用于扫描包内类名的
@source - React、框架包、Radix/无头库、Tailwind 辅助工具和图标的对等依赖
- 包级示例、故事,或使用方导入冒烟测试
如果没有共享包,仅在证据表明确有跨边界复用需求时才建议创建:
- 两个或更多应用重复实现 UI 基元或布局
- 某个组件已经在多个包之间复制
- 应用代码跨应用边界导入
- 多个位置都出现了可复用设计系统的雏形
- 重复的 UI 正在妨碍无障碍、主题或交互行为保持一致
- 候选 API 与具体业务领域无关,足以服务所有已知使用方
如果复用只发生在单个应用内、重复内容较少,或 API 仍与具体产品相关,默认使用应用本地的共享文件夹。除非本地组件树已经足够庞大,以至于包边界有助于简化归属管理,否则不要为单个应用创建包。
提取目标
优先提取复用价值高、与具体业务领域耦合度低的组件:
- 基元:Button、IconButton、LinkButton、Input、Textarea、Select、Checkbox、Switch、RadioGroup、Badge、Avatar、Tooltip
- UI 容器:Card、Panel、EmptyState、ErrorState、LoadingState、Dialog、Sheet、Popover
- 数据展示:Table、DataList、Stat、MetricCard、Pagination、Tabs
- 表单:Field、FieldLabel、FieldError、FormSection、SearchInput
条件性目标:
- 布局外壳:AppShell、PageHeader、Section、Toolbar、Sidebar、Breadcrumbs
谨慎处理布局外壳,以及任何包含产品专属数据获取、身份验证、分析、功能标记、路由假设、导航状态、权限或业务文案的组件。提取其展示部分,将业务领域编排留在应用中。
组件 API 规则
共享组件应:
- 接受
className,除非有充分理由不接受 - 将布局外边距留给调用方;组件可以负责内部内边距和间距
- 为真正的语义差异提供变体,而不是为每种一次性视觉样式都提供变体;使用
cva或等效方案时,变体名称应体现语义(intent、tone、size),而不是绑定到某个调用方 - 为每个新增属性或变体找到实际调用位置差异作为依据;在有其他具体使用方需要之前,拒绝推测性的选项
- 对复杂内容优先使用
children、插槽或组合方式 - 在技术栈支持时,让基础组件保持叶子节点特性并便于插槽组合;如果图标、标签、链接或布局容器可以由调用方组合,就不要将它们固化在基础组件中
- 对封装交互或可聚焦元素的基础组件转发 ref,并展开有效的 DOM 属性
- 使用仓库现有的辅助工具(
cn、mergeProps、Slot 或等效方案)合并事件处理器、ref、className、style、data 属性和 ARIA 属性 - 保留无障碍名称、焦点状态、键盘行为和 ARIA 契约
- 除非直接使用仅限客户端的 API,否则让与环境无关的基础组件不带
'use client';对于有状态行为,应创建轻量的客户端封装,而不是将宽泛的共享组件标记为客户端组件 - 避免导入仅供应用使用的模块、路由辅助工具、环境变量或仅限服务器的代码
- 控制依赖体量;适当情况下,应用框架应设为对等依赖
不要让组件变得过度可配置,以至于掩盖了五种互不相关的设计。如果变体在概念上相互冲突,就拆分组件或提取更小的基础组件。
不要将 asChild、render 或插槽属性用作规避所有权不明问题的手段。它们用于保留有效语义并支持组合,而不是把互不相关的组件藏在同一个 API 后面。
在确认组件的导入、属性和子项在目标环境中都有效之前,不要将组件移到服务器/客户端边界的另一侧。
UI 之外的两项工作
上面的工作围绕 UI 展开。拆分过载文件和将代码提取到独立包中,也需要同样的前期摸查和同一条规则——先证明存在重复,再进行整合——因此也归入这里。
拆分巨型文件
一个文件承担太多职责,以至于改动其中一处就得通读整个文件。绝不能仅仅因为文件太长就拆分。 当文件承担多项职责、存在重复逻辑、变更原因不稳定、分支难以测试,或可以缩小公共接口时,才进行拆分。
先用扫描器找出候选文件,再逐一阅读——扫描器通过启发式规则为文件排序,最终判断仍由你来做:
python3 scripts/find-god-files.py /path/to/repo
python3 scripts/find-god-files.py /repo --min-score 20 --duplicate-window 10
python3 scripts/find-god-files.py /repo --include-tests --json
扫描器会为长度和职责信号打分,默认将得分达到 35 的文件标记出来;在默认阈值下,行数达到或超过 300 的文件仅凭长度也可能被列出。决定文件是否确实混合了多项职责之前,请检查标记原因和源代码。降低 --min-score 可扩大筛选范围。
将每个确认的候选文件归类为巨型组件(在一个文件中处理渲染、数据整形、副作用、变更、验证和子视图控制)、巨型脚本(混杂参数解析、I/O、领域逻辑、格式化和副作用)、巨型模块(非 UI,包含多项互不相关的职责),或重复代码。
然后按以下顺序拆分,一次处理一个候选文件,每完成一步就运行最小范围的相关检查:
- 纯辅助函数。
- 重复逻辑,先于迁移任何调用方。
- 状态纠缠时,先拆钩子和状态机,再拆子组件。
- JSX 较大时,先拆叶子子组件,再拆布局壳层。
- 在脚本中,将 I/O 适配器与领域逻辑分离。
测试通过前,保持公共导入稳定;之后再整理 barrel 文件和导出。
对于组件:只有在数据获取会被复用或能消除实际分支时,才将其移入钩子;将重复的 JSX 移入有明确名称的领域组件,而不是通用的 Content 或 Wrapper;将状态放在拥有相应交互的组件附近。避免创建大量浅层文件,导致理解一种小行为也必须逐个打开文件。
对于脚本:分离参数解析、配置、转换、I/O 和输出格式化;将确定性逻辑放在可通过单元测试的函数中;保持入口精简;保留退出代码和流行为。
对于重复代码:确认副本本应保持一致后,再进行整合。碰巧相似但领域含义不同的代码应保持分离;只有当共享模式确实代表同一概念时,才将其提取出来。
提取为独立包
如果目标是可发布的包或独立仓库,而不是共享文件夹,仍应遵循上述包决策,其余事项由以下内容说明。完整的验证清单见 references/package-extraction.md。
编辑前先界定范围。 纳入:组件、钩子、实用工具、类型、样式、资源、测试夹具、测试、必要的支持模块,以及展示预期 API 的示例。排除:路由、身份验证和会话代码、数据库客户端、环境访问、产品文案、分析、功能标志、无关的设计系统封装,以及用于旧导入路径的兼容垫片。如果提取过程中发现隐藏的耦合,先报告再编辑,并选择范围最小且完整的边界。
创建文件前先设计公共 API: 包名、入口点、导出符号、对等依赖与打包依赖、样式约定、资源处理方式,以及服务端/客户端运行时约束。优先采用精简的 API——因为某个内部文件存在就将其全部导出,正是包变得难以维护的原因。
对于可发布的 npm 包:应设置明确的 exports 映射、生成或引用类型、配置 files 字段;当消费者必须自行提供 React、框架和样式库时,应将它们标记为对等依赖。
迁移到独立仓库时,应先复制,并保持原内容不变,除非明确要求移除。在同一仓库内提取时,迁移范围内的调用方,并默认移除已被取代的实现。只有在用户要求或现有约定要求时,才保留有计划的分阶段迁移,并记录其归属。独立的目标仓库可以有意保持源应用不变,直到收到单独的采用请求。
未经明确批准,绝不自动发布、打标签、推送或创建版本;也绝不悄然更改许可证、发布可见性、注册表范围或包的所有权。
工作流
1. 清点
生成简明的清单:
- 现有共享 UI 的位置
- 重复模式及其出现位置
- 无需更改 API 即可替换重复代码的现有组件
- 应通过 props 调整的组件
- 应提取的组件
- 应保留在应用本地的组件
2. 选择规范归属位置
为每个候选项选择一个方案:
- 现有组件:调整 API 并迁移调用点
- 现有 UI 包:将组件移入其中或添加到其中
- 新建 UI 包:仅在达到包的必要门槛时,创建最小可行的包
- 应用内共享文件夹:仅在复用范围限于应用内部时使用
- 不采取行动:重复只是表面相似或与特定领域相关
3. 设计 API
移动文件前先定义公共组件 API:
- 组件名称和导入路径
- props 和变体,包括变体是通过
cva、tailwind-variants、CSS 模块还是现有本地辅助工具实现 - 组合/插槽模型,包括多态行为使用 Radix/shadcn
asChild、Base UIrender/useRender、MUI Baseslots/slotProps,还是不使用多态 - 服务端/客户端边界,包括组件保持服务端渲染、环境无关,还是需要一个简短的
'use client'包装器 - 样式/主题约定
- 无障碍约定
- 用于验证预期用法的示例或测试
4. 分小批次实现
每次提取一组内聚内容。立即更新导入和调用点,避免仓库中保留两个规范版本。
对于包含许多示例或调用方的集合,加载 references/collection-migration.md,以进行稳定对比、同类项迁移和覆盖检查。
仅当请求需要视觉方向与润色时才加载 chiaroscuro;重复样式属于排版问题时加载 typecase。去重和共享组件决策仍由此技能负责,其组件化工作流是本任务的主导范围。
5. 验证
运行范围最小且有实际意义的检查:
- 对修改过的 package/app 执行类型检查
- 对修改过的 package/app 执行 lint
- 运行相关测试
- 存在共享 UI 示例时,构建 Storybook/示例
- 修改 package 导出或导入时,构建使用该 package 的应用
- 修改共享 package 时,执行消费者导入冒烟测试,验证导出的 JS、类型和样式均可正常解析
- 存在可运行的应用或 Storybook 时,对受影响的页面/组件执行渲染 UI 冒烟检查
- 对对话框、选择器、标签页和按钮等交互式基础组件进行键盘/焦点检查
- 当修改移动了
'use client'边界、从服务器代码导入仅限客户端的库,或更改了 Server Components 使用的 package 入口点时,执行 bundle 或构建冒烟检查
如果无法执行检查,或检查范围过大,请明确说明已验证和未验证的内容。
输出
返回分析时,使用以下格式:
## Reuse Audit
### Existing Shared Surface
- `path`: purpose, health, import pattern
### Highest-Value Opportunities
| Priority | Pattern | Evidence | Recommendation | Why |
|---|---|---|---|---|
### Proposed Shared API
- `ComponentName`: props, variants, slots, import path
### Package Plan
- Use existing package / create `packages/ui` / keep app-local
- Export strategy
- Dependency changes
- Server/client boundary notes for RSC-aware apps
### Migration Plan
1. Extract/adapt component
2. Migrate call sites
3. Delete duplicate implementations
4. Validate
### Risks
- Coupling, accessibility, styling, or package-boundary concerns
在最高价值机会表中,证据应包含具有代表性的 file:line 引用、出现次数或调用点数量、当前归属位置,以及操作属于原样复用、调整、提取、迁移还是不采取行动。
注意事项
- 不要一开始就创建新组件;先搜索是否可以复用。
- 迁移后不要留下重复的规范实现,除非用户要求分阶段推出。
- 不要将领域行为移入共享 UI package。
- 不要为了让导入通过而放宽 package 边界。
- 将组件化与大范围重新设计分开,除非用户要求进行视觉设计改版。
- 引入组件库依赖前,先检查现有技术栈并权衡利弊。
完成检查
完成前,请确认:
- 每个新增或修改的共享组件都有明确的使用方
- 重复实现已移除,或明确说明了保留原因
- 导入使用规范的公共路径
- props/组合方式涵盖已知变体,且没有过度拟合
- 已执行验证,或已报告未验证的部分