Vue 3 Composition API实战:从组件通信到状态管理的完整开发流程

BoldWater
BoldWater 2026-02-10T03:12:11+08:00
0 0 1

引言

Vue 3的发布带来了革命性的Composition API,它为开发者提供了更加灵活和强大的组件开发方式。相比Vue 2的选项式API,Composition API让代码组织更加清晰,逻辑复用更加容易,特别是在处理复杂应用的状态管理和组件通信方面表现尤为突出。

本文将通过一个完整的项目案例,深入探讨如何在实际开发中运用Vue 3 Composition API的各项特性,涵盖从基础组件通信到复杂状态管理的完整流程,帮助开发者构建更优雅、更可维护的前端应用架构。

Vue 3 Composition API核心概念

什么是Composition API

Composition API是Vue 3引入的一种新的组件开发方式,它将组件逻辑按功能模块进行组织,而不是按照传统的选项(data、methods、computed等)来划分。这种设计模式使得代码更加灵活,便于复用和维护。

// Vue 2 选项式API
export default {
  data() {
    return {
      count: 0,
      message: 'Hello'
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}

// Vue 3 Composition API
import { ref, computed } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const message = ref('Hello')
    
    const doubleCount = computed(() => count.value * 2)
    
    const increment = () => {
      count.value++
    }
    
    return {
      count,
      message,
      doubleCount,
      increment
    }
  }
}

核心响应式API

Composition API提供了多种响应式API,包括ref、reactive、computed、watch等:

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

// ref - 创建响应式引用
const count = ref(0)
const name = ref('Vue')

// reactive - 创建响应式对象
const state = reactive({
  user: {
    name: 'John',
    age: 30
  },
  items: []
})

// computed - 创建计算属性
const doubleCount = computed(() => count.value * 2)

// watch - 监听响应式数据变化
watch(count, (newVal, oldVal) => {
  console.log(`count changed from ${oldVal} to ${newVal}`)
})

组件间通信实战

Props传递与验证

在Vue 3中,props的使用变得更加灵活。我们可以使用setup函数来接收和处理props:

// Parent.vue
<template>
  <Child 
    :title="pageTitle" 
    :user-info="userInfo"
    :items="listItems"
    @update-items="handleUpdateItems"
  />
</template>

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

const pageTitle = ref('用户管理')
const userInfo = ref({
  name: '张三',
  email: 'zhangsan@example.com'
})
const listItems = ref([
  { id: 1, name: '项目A' },
  { id: 2, name: '项目B' }
])

const handleUpdateItems = (newItems) => {
  listItems.value = newItems
}
</script>
// Child.vue
<script setup>
import { computed } from 'vue'

// 定义props和验证
const props = defineProps({
  title: {
    type: String,
    required: true
  },
  userInfo: {
    type: Object,
    default: () => ({})
  },
  items: {
    type: Array,
    default: () => []
  }
})

// 定义事件
const emit = defineEmits(['updateItems'])

// 计算属性
const userDisplayName = computed(() => {
  return props.userInfo.name || '匿名用户'
})

// 方法
const addItem = (item) => {
  const newItems = [...props.items, item]
  emit('updateItems', newItems)
}
</script>

<template>
  <div class="child-component">
    <h2>{{ title }}</h2>
    <p>当前用户:{{ userDisplayName }}</p>
    <ul>
      <li v-for="item in items" :key="item.id">{{ item.name }}</li>
    </ul>
    <button @click="addItem({ id: Date.now(), name: '新项目' })">
      添加项目
    </button>
  </div>
</template>

Provide/Inject模式

Provide/Inject是Vue中用于跨层级组件通信的重要机制。在Composition API中,我们可以更优雅地使用它:

// Parent.vue
<script setup>
import { provide, reactive } from 'vue'
import ChildComponent from './ChildComponent.vue'

// 创建共享状态
const sharedState = reactive({
  theme: 'light',
  language: 'zh-CN',
  currentUser: null
})

// 提供数据
provide('appContext', {
  state: sharedState,
  updateTheme: (theme) => {
    sharedState.theme = theme
  },
  setCurrentUser: (user) => {
    sharedState.currentUser = user
  }
})
</script>

<template>
  <div class="app">
    <ChildComponent />
  </div>
</template>
// ChildComponent.vue
<script setup>
import { inject, computed } from 'vue'

