axios拦截器_Axios源码解析 —— 一个小而美的HttpClient

使用HTTP协议沟通交流信息在目前的程序开发中几乎是随处可见 —— web 前端 / Android / IOS 与服务器后端交互;后端服务集群中,微服务之间也常常使用 Http 协议沟通。

现在假如让你写一个http request发送给某个服务端,你会怎么做?—— 通常我们会使用现有的流行的http客户端库,比如 Java开发中 Apache HttpComponents HttpClient 或者 OkHttp,Android 还流行 Retrofit,而 web前端最流行的是 Axios。

尽管语言不同,运行环境不同,但是作为Http客户端,它们的职责是类似的,常见的功能需求也是类似的。所以,只要你能理解任意一个就能触类旁通,举一反三。

今天我们一起来阅读Axios的源码(我选择的是 0.18 这个比较稳定的版本),并理解和学习它的代码设计。

* Axios是个JS库,里面用到了不少JS的特性 —— 后端的同学如果不想学习 JS ,可以跳过代码的部分,重点学习 Axios 的设计思想

首先看看作为module.exports的 axios.js 文件

'use strict';

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

/**
 * 创建一个Axios实例
 */
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);

  //实例的底子实际上是 Axios.prototype.request 这个方法
  var instance = bind(Axios.prototype.request, context);

  //把Axios.prototype的所有方法和域都复制到实例中
  utils.extend(instance, Axios.prototype, context);

  //把context的所有域都复制到实例中
  utils.extend(instance, context);

  return instance;
}

//创建默认实例
var axios = createInstance(defaults);

//暴露 Axios类 方便外部使用继承
axios.Axios = Axios;

// 暴露创建Axios实例的工厂方法
axios.create = function create(instanceConfig) {
  return createInstance(utils.merge(defaults, instanceConfig));
};

// 暴露 取消request相关的方法
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

// 暴露 all 和 spread 这2个工具方法
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = require('./helpers/spread');

module.exports = axios;

module.exports.default = axios;

这个文件主要作用就是把暴露出来的API定义好了:

  1. 暴露了一个默认的Axios实例
  2. 暴露了创建Axios实例的工厂方法
  3. 暴露了Axios类方便继承(一般很少用到)
  4. 暴露了Cancel相关的API(据说会废弃掉,本文先不讨论这个特性)

其中最复杂的是创建Axios实例的方法实现,里面用到了 两个自定义的工具方法 bind 和 utils.extend

(不熟悉 js function apply 和 call 的先阅读官方文档
JavaScript/Reference/Global_Objects/Function/apply JavaScript/Reference/Global_Objects/Function/call )

bind

//这是个高阶函数,返回一个wrap函数
function bind(fn, thisArg) {
  return function wrap() {
    // 制作一份参数拷贝
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }
    //以 thisArg 为 this, 以 args 为参数,调用 fn
    return fn.apply(thisArg, args);
  };
};

所以 bind 的作用就是将 参数中的 方法和thisArg组装成一个新的函数,对这个函数的任何调用都会保证使用 thisArg 作为 this

—— 熟悉新版 js 的同学可能会发现这不就是 Function.prototype.bind 的功能吗?

思考:为什么要重复造个轮子?

看了官方文档就能发现,Function.prototype.bind 只能兼容 IE9以上 的版本(而 apply可以兼容到 IE5.5),所以为了让 IE9 以下也能使用 axios,axios选择了"重复"造这个轮子

*启发:以后在第三方库的代码里发现“重复造现有语言API的轮子” 都可以尝试猜测一下是不是为了兼容旧版浏览器(JS)或者是仍然在使用语言旧版本(Java 5 vs Java 8)

utils.extend

function extend(a, b, thisArg) {
// forEach 也是 utils的一个方法,用于遍历数组或者对象属性
  forEach(b, function assignValue(val, key) {
    // 如果是函数,就bind好thisArg然后赋值给 a 
    if (thisArg && typeof val === 'function') {
      a[key] = bind(val, thisArg);
    } else {
    // 如果不是函数,直接复制
      a[key] = val;
    }
  });
  return a;
}

