Vue 生态核心库实战完全指南
本文将从 vue-router (路由)、Pinia (Vue 3 状态管理)、Vuex(Vue 2 状态管理)三个维度,系统讲解其使用方式、核心 API、持久化存储、开发注意事项与常见坑点,并结合大量代码示例进行详细拆解,最后给出完整对比和优化建议。
一、vue-router 完全指南
1. 基本使用方式
安装与版本对照:
- Vue 3 项目:
npm install vue-router@4 - Vue 2 项目:
npm install vue-router@3
路由配置骨架(Vue 3 示例):
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue') // 懒加载
},
{
path: '/user/:id',
name: 'User',
component: () => import('@/views/User.vue'),
meta: { requiresAuth: true }
},
// 404 通配路由,必须放在最后
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue')
}
]
const router = createRouter({
history: createWebHistory(), // HTML5 模式
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) return savedPosition // 浏览器前进/后退
else return { top: 0 } // 新导航滚动到顶部
}
})
export default router
main.js 挂载:
javascript
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
2. 核心 API 详解
2.1 <router-link> 与 <router-view>
html
<!-- 声明式导航 -->
<router-link to="/user/123">用户</router-link>
<router-link :to="{ name: 'User', params: { id: 123 }}">用户</router-link>
<router-link :to="{ path: '/user', query: { tab: 'profile' }}">查询参数</router-link>
<!-- 自定义激活类名 -->
<router-link to="/" active-class="active">首页</router-link>
<!-- 替换历史记录 -->
<router-link to="/" replace>首页</router-link>
<!-- 视图出口,支持命名视图 -->
<router-view />
<router-view name="sidebar" />
2.2 编程式导航(useRouter / this.$router)
javascript
import { useRouter } from 'vue-router'
const router = useRouter()
router.push('/home') // 字符串路径
router.push({ name: 'User', params: { id: 1 } }) // 命名路由
router.replace('/login') // 替换
router.go(-1) // 前进后退
Vue 2 选项式: 通过 this.$router.push(...)。
2.3 获取路由信息(useRoute / this.$route)
javascript
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.params.id) // 动态路径参数
console.log(route.query.page) // 查询字符串
console.log(route.name) // 路由名称
console.log(route.fullPath) // 完整路径
console.log(route.matched) // 匹配到的嵌套路由记录数组
2.4 导航守卫(重要)
全局前置守卫:
javascript
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
if (to.meta.requiresAuth && !userStore.token) {
next({ name: 'Login', query: { redirect: to.fullPath } })
} else {
next()
}
})
// Vue Router 4 还支持直接返回目标路由
router.beforeEach((to, from) => {
if (!isLoggedIn && to.meta.requiresAuth) return '/login'
})
组件内守卫(Vue 3 组合式):
javascript
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
onBeforeRouteUpdate((to, from) => {
// 同一组件路由参数变化时触发,例如 /user/1 → /user/2
fetchUser(to.params.id)
})
onBeforeRouteLeave((to, from) => {
if (formDirty) {
const answer = window.confirm('未保存更改,确定离开吗?')
if (!answer) return false // 阻止导航
}
})
2.5 动态路由与权限控制
javascript
// 动态添加路由(常用于权限管理)
router.addRoute({
path: '/admin',
component: AdminLayout,
children: [ ... ]
})
// 为已存在的命名路由添加子路由
router.addRoute('AdminLayout', {
path: 'settings',
component: Settings
})
// 完整的动态权限流程
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
if (!userStore.menusLoaded) {
const routes = await userStore.generateRoutes()
routes.forEach(route => router.addRoute(route))
userStore.menusLoaded = true
next({ ...to, replace: true }) // 重新匹配
} else {
next()
}
})
注意: 添加路由后需手动触发重新匹配,且 404 路由必须放在所有动态路由之后,否则新路由会被捕获。
2.6 路由元信息与嵌套
javascript
const routes = [
{
path: '/user',
component: UserLayout,
meta: { title: '用户中心' },
children: [
{
path: 'profile',
component: Profile,
meta: { requiresAuth: true }
}
]
}
]
// 在组件内通过 route.matched 获取所有路径元信息,常用于面包屑
2.7 路由懒加载
javascript
// 动态 import
const About = () => import('@/views/About.vue')
// 带有 webpackChunkName 的魔法注释
const Admin = () => import(/* webpackChunkName: "admin" */ '@/views/Admin.vue')
3. 持久化与历史模式
createWebHistory:HTML5 History API,URL 美观,需后端配置回退。createWebHashHistory:hash 模式(/#/),不需要服务器配置,但 SEO 差。createMemoryHistory:用于 SSR 或非浏览器环境,不会修改 URL。
history 模式 nginx 配置:
nginx
location / {
try_files $uri $uri/ /index.html;
}
4. 开发注意事项与坑点
| 问题 | 原因 | 解决 |
|---|---|---|
| 参数变化组件不更新 | 同一组件被复用 | watch(() => route.params.id, ...) 或 onBeforeRouteUpdate |
| 导航守卫死循环 | 未判断目标路径,反复重定向 | 白名单机制,避免重复跳转登录页 |
重复导航报错 (NavigationDuplicated) |
跳转当前路由 | router.push(...).catch(() => {}) 或重写 push |
| 动态路由后白屏 | 添加路由后未重新匹配 | next({ ...to, replace: true }) |
| 动态路由与 404 冲突 | 404 路由先注册,动态路由被吞 | 404 始终放在最后 |
keep-alive 缓存错乱 |
include 数组与组件名不匹配 |
统一组件 name 与路由 meta 管理 |
v-slot 与过渡动画失效 |
未使用正确的插槽写法 | <router-view v-slot="{ Component }"> 配合 <component :is="Component"> |
路由切换动画示例:
html
<router-view v-slot="{ Component, route }">
<transition :name="route.meta.transition || 'fade'">
<component :is="Component" />
</transition>
</router-view>
二、Pinia 完全指南(Vue 3 官方状态管理)
1. 安装与初始化
bash
npm install pinia
javascript
// main.js
import { createPinia } from 'pinia'
const pinia = createPinia()
app.use(pinia)
2. Store 定义(两种风格)
2.1 Options Store(类 Vuex 模块)
javascript
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
name: '',
token: '',
roles: []
}),
getters: {
isLoggedIn: (state) => !!state.token,
upperName: (state) => state.name.toUpperCase(),
// 使用其他 getter
welcomeMessage() {
return `Hello, ${this.upperName}`
}
},
actions: {
async login(credentials) {
const res = await api.login(credentials)
this.token = res.token
this.name = res.username
},
logout() {
this.$reset() // 内置重置方法
}
}
})
2.2 Setup Store(组合式风格)
javascript
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const totalPrice = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.qty, 0)
)
function addItem(product) {
const existing = items.value.find(i => i.id === product.id)
if (existing) existing.qty++
else items.value.push({ ...product, qty: 1 })
}
function $reset() {
items.value = []
}
return { items, totalPrice, addItem, $reset }
})
3. 组件内使用
vue
<script setup>
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
// 解构 state/getters 必须用 storeToRefs 保持响应性
const { name, token, isLoggedIn } = storeToRefs(userStore)
// actions 可以直接解构
const { login } = userStore
</script>
<template>
<div v-if="isLoggedIn">
{{ name }}
<button @click="userStore.logout">退出</button>
</div>
</template>
4. 修改状态的方式
javascript
// 1. 直接修改
userStore.name = 'New Name'
// 2. $patch 批量修改
userStore.$patch({ name: 'new', token: 'abc' })
// 函数式 patch
userStore.$patch((state) => {
state.items.push(newItem)
})
// 3. 通过 action(推荐,封装业务逻辑)
userStore.login(credentials)
5. 持久化存储
5.1 手动实现($subscribe + localStorage)
javascript
// 在 store 使用的地方订阅
cartStore.$subscribe((mutation, state) => {
localStorage.setItem('cart', JSON.stringify(state.items))
}, { detached: true }) // detached: true 使组件卸载后仍监听
// 初始化时从 localStorage 恢复
const saved = localStorage.getItem('cart')
if (saved) cartStore.items = JSON.parse(saved)
5.2 使用官方推荐插件 pinia-plugin-persistedstate
bash
npm install pinia-plugin-persistedstate
javascript
// main.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
在 Options Store 中启用持久化:
javascript
export const useUserStore = defineStore('user', {
state: () => ({ token: '' }),
persist: {
key: 'my-user', // 存储键名
storage: localStorage, // 也可用 sessionStorage
paths: ['token'] // 指定持久化的字段
}
})
在 Setup Store 中使用(函数式):
javascript
export const useCartStore = defineStore('cart', () => {
const items = ref([])
return { items }
}, {
persist: { storage: localStorage, paths: ['items'] }
})
6. 核心 API 速查
| API | 用途 |
|---|---|
defineStore(id, options) |
定义 store |
store.$patch(obj/function) |
批量修改 state |
store.$reset() |
重置 state 到初始值(仅 Options Store 自带) |
store.$subscribe(cb, { detached }) |
监听 state 变化 |
store.$onAction(cb) |
监听 action 调用与结果 |
storeToRefs(store) |
解构 state/getters 并保持响应 |
createPinia() |
创建 Pinia 实例 |
setActivePinia(pinia) |
在 setup 外激活 Pinia(如路由守卫) |
7. 注意事项与坑点
7.1 解构失去响应
javascript
// ❌ 错误
const { name } = userStore // 失去响应性
// ✅ 正确
const { name } = storeToRefs(userStore)
7.2 Setup Store 没有自动 $reset()
需手动实现一个重置函数并返回,或在 Options Store 中利用内置方法。
7.3 SSR 状态污染
服务端渲染时每个请求必须创建独立的 pinia 实例:
javascript
// 避免模块级单例,采用工厂函数
export function createApp() {
const pinia = createPinia()
const app = createSSRApp(App)
app.use(pinia)
return { app, pinia }
}
7.4 Store 间相互调用(避免循环依赖)
javascript
// 在 action 内部按需引入,而不是在顶层导入
actions: {
async checkout() {
const cartStore = useCartStore()
// ...
}
}
7.5 在组件外使用 Store(如路由守卫)
javascript
// router/index.js
import { useUserStore } from '@/stores/user'
router.beforeEach(() => {
// 因为 Pinia 已经被 app.use,这里可以直接调用
const user = useUserStore()
// ...
})
若在 Pinia 未挂载时(如单元测试),需调用 setActivePinia(createPinia())。
7.6 $subscribe 的内存泄漏
订阅默认与组件生命周期绑定,组件卸载时自动销毁。若希望保持订阅,需传入 { detached: true },并记得手动停止。
javascript
const unsubscribe = cartStore.$subscribe(cb, { detached: true })
// 适时调用 unsubscribe() 清除
三、Vuex 完全指南(Vue 2 状态管理)
1. 安装与项目集成
bash
npm install vuex@3 # Vue 2 专用
javascript
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {},
getters: {},
mutations: {},
actions: {},
modules: {}
})
export default store
javascript
// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
new Vue({ store, render: h => h(App) }).$mount('#app')
2. 五大核心概念与 API
2.1 State
javascript
state: {
count: 0,
user: null
}
// 组件访问
this.$store.state.count
// mapState 辅助函数
import { mapState } from 'vuex'
export default {
computed: {
...mapState(['count', 'user']),
...mapState('userModule', ['name']) // 带命名空间
}
}
2.2 Getters
javascript
getters: {
doubleCount: state => state.count * 2,
// 访问其他 getter
message: (state, getters) => `Count is ${getters.doubleCount}`
}
// 使用
this.$store.getters.doubleCount
// mapGetters
...mapGetters(['doubleCount'])
2.3 Mutations(同步修改 state)
javascript
mutations: {
INCREMENT(state, payload) {
state.count += payload || 1
}
}
// 触发
this.$store.commit('INCREMENT', 5)
// 对象风格
this.$store.commit({ type: 'INCREMENT', amount: 5 })
// mapMutations
methods: {
...mapMutations(['INCREMENT'])
}
2.4 Actions(异步与业务逻辑)
javascript
actions: {
async incrementAsync({ commit }, payload) {
const data = await api.fetchData()
commit('INCREMENT', data.value)
}
}
// 触发
this.$store.dispatch('incrementAsync', 10).then(...)
// mapActions
methods: {
...mapActions(['incrementAsync'])
}
2.5 Modules(模块拆分)
javascript
const moduleA = {
namespaced: true, // 启用命名空间
state: () => ({ list: [] }),
getters: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: { a: moduleA }
})
带命名空间访问:
javascript
this.$store.state.a.list
this.$store.getters['a/filteredList']
this.$store.commit('a/SET_LIST', data)
this.$store.dispatch('a/fetchList')
3. 持久化存储
3.1 手动实现插件
javascript
const persistPlugin = store => {
// 初始化时恢复状态
const saved = localStorage.getItem('vuex-state')
if (saved) {
store.replaceState(Object.assign(store.state, JSON.parse(saved)))
}
// 订阅 mutation,每次变化保存
store.subscribe((mutation, state) => {
localStorage.setItem('vuex-state', JSON.stringify(state))
})
}
// 使用
new Vuex.Store({
plugins: [persistPlugin]
})
3.2 使用第三方库 vuex-persistedstate
bash
npm install vuex-persistedstate
javascript
import createPersistedState from 'vuex-persistedstate'
const store = new Vuex.Store({
plugins: [
createPersistedState({
key: 'my-app',
storage: window.sessionStorage,
paths: ['user.token'] // 只持久化部分
})
]
})
4. 严格模式与表单处理
javascript
const store = new Vuex.Store({
strict: process.env.NODE_ENV !== 'production',
// ...
})
严格模式下,任何非 mutation 修改 state 都会抛出错误。处理 v-model 的正确方式:
javascript
// 使用计算属性包装
computed: {
message: {
get() { return this.$store.state.message },
set(value) { this.$store.commit('UPDATE_MESSAGE', value) }
}
}
5. 注意事项与坑点
5.1 不要直接修改 state
必须通过 commit 或 dispatch,否则 Devtools 无法追踪,严格模式报错。
5.2 模块重用与状态污染
模块 state 必须用函数返回,避免多个实例共享引用。
javascript
// 正确
state: () => ({ items: [] })
// 错误
state: { items: [] }
5.3 动态注册/卸载模块
javascript
// 注册前检查
if (!store.hasModule('myModule')) {
store.registerModule('myModule', myModule)
}
// 组件销毁前卸载
beforeDestroy() {
this.$store.unregisterModule('myModule')
}
5.4 Action 必须返回 Promise
javascript
actions: {
async fetchData({ commit }) {
return api.getData().then(data => {
commit('SET_DATA', data)
return data // 让调用方可以链式处理
})
}
}
5.5 命名空间模块路径冗长
使用 map 辅助函数第一个参数简化:
javascript
...mapState('user', ['name', 'token'])
...mapActions('user', ['login'])
5.6 监控 mutation 进行调试
javascript
store.subscribe((mutation, state) => {
console.log(mutation.type, mutation.payload)
})
四、完整对比与选型总结
| 特性 | Vue Router | Pinia | Vuex |
|---|---|---|---|
| 定位 | 路由导航 | 状态管理(Vue3 官方) | 状态管理(Vue2 官方) |
| 核心概念 | 路由表、守卫、组件导航 | Stores、State、Getters、Actions | State、Getters、Mutations、Actions、Modules |
| API 风格 | 选项式 / 组合式 | 选项式 Store / 组合式 Store | 选项式(辅助函数) |
| 类型支持 | TypeScript 友好 | 极佳的 TS 推导 | 需要额外包装,较弱 |
| 直接修改 State | N/A | ✅ 允许($patch、直接赋值) |
❌ 必须通过 mutation |
| 模块化 | 嵌套路由配置 | 扁平独立 Store | 嵌套模块 + 命名空间 |
| 持久化 | 依赖 History API 或 hash | 插件或 $subscribe |
插件或 subscribe |
| 体积 | ~25KB | ~1.5KB | ~10KB |
| 适用场景 | 全部 Vue 项目 | Vue 3 新项目 | Vue 2 老项目 / 迁移中 |
| 社区状态 | 活跃维护 | 官方主推 | 维护模式,不再新增功能 |
五、综合优化建议
-
路由设计:
- 采用目录结构
views/和router/modules/拆分,大型项目按业务模块分割路由文件。 - 通用路由(如登录、404)单独存放,动态权限路由单独管理。
- 善用
meta存放标题、图标、缓存标记等,避免在组件内硬编码。
- 采用目录结构
-
状态管理选择:
- Vue 3 新项目一律使用 Pinia,代码更简洁,TS 集成更好。
- Vue 2 维护项目可继续使用 Vuex ,若计划升级 Vue 3,可逐步引入
@vue/composition-api+ Pinia。 - 避免过度使用全局状态:仅跨组件共享的数据放入 Store,局部状态留在组件内。
-
持久化策略:
- 敏感信息(如 token)建议用
sessionStorage或httpOnly cookie,而非持久化到localStorage。 - 使用插件统一持久化逻辑,避免零散的
localStorage.setItem。 - 注意序列化问题:
Date、Map、Set等类型需自定义序列化与恢复。
- 敏感信息(如 token)建议用
-
性能与体验:
- 路由懒加载 + 代码分割,减少首屏包体积。
- 大型列表状态使用
shallowRef或分页,避免 Store 中存储海量数据。 - 使用
keep-alive时合理管理include数组,防止内存泄漏。
-
调试与维护:
- 始终开启 Vue Devtools(浏览器插件),利用时间旅行、路由状态检查等功能。
- 在 Pinia 中可添加
$onAction日志,在 Vuex 中使用subscribe记录 mutation 历史。 - 为路由定义
name并使用命名跳转,避免硬编码路径字符串。
-
避坑清单速查:
- 路由参数不变 → 用
watch或onBeforeRouteUpdate - 权限守卫死循环 → 白名单+条件终止
- 动态路由空白 →
next({ ...to, replace: true }) - Pinia 解构失效 →
storeToRefs - Setup Store 无
$reset→ 自定义重置函数 - SSR 状态污染 → 请求级 Pinia 实例
- Vuex 严格模式报错 → 使用计算属性包装
v-model - Vuex 动态模块未卸载 →
hasModule+unregisterModule
- 路由参数不变 → 用
这份指南涵盖了从基础使用到进阶避坑的全部内容,可作为日常开发的参考手册。实际项目中结合这些模式与代码示例,能够显著提升代码质量和开发效率。