Vue3 Composable组合式函数设计与状态复用最佳实践

Vue3的组合式API(Composition API)改变了组件逻辑的组织方式,而Composable组合式函数则是这一体系下实现状态复用和逻辑抽取的核心模式。通过将响应式状态、计算属性、副作用等封装为独立函数,多个组件之间可以共享复杂逻辑而不受Options API中mixin命名冲突和数据来源不透明的困扰。本文从Composable设计原则出发,结合实际项目中的表单验证、数据请求、分页等场景,给出Vue3组合式函数的完整设计实践。

Composable组合式函数设计原则与响应式基础

Composable是利用Vue3组合式API封装的可复用逻辑函数,命名约定以 use 前缀开头,如 useMousePositionuseFetch。核心设计原则有三条:单一职责、显式输入输出、响应式驱动。

单一职责要求每个Composable只关注一个领域逻辑。 useFormValidation 只处理表单校验,不混入数据请求逻辑; usePagination 只管理分页状态,不关心数据来源。

显式输入输出要求Composable通过参数接收配置,通过返回值暴露状态和方法。调用方能清楚知道数据从哪里来、有哪些可用方法,避免mixin中隐式注入的属性导致的来源不透明问题。

响应式驱动要求内部状态使用 refreactivecomputed 等响应式API,确保状态变化自动触发依赖更新。

一个基础示例:

import { ref, onMounted, onUnmounted } from 'vue'

