Vue 3 Composition API实战指南:组件化开发与状态管理最佳实践

Yara968
Yara968 2026-02-03T02:01:40+08:00
0 0 1

引言

Vue 3 的发布带来了革命性的变化,其中最引人注目的就是 Composition API 的引入。相比于传统的 Options API,Composition API 提供了更灵活、更强大的组件逻辑复用方式,极大地提升了代码的可维护性和开发效率。本文将深入探讨 Vue 3 Composition API 的核心概念、使用技巧以及在实际项目中的最佳实践。

Vue 3 Composition API 核心概念

什么是 Composition API

Composition API 是 Vue 3 中引入的一种新的组件逻辑组织方式。它允许开发者以函数的形式组织和复用组件逻辑,而不再局限于传统的选项(options)形式。通过 Composition API,我们可以将相关的逻辑代码组合在一起,使组件更加清晰和易于维护。

与 Options API 的对比

在 Vue 2 中,我们通常使用 datamethodscomputedwatch 等选项来组织组件逻辑。这种方式在小型组件中表现良好,但在大型复杂组件中容易导致逻辑分散,难以维护。

// Vue 2 Options API 示例
export default {
  data() {
    return {
      count: 0,
      message: ''
    }
  },
  computed: {
    doubledCount() {
      return this.count * 2;
    }
  },
  methods: {
    increment() {
      this.count++;
    }
  },
  watch: {
    count(newVal, oldVal) {
      console.log(`count changed from ${oldVal} to ${newVal}`);
    }
  }
}

而使用 Composition API,我们可以将相关的逻辑组合在一起:

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

export default {
  setup() {
    const count = ref(0);
    const message = ref('');
    
    const doubledCount = computed(() => count.value * 2);
    
    const increment = () => {
      count.value++;
    };
    
    watch(count, (newVal, oldVal) => {
      console.log(`count changed from ${oldVal} to ${newVal}`);
    });
    
    return {
      count,
      message,
      doubledCount,
      increment
    }
  }
}

响应式系统详解

ref 和 reactive 的使用

在 Composition API 中,响应式系统是核心概念之一。refreactive 是两个主要的响应式API:

import { ref, reactive } from 'vue';

// 使用 ref 创建响应式变量
const count = ref(0);
const message = ref('Hello World');

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

// 访问和修改值
console.log(count.value); // 0
count.value = 1; // 修改值

console.log(state.name); // Vue
state.name = 'Vue 3'; // 修改对象属性

响应式数据的解构问题

需要注意的是,当从 reactive 对象中解构出属性时,会失去响应性:

import { reactive } from 'vue';

const state = reactive({
  count: 0,
  message: 'Hello'
});

// ❌ 错误:解构会丢失响应性
const { count, message } = state;

// ✅ 正确:使用 ref 或保持对象引用
const countRef = ref(state.count);
const messageRef = ref(state.message);

// 或者直接使用原始对象
const getCount = () => state.count;

computed 的高级用法

computed 函数支持更复杂的计算逻辑:

import { ref, computed } from 'vue';

export default {
  setup() {
    const firstName = ref('John');
    const lastName = ref('Doe');
    const age = ref(30);
    
    // 基本计算属性
    const fullName = computed(() => `${firstName.value} ${lastName.value}`);
    
    // 带有 getter 和 setter 的计算属性
    const displayName = computed({
      get: () => `${firstName.value} ${lastName.value}`,
      set: (value) => {
        const names = value.split(' ');
        firstName.value = names[0];
        lastName.value = names[1];
      }
    });
    
    // 复杂计算
    const userStatus = computed(() => {
      if (age.value < 18) return 'minor';
      if (age.value < 65) return 'adult';
      return 'senior';
    });
    
    return {
      firstName,
      lastName,
      age,
      fullName,
      displayName,
      userStatus
    }
  }
}

组件通信实战

父子组件通信

在 Composition API 中,父子组件通信依然遵循 Vue 的设计原则:

<!-- Parent.vue -->
<template>
  <div>
    <h2>Parent Component</h2>
    <Child 
      :message="parentMessage" 
      :count="count"
      @child-event="handleChildEvent"
    />
    <button @click="increment">Increment: {{ count }}</button>
  </div>
</template>

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

const parentMessage = ref('Hello from Parent');
const count = ref(0);

const increment = () => {
  count.value++;
};

const handleChildEvent = (data) => {
  console.log('Received from child:', data);
};
</script>
<!-- Child.vue -->
<template>
  <div>
    <h3>Child Component</h3>
    <p>{{ message }}</p>
    <p>Count: {{ count }}</p>
    <button @click="sendToParent">Send to Parent</button>
  </div>
