第253集:Vue 3.3+泛型组件改进

概述

Vue 3.3版本对TypeScript支持进行了重大增强,其中泛型组件的改进尤为显著。本集将深入探讨Vue 3.3+中泛型组件的新特性,包括更简洁的泛型语法、多个泛型参数支持、与defineExpose的集成以及在实际项目中的最佳实践。

泛型组件基础回顾

在Vue 3.3之前,我们定义泛型组件需要使用defineComponent函数并在类型参数中指定:

<template>
  <div class="list-component">
    <h2>{{ title }}</h2>
    <ul>
      <li v-for="item in items" :key="item.id">
        {{ item.name }}
      </li>
    </ul>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  props: {
    title: {
      type: String,
      required: true
    },
    items: {
      type: Array as () => Array<{ id: any; name: string }>,
      required: true
    }
  }
})
</script>

这种方式在处理复杂泛型时显得冗长且不够直观。

Vue 3.3+泛型组件新语法

Vue 3.3引入了更简洁的泛型组件语法,直接在&lt;script setup&gt;标签中使用generic属性:

基本泛型组件

<template>
  <div class="list-component">
    <h2>{{ title }}</h2>
    <ul>
      <li v-for="item in items" :key="getKey(item)">
        {{ item.name }}
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts" generic="T extends { id: any; name: string }">
// 定义props,使用泛型类型T
const props = defineProps<{
  title: string
  items: T[]
}>()

// 定义泛型方法
const getKey = (item: T): any => {
  return item.id
}
</script>

多个泛型参数

Vue 3.3+支持多个泛型参数,使用逗号分隔:

<template>
  <div class="pair-component">
    <div class="key">{{ pair.key }}</div>
    <div class="value">{{ displayValue(pair.value) }}</div>
  </div>
</template>

<script setup lang="ts" generic="K, V">
// 定义带有两个泛型参数的props
const props = defineProps<{
  pair: { key: K; value: V }
  formatter?: (value: V) => string
}>()

// 使用泛型参数V
const displayValue = (value: V): string => {
  if (props.formatter) {
    return props.formatter(value)
  }
  return String(value)
}
</script>

泛型约束与默认值

可以为泛型参数添加约束和默认值:

<template>
  <div class="select-component">
    <select v-model="selectedValue">
      <option 
        v-for="option in options" 
        :key="option.value" 
        :value="option.value"
      >
        {{ option.label }}
      </option>
    </select>
    <div class="selected">Selected: {{ selectedValue }}</div>
  </div>
</template>

<script setup lang="ts" generic="T = string">
// 泛型T默认为string类型
const props = defineProps<{
  options: Array<{ label: string; value: T }>
  defaultValue?: T
}>()

// 使用泛型T作为ref类型
const selectedValue = ref<T>(props.defaultValue as T)
</script>

泛型组件与defineExpose集成

Vue 3.3+允许泛型组件通过defineExpose暴露泛型方法和属性:

<template>
  <div class="data-table">
    <table>
      <thead>
        <tr>
          <th v-for="col in columns" :key="col.key">{{ col.label }}</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="row in data" :key="row.id">
          <td v-for="col in columns" :key="col.key">
            {{ row[col.key as keyof T] }}
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup lang="ts" generic="T extends { id: any }">
interface Column {
  key: string
  label: string
}

const props = defineProps<{
  data: T[]
  columns: Column[]
}>()

// 暴露泛型方法
const findRow = (id: T['id']): T | undefined => {
  return props.data.find(row => row.id === id)
}

const sortBy = <K extends keyof T>(key: K, ascending: boolean = true): T[] => {
  return [...props.data].sort((a, b) => {
    const aVal = a[key]
    const bVal = b[key]
    if (aVal < bVal) return ascending ? -1 : 1
    if (aVal > bVal) return ascending ? 1 : -1
    return 0
  })
}

// 使用defineExpose暴露泛型方法
defineExpose({
  findRow,
  sortBy
})
</script>

父组件使用时可以获取到正确的泛型类型:

<template>
  <div>
    <h1>用户管理</h1>
    <DataTable 
      ref="tableRef"
      :data="users" 
      :columns="columns"
    />
    <button @click="handleFindUser">查找用户</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import DataTable from './DataTable.vue'

