如果你正在从 Vue2 转向 Vue3,或者已经在 Vue3 项目中摸爬滚打了一段时间,却总感觉对 Composition API、Pinia、Router 这些核心概念的理解停留在表面——这篇文章就是为你准备的。
很多开发者以为 Vue3 只是"语法变了",但实际上这是一次开发范式的彻底升级。Composition API 不只是替代 Options API 的另一种写法,它改变了我们组织代码逻辑的方式;Pinia 也不仅仅是 Vuex 的替代品,它提供了更直观的状态管理体验;而 Vue Router 在 Vue3 中的变化,更是直接影响着页面性能和用户体验。
本文将带你深入 Vue3 的核心三件套,通过实际案例展示如何避免常见的坑点,构建可维护的大型应用。无论你是准备面试还是提升项目质量,这里都有你需要的实战经验。
1. 为什么 Vue3 值得深入学习?
Vue3 自 2020 正式发布以来,已经成为了前端开发的主流选择。但很多开发者对其认知仍停留在"性能更好"的层面,实际上 Vue3 带来的改变远不止于此。
性能提升只是表象,开发体验的革新才是核心。Composition API 让逻辑关注点更加集中,而不是分散在各个选项中。想象一下,在 Vue2 中处理一个用户认证逻辑,你需要在 data 中定义状态,在 methods 中定义方法,在 computed 中定义计算属性,在 mounted 中处理初始化。而在 Vue3 中,所有相关的逻辑可以组织在同一个函数中,大大提升了代码的可读性和可维护性。
TypeScript 支持从"可用"变成了"优秀"。Vue3 的源码完全用 TypeScript 重写,提供了更完善的类型推断。这意味着你在开发时能获得更好的智能提示和类型检查,减少运行时错误。
生态系统已经成熟。Pinia 作为官方推荐的状态管理库,相比 Vuex 有更简单的 API 和更好的 TypeScript 支持。Vue Router 4 也针对 Vue3 进行了优化,提供了更好的组合式 API 集成。
如果你还在犹豫是否要深入学习 Vue3,现在就是最佳时机——市场需求已经从"会 Vue2"转向"精通 Vue3",而真正理解其核心原理的开发者仍然稀缺。
2. Composition API 深度解析
2.1 从 Options API 到 Composition API 的思维转变
很多开发者刚开始接触 Composition API 时,容易陷入一个误区:认为这只是另一种组织代码的方式。实际上,这是从"选项式思维"到"函数式思维"的转变。
在 Options API 中,代码按照选项类型分组:
<script> export default { data() { return { count: 0, user: null } }, computed: { doubleCount() { return this.count * 2 } }, methods: { increment() { this.count++ } }, mounted() { this.fetchUser() } } </script>在 Composition API 中,代码按照逻辑关注点组织:
<script setup> import { ref, computed, onMounted } from 'vue' // 计数器逻辑 const count = ref(0) const doubleCount = computed(() => count.value * 2) const increment = () => count.value++ // 用户逻辑 const user = ref(null) const fetchUser = async () => { user.value = await fetch('/api/user').then(r => r.json()) } onMounted(() => { fetchUser() }) </script>这种组织方式让相关的逻辑聚集在一起,当组件复杂时,你甚至可以提取自定义组合式函数:
// composables/useCounter.js export function useCounter(initialValue = 0) { const count = ref(initialValue) const doubleCount = computed(() => count.value * 2) const increment = () => count.value++ return { count, doubleCount, increment } } // composables/useUser.js export function useUser() { const user = ref(null) const fetchUser = async (id) => { user.value = await fetch(`/api/users/${id}`).then(r => r.json()) } return { user, fetchUser } }2.2 ref 和 reactive 的正确使用场景
这是 Composition API 中最容易混淆的概念之一。简单来说:
ref用于基本类型(string, number, boolean)和对象引用reactive仅用于对象
但实际选择时,有更实用的判断标准:
import { ref, reactive } from 'vue' // 适合用 ref 的情况: const count = ref(0) // 基本类型 const user = ref(null) // 可能为 null 的对象引用 const loading = ref(false) // 布尔值 // 适合用 reactive 的情况: const form = reactive({ name: '', email: '', age: 0 }) // 总是存在的表单对象 const config = reactive({ apiUrl: 'https://api.example.com', timeout: 5000 }) // 配置对象关键区别:ref需要通过.value访问值,而reactive可以直接访问属性。在模板中,ref 会自动解包,所以不需要写.value。
2.3 生命周期钩子的变化
Vue3 的生命周期钩子都带有on前缀,并且可以在 setup 中多次调用:
<script setup> import { onMounted, onUpdated, onUnmounted } from 'vue' onMounted(() => { console.log('第一个 mounted 钩子') // 初始化操作 }) onMounted(() => { console.log('第二个 mounted 钩子') // 其他初始化操作 }) onUpdated(() => { console.log('组件更新了') }) onUnmounted(() => { console.log('组件卸载前的清理工作') }) </script>这种设计让相关的生命周期逻辑可以组织在一起,而不是分散在组件选项中。
3. Pinia 状态管理实战
3.1 Pinia 与 Vuex 的核心差异
Pinia 不是 Vuex 5,而是一个全新的状态管理库。其主要优势在于:
- 更简单的 API:没有 mutations,只有 state、getters、actions
- 完美的 TypeScript 支持:完全的类型推断
- 模块化设计:每个 store 都是自动分割的
- 组合式 API 友好:与 Vue3 的 Composition API 完美结合
3.2 定义 Store 的两种方式
根据 Pinia 官方文档,定义 store 有两种方式:Option Store 和 Setup Store。
Option Store(类似 Vuex):
// stores/counter.js import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', { state: () => ({ count: 0, name: '计数器' }), getters: { doubleCount: (state) => state.count * 2, // 使用其他 getters doubleCountPlusOne() { return this.doubleCount + 1 } }, actions: { increment() { this.count++ }, async fetchData() { const data = await fetch('/api/data') this.count = await data.json() } } })Setup Store(类似 Composition API):
// stores/counter.js import { defineStore } from 'pinia' import { ref, computed } from 'vue' export const useCounterStore = defineStore('counter', () => { const count = ref(0) const name = ref('计数器') const doubleCount = computed(() => count.value * 2) const doubleCountPlusOne = computed(() => doubleCount.value + 1) function increment() { count.value++ } async function fetchData() { const data = await fetch('/api/data') count.value = await data.json() } return { count, name, doubleCount, doubleCountPlusOne, increment, fetchData } })选择建议:如果你来自 Vuex 背景,Option Store 会更熟悉;如果你习惯 Composition API,Setup Store 会更自然。两者在功能上是等价的。
3.3 在组件中使用 Store
<script setup> import { useCounterStore } from '@/stores/counter' import { storeToRefs } from 'pinia' const counterStore = useCounterStore() // 直接访问 state console.log(counterStore.count) // 调用 action counterStore.increment() // 使用 getter console.log(counterStore.doubleCount) // 如果需要解构并保持响应性,使用 storeToRefs const { count, doubleCount } = storeToRefs(counterStore) // actions 可以直接解构,因为它们绑定到 store const { increment } = counterStore </script> <template> <div> <p>计数: {{ counterStore.count }}</p> <p>双倍计数: {{ counterStore.doubleCount }}</p> <button @click="counterStore.increment">增加</button> </div> </template>3.4 持久化状态配置
在实际项目中,我们经常需要持久化某些状态(如用户登录信息)。推荐使用pinia-plugin-persistedstate:
npm install pinia-plugin-persistedstate// main.js import { createApp } from 'vue' import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' const app = createApp(App) const pinia = createPinia() pinia.use(piniaPluginPersistedstate) app.use(pinia)在 store 中配置持久化:
export const useUserStore = defineStore('user', { state: () => ({ token: '', userInfo: null }), persist: { enabled: true, strategies: [ { key: 'user', storage: localStorage, paths: ['token'] // 只持久化 token } ] } })4. Vue Router 4 路由管理
4.1 路由定义与导航守卫
Vue Router 4 最大的变化是创建路由实例的方式:
// router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', name: 'Home', component: () => import('@/views/Home.vue'), meta: { requiresAuth: true } }, { path: '/login', name: 'Login', component: () => import('@/views/Login.vue') }, { path: '/user/:id', name: 'User', component: () => import('@/views/User.vue'), props: true // 将路由参数作为 props 传递 } ] const router = createRouter({ history: createWebHistory(), routes }) // 全局前置守卫 router.beforeEach((to, from) => { const userStore = useUserStore() if (to.meta.requiresAuth && !userStore.token) { return { name: 'Login' } } }) export default router4.2 组合式 API 中使用路由
在 Composition API 中,我们使用useRouter和useRoute:
<script setup> import { useRouter, useRoute } from 'vue-router' const router = useRouter() const route = useRoute() // 编程式导航 const goToUser = (userId) => { router.push({ name: 'User', params: { id: userId } }) } // 获取当前路由参数 const userId = computed(() => route.params.id) // 监听路由变化 watch( () => route.params.id, (newId) => { fetchUser(newId) } ) </script>4.3 解决路由参数变化页面不刷新的问题
这是 Vue Router 常见的痛点:当从/user/1导航到/user/2时,组件不会重新创建。
解决方案:
<script setup> import { watch } from 'vue' import { useRoute } from 'vue-router' const route = useRoute() const userData = ref(null) // 方法1:监听 params 变化 watch( () => route.params.id, async (newId) => { userData.value = await fetchUser(newId) }, { immediate: true } // 立即执行一次 ) // 方法2:使用 onBeforeRouteUpdate 导航守卫 import { onBeforeRouteUpdate } from 'vue-router' onBeforeRouteUpdate(async (to, from) => { if (to.params.id !== from.params.id) { userData.value = await fetchUser(to.params.id) } }) </script> <template> <!-- 方法3:为 router-view 添加 key --> <router-view :key="route.fullPath" /> </template>5. 项目实战:构建用户管理系统
让我们通过一个完整的用户管理系统,将所学知识整合起来。
5.1 项目结构设计
src/ ├── components/ # 可复用组件 │ ├── UserForm.vue │ └── UserList.vue ├── composables/ # 组合式函数 │ ├── useApi.js │ └── usePagination.js ├── stores/ # Pinia stores │ ├── user.js │ └── app.js ├── router/ # 路由配置 │ └── index.js ├── views/ # 页面组件 │ ├── Home.vue │ ├── Login.vue │ └── UserManagement.vue └── App.vue5.2 用户状态管理 Store
// stores/user.js import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token') || '', currentUser: null, users: [] }), getters: { isLoggedIn: (state) => !!state.token, getUserById: (state) => (id) => { return state.users.find(user => user.id === id) } }, actions: { setToken(token) { this.token = token localStorage.setItem('token', token) }, clearToken() { this.token = '' localStorage.removeItem('token') }, async login(credentials) { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) }) if (response.ok) { const { token, user } = await response.json() this.setToken(token) this.currentUser = user return true } return false }, async logout() { this.clearToken() this.currentUser = null this.users = [] }, async fetchUsers() { const response = await fetch('/api/users', { headers: { 'Authorization': `Bearer ${this.token}` } }) if (response.ok) { this.users = await response.json() } } }, persist: { enabled: true, strategies: [ { key: 'user', storage: localStorage, paths: ['token'] } ] } })5.3 路由配置与权限控制
// router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', name: 'Home', component: () => import('@/views/Home.vue'), meta: { requiresAuth: true } }, { path: '/login', name: 'Login', component: () => import('@/views/Login.vue'), meta: { guestOnly: true } }, { path: '/users', name: 'UserManagement', component: () => import('@/views/UserManagement.vue'), meta: { requiresAuth: true, requiresAdmin: true } }, { path: '/users/:id', name: 'UserDetail', component: () => import('@/views/UserDetail.vue'), meta: { requiresAuth: true }, props: true } ] const router = createRouter({ history: createWebHistory(), routes }) router.beforeEach((to, from) => { const userStore = useUserStore() // 检查是否需要认证 if (to.meta.requiresAuth && !userStore.isLoggedIn) { return { name: 'Login', query: { redirect: to.fullPath } } } // 检查是否只允许未登录用户访问 if (to.meta.guestOnly && userStore.isLoggedIn) { return { name: 'Home' } } // 检查管理员权限 if (to.meta.requiresAdmin && (!userStore.currentUser || !userStore.currentUser.isAdmin)) { return { name: 'Home' } } }) export default router5.4 用户管理页面实现
<!-- views/UserManagement.vue --> <script setup> import { onMounted, ref, computed } from 'vue' import { useUserStore } from '@/stores/user' import { usePagination } from '@/composables/usePagination' const userStore = useUserStore() const searchQuery = ref('') // 使用组合式函数处理分页 const { currentPage, pageSize, totalPages, paginatedData } = usePagination( computed(() => userStore.users), 10 ) // 搜索过滤 const filteredUsers = computed(() => { if (!searchQuery.value) return userStore.users return userStore.users.filter(user => user.name.toLowerCase().includes(searchQuery.value.toLowerCase()) || user.email.toLowerCase().includes(searchQuery.value.toLowerCase()) ) }) onMounted(async () => { if (userStore.users.length === 0) { await userStore.fetchUsers() } }) const deleteUser = async (userId) => { if (confirm('确定要删除这个用户吗?')) { await fetch(`/api/users/${userId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${userStore.token}` } }) await userStore.fetchUsers() // 重新加载用户列表 } } </script> <template> <div class="user-management"> <h1>用户管理</h1> <div class="toolbar"> <input v-model="searchQuery" placeholder="搜索用户..." class="search-input" > <button @click="$router.push('/users/new')" class="btn-primary"> 添加用户 </button> </div> <table class="user-table"> <thead> <tr> <th>ID</th> <th>姓名</th> <th>邮箱</th> <th>操作</th> </tr> </thead> <tbody> <tr v-for="user in paginatedData" :key="user.id"> <td>{{ user.id }}</td> <td>{{ user.name }}</td> <td>{{ user.email }}</td> <td> <button @click="$router.push(`/users/${user.id}`)">编辑</button> <button @click="deleteUser(user.id)" class="btn-danger">删除</button> </td> </tr> </tbody> </table> <!-- 分页控件 --> <div class="pagination"> <button :disabled="currentPage === 1" @click="currentPage--" > 上一页 </button> <span>第 {{ currentPage }} 页,共 {{ totalPages }} 页</span> <button :disabled="currentPage >= totalPages" @click="currentPage++" > 下一页 </button> </div> </div> </template>6. 性能优化与最佳实践
6.1 组件懒加载
对于大型应用,使用路由懒加载可以显著提升初始加载速度:
// router/index.js const routes = [ { path: '/admin', name: 'Admin', component: () => import(/* webpackChunkName: "admin" */ '@/views/Admin.vue') } ]6.2 状态管理优化
避免在 store 中存储过大或不需要响应式的数据:
// 不推荐:存储大量不需要响应式的数据 state: () => ({ largeData: [] // 数千条数据,会影响性能 }) // 推荐:使用 shallowRef 或标记为非响应式 import { shallowRef } from 'vue' export const useDataStore = defineStore('data', () => { const largeData = shallowRef([]) // 浅层响应式 return { largeData } })6.3 计算属性缓存与记忆化
合理使用计算属性避免重复计算:
const expensiveValue = computed(() => { // 复杂的计算逻辑 return heavyCalculation(someData.value) }) // 对于依赖多个参数的计算,使用记忆化 import { computed } from 'vue' function useMemoizedComputed(getter, deps) { return computed(() => { // 读取依赖项以确保响应性 deps.forEach(dep => dep.value) return getter() }) }7. 常见问题与解决方案
7.1 响应式丢失问题
// ❌ 错误:直接解构会丢失响应性 const { count, name } = useCounterStore() // ✅ 正确:使用 storeToRefs import { storeToRefs } from 'pinia' const { count, name } = storeToRefs(useCounterStore()) // ✅ 正确:在模板中直接访问 const store = useCounterStore() // 在模板中:{{ store.count }}7.2 循环依赖问题
当 store 之间需要相互引用时:
// stores/user.js export const useUserStore = defineStore('user', { actions: { async someAction() { // 在函数内引用其他 store,避免循环导入 const { useProductStore } = await import('@/stores/product') const productStore = useProductStore() // ... 使用 productStore } } })7.3 TypeScript 类型定义
为 store 提供完整的类型支持:
// types/user.ts export interface User { id: number name: string email: string isAdmin: boolean } // stores/user.ts import { defineStore } from 'pinia' import type { User } from '@/types/user' interface UserState { token: string currentUser: User | null users: User[] } export const useUserStore = defineStore('user', { state: (): UserState => ({ token: '', currentUser: null, users: [] }), getters: { isLoggedIn: (state) => !!state.token, getUserById: (state) => (id: number) => { return state.users.find(user => user.id === id) } } })8. 从 Vue2 迁移的实用技巧
8.1 渐进式迁移策略
- 混合使用:在 Vue3 项目中可以使用
@vue/composition-api插件逐步迁移 - 按组件迁移:逐个组件重写,而不是一次性重写整个项目
- 工具辅助:使用 vue-composition-api-migration-helper 工具
8.2 API 映射指南
| Vue2 Options API | Vue3 Composition API |
|---|---|
data() | ref()或reactive() |
methods | 普通函数 |
computed | computed() |
watch | watch()或watchEffect() |
mounted等生命周期 | onMounted()等 |
8.3 常见迁移陷阱
- this 上下文变化:Composition API 中不再使用 this
- 生命周期钩子名称变化:添加
on前缀,如mounted→onMounted - 事件总线模式变化:推荐使用
mitt或provide/inject替代$on/$emit
通过系统学习 Composition API、Pinia 和 Vue Router 的深度集成,你不仅能够构建更复杂的 Vue3 应用,还能在面试和团队协作中展现出对现代前端开发的深刻理解。真正的进阶不在于记住所有 API,而在于理解其设计哲学并能在实际项目中做出合理的技术决策。