设计组件库时需要考虑什么:从通用 UI 到业务组件
面试里问「如果让你设计一个组件库,需要考虑什么」,很容易从 TypeScript、打包工具、按需加载一路讲到单元测试。这些都需要考虑,但在选工具之前,还有一个决定后续设计的问题:准备把什么东西复用起来?
按钮、下拉框复用的是视觉和交互;查询表格复用的是查询、分页、排序等页面模式;店铺选择器复用的则可能是店铺身份、可选范围、历史值回显和业务限制。它们都叫组件,承担的责任却不一样。
知乎这道关于组件库设计的问题里,有几条回答值得放在技术清单之前读:Cat Chen强调先明确谁会受益,我是救难背包讨论组件粒度和参数设计,cinwell提醒 API 规范、文档和升级成本。这些判断同样适用于业务组件库。
通用组件与业务组件,复用的东西不同
通用 UI 组件提供与具体业务领域无关的视觉和交互能力。这里的「UI」包含完整的交互逻辑,例如弹窗的焦点管理、选择器的键盘导航、表单的错误展示,并不等于只写样式。
业务组件则围绕某个业务概念提供可重复使用的界面能力。例如,一个店铺选择器不仅能选中某个选项,还知道选项代表店铺、店铺有哪些业务状态、哪些状态允许被选择。它可以在约定的范围内依赖业务服务。
为便于讨论,可以按复用内容划出三层。这个划分是分析工具,各团队的命名可能不同。
| 层次 | 主要复用内容 | 例子 |
|---|---|---|
| 基础 UI 组件 | 视觉、交互与无障碍行为 | Button、Dialog、Select |
| 通用场景组件 | 多个基础组件组成的交互模式 | 查询表格、分步表单 |
| 领域业务组件 | 业务对象、规则与界面能力 | 店铺选择器、订单摘要 |
例如,ProComponents 的架构说明把 ProTable 定位为抽象网络请求和表格格式化的模板组件,把 ProForm 定位为预设布局与行为的表单模板组件。它们可以服务多种业务,仍然需要应用提供具体的数据和规则。
能发请求、配置多、体积大,都不能单独作为业务组件的判断标准。更有用的判断是:移到另一个业务领域时,它的对象和规则是否仍然成立。

