axios流程图
axios 项目目录概览
├── /dist/ # 项目输出目录
├── /lib/ # 项目源码目录
│ ├── /cancel/ # 定义取消功能
│ ├── /core/ # 一些核心功能
│ │ ├── Axios.js # axios的核心主类
│ │ ├── dispatchRequest.js # 用来调用http请求适配器方法发送请求
│ │ ├── InterceptorManager.js # 拦截器构造函数
│ │ └── settle.js # 根据http响应状态,改变Promise的状态
│ ├── /helpers/ # 一些辅助方法
│ ├── /adapters/ # 定义请求的适配器 xhr、http
│ │ ├── http.js # 实现http适配器
│ │ └── xhr.js # 实现xhr适配器
│ ├── axios.js # 对外暴露接口
│ ├── defaults.js # 默认配置
│ └── utils.js # 公用工具
├── package.json # 项目信息
├── index.d.ts # 配置TypeScript的声明文件
└── index.js # 入口文件
note:以下所有代码的讲解都是基于lib目录
名词概述
-
拦截器interceptors
axios里面的拦截器分为请求拦截器(request interceptor)和响应拦截器(response interceptor),前者是可以在每次http请求前拦截,并可修改配置config;后者则是在http请求后,对返回的数据进行修改处理;
-
数据转换器transformData
axios里面的转换器分为请求转换器(transformRequest)和响应转换器 (transformResponse),前者可以在请求前改变即将发送的data数据,后者则可以在请求完成后对于data进行数据修改。
-
http请求适配器
顾名思义,就是选择合适的方法进行http请求(node环境使用http模块,browser环境使用xhr模块)
-
config配置项
每次请求使用的配置项,config非常重要,是串起整个axios内部与用户通信的主要通道
源码分析
首先来看看axios的入口文件,为什么axios可以提供如此多的接口调用,原因就是axios的createInstance方法的作用使然。
// /lib/axios.js
/**
* 创建Axios的实例
*
* @param 实例的默认配置
* @return Axios的实例
*/
function createInstance(defaultConfig) {
// 创建一个Axios实例
var context = new Axios(defaultConfig);
// instance 指向request方法,并且绑定上下文context,所以instance(option)得以调用
var instance = bind(Axios.prototype.request, context);
// 把Axios.prototype的方法扩展到instance上来
// 这样instance就有了get post put delete等等方法
// 而且指定了context上下文,这样在访问Axios.prototype上的方法时,this指向context
utils.extend(instance, Axios.prototype, context);
// 拷贝context自身的属性到instance上
utils.extend(instance, context);
return instance;
}
// invoke
// 接收默认配置项作为参数,创建一个axios实例,最终导出的是一个对象
var axios = createInstance(defaults);
看到没有,createInstance最终返回的是一个Function,这个Function其实就是Axios.prototype.request,这个Function还有Axios.prototype的每个方法作为静态方法,而且这些方法都是指向同一个上下文context。
Axios是axios的核心,一个Axios的实例就是一个axios应用。而Axios上核心的方法就是request,所有的请求都是通过这个方法发出的。
/**
* 创建一个Axios的实例
*
* @param {Object} instanceConfig作为默认配置项
*/
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
/**
* 触发一个请求
*
* @param {Object} 配置项(merge 传入的参数和默认的参数)
*/
Axios.prototype.request = function request(config) {
...省略
};
// 提供axios请求方法的别名
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
}));
};
});
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.get、axios.post、axios.put等,可以看到,即是名称不同,但是最终都是通过Axios.prototype.request去触发请求的
一般情况上,上面的方法就可以满足大部分的需求了,如果不满足的导出的axios实例,axios还提供了创建新的axios实例的接口
axios.Axios = Axios;
axios.create = function create(instanceConfig) {
return createInstace(Util.merge(axios.defaults, instanceConfig));
}
绕来绕去都是要通过Axios.prototype.request去触发请求
是不是很想知道Axios.prototype.request做了什么呢?
这个request到底是怎么根据我们配置的config去发送请求
贯穿axios项目的config
Axios的config里面的项有:
http请求适配器(adapter)、请求地址(url)、请求方法(method)、请求header、请求数据(data)、请求或者响应数据的转换、请求进度、http状态码验证规则、超时(timeout)、取消请求(cancelRequest)等等
可以看到所有的功能都是通过对象的形式传递的
再来看下,用户一般如何来设置配置项的
1 直接修改Axios实例上的defaults属性,主要设置通用配置
axios.defaults[configName] = value;
2 发起请求时,传入配置项
axios({url, method, headers})
3 新创建axios实例时,传入配置项,此处也是设置通用配置项
let newAxiosInstance = axios.create({[configName]: value})
然后找到Axios.prototype.request方法的其中一行(/lib/core/Axios.js)
config = merge(defaults, {method: 'get'}, this.defaults, config)
很明显,上面把defaults(/lib/defaults)、this.defaults(Axios实例的defaults)、config(请求传入的config)进行合并
优先级是config > this.defaults > {method: ‘get’} > defaults
上面说config贯穿了整个axios项目,那config到底是如何传递的呢?
Axios.prototype.request = function request(config) {
config = mergeConfig(this.defaults, config);
// interceptor中间件
var chain = [dispatchRequest, undefined];
// 重点就是这里,把config对象作为参数传递给Promise.resolve
var promise = Promise.resolve(config);
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) {
// config会按顺序通过 requestInterceptors - dispatchRequest - resposeInterceptors
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
看到没有,config就是通过这样,作为promise.resolve的参数,传递到后续的promise.then(fulfilled, reject).then…,是不是很有趣?
上面出现chain其实就是个中间件,里面放置的数据主要是请求拦截器,(dispatchReuest、undefined),响应拦截器
这里的
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);
});
必须要这样做吗?
答案是确实需要这样,因为有地方已经说过了,axios的请求流程是 request interceptors > dispatchRequest > response interceptors
最终chain生成的结果是
[requestInterce2.fulfilled, requestInterceptor2.rejected, requestInterce1.fulfilled, requestInterceptor1.rejected, dispatchRequest, undefined, responseInterce1.fulfilled, responseInterceptor1.rejected, responseInterce2.fulfilled, responseInterceptor2.rejected]
为什么要生成这样的数组?为什么中间有个是undefined?
while (chain.length) {
// config会按顺序通过 requestInterceptors - dispatchRequest - resposeInterceptors
promise = promise.then(chain.shift(), chain.shift());
}
以上的的代码很好解释了为什么要生成这样的数组,因为promise.then方法的参数都是chain.shift()出来的,这样就可以做到先执行请求拦截器,然后dispatchRequest,最后执行响应拦截器。
拦截器如何做到拦截的
先看看axios拦截器的正确打开方式
// 添加请求拦截器
const myRequestInterceptor = axios.interceptors.request.use(config => {
// 在发送http请求之前做些什么
return config; // 有且必须有一个config对象被返回
}, error => {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(response => {
// 对响应数据做点什么
return response; // 有且必须有一个response对象被返回
}, error => {
// 对响应错误做点什么
return Promise.reject(error);
});
// 移除某次拦截器
axios.interceptors.request.eject(myRequestInterceptor);
拦截器在Axios源码里面是如何定义并且使用的
function Axios(instanceConfig) {
// ...
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
每一个Axios实例都有一个interceptors属性,这个属性就是用来保存管理请求、响应拦截器的
接着继续看下Interceptor构造函数
// /lib/core/InterceptorManager.js
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;
}
};
// 遍历this.handlers,并将this.handlers里的每一项作为参数传给fn执行
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
};
想想为什么InterceptorManager.prototype.eject注销方法,this.handlers[id] = null?
答案是为了不改变this.handlers的顺序,方便插入、查找
结合以上可以看到,dispatchRequest做了以下的事情:
- 拿到config对象,对config对象进行传递,request interceptor优先对于config进行处理
- http请求适配器根据config配置,发起请求
- http请求完成后,则根据header、data、config.transformResponse
请求适配器是如何工作的
return adapter(config).then(function onAdapterResolution(response) {
// ...
return response;
}, function onAdapterRejection(reason) {
// ...
return Promise.reject(reason);
});
};
除非重新配置adapter否则默认使用默认的adapter,首先得到对应的adapter方法返回的promise对象,通过then方法,对结果进行进一步处理,成功使用onAdapterResolution处理,失败则使用onAdapterRejection处理
如何选择合适的请求模块?
var defaults = {
adapter: getDefaultAdapter()
};
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// 如果是浏览器环境使用xhr adapter
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// 如果是node环境使用http模块
adapter = require('./adapters/http');
}
return adapter;
}
数据转换器
官方教程使用:
// 全局通用
import axios from 'axios'
// 往现有的请求转换器里增加转换方法
axios.defaults.transformRequest.push((data, headers) => {
// ...处理data
return data;
});
// 重写请求转换器
axios.defaults.transformRequest = [(data, headers) => {
// ...处理data
return data;
}];
// 往现有的响应转换器里增加转换方法
axios.defaults.transformResponse.push((data, headers) => {
// ...处理data
return data;
});
// 重写响应转换器
axios.defaults.transformResponse = [(data, headers) => {
// ...处理data
return data;
}];
// 具体请求携带的转换器
import axios from 'axios'
// 往已经存在的转换器里增加转换方法
axios.get(url, {
// ...
transformRequest: [
...axios.defaults.transformRequest, // 去掉这行代码就等于重写请求转换器了
(data, headers) => {
// ...处理data
return data;
}
],
transformResponse: [
...axios.defaults.transformResponse, // 去掉这行代码就等于重写响应转换器了
(data, headers) => {
// ...处理data
return data;
}
],
})
axios里面的转换器源码部分
// /lib/defaults.js
var defaults = {
transformRequest: [function transformRequest(data, headers) {
normalizeHeaderName(headers, 'Content-Type');
// ...
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运行中,什么时候什么地方使用了转换器呢?
// /lib/core/dispatchRequest.js
function dispatchRequest(config) {
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
return adapter(config).then(/* ... */);
};
transformData具体做了什么呢?
// /lib/core/transformData.js
function transformData(data, headers, fns) {
utils.forEach(fns, function transform(fn) {
data = fn(data, headers);
});
return data;
};
主要是遍历转换器数组,分别执行每一个转换器,根据data和headers参数,返回新的data
// /lib/core/dispatchRequest.js
return adapter(config).then(function onAdapterResolution(response) {
// ...
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
// ...
if (reason && reason.response) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
});
响应转换器的使用地方是在http请求完成后,根据http请求适配器的返回值做数据转换处理
拦截器和转换器的关系
拦截器主要负责config的更改配置,数据转化器主要负责转换请求体(data),比如请求前转换对象为字符串,
请求后把响应的字符串转换为对象。
如何cancel一个已经发出请求
使用
import axios from 'axios'
// 第一种取消方法
axios.get(url, {
cancelToken: new axios.CancelToken(cancel => {
if (/* 取消条件 */) {
cancel('取消日志');
}
})
});
// 第二种取消方法
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get(url, {
cancelToken: source.token
});
source.cancel('取消日志');
源码
// /cancel/CancelToken.js
function CancelToken(executor) {
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
if (token.reason) {
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
// /lib/adapters/xhr.js
if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
request = null;
});
}
取消功能的核心是通过CancelToken内的this.promise = new Promise(resolve => resolvePromise = resolve)
, 得到实例属性promise
,此时该promise
的状态为pending
通过这个属性,在/lib/adapters/xhr.js
文件中继续给这个promise
实例添加.then
方法 (xhr.js
文件的159行config.cancelToken.promise.then(message => request.abort())
);
在CancelToken
外界,通过executor
参数拿到对cancel
方法的控制权, 这样当执行cancel
方法时就可以改变实例的promise
属性的状态为rejected
, 从而执行request.abort()
方法达到取消请求的目的。
总结
这个项目最终成型的大小只有几十k,但是里面富含很多有意思的使用,比如对于promise的串联操作,请求前后各种数据处理等的流程控制,同时又支持浏览器和node环境。真心可以多读。
最后附上本人参考axios的实现,重新造了个axios,也算是加深对axios的理解。