axios简介

一、axios是一个基于Promise的HTTP客户端的网络请求库,作用于Node.js和浏览器中。它是isomorphic的(即同一套代码可以运行在浏览器和Node.js中)。在服务端它使用原生Node.js http模块,而在客户端则使用XMLHttpRequest。
二、拥有的特性

  1. 同时支持浏览器和Node.js环境
  2. 支持Promise API
  3. 拦截请求和响应
  4. 转换请求和响应数据
  5. 能够取消请求
  6. 自动转换JSON数据
  7. 客户端支持防御CSRF攻击

三、axios生态
axios - 图1

HTTP拦截器的设计与实现

拦截器是axios的核心功能

拦截器简介

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

所以Axios提供了请求拦截器和响应拦截器来分别处理请求和响应。
二、请求拦截器、响应拦截器的作用
1、请求拦截器:该类拦截器的作用是在请求发送前统一执行某些操作,比如在请求头中添加token字段。
2、响应拦截器:该类拦截器的作用是接收到服务器响应后统一执行某些操作,比如发现响应状态码为401时,自动跳转到登录页。
三、设置拦截器,通过axios.interceptors.request和axios.interceptors.response对象提供的use方法,就可以分别设置请求拦截器和响应拦截器

  1. // 添加请求拦截器 —— 处理请求配置对象
  2. axios.interceptors.request.use(function (config) {
  3. config.headers.token = 'added by interceptor'
  4. return config
  5. })
  6. // 添加响应拦截器 —— 处理响应对象
  7. axios.interceptors.response.use(function (data) {
  8. data.data = data.data + '- modified by interceptor'
  9. return data
  10. })

拦截器实现

一、按照功能把发送HTTP请求拆解成不同类型的子任务
1、用于处理请求配置对象的子任务
2、用于发送HTTP请求的子任务
3、用于处理响应对象的子任务。
当我们按照指定的顺序来执行子任务时,就可以完成一次完成的HTTP请求。
二、从任务注册、任务编排、任务调度三个方面分析axios拦截器的实现

任务注册

一、在Axios源码中,可以找到axios对象的定义。
1、默认的axios实例是通过createInstance 方法创建的,该方法最终返回的是Axios.prototype.request函数对象。

  1. // lib/axios.js
  2. function createInstance(defaultConfig) {
  3. var context = new Axios(defaultConfig);
  4. var instance = bind(Axios.prototype.request, context);
  5. // Copy axios.prototype to instance
  6. utils.extend(instance, Axios.prototype, context);
  7. // Copy context to instance
  8. utils.extend(instance, context);
  9. return instance;
  10. }
  11. // Create the default instance to be exported
  12. var axios = createInstance(defaults);

二、Axios的构造函数
1、可以找到axios.interceptors对象的定义,也知道了interceptors.request和interceptors.response对象都是InterceptorManager类的实例。

  1. // lib/core/Axios.js
  2. function Axios(instanceConfig) {
  3. this.defaults = instanceConfig;
  4. this.interceptors = {
  5. request: new InterceptorManager(),
  6. response: new InterceptorManager()
  7. };
  8. }

三、InterceptorMangager构造函数及相关的use方法

  1. // lib/core/InterceptorManager.js
  2. function InterceptorManager() {
  3. this.handlers = [];
  4. }
  5. InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  6. this.handlers.push({
  7. fulfilled: fulfilled,
  8. rejected: rejected
  9. });
  10. // 返回当前的索引,用于移除已注册的拦截器
  11. return this.handlers.length - 1;
  12. };

1、通过观察use方法,我们可知注册的拦截器都会被保存到InterceptorManager对象的handlers属性中。
2、Axios对象与InterceptorManager对象的内部结构与关系
image.png

任务编排

一、对已注册的任务进行编排,这样才能确保任务的执行顺序。
二、把一次完整的HTTP请求分为处理请求配置对象,发起HTTP请求和处理响应对象3个阶段。
三、Axios如何发请求的
1、

  1. axios({
  2. url: '/hello',
  3. method: 'get',
  4. }).then(res =>{
  5. console.log('axios res: ', res)
  6. console.log('axios res.data: ', res.data)
  7. })

