Vue 3 Composition API 实战:从组件通信到状态共享的完整解决方案

Kyle74
Kyle74 2026-01-31T10:06:26+08:00
0 0 1

前言

Vue 3 的发布带来了全新的 Composition API,它为开发者提供了更灵活、更强大的组件开发方式。相比于传统的 Options API,Composition API 更加模块化和可组合,让代码组织更加清晰,逻辑复用更加方便。本文将深入探讨 Vue 3 Composition API 的高级用法,从基础的响应式数据管理到复杂的组件通信模式,帮助开发者构建可维护、可扩展的 Vue 应用架构。

一、Composition API 核心概念与基础用法

1.1 响应式数据管理

在 Vue 3 中,Composition API 的核心是响应式系统。我们可以通过 refreactive 来创建响应式数据:

import { ref, reactive } from 'vue'

// 使用 ref 创建响应式数据
const count = ref(0)
const message = ref('Hello Vue 3')

// 使用 reactive 创建响应式对象
const state = reactive({
  name: 'Vue',
  version: '3.0',
  features: ['Composition API', 'Better Performance']
})

// 访问和修改数据
console.log(count.value) // 0
count.value = 10
console.log(count.value) // 10

// 对于对象,可以直接访问属性
console.log(state.name) // Vue
state.name = 'Vue.js'

1.2 生命周期钩子

Composition API 提供了与 Options API 对应的生命周期钩子:

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

export default {
  setup() {
    onMounted(() => {
      console.log('组件已挂载')
    })
    
    onUpdated(() => {
      console.log('组件已更新')
    })
    
    onUnmounted(() => {
      console.log('组件即将卸载')
    })
    
    return {
      // 返回的数据和方法
    }
  }
}

1.3 计算属性与监听器

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

export default {
  setup() {
    const firstName = ref('John')
    const lastName = ref('Doe')
    
    // 计算属性
    const fullName = computed(() => {
      return `${firstName.value} ${lastName.value}`
    })
    
    // 监听器
    watch(firstName, (newVal, oldVal) => {
      console.log(`firstName 从 ${oldVal} 变为 ${newVal}`)
    })
    
    // 监听多个值
    watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
      console.log(`名字从 ${oldFirst} ${oldLast} 变为 ${newFirst} ${newLast}`)
    })
    
    return {
      firstName,
      lastName,
      fullName
    }
  }
}

二、组合式函数的创建与复用

2.1 创建可复用的组合式函数

组合式函数是 Composition API 的核心特性之一,它允许我们将逻辑封装成可复用的函数:

// composables/useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const increment = () => {
    count.value++
  }
  
  const decrement = () => {
    count.value--
  }
  
  const reset = () => {
    count.value = initialValue
  }
  
  const doubleCount = computed(() => count.value * 2)
  
  return {
    count,
    increment,
    decrement,
    reset,
    doubleCount
  }
}

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

export function useLocalStorage(key, defaultValue) {
  const value = ref(defaultValue)
  
  // 初始化时从 localStorage 中读取
  const storedValue = localStorage.getItem(key)
  if (storedValue) {
    value.value = JSON.parse(storedValue)
  }
  
  // 监听值的变化并保存到 localStorage
  watch(value, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true })
  
  return value
}

2.2 使用组合式函数

<template>
  <div>
    <p>计数器: {{ counter.count }}</p>
    <p>双倍计数: {{ counter.doubleCount }}</p>
    <button @click="counter.increment">增加</button>
    <button @click="counter.decrement">减少</button>
    <button @click="counter.reset">重置</button>
    
    <p>本地存储数据: {{ localStorageData }}</p>
    <input v-model="localStorageData" placeholder="输入数据">
  </div>
</template>

<script setup>
import { useCounter } from '@/composables/useCounter'
import { useLocalStorage } from '@/composables/useLocalStorage'

const counter = useCounter(0)
const localStorageData = useLocalStorage('userData', '默认值')
</script>

三、组件间通信模式

3.1 Props 传递数据

<!-- Parent.vue -->
<template>
  <div>
    <Child :message="parentMessage" :user-data="userData" />
    <button @click="updateParentData">更新父组件数据</button>
  </div>
</template>

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

const parentMessage = ref('来自父组件的消息')
const userData = ref({
  name: 'John',
  age: 25
})

const updateParentData = () => {
  parentMessage.value = '更新后的消息'
  userData.value.age++
}
</script>

