深入了解XMLHttpRequest

2,612 阅读7分钟

XMLHttpRequest

浏览器在 XMLHttpRequest 类上定义了他们的 HTTP API。这个类的每一个实例都表示一个独立的请求/响应,并且这个对象的属性和方法允许指定请求细节和提取响应数据。W3C在 XMLHttpRequest 规范的基础上制定了2级 XMLHttpRequest (下文简称 XHR2 )标准草案,且大部分浏览器都已经支持了。

在介绍 XMLHttpRequest 之前,我想先简单说一下HTTP请求组成部分和响应的组成部分

一个 HTTP 请求由4部分组成:

  • HTTP 请求方法或‘动作’。
  • 正在请求的URL。
  • 一个可选的请求头集合,其中可能包括身份验证信息。
  • 一个可选的请求主体。

服务器返回的 HTTP 响应包含3部分:

  • 一个数字或文字组成的状态码,用来显示请求的成功或失败。
  • 一个响应头集合。
  • 响应主体。

HTTP 请求的各部分有指定的顺序:请求方法和URL首先到达,然后是请求头,最后是主体。XHMLHttpRequest 实现通常直到调用send()方法才开始启动网络。但 XMLHttpRequest API的设计似乎使每个方法都将写入网络流。这意味着调用 XMLHttpRequest 上的方法的顺序必须匹配 HTTP 请求的架构。例如,setRequestHeader() 方法的调用必须在调用 open() 之后但是在 send() 之前,否则就会跑出异常。下面我门会按照这个顺序来介绍。

实例化

使用 XMLHttpRequest API的第一件事就是先实例化;

  var request = new XMLHttpRequest();

我们也能重用已经存在的 XMLHttpRequest对象,但是这将会中止之前通过该对象发起的任何请求,但一般不会这么用

指定请求

  request.open('POST', url);
  • 第一个参数指定HTTP方法或动作,注意: 不区分大小写,但通常都是使用大写来匹配HTTP协议。

    • GET 用于常规的请求,它适用于当URL完全指定请求资源,当请求对服务器没有任何副作用以及当服务器的响应是可缓存时。
    • POST 方法常用语HTML表单,他在请求主体中包含额外数据(表单数据)且这些数据场储存到服务器上的数据库中(副作用)。相同 URL 的重复请求从服务器得到的相应数据可能不同。同时缓存时不应该使用这个请求。
  • 第二个参数是URL,这个URL可以是相对url也可以是绝对的url。

  • 第三个参数用来设置请求的异步还是同步

    • false表示同步
    • true表示异步,默认true
  • 如果请求一个受保护的URL,把用户和密码作为第4和第5个参数传递。

设置请求头

  request.setRequestheader('Content-Type': 'text/plain');

注意:

  • 如果对相同的头调用 setRequestHeader() 多次,新值不会取代之前指定的值,相反,HTTP请求将包含这个头的多个副本或这个头将指定多个值。

  • setRequestHeader() 没有办法设置下面的 headerXMLHttpRequest 将自动添加这些头而防止伪造他们。类似的,XMLHttpRequest 对象自动处理 cookie,连接时间,字符集和编码判断,所以无法向 setRequestHeader() 传递这些头:

Accept-CharsetAccept-EncodingConnectionContent-LengthCookie
Cookie2Content-Transfer-EncodingDateExpectHost
Keep-LiveRefererTETrailerTransfer-Encoding
UpgrageUser-AgentVia
  • send() 方法传入 XML 文档时,没有指定 Content-TypeXMLHttpRequest 会自动设置一个合适的头。类似的如果给 send() 传入一个字符串但没有指定 Content-Type,那么 XMLHttpRequest 将会自动添加 text/plain; charset=utf-8 头。

  • 当使用 GET 方法时,不需要调用 setRequestHeader() 这个方法,因为 GET 请求只能进行 url编码(application/x-www-form-urlencoded),而如果使用 POST 方法且传递的参数是以 ‘&’ 和 ‘=’ 符号进行键值连接时,Content-Type 头必须设置 application/x-www-form-urlencoded

发送请求

使用 XMLHttpRequest 发起HTTP请求的最后一步就是指定可选的请求主体并向服务器发送它:

  request.send(null);
  • GET 请求绝对没有主体,所以应该传递null或省略这个参数。
  • POST 请求通常拥有主体,同时它应该匹配使用 setRequestHeader() 指定的 Content-type 头。

XMLHttpRequest 属性

readystatechange

