问题背景
在新项目开始时,axios的封装是必须的,这里就总结回顾一下axios都需要进行哪些封装把
基础配置(参考vue-element-admin,可直接使用)
1.请求自动携带token
2.统一处理错误情况
3.默认去除response的包装,只返回data。通过meta的responseAll配置为true获取所有的response
其中请求拦截器的逻辑为:如果用户登陆了有token,则在请求头上携带token
其中响应拦截器的逻辑为:
返回的code是否为200
是:根据配置返回全部的res或者直接返回data
否:message提示用户,抛出异常
并且同时判断是否为token有问题的情况
有问题:让用户确认是否退出
确认退出:调用vuex退出系统的方法并重新加载一下login页面
import axios from 'axios'import { Message } from 'element-ui'import store from '@/store'import { getToken } from '@/utils/auth'// 创建axios实例const service = axios.create({baseURL: process.env.BASE_API, // api的base_urltimeout: 5000, // 请求超时时间})// request拦截器service.interceptors.request.use(config => {// 如果登录了,有token,则请求携带token// Do something before request is sentif (store.state.userInfo.token) {config.headers['X-Token'] = getToken() // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改}return config},error => {// Do something with request errorconsole.log(error) // for debugPromise.reject(error)})// respone拦截器service.interceptors.response.use(// response => response,/** * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页 * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中 */response => {const res = response.data// 处理异常的情况if (res.code !== 200) {Message({message: res.message,type: 'error',duration: 5 * 1000,})// 403:非法的token; 50012:其他客户端登录了;401:Token 过期了;if (res.code === 403 || res.code === 50012 || res.code === 401) {MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {confirmButtonText: '重新登录',cancelButtonText: '取消',type: 'warning',}).then(() => {store.dispatch('FedLogOut').then(() => {location.reload() // 为了重新实例化vue-router对象 避免bug})})}return Promise.reject('error')} else {// 默认只返回data,不返回状态码和message// 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data)const isbackAll = response.config.meta && response.config.meta.responseAllif(isbackAll){return res}else{return res.data}}},error => {console.log('err' + error) // for debugMessage({message: error.message,type: 'error',duration: 5 * 1000,})return Promise.reject(error)})export default service
如果不想配置过多的axios,那么上面的代码已经可以满足需求了。搭配使用方式为:
优化一:取消重复请求
参考地址:https://juejin.cn/post/6968630178163458084#heading-7
发生重复请求的场景一般有这两个(主要还是tab的切换会导致数据错乱):
- 对于列表数据,可能有tab状态栏的频繁切换查询,如果请求响应很慢,也会产生重复请求。当然现在很多列表都会做缓存,如Vue中用
- 快速连续点击一个按钮,如果这个按钮未进行控制,就会发出重复请求,假设该请求是生成订单,那么就有产生两张订单了,这是件可怕的事情。当然一般前端会对这个按钮进行状态处理控制,后端也会有一些幂等控制处理策略啥的,这是个假设场景,但也可能会发生的场景。
实现思路:
我们大致整体思路就是收集正在请求中的接口,也就是接口状态还是pending状态的,让他们形成队列储存起来。如果相同接口再次被触发,则直接取消正在请求中的接口并从队列中删除,再重新发起请求并储存进队列中;如果接口返回结果,就从队列中删除,以此过程来操作。
效果:同时发送三个请求,出现这个canceled就是成功的把前两次请求给取消了
需要用到的几个函数
// axios.jsconst pendingMap = new Map();/** * 生成每个请求唯一的键 * @param {*} config* @returns string */function getPendingKey(config) {let {url, method, params, data} = config;if(typeof data === 'string') data = JSON.parse(data); // response里面返回的config.data是个字符串对象return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&');}/** * 储存每个请求唯一值, 也就是cancel()方法, 用于取消请求 * @param {*} config*/function addPending(config) {const pendingKey = getPendingKey(config);config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {if (!pendingMap.has(pendingKey)) {pendingMap.set(pendingKey, cancel);}});}/** * 删除重复的请求 * @param {*} config*/function removePending(config) {const pendingKey = getPendingKey(config);if (pendingMap.has(pendingKey)) { const cancelToken = pendingMap.get(pendingKey); cancelToken(pendingKey); pendingMap.delete(pendingKey);}}
axios简化版代码(方便看上面方法加入的位置):
// axios.jsconst service = axios.create({baseURL: 'http://localhost:8888', // 设置统一的请求前缀timeout: 10000, // 设置统一的超时时长});service.interceptors.request.use(config => {// 删除重复的请求removePending(config);// 如果repeatRequest不配置,那么该请求则不能多次请求!config.repeatRequest && addPending(config)return config;}, error => {return Promise.reject(error);});service.interceptors.response.use(response => {// 删除重复的请求removePending(response.config);return response;},error => {// 删除重复的请求error.config && removePending(error.config);return Promise.reject(error);});
加入到基础配置后的代码:
import axios from 'axios'import { Message } from 'element-ui'import store from '@/store'import { getToken } from '@/utils/auth'// 创建axios实例const service = axios.create({baseURL: process.env.BASE_API, // api的base_urltimeout: 5000, // 请求超时时间})// request拦截器service.interceptors.request.use(config => {removePending(config)// 如果repeatRequest不配置,那么默认该请求就取消重复接口请求!config.repeatRequest && addPending(config)// 如果登录了,有token,则请求携带token// Do something before request is sentif (store.state.userInfo.token) {config.headers['X-Token'] = getToken() // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改}return config},error => {// Do something with request errorconsole.log(error) // for debugPromise.reject(error)})// respone拦截器service.interceptors.response.use(// response => response,/** * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页 * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中 */response => {// 已完成请求的删除请求中数组removePending(response.config);const res = response.data// 处理异常的情况if (res.code !== 200) {Message({message: res.message,type: 'error',duration: 5 * 1000,})// 403:非法的token; 50012:其他客户端登录了;401:Token 过期了;if (res.code === 403 || res.code === 50012 || res.code === 401) {MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {confirmButtonText: '重新登录',cancelButtonText: '取消',type: 'warning',}).then(() => {store.dispatch('FedLogOut').then(() => {location.reload() // 为了重新实例化vue-router对象 避免bug})})}return Promise.reject('error')} else {// 默认只返回data,不返回状态码和message// 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data)const isbackAll = response.config.meta && response.config.meta.responseAllif(isbackAll){return res}else{return res.data}}},error => {error.config && removePending(error.config)console.log('err' + error) // for debugMessage({message: error.message,type: 'error',duration: 5 * 1000,})return Promise.reject(error)})// axios.jsconst pendingMap = new Map();/** * 生成每个请求唯一的键 * @param {*} config* @returns string */function getPendingKey(config) {let {url, method, params, data} = config;if(typeof data === 'string') data = JSON.parse(data); // response里面返回的config.data是个字符串对象return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&');}/** * 储存每个请求唯一值, 也就是cancel()方法, 用于取消请求 * @param {*} config*/function addPending(config) {const pendingKey = getPendingKey(config);config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {if (!pendingMap.has(pendingKey)) {pendingMap.set(pendingKey, cancel);}});}/** * 删除重复的请求 * @param {*} config*/function removePending(config) {const pendingKey = getPendingKey(config);if (pendingMap.has(pendingKey)) { const cancelToken = pendingMap.get(pendingKey); cancelToken(pendingKey); pendingMap.delete(pendingKey);}}export default service
搭配使用api使用,比如某个请求是可以同时进行请求的:
提问:取消了接口的重复请求,还有必要做按钮的防抖节流嘛?
回:不一定,取消重复请求只是在前端过滤了这个请求,后端还是收到了2份请求的,如果是表单类的提交,那数据库还是会生成2条数据的
优化二:配置loading
参考:https://blog.csdn.net/weixin_43239880/article/details/121688263?spm=1001.2014.3001.5501
为什么需要在axios中配置loading?
原因:每次发请求都需要去配置一个值,然后请求开始将该值设为true,请求完毕设为false。感觉很麻烦
效果:发送请求时加入配置:可以让任意一个盒子loading
const res3 = await getListById({ loading: true, loadingDom: ".bg3" })
需要用到的函数
const LoadingInstance = {_target: null, // 保存Loading实例_count: 0, // 计算数量,保证一次只有一个loading}function openLoading(loadingDom) {LoadingInstance._target = Loading.service({lock: true,text: '数据正在加载中',spinner: 'el-icon-loading',background: 'rgba(25, 32, 53, 1)',target: loadingDom || 'body',})}function closeLoading() {if (LoadingInstance._count > 0) LoadingInstance._count--if (LoadingInstance._count === 0) {LoadingInstance._target.close()LoadingInstance._target = null}}
axios简化版代码(方便看上面方法加入的位置):
// axios.jsconst service = axios.create({baseURL: 'http://localhost:8888', // 设置统一的请求前缀timeout: 10000, // 设置统一的超时时长});service.interceptors.request.use(config => { // 打开loadingif (config.loading) {LoadingInstance._count++if(LoadingInstance._count === 1){openLoading(config.loadingDom)}}return config;}, error => {return Promise.reject(error);});service.interceptors.response.use(response => {// 关闭loadingif (response.config.loading) {closeLoading()}return response;},error => {// 关闭loadingif (error.config.loading) {closeLoading()}return Promise.reject(error);});
加入到基础配置后的代码。这也是最终的完整代码:
import axios from 'axios'import { Message, Loading } from 'element-ui'import store from '@/store'import { getToken } from '@/utils/auth'// 创建axios实例const service = axios.create({baseURL: process.env.BASE_API, // api的base_urltimeout: 5000, // 请求超时时间})// request拦截器service.interceptors.request.use(config => {removePending(config)// 如果repeatRequest不配置,那么默认该请求就取消重复接口请求!config.repeatRequest && addPending(config)// 打开loadingif (config.loading) {LoadingInstance._count++if(LoadingInstance._count === 1){openLoading(config.loadingDom)}}// 如果登录了,有token,则请求携带token// Do something before request is sentif (store.state.userInfo.token) {config.headers['X-Token'] = getToken() // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改}return config},error => {// Do something with request errorconsole.log(error) // for debugPromise.reject(error)})// respone拦截器service.interceptors.response.use(// response => response,/** * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页 * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中 */response => {// 已完成请求的删除请求中数组removePending(response.config)// 关闭loadingif (response.config.loading) {closeLoading()}const res = response.data// 处理异常的情况if (res.code !== 200) {Message({message: res.message,type: 'error',duration: 5 * 1000,})// 403:非法的token; 50012:其他客户端登录了;401:Token 过期了;if (res.code === 403 || res.code === 50012 || res.code === 401) {MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {confirmButtonText: '重新登录',cancelButtonText: '取消',type: 'warning',}).then(() => {store.dispatch('FedLogOut').then(() => {location.reload() // 为了重新实例化vue-router对象 避免bug})})}return Promise.reject('error')} else {// 默认只返回data,不返回状态码和message// 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data)const isbackAll = response.config.meta && response.config.meta.responseAllif (isbackAll) {return res} else {return res.data}}},error => {error.config && removePending(error.config)// 关闭loadingif (error.config.loading) {closeLoading()}console.log('err' + error) // for debugMessage({message: error.message,type: 'error',duration: 5 * 1000,})return Promise.reject(error)})// --------------------------------取消接口重复请求的函数-----------------------------------// axios.jsconst pendingMap = new Map()/** * 生成每个请求唯一的键 * @param {*} config * @returns string */function getPendingKey(config) {let { url, method, params, data } = configif (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&')}/** * 储存每个请求唯一值, 也就是cancel()方法, 用于取消请求 * @param {*} config */function addPending(config) {const pendingKey = getPendingKey(config)config.cancelToken =config.cancelToken ||new axios.CancelToken(cancel => {if (!pendingMap.has(pendingKey)) {pendingMap.set(pendingKey, cancel)}})}/** * 删除重复的请求 * @param {*} config */function removePending(config) {const pendingKey = getPendingKey(config)if (pendingMap.has(pendingKey)) {const cancelToken = pendingMap.get(pendingKey)cancelToken(pendingKey)pendingMap.delete(pendingKey)}}// ----------------------------------loading的函数-------------------------------const LoadingInstance = {_target: null, // 保存Loading实例_count: 0,}function openLoading(loadingDom) {LoadingInstance._target = Loading.service({lock: true,text: '数据正在加载中',spinner: 'el-icon-loading',background: 'rgba(25, 32, 53, 1)',target: loadingDom || 'body',})}function closeLoading() {if (LoadingInstance._count > 0) LoadingInstance._count--if (LoadingInstance._count === 0) {LoadingInstance._target.close()LoadingInstance._target = null}}export default service
loading搭配api使用:
loading页面中使用:
优化三:用qs模块来序列化参数
我这边没有使用qs进行序列化。主要原因是因为现在前后端交互的数据格式主流就是json格式。只有图片上传接口会使用表单的方式进行提交。 如果是个别的post接口,后台要求你用表单提交,你可以进行交涉下。
前后端交互格式参考https://blog.csdn.net/qq_43654065/article/details/114642300
最终的完整代码
import axios from 'axios'import { Message, Loading } from 'element-ui'import store from '@/store'import { getToken } from '@/utils/auth'// 创建axios实例const service = axios.create({baseURL: process.env.BASE_API, // api的base_urltimeout: 5000, // 请求超时时间})// request拦截器service.interceptors.request.use(config => {removePending(config)// 如果repeatRequest不配置,那么默认该请求就取消重复接口请求!config.repeatRequest && addPending(config)// 打开loadingif (config.loading) {LoadingInstance._count++if(LoadingInstance._count === 1){openLoading(config.loadingDom)}}// 如果登录了,有token,则请求携带token// Do something before request is sentif (store.state.userInfo.token) {config.headers['X-Token'] = getToken() // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改}return config},error => {// Do something with request errorconsole.log(error) // for debugPromise.reject(error)})// respone拦截器service.interceptors.response.use(// response => response,/** * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页 * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中 */response => {// 已完成请求的删除请求中数组removePending(response.config)// 关闭loadingif (response.config.loading) {closeLoading()}const res = response.data// 处理异常的情况if (res.code !== 200) {Message({message: res.message,type: 'error',duration: 5 * 1000,})// 403:非法的token; 50012:其他客户端登录了;401:Token 过期了;if (res.code === 403 || res.code === 50012 || res.code === 401) {MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {confirmButtonText: '重新登录',cancelButtonText: '取消',type: 'warning',}).then(() => {store.dispatch('FedLogOut').then(() => {location.reload() // 为了重新实例化vue-router对象 避免bug})})}return Promise.reject('error')} else {// 默认只返回data,不返回状态码和message// 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data)const isbackAll = response.config.meta && response.config.meta.responseAllif (isbackAll) {return res} else {return res.data}}},error => {error.config && removePending(error.config)// 关闭loadingif (error.config.loading) {closeLoading()}console.log('err' + error) // for debugMessage({message: error.message,type: 'error',duration: 5 * 1000,})return Promise.reject(error)})// --------------------------------取消接口重复请求的函数-----------------------------------// axios.jsconst pendingMap = new Map()/** * 生成每个请求唯一的键 * @param {*} config * @returns string */function getPendingKey(config) {let { url, method, params, data } = configif (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&')}/** * 储存每个请求唯一值, 也就是cancel()方法, 用于取消请求 * @param {*} config */function addPending(config) {const pendingKey = getPendingKey(config)config.cancelToken =config.cancelToken ||new axios.CancelToken(cancel => {if (!pendingMap.has(pendingKey)) {pendingMap.set(pendingKey, cancel)}})}/** * 删除重复的请求 * @param {*} config */function removePending(config) {const pendingKey = getPendingKey(config)if (pendingMap.has(pendingKey)) {const cancelToken = pendingMap.get(pendingKey)cancelToken(pendingKey)pendingMap.delete(pendingKey)}}// ----------------------------------loading的函数-------------------------------const LoadingInstance = {_target: null, // 保存Loading实例_count: 0,}function openLoading(loadingDom) {LoadingInstance._target = Loading.service({lock: true,text: '数据正在加载中',spinner: 'el-icon-loading',background: 'rgba(25, 32, 53, 1)',target: loadingDom || 'body',})}function closeLoading() {if (LoadingInstance._count > 0) LoadingInstance._count--if (LoadingInstance._count === 0) {LoadingInstance._target.close()LoadingInstance._target = null}}export default service
总结
基础的axios即可满足大部分项目的需求,配置loading和取消重复请求就见仁见智了,可加可不加。
项目demo源码地址:https://github.com/rui-rui-an/packageAxios