专栏首页Vue中文社区一文读懂Axios核心源码思想

一文读懂Axios核心源码思想

阅读完本文,下面的问题会迎刃而解,

  • Axios 的适配器原理是什么?
  • Axios 是如何实现请求和响应拦截的?
  • Axios 取消请求的实现原理?
  • CSRF 的原理是什么?Axios 是如何防范客户端 CSRF 攻击?
  • 请求和响应数据转换是怎么实现的?

全文约两千字,阅读完大约需要 6 分钟,文中 Axios 版本为 0.21.1

我们以特性作为入口,解答上述问题的同时一起感受下 Axios 源码极简封装的艺术。

Features

  • 从浏览器创建 XMLHttpRequest
  • 从 Node.js 创建 HTTP 请求
  • 支持 Promise API
  • 拦截请求与响应
  • 取消请求
  • 自动装换 JSON 数据
  • 支持客户端 XSRF 攻击

前两个特性解释了为什么 Axios 可以同时用于浏览器和 Node.js 的原因,简单来说就是通过判断是服务器还是浏览器环境,来决定使用 XMLHttpRequest 还是 Node.js 的 HTTP 来创建请求,这个兼容的逻辑被叫做适配器,对应的源码在 lib/defaults.js 中,

// defaults.js
function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}

以上是适配器的判断逻辑,通过侦测当前环境的一些全局变量,决定使用哪个 adapter。其中对于 Node 环境的判断逻辑在我们做 ssr 服务端渲染的时候,也可以复用。接下来我们来看一下 Axios 对于适配器的封装。

Adapter xhr

定位到源码文件 lib/adapters/xhr.js,先来看下整体结构,

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // ...
  })
}

导出了一个函数,接受一个配置参数,返回一个 Promise。我们把关键的部分提取出来,

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;

    var request = new XMLHttpRequest();

    request.open(config.method.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);
  });
};

是不是感觉很熟悉?没错,这就是 XMLHttpRequest 的使用姿势呀,先创建了一个 xhr 然后 open 启动请求,监听 xhr 状态,然后 send 发送请求。我们来展开看一下 Axios 对于 onreadystatechange 的处理,

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;
  }

  // Prepare the response
  var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
  var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
  var response = {
    data: responseData,
    status: request.status,
    statusText: request.statusText,
    headers: responseHeaders,
    config: config,
    request: request
  };

  settle(resolve, reject, response);

  // Clean up request
  request = null;
};

首先对状态进行过滤,只有当请求完成时(readyState === 4)才往下处理。需要注意的是,如果 XMLHttpRequest 请求出错,大部分的情况下我们可以通过监听 onerror 进行处理,但是也有一个例外:当请求使用文件协议(file://)时,尽管请求成功了但是大部分浏览器也会返回 0 的状态码。

Axios 针对这个例外情况也做了处理。

请求完成后,就要处理响应了。这里将响应包装成一个标准格式的对象,作为第三个参数传递给了 settle 方法,settle 在 lib/core/settle.js 中定义,

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 的回调进行了简单的封装,确保调用按一定的格式返回。

以上就是 xhrAdapter 的主要逻辑,剩下的是对请求头,支持的一些配置项以及超时,出错,取消请求等回调的简单处理,其中对于 XSRF 攻击的防范是通过请求头实现的。

我们先来简单回顾下什么是 XSRF (也叫 CSRF跨站请求伪造)。

CSRF

背景:用户登录后,需要存储登录凭证保持登录态,而不用每次请求都发送账号密码。

怎么样保持登录态呢?

目前比较常见的方式是,服务器在收到 HTTP请求后,在响应头里添加 Set-Cookie 选项,将凭证存储在 Cookie 中,浏览器接受到响应后会存储 Cookie,根据浏览器的同源策略,下次向服务器发起请求时,会自动携带 Cookie 配合服务端验证从而保持用户的登录态。

所以如果我们没有判断请求来源的合法性,在登录后通过其他网站向服务器发送了伪造的请求,这时携带登录凭证的 Cookie 就会随着伪造请求发送给服务器,导致安全漏洞,这就是我们说的 CSRF,跨站请求伪造。

所以防范伪造请求的关键就是检查请求来源,refferer 字段虽然可以标识当前站点,但是不够可靠,现在业界比较通用的解决方案还是在每个请求上附带一个 anti-CSRF token,这个的原理是攻击者无法拿到 Cookie,所以我们可以通过对 Cookie 进行加密(比如对 sid 进行加密),然后配合服务端做一些简单的验证,就可以判断当前请求是不是伪造的。

Axios 简单地实现了对特殊 csrf token 的支持,

// Add xsrf header
// This is only done if running in a standard browser environment.
// Specifically not if we're in a web worker, or react-native.
if (utils.isStandardBrowserEnv()) {
  // Add xsrf header
  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) => {
    // do something before request is sent
    return config;
  },
  (err) => {
    // do somthing with request error
    return Promise.reject(err);
  }
);

