基类 Axios
跟随入口 index.js 进入/lib/axios.js
,第一个方法则是createInstance
创建Axios
实例。先理解一些属性后,再看 /core/Axios.js 的代码。
- interceptors,拦截器 /core/InterceptorManager.js
interceptors.request,interceptors.response为InterceptorManager的实例
InterceptorManager
的本质是一个订阅发布者模型
handlers
是收集订阅者的容器
use
是订阅方法,向容器中添加{ fulfilled, rejected },分别代表Promise的resolve和reject的两种状态
eject
是退订方法
forEach
进行了重写,绑定方法,遍历通知订阅回调函数的执行发布
- dispatchRequest,请求的触发
dispatchRequest 的本质是调用了config中的adapter
方法,adapter在客户端是返回一个Promise
,内部逻辑是对XMLHttpRequest
的封装,服务端是一个基于Node.js
的 http server。后面会讲到 adapter。
/core/dispatchRequest
module.exports = function dispathRequest(config) {
// ...
// config.adapter 返回Promise,在客户端本质上是对XMLHttpRequest的封装
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
// ...
return response;
}, function onAdapterRejection(reason) {
// ...
return Promise.reject(reason)
})
}
- cancelToken 取消请求的令牌
cancelToken
是用于执行 XMLHttpRequest 中断请求的方法abort
,内部通过高阶函数实现,稍显绕脑,作者的设计思路,尤其是外部调用 Promise 中的 resolve 方法让人眼前一亮
,我们放在最后讲。
基类Axios /core/Axios.js
function Axios(instanceConfig) {
// 缓存请求设置
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
}
}
// axios[method]实际上就是调用的request
Axios.prototype.request = function request(config) {
if (typeof config === 'string') {
// 满足axios('example/url')调用
config = arguments[1] || {};
config.url = arguments[0]
} else {
config = config || {};
}
// ...
// 优先入参中的方法,其次为实例化时默认的方法,再次默认为 GET请求
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);
// 拦截器请求订阅放在dispatchRequest前
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 拦截器响应订阅放在dispatchRequest后
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
// 拦截器依次执行,并修改原订阅数组,触发dispatchRequest,执行请求后,执行响应拦截器
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift())
}
return promise;
}
// 返回请求路径,处理了get请求的queryString拼接
Axios.prototype.getUri = function (...) {
// ...
}
// ...
module.exports = Axios;
- methods,请求方法
优先参数设置,默认为GET
方法
'delete', 'get', 'head', 'options'方法类似于get,request参数中接收method,url但不接收data
'post', 'put', 'patch'方法类似于post,request参数中接收method,url以及data
axios[method]实际上就是调用的request({ method, url, ... })
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
}));
};
});
defaultConfig adapter - xhrAdapter,客户端XMLHttpRequest
/lib/axios.js 首先会创建一个默认请求设置的Axios
实例,默认设置中adapter
属性在客户端指向文件/adapters/xhr
,导出一个方法,即请求的发起 new XMLHttpRequest(),并返回一个Promise。
module.exports = function xhrAdapter(config) {
// ...
if (utils.isFormData(requestData)) {
// 如果提交的是form表单,则要浏览器去设置Content-Type,"multipart/form-data"
delete requestHeaders['Content-Type'];
}
// 实例化XMLHttpRequest对象
var request = new XMLHttpRequest();
if (config.auth) {
// ...
// 设置 Authorization 头信息
requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}
// ...
// 初始化一个异步请求
request.open(method, url, true)
// 设置超时时间
request.timeout = confit.timeout;
// 当request的readyState变化时,触发
// 0-UNSENT-代理被创建,但尚未调用open()方法
// 1-OPENED-open()方法已经被调用
// 2-HEADERS_RECEIVED-send()方法已经被调用,并且头部和状态已经可获得
// 3-LOADING-下载中,responseText已包含部分数据
// 4-DONE-下载操作已完成
request.onreadystatechange = function handleLoad() {
// 处理已完成的请求
if (!request || request.readyState !==4) return;
// status-只读状态码,请求完成前以及请求出错,状态码均为0
// responseURL-响应的序列化URL
// 处理已正常完成,且响应URL为非文件的请求
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) return;
var response = {
data: requset.responseType === 'text' ? requset.responseText : request.response,
status: request.status,
statusText: request.statusText,
headers: parseHeaders(request.getAllResponseHeaders()),
config: config,
request: request
}
resolve(response);
request = null;
}
// 请求终止
request.onabort = function ...
// 请求异常
request.onerror = function ...
// 请求超时,config中可以设置属性timeoutErrorMessage
// 这个属性是axios官方没有说的,定义用于reject提供的异常message
request.ontimeout = function...
// 配置XMLHttpRequest头信息属性 responseType, withCredentials等...
// 绑定进度函数 config.onDownloadProgress config.onUploadProgress
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', config.onDownloadProgress);
}
// 上传进度还需要判断浏览器是否支持,loadstart, loadend, progress等进度都需要绑定在upload上
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', config.onUploadProgress)
}
// 取消令牌,终止请求,Promise状态reject
if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) return;
request.abort();
reject(cancel);
request = null;
})
}
// 发送请求
request.send(requestData)
}
请求的流程
到这里,整个请求的流程已经清晰了。
- 当执行axios(url)或者axios[method]对应的都是Axios中的request方法
- 拦截器interceptors收集订阅,顺序为,请求拦截器,dispatchRequest,响应拦截器
- 拦截器Promise.then(chain.shift())执行,首先执行请求拦截器,并改变原订阅数组
- 直至dispatchRequest触发config.adapter(客户端是XMLHttpRequest, 返回Promise)
- 后继续Promise.then(chain.shift()),执行响应拦截器,直至订阅数组长度为0
在过程4,dispatchRequest触发请求即XMLHttpRequest的执行过程是,open初始化,绑定所有方法,添加属性和配置后,send发起请求。
过程中执行绑定的方法,非预期时reject;只有当readyState为4时,才有可能resolve拿到我们期望的数据。
常见的使用Axios的方法总是配合着then + catch
或async/await + catch
使用。
CancelToken,用于中断取消请求
首先对比下CancelToken的源码与CancelToken的使用方式
CancelToken 源码 /cancel/CancelToken.js
function CancelToken(executor) {
if (typeof executor !== 'function') {
// executor必须是函数
throw new TypeError('executor must be a function.');
}
// 很关键!!
// promise可以在外部被调用resolve
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
// 标记1
executor(function cancel(message) {
// message是执行后面source.cancel()传入的参数
if (token.reason) {
// dispatchRequest 已经从通过 config.adapter 接收到响应结果了,会调用下面的 throwIfRequested 方法
// 无法手动终止请求
return;
}
// reason 理解成一个非空字符串就好
token.reason = new Cancel(message);
// 很关键!!
// 非Promise内部执行CancelToken.promise的Promise.resolve(token.reason)
resolvePromise(token.reason);
});
}
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
CancelToken.source = function source() {
var cancel;
// 定义 token 接收一个 CancelToken 实例
// 上文的标记1中的 executor 的参数 function cancel(message) 就对应下面的参数 c
// 定义 cancel 来接收c
// !!!那么,cancel() 就可以调用 token.promise 中的 Promise.resolve
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
// token 中有 Promise
token: token,
// cancel 可以调用 token.promise 中的 Promise.resolve
cancel: cancel
};
};
module.exports = CancelToken;
example: 本质上就是 cancel 执行了 token.promise 中的 Promise.resolve
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
// cancel the request (the message parameter is optional)
// 执行了 config.cancelToken.promise 中的 Promise.resolve
source.cancel('手动中断请求'');
Promise.resolve那么然后呢?还记得最初提到的XMLHttpRequest的abort方法吗?
xhr /adapters/xhr.js
// ...
// 都清晰了吧
if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) return;
request.abort();
reject(cancel);
request = null;
})
}
// ...
好啦!Axios源码解析到这里就结束了,希望大家能够看明白,能够喜欢!