Vue 3的发布带来了Composition API这一革命性的特性,它为开发者提供了更加灵活和可复用的代码组织方式。相比Vue 2的Options API,Composition API让开发者能够更好地组织逻辑、管理状态,并构建更加健壮的前端应用。本文将深入探讨Vue 3 Composition API的核心概念和最佳实践,帮助开发者快速掌握现代化Vue开发技巧。
Composition API核心概念
什么是Composition API
Composition API是Vue 3引入的一种新的API风格,它允许开发者基于函数的组合来组织组件逻辑。与Options API将组件选项(data、methods、computed等)分散在不同部分不同,Composition API让相关逻辑能够集中在一起,提高了代码的可读性和可维护性。
// Options API 示例
export default {
data() {
return {
count: 0,
message: 'Hello'
}
},
methods: {
increment() {
this.count++
}
},
computed: {
doubleCount() {
return this.count * 2
}
}
}
// Composition API 示例
import { ref, computed } from 'vue'
export default {
setup() {
const count = ref(0)
const message = ref('Hello')
const increment = () => {
count.value++
}
const doubleCount = computed(() => count.value * 2)
return {
count,
message,
increment,
doubleCount
}
}
}
响应式系统详解
Vue 3的响应式系统基于Proxy实现,提供了更强大和灵活的响应式能力。主要的响应式API包括:
ref和reactive
ref用于创建响应式的值类型数据,而reactive用于创建响应式的对象类型数据。
import { ref, reactive } from 'vue'
// ref - 值类型响应式
const count = ref(0)
const name = ref('Vue')
// reactive - 对象类型响应式
const state = reactive({
user: {
name: 'John',
age: 30
},
todos: []
})
// ref在模板中的使用
// <p>{{ count }}</p> - 无需.value
// <p>{{ count.value }}</p> - 在JavaScript中需要.value
toRefs和toRef
当需要将reactive对象的属性解构为ref时,使用toRefs可以保持响应性。
import { reactive, toRefs } from 'vue'
const state = reactive({
name: 'Vue',
version: 3
})
// 错误做法 - 丢失响应性
const { name, version } = state
// 正确做法 - 保持响应性
const { name, version } = toRefs(state)
// toRef用于创建单个属性的ref
const nameRef = toRef(state, 'name')
状态管理最佳实践
本地状态管理
对于组件内部的状态管理,Composition API提供了多种灵活的方案。
使用ref管理简单状态
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 isEven = computed(() => count.value % 2 === 0)
return {
count,
increment,
decrement,
reset,
isEven
}
}
使用reactive管理复杂状态
import { reactive, computed } from 'vue'
export function useFormState() {
const form = reactive({
user: {
name: '',
email: '',
age: null
},
errors: {},
isSubmitting: false
})
const isValid = computed(() => {
return form.user.name &&
form.user.email &&
!Object.keys(form.errors).length
})
const validate = () => {
form.errors = {}
if (!form.user.name) {
form.errors.name = 'Name is required'
}
if (!form.user.email) {
form.errors.email = 'Email is required'
}
}
const submit = async () => {
validate()
if (!isValid.value) return
form.isSubmitting = true
try {
// 提交逻辑
await submitForm(form.user)
} catch (error) {
// 错误处理
} finally {
form.isSubmitting = false
}
}
return {
form,
isValid,
submit
}
}
全局状态管理
Pinia vs Vuex
Vue 3推荐使用Pinia作为状态管理方案,它提供了更简洁的API和更好的TypeScript支持。
// stores/counter.js - Pinia store
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Eduardo'
}),
getters: {
doubleCount: (state) => state.count * 2,
doubleCountPlusOne() {
return this.doubleCount + 1
}
},
actions: {
increment() {
this.count++
},
randomizeCounter() {
this.count = Math.round(100 * Math.random())
}
}
})
// 在组件中使用
import { useCounterStore } from '@/stores/counter'
export default {
setup() {
const counter = useCounterStore()
// 直接访问状态
console.log(counter.count)
// 调用action
counter.increment()
// 访问getter
console.log(counter.doubleCount)
return { counter }
}
}
自定义全局状态管理
对于简单的应用,也可以创建自定义的状态管理方案:
// composables/useGlobalState.js
import { reactive, readonly } from 'vue'
const state = reactive({
user: null,
theme: 'light',
notifications: []
})
const mutations = {
setUser(user) {
state.user = user
},
setTheme(theme) {
state.theme = theme
},
addNotification(notification) {
state.notifications.push({
id: Date.now(),
...notification
})
},
removeNotification(id) {
const index = state.notifications.findIndex(n => n.id === id)
if (index > -1) {
state.notifications.splice(index, 1)
}
}
}
export function useGlobalState() {
return {
state: readonly(state),
...mutations
}
}
组件通信模式
Props和Emit
传统的父子组件通信方式依然适用,但在Composition API中有了更好的组织方式。
// 父组件
<template>
<UserCard
:user="user"
@update-user="handleUpdateUser"
@delete-user="handleDeleteUser"
/>
</template>
<script setup>
import { ref } from 'vue'
import UserCard from './UserCard.vue'
const user = ref({
id: 1,
name: 'John Doe',
email: 'john@example.com'
})
const handleUpdateUser = (updatedUser) => {
user.value = { ...user.value, ...updatedUser }
}
const handleDeleteUser = (userId) => {
// 删除用户逻辑
}
</script>
// 子组件
<template>
<div class="user-card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<button @click="updateUser">编辑</button>
<button @click="deleteUser">删除</button>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
user: {
type: Object,
required: true
}
})
const emit = defineEmits(['update-user', 'delete-user'])
const updateUser = () => {
emit('update-user', { ...props.user, name: 'Updated Name' })
}
const deleteUser = () => {
emit('delete-user', props.user.id)
}
</script>
Provide/Inject
Provide/Inject是跨层级组件通信的有效方式,在Composition API中更加简洁。
// 祖先组件
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
const user = ref({ name: 'John', role: 'admin' })
provide('theme', theme)
provide('user', user)
provide('updateTheme', (newTheme) => {
theme.value = newTheme
})
</script>
// 后代组件
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
const user = inject('user')
const updateTheme = inject('updateTheme')
// 使用响应式数据
console.log(theme.value) // 'dark'
// 调用方法更新状态
updateTheme('light')
</script>
事件总线模式
对于非父子关系的组件通信,可以使用事件总线模式:
// utils/eventBus.js
import { reactive, onMounted, onUnmounted } from 'vue'
class EventBus {
constructor() {
this.events = reactive({})
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
}
off(event, callback) {
if (this.events[event]) {
const index = this.events[event].indexOf(callback)
if (index > -1) {
this.events[event].splice(index, 1)
}
}
}
emit(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(data))
}
}
}
export const eventBus = new EventBus()
// 在组件中使用
import { onMounted, onUnmounted } from 'vue'
import { eventBus } from '@/utils/eventBus'
export default {
setup() {
const handleUserLogin = (userData) => {
console.log('User logged in:', userData)
}
onMounted(() => {
eventBus.on('user-login', handleUserLogin)
})
onUnmounted(() => {
eventBus.off('user-login', handleUserLogin)
})
const triggerLogin = () => {
eventBus.emit('user-login', { name: 'John', id: 1 })
}
return { triggerLogin }
}
}
可复用逻辑封装
自定义Hook开发
Composition API最大的优势之一就是能够将可复用逻辑封装成自定义Hook。
// 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
}
// composables/useApi.js
import { ref, onMounted } from 'vue'
export function useApi(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
}
}
onMounted(fetchData)
return {
data,
loading,
error,
refetch: fetchData
}
}
// composables/useDebounce.js
import { ref, watch } from 'vue'
export function useDebounce(value, delay = 300) {
const debouncedValue = ref(value.value)
let timeout = null
watch(value, (newValue) => {
clearTimeout(timeout)
timeout = setTimeout(() => {
debouncedValue.value = newValue
}, delay)
})
return debouncedValue
}
高级Hook示例
// composables/usePagination.js
import { ref, computed, watch } from 'vue'
export function usePagination(fetchData, options = {}) {
const {
pageSize = 10,
initialPage = 1
} = options
const currentPage = ref(initialPage)
const total = ref(0)
const items = ref([])
const loading = ref(false)
const totalPages = computed(() => Math.ceil(total.value / pageSize))
const hasNextPage = computed(() => currentPage.value < totalPages.value)
const hasPrevPage = computed(() => currentPage.value > 1)
const fetchPage = async (page = currentPage.value) => {
loading.value = true
try {
const result = await fetchData(page, pageSize)
items.value = result.items
total.value = result.total
currentPage.value = page
} catch (error) {
console.error('Failed to fetch data:', error)
} finally {
loading.value = false
}
}
const nextPage = () => {
if (hasNextPage.value) {
fetchPage(currentPage.value + 1)
}
}
const prevPage = () => {
if (hasPrevPage.value) {
fetchPage(currentPage.value - 1)
}
}
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
fetchPage(page)
}
}
// 监听参数变化,重新获取数据
watch(() => [pageSize, initialPage], () => {
fetchPage(initialPage)
}, { immediate: true })
return {
currentPage,
totalPages,
total,
items,
loading,
hasNextPage,
hasPrevPage,
fetchPage,
nextPage,
prevPage,
goToPage
}
}
// 使用示例
import { usePagination } from '@/composables/usePagination'
export default {
setup() {
const fetchUsers = async (page, size) => {
const response = await fetch(`/api/users?page=${page}&size=${size}`)
return response.json()
}
const {
currentPage,
totalPages,
items: users,
loading,
nextPage,
prevPage,
goToPage
} = usePagination(fetchUsers, { pageSize: 20 })
return {
users,
loading,
currentPage,
totalPages,
nextPage,
prevPage,
goToPage
}
}
}
代码组织架构
项目结构推荐
合理的项目结构是维护大型Vue应用的基础:
src/
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── base/ # 基础组件
│ └── business/ # 业务组件
├── composables/ # 自定义Hook
│ ├── useAuth.js
│ ├── useApi.js
│ └── useLocalStorage.js
├── layouts/ # 布局组件
├── pages/ # 页面组件
├── router/ # 路由配置
├── stores/ # 状态管理
├── utils/ # 工具函数
├── services/ # API服务
└── App.vue
组件设计模式
容器组件与展示组件分离
// 容器组件 - UserProfileContainer.vue
<script setup>
import { useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
import UserProfile from './UserProfile.vue'
const route = useRoute()
const userStore = useUserStore()
const userId = route.params.id
const user = userStore.getUserById(userId)
const handleUpdateUser = async (userData) => {
await userStore.updateUser(userId, userData)
}
</script>
<template>
<UserProfile
:user="user"
@update="handleUpdateUser"
/>
</template>
// 展示组件 - UserProfile.vue
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
user: {
type: Object,
required: true
}
})
const emit = defineEmits(['update'])
const handleInputChange = (field, value) => {
emit('update', { ...props.user, [field]: value })
}
</script>
<template>
<div class="user-profile">
<input
:value="user.name"
@input="handleInputChange('name', $event.target.value)"
/>
<input
:value="user.email"
@input="handleInputChange('email', $event.target.value)"
/>
</div>
</template>
组件插槽模式
// BaseCard.vue - 通用卡片组件
<template>
<div class="card">
<div class="card-header" v-if="$slots.header || title">
<slot name="header">
<h3>{{ title }}</h3>
</slot>
</div>
<div class="card-body">
<slot></slot>
</div>
<div class="card-footer" v-if="$slots.footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
title: String
})
</script>
// 使用示例
<template>
<BaseCard title="用户信息">
<template #header>
<h2>自定义头部</h2>
</template>
<p>这里是卡片内容</p>
<template #footer>
<button>操作按钮</button>
</template>
</BaseCard>
</template>
性能优化技巧
响应式数据优化
避免不必要的响应式转换
// 错误做法 - 不必要的响应式
const config = reactive({
apiUrl: 'https://api.example.com',
version: '1.0.0'
})
// 正确做法 - 使用普通对象
const config = {
apiUrl: 'https://api.example.com',
version: '1.0.0'
}
// 或者使用shallowReactive
import { shallowReactive } from 'vue'
const state = shallowReactive({
config: {
apiUrl: 'https://api.example.com',
version: '1.0.0'
},
users: []
})
计算属性缓存
import { ref, computed } from 'vue'
const items = ref([])
const filter = ref('')
// 正确使用计算属性 - 自动缓存
const filteredItems = computed(() => {
console.log('Computing filtered items...') // 只在依赖变化时执行
return items.value.filter(item =>
item.name.includes(filter.value)
)
})
// 错误做法 - 每次都重新计算
const getFilteredItems = () => {
console.log('Computing filtered items...') // 每次调用都执行
return items.value.filter(item =>
item.name.includes(filter.value)
)
}
监听器优化
import { ref, watch, watchEffect } from 'vue'
const searchQuery = ref('')
const results = ref([])
// 使用watchEffect自动追踪依赖
watchEffect(async () => {
if (searchQuery.value) {
results.value = await searchAPI(searchQuery.value)
} else {
results.value = []
}
})
// 使用watch精确控制依赖
watch(
searchQuery,
async (newQuery, oldQuery) => {
if (newQuery !== oldQuery) {
results.value = await searchAPI(newQuery)
}
},
{
immediate: true,
flush: 'post'
}
)
// 防抖处理
import { useDebounce } from '@/composables/useDebounce'
const debouncedQuery = useDebounce(searchQuery, 500)
watch(debouncedQuery, async (query) => {
results.value = await searchAPI(query)
})
TypeScript集成
类型定义最佳实践
// types/user.ts
export interface User {
id: number
name: string
email: string
age?: number
}
export interface UserState {
users: User[]
currentUser: User | null
loading: boolean
error: string | null
}
// composables/useUser.ts
import { ref, Ref } from 'vue'
import type { User, UserState } from '@/types/user'
export function useUser(): {
state: Ref<UserState>
fetchUsers: () => Promise<void>
addUser: (user: Omit<User, 'id'>) => Promise<void>
} {
const state = ref<UserState>({
users: [],
currentUser: null,
loading: false,
error: null
})
const fetchUsers = async () => {
state.value.loading = true
try {
const response = await fetch('/api/users')
state.value.users = await response.json()
} catch (error) {
state.value.error = (error as Error).message
} finally {
state.value.loading = false
}
}
const addUser = async (userData: Omit<User, 'id'>) => {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
})
const newUser = await response.json()
state.value.users.push(newUser)
} catch (error) {
state.value.error = (error as Error).message
}
}
return {
state,
fetchUsers,
addUser
}
}
Props和Emits类型定义
// 组件Props定义
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
import type { User } from '@/types/user'
interface Props {
user: User
editable?: boolean
size?: 'small' | 'medium' | 'large'
}
const props = withDefaults(defineProps<Props>(), {
editable: false,
size: 'medium'
})
// Emits定义
const emit = defineEmits<{
(e: 'update', user: User): void
(e: 'delete', id: number): void
}>()
const handleUpdate = () => {
emit('update', props.user)
}
</script>
测试最佳实践
单元测试
// composables/useCounter.test.js
import { ref } from 'vue'
import { useCounter } from '@/composables/useCounter'
describe('useCounter', () => {
it('should initialize with correct value', () => {
const { count } = useCounter(5)
expect(count.value).toBe(5)
})
it('should increment correctly', () => {
const { count, increment } = useCounter()
increment()
expect(count.value).toBe(1)
})
it('should compute double count', () => {
const { count, doubleCount } = useCounter()
count.value = 5
expect(doubleCount.value).toBe(10)
})
})
// 组件测试示例
import { mount } from '@vue/test-utils'
import Counter from '@/components/Counter.vue'
describe('Counter', () => {
it('renders correctly', () => {
const wrapper = mount(Counter, {
props: { initialCount: 5 }
})
expect(wrapper.text()).toContain('Count: 5')
})
it('increments when button is clicked', async () => {
const wrapper = mount(Counter)
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('Count: 1')
})
})
端到端测试
// tests/e2e/specs/user-management.js
describe('User Management', () => {
beforeEach(() => {
cy.visit('/users')
})
it('should display user list', () => {
cy.get('[data-cy=user-list]').should('be.visible')
cy.get('[data-cy=user-item]').should('have.length.greaterThan', 0)
})
it('should add new user', () => {
cy.get('[data-cy=add-user-btn]').click()
cy.get('[data-cy=user-form]').should('be.visible')
cy.get('[data-cy=name-input]').type('John Doe')
cy.get('[data-cy=email-input]').type('john@example.com')
cy.get('[data-cy=submit-btn]').click()
cy.get('[data-cy=user-list]').contains('John Doe')
})
})
错误处理和调试
全局错误处理
// utils/errorHandler.js
import { onErrorCaptured } from 'vue'
export function useErrorHandler() {
const error = ref(null)
onErrorCaptured((err, instance, info) => {
console.error('Error captured:', err)
console.error('Component:', instance?.type?.name || 'Unknown')
console.error('Info:', info)
error.value = {
message: err.message,
stack: err.stack,
component: instance?.type?.name,
info
}
// 发送错误报告到监控服务
reportError(err, instance, info)
return false
})
return { error }
}
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { useErrorHandler } from '@/utils/errorHandler'
const app = createApp(App)
// 全局错误处理
app.config.errorHandler = (err, instance, info) => {
console.error('Global error:', err)
// 错误上报逻辑
}
app.mount('#app')
调试工具和技巧
// 开发环境调试Hook
import { getCurrentInstance } from 'vue'
export function useDebug(name) {
if (process.env.NODE_ENV === 'development') {
const instance = getCurrentInstance()
console.log(`[${name}] Component mounted:`, instance?.type?.name)
// 监听组件更新
onUpdated(() => {
console.log(`[${name}] Component updated`)
})
// 监听组件卸载
onUnmounted(() => {
console.log(`[${name}] Component unmounted`)
})
}
}
// 性能监控Hook
import { onMounted, onUnmounted } from 'vue'
export function usePerformance(name) {
if (process.env.NODE_ENV === 'development') {
const startTime = performance.now()
onMounted(() => {
const mountTime = performance.now() - startTime
console.log(`[${name}] Mount time: ${mountTime.toFixed(2)}ms`)
})
}
}
实际应用案例
完整的用户管理模块
// composables/useUserManagement.js
import { ref, computed, watch } from 'vue'
import { useApi } from '@/composables/use
评论 (0)