- 前言
你好,我是若川。这是
学习源码整体架构系列
第六篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。
学习源码整体架构系列
文章如下:
1.学习 jQuery 源码整体架构,打造属于自己的 js 类库
2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库
3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库
4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK
感兴趣的读者可以点击阅读。下一篇可能是vue-router
源码。
本文比较长,手机上阅读,可以直接看文中的几张图即可。建议点赞或收藏后在电脑上阅读,按照文中调试方式自己调试或许更容易吸收消化。
导读
文章详细介绍了 axios
调试方法。详细介绍了 axios
构造函数,拦截器,取消等功能的实现。最后还对比了其他请求库。
本文学习的版本是v0.19.0
。克隆的官方仓库的master
分支。
截至目前(2019年12月14日),最新一次commit
是2019-12-09 15:52 ZhaoXC
dc4bc49673943e352
,fix: fix ignore set withCredentials false (#2582)
。
本文仓库在这里若川的 axios-analysis github 仓库。求个star
呀。
如果你是求职者,项目写了运用了axios
,面试官可能会问你:
1.为什么
axios
既可以当函数调用,也可以当对象使用,比如axios({})
、axios.get
。2.简述
axios
调用流程。3.有用过拦截器吗?原理是怎样的?
4.有使用
axios
的取消功能吗?是怎么实现的?5.为什么支持浏览器中发送请求也支持
node
发送请求?诸如这类问题。
- chrome 和 vscode 调试 axios 源码方法
前不久,笔者在知乎回答了一个问题一年内的前端看不懂前端框架源码怎么办?
推荐了一些资料,阅读量还不错,大家有兴趣可以看看。主要有四点:
1.借助调试
2.搜索查阅相关高赞文章
3.把不懂的地方记录下来,查阅相关文档
4.总结
看源码,调试很重要,所以笔者详细写下 axios
源码调试方法,帮助一些可能不知道如何调试的读者。
2.1 chrome 调试浏览器环境的 axios
调试方法
axios
打包后有sourcemap
文件。
1 | bash复制代码# 可以克隆笔者的这个仓库代码 |
本文就是通过上述的例子axios/sandbox/client.html
来调试的。
顺便简单提下调试example
的例子,虽然文章最开始时写了这部分,后来又删了,最后想想还是写下。
找到文件axios/examples/server.js
,修改代码如下:
1 | js复制代码server = http.createServer(function (req, res) { |
1 | bash复制代码# 上述安装好依赖后 |
打开http://localhost:5000,然后就可以开心的在Chrome
浏览器中调试examples
里的例子了。
axios
是支持 node
环境发送请求的。接下来看如何用 vscode
调试 node
环境下的axios
。
2.2 vscode 调试 node 环境的 axios
在根目录下 axios-analysis/
创建.vscode/launch.json
文件如下:
1 | json复制代码{ |
按F5
开始调试即可,按照自己的情况,单步跳过(F10)
、单步调试(F11)
断点调试。
其实开源项目一般都有贡献指南axios/CONTRIBUTING.md
,笔者只是把这个指南的基础上修改为引用sourcemap
的文件可调试。
- 先看 axios 结构是怎样的
1 | bash复制代码git clone https://github.com/lxchuan12/axios-analysis.git |
按照上文说的调试方法, npm start
后,直接在 chrome
浏览器中调试。
打开 http://localhost:3000,在控制台打印出axios
,估计很多人都没打印出来看过。
1 | js复制代码console.log({axios: axios}); |
层层点开来看,axios
的结构是怎样的,先有一个大概印象。
笔者画了一张比较详细的图表示。
看完结构图,如果看过jQuery
、underscore
和lodash
源码,会发现其实跟axios
源码设计类似。
jQuery
别名 $
,underscore
loadsh
别名 _
也既是函数,也是对象。比如jQuery
使用方式。$('#id')
, $.ajax
。
接下来看具体源码的实现。可以跟着断点调试一下。
断点调试要领:
赋值语句可以一步跳过,看返回值即可,后续详细再看。
函数执行需要断点跟着看,也可以结合注释和上下文倒推这个函数做了什么。
- axios 源码 初始化
看源码第一步,先看package.json
。一般都会申明 main
主入口文件。
1 | json复制代码// package.json |
主入口文件
1 | js复制代码// index.js |
4.1 lib/axios.js
主文件
axios.js
文件 代码相对比较多。分为三部分展开叙述。
- 第一部分:引入一些工具函数
utils
、Axios
构造函数、默认配置defaults
等。- 第二部分:是生成实例对象
axios
、axios.Axios
、axios.create
等。- 第三部分取消相关API实现,还有
all
、spread
、导出等实现。
4.1.1 第一部分
引入一些工具函数utils
、Axios
构造函数、默认配置defaults
等。
1 | js复制代码// 第一部分: |
4.1.2 第二部分
是生成实例对象 axios
、axios.Axios
、axios.create
等。
1 | js复制代码/** |
这里简述下工厂模式。axios.create
,也就是用户不需要知道内部是怎么实现的。
举个生活的例子,我们买手机,不需要知道手机是怎么做的,就是工厂模式。
看完第二部分,里面涉及几个工具函数,如bind
、extend
。接下来讲述这几个工具方法。
4.1.3 工具方法之 bind
axios/lib/helpers/bind.js
1 | js复制代码'use strict'; |
传递两个参数函数和thisArg
指向。
把参数arguments
生成数组,最后调用返回参数结构。
其实现在 apply
支持 arguments
这样的类数组对象了,不需要手动转数组。
那么为啥作者要转数组,为了性能?当时不支持?抑或是作者不知道?这就不得而知了。有读者知道欢迎评论区告诉笔者呀。
关于apply
、call
和bind
等不是很熟悉的读者,可以看笔者的另一个面试官问系列
。
举个例子
1 | js复制代码function fn(){ |
4.1.4 工具方法之 utils.extend
axios/lib/utils.js
1 | js复制代码function extend(a, b, thisArg) { |
其实就是遍历参数 b
对象,复制到 a
对象上,如果是函数就是则用 bind
调用。
4.1.5 工具方法之 utils.forEach
axios/lib/utils.js
遍历数组和对象。设计模式称之为迭代器模式。很多源码都有类似这样的遍历函数。比如大家熟知的jQuery
$.each
。
1 | js复制代码/** |
如果对Object
相关的API
不熟悉,可以查看笔者之前写过的一篇文章。JavaScript 对象所有API解析
4.1.6 第三部分
取消相关API实现,还有all
、spread
、导出等实现。
1 | js复制代码// Expose Cancel & CancelToken |
这里介绍下 spread
,取消的API
暂时不做分析,后文再详细分析。
假设你有这样的需求。
1 | js复制代码function f(x, y, z) {} |
那么可以用spread
方法。用法:
1 | js复制代码axios.spread(function(x, y, z) {})([1, 2, 3]); |
实现也比较简单。源码实现:
1 | js复制代码/** |
上文var context = new Axios(defaultConfig);
,接下来介绍核心构造函数Axios
。
4.2 核心构造函数 Axios
axios/lib/core/Axios.js
构造函数Axios
。
1 | js复制代码function Axios(instanceConfig) { |
1 | js复制代码Axios.prototype.request = function(config){ |
接下来看拦截器部分。
4.3 拦截器管理构造函数 InterceptorManager
请求前拦截,和请求后拦截。
在Axios.prototype.request
函数里使用,具体怎么实现的拦截的,后文配合例子详细讲述。
如何使用:
1 | js复制代码// Add a request interceptor |
如果想把拦截器,可以用eject
方法。
1 | js复制代码const myInterceptor = axios.interceptors.request.use(function () {/*...*/}); |
拦截器也可以添加自定义的实例上。
1 | js复制代码const instance = axios.create(); |
源码实现:
构造函数,handles
用于存储拦截器函数。
1 | js复制代码function InterceptorManager() { |
接下来声明了三个方法:使用、移除、遍历。
4.3.1 InterceptorManager.prototype.use 使用
传递两个函数作为参数,数组中的一项存储的是{fulfilled: function(){}, rejected: function(){}}
。返回数字 ID
,用于移除拦截器。
1 | js复制代码/** |
4.3.2 InterceptorManager.prototype.eject 移除
根据 use
返回的 ID
移除 拦截器。
1 | js复制代码/** |
有点类似定时器setTimeout
和 setInterval
,返回值是id
。用clearTimeout
和clearInterval
来清除定时器。
1 | js复制代码// 提一下 定时器回调函数是可以传参的,返回值 timer 是数字 |
4.3.3 InterceptorManager.prototype.forEach 遍历
遍历执行所有拦截器,传递一个回调函数(每一个拦截器函数作为参数)调用,被移除的一项是null
,所以不会执行,也就达到了移除的效果。
1 | js复制代码/** |
- 实例结合
上文叙述的调试时运行npm start
是用axios/sandbox/client.html
路径的文件作为示例的,读者可以自行调试。
以下是一段这个文件中的代码。
1 | js复制代码axios(options) |
5.1 先看调用栈流程
如果不想一步步调试,有个偷巧的方法。
知道 axios
使用了XMLHttpRequest
。
可以在项目中搜索:new XMLHttpRequest
。
定位到文件 axios/lib/adapters/xhr.js
在这条语句 var request = new XMLHttpRequest();
chrome
浏览器中 打个断点调试下,再根据调用栈来细看具体函数等实现。
Call Stack
1 | bash复制代码dispatchXhrRequest (xhr.js:19) |
简述下流程:
Send Request
按钮点击submit.onclick
- 调用
axios
函数实际上是调用Axios.prototype.request
函数,而这个函数使用bind
返回的一个名为wrap
的函数。 - 调用
Axios.prototype.request
- (有请求拦截器的情况下执行请求拦截器),中间会执行
dispatchRequest
方法 dispatchRequest
之后调用adapter (xhrAdapter)
- 最后调用
Promise
中的函数dispatchXhrRequest
,(有响应拦截器的情况下最后会再调用响应拦截器)
如果仔细看了文章开始的axios 结构关系图
,其实对这个流程也有大概的了解。
接下来看 Axios.prototype.request
具体实现。
5.2 Axios.prototype.request 请求核心方法
这个函数是核心函数。
主要做了这几件事:
1.判断第一个参数是字符串,则设置 url,也就是支持
axios('example/url', [, config])
,也支持axios({})
。2.合并默认参数和用户传递的参数
3.设置请求的方法,默认是是
get
方法4.将用户设置的请求和响应拦截器、发送请求的
dispatchRequest
组成Promise
链,最后返回还是Promise
实例。也就是保证了请求前拦截器先执行,然后发送请求,再响应拦截器执行这样的顺序。
也就是为啥最后还是可以
then
,catch
方法的缘故。
1 | js复制代码Axios.prototype.request = function request(config) { |
5.2.1 组成Promise
链,返回Promise
实例
这部分:用户设置的请求和响应拦截器、发送请求的
dispatchRequest
组成Promise
链。也就是保证了请求前拦截器先执行,然后发送请求,再响应拦截器执行这样的顺序也就是保证了请求前拦截器先执行,然后发送请求,再响应拦截器执行这样的顺序
也就是为啥最后还是可以
then
,catch
方法的缘故。
如果读者对Promise
不熟悉,建议读阮老师的书籍《ES6 标准入门》。
阮一峰老师 的 ES6 Promise-resolve 和 JavaScript Promise迷你书(中文版)
1 | js复制代码 // 组成`Promise`链 |
1 | js复制代码var promise = Promise.resolve(config); |
解释下这句。作用是生成Promise
实例。
1 | js复制代码var promise = Promise.resolve({name: '若川'}) |
同样解释下后文会出现的Promise.reject(error);
:
1 | js复制代码Promise.reject(error); |
1 | js复制代码var promise = Promise.reject({name: '若川'}) |
接下来结合例子,来理解这段代码。
很遗憾,在example
文件夹没有拦截器的例子。笔者在example
中在example/get
的基础上添加了一个拦截器的示例。axios/examples/interceptors
,便于读者调试。
1 | bash复制代码node ./examples/server.js -p 5000 |
promise = promise.then(chain.shift(), chain.shift());
这段代码打个断点。
会得到这样的这张图。
特别关注下,右侧,local
中的chain
数组。也就是这样的结构。
1 | js复制代码var chain = [ |
这段代码相对比较绕。也就是会生成如下类似的代码,中间会调用dispatchRequest
方法。
1 | js复制代码// config 是 用户配置和默认配置合并的 |
这里提下promise
then
和catch
知识:
Promise.prototype.then
方法的第一个参数是resolved
状态的回调函数,第二个参数(可选)是rejected
状态的回调函数。所以是成对出现的。
Promise.prototype.catch
方法是.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。
then
方法返回的是一个新的Promise
实例(注意,不是原来那个Promise
实例)。因此可以采用链式写法,即then
方法后面再调用另一个then
方法。
结合上述的例子更详细一点,代码则是这样的。
1 | js复制代码var promise = Promise.resolve(config); |
仔细看这段Promise
链式调用,代码都类似。then
方法最后返回的参数,就是下一个then
方法第一个参数。
catch
错误捕获,都返回Promise.reject(error)
,这是为了便于用户catch
时能捕获到错误。
举个例子:
1 | js复制代码var p1 = new Promise((resolve, reject) => { |
err2
不会捕获到,也就是不会执行,但如果都返回了return Promise.reject(err)
,则可以捕获到。
最后画个图总结下 Promise
链式调用。
小结:1. 请求和响应的拦截器可以写
Promise
。
如果设置了多个请求响应器,后设置的先执行。
如果设置了多个响应拦截器,先设置的先执行。
dispatchRequest(config)
这里的config
是请求成功拦截器返回的。接下来看dispatchRequest
函数。
5.3 dispatchRequest 最终派发请求
这个函数主要做了如下几件事情:
1.如果已经取消,则
throw
原因报错,使Promise
走向rejected
。2.确保
config.header
存在。3.利用用户设置的和默认的请求转换器转换数据。
4.拍平
config.header
。5.删除一些
config.header
。6.返回适配器
adapter
(Promise
实例)执行后then
执行后的Promise
实例。返回结果传递给响应拦截器处理。
1 | js复制代码'use strict'; |
5.3.1 dispatchRequest 之 transformData 转换数据
上文的代码里有个函数 transformData
,这里解释下。其实就是遍历传递的函数数组 对数据操作,最后返回数据。
axios.defaults.transformResponse
数组中默认就有一个函数,所以使用concat
链接自定义的函数。
使用:
文件路径axios/examples/transform-response/index.html
这段代码其实就是对时间格式的字符串转换成时间对象,可以直接调用getMonth
等方法。
1 | js复制代码var ISO_8601 = /(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})Z/; |
源码:
就是遍历数组,调用数组里的传递 data
和 headers
参数调用函数。
1 | js复制代码module.exports = function transformData(data, headers, fns) { |
5.3.2 dispatchRequest 之 adapter 适配器执行部分
适配器,在设计模式中称之为适配器模式。讲个生活中简单的例子,大家就容易理解。
我们常用以前手机耳机孔都是圆孔,而现在基本是耳机孔和充电接口合二为一。统一为typec
。
这时我们需要需要一个typec转圆孔的转接口
,这就是适配器。
1 | js复制代码 // adapter 适配器部分 |
接下来看具体的 adapter
。
5.4 adapter 适配器 真正发送请求
1 | js复制代码var adapter = config.adapter || defaults.adapter; |
看了上文的 adapter
,可以知道支持用户自定义。比如可以通过微信小程序 wx.request
按照要求也写一个 adapter
。
接着来看下 defaults.ddapter
。
文件路径:axios/lib/defaults.js
根据当前环境引入,如果是浏览器环境引入xhr
,是node
环境则引入http
。
类似判断node
环境,也在sentry-javascript
源码中有看到。
1 | js复制代码function getDefaultAdapter() { |
xhr
接下来就是我们熟悉的 XMLHttpRequest
对象。
可能读者不了解可以参考XMLHttpRequest MDN 文档。
主要提醒下:onabort
是请求取消事件,withCredentials
是一个布尔值,用来指定跨域 Access-Control
请求是否应带有授权信息,如 cookie
或授权 header
头。
这块代码有删减,具体可以看若川的axios-analysis
仓库,也可以克隆笔者的axios-analysis
仓库调试时再具体分析。
1 | js复制代码module.exports = function xhrAdapter(config) { |
而实际上现在 fetch
支持的很好了,阿里开源的 umi-request 请求库,就是用fetch
封装的,而不是用XMLHttpRequest
。
文章末尾,大概讲述下 umi-request
和 axios
的区别。
http
http
这里就不详细叙述了,感兴趣的读者可以自行查看,若川的axios-analysis
仓库。
1 | js复制代码module.exports = function httpAdapter(config) { |
上文 dispatchRequest
有取消模块,我觉得是重点,所以放在最后来细讲:
5.5 dispatchRequest 之 取消模块
可以使用cancel token
取消请求。
axios cancel token API 是基于撤销的 promise
取消提议。
The axios cancel token API is based on the withdrawn cancelable promises proposal.
文档上详细描述了两种使用方式。
很遗憾,在example
文件夹也没有取消的例子。笔者在example
中在example/get
的基础上添加了一个取消的示例。axios/examples/cancel
,便于读者调试。
1 | bash复制代码node ./examples/server.js -p 5000 |
request
中的拦截器和dispatch
中的取消这两个模块相对复杂,可以多调试调试,吸收消化。
1 | js复制代码const CancelToken = axios.CancelToken; |
5.5.1 取消请求模块代码示例
结合源码取消流程大概是这样的。这段放在代码在axios/examples/cancel-token/index.html
。
参数的 config.cancelToken
是触发了source.cancel('哎呀,我被若川取消了');
才生成的。
1 | js复制代码// source.cancel('哎呀,我被若川取消了'); |
5.5.2 接下来看取消模块的源码
看如何通过生成config.cancelToken
。
文件路径:
axios/lib/cancel/CancelToken.js
1 | js复制代码const CancelToken = axios.CancelToken; |
由示例看 CancelToken.source
的实现,
1 | js复制代码CancelToken.source = function source() { |
执行后source
的大概结构是这样的。
1 | js复制代码{ |
接着看 new CancelToken
1 | js复制代码// CancelToken |
发送请求的适配器里是这样使用的。
1 | js复制代码// xhr |
dispatchRequest
中的throwIfCancellationRequested
具体实现:throw 抛出异常。
1 | js复制代码// 抛出异常函数 |
取消流程调用栈
1.source.cancel()
2.resolvePromise(token.reason);
3.config.cancelToken.promise.then(function onCanceled(cancel) {})
最后进入request.abort();``reject(cancel);
到这里取消的流程就介绍完毕了。主要就是通过传递配置参数cancelToken
,取消时才会生成cancelToken
,判断有,则抛出错误,使Promise
走向rejected
,让用户捕获到消息{message: ‘用户设置的取消信息’}。
文章写到这里就基本到接近尾声了。
能读到最后,说明你已经超过很多人啦^_^
axios
是非常优秀的请求库,但肯定也不能满足所有开发者的需求,接下来对比下其他库,看看其他开发者有什么具体需求。
- 对比其他请求库
6.1 KoAjax
FCC成都社区负责人水歌开源的KoAJAX。
如何用开源软件办一场技术大会?
以下这篇文章中摘抄的一段。
前端请求库 —— KoAJAX
国内前端同学最常用的 HTTP 请求库应该是 axios 了吧?虽然它的 Interceptor(拦截器)API 是 .use(),但和 Node.js 的 Express、Koa 等框架的中间件模式完全不同,相比 jQuery .ajaxPrefilter()、dataFilter() 并没什么实质改进;上传、下载进度比 jQuery.Deferred() 还简陋,只是两个专门的回调选项。所以,它还是要对特定的需求记忆特定的 API,不够简洁。
幸运的是,水歌在研究如何用 ES 2018 异步迭代器实现一个类 Koa 中间件引擎的过程中,做出了一个更有实际价值的上层应用 —— KoAJAX。它的整个执行过程基于 Koa 式的中间件,而且它自己就是一个中间件调用栈。除了 RESTful API 常用的 .get()、.post()、.put()、.delete() 等快捷方法外,开发者就只需记住 .use() 和 next(),其它都是 ES 标准语法和 TS 类型推导。
6.2 umi-request 阿里开源的请求库
umi-request
与 fetch
, axios
异同。
不得不说,umi-request
确实强大,有兴趣的读者可以阅读下其源码。
看懂axios
的基础上,看懂umi-request
源码应该不难。
比如 umi-request
取消模块代码几乎与axios
一模一样。
- 总结
文章详细介绍了 axios
调试方法。详细介绍了 axios
构造函数,拦截器,取消等功能的实现。最后还对比了其他请求库。
最后画个图总结一下axios的总体大致流程。
解答下文章开头提的问题:
如果你是求职者,项目写了运用了axios
,面试官可能会问你:
1.为什么
axios
既可以当函数调用,也可以当对象使用,比如axios({})
、axios.get
。答:
axios
本质是函数,赋值了一些别名方法,比如get
、post
方法,可被调用,最终调用的还是Axios.prototype.request
函数。2.简述
axios
调用流程。答:实际是调用的
Axios.prototype.request
方法,最终返回的是promise
链式调用,实际请求是在dispatchRequest
中派发的。3.有用过拦截器吗?原理是怎样的?
答:用过,用
axios.interceptors.request.use
添加请求成功和失败拦截器函数,用axios.interceptors.response.use
添加响应成功和失败拦截器函数。在Axios.prototype.request
函数组成promise
链式调用时,Interceptors.protype.forEach
遍历请求和响应拦截器添加到真正发送请求dispatchRequest
的两端,从而做到请求前拦截和响应后拦截。拦截器也支持用Interceptors.protype.eject
方法移除。4.有使用
axios
的取消功能吗?是怎么实现的?答:用过,通过传递
config
配置cancelToken
的形式,来取消的。判断有传cancelToken
,在promise
链式调用的dispatchRequest
抛出错误,在adapter
中request.abort()
取消请求,使promise
走向rejected
,被用户捕获取消信息。5.为什么支持浏览器中发送请求也支持
node
发送请求?答:
axios.defaults.adapter
默认配置中根据环境判断是浏览器还是node
环境,使用对应的适配器。适配器支持自定义。
回答面试官的问题,读者也可以根据自己的理解,组织语言,笔者的回答只是做一个参考。
axios
源码相对不多,打包后一千多行,比较容易看完,非常值得学习。
建议 clone
若川的 axios-analysis github 仓库,按照文中方法自己调试,印象更深刻。
基于Promise
,构成Promise
链,巧妙的设置请求拦截,发送请求,再试试响应拦截器。
request
中的拦截器和dispatch
中的取消这两个模块相对复杂,可以多调试调试,吸收消化。
axios
既是函数,是函数时调用的是Axios.prototype.request
函数,又是对象,其上面有get
、post
等请求方法,最终也是调用Axios.prototype.request
函数。
axios
源码中使用了挺多设计模式。比如工厂模式、迭代器模式、适配器模式等。如果想系统学习设计模式,一般比较推荐豆瓣评分9.1的JavaScript设计模式与开发实践
如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对笔者的一种支持,非常感谢呀。
推荐阅读
写文章前,搜索了以下几篇文章泛读了一下。有兴趣在对比看看以下这几篇,有代码调试的基础上,看起来也快。
一直觉得多搜索几篇文章看,对自己学习知识更有用。有个词语叫主题阅读。大概意思就是一个主题一系列阅读。
@小贼先生_ronffy:Axios源码深度剖析 - AJAX新王者
知乎@Lee : TypeScript 重构 Axios 经验分享
笔者另一个系列
关于
作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
若川的博客,使用vuepress
重构了,阅读体验可能更好些
掘金专栏,欢迎关注~
segmentfault
前端视野专栏,欢迎关注~
知乎前端视野专栏,欢迎关注~
github blog,相关源码和资源都放在这里,求个star
^_^~
欢迎加微信交流 微信公众号
可能比较有趣的微信公众号,长按扫码关注。欢迎加笔者微信ruochuan12
(注明来源,基本来者不拒),拉您进【前端视野交流群】,长期交流学习~
本文转载自: 掘金