组合式函数在Vue3工程中解决什么问题
Vue3的组合式API(Composition API)相比选项式API最大的优势在于逻辑复用。Options API里同一个功能的data、computed、watch分散在多个选项中,跨组件复用只能靠mixin——而mixin存在命名冲突和来源不透明的致命缺陷。组合式函数(Composable)把相关状态和逻辑封装为独立函数,显式导入导出,彻底解决了mixin的问题。
一个合格的Composable需要满足三个条件:响应式数据可被消费者控制、副作用可清理、类型推断完整。
基础模式:带请求状态的数据拉取Composable
最常见的场景是异步数据获取,手动管理loading/error状态非常繁琐。封装一个通用方案:
import { ref, shallowRef, type Ref } from 'vue'
interface UseFetchOptions<T> {
immediate?: boolean
initialData?: T
refetch?: boolean
}
interface UseFetchReturn<T> {
data: Ref<T | undefined>
error: Ref<Error | null>
isLoading: Ref<boolean>
execute: () => Promise<void>
}
export function useFetch<T>(
url: string | Ref<string>,
options: UseFetchOptions<T> = {}
): UseFetchReturn<T> {
const data = shallowRef<T | undefined>(options.initialData)
const error = ref<Error | null>(null)
const isLoading = ref(false)
const execute = async () => {
isLoading.value = true
error.value = null
try {
const resolvedUrl = typeof url === 'string' ? url : url.value
const res = await fetch(resolvedUrl)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = await res.json()
} catch (e) {
error.value = e instanceof Error ? e : new Error(String(e))
} finally {
isLoading.value = false
}
}
if (options.immediate !== false) {
execute()
}
return { data, error, isLoading, execute }
}
关键细节:data用shallowRef而非ref,避免大对象的深层响应式开销;url参数支持Ref类型,响应式URL变化时可以配合watch自动重新请求。
进阶模式:带防抖和取消的搜索Composable
搜索场景需要防抖、取消上一次请求、响应式参数联动。这是Composable设计能力的试金石:
import { ref, watch, onUnmounted, type Ref } from 'vue'
export function useDebouncedSearch<T>(
searchFn: (query: string) => Promise<T[]>,
query: Ref<string>,
debounceMs = 300
) {
const results = shallowRef<T[]>( [])
const isSearching = ref(false)
let abortController: AbortController | null = null
let timer: ReturnType<typeof setTimeout>
const search = async (q: string) => {
// 取消上一次请求
abortController?.abort()
abortController = new AbortController()
if (!q.trim()) {
results.value = []
return
}
isSearching.value = true
try {
results.value = await searchFn(q)
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') return
throw e
} finally {
isSearching.value = false
}
}
watch(query, (newVal) => {
clearTimeout(timer)
timer = setTimeout(() => search(newVal), debounceMs)
}, { immediate: true })
// 组件卸载时清理
onUnmounted(() => {
clearTimeout(timer)
abortController?.abort()
})
return { results, isSearching }
}
onUnmounted里清理定时器和请求,防止组件卸载后的内存泄漏和状态更新。这是Composable编写的基本准则——任何副作用都必须在组件卸载时清理。
跨组件状态共享的Composable模式
当多个组件需要共享同一份状态时,Composable可以通过模块级变量实现轻量级状态管理,无需引入Pinia:
// useAuth.ts - 全局认证状态
import { ref, computed, readonly } from 'vue'
// 模块级变量,所有调用共享同一份状态
const user = ref<{ id: string; name: string } | null>(null)
const token = ref<string | null>(null)
const isAuthenticated = computed(() => !!token.value)
export function useAuth() {
const login = async (credentials: { username: string; password: string }) => {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
})
const data = await res.json()
user.value = data.user
token.value = data.token
localStorage.setItem('token', data.token)
}
const logout = () => {
user.value = null
token.value = null
localStorage.removeItem('token')
}
return {
user: readonly(user), // 外部只读,防止直接修改
token: readonly(token),
isAuthenticated,
login,
logout
}
}
readonly包装返回值,确保状态只能通过暴露的方法修改,符合单一数据流原则。比Pinia轻量,适合不需要DevTools和插件机制的中等规模应用。
Composable的TypeScript类型设计原则
类型安全的Composable需要在泛型约束和类型推断间找平衡。过度标注会让代码冗长,依赖推断又可能在边界场景丢失类型。工程实践中的建议:公共API的入参和返回值显式标注,内部实现依赖推断。使用泛型约束确保类型之间的关系正确传播,不要到处用any强制绕过。
组合式函数是Vue3项目架构的基石。好的Composable封装让组件保持薄层只管渲染,业务逻辑全部下沉到可测试、可复用的函数中。掌握上述几种模式后,日常开发中80%的逻辑复用场景都能覆盖。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-han-shu-feng-zhuang-shi-zhan-cong-ye-wu-luo/