Vue3生态中,Composables(组合式函数)是实现逻辑复用的核心模式。相比Vue2的Mixin,Composables具有显式依赖注入、类型推断友好、无命名冲突等优势。但实际项目中,很多Composables写法随意,缺少统一规范,导致维护困难甚至引入响应式泄漏。本文从设计原则到TypeScript类型安全实践,给出一套可落地的Composables工程化方案。
Composable的核心设计原则
一个合格的Composable需遵循三条原则:命名以use开头,返回值只包含响应式状态和方法;内部副作用在onScopeDispose中清理;接受ref作为参数时,用computed保持响应式链路不断裂。
// 标准Composable结构
import { ref, onScopeDispose, computed } from 'vue'
import type { Ref } from 'vue'
export function useDataFetcher<T>(
url: Ref<string> | string,
options?: { immediate?: boolean }
) {
const data = ref<T | null>(null) as Ref<T | null>
const error = ref<Error | null>(null)
const loading = ref(false)
const urlRef = computed(() =>
typeof url === 'string' ? url : url.value
)
let controller: AbortController | null = null
async function execute() {
controller?.abort()
controller = new AbortController()
loading.value = true
error.value = null
try {
const res = await fetch(urlRef.value, {
signal: controller.signal
})
data.value = await res.json()
} catch (e) {
if (e instanceof Error && e.name !== 'AbortError') {
error.value = e
}
} finally {
loading.value = false
}
}
onScopeDispose(() => controller?.abort())
if (options?.immediate !== false) {
execute()
}
return { data, error, loading, execute }
}
响应式参数传递的常见陷阱与解决方案
Composable接受参数时,开发者常犯的错误是直接用toValue解构ref,导致响应式断开:
// 错误写法:丢失响应式
export function usePagination(pageSize: Ref<number>) {
const size = pageSize.value // 仅在初始化时取值
const total = computed(() => someCalc(size))
return { total }
}
// 正确写法:使用toValue保持响应式
import { toValue } from 'vue'
export function usePagination(pageSize: Ref<number> | number) {
const total = computed(() => someCalc(toValue(pageSize)))
return { total }
}
toValue()同时处理ref、getter和普通值三种输入类型,是Composable参数解包的标准做法。所有内部依赖参数的计算逻辑都应包裹在computed或watch中,确保参数变化能正确触发更新。
异步Composable的状态机模式
异步操作(API请求、定时器等)需要管理idle/loading/success/error四种状态。手动维护多个布尔值容易遗漏,建议用状态机模式统一管理状态转换,避免状态不一致问题。核心思路是用discriminated union定义四种状态类型,通过computed派生布尔判断,execute方法驱动状态流转。
Composable之间的组合与依赖注入
复杂业务场景下,Composable需要组合使用。直接在Composable内部调用另一个Composable会导致组件绑定问题。推荐通过参数注入解耦:将依赖的Composable返回值作为参数传入,而非在内部硬编码调用。对于跨层级共享的Composable实例,使用provide/inject而非全局单例。
// 推荐:参数注入
export function useOrder(user: Ref<User>, options?: OrderOptions) {
const orders = computed(() =>
fetchOrdersByUser(user.value.id)
)
return { orders }
}
// 在组件中组合使用
const { user } = useUser()
const { orders } = useOrder(user)
TypeScript泛型Composable的类型推导
带泛型的Composable能根据入参自动推导返回类型,减少手动标注:
import { ref, type Ref } from 'vue'
interface FetchOptions {
immediate?: boolean
initialData?: unknown
}
export function useFetch<T>(
url: string | Ref<string>,
options?: FetchOptions
) {
const data = ref<T | undefined>(options?.initialData as T)
const error = ref<Error | null>(null)
const loading = ref(false)
// 返回类型自动推导:Ref<T | undefined>
return { data, error, loading } as const
}
// 使用时类型自动推导
const { data } = useFetch<User[]>('/api/users')
as const断言确保返回对象为readonly类型,防止外部意外修改内部ref引用。对返回值中不应被外部修改的ref,使用shallowRef加上readonly包装。
Composable单元测试策略
Composable的测试使用@vue/test-utils的withSetup辅助函数,在组件上下文中执行。异步Composable测试需配合vi.useFakeTimers()或flushPromises()确保异步操作完成后再断言。对于依赖外部API的Composable,通过依赖注入传入mock函数而非拦截全局fetch,保持测试独立性。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-han-shu-composables-she-ji-cong-luo-ji-fu/