Vue 3 Composition API实战:构建可复用的组合式逻辑与组件设计模式

RedHannah
RedHannah 2026-02-09T20:09:05+08:00
0 0 0

引言:从Options API到Composition API的演进

在Vue 2时代,开发者主要依赖Options API来组织组件逻辑。这种基于选项的对象结构虽然简单直观,但在复杂组件中逐渐暴露出诸多问题:逻辑分散、难以复用、代码冗长、类型推导困难等。随着现代前端应用的复杂度不断提升,这些痛点愈发明显。

Vue 3引入了Composition API,从根本上改变了组件开发的方式。它不再依赖于datamethodscomputed等选项对象,而是通过函数式编程的方式将逻辑组织为可复用的组合式函数(Composables)。这一变革不仅提升了代码的可读性和可维护性,更带来了前所未有的灵活性和模块化能力。

为什么需要Composition API?

  1. 逻辑复用难题
    在Options API中,如果多个组件需要共享相同逻辑(如表单验证、数据获取),只能通过mixins实现。但mixins存在命名冲突、作用域污染、调试困难等问题。

  2. 复杂组件的可读性下降
    当一个组件包含大量datamethodscomputedwatch时,其逻辑被分割成不同部分,难以理解整体行为。

  3. 类型支持不足
    mixinsOptions API对TypeScript的支持有限,导致类型推导不准确,增加了开发成本。

  4. 更好的测试能力
    组合式函数是纯函数,易于单元测试,且能独立于组件运行。

Composition API的核心思想

  • 逻辑即函数:将组件中的业务逻辑封装为独立的函数。
  • 响应式系统:使用refreactive等API创建响应式数据。
  • 生命周期钩子函数化onMountedonUnmounted等函数替代mountedbeforeDestroy等选项。
  • 组合而非继承:通过组合多个函数来构建复杂功能,避免深层嵌套。

接下来,我们将深入探讨Composition API的各个核心概念,并通过真实项目案例展示如何构建可复用的组合式逻辑与组件设计模式。

核心概念详解:响应式系统与生命周期

1. 响应式基础:refreactive

refreactive 是Vue 3响应式系统的两大基石。它们共同构成了数据驱动视图更新的基础。

ref:引用类型包装器

import { ref } from 'vue'

// 声明一个响应式变量
const count = ref(0)

// 访问值时需要 .value
console.log(count.value) // 0

// 改变值会触发视图更新
count.value++

ref适用于基本类型(如字符串、数字、布尔值)或复杂对象。它的优势在于:

  • 提供统一的访问方式(.value
  • 自动解包在模板中使用(无需.value
<template>
  <div>
    <p>计数: {{ count }}</p> <!-- 直接使用,自动解包 -->
    <button @click="count++">增加</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

reactive:对象级别的响应式

import { reactive } from 'vue'

const state = reactive({
  name: 'Alice',
  age: 25,
  isActive: true
})

// 修改属性直接触发更新
state.age = 26

reactive适用于对象和数组。它返回一个代理对象,所有属性变化都会被追踪。

⚠️ 注意:reactive不能用于基本类型,也不能直接替换原始值。

ref vs reactive 选择指南

场景 推荐使用
简单值(数字、字符串) ref
复杂对象/数组 reactive
需要解构赋值 ref(避免破坏响应性)
类型检查友好 ref(更易配合TS)

2. 响应式计算:computed

computed用于定义依赖响应式数据的派生状态,并缓存结果以提高性能。

import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})

// 只有当 firstName 或 lastName 变化时才会重新计算
console.log(fullName.value) // John Doe

双向计算属性(可写)

const fullName = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (newName) => {
    const [first, last] = newName.split(' ')
    firstName.value = first
    lastName.value = last
  }
})

3. 响应式监听:watch

watch用于监听响应式数据的变化,执行副作用操作。

基本用法

import { ref, watch } from 'vue'

const count = ref(0)

watch(count, (newValue, oldValue) => {
  console.log(`count 从 ${oldValue} 变为 ${newValue}`)
})

监听多个源

watch([count, firstName], ([newCount, newFirst], [oldCount, oldFirst]) => {
  console.log(`count: ${newCount}, name: ${newFirst}`)
})

深度监听

const user = ref({ name: 'Alice', address: { city: 'Beijing' } })

