Vue3组合式函数设计模式:从useRequest到状态工厂的进阶封装

组合式函数的设计原则

Vue3的Composition API将逻辑复用从Mixin转向了Composables(组合式函数)。一个设计良好的组合式函数应当满足三个条件:单一职责、响应式状态自包含、显式的输入输出。函数内部创建的ref和reactive不应泄漏到外部,外部只通过返回值访问受控的响应式引用。

useRequest:通用异步请求封装

前端项目中最频繁的异步操作是API请求。useRequest需要处理加载状态、错误处理、数据缓存和请求取消四个核心问题:

import { ref, shallowRef, onUnmounted } from "vue"

export function useRequest(fn, options = {}) {
  const { immediate = true, initialData = null, debounce = 0 } = options
  const data = shallowRef(initialData)
  const loading = ref(false)
  const error = ref(null)
  let controller = null
  let timer = null

  const execute = async (...args) => {
    if (timer) clearTimeout(timer)
    const run = async () => {
      controller?.abort()
      controller = new AbortController()
      loading.value = true
      error.value = null
      try {
        data.value = await fn(...args, { signal: controller.signal })
      } catch (e) {
        if (e.name !== "AbortError") error.value = e
      } finally {
        loading.value = false
      }
    }
    if (debounce > 0) {
      timer = setTimeout(run, debounce)
    } else {
      await run()
    }
  }

  onUnmounted(() => controller?.abort())

  if (immediate) execute()
  return { data, loading, error, execute }
}

响应式状态工厂:创建可复用的状态模板

当多个组件需要相同结构但独立的状态(如多标签页各自维护筛选条件),可以用工厂函数生成独立实例:

export function createFilterState(defaults = {}) {
  const keyword = ref(defaults.keyword || "")
  const status = ref(defaults.status || "all")
  const dateRange = ref(defaults.dateRange || [])
  const page = ref(1)
  const pageSize = ref(20)

  const reset = () => {
    keyword.value = defaults.keyword || ""
    status.value = defaults.status || "all"
    dateRange.value = defaults.dateRange || []
    page.value = 1
    pageSize.value = 20
  }

  const params = computed(() => ({
    keyword: keyword.value,
    status: status.value === "all" ? undefined : status.value,
    startDate: dateRange.value[0],
    endDate: dateRange.value[1],
    page: page.value,
    pageSize: pageSize.value
  }))

  watch([keyword, status], () => { page.value = 1 })

  return { keyword, status, dateRange, page, pageSize, params, reset }
}

// 在组件中使用,每个实例完全独立
const filterA = createFilterState({ status: "active" })
const filterB = createFilterState({ status: "archived" })

事件通信:useEventBus的轻量实现

跨组件通信不一定需要Pinia,轻量场景用事件总线更直接。Vue3没有内置事件系统,可以用Map组合实现:

import { onUnmounted } from "vue"

const bus = new Map()

export function useEventBus() {
  const on = (event, handler) => {
    if (!bus.has(event)) bus.set(event, new Set())
    bus.get(event).add(handler)
  }

  const off = (event, handler) => {
    bus.get(event)?.delete(handler)
  }

  const emit = (event, payload) => {
    bus.get(event)?.forEach(fn => fn(payload))
  }

  const useOn = (event, handler) => {
    on(event, handler)
    onUnmounted(() => off(event, handler))
  }

  return { on, off, emit, useOn }
}

useOn在组件卸载时自动取消订阅,避免内存泄漏。对于需要持久化的全局状态,仍然推荐Pinia。

组合式函数的测试策略

组合式函数本质是普通JavaScript函数,测试比组件简单得多。使用Vue的withSetup工具在测试中执行setup上下文:

import { withSetup } from "@vue/test-utils"
import { useRequest } from "./useRequest"

test("useRequest handles success", async () => {
  const mockFn = vi.fn().mockResolvedValue({ id: 1, name: "test" })
  const { data, loading, error } = withSetup(() => useRequest(mockFn))

  expect(loading.value).toBe(true)
  await vi.waitFor(() => expect(loading.value).toBe(false))
  expect(data.value).toEqual({ id: 1, name: "test" })
  expect(error.value).toBeNull()
})

组合式函数的单元测试覆盖率应优先于组件测试,因为它们承载了核心业务逻辑,且调试成本低、执行速度快。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-han-shu-she-ji-mo-shi-cong-userequest-dao/

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

相关推荐