当项目里出现多个重复的按钮、弹窗和表单组件时,把它们沉淀为独立的 Vue 3 组件库是降低维护成本的有效手段。但组件库开发与业务页面开发差异很大,不仅要考虑组件的 API 设计,还要处理构建产物、类型文件、样式导出和文档演示。本文会从初始化工程开始,逐步搭建一个可发布的 Vue 3 组件库,涵盖 Vite 库模式配置、组件开发、类型生成和文档发布。

一、初始化 monorepo 工程与目录结构
组件库通常需要同时维护组件源码、文档站点和示例项目,采用 monorepo 结构能让这些模块共享依赖、统一管理版本。pnpm 的 workspace 协议非常适合这种场景,它通过软链接复用 node_modules,节省磁盘空间,也避免了 npm 或 yarn 常见的依赖提升问题。下面先创建根目录并初始化 package.json。
mkdir vue3-component-lib cd vue3-component-lib pnpm init mkdir packages mkdir packages/components mkdir packages/docs
在根目录创建 pnpm-workspace.yaml,声明包含哪些子包。然后在根 package.json 中添加脚本和公共开发依赖。配置如下:
packages: - "packages/*"
{
"name": "vue3-component-lib",
"private": true,
"scripts": {
"dev": "pnpm --filter docs dev",
"build": "pnpm --filter components build",
"docs:build": "pnpm --filter docs build"
},
"devDependencies": {
"typescript": "^5.4.0",
"vite": "^5.2.0",
"vue": "^3.4.0"
}
}
其中 components 子包存放组件源码和构建配置,docs 子包用于 VitePress 文档。每个子包也需要自己的 package.json,在 components 包中声明组件库的名称、版本和入口文件。这种隔离方式可以让组件库独立发布,文档站点只做为开发辅助。
二、配置 Vite 库模式打包组件
Vite 从 2.x 开始内置了库模式,通过 build.lib 选项可以快速生成 ES 和 CommonJS 两种格式的产物。对于 Vue 3 组件库,需要将 Vue 及其相关依赖外部化,避免重复打包导致运行时冲突。同时要处理组件样式的导出,否则用户使用时会出现样式丢失。
在 packages/components 下创建 vite.config.ts,基本配置如下:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dts from 'vite-plugin-dts'
export default defineConfig({
plugins: [
vue(),
dts({ insertTypesEntry: true })
],
build: {
lib: {
entry: 'src/index.ts',
name: 'Vue3ComponentLib',
fileName: (format) => `vue3-component-lib.${format}.js`,
formats: ['es', 'cjs']
},
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue'
},
assetFileNames: (assetInfo) => {
if (assetInfo.name === 'style.css') return 'index.css'
return assetInfo.name
}
}
},
cssCodeSplit: false
}
})
cssCodeSplit 设置为 false 会将所有 CSS 合并到一个文件,并命名为 index.css,这能简化样式的导入。dts 插件用于生成 .d.ts 类型声明,并通过 insertTypesEntry 自动在 package.json 中插入 types 字段。接着配置 components 包的 package.json,明确入口和导出映射。
{
"name": "vue3-component-lib",
"version": "1.0.0",
"main": "./dist/vue3-component-lib.cjs.js",
"module": "./dist/vue3-component-lib.es.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/vue3-component-lib.es.js",
"require": "./dist/vue3-component-lib.cjs.js"
},
"./style.css": "./dist/index.css"
},
"files": ["dist"],
"peerDependencies": {
"vue": "^3.4.0"
},
"sideEffects": ["**/*.css"]
}
exports 字段让现代打包工具能正确解析类型、ES 和 CJS 产物,./style.css 子路径方便用户按需引入样式。sideEffects 标记 CSS 文件具有副作用,防止 tree shaking 把样式摇掉。
三、开发第一个组件并生成类型声明
以一个最常用的 Button 组件为例,说明组件库中的组件应该怎样编写。组件库组件与业务组件最大的区别在于 API 的稳定性和透传支持,因此需要仔细定义 props、emits 和插槽。下面使用 script setup 语法实现一个简洁的按钮。
<template>
<button
class="v-button"
:class="[`v-button--${type}`, { 'is-disabled': disabled }]"
:disabled="disabled"
@click="handleClick"
>
<slot></slot>
</button>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
type ButtonType = 'default' | 'primary' | 'danger'
interface Props {
type?: ButtonType
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
type: 'default',
disabled: false
})
const emit = defineEmits<{
(e: 'click', event: MouseEvent): void
}>()
function handleClick(event: MouseEvent) {
if (!props.disabled) {
emit('click', event)
}
}
</script>
<style scoped>
.v-button {
padding: 8px 16px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
}
.v-button--primary {
background-color: #409eff;
color: white;
border-color: #409eff;
}
.is-disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
这个 Button 组件使用了 withDefaults 为 props 提供默认值,并在点击时判断 disabled 状态,避免禁用时触发事件。scoped 样式只会作用于当前组件,打包后这些样式会通过 Vite 的 CSS 处理机制提取到独立的 index.css 中。
然后需要创建组件库的入口文件 src/index.ts,统一导出所有组件和类型声明。这有利于用户按需引入或全量引入。
import Button from './components/Button.vue'
import type { App } from 'vue'
export { Button }
export default {
install(app: App) {
app.component(Button.name as string, Button)
}
}
入口文件同时支持具名导出和默认导出,默认导出实现了 Vue 插件的 install 方法,这样用户可以通过 app.use(ComponentLib) 全局注册。为了生成完整的类型声明,项目中安装了 vite-plugin-dts,它会在构建时分析源码并输出 .d.ts 文件。生成的文件位于 dist 目录下,package.json 中的 types 字段会指向 dist/index.d.ts。
四、用 VitePress 生成文档并发布到 npm
文档是组件库能否被团队采用的关键。VitePress 基于 Vite 构建,天然支持 Vue 3 组件,可以直接在 Markdown 文件中演示组件。先在 packages/docs 目录初始化 VitePress,并创建文档页面。
docs 目录下创建 .vitepress/config.js 配置导航和侧边栏:
import { defineConfig } from 'vitepress'
export default defineConfig({
title: 'Vue3 Component Lib',
description: 'A minimal Vue 3 component library',
themeConfig: {
nav: [
{ text: '指南', link: '/guide/' },
{ text: '组件', link: '/components/' }
],
sidebar: {
'/guide/': [
{ text: '快速开始', link: '/guide/getting-started' }
],
'/components/': [
{ text: 'Button 按钮', link: '/components/button' }
]
}
}
})
然后在 Markdown 文件中使用组件,需要先导入组件库。给出一个示例 markdown 文件内容:
<script setup>
import { Button } from 'vue3-component-lib'
import 'vue3-component-lib/style.css'
</script>
# Button 按钮
<Button type="primary">主要按钮</Button>
在 VitePress 的 Markdown 中可以直接使用 Vue 组件,但需要引入组件库的样式文件。文档站点的构建配置中通过别名将组件库指向源码,方便调试。之后可以运行 pnpm --filter docs dev 启动本地文档预览。
最后是发布到 npm。在发布前需要确认 components 包的 package.json 中已经声明了 peerDependencies 和 files 字段,确保只发布 dist 目录。执行 npm login 后,在 components 目录运行 pnpm build 和 npm publish 即可。如果包名带有作用域,需要添加 --access public 参数。
常见坑点有两个:一是样式丢失,多数原因是 sideEffects 字段未配置或用户没有手动导入 style.css,可以在文档中明确告知导入方式,或者提供自动按需引入的插件。二是 Vue 版本冲突,如果组件库将 Vue 打包进产物,会导致多个 Vue 实例共存,所以必须在 rollupOptions.external 中外部化 vue,同时在 peerDependencies 中声明正确的版本范围。
通过以上步骤,一个基础但完整的 Vue 3 组件库就搭建完成了。后续可以继续扩展组件、完善测试、加入 CI/CD 自动发布,但核心的构建链路已经清晰。