合抱之木生于毫末,九层之台起于累土,千里之行始于足下

XMLHttpRequest的属性和方法


XMLHttpRequest的属性和方法

Ajax技术的出现是为了解决发送服务器请求额外数据而不刷新整张页面的问题,从而实现更好的用户体验。把Ajax技术推到历史舞台上的关键技术是XMLHttpRequest对象。最早由微软发明,然后被其他浏览器所借鉴。

在Ajax技术出现之前,这种浏览器与服务器的通信早在1998年就通过不同方式实现了。最初JavaScript对服务器的请求可以通过中介(如Java小程序或Flash影片)来发送。后来XHR对象提供了原生的浏览器通信能力,减少了实现这个目的的工作量。

由于XMLHttpRequest对象本身非常难用,所以以往我们看到很多库实现了封装来帮助我们更好地使用它。为了了解更多XMLHttpRequest的细节,今天我们就来了解一下XMLHttpRequest的属性、方法和事件,帮助大家了解Ajax的底层能力。

declare var XMLHttpRequest: {
    prototype: XMLHttpRequest;
    new(): XMLHttpRequest;
    readonly DONE: number;
    readonly HEADERS_RECEIVED: number;
    readonly LOADING: number;
    readonly OPENED: number;
    readonly UNSENT: number;
}

属性

readyState属性(只读)

该属性表示当前所处状态。

状态说明
0UNSENT未初始化状态,XMLHttpRequest已被创建,但尚未调用open方法
1OPENED发送状态,open方法已被触发,在这个状态中,可以通过setRequestHeader方法来设置请求的头部,可以通过send方法来发起请求
2HEADERS_RECEIVED发送状态,send方法已被触发,响应头也已经被接收
3LOADING正在接收状态,响应体部分正在被接收
4DONE接受完全状态,数据传输已经彻底完成或者失败
var xhr = new XMLHttpRequest();
console.log('UNSENT', xhr.readyState); // readyState 为 0

xhr.onprogress = function () {
    console.log('LOADING', xhr.readyState); // readyState 为 3
};

xhr.onload = function () {
    console.log('DONE', xhr.readyState); // readyState 为 4
};

xhr.open('GET', '/api', true);
console.log('OPENED', xhr.readyState); // readyState 为 1

// 在open方法和send方法之间执行setRequestHeader,以保证Header设置有效
xhr.setRequestHeader('x-token', 'test_token');

xhr.send(null);

status属性(只读)

返回XMLHttpRequest请求中由服务器返回的数字状态码。

注意

  1. 在请求完成前,状态码始终为0。
  2. 如果XMLHttpRequest出错,浏览器返回的状态码也为0。
  3. 如果请求正常执行,状态码与标准的HTTP状态码一致。
/**
 * 输出如下:
 *
 * UNSENT(未发送)0
 * OPENED(已打开)0
 * LOADING(载入中)200
 * DONE(完成)200
 */
var xhr = new XMLHttpRequest();
console.log('UNSENT', xhr.status); // status 为 0

xhr.onprogress = function () {
  console.log('LOADING', xhr.status); // status 为 200
};

xhr.onload = function () {
  console.log('DONE', xhr.status); // status 为 200
};

xhr.open('GET', '/server', true);
console.log('OPENED', xhr.status); // status 为 0

xhr.send(null);

statusText属性(只读)

返回XMLHttpRequest请求中由服务器返回的字符串状态信息,这则信息中也包含了响应的数字状态码。不同于使用一个数字来指示的状态码,这个属性包含了返回状态对应的文本信息,例如“OK”或者“Not Found”。

注意

  1. 如果请求的状态readyState的值为UNSENT或者OPENED,则这个属性的值将会是一个空字符串。
  2. 如果服务器未明确指定一个状态文本信息,则statusText的值将会被默认赋值为”OK”。

responseType属性

该属性是一个枚举字符串值,用于指定响应中包含的数据类型。它允许作者更改响应类型。

可能得值:

response属性(只读)

当请求体响应时,该属性会接收到请求体返回的数据。具体类型取决于responseType的值。

可能的响应类型有ArrayBufferBlobDocumentDOMString

responseText属性(只读)

当请求体响应时,该属性会接收到请求体返回的数据。

responseXML属性(只读)

当请求体响应时,该属性返回一个包含请求检索的HTML或XML的Document。(根据Content-Typetext/html或者application/xml判断)

注意

  1. 如果请求未成功,尚未发送,或者检索的数据无法解析为XML或者HTML,则为null。
  2. 如果服务器没有明确指出Content-Type头,可以使用overrideMimeType方法强制XMLHttpRequest解析为XML。

responseURL属性(只读)

当请求体响应时,返回序列化URL。

注意

  1. 如果URL为空,则返回空字符串。
  2. 如果URL有锚点,则位于URL#后面的内容会被删除。
  3. 如果URL有重定向,responseURL的值会是经过多次重定向后的最终URL。

timeout属性

代表着一个请求在被自动终止前所消耗的毫秒数。默认为0,意味着没有超时。

注意

  1. 超时并不应该用在一个全局文档环境中的同步XMLHttpRequest请求中,否则将会抛出一个InvalidAccessError类型的错误。
  2. 当超时发生时,timeout事件将被触发。
var xhr = new XMLHttpRequest();

xhr.onload = function () {
  // 请求完成。在此进行处理。
};
xhr.ontimeout = function (e) {
  // XMLHttpRequest 超时。在此做某事。
};
xhr.open('GET', '/server', true);
xhr.timeout = 2000; // 超时时间,单位是毫秒
xhr.send(null);

upload属性(只读)

返回一个XMLHttpRequestUpload对象,用来表示上传的进度。这个对象时不透明的,但是作为一个XMLHttpRequestEventTarget,可以通过对其绑定事件来追踪它的进度。

可以被绑定在upload对象上的事件监听器如下:

事件相应属性的信息类型
onloadstart获取开始
onprogress数据传输进行中
onabort获取操作终止
onerror获取失败
onload获取成功
ontimeout获取操作在用户规定的时间内未完成
onloadend获取完成(无论成功与否)
interface XMLHttpRequestEventTarget extends EventTarget {
    onabort: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
    onerror: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
    onload: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
    onloadend: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
    onloadstart: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
    onprogress: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
    ontimeout: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
    addEventListener<K extends keyof XMLHttpRequestEventTargetEventMap>(type: K, listener: (this: XMLHttpRequestEventTarget, ev: XMLHttpRequestEventTargetEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
    addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
    removeEventListener<K extends keyof XMLHttpRequestEventTargetEventMap>(type: K, listener: (this: XMLHttpRequestEventTarget, ev: XMLHttpRequestEventTargetEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
    removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}

interface XMLHttpRequestUpload extends XMLHttpRequestEventTarget {
    addEventListener<K extends keyof XMLHttpRequestEventTargetEventMap>(type: K, listener: (this: XMLHttpRequestUpload, ev: XMLHttpRequestEventTargetEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
    addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
    removeEventListener<K extends keyof XMLHttpRequestEventTargetEventMap>(type: K, listener: (this: XMLHttpRequestUpload, ev: XMLHttpRequestEventTargetEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
    removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}

withCredentials属性

该属性是一个布尔类型,它指示了是否该使用类似Cookies、Authorization Headers(头部授权)或者TLS客户端证书这类资格证书来创建一个跨站点访问控制(cross-siteAccess-Control)请求。

注意

  1. 在同一站点下使用withCredentials属性是无效的。
  2. 这个指示会被用做响应中Cookies被忽视的标识。默认是false。
  3. 不同域下的XMLHttpRequest响应,无论其Access-Control- Header设置什么值,都无法为它自身站点设置Cookie值(除非请求之前withCredentials设置为true)。
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/', true);
xhr.withCredentials = true; // 在发出请求之前设置为 true
xhr.send(null);

方法

open方法

该方法接收三个参数:请求类型(GET、POST等)、请求的URL、是否异步。

需要注意的是,URL是相对于代码所在的页面的,当然你也可以使用绝对URL。另外,调用open方法并不会实际发送请求,只是为发送请求做好准备。

注意:请求默认情况下只能访问同源URL(域名相同、端口相同、协议相同)。如果请求的URL于发送请求的页面在任何方面有所不同,则会抛出安全错误。

注意:由于同步的请求会阻塞JavaScript执行,所以一般情况下,我们不建议使用同步请求的方式。

send方法

该方法接收一个参数:要发送的数据。如果不需要发送任何数据,则必须传null(因为该参数在某些浏览器中是必需的)。调用send方法后,请求就会发送到服务器。

getRequestHeader方法

该方法接收一个参数:Header的名称。返回包含指定响应头文本的字符串。

注意

  1. 如果在返回的响应头中有多个一样的名称,那么返回的值就会是用逗号和空格将值分隔的字符串。该方法以UTF编码返回值。
  2. 搜索的报文名是不区分大小写的。
  3. 如果连接未完成(响应中不存在报文项或者被W3C限制),则返回null。

setRequestHeader方法

设置HTTP请求头部的方法。

注意

  1. 该方法必须在open方法和send方法之间调用。
  2. 如果多次对同一个请求头赋值,只会生成一个合并了多个值的请求头。
  3. 如果没有设置Accept属性,那么这个请求头会被默认设置为*/*
  4. 安全起见,有些请求头的值只能由UserAgent设置:禁止修改的标头禁止修改的响应标头
  5. 自定义的header属性进行跨域请求时,可能会遇到”not allowed by Access-Control-Allow-Headers in preflight response”,你可能需要再你的服务器设置”Access-Control-Allow-Headers”。

getAllResponseHeaders方法

返回所有的响应头,以CRLF分割的字符串。如果没有收到任何响应,则返回null

注意:对于复合请求,这个方法返回的当前请求的头部,而不是最初的请求头的头部。

var request = new XMLHttpRequest();

// 为了兼容所有浏览器,该事件处理函数必须放在open方法执行之前
request.onreadystatechange = function() {
  if(this.readyState == this.HEADERS_RECEIVED) {

    // Get the raw header string
    var headers = request.getAllResponseHeaders();

    // Convert the header string into an array
    // of individual headers
    var arr = headers.trim().split(/[\r\n]+/);

    // Create a map of header names to values
    var headerMap = {};
    arr.forEach(function (line) {
      var parts = line.split(': ');
      var header = parts.shift();
      var value = parts.join(': ');
      headerMap[header] = value;
    });
  }
}
request.open("GET", "foo.txt", true);
request.send();

// Headers For Example
// date: Fri, 08 Dec 2017 21:04:30 GMT\r\n
// content-encoding: gzip\r\n
// x-content-type-options: nosniff\r\n
// server: meinheld/0.6.1\r\n
// x-frame-options: DENY\r\n
// content-type: text/html; charset=utf-8\r\n
// connection: keep-alive\r\n
// strict-transport-security: max-age=63072000\r\n
// vary: Cookie, Accept-Encoding\r\n
// content-length: 6502\r\n
// x-xss-protection: 1; mode=block\r\n

overrideMimeType方法

该方法是指一个MIME类型用户替代服务器指定的类型,使服务端响应信息中传输的数据按照指定MIME类型处理。

注意

  1. 此方法必须在send方法之前调用方为有效。
  2. 如果服务器没有指定一个Content-Type头,MIME类型默认设置为text/xml。如果接受的数据不是有效的XML,将会出现“格式不正确”的错误。你能够通过调用overrideMimeType方法指定各种类型来避免这种情况。
// Interpret the received data as plain text

req = new XMLHttpRequest();
req.overrideMimeType("text/plain");
req.addEventListener("load", callback, false);
req.open("get", url);
req.send();

abort方法

如果请求已被发出,该方法可以终止请求。当一个请求被终止,它的readyState将被置为XMLHttpRequest.UNSENT(0),并且请求的status置为0。

注意:如果请求已经达到服务器,服务器相关Controller方法并不会停止执行,只会造成执行结果无法返回,并且服务器会有报错提示。

var xhr = new XMLHttpRequest(),
    method = "GET",
    url = "https://developer.mozilla.org/";
xhr.open(method, url, true);

xhr.send();

if (OH_NOES_WE_NEED_TO_CANCEL_RIGHT_NOW_OR_ELSE) {
  xhr.abort();
}

事件

事件响应顺序:loadstart -> progress -> (error/abort/load) -> loadend

interface XMLHttpRequestEventTargetEventMap {
    "abort": ProgressEvent<XMLHttpRequestEventTarget>;
    "error": ProgressEvent<XMLHttpRequestEventTarget>;
    "load": ProgressEvent<XMLHttpRequestEventTarget>;
    "loadend": ProgressEvent<XMLHttpRequestEventTarget>;
    "loadstart": ProgressEvent<XMLHttpRequestEventTarget>;
    "progress": ProgressEvent<XMLHttpRequestEventTarget>;
    "timeout": ProgressEvent<XMLHttpRequestEventTarget>;
}

interface XMLHttpRequestEventTarget extends EventTarget {
    onabort: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
    onerror: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
    onload: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
    onloadend: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
    onloadstart: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
    onprogress: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
    ontimeout: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null;
    addEventListener<K extends keyof XMLHttpRequestEventTargetEventMap>(type: K, listener: (this: XMLHttpRequestEventTarget, ev: XMLHttpRequestEventTargetEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
    addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
    removeEventListener<K extends keyof XMLHttpRequestEventTargetEventMap>(type: K, listener: (this: XMLHttpRequestEventTarget, ev: XMLHttpRequestEventTargetEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
    removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}

XMLHttpRequest支持DOM 0级事件处理机制DOM 2级事件处理机制

DOM 0级事件

DOM 2级事件

注意:为保证兼容性,所有事件都应在open方法执行之前添加好。

跨源资源共享

通过XHR进行Ajax通信的一个主要限制跨源安全策略。这个安全限制是为了防止某些恶意行为。而跨源资源共享(CORS)定义了浏览器与服务器之间如何实现跨源通信。

跨源资源共享的原理是通过HTTP头部信息,允许浏览器和服务器相互了解,以确实请求或响应应该成功还是失败。最基本的头部信息是origin字段。该字段包含发送请求的页面源(协议、域名和端口),以便服务器确定是否为其提供响应。

为了安全起见,跨域XHR对象施加了一些额外限制:

  1. 不能使用setRequestHeader方法设置自定义头部。
  2. 不能发送和接收Cookie
  3. getAllResponseHeaders方法始终返回空字符串。

CORS通过一种叫预检请求的服务器验证机制。以下情况会触发预检机制:

  1. 自定义头部
  2. 除GET和POST之外的方法
  3. 不同请求体内容类型

预检请求使用OPTIONS方法,并包含以下头部:

这个请求发送后,服务器可以确定是否允许这种类型的请求。在响应中,服务器会返回如下头部信息:

上面说到了CORS请求默认情况下会存在一些限制,可以通过将withCredentials属性设置为true来表明请求会发送凭据(Cookie、HTTP认证和客户端SSL证书)。如果服务器允许带凭据的请求,那么可以在响应中包含如下HTTP头部:

如果发送了凭据请求而服务器返回的响应中没有这个头部,则浏览器不会把响应交给JavaScript(responseText为空,status是0,onerror被触发)。

早期的跨域方案

在没有CORS之前,开发者需要利用DOM特性发送跨域请求。

注意事项

  1. 对于HTTP请求状态的判断,请使用status而不是statusText,因为后者已经被证明在跨浏览器的情况下不可靠。
  2. 无论是什么响应内容类型,responseText属性始终都会保存响应体,而responseXML属性不一定。
  3. response属性会根据responseType来自动序列化数据。目前来看,兼容性不成问题,所以推荐使用
  4. 为了保证跨浏览器兼容,onreadystatechange事件处理程序应该在open方法之前赋值。
  5. 为了保证请求头部被发送,setRequestHeader方法必须在open方法之后send方法之前被调用。
  6. POST请求相比GET请求要占用更多的资源。从性能方面说,发送相同数量的数据,GET请求比POST请求要快两倍。
  7. Content-Type如果是application/x-www-formurlencoded,使用URLSearchParams来创建数据;如果是multipart/form-data,使用FormData来创建数据。

参考