三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Vue 3 UI组件库从零搭建:Monorepo架构、按需加载与工程化实践

Vue 3 UI组件库从零搭建:Monorepo架构、按需加载与工程化实践

如果你是一名 Vue 开发者,是否曾有过这样的困惑:项目里用了不少第三方 UI 库,但总有几个组件样式不符合业务需求,改起来又怕破坏原有逻辑;或者,团队内部缺乏一套统一的视觉和交互规范,导致每个项目都像在“打补丁”?

更关键的是,当面试官问起“你了解 UI 组件库的设计和实现吗?”时,很多人只能泛泛而谈,却说不清一个按钮从设计到发布的全链路细节。

这篇文章要解决的,正是这个“知其然,不知其所以然”的问题。我们不会空谈“组件化思想”或“设计模式”,而是用 Vue 3.4 这个当前最稳定、性能最优的版本,从零开始,手把手带你搭建一套真正可发布、可维护、可复用的 UI 组件库。这不是一个简单的 Demo,而是一个包含单包构建、多包管理(Monorepo)、自动化文档、按需加载、主题定制、单元测试的完整工程化实践。

读完本文,你将彻底掌握:

  1. 工程基石:如何用 Vite + TypeScript + pnpm workspace 搭建一个现代前端 Monorepo 项目结构。
  2. 组件核心:如何设计一个高内聚、低耦合的 Vue 组件,包括 Props、Slots、Events、Expose 等 API 设计规范。
  3. 打包艺术:如何配置构建工具,同时输出 ES Module、CommonJS、UMD 等多种格式,并支持 Tree Shaking。
  4. 开发者体验:如何集成 Vitepress 实现自动化组件文档和演示,让使用和协作变得简单。
  5. 质量保障:如何为组件编写单元测试(Vitest)和类型声明,确保代码健壮性。

我们直接从最关键的工程决策开始。

1. 为什么从零搭建?理解现代 UI 库的完整生命周期

在直接敲代码之前,我们必须想清楚:在 Ant Design、Element Plus 等成熟方案遍地的今天,为什么还要自己造轮子?

答案不在于“替代”,而在于“理解”和“掌控”。自己搭建一遍,你会深刻理解以下问题,这些是单纯使用库无法获得的认知:

  • 依赖管理的边界:你的组件库应该依赖 Vue 本身,但要不要依赖 Lodash、Day.js 这些工具库?如何避免版本冲突和包体积膨胀?
  • 样式方案的抉择:是用 CSS-in-JS(如 unocss)、预处理器(Sass/Less),还是纯 CSS 变量?如何实现主题切换和暗黑模式?
  • 类型安全的保障:如何利用 TypeScript 提供完美的类型提示,让使用者在编码阶段就能发现错误?
  • 版本发布的流程:如何管理多个包的版本号?如何自动化生成 CHANGELOG?如何发布到私有或公有仓库?

本次实战,我们将采用目前社区最主流的“黄金组合”:Vue 3.4 + Vite 5 + TypeScript + pnpm Monorepo。这个组合在开发体验、构建速度和包管理效率上达到了最佳平衡。

2. 项目初始化与 Monorepo 结构搭建

我们首先创建项目的根目录,并初始化包管理和工作区配置。

2.1 初始化项目与 pnpm workspace

# 创建项目根目录 mkdir vue-ui-library && cd vue-ui-library # 初始化 package.json pnpm init # 创建 pnpm-workspace.yaml 文件,定义工作区 cat > pnpm-workspace.yaml << EOF packages: - 'packages/*' - 'docs' - 'play' # 用于开发调试的 playground EOF

pnpm-workspace.yaml文件是 pnpm Monorepo 的核心,它告诉 pnpm 哪些目录是独立的子包。

2.2 创建核心包与文档包

接下来,我们创建主要的包结构:

# 创建 packages 目录存放核心包 mkdir -p packages/core mkdir -p packages/utils mkdir -p docs mkdir -p play # 初始化核心组件库包 cd packages/core pnpm init # 将包名改为 @vue-ui-library/core,scope 可以根据需要修改

编辑packages/core/package.json,设置基本信息和入口:

{ "name": "@vue-ui-library/core", "version": "0.0.1", "description": "A Vue 3 UI component library.", "type": "module", "main": "./dist/vue-ui-library.umd.cjs", "module": "./dist/vue-ui-library.mjs", "types": "./dist/index.d.ts", "exports": { ".": { "import": "./dist/vue-ui-library.mjs", "require": "./dist/vue-ui-library.umd.cjs", "types": "./dist/index.d.ts" }, "./*": "./*" }, "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" } }

