Vue 3 自定义 Hooks

647 阅读5分钟

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 开头,比如 useCounteruseFetchData 等。
  • 命名应简洁明了,能够反映 Hook 的功能。

使用约定

  • 自定义 Hooks 应该是纯函数,即不直接修改外部状态,只通过返回值暴露必要的状态和方法。
  • 避免在 Hook 中引入副作用操作,比如直接操作 DOM 或使用全局变量。
  • 在 Hook 内部处理错误,不要把错误抛出到外部,否则会增加 Hook 的使用成本。
  • Hook 是单一功能的,不要给一个 Hook 设计过多功能。

使用方式

在组件中使用自定义 Hook 时,需要在 setup 函数中调用 Hook,并将返回值解构赋值给组件的局部变量,从而在模板中使用。

创建自定义 Hook

展示数据表格是一个常见需求。在实现表格数据展示时,通常需要处理几个关键点:loading 状态、表格数据 tableData、总数total和数据获取函数 queryTableData。我们可以通过自定义 Hooks 来实现这一需求。

效果展示

20240613000816_rec_.gif

目录结构

image.png

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>