ajax是一种技术方案,但并不是一种新技术。它依赖的是现有的CSS/HTML/Javascript,而其中最核心的依赖是浏览器提供的XMLHttpRequest对象,是这个对象使得浏览器可以发出HTTP请求与接收HTTP响应。
XMLHttpRequest一开始只是微软浏览器提供的一个接口,后来各大浏览器纷纷效仿也提供了这个接口,再后来W3C对它进行了标准化,提出了XMLHttpRequest标准。XMLHttpRequest标准又分为Level 1和Level 2。
XMLHttpRequest Level 1主要存在以下缺点:

  • 受同源策略的限制,不能发送跨域请求;
  • 不能发送二进制文件(如图片、视频、音频等),只能发送纯文本数据;
  • 在发送和获取数据的过程中,无法实时获取进度信息,只能判断是否完成;
    那么Level 2对Level 1 进行了改进,XMLHttpRequest Level 2中新增了以下功能:
  • 可以发送跨域请求,在服务端允许的情况下;
  • 支持发送和接收二进制数据;
  • 新增formData对象,支持发送表单数据;
  • 发送和获取数据时,可以获取进度信息;
    可以设置请求的超时时间;

FormData

FormData对象用以将数据编译成键值对,以便用XMLHttpRequest来发送数据。
可以通过两种方式调用FormData对象:

1
2
3
var formData = new FormData();
formData.append("username", "fyt");
formData.append("password", 123456); //数字123456会被立即转换成字符串 "123456"

或是获取表单对象,传入FormData构造函数参数中:

1
2
3
4
var formElement = document.querySelector("form");
var request = new XMLHttpRequest();
request.open("POST", "submitform.php");
request.send(new FormData(formElement));

FormData可以用来上传文件。

发送的数据类型

xhr.send(data)的参数data可以是以下几种类型:

  • ArrayBuffer
  • Blob
  • Document
  • DOMString
  • FormData
  • null
    xhr.send(data)中data参数的数据类型会影响请求头部content-type的默认值:
  • 如果data是 Document 类型,同时也是HTML Document类型,则content-type默认值为text/html;charset=UTF-8;否则为application/xml;charset=UTF-8;
  • 如果data是 DOMString 类型,content-type默认值为text/plain;charset=UTF-8;
  • 如果data是 FormData 类型,content-type默认值为multipart/form-data; boundary=[xxx]
  • 如果data是其他类型,则不会设置content-type的默认值
    当然这些只是content-type的默认值,但如果用xhr.setRequestHeader()手动设置了中content-type的值,以上默认值就会被覆盖。
    另外需要注意的是,若在断网状态下调用xhr.send(data)方法,则会抛错:Uncaught NetworkError: Failed to execute ‘send’ on ‘XMLHttpRequest’。一旦程序抛出错误,如果不 catch 就无法继续执行后面的代码,所以调用 xhr.send(data)方法时,应该用 try-catch捕捉错误。
    1
    2
    3
    4
    5
    try{
    xhr.send(data)
    }catch(e) {
    //doSomething...
    };

接收的数据类型

responseType是xhr level 2新增的属性,用来指定xhr.response的数据类型,目前还存在些兼容性问题,可以参考本文的【XMLHttpRequest的兼容性】这一小节。那么responseType可以设置为哪些格式呢,我简单做了一个表,如下:
| 值 | xhr.response数据类型 | 说明 |
| :—-: | :—–: | :—–: |
| “” | String字符串 | 默认值(在不设置responseType时) |
| “text” | String字符串 |
| “document” | Document对象 | 希望返回 XML 格式数据时使用 |
| “json” | javascript 对象 | 存在兼容性问题,IE10/IE11不支持 |
| “blob” | Blob对象 |
| “arrayBuffer”| ArrayBuffer对象 |
比如获取二进制图片可以使用如下写法:

1
2
3
4
5
6
7
8
9
10
11
12
var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
//可以将`xhr.responseType`设置为`"blob"`也可以设置为`" arrayBuffer"`
//xhr.responseType = 'arrayBuffer';
xhr.responseType = 'blob';
xhr.onload = function(e) {
if (this.status == 200) {
var blob = this.response;
...
}
};
xhr.send();

如何获取response数据