watch(user, (newUser, oldUser) => {
  console.log('user changed')
}, { deep: true })

立即执行

watch(count, handler, { immediate: true }) // 初次调用

4. 生命周期钩子函数化

在Composition API中,生命周期钩子被转换为函数形式:

选项API Composition API
mounted() onMounted(() => {})
updated() onUpdated(() => {})
unmounted() onUnmounted(() => {})
beforeMount() onBeforeMount(() => {})
beforeUpdate() onBeforeUpdate(() => {})
beforeUnmount() onBeforeUnmount(() => {})
<script setup>
import { onMounted, onUnmounted, onUpdated } from 'vue'

onMounted(() => {
  console.log('组件已挂载')
})

onUpdated(() => {
  console.log('组件更新完成')
})

onUnmounted(() => {
  console.log('组件已卸载')
})
</script>

💡 小技巧:onMounted等函数必须在setup上下文中调用,不能在异步函数中直接使用。

构建可复用的组合式逻辑(Composables)

什么是Composable?

Composable是一个返回响应式状态和方法的函数,它封装了可复用的业务逻辑。它不是组件,而是一种“逻辑单元”。

实战案例1:用户信息获取与缓存

// composables/useUser.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useUser(userId) {
  const user = ref(null)
  const loading = ref(false)
  const error = ref(null)

  const fetchUser = async () => {
    loading.value = true
    error.value = null

    try {
      const response = await fetch(`/api/users/${userId}`)
      if (!response.ok) throw new Error('User not found')
      user.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }

  // 自动加载
  onMounted(fetchUser)

  // 清理资源
  onUnmounted(() => {
    user.value = null
    error.value = null
  })

  return {
    user,
    loading,
    error,
    refresh: fetchUser
  }
}

使用方式

<!-- UserDetail.vue -->
<script setup>
import { useUser } from '@/composables/useUser'

const props = defineProps({
  userId: { type: Number, required: true }
})

const { user, loading, error, refresh } = useUser(props.userId)
</script>

<template>
  <div v-if="loading">加载中...</div>
  <div v-else-if="error">错误: {{ error }}</div>
  <div v-else>
    <h2>{{ user?.name }}</h2>
    <p>年龄: {{ user?.age }}</p>
    <button @click="refresh">刷新</button>
  </div>
</template>

优势分析

  • ✅ 逻辑集中,易于维护
  • ✅ 可在多个组件间复用
  • ✅ 支持参数化(传入userId
  • ✅ 内置生命周期管理(自动加载/清理)
  • ✅ 类型安全(配合TS)

高级组合式逻辑:表单处理与验证

实战案例2:通用表单管理器

// composables/useForm.js
import { reactive, computed } from 'vue'

export function useForm(initialValues = {}, validators = {}) {
  const form = reactive({ ...initialValues })
  const errors = reactive({})
  const touched = reactive({})

  // 验证规则
  const validateField = (field, value) => {
    const rules = validators[field]
    if (!rules) return true

    for (const rule of rules) {
      const result = rule(value)
      if (result !== true) {
        errors[field] = result
        return false
      }
    }
    return true
  }

  // 表单提交
  const submit = async (onSuccess, onError) => {
    // 重置错误
    Object.keys(errors).forEach(key => delete errors[key])

    let isValid = true
    for (const field in form) {
      if (!validateField(field, form[field])) {
        isValid = false
      }
    }

    if (isValid) {
      try {
        await onSuccess(form)
      } catch (err) {
        onError(err)
      }
    } else {
      // 触发校验状态
      Object.keys(errors).forEach(key => {
        touched[key] = true
      })
    }
  }

  // 重置表单
  const reset = () => {
    Object.keys(form).forEach(key => {
      form[key] = initialValues[key] || ''
    })
    Object.keys(errors).forEach(key => delete errors[key])
    Object.keys(touched).forEach(key => delete touched[key])
  }

  // 通用字段更新
  const updateField = (field, value) => {
    form[field] = value
    if (touched[field]) {
      validateField(field, value)
    }
  }

  // 标记字段为已触碰
  const markTouched = (field) => {
    touched[field] = true
    if (errors[field]) {
      validateField(field, form[field])
    }
  }

  // 计算是否所有字段都有效
  const isValid = computed(() => {
    return !Object.keys(errors).length
  })

  // 计算是否已触碰
  const isSubmitted = computed(() => {
    return Object.values(touched).some(Boolean)
  })

  return {
    form,
    errors,
    touched,
    isValid,
    isSubmitted,
    submit,
    reset,
    updateField,
    markTouched,
    validateField
  }
}

应用示例

<!-- LoginForm.vue -->
<script setup>
import { useForm } from '@/composables/useForm'

const validators = {
  email: [
    (v) => !!v.trim() || '邮箱不能为空',
    (v) => /^\S+@\S+\.\S+$/.test(v) || '邮箱格式不正确'
  ],
  password: [
    (v) => !!v || '密码不能为空',
    (v) => v.length >= 6 || '密码至少6位'
  ]
}

const { form, errors, isSubmitted, submit, reset } = useForm(
  { email: '', password: '' },
  validators
)

const onSubmit = async (formData) => {
  try {
    await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(formData),
      headers: { 'Content-Type': 'application/json' }
    })
    alert('登录成功!')
  } catch (err) {
    console.error(err)
  }
}
</script>