图中的箭头表示上层可以依赖下层,不要求每次调用都经过所有层。页面还负责路由、整页任务和提交流程;一个店铺选择器通常只交付选择结果,页面再决定用这些结果创建什么任务。
业务组件也可以包含完整的提交能力,例如一个独立的地址编辑面板。边界由它承诺完成的业务能力决定。复用对象如果根本没有界面,更适合做成领域函数、hook、SDK 或服务,无需强行包装成组件。
先确认值得建设什么
组件库面向的直接使用者是开发者,最终还要改善产品使用者的体验。开始设计时,需要知道哪些团队会接入、哪些项目正在重复实现同一种能力,以及它们为什么不能直接使用已有方案。
如果目标只是统一品牌视觉,在成熟组件库上维护主题和少量封装,可能已经足够。如果多个系统反复实现相同的店铺查询、回显和禁选逻辑,领域业务组件就有更明确的价值。框架、交互和业务规则都差异很大的项目,则需要先找出真正共有的部分。
盘点现有页面时,除了截图,还应该看调用代码、接口和变更原因。两个页面今天长得一样,可能只是偶然;两个页面样式不同,却可能一直遵守同一条业务规则。
适合共同维护的能力,通常能回答这些问题:
- 哪些调用方需要它?他们的输入、输出和规则是否一致?
- 哪一部分长期稳定,哪一部分总在变化?这些变化是否来自同一个原因?
- 一个问题修复后,哪些调用方理应一起受益?
- 谁对它负责,谁决定新需求能否进入公共接口?
GOV.UK Design System 的贡献标准要求新增提案有证据表明对多个团队或服务有用,并且与已有能力相比有独特价值;发布前还要检查可用性、一致性和适用范围。这套思路可以迁移到内部组件库:入库需要复用证据,也需要维护承诺。
不必把「出现三次才抽象」当作硬门槛。已经明确的设计标准可以提前建设;规则仍在快速分化时,即使出现多处相似代码,也可以先保留局部实现,等待边界变清楚。
两类组件库共同需要的基础
业务组件也需要可靠的交互、清楚的 API 和稳定的交付方式。两类库的区别主要在于承诺的范围,而这些公共基础决定了调用方能否放心使用它们。
设计规范与交互行为
颜色、间距、字体、圆角、层级和动效需要一套共同语言。Design Token 可以把这些设计决策表达成可复用的命名值,例如「危险操作颜色」,让组件与主题围绕同一语义变化。
视觉规范还需要对应的行为规范:加载期间能否再次点击,弹窗关闭后焦点回到哪里,错误信息如何关联到输入框,移动端和键盘操作怎样完成同一个任务。
Radix Primitives把样式定制与底层交互能力分开,处理 ARIA、焦点管理和键盘导航等细节,是理解通用组件职责的一个参考。WAI-ARIA APG 的键盘交互指南也明确指出,自定义 ARIA 控件的键盘支持需要开发者实现;添加角色属性不会自动得到这些行为。
API 与状态归属
API 需要让调用者知道自己控制什么、组件负责什么。至少要明确:
- 输入值、默认值和空值分别是什么,单选与多选的值形态怎样区分。
- 哪些状态由调用者控制,哪些是组件内部的临时状态。
- 事件表示交互发生、值发生变化,还是提交已经成功。
- 文案、局部内容和整体布局分别通过什么入口扩展。
- 不支持的参数组合能否在类型或运行时被发现。
受控组件由外部持有当前值,组件通过事件请求修改;非受控组件由内部持有当前值,外部通常只提供初始值。两种模式可以按需求支持,但必须讲清模式和同步规则,避免父组件与子组件各维护一份「最终值」。
一个选择器的选中值可能属于页面表单,搜索关键字和高亮项属于组件内部,候选数据属于查询状态。这些状态无需因为出现在同一块界面里,就交给同一个全局 store 管理。
扩展能力也应分层。调整文案用属性,替换局部内容用 slot 或 render 函数;只有已经证实会变化的业务策略,才值得提供策略接口。直接开放全部内部状态和 DOM 细节,会让内部实现逐渐变成调用方的依赖。
构建、分发与兼容
打包方案取决于消费环境。需要先明确支持的框架及版本、浏览器与 WebView 范围、是否支持服务端渲染,以及库如何被应用引入。
交付检查应落到实际产物:公开入口和类型声明是否正确,样式怎样加载,只用一个组件时会引入多少代码,框架运行时是否被重复打包,依赖版本是否能与应用共存。peerDependencies 声明兼容关系,构建工具的 external 配置控制是否打入产物,两者需要配合。
Vite 的库模式文档专门说明了框架依赖外部化、包入口和 CSS 导出等问题。选择某个工具只是起点,验收还应该把真正打出的包安装进消费项目,验证类型、样式、生产构建和运行行为。
按需使用也要分别检查 JavaScript 与 CSS。提供 ESM 或多个入口,有助于消费方移除未使用代码,但不能据此直接保证样式也按组件拆分,更不能只看源码目录判断最终包体积。
文档与质量保障
文档需要解释默认行为、使用边界、扩展方式和失败处理。业务组件还要写明它依赖哪些服务、需要什么权限、数据什么时候刷新,以及调用方还需要接上哪些动作。
测试可以按承诺的行为分工:
| 验证对象 | 主要验证方式 |
|---|---|
| 计算、映射与业务规则 | 单元测试 |
| 输入、事件、键盘与焦点 | 组件交互测试 |
| 主题、布局与状态展示 | 视觉回归 |
| 实际接口契约与关键任务 | 集成测试、少量端到端测试 |
| 安装、类型、样式与运行环境 | 发布产物的消费验证 |
一个常见但不足的验收是「文档站示例正常」。示例通常处于最理想的环境,真实应用还可能有表单联动、嵌套弹窗、旧版依赖、长文案和服务端渲染,需要根据支持范围补充覆盖。
业务组件要额外设计哪些边界
以一个假设的运营后台为例:运营人员创建活动时需要选择店铺,编辑已有活动时需要回显历史店铺。候选店铺来自服务端,操作者只能看到被授权的范围;某家店铺可能已经停业,也可能在选择后、提交前失去参与资格。
这个例子能同时暴露业务组件最常见的几种压力:业务含义、数据获取、历史状态和提交校验。组件要承诺处理哪些部分,需要在抽取之前确定。
同一个业务名词,也可能属于不同规则
「店铺选择」看似是一个稳定需求,但选择活动参与店铺与查询历史经营记录,未必遵守相同规则。前者可能只允许选择当前可参与的店铺,后者可能需要包含已经停业的店铺。
可以复用搜索列表、选中标签和回显机制,也可以复用相同的店铺身份模型;可选资格如果来自不同业务,应由对应领域规则或服务决定。仅仅给组件增加一个 scene 参数,再在内部根据页面名切换接口和规则,会把页面差异长期积累在组件里。
限界上下文是某个业务模型成立的明确范围。一个名词在不同范围内,可能需要不同的字段和规则。Martin Fowler 对 Bounded Context 的说明讨论的就是这种模型边界。这里借用这个思路判断复用范围,不要求小型组件库先引入完整的领域驱动设计体系。
我的默认选择是先围绕同一领域的共同规则建设组件;跨领域时共享稳定的基础机制。业务含义已经分化的组件,可以保留不同的名字和入口。

