axios的秘密

vue自2.0开始,vue-resource再也不做为官方推荐的ajax方案,转而推荐使用axiosvue

按照做者的原话来讲:node

“Ajax 自己跟 Vue 并无什么须要特别整合的地方,使用 fetch polyfill 或是 axios、superagent 等等均可以起到同等的效果,vue-resource 提供的价值和其维护成本相比并不划算,因此决定在不久之后取消对 vue-resource 的官方推荐。已有的用户能够继续使用,但之后再也不把 vue-resource 做为官方的 ajax 方案。”ios

除了维护成本方面的缘由,axios自己的优势也使得它在一众ajax异步请求的框架中脱颖而出,下面咱们经过分析部分axios的源码来看看,是什么让axios成为大多数人的选择。git

GitHub上axios的主页标注了它具备以下特性:github

Make XMLHttpRequests from the browserajax

Make http requests from node.js编程

Supports the Promise APIjson

Intercept request and responseaxios

Transform request and response data后端

Cancel requests

Automatic transforms for JSON data

Client side support for protecting against XSRF

咱们来一一分析。

同时支持浏览器端和服务端的请求。

因为axios的这一特性,vue的服务端渲染对于axios简直毫无抵抗力。 让咱们一块儿来读读源码,看看它是如何实现的。

axios/lib/core/dispatchRequest.js文件中暴露的dispatchRequest方法就是axios发送请求的方法,其中有一段代码为:

  1. //定义适配器,判断是在服务器环境仍是浏览器环境

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

  3. return adapter(config).then(function onAdapterResolution(response) {

  4.    throwIfCancellationRequested(config);

  5.  

  6.    // 处理返回的数据

  7.    response.data = transformData(

  8.          response.data,

  9.          response.headers,

  10.         config.transformResponse

  11.    );

  12.  

  13.    return response;

  14.  }, function onAdapterRejection(reason) {

  15.    if (!isCancel(reason)) {

  16.          throwIfCancellationRequested(config);

  17.  

  18.      // 处理失败缘由

  19.      if (reason && reason.response) {

  20.        reason.response.data = transformData(

  21.              reason.response.data,

  22.              reason.response.headers,

  23.              config.transformResponse

  24.        );

  25.      }

  26.    }

  27.  

  28.    return Promise.reject(reason);

  29.  });

  30. };

这段代码首先定义了一个适配器,而后返回了适配器处理后的内容。 若是没有在传入的配置参数中指定适配器,则取默认配置文件中定义的适配器,再让咱们来看看默认文件/lib/defaults.js定义的适配器:

  1. function getDefaultAdapter() {

  2.  var adapter;

  3.  

  4.  if (typeof XMLHttpRequest !== 'undefined') {

  5.    //经过判断XMLHttpRequest是否存在,来判断是不是浏览器环境

  6.    adapter = require('./adapters/xhr');

  7.  

  8.  } else if (typeof process !== 'undefined') {

  9.    //经过判断process是否存在,来判断是不是node环境

  10.    adapter = require('./adapters/http');

  11.  }

  12.  return adapter;

  13. }

到这里真相大白,XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能;process 对象是一个 global (全局变量),提供有关信息,控制当前 Node.js 进程。原来做者是经过判断XMLHttpRequest和process这两个全局变量来判断程序的运行环境的,从而在不一样的环境提供不一样的http请求模块,实现客户端和服务端程序的兼容

同理,咱们在作ssr服务端渲染时,也可使用这个方法来判断代码当前的执行环境。

支持promise

  1. /**

  2. * 处理一个请求

  3. *

  4. * @param config 请求的配置

  5. */

  6. Axios.prototype.request = function request(config) {

  7.  

  8.  // 若是是字符串,则直接赋值给配置的url属性

  9.  if (typeof config === 'string') {

  10.    config = utils.merge({

  11.      url: arguments[0]

  12.    }, arguments[1]);

  13.  }

  14.  // 合并默认配置和配置参数    

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

  16.  config.method = config.method.toLowerCase();

  17.  

  18.  // 链接拦截器中间件

  19.  var chain = [dispatchRequest, undefined];

  20.  var promise = Promise.resolve(config);

  21.  

  22.  //依次在处理链路数组中,从头部添加请求拦截器中间件

  23.  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {

  24.    chain.unshift(interceptor.fulfilled, interceptor.rejected);

  25.  });

  26.  //依次在处理链路数组中,从尾部添加返回拦截器中间件

  27.  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {

  28.    chain.push(interceptor.fulfilled, interceptor.rejected);

  29.  });

  30.  //依次执行 请求拦截器中间件-> 请求 -> 返回拦截器中间件    

  31.  while (chain.length) {

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

  33.  }

  34.  //返回promise对象

  35.  return promise;

  36. };

