企业级Vue3组件库的架构分层设计
Vue3组件库不是把组件写完丢进一个仓库就完事了。一个可持续维护的企业级组件库,至少需要三层架构:基础层、业务层和文档层。基础层提供原子化组件和设计Token,业务层基于基础层组合出业务组件,文档层提供可交互的组件示例和API文档。
目录结构设计:
packages/
components/ # 基础组件
button/
input/
select/
...
composables/ # 组合式函数
useResponsive/
useBreakpoint/
theme/ # 设计Token
tokens.css
dark.css
utils/ # 工具函数
business/ # 业务组件
search-form/
data-table/
docs/ # 文档站点
playground/ # 开发调试
internal/ # 构建/发布工具
每层独立打包,按需引用。不要把所有组件打进一个bundle,用户只引入Button就不该加载Table的代码。
组件设计与TypeScript类型约束
组件的Props定义是组件库的API契约,必须用TypeScript严格约束。一个Button组件的完整类型定义:
// types.ts
export type ButtonSize = 'small' | 'medium' | 'large'
export type ButtonType = 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
export type ButtonHTMLType = 'button' | 'submit' | 'reset'
export interface ButtonProps {
type?: ButtonType
size?: ButtonSize
disabled?: boolean
loading?: boolean
htmlType?: ButtonHTMLType
block?: boolean
icon?: Component
href?: string // 渲染为a标签时使用
target?: string
}
export interface ButtonEmits {
(e: 'click', event: MouseEvent): void
}
组件实现中用defineProps和defineEmits约束输入输出:
<script setup lang="ts">
import { computed } from 'vue'
import type { ButtonProps, ButtonEmits } from './types'
import { useNamespace } from '@lib/composables/useNamespace'
const props = withDefaults(defineProps<ButtonProps>(), {
type: 'secondary',
size: 'medium',
disabled: false,
loading: false,
block: false,
})
const emit = defineEmits<ButtonEmits>()
const ns = useNamespace('button')
const classes = computed(() => [
ns.b(),
ns.m(props.type),
ns.m(props.size),
ns.is('disabled', props.disabled),
ns.is('loading', props.loading),
ns.is('block', props.block),
])
const handleClick = (e: MouseEvent) => {
if (props.disabled || props.loading) return
emit('click', e)
}
</script>
useNamespace是BEM命名规范的封装,生成规范的CSS类名:
// composables/useNamespace.ts
export function useNamespace(block: string) {
const prefix = 'my'
const b = () => `${prefix}-${block}`
const m = (modifier: string) => `${prefix}-${block}--${modifier}`
const e = (element: string) => `${prefix}-${block}__${element}`
const is = (state: string, active: boolean) =>
active ? `${prefix}-${block}-is-${state}` : ''
return { b, m, e, is }
}
响应式布局的工程化方案
响应式布局的核心不是写几条media query,而是建立一套从设计到代码的完整映射体系。
断点系统:用CSS自定义属性定义断点,配合Vue3组合式函数在JS中同步使用:
// composables/useBreakpoint.ts
import { ref, computed, onMounted, onUnmounted } from 'vue'
const breakpoints = {
xs: 0,
sm: 576,
md: 768,
lg: 992,
xl: 1200,
xxl: 1600,
} as const
export type Breakpoint = keyof typeof breakpoints
export function useBreakpoint() {
const current = ref<Breakpoint>('md')
const width = ref(window.innerWidth)
const update = () => {
width.value = window.innerWidth
const w = width.value
if (w >= breakpoints.xxl) current.value = 'xxl'
else if (w >= breakpoints.xl) current.value = 'xl'
else if (w >= breakpoints.lg) current.value = 'lg'
else if (w >= breakpoints.md) current.value = 'md'
else if (w >= breakpoints.sm) current.value = 'sm'
else current.value = 'xs'
}
onMounted(() => {
update()
window.addEventListener('resize', update)
})
onUnmounted(() => window.removeEventListener('resize', update))
const isMobile = computed(() => ['xs', 'sm'].includes(current.value))
const isDesktop = computed(() => !isMobile.value)
return { current, width, isMobile, isDesktop }
}
栅格组件:Row和Col组件是响应式布局的基础设施。Col组件需要支持不同断点下的span值:
<Row :gutter="{ xs: 8, sm: 16, md: 24 }">
<Col :xs="24" :sm="12" :md="8" :lg="6">内容A</Col>
<Col :xs="24" :sm="12" :md="8" :lg="6">内容B</Col>
<Col :xs="24" :sm="12" :md="8" :lg="6">内容C</Col>
<Col :xs="24" :sm="12" :md="8" :lg="6">内容D</Col>
</Row>
Col组件的CSS使用media query动态计算宽度:
.my-col {
box-sizing: border-box;
width: 100%;
}
/* 24栅格系统,每个span = 100/24 = 4.167% */
@each $span, $i in (1..24) {
.my-col--span-$(i) {
flex: 0 0 percentage($i / 24);
max-width: percentage($i / 24);
}
}
/* 响应式覆盖 */
@media (min-width: 576px) {
.my-col-sm--span-12 { flex: 0 0 50%; max-width: 50%; }
}
@media (min-width: 768px) {
.my-col-md--span-8 { flex: 0 0 33.333%; max-width: 33.333%; }
}
组件库的按需加载与Tree-Shaking配置
组件库必须支持按需引用,否则一个只用了3个组件的项目也会加载全部代码。
Vite环境下的按需引用配置:
// vite.config.ts
import Components from 'unplugin-vue-components/vite'
import { MyLibResolver } from 'my-lib/resolver'
export default defineConfig({
plugins: [
Components({
resolvers: [MyLibResolver()],
}),
],
})
Resolver的实现核心是约定组件的导入路径格式:
// resolver.ts
export function MyLibResolver(): ComponentResolver {
return {
type: 'component',
resolve: (name: string) => {
if (name.startsWith('My')) {
const partial = name.slice(2).replace(/[A-Z]/g, (c, i) =>
i ? '-' + c.toLowerCase() : c.toLowerCase()
)
return {
name: 'default',
from: 'my-lib',
sideEffects: `my-lib/es/${partial}/style/index`,
}
}
},
}
}
组件库打包配置中,每个组件需要独立入口:
// build.config.ts
import { defineBuildConfig } from 'unbuild'
export default defineBuildConfig({
entries: [
'src/index.ts', // 全量入口
'src/components/button/index.ts',
'src/components/input/index.ts',
'src/components/select/index.ts',
// ...每个组件一个入口
],
externals: ['vue'],
rollup: {
emitCJS: true,
},
})
暗色主题的Token化实现
暗色模式不是简单的背景色反转,而是需要完整的设计Token体系支撑:
// theme/tokens.css
:root {
/* 语义化颜色Token */
--my-color-bg-primary: #ffffff;
--my-color-bg-secondary: #f5f5f5;
--my-color-text-primary: #1a1a1a;
--my-color-text-secondary: #666666;
--my-color-border: #e5e5e5;
--my-color-primary: #1677ff;
--my-color-primary-hover: #4096ff;
--my-shadow-card: 0 2px 8px rgba(0, 0, 0, 0.08);
/* 间距Token */
--my-spacing-xs: 4px;
--my-spacing-sm: 8px;
--my-spacing-md: 16px;
--my-spacing-lg: 24px;
/* 圆角Token */
--my-radius-sm: 2px;
--my-radius-md: 6px;
--my-radius-lg: 12px;
}
/* 暗色主题覆盖 */
:root[data-theme='dark'] {
--my-color-bg-primary: #141414;
--my-color-bg-secondary: #1f1f1f;
--my-color-text-primary: #ffffffd9;
--my-color-text-secondary: #ffffff73;
--my-color-border: #424242;
--my-color-primary: #1668dc;
--my-color-primary-hover: #3c89e8;
--my-shadow-card: 0 2px 8px rgba(0, 0, 0, 0.36);
}
主题切换通过修改根元素的data-theme属性实现,无需重新加载CSS。组件库所有样式必须引用Token变量,禁止使用硬编码颜色值。这一约束通过Stylelint规则强制执行:
// .stylelintrc.js
module.exports = {
rules: {
'color-no-hex': true,
'color-named': 'never',
'declaration-property-value-disallowed-list': {
'/.*/': ['/#[0-9a-fA-F]{3,8}/'],
},
},
}
这套方案确保暗色主题切换时,所有组件自动适配,无需为每个组件单独写暗色样式覆盖。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-jian-ku-she-ji-mo-shi-yu-xiang-ying-shi-bu-ju-gong/