展示、领域规则与数据访问分别负责什么
一个店铺选择器可以提供完整使用体验,同时在内部保留清楚的责任边界:
- 展示与交互负责搜索框、候选项、选中标签、键盘操作和状态提示。
- 领域规则负责理解店铺身份、资格、禁选原因和历史选择的含义。
- 数据访问负责搜索、按 ID 查询、接口字段映射和错误转换。
- 页面负责活动表单、任务提交以及成功后的跳转。
这种拆分借鉴了展示、领域逻辑与数据访问分离的原则。它可以只是一个包里的几个模块,无需立刻拆成多个 npm 包。
如果资格由服务端决定,领域层就负责解释服务端返回的资格状态,而不是在浏览器里另写一套可能漂移的判定规则。纯计算规则可以独立测试;依赖实时数据的规则,需要明确服务端返回值的有效范围。
每一层还应有明确的输入输出。界面使用一个能满足展示和交互的店铺模型;接口返回结构变化时,在数据访问边界处理必要映射。已有接口模型足够清楚、稳定时可以直接使用,无需为了「分层」再复制一套同名字段。
请求可以内聚,但依赖需要显式
业务组件内部发请求可以减少重复接入代码。问题在于,它依赖的是公开约定的领域服务,还是某个应用的私有环境。
如果组件直接读取应用全局 store、拼接固定地址、获取隐含的当前租户,并在失败时调用应用专属弹窗,移到另一个系统后,调用方就必须复制这一整套环境。可以把所需能力收敛成显式的数据服务接口,由应用在装配位置提供实现。
对店铺选择器,最重要的两类能力通常是搜索候选店铺,以及按已选 ID 解析回显信息。这两个请求的语义不同:搜索返回当前候选范围,回显则需要解释已经存在的选择。
React-admin 的 Data Provider提供了一个现成参考:组件通过约定的数据方法访问资源,由 provider 对接实际 API。值得借鉴的是依赖边界,不是要求每个组件都复刻它的完整接口。
如果所有消费方都处于同一个平台,组件库也可以提供默认服务实现;涉及不同租户、不同服务或测试场景时,仍需要清楚的替换入口。鉴权、租户上下文、错误处理和日志各由谁提供,要写进接入契约。
历史值与异步状态需要独立建模
如果把「当前搜索结果里找不到」直接解释成「选中值无效」,编辑页面就会出现问题:原来选中的店铺可能只是没出现在这一页搜索结果里,也可能已经停业,或者当前操作者已经没有查看权限。
这些状态要分别处理。停业店铺能否展示名称、能否保留原选择、是否需要重新选择,由业务规则决定;权限丢失时只能展示服务端允许返回的信息。解析失败也不应悄悄变成清空选择,否则一次网络错误就可能改写表单。
对于这个假设的组件,可以把契约写成:
| 情况 | 组件需要明确的行为 |
|---|---|
| 搜索成功但没有候选项 | 显示无结果,保留已有选择 |
| 搜索失败 | 提供失败提示和重试入口 |
| 历史 ID 解析失败 | 保留 ID,显示可辨认的异常状态 |
| 店铺已不允许新增选择 | 展示禁选原因,按约定处理历史值 |
| 租户或权限上下文改变 | 隔离旧缓存,作废旧请求结果 |
异步搜索还要处理响应顺序:后发出的关键字查询,可能先返回。防抖只减少请求数量,不能保证旧响应不覆盖新结果;组件需要取消旧请求,或在写入结果前核对请求是否仍属于当前查询和上下文。
缓存的范围同样属于契约。不同租户、身份或资格条件下的结果不能只按关键字共用;作用域变化后,候选列表、历史值解析和在途请求都要重新判断。切换租户时,页面还需重置选择,或按新上下文重新解析并验证已有选择。卸载时清理订阅和计时器,也要避免已经失效的异步结果继续改变状态。

