Axios源码深度剖析 - AJAX新王者

axios源码分析 - XHR篇javascript

文章源码托管在github上,欢迎fork指正!

axios 是一个基于 Promise 的http请求库,能够用在浏览器和node.js中,目前在github上有 42K 的star数java

备注:

  1. 每一小节都会从两个方面介绍:如何使用 -> 源码分析
  2. [工具方法简单介绍]一节可先跳过,后面用到了再过来查看
  3. axios最核心的技术点是如何拦截请求响应并修改请求参数修改响应数据axios是如何用promise搭起基于xhr的异步桥梁的

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                   # 入口文件

复制代码

注:由于咱们须要要看的代码都是/lib/目录下的文件,因此如下全部涉及到文件路径的地方, 咱们都会在/lib/下进行查找node

名词解释

  • 拦截器 interceptorsios

    (若是你熟悉中间件,那么就很好理解了,由于它起到的就是基于promise的中间件的做用)git

    拦截器分为请求拦截器和响应拦截器,顾名思义: 请求拦截器(interceptors.request)是指能够拦截住每次或指定http请求,并可修改配置项 响应拦截器(interceptors.response)能够在每次http请求后拦截住每次或指定http请求,并可修改返回结果项。程序员

    这里先简单说明,后面会作详细的介绍如何拦截请求响应并修改请求参数修改响应数据github

  • 数据转换器 (其实就是对数据进行转换,好比将对象转换为JSON字符串)json

    数据转换器分为请求转换器和响应转换器,顾名思义: 请求转换器(transformRequest)是指在请求前对数据进行转换, 响应转换器(transformResponse)主要对请求响应后的响应体作数据转换。axios

  • http请求适配器(其实就是一个方法)跨域

    在axios项目里,http请求适配器主要指两种:XHR、http。 XHR的核心是浏览器端的XMLHttpRequest对象, http核心是node的http[s].request方法

    固然,axios也留给了用户经过config自行配置适配器的接口的, 不过,通常状况下,这两种适配器就可以知足从浏览器端向服务端发请求或者从node的http客户端向服务端发请求的需求。

    本次分享主要围绕XHR。

  • config配置项 (其实就是一个对象)

    此处咱们说的config,在项目内不是真的都叫config这个变量名,这个名字是我根据它的用途起的一个名字,方便你们理解。

    在axios项目中的,设置\读取config时, 有的地方叫它defaults(/lib/defaults.js),这儿是默认配置项, 有的地方叫它config,如Axios.prototype.request的参数,再如xhrAdapterhttp请求适配器方法的参数。

    config在axios项目里的是很是重要的一条链,是用户跟axios项目内部“通讯”的主要桥梁。

axios内部的运做流程图

工具方法简单介绍

(注:本节可先跳过,后面用到了再过来查看)

有一些方法在项目中多处使用,简单介绍下这些方法

  1. bind: 给某个函数指定上下文,也就是this指向
bind(fn, context); 

复制代码

实现效果同Function.prototype.bind方法: fn.bind(context)

  1. forEach:遍历数组或对象
var utils = require('./utils');
var forEach = utils.forEach;

// 数组
utils.forEach([], (value, index, array) => {})

// 对象
utils.forEach({}, (value, key, object) => {})

复制代码
  1. merge:深度合并多个对象为一个对象
var utils = require('./utils');
var merge = utils.merge;

var obj1 = {
  a: 1,
  b: {
    bb: 11,
    bbb: 111,
  }
};
var obj2 = {
  a: 2,
  b: {
    bb: 22,
  }
};
var mergedObj = merge(obj1, obj2); 

复制代码

mergedObj对象是:

{ 
  a: 2, 
  b: { 
    bb: 22, 
    bbb: 111 
  } 
}

复制代码
  1. extend:将一个对象的方法和属性扩展到另一个对象上,并指定上下文
var utils = require('./utils');
var extend = utils.extend;

var context = {
  a: 4,
};
var target = {
  k: 'k1',
  fn(){
    console.log(this.a + 1)
  }
};
var source = {
  k: 'k2',
  fn(){
    console.log(this.a - 1)
  }
};
let extendObj = extend(target, source, context);

复制代码

extendObj对象是:

{
  k: 'k2',
  fn: source.fn.bind(context),
}