export function useMousePosition() {
  const x = ref(0)
  const y = ref(0)

  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

在组件中使用:

<script setup>
import { useMousePosition } from './composables/useMousePosition'

const { x, y } = useMousePosition()
</script>

<template>
  <p>Mouse: {{ x }}, {{ y }}</p>
</template>

生命周期钩子( onMountedonUnmounted)必须在 setup 同步执行期间调用,因此Composable函数必须在 <script setup> 顶层调用,不能放在异步回调中。

表单验证Composable与异步校验实现

表单验证是前端项目中的高频需求。通过Composable将验证逻辑从组件中抽离,实现多表单复用:

import { ref, reactive, computed } from 'vue'

export function useFormValidation(initialValues, rules) {
  const values = reactive({ ...initialValues })
  const errors = reactive({})
  const touched = reactive({})
  const isSubmitting = ref(false)

  // 单字段验证
  function validateField(field) {
    const fieldRules = rules[field]
    if (!fieldRules) return true

    for (const rule of fieldRules) {
      const result = rule(values[field], values)
      if (result !== true) {
        errors[field] = result
        return false
      }
    }
    errors[field] = ''
    return true
  }

  // 全表单验证
  function validateAll() {
    let valid = true
    for (const field in rules) {
      if (!validateField(field)) {
        valid = false
      }
    }
    return valid
  }

  // 异步验证(如远程检查用户名是否已注册)
  async function validateFieldAsync(field, asyncFn) {
    if (!validateField(field)) return false
    const result = await asyncFn(values[field])
    if (!result.valid) {
      errors[field] = result.message
      return false
    }
    return true
  }

  // 处理字段blur事件
  function handleBlur(field) {
    touched[field] = true
    validateField(field)
  }

  // 处理字段input事件
  function handleInput(field) {
    if (touched[field]) {
      validateField(field)
    }
  }

  const isValid = computed(() => {
    for (const field in rules) {
      if (errors[field]) return false
    }
    return true
  })

  return {
    values, errors, touched, isSubmitting, isValid,
    validateField, validateAll, validateFieldAsync,
    handleBlur, handleInput
  }
}

定义验证规则和组件中使用:

// rules.js
export const loginRules = {
  username: [
    (v) => !!v || '请输入用户名',
    (v) => v.length >= 3 || '用户名至少3个字符',
  ],
  password: [
    (v) => !!v || '请输入密码',
    (v) => v.length >= 8 || '密码至少8位',
    (v) => /[A-Z]/.test(v) || '密码需包含大写字母',
  ],
}

// LoginForm.vue
<script setup>
import { useFormValidation } from './composables/useFormValidation'
import { loginRules } from './rules'

const { values, errors, validateAll, handleBlur, handleInput } =
  useFormValidation(
    { username: '', password: '' },
    loginRules
  )

async function handleSubmit() {
  if (!validateAll()) return
  // 提交逻辑
  const res = await api.login(values)
  // ...
}
</script>

数据请求Composable与请求缓存设计

数据请求是另一个高频场景。封装带缓存、防抖、错误处理的请求Composable:

import { ref, watch, isRef } from 'vue'

export function useFetch(url, options = {}) {
  const {
    immediate = true,
    initialData = null,
    refetch = false,
    transform,
    onError,
  } = options

  const data = ref(initialData)
  const error = ref(null)
  const loading = ref(false)

  // 简单内存缓存
  const cache = new Map()

  async function execute() {
    const urlValue = isRef(url) ? url.value : url

    // 检查缓存
    if (!refetch && cache.has(urlValue)) {
      data.value = cache.get(urlValue)
      return
    }

    loading.value = true
    error.value = null

    try {
      const response = await fetch(urlValue)
      if (!response.ok) throw new Error(`HTTP ${response.status}`)

      let result = await response.json()
      if (transform) result = transform(result)

      data.value = result
      cache.set(urlValue, result)
    } catch (err) {
      error.value = err
      if (onError) onError(err)
    } finally {
      loading.value = false
    }
  }

  if (isRef(url)) {
    watch(url, execute)
  }

  if (immediate) {
    execute()
  }

  return { data, error, loading, execute, refresh: execute }
}

组件中使用,支持响应式URL和自动重新请求:

<script setup>
import { ref } from 'vue'
import { useFetch } from './composables/useFetch'

const userId = ref(1)

const { data: user, error, loading } = useFetch(
  () => `/api/users/${userId.value}`,
  {
    transform: (res) => ({
      ...res,
      fullName: `${res.firstName} ${res.lastName}`,
    }),
    onError: (err) => console.error('请求失败:', err),
  }
)

function switchUser(id) {
  userId.value = id  // 自动触发重新请求
}
</script>

分页Composable与排序状态管理

分页和排序是列表页面的通用逻辑,通过Composable统一管理状态:

import { ref, computed, reactive } from 'vue'

export function usePagination(fetchFn, options = {}) {
  const {
    initialPage = 1,
    initialPageSize = 20,
    initialSortBy = '',
    initialSortOrder = 'asc',
  } = options

  const currentPage = ref(initialPage)
  const pageSize = ref(initialPageSize)
  const sortBy = ref(initialSortBy)
  const sortOrder = ref(initialSortOrder)
  const total = ref(0)
  const items = ref([])
  const loading = ref(false)

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

  const paginationParams = computed(() => ({
    page: currentPage.value,
    pageSize: pageSize.value,
    sortBy: sortBy.value,
    sortOrder: sortOrder.value,
  }))

  async function loadData() {
    loading.value = true
    try {
      const result = await fetchFn(paginationParams.value)
      items.value = result.items
      total.value = result.total
    } finally {
      loading.value = false
    }
  }

  function goToPage(page) {
    currentPage.value = page
    loadData()
  }

  function changePageSize(size) {
    pageSize.value = size
    currentPage.value = 1
    loadData()
  }

  function toggleSort(field) {
    if (sortBy.value === field) {
      sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
    } else {
      sortBy.value = field
      sortOrder.value = 'asc'
    }
    currentPage.value = 1
    loadData()
  }

  return {
    currentPage, pageSize, sortBy, sortOrder,
    total, totalPages, items, loading,
    paginationParams,
    goToPage, changePageSize, toggleSort, loadData,
  }
}

在列表组件中组合使用:

<script setup>
import { usePagination } from './composables/usePagination'
import { useFetch } from './composables/useFetch'

const { items, currentPage, pageSize, total, totalPages,
        loading, goToPage, changePageSize, toggleSort } =
  usePagination(
    async (params) => {
      const res = await fetch(
        `/api/users?page=${params.page}&size=${params.pageSize}` +
        `&sort=${params.sortBy}&order=${params.sortOrder}`
      )
      return res.json()
    },
    { initialPageSize: 15 }
  )
</script>

Composable组合与依赖管理

多个Composable可以组合使用,形成更高层次的逻辑抽象。例如将数据请求和分页组合:

export function useUserList() {
  const { paginationParams, ...pagination } = usePagination()
  const { data, loading, error, execute } = useFetch(
    () => `/api/users?${new URLSearchParams(paginationParams.value)}`,
    { immediate: false }
  )

  // 将分页操作和请求绑定
  pagination.loadData = async () => {
    await execute()
    pagination.items.value = data.value.items
    pagination.total.value = data.value.total
  }

  pagination.loadData()
  return { ...pagination, error }
}

组合时注意避免循环依赖——Composable A调用Composable B,B又回调A的情况。通过将共享状态提升到父级Composable统一管理,子级Composable通过参数接收状态而非自行创建,可以有效解耦依赖关系。

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

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

相关推荐