<!-- Child.vue -->
<template>
  <div>
    <p>{{ message }}</p>
    <p>用户姓名: {{ userData.name }}</p>
    <p>用户年龄: {{ userData.age }}</p>
  </div>
</template>

<script setup>
// 定义 props
defineProps({
  message: {
    type: String,
    required: true
  },
  userData: {
    type: Object,
    required: true
  }
})
</script>

3.2 emit 事件通信

<!-- Parent.vue -->
<template>
  <div>
    <Child @child-event="handleChildEvent" />
    <p>接收到的子组件消息: {{ childMessage }}</p>
  </div>
</template>

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

const childMessage = ref('')

const handleChildEvent = (data) => {
  childMessage.value = data
}
</script>

<!-- Child.vue -->
<template>
  <div>
    <button @click="sendMessage">发送消息给父组件</button>
  </div>
</template>

<script setup>
// 定义 emits
const emit = defineEmits(['child-event'])

const sendMessage = () => {
  emit('child-event', '来自子组件的消息')
}
</script>

3.3 provide/inject 模式

// composables/useGlobalState.js
import { reactive, readonly } from 'vue'

export function useGlobalState() {
  const state = reactive({
    theme: 'light',
    language: 'zh-CN',
    user: null
  })
  
  const setTheme = (theme) => {
    state.theme = theme
  }
  
  const setUser = (user) => {
    state.user = user
  }
  
  return {
    state: readonly(state),
    setTheme,
    setUser
  }
}
<!-- App.vue -->
<template>
  <div :class="`app ${globalState.state.theme}`">
    <Header />
    <MainContent />
    <Footer />
  </div>
</template>

<script setup>
import { provide } from 'vue'
import { useGlobalState } from '@/composables/useGlobalState'
import Header from './components/Header.vue'
import MainContent from './components/MainContent.vue'
import Footer from './components/Footer.vue'

const { state, setTheme, setUser } = useGlobalState()

// 提供全局状态
provide('globalState', {
  state,
  setTheme,
  setUser
})
</script>

<!-- Header.vue -->
<template>
  <header>
    <h1>我的应用</h1>
    <button @click="toggleTheme">切换主题</button>
  </header>
</template>

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

const globalState = inject('globalState')

const toggleTheme = () => {
  const newTheme = globalState.state.theme === 'light' ? 'dark' : 'light'
  globalState.setTheme(newTheme)
}
</script>

四、状态管理与共享

4.1 基于响应式的简单状态管理

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

const state = reactive({
  users: [],
  currentUser: null,
  loading: false
})

const fetchUsers = async () => {
  state.loading = true
  try {
    const response = await fetch('/api/users')
    state.users = await response.json()
  } catch (error) {
    console.error('获取用户失败:', error)
  } finally {
    state.loading = false
  }
}

const setCurrentUser = (user) => {
  state.currentUser = user
}

const addUser = (user) => {
  state.users.push(user)
}

export const userStore = {
  state: readonly(state),
  fetchUsers,
  setCurrentUser,
  addUser
}
<!-- UserList.vue -->
<template>
  <div>
    <div v-if="userStore.state.loading">加载中...</div>
    <ul v-else>
      <li v-for="user in userStore.state.users" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
    <button @click="loadUsers">刷新用户列表</button>
  </div>
</template>

<script setup>
import { onMounted } from 'vue'
import { userStore } from '@/stores/userStore'

const loadUsers = async () => {
  await userStore.fetchUsers()
}

onMounted(() => {
  loadUsers()
})
</script>

4.2 复杂状态管理的实现

// stores/appStore.js
import { reactive, readonly } from 'vue'

export class AppStore {
  constructor() {
    this.state = reactive({
      // 应用配置
      config: {
        theme: 'light',
        language: 'zh-CN',
        notifications: true
      },
      
      // 用户相关状态
      user: null,
      
      // 页面状态
      currentPage: 'home',
      
      // 加载状态
      loading: false,
      
      // 错误信息
      error: null,
      
      // 缓存数据
      cache: new Map()
    })
    
    this.actions = {
      setTheme: (theme) => {
        this.state.config.theme = theme
      },
      
      setUser: (user) => {
        this.state.user = user
      },
      
      setCurrentPage: (page) => {
        this.state.currentPage = page
      },
      
      setLoading: (loading) => {
        this.state.loading = loading
      },
      
      setError: (error) => {
        this.state.error = error
      },
      
      setCache: (key, value) => {
        this.state.cache.set(key, value)
      },
      
      getCache: (key) => {
        return this.state.cache.get(key)
      }
    }
  }
  
