axios 源码解析(中) 代码结构
axios现在最新的版本的是v0.19.0,本节我们来分析一下它的实现源码,首先通过 github地址获取到它的源代码,地址:
下载后就可以看到axios的目录结构,主目录下有一个index.js文件,该文件比较简单,内容如下:
就是去引入./lib/axios模块而已,lib目录内容如下:
大致文件说明如下:
index.js ;入口文件
├lib ;代码主目录
├helpers ;定义了一些辅助函数
├adapters ;原生ajax和node环境下请求的封装
├cancel ;取消请求的一些封装
├core ;请求派发、拦截器管理、数据转换等处理
axios.js ;也算是入口文件吧
default.js ;默认配置文件
utils.js ;工具函数
writer by:大沙漠 qq:22969969
./lib/axios应该也可以说是一个入口文件,主要的分支如下:
var utils = require('./utils'); var bind = require('./helpers/bind'); var axios = require('./core/axios'); var mergeconfig = require('./core/mergeconfig'); var defaults = require('./defaults'); //默认配置对象 /*略*/ function createinstance(defaultconfig) { //创建一个axios的实例 参数为:axios的默认配置 var context = new axios(defaultconfig); //创建一个./lib/core/axios对象,作为上下文 var instance = bind(axios.prototype.request, context); //创建一个instance属性,值为bind()函数的返回值 // copy axios.prototype to instance utils.extend(instance, axios.prototype, context); //将axios.prototype上的方法(delete、get、head、options、post、put、patch、request)extend到instans上,通过bind进行绑定 // copy context to instance utils.extend(instance, context); //将context上的两个defaults和interceptors属性保存到utils上面,这两个都是对象,这样我们就可以通过axios.defaults修改配置信息,通过axios.interceptors去设置拦截器了 return instance; //返回instance方法 } // create the default instance to be exported var axios = createinstance(defaults); //创建一个默认的实例作为输出 /*略*/ module.exports = axios; //导出符号 // allow use of default import syntax in typescript module.exports.default = axios; //默认导出符号
createinstance会创建一个./lib/core/axios的一个对象实例,保存到局部变量context中,然后调用bind函数,将返回值保存到instance中(这就是我们调用axios()执行ajax请求时所调用的符号),bind()是一个辅助函数,如下:
module.exports = function bind(fn, thisarg) { //以thisarg为上下文,执行fn函数 return function wrap() { var args = new array(arguments.length); //将arguments按照顺序依次保存到args里面 for (var i = 0; i < args.length; i++) { args[i] = arguments[i]; } return fn.apply(thisarg, args); //执行fn函数,参数为thisarg为上下文,args为参数 }; };
该函数是一个高阶函数的实现,它会以参数2作为上下文,执行参数1,也就是以context为上下文,执行axios.prototype.request函数,axios.prototype.request就是所有异步请求的入口了
我们看一下axios.prototype.request的实现,如下:
axios.prototype.request = function request(config) { //派发一个请求,也是ajax请求的入口 /*eslint no-param-reassign:0*/ // allow for axios('example/url'[, config]) a la fetch api if (typeof config === 'string') { //如果config对象是个字符串, ;例如:axios('/api/1.php').then(function(){},function(){}) config = arguments[1] || {}; //则将其转换为对象 config.url = arguments[0]; } else { config = config || {}; } config = mergeconfig(this.defaults, config); //合并默认值 config.method = config.method ? config.method.tolowercase() : 'get'; //ajax方法,例如:get,这里是转换为小写 // hook up interceptors middleware var chain = [dispatchrequest, undefined]; //这个是发送ajax的异步对列 var promise = promise.resolve(config); //将config转换为promise对象 this.interceptors.request.foreach(function unshiftrequestinterceptors(interceptor) { //请求拦截器的逻辑(下一节介绍) chain.unshift(interceptor.fulfilled, interceptor.rejected); }); this.interceptors.response.foreach(function pushresponseinterceptors(interceptor) { //响应拦截的逻辑(下一节介绍) chain.push(interceptor.fulfilled, interceptor.rejected); }); while (chain.length) { //如果chain.length存在 promise = promise.then(chain.shift(), chain.shift()); //则执行promise.then(),这里执行dispatchrequest函数,这样就组成了异步队列 } return promise; //最后返回promise对象 };
这里有一个while(chain.length){}遍历循环比较难以理解,这个设计思想很新颖,这里理解了整个axios的执行流程就能理解了,拦截器也是在这里实现的。它就是遍历chain数组,依次把前两个元素分别作为promise().then的参数1和参数2来执行,这样当promise之前的队列执行完后就会接着执行后面的队列,默认就是[dispatchrequest,undefined],也就是首先会执行dispatchrequest,如果有添加了请求拦截器则会在dispatchrequest之前执行拦截器里的逻辑,同样的,如果有响应拦截器,则会在执行dispatchrequest之后执行响应拦截器里的逻辑。
dispatchrequest逻辑如下:
module.exports = function dispatchrequest(config) { //派发一个到服务器的请求,用config里的配置 throwifcancellationrequested(config); // support baseurl config if (config.baseurl && !isabsoluteurl(config.url)) { //如果config.baseurl存在,且config.url不是绝对url(以http://开头的) config.url = combineurls(config.baseurl, config.url); //则调用combineurls将config.baseurl拼凑在config.url的前面,我们在项目里设置的baseurl="api/"就是在这里处理的 } // ensure headers exist config.headers = config.headers || {}; //确保headers存在 // transform request data config.data = transformdata( //修改请求数据,会调用默认配置里的transformrequest进行处理 config.data, config.headers, config.transformrequest ); // flatten headers config.headers = utils.merge( //将请求头合并为一个数组 config.headers.common || {}, config.headers[config.method] || {}, config.headers || {} ); utils.foreach( //再删除config.headers里的delete、get、head、post、put、patch、common请求头 ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'], function cleanheaderconfig(method) { delete config.headers[method]; } ); //执行到这里请求头已经设置好了 var adapter = config.adapter || defaults.adapter; //获取默认配置里的adapter,也就是封装好的ajax请求器 return adapter(config).then(function onadapterresolution(response) { //执行adapter()就会发送ajax请求了,then()的第一个参数会修正返回的值 throwifcancellationrequested(config); // transform response data response.data = transformdata( //调用默认配置里的transformresponse对返回的数据进行处理 response.data, response.headers, config.transformresponse ); return response; }, function onadapterrejection(reason) { if (!iscancel(reason)) { throwifcancellationrequested(config); // transform response data if (reason && reason.response) { reason.response.data = transformdata( reason.response.data, reason.response.headers, config.transformresponse ); } } return promise.reject(reason); }); };
最后会执行默认配置里的adapter属性对应的函数,我们来看一下,如下:
function getdefaultadapter() { //获取默认的适配器,就是ajax的发送器吧 var adapter; // only node.js has a process variable that is of [[class]] process if (typeof process !== 'undefined' && object.prototype.tostring.call(process) === '[object process]') { //对于浏览器来说,用xhr adapter // for node use http adapter adapter = require('./adapters/http'); } else if (typeof xmlhttprequest !== 'undefined') { //对于node环境来说,则使用http adapter // for browsers use xhr adapter adapter = require('./adapters/xhr'); } return adapter; } var defaults = { adapter: getdefaultadapter(), //适配器 /*略*/ }
./adapters/http就是最终发送ajax请求的实现,主要的逻辑如下:
module.exports = function xhradapter(config) { //发送xmlhttprequest()请求等 return new promise(function dispatchxhrrequest(resolve, reject) { var requestdata = config.data; var requestheaders = config.headers; if (utils.isformdata(requestdata)) { delete requestheaders['content-type']; // let the browser set it } var request = new xmlhttprequest(); // http basic authentication if (config.auth) { var username = config.auth.username || ''; var password = config.auth.password || ''; requestheaders.authorization = 'basic ' + btoa(username + ':' + password); } request.open(config.method.touppercase(), buildurl(config.url, config.params, config.paramsserializer), true); //初始化http请求,采用异步请求 调用buildurl获取url地址 // set the request timeout in ms request.timeout = config.timeout; //设置超时时间 // listen for ready state request.onreadystatechange = function handleload() { //绑定onreadystatechange事件 if (!request || request.readystate !== 4) { //如果http响应已经还没有接收完成 return; //则直接返回,不做处理 } // the request errored out and we didn't get a response, this will be // handled by onerror instead // with one exception: request that using file: protocol, most browsers // will return status as 0 even though it's a successful request if (request.status === 0 && !(request.responseurl && request.responseurl.indexof('file:') === 0)) { //请求出错,没有得到响应的逻辑 如果request.responseurl不是以file:开头且request.status=0,则直接返回 return; } // prepare the response var responseheaders = 'getallresponseheaders' in request ? parseheaders(request.getallresponseheaders()) : null; //解析响应头,并调用parseheaders将其转换为对象,保存到responseheaders里面 var responsedata = !config.responsetype || config.responsetype === 'text' ? request.responsetext : request.response; //如果未设置config.responsetype或者设置了responsetype.responsetype且等于text,则直接获取request.responsetext,否则获取request.response var response = { //拼凑返回的数据,也就是上一篇说的axios请求后返回的promise对象 data: responsedata, //接收到的数据 status: request.status, //状态 ie浏览器是用1223端口代替204端口 ,见:https://github.com/axios/axios/issues/201 statustext: request.statustext, //响应头的状态文字 headers: responseheaders, //头部信息 config: config, //配置信息 request: request //对应的xmlhttprequest对象 }; settle(resolve, reject, response); //调用settle函数进行判断,是resolve或者reject // clean up request request = null; }; /*略,主要是对于错误、超时、的一些处理*/ // add headers to the request if ('setrequestheader' in request) { //如果request里面存在setrequestheader utils.foreach(requestheaders, function setrequestheader(val, key) { //遍历requestheaders if (typeof requestdata === 'undefined' && key.tolowercase() === 'content-type') { //如果key等于content-type 且没有发送数据 // remove content-type if data is undefined delete requestheaders[key]; //则删除content-type这个请求头 ;只有发送数据时content-type才有用的吧 } else { // otherwise add header to the request request.setrequestheader(key, val); //否则设置请求头 } }); } // add withcredentials to request if needed if (config.withcredentials) { //如果设置了跨域请求时使用凭证 request.withcredentials = true; //设置request.withcredentials为true } // add responsetype to request if needed if (config.responsetype) { //如果设置了服务器响应的数据类型,默认为json try { request.responsetype = config.responsetype; } catch (e) { // expected domexception thrown by browsers not compatible xmlhttprequest level 2. // but, this can be suppressed for 'json' type as it can be parsed by default 'transformresponse' function. if (config.responsetype !== 'json') { throw e; } } } // handle progress if needed if (typeof config.ondownloadprogress === 'function') { //如果设置了下载处理进度事件 request.addeventlistener('progress', config.ondownloadprogress); } // not all browsers support upload events if (typeof config.onuploadprogress === 'function' && request.upload) { //如果设置了上传处理进度事件 request.upload.addeventlistener('progress', config.onuploadprogress); } if (config.canceltoken) { // handle cancellation config.canceltoken.promise.then(function oncanceled(cancel) { if (!request) { return; } request.abort(); reject(cancel); // clean up request request = null; }); } if (requestdata === undefined) { //修正requestdata,如果为undefined,则修正为null requestdata = null; } // send the request request.send(requestdata); //发送数据 }); };
也就是原生的ajax请求了,主要的逻辑都备注了一下,这样整个流程就跑完了
对于便捷方法来说,例如axios.get()、axios.post()来说,就是对axios.prototype.request的一次封装,实现代码如下:
utils.foreach(['delete', 'get', 'head', 'options'], function foreachmethodnodata(method) { //定义delete、get、head、options方法 /*eslint func-names:0*/ axios.prototype[method] = function(url, config) { return this.request(utils.merge(config || {}, { //调用utils.merge将参数合并为一个对象,然后调用request()方法 method: method, url: url })); }; }); utils.foreach(['post', 'put', 'patch'], function foreachmethodwithdata(method) { //定义post、put、patch方法 /*eslint func-names:0*/ axios.prototype[method] = function(url, data, config) { //调用utils.merge将参数合并为一个对象,然后调用request()方法 return this.request(utils.merge(config || {}, { method: method, url: url, data: data //post、put和patch比get等请求多了个data,其它一样的 })); }; });
ok,搞定。