Axios 的适配器原理是什么
Axios 是如何是实现请求和响应拦截的
Axios 取消请求的实现原理
CSRF的原理是什么? Axios 是如何防范客户端的CSRF的攻击
请求和响应数据转换是怎么实现的?
Features(特征)
从浏览器创建XMLHttpRequest
从node.js 创建HTTP请求
支持Promise APi
拦截请求与响应
取消请求
自动转换JSON 数据
支持客户端XSRF攻击
判断使用环境 的兼容逻辑被称做适配器
function getDefaultAdpter(){
var adapter;
if(typeof xmhttpRequest !== 'undefined'){
adapter = require('./adapters/xhr')
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]'){
adapter = require('./adapters/http')
}
return adapter
}
Axios 对adapter 封装
module.exports = function xhrAdapter(config) {
return New promise( function dispatchXhrRequest(resolve,reject){
})
}
导出一个函数,接受一个配置参数,返回一个Promise 。
module.exports = function XhrAdapter(config){
return New Promise(function disptchXhrRequest(resolve,reject){
var requestData = config.data
var request = new XmlHttpRequest()
request.open(config.methods.toUpperCase(),buildURL(fullpath,config.params,config.paramsSerializer),true);
request.onreadyStatechange = function handleLoad(){}
request.onabort = function handleAbort(){}
request.onerror = function handleError(){}
request.ontimeout = function handleTimeout(){}
request.send(requestData)
});
};
展开Axios对onreadyStatechange 的处理
request.onreacdystatechange = function handleLoad(){
if(!request || request.readyState !==4) {
return
}
// 请求出错,我们没有得到响应,这将是
// 由 onerror 处理
// 有一个例外:请求使用文件:协议,大多数浏览器
// 即使请求成功,也会返回状态为 0
if(request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:')===0)){
return
}
// 准备响应
var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : resquest.response;
var response = {
data: reponseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request
};
settle(resolve,reject,response);
request = null
}
首先对状态进行过滤,中有当请求完成时(readyState === 4 )才往下处理。需要注意的是。如果XMLHttprequest 请求出错,大部分的情况下我们可以通过监听onerror 进行处理,但是有一个列外: 当请求使用问价协议时(file://),尽管请求成功了但是大部分浏览器也会返回 0 的状态码。
Axios 针对这个例外情况也做了处理。
请求完成后 就要处理响应了。这里将相应包装城一个标准格式的对象,作为迪桑而参数传递给了settle的方法,
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
))
}
settle 对promise的回调进行了简单的封装,确保调用按一定的格式返回。
CSRF 攻击 (XSRF 跨站请求伪造 )
csrf :
背景: 用户登录后需要存储登录凭证保持登录状态,而不是用每次请求都发送账号密码
登录状态保持:
setCookie
如果我们没有判断请求来源的合法性,在登录后通过其他网站向服务器发送伪造的请求,这时携带的登录凭证的cookie就会随着伪造请求发送给服务器 导致安全漏洞 。
所有防范伪造请求的关键就是检查请求来源。refferer字段虽然可以便是当前站点,但是不够可靠,现在业界比较通用的方案还是在每个i请求上附带一个anyi-csrf token ,这个原理是攻击者无法拿到cookie ,所以我们可以通过对Cookie进行加密 (比如sid 进行加密),然后配合服务器做一些简答的验证就可以判断当前请求时不是伪造的。
Axios 简答的实现了对特俗的csrfd token的支持
if(utils.isStandardBrowserEnv()){
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
Interceptor
拦截器是Axios的特色Feature,
拦截器可以拦截请求或响应
拦截器的回调将在请求或响应的then或者catch 回调前被调用
var instance= axios.create(options);
var requestInterceptor = axios.interceptors.request.use((config)=>{
return config
},err=>{
return Promise.reject(err)
})
axios.interceptors.request.eject(requestInterceptor)
拦截器的实现
function Axios(instanceConfig){
this.defaults = instanceConfig
this.interceptors = {
request: new InterceptorManger()
response: new intercepotrManger()
}
}
Axios 的构造函数可以看到拦截器Interceptors 中的request,response两者都是一个叫做InterceptorManager的实例,这个IntercerptorManager是什么?
源码:
function InterceptorManger(){
this.handler = []
}
InterceptorManger.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
}
interceptorManger.prototype.eject = function eject(id){
if(this.handlers[id]){
this.handlers[id] = null
}
}
InterceptorManger.prototype.forEach = function forEach(fn){
utils.forEach(this.handlers, function forEachHandler(h){
if(h!==null){
fn(h)
}
}
}
Interceptormanger是一个简单的时间管理器,实现了对拦截器的管理
同过handlers村村拦截器,然后提供了添加\移除\遍历执行拦截器的实力方法,储存的每一个拦截器对象都包含了作为Promise中resolve 和reject 的回到以及两个配置项.
值得一提的是,移除方法是通过直接将;安桀齐对象设置为null 实现的.遍历方法中也增加了相应的null 值处理.这样做一方面使得每项ID 保持为项的数组索引不变,零一一方面也避免了重新剪切拼接数组的性能损失.
拦截器的回调会在请求或响应的then或者catch 回调前被调用 这是如何实现的??
Axios.prototype.request = function request(config){
var requestInterceptorChain = []
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor){
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected)
})
var responseInterceptorChain = []
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
var promise
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;
}
拦截器逻辑 :
1\首先初始化请求和响应的拦截器队列,将 resolve,reject 回调依次放入队头
2\然后初始化一个 Promise 用来执行回调,chain 用来存储和管理实际请求和拦截器
3\将请求拦截器放入 chain 队头,响应拦截器放入 chain 队尾
4\队列不为空时,通过 Promise.then 的链式调用,依次将请求拦截器,实际请求,响应拦截器出队
5\最后返回链式调用后的 Promise
这里的是激情i去是对适配器的封装,请求响应数据的转换都在这里完成/
数据转化实现
Transform data
// 这里的 throwIfCancellationRequested 方法用于取消请求
function dispatchRequest(config){
throwIfCancellationRequested()
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
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);
});
}
这里的 throwIfCancellationRequested 方法用于取消请求,关于取消请求稍后我们再讨论,可以看到发送请求是通过调用适配器实现的,在调用前和调用后会对请求和响应数据进行转换。
转换通过 transformData 函数实现,它会遍历调用设置的转换函数,转换函数将 headers 作为第二个参数,所以我们可以根据 headers 中的信息来执行一些不同的转换操作,
// 源码 core/transformData.js
function transformData(data, headers, fns) {
utils.forEach(fns, function transform(fn) {
data = fn(data, headers);
});
return data;
Axios 也提供了两个默认的转换函数,用于对请求和响应数据进行转换
var defaults = {
// Line 31
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)) {
setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
return JSON.stringify(data);
}
return data;
}],
transformResponse: [function transformResponse(data) {
var result = data;
if (utils.isString(result) && result.length) {
try {
result = JSON.parse(result);
} catch (e) { /* Ignore */ }
}
return result;
}],
}
Axios 取消请求的原理 CancelToken
其实不管是浏览器端的 xhr 或 Node.js 里 http 模块的 request 对象,都提供了 abort 方法用于取消请求,所以我们只需要在合适的时机调用 abort 就可以实现取消请求了。
那么,什么是合适的时机呢?控制权交给用户就合适了。所以这个合适的时机应该由用户决定,也就是说我们需要将取消请求的方法暴露出去,Axios 通过 CancelToken 实现取消请求,我们来一起看下它的姿势。
首先 Axios 提供了两种方式创建 cancel token,
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
// 方式一,使用 CancelToken 实例提供的静态属性 source
axios.post("/user/12345", { name: "monch" }, { cancelToken: source.token });
source.cancel();
// 方式二,使用 CancelToken 构造函数自己实例化
let cancel;
axios.post(
"/user/12345",
{ name: "monch" },
{
cancelToken: new CancelToken(function executor(c) {
cancel = c;
}),
}
);
cancel();
什么是cancelToken ?
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) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
CancelToken 就是一个由 promise 控制的极简的状态机,实例化时会在实例上挂载一个 promise,这个 promise 的 resolve 回调暴露给了外部方法 executor,这样一来,我们从外部调用这个 executor方法后就会得到一个状态变为 fulfilled 的 promise,那有了这个 promise 后我们如何取消请求呢?
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
// ----------------------------
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (req.aborted) return;
req.abort();
reject(cancel);
});
}
果然如此,在适配器里 CancelToken 实例的 promise 的 then 回调里调用了 xhr 或 http.request 的 abort 方法。试想一下,如果我们没有从外部调用取消 CancelToken 的方法,是不是意味着 resolve 回调不会执行,适配器里的 promise 的 then 回调也不会执行,就不会调用 abort 取消请求了。
摘抄地址 https://mp.weixin.qq.com/s/-9u5CPTTc_OwFIQX8EYpQw