Axios源码解析 —— 一个小而美的HttpClient

2022-12-24 0 1,017

采用HTTP协定沟通交流重要信息在现阶段的软件合作开发中基本上是经常出现 —— web 后端 / Android / IOS 与服务项目器后端可视化;后端服务项目软件产业中,微服务项目间也经常采用 Http 协定沟通交流。

那时倘若让你写两个http request发送到某一服务项目器端,你会怎么做?—— 一般来说他们会采用原有的盛行的http应用程序库,比如说 Java合作开发中 Apache HttpComponents HttpClient 或是 OkHttp,Android 还盛行 Retrofit,而 web后端最盛行的是 Axios。

虽然词汇相同,运转自然环境相同,但做为Http应用程序,它的职能是类似于的,常用的机能市场需求也是类似于的。所以,假如你能认知任一两个就能Monestier,总结经验。

那时他们一起来写作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:

-适配器模式:

– OkHttp :

– Promise:

– retry-axios:

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务