Ajax——Asynchronous JavaScript+XML,即异步JavaScript加XML。
这个技术主要是可以实现在不刷新页面的情况下从服务器获取数据,格式并不一定是XML。
让Ajax迅速流行的中心对象是XMLHttpRequest(XHR)。
XMLHttpRequest(XHR)对象
所有现代浏览器都通过XMLHttpRequest构造函数原生支持XHR对象:
let xhr = new XMLHttpRequest();
使用XHR
使用XHR对象,首先要调用open()方法,接收3个参数:请求类型(“get”、“post”等)、请求URL,以及表示请求是否异步的布尔值。
xhr.open("get", "example.php", false); 此处的URL是相对于代码所在页面的,也可以使用绝对URL。只能访问同源URL,即域名、端口和协议都相同的URL。 调用open()不会实际发送请求,只是为请求做好准备。
若要发生定义好的请求,必须调用send()方法,接收一个参数,是作为请求体发送的数据。若不需要发送请求体,则必须传null。
xhr.open("get", "example.txt", false);
xhr.send(null);
调用send()之后,请求就会发送到服务器。
收到响应后,XHR对象的以下属性会被填充上数据。
- responseText:作为响应体返回的文本。
- responseXML:如果响应的内容类型是“text/xml”或“application/xml”,那就是包含响应数据的XML DOM文档。
- status:响应的HTTP状态。
- statusText:响应的HTTP状态描述。
收到响应后,首先要检查status属性,以确保响应成功返回。
- 一般HTTP状态码为2xx表示成功,此时responseText和responseXML(若内容类型正确)属性中会有内容。
- 如果HTTP状态码为304,则表示资源未修改过,是从浏览器缓存中直接取出的,也是意味着响应有效。
xhr.open("get", "example.txt", false);
xhr.send(null);
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
}else{
alert("Request was unsuccessful: " + xhr.status);
}
无论是什么响应内容类型,responseText属性始终会保存响应体,而responseXML则对于非XML数据是null。
大多数情况下,使用的都是异步请求,这样不阻塞JS代码继续执行。
XHR对象有一个readyState属性,表示当前处在请求/响应过程的哪个阶段。属性有以下可能的值:
- 0:未初始化(Undefined)。尚未调用open()方法。
- 1:已打开(Open)。已调用open()方法,尚未调用send()方法。
- 2:已发送(Sent)。已调用send()方法,尚未收到响应。
- 3:接收中(Receiving)。已经收到部分响应。
- 4:完成(Complete)。已经说到所有响应,可以使用了。
每次readyState从一个值变成另一个值,都会触发readystatechange事件,可借此机会来检查readyState的值。 为保证跨浏览器兼容,onreadystatechange事件处理程序应该在调用open()之前赋值。
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if(xhr.readyState == 4){
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
}else{
alert("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.open("get", "example.txt", true);
xhr.send(null);
在收到响应之前如果想取消异步请求,可以调用abort()方法。
xhr.abort(); 调用此方法后,XHR对象会停止触发事件,并阻止访问这个对象上任何与响应相关的属性。 中断请求后,应该取消对XHR对象的引用。由于内存问题,不推荐重用XHR对象。
HTTP头部
每个HTTP请求和响应都会携带一些头部字段,XHR对象会通过一些方法暴露与请求和响应相关的头部字段。 默认情况下,XHR请求会发送以下头部字段:
不同的浏览器发送的确切头部字段可能各不相同,如果需要发送额外的请求头部,可以使用setRequestHeader()方法,接收两个参数:头部字段的名称和值。 为保证请求头部被发送,必须在open()之后、send()之前调用setRequestHeader()。
xhr.open("get", "example.txt", true);
xhr.setRequestHeader("MyHeader", "MyValue");
xhr.send(null);
自定义头部一定要区别于浏览器正常发送的头部,否则可能影响服务器正常响应。
可以使用getResponseHeader()方法从XHR对象获取响应头部,只要传入要获取的头部的名称即可。 可以使用getAllResponseHeaders()方法来获取所有的响应头部。
GET请求【常用】
用于向服务器查询某些信息。必要时,需要在GET请求的URL后面添加查询字符串参数。 对XHR而言,查询字符串必须正确编码后添加到URL后面,然后再传给open()方法。
发送GET请求时最常见的错误是查询字符串格式不对,查询字符串中的每个名和值都必须使用encodeURIComponent()编码, 所有名/值对必须以和号(&)分隔。
xhr.open("get", "example.php?name1=value1&name2=value2", true);
可以使用以下函数来查询字符串参数添加到现有的URL末尾:
function addURLParam(url, name, value){
url += (url.indexOf("?")) == -1 ? "?" : "&";
url += encodeURIComponent(name)+ "=" + encodeURIComponent(value);
return url;
}
这里的addURLParam()方法,接收三个参数:要添加查询字符串的URL、查询参数和参数值。
POST请求【常用】
用于向服务器发送应该保存的数据。
每个POST请求都应该在请求体中携带提交的数据,而GET请求则不然。 POST请求的请求体可以包含非常多的数据,而且可以是任意格式的数据。
send()方法传入要发送的数据,由于XHR最初主要设计用于发送XML,所以可以传入序列化之后的XML DOM文档作为请求体,也可传入任意字符串。
默认情况下,对服务器而言,POST请求与提交表单是不一样的。服务器逻辑需要读取原始POST数据才能取得浏览器发送的数据。
可以使用XHR模拟表单提交,第一步把Content-Type头部设置为“application/x-www-formurlencoded",这是提交表单时使用的内容类型。 第二步是创建对应格式的字符串,如果网页中有一个表单需要序列化并通过XHR发送到服务器,则可以使用serialize()函数来创建相应的字符串。
xhr.open("post", "postexample.php", true);
xhr.setRequestHeader("Content-Type", "application/x-www-formurlencoded");
let form = document.getElementById("user-info");
xhr.send(serialize(form));
PHP文件postexample.php随后可以通过$_POST取得POST的数据,如$_POST['user-name']。
POST请求相比GET请求占用更多资源。在性能上,发送相同数量的数据,GET请求比POST请求要快两倍。
XMLHttpRequest Level2
所有浏览器都实现了 XMLHttpRequest Level2其中的部分功能。
1. FromData类型
便于表单序列化,也便于创建与表单类似格式的数据,然后通过XHR发送。
let data = new FromData();
data.append("name", "CLN");
append()方法,接收两个参数:键和值,相当于表单字段名称和该字段的值。 可以添加任意多个键/值对数据。此外,可以通过直接给FromData构造函数传入一个表单元素,也可以将表单中的数据作为键/值对填充进去。
let data = new FromData(document.forms[0]);
有了FormData实例,可以直接传给XHR对象的send()方法:
let form = document.getElementById("user-info");
xhr.send(new FormData(form));
使用此类型的另一个方便之处是不再需要给XHR对象,显示设置任何请求头部。
2. 超时
所有浏览器都在自己的XHR实现了timeout属性,用于表示发送请求后等待多少毫秒。 在给timeout属性设置了时间,且超时后没收到响应时,XHR对象就触发timeout事件,调用ontimeout事件处理程序。
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if(xhr.readyState == 4){
try{
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
}else{
alert("Request was unsuccessful: " + xhr.status);
}
} catch(ex){
// 假设由ontimeout来处理
}
}
};
xhr.open("get", "example.php", true);
xhr.timout = 1000;
xhr.ontimeout = function(){
alert("Request did not return in a second!!");
};
xhr.send(null);
如果在超时之后,访问status属性会发生错误,可以把检查status属性的代码封装在try/catch语句中。
3. overrideMimeType()方法
用于重写XHR响应的MIME类型。
假如服务器实际发送了XML数据,但响应头设置的MIME类型为text/plain,结果会导致数据是XML,但responseXML的值为null。 此时overrideMimeType()调用,可以保证将响应当成XML而不是纯文本处理。
let xhr = new XMLHttpRequest();
xhr.open("get", "text.php", true);
xhr.overrideMimeType("text/xml");
xhr.send(null);
进度事件
这些事件最初只针对XHR,现在也推广到其它类似的API。以下6个进度相关事件:
load事件
Firefox增加load事件用于替代readystatechange事件,load事件在响应接收完成后立即触发。
onload事件处理程序会收到一个event对象,其target属性设置为XHR实例,在这个实例上可以访问所有XHR对象属性和方法。
并非所有浏览器都实现了这个事件的event对象,考虑到跨浏览器兼容,需要像这样使用XHR对象变量:
let xhr = new XMLHttpRequest();
xhr.onload = function(event){
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
}else{
alert("Request was unsuccessful: " + xhr.status);
}
};
xhr.open("get", "example.php", true);
xhr.send(null);
只要是从服务器收到响应,无论状态码是什么,都会触发load事件。 Firefox、Opera、Chrome和Safari都支持load事件。
progress事件
在浏览器接收数据期间,这个事件会反复触发。每次触发时,onprogress事件处理程序都会接受到event对象,其target属性是XHR对象, 且包含3个额外属性:
- lengthComputable—表示进度信息是否可用,布尔值表示;
- position—接受到的字节数;
- totalSize—响应的Content-Length头部定义的总字节数。
向用户展示进度:
let xhr = new XMLHttpRequest();
xhr.onload = function(event){
if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
}else{
alert("Request was unsuccessful: " + xhr.status);
}
};
xhr.onprogress = function(event){
let divStatus = document.getElementById("status");
if(event.lengthComputable){
divStatus.innerHTML = "Received"+event.position+"of"+event.totalSize+"bytes";
}
}
xhr.open("get", "example.php", true);
xhr.send(null);
为了保证正确执行,必须在调用open()之前添加onprogress事件处理程序。 假设响应由Content-Length头部,就可以利用这些信息计算出已经收到响应的百分比。
跨源资源共享(CORS)
默认情况下,XHR只能访问与发起请求的页面在同一个域内的资源。
跨源资源共享(CORS,Cross-Origin Resource Sharing)定义了浏览器与服务器如何实现跨源通信,基本思路是使用自定义HTTP头部允许浏览器和服务器相互了解,以确定请求或响应应该成功还是失败。
对于简单的请求,如GET或POST请求,没有自定义头部,而且请求体是text/plain类型,这样的请求在发送时会有一个额外的头部叫Origin。 Origin头部包含发送请求的页面的源(即协议、域名和端口),以便服务器确定是否为其提供响应。
Origin : www.nczonline.net
若服务器决定响应请求,则应该发送Access-Control-Allow-Origin头部,包含相同的源;或者如果资源是公开的,则包含“*”。
Access-Control-Allow-Origin : www.nczonline.net
如果没有这个头部,或者有但源不匹配,则表明不会响应浏览器请求。否则,服务器会处理这个请求。 无论请求还是响应都不会包含cookie信息。
要向不同域的源发送请求,可以使用标准XHR对象并给open()方法传入一个绝对URL。
xhr.open("get", "www.baidu.com/page/", true);
跨域XHR对象允许访问status和statusText属性,也允许同步请求。但也有限制:
- 不能使用setRequestHeader()设置自定义头部。
- 不能发送和接收cookie。
- getAllResponseHeaders()方法始终返回空字符串。
因为无论同域还是跨域请求都使用同一个接口,最好在访问本地资源时使用相对URL,在访问远程资源时使用绝对URL。
预检请求
是服务器验证机制,允许使用自定义头部、除GET和POST之外的方法,以及不同请求体内容类型。
在要发生涉及上述某种高级选项的请求时,会先向服务器发送一个“预检”请求。 这个请求使用OPTIONS方法发送并包含以下头部。
- Origin:与简单请求相同。
- Access-Control-Request-Method:请求希望使用的方法
- Access-Control-Request-Headers:(可选)要使用的逗号分隔的自定义头部列表。
如一个POST请求,包含自定义的NCZ头部:
Origin: http://www.nczonline.net
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ
在这个请求发送后,服务器可确定是否允许这个类型的请求,会通过在响应中发送如下头部与浏览器交流的信息:
- Access-Control-Allow-Origin:与简单请求相同。
- Access-Control-Allow-Method:允许的方法(逗号分隔的列表)。
- Access-Control-Allow-Headers:服务器允许的头部(逗号分隔的列表)。
- Access-Control-Max-Age:缓存预检请求的秒数。
Access-Control-Allow-Origin:http://www.nczonline.net
Access-Control-Allow-Method:POST,GET
Access-Control-Allow-Headers:NCZ
Access-Control-Max-Age:1728000
预检请求返回后,结果会按响应指定的事件缓存一段时间。
凭据请求
默认情况下,跨域请求不提供凭据(cookie、HTTP认证和客户端SSL证书)。 可以通过设置withCredentials属性为true来表明请求会发生凭据。
若服务器允许带凭据的请求,可以在响应中包含以下HTTP头部:
Access-Control-Allow-Credentials:true
若发生了凭据请求,而服务器返回的响应中没有这个头部,则浏览器不会把响应交给JS(即responseText为空字符串,status为0,调用onerror())。
服务器也可以在预检请求的响应中发送这个HTTP头部,以表示这个源允许发送凭据请求。
替代性跨域技术
图片探测
是利用<img>标签实现跨域通信的最早一种技术。 可以动态创建图片,然后通过它们的onload和onerror事件处理程序得知何时收到响应。
图片探测是与服务器之间简单、跨域、单向的通信。
数据通过查询字符串发送,响应可以随意设置,不过一般是位图图片或值为204的状态码。 浏览器通过图片探测拿不到任何数据,但可通过监听onload和onerror事件知道何时能接受到响应。
let img = new Image();
img.onload = img.onerror = function(){
alert("Done!");
};
img.src = "http://www.example.com.test?name=Nike";
设置完src属性之后请求就开始了,向服务器发送了一个name值。
频繁用于跟踪用户在页面上的点击操作或动态显示广告。其缺点是只能发送GET请求和无法获取服务器响应的内容。
JSONP
即“JSON with padding”,与JSON看起来一样,只是会被包在一个函数调用里。
callback{{ "name" : "Nike" }};
JSONP格式包含两个部分:回调和数据,回调是在页面接收到响应之后应调用的函数,通常回调函数名是通过请求来动态指定的。 而数据是作为参数传给回调函数的JSON数据。
JSONP服务通常以查询字符串形式指定回调函数名称。
JSONP调用是通过动态创建<script>元素并为src属性指定跨域URL实现的。 此时<script>与<img>元素类似,能够不受限制地从其它域加载资源,由于JSONP是有效的JS,所以JSONP响应在被加载完成之后会立即执行。
function handleResponse(response){
console.log(`Your IP address is ${response.ip},which is in ${response.city}, ${response.region_name}`);
}
let script = document.createElement("script");
script.src = "http://freetest.net/json/?callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);
相比于图片探测,使用JSONP可以直接访问响应,实现浏览器与服务器的双向通信。
其缺点: 第一,JSONP是从不同的域拉取可执行代码,若这个域并不可信,则可能在响应中加入恶意内容,此时只能完全删除JSONP。 在使用不受控的Web服务时,一定要保证是可以信任的。 第二,不好确定JSONP请求是否失败。
Fetch API
能够执行XMLHttpRequest对象的所有任务。 XMLHttpRequest可以选择异步,而Fetch API则必须是异步。
Fetch API本身是使用JS请求资源的优秀工具,同时也能够应用在服务线程中,提供拦截、重定向和修改通过fetch()生成的请求接口。
代替AJAX原生对象XMLHttpRequest。建议使用基于Promise(期约)的Fetch API。
基本用法
fetch()方法是暴露在全局作用域中,调用此方法,浏览器就会向给定URL发送请求。
1. 分派请求
fetch()只有一个必需的参数input。多数情况下,这个参数是要获取资源的URL,此方法返回一个期约。
URL的格式(相对路径、绝对路径等)的解释与XHR对象一样。
请求完成、资源可用时,期约会解决为一个Response对象,此对象是API的封装,可以通过它获取相应资源。
fetch('bar.txt')
.then((response) => {
console.log(response);
});
// Response{ type : "basic", url : ... }
2. 读取响应
最简单方式是取得纯文本格式的内容,要用到text()方法,会返回一个期约,会解决为取得资源的完整内容:
fetch('bar.txt')
.then((response) => {
response.text().then((data) => {
console.log(response);
});
});
// bar.txt的内容
3. 处理状态码和请求失败
Fetch API支持通过Response的status和statusText属性检查响应状态。
成功获取响应的请求,通常会产生值为200的状态码。
fetch('/bar')
.then((response) => {
console.log(response.status); // 200
console.log(response.statusText); // OK
});
请求不存在的资源,通常会产生值为404的状态码。 请求的URL如果抛出服务器错误,会产生值为500的状态码。
只要服务器有响应,即无论是状态码是200/404/500,response都会成功返回。
通常状态码为200~299时就会被认为成功了,其他情况可被视为未成功。 为区分这两种情况,可以在状态码是非200~299时检查Response对象的ok属性:
fetch('/does-not-exist')
.then((response) => {
console.log(response.status); // 404
console.log(response.ok); // false
});
服务器没响应,反应超时,错误URL,违反CORS、无网络连接、HTTPS错配及其他浏览器/网络策略问题,都会导致期约被拒绝。
自定义选项
只使用URL时,fetch()会发生GET请求。要进一步配置如何发送请求,需要传入可选的第二个参数init对象,此对象要按照下面的键/值进行填充。
常见Fetch请求模式
与XHRHttpRequest一样,fetch()既可以发送数据也可接收数据。使用init对象参数,可以配置fetch()在请求体中发送各种序列化的数据。
1.发送JSON数据
let payload = JSON.stringfy({ foo : 'bar' });
let jsonHeaders = new Headers({ 'Content-Type' : 'application/json' });
fetch('/send-me-json', {
method : 'POST', // 发送请求时必须使用一种HTTP方法
body : payload,
headers : jsonHeaders
});
2.在请求体中发送参数
由于请求体支持任意字符串值,所以可以通过它发送请求参数:
let payload = 'foo=bar&baz=qux';
let paramHeaders = new Headers({ 'Content-Type' : 'application/x-www-form-urlencoded; charset=URF-8' });
fetch('/send-me-param', {
method : 'POST', // 发送请求时必须使用一种HTTP方法
body : payload,
headers : paramHeaders
});
3.发送文件
由于请求体支持FormData实现,所以fetch()可以序列化发送文件字段中的文件。
let imgFD = new FormData();
let imgInput = document.quertSelector("input[type = 'file']");
imgFD.append('image', imgInput.files[0]);
fetch('/img-upload', {
method : 'POST',
body : imgFD
});
4.加载Blob文件
常见的做法是明确将图片文件加载到内存,然后将其添加到HTML图片元素。
为此,可以使用响应对象(response)上暴露的blob()方法,返回一个期约,解决为一个Blob的实例。 然后可以将这个实例传给URL.createObjectUrl(),来生成可以添加给图片元素src属性的值。
const imageElement = document.quertSelector('img');
fetch('myimage.png')
.then((response) => response.blob())
.then((blob) => {
imageElement.src = URL.createObjectUrl(blob);
});
5.发送跨源请求
从不同的源请求资源,响应要包含CORS头部(即Access-Control-Allow-Origin)才能保证浏览器收到响应。若没有这些头部,跨源请求会失败并抛出错误。
若代码不需要访问响应,也可发送no-cors请求,此时响应的type属性值为opaque,因此无法访问响应内容,这种方式适合发送探测请求或者将响应缓存起来供以后使用。
6.中断请求
Fetch API支持通过AbortController/AbortSignal对中断请求。 调用AbortController.abort()会中断所有网络传输,特别适合希望停止传输大型负载情况。 中断进行中的fetch()请求,会导致包含错误的拒绝。
let abortController = new AbortController();
fetch('test.zip', { signal : abortController.signal })
.catch(() => console.log('aborted!!'));
setTimeout(() => abortController.abort(), 10);
// 已经中断
AbortController拥有signal属性,而signal为AbortSignal的实例。
Headers对象
是所有外发请求和入站响应头部的容器。
- 每个外发的Request实例都包含一个空的Headers实例,可通过Request.prototype.headers访问。
- 每个入站的Response实例也可通过Response.prototype.headers访问包含着响应头部的Headers对象。 这两个属性都是可修改的。使用new Headers()也可创建一个新实例。
Headers与Map
Headers对象与Map对象极为相似。因为HTTP头部本质上是序列化后的键/值对,它们的JavaScript表示则是中间接口。 两个类型都具有get()、set()、has()和delete()等实例方法。
Headers和Map都可以使用一个可迭代对象来初始化:
let seed = [['foo', 'bar']];
let h = new Headers(seed);
let m = new Map(seed);
console.log(h.get('foo')); // bar
console.log(m.get('foo')); // bar
它们也都有相同的keys()、values()和entries()迭代器接口。
Headers独有的特性
在初始化Headers对象时,也可以使用键/值对形式的对象,而Map不可以。
let seed = { foo : 'bar' };
let h = new Headers(seed);
console.log(h.get('foo')); // bar
let m = new Map(seed); // TypeError:object is not iterable
一个HTTP头部字段可有多个值,而Headers对象通过append()方法支持添加多个值,后续调用会以逗号为分隔符拼接多个值。
let h = new Headers();
h.append('foo', 'bar');
console.log(h.get('foo')); // "bar"
h.append('foo', 'cln');
console.log(h.get('foo')); // "bar、cln"
3.头部护卫
某些情况下,并非所有HTTP头部都可以被客户端修改,而Headers对象使用护卫来防止不被允许的修改。 违反护卫限制会抛出TypeError。
Headers实例会因来源不同而展现不同的行为,它们的行为由护卫来控制。
Request对象
是获取资源请求的接口。
1. 创建Request对象
可通过构造函数初始化Request对象,需要传入一个input参数,一般是URL:
let r = new Request('foo.com');
也接收第二个参数——一个init对象,这个对象与起那面fetch()的init对象一样。 没有在init对象中涉及的值则会使用默认值。
2. 克隆Request对象
Fetch API提供两种方式用于创建Request对象的副本:
使用Request构造函数
将Request实例作为input参数传给Request构造函数,会得到该请求的一个副本。
如果再传入init对象,则init对象的值会覆盖源对象中同名的值。
此克隆方式并不总能得到一模一样的副本。最明显的是,第一个请求的请求体会被标记为“已使用”。
let r1 = new Request('http://cln.com', { method : 'POST', body : 'jjk' });
let r2 = new Request(r1);
console.log(r1.bodyUsed); // true
console.log(r2.bodyUsed); // false
如果源对象与创建新对象不同源,则referrer属性会被清除。 如果源对象的mode为navigate,则会被转换为same-origin。
使用clone()方法
此方法会创建一模一样的副本,任何值都不会被覆盖。 与第一种方式不同,这种方法不会将任何请求的请求体标记为“已使用”。
let r1 = new Request('http://cln.com', { method : 'POST', body : 'jjk' });
let r2 = r1.clone();
console.log(r1.bodyUsed); // false
console.log(r2.bodyUsed); // false
如果请求对象的bodyUsed属性为true(即请求体已被读取),则以上任何方式都不能创建这个对象的副本。 在请求体被读取之后,再克隆会导致抛出TypeError。
let r = new Request('http://cln.com', { method : 'POST', body : 'jjk' });
r.clone();
new Request(r);
r.text(); // 设置bodyUsed为true
r.clone(); // TypeError
new Request(r); // TypeError
3.在fetch()中使用Request对象
fetch()和Request构造函数拥有相同的函数签名。
在调用fetch()时,可传入已创建的Request实例,而不是URL。 与Request构造函数一样,传给fetch()的init对象会覆盖传入请求对象的值。
fetch()会在内部克隆传入的Request对象。 与克隆Request一样,fetch()也不能拿请求体已使用的Request对象来发送请求,会抛出TypeError。
有请求的Request只能在一次fetch中使用:
let r = new Request('http://cln.com', { method : 'POST', body : 'jjk' });
fetch(r);
fetch(r); // TypeError
要想基于包含请求体的相同Request对象多次调用fetch(),必须在第一次发送fetch()请求之前调用clone()。
let r = new Request('http://cln.com', { method : 'POST', body : 'jjk' });
// 三个都会成功
fetch(r.clone());
fetch(r.clone());
fetch(r);
Response对象
是获取资源响应的接口。
1. 创建Response对象
可通过构造函数初始化Response对象且不需要参数,此时响应实例的属性均为默认值。
Response构造函数接收一个可选的body参数,可以是null,等同于fetch()参数init中的body。 还可接收一个可选的init对象,可以包含下列的键和值:
可以这样使用body和init来构建Response对象:
let r = new Response('foobar', {
status : 418,
statusText : 'I 'm Jungkooked!'
});
console.log(r);
// Response{
// body : {...}
// bodyUsed : false
// headers : Headers{}
// ok : false
// redirected : false
// status : 418
// statusText : "I 'm Jungkooked!"
// type : "default"
// url : ""
// }
大多数情况下,产生Response对象的主要方式是调用fetch(),返回一个最后会解决为Response对象的期约,这Response对象代表实际的HTTP响应。
fetch('https://foo.com')
.then((response) => {
console.log(response);
})
// Response{
// body : {...}
// bodyUsed : false
// headers : Headers{}
// ok : true
// redirected : false
// status : 200
// statusText : "OK"
// type : "basic"
// url : "https://foo.com/"
// }
Response类有两个用于生成Response对象的静态方法:Response.redirect()和Response.error()。
Response.redirect()接收一个URL和一个重定向状态码(301、302、303、307或308),返回重定向的Response对象。 提供的状态码必须对应重定向,否则会抛出错误。
Response.error()用于产生表示网络错误的Response对象(网络错误会导致fetch()期约被拒绝)。
2. 读取响应状态的信息
Response对象包含一组只读属性,描述了请求完成后的状态。
返回200状态码的URL对应的响应:
ok : true
redirected : false
status : 200
statusText : "OK"
返回302状态码的URL对应的响应:
ok : true
redirected : true
status : 200
statusText : "OK"
url : "https/foo.com/redirected-url/"
返回404状态码的URL对应的响应:
ok : false
redirected : true
status : 404
statusText : "Not Found"
返回500状态码的URL对应的响应:
ok : false
redirected : true
status : 500
statusText : "Internal Server Error"
3. 克隆Response对象
主要方式是使用clone方法,会创建一个一模一样的副本,不会覆盖任何值,不会将任何请求的请求体标记为已使用。
若响应对象的bodyUsed属性为true,则不能再创建这个对象的副本。 在响应体被读取之后再克隆会导致抛出TypeError。
有响应体的Response对象只能读取一次。 要多次读取包含响应体的同一个Response对象,必须在第一次读取前调用clone()。
此外,通过创建带有原始响应体的Response实例,可以执行伪克隆操作。 这样不会把第一个Response实例标记为已读,而是会在两个响应之间共享:
let r1 = new Response('foobar');
let r2 = new Response(r1.body);
console.log(r1.bodyUsed); // false
console.log(r2.bodyUsed); // false
r2.text().then(console.log); // foobar
r1.text().then(console.log); // TypeError: Failed to execute 'text' on 'Response' : body stream id locked
Request、Response及Body混入
Request和Response都使用了Fetch API的Body混入,以实现两者承担有效载荷的能力。 这个混入为两个类型提供了只读的body属性(实现为ReadableStream)、只读的bodyUsed布尔值(表示body流是否已读)和一组方法, 用于从流中读取内容并将结果转换为某种JavaScript对象类型。
Body混入提供了5个方法,用于将ReadableStream转存到缓冲区的内存里,将缓冲区转换为某种JavaScript对象类型,以及通过期约来产生结果。 在解决之前,期约会等待主体流报告完成及缓冲被解析。意味着客户端必须等待响应的资源完全加载才能访问其内容。
1. Body.text()
返回期约,解决为将缓冲区转存得到的 UTF-8格式字符串。 在Response对象上使用此方法:
fetch('https://foo.com')
.then((response) => response.text())
.then(console.log);
// <!doctype html><html lang="en">
// <head>
// <meta charset="utf-8">
// ...
在Request对象上使用此方法:
let request = new Request('https://foo.com', { method : 'POST', body : 'clnjjk' });
request.text()
.then(console.log);
// clnjjk
2. Body.json()
返回期约,解决为将缓冲区转存得到的JSON。 在Response对象上使用此方法:
fetch('https://foo.com/foo.json')
.then((response) => response.json())
.then(console.log);
// { "foo" : "bar" }
在Request对象上使用此方法:
let request = new Request('https://foo.com', { method : 'POST', body : JSON.stringfy({ bar : 'baz' }) });
request.json()
.then(console.log);
// { bar : 'baz' }
3. Body.formData()
浏览器可以将FormData对象序列化/反序列化为主体。
FormData实例:
let myFormData = new FormData();
myFormData.append('foo', 'bar');
Body.formData()返回期约,解决为将缓冲区转存得到的FormData实例。 在Response对象上使用此方法:
fetch('https://foo.com/form-data')
.then((response) => response.formData())
.then((formData) => console.log(formData.get('foo'));
// bar
在Request对象上使用此方法:
let myFormData = new FormData();
myFormData.append('foo', 'bar');
let request = new Request('https://foo.com', { method : 'POST', body : myFormData });
request.formData()
.then((formData) => console.log(formData.get('foo'));
// bar
4. Body.arrayBuffer()
有时可能需要以原始二进制格式查看和修改主体,而此方法可以将主体内容转换为ArrayBuffer实例,返回期约,解决为将缓冲区转存得到的ArrayBuffer实例。 在Response对象上使用此方法:
fetch('https://foo.com')
.then((response) => response.arrayBuffer())
.then(console.log);
// ArrayBuffer(...){}
在Request对象上使用此方法:
let request = new Request('https://foo.com', { method : 'POST', body : 'abcdefg' });
// 以整数形式打印二进制编码的字符串
request.arrayBuffer()
.then((buf) => console.log(new Int8Array(buf)));
// Int8Array(7) [97, 98, 99, 100, 101, 102, 103]
5. Body.blob()
有时可能需要以原始二进制格式使用主体,不用查看和修改,而此方法可以将主体内容转换为Blob实例,返回期约,解决为将缓冲区转存得到的Blob实例。 在Response对象上使用此方法:
fetch('https://foo.com')
.then((response) => response.blob())
.then(console.log);
// Blob(...){ size : ..., type : "..." }
在Request对象上使用此方法:
let request = new Request('https://foo.com', { method : 'POST', body : 'abcdefg' });
request.blob()
.then(console.log);
// Blob(7) { size : 7, type : "text/plain;charset=utf-8" }
6. 一次性流
由于Body混入是构建在ReadableSteam之上的,所以主体流只能使用一次,意味着所有主体混入方法都只能调用一次,再次调用会抛出错误。
fetch('https://foo.com')
.then((response) => response.blob().then(() => response.blob()));
// TypeError : Failed to execute 'blob' on 'Response' : body steam is locked;
即使是在读取流的过程中,所有这些方法也会在它们被调用时给ReadableSteam加锁,以阻止其他读取器访问:
fetch('https://foo.com')
.then((response) => {
response.blob(); // 第一次调用时给流加锁
response.blob(); // 第二次调用再次加锁会失败
});
// TypeError : Failed to execute 'blob' on 'Response' : body steam is locked;
let request = new Request('https://foo.com', { method : 'POST', body : 'cln' });
request.blob(); // 第一次调用时给流加锁
request.blob(); // 第二次调用再次加锁会失败
// TypeError : Failed to execute 'blob' on 'Request' : body steam is locked;
作为Body混入的一部分,bodyUsed布尔值属性表示ReadableSteam是否已摄受,即读取器是否已经在流上加了锁,这不一定表示流已经被完全读取。
let request = new Request('https://foo.com', { method : 'POST', body : 'cln' });
let response = new Response('cln');
console.log(request.bodyUsed); // false
console.log(response.bodyUsed); // false
request.text().then(console.log); // cln
response.text().then(console.log); // cln
console.log(request.bodyUsed); // true
console.log(response.bodyUsed); // true
Beacon API
为了解决在页面周期末尾时,使用同步XMLHttpRequest强制发送请求,导致的用户体验问题,浏览器因为要等待unload事件处理程序完成而延迟导航到下一个页面。
W3C引入了Beacon API,这个API给navigator增加了一个sendBeacon()方法, 接收一个URL和一个数据有效载荷参数,并会发送一个POST请求。 可选的数据有效载荷参数有ArrayBufferView、Blob、DOMString、FormData实例。 如果请求成功进入了最终要发送的任务队列,则这个方法返回true,否则false。
navigator.sendBeacon('cln.com/analytics-r…', '{ foo : "bar" }');
这个方法看起来只不过是POST请求的一个语法糖,其几个重要的特性:
- sendBeacon()可以在任何时候都可以使用。
- 调用此方法后,浏览器会把请求添加到一个内部的请求队列,会主动地发送队列中的请求。
- 浏览器保证在原始页面已经关闭地情况下也会发送请求。
- 状态码、超时和其他网络原因造成的失败完全是不透明的,不能通过编程方式处理。
- 信标(beacon)请求会携带调用sendBeacon()时所有相关的cookie。
Web Socket(套接字)
Web Socket的目标是通过一个长时连接实现与服务器全双工、双向的通信。
在JavaScript中创建WebSocket时,一个HTTP请求会发送到服务器以初始化连接。 服务器响应后,连接使用HTTP的Upgrade头部从HTTP协议切换到Web Socket协议。 Web Socket 必须使用支持该协议的专有服务器。
URL使用ws://和wss://,前者为不安全的连接,后者为安全连接。 在指定Web Socket URL时,必须包含URL方案。
使用自定义协议的好处是,客户端与服务器之间可发送非常少的数据,更快地发送小数据块,不会对HTTP造成任何负担。
其缺点是,定义协议的时间比定义JavaScript API要长。
Web Socket得到了所有主流浏览器支持。
API
实例化一个Web Socket对象并传入提供连接的URL,可创建一个新的Web Socket:
let socket = new Socket("ws://www.example.com/server.php"); 必须给构造函数传入一个绝对URL。同源策略不适用于Web Socket,因此可以打开到任意站点的连接。
浏览器会在初始化Web Socket对象之后立即创建连接。 与XHR类似,Web Socket也有一个readyState属性表示当前状态,不过两者的值不一样。
- Web Socket.OPENING(0):连接正在建立。
- Web Socket.OPEN(1):连接已经建立。
- Web Socket.CLOSING(2):连接正在关闭。
- Web Socket.CLOSE(3):连接已经关闭。
Web Socket对象没有readystatechange事件,而是有与上述状态对应的其他事件,readyState值从0开始。
任何时候都可调用close()方法关闭Web Socket连接。 调用close()之后,readyState立即变为2,并会在关闭后变为3。
发送和接收数据
打开Web Socket之后,可以通过连接发送和接收数据。
使用send()方法并传入一个字符串、ArrayBuffer和Blob方法,可以向服务器发送数据。
服务器向客户端发送消息时,Web Socket对象上会触发message事件,与其他消息协议类似,可通过event.data属性访问到有效载荷:
socket.onmessage = function(event){
let data = event.data;
// 对数据执行某种操作
};
与send()发送的数据类似,event.data返回的数据也可能是ArrayBuffer和Blob,由Web Socket对象的binaryType属性决定,该属性可能是"blob"或"arraybuffer"。
其他事件
Web Socket对象在连接生命周期中可能会触发3个其他事件:
- open:在连接成功建立时触发。
- error:在发生错误时触发。连接无法存续。
- close:在连接关闭时触发。
Web Socket对象需要使用DOM0风格的事件处理程序来监听这些事件:
let socket = new WebSocket("ws://www.example.com/server.php");
socket.onopen = function(){
alert("Connection established!");
}
在这些事件中,只有close事件的event对象上有额外信息,这个对象上有3个额外属性:
- wasClean:表示连接是否干净地关闭,布尔值。
- code:来自服务器的数值状态码。
- reason:一个字符串,包含服务器发来的消息。
安全
关于安全防护Ajax相关URL的一般理论认为,需要验证请求发生者拥有对资源的访问权限,可通过以下方式实现:
- 要求通过SSL访问能够被Ajax访问的资源。
- 要求每个请求都发送一个按约定算法计算好的令牌(token)。
在未授权系统可以访问某个资源时,可以将其视为跨站点请求伪造(CSRF)攻击。
以下手段对防护CSRF攻击是无效的:
- 要求POST而非GET请求(很容易修改请求方法)。
- 使用来源URL验证资源(来源URL很容易伪造)。
- 基于cookie验证(同样容易伪造)。