共计 9592 个字符,预计需要花费 24 分钟才能阅读完成。
axios
是个很优秀的项目,截止 2022/2/25 为止 GitHub 上有着 91.3k
的 start。而它的源码也不多,所以很值得一看。
阅读源码不仅能学习到新的知识点也能发现自己的不足,带着问题去读源码是个好的习惯哦:
- 1.axios 是怎么实现可以创建多个实例的。
- 2.axios 的拦截器是怎么实现的。
- 3.axios 取消请求是怎么实现的。
- 4.axios 是怎么做到防
xsrf(csrf)
攻击的 - 5.axios 的优缺点
axios 的所有 源码 都在 lib
文件加下。建议 clone
下来仔细阅读。
1.axios 是怎么实现可以创建多个实例的
打开 lib/axios.js
这个入口文件的源码,可以看到这个文件代码很少,主要的一个函数就是 createInstance
,这个函数返回一个实例,这个实例就是axios
。此外还在axios
上添加了一些属性和方法,方便用户使用。所以我们主要看看 createInstance
这个函数,这个也是 axios 可以创建多个实例的核心函数:
function createInstance(defaultConfig) {var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context);
utils.extend(instance, Axios.prototype, context);
utils.extend(instance, context);
instance.create = function create(instanceConfig) {return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
return instance;
}
从上代码可以看出
createInstance
其实是个工厂函数。通过返回实例上的 create 函数可以创建新的实例。这样一个好处就是用户除了可以使用默认配置外还可以覆盖默认配置。
在之前版本的代码中 create
函数并不在 createInstance
里面,而是放在 axios
上,既:axios.create(config)
。为什么这么修改呢?可以看看 Github
上的这个PR-#2795。这么写是为了能更方便的在有多个域名的复杂的项目提供更深层次的构建。
2.axios 的拦截器是怎么实现的
axios
拦截器的源码主要在 Axios.js
和InterceptorManager.js
文件中。
我们先来看看 Axios
函数: lib/core/Axios.js
function Axios(instanceConfig) {this.defaults = instanceConfig;
this.interceptors = {request: new InterceptorManager(),
response: new InterceptorManager()};
}
Axios
函数在实例对象上有两个属性 default
和interceptors
,defaults
是默认配置;interceptors
就是我们的拦截器对象,它也有两个属性 request
和response
分别对应请求拦截和响应拦截;它们的值都是 InterceptorManager
对象实例。
再来看看我们拦截器的使用方式:axios.interceptors.request.use
。use
是 InterceptorManager
实例对象上的函数,InterceptorManager
顾名思义是对拦截器的管理,我们来看看它的源码:
lib/core/InterceptorManager.js
function InterceptorManager() {this.handlers = [];}
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {this.handlers.push({fulfilled: fulfilled,
rejected: rejected,
synchronous: options ? options.synchronous : false,
runWhen: options ? options.runWhen : null
});
return this.handlers.length - 1;
};
InterceptorManager.prototype.eject = function eject(id) {if (this.handlers[id]) {this.handlers[id] = null;
}
};
InterceptorManager.prototype.forEach = function forEach(fn) {utils.forEach(this.handlers, function forEachHandler(h) {if (h !== null) {fn(h);
}
});
};
InterceptorManager
源码很简单,提供 handlers
堆栈来储存拦截器,同时在原型上增加了 3 个函数对这个堆栈的增删以及遍历。
Axios
实例的 interceptors
对象只在 Axios.prototype.request
函数中使用,而这个函数是 axios
请求的源函数,你调用的请求函数像 axios.get
和axios.post
等本质都是调用 Axios.prototype.request
这个函数。而拦截器的的处理也是在这个函数中。我们回到 Axios.js
文件,看看这个函数的源码:
Axios.prototype.request = function request(configOrUrl, config) {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 = 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;
};
在执行请求前定义了两个堆栈 requestInterceptorChain
和responseInterceptorChain
来存储拦截器处理函数
requestInterceptorChain
存储的是请求拦截器的处理函数,要注意它通过unshift
添加的,是先进后出的,所以越早添加的拦截器越晚执行。responseInterceptorChain
存储的是响应拦截器的处理函数,这个是先进先出的,也就是越早添加越先执行。
这里需要注意的是,在存入堆栈时都是两个为一组存储的,第一个始终是
fulfilled
的处理函数,第二个始终是rejected
,因为后续取值的时候也是两个为一组取,刚好对应Promise.then
函数对应的两个参数。
我们现在再来看请求的执行,进入 if
语句块的代码(默认执行 if 语句块里的代码,原因后面再来讲解)。
我们可以看到定义了一个 chain
数组来存放要执行的函数,默认有两个值,第一个是 dispatchRequest
, 第二个是undefined
。现在暂时不去看dispatchRequest
是怎么样的,只要明白这个函数是可以发起请求就行了。
Array.prototype.unshift.apply(chain, requestInterceptorChain);
chain = chain.concat(responseInterceptorChain);
chain
通过上面代码处理 之后变成这样了: chain = [... 请求拦截函数, dispatchRequest, undefined, ... 响应拦截函数]
。之后使用 Promise
链式调用执行函数。这样就使得请求拦截函数始终在发起请求前执行,响应拦截函数在请求之后执行。
再来看看刚刚问题:为什么默认执行 if 语句里面的代码? 看 if (!synchronousRequestInterceptors) {...}
这个判断条件。
在 axios.interceptors.request
中synchronousRequestInterceptors
默认值为 false
,如果在请求拦截器中没有配置synchronous
为true
的情况下这个值会被设置为 false
。synchronous
是用于设置请求拦截器是否为同步执行。
使用代码如下:
axios.interceptors.request.use(function (config) {config.headers.test = 'I am only a header!';
return config;
}, null, {synchronous: true });
synchronous
是用来控制请求拦截器是否为同步执行的。我们一般情况下使用是不用配置这个的,那什么时候需要配置呢?
假如请求拦截器是异步的 (其实默认就是异步的),而请求的promise(dispatchRequest)
又是在请求拦截堆栈后面,所以当主线程被阻塞时,那么 axios
请求发起时机就会被延迟。所以想要避免发起请求时机会延迟这个问题,可以设置请求拦截器是同步执行的。
所以会默认情况是会执行 if
语句块里的代码。后面的代码就是请求拦截器同步执行的代码,这里就不多赘述啦。
3.axios 取消请求是怎么实现的
先来看看取消请求是如何使用的:
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 {}});
source.cancel('Operation canceled by the user.');
这里通过 CancelToken
的source
函数返回一个对象,然后把 source.token
传入 axios
配置,使用 source.cancel
则可以取消请求。
来看看源码的实现:取消请求有两部分关键代码分别在 lib/cancel/CancelToken.js
和lib/adapters/xhr.js
lib/cancel/CancelToken.js
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;
this.promise.then(function(cancel) {if (!token._listeners) return;
var i;
var l = token._listeners.length;
for (i = 0; i null;
});
this.promise.then = function(onfulfilled) {var _resolve;
var promise = new Promise(function(resolve) {token.subscribe(resolve);
_resolve = resolve;
}).then(onfulfilled);
promise.cancel = function reject() {token.unsubscribe(_resolve);
};
return promise;
};
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.prototype.subscribe = function subscribe(listener) {if (this.reason) {listener(this.reason);
return;
}
if (this._listeners) {this._listeners.push(listener);
} else {this._listeners = [listener];
}
};
CancelToken.prototype.unsubscribe = function unsubscribe(listener) {if (!this._listeners) {return;
}
var index = this._listeners.indexOf(listener);
if (index !== -1) {this._listeners.splice(index, 1);
}
};
CancelToken.source = function source() {var cancel;
var token = new CancelToken(function executor(c) {cancel = c;});
return {token: token,
cancel: cancel
};
};
lib/adapters/xhr.js
function done() {if (config.cancelToken) {config.cancelToken.unsubscribe(onCanceled);
}
if (config.signal) {config.signal.removeEventListener('abort', onCanceled);
}
}
if (config.cancelToken || config.signal) {onCanceled = function(cancel) {if (!request) {return;
}
reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
request.abort();
request = null;
};
config.cancelToken && config.cancelToken.subscribe(onCanceled);
if (config.signal) {config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
取消请求的核心代码是在 CancelToken.js
中,以及在创建 xhr
时对 config.cancelToken 和 config.signal
配置的处理上。
从代码可以看出,当调用 CancelToken.source()
时会返回一个 CancelToken
实例对象和一个可以取消请求的函数。
在 CancelToken 函数中:
- 1、当创建
CancelToken
实例时this.promise
指向一个创建好的Promise
实例,此时这个promise
为挂起状态,等待resolve
或reject
。 - 2、接着会执行
executor
函数,executor
函数就是实例化CancelToken
时传入函数,传入executor
参数是一个函数,这个函数就是cancel
函数,调用这个函数后,该函数会resolve
实例的promise
,同时传入Cancel
实例对象,CancelToken
中的promise
就会变为fulfilled
,此时this.promise.then
中的代码将会执行,遍历订阅列表,执行取消函数,请求会被逐个取消。
在 xhr.js 文件中:
- 1、定义了一个
done
函数,这个函数在请求完成后会执行。用于移除取消请求的监听。 - 2、在发送请求前会检查是否有传入取消请求的配置,如果有配置,则会给
onCanceled
赋值为一个函数,这个函数就是用于真正取消请求的函数,这个函数还会reject
掉 axios 请求的Promise
。onCanceled
被赋值后会把onCanceled
这个函数添加到CancelToken
实例中的_listeners
订阅列表中,当CancelToken
的promise
被resolve
后这个函数被执行,取消请求。 - 3、你会发
axios
现取消请求的方式有两种,一种是axios
本身自己实现的cancelToken
,还有一种是signal
;这种用的是Web
的原生的 AbortController API,官网的第一句话是这么说的:AbortController
接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。所以它不仅可以取消axios
发起的请求,Fetch
发起的可以用这个 API 取消。详情查看:Abortable fetch
除了 CancelToken.js
还有 Cancel.js
和isCancel.js
:
Cancel
是一个构造函数,有一个message
属性用于存储用户调用取消函数时的错误提示,当用户取消请求时,会把Cancel
实例对象传递给取消函数,在错误处可以捕获这个对象实例。并且Cancel
原型上有个__CANCEL__
属性,可以用于判断axios
抛出的错误是否是由于取消请求而导致的。isCancel
就是个很简单的函数,用于判断是否是Cancel
对象。
4.axios 是怎么做到防 xsrf(csrf)
攻击的
在 axios
使用很简单,在请求上添加配置即可
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
复制代码
防护 XSRF
策略有多种,一般的防护策略有:
- 阻止不明外域的访问
- 同源检测
- Samesite Cookie
- 提交时要求附加本域才能获取的信息
- 双重 Cookie 验证
- CSRF Token
同源策略虽然可以防护,但多少还有点缺陷,比如来之搜索引擎的访问。而在请求头上加 token
是目前一种更有效的防护策略。详情参考这篇博文:如何防止 CSRF 攻击?
5.axios 的优缺点
axios
的优点有很多,比如
- 体积小
- 支持请求响应拦截
- 支持取消请求
- 返回自动转换 JSON
- 兼容性好
- 支持 node
- 等等…
axios
优点很多,当然也有缺点。axios
在请求的处理上做的很优秀,但随着业务的或者技术的进步可能你需要跟好的请求库:
- 给予 xhr,兼容性好,但 XHR 本身的架构不清晰。
- 你可能需要防抖、节流、轮询等比较高级的需求,但 axios 没有提供,需要自己手动编写。
- 等等…(我想不到了!!!我不管!它就是很好!!!【狗头保命】)
当然第二条也可以说不算缺点,硬凑的~ 如果 axios
做的越智能,那么它的体积也就不会这么小了,也不是每个人都需要如此复杂的功能。这也是编写库时要去明确以及取舍的啦~
最后
除了以上的内容,axios
还有很多值得学习的地方,这里就不一一讲解了。比如 axios
对config 配置
的合并、处理;对请求响应的自动转化;对 url
的处理;如何适配 node
和web
端和一些对 JS 使用的小技巧等;此外 axios
的utils
工具函数也值得一看,比如 merge
、extend
、forEach
和isPlainObject
等。
一个额外的小知识点,下面的 isPlainObject
和axios
中的写法不同,为什么要这么写呢?
export default function isPlainObject(obj: any): boolean {if (typeof obj !== 'object' || obj === null) return false
let proto = obj
while (Object.getPrototypeOf(proto) !== null) {proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(obj) === proto
}
作者:烟笼寒水月笼沙
链接:https://juejin.cn/post/7068850606626045959
来源:稀土掘金