文章目录
- 章节介绍
- 本章学习目标
- 路由的基本搭建与嵌套路由模式
- vue路由的搭建
- 嵌套路由模式
- 动态路由模式与编程式路由模式
- 动态路由模式
- 编程式路由
- 命名路由与命名视图与路由元信息
- 命名路由
- 命名视图
- 路由元信息
- 路由传递参数的多种方式及应用场景
- 路由传参
- 详解route对象与router对象
- route对象与router对象
- 路由守卫详解及应用场景
- 路由守卫分类
- 完整的导航解析流程
- Vuex共享状态的基本开发流程
- Vuex处理异步状态及应用场景
- Vuex计算属性和辅助函数的使用
- getters计算属性
- 辅助函数
- Vuex-persist对数据进行持久化处理
- Vuex分割模块及多状态管理
- 组合式API中使用Router和Vuex注意事项
- Router_Vuex的任务列表综合案例
- 搭建 Vite3 + Pinia2 组合模式
- 总结内容
章节介绍
小伙伴大家好,本章将学习VueRouter路由与Vuex状态管理 - 组织与架构应用。
本章学习目标
本章将学习Vue3中的路由与状态管理,随着前后端分离式开发的兴起,单页面应用开发(即SPA页面)也越来越流行,所以前端路由与共享状态也越来越重要!
路由的基本搭建与嵌套路由模式
在前面的小节中,已经介绍了什么是前端路由以及前端路由所具备的特点。本小节就来对路由进行实际搭建吧。
vue路由的搭建
路由在vue中属于第三方插件,需要下载安装后进行使用。版本说明一下,Vue3搭配的是Vue Router4,目前正常安装的话,就是路由4的版本。如下:
npm install vue-router
安装好后,需要对路由进行配置,并且与Vue进行结合,让路由插件生效。在/src/router/index.js
创建配置文件。
可以通过 createWebHistory()来创建history模式的路由,也可以通过createWebHashHistory()来创建hash模式的路由。那么在浏览器中输入的URL该如何展示对应的组件呢?可以通过这个组件来实现。
除了进行展示外,还可以通过方式进行声明式的路由跳转,代码如下:
<template>
<div>
<router-link to="/">首页</router-link> |
<router-link to="/about">关于</router-link>
<router-view></router-view>
</div>
</template>
嵌套路由模式
往往我们的路由是比较复杂的,需要的层级也比较多,那么就会产生嵌套路由的模式,比如:localhost:8080/about/foo
和localhost:8080/about/bar
import Foo from '@/views/Foo.vue'
import Bar from '@/views/Bar.vue'
const routes = [
{
path: '/about',
component: About,
children: [
{
path: 'foo',
component: Foo
},
{
path: 'bar',
component: Bar
}
]
}
]
可以看到嵌套路由是通过children属性来完成的,那么对于这种嵌套路由写法,我们对应的也要在一级路由对应页面中添加一个,这样才能切换显示二级路由所对应的页面。
动态路由模式与编程式路由模式
动态路由模式
所谓的动态路由其实就是不同的URL可以对应同一个页面,这种动态路由一般URL还是有规律可循的,所以非常适合做类似于详情页的功能。
// router/index.js,定义动态路由
const routes = [
{
path: 'foo/:id',
component: Foo
}
]
// views/Foo.vue,获取动态路由的id
export default {
mounted(){
console.log( this.$route.params.id );
}
}
编程式路由
前面介绍过如何在页面中对路由进行切换,可以采用声明式写法来完成,但是往往这种方式不够灵活,首先不能更灵活的控制结构和样式,其次不能自动进行路由的跳转,必须点击操作才行。
那么编程式路由就会在JavaScript中通过逻辑的方式进行路由跳转,我们把这种写在JS中进行跳转路由的方式叫做编程式路由,这样方式会更加的灵活。
<template>
<button @click="handleToBar">编程式路由跳转</button>
</template>
<script>
export default {
methods: {
handleToBar(){
this.$router.push('/about/bar');
}
}
}
</script>
可以看到在动态路由中使用到了一个route对象,而在编程式路由中使用了一个router对象,那么这两个比较重要的路由对象,会在后面的小节中给大家进行详细的讲解。
命名路由与命名视图与路由元信息
命名路由
在路由跳转中,除了path 之外,你还可以为任何路由提供 name 的形式进行路由跳转。那么name方式的好处如下:
- 没有硬编码的 URL
- params 的自动编码/解码
- 防止你在 url 中出现打字错误
- 绕过路径排序(如显示一个)
// router/index.js,定义命名路由
const routes = [
{
path: '/about/bar',
name: 'bar',
component: Bar
},
{
path: '/about/foo/:id',
name: 'foo',
component: Foo
}
];
<!-- About.vue,使用命名路由跳转 -->
<template>
<div>
<router-link :to="{ name: 'bar' }">bar</router-link>
<router-link :to="{ name: 'foo', params: {id: 123} }">foo 123</router-link>
</div>
</template>
name的方式也支持动态路由形式。
命名视图
有时候想同时 (同级) 展示多个视图,而不是嵌套展示,这个时候就非常适合使用命名视图。
通过components字段配置多个组件,根据不同的router-view去渲染不同的组件。
路由元信息
有时,你可能希望将任意信息附加到路由上,如过渡名称、谁可以访问路由等。这些事情可以通过接收属性对象的meta属性来实现。
const routes = [
{
path: '/about/bar',
name: 'bar',
component: Bar,
meta: { auth: false }
},
{
path: '/about/foo/:id',
name: 'foo',
component: Foo,
meta: { auth: true }
}
];
定义好meta元信息后,可通过route对象去访问meta属性。
<!-- Foo.vue -->
<script>
export default {
mounted(){
this.$route.meta.auth // true
}
}
</script>
路由传递参数的多种方式及应用场景
路由传参
我们经常要在路由跳转的时候传递一些参数,这些参数可以帮助我们完成后续的一些开发和一些处理。路由的传递参数主要有以下三种方式:
- query方式(显示) -> $route.query
- params方式(显示) -> $route.params
两种显示传递数据的差异点主要为,query是携带辅助信息,而params是界面的差异化。
<!-- About.vue -->
<template>
<div>
<router-link :to="{ name: 'bar', query: { username: 'xiaobai' } }">bar</router-link>
<router-link :to="{ name: 'foo', params: { username: 'xiaoqiang' } }">foo</router-link>
</div>
</template>
<!-- Bar.vue -->
<script>
export default {
mounted(){
console.log(this.$route.query);
}
}
</script>
<!-- foo.vue -->
<script>
export default {
mounted(){
console.log(this.$route.params);
}
}
</script>
前两种都是显示传递数据,那么第三种是隐式传递数据,这种方式并不会把数据暴露出来。
<!-- About.vue -->
<template>
<div>
<router-link :to="{ name: 'bar', params: { username: 'xiaoqiang' } }">bar</router-link>
</div>
</template>
<!-- Bar.vue -->
<script>
export default {
mounted(){
console.log(this.$route.params);
}
}
</script>
但是这里需要注意以下,隐式发送过来的数据,只是临时性获取的,一旦刷新页面,隐藏的数据就会消失,所以在使用的时候要额外注意以一下。
详解route对象与router对象
在前面小节中,我们频繁的使用过route对象和router对象,这两个对象在路由中非常的重要,下面我们来详细的学习一下。
route对象与router对象
首先route对象用来获取路由信息,而router对象用来调用路由方法的。具体区别在于,route对象是针对获取操作的,主要是操作当前路由的,而router对象是针对设置操作的,主要是操作整个路由系统对应的功能。
route对象具体功能如下:
- fullPath
- hash
- href
- matched
- meta
- name
- params
- path
- query
主要就是一些路由信息,像常见的动态路由参数,query数据,meta元信息,url路径等等。
router对象具体功能如下:
- addRoute
- afterEach
- back
- beforeEach
- beforeResolve
- currentRoute
- forward
- getRoutes
- go
- hasRoute
- push
- removeRoute
主要就是一些方法,动态改变路由表,路由守卫, 历史记录的前进后退,编程式路由等等。
路由守卫详解及应用场景
正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。这里有很多方式植入路由导航中:全局的,单个路由独享的,或者组件级的。
守卫主要的作用就是在进入到指定路由前做一个拦截,看一下我们是否具备权限,如果有权限就直接进入,如果没权限就跳转到其他页面。
路由守卫分类
一般可以分为三种路由守卫使用的方式:
- 全局环境的守卫
- 路由独享的守卫
- 组件内的守卫
先来看一下如何设置全局的路由守卫,一般在路由配置文件中进行设置。
router.beforeEach((to, from, next)=>{
if(to.meta.auth){
next('/');
}
else{
next();
}
})
其中to
表示需要进入到哪个路由,from
表示从哪个路由离开的,那么next
表示跳转到指定的页面。
有时候我们只是想给某一个指定的路由添加守卫,那么可以选择设置路由独享的守卫方式。
const routes = [
{
name: 'bar',
component: Bar,
beforeEnter(to, from, next){
if(to.meta.auth){
next('/');
}
else{
next();
}
}
}
];
还可以通过在.vue文件中进行路由守卫的设置,代码如下:
<script>
export default {
name: 'FooView',
beforeRouteEnter(to, from, next){
if(to.meta.auth){
next('/');
}
else{
next();
}
}
}
</script>
完整的导航解析流程
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。 - 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫(2.2+)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫(2.5+)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入。
Vuex共享状态的基本开发流程
在上一个小节中,我们介绍了什么是Vuex,本小节我们一起来看一下Vuex共享状态的基本开发流程。首先我们来看一下Vuex的经典流程图。
我们可以看到,基本上就是先准备好一个共享数据state,然后渲染我组件中,通过组件调用dispatch -> actions -> commit -> mutations的方式来进行state修改。
那么这里我们先不考虑dispatch -> actions,因为这两个环节是处理异步程序的,那么我们直接组件去调用commit就可以触发mutations中定义的方法,这样在这个方法中进行state的修改。
首先在/src/store/index.js创建一个状态管理的配置文件,然后在main.js中让vuex于vue进行结合,就像我们路由的使用一样。
// store/index.js
const store = createStore({
state: {
count: 0
},
mutations: {
change(state, payload){
state.count += payload;
}
}
});
// main.js
import store from './store'
app.use(store)
下面看一下如何在页面中显示state数据和如何通过commit修改我们的共享数据,代码如下:
<template>
<div>
<button @click="change(5)">点击</button>
hello home, {{ $store.state.count }}
</div>
</template>
<script>
export default {
name: 'HomeView',
methods: {
handleClick(){
this.$store.commit('change', 5);
}
}
}
</script>
Vuex处理异步状态及应用场景
本小节中将讲解一下Vuex中如何处理异步程序,因为在上一个小节中提到过,mutations中只能处理同步,不能处理异步,所以异步的工作就交给了 actions 来完成。
那么如何触发actions中定义的方法呢,就需要通过dispatch进行触发,具体代码如下:
const store = createStore({
state: {
count: 0
},
actions: {
change(context, payload){
setTimeout(()=>{
context.commit('change', payload)
}, 1000)
}
},
mutations: {
change(state, payload){
state.count += payload;
}
}
});
<script>
export default {
name: 'HomeView',
methods: {
handleClick(){
this.$store.dispatch('change', 5);
}
}
}
</script>
这样在vue devtools插件中就可以更好的观察到异步数据的变化。那么异步处理的应用场景有哪些呢?异步的获取后端的数据,这样可以利用状态管理来充当MVC架构中的C层,不仅衔接前后端,还能对数据进行共享,可以在切换路由的时候做到减少请求次数,而且状态管理配合本地存储进行数据持久化也是非常的方便。
Vuex计算属性和辅助函数的使用
Vuex中除了提供常见的异步处理和同步处理外,还提供了一些辅助的操作方式,比如:状态管理下的计算属性和简化操作的辅助函数。
getters计算属性
在Vuex中通过定义getters字段来实现计算属性,代码如下:
const store = createStore({
state: {
count: 0
}
getters: {
doubleCount(state){
return state.count * 2;
}
}
});
<template>
<div>
{{ count }}, {{ doubleCount }}
</div>
</template>
当count数据发生改变的手,对应的计算属性doubleCount也会跟着发生改变。
辅助函数
在Vuex中为了让我们使用共享数据或调用方法的时候,更加简单易用,提供了对应的辅助函数,分别为:mapState、mapGetters、mapMutations、mapActions。
<template>
<div>
<button @click="change(5)">点击</button>
hello home, {{ count }}, {{ doubleCount }}
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
export default {
name: 'HomeView',
methods: {
...mapActions(['change'])
},
computed: {
...mapState(['count']),
...mapGetters(['doubleCount'])
}
}
</script>
辅助函数最大的优点就是可以处理大量共享数据的需求,这样写起来会非常的简便,因为只需要往数组里添加子项即可。
Vuex-persist对数据进行持久化处理
默认情况下Vuex状态管理是不会对数据进行持久化存储的,也就是说当我们刷新浏览器后,共享状态就会被还原。那么我们想要在刷新的时候能够保持成修改后的数据就需要进行持久化存储,比较常见的持久化存储方案就是利用本地存储来完成,不过自己去实现还是比较不方便的,下面我们通过第三方模块来实现其功能。
模块github地址:https://github.com/championswimmer/vuex-persist。根据地址要求的配置操作如下:
// npm install vuex-persist
import VuexPersistence from 'vuex-persist';
const vuexLocal = new VuexPersistence({
storage: window.localStorage,
reducer: (state) => ({count: state.count})
})
const store = createStore({
state: {
count: 0,
msg: 'hello'
}
plugins: [vuexLocal.plugin]
});
默认情况下,vuex-persist会对所有共享状态进行持久化,那么如果我们只需要对指定的属性进行持久化就需要配置 reducer字段,这个字段可以指定需要持久化的状态。
这样当我们修改了state下的count,那么刷新的时候会不会还原了,并且通过chrome浏览器中Application下的Local Storage进行查看。
Vuex分割模块及多状态管理
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
那么这个时候,所有共享状态的值都在一起,所有的方法也都放在了一起,维护起来非常的不方便。那么Vuex中可利用modules属性来配置模块化的共享状态,那么对于后期维护起来是非常方便的,也利于大型项目的架构。
在/store下创建一个modules文件夹,然后编写一个message.js,代码如下:
const state = {
msg: 'hello'
};
const getters = {
upperMsg(state){
return state.msg.toUpperCase()
}
};
const actions = {};
const mutations = {
change(state, payload){
state.msg = payload;
}
};
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
模块中的选项跟index.js中的选项是一样的,对外提供接口的时候,可以看到一个namespaced字段,这表示当前模块的一个命名空间,主要是为了防止跟其他模块之间产生冲突,在调用的时候需要携带对应的命名空间标识符才行。
再来看一下index.js如何去收集我们的模块,并如何去使用我们的模块。
// store/index.js
import message from '@/store/modules/message'
const store = createStore({
modules: {
message
}
});
<!-- About.vue -->
<template>
<div>
<button @click="change('hi')">点击</button>
hello about, {{ msg }}, {{ upperMsg }}
</div>
</template>
<script>
import { mapState, mapGetters, mapMutations } from 'vuex';
export default {
name: 'AboutView',
methods: {
// handleClick(){
// this.$store.commit('message/change', 'hi');
// }
...mapMutations('message', ['change'])
},
computed: {
// msg(){
// return this.$store.message.msg;
// },
// upperMsg(){
// return this.$store.getters['message/upperMsg']
// }
...mapState('message', ['msg']),
...mapGetters('message', ['upperMsg'])
}
}
</script>
在辅助函数的情况下,也可以进行很好的调用,辅助函数的第一个参数就是命名空间的名字。
组合式API中使用Router和Vuex注意事项
前面介绍的路由和状态管理都是在选项式API中进行使用的,那么路由和状态管理在组合式API中使用的时候,需要注意哪些问题呢?
主要就是路由会提供两个use函数,分别为:useRoute和useRouter;同理状态管理页提供了一个use函数,useStore来操作的。
先来看一下路由相关use函数的使用情况:
<script setup>
import { useRoute, useRouter } from 'vue-router'
const route = useRoute();
const router = useRouter();
console.log( route.meta );
console.log( router.getRoutes() );
</script>
基本上跟选项式API中的用法是一样的,并没有太大的区别。
再来看一下状态管理相关use函数的使用情况:
<script setup>
import { useStore } from 'vuex'
const store = useStore();
console.log( store.state.count );
console.log( store.state.message.msg );
</script>
得到store对象,接下来的操作也是跟选项式API中使用的是一样的。
Router_Vuex的任务列表综合案例
本小节将对本章学习的路由加状态管理做一个综合案例,通过案例来巩固本章所学知识点。
首先先来配置案例中的路由,主要有三个页面,分别对应所有任务,已完成任务,未完成任务。
import { createRouter, createWebHistory } from 'vue-router'
import Todo from '@/views/Todo.vue'
import Complete from '@/views/Complete.vue'
import Incomplete from '@/views/Incomplete.vue'
const routes = [
{
path: '/',
redirect: '/todo'
},
{
path: '/todo',
component: Todo
},
{
path: '/complete',
component: Complete
},
{
path: '/incomplete',
component: Incomplete,
}
];
const router = createRouter({
history: createWebHistory(),
routes
})
export default router;
在来配置一下Vuex状态管理,主要对任务列表进行共享状态处理:
import { createStore } from "vuex";
const store = createStore({
state: {
todoList: [
{
isChecked: true, id: 1, task: '第一个任务'
},
{
isChecked: false, id: 2, task: '第二个任务'
}
]
},
actions: {
},
mutations: {
add(state, payload){
state.todoList.unshift({ isChecked: false, id: state.todoList.length, task: payload });
}
},
getters: {
complete(state){
return state.todoList.filter((v)=> v.isChecked)
},
incomplete(state){
return state.todoList.filter((v)=> !v.isChecked)
}
}
});
export default store;
最后看一下三个页面的基本逻辑处理:
<!-- Todo.vue -->
<template>
<div>
<ul>
<li v-for="item in todoList" :key="item.id" :class="{ through: item.isChecked }">
<input type="checkbox" v-model="item.isChecked"> {{ item.task }}
</li>
</ul>
</div>
</template>
<script setup>
import { computed, defineComponent } from 'vue';
import { useStore } from 'vuex';
defineComponent({
name: 'TodoView'
});
const store = useStore();
const todoList = computed(()=> store.state.todoList)
</script>
<!-- Complete.vue -->
<template>
<div>
<ul>
<li v-for="item in todoList" :key="item.id" :class="{ through: item.isChecked }">
<input type="checkbox" v-model="item.isChecked"> {{ item.task }}
</li>
</ul>
</div>
</template>
<script setup>
import { computed, defineComponent } from 'vue';
import { useStore } from 'vuex';
defineComponent({
name: 'CompleteView'
});
const store = useStore();
const todoList = computed(()=> store.getters.complete)
</script>
<!-- Incomplete.vue -->
<template>
<div>
<ul>
<li v-for="item in todoList" :key="item.id" :class="{ through: item.isChecked }">
<input type="checkbox" v-model="item.isChecked"> {{ item.task }}
</li>
</ul>
</div>
</template>
<script setup>
import { computed, defineComponent } from 'vue';
import { useStore } from 'vuex';
defineComponent({
name: 'IncompleteView'
});
const store = useStore();
const todoList = computed(()=> store.getters.incomplete)
</script>
搭建 Vite3 + Pinia2 组合模式
前面我们介绍过Vite和Pinia,其中Vite是最新的脚手架,基于原生ESM方式;而Pinia则是最新的状态管理,比Vuex使用起来更加简单。
本小节我们将演示Vite如何去搭配Pinia来完成项目的开发。
首先对Vite3脚手架进行初始化的安装。
-
安装脚手架
# npm 6.x npm create vite@latest vite-study # yarn yarn create vite vite-study # pnpm pnpm create vite vite-study
-
选择框架:因为Vite可以和很多框架配合使用,所以这里我们选择:Vite + Vue
? Select a framework: » - Use arrow-keys. Return to submit. Vanilla > Vue React Preact Lit Svelte
-
选择变体:这里先选择自定义形式
? Select a variant: » - Use arrow-keys. Return to submit. JavaScript TypeScript > Customize with create-vue Nuxt
-
选择安装Pinia
? Add Pinia for state management? >> No / Yes Yes
-
进入项目:安装第三方模块,并启动项目
cd vite-study npm install npm run dev VITE v3.1.0 ready in 408 ms ➜ Local: http://127.0.0.1:5173/ ➜ Network: use --host to expose
在安装好Vite后,打开/src/stores可以看到自动安装好了一个示例模块counter.js,代码如下:
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
setTimeout(()=>{
count.value++
}, 2000)
}
return { count, doubleCount, increment }
})
这里的风格可以是上一个小节中介绍的配置写法,也可以利用组合式API风格来编程Pinia。这里做了一个共享状态count,又做了一个计算属性doubleCount,还有一个方法increment。
在共享方法中是不分同步还是异步的,对于vue devtools都是可以很好的进行监控的,所以比Vuex使用起来要简单一些。下面看一下共享状态是如何进行渲染和方法调用的。
<!-- App.vue -->
<script setup>
import { useCounterStore } from './stores/counter';
import { storeToRefs } from 'pinia';
const counterStore = useCounterStore();
const { count, doubleCount } = storeToRefs(counterStore);
const handleClick = () => {
counterStore.increment();
};
</script>
<template>
<header>
<button @click="handleClick">点击</button>
{{ count }}, {{ doubleCount }}
</header>
</template>
Pinia对于模块化的操作方式也比Vuex要简单一些,直接在/stores创建下新创建一个模块JS即可,如:message.js。message.js的代码跟counter.js的代码是一样的格式,使用的时候也是一样的操作行为。
总结内容
- 了解什么是前端路由,以及Vue3中如何使用vue-router路由
- 掌握路由的基本操作,如:编程式路由、动态路由、路由守卫等
- 了解什么是共享状态,以及Vue3中如何使用vuex共享状态
- 掌握vuex的基本操作,如:同步异步方法、模块化、持久化等
- 综合应用以及Vuex下一代版本,Pinia存储库