// 注入数据
const appContext = inject('appContext')

// 使用注入的数据
const currentTheme = computed(() => appContext.state.theme)
const currentUser = computed(() => appContext.state.currentUser)

// 修改共享状态
const switchTheme = () => {
  const newTheme = appContext.state.theme === 'light' ? 'dark' : 'light'
  appContext.updateTheme(newTheme)
}
</script>

<template>
  <div class="child-component" :class="currentTheme">
    <p>当前主题:{{ currentTheme }}</p>
    <p>当前用户:{{ currentUser?.name || '未登录' }}</p>
    <button @click="switchTheme">切换主题</button>
  </div>
</template>

事件总线模式

虽然Vue 3推荐使用Props和Events进行组件通信,但有时我们仍然需要一个全局的事件系统:

// eventBus.js
import { createApp } from 'vue'

export const EventBus = {
  // 创建事件总线实例
  instance: null,
  
  // 初始化事件总线
  init(app) {
    this.instance = app.config.globalProperties.$bus = {}
    this.setupEventHandlers()
  },
  
  // 设置事件处理方法
  setupEventHandlers() {
    const bus = this.instance
    
    bus.on = (event, callback) => {
      if (!bus._events) bus._events = {}
      if (!bus._events[event]) bus._events[event] = []
      bus._events[event].push(callback)
    }
    
    bus.emit = (event, data) => {
      if (bus._events && bus._events[event]) {
        bus._events[event].forEach(callback => callback(data))
      }
    }
    
    bus.off = (event, callback) => {
      if (bus._events && bus._events[event]) {
        if (callback) {
          bus._events[event] = bus._events[event].filter(cb => cb !== callback)
        } else {
          delete bus._events[event]
        }
      }
    }
  }
}

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { EventBus } from './utils/eventBus'

const app = createApp(App)
EventBus.init(app)
app.mount('#app')
// ComponentA.vue
<script setup>
import { inject } from 'vue'

const bus = inject('$bus')

const sendMessage = () => {
  bus.emit('message-sent', {
    content: 'Hello from Component A',
    timestamp: Date.now()
  })
}
</script>

<template>
  <button @click="sendMessage">发送消息</button>
</template>
// ComponentB.vue
<script setup>
import { inject, onMounted, onUnmounted } from 'vue'

const bus = inject('$bus')
const messages = ref([])

const handleMessage = (data) => {
  messages.value.push(data)
}

onMounted(() => {
  bus.on('message-sent', handleMessage)
})

onUnmounted(() => {
  bus.off('message-sent', handleMessage)
})
</script>

<template>
  <div>
    <h3>接收的消息:</h3>
    <ul>
      <li v-for="msg in messages" :key="msg.timestamp">
        {{ msg.content }} - {{ new Date(msg.timestamp).toLocaleTimeString() }}
      </li>
    </ul>
  </div>
</template>

响应式数据管理

状态管理基础

在Vue 3中,我们可以使用响应式API来构建简单但有效的状态管理系统:

// store/userStore.js
import { reactive, readonly } from 'vue'

// 创建响应式状态
const state = reactive({
  currentUser: null,
  isLoggedIn: false,
  profile: {
    name: '',
    email: '',
    avatar: ''
  }
})

// 提供只读状态访问
export const useUserStore = () => {
  // 获取用户信息
  const getUserInfo = () => readonly(state.currentUser)
  
  // 登录
  const login = (userData) => {
    state.currentUser = userData
    state.isLoggedIn = true
    state.profile = {
      name: userData.name,
      email: userData.email,
      avatar: userData.avatar || ''
    }
  }
  
  // 登出
  const logout = () => {
    state.currentUser = null
    state.isLoggedIn = false
    state.profile = {
      name: '',
      email: '',
      avatar: ''
    }
  }
  
  // 更新用户信息
  const updateProfile = (profileData) => {
    Object.assign(state.profile, profileData)
    if (state.currentUser) {
      state.currentUser.name = profileData.name
      state.currentUser.email = profileData.email
    }
  }
  
  return {
    state: readonly(state),
    getUserInfo,
    login,
    logout,
    updateProfile
  }
}

复杂状态管理

对于更复杂的应用,我们可以构建一个更加完整的状态管理系统:

// store/index.js
import { reactive, readonly } from 'vue'

// 创建全局状态
const globalState = reactive({
  loading: false,
  error: null,
  notifications: [],
  theme: 'light'
})

