Vue 3 Composition API最佳实践:从基础语法到复杂组件状态管理的完整指南

David99
David99 2026-03-01T07:08:11+08:00
0 0 0

引言

Vue 3的发布带来了革命性的变化,其中最引人注目的就是Composition API的引入。相比于Vue 2的Options API,Composition API提供了一种更加灵活和强大的方式来组织和管理组件逻辑。本文将深入探讨Composition API的核心概念、使用技巧以及最佳实践,帮助开发者构建更加高效和可维护的前端应用。

什么是Composition API

核心概念

Composition API是Vue 3中引入的一种新的组件逻辑组织方式。它允许开发者将组件的逻辑按照功能进行分组,而不是按照选项类型进行组织。这种组织方式使得代码更加灵活,更容易复用和维护。

在Vue 2中,我们通常按照datamethodscomputedwatch等选项来组织组件逻辑。而Composition API则允许我们按照功能模块来组织代码,比如将所有与用户相关的逻辑放在一起,将所有与数据获取相关的逻辑放在一起。

与Options API的区别

// Vue 2 Options API
export default {
  data() {
    return {
      count: 0,
      name: 'Vue'
    }
  },
  computed: {
    reversedName() {
      return this.name.split('').reverse().join('')
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  watch: {
    count(newVal, oldVal) {
      console.log(`count changed from ${oldVal} to ${newVal}`)
    }
  }
}

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

export default {
  setup() {
    const count = ref(0)
    const name = ref('Vue')
    
    const reversedName = computed(() => {
      return name.value.split('').reverse().join('')
    })
    
    const increment = () => {
      count.value++
    }
    
    watch(count, (newVal, oldVal) => {
      console.log(`count changed from ${oldVal} to ${newVal}`)
    })
    
    return {
      count,
      name,
      reversedName,
      increment
    }
  }
}

响应式数据处理基础

ref和reactive的基础使用

在Composition API中,响应式数据的处理主要依赖于refreactive两个核心函数。

import { ref, reactive } from 'vue'

// 使用ref创建响应式数据
const count = ref(0)
const name = ref('Vue')
const isActive = ref(true)

// 使用reactive创建响应式对象
const user = reactive({
  name: 'Vue',
  age: 3,
  email: 'vue@vuejs.org'
})

// 访问响应式数据
console.log(count.value) // 0
console.log(name.value)  // 'Vue'

// 修改响应式数据
count.value = 1
name.value = 'Vue 3'

ref的深层理解

ref会将传入的值包装成一个响应式对象,这个对象有一个value属性指向原始值。对于基本类型数据,ref是必需的;而对于对象类型数据,我们通常使用reactive

import { ref, reactive } from 'vue'

// 基本类型使用ref
const count = ref(0)
const message = ref('Hello')

// 对象类型使用reactive
const userInfo = reactive({
  name: 'Vue',
  age: 3,
  address: {
    city: 'Shanghai',
    country: 'China'
  }
})

// 数组使用reactive
const items = reactive([1, 2, 3, 4])

// ref在模板中的使用
// <template>
//   <p>{{ count }}</p>
//   <p>{{ message }}</p>
//   <p>{{ userInfo.name }}</p>
// </template>

reactive的使用技巧

reactive函数可以将对象转换为响应式对象,但需要注意的是,它不能替代ref来处理基本类型数据。

import { reactive } from 'vue'

// 创建响应式对象
const state = reactive({
  count: 0,
  name: 'Vue',
  user: {
    profile: {
      avatar: 'avatar.png'
    }
  }
})

// 修改嵌套对象属性
state.user.profile.avatar = 'new-avatar.png'
state.count = 10

// 注意:直接替换整个对象会丢失响应性
// state = reactive({ count: 5 }) // 这样做会丢失响应性

组合式函数的封装与复用

创建组合式函数的基本模式

组合式函数是Vue 3中实现逻辑复用的核心机制。它们本质上是函数,接收参数并返回响应式数据和方法。

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

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

// 在组件中使用
import { useCounter } from '@/composables/useCounter'

export default {
  setup() {
    const { count, increment, decrement, reset } = useCounter(10)
    
    return {
      count,
      increment,
      decrement,
      reset
    }
  }
}

复杂组合式函数示例

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

export function useApi(url, options = {}) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const fetchData = async () => {
    loading.value = true
    error.value = null
    
    try {
      const response = await fetch(url, options)
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      data.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  // 自动获取数据
  if (options.autoFetch !== false) {
    fetchData()
  }
  
  // 监听URL变化,重新获取数据
  watch(url, fetchData)
  
  return {
    data,
    loading,
    error,
    refetch: fetchData
  }
}

// 在组件中使用
export default {
  setup() {
    const { data, loading, error, refetch } = useApi('/api/users')
    
    return {
      data,
      loading,
      error,
      refetch
    }
  }
}

组合式函数中的副作用处理

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

export function useLocalStorage(key, defaultValue) {
  const value = ref(defaultValue)
  
  // 从localStorage初始化
  const initValue = localStorage.getItem(key)
  if (initValue) {
    try {
      value.value = JSON.parse(initValue)
    } catch (e) {
      console.error('Failed to parse localStorage value:', e)
    }
  }
  
  // 监听值变化并保存到localStorage
  watch(value, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue))
  }, { deep: true })
  
  return value
}

