一个ajax请求库的实现

本类库脚手架基于lime-cli创建的felib模板进行开发,该套脚手架模板提供了基于webpack的打包构建流程。了解脚手架详情请参考博客:

本类库已经开源于github https://github.com/cuiyongjian/lime-ajax

ajax

ajax是一种异步请求的技术方案,不是特指XMLHttpRequest。它并不是一种新技术。它依赖的是现有的CSS/HTML/Javascript,而其中最核心的依赖是浏览器提供的XMLHttpRequest对象,是这个对象使得浏览器可以发出HTTP请求与接收HTTP响应。所以两者的关系是:我们使用XMLHttpRequest对象来发送一个Ajax请求。

XHR标准

XHR有level1和level2两个标准

level1

  • 受同源策略的限制,不能发送跨域请求;(之前跨域是通过jsonp的方式来搞的)
  • 不能发送二进制文件(如图片、视频、音频等),只能发送纯文本数据;
  • 在发送和获取数据的过程中,无法实时获取进度信息,只能判断是否完成;

level2

新增了如下功能

  • 可以发送跨域请求,在服务端允许的情况下(CORS配合);
  • 支持发送和接收二进制数据;
  • 新增formData对象,支持发送表单数据(不仅不需要自己拼字符串,而且可以传递表单文件上传类型的文件);
  • 发送和获取数据时,可以获取进度信息;
  • 可以设置请求的超时时间;

兼容性参考: http://caniuse.com/#search=XMLHttpRequest

使用XHR发送ajax请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function ajax () {
var f = new FormData()
f.append('username', 'sheldon')
f.append('password', 'notellu')
var xhr = new XMLHttpRequest()
xhr.timeout = 3000
xhr.responseType = 'text'
xhr.open('post', '/testapi')
xhr.onload = function (e) {
if (this.status === 200 || this.status === 304) {
console.log('success', this.responseText)
}
}
xhr.ontimeout = function(e) { ... };
xhr.onerror = function(e) { ... };
xhr.upload.onprogress = function(e) { ... };
xhr.send(f)
}

设置请求头

xhr.setRequestHeader
函数定义为:
void setRequestHeader(DOMString header, DOMString value)

注意点:

  • 方法的第一个参数 header 大小写不敏感,即可以写成content-type,也可以写成Content-Type,甚至写成content-Type;
  • Content-Type的默认值与具体发送的数据类型有关,请参考本文【可以发送什么类型的数据】一节;
  • setRequestHeader必须在open()方法之后,send()方法之前调用,否则会抛错;
  • setRequestHeader可以调用多次,最终的值不会采用覆盖override的方式,而是采用追加append的方式。下面是一个示例代码:

设置请求头可以用来模拟表单提交,比如表单提交都是application/x-www-form-urlencode形式的提交,这样那些大部分的传统的针对表单提交进行解析的webserver就能处理你的请求了。当然,发送formData的方式,应该会自动设置为这种请求头了。

获取响应头

有两个函数,getAllResponseHeaders() 和 getResponseHeader(header)。

但是要注意,getAllResponseHeaders()只能拿到限制以外(即被视为safe)的header字段,而不是全部字段;而调用getResponseHeader(header)方法时,header参数必须是限制以外的header字段,否则调用就会报Refused to get unsafe header的错误。

W3C的 xhr 标准中做了限制,规定客户端无法获取 response 中的 Set-Cookie、Set-Cookie2这2个字段,无论是同域还是跨域请求;
而跨域请求更加严格,除了set-cookie,客户端允许获取的response header字段只限于“simple response header”和“Access-Control-Expose-Headers。如 Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma;

设置响应格式

对xhr的响应格式提前进行设置,可以让XHR对象自动帮我们转换响应内容。这样我们可以在xhr中直接读取到自己想要的格式,而不需要自己转换。

先来看看比较方便的XHR level2,他提供了xhr.reponseType属性,可以支持多种类型的设置:


这样的话,获取服务器的一张图片,并在前端拿到二进制格式的数据,就如此简单了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();

xhr.responseType取代了xhr.overrideMimeType()

我们来看下XHR level1怎么处理的:

  • 如果希望从xhr.reponse里读到dom对象,(让服务端返回的xml格式自动转换为dom对象),则需要这样: xhr.overrideMimeType(‘text/xml; charset = utf-8’)

如果要用leve1读取一个二进制的东西,那就不能让XHR对象给你自动解析成字符串,因为XHR给你解析的话很可能把返回的字节流当成UTF8或其他编码的数据,然后解析成内存中的UCS或UTF16字符,显然已经破坏了原有二进制的数据。

解决方案就是不让XHR给你解析这些二进制流,或者是说让XHR按照单个字节就是一个字符的形式来解析你的流。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var xhr = new XMLHttpRequest();
//向 server 端获取一张图片
xhr.open('GET', '/path/to/image.png', true);