</template>

<script setup>
import { defineProps, defineEmits } from 'vue';

const props = defineProps({
  message: String,
  count: Number
});

const emit = defineEmits(['child-event']);

const sendToParent = () => {
  emit('child-event', {
    timestamp: Date.now(),
    data: 'Hello from child'
  });
};
</script>

兄弟组件通信

使用事件总线或全局状态管理来实现兄弟组件通信:

<!-- ComponentA.vue -->
<template>
  <div>
    <h3>Component A</h3>
    <button @click="sendData">Send Data</button>
  </div>
</template>

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

const eventBus = inject('eventBus');

const sendData = () => {
  eventBus.emit('data-sent', {
    message: 'Hello from Component A',
    timestamp: Date.now()
  });
};
</script>
<!-- ComponentB.vue -->
<template>
  <div>
    <h3>Component B</h3>
    <p>Received: {{ receivedData }}</p>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

const receivedData = ref('');

const eventBus = inject('eventBus');

onMounted(() => {
  eventBus.on('data-sent', (data) => {
    receivedData.value = data.message;
  });
});

onUnmounted(() => {
  eventBus.off('data-sent');
});
</script>

状态管理最佳实践

使用 Pinia 进行状态管理

Pinia 是 Vue 3 推荐的状态管理库,它提供了更简洁的 API 和更好的 TypeScript 支持:

// stores/user.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useUserStore = defineStore('user', () => {
  // 状态
  const user = ref(null);
  const isLoggedIn = ref(false);
  
  // 计算属性
  const userName = computed(() => user.value?.name || '');
  const userRole = computed(() => user.value?.role || 'guest');
  
  // 方法
  const login = (userData) => {
    user.value = userData;
    isLoggedIn.value = true;
  };
  
  const logout = () => {
    user.value = null;
    isLoggedIn.value = false;
  };
  
  const updateProfile = (profileData) => {
    if (user.value) {
      user.value = { ...user.value, ...profileData };
    }
  };
  
  return {
    user,
    isLoggedIn,
    userName,
    userRole,
    login,
    logout,
    updateProfile
  };
});
<!-- UserComponent.vue -->
<template>
  <div>
    <div v-if="isLoggedIn">
      <p>Welcome, {{ userName }}!</p>
      <p>Role: {{ userRole }}</p>
      <button @click="logout">Logout</button>
    </div>
    <div v-else>
      <button @click="login">Login</button>
    </div>
  </div>
</template>

<script setup>
import { useUserStore } from '@/stores/user';
import { onMounted } from 'vue';

const userStore = useUserStore();

const login = () => {
  userStore.login({
    name: 'John Doe',
    role: 'admin'
  });
};

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

onMounted(() => {
  // 初始化用户状态
  if (localStorage.getItem('user')) {
    const userData = JSON.parse(localStorage.getItem('user'));
    userStore.login(userData);
  }
});
</script>

复杂状态管理示例

// stores/cart.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useCartStore = defineStore('cart', () => {
  // 状态
  const items = ref([]);
  
  // 计算属性
  const totalItems = computed(() => items.value.reduce((sum, item) => sum + item.quantity, 0));
  
  const totalPrice = computed(() => {
    return items.value.reduce((sum, item) => {
      return sum + (item.price * item.quantity);
    }, 0);
  });
  
  const isEmpty = computed(() => items.value.length === 0);
  
  // 方法
  const addItem = (product) => {
    const existingItem = items.value.find(item => item.id === product.id);
    
    if (existingItem) {
      existingItem.quantity += 1;
    } else {
      items.value.push({
        ...product,
        quantity: 1
      });
    }
  };
  
  const removeItem = (productId) => {
    items.value = items.value.filter(item => item.id !== productId);
  };
  
  const updateQuantity = (productId, quantity) => {
    const item = items.value.find(item => item.id === productId);
    
    if (item) {
      if (quantity <= 0) {
        removeItem(productId);
      } else {
        item.quantity = quantity;
      }
    }
  };
  
  const clearCart = () => {
    items.value = [];
  };
  
  // 持久化存储
  const loadFromStorage = () => {
    const savedCart = localStorage.getItem('cart');
    if (savedCart) {
      items.value = JSON.parse(savedCart);
    }
  };
  
  const saveToStorage = () => {
    localStorage.setItem('cart', JSON.stringify(items.value));
  };
  
  // 监听状态变化
  const watch = () => {
    items.value = items.value;
    saveToStorage();
  };
  
  return {
    items,
    totalItems,
    totalPrice,
    isEmpty,
    addItem,
    removeItem,
    updateQuantity,
    clearCart,
    loadFromStorage,
    saveToStorage,
    watch
  };
});

