Vue 3 组合式 API 实战指南
Vue 3 引入了革命性的组合式 API(Composition API),它提供了更灵活、更强大的代码组织方式。本文将通过实例详细介绍组合式 API 的使用方法和最佳实践。
基础概念
1. Options API vs Composition API
export default { data() { return { count: 0, todos: [] } }, methods: { increment() { this.count++ }, addTodo(todo) { this.todos.push(todo) } }, computed: { doubleCount() { return this.count * 2 } } }
import { ref, computed, methods } from 'vue'
export default { setup() { const count = ref(0) const todos = ref([]) const increment = () => { count.value++ } const addTodo = (todo) => { todos.value.push(todo) } const doubleCount = computed(() => count.value * 2) return { count, todos, increment, addTodo, doubleCount } } }
|
2. 生命周期钩子
import { onMounted, onUpdated, onUnmounted } from 'vue'
export default { setup() { onMounted(() => { console.log('组件已挂载') }) onUpdated(() => { console.log('组件已更新') }) onUnmounted(() => { console.log('组件已卸载') }) } }
|
3. 响应式系统
import { ref, reactive, computed, watch } from 'vue'
const counter = ref(0) const state = reactive({ count: 0, name: 'Vue' })
const doubleCount = computed(() => counter.value * 2)
watch(counter, (newVal, oldVal) => { console.log(`Counter changed from ${oldVal} to ${newVal}`) })
|
实战应用
1. 表单处理
<template> <div> <input v-model="form.name" placeholder="Name" /> <input v-model="form.email" placeholder="Email" type="email" /> <button @click="submitForm">Submit</button> <div v-if="errors.name" class="error">{{ errors.name }}</div> <div v-if="errors.email" class="error">{{ errors.email }}</div> </div> </template>
<script> import { reactive, computed } from 'vue'
export default { setup() { const form = reactive({ name: '', email: '' }) const errors = reactive({ name: '', email: '' }) const validateForm = () => { let isValid = true if (!form.name.trim()) { errors.name = 'Name is required' isValid = false } else { errors.name = '' } if (!form.email.trim()) { errors.email = 'Email is required' isValid = false } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) { errors.email = 'Email is invalid' isValid = false } else { errors.email = '' } return isValid } const submitForm = () => { if (validateForm()) { console.log('Form submitted:', form) // 提交逻辑 } } return { form, errors, submitForm } } } </script>
|
2. 列表操作
<template> <div> <input v-model="newTodo" placeholder="Add a new todo" /> <button @click="addTodo">Add</button> <ul> <li v-for="todo in todos" :key="todo.id"> {{ todo.text }} <button @click="removeTodo(todo.id)">Remove</button> </li> </ul> <div v-if="todos.length === 0">No todos yet</div> </div> </template>
<script> import { ref } from 'vue'
export default { setup() { const todos = ref([ { id: 1, text: 'Learn Vue 3', completed: false }, { id: 2, text: 'Build an app', completed: false } ]) const newTodo = ref('') const addTodo = () => { if (newTodo.value.trim()) { todos.value.push({ id: Date.now(), text: newTodo.value, completed: false }) newTodo.value = '' } } const removeTodo = (id) => { todos.value = todos.value.filter(todo => todo.id !== id) } const toggleTodo = (id) => { const todo = todos.value.find(todo => todo.id === id) if (todo) { todo.completed = !todo.completed } } return { todos, newTodo, addTodo, removeTodo, toggleTodo } } } </script>
|
3. API 调用
<template> <div> <div v-if="loading">Loading...</div> <div v-else-if="error">{{ error }}</div> <div v-else> <div v-for="post in posts" :key="post.id"> <h3>{{ post.title }}</h3> <p>{{ post.body }}</p> </div> </div> </div> </template>
<script> import { ref, onMounted } from 'vue'
export default { setup() { const posts = ref([]) const loading = ref(false) const error = ref(null) const fetchPosts = async () => { loading.value = true try { const response = await fetch('https://jsonplaceholder.typicode.com/posts') if (!response.ok) { throw new Error('Failed to fetch posts') } posts.value = await response.json() } catch (err) { error.value = err.message } finally { loading.value = false } } onMounted(() => { fetchPosts() }) return { posts, loading, error } } } </script>
|
高级特性
1. 自定义 Composition 函数
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 } }
import { useCounter } from './useCounter'
export default { setup() { const { count, increment, decrement, doubleCount } = useCounter(10) return { count, increment, decrement, doubleCount } } }
|
2. 自定义 Hook 组合
import { ref, onMounted } from 'vue'
export function useFetch(url) { const data = ref(null) const loading = ref(false) const error = ref(null) const fetchData = async () => { loading.value = true try { const response = await fetch(url) if (!response.ok) { throw new Error('Failed to fetch data') } data.value = await response.json() } catch (err) { error.value = err.message } finally { loading.value = false } } onMounted(() => { fetchData() }) return { data, loading, error, refetch: fetchData } }
import { ref, watch } from 'vue'
export function useLocalStorage(key, initialValue) { const storedValue = localStorage.getItem(key) const value = ref(storedValue ? JSON.parse(storedValue) : initialValue) watch(value, (newValue) => { localStorage.setItem(key, JSON.stringify(newValue)) }, { deep: true }) return value }
import { useFetch } from './useFetch' import { useLocalStorage } from './useLocalStorage' import { computed } from 'vue'
export default { setup() { const { data: posts, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts') const favorites = useLocalStorage('favorites', []) const favoritePosts = computed(() => { return posts.value?.filter(post => favorites.value.includes(post.id)) || [] }) const toggleFavorite = (postId) => { const index = favorites.value.indexOf(postId) if (index === -1) { favorites.value.push(postId) } else { favorites.value.splice(index, 1) } } return { posts, loading, error, favoritePosts, toggleFavorite } } }
|
3. TypeScript 支持
export interface Todo { id: number text: string completed: boolean }
export interface FormState { name: string email: string }
import { ref, reactive } from 'vue' import type { Ref, Reactive } from 'vue' import { Todo, FormState } from './types'
export default { setup() { const todos: Ref<Todo[]> = ref([]) const form: Reactive<FormState> = reactive({ name: '', email: '' }) const addTodo = (todo: Todo) => { todos.value.push(todo) } return { todos, form, addTodo } } }
|
最佳实践
1. 代码组织
function useAuth() { }
function useTodos() { }
function useUser() { }
export default { setup() { const { user } = useUser() const { todos } = useTodos() const { login } = useAuth() return { user, todos, login } } }
|
2. 性能优化
import { shallowRef, shallowReactive } from 'vue'
const largeObject = shallowReactive({ })
const largeArray = shallowRef([])
|
3. 错误处理
import { onErrorCaptured } from 'vue'
export default { setup() { const error = ref(null) onErrorCaptured((err) => { error.value = err return false }) return { error } } }
|
4. 测试策略
import { mount } from '@vue/test-utils' import MyComponent from './MyComponent.vue'
describe('MyComponent', () => { it('renders correctly', () => { const wrapper = mount(MyComponent) expect(wrapper.text()).toContain('Hello World') }) it('handles user interaction', async () => { const wrapper = mount(MyComponent) await wrapper.find('button').trigger('click') expect(wrapper.vm.count).toBe(1) }) })
import { renderHook, act } from '@vue/test-utils' import { useCounter } from './useCounter'
describe('useCounter', () => { it('initializes with default value', () => { const { result } = renderHook(() => useCounter()) expect(result.value.count.value).toBe(0) }) it('increments count', () => { const { result } = renderHook(() => useCounter()) act(() => { result.value.increment() }) expect(result.value.count.value).toBe(1) }) })
|
迁移指南
1. 从 Options API 迁移
export default { data() { return { count: 0 } }, methods: { increment() { this.count++ } } }
import { ref } from 'vue'
export default { setup() { const count = ref(0) const increment = () => { count.value++ } return { count, increment } } }
|
2. 生命周期映射
export default { setup() { } }
export default { setup() { } }
import { onMounted } from 'vue'
export default { setup() { onMounted(() => { }) } }
import { onMounted } from 'vue'
export default { setup() { onMounted(() => { }) } }
|
3. 计算属性和监听器
import { computed } from 'vue'
export default { setup() { const count = ref(0) const doubleCount = computed(() => count.value * 2) return { count, doubleCount } } }
import { watch } from 'vue'
export default { setup() { const count = ref(0) watch(count, (newVal, oldVal) => { console.log(`Count changed from ${oldVal} to ${newVal}`) }) return { count } } }
|
总结
Vue 3 组合式 API 提供了更灵活、更强大的代码组织方式,特别适合:
- 大型应用:更好地组织复杂逻辑
- 团队协作:逻辑复用更简单
- TypeScript 支持:更好的类型推断
- 代码复用:自定义组合函数
通过合理使用组合式 API,可以构建更加模块化、可维护的应用程序。建议在项目中逐步采用,并结合项目需求选择合适的组织方式。