探析axios源码实现

Posted by franki on November 26, 2019

axios流程图

axios flow

axios 项目目录概览


├── /dist/                     # 项目输出目录
├── /lib/                      # 项目源码目录
│ ├── /cancel/                 # 定义取消功能
│ ├── /core/                   # 一些核心功能
│ │ ├── Axios.js               # axios的核心主类
│ │ ├── dispatchRequest.js     # 用来调用http请求适配器方法发送请求
│ │ ├── InterceptorManager.js  # 拦截器构造函数
│ │ └── settle.js              # 根据http响应状态,改变Promise的状态
│ ├── /helpers/                # 一些辅助方法
│ ├── /adapters/               # 定义请求的适配器 xhr、http
│ │ ├── http.js                # 实现http适配器
│ │ └── xhr.js                 # 实现xhr适配器
│ ├── axios.js                 # 对外暴露接口
│ ├── defaults.js              # 默认配置
│ └── utils.js                 # 公用工具
├── package.json               # 项目信息
├── index.d.ts                 # 配置TypeScript的声明文件
└── index.js                   # 入口文件

note:以下所有代码的讲解都是基于lib目录

名词概述

  • 拦截器interceptors

    axios里面的拦截器分为请求拦截器(request interceptor)和响应拦截器(response interceptor),前者是可以在每次http请求前拦截,并可修改配置config;后者则是在http请求后,对返回的数据进行修改处理;

  • 数据转换器transformData

    axios里面的转换器分为请求转换器(transformRequest)和响应转换器 (transformResponse),前者可以在请求前改变即将发送的data数据,后者则可以在请求完成后对于data进行数据修改。

  • http请求适配器

    顾名思义,就是选择合适的方法进行http请求(node环境使用http模块,browser环境使用xhr模块)

  • config配置项

    每次请求使用的配置项,config非常重要,是串起整个axios内部与用户通信的主要通道

源码分析

首先来看看axios的入口文件,为什么axios可以提供如此多的接口调用,原因就是axios的createInstance方法的作用使然。

// /lib/axios.js
/**
 * 创建Axios的实例
 *
 * @param 实例的默认配置
 * @return Axios的实例
 */
function createInstance(defaultConfig) {
  // 创建一个Axios实例
  var context = new Axios(defaultConfig);
  // instance 指向request方法,并且绑定上下文context,所以instance(option)得以调用
  var instance = bind(Axios.prototype.request, context);

  // 把Axios.prototype的方法扩展到instance上来
  // 这样instance就有了get post put delete等等方法
  // 而且指定了context上下文,这样在访问Axios.prototype上的方法时,this指向context
  utils.extend(instance, Axios.prototype, context);

  // 拷贝context自身的属性到instance上
  utils.extend(instance, context);

  return instance;
}

// invoke
// 接收默认配置项作为参数,创建一个axios实例,最终导出的是一个对象
var axios = createInstance(defaults);

看到没有,createInstance最终返回的是一个Function,这个Function其实就是Axios.prototype.request,这个Function还有Axios.prototype的每个方法作为静态方法,而且这些方法都是指向同一个上下文context。

Axios是axios的核心,一个Axios的实例就是一个axios应用。而Axios上核心的方法就是request,所有的请求都是通过这个方法发出的。

/**
 * 创建一个Axios的实例
 *
 * @param {Object} instanceConfig作为默认配置项
 */
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

/**
 * 触发一个请求
 *
 * @param {Object} 配置项(merge 传入的参数和默认的参数)
 */
Axios.prototype.request = function request(config) {
  ...省略
};

// 提供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
    }));
  };
});

通过以上的代码,可以做到axios.get、axios.post、axios.put等,可以看到,即是名称不同,但是最终都是通过Axios.prototype.request去触发请求的

一般情况上,上面的方法就可以满足大部分的需求了,如果不满足的导出的axios实例,axios还提供了创建新的axios实例的接口

axios.Axios = Axios;
axios.create = function create(instanceConfig) {
  return createInstace(Util.merge(axios.defaults, instanceConfig));
}

绕来绕去都是要通过Axios.prototype.request去触发请求

是不是很想知道Axios.prototype.request做了什么呢?

这个request到底是怎么根据我们配置的config去发送请求

贯穿axios项目的config

