封装axios

| 前端 | axios / 封装 | 12k | 20 分钟

封装axios

github地址

功能点

  • 支持多模块多 baseUrl 配置,每个实例可以单独配置
  • 防重入策略
  • 参数处理策略
  • 校验业务状态是否为失败状态
  • 失败状态进行全局提醒
  • 提供一个方法取消当前正在进行的请求
  • 消息国际化 ✖未实装

目录结构

文件 描叙
index.js 入口文件
msg.js http状态码对应的消息文案
option.js 默认的配置参数
queen.js 记录当前正在进行的请求
request.js request拦截器代码
response.js response拦截器代码
util.js 拥到的一些封装函数

安装使用

安装request-by-axios

npm install request-by-axios --save
#or
yarn add request-by-axios --dev

配置axios的实例

import { create } from "request-by-axios";

const options = [
  {
    baseUrl: "/api/user",
    serverName: "user",
    log: true,
  },
  {
    baseUrl: "/api/order",
    serverName: "order",
    log: true,
    adapter: (config) => {
      return new Promise((resolve) => {
        const res = {
          data: {
            success: true,
            message: "Ok",
            data: [{ id: 1 }, { id: 2 }],
          },
          status: 200,
          statusText: "OK",
          config,
        };
        // 调用响应函数
        setTimeout(() => {
          resolve(res);
        }, 5000);
      });
    },
  },
];
const instance = create(options);

export default instance;

维护不同模块的后端服务接口

// src/api/module/order.js
import http from "request-by-axios";
const { order } = http;

export default {
  list: function (params) {
    return order.get("url", {
      params,
      cancelModel: 2,
    });
  },
};

// src/api/module/user.js
import http from "request-by-axios";
const { user } = http;

export default {
  login: function (data, config) {
    return user.post("login", data, config);
  },
};

Vue创建全局api

import "./config";
import user from "./module/user";
import order from "./module/order";

const apiList = Object.assign({}, user, order);

function install(Vue) {
  if (install.installed) {
    return console.warn("api重复注册");
  }
  Object.defineProperties(Vue.prototype, {
    $api: {
      get() {
        return apiList;
      },
    },
  });
  install.installed = true;
}

export default {
  install,
};

调用

<template>
  <div>
    <div>
      自定义adapter
      <button @click="sendOrder">发起order请求</button>
    </div>
    <div>
      不同模块不同配置
      <button @click="sendUser">发起user请求</button>
      <button @click="sendOrder">发起order请求</button>
    </div>
    <div>
      防重入
      <button @click="sendOrder2">连续发起两次order请求</button>
    </div>
    <div>
      自定义错误提醒
      <button @click="sendUser2">
        调用接口传入或全局配置, 进行500系列提醒
      </button>
    </div>
    <div>
      参数去空格
      <button @click="sendUser3">参数去空格</button>
    </div>
  </div>
</template>
export default {
  name: "Axios",
  methods: {
    sendUser() {
      this.$api.login();
    },
    sendOrder() {
      this.$api.list();
    },
    sendOrder2() {
      this.sendOrder();
      setTimeout(this.sendOrder, 2000);
    },
    sendUser2() {
      this.$api.login(
        { username: "", password: "" },
        {
          showErrorMsg: ({ response }) => {
            if (response.status === 404) {
              alert("接口未实现");
            }
          },
        }
      );
    },
    sendUser3() {
      const data = { username: " das f ", password: "zzzz " };
      this.$api.login(data, {
        beforeAxiosSend: function (config) {
          alert(
            "原数据:" +
              JSON.stringify(data) +
              "\n" +
              " 处理后数据:" +
              JSON.stringify(config.data)
          );
        },
        autoTrim: true,
      });
    },
  },
};

核心代码

默认配置参数 option.js