// 移除已设置的拦截器
axios.interceptors.request.eject(requestInterceptor)

那么拦截器是怎么实现的呢?

定位到源码 lib/core/Axios.js 第 14 行,

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

通过 Axios 的构造函数可以看到,拦截器 interceptors 中的 request 和 response 两者都是一个叫做 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 存储拦截器,然后提供了添加,移除,遍历执行拦截器的实例方法,存储的每一个拦截器对象都包含了作为 Promise 中 resolve 和 reject 的回调以及两个配置项。

值得一提的是,移除方法是通过直接将拦截器对象设置为 null 实现的,而不是 splice 剪切数组,遍历方法中也增加了相应的 null 值处理。这样做一方面使得每一项ID保持为项的数组索引不变,另一方面也避免了重新剪切拼接数组的性能损失。

拦截器的回调会在请求或响应的 then 或 catch 回调前被调用,这是怎么实现的呢?

回到源码 lib/core/Axios.js 中第 27 行,Axios 实例对象的 request 方法,

我们提取其中的关键逻辑如下,

Axios.prototype.request = function request(config) {
  // Get merged config
  // Set config.method
  // ...
  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;
};

可以看到,当执行 request 时,实际的请求(dispatchRequest)和拦截器是通过一个叫 chain 的队列来管理的。整个请求的逻辑如下,

  1. 首先初始化请求和响应的拦截器队列,将 resolve,reject 回调依次放入队头
  2. 然后初始化一个 Promise 用来执行回调,chain 用来存储和管理实际请求和拦截器
  3. 将请求拦截器放入 chain 队头,响应拦截器放入 chain 队尾
  4. 队列不为空时,通过 Promise.then 的链式调用,依次将请求拦截器,实际请求,响应拦截器出队
  5. 最后返回链式调用后的 Promise

这里的实际请求是对适配器的封装,请求和响应数据的转换都在这里完成。

那么数据转换是如何实现的呢?

Transform data

定位到源码 lib/core/dispatchRequest.js

function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  // Transform request data
  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 也提供了两个默认的转换函数,用于对请求和响应数据进行转换。默认情况下,

Axios 会对请求传入的 data 做一些处理,比如请求数据如果是对象,会序列化为 JSON 字符串,响应数据如果是 JSON 字符串,会尝试转换为 JavaScript 对象,这些都是非常实用的功能,

对应的转换器源码可以在 lib/default.js 的第 31 行找到,

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?定位到源码 lib/cancel/CancelToken.js 第 11 行,

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 后我们如何取消请求呢?

是不是只要在请求时拿到这个 promise 实例,然后在 then 回调里取消请求就可以了?

定位到适配器的源码 lib/adapters/xhr.js 第 158 行,

if (config.cancelToken) {
  // Handle cancellation
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }

    request.abort();
    reject(cancel);
    // Clean up request
    request = null;
  });
}

以及源码 lib/adaptors/http.js 第 291 行,

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 取消请求了。

小结

Axios 通过适配器的封装,使得它可以在保持同一套接口规范的前提下,同时用在浏览器和 node.js 中。源码中大量使用 Promise 和闭包等特性,实现了一系列的状态控制,其中对于拦截器,取消请求的实现体现了其极简的封装艺术,值得学习和借鉴。

参考链接

  • Axios Docs - axios-http.com[1]
  • Axios Github Source Code[2]
  • 源码拾遗 Axios —— 极简封装的艺术[3]
  • Cross Site Request Forgery - Part III. Web Application Security[4]
  • tc39/proposal-cancelable-promises[5]

写在最后

本文首发于我的 博客[6],才疏学浅,难免有错误,文章有误之处还望不吝指正!

如果有疑问或者发现错误,可以在相应的 issues 进行提问或勘误

如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励

(完)

作者:campcc https://github.com/campcc/blog/issues/23

文章分享自微信公众号:
Vue中文社区

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!