  get state() {
    return readonly(this._state)
  }
  
  // 获取只读状态
  getReadOnlyState() {
    return readonly(this.state)
  }
  
  // 批量更新状态
  updateState(updates) {
    Object.assign(this.state, updates)
  }
}

// 创建全局 store 实例
export const appStore = new AppStore()
<!-- Dashboard.vue -->
<template>
  <div class="dashboard">
    <div class="header">
      <h1>仪表板</h1>
      <button @click="logout">退出登录</button>
    </div>
    
    <div class="content">
      <div class="stats">
        <div class="stat-card" v-for="stat in stats" :key="stat.title">
          <h3>{{ stat.title }}</h3>
          <p>{{ stat.value }}</p>
        </div>
      </div>
      
      <div class="chart">
        <canvas ref="chartCanvas"></canvas>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, watch } from 'vue'
import { appStore } from '@/stores/appStore'

const chartCanvas = ref(null)
const stats = ref([
  { title: '总用户数', value: '1,234' },
  { title: '活跃用户', value: '890' },
  { title: '新增用户', value: '123' }
])

// 监听用户变化
watch(() => appStore.state.user, (newUser, oldUser) => {
  if (!newUser && oldUser) {
    // 用户登出处理
    console.log('用户已登出')
  }
})

const logout = () => {
  appStore.actions.setUser(null)
  appStore.actions.setCurrentPage('login')
}

onMounted(() => {
  // 初始化图表
  initChart()
})

const initChart = () => {
  // 图表初始化逻辑
  console.log('初始化图表')
}
</script>

五、高级用法与最佳实践

5.1 异步操作处理

// composables/useAsyncData.js
import { ref, readonly } from 'vue'

export function useAsyncData(asyncFunction, initialValue = null) {
  const data = ref(initialValue)
  const loading = ref(false)
  const error = ref(null)
  
  const execute = async (...args) => {
    loading.value = true
    error.value = null
    
    try {
      const result = await asyncFunction(...args)
      data.value = result
      return result
    } catch (err) {
      error.value = err
      throw err
    } finally {
      loading.value = false
    }
  }
  
  return {
    data: readonly(data),
    loading: readonly(loading),
    error: readonly(error),
    execute
  }
}

// 使用示例
export default {
  setup() {
    const { data, loading, error, execute } = useAsyncData(
      async (userId) => {
        const response = await fetch(`/api/users/${userId}`)
        return response.json()
      },
      null
    )
    
    return {
      user: data,
      loading,
      error,
      fetchUser: execute
    }
  }
}

5.2 条件渲染与动态组件

<template>
  <div class="dynamic-component">
    <div class="tabs">
      <button 
        v-for="tab in tabs" 
        :key="tab.name"
        :class="{ active: activeTab === tab.name }"
        @click="activeTab = tab.name"
      >
        {{ tab.label }}
      </button>
    </div>
    
    <component 
      :is="activeComponent" 
      :data="currentData"
      @update-data="handleDataUpdate"
    />
  </div>
</template>

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

const tabs = [
  { name: 'profile', label: '个人信息' },
  { name: 'settings', label: '设置' },
  { name: 'security', label: '安全' }
]

const activeTab = ref('profile')
const currentData = ref({})

const activeComponent = computed(() => {
  switch (activeTab.value) {
    case 'profile':
      return 'ProfileComponent'
    case 'settings':
      return 'SettingsComponent'
    case 'security':
      return 'SecurityComponent'
    default:
      return 'ProfileComponent'
  }
})

const handleDataUpdate = (data) => {
  currentData.value = data
}
</script>

5.3 性能优化技巧

// composables/useMemo.js
import { ref, computed } from 'vue'

export function useMemo(computation, dependencies) {
  const cache = ref(null)
  const lastDependencies = ref([])
  
  return computed(() => {
    // 检查依赖是否发生变化
    const depsChanged = dependencies.some((dep, index) => {
      return dep !== lastDependencies.value[index]
    })
    
    if (depsChanged || !cache.value) {
      cache.value = computation()
      lastDependencies.value = [...dependencies]
    }
    
    return cache.value
  })
}

