读axios的源码,我学到了这些

1,512 阅读11分钟

1、前言

一个好的程序员一般都具备一个良好的习惯,如果觉得这个东西好吃,就一定会想办法去拆解它,从而具备自行生产的能力。往往在这个过程中,会见识到很多新奇的东西,然后再不禁感慨道:“wocao,还可以这样啊,666”,然后在将来某个项目里面运用从这儿学到的奇技淫巧,当被同事看到的时候,别人直呼“666”,然后心中充满成就感。哈哈哈,画面感十足啊。

读完此文:

1、你将对axios的运行原理有清晰的认识

2、你将学会如何使用策略模式和模板方法模式;

3、你将学会如何使用发布订阅模式;

4、你将对Promise.prototype.then()方法有更深刻的认识;

如果亲爱的读者读完此文还搞不懂,别急,你目前的功力尚欠,可以先点赞收藏,等将来你打怪升级以后再回顾本文,你就能理解了,哈哈哈。

本文基于axios@0.21.1的版本进行阐述,解读axios中应用的一些优秀的设计模式和编程技巧。 首先安装axios到我们的项目目录中:

$ npm install axios@0.21.1 -S

安装成功后,我们通过VSCode打开axios的源码开始分析。 axios的目录如图(本文不会逐文件阐述,将会挑重点文件进行阐述):

axios.png

dist目录是axios使用构建工具输出的UMD形式的文件,一般我们只会在非打包环境下引入,入口文件是index.js,这个文件引入了lib文件目录下的axios.js文件,因此我们的目光将主要聚焦在lib目录并开始分析。

2、核心模块

2.1、axios.js

在这个文件中,引入了axios的核心构造函数Axios,引入了axios的一些默认配置

'use strict';
// 已省略非关键代码
var Axios = require('./core/Axios');
var mergeConfig = require('./core/mergeConfig');
// 默认配置将会在后面章节介绍
var defaults = require('./defaults');
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);
  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);
  // Copy context to instance
  utils.extend(instance, context);
  return instance;
}
// 创建Axios的实例,这就是我们能够直接使用axios进行请求的原因。
var axios = createInstance(defaults);
// 暴露Axios的构造函数,可以供外界继承
axios.Axios = Axios;
// 暴露Axios的createAPI
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
// 暴露CancelToken,是用来执行请求可取消的接口
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
module.exports = axios;
// 对ES6模块的支持
module.exports.default = axios;

2.2、Axios的构造函数

axios的构造函数位于lib/core/Axios.js中,我们顺着axios.js进入到这个文件中。

'use strict';
// 已省略非关键代码
var utils = require('./../utils');
var buildURL = require('../helpers/buildURL');
var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');
var mergeConfig = require('./mergeConfig');
/**
 * Create a new instance of Axios
 */
function Axios(instanceConfig) {
  // 定义默认配置 
  this.defaults = instanceConfig;
  // 定义自身内部的请求和响应拦截器
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
/**
 * Axios的核心请求方法
 */
Axios.prototype.request = function request(config) {
  /*
   * 处理参数,使得axios即可以支持fetch API的调用形式,又可以支持axios({...params})的调用形式
   */ 
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }
  config = mergeConfig(this.defaults, config);
  // 定义请求方式,如果没有设置则默认GET
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }
  // 管道处理中间件,这儿也是我觉得比较经典的一处处理,我们即将阐述dispatchRequest
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    // 将request拦截器全部插入在管道的前端
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    // 将response拦截器全部插入在管道的后端
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  // 应用管道中的所有拦截器  
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  return promise;
};
// 定义原型上的请求方法别名,使得其支持axios.get等类似的请求方式
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: (config || {}).data
    }));
  };
});
// 原理同上
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, data, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});
module.exports = Axios;

在这个文件中,我们需要关注几个点,首先是为什么axios可以支持axios({...params})或axios('url', {...params})这样的写法;其次是为什么axios可以支持axios.get(path, config)这样的写法;然后是axios是怎么样处理我们所配置的请求或响应拦截器的(这个点是这个文件的重中之重)。

首先,我们来回忆一下,我们写拦截器的场景

import store from '@/store'
import axios from 'axios';
axios.interceptors.request.use(    
    config => {        
        const token = store.state.token;        
        token && (config.headers.Authorization = token);        
        return config;    
    },    
    error => {        
        return Promise.error(error);    
})

然后我们再回到阮一峰老师的ES6入门Promise一节中对Promise.prototype.then()的阐述

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。

首先我们先看这个chain里面初始化的时候定义了[dispatchRequest,undefined],先来到core/dispatchRequest.js中,看看这个dispatchRequest是个什么东西。

'use strict';
var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
/**
 * Throws a `Cancel` if cancellation has been requested.
 */
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}
/**
 * Dispatch a request to the server using the configured adapter.
 */
module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);
  // Ensure headers exist
  config.headers = config.headers || {};
  // Transform request data
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );
  // Flatten headers
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers
  );
  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );
  // 真正干活儿的方法
  var adapter = config.adapter || defaults.adapter;
  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);
  });
};

dispatchRequest就是axios核心的内置的请求中间件,并且部署了内置的请求(正常我们的请求拦截器一般都做一些配置操作,因此我将它对请求的配置也视为一个请求拦截器)和响应拦截器而已,并在拦截器里面引入了throwIfCancellationRequested,这是我们的请求可以取消的基石,稍后我们在可取消Promise节将会详细介绍。 将request拦截器插入到这个内置中间件之前,然后将response拦截器插入到这个内置中间件之后执行。根据ES6 Promise.prototype.then()的定义,我们相当于部署了N个中间件,但并不立即触发,将会在上一个Promise的状态发生改变的时候触发,这儿的流程有点儿像多米诺骨牌,首先拿着被包裹的配置内容经过请求拦截器,然后部署dispatchRequest,然后再部署响应拦截器。当我们的dispatchRequest状态改变的时候,挨着走响应拦截器,最终经过一些列处理,成为用户最终拿到的response结果。

我相信,此时已经有同学已经高呼“666”了,哈哈哈,的确是这样啊,因为当我第一次看到这儿的时候也发出了感慨,真是厉害啊,原来我们对Promise的理解一点儿都不深刻,害。

3、默认配置

前两节阐述了那么多,我们基本上阐述了axios的主线处理流程,但是axios可以在极简的配置下正常运行。在这节,我们来瞅瞅axios为我们做了哪些默认配置。来到defaults.js中,

'use strict';

var utils = require('./utils');
var normalizeHeaderName = require('./helpers/normalizeHeaderName');

var DEFAULT_CONTENT_TYPE = {
  'Content-Type': 'application/x-www-form-urlencoded'
};

function setContentTypeIfUnset(headers, value) {
  if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
    headers['Content-Type'] = value;
  }
}

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}

var defaults = {
  adapter: getDefaultAdapter(),

  transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Accept');
    normalizeHeaderName(headers, 'Content-Type');
    // 支持了所有的文件流,浏览器只支持FormData和Blob
    if (utils.isFormData(data) ||
      utils.isArrayBuffer(data) ||
      utils.isBuffer(data) ||
      utils.isStream(data) ||
      utils.isFile(data) ||
      utils.isBlob(data)
    ) {
      return data;
    }
    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;
  }],
  // 使得前台的调用者可以直接拿到一个JSON对象
  transformResponse: [function transformResponse(data) {
    /*eslint no-param-reassign:0*/
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
  }],

  /**
   * A timeout in milliseconds to abort a request. If set to 0 (default) a
   * timeout is not created.
   */
  timeout: 0,

  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',

  maxContentLength: -1,
  maxBodyLength: -1,

  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  }
};
defaults.headers = {
  common: {
    'Accept': 'application/json, text/plain, */*'
  }
};
utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
  defaults.headers[method] = {};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});
module.exports = defaults;

在这儿,有一个关键的地方是是对于adaptor的获取,如果当前环境存在XMLHttpRequest,则说明是浏览器环境,如果存在process对象,则说明是nodejs环境,这儿是一个策略模式的具体应用。

我们在实际使用策略模式的时候,往往会和模板方法模式(符合里氏代换原则、开闭原则)一同使用,通常我们会在父类里面定义一系列的行为规范,然后在子类中分别基于各自的策略实现行为,因此,如果拿这个例子举例,可以写出如下伪代码:

//base-request.ts
export abstract class BaseRequest{ 
    // 具备共同抽象特征的方法
    abstract void abort();
    abstract void send();
    // ...省略其它方法
}
//http-request.ts
export class HttpRequest extends BaseRequest{ 
    // 根据自身特征实现抽象方法
    void abort() {
        console.log('http策略实现取消请求')
    }
    void send(){
        console.log('http策略实现发送请求')
    }
    // ...省略其它方法
}
//xhr-request.ts
export class XhrRequest extends BaseRequest{ 
    // 根据自身特征实现抽象方法
    void abort() {
        console.log('xhr策略实现取消请求')
    }
    void send(){
        console.log('xhr策略实现发送请求')
    }
    // ...省略其它方法
}

因此,我们在实际的编码过程中,应当注意对业务的共同特征进行抽象,这样更有助于我们编写出高内聚,低耦合的代码。

4、拦截器管理

axios对拦截器的管理位于libs/core/InterceptorManager.js中,在这儿其实是一个发布订阅模式的应用

'use strict';
var utils = require('./../utils');
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;
  }
};
/**
 * 通知订阅者
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};
module.exports = InterceptorManager;

axios并没有设计基于channel的发布订阅模式,如果我们要实现基于channel的发布订阅模式的话,可以基于上述代码进行改造:

function Manager() {
  // 持有订阅者
  this.handlers = {};
}
/**
 * 订阅消息 支持单次订阅
 */
Manager.prototype.subscribe = function(channel, handler, once =false) {
  if(!this.handlers[channel]) {
      this.handlers[channel] = [{
          handler,
          once
      }];
  } else {
      this.handlers[channel].push({
          handler,
          once,
      });
  }
};

/**
 * 取消订阅
 */
Manager.prototype.unsubscribe = function(channel) {
  if (this.handlers[channel]) {
    this.handlers[channel] = null;
  }
};

