axios二次封装
前言
提示功能用的是element-ui,大家可根据实际使用的ui库进行替换
开始封装之前,翻了官方文档,看了很多博文,总结如下:
- 不需要过度封装
- 重点关注“请求拦截”和“响应拦截”
- 处理重复请求机制(保留第一个请求 or 保留最后一个请求)
- 自动重试请求的时机
- 封装同时也要保留好拓展性
亮点
- 处理重复请求(只发送第一个请求)
- 请求自动重试(重试时机:超时触发)
- 清空全部请求(使用场景:切换页面后,停止上一个页面的所有请求)
- 封装 get post put delete upload 请求
- 根据不同环境更改不同 baseUrl
⚠️ 处理重复请求有两种选择:
1️⃣ 第一种:保留第一个请求,后续相同请求全部取消
2️⃣ 第二种:保留最后一个请求,前面相同请求全部取消
🌈 总结:实际请求发现第二种,只保留最后一个请求,前面发出去的请求就算前端取消了,后端日志显示还是收到请求,这与我们的预期不符合,故使用第一种方案。
request.js
'use strict';
import axios from 'axios';
import { Message } from 'element-ui';
/**
* [汇总] 自定义 config 属性
* allowDuplicateRequest {boolean} false 允许重复请求
* restoreDataFormat {boolean} true 还原数据结构
* successText {string} '' 成功提示文本
* errorText {string} '' 失败提示文本
* retry {number} 3 重试次数
* retryDelay {number} 2 重试时间(单位s)
*/
const axiosInstance = axios.create({
baseURL: '/',
timeout: 3 * 1000,
});
// 根据不同环境更改不同 baseUrl
if (process.env.NODE_ENV === 'development') {
axiosInstance.defaults.baseURL = '/'; // 开发环境
} else if (process.env.NODE_ENV === 'production') {
axiosInstance.defaults.baseURL = '/'; // 生产环境
}
// 请求队列
const requestQueue = new Map();
// 请求拦截器
axiosInstance.interceptors.request.use((config) => {
// 请求之前做些什么
// 例如:请求头添加 token
// console.log('请求拦截', config);
const { allowDuplicateRequest = false } = config;
// 允许重复请求
if (allowDuplicateRequest) {
return config;
}
/**
* 以下逻辑是处理 => 重复请求
*/
// 生成请求key
const requestKey = getRequestKey(config);
// 创建请求控制器
const controller = new AbortController();
config.signal = controller.signal;
// 判断请求队列是否存在相同请求
if (requestQueue.has(requestKey)) {
// 停止请求
controller.abort();
} else {
// 把本次请求提交到队列
requestQueue.set(requestKey, controller);
}
return config;
}, (error) => {
// 请求错误
console.log('请求错误', error);
return Promise.reject(error);
});
// 响应拦截器
axiosInstance.interceptors.response.use((response) => {
// 响应 status === 200
// console.log('响应拦截', response);
const config = response.config;
// 生成请求key
const requestKey = getRequestKey(config);
// 从请求队列移除掉
requestQueue.delete(requestKey);
// 还原数据结构, 成功提示文本, 失败提示文本
const { restoreDataFormat = true, successText, errorText } = config;
// 还原数据结构
if (restoreDataFormat) {
const { code, msg, data } = response.data;
if (code === 200) {
Message({
message: successText || msg || '成功',
type: 'success'
});
} else {
Message({
message: errorText || msg || '失败',
type: 'error'
});
return Promise.reject(msg)
}
return data;
}
return response;
}, async (error) => {
// 响应 status !== 200
console.log('响应错误', error);
const config = error.config;
/**
* 以下逻辑处理 => 请求重试
* 重试时机: 超时
*/
if (error.message.indexOf('timeout') !== -1) {
// 生成请求key
const requestKey = getRequestKey(config);
// 从请求队列移除掉
requestQueue.delete(requestKey);
// 重试次数、重试时间、当前重试次数
const { retry = 3, retryDelay = 2, retryCount = 0 } = config;
// 延时处理
const delay = new Promise((resolve) => {
setTimeout(() => {
resolve();
}, retryDelay * 1000);
});
// 重试次数 && 当前重试次数未超过最大重试次数
if (retry && (retryCount < retry)) {
// 当前重试次数加一
config.retryCount = retryCount + 1;
console.log(`[请求异常] 尝试重新请求 => [${config.url}] => 第 ${config.retryCount} 次`);
Message({
message: `[请求异常] 尝试重新请求第 ${config.retryCount} 次`,
type: 'warning'
});
// 重新发起请求
return delay.then(function () {
return baseRequest(config);
});
}
Message({
message: '接口异常, 请联系管理员',
type: 'error'
});
}
return Promise.reject(error);
});
// 基础请求
const baseRequest = (config) => {
return axiosInstance(config);
}
const get = (url = '', params = {}, config = {}) => {
const newConfig = Object.assign({}, {
method: 'get',
url,
params,
}, config);
return baseRequest(newConfig)
}
const post = (url = '', data = {}, config = {}) => {
const newConfig = Object.assign({}, {
method: 'post',
url,
data,
}, config);
return baseRequest(newConfig)
}
const put = (url = '', data = {}, config = {}) => {
const newConfig = Object.assign({}, {
method: 'put',
url,
data,
}, config);
return baseRequest(newConfig)
}
const destroy = (url = '', data = {}, config = {}) => {
const newConfig = Object.assign({}, {
method: 'delete',
url,
data,
}, config);
return baseRequest(newConfig)
}
const upload = (url = '', data = {}, config = {}) => {
const newConfig = Object.assign({}, {
method: 'post',
url,
data,
headers: {
'Content-Type': 'multipart/form-data'
},
timeout: 60 * 1000,
}, config);
return baseRequest(newConfig)
}
// 字符串转hash
const strToHash = (str) => {
let hash = 0;
let chr;
if (str.length === 0) {
return hash
}
for (let i = 0; i < str.length; i++) {
chr = str.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0;
}
return hash;
};
const getRequestKey = (config) => {
let { url, method, data, params } = config;
data = (typeof data === 'string') ? JSON.parse(data) : data;
// // 把请求转为字符串
const requestStr = `${method}_${url}_${params ? JSON.stringify(params) : JSON.stringify(data)}`;
// // 设置请求key
const requestKey = strToHash(requestStr);
return requestKey;
}
const cancelAllRequest = () => {
requestQueue.forEach((controller) => {
// 停止请求
controller.abort();
});
requestQueue.clear();
return 'ok';
}
export default {
axiosInstance,
get,
post,
put,
delete: destroy,
upload,
cancelAllRequest,
}
get post put delete算是过度封装了,考虑到大家使用场景不同,就保留了下来,觉得不需要的删的就好
使用
上传文件示例
files是数组,可上传多个文件,同时有上传回调,大家可以根据场景添加上传进度
<template>
<div id="app">
<input type="file" id="upload" />
<button @click="uploadFile()">上传文件</button>
</div>
</template>
<script>
import request from "./request";
export default {
name: "App",
components: {},
mounted() {
},
methods: {
async uploadFile() {
const input = document.querySelector("#upload");
const files = input.files;
const data = await request.upload(
"http://api.com/upload",
{
id: 123,
files,
},
{
onUploadProgress: function (progressEvent) {
const { loaded, total, progress, estimated, rate } = progressEvent;
console.log("文件总大小: ", parseInt(total / 1024));
console.log("已上传文件大小: ", parseInt(loaded / 1024));
console.log("当前进度: ", parseInt(progress * 100));
console.log("预估完成时间: ", parseInt(estimated || 0));
console.log("当前上传速度: ", parseInt((rate || 0) / 1024));
},
}
);
console.log("data: ", data);
},
},
};
</script>
<style>
</style>