组件逻辑复用

自定义 Composable 函数

Composable 是 Vue 3 中用于复用逻辑的函数,它们以 use 开头命名:

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

export function useFetch(url) {
  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);
      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;
    }
  };
  
  // 立即执行
  fetchData();
  
  // 监听 url 变化
  watch(url, fetchData);
  
  return {
    data,
    loading,
    error,
    refetch: fetchData
  };
}
<!-- DataComponent.vue -->
<template>
  <div>
    <div v-if="loading">Loading...</div>
    <div v-else-if="error">{{ error }}</div>
    <div v-else>
      <h2>{{ data?.title }}</h2>
      <p>{{ data?.body }}</p>
      <button @click="refetch">Refresh</button>
    </div>
  </div>
</template>

<script setup>
import { useFetch } from '@/composables/useFetch';

const url = 'https://jsonplaceholder.typicode.com/posts/1';
const { data, loading, error, refetch } = useFetch(url);
</script>

复杂逻辑复用

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

export function useLocalStorage(key, defaultValue) {
  const storedValue = localStorage.getItem(key);
  const value = ref(storedValue ? JSON.parse(storedValue) : defaultValue);
  
  watch(value, (newValue) => {
    localStorage.setItem(key, JSON.stringify(newValue));
  }, { deep: true });
  
  return value;
}
<!-- SettingsComponent.vue -->
<template>
  <div>
    <h3>Settings</h3>
    <div>
      <label>
        Theme:
        <select v-model="settings.theme">
          <option value="light">Light</option>
          <option value="dark">Dark</option>
        </select>
      </label>
    </div>
    <div>
      <label>
        Language:
        <select v-model="settings.language">
          <option value="en">English</option>
          <option value="zh">中文</option>
        </select>
      </label>
    </div>
  </div>
</template>

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

const settings = useLocalStorage('user-settings', {
  theme: 'light',
  language: 'en'
});
</script>

生命周期钩子

setup 函数的使用

setup 是 Composition API 的入口函数,它在组件实例创建之前执行:

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script setup>
import { ref, onMounted, onUpdated, onUnmounted } from 'vue';

const count = ref(0);

const increment = () => {
  count.value++;
};

// 生命周期钩子
onMounted(() => {
  console.log('Component mounted');
  // 初始化逻辑
});

onUpdated(() => {
  console.log('Component updated');
  // 更新后的逻辑
});

onUnmounted(() => {
  console.log('Component unmounted');
  // 清理工作
});
</script>

异步操作处理

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

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

const fetchData = async () => {
  try {
    loading.value = true;
    const response = await fetch('/api/data');
    data.value = await response.json();
  } catch (err) {
    error.value = err.message;
  } finally {
    loading.value = false;
  }
};

onMounted(() => {
  fetchData();
});
</script>

TypeScript 集成

类型定义最佳实践

// types/user.ts
export interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
}

export interface UserProfile {
  avatar: string;
  bio: string;
  preferences: {
    theme: 'light' | 'dark';
    language: string;
  };
}
<!-- TypedComponent.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue';
import type { User, UserProfile } from '@/types/user';

const user = ref<User | null>(null);
const profile = ref<UserProfile | null>(null);

const userName = computed(() => user.value?.name || '');
const userEmail = computed(() => user.value?.email || '');

// 定义 props 类型
interface Props {
  title?: string;
  showAvatar?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  title: 'Default Title',
  showAvatar: false
});

// 定义 emits 类型
type Emits = {
  (e: 'update:user', user: User): void;
  (e: 'error', error: Error): void;
};

const emit = defineEmits<Emits>();
</script>

性能优化技巧

计算属性缓存

// 正确的计算属性使用方式
import { ref, computed } from 'vue';

export default {
  setup() {
    const firstName = ref('');
    const lastName = ref('');
    
    // 复杂计算属性,会自动缓存
    const fullName = computed(() => {
      return `${firstName.value} ${lastName.value}`;
    });
    
    // 带有依赖的复杂计算
    const expensiveCalculation = computed(() => {
      // 模拟耗时操作
      let result = 0;
      for (let i = 0; i < 1000000; i++) {
        result += i;
      }
      return result;
    });
    
    return {
      firstName,
      lastName,
      fullName,
      expensiveCalculation
    };
  }
};

避免不必要的响应式

// ❌ 不推荐:不必要的响应式
const data = ref({
  name: 'John',
  age: 30,
  // 这些数据不需要响应式
  config: {
    theme: 'light',
    language: 'en'
  }
});

