在web开发中,往往需要在前端向后端请求数据,以前可能用到的就是ajax,但是大家现在可能用的是axios或者fetch,而在公司里往往会对axios进行二次封装去进行网络请求,今天就聊聊axios的一些源码知识,最后会给出一个我自己的axios封装。
首先说说axios的优点:
1、基于promise API;
2、请求拦截与响应拦截;
3、自动转换数据类型;
4、默认请求配置与取消请求;
axios与Axios
在axios中,可以发现一个很重要的构造函数Axios,那么先从axios与Axios关系说起。
在源码的axios.js中,有代码
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
// axios,axios.create()其实返回的都是request函数,request函数才是真正发请求的函数
// Axios.prototype.request.bind(context)
var instance = bind(Axios.prototype.request, context); //axios
// 将原型上的方法拷贝到instance上:get、request、post、put等
utils.extend(instance, Axios.prototype, context);
// 将context的属性拷贝到instance上:defaults以及拦截器
utils.extend(instance, context);
return instance;
}
// 可以看出axios其实就是一个request实例,而这个实例上挂载这很多方法。
var axios = createInstance(defaults);
//Axios构造函数挂载到axios上
axios.Axios = Axios;
Axios与axios的关系,可以从这段代码中知道:
1、从形式上看,Axios会被挂载到axios的Axios上;
2、从功能上看,axios具有Axios实例的功能,但是axios本身却是一个函数;
3、axios()等同于Axios.prototype.request.bind(context)(),也就是说我们发送请求的底层原理其实是调用了Axios.prototype.request方法。
// 第三点之所以指出是因为,在后续理解axios的执行流程时很重要。
axios与axios.create()
在axios中,有一个很好的点就是可以创建axios实例,其也可以发送网络,但是相较于axios本身,其又缺少一些功能,下面通过源码比较一下之间的区别:
// 本质上也是调用createInstance方法,得到拥有各种方法和属性的实例对象。
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');
可以看出axios.create得到也是一个可以发送任何网络请求以及设置拦截的实例对象,但是这个实例对象会缺少axios本身挂载的属性比如create以及取消请求的相关api,再者就是该实例的默认配置与axios的默认配置进行合并。
数据转换
数据包括请求数据与响应数据,首先来看看请求数据转换(defaults.js文件中:):
// 请求转换器:
transformRequest: [function transformRequest(data, headers) {
normalizeHeaderName(headers, 'Accept');
normalizeHeaderName(headers, 'Content-Type');
if (utils.isFormData(data) ||
utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data)
) {
return data;
}
if (utils.isArrayBufferView(data)) {
return data.buffer;
}
if (utils.isURLSearchParams(data)) {
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
// 需要特别注意
if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) {
// 如果数据为对象,或者Content-Type为application/json,则进行序列化。
setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
return JSON.stringify(data);
}
return data;
}],
这里需要特别注意的就是,从源码中可以看出,如果我们携带的数据是对象类型或者设置了请求头’Content-Type’: ‘application/json’,那么会默认对数据进行字符串序列化和加上请求头,也就是说自动做了类型转化。因为我本人在这个地方踩过坑,具体可以看我另一篇踩坑文章–node后台与前端。其实从源码看来,完全不用自己做序列化。
用过axios的人,应该都会发现结果都是对象,其实在这里axios内部是做了一定处理的,来看看响应数据类型转换相关源码:
// 响应转换器
transitional: {
silentJSONParsing: true,
forcedJSONParsing: true,
clarifyTimeoutError: false
},
transformResponse: [function transformResponse(data) {
var transitional = this.transitional;
var silentJSONParsing = transitional && transitional.silentJSONParsing;
var forcedJSONParsing = transitional && transitional.forcedJSONParsing;
var strictJSONParsing = !silentJSONParsing && this.responseType === 'json';
if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) {
try {
return JSON.parse(data);
} catch (e) {
if (strictJSONParsing) {
if (e.name === 'SyntaxError') {
throw enhanceError(e, this, 'E_JSON_PARSE');
}
throw e;
}
}
}
return data;
}],
这段代码的(个人)理解,其实就是看响应数据是不是可以进行json解析。
请求过程
接下来我们看看整个请求过程,关于拦截器方面,会在后面进行阐述
以axios(config)为例
1、axios(config)实际上调用的是Axios.prototype.request.bind(context)(config)
2、在Axios.prototype.request.bind(context)(config)中会进行dispatchRequest(newConfig)调用;
3、dispatchRequest(newConfig)会调用adapter(config),这里的adapter就是xhr.js中的xhrAdapter函数,这个函数其实封装的就是原生xhr对象。
其实整个请求过程其实就是这三个函数的调用,其顺序:request => dispatchRequest => adapter
Axios.js:
Axios.prototype.request = function request(config) {
// config处理
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
// 默认配置合并
config = mergeConfig(this.defaults, config);
// GET、POST => get、post
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
var transitional = config.transitional;
if (transitional !== undefined) {
validator.assertOptions(transitional, {
silentJSONParsing: validators.transitional(validators.boolean, '1.0.0'),
forcedJSONParsing: validators.transitional(validators.boolean, '1.0.0'),
clarifyTimeoutError: validators.transitional(validators.boolean, '1.0.0')
}, false);
}
// 拦截器相关,后面会详细阐述。
var requestInterceptorChain = [];
var synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
return;
}
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
// 添加的请求拦截器放在数组头部
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
var responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
// 添加的响应拦截器放在数组尾部
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
var promise;
if (!synchronousRequestInterceptors) {
var chain = [dispatchRequest, undefined];
Array.prototype.unshift.apply(chain, requestInterceptorChain);
chain.concat(responseInterceptorChain);
promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
var newConfig = config;
while (requestInterceptorChain.length) {
var onFulfilled = requestInterceptorChain.shift();
var onRejected = requestInterceptorChain.shift();
try {
newConfig = onFulfilled(newConfig);
} catch (error) {
onRejected(error);
break;
}
}
// 特别注意
try {
promise = dispatchRequest(newConfig);
} catch (error) {
return Promise.reject(error);
}
while (responseInterceptorChain.length) {
promise = promise.then(responseInterceptorChain.shift(),
responseInterceptorChain.shift());
}
return promise;
};
dispatchRequest.js:
module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);
// Ensure headers exist
config.headers = config.headers || {};
// Transform request data
config.data = transformData.call(
config,
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.call(
config,
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.call(
config,
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}
return Promise.reject(reason);
});
};
取消请求
关于取消请求,请移步另一篇我关于取消请求的详细阐述博客。
axios底层封装
xhr.js:
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestData = config.data;
var requestHeaders = config.headers;
var responseType = config.responseType;
if (utils.isFormData(requestData)) {
delete requestHeaders['Content-Type']; // Let the browser set it
}
极度需要注意:这里request不是请求而是xhr,第一次看,千万注意!!!
var request = new XMLHttpRequest();
if (config.auth) {
var username = config.auth.username || '';
var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}
var fullPath = buildFullPath(config.baseURL, config.url);
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
request.timeout = config.timeout;
function onloadend() {
if (!request) {
return;
}
var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
var responseData = !responseType || responseType === 'text' || responseType === 'json' ?
request.responseText : request.response;
// 可以看出response在axios内部做了整理,所以默认为对象格式。
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request
};
// 判断状态码:axios的有效状态码为200-299。这个函数里面会执行成功的回调,也就是.then(res=>res.data)。
settle(resolve, reject, response);
// 成功之后清除xhr,个人更习惯写为xhr。
request = null;
}
if ('onloadend' in request) {
// Use onloadend if available
request.onloadend = onloadend;
} else {
// Listen for ready state to emulate onloadend
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}
异步返回数据,由于事件队列关系会最后执行。
setTimeout(onloadend);
};
}
request.onabort = function handleAbort() {
if (!request) {
return;
}
reject(createError('Request aborted', config, 'ECONNABORTED', request));
request = null;
};
request.onerror = function handleError() {
// Real errors are hidden from us by the browser
// onerror should only fire if it's a network error
reject(createError('Network Error', config, null, request));
// Clean up request
request = null;
};
request.ontimeout = function handleTimeout() {
var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
if (config.timeoutErrorMessage) {
timeoutErrorMessage = config.timeoutErrorMessage;
}
reject(createError(
timeoutErrorMessage,
config,
config.transitional && config.transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED',
request));
// Clean up request
request = null;
};
// Add xsrf header
if (utils.isStandardBrowserEnv()) {
// 携带cookie
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
// Add headers to the request
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 {
request.setRequestHeader(key, val);
}
});
}
// 是否携带cookie
if (!utils.isUndefined(config.withCredentials)) {
request.withCredentials = !!config.withCredentials;
}
// Add responseType to request if needed
if (responseType && responseType !== 'json') {
request.responseType = config.responseType;
}
// Handle progress if needed
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', config.onDownloadProgress);
}
// Not all browsers support upload events
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', config.onUploadProgress);
}
if (config.cancelToken) {
// 取消请求
config.cancelToken.promise.then(cancel=> {
if (!request) {
return;
}
// 取消请求
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
if (!requestData) {
requestData = null;
}
request.send(requestData);
});
};
从源码中可以看出,axios是基于ajax封装的,其过程依旧是满足ajax的几大步,但是在其中增加了监听事件以及配置项的处理,尤为需要注意的是,在axios中实际上是先得到响应数据,再根据状态码判断是否向外界传数据的,而不是先判断状态码的。所以我们可以得出,即使状态码不是在200-299之间,response中依旧可以有数据。明白这一点,对于二次封装axios非常重要。
未完待续····
ps:大家能看到这篇博文,想必也有在看axios源码,我在这里只是略作疏导以及自己的一些想法,axios里面其实有很多东西可以研究的,而我表达能力有限,之所以会写这篇博客更多是想让自己更加了解axios底层原理,可以很清晰地理解知识,也是对自己的表达能力一种考验,毕竟学习知识难,能很好地表达给更多人懂更难,但是我相信如果能跨过后面这一步,那么就可以对知识有着更深地理解,大家加油!
axios封装:gitub:https://github.com/heD0ng/axios
后续会慢慢完善。