复制代码

执行extendObj.fn();, 打印3

axios为什么会有多种使用方式

如何使用

// 首先将axios包引进来
import axios from 'axios'

复制代码

第1种使用方式:axios(option)

axios({
  url,
  method,
  headers,
})

复制代码

第2种使用方式:axios(url[, option])

axios(url, {
  method,
  headers,
})

复制代码

第3种使用方式(对于get、delete等方法):axios[method](url[, option])

axios.get(url, {
  headers,
})

复制代码

第4种使用方式(对于post、put等方法):axios[method](url[, data[, option]])

axios.post(url, data, {
  headers,
})

复制代码

第5种使用方式:axios.request(option)

axios.request({
  url,
  method,
  headers,
})

复制代码

源码分析

做为axios项目的入口文件,咱们先来看下axios.js的源码 可以实现axios的多种使用方式的核心是createInstance方法:

// /lib/axios.js
function createInstance(defaultConfig) {
  // 建立一个Axios实例
  var context = new Axios(defaultConfig);

  // 如下代码也能够这样实现:var instance = Axios.prototype.request.bind(context);
  // 这样instance就指向了request方法,且上下文指向context,因此能够直接以 instance(option) 方式调用 
  // Axios.prototype.request 内对第一个参数的数据类型判断,使咱们可以以 instance(url, option) 方式调用
  var instance = bind(Axios.prototype.request, context);

  // 把Axios.prototype上的方法扩展到instance对象上,
  // 这样 instance 就有了 get、post、put等方法
  // 并指定上下文为context,这样执行Axios原型链上的方法时,this会指向context
  utils.extend(instance, Axios.prototype, context);

  // 把context对象上的自身属性和方法扩展到instance上
  // 注:由于extend内部使用的forEach方法对对象作for in 遍历时,只遍历对象自己的属性,而不会遍历原型链上的属性
  // 这样,instance 就有了 defaults、interceptors 属性。(这两个属性后面咱们会介绍)
  utils.extend(instance, context);

  return instance;
}

// 接收默认配置项做为参数(后面会介绍配置项),建立一个Axios实例,最终会被做为对象导出
var axios = createInstance(defaults);

复制代码

以上代码看上去很绕,其实createInstance最终是但愿拿到一个Function,这个Function指向Axios.prototype.request,这个Function还会有Axios.prototype上的每一个方法做为静态方法,且这些方法的上下文都是指向同一个对象。

那么在来看看Axios、Axios.prototype.request的源码是怎样的?

Axios是axios包的核心,一个Axios实例就是一个axios应用,其余方法都是对Axios内容的扩展 而Axios构造函数的核心方法是request方法,各类axios的调用方式最终都是经过request方法发请求的

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

Axios.prototype.request = function request(config) {
  // ...省略代码
};

// 为支持的请求方法提供别名
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
    }));
  };
});

复制代码

经过以上代码,咱们就能够以多种方式发起http请求了: axios()、axios.get()、axios.post()

通常状况,项目使用默认导出的axios实例就能够知足需求了, 若是不知足需求须要建立新的axios实例,axios包也预留了接口, 看下面的代码:

// /lib/axios.js - 31行
axios.Axios = Axios;
axios.create = function create(instanceConfig) {
  return createInstance(utils.merge(defaults, instanceConfig));
};

复制代码

说完axios为何会有这么多种使用方式,可能你心中会有一个疑问: 使用axios时,不管get方法仍是post方法,最终都是调用的Axios.prototype.request方法,那么这个方法是怎么根据咱们的config配置发请求的呢?

在开始说Axios.prototype.request以前,咱们先来捋一捋在axios项目中,用户配置的config是怎么起做用的?

用户配置的config是怎么起做用的

这里说的config,指的是贯穿整个项目的配置项对象, 经过这个对象,能够设置:

http请求适配器、请求地址、请求方法、请求头header、 请求数据、请求或响应数据的转换、请求进度、http状态码验证规则、超时、取消请求等

能够发现,几乎axios全部的功能都是经过这个对象进行配置和传递的, 既是axios项目内部的沟通桥梁,也是用户跟axios进行沟通的桥梁。

首先咱们看看,用户能以什么方式定义配置项:

import axios from 'axios'