这一段是axios请求总体流程的核心方法,能够看到请求返回的是一个promise对象。这样可让咱们的异步请求自然的支持promise,方便咱们对于异步的处理。

支持请求和和数据返回的拦截

依然是上面的核心流程代码,在设置好请求参数后,做者定义了一个chain数组,同时放入了dispatchRequest, undefined这两个元素对应promise的resolve和reject方法,以后将请求拦截器的成功和失败处理依次压入chain数组头部,将返回拦截器的成功和失败处理依次推入chain数组尾部。 最后循环取出chain数组,先依次取出chain数组中成对的请求拦截处理方法,promise执行,而后取出最初定义的dispatchRequest, undefined这两个元素执行请求,最后依次取出chain数组中成对的返回拦截器。

它的流程能够概括为

  1. resolve(request interceptor fulfilled N),reject(request interceptor rejected N)

  2. -> ...

  3. -> resolve(request interceptor fulfilled 0),reject(request interceptor rejected 0)

  4. -> resolve(dispatchRequest ),reject(undefined)

  5. -> resolve(response interceptor fulfilled 0),reject(response interceptor rejected 0)

  6. -> ...

  7. -> resolve(response interceptor fulfilled N),reject(response interceptor rejected N)

转换请求返回数据,自动转换JSON数据

在axios/lib/core/dispatchRequest.js文件中暴露的核心方法dispatchRequest中,有这样两段:

  1. // 转换请求的数据  

  2. config.data = transformData(

  3. config.data,

  4. config.headers,

  5. config.transformRequest

  6. );

  7.  

  8.  

  9.  

  10. // 转换返回的数据

  11. response.data = transformData(

  12.  response.data,

  13.  response.headers,

  14.  config.transformResponse

  15. );

axios经过设置transformResponse,可自动转换请求返回的json数据

  1. transformResponse: [function transformResponse(data) {

  2.    /*eslint no-param-reassign:0*/

  3.    if (typeof data === 'string') {

  4.        try {

  5.        data = JSON.parse(data);

  6.        } catch (e) { /* Ignore */ }

  7.    }

  8.    return data;

  9. }],

取消请求

文档上给了两种示例:

第一种:

  1. var CancelToken = axios.CancelToken;

  2. var source = CancelToken.source();

  3.  

  4. axios.get('/user/12345', {

  5.  cancelToken: source.token

  6. }).catch(function(thrown) {

  7.  if (axios.isCancel(thrown)) {

  8. console.log('Request canceled', thrown.message);

  9.  } else {

  10. // 处理错误

  11.  }

  12. });

  13.  

  14. // 取消请求(message 参数是可选的)

  15. source.cancel('取消请求');

第二种:

  1. var CancelToken = axios.CancelToken;

  2. var cancel;

  3.  

  4. axios.get('/user/12345', {

  5.  cancelToken: new CancelToken(function executor(c) {

  6.    // executor 函数接收一个 cancel 函数做为参数

  7.       cancel = c;

  8.  })

  9. });

  10.  

  11. // 取消请求

  12. cancel();

这两种方法均可以取消发出的请求。先看具体的流程:

  1. 执行 cancel 方法 -> CancelToken.promise获取到resolve -> request.abort(); -> reject(cancel);

下面来看源码是如何实现的。