可以看出 extend 就是一个简单的复制函数 —— 把 b 的属性都复制到 a 上(属性名一样的就会覆盖掉)


创建 Axios 实例的方法中,我们看到是 以 Axios.prototype.request 为底,复制了 Axios.prototype 和 new Axios(config) 的 属性

为什么不直接使用 new Axios(config) 呢?
读者可以先思考一下,我暂时先不解答这个问题,继续往下看源码。

接下来看的是 core/Axios.js

这个文件定义了Axios类,我们一起看看它怎么写的

'use strict';

var defaults = require('./../defaults');
var utils = require('./../utils');
var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');

//构造器
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  //初始化了 interceptors (拦截器)
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

// Axios的核心方法 ——
// 将 request发起,request拦截器和 response拦截器 链式拼接,最后用 promise 串起来调用
Axios.prototype.request = function request(config) {
  /*eslint no-param-reassign:0*/
  // Allow for axios('example/url'[, config]) a la fetch API
  if (typeof config === 'string') {
    config = utils.merge({
      url: arguments[0]
    }, arguments[1]);
  }

  config = utils.merge(defaults, {method: 'get'}, this.defaults, config);
  config.method = config.method.toLowerCase();

  //链表初始值是 dispatchRequest 和 undefined 组成的 2个元素的数组
  // 为什么要加个 undefined ? 接着往下看
  var chain = [dispatchRequest, undefined];
  
  // promise 链的第一个promise将 config 传入
  var promise = Promise.resolve(config);

  // 将request拦截器逐一插入到 链表的头部
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

// 将request拦截器逐一插入到 链表的尾部
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    // 从链表中从头连续取出2个元素,第一个作为 promise 的 resolve handler, 第二个做 reject handler
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

// 用更优雅更短的代码定义了 Axios.prototype.delete, Axios.prototype.get, Axios.prototype.head, Axios.prototype.options
// 注意这四个方法都不需要 请求负载(request payload)
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});

// 用更优雅更短的代码定义了  Axios.prototype.put, Axios.prototype.post, Axios.prototype.patch
// 注意这四个方法都有 请求负载(request payload)
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

module.exports = Axios;

Axios.js 里最核心的代码就是 Axios.prototype.request

我们一起看看 Axios.prototype.request 的精妙设计:

  1. 用一个链表 把 request拦截器(interceptors.request) + 真正发起request(dispatchRequest) + response拦截器(interceptors.response) 连起来
  2. 用 promise 逐个调用这个链表 —— 完美的异步调用链(promise的异步特性完美适合网络请求这种场景)
  3. 返回 promise 给调用者

先回答一下上面的问题,为什么不直接使用 new Axios(config) 作为 Axios 实例 而是 以 Axios.prototype.request 为底?

- 答案: 因为这样 axios 本身就是个函数可以直接使用 —— 提高调用者的体验

举例:调用者 import或require axios 后,可以直接这么用

import axios from 'axios';

axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
  // ...other config
});

因为 axios 就是 Axios.prototype.request ,其实上面代码等同于

axios.request({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
  // ...other config
});
Axios.prototype.request 用一个链表 把 request拦截器(interceptors.request) + 真正发起request(dispatchRequest) + response拦截器(interceptors.response) 连起来

接着我们看看 dispatchRequest 的内部实现

'use strict';

var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
var isAbsoluteURL = require('./../helpers/isAbsoluteURL');
var combineURLs = require('./../helpers/combineURLs');

// 暂时忽略
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

/**
 * 使用配置的 adapter 发起request, 返回一个 Promise
 */
module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  // 如果配置了 baseURL 就拼装一下
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    config.url = combineURLs(config.baseURL, config.url);
  }

  config.headers = config.headers || {};

  // 对即将发起的请求的数据和header 做 预处理 
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );

  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

// 获得适配器
  var adapter = config.adapter || defaults.adapter;

// 使用适配器 发起request
  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);

    // 对 返回的数据和header 做 后处理
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);

      // 如果请求错误且有数据,也做后处理
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });
};