Axios的config里面的项有:

http请求适配器(adapter)、请求地址(url)、请求方法(method)、请求header、请求数据(data)、请求或者响应数据的转换、请求进度、http状态码验证规则、超时(timeout)、取消请求(cancelRequest)等等

可以看到所有的功能都是通过对象的形式传递的

再来看下,用户一般如何来设置配置项的

1 直接修改Axios实例上的defaults属性主要设置通用配置
axios.defaults[configName] = value;

2 发起请求时传入配置项
axios({url, method, headers})

3 新创建axios实例时传入配置项此处也是设置通用配置项
let newAxiosInstance = axios.create({[configName]: value})

然后找到Axios.prototype.request方法的其中一行(/lib/core/Axios.js)

config = merge(defaults, {method: 'get'}, this.defaults, config)

很明显,上面把defaults(/lib/defaults)、this.defaults(Axios实例的defaults)、config(请求传入的config)进行合并

优先级是config > this.defaults > {method: ‘get’} > defaults

上面说config贯穿了整个axios项目,那config到底是如何传递的呢?

Axios.prototype.request = function request(config) {

  config = mergeConfig(this.defaults, config);

  // interceptor中间件
  var chain = [dispatchRequest, undefined];
  // 重点就是这里,把config对象作为参数传递给Promise.resolve
  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) {
    // config会按顺序通过 requestInterceptors - dispatchRequest - resposeInterceptors
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

看到没有,config就是通过这样,作为promise.resolve的参数,传递到后续的promise.then(fulfilled, reject).then…,是不是很有趣?

上面出现chain其实就是个中间件,里面放置的数据主要是请求拦截器,(dispatchReuest、undefined),响应拦截器

这里的

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

必须要这样做吗?

答案是确实需要这样,因为有地方已经说过了,axios的请求流程是 request interceptors > dispatchRequest > response interceptors

最终chain生成的结果是

[requestInterce2.fulfilled, requestInterceptor2.rejected, requestInterce1.fulfilled, requestInterceptor1.rejected, dispatchRequest, undefined, responseInterce1.fulfilled, responseInterceptor1.rejected, responseInterce2.fulfilled, responseInterceptor2.rejected]

为什么要生成这样的数组?为什么中间有个是undefined?

while (chain.length) {
  // config会按顺序通过 requestInterceptors - dispatchRequest - resposeInterceptors
  promise = promise.then(chain.shift(), chain.shift());
}

以上的的代码很好解释了为什么要生成这样的数组,因为promise.then方法的参数都是chain.shift()出来的,这样就可以做到先执行请求拦截器,然后dispatchRequest,最后执行响应拦截器。

拦截器如何做到拦截的

先看看axios拦截器的正确打开方式

// 添加请求拦截器
const myRequestInterceptor = axios.interceptors.request.use(config => {
    // 在发送http请求之前做些什么
    return config; // 有且必须有一个config对象被返回
}, error => {
    // 对请求错误做些什么
    return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use(response => {
  // 对响应数据做点什么
  return response; // 有且必须有一个response对象被返回
}, error => {
  // 对响应错误做点什么
  return Promise.reject(error);
});

// 移除某次拦截器
axios.interceptors.request.eject(myRequestInterceptor);

拦截器在Axios源码里面是如何定义并且使用的


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

每一个Axios实例都有一个interceptors属性,这个属性就是用来保存管理请求、响应拦截器的

接着继续看下Interceptor构造函数


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

// 用来注销指定的拦截器
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

// 遍历this.handlers,并将this.handlers里的每一项作为参数传给fn执行
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

想想为什么InterceptorManager.prototype.eject注销方法,this.handlers[id] = null?

答案是为了不改变this.handlers的顺序,方便插入、查找

结合以上可以看到,dispatchRequest做了以下的事情:

  1. 拿到config对象,对config对象进行传递,request interceptor优先对于config进行处理
  2. http请求适配器根据config配置,发起请求
  3. http请求完成后,则根据header、data、config.transformResponse

请求适配器是如何工作的


  return adapter(config).then(function onAdapterResolution(response) {
    // ...
    return response;
  }, function onAdapterRejection(reason) {
    // ...
    return Promise.reject(reason);
  });
};

除非重新配置adapter否则默认使用默认的adapter,首先得到对应的adapter方法返回的promise对象,通过then方法,对结果进行进一步处理,成功使用onAdapterResolution处理,失败则使用onAdapterRejection处理

如何选择合适的请求模块?

var defaults = {
  adapter: getDefaultAdapter()
};

function getDefaultAdapter() {
  var adapter;
  
  if (typeof XMLHttpRequest !== 'undefined') {
    // 如果是浏览器环境使用xhr adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // 如果是node环境使用http模块
    adapter = require('./adapters/http');
  }
  return adapter;
}

数据转换器

官方教程使用:

// 全局通用
import axios from 'axios'

// 往现有的请求转换器里增加转换方法
axios.defaults.transformRequest.push((data, headers) => {
  // ...处理data
  return data;
});

// 重写请求转换器
axios.defaults.transformRequest = [(data, headers) => {
  // ...处理data
  return data;
}];

// 往现有的响应转换器里增加转换方法
axios.defaults.transformResponse.push((data, headers) => {
  // ...处理data
  return data;
});

// 重写响应转换器
axios.defaults.transformResponse = [(data, headers) => {
  // ...处理data
  return data;
}];



// 具体请求携带的转换器
import axios from 'axios'

// 往已经存在的转换器里增加转换方法
axios.get(url, {
  // ...
  transformRequest: [
    ...axios.defaults.transformRequest, // 去掉这行代码就等于重写请求转换器了
    (data, headers) => {
      // ...处理data
      return data;
    }
  ],
  transformResponse: [
    ...axios.defaults.transformResponse, // 去掉这行代码就等于重写响应转换器了
    (data, headers) => {
      // ...处理data
      return data;
    }
  ],
})

axios里面的转换器源码部分


// /lib/defaults.js
var defaults = {

  transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Content-Type');
    // ...
    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) {
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
  }],
  
};

那在axios运行中,什么时候什么地方使用了转换器呢?


// /lib/core/dispatchRequest.js
function dispatchRequest(config) {
  
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  return adapter(config).then(/* ... */);
};

transformData具体做了什么呢?


// /lib/core/transformData.js
function transformData(data, headers, fns) {
  utils.forEach(fns, function transform(fn) {
    data = fn(data, headers);
  });
  return data;
};


主要是遍历转换器数组,分别执行每一个转换器,根据data和headers参数,返回新的data


// /lib/core/dispatchRequest.js
return adapter(config).then(function onAdapterResolution(response) {
    // ...
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      // ...
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });


响应转换器的使用地方是在http请求完成后,根据http请求适配器的返回值做数据转换处理

拦截器和转换器的关系

拦截器主要负责config的更改配置,数据转化器主要负责转换请求体(data),比如请求前转换对象为字符串,

请求后把响应的字符串转换为对象。

如何cancel一个已经发出请求

使用


import axios from 'axios'

// 第一种取消方法
axios.get(url, {
  cancelToken: new axios.CancelToken(cancel => {
    if (/* 取消条件 */) {
      cancel('取消日志');
    }
  })
});

// 第二种取消方法
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get(url, {
  cancelToken: source.token
});
source.cancel('取消日志');

源码


// /cancel/CancelToken.js
function CancelToken(executor) {

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

// /lib/adapters/xhr.js
if (config.cancelToken) {
    config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
            return;
        }
        request.abort();
        reject(cancel);
        request = null;
    });
}

取消功能的核心是通过CancelToken内的this.promise = new Promise(resolve => resolvePromise = resolve), 得到实例属性promise,此时该promise的状态为pending 通过这个属性,在/lib/adapters/xhr.js文件中继续给这个promise实例添加.then方法 (xhr.js文件的159行config.cancelToken.promise.then(message => request.abort()));

CancelToken外界,通过executor参数拿到对cancel方法的控制权, 这样当执行cancel方法时就可以改变实例的promise属性的状态为rejected, 从而执行request.abort()方法达到取消请求的目的。

总结

这个项目最终成型的大小只有几十k,但是里面富含很多有意思的使用,比如对于promise的串联操作,请求前后各种数据处理等的流程控制,同时又支持浏览器和node环境。真心可以多读。

最后附上本人参考axios的实现,重新造了个axios,也算是加深对axios的理解。

axios-relize