Vue3组合式API实战:自定义Hook函数封装与响应式数据管理

Vue3组合式API(Composition API)为前端开发提供了更灵活的逻辑组织和复用方案。通过自定义Hook函数(Composables),开发者可以将组件中的响应式状态、计算属性和副作用逻辑抽离为独立模块,实现跨组件复用。本文围绕Vue3.4+版本,系统讲解自定义Hook的封装模式、响应式数据管理策略以及生产环境的最佳实践。

组合式API核心响应式原理

Vue3的响应式系统基于Proxy实现,通过reactive、ref、computed三个核心API构建响应式数据流。理解响应式原理是编写高质量Hook函数的前提。

import { ref, reactive, computed, watch } from 'vue'

// ref:基本类型响应式
const count = ref(0)
count.value++  // 通过.value访问和修改

// reactive:对象类型响应式
const state = reactive({
  user: { name: '张三', age: 25 },
  list: []
})
state.user.name = '李四'  // 直接修改

// computed:派生响应式
const doubleCount = computed(() => count.value * 2)

// watch:响应式副作用
watch(count, (newVal, oldVal) => {
  console.log(`count: ${oldVal} -> ${newVal}`)
})

自定义Hook函数封装模式

自定义Hook本质上是一个以”use”为前缀的函数,内部使用组合式API封装逻辑,返回响应式状态和操作方法。标准封装模式如下:

// hooks/useCounter.ts
import { ref, computed } from 'vue'

export function useCounter(initialValue: number = 0, step: number = 1) {
  const count = ref(initialValue)
  const double = computed(() => count.value * 2)

  function increment() {
    count.value += step
  }

  function decrement() {
    count.value -= step
  }

  function reset() {
    count.value = initialValue
  }

  return {
    count,
    double,
    increment,
    decrement,
    reset
  }
}

在组件中使用:

<script setup lang="ts">
import { useCounter } from '@/hooks/useCounter'

const { count, double, increment, decrement } = useCounter(0, 2)
</script>

