XHR、Fetch和axios

太长不看

  1. XHR设计关注点不分离,数据输入/输出、状态变化追踪全部放在了一个对象里面;
  2. 由jQuery封装的XHR依旧达不到关注点分离,代码逻辑上不具备时序性;
  3. Fetch基于Promise设计支持ES6的语法与JS语言发展趋势契合,关注点分离,代码具备时序性,并且属于JavaScript语言侧易于同构但浏览器支持度不如XHR(废话);
  4. axios基于Promise封装同样支持ES6语法、关注点分离、代码具备时序性、易于同构且封装完善久经沙场适合在生产环境使用;
  5. XHR年老色衰,Fetch初露锋芒,axios正当壮年;
  6. 本文完。(大雾)

XHR

XHR就是XMLHttpRequest的简写,XMLHttpRequest是一个对象并为你提供一些方法让你获取后端数据的同时不需要刷新整个页面,这样的方式叫ajax。
下面贴一个简单的xhr代码:

// 手动对老版本的IE做兼容
let xhr
if (window.XMLHttpRequest) {  // Mozilla, Safari...
   xhr = new XMLHttpRequest();
} else if (window.ActiveXObject) { // IE
   try {
     xhr = new ActiveXObject('Msxml2.XMLHTTP');
   } catch (e) {
     try {
       xhr = new ActiveXObject('Microsoft.XMLHTTP');
     } catch (e) {}
   }
}
// onreadystatechange 方法
const onReadyStateChange = () => {
    if (!xhr || xhr.readyState !== 4) {
       return;
     }

     // 处理文件传输的特殊情况
     if (xhr.status === 0 && !(xhr.responseURL && xhr.responseURL.indexOf('file:') === 0)) {
       return;
     }
    var responseData = !xhr.responseType || xhr.responseType === 'text' ? xhr.responseText : xhr.response;

}
if (xhr) {
    // 注册回调函数
   xhr.onreadystatechange = onReadyStateChange;
   // 打开XHR
   xhr.open('POST', '/api', true);
   
   // 设置XHR的Header
   xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
   // 发送数据
   xhr.send('username=admin&password=root');
}

从上述代码可以看出来,要发送一个xhr请求其实还挺麻烦的,而且代码看上去一点也不简洁明了。下面一个例子中就算使用了jQuery,也并没特别好的改善现状,数据的输入、输出,错误处理依旧混杂在一起并不明晰,即使success、error、complete语义清晰但并不与ES6的规范(then、catch、finally)相同,命名显得更“业务”且框架众多时大家各取各的名字(例如:done、fail等)只会变得更混乱。

   $.ajax({
            type:"POST",
            url:"testLogin.aspx",
            data:{Name:"sanmao",Password:"sanmaoword"},
            beforeSend:function(){$("#msg").html("logining");},
            success:function(data){
           		$("#msg").html(decodeURI(data));            
            },
            complete: function(XMLHttpRequest, textStatus){
               alert(XMLHttpRequest.responseText);
               alert(textStatus);
                //HideLoading();
            },
            error: function(){
                //请求出错处理
            }         
         });

fetch

XHR其实不是属于ECMAScript的标准方法,是在发展过程中各个浏览器厂家所开发出来的东西,逐渐成为主流。但也因为“各自为政”(IE),搞得一些浏览器的实现不太一样,对于开发者来说不友好。实现基于Promise的Fetch作为XHR的替代方案横空出世,配合上ES6的Promise语法和链式写法代码具有了更好的语义,这是XHR的回调函数所不具备的。
下面的两个例子很好的反映了这个情况:

fetch("http://www.example.org/submit.php", {
  		method: "POST",
  		headers: {
    		"Content-Type": "application/x-www-form-urlencoded"
  		},
  		body: "firstName=Nikhil&favColor=blue&password=easytoguess"
  	})
	.then((res) => {
  		if (res.ok) {
    		alert("Perfect! Your settings are saved.");
  		} else if (res.status == 401) {
    		alert("Oops! You are not authorized.");
  		}
	})
	.catch(()=>{
 		alert("Error submitting form!");
	})

下面是一个链式调用的fetch示例:

const status = (response) => {
  if (response.status >= 200 && response.status < 300) {
    return Promise.resolve(response)
  } else {
    return Promise.reject(new Error(response.statusText))
  }
}

const json = (response) => {
  return response.json()
}

fetch('users.json')
  .then(status)
  .then(json)
  .then(function(data) {
    console.log('Request succeeded with JSON response', data);
  }).catch(function(error) {
    console.log('Request failed', error);
  });

