适合 Vue / React 开发者,面向 B 端后台系统(跨境、ERP、SaaS 场景),兼顾设计原则、避坑经验,附思维导图要点,适合技术博客发布
很多团队都会踩同一个坑:业务写着写着,到处是重复弹窗、选择器、表格,每个页面一套逻辑,改一个交互就要全项目搜代码。
于是大家想到抽通用组件。但很多组件抽完之后,反而更难用:props 一大堆、样式覆盖满天飞、改一个参数就引发连锁 bug。
通用组件不是 “把页面里的代码复制一份包起来”,它是一套有边界、有规范、有兜底的独立单元。下面是我在 B 端长期开发沉淀下来的一套完整开发思路。
一、第一步:先划定组件边界,再写一行代码
组件开发最容易翻车的根源,就是边界模糊。
- ✅ 你要先回答 3 个问题:
- 这个组件负责什么?(UI 渲染、基础交互、状态托管)
- 这个组件不负责什么?(接口请求、业务校验、权限判断、特定业务字段)
- 目标场景:是后台内部系统、移动端 H5,还是多端共用?
举个反面例子:
❌ 错误:把「订单选择组件」写死orderNo、shopId,绑定 Shopee 接口。换一个业务页面直接不能用。
- ✅ 正确:只做一个通用的远程搜索选择器,只接收
request、value、labelField,业务自己传入请求方法和字段映射。
核心原则:通用组件只解决通用交互,业务逻辑全部交给上层。
二、API 设计:组件好不好用,API 占 70%
API 是组件和业务之间的 “契约”,契约设计得好,后续维护事半功倍。
我们一般分成 4 大块:Props、事件、插槽、实例方法,同时配合 TS 严格约束类型。
1. Props 设计要点
- 统一基础属性:
size/disabled/loading/readonly,和团队 UI 库(BsAntd/AntD)对齐 - 区分受控 & 非受控(Vue 的 v-model、React value+onChange),两种模式同时支持
- 预留样式入口:
customClass、customStyle,作为业务覆盖样式的唯一入口 - 所有参数写清楚默认值,避免 undefined 引发渲染异常
- 优先泛型,像选择器、表格组件,支持
Select<T>,类型联动
2. 事件规范
事件命名统一,优先语义化:change / confirm / close / clear
禁止一个组件叫onOk,另一个叫onSubmit,团队内保持统一。
事件回调携带最小必要参数,不要直接把组件内部整个实例抛出去。
3. 插槽
- 默认插槽 + 具名插槽(header /footer/extra)
- 复杂场景提供作用域插槽,把内部数据抛给业务自定义渲染(表格单元格是高频场景)
4. 实例方法
只暴露必要方法:open() / reset() / validate(),内部私有方法绝不对外暴露;
TS 导出实例类型,业务 ref 调用的时候有完整类型提示。
❗避坑:API 一旦发布,尽量向下兼容;破坏性变更必须升级大版本号,并且写在更新日志。
三、样式、主题与覆盖方案
样式是通用组件第二大重灾区:业务想改样式,直接深层穿透改内部 class,组件版本一升级直接样式崩塌。
推荐方案:
- 样式隔离
Vue 用scoped;React 用 CSS Modules; - 主题变量优先
核心颜色、圆角、间距全部抽成 CSS 变量--comp-primary-color,全局一键切换主题、暗黑模式; - 约定覆盖规则
业务只能通过传入customClass来覆盖,禁止业务写深度选择器去篡改组件内部类名; - 响应式适配
B 端后台大多以固定尺寸为主;对外 H5 组件要额外处理 vw/rem 适配。
四、状态、边界 case:决定组件稳不稳定
业务页面只会测试 “正常流程”,通用组件必须兜底所有异常场景:
- 空数据状态 empty
- 加载 loading、禁用 disabled、只读 readonly、报错 error
- 超长文本溢出,自动省略 + tooltip
- 入参为
null / undefined / 非法格式的容错处理 - 防重复点击、防抖节流(按需封装,可开关)
- 动画处理:弹窗 / 抽屉过渡动画,动画结束回调,防止销毁时报错
额外不要忘记无障碍 a11y:
弹窗支持Esc关闭、Tab 焦点循环、aria 标签,后台组件强烈建议加上,成本不高。
五、内存泄漏:B 端组件高频翻车点
很多组件看起来能用,长时间切换页面之后,页面越来越卡,根源就是销毁清理没做:
- 挂载到 body 的弹窗、抽屉,unmount 时必须移除 DOM 节点
- 定时器、addEventListener、全局事件订阅、resize 监听,组件销毁时全部取消
- watch、effect 自动清理,避免残留响应式监听
一句话:组件创建时注册的东西,销毁时必须全部清理干净。
六、透传、依赖与兼容性
- 属性透传:id、data-*、title 这类原生属性,自动透传到根 DOM,减少业务层层传递成本;
- 依赖策略:公共 UI 库(BsAntd、Vue、React)放到
peerDependencies,不打包进组件包,避免项目重复打包; - 浏览器兼容:B 端后台默认以 Chrome 为主;面向 C 端 H5 需要单独降级处理。
七、测试、文档、Demo,是通用组件的一部分
一个合格的通用组件,不是代码写完就算完工:
- 单元测试:Vitest/Jest,覆盖参数变更、事件触发、边界入参;
- 可视化 Demo:Storybook,提供最简示例、禁用、加载、自定义插槽等多场景预览;
- 文档:API 表格(Props/Events/Slots/Methods)、使用示例、版本变更日志 CHANGELOG;
很多团队组件没人愿意用,就是因为没有文档,每次用都要翻源码猜参数。
八、打包发包(如果作为 npm 公共包)
打包配置要点:
- 同时输出 ESM + CJS,支持 Tree-Shaking;
- 外部化 peer 依赖,不把 vue、antd 打包进产物;
- 语义化版本 semver:主版本 = 破坏性变更、次版本 = 新增功能、补丁 = bug 修复。
九、一张极简自查清单(开发完成前对照)
- ✅ 边界清晰,没有硬编码业务字段
- ✅ 完整 TS 类型,无 any
- ✅ 受控、非受控双模式支持
- ✅ 所有异常状态兜底(empty/loading/error/disabled)
- ✅ 定时器、全局事件在组件销毁时清除
- ✅ 样式通过 customClass + css 变量定制,不鼓励深度穿透
- ✅ 有 Storybook 示例 + API 文档
- ✅ 没有直接发起接口请求,业务请求由上层传入
结尾
通用组件开发,本质是做一套约束和抽象。
写组件的时候,你不是在为当前这一个页面服务,而是为未来几十上百个业务页面提前降低成本。
好的通用组件:业务拿来就能用,想要扩展有入口,升级的时候不翻车。