使用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定义好了:
- 暴露了一个默认的Axios实例
- 暴露了创建Axios实例的工厂方法
- 暴露了Axios类方便继承(一般很少用到)
- 暴露了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 的精妙设计:
- 用一个链表 把 request拦截器(interceptors.request) + 真正发起request(dispatchRequest) + response拦截器(interceptors.response) 连起来
- 用 promise 逐个调用这个链表 —— 完美的异步调用链(promise的异步特性完美适合网络请求这种场景)
- 返回 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 之前 做一些操作,能做点啥呢?发挥你的想象力!我举几个例子:
- 打 log(日志),记录发起request的时间和内容
- dispatch 某种 redux action,让 UI 显示 loading 效果
- 给 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 之后 做一些操作,也举几个例子:
- 打 log(日志),记录发起request的时间和内容 —— 结合request 拦截器的log,还能计算出整个 request 花费的时间
- dispatch 某种 redux action,让 UI 显示请求完成的效果
- 某些时候 request 失败/出错只是因为网络抖动,通常重试一两次就能成功。response拦截器让自动重试功能得以轻易实现 —— retry-axios
- 根据 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/axiosgithub.com![dc0e57a7774637c256b1ce1b323a80cd.png](https://i-blog.csdnimg.cn/blog_migrate/27f5289b9b78f8c3dc9f663012529a89.png)
-适配器模式:
https://zh.wikipedia.org/wiki/%E9%80%82%E9%85%8D%E5%99%A8%E6%A8%A1%E5%BC%8Fzh.wikipedia.org- OkHttp :
OkHttpsquare.github.io- Promise:
Using Promisesdeveloper.mozilla.org![3ce1258ec49dfee40897ca1a675b4d4e.png](https://i-blog.csdnimg.cn/blog_migrate/379d70cd7c85850b9bdd419301b48d68.jpeg)
- retry-axios:
https://github.com/JustinBeckwith/retry-axiosgithub.com