别再死记硬背了!用这5个真实项目案例,带你吃透Vue 3的Composition API
用5个真实项目案例彻底掌握Vue 3的Composition API
还在为Vue 3的Composition API感到困惑吗?与其死记硬背各种API的用法,不如通过实际项目来学习。本文将带你用5个真实场景下的项目案例,从零开始掌握Composition API的核心用法和设计思想。
1. 用户表单管理:从Options到Composition的蜕变
表单处理是前端开发中最常见的需求之一。让我们从一个典型的用户注册表单开始,看看如何用Composition API重构传统的Options API代码。
1.1 传统Options API的实现
在Vue 2中,我们可能会这样写一个表单组件:
export default { data() { return { form: { username: '', password: '', confirmPassword: '' }, errors: {} } }, methods: { validateUsername() { // 用户名验证逻辑 }, validatePassword() { // 密码验证逻辑 }, submitForm() { // 表单提交逻辑 } }, watch: { 'form.username': 'validateUsername', 'form.password': 'validatePassword' } }这种写法有几个明显的问题:
- 相关逻辑分散在不同的选项中
- 复用困难
- 随着组件复杂度增加,代码变得难以维护
1.2 Composition API重构
现在,让我们用Composition API重写这个表单组件:
import { reactive, watch } from 'vue' export function useFormValidation() { const form = reactive({ username: '', password: '', confirmPassword: '' }) const errors = reactive({}) function validateUsername() { if (!form.username) { errors.username = '用户名不能为空' } else if (form.username.length < 3) { errors.username = '用户名至少需要3个字符' } else { delete errors.username } } function validatePassword() { // 密码验证逻辑 } watch(() => form.username, validateUsername) watch(() => form.password, validatePassword) function submitForm() { validateUsername() validatePassword() if (Object.keys(errors).length === 0) { // 提交表单 } } return { form, errors, submitForm } }在组件中使用:
import { useFormValidation } from './useFormValidation' export default { setup() { const { form, errors, submitForm } = useFormValidation() return { form, errors, submitForm } } }关键改进点:
- 相关逻辑集中在一个函数中
- 验证逻辑可以轻松复用
- 代码组织更加清晰
2. 数据可视化图表:响应式数据的艺术
数据可视化是现代Web应用的重要组成部分。让我们看看如何用Composition API构建一个响应式的图表组件。
2.1 构建可复用的图表逻辑
import { ref, computed, onMounted, onUnmounted } from 'vue' import * as echarts from 'echarts' export function useChart(containerRef, options) { const chartInstance = ref(null) const isLoading = ref(false) const chartOptions = computed(() => ({ ...options, // 可以根据需要添加默认配置 })) function initChart() { if (!containerRef.value) return chartInstance.value = echarts.init(containerRef.value) updateChart() } function updateChart() { if (!chartInstance.value) return isLoading.value = true chartInstance.value.setOption(chartOptions.value) isLoading.value = false } function resizeChart() { if (chartInstance.value) { chartInstance.value.resize() } } onMounted(() => { initChart() window.addEventListener('resize', resizeChart) }) onUnmounted(() => { if (chartInstance.value) { chartInstance.value.dispose() } window.removeEventListener('resize', resizeChart) }) return { isLoading, updateChart, resizeChart } }2.2 在组件中使用
<template> <div ref="chartContainer" style="width: 100%; height: 400px;"></div> <button @click="refreshData">刷新数据</button> </template> <script> import { ref } from 'vue' import { useChart } from './useChart' export default { setup() { const chartContainer = ref(null) const chartData = ref({ // 初始数据 }) const { isLoading, updateChart } = useChart(chartContainer, { // 图表配置 series: [{ data: chartData.value }] }) function refreshData() { // 获取新数据 fetchData().then(data => { chartData.value = data updateChart() }) } return { chartContainer, refreshData, isLoading } } } </script>优势体现:
- 图表逻辑与组件解耦
- 响应式数据自动更新图表
- 生命周期管理更加清晰
3. 拖拽列表:组合API的威力
拖拽排序是提升用户体验的常见功能。让我们看看如何用Composition API实现一个高性能的拖拽列表。
3.1 创建拖拽逻辑Hook
import { ref, onMounted, onUnmounted } from 'vue' export function useDragAndDrop(listRef, items) { const draggedItem = ref(null) const dragOverItem = ref(null) function handleDragStart(index) { draggedItem.value = index } function handleDragOver(index, event) { event.preventDefault() dragOverItem.value = index } function handleDrop() { if (draggedItem.value !== null && dragOverItem.value !== null) { const itemToMove = items.value[draggedItem.value] items.value.splice(draggedItem.value, 1) items.value.splice(dragOverItem.value, 0, itemToMove) } draggedItem.value = null dragOverItem.value = null } function addDragEvents() { const listItems = listRef.value.querySelectorAll('.draggable-item') listItems.forEach((item, index) => { item.setAttribute('draggable', 'true') item.addEventListener('dragstart', () => handleDragStart(index)) item.addEventListener('dragover', (e) => handleDragOver(index, e)) item.addEventListener('drop', handleDrop) }) } onMounted(() => { addDragEvents() }) onUnmounted(() => { const listItems = listRef.value?.querySelectorAll('.draggable-item') listItems?.forEach(item => { item.removeEventListener('dragstart', handleDragStart) item.removeEventListener('dragover', handleDragOver) item.removeEventListener('drop', handleDrop) }) }) return { draggedItem, dragOverItem } }3.2 在组件中集成
<template> <ul ref="listRef"> <li v-for="(item, index) in items" :key="item.id" class="draggable-item" :class="{ 'dragging': draggedItem === index, 'drag-over': dragOverItem === index }" > {{ item.name }} </li> </ul> </template> <script> import { ref } from 'vue' import { useDragAndDrop } from './useDragAndDrop' export default { setup() { const listRef = ref(null) const items = ref([ { id: 1, name: '项目1' }, { id: 2, name: '项目2' }, // 更多项目... ]) const { draggedItem, dragOverItem } = useDragAndDrop(listRef, items) return { listRef, items, draggedItem, dragOverItem } } } </script>关键收获:
- 拖拽逻辑完全封装
- 可以轻松应用到任何列表
- 性能优化更简单
4. 权限按钮组件:逻辑复用的典范
在企业级应用中,权限控制是必不可少的。让我们构建一个灵活的权限按钮组件。
4.1 创建权限控制Hook
import { ref, computed } from 'vue' export function usePermission(currentUser) { const permissions = ref([]) async function fetchPermissions() { // 从API获取用户权限 const response = await fetch('/api/permissions') permissions.value = await response.json() } const hasPermission = computed(() => { return (requiredPermission) => { return permissions.value.includes(requiredPermission) } }) return { fetchPermissions, hasPermission } }4.2 构建权限按钮组件
<template> <button v-if="hasPermission(requiredPermission)" v-bind="$attrs" @click="$emit('click', $event)" > <slot></slot> </button> </template> <script> import { defineProps, defineEmits } from 'vue' import { usePermission } from './usePermission' export default { props: { requiredPermission: { type: String, required: true } }, emits: ['click'], setup(props) { const { hasPermission } = usePermission() return { hasPermission } } } </script>4.3 使用示例
<template> <PermissionButton required-permission="create_user" @click="createUser" > 创建用户 </PermissionButton> </template>设计亮点:
- 权限逻辑集中管理
- 按钮组件简洁专注
- 类型安全(TypeScript友好)
5. 全局状态共享:小型应用的状态管理
对于不需要Vuex或Pinia的小型应用,我们可以用Composition API实现简单的全局状态共享。
5.1 创建全局状态Hook
import { reactive, readonly } from 'vue' const globalState = reactive({ user: null, settings: {}, notifications: [] }) export function useGlobalState() { function setUser(user) { globalState.user = user } function addNotification(message, type = 'info') { globalState.notifications.push({ id: Date.now(), message, type }) } function removeNotification(id) { const index = globalState.notifications.findIndex(n => n.id === id) if (index !== -1) { globalState.notifications.splice(index, 1) } } return { state: readonly(globalState), setUser, addNotification, removeNotification } }5.2 在组件中使用
<template> <div v-if="state.user"> 欢迎, {{ state.user.name }} <button @click="logout">退出</button> </div> <div v-else> <button @click="login">登录</button> </div> <div class="notifications"> <div v-for="notification in state.notifications" :key="notification.id" :class="`notification-${notification.type}`" > {{ notification.message }} <button @click="removeNotification(notification.id)">×</button> </div> </div> </template> <script> import { useGlobalState } from './useGlobalState' export default { setup() { const { state, setUser, addNotification, removeNotification } = useGlobalState() function login() { // 登录逻辑 setUser({ name: '用户1' }) addNotification('登录成功', 'success') } function logout() { setUser(null) addNotification('您已退出', 'info') } return { state, login, logout, removeNotification } } } </script>实现优势:
- 不需要额外状态管理库
- 响应式更新自动处理
- 状态变更可控
- 适合中小型应用
6. 组合式API的最佳实践
通过以上5个案例,我们已经看到了Composition API的强大之处。下面总结一些最佳实践:
6.1 逻辑组织原则
- 单一职责:每个组合式函数应该只关注一个特定功能
- 命名清晰:使用有意义的函数和变量名
- 参数明确:明确函数需要哪些参数
- 返回值合理:只返回组件真正需要的值和方法
6.2 性能优化技巧
- 使用
computed处理派生状态 - 使用
watchEffect自动追踪依赖 - 对于大型列表,考虑使用
shallowRef或shallowReactive - 及时清理副作用(如事件监听器、定时器)
6.3 测试策略
组合式函数的一个巨大优势是易于测试。例如,测试我们的表单验证Hook:
import { useFormValidation } from './useFormValidation' import { reactive } from 'vue' describe('useFormValidation', () => { it('应该验证用户名', () => { const { form, errors } = useFormValidation() form.username = '' expect(errors.username).toBe('用户名不能为空') form.username = 'ab' expect(errors.username).toBe('用户名至少需要3个字符') form.username = 'abc' expect(errors.username).toBeUndefined() }) })6.4 渐进式采用策略
如果你正在迁移一个Vue 2项目,可以采用渐进式策略:
- 从小型、独立的组件开始
- 先尝试重构工具函数为组合式函数
- 逐步替换mixins
- 最后处理复杂业务逻辑
7. 常见问题与解决方案
在实际开发中,你可能会遇到以下问题:
7.1 响应式丢失问题
问题:解构响应式对象时失去响应性
const state = reactive({ count: 0 }) const { count } = state // 失去响应性解决方案:
const state = reactive({ count: 0 }) const count = toRef(state, 'count') // 保持响应性7.2 生命周期困惑
问题:Options API生命周期钩子与Composition API的对应关系
| Options API | Composition API |
|---|---|
| beforeCreate | 不需要 (setup替代) |
| created | 不需要 (setup替代) |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
7.3 TypeScript集成
Composition API与TypeScript配合得天衣无缝。例如:
interface User { id: number name: string email: string } export function useUser() { const user = ref<User | null>(null) function setUser(newUser: User) { user.value = newUser } return { user, setUser } }7.4 代码组织建议
对于大型项目,建议这样组织组合式函数:
src/ composables/ useForm.ts useChart.ts useDragAndDrop.ts usePermission.ts useGlobalState.ts components/ FormComponent.vue ChartComponent.vue // 其他组件...8. 进阶技巧与模式
掌握了基础用法后,让我们看看一些进阶技巧。
8.1 依赖注入模式
// provider组件 import { provide } from 'vue' export default { setup() { const theme = reactive({ primaryColor: '#42b983', secondaryColor: '#35495e' }) provide('theme', theme) return { theme } } } // consumer组件 import { inject } from 'vue' export default { setup() { const theme = inject('theme') return { theme } } }8.2 异步状态管理
import { ref } from 'vue' export function useAsync(fn) { const data = ref(null) const error = ref(null) const loading = ref(false) async function execute(...args) { loading.value = true error.value = null try { data.value = await fn(...args) } catch (err) { error.value = err } finally { loading.value = false } } return { data, error, loading, execute } }使用示例:
const { data, loading, execute } = useAsync(fetchUsers) onMounted(() => { execute() })8.3 渲染函数与JSX
Composition API与渲染函数配合良好:
import { h, ref } from 'vue' export default { setup() { const count = ref(0) return () => h('div', [ h('button', { onClick: () => count.value++ }, '增加'), h('span', `当前计数: ${count.value}`) ]) } }或者使用JSX:
import { ref } from 'vue' export default { setup() { const count = ref(0) return () => ( <div> <button onClick={() => count.value++}>增加</button> <span>当前计数: {count.value}</span> </div> ) } }9. 与其他技术栈的集成
Composition API可以很好地与其他技术栈配合使用。
9.1 与Vue Router集成
import { useRoute, useRouter } from 'vue-router' export default { setup() { const route = useRoute() const router = useRouter() function navigateTo(path) { router.push(path) } return { currentPath: computed(() => route.path), navigateTo } } }9.2 与Vuex/Pinia集成
import { computed } from 'vue' import { useStore } from 'vuex' export default { setup() { const store = useStore() const count = computed(() => store.state.count) function increment() { store.commit('increment') } return { count, increment } } }9.3 与第三方库集成
以axios为例:
import { ref } from 'vue' import axios from 'axios' export function useApi() { const data = ref(null) const error = ref(null) const loading = ref(false) async function fetch(url) { loading.value = true try { const response = await axios.get(url) data.value = response.data } catch (err) { error.value = err } finally { loading.value = false } } return { data, error, loading, fetch } }10. 从Options API迁移的实用建议
如果你有Vue 2项目,以下迁移建议可能对你有帮助:
- 逐步迁移:不必一次性重写整个应用
- 从简单组件开始:先迁移展示组件,再处理复杂业务组件
- 利用工具:Vue官方提供了迁移辅助工具
- 团队培训:确保团队成员都理解Composition API的概念
- 代码审查:迁移过程中保持严格的代码审查
迁移步骤示例:
- 将组件选项转换为setup函数
- 将data属性转换为ref或reactive
- 将methods转换为普通函数
- 将computed属性转换为computed函数
- 将生命周期钩子转换为onXxx函数
- 将watch选项转换为watch或watchEffect
11. 实战中的性能考量
虽然Composition API本身很高效,但仍需注意以下性能问题:
11.1 避免不必要的响应式
// 不推荐 - 整个对象变成响应式,但可能只需要部分属性 const state = reactive({ largeData: fetchLargeData(), // 其他属性... }) // 推荐 - 只对需要响应式的部分使用ref const largeData = fetchLargeData() // 普通对象 const count = ref(0) // 响应式值11.2 合理使用watch和watchEffect
// 不推荐 - 每次渲染都创建新的watcher function useSomething() { watch(someRef, () => { // 副作用 }) } // 推荐 - 确保watcher只创建一次 function useSomething() { const stop = watch(someRef, () => { // 副作用 }) onUnmounted(stop) }11.3 大型列表优化
对于大型列表,考虑使用虚拟滚动或手动控制响应式:
const list = ref([]) // 批量更新时先取消响应性 const rawList = list.value for (let i = 0; i < 1000; i++) { rawList.push(createItem(i)) } list.value = rawList // 一次性触发更新12. 调试技巧与工具
Composition API的调试与传统Vue有些不同:
12.1 Chrome DevTools扩展
Vue DevTools已经支持Composition API的调试:
- 检查组件实例的setup状态
- 跟踪ref和reactive值的变化
- 查看组件层次结构
12.2 自定义调试Hook
import { onRenderTracked, onRenderTriggered } from 'vue' export function useDebug() { onRenderTracked((event) => { console.log('跟踪到依赖:', event) }) onRenderTriggered((event) => { console.log('触发重新渲染:', event) }) }12.3 日志调试
function useSomething() { const state = reactive({ /* ... */ }) // 添加调试日志 watchEffect(() => { console.log('状态变化:', state) }) return { state } }13. 社区资源与学习路径
要深入学习Composition API,可以参考以下资源:
13.1 官方文档
- Vue 3官方文档
- Composition API RFC
- 迁移指南
13.2 优质教程
- Vue Mastery的Vue 3课程
- 官方示例项目
- 社区博客和技术文章
13.3 开源项目参考
研究使用Composition API的开源项目:
- VueUse - 组合式工具集合
- Nuxt.js - 最新版本已支持Vue 3
- Vuetify - Material组件库的Vue 3版本
14. 未来展望与演进方向
虽然我们已经介绍了Composition API的核心用法,但Vue生态系统仍在不断演进:
- 更好的TypeScript支持:Vue团队持续改进类型推断
- 更多官方组合式函数:如VueUse风格的实用工具
- 性能优化:编译器级别的改进
- 教育资料丰富:更多教程和最佳实践
15. 总结与个人实践心得
通过这5个真实项目案例,我们从不同角度探索了Composition API的强大功能。在实际项目中,我发现以下几点特别有价值:
- 逻辑复用变得简单:不再受限于mixins的命名冲突问题
- 代码组织更灵活:可以按照功能而非选项类型组织代码
- 类型支持更好:与TypeScript配合更加自然
- 测试更简单:组合式函数可以独立于组件进行测试
迁移到Composition API可能需要一些适应时间,但一旦熟悉,你会发现它能让你的Vue代码更加清晰、可维护和可扩展。