<template>
  <form @submit.prevent="submit(onSubmit, (err) => alert(err.message))">
    <div>
      <label>邮箱:</label>
      <input
        v-model="form.email"
        @blur="markTouched('email')"
        :class="{ error: isSubmitted && errors.email }"
      />
      <span v-if="isSubmitted && errors.email" class="error-msg">{{ errors.email }}</span>
    </div>

    <div>
      <label>密码:</label>
      <input
        type="password"
        v-model="form.password"
        @blur="markTouched('password')"
        :class="{ error: isSubmitted && errors.password }"
      />
      <span v-if="isSubmitted && errors.password" class="error-msg">{{ errors.password }}</span>
    </div>

    <button type="submit" :disabled="!isValid">登录</button>
    <button type="button" @click="reset">重置</button>
  </form>
</template>

<style scoped>
.error { border-color: red; }
.error-msg { color: red; font-size: 12px; margin-top: 4px; display: block; }
</style>

优势总结

  • ✅ 逻辑完全抽象,可跨项目复用
  • ✅ 支持动态验证规则
  • ✅ 状态管理清晰(formerrorstouched
  • ✅ 支持异步提交和错误处理
  • ✅ 与UI解耦,便于测试

组件通信新模式:事件总线与全局状态管理

1. 自定义事件机制(Event Bus)

在复杂应用中,组件间通信常需跨层级传递数据。Vue 3推荐使用自定义事件全局状态管理

实现一个轻量级事件总线

// utils/eventBus.js
import { ref } from 'vue'

const events = ref({})

export const useEventBus = () => {
  const emit = (event, payload) => {
    if (events.value[event]) {
      events.value[event].forEach(callback => callback(payload))
    }
  }

  const on = (event, callback) => {
    if (!events.value[event]) {
      events.value[event] = []
    }
    events.value[event].push(callback)

    // 返回移除函数
    return () => {
      events.value[event] = events.value[event].filter(cb => cb !== callback)
    }
  }

  const off = (event, callback) => {
    if (events.value[event]) {
      events.value[event] = events.value[event].filter(cb => cb !== callback)
    }
  }

  return { emit, on, off }
}

2. 使用事件总线进行组件通信

<!-- NotificationPanel.vue -->
<script setup>
import { useEventBus } from '@/utils/eventBus'

const { emit } = useEventBus()

const showNotification = (message, type = 'info') => {
  emit('notification', { message, type })
}
</script>

<template>
  <button @click="showNotification('操作成功', 'success')">发送通知</button>
</template>
<!-- App.vue -->
<script setup>
import { useEventBus } from '@/utils/eventBus'
import { onMounted } from 'vue'

const { on } = useEventBus()

on('notification', (payload) => {
  console.log('收到通知:', payload)
  // 显示弹窗或气泡
})
</script>

⚠️ 注意:事件总线适用于非父子关系的组件通信,但不建议过度使用,应优先考虑状态管理。

全局状态管理:Pinia集成实践

1. 安装与配置

npm install pinia
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.mount('#app')

2. 定义Store

// stores/userStore.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    profile: null,
    token: null,
    preferences: {}
  }),

  getters: {
    isLoggedIn: (state) => !!state.token,
    displayName: (state) => state.profile?.name || '未知用户'
  },

  actions: {
    setToken(token) {
      this.token = token
    },

    async fetchProfile() {
      try {
        const res = await fetch('/api/user/profile')
        this.profile = await res.json()
      } catch (err) {
        console.error('获取用户信息失败:', err)
      }
    },

    logout() {
      this.$reset()
    }
  }
})

