Vue3组合式函数封装实战:从重复逻辑到可复用Composables

Vue3组合式函数的设计原则

Vue3前端开发中,组合式函数(Composables)是代码复用的核心手段。和Vue2的Mixin相比,Composables具备明确的输入输出、无命名冲突、可追踪数据来源三大优势。一个设计良好的Composable应当遵循以下原则:单一职责、参数是ref响应式的、返回值包含响应式状态和方法、副作用在内部管理生命周期。

网络请求Composable的标准实现

几乎所有Vue3项目都需要处理异步数据获取。下面是一个生产级的useFetch实现:

// composables/useFetch.ts
import { ref, watch, type Ref } from 'vue'

interface UseFetchOptions<T> {
  immediate?: boolean
  initialData?: T
  refetch?: boolean
  transform?: (data: unknown) => T
  onError?: (error: Error) => void
}

interface UseFetchReturn<T> {
  data: Ref<T | undefined>
  error: Ref<Error | null>
  loading: Ref<boolean>
  execute: () => Promise<void>
}

export function useFetch<T>(
  url: Ref<string> | string,
  options: UseFetchOptions<T> = {}
): UseFetchReturn<T> {
  const { immediate = true, initialData, transform, onError } = options
  
  const data = ref<T | undefined>(initialData)
  const error = ref<Error | null>(null)
  const loading = ref(false)
  
  let controller: AbortController | null = null
  
  const execute = async () => {
    const resolvedUrl = typeof url === 'string' ? url : url.value
    
    // 取消上一个未完成请求
    controller?.abort()
    controller = new AbortController()
    
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(resolvedUrl, {
        signal: controller.signal
      })
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }
      
      const rawData = await response.json()
      data.value = transform ? transform(rawData) : rawData as T
    } catch (e) {
      if (e instanceof DOMException && e.name === 'AbortError') return
      error.value = e as Error
      onError?.(e as Error)
    } finally {
      loading.value = false
    }
  }
  
  // 响应式URL变化自动重新请求
  if (typeof url !== 'string') {
    watch(url, execute)
  }
  
  if (immediate) {
    execute()
  }
  
  return { data, error, loading, execute }
}

这个实现覆盖了生产环境的核心需求:竞态请求处理(AbortController)、响应式URL监听、数据转换、错误回调。使用方式非常简洁:

// 在组件中使用
const { data: users, loading, error } = useFetch<User[]>(
  computed(() => `/api/users?page=${page.value}`),
  { transform: (d) => d.items }
)

表单验证Composable的完整方案

表单验证是前端开发中重复度最高的逻辑之一。封装为Composable后,各表单组件只需声明规则:

// composables/useForm.ts
import { reactive, ref, type Ref } from 'vue'

type ValidationRule<T> = {
  validator: (value: T, form: Record<string, unknown>) => boolean | string
  message: string
}

type FieldConfig<T> = {
  value: Ref<T>
  rules: ValidationRule<T>[]
}

export function useForm<T extends Record<string, unknown>>(
  fields: { [K in keyof T]: FieldConfig<T[K]> }
) {
  const errors = reactive<Record<string, string>>({})
  const touched = reactive<Record<string, boolean>>({})
  const isSubmitting = ref(false)
  
  const validateField = async (key: string) => {
    const field = fields[key]
    if (!field) return true
    
    touched[key] = true
    delete errors[key]
    
    for (const rule of field.rules) {
      const result = rule.validator(field.value.value, 
        Object.fromEntries(
          Object.entries(fields).map(([k, f]) => [k, f.value.value])
        )
      )
      if (result !== true) {
        errors[key] = typeof result === 'string' ? result : rule.message
        return false
      }
    }
    return true
  }
  
  const validate = async () => {
    let valid = true
    for (const key of Object.keys(fields)) {
      if (!(await validateField(key))) {
        valid = false
      }
    }
    return valid
  }
  
  const handleSubmit = async (callback: () => Promise<void>) => {
    isSubmitting.value = true
    const valid = await validate()
    if (valid) {
      try {
        await callback()
      } finally {
        isSubmitting.value = false
      }
    } else {
      isSubmitting.value = false
    }
  }
  
  const resetValidation = () => {
    Object.keys(errors).forEach(k => delete errors[k])
    Object.keys(touched).forEach(k => delete touched[k])
  }
  
  return { errors, touched, isSubmitting, validate, validateField, handleSubmit, resetValidation }
}

组件中的使用示例:

const username = ref('')
const password = ref('')

const { errors, handleSubmit, isSubmitting } = useForm({
  username: {
    value: username,
    rules: [
      { validator: (v) => v.length >= 3, message: '用户名至岑3个字符' },
      { validator: (v) => /^[a-zA-Z0-9_]+$/.test(v), message: '仅支持字母数字下划线' }
    ]
  },
  password: {
    value: password,
    rules: [
      { validator: (v) => v.length >= 8, message: '密码至岑8个字符' },
      { validator: (v) => /(?=.*[A-Z])(?=.*[0-9])/.test(v), message: '需含大写字母和数字' }
    ]
  }
})

const onSubmit = handleSubmit(async () => {
  await api.login({ username: username.value, password: password.value })
})

Composables组合与依赖管理

Composables之间可以组合形成更复杂的功能。关键是避免循环依赖和共享状态污染:

// composables/usePaginatedList.ts - 组合useFetch实现分页
import { computed, ref } from 'vue'
import { useFetch } from './useFetch'

export function usePaginatedList<T>(baseUrl: string) {
  const page = ref(1)
  const pageSize = ref(20)
  
  const url = computed(() => 
    `${baseUrl}?page=${page.value}&size=${pageSize.value}`
  )
  
  const { data, loading, error, execute: refresh } = useFetch<{ 
    items: T[], total: number 
  }>(url, { initialData: { items: [], total: 0 } })
  
  const totalPages = computed(() => 
    Math.ceil((data.value?.total ?? 0) / pageSize.value)
  )
  
  const nextPage = () => {
    if (page.value < totalPages.value) page.value++
  }
  
  const prevPage = () => {
    if (page.value > 1) page.value--
  }
  
  return { 
    items: computed(() => data.value?.items ?? []),
    page, pageSize, totalPages, loading, error, 
    nextPage, prevPage, refresh
  }
}

Vue3 Composables的最佳实践清单

实际项目中,好的Composables设计能显著降低组件复杂度。命名统一以use开头,文件放在src/composables/目录下按功能分组。始终将Composable的调用放在setup函数或script setup的顶层,不要在条件语句中调用以确保生命周期钩子正确注册。对外的接口保持精简,内部状态不要暴露。为复杂的Composable编写单元测试,用vi.fn()模拟依赖比挂载组件更高效。遇到跨组件共享状态的需求,优先使用Composables而非Pinia store——Composables更轻量且作用域可控。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-han-shu-feng-zhuang-shi-zhan-cong-chong-fu/

(0)
小编小编
上一篇 1天前
下一篇 1天前

相关推荐