
这次我们来看一个面向 Vue 3.4 的 UI 组件库开发实战项目。如果你已经熟悉 Vue 3 的基础使用想深入理解现代前端工程化、组件设计模式并最终拥有一套可复用、可维护的自有组件库那么这篇文章就是为你准备的。本文不会空谈概念而是聚焦于从零搭建一个组件库的核心流程、关键技术选型、以及如何让这套库真正可用、可测、可发布。我们将重点关注几个核心问题在 Vue 3.4 的 Composition API 和script setup语法下如何设计组件如何构建支持按需引入和全量引入的库如何搭建高效的开发、调试、测试、文档和构建流水线最终产出的不仅是一堆.vue文件而是一个具备完整生命周期、能接入现代前端工具链的标准化项目。本文会带你走通从项目初始化、组件开发、样式方案、打包构建、单元测试、文档生成到发布 npm 的全过程。无论你是想为团队打造基础组件库还是作为个人技术沉淀这套方法论都能提供直接的参考。1. 核心能力速览在深入细节之前我们先通过一个表格快速了解构建一个现代 Vue 3.4 UI 组件库所涉及的核心能力和产出物。能力项说明与目标产出技术栈Vue 3.4 TypeScript Vite使用script setup语法组件设计基于 Composition API 的函数式逻辑复用支持 Props、Emits、Slots、Expose 等完整组件接口样式方案支持 CSS 预处理器如 Sass/Less采用 CSS-in-JS如 UnoCSS或 CSS Modules 等隔离方案主题定制能力开发体验基于 Vite 的极速 HMR支持单组件独立开发与调试构建打包使用vite或rollup打包产出 ES Module、CommonJS、UMD 格式支持 Tree Shaking 和按需引入类型支持完整的 TypeScript 类型定义.d.ts 文件提供良好的 IDE 智能提示单元测试使用 Vitest Vue Test Utils 进行组件单元测试保证代码质量文档系统使用 VitePress 或 Storybook 搭建交互式文档站支持 Props 表格、事件说明和实时演示代码质量集成 ESLint、Prettier、Commitlint 进行代码规范和提交信息管理发布流程支持一键发布到 npm 私有或公有仓库版本号遵循语义化版本控制2. 适用场景与使用边界适合谁前端团队负责人或核心开发者需要为团队建立统一、高效、可维护的前端 UI 基础提升业务开发效率。中级向高级进阶的 Vue 开发者希望深入理解 Vue 3 组件化、工程化、构建工具链的完整实践。个人项目或开源项目维护者需要一套设计良好、便于扩展的 UI 基础库来支撑项目发展。能解决什么问题UI 一致性统一团队内的按钮、输入框、弹窗等基础组件的交互与视觉风格。开发效率封装复杂交互逻辑如表单验证、虚拟列表、懒加载业务开发只需关注数据与配置。维护成本一处修改处处更新。修复 Bug 或升级样式只需改动组件库。技术沉淀将最佳实践固化为可复用的组件形成团队的技术资产。不适合什么场景超简单、无复用的单页应用如果项目只有一两个页面引入组件库可能增加不必要的复杂度。对 bundle 大小极度敏感的场景虽然支持按需引入但引入一整套设计规范和组件体系仍会增加体积需权衡。需要完全定制、与现有设计系统无法兼容的场景如果设计风格与组件库预设差异巨大改造成本可能高于从零开发。版权与合规边界设计资源组件库中使用的图标、字体等资源务必确认其开源协议或购买商用授权。代码借鉴参考开源组件库如 Element Plus、Ant Design Vue的实现思路时应理解其设计原理并独立实现避免直接复制代码导致版权风险。命名规范避免使用与知名 UI 库如el-、a-、van-相同或极易混淆的组件前缀以减少使用者的困惑和法律风险。3. 环境准备与前置条件开始前请确保你的开发环境满足以下要求。这是项目能顺利跑起来的基础。Node.js推荐使用最新的 LTS 版本如 18.x 或 20.x。你可以通过node -v检查。包管理器npm、yarn 或 pnpm 均可。本文示例使用pnpm因其速度快、磁盘空间利用高效非常适合 Monorepo 组件库项目。可通过pnpm -v检查。代码编辑器推荐 VS Code并安装以下插件以获得最佳开发体验Volar(Vue 语言支持)TypeScript Vue Plugin (Volar)ESLintPrettier浏览器现代浏览器即可用于调试和文档预览。Git用于版本管理。4. 项目初始化与工程结构我们从一个干净的空目录开始搭建一个标准的 Monorepo 结构。这种结构将库的源码、文档、示例工程等分离清晰且易于管理。# 创建项目根目录并进入 mkdir my-ui-library cd my-ui-library # 初始化项目生成 package.json pnpm init # 在根目录创建 pnpm-workspace.yaml定义工作空间 echo packages: - packages/* - docs - play pnpm-workspace.yaml接下来创建主要的子包目录# 创建组件库源码目录 mkdir -p packages/components # 创建工具函数目录可选用于共享工具 mkdir -p packages/utils # 创建文档站点目录 mkdir -p docs # 创建开发调试用的示例工程目录 mkdir -p play初始化组件库主包 (packages/components)cd packages/components pnpm init # 将 package.json 中的 name 改为你的库名例如 my-ui/components # 修改 version 为 0.0.1一个典型的组件库工程结构如下my-ui-library/ ├── packages/ │ ├── components/ # 组件库源码 │ │ ├── src/ │ │ │ ├── button/ # Button 组件 │ │ │ │ ├── Button.vue │ │ │ │ ├── index.ts # 导出组件 │ │ │ │ └── style/ # 组件样式 │ │ │ ├── input/ # Input 组件 │ │ │ └── index.ts # 全量导出入口 │ │ ├── package.json │ │ └── vite.config.ts # 组件库构建配置 │ └── utils/ # 共享工具库 ├── docs/ # 文档站点 (基于 VitePress) │ ├── .vitepress/ │ ├── index.md │ └── components/ # 组件文档 ├── play/ # 开发调试项目 (一个独立的 Vite Vue 项目) │ ├── src/ │ ├── index.html │ └── vite.config.ts ├── package.json # 根 package.json管理脚本和依赖 ├── pnpm-workspace.yaml # pnpm 工作空间配置 └── README.md5. 配置开发环境与构建工具我们选择 Vite 作为开发和构建工具因为它速度快、配置简单且与 Vue 3 生态完美契合。在根目录安装共享依赖# 在项目根目录执行 pnpm add -Dw vue^3.4 typescript vitejs/plugin-vue vue-tsc # -Dw 表示安装到根目录的 devDependencies供所有子包使用在packages/components目录下创建vite.config.ts// packages/components/vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue import { resolve } from path import dts from vite-plugin-dts // 用于生成 .d.ts 类型文件 // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), dts({ tsConfigFilePath: ../../tsconfig.json, // 指向根目录的 tsconfig outDir: dist/types, // 类型文件输出目录 include: [src/**/*.vue, src/**/*.ts], }), ], build: { lib: { // 库的入口文件 entry: resolve(__dirname, src/index.ts), name: MyUI, fileName: (format) my-ui.${format}.js, }, rollupOptions: { // 确保外部化处理那些你不想打包进库的依赖 external: [vue], output: { // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量 globals: { vue: Vue, }, }, }, outDir: dist, }, })创建 TypeScript 配置文件tsconfig.json于项目根目录{ compilerOptions: { target: ES2020, useDefineForClassFields: true, module: ESNext, lib: [ES2020, DOM, DOM.Iterable], skipLibCheck: true, moduleResolution: bundler, allowImportingTsExtensions: true, resolveJsonModule: true, isolatedModules: true, noEmit: true, jsx: preserve, strict: true, noUnusedLocals: true, noUnusedParameters: true, noFallthroughCasesInSwitch: true, declaration: true, declarationDir: ./dist/types, baseUrl: ., paths: { my-ui/components: [packages/components/src], my-ui/components/*: [packages/components/src/*] } }, include: [packages/**/*.ts, packages/**/*.d.ts, packages/**/*.tsx, packages/**/*.vue], exclude: [node_modules, dist] }6. 开发第一个组件Button让我们从最基础的 Button 组件开始实践完整的组件开发流程。6.1 组件实现创建文件packages/components/src/button/Button.vuetemplate button classmy-button :class[ my-button--${type}, my-button--${size}, { is-plain: plain, is-round: round, is-circle: circle, is-disabled: disabled || loading, is-loading: loading, } ] :disableddisabled || loading clickhandleClick span v-ifloading classmy-button__loading !-- 这里可以放一个加载图标组件暂时用文字代替 -- span加载中.../span /span span v-else classmy-button__content slot / /span /button /template script setup langts import { computed } from vue // 定义 Props interface ButtonProps { type?: primary | success | warning | danger | info | default size?: large | default | small plain?: boolean round?: boolean circle?: boolean disabled?: boolean loading?: boolean } const props withDefaults(definePropsButtonProps(), { type: default, size: default, plain: false, round: false, circle: false, disabled: false, loading: false, }) // 定义 Emits const emit defineEmits{ click: [event: MouseEvent] }() const handleClick (event: MouseEvent) { if (!props.disabled !props.loading) { emit(click, event) } } // 如果需要暴露方法或属性给父组件可以使用 defineExpose // defineExpose({ someMethod }) /script style scoped langscss .my-button { display: inline-flex; align-items: center; justify-content: center; line-height: 1; white-space: nowrap; cursor: pointer; border: 1px solid #dcdfe6; border-color: #dcdfe6; color: #606266; text-align: center; box-sizing: border-box; outline: none; margin: 0; transition: .1s; font-weight: 500; user-select: none; padding: 12px 20px; font-size: 14px; border-radius: 4px; background-color: #fff; --primary { color: #fff; background-color: #409eff; border-color: #409eff; } --success { color: #fff; background-color: #67c23a; border-color: #67c23a; } --warning { color: #fff; background-color: #e6a23c; border-color: #e6a23c; } --danger { color: #fff; background-color: #f56c6c; border-color: #f56c6c; } --info { color: #fff; background-color: #909399; border-color: #909399; } --large { padding: 14px 24px; font-size: 16px; border-radius: 6px; } --small { padding: 8px 16px; font-size: 12px; border-radius: 3px; } .is-plain { .my-button--primary { color: #409eff; background-color: #ecf5ff; border-color: #b3d8ff; } // ... 其他类型的朴素样式 } .is-round { border-radius: 20px; } .is-circle { border-radius: 50%; padding: 12px; } .is-disabled, .is-disabled:focus, .is-disabled:hover { color: #c0c4cc; cursor: not-allowed; background-image: none; background-color: #fff; border-color: #ebeef5; } .is-loading { position: relative; pointer-events: none; opacity: 0.7; } __loading { display: inline-flex; align-items: center; justify-content: center; } __content { display: inline-flex; align-items: center; justify-content: center; } } /style6.2 组件导出创建packages/components/src/button/index.tsimport Button from ./Button.vue import type { App } from vue // 为组件提供 install 方法用于 Vue.use() 全局注册 Button.install (app: App) { app.component(Button.name || MyButton, Button) } // 默认导出组件 export default Button // 导出组件的类型定义 export * from ./Button.vue6.3 库的入口文件创建packages/components/src/index.ts用于全量导出所有组件// 全量导出 import MyButton from ./button // 组件列表 const components [MyButton] // 全局注册的 install 方法 const install (app: any) { components.forEach(component { app.component(component.name, component) }) } // 支持按需引入 export { MyButton } // 默认导出用于全局注册 export default { install, version: 0.0.1 }7. 搭建开发调试环境 (Playground)为了在开发过程中实时预览和调试组件我们需要一个独立的 Vue 项目。在play目录下初始化一个 Vite 项目。cd play pnpm create vite . --template vue-ts安装依赖并链接本地组件库# 在 play 目录下 pnpm install # 将本地组件库添加为依赖使用 workspace 协议 pnpm add my-ui/componentsworkspace:*修改play/src/App.vue用于测试我们的 Button 组件template div idapp h1My UI Library Playground/h1 div classdemo-block h3Button 类型/h3 MyButton默认按钮/MyButton MyButton typeprimary主要按钮/MyButton MyButton typesuccess成功按钮/MyButton MyButton typewarning警告按钮/MyButton MyButton typedanger危险按钮/MyButton MyButton typeinfo信息按钮/MyButton /div div classdemo-block h3Button 状态/h3 MyButton plain朴素按钮/MyButton MyButton round圆角按钮/MyButton MyButton circle圆/MyButton MyButton disabled禁用按钮/MyButton MyButton loading加载中/MyButton /div div classdemo-block h3Button 尺寸/h3 MyButton sizelarge大号按钮/MyButton MyButton sizedefault默认按钮/MyButton MyButton sizesmall小号按钮/MyButton /div /div /template script setup langts // 按需引入 import { MyButton } from my-ui/components /script style #app { font-family: Avenir, Helvetica, Arial, sans-serif; padding: 20px; } .demo-block { margin-bottom: 30px; } .demo-block h3 { margin-bottom: 15px; } .my-button { margin-right: 10px; margin-bottom: 10px; } /style修改play/src/main.ts全局注册组件库可选演示用import { createApp } from vue import App from ./App.vue // 全量引入 import MyUI from my-ui/components // 按需引入时注释掉上面一行使用下面的方式 // import { MyButton } from my-ui/components const app createApp(App) // 全局注册 app.use(MyUI) // 按需注册 // app.component(MyButton, MyButton) app.mount(#app)现在在项目根目录下你可以通过以下命令同时启动组件库的构建监听和 Playground 的开发服务器// 在根目录的 package.json 中添加 scripts { scripts: { dev:components: cd packages/components pnpm run dev, dev:play: cd play pnpm run dev, dev: pnpm run dev:components pnpm run dev:play } }运行pnpm run dev访问 Playground 的本地地址通常是http://localhost:5173就能实时看到并调试你的 Button 组件了。修改Button.vue的代码页面会热更新。8. 构建与打包当组件开发完成我们需要将其打包成可供发布的格式。在packages/components/package.json中配置构建脚本{ name: my-ui/components, version: 0.0.1, type: module, main: ./dist/my-ui.umd.js, module: ./dist/my-ui.es.js, types: ./dist/types/index.d.ts, exports: { .: { import: ./dist/my-ui.es.js, require: ./dist/my-ui.umd.js, types: ./dist/types/index.d.ts }, ./dist/style.css: ./dist/style.css, ./*: ./* }, files: [ dist ], scripts: { dev: vite build --watch --mode development, build: vue-tsc --noEmit vite build, prepublishOnly: pnpm run build }, peerDependencies: { vue: ^3.4.0 }, devDependencies: { // ... 与根目录共享的依赖会自动链接这里可以放组件库特有的 devDeps } }执行构建cd packages/components pnpm run build构建完成后dist目录下会生成my-ui.es.js: ES Module 格式支持 Tree Shaking。my-ui.umd.js: UMD 格式可用于script标签直接引入或旧环境。my-ui.umd.cjs: CommonJS 格式。types/: 完整的 TypeScript 类型定义文件。9. 单元测试保障为了保证组件质量必须编写单元测试。我们使用 Vitest与 Vite 生态兼容性好和 Vue Test Utils。在项目根目录安装测试相关依赖pnpm add -Dw vitest vue/test-utilsnext jsdom vitest/ui happy-dom在packages/components目录下创建vitest.config.tsimport { defineConfig } from vitest/config import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], test: { environment: happy-dom, // 或 jsdom // 匹配测试文件 include: [src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}], }, })为 Button 组件创建测试文件packages/components/src/button/Button.spec.tsimport { describe, it, expect } from vitest import { mount } from vue/test-utils import Button from ./Button.vue describe(Button.vue, () { it(renders slot content, () { const wrapper mount(Button, { slots: { default: Click Me, }, }) expect(wrapper.text()).toContain(Click Me) }) it(applies the correct type class, () { const wrapper mount(Button, { props: { type: primary, }, }) expect(wrapper.classes()).toContain(my-button--primary) }) it(emits click event when clicked and not disabled, async () { const wrapper mount(Button) await wrapper.trigger(click) expect(wrapper.emitted()).toHaveProperty(click) }) it(does not emit click event when disabled, async () { const wrapper mount(Button, { props: { disabled: true, }, }) await wrapper.trigger(click) expect(wrapper.emitted().click).toBeFalsy() }) it(shows loading state, () { const wrapper mount(Button, { props: { loading: true, }, }) expect(wrapper.classes()).toContain(is-loading) expect(wrapper.find(.my-button__loading).exists()).toBe(true) }) })在packages/components/package.json中添加测试脚本{ scripts: { test: vitest, test:ui: vitest --ui, test:run: vitest run } }运行测试cd packages/components pnpm run test # 进入监听模式 # 或 pnpm run test:run # 单次运行10. 文档系统搭建好的文档是组件库成功的关键。我们使用 VitePress它能与 Vite 项目无缝集成支持 Markdown 和 Vue 组件混写。在项目根目录安装 VitePresspnpm add -Dw vitepress在docs目录下初始化cd docs npx vitepress init按照提示选择主题、是否启用搜索等。完成后修改docs/.vitepress/config.tsimport { defineConfig } from vitepress export default defineConfig({ title: My UI, description: A Vue 3 UI Component Library, themeConfig: { nav: [ { text: 指南, link: /guide/ }, { text: 组件, link: /components/button }, { text: GitHub, link: https://github.com/your-repo }, ], sidebar: { /components/: [ { text: 基础组件, items: [ { text: Button 按钮, link: /components/button }, // 后续添加其他组件文档 ], }, ], }, }, })创建组件文档docs/components/button.md# Button 按钮 常用的操作按钮。 ## 基础用法 使用 type、size、plain、round、circle、disabled、loading 属性来定义按钮的样式和行为。 demo src./demo/ButtonDemo.vue/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) | ### Slots | 名称 | 说明 | |------|------| | default | 自定义按钮内容 |创建演示组件docs/components/demo/ButtonDemo.vue可以直接引入 Playground 中的示例代码。在docs/.vitepress/theme/index.ts中注册全局的demo组件用于在 Markdown 中渲染 Vue 示例。最后在根目录package.json中添加文档脚本{ scripts: { docs:dev: cd docs pnpm run dev, docs:build: cd docs pnpm run build, docs:preview: cd docs pnpm run preview } }运行pnpm run docs:dev即可启动文档站点。11. 代码规范与提交约定为了保证团队协作质量需要集成代码检查和格式化工具。ESLint Prettier:pnpm add -Dw eslint eslint-plugin-vue typescript-eslint/parser typescript-eslint/eslint-plugin prettier eslint-config-prettier配置.eslintrc.cjs和.prettierrc。Husky lint-staged在 Git 提交前自动检查。pnpm add -Dw husky lint-staged npx husky install npx husky add .husky/pre-commit npx lint-staged在package.json中配置lint-staged。Commitizen Commitlint规范提交信息。pnpm add -Dw commitizen cz-conventional-changelog commitlint/config-conventional commitlint/cli配置commitlint.config.js和package.json中的config.commitizen。12. 发布到 npm在发布前确保packages/components/package.json中的信息正确特别是name、version、main、module、types、files和peerDependencies。登录 npm如果没有账号先去 npmjs.com 注册npm login构建最新版本cd packages/components pnpm run build发布npm publish --access public # 如果是 scoped package 且首次发布需要 --access public发布后其他项目就可以通过npm install my-ui/components来使用你的组件库了。13. 常见问题与排查方法在开发过程中你可能会遇到以下问题问题现象可能原因排查方式解决方案Vite 构建时报 Vue 类型错误vue-tsc版本与 Vue 3.4 不兼容或tsconfig.json配置有误。检查终端错误信息确认是否来自vue-tsc。确保vue-tsc版本与 Vue 3.4 兼容。在tsconfig.json中正确配置vueCompilerOptions。Playground 中引入组件报错Cannot find module工作空间链接未正确建立或play项目的node_modules未更新。检查play/package.json中依赖版本是否为workspace:*。在play目录下运行pnpm install重新链接。或直接在根目录运行pnpm install -r。组件样式在 Playground 中不生效style scoped可能导致样式被隔离或构建时样式未正确提取。检查浏览器开发者工具看样式是否被应用或是否有哈希后缀。对于组件库通常使用非scoped的全局样式并通过 BEM 等命名约定防止冲突。检查 Vite 配置中是否包含 CSS 处理。TypeScript 类型提示不工作.d.ts文件未生成或生成路径不对。检查dist/types目录下是否有对应的.d.ts文件。确保vite.config.ts中dts插件配置正确且tsconfig.json中declaration和declarationDir已设置。按需引入时 Tree Shaking 无效组件库打包格式不是 ES Module或项目构建工具未正确配置。检查发布的package.json中module字段是否指向.es.js文件。确保使用vite或rollup打包出标准的 ES Module 格式。在消费侧项目使用支持 Tree Shaking 的构建工具如 Vite、Webpack 4。发布到 npm 时提示无权限包名已被占用或未登录 npm。检查package.json中的name是否唯一。运行npm whoami检查登录状态。更换一个唯一的包名尤其是非 scoped 名称。使用npm login重新登录。对于 scoped package (xxx/yyy)需使用npm publish --access public。14. 最佳实践与进阶建议组件设计原则单一职责一个组件只做一件事。受控与非受控同时支持受控v-model和非受控模式。无障碍访问考虑aria-*属性确保键盘可操作。向前兼容废弃 API 时提供警告和迁移路径。样式方案选择CSS-in-JS如 UnoCSS、Tailwind CSS适合工具类优先但需考虑运行时体积。CSS Modules / Scoped CSS默认选择能提供良好的样式隔离。Sass/Less提供变量、混合等高级功能适合复杂主题系统。推荐使用 Scoped CSS 编写组件基础样式同时提供一套基于 CSS 变量的主题系统允许使用者覆盖。性能优化按需引入确保库的构建产物支持 ES Module 和 Tree Shaking。虚拟滚动对于长列表组件如 Select、Table是必须的。懒加载对于复杂组件或图标可以考虑动态导入。避免不必要的渲染合理使用v-once、v-memo和shallowRef。版本管理与发布语义化版本严格遵守major.minor.patch。变更日志使用standard-version或conventional-changelog自动生成 CHANGELOG.md。Beta 测试发布前先发布beta或next版本供内部测试。Monorepo 多包管理如果组件库拆分为多个包如components、utils、theme使用changesets或lerna管理版本和发布。生态建设图标库提供一套与组件风格匹配的图标组件。Hooks 库将与 UI 无关的逻辑抽离为 Composition API 的 Hooks如useForm、usePagination。主题编辑器提供一个在线工具让使用者可视化地定制主题变量。插件系统允许第三方开发者为你的组件库开发插件。从零开始构建一个 Vue 3.4 UI 组件库是一项系统工程但每一步都有明确的目标和可验证的产出。核心在于将大目标拆解为可执行的小任务初始化项目、开发单个组件、配置构建、编写测试、搭建文档、最终发布。在这个过程中你会深刻理解 Vue 3 的组合式 API、Vite 的构建流程、TypeScript 的类型系统以及前端工程化的各个环节。当你成功发布第一个版本并在另一个项目中通过npm install引入并使用自己开发的组件时那种成就感是无可替代的。接下来你可以按照同样的模式逐步添加 Input、Select、Modal、Table 等更复杂的组件不断完善你的组件库生态。