(建议收藏)仔细研读,还不懂Axios原理的话组团来打我

·  阅读 61

Axios是一款基于Promise的http请求库,小巧玲珑,目前github上stars 82K,支持运行在浏览器端和Nodejs中。能够常年霸占榜位,其源码还是非常值得分析分析的。

写在开头

本文是基于源码 V0.21.1版本分析,详细介绍Axios生成实例的过程、构造函数、拦截器、取消等功能,每个小节都会从如何使用到源码逐步解剖。

在分析源码之前,我们先思考几个问题,如果让你写源码,会如何实现?

  1. 为何在浏览器端、Nodejs中都可以使用Axios·? 
  2. Axios拦截器是如何实现的? 
  3. 每个开源项目都有自己的工具库,Axios工具库里是否会出现让你拍大腿的实现方式? 
  4. Axios是怎么实现取消功能的?
  5. Axios从请求到结束,都经过了哪些流程,有哪些实现的很巧妙的方式值得学习的? 
  6. 从使用者的角度看,Axios使用方式有 axios({config})、axios.post()、axios.get() 等等。axios很像一个Object对象,又好像是一个Function,但到底是什么类型的呢?源代码是怎么支持这些请求方式的呢?
├── /dist/                     # build输出目录,支持直接script引用 
├── /lib/                      # 项目源码目录
│ ├── /adapters/               # 适配浏览器、nodejs请求
│ │ ├── http.js                # nodejs请求核心
│ │ └── xhr.js                 # 浏览器请求核心
│ ├── /cancel/                 # 实现取消请求的一系列功能
│ ├── /core/                   # 核心功能
│ │ ├── Axios.js               # axios的核心类
│ │ ├── dispatchRequest.js     # 用来调用http请求适配器方法发送请求
│ │ ├── InterceptorManager.js  # 拦截器构造函数
│ │ └── settle.js              # 根据http响应状态,改变Promise的状态
│ ├── /helpers/                # 一些辅助方法
│ ├── axios.js                 # 总入口,生成实例,对外暴露接口
│ ├── defaults.js              # 库默认配置 
│ └── utils.js                 # 公用方法库
├── package.json               # 项目信息
├── index.d.ts                 # 配置TypeScript的声明文件
└── index.js                   # 入口文件
复制代码

先举个栗子

在异步请求前先调用指定方法,请求后也调用指定方法进行"过滤"

function myAxios(config){
    this.config = config;
    this.before = [];
    this.after = [];
}
myAxios.prototype.request = function(config){
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(config)
        },3000)
    })
}
复制代码

以上是一个简单的构造函数myAxios,原型链上有request方法。现在要求在request方法执行前,若before数组中有方法,则先执行,after中若有方法,则在request之后继续执行,该如何修改以上代码才能实现?答案其实就是Promise链式调用。不熟悉Promise的同学建议先看下阮老师的《ES6入门》。接下来我们修改下request方法

function myAxios(config){
    this.config = config;
    this.before = [];
    this.after = [];
}
myAxios.prototype.addBeforeFn = function(fn){
    this.before.push(fn)
}
myAxios.prototype.addAfterFn = function(fn){
    this.after.push(fn)
}
function dispatchRequest(config){
    //发送真正请求
    return new Promise(resolve => {
        console.log('发送真正异步请求')
        setTimeout(() => {
            resolve(config)
        },3000)
    })
}
myAxios.prototype.request = function(){
    let promise = Promise.resolve(this.config);
    let chain = [dispatchRequest];
    if(this.before.length) chain.unshift(this.before[0])
    if(this.after.length) chain.push(this.after[0])
    for(let i=0;i<chain.length;i++){
        promise = promise.then(chain[i])
    }
    return promise
}
复制代码

以上为改造后的方法:增加了往实例的属性before或者after添加属性的方法(注意:添加的方法必须有return返回)。原型链上request方法中声明一个chain属性数组,dispatchRequest为真正异步请求。拦截方法分别放入chain数组中dispatchRequest属性前后,方便Promise按照顺序依次then

var instance = new myAxios({a:1});
instance.addBeforeFn((config) => {
    console.log('真正请求前')
    return config
})
instance.addAfterFn((config) => {
    console.log('真正请求后')
    return config
})
instance.request().then(res => {
    console.log(res)
})
复制代码

执行后得到的输出是:

// 真正请求前
// 发送真正异步请求
// 真正请求后
// {a:1}
复制代码

这样可以在真正请求前后,对输入或者输出内容做一个拦截处理。当然,在实际应用过程中以上demo拥有很多瑕疵,但在讲解axios源码之前举这个例子,就是为了方便后面理解拦截器原理

工具方法篇

在深入了解axios源码之前,有必要先分析下util.js中的几个主要工具方法,了解清楚后才更加容易理解源码

1、手动写了一个bind方法,给某个方法指定上下文,也就是this的指向,生成一个新的方法,实现效果同原生Function.prototype.bind

module.exports = function bind(fn, thisArg) {
  return function wrap() {
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }
    return fn.apply(thisArg, args);
  };
};
复制代码

2、forEach(obj, fn)使用给定的方法遍历执行给定的数组或对象

function forEach(obj, fn) {
  //省略部分边界代码
  if (isArray(obj)) {
    //数组的处理方式
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    //对象的处理方式
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}
复制代码

3、merge(/* obj1, obj2, obj3, ... */)深度合并多个对象为一个新对象

function merge(/* obj1, obj2, obj3, ... */) {
  var result = {};
  function assignValue(val, key) {
    if (isPlainObject(result[key]) && isPlainObject(val)) {
      result[key] = merge(result[key], val);
    } else if (isPlainObject(val)) {
      result[key] = merge({}, val);
    } else if (isArray(val)) {
      result[key] = val.slice();
    } else {
      result[key] = val;
    }
  }

  for (var i = 0, l = arguments.length; i < l; i++) {
    forEach(arguments[i], assignValue);
  }
  return result;
}
复制代码

思考:如果工作中我们遇到合并多个对象为一个新对象的情况,会如何处理?

3、extend(a, b, thisArg)b对象中的方法或属性扩展到a中,并指定上下文

function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
    if (thisArg && typeof val === 'function') {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  });
  return a;
}
复制代码

评论:以上三个方法,虽然看起来很简单,但非常有意思,值得我们手动实现感受一下,相信会理解的更加深刻。

入口文件lib/axios.js

引入工具函数util、绑定函数bind、默认配置defaults、构造函数Axios、合并配置函数mergeConfig

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) {
  //生成一个实例对象context,包含属性defaults(默认配置)、interceptors(拦截器)
  var context = new Axios(defaultConfig);
  //bind方法在工具篇已有介绍
  //其实所有的请求,都是走的Axios.prototype.request方法
  //绑定返回一个新Function,指定this的指向为context,这也就是axios(config)可以使用的原因
  var instance = bind(Axios.prototype.request, context);
  //复制 Axios.prototype原型的方法到实例上,并绑定this的指向到context
  //这就是为什么可以使用axios.post、axios.get等别名的原因
  utils.extend(instance, Axios.prototype, context);
  //复制context的属性到实例instance上
  //这就是为啥实际使用的时候axios.defaults、axios.interceptors
  utils.extend(instance, context);
  //返回实例对象(其实是一个方法)
  return instance;
}
var axios = createInstance(defaults);
复制代码

暴露取消API等其他方法

//暴露核心类库
axios.Axios = Axios;
//暴露 工厂模式 创建实例,满足个性化用户需求
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
//* 以上两者一般使用较少 *//

// 导出 Cancel 和 CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
//其实就是Promise.all方法
axios.all = function all(promises) {
  return Promise.all(promises);
};
//导出 spread
axios.spread = require('./helpers/spread');
//导出错误捕获 isAxiosError
axios.isAxiosError = require('./helpers/isAxiosError');
复制代码

axios到底是何种类型以及暴露的方法、属性

其实从axios(config)可以调用就可以发现,axios是一个方法

Object.prototype.toString.call(axios)
// [object Function]
复制代码

测试一下axios本身暴露的方法以及属性

console.log(Object.keys(axios))
//['request', 'getUri', 'delete', 'get', 'head', 'options', 'post', 'put', 'patch', 'defaults', 'interceptors', 'Axios', 'create', 'Cancel', 'CancelToken', 'isCancel', 'all', 'spread', 'default']
复制代码

核心类库lib/Axios.js

1、引入工具方法、拦截器、实际发送请求方法等

var utils = require('./../utils');
var buildURL = require('../helpers/buildURL');
var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');
var mergeConfig = require('./mergeConfig');
复制代码