2、通过前面的分析,已知axios对象对应的是Axios.prototype.request函数对象,该函数的具体实现如下:

  1. // lib/core/Axios.js
  2. Axios.prototype.request = function request(config) {
  3. config = mergeConfig(this.defaults, config);
  4. // 省略部分代码
  5. var chain = [dispatchRequest, undefined];
  6. var promise = Promise.resolve(config);
  7. // 任务编排
  8. this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
  9. chain.unshift(interceptor.fulfilled, interceptor.rejected);
  10. });
  11. this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
  12. chain.push(interceptor.fulfilled, interceptor.rejected);
  13. });
  14. // 任务调度
  15. while (chain.length) {
  16. promise = promise.then(chain.shift(), chain.shift());
  17. }
  18. return promise;
  19. };

四、任务编排前后对比图
image.png

任务调度

一、任务编排完成后,要发起HTTP请求,我们还需要按编排后的顺序执行任务调度。
二、在Axios中具体的调度方式

  1. // lib/core/Axios.js
  2. Axios.prototype.request = function request(config) {
  3. // 省略部分代码
  4. var promise = Promise.resolve(config);
  5. while (chain.length) {
  6. promise = promise.then(chain.shift(), chain.shift());
  7. }
  8. }

1、因为chain是数组,所以通过while语句我们就可以不断地取出设置的任务,然后组装成Promise调用链从而实现任务调度,对应的处理流程如下图所示
image.png

Axios拦截器的优点

一、Axios通过提供拦截器机制,让开发者可以很容易在请求的生命周期中自定义不同的处理逻辑。
二、可以通过拦截器机制来灵活地扩展Axios的功能,比如Axios生态中列举的axios-response-logger和axios-debug-log这两个库。
三、参考Axios拦截器的设计模型,我们就可以抽出以下通用的任务处理模型
image.png

HTTP适配器的设计与实现

默认HTTP适配器

一、Axios同时支持浏览器和Node.js环境。
1、浏览器环境,我们可以通过XMLHttpRequst 或 fetch API 来发送HTTP请求。
2、Node.js环境,我们可以通过Node.js内置的http或https模块来发送HTTP请求。
二、为了支持不同的环境,Axios引入了适配器。

axios中适配器的实现

一、在HTTP拦截设计部分,我没看到了一个dispatchRequst方法,该方法用于发送HTTP请求,它的具体实现如下所示

  1. // lib/core/dispatchRequest.js
  2. module.exports = function dispatchRequest(config) {
  3. // 省略部分代码
  4. var adapter = config.adapter || defaults.adapter;
  5. return adapter(config).then(function onAdapterResolution(response) {
  6. // 省略部分代码
  7. return response;
  8. }, function onAdapterRejection(reason) {
  9. // 省略部分代码
  10. return Promise.reject(reason);
  11. });
  12. };

1、通过查看dispatchRequest方法,可知Axios支持自定义适配器,同时也提供了默认的适配器。对于大多数场景,我们并不需要自定义适配器,而是直接使用默认的适配器。
2、默认的适配器包含浏览器和Node.js环境的适配代码,具体的适配逻辑如下
(1)在getDefaultAdapter方法中,首先通过平台中特定的对象来区分不同的平台,然后再导入不同的适配器。

  1. // lib/defaults.js
  2. var defaults = {
  3. adapter: getDefaultAdapter(),
  4. xsrfCookieName: 'XSRF-TOKEN',
  5. xsrfHeaderName: 'X-XSRF-TOKEN',
  6. //...
  7. }
  8. function getDefaultAdapter() {
  9. var adapter;
  10. if (typeof XMLHttpRequest !== 'undefined') {
  11. // For browsers use XHR adapter
  12. adapter = require('./adapters/xhr');
  13. } else if (typeof process !== 'undefined' &&
  14. Object.prototype.toString.call(process) === '[object process]') {
  15. // For node use HTTP adapter
  16. adapter = require('./adapters/http');
  17. }
  18. return adapter;
  19. }

自定义适配器

一、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);

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

1、主要关注转换器、拦截器的运行是几点和适配器的基本要求。
(1)当调用自定义适配器之后,需要返回Promise对象。这是因为Axios内部是通过Promise链式调用来完成请求调度。

自定义适配器的用处

一、在Axios生态中,axios-mock-adapter这个库通过自定义适配器,让开发者可以轻松地模拟请求。
1、示例

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

请求的处理流程

Axios使用请求拦截器和响应拦截器后,请求的处理流程
image.png

CSRF防御

CSRF防御措施

一、Axios内部是使用双重Cookie防御的方案来防御CSRF攻击。
二、Axios提供了xsrfCookieName和xsrfHeaderName两个属性来分别设置CSRF的Cookie名称和HTTP请求头的名称,它们的默认值如下

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

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

1、以浏览器平台为例,来看一下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);
  });
};

CancelToken的设计

异常处理机制