组件库项目架构怎么搭
Vue3组件库不是简单的组件堆叠,从目录结构到构建工具链再到发布流程,每一步都影响后续维护和接入成本。一个合理的项目架构需要同时满足开发体验和生产性能——开发时热更新快、类型提示全,生产时Tree-shaking干净、产物体积小。
推荐的Monorepo结构:
my-ui/
├── packages/
│ ├── components/ # 组件源码
│ │ ├── button/
│ │ │ ├── src/
│ │ │ │ ├── Button.vue
│ │ │ │ └── Button.ts # 组件入口
│ │ │ └── index.ts
│ │ ├── input/
│ │ └── index.ts # 组件统一导出
│ ├── theme/ # 样式系统
│ │ ├── src/
│ │ │ ├── var.scss # CSS变量
│ │ │ └── button.scss
│ │ └── index.scss
│ └── utils/ # 工具函数
├── docs/ # 文档站
├── playground/ # 调试沙盒
├── package.json
├── pnpm-workspace.yaml
├── tsconfig.json
└── vite.config.ts
pnpm-workspace.yaml:
packages:
- 'packages/*'
- 'docs'
- 'playground'
组件设计与TypeScript类型定义
组件设计的第一步是定义Props类型。以Button组件为例,用TypeScript的类型系统把API约束清楚:
// packages/components/button/src/Button.ts
import type { ExtractPropTypes, PropType } from 'vue'
export type ButtonType = 'primary' | 'success' | 'warning' | 'danger' | 'info'
export type ButtonSize = 'large' | 'default' | 'small'
export const buttonProps = {
type: {
type: String as PropType<ButtonType>,
default: 'primary',
},
size: {
type: String as PropType<ButtonSize>,
default: 'default',
},
disabled: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
icon: {
type: String,
default: '',
},
} as const
export type ButtonProps = ExtractPropTypes<typeof buttonProps>
组件实现:
<template>
<button
:class="[
'my-button',
`my-button--${type}`,
`my-button--${size}`,
{
'is-disabled': disabled,
'is-loading': loading,
}
]"
:disabled="disabled || loading"
@click="handleClick"
>
<span v-if="loading" class="my-button__loading">
<slot name="loading">⏳</slot>
</span>
<span class="my-button__content">
<slot />
</span>
</button>
</template>
<script setup lang="ts">
import { buttonProps } from './Button'
defineOptions({ name: 'MyButton' })
const props = defineProps(buttonProps)
const emit = defineEmits<{ click: [e: MouseEvent] }>()
const handleClick = (e: MouseEvent) => {
if (props.disabled || props.loading) return
emit('click', e)
}
</script>
组件注册与按需导出
每个组件单独导出,同时提供全量注册的插件入口。这是Tree-shaking的基础:
// packages/components/button/index.ts
import { withInstall } from '@my-ui/utils'
import Button from './src/Button.vue'
export const MyButton = withInstall(Button)
export default MyButton
// packages/components/index.ts
export { MyButton } from './button'
export { MyInput } from './input'
// ...其他组件
withInstall工具函数给组件添加install方法:
// packages/utils/with-install.ts
import type { Plugin, App, Component } from 'vue'
type SFCWithInstall<T> = T & Plugin
export function withInstall<T extends Component>(component: T): SFCWithInstall<T> {
const comp = component as SFCWithInstall<T>
comp.install = (app: App) => {
app.component(comp.name!, comp)
}
return comp
}
Vite构建配置与Tree-shaking优化
构建产物需要同时支持ESM和UMD两种格式。ESM供现代打包工具做Tree-shaking,UMD供CDN直接引入。Vite的rollup配置:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
build: {
lib: {
entry: resolve(__dirname, 'packages/components/index.ts'),
name: 'MyUI',
formats: ['es', 'umd'],
fileName: (format) => `my-ui.${format}.js`,
},
rollupOptions: {
external: ['vue'],
output: {
globals: { vue: 'Vue' },
// ESM格式保留模块结构,支持Tree-shaking
exports: 'named',
// 手动拆分每个组件为独立chunk
manualChunks(id) {
if (id.includes('packages/components')) {
const match = id.match(/components\/([^/]+)/)
if (match) return `my-ui-${match[1]}`
}
},
},
},
},
})
关键点:manualChunks把每个组件拆成独立文件,用ESM格式的用户只需要导入的组件代码,不会把整个库拉进来。
CSS变量主题系统
样式隔离用CSS变量方案,不依赖运行时JS切换,性能最好:
/* packages/theme/src/var.scss */
:root {
--my-color-primary: #409eff;
--my-color-success: #67c23a;
--my-color-warning: #e6a23c;
--my-color-danger: #f56c6c;
--my-color-info: #909399;
--my-font-size-large: 16px;
--my-font-size-default: 14px;
--my-font-size-small: 12px;
--my-border-radius: 4px;
}
/* 暗色主题 */
:root[data-theme='dark'] {
--my-color-primary: #3a8ee6;
--my-color-success: #5daf34;
--my-color-warning: #cf9236;
--my-color-danger: #dd5a5a;
}
Button样式使用变量:
/* packages/theme/src/button.scss */
.my-button {
display: inline-flex;
align-items: center;
padding: 8px 16px;
font-size: var(--my-font-size-default);
border-radius: var(--my-border-radius);
border: 1px solid var(--my-color-primary);
background: var(--my-color-primary);
color: #fff;
cursor: pointer;
transition: all 0.2s;
&--large { padding: 10px 20px; font-size: var(--my-font-size-large); }
&--small { padding: 6px 12px; font-size: var(--my-font-size-small); }
&.is-disabled { opacity: 0.6; cursor: not-allowed; }
&.is-loading { opacity: 0.7; cursor: wait; }
}
组件库发布与版本管理
发布到npm前,package.json的入口配置必须正确:
{
"name": "@my-ui/components",
"version": "1.0.0",
"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"
},
"./button": {
"import": "./dist/my-ui-button.es.js",
"types": "./dist/types/button/index.d.ts"
}
},
"sideEffects": ["**/*.scss", "**/*.css"]
}
sideEffects字段告诉打包工具:除了样式文件外,其他模块都是纯函数,可以安全Tree-shaking。exports字段支持import { MyButton } from '@my-ui/components/button'这样的按需导入,Vite/Webpack会自动处理。
类型声明生成用vue-tsc:
vue-tsc --declaration --emitDeclarationOnly --outDir dist/types
这样用户在TS项目中使用组件库就能获得完整的类型提示和自动补全,开发体验和用Element Plus一样顺滑。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-jian-ku-she-ji-shi-zhan-cong-jia-gou-da-jian-dao/