77.9K Star 的 Axios 项目有哪些值得借鉴的地方

Axios 是一个基于 Promise 的 HTTP 客户端,同时支持浏览器和 Node.js 环境。它是一个优秀的 HTTP 客户端,被普遍地应用在大量的 Web 项目中。javascript

由上图可知,Axios 项目的 Star 数为 77.9K,Fork 数也高达 7.3K,是一个很优秀的开源项目,因此接下来阿宝哥将带你们一块儿来分析 Axios 项目中一些值得借鉴的地方。阅读完本文,你将了解如下内容:html

  • HTTP 拦截器的设计与实现;
  • HTTP 适配器的设计与实现;
  • 如何防护 CSRF 攻击。

下面咱们从简单的开始,先来了解一下 Axios。java

关注 「全栈修仙之路」 阅读其余源码分析文章及 50 篇 “重学TS” 教程。node

1、Axios 简介

Axios 是一个基于 Promise 的 HTTP 客户端,拥有如下特性:ios

  • 支持 Promise API;
  • 可以拦截请求和响应;
  • 可以转换请求和响应数据;
  • 客户端支持防护 CSRF 攻击;
  • 同时支持浏览器和 Node.js 环境;
  • 可以取消请求及自动转换 JSON 数据。

在浏览器端 Axios 支持大多数主流的浏览器,好比 Chrome、Firefox、Safari 和 IE 11。此外,Axios 还拥有本身的生态:git

(数据来源 —— github.com/axios/axios…github

简单介绍完 Axios,咱们来分析一下它提供的一个核心功能 —— 拦截器。ajax

2、HTTP 拦截器的设计与实现

2.1 拦截器简介

对于大多数 SPA 应用程序来讲, 一般会使用 token 进行用户的身份认证。这就要求在认证经过后,咱们须要在每一个请求上都携带认证信息。针对这个需求,为了不为每一个请求单独处理,咱们能够经过封装统一的 request 函数来为每一个请求统一添加 token 信息。axios

但后期若是须要为某些 GET 请求设置缓存时间或者控制某些请求的调用频率的话,咱们就须要不断修改 request 函数来扩展对应的功能。此时,若是在考虑对响应进行统一处理的话,咱们的 request 函数将变得愈来愈庞大,也愈来愈难维护。那么对于这个问题,该如何解决呢?Axios 为咱们提供了解决方案 —— 拦截器。数组

Axios 是一个基于 Promise 的 HTTP 客户端,而 HTTP 协议是基于请求和响应:

因此 Axios 提供了请求拦截器和响应拦截器来分别处理请求和响应,它们的做用以下:

  • 请求拦截器:该类拦截器的做用是在请求发送前统一执行某些操做,好比在请求头中添加 token 字段。
  • 响应拦截器:该类拦截器的做用是在接收到服务器响应后统一执行某些操做,好比发现响应状态码为 401 时,自动跳转到登陆页。

在 Axios 中设置拦截器很简单,经过 axios.interceptors.requestaxios.interceptors.response 对象提供的 use 方法,就能够分别设置请求拦截器和响应拦截器:

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
  config.headers.token = 'added by interceptor';
  return config;
});

// 添加响应拦截器
axios.interceptors.response.use(function (data) {
  data.data = data.data + ' - modified by interceptor';
  return data;
});
复制代码

那么拦截器是如何工做的呢?在看具体的代码以前,咱们先来分析一下它的设计思路。Axios 的做用是用于发送 HTTP 请求,而请求拦截器和响应拦截器的本质都是一个实现特定功能的函数。

咱们能够按照功能把发送 HTTP 请求拆解成不一样类型的子任务,好比有用于处理请求配置对象的子任务,用于发送 HTTP 请求的子任务和用于处理响应对象的子任务。当咱们按照指定的顺序来执行这些子任务时,就能够完成一次完整的 HTTP 请求。

了解完这些,接下来咱们将从 任务注册、任务编排和任务调度 三个方面来分析 Axios 拦截器的实现。

2.2 任务注册

经过前面拦截器的使用示例,咱们已经知道如何注册请求拦截器和响应拦截器,其中请求拦截器用于处理请求配置对象的子任务,而响应拦截器用于处理响应对象的子任务。要搞清楚任务是如何注册的,就须要了解 axiosaxios.interceptors 对象。