// 使用示例
export default {
  setup() {
    const userPreferences = useLocalStorage('user-preferences', {
      theme: 'light',
      language: 'zh-CN'
    })
    
    return {
      userPreferences
    }
  }
}

状态管理与跨组件通信

简单的状态管理

对于简单的应用状态管理,我们可以使用reactive来创建全局状态。

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

export const appStore = reactive({
  user: null,
  theme: 'light',
  language: 'zh-CN',
  notifications: [],
  
  setUser(user) {
    this.user = user
  },
  
  setTheme(theme) {
    this.theme = theme
  },
  
  addNotification(notification) {
    this.notifications.push({
      id: Date.now(),
      ...notification
    })
  },
  
  removeNotification(id) {
    this.notifications = this.notifications.filter(n => n.id !== id)
  }
})

// 在组件中使用
import { appStore } from '@/stores/appStore'

export default {
  setup() {
    const { user, theme, notifications } = appStore
    
    const login = (userData) => {
      appStore.setUser(userData)
    }
    
    const changeTheme = (newTheme) => {
      appStore.setTheme(newTheme)
    }
    
    return {
      user,
      theme,
      notifications,
      login,
      changeTheme
    }
  }
}

使用provide和inject进行状态传递

// parent.vue
import { provide, reactive } from 'vue'

export default {
  setup() {
    const globalState = reactive({
      user: null,
      theme: 'light',
      loading: false
    })
    
    provide('globalState', globalState)
    
    const updateUser = (user) => {
      globalState.user = user
    }
    
    return {
      updateUser
    }
  }
}

// child.vue
import { inject } from 'vue'

export default {
  setup() {
    const globalState = inject('globalState')
    
    const changeTheme = (theme) => {
      globalState.theme = theme
    }
    
    return {
      globalState,
      changeTheme
    }
  }
}

复杂状态管理的实现

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

const state = reactive({
  currentUser: null,
  isAuthenticated: false,
  permissions: [],
  profile: null
})

const getters = {
  hasPermission: (permission) => {
    return state.permissions.includes(permission)
  },
  
  canAccess: (route) => {
    // 实现路由访问控制逻辑
    return true
  }
}

const actions = {
  async login(credentials) {
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(credentials)
      })
      
      const data = await response.json()
      
      state.currentUser = data.user
      state.isAuthenticated = true
      state.permissions = data.permissions
      state.profile = data.profile
      
      return data
    } catch (error) {
      throw new Error('Login failed')
    }
  },
  
  logout() {
    state.currentUser = null
    state.isAuthenticated = false
    state.permissions = []
    state.profile = null
  }
}

// 提供只读状态访问
export const useUserStore = () => {
  return {
    state: readonly(state),
    getters,
    actions
  }
}