interface User {
  id: number
  name: string
  email: string
  age: number
}

const users: User[] = [
  { id: 1, name: '张三', email: 'zhangsan@example.com', age: 25 },
  { id: 2, name: '李四', email: 'lisi@example.com', age: 30 },
  { id: 3, name: '王五', email: 'wangwu@example.com', age: 28 }
]

const columns = [
  { key: 'id', label: 'ID' },
  { key: 'name', label: '姓名' },
  { key: 'email', label: '邮箱' },
  { key: 'age', label: '年龄' }
]

// 获取DataTable实例,自动推断泛型类型为User
const tableRef = ref<InstanceType<typeof DataTable<User>> | null>(null)

const handleFindUser = () => {
  if (tableRef.value) {
    const user = tableRef.value.findRow(1)
    console.log('Found user:', user)
    
    // 按年龄排序,类型安全
    const sortedUsers = tableRef.value.sortBy('age')
    console.log('Sorted users by age:', sortedUsers)
  }
}
</script>

泛型组件与Props解构

Vue 3.3+支持在泛型组件中解构props并保持类型安全:

<template>
  <div class="generic-card">
    <div class="card-header" v-if="header">
      {{ header }}
    </div>
    <div class="card-content">
      <slot :item="item">{{ defaultSlotContent }}</slot>
    </div>
    <div class="card-footer" v-if="footer">
      {{ footer }}
    </div>
  </div>
</template>

<script setup lang="ts" generic="T">
const {
  item,
  header,
  footer
} = defineProps<{
  item: T
  header?: string
  footer?: string
}>()

// 使用泛型T定义计算属性
const defaultSlotContent = computed(() => {
  return JSON.stringify(item, null, 2)
})
</script>

复杂泛型场景:条件类型与映射类型

Vue 3.3+泛型组件支持复杂的TypeScript特性,如条件类型和映射类型:

<template>
  <div class="form-builder">
    <form @submit.prevent="handleSubmit">
      <div 
        v-for="field in fields" 
        :key="field.name" 
        class="form-field"
      >
        <label :for="field.name">{{ field.label }}:</label>
        <input
          :type="field.type"
          :id="field.name"
          v-model="formData[field.name as keyof T]"
        />
      </div>
      <button type="submit">提交</button>
    </form>
    <div class="form-preview">
      <h3>表单数据预览:</h3>
      <pre>{{ formData }}</pre>
    </div>
  </div>
</template>

<script setup lang="ts" generic="T extends Record<string, any>">
interface FormField {
  name: keyof T
  label: string
  type: 'text' | 'number' | 'email' | 'password'
}

const props = defineProps<{
  fields: FormField[]
  initialData?: Partial<T>
}>()

// 使用映射类型创建表单数据
const formData = ref<T>({
  ...{} as T,
  ...props.initialData
})

const handleSubmit = () => {
  console.log('Form submitted:', formData.value)
  // 可以在这里添加表单验证和提交逻辑
}
</script>

泛型组件最佳实践

1. 明确泛型约束

始终为泛型参数添加明确的约束,提高类型安全性和开发体验:

<script setup lang="ts" generic="T extends { id: string | number; createdAt: Date }">
// 明确T必须包含id和createdAt属性
</script>

2. 使用有意义的泛型参数名

对于复杂组件,使用有意义的泛型参数名而非单个字母:

<script setup lang="ts" generic="UserType extends { id: number }, RoleType extends { id: number }">
// 使用UserType和RoleType而非T和U,提高可读性
</script>

3. 提供默认泛型类型

对于通用组件,提供默认泛型类型可以简化使用:

<script setup lang="ts" generic="T = { id: number; name: string }">
// 默认为常见的id-name结构
</script>

4. 结合TypeScript工具类型

充分利用TypeScript的工具类型增强泛型组件的功能:

<script setup lang="ts" generic="T">
// 使用Required工具类型确保所有属性都存在
const requiredProps = defineProps<Required<{
  data: T[]
  config?: {
    showHeader?: boolean
    showFooter?: boolean
  }
}>>()
</script>

泛型组件在实际项目中的应用

