axios源码学习到使用

0、写在前面

先掌握源码结构再到实际的运行使用中去复盘源码。就是 源码—>使用—>源码 的学习线路。
思维导图配合文章更清晰


axios.png
0.1 取源码

源码取到打开已经打包好的文件 dist/axios.js 看一下,注释加上空行也就两千行不到。


git clone https://github.com/axios/axios.git
0.2 入口文件

打开package.json找到入口为index.js



index.js

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

主要看的就是红框里这部分内容了。


由上至下大致分析一下文件及文件夹的主要内容。
adapter:适配器,浏览器环境用xhr,node环境用http
cancel:取消请求
core:核心代码
helpers:功能助手
axios.js:声明定义文件
default.js:默认的配置参数
utils.js:一些小工具函数集合
下面我们就从定义文件开始看起。

1、axios.js

1.1 直接找到声明语句开始
var axios = createInstance(defaults);

1.1.1 看传入的默认配置 defaults 定义了哪些东西
adapter:适配器根据运行环境选择哪种请求方式(浏览器用xhr—lib/adapters/xhr.js,node环境用http—lib/adapters/http.js)
transformRequest:请求数据处理方法数组
transformResponse:响应数据处理方法数组
timeout:响应最长返回时间,超过这个时间就取消请求
xsrfCookieName:xsrf在cookie内名称
xsrfHeaderName:xsrf在HTTP头内名称
maxContentLength:允许的响应内容的最大尺寸
maxBodyLength:(仅在node中生效)允许发送HTTP请求内容的最大尺寸
validateStatus:是否 2** 类型的状态码
headers:定义了共用的请求头 common 和各种不同请求方法的请求头(用了两个循环完成)

再看 function createInstance(defaultConfig) 里面的语句,也就5行代码,一行一行看。

1.1.2 返回 Axios 对象

 var context = new Axios(defaultConfig);
  • 打开 core/Axios.js 查看 Axios 所拥有的属性方法:
    defaults:刚刚的默认配置赋值;
    interceptors:拦截器,包括 request、response 两个对象;
    request:发送的请求处理,里面主要处理有合并默认配置和自定义配置,确认请求方法,拦截器处理,最后循环执行拦截器至promise对象并返回该对象;
    getUri:获取请求的完整地址;
    两个forEach:循环定义HTTP请求方法(delete、get、head、options、post、put、patch),注意前四个与后三个传参的不同 function(url, config) 和 function(url, data, config)。

  • 另外再看一下拦截器 interceptors 所使用的对象,在 InterceptorManager.js中:
    handlers:拦截处理数组;
    use:增加一个拦截器;
    eject:移除一个拦截器;
    forEach:遍历处理,将有效的拦截器绑定上处理方法(这个可以在 Axios 对象方法 request 里看拦截器处理那段)。

1.1.3 定义返回 wrap 函数

var instance = bind(Axios.prototype.request, context);

打开 helpers/bind.js 查看 bind 函数,得到这里 instance 是被定义成了一个 wrap 函数,该函数返回值是Axios.prototype.request 方法调用结果,并且 request 内部this指向传入的值 context,传入参数为 args ,另外在前面我们看到这个 request 结果是返回 promise 类型对象的。

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

1.1.4 这是将 Axios.prototype 定义的 request、getUri以及delete、get、head、options、post、put、patch 都以返回 wrap 函数的方式复制到 intance 上,形同上面的 bind(Axios.prototype.request, context)

utils.extend(instance, Axios.prototype, context);

1.1.5 将创建的 context 对象扩展复制到 instance 上

utils.extend(instance, context);

1.1.6 返回 instance ,即 wrap 函数赋值给变量 axios

return instance;
1.2 将 Axios 暴露出来,允许继承
axios.Axios = Axios;
1.3 实例函数工厂,用这个可以建立自己的默认配置和拦截器等的实例
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
1.4 定义取消请求方法

具体使用看下一节 2.4。

axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
1.5 定义多请求方法,需要可以与spread配合将返回值数组包装成多个变量返回值形式

这里结合后面的 2.5 使用更清楚。

axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = require('./helpers/spread');

以上就把整个axios从新建到内部核心动作大致看完了,下面我们通过使用再具体看一下实际使用过程中的运行情况。

2、从使用运行回到源码学习

2.1 安装

安装有两种方式,一种是包管理,一种是 script 标签引入,不管是哪种安装,我们都会得到一个全局变量 axios(包管理使用时需要自己导入定义一下)。

2.2 使用

2.2.1 通过方法名
看一下常用的 get 示例请求

axios.get('/user?ID=12345')
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  })

先看为什么可以直接用 get 这个方法名呢?
这个就是 1.1.4 那节的代码 utils.extend(instance, Axios.prototype, context); 的作用,把 request、getUri以及delete、get、head、options、post、put、patch 都复制到了 axios 上,所以同样的道理可以得到以下几种请求方法,getUri 不是请求方法也不咋用到在这里先忽略。

axios.request(config)

axios.delete(url[, config])

axios.get(url[, config])

axios.head(url[, config])

axios.options(url[, config]])

axios.post(url[, data[, config]])

axios.put(url[, data[, config]])

axios.patch(url[, data[, config]])

现在我们通过上面的示例get请求来看代码是怎么运行的。

Axios.js 中是通过以下代码来定义 get 请求的,那么示例代码中唯一的实参 '/user?ID=12345' 就代表着定义中的形参 url ,并没有传入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
    }));
  };
});

定义里返回的是this.request(......)方法,我们之前在 1.1.3 说过这个 this 是指向传入的 context 的,这个 this.request 就是里面的 request 方法 ,是返回 promise 对象的,所以在示例里我们可以接着 axios.get('/user?ID=12345') 写 then和catch 方法。

再看 this.request 里对传入参数的处理。
config || {}:这里我们没有传入 congfig 实参,所以 config 是 undefined ,这个结果就是后面的空对象 {}
{method: method,url: url}:method 是定义方法时就赋值的,跟定义的方法同名,url就是我们传入的 '/user?ID=12345'
utils.merge(.....):这个方法就是把上面的两个对象合并成一个对象传入 this.request
程序走到这里合成的这个对象应该如下:

{
  method:"get",
  url:"/user?ID=12345"
}

看下 request 定义代码,这里的 function request(config) 中的 config 就是上面合成的对象值。

Axios.prototype.request = function request(config) {
 // Allow for axios('example/url'[, config]) a la fetch API
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }
  config = mergeConfig(this.defaults, config);
  // Set config.method
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }
  // 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;
};

先略掉开头一小段,其中有个 config 是否为 string 类型的判断和请求方法methods的处理,这个主要是 axios('/user?ID=12345') 使用形式的,我们待会说。现在从 var chain = [dispatchRequest, undefined]; 看起。

这是定义了一个拦截器数组 chain ,并且给了初始值 [dispatchRequest, undefined]。

 var promise = Promise.resolve(config);

定义 promise 对象,方便后面的链式调用。


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 是没有变化的。


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

开始调用运行 chain 数据,这里参数为什么是 (chain.shift(), chain.shift()) 呢,可以看下上两句请求拦截器中(interceptor.fulfilled, interceptor.rejected) 成功与失败处理函数都是成对添加到数组的,所以这也就解释了为什么初始化 chain 的时候是 dispatchRequest 和一个undefined 的了。当然是为了保证后续的响应拦截器的成功与失败处理函数是成对的。
这个 dispatchRequest 定义在 dispatchRequest.js 文件里。我们对照源码一一分析。

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

在处理请求前先判断有没有取消该请求,有了就抛出异常。


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

定义赋值请求头,在get示例我们并没有定义,所以这里的 config.headers 只有之前 default.js 文件中预先定义的部分,即:

headers:{
  common:{
    'Accept': 'application/json, text/plain, */*'
  },
  delete:{},
  get:{},
  head:{},
  post:{
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  put:{
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  patch:{
    'Content-Type': 'application/x-www-form-urlencoded'
  },
}

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

格式化请求数据,但是我们的get示例是没有请求 data 的,这里经过一圈处理 config.data 为 undefined。


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

将几个对象合成一个对象赋值给 config.headers。


  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

删除默认的 headers 配置


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

赋值适配器,需返回promise对象的,我们没有定义,就用默认的 default.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);
  });

终于到这了,config 进入 adapter

adapter: getDefaultAdapter()

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

打开两个文件看下都是返回的 new Promise(),如果调用过取消请求的方法会执行 function onAdapterRejection(reason) ,我们这里自然进入到了 function onAdapterResolution(response) ,这里就得到带有响应值的 promise 对象,这就保证了示例get中 axios.get() 后还可以再添加 then 方法。
关于 xhr.js 和 http.js 后续会出专门的文章解释。

2.2.2 参数调用
我们平时还会用一下的两方式去调用

axios("/user?ID=12345")

axios({
   url:"/user?ID=12345",
   method:'get"  //post等方法一样
})

为什么我们可以直接用这种形式调用呢,请看 1.1.3 var instance = bind(Axios.prototype.request, context); 这个在定义的时候就把 request 这个给复制给了 变量本身,所以 axios() 中是直接用的 Axios.prototype.request 方法。

再看Axios.prototype.request中之前略过的几行代码,就是为了这种类型的使用做的判断。

 // Allow for axios('example/url'[, config]) a la fetch API
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }
  config = mergeConfig(this.defaults, config);
  // Set config.method
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }

第一个判断, axios("/user?ID=12345") 中的config 就是 string 类型。
第二个判断,axios("/user?ID=12345") 中没有定义请求方法所以默认get。
经过第二个判断处理以后我们知道会扩展识别 config 的 method 属性,也就是说虽然 default.js 里没有定义到,但是程序处理的时候程序是可以读取或者建立 method 这个属性的,所以 axios({ url:"/user?ID=12345"}) 、axios({ url:"/user?ID=12345",method:'post"}) 也都是可以的。

另外中间还有有个 mergeConfig 方法定义在 mergeConfig.js 中,那么在这个文件里,我们看到所有可以设置的配置参数。

  var valueFromConfig2Keys = ['url', 'method', 'data'];
  var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy', 'params'];
  var defaultToConfig2Keys = [
    'baseURL', 'url', 'transformRequest', 'transformResponse', 'paramsSerializer',
    'timeout', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName',
    'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress',
    'maxContentLength', 'maxBodyLength', 'validateStatus', 'maxRedirects', 'httpAgent',
    'httpsAgent', 'cancelToken', 'socketPath', 'responseEncoding'
  ];

这里比较单一自己多看看。

2.3 axios.create()——自己创建 axios 实例

这个在文章 四、vue+ElementUI开发后台管理模板—方法指令、接口数据 第2大点 获取接口数据、模拟数据 里实际运用了,这里不再多说。

2.4 axios.CancelToken——取消请求

如果我们浏览SAP页面,在资源大、多或者响应比较慢的页面在发生跳转后,前一个页面的请求还会继续等待响应,那最好的是到新页面后,之前的请求应该全部取消,将资源留给新页面用。

我们先用 GitHub/chizijijiadami/vue-elementui-5 的代码看下这样的请求效果,这个是要配合之前用 phpstudy 写的跨域接口的,具体见文章 四、vue+ElementUI开发后台管理模板—方法指令、接口数据 底部部分。

获取代码后,做如下修改:
src>pages>Index>index.vue

  created() {
    this.getList();
-    //this.getCrossDomainList();
+   this.getCrossDomainList();
  },

src>pages>List>Detail>index.vue

+    created(){
+        console.log('list-detail');
+    }

对phpstudy中建立的网站文件增加一句 sleep(5) 代表延时5秒钟返回,可以模拟服务器延时情况。


上面这些修改后运行项目,打开首页后立即切换至详情页,在等待数秒后,控制台如下图。


页面已切换,前页面未完成请求仍然在继续返回数据

要想实现跳转至新页面后,立即取消前面页面的请求,我们可以做如下操作:
src>data>store>modules>app.js

        system: {
            title: "大米工厂",
+            requestCancel:[]
        },

......

   mutations: {
+        ADD_SYSTEM_REQUEST_CANCEL:(state,c)=>{
+            state.system.requestCancel.push(c)
+        },
+        SET_SYSTEM_REQUEST_CANCEL:state=>{
+            state.system.requestCancel=[]
+        },
    
......
    },
    actions: {
+        addSystemRequestCancel({ commit },c){
+            commit('ADD_SYSTEM_REQUEST_CANCEL',c)
+        },
+        setSystemRequestCancel({ commit }){
+            commit('SET_SYSTEM_REQUEST_CANCEL')
+        },

......
    }
 

src>common>utils>axiosApi.js,给每个请求添加取消方法。

import store from 'data/store'
import axios from 'axios'
import ErrorMessage from './errorMessage'
import { MessageBox } from 'element-ui'
var instance = axios.create({
    baseURL: '',
    timeout: 5000
});

+  const CancelToken = axios.CancelToken;

// 添加请求拦截器
instance.interceptors.request.use(
    // 在发送请求之前做些什么
    config => {
          config.headers['version'] = '1'
+          config.cancelToken = new CancelToken((c) => {
+              store.dispatch('addSystemRequestCancel', c)
+          });
          return config
    },
    error => {
        Promise.reject(error)
    }
);

src>common>routerFilter>index.js,登录权限信息肯定是不能取消的,这里应该在登录后实行这种机制。
传递取消信息,并且每到一个新页面前 requestCancel 数组应该清零。

            if (to.path === '/login') {
                next('/')
            } else {
-                next()
+                store.getters.app.system.requestCancel.forEach(cancel => {
+                    cancel('Cancel')
+                })
+                store.dispatch("setSystemRequestCancel").then(() => {
+                    next()
+                })
            }

现在我们再运行切换页面,看下控制台在进入 detail 页之前那个跨域请求被取消报了错,并且弹出了错误提示,这里就要进一步修改,取消请求的情况是不应该弹窗报错的


src>common>utils>axiosApi.js

    // 对响应错误做点什么
    if (!error.response) {
        // 服务器请求失败时错误提示
+        if (error.message !== 'Cancel') {
            MessageBox({
                message: `请求超时${ErrorMessage.API_ERROR_LOAD}`,
                showCancelButton: false,
                confirmButtonText: '确定',
                type: 'error',
                callback() { }
            })
+        }
    }

src>pages>Index>index.vue,这里添加错误处理代码

    getCrossDomainList() {
      api
        .getCrossDomainList()
        .then(res => {
          console.log(res);
        })
+        .catch(() => { });
    }

以上修改完成后我们再切换看看,没有任何报错了


这里是axios 取消的一种方式,还有另一种使用方式差不多,自己试试这里就不多说了。

2.5 axios.all()——多请求并发

src>pages>Index>index.vue

  created() {
    // this.getList();
    // this.getCrossDomainList();
+    this.all();
  },

methods:{
......
    all() {
      axios.all([api.getList(), api.getCrossDomainList()]).then(arr => {
        console.log(arr, "arr");
      });
      axios.all([api.getList(), api.getCrossDomainList()]).then(
        axios.spread(function(one, two) {
          console.log(one, two, "spread");
        })
      );
    }
}

运行结果如下图,就是拆分了返回值数组。还有一点可以看到这个all里面会等请求执行成功以后才一起返回,如果取消了其中一个,就会返回 reject。


感谢阅读,喜欢的话点个赞吧:)
更多内容请关注后续文章。。。

你可能感兴趣的:(axios源码学习到使用)