高级特性与最佳实践

计算属性和监听器的高级用法

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

export default {
  setup() {
    const firstName = ref('')
    const lastName = ref('')
    const age = ref(0)
    
    // 计算属性
    const fullName = computed(() => {
      return `${firstName.value} ${lastName.value}`
    })
    
    const isAdult = computed(() => {
      return age.value >= 18
    })
    
    // 带有getter和setter的计算属性
    const displayName = computed({
      get: () => {
        return `${firstName.value} ${lastName.value}`
      },
      set: (value) => {
        const names = value.split(' ')
        firstName.value = names[0]
        lastName.value = names[1]
      }
    })
    
    // 监听器
    const watchHandler = (newVal, oldVal) => {
      console.log(`Value changed from ${oldVal} to ${newVal}`)
    }
    
    watch(firstName, watchHandler)
    watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
      console.log(`Name changed from ${oldFirst} ${oldLast} to ${newFirst} ${newLast}`)
    })
    
    // watchEffect
    const watchEffectHandler = watchEffect(() => {
      console.log(`Full name is: ${fullName.value}`)
      console.log(`Is adult: ${isAdult.value}`)
    })
    
    // 清理副作用
    const cleanup = watchEffect((onInvalidate) => {
      const timer = setTimeout(() => {
        console.log('Timer completed')
      }, 1000)
      
      onInvalidate(() => {
        clearTimeout(timer)
        console.log('Timer cleared')
      })
    })
    
    return {
      firstName,
      lastName,
      age,
      fullName,
      isAdult,
      displayName,
      watchEffectHandler
    }
  }
}

异步操作和错误处理

import { ref, reactive } from 'vue'

