前言
最近在项目中使用axios封装Ajax请求时,看到调用axios请求写法有很多种,以前都是习惯使用axios({})这种,很少使用其他写法,感觉还挺有趣,干脆看看源码,整理整理若文中有什么错误之处,欢迎指出和探讨
正文
环境准备
还是直接在Vue项目中封装调试了,方便也符合我的应用场景
vue-cli 起一个简单的项目,然后安装 axios 依赖,在 utils 目录下新建 request.js 文件,用来简单封装 axios
//utils/request.js
import axios from 'axios'
const service = axios.create({
baseUrl: 'api',
timeout: 10000
})
service.interceptors.request.use(
config => {
console.log('发出请求')
return config
},
error => {
console.log('请求失败')
}
)
service.interceptors.response.use(
response=>{
console.log('响应成功:', response)
return response
}
)
export default service
写一个用户登录的测试接口
//api/user.js
import axios from '@/utils/request'
export function login (data) {
return axios({
url: '/login',
data: data,
method: 'post'
})
}
初始化
const service = axios.create( config )
axios定义在 lib/axios.js 文件中
'use strict';
var utils = require('./utils');
var bind = require('./helpers/bind');
var Axios = require('./core/Axios');
var mergeConfig = require('./core/mergeConfig');
var defaults = require('./defaults');
/**
* Create an instance of Axios
*
* @param {Object} defaultConfig The default config for the instance
* @return {Axios} A new instance of Axios
*/
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context);
// Copy axios.prototype to instance
utils.extend(instance, Axios.prototype, context);
// Copy context to instance
utils.extend(instance, context);
return instance;
}
// Create the default instance to be exported
var axios = createInstance(defaults);
// Expose Axios class to allow class inheritance
axios.Axios = Axios;
// Factory for creating new instances
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
// Expose Cancel & CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
// Expose all/spread
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
module.exports = axios;
// Allow use of default import syntax in TypeScript
module.exports.default = axios;
先不急着看代码,可以打印看一下模块导出的axios是什么
console.log(axios)
console.log(Object.keys(axios))
这里的 wrap 函数,应该是定义的一个通用工具类函数,它返回的是 fn 绑定 thisArg这个 上下文后执行的结果
fn,thisArg是什么后面调试看代码的时候再说,现在能明确的是使用 aixos 时,import 导入的,实际上是一个函数,同时这个函数(Object),有18个属性(JS中,可以给函数添加属性,添加的属性可通过 Object.keys() 方法获得)
下面开始调试梳理流程
预处理(参数预处理)
从入口文件可以看出来,实例化 axios
调用 axios.create( config )方法
该方法使用工厂模式实例化 一个新的 axios,除了会接受一个 config,还会调用 mergeConfig() 方法添加一些默认属性
关于这个mergeConfig() 方法具体逻辑就不看了,它会将我们传入的config及 默认的一些属性一并处理返回作为实例化axios的config参数,
// Factory for creating new instances
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
可以打印看一下执行这个方法后的返回值
如果我们传入了用来添加至实例的参数,它会一并遍历处理添加至这个Object,作为实例化 axios 的参数
后面会进入 createInstance() 这个方法开始实例化,看一下这个方法是如何实例化 axios的
实例化
主要逻辑
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context);
// Copy axios.prototype to instance
utils.extend(instance, Axios.prototype, context);
// Copy context to instance
utils.extend(instance, context);
return instance;
}
createInstance 函数通过函数也可以看出来,是用来实例化 Axios 的,上面提到的module导出的 axios 就是由它实例化返回的。axios 与 axios.create() 均会调用 createInstance 来实例化,二者的区别在于
axios是已经生成的一个默认配置的实例,而 axios.create() 接受参数用来自定义配置后生成一个新的实例
继续看这个 createInstance 函数
- 首先,new 了一个 Axios 实例 (context)
- 将Axios的 request 方法绑定一个执行上下文,这个上下文就是 刚才new的 Axios的一个实例
- 将 Axios.prototype 上的可枚举属性遍历 深度复制到 instance 上,如果某个属性是函数 function ,绑定context 这个上下文到该 function 的执行作用域
代码中已经给出了注释,还是可以看一下这个extend 方法(明白大概流程就行,这些都是具函数,目的是了解这些方法做了什么,不必逐句分析)
function forEach(obj, fn) { // Don't bother if no value provided if (obj === null || typeof obj === 'undefined') { return; } // Force an array if not already something iterable if (typeof obj !== 'object') { /*eslint no-param-reassign:0*/ obj = [obj]; } if (isArray(obj)) { // Iterate over array values for (var i = 0, l = obj.length; i < l; i++) { fn.call(null, obj[i], i, obj); } } else { // Iterate over object keys for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { fn.call(null, obj[key], key, obj); } } } } function bind(fn, thisArg) { return function wrap() { var args = new Array(arguments.length); for (var i = 0; i < args.length; i++) { args[i] = arguments[i]; } return fn.apply(thisArg, args); }; }; function extend(a, b, thisArg) { forEach(b, function assignValue(val, key) { if (thisArg && typeof val === 'function') { a[key] = bind(val, thisArg); } else { a[key] = val; } }); return a; } - 将 context 这个实例上所有的可枚举属性深度复制到 instance 上,并且不绑定上下文(至于为什么这里还不太清楚,继续往下看吧)
createInstance 这个方法流程基本清楚了,现在只需要具体了解一下两个东西
- context
- instance,即 Axios.prototype.request 这个方法
搞清楚这两个具体内容,实例化 这个流程就基本跑完了
context
context,由代码就可以看出来,是Axios这个类的一个实例
var context = new Axios(defaultConfig);
还是先打印看一下,这个实例是什么,有哪些属性,然后再去看这个 Axios 类
进入定义 Axios 这个类的代码,在 /core/Axios.js 文件中
//构造函数
function Axios(instanceConfig) {
// instanceConfig 是一个对象,属性是用户自定义的和默认的属性
this.defaults = instanceConfig;
// interceptors 即我们常用的axios的拦截器对象
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
构造函数比较简单,做了以下两件事
- 给实例的 defaults 属性初始化了一些属性,比如我们常见的 请求的 header,timeout等,上图 context 已经打印出来了,也可以看到。
- 初始化拦截器,这里还没有添加我们自定义的拦截方法到拦截器
为啥这里我要说到这一步还没有添加自定义的拦截方法到拦截器,因为我看到new InterceptorManager的时候没有传参,所以得出结论。后来一想,到这一步还在实例化 axios 阶段,也就是我们封装时的 axios.create() 阶段,压根还没到自定义拦截器方法( service.interceptors.request.use() 和service.interceptors.response.use() )的阶段,脑子抽了。所以应该是,此时拦截器还未自定义(不存在自定义的拦截器的拦截方法)
Axios.js 这个文件里其他部分的代码,都是在做一件事,就是给 Axios 的 prototype 对象添加属性,代码的逻辑不细看了,后面用到再分析,还是先打印看一下添加了哪些属性,继续走实例化的流程
console.log('Axios.prototype',Object.keys(Axios.prototype))
回到上文的主线 axios.js 中的 createInstance 方法
context 已经明确是什么了
它是 Axios 类的一个实例,defaults 属性是包含我们自定义的配置和默认配置的对象,interceptors 是一个拦截器对象,有 request 拦截器和 response 拦截器
至于下一步的 instance ,也就是上面分析 createInstance 函数大概流程的时候的 第 2, 3和 4 步
instance
它实际上就是 Axios.prototype.request 方法,只不过改变了该方法的执行上下文,也就是说,我们 import 到项目里的 axios,其实是 Axios.prototype.request 方法。初始化部分 开始就打印过我们导入的 axios 模块,留了两个疑问 fn 和 thisArg,现在已经明确了
上面 分析 Axios.js 文件的时候没看这个 方法,现在看一下这个方法的逻辑
因为这段代码在我们实例化后以 axios() 方式调用时才会执行,具体逻辑在后面调用部分再一步步分析,这里简单分析一下这段代码作用就行
有一个细节提一下
关于函数的 arguments,它是调用时传入的实参生成的数组,传入几个参数该数组的长度就是多少,与函数定义时定义的形参个数无关,写一段测试代码就明白了
var test = function (a) {
console.log(arguments)
console.log(a)
}
test('a',1,2)
//Arguments(3) ["a", 1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// a
Axios.prototype.request 部分代码
/**
* Dispatch a request
*
* @param {Object} config The config specific for this request (merged with this.defaults)
*/
Axios.prototype.request = function request(config) {
// Allow for axios('example/url'[, config]) a la fetch API
//接收一个形参 config(调用该方法时传入的第一参数)
//判断传入的第一个参数的类型
if (typeof config === 'string') {
//传入的第二个参数赋值给 config
config = arguments[1] || {};
//添加url属性至 config,值为传入的第一个参数
config.url = arguments[0];
} else {
config = config || {};
}
/* 因为绑定的执行上下文是 new Axios()生成的实例,this.defaults就是实例的defaults属性
mergeConfig方法做的事情是将 config 和 defaults属性处理合并
*/
config = mergeConfig(this.defaults, config);
// 设置 config的 method属性
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
// 连接拦截器中间件
var chain = [dispatchRequest, undefined];
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) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
这段代码,大致流程是根据我们传入的配置和自定义的配置,生成了一个http请求,并添加了拦截器中间件,所以我们调用 axios()会根据传入的参数发起一个http请求
所以,上面第 2 步,是将instance 定义为一个发起http请求的方法
第 3 步,很简单,instance 是 request 方法,通过extend 方法将Axios.prototype对象的可枚举属性添加至 instance(再说一下,function也是对象,也可以添加属性,上面已经写过测试代码),如果属性是function,还要给它绑定执行作用域
第 4 步,还是一样的流程,通过extend方法将 context对象的可枚举属性添加至instance,只不过少了一步,将类型是 function的属性的绑定执行上下文,这个疑问在上面已经抛出来了
实例化的流程就先到这里吧,流程很简单,废话有些多了
之所以花这么多时间走这个流程,或者想走一下axios源码流程的初衷,是因为一个错误的写法。当我试图使用 axios.interceptors 给请求添加拦截器属性,发现报了一个undefined错误,于是我打印axios,得到了一个函数,而这个函数只是一个工具类函数,而不是我们常见的写法,一个显示的函数、类或者Object。这种方式和思想之前是没有去详细了解过,所以更多的是了解学习这种思路。
实例化部分到这里吧,后续再写其他部分的源码梳理
调用
调用部分,就从 api 入手,分析几个常用的 api 的流程,整个 axios 的结构就基本清楚了
Requests
axios(config)
// Send a POST request
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
})
这一步上面已经分析过了,其实就是调用了 Axios.prototype.request 方法。现在重点看一下这个方法
Axios.prototype.request = function request(config) {
// Allow for axios('example/url'[, config]) a la fetch API
//接收一个形参 config(调用该方法时传入的第一参数)
//判断传入的第一个参数的类型
if (typeof config === 'string') {
//传入的第二个参数赋值给 config
config = arguments[1] || {};
//添加url属性至 config,值为传入的第一个参数
config.url = arguments[0];
} else {
config = config || {};
}
/* 因为绑定的执行上下文是 new Axios()生成的实例,this.defaults就是实例的defaults属性
mergeConfig方法做的事情是将 config 和 defaults属性处理并合并
*/
config = mergeConfig(this.defaults, config);
// 设置 config的 method属性
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
// 连接拦截器中间件
var chain = [dispatchRequest, undefined];
//生成一个Promise对象
var promise = Promise.resolve(config);
/*
将 request (请求前)拦截器 (两个方法)两两插入chain 数组的头部,
插入的相邻的两项分别为 fulfilled 和 rejected 两种状态时调用的函数
*/
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
//将 response (请求后)拦截器推入到 chain 数组的尾部
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
/*
Promise.then链式调用 chain 数组里的方法
Promise.then 接受两个参数,第一个参数是resolved状态的回调函数,
第二个参数(可选)是rejected状态的回调函数。
shift()方法会删除数组第一个元素,并返回该元素
每执行一次 Promise.then(chain.shift(), chain.shift()),
chain 数组头部相邻的两个元素被删除,第一个作为 Promise状态变为 resolved 时调用的方法
被传入,第二个作为 Promise状态变为 rejected 时调用的方法被传入
所有 请求前拦截器 被 Promise.then 依次执行后执行 dispatchRequest(http请求方法)
http请求以 Promise 方式执行完后开始 以同样的方式依次执行 请求后拦截器
*/
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
这个方法我在代码中已经给出了比较详细的注释,重点在三个地方
- chain 数组
- Promise.then() 方法依次调用 chain 数组中的方法
- dispatchRequest 方法
前两个代码注释已经很清楚了,下面看一下 dispatchRequest 方法的源码
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
/**
* Dispatch a request to the server using the configured adapter.
*
* @param {object} config The config that is to be used for the request
* @returns {Promise} The Promise to be fulfilled
*/
module.exports = function dispatchRequest(config) {
// 取消请求的方法
throwIfCancellationRequested(config);
// Ensure headers exist
config.headers = config.headers || {};
// 转换请求的数据
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
// 合并自定义的配置和默认配置
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers
);
utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {
delete config.headers[method];
}
);
var adapter = config.adapter || defaults.adapter;
// adapter即为发起http请求的方法
return adapter(config).then(function onAdapterResolution(response) {
//取消请求
throwIfCancellationRequested(config);
// 转换请求后的数据
response.data = transformData(
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 方法
取消请求在后面使用时再去看源码
简单看一下 adapter 部分的代码
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// 浏览器环境
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// node 环境
adapter = require('./adapters/http');
}
return adapter;
}
var defaults = {
adapter: getDefaultAdapter()
}
这个文件中的代码,做的是根据运行环境来创建请求。axios之所以既能在浏览器端运行,又能在node.js中运行,主要代码就在这里。
- 浏览器端,通过 XMLHttpRequest 对象创建请求
- node.js 中,通过 http 模块创建请求
创建请求的具体逻辑就不看了,了解 axios 发起请求逻辑就可以了
axios(url[, config])
这种写法的处理逻辑,其实前面已经看过了
Axios.prototype.request = function request(config) {
// Allow for axios('example/url'[, config]) a la fetch API
//接收一个形参 config(调用该方法时传入的第一参数)
//判断传入的第一个参数的类型
if (typeof config === 'string') {
//传入的第二个参数赋值给 config
config = arguments[1] || {};
//添加url属性至 config,值为传入的第一个参数
config.url = arguments[0];
} else {
config = config || {};
}
// ......
}
判断传入的第一个参数类型,如果是 string,便会将该值作为请求的 url
Request method aliases
-
axios.request(config)
-
axios.get(url[, config])
-
axios.delete(url[, config])
-
axios.head(url[, config])
-
axios.options(url[, config])
-
axios.post(url[, data[, config]])
-
axios.put(url[, data[, config]])
-
axios.patch(url[, data[, config]])
第一种的 request方法,其实就是 axios(),上面已经清楚了
现在看其他几种写法的处理逻辑
代码还是在 Axios.js 文件中
// Provide aliases for supported request methods
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, data, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url,
data: data
}));
};
})
逻辑很简单,就不细说了,流程就是把这七种 http 请求方式 添加到原型上,相当于做了语义化处理,让我们以请求方式的形式调用,预处理后还是调用 request 方法