dispatchRequest 的代码也清晰易懂,其中个人觉得最棒的功能在于 request 和 response 的处理 —— transformRequest 和 transformResponse

这两个功能都是可配置的,也都有默认的配置,一起看看 defaults.js 里这部分的内容

transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Content-Type');
    if (utils.isFormData(data) ||
      utils.isArrayBuffer(data) ||
      utils.isBuffer(data) ||
      utils.isStream(data) ||
      utils.isFile(data) ||
      utils.isBlob(data)
    ) {
      return data;
    }
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    // 根据数据类型 补充request header
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    // 根据数据类型 补充request header 且 转换成 json string
    if (utils.isObject(data)) {
      setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
      return JSON.stringify(data);
    }
    return data;
  }],

  // 默认后端 response 的数据 是 json 格式
  transformResponse: [function transformResponse(data) {
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ } // “竟然” 默认吞掉了 error 
    }
    return data;
  }],

知道这2个功能后,我们可以

1. 自定义请求数据的格式,然后在 transformRequest 里识别并且对数据做预处理,加header 等等,如

transformRequest: [function transformRequest(data, headers) {
    ...
    if(myUtil.isMyDataFormat(data)){
       data = myUtil.proceess(data);
       headers['specialHeaderName'] = "specialHeaderValue";
    }
    return data;
  }],

2. 对 response 的数据做处理

来自我的真实案例,由于网络抖动原因,前端收到的数据偶尔是个不完整的 json 数据,且默认的 transformResponse 对错误json 不作处理,于是我自定义了如下代码

transformResponse: [function transformResponse(data) {
    /*eslint no-param-reassign:0*/
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) {  
         return { badJson: true, data: data }
      }  
    }
    return data;
  }],

一旦 JSON.parse 出错,就返回一个 object,且 有个属性 badJson 标记为 true。 然后我在之后会讲到的 response interceptor 里检测 badJson 然后做进一步的处理。

3. 还可以有很多其他骚操作

这两个功能可以让我们把 request / response data 和 header 的处理集中在 axios 配置里,减少业务代码里的重复逻辑,同时让业务代码专注于业务。


Axios.prototype.request 用一个链表 把 request拦截器(interceptors.request) + 真正发起request(dispatchRequest) + response拦截器(interceptors.response) 连起来

接下来一起看看 拦截器(interceptor)

在 Axios 的构造器里,request 和 response 拦截器就被初始化了

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

可以看到,它们都是 InterceptorManager 实例,一起来看看InterceptorManager 的源码

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

function InterceptorManager() {
  // 内部使用一个简单数组存放所有 handler
  this.handlers = [];
}

/**
 * 加入新的 handler —— 加到数组的尾部,返回所在的下标(即数组最后一位)作为 id
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

/**
 * 根据 id (实际上就是数组下标)废除使用指定的 handler
 * 实际上只是简单地设置为 null 
 */
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null; 
  }
};

