log
码中赤兔

[Vue 3] 泛型组件:打造类型安全、高度复用的 UI 组件

发布于 2025年5月10日
更新于 2025年5月10日
17 分钟阅读
Vue

在现代前端开发中,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,并且追求更高质量、更易维护的代码,那么泛型组件绝对值得你深入了解和在项目中实践!

关于

分享技术见解、经验和思考的个人博客

联系方式

  • Email: hushukang_blog@proton.me
  • GitHub

© 2025 码中赤兔. 版权所有