关键点解析

  1. name: 使用@scope/package-name的形式,这是管理私有或组织包的常见做法。
  2. exports: 现代包的入口定义,清晰地声明了 ESM、CJS 和类型的路径,对工具链友好。
  3. peerDependencies: 将vue声明为 peer 依赖,是开发 UI 库的最佳实践。这能确保你的库使用项目中的 Vue 实例,避免多个 Vue 副本导致的错误。
  4. files: 指定发布到 npm 时包含的文件,通常只有构建产物dist目录。

2.3 配置根目录的 TypeScript 和 Vite

回到项目根目录,安装共享的开发依赖:

# 在根目录执行 pnpm add -Dw typescript @types/node vite @vitejs/plugin-vue vue-tsc

-Dw表示作为开发依赖(devDependencies)安装到根 workspace。

创建根目录的tsconfig.jsonvite.config.ts作为基础配置:

// tsconfig.json (根目录) { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "preserve", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "baseUrl": ".", "paths": { "@vue-ui-library/*": ["packages/*/src"] } }, "include": ["packages/**/*.ts", "packages/**/*.tsx", "packages/**/*.vue"], "references": [ { "path": "./packages/core" } ] }

paths配置让我们在代码中可以使用@vue-ui-library/core这样的别名导入。

3. 开发第一个组件:Button

理论铺垫完成,现在开始实战。我们从最基础的Button组件开始。

3.1 组件源码结构

packages/core/src下创建组件目录和文件:

packages/core/src/ ├── button/ │ ├── Button.vue // 组件模板与逻辑 │ ├── index.ts // 组件导出文件 │ └── style.css // 组件样式 ├── index.ts // 库的主入口文件 └── vite-env.d.ts // Vite 环境类型声明

首先,编写Button.vue

<!-- packages/core/src/button/Button.vue --> <template> <button class="vul-button" :class="[ `vul-button--${type}`, `vul-button--${size}`, { 'is-plain': plain, 'is-round': round, 'is-circle': circle, 'is-disabled': disabled || loading, 'is-loading': loading } ]" :disabled="disabled || loading" @click="handleClick" > <span v-if="loading" class="vul-button__loading"> <!-- 这里可以放一个加载图标组件,暂时用文本 --> ⌛ </span> <span class="vul-button__content"> <slot /> </span> </button> </template> <script setup lang="ts"> import { withDefaults } 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 } // 使用 withDefaults 提供默认值 const props = withDefaults(defineProps<Props>(), { type: 'default', size: 'default', plain: false, round: false, circle: false, disabled: false, loading: false }) // 定义组件的事件 const emit = defineEmits<{ click: [event: MouseEvent] }>() const handleClick = (event: MouseEvent) => { if (!props.disabled && !props.loading) { emit('click', event) } } </script> <style scoped> .vul-button { display: inline-flex; align-items: center; justify-content: center; line-height: 1; height: 32px; padding: 8px 16px; white-space: nowrap; cursor: pointer; border: 1px solid #dcdfe6; border-radius: 4px; background-color: #ffffff; color: #606266; font-size: 14px; transition: all 0.1s; outline: none; user-select: none; } .vul-button:hover { border-color: #c6e2ff; background-color: #ecf5ff; color: #409eff; } .vul-button--primary { background-color: #409eff; border-color: #409eff; color: #ffffff; } .vul-button--primary:hover { background-color: #66b1ff; border-color: #66b1ff; } .vul-button--small { height: 28px; padding: 6px 12px; font-size: 12px; } .vul-button--large { height: 36px; padding: 10px 20px; font-size: 16px; } .vul-button.is-round { border-radius: 20px; } .vul-button.is-circle { border-radius: 50%; width: 32px; padding: 8px; } .vul-button.is-circle.vul-button--small { width: 28px; } .vul-button.is-circle.vul-button--large { width: 36px; } .vul-button.is-disabled { cursor: not-allowed; opacity: 0.6; } .vul-button__loading { margin-right: 4px; animation: rotate 1s linear infinite; } @keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } </style>

设计要点

  1. CSS 命名规范:采用vul-(Vue UI Library) 作为前缀,遵循 BEM 思想(block__element--modifier),避免样式污染。
  2. TypeScript 集成:使用<script setup lang="ts">defineProps/defineEmits的泛型写法,获得完整的类型推断。
  3. 可访问性:正确处理disabledloading状态下的click事件和光标样式。

