Vue3组合式API与TypeScript型企业级组件库设计实践

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/

(0)
小编小编
上一篇 17小时前
下一篇 17小时前

相关推荐