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){
    			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得到了所有主流浏览器支持。

    实例化一个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验证(同样容易伪造)。
  • 分类:
    前端
    标签: