vue-router + Pinia + Vuex


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

必须通过 commitdispatch,否则 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 老项目 / 迁移中
社区状态 活跃维护 官方主推 维护模式,不再新增功能

五、综合优化建议

  1. 路由设计:

    • 采用目录结构 views/router/modules/ 拆分,大型项目按业务模块分割路由文件。
    • 通用路由(如登录、404)单独存放,动态权限路由单独管理。
    • 善用 meta 存放标题、图标、缓存标记等,避免在组件内硬编码。
  2. 状态管理选择:

    • Vue 3 新项目一律使用 Pinia,代码更简洁,TS 集成更好。
    • Vue 2 维护项目可继续使用 Vuex ,若计划升级 Vue 3,可逐步引入 @vue/composition-api + Pinia。
    • 避免过度使用全局状态:仅跨组件共享的数据放入 Store,局部状态留在组件内。
  3. 持久化策略:

    • 敏感信息(如 token)建议用 sessionStoragehttpOnly cookie,而非持久化到 localStorage
    • 使用插件统一持久化逻辑,避免零散的 localStorage.setItem
    • 注意序列化问题:DateMapSet 等类型需自定义序列化与恢复。
  4. 性能与体验:

    • 路由懒加载 + 代码分割,减少首屏包体积。
    • 大型列表状态使用 shallowRef 或分页,避免 Store 中存储海量数据。
    • 使用 keep-alive 时合理管理 include 数组,防止内存泄漏。
  5. 调试与维护:

    • 始终开启 Vue Devtools(浏览器插件),利用时间旅行、路由状态检查等功能。
    • 在 Pinia 中可添加 $onAction 日志,在 Vuex 中使用 subscribe 记录 mutation 历史。
    • 为路由定义 name 并使用命名跳转,避免硬编码路径字符串。
  6. 避坑清单速查:

    • 路由参数不变 → 用 watchonBeforeRouteUpdate
    • 权限守卫死循环 → 白名单+条件终止
    • 动态路由空白next({ ...to, replace: true })
    • Pinia 解构失效storeToRefs
    • Setup Store 无 $reset → 自定义重置函数
    • SSR 状态污染 → 请求级 Pinia 实例
    • Vuex 严格模式报错 → 使用计算属性包装 v-model
    • Vuex 动态模块未卸载hasModule + unregisterModule

这份指南涵盖了从基础使用到进阶避坑的全部内容,可作为日常开发的参考手册。实际项目中结合这些模式与代码示例,能够显著提升代码质量和开发效率。

相关推荐
小雨下雨的雨1 小时前
家庭药品管理系统智能过期预警鸿蒙PC Electron框架技术深度解析
前端·javascript·人工智能·华为·electron·鸿蒙·鸿蒙系统
ct9782 小时前
vue2 + vue3差异点
前端·javascript·vue.js
小徐_23332 小时前
程序员每天盯屏 10 小时,我开始认真研究“专业编程屏”这件事
前端
悟空瞎说2 小时前
Git 协作工作流详解:从个人单打独斗到规模化团队协同
前端·git
颜进强2 小时前
20-Spec-Kit Tasks 是怎么把技术方案拆成可执行任务的?
前端·后端·ai编程
程序员鱼皮2 小时前
Cursor 零基础实战教程,夯爆了!带你速通 6 大核心能力
前端·后端·ai编程
颜进强2 小时前
14-Spec-Kit、SDD 和 OpenSpec 到底有什么区别?其实核心思想都一样:先写清楚,再让 AI 干活
前端·后端·ai编程
颜进强2 小时前
16-Spec-Kit 是什么?先从整体流程机制讲起
前端·后端·ai编程
悟空瞎说2 小时前
QML 集成 WebView 开发桌面内嵌浏览器实战
前端