theme: channing-cyan highlight: agate
axios是目前前端最流行的网络请求库。 秉着对流行库的学习,那就开一个新坑,介绍下axios核心源码逻辑。
用Vite新建一个项目最简单的ts项目
npm create vite@latest axios-debugger
npm install axios
大致回顾下axios基本用法
调用request方法
axios
.request({
baseURL: "https://api.github.com",
url: "/users/mzabriskie",
})
.then((res) => {
console.log(res.data);
})
.catch((err) => {
console.log(err);
});
亦或者调用对应的请求方法
axios.get("url").then((res) => {
console.log(res.data);
});
axios.post('url')
axios.put('url')
或者创建一个拥有全局配置的axios对象
const instance = axios.create({
baseURL: "https://api.github.com",
method: "get",
// ... 其他配置
});
axios是如何做到的。
我们通过打断调试点的方式来看下源码逻辑
实现在如图所示的地方下一个断点
打开vscode的debuger菜单,点击JavaScript调试终端
在终端中执行运行命令
npm run dev
通过调试链接打开项目
此时端点就会断在这里
你就可以沿着断点执行,直接定位到源码里面。
axios是如何初始化的?
axios初始化
// 创建实例方法
function createInstance(defaultConfig) {
// 创建 axios 实例
const context = new Axios(defaultConfig);
// 绑定request方法内部的this指向 axios 实例
// 注意:此时的instance是一个function
const instance = bind(Axios.prototype.request, context);
// 将Axios原型对象绑定到instance中
utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});
// 将 axios 实例 绑定到 instance function中
utils.extend(instance, context, null, {allOwnKeys: true});
// 申明create方法
// 这个是我们最常用的: axios.create({})
instance.create = function create(instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
return instance;
}
经过createInstance方法后,我们看看instance 这个function上到底有哪些属性
可以看到最常用的请求方法request、get、post、put、delete以及拦截器interceptors都在里面。很显然这些方法都在Axios.prototype实例对象上。来看下Axios这个类
在源码中Axios.js这个文件中,默认就执行了请求方法绑定到Axios.prototype实例上
列举了所有用到的请求方法。
// Provide aliases for supported request methods
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, config) {
return this.request(mergeConfig(config || {}, {
method,
url,
data: (config || {}).data
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
function generateHTTPMethod(isForm) {
return function httpMethod(url, data, config) {
return this.request(mergeConfig(config || {}, {
method,
headers: isForm ? {
'Content-Type': 'multipart/form-data'
} : {},
url,
data
}));
};
}
Axios.prototype[method] = generateHTTPMethod();
Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
});
axios有一个默认配置,放在源码目录中的defaults文件夹下index.js中。
在初始化axios中,默认传进去
const axios = createInstance(defaults);
拦截器实现原理
下面重点看Axios这个类的实现,最重要的拦截器和请求方法都在这个里面实现的。
constructor(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
构造函数很简单,保存了外部传入的默认配置,如果用户不传,那就用默认的一套。另外保存了request和response拦截器相关信息。
提供了2个方法,一个是对外的核心请求方法requst, 当调用request,执行内部的_request方法
在实际开发中,在真正的发送请求前,我们都会对请求参数和已经返回的response做一些处理。比如在请求参数中根据业务添加特定参数,例如用户信息(token)。
回顾下拦截器基本用法
instance.interceptors.request.use(function (config) {
console.log(111);
return config;
});
instance.interceptors.request.use(function (config) {
console.log(222);
return config;
});
instance.interceptors.response.use(
function (response) {
console.log(333);
return response;
},
function (err) {
console.log(err);
}
);
instance.interceptors.response.use(
function (response) {
console.log(444);
return response;
},
function (err) {
console.log(err);
}
);
输出
222
111
333
444
可以看到,对于请求拦截器执行顺序是倒序,响应拦截器是正序。
首先InterceptorManager管理器会收集请求拦截器和响应连接器。
constructor() {
this.handlers = [];
}
use(fulfilled, rejected, options) {
this.handlers.push({
fulfilled,
rejected,
synchronous: options ? options.synchronous : false, // 默认是异步
runWhen: options ? options.runWhen : null
});
return this.handlers.length - 1;
}
拦截器默认执行顺序是异步。runWhen这个参数决定了本次拦截器要不要执行。
use函数返回了一个数字,本次拦截器所在数组中的位置, 表示了本次拦截器的唯一标识id,可用作移除拦截器
eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
}
再发送请求前,遍历用户设置的请求拦截器和响应拦截器
const requestInterceptorChain = [];
let 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);
});
const responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
请求拦截器的方法它是通过unshift压进数组中,最后一个拦截器会被推到要执行的数组中中第一个, 响应拦截器则是顺序添加。
接下来看拦截器最核心的逻辑:
let promise;
let i = 0;
let len;
// 默认是异步
if (!synchronousRequestInterceptors) {
const chain = [dispatchRequest.bind(this), undefined];
chain.unshift.apply(chain, requestInterceptorChain);
chain.push.apply(chain, responseInterceptorChain);
len = chain.length;
promise = Promise.resolve(config);
while (i < len) {
promise = promise.then(chain[i++], chain[i++]);
}
return promise;
}
首先chain数组中默认保存了请求方法,dispatchRequest是真正的发送请求的方法,该方法默认绑定了当前的axios对象。因为用户有可能会创建多个axios实例,传入当前的 axios,就保证了每个请求是对应的axios发起的。返回的数据不会错乱。
请求拦截器requestInterceptorChain被推chain数组中最前面,然后响应拦截器就被顺序添加在在dispatchRequest后面。
拿上面例子说:
对应的chain数据模型如下
['请求拦截器2', '请求拦截器1', dispatchRequest, '响应拦截器1', '响应拦截器2']
根据用户传入的config参数创建一个promise对象,循环chain,将请求拦截器和响应拦截器执行的方法都添加到promise链中。用户设置的请求拦截器config 会沿着promise链传递到dispatchRequest中,dispatchRequest方法返回的response数据沿着后续的promise链传递到响应拦截器中。
为了方面理解,我们可以模拟下这个流程。
let config = { a: 1 };
let promise = Promise.resolve(config);
function onRequest1(config: any) {
config.age = 10;
console.log("onRequest1", config);
return config;
}
function onReques2(config: any) {
config.name = { a: 1 };
console.log("onReques2", config);
return config;
}
function onResponse1(response: any) {
response.data.name = { a: 1 };
console.log("response1", response);
return response;
}
function onResponse2(response: any) {
console.log("response2", response);
}
function doFetch(config: any) {
console.log('doFectch', config);
return {
data: {
success: true,
},
};
}
[onReques2, onRequest1, doFetch, onResponse1, onResponse2].forEach((fn) => {
promise = promise.then(fn);
});
除了默认异步执行外,还支持用户设置为同步操作,对应的代码如下
instance.interceptors.request.use(
function (config) {
return config;
},
function (err) {
console.log(err);
},
{ synchronous: true }
);
instance.interceptors.request.use(
function (config) {
return config;
},
null,
{ synchronous: true }
);
第三个参数支持传入synchronous明确表示同步。同步执行源码这块逻辑就比较好理解了
len = requestInterceptorChain.length;
let newConfig = config;
i = 0;
// 循环拦截器
while (i < len) {
const onFulfilled = requestInterceptorChain[i++];
const onRejected = requestInterceptorChain[i++];
try {
// 获取每个拦截器返回的config
newConfig = onFulfilled(newConfig);
} catch (error) {
onRejected.call(this, error);
break;
}
}
try {
// 将最后一个拦截器返回的config丢给请求方法
promise = dispatchRequest.call(this, newConfig);
} catch (error) {
return Promise.reject(error);
}
i = 0;
len = responseInterceptorChain.length;
// 循环响应拦截器
while (i < len) {
promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]);
}
return promise;
循环requestInterceptorChain方法拿到每次拦截器返回的config,最后丢给dispatchRequest方法,该方法返回一个promise。循环响应式拦截器数据,将response数据沿着promise链传递下去。
这里总结下:
- 拦截器模型:请求拦截器倒序添加到
chain数组中,即:最后一个请求拦截器会在第一个执行,紧接着就是dispatchRequest请求方法,然后就是响应拦截器顺序添加到chain中。最后每一步的执行结果都会沿着promise链传递下去 - 同步执行和异步执行的拦截器模型一直,不同的就是,会先执行请求拦截器获取到每次返回的config数据,丢给
dispatchRequest方法,该方法返回一个promise,响应数据会沿着这个promise链传递下去。
更新下:拦截器同步异步的区别
评论区有小伙伴在问:拦截器同步我们实际开发基本没用过。能列举具体场景么?
确实, 我们在实际开发中很少指定拦截器是同步执行。我举个例子大概就能理解了。
比如某个字段需要你通过一个异步操作获取然后添加到data,或者headers中,或者其他参数中。此时拦截器你可以这么写
// 模拟异步获取token
function getToken() {
return new Promise((resolve) => {
setTimeout(() => resolve("token"), 1000);
});
}
instance.interceptors.request.use(function (config) {
console.log(config);
return config;
});
instance.interceptors.request.use(function (config) {
return new Promise((resolve) => {
getToken().then((token) => {
config.headers.set("Authorization", `token ${token}`);
resolve(config);
});
});
});
由于异步请求拦截器的config是promise传递的,所以你可以在上游的请求拦截器中做一些异步操作,丢给下游的请求拦截器
如果改为同步拦截器,你就只能提前获取token,在设置请求拦截器,以及请求。
function getToken() {
return new Promise((resolve) => {
setTimeout(() => resolve("token"), 1000);
});
}
getToken().then((token) => {
instance.interceptors.request.use(
function (config) {
console.log(config);
return config;
},
null,
{
synchronous: true,
}
);
instance.interceptors.request.use(
function (config) {
config.headers.set("Authorization", `token ${token}`);
return config;
},
null,
{
synchronous: true,
}
);
instance.get("/users/mzabriskie").then((res) => {
console.log(res?.data);
});
});
异步请求拦截器你可以在每个拦截器做单独的异步操作,同步拦截器,你就只能拿到所有你想要的数据,才能设置拦截器的config。
runWhen
第三个参数,还支持runWhen函数,该函数控制当前拦截器要不要执行。
instance.interceptors.request.use(
function (config) {
console.log(111);
return config;
},
null,
{
runWhen(config) {
return false
},
}
);
内部源码判断该钩子函数是否返回false, 是的话,则没有加入到requestInterceptorChain数组中。
if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
return;
}
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
实际项目中会有以下场景:
对全局拦截器处理header信息,比如加入token认证,所有接口都要加入headers信息,但是登录接口不需要。
instance.interceptors.request.use(
function (config) {
config.auth = {
username: "111",
password: "222",
};
// 或者是下面这样:
config.headers.set('token', '服务端返回的token')
return config;
},
null,
{
runWhen(config) {
// 单独针对 login接口,过滤该请求拦截器
// 其他接口都要设置headers信息
return config.url !== "/login";
},
}
);
dispatchRequest执行
说到了真正发请求的地方了。
axios有一个适配器的概念,默认有2个,一个是node端的http请求,一个web端的xhr请求。axios内部会判断当前环境支持哪种请求方式:
const isHttpAdapterSupported = typeof process !== 'undefined' && utils.kindOf(process) === 'process';
const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';
拿web端来说,就是对xhr请求做了一程封装,返回一个promise。整体逻辑还算简单。这里就不介绍了。有兴趣可以看下源码:
export default isXHRAdapterSupported && function (config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
let requestData = config.data;
const requestHeaders = AxiosHeaders.from(config.headers).normalize();
let {responseType, withXSRFToken} = config;
let onCanceled;
function done() {
if (config.cancelToken) {
config.cancelToken.unsubscribe(onCanceled);
}
if (config.signal) {
config.signal.removeEventListener('abort', onCanceled);
}
}
let contentType;
// 处理header信息
if (utils.isFormData(requestData)) {
if (platform.hasStandardBrowserEnv || platform.hasStandardBrowserWebWorkerEnv) {
requestHeaders.setContentType(false); // Let the browser set it
} else if ((contentType = requestHeaders.getContentType()) !== false) {
// fix semicolon duplication issue for ReactNative FormData implementation
const [type, ...tokens] = contentType ? contentType.split(';').map(token => token.trim()).filter(Boolean) : [];
requestHeaders.setContentType([type || 'multipart/form-data', ...tokens].join('; '));
}
}
// xhr请求
let request = new XMLHttpRequest();
// 支持Auth认证
if (config.auth) {
const username = config.auth.username || '';
const password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
requestHeaders.set('Authorization', 'Basic ' + btoa(username + ':' + password));
}
// 请求全路径
const fullPath = buildFullPath(config.baseURL, config.url);
// 指定请求方式
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
// Set the request timeout in MS
request.timeout = config.timeout;
// 处理请求成功数据
function onloadend() {
if (!request) {
return;
}
// Prepare the response
const responseHeaders = AxiosHeaders.from(
'getAllResponseHeaders' in request && request.getAllResponseHeaders()
);
const responseData = !responseType || responseType === 'text' || responseType === 'json' ?
request.responseText : request.response;
const response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config,
request
};
settle(function _resolve(value) {
resolve(value);
done();
}, function _reject(err) {
reject(err);
done();
}, response);
// Clean up request
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;
}
// 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;
}
// readystate handler is calling before onerror or ontimeout handlers,
// so we should call onloadend on the next 'tick'
setTimeout(onloadend);
};
}
// 取消请求
request.onabort = function handleAbort() {
if (!request) {
return;
}
reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));
// Clean up 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(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));
// Clean up request
request = null;
};
// 处理超时
request.ontimeout = function handleTimeout() {
let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
const transitional = config.transitional || transitionalDefaults;
if (config.timeoutErrorMessage) {
timeoutErrorMessage = config.timeoutErrorMessage;
}
reject(new AxiosError(
timeoutErrorMessage,
transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
config,
request));
// Clean up request
request = null;
};
// csrf
if(platform.hasStandardBrowserEnv) {
withXSRFToken && utils.isFunction(withXSRFToken) && (withXSRFToken = withXSRFToken(config));
if (withXSRFToken || (withXSRFToken !== false && isURLSameOrigin(fullPath))) {
// Add xsrf header
const xsrfValue = config.xsrfHeaderName && config.xsrfCookieName && cookies.read(config.xsrfCookieName);
if (xsrfValue) {
requestHeaders.set(config.xsrfHeaderName, xsrfValue);
}
}
}
// Remove Content-Type if data is undefined
requestData === undefined && requestHeaders.setContentType(null);
// Add headers to the request
if ('setRequestHeader' in request) {
utils.forEach(requestHeaders.toJSON(), function setRequestHeader(val, key) {
request.setRequestHeader(key, val);
});
}
// Add withCredentials to request if needed
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', progressEventReducer(config.onDownloadProgress, true));
}
// Not all browsers support upload events
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', progressEventReducer(config.onUploadProgress));
}
if (config.cancelToken || config.signal) {
// Handle cancellation
// eslint-disable-next-line func-names
onCanceled = cancel => {
if (!request) {
return;
}
reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
request.abort();
request = null;
};
config.cancelToken && config.cancelToken.subscribe(onCanceled);
if (config.signal) {
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
const protocol = parseProtocol(fullPath);
if (protocol && platform.protocols.indexOf(protocol) === -1) {
reject(new AxiosError('Unsupported protocol ' + protocol + ':', AxiosError.ERR_BAD_REQUEST, config));
return;
}
// 发送请求
request.send(requestData || null);
});
}
最后
axios源码不算太难,其中比较难理解的就是拦截器那块异步执行流程,搞懂了那块逻辑就基本上算是了解了axios核心。面试题中最常见的就是喜欢考拦截器原理: 就是用户配置拦截器数据沿着promise链传递的过程。配合文章中辅助代码,就能够很好理解了。