/**
 * 通知特征订阅者
 */
Manager.prototype.notify = function(channel, ...args) {
   var subscribers = this.handlers[channel];
   subscribers.forEach((ele) => {
       const { handler, once } = ele || {};
       typeof handler === 'function' && handler.apply(this, args);
       if(once) {
           ele.destroy = true;
       }
   })
   this.handlers[channel] = subscribers.filter(x => !x.destroy);
};

/**
 * 通知所有订阅者
 */
Manager.prototype.notifyAll = function(...args) {
   for(var channel in this.handlers) {
       if(this.handlers.hasOwnProperty(channel)) {
           var subscribers = this.handlers[channel];
           if(!Array.isArray(subscribers)) {
              continue;
           }
           subscribers.forEach(ele => {
               const { handler, once } = ele || {};
               typeof handler === 'function' && handler.apply(this, args);
               if(once) {
                   ele.destroy = true;
               }
           })
           this.handlers[channel] = subscribers.filter(x => !x.destroy)
       }
   }
};

5、可取消的Promise

这个特性是最吸引我的,也是促使我阅读axios源码的动力。当某个接口的请求时间过长,我不想请求了,我想取消,如果基于XMLHttpRequest编程,大家一定想的到调用XMLHttpRequest.prototyp.abort()方法,并且可以在XMLHttpRequest.prototyp.onabort()回调里面做一些处理,但是众所周知,当前的Promise是不可以取消的(一旦new出来就不能取消),因此,我们接下来看看axios是如何实现“可取消的Promise”的。 首先是adaptors下面的xhr实现类,关键代码如下:

// 已省略部分非关键代码
module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;
    var requestHeaders = config.headers;
    
    var request = new XMLHttpRequest();
    var fullPath = buildFullPath(config.baseURL, config.url);
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    // Listen for ready state
    request.onreadystatechange = function handleLoad() {
      if (!request || request.readyState !== 4) {
        return;
      }
      if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
        return;
      }
    // Handle browser request cancellation (as opposed to a manual cancellation)
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }
      reject(createError('Request aborted', config, 'ECONNABORTED', request));
      // Clean up request
      request = null;
    };
    // Handle low level network errors
    request.onerror = function handleError() {
      reject(createError('Network Error', config, null, request));
      // Clean up request
      request = null;
    };
    // 如果用户配置了CancelToken,在此注册异步取消操作,当用户触发取消以后,调用xhr的abort方法取消xhr,并做一些清理工作
    if (config.cancelToken) {
      // Handle cancellation
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }
        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
      });
    }
    // Send the request
    request.send(requestData);
  });
};

找到定义可取消行为的关键代码,这部分代码位于cancel目录下,其中Cancel.js和isCancel比较简单,大家也一看就明白,此处不讨论。我们主要来讨论一下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);
  });
}

我们回忆一下axios的CancelToken的使用方式:

const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
    cancel = c;
  })
});
// cancel the request
cancel('我不想搞了');

可以看到,我们外部调用的cancel方法其实就是在执行上述的resolvePromise()方法,而resolvePromise指向的是这个内部promise的resolve方法; 而我们在xhr里面设置的异步回调执行监听的这个promise状态的改变立即调用xhr的取消方法,至此,我们搞懂了为什么axios的“Promise能取消”

其实axios实现的“可取消Promise”并没有真正的可取消,在xhr.js中我们可以看到,如果用户执行了取消操作的话,只是让Promise走的reject态而已。

现在再回到最开始我们在核心模块说过的一个方法叫throwIfCancellationRequested,axios在自己的默认拦截器中调用了它,它就是用来确保用户如果在请求之前就取消的话,直接走reject态。

最后再看看一看我们刚才忽略的一段代码:

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

axios的作者可能考虑到之前的写法比较复杂,于是为我们实现了一个默认的供懒人使用吧,哈哈哈。

6、总结

本文仅仅挑选了一些比较核心的模块进行了分析,这只是axios的冰山一角,有兴趣的朋友可以进行跟详细的阅读(尤其是可探究一下axios如何发送http请求的),相信您一定可以学到很多意想不到的知识呢。

从axios的API设计中,我们可以看出作者为了简化我们的调用付出了相当的努力,各位读者平时在设计API的时候务必要考虑调用者的使用体验,我在开发中一直追求的信念是“恶心自己(在设计API的时候支持更简单的调用方式,做更多的默认设置),成全别人(尽量减少信息的传递,降低使用者阅读API的时间消耗,让使用者可以傻瓜式的调用接口)”,哈哈哈。

读完此文,你对axios的整个工作流程应该已经有了清晰的认识吧;你对Promise.prototype.then()一定又有了新的认识吧;你对发布订阅模式认识也又更加深刻了吧;你对策略模式和模板方法模式的使用认识也更深刻了吧;愿每个努力的人都不会被辜负。

由于笔者水平有限,写作过程中难免出现错误,若有纰漏,请各位读者指正,请联系作者本人,邮箱404189928@qq.com,你们的意见将会帮助我更好的进步。本文乃作者原创,若转载请联系作者本人。