3.2 组件导出与库入口

创建组件的导出文件:

// packages/core/src/button/index.ts import Button from './Button.vue' import type { App } from 'vue' // 为组件添加 install 方法,使其可以被 Vue.use() 全局安装 Button.install = (app: App) => { app.component(Button.name || 'VulButton', Button) } export default Button export { Button }

现在,创建库的主入口文件,负责导出所有组件:

// packages/core/src/index.ts import Button from './button' // 组件列表,用于全局安装和按需引入 const components = [Button] // 全局安装插件 const install = (app: any) => { components.forEach(component => { app.component(component.name || component.displayName, component) }) } // 支持按需导入 export { Button, install } // 默认导出插件 export default { install, version: '__VERSION__' // 构建时会替换 }

3.3 配置核心包的 Vite 构建

每个子包可以有独立的构建配置。在packages/core目录下创建vite.config.ts

// packages/core/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({ outDir: 'dist', include: ['src/**/*'], staticImport: true, insertTypesEntry: true, }), ], build: { outDir: 'dist', lib: { entry: resolve(__dirname, 'src/index.ts'), name: 'VueUILibrary', fileName: (format) => `vue-ui-library.${format}.js`, }, rollupOptions: { // 确保外部化处理那些你不想打包进库的依赖 external: ['vue'], output: { // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量 globals: { vue: 'Vue', }, exports: 'named', }, }, sourcemap: true, }, resolve: { alias: { '@': resolve(__dirname, 'src'), }, }, })

关键配置解析

  1. build.lib: 指定库模式的入口和输出文件名。
  2. rollupOptions.external:至关重要。将vue外部化,不打包进你的库,这与你声明的peerDependencies一致。
  3. vite-plugin-dts: 自动生成.d.ts类型声明文件,这是 TypeScript 项目引用你的库所必需的。

packages/core目录下运行pnpm build,你将在dist目录下看到构建产物,包括多种格式的 JS 文件和类型声明。

4. 搭建开发环境与实时预览 (Playground)

packages里开发组件,我们需要一个独立的应用来实时预览和调试。这就是play目录的作用。

4.1 创建 Playground 应用

cd play pnpm create vite@latest . -- --template vue-ts

按照提示完成初始化后,修改其vite.config.ts,使其能正确解析本地包:

// play/vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@vue-ui-library/core': resolve(__dirname, '../packages/core/src/index.ts'), }, }, })

这个别名配置让我们在 Playground 中可以直接导入正在开发的组件库源码。

4.2 在 Playground 中使用组件

修改play/src/App.vue

<!-- play/src/App.vue --> <template> <div class="playground"> <h1>Vue UI Library Playground</h1> <div class="demo-section"> <h2>Button 组件</h2> <div class="button-group"> <VulButton @click="handleClick">默认按钮</VulButton> <VulButton type="primary">主要按钮</VulButton> <VulButton type="success">成功按钮</VulButton> <VulButton type="warning">警告按钮</VulButton> <VulButton type="danger">危险按钮</VulButton> <VulButton type="info">信息按钮</VulButton> </div> <div class="button-group"> <VulButton plain>朴素按钮</VulButton> <VulButton type="primary" plain>主要朴素</VulButton> <VulButton type="success" round>成功圆角</VulButton> <VulButton type="warning" circle>警</VulButton> </div> <div class="button-group"> <VulButton size="large">大型按钮</VulButton> <VulButton size="small">小型按钮</VulButton> <VulButton loading>加载中</VulButton> <VulButton disabled>禁用按钮</VulButton> </div> </div> </div> </template> <script setup lang="ts"> import { Button as VulButton } from '@vue-ui-library/core' const handleClick = (event: MouseEvent) => { console.log('Button clicked!', event) } </script> <style scoped> .playground { padding: 24px; } .demo-section { margin-bottom: 32px; } .button-group { margin-bottom: 16px; } .button-group > * { margin-right: 12px; margin-bottom: 12px; } </style>

play目录下运行pnpm dev,打开浏览器,你就能看到正在开发的 Button 组件,并且可以实时修改packages/core/src/button/Button.vue来观察热更新效果。

5. 实现按需加载与全量导入

一个专业的组件库必须支持两种使用方式:全量导入按需导入。我们已经通过入口文件支持了全量导入。现在来实现按需导入,这能极大优化生产环境的包体积。

5.1 配置构建工具生成按需加载文件

