Vue入门到实战(day7):Vuex 与 Vue Router 深度解析,从原理到实战的前端状态与路由管理(附代码案例)
在现代前端开发中,构建复杂单页应用(SPA)离不开高效的状态管理和灵活的路由控制。Vue 生态中的 Vuex 和 Vue Router Router 恰好解决了这两大核心问题。本文将从基础概念出发,通过实例代码详解 Vuex 的状态管理机制和 Vue Router 的路由控制方案,帮助开发者系统性掌握这两个工具的使用技巧。
一、Vuex:Vue 应用的状态管理中心
1.1 什么是 Vuex?
Vuex 是 Vue 官方提供的集中式状态管理模式,它采用单一状态树(Single Source of Truth)思想,将应用中所有组件需要共享的数据集中存储在一个全局的 "仓库(Store)" 中,并提供了一套严格的规则保证状态的变更可预测。
简单来说,Vuex 就是应用中的 "全局数据银行",任何组件都能按照规范的方式存取数据,彻底解决了多层嵌套组件通信、跨组件数据共享的难题。
1.2 为什么需要 Vuex?
在小型应用中,组件间的数据传递可以通过props(父传子)和$emit(子传父)实现,但当应用规模扩大,会面临以下问题:
- 多个组件依赖同一数据(如用户信息、购物车商品)
- 多个组件需要修改同一数据(如多组件操作同一表单)
- 非父子组件(如兄弟组件、跨级组件)通信复杂
此时 Vuex 的优势就会凸显:
- 集中管理共享数据,避免数据流转混乱
- 提供清晰的状态变更路径,便于调试和维护
- 支持时间旅行调试(通过 Vue Devtools 追踪状态变化)
1.3 搭建 Vuex 环境(实战步骤)
步骤 1:创建 Store 核心文件
在项目中新建src/store/index.js,作为 Vuex 的配置中心:
// 引入Vue核心库
import Vue from 'vue'
// 引入Vuex
import Vuex from 'vuex'
// 应用Vuex插件(必须在创建store前执行)
Vue.use(Vuex)// 1. 准备actions:响应组件中的用户动作(处理业务逻辑、异步操作)
const actions = {}// 2. 准备mutations:唯一修改state的地方(必须同步操作)
const mutations = {}// 3. 准备state:存储所有共享数据
const state = {}// 4. 创建并暴露store实例
export default new Vuex.Store({actions,mutations,state
})
步骤 2:在入口文件中注入 Store
修改main.js,将 store 挂载到 Vue 实例,使所有组件可访问:
import Vue from 'vue'
import App from './App.vue'
// 引入store
import store from './store'new Vue({el: '#app',render: h => h(App),store, // 注入storebeforeCreate() {Vue.prototype.$bus = this // 全局事件总线(可选,用于简单通信)}
})
1.4 Vuex 核心工作流程与基本使用
Vuex 严格遵循单向数据流原则,数据流转路径为:组件触发动作 → actions 处理 → mutations 修改 → state 更新 → 组件重新渲染
实战案例:实现计数器功能
// src/store/index.js
const actions = {// 响应组件中的"加"操作(可处理业务逻辑)jia(context, value) {// context是一个迷你store,包含commit、dispatch、state等方法// 可在此处添加业务逻辑(如判断、日志记录等)context.commit('JIA', value) // 调用mutations中的方法},// 处理异步操作(如延迟加)jiaAsync(context, value) {setTimeout(() => {context.commit('JIA', value) // 1秒后提交mutation}, 1000)}
}const mutations = {// 真正修改state的地方(必须同步执行)JIA(state, value) {state.sum += value // 直接修改state中的数据}
}// 初始化共享数据
const state = {sum: 0 // 初始值为0
}export default new Vuex.Store({actions,mutations,state
})
组件中操作 Vuex 数据:
<template><div class="counter"><h3>当前和:{{ $store.state.sum }}</h3><div class="buttons"><!-- 直接调用mutation(无业务逻辑时) --><button @click="$store.commit('JIA', 1)">+1</button><!-- 调用action处理业务逻辑 --><button @click="$store.dispatch('jia', 2)">+2</button><!-- 调用异步action --><button @click="$store.dispatch('jiaAsync', 3)">延迟+3</button></div></div>
</template><style scoped>
.buttons {margin-top: 20px;display: flex;gap: 10px;
}
button {padding: 5px 10px;cursor: pointer;
}
</style>
关键原则:
- 组件中不能直接修改 state(如
this.$store.state.sum++是禁止的) - 必须通过 mutations 修改 state,确保状态变更可追踪
- 异步操作(如接口请求)必须放在 actions 中,再通过 commit 调用 mutations
1.5 Getters:状态的计算属性
当 state 中的数据需要加工后再使用时,可使用 getters(类似组件的计算属性,会缓存结果)。
// src/store/index.js
const getters = {// 计算sum的10倍bigSum(state) {return state.sum * 10},// 带条件的计算(如sum大于10时显示"较大")sumDesc(state) {return state.sum > 10 ? '数值较大' : '数值较小'}
}export default new Vuex.Store({// ...其他配置getters // 添加getters配置
})
组件中访问 getters:
<template><div><p>当前和的10倍:{{ $store.getters.bigSum }}</p><p>描述:{{ $store.getters.sumDesc }}</p></div>
</template>
1.6 四个 map 方法:简化组件代码
Vuex 提供了mapState、mapGetters、mapActions、mapMutations四个辅助函数,用于简化组件中对 Vuex 的操作。
1. mapState:映射 state 到计算属性
<script>
import { mapState } from 'vuex'export default {computed: {// 方式1:对象写法(组件属性名与state属性名不同时)...mapState({ mySum: 'sum', mySchool: 'school',mySubject: 'subject'}),// 方式2:数组写法(组件属性名与state属性名相同时)...mapState(['sum', 'school', 'subject'])}
}
</script>
使用时直接访问映射后的属性:
<template><div><p>{{ mySum }}</p><p>{{ sum }}</p></div>
</template>
2. mapGetters:映射 getters 到计算属性
<script>
import { mapGetters } from 'vuex'export default {computed: {// 对象写法...mapGetters({ myBigSum: 'bigSum' }),// 数组写法...mapGetters(['bigSum', 'sumDesc'])}
}
</script>
3. mapActions:生成与 actions 交互的方法
<script>
import { mapActions } from 'vuex'export default {methods: {// 对象写法(组件方法名映射到actions中的方法)...mapActions({ add: 'jia', addAsync: 'jiaAsync' }),// 数组写法(方法名需与actions中一致)...mapActions(['jia', 'jiaAsync'])}
}
</script>
4. mapMutations:生成与 mutations 交互的方法
<script>
import { mapMutations } from 'vuex'export default {methods: {// 对象写法...mapMutations({ increment: 'JIA' }),// 数组写法...mapMutations(['JIA'])}
}
</script>
使用注意:调用 mapActions 和 mapMutations 生成的方法时,若需要传递参数,需在模板中绑定事件时传入:
<button @click="add(5)">+5</button> <!-- 传递参数5 -->
<button @click="JIA(10)">+10</button>
1.7 模块化与命名空间:大型项目的必备方案
当应用规模扩大,共享数据增多时,单一的 store 会变得臃肿。Vuex 的模块化功能可将 store 拆分为多个子模块,每个模块拥有独立的 state、mutations、actions、getters。
实现步骤:
- 拆分模块并开启命名空间
// src/store/index.js
// 计数器相关模块
const countAbout = {namespaced: true, // 开启命名空间,避免模块间命名冲突state: { sum: 0 },mutations: {JIA(state, value) {state.sum += value}},actions: {jiaAsync(context, value) {setTimeout(() => {context.commit('JIA', value)}, 1000)}},getters: {bigSum(state) {return state.sum * 10}}
}// 人员管理模块
const personAbout = {namespaced: true,state: {list: [{ id: '001', name: '张三' }]},mutations: {ADD_PERSON(state, person) {state.list.unshift(person) // 添加到数组开头}},actions: {addPersonWang(context, person) {if (person.name.indexOf('王') === 0) {context.commit('ADD_PERSON', person)} else {alert('只能添加姓王的人')}}}
}// 组合模块
export default new Vuex.Store({modules: {countAbout, // 注册count模块personAbout // 注册person模块}
})
- 组件中访问模块化数据
<template><div><!-- 1. 访问state --><p>count模块sum:{{ $store.state.countAbout.sum }}</p><p>person模块列表:{{ $store.state.personAbout.list[0].name }}</p><!-- 2. 访问getters --><p>count模块bigSum:{{ $store.getters['countAbout/bigSum'] }}</p><!-- 3. 调用actions --><button @click="$store.dispatch('countAbout/jiaAsync', 1)">+1(异步)</button><button @click="$store.dispatch('personAbout/addPersonWang', {id:'002',name:'王五'})">添加王五</button><!-- 4. 调用mutations --><button @click="$store.commit('countAbout/JIA', 1)">+1</button><button @click="$store.commit('personAbout/ADD_PERSON', {id:'003',name:'赵六'})">添加赵六</button></div>
</template>
- 结合 map 方法访问模块化数据
<script>
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'export default {computed: {// 映射count模块的state...mapState('countAbout', ['sum']),// 映射person模块的state...mapState('personAbout', ['list']),// 映射count模块的getters...mapGetters('countAbout', ['bigSum'])},methods: {// 映射count模块的actions...mapActions('countAbout', ['jiaAsync']),// 映射person模块的actions...mapActions('personAbout', ['addPersonWang']),// 映射count模块的mutations...mapMutations('countAbout', ['JIA']),// 映射person模块的mutations...mapMutations('personAbout', ['ADD_PERSON'])}
}
</script>
二、Vue Router:Vue 应用的路由管理系统
2.1 路由的基本概念
- 路由(Route):路径与组件的映射关系(key-value)
- 路由器(Router):管理多个路由的容器,负责路径匹配和组件切换
- 前端路由:基于 URL 路径实现组件切换,无需刷新页面,是 SPA 的核心特性
前端路由的实现原理:
- 监听 URL 变化(如
hashchange事件或historyAPI) - 根据 URL 匹配对应的组件
- 渲染匹配到的组件(替换页面中的指定区域)
2.2 基本使用步骤(实战)
步骤 1:安装 vue-router
根据 Vue 版本选择对应的 vue-router 版本:
# Vue2项目安装3.x版本
npm i vue-router@3
# Vue3项目安装4.x版本
npm i vue-router@4
步骤 2:创建路由配置文件
新建src/router/index.js:
import Vue from 'vue'
import VueRouter from 'vue-router'
// 引入路由组件(建议放在pages文件夹,区分于普通组件)
import About from '../pages/About'
import Home from '../pages/Home'// 应用路由插件
Vue.use(VueRouter)// 定义路由规则
const routes = [{path: '/about', // 访问路径component: About // 对应组件},{path: '/home',component: Home}
]// 创建路由实例
const router = new VueRouter({routes // 配置路由规则(ES6属性简写)
})// 暴露路由实例
export default router
步骤 3:在入口文件中注入路由
修改main.js:
import Vue from 'vue'
import App from './App.vue'
// 引入路由实例
import router from './router'new Vue({el: '#app',render: h => h(App),router // 注入路由,使所有组件可访问$router和$route
})
步骤 4:使用路由实现导航与组件展示
<template><div id="app"><!-- 路由导航:使用router-link代替a标签 --><div class="nav"><router-link to="/about" active-class="active">关于我们</router-link><router-link to="/home" active-class="active">首页</router-link></div><!-- 组件展示区域:匹配的组件将在这里渲染 --><router-view></router-view></div>
</template><style>
.nav {margin: 20px;
}
/* 路由激活状态样式 */
.active {color: #42b983;text-decoration: none;margin: 0 10px;font-weight: bold;
}
/* 去除默认下划线 */
router-link {text-decoration: none;color: #333;margin: 0 10px;
}
</style>
2.3 路由使用的注意事项
组件分类与存放:
- 路由组件:与路由匹配的组件(如 About、Home),建议放在
pages文件夹 - 普通组件:被路由组件引用的组件(如 Button、Card),建议放在
components文件夹
- 路由组件:与路由匹配的组件(如 About、Home),建议放在
路由组件的生命周期:
- 路由切换时,未显示的路由组件会被销毁(触发
beforeDestroy和destroyed) - 再次访问时,路由组件会重新挂载(触发
beforeCreate、created、mounted)
- 路由切换时,未显示的路由组件会被销毁(触发
路由相关属性:
$route:当前路由信息对象(包含路径、参数、查询等),每个组件独有$router:全局路由实例对象(包含跳转方法),整个应用只有一个
2.4 多级路由(嵌套路由)
实际应用中经常需要多级路由(如/home/news、/home/message),配置方式如下:
// src/router/index.js
import News from '../pages/News'
import Message from '../pages/Message'const routes = [{path: '/home',component: Home,// 子路由配置(children是数组,内部放路由规则)children: [{path: 'news', // 注意:子路由路径不要加斜杠/component: News},{path: 'message', // 完整路径为/home/messagecomponent: Message}]}
]
在父组件中添加子路由导航:
<!-- Home组件 -->
<template><div><h2>首页</h2><div><router-link to="/home/news">新闻</router-link><router-link to="/home/message">消息</router-link></div><router-view></router-view> <!-- 子组件将在这里渲染 --></div>
</template>
关键原则:子路由路径不要加斜杠,跳转时需写完整路径(如/home/news)。
2.5 路由参数传递
路由参数是组件间传递数据的重要方式,Vue Router 支持两种参数传递方式:query 参数和 params 参数。
1. query 参数(URL 参数)
- 特点:参数显式在 URL 中(如
/detail?id=1&title=消息),类似 GET 请求参数 - 适用场景:非敏感数据传递,支持刷新页面保留参数
传递方式:
<!-- 方式1:字符串写法 -->
<router-link to="/home/message/detail?id=1&title=第一条消息">查看详情</router-link><!-- 方式2:对象写法(更灵活) -->
<router-link :to="{path: '/home/message/detail', // 路径query: { // 参数对象id: 1,title: '第一条消息'}}"
>查看详情</router-link>
接收参数:
<!-- Detail组件 -->
<template><div><p>消息ID:{{ $route.query.id }}</p><p>消息标题:{{ $route.query.title }}</p></div>
</template>
2. params 参数(路径参数)
- 特点:参数隐藏在路径中(如
/detail/1/第一条消息),类似 RESTful 风格 - 适用场景:需要参数作为路径一部分的场景(如详情页 ID)
传递方式:
- 先在路由规则中声明 params 参数(使用占位符):
{name: 'xiangqing', // 建议给路由命名path: 'detail/:id/:title', // 用:参数名声明params参数component: Detail
}
- 传递参数:
<!-- 方式1:字符串写法 -->
<router-link to="/home/message/detail/1/第一条消息">查看详情</router-link><!-- 方式2:对象写法(必须用name,不能用path) -->
<router-link :to="{name: 'xiangqing', // 使用路由名称(必须)params: { // 参数对象id: 1,title: '第一条消息'}}"
>查看详情</router-link>
接收参数:
<template><div><p>消息ID:{{ $route.params.id }}</p><p>消息标题:{{ $route.params.title }}</p></div>
</template>
重要区别:params 参数在路由规则中声明后,路径必须包含这些参数,否则会匹配失败;而 query 参数是可选的。
2.6 命名路由:简化路由跳转
给路由设置name属性,可以简化跳转路径的编写,尤其适合多级路由。
// 路由规则中添加name属性
{path: '/home/message/detail',name: 'xiangqing', // 路由名称component: Detail,props: true // 开启props接收参数
}
使用命名路由跳转:
<!-- 简化前:需写完整路径 -->
<router-link to="/home/message/detail">跳转</router-link><!-- 简化后:直接使用name -->
<router-link :to="{name: 'xiangqing'}">跳转</router-link><!-- 配合参数传递 -->
<router-link :to="{name: 'xiangqing',query: { id: 1 },params: { title: '消息' }}"
>跳转</router-link>
2.7 路由的 props 配置:优雅接收参数
通过 props 配置,可以让路由组件更方便地接收参数,避免在组件中直接使用$route(降低耦合)。
{name: 'xiangqing',path: 'detail/:id',component: Detail,// 方式1:对象形式(传递固定值)// props: { a: 100, b: '固定值' }// 方式2:布尔值(自动将params参数转为props)// props: true// 方式3:函数形式(灵活处理参数,支持query和params)props($route) {return {id: $route.params.id,title: $route.query.title,extra: '附加信息' // 可添加额外参数}}
}
组件中通过 props 接收:
<script>
export default {props: ['id', 'title', 'extra'], // 直接声明接收的参数mounted() {console.log(this.id, this.title, this.extra) // 使用参数}
}
</script>
2.8 编程式路由导航:通过代码控制跳转
除了使用<router-link>,还可以通过 JS 代码实现路由跳转,更灵活地控制跳转时机(如点击按钮后验证通过才跳转)。
<template><div><button @click="goDetail">跳转到详情页</button><button @click="goBack">后退</button><button @click="goForward">前进</button></div>
</template><script>
export default {methods: {goDetail() {// 1. push跳转:新增历史记录(可回退)this.$router.push({name: 'xiangqing',params: { id: 1 },query: { title: '消息' }})// 2. replace跳转:替换当前历史记录(不可回退到当前页)this.$router.replace({name: 'xiangqing',params: { id: 1 }})},goBack() {this.$router.back() // 后退一步},goForward() {this.$router.forward() // 前进一步},go(n) {this.$router.go(n) // 前进n步(n为负数则后退,如go(-2)后退两步)}}
}
</script>
2.9 缓存路由组件:保持组件状态
默认情况下,路由切换时组件会被销毁。使用<keep-alive>可以缓存路由组件,使其保持挂载状态(如保留表单输入内容)。
<!-- 缓存指定组件(通过组件名) -->
<keep-alive include="News"><router-view></router-view>
</keep-alive><!-- 缓存多个组件(数组形式) -->
<keep-alive :include="['News', 'Message']"><router-view></router-view>
</keep-alive><!-- 缓存所有组件 -->
<keep-alive><router-view></router-view>
</keep-alive>
注意:被缓存的组件会触发两个特殊的生命周期钩子:
activated:组件被激活(显示)时触发deactivated:组件失活(隐藏)时触发
2.10 路由守卫:控制路由访问权限
路由守卫用于在路由跳转过程中进行拦截和控制(如权限验证、登录判断),分为三类:全局守卫、独享守卫、组件内守卫。
1. 全局守卫
作用于所有路由,在src/router/index.js中配置:
// 全局前置守卫:初始化时执行,每次路由切换前执行
router.beforeEach((to, from, next) => {console.log('全局前置守卫', to, from)// 判断是否需要权限验证(通过路由元信息meta)if (to.meta.isAuth) {// 权限验证逻辑(如判断本地存储中的token)if (localStorage.getItem('token')) {next() // 验证通过,放行} else {alert('请先登录')// next('/login') // 可跳转到登录页}} else {next() // 无需验证,直接放行}
})// 全局后置守卫:初始化时执行,每次路由切换后执行
router.afterEach((to, from) => {console.log('全局后置守卫', to, from)// 修改页面标题(通过路由元信息)document.title = to.meta.title || '默认标题'
})
在路由规则中添加元信息meta:
{path: '/about',component: About,meta: { isAuth: true, // 是否需要权限验证title: '关于我们' // 页面标题}
}
2. 独享守卫
只作用于单个路由,在路由规则中配置:
{path: '/home/news',component: News,meta: { isAuth: true },// 独享守卫(只有前置,无后置)beforeEnter(to, from, next) {console.log('独享守卫', to, from)if (to.meta.isAuth) {if (localStorage.getItem('token')) {next()} else {alert('无权限访问')}} else {next()}}
}
3. 组件内守卫
在路由组件内部定义,作用于当前组件:
<script>
export default {// 进入守卫:通过路由规则进入组件时触发beforeRouteEnter(to, from, next) {console.log('进入组件', to, from)if (to.meta.isAuth) {if (localStorage.getItem('token')) {next()} else {next(false) // 阻止进入}} else {next()}},// 离开守卫:通过路由规则离开组件时触发beforeRouteLeave(to, from, next) {console.log('离开组件', to, from)const confirm = window.confirm('确定要离开吗?未保存的数据会丢失')if (confirm) {next() // 确认离开} else {next(false) // 取消离开}}
}
</script>
2.11 路由器的两种工作模式
Vue Router 支持两种 URL 模式:hash模式和history模式,可在创建路由实例时配置。
1. hash 模式(默认)
- URL 格式:
http://localhost:8080/#/home(包含 #号) - 原理:通过
hashchange事件监听 URL 中 #后的变化 - 特点:
- #后的内容不会发送到服务器,兼容性好(支持所有浏览器)
- URL 中带有 #号,不够美观
- 部分第三方平台分享时可能识别为不合法 URL
2. history 模式
- URL 格式:
http://localhost:8080/home(无 #号) - 原理:使用 HTML5 的
historyAPI(pushState、replaceState) - 特点:
- URL 干净美观,符合常规 URL 规范
- 兼容性稍差(IE10 + 支持)
- 应用部署时需要后端支持(避免刷新页面出现 404)
配置方式:
const router = new VueRouter({mode: 'history', // 默认为hashroutes
})
部署注意:history 模式需要后端配合,在服务器端配置所有路由指向 index.html(如 Nginx 的 try_files 配置),否则刷新页面会出现 404 错误。
三、总结
Vuex 和 Vue Router 是 Vue 生态中构建复杂应用的必备工具:
Vuex通过集中式存储管理应用状态,解决了跨组件数据共享的难题。其核心是
state(数据存储)、mutations(同步修改)、actions(异步处理)、getters(数据加工),配合模块化可应对大型项目的状态管理需求。Vue Router实现了单页应用的路由控制,支持多级路由、参数传递、编程式导航、路由守卫等功能。通过
<router-link>和<router-view>实现组件的按需加载与切换,让 SPA 开发更加高效。
掌握这两个工具的使用,不仅能提升开发效率,更能让代码结构更清晰、可维护性更强。建议在实际项目中多练习,结合 Vue Devtools 调试工具深入理解其工作原理。