// 使用示例
export default {
  setup() {
    const items = ref([])
    const filter = ref('')
    
    // 计算过滤后的项目,只有当 items 或 filter 变化时才重新计算
    const filteredItems = useMemo(() => {
      return items.value.filter(item => 
        item.name.toLowerCase().includes(filter.value.toLowerCase())
      )
    }, [items, filter])
    
    return {
      filteredItems
    }
  }
}

5.4 错误边界处理

<template>
  <div class="error-boundary">
    <div v-if="hasError" class="error-container">
      <h2>发生错误</h2>
      <p>{{ error.message }}</p>
      <button @click="retry">重试</button>
    </div>
    <div v-else>
      <slot />
    </div>
  </div>
</template>

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

const hasError = ref(false)
const error = ref(null)

onErrorCaptured((err, instance, info) => {
  hasError.value = true
  error.value = err
  console.error('错误捕获:', err, info)
  return false // 阻止错误继续向上传播
})

const retry = () => {
  hasError.value = false
  error.value = null
}
</script>

六、实际项目架构示例

6.1 完整的用户管理系统架构

// stores/userManagementStore.js
import { reactive, readonly } from 'vue'

export class UserManagementStore {
  constructor() {
    this.state = reactive({
      users: [],
      currentUser: null,
      pagination: {
        page: 1,
        pageSize: 20,
        total: 0
      },
      filters: {
        search: '',
        role: '',
        status: ''
      },
      loading: false,
      error: null
    })
    
    this.api = {
      // 用户相关 API 调用
      fetchUsers: async (params = {}) => {
        this.state.loading = true
        try {
          const response = await fetch('/api/users', {
            method: 'GET',
            headers: {
              'Content-Type': 'application/json'
            },
            params: {
              ...this.state.pagination,
              ...this.state.filters,
              ...params
            }
          })
          
          const data = await response.json()
          this.state.users = data.items
          this.state.pagination.total = data.total
          return data
        } catch (error) {
          this.state.error = error
          throw error
        } finally {
          this.state.loading = false
        }
      },
      
      createUser: async (userData) => {
        this.state.loading = true
        try {
          const response = await fetch('/api/users', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify(userData)
          })
          
          const user = await response.json()
          this.state.users.push(user)
          return user
        } catch (error) {
          this.state.error = error
          throw error
        } finally {
          this.state.loading = false
        }
      },
      
      updateUser: async (userId, userData) => {
        this.state.loading = true
        try {
          const response = await fetch(`/api/users/${userId}`, {
            method: 'PUT',
            headers: {
              'Content-Type': 'application/json'
            },
            body: JSON.stringify(userData)
          })
          
          const user = await response.json()
          const index = this.state.users.findIndex(u => u.id === userId)
          if (index !== -1) {
            this.state.users[index] = user
          }
          return user
        } catch (error) {
          this.state.error = error
          throw error
        } finally {
          this.state.loading = false
        }
      },
      
      deleteUser: async (userId) => {
        this.state.loading = true
        try {
          await fetch(`/api/users/${userId}`, {
            method: 'DELETE'
          })
          
          const index = this.state.users.findIndex(u => u.id === userId)
          if (index !== -1) {
            this.state.users.splice(index, 1)
          }
        } catch (error) {
          this.state.error = error
          throw error
        } finally {
          this.state.loading = false
        }
      }
    }
    
    this.actions = {
      setCurrentUser: (user) => {
        this.state.currentUser = user
      },
      
      setPagination: (pagination) => {
        Object.assign(this.state.pagination, pagination)
      },
      
      setFilters: (filters) => {
        Object.assign(this.state.filters, filters)
      },
      
      clearError: () => {
        this.state.error = null
      }
    }
  }
  
  get state() {
    return readonly(this.state)
  }
}

