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的主页标注了它具有如下特性:

  • 从浏览器中创建 XMLHttpRequest
  • 从 node.js 发出 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 自动转换JSON数据
  • 客户端支持防止 CSRF/XSRF

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

由于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服务端渲染时,也可以使用这个方法来判断代码当前的执行环境。

2、支持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);
 //依次在处理链路数组中,从头部添加请求拦截器中间件(unshift:可向数组的开头添加一个或更多元素,并返回新的长度。)
 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);
 });

 //依次执行 请求拦截器中间件-> 请求 -> 返回拦截器中间件(shift:用于把数组的第一个元素从其中删除,并返回第一个元素的值。 )    
 while (chain.length) {
   promise = promise.then(chain.shift(), chain.shift());
 }
 //返回promise对象
 return promise;
}

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

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

依然是上面的核心流程代码,在设置好请求参数后,作者定义了一个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)

4、转换请求返回数据,自动转换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;
}],

5、取消请求

文档上给了两种示例:

方法一:

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的具体实现为(lib\cancel\CancelToken.js):

/**
* 可以进行取消请求操作的对象
*
* @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方法被触发就会执行取消请求的流程。(执行 cancel 方法 -> CancelToken.promise获取到resolve -> request.abort(); -> reject(cancel);)

方法一的源码实现(原理同方法二的源码实现,只是CancelToken.source会返回这个cancel):

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

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

6、客户端防止xsrf攻击

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

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

axios是如何防止xsrf攻击的?

// 添加 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 头中。

7、在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) {

   zar 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的赞,其优秀程度可见一斑。本文通过他的一些特性,分析了部分源码,旨在能够在使用它的同时,更加懂得它。

转自:https://www.cnblogs.com/zhuanzhuanfe/p/8458421.html

另外可参照阅读:https://blog.csdn.net/weixin_44475093/article/details/111940352

posted @ 2021-02-17 23:54  vickylinj  阅读(1009)  评论(0编辑  收藏  举报