likes
comments
collection
share

Vue Hooks: 让Vue开发更简单与高效

作者站长头像
站长
· 阅读数 12

前言

Hooks是React等函数式编程框架中非常受欢迎的工具,随着VUE3 Composition API 函数式编程风格的推出,现在也受到越来越多VUE3开发者的青睐,它让开发者的代码具有更高的复用度且更加清晰、易于维护。

本文将快速略过并了解Hooks的使用基础以及自定义HOOK开发相关的要点。

含有较多参考自官方文档、他人文章的内容,侵删。

Hooks简介

1. 什么是Hooks

Hooks并不是VUE特有的概念,实际上它原本被用于指代一些特定时间点会触发的勾子。而在React16之后,它被赋予了新的意义:

一系列以 use 作为开头的方法,它们提供了让你可以完全避开 class式写法,在函数式组件中完成生命周期、状态管理、逻辑复用等几乎全部组件开发工作的能力。

····

而在VUE3中,Hooks的概念结合了VUE的响应式系统,被称为组合函数。组合函数是VUE3组合式API中提供的新的逻辑复用的方案,是一类利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

简单来说,它就是一个创建工具的工具

2. Hooks与composition Api

Hooks是一种函数式的编程思维,所以通常我们会在函数式风格的框架或组件中使用hook,比如VUE的组合式API(Composition Api)。Hooks在VUE2所使用的选项式风格API中也不是不可以使用,毕竟Hook本质只是一个函数,只要hook内部所使用的api能够得到支持,我们可以在任何地方使用它们,只是可能需要额外的支持以及效果没有函数式组件中那么好,因为仍会被选项分割。

VUE3推出时为开发者带来了全新的Composition API即组合式API。它是一种通过函数来描述组件逻辑的开发模式。组合式API为开发者带来了更好的逻辑复用能力,通过组合函数来实现更加简洁高效的逻辑复用。

<script setup>
import { ref, onMounted } from 'vue'

// 响应式状态
const count = ref(0)

// 用来修改状态、触发更新的函数
function increment() {
  count.value++
}

// 生命周期钩子
onMounted(() => {
  console.log(`The initial count is ${count.value}.`)
})
</script>

<template>
  <button @click="increment">Count is: {{ count }}</button>
</template>

为什么要使用Hook

1. Mixin/Class的局限性

在以往VUE2的选项式API中,主要通过Mixin或是Class继承来实现逻辑复用,但这种方式有三个明显的短板

  • 不清晰的数据来源:当使用了多个mixin或class时,哪个数据是哪个模块提供的将变得难以追寻,这将提高维护难度

  • 命名空间冲突:来自多个class/mixin的开发者可能会注册同样的属性名,造成冲突

  • 隐性的跨模块交流:不同的mixin/class之间可能存在某种相互作用,产生未知的后果

以上三种主要的缺点导致在大型项目的开发中,多mixin/class的组合将导致逻辑的混乱以及维护难度的提升,因而在VUE3的官方文档中不再继续推荐使用,保留mixin也只是为了迁移的需求或方便VUE2用户熟悉。

2. Hooks的优势

其实Mixin/Class的缺点反过来就是Hooks的优点:

  • 清晰一目了然的源头:Hooks不是一个类,没有将状态、方法存放在对象中,然后通过导出对象的形式实现复用,也就不会有对象间过度耦合、干扰等问题。Hooks中的各类状态是封装在内部的,与外界隔离,仅暴露函数、变量,这使得其来源、功能清晰可辨

  • 没有命名冲突的问题:Hooks本质是闭包函数,内部所导出的变量、方法支持重命名,因而同一个Hook在同一个组件中可以N次使用而不冲突

  • 精简逻辑:一个Hook开发完成后,在使用Hook时不需要关心其内部逻辑,只需知道有什么效果、如何使用即可,专注于其他核心业务逻辑

3. 组合式API的优点

组合式API有一个很重要的优点,在组合式API中可以实现更灵活的代码组织Vue Hooks: 让Vue开发更简单与高效 在选项式API中,人为地将代码分为了多个模块,非常有益于开发者上手,但是在模块复杂、代码量多的情况下将带来一些问题:

模块复杂的情况下,查阅相同逻辑的内容时,需要反复翻阅组件的内容,对于开发者特别是非原本组件开发者而言,这会极大地加重负担,而如果使用组合式API,因为整个组件都是基于响应式变量以及函数,我们可以把处理相同业务逻辑的内容放在同一个区域,这样可以方便阅读理解,并且在抽离、复用时提供了很大的便利,在大型项目维护中非常重要。

<script lang="ts" setup>
import { getAllForSelect, type UserInfo } from '/@/api'
import { useAutoRequest } from '/@/utils/hooks'

interface Option {
  label: string
  value: string | number
}

const emit = defineEmits(['focus', 'update:modelValue', 'clear', 'blur', 'change'])
const props = defineProps({...})

const selectRef = ref<any>(null)
const entity = reactive({
  value: <string | number>'',
  options: <Array<Option>>[],
})

const { modelValue } = toRefs(props)
const onChange = (v: number | string) => (emit('update:modelValue', v), emit('change', v))
watch(modelValue, val => (entity.value = val), { immediate: true })

// 请求信息
const [loading, getOptions] = useAutoRequest(getAllForSelect, {
  onSuccess: res => entity.options = res.map(...),
})
onBeforeMount(getOptions)

const getFocus = () => emit('focus')
const getBlur = () => emit('blur')
const clickClear = () => emit('clear')

const blur = () => selectRef.value.blur()
const focus = () => selectRef.value.focus()

defineExpose({ blur, focus })
</script>

怎么开始玩Hooks

1. Hooks的各类规范

在开始使用/创建Hook之前,我们需要明白它的一些规范,以下是创建/使用hook时的一些要求:

  1. 通常来讲,一个Hook的命名需要以use开头,比如useTimeOut,这是约定俗成的,开发者看到useXXX即可明白这是一个Hook。Hook的名称需要清楚地表明其功能。

  2. 只在组件生命周期中调用Hook,而不在普通函数中调用Hook (React中规定,但在Hook概念扩大化后,其实并非绝对)

  3. 只在当前关注的最顶级作用域使用Hook,而不要在嵌套函数、循环中调用Hook

补充:

  • 函数必须是纯函数,没有副作用

  • 返回值是一个函数或数据,供外部使用

  • hook内部可以使用其他的hook,组合功能

  • 数据必须依赖于输入,不依赖于外部状态,保持数据流的明确性

  • hook是单一功能的,不要给单一的hook设计过多功能。单个hook只负责做一件事,复杂的功能可以使用多个hook互相组合实现,如果给单个hook增加过多功能,又会陷入过于臃肿、使用成本高、难维护的问题

规范化使用Hook可以使得除了开发者本人之外的其他协作者也可以快速上手他人代码。

2. 如何使用Hooks

在VUE中,使用Hooks时,需要使用组合式API,因而最好在VUE3中使用,VUE2想要使用组合式API则需要配合@vue/composition-api,并且版本需要高于VUE2.6

Hooks的使用十分简单,这也是它们被设计的意义所在,只需引入并调用函数即可。

<script setup lang="ts">
import { useScroll } from '@vueuse/core'

const el = ref<HTMLElement | null>(null)
const { x, y, isScrolling, arrivedState, directions } = useScroll(el)
</script>

<template>
  <div ref="el"></div>
</template>

VUE社区有很多优秀的Hooks库,比如VueUse,它是由部分VUE核心成员开发的VUE Hook库,提供了很多非常好用的hook,查看它的源码也非常有助于开发自己的hook。

3. 如何创建自己的自定义hook

在设计一个定制的hook之前,应当至少明白以下几点:

  • 明确自己想要的功能以及实现的效果
  • 遵守hook的命名规范以及其他注意事项
  • 尽可能好的性能表现以及精简的代码
  • 使用typescript