// 状态管理器
export const useGlobalStore = () => {
  // 设置加载状态
  const setLoading = (loading) => {
    globalState.loading = loading
  }
  
  // 设置错误信息
  const setError = (error) => {
    globalState.error = error
  }
  
  // 添加通知
  const addNotification = (notification) => {
    const id = Date.now()
    const notificationWithId = { ...notification, id }
    globalState.notifications.push(notificationWithId)
    
    // 自动移除通知(3秒后)
    setTimeout(() => {
      removeNotification(id)
    }, 3000)
  }
  
  // 移除通知
  const removeNotification = (id) => {
    const index = globalState.notifications.findIndex(n => n.id === id)
    if (index > -1) {
      globalState.notifications.splice(index, 1)
    }
  }
  
  // 切换主题
  const toggleTheme = () => {
    globalState.theme = globalState.theme === 'light' ? 'dark' : 'light'
  }
  
  return {
    state: readonly(globalState),
    setLoading,
    setError,
    addNotification,
    removeNotification,
    toggleTheme
  }
}
// composables/useApi.js
import { ref, reactive } from 'vue'
import { useGlobalStore } from '@/store'

export const useApi = () => {
  const { setLoading, setError, addNotification } = useGlobalStore()
  
  // API调用函数
  const apiCall = async (apiFunction, ...args) => {
    try {
      setLoading(true)
      setError(null)
      
      const result = await apiFunction(...args)
      
      return result
    } catch (error) {
      setError(error.message)
      addNotification({
        type: 'error',
        message: `API调用失败:${error.message}`
      })
      throw error
    } finally {
      setLoading(false)
    }
  }
  
  // 数据获取函数
  const fetchData = async (url) => {
    const response = await apiCall(
      fetch.bind(null, url),
      { method: 'GET' }
    )
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
    }
    
    return response.json()
  }
  
  // 数据提交函数
  const submitData = async (url, data) => {
    const response = await apiCall(
      fetch.bind(null, url),
      {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      }
    )
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
    }
    
    return response.json()
  }
  
  return {
    apiCall,
    fetchData,
    submitData
  }
}

自定义Hook封装

数据获取Hook

自定义Hook是Composition API的核心优势之一,它让我们能够将可复用的逻辑封装起来:

// composables/useFetch.js
import { ref, watch } from 'vue'

export const useFetch = (url, options = {}) => {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  // 执行请求
  const execute = async () => {
    try {
      loading.value = true
      error.value = null
      
      const response = await fetch(url, options)
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }
      
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
      console.error('Fetch error:', err)
    } finally {
      loading.value = false
    }
  }
  
  // 初始加载
  if (options.immediate !== false) {
    execute()
  }
  
  return {
    data,
    loading,
    error,
    execute
  }
}
// composables/usePagination.js
import { ref, computed } from 'vue'

export const usePagination = (items, pageSize = 10) => {
  const currentPage = ref(1)
  const _items = ref(items)
  
  // 计算总页数
  const totalPages = computed(() => {
    return Math.ceil(_items.value.length / pageSize)
  })
  
  // 计算当前页数据
  const currentItems = computed(() => {
    const start = (currentPage.value - 1) * pageSize
    const end = start + pageSize
    return _items.value.slice(start, end)
  })
  
  // 切换页面
  const goToPage = (page) => {
    if (page >= 1 && page <= totalPages.value) {
      currentPage.value = page
    }
  }
  
  // 上一页
  const prevPage = () => {
    if (currentPage.value > 1) {
      currentPage.value--
    }
  }
  
  // 下一页
  const nextPage = () => {
    if (currentPage.value < totalPages.value) {
      currentPage.value++
    }
  }
  
  // 更新数据
  const updateItems = (newItems) => {
    _items.value = newItems
    currentPage.value = 1
  }
  
  return {
    currentPage,
    totalPages,
    currentItems,
    goToPage,
    prevPage,
    nextPage,
    updateItems
  }
}

表单处理Hook

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