export default {
  // 👇axios原生的参数配置
  // http://www.axios-js.com/zh-cn/docs/#%E8%AF%B7%E6%B1%82%E9%85%8D%E7%BD%AE

  // baseUrl,
  // timeout: 5000,
  // headers: {},
  // maxContentLength: -1,
  // maxBodyLength: -1,
  // transformResponse,
  // transformRequest,
  // adapter,
  // xsrfCookieName: 'XSRF-TOKEN',
  // xsrfHeaderName: 'X-XSRF-TOKEN',
  // validateStatus,


  // ================
  // 👇新增的参数配置

  // axios实例的引用名
  serverName: 'default',
  // 是否对参数进行去头尾空格
  autoTrim: false,
  // 是否打印log
  log: false,
  // 防重复中断策略 0 关闭 1 严格校验后中断前 2 严格校验后中断后 3 宽松校验中断前 4 宽松校验中断后
  cancelModel: 0,
  // 按钮的引用,用来统一开启和关闭按钮的loading效果
  // btnLoadingRef: null, // 未实装
  // 模态提示效果的引用,用来统一开启和关闭模态式的loading效果
  // modalLoadingRef: null, // 未实装
  // 验证业务是否成功,用来配合showErrorMsg进行统一的Http错误和业务错误提醒
  validateBusiStatus: function ({ data }) {
    // return data.success
    return true
  },
  // 非正常业务流产生时进行的统一提醒
  showErrorMsg: function ({config, response}) {
    // if (response.status >= 500 && response.status < 600) {
      // 服务器错误
    // }
    // if ...
    // 业务错误
    // console.log('showErrorMsg -> 请求出错了')
  },
  // 用来操作header的钩子
  onHeaderCreated: function ({ header }) {
    // const token = localStorage.getItem('token')
    // if (token) {
    //   header.token = token
    // }
  },
  // 用来在axios send前做一些逻辑
  beforeAxiosSend: function ({ header }) {}
}

入口文件index.js

在入口文件中对外暴露创建实例的方法,和清空当前所有进行中请求的方法

import axios from 'axios'
import defaultOption from './option'
import requestInterceptor from './request'
import responseInterceptor from './response'
import {
  guid
} from './util'
import { deleteByUUID, clearQueen } from './queen'

/**
 * 根据参数创建axios实例
 * @param {Object} option 
 */
function createInstance(option) {
  // 创建axios实例
  const axiosInstance = axios.create(option)
  // 注册拦截器
  requestInterceptor(axiosInstance)
  // 注册拦截器
  responseInterceptor(axiosInstance)
  // .match(/\d+$/g)
  return axiosInstance
}

/**
 * 传入一组配置参数,创建对应的axios实例
 * @param {Array} options 一组axios的创建参数
 */
function create(options) {
  // 没有传参,取默认参数
  const listOptions = [].concat(options || defaultOption)
  listOptions.forEach(option => {
    // 合并自定义参数和默认参数,生成实例
    const axiosInstance = createInstance(Object.assign(defaultOption, option))
    // 将实例根据参数中的serverName挂载到instance上,用于import
    instance[option.serverName] = axiosInstance
  })
  // 重写request方法,为每一次请求生成一个uuid,和删除请求队列
  const orginRequest = axios.Axios.prototype.request
  axios.Axios.prototype.request = function (config) {
    // 生成唯一标识
    config.uuid = guid()
    return orginRequest.call(this, config).finally(() => {
      // 确保每一次请求都会被清空
      deleteByUUID(config.uuid)
    })
  }
  return instance
}

// 对外提供两个函数
export {
  create,
  clearQueen
}

const instance = {
  axios
}

export default instance

维护一个正在请求中的请求队列queen.js

用于防重入取消请求,或者页面刷新后取消以前的请求

import {
  isEqualObj
} from './util'

let REQUEST_QUEEN = {}

/**
 * 根据校验参数校验两次请求是否判定为相同
 * @param {Object} a 请求特征
 * @param {Object} b 请求特征
 * @param {Number} m 校验方式
 */
function isEqualXhr(a, b, m) {
  // 宽松校验,url相等则认定为相等
  if (m > 3) {
    return a.url === b.url
  }
  // 深度校验
  return isEqualObj(a, b)
}

/**
 * 队列中插入请求
 * @param {Object} config 请求时参数对象
 */