// lib/axios.js
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);
  // Copy context to instance
  utils.extend(instance, context);
  return instance;
}

// Create the default instance to be exported
var axios = createInstance(defaults);
复制代码

在 Axios 的源码中,咱们找到了 axios 对象的定义,很明显默认的 axios 实例是经过 createInstance 方法建立的,该方法最终返回的是 Axios.prototype.request 函数对象。同时,咱们发现了 Axios 的构造函数:

// lib/core/Axios.js
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
复制代码

在构造函数中,咱们找到了 axios.interceptors 对象的定义,也知道了 interceptors.requestinterceptors.response 对象都是 InterceptorManager 类的实例。所以接下来,进一步分析 InterceptorManager 构造函数及相关的 use 方法就能够知道任务是如何注册的:

// lib/core/InterceptorManager.js
function InterceptorManager() {
  this.handlers = [];
}

InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  // 返回当前的索引,用于移除已注册的拦截器
  return this.handlers.length - 1;
};
复制代码

经过观察 use 方法,咱们可知注册的拦截器都会被保存到 InterceptorManager 对象的 handlers 属性中。下面咱们用一张图来总结一下 Axios 对象与 InterceptorManager 对象的内部结构与关系:

2.3 任务编排

如今咱们已经知道如何注册拦截器任务,但仅仅注册任务是不够,咱们还须要对已注册的任务进行编排,这样才能确保任务的执行顺序。这里咱们把完成一次完整的 HTTP 请求分为处理请求配置对象、发起 HTTP 请求和处理响应对象 3 个阶段。

接下来咱们来看一下 Axios 如何发请求的:

axios({
  url: '/hello',
  method: 'get',
}).then(res =>{
  console.log('axios res: ', res)
  console.log('axios res.data: ', res.data)
})
复制代码

经过前面的分析,咱们已经知道 axios 对象对应的是 Axios.prototype.request 函数对象,该函数的具体实现以下:

// lib/core/Axios.js
Axios.prototype.request = function request(config) {
  config = mergeConfig(this.defaults, config);

  // 省略部分代码
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  // 任务编排
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  // 任务调度
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};
复制代码

任务编排的代码比较简单,咱们来看一下任务编排前和任务编排后的对比图:

2.4 任务调度

任务编排完成后,要发起 HTTP 请求,咱们还须要按编排后的顺序执行任务调度。在 Axios 中具体的调度方式很简单,具体以下所示:

// lib/core/Axios.js
Axios.prototype.request = function request(config) {
  // 省略部分代码
  var promise = Promise.resolve(config);
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
}
复制代码

由于 chain 是数组,因此经过 while 语句咱们就能够不断地取出设置的任务,而后组装成 Promise 调用链从而实现任务调度,对应的处理流程以下图所示:

下面咱们来回顾一下 Axios 拦截器完整的使用流程:

// 添加请求拦截器 —— 处理请求配置对象
axios.interceptors.request.use(function (config) {
  config.headers.token = 'added by interceptor';
  return config;
});

// 添加响应拦截器 —— 处理响应对象
axios.interceptors.response.use(function (data) {
  data.data = data.data + ' - modified by interceptor';
  return data;
});

axios({
  url: '/hello',
  method: 'get',
}).then(res =>{
  console.log('axios res.data: ', res.data)
})
复制代码

介绍完 Axios 的拦截器,咱们来总结一下它的优势。Axios 经过提供拦截器机制,让开发者能够很容易在请求的生命周期中自定义不一样的处理逻辑。此外,也能够经过拦截器机制来灵活地扩展 Axios 的功能,好比 Axios 生态中列举的 axios-response-loggeraxios-debug-log 这两个库。

参考 Axios 拦截器的设计模型,咱们就能够抽出如下通用的任务处理模型:

3、HTTP 适配器的设计与实现

3.1 默认 HTTP 适配器

Axios 同时支持浏览器和 Node.js 环境,对于浏览器环境来讲,咱们能够经过 XMLHttpRequestfetch API 来发送 HTTP 请求,而对于 Node.js 环境来讲,咱们能够经过 Node.js 内置的 httphttps 模块来发送 HTTP 请求。