// 第1种:直接修改Axios实例上defaults属性,主要用来设置通用配置
axios.defaults[configName] = value;

// 第2种:发起请求时最终会调用Axios.prototype.request方法,而后传入配置项,主要用来设置“个例”配置
axios({
  url,
  method,
  headers,
})

// 第3种:新建一个Axios实例,传入配置项,此处设置的是通用配置
let newAxiosInstance = axios.create({
  [configName]: value,
})

复制代码

看下 Axios.prototype.request 方法里的一行代码: (/lib/core/Axios.js - 第35行)

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

复制代码

能够发现此处将默认配置对象defaults/lib/defaults.js)、Axios实例属性this.defaultsrequest请求的参数config进行了合并。

由此得出,多处配置的优先级由低到高是: —> 默认配置对象defaults/lib/defaults.js)
—> { method: 'get' }
—> Axios实例属性this.defaults
—> request请求的参数config

留给你们思考一个问题: defaultsthis.defaults 何时配置是相同的,何时是不一样的?

至此,咱们已经获得了将多处merge后的config对象,那么这个对象在项目中又是怎样传递的呢?

Axios.prototype.request = function request(config) {
  // ...
  config = utils.merge(defaults, {method: 'get'}, this.defaults, config);

  var chain = [dispatchRequest, undefined];
  // 将config对象看成参数传给Primise.resolve方法
  var promise = Promise.resolve(config);

  // ...省略代码
  
  while (chain.length) {
    // config会按序经过 请求拦截器 - dispatchRequest方法 - 响应拦截器
    // 关于拦截器 和 dispatchRequest方法,下面会做为一个专门的小节来介绍。
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

复制代码

至此,config走完了它传奇的一辈子 -_- 下一节就要说到重头戏了: Axios.prototype.request

axios.prototype.request

这里面的代码比较复杂,一些方法须要追根溯源才能搞清楚, 因此只需对chain数组有个简单的了解就好,涉及到的拦截器、[dispatchRequest]后面都会详细介绍

chain数组是用来盛放拦截器方法和dispatchRequest方法的, 经过promise从chain数组里按序取出回调函数逐一执行,最后将处理后的新的promise在Axios.prototype.request方法里返回出去, 并将response或error传送出去,这就是Axios.prototype.request的使命了。

查看源码:

// /lib/core/Axios.js
Axios.prototype.request = function request(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;
};

复制代码

此时,你必定对拦截器充满了好奇,这个拦截器究竟是个什么家伙,下一节就让咱们一探究竟吧

如何拦截请求响应并修改请求参数修改响应数据

如何使用

// 添加请求拦截器
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);

复制代码

思考

  1. 是否能够直接 return error?
axios.interceptors.request.use(config => config, error => {
  // 是否能够直接 return error ?
  return Promise.reject(error); 
});

复制代码
  1. 如何实现promise的链式调用
new People('whr').sleep(3000).eat('apple').sleep(5000).eat('durian');

// 打印结果
// (等待3s)--> 'whr eat apple' -(等待5s)--> 'whr eat durian'

复制代码

源码分析

关于拦截器,名词解释一节已经作过简单说明。

每一个axios实例都有一个interceptors实例属性, interceptors对象上有两个属性requestresponse

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

复制代码

这两个属性都是一个InterceptorManager实例,而这个InterceptorManager构造函数就是用来管理拦截器的。

咱们先来看看InterceptorManager构造函数:

InterceptorManager构造函数就是用来实现拦截器的,这个构造函数原型上有3个方法:use、eject、forEach。 关于源码,实际上是比较简单的,都是用来操做该构造函数的handlers实例属性的。

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


复制代码

那么当咱们经过axios.interceptors.request.use添加拦截器后, axios内部又是怎么让这些拦截器可以在请求前、请求后拿到咱们想要的数据的呢?

先看下代码:

// /lib/core/Axios.js
Axios.prototype.request = function request(config) {
  // ...
  var chain = [dispatchRequest, undefined];

  // 初始化一个promise对象,状态微resolved,接收到的参数微config对象
  var promise = Promise.resolve(config);

  // 注意:interceptor.fulfilled 或 interceptor.rejected 是可能为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);
  });

  // 添加了拦截器后的chain数组大概会是这样的:
  // [
  // requestFulfilledFn, requestRejectedFn, ..., 
  // dispatchRequest, undefined,
  // responseFulfilledFn, responseRejectedFn, ....,
  // ]

  // 只要chain数组长度不为0,就一直执行while循环
  while (chain.length) {
    // 数组的 shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。
    // 每次执行while循环,从chain数组里按序取出两项,并分别做为promise.then方法的第一个和第二个参数

    // 按照咱们使用InterceptorManager.prototype.use添加拦截器的规则,正好每次添加的就是咱们经过InterceptorManager.prototype.use方法添加的成功和失败回调

    // 经过InterceptorManager.prototype.use往拦截器数组里添加拦截器时使用的数组的push方法,
    // 对于请求拦截器,从拦截器数组按序读到后是经过unshift方法往chain数组数里添加的,又经过shift方法从chain数组里取出的,因此得出结论:对于请求拦截器,先添加的拦截器会后执行
    // 对于响应拦截器,从拦截器数组按序读到后是经过push方法往chain数组里添加的,又经过shift方法从chain数组里取出的,因此得出结论:对于响应拦截器,添加的拦截器先执行

    // 第一个请求拦截器的fulfilled函数会接收到promise对象初始化时传入的config对象,而请求拦截器又规定用户写的fulfilled函数必须返回一个config对象,因此经过promise实现链式调用时,每一个请求拦截器的fulfilled函数都会接收到一个config对象

    // 第一个响应拦截器的fulfilled函数会接受到dispatchRequest(也就是咱们的请求方法)请求到的数据(也就是response对象),而响应拦截器又规定用户写的fulfilled函数必须返回一个response对象,因此经过promise实现链式调用时,每一个响应拦截器的fulfilled函数都会接收到一个response对象

    // 任何一个拦截器的抛出的错误,都会被下一个拦截器的rejected函数收到,因此dispatchRequest抛出的错误才会被响应拦截器接收到。

    // 由于axios是经过promise实现的链式调用,因此咱们能够在拦截器里进行异步操做,而拦截器的执行顺序仍是会按照咱们上面说的顺序执行,也就是 dispatchRequest 方法必定会等待全部的请求拦截器执行完后再开始执行,响应拦截器必定会等待 dispatchRequest 执行完后再开始执行。

    promise = promise.then(chain.shift(), chain.shift());

  }

  return promise;
};

