Vue3组合式API最佳实践指南
引言Vue 3 引入的组合式 API (Composition API) 为 Vue 开发带来了全新的编程范式。相比于选项式 API,组合式 API 提供了更好的逻辑复用、类型推导和代码组织方式。本文将深入探讨 Vue 3 组合式 API 的最佳实践,帮助开发者充分发挥其潜力。## 组合式 API 基础概念### setup() 函数setup()
是组合式 API 的入口点:vue<template> <div> <h1>{{ title }}</h1> <p>计数: {{ count }}</p> <button @click="increment">增加</button> </div></template><script>import { ref, reactive } from 'vue'export default { name: 'BasicExample', setup() { // 响应式数据 const title = ref('Vue 3 组合式 API') const count = ref(0) // 方法 const increment = () => { count.value++ } // 返回模板需要的数据和方法 return { title, count, increment } }}</script>
### script setup 语法糖更简洁的写法:vue<template> <div> <h1>{{ title }}</h1> <p>计数: {{ count }}</p> <button @click="increment">增加</button> </div></template><script setup>import { ref } from 'vue'// 直接定义,无需返回const title = ref('Vue 3 组合式 API')const count = ref(0)const increment = () => { count.value++}</script>
## 响应式系统深入理解### ref vs reactive#### ref 的使用场景vue<script setup>import { ref, computed, watch } from 'vue'// 基本类型使用 refconst count = ref(0)const message = ref('Hello')const isVisible = ref(true)// 对象也可以使用 refconst user = ref({ name: 'John', age: 30})// 访问和修改console.log(count.value) // 需要 .valuecount.value++// 在模板中自动解包,不需要 .value// <template>{{ count }}</template>// 计算属性const doubleCount = computed(() => count.value * 2)// 监听器watch(count, (newVal, oldVal) => { console.log(`count changed from ${oldVal} to ${newVal}`)})</script>
#### reactive 的使用场景vue<script setup>import { reactive, toRefs, computed } from 'vue'// 对象使用 reactiveconst state = reactive({ count: 0, message: 'Hello', user: { name: 'John', age: 30 }, todos: []})// 直接访问,无需 .valueconsole.log(state.count)state.count++// 添加新属性state.newProperty = 'new value'// 解构时保持响应性const { count, message } = toRefs(state)// 计算属性const userInfo = computed(() => `${state.user.name} (${state.user.age})`)// 方法const addTodo = (text) => { state.todos.push({ id: Date.now(), text, completed: false })}</script>
### 响应式工具函数#### toRef 和 toRefsvue<script setup>import { reactive, toRef, toRefs } from 'vue'const state = reactive({ count: 0, message: 'Hello', user: { name: 'John', age: 30 }})// toRef: 为单个属性创建 refconst count = toRef(state, 'count')// toRefs: 为所有属性创建 refconst { message, user } = toRefs(state)// 现在可以解构使用,且保持响应性console.log(count.value) // 0console.log(message.value) // 'Hello'// 修改会影响原对象count.value++console.log(state.count) // 1</script>
#### readonly 和 shallowRefvue<script setup>import { ref, reactive, readonly, shallowRef } from 'vue'// readonly: 创建只读代理const state = reactive({ count: 0, message: 'Hello'})const readonlyState = readonly(state)// 尝试修改会在开发环境发出警告// readonlyState.count++ // 警告// shallowRef: 浅层响应式const shallowState = shallowRef({ count: 0, nested: { value: 1 }})// 只有根级别的替换会触发响应shallowState.value = { count: 1, nested: { value: 2 } } // 响应式shallowState.value.count++ // 非响应式shallowState.value.nested.value++ // 非响应式</script>
## 生命周期钩子### 组合式 API 中的生命周期vue<script setup>import { ref, onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount} from 'vue'const count = ref(0)const message = ref('')// 组件挂载前onBeforeMount(() => { console.log('组件即将挂载')})// 组件挂载后onMounted(() => { console.log('组件已挂载') // DOM 操作 document.title = '组合式 API 示例'})// 组件更新前onBeforeUpdate(() => { console.log('组件即将更新')})// 组件更新后onUpdated(() => { console.log('组件已更新')})// 组件卸载前onBeforeUnmount(() => { console.log('组件即将卸载') // 清理工作})// 组件卸载后onUnmounted(() => { console.log('组件已卸载') // 清理定时器、事件监听器等})</script>
### 生命周期最佳实践vue<script setup>import { ref, onMounted, onUnmounted } from 'vue'const count = ref(0)let timer = null// 封装定时器逻辑const useTimer = (callback, interval) => { let timer = null const start = () => { if (timer) return timer = setInterval(callback, interval) } const stop = () => { if (timer) { clearInterval(timer) timer = null } } // 组件卸载时自动清理 onUnmounted(() => { stop() }) return { start, stop }}// 使用定时器const { start: startTimer, stop: stopTimer } = useTimer(() => { count.value++}, 1000)onMounted(() => { startTimer()})</script>
## 计算属性和监听器### 计算属性最佳实践vue<script setup>import { ref, computed, reactive } from 'vue'const firstName = ref('John')const lastName = ref('Doe')// 简单计算属性const fullName = computed(() => { return `${firstName.value} ${lastName.value}`})// 可写计算属性const writableFullName = computed({ get() { return `${firstName.value} ${lastName.value}` }, set(value) { const names = value.split(' ') firstName.value = names[0] lastName.value = names[names.length - 1] }})// 复杂计算属性const todos = reactive([ { id: 1, text: '学习 Vue', completed: false }, { id: 2, text: '写代码', completed: true }, { id: 3, text: '休息', completed: false }])const completedTodos = computed(() => { return todos.filter(todo => todo.completed)})const todoStats = computed(() => { const total = todos.length const completed = completedTodos.value.length const remaining = total - completed return { total, completed, remaining, completionRate: total > 0 ? (completed / total * 100).toFixed(1) : 0 }})// 使用示例console.log(fullName.value) // 'John Doe'writableFullName.value = 'Jane Smith'console.log(firstName.value) // 'Jane'console.log(lastName.value) // 'Smith'</script>
### 监听器最佳实践vue<script setup>import { ref, reactive, watch, watchEffect, nextTick } from 'vue'const count = ref(0)const message = ref('')const state = reactive({ user: { name: 'John', age: 30 }})// 基本监听watch(count, (newVal, oldVal) => { console.log(`count changed from ${oldVal} to ${newVal}`)})// 监听多个源watch([count, message], ([newCount, newMessage], [oldCount, oldMessage]) => { console.log('Multiple values changed')})// 深度监听watch( () => state.user, (newUser, oldUser) => { console.log('User changed:', newUser) }, { deep: true })// 立即执行watch( count, (newVal) => { console.log('Count is:', newVal) }, { immediate: true })// watchEffect: 自动追踪依赖watchEffect(() => { console.log(`Count is ${count.value}, message is ${message.value}`)})// 停止监听const stopWatcher = watch(count, (newVal) => { if (newVal > 10) { console.log('Count exceeded 10, stopping watcher') stopWatcher() }})// 异步监听watch( count, async (newVal) => { if (newVal > 0) { await nextTick() console.log('DOM updated') } })// 监听器清理watchEffect((onInvalidate) => { const timer = setTimeout(() => { console.log('Timer executed') }, 1000) onInvalidate(() => { clearTimeout(timer) console.log('Timer cleared') })})</script>
## 逻辑复用和组合### 自定义组合函数 (Composables)#### 基础组合函数javascript// composables/useCounter.jsimport { 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 isEven = computed(() => count.value % 2 === 0) const isPositive = computed(() => count.value > 0) return { count, increment, decrement, reset, isEven, isPositive }}``````vue<!-- 使用组合函数 --><template> <div> <p>计数: {{ count }}</p> <p>是偶数: {{ isEven }}</p> <p>是正数: {{ isPositive }}</p> <button @click="increment">+</button> <button @click="decrement">-</button> <button @click="reset">重置</button> </div></template><script setup>import { useCounter } from '@/composables/useCounter'const { count, increment, decrement, reset, isEven, isPositive } = useCounter(10)</script>
#### 高级组合函数javascript// composables/useFetch.jsimport { ref, reactive, watchEffect } from 'vue'export function useFetch(url, options = {}) { const data = ref(null) const error = ref(null) const loading = ref(false) const state = reactive({ data, error, loading }) const execute = async () => { loading.value = true error.value = null try { const response = await fetch(url.value || url, { ...options }) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const result = await response.json() data.value = result } catch (err) { error.value = err.message } finally { loading.value = false } } // 如果 url 是响应式的,自动重新请求 if (typeof url === 'object' && url.value !== undefined) { watchEffect(() => { if (url.value) { execute() } }) } else { execute() } return { ...state, execute, refresh: execute }}``````vue<!-- 使用 useFetch --><template> <div> <div v-if="loading">加载中...</div> <div v-else-if="error">错误: {{ error }}</div> <div v-else-if="data"> <h3>用户信息</h3> <p>姓名: {{ data.name }}</p> <p>邮箱: {{ data.email }}</p> </div> <button @click="refresh">刷新</button> </div></template><script setup>import { ref } from 'vue'import { useFetch } from '@/composables/useFetch'const userId = ref(1)const url = computed(() => `https://jsonplaceholder.typicode.com/users/${userId.value}`)const { data, loading, error, refresh } = useFetch(url)</script>
### 复杂状态管理javascript// composables/useStore.jsimport { reactive, readonly, computed } from 'vue'function createStore(initialState, actions) { const state = reactive(initialState) const getters = {} const mutations = {} // 创建 getters Object.keys(actions.getters || {}).forEach(key => { getters[key] = computed(() => actions.getters[key](state)) }) // 创建 mutations Object.keys(actions.mutations || {}).forEach(key => { mutations[key] = (...args) => actions.mutations[key](state, ...args) }) return { state: readonly(state), getters, ...mutations }}// 使用示例export const useUserStore = () => { return createStore( { users: [], currentUser: null, loading: false }, { getters: { userCount: (state) => state.users.length, isLoggedIn: (state) => !!state.currentUser, activeUsers: (state) => state.users.filter(user => user.active) }, mutations: { setUsers(state, users) { state.users = users }, setCurrentUser(state, user) { state.currentUser = user }, setLoading(state, loading) { state.loading = loading }, addUser(state, user) { state.users.push(user) }, removeUser(state, userId) { const index = state.users.findIndex(user => user.id === userId) if (index > -1) { state.users.splice(index, 1) } } } } )}
## 组件通信### Props 和 Emitsvue<!-- 子组件 --><template> <div class="user-card"> <h3>{{ user.name }}</h3> <p>{{ user.email }}</p> <button @click="handleEdit">编辑</button> <button @click="handleDelete">删除</button> </div></template><script setup>import { computed } from 'vue'// 定义 propsconst props = defineProps({ user: { type: Object, required: true, validator: (user) => { return user && typeof user.name === 'string' && typeof user.email === 'string' } }, editable: { type: Boolean, default: true }})// 定义 emitsconst emit = defineEmits(['edit', 'delete', 'update'])// 计算属性const isEditable = computed(() => props.editable && props.user.id)// 方法const handleEdit = () => { if (isEditable.value) { emit('edit', props.user) }}const handleDelete = () => { emit('delete', props.user.id)}// 暴露给父组件的方法defineExpose({ focus: () => { // 聚焦逻辑 }})</script>``````vue<!-- 父组件 --><template> <div> <UserCard v-for="user in users" :key="user.id" :user="user" :editable="true" @edit="handleEditUser" @delete="handleDeleteUser" ref="userCards" /> </div></template><script setup>import { ref } from 'vue'import UserCard from './UserCard.vue'const users = ref([ { id: 1, name: 'John', email: 'john@example.com' }, { id: 2, name: 'Jane', email: 'jane@example.com' }])const userCards = ref([])const handleEditUser = (user) => { console.log('编辑用户:', user)}const handleDeleteUser = (userId) => { const index = users.value.findIndex(user => user.id === userId) if (index > -1) { users.value.splice(index, 1) }}// 调用子组件方法const focusFirstCard = () => { if (userCards.value[0]) { userCards.value[0].focus() }}</script>
### Provide/Injectvue<!-- 祖先组件 --><template> <div> <ThemeProvider> <UserList /> </ThemeProvider> </div></template><script setup>import { provide, reactive } from 'vue'import ThemeProvider from './ThemeProvider.vue'import UserList from './UserList.vue'// 提供主题配置const theme = reactive({ primaryColor: '#007bff', fontSize: '14px', darkMode: false})const toggleDarkMode = () => { theme.darkMode = !theme.darkMode theme.primaryColor = theme.darkMode ? '#6c757d' : '#007bff'}provide('theme', theme)provide('toggleDarkMode', toggleDarkMode)</script>``````vue<!-- 后代组件 --><template> <div :style="themeStyles"> <h2>用户列表</h2> <button @click="toggleDarkMode"> 切换到{{ theme.darkMode ? '浅色' : '深色' }}模式 </button> </div></template><script setup>import { inject, computed } from 'vue'// 注入主题const theme = inject('theme')const toggleDarkMode = inject('toggleDarkMode')// 计算样式const themeStyles = computed(() => ({ color: theme.darkMode ? '#fff' : '#000', backgroundColor: theme.darkMode ? '#333' : '#fff', fontSize: theme.fontSize}))</script>
## 性能优化### 响应式优化vue<script setup>import { ref, reactive, shallowRef, shallowReactive, markRaw } from 'vue'// 对于大型数据结构,使用 shallowRefconst largeData = shallowRef({ items: new Array(10000).fill(0).map((_, i) => ({ id: i, value: i }))})// 更新整个对象才会触发响应const updateLargeData = () => { largeData.value = { items: largeData.value.items.map(item => ({ ...item, value: item.value + 1 })) }}// 对于不需要响应式的数据,使用 markRawconst nonReactiveData = markRaw({ heavyObject: new SomeHeavyClass(), config: { /* 大量配置 */ }})// 使用 shallowReactive 优化嵌套对象const shallowState = shallowReactive({ count: 0, nested: { value: 1 // 这个不会是响应式的 }})</script>
### 计算属性缓存优化vue<script setup>import { ref, computed, watchEffect } from 'vue'const items = ref([])const filter = ref('')const sortBy = ref('name')// 分步计算,利用缓存const filteredItems = computed(() => { if (!filter.value) return items.value return items.value.filter(item => item.name.toLowerCase().includes(filter.value.toLowerCase()) )})const sortedItems = computed(() => { return [...filteredItems.value].sort((a, b) => { const aVal = a[sortBy.value] const bVal = b[sortBy.value] return aVal < bVal ? -1 : aVal > bVal ? 1 : 0 })})// 避免在模板中进行复杂计算const itemsWithIndex = computed(() => { return sortedItems.value.map((item, index) => ({ ...item, displayIndex: index + 1 }))})</script>
### 组件懒加载vue<script setup>import { ref, defineAsyncComponent } from 'vue'// 异步组件const HeavyComponent = defineAsyncComponent({ loader: () => import('./HeavyComponent.vue'), loadingComponent: () => import('./LoadingComponent.vue'), errorComponent: () => import('./ErrorComponent.vue'), delay: 200, timeout: 3000})const showHeavyComponent = ref(false)// 条件加载const ConditionalComponent = defineAsyncComponent(() => { return showHeavyComponent.value ? import('./HeavyComponent.vue') : Promise.resolve({ template: '<div>占位符</div>' })})</script>
## 类型安全 (TypeScript)### 基础类型定义vue<script setup lang="ts">import { ref, reactive, computed } from 'vue'// 接口定义interface User { id: number name: string email: string avatar?: string}interface UserState { users: User[] currentUser: User | null loading: boolean}// 类型化的响应式数据const users = ref<User[]>([])const currentUser = ref<User | null>(null)const state = reactive<UserState>({ users: [], currentUser: null, loading: false})// 类型化的计算属性const userCount = computed<number>(() => users.value.length)const hasUsers = computed<boolean>(() => userCount.value > 0)// 类型化的方法const addUser = (user: Omit<User, 'id'>): void => { const newUser: User = { id: Date.now(), ...user } users.value.push(newUser)}const findUser = (id: number): User | undefined => { return users.value.find(user => user.id === id)}</script>
### 组合函数类型定义typescript// composables/useApi.tsimport { ref, Ref } from 'vue'interface ApiResponse<T> { data: T | null loading: boolean error: string | null}interface UseApiReturn<T> extends ApiResponse<T> { execute: () => Promise<void> reset: () => void}export function useApi<T>( url: string | Ref<string>, options?: RequestInit): UseApiReturn<T> { const data = ref<T | null>(null) const loading = ref<boolean>(false) const error = ref<string | null>(null) const execute = async (): Promise<void> => { loading.value = true error.value = null try { const response = await fetch( typeof url === 'string' ? url : url.value, options ) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } data.value = await response.json() } catch (err) { error.value = err instanceof Error ? err.message : 'Unknown error' } finally { loading.value = false } } const reset = (): void => { data.value = null error.value = null loading.value = false } return { data: data as Ref<T | null>, loading, error, execute, reset }}
### Props 和 Emits 类型定义vue<script setup lang="ts">interface User { id: number name: string email: string}interface Props { user: User editable?: boolean maxLength?: number}interface Emits { (e: 'update', user: User): void (e: 'delete', id: number): void (e: 'error', message: string): void}// 定义 props 和 emitsconst props = withDefaults(defineProps<Props>(), { editable: true, maxLength: 100})const emit = defineEmits<Emits>()// 类型安全的方法const handleUpdate = (updatedUser: User): void => { emit('update', updatedUser)}const handleDelete = (): void => { emit('delete', props.user.id)}const handleError = (message: string): void => { emit('error', message)}</script>
## 测试策略### 组合函数测试javascript// tests/composables/useCounter.test.jsimport { describe, it, expect } from 'vitest'import { useCounter } from '@/composables/useCounter'describe('useCounter', () => { it('should initialize with default value', () => { const { count } = useCounter() expect(count.value).toBe(0) }) it('should initialize with custom value', () => { const { count } = useCounter(10) expect(count.value).toBe(10) }) it('should increment count', () => { const { count, increment } = useCounter(5) increment() expect(count.value).toBe(6) }) it('should decrement count', () => { const { count, decrement } = useCounter(5) decrement() expect(count.value).toBe(4) }) it('should reset count', () => { const { count, increment, reset } = useCounter(5) increment() increment() expect(count.value).toBe(7) reset() expect(count.value).toBe(5) }) it('should compute isEven correctly', () => { const { count, increment, isEven } = useCounter(2) expect(isEven.value).toBe(true) increment() expect(isEven.value).toBe(false) })})
### 组件测试javascript// tests/components/UserCard.test.jsimport { describe, it, expect, vi } from 'vitest'import { mount } from '@vue/test-utils'import UserCard from '@/components/UserCard.vue'describe('UserCard', () => { const mockUser = { id: 1, name: 'John Doe', email: 'john@example.com' } it('should render user information', () => { const wrapper = mount(UserCard, { props: { user: mockUser } }) expect(wrapper.text()).toContain('John Doe') expect(wrapper.text()).toContain('john@example.com') }) it('should emit edit event when edit button is clicked', async () => { const wrapper = mount(UserCard, { props: { user: mockUser, editable: true } }) await wrapper.find('[data-test="edit-button"]').trigger('click') expect(wrapper.emitted('edit')).toBeTruthy() expect(wrapper.emitted('edit')[0]).toEqual([mockUser]) }) it('should emit delete event when delete button is clicked', async () => { const wrapper = mount(UserCard, { props: { user: mockUser } }) await wrapper.find('[data-test="delete-button"]').trigger('click') expect(wrapper.emitted('delete')).toBeTruthy() expect(wrapper.emitted('delete')[0]).toEqual([mockUser.id]) }) it('should not show edit button when not editable', () => { const wrapper = mount(UserCard, { props: { user: mockUser, editable: false } }) expect(wrapper.find('[data-test="edit-button"]').exists()).toBe(false) })})
## 最佳实践总结### 代码组织1. 按功能分组:将相关的响应式数据、计算属性和方法放在一起2. 提取组合函数:将可复用的逻辑提取为组合函数3. 保持单一职责:每个组合函数只负责一个特定的功能vue<script setup>// 1. 导入import { ref, computed, onMounted } from 'vue'import { useRouter } from 'vue-router'import { useUserStore } from '@/stores/user'// 2. 组合函数const router = useRouter()const userStore = useUserStore()// 3. 响应式数据const loading = ref(false)const error = ref(null)// 4. 计算属性const isLoggedIn = computed(() => userStore.currentUser !== null)// 5. 方法const handleLogin = async () => { // 登录逻辑}// 6. 生命周期onMounted(() => { // 初始化逻辑})</script>
### 性能优化原则1. 合理使用响应式:不是所有数据都需要响应式2. 避免不必要的计算:使用计算属性缓存复杂计算3. 组件懒加载:按需加载大型组件4. 优化渲染:使用 v-memo
和 v-once
指令### 类型安全1. 使用 TypeScript:获得更好的开发体验和代码质量2. 定义清晰的接口:为数据结构定义明确的类型3. 类型化组合函数:确保组合函数的类型安全### 测试策略1. 单元测试组合函数:测试业务逻辑的核心部分2. 组件测试:测试组件的行为和交互3. 集成测试:测试组件间的协作## 总结Vue 3 的组合式 API 为我们提供了更灵活、更强大的开发方式。通过合理使用响应式系统、生命周期钩子、计算属性和监听器,我们可以构建出更加健壮和可维护的应用。关键要点:1. 理解响应式原理:掌握 ref
和 reactive
的使用场景2. 善用组合函数:提高代码复用性和可测试性3. 注重性能优化:合理使用响应式,避免不必要的计算4. 保证类型安全:使用 TypeScript 提高代码质量5. 完善测试覆盖:确保代码的可靠性组合式 API 不仅仅是一种新的语法,更是一种新的思维方式。它鼓励我们以更加函数式的方式思考和组织代码,最终构建出更加优雅和高效的 Vue 应用。大数据