案例:通用表格组件

<template>
  <div class="generic-table">
    <div class="table-header" v-if="showHeader">
      <slot name="header">
        <h2>{{ title }}</h2>
      </slot>
    </div>
    
    <table>
      <thead>
        <tr>
          <th 
            v-for="column in columns" 
            :key="column.key"
            @click="handleSort(column.key)"
            class="sortable"
          >
            {{ column.label }}
            <span v-if="sortKey === column.key">
              {{ sortOrder === 'asc' ? '↑' : '↓' }}
            </span>
          </th>
        </tr>
      </thead>
      <tbody>
        <tr 
          v-for="(row, index) in sortedData" 
          :key="row[idField]"
          @click="$emit('row-click', row, index)"
          class="clickable-row"
        >
          <td v-for="column in columns" :key="column.key">
            <slot 
              :name="`cell-${column.key}`" 
              :row="row" 
              :index="index"
            >
              {{ row[column.key as keyof T] }}
            </slot>
          </td>
        </tr>
      </tbody>
    </table>
    
    <div class="table-footer" v-if="showFooter">
      <slot name="footer">
        <div class="pagination">
          <button @click="$emit('page-change', currentPage - 1)" :disabled="currentPage === 1">
            上一页
          </button>
          <span>第 {{ currentPage }} 页,共 {{ totalPages }} 页</span>
          <button @click="$emit('page-change', currentPage + 1)" :disabled="currentPage === totalPages">
            下一页
          </button>
        </div>
      </slot>
    </div>
  </div>
</template>

<script setup lang="ts" generic="T extends Record<string, any>">
interface Column {
  key: string
  label: string
  sortable?: boolean
}

const props = withDefaults(defineProps<{
  title?: string
  data: T[]
  columns: Column[]
  idField?: keyof T
  sortKey?: string
  sortOrder?: 'asc' | 'desc'
  currentPage?: number
  pageSize?: number
  showHeader?: boolean
  showFooter?: boolean
}>(), {
  idField: 'id',
  sortOrder: 'asc',
  currentPage: 1,
  pageSize: 10,
  showHeader: true,
  showFooter: true
})

const emit = defineEmits<{
  'row-click': [row: T, index: number]
  'page-change': [page: number]
  'sort-change': [key: string, order: 'asc' | 'desc']
}>()

// 计算排序后的数据
const sortedData = computed(() => {
  if (!props.sortKey) return props.data
  
  const column = props.columns.find(col => col.key === props.sortKey)
  if (!column?.sortable) return props.data
  
  return [...props.data].sort((a, b) => {
    const aVal = a[props.sortKey as keyof T]
    const bVal = b[props.sortKey as keyof T]
    
    if (aVal < bVal) return props.sortOrder === 'asc' ? -1 : 1
    if (aVal > bVal) return props.sortOrder === 'asc' ? 1 : -1
    return 0
  })
})

// 计算总页数
const totalPages = computed(() => {
  return Math.ceil(props.data.length / props.pageSize)
})

// 处理排序
const handleSort = (key: string) => {
  const newSortOrder = props.sortKey === key && props.sortOrder === 'asc' ? 'desc' : 'asc'
  emit('sort-change', key, newSortOrder)
}
</script>

总结

Vue 3.3+对泛型组件的改进显著提升了TypeScript开发体验,主要包括:

  1. 更简洁的语法:在&lt;script setup&gt;标签中直接使用generic属性
  2. 多个泛型参数支持:允许定义多个泛型类型参数
  3. 泛型默认值:可以为泛型参数指定默认类型
  4. 与defineExpose集成:支持暴露泛型方法和属性
  5. 完整的TypeScript特性支持:包括条件类型、映射类型等
  6. 更好的IDE支持:提供更准确的类型推断和自动补全

这些改进使得Vue 3.3+泛型组件在处理复杂数据结构时更加灵活和类型安全,特别适合开发通用组件库和大型应用。

在下一集中,我们将探讨Vue 3.3+中Suspense组件的增强功能。

« 上一篇 Vue 3 defineModel简化双向绑定:提升组件开发效率 下一篇 » Vue 3 泛型组件改进:构建类型安全的灵活组件