xhr提供了3个属性来获取请求返回的数据,分别是:xhr.response、xhr.responseText、xhr.responseXML
xhr.response

  • 默认值:空字符串””
  • 当请求完成时,此属性才有正确的值
  • 请求未完成时,此属性的值可能是””或者 null,具体与 xhr.responseType有关:当responseType为””或”text”时,值为””;responseType为其他值时,值为 null
    xhr.responseText
  • 默认值为空字符串””
  • 只有当 responseType 为”text”、””时,xhr对象上才有此属性,此时才能调用xhr.responseText,否则抛错
  • 只有当请求成功时,才能拿到正确值。以下2种情况下值都为空字符串””:请求未完成、请求失败
    xhr.responseXML
  • 默认值为 null
  • 只有当 responseType 为”text”、””、”document”时,xhr对象上才有此属性,此时才能调用xhr.responseXML,否则抛错
  • 只有当请求成功且返回数据被正确解析时,才能拿到正确值。以下3种情况下值都为null:请求未完成、请求失败、请求成功但返回数据无法被正确解析时

如何追踪ajax请求的当前状态

在发一个ajax请求后,如果想追踪请求当前处于哪种状态,该怎么做呢?
用xhr.readyState这个属性即可追踪到。这个属性是只读属性,总共有5种可能值,分别对应xhr不同的不同阶段。每次xhr.readyState的值发生变化时,都会触发xhr.onreadystatechange事件,我们可以在这个事件中进行相关状态判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xhr.onreadystatechange = function () {
switch(xhr.readyState){
case 1://OPENED
//do something
break;
case 2://HEADERS_RECEIVED
//do something
break;
case 3://LOADING
//do something
break;
case 4://DONE
//do something
break;
}
}

对于readyState一共有0、1、2、3、4这五种状态码。
| 值 | 状态 | 描述 |
| :—-: | :—–: | :—–: |
| 0 | UNSENT (初始状态,未打开) | 此时xhr对象被成功构造,open()方法还未被调用 |
| 1 | OPENED (已打开,未发送) | open()方法已被成功调用,send()方法还未被调用。注意:只有xhr处于OPENED状态,才能调用xhr.setRequestHeader()和xhr.send(),否则会报错 |
| 2 | HEADERS_RECEIVED (已获取响应头) | send()方法已经被调用, 响应头和响应状态已经返回 |
| 3 | LOADING (正在下载响应体) | 响应体(response entity body)正在下载中,此状态下通过xhr.response可能已经有了响应数据 |
| 4 | DONE (整个数据传输过程结束) | 整个数据传输过程结束,不管本次请求是成功还是失败 |

timeout事件何时触发

如果请求过了很久还没有成功,为了不会白白占用的网络资源,我们一般会主动终止请求。XMLHttpRequest提供了timeout属性来允许设置请求的超时时间:xhr.timeout。
当xhr.onloadstart事件触发的时候,也就是你调用xhr.send()方法的时才算是请求开始。从请求开始 算起,若超过 timeout 时间请求还没有结束(包括成功/失败),则会触发ontimeout事件,主动结束该请求。
因为xhr.open()只是创建了一个连接,但并没有真正开始数据的传输,而xhr.send()才是真正开始了数据的传输过程。只有调用了xhr.send(),才会触发xhr.onloadstart 。
xhr.loadend事件触发时才算是请求结束。
另外,还有2个需要注意的坑儿:

  1. 可以在 send()之后再设置此xhr.timeout,但计时起始点仍为调用xhr.send()方法的时刻。
  2. 当xhr为一个sync同步请求时,xhr.timeout必须置为0,否则会抛错。

如何获取上传、下载的进度

在上传或者下载比较大的文件时,实时显示当前的上传、下载进度是很普遍的产品需求。
我们可以通过onprogress事件来实时显示进度,默认情况下这个事件每50ms触发一次。需要注意的是,上传过程和下载过程触发的是不同对象的onprogress事件:

  • 上传触发的是xhr.upload对象的onprogress事件
  • 下载触发的是xhr对象的onprogress事件
1
2
3
4
5
6
7
xhr.onprogress = updateProgress;
xhr.upload.onprogress = updateProgress;
function updateProgress(event) {
if (event.lengthComputable) {
var completedPercent = event.loaded / event.total;
}
}

xhr.withCredentials与 CORS 的关系

