zoukankan      html  css  js  c++  java
  • 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的具体实现为(libcancelCancelToken.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

  • 相关阅读:
    【模板时间】◆模板·III◆ 单调子序列
    【学时总结】◆学时·VII◆ 高维DP
    【例题收藏】◇例题·IV◇ Wooden Sticks
    【赛时总结】◇赛时·VI◇ Atcoder ABC-104
    【例题收藏】◇例题·III◇ 木と整数 / Integers on a Tree
    【学时总结】◆学时·VI◆ SPLAY伸展树
    【模板时间】◆模板·II◆ 树链剖分
    【赛时总结】◇赛时·V◇ Codeforces Round #486 Div3
    【例题收藏】◇例题·II◇ Berland and the Shortest Paths
    【例题收藏】◇例题·I◇ Snuke's Subway Trip
  • 原文地址:https://www.cnblogs.com/vickylinj/p/14410296.html
Copyright © 2011-2022 走看看