3. 组合式逻辑与Pinia结合

// composables/useAuth.js
import { useUserStore } from '@/stores/userStore'

export function useAuth() {
  const userStore = useUserStore()

  const login = async (credentials) => {
    const res = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(credentials)
    })
    const data = await res.json()
    userStore.setToken(data.token)
    await userStore.fetchProfile()
  }

  const logout = () => {
    userStore.logout()
  }

  return {
    user: userStore.$state,
    isLoggedIn: userStore.isLoggedIn,
    login,
    logout
  }
}

4. 组合式逻辑 + Pinia的最佳实践

  • ✅ 将业务逻辑封装在composables
  • ✅ 通过useXxxStore()获取状态
  • ✅ 保持composables无状态,仅负责逻辑
  • ✅ 避免在composables中直接修改store状态(除非必要)

高级设计模式:组件工厂与插件系统

1. 组件工厂模式

// factories/createModal.js
import { defineComponent } from 'vue'

export function createModal(options = {}) {
  return defineComponent({
    name: options.name || 'Modal',
    props: {
      visible: { type: Boolean, default: false },
      title: { type: String, default: '提示' }
    },
    emits: ['update:visible', 'confirm', 'cancel'],
    setup(props, { emit }) {
      const close = () => {
        emit('update:visible', false)
      }

      const handleConfirm = () => {
        emit('confirm')
        close()
      }

      const handleCancel = () => {
        emit('cancel')
        close()
      }

      return () => (
        <div class="modal-overlay" v-show={props.visible}>
          <div class="modal">
            <header class="modal-header">
              <h3>{props.title}</h3>
              <button onClick={close}>×</button>
            </header>
            <main class="modal-body">
              <slot />
            </main>
            <footer class="modal-footer">
              <button onClick={handleCancel}>取消</button>
              <button onClick={handleConfirm} class="primary">
                确定
              </button>
            </footer>
          </div>
        </div>
      )
    }
  })
}

2. 使用工厂创建可配置组件

<script setup>
import { createModal } from '@/factories/createModal'

const ConfirmModal = createModal({
  name: 'ConfirmModal',
  title: '确认删除?'
})
</script>

<template>
  <ConfirmModal v-model:visible="showModal">
    <p>此操作无法撤销,确定要删除吗?</p>
  </ConfirmModal>
</template>

最佳实践与常见陷阱

✅ 推荐做法

  1. 始终使用ref处理基本类型
  2. 使用reactive处理对象/数组
  3. 将逻辑封装为composables
  4. 使用computed优化性能
  5. 合理使用watch,避免过度监听
  6. 优先使用Pinia进行全局状态管理

❌ 常见错误

  1. setup外使用响应式函数

    // 错误
    const count = ref(0)
    setTimeout(() => count.value++, 1000) // 可能丢失响应性
    
  2. 忘记onUnmounted清理

    // 错误
    const timer = setInterval(() => {}, 1000) // 内存泄漏
    
  3. watch中修改被监听的数据

    watch(count, (val) => {
      count.value = val * 2 // 无限循环
    })
    
  4. 滥用ref包裹对象

    const obj = ref({}) // 虽然可行,但不如直接使用`reactive`
    

总结:迈向现代化的Vue开发

Vue 3的Composition API不仅是语法上的改进,更是一次架构思维的升级。它让我们从“组件即容器”转向“逻辑即服务”的开发范式。

通过以下关键实践,你可以显著提升项目的可维护性与扩展性:

  • ✅ 使用composables实现逻辑复用
  • ✅ 结合Pinia构建可预测的状态管理
  • ✅ 利用ref/reactive/computed/watch构建响应式系统
  • ✅ 采用工厂模式和插件系统增强组件灵活性
  • ✅ 遵循最佳实践,避免常见陷阱

未来,随着Vue生态的持续演进,Composition API将成为前端工程化的标准范式。掌握它,意味着你已站在现代前端开发的前沿。

📌 行动建议:立即重构现有项目中的Options API组件,将其迁移为Composition API风格,并建立自己的composables库。

本文由[作者名]撰写,发布于[日期],转载请注明出处。

相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000