export const userManagementStore = new UserManagementStore()
<!-- UserListPage.vue -->
<template>
  <div class="user-list-page">
    <div class="page-header">
      <h1>用户管理</h1>
      <button @click="showCreateModal = true">添加用户</button>
    </div>
    
    <div class="filters">
      <input 
        v-model="userManagementStore.state.filters.search" 
        placeholder="搜索用户名或邮箱"
        @input="debouncedSearch"
      />
      <select v-model="userManagementStore.state.filters.role">
        <option value="">所有角色</option>
        <option value="admin">管理员</option>
        <option value="user">普通用户</option>
      </select>
      <select v-model="userManagementStore.state.filters.status">
        <option value="">所有状态</option>
        <option value="active">活跃</option>
        <option value="inactive">非活跃</option>
      </select>
      <button @click="loadUsers">刷新</button>
    </div>
    
    <div class="loading-overlay" v-if="userManagementStore.state.loading">
      加载中...
    </div>
    
    <div class="users-table">
      <table>
        <thead>
          <tr>
            <th>用户名</th>
            <th>邮箱</th>
            <th>角色</th>
            <th>状态</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="user in userManagementStore.state.users" :key="user.id">
            <td>{{ user.username }}</td>
            <td>{{ user.email }}</td>
            <td>{{ user.role }}</td>
            <td>{{ user.status }}</td>
            <td>
              <button @click="editUser(user)">编辑</button>
              <button @click="deleteUser(user.id)">删除</button>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
    
    <div class="pagination">
      <button 
        @click="changePage(page - 1)" 
        :disabled="userManagementStore.state.pagination.page <= 1"
      >
        上一页
      </button>
      <span>第 {{ userManagementStore.state.pagination.page }} 页</span>
      <button 
        @click="changePage(page + 1)" 
        :disabled="userManagementStore.state.pagination.page >= totalPages"
      >
        下一页
      </button>
    </div>
    
    <!-- 用户创建/编辑模态框 -->
    <UserModal 
      v-model:visible="showCreateModal" 
      :user="editingUser"
      @save="handleSaveUser"
    />
  </div>
</template>

<script setup>
import { ref, onMounted, watch } from 'vue'
import { userManagementStore } from '@/stores/userManagementStore'
import UserModal from '@/components/UserModal.vue'

const showCreateModal = ref(false)
const editingUser = ref(null)

// 计算总页数
const totalPages = computed(() => {
  return Math.ceil(
    userManagementStore.state.pagination.total / 
    userManagementStore.state.pagination.pageSize
  )
})

// 加载用户列表
const loadUsers = async () => {
  try {
    await userManagementStore.api.fetchUsers()
  } catch (error) {
    console.error('加载用户失败:', error)
  }
}

// 分页改变
const changePage = (page) => {
  if (page >= 1 && page <= totalPages.value) {
    userManagementStore.actions.setPagination({ page })
    loadUsers()
  }
}

// 搜索防抖
const debouncedSearch = debounce(() => {
  userManagementStore.actions.setPagination({ page: 1 })
  loadUsers()
}, 300)

// 删除用户
const deleteUser = async (userId) => {
  if (confirm('确定要删除这个用户吗?')) {
    try {
      await userManagementStore.api.deleteUser(userId)
      // 重新加载列表
      loadUsers()
    } catch (error) {
      console.error('删除用户失败:', error)
    }
  }
}

// 编辑用户
const editUser = (user) => {
  editingUser.value = { ...user }
  showCreateModal.value = true
}

// 处理保存用户
const handleSaveUser = async (userData) => {
  try {
    if (editingUser.value?.id) {
      await userManagementStore.api.updateUser(editingUser.value.id, userData)
    } else {
      await userManagementStore.api.createUser(userData)
    }
    
    // 重新加载列表
    loadUsers()
    showCreateModal.value = false
    editingUser.value = null
  } catch (error) {
    console.error('保存用户失败:', error)
  }
}

// 监听过滤条件变化
watch(
  () => userManagementStore.state.filters,
  () => {
    userManagementStore.actions.setPagination({ page: 1 })
    loadUsers()
  },
  { deep: true }
)

// 初始化加载
onMounted(() => {
  loadUsers()
})

// 防抖函数
function debounce(func, wait) {
  let timeout
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout)
      func(...args)
    }
    clearTimeout(timeout)
    timeout = setTimeout(later, wait)
  }
}
</script>

结语

Vue 3 的 Composition API 为前端开发带来了革命性的变化,它让组件逻辑更加清晰、可复用性更强。通过本文的详细介绍,我们看到了从基础的响应式数据管理到复杂的状态共享和组件通信模式的完整解决方案。

在实际项目中,建议遵循以下最佳实践:

  1. 合理组织代码结构:将逻辑提取到组合式函数中,提高代码复用性
  2. 状态管理规范化:建立统一的状态管理机制,避免状态混乱
  3. 性能优化:合理使用计算属性、监听
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000