ajax作为前端开发必需的基础能力之一,你可能会使用它,但并不一定懂得其原理,以及更深入的服务器通信相关的知识。在最近两天的整理过程中,看了大量的文章,发现自己的后端能力已经限制自己在网络通信相关的知识领域的探索,还是应该尽快补齐短板。
下面我们来聊一聊
ajax
相关的东西,包括
xhr/xdr/ajax/cors/http
的一部分内容,其中会抛弃一些被弃用的历史包袱,如IE6/7等。
Ajax的出现
2005年,
JesseJamesGarrett
提出了Ajax的技术,其全称为
AsynchronousJavascriptandXML
,Ajax的核心是
XMLHttpRequest
对象,简称
XHR
,它用于使浏览器向服务器请求额外的数据而不卸载页面,极大的提高了用户体验。在此之前,其实这种技术已经存在并被一些人实现,但并没有流行也没有被浏览器支持。不过在此之后,IE5第一次引入
XHR
对象,并支持
ajax
技术,后续被所有浏览器支持。
XMLHttpRequest
对象和请求
XHR
是一个API,为客户端提供服务端和客户端之间通信的功能,并且不会刷新页面。它并不仅仅能取回XML类型的数据,而能取回所有类型的数据,除了http协议,还支持file和ftp协议。我们可以通过其构造函数来创建一个新的
XHR
对象,这个操作需要在其它所有操作之前完成:
var xhr = new XMLHttpRequest();
通过控制台我们可以很方便看到
XHR
的原型链:
Object->EventTarget->XMLHttpRequestEventTarget->XMLHttpRequest
。它拥有原型链上和本身的方法和属性,现在看下我们常用的方法:
我们解释下它的几个主要方法,我们在创建了新的xhr对象之后,首先要调用它的
open()
方法:
// 第一个参数可以为get/post等,表示该请求的类型
// 第二个参数是请求的url,可以为相对路径或绝对路径
// 第三个参数代表是否异步,为true时异步,为false时同步
// 第四五个参数为可选的授权使用的参数,因为安全性不推荐明文使用
xhr.open('get', 'example.php', true, username, password);
在这里受同源策略的影响,当第二个参数url跨域的时候会被浏览器报安全错误。同源策略指的是当前页面和目标url协议、域名和端口均相同。后面也会讲到,除IE之外的浏览器通过XHR对象实现跨域请求,只需将url设置为绝对url即可。
当初始化请求完成后,我们调用
send()
方法发送请求:
var data = new FormData
();
data.append('name', 'Nicholas');
// 接受一个请求主体发送的数据,如果不需要,传入null
xhr.send(data);
当请求的类型为
get/head
时,send()的参数会被忽略并置为null,send()传递的参数会影响到我们请求的头部
content-type
的默认值,该字段代表返回的资源内容的类型,用于浏览器处理,如果没有设置或在一些场景下,浏览器会进行MIME嗅探来确定怎么处理返回的资源。
在
XHR2级
中定义了
FormData
数据,用于常见的类表单数据序列化:
// 直接传入表单id
var data = new FormData(document.getElementById('user-form'));
// 创建类表单数据
var data = new FormData();
data.append('name', 'Nicholas');
// `FormData`可以直接被send()调用,会自动修改xhr的content-type头部
xhr.send(data);
// 请求头部的content-type: multipart/form-data; boundary=----WebKitFormBoundaryjn3q2KKRYrEH55Vz
// 请求的上传数据 Request Payload:
------WebKitFormBoundaryjn3q2KKRYrEH55Vz
Content-Disposition: form-data; name="name"
Nicholas
------WebKitFormBoundaryjn3q2KKRYrEH55Vz--
FormData
常用的方法有
append/delete/entries/forEach/get/getAll/has/keys/set/values
,都是常用的跟数组类似的方法,不再解释。
请求方法
GET
是最常见的请求类型,可以将查询字符串参数添加到URL尾部,对XHR而言,该查询字符串必须经过正确编码,每个键值对必须使用
encodeURIComponent()
进行编码,键值对之间由
&
分割:
// 封装序列化键值对
function addURLParam(url, name, value) {
url += (url.indexOf('?') === -1 ? '?' : '&';
url += encodeURIComponent(name) + '=' + encodeURIComponent(value);
return url;
}
POST
请求使用频率仅次于
GET
请求,通常发送较多数据,且格式不限,数据传递给
send()
作为参数。
HTTP一共规定了九种请求方法,每一个动词代表不同的语义,但是常用的只有上面两种:
- OPTIONS:返回服务器针对特定资源所支持的HTTP请求方法。也可以利用向Web服务器发送'*'的请求来测试服务器的功能性。
- HEAD:向服务器索要与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。
- GET:向特定的资源发出请求。
- POST:向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的创建和/或已有资源的修改。
- PUT:向指定资源位置上传其最新内容。
- DELETE:请求服务器删除Request-URI所标识的资源。
- TRACE:回显服务器收到的请求,主要用于测试或诊断。
- CONNECT:HTTP/
1.1协议中预留给能够将连接改为管道方式的代理服务器。
- PATCH: 用于对资源进行部分修改
HTTP头部信息
每个HTTP请求和响应都带有头部信息,xhr对象允许我们操作部分头部信息。我们可以通过
xhr.setRequestHeader()
方法来设置自定义的头部信息或者修改浏览器默认的正常头部信息。常用的请求头部:
// 下面的实例是从我本地的一次请求取出的
Accept: 浏览器能够处理的内容类型。// */*
Accept-Charset: 浏览器能够显示的字符集。// 未取到
Accept-Encoding: 浏览器能够处理的压缩编码。
// gzip,deflate
Accept-Language: 浏览器当前设置的语言。// zh-CN,zh;q=0.8,en;q=0.6
Connection: 浏览器与服务器之间连接的类型。// keep-alive
Cookie: 当前页面设置的任意Cookie。// JlogDataSource=jomodb
Host: 发出请求的页面所在域。// gzhxy-cdn-oss-06.gzhxy.baidu.com:8090
Referer: 发出请求的页面URI。// http://gzhxy-cdn-oss-06.gzhxy.baidu.com:8090/jomocha/index.php?r=tools/offline/index
User-Agent: 浏览器的用户代理字符串。// Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36
我们一般不修改浏览器正常的头部信息,可能会影响到服务器响应。如果需要可以通过
xhr.setRequestHeader()
进行修改:
// 传入头部键值对,键值不区分大小写,如果多次设置,则追加
// 此时请求头部的content-type: application/json, text/html
xhr.setRequestHeader('content-type', 'application/json');
xhr.setRequestHeader('content-type', 'application/json');
设置头部信息需要在
open()
之后,
send()
之前进行调用。响应的头部信息在后端处理,不在此处讲解。有一部分请求头部信息不允许设置,如
Accept-Encoding,Cookie
等。
在请求返回后,我们可以获取到响应头部:
// 获取指定项的响应头
xhr.getResponseHeader('content-type'); // application/json;charset=utf-8
// 获取所有的响应头部信息
xhr.getAllResponseHeaders();
这里简单说下content-type值,指的是请求和响应的HTTP内容类型,影响到服务器和浏览器对数据的处理方式,默认为
text/html
,常用的如:
// 包含资源类型,字符编码, 边界字符串三个参数,可选填
text/html;charset=utf-8 // html标签文本
text/plain
// 纯文本
text/css // css文件
text/javascript // js文件
// 普通的表单数据,可以通过表单标签的enctype属性指定
application/x-www-form-urlencode
// 发送文件的POST包,包过大需要分片时使用`boundary`属性分割数据作边界
multipart/form-data; boundary=something
// json数据格式
application/json
// xml类型的标记语言
application/xml
XHR
对象的响应
我们现在对请求的发起很了解了,接着看下如何拿到响应数据。如果我们给
open()
传递的第三个参数是
true
,则代表为同步请求,那么js会被阻塞直到拿到响应,而如果为
false
则是异步请求,我们只需要绑定
xhr.onreadystatechange()
事件监听响应即可。最上面的图已经说明了
readystate
的值含义,所以我们可以:
// xhr v1 的写法,检测readystate的值,为4则说明数据准备完毕,需要在open()前定义
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200 || xhr.status === 304) {
console.log(xhr.responseText);
} else {
console.log(xhr.statusText);
}
}
}
// xhr v2 的写法,onload()事件说明数据准备完毕
xhr.onload = function () {
if (xhr.status === 200 || xhr.status === 304) {
console.log(xhr.responseText);
} else {
console.log(xhr.statusText);
}
}
xhr
对象的响应数据中包含几个属性:
response
// 响应的数据
responseURL // 发起响应的URL
responseType // 响应的类型,用于浏览器强行重置响应数据的类型
responseText // 如果为普通文本,则在这显示
responseXML // 如果为xml类型文本,在这里显示
数据会出现在
responseText/responseXML
中的哪一个,取决于服务器返回的
MIME
类型,当然我们也有一些方式在浏览器端设置如何处理这些数据:
// xhr v1 的写法,设置响应资源的处理类型
xhr.overrideMimeType('text/xml');
// xhr v2 的写法, 可用值为 arraybuffer/blob/document/json/text
xhr.responseType = 'document';
响应数据相关的属性默认为
null/''
,只有当请求完成并被正确解析的时候才会有值,取决于responseType的值,来确定
response/responseText/responseXML
谁最终具有值。
XHR
的高级功能
在
xhr v2
里提供了超时和进度事件。
超时
xhr.timeout = 1000; // 1分钟,单位为ms
xhr.ontimeout = function () {};
在请求
send()
之后开始计时,等待
timeout
时长后,如果没有收到响应,则触发
ontimeout()
事件,超时会将
readystate=4
,直接触发
onreadystatechange()
事件。
请求进度
像上图所示,
xhr v2
定义了不同的进度事件:
loadstart/progress/error/abort/load/loadend
,这其中我们已经说过了
onload()
事件为内容加载完成可用。现在说一下
onprogress()
进度事件:
xhr.onprogress = function (event) {
if (event.lengthComputable) {
console.log(
event.loaded / event.total);
}
}
该事件会接收一个
event
对象,其
target
属性为该xhr对象,
lengthComputable
属性为
total size
是否已知,即是否可用进度信息,
loaded
属性为已经接收的字节数,
total
为总字节数。该事件会在数据接收期间不断触发,但间隔不确定。
跨域
CORS
提到
XHR
对象,我们就会讲到跨域问题,它是为了预防某些恶意行为的安全策略,但有时候我们需要跨域来实现某些功能。需要注意的是跨域并不仅仅是前端单方面的事情,它需要后端代码进行配合,我们只是通过一些方式跳过了浏览器的阻拦。
对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。
CORS(Cross-OriginResourceSharing,跨域资源共享)