Vue3组合式API工程化实践:自定义Hook设计与TypeScript类型推导

Vue3的组合式API(Composition API)改变了组件逻辑的组织方式,将同一功能关注点的代码集中在一起。自定义Hook(Composables)是组合式API的核心实践,将可复用的响应式逻辑从组件中抽离为独立函数。结合TypeScript的泛型推导,能实现类型安全的Hook复用。本文从基础Hook设计到复杂场景的异步状态管理,给出完整的工程化实现。

自定义Hook的基本结构设计

Vue3自定义Hook命名约定以use开头,返回响应式数据和方法。一个规范的Hook应包含状态定义、副作用处理和返回值导出三个部分:

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

export interface CounterOptions {
  initial?: number
  min?: number
  max?: number
  step?: number
}

export function useCounter(options: CounterOptions = {}) {
  const { initial = 0, min = -Infinity, max = Infinity, step = 1 } = options

  const count = ref(initial)
  const isMin = computed(() => count.value <= min)
  const isMax = computed(() => count.value >= max)

  function increment(n: number = step) {
    count.value = Math.min(count.value + n, max)
  }

  function decrement(n: number = step) {
    count.value = Math.max(count.value - n, min)
  }

  function reset() {
    count.value = initial
  }

  watch(
    () => [min, max],
    ([newMin, newMax]) => {
      if (count.value < newMin) count.value = newMin
      if (count.value > newMax) count.value = newMax
    }
  )

  return { count, isMin, isMax, increment, decrement, reset }
}

返回值中的refcomputed不能被解构后丢失响应性。在组件中使用时需通过.value访问,或在template中自动解包。如果需要解构使用,用toRefs包装返回对象。

异步数据请求Hook的类型安全实现

异步数据获取是前端最常见的场景。一个完善的异步数据Hook需要处理加载状态、错误处理、请求取消和重试机制:

// composables/useAsync.ts
import { ref, shallowRef, onUnmounted, type Ref } from 'vue'

export interface UseAsyncOptions {
  immediate?: boolean
  retries?: number
  retryDelay?: number
  onSuccess?: (data: T) => void
  onError?: (error: Error) => void
}

export interface UseAsyncResult {
  data: Ref
  loading: Ref
  error: Ref
  execute: () => Promise
  abort: () => void
}

export function useAsync(
  fn: (signal: AbortSignal) => Promise,
  options: UseAsyncOptions = {}
): UseAsyncResult {
  const { immediate = true, retries = 3, retryDelay = 1000, onSuccess, onError } = options

  const data = shallowRef(null)
  const loading = ref(false)
  const error = ref(null)

  let abortController: AbortController | null = null
  let retryCount = 0

  async function execute() {
    loading.value = true
    error.value = null

    abortController?.abort()
    abortController = new AbortController()

    try {
      const result = await fn(abortController.signal)
      data.value = result
      retryCount = 0
      onSuccess?.(result)
    } catch (e) {
      if (e instanceof DOMException && e.name === 'AbortError') return
      if (retryCount < retries) {
        retryCount++
        setTimeout(() => execute(), retryDelay)
        return
      }
      error.value = e as Error
      onError?.(e as Error)
    } finally {
      loading.value = false
    }
  }

  function abort() {
    abortController?.abort()
    loading.value = false
  }

  onUnmounted(() => abortController?.abort())

  if (immediate) execute()

  return { data, loading, error, execute, abort }
}

使用shallowRef而非ref存储异步数据。如果返回的是大型对象或数组,ref会深度递归代理整个数据结构,造成不必要的性能开销。shallowRef只代理.value本身,内部数据保持原始引用。

泛型Tfn的返回类型自动推导,无需手动指定:

// 类型自动推导为 UseAsyncResult
const { data, loading, error } = useAsync(async (signal) => {
  const res = await fetch('/api/user/profile', { signal })
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
  return res.json() as Promise
})

表单验证Hook的响应式设计

表单验证是前端高频需求。将验证逻辑抽离为Hook,实现声明式校验:

// composables/useForm.ts
import { reactive, computed, type UnwrapNestedRefs } from 'vue'

export type Validator = (value: T, formData: Record) => string | true
export type ValidationSchema = { [K in keyof T]?: Validator[] }

