Vue 3 自定义 Hooks
Vue 3 引入了组合式 API(Composition API),带来了一个强大而灵活的工具——自定义 Hooks。自定义 Hooks 让我们可以轻松地在不同组件之间共享逻辑,它让开发者的代码具有更高的复用度且更加清晰、易于维护。
概念和简介
在 Vue 3 中,自定义 Hooks 是指使用组合式 API 创建的函数,这些函数封装了组件的可重用逻辑。通过自定义 Hooks,我们可以将状态、方法、生命周期钩子等逻辑封装在一个独立的函数中,从而在多个组件中重复使用
为什么使用Hook
在大型应用中,不同组件可能会有相似的逻辑。我们需要将这些相似的逻辑提取出来,从而在多个组件中复用。
在 Vue 2 中,我们通常使用 mixins 和高阶组件来复用逻辑,但这些方法存在许多缺点。Vue 3 的 Hooks 相较于 Vue 2 的 Mixin,具有清晰的逻辑来源和功能、无命名冲突、精简逻辑、关注点分离、更好的代码重用、更好的类型支持和更好的调试体验等诸多优势。这使得我们可以更高效地编写、维护和复用代码,提高整体开发效率和代码质量。
1. 清晰的逻辑来源和功能
Mixin 的问题:Mixin 将状态和方法混合到组件中,使得来源不明确,逻辑分散。多个 Mixin 组合在一起时,难以追踪每个功能的具体来源。
Hooks 的优势:Hooks 将逻辑封装在独立的函数中,与外界隔离,仅暴露必要的函数和变量。每个 Hook 的来源和功能清晰可辨,易于理解和维护。
2. 无命名冲突
Mixin 的问题:多个 Mixin 可能会带来命名冲突,因为它们将状态和方法直接混合到组件中,不同 Mixin 中的同名属性或方法会互相覆盖或干扰。
Hooks 的优势:Hooks 是闭包函数,内部导出的变量和方法可以重命名,因此同一个 Hook 可以在同一个组件中多次使用而不会发生命名冲突。
3. 精简逻辑,关注点分离
Mixin 的问题:Mixin 的逻辑分散在不同的文件中,导致组件变得复杂,难以维护。需要理解和处理每个 Mixin 的内部逻辑,才能掌握组件的整体逻辑。
Hooks 的优势:Hooks 将相关逻辑集中在一个函数中,逻辑清晰且独立。使用 Hooks 时,只需了解其提供的功能和使用方法,不必关心内部实现。这样可以专注于其他核心业务逻辑,节省大量重复代码,提高开发效率。
4. 更好的代码重用
Mixin 的问题:Mixin 的代码重用性较差,因为它们将所有逻辑混合到组件中,导致组件之间的耦合度增加,复用时需要处理大量无关逻辑。
Hooks 的优势:Hooks 提供了高度模块化的代码结构,可以将常用的逻辑封装在独立的 Hook 中,并在不同组件中复用。Hooks 之间没有耦合,使用起来更加灵活。
5. 更好的类型支持
Mixin 的问题:Mixin 的类型支持较差,难以在使用 TypeScript 时提供良好的类型推断和检查。
Hooks 的优势:Hooks 是纯函数,具有明确的输入和输出,更容易与 TypeScript 集成,提供良好的类型支持,提升代码的可靠性和可维护性。
6. 更好的调试体验
Mixin 的问题:多个 Mixin 混合在一起时,调试变得困难,因为需要在多个文件和逻辑之间切换。
Hooks 的优势:Hooks 将相关逻辑集中在一个函数中,调试时只需关注这个函数即可。此外,Hooks 可以在 Vue DevTools 中更清晰地显示,提升调试体验。
规范、约定和使用方式
命名约定
- 自定义 Hooks 通常以
use开头,比如useCounter、useFetchData等。 - 命名应简洁明了,能够反映 Hook 的功能。
使用约定
- 自定义 Hooks 应该是纯函数,即不直接修改外部状态,只通过返回值暴露必要的状态和方法。
- 避免在 Hook 中引入副作用操作,比如直接操作 DOM 或使用全局变量。
- 在 Hook 内部处理错误,不要把错误抛出到外部,否则会增加 Hook 的使用成本。
- Hook 是单一功能的,不要给一个 Hook 设计过多功能。
使用方式
在组件中使用自定义 Hook 时,需要在 setup 函数中调用 Hook,并将返回值解构赋值给组件的局部变量,从而在模板中使用。
创建自定义 Hook
展示数据表格是一个常见需求。在实现表格数据展示时,通常需要处理几个关键点:loading 状态、表格数据 tableData、总数total和数据获取函数 queryTableData。我们可以通过自定义 Hooks 来实现这一需求。
效果展示
目录结构
interface.ts
export interface ISearchParams {
pageNum: number
pageSize: number
name: string
}
export interface ITableDataRecord {
id: string
name: string
age: number
address: string
}
export interface ITableDataRes {
total: number
list: ITableDataRecord[]
}
export interface IColumn {
title: string
dataIndex: string
key: string
}
api.ts
import { ISearchParams, ITableDataRes } from "./interface"
let data = [
{
id: "1",
name: "胡彦斌",
age: 32,
address: "西湖区湖底公园1号"
}
]
export const fetchTableData = async (params: ISearchParams): Promise<ITableDataRes> => {
// 模拟数据获取
return new Promise((resolve) => {
data = [
...data,
{
id: String(data.length + 1),
name: "胡彦斌" + data.length,
age: 32,
address: "西湖区湖底公园1号"
}
]
setTimeout(() => {
resolve({
total: 2,
list: data
})
}, 1000)
})
}
hooks.ts
import { Ref, ref } from "vue"
import { ISearchParams, ITableDataRecord } from "./interface"
import { fetchTableData } from "./api"
export const useTableData = (): {
loading: Ref<boolean>
total: Ref<number>
tableData: Ref<ITableDataRecord[]>
queryTableData: (params: ISearchParams) => void
} => {
const loading = ref(false)
const total = ref(0)
const list = ref<ITableDataRecord[]>([])
const queryTableData = async (params: ISearchParams) => {
try {
loading.value = true
const res = await fetchTableData(params)
total.value = res.total
list.value = res.list
} catch (error) {
console.log(error)
} finally {
loading.value = false
}
}
return {
loading,
total,
tableData: list,
queryTableData
}
}
columns.ts
import { IColumn } from "./services/interface"
export const columns: IColumn[] = [
{
title: "姓名",
dataIndex: "name",
key: "name"
},
{
title: "年龄",
dataIndex: "age",
key: "age"
},
{
title: "住址",
dataIndex: "address",
key: "address"
}
]
index.vue
<template>
<div class="app-container">
<Card>
搜索框
<Button @click="getTableData">搜索</Button>
</Card>
<Card>
<Table
:loading="tableLoading"
:dataSource="tableData"
:columns="columns"
></Table>
<div>分页器</div>
</Card>
</div>
</template>
<script lang="ts" setup>
import { onMounted } from "vue"
import { Button, Card, Table } from "ant-design-vue"
import { columns } from "./columns"
import { useTableData } from "./services/hooks"
const {
loading: tableLoading,
total,
tableData,
queryTableData
} = useTableData()
const getTableData = () => {
queryTableData({
pageNum: 1,
pageSize: 10,
name: "张三"
})
}
onMounted(() => {
getTableData()
})
</script>