引言
Vue 3的发布带来了革命性的Composition API,它为开发者提供了更加灵活和强大的组件开发方式。相比Vue 2的选项式API,Composition API让代码组织更加清晰,逻辑复用更加容易,特别是在处理复杂应用的状态管理和组件通信方面表现尤为突出。
本文将通过一个完整的项目案例,深入探讨如何在实际开发中运用Vue 3 Composition API的各项特性,涵盖从基础组件通信到复杂状态管理的完整流程,帮助开发者构建更优雅、更可维护的前端应用架构。
Vue 3 Composition API核心概念
什么是Composition API
Composition API是Vue 3引入的一种新的组件开发方式,它将组件逻辑按功能模块进行组织,而不是按照传统的选项(data、methods、computed等)来划分。这种设计模式使得代码更加灵活,便于复用和维护。
// Vue 2 选项式API
export default {
data() {
return {
count: 0,
message: 'Hello'
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
}
}
// Vue 3 Composition API
import { ref, computed } from 'vue'
export default {
setup() {
const count = ref(0)
const message = ref('Hello')
const doubleCount = computed(() => count.value * 2)
const increment = () => {
count.value++
}
return {
count,
message,
doubleCount,
increment
}
}
}
核心响应式API
Composition API提供了多种响应式API,包括ref、reactive、computed、watch等:
import { ref, reactive, computed, watch } from 'vue'
// ref - 创建响应式引用
const count = ref(0)
const name = ref('Vue')
// reactive - 创建响应式对象
const state = reactive({
user: {
name: 'John',
age: 30
},
items: []
})
// computed - 创建计算属性
const doubleCount = computed(() => count.value * 2)
// watch - 监听响应式数据变化
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
组件间通信实战
Props传递与验证
在Vue 3中,props的使用变得更加灵活。我们可以使用setup函数来接收和处理props:
// Parent.vue
<template>
<Child
:title="pageTitle"
:user-info="userInfo"
:items="listItems"
@update-items="handleUpdateItems"
/>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const pageTitle = ref('用户管理')
const userInfo = ref({
name: '张三',
email: 'zhangsan@example.com'
})
const listItems = ref([
{ id: 1, name: '项目A' },
{ id: 2, name: '项目B' }
])
const handleUpdateItems = (newItems) => {
listItems.value = newItems
}
</script>
// Child.vue
<script setup>
import { computed } from 'vue'
// 定义props和验证
const props = defineProps({
title: {
type: String,
required: true
},
userInfo: {
type: Object,
default: () => ({})
},
items: {
type: Array,
default: () => []
}
})
// 定义事件
const emit = defineEmits(['updateItems'])
// 计算属性
const userDisplayName = computed(() => {
return props.userInfo.name || '匿名用户'
})
// 方法
const addItem = (item) => {
const newItems = [...props.items, item]
emit('updateItems', newItems)
}
</script>
<template>
<div class="child-component">
<h2>{{ title }}</h2>
<p>当前用户:{{ userDisplayName }}</p>
<ul>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
<button @click="addItem({ id: Date.now(), name: '新项目' })">
添加项目
</button>
</div>
</template>
Provide/Inject模式
Provide/Inject是Vue中用于跨层级组件通信的重要机制。在Composition API中,我们可以更优雅地使用它:
// Parent.vue
<script setup>
import { provide, reactive } from 'vue'
import ChildComponent from './ChildComponent.vue'
// 创建共享状态
const sharedState = reactive({
theme: 'light',
language: 'zh-CN',
currentUser: null
})
// 提供数据
provide('appContext', {
state: sharedState,
updateTheme: (theme) => {
sharedState.theme = theme
},
setCurrentUser: (user) => {
sharedState.currentUser = user
}
})
</script>
<template>
<div class="app">
<ChildComponent />
</div>
</template>
// ChildComponent.vue
<script setup>
import { inject, computed } from 'vue'
// 注入数据
const appContext = inject('appContext')
// 使用注入的数据
const currentTheme = computed(() => appContext.state.theme)
const currentUser = computed(() => appContext.state.currentUser)
// 修改共享状态
const switchTheme = () => {
const newTheme = appContext.state.theme === 'light' ? 'dark' : 'light'
appContext.updateTheme(newTheme)
}
</script>
<template>
<div class="child-component" :class="currentTheme">
<p>当前主题:{{ currentTheme }}</p>
<p>当前用户:{{ currentUser?.name || '未登录' }}</p>
<button @click="switchTheme">切换主题</button>
</div>
</template>
事件总线模式
虽然Vue 3推荐使用Props和Events进行组件通信,但有时我们仍然需要一个全局的事件系统:
// eventBus.js
import { createApp } from 'vue'
export const EventBus = {
// 创建事件总线实例
instance: null,
// 初始化事件总线
init(app) {
this.instance = app.config.globalProperties.$bus = {}
this.setupEventHandlers()
},
// 设置事件处理方法
setupEventHandlers() {
const bus = this.instance
bus.on = (event, callback) => {
if (!bus._events) bus._events = {}
if (!bus._events[event]) bus._events[event] = []
bus._events[event].push(callback)
}
bus.emit = (event, data) => {
if (bus._events && bus._events[event]) {
bus._events[event].forEach(callback => callback(data))
}
}
bus.off = (event, callback) => {
if (bus._events && bus._events[event]) {
if (callback) {
bus._events[event] = bus._events[event].filter(cb => cb !== callback)
} else {
delete bus._events[event]
}
}
}
}
}
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { EventBus } from './utils/eventBus'
const app = createApp(App)
EventBus.init(app)
app.mount('#app')
// ComponentA.vue
<script setup>
import { inject } from 'vue'
const bus = inject('$bus')
const sendMessage = () => {
bus.emit('message-sent', {
content: 'Hello from Component A',
timestamp: Date.now()
})
}
</script>
<template>
<button @click="sendMessage">发送消息</button>
</template>
// ComponentB.vue
<script setup>
import { inject, onMounted, onUnmounted } from 'vue'
const bus = inject('$bus')
const messages = ref([])
const handleMessage = (data) => {
messages.value.push(data)
}
onMounted(() => {
bus.on('message-sent', handleMessage)
})
onUnmounted(() => {
bus.off('message-sent', handleMessage)
})
</script>
<template>
<div>
<h3>接收的消息:</h3>
<ul>
<li v-for="msg in messages" :key="msg.timestamp">
{{ msg.content }} - {{ new Date(msg.timestamp).toLocaleTimeString() }}
</li>
</ul>
</div>
</template>
响应式数据管理
状态管理基础
在Vue 3中,我们可以使用响应式API来构建简单但有效的状态管理系统:
// store/userStore.js
import { reactive, readonly } from 'vue'
// 创建响应式状态
const state = reactive({
currentUser: null,
isLoggedIn: false,
profile: {
name: '',
email: '',
avatar: ''
}
})
// 提供只读状态访问
export const useUserStore = () => {
// 获取用户信息
const getUserInfo = () => readonly(state.currentUser)
// 登录
const login = (userData) => {
state.currentUser = userData
state.isLoggedIn = true
state.profile = {
name: userData.name,
email: userData.email,
avatar: userData.avatar || ''
}
}
// 登出
const logout = () => {
state.currentUser = null
state.isLoggedIn = false
state.profile = {
name: '',
email: '',
avatar: ''
}
}
// 更新用户信息
const updateProfile = (profileData) => {
Object.assign(state.profile, profileData)
if (state.currentUser) {
state.currentUser.name = profileData.name
state.currentUser.email = profileData.email
}
}
return {
state: readonly(state),
getUserInfo,
login,
logout,
updateProfile
}
}
复杂状态管理
对于更复杂的应用,我们可以构建一个更加完整的状态管理系统:
// store/index.js
import { reactive, readonly } from 'vue'
// 创建全局状态
const globalState = reactive({
loading: false,
error: null,
notifications: [],
theme: 'light'
})
// 状态管理器
export const useGlobalStore = () => {
// 设置加载状态
const setLoading = (loading) => {
globalState.loading = loading
}
// 设置错误信息
const setError = (error) => {
globalState.error = error
}
// 添加通知
const addNotification = (notification) => {
const id = Date.now()
const notificationWithId = { ...notification, id }
globalState.notifications.push(notificationWithId)
// 自动移除通知(3秒后)
setTimeout(() => {
removeNotification(id)
}, 3000)
}
// 移除通知
const removeNotification = (id) => {
const index = globalState.notifications.findIndex(n => n.id === id)
if (index > -1) {
globalState.notifications.splice(index, 1)
}
}
// 切换主题
const toggleTheme = () => {
globalState.theme = globalState.theme === 'light' ? 'dark' : 'light'
}
return {
state: readonly(globalState),
setLoading,
setError,
addNotification,
removeNotification,
toggleTheme
}
}
// composables/useApi.js
import { ref, reactive } from 'vue'
import { useGlobalStore } from '@/store'
export const useApi = () => {
const { setLoading, setError, addNotification } = useGlobalStore()
// API调用函数
const apiCall = async (apiFunction, ...args) => {
try {
setLoading(true)
setError(null)
const result = await apiFunction(...args)
return result
} catch (error) {
setError(error.message)
addNotification({
type: 'error',
message: `API调用失败:${error.message}`
})
throw error
} finally {
setLoading(false)
}
}
// 数据获取函数
const fetchData = async (url) => {
const response = await apiCall(
fetch.bind(null, url),
{ method: 'GET' }
)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response.json()
}
// 数据提交函数
const submitData = async (url, data) => {
const response = await apiCall(
fetch.bind(null, url),
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
}
)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response.json()
}
return {
apiCall,
fetchData,
submitData
}
}
自定义Hook封装
数据获取Hook
自定义Hook是Composition API的核心优势之一,它让我们能够将可复用的逻辑封装起来:
// composables/useFetch.js
import { ref, watch } from 'vue'
export const useFetch = (url, options = {}) => {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
// 执行请求
const execute = async () => {
try {
loading.value = true
error.value = null
const response = await fetch(url, options)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
data.value = await response.json()
} catch (err) {
error.value = err.message
console.error('Fetch error:', err)
} finally {
loading.value = false
}
}
// 初始加载
if (options.immediate !== false) {
execute()
}
return {
data,
loading,
error,
execute
}
}
// composables/usePagination.js
import { ref, computed } from 'vue'
export const usePagination = (items, pageSize = 10) => {
const currentPage = ref(1)
const _items = ref(items)
// 计算总页数
const totalPages = computed(() => {
return Math.ceil(_items.value.length / pageSize)
})
// 计算当前页数据
const currentItems = computed(() => {
const start = (currentPage.value - 1) * pageSize
const end = start + pageSize
return _items.value.slice(start, end)
})
// 切换页面
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
// 上一页
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
}
}
// 下一页
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
}
}
// 更新数据
const updateItems = (newItems) => {
_items.value = newItems
currentPage.value = 1
}
return {
currentPage,
totalPages,
currentItems,
goToPage,
prevPage,
nextPage,
updateItems
}
}
表单处理Hook
// composables/useForm.js
import { reactive, computed } from 'vue'
export const useForm = (initialValues = {}) => {
const formState = reactive({ ...initialValues })
const errors = reactive({})
// 验证规则
const validateRules = {}
// 设置验证规则
const setRules = (rules) => {
Object.assign(validateRules, rules)
}
// 验证单个字段
const validateField = (field, value) => {
if (!validateRules[field]) return true
const rules = validateRules[field]
for (const rule of rules) {
if (typeof rule === 'function') {
if (!rule(value)) {
errors[field] = rule.message || '验证失败'
return false
}
} else if (rule.required && !value) {
errors[field] = rule.message || '此字段为必填项'
return false
}
}
delete errors[field]
return true
}
// 验证整个表单
const validateForm = () => {
let isValid = true
Object.keys(validateRules).forEach(field => {
if (!validateField(field, formState[field])) {
isValid = false
}
})
return isValid
}
// 设置字段值
const setFieldValue = (field, value) => {
formState[field] = value
validateField(field, value)
}
// 重置表单
const resetForm = () => {
Object.keys(formState).forEach(key => {
formState[key] = initialValues[key] || ''
})
Object.keys(errors).forEach(key => {
delete errors[key]
})
}
// 表单是否有效
const isValid = computed(() => {
return Object.keys(errors).length === 0
})
return {
formState,
errors,
isValid,
setRules,
setFieldValue,
validateField,
validateForm,
resetForm
}
}
实际项目案例:任务管理应用
应用架构设计
让我们通过一个完整的任务管理应用来演示这些技术的综合运用:
// App.vue
<template>
<div class="app" :class="globalStore.state.theme">
<header class="app-header">
<h1>任务管理系统</h1>
<button @click="globalStore.toggleTheme">切换主题</button>
</header>
<main class="app-main">
<div class="sidebar">
<TaskFilter
:filter="currentFilter"
@update-filter="handleFilterUpdate"
/>
</div>
<div class="content">
<TaskList
:tasks="filteredTasks"
:loading="fetching"
@task-updated="handleTaskUpdated"
@task-deleted="handleTaskDeleted"
/>
<TaskForm
@task-created="handleTaskCreated"
/>
</div>
</main>
<NotificationList :notifications="globalStore.state.notifications" />
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useGlobalStore } from '@/store'
import { useFetch } from '@/composables/useFetch'
import TaskFilter from './components/TaskFilter.vue'
import TaskList from './components/TaskList.vue'
import TaskForm from './components/TaskForm.vue'
import NotificationList from './components/NotificationList.vue'
const globalStore = useGlobalStore()
const { data: tasks, loading: fetching, execute: fetchTasks } = useFetch('/api/tasks')
const currentFilter = ref('all')
// 筛选任务
const filteredTasks = computed(() => {
if (!tasks.value) return []
switch (currentFilter.value) {
case 'active':
return tasks.value.filter(task => !task.completed)
case 'completed':
return tasks.value.filter(task => task.completed)
default:
return tasks.value
}
})
const handleFilterUpdate = (filter) => {
currentFilter.value = filter
}
const handleTaskCreated = async (taskData) => {
try {
const newTask = await fetch('/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(taskData)
}).then(res => res.json())
tasks.value.push(newTask)
globalStore.addNotification({
type: 'success',
message: '任务创建成功'
})
} catch (error) {
globalStore.addNotification({
type: 'error',
message: '任务创建失败'
})
}
}
const handleTaskUpdated = async (taskData) => {
try {
const updatedTask = await fetch(`/api/tasks/${taskData.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(taskData)
}).then(res => res.json())
const index = tasks.value.findIndex(t => t.id === taskData.id)
if (index > -1) {
tasks.value[index] = updatedTask
}
globalStore.addNotification({
type: 'success',
message: '任务更新成功'
})
} catch (error) {
globalStore.addNotification({
type: 'error',
message: '任务更新失败'
})
}
}
const handleTaskDeleted = async (taskId) => {
try {
await fetch(`/api/tasks/${taskId}`, {
method: 'DELETE'
})
const index = tasks.value.findIndex(t => t.id === taskId)
if (index > -1) {
tasks.value.splice(index, 1)
}
globalStore.addNotification({
type: 'success',
message: '任务删除成功'
})
} catch (error) {
globalStore.addNotification({
type: 'error',
message: '任务删除失败'
})
}
}
// 初始化加载
fetchTasks()
</script>
<style scoped>
.app {
min-height: 100vh;
background-color: #f5f5f5;
transition: background-color 0.3s ease;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.app-main {
display: flex;
min-height: calc(100vh - 60px);
}
.sidebar {
width: 250px;
padding: 1rem;
background-color: #fff;
border-right: 1px solid #eee;
}
.content {
flex: 1;
padding: 1rem;
}
</style>
组件实现
// components/TaskFilter.vue
<template>
<div class="task-filter">
<h3>任务筛选</h3>
<div class="filter-buttons">
<button
v-for="filter in filters"
:key="filter.value"
:class="{ active: currentFilter === filter.value }"
@click="handleFilterClick(filter.value)"
>
{{ filter.label }}
</button>
</div>
</div>
</template>
<script setup>
const props = defineProps({
filter: {
type: String,
default: 'all'
}
})
const emit = defineEmits(['updateFilter'])
const filters = [
{ value: 'all', label: '全部任务' },
{ value: 'active', label: '未完成' },
{ value: 'completed', label: '已完成' }
]
const currentFilter = computed(() => props.filter)
const handleFilterClick = (filter) => {
emit('updateFilter', filter)
}
</script>
<style scoped>
.task-filter h3 {
margin-top: 0;
margin-bottom: 1rem;
}
.filter-buttons {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.filter-buttons button {
padding: 0.5rem;
border: 1px solid #ddd;
background-color: #f8f9fa;
cursor: pointer;
transition: all 0.2s ease;
}
.filter-buttons button:hover {
background-color: #e9ecef;
}
.filter-buttons button.active {
background-color: #007bff;
color: white;
border-color: #007bff;
}
</style>
// components/TaskList.vue
<template>
<div class="task-list">
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="tasks.length === 0" class="empty-state">
暂无任务
</div>
<div v-else class="tasks-container">
<TaskItem
v-for="task in tasks"
:key="task.id"
:task="task"
@update-task="handleUpdate"
@delete-task="handleDelete"
/>
</div>
</div>
</template>
<script setup>
import TaskItem from './TaskItem.vue'
const props = defineProps({
tasks: {
type: Array,
default: () => []
},
loading: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['taskUpdated', 'taskDeleted'])
const handleUpdate = (task) => {
emit('taskUpdated', task)
}
const handleDelete = (taskId) => {
emit('taskDeleted', taskId)
}
</script>
<style scoped>
.loading {
text-align: center;
padding: 2rem;
color: #666;
}
.empty-state {
text-align: center;
padding: 2rem;
color: #999;
}
.tasks-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
</style>
// components/TaskItem.vue
<template>
<div class="task-item" :class="{ completed: task.completed }">
<div class="task-content">
<input
type="checkbox"
:checked="task.completed"
@change="handleToggle"
/>
<span class="task-text">{{ task.title }}</span>
</div>
<div class="task-actions">
<button @click="handleEdit">编辑</button>
<button @click="handleDelete">删除</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
task: {
type: Object,
required: true
}
})
const emit = defineEmits(['updateTask', 'deleteTask'])
const handleToggle = () => {
const updatedTask = {
...props.task,
completed: !props.task.completed
}
emit('updateTask', updatedTask)
}
const handleEdit = () => {
// 这里可以打开编辑模态框
console.log('编辑任务:', props.task)
}
const handleDelete = () => {
emit('deleteTask', props.task.id)
}
</script>
<style scoped>
.task-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background-color: #fff;
border: 1px solid #eee;
border-radius: 4px;
transition: all 0.2s ease;
}
.task-item:hover {
box-shadow: 0 
评论 (0)