复制代码

如今,你应该已经清楚了拦截器是怎么回事,以及拦截器是如何在Axios.prototype.request方法里发挥做用的了, 那么处于"中游位置"的dispatchRequest是如何发送http请求的呢?

dispatchrequest都作了哪些事

dispatchRequest主要作了3件事: 1,拿到config对象,对config进行传给http请求适配器前的最后处理; 2,http请求适配器根据config配置,发起请求 3,http请求适配器请求完成后,若是成功则根据header、data、和config.transformResponse(关于transformResponse,下面的数据转换器会进行讲解)拿到数据转换后的response,并return。

// /lib/core/dispatchRequest.js
module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  // Support baseURL config
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    config.url = combineURLs(config.baseURL, config.url);
  }

  // Ensure headers exist
  config.headers = config.headers || {};

  // 对请求data进行转换
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  // 对header进行合并处理
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );

  // 删除header属性里无用的属性
  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

  // http请求适配器会优先使用config上自定义的适配器,没有配置时才会使用默认的XHR或http适配器,不过大部分时候,axios提供的默认适配器是可以知足咱们的
  var adapter = config.adapter || defaults.adapter;

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

复制代码

好了,看到这里,咱们是时候梳理一下:axios是如何用promise搭起基于xhr的异步桥梁的?

axios是如何用promise搭起基于xhr的异步桥梁的

axios是如何经过Promise进行异步处理的?

如何使用

import axios from 'axios'

axios.get(/**/)
.then(data => {
  // 此处能够拿到向服务端请求回的数据
})
.catch(error => {
  // 此处能够拿到请求失败或取消或其余处理失败的错误对象
})

复制代码

源码分析

先来一个图简单的了解下axios项目里,http请求完成后到达用户的顺序流:

经过axios为什么会有多种使用方式咱们知道, 用户不管以什么方式调用axios,最终都是调用的Axios.prototype.request方法, 这个方法最终返回的是一个Promise对象。

