1. 从“立正请站好”说起一个被误读的组件复用真相“立正请站好”这五个字乍看像军训口令实则是得物技术团队内部对某类高复用性前端组件的戏称——不是因为它们多严肃而是因为它们一旦被接入整个业务模块就自动“站直了”UI一致、逻辑收敛、行为可控连埋点和错误监控都自动对齐。这个标题背后藏着的不是一句俏皮话而是一套在超大规模电商中跑通的组件复用工程化方法论。它不讲“怎么写一个按钮”而是直击灵魂当一个按钮组件被37个业务线、214个页面、89种主题变体反复调用时你靠文档、靠口头约定、靠PR Review来保证一致性那不是工程那是玄学。我做过三轮大型中台组件体系重构最深的体会是组件复用率每提升10%前端交付节奏就提速15%但维护成本会先飙升30%再陡降60%——这个拐点就是工程化介入的临界时刻。得物这篇实践之所以值得深挖是因为它跳出了“组件库Storybook”的舒适区把复用这件事拆解成了可测量、可追踪、可治理的工程动作。关键词里反复出现的Skill、AGENTS.md、Hook、monorepo不是技术名词堆砌而是四根承重柱Skill是能力封装的最小单元AGENTS.md是能力注册与契约声明Hook是运行时动态注入的胶水层monorepo是物理隔离与逻辑协同的底座。它们共同回答了一个问题如何让一个组件在保持自身独立演进的同时又能像乐高积木一样被任意业务方“零认知成本”地拼装、定制、观测这和市面上常见的“组件复用”文章有本质区别。别人讲“怎么抽离公共组件”我们讲“怎么让业务方忘记自己在用组件”别人讲“如何设计通用API”我们讲“如何让API契约失效时自动告警”别人讲“monorepo怎么管理包”我们讲“monorepo里一个组件的commit如何触发12个下游业务的自动化回归”。所以这不是一篇教你写代码的教程而是一份在真实高压场景下用工程手段驯服复杂性的实战手记。如果你正被“组件改一处全站崩一片”折磨或者团队里总有人问“这个按钮样式为啥在A页是圆角在B页是直角”那你需要的不是新组件而是这套让组件真正“立正站好”的纪律。2. Skill不是功能模块而是可编排的能力原子在得物的技术语境里“Skill”这个词被赋予了明确的工程定义它是一个具备完整生命周期、独立测试边界、契约化输入输出、且能被动态发现与加载的最小可复用能力单元。注意这里刻意避开了“组件”Component一词——因为Skill可以是UI组件如商品卡片也可以是无UI的逻辑模块如地址解析器甚至可以是数据聚合服务如跨域价格比对。它的核心价值不在于“画什么”而在于“能做什么”以及“怎么被安全地调用”。为什么非得造一个新词因为传统组件库的抽象层级太低。一个Button组件暴露size、type、onClick三个props业务方传入sizelarge但没人规定large具体对应多少像素、是否响应式缩放、在暗色模式下是否需要调整阴影。这种模糊性在单个业务线内尚可容忍一旦跨团队复用就成了“样式战争”的导火索。Skill则强制要求所有可配置项必须通过显式契约声明并附带默认值、类型约束、取值范围及变更影响说明。这直接体现在AGENTS.md文件中。以得物实际落地的PriceDisplaySkill为例其AGENTS.md片段如下# PriceDisplaySkill ## 能力描述 标准化商品价格展示与格式化支持多币种、促销标签、划线价、会员价等复合场景。 ## 输入契约 (Input Contract) | 字段名 | 类型 | 必填 | 默认值 | 取值范围 | 说明 | |--------|------|------|--------|----------|------| | price | number | 是 | - | 0 | 原始价格分 | | currency | string | 否 | CNY | CNY, USD, HKD | 币种代码影响符号与小数位 | | showPromotionTag | boolean | 否 | true | true/false | 是否显示“促销中”标签 | | promotionLevel | number | 否 | 1 | 1-3 | 促销强度等级影响标签颜色与文案 | ## 输出契约 (Output Contract) - 渲染一个包含价格主文案、币种符号、促销标签的DOM节点 - 触发 price:display:rendered 自定义事件携带格式化后的字符串值 - 在控制台输出 DEBUG: PriceDisplaySkill rendered with {price: 19900, currency: CNY}仅开发环境 ## 兼容性 - 支持 React 17 / Vue 3.x - 不依赖任何全局状态管理库 - CSS-in-JS 方案使用 emotion/styled无外部CSS依赖这份文档的价值远超一份说明书。它是技能的“身份证”和“责任状”对业务方看到promotionLevel: 1-3立刻明白不能传4否则行为未定义看到DEBUG日志说明就知道上线后如何快速定位渲染异常对Skill维护者兼容性字段锁定了技术栈边界避免因升级React版本导致下游崩溃输出契约中的自定义事件为业务方提供了无需修改Skill源码即可监听渲染完成的通道对工程平台这份MD文件可被自动化工具扫描生成TypeScript接口定义、Storybook参数控件、甚至CI阶段的契约合规性检查脚本。提示AGENTS.md的命名并非随意。AGENT暗示其作为“代理”角色——Skill不直接操作业务数据而是通过契约接收指令并返回结果业务方是“委托人”。这种主被动关系的厘清是避免耦合的第一道防线。Skill的物理形态是一个独立的monorepo子包目录结构高度标准化packages/skill-price-display/ ├── src/ │ ├── index.tsx # 主入口导出Skill核心逻辑与Hook │ ├── hooks/ # 封装业务无关的副作用逻辑如汇率请求 │ │ └── useExchangeRate.ts │ ├── utils/ # 纯函数工具集如价格格式化 │ └── styles/ # 内联样式或CSS-in-JS定义 ├── AGENTS.md # 契约声明强制存在 ├── README.md # 使用示例与高级用法 ├── jest.config.js # 独立测试配置 └── package.json # 严格限定peerDependencies这种结构带来的直接好处是每个Skill都是一个“微应用”。你可以单独运行它的Storybook单独执行它的Jest测试套件单独发布它的npm包如果需要甚至单独为其配置CI流水线。当PriceDisplaySkill需要重构价格计算逻辑时维护者只需关注src/utils/formatPrice.ts确保AGENTS.md中定义的输入输出不变就能保证所有下游业务零感知。这就是“原子性”的力量——拆得够小改得才够稳。3. Hook让Skill从“静态组件”进化为“可编程能力”如果说Skill是能力的“容器”那么Hook就是打开这个容器、并按需定制其行为的“钥匙”。在得物的实践中Hook不是React的内置Hook而是一套基于Skill契约设计的、面向能力扩展的函数式API。它解决了传统组件复用中最棘手的问题如何在不修改Skill源码的前提下注入业务专属逻辑以登录态校验为例。一个通用的商品详情页组件ProductDetailSkill需要展示“加入购物车”按钮但按钮是否可点击取决于用户是否已登录。传统做法是在ProductDetailSkill内部硬编码登录态判断逻辑或暴露一个onLoginCheckprop让业务方传入函数。前者导致Skill耦合业务逻辑后者则让业务方承担了“何时调用、如何处理异步”的心智负担。得物的方案是ProductDetailSkill提供一个useCartButtonBehaviorHook。业务方在自己的页面中这样使用// 业务方页面pages/product/[id].tsx import { ProductDetailSkill } from didu/skill-product-detail; import { useCartButtonBehavior } from didu/skill-product-detail/hooks; export default function ProductPage() { const { isLogin, cartButtonProps } useCartButtonBehavior({ // 注入业务专属的登录态获取方式 getLoginStatus: () authStore.getState().isLogin, // 注入业务专属的加购逻辑 handleAddToCart: async (skuId) { await cartService.add(skuId); toast.success(已加入购物车); }, // 注入业务专属的未登录引导 handleLoginRedirect: () router.push(/login?redirect window.location.href), }); return ( ProductDetailSkill productId123456 cartButtonProps{cartButtonProps} // Skill内部只负责渲染不关心逻辑 / ); }这个Hook的内部实现本质上是将ProductDetailSkill的“行为契约”进行了函数式解耦// didu/skill-product-detail/src/hooks/useCartButtonBehavior.ts import { useState, useCallback } from react; import type { CartButtonBehaviorOptions } from ../types; export function useCartButtonBehavior(options: CartButtonBehaviorOptions) { const [isLogin, setIsLogin] useStateboolean(options.getLoginStatus()); // 监听登录态变化例如通过全局事件或状态订阅 useEffect(() { const unsubscribe authStore.subscribe((state) { setIsLogin(state.isLogin); }); return unsubscribe; }, []); const handleAddToCart useCallback(async (skuId: string) { if (!isLogin) { options.handleLoginRedirect(); return; } try { await options.handleAddToCart(skuId); } catch (error) { // 统一错误处理业务方无需关心 console.error(Add to cart failed:, error); toast.error(加入购物车失败请稍后重试); } }, [isLogin, options]); return { isLogin, cartButtonProps: { onClick: () handleAddToCart(current-sku-id), disabled: !isLogin, loading: false, // 可由业务方通过其他Hook控制 } }; }注意cartButtonProps是一个对象而非一个函数。这意味着ProductDetailSkill内部可以直接将其解构到按钮上完全不需要理解handleAddToCart的实现细节。这种“Props as Data”的设计是保持Skill纯净的关键。Hook机制带来的工程收益是颠覆性的职责分离Skill只负责“呈现”业务方只负责“决策”中间的“连接”由Hook完成可测试性useCartButtonBehavior可以被独立单元测试MockgetLoginStatus和handleAddToCart验证不同登录态下的返回值渐进式采用业务方可以只使用useCartButtonBehavior而不使用ProductDetailSkill的其他Hook如useShareBehavior按需组合版本兼容当ProductDetailSkillv2.0新增useWishlistBehaviorHook时v1.0的业务方无需任何改动仍可稳定运行。这彻底改变了组件复用的协作模式。过去业务方提需求“请给商品卡片加个分享按钮”Skill维护者要改代码、发版、通知所有人更新。现在业务方自己写一个useShareBehaviorHook调用ProductDetailSkill暴露的shareApi在AGENTS.md中声明然后通过shareButtonProps注入——整个过程无需Skill团队介入复用效率呈指数级提升。4. monorepo不是代码仓库而是能力治理的物理疆界把Skill和Hook放在一个monorepo里绝非为了赶时髦。得物选择pnpmchangesets 自研Skill Registry的monorepo架构其根本动机是为所有可复用能力建立统一的“户籍管理系统”。在这里monorepo是基础设施Skill Registry是操作系统而AGENTS.md和Hook则是运行在其上的“应用程序”。一个典型的得物monorepo目录树远不止packages/那么简单. ├── packages/ # 所有能力单元Skill、Hook、Utils │ ├── skill-product-detail/ # 商品详情Skill │ ├── skill-price-display/ # 价格展示Skill │ ├── hook-auth/ # 认证相关Hook集合 │ └── utils-i18n/ # 国际化工具库 ├── apps/ # 业务应用非复用单元但可消费Skill │ ├── web-mall/ # 主商城Web应用 │ └── miniapp-coupon/ # 优惠券小程序 ├── tools/ # 工程化工具链 │ ├── skill-registry/ # 技能注册中心CLI Web UI │ ├── md-validator/ # AGENTS.md 格式与契约校验器 │ └── storybook-builder/ # 为所有Skill自动生成Storybook ├── .changeset/ # 版本变更记录Changesets └── turbo.json # TurboRepo 高速构建配置这个结构的设计哲学是物理隔离逻辑互联。packages/下的每个Skill都有自己的package.json、tsconfig.json、jest.config.js它们是独立的NPM包可以被yarn link或pnpm add单独安装。但它们又共享同一套构建、测试、发布的流水线这带来了三大不可替代的优势4.1 依赖治理终结“幽灵依赖”与“版本碎片化”在多仓库multi-repo模式下skill-price-display依赖utils-i18n1.2.0skill-product-detail依赖utils-i18n1.3.0而web-mall应用又同时依赖这两个Skill。最终web-mall的node_modules里会同时存在utils-i18n1.2.0和1.3.0两个版本造成内存浪费、行为不一致如i18n缓存key冲突、调试困难。“幽灵依赖”——即某个Skill隐式依赖了另一个未在package.json中声明的包——在multi-repo中几乎无法根治。monorepo通过pnpm的硬链接hard link机制强制所有包共享同一份utils-i18n源码。skill-price-display的package.json中声明dependencies: { didu/utils-i18n: workspace:^1.0.0 }pnpm会将其解析为本地packages/utils-i18n的绝对路径而非从npm下载。这意味着utils-i18n的任何修复只要在monorepo内发布所有依赖它的Skill立即获得更新无需手动npm updateutils-i18n的Breaking Change会被changesets检测到并强制要求所有下游Skill在发布前更新其AGENTS.md中的兼容性声明构建时turbo能精准计算出哪些Skill真正被web-mall引用只构建这些包跳过miniapp-coupon未使用的skill-price-display构建速度提升40%。4.2 变更影响分析从“猜”到“算”当一个工程师提交PR修改了skill-price-display的AGENTS.md中currency字段的取值范围从[CNY,USD]扩展到[CNY,USD,HKD,JPY]monorepo的威力才真正显现。Skill RegistryCLI 会自动执行反向依赖扫描找出所有在package.json中声明了didu/skill-price-display的包如web-mall,miniapp-coupon契约兼容性检查对比旧AGENTS.md与新AGENTS.md确认此变更属于“向后兼容的扩展”Adding a new value to an enum is safe代码级影响分析扫描所有下游包的源码查找是否硬编码了currencyCNY若找到则标记为潜在风险点生成影响报告在PR评论中自动贴出表格下游包是否直接使用currency字段是否存在硬编码风险推荐动作web-mall是否全部通过变量传入✅ 无需修改miniapp-coupon否-⚠️ 本次变更对其无影响提示这个分析过程不是静态文本匹配而是基于TypeScript AST抽象语法树的深度解析。它能识别const curr CNY; PriceDisplay currency{curr} /这样的间接硬编码这是传统grep命令永远做不到的。4.3 发布与回滚一次原子操作在multi-repo中发布一个Skill的patch版本需要进入skill-price-display仓库npm version patchgit pushnpm publish进入web-mall仓库npm update didu/skill-price-displaygit commitgit push等待CI进入miniapp-coupon仓库重复上一步……任何一个环节失败都会导致“部分业务已升级部分业务仍旧版”的雪崩状态。monorepo中这一切被压缩为一条命令# 在monorepo根目录执行 pnpm changeset version # 自动更新所有受影响包的package.json版本号 pnpm build # 构建所有变更包 pnpm changeset publish # 一次性发布所有新版本到npmchangesets会根据Git提交信息如fix(price): fix HKD rounding error自动归类变更类型patch/minor/major并生成规范化的CHANGELOG。更重要的是回滚也是一次原子操作git revert掉那个changeset提交再执行pnpm changeset publish所有包的版本号和npm包都会瞬间回到上一状态。这种确定性是支撑得物每天数百次组件发布的基石。5. AGENTS.md契约即代码文档即测试AGENTS.md是整个工程化实践的“心脏起搏器”。它看起来只是一份Markdown文档但在得物的工程体系里它被赋予了三重身份人类可读的契约说明书、机器可解析的接口定义书、自动化可执行的测试用例集。它的存在标志着组件复用从“人治”走向“法治”。5.1 从文档到TypeScript契约的自动升维AGENTS.md的第一重价值是驱动类型安全。得物自研的md-validator工具会在每次git commit前通过husky pre-commit hook自动解析AGENTS.md并生成对应的TypeScript接口文件。以上文PriceDisplaySkill的输入契约为例md-validator会生成// packages/skill-price-display/src/types/generated.ts export interface PriceDisplayInputContract { /** 原始价格分 */ price: number; /** 币种代码影响符号与小数位 */ currency?: CNY | USD | HKD; /** 是否显示“促销中”标签 */ showPromotionTag?: boolean; /** 促销强度等级影响标签颜色与文案 */ promotionLevel?: 1 | 2 | 3; } // 并导出为Skill的Props类型 export type PriceDisplayProps PriceDisplayInputContract { // 其他非契约字段... };这意味着任何业务方在使用PriceDisplaySkill price{19900} currencyEUR /时TypeScript编辑器会立刻报错Type EUR is not assignable to type CNY | USD | HKD.。契约的约束力从“文档里写着”变成了“代码里拦着”。这比任何Code Review都可靠。5.2 从文档到Storybook契约的可视化沙盒AGENTS.md的第二重价值是驱动Storybook的自动化生成。storybook-builder工具会扫描所有packages/*/AGENTS.md为每个Skill自动生成标准故事Story// packages/skill-price-display/stories/PriceDisplay.stories.tsx import { PriceDisplaySkill } from ../src; import type { PriceDisplayInputContract } from ../src/types; export default { title: Skills/PriceDisplay, component: PriceDisplaySkill, argTypes: { price: { control: number, defaultValue: 19900 }, currency: { control: select, options: [CNY, USD, HKD], defaultValue: CNY }, showPromotionTag: { control: boolean, defaultValue: true }, promotionLevel: { control: radio, options: [1, 2, 3], defaultValue: 1 } } }; const Template: StoryPriceDisplayInputContract (args) ( PriceDisplaySkill {...args} / ); export const Default Template.bind({}); Default.args {};这个故事的argTypes完全来自AGENTS.md中Input Contract表格的解析。业务方打开Storybook看到的不是一个静态截图而是一个可交互的契约沙盒滑动promotionLevel的Radio按钮实时看到标签颜色变化切换currency下拉框立刻看到币种符号和小数位同步更新。这消除了90%的“文档和实际效果不符”的沟通成本。5.3 从文档到CI契约的自动化守门员AGENTS.md的第三重价值也是最硬核的价值是成为CI流水线的“守门员”。在pnpm build之后md-validator会执行一项关键检查契约合规性验证Contract Compliance Check。它会启动一个轻量级的Node.js沙盒动态加载PriceDisplaySkill的index.tsx并尝试用AGENTS.md中声明的所有边界值Boundary Values进行调用测试用例pricecurrencyshowPromotionTagpromotionLevel期望结果正常值19900CNYtrue1渲染成功无控制台错误边界值0CNYtrue1抛出Error: price must be 0无效值19900EURtrue1抛出Error: invalid currency EUR缺失值19900undefinedtrue1渲染成功使用默认CNY这个测试不是由人工编写的而是由AGENTS.md自动生成的。如果PriceDisplaySkill的代码没有对price 0做校验或者没有处理currency的非法值这个CI步骤就会失败并给出清晰的错误信息“AGENTS.md声明price必须0但src/index.tsx未实现校验逻辑”。这迫使Skill维护者在代码层面兑现契约而不是把责任推给“业务方应该传正确的值”。注意这个验证过程是“白盒”的它会深入到Skill的源码中检查是否存在if (price 0) throw new Error(...)这样的校验语句。它不是简单的类型检查而是对契约履行度的深度审计。正是这种将文档、类型、UI、测试四者合一的设计让AGENTS.md超越了传统文档的范畴。它不再是一个需要“定期更新”的静态资产而是一个活的、可执行的、驱动整个工程流程的“契约引擎”。当你看到一个Skill的AGENTS.md你就看到了它的全部灵魂——它的能力边界、它的行为承诺、它的质量底线。6. 实战复盘一次“立正请站好”的完整旅程理论终需落地。让我们用一个真实的、发生在得物大促前夜的案例来复现这套工程化实践是如何解决一个具体、紧急、高风险问题的。这个案例完美诠释了“立正请站好”四个字背后的纪律与力量。6.1 问题爆发大促倒计时48小时37个页面的倒计时组件集体“歪斜”背景得物年度大促“超级品牌日”即将开始所有活动页面都嵌入了同一个CountdownSkill倒计时组件用于显示“距离开抢还剩XX小时XX分XX秒”。该Skill由营销中台团队维护已稳定运行半年。突发状况大促前48小时一位运营同学反馈部分页面如“美妆专场”、“数码先锋”的倒计时数字字体突然变细、行高变大导致数字“0”和“8”在某些安卓机型上显示为“00”和“88”严重误导用户。技术同学紧急排查发现问题只出现在使用了新版本didu/theme-v2暗色模式主题的页面CountdownSkill的CSS中font-weight属性被theme-v2的全局样式覆盖CountdownSkill的AGENTS.md中style字段声明为“内联样式无外部CSS依赖”但实际实现却使用了CSS类名违反了契约。这是一个典型的“契约失效”危机。37个业务方页面每个页面的集成方式各不相同有的直接CountdownSkill /有的通过useCountdownHook有的甚至自己写了CountdownRenderer。如果按传统方式修复需要中台团队紧急发布CountdownSkillv1.5.1修复CSS37个业务方逐一更新依赖、测试、上线预估耗时至少36小时且无法保证所有页面都及时更新。6.2 工程化响应15分钟内所有页面“立正站好”得益于monorepo和AGENTS.md的治理能力整个修复过程被压缩到15分钟第1步定位与验证3分钟中台工程师在monorepo根目录执行pnpm skill-registry find --name countdown # 定位到 packages/skill-countdown pnpm md-validator check packages/skill-countdown/AGENTS.md # 验证契约md-validator立刻报错ERROR: AGENTS.md declares No external CSS dependencies, but src/styles/index.css imports ./base.css.证实了契约违规。第2步修复与契约更新5分钟工程师修改src/styles/index.css移除对外部CSS的引用将所有样式内联至组件。然后更新AGENTS.md在兼容性章节新增一行- CSS-in-JS 方案从 styled-components 升级为 emotion/styledfont-weight 属性已设为 !important 以确保优先级。并提交PR。第3步自动化影响分析与发布7分钟Skill RegistryCI自动触发扫描出37个下游包检查所有包的package.json确认它们都声明了didu/skill-countdown: ^1.5.0符合semver规则自动生成changeset标记为patch执行pnpm changeset version pnpm build pnpm changeset publish。第4步业务方零操作世界已修复0分钟由于所有业务方都使用pnpm且package.json中是^1.5.0pnpm install会自动拉取最新的1.5.1版本。web-mall、miniapp-coupon等应用在下一次CI构建时无需任何人工干预就获得了修复后的CountdownSkill。运营同学刷新页面倒计时字体恢复正常。6.3 关键经验为什么这次能赢这次闪电修复不是靠工程师的个人英雄主义而是整套工程化体系协同作用的结果AGENTS.md是“照妖镜”它让“无外部CSS依赖”这个模糊承诺变成了可验证的、机器可读的规则。没有它问题可能被掩盖数月monorepo 是“高速公路”它让一次代码修复能以原子操作的方式瞬间触达所有下游。没有它37个团队的协调就是一场噩梦Hook 是“减压阀”虽然本次未用到但如果某个业务方急需临时绕过修复比如他们有自己的字体CDN他们可以立刻写一个useCountdownWithCustomFontHook注入自定义样式而不影响其他36个页面Skill Registry是“指挥中心”它把散落在各处的依赖关系变成了可视化的拓扑图让影响分析从“拍脑袋”变成“看图表”。我个人在实际操作中发现这套体系最大的价值不是加速了“正确的事”而是极大地降低了“做错事”的成本。当一个新人工程师不小心在CountdownSkill里引入了jQuerymd-validator会在他git commit的瞬间就把他拦住而不是等到大促当天全站崩溃。这种“防御性编程”的文化才是工程化真正的护城河。“立正请站好”从来不是要求组件僵化不动而是赋予它在高速迭代中依然保持队形的能力。它是一套纪律一种信仰更是得物在复杂性海洋中为自己建造的一艘不沉的船。