export default {
  setup() {
    const loading = ref(false)
    const error = ref(null)
    const data = ref(null)
    
    const fetchData = async (url) => {
      loading.value = true
      error.value = null
      
      try {
        const response = await fetch(url)
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`)
        }
        
        const result = await response.json()
        data.value = result
      } catch (err) {
        error.value = err.message
        console.error('Fetch error:', err)
      } finally {
        loading.value = false
      }
    }
    
    const handleAsyncOperation = async () => {
      try {
        const result = await someAsyncFunction()
        // 处理结果
        return result
      } catch (err) {
        // 统一错误处理
        error.value = err.message
        throw err
      }
    }
    
    return {
      loading,
      error,
      data,
      fetchData,
      handleAsyncOperation
    }
  }
}

性能优化技巧

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

export default {
  setup() {
    const count = ref(0)
    const list = ref([])
    
    // 使用computed缓存计算结果
    const expensiveValue = computed(() => {
      // 模拟耗时计算
      return list.value.reduce((sum, item) => sum + item.value, 0)
    })
    
    // 优化监听器
    const optimizedWatch = watch(
      () => count.value,
      (newVal, oldVal) => {
        // 只在必要时执行
        if (newVal > 100) {
          console.log('Count is large')
        }
      },
      { flush: 'post' } // 在DOM更新后执行
    )
    
    // 防抖和节流
    const debouncedHandler = debounce((value) => {
      console.log('Debounced:', value)
    }, 300)
    
    const throttledHandler = throttle((value) => {
      console.log('Throttled:', value)
    }, 1000)
    
    // 生命周期钩子
    onMounted(() => {
      console.log('Component mounted')
      // 初始化操作
    })
    
    onUnmounted(() => {
      console.log('Component unmounted')
      // 清理操作
    })
    
    return {
      count,
      list,
      expensiveValue,
      debouncedHandler,
      throttledHandler
    }
  }
}

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

// 节流函数
function throttle(func, limit) {
  let inThrottle
  return function(...args) {
    if (!inThrottle) {
      func(...args)
      inThrottle = true
      setTimeout(() => inThrottle = false, limit)
    }
  }
}

实际项目应用案例

电商商品列表组件

<template>
  <div class="product-list">
    <div class="controls">
      <input v-model="searchQuery" placeholder="搜索商品..." />
      <select v-model="sortBy">
        <option value="name">按名称排序</option>
        <option value="price">按价格排序</option>
        <option value="rating">按评分排序</option>
      </select>
      <button @click="loadMore">加载更多</button>
    </div>
    
    <div v-if="loading" class="loading">加载中...</div>
    
    <div v-if="error" class="error">{{ error }}</div>
    
    <div class="products">
      <product-card 
        v-for="product in filteredProducts" 
        :key="product.id"
        :product="product"
        @favorite="toggleFavorite"
      />
    </div>
    
    <div v-if="hasMore" class="load-more">
      <button @click="loadMore">加载更多</button>
    </div>
  </div>
</template>

<script>
import { ref, computed, watch } from 'vue'
import { useApi } from '@/composables/useApi'
import { useLocalStorage } from '@/composables/useLocalStorage'

export default {
  setup() {
    // 响应式数据
    const searchQuery = ref('')
    const sortBy = ref('name')
    const page = ref(1)
    const favorites = useLocalStorage('favorites', [])
    
    // API调用
    const { data, loading, error, refetch } = useApi(
      `/api/products?page=${page.value}&limit=20`
    )
    
    // 计算属性
    const products = computed(() => data?.products || [])
    
    const filteredProducts = computed(() => {
      let result = products.value
      
      // 搜索过滤
      if (searchQuery.value) {
        const query = searchQuery.value.toLowerCase()
        result = result.filter(product => 
          product.name.toLowerCase().includes(query) ||
          product.description.toLowerCase().includes(query)
        )
      }
      
      // 排序
      result.sort((a, b) => {
        switch (sortBy.value) {
          case 'price':
            return a.price - b.price
          case 'rating':
            return b.rating - a.rating
          default:
            return a.name.localeCompare(b.name)
        }
      })
      
      return result
    })
    
    const hasMore = computed(() => {
      return data?.hasMore || false
    })
    
    // 方法
    const loadMore = () => {
      page.value++
      refetch()
    }
    
    const toggleFavorite = (productId) => {
      const index = favorites.value.indexOf(productId)
      if (index > -1) {
        favorites.value.splice(index, 1)
      } else {
        favorites.value.push(productId)
      }
    }
    
    // 监听器
    watch([searchQuery, sortBy], () => {
      page.value = 1
      refetch()
    })
    
    return {
      searchQuery,
      sortBy,
      loading,
      error,
      filteredProducts,
      hasMore,
      loadMore,
      toggleFavorite
    }
  }
}
</script>

<style scoped>
.product-list {
  padding: 20px;
}

.controls {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}

.products {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 20px;
}

.loading, .error {
  text-align: center;
  padding: 20px;
}

.load-more {
  text-align: center;
  margin-top: 20px;
}
</style>

用户管理面板组件

<template>
  <div class="user-panel">
    <div class="header">
      <h2>用户管理</h2>
      <button @click="showCreateForm = true">添加用户</button>
    </div>
    
    <div class="filters">
      <input v-model="filter.name" placeholder="用户名搜索" />
      <select v-model="filter.role">
        <option value="">所有角色</option>
        <option value="admin">管理员</option>
        <option value="user">普通用户</option>
        <option value="guest">访客</option>
      </select>
      <button @click="resetFilters">重置</button>
    </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 filteredUsers" :key="user.id">
            <td>{{ user.name }}</td>
            <td>{{ user.email }}</td>
            <td><span class="role-badge" :class="user.role">{{ user.role }}</span></td>
            <td>
              <span :class="user.isActive ? 'status-active' : 'status-inactive'">
                {{ user.isActive ? '活跃' : '非活跃' }}
              </span>
            </td>
            <td class="actions">
              <button @click="editUser(user)">编辑</button>
              <button @click="deleteUser(user.id)">删除</button>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
    
    <pagination 
      :current-page="currentPage" 
      :total-pages="totalPages" 
      @page-changed="handlePageChange"
    />
    
    <!-- 用户表单模态框 -->
    <user-form 
      v-if="showCreateForm || editingUser" 
      :user="editingUser" 
      @save="saveUser"
      @cancel="cancelForm"
    />
  </div>
</template>

<script>
import { ref, computed, watch } from 'vue'
import { useApi } from '@/composables/useApi'
import UserForm from './UserForm.vue'
import Pagination from './Pagination.vue'

export default {
  components: {
    UserForm,
    Pagination
  },
  
  setup() {
    // 响应式数据
    const currentPage = ref(1)
    const filter = ref({
      name: '',
      role: ''
    })
    
    const showCreateForm = ref(false)
    const editingUser = ref(null)
    
    // API调用
    const { data, loading, error, refetch } = useApi(
      `/api/users?page=${currentPage.value}&limit=10`
    )
    
    // 计算属性
    const users = computed(() => data?.users || [])
    const totalPages = computed(() => data?.totalPages || 1)
    
    const filteredUsers = computed(() => {
      let result = users.value
      
      if (filter.value.name) {
        const name = filter.value.name.toLowerCase()
        result = result.filter(user => 
          user.name.toLowerCase().includes(name)
        )
      }
      
      if (filter.value.role) {
        result = result.filter(user => 
          user.role === filter.value.role
        )
      }
      
      return result
    })
    
    // 方法
    const handlePageChange = (page) => {
      currentPage.value = page
      refetch()
    }
    
    const resetFilters = () => {
      filter.value = { name: '', role: '' }
    }
    
    const editUser = (user) => {
      editingUser.value = { ...user }
      showCreateForm.value = true
    }
    
    const deleteUser = async (userId) => {
      if (confirm('确定要删除这个用户吗?')) {
        try {
          await fetch(`/api/users/${userId}`, { method: 'DELETE' })
          await refetch()
        } catch (err) {
          console.error('删除用户失败:', err)
        }
      }
    }
    
    const saveUser = async (userData) => {
      try {
        const method = userData.id ? 'PUT' : 'POST'
        const url = userData.id ? `/api/users/${userData.id}` : '/api/users'
        
        await fetch(url, {
          method,
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(userData)
        })
        
        await refetch()
        showCreateForm.value = false
        editingUser.value = null
      } catch (err) {
        console.error('保存用户失败:', err)
      }
    }
    
    const cancelForm = () => {
      showCreateForm.value = false
      editingUser.value = null
    }
    
    // 监听器
    watch([currentPage, filter], () => {
      refetch()
    })
    
    return {
      currentPage,
      filter,
      showCreateForm,
      editingUser,
      loading,
      error,
      filteredUsers,
      totalPages,
      handlePageChange,
      resetFilters,
      editUser,
      deleteUser,
      saveUser,
      cancelForm
    }
  }
}
</script>

<style scoped>
.user-panel {
  padding: 20px;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.filters {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}

.users-table {
  overflow-x: auto;
  margin-bottom: 20px;
}

table {
  width: 100%;
  border-collapse: collapse;
}

th, td {
  padding: 12px;
  text-align: left;
  border-bottom: 1px solid #ddd;
}

.role-badge {
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 12px;
  font-weight: bold;
}

.role-badge.admin {
  background-color: #ff6b6b;
  color: white;
}

.role-badge.user {
  background-color: #4ecdc4;
  color: white;
}

.role-badge.guest {
  background-color: #45b7d1;
  color: white;
}

.status-active {
  color: #28a745;
  font-weight: bold;
}

.status-inactive {
  color: #dc3545;
  font-weight: bold;
}

.actions button {
  margin-right: 5px;
  padding: 4px 8px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.actions button:first-child {
  background-color: #007bff;
  color: white;
}

.actions button:last-child {
  background-color: #dc3545;
  color: white;
}
</style>

总结与展望

Vue 3的Composition API为前端开发带来了革命性的变化,它不仅提供了更加灵活的组件逻辑组织方式,还大大增强了代码的可复用性和可维护性。通过本文的介绍,我们可以看到:

  1. 响应式数据处理refreactive的正确使用是基础,理解它们的区别和适用场景至关重要。

  2. 组合式函数:这是实现逻辑复用的核心机制,通过合理设计

相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000