原始发表时间:2021-11-10
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • 一文读懂Kafka Connect核心概念

    Kafka Connect 是一种用于在 Apache Kafka 和其他系统之间可扩展且可靠地流式传输数据的工具。 它使快速定义将大量数据移入和移出 Kafk...

    从大数据到人工智能
  • 一文带你读懂 BigDecimal 源码

    本章带来的是BigDecimal类的源码解读。BigDecimal类是 Java 在 java.math 包中提供的API类,用来对超过16位有效位的数进行精确...

    后台技术汇
  • 一文读懂JDK源码:ThreadPoolExecutor

    线程池的思想是一种对象池的思想,开放一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。

    后台技术汇
  • 一文读懂 React 组件渲染核心原理

    相信大家对 React 都已经非常熟悉了,像 React,Vue 这样的现代前端框架已经是我们日常开发离不开的工具了,这篇文章主要是从源码的角度剖析 React...

    Tecvan
  • 硬核 | 一文搞懂网络设计核心思想

    本篇主要是对计算机网络一些核心思想理解,属于内功心法,初学者适合入门,非初学者可以学习其设计思想,总之希望帮助大家提高对网络的理解;

    CloudBest
  • 一文带你读懂Object类源码

    Object类是一个比较特殊的类,是所有类的超级父类,java中如果一个类没有用 extends关键字 明确指出继承于某个类,那么它默认继承Object类。下面...

    后台技术汇
  • 一文带你读懂String类源码

    String 是日常开发非常频繁的类,此外我们常用的操作还有字符串连接操作符等等。String对象是不可变的,查看JDK文档,我们不难发现String类的每个修...

    后台技术汇
  • 一文带你读懂JDK源码:synchronized

    Java提供的常用同步手段之一就是sychronized关键字,synchronized 是利用锁的机制来实现同步的。

    后台技术汇
  • [源码解读]一文读懂Vuex4源码

    前端LeBron
  • 一文带你读懂JDK源码:ThreadLocal类

    线程封闭是实现线程安全的手段之一(另外的线程安全手段还有:使用并发工具类,可以参考)。

    后台技术汇
  • 一文读懂Serverless,配置化思想复用到平台系统中

    在SaaS领域Saleforce是佼佼者,其CRM的概念已经扩展到了Marketing、Sales、Service等领域。那么Saleforce靠什么变成了这三...

    春哥大魔王
  • 一文读懂 HBase 核心原理与应用场景

    HBase是大数据NoSQL领域里非常重要的分布式KV数据库,是一个高可靠、高性能、高伸缩的分布式存储系统,目前国内知名公司都有在大规模使用,社区也非常活跃。本...

    大数据技术架构
  • 一文带你读懂容器技术(一):Docker 核心设计理念

    Docker 的操作围绕镜像、容器、仓库三大核心概念。下面我们通过解决三个问题:1、容器是什么;2、容器与虚拟机的区别;3、Docker 的三个核心概念是什么,...

    后台技术汇
  • 一文带你读懂JDK源码:Lambda表达式

    Lambda是函数式编程,可以将匿名方法像参数一样传递,本章节将从4个方面来介绍lambda:Lambda基础语法、Lambda表达式的应用层面、Lambda的...

    后台技术汇
  • 一文读懂以太坊存储数据核心数据结构:MPT

    MPT (Merkle Patricia Tries) 是以太坊存储数据的核心数据结构,它是由 Merkle Tree 和 Patricia Tree 结合的一...

    区块链大本营
  • 一文带你读懂JDK源码:JVM常用性能监控指令

    我们日常给系统定位问题,依据的是系统运行时的各项指标数据和工具,数据包括了:运行日志、异常堆栈、GC日志、线程快照、堆转储快照等;而工具则包括了:监视虚拟机和故...

    后台技术汇
  • 一文读懂 @Decorator 装饰器——理解 VS Code 源码的基础

    作者:easonruan,腾讯 CSIG 前端开发工程师 1. 装饰器的样子 我们先来看看 Decorator 装饰器长什么样子,大家可能没在项目中用过 Dec...

    腾讯技术工程官方号
  • 一文带你通过Sentinel源码,读懂微服务限流底层原理!

    限流是保障服务高可用的方式之一,尤其是在微服务架构中,对接口或资源进行限流可以有效地保障服务的可用性和稳定性。

    Java程序猿
  • 从源码入手,一文带你读懂Spring AOP面向切面编程

    之前《零基础带你看Spring源码——IOC控制反转》详细讲了Spring容器的初始化和加载的原理,后面《你真的完全了解Java动态代理吗?看这篇就够了》介绍了...

    Zack说码

扫码关注腾讯云开发者

领取腾讯云代金券