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前缀,如useUser、useCart、useTheme。
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代码复用的最佳实践,通过本文的三个案例,相信你已经掌握了其核心用法。在实际项目中,建议:
- 从简单场景开始,逐步封装通用逻辑
- 保持函数职责单一,便于测试和维护
- 充分利用TypeScript类型系统,提升开发体验
- 建立项目的Composables目录规范
💡 提示:VueUse(vueuse.org)提供了大量高质量的Composables实现,推荐作为参考学习和项目依赖。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END
















暂无评论内容