[前端项目创新]如何避免axios拦截器上的代码过多

2,007 阅读7分钟

前言

axios的拦截器使用场景很多。就拿自己负责的项目来说,需要通过拦截器实现的功能有:

  1. 请求返回登录权限失效时,网站切换到登陆页面
  2. 处理数据竞态问题
  3. 请求失败时,以统一的UI和UR以及文案格式呈现失败提示和失败原因
  4. 按照统一的数据结构处理响应数据,然后返回给业务层

每一个功能都需要多行的代码去实现。如果我们把这些功能都写在一个拦截器的function中,会导致文件代码过多,不利于维护。自己最近通过阅读axios的源码,顺利解决了这个问题。

源码分析

以下展示和分析涉及到拦截器的源码:

lib\axios.js

这是axios的主文件,即我们通过requireimportnode_modules引入axios都是引入这个文件。

'use strict';

var utils = require('./utils');
var bind = require('./helpers/bind');
var Axios = require('./core/Axios');
var mergeConfig = require('./core/mergeConfig');
var defaults = require('./defaults');

/**
 * Create an instance of Axios
 *
 * @param {Object} defaultConfig The default config for the instance
 * @return {Axios} A new instance of Axios
 */
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  // 创建一个实例,该实例其实是以上面的context为执行上下文的Axios.prototype.request方法
  // Axios.prototype.request是发出请求的方法
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  // utils.extend方法:把第二个参数的属性复制到第一个参数中,如果被复制的属性的值的类型是function,则把该值的上下文设置成第三个参数
  // 这里就是把Axios.prototype中的值和绑定context为执行上下文的方法复制到instance中
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  utils.extend(instance, context);

  return instance;
}

// Create the default instance to be exported
var axios = createInstance(defaults);

// Expose Axios class to allow class inheritance
axios.Axios = Axios;

// Factory for creating new instances
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

module.exports = axios;

// Allow use of default import syntax in TypeScript
module.exports.default = axios;

从上可见,我们不管通过axios(config)还是axios(url[, config])方法发出请求,都是调用Axios.prototype.request方法。接下来看定义Axios类的源代码。

lib\core\Axios.js

var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');
var mergeConfig = require('./mergeConfig');

/**
 * Create a new instance of Axios
 *
 * @param {Object} instanceConfig The default config for the instance
 * Axios类的实例的初始化过程就是声明了存放配置的defaults以及存放请求和相应拦截器的对象
 */
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

Axios.prototype.request = function request(config) {
	//....
    // 此处代码接下来会分析
}

module.exports = Axios;

从上可见,我们用来注册拦截器的axios.interceptors.use方法是InterceptorManager类的内部方法了。接下来看定义InterceptorManager类的源代码。

lib\core\InterceptorManager.js

'use strict';

var utils = require('./../utils');

function InterceptorManager() {
  // 定义类型为数组的handlers去存放拦截器
  this.handlers = [];
}

/**
 * Add a new interceptor to the stack
 *
 * @param {Function} fulfilled The function to handle `then` for a `Promise`
 * @param {Function} rejected The function to handle `reject` for a `Promise`
 *
 * @return {Number} An ID used to remove interceptor later
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

/**
 * Remove an interceptor from the stack
 *
 * @param {Number} id The ID that was returned by `use`
 */
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

/**
 * Iterate over all the registered interceptors
 *
 * This method is particularly useful for skipping over any
 * interceptors that may have become `null` calling `eject`.
 *
 * @param {Function} fn The function to call for each interceptor
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
 // 这里的utils.forEach(arr,fn)相当于arr.forEach((arr[index],index,arr)=>fn(arr[index],index,arr))
 // 这里的InterceptorManager.prototype.forEach用于遍历this.handlers。相当于this.handlers.forEach(h=>fn(h))
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

module.exports = InterceptorManager;

从上可知,我们可以用axios.interceptor.use注册多个拦截器。该方法会返回你所注册的拦截器的id。用于之后你想撤销该拦截器时,通过axios.interceptor.eject(id)实现。可是所注册的拦截器彼此之间在请求和相应时会有什么影响还不知道,我们继续看Axios.prototype.request方法。

lib\core\Axios.js

var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');
var mergeConfig = require('./mergeConfig');

/**
 * Create a new instance of Axios
 *
 * @param {Object} instanceConfig The default config for the instance
 * Axios类的实例的初始化过程就是声明了存放配置的defaults以及存放请求和相应拦截器的对象
 */
function Axios(instanceConfig) {
  //...
}

/**
 * Dispatch a request
 *
 * @param {Object} config The config specific for this request (merged with this.defaults)
 */
Axios.prototype.request = function request(config) {
  // ...

  // Hook up interceptors middleware
  // dispatchRequest为发出请求的方法
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  // 把请求拦截器插入到chain的头部
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  // 把相应拦截器插入到chain的尾部
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  // 遍历chain,每次循环都取出两个元素一次放在then的resolve和reject位置上
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

// Provide aliases for supported request methods
// 此处可知axios.delete等方法是调用Array.prototype.request且传入config配置参数实现的
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: (config || {}).data
    }));
  };
});

