Composable的核心定位与设计原则
Vue3的组合式API将逻辑复用从Mixins的无约束合并推进到显式导入的函数模式。Composable本质上是一个以ref、reactive、computed、watch等组合式API为构建块的函数,返回响应式状态和方法供组件消费。设计一个合格的Composable需要遵循三个原则:输入参数尽量用ref或getter保持响应式链路、内部副作用用watchEffect或watch自动清理、返回值解构后仍保持响应性。
// 基础Composable示例:窗口尺寸
import { ref, onMounted, onUnmounted } from 'vue'
export function useWindowSize() {
const width = ref(window.innerWidth)
const height = ref(window.innerHeight)
function update() {
width.value = window.innerWidth
height.value = window.innerHeight
}
onMounted(() => window.addEventListener('resize', update))
onUnmounted(() => window.removeEventListener('resize', update))
return { width, height }
}
这个Composable符合原则:生命周期钩子自动在宿主组件上注册和清理,返回的ref解构后仍保持响应性。
异步数据管理Composable的进阶设计
实际项目中大量Composable需要处理异步数据获取。一个健壮的异步Composable需要管理loading、error、data三个状态,并支持取消请求、防抖、缓存等高级功能:
import { ref, watch, toValue, type Ref } from 'vue'
export function useFetch<T>(
url: Ref<string> | (() => string),
options: {
immediate?: boolean
refetch?: boolean
debounce?: number
initialData?: T
} = {}
) {
const data = ref<T | undefined>(options.initialData)
const error = ref<Error | null>(null)
const loading = ref(false)
const abortController = ref<AbortController | null>(null)
let debounceTimer: ReturnType<typeof setTimeout> | null = null
async function execute() {
const resolvedUrl = toValue(url)
if (!resolvedUrl) return
// 取消前一个请求
abortController.value?.abort()
abortController.value = new AbortController()
loading.value = true
error.value = null
try {
const response = await fetch(resolvedUrl, {
signal: abortController.value.signal
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
data.value = await response.json()
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') return
error.value = e as Error
} finally {
loading.value = false
}
}
function debouncedExecute() {
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(execute, options.debounce ?? 0)
}
if (options.immediate !== false) {
execute()
}
if (options.refetch) {
watch(url, options.debounce ? debouncedExecute : execute)
}
return { data, error, loading, execute, refresh: execute }
}
这个Composable的关键设计点:toValue支持Ref、getter和原始值三种输入,AbortController确保URL变化时取消前一个请求避免竞态,refetch选项控制是否在URL变化时自动重新请求。
Composable组合:多层嵌套的逻辑拼装
复杂业务逻辑往往需要多个Composable协同工作。以分页列表为例:
// usePagination.ts
import { ref, computed, type Ref } from 'vue'
export function usePagination(options: {
pageSize: number
total: Ref<number>
}) {
const currentPage = ref(1)
const pageSize = ref(options.pageSize)
const totalPages = computed(() =>
Math.ceil(options.total.value / pageSize.value)
)
const offset = computed(() =>
(currentPage.value - 1) * pageSize.value
)
function goToPage(page: number) {
currentPage.value = Math.max(1, Math.min(page, totalPages.value))
}
return { currentPage, pageSize, totalPages, offset, goToPage }
}
// 组合使用
import { useFetch } from './useFetch'
import { usePagination } from './usePagination'
import { computed } from 'vue'
export function usePaginatedList(apiBase: string) {
const total = ref(0)
const pagination = usePagination({ pageSize: 20, total })
const url = computed(() =>
`${apiBase}?offset=${pagination.offset.value}&limit=${pagination.pageSize.value}`
)
const { data, loading, error } = useFetch(url, { refetch: true })
// 监听数据更新总数
watch(data, (newData) => {
if (newData) total.value = newData.total
})
return { ...pagination, data, loading, error }
}
组合模式的核心是computed作为Composable之间的连接器——url随pagination状态计算得出,useFetch监听url变化自动重新请求,total从响应数据反哺分页计算。数据流单向且可追踪。
全局状态Composable与依赖注入
跨组件共享状态时,Composable可以实现轻量级状态管理,避免引入Pinia的全局开销:
// useAuth.ts - 单例模式全局状态
import { ref, computed } from 'vue'
const user = ref<{ id: string; name: string; role: string } | null>(null)
const token = ref<string | null>(localStorage.getItem('auth_token'))
export function useAuth() {
const isAuthenticated = computed(() => !!token.value)
const isAdmin = computed(() => user.value?.role === 'admin')
async function login(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()
token.value = data.token
user.value = data.user
localStorage.setItem('auth_token', data.token)
}
function logout() {
token.value = null
user.value = null
localStorage.removeItem('auth_token')
}
return { user, token, isAuthenticated, isAdmin, login, logout }
}
模块顶层的ref在同一模块作用域内是单例——所有调用useAuth()的组件共享同一份响应式数据。这比Pinia更轻量,但缺少DevTools集成和SSR支持。选择标准:单页应用用Composable单例即可,SSR应用用Pinia。
TypeScript类型推导与Composable的泛型约束
完善的类型推导是高质量Composable的标志。核心技巧是利用泛型和UnwrapRef让返回值类型自动推导:
import { ref, computed, type Ref, type ComputedRef } from 'vue'
// 约束返回值为对象,每个属性都是Ref或ComputedRef
type ComposableReturn<T> = {
[K in keyof T]: T[K] extends Ref ? T[K] :
T[K] extends ComputedRef ? T[K] : Ref<T[K]>
}
// 工具类型:提取async函数的返回值类型
type AsyncReturnType<T> = T extends (...args: any[]) => Promise<infer R> ? R : never
export function useAsyncAction<TArgs extends any[], TResult>(
fn: (...args: TArgs) => Promise<TResult>,
options: { onSuccess?: (result: TResult) => void } = {}
) {
const loading = ref(false)
const error = ref<Error | null>(null)
const result = ref<TResult | null>(null) as Ref<TResult | null>
async function execute(...args: TArgs) {
loading.value = true
error.value = null
try {
result.value = await fn(...args)
options.onSuccess?.(result.value)
return result.value
} catch (e) {
error.value = e as Error
throw e
} finally {
loading.value = false
}
}
return { loading, error, result, execute }
}
泛型TArgs和TResult让execute的参数类型和result的返回类型与传入函数完全一致,调用方无需手动标注类型。这种类型推导能力是Composable优于Mixins的根本原因——每个Composable的类型边界清晰,不存在Mixins的属性冲突和类型丢失问题。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3composable-she-ji-mo-shi-shi-zhan-cong-zhuang-tai-fu/