Vue3的Composition API重构了组件逻辑组织方式,配合TypeScript类型系统,使得复杂组件的可维护性和复用性大幅提升。本文从组合式函数提取、Props类型约束、组件通信到按需加载,演示构建企业级Vue3组件库的完整实践。
组合式函数提取与复用模式
组合式函数(Composables)是Vue3中逻辑复用的核心机制。将组件中的响应式逻辑提取为独立函数,可在多个组件间共享。以一个表单验证逻辑为例:
// composables/useFormValidation.ts
import { ref, computed, type Ref } from 'vue'
interface ValidationRule {
required?: boolean
min?: number
max?: number
pattern?: RegExp
message: string
}
type Rules<T> = Partial<Record<keyof T, ValidationRule[]>>
export function useFormValidation<T extends Record<string, any>>(
formData: Ref<T>,
rules: Rules<T>
) {
const errors = ref<Partial<Record<keyof T, string>>>({})
const validateField = (field: keyof T): boolean => {
const fieldRules = rules[field]
if (!fieldRules) return true
const value = formData.value[field]
for (const rule of fieldRules) {
if (rule.required && !value) {
errors.value[field] = rule.message
return false
}
if (rule.min !== undefined && value.length < rule.min) {
errors.value[field] = rule.message
return false
}
if (rule.pattern && !rule.pattern.test(value)) {
errors.value[field] = rule.message
return false
}
}
delete errors.value[field]
return true
}
const validateAll = computed(() => {
return Object.keys(rules).every(field => validateField(field as keyof T))
})
const reset = () => {
errors.value = {}
}
return { errors, validateField, validateAll, reset }
}
泛型参数T约束表单数据类型,使得rules和errors的类型推导完整。在组件中使用时获得完整的类型提示:
<script setup lang="ts">
import { ref } from 'vue'
import { useFormValidation } from '@/composables/useFormValidation'
interface LoginForm {
username: string
password: string
email: string
}
const form = ref<LoginForm>({
username: '',
password: '',
email: ''
})
const { errors, validateField, validateAll, reset } = useFormValidation(form, {
username: [
{ required: true, message: '用户名不能为空' },
{ min: 3, message: '用户名至少3个字符' }
],
password: [
{ required: true, message: '密码不能为空' },
{ min: 8, message: '密码至少8位' }
],
email: [
{ required: true, message: '邮箱不能为空' },
{ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '邮箱格式不正确' }
]
})
const handleSubmit = () => {
if (validateAll.value) {
console.log('提交:', form.value)
}
}
</script>
defineProps与defineEmits的类型约束
Vue3.3+支持基于类型的Props声明,TypeScript泛型约束直接传入defineProps:
// components/DataTable.vue
<script setup lang="ts">
interface Column<T> {
key: keyof T
title: string
width?: number
sortable?: boolean
render?: (value: T[keyof T], row: T) => string
}
interface TableProps<T extends Record<string, any>> {
data: T[]
columns: Column<T>[]
loading?: boolean
rowKey: keyof T
selectable?: boolean
}
interface TableEmits<T> {
(e: 'sort', field: keyof T, order: 'asc' | 'desc'): void
(e: 'select', rows: T[]): void
(e: 'row-click', row: T): void
}
const props = defineProps<TableProps<any>>()
const emit = defineEmits<TableEmits<any>>()
</script>
使用泛型约束后,columns配置中的key字段会获得联合类型提示,传错字段名时编译期报错。emit的事件参数同样受类型约束,调用emit(‘sort’, ‘invalid_field’, ‘asc’)会触发类型错误。
组件间通信与依赖注入
深层嵌套组件的通信使用provide/inject。通过InjectionKey实现类型安全的依赖注入:
// composables/useTheme.ts
import type { InjectionKey, Ref } from 'vue'
import { ref, provide, inject } from 'vue'
interface ThemeContext {
mode: Ref<'light' | 'dark'>
primaryColor: Ref<string>
toggle: () => void
}
export const ThemeKey: InjectionKey<ThemeContext> = Symbol('theme')
export function provideTheme(initialMode: 'light' | 'dark' = 'light') {
const mode = ref(initialMode)
const primaryColor = ref('#1890ff')
const toggle = () => {
mode.value = mode.value === 'light' ? 'dark' : 'light'
primaryColor.value = mode.value === 'dark' ? '#177ddc' : '#1890ff'
}
const context: ThemeContext = { mode, primaryColor, toggle }
provide(ThemeKey, context)
return context
}
export function useTheme(): ThemeContext {
const context = inject(ThemeKey)
if (!context) {
throw new Error('useTheme必须在provideTheme的子组件中调用')
}
return context
}
InjectionKey携带类型信息,inject的返回值自动推导为ThemeContext类型。调用方获取的context有完整的类型提示,访问不存在的属性会在编译期报错。
组件库按需加载与Tree-shaking配置
组件库打包时需配置sideEffects和按需导入支持。使用unplugin-vue-components实现自动按需导入:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { resolve } from 'path'
export default defineConfig({
plugins: [
vue(),
Components({
dirs: [resolve(__dirname, 'src/components')],
extensions: ['vue'],
dts: 'src/components.d.ts',
resolvers: [
(name) => {
if (name.startsWith('App')) {
return { name: name.slice(3), from: '@mylib/ui' }
}
}
]
})
],
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['@mylib/ui']
}
}
}
}
})
dts选项自动生成组件类型声明文件,模板中直接使用组件名即可,无需手动import。manualChunks将第三方依赖分离为独立chunk,利用浏览器缓存减少重复下载。
组件库的package.json中设置sideEffects字段帮助构建工具进行Tree-shaking:
{
"name": "@mylib/ui",
"sideEffects": ["*.css", "*.scss"]
}
仅CSS文件标记为有副作用,JS文件可被安全Tree-shaking。这样打包时未引用的组件不会进入最终产物。
组件测试与快照验证
使用Vitest和@vue/test-utils编写组件单元测试。测试覆盖props传递、事件触发和插槽渲染:
// __tests__/DataTable.test.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import DataTable from '../DataTable.vue'
interface User { id: number; name: string; email: string }
const columns = [
{ key: 'name' as const, title: '姓名' },
{ key: 'email' as const, title: '邮箱' }
]
const data: User[] = [
{ id: 1, name: '张三', email: 'zhang@test.com' },
{ id: 2, name: '李四', email: 'li@test.com' }
]
describe('DataTable', () => {
it('渲染所有数据行', () => {
const wrapper = mount(DataTable, {
props: { data, columns, rowKey: 'id' }
})
expect(wrapper.findAll('tbody tr')).toHaveLength(2)
expect(wrapper.text()).toContain('张三')
expect(wrapper.text()).toContain('李四')
})
it('点击行触发row-click事件', async () => {
const wrapper = mount(DataTable, {
props: { data, columns, rowKey: 'id' }
})
await wrapper.findAll('tbody tr')[0].trigger('click')
expect(wrapper.emitted('row-click')).toBeTruthy()
expect(wrapper.emitted('row-click')![0]).toEqual([data[0]])
})
})
测试覆盖率目标设定为组件核心逻辑80%以上,UI交互测试使用快照对比检测意外的视觉变更。CI流水线中接入测试报告,覆盖率低于阈值时阻断合并。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-yu-typescript-xing-qi-ye-ji-zu-jian-ku/