问题 1:请介绍一下这个组件库项目
标准回答(3-5 分钟)
项目背景 我们公司有 6 个业务线并行开发,前端团队规模在 30 人左右。之前各个项目都是独立开发 UI 组件,导致出现了几个问题:
- 同样的 Button、Table 组件被重复开发了 5-6 次
- 不同系统的交互体验不统一,用户反馈割裂感强
- 代码维护成本高,同样的 Bug 要修复多次
- 新人上手慢,每个项目都要重新学习
所以我主导从 0 到 1 设计并落地了这个企业级组件库。
技术选型
- 选择 Vue3 是因为它的 Composition API 有更好的逻辑复用能力和类型推导
- 构建工具用 Vite + Rollup,Vite 开发体验快,Rollup 打包产物可控
- 样式方案用 CSS Variables + Less,可以实现运行时主题切换,零编译成本
- 文档用 Storybook,能把组件文档和开发调试结合在一起
- 包管理用 pnpm + changeset,管理 monorepo 并自动化版本发布
核心工作 我主要做了三方面工作:
- 组件体系建设:沉淀了 40+ 个通用组件,分为基础组件、表单组件、数据展示、反馈组件、导航组件五大类。每个组件都经过精心设计,支持灵活的配置和扩展。
- 性能优化:
- 实现了虚拟滚动,Table 组件可以流畅渲染 10 万条数据
- 支持按需加载,打包体积从 800KB 降到 120KB
- 封装了一系列性能优化的 hooks,比如 useVirtualList、useDebouncedRef
- 主题系统:基于 CSS Variables 实现主题切换,定义了 100+ 个设计变量,支持亮色暗色两套主题,不同业务线可以定制自己的品牌色。
项目成果
- 组件库覆盖了公司 6 个业务系统
- 减少重复开发工作量 60%,节省人力成本 10 人月/年
- 前端开发效率提升 40%,需求交付周期缩短 30%
- 组件 Bug 率下降 30%,因为代码复用度高,问题集中修复
问题 2:组件库的架构是如何设计的?
标准回答(2-3 分钟)
我设计的是一个 monorepo 架构,主要分为三层:
第一层:组件层(packages/components) 这是核心,包含 40+ 个组件。每个组件都是一个独立的文件夹,包含:
- index.vue:组件实现
- index.js:组件注册和导出
- style/:组件样式(Less)
- tests/:单元测试
组件之间通过 Composition API 共享逻辑,比如 useFormItem、useTheme 这些公共 hooks。
第二层:基础设施层 包含三个模块:
- theme:主题系统,定义 CSS Variables 和 Less 变量
- utils:工具函数库,比如类型判断、DOM 操作
- hooks:通用的 Composition API hooks
第三层:构建和文档层
- Vite + Rollup 负责构建,输出 ESM、CJS、UMD 三种格式
- Storybook 负责文档和组件调试
- pnpm workspace 管理 monorepo
- changeset 管理版本发布
为什么这样设计?
- 解耦:每个组件独立维护,互不影响
- 复用:公共逻辑提取到 hooks 和 utils,避免重复
- 扩展性:新增组件只需要在 components 文件夹新建目录
- 可维护:职责清晰,每个模块功能单一
构建流程 我配置了 Rollup 的构建流程:
- 解析所有组件的入口文件
- 使用 @vue/compiler-sfc 编译 .vue 文件
- Less 编译成 CSS,并提取 CSS Variables
- Tree Shaking 优化,支持按需加载
- 输出三种格式:ESM(给 Vite 用)、CJS(给 Webpack 用)、UMD(给 CDN 用)
问题 3:如何实现按需加载?
标准回答(2-3 分钟)
按需加载是这个项目的核心优化之一,我从三个维度实现:
1. 构建层面:ES Module + Tree Shaking
首先,组件库要基于 ES Module 打包,Rollup 配置关键点:
export default {
output: {
format: 'es',
preserveModules: true, // 保留模块结构
dir: 'es',
},
external: ['vue'], // 外部化 Vue
}
这样每个组件都是独立的模块,打包工具可以进行 Tree Shaking。
2. 组件注册方式
提供两种注册方式:
// 全量引入(不推荐)
import VUI from 'vue3-ui-library'
app.use(VUI)
// 按需引入(推荐)
import { Button, Input } from 'vue3-ui-library'
app.use(Button).use(Input)
关键是在 package.json 中配置:
{
"main": "lib/index.js", // CJS 格式
"module": "es/index.js", // ESM 格式
"sideEffects": [
// 标记副作用
"*.css",
"*.less"
]
}
3. 样式按需加载
样式分离是难点,我的方案是:
- 每个组件的样式独立打包成一个 .css 文件
- 在组件 JS 中不直接 import 样式
- 提供 babel-plugin 自动引入样式
用户配置 babel 插件后:
import { Button } from 'vue3-ui-library'
// 自动转换为
import { Button } from 'vue3-ui-library'
import 'vue3-ui-library/es/button/style'
效果验证
我做过测试:
- 全量引入:打包体积 800KB(gzip 后 280KB)
- 按需引入 5 个组件:120KB(gzip 后 40KB)
- Tree Shaking 生效率:85%
实际业务中,大多数页面只用到 5-10 个组件,所以按需加载效果非常明显。
遇到的坑
- CSS 副作用问题:一开始样式 Tree Shaking 不生效,发现要在 package.json 配置 sideEffects
- 样式加载顺序:全局样式和组件样式的加载顺序会影响优先级,需要规划好 CSS 层级
- 第三方依赖:有些组件依赖第三方库(如 date-fns),要配置 external 避免打包进来
问题 4:如何实现主题切换?
标准回答(2-3 分钟)
主题切换是组件库的核心能力,我设计了一套基于 CSS Variables 的主题系统。
为什么选择 CSS Variables?
- 运行时切换:不需要重新编译,改变 CSS 变量立即生效
- 零成本:不增加打包体积,不影响性能
- 灵活性:可以通过 JS 动态修改,支持个性化定制
主题系统设计
定义了 100+ 个设计变量,分为几类:
:root {
/* 品牌色 */
--primary-color: #1890ff;
--success-color: #52c41a;
--warning-color: #faad14;
--error-color: #f5222d;
/* 中性色 */
--text-color: #000000d9;
--text-color-secondary: #00000073;
--border-color: #d9d9d9;
--bg-color: #ffffff;
/* 尺寸 */
--border-radius-base: 4px;
--padding-base: 12px;
/* 阴影 */
--shadow-base: 0 2px 8px rgba(0, 0, 0, 0.15);
}
暗色主题:
[data-theme='dark'] {
--text-color: #ffffffd9;
--bg-color: #141414;
--border-color: #434343;
}
主题切换实现
封装了一个 useTheme hook:
export function useTheme() {
const theme = ref(localStorage.getItem('theme') || 'light')
const setTheme = (value) => {
theme.value = value
document.documentElement.setAttribute('data-theme', value)
localStorage.setItem('theme', value)
}
return { theme, setTheme }
}
组件中使用
.v-button {
background-color: var(--primary-color);
color: var(--bg-color);
border-radius: var(--border-radius-base);
}
进阶能力
- 动态主题定制: 提供 ConfigProvider 组件,支持运行时修改任意变量:
<v-config-provider :theme="{ primaryColor: '#ff0000' }">
<App />
</v-config-provider>
- 颜色阶梯生成: 基于主色自动生成 10 个色阶,用于 hover、active 等状态:
function generateColorPalette(color) {
// 使用 HSL 色彩空间,调整明度和饱和度
return Array.from({ length: 10 }, (_, i) => {
const lightness = 50 + (5 - i) * 8
return `hsl(h, s, ${lightness}%)`
})
}
遇到的难点
- 主题切换闪烁:
- 问题:切换主题时页面闪烁
- 解决:使用 document.startViewTransition API 平滑过渡,降级用 CSS transition
- SSR 主题不一致:
- 问题:服务端渲染时主题和客户端不一致
- 解决:在 HTML 中内联主题脚本,优先于组件渲染执行
- Less 变量和 CSS Variables 的配合:
- Less 编译时变量用于计算,CSS Variables 用于运行时切换
- 需要设计好两者的分工和转换逻辑
问题 5:Table 组件的虚拟滚动是如何实现的?
标准回答(3-4 分钟)
虚拟滚动是这个项目最有挑战的技术点,我花了两周时间攻克。
为什么需要虚拟滚动?
Table 组件要支持万级数据展示,如果直接渲染:
- 1 万行数据,每行 20 个 DOM 节点 = 20 万个 DOM
- 首次渲染耗时 3-5 秒,滚动严重卡顿
- 内存占用 500MB+
虚拟滚动的核心思想:只渲染可视区域的数据。
基础实现
- 计算可视区域
const visibleCount = Math.ceil(containerHeight / itemHeight) // 可见行数
const startIndex = Math.floor(scrollTop / itemHeight) // 开始索引
const endIndex = startIndex + visibleCount // 结束索引
- 渲染偏移
const offsetY = startIndex * itemHeight // 偏移量
const visibleData = data.slice(startIndex, endIndex)
- 撑开容器
<div :style="{ height: totalHeight + 'px' }">
<div :style="{ transform: `translateY(${offsetY}px)` }">
<!-- 只渲染可见数据 -->
</div>
</div>
核心难点:动态高度支持
业务场景中,Table 的行高是动态的(比如文本换行),传统方案失效。
我的解决方案
- 高度缓存
const itemHeights = ref([]) // 缓存每行真实高度
const positions = ref([]) // 缓存每行的位置信息
// 计算位置信息
function updatePositions() {
let top = 0
positions.value = itemHeights.value.map((height, index) => {
const position = { index, top, bottom: top + height, height }
top += height
return position
})
}
- 首次渲染测量
onMounted(() => {
// 首次渲染所有数据,测量高度
itemHeights.value = Array.from(listRef.value.children).map(
(el) => el.getBoundingClientRect().height
)
updatePositions()
})
- 二分查找可视区域
function getStartIndex(scrollTop) {
let left = 0
let right = positions.value.length - 1
while (left < right) {
const mid = Math.floor((left + right) / 2)
if (positions.value[mid].bottom < scrollTop) {
left = mid + 1
} else {
right = mid
}
}
return left
}
- ResizeObserver 监听变化
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const index = entry.target.dataset.index
const newHeight = entry.contentRect.height
if (itemHeights.value[index] !== newHeight) {
itemHeights.value[index] = newHeight
updatePositions()
}
})
})
性能优化
- 缓冲区:上下各多渲染 5 行,避免滚动时白屏
- 节流:scroll 事件用 requestAnimationFrame 节流
- 预估高度:第一次渲染用平均高度预估,避免抖动
实测效果
- 支持 10 万条数据流畅滚动
- 首屏渲染时间从 3s 降到 80ms
- 内存占用从 500MB 降到 50MB
- 滚动帧率稳定在 60fps
封装 useVirtualList Hook
把虚拟滚动逻辑封装成 hook,其他组件也能复用:
const {
containerProps, // 容器属性
wrapperProps, // 包裹层属性
list, // 可见数据列表
} = useVirtualList(data, {
itemHeight: 50,
dynamic: true, // 支持动态高度
})
问题 6:如何保证组件库的质量?
标准回答(2-3 分钟)
质量保障是组件库的生命线,我从四个维度建设:
1. 代码规范
- ESLint + Prettier 统一代码风格
- Husky + lint-staged 提交前自动检查
- Commitlint 规范提交信息,遵循 Conventional Commits
2. 测试体系
- 单元测试: 用 Vitest 测试组件逻辑,覆盖率 85%+
- 快照测试: 确保组件渲染结果不会意外变化
- E2E 测试: Playwright 测试关键交互流程
示例:
describe('Button', () => {
it('should emit click event', async () => {
const wrapper = mount(Button)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeTruthy()
})
it('should be disabled', () => {
const wrapper = mount(Button, { props: { disabled: true } })
expect(wrapper.classes()).toContain('v-button--disabled')
})
})
3. 文档体系
- Storybook 提供交互式文档
- 每个组件至少 5 个使用示例
- API 文档自动从注释生成
- 提供最佳实践和注意事项
4. CI/CD 流程
- GitHub Actions 自动运行测试
- Pull Request 必须通过 lint 和测试才能合并
- 自动生成测试覆盖率报告
- changeset 自动生成 CHANGELOG
5. Code Review 机制
- 所有代码必须经过 2 人 Review
- 重点关注 API 设计、性能、可访问性
- 建立组件 Checklist: 是否支持 v-model、是否支持插槽、是否有完整文档
效果
- 组件 Bug 率降低 30%
- 线上故障减少 50%
- 新增组件从开发到发布周期从 1 周缩短到 3 天