在设计上,XHR缺乏关注点分离,XHR将数据输入、输出和状态三个关注点都管理在一个对象下,并且状态变化依旧使用‘event’模式。并且‘event’模式与现在JavaScript所推崇Promise和generator的异步写法配合得并不那么好。
那么总结起来Fetch具有一下优点:

  1. 语法简洁,更加语义化
  2. 基于标准 Promise 实现,支持 async/await
  3. 同构方便,使用 isomorphic-fetch
  4. 更加底层,提供的API丰富(request, response)
  5. 脱离了XHR,是ES规范里新的实现方式
    但fetch有一个缺点就是浏览器支持度不如xhr:
    XHR、Fetch和axios
    XHR、Fetch和axios
    fetch既然有这么多优点,那我们应该马上开始使用fetch吗?其实不然

axios

axios是一个基于Promise实现的并同时为浏览器和Node提供请求解决方案的包。axios同样具有语言简洁和语义化的特点,同样支持async/await操作,同构上甚至比fetch更方便,不需要引入别的库,因为axios已经支持了nodejs上的请求。所以对于1、2、3的特点,axios都已经具备了,并不能成为Fetch取代axios的理由。况且axios适用范围之广,也已经饱受考验并值得信赖了。
那让我们来看一下axios还为你做了什么事,翻开源码撸起来:

入口

index.js

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

axios.js

'use strict';

var utils = require('./utils');
var bind = require('./helpers/bind');
var Axios = require('./core/Axios');
var mergeConfig = require('./core/mergeConfig');
var defaults = require('./defaults');

/**
 * Create an instance of Axios
 *
 * @param {Object} defaultConfig The default config for the instance
 * @return {Axios} A new instance of Axios
 */
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  utils.extend(instance, context);

  return instance;
}

// Create the default instance to be exported
var axios = createInstance(defaults);

// Expose Axios class to allow class inheritance
axios.Axios = Axios;

// Factory for creating new instances
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

// Expose Cancel & CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

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

module.exports = axios;

// Allow use of default import syntax in TypeScript
module.exports.default = axios;

总结起来大概干了:创建axios实例、挂载create/all/spread方法、挂载cancel/cancelToken/isCancel属性这么几件事。暴露出来的craete/all都是比较常用的方法,其中比较重要一点是defaults,axios的默认配置。

defaults

我不再贴代码了,下面一张图告诉你,defaults里面有啥:
XHR、Fetch和axios
标红的地方可以稍微说一下:

  1. transformRequest
    其中transformRequest是一个数组,数组里面是一堆函数,这个数组会在请求发送前挨个执行,此处默认有一个对数据类型进行处理的函数,我们也自己在配置config时加入这个参数对发送前的请求做一些处理,不过要求每个函数最后都要return data方便传递给下一个函数。
  2. 对象类型的data会默认以json格式传输,有部分后端同学在Controller层获取参数时并没根据Content-Type类型来做处理,导致我们可能需要使用到qs包来对这部分参数做处理再传给后端,那么Content-Type就变成了application/x-www-form-urlencoded类型
  3. transformResponse
    也是一个数组,里面有一个默认函数会对字符串类型的数据进行parse,如果成功就返回parse之后的数据给我们,如果失败就默认返回原数据。这也是我们为什么在进行网络传输时,不需要再对data进行一次parse而直接就得到了一个对象的原因

adapter

adapter是defaults配置中的一项,也正是adapter让axios可以在浏览器和node两端都能使用。XHR部分是基于Promise的对XMLHttpRequest的封装,http也是基于Promise的对node http模块的封装。
那么xhr的封装,给我们做了哪些事情呢,我列举一下不再大量贴源码了,感兴趣的同学可以自己去看

  1. multipart/form-data类型数据删除Content-Type交给浏览器设置;(涉及boundary的随机生成)
  2. 对Authorization的基本方案进行了实现;
  3. 设置onreadystatechange方法,其中对file类型协议做了一些浏览器兼容;
  4. 支持了防范xsrf的header头;
  5. 支持了跨域资源设置withCredentials设置;
  6. 同样支持下载/上传进度;

总结

翻到最顶部

一些其他细节

  • XHR在以下几种情况都不会被垃圾回收:
    • 如果一个XHR对象的state是opened/received/loading;
    • 如果在readystatechange/progress/abort/error/load/timeout/loaded上注册有event listeners;
    • axios在执行完一次请求后会对request置null,这样就能被标记清除垃圾回收了。
  • Origin/Referer/Cookie/Cookie2等是XHR的forbidden header name,不能被js所设置,在现代浏览器中通过Referer则能有效的防范CSRF/XSRF攻击;
  • XHR在请求时会主动带上同源cookie写在header中,Fetch在稍老版本浏览器中是默认不会携带cookie的但在新的浏览器中已经默认携带同源cookie;

参考资料

  1. whatwg–XMLHttpRequest
  2. whatwg–fetch
  3. MDN-XMLHttpRequest
  4. GoogleDeveloper-fetch

扩展学习

  1. JavaScript的垃圾回收
  2. 详细介绍XMLHttpRequest
  3. [详细介绍Fetch]:待填坑
  4. [axios源码分析]:待填坑