XMLHttpRequest 对象通常异步使用:发送请求后,send() 方法立即返回,直到响应返回。为了在响应准备就绪的时候得到通知,必须监听 XMLHttpRequest 对象上的 readystatechange 事件。

readyState

它是一个整数, 他指定了 HTTP 请求的状态。

  • 0: 初始化状态。 xhr 对象已创建或已被 abort() 方法重置。
  • 1: open() 方法已调用,请求连接已经建立。但是 send() 方法未调用,请求数据未发送。
  • 2: send() 方法已调用,HTTP 请求已发送到 Web 服务器。接收到头信息
  • 3: 所有响应头部都已经接收到。响应体开始接收但未完成。
  • 4: HTTP 响应已经完全接收

status

服务器返回的http状态码,当 readyState 小于 3 的时候读取这一属性会导致一个异常。

  • 200 表示成功
  • 404 表示'Not Found'错误

statusText

以数字和文字的形式返回 HTTP 状态码。

  • status === 200 statusText 为 'OK'
  • status === 404 statusText 为 'Not Found'

getRequestHeader()/getAllRequestHeaders()

使用这两个方法都可以查询到响应头。XMLHttpRequest 会自动处理 cookie,他会从 getAllRequestHeaders() 头返回集中过滤掉 cookie 头。而如果给 getRequestHeader() 传递 Set-CookieSet-Cookie2 则会返回 null

responseText

responseText 接受到服务器的相应数据 返回的值是一个json字符串 通过 JSON.parse(xhr.responseText) 可以得到数据对象

  • readyState < 3 responseText 为空字符串
  • readyState = 3 responseText 为已经接收到数据部分
  • readyState = 4 responseText 为接受到了所有的相应部分

responseXML

responseXML属性可以得到 XML 的 Document形式的数据。

XHR2新增的事件集

XHR2规范定义了很多有用的事件集,在这个新的事件模型中,XMLHttpRequest 对象在请求的不同阶段触发不同类型的事件,所以它不再需要检查 readyState 属性
。

  • onloadstart : 当调用 send() 时,触发单个 loadstart 事件

  • onprogress : xhr对象会发生 progress 事件,通常每隔50ms左右触发一次,所以可以使用这个事件给用户反馈请求的进度。如果请求快速完成,他可能不会触发 progress 事件。注意这里的 progress 是下载的进度,xhr2 额外的定义了上传 upload 属性,用来定义上传的相关事件。

  • onload : 当事件完成时,触发 load 事件,load 事件的处理程序应该检查xhr对象的 status 状态码来确定收到的是 200 还是 404

  • ontimeout : 如果请求超时,会触发 timeout 事件。

  • onerror : 大多重定向这样的网络错误会阻止请求的完成,但这些情况发生时会触发 error 事件。

  • onabort : 如果请求中止,会触发 abort 事件。

  • onloadend : 对于任何具体的请求,浏览器将只会触发 load/abort/timeout/error 事件中的一个。一旦这些事件触发以后,浏览器将会触发 loadend 事件。

注意: 上面的这些事件我们可以通过 xhr.addEventListener() 方法进行监听。

progress事件中有三个属性需要讲解一下:

  • loaded : 目前传输的字节数值
  • total : 传输的数据的整体长度(单位字节),Content-Length == total,如果不知道内容的长度则 total == 0
  • lengthComputable : 如果知道内容的长度则 lengthComputable == true, 否则 false
  xhr.onprogress = function(event) {
    if (event.lengthComputable) {
            
      const progress = event.loaded / event.total * 100;
    }
  }

XHR2 新增的 upload 属性

XHR2 中新增了一个 upload 属性,这个属性值是一个对象,他定义了 addEventListener() 和 整个 progress 事件集合,比如说 onprogressonload。(但 upload 没有定义 onreadystatechange 属性,upload 仅能触发新的事件类型)。

  • onloadstart : 和 XMLHttpRequest 中的 loadstart 事件一样。

  • onprogress : 和 XMLHttpRequest 中的 progress 事件一样。

  • onload : 和 XMLHttpRequest 中的 load 事件一样。

  • ontimeout : 和 XMLHttpRequest 中的 timeout 事件一样。

  • onerror : 和 XMLHttpRequest 中的 error 事件一样。

  • onabort : 和 XMLHttpRequest 中的 abort 事件一样。

  • onloadend : 和 XMLHttpRequest 中的 loadend 事件一样。

注意

upload 属性上定义的事件主要用在上传文件时。我们可以使用 upload 上的 onloadstart,onprogress 分别监听文件开始上传和上传过程中进度的变化。

