async/await,踩雷,已解决
背景:
在vue项目中,封装http请求方法,后端接口报错约定的是特定的JSON报文格式,且拦截报错内容统一用iview的消息弹框进行渲染,在实际业务功能中接口调用可以省去catch报错的处理。🍬
源码
/**
* 引入axios,创建axios实例
* 封装axios请求拦截器
*/
import axios from "axios";
import util from "../utils/util";
import Message from "../components/message/index";
import { spinShow, spinHide } from "./globalLoading";
let apiCache = new Map();
const CancelToken = axios.CancelToken;
const storeFlag = "cache_";
const _proxyLevel = window.RPConfig.proxyLevel;
const isGetProxy = _proxyLevel === "GET";
const isSetProxy = _proxyLevel === "SET" || _proxyLevel === "GET";
if (isGetProxy) {
let storeKeys = Object.keys(localStorage);
storeKeys.forEach(key => {
if (key.indexOf(storeFlag) > -1) {
let data = JSON.parse(localStorage[key]);
key = key.replace(storeFlag, "");
apiCache.set(key, data);
}
});
}
// 配置请求头
var instance = axios.create({
timeout: 20000
});
function getFormSetString(obj) {
var paramArr = [];
for (const key in obj) {
paramArr.push(`${key}=${obj[key]}`);
}
return paramArr.join("&");
}
window.addEventListener("message", receiveMessage);
function receiveMessage(event) {
// For Chrome, the origin property is in the event.originalEvent
let origin = event.origin || event.originalEvent.origin;
if (origin !== location.origin) return;
if (event.data === "RefreshToken") {
refreshToken().then(() => event.source.postMessage("SendToken"));
}
}
let headers = {
"Content-Type": "application/json;charset=UTF-8",
Pragma: "no-cache",
PostId: getPostId() || sessionStorage.getItem("PostId")
};
function getPostId() {
try {
const component = $store.state[window.$moduleName] || {};
return component.currentPost ? component.currentPost.postId : "";
} catch (error) {
return sessionStorage.getItem("PostId");
}
}
export function getHttpHeaders(config = {}) {
let _headers = Object.assign({}, headers, {
RoleId: sessionStorage.RoleId || "",
LangCode: sessionStorage.LangCode || "",
...config.headers
});
let auth = localStorage.Authorization;
_headers.authorization = auth ? "Bearer " + auth : "";
return _headers;
}
export function setHttpHeaders(config = {}) {
headers = {
...headers,
...config
};
}
let refreshQuery = [],
canRefreshToken = true;
function clearRefreshQuery() {
let resolve;
while ((resolve = refreshQuery.shift())) {
resolve();
}
}
function refreshToken() {
return new Promise((resolve, reject) => {
let needRefresh = !util.isAuthValid() && !!localStorage.Authorization;
if (!needRefresh) {
resolve();
} else {
if (canRefreshToken) {
canRefreshToken = false;
let refreshToken = localStorage.RefreshToken;
let domain = window.location.hostname;
let url = window.RPConfig.oauth + "/refreshToken?refresh_token=" + refreshToken + "&domain=" + domain;
_post(url)
.then(res => {
canRefreshToken = true;
if (res && res.data && res.data.retCode && res.data.retCode == "1") {
res = res.data.data;
util.setLoginInfo(res);
resolve();
clearRefreshQuery();
} else {
window.$channel.$emit("LoginOutEvent");
reject(new Error("服务有问题"));
}
})
.catch(e => {
canRefreshToken = true;
reject(e);
});
} else {
refreshQuery.push(resolve);
}
}
});
}
function appendUrlParam(url, paramStr) {
let dash;
if (url.indexOf("?") > -1) {
dash = "&";
} else {
dash = "?";
}
return url + dash + paramStr;
}
// 获取当前时间
function getNowTime() {
return new Date().getTime();
}
// 获取超时时间设置
function getExpireTime() {
return window.RPConfig.expire_time || (window.RPConfig.expire_time = 5 * 60 * 1000);
}
function getCacheKey({ url, data }) {
if (typeof data === "object") {
data = JSON.stringify(data);
}
return url + data;
}
let clearCacheEventInited = false;
// request 拦截器 在请求或响应被 then 或 catch 处理前拦截它们
instance.interceptors.request.use(
config => {
// 加载效果-显示
let { showLoading = window.RPConfig.showLoading } = config;
if (showLoading) {
spinShow();
}
let contentType = config.headers["Content-Type"];
contentType = contentType && contentType.toLowerCase();
// 请求参数序列化
if (config.method === "post" && config.data) {
let requestType = config.data.requestType;
delete config.data.requestType;
if (requestType === "formSet") {
config.data = getFormSetString(config.data);
} else if (requestType === "query") {
config.url = appendUrlParam(config.url, getFormSetString(config.data));
} else if (contentType && contentType.indexOf("application/json") > -1) {
config.data = JSON.stringify(config.data);
}
}
// 如果需要缓存--考虑到并不是所有接口都需要缓存的情况
if (config.cache || isGetProxy) {
if (!clearCacheEventInited) {
clearCacheEventInited = true;
window.$channel.$on("ClearAPICache", () => {
apiCache.clear();
});
}
let source = CancelToken.source();
config.cancelToken = source.token;
// 去缓存池获取缓存数据
const cacheKey = getCacheKey(config);
let data = apiCache.get(cacheKey);
// 获取当前时间戳
let now = getNowTime();
// 如果开启取模式, 不校验数据有效性
// 判断缓存池中是否存在已有数据 存在的话 再判断是否过期
// 未过期 source.cancel会取消当前的请求 并将内容返回到拦截器的err中
if (data && (now - data.expire < getExpireTime() || isGetProxy)) {
source.cancel(data);
// 加载效果-隐藏
if (showLoading) {
spinHide();
}
}
}
return config;
},
error => {
// 对错误请求的处理
// 弹出错误消息
Message.error({
content: error.message,
closable: true
});
return Promise.reject(error);
}
);
// response拦截器 对请求结果做一些处理
instance.interceptors.response.use(
response => {
const { config } = response;
// 加载效果-隐藏
let { showLoading = window.RPConfig.showLoading } = config;
if (showLoading) {
spinHide();
}
const { cache } = config;
if (cache || isSetProxy || isGetProxy) {
// 缓存数据 并将当前时间存入 方便之后判断是否过期
let data = {
expire: getNowTime(),
data: response
};
const cacheKey = getCacheKey(config);
apiCache.set(cacheKey, data);
if (isSetProxy || isGetProxy) {
try {
localStorage[storeFlag + cacheKey] = JSON.stringify(data);
} catch (error) {
console.error(error);
}
}
}
return response;
},
error => {
// 请求拦截器中的source.cancel会将内容发送到error中
// 通过axios.isCancel(error)来判断是否返回有数据 有的话直接返回给用户
if (axios.isCancel(error)) return Promise.resolve(error.message.data);
// 如果没有的话 则是正常的接口错误 直接返回错误信息给用户
return Promise.reject(error);
}
);
function getErrorMessage(e) {
let msg, code, traceId;
let data = e.response && e.response.data;
if (data) {
msg = data.message || data.msg || data.error || data.error_description;
code = data.code;
traceId = data.traceId;
}
msg = msg || e.message || e;
code = code || (e.response && e.response.status);
const pos = msg.indexOf('"message"');
if (pos >= 0) {
msg = msg.substr(pos + 10);
msg = msg.substr(msg.indexOf('"') + 1);
msg = msg.substring(0, msg.indexOf('"'));
}
return { msg, code, traceId };
}
// 读取文件内容
function judgeErrorByResponseType(response) {
return new Promise((resolve, reject) => {
// 判断是文件流,而且是JSON格式
if (response.headers['content-type'].includes('json')) {
// 此处拿到的data是blob文件流
const { data } = response
const reader = new FileReader()
reader.onload = () => {
const { result } = reader
const errorInfos = JSON.parse(result)
resolve(errorInfos)
}
reader.onerror = err => {
resolve(err)
}
reader.readAsText(data)
} else {
resolve(response)
}
})
}
// 报错同时走了then问题(已解决)
function exceptionProcess(e, error) {
// 后端返回JSON格式的文件流错误信息,单独处理
//方法1.使用catch/then
if (e.response) {
const isBlob = e.response.request && e.response.request.responseType == 'blob'
const isJson = e.response.headers['content-type'].includes('json')
const hasTracId = e.response.data && e.response.data.traceId
if (!hasTracId && isBlob && isJson) {
judgeErrorByResponseType(e.response).then(res => {
const errMsg = "错误信息: " + res.message;
Message.error({
content: res.message,
more: { msg: res.message, code: res.code, traceId: res.traceId },
onClose: _ => _
});
throw new Error(errMsg);
}).catch(() => {})
throw new Error('错误内容为文件流');
}
}
const resStatus = e.response && e.response.status;
const { msg, code, traceId } = getErrorMessage(e);
if (resStatus == 401 || (resStatus == 500 && msg == "会话超时")) {
Message.error({
content: "授权失败,系统自动重新登录!",
duration: 3,
onClose: () => {
window.$channel.$emit("LoginOutEvent");
}
});
} else if (!error) {
if (resStatus == 404 || status == 404) {
Message.error({
content: "服务接口未找到!",
duration: 3
});
} else if (msg) {
const errMsg = "错误信息: " + msg;
Message.error({
content: msg,
more: { msg, code, traceId },
onClose: _ => _
});
throw new Error(errMsg);
}
} else {
error(e.response || e);
}
}
/**
* refresh token 时不要在走对外的post 方法, 否则当需要刷新 token 时, 会多调一次 refreshToken
*/
function _post(url, params, error, config) {
if (Object.prototype.toString.call(params) === "[object Object]") {
Object.keys(params).forEach(key => {
if (params[key] === undefined) {
params[key] = "";
}
});
}
return instance
.post(url, params, {
...config,
headers: getHttpHeaders(config)
})
.catch(e => {
exceptionProcess(e, error);
});
}
/**
* 封装并导出get方法、post方法。
*/
export const http = {
/**
* Axios 的实例对象
*/
instance,
getHttpHeaders,
setHttpHeaders,
refreshToken,
/**
* get请求
* @param {String} url 服务地址
* @param {Object} params 请求参数
* @param {Funciton} error 异常处理函数. 若不配置, 将使用默认处理
* @param {Object} config 配置请求设置, 常用语设置content-type, timeout 等 axios 的特殊设置。以及 showLoading 控制是否添加加载效果, true为添加,false为不添加,默认取 window.RPConfig.showLoading 配置项
*/
async get(url, params, error, config = {}) {
if (error && typeof error !== "function") {
console.error("参数传递有误吧? 第三个参数是异常处理函数, 第四个参数是 Axios 的配置对象!");
}
await refreshToken();
return instance
.get(url, {
params,
...config,
headers: getHttpHeaders(config)
})
.catch(e => {
exceptionProcess(e, error);
});
},
/**
* post请求
* @param {String} url 服务地址
* @param {Object} params 请求参数
* @param {Funciton} error 异常处理函数. 若不配置, 将使用默认处理
* @param {Object} config 配置请求设置, 常用语设置content-type, timeout 等 axios 的特殊设置。以及 showLoading 控制是否添加加载效果, true为添加,false为不添加,默认取 window.RPConfig.showLoading 配置项
*/
async post(url, params, error, config = {}) {
if (error && typeof error !== "function") {
console.error("参数传递有误吧? 第三个参数是异常处理函数, 第四个参数是 Axios 的配置对象!");
}
await refreshToken();
return _post.apply(undefined, arguments);
}
};
但是,文件下载时加了请求头{ responseType: ‘blob’ },导致文件下载失败后,返回的报错信息是JSON格式的报文(文件流),在chrome的network中也能看到返回的是正确的报文格式,但实际是文件流,需要封装一个文件内容读取方法,来获取JSON报文,然后把内容正确的渲染出来。
文件读取的方法如下:
// 读取文件内容
function judgeErrorByResponseType(response) {
return new Promise((resolve, reject) => {
// 判断是文件流,而且是JSON格式
if (response.headers['content-type'].includes('json')) {
// 此处拿到的data才是blob
const { data } = response
const reader = new FileReader()
reader.onload = () => {
const { result } = reader
const errorInfos = JSON.parse(result)
resolve(errorInfos)
}
reader.onerror = err => {
resolve(err)
}
reader.readAsText(data)
} else {
resolve(response)
}
})
}
由于文件读取judgeErrorByResponseType
方法是一个Promise方法,当时在exceptionProcess
中调用的时候,使用了async/await进行同步处理。
同步处理:
async function exceptionProcess(e, error) {
// 后端返回JSON格式的文件流错误信息,单独处理
/**--------------------方法二、方法三同步处理------------------------------*/
let jsonData;
if (e.response) {
const isBlob = e.response.request && e.response.request.responseType == 'blob'
const isJson = e.response.headers['content-type'].includes('json')
const hasTracId = e.response.data && e.response.data.traceId
if (!hasTracId && isBlob && isJson) {
// 一般会使用try/catch,处理judgeErrorByResponseType报错
const errInfo = await judgeErrorByResponseType(e.response)
jsonData = { response: { data: errInfo } }
}
}
const resStatus = e.response && e.response.status;
const { msg, code, traceId } = jsonData && getErrorMessage(jsonData) && getErrorMessage(e);
/**------------------------------------------------------------------------*/
if (resStatus == 401 || (resStatus == 500 && msg == "会话超时")) {
Message.error({
content: "授权失败,系统自动重新登录!",
duration: 3,
onClose: () => {
window.$channel.$emit("LoginOutEvent");
}
});
} else if (!error) {
if (resStatus == 404 || status == 404) {
Message.error({
content: "服务接口未找到!",
duration: 3
});
} else if (msg) {
const errMsg = "错误信息: " + msg;
Message.error({
content: msg,
more: { msg, code, traceId },
onClose: _ => _
});
throw new Error(errMsg);
}
} else {
error(e.response || e);
}
}
业务开发中,http使用
// http方法调用(全局注册http为$http)
function url(s) {
return `${window.RPConfig.fmg}/*****/${s}`
}
// 列表查询,不分页
export const getList = param => $http.post(url('list'), param)
// 接口调用
getList(param).then(res => {
// 请求正确返回
}).catch(err => {
// 请求错误返回,做了错误拦截,统一渲染,可省略message提示
})
但是,调用接口时,在报错的情况下,接口任然走了.then。
为什么呢????
我们看下面这个例子:
async function test1() {
}
async function test() {
await test1()
return 'sssssssss'
}
console.log('00000000000', test())
test().then(res => {
console.log('----------res', res)
}).catch(err => {
console.log('-------err---', err)
})
错误定位:
function 方法前加async, 方法内部使用await,实现了方法内部的同步操作,但是我们在调用该方法时,要注意:async function
本身就是一个Promise方法, 所以本身就会曝出一个.then和.catch。exceptionProcess
调用时,then和catch未被拦截,导致接口在报错的情况下,任然走了then。
function _post(url, params, error, config) {
if (Object.prototype.toString.call(params) === "[object Object]") {
Object.keys(params).forEach(key => {
if (params[key] === undefined) {
params[key] = "";
}
});
}
return instance
.post(url, params, {
...config,
headers: getHttpHeaders(config)
})
.catch(e => {
// exceptionProcess方法未Promise方法,这里then/catch未被拦截
exceptionProcess(e, error);
});
}
解决办法:
方法一:将async/await改为then/catch。
function exceptionProcess(e, error) {
// 后端返回JSON格式的文件流错误信息,单独处理
/**-------------------方法一:使用catch/then------------------------------*/
if (e.response) {
const isBlob = e.response.request && e.response.request.responseType == 'blob'
const isJson = e.response.headers['content-type'].includes('json')
const hasTracId = e.response.data && e.response.data.traceId
if (!hasTracId && isBlob && isJson) {
judgeErrorByResponseType(e.response).then(res => {
const errMsg = "错误信息: " + res.message;
Message.error({
content: res.message,
more: { msg: res.message, code: res.code, traceId: res.traceId },
onClose: _ => _
});
throw new Error(errMsg);
}).catch(() => {})
throw new Error('错误内容为文件流');
}
}
/**------------------------------------------------------------------------*/
const resStatus = e.response && e.response.status;
const { msg, code, traceId } = getErrorMessage(e);
if (resStatus == 401 || (resStatus == 500 && msg == "会话超时")) {
Message.error({
content: "授权失败,系统自动重新登录!",
duration: 3,
onClose: () => {
window.$channel.$emit("LoginOutEvent");
}
});
} else if (!error) {
if (resStatus == 404 || status == 404) {
Message.error({
content: "服务接口未找到!",
duration: 3
});
} else if (msg) {
const errMsg = "错误信息: " + msg;
Message.error({
content: msg,
more: { msg, code, traceId },
onClose: _ => _
});
throw new Error(errMsg);
}
} else {
error(e.response || e);
}
}
方法二:如果非要使用async/await,那么调用async function
时,使用await。也就是,从头至尾,
都使用async/await,一条线。
注意:await 异步方法:能够得到reject/和resolve爆出的值,需要判断是否是正确返回还是错误返回。
async function _post(url, params, error, config) {
if (Object.prototype.toString.call(params) === "[object Object]") {
Object.keys(params).forEach(key => {
if (params[key] === undefined) {
params[key] = "";
}
});
}
return instance
.post(url, params, {
...config,
headers: getHttpHeaders(config)
})
.catch(e => {
// 错误信息已渲染,这里也使用await,拦截then/catch
await exceptionProcess(e, error);
});
}
async post(url, params, error, config = {}) {
if (error && typeof error !== "function") {
console.error("参数传递有误吧? 第三个参数是异常处理函数, 第四个参数是 Axios 的配置对象!");
}
await refreshToken();
// 继续使用await,拦截then/catch
return await _post.apply(undefined, arguments);
}
方法三:then/catch和async/await嵌套使用,在调用async function
时,加上then/catch,拦截
返回信息,不做处理。
function _post(url, params, error, config) {
if (Object.prototype.toString.call(params) === "[object Object]") {
Object.keys(params).forEach(key => {
if (params[key] === undefined) {
params[key] = "";
}
});
}
return instance
.post(url, params, {
...config,
headers: getHttpHeaders(config)
})
.catch(e => {
// 错误信息已渲染,这里拦截then和catch
exceptionProcess(e, error).then(() => {}).catch(() => {});
});
}
结论:
1、then/catch和async/await不建议嵌套使用。
2、async/await方法,使用时,如果嵌套在then/catch中,要做async方法then/catch拦截。
ndefined, arguments);
}
**方法三:then/catch和async/await嵌套使用,在调用`async function`时,加上then/catch,拦截**
**返回信息,不做处理。**
```js
function _post(url, params, error, config) {
if (Object.prototype.toString.call(params) === "[object Object]") {
Object.keys(params).forEach(key => {
if (params[key] === undefined) {
params[key] = "";
}
});
}
return instance
.post(url, params, {
...config,
headers: getHttpHeaders(config)
})
.catch(e => {
// 错误信息已渲染,这里拦截then和catch
exceptionProcess(e, error).then(() => {}).catch(() => {});
});
}
结论:
1、then/catch和async/await不建议嵌套使用。
2、async/await方法,使用时,如果嵌套在then/catch中,要做async方法then/catch拦截。