- 摘要
vue使用axios进行http通讯,类似jquery/ajax的作用,类似angular http的作用,axios功能强大,使用方便,是一个优秀的http软件,本文旨在分享axios源代码重点难点分析,无意从头到尾详细分析源代码的各个细节。
- axios的封装
axios做了复杂深奥的封装,不同于普通的对象/实例方法。
debug看axios.get()代码是:
bind.js:
module.exports = function bind(fn, thisArg) {
return function wrap() { //axios是这个方法
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
return fn.apply(thisArg, args);
};
再看axios入口文件axios.js代码:
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig); //这是axios实例
var instance = bind(Axios.prototype.request, context); //bind返回wrap方法,而不是axios实例
// Copy axios.prototype to instance
utils.extend(instance, Axios.prototype, context); //把Axios静态对象的属性方法复制到instance,由于要改变属性方法中的this指针,因此不能简单复制属性方法,
而是要写成warp方法,此方法返回fn.apply(context,args),那么调用属性方法时,就是执行wrap方法,就是执行
fn.apply然后返回,也就是给方法fn加一层封装以便调整指针,否则属性方法就是fn很简单。
// Copy context to instance
utils.extend(instance, context); //把axios实例的属性方法复制到instance
return instance;
}
var axios = createInstance(defaults); //defaults对象是缺省配置数据,用var defaults=require('./defaults')获取
module.exports = axios;
在bind看fn是:
Axios.prototype.request
Axios.prototype[method]
fn就是Axios静态对象的属性方法:request,get, get方法代码如下:
utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {
return this.request(utils.merge(config || {}, { //this在调用这个方法时已经被调整为Axios实例
method: method,
url: url
}));
};
就是执行Axios实例的request方法。
在extend代码用debug看Axios.prototype有如下属性方法:
request
delete
get
head
post
put
patch
defaults
interceptors
这些属性方法复制到axios,axios就有这些属性方法,因此可以写axios.get调用其方法,但debug看axios是一个wrap方法,
是一个构造函数,也是一个对象,是一个特殊的对象,不能new axios()实例化,看axios看不见属性,但看axios.get是有的,
看axios.abc是没有的。
因此axios是一个很特殊的构造函数/对象,其属性方法也是特殊构造的,就是wrap()函数,代码就是fn.apply(),因此
调用axios.get时,确实是调用axios对象的get属性方法,就是调用Axios.prototype.get方法,Axios是原型,axios是一个
instance,instance继承原型,在调用原型方法时把this调整为原型实例new Axios()。
这个编程方法很高级,照理说,一般都是new Axios(),然后调用实例的属性方法,很简单,但它不是这样设计,它是设计
一个特殊的对象axios,再把Axios做为原型复制到axios,那么axios就是Axios原型的一个instance,再把方法中的this调整为
new Axios实例,这样当调用axios的属性方法时,相当于是调用new Axios实例的属性方法。
先定义一个标准的原型Axios,再如此创建一个instance(axios),匪夷所思,这样设计是为了层次化,把原型和instance
分开。
axios其实就是Axios,只是axios本身和属性方法做了一个封装。
- axios重点源代码分析
用循环方法可以看到axios的所有属性名:
for(var key in axios){
console.log(key);
};
request
delete
get
head
post
put
patch
defaults
interceptors
Axios
create
Cancel
CancelToken
isCancel
all
spread
default
因此axios.defaults是访问/修改axios的defaults属性,也是从Axios.prototype复制来的,复制属性/访问属性没有this问题。
调用axios.get实际上就是调用Axios.prototype.get原型方法,方法中的this代表new Axios实例:
utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url
}));
get方法就是调用request方法,传参method:'get'而已,因此axios.get代码实际上是从request代码开始:
Axios.prototype.request = function request(config) {
config = utils.merge(defaults, this.defaults, { method: 'get' }, config);
//axios.defaults会合并到config,config本来只是传入的数据,这之后增加了data,baseUrl等属性
config.url = combineURLs(config.baseURL, config.url); //拼接url,调用get时如果输入绝对url路径就不用再拼接
promise = promise.then(http拦截函数/dispatchrequest); //这里面http过程有嵌套promise
return promise; //for axios.get().then(callback)
function dispatchRequest(config) {
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers || {}
);
可以看到axios.headers.common或axios.headers["get"]这样写法的都做为header属性配置都会合并到headers,因此像axios.defaults.headers.common["access_token"] = "123456"这种写法结果是一样的。
return adapter(config).then(function onAdapterResolution(response) {
return response;
}, function onAdapterRejection(reason) {
return Promise.reject(reason);
function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestData = config.data;
var requestHeaders = config.headers;
utils.forEach(requestHeaders, function setRequestHeader(val, key) {
request.setRequestHeader(key, val);
在adapter底层调setRequestHeader原生方法设置http request header属性,看http报文头是:
Request Headers:
Accept:application/json, text/plain, */* //这个属性是axios设置的缺省属性
Accept-Encoding:gzip, deflate, br
Accept-Language:zh-CN,zh;q=0.8
access_token:123456
Authorization:token 4b26496037665e0c4d308ec682b6e7fb
test:hello
其中一个属性是在http.js中设置的global属性(每个http请求都有效):
axios.defaults.headers["access_token"] = "123456";
对于特定的http请求也可以传一个json object {}来设置http/header属性,类似jquery ajax写法:
axios.get(url,{
headers:{
test:"hello"
},
params:{
ID:"123456" //由于是get请求,此参数会自动附加在url中以query string形式传递,与直接在url写?ID=123456一样。
}
)
有些属性是浏览器进行http通讯时使用的缺省属性。
因此http参数配置,一是可以在http.js写global配置,二是可以在调用get方法时直接写一个{属性名:属性值}传进去即可,
axios约定的属性名可以参考api,也可以直接看原代码,写参数类似jquery ajax,都是用json object形式,比如:
{
headers: {'X-Requested-With': 'XMLHttpRequest'},
params: { //做为query string参数
ID: 12345
},
data: { // post data
firstName: 'Fred'
},
timeout: 1000, //http request timeout可以每次请求时配置,缺省是5s
}
调用写法与jquery ajax类似:axios.get(url,{}),原代码中config数据就是传入的{}。
原代码中有:
path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''),
这是构造url时使用配置object的params属性来构造?a=b这样的参数。
axios功能很强,支持upload/download进度处理,cancelToken机制,还支持proxy,一般只有项目级的软件才支持proxy,
比如ionic项目环境。
底层adapter有http.js和xhr.js两个,xhr.js没有proxy处理代码,决定用哪个adapter的代码:
dispatchrequest:
var adapter = config.adapter || defaults.adapter;
如果有自定义的adapter,就执行自定义adapter,否则就执行缺省的adapter,缺省adapter如下决定:
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined') {
// For node use HTTP adapter
adapter = require('./adapters/http');
}
return adapter;
如果是浏览器则执行xhr,不支持proxy,如果在nodejs运行,用http adapter,支持proxy,所以proxy是nodejs的功能。
在axios基础之上,可以进一步封装,比如fecth就是调用get,但做了一层封装:
function fetch(url, params = {}) {
return new Promise((resolve, reject) => {
axios.get(url, {
params: params
})
.then(response => {
resolve(response); //因为是new实例时执行回调传递resolve,因此是resove new实例
})
.catch(err => {
// reject(err)
})
批量http写法:
axios.all([getCarousel(), getthemes(), gethotline(), getArea()])
.then(axios.spread(function(getCarouselData, getthemesData, gethotlineData, getAreaData) {
里面直接写axios.get(url)或fetch(url)也一样,但加一层封装逻辑上更直观。
- axios.all()批量执行http的源代码分析
axios.spread方法代码:
function spread(callback) {
return function wrap(arr) {
return callback.apply(null, arr);
};
它是返回一个wrap封装函数,因此实际上就是then(function wrap(arr){return callback.apply(null,arr);}),promise
会resolve之前promise实例再传递数据执行then里面的函数,传递的数据是数组,函数形参写,,,是可以的,函数代码用
arguments[]取参数也可以,去掉spread直接写then(funciton(){也是可以的,用arguments[0]可以获取到数组,axios变换
参数时加了一层[0]。
关键还是要看all promise如何resolve,涉及到每个http promise状态变化,挺复杂的。
axios.all代码:
axios.all = function all(promises) {
return Promise.all(promises);
};
es6-promise:
function all(entries) {
return new Enumerator(this, entries).promise;
}
function Enumerator(Constructor, input) {
this.promise = new Constructor(noop); // all返回这个promise实例
this._result = new Array(this.length);
this._enumerate();
fulfill(this.promise, this._result); //在这里resolve返回的promise实例并传递一个数组
Enumerator.prototype._enumerate = function () {
this._eachEntry(_input[i], i);
Enumerator.prototype._eachEntry = function (entry, i) {
this._settledAt(entry._state, i, entry._result);
this._willSettleAt(resolve$$(entry), i);
this._remaining--;
this._result[i] = entry;
Enumerator.prototype._settledAt = function (state, i, value) {
检查处理一个promise,如果完成就更新all promise数组以及计数
this._remaining--;
this._result[i] = value;
if (this._remaining === 0) {
//如果所有http promise都完成则resolve axios.all返回的promise
fulfill(promise, this._result);
Enumerator.prototype._willSettleAt = function (promise, i) {
//如果http promise没完成,就定阅一个http promise完成事件,相当于写一个http promise.then(callback),当http promise完成时执行callback执行_settledAt更新all promise数组
subscribe(promise, undefined, function (value) {
return enumerator._settledAt(FULFILLED, i, value);
all/spread只是简单的封装,批处理代码都在Enumerator这个对象中,all的底层就是Enumerator。
因此all检查等待所有http promise完成,获取每个http的response/value,再resovle本身promise,传递数组,执行then里面
的callback,callback入口参数是数组,按all数组的书写顺序,不是按每个http完成先后顺序,因为http过程是异步的,完成
顺序是随机的。等待的办法就是用事件机制,用subscriber()方法,相当于写了then,每个http promise原来没写then,all
给每个http promise写了一个then,每个http promise完成之后更新all数组并判断是否都完成,如果都完成就执行all后面的
then。
另外,InterceptorManager就是建一个handlers[],通过调用use可以把自定义的拦截函数存储在里面。
Axios底层就是调用xmlhttprequest,就是加了封装,这样书写更方便,使用更方便,相当于angular的http封装,webuploader底层也是,但webuploader是一个应用功能,
不仅仅是把http封装一下。