export const useForm = (initialValues = {}) => {
  const formState = reactive({ ...initialValues })
  const errors = reactive({})
  
  // 验证规则
  const validateRules = {}
  
  // 设置验证规则
  const setRules = (rules) => {
    Object.assign(validateRules, rules)
  }
  
  // 验证单个字段
  const validateField = (field, value) => {
    if (!validateRules[field]) return true
    
    const rules = validateRules[field]
    for (const rule of rules) {
      if (typeof rule === 'function') {
        if (!rule(value)) {
          errors[field] = rule.message || '验证失败'
          return false
        }
      } else if (rule.required && !value) {
        errors[field] = rule.message || '此字段为必填项'
        return false
      }
    }
    
    delete errors[field]
    return true
  }
  
  // 验证整个表单
  const validateForm = () => {
    let isValid = true
    Object.keys(validateRules).forEach(field => {
      if (!validateField(field, formState[field])) {
        isValid = false
      }
    })
    return isValid
  }
  
  // 设置字段值
  const setFieldValue = (field, value) => {
    formState[field] = value
    validateField(field, value)
  }
  
  // 重置表单
  const resetForm = () => {
    Object.keys(formState).forEach(key => {
      formState[key] = initialValues[key] || ''
    })
    Object.keys(errors).forEach(key => {
      delete errors[key]
    })
  }
  
  // 表单是否有效
  const isValid = computed(() => {
    return Object.keys(errors).length === 0
  })
  
  return {
    formState,
    errors,
    isValid,
    setRules,
    setFieldValue,
    validateField,
    validateForm,
    resetForm
  }
}

实际项目案例:任务管理应用

应用架构设计

让我们通过一个完整的任务管理应用来演示这些技术的综合运用:

// App.vue
<template>
  <div class="app" :class="globalStore.state.theme">
    <header class="app-header">
      <h1>任务管理系统</h1>
      <button @click="globalStore.toggleTheme">切换主题</button>
    </header>
    
    <main class="app-main">
      <div class="sidebar">
        <TaskFilter 
          :filter="currentFilter"
          @update-filter="handleFilterUpdate"
        />
      </div>
      
      <div class="content">
        <TaskList 
          :tasks="filteredTasks"
          :loading="fetching"
          @task-updated="handleTaskUpdated"
          @task-deleted="handleTaskDeleted"
        />
        
        <TaskForm 
          @task-created="handleTaskCreated"
        />
      </div>
    </main>
    
    <NotificationList :notifications="globalStore.state.notifications" />
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useGlobalStore } from '@/store'
import { useFetch } from '@/composables/useFetch'
import TaskFilter from './components/TaskFilter.vue'
import TaskList from './components/TaskList.vue'
import TaskForm from './components/TaskForm.vue'
import NotificationList from './components/NotificationList.vue'

const globalStore = useGlobalStore()
const { data: tasks, loading: fetching, execute: fetchTasks } = useFetch('/api/tasks')
const currentFilter = ref('all')

// 筛选任务
const filteredTasks = computed(() => {
  if (!tasks.value) return []
  
  switch (currentFilter.value) {
    case 'active':
      return tasks.value.filter(task => !task.completed)
    case 'completed':
      return tasks.value.filter(task => task.completed)
    default:
      return tasks.value
  }
})

const handleFilterUpdate = (filter) => {
  currentFilter.value = filter
}

const handleTaskCreated = async (taskData) => {
  try {
    const newTask = await fetch('/api/tasks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(taskData)
    }).then(res => res.json())
    
    tasks.value.push(newTask)
    globalStore.addNotification({
      type: 'success',
      message: '任务创建成功'
    })
  } catch (error) {
    globalStore.addNotification({
      type: 'error',
      message: '任务创建失败'
    })
  }
}