/**
 * 遍历所有非null的 handler,并且调用
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

module.exports = InterceptorManager;

可以看到其实代码也是比较简单的,可读性非常好 —— 优秀的代码可读性往往非常好,糟糕的代码才会花里胡哨让人无法阅读。

InterceptorManager把内部handlers数组封装起来,只暴露三个方法让外部调用 —— use, eject, forEach。回顾一下 Axios.prototype.request 里是怎么调用的其中的forEach方法的

//链表初始值是 dispatchRequest 和 undefined 组成的 2个元素的数组
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);
  
// 将request拦截器逐一插入到 链表的头部
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

// 将response 拦截器逐一插入到 链表的尾部
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
 // 从链表中从头连续取出2个元素,第一个作为 promise 的 resolve方法,第二个做 reject方法
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;

* 外部调用 forEach 时,只要知道这个方法能够遍历拦截器里的handlers就够了,无需知道内部细节(比如 handlers 是个数组,以及遍历时会自动跳过 null )

—— 假如以后 InterceptorManager 内部将 handlers 换成链表,树或者其他数据结构,只要forEach对应地更新,那么外部调用就不会有任何影响且无感知 —— 这就是 封装 的优点


接下来,让我们跳出代码层面,从设计层面思考一下 拦截器的意义:

  • request 拦截器可以在 真正发送 request 之前 做一些操作,能做点啥呢?发挥你的想象力!我举几个例子:
  1. 打 log(日志),记录发起request的时间和内容
  2. dispatch 某种 redux action,让 UI 显示 loading 效果
  3. 给 request 加上额外的数据或者header,比如这个web app使用了 JWT (Json Web Token),需要加入 authorization 这个header,就可以这么写
axios.interceptors.request.use(request => {
    if (request.method.toUpperCase() !== 'OPTIONS') {
        const jwt = localStorage.getItem('jwt');
        if (jwt) {
            request.headers.common.authorization = 'Bearer ' + jwt;
        }
    }
    return request;
}, error => {
    return Promise.reject(error);
});
  • response拦截器可以在接收到 response 之后 做一些操作,也举几个例子:
  1. 打 log(日志),记录发起request的时间和内容 —— 结合request 拦截器的log,还能计算出整个 request 花费的时间
  2. dispatch 某种 redux action,让 UI 显示请求完成的效果
  3. 某些时候 request 失败/出错只是因为网络抖动,通常重试一两次就能成功。response拦截器让自动重试功能得以轻易实现 —— retry-axios
  4. 根据 response 里的数据某些特别的标记,更改 response 的状态码 —— 比如上面中提到的 不完整的json数据的案例吗
axios.defaults.transformResponse = [function transformResponse(data) {
    /*eslint no-param-reassign:0*/
    if (typeof data === 'string') {
        try {
            data = JSON.parse(data);
        } catch (e) {
  // 如果 JSON.parse 出错 就标记 badJson 
            return {
                badJson: true,
                message: data
            }
        }
    }
    return data;
}];

axios.interceptors.response.use(response => {
    if (response
        && response.data
        && response.status === 200
        && response.data.badJson) {
  // 如果发现了不完整的 json 数据,就篡改这个 response 状态码为 500      
        response.status = 500;
        return Promise.reject(response);
    } else {
        return response;
    }
}, error => {
    return Promise.reject(error);
});

以上都是些简单的例子,但毫无疑问,拦截器让 Http Client 变得更加强大,在其他的 Http Client 的实现中也是必不可少的一项功能,比如 Java 的 OKHttp (okhttp/interceptors )


axios 的核心功能基本就介绍完了, 当然axios还有很多其他的代码和设计值得学习。比如:

- dispatchRequest 方法里使用了 适配器模式,在 adapters(https://github.com/axios/axios/tree/release/0.18.X/lib/adapters) 文件夹下有2种adpaters,分别对应了 Node 环境和浏览器环境下的底层Http Request 适配器;

- 不少工具方法里大量使用了 正则表达式

- 对 cookie 和 防范xsrf 的支持

总的来说,虽然Axios 的代码从规模上还是比较小的,但是从功能和设计上是非常足够且优雅的,满足了大部分前端开发在http client方面的需求。


第一次写源码解析相关的文章,觉得有点别扭,写的不好,请大家谅解 —— 有什么建议和意见,欢迎留言!

最后, 希望大家不要畏惧阅读源码,大多数优秀的项目的源代码往往写的比较规范、清晰、易懂,大胆去读,多读,可以深入理解并用好这些项目;同时,多模仿这些优秀项目的代码设计和代码规范,让自己的代码也越来越优秀!


相关链接

- Axios:

axios/axios​github.com
dc0e57a7774637c256b1ce1b323a80cd.png

-适配器模式:

https://zh.wikipedia.org/wiki/%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8F​zh.wikipedia.org

- OkHttp :

OkHttp​square.github.io

- Promise:

Using Promises​developer.mozilla.org
3ce1258ec49dfee40897ca1a675b4d4e.png

- retry-axios:

https://github.com/JustinBeckwith/retry-axios​github.com
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值