0、写在前面
先掌握源码结构再到实际的运行使用中去复盘源码。就是 源码—>使用—>源码 的学习线路。
思维导图配合文章更清晰
0.1 取源码
源码取到打开已经打包好的文件 dist/axios.js 看一下,注释加上空行也就两千行不到。
git clone https://github.com/axios/axios.git
0.2 入口文件
打开package.json找到入口为index.js
index.js
module.exports = require('./lib/axios');
主要看的就是红框里这部分内容了。
由上至下大致分析一下文件及文件夹的主要内容。
adapter:适配器,浏览器环境用xhr,node环境用http
cancel:取消请求
core:核心代码
helpers:功能助手
axios.js:声明定义文件
default.js:默认的配置参数
utils.js:一些小工具函数集合
下面我们就从定义文件开始看起。
1、axios.js
1.1 直接找到声明语句开始
var axios = createInstance(defaults);
1.1.1 看传入的默认配置 defaults 定义了哪些东西
adapter:适配器根据运行环境选择哪种请求方式(浏览器用xhr—lib/adapters/xhr.js,node环境用http—lib/adapters/http.js)
transformRequest:请求数据处理方法数组
transformResponse:响应数据处理方法数组
timeout:响应最长返回时间,超过这个时间就取消请求
xsrfCookieName:xsrf在cookie内名称
xsrfHeaderName:xsrf在HTTP头内名称
maxContentLength:允许的响应内容的最大尺寸
maxBodyLength:(仅在node中生效)允许发送HTTP请求内容的最大尺寸
validateStatus:是否 2** 类型的状态码
headers:定义了共用的请求头 common 和各种不同请求方法的请求头(用了两个循环完成)
再看 function createInstance(defaultConfig) 里面的语句,也就5行代码,一行一行看。
1.1.2 返回 Axios 对象
var context = new Axios(defaultConfig);
打开 core/Axios.js 查看 Axios 所拥有的属性方法:
defaults:刚刚的默认配置赋值;
interceptors:拦截器,包括 request、response 两个对象;
request:发送的请求处理,里面主要处理有合并默认配置和自定义配置,确认请求方法,拦截器处理,最后循环执行拦截器至promise对象并返回该对象;
getUri:获取请求的完整地址;
两个forEach:循环定义HTTP请求方法(delete、get、head、options、post、put、patch),注意前四个与后三个传参的不同 function(url, config) 和 function(url, data, config)。另外再看一下拦截器 interceptors 所使用的对象,在 InterceptorManager.js中:
handlers:拦截处理数组;
use:增加一个拦截器;
eject:移除一个拦截器;
forEach:遍历处理,将有效的拦截器绑定上处理方法(这个可以在 Axios 对象方法 request 里看拦截器处理那段)。
1.1.3 定义返回 wrap 函数
var instance = bind(Axios.prototype.request, context);
打开 helpers/bind.js 查看 bind 函数,得到这里 instance 是被定义成了一个 wrap 函数,该函数返回值是Axios.prototype.request 方法调用结果,并且 request 内部this指向传入的值 context,传入参数为 args ,另外在前面我们看到这个 request 结果是返回 promise 类型对象的。
module.exports = 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);
};
};
1.1.4 这是将 Axios.prototype 定义的 request、getUri以及delete、get、head、options、post、put、patch 都以返回 wrap 函数的方式复制到 intance 上,形同上面的 bind(Axios.prototype.request, context)。
utils.extend(instance, Axios.prototype, context);
1.1.5 将创建的 context 对象扩展复制到 instance 上
utils.extend(instance, context);
1.1.6 返回 instance ,即 wrap 函数赋值给变量 axios
return instance;
1.2 将 Axios 暴露出来,允许继承
axios.Axios = Axios;
1.3 实例函数工厂,用这个可以建立自己的默认配置和拦截器等的实例
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
1.4 定义取消请求方法
具体使用看下一节 2.4。
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
1.5 定义多请求方法,需要可以与spread配合将返回值数组包装成多个变量返回值形式
这里结合后面的 2.5 使用更清楚。
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
以上就把整个axios从新建到内部核心动作大致看完了,下面我们通过使用再具体看一下实际使用过程中的运行情况。
2、从使用运行回到源码学习
2.1 安装
安装有两种方式,一种是包管理,一种是 script 标签引入,不管是哪种安装,我们都会得到一个全局变量 axios(包管理使用时需要自己导入定义一下)。
2.2 使用
2.2.1 通过方法名
看一下常用的 get 示例请求
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
})
先看为什么可以直接用 get 这个方法名呢?
这个就是 1.1.4 那节的代码 utils.extend(instance, Axios.prototype, context); 的作用,把 request、getUri以及delete、get、head、options、post、put、patch 都复制到了 axios 上,所以同样的道理可以得到以下几种请求方法,getUri 不是请求方法也不咋用到在这里先忽略。
axios.request(config)
axios.delete(url[, config])
axios.get(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]])
现在我们通过上面的示例get请求来看代码是怎么运行的。
Axios.js 中是通过以下代码来定义 get 请求的,那么示例代码中唯一的实参 '/user?ID=12345' 就代表着定义中的形参 url ,并没有传入config的实参。
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
}));
};
});
定义里返回的是this.request(......)方法,我们之前在 1.1.3 说过这个 this 是指向传入的 context 的,这个 this.request 就是里面的 request 方法 ,是返回 promise 对象的,所以在示例里我们可以接着 axios.get('/user?ID=12345') 写 then和catch 方法。
再看 this.request 里对传入参数的处理。
config || {}:这里我们没有传入 congfig 实参,所以 config 是 undefined ,这个结果就是后面的空对象 {}
{method: method,url: url}:method 是定义方法时就赋值的,跟定义的方法同名,url就是我们传入的 '/user?ID=12345'
utils.merge(.....):这个方法就是把上面的两个对象合并成一个对象传入 this.request
程序走到这里合成的这个对象应该如下:
{
method:"get",
url:"/user?ID=12345"
}
看下 request 定义代码,这里的 function request(config) 中的 config 就是上面合成的对象值。
Axios.prototype.request = function request(config) {
// Allow for axios('example/url'[, config]) a la fetch API
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
config = mergeConfig(this.defaults, config);
// Set 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';
}
// Hook up interceptors middleware
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;
};
先略掉开头一小段,其中有个 config 是否为 string 类型的判断和请求方法methods的处理,这个主要是 axios('/user?ID=12345') 使用形式的,我们待会说。现在从 var chain = [dispatchRequest, undefined]; 看起。
这是定义了一个拦截器数组 chain ,并且给了初始值 [dispatchRequest, undefined]。
var promise = Promise.resolve(config);
定义 promise 对象,方便后面的链式调用。
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);
});
增加请求和响应拦截器,我们没有另外定义,这里过后 chain 是没有变化的。
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
开始调用运行 chain 数据,这里参数为什么是 (chain.shift(), chain.shift()) 呢,可以看下上两句请求拦截器中(interceptor.fulfilled, interceptor.rejected) 成功与失败处理函数都是成对添加到数组的,所以这也就解释了为什么初始化 chain 的时候是 dispatchRequest 和一个undefined 的了。当然是为了保证后续的响应拦截器的成功与失败处理函数是成对的。
这个 dispatchRequest 定义在 dispatchRequest.js 文件里。我们对照源码一一分析。
throwIfCancellationRequested(config);
......
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
.....
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
在处理请求前先判断有没有取消该请求,有了就抛出异常。
// Ensure headers exist
config.headers = config.headers || {};
定义赋值请求头,在get示例我们并没有定义,所以这里的 config.headers 只有之前 default.js 文件中预先定义的部分,即:
headers:{
common:{
'Accept': 'application/json, text/plain, */*'
},
delete:{},
get:{},
head:{},
post:{
'Content-Type': 'application/x-www-form-urlencoded'
},
put:{
'Content-Type': 'application/x-www-form-urlencoded'
},
patch:{
'Content-Type': 'application/x-www-form-urlencoded'
},
}
// Transform request data
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
格式化请求数据,但是我们的get示例是没有请求 data 的,这里经过一圈处理 config.data 为 undefined。
// Flatten headers
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers
);
将几个对象合成一个对象赋值给 config.headers。
utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {
delete config.headers[method];
}
);
删除默认的 headers 配置
var adapter = config.adapter || defaults.adapter;
赋值适配器,需返回promise对象的,我们没有定义,就用默认的 default.adapter。
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// Transform response data
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);
});
终于到这了,config 进入 adapter
adapter: getDefaultAdapter()
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
}
return adapter;
}
打开两个文件看下都是返回的 new Promise(),如果调用过取消请求的方法会执行 function onAdapterRejection(reason) ,我们这里自然进入到了 function onAdapterResolution(response) ,这里就得到带有响应值的 promise 对象,这就保证了示例get中 axios.get() 后还可以再添加 then 方法。
关于 xhr.js 和 http.js 后续会出专门的文章解释。
2.2.2 参数调用
我们平时还会用一下的两方式去调用
axios("/user?ID=12345")
axios({
url:"/user?ID=12345",
method:'get" //post等方法一样
})
为什么我们可以直接用这种形式调用呢,请看 1.1.3 var instance = bind(Axios.prototype.request, context); 这个在定义的时候就把 request 这个给复制给了 变量本身,所以 axios() 中是直接用的 Axios.prototype.request 方法。
再看Axios.prototype.request中之前略过的几行代码,就是为了这种类型的使用做的判断。
// Allow for axios('example/url'[, config]) a la fetch API
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
config = mergeConfig(this.defaults, config);
// Set 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';
}
第一个判断, axios("/user?ID=12345") 中的config 就是 string 类型。
第二个判断,axios("/user?ID=12345") 中没有定义请求方法所以默认get。
经过第二个判断处理以后我们知道会扩展识别 config 的 method 属性,也就是说虽然 default.js 里没有定义到,但是程序处理的时候程序是可以读取或者建立 method 这个属性的,所以 axios({ url:"/user?ID=12345"}) 、axios({ url:"/user?ID=12345",method:'post"}) 也都是可以的。
另外中间还有有个 mergeConfig 方法定义在 mergeConfig.js 中,那么在这个文件里,我们看到所有可以设置的配置参数。
var valueFromConfig2Keys = ['url', 'method', 'data'];
var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy', 'params'];
var defaultToConfig2Keys = [
'baseURL', 'url', 'transformRequest', 'transformResponse', 'paramsSerializer',
'timeout', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName',
'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress',
'maxContentLength', 'maxBodyLength', 'validateStatus', 'maxRedirects', 'httpAgent',
'httpsAgent', 'cancelToken', 'socketPath', 'responseEncoding'
];
这里比较单一自己多看看。
2.3 axios.create()——自己创建 axios 实例
这个在文章 四、vue+ElementUI开发后台管理模板—方法指令、接口数据 第2大点 获取接口数据、模拟数据 里实际运用了,这里不再多说。
2.4 axios.CancelToken——取消请求
如果我们浏览SAP页面,在资源大、多或者响应比较慢的页面在发生跳转后,前一个页面的请求还会继续等待响应,那最好的是到新页面后,之前的请求应该全部取消,将资源留给新页面用。
我们先用 GitHub/chizijijiadami/vue-elementui-5 的代码看下这样的请求效果,这个是要配合之前用 phpstudy 写的跨域接口的,具体见文章 四、vue+ElementUI开发后台管理模板—方法指令、接口数据 底部部分。
获取代码后,做如下修改:
src>pages>Index>index.vue
created() {
this.getList();
- //this.getCrossDomainList();
+ this.getCrossDomainList();
},
src>pages>List>Detail>index.vue
+ created(){
+ console.log('list-detail');
+ }
对phpstudy中建立的网站文件增加一句 sleep(5) 代表延时5秒钟返回,可以模拟服务器延时情况。
上面这些修改后运行项目,打开首页后立即切换至详情页,在等待数秒后,控制台如下图。
要想实现跳转至新页面后,立即取消前面页面的请求,我们可以做如下操作:
src>data>store>modules>app.js
system: {
title: "大米工厂",
+ requestCancel:[]
},
......
mutations: {
+ ADD_SYSTEM_REQUEST_CANCEL:(state,c)=>{
+ state.system.requestCancel.push(c)
+ },
+ SET_SYSTEM_REQUEST_CANCEL:state=>{
+ state.system.requestCancel=[]
+ },
......
},
actions: {
+ addSystemRequestCancel({ commit },c){
+ commit('ADD_SYSTEM_REQUEST_CANCEL',c)
+ },
+ setSystemRequestCancel({ commit }){
+ commit('SET_SYSTEM_REQUEST_CANCEL')
+ },
......
}
src>common>utils>axiosApi.js,给每个请求添加取消方法。
import store from 'data/store'
import axios from 'axios'
import ErrorMessage from './errorMessage'
import { MessageBox } from 'element-ui'
var instance = axios.create({
baseURL: '',
timeout: 5000
});
+ const CancelToken = axios.CancelToken;
// 添加请求拦截器
instance.interceptors.request.use(
// 在发送请求之前做些什么
config => {
config.headers['version'] = '1'
+ config.cancelToken = new CancelToken((c) => {
+ store.dispatch('addSystemRequestCancel', c)
+ });
return config
},
error => {
Promise.reject(error)
}
);
src>common>routerFilter>index.js,登录权限信息肯定是不能取消的,这里应该在登录后实行这种机制。
传递取消信息,并且每到一个新页面前 requestCancel 数组应该清零。
if (to.path === '/login') {
next('/')
} else {
- next()
+ store.getters.app.system.requestCancel.forEach(cancel => {
+ cancel('Cancel')
+ })
+ store.dispatch("setSystemRequestCancel").then(() => {
+ next()
+ })
}
现在我们再运行切换页面,看下控制台在进入 detail 页之前那个跨域请求被取消报了错,并且弹出了错误提示,这里就要进一步修改,取消请求的情况是不应该弹窗报错的
src>common>utils>axiosApi.js
// 对响应错误做点什么
if (!error.response) {
// 服务器请求失败时错误提示
+ if (error.message !== 'Cancel') {
MessageBox({
message: `请求超时${ErrorMessage.API_ERROR_LOAD}`,
showCancelButton: false,
confirmButtonText: '确定',
type: 'error',
callback() { }
})
+ }
}
src>pages>Index>index.vue,这里添加错误处理代码
getCrossDomainList() {
api
.getCrossDomainList()
.then(res => {
console.log(res);
})
+ .catch(() => { });
}
以上修改完成后我们再切换看看,没有任何报错了
这里是axios 取消的一种方式,还有另一种使用方式差不多,自己试试这里就不多说了。
2.5 axios.all()——多请求并发
src>pages>Index>index.vue
created() {
// this.getList();
// this.getCrossDomainList();
+ this.all();
},
methods:{
......
all() {
axios.all([api.getList(), api.getCrossDomainList()]).then(arr => {
console.log(arr, "arr");
});
axios.all([api.getList(), api.getCrossDomainList()]).then(
axios.spread(function(one, two) {
console.log(one, two, "spread");
})
);
}
}
运行结果如下图,就是拆分了返回值数组。还有一点可以看到这个all里面会等请求执行成功以后才一起返回,如果取消了其中一个,就会返回 reject。
感谢阅读,喜欢的话点个赞吧:)
更多内容请关注后续文章。。。