为了支持不一样的环境,Axios 引入了适配器。在 HTTP 拦截器设计部分,咱们看到了一个 dispatchRequest 方法,该方法用于发送 HTTP 请求,它的具体实现以下所示:

// lib/core/dispatchRequest.js
module.exports = function dispatchRequest(config) {
  // 省略部分代码
  var adapter = config.adapter || defaults.adapter;
  
  return adapter(config).then(function onAdapterResolution(response) {
    // 省略部分代码
    return response;
  }, function onAdapterRejection(reason) {
    // 省略部分代码
    return Promise.reject(reason);
  });
};
复制代码

经过查看以上的 dispatchRequest 方法,咱们可知 Axios 支持自定义适配器,同时也提供了默认的适配器。对于大多数场景,咱们并不须要自定义适配器,而是直接使用默认的适配器。所以,默认的适配器就会包含浏览器和 Node.js 环境的适配代码,其具体的适配逻辑以下所示:

// lib/defaults.js
var defaults = {
  adapter: getDefaultAdapter(),
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',
  //...
}

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;
}
复制代码

getDefaultAdapter 方法中,首先经过平台中特定的对象来区分不一样的平台,而后再导入不一样的适配器,具体的代码比较简单,这里就不展开介绍。

3.2 自定义适配器

其实除了默认的适配器外,咱们还能够自定义适配器。那么如何自定义适配器呢?这里咱们能够参考 Axios 提供的示例:

var settle = require('./../core/settle');
module.exports = function myAdapter(config) {
  // 当前时机点:
  // - config配置对象已经与默认的请求配置合并
  // - 请求转换器已经运行
  // - 请求拦截器已经运行
  
  // 使用提供的config配置对象发起请求
  // 根据响应对象处理Promise的状态
  return new Promise(function(resolve, reject) {
    var response = {
      data: responseData,
      status: request.status,
      statusText: request.statusText,
      headers: responseHeaders,
      config: config,
      request: request
    };

    settle(resolve, reject, response);

    // 此后:
    // - 响应转换器将会运行
    // - 响应拦截器将会运行
  });
}
复制代码

在以上示例中,咱们主要关注转换器、拦截器的运行时机点和适配器的基本要求。好比当调用自定义适配器以后,须要返回 Promise 对象。这是由于 Axios 内部是经过 Promise 链式调用来完成请求调度,不清楚的小伙伴能够从新阅读 “拦截器的设计与实现” 部分的内容。

如今咱们已经知道如何自定义适配器了,那么自定义适配器有什么用呢?在 Axios 生态中,阿宝哥发现了 axios-mock-adapter 这个库,该库经过自定义适配器,让开发者能够轻松地模拟请求。对应的使用示例以下所示:

var axios = require("axios");
var MockAdapter = require("axios-mock-adapter");

// 在默认的Axios实例上设置mock适配器
var mock = new MockAdapter(axios);

// 模拟 GET /users 请求
mock.onGet("/users").reply(200, {
  users: [{ id: 1, name: "John Smith" }],
});

axios.get("/users").then(function (response) {
  console.log(response.data);
});
复制代码

对 MockAdapter 感兴趣的小伙伴,能够自行了解一下 axios-mock-adapter 这个库。到这里咱们已经介绍了 Axios 的拦截器与适配器,下面阿宝哥用一张图来总结一下 Axios 使用请求拦截器和响应拦截器后,请求的处理流程:

4、CSRF 防护

4.1 CSRF 简介

跨站请求伪造(Cross-site request forgery),一般缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登陆的 Web 应用程序上执行非本意的操做的攻击方法。

跨站请求攻击,简单地说,是攻击者经过一些技术手段欺骗用户的浏览器去访问一个本身曾经认证过的网站并运行一些操做(如发邮件,发消息,甚至财产操做如转帐和购买商品)。因为浏览器曾经认证过,因此被访问的网站会认为是真正的用户操做而去运行。

为了让小伙伴更好地理解上述的内容,阿宝哥画了一张跨站请求攻击示例图:

在上图中攻击者利用了 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求自己是用户自愿发出的。既然存在以上的漏洞,那么咱们应该怎么进行防护呢?接下来咱们来介绍一些常见的 CSRF 防护措施。

4.2 CSRF 防护措施

4.2.1 检查 Referer 字段

HTTP 头中有一个 Referer 字段,这个字段用以标明请求来源于哪一个地址。在处理敏感数据请求时,一般来讲,Referer 字段应和请求的地址位于同一域名下

以示例中商城操做为例,Referer 字段地址一般应该是商城所在的网页地址,应该也位于 www.semlinker.com 之下。而若是是 CSRF 攻击传来的请求,Referer 字段会是包含恶意网址的地址,不会位于 www.semlinker.com 之下,这时候服务器就能识别出恶意的访问。

这种办法简单易行,仅须要在关键访问处增长一步校验。但这种办法也有其局限性,因其彻底依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并没有法保证来访的浏览器的具体实现,亦没法保证浏览器没有安全漏洞影响到此字段。而且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。

4.2.2 同步表单 CSRF 校验

CSRF 攻击之因此可以成功,是由于服务器没法区分正常请求和攻击请求。针对这个问题咱们能够要求全部的用户请求都携带一个 CSRF 攻击者没法获取到的 token。对于 CSRF 示例图中的表单攻击,咱们可使用 同步表单 CSRF 校验 的防护措施。

同步表单 CSRF 校验 就是在返回页面时将 token 渲染到页面上,在 form 表单提交的时候经过隐藏域或者做为查询参数把 CSRF token 提交到服务器。好比,在同步渲染页面时,在表单请求中增长一个 _csrf 的查询参数,这样当用户在提交这个表单的时候就会将 CSRF token 提交上来:

<form method="POST" action="/upload?_csrf={{由服务端生成}}" enctype="multipart/form-data">
  用户名: <input name="name" />
  选择头像: <input name="file" type="file" />
  <button type="submit">提交</button>
</form>
复制代码
4.2.3 双重 Cookie 防护

双重 Cookie 防护 就是将 token 设置在 Cookie 中,在提交(POST、PUT、PATCH、DELETE)等请求时提交 Cookie,并经过请求头或请求体带上 Cookie 中已设置的 token,服务端接收到请求后,再进行对比校验。

下面咱们以 jQuery 为例,来看一下如何设置 CSRF token:

let csrfToken = Cookies.get('csrfToken');

function csrfSafeMethod(method) {
  // 如下HTTP方法不须要进行CSRF防御
  return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}

$.ajaxSetup({
  beforeSend: function(xhr, settings) {
    if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
      xhr.setRequestHeader('x-csrf-token', csrfToken);
    }
  },
});
复制代码

介绍完 CSRF 攻击的方式和防护手段,最后咱们来看一下 Axios 是如何防护 CSRF 攻击的。

4.3 Axios CSRF 防护

Axios 提供了 xsrfCookieNamexsrfHeaderName 两个属性来分别设置 CSRF 的 Cookie 名称和 HTTP 请求头的名称,它们的默认值以下所示:

// lib/defaults.js
var defaults = {
  adapter: getDefaultAdapter(),

  // 省略部分代码
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',
};
复制代码

前面咱们已经知道在不一样的平台中,Axios 使用不一样的适配器来发送 HTTP 请求,这里咱们以浏览器平台为例,来看一下 Axios 如何防护 CSRF 攻击:

// lib/adapters/xhr.js
module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestHeaders = config.headers;
    
    var request = new XMLHttpRequest();
    // 省略部分代码
    
    // 添加xsrf头部
    if (utils.isStandardBrowserEnv()) {
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;

      if (xsrfValue) {
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }

    request.send(requestData);
  });
};
复制代码

看完以上的代码,相信小伙伴们就已经知道答案了,原来 Axios 内部是使用 双重 Cookie 防护 的方案来防护 CSRF 攻击。好的,到这里本文的主要内容都已经介绍完了,其实 Axios 项目还有一些值得咱们借鉴的地方,好比 CancelToken 的设计、异常处理机制等,感兴趣的小伙伴能够自行学习一下。

5、参考资源

相关文章
相关标签/搜索