Axios封装示例代码+封装思路

Author: Echo
Time: 2019-09-20 10:50:39

axios文档地址

在vue项目中,和后台交互获取数据这块,我们通常使用的是axios库,它是基于promise的http库,可运行在浏览器端和node.js中。他有很多优秀的特性,例如拦截请求和响应、取消请求、转换json、客户端防御cSRF等。所以我们的尤大大也是果断放弃了对其官方库vue-resource的维护,直接推荐我们使用axios库.

本次封装以vue项目为例做实践。

安装
$ npm install axios
引入

在项目的src目录下新建文件夹request,文件夹下新建http.js和api.js文件:

  • http.js: 封装axios
  • api.js: 用来统一管理接口
// 在http.js中引入axios
import axios from 'axios'; // 引入axios
import QS from 'qs'; // 引入qs模块,用来序列化post类型的数据,后面会提到

环境的切换->配置不同的baseURL

我们的项目环境可能有开发环境、测试环境和生产环境.
我们通过node的环境变量来匹配我们的默认的接口url前缀
axios.defaults.baseURL可以设置axios的默认请求地址

const baseURL_Map = {
  dev:'https://www.baidu.com',
  uat: 'https://www.ceshi.com',
  product: 'https://www.production.com'
}
axios.defaults.baseURL = baseURL_Map[process.env.NODE_ENV] 

扩展:

process.env.NODE_ENV
  • process对象是全局变量: 提供当前node.js的有关信息,以及控制当前node.js的有关进程。因为是全局变量,它对于node应用程序是始终可用的,无需require()
  • process.env属性返回一个包含用户环境信息的对象
  • process.env.NODE_ENV:不是process.env对象上原有的属性,是我们自己添加进去的一个环境变量,用来确定当前所处的开发阶段。
  • 测试环境: uat; 生产环境:product; 开发环境: dev
设置请求超时变量
// 例如超过了10s,就会告知用户当前请求超时,请刷新等。

const TIMEOUT = 30000;

扩展:

  • 统一处理所有接口的超时时间: axios.defaults.timeout = 10000;
设置post请求头
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';

此处无需设置,在下面封装的时候会针对get和post请求做统一对应处理

定义HTTP响应状态码的范围
/**
 * 判断 HTTP 请求是否成功
 * @param {number} status - HTTP 状态码
 * @returns {Boolean}
 */
const isBadRequest = status => status >= 300;

映射 HTTP 错误时的提示语句

/**
 * 映射 HTTP 错误时的提示语句
 * @param {enhanceError} error
 * @returns {Object}
 */
const badStatusMap = error => ({
  400: '请求错误',
  401: '未授权,请登录',
  403: '拒绝访问',
  404: `请求地址出错 ${error.response ? error.response.config.url : ''}`,
  408: '请求超时',
  500: '服务器内部错误',
  501: '服务未实现',
  502: '网关错误',
  503: '服务不可用',
  504: '网关超时',
  505: 'HTTP版本不受支持'
});

封装axios
  • http.js
/**
 * http.js
 * @param {Object} [globalOption]
 */
import {
  mergeWith, omit, isPlainObject
} from 'lodash';

// isPlainObject : checks if value is a plain object, that is, an object created by the Object constructor or one with a [[Prototype]] of null
// mergeWith 是用于递归合并来源对象的自身和继承的可枚举属性到目标对象。 
// 基本意思就是将默认的option配置和手动配置的globalOption,通过递归的形式合并成一个option.


/**
 * 修复 axios 转换 bug,如可能把请求中的日期等转成空对象的问题
 *  'utils.merge' in 'node_modules\axios\lib\core\Axios.js'
 * @param {any} obj
 */
const fixAxiosDateMerge = (obj) => {
  if (!obj) return obj;
  if (obj instanceof Date) return obj.getTime();
  if (!isPlainObject(obj)) return obj;
  Object.keys(obj).forEach((key) => {
    obj[key] = fixAxiosDateMerge(obj[key]);
  });
  return obj;
};

