如果你正在使用 Vue 3.4 开发中后台项目是否曾有过这样的困惑面对琳琅满目的第三方 UI 组件库要么风格与产品设计不符需要大量覆写样式要么某些业务场景下的特殊交互现有组件无法满足需要自己“魔改”要么随着项目迭代组件库的版本升级带来意想不到的 Breaking Changes导致整个项目需要紧急适配。这背后的根本矛盾在于使用现成组件库是用“通用性”交换了“控制权”。你获得了开箱即用的便利却失去了对组件原子行为的精准掌控、对设计系统的完全贯彻以及对技术债务的终极解释权。因此从零开始构建一套属于自己的 Vue 3.4 UI 组件库并非重复造轮子而是一次对前端工程化、设计系统、以及团队协作模式的深度投资。它解决的不仅是“用什么组件”的问题更是“如何高效、一致、可持续地构建产品界面”的系统性问题。本文将带你进行一次“去忽悠”的实战。我们不会空谈理论而是聚焦于 Vue 3.4 的最新特性如defineModel的稳定、defineOptions的引入、性能优化从 Monorepo 工程架构、原子化设计、组件开发、文档与构建到最终的私有化发布完整走通一套企业级 UI 组件库的开发链路。读完本文你将掌握工程基石如何用pnpmMonorepo搭建高内聚、低耦合的组件库项目结构。开发范式如何运用 Vue 3.4 的组合式 API 与最新语法编写高复用、类型安全的组件。样式方案如何结合 CSS 变量、现代 CSS 特性如:where与构建工具实现主题定制与样式隔离。质量保障如何配置单元测试、类型检查与自动化构建确保组件库的稳定性。交付闭环如何打包、生成类型声明、编写文档并发布到私有 npm 仓库。我们的目标是让你拥有一个可维护、可测试、可扩展的“资产”而不仅仅是一堆.vue文件。1. 为什么你需要从零开发 UI 组件库在决定投入精力之前我们必须算清这笔账自研组件库的成本与收益究竟在哪里核心收益设计一致性完全掌控设计 Token色彩、间距、圆角、字体确保公司所有产品视觉语言统一。技术栈绑定深度契合 Vue 3.4 生态可以利用effectScope、defineModel等新特性进行性能与开发体验优化避免为兼容旧版本而妥协。业务强定制组件 API 设计可完全贴合自身业务逻辑例如表格组件内置特定的数据格式化、权限控制逻辑减少业务层重复代码。升级自主权技术栈升级节奏由自己掌控避免被第三方库的重大更新“突袭”。团队能力提升过程本身是对团队成员在架构设计、代码规范、工程化方面极好的锻炼。需要承受的成本初期投入大从零到一需要完整的规划、基建和多个核心组件的开发。持续维护责任需要专人负责迭代、修复 Bug、编写文档和解答内部使用问题。生态丰富度初期无法媲美 Ant Design Vue、Element Plus 等成熟库的组件数量。结论如果你的团队长期维护一个或多个中大型 Vue 3 项目对 UI 一致性有高要求且业务存在大量定制化交互那么自研组件库的长期收益将远超初期成本。反之对于短平快或一次性项目直接使用成熟开源库仍是更优解。2. 现代 UI 组件库的核心架构概念在动手写代码前需要建立几个关键认知这决定了组件库的“基因”。2.1 设计系统 (Design System) 驱动组件库是设计系统在代码层面的实现。核心是Design Tokens即一系列代表设计决策的变量如--color-primary: #1890ff;。所有组件的样式都应引用这些 Token而非硬编码的值。这样只需修改 Token就能实现全局主题切换。2.2 原子化设计 (Atomic Design)这是一种构建界面的方法论将组件分为五个层次原子 (Atoms)按钮、输入框、图标等基础组件。分子 (Molecules)由原子组合而成如搜索框输入框按钮。组织 (Organisms)由分子和原子组合的复杂区块如页头。模板 (Templates)页面级别的骨架聚焦于布局。页面 (Pages)填充真实内容的模板。在开发时我们重点构建原子和分子组件确保它们的纯粹性和可复用性。2.3 Monorepo 项目结构这是管理组件库、文档、示例项目的标准姿势。它允许你在一个仓库内管理多个包共享配置简化依赖管理。my-ui-library/ ├── packages/ │ ├── core/ # 核心工具函数、常量、类型定义 │ ├── theme/ # 样式与主题包 (CSS/SCSS变量) │ ├── button/ # 按钮组件包 │ ├── input/ # 输入框组件包 │ └── vue-ui/ # 主包聚合所有组件并导出 ├── docs/ # 组件文档网站项目 ├── play/ # 开发调试用的示例项目 ├── package.json # 根目录 package.json (workspace配置) └── pnpm-workspace.yaml # pnpm workspace 配置文件2.4 “按需引入”与“全量打包”这是组件库的两个核心交付形态。现代工具链如 Vite、unplugin-vue-components可以轻松实现基于 ES Module 的 Tree Shaking让“按需引入”近乎零成本。同时我们也需要提供一份全量打包的文件供传统构建方式或 CDN 引入使用。3. 环境准备与 Monorepo 初始化我们选择pnpm作为包管理器因其对 Monorepo 的出色支持和高效的磁盘空间利用。步骤 1初始化项目并配置 Workspace# 创建项目目录 mkdir my-ui-library cd my-ui-library # 初始化 package.json pnpm init # 创建 pnpm-workspace.yaml 文件定义工作空间 echo packages: - packages/* - docs - play pnpm-workspace.yaml步骤 2创建核心包与示例项目在packages/目录下我们创建几个核心包。以core包和button组件包为例# 创建 core 包 mkdir -p packages/core cd packages/core pnpm init # 修改 packages/core/package.json 的 name 为 my-ui/corepackages/core/package.json示例{ name: my-ui/core, version: 0.0.1, description: Core utilities for My UI library, main: index.ts, module: index.ts, types: index.ts, sideEffects: false, scripts: {}, dependencies: {}, devDependencies: {} }同理创建my-ui/theme,my-ui/button等包。vue-ui主包稍后创建。步骤 3创建开发环境 (play)在根目录下使用 Vite 快速创建一个 Vue 3 项目用于开发和调试组件。pnpm create vite play --template vue-ts cd play pnpm install步骤 4统一依赖与工具链在根目录安装共享的开发依赖如 TypeScript、Vue、ESLint、Prettier、Vitest 等。pnpm add -Dw typescript vue vue/tsconfig eslint prettier vitest vitejs/plugin-vue创建共享的tsconfig.json和eslint配置在根目录各子包通过extends引用。4. 开发第一个组件Button我们从最基础的 Button 组件开始实践 Vue 3.4 的完整开发流程。4.1 组件设计与 API 规划首先在packages/button/src目录下创建组件文件。packages/button/src/button.vuetemplate button :class[ my-button, my-button--${type}, my-button--${size}, { is-plain: plain, is-round: round, is-circle: circle, is-disabled: disabled || loading, is-loading: loading } ] :disableddisabled || loading :autofocusautofocus :typenativeType clickhandleClick !-- 加载状态 -- span v-ifloading classmy-button__loading svg classcircular viewBox25 25 50 50 circle cx50 cy50 r20 fillnone/ /svg /span !-- 图标 -- span v-if$slots.icon || icon classmy-button__icon slot nameicon i :classicon/i /slot /span !-- 默认插槽内容 -- span classmy-button__content slot / /span /button /template script setup langts import { computed } from vue // 使用 Vue 3.4 的 defineOptions 定义组件名可选便于调试 defineOptions({ name: MyButton }) // 定义 Props interface ButtonProps { type?: primary | success | warning | danger | info | text size?: large | default | small plain?: boolean round?: boolean circle?: boolean disabled?: boolean loading?: boolean autofocus?: boolean nativeType?: button | submit | reset icon?: string } const props withDefaults(definePropsButtonProps(), { type: default, size: default, plain: false, round: false, circle: false, disabled: false, loading: false, autofocus: false, nativeType: button, icon: }) // 使用 Vue 3.4 稳定的 defineModel 定义双向绑定本例中不需要仅作演示 // const modelValue defineModelstring() // 定义 Emits const emit defineEmits{ click: [evt: MouseEvent] }() const handleClick (evt: MouseEvent) { if (props.loading || props.disabled) return emit(click, evt) } /script style scoped .my-button { /* 基础样式后续会替换为 Design Tokens */ display: inline-flex; align-items: center; justify-content: center; line-height: 1; height: 32px; padding: 8px 15px; white-space: nowrap; cursor: pointer; border: 1px solid #dcdfe6; border-radius: 4px; background-color: #fff; color: #606266; font-size: 14px; transition: all .1s; /* 引入 CSS 变量 */ border-color: var(--my-border-color, #dcdfe6); background-color: var(--my-bg-color, #fff); color: var(--my-text-color, #606266); } .my-button:hover { opacity: 0.8; } .my-button.is-disabled { cursor: not-allowed; opacity: 0.6; } /* 类型样式 */ .my-button--primary { --my-border-color: #409eff; --my-bg-color: #409eff; --my-text-color: #fff; } /* 尺寸样式 */ .my-button--small { height: 24px; padding: 5px 11px; font-size: 12px; } .my-button--large { height: 40px; padding: 12px 19px; font-size: 16px; } /* 加载动画 */ .my-button__loading { display: inline-flex; margin-right: 6px; } .circular { animation: rotate 2s linear infinite; width: 14px; height: 14px; } keyframes rotate { 100% { transform: rotate(360deg); } } /style4.2 导出组件并定义类型创建packages/button/index.ts和类型定义文件。packages/button/index.ts:import Button from ./src/button.vue import type { App } from vue // 为组件提供 install 方法用于 Vue.use() 全局注册 Button.install (app: App) { app.component(Button.name!, Button) } export default Button export { Button } // 导出组件的 Props 类型方便使用者进行类型推断 export type { ButtonProps } from ./src/button.vuepackages/button/vue-shim.d.ts(确保 TypeScript 识别.vue文件):declare module *.vue { import type { DefineComponent } from vue const component: DefineComponent{}, {}, any export default component }4.3 在主包中聚合所有组件创建packages/vue-ui包作为组件库的主入口。packages/vue-ui/index.ts:// 导入所有组件 import Button from my-ui/button // 后续导入 Input, Select 等... // 组件列表 const components [Button /*, ...*/] // 全局安装方法 const install (app: any) { components.forEach(component { app.component(component.name, component) }) } // 按需导出 export { Button /*, ...*/ } export default { install, version: 0.0.1 }5. 构建与打包配置组件库需要被构建成多种格式ES Module, CommonJS, UMD以供不同环境使用。我们使用vite进行构建。在packages/vue-ui目录下创建vite.config.tsimport { 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: resolve(__dirname, tsconfig.json), outDir: dist/types, insertTypesEntry: true, }) ], build: { lib: { entry: resolve(__dirname, index.ts), name: MyUI, fileName: (format) my-ui.${format}.js }, rollupOptions: { // 确保外部化处理那些你不想打包进库的依赖 external: [vue], output: { // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量 globals: { vue: Vue } } }, sourcemap: true, minify: terser } })在packages/vue-ui/package.json中定义构建脚本和入口{ name: my-ui, version: 0.0.1, description: A UI component library based on Vue 3.4, main: ./dist/my-ui.umd.cjs, module: ./dist/my-ui.es.js, types: ./dist/types/index.d.ts, exports: { .: { import: ./dist/my-ui.es.js, require: ./dist/my-ui.umd.cjs, types: ./dist/types/index.d.ts }, ./dist/style.css: ./dist/style.css }, files: [dist], scripts: { build: vite build }, peerDependencies: { vue: 3.4.0 }, devDependencies: { vitejs/plugin-vue: ^5.0.0, vite: ^5.0.0, vue: ^3.4.0, vite-plugin-dts: ^3.0.0 } }运行pnpm run build后将在dist目录生成构建产物。6. 样式系统与主题定制硬编码的样式是组件库的“死穴”。我们需要建立基于 CSS 变量的主题系统。步骤 1定义 Design Tokens在packages/theme/src目录下创建tokens.css/* packages/theme/src/tokens.css */ :root { /* 颜色 */ --my-color-primary: #409eff; --my-color-success: #67c23a; --my-color-warning: #e6a23c; --my-color-danger: #f56c6c; --my-color-info: #909399; --my-color-primary-light-3: color-mix(in srgb, var(--my-color-primary) 70%, white); --my-color-primary-dark-2: color-mix(in srgb, var(--my-color-primary) 20%, black); /* 中性色 */ --my-color-text-primary: #303133; --my-color-text-regular: #606266; --my-color-text-secondary: #909399; --my-color-text-placeholder: #c0c4cc; --my-border-color: #dcdfe6; --my-border-color-light: #e4e7ed; --my-bg-color: #ffffff; --my-bg-color-page: #f2f3f5; /* 尺寸 */ --my-border-radius-base: 4px; --my-border-radius-small: 2px; --my-border-radius-round: 20px; --my-border-radius-circle: 100%; /* 字体 */ --my-font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif; --my-font-size-base: 14px; --my-font-size-small: 12px; --my-font-size-large: 16px; /* 间距 */ --my-spacing-base: 8px; }步骤 2组件样式引用 Tokens修改 Button 组件的样式将硬编码值替换为 CSS 变量.my-button { /* ... 其他基础样式 ... */ border: 1px solid var(--my-border-color); background-color: var(--my-bg-color); color: var(--my-text-color-regular); border-radius: var(--my-border-radius-base); font-size: var(--my-font-size-base); font-family: var(--my-font-family); padding: calc(var(--my-spacing-base) * 1) calc(var(--my-spacing-base) * 1.875); } .my-button--primary { --my-button-border-color: var(--my-color-primary); --my-button-bg-color: var(--my-color-primary); --my-button-text-color: #fff; }步骤 3打包样式文件在vite.config.ts中配置将tokens.css和组件样式一起打包。7. 在示例项目中集成与调试回到play项目通过pnpmworkspace 链接本地包进行测试。步骤 1安装本地包在play/package.json中添加依赖{ dependencies: { my-ui: workspace:*, my-ui/theme: workspace:* } }然后在play目录下运行pnpm install。步骤 2在 Play 项目中使用组件修改play/src/App.vuetemplate div h1My UI Playground/h1 MyButton typeprimary clickhandleClickPrimary Button/MyButton MyButton typesuccess plainSuccess Plain/MyButton MyButton sizelarge loadingLoading/MyButton /div /template script setup langts import { MyButton } from my-ui import my-ui/theme/dist/tokens.css // 引入主题变量 const handleClick (evt: MouseEvent) { console.log(Button clicked!, evt) } /script步骤 3配置按需引入与自动导入可选但推荐使用unplugin-vue-components和unplugin-auto-import可以实现在业务项目中无需手动导入组件。这需要在组件库侧提供解析器并在业务项目的 Vite 配置中配置。8. 单元测试与质量保障没有测试的组件库是不可靠的。我们使用Vitest进行单元测试。在packages/button目录下安装 Vitest 和测试工具pnpm add -D vitest vue/test-utils happy-dom创建测试文件packages/button/__tests__/button.spec.tsimport { describe, it, expect } from vitest import { mount } from vue/test-utils import Button from ../src/button.vue describe(Button.vue, () { it(renders default slot content, () { const wrapper mount(Button, { slots: { default: Click Me } }) expect(wrapper.text()).toContain(Click Me) }) 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)).toBeUndefined() }) it(applies correct css class for type, () { const wrapper mount(Button, { props: { type: primary } }) expect(wrapper.classes()).toContain(my-button--primary) }) })在package.json中添加测试脚本{ scripts: { test: vitest run, test:watch: vitest } }9. 文档与发布9.1 使用 Vitepress 构建文档在docs目录下初始化 Vitepress为每个组件编写使用说明、API 文档和示例。9.2 版本管理与发布版本号遵循语义化版本控制 (SemVer)。变更日志 (CHANGELOG)使用conventional-changelog工具自动生成。发布到私有 npm在根目录配置.npmrc指向公司私有仓库。使用npm publish或pnpm publish发布packages/vue-ui包。注意package.json中的files字段要包含所有需要发布的文件如dist,README.md。9.3 持续集成 (CI)在.github/workflows下配置 CI 脚本实现提交代码时自动运行测试、构建和发布预览。10. 常见问题与排查思路问题现象可能原因排查方式解决方案在 Play 项目中引入组件提示Cannot find module1. Workspace 链接未生效。2. 主包index.ts导出路径错误。1. 检查play/package.json依赖版本是否为workspace:*。2. 在 Play 项目根目录运行pnpm list my-ui查看链接情况。3. 检查主包index.ts导入语句是否正确。1. 在根目录运行pnpm install重新建立链接。2. 确保导出路径指向编译前的源码或正确的入口文件。组件样式不生效1. CSS 变量未正确引入或定义。2.scoped样式导致选择器权重问题。3. 构建时样式文件未打包。1. 检查浏览器开发者工具查看 CSS 变量是否被应用到元素上。2. 检查最终打包的 CSS 文件中是否包含组件样式。1. 确保在项目入口引入了主题 CSS 文件。2. 对于需要覆盖的样式考虑使用:deep()选择器或降低scoped的使用。3. 检查 Vite 配置确保 CSS 被正确处理。TypeScript 类型报错1..vue文件类型声明缺失。2. 导出的类型定义文件 (d.ts) 未生成或路径不对。1. 检查是否在tsconfig.json中包含了vue-shim.d.ts。2. 检查vite-plugin-dts插件是否正常工作生成的index.d.ts文件是否存在。1. 确保项目中有正确的*.vue类型声明文件。2. 检查vite.config.ts中dts插件的配置确保outDir正确。按需引入 (unplugin) 不工作1. 解析器 (resolver) 路径配置错误。2. 组件未提供正确的install方法或name属性。1. 检查unplugin-vue-components的resolvers配置路径是否指向组件库目录。2. 检查组件是否通过component.install或component.name暴露了必要信息。1. 参考unplugin文档编写或使用社区提供的对应组件库解析器。2. 确保每个组件都设置了name并提供了install方法。构建产物文件过大1. 未正确 externalize Vue 等依赖。2. 未开启代码压缩。3. 包含了未使用的源代码或文件。1. 使用rollup-plugin-visualizer分析包体积。2. 检查rollupOptions.external配置。1. 确保external配置了[vue]。2. 开启minify: terser。3. 检查package.json的files字段只包含必要文件。11. 最佳实践与工程建议单一职责与组合每个组件只做一件事。复杂组件通过组合更小的子组件来实现。受控与非受控为组件设计好受控使用v-model和非受控模式提供灵活性。Vue 3.4 的defineModel让这变得非常简单。无障碍访问 (A11y)为交互式组件添加必要的 ARIA 属性如aria-label,aria-disabled确保残障用户可使用。性能优化使用defineAsyncComponent懒加载非首屏必需的大型组件。合理使用v-once和v-memo。避免在v-for中使用复杂的组件。版本管理使用 Changesets 或 Lerna 来管理 Monorepo 内多个包的版本号和生成 CHANGELOG。代码规范在项目初期就配置好 ESLint、Prettier、Commitlint并集成到 CI/CD 流程中。文档即代码将示例代码嵌入 Vitepress 文档并确保示例是可交互、可运行的。渐进式发布先在小范围项目内试用收集反馈并迭代稳定后再推广至全公司。从零开始构建 Vue 3.4 UI 组件库是一次贯穿前端工程化、设计系统、团队协作和软件产品思维的完整训练。它始于一个具体的 Button 组件但成于一套可持续演进的标准和流程。本文为你铺设了从技术选型、架构设计、开发实践到交付上线的完整路径。真正的挑战不在于写出第一个组件而在于如何让这套系统在业务需求的冲刷下保持活力与秩序。现在你可以从packages/button开始将蓝图转化为代码逐步积累起属于自己团队的核心前端资产。