
这次我们来看一个面向高级开发者的实战项目从零开始构建一套基于 Vue 3.4 的 UI 组件库。这不是一个简单的组件使用教程而是深入到组件库的设计、开发、构建、测试和发布的完整工程化实践。对于希望在前端架构和工程能力上有所突破的开发者来说掌握这套流程至关重要。本文将带你完整走一遍构建私有 UI 组件库的核心链路。重点不是概念而是可落地的工程实践如何搭建项目结构、如何设计组件 API、如何利用 Vue 3.4 的新特性、如何配置高效的构建工具链、如何实现按需加载与主题定制、以及如何打包发布到私有或公共仓库。整个过程会重点关注技术选型的合理性、开发体验的流畅性以及产出的可用性。如果你关心如何将散落的业务组件沉淀为可复用的资产如何为团队建立统一的设计与开发规范或者如何深入理解现代前端构建工具链那么这篇文章可以直接收藏。我们将从最基础的 Monorepo 项目初始化开始一步步实现组件开发、文档编写、打包构建和版本发布。1. 核心能力速览在深入代码之前我们先通过下表快速了解构建一个 Vue 3.4 UI 组件库所涉及的核心环节和关键技术栈这有助于你把握全局。能力项说明与关键技术选型项目类型企业级、可复用的 Vue 3.4 UI 组件库核心框架Vue 3.4 TypeScript Composition API构建工具链Vite 5.x (用于开发调试与库模式构建) Rollup (可选用于精细控制)项目组织Monorepo (使用 pnpm workspace)分离 packages组件库、文档、工具函数等样式方案CSS-in-JS (如unocss或vueuse/core的useCssVar)、CSS 预处理器 (Sass/Less) 或 Utility-First CSS 框架 (Tailwind CSS)组件开发单文件组件 (SFC) 与setup语法糖充分利用defineProps、defineEmits、defineExpose类型支持完整的 TypeScript 类型定义 (.d.ts 文件)支持 Volar 的完美智能提示按需加载通过unplugin-vue-components等插件实现自动导入或提供 ES Module 构建产物文档与演示Vitepress 或 Storybook用于展示组件 API、示例和交互式演示测试策略单元测试 (Vitest Vue Test Utils) 组件测试 (Cypress Component Testing 或 Vitest)打包输出支持多种格式ES Module (es)、CommonJS (lib)、UMD (umd)以及全局样式文件发布管理版本管理 (standard-version 或 changesets)、私有 npm 仓库或 npmjs 公共发布2. 适用场景与使用边界适合谁中大型前端团队需要统一设计语言和代码规范提升开发效率与一致性。拥有多条产品线的业务希望将通用业务组件抽象、沉淀避免重复造轮子。进阶的 Vue 开发者希望深入理解 Vue 3 的 Composition API、渲染函数、自定义指令等高级特性并学习库开发的完整流程。技术架构师或负责人需要为团队搭建基础技术设施制定组件开发标准。能解决什么问题统一性与一致性确保公司内所有产品拥有相同的视觉和交互体验。开发提效开发者无需从零开始编写基础组件专注于业务逻辑。易于维护组件逻辑集中管理一处修改处处更新。质量保障集中的单元测试和类型检查提升代码质量。知识沉淀将最佳实践封装成组件形成团队的技术资产。不适合什么场景小型或个人项目如果项目规模很小直接使用现成的开源 UI 库如 Element Plus、Ant Design Vue是更高效的选择。对打包体积极其敏感虽然可以按需加载但自研组件库仍会引入额外的维护成本和基础体积。超轻量级场景可能更适合直接编写样式或使用轻量工具。缺乏持续维护资源组件库需要随着业务和技术栈迭代而更新如果没有专人维护很容易过时并成为技术负债。合规与安全边界设计版权组件库的视觉设计应避免直接抄袭知名开源或商业产品以免引发版权纠纷。代码引用如果借鉴了开源项目的实现思路需遵守其开源协议如 MIT、Apache-2.0并在必要时注明。内部使用若仅内部使用需确保构建和发布流程与公司内部 DevOps 平台如私有 GitLab、私有 npm 仓库集成顺畅。3. 环境准备与前置条件开始前请确保你的开发环境满足以下要求。这是后续所有步骤能顺利进行的基础。Node.js 环境推荐使用最新的 LTS 版本如 18.x 或 20.x。你可以使用nvm或fnm来管理多个 Node 版本。# 检查 Node.js 和 npm/pnpm 版本 node --version pnpm --version # 推荐使用 pnpm包管理器强烈推荐使用pnpm它对 Monorepo 的支持和磁盘空间利用效率远超 npm 和 yarn。# 安装 pnpm (如果未安装) npm install -g pnpm代码编辑器推荐使用 Visual Studio Code并安装以下插件以获得最佳开发体验VolarVue 3 官方推荐的语言支持插件。TypeScript Vue Plugin (Volar)提供更好的 TypeScript 支持。ESLint与Prettier代码规范和格式化。Git用于版本控制。确保已安装并配置好用户信息。磁盘空间建议预留至少 2GB 的可用空间用于安装依赖和生成构建产物。4. 项目初始化与 Monorepo 结构搭建我们将采用 Monorepo 结构来组织代码这有利于管理多个相互关联的包如组件库、文档站点、共享工具。4.1 创建项目根目录mkdir vue3-ui-library cd vue3-ui-library4.2 初始化根目录package.jsonpnpm init初始化后编辑根目录的package.json设置private: true并定义 workspaces。{ name: vue3-ui-library, private: true, version: 1.0.0, description: A Vue 3.4 UI component library monorepo, scripts: { dev: pnpm -C packages/docs dev, build: pnpm -C packages/docs build, build:lib: pnpm -C packages/ui-lib build, test: pnpm -C packages/ui-lib test }, keywords: [], author: , license: MIT, devDependencies: { types/node: ^20.0.0 }, engines: { node: 18.0.0, pnpm: 8.0.0 }, packageManager: pnpm8.0.0, workspaces: [ packages/* ] }4.3 创建 packages 目录及子包mkdir -p packages/ui-lib packages/docs4.4 初始化组件库包 (packages/ui-lib)进入packages/ui-lib目录使用 Vite 的library模式模板快速初始化。cd packages/ui-lib pnpm create vite . --template vue-ts创建完成后调整package.json这是组件库对外的“名片”。{ name: vue3-ui/lib, version: 0.0.1, description: A Vue 3.4 UI Component Library, type: module, main: ./dist/vue3-ui-lib.umd.cjs, module: ./dist/vue3-ui-lib.es.js, types: ./dist/index.d.ts, exports: { .: { import: ./dist/vue3-ui-lib.es.js, require: ./dist/vue3-ui-lib.umd.cjs, types: ./dist/index.d.ts }, ./dist/style.css: ./dist/style.css }, files: [dist], scripts: { dev: vite, build: vue-tsc vite build, preview: vite preview, test: vitest }, peerDependencies: { vue: ^3.4.0 }, devDependencies: { vitejs/plugin-vue: ^5.0.0, vue/test-utils: ^2.4.0, vue/tsconfig: ^0.4.0, jsdom: ^23.0.0, typescript: ~5.3.0, vite: ^5.0.0, vitest: ^1.0.0, vue-tsc: ^1.8.0 } }关键点name: 使用scope/package-name格式便于发布到私有或公共仓库。type: 设置为module使用 ES 模块。exports: 定义包的入口点支持 ESM 和 CJS并导出类型定义和样式文件。files: 指定发布到 npm 时包含的文件通常只有dist目录。peerDependencies: 声明对vue的依赖使用该库的项目必须自行安装 Vue避免版本冲突。4.5 初始化文档站点包 (packages/docs)进入packages/docs目录我们使用 VitePress 来构建文档。cd packages/docs pnpm add -D vitepress vue vue3-ui/libworkspace:*初始化 VitePress 配置。npx vitepress init在docs/index.md中我们可以开始编写组件库的介绍和快速开始指南。5. 开发第一个组件Button我们将从最基础的 Button 组件开始实践组件设计、开发、测试和导出的完整流程。5.1 组件设计与 API 定义在packages/ui-lib/src/components/Button目录下创建文件。packages/ui-lib/src/components/Button/ ├── Button.vue # 组件模板与逻辑 ├── index.ts # 组件导出文件 └── __tests__/ └── Button.spec.ts # 组件测试首先设计组件的 Props。在Button.vue中使用 Vue 3.4 的defineProps和defineEmits。!-- packages/ui-lib/src/components/Button/Button.vue -- template button :class[ v-button, v-button--${type}, v-button--${size}, { is-plain: plain, is-round: round, is-circle: circle, is-disabled: disabled, is-loading: loading } ] :disableddisabled || loading clickhandleClick span v-ifloading classv-button__loading/span slot v-if$slots.icon nameicon / span classv-button__content slot / /span /button /template script setup langts import { computed } from vue // 定义 Props interface Props { type?: primary | success | warning | danger | info | default size?: large | default | small plain?: boolean round?: boolean circle?: boolean disabled?: boolean loading?: boolean } const props withDefaults(definePropsProps(), { type: default, size: default, plain: false, round: false, circle: false, disabled: false, loading: false }) // 定义 Emits const emit defineEmits{ click: [e: MouseEvent] }() const handleClick (e: MouseEvent) { if (!props.disabled !props.loading) { emit(click, e) } } /script style scoped .v-button { /* 基础样式 */ display: inline-flex; align-items: center; justify-content: center; /* ... 更多样式 */ } /* ... 其他样式规则 */ /style5.2 组件导出与全局注册在index.ts中导出组件便于批量管理和按需导入。// packages/ui-lib/src/components/Button/index.ts import Button from ./Button.vue import type { App } from vue Button.install (app: App) { app.component(Button.name || VButton, Button) } export default Button export { Button }在组件库的入口文件src/index.ts中集中导出所有组件。// packages/ui-lib/src/index.ts export { default as Button } from ./components/Button // 未来可以继续导出其他组件 // export { default as Input } from ./components/Input import type { App } from vue import * as components from ./components const install (app: App) { Object.values(components).forEach(component { if (component.install) { app.use(component) } }) } export default { install, ...components }5.3 编写组件单元测试使用 Vitest 和 Vue Test Utils 为 Button 组件编写测试。// packages/ui-lib/src/components/Button/__tests__/Button.spec.ts import { describe, it, expect } from vitest import { mount } from vue/test-utils import Button from ../Button.vue describe(Button.vue, () { it(renders default button, () { const wrapper mount(Button, { slots: { default: Click me } }) expect(wrapper.text()).toBe(Click me) expect(wrapper.classes()).toContain(v-button) expect(wrapper.classes()).toContain(v-button--default) }) it(emits click event when clicked, async () { const wrapper mount(Button) await wrapper.trigger(click) expect(wrapper.emitted()).toHaveProperty(click) }) it(does not emit click when disabled, async () { const wrapper mount(Button, { props: { disabled: true } }) await wrapper.trigger(click) expect(wrapper.emitted().click).toBeUndefined() }) it(shows loading state, () { const wrapper mount(Button, { props: { loading: true } }) expect(wrapper.find(.v-button__loading).exists()).toBe(true) expect(wrapper.classes()).toContain(is-loading) }) })运行测试以确保组件行为符合预期。cd packages/ui-lib pnpm test6. 配置 Vite 构建与打包组件开发完成后需要将其打包成可供其他项目使用的库。我们使用 Vite 的library模式。6.1 配置vite.config.ts// packages/ui-lib/vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue import { resolve } from path import dts from vite-plugin-dts // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), dts({ tsconfigPath: ./tsconfig.json, outDir: dist, insertTypesEntry: true, }), ], build: { lib: { // 入口文件 entry: resolve(__dirname, src/index.ts), // 库名称 name: Vue3UILib, // 输出文件名 fileName: (format) vue3-ui-lib.${format}.js }, rollupOptions: { // 确保外部化处理那些你不想打包进库的依赖 external: [vue], output: { // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量 globals: { vue: Vue } } } }, resolve: { alias: { : resolve(__dirname, src) } } })关键配置解析build.lib: 指定库模式的入口和输出配置。rollupOptions.external: 将vue标记为外部依赖不打包进库由使用方提供。vite-plugin-dts: 自动生成 TypeScript 类型声明文件 (.d.ts)这对使用者获得类型提示至关重要。6.2 配置tsconfig.json确保 TypeScript 配置支持库开发。{ extends: vue/tsconfig/tsconfig.dom.json, include: [src/**/*.ts, src/**/*.d.ts, src/**/*.tsx, src/**/*.vue], exclude: [src/**/__tests__/*], compilerOptions: { composite: true, baseUrl: ., paths: { /*: [./src/*] }, outDir: dist, declaration: true, declarationDir: dist } }6.3 执行构建运行构建命令生成最终产物。cd packages/ui-lib pnpm build构建成功后dist目录下应包含以下文件vue3-ui-lib.es.js(ES Module用于现代打包工具)vue3-ui-lib.umd.cjs(UMD可用于script标签直接引入)index.d.ts(类型声明文件)style.css(提取出的所有组件样式)7. 在文档站点中集成与演示组件库需要配套的文档。我们在packages/docs中使用 VitePress 来展示组件。7.1 配置 VitePress 以解析本地包修改docs/.vitepress/config.ts添加对本地ui-lib包的解析。// packages/docs/.vitepress/config.ts import { defineConfig } from vitepress import { resolve } from path export default defineConfig({ title: Vue3 UI Library, description: A Vue 3.4 UI Component Library, themeConfig: { // 主题配置... }, vite: { resolve: { alias: { vue3-ui/lib: resolve(__dirname, ../../ui-lib/src) } } } })7.2 创建 Button 组件文档页在docs/components目录下创建button.md。# Button 按钮 常用的操作按钮。 ## 基础用法 使用 type、size、disabled 等属性来定义按钮的样式。 demo template div styledisplay: flex; gap: 12px; margin-bottom: 20px; VButton默认按钮/VButton VButton typeprimary主要按钮/VButton VButton typesuccess成功按钮/VButton VButton typewarning警告按钮/VButton VButton typedanger危险按钮/VButton /div /template script setup import { VButton } from vue3-ui/lib /script /demo ## API ### Props | 属性名 | 说明 | 类型 | 可选值 | 默认值 | |--------|------|------|--------|--------| | type | 类型 | string | primary / success / warning / danger / info / default | default | | size | 尺寸 | string | large / default / small | default | | plain | 是否朴素按钮 | boolean | — | false | | round | 是否圆角按钮 | boolean | — | false | | circle | 是否圆形按钮 | boolean | — | false | | disabled | 是否禁用 | boolean | — | false | | loading | 是否加载中 | boolean | — | false | ### Events | 事件名 | 说明 | 回调参数 | |--------|------|----------| | click | 点击按钮时触发 | (event: MouseEvent) |7.3 启动文档站点在根目录下运行pnpm dev访问http://localhost:5173即可看到包含交互式演示的文档。8. 实现按需加载与自动导入对于使用者来说全量导入组件库会增加打包体积。我们需要提供按需加载的能力。8.1 提供 ES Module 构建产物我们的 Vite 配置已经生成了vue3-ui-lib.es.js使用者可以通过以下方式按需导入import { Button } from vue3-ui/lib import vue3-ui/lib/dist/style.css8.2 配置自动导入 (推荐)我们可以提供一个 Vite/Webpack 插件或者指导用户使用unplugin-vue-components实现自动导入。 首先在组件库根目录创建一个components.d.ts文件声明组件类型。// packages/ui-lib/components.d.ts import * as components from ./src/index declare module vue { export interface GlobalComponents { VButton: typeof components.Button // 未来添加其他组件... } } export {}然后指导使用者在他们的vite.config.ts中配置// 使用方项目的 vite.config.ts import Components from unplugin-vue-components/vite import { Vue3UILibResolver } from ./resolver // 需要自定义一个解析器 export default defineConfig({ plugins: [ Components({ resolvers: [ Vue3UILibResolver() ] }) ] })自定义解析器 (resolver.ts) 需要根据组件命名规则将VButton解析到vue3-ui/lib的具体路径。这需要根据你的组件命名规范来实现。9. 样式方案与主题定制一个优秀的组件库必须提供灵活的样式定制能力。9.1 使用 CSS 变量在组件样式中尽量使用 CSS 自定义属性变量便于主题覆盖。/* packages/ui-lib/src/styles/vars.css */ :root { --v-color-primary: #409eff; --v-color-success: #67c23a; --v-color-warning: #e6a23c; --v-color-danger: #f56c6c; --v-color-info: #909399; --v-button-border-radius: 4px; /* ... 更多变量 */ }在组件中引用style scoped .v-button--primary { background-color: var(--v-color-primary); border-color: var(--v-color-primary); } /style9.2 提供主题定制入口在库的入口或构建脚本中提供覆盖默认变量的方式。// 使用方项目的主入口文件 import { createApp } from vue import App from ./App.vue import Vue3UILib from vue3-ui/lib import vue3-ui/lib/dist/style.css // 在引入组件库样式前覆盖 CSS 变量 const style document.createElement(style) style.textContent :root { --v-color-primary: #f06292; /* 覆盖为主题粉色 */ } document.head.appendChild(style) createApp(App).use(Vue3UILib).mount(#app)9.3 支持 Sass/Less (可选)如果你的团队习惯使用预处理器可以在组件库中提供 Sass 或 Less 的源文件并在构建时一同输出。10. 版本管理与发布流程组件库需要一套规范的版本管理和发布流程。10.1 版本号管理遵循语义化版本控制 (SemVer)主版本号 (Major)不兼容的 API 修改。次版本号 (Minor)向下兼容的功能性新增。修订号 (Patch)向下兼容的问题修正。可以使用standard-version或changesets自动化版本管理和 CHANGELOG 生成。# 安装 standard-version pnpm add -D standard-version # 在根目录 package.json 中添加脚本 { scripts: { release: standard-version git push --follow-tags origin main } }10.2 发布到 npm首先确保你已登录 npm。npm login然后在packages/ui-lib目录下执行发布。cd packages/ui-lib pnpm publish --access public # 如果是公共包 # 或发布到私有仓库 pnpm publish --registry https://your-private-registry.npmjs.org/发布前确保package.json中的version字段已更新并且dist目录已构建完成。10.3 使用 GitHub Actions 自动化 (可选)可以配置 CI/CD 流水线在推送标签时自动构建、测试和发布。# .github/workflows/release.yml name: Release on: push: tags: - v* jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - uses: pnpm/action-setupv2 - uses: actions/setup-nodev3 with: node-version: 18 registry-url: https://registry.npmjs.org/ - run: pnpm install - run: pnpm -C packages/ui-lib build - run: pnpm -C packages/ui-lib publish --no-git-checks env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}11. 常见问题与排查方法在开发和使用的过程中你可能会遇到以下问题。问题现象可能原因排查方式解决方案Vite 构建时报vue未找到vue未正确配置为外部依赖 (external)检查vite.config.ts中的rollupOptions.external确保external数组中包含vueTypeScript 提示找不到模块vue3-ui/lib类型声明文件未生成或路径不对检查dist目录下是否有index.d.ts检查package.json的types字段确保vite-plugin-dts插件配置正确tsconfig.json中declaration为true组件样式未生效样式文件未导入或 CSS 变量作用域问题检查是否在项目中导入了dist/style.css检查浏览器开发者工具中 CSS 变量是否被覆盖确保在入口文件导入样式检查自定义变量的优先级按需加载/自动导入不工作unplugin-vue-components解析器配置错误检查自定义解析器的实现确认组件名称映射正确参考unplugin-vue-components文档编写正确的resolver函数文档站点中组件无法渲染VitePress 的vite.resolve.alias配置错误检查docs/.vitepress/config.ts中的alias路径是否正确指向ui-lib源码使用绝对路径resolve(__dirname, ../../ui-lib/src)peerDependencies警告使用方项目安装的 Vue 版本与组件库声明的版本范围不匹配查看安装时的警告信息在使用方项目中安装符合peerDependencies要求的 Vue 版本发布到 npm 时提示无权限未登录 npm或包名已被占用运行npm whoami检查登录状态检查包名是否唯一使用npm login登录更换package.json中的name如添加 scope12. 最佳实践与使用建议原子化设计从最小的、不可分割的组件如 Button、Input开始构建再组合成复杂组件如 Form、Table。单向数据流Props 向下Events 向上。避免在子组件中直接修改父组件传递的 Prop。充分的测试覆盖为每个组件编写单元测试特别是核心交互逻辑和边界条件。完善的文档文档应包含 API 说明、代码示例、可交互的 Demo 以及设计指南。版本兼容性在发布 Major 版本前仔细评估 API 变更的影响并为使用者提供迁移指南。性能考量注意组件渲染性能对于复杂组件考虑使用v-memo或shallowRef进行优化。可访问性 (A11y)为组件添加合理的 ARIA 属性确保键盘导航和屏幕阅读器支持。国际化 (i18n)如果面向全球用户提前设计好多语言支持的结构。构建一个成熟的 UI 组件库是一个持续迭代的过程。从第一个 Button 组件开始逐步丰富组件类型完善构建工具链建立设计规范并形成团队的开发共识。这套从零开始的实践不仅产出可复用的代码资产更能系统性地提升团队在前端工程化、架构设计和代码质量方面的能力。建议将本文作为路线图在实际项目中分阶段实施和验证。