async/await

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拦截。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值