// 这行是关键!
//将响应数据按照纯文本格式来解析,字符集替换为用户自己定义的字符集
xhr.overrideMimeType('text/plain; charset=x-user-defined');

xhr.onreadystatechange = function(e) {
if (this.readyState == 4 && this.status == 200) {
//通过 responseText 来获取图片文件对应的二进制字符串
var binStr = this.responseText;
//然后自己再想方法将逐个字节还原为二进制数据
for (var i = 0, len = binStr.length; i < len; ++i) {
var c = binStr.charCodeAt(i);
//String.fromCharCode(c & 0xff);
var byte = c & 0xff;
}
}
};

xhr.send();

获取响应内容

上面已经看到了,可以通过xhr.response拿到响应内容。而其他的responseText、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:请求未完成、请求失败、请求成功但返回数据无法被正确解析时

XHR的状态跟踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xhr.onreadystatechange = function () {
switch(xhr.readyState){
case 1://OPENED,此时send还没调用。
//do something
break;
case 2://HEADERS_RECEIVED。 send()方法已经被调用, 响应头和响应状态已经返回
//do something
break;
case 3://LOADING,响应体(response entity body)正在下载中,此状态下通过xhr.response可能已经有了响应数据
//do something
break;
case 4://DONE, 整个数据传输过程结束,不管本次请求是成功还是失败,所以还要检测xhr.status
//do something
break;
}

0表示的是xhr对象被成功构造,open()方法还未被调用。也就是new之后了。
只有xhr处于OPENED状态,才能调用xhr.setRequestHeader()和xhr.send(),否则会报错

xhr.timeout可以设置请求超时时间,超时是从xhr.send开始算起的。
send调用时,会触发onloadstart事件,timeout超时后会触发ontimeout事件,用不超时则设置timeoout为0.

send() 发送请求格式与content-type

send支持的类型:

  • ArrayBuffer
  • Blob
  • Document
  • DOMString
  • FormData
  • null
  1. 如果data是 Document 类型,同时也是HTML Document类型,则content-type默认值为text/html;charset=UTF-8;否则为application/xml;charset=UTF-8;
  2. 如果data是 DOMString 类型,content-type默认值为text/plain;charset=UTF-8;
  3. 如果data是 FormData 类型,content-type默认值为multipart/form-data; boundary=[xxx]
  4. 如果data是其他类型,则不会设置content-type的默认值
  5. 当然这些只是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...
};

上传下载进度

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.upload对象的 onprogress事件
  • 下载触发的是xhr对象的onprogress事件

xhr对象的事件

7个XMLHttpRequestEventTarget事件+1个独有的onreadystatechange事件;而xhr.upload只有7个XMLHttpRequestEventTarget事件。

  • onloadstart
  • onprogress
  • onabort
  • ontimeout
  • onerror
  • onload
  • onloadend

在onload里注册成功的回调

若xhr请求成功,就会触发xhr.onreadystatechange和xhr.onload两个事件。 那么我们到底要将成功回调注册在哪个事件中呢?因为xhr.onreadystatechange是每次xhr.readyState变化时都会触发,而不是xhr.readyState=4时才触发,所以最好用onload,

因为readyState == 4,就相当于onload,他们是等价的,所以onload避免了自己去判断xhr.readyState状态。但在load之后,我们判断xhr.status的http状态码时,最好的实践应该是如下这样的判断,而不能只判断200.

1
2
3
4
5
6
7
xhr.onload = function () {

//如果请求成功
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
//do successCallback
}
}

因为有些时候,不是200也表示成功。尤其在RESTAPI的今天。

同步请求

应该避免同步请求,不仅因为他会阻塞页面,导致卡主。而且有很多限制,导致他没什么用处。如:

  • xhr.timeout必须为0
  • xhr.withCredentials必须为 false
  • xhr.responseType必须为””(注意置为”text”也不允许)

发送同步请求,只需修改open:
open(method, url [, async = true [, username = null [, password = null]]])

跨域请求,携带认证信息

在跨域请求中,client端必须手动设置xhr.withCredentials=true

另外,要特别注意一点,一旦跨域request能够携带认证信息,server端一定不能将Access-Control-Allow-Origin设置为*,而必须设置为请求页面的域名。

服务端如何鉴别ajax请求

ajax请求可以故意带上一个特殊的http请求头字段: X-Request-With,这样的话服务端才有可能判断出是否是Ajax异步请求。

支持文件上传

由于XHR level2支持了formdata,他可以模拟表单提交。所以通过XHR level2实现文件上传就简单了,我们可以自己获取到File对象,append到FormData里,然后通过ajax send给服务端即可,跟表单提交的效果是一样的。

如果使用ajax的二进制上传功能,则需要服务端做相应的二进制接收处理。

实现过程

  1. 先初始化一个前端库的项目,这里我们基于felib-template 这个模板来实现。

  2. 设计接口

  3. 暴漏入口

Refer

https://segmentfault.com/a/1190000004322487