Vue3组合式API封装复杂业务逻辑的实战模式与性能优化

为什么组合式API比选项式API更适合复杂业务

Vue3的组合式API(Composition API)不是语法糖,而是解决选项式API(Options API)在复杂业务场景下逻辑碎片化问题的架构方案。选项式API把同一个业务逻辑的data、methods、computed、watch分散到不同选项中,一个”用户搜索+筛选+分页”的功能,代码横跨4个选项块,维护时需要反复跳转。组合式API把相关逻辑聚合到同一个composable函数中,逻辑内聚,复用清晰。

组合式API的核心原则:按功能聚合,非按类型分散。一个useUserSearch函数包含搜索所需的全部状态和方法,外部只需调用返回值即可使用,无需关心内部实现。

Composable函数设计模式

Composable函数的命名约定以use开头,参数接收外部依赖,返回响应式状态和方法。以下是一个完整的数据表格composable:

// useDataTable.ts - 通用数据表格逻辑封装
import { ref, computed, watch } from 'vue'
import type { Ref } from 'vue'

interface DataTableOptions<T> {
  fetchData: (params: Record<string, any>) => Promise<{ list: T[]; total: number }>
  pageSize?: number
  immediate?: boolean
}

export function useDataTable<T>(
  options: DataTableOptions<T>
) {
  const data = ref<T[]>([]) as Ref<T[]>
  const loading = ref(false)
  const currentPage = ref(1)
  const pageSize = ref(options.pageSize || 20)
  const total = ref(0)
  const searchParams = ref<Record<string, any>>({})

  const totalPages = computed(() => Math.ceil(total.value / pageSize.value))

  async function fetchList() {
    loading.value = true
    try {
      const params = {
        ...searchParams.value,
        page: currentPage.value,
        pageSize: pageSize.value,
      }
      const result = await options.fetchData(params)
      data.value = result.list
      total.value = result.total
    } catch (error) {
      console.error('fetchData failed:', error)
      data.value = []
    } finally {
      loading.value = false
    }
  }

  function handlePageChange(page: number) {
    currentPage.value = page
    fetchList()
  }

  function handleSearch(params: Record<string, any>) {
    searchParams.value = params
    currentPage.value = 1
    fetchList()
  }

  if (options.immediate !== false) {
    fetchList()
  }

  return {
    data,
    loading,
    currentPage,
    pageSize,
    total,
    totalPages,
    fetchList,
    handlePageChange,
    handleSearch,
  }
}

组件中使用:

<script setup lang="ts">
import { useDataTable } from '@/composables/useDataTable'

const {
  data: userList,
  loading,
  currentPage,
  total,
  handlePageChange,
  handleSearch,
} = useDataTable({
  fetchData: (params) => api.getUserList(params),
  pageSize: 20,
})
</script>

复杂表单场景的组合式API封装

复杂表单是前端最容易出现逻辑膨胀的场景。一个包含多步骤、动态校验、联动的表单,用选项式API写出上千行毫不罕见。用组合式API可以按步骤或功能模块拆分:

// useOrderForm.ts - 订单表单逻辑
import { ref, reactive, computed } from 'vue'

export function useOrderForm() {
  const currentStep = ref(0)
  const formState = reactive({
    customerName: '',
    contactPhone: '',
    items: [] as Array<{ skuId: string; quantity: number; price: number }>,
    shippingMethod: '',
    shippingAddress: '',
  })

  const validators = {
    step0: () => !!formState.customerName && /^1\d{10}$/.test(formState.contactPhone),
    step1: () => formState.items.length > 0 && formState.items.every(i => i.quantity > 0),
    step2: () => !!formState.shippingMethod && !!formState.shippingAddress,
  }

  const canProceed = computed(() => {
    const validator = validators[`step${currentStep.value}` as keyof typeof validators]
    return validator ? validator() : false
  })

  const totalAmount = computed(() =>
    formState.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
  )

  function nextStep() {
    if (canProceed.value && currentStep.value < 2) {
      currentStep.value++
    }
  }

  function prevStep() {
    if (currentStep.value > 0) currentStep.value--
  }

  function addItem(skuId: string, price: number) {
    const existing = formState.items.find(i => i.skuId === skuId)
    if (existing) {
      existing.quantity++
    } else {
      formState.items.push({ skuId, quantity: 1, price })
    }
  }

  return {
    currentStep,
    formState,
    canProceed,
    totalAmount,
    nextStep,
    prevStep,
    addItem,
  }
}

Composable的依赖注入与测试

Composable函数的依赖应该通过参数注入,而非在函数内部硬编码。这既是设计原则,也是测试的前提。反例:

// 反例:硬编码API调用,无法测试
export function useUserData() {
  const fetch = async () => {
    const res = await axios.get('/api/users')  // 硬编码
    return res.data
  }
}

正例:依赖通过参数注入,可替换为mock:

// 正例:依赖注入
export function useUserData(api: { getUsers: () => Promise<User[]> }) {
  const fetch = async () => {
    return await api.getUsers()  // 可替换
  }
}

// 测试时注入mock
const mockApi = { getUsers: vi.fn().mockResolvedValue([{ id: 1, name: 'test' }]) }
const { fetch } = useUserData(mockApi)

性能优化:shallowRef与computed的正确使用

组合式API场景下,性能问题常出现在响应式追踪范围过大。深层reactive对象中任何一个属性变化都会触发依赖它的effect重新执行。当数据量较大时(如1000行的表格数据),用shallowRef替代ref可以避免深层追踪:

// 大数据量表格用shallowRef避免深层响应式追踪
const tableData = shallowRef<TableRow[]>([])

// 更新数据时,创建新数组触发引用变化
function updateRow(index: number, row: TableRow) {
  const newData = [...tableData.value]
  newData[index] = { ...newData[index], ...row }
  tableData.value = newData  // 浅层引用变更,触发更新
}

// 避免的做法:直接修改属性,不会触发更新
// tableData.value[index].name = 'new name'  // 无效!

computed的缓存机制也值得注意。computed只在依赖变化时重新计算,但Vue3.4之前computed有内部dirty标记的优化问题。大量computed依赖同一个reactive对象的多个属性时,任一属性变化都会让所有computed重新计算。拆分大reactive为多个小ref可以缓解:

// 拆分前:一个大reactive导致computed过度重算
const state = reactive({ a: 1, b: 2, c: 3 })
const computedA = computed(() => state.a * 2)  // b或c变化也会触发

// 拆分后:独立ref,精确追踪
const a = ref(1)
const b = ref(2)
const c = ref(3)
const computedA = computed(() => a.value * 2)  // 只有a变化才触发

组合式API不是银弹,但它在复杂业务逻辑的组织上确实优于选项式API。核心实践原则:按功能聚合而非按类型分散、依赖注入而非硬编码、大数据量用shallowRef避免深层追踪。掌握这些模式,才能在真实项目中写出可维护、可测试、高性能的Vue3代码。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-feng-zhuang-fu-za-ye-wu-luo-ji-de-shi/

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

相关推荐