早在 Vue3.3 版本开始,Vue3 开始支持泛型组件。可笔者在写这篇文章的时候,依然发现一些群友在 Vue3 的项目中还没有用上,于是今天出一篇文章,和大家一起交流。
什么是泛型
泛型编程是一种编程范式,允许你编写能够与多种数据类型一起工作的代码,同时保留编译时类型检查的益处。通过使用泛型,你可以创建类、接口和方法,这些组件可以操作不特定于某一个类型的对象。
如上所说,泛型可以用在类、接口、方法、属性中,那自然也能用在组件中。
于是 Vue3 在组件中支持了泛型参数,让我们能更好的传递参数类型,从而提升我们的组件使用体验。
泛型组件的优点
定义和使用泛型组件,你可以体验到:
限制props
参数的类型
可以限制传入组件的数据必须是某个类或者某个接口的继承者的类型,于是你可以在组件内部放心大胆的调用该类型下应该有的属性和方法等。
例如限制了传入 User
的类型,那么组件内部可以大胆的调用用户的 昵称、性别等信息。
提供emit
事件的属性类型
组件我们往往还会提供一系列的事件,而事件一般都是通过 emit
来传递的,那么组件内部就可以通过泛型来定义事件类型,使用者在获取 emit
属性的时候,可以无需断言,直接依赖泛型传递的参数类型进行接下来的操作。
例如 一个组件要求传入了一个用户列表,那组件的点击事件会返回列表中点击的用户信息,@click 事件的参数就可以直接拿到是用户的类型。
提供slot
插槽的参数类型
组件提供插槽之后,可能会给插槽传递一些作用域带参数的类型,我们也可以用泛型组件来直接传递。
如何定义泛型组件
基于上面的三点,我们来定义一个卡片列表组件:
泛型组件和基础类型定义
首先,我们定义了一个 Item
接口,用于定义传入组件的数据基础类型。
ts
export interface Item{
id: number
disabled: boolean
description?: string
}
其中包含了必要的 ID 、禁用 以及可选的 描述 三个属性。
为了方便测试,我们起一个 Item
的子类型:
ts
/**
* # 用户
*/
export interface User extends Item {
/**
* ## 昵称
*/
name: string
/**
* ## 年龄
*/
age: number
}
接下来,我们定义了一个泛型组件,并使用 generic
关键字来指定组件的泛型类型。
html
<script lang="ts" setup generic="T extends Item">
</script>
这表示,这个组件是个泛型组件,它的类型参数 T
必须是 Item
类型或子类型。
html
<script lang="ts" setup>
const users: Ref<User[]> = ref([
{
// 用户信息
}
])
</script>
<template>
<card-list :items="users"/>
</template>
定义Props
参数
我们在定义了泛型组件之后,就可以在组件的 props
中使用泛型类型了。
ts
defineProps({
items: {
type: Array<T>,
required: true,
}
})
上面的代码表示,这个组件必须传入 :items="[]"
的数组,并且这个数组中的每一项都必须是 Item
类型或者它的子类型。
定义Emit
事件
定义Emit事件,我们使用 defineEmits
来定义。
ts
const emits = defineEmits<{
click:[T]
}>()
这里我们定义了一个名为 click
的事件,它的参数类型是 T
,表示点击事件会返回一个 Item
类型的对象。
所以我们就可以在组件内部通过 @click="emits('click', item)"
来传递事件,并且 item
的类型就是 T
。
定义Slot
插槽
我们通过 slot
来定义插槽,并使用泛型类型来定义插槽的参数类型。
html
<slot name="title" :row="item" />
这时,插槽就带上了一个 row
属性,而且属性的类型和 item
一致,都是传入的 :items
的真实类型。
使用的时候,我们就可以直接拿到 row
的属性类型,而不用断言了。
完整代码
组件定义
html
<template>
<div class="list">
<div
v-for="item in items"
:key="item.id"
class="card"
:class="item.disabled ? 'disabled' : ''"
@click="item.disabled ? {} : emits('click', item)"
>
<div class="card-body">
<div class="header">
<div class="id">
ID: {{ item.id }}
</div>
<div class="title">
<slot
name="title"
:row="item"
/>
</div>
</div>
<div class="description">
{{ item.description || '暂无描述' }}
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup generic="T extends Item">
import { Item } from './Item'
defineProps({
items: {
type: Array<T>,
required: true,
}
})
const emits = defineEmits<{
click:[T]
}>()
</script>
<style lang="scss" scoped>
.list {
display: flex;
flex-wrap: wrap;
flex-direction: column;
gap: 1rem;
}
/* 一些其他样式 */
.disabled {
background-color: #aaa;
opacity: 0.1;
pointer-events: none;
}
</style>
组件调用
html
<template>
<div class="home">
<CardList
:items="users"
@click="console.log($event.name)"
>
<template #title="{ row }">
{{ row.name }}
</template>
</CardList>
</div>
</template>
<script lang="ts" setup>
import { Ref, ref } from 'vue'
import CardList from './console/card-list.vue'
import { User } from './console/User'
const users: Ref<User[]> = ref([])
for (let i = 1; i <= 5; i += 1) {
let descrption = ''
if (i % 3 === 0) {
descrption = `第${i}个用户`
}
users.value.push({
id: i,
name: `name ${i}`,
disabled: i % 2 === 0,
age: i,
description: descrption,
})
}
</script>
实现的效果
总结
通过使用泛型组件,我们可以在定义组件时指定其类型参数,并在组件内部使用这些类型参数来定义组件的属性、事件和插槽。这样,我们就可以确保组件的参数类型和返回类型都是正确的,从而提高日常编码中的一些数据类型提示、类型检查等。
今天的话题就到这,拜拜了您嘞~
更多的源代码参考,可以关注我们的开源项目: