The introduction
Last time we looked at the basic examples and configuration of Axios, this time we’ll focus on its core request logic and cancel request logic
Axios source code analysis (a) — Axios instance and configuration
axios.request
Now let’s take a look at the core logic of making a request. This logic consists of three parts, and we’ll work our way down
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;
};
Copy the code
This code isn’t that hard to understand, so let’s break it down
-
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'; } Copy the code
If config is a string, it is considered as the URL attribute of the config object. Finally, the config object is used to ensure the consistency of the data structure. Then, the default configuration of this instance is merged, and the method attribute is guaranteed. At least, is the ‘get’
Method is not specified in the default configuration, so you must assign a value to method so it cannot be null.
-
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; Copy the code
DispatchRequest is the abstract method that actually sends the request, which we’ll explain in detail below, while interceptors.request and interceptors.response are the request and response interceptors. For then and catch, each method returns a Promise, puts it in the chain array, and then loops through the array to generate a Promise call chain. As long as there is an exception thrown in the middle, it will go to the nearest catch. If the catch returns a Promise of a resolve state, then the call chain can continue down
Request is added to the InterceptorManager class in order. The first interceptor to be added will be called last. This is actually the case. If multiple request interceptor rules are added and the order is required, the first interceptor will be called last. Always write backwards, otherwise you won’t get the result you want
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) {
deleteconfig.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);
});
};
Copy the code
There’s also the code for Cancel, so let’s skip over it and focus on the request part
-
config.data = transformData( config.data, config.headers, config.transformRequest ); Copy the code
The config.transformRequest method in configuration is applied to config.data using the transformData method
-
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) { deleteconfig.headers[method]; });Copy the code
This section merges headers from different sources, and you can clearly see the source in the code, This includes config.headers.com common configuration, config.headers[config.method] configuration (i.e., config.headers. Get/config.headers. Post, etc…..) The merge method is a deep copy, so delete all of the common, get, and POST configurations without affecting the original object
-
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); }); Copy the code
The last part is also easy to understand, calling the adapter, passing config, and then processing the then, or catch, step, where the config.transformResponse method is applied to the result
adapter
The final process of sending the request will be implemented in different adapters. Personally, I don’t use Node very much. I’ll take a look at the browser adapter, which is lib/ Adapters /xhr.js
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestData = config.data;
var requestHeaders = config.headers;
// Delete content-type for formData
if(utils.isFormData(requestData)) {... }var request = new XMLHttpRequest();
// Set header Authorization
if(config.auth) {... }var fullPath = buildFullPath(config.baseURL, config.url);
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// Set the timeout period
request.timeout = config.timeout;
// Listen for XHR events
request.onreadystatechange = function handleLoad() {... }; request.onabort =function handleAbort() {... }; request.onerror =function handleError() {... }; request.ontimeout =function handleTimeout() {... };// Against XSRF/CSRF attacks, you can configure cookies to be placed in headers
if(utils.isStandardBrowserEnv()) {... }// Set the other config.headers to the header
if ('setRequestHeader' inrequest) {... }// Set the withCredentials attribute
if(! utils.isUndefined(config.withCredentials)) {... }/ / set the responseType
if(config.responseType) {... }// Set the upload progress
if (typeof config.onDownloadProgress === 'function') {... }if (typeof config.onUploadProgress === 'function'&& request.upload) {... }// Cancel the requested action
if(config.cancelToken) {... }// Request body
if(! requestData) { requestData =null;
}
/ / send the request
request.send(requestData);
});
};
Copy the code
Here I have omitted most of the code, so that the whole adaptation process is more clear, I have marked the basic process in the above code comments, let’s take a step by step
-
if (utils.isFormData(requestData)) { delete requestHeaders['Content-Type']; // Let the browser set it } Copy the code
If it’s FormData, remove the content-Type and let the browser set it, which is usually 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); } Copy the code
This section provides a quick way to set HTTP authentication. The Authorization field is added to the request header based on the user name and password. The value is the user name and password encoded in Basic and Base64
-
var fullPath = buildFullPath(config.baseURL, config.url); request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true); Copy the code
It initializes a request, including the request method, the request address, whether it is asynchronous, etc. The request method is all lowercase, and the request address is spelled with the parameters and address, as in: 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; varresponseData = ! 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; }; Copy the code
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 )); }};Copy the code
We’ll focus on onreadyStatechange, which is the processing of the request response, and we’ll notice that there’s an important comment in the status statement, which says: If the request has an error, it is handled by the onError handler, with the exception of file: Most browsers will return status: 0, even if the request was successful. As we know, the HTTP status code 200 indicates success, so this code makes a special determination
The rest is handled as normal, passing resolve and reject to settle methods, where the judgment logic can define its own validateStatus method
Finally, there are a few additional processes that don’t affect the main flow, but are worth looking at
-
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; }}Copy the code
If you have xsrfHeaderName and xsrfCookieName configured, the cookie values are automatically read and carried to the header when the request is made
-
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 requestrequest.setRequestHeader(key, val); }}); }Copy the code
If the headers attribute is set, it goes to the request header. Note that the content-Type header is dropped if the request body has no data
Finally send ajax request, if there is no requestData, send(NULL)
Cancel a request
In the previous code, we skipped the cancellation request processing logic, because it has little to do with the logic of the topic
usage
So let’s review, how does Cancel work, which most of you probably haven’t used
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.');
Copy the code
Canceltoken.source () : canceltoken.source () : canceltoken.source () : canceltoken.source () : canceltoken.source () : canceltoken.source () The request can be cancelled at any time by calling the Cancel method
So, this source is the core of cancellation
Cancel core code
var Cancel = require('./Cancel');
function CancelToken(executor) {
if (typeofexecutor ! = ='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;
Copy the code
Resolution:
The canceltoken. source method, which generates a token from CancelToken and, while generating, CancelToken can pass a callback function that binds certain methods to the Cancel attribute
Moving on to the CancelToken factory, when called through the new operator, the instance itself is returned, that is, the token is an instance object of the CancelToken
During initialization, the token object has two attributes: Promise and Reason
First bind the resolvePromise variable to the resolve method of a promise property, and then pass a cancel method (canceltoken.source ().cancel) into the callback. When cancel is called, the resolve Promise will be used. The reason attribute of the token object may also have a value, which means that the token object has been cancelled
Token objects also have a throwIfRequested method, which checks whether there is a reason value to throw an exception, so that the request directly goes to the catch phase
Lib /cancel/ cancel. js and lib/cancel/ iscancel. js files are used to determine whether an instance has been cancelled
Cancel is used in the request
In the process of analyzing the sending of the request before, the logic of Cancel appeared, and we completed this part together
// lib/axios.js
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
Copy the code
Here is mainly the CancelToken factory that produces the source
// 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);
});
}
Copy the code
Here, in the initialization, success and failure of the request, throwIfRequested is used to determine whether the user has called the cancel method. Once it is called, an exception will be thrown directly and the process of catch will not be processed regardless of the status of the request
The throwIfRequested method will throw exceptions only if the token has a reason attribute. When the cancel method is not called, the token will never have a reason attribute, so throwIfRequested can be called at any time. Without worrying about whether the request is cancelled
// 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;
});
}
Copy the code
In the adapter, when you initialize the Ajax object, you bind a promise. Remember that the Token object has a Promise property, which is used here. When the promise is resolved, the user calls the cancel method, This gets the Ajax object through the closure and calls Request.Abort (), which in the XHR process also interrupts the request and sets the entire adapter state to reject
At this point, cancel participates in all phases of the request, and at any time you cancel, you go straight into the Reject process
conclusion
That’s the core of Axios. The logic isn’t that hard to understand. It’s about packaging ideas and some design patterns that you can learn from. If you have new ideas, you can also go to GitHub directly to raise requirements. I hope you don’t be afraid to look at the source code. After all, the source code is written more beautiful than the project we took over, isn’t it?