function addByConfig({ cancel, uuid, url, data, parms, method }) {
  REQUEST_QUEEN[uuid] = { uuid, cancel, url, data, parms, method }
}

/**
 * 根据uuid移除已完成的请求
 * @param {*} uuid 请求的唯一标识
 */
function deleteByUUID(uuid) {
  delete REQUEST_QUEEN[uuid]
}

/**
 * 当最新的请求产生,根据配置参数查询是否有需要中止的请求
 * @param {Object} config 请求的参数
 * @param {*} cancel 当前这次请求的取消凭证
 */
function cancelByConfig({ url, cancelModel, data, params }, cancel) {
  const urls = Object.values(REQUEST_QUEEN)
  if (!urls.length) {
    return
  }
  // 寻找被认定为相同的请求
  const equals = urls.filter(item => isEqualXhr({
    url: item.url,
    data: item.data,
    params: item.params
  }, { url, data, params }, cancelModel))

  if (!equals.length) {
    return
  }
  // 中断历史请求
  if (cancelModel % 2 === 1) {
    equals.forEach(item => item.cancel())
  } else {
    // 取消本次请求
    cancel()
  }
}

/**
 * 清空正在请求的队列
 */
function clearQueen() {
  Object.values(REQUEST_QUEEN).forEach(item => item.cancel())
  REQUEST_QUEEN = {}
}

export {
  addByConfig,
  deleteByUUID,
  cancelByConfig,
  clearQueen
}

请求拦截器request.js

import axios from 'axios'
import { isFunction, isPrimitiveButString } from './util'
import {
  addByConfig,
  cancelByConfig,
} from './queen'

function requestTrim(config) {
  const { data, params, autoTrim, trimFn } = config
  // 如果自动去参数
  if (autoTrim || trimFn) {
    try {
      if (isFunction(trimFn)) {
        return trimFn(config)
      }
      if (!String.prototype.trim) {
        String.prototype.trim = function () {
          return this.replace(/^\s+|\s+$/g, '')
        }
      }

      function itTrim(value, key, obj) {
        // 非对象引用和string类型
        if (isPrimitiveButString(value) || value === null) {
          return
        }
        if (typeof value === 'string') {
          return obj[key] = value.trim()
        }
        // FormData数据独特的对比校验
        if (value instanceof FormData) {
          return new Set(Array.from(value.keys())).forEach(name => {
            const valuesByName = value.getAll(name)
            value.delete(name)
            valuesByName.map(item => {
              return item instanceof File ? item : item.trim()
            }).forEach(item => {
              value.append(name, item)
            })
          })
        }
        // 其他的引用类型用这个校验
        // TODO 还没有跑过所有的引用类型数据,所以这里的代码可能会有漏洞
        Object.entries(value).forEach(item => itTrim(item[1], item[0], value))
      }

      itTrim(data)
      itTrim(params)
    } catch (error) {
      console.error('自动trim失败', error)
    }
  }
}

function requestInfoLog(config) {
  if (config.log && process.env.NODE_ENV === "development") {
    config.timestamp = +new Date()
    console.log('start... ', config.url, {
      pagePath: location.href,
      requestUrl: config.url,
      timestamp: config.timestamp,
      data: config.data,
      params: config.params,
      method: config.method
    });
  }
}

function openLoading () {

}

export default function requestInterceptor(axiosInstance) {
  axiosInstance.interceptors.request.use(config => {
    const {
      beforeAxiosSend, cancelModel, log, onHeaderCreated
    } = config

    if (log) {
      console.log(config)
    }

    // 参数去空格
    requestTrim(config)

    // 缓存请求的取消凭证
    let cancel
    config.cancelToken = new axios.CancelToken(c => cancel = c)

    // 中断策略
    if(cancelModel) {
      cancelByConfig(config, cancel)
    }

    config.cancel = cancel

    // 加入队列
    addByConfig(config)
   
    requestInfoLog(config)

    // 开启加载
    openLoading()

    if (isFunction(onHeaderCreated)) {
      onHeaderCreated(config)
    }
    if (isFunction(beforeAxiosSend)) {
      beforeAxiosSend(config)
    }
    return config
  }, error => {
    return Promise.reject(error);
  })
}

