多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

组件库封装:别在错误的时间用错误的姿势造轮子

组件库封装:别在错误的时间用错误的姿势造轮子 1. 先把结论摆出来多数项目死在第一步这个标题看起来像在抬杠但我是认真的。这几年我在不同团队里见过太多次类似的场景业务刚跑通两个页面就有同学热血沸腾地提出来“我们把公共组件整理一下吧搞一套自己的组件库”。动机听起来很正当——代码重复、样式混乱、以后多个项目都能用。结果是三个月以后组件库倒是建起来了但没人愿意用或者用的人天天骂维护的人改得想跑路。我不是反对封装也不是反对组件复用。我反对的是在错误的时间、用错误的理由、以错误的姿势去做组件库这件事。很多团队把“封装组件库”当成技术能力的证明或者当成解决代码混乱的万能药但实际上它更像是一种组织承诺你需要持续投入、需要忍受前期的低效、需要有能力做设计决策。而大部分项目根本不具备这个条件。这篇文章我尽量讲得直接一点把这些年我看过的失败案例和少数成功案例的底层逻辑拆开讲。如果你正打算给团队搞组件库或者正在封装某个功能却总觉得哪里不对劲这篇文章就是给你看的。1.1 为什么一上来就想封装组件库我得先泼一盆冷水绝大多数团队想封装组件库的冲动不是来自“复用需求足够强烈”而是来自“看着代码重复难受”。这两个动机有本质区别。如果你只是觉得某个组件在两个后台页面里长得像然后就开始抽象、提参、搞配置化那你很可能是在过度设计。最典型的失败路径是A 页面需要一个带筛选条件的表格B 页面也需要一个类似的表格于是有人封装了一个SuperTable支持 20 个 props从列配置到分页、从操作按钮到行样式全部接管。结果 C 页面接入后发现需求不一样——它需要在表格里嵌入表单长时间维护得想骂人。更麻烦的是一旦你把这个组件放进了“公共组件库”并且写进了 README它就有了某种神圣感。后面的人不敢改也舍不得删只能在外面继续加一层 wrapper于是一层套一层最后谁都不知道真相在哪。我的观察是代码重复不是坏事它说明你的业务还在快速变化。过早抽象才是真正的问题。重复三遍以上的代码才有资格被抽象重复两次顶多算“有相似的苗头”。1.2 被低估的长期维护成本很多人计算组件库的成本时只算了“写出来的时间”没有算“养它的时间”。一个组件库真正消耗精力的地方在发布之后别人提 issue 你要不要回API 设计得不合理要不要 break change多个项目用的版本不一致升级回归谁来做我之前接手过一个内部组件库里面有一百多个组件但真正被三个以上项目复用的不到十个。剩下的都是某个项目临时需要、当时没想明白、硬塞进来“先放着以后可能用得上”的东西。结果每个组件都有各自的依赖版本有的依赖 jQuery有的依赖老版 moment升级任何一个底层库都会牵连一片。组件库不是把代码放进独立仓库就算完它是一个需要长期稳定投入的产品。如果你没有专职的人或稳定的时间预算来维护它我建议你干脆不要做。另一方面如果公司已经有一个设计团队在推进统一的设计标准那组件库才有底座否则你只是把无数个未经打磨的“页面级代码”搬了一个家而已。1.3 组件库是组织问题不是技术问题这句话是我最想强调的。很多团队以为组件库搞不好是技术能力不够其实绝大多数是组织协作问题。举个例子两个前端团队同时用一套组件库A 团队想要全屏弹窗B 团队想要居中弹窗。组件库维护者如果两边都不敢得罪最后就会做一个支持 10 个参数的弹窗组件用position、width、maskClosable、customFooter、heightType…… 堆到怀疑人生。这哪里是设计组件 API这分明是在搞外交。真正的组件库在 API 层面一定是敢做取舍的。它背后要有一个明确的决策机制哪些场景支持、哪些场景不惯着。但在大部分公司里这个决策机制根本不存在。每个人只对自己的项目负责没有人对“组件库长期健康”负责于是组件库很快变成一个大杂烩。所以如果你准备做组件库先回答三个组织问题谁说了算谁长期维护业务需求变化时是让组件库小步演进还是冻结版本想不清楚这三个问题代码写得再漂亮也撑不了多久。2. 什么时候该封装什么时候不该封装写到这里肯定有人要问那到底什么时候才应该封装我的判断标准其实很朴素就三个问题。三个都满足你再动手缺一个我都建议你先忍一忍。2.1 硬性判断标准三个“是否”这是我常用的判断框架基本上可以直接拿去用。第一个问题同样的业务场景是否已经在三个以上的独立项目里出现过注意是“独立项目”不是同一个项目里的三个页面。同一个项目里的相似页面只能说明你的项目里有重复代码抽取到项目内部components目录就足够了没必要上升到组件库。真正值得放进组件库的必须是跨项目的共性需求比如统一的用户选择器、组织架构树、审批流控件。第二个问题使用方的业务变化是否已经稳定如果你的业务还在快速试错期今天要 A 交互、明天要 B 交互那你封装得越多后面改造成本就越大。很多后台系统最怕的就是“基础组件刚封好业务规则变了”。这不是封装技巧能解决的这是时序问题。第三个问题团队里有没有人能对这个组件的长期演进负责注意我说的是“负责”不是“有空的时候改一改”。如果一个组件库没有明确的 owner它很快就会烂掉。因为组件库是需要做兼容设计、写文档、回 issue 的这些工作没人认领最后就是谁用谁改、谁改谁骂。三个问题全部通过你再考虑做组件库。否则我建议你在项目里先创建components/common目录把公共的东西放进去等项目多了以后再做迁移不迟。2.2 组件抽取的先后顺序先有页面再有组件最后才有库这是很多新人最容易搞反的地方。正确的路径应该是先写页面再从页面里抽出局部组件最后在多个项目验证后才沉淀成正式组件库。先写页面是为了让你理解真实的业务约束。你只有在写页面的时候才知道这个表格筛选在移动端要折叠、在桌面端要展开才知道权限控制的按钮在接口返回前要禁用才知道表单校验在某些场景下不是整体的而是分区块的。这些细节坐在那里凭空抽象是想不出来的。从页面里抽局部组件是指把某个页面里重复出现的区块提升为独立组件但它的作用域只限当前项目。比如审批项目里的流程卡片、订单项目里的订单状态标签。这一步已经是“封装”了但它是局部的、低成本的改起来不用发版本、不用顾虑别的项目。最后才是沉淀到库。沉淀的意思是这个组件已经被两个以上项目用过了、接口形态和交互方式趋于稳定、API 设计经过至少一轮 review。只有到这个阶段它才有资格进入“库”。我见过太多团队把项目里刚抽出来的组件直接灌进所谓公共组件库结果其他项目一用就发现全是某个页面特有的硬编码这就是顺序搞反了。2.3 真正该做的方案项目内组件 工具集分包如果判断下来暂时不该做组件库但又确实存在公共代码怎么办我的建议是项目内组件 独立工具集分包。项目内组件很好理解就是在每个项目里维护自己的src/components只解决当前项目的重复代码问题。工具集分包则是把不涉及 UI、不涉及业务、纯逻辑的方法拿出来做成独立的 npm 包比如formatDate、treeToArray、downloadBlob、safeParse这类函数。因为纯函数天然稳定跟 UI 和业务解耦跨项目复用几乎没有副作用。UI 组件与纯函数有本质区别。纯函数的输入输出是确定的你给它{ name: 张三 }它永远返回{ label: 张三 }。但 UI 组件要面对样式主题、响应式、可访问性、业务扩展等一堆变数所以把 UI 组件过早跨项目共享往往是在给自己挖坑。这个方案的好处是你的代码复用诉求被满足了但不会背上“组件库”这个沉重的包袱。等团队和业务真的走到该有库的那一天再启动也不迟。而到时候那些经过多项目验证的工具函数可以直接迁移项目内组件也可以根据实际情况决定是上收还是保持现状非常平滑。3. 如果真要封装怎么避免做成人人踩坑的“轮子”假设你已经经过了前面的判断确定要做组件库了。那接下来我要讲的是实操层面的东西也是我踩坑最多的地方。说实话很多组件库不是死在“要不要做”上而是死在“怎么做”上。3.1 先定边界业务组件与基础组件严格分家这是我认为最重要、也最容易被忽略的一条原则。基础组件是什么是Button、Input、Table、Modal这类不携带任何业务语义的原子组件。它们的特点是不管在电商后台、内容管理还是大数据看板里长得都可以一样。做这类组件你本质上是在做一个简化版或定制版的 Element Plus / Ant Design那你得想清楚一个问题团队为什么不用开源组件库而要自己造一套业务组件是什么是“客户选择器”“订单状态标签”“审批流程卡片”这类与具体业务强绑定的组件。它可能由多个基础组件组合而成并且自带接口请求、权限判断、数据格式化。这类组件复用性确实强但它的维护依赖业务接口的稳定。我的建议是在目录结构和 publish 策略上把这两类严格分开。基础组件一个包业务组件一个包。这样做的好处是版本节奏可以独立业务组件可以跟着业务快速迭代不影响基础组件的稳定性基础组件的 break change 也不会把所有业务组件卷进来。我见过最惨的项目就是把业务组件和基础组件放在同一个包里结果一个业务的接口字段改了被迫发了一个大版本所有项目跟着升级一地鸡毛。3.2 API 设计别贪多props 越多死得越快我在评审代码的时候只要看到一个组件的 props 超过 15 个就会开始高度警惕。因为 props 数量往往不代表功能强大而是代表这个组件搞不清楚自己要什么。举个很典型的例子某团队封装了一个BusinessTable它的 props 包括columns、dataSource、pagination、rowSelection、searchForm、searchApi、searchParams、toolbar、emptyText、loading、scrollY、expandable、dragSort、onSearch、onReset、onExport…… 用起来确实感觉“什么都能干”但这个组件的 render 逻辑已经膨胀到上千行任何一个新需求都会触碰它那脆弱的if/else。我后来总结出一个更健康的做法让组件保持简单把复杂交互交给组合。也就是说BusinessTable只需要做好一件事——接收columns和dataSource渲染一个带分页的表格。至于搜索表单、右侧工具栏、行内操作按钮这些全部通过slot或children交给使用方自己组合。这样组件的核心逻辑不会膨胀使用方也保留了充分的灵活性。API 设计这件事本质上是做减法不是做加法。每加一个 props你都要问自己这个参数是真实高频的需求还是为了掩盖组件本身的功能缺失如果是后者请停下来重新设计。3.3 样式隔离与主题定制不做就是灾难组件库的样式问题几乎是所有国产组件库都会踩的坑但因为踩的人太多反而觉得不是问题。我认真提醒一句如果你要封装组件库CSS 变量Custom Properties这个问题必须提前想清楚。最理想的做法是组件库内部只使用设计令牌Design Token比如--color-primary、--radius-md、--space-lg然后在根节点把这些变量映射到具体数值。接入方只要覆盖这些变量就能实现主题定制不需要!important不需要深层覆盖更不需要:deep()去篡改内部样式。另一个常见问题是如果业务项目本身也在使用某个 UI 框架比如业务方用的是 Element Plus组件库内部也用了 Element Plus那么组件库打包的时候必须把 Element Plus 设置为 peerDependency不能把它打包进组件库产物里。否则就会两套样式打架、重复注册插件。这个问题在多个项目同时接入时特别明显一个项目里出现两个版本的某个组件页面表现完全不同排查起来会让人崩溃。不用担心基础样式:组件库的样式应该自带 reset 能力但注意不要污染全局。很多组件库喜欢在入口文件里写* { box-sizing: border-box }这个在大型项目里容易引发意料之外的布局变化慎用。3.4 文档、示例、版本发布缺一不可最后一个实操重点是我见过最多团队忽略的组件库的文档与示例和组件本身的代码同等重要。很多团队喜欢用 Storybook 来做组件预览但 Storybook 偏向开发环境等组件库被多个项目使用后你会发现一个简单的在线文档站可能更实用。这个文档站不需要多复杂每个组件要有基础用法示例、API 表格、不同状态截图、变更日志。最重要的是示例代码必须能直接复制到项目里跑起来——如果示例代码还要二次加工才能用那文档的价值就大打折扣。版本发布这块建议从一开始就建立规范语义化版本SemVerbreaking change必须单独列出来每次发布前要有 changelog组件库的版本要尽可能频繁但幅度小而不是攒一个大版本吓死人。同时最好有自动化发布流程比如打 tag 自动构建、自动生成文档。手工发布组件库的个人体验是第一次手工发布还行第十次一定会漏掉东西。另外我有个特别具体的建议每个组件的 README 里都要写清“什么时候不要用这个组件”。比如“本组件只适用于低频的后台表格展示场景如果你需要高频编辑请使用 XX 组件”。这种反向约束能拦下一大批误用省去后续无数沟通成本。4. 常见问题与避坑实录组件库这件事光讲原则还不够很多问题必须放到具体场景里才有感觉。这一部分我整理了这些年高频遇到的几个问题和我的处理思路你可以对照着排查自己的项目。4.1 需求又变了封装的组件要不要改这是最经典的场景业务组件封装好了结果业务方提了新需求你的第一反应是“要不要给组件加个 props 来控制这个新行为”。我的建议是如果这个需求只影响当前项目不要动组件库里的组件在当前项目里通过 wrapper 包一层覆盖或扩展它的行为。比如组件库里的OrderStatusTag只支持“待审核、已通过、已驳回”新项目需要“已撤销”状态你不要急着给它加 props先在项目里写一个ProjectOrderStatusTag内部直接调用基础组件然后多传一个状态映射。这样做的好处是组件库保持稳定不会被各种零碎需求蚕食新项目的特殊逻辑也不会污染通用组件。等到不少项目都出现相同的新状态时再考虑回收到组件库。说白了组件库演进要慢半拍让需求先飞一会儿别急着接。4.2 组件升级引发全站回归怎么排查组件库一旦被多个项目引用升一个版本就相当于全站回归。这个问题没法完全避免但可以控制爆炸半径。首先建议所有接入方都锁定 minor 版本。比如^1.4.0可以接受1.x.x的更新但不要默认接受2.x.x的大版本。其次每次组件库发布都要注意输出 changelog并且给重要变更打上BREAKING CHANGE的标记不要让接入的人靠猜。如果真的遇到升级后样式错乱或者功能异常第一步不是去组件库代码里翻而是先确认接入方项目的依赖树。我排查过的大多数诡异问题最后定位到的都不是组件代码的问题而是因为某个子依赖被 hoist 到了顶层导致组件内部import到了错误版本。用npm ls 包名查看依赖树比你去翻组件库源码快得多。还有一点组件库一定要有回归测试尤其是核心组件。不需要多全面的测试但至少要覆盖基础渲染、关键交互事件、API 的快照。没有测试的组件库升级一次就心惊胆战一次最后大家干脆不敢升级。4.3 多人维护时API 与代码风格冲突怎么解决这个问题的本质是缺少“单一决策人”。几个项目都在用组件库每个项目的负责人都有自己的诉求每个人都想按自己的想法改组件的 API。如果都是平级关系组件库很快会变成夹在中间的出气筒。我见过的成功团队一般都有一个明确的机制组件的 API 变更必须有提案和评审不能想改就改评审时需求方要说明为什么现有 API 覆盖不了维护者要评估影响面这两方如果争执不下则由更高一级的技术负责人拍板。代码风格冲突是另一个问题。组件库的代码风格应该比业务代码更严格因为它属于公共资产。建议在组件库仓库里单独配置 ESLint、Prettier并且开 PR 的时候必须过 CI 检查。风格不统一的话别人 review 你的组件代码时一半精力都花在适应代码风格上哪还有心思看逻辑。4.4 复杂场景组件的正确拆分思路很多团队的组件库最终死在“复杂组件”上比如带权限控制的表格、带流程引擎的审批按钮、带批量导入的弹窗。这种组件如果硬做成一个黑盒表面上是方便实际上是把复杂度转移给了组件库的维护者。我的经验是把复杂组件拆成三层。第一层是无状态展示组件只管根据 props 渲染 UI比如Table。第二层是容器组件负责数据获取和业务逻辑比如OrderTable它内部调接口、做筛选、处理 loading但不关心外部长什么样。第三层是页面组合层由使用方的页面来拼装比如在这个页面里OrderTable要和SearchForm、Toolbar组合那就由页面代码完成组合不让组件库把这几个东西焊死在一起。这样拆分的好处是每一层都可以独立演进组件库维护者只需要关心第一层和第二层的稳定性而不用为一个特定页面定制一坨无法复用的逻辑。实际做下来我发现这种拆分的维护负担比把所有东西塞进一个大组件低了一个数量级。4.5 组件库和低代码平台别把两件事混为一谈最近低代码这个概念又火了起来有些团队喊着要搞“低代码 UI 组件库”想在组件库基础上做一个拖拽就能搭建页面的平台。我先泼一盆冷水低代码平台和组件库的边界是完全不同的。组件库面对的是开发者它的 API 可以复杂因为它由代码调用低代码平台面对的可能是不写代码的运营或产品它需要的是配置协议和渲染器。你把组件库直接抛给低代码平台最大的问题是组件的数据源、事件交互、权限控制全都需要配置化这一层抽象可不是在组件上多包一层就能解决的。如果你真的想做低代码不要把精力花在“自研组件库”上而是应该把精力花在设计“组件描述协议”上。组件库里已有的组件可以通过协议层暴露给低代码平台而不是为低代码重新写一套组件。协议先行渲染器适配组件库保持纯粹这样才不会被低代码这个宏大的概念拖垮。我还见过一种不算罕见的情况团队连基础表单组件都还没沉淀好就急着上低代码结果低代码平台自己造了一套表单组件和已有的前端组件库完全割裂。研发要维护两套组件运营也分不清在哪配置最后变成双倍工作量。低代码是金字塔尖的东西底层组件库打不牢塔尖立不起来。结尾我个人在实际操作中的体会是组件库这件事最难的从来不是代码而是克制。每一次想加 props 的时候每一次想把某个页面里的“宝贝”代码上收到库里的时候每一次想用组件库去解决组织协作问题的时候你都需要踩一下刹车问自己一句“这个东西真的已经到了非收不可的时候了吗”我自己的经验是项目里先用好本地组件、把工具函数独立成包让需求先跑几个月真正常用的东西自然会被两三个项目用出来而不是被预判出来。等到它们真的被多个项目一起用的时候再动手做组件库你会发现自己设计的 API 稳稳地贴合实际场景改起来也不会三天两头要 break change。最后再分享一个小技巧当你忍不住想封装一个组件库时先写一个这个组件的使用 demo。如果 demo 代码里出现了超过 15 个 props或者使用方需要读三遍文档才能理解参数含义停手重新设计你的 API。宁可多写一点“笨”代码也不要为了省事而封装一个日后人人喊打的轮子。
返回列表