<template>
  <div>
    <p>Count: {{ count }} | Double: {{ double }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </div>
</template>

异步数据请求Hook封装

数据请求是前端开发中最高频的场景。封装通用的useFetch Hook,统一处理loading、error和data状态:

// hooks/useFetch.ts
import { ref, watchEffect, isRef, unref } from 'vue'

interface UseFetchOptions<T> {
  immediate?: boolean
  initialData?: T
  refetch?: boolean
}

export function useFetch<T>(
  url: string | Ref<string>,
  options: UseFetchOptions<T> = {}
) {
  const {
    immediate = true,
    initialData = null as T,
    refetch = false
  } = options

  const data = ref<T>(initialData)
  const error = ref<string | null>(null)
  const loading = ref(false)

  async function execute() {
    loading.value = true
    error.value = null
    try {
      const response = await fetch(unref(url), {
        headers: { 'Content-Type': 'application/json' }
      })
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`)
      }
      data.value = await response.json()
    } catch (err) {
      error.value = err instanceof Error ? err.message : String(err)
    } finally {
      loading.value = false
    }
  }

  if (immediate) execute()

  if (refetch && isRef(url)) {
    watchEffect(() => execute())
  }

  return { data, error, loading, execute, refresh: execute }
}

表单验证Hook封装

表单验证是另一个高频场景。封装响应式表单验证Hook,支持实时校验和提交校验:

// hooks/useForm.ts
import { reactive, computed } from 'vue'

type Validator = (value: any) => string | true

export function useForm<T extends Record<string, any>>(
  initialValues: T,
  validators: Partial<Record<keyof T, Validator[]>>
) {
  const values = reactive({ ...initialValues }) as T
  const errors = reactive({} as Record<keyof T, string>)
  const touched = reactive({} as Record<keyof T, boolean>)

  function validateField(field: keyof T): boolean {
    const fieldValidators = validators[field]
    if (!fieldValidators) return true

    for (const validator of fieldValidators) {
      const result = validator(values[field])
      if (result !== true) {
        errors[field] = result
        return false
      }
    }
    errors[field] = ''
    return true
  }

  function validateAll(): boolean {
    let valid = true
    for (const field in validators) {
      if (!validateField(field)) valid = false
    }
    return valid
  }

  function touch(field: keyof T) {
    touched[field] = true
    validateField(field)
  }

  function reset() {
    Object.assign(values, initialValues)
    Object.keys(errors).forEach(k => delete errors[k as keyof T])
    Object.keys(touched).forEach(k => delete touched[k as keyof T])
  }

  const isValid = computed(() => {
    return Object.keys(validators).every(f => validateField(f as keyof T))
  })

  return { values, errors, touched, isValid, validateField, validateAll, touch, reset }
}

使用示例:

const { values, errors, validateAll, touch } = useForm(
  { username: '', email: '', password: '' },
  {
    username: [
      (v) => v.length >= 3 || '用户名至少3个字符',
      (v) => /^[a-zA-Z0-9_]+$/.test(v) || '用户名只能包含字母数字下划线'
    ],
    email: [
      (v) => /^[^@]+@[^@]+\.[^@]+$/.test(v) || '邮箱格式不正确'
    ],
    password: [
      (v) => v.length >= 8 || '密码至少8位',
      (v) => /[A-Z]/.test(v) || '密码需包含大写字母'
    ]
  }
)

本地存储响应式Hook

将localStorage封装为响应式Hook,数据变化自动同步到浏览器存储:

// hooks/useStorage.ts
import { ref, watch, type Ref } from 'vue'

export function useStorage<T>(
  key: string,
  defaultValue: T,
  storage: Storage = localStorage
): Ref<T> {
  const stored = storage.getItem(key)
  const data = ref<T>(stored ? JSON.parse(stored) : defaultValue) as Ref<T>

  watch(data, (newVal) => {
    try {
      storage.setItem(key, JSON.stringify(newVal))
    } catch (e) {
      console.error('Storage write failed:', e)
    }
  }, { deep: true })

  return data
}

防抖节流Hook封装

搜索输入、窗口resize等高频事件场景需要防抖/节流处理:

// hooks/useDebounce.ts
import { ref, watch, onUnmounted, type Ref } from 'vue'

export function useDebounce<T>(source: Ref<T>, delay: number = 300): Ref<T> {
  const debounced = ref(source.value) as Ref<T>
  let timer: ReturnType<typeof setTimeout>

  watch(source, (newVal) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      debounced.value = newVal
    }, delay)
  })

  onUnmounted(() => clearTimeout(timer))

  return debounced
}

// 使用
const keyword = ref('')
const debouncedKeyword = useDebounce(keyword, 500)

watch(debouncedKeyword, (val) => {
  if (val) searchAPI(val)
})

组件生命周期与Hook注意事项

自定义Hook中使用的onMounted、onUnmounted等生命周期钩子,必须在setup函数或组件编译过程中同步调用。不能在异步回调中调用生命周期钩子。

正确用法:

// hooks/useEventListener.ts
import { onMounted, onUnmounted } from 'vue'

export function useEventListener(
  target: EventTarget,
  event: string,
  callback: (e: Event) => void
) {
  onMounted(() => target.addEventListener(event, callback))
  onUnmounted(() => target.removeEventListener(event, callback))
}

错误用法(在异步函数中调用生命周期钩子):

// 错误示范
export function useWrongHook() {
  setTimeout(() => {
    onMounted(() => {})  // 报错:onMounted is called when there is no active component instance
  }, 100)
}

TypeScript类型安全增强

生产环境建议为所有Hook函数添加完整的TypeScript类型定义。使用泛型约束确保类型推导的准确性:

// hooks/usePagination.ts
import { ref, computed } from 'vue'

interface PaginationOptions {
  pageSize: number
  total: number
  initialPage?: number
}

export function usePagination(options: PaginationOptions) {
  const currentPage = ref(options.initialPage ?? 1)
  const pageSize = ref(options.pageSize)
  const total = ref(options.total)

  const totalPages = computed(() =>
    Math.ceil(total.value / pageSize.value)
  )

  const hasNext = computed(() => currentPage.value < totalPages.value)
  const hasPrev = computed(() => currentPage.value > 1)

  const range = computed(() => {
    const start = (currentPage.value - 1) * pageSize.value
    const end = Math.min(start + pageSize.value, total.value)
    return { start, end }
  })

  function goTo(page: number) {
    currentPage.value = Math.max(1, Math.min(page, totalPages.value))
  }

  function next() { goTo(currentPage.value + 1) }
  function prev() { goTo(currentPage.value - 1) }

  return { currentPage, pageSize, total, totalPages, hasNext, hasPrev, range, goTo, next, prev }
}

Hook函数组织与目录结构

推荐的项目Hook目录结构:

src/
├── hooks/
│   ├── useCounter.ts       # 计数器
│   ├── useFetch.ts         # 数据请求
│   ├── useForm.ts          # 表单验证
│   ├── useStorage.ts       # 本地存储
│   ├── useDebounce.ts      # 防抖
│   ├── useEventListener.ts # 事件监听
│   ├── usePagination.ts    # 分页
│   └── useTheme.ts         # 主题切换
├── components/
└── views/

每个Hook文件保持单一职责,文件名以”use”开头,导出同名函数。跨项目复用的Hook可以发布为独立的npm包,按功能域划分(如@hooks/use-form、@hooks/use-fetch)。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-shi-zhan-zi-ding-yi-hook-han-shu-feng/

(0)
小编小编
上一篇 22小时前
下一篇 22小时前

相关推荐