Axios.prototype.request = function request(config) {
  // ...
  var chain = [dispatchRequest, undefined];
  // 将config对象看成参数传给Primise.resolve方法
  var promise = Promise.resolve(config);

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

复制代码

Axios.prototype.request方法会调用dispatchRequest方法,而dispatchRequest方法会调用xhrAdapter方法,xhrAdapter方法返回的是还一个Promise对象

// /lib/adapters/xhr.js
function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // ... 省略代码
  });
};

复制代码

xhrAdapter内的XHR发送请求成功后会执行这个Promise对象的resolve方法,并将请求的数据传出去, 反之则执行reject方法,并将错误信息做为参数传出去。

// /lib/adapters/xhr.js
var request = new XMLHttpRequest();
var loadEvent = 'onreadystatechange';

request[loadEvent] = function handleLoad() {
  // ...
  // 往下走有settle的源码
  settle(resolve, reject, response);
  // ...
};
request.onerror = function handleError() {
  reject(/**/);
  request = null;
};
request.ontimeout = function handleTimeout() {
  reject(/**/);
  request = null;
};

复制代码

验证服务端的返回结果是否经过验证:

// /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(/**/);
  }
};

复制代码

回到dispatchRequest方法内,首先获得xhrAdapter方法返回的Promise对象, 而后经过.then方法,对xhrAdapter返回的Promise对象的成功或失败结果再次加工, 成功的话,则将处理后的response返回, 失败的话,则返回一个状态为rejected的Promise对象,

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

复制代码

那么至此,用户调用axios()方法时,就能够直接调用Promise的.then.catch进行业务处理了。

回过头来,咱们在介绍dispatchRequest一节时说到的数据转换,而axios官方也将数据转换专门做为一个亮点来介绍的,那么数据转换到底能在使用axios发挥什么功效呢?

数据转换器-转换请求与响应数据

如何使用

  1. 修改全局的转换器
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;
}];

复制代码
  1. 修改某次axios请求的转换器
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;
    }
  ],
})

复制代码

源码分析

默认的defaults配置项里已经自定义了一个请求转换器和一个响应转换器, 看下源码:

// /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项目里,是在什么地方使用了转换器呢?

请求转换器的使用地方是http请求前,使用请求转换器对请求数据作处理, 而后传给http请求适配器使用。

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

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

复制代码

看下transformData方法的代码, 主要遍历转换器数组,分别执行每个转换器,根据data和headers参数,返回新的data。

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

复制代码

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

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

复制代码

转换器和拦截器的关系?

拦截器一样能够实现转换请求和响应数据的需求,但根据做者的设计和综合代码能够看出, 在请求时,拦截器主要负责修改config配置项,数据转换器主要负责转换请求体,好比转换对象为字符串 在请求响应后,拦截器能够拿到response,数据转换器主要负责处理响应体,好比转换字符串为对象。

axios官方是将"自动转换为JSON数据"做为一个独立的亮点来介绍的,那么数据转换器是如何完成这个功能的呢? 其实很是简单,咱们一块儿看下吧。

自动转换json数据

在默认状况下,axios将会自动的将传入的data对象序列化为JSON字符串,将响应数据中的JSON字符串转换为JavaScript对象

源码分析

// 请求时,将data数据转换为JSON 字符串
// /lib/defaults.js 
transformRequest: [function transformRequest(data, headers) {
  // ...
    if (utils.isObject(data)) {
      setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
      return JSON.stringify(data);
    }
    return data;
}]

// 获得响应后,将请求到的数据转换为JSON对象
// /lib/defaults.js
transformResponse: [function transformResponse(data) {
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
}]

复制代码

至此,axios项目的运做流程已经介绍完毕,是否是已经打通了任督二脉了呢 接下来咱们一块儿看下axios还带给了咱们哪些好用的技能点吧。

header设置

如何使用

import axios from 'axios'

// 设置通用header
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; // xhr标识

// 设置某种请求的header
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=utf-8';

// 设置某次请求的header
axios.get(url, {
  headers: {
    'Authorization': 'whr1',
  },
})

复制代码

源码分析

// /lib/core/dispatchRequest.js - 44行

  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );

复制代码

如何取消已经发送的请求

如何使用

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 - 11行
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 - 159行
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()方法达到取消请求的目的。

