axios的秘密

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

按照作者的原话来说:

“Ajax 本身跟 Vue 并没有什么需要特别整合的地方,使用 fetch polyfill 或是 axios、superagent 等等都可以起到同等的效果,vue-resource 提供的价值和其维护成本相比并不划算,所以决定在不久以后取消对 vue-resource 的官方推荐。已有的用户可以继续使用,但以后不再把 vue-resource 作为官方的 ajax 方案。”

除了维护成本方面的原因,axios本身的优点也使得它在一众ajax异步请求的框架中脱颖而出,下面我们通过分析部分axios的源码来看看,是什么让axios成为大多数人的选择。

GitHub上axios的主页标注了它具有如下特性:

Make XMLHttpRequests from the browser Make http requests from node.js Supports the Promise API Intercept request and response 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发送请求的方法,其中有一段代码为:

//定义适配器,判断是在服务器环境还是浏览器环境
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
    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);
  });
};

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

function getDefaultAdapter() {
  var adapter;

  if (typeof XMLHttpRequest !== 'undefined') {
    //通过判断XMLHttpRequest是否存在,来判断是否是浏览器环境
    adapter = require('./adapters/xhr');

  } else if (typeof process !== 'undefined') {
    //通过判断process是否存在,来判断是否是node环境
    adapter = require('./adapters/http');
  }
  return adapter;
}

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

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

支持promise

/**
 * 处理一个请求
 *
 * @param config 请求的配置
 */
Axios.prototype.request = function request(config) {

  // 如果是字符串,则直接赋值给配置的url属性
  if (typeof config === 'string') {
    config = utils.merge({
      url: arguments[0]
    }, arguments[1]);
  }
  // 合并默认配置和配置参数
  config = utils.merge(defaults, this.defaults, { method: 'get' }, config);
  config.method = config.method.toLowerCase();

  // 连接拦截器中间件
  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());
  }
  //返回promise对象
  return promise;
};

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

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

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

它的流程可以归纳为

resolve(request interceptor fulfilled N),reject(request interceptor rejected N)
-> ...
-> resolve(request interceptor fulfilled 0),reject(request interceptor rejected 0)
-> resolve(dispatchRequest ),reject(undefined)
-> resolve(response interceptor fulfilled 0),reject(response interceptor rejected 0)
-> ...
-> resolve(response interceptor fulfilled N),reject(response interceptor rejected N)

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

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

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



// 转换返回的数据
response.data = transformData(
  response.data,
  response.headers,
  config.transformResponse
);

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

transformResponse: [function transformResponse(data) {
    /*eslint no-param-reassign:0*/
    if (typeof data === 'string') {
        try {
        data = JSON.parse(data);
        } catch (e) { /* Ignore */ }
    }
    return data;
}],

取消请求

文档上给了两种示例:

第一种:

var CancelToken = axios.CancelToken;
var source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
  } else {
// 处理错误
  }
});

// 取消请求(message 参数是可选的)
source.cancel('取消请求');

第二种:

var CancelToken = axios.CancelToken;
var cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
       cancel = c;
  })
});

// 取消请求
cancel();

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

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

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

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

if (config.cancelToken) {
  // 处理取消请求的方法
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }

    request.abort();
    reject(cancel);
    // 清空请求
    request = null;
  });
}

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

cancelToken的具体实现为:

/**
 * 可以进行取消请求操作的对象
 *
 * @param {Function} executor 具体的执行方法.
 */
function CancelToken(executor) {
  //判断executor是一个可执行的函数
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }
  //定义一个promise的resolve回调
  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  //executor的参数为取消请求时需要执行的cancel函数
  executor(function cancel(message) {
    if (token.reason) {
      // 已经取消了请求
      return;
    }

    token.reason = new Cancel(message);
    //触发promise的resolve
    resolvePromise(token.reason);
  });
}

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

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

客户端防止xsrf攻击

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

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

举个例子:

假如一家网站执行转账的操作URL地址如下:

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

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

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

如果有账户名为Alice的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失1000资金。

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

axios是如何做的?

// 添加 xsrf 请求头
// 只在标准浏览器环境中才会起作用
if (utils.isStandardBrowserEnv()) {
  var cookies = require('./../helpers/cookies');

  // 添加 xsrf 请求头
  var xsrfValue = (config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName ?
      cookies.read(config.xsrfCookieName) :
      undefined;

  if (xsrfValue) {
    requestHeaders[config.xsrfHeaderName] = xsrfValue;
  }
}

首先,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功能,轻松的解决了这一问题。

//判断当前的部署环境
const isDev = process.env.NODE_ENV !== 'production'
if(isDev){

    let proxy = null;
    //如果不是线上环境,且配置了代理地址则进行代理的设置,devHost是具体的ip配置
    if(devHost.https){
        proxy = {
        host: devHost.https,
        port: 443
        };
        Axios.defaults.proxy = proxy;
    }else if(devHost.http){
        proxy = {
            host: devHost.http,
            port: 80
        };

        Axios.defaults.proxy = proxy;
    }else {
        //do nothing
    }

}

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

//如果设置了代理
if (proxy) {
  //取代理的域名为请求的域名
  options.hostname = proxy.host;
  options.host = proxy.host;
  options.headers.host = parsed.hostname + (parsed.port ? ':' + parsed.port : '');
  options.port = proxy.port;
  options.path = protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path;

  // Basic proxy authorization
  if (proxy.auth) {
    var base64 = new Buffer(proxy.auth.username + ':' + proxy.auth.password, 'utf8').toString('base64');
    options.headers['Proxy-Authorization'] = 'Basic ' + base64;
  }
}

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

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

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

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

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

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

let get = Axios.get;
/**
 * 对原方法的get做一层装饰,可以传参时不必写params参数,直接传递参数对象,同时对已有的params写法兼容
 * @param args 参数 url config
 * @returns {*}
 */
Axios.get = (...args) => {
    let param = args[1];
    //如果以参数方式传递query,同时不存在axios需要的key:params,则为它添加
    if (param && !param.hasOwnProperty('params')) {
        args[1] = {
            params: param
        }
    }
    return get(...args)
}

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

而对于post方式,则做了如下封装:

let post = Axios.post;
/**
 * axios的post请求默认会依据数据类型设置请求头,但是目前后台没有识别json,因此统一将请求的数据设置为x-www-form-urlencoded需要的字符串格式
 * @param args 参数 url data config
 * @returns {*}
 */
Axios.post = (...args) => {
    let data = args[1];
    //判断是对象就转化为字符串
    if (data && typeof data === 'object') {
        args[1] = qs.stringify(data)
    }
    return post(...args)
}

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