55

 深入浅出 axios 源码

 5 years ago
source link: https://zhuanlan.zhihu.com/p/37962469
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

深入浅出 axios 源码

前言

axios 是目前最常用的 http 请求库,可以用于浏览器和 node.js , 在 github 上已有 43k star 左右之多。

Axios 的主要特性包括:

  • 基于 Promise
  • 支持浏览器和 node.js
  • 可添加拦截器和转换请求和响应数据
  • 请求可以取消
  • 自动转换 JSON 数据
  • 客户端支持防范 XSRF
  • 支持各主流浏览器及 IE8+

本文 将 带大家一起阅读 axios 的源码, 解析当中的一些封装技巧、具体的功能实现、以及阅读源码的一些思路。

环境搭建

阅读源码并不是只是一味单纯的‘读’,很多时候面对复杂的前后文依赖关系以及输入输出, 经常会如同乱麻一般理不出思绪。这时候往往加上一些 log 远胜于凭空的想象。而且可以忽略一些不那么重要的环节,使得更容易抓住主干。

工欲善其事必先利器,我们需要打造一个 playground ,用来 watch 变化 观察我们的 log 输出。在 axios 中已经包含了 一些 examples ,但是项目构建工具基于 grunt 并 没有 watch 变化,我们需要自己去添加。
./GruntFile.js 中添加grunt.loadNpmTasks('grunt-contrib-watch'); 即可。 或者我们可以使用 构建工具来做到。然后启动 examples 服务器和 watch 构建任务即可。

npm run examples

npm run dev

# open localhost:3000

项目目录结构

├── /lib/                      # 项目源码目
│ ├── /adapters/               # 定义发送请求的适配器
│ │ ├── http.js                # node环境http对象
│ │ └── xhr.js                 # 浏览器环境XML对象
│ ├── /cancel/                 # 定义取消功能
│ ├── /helpers/                # 一些辅助方法
│ ├── /core/                   # 一些核心功能
│ │ ├── Axios.js               # axios实例构造函数
│ │ ├── createError.js         # 抛出错误
│ │ ├── dispatchRequest.js     # 用来调用http请求适配器方法发送请求
│ │ ├── InterceptorManager.js  # 拦截器管理器
│ │ ├── mergeConfig.js         # 合并参数
│ │ ├── settle.js              # 根据http响应状态,改变Promise的状态
│ │ └── transformData.js       # 改变数据格式
│ ├── axios.js                 # 入口,创建构造函数
│ ├── defaults.js              # 默认配置
│ └── utils.js                 # 公用工具

从 API 入手

分析源码的时候,我们需要先要从 API 入手,尝试着猜想下内部的结构、带着问题再去 看源码会更加有效。
我们来大致把 axios 的 API 进行 归纳分类:

axios API 分类

以上大致就是 axios 中 api 的大致分类。我们可以看出 暴露给我们的 axios 对象下挂载的除了 all 和 spread 以及取消请求的方法以外都被创建的实例给继承,可以单独使用并保持自己的上下文环境。让我们从入口开始看看是怎么实现的:

"use strict";

var utils = require("./utils");
var bind = require("./helpers/bind");
var Axios = require("./core/Axios");
var mergeConfig = require("./core/mergeConfig");
var defaults = require("./defaults");

/**
 * 创建Axios实例
 */
function createInstance(defaultConfig) {
  // new Axios 得到一个上下文环境 包含defatults配置以及拦截器
  var context = new Axios(defaultConfig);

  // instance实例为bind返回的一个函数(即是request发送请求方法),此时this绑定到context上下文环境
  var instance = bind(Axios.prototype.request, context);
  // 将Axios构造函数中的原型方法绑定到instance上并且指定this作用域为context上下文环境
  utils.extend(instance, Axios.prototype, context);
  // 把上下文环境中的defaults 以及拦截器绑定到instance实例中
  utils.extend(instance, context);

  return instance;
}

// axios入口其实就是一个创建好的实例
var axios = createInstance(defaults);
// 这句没太理解,根据作者的注释是:暴露Axios类去让类去继承
axios.Axios = Axios;

// 工厂函数 根据配置创建新的实例
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

// 绑定取消请求相关方法到入口对象
axios.Cancel = require("./cancel/Cancel");
axios.CancelToken = require("./cancel/CancelToken");
axios.isCancel = require("./cancel/isCancel");

// all 和 spread 两个处理并行的静态方法
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = require("./helpers/spread");

module.exports = axios;

// 允许使用Ts 中的 default import 语法
module.exports.default = axios;

通过以上入口方面分析我们可以看出端倪, axios 入口其实就是通过 createInstance 创建出的实例和 axios.create() 创建出的实例一样。而源码入口中的重中之中就是 createInstance 这个方法。createInstance 流程大致为:

  1. 使用 Axios 函数创建上下文 context ,包含自己的 defaults config 和 管理拦截器的数组
  2. 利用 Axios.prototype.request 和 上下文 创建实例 instance,实例为一个 request 发送请求的函数 this 指向上下文 context
  3. 绑定 Axios.prototype 的其他方法到 instance 实例,this 指向上下文 context
  4. 把上下文 context 中的 defaults 和拦截器绑定到 instance 实例

请求别名

在 axios 中 axios.get 、axios.delete 、axios.head 等别名请求方法其实都是指向同一个方法 axios.request 只是把 default config 中的 请求 methods 进行了修改而已。 具体代码在 Axios 这个构造函数的原型上,让我们来看下源码的实现:

utils.forEach(
  ["delete", "get", "head", "options"],
  function forEachMethodNoData(method) {
    Axios.prototype[method] = function(url, config) {
      return this.request(
        utils.merge(config || {}, {
          method: method,
          url: url
        })
      );
    };
  }
);

utils.forEach(["post", "put", "patch"], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(
      utils.merge(config || {}, {
        method: method,
        url: url,
        data: data
      })
    );
  };
});

因为 post 、 put 、 patch 有请求体,所以要 分开进行处理。请求别名方便用户快速使用各种不同 API 进行请求。

拦截器的实现

首先在我们创建实例中,会去创建上下文实例 也就是 new Axios ,会得到 interceptors 这个属性,这个属性分别又有 request 和 response 两个属性 , 它们的值分别是 new InterceptorManager 构造函数返回的数组。这个构造函数同样负责拦截器数组的添加和移除。让我们看下源码:

"use strict";

var utils = require("./../utils");

function InterceptorManager() {
  this.handlers = [];
}

// axio或实例上调用 interceptors.request.use 或者 interceptors.resopnse.use
// 传入的resolve, reject 将被添加入数组尾部
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};
// 移除拦截器,将该项在数组中置成null
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

// 辅助方法,帮助便利拦截器数组,跳过被eject置成null的项
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

module.exports = InterceptorManager;

上下文环境有了拦截器的数组, 又如何去 做到多个拦截器请求到响应的顺序处理以及实现呢?为了了解这点我们还需要进一步往下看 Axios.protoType.request 方法。

Axios.protoType.request

Axios.protoType.request 方法是请求开始的入口,分别处理了请求的 config,以及链式处理请求拦截器 、请求、响应拦截器,并返回 Proimse 的格式方便我们处理回调。让我们来看下源码部分:

Axios.prototype.request = function request(config) {
  //判断参数类型,支持axios('url',{})以及axios(config)两种形式
  if (typeof config === "string") {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }
  //传入参数与axios或实例下的defaults属性合并
  config = mergeConfig(this.defaults, config);
  config.method = config.method ? config.method.toLowerCase() : "get";

  // 创造一个请求序列数组,第一位是发送请求的方法,第二位是空
  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);
  });
  //遍历请求序列数组形成prmise链依次处理并且处理完弹出请求序列数组
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  //返回最终promise对象
  return promise;
};

我们可以看到 Axios.protoType.request 中使用了精妙的封装方法,形成 promise 链 去依次挂载在 then 方法顺序处理。为了更清晰的认识我们可以画个图去方便认识这一过程。

拦截器请求序列数组

取消请求

Axios.prototype.request 调用 dispatchRequest 是最终处理 axios 发起请求的函数,他的执行 过程流程包括了:

  1. 取消请求的处理和判断
  2. 处理 参数和默认参数
  3. 使用相对应的环境 adapter 发送请求(浏览器环境使用 XMLRequest 对象、Node 使用 http 对象)
  4. 返回后抛出取消请求 message,根据配置 transformData 转换 响应数据

这一过程除了取消请求的处理, 其余的流程都相对十分的简单,所以我们要对取消请求进行详细的分析。我们还是先看调用方式:

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 {
      // handle error
    }
  });

source.cancel("Operation canceled by the user.");

从调用方式我们可以看到,我们需要从 config 传入 axios.CancelToken.source().token , 并且可以用 axios.CancelToken.source().cancel() 执行取消请求。我们还可以从 看出 canel 函数不仅是取消了请求,并且 使得整个请求走入了 rejected 。从整个 API 设计我们就可以看出这块的 功能可能有点复杂, 让我们一点点来分析,从 CancelToken.source 这个方法看实现过程 :

CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

axios.CancelToken.source().token 返回的是一个 new CancelToken 的实例,axios.CancelToken.source().cancel,是 new CancelToken 是传入 new CancelToken 中的方法的一个参数。再看下 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) {
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

我们根据构造函数可以知道 axios.CancelToken.source().token 最终拿到的实例下挂载了 promise 和 reason 两个属性,promise 属性是一个处于 pending 状态的 promise 实例,reason 是执行 cancel 方法后传入的 message。而 axios.CancelToken.source().cancel 是一个函数方法,负责判断是否执行,若未执行拿到 axios.CancelToken.source().token.promise 中 executor 的 resolve 参数,作为触发器,触发处于处于 pending 状态中的 promise 并且 传入的 message 挂载在 xios.CancelToken.source().token.reason 下。若有 已经挂载在 reason 下则返回防止反复触发。而这个 pending 状态的 promise 在 cancel 后又是怎么进入 axios 总体 promise 的 rejected 中呢。我们需要看看 adpater 中的处理:

//如果有cancelToken
if (config.cancelToken) {
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }
    //取消请求
    request.abort();
    //axios的promise进入rejected
    reject(cancel);
    // 清楚request请求对象
    request = null;
  });
}

取消请求的总体逻辑大体如此,可能理解起来比较困难,需要反复看源码感受内部的流程,让我们大致在屡一下大致流程:

  1. axios.CancelToken.source()返回一个对象,tokens 属性 CancelToken 类的实例,cancel 是 tokens 内部 promise 的 resove 触发器
  2. axios 的 config 接受了 CancelToken 类的实例
  3. 当 cancel 触发处于 pending 中的 tokens.promise,取消请求,把 axios 的 promise 走向 rejected 状态

axios 的大体流程 如上述般大体介绍完了,我们可以画个图更加直观的梳理一下

axios 请求流程

Axios 的源码分析就到这里,如果有错请多多指教


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK