原生JavaScript中的XMLHttpRequest详解

    jwolf 
2723  0  0   2020-5-16 2:49



XMLHttpRequest


XMLHttpRequest 是一个内置的浏览器对象,允许使用JavaScript发出HTTP请求。

尽管名称中带有“ XML”一词,但它可以对任何数据进行操作,而不仅限于XML格式。我们可以上传/下载文件,跟踪进度等等。

现在,还有另一种更现代的方法fetch在某种程度上过时了XMLHttpRequest

在现代网络开发XMLHttpRequest中,出于以下三个原因:

  1. 历史原因:我们需要使用来支持现有脚本XMLHttpRequest

  2. 我们需要支持旧的浏览器,并且不需要polyfill(例如,使脚本很小)。

  3. 我们需要一些fetch尚无法做到的事情,例如跟踪上传进度。

基础

XMLHttpRequest具有两种操作模式:同步和异步。

首先让我们看看异步,因为它在大多数情况下都被使用。

要执行此请求,我们需要3个步骤:

  1. 创建XMLHttpRequest

    let xhr = new XMLHttpRequest();

    构造函数没有参数。

  2. 初始化它,通常在之后new XMLHttpRequest

    xhr.open(method, URL, [async, user, password])

    此方法指定请求的主要参数:

    请注意,open仅配置请求,真正开始发送请求的是 send

    • method– HTTP方法。通常"GET"还是"POST"

    • URL–请求的URL(字符串)可以是URL对象。

    • async–如果显式设置为false,则请求是同步的,我们稍后再讨论。

    • userpassword–基本HTTP身份验证的登录名和密码(如果需要)。

  3. 发送请求。

    xhr.send([body])

    此方法打开连接并将请求发送到服务器。可选body参数包含请求正文。

    某些请求方法例如GET没有主体。其中一些类似的POST使用body将数据发送到服务器。我们稍后将看到示例。

  4. 聆听xhr事件以作出回应。

    这三个事件使用最广泛:

    xhr.onload = function() {
      alert(`Loaded: ${xhr.status} ${xhr.response}`);};
      xhr.onerror = function() { 
      // only triggers if the request couldn't be made at all  
      alert(`Network Error`);
    };
    xhr.onprogress = function(event) { 
      // triggers periodically  
      // event.loaded - how many bytes downloaded  
      // event.lengthComputable = true if the server sent Content-Length header  
      // event.total - total number of bytes (if lengthComputable)  
      alert(`Received ${event.loaded} of ${event.total}`);
    };
    • load –请求完成后(即使HTTP状态为400或500),并且响应已完全下载。

    • error –无法发出请求时,例如网络中断或无效的URL。

    • progress –在下载响应时定期触发,报告已下载了多少。

这是一个完整的例子。下面的代码/article/xmlhttprequest/example/load从服务器上加载URL 并显示进度:

// 1. Create a new XMLHttpRequest object
let xhr = new XMLHttpRequest();

// 2. Configure it: GET-request for the URL /article/.../load
xhr.open('GET', '/article/xmlhttprequest/example/load');

// 3. Send the request over the network
xhr.send();

// 4. This will be called after the response is received
xhr.onload = function() {  
  if (xhr.status != 200) { 
  // analyze HTTP status of the response    
    alert(`Error ${xhr.status}: ${xhr.statusText}`); 
  // e.g. 404: Not Found  
  } else { 
  // show the result    
    alert(`Done, got ${xhr.response.length} bytes`); 
  // response is the server  }
};
xhr.onprogress = function(event) {  
  if (event.lengthComputable) {    
    alert(`Received ${event.loaded} of ${event.total} bytes`);  
  } else {    
    alert(`Received ${event.loaded} bytes`); // no Content-Length  
  }
};
xhr.onerror = function() {  
  alert("Request failed");
};

服务器响应后,我们可以在以下xhr属性中接收结果

  • status

      200,400,404等等这些都可以在非HTTP故障的情况下。

  • statusText

    HTTP状态消息(字符串):通常OK用于200Not Foundfor 404Forbiddenfor 403等。

  • response(旧脚本可能会使用responseText

    服务器响应主体。

我们还可以使用相应的属性指定超时:

xhr.timeout = 10000; // timeout in ms, 10 seconds

如果请求在给定时间内没有成功,它将被取消并timeout触发事件。

网址搜索参数

要向URL中添加参数(例如?name=value)并确保编码正确,我们可以使用URL对象:

let url = new URL(');

url.searchParams.set('q', 'test me!');
// the parameter 'q' is encoded

xhr.open('GET', url); 
// https://google.com/search?q=test+me%21

回应类型

我们可以使用xhr.responseTypeproperty设置响应格式:

  • "" (默认)–以字符串形式获取,

  • "text" –作为字符串,

  • "arraybuffer"–获取为ArrayBuffer(有关二进制数据,请参见ArrayBuffer,二进制数组一章),

  • "blob"–作为Blob(获取二进制数据,请参见Blob章节),

  • "document" –作为XML文档获取(可以使用XPath和其他XML方法),

  • "json" –作为JSON获取(自动分析)。

例如,让我们以JSON作为响应:

let xhr = new XMLHttpRequest();
xhr.open('GET', '/article/xmlhttprequest/example/json');
xhr.responseType = 'json';

xhr.send();
// the response is {"message": "Hello, world!"}

xhr.onload = function() {
  let responseObj = xhr.response
  alert(responseObj.message) // Hello, world!
};

请注意:

在旧脚本中,您可能还会找到xhr.responseText甚至xhr.responseXML属性。

它们由于历史原因而存在,以获取字符串或XML文档。

就绪状态

XMLHttpRequest在状态之间变化。当前状态可以通过访问   xhr.readyState

所有状态,如规范中所示

UNSENT = 0; // initial state
OPENED = 1; // open called
HEADERS_RECEIVED = 2; // response headers received
LOADING = 3; // response is loading (a data packed is received)
DONE = 4; // request complete

一个XMLHttpRequest物体移动它们的顺序0→交通1→交通2→交通3→交通...→交通3→交通43每次通过网络接收到数据包时,状态都会重复。

我们可以使用readystatechange事件跟踪它们

xhr.onreadystatechange = function() {  
  if (xhr.readyState == 3) {    
    // loading  
  }  
  if (xhr.readyState == 4) {   
   // request finished  
  }
};

您可以readystatechange在非常古老的代码中找到侦听器,这是有历史原因的,因为曾经有一段时间没有load其他事件。如今,load/error/progress处理程序已弃用它。

中止要求

我们可以随时终止请求。的调用xhr.abort()执行此操作:

xhr.abort(); // terminate the request

触发abort事件,并xhr.status变为0

同步请求

如果在open方法中将第三个参数async设置为false,则同步进行请求。

换句话说,JavaScript执行在send()接收到响应时暂停并恢复。有点像alertprompt命令。

这是重写的示例,的第三个参数openfalse

  let xhr = new XMLHttpRequest();
  xhr.open('GET', '/article/xmlhttprequest/hello.txt', false);
  try {
    xhr.send();  
    if (xhr.status != 200) {    
      alert(`Error ${xhr.status}: ${xhr.statusText}`);  
    } else {    
      alert(xhr.response);  
    }
  } catch(err) { 
    // instead of onerror  
    alert("Request failed");
  }

看起来不错,但是很少使用同步调用,因为它们会阻塞页面内JavaScript,直到加载完成。在某些浏览器中,无法滚动。如果同步呼叫花费太多时间,浏览器可能会建议关闭“挂起”的网页。

XMLHttpRequest从的其他域请求或指定超时等许多高级功能对同步请求不可用。另外,如您所见,没有进度指示。

事实上,这种同步调用极少使用。

HTTP头

XMLHttpRequest 允许发送自定义header和从响应读取header。

HTTP header有3种方法:

  • setRequestHeader(name, value)

  • 使用给定的name设置请求header value

    例如:

    xhr.setRequestHeader('Content-Type', 'application/json');

    header 限制

    几个标头仅由浏览器管理,例如RefererHost完整列表在规范中

    XMLHttpRequest 为了用户安全和请求的正确性,不允许手动更改它们。

    setRequestHeader 不是覆盖性

    的另一个特点XMLHttpRequest是不能撤消setRequestHeader

    header 设置好后,就设置好了。其他调用会将信息添加到header中,而不是覆盖。

    例如:

    xhr.setRequestHeader('X-Auth', '123');
    xhr.setRequestHeader('X-Auth', '456');
    // the header will be:// X-Auth: 123, 456
  • getResponseHeader(name)

    获取具有给定的响应标头nameSet-Cookie除外Set-Cookie2)。

    例如:

    xhr.getResponseHeader('Content-Type')
  • getAllResponseHeaders()

    返回除Set-Cookie之外的所有响应头Set-Cookie2

    header 作为一行返回,例如:

    Cache-Control: max-age=31536000
    Content-Length: 4260
    Content-Type: image/png
    Date: Sat, 08 Sep 2012 16:53:16 GMT

    标头之间的换行符始终是"\r\n"(不取决于OS),因此我们可以轻松地将其拆分为单独的标头。名称和值之间的分隔符始终是一个冒号,后跟一个空格": "这在规范中已解决。

    因此,如果要获取具有名称/值对的对象,则需要添加一些JS。

    这样(假设两个标头具有相同的名称,则后一个标头将覆盖前一个标头):

    let headers = xhr
      .getAllResponseHeaders()
      .split('\r\n')
      .reduce((result, current) => {    
         let [name, value] = current.split(': ');
         result[name] = value;    
         return result;  
      }, {});
      // headers['Content-Type'] = 'image/png'

POST,FormData

要发出POST请求,我们可以使用内置的FormData对象。

语法:

let formData = new FormData([form]); // creates an object, optionally fill from <form>
formData.append(name, value); // appends a field

我们创建它,可以选择从表单中填写,append如果需要,可以添加更多字段,然后:

  1. xhr.open('POST', ...)–使用POST方法。

  2. xhr.send(formData) 将表单提交到服务器。

例如:

<form name="person">  
  <input name="name" value="John">  <input name="surname" value="Smith">
</form>
<script>  // pre-fill FormData from the form  
  let formData = new FormData(document.forms.person);  
  
  // add one more field
  formData.append("middle", "Lee"); 
   
  // send it out  let xhr = new XMLHttpRequest();
  xhr.open("POST", "/article/xmlhttprequest/post/user");
  xhr.send(formData);

  xhr.onload = () => alert(xhr.response);
</script>

表单以multipart/form-data编码形式发送

或者,如果我们更喜欢JSON,则将其JSON.stringify作为字符串发送。

只是不要忘记设置标头Content-Type: application/json,许多服务器端框架都使用它自动解码JSON:

let xhr = new XMLHttpRequest();
let json = JSON.stringify({
  name: "John",
  surname: "Smith"});
xhr.open("POST", '/submit')
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
xhr.send(json);

.send(body)方法非常简单。它几乎可以发送body,包括BlobBufferSource对象。

上传进度

progress事件仅在下载阶段触发。

那就是:如果有POST什么事情,XMLHttpRequest首先上传我们的数据(请求主体),然后下载响应。

如果我们要上传较大的内容,那么我们肯定会对跟踪上传进度更感兴趣。但是xhr.onprogress对这里没有帮助。

还有一个没有方法的对象,专门用于跟踪上传事件:xhr.upload

它生成类似于的事件,xhrxhr.upload仅在上传时触发它们:

  • loadstart –开始上传。

  • progress –在上传期间定期触发。

  • abort –上传中止。

  • error –非HTTP错误。

  • load –上传成功完成。

  • timeout–上传超时(如果timeout设置了属性)。

  • loadend –上传成功或失败。

处理程序示例:

xhr.upload.onprogress = function(event) {  
  alert(`Uploaded ${event.loaded} of ${event.total} bytes`);
};
xhr.upload.onload = function() {  
  alert(`Upload finished successfully.`);
};
xhr.upload.onerror = function() {  
  alert(`Error during the upload: ${xhr.status}`);
};

这是一个真实的示例:带有进度指示的文件上传:

<input type="file" onchange="upload(this.files[0])">
<script>
function upload(file) {  
  let xhr = new XMLHttpRequest();  // track upload progress
  xhr.upload.onprogress = function(event) {
    console.log(`Uploaded ${event.loaded} of ${event.total}`);  
  };  // track completion: both successful or not
  xhr.onloadend = function() {    
    if (xhr.status == 200) {
      console.log("success");    
    } else {
      console.log("error " + this.status);    
    }  
  };

  xhr.open("POST", "/article/xmlhttprequest/post/upload");
  xhr.send(file);
}
</script>

跨域请求

XMLHttpRequest可以使用与fetch相同的CORS策略发出跨域请求

就像一样fetch,默认情况下它不会将cookie和HTTP授权发送到另一个来源。要启用它们,请设置xhr.withCredentialstrue

let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('POST', 'http://anywhere.com/request');...


摘要

GET-request的典型代码XMLHttpRequest

let xhr = new XMLHttpRequest();
xhr.open('GET', '/my/url');
xhr.send();
xhr.onload = function() {  
  if (xhr.status != 200) { 
    // HTTP error?    
    // handle error    
    alert( 'Error: ' + xhr.status);    
    return;  
  }  
  // get the response from xhr.response
};
xhr.onprogress = function(event) {  
  // report progress  
  alert(`Loaded ${event.loaded} of ${event.total}`);
};
xhr.onerror = function() {  
// handle non-HTTP error (e.g. network down)
};

实际上还有更多事件,列出(按生命周期顺序):

  • loadstart –请求已开始。

  • progress–响应的数据包已到达,此刻整个响应主体都在response

  • abort–该请求已被呼叫取消xhr.abort(),不同于http request aborted

  • error–发生连接错误,例如域名错误。不会发生HTTP错误(例如404)。

  • load –请求已成功完成。

  • timeout –由于超时,请求被取消(仅在已设置的情况下发生)。

  • loadend-触发后loaderrortimeoutabort

erroraborttimeout,和load事件是相互排斥的。其中只有一种可能发生。

最常用的事件是加载完成(load),加载失败(error),或者我们可以使用单个loadend处理程序并检查请求对象的属性xhr以查看发生了什么。

我们已经看到了另一个事件:readystatechange从历史上看,它早在规范制定之前就出现了。如今,无需使用它,我们可以用较新的事件替换它,但通常可以在旧脚本中找到它。

如果我们需要专门跟踪上传,那么我们应该在xhr.upload对象上监听相同的事件

这里我们补充一种情况
当我们发送一个异步请求,而请求还没得到响应的时候,页面被重定向了,上面生命周期的方法会被trigger吗?
这个跟各大浏览器的核心相关,不同浏览器在这点上有不同表现,如chrome上页面被重定向后是不会再trigger上面生命周期事件的,而在safari上,在 http 状态被aborted的时候,safari依然会触发其中的error事件。开发者面对这种不同机制带来的处理上的区别,只能耗费更多的时间用于兼容性调试上了。