我们需要修改核心包的构建配置,让每个组件都能被单独构建和引入。这通常需要一个插件,但 Vite 的库模式本身支持多入口。我们可以创建一个脚本,或者使用社区成熟的方案。这里我们展示一种基于 Vite 多入口配置的简化思路。

首先,安装一个辅助工具来生成组件入口映射:

# 在根目录 pnpm add -Dw fast-glob

然后,创建一个构建脚本scripts/build.mjs

// scripts/build.mjs import { defineConfig, build } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path' import { fileURLToPath } from 'url' import { readdirSync, statSync, existsSync, mkdirSync, writeFileSync } from 'fs' import { glob } from 'fast-glob' const __dirname = fileURLToPath(new URL('.', import.meta.url)) const rootDir = resolve(__dirname, '..') const componentsDir = resolve(rootDir, 'packages/core/src') // 1. 自动查找所有组件目录 const getComponents = () => { const componentEntries = {} const files = glob.sync('*/index.ts', { cwd: componentsDir, absolute: false }) files.forEach(file => { const componentName = file.split('/')[0] // 入口格式:'button/index.ts' -> 'button' componentEntries[componentName] = resolve(componentsDir, file) }) return componentEntries } // 2. 为每个组件生成独立的构建配置并构建 const buildSingleComponent = async (componentName, entryPath) => { const outDir = resolve(rootDir, `packages/core/dist/${componentName}`) const config = defineConfig({ plugins: [vue()], build: { outDir, lib: { entry: entryPath, name: `Vul${componentName.charAt(0).toUpperCase() + componentName.slice(1)}`, fileName: (format) => `index.${format}.js`, formats: ['es', 'umd'] }, rollupOptions: { external: ['vue'], output: { globals: { vue: 'Vue' }, exports: 'named' } }, emptyOutDir: false, // 避免清空整个 dist } }) await build(config) console.log(`✅ Built component: ${componentName}`) } // 3. 主构建函数 const main = async () => { const components = getComponents() console.log('Found components:', Object.keys(components)) // 并行构建所有组件 const buildPromises = Object.entries(components).map(([name, entry]) => buildSingleComponent(name, entry) ) await Promise.all(buildPromises) // 4. 生成一个用于按需引入的辅助文件 const componentNames = Object.keys(components) const esmEntryContent = componentNames.map(name => `export { default as ${name.charAt(0).toUpperCase() + name.slice(1)} } from './${name}/index.es.js'` ).join('\n') const cjsEntryContent = componentNames.map(name => `module.exports.${name.charAt(0).toUpperCase() + name.slice(1)} = require('./${name}/index.umd.cjs').default` ).join('\n') const distDir = resolve(rootDir, 'packages/core/dist') writeFileSync(resolve(distDir, 'es.js'), esmEntryContent) writeFileSync(resolve(distDir, 'lib.js'), cjsEntryContent) console.log('🎉 All components built successfully!') } main().catch(console.error)

这个脚本会遍历src下的每个组件目录,为每个组件单独执行一次 Vite 构建,并将产物输出到dist/componentName/下。最后生成es.jslib.js作为按需导入的入口。

5.2 修改 package.json 支持按需导入

更新packages/core/package.jsonexports字段:

{ "exports": { ".": { "import": "./dist/vue-ui-library.mjs", "require": "./dist/vue-ui-library.umd.cjs", "types": "./dist/index.d.ts" }, "./es": { "import": "./dist/es.js", "require": "./dist/es.js" }, "./lib": { "import": "./dist/lib.js", "require": "./dist/lib.js" }, "./*": "./*" } }

这样,用户就可以通过以下方式按需引入了:

// ES Modules import { Button } from '@vue-ui-library/core/es' // 或者 CommonJS const { Button } = require('@vue-ui-library/core/lib')

在实际项目中,用户通常会配合 unplugin-vue-components 这类自动导入插件,实现真正的“无感”按需加载,这需要在组件库侧提供对应的解析器。

6. 集成自动化文档 (Vitepress)

优秀的文档是组件库不可或缺的一部分。Vitepress 凭借其与 Vite 的深度集成和 Markdown 中心的设计,成为 Vue 生态中文档站的首选。

6.1 初始化文档项目

docs目录下初始化 Vitepress:

cd docs pnpm add -D vitepress vue # 初始化 npx vitepress init

按照提示选择主题、是否启用搜索等。完成后,修改docs/.vitepress/config.ts

