Axios源码分析

769 阅读8分钟

目录结构

│
├ lib/
│ ├ adapters/
│ ├ cancel/
│ ├ core/
│ ├ helpers/
│ ├ axios.js
│ ├ defaults.js     // 默认配置
│ └ utils.js
│
├ index.js // 入口

从入口开始

从入口我们可以知道axios提供了些什么

1. 从webpack.config得知入口文件是index.js

// index.js

module.exports = require('./lib/axios');

从这里我们知道库的入口文件是lib/axios.js

2. lib/axios.js导出了什么

// lib/axios.js

module.exports = axios;

module.exports.default = axios;

导出了 "axios" 这个对象,并且还有兼容import写法

3. 导出的axios是啥

var defaults = require('./defaults');

function createInstance(defaultConfig) {
  // ...
}

var axios = createInstance(defaults);

这里可以看出axios是一个实例,并且使用了默认参数。

4. 导出的axios提供了啥

a. axios.Axios

// lib/axios.js
axios.Axios = Axios;

这里的Axios是该实例的类

b. axios.create

// lib/axios.js
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

axios还提供了给用户自己创建实例的方法,并且用户传入的config能覆盖默认config(mergeConfig的逻辑)

c. axios.Cancelaxios.CancelTokenaxios.isCancel

// lib/axios.js
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

axios提供了取消请求的方法

d. axios.all

// lib/axios.js
axios.all = function all(promises) {
  return Promise.all(promises);
};

Promise.all的封装

e. axios.spread

// lib/axios.js
axios.spread = require('./helpers/spread');

参数解构的封装,类似es6数组的...操作符

Axios类

从入口来看,我们发现了重点方法axios.create,并且知道它是用方法createInstance创建实例的。

// lib/axios.js

var Axios = require('./core/Axios');

function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  // ...
}

这里用到了Axios类,并且它是整个库的核心,所以,接下来我们看看这个类是怎么定义的。

1. 构造器

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

构造器做了两件事:i.初始化配置、ii.初始化拦截器。

2. 原型链方法

a. Axios.prototype.request

发起一个请求

发送请求,以及对请求的处理,这部分我们放在下一节详细分析。

b. Axios.prototype.getUri

获取请求完整地址

c. Axios.prototype.get,Axios.prototype.post...

请求的别名

  • 语法糖,使得请求调用更加的语义化,更加的方便,其实现是基于Axios.prototype.request
  • 对于delete, get, head, options
function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
}
  • 对于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
    }));
  };
}

3. Axios.prototype.request

这里是axios的核心实现,包含了配置合并,请求发送,拦截器加入

// core/Axios.js
Axios.prototype.request = function request(config) {
    if (typeof config === 'string') {
        config = arguments[1] || {};
        config.url = arguments[0];
    } else {
        config = config || {};
    }
    // ...
}

对参数约定进行判断,使得调用的时候可以更灵活。 比如:axios.request('api.example.com', config)axios.request(config)

// core/Axios.js
config = mergeConfig(this.defaults, config);
config.method = config.method ? config.method.toLowerCase() : 'get';

合并配置,并且对方法有小写处理,所以config传入不用在意大小写。

重点来了,这里不仅处理了请求,还用了一个很巧妙的方法去处理请求和拦截器,以及拦截器之间的先后顺序。

  // Hook up interceptors middleware
  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;

首先执行到循环前我们看看chain是啥结构

[请求拦截1成功,请求拦截1失败,...,dispatchRequest, undefined,响应拦截1成功,响应拦截1失败,...]

入口通过Promise.resolve(config)将配置传入,在经历请求拦截器处理后,发起请求,请求成功获得相应,再依次经历响应拦截器处理。

dispatchRequest

发起请求的主要方法

处理URL
// core/dispatchRequest.js

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

区分相对路径和绝对路径

处理请求的data
// core/dispatchRequest.js

// Transform request data
config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
);

处理PUT, POST, PATCH传入的data

处理Headers
// core/dispatchRequest.js

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

这里的config已经是合并以及处理过的,这里分了三个headers

  1. 默认的通用headers
  2. 请求方法对应的headers
  3. 用户传入的headers

优先级依次递增,将这三个headers合并平铺到config.headers

紧接着删除config.headers下非http header的属性

// core/dispatchRequest.js
utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
);
★请求适配器★

在不同环境下,发起请求的方式可能会不一样,比如:在Node环境下发起请求可能是基于http模块,而在浏览器环境下发起请求又可能是基于XMLHttpRequest对象

但是适配器的存在,创建了一个通用的接口,它有通用的输入输出。即不用管内部实现的差异,只需要按照约定输入,以及处理约定的输出即可。

默认适配器:

// core/dispatchRequest.js
var defaults = require('../defaults');

var adapter = config.adapter || defaults.adapter;
// defaults.js

function getDefaultAdapter() {
  var adapter;
  // Only Node.JS has a process variable that is of [[Class]] process
  if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  } else if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  }
  return adapter;
}

var defaults = {
    adapter: getDefaultAdapter()
    // ...
}

axios内置了两个适配器adapters/http,adapters/xhr,在初始化默认配置时,它判断了当前环境,并且应用了相应的适配器。

当然你也可以传入自己的适配器,并且会被优先使用。适配器可以应用于小程序开发等有自己的请求方法的场景。之后我们再来看看如何新建一个自定义适配器。

接下来是处理请求适配器的返回

// core/dispatchRequest.js

return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);

    // Transform response data
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
}, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);

      // Transform response data
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
});

这里就是简单的处理了成功和失败情况,格式化数据。

这里的transformData做的事就是将用户传入的config.transformResponse全部执行一遍

throwIfCancellationRequested后面再细讲。

4. InterceptorManager

拦截管理器,拦截器的增删,遍历