响应拦截器response.js

import axios from 'axios'
import { isFunction } from './util';
import {
  clearQueen
} from './queen'

/**
 * 打印响应数据
 * @param {Object} response 
 */
function responseInfoLog(response) {
  if (response.config.log && process.env.NODE_ENV === "development") {
    console.log('done... ' + response.config.url, {
      pagePath: location.href,
      requestUrl: response.config.url,
      timestamp: response.config.timestamp,
      time: new Date() - response.config.timestamp,
      status: response.status,
      data: response.data
    });
  }
}

export default function responseInterceptor(axiosInstance) {
  axiosInstance.interceptors.response.use(response => {
    const { validateBusiStatus } = response.config
    // 检测业务操作是否成功
    // 认定不成功的话会使其进去catch后调
    if (isFunction(validateBusiStatus)) {
      if (!validateBusiStatus(response)) {
        return Promise.reject(response)
      }
    }
    // 打印成功日志
    responseInfoLog(response)
    return Promise.resolve(response.data);
  }, error => {
    // TODO 打印失败日志

    // 如果不是请求被取消
    if (!axios.isCancel(error)) {
      const { config, response } = error
      // 当请求被响应
      // 其他原因可能导致请求异常,此时无response
      if (response) {
        const { status, data } = response
        if (status === 401) {
          // TODO 配置化
          // 清空所有正在请求队列
          clearQueen()
          // TODO 未登录提醒
          // ...
          // TODO 重定向到登录页
          // store.dispatch('logout')
          return Promise.reject(data.message || '登录失败')
        }
      }
      // 默认提示
      if (config && isFunction(config.showErrorMsg)) {
        config.showErrorMsg(error)
      }
    } else {
      // 接口被取消也执行下面的Promise.reject(error),终止promise调用链
    }
    Promise.reject(error)
  })
}

用到的一些util方法

function guid() {
  function S4() {
    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
  }
  return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
}

function isObject(val) {
  return val !== null && typeof val === 'object';
}

const toString = Object.prototype.toString;

function isPlainObject(val) {
  if (toString.call(val) !== '[object Object]') {
    return false;
  }

  var prototype = Object.getPrototypeOf(val);
  return prototype === null || prototype === Object.prototype;
}

function isFormData(val) {
  return (typeof FormData !== 'undefined') && (val instanceof FormData);
}

function isFunction(fn) {
  return typeof fn === 'function'
}

function isPrimitiveButString(value) {
  const type = typeof value
  return (
    type === 'number' ||
    type === 'symbol' ||
    type === 'boolean' ||
    type === 'function' ||
    type === 'undefined'
  )
}

function isPrimitive(value) {
  const type = typeof value
  return (
    type === 'string' ||
    type === 'number' ||
    type === 'symbol' ||
    type === 'boolean' ||
    type === 'function' ||
    type === 'undefined'
  )
}

// 深度比较
function isEqualObj(a, b) {
  if (a === b) {
    return true
  }
  if (isPrimitive(a) || isPrimitive(b)) {
    return false
  }
  // obj
  if (isPlainObject(a)) {
    if (!isPlainObject(b)) {
      return false
    }
    const aKeys = Object.keys(a)
    const bKeys = Object.keys(b)
    if (aKeys.length !== bKeys.length) {
      return false
    }
    for(let key of aKeys) {
      if (!isEqualObj(a[key], b[key])) {
        return false
      }
    }
    return true
  }

  // TODO 使用中如果遇到了未排序引起了 相等传参但是判断失败,则进行对等排序
  const aArr = Array.from(a)
  const bArr = Array.from(b)
  if (aArr.length !== bArr.length) {
      return false
  }
  for(let index of aArr) {
      if (!isEqualObj(aArr[index], bArr[index])) {
        return false
      }
  }
  return true
  
}

export {
  guid,
  isObject,
  isFormData,
  isFunction,
  isPrimitive,
  isPrimitiveButString,
  isEqualObj
}