Vue3组合式函数Composables实战:从入门到精通,打造高复用逻辑组件

Vue3的组合式API为我们带来了全新的代码组织方式,而Composables(组合式函数)正是这套API的核心精髓。本文将带你深入理解Composables的设计理念,并通过三个实战案例掌握其使用技巧。

一、什么是Composables?

Composables是利用Vue3组合式API封装的、可复用的状态逻辑函数。它类似于React Hooks,但拥有Vue独特的响应式特性。一个好的Composable应该具备:

  • 单一职责:每个函数只负责一个特定功能
  • 响应式封装:返回ref或reactive对象
  • 生命周期管理:自动清理副作用
  • 类型友好:完整的TypeScript类型支持

二、实战案例1:useRequest – 请求状态管理

在业务开发中,我们经常需要处理接口请求的loading、error、data等状态。下面封装一个通用的请求管理Composable:

// composables/useRequest.ts
import { ref, Ref, unref, watchEffect, toValue } from 'vue'

interface UseRequestOptions {
  immediate?: boolean      // 是否立即执行
  initialData?: T          // 初始数据
  onSuccess?: (data: T) => void
  onError?: (error: Error) => void
}

interface UseRequestReturn {
  data: Ref
  loading: Ref
  error: Ref
  execute: () => Promise
}

export function useRequest(
  fetcher: () => Promise,
  options: UseRequestOptions = {}
): UseRequestReturn {
  const { immediate = true, initialData, onSuccess, onError } = options
  
  const data = ref(initialData) as Ref
  const loading = ref(false)
  const error = ref(null)
  
  const execute = async () => {
    loading.value = true
    error.value = null
    
    try {
      const result = await fetcher()
      data.value = result
      onSuccess?.(result)
    } catch (e) {
      error.value = e as Error
      onError?.(e as Error)
    } finally {
      loading.value = false
    }
  }
  
  if (immediate) {
    execute()
  }
  
  return { data, loading, error, execute }
}

使用示例:

<template>
  <div v-if="loading">加载中...</div>
  <div v-else-if="error">{{ error.message }}</div>
  <div v-else>
    <ul>
      <li v-for="user in data" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { useRequest } from '@/composables/useRequest'

interface User {
  id: number
  name: string
}

const { data, loading, error } = useRequest<User[]>(
  () => fetch('/api/users').then(r => r.json()),
  {
    initialData: [],
    onError: (e) => console.error('请求失败:', e)
  }
)
</script>

三、实战案例2:useForm – 表单验证管理

表单是业务开发中的高频场景,下面封装一个支持验证的表单Composable:

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

type Validator = (value: any) => string | boolean
type FormRules = Record<string, Validator[]>
type FormErrors = Record<string, string>

interface UseFormOptions {
  rules?: FormRules
  onSubmit?: (values: Record<string, any>) => void | Promise<void>
}

interface UseFormReturn {
  values: Record<string, any>
  errors: Ref<FormErrors>
  touched: Record<string, boolean>
  isValid: Ref<boolean>
  setValue: (field: string, value: any) => void
  setTouched: (field: string) => void
  validate: (field?: string) => boolean
  validateAll: () => boolean
  reset: () => void
  handleSubmit: () => Promise<void>
}

export function useForm(
  initialValues: Record<string, any> = {},
  options: UseFormOptions = {}
): UseFormReturn {
  const { rules = {}, onSubmit } = options
  
  const values = reactive({ ...initialValues })
  const touched = reactive<Record<string, boolean>>({})
  const errors = ref<FormErrors>({})
  
  const isValid = computed(() => {
    return Object.values(errors.value).every(e => !e)
  })
  
  const validate = (field?: string): boolean => {
    const fieldsToValidate = field ? [field] : Object.keys(values)
    
    fieldsToValidate.forEach(key => {
      const fieldRules = rules[key] || []
      const value = values[key]
      
      for (const rule of fieldRules) {
        const result = rule(value)
        if (result !== true) {
          errors.value[key] = result as string
          return
        }
      }
      errors.value[key] = ''
    })
    
    return field ? !errors.value[field] : isValid.value
  }
  
  const validateAll = () => {
    Object.keys(values).forEach(key => validate(key))
    return isValid.value
  }
  
  const setValue = (field: string, value: any) => {
    values[field] = value
    if (touched[field]) {
      validate(field)
    }
  }
  
  const setTouched = (field: string) => {
    touched[field] = true
    validate(field)
  }
  
  const reset = () => {
    Object.assign(values, initialValues)
    Object.keys(touched).forEach(k => touched[k] = false)
    errors.value = {}
  }
  
  const handleSubmit = async () => {
    validateAll()
    if (isValid.value) {
      await onSubmit?.(values)
    }
  }
  
  return {
    values,
    errors,
    touched,
    isValid,
    setValue,
    setTouched,
    validate,
    validateAll,
    reset,
    handleSubmit
  }
}

