真正适合高级前端 / React 面试的“组件库设计”面试题体系

面试时建议把这个问题理解成:

组件库不是"写一堆组件",而是设计一套可复用、可约束、可扩展、可发布、可持续演进的前端基础设施。


一、面试题:如果让你从零设计一个组件库,你会考虑哪些方面?

核心思路(一句话)

先确定设计系统和组件边界,再设计组件 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">

disabledaria-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. disabledaria-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 优先使用 disabledsizevariantloading,事件统一使用 onClickonChange 这类约定。对于复杂组件,我会优先使用 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 源码级实现",这是更接近高级前端面试的版本。

相关推荐
DsirNg1 个月前
先守住数据,再下沉交互:RSC 组件分层的实战决策法
react·next.js·app router·前端架构·组件设计·react server components·server actions
带娃的IT创业者7 个月前
解密OpenClaw系列08-OpenClaw组件交互关系(1)
软件工程·交互·ai编程·ai智能体·智能体开发·openclaw·组件设计
带娃的IT创业者7 个月前
系列5/5-WinClaw工程实战:从“写博客“灾难到TaskTrace追踪系统的涅槃重生
软件工程·ai agent·智能体开发·openclaw·编程文档·组件设计·winclaw
带娃的IT创业者7 个月前
解密OpenClaw系列08-OpenClaw组件交互关系(2)
软件工程·ai编程·代码规范·ai智能体·openclaw·编程文档·组件设计