在xhr.js或http.js中都有这样一段,下面取自/lib/adapters/xhr.js

  1. if (config.cancelToken) {

  2.  // 处理取消请求的方法

  3.  config.cancelToken.promise.then(function onCanceled(cancel) {

  4.    if (!request) {

  5.      return;

  6.    }

  7.  

  8.    request.abort();

  9.    reject(cancel);

  10.    // 清空请求

  11.    request = null;

  12.  });

  13. }

在请求时若是设置了cancelToken参数,就会监听来自cancelToken的promise,一旦来自cancelToken的promise被触发,就会执行取消请求的流程。

cancelToken的具体实现为:

  1. /**

  2. * 能够进行取消请求操做的对象

  3. *

  4. * @param {Function} executor 具体的执行方法.

  5. */

  6. function CancelToken(executor) {

  7.  //判断executor是一个可执行的函数

  8.  if (typeof executor !== 'function') {

  9.    throw new TypeError('executor must be a function.');

  10.  }

  11.  //定义一个promise的resolve回调

  12.  var resolvePromise;

  13.  this.promise = new Promise(function promiseExecutor(resolve) {

  14.    resolvePromise = resolve;

  15.  });

  16.  

  17.  var token = this;

  18.  //executor的参数为取消请求时须要执行的cancel函数

  19.  executor(function cancel(message) {

  20.    if (token.reason) {

  21.      // 已经取消了请求

  22.      return;

  23.    }

  24.  

  25.    token.reason = new Cancel(message);

  26.    //触发promise的resolve

  27.    resolvePromise(token.reason);

  28.  });

  29. }

就是在上面的代码里,CancelToken会给本身添加一个promise属性,一旦cancel方法被触发就会执行取消请求的流程。

利用这个方法,一方面能够在按钮的重复点击方面大显身手,另外一方面能够在数据的获取方面直接获取最新的数据。

客户端防止xsrf攻击

先来了解一下XSRF,如下内容来自维基百科。

XSRF跨站请求伪造(Cross-site request forgery),是一种挟制用户在当前已登陆的Web应用程序上执行非本意的操做的攻击方法。

举个例子:

假如一家网站执行转帐的操做URL地址以下:

  1. http://www.examplebank.com/withdraw?account=AccoutName&psd=1000&for=PayeeName

那么,一个恶意攻击者能够在另外一个网站上放置以下代码:

  1. <img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">

若是有帐户名为Alice的用户访问了恶意站点,而她以前刚访问过银行不久,登陆信息还没有过时,那么她就会损失1000资金。

-------我是分割线,以上内容来自维基百科-------

