面试时建议把这个问题理解成:
组件库不是"写一堆组件",而是设计一套可复用、可约束、可扩展、可发布、可持续演进的前端基础设施。
一、面试题:如果让你从零设计一个组件库,你会考虑哪些方面?
核心思路(一句话)
先确定设计系统和组件边界,再设计组件 API 与可访问性,随后解决工程化、构建发布、性能、测试和版本治理,最终形成可持续演进的组件基础设施。
解决方案流程图
text
业务目标 / 使用场景
↓
确定组件库定位
↓
设计系统 Design System
↓
设计 Token
颜色 / 字体 / 间距 / 圆角 / 阴影 / 层级
↓
组件分层
基础组件 → 复合组件 → 业务组件
↓
设计组件 API
Props / Events / Slots / Composition
↓
确定组件状态
默认 / Hover / Focus / Active / Disabled / Loading / Error
↓
可访问性设计
语义化 HTML / Keyboard / Focus / ARIA
↓
技术架构
React / Vue / Web Components
CSS / CSS Modules / CSS Variables
↓
工程化
Monorepo / TypeScript / ESLint / 构建 / Storybook
↓
质量体系
Unit Test / Integration Test / Visual Regression / E2E
↓
性能
Tree Shaking / 按需加载 / 依赖治理 / Runtime 优化
↓
发布体系
Semantic Versioning / Changelog / CI/CD / npm
↓
版本治理
兼容性 / Deprecated / Migration / LTS
↓
持续演进
业务反馈 → 数据 → 迭代 → 新版本
二、第一步:先明确组件库到底解决什么问题
这是很多候选人容易忽略的地方。组件库不是一上来就:
text
Button
Input
Select
Modal
Table
...
而应该先问:
这个组件库服务谁?解决什么问题?
例如:
场景 A:公司内部业务组件库
目标:
text
统一 UI
统一交互
减少重复开发
提高业务开发效率
降低维护成本
场景 B:面向开源社区
除了上面的目标,还需要:
text
跨项目兼容
文档完整
API 稳定
版本治理
社区贡献
浏览器兼容
生态建设
场景 C:大型企业级组件库
还要考虑:
text
主题系统
国际化
无障碍
SSR
微前端
多框架
设计系统
灰度发布
长期维护
三、问题的主要矛盾和次要矛盾
这是高级面试里非常值得主动说的一点。
主要矛盾
统一性 vs 灵活性
组件库需要:
text
统一
↓
视觉统一
交互统一
API 统一
行为统一
但是业务又要求:
text
灵活
↓
不同业务
不同布局
不同主题
不同数据
不同交互
如果组件库限制太死:
text
业务无法使用
↓
开始绕过组件库
↓
出现大量魔改
↓
组件库失去价值
如果组件库过度灵活:
text
任何东西都可以覆盖
↓
每个项目都长得不一样
↓
组件库失去统一性
所以组件设计的核心实际上是:
text
组件库
│
┌────────┴────────┐
↓ ↓
统一约束 合理扩展
│ │
Design Token Composition
标准 API slots / children
默认行为 className
无障碍 CSS Variables
次要矛盾
还包括:
text
开发体验 vs 实现复杂度
包体积 vs 功能完整度
兼容性 vs 技术先进性
稳定性 vs 快速迭代
抽象程度 vs 易用性
四、面试题:组件库和 Design System 有什么区别?
核心思路(一句话)
Design System 是设计和工程规则体系,组件库是其中负责可复用 UI 实现的工程载体。
架构图
text
Design System
│
├── Design Principles
│
├── Design Tokens
│ ├── Color
│ ├── Typography
│ ├── Spacing
│ ├── Radius
│ ├── Shadow
│ └── Z-Index
│
├── Components
│ ├── Button
│ ├── Input
│ ├── Modal
│ └── Table
│
├── Patterns
│ ├── Form
│ ├── Navigation
│ └── Search
│
├── Accessibility
│
└── Documentation
│
↓
Component Library
五、问题:为什么要设计 Design Token?
核心思路(一句话)
把视觉设计中的"硬编码值"抽象成语义化变量,使主题切换、设计统一和全局修改从"改组件"变成"改 Token"。
错误方式
css
.button {
background: #1677ff;
padding: 8px 16px;
border-radius: 6px;
}
几十个组件都这么写。
如果品牌色变了:
text
修改几十个组件
↓
容易遗漏
↓
产生不一致
正确方式
css
:root {
--color-primary: #1677ff;
--color-primary-hover: #4096ff;
--color-text: #1f1f1f;
--spacing-sm: 8px;
--spacing-md: 16px;
--radius-sm: 4px;
--radius-md: 6px;
}
组件:
css
.button {
background: var(--color-primary);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
}
这样:
text
Design Token
↓
CSS Variables
↓
Component
↓
Application
六、主题系统应该怎么设计?
核心思路(一句话)
组件不要直接依赖具体颜色,而应该依赖语义 Token,通过 CSS 自定义属性实现运行时主题切换。
架构
text
基础 Token
↓
语义 Token
↓
CSS Variables
↓
组件
↓
主题切换
例如:
css
:root {
--color-bg: #ffffff;
--color-text: #1f1f1f;
--color-primary: #1677ff;
}
[data-theme="dark"] {
--color-bg: #141414;
--color-text: #ffffff;
--color-primary: #4096ff;
}
React:
tsx
import React from "react";
/**
* 主题切换示例。
*
* 核心原理:
* 1. 不直接修改每个组件的颜色。
* 2. 只修改根节点上的 data-theme。
* 3. CSS 根据 data-theme 选择对应的 CSS 自定义属性。
* 4. 所有组件自动使用新的主题变量。
*/
export function ThemeSwitcher() {
const switchTheme = (theme: "light" | "dark") => {
/**
* 修改 html 元素的 data-theme 属性。
*
* 例如:
*
* <html data-theme="dark">
*
* CSS 中的:
*
* [data-theme="dark"] {
* --color-bg: #141414;
* }
*
* 就会立即生效。
*/
document.documentElement.dataset.theme = theme;
};
return (
<div>
<button onClick={() => switchTheme("light")}>
浅色主题
</button>
<button onClick={() => switchTheme("dark")}>
深色主题
</button>
</div>
);
}
七、面试题:组件应该如何划分?
核心思路(一句话)
优先抽象稳定、重复、边界清晰的能力,而不是看到重复代码就立即抽象。
推荐分层
text
组件库
│
├── 基础组件
│ ├── Button
│ ├── Input
│ ├── Checkbox
│ └── Icon
│
├── 反馈组件
│ ├── Message
│ ├── Notification
│ ├── Modal
│ └── Drawer
│
├── 数据展示
│ ├── Table
│ ├── Pagination
│ ├── Tree
│ └── List
│
├── 布局组件
│ ├── Grid
│ ├── Space
│ └── Flex
│
└── 业务组件
├── UserSelector
├── OrderStatus
└── PermissionSelector
八、基础组件和业务组件不要混在一起
这是组件库架构非常重要的一点。
例如:
tsx
<UserSelector />
里面可能包含:
text
用户接口
权限逻辑
用户搜索
用户头像
用户组织结构
这种组件强依赖业务。
而:
tsx
<Select />
应该只解决:
text
下拉选择
键盘操作
焦点
选中状态
搜索
展示
因此:
text
基础组件
↓
稳定、通用、低业务耦合
而:
text
业务组件
↓
高业务价值、高业务耦合
不要为了"复用"而强行把所有业务代码塞进基础组件。
九、面试题:如何设计一个好的组件 API?
核心思路(一句话)
API 要做到语义清晰、符合平台习惯、可组合、难误用,并把复杂性封装在组件内部。
1. Props 命名统一
推荐:
tsx
disabled
loading
size
variant
onClick
onChange
而不是:
tsx
isDisabled
isLoading
buttonSize
clickHandler
但这里要注意:
不是说
isDisabled一定错误,而是对于描述组件状态的公开 API,优先采用业界约定和 HTML 语义,例如disabled。
十、组件 API 的核心是"组合能力"
不要设计成:
tsx
<Card
title="标题"
description="描述"
image="xxx"
footer="xxx"
showAction
actionText="..."
...
/>
如果不断增加 Props:
text
Props 爆炸
↓
API 越来越复杂
↓
组件越来越难维护
更好的方式:
tsx
<Card>
<Card.Header>
标题
</Card.Header>
<Card.Body>
内容
</Card.Body>
<Card.Footer>
操作
</Card.Footer>
</Card>
也就是:
text
Composition
十一、React 中如何设计一个比较合理的 Button?
完整示例
tsx
import React from "react";
/**
* Button 组件支持的视觉变体。
*
* variant 只负责"视觉语义",
* 不建议使用 primaryButton、blueButton 这种具体视觉名称。
*/
type ButtonVariant =
| "primary"
| "secondary"
| "danger"
| "ghost";
/**
* Button 支持的尺寸。
*/
type ButtonSize = "small" | "medium" | "large";
/**
* Button Props。
*
* extends React.ButtonHTMLAttributes<HTMLButtonElement>
* 的目的:
*
* 让组件天然支持标准 button 属性,
* 例如:
*
* - type
* - disabled
* - name
* - value
* - aria-*
* - onFocus
* - onBlur
* - onKeyDown
* 等。
*
* 这比重新定义一遍 HTML API 更合理。
*/
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/**
* Button 的视觉变体。
*/
variant?: ButtonVariant;
/**
* Button 的尺寸。
*/
size?: ButtonSize;
/**
* loading 状态。
*
* loading 时通常需要:
* 1. 阻止重复提交。
* 2. 告知用户当前正在处理。
*/
loading?: boolean;
/**
* 子内容。
*/
children: React.ReactNode;
}
/**
* 根据 Props 生成 CSS class。
*
* 实际项目中可以使用 class-variance-authority、
* 自己的 className 工具函数,
* 或者其他经过验证的方案。
*/
function getButtonClassName(
variant: ButtonVariant,
size: ButtonSize,
className?: string,
) {
return [
"button",
`button--${variant}`,
`button--${size}`,
className,
]
.filter(Boolean)
.join(" ");
}
/**
* Button 组件。
*/
export function Button({
variant = "primary",
size = "medium",
loading = false,
/**
* disabled 从原生 button API 中继承。
*/
disabled = false,
children,
className,
/**
* 通过解构取出 type,
* 默认使用 button,而不是 submit。
*
* 这是一个非常重要的边界问题:
*
* <button> 在 form 中默认可能具有 submit 行为。
*
* 如果组件库 Button 默认不设置 type,
* 用户在表单里使用时可能发生意外提交。
*/
type = "button",
...rest
}: ButtonProps) {
/**
* loading 时同时视为不可操作。
*
* 这里最终传给原生 button 的 disabled,
* 可以防止点击事件继续触发。
*/
const isDisabled = disabled || loading;
return (
<button
{...rest}
type={type}
disabled={isDisabled}
className={getButtonClassName(
variant,
size,
className,
)}
/**
* aria-busy 表示组件当前正在处理。
*
* 注意:
* aria-busy 和 disabled 解决的是不同问题。
*
* disabled:
* 控制原生 button 是否可以交互。
*
* aria-busy:
* 告诉辅助技术当前内容/操作处于忙碌状态。
*/
aria-busy={loading || undefined}
>
{loading ? (
<>
{/* aria-hidden 防止装饰性的 loading 图标被重复朗读 */}
<span aria-hidden="true" className="button__spinner">
⏳
</span>
{/*
文本用于向视觉用户和辅助技术提供明确状态。
如果产品设计要求,也可以使用 aria-live
或更专门的状态提示方案。
*/}
<span>处理中...</span>
</>
) : (
children
)}
</button>
);
}
十二、关于disabled的补充
对于原生:
html
<button disabled>
浏览器和辅助技术已经能够理解它是 disabled。
一般不需要再:
html
<button disabled aria-disabled="true">
disabled 和 aria-disabled 的真正区别
原生按钮
html
<button disabled>
删除
</button>
disabled:
text
真正改变 HTML 控件行为
↓
不能正常触发点击
不能参与表单提交
不能获得正常的交互焦点
非原生交互元素
例如:
html
<div role="button" aria-disabled="true">
删除
</div>
这里:
text
aria-disabled="true"
主要是告诉辅助技术:
这个元素在语义上处于不可用状态。
但它不会自动阻止 JavaScript 点击逻辑。
所以:
tsx
<div
role="button"
aria-disabled="true"
onClick={handleClick}
/>
仍然可能触发:
text
handleClick()
必须自己阻止。
因此:
ARIA 主要补充语义,不应该被误认为是原生 HTML 行为的替代品。
十三、面试题:为什么组件库应该优先使用语义化 HTML?
核心思路(一句话)
原生 HTML 自带语义、键盘行为、焦点管理和辅助技术支持,优先使用原生元素可以显著降低无障碍实现成本。
例如:
错误:
tsx
<div onClick={handleClick}>
提交
</div>
正确:
tsx
<button type="button" onClick={handleClick}>
提交
</button>
因为 <button> 天然支持:
text
点击
键盘操作
焦点
辅助技术
disabled
表单语义
而 div 什么都没有。
十四、面试题:组件库如何设计可访问性?
核心思路(一句话)
先使用正确的原生语义,再解决键盘、焦点、状态、名称和辅助技术支持,最后用自动化和人工测试验证。
完整流程
text
语义化 HTML
↓
正确的 Accessible Name
↓
键盘可操作
↓
Focus 管理
↓
状态表达
↓
ARIA
↓
颜色 / 对比度
↓
屏幕阅读器
↓
自动化测试 + 人工测试
重点关注
Button
text
Enter
Space
Focus
Disabled
Loading
Accessible Name
Dialog
text
焦点进入 Dialog
↓
焦点不能意外逃逸
↓
Escape 关闭
↓
关闭后焦点返回触发元素
Select
text
ArrowUp
ArrowDown
Enter
Escape
Home
End
Tooltip
不能简单依赖:
text
hover
因为键盘用户没有鼠标 Hover。
十五、面试题:组件库技术架构怎么设计?
核心思路(一句话)
技术选型优先考虑生态、团队能力、业务场景和兼容范围,而不是追求技术先进本身。
React 组件库典型架构
text
Component Library
│
┌──────────────────┼──────────────────┐
↓ ↓ ↓
Components Styles Utilities
│ │ │
Button/Input Token DOM
Modal/Table CSS Hooks
Select/Tree Theme Utils
│ │ │
└──────────────────┼──────────────────┘
↓
Build System
↓
ESM / TypeScript
↓
npm
↓
Application
十六、React、Vue、Web Components 怎么选?
| 方案 | 特点 | 适合场景 |
|---|---|---|
| React | React 生态成熟 | React 技术栈 |
| Vue | Vue 生态成熟 | Vue 技术栈 |
| Web Components | 浏览器原生标准 | 跨框架复用 |
| 多框架封装 | React/Vue 各提供一层 | 大型企业生态 |
不要为了"跨框架"盲目选择 Web Components
因为 Web Components 虽然可以跨框架,但复杂组件仍然需要处理:
text
属性映射
事件
表单参与
Slot
样式隔离
SSR
框架适配
类型定义
生态工具
所以应该:
text
如果公司全部是 React
↓
优先 React Component
而:
text
React + Vue + Angular
↓
才有充分理由考虑
Web Components / Headless Core
十七、更高级的组件库架构:Headless + UI
这是高级面试非常值得补充的方案。
架构
text
Component
│
┌────────┴────────┐
↓ ↓
Behavior UI
│ │
状态 / 键盘 / 焦点 样式 / DOM
│ │
↓ ↓
Headless Logic Styled Component
例如:
text
Combobox
可以拆成:
text
Combobox Behavior
↓
React
Vue
Web Component
↓
不同 UI
为什么这么做?
因为:
text
行为逻辑
和:
text
视觉样式
其实是两个维度。
例如:
text
键盘导航
焦点管理
选择状态
搜索逻辑
可以复用。
但是:
text
Ant Design 风格
Material 风格
公司内部风格
可以不同。
十八、面试题:组件库 CSS 应该怎么设计?
核心思路(一句话)
优先保证样式隔离、主题能力、可覆盖性和运行时成本之间的平衡。
常见方案:
text
CSS Modules
CSS Variables
CSS-in-JS
Tailwind CSS
Sass
Less
原生 CSS
对组件库而言,一个很重要的原则
不要污染全局 CSS
组件库尽量避免:
css
button {
...
}
div {
...
}
* {
...
}
否则:
text
组件库
↓
污染业务应用
↓
业务样式被影响
↓
冲突
十九、CSS Modules 为什么适合组件库?
例如:
css
/* Button.module.css */
.button {
padding: 8px 16px;
}
.primary {
background: var(--color-primary);
}
构建之后类似:
text
button_x7f82
primary_a92kd
这样:
text
局部 CSS
↓
自动生成唯一 class
↓
降低命名冲突
二十、为什么 CSS Variables 特别适合主题系统?
因为它支持:
text
运行时修改
例如:
js
document.documentElement.style.setProperty(
"--color-primary",
"#ff0000",
);
不需要:
text
重新构建 CSS
重新加载 CSS
重新生成组件
因此非常适合:
text
Dark Mode
品牌主题
租户主题
动态换肤
二十一、面试题:组件库如何设计包结构?
核心思路(一句话)
让消费者能够只引入真正使用的代码,同时让公共依赖得到复用。
例如:
text
packages/
├── components/
│ ├── button/
│ ├── input/
│ ├── modal/
│ └── table/
│
├── hooks/
│
├── utils/
│
├── tokens/
│
└── icons/
对外:
text
@company/ui
@company/ui/button
@company/ui/input
@company/ui/modal
二十二、Tree Shaking 到底是怎么工作的?
这是非常高频的追问。
核心思路(一句话)
Tree Shaking 本质是构建工具基于静态模块依赖关系删除确定不会被使用的代码。
例如:
ts
// math.ts
export function add(a: number, b: number) {
return a + b;
}
export function multiply(a: number, b: number) {
return a * b;
}
业务:
ts
import { add } from "./math";
console.log(add(1, 2));
如果构建工具能够静态分析:
text
使用 add
未使用 multiply
就可能最终删除:
text
multiply
二十三、为什么 ES Module 更利于 Tree Shaking?
因为:
ts
import
export
具有静态结构。
例如:
ts
import { Button } from "@company/ui";
构建工具可以在构建阶段分析模块依赖。
而 CommonJS:
js
const moduleName = getModuleName();
require(moduleName);
模块依赖可能直到运行时才确定。
所以现代组件库:
应该优先提供 ES Module。
二十四、纠正:UMD 并不是现代组件库的必选项
一些旧的面试答案说:
text
ESM
CommonJS
UMD
都应该支持
这个观点需要更新。
现代前端组件库通常优先:
text
ESM
TypeScript 类型
是否支持 CommonJS,要根据:
text
Node.js 版本
构建环境
历史项目
消费者环境
决定。
UMD 更适合:
text
浏览器直接通过 script 标签加载
如果目标用户全部使用现代构建工具:
text
Vite
Webpack
Rspack
Rollup
通常没有必要为了"兼容"而强制输出 UMD。
二十五、面试题:Tree Shaking 为什么可能失效?
这是比"支持 Tree Shaking"更高级的回答。
可能原因:
text
CommonJS
↓
静态分析困难
副作用代码
↓
不能安全删除
错误的 package.json 配置
↓
构建工具无法正确判断
barrel 文件导出方式
↓
可能扩大模块依赖
第三方依赖本身不支持 Tree Shaking
↓
最终仍然进入 Bundle
因此组件库不仅要:
text
提供 ESM
还应该:
text
控制副作用
正确配置 package.json
优化导出结构
分析 Bundle
治理第三方依赖
二十六、sideEffects 是什么?
例如:
json
{
"sideEffects": false
}
它告诉构建工具:
这个包中的模块默认没有执行副作用,可以更激进地进行 Tree Shaking。
但是这里有一个非常危险的地方:
如果存在:
ts
import "./global.css";
这种代码,CSS 导入本身就是有副作用的。
如果错误声明:
json
{
"sideEffects": false
}
可能导致:
text
CSS 被错误删除
↓
组件没有样式
因此不能机械地写:
json
"sideEffects": false
需要根据真实代码确定。
二十七、面试题:组件库应该如何按需加载?
核心思路(一句话)
优先保证模块可被静态分析,并通过 ESM、子路径导出和构建工具实现真正的按需引入。
例如:
ts
import { Button } from "@company/ui";
如果整个包的结构设计良好,构建工具可以只保留 Button。
也可以:
ts
import { Button } from "@company/ui/button";
这种方式对大型组件库尤其直接。
二十八、组件库构建应该怎么设计?
一个典型流程:
text
TypeScript
↓
源代码
↓
Build
↓
┌───────────────┐
│ ESM │
│ Type Declarations
│ CSS │
└───────────────┘
↓
npm Package
↓
Consumer
推荐现代技术栈
例如:
text
TypeScript
Vite / Rollup / Rspack
pnpm
Monorepo
Storybook
Vitest
Testing Library
Playwright
Changesets
不要求全部使用。
核心是:
每一个工具都要对应一个工程问题,而不是为了堆技术栈。
二十九、面试题:为什么组件库适合 Monorepo?
核心思路(一句话)
组件库往往由多个相互依赖的包组成,Monorepo 可以统一管理源码、依赖、版本、测试和发布。
例如:
text
packages/
│
├── ui
├── icons
├── hooks
├── utils
├── tokens
└── eslint-config
它们之间可能:
text
ui
↓
hooks
↓
utils
使用 Monorepo 后:
text
统一代码规范
统一构建
统一测试
统一 CI
统一发布
统一依赖管理
三十、面试题:组件库为什么需要 Storybook?
核心思路(一句话)
Storybook 的核心价值不是"展示组件",而是把组件变成可独立开发、调试、测试和验证的开发单元。
例如 Button:
text
Button
├── Default
├── Primary
├── Disabled
├── Loading
├── Small
├── Large
└── WithIcon
这样设计师、开发者、测试人员都可以直接查看。
三十一、组件库测试应该怎么设计?
核心思路(一句话)
单元测试保证逻辑正确,集成测试保证组件协作正确,视觉回归保证 UI 稳定,端到端测试保证真实用户流程。
测试金字塔
text
E2E
/ \
Integration
/ \
Unit / Component
1. 单元 / 组件测试
测试:
text
Props
状态
事件
边界条件
例如:
text
disabled Button
点击不触发
2. 集成测试
例如:
text
Form
↓
Input
↓
Validation
↓
Submit
测试多个组件组合后的行为。
3. Visual Regression
例如:
text
组件当前截图
↓
历史基准截图
↓
Pixel Difference
↓
发现视觉变化
特别适合:
text
Button
Table
Modal
DatePicker
复杂表单
4. E2E
例如:
text
登录
↓
打开表单
↓
输入
↓
提交
↓
Toast
不需要每个 Button 都做 E2E。
三十二、面试题:组件库如何设计性能优化?
核心思路(一句话)
先控制 Bundle 成本,再控制运行时渲染成本,最后针对重型组件做专项优化。
第一层:Bundle
text
ESM
↓
Tree Shaking
↓
按需加载
↓
依赖优化
↓
减少重复依赖
第二层:Runtime
例如 React:
tsx
import React from "react";
/**
* 一个简单的 Item 组件。
*
* React.memo 的作用:
* 当父组件重新渲染时,
* 如果 Item 的 props 没有变化,
* React 可以跳过该组件的一次重新渲染。
*/
const Item = React.memo(function Item({
name,
}: {
name: string;
}) {
return <div>{name}</div>;
});
但是:
不要把 React.memo 当成"组件库性能优化的万能答案"。
如果:
tsx
<Item
data={{ name: "张三" }}
/>
每次父组件都创建新的对象:
text
{} !== {}
那么 memo 仍然可能重新渲染。
所以真正要关注的是:
text
Props 稳定性
↓
组件粒度
↓
状态位置
↓
Context 范围
↓
渲染次数
三十三、事件委托应该怎么理解?
提到事件委托,这个方向没错,但不能作为组件库的普遍优化手段。
例如:
text
10000 个 item
如果每个 item 都挂一个监听器:
text
item1 → click
item2 → click
item3 → click
...
可以考虑父元素:
text
List
↓
click
↓
event.target
但是现代浏览器事件监听本身通常并不是首先需要优化的问题。
所以面试最好说:
事件委托适合大量同类 DOM 节点的事件管理,但是否值得使用需要基于实际性能数据,而不是默认使用。
三十四、长列表为什么需要虚拟化?
假设:
text
100,000 条数据
普通列表:
text
100,000 DOM Nodes
会产生:
text
DOM 数量巨大
Layout 成本
Paint 成本
内存成本
React reconciliation 成本
虚拟列表:
text
100000 数据
↓
当前可视区域
↓
只渲染几十个节点
架构:
text
Data: 100000
↓
Viewport
↓
Visible Range
↓
Render 30 Items
三十五、懒加载应该用在哪里?
适合:
text
大型组件
复杂编辑器
图表
富文本
非首屏组件
重量级依赖
例如:
tsx
const HeavyEditor = React.lazy(
() => import("./HeavyEditor")
);
然后:
tsx
<React.Suspense fallback={<div>加载中...</div>}>
<HeavyEditor />
</React.Suspense>
三十六、面试题:组件库如何处理依赖?
核心思路(一句话)
组件库必须区分运行时依赖、开发依赖和宿主依赖,避免把 React 等基础依赖重复打进消费者应用。
例如 React 组件库:
text
react
react-dom
通常应该根据发布目标考虑放入:
text
peerDependencies
而不是简单地打包进组件库。
原因:
text
业务应用
↓
React 19.x
组件库自己又带一个 React
↓
React 另一个版本
可能造成:
text
重复依赖
版本冲突
运行时问题
Bundle 变大
三十七、面试题:组件库版本如何管理?
核心思路(一句话)
以 Semantic Versioning 为基础,让版本号准确表达兼容性变化,并配合 Changelog、Deprecated 和迁移指南降低升级成本。
Semantic Versioning 语义化版本规范:
text
MAJOR.MINOR.PATCH
例如:
text
2.4.3
│ │ │
│ │ └── PATCH
│ └──── MINOR
└────── MAJOR
三十八、什么情况下升 MAJOR?
通常是:
text
破坏性 API 变化
例如:
tsx
<Button type="primary" />
变成:
tsx
<Button variant="primary" />
如果删除:
text
type="primary"
原用户代码无法正常工作,就属于 Breaking Change。
三十九、什么情况下升 MINOR?
新增向后兼容功能:
tsx
<Button loading />
原来的:
tsx
<Button />
仍然正常。
那么:
text
MINOR
四十、什么情况下升 PATCH?
例如:
text
Bug Fix
:
text
Button disabled 状态样式错误
修复它:
text
PATCH
四十一、弃用 API 怎么处理?
不要:
text
今天删除
明天用户报错
应该:
text
旧 API
↓
标记 Deprecated
↓
文档说明替代 API
↓
控制台开发环境警告
↓
保留过渡周期
↓
Migration Guide
↓
Major Version 删除
示例
旧:
tsx
<Button type="primary" />
新:
tsx
<Button variant="primary" />
迁移:
text
v4
↓
type deprecated
↓
v5
↓
type 仍支持 + warning
↓
v6
↓
删除 type
四十二、面试题:组件库发布流程怎么设计?
核心思路(一句话)
发布必须自动化,让"代码合并 → 测试 → 构建 → 版本 → 发布 → Changelog"形成可追溯流水线。
CI/CD 流程
text
Pull Request
↓
Lint
↓
Type Check
↓
Unit Test
↓
Integration Test
↓
Visual Regression
↓
Build
↓
Bundle Size Check
↓
Review
↓
Merge
↓
Changeset
↓
Version
↓
npm Publish
↓
Changelog
↓
Documentation
四十三、为什么需要 Bundle Size Check?
因为组件库是基础设施。
如果某次提交:
text
Button
突然从:
text
5 KB
变成:
text
500 KB
即使:
text
测试全部通过
也不应该直接发布。
所以可以设置:
text
Bundle Size Budget
例如:
text
Button < 10 KB
核心运行时 < 50 KB
具体阈值需要结合压缩方式和真实项目确定。
四十四、组件库应该如何做浏览器兼容?
不要直接回答:
"支持所有浏览器。"
应该先定义:
text
支持哪些浏览器?
最低版本是多少?
是否支持 SSR?
是否支持移动端?
是否支持旧项目?
然后:
text
Browserslist
↓
Transpile
↓
Polyfill
↓
CSS Prefix
↓
Browser Test
四十五、SSR 场景有什么坑?
如果组件库需要支持 SSR:
要注意:
text
window
document
localStorage
ResizeObserver
IntersectionObserver
这些浏览器 API 在服务端可能不存在。
错误:
tsx
const width = window.innerWidth;
组件模块一加载:
text
SSR
↓
window undefined
↓
Crash
更安全:
tsx
import { useEffect, useState } from "react";
export function ViewportWidth() {
const [width, setWidth] = useState<number | null>(null);
useEffect(() => {
/**
* useEffect 只会在浏览器客户端执行。
*
* 因此这里访问 window,
* 不会在服务器渲染阶段直接执行。
*/
const updateWidth = () => {
setWidth(window.innerWidth);
};
updateWidth();
window.addEventListener("resize", updateWidth);
return () => {
window.removeEventListener("resize", updateWidth);
};
}, []);
return <span>{width ?? "加载中..."}</span>;
}
四十六、React 组件库还有一个非常重要的问题:Ref
例如:
tsx
<Button ref={buttonRef} />
消费者可能希望:
text
focus()
click()
measure()
因此组件库要考虑:
text
Ref Forwarding
对于 React 版本差异,需要结合目标 React 版本设计 API,不能机械套用历史上的 forwardRef 模式。
核心思想始终是:
text
组件包装 DOM
↓
不要把底层 DOM 能力完全封死
↓
提供稳定的 ref / imperative API
四十七、面试题:组件 API 为什么需要"难以误用"?
这是高级组件设计非常重要的思想。
例如 Button:
tsx
<Button />
默认:
text
type="button"
而不是让它在 Form 中意外:
text
submit
再例如:
tsx
<Button loading>
提交
</Button>
应该自动:
text
loading
↓
阻止重复点击
↓
disabled
而不是让业务自己:
tsx
<Button
loading={loading}
disabled={loading}
onClick={loading ? undefined : submit}
/>
组件库的价值之一就是:
把正确的行为封装起来,让消费者更难写错。
四十八、边界场景:Button 应该考虑什么?
一个成熟的 Button 至少需要考虑:
text
Default
Hover
Active
Focus
Disabled
Loading
Icon
Icon-only
Long Text
RTL
Dark Theme
High Contrast
Keyboard
Form
Mobile
Touch
SSR
四十九、Icon-only Button 是一个经典无障碍边界
错误:
tsx
<button>
<DeleteIcon />
</button>
辅助技术可能不知道:
text
这是干什么的?
应该:
tsx
<button
type="button"
aria-label="删除"
>
<DeleteIcon aria-hidden="true" />
</button>
这里:
text
aria-label
提供可访问名称。
而:
text
aria-hidden="true"
告诉辅助技术:
这个图标只是装饰,不需要重复朗读。
五十、面试题:组件库最大的架构风险是什么?
核心思路(一句话)
最大的风险不是某个组件写得不好,而是抽象失控,最终形成"既不统一又难以修改"的组件 API。
典型过程:
text
业务 A
↓
加一个 Props
业务 B
↓
再加一个 Props
业务 C
↓
再加一个 Props
业务 D
↓
特殊逻辑
最终:
tsx
<Component
mode=""
type=""
variant=""
custom=""
special=""
enableX=""
enableY=""
renderHeader=""
renderFooter=""
...
/>
最终:
text
API 爆炸
↓
理解困难
↓
维护困难
↓
无法删除历史 API
五十一、如何避免组件 API 爆炸?
推荐:
text
稳定属性
+
Composition
+
Slot / Children
+
Render Props
+
CSS Variables
+
扩展点
而不是:
text
所有业务差异
→
新增一个 boolean Props
五十二、组件库的抽象边界怎么判断?
可以问三个问题:
1. 是否稳定?
text
多个业务都存在
2. 是否通用?
text
不依赖特定业务
3. 是否存在统一行为?
text
视觉统一
交互统一
状态统一
如果答案都是否:
text
不要急着进入基础组件库
五十三、组件库如何处理"定制化"?
核心思路(一句话)
优先提供有限且稳定的扩展点,而不是允许业务无限覆盖组件内部实现。
推荐:
text
Design Token
↓
CSS Variables
↓
className
↓
slot / children
↓
render props
谨慎允许:
text
直接覆盖内部 DOM
!important
全局 CSS 覆盖
深层 CSS Selector
五十四、为什么不推荐大量 !important?
因为:
css
.component {
color: red !important;
}
之后业务:
css
.page .component {
color: blue;
}
可能仍然无法覆盖。
最终:
text
!important
!important
!important
形成 CSS 权重战争。
所以组件库应该从架构层面提供:
text
Token
CSS Variables
明确 class
扩展 API
而不是逼用户改内部样式。
五十五、组件库是否应该暴露内部 DOM 结构?
通常应该谨慎。
例如:
text
.my-component > div > span > button
如果消费者大量依赖这个结构:
text
组件内部重构
↓
消费者全部崩
因此:
DOM 结构属于实现细节,公开 API 才属于稳定契约。
五十六、面试题:如何衡量组件库是否成功?
这个问题非常重要。
不要只说:
text
组件数量
更应该看:
text
组件使用率
↓
业务接入率
↓
重复代码下降
↓
开发时间下降
↓
Bug 下降
↓
视觉一致性
↓
升级成功率
↓
Bundle 成本
↓
Issue / 反馈
例如:
text
80% 的新页面使用 Button
95% 的表单使用 Form
组件相关 Bug 下降 30%
业务开发时间下降 20%
这些指标比:
text
"我们有 150 个组件"
更有意义。
五十七、一个完整的组件库架构
可以把前面的知识串成这样:
text
Design System
│
┌─────────────┼─────────────┐
↓ ↓ ↓
Tokens Patterns Accessibility
│
↓
┌───────────────────┐
│ Component Library │
└───────────────────┘
│
┌────────┼─────────┐
↓ ↓ ↓
UI Hooks Utils
│
↓
React Components
│
↓
TypeScript
│
↓
┌─────────────────────┐
│ Build System │
│ ESM │
│ Type Declarations │
│ CSS │
└─────────────────────┘
│
↓
npm Package
│
↓
┌─────────────────────┐
│ Consumer Application│
└─────────────────────┘
│
↓
Monitoring / Feedback
│
↓
Version Iteration
五十八、如果面试官继续追问:"你会怎么从零落地?"
可以直接回答:
text
第一阶段:需求
↓
确定目标用户、技术栈、浏览器范围
第二阶段:设计系统
↓
建立 Design Token
↓
颜色 / 字体 / 间距 / 圆角 / 阴影
第三阶段:组件
↓
Button / Input / Form / Modal
↓
定义状态和 API
第四阶段:基础设施
↓
TypeScript
↓
Monorepo
↓
Build
↓
ESM
↓
CSS
第五阶段:质量
↓
Unit
↓
Integration
↓
Visual Regression
↓
E2E
第六阶段:开发体验
↓
Storybook
↓
文档
↓
示例
↓
Playground
第七阶段:发布
↓
Changeset
↓
Semantic Versioning
↓
CI/CD
↓
npm
第八阶段:持续治理
↓
兼容性
↓
Deprecated
↓
Migration
↓
性能监控
↓
社区反馈
五十九、最终整理:组件库设计面试题清单
建议把这个 Topic 记成下面 15 个核心问题:
1. 如果让你从零设计一个组件库,你会考虑哪些方面?
核心:
text
目标
→ Design System
→ API
→ 组件架构
→ A11y
→ 工程化
→ 性能
→ 测试
→ 发布
→ 版本治理
2. 组件库和 Design System 有什么区别?
text
Design System = 规则体系
Component Library = 工程实现
3. 如何设计 Design Token?
text
基础 Token
→ 语义 Token
→ CSS Variables
→ Component
4. 如何设计组件 API?
text
简单
一致
可预测
难误用
可组合
5. 基础组件和业务组件如何划分?
text
基础组件低业务耦合
业务组件高业务耦合
6. React 组件库为什么应该优先使用语义化 HTML?
text
原生语义
+ 键盘
+ Focus
+ Accessibility
7. disabled 和 aria-disabled 有什么区别?
text
disabled = 原生行为
aria-disabled = 辅助技术语义
8. 如何设计主题系统?
text
Design Token
→ CSS Variables
→ Theme
9. 如何保证组件库 Tree Shaking?
text
ESM
+ 正确 exports
+ sideEffects
+ 减少副作用
+ 依赖治理
10. 组件库为什么适合 Monorepo?
text
统一开发
统一依赖
统一测试
统一构建
统一发布
11. 组件库如何测试?
text
Unit
→ Integration
→ Visual Regression
→ E2E
12. 组件库如何优化性能?
text
Bundle
→ Tree Shaking
→ 按需加载
→ 依赖治理
→ Runtime
→ Virtualization
13. 如何设计版本体系?
text
Semantic Versioning
+ Changelog
+ Deprecated
+ Migration
14. 如何避免组件 API 爆炸?
text
Composition
+ Slots / Children
+ Render Props
+ Token
+ 扩展点
15. 如何判断一个组件库设计是否成功?
text
使用率
+ 一致性
+ 开发效率
+ Bug
+ 性能
+ 升级成本
+ 用户反馈
六十、满分答案 1:面试时可以直接这样回答
如果让我从零设计一个组件库,我不会先从 Button、Input 这些组件开始,而会先明确组件库服务的用户、业务场景、技术栈和浏览器兼容范围。
整体上我会从设计系统、组件 API、技术架构、可访问性、性能、测试、开发体验以及版本治理几个层面设计。
第一层是 Design System。我会先建立统一的 Design Token,比如颜色、字体、间距、圆角、阴影等,然后通过 CSS 自定义属性把 Token 暴露给组件,这样可以统一视觉,也方便支持深色主题、品牌主题和多租户定制。
第二层是 组件设计 。我会按照基础组件、复合组件和业务组件进行分层,基础组件尽量保持低业务耦合。API 设计遵循简单、统一、可预测和难以误用的原则,例如 React Button 优先使用
disabled、size、variant、loading,事件统一使用onClick、onChange这类约定。对于复杂组件,我会优先使用children、组合模式和扩展点,而不是不断增加 Boolean Props,避免 API 爆炸。第三层是 可访问性 。我会优先使用语义化 HTML,比如按钮使用
<button>,表单控件使用正确的原生元素。在此基础上处理键盘操作、焦点管理、可访问名称、状态和 ARIA。需要特别注意,aria-disabled只是辅助技术语义,并不会像原生disabled一样自动禁止交互,所以原生 Button 应优先使用disabled。第四层是 技术架构和工程化。如果主要服务 React 项目,我会使用 TypeScript 构建 React 组件;如果存在 React、Vue 等多框架需求,再考虑 Web Components 或 Headless Core。工程上可以采用 Monorepo 管理 components、hooks、utils、tokens、icons 等包,使用现代构建工具输出 ESM 和 TypeScript 类型,并通过正确的模块导出和副作用声明保证 Tree Shaking。
第五层是 性能 。我会把性能分成 Bundle 和 Runtime 两部分。Bundle 侧主要关注 ESM、Tree Shaking、按需加载以及第三方依赖治理;Runtime 侧根据实际性能数据优化组件渲染、状态范围、长列表虚拟化和重量级组件懒加载,而不是简单地到处使用
React.memo。第六层是 开发体验和质量体系。我会使用 Storybook 或类似工具提供交互式文档和组件 Playground,同时建立单元测试、组件集成测试、视觉回归测试和必要的端到端测试。代码层面通过 TypeScript、ESLint、Formatter、CI 等保证质量。
第七层是 版本和发布治理。组件库发布后最大的成本其实不是写新组件,而是维护兼容性。因此我会使用 Semantic Versioning,通过 MAJOR、MINOR、PATCH 表达破坏性变更、新功能和 Bug 修复,并配合 Changelog、Deprecated API、Migration Guide 和 CI/CD,保证组件库可以长期演进。
如果总结整个架构,我认为组件库最核心的矛盾是统一性和灵活性的平衡。统一性保证设计和交互一致,灵活性保证业务能够真正落地。所以我不会让组件无限开放内部实现,而是通过 Design Token、Composition、Children、扩展点和 CSS Variables 提供有限且稳定的定制能力。
最终,一个成熟的组件库不是 Button、Input、Modal 的简单集合,而是一套由 Design System、组件实现、工程化、测试、发布和版本治理共同组成的前端基础设施。
面试官继续追问时,最值得深挖的 8 个方向
text
① Tree Shaking 为什么能够工作?
↓
ES Module 静态依赖分析
↓
sideEffects
↓
副作用
↓
Bundle 分析
② 为什么使用 CSS Variables 做主题?
↓
运行时修改
↓
无需重新构建
↓
主题继承
③ 为什么基础组件和业务组件要分离?
↓
业务耦合
↓
复用边界
↓
维护成本
④ 为什么 Composition 比大量 Props 更好?
↓
避免 API 爆炸
↓
开放扩展能力
↓
稳定核心 API
⑤ disabled 和 aria-disabled 区别?
↓
原生行为
↓
ARIA 语义
↓
可访问性
⑥ 为什么组件库使用 Monorepo?
↓
多 Package
↓
依赖关系
↓
统一构建测试发布
⑦ 如何保证组件升级不影响业务?
↓
Semantic Versioning
↓
Deprecated
↓
Migration
↓
Visual Regression
⑧ 如何设计跨框架组件库?
↓
Web Components
↓
Headless Core
↓
Framework Adapter
这一题真正的高分关键不是"能背出多少组件",而是让面试官听到你具备从"组件实现"上升到"基础设施设计"的能力。
如果继续深挖这个 Topic,建议下一步直接做 "组件库源码架构设计 + Monorepo + Tree Shaking + 构建发布 + Button/Modal/Select 源码级实现",这是更接近高级前端面试的版本。