2、核心构造函数Axios(拦截器方法InterceptorManager我们稍后分析)

function Axios(instanceConfig) {
  //defaults参数保存默认配置
  this.defaults = instanceConfig;
  //创建请求拦截器、相应拦截器
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
复制代码

3、核心请求方法。不论使用axiosaxios.post、还是axios.get等方式请求,最终实际请求的方法都是原型链上的request方法。

总结:request方法中真正的请求,其实就是dispatchRequest真正请求接口,其他的一些操作,其实是为了实现请求前后的拦截,应用原理其实就是Promise.then()链式调用。若你已看懂前面例子,这里理解起来将会更加容易。

Axios.prototype.request = function request(config) {
  //忽略一些边界、异常情况处理代码
    
  //创建一个数组,成对保存promise回调方法(成对出现,一个是成功,一个是失败,这也是初始化数组的时候,第二个参数要设置为undefined的原因)
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);
  //遍历执行,将请求拦截方法添加到数组chain前部
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  //遍历执行,将响应拦截方法添加到数组chain尾部
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  //遍历数组,组成Promise链式格式
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  return promise;
};
复制代码

request方法最终返回的是一个Promise链式调用,最终返回的格式大概如下

//返回的格式大概是 
  Promise.resolve(config).then(config => {
    //请求前拦截
  }).then(config => {
    //真正请求
  }).then(config => {
    //请求后相应拦截
  })
复制代码

获取URL请求的函数(个人没太理解这个函数用处多大,懂的小伙伴可指教指教)

Axios.prototype.getUri = function getUri(config) {
  config = mergeConfig(this.defaults, config);
  return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, '');
};
复制代码

Axios原型链上遍历添加别名方法deletegetheadoptionspostputpatch。在生成实例的方法中,Axios原型链的方法都复制拷贝作为axios属性方法,所以可以使用axios.postaxios.get等方式请求

utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  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) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});
复制代码

拦截器构造函数lib/cor/InterceptorManager.js

1、如何使用?
拦截器分为请求前拦截器,请求后拦截器

//请求前拦截器使用方法
axios.interceptors.request.use(config => {
  return config
},err => {
  Promise.reject(err)
})

//请求后拦截器
axios.interceptors.response.use(response => {
  return response
},err => {
  
})
复制代码

2、源码分析

// 增加拦截器操作数组,保存拦截函数
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;
  }
};
//遍历执行所有拦截器,传递一个函数调用。若是null,则不遍历
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};
复制代码

一般来说,添加函数use经常用,后两者用的较少,了解原理即可。

默认配置lib/defaults.js

1、适配器。在设计模式中被称为适配器模式,以电压转换器为例,经常全球出差的人知道的,每个国家的电压不同,而我们的电器需要的电压是220V,有了转换器后,只管使用即可,至于输入多少V,则不需要关心

//若无ContentType,则设置默认值,这个很好理解
function setContentTypeIfUnset(headers, value) {
    if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
        headers['Content-Type'] = value;
    }
}

//请求适配器
//这里解释了axios同时支持浏览器以及nodejs环境的原因
//若是浏览器环境,则使用xhr.js 否则若是nodejs环境,则使用http.js
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;
}
复制代码

2、其他默认配置

var defaults = {
    //适配器
    adapter: getDefaultAdapter(),
    //请求转换器
    transformRequest: [function transformRequest(data, headers) {
        
    }],
    //请求后数据转换器
    transformResponse: [function transformResponse(data) {
        
    }],
    //默认超时时间为0,意味着无超时时间
    timeout: 0,

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

    maxContentLength: -1,
    maxBodyLength: -1,

    validateStatus: function validateStatus(status) {
        return status >= 200 && status < 300;
    }
};
复制代码

派发请求方法dispatchRequest

module.exports = function dispatchRequest(config) {
  //如果config中存在cancelToken,则throw 错误,从而使得Promise走向错误
  throwIfCancellationRequested(config);

  // 确保存在headers头
  config.headers = config.headers || {};

  // 转换请求参数
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  // 拍平头
  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) {
    //让请求返回Promise.reject(),从而达到取消请求的目的
    throwIfCancellationRequested(config);

    // 转换请求结果
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      //取消相关
      throwIfCancellationRequested(config);
      //转换响应数据
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });
};
复制代码