对于文件上传,我们如何设置请求头??

  const input = document.getElementsByTagName('input')[0];
  input.addEventListener('change', function() {
    var file = this.files[0];
    if (!file) return;
    var xhr = new XMLHttpRequest();
    xhr.open('POST', url);
    xhr.send(file);
  }, false);

文件类型是更普通的二进制大对象 Blob 类型中的一个字类型。XHR2 允许向 send() 方法传入任何 Blob 对象。如果没有显示的设置 Content-Type 头,这个 Blob 对象的 type 属性用于设置待上传的 Content-Type 头。

multipart/form-data请求

XHR2 定义了新的 FormData API, 它容易实现多部分请求主体。首先,使用 FormData() 构造函数创建 FormData 对象,然后按需多次调用这个对象的 append() 方法把个体部分(string/File/Blob对象) 添加到请求中。最后把 FormData 对象传递给 send() 方法。send() 方法将对请求定义合适的边界字符串和设置 Content-Type 头。

中止请求和超时

中止请求

通过调用 XMLHttpRequest 对象的 abort() 方法来取消正在进行的 HTTP 请求。当调用 abort() 方法后会触发 xhr 对象的 onabort 事件。

超时

XHR2 定义了 timeout 属性来指定请求自动终止的毫秒数。同时也定义了 timeout 事件,当超时发生时触发。

demo

  // 封装一个request方法
  const request = (url, formData, cb) => {
    // 初始化
    const xhr = new XMLHttpRequest();
    // 定义请求的方法/动作和url
    xhr.open('POST', url);
    // 设置超时时间,单位是毫秒
    xhr.timeout = 2000; 

    xhr.ontimeout = function() {
      console.log('timeout');
    };
    // 开始上传
    xhr.upload.onloadstart = function() {
      console.log('开始上传');
    };
    // 上传的进度
    xhr.upload.onprogress = function(event) {
      // 只有当 lengthComputable 为true是,loaded 才有值
      if (event.lengthComputable) {
        const value = Math.ceil((event.loaded / event.total) * 100);
        cb && cb({
          status: 'loading',
          progress: value,
          data: null,
        });
      }
    };
    // 监听事件完成, 完成并不一定代表请求成功,所以需要判断 status 状态码
    xhr.onload = function() {
      const resp: Response = {
        status: 'success',
        progress: 100,
        data: null,
      };
      if (xhr.status === 200) {
        resp.data = JSON.parse(xhr.responseText);
        cb && cb(resp);
      } else {
        resp.status = 'error';
        cb && cb(resp);
      }
    };
    xhr.onerror = function() {
      cb && cb({
        status: 'error',
        progress: 0,
        data: null,
      });
    };
    xhr.onabort = function() {
      console.log('onabort');
    };
    xhr.onloadend = function() {
      console.log('上传结束');
    };
    xhr.send(formData);
    return xhr;
  };

HTTP跨域请求

作为同源策略的一部分,XMLHttpRequest 对象通常仅可以发起和文档具有相同服务器的 HTTP 请求。这个限制关闭了安全里漏洞,但同时也阻止了大量可使用的跨域请求。好在 XHR2 通过在 HTTP 响应中选择发送合适的 CORS(Cross-Origin Resource Sharing,跨域资源共享) 允许跨域访问网站。在日常开发中使用跨域请求并不需要进行的额外的其他设置,只要浏览器支持 CORS 跨域请求就行。 虽然实现 CORS 支持跨域的请求工作不需要做任务的事情,但有一些安全细节需要了解:

  • 如果 xhr.open() 方法传入第四和第五个参数(用户名和密码)时,将不会通过跨域请求发送
  • 跨域请求默认情况是不会携带 cookie 的。如果需要携带 cookie,那么可以在调用 send() 方法之前设置 XMLHttpRequestwithCredentials属性为 true
  • 监测浏览器是否支持 CORS 跨域请求,可以直接通过检测 XMLHttpRequestwithCredentials 的属性是否存在即可。

注意:XMLHttpRequest 的跨域请求同样包含简单请求和非简单请求,非简单请求又会进行预检请求,具体 CORS 的相关知识可以查看之前的分享的文章点击这里

总结

XMLHttpRequest API非常的好用,而且目前市面上的主浏览器也基本上都支持。相比 fetch 而言,兼容性肯定是更胜一筹,唯一不足的是不支持 Promise,但是这也难不倒我们程序员,自己封装一层就可以了。更为重要的是 XMLHttpRequest 支持超时设置和中止请求,还有进度事件,这些都是 'fetch' 所不具备的。

参考