[Vue 3] 泛型组件:打造类型安全、高度复用的 UI 组件
在现代前端开发中,TypeScript 凭借其强大的类型系统,为我们构建大型、健壮的应用程序提供了坚实的基础。
其中,Vue 3.3 版本引入的泛型组件(Generic Components)就是一个重要的里程碑,它允许我们像在 TypeScript 函数或类中使用泛型一样,在 Vue 组件中定义和使用类型参数,极大地提升了组件的可复用性和类型安全性。
1. 什么是泛型组件?
如果你熟悉 TypeScript,对泛型(Generics)一定不陌生。泛型允许我们在定义函数、接口或类的时候不预先指定具体的类型,而在使用的时候再指定类型。
// TypeScript 中的泛型函数示例
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("hello"); // T 被指定为 string
let output2 = identity<number>(123); // T 被指定为 number
Vue 3 的泛型组件将这个概念引入到了 .vue 单文件组件(SFC)中。通过在 <script setup> 标签上添加 generic 属性,我们可以声明一个或多个类型参数,并在 props、emits 甚至组件内部的逻辑中使用这些类型参数。
2. 如何定义一个泛型组件?
在 Vue 3.3+ 版本中,使用 <script setup> 语法糖定义泛型组件非常直观。下面我们用一个更具体的 GenericTable 组件来演示 Vue 3 泛型组件的用法。这个表格组件将能够接收不同类型的数据,以及相应的列定义,并以表格形式展示出来。
2.1 定义数据类型
export type User = {
userId: string;
userName: string;
email: string;
isActive: boolean;
};
export type Manager = {
managerId: number;
managerName: string;
department: string;
level: number;
};
2.2 创建泛型表格组件
这个组件将使用泛型 T 代表表格中每一行的数据类型。它还需要一个 columns prop 来定义如何展示数据。
<!-- GenericTable.vue -->
<script setup lang="ts" generic="T extends { [key: string]: any }">
// 使用 generic="T" 声明泛型参数 T
// 添加 extends { [key: string]: any } 是一个基础约束,表示 T 是一个对象
// 你也可以根据需要添加更具体的约束,例如 T extends { id: string | number }
import type { VNode } from 'vue'; // 如果 render 函数可能返回 VNode
// 1. 定义列配置的接口,也使用泛型
// 这个接口描述了每一列如何从 T 类型的对象中提取数据并显示
export interface ColumnDefinition<ItemType> {
key: keyof ItemType; // 关键点:使用 keyof 确保 key 存在于 ItemType 中
header: string; // 表头显示的文字
// 可选的自定义渲染函数,提供最大灵活性
render?: (item: ItemType) => VNode | string | number | boolean;
}
// 2. 定义 Props,使用泛型 T 和 ColumnDefinition<T>
const props = defineProps<{
items: T[]; // 表格数据,是 T 类型的数组
columns: ColumnDefinition<T>[]; // 列定义数组
itemKey?: keyof T; // 可选:指定用于行的 :key 的属性名
}>();
// 3. 辅助函数:获取单元格要显示的内容
const getCellValue = (item: T, column: ColumnDefinition<T>) => {
if (column.render) {
// 如果有自定义渲染函数,则使用它
return column.render(item);
}
// 否则,直接访问属性值
// Vue 模板会自动处理大多数基本类型
return item[column.key];
};
// 4. 辅助函数:获取行的唯一 key
const getRowKey = (item: T, index: number): string | number => {
if (props.itemKey && (typeof item[props.itemKey] === 'string' || typeof item[props.itemKey] === 'number')) {
// 如果提供了有效的 itemKey prop,并且对应的值是 string 或 number,则使用它
return item[props.itemKey] as string | number;
}
// 否则回退到使用索引(在没有唯一ID时是必要的,但有唯一ID更好)
return index;
};
</script>
<template>
<table class="generic-table">
<thead>
<tr>
<th v-for="column in columns" :key="String(column.key)">
{{ column.header }}
</th>
</tr>
</thead>
<tbody>
<template v-if="items.length > 0">
<tr v-for="(item, index) in items" :key="getRowKey(item, index)">
<td v-for="column in columns" :key="String(column.key)">
<span>{{ getCellValue(item, column) }}</span>
</td>
</tr>
</template>
<tr v-else>
<td :colspan="columns.length || 1" class="no-data">
暂无数据
</td>
</tr>
</tbody>
</table>
</template>
2.3 使用泛型表格组件
现在,我们可以在父组件中使用 GenericTable 来展示 User 和 Manager 数据。
<script setup lang="ts">
import { ref } from 'vue';
import GenericTable, { type ColumnDefinition } from './GenericTable.vue'; // 导入组件和类型
type UserColumns = ColumnDefinition<User>[];
type ManagerColumns = ColumnDefinition<Manager>[];
// 1. 准备数据
const users = ref<User[]>([
{ userId: 'USR001', userName: '张三', email: 'zhangsan@example.com', isActive: true },
{ userId: 'USR002', userName: '李四', email: 'lisi@example.com', isActive: false },
{ userId: 'USR003', userName: '王五', email: 'wangwu@example.com', isActive: true },
]);
const managers = ref<Manager[]>([
{ managerId: 101, managerName: '赵六', department: '技术部', level: 3 },
{ managerId: 102, managerName: '孙七', department: '市场部', level: 4 },
]);
// 2. 定义 User 表格的列配置
const userColumns = ref<UserColumns>([
{ key: 'userId', header: '用户ID' },
{ key: 'userName', header: '姓名' },
{ key: 'email', header: '邮箱' },
{
key: 'isActive',
header: '状态',
// 使用自定义渲染函数
render: (user) => user.isActive ? '活动' : '禁用'
}
]);
// 3. 定义 Manager 表格的列配置
const managerColumns = ref<ManagerColumns>([
{ key: 'managerId', header: '经理ID' },
{ key: 'managerName', header: '姓名' },
{ key: 'department', header: '部门' },
{
key: 'level',
header: '级别',
render: (manager) => `Level ${manager.level}` // 简单格式化
}
]);
</script>
<template>
<div>
<h2>用户列表</h2>
<GenericTable
:items="users"
:columns="userColumns"
itemKey="userId"
/>
<hr style="margin: 2em 0;">
<h2>经理列表</h2>
<GenericTable
:items="managers"
:columns="managerColumns"
itemKey="managerId"
/>
</div>
</template>
3. 总结
Vue 3 的泛型组件是一个强大的特性,它将 TypeScript 的类型系统与 Vue 的组件模型无缝结合,使得构建类型安全、高度可复用的 UI 组件变得前所未有的简单和优雅。如果你正在使用 Vue 3 和 TypeScript,并且追求更高质量、更易维护的代码,那么泛型组件绝对值得你深入了解和在项目中实践!