// 处可知axios.get是调用Array.prototype.request且传入config配置参数实现的
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, data, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

module.exports = Axios;

从上可知,注册的请求和响应拦截器都会放到一个promise链上执行。

下面以图片的形式总结一下:

假设我们注册A、B作为请求中的成功和失败拦截器,C、D作为相应中的成功和失败拦截器。如下所示会放在对应的handlers中。

当调用axios或者axios.request发出请求时,就会运行Array.prototype.request方法。此时在Array.prototype.request中定义的chain会如下变化:

最终chain中的元素会双双取出放到promise链中,然后把promise链返回出去。

注册多个拦截器

由上可知,拦截器无论是request.interceptors还是response.interceptors都可以注册多个。此时,基本思路已经确定了,就是把一个拦截器里的代码按照不同的需求拆分成多个拦截器。例如把上一章节A请求成功拦截器拆分成a1a2,以及把C响应成功拦截器拆分成c1,c2。则我们可以这么做:

const axios=require('axios')

// 注册a1,B
axios.interceptors.request.use(
    function a1(config){
        console.log('a1')
        return config
    },
    function B(err){
        console.log('B')
        return Promise.reject(err)
    }
)
//注册a2
axios.interceptors.request.use(
    function a2(config){
        console.log('a2')
        return config
    },
    undefined
)
//注册c1,D
axios.interceptors.response.use(
    function c1(response){
        console.log('c1')
        return response
    },
    function D(err){
        console.log('D')
        return Promise.reject(err)
    }
)
//注册c2
axios.interceptors.response.use(
    function c2(response){
        console.log('c2')
        return response
    },
    undefined
)

axios.head('https://www.baidu.com')

上面的代码最后会陆续输出:

a2
a1
c1
c2

要注意的是,由于是在promise链上执行的,针对每个请求成功拦截器和响应陈工拦截器的代码中,都必须分别以return configreturn response结尾。保证下一个拦截器能接收到传入形参。

同理,每个请求失败拦截器和响应失败拦截器的代码中,在以return Promise.reject(err)结尾。才能保证err能传入到下一个拦截器的同时,能保证下一个拦截器是请求失败拦截器或响应失败拦截器。如果你在一个响应失败拦截器的代码中以return err结尾,则会传入到下一个响应成功拦截器上执行。

拓展:我在自己的项目里是怎么优化的

我的目录结构:

modules中存放的js文件里面写着对应业务的拦截器。每个拦截器的名称统一:

  • 请求成功拦截器:requestSuccessHandler
  • 请求失败拦截器:requestFailHandler
  • 响应成功拦截器:responseSuccessHandler
  • 响应失败拦截器:responseFailHandler

以上拦截器如果被定义了都要以export导出去。

handlers\index.js

import axios from 'axios'
import { AXIOS_DEFAULT_CONFIG } from 'Config/index'
import baseHandler from './modules/base.js' //把base.js文件单独引出来

// 扫描引入除base.js以外的在modules文件夹的文件
const files = require.context('./modules', false, /(?<!base)\.js$/)
const handlers = files.keys().reduce((cur, key) => {
  cur.push(files(key).default)
  return cur
}, [])

// AXIOS_DEFAULT_CONFIG为默认参数创建axios实例
const axiosInstance = axios.create(AXIOS_DEFAULT_CONFIG)
// 把base.js中导出的拦截器放在最后,然后遍历注册拦截器
const handlerList = [...handlers, baseHandler]
handlerList.forEach(handler => {
  const { requestSuccessHandler, requestFailHandler, responseSuccessHandler, responseFailHandler } = handler
  if (requestSuccessHandler || requestFailHandler) {
    axiosInstance.interceptors.request.use(requestSuccessHandler, requestFailHandler)
  }
  if (responseSuccessHandler || responseFailHandler) {
    axiosInstance.interceptors.response.use(responseSuccessHandler, responseFailHandler)
  }
})
export default axiosInstance

为什么要把base.js放在最后?看我写在base.js里面的代码:

const responseSuccessHandler = function (reponse) {
  return Promise.resolve({ err: null, res: reponse.data })
}

const responseFailHandler = function (err) {
  return Promise.resolve({ err, res: null })
}

export default {
  responseSuccessHandler,
  responseFailHandler
}

因为base.js用于把返回的数据处理成统一格式返回给业务层。因此要保证base.js里面的responseSuccessHandlerresponseFailHandler要最后一个执行。才能保证请求结果以{err,res}格式的对象返回出去。才能保证业务层代码可以以下面的代码格式接受请求结果:

const {err,res}=await axios(config)

后记

其实把拦截器拆分成多个文件的好处就是方便维护。在提高可读性同时减少多人协作带来的合并冲突。之后自己也会写更多关于前端项目创新的文章。