我们在开发自己的hook时应该明确它的设计目的,遵守各项规范,最好使用typescript,特别是复杂的hook。当一个hook内部较为复杂,配置项较多时,为了避免被错误使用,使用typescript的类型检查是非常有必要的,甚至为了更好的使用体验,应该结合TS类型计算,约束输入以及做到对输出内容的更好的类型提示。

以下是一个简单的分页模块Hook:

export function usePagination(val?: number): [Ref<number>, Ref<number>, computedRef<number>, Ref<number>, () => void] {
  const pageSize = ref(val ?? 20);
  const currentPage = ref(1);
  const total = ref(0);
  const skipCount = computed(() => (currentPage.value - 1) * pageSize.value);

  return [currentPage, pageSize, total, skipCount, reset];

  function reset() {
    currentPage.value = 1;
    total.value = 0;
  }
}

// 使用
const [currentPage, pageSize, totalCount, skipCount, reset] = usePagination();

这样就可以创建一个简单的分页功能hook,只需在需要分页的VUE组件中引入调用usePagination,就可以轻松创建分页模块,高效且清晰。

创建复杂Hook时,需要尽可能地对各种情况做好预先的处理,保证它代码的健壮性。

Hooks在一定程度上可以取代传统的VUE组件

4. 使用TypeScript类型计算的Hook

import m from '/@/utils/message'

type TApiFun<TData, TParams extends Array<any>> = (...params: TParams) => Promise<TData>
interface AutoRequestOptions<T> {
  // 初始状态
  loading?: boolean
  // 成功自动提示
  message?: string
  // 接口调用成功时的回调
  onSuccess?: (data: T) => unknown | Promise<unknown>
  // 接口调用失败时的回调
  onCatch?: (err: Error) => unknown | Promise<unknown>
}
type AutoRequestResult<TData, TParams extends Array<any>> = [Ref<boolean>, TApiFun<TData, TParams>]

/**
 * @description loading状态hooks
 * @param fun 接口方法
 * @param options 配置选项:设置默认loading状态、接口回调与自动提示开关
 * @returns [loading,接口]
 */
export default function useAutoRequest<TData, TParams extends any[] = any[]>(
  fun: TApiFun<TData, TParams>,
  options?: AutoRequestOptions<TData>,
): AutoRequestResult<TData, TParams> {
  const { loading = false, onSuccess, onCatch, message } = options || { loading: false }

  const requestLoading = ref(loading)

  const run: TApiFun<TData, TParams> = (...params) => {
    requestLoading.value = true

    return fun(...params)
      .then(async res => {
        onSuccess && (await onSuccess(res))
        message && m.success(message)
        return res
      })
      .catch(async err => {
        onCatch && (await onCatch(err))
        throw new Error(err)
      })
      .finally(() => {
        requestLoading.value = false
      })
  }

  return [requestLoading, run]
}

经过处理后的useAutoRequest可以在外部使用时自动推断onSuccess回调参数的类型:

Vue Hooks: 让Vue开发更简单与高效

5. Hooks与普通工具函数的区别

简单来讲,Hooks是创建工具的工具而普通工具函数则是纯粹的工具。实际上根据开发者的喜好,一个普通的工具函数也可以被创建成Hooks的形式,但并不是很有必要,因为作为工具本身它已经很好用了,一定要包装成Hook反而多饶了一层。

什么情况下适合创建为Hooks呢?

  • 具有一定泛用性的功能
  • 具有一定复杂度,需要外部提供初始条件,由模块内部进行状态管理的功能
  • 若干相关的、共享状态的业务功能
  • 等等

总结

Hooks是VUE3中利用组合式API响应式的特性的,实现简单高效的逻辑复用、提高开发效率、提高VUE模块可维护性的工具。Hooks的组合可以让组件以低代价、高效率地实现高复杂度业务,Hooks之间通常相互独立,没有过度耦合,通常不会有后期的维护地狱而且可以使得功能模块更加易于测试。

使用开源的Hook将为开发带来很多便利,而开发自定义Hook则需要花费一些时间,但在实现后,高度的定制化将为项目开发带来巨大的便利。

参考文章/拓展阅读