第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引入了更简洁的泛型组件语法,直接在<script setup>标签中使用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开发体验,主要包括:
- 更简洁的语法:在
<script setup>标签中直接使用generic属性 - 多个泛型参数支持:允许定义多个泛型类型参数
- 泛型默认值:可以为泛型参数指定默认类型
- 与defineExpose集成:支持暴露泛型方法和属性
- 完整的TypeScript特性支持:包括条件类型、映射类型等
- 更好的IDE支持:提供更准确的类型推断和自动补全
这些改进使得Vue 3.3+泛型组件在处理复杂数据结构时更加灵活和类型安全,特别适合开发通用组件库和大型应用。
在下一集中,我们将探讨Vue 3.3+中Suspense组件的增强功能。