总结dispatchRequest主要做了些啥:除了一些边界处理之外,就是取消相关的API操作,真正请求前后的数据转换

浏览器原生XMLHttpRequest方法

位置:lib/adapters/xhr.js。在浏览器中真正的请求,都是会依赖并调用原生XMLHttpRequest方法去执行的,axios其实说到底也只是对XMLHttpRequest的封装,围绕请求做了一些边界处理,以及更好的开发体验而已,从而让开发者无需关心底层是如何请求的,只需要将请求参数、请求地址配置好坐等结果即可。

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    //忽略一些边界处理方式
    var request = new XMLHttpRequest();
    //获取请求全连接
    var fullPath = buildFullPath(config.baseURL, config.url);
    //配置请求
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    //设置超时时间
    request.timeout = config.timeout;
    //监听请求状态
    request.onreadystatechange = function handleLoad() {
      
    };
    // 处理浏览器请求取消(与手动取消相反
    request.onabort = function handleAbort() {
      
    };
    // 捕获请求过程中产生的错误
    request.onerror = function handleError() {
      
    };
    // 超时请求
    request.ontimeout = function handleTimeout() {
      
    };
    // 只有在标准浏览器中,才会添加可能的xsrf头。何为标准浏览器内,可移步util.js中查看isStandardBrowserEnv方法,相对简单,此处不再赘述
    if (utils.isStandardBrowserEnv()) {
      //添加 xsrf 头
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;
      if (xsrfValue) {
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }
    // 是否允许 跨站携带cookie
    if (!utils.isUndefined(config.withCredentials)) {
      request.withCredentials = !!config.withCredentials;
    }
    // 若配置中将config.responseType设置为true,则将responseType添加到请求
    if (config.responseType) {
      try {
        request.responseType = config.responseType;
      } catch (e) {
        
      }
    }
    // 若提前配置了onDownloadProgress,则监听处理进度
    if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', config.onDownloadProgress);
    }

    // Not all browsers support upload events
    if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', config.onUploadProgress);
    }
    //处理取消请求相关
    if (config.cancelToken) {
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }
        request.abort();
        reject(cancel);
        request = null;
      });
    }

    if (!requestData) {
      requestData = null;
    }
    // 发送请求
    request.send(requestData);
  });
};
复制代码

取消API如何使用以及源码分析

1、取消请求的原理,其实就是让Promise throw一个错误,从而阻断链式调用达到目的。取消示例:

const source = axios.CancelToken.source();
axios.post('/yourServerAddress', {
  cancelToken: source.token
}).catch(err => {
  if (axios.isCancel(err)) {
    console.log('请求取消,原因是:', err.message);
  }else{
   
  }
});
// 取消函数
source.cancel('因某种原因,请求被取消');

//* 或者这样取消(个人更推荐后者写法,这样显得直观,且更像一个整体,方便阅读) *//
axios.post('/yourServerAddress',{
  cancelToken:new axios.CancelToken(cancel => {
    if(/* 某种原因 */){
      //取消请求
    }
  })
})

复制代码

2、取消请求源码分析,位置:lib/cancel/CancelToken.js,关键代码:

function CancelToken(executor) {
  // ....
  //忽略一些边界处理方式
  
  //取消关键点:得到实例属性promise,此时请求状态为pendding中,而xhr.js中有这么一行关键代码,当发现存在cancelToken,则abort()原生XMLHttpRequest,且当前Promise.reject,中断链式调用
  
  //xhr.js中取消相关代码(乱入)
  if (config.cancelToken) {
    config.cancelToken.promise.then(function onCanceled(cancel) {
      request.abort();
      reject(cancel);
      request = null;
    });
  }
    
  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);
  });
}

//...忽略部分不太重要的代码

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

总结

至此,axios源码基本分析完毕。个人认为其中有这么几点非常值得学习,其想法值得借鉴到我们的项目中
1、工具方法之forEachmergeextend,短短几行代码,写的非常巧妙;
2、生成axios实例的时候,拷贝合并暴露原型链方法;
3、Axios.prototype.request中使用Promise链式调用实现的请求前后拦截;
4、原生方法XMLHttpRequest对请求的处理等一系列操作;
5、取消API的实现

分类:
前端
收藏成功!
已添加到「」, 点击更改