构造器

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

this.handlers存的就是所有拦截器

InterceptorManager.prototype.use

// core/InterceptorManager.js

InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

新增一个拦截器并且返回拦截器序号(id)

InterceptorManager.prototype.eject

// core/InterceptorManager.js

InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

根据序号(id)删除一个拦截器

InterceptorManager.prototype.forEach

// core/InterceptorManager.js

InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

遍历拦截器

使用

  1. Axios的构造函数中,初始化了两个拦截管理器
// core/Axios.js

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

对于每一个Axios实例,都可以访问自己的两个拦截管理器实例,对其进行增删

  1. Axios.prototype.request方法中,将拦截器全部丢到队列里执行。

请求取消

首先我们举个例子来看看如何取消请求

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // An executor function receives a cancel function as a parameter
    cancel = c;
  })
});

// cancel the request
cancel();

lib/axios.js

// lib/axios.js
axios.CancelToken = require('./cancel/CancelToken');

接下来我们看看CancelToken类做了什么

CancelToken类

1. 构造器

// cancel/CancelToken.js
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) {
      // Cancellation has already been requested
      return;
    }

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

构造器中用到了两个属性
cancelToken.promise
cancelToken.reason

构造器接收一个参数executor,并且在构造器最后执行,传入一个cancel function作为参数

即在例子中传入的config

{
  cancelToken: new CancelToken(function executor(c) {
    // An executor function receives a cancel function as a parameter
    cancel = c;
}

这里的c引用的就是那个cancel function

这个cancel function可以随时调用,并且在调用后,会将cancelToken.promisereslove掉,这有什么用吗你可能会问。

// adapters/xhr.js

var request = new XMLHttpRequest();

// ...

config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }

    request.abort();
    // ...
});

当调用cancel function的时候,会终止掉请求,这就是cancel的实现原理

2. CancelToken.source

// cancel/CancelToken.js

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

静态方法source,自动新建cancelToken实例,并且将cancel function绑定返回

3. CancelToken.prototype.throwIfRequested

实例方法

// cancel/CancelToken.js

CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

当取消时,抛错

Axios.prototype.requestdispatchRequest

// core/dispathRequest.js

function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);
  // ...
  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);
    // ...
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);
      // ...
    }
  });
}

在请求前后都调用了throwIfCancellationRequested方法,在请求前取消不会发起请求,在请求后取消导致reject

Cancel类

1. 构造器

function Cancel(message) {
  this.message = message;
}

2. 重写了toString方法

3. Cancel.prototype.__CANCEL__

默认为true

4. 使用

// cancel/CancelToken.js

token.reason = new Cancel(message);

token.reasonCancel的实例,它表示了cancelToken的状态。而验证状态是由接下来这个方法实现的。

isCancel方法

module.exports = function isCancel(value) {
  return !!(value && value.__CANCEL__);
};

xhrAdapter

浏览器端的请求适配器

// adapters/xhr.js

module.exports = function xhrAdapter(config) {
    return new Promise(function dispatchXhrRequest() {})
})

接收配置config作为参数,返回一个promise

dispatchXhrRequest就是ajax的封装,来看看他是怎么封装的

  1. 首先,最基础的一个请求建立,发送流程
// adapters/xhr.js

var requestData = config.data;
var requestHeaders = config.headers;
var request = new XMLHttpRequest();
// ...
request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true);
// ...
request.onreadystatechange = function handleLoad() {}
// ...
// 设置headers
if ('setRequestHeader' in request) {
      utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
          // Remove Content-Type if data is undefined
          delete requestHeaders[key];
        } else {
          // Otherwise add header to the request
          request.setRequestHeader(key, val);
        }
      });
}
// ...
if (requestData === undefined) {
      requestData = null;
}
request.send(requestData);
  1. 设置超时
// adapters/xhr.js

request.timeout = config.timeout;
// ...
request.ontimeout = function handleTimeout() {
      reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED',
        request));

      // Clean up request
      request = null;
};
// ...
  1. 上传下载进度
// adapters/xhr.js

if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', config.onDownloadProgress);
}

if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', config.onUploadProgress);
}
// ...
  1. 取消请求,之前已经提及过
// adapters/xhr.js

if (config.cancelToken) {
    // ...
}
  1. 处理错误
request.onerror = function handleError() {
      // Real errors are hidden from us by the browser
      // onerror should only fire if it's a network error
      reject(createError('Network Error', config, null, request));

      // Clean up request
      request = null;
};
  1. 处理中断
request.onabort = function handleAbort() {
      if (!request) {
        return;
      }

      reject(createError('Request aborted', config, 'ECONNABORTED', request));

      // Clean up request
      request = null;
};

由于在手动触发cancel时有reject,因此这里判断当没有request的时候不重复reject

wxAdapter

在处理非预设环境时,可以自定义适配器

import axios from 'axios'

const wxrequest = axios.create({
  adapter: function (config) {
    return new Promise(function (resolve, reject) {
      var response = {
        statusText: '',
        config: config
      }

      var request = wx.request({
        url: config.url,
        data: config.data,
        method: config.method.toUpperCase(),
        header: config.headers,
        responseType: config.responseType,
        success(res) {
          response.data = res.data
          response.status = res.statusCode
          response.headers = res.headers
          resolve(response)
        },
        fail(err) {
          reject(err)
          request = null
        }
      })

      response.request = request

      if (config.cancelToken) {
        config.cancelToken.promise.then(function onCanceled(cancel) {
          if (!request) {
            return
          }

          request.abort()
          reject(cancel)
          request = null
        })
      }
    })
  }
})

export default wxrequest

在适配器中加入取消功能,格式化返回数据

总结

拦截器和适配器的设计,使得axios十分的灵活,更易扩展,其实现方式也值得学习使用。