Vue3生态下组件库设计是前端工程化的核心环节。一套设计良好的业务组件库能提升团队开发效率、保证UI一致性、降低维护成本。本文从组件架构设计到工程化构建,完整演示Vue3组件库的搭建流程,涵盖Composables复用、响应式布局方案和构建发布配置。
组件库架构设计:分层设计与目录结构规划
组件库采用三层架构:基础组件层(Base Components)、业务组件层(Business Components)、布局组件层(Layout Components)。各层职责清晰,依赖方向单一:
# 项目目录结构
vue3-ui-library/
├── packages/
│ ├── base/ # 基础组件
│ │ ├── button/
│ │ │ ├── src/Button.vue
│ │ │ ├── style/index.scss
│ │ │ └── index.ts
│ │ └── input/
│ ├── business/ # 业务组件
│ │ ├── search-form/
│ │ └── data-table/
│ ├── layout/ # 布局组件
│ │ ├── grid/
│ │ └── container/
│ └── shared/ # 共享工具
│ ├── composables/
│ │ ├── useResponsive.ts
│ │ ├── useTheme.ts
│ │ └── useForm.ts
│ └── utils/
├── play/ # 组件调试环境
├── docs/ # 文档站点
└── vite.config.ts
基础组件实现:Button组件的设计模式
以Button组件为例,展示Vue3组件的标准实现模式。利用defineProps、defineEmits和CSS变量实现主题定制:
<!-- packages/base/button/src/Button.vue -->
<template>
<button
:class="[ns.b(), ns.m(type), ns.m(size),
ns.is('disabled', disabled), ns.is('loading', loading)]"
:disabled="disabled || loading"
@click="handleClick"
>
<span v-if="loading" class="btn-loading-icon" />
<slot name="icon" />
<span class="btn-content"><slot /></span>
</button>
</template>
<script setup lang="ts">
import { useNamespace } from '../../../shared/composables/useNamespace'
type ButtonType = 'default' | 'primary' | 'success' | 'warning' | 'danger'
type ButtonSize = 'small' | 'medium' | 'large'
const props = withDefaults(defineProps<{
type?: ButtonType
size?: ButtonSize
disabled?: boolean
loading?: boolean
}>(), {
type: 'default',
size: 'medium',
disabled: false,
loading: false,
})
const emit = defineEmits<{
(e: 'click', event: MouseEvent): void
}>()
const ns = useNamespace('btn')
const handleClick = (event: MouseEvent) => {
if (props.disabled || props.loading) return
emit('click', event)
}
</script>
<style scoped>
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border: 1px solid var(--btn-border-color, #dcdfe6);
border-radius: var(--btn-border-radius, 4px);
cursor: pointer;
transition: all 0.2s ease;
padding: 8px 16px;
}
.btn--primary {
background: var(--color-primary, #409eff);
border-color: var(--color-primary, #409eff);
color: #fff;
}
.btn--small { padding: 5px 11px; font-size: 12px; }
.btn--large { padding: 12px 22px; font-size: 16px; }
.btn.is-disabled, .btn.is-loading { cursor: not-allowed; opacity: 0.5; }
</style>
useNamespace是BEM命名空间的Composable,统一管理组件class命名规范:
// packages/shared/composables/useNamespace.ts
const statePrefix = 'is-'
const _bem = (namespace, block, blockSuffix, element, modifier) => {
let cls = `${namespace}-${block}`
if (blockSuffix) cls += `-${blockSuffix}`
if (element) cls += `__${element}`
if (modifier) cls += `--${modifier}`
return cls
}
export const useNamespace = (block, namespaceOverride) => {
const namespace = namespaceOverride || 'vue3-ui'
const b = (blockSuffix = '') => _bem(namespace, block, blockSuffix, '', '')
const e = (element) => element ? _bem(namespace, block, '', element, '') : ''
const m = (modifier) => modifier ? _bem(namespace, block, '', '', modifier) : ''
const is = (name, state) => name && state ? `${statePrefix}${name}` : ''
return { b, e, m, is }
}
响应式布局方案:CSS Grid与断点系统设计
组件库需要内置响应式布局能力。通过CSS Grid和媒体查询断点系统,实现自适应栅格布局:
<!-- packages/layout/grid/src/Row.vue -->
<template>
<div :class="rowClass" :style="rowStyle">
<slot />
</div>
</template>
<script setup lang="ts">
import { computed, provide, reactive } from 'vue'
const props = withDefaults(defineProps<{
gutter?: number
justify?: 'start' | 'end' | 'center' | 'space-around' | 'space-between'
align?: 'top' | 'middle' | 'bottom'
}>(), { gutter: 0, justify: 'start', align: 'top' })
provide('rowContext', reactive({
gutter: computed(() => props.gutter),
}))
const rowClass = computed(() => [
'vue3-ui-row', `is-justify-${props.justify}`, `is-align-${props.align}`
])
const rowStyle = computed(() => ({
marginLeft: `-${props.gutter / 2}px`,
marginRight: `-${props.gutter / 2}px`,
}))
</script>
<!-- packages/layout/grid/src/Col.vue -->
<template>
<div :class="colClass" :style="colStyle">
<slot />
</div>
</template>
<script setup lang="ts">
import { computed, inject } from 'vue'
const props = withDefaults(defineProps<{
span?: number
offset?: number
xs?: number | { span?: number; offset?: number }
sm?: number | { span?: number; offset?: number }
md?: number | { span?: number; offset?: number }
lg?: number | { span?: number; offset?: number }
xl?: number | { span?: number; offset?: number }
}>(), { span: 24, offset: 0 })
const rowContext = inject('rowContext', { gutter: computed(() => 0) })
const colClass = computed(() => {
const classes = ['vue3-ui-col', `vue3-ui-col-${props.span}`]
if (props.offset) classes.push(`vue3-ui-col-offset-${props.offset}`)
const bps = ['xs', 'sm', 'md', 'lg', 'xl']
bps.forEach(bp => {
const val = props[bp]
if (typeof val === 'number') classes.push(`vue3-ui-col-${bp}-${val}`)
else if (val && typeof val === 'object') {
if (val.span) classes.push(`vue3-ui-col-${bp}-${val.span}`)
if (val.offset) classes.push(`vue3-ui-col-${bp}-offset-${val.offset}`)
}
})
return classes
})
const colStyle = computed(() => ({
paddingLeft: `${rowContext.gutter.value / 2}px`,
paddingRight: `${rowContext.gutter.value / 2}px`,
}))
</script>
对应的SCSS断点定义生成24栅格响应式类:
// packages/layout/grid/style/index.scss
$breakpoints: (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px);
@mixin respond-to($bp) {
$value: map-get($breakpoints, $bp);
@if $value == 0 { @content; }
@else { @media (min-width: $value) { @content; } }
}
@each $bp, $value in $breakpoints {
@for $i from 0 through 24 {
@include respond-to($bp) {
.vue3-ui-col-#{$bp}-#{$i} {
max-width: percentage($i / 24);
flex: 0 0 percentage($i / 24);
}
}
}
}
Composables复用:表单逻辑抽象与状态管理
Vue3的Composition API让逻辑复用变得简单。表单验证是高频场景,抽象为useFormComposable:
// packages/shared/composables/useForm.ts
import { reactive, computed } from 'vue'
export function useForm(initialValues, rules) {
const values = reactive({ ...initialValues })
const errors = reactive({})
const validateField = async (field) => {
const fieldRules = rules[field]
if (!fieldRules) return true
const value = values[field]
for (const rule of fieldRules) {
if (rule.required && !value) {
errors[field] = rule.message
return false
}
if (rule.min && value.length < rule.min) {
errors[field] = rule.message
return false
}
if (rule.pattern && !rule.pattern.test(value)) {
errors[field] = rule.message
return false
}
if (rule.validator) {
const result = rule.validator(value)
if (result !== true) {
errors[field] = typeof result === 'string' ? result : rule.message
return false
}
}
}
delete errors[field]
return true
}
const validate = async () => {
let valid = true
for (const field in rules) {
if (!await validateField(field)) valid = false
}
return valid
}
const isValid = computed(() => Object.keys(errors).length === 0)
return { values, errors, validateField, validate, isValid }
}
// 使用示例
const { values, errors, validate } = useForm(
{ username: '', email: '', password: '' },
{
username: [
{ required: true, message: '用户名不能为空' },
{ min: 3, max: 20, message: '用户名长度3-20位' },
],
email: [
{ required: true, message: '邮箱不能为空' },
{ pattern: /^\S+@\S+\.\S+$/, message: '邮箱格式不正确' },
],
}
)
工程化构建配置:Vite打包与按需引入
组件库需要支持全量引入和按需引入两种模式。Vite的库模式配合preserveModules可以实现按需加载:
// vite.config.ts
import { resolve } from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dts from 'vite-plugin-dts'
export default defineConfig({
plugins: [vue(), dts({ entryRoot: 'packages', outDir: 'dist/types' })],
build: {
lib: {
entry: resolve(__dirname, 'packages/index.ts'),
name: 'Vue3UI',
fileName: (format) => `vue3-ui.${format}.js`,
formats: ['es', 'cjs', 'umd'],
},
rollupOptions: {
external: ['vue'],
output: {
preserveModules: true,
preserveModulesRoot: 'packages',
exports: 'named',
globals: { vue: 'Vue' },
},
},
cssCodeSplit: true,
},
})
// packages/index.ts
export * from './base/button'
export * from './base/input'
export * from './layout/grid'
export * from './shared/composables/useForm'
import type { App } from 'vue'
import Button from './base/button'
import Input from './base/input'
export default {
install(app: App) {
[Button, Input].forEach(c => app.component(c.name, c))
},
}
构建完成后,消费端可以通过unplugin-vue-components实现自动按需导入,无需手动import。配合TypeScript类型声明文件,组件库的使用体验与原生Vue组件无异。跨端小程序开发场景下,通过条件编译可以复用大部分组件逻辑层代码,仅替换渲染层实现即可。TypeScript实战中,组件Props类型应导出为独立接口,便于消费端进行类型推断和扩展。Web性能优化方面,组件库支持Tree-Shaking,按需引入的产物体积可控制在合理范围内。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-jian-ku-she-ji-shi-zhan-cong-ling-da-jian-xiang/