前端要看源码,就从axios开始吧,由于它的逻辑不只没有很复杂,并且设计也很巧妙。打包后只有一千六百多行,挑三拣四,蜻蜓点水,去掉注释后,就更没多少了。
来句官方的话:Axios 是一个基于 promise 的 HTTP 库,能够用在浏览器和 node.js 中。javascript
在发起[delete
, get
, head
, options
]请求时,能够这样使用:前端
axios.get('/user',{
headers,
data
}).then(data=>{
//do something
})
复制代码
在发起[post
, put
, patch
]请求时,能够这样使用:java
axios.post('/user',data,{
headers,
}).then(data=>{
//do something
})
复制代码
这种方式发起请求,能够看到,axios
被看成了一个对象,该对象上拥有许多方法,看起来就是这样子的:node
let axios = {
delete() { },
get() { },
head() { },
options() { },
post() { },
put() { },
patch() { }
}
复制代码
发送全部类型请求时,均可以这样使用:ios
axios('/user',{
method:'get',
headers,
}).then(data=>{
//do something
})
复制代码
或者json
axios({
url:'/user',
method:'get',
headers,
}).then(data=>{
//do something
})
复制代码
固然,get
请求能够简化成这样:axios
axios('/user').then(data=>{
//do something
})
复制代码
这种方式发起请求,能够看到,axios
被看成了一个函数,看起来是这样子的:promise
function axios(url,config) {
//do something
}
复制代码
经过了解axios
的用法,咱们知道了,axios既能够是一个对象,也能够是一个函数,看成为对象时,它身上挂载了不少请求方法,好比:get
、post
、head
等等;看成为函数时,它能够直接调用,传递配置参数,参数传递有两种形式,分别是axios(url[,config])
、axios(config)
。要作到这种效果,咱们先来了解一下函数。浏览器
Javascript中函数是很值得思考的东西,好比说,它有3种角色:app
函数就是一个普通函数,这种角色在常见不过了,也是咱们常用的,好比定义一个函数foo
,而后调用它,能够是下面这样:
//定义
function foo(){
//do something
}
//调用
foo()
复制代码
函数也能够被看成一个构造函数来使用,好比定义一个构造函数Foo
,而后获取它的一个实例,能够是下面这样:
//定义
function Foo(){
//do something
}
//获取一个实例
let instance=new Foo()
复制代码
这里函数名首字母大写纯粹是为了向某语言、某标准、某约定俗成看齐,其余什么做用都没有(无知的说这话)
敲黑板,划重点,函数也能够被看成一个对象,也就是说,咱们能够将一个函数做为对象来使用,给它定义属性,获取它的属性值,此时和它的另一个身份(函数)毫无关系。好比有一个函数foo
,我就不把你当成函数,就把它当成对象,给它定义属性,获取它的属性值,ok,没毛病。
function foo(){
//do something
}
//给foo定义一个属性,值为数字
foo.a=1;
//给foo定义一个属性,值为函数
foo.b=function(){};
//获取foo的属性a的值
console.log(foo.a)
复制代码
如今,我又想把foo
当成一个函数对待了,ok,没毛病,你是导演,你说了算。
//如今把foo当成函数来调用
foo()
复制代码
了解了这些,咱们来看下axios是如何作到多种使用方式的。
源码以下:
function createInstance(defaultConfig) {
//获取Axios的一个实例
var context = new Axios(defaultConfig);
//将Axios原型上的request方法的上下文(也就是this)绑定为刚建立的实例
var instance = bind(Axios.prototype.request, context);
//将Axios原型上的属性和方法扩展到instance上
utils.extend(instance, Axios.prototype, context);
//将建立的实例上的属性和方法扩展到instance上
utils.extend(instance, context);
//返回instance
return instance;
}
//axios就是上面的instance
var axios = createInstance(defaults);
//...
module.exports = axios;
复制代码
上面代码中,Axios.prototype.request
是一个方法,用工具函数bind
将它的上下文(this)绑定为Axios的实例,获得instance
,也就是说instance
是绑定this
后的request
方法,接下来就是围绕instance
来搞事情了。 instance
自己是一个函数,可是这里,将它做为一个对象来处理了,给它身上定义了一些属性和方法。
将Axios.prototype
上的属性和方法扩展到了instance
上,Axios.prototype
上有哪些属性和方法呢,源码以下:
//定义request方法,该方法是重点,其余调用方式最终调用的都是这个方法
Axios.prototype.request = function request(config){/* ... */}
//在原型上批量定义方法,这些方法不接收请求体数据,好比get请求
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {/* ... */};
});
//在原型上批量定义方法,这些方法接收请求体数据,好比post请求
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
Axios.prototype[method] = function(url, data, config) {/* ... */};
});
复制代码
上面代码中都是在Axios的原型上定义方法,首先定义request
方法,其次批量定义[delete
, get
, head
, options
]方法,最后批量定义[post
, put
, patch
]方法,也就是说给原型上定义的这些方法最后都会被扩展到instance
上,咱们就能够这样使用了:axios.get()
、axios.post()
等等。 其实,不管咱们是将axios
做为一个对象来调用get
、post
方法,仍是将axios
做为一个函数来调用,最终调用的都是原型上的request
方法。
了解了axios
是如何作到多种使用方式的,接下来看一下axios
中的配置,也就是用户传入的配置项是如何走完整个流程的。
axios.get()
等没有请求体方法传入的配置当咱们这样调用axios
时:
axios.get('/user',{headers,timeout,...})
复制代码
axios
源码内部会将咱们传入的配置作一层处理,这层处理很简单,首先判断是否传递config
,而后将method
、url
合并到config
,最后调用request
方法,并将处理后的配置传递给request
。 源码以下:
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url
}));
};
});
复制代码
axios.post()
等有请求体方法传入的配置当咱们这样调用axios
时:
axios.post('/user',{name:'wz'},{headers,timeout,...})
复制代码
axios
内部的处理方式和上面调用get
方式几乎相同,惟一不一样的是多处理了一下请求体数据。源码以下:
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
Axios.prototype[method] = function(url, data, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url,
data: data //处理请求体参数
}));
};
});
复制代码
Axios.prototype.request
方法中对配置作了什么axios.get()
、axios.post
、axios()
,不管是哪一种调用方式,最终都会调用request
方法,看一下关于request
方法的源码:
Axios.prototype.request = function request(config) {
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();
//...
};
复制代码
request
方法先对参数作一次判断,若是第一个参数类型为字符串,也就是咱们是这样使用的,axios('/user',{...})
,会将字符串放到一个新对象上的url
属性上,而后和第二个参数作依次合并。接下来就是按照从小到大的权重合并多个配置。
defaults
是axios
的默认配置,权重最低。{method:'get'}
是设置默认请求方法。this.defaults
,是新建立axios
实例时传入的配置对象.axios.create({...})
config
,调用axios
时传递的参数,权重最高。到这里axios
内部对config
的处理就算完成了,接下来,config
会被依次交给请求拦截器去处理,让咱们看一下axios
中的拦截器吧。
先看一下这3种东西的关系,以下图:
XMLHttpRequest
,
node
环境使用
http(s).request()
方法;响应拦截器是处理响应数据。
拦截器是一个函数,分为两种类型:请求拦截器和响应拦截器,请求拦截器的主要做用是,提供了一种机制,使得咱们能够集中处理各类请求时传递的参数,就比如人们去乘坐地铁同样,无论你是从哪里来的,手里拿着什么东西,只要乘坐地铁都会通过安检,拦截器就比如这道安检同样,每一个安检有各自的安检任务,第一个安检经过后,会转交到第二个安检,第三个,直到全部的安检都经过了,才能抵达候车区。
axios
中关于拦截器的源码:
function InterceptorManager() {
//存放拦截器的容器
this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
//将用户传入的拦截器放入容器中,
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
//返回拦截器在容器中的位置,用于取消拦截器
return this.handlers.length - 1;
};
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
//经过拦截器在容器中的位置,删除指定的拦截器
this.handlers[id] = null;
}
};
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);//将遍历到的每个拦截器回传给外部
}
});
};
复制代码
拦截器相关的源码不难理解,InterceptorManager
构造函数原型上定义了拦截器的添加、删除、遍历3个方法。须要注意的是,每个拦截器又分为两个方法,成功和失败,这是由于,axios
是基于promise
实现链式调用的,因此每个拦截器的成功失败方法会分别传递给promise
的then
方法当中。对promise
不太了解的,能够看这里
响应拦截器的道理和请求拦截器同样,只不过响应拦截器拦截的是返回的数据,而请求拦截器拦截的是请求的配置。 接下来就走到转换器了。
转换器是一个函数,分为两种类型:请求数据转换器和响应数据转换器。请求转换器函数接受2个参数,分别为data
,headers
,主要是处理请求体和请求头的。响应数据转换器主要是处理响应数据的,好比将JSON数据转为普通数据。
源码中默认的转换器:
let defaults={
//请求数据转化器
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;
}
if (utils.isURLSearchParams(data)) {
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
if (utils.isObject(data)) {
setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
return JSON.stringify(data);
}
return data;
}],
//响应数据转换器
transformResponse: [function transformResponse(data) {
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (e) { /* Ignore */ }
}
return data;
}],
}
复制代码
上面代码简单看看就能够了,大概知道拦截器在作什么事情,其实仔细思考能够发现,转化器作到事情在拦截器里一样可以作到,只不过为了职责单一,分工明确,将数据处理这部分工做单独放到转换器里来作了。
接下来就到发送请求了,这部分工做是由适配器作的。
适配器会根据当前使用axios
的环境来决定使用哪一种工具去发送请求,当前环境是浏览器的话,就会使用XMLHttpRequest
,是node
的话,会使用http(s).request()
。
源码以下:
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// 浏览器环境,使用 xhr 适配器
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined') {
// node环境,使用 http 适配器
adapter = require('./adapters/http');
}
return adapter;
}
复制代码
./adapters/xhr
文件里,除了对IE进行一些判断处理外,基本上就是发送AJAX那几个经典步骤了,以下:
//获取 XMLHttpRequest 实例
let xhr = new new XMLHttpRequest();
//肯定请求方法等参数
xhr.open(method,url,...);
//监听请求状态
xhr.onreadystatechange = function () {
//...
}
//发送请求
xhr.send();
复制代码
./adapters/http
文件里,是使用node
的包http
、https
来完成请求的。
axios
内部是如何组织拦截器、转换器、适配器呢,看一下源码:
Axios.prototype.request = function request(config) {
//chain里存放着全部的拦截器、转换器、适配器
var chain = [dispatchRequest, undefined];
//生成一个成功的promise,数据为通过一系列处理后的配置
var promise = Promise.resolve(config);
//this.interceptors.request存放着全部的请求拦截器
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
//将全部的请求拦截器依次放入chain容器中,这里使用的是unshift,
//也就是说,请求拦截器被是从chain容器的头部开始,依次放入。
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
//this.interceptors.response存放着全部的响应拦截器
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
//将全部的响应拦截器依次放入chain容器中,这里使用的是push,
//也就是说,响应拦截器被是从chain容器的尾部开始,依次放入。
chain.push(interceptor.fulfilled, interceptor.rejected);
});
while (chain.length) {
//链式调用chain容器中的拦截器、转换器、适配器,注意这里使用的是
//chain.shift()方法,就是说取的时候是从chain容器头部开始,直到尾部,线性顺序。
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
复制代码
chain
容器在初始化的时候就加入了dispatchRequest
和undefined
,接下来咱们看一下dispatchRequest
具体作了什么
dispatchRequest
都作了什么从发送一个请求到这里只剩下这几件事情了,
dispatchRequest
主要就是作了以上3件事。 简化后的源码以下:module.exports = function dispatchRequest(config) {
// 转化请求数据
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
var adapter = config.adapter || defaults.adapter;
//adapter方法会根据调用请求方法所在的环境来选择具体的请求方法。
return adapter(config).then(function onAdapterResolution(response) {
// 转换响应数据
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
return response;
}, function onAdapterRejection(reason) {/* ... */});
};
复制代码
回顾一下,从发送一个请求开始,到成功接收到响应,这风风雨雨的一路,全在图里了。