Vue3 Composable设计模式实战:从状态复用到异步数据管理的架构演进

Composable的核心定位与设计原则

Vue3的组合式API将逻辑复用从Mixins的无约束合并推进到显式导入的函数模式。Composable本质上是一个以refreactivecomputedwatch等组合式API为构建块的函数,返回响应式状态和方法供组件消费。设计一个合格的Composable需要遵循三个原则:输入参数尽量用refgetter保持响应式链路、内部副作用用watchEffectwatch自动清理、返回值解构后仍保持响应性。

// 基础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之间的连接器——urlpagination状态计算得出,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 }
}

泛型TArgsTResultexecute的参数类型和result的返回类型与传入函数完全一致,调用方无需手动标注类型。这种类型推导能力是Composable优于Mixins的根本原因——每个Composable的类型边界清晰,不存在Mixins的属性冲突和类型丢失问题。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3composable-she-ji-mo-shi-shi-zhan-cong-zhuang-tai-fu/

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

相关推荐