上面第二种写法能够看做是对第一种写法的完善, 由于不少是时候咱们取消请求的方法是用在本次请求方法外, 例如,发送A、B两个请求,当B请求成功后,取消A请求。

// 第1种写法:
let source;
axios.get(Aurl, {
  cancelToken: new axios.CancelToken(cancel => {
    source = cancel;
  })
});
axios.get(Burl)
.then(() => source('B请求成功了'));

// 第2种写法:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get(Aurl, {
  cancelToken: source.token
});
axios.get(Burl)
.then(() => source.cancel('B请求成功了'));

复制代码

相对来讲,我更推崇第1种写法,由于第2种写法太隐蔽了,不如第一种直观好理解。

发现的问题
  1. /lib/adapters/xhr.js文件中,onCanceled方法的参数不该该叫message么,为何叫cancel?

  2. /lib/adapters/xhr.js文件中,onCanceled方法里,reject里应该将config信息也传出来

跨域携带cookie

如何使用

import axios from 'axios'

axios.defaults.withCredentials = true;

复制代码

源码分析

咱们在用户配置的config是怎么起做用的一节已经介绍了config在axios项目里的传递过程, 由此得出,咱们经过axios.defaults.withCredentials = true作的配置, 在/lib/adapters/xhr.js里是能够取到的,而后经过如下代码配置到xhr对象项。

var request = new XMLHttpRequest();

// /lib/adapters/xhr.js
if (config.withCredentials) {
  request.withCredentials = true;
}

复制代码

超时配置及处理

如何使用

import axios from 'axios'

axios.defaults.timeout = 3000;

复制代码

源码分析

// /adapters/xhr.js
request.timeout = config.timeout;

// /adapters/xhr.js
// 经过createError方法,将错误信息合为一个字符串
request.ontimeout = function handleTimeout() {
  reject(createError('timeout of ' + config.timeout + 'ms exceeded', 
    config, 'ECONNABORTED', request));
};

复制代码
  • axios库外如何添加超时后的处理
axios().catch(error => {
  const { message } = error;
  if (message.indexOf('timeout') > -1){
    // 超时处理
  }
})

复制代码

改写验证成功或失败的规则validatestatus

自定义http状态码的成功、失败范围

如何使用

import axios from 'axios'

axios.defaults.validateStatus = status => status >= 200 && status < 300;

复制代码

源码分析

在默认配置中,定义了默认的http状态码验证规则, 因此自定义validateStatus实际上是对此处方法的重写

// `/lib/defaults.js`
var defaults = {
  // ...
  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  },
  // ...
}

复制代码

axios是什么时候开始验证http状态码的?

// /lib/adapters/xhr.js
var request = new XMLHttpRequest();
var loadEvent = 'onreadystatechange';

// /lib/adapters/xhr.js
// 每当 readyState 改变时,就会触发 onreadystatechange 事件
request[loadEvent] = function handleLoad() {
  if (!request || (request.readyState !== 4 && !xDomain)) {
    return;
  }
  // ...省略代码
  var response = {
      // ...
      // IE sends 1223 instead of 204 (https://github.com/axios/axios/issues/201)
      status: request.status === 1223 ? 204 : request.status,
      config: config,
  };
  settle(resolve, reject, response);
  // ...省略代码
}

复制代码
// /lib/core/settle.js
function settle(resolve, reject, response) {
  // 若是咱们往上捣一捣就会发现,config对象的validateStatus就是咱们自定义的validateStatus方法或默认的validateStatus方法
  var validateStatus = response.config.validateStatus;
  // validateStatus验证经过,就会触发resolve方法
  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
    ));
  }
};

复制代码

总结

axios这个项目里,有不少对JS使用很巧妙的地方,好比对promise的串联操做(固然你也能够说这块是借鉴不少异步中间件的处理方式),让咱们能够很方便对请求先后的各类处理方法的流程进行控制;不少实用的小优化,好比请求先后的数据处理,省了程序员一遍一遍去写JSON.xxx了;同时支持了浏览器和node两种环境,对使用node的项目来讲无疑是极好的。

总之,这个可以在github斩获42K+(截止2018.05.27)的star,实力毫不是盖的,值得好好交交心!

相关文章
相关标签/搜索