在上一篇《若依框架:前端登录组件与图像验证码|用户登录逻辑》中的篇末,对Vuex全局状态管理、Axios二次封装部分介绍的较为粗略,因此就有了这个补充篇。
目录
Vuex全局状态管理
Vuex是什么?
如何理解“状态管理模式”?
Vuex的注册和配置
主模块index.js代码
子模块index.js代码
Vuex状态值的获取
Axios与网络请求
什么是Axios?
Axios的配置规则
Axios二次封装
后端接口状态码定义
前端Axios二次封装
Vuex全局状态管理
Vuex是什么?
Vuex的官网地址为:Vuex 是什么? | Vuex。
Vuex是什么呢?Vuex是一个专门为Vue.js应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。其实它就类似于Java后端框架中的Spring容器的概念,只不过Spring容器是用于专门管理各种Bean实例的,而Vuex则是前端应用中专门用于管理全局共享状态的。
早期的前端Web应用开发,针对那些全局共享的变量,一般会将其挂载到window对象上,因为JavaScript是单线程的,所以也无需担心状态不一致的问题。Vuex可以被视为是用于替代这套原始方案的一种前端框架。在前后端分离模式的Vue前端应用开发过程中,例如:有一个全局共享的User用户信息对象,那么就可以将其存储到Vuex的store中,在需要获取时,直接去调用Vuex暴露出来的commit()/dispatch()方法;在需要获取User用户对象信息时,直接去调用getters方法即可。这是一种可以简化全局状态管理方案的前端应用框架。
如何理解“状态管理模式”?
一个包含状态管理模式的应用可以分为以下三个部分:
- state,驱动应用的数据源;
- view,以声明方式将 state 映射到视图;
- actions,响应在 view 上的用户输入导致的状态变化。
如下图所示,由于Vuex中存储的状态值是响应式的,因此,当state数据源变化时,就会推动Vue组件进行Render重新渲染view视图,以更新页面中的显示的值。
而state数据源的变化,则需要由用户交互事件来进行触发,例如:点击了某个按钮、鼠标划过了某个DOM元素等等,导致在组件内调用Vuex暴露出来的Dispatch()进行异步更新、调用Vuex暴露出来的Commit进行同步更新,最终使得state数据源产生变化。这就是actions的一个触发过程。
而状态管理模式,则是将与state数据源相关的部分进行封装,同时提供了获取和更新状态值的方法,最后将这个对象作为一个单例对象存在于Vue前端应用中。这种状态管理模式的优点在于:
①在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!
②通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。
Vuex的注册和配置
使用Vuex时,需要注意:在一个模块化的打包系统中,您必须显式地通过 Vue.use()
来安装 Vuex。通常地,我们会在项目根目录下创建文件——src/store/index.js,用于对Vuex进行安装和配置操作。示例代码如下,其中:针对Vuex的核心概念——State、Getters、Mutations、Actions、Modules都做了代码注释。
主模块index.js代码
import Vuex from "vuex";
import Vue from "vue";
import user from "./modules/user";
import { getStatus } from "@/apis/home";
//注册Vuex
Vue.use(Vuex);
/**
* 配置全局状态管理模式
* 每一个Vuex应用的核心就是store仓库对象,它本质上就是一个容器,包含着应用中大部分的状态 (state)。
* Vuex 和单纯的全局对象有以下两点不同:
* [1] Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
* [2] 不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。
*/
const store = {
//state:存储响应式的全局状态值
state: {
count: 0,
name: "Vuex",
status: {
code: 200,
msg: "获取成功",
data: [
{
name: "Tom",
age: 18,
address: "London",
},
],
},
},
/**
*
* getters:用于从state中的全局状态值中获取派生数据,像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算
* 有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数
* 每一个getters下的函数,可以接收如下参数:
* 参数1:state,可以通过state参数获取state配置项中的状态值
* 参数2:getters,可以通过getters参数调用getters配置项中的getXXX()方法
*/
getters: {
//判断数据是否获取成功
getStatus(state) {
const { code, data, msg } = state.status;
if (code === 200) {
return data;
} else {
return msg;
}
},
},
/**
* keyPoints:。在 Vuex 中,mutation 都是同步事务,因此,mutation 必须是同步函数
* 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation
* Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。
* 这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
*
* 要唤醒一个 mutation handler,你需要以相应的 type 调用 store.commit 方法,例如:store.commit("setName");
* 并且,可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload),这样可以使用payload载荷来更新目标值,例如:store.commit("setName","Lily"),这样state.name的值就被更新为Lily
*/
mutations: {
setName(state, payload) {
state.name = payload;
},
setCount(state, payload) {
state.count = payload;
},
setStatus(state, payload) {
state.status = payload;
},
},
/**
* Action 类似于 mutation,不同在于:
Action 提交的是 mutation,而不是直接变更状态。
Action 可以包含任意异步操作。
*/
actions: {
//在方法内部异步调用后端接口,来更新status的值
asyncSetStatus({ commit }, payload) {
console.log(`payload=${payload}`);
return new Promise((resolve, reject) => {
getStatus(payload)
.then((result) => {
const { data } = result;
commit("setStatus", data);
resolve(data);
})
.catch((error) => {
reject(error);
});
});
},
},
/**
* Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割
*/
modules:{
user:user,
}
};
export default new Vuex.Store(store);
子模块index.js代码
/**
* Vuex-store子模块定义
*/
const user = {
namespaced: true,//定义独立的命名空间
state:()=>({
name:"user",
}),
getters:{
getName(state){
return state.name;
}
},
mutations:{
setUserName(state,name){
state.name = name;
}
},
actions:{
asyncSetUserName({commit},payload){
commit("setUserName",payload);
}
}
}
export default user;
Vuex状态值的获取
Vuex状态值的获取可以通过Vuex暴露出来的mappers接口来简化代码书写,也可以直接从Vue实例的原型对象上获取。以下是示例代码,以及打印结果,
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
import { mapState,mapGetters, mapMutations, mapActions } from 'vuex';
export default {
name: 'Home',
components: {
HelloWorld
},
computed:{
//计算属性写法
count(){
return this.$store.state.count;
},
name(){
return this.$store.state.name;
},
status(){
return this.$store.state.status;
},
/**
* Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值
* 通过getters暴露出来的store.getters对象获取status对应的派生数据
*/
status_data(){
return this.$store.getters.getStatus;
},
//mapState辅助函数写法
// ...mapState({
// count:state=>state.count,
// name:state=>state.name,
// status:state=>state.status,
// }),
/**
* mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性
*/
...mapGetters({
get_status_data:"getStatus",
get_userName:"user/getName",
})
},
created(){
console.log(this.$store)
console.log(this.name);
console.log(this.status);
console.log(this.status_data)
console.log(this.get_status_data);
console.log(this.get_userName);
//使用mapMutations -更新count的值
this.setCount(15); //也可以直接修改count的值: this.$store.commit("setCount",15);
console.log(this.count);
//修改actions的值
// this.asyncSetStatus(256).then(data=>{
// console.log(data);
// });
this.$store.dispatch("asyncSetStatus",256).then(data=>{
console.log(data);
})
},
methods:{
...mapMutations({
setCount:"setCount",
}),
...mapActions({
asyncSetStatus:"asyncSetStatus"
})
}
}
</script>
Axios与网络请求
什么是Axios?
Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使用原生 node.js http
模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests。
Axios的配置规则
Axios类似于传统项目开发时使用的Ajax的替代品,它也有自己的一套默认配置规则,也支持开发者自定义请求的规则,详情可见Axios官网介绍:
请求配置 | Axios 中文文档 | Axios 中文网https://www.axios-http.cn/docs/req_config
请求配置 | Axios 中文文档 | Axios 中文网https://www.axios-http.cn/docs/req_config
Axios二次封装
在实际开发中,我们较少直接使用原生的Axios(默认配置)去做网络请求操作,而是会对其进行二次封装。像若依前端项目中,就在@/src/utils/request.js脚本中对Axios进行了二次封装。
当然,我们也可以根据实际项目需求,进行自定义的二次封装。
后端接口状态码定义
而谈到Axios的二次封装,通常也离不开后端接口状态码这个话题。因为很多请求的响应结果,在前端进行处理时,是需要根据状态码来判断是否获取到了正确的结果,以此为依据执行不同的逻辑处理流程。
在上一篇文章中有谈到若依后端项目中对于状态码的封装,形成了一个HttpStatus类。方便起见,我们就根据这个类的定义,在下一部分来对Axios进行二次封装。
package com.ruoyi.common.constant;
/**
* 返回状态码
*
* @author ruoyi
*/
public class HttpStatus
{
/**
* 操作成功
*/
public static final int SUCCESS = 200;
/**
* 对象创建成功
*/
public static final int CREATED = 201;
/**
* 请求已经被接受
*/
public static final int ACCEPTED = 202;
/**
* 操作已经执行成功,但是没有返回数据
*/
public static final int NO_CONTENT = 204;
/**
* 资源已被移除
*/
public static final int MOVED_PERM = 301;
/**
* 重定向
*/
public static final int SEE_OTHER = 303;
/**
* 资源没有被修改
*/
public static final int NOT_MODIFIED = 304;
/**
* 参数列表错误(缺少,格式不匹配)
*/
public static final int BAD_REQUEST = 400;
/**
* 未授权
*/
public static final int UNAUTHORIZED = 401;
/**
* 访问受限,授权过期
*/
public static final int FORBIDDEN = 403;
/**
* 资源,服务未找到
*/
public static final int NOT_FOUND = 404;
/**
* 不允许的http方法
*/
public static final int BAD_METHOD = 405;
/**
* 资源冲突,或者资源被锁
*/
public static final int CONFLICT = 409;
/**
* 不支持的数据,媒体类型
*/
public static final int UNSUPPORTED_TYPE = 415;
/**
* 系统内部错误
*/
public static final int ERROR = 500;
/**
* 接口未实现
*/
public static final int NOT_IMPLEMENTED = 501;
/**
* 系统警告消息
*/
public static final int WARN = 601;
}
前端Axios二次封装
对Axios的二次封装流程,通常是包括:①Axios实例的创建;②拦截器配置;③通用网络请求方法的封装;④重复的异步请求的过滤处理。
以下,我们进行Axios的二次封装,实现上述的:①、②、④的需求,示例代码如下,
import axios from "axios";
import { MessageBox, Message, Notification } from "element-ui";
import { getToken, removeToken } from "@/utils/auth";
//请求池-用于存储请求接口-防止重复提交
const pendingRequestPool = new Map();
//间隔时间小于interval的,被视为重复提交的请求
const interval = 1000;
console.log(process.env);
//创建Axios实例
const request = axios.create({
baseURL: process.env.VUE_APP_BASE_URL,
timeout: 1000 * 60, //60s
headers: { "X-Custom-Header": "foobar" },
});
//Axios请求拦截器
request.interceptors.request.use(
(config) => {
//在发送请求之前做些什么
const isToken = Boolean((config.headers || {}).isToken); //根据请求配置是否需要为请求头添加token
const isRepeated = Boolean((config.headers || {}).isRepeated); //根据请求配置判断是否需要防止重复提交请求
let cancelFunction = undefined; //引用取消请求的cancelToken
//挂载请求key
config.requestKey = `${config.method}-${config.url}`;
if (!!isToken && getToken()) {
//添加token到请求头
config.headers["token"] = getToken();
}
if (!!isRepeated) {
//防止重复提交请求
const requestObject = {
url: config.url,
data:
typeof config.data === "object"
? JSON.stringify(config.data)
: config.data,
time: Date.now(),
};
const sessionObejct = pendingRequestPool.get(config.requestKey);
//创建一个 cancel token
config.cancelToken = new axios.CancelToken((cancel) => {
cancelFunction = cancel;
});
if (sessionObejct === undefined || sessionObejct === null) {
//请求池中不存在相同的请求-将其加入请求池
pendingRequestPool.set(config.requestKey, requestObject);
} else {
//请求池中存在相同的请求-根据时间间隔interval判断是否为重复的请求
const { url, data, time } = sessionObejct;
if (Date.now() - time <= interval) {
//重复请求-取消当前请求
cancelFunction();
Promise.reject(new Error(`数据正在处理,请勿重复提交`));
}
}
}
return Promise.resolve(config);
},
(error) => {
//对请求错误做些什么
return Promise.reject(error);
}
);
//Axios响应拦截器
request.interceptors.response.use(
(response) => {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
const requestKey = response.config.requestKey;
pendingRequestPool.delete(requestKey);
console.log(response);
//对不同的状态码进行判断处理
const { code, data, msg } = response.data;
//判断状态码
if (code === 401) {
Message({ message: `登录状态已过期`, type: "error" });
removeToken(); //移除Cookie中的缓存Token信息
return Promise.reject(`无效的会话,会话已过期,请重新登录`);
} else if (code === 500) {
Message({ message: `系统内部错误,请联系管理员!`, type: "error" });
return Promise.reject(new Error(msg));
} else if (code === 601) {
Message({ message: `系统未知警告,请稍后再试!`, type: "warning" });
return Promise.reject(new Error(msg));
} else if (code !== 200) {
Notification.error({ title: "未知错误,请联系管理员!" });
return Promise.reject("error");
} else {
return Promise.resolve(response);
}
},
(error) => {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
}
);
//导出Axios实例
export default request;
测试接口如下,
import request from "@/utils/request";
/**
* 获取状态码status
* @param {*} url
* @param {*} code
* @returns
*/
export const getStatus = (code)=>{
return request({
url:"/app/status",
method:"get",
params:{
code:code,
},
headers:{
isToken:true,
isRepeated:true,
}
});
}
响应结果如下,