axios版本
一、找到入口文件
先看package.json
再看index.js文件
再来看下lib目录
- /lib/ // 项目源码目
- /adapters/ // 定义发送请求的适配器
- http.js // node环境http对象
- xhr.js // 浏览器环境XML对象
- /cancel/ // 定义取消请求功能
- /helpers/ // 一些辅助方法
- /core/ // 一些核心功能
- Axios.js // axios实例构造函数
- createError.js // 抛出错误
- dispatchRequest.js // 用来调用http请求适配器方法发送请求
- InterceptorManager.js // 拦截器管理器
- mergeConfig.js // 合并参数
- settle.js // 根据http响应状态,改变Promise的状态
- transformData.js // 转数据格式
- axios.js // 入口,创建构造函数
- defaults.js // 默认配置
- utils.js // 公用工具函数
二、分析axios.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| // 创建axios实例 function createInstance(defaultConfig) {
// 根据默认配置构建个上下文对象,包括默认配置和请求、响应拦截器对象 const context = new Axios(defaultConfig);
// 创建实例 将上下文对象绑定到Axios.prototype.request上 const instance = bind(Axios.prototype.request, context);
// 将 axios.prototype 复制到 instance utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});
// 将 context 复制到 instance utils.extend(instance, context, null, {allOwnKeys: true});
// 用于创建新实例的工厂函数 instance.create = function create(instanceConfig) { return createInstance(mergeConfig(defaultConfig, instanceConfig)); };
return instance; }
// 创建要导出的默认实例 const axios = createInstance(defaults);
|
1、对比axios的使用方法
方法一
1 2 3 4 5 6 7 8 9 10
| import axios from "axios";
axios({ method: 'post', url: '/user/12345', data: { firstName: 'Fred', lastName: 'Flintstone' } });
|
该方法直接将导出的axios对象用于请求,正式因为源码中它返回的是一个通过createInstance
创建好的实例对象,只不过你都是使用的默认配置而已。
方法二
1 2 3 4 5 6 7
| import axios from "axios";
const http = axios.create({ baseURL: xxx, method: "post", headers: {}, });
|
我们也可以通过axios对象再来创建一个自定义的实例,因为源码中它的实例对象上又挂载了一个create方法,返回的是一个新的实例对象,但是这里我们可以传入自定义的配置了。
1 2 3
| instance.create = function create(instanceConfig) { return createInstance(mergeConfig(defaultConfig, instanceConfig)); };
|
2、createInstance方法做了哪些事情
第一行代码
1
| const context = new Axios(defaultConfig);
|
1 2 3 4 5 6 7 8 9 10
| class Axios { constructor(instanceConfig) { this.defaults = instanceConfig; this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager() }; } }
|
根据默认配置对象创建了一个Axios对象,并且包含默认配置、请求和相应的拦截器对象
第二行代码
1
| const instance = bind(Axios.prototype.request, context);
|
这段代码的作用是创建一个绑定了特定上下文的函数。在这里,Axios.prototype.request 是一个函数,context 是一个特定的上下文对象。bind() 函数将这个函数绑定到指定的上下文中,以确保在调用时函数内部的 this 关键字指向该上下文对象。
假设有一个简单的 Axios 类,定义了一个 request 方法如下:
1 2 3 4 5
| class Axios { request() { console.log(this.baseUrl); } }
|
还有一个 context 对象,包含了一个 baseUrl 属性:
1 2 3
| const context = { baseUrl: 'https://api.example.com' };
|
现在,我们可以使用 bind() 来创建一个绑定了上下文的实例:
1
| const instance = bind(Axios.prototype.request, context);
|
这样,instance 实际上就是一个绑定了特定上下文的 request 方法。当我们调用 instance 时,它将使用预定义的 context 中的 baseUrl 属性:
第三行代码
1
| utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});
|
这行代码是将 Axios 类的原型属性和方法复制到 Axios 实例上,同时保留原始上下文(context)中的属性和方法。这种操作可以用来确保 Axios 实例具有与 Axios 类相同的功能,同时也保留了原始上下文中的属性和方法,以便实例可以使用。
假设 Axios 类定义了一个原型方法 get():
1 2 3
| Axios.prototype.get = function() { console.log('Executing Axios get method'); };
|
现在,如果我们使用 utils.extend() 来复制这个方法到 Axios 实例上:
1
| utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});
|
那么 instance 实例就会具有 get() 方法:
1
| instance.get(); // 输出:Executing Axios get method
|
第四行代码
1
| utils.extend(instance, context, null, {allOwnKeys: true});
|
与第三行代码类似,都是将不同的对象属性复制到 instance 实例上,它们的差异在于复制的源对象不同,一个是 Axios 类的原型对象,另一个是一个自定义的上下文对象。
3、axios({}) 方法到底是执行了谁
axios对应的就是instance,在createInstance方法中,既给它绑定了create方法,也把它的指针指向了Axios.prototype.request方法;所以当你直接执行时,实际就是执行的request方法。
1 2 3 4 5
| function instance(){
}
instance.create = function(){}
|
是不是设计挺有意思,方法上挂了一个方法的确很少这么玩 ~
4、axios.get、post、put又是在哪实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method)
,
).data })); }; });
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method)
,
: , url, data })); }; }
Axios.prototype[method] = generateHTTPMethod();
Axios.prototype[method + 'Form'] = generateHTTPMethod(true); });
|
在Axios文件中,有这样两个遍历函数,当这个文件被引入的时候,就自动给Axios添加了这10个方法
1 2 3 4 5 6 7 8 9 10
| - delete - get - head - options - post - postForm - put - putForm - patch - patchFrom
|
三、分析Axios中方法Axios.prototype.request
1 2 3 4 5 6 7 8 9 10 11
| class Axios { constructor(instanceConfig) { }
async request(configOrUrl, config) { try { return await this._request(configOrUrl, config); } catch (err) { } } }
|
接着来看this._request(configOrUrl, config);
1 2 3 4 5 6 7 8 9
| _request(configOrUrl, config) { if (typeof configOrUrl === 'string') { config = config || {}; config.url = configOrUrl; } else { config = configOrUrl || {}; } }
|
首先解析了参数configOrUrl,这也是为啥参数可以有不同写法
1 2 3 4 5
| axios({ url: 'xxx' })
axios(url, {})
|
1、自定义参数序列化
1 2 3 4 5 6 7 8 9 10 11 12
| if (paramsSerializer != null) { if (utils.isFunction(paramsSerializer)) { config.paramsSerializer = { serialize: paramsSerializer } } else { validator.assertOptions(paramsSerializer, { encode: validators.function, serialize: validators.function }, true); } }
|
举个例子,假设我们有一个请求需要发送以下参数:
1 2 3 4 5
| const params = { key1: 'value1', key2: 'value2', key3: ['value3', 'value4'] };
|
默认情况下,Axios 会将这个参数对象序列化为以下形式的 URL 查询字符串:
1
| ?key1=value1&key2=value2&key3=value3,value4
|
然而我们可以传递自定义的序列化方法传递给 Axios 请求的 paramsSerializer 参数
1 2 3 4
| axios.get('/api/data', { params: params, paramsSerializer: customParamsSerializer });:
|
2、拦截器初始化
1 2 3 4 5 6 7 8 9 10 11 12 13
| const requestInterceptorChain = []; let synchronousRequestInterceptors = true; this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected); });
const responseInterceptorChain = []; this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); });
|
这里将我们通过 axios.interceptors.request.use的拦截器 装入到了 requestInterceptorChain 开始位置。将我们通过 axios.interceptors.response.use的拦截器 装入到了 responseInterceptorChain 结束位置。
同时我们也注意到,定义了一个synchronousRequestInterceptors变量,默认所有拦截器函数都是同步的,但是也可以在注册使用时传递一个参数synchronous来把拦截器变为异步函数。
拦截器注册的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class InterceptorManager { constructor() { this.handlers = []; } use(fulfilled, rejected, options) { this.handlers.push({ fulfilled, rejected, synchronous: options ? options.synchronous : false, runWhen: options ? options.runWhen : null }); return this.handlers.length - 1; } }
|
3、拦截器执行
非全部同步拦截器执行
如果存在异步请求拦截器,则构建拦截器链,并使用 Promise 链式调用来依次执行拦截器的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| if (!synchronousRequestInterceptors) {
const chain = [dispatchRequest.bind(this), undefined];
chain.unshift.apply(chain, requestInterceptorChain);
chain.push.apply(chain, responseInterceptorChain);
len = chain.length;
promise = Promise.resolve(config);
while (i < len) {
promise = promise.then(chain[i++], chain[i++]); }
return promise;
}
|
至此,明白了,原来队列中的函数都是按对来存放的,存入和读取每一次都是2个;这样在遍历执行时,确保每一个函数都有自己的catch。
上面的例子中,判断了synchronousRequestInterceptors变量,也就是存在异步拦截器的场景,接下来继续向下看,都是同步拦截器的场景。
都为同步拦截器执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
len = requestInterceptorChain.length;
let newConfig = config;
i = 0;
while (i < len) { const onFulfilled = requestInterceptorChain[i++]; const onRejected = requestInterceptorChain[i++]; try { newConfig = onFulfilled(newConfig); } catch (error) { onRejected.call(this, error); break; } }
try { promise = dispatchRequest.call(this, newConfig); } catch (error) { return Promise.reject(error); }
i = 0; len = responseInterceptorChain.length;
while (i < len) { promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]); }
return promise;
|
我个人对这段代码其实不太理解,为什么不把同步的部分写为跟异步一模一样的实现,因为最终都是构造了一个promise链,后续有思路再来解。
4、执行request方法
不管是异步拦截器还是同步,我们都看到了dispatchRequest方法,没错,它就是我们最终执行请求的方法,接下来一起去看看它做了啥
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| export default function dispatchRequest(config) {
throwIfCancellationRequested(config);
config.headers = AxiosHeaders.from(config.headers);
config.data = transformData.call( config, config.transformRequest );
if (['post', 'put', 'patch'].indexOf(config.method) !== -1) { config.headers.setContentType('application/x-www-form-urlencoded', false); }
const adapter = adapters.getAdapter(config.adapter || defaults.adapter);
return adapter(config).then(function onAdapterResolution(response) { throwIfCancellationRequested(config);
response.data = transformData.call( config, config.transformResponse, response );
response.headers = AxiosHeaders.from(response.headers);
return response; }, function onAdapterRejection(reason) { if (!isCancel(reason)) { throwIfCancellationRequested(config);
if (reason && reason.response) { reason.response.data = transformData.call( config, config.transformResponse, reason.response ); reason.response.headers = AxiosHeaders.from(reason.response.headers); } }
return Promise.reject(reason); }); }
|
这段代码实现了发送请求的功能,并对请求成功和失败的情况进行了处理。具体步骤包括:
- 检查是否有请求被取消,如果有则抛出异常。
- 使用 AxiosHeaders.from() 方法处理请求头部。
- 转换请求数据。
- 如果请求方法为 post、put 或 patch,则设置请求头部的 Content-Type。
- 获取适配器并发送请求。
- 处理请求成功的情况:
- 转换响应数据。
- 使用 AxiosHeaders.from() 方法处理响应头部。
- 返回响应对象。
- 处理请求失败的情况:
- 如果请求未被取消,则处理响应对象中的响应数据和头部。
- 返回 Promise 对象并拒绝原因。
由于axios可以在浏览器也可以在node中执行,故而它的内部针对两种环境构造了不同的方法用于http连接;
- ./adapters/xhr.js 是对原生ajax XMLHttpRequest对象的的封装
- ./adapters/http.js 则是对node http模块的封装
node环境的先不管,直接看下xhr.js的实现
四、xhr.js实现了什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| export default isXHRAdapterSupported && function (config) { return new Promise(function dispatchXhrRequest(resolve, reject) {
let request = new XMLHttpRequest();
if (config.auth) { const username = config.auth.username || ''; const password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : ''; requestHeaders.set('Authorization', 'Basic ' + btoa(username + ':' + password)); }
const fullPath = buildFullPath(config.baseURL, config.url);
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
request.timeout = config.timeout;
function onloadend() {}
if ('onloadend' in request) {
request.onloadend = onloadend;
} else {
request.onreadystatechange = function handleLoad() { if (!request || request.readyState !== 4) { return; } if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) { return; } setTimeout(onloadend); }; }
request.onabort = function handleAbort() {};
request.onerror = function handleError() {};
request.ontimeout = function handleTimeout() {};
request.send(requestData || null); }); }
|
这段代码实现了通过 XMLHttpRequest(XHR)发送请求的功能。主要步骤如下:
- 准备请求数据(requestData)和请求头部(requestHeaders),包括对数据的处理和头部的标准化。
- 创建 XMLHttpRequest 对象,并配置请求方法、URL、超时时间等信息。
- 处理 HTTP basic authentication,如果配置中包含 auth 字段,则在请求头部添加 Authorization 字段,以进行基本认证。
- 设置 XMLHttpRequest 的各种事件处理程序,包括请求成功、请求失败、请求超时、请求取消等。
- 处理请求中的一些特殊情况,如 FormData 类型的请求数据、低级网络错误、请求超时等。
- 处理跨站请求伪造(XSRF)保护,如果需要,添加 XSRF 头部。
- 添加请求头部,并根据配置设置是否携带凭据。
- 添加响应类型和进度事件处理程序。
- 处理请求取消,包括取消令牌和 AbortController 信号。
- 发送请求,并根据情况处理成功或失败的响应。
没错,你肯定看到了这几行代码
1 2 3 4
| let request = new XMLHttpRequest(); request.onloadend = onloadend; request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true); request.send(requestData || null);
|
onloadend
1 2 3 4 5 6 7
| settle(function _resolve(value) { resolve(value); done(); }, function _reject(err) { reject(err); done(); }, response);
|
在onloadend方法中,调用了settle函数,传递了resolve、reject和response;也是文章最开始处讲的根据根据http响应状态,改变Promise的状态,至此一个完整的request结束。