前端交互与服务端约束各自生效
店铺不可选时,前端应给出可理解的原因;最终提交时,服务端仍然要重新检查身份、权限和业务资格。选择时有效,不代表提交时仍然有效。
OWASP 的授权指南要求在服务端验证访问权限,不能依赖客户端访问控制。组件的禁用和隐藏负责交互体验,服务端约束负责实际操作是否合法。
一个可借鉴的真实例子是 Salesforce 的 lightning-record-edit-form:它围绕平台记录和字段工作,通过 Lightning Data Service 管理数据,并处理字段级安全与共享规则。这类组件之所以可以承担较完整的业务能力,依赖的是明确的平台数据与权限契约。
从这个例子可以推导出一个设计判断:组件能够内聚多少业务能力,取决于它背后有多少稳定的业务契约。平台已经统一的能力适合内聚;平台尚未统一的差异,需要显式留在服务或应用装配边界。
扩展性需要容纳变化,也需要限制变化
业务组件面对新需求时,很容易增加一组开关:是否显示停业店铺、是否支持跨区域、是否使用另一个接口、是否跳过某项校验。每个开关都很小,组合之后却可能形成大量难以解释的状态。
可以按变化原因选择扩展方式。店铺名称旁多一个标记,适合局部渲染扩展;两个服务返回字段不同,适合数据适配;活动资格不同,适合领域规则或服务接口;整套操作流程不同,则需要重新判断是否还属于同一个组件。
Sandi Metz 的《The Wrong Abstraction》描述了共享代码不断增加参数和条件分支,最终失去共同抽象的过程。业务组件为了复用而合并了不同的变化原因,也可能遇到同样的问题;出现少量分支本身,并不能证明抽象有问题。
配置化和组合式 API 也有各自边界。字段、布局与流程足够一致时,配置能减少重复;内容和交互差异明显时,组合往往更容易理解。配置一旦需要表达任意嵌套、事件顺序和复杂条件,就开始接近一门小语言,维护者还要承担校验、调试和兼容成本。
美团在《开源 React Native 组件库 beeshell 2.0 发布》中,分别讨论通用版本、业务定制版本与社区版本,也把主题、属性和渲染区域作为不同的定制层次。它是 2019 年的架构案例;可借鉴的是关注点与扩展层次,不必照搬当年的项目拆分和依赖方式。
从项目里的组件到可持续维护的库
项目里有一个封装良好的组件,与团队拥有一个组件库,中间还差着接入、发布和维护过程。
一个比较稳妥的建设顺序是:
- 盘点实际调用方,记录共同需求和无法统一的差异。
- 选择少量重复成本高、规则相对稳定的能力,在项目内整理边界。
- 让另一个有代表性的页面或项目接入,检查它是否需要理解原项目的内部环境。
- 补齐契约、示例、测试和发布产物验证,再作为共享库交付。
- 建立需求评审、版本升级、弃用和问题响应机制,根据接入反馈继续调整。
第一批组件无需覆盖所有基础控件。已有 UI 库能够满足要求时,可以优先建设缺失的场景与业务能力。
美团在 2018 年的金融平台 Web 前端技术体系中,把 Vix 分成基础、复杂和业务组件,并强调与设计团队共同维护标准。这份历史案例说明,复用效果与上下游标准有关;不能把其中的效率数据直接当成另一个团队的收益预测。
治理至少需要一个明确的维护者,负责判断新增需求属于公共能力还是调用方差异,并保证文档、实现和测试同步变化。Monorepo 可以帮助组织协作,但包应按职责、依赖和发布节奏拆分,不需要一开始就做到每个组件一个包。
语义化版本规范以公共 API 是否兼容为版本递增依据。组件的公共契约也包含默认行为、事件时机和约定的样式扩展点:属性名字没变,默认值或回调含义变了,仍可能让调用方出错。
业务规则还可能由服务端独立更新。把某条规则下沉到服务,不代表客户端永远不需要迁移;双方需要约定状态与错误含义、兼容范围,以及旧客户端遇到新规则时如何表现。重大变化应提供迁移说明、试用版本和代表项目验证,发布后还要有可执行的回退路径。
衡量收益时,可以看新项目的接入与适配时间、一次修复能覆盖多少调用方、版本升级需要多少人工处理,以及接入后是否仍大量复制组件。组件数量、下载量和引用次数只能说明规模,无法单独说明节省了多少工作。
面试时可以怎样回答
如果时间有限,可以先围绕设计顺序回答,再用店铺选择器这样的例子展开:
我会先确认组件库服务哪些团队、解决哪些重复问题,以及为什么现有方案不够。然后按复用内容区分基础 UI、通用场景和领域业务组件,确定公共边界与页面责任。
通用部分重点考虑设计规范、API 一致性、状态归属、组合扩展、无障碍、性能和兼容,以及构建分发、文档、测试和版本管理。
业务组件会额外关注领域规则、数据模型、接口依赖、权限、异步状态和规则演进。例如店铺选择器,除了搜索和选择,还要约定可选范围、历史 ID 回显、停业店铺的处理、请求竞态,以及提交前由服务端重新校验。
落地时我会从实际项目里选择少量稳定能力,经过第二个调用方验证,再逐步形成共享库;同时明确维护者、入库标准和升级机制。验收既看组件行为,也看真实项目的接入、适配和维护成本。
面试官追问某个方向时,再解释对应取舍:请求是否内置,要看服务契约是否稳定;相似页面是否合并,要看规则是否共同变化;扩展是否配置化,要看差异能否被有限、清楚的配置表达。能把这些选择落到一个具体组件上,比继续罗列工具名称更能展示设计能力。