造成这个问题的原因是:在CORS标准中做了规定,默认情况下,浏览器在发送跨域请求时,不能发送任何认证信息(credentials)如”cookies”和”HTTP authentication schemes”。除非xhr.withCredentials为true(xhr对象有一个属性叫withCredentials,默认值为false)。
所以根本原因是cookies也是一种认证信息,在跨域请求中,client端必须手动设置xhr.withCredentials=true,且server端也必须允许request能携带认证信息(即response header中包含Access-Control-Allow-Credentials:true),这样浏览器才会自动将cookie加在request header中。

xhr相关事件

  1. XMLHttpRequestEventTarget接口定义了7个事件:onloadstart、onprogress、onabort、ontimeout、onerror、onload、onloadend
  2. 每一个XMLHttpRequest里面都有一个upload属性,而upload是一个XMLHttpRequestUpload对象
  3. XMLHttpRequest和XMLHttpRequestUpload都继承了同一个XMLHttpRequestEventTarget接口,所以xhr和xhr.upload都有第一条列举的7个事4. 4. onreadystatechange是XMLHttpRequest独有的事件
    所以这么一看就很清晰了:
    xhr一共有8个相关事件:7个XMLHttpRequestEventTarget事件+1个独有的onreadystatechange事件;而xhr.upload只有7个XMLHttpRequestEventTarget事件。
    下面讲一讲事件触发的条件:
    | 事件 | 触发条件 |
    | :—-: | :—–: |
    | onreadystatechange | 每当xhr.readyState改变时触发;但xhr.readyState由非0值变为0时不触发。 |
    | onloadstart | 调用xhr.send()方法后立即触发,若xhr.send()未被调用则不会触发此事件。 |
    | onprogress | xhr.upload.onprogress在上传阶段(即xhr.send()之后,xhr.readystate=2之前)触发,每50ms触发一次;xhr.onprogress在下载阶段(即xhr.readystate=3时)触发,每50ms触发一次。 |
    | onload | 当请求成功完成时触发,此时xhr.readystate=4 |
    | onloadend | 当请求结束(包括请求成功和请求失败)时触发 |
    | onabort | 当调用xhr.abort()后触发 |
    | ontimeout | xhr.timeout不等于0,由请求开始即onloadstart开始算起,当到达xhr.timeout所设置时间请求还未结束即onloadend,则触发此事件。 |
    | onerror | 在请求过程中,若发生Network error则会触发此事件(若发生Network error时,上传还没有结束,则会先触发xhr.upload.onerror,再触发xhr.onerror;
    若发生Network error时,上传已经结束,则只会触发xhr.onerror)。注意,只有发生了网络层级别的异常才会触发此事件,对于应用层级别的异常,如响应返回的xhr.statusCode是4xx时,并不属于Network error,所以不会触发onerror事件,而是会触发onload事件。 |
  • 当请求一切正常时,相关的事件触发顺序如下:
    触发xhr.onreadystatechange(之后每次readyState变化时,都会触发一次)
    触发xhr.onloadstart
    //上传阶段开始:
    触发xhr.upload.onloadstart
    触发xhr.upload.onprogress
    触发xhr.upload.onload
    触发xhr.upload.onloadend
    //上传结束,下载阶段开始:
    触发xhr.onprogress
    触发xhr.onload
    触发xhr.onloadend
  • 发生abort/timeout/error异常的处理
    在请求的过程中,有可能发生 abort/timeout/error这3种异常。那么一旦发生这些异常,xhr后续会进行哪些处理呢?后续处理如下:
    一旦发生abort或timeout或error异常,先立即中止当前请求
    将 readystate 置为4,并触发 xhr.onreadystatechange事件
    如果上传阶段还没有结束,则依次触发以下事件:xhr.upload.onprogress、xhr.upload.[onabort或ontimeout或onerror]、xhr.upload.onloadend
    触发 xhr.onprogress事件
    触发 xhr.[onabort或ontimeout或onerror]事件
    触发xhr.onloadend 事件

在哪个xhr事件中注册成功回调?

从上面介绍的事件中,可以知道若xhr请求成功,就会触发xhr.onreadystatechange和xhr.onload两个事件。 那么我们到底要将成功回调注册在哪个事件中呢?我倾向于 xhr.onload事件,因为xhr.onreadystatechange是每次xhr.readyState变化时都会触发,而不是xhr.readyState=4时才触发。

1
2
3
4
5
6
xhr.onload = function () {
//如果请求成功
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
//do successCallback
}
}