Vue3组合式函数Composables设计:从逻辑复用到TypeScript类型安全的进阶实践

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/

(0)
小编小编
上一篇 16小时前
下一篇 16小时前

相关推荐