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/