// ✅ 推荐:合理使用响应式
const user = ref({
  name: 'John',
  age: 30
});

const config = {
  theme: 'light',
  language: 'en'
};

实际项目案例

完整的购物车应用示例

<!-- ShoppingCart.vue -->
<template>
  <div class="shopping-cart">
    <h2>Shopping Cart</h2>
    
    <div v-if="cartStore.isEmpty" class="empty-cart">
      Your cart is empty
    </div>
    
    <div v-else>
      <div 
        v-for="item in cartStore.items" 
        :key="item.id"
        class="cart-item"
      >
        <img :src="item.image" :alt="item.name" class="item-image">
        <div class="item-info">
          <h3>{{ item.name }}</h3>
          <p class="price">¥{{ item.price }}</p>
          <div class="quantity-controls">
            <button @click="updateQuantity(item.id, item.quantity - 1)">-</button>
            <span>{{ item.quantity }}</span>
            <button @click="updateQuantity(item.id, item.quantity + 1)">+</button>
          </div>
        </div>
        <button @click="removeItem(item.id)" class="remove-btn">Remove</button>
      </div>
      
      <div class="cart-summary">
        <h3>Order Summary</h3>
        <div class="summary-row">
          <span>Total Items:</span>
          <span>{{ cartStore.totalItems }}</span>
        </div>
        <div class="summary-row">
          <span>Total Price:</span>
          <span class="total-price">¥{{ cartStore.totalPrice.toFixed(2) }}</span>
        </div>
        <button @click="checkout" class="checkout-btn">Checkout</button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { useCartStore } from '@/stores/cart';
import { onMounted } from 'vue';

const cartStore = useCartStore();

const updateQuantity = (productId, quantity) => {
  cartStore.updateQuantity(productId, quantity);
};

const removeItem = (productId) => {
  cartStore.removeItem(productId);
};

const checkout = () => {
  // 模拟结账逻辑
  alert(`Checkout complete! Total: ¥${cartStore.totalPrice.toFixed(2)}`);
  cartStore.clearCart();
};

onMounted(() => {
  cartStore.loadFromStorage();
});
</script>

<style scoped>
.shopping-cart {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.cart-item {
  display: flex;
  align-items: center;
  padding: 15px;
  border: 1px solid #ddd;
  margin-bottom: 10px;
  border-radius: 5px;
}

.item-image {
  width: 80px;
  height: 80px;
  object-fit: cover;
  margin-right: 15px;
}

.item-info {
  flex: 1;
}

.quantity-controls {
  display: flex;
  align-items: center;
  margin-top: 10px;
}

.quantity-controls button {
  width: 30px;
  height: 30px;
  margin: 0 5px;
}

.remove-btn {
  background-color: #ff4757;
  color: white;
  border: none;
  padding: 8px 12px;
  border-radius: 4px;
  cursor: pointer;
}

.cart-summary {
  margin-top: 30px;
  padding: 20px;
  background-color: #f8f9fa;
  border-radius: 5px;
}

.summary-row {
  display: flex;
  justify-content: space-between;
  margin-bottom: 10px;
}

.total-price {
  font-weight: bold;
  color: #ff6b6b;
}

.checkout-btn {
  width: 100%;
  padding: 12px;
  background-color: #48dbfb;
  color: white;
  border: none;
  border-radius: 5px;
  font-size: 16px;
  cursor: pointer;
  margin-top: 15px;
}
</style>

总结

Vue 3 Composition API 为前端开发带来了革命性的变化,它提供了更灵活的组件逻辑组织方式和强大的复用能力。通过本文的详细介绍,我们看到了:

  1. 响应式系统的深入理解:正确使用 refreactive 创建响应式数据
  2. 组件通信的最佳实践:父子、兄弟组件间的有效通信
  3. 状态管理的完整方案:结合 Pinia 实现复杂的状态管理
  4. 逻辑复用的强大能力:通过自定义 Composable 函数实现代码复用
  5. 性能优化策略:合理使用计算属性和避免不必要的响应式
  6. TypeScript 集成:提供完整的类型安全支持

在实际项目中,建议根据具体需求选择合适的 API 形式。对于简单的组件,Options API 依然简单易用;而对于复杂的逻辑复用场景,Composition API 显得更加灵活和强大。

通过掌握这些技术要点,开发者可以构建出更加健壮、可维护的 Vue 应用程序,同时提升开发效率和代码质量。随着 Vue 生态的不断发展,Composition API 将成为现代前端开发的重要工具。

相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000