企业级 Vue3 组件库为什么必须从架构设计开始
组件库不是几个 UI 组件的简单堆砌。一个在多个业务线使用的组件库,必须解决类型安全、样式隔离、按需加载、主题定制、版本管理等工程问题。跳过架构设计直接写组件,后期重构成本远大于前期规划成本。Vue3 的组合式 API(Composition API)为组件库设计提供了更强的逻辑复用能力,但也需要更规范的目录结构和构建策略。
组件库项目结构设计
推荐的项目目录结构:
my-ui/
├── packages/
│ ├── components/
│ │ ├── button/
│ │ │ ├── src/
│ │ │ │ ├── Button.vue
│ │ │ │ ├── useButton.ts
│ │ │ │ └── button.props.ts
│ │ │ ├── index.ts
│ │ │ └── __tests__/
│ │ │ └── button.test.ts
│ │ ├── input/
│ │ └── ...
│ ├── hooks/
│ │ ├── use-click-outside.ts
│ │ ├── use-intersection-observer.ts
│ │ └── index.ts
│ ├── theme/
│ │ ├── src/
│ │ │ ├── variables.css
│ │ │ ├── dark.css
│ │ │ └── light.css
│ │ └── index.ts
│ └── utils/
│ ├── props.ts
│ └── install.ts
├── scripts/
│ ├── build.ts
│ └── publish.sh
├── package.json
├── tsconfig.json
├── vite.config.ts
└── vitest.config.ts
每个组件一个目录,内部包含实现、composable 逻辑抽取、props 类型定义和单测。hooks 目录存放跨组件复用的组合式函数,theme 目录管理样式变量和主题切换。
组合式 API 组件封装模式
以 Button 组件为例,展示组合式 API 的封装标准:
Props 类型定义(button.props.ts):
import type { PropType } from 'vue'
export const buttonProps = {
type: {
type: String as PropType<'primary' | 'success' | 'warning' | 'danger' | 'info' | 'default'>,
default: 'default',
},
size: {
type: String as PropType<'large' | 'default' | 'small'>,
default: 'default',
},
disabled: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
icon: {
type: [String, Object] as PropType<string | Component>,
default: '',
},
} as const
Props 独立定义文件,方便 SFC 组件和 TSX 组件复用,也能自动生成文档。
Composable 逻辑抽取(useButton.ts):
import type { SetupContext } from 'vue'
import { buttonProps } from './button.props'
export function useButton(props: typeof buttonProps, emit: SetupContext['emit']) {
const handleClick = (e: MouseEvent) => {
if (props.disabled || props.loading) return
emit('click', e)
}
const buttonClass = computed(() => [
'my-button',
`my-button--${props.type}`,
`my-button--${props.size}`,
{
'is-disabled': props.disabled,
'is-loading': props.loading,
},
])
return { handleClick, buttonClass }
}
逻辑与模板分离,composable 可以在不同组件间复用。如果后续要实现 LinkButton,只需新写一个模板文件引用同一个 useButton。
组件模板(Button.vue):
<template>
<button :class="buttonClass" :disabled="disabled || loading" @click="handleClick">
<span v-if="loading" class="my-button__loading">
<svg class="my-button__spinner" viewBox="0 0 1024 1024">...</svg>
</span>
<slot />
</button>
</template>
<script setup lang="ts">
import { buttonProps } from './button.props'
import { useButton } from './useButton'
const props = defineProps(buttonProps)
const emit = defineEmits(['click'])
const { handleClick, buttonClass } = useButton(props, emit)
</script>
CSS 变量与主题系统设计
样式隔离是组件库的基础要求。使用 CSS 变量实现主题定制:
/* packages/theme/src/variables.css */
:root {
--my-color-primary: #409eff;
--my-color-success: #67c23a;
--my-color-warning: #e6a23c;
--my-color-danger: #f56c6c;
--my-color-info: #909399;
--my-font-size-base: 14px;
--my-font-size-small: 12px;
--my-font-size-large: 16px;
--my-border-radius-base: 4px;
--my-border-color: #dcdfe6;
--my-button-height-default: 32px;
--my-button-height-large: 40px;
--my-button-height-small: 24px;
}
/* packages/theme/src/dark.css */
html.dark {
--my-color-primary: #3a8ee6;
--my-border-color: #4c4d4f;
--my-bg-color: #1d1e1f;
}
组件样式使用变量而非硬编码颜色值:
.my-button--primary {
background-color: var(--my-color-primary);
border-color: var(--my-color-primary);
color: #fff;
}
.my-button--small {
height: var(--my-button-height-small);
font-size: var(--my-font-size-small);
}
业务方只需覆盖 CSS 变量就能实现品牌定制,不需要改组件源码。
按需加载与 Tree-shaking 配置
组件库全量引入会显著增加打包体积。按需加载通过 ES Module 的导出结构实现:
每个组件的 index.ts:
import { withInstall } from '@my-ui/utils'
import Button from './src/Button.vue'
export const MyButton = withInstall(Button)
export default MyButton
export * from './button.props'
总入口 packages/components/index.ts:
export { MyButton } from './button'
export { MyInput } from './input'
// ... 逐个导出
构建配置使用 Vite 的 library 模式:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
lib: {
entry: resolve(__dirname, 'packages/components/index.ts'),
formats: ['es'],
fileName: () => 'index.mjs',
},
rollupOptions: {
external: ['vue'],
output: {
preserveModules: true,
preserveModulesRoot: 'packages',
},
},
},
})
preserveModules: true 让 Rollup 保留原始模块结构,业务项目 import 某个组件时,打包工具可以只打包该组件及其依赖。
package.json 的 exports 字段支持子路径导入:
{
"exports": {
".": { "import": "./dist/index.mjs" },
"./button": { "import": "./dist/components/button/index.mjs" },
"./input": { "import": "./dist/components/input/index.mjs" },
"./theme": { "import": "./dist/theme/index.mjs" }
}
}
业务项目直接 import { MyButton } from '@my-ui/button',只引入 Button 相关代码。
自动化测试策略
组件库测试分两层:单元测试覆盖逻辑,组件测试覆盖渲染和交互:
// button.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { MyButton } from '../index'
describe('MyButton', () => {
it('emits click event when not disabled', async () => {
const wrapper = mount(MyButton)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toHaveLength(1)
})
it('does not emit click when disabled', async () => {
const wrapper = mount(MyButton, { props: { disabled: true } })
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toBeUndefined()
})
it('applies size class correctly', () => {
const wrapper = mount(MyButton, { props: { size: 'small' } })
expect(wrapper.classes()).toContain('my-button--small')
})
})
CI 流水线中配置 Vitest 跑全量测试,覆盖率门禁 80%。
发布与版本管理
使用 changesets 管理版本和 changelog:
# 安装
pnpm add -D @changesets/cli
# 初始化
pnpm changeset init
# 每次发版前记录变更
pnpm changeset
# 自动更新版本号和 changelog
pnpm changeset version
# 发布
pnpm changeset publish
changesets 的优势在于支持 monorepo 中多包联动版本,修改了 Button 组件后可以只发 Button 的补丁版本,不需要全量发版。
Vue3 组合式 API 组件库的核心设计思路是逻辑与模板分离、样式与变量分离、导出与构建分离。这三个分离让组件库在多业务线复用时具备了按需加载、主题定制和独立发版的能力,是前端工程化体系中基础设施级别的工作。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-zu-jian-ku-she-ji-cong-jia-gou-gui-hua/