一个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 | function ajax () { |
设置请求头
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 | var xhr = new XMLHttpRequest(); |
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 | var xhr = new XMLHttpRequest(); |
获取响应内容
上面已经看到了,可以通过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 | xhr.onreadystatechange = function () { |
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
- 如果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 | try{ |
上传下载进度
1 | xhr.onprogress = updateProgress; |
注意:
- 上传触发的是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
7xhr.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的二进制上传功能,则需要服务端做相应的二进制接收处理。
实现过程
先初始化一个前端库的项目,这里我们基于felib-template 这个模板来实现。
设计接口
暴漏入口