引言
Vue 3 的发布带来了革命性的变化,其中最引人注目的就是 Composition API 的引入。相比于传统的 Options API,Composition API 提供了更加灵活和强大的开发方式,特别是在处理复杂组件逻辑时展现出了巨大优势。本文将深入探讨 Vue 3 Composition API 的设计理念和使用方法,并通过构建完整的应用案例来展示如何在实际项目中运用这些技术。
Vue 3 Composition API 核心概念
什么是 Composition API?
Composition API 是 Vue 3 中引入的一种新的组件逻辑组织方式,它允许开发者以函数的形式组织组件逻辑,而不是传统的选项式结构。这种方式使得代码更加灵活,更容易复用和维护。
主要优势
- 更好的逻辑复用:通过组合式函数(Composable Functions)实现逻辑的复用
- 更清晰的代码组织:将相关的逻辑组织在一起,而不是分散在不同的选项中
- 更强的类型支持:与 TypeScript 集成更好,提供更好的开发体验
- 更灵活的组件设计:可以更自由地组织和管理组件状态
响应式系统详解
reactive 和 ref 的使用
import { reactive, ref, computed } from 'vue'
// 使用 ref 创建响应式数据
const count = ref(0)
const name = ref('Vue')
// 使用 reactive 创建响应式对象
const state = reactive({
user: {
name: 'John',
age: 30
},
posts: []
})
// 访问和修改数据
console.log(count.value) // 0
count.value = 1
computed 的高级用法
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// 基本计算属性
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
// 带有 getter 和 setter 的计算属性
const reversedName = computed({
get: () => {
return firstName.value.split('').reverse().join('')
},
set: (value) => {
firstName.value = value.split('').reverse().join('')
}
})
组件通信实战
父子组件通信
父组件向子组件传递数据
<!-- Parent.vue -->
<template>
<div class="parent">
<h2>父组件</h2>
<Child
:message="parentMessage"
:user-info="userInfo"
@child-event="handleChildEvent"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const parentMessage = ref('Hello from parent')
const userInfo = ref({
name: 'Alice',
age: 25
})
const handleChildEvent = (data) => {
console.log('收到子组件事件:', data)
}
</script>
<!-- Child.vue -->
<template>
<div class="child">
<h3>子组件</h3>
<p>{{ message }}</p>
<p>用户信息: {{ userInfo.name }} - {{ userInfo.age }}</p>
<button @click="sendToParent">发送消息给父组件</button>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
// 定义 props
const props = defineProps({
message: {
type: String,
default: ''
},
userInfo: {
type: Object,
required: true
}
})
// 定义 emits
const emit = defineEmits(['child-event'])
const sendToParent = () => {
emit('child-event', {
message: 'Hello from child',
timestamp: Date.now()
})
}
</script>
兄弟组件通信
<!-- ComponentA.vue -->
<template>
<div class="component-a">
<h3>组件 A</h3>
<input v-model="message" placeholder="输入消息" />
<button @click="sendMessage">发送消息</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useSharedState } from '../composables/useSharedState'
const message = ref('')
const { sharedMessage, setSharedMessage } = useSharedState()
const sendMessage = () => {
setSharedMessage(message.value)
}
</script>
<!-- ComponentB.vue -->
<template>
<div class="component-b">
<h3>组件 B</h3>
<p>接收到的消息: {{ sharedMessage }}</p>
</div>
</template>
<script setup>
import { useSharedState } from '../composables/useSharedState'
const { sharedMessage } = useSharedState()
</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
}
// composables/useApi.js
import { ref, reactive } from 'vue'
export function useApi() {
const loading = ref(false)
const error = ref(null)
const data = ref(null)
const fetchData = async (url) => {
try {
loading.value = true
error.value = null
const response = await fetch(url)
data.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
return {
loading,
error,
data,
fetchData
}
}
// composables/useSharedState.js
import { ref } from 'vue'
const sharedMessage = ref('')
const sharedUser = ref(null)
export function useSharedState() {
const setSharedMessage = (message) => {
sharedMessage.value = message
}
const setSharedUser = (user) => {
sharedUser.value = user
}
return {
sharedMessage,
sharedUser,
setSharedMessage,
setSharedUser
}
}
使用组合式函数
<!-- UserProfile.vue -->
<template>
<div class="user-profile">
<h2>用户信息</h2>
<div v-if="loading">加载中...</div>
<div v-else-if="error">{{ error }}</div>
<div v-else-if="data">
<p>姓名: {{ data.name }}</p>
<p>邮箱: {{ data.email }}</p>
<p>年龄: {{ data.age }}</p>
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
import { useApi } from '../composables/useApi'
const { loading, error, data, fetchData } = useApi()
onMounted(() => {
fetchData('/api/user/1')
})
</script>
<!-- ThemeSwitcher.vue -->
<template>
<div class="theme-switcher">
<button @click="toggleTheme">切换主题</button>
<p>当前主题: {{ theme }}</p>
</div>
</template>
<script setup>
import { useLocalStorage } from '../composables/useLocalStorage'
const theme = useLocalStorage('theme', 'light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
</script>
状态管理实践
简单的状态管理器
// store/userStore.js
import { ref, computed } from 'vue'
const currentUser = ref(null)
const isLoggedIn = ref(false)
const userPreferences = ref({})
export function useUserStore() {
const login = (userData) => {
currentUser.value = userData
isLoggedIn.value = true
}
const logout = () => {
currentUser.value = null
isLoggedIn.value = false
}
const updatePreferences = (preferences) => {
userPreferences.value = { ...userPreferences.value, ...preferences }
}
const getUserInfo = computed(() => ({
user: currentUser.value,
isAuthenticated: isLoggedIn.value,
preferences: userPreferences.value
}))
return {
login,
logout,
updatePreferences,
getUserInfo
}
}
<!-- Login.vue -->
<template>
<div class="login">
<form @submit.prevent="handleLogin">
<input v-model="username" placeholder="用户名" />
<input v-model="password" type="password" placeholder="密码" />
<button type="submit">登录</button>
</form>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useUserStore } from '../store/userStore'
const username = ref('')
const password = ref('')
const { login } = useUserStore()
const handleLogin = async () => {
try {
// 模拟 API 调用
const userData = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username: username.value, password: password.value })
}).then(res => res.json())
login(userData)
// 跳转到主页或其他页面
} catch (error) {
console.error('登录失败:', error)
}
}
</script>
复杂状态管理示例
// store/appStore.js
import { ref, computed, watch } from 'vue'
const state = ref({
loading: false,
error: null,
data: [],
filters: {
category: '',
search: '',
sortBy: 'name'
},
pagination: {
page: 1,
limit: 10,
total: 0
}
})
export function useAppStore() {
// 获取数据
const fetchData = async () => {
state.value.loading = true
state.value.error = null
try {
const response = await fetch('/api/data', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
const result = await response.json()
state.value.data = result.data
state.value.pagination.total = result.total
} catch (error) {
state.value.error = error.message
} finally {
state.value.loading = false
}
}
// 更新过滤器
const updateFilter = (key, value) => {
state.value.filters[key] = value
state.value.pagination.page = 1
}
// 更新分页
const updatePagination = (page) => {
state.value.pagination.page = page
}
// 重置过滤器
const resetFilters = () => {
state.value.filters = {
category: '',
search: '',
sortBy: 'name'
}
state.value.pagination.page = 1
}
// 计算属性
const filteredData = computed(() => {
let result = [...state.value.data]
// 应用搜索过滤
if (state.value.filters.search) {
const searchLower = state.value.filters.search.toLowerCase()
result = result.filter(item =>
item.name.toLowerCase().includes(searchLower) ||
item.description.toLowerCase().includes(searchLower)
)
}
// 应用分类过滤
if (state.value.filters.category) {
result = result.filter(item => item.category === state.value.filters.category)
}
// 排序
if (state.value.filters.sortBy) {
result.sort((a, b) => {
if (a[state.value.filters.sortBy] < b[state.value.filters.sortBy]) return -1
if (a[state.value.filters.sortBy] > b[state.value.filters.sortBy]) return 1
return 0
})
}
return result
})
const paginatedData = computed(() => {
const start = (state.value.pagination.page - 1) * state.value.pagination.limit
const end = start + state.value.pagination.limit
return filteredData.value.slice(start, end)
})
// 监听过滤器变化,自动重新获取数据
watch(
() => state.value.filters,
() => {
fetchData()
},
{ deep: true }
)
return {
state: computed(() => state.value),
fetchData,
updateFilter,
updatePagination,
resetFilters,
filteredData,
paginatedData
}
}
高级技巧和最佳实践
条件渲染和动态组件
<!-- DynamicComponent.vue -->
<template>
<div class="dynamic-component">
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.name"
@click="activeTab = tab.name"
:class="{ active: activeTab === tab.name }"
>
{{ tab.label }}
</button>
</div>
<component
:is="currentComponent"
v-bind="currentProps"
@update-data="handleDataUpdate"
/>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
const tabs = [
{ name: 'componentA', label: '组件 A' },
{ name: 'componentB', label: '组件 B' }
]
const activeTab = ref('componentA')
const componentData = ref({})
const currentComponent = computed(() => {
return activeTab.value === 'componentA' ? ComponentA : ComponentB
})
const currentProps = computed(() => {
return {
data: componentData.value,
...activeTab.value === 'componentA' ? { title: '组件 A 标题' } : { title: '组件 B 标题' }
}
})
const handleDataUpdate = (data) => {
componentData.value = data
}
</script>
错误处理和加载状态
// composables/useAsync.js
import { ref, computed } from 'vue'
export function useAsync(asyncFunction) {
const loading = ref(false)
const error = ref(null)
const data = ref(null)
const executed = ref(false)
const execute = async (...args) => {
try {
loading.value = true
error.value = null
data.value = await asyncFunction(...args)
executed.value = true
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
const reset = () => {
loading.value = false
error.value = null
data.value = null
executed.value = false
}
return {
loading,
error,
data,
executed,
execute,
reset
}
}
性能优化技巧
<!-- OptimizedComponent.vue -->
<template>
<div class="optimized-component">
<div v-for="item in items" :key="item.id" class="item">
<span>{{ item.name }}</span>
<button @click="handleItemClick(item)">操作</button>
</div>
</div>
</template>
<script setup>
import { ref, shallowRef, watch, computed } from 'vue'
// 使用 shallowRef 优化对象引用
const items = shallowRef([])
// 使用计算属性缓存复杂计算
const processedItems = computed(() => {
return items.value.map(item => ({
...item,
processed: item.name.toUpperCase()
}))
})
// 监听特定属性变化
watch(
() => items.value.length,
(newLength, oldLength) => {
console.log(`项目数量从 ${oldLength} 变为 ${newLength}`)
}
)
const handleItemClick = (item) => {
// 避免不必要的重新渲染
console.log('点击项目:', item.id)
}
</script>
实际应用案例
完整的博客管理系统
<!-- BlogApp.vue -->
<template>
<div class="blog-app">
<header class="app-header">
<h1>我的博客</h1>
<nav>
<router-link to="/">首页</router-link>
<router-link to="/admin">管理</router-link>
</nav>
</header>
<main class="app-main">
<router-view />
</main>
<footer class="app-footer">
<p>© 2023 我的博客</p>
</footer>
</div>
</template>
<script setup>
import { onMounted } from 'vue'
import { useUserStore } from './store/userStore'
const { getUserInfo } = useUserStore()
onMounted(() => {
// 初始化用户状态
console.log('应用初始化')
})
</script>
<!-- BlogList.vue -->
<template>
<div class="blog-list">
<div class="controls">
<input v-model="searchQuery" placeholder="搜索文章..." />
<select v-model="selectedCategory">
<option value="">所有分类</option>
<option v-for="category in categories" :key="category" :value="category">
{{ category }}
</option>
</select>
</div>
<div class="articles">
<article
v-for="article in paginatedArticles"
:key="article.id"
class="article-card"
>
<h2>{{ article.title }}</h2>
<p class="meta">
{{ formatDate(article.createdAt) }} |
{{ article.category }}
</p>
<p class="excerpt">{{ article.excerpt }}</p>
<router-link :to="`/article/${article.id}`" class="read-more">
阅读更多
</router-link>
</article>
</div>
<div class="pagination">
<button
@click="currentPage--"
:disabled="currentPage === 1"
>
上一页
</button>
<span>第 {{ currentPage }} 页</span>
<button
@click="currentPage++"
:disabled="currentPage === totalPages"
>
下一页
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useAppStore } from '../store/appStore'
const searchQuery = ref('')
const selectedCategory = ref('')
const currentPage = ref(1)
const {
state: appState,
fetchData,
updateFilter,
updatePagination
} = useAppStore()
// 获取分类列表
const categories = computed(() => {
const allCategories = appState.value.data.map(item => item.category)
return [...new Set(allCategories)]
})
// 过滤后的文章
const filteredArticles = computed(() => {
let result = [...appState.value.data]
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
result = result.filter(article =>
article.title.toLowerCase().includes(query) ||
article.content.toLowerCase().includes(query)
)
}
if (selectedCategory.value) {
result = result.filter(article =>
article.category === selectedCategory.value
)
}
return result
})
// 分页文章
const paginatedArticles = computed(() => {
const start = (currentPage.value - 1) * 10
const end = start + 10
return filteredArticles.value.slice(start, end)
})
const totalPages = computed(() => {
return Math.ceil(filteredArticles.value.length / 10)
})
// 格式化日期
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('zh-CN')
}
// 监听分页变化
watch(currentPage, () => {
updatePagination(currentPage.value)
})
// 初始化数据
fetchData()
</script>
总结
Vue 3 Composition API 为前端开发带来了革命性的变化,它不仅提供了更灵活的组件逻辑组织方式,还通过组合式函数实现了强大的逻辑复用能力。本文通过详细的代码示例和实际应用案例,展示了如何在真实项目中运用 Composition API 进行组件通信、状态管理和性能优化。
通过合理使用 ref、reactive、computed 等响应式API,结合自定义的组合式函数,我们可以构建出更加模块化、可维护的Vue应用。同时,借助组合式函数的复用能力,可以大大减少代码重复,提高开发效率。
在实际项目中,建议遵循以下最佳实践:
- 合理组织逻辑:将相关的功能逻辑封装到组合式函数中
- 类型安全:充分利用 TypeScript 提供的类型检查
- 性能优化:注意避免不必要的重新渲染和计算
- 错误处理:建立完善的错误处理机制
- 状态管理:根据应用复杂度选择合适的全局状态管理方案
通过深入理解和掌握 Vue 3 Composition API,开发者能够构建出更加现代化、可维护的前端应用,显著提升开发效率和代码质量。

评论 (0)