常用验证规则:

// utils/validators.ts
export const required = (msg = '此项为必填') => 
  (v: any) => (v !== null && v !== undefined && v !== '') || msg

export const email = (msg = '请输入正确的邮箱格式') => 
  (v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || msg

export const minLength = (min: number) => (msg?: string) => 
  (v: string) => v.length >= min || (msg || `最少${min}个字符`)

export const maxLength = (max: number) => (msg?: string) => 
  (v: string) => v.length <= max || (msg || `最多${max}个字符`)

export const pattern = (regex: RegExp, msg: string) => 
  (v: string) => regex.test(v) || msg

使用示例:

<template>
  <form @submit.prevent="handleSubmit">
    <div class="form-item">
      <input 
        v-model="values.username"
        @blur="setTouched('username')"
        placeholder="用户名"
      />
      <span v-if="errors.username" class="error">{{ errors.username }}</span>
    </div>
    
    <div class="form-item">
      <input 
        v-model="values.email"
        @blur="setTouched('email')"
        placeholder="邮箱"
      />
      <span v-if="errors.email" class="error">{{ errors.email }}</span>
    </div>
    
    <button type="submit" :disabled="!isValid">提交</button>
  </form>
</template>

<script setup lang="ts">
import { useForm } from '@/composables/useForm'
import { required, email, minLength } from '@/utils/validators'

const { values, errors, touched, isValid, setTouched, handleSubmit } = useForm(
  { username: '', email: '' },
  {
    rules: {
      username: [required(), minLength(3)()],
      email: [required(), email()]
    },
    onSubmit: async (values) => {
      console.log('提交数据:', values)
      // 调用API
    }
  }
)
</script>

四、实战案例3:useLocalStorage - 本地存储响应式封装

将localStorage封装为响应式对象,自动同步数据:

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

type Serializer<T> = {
  read: (v: string) => T
  write: (v: T) => string
}

const defaultSerializer: Serializer<any> = {
  read: (v) => JSON.parse(v),
  write: (v) => JSON.stringify(v)
}

export function useLocalStorage<T>(
  key: string,
  initialValue: T,
  serializer: Serializer<T> = defaultSerializer
): Ref<T> {
  const storedValue = localStorage.getItem(key)
  const data = ref<T>(
    storedValue !== null ? serializer.read(storedValue) : initialValue
  ) as Ref<T>
  
  watch(
    data,
    (newValue) => {
      if (newValue === null || newValue === undefined) {
        localStorage.removeItem(key)
      } else {
        localStorage.setItem(key, serializer.write(newValue))
      }
    },
    { deep: true }
  )
  
  return data
}

// 使用示例
const theme = useLocalStorage('theme', 'light')
const user = useLocalStorage('user', { name: '', id: 0 })

// 直接修改,自动同步到localStorage
theme.value = 'dark'
user.value.name = 'John'

五、Composables最佳实践

1. 命名规范

统一使用use前缀,如useUseruseCartuseTheme

2. 返回值解构

推荐返回对象而非数组,便于IDE提示和按需使用:

// ✅ 推荐
const { data, loading, error } = useRequest(fetcher)

// ❌ 不推荐
const [data, loading, error] = useRequest(fetcher)

3. 副作用清理

使用onScopeDispose或返回清理函数:

export function useEventListener(target: EventTarget, event: string, callback: Function) {
  const handler = (e: Event) => callback(e)
  target.addEventListener(event, handler)
  
  onScopeDispose(() => {
    target.removeEventListener(event, handler)
  })
}

4. 组合使用

Composables之间可以相互组合,构建复杂逻辑:

export function useUser() {
  const { data, loading, error } = useRequest(() => fetchUser())
  const savedUser = useLocalStorage('cachedUser', null)
  
  watch(data, (newData) => {
    if (newData) {
      savedUser.value = newData
    }
  })
  
  return { 
    user: computed(() => data.value || savedUser.value),
    loading,
    error 
  }
}

六、总结

Composables是Vue3代码复用的最佳实践,通过本文的三个案例,相信你已经掌握了其核心用法。在实际项目中,建议:

  1. 从简单场景开始,逐步封装通用逻辑
  2. 保持函数职责单一,便于测试和维护
  3. 充分利用TypeScript类型系统,提升开发体验
  4. 建立项目的Composables目录规范

💡 提示:VueUse(vueuse.org)提供了大量高质量的Composables实现,推荐作为参考学习和项目依赖。

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容