// docs/.vitepress/config.ts import { defineConfig } from 'vitepress' import { resolve } from 'path' export default defineConfig({ title: 'Vue UI Library', description: 'A Vue 3 UI Component Library.', themeConfig: { nav: [ { text: '指南', link: '/guide/' }, { text: '组件', link: '/components/button' } ], sidebar: { '/guide/': [ { text: '介绍', items: [ { text: '快速开始', link: '/guide/' }, { text: '安装', link: '/guide/installation' } ] } ], '/components/': [ { text: '基础组件', items: [ { text: 'Button 按钮', link: '/components/button' } ] } ] } }, vite: { resolve: { alias: { '@vue-ui-library/core': resolve(__dirname, '../../packages/core/src/index.ts'), } } } })

6.2 编写组件文档页

创建docs/components/button.md

--- title: Button 按钮 --- # Button 按钮 常用的操作按钮。 ## 基础用法 使用 `type`、`size`、`plain`、`round`、`circle` 属性来定义按钮的样式。 <demo src="./demo/button-basic.vue" /> ::: details 查看代码 <<< @/components/demo/button-basic.vue ::: ## 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 |

创建对应的演示组件docs/components/demo/button-basic.vue

<!-- docs/components/demo/button-basic.vue --> <template> <div class="demo-button"> <div style="margin-bottom: 16px;"> <VulButton>默认按钮</VulButton> <VulButton type="primary">主要按钮</VulButton> <VulButton type="success">成功按钮</VulButton> <VulButton type="warning">警告按钮</VulButton> <VulButton type="danger">危险按钮</VulButton> </div> <div style="margin-bottom: 16px;"> <VulButton plain>朴素按钮</VulButton> <VulButton type="primary" plain>主要朴素</VulButton> <VulButton type="success" round>成功圆角</VulButton> <VulButton type="warning" circle>警</VulButton> </div> </div> </template> <script setup> import { Button as VulButton } from '@vue-ui-library/core' </script>

docs目录下运行pnpm docs:dev,一个实时热更新的组件文档站就运行起来了。你可以看到组件的实时演示和详细的 API 文档。

7. 添加单元测试 (Vitest)

测试是保障组件库质量的生命线。我们使用 Vitest,因为它与 Vite 配置共享,速度极快。

7.1 安装与配置

packages/core目录下安装 Vitest 和测试工具:

cd packages/core pnpm add -D vitest @vue/test-utils @vitest/ui happy-dom

创建packages/core/vitest.config.ts

// packages/core/vitest.config.ts import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' import { resolve } from 'path' export default defineConfig({ plugins: [vue()], test: { environment: 'happy-dom', // 模拟浏览器环境 globals: true, // 启用全局 API,如 describe, it, expect }, resolve: { alias: { '@': resolve(__dirname, 'src'), }, }, })

7.2 编写第一个组件测试

创建packages/core/src/button/__tests__/Button.spec.ts

// packages/core/src/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('vul-button') expect(wrapper.classes()).toContain('vul-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 event when disabled', async () => { const wrapper = mount(Button, { props: { disabled: true } }) await wrapper.trigger('click') expect(wrapper.emitted()).not.toHaveProperty('click') }) it('applies correct type class', () => { const wrapper = mount(Button, { props: { type: 'primary' } }) expect(wrapper.classes()).toContain('vul-button--primary') }) it('shows loading state', () => { const wrapper = mount(Button, { props: { loading: true } }) expect(wrapper.classes()).toContain('is-loading') expect(wrapper.find('.vul-button__loading').exists()).toBe(true) }) })

packages/core/package.json中添加测试脚本:

{ "scripts": { "test": "vitest", "test:ui": "vitest --ui" } }

运行pnpm test执行测试,或pnpm test:ui打开图形化界面查看测试结果和覆盖率。通过编写全面的测试用例,可以确保组件在各种状态和交互下的行为符合预期。

8. 常见问题与排查思路

在搭建和使用组件库的过程中,你一定会遇到各种问题。以下是一些典型问题及其解决方案:

问题现象可能原因排查方式解决方案
构建失败,提示Vue未找到1.peerDependencies未正确声明。
2.rollupOptions.external未配置或配置错误。
1. 检查package.jsonpeerDependencies
2. 检查vite.config.tsexternal数组是否包含'vue'
1. 确保peerDependencies包含"vue": "^3.4.0"
2. 确保构建配置中external: ['vue']
在 Playground 中组件样式丢失1. 组件样式文件未导入。
2. Vite 别名配置错误,导致源码路径解析失败。
1. 检查组件是否在src/index.ts中正确导出。
2. 检查 Playground 的vite.config.ts别名路径是否正确指向源码。
1. 确保组件样式通过<style scoped>或单独导入。
2. 确认别名路径resolve(__dirname, '../packages/core/src/index.ts')存在。
TypeScript 报错:找不到模块声明1. 未生成.d.ts类型声明文件。
2.package.jsontypes字段指向错误。
1. 运行构建后检查dist目录下是否有.d.ts文件。
2. 检查package.jsontypes字段是否为"./dist/index.d.ts"
1. 确保vite-plugin-dts插件正确配置并运行。
2. 确保主入口文件src/index.ts导出了所有类型。
按需引入时 Tree Shaking 不生效1. 组件库未配置sideEffects
2. 用户项目构建工具配置问题。
1. 检查库的package.json是否设置了"sideEffects": false
2. 确认组件是 ES Module 格式导出。
1. 在package.json中添加"sideEffects": false
2. 确保构建产物包含 ES Module 格式 (format: 'es')。
文档站中组件无法渲染1. Vitepress 未正确配置 Vue 插件。
2. 文档中导入路径错误。
1. 检查.vitepress/theme/index.ts是否注册了组件库。
2. 检查文档 Markdown 中导入语句的路径。
1. 在 Vitepress 主题入口文件中使用app.use(YourLib)
2. 使用 Vitepress 的vite配置项设置别名,确保路径正确。
发布到 npm 后安装报错1. 依赖未正确声明 (dependenciesvspeerDependencies)。
2. 文件未包含在files字段中。
1. 检查package.jsondependenciesdevDependenciespeerDependencies
2. 运行npm pack查看将要发布的文件列表。
1. 将vue等宿主环境依赖放入peerDependencies
2. 确保files字段包含了所有需要发布的文件(如dist,README.md)。

9. 最佳实践与工程建议

走到这一步,一个组件库的雏形已经完成。但要将其用于生产或团队协作,还需要遵循一些最佳实践:

  1. 版本管理与发布自动化

    • 使用changesetsstandard-version管理版本号和生成 CHANGELOG。
    • 在 CI/CD 中自动化执行测试、构建和发布到 npm 的流程。
    • 遵循语义化版本控制 (SemVer)。
  2. 样式系统设计

    • CSS 变量:使用 CSS 自定义属性定义主题色、间距、字体等,便于实现动态主题和暗黑模式。
    • 设计令牌:将颜色、间距、阴影等视觉元素抽象为设计令牌,并在所有组件中统一引用。
    • 样式隔离:坚持使用scopedCSS 或 CSS Modules,避免全局样式污染。
  3. 组件设计原则

    • 单一职责:一个组件只做一件事。
    • 受控 vs 非受控:明确组件的状态管理方式,优先设计为“受控组件”。
    • 复合组件:对于复杂组件(如 Select、Table),使用provide/inject或 Composition API 实现子组件间的通信。
    • 无障碍访问:为交互组件添加必要的 ARIA 属性,确保键盘导航和屏幕阅读器支持。
  4. 代码质量与规范

    • 集成 ESLint + Prettier 统一代码风格。
    • 使用 Husky + lint-staged 在提交前自动检查和修复代码。
    • 为所有公共 API 编写详细的 JSDoc/TSDoc 注释。
  5. 性能优化

    • 虚拟滚动:为长列表组件(如 Table、Select)实现虚拟滚动。
    • 懒加载:对于非首屏必需的组件或图标,支持动态导入。
    • 构建优化:利用 Vite/Rollup 的代码分割、Tree Shaking 能力。

从零开始搭建一个 UI 组件库,远不止是写几个.vue文件。它是一套完整的工程化体系,涉及项目架构、构建打包、类型系统、文档化、测试和发布流程。通过本次实践,你不仅学会了如何创建一个 Button 组件,更重要的是掌握了构建一个可维护、可扩展的前端库的完整方法论。

下一步,你可以尝试:

  • 添加更多基础组件(Input、Select、Modal 等)。
  • 实现一个完整的主题切换系统。
  • 集成图标库(如 Iconify)。
  • 编写 E2E 测试(使用 Cypress 或 Playwright)。
  • 搭建完整的 CI/CD 流水线,实现自动化测试、构建和发布。

这套架构和工具链是当前 Vue 3 生态下的主流选择,掌握了它,你就能从容应对任何定制化组件开发的需求,也能更深入地理解社区中那些优秀开源库的设计精髓。建议你将这个项目作为模板收藏,在未来的开发中不断迭代和丰富。

← 返回列表