axios是如何作的?

  1. // 添加 xsrf 请求头

  2. // 只在标准浏览器环境中才会起做用

  3. if (utils.isStandardBrowserEnv()) {

  4.  var cookies = require('./../helpers/cookies');

  5.  

  6.  // 添加 xsrf 请求头

  7.  var xsrfValue = (config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName ?

  8.      cookies.read(config.xsrfCookieName) :

  9.      undefined;

  10.  

  11.  if (xsrfValue) {

  12.    requestHeaders[config.xsrfHeaderName] = xsrfValue;

  13.  }

  14. }

首先,axios会检查是不是标准的浏览器环境,而后在标准的浏览器环境中判断,若是设置了跨域请求时须要凭证且请求的域名和页面的域名相同时,读取cookie中xsrf token 的值,并设置到承载 xsrf token 的值的 HTTP 头中。

在node端支持设置代理

若是在标准浏览器环境则执行/lib/adapters/xhr.js,axios不支持proxy;若是在node环境运行,用/lib/adapters/http.js,支持proxy。

在组内的一个项目中,使用了vue的ssr服务端渲染的技术,其中ajax方案就是采用了axios。因为ssr是在服务端请求,所以在开发、测试、上线的过程当中须要相应的host环境。例如在开发过程当中,要么须要在程序的请求地址中写上ip地址替代域名,要么须要设置电脑的host文件,改变域名映射的ip地址。而在测试环境中,一样须要更改代码或者测试环境的host文件。这样一来,若是是改代码的方案则影响了代码的稳定性,每一次部署都须要修改代码;若是是修改host文件,则会影响环境的一致性,假如环境还部署了其余的服务,还会影响其余服务的测试。所以咱们组内的男神便使用了axios的proxy功能,轻松的解决了这一问题。

  1. //判断当前的部署环境

  2. const isDev = process.env.NODE_ENV !== 'production'

  3. if(isDev){

  4.  

  5.    let proxy = null;

  6.    //若是不是线上环境,且配置了代理地址则进行代理的设置,devHost是具体的ip配置

  7.    if(devHost.https){

  8.        proxy = {

  9.        host: devHost.https,

  10.        port: 443

  11.        };

  12.        Axios.defaults.proxy = proxy;

  13.    }else if(devHost.http){

  14.        proxy = {

  15.            host: devHost.http,

  16.            port: 80

  17.        };

  18.  

  19.        Axios.defaults.proxy = proxy;

  20.    }else {

  21.        //do nothing

  22.    }

  23.  

  24. }

而在axios的源码/lib/adapters/http.js中,则是如此实现代理的:

  1. //若是设置了代理

  2. if (proxy) {

  3.  //取代理的域名为请求的域名

  4.  options.hostname = proxy.host;

  5.  options.host = proxy.host;

  6.  options.headers.host = parsed.hostname + (parsed.port ? ':' + parsed.port : '');

  7.  options.port = proxy.port;

  8.  options.path = protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path;

  9.  

  10.  // Basic proxy authorization

  11.  if (proxy.auth) {

  12.    var base64 = new Buffer(proxy.auth.username + ':' + proxy.auth.password, 'utf8').toString('base64');

  13.    options.headers['Proxy-Authorization'] = 'Basic ' + base64;

  14.  }

  15. }

内部一些针对具体项目环境的二次封装

上面基于源码具体分析了axios的各项特性,下面再来说一讲咱们在具体使用时的一些二次封装。因为axios使用get方式设置参数时,都须要使用params的方式,例如:

  1. axios.get('/user', {

  2.    params: {

  3.      ID: 12345

  4.    }

  5.  })

  6.  .then(function (response) {

  7.    console.log(response);

  8.  })

  9.  .catch(function (error) {

  10.    console.log(error);

  11.  });

而以前使用vue-resource则习惯直接写上参数,形如:

  1. axios.get('/user',

  2.    {

  3.      ID: 12345

  4.    }

  5.  )

  6.  .then(function (response) {

  7.    console.log(response);

  8.  })

  9.  .catch(function (error) {

  10.    console.log(error);

  11.  });

所以,对于组内的axios统一加了一层封装,承接以前的使用习惯:

  1. let get = Axios.get;

  2. /**

  3. * 对原方法的get作一层装饰,能够传参时没必要写params参数,直接传递参数对象,同时对已有的params写法兼容

  4. * @param args 参数 url config

  5. * @returns {*}

  6. */

  7. Axios.get = (...args) => {

  8.    let param = args[1];

  9.    //若是以参数方式传递query,同时不存在axios须要的key:params,则为它添加

  10.    if (param && !param.hasOwnProperty('params')) {

  11.        args[1] = {

  12.            params: param

  13.        }

  14.    }

  15.    return get(...args)

  16. }

这里须要注意的是,要确保在提交到服务端的query参数中不包含‘params’字段,否则仍是要使用默认的参数格式。

而对于post方式,则作了以下封装:

  1. let post = Axios.post;

  2. /**

  3. * axios的post请求默认会依据数据类型设置请求头,可是目先后台没有识别json,所以统一将请求的数据设置为x-www-form-urlencoded须要的字符串格式

  4. * @param args 参数 url data config

  5. * @returns {*}

  6. */

  7. Axios.post = (...args) => {

  8.    let data = args[1];

  9.    //判断是对象就转化为字符串

  10.    if (data && typeof data === 'object') {

  11.        args[1] = qs.stringify(data)

  12.    }

  13.    return post(...args)

  14. }

axios使用方便,功能齐备强大,其中的一些编程思想也很不入俗套,是一款先后端通用的ajax请求框架,目前在github上已经有接近36K的赞,其优秀程度可见一斑。本文经过他的一些特性,分析了部分源码,旨在可以在使用它的同时,更加懂得它。

 

——————————————————

长按二维码,关注大转转FE

相关文章
相关标签/搜索