引言
上一次我们初步认识了 Axios 的基本实例与基础配置,这一次我们主要分析一下他的核心请求逻辑以及取消请求逻辑
axios.request
接下来我们看看发请求的核心逻辑,这部分逻辑由 3 个部分组成,我们一层层看下去
request
// lib/core/Axios.js
var dispatchRequest = require('./dispatchRequest');
Axios.prototype.request = function request(config) {
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;
};
这段代码其实不难理解,我们分解一下
-
if (typeof config === 'string') { config = arguments[1] || {}; config.url = arguments[0]; } else { config = config || {}; } config = mergeConfig(this.defaults, config); if (config.method) { config.method = config.method.toLowerCase(); } else if (this.defaults.method) { config.method = this.defaults.method.toLowerCase(); } else { config.method = 'get'; }这里先做了一下兼容,如果
config是字符串,那么就认为它是config对象的url属性,最终使用的还是config对象,保证数据结构的一致性,然后将传参与本实例的默认配置合并,并保证method属性一定有,至少是'get'这里特意判断
method,一是为了格式化为小写,再一个默认配置里没有method的配置,所以为了不能为空,必须赋一个值,不过这里我不是很理解为何不给method一个默认值。。 -
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;dispatchRequest是真正发请求的抽象方法,我们会在下面详细说明,而interceptors.request和interceptors.response则是请求和响应的拦截器,连续两个方法为一组,分别对应then和catch中,每个方法都返回一个Promise,放到chain数组中,然后通过循环这个数组,生成一个Promise调用链,中间只要有一步抛异常,就会走到最近的catch中,如果catch返回了一个resolve状态的Promise,那么调用链还可以继续往下走,有点迷惑的童鞋可以看下图帮助理解
这里虽然文档没有明说,但是分析代码可以看出,请求拦截器
interceptors.request我们使用的时候是顺序加入到InterceptorManager类里,但是调用的时候却是顺序循环并通过Array.prototype.unshift到调用链数组里,那么也就是说第一个加入的拦截器会放在最后一个去调用,实际上也确实如此,这里可能有点小坑,如果加了多个请求拦截器规则又需要有顺序的话,一定要倒着写,不然无法得到想要的结果
dispatchRequest
module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);
// Ensure headers exist
config.headers = config.headers || {};
// Transform request data
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
// Flatten headers
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;
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);
});
};
这里还有 Cancel 的代码,我们暂且略过,着重看请求部分
-
config.data = transformData( config.data, config.headers, config.transformRequest );这里通过
transformData这个方法,将配置中的config.transformRequest方法,应用到config.data中,关于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]; } );这部分合并了不同来源的
headers,都有什么来源呢,代码里也能很清晰的看到,包括通用配置config.headers.common、特定方法的配置config.headers[config.method](也就是config.headers.get/config.headers.post等等....),以及当下请求传来的headers配置,最后,因为common,get,post等等这些配置都写在 headers 里,发请求时是不需要的,所以通通删了,当然这里merge方法是个深拷贝,所以随便删,不影响原对象 -
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传过去,然后处理then,或者catch步骤,这里对结果应用了config.transformResponse方法
adapter
发请求的最终过程,还是在不同的适配器里实现,因为我个人用 Node 比较少,这里就看一下浏览器的适配器,也就是 lib/adapters/xhr.js
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestData = config.data;
var requestHeaders = config.headers;
// formData 时删除 Content-Type
if (utils.isFormData(requestData)) {...}
var request = new XMLHttpRequest();
// 设置 header Authorization
if (config.auth) {...}
var fullPath = buildFullPath(config.baseURL, config.url);
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// 设置超时时间
request.timeout = config.timeout;
// 监听 xhr 各种事件
request.onreadystatechange = function handleLoad() {...};
request.onabort = function handleAbort() {...};
request.onerror = function handleError() {...};
request.ontimeout = function handleTimeout() {...};
// 应对 xsrf/csrf 攻击,可以配置将 cookie 放在 header 中
if (utils.isStandardBrowserEnv()) {...}
// 将其他 config.headers 设置到 header 中
if ('setRequestHeader' in request) {...}
// 设置 withCredentials 属性
if (!utils.isUndefined(config.withCredentials)) {...}
// 设置 responseType
if (config.responseType) {...}
// 设置上传文件的进度
if (typeof config.onDownloadProgress === 'function') {...}
if (typeof config.onUploadProgress === 'function' && request.upload) {...}
// 取消请求的操作
if (config.cancelToken) {...}
// 请求主体
if (!requestData) {
requestData = null;
}
// 发请求
request.send(requestData);
});
};
这里我将大部分代码都省略了,这样对整个适配过程更加清楚,基本的过程我都标注在上面代码里的注释了,接下来我们一步一步的细看
-
if (utils.isFormData(requestData)) { delete requestHeaders['Content-Type']; // Let the browser set it }如果是
FormData,就删除Content-Type,让浏览器自己设置,这里一般都会设置成multipart/form-data -
if (config.auth) { var username = config.auth.username || ''; var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : ''; requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password); }这里提供了一个快速设置 HTTP 认证的方法,设置方法就是通过用户名和密码,在请求头里增加
Authorization字段,值为Basic加 base64 编码后的用户名密码字符串,具体可以参见 HTTP 身份认证 -
var fullPath = buildFullPath(config.baseURL, config.url); request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);这里初始化一个请求,包括请求方法,请求地址,是否为异步等等,请求方法全部转为小写,请求地址是参数和地址拼起来的,就如:
xxx?a=b&c=d -
request.onreadystatechange = function handleLoad() { if (!request || request.readyState !== 4) { return; } // The request errored out and we didn't get a response, this will be // handled by onerror instead // With one exception: request that using file: protocol, most browsers // will return status as 0 even though it's a successful request if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) { return; } // Prepare the response var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null; var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response; var response = { data: responseData, status: request.status, statusText: request.statusText, headers: responseHeaders, config: config, request: request }; settle(resolve, reject, response); // Clean up request request = null; };module.exports = function settle(resolve, reject, response) { var validateStatus = response.config.validateStatus; if (!response.status || !validateStatus || validateStatus(response.status)) { resolve(response); } else { reject(createError( 'Request failed with status code ' + response.status, response.config, null, response.request, response )); } };这里主要看一下
onreadystatechange,也就是请求响应之后的处理,我们注意到在判断status的时候有一句注释,很重要,就是说:如果请求发生错误了,是通过onerror这个handler来处理的,但是有一种例外,那就是用file:发起的请求,大多数浏览器会返回status:0,即使这个请求成功了,我们知道,一般 HTTP 状态码,200 表示成功,所以,这里代码特殊判断了一下其余的就正常处理,将
resolve和reject交给 settle 方法来处理,这里判断逻辑也可以定义自己的validateStatus方法最后还有几个额外的处理,虽然不影响主流程,但是还是可以看一下
-
if (utils.isStandardBrowserEnv()) { // Add xsrf header var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ? cookies.read(config.xsrfCookieName) : undefined; if (xsrfValue) { requestHeaders[config.xsrfHeaderName] = xsrfValue; } }如果你配置了
xsrfHeaderName和xsrfCookieName这个属性,那么在发请求时,会自动读取cookie中的相应值并带到header中 -
var requestHeaders = config.headers; if ('setRequestHeader' in request) { utils.forEach(requestHeaders, function setRequestHeader(val, key) { if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') { // Remove Content-Type if data is undefined delete requestHeaders[key]; } else { // Otherwise add header to the request request.setRequestHeader(key, val); } }); }如果设置了
headers属性,则设置到请求头里,注意,这里如果请求体没有数据的话,会直接删掉content-type头
最后发送 ajax 请求,如果没有 requestData ,则 send(null)
取消一个请求
之前的几个代码里,我们都跳过了取消请求相关的处理逻辑,因为跟主题的逻辑关系不大,在这里,我们统一解析一下
用法
我们先回顾一下,Cancel 这个功能是怎么用的,可能大部分人都没用过
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
}
})
// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
首先,通过 CancelToken.source() 方法生成一个 source 对象,source 对象里有两个属性,一个是 token,一个是 cancel 方法,请求的时候将 token 传到配置里,即可在任意时刻通过调用 cancel 方法来取消请求
所以,这个 source 就是取消的核心
Cancel 核心代码
var Cancel = require('./Cancel');
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
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);
});
}
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
module.exports = CancelToken;
解析:
CancelToken.source 方法,通过 CancelToken 生成一个 token,并且生成的同时, CancelToken 可以传一个回调函数,将某些方法绑定到了 cancel 属性上
继续看 CancelToken 工厂,通过 new 操作符调用的时候,会返回实例本身,也就是说,token 就是 CancelToken 的一个实例对象
在初始化过程中,token 对象会有 promise 与 reason 两个属性
首先将 resolvePromise 变量绑定为一个 promise 属性的 resolve 方法,接着在回调函数中传一个 cancel 方法(也就是 CancelToken.source().cancel 方法),当 cancel 被调用的时候,会 resolve Promise,同时,token 这个对象中的 reason 属性也可能会有值,有值的话即代表已经取消过了,可以防止重复使用
token 对象还有一个 throwIfRequested 方法,即通过判断是否有 reason 值,来抛一个异常,使请求直接走到 catch 阶段
两外两个文件 lib/cancel/Cancel.js 和 lib/cancel/isCancel.js 这里就不详细说明了,代码很少,基本是用某个实例的静态属性用来判断是否已经取消的逻辑,大家可以自行看一下
请求中使用 Cancel
之前我们分析发请求的过程中,出现了 Cancel 逻辑,一起将这部分补完
// lib/axios.js
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
这里主要是导出了生产 source 的工厂 CancelToken
// lib/core/dispatchRequest.js
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
function dispatchRequest(config) {
throwIfCancellationRequested(config);
...
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
...
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);
...
}
return Promise.reject(reason);
});
}
这里在发请求的初始化、请求成功、请求失败时都通过 throwIfRequested 判断了用户是否调用过 cancel 方法,一旦判断调用过,直接抛出异常,走到 catch 流程,不管请求状态如何都不去处理了
throwIfRequested方法只有当token有reason属性的时候,才会抛异常,当没有调用cancel方法的时候,token永远没有reason属性,因此throwIfRequested可以在任何时候调用,而不用担心请求是否被取消
// lib/adapters/xhr.js
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
在适配器里,初始化 ajax 对象的时候,绑定一个 promise,大家还记得 token 对象里有一个 promise 属性吧,就是用在这里的,当这个 promise 被 resolve 的时候,也就是用户调用了 cancel 方法,这里会通过闭包获取到 ajax 对象,然后调用 request.abort() ,这样在 xhr 流程中,也中断了请求的继续发送,同时将整个适配器的状态置为 reject
到这里,请求的所有阶段就都有了 cancel 的参与,在任何时候取消,都可以直接走入 reject 流程
结语
以上就是 Axios 的核心功能,其实逻辑并不难理解,主要是封装思路以及一些设计模式值得我们学习,另外,也可以了解一些请求过程中的坑(比如:file 开头的请求),以后在使用的过程中,如果遇到了文档里看不到的问题,不妨在源码里寻找答案,如果有新的想法,也可以直接去 GitHub 提需求,希望大家不要畏惧看源码,毕竟,源码写的要比我们接手的项目更加优美,不是吗?