Axios 源码解读看这一篇就够了

14,784次阅读
没有评论

共计 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.jsInterceptorManager.js文件中。

我们先来看看 Axios 函数: lib/core/Axios.js

function Axios(instanceConfig) {this.defaults = instanceConfig;
  this.interceptors = {request: new InterceptorManager(),
    response: new InterceptorManager()};
}

Axios函数在实例对象上有两个属性 defaultinterceptorsdefaults是默认配置;interceptors就是我们的拦截器对象,它也有两个属性 requestresponse分别对应请求拦截和响应拦截;它们的值都是 InterceptorManager 对象实例。

再来看看我们拦截器的使用方式:axios.interceptors.request.useuseInterceptorManager 实例对象上的函数,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.getaxios.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;
};

在执行请求前定义了两个堆栈 requestInterceptorChainresponseInterceptorChain来存储拦截器处理函数

  • 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.requestsynchronousRequestInterceptors默认值为 false,如果在请求拦截器中没有配置synchronoustrue的情况下这个值会被设置为 falsesynchronous 是用于设置请求拦截器是否为同步执行。

使用代码如下:

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.');

这里通过 CancelTokensource函数返回一个对象,然后把 source.token 传入 axios 配置,使用 source.cancel 则可以取消请求。

来看看源码的实现:取消请求有两部分关键代码分别在 lib/cancel/CancelToken.jslib/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 为挂起状态,等待 resolvereject
  • 2、接着会执行 executor 函数,executor函数就是实例化 CancelToken 时传入函数,传入 executor 参数是一个函数,这个函数就是 cancel 函数,调用这个函数后,该函数会 resolve 实例的 promise,同时传入Cancel 实例对象,CancelToken中的 promise 就会变为 fulfilled,此时this.promise.then 中的代码将会执行,遍历订阅列表,执行取消函数,请求会被逐个取消。

xhr.js 文件中:

  • 1、定义了一个 done 函数,这个函数在请求完成后会执行。用于移除取消请求的监听。
  • 2、在发送请求前会检查是否有传入取消请求的配置,如果有配置,则会给 onCanceled 赋值为一个函数,这个函数就是用于真正取消请求的函数,这个函数还会 reject 掉 axios 请求的 PromiseonCanceled 被赋值后会把 onCanceled 这个函数添加到 CancelToken 实例中的 _listeners 订阅列表中,当 CancelTokenpromiseresolve 后这个函数被执行,取消请求。
  • 3、你会发 axios 现取消请求的方式有两种,一种是 axios 本身自己实现的 cancelToken,还有一种是signal;这种用的是Web 的原生的 AbortController API,官网的第一句话是这么说的:AbortController 接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。所以它不仅可以取消 axios 发起的请求,Fetch发起的可以用这个 API 取消。详情查看:Abortable fetch

除了 CancelToken.js 还有 Cancel.jsisCancel.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还有很多值得学习的地方,这里就不一一讲解了。比如 axiosconfig 配置 的合并、处理;对请求响应的自动转化;对 url 的处理;如何适配 nodeweb端和一些对 JS 使用的小技巧等;此外 axiosutils工具函数也值得一看,比如 mergeextendforEachisPlainObject等。

一个额外的小知识点,下面的 isPlainObjectaxios中的写法不同,为什么要这么写呢?

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
来源:稀土掘金

    正文完
     0
    Yojack
    版权声明:本篇文章由 Yojack 于1970-01-01发表,共计9592字。
    转载说明:
    1 本网站名称:优杰开发笔记
    2 本站永久网址:https://yojack.cn
    3 本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
    4 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
    5 本站所有内容均可转载及分享, 但请注明出处
    6 我们始终尊重原创作者的版权,所有文章在发布时,均尽可能注明出处与作者。
    7 站长邮箱:laylwenl@gmail.com
    评论(没有评论)