export function useForm>(
  initialValues: T,
  schema: ValidationSchema
) {
  const values = reactive({ ...initialValues }) as UnwrapNestedRefs
  const errors = reactive({}) as Record
  const touched = reactive({}) as Record

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

    const value = values[field]
    for (const validator of validators) {
      const result = validator(value, values)
      if (result !== true) {
        errors[field] = result
        return false
      }
    }
    delete errors[field]
    return true
  }

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

  function setField(field: keyof T, value: any) {
    values[field] = value
    touched[field] = true
    if (errors[field]) 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(() => Object.keys(errors).length === 0)
  const isDirty = computed(() => Object.keys(touched).some(k => touched[k as keyof T]))

  return { values, errors, touched, isValid, isDirty, validateField, validateAll, setField, reset }
}

// 使用示例
interface LoginForm { email: string; password: string }

const form = useForm(
  { email: '', password: '' },
  {
    email: [
      (v) => v.trim() === '' ? '邮箱不能为空' : true,
      (v) => /^[^@]+@[^@]+\.[^@]+$/.test(v) ? true : '邮箱格式不正确'
    ],
    password: [
      (v) => v === '' ? '密码不能为空' : true,
      (v) => v.length >= 8 ? true : '密码至少8位'
    ]
  }
)

验证函数的类型约束Validator<T>确保每个字段的验证器接收正确类型的值。ValidationSchema<T>使用映射类型,schema的字段必须与表单数据结构T的key一一对应,TypeScript编译器在编写阶段就能发现字段名拼写错误。

跨组件状态共享的Hook模式

当多个组件需要共享同一份状态时,使用provide/inject配合组合式函数实现轻量级状态管理,避免引入完整的状态管理库:

// composables/useTheme.ts
import { ref, provide, inject, type InjectionKey, type Ref } from 'vue'

export type Theme = 'light' | 'dark' | 'auto'

interface ThemeContext {
  theme: Ref
  resolvedTheme: Ref<'light' | 'dark'>
  setTheme: (t: Theme) => void
  toggleTheme: () => void
}

export const ThemeKey: InjectionKey = Symbol('theme')

export function provideTheme(initial: Theme = 'auto'): ThemeContext {
  const theme = ref(initial)
  const resolvedTheme = ref<'light' | 'dark'>('light')

  function resolve() {
    if (theme.value === 'auto') {
      resolvedTheme.value = window.matchMedia('(prefers-color-scheme: dark)').matches
        ? 'dark' : 'light'
    } else {
      resolvedTheme.value = theme.value
    }
    document.documentElement.setAttribute('data-theme', resolvedTheme.value)
  }

  function setTheme(t: Theme) {
    theme.value = t
    localStorage.setItem('theme', t)
    resolve()
  }

  function toggleTheme() {
    setTheme(resolvedTheme.value === 'light' ? 'dark' : 'light')
  }

  window.matchMedia('(prefers-color-scheme: dark)')
    .addEventListener('change', () => { if (theme.value === 'auto') resolve() })

  const saved = localStorage.getItem('theme') as Theme | null
  if (saved) theme.value = saved

  resolve()

  const context: ThemeContext = { theme, resolvedTheme, setTheme, toggleTheme }
  provide(ThemeKey, context)
  return context
}

export function useTheme(): ThemeContext {
  const context = inject(ThemeKey)
  if (!context) throw new Error('useTheme() must be used within a component that calls provideTheme()')
  return context
}

使用InjectionKey作为provide/inject的key,确保注入值的类型安全。父组件调用provideTheme()提供状态,所有子孙组件通过useTheme()注入同一份响应式状态。避免了Pinia等状态管理库的学习成本和打包体积,同时保持了完整的TypeScript类型推导。

Hook的单元测试策略

自定义Hook应与组件一样编写单元测试。使用@vue/test-utilswithSetup辅助函数在测试环境中运行Hook:

// tests/useForm.test.ts
import { describe, it, expect } from 'vitest'
import { withSetup } from './helpers'
import { useForm } from '@/composables/useForm'

describe('useForm', () => {
  it('验证必填字段', () => {
    const { validateField, errors } = withSetup(() => useForm(
      { name: '' },
      { name: [(v) => v ? true : '必填'] }
    ))
    
    expect(validateField('name')).toBe(false)
    expect(errors.name).toBe('必填')
  })

  it('validateAll返回整体校验结果', () => {
    const { validateAll } = withSetup(() => useForm(
      { name: '', age: 25 },
      {
        name: [(v) => v ? true : '必填'],
        age: [(v) => v >= 18 ? true : '需年满18岁']
      }
    ))
    
    expect(validateAll()).toBe(false)
  })
})

测试覆盖验证规则触发、错误消息生成、整体校验结果三个维度。异步Hook的测试需配合vi.useFakeTimers()控制定时器,验证重试逻辑和超时处理。

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

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

相关推荐