function axiosWrapper(globalOption) {
  return function(option) {
    // 此处的操作相对与开放一点,把baseURL、timeout、responseType、可以根据接口不同做单独配置。
    option = mergeWith({
      baseURL: BASE_URL,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Cache-Control': 'no-cache'
      },
      timeout: TIMEOUT,
      withCredentials: true,
      responseType: '',
      validateStatus: status => !isBadRequest(status)

      // 可以定义HTTP响应状态码的范围。如果`validateStatus`返回`true`(或被设置为`null` promise将被解析;否则,promise将被拒绝。
    }, globalOption, option);

  
    if (option.data) {
      option.data = fixAxiosDateMerge(option.data);
    }
    if (option.params) {
      option.params = fixAxiosDateMerge(option.params);
    }

    const r = axios.create(option);
    
    // 在axios的源代码中,超时后, 会在拦截器那里 axios.interceptors.response 捕抓到错误信息, 且 error.code = "ECONNABORTED"

    // timeout
    r.interceptors.request.use(
      config => config,
      (error) => {
        
        if (error && error.code === 'ECONNABORTED'
          && ~error.message.indexOf('timeout')) {
          console.log('[AxiosWrapper] request timeout');
        }
        return Promise.reject(error);
      }
    );

    // other errors // 跳转到对应的报错页面
    r.interceptors.request.use(
      config => config,
      (error) => {
        const errorInfo = error.response;
        if (errorInfo) {
          const errorStatus = errorInfo.status;
          router.push({
            path: `/error/${errorStatus}`
          });
        }
        return Promise.reject(error);
      }
    );

    // make data
    r.interceptors.request.use(
      (opt) => {
        const params = mergeWith({}, opt.data, opt.params); // cloneDeep(opt.data);
        opt = omit(opt, ['data', 'params']);
        const needBody = /^(put|post|patch)$/i.test(opt.method);
        const sendJSON = opt.headers
          && opt.headers['Content-Type'] === 'application/json';
          // 根据请求头和method来处理入参的格式
        if (needBody) {
          opt.data = sendJSON
            ? JSON.stringify(params)
            : qs.stringify(params);
        } else {
          opt.params = params;
        }
        return opt;
      }
    );

    // bad HTTP request
    r.interceptors.response.use(
      response => response,
      (error) => {
        if (error && error.response) {
          const { status, statusText } = error.response;
          if (isBadRequest(status)) {
            console.warn('[AxiosWrapper] bad HTTP request: status is %s \n', status, error.response);
            error.message = badStatusMap(error)[status] || statusText;
            if (!error.message) {
              try {
                error.message = JSON.parse(error.response.request.responseText).errmsg;
              } catch (ex) {
                console.log('[AxiosWrapper] json parse error', ex);
              }
            }
            Vue.prototype.Config.ROOT_VUE.$message.error(error.message);
            return Promise.reject(new Error(error.message));
          }
        }
        return Promise.reject(error);
      }
    );

    // check business logic
    // 请求失败,根据HTTP响应状态码做出对应的messageTip
    r.interceptors.response.use(
      (response) => {
        if (option.responseType === 'arraybuffer') {
          return response;
        }
        const [reqURL, resData] = takeapartResponse(response);
        const noWarn = 'no-global-config-warn' in response.config.headers
          && !!response.config.headers['no-global-config-warn'];
        const isValid = isValidCode(resData.errno);
        const method = response.config.method.toUpperCase();
        if (!isValid) {
          // eslint-disable-next-line prefer-promise-reject-errors
          return Promise.reject({
            type: ERR_CODE,
            reqURL,
            resData,
            noWarn,
            method
          });
        }
        const reqStr = `${method} ${reqURL}`;
        // warnByResponse 函数是根据业务针对不同接口返回不同的错误提示。这个函数可以根据业务自行处理。
        warnByResponse(reqStr, resData, true, noWarn);
        return resData;
      }
    );
    

    // wrong business logic
    // r.interceptors.response.use(
    //   response => response,
    //   (error) => {
    //     if (error.type === ERR_CODE) {
    //       const reqStr = `${error.method} ${error.reqURL}`;
    //       warnByResponse(reqStr, error.resData, false, error.noWarn);
    //     }
    //     return Promise.reject(error.resData);
    //   }
    // );

    return r(option);
  };
}

简单的封装axios函数

/**
 * 获得一般的 wrapper
 * @param {Object} [option]
 */
 // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
export default option => axiosWrapper({
  transformResponse: data => void (0), // eslint-disable-line no-unused-vars
  
})(option);

在使用axios时,注意到配置选项中包含params和data两者,以为他们是相同的,实则不然。

因为params是添加到url的请求字符串中的,用于get请求。

而data是添加到请求体(body)中的, 用于post请求。

api.js使用实例:
import axios, { download } from './http.js';
export const mock = params => axios({
    url: '/mock',
    method: 'get',
    params
});

export const upload = data => axios({
    url: '/upload',
    method: 'post',
    data
});
用于下载的axiosWrapper
export const download = option => axiosWrapper({
  responseType: 'arraybuffer'
})(option)
  .then((res) => {
    const contentType = res.headers['content-type'];

    if (~contentType.indexOf('json')) {
      const resData = res.data || res.request.response;
      const text = Buffer.from(resData).toString('utf8');
      const json = JSON.parse(text);
      return Promise.reject(json);
    }

    const disposition = res.headers['content-disposition'];
    if (disposition && disposition.match(/attachment/)) {
      let filename = disposition.replace(/attachment;.*filename=/, '').replace(/"/g, '');
      filename = filename && filename !== ''
        ? filename
        : (option.defaultFilename || 'noname');
      const blob = new Blob([res.data], { type: contentType });
      saveAs(blob, filename);
    }

    return Promise.resolve(res);
  });

api.js使用实例:

import axios, { download } from './http';

export const export = (data, defaultFilename) = download({
  url: '/api/export',
  method: 'post',
  data,
  defaultFilename
})

封装一个axios的思路

  • 配置请求基本信息的常量:(用于基本处理)
    BASE_URL :根据环境变量,针对生成、测试、开发环境配置对应的url
    TIMEOUT: 配置接口请求超时时间。
    headers: 配置请求头
    validateStatus: 定义HTTP响应状态码的范围。如果validateStatus返回true(或被设置为null promise将被解析;否则,promise将被拒绝。

可以请求接口的不同,封装axios的时候,将 请求基本信息的常量 通过 参数 的方式单独进行配置。

扩展:
具体方法:通过lodash的mergeWith方法将默认的option配置和手动配置的globalOption,通过递归的形式合并成一个option。

  • 请求拦截

timeout: 在axios的源代码中,超时后, 会在拦截器那里 axios.interceptors.response 捕抓到错误信息, 且 error.code = "ECONNABORTED",可以根据这个code提示超时报错信息。

token:统一在http请求的header都加上token,这样后台根据token判断你的登录情况,即使本地存在token,也有可能token是过期的,所以在响应拦截器中要对返回状态进行判断

other errors: 根据请求状态status,直接promise.reject.路由跳转到对应的报错页面。
根据请求头和method来处理入参的格式

在使用axios时,注意到配置选项中包含params和data两者,以为他们是相同的,实则不然。
因为params是添加到url的请求字符串中的,用于get请求。
而data是添加到请求体(body)中的, 用于post请求。

  • 响应拦截
    badStatusMap: 请求失败,根据HTTP响应状态码做出对应的messageTip

你可能感兴趣的:(Axios封装示例代码+封装思路)