const handleTaskUpdated = async (taskData) => {
  try {
    const updatedTask = await fetch(`/api/tasks/${taskData.id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(taskData)
    }).then(res => res.json())
    
    const index = tasks.value.findIndex(t => t.id === taskData.id)
    if (index > -1) {
      tasks.value[index] = updatedTask
    }
    
    globalStore.addNotification({
      type: 'success',
      message: '任务更新成功'
    })
  } catch (error) {
    globalStore.addNotification({
      type: 'error',
      message: '任务更新失败'
    })
  }
}

const handleTaskDeleted = async (taskId) => {
  try {
    await fetch(`/api/tasks/${taskId}`, {
      method: 'DELETE'
    })
    
    const index = tasks.value.findIndex(t => t.id === taskId)
    if (index > -1) {
      tasks.value.splice(index, 1)
    }
    
    globalStore.addNotification({
      type: 'success',
      message: '任务删除成功'
    })
  } catch (error) {
    globalStore.addNotification({
      type: 'error',
      message: '任务删除失败'
    })
  }
}

// 初始化加载
fetchTasks()
</script>

<style scoped>
.app {
  min-height: 100vh;
  background-color: #f5f5f5;
  transition: background-color 0.3s ease;
}

.app-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem;
  background-color: #fff;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.app-main {
  display: flex;
  min-height: calc(100vh - 60px);
}

.sidebar {
  width: 250px;
  padding: 1rem;
  background-color: #fff;
  border-right: 1px solid #eee;
}

.content {
  flex: 1;
  padding: 1rem;
}
</style>

组件实现

// components/TaskFilter.vue
<template>
  <div class="task-filter">
    <h3>任务筛选</h3>
    <div class="filter-buttons">
      <button 
        v-for="filter in filters" 
        :key="filter.value"
        :class="{ active: currentFilter === filter.value }"
        @click="handleFilterClick(filter.value)"
      >
        {{ filter.label }}
      </button>
    </div>
  </div>
</template>

<script setup>
const props = defineProps({
  filter: {
    type: String,
    default: 'all'
  }
})

const emit = defineEmits(['updateFilter'])

const filters = [
  { value: 'all', label: '全部任务' },
  { value: 'active', label: '未完成' },
  { value: 'completed', label: '已完成' }
]

const currentFilter = computed(() => props.filter)

const handleFilterClick = (filter) => {
  emit('updateFilter', filter)
}
</script>

<style scoped>
.task-filter h3 {
  margin-top: 0;
  margin-bottom: 1rem;
}

.filter-buttons {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.filter-buttons button {
  padding: 0.5rem;
  border: 1px solid #ddd;
  background-color: #f8f9fa;
  cursor: pointer;
  transition: all 0.2s ease;
}

.filter-buttons button:hover {
  background-color: #e9ecef;
}

.filter-buttons button.active {
  background-color: #007bff;
  color: white;
  border-color: #007bff;
}
</style>
// components/TaskList.vue
<template>
  <div class="task-list">
    <div v-if="loading" class="loading">加载中...</div>
    
    <div v-else-if="tasks.length === 0" class="empty-state">
      暂无任务
    </div>
    
    <div v-else class="tasks-container">
      <TaskItem 
        v-for="task in tasks" 
        :key="task.id"
        :task="task"
        @update-task="handleUpdate"
        @delete-task="handleDelete"
      />
    </div>
  </div>
</template>

<script setup>
import TaskItem from './TaskItem.vue'

const props = defineProps({
  tasks: {
    type: Array,
    default: () => []
  },
  loading: {
    type: Boolean,
    default: false
  }
})

const emit = defineEmits(['taskUpdated', 'taskDeleted'])

const handleUpdate = (task) => {
  emit('taskUpdated', task)
}

const handleDelete = (taskId) => {
  emit('taskDeleted', taskId)
}
</script>

<style scoped>
.loading {
  text-align: center;
  padding: 2rem;
  color: #666;
}

.empty-state {
  text-align: center;
  padding: 2rem;
  color: #999;
}

.tasks-container {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
</style>
// components/TaskItem.vue
<template>
  <div class="task-item" :class="{ completed: task.completed }">
    <div class="task-content">
      <input 
        type="checkbox" 
        :checked="task.completed"
        @change="handleToggle"
      />
      <span class="task-text">{{ task.title }}</span>
    </div>
    
    <div class="task-actions">
      <button @click="handleEdit">编辑</button>
      <button @click="handleDelete">删除</button>
    </div>
  </div>
</template>

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

const props = defineProps({
  task: {
    type: Object,
    required: true
  }
})

const emit = defineEmits(['updateTask', 'deleteTask'])

const handleToggle = () => {
  const updatedTask = {
    ...props.task,
    completed: !props.task.completed
  }
  emit('updateTask', updatedTask)
}

const handleEdit = () => {
  // 这里可以打开编辑模态框
  console.log('编辑任务:', props.task)
}

const handleDelete = () => {
  emit('deleteTask', props.task.id)
}
</script>

<style scoped>
.task-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.75rem;
  background-color: #fff;
  border: 1px solid #eee;
  border-radius: 4px;
  transition: all 0.2s ease;
}

.task-item:hover {
  box-shadow: 0 
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000