使用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 为参数,调用 fnreturn 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 APIif (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 handlerpromise = 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;// 使用适配器 发起requestreturn 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 headerif (utils.isURLSearchParams(data)) {setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');return data.toString();}// 根据数据类型 补充request header 且 转换成 json stringif (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() {// 内部使用一个简单数组存放所有 handlerthis.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

-适配器模式:

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

- retry-axios:

https://github.com/JustinBeckwith/retry-axios​github.com

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

  1. axios网络请求框架源码解析

    早期axios0.1.0版本做了对IE浏览器与包含XmlHttpRequest的浏览器的支持.并且做了对请求参数拼接.Json对象序列化等基本功能. 到0.19.0版本时,内部请求已经变为了在Node ...

  2. spring拦截器覆盖_springmvc拦截器及源码分析

    前言 springmvc拦截器是我们项目开发中用到的一个功能,常常用于对Handler进行预处理和后处理.本案例来演示一个较简单的springmvc拦截器的使用,并通过分析源码来探究拦截器的执行顺序是 ...

  3. Mybatis Interceptor 拦截器原理 源码分析

    Mybatis采用责任链模式,通过动态代理组织多个拦截器(插件),通过这些拦截器可以改变Mybatis的默认行为(诸如SQL重写之类的),由于插件会深入到Mybatis的核心,因此在编写自己的插件前最 ...

  4. 第五章 类加载器ClassLoader源码解析

    说明:了解ClassLoader前,先了解 第四章 类加载机制 1.ClassLoader作用 类加载流程的"加载"阶段是由类加载器完成的. 2.类加载器结构 结构:Bootstr ...

  5. okhttp源码解析

    OkHttp是一个非常优秀的网络请求框架,已被谷歌加入到Android的源码中.目前比较流行的Retrofit也是默认使用OkHttp的.所以OkHttp的源码是一个不容错过的学习资源,学习源码之前, ...

  6. OkHttp的运用与原理(cookie、缓存、源码解析)

    简介 作为当下最流行的网络请求底层框架,如何战胜其他框架立于不败之地,被广大人们所认可呢?相较于其他网络框架来说,其具有的优势: 支持对数据的gizp压缩与解压 支持http1.0,http2.0,S ...

  7. Dubbo第三讲:Dubbo的可扩展机制SPI源码解析

    本文是Dubbo第三讲:Dubbo的可扩展机制SPI源码解析 文章目录 1.Dubbo SPI机制 1.1.Dubbo具有良好拓展性的原因 1.2.Dubbo SPI和Java SPI的区别? 1.3 ...

  8. Spring AOP源码解析-拦截器链的执行过程

    一.简介 在前面的两篇文章中,分别介绍了 Spring AOP 是如何为目标 bean 筛选合适的通知器,以及如何创建代理对象的过程.现在得到了 bean 的代理对象,且通知也以合适的方式插在了目标方 ...

  9. Spring MVC源码解析——HandlerMapping(处理器映射器)

    Sping MVC 源码解析--HandlerMapping处理器映射器 1. 什么是HandlerMapping 2. HandlerMapping 2.1 HandlerMapping初始化 2. ...

  10. 【vuejs深入三】vue源码解析之二 htmlParse解析器的实现

    写在前面 一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残. 昨天博主分析了一下在vue中,最为基础核心的api,parse函数,它的作用是将vue的模板字符串转换成ast,从而 ...

最新文章

  1. 2019最新 iOS Native项目集成Unity3D
  2. Java反射机制的使用方法
  3. python人脸识别训练模型生产_深度学习-人脸识别DFACE模型pytorch训练(二)
  4. 【BZOJ4407】于神之怒加强版
  5. CVPR 2020 论文大盘点-超分辨率篇
  6. ajax五种回调函数,Ajax的回调函数
  7. Alibaba代码规约插件使用IDEA
  8. Xpath在选择器中正确,在代码中返回的是空列表问题
  9. 第十三章 确定性策略梯度(Deterministic Policy Gradient Algorithms,DPG)-强化学习理论学习与代码实现(强化学习导论第二版)
  10. 天题系列:Substring with Concatenation of All Words
  11. unity 使用粒子系统 实现一个火焰燃烧效果
  12. openwrt路由器(红米AC2100)折腾全程——多拨、ipv6负载均衡、ipv6 nat6、ddns、端口转发
  13. 一键重装系统win7旗舰版系统教程
  14. openfeign接口启动报错: is not assignable to interface feign.hystrix.FallbackFactory
  15. LintCode 木材加工
  16. 调查:秋色园QBlog 博客开源不开源,您的建议是?
  17. C语言实现简单的四则运算计算器
  18. 开源飞控ardupilot避障传感器的使用-1乐迪超声波避障SUI04
  19. 老车厂福特为何要选择阿里斑马的智能车机系统? 这才是你该知道的原因
  20. 正则表达式\S\s的意思

热门文章

  1. 生产环境中CentOS5.6下配置LVS(续)
  2. 在Vmware中安装archlinux(2008.3core)的流程与心得
  3. Silverlight 3.0正式版RTW的发布日期
  4. MySQL 和 MySQL Workbench图形化安装教程
  5. 【Linux】03 文件权限
  6. vue4 库模式打包_Steam“小模式”游戏库回归 界面轻快简洁可随时切换
  7. linux把一个文件拷贝到另一个目录,linux把某个文件拷贝到不同的目录下面
  8. 一个软件工程师在北京的反省
  9. 计算字符串的相似度-两种解法
  10. ARM 和 RISC-V 公然开撕,GNOME 之父指责 ARM