//创建xhr对象
let xhr = new XMLHttpRequest();
//打开链接,对应参数: "请求类型","请求url",“是否异步处理”
xhr.open("get", "example.html", true);
//设置自定义请求头
xhr.setRequestHeader("MyHeader", "MyValue");
//监听回调
xhr.onreadystatechange = function() {
//readyState状态为4时代表请求完成
if (xhr.readyState == 4) {
//http请求状态码200-300一般代表成功,304代表资源未改动,从缓存读取
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
}
};
//发送请求
xhr.send(null);
查询字符串必须正确编码后添加到URL后面,查询字符串中的每个名和值都必须使用encodeURIComponent()编码,所有名/值对必须以和号(&)分隔。不需要传入请求体。
每个POST请求都应该在请求体中携带提交的数据,数据可以是任意格式。
并非所有浏览器都实现了XMLHttpRequest Level 2的所有部分,但所有浏览器都实现了其中部分功能。
FormData类型便于表单序列化,也便于创建与表单类似格式的数据然后通过XHR发送。
//方式一:创建一个模拟表单数据
let data = new FormData();
data.append("name", "Nicholas");
//方式二:传入一个表单元素
let data = new FormData(document.forms[0]);
//直接发送表单数据
let xhr = new XMLHttpRequest();
//......此处省略
xhr.send(data);
使用FormData不再需要给XHR对象显式设置任何请求头部。XHR对象能够识别作为FormData实例传入的数据类型并自动配置相应的头部。
let xhr = new XMLHttpRequest();
//......此处省略
xhr.timeout = 1000; // 设置1秒超时
xhr.ontimeout = function() {
alert("Request did not return in a second.");
};
xhr.send(null);
由于http请求的返回收到服务器或网络延迟的影响,因此可以为xhr对象设置一个超时时间。如果时间超时,则会中断请求,触发ontimeout 事件。readyState会变成4,也会调用onreadystatechange事件处理程序。
overrideMimeType()方法用于重写XHR响应的MIME类型。假如服务器返回的数据类型和响应头设置的MIME类型不一致,比如返回的是XML数据,设置的响应头是text/plain,这会导致xhr对象的responseXML属性为null,调用overrideMimeType()覆盖MIME类型可以保证返回值按照覆盖的类型处理。
let xhr = new XMLHttpRequest();
xhr.open("get", "text.xml", true);
xhr.overrideMimeType("text/xml");
xhr.send(null);
load事件在响应接收完成后立即触发,不用检查readyState属性。
let xhr = new XMLHttpRequest();
xhr.onload = function() {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
};
xhr.open("get", "text.html", true);
xhr.send(null);
在收数据期间,这个事件会反复触发。每次触发时,onprogress事件处理程序都会收到event对象,其target属性是XHR对象,且包含3个额外属性:lengthComputable、position和totalSize(不同浏览器可能参数不一样,我使用谷歌浏览器不是这3个参数,但有含义相同的另外几个参数)。其中,lengthComputable是一个布尔值,表示进度信息是否可用;position是接收到的字节数;totalSize是响应的ContentLength头部定义的总字节数。要想lengthComputable可用,必须在返回头里面加上Content-Length和Content-Encoding,否则事件对象的lengthComputable属性不可用。
默认情况下,XHR只能访问与发起请求的页面在同一个域内的资源。这个安全限制可以防止某些恶意行为。不过浏览器也需要支持合法跨源访问的能力,基本思路是使用自定义的HTTP头部允许浏览器和服务器相互了解,以确实请求或响应应该成功还是失败。
对于简单的请求,比如GET或POST请求,没有自定义头部,样的请求在发送时会有一个额外的头部叫Origin。Origin头部包含发送请求的页面的源(协议、域名和端口),以便服务器确定是否为其提供响应。如果服务器决定响应请求,那么应该发送Access-Control-Allow-Origin头部,包含相同的源;或者如果资源是公开的,那么就包含"*"。
出于安全考虑,跨域XHR对象也施加了一些额外限制。
跨源资源共享(CORS)通过一种叫预检请求的服务器验证机制,允许使用自定义头部、除GET和POST之外的方法,以及不同请求体内容类型。在要发送涉及上述某种高级选项的请求时,会先向服务器发送一个“预检”请求。这个请求使用OPTIONS方法发送并包含以下头部。
Origin: http://www.nczonline.net
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ
在这个请求发送后,服务器可以确定是否允许这种类型的请求。服务器会通过在响应中发送如下头部与浏览器沟通这些信息。
Access-Control-Allow-Origin: http://www.nczonline.net
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000
预检请求返回后,结果会按响应指定的时间缓存一段时间。换句话说,只有第一次发送这种类型的请求时才会多发送一次额外的HTTP请求。
默认情况下,跨源请求不提供凭据(cookie、HTTP认证和客户端SSL证书)。可以通过将withCredentials属性设置为true来表明请求会发送凭据。如果服务器允许带凭据的请求,那么可以在响应中包含如下HTTP头部:
Access-Control-Allow-Credentials: true
如果发送了凭据请求而服务器返回的响应中没有这个头部,则responseText是空字符串,status是0,onerror()被调用。服务器也可以在预检请求的响应中发送这个HTTP头部,以表明这个源允许发送凭据请求。
Fetch API能够执行XMLHttpRequest对象的所有任务,但更容易使用,接口也更现代化。XMLHttpRequest可以选择异步,而Fetch API则必须是异步。
declare function fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
fetch函数只有一个必选参数,请求资源的URL,路径格式和XHR对象一致,只要服务器返回,不论状态码多少,都会执行Promise对象的onResolve方法。
只传入一个必选URL参数的默认fetch请求是一个简单的GET请求,因此要想满足开发需求,必须通过第二个参数来自定义请求。
选项对象示例:
{
//以下键值对只列举默认参数,详细参数请查阅文档。
body:"请求体内容", //post请求中的请求体
cache:"default", //用于控制浏览器与HTTP缓存的交互。
credentials:"same-origin", //用于指定在外发请求中如何包含cookie。
headers:Headers对象实例或包含字符串格式键/值对的常规对象, //指定请求头
integrity:"", //用于指定子资源完整性
keepalive:false, //指示浏览器允许请求存在时间超出页面生命周期
method:"POST", //请求方法
mode:"cors", //请求模式,这个模式决定来自跨源请求的响应是否有效,以及客户端可以读取多少响应。
redirect:"follow", //指定如何处理重定向行为
referrer:"client/about:client", //指定Referer头部的内容
referrerPolicy:"no-referrer-when-downgrade", //指定HTTP的Referer头部
signal:new AbortController() //必须是AbortSignal的实例,用于支持通过AbortController中断进行中的fetch()请求
}
let body= JSON.stringify({
body: '这是我的请求体'
});
//中断请求对象
let abortController = new AbortController();
//Headers对象
let jsonHeaders = new Headers({
'Content-Type': 'application/json'
});
fetch('/config.json', {
method: 'POST', // 发送请求体时必须使用一种HTTP方法
body: payload,
headers: jsonHeaders,
signal:abortController.signal
});
Headers对象和Map对象在API上有很多的相似之处,但也有其独特的地方。
由于请求的安全策略限制,在跨域请求中,某些头部是不允许修改,Headers对象根据来源不同会自适应展示不同的行为。规则如下:
护卫 | 触发条件 | 限制 |
---|---|---|
none | 通过构造函数创建Headers对象 | 无 |
request | 在通过构造函数初始化Request对象,且mode值为非no-cors时激活 | 不允许修改禁止修改的头部(参见MDN文档中的forbidden header name词条) |
request-no-cors | 在通过构造函数初始化Request对象,且mode值为no-cors时激活 | 不允许修改非简单头部(参见MDN文档中的simple header词条) |
response | 在通过构造函数初始化Response对象时激活 | 不允许修改禁止修改的响应头部(参见MDN文档中的forbidden response header name词条) |
immutable | 在通过error()或redirect()静态方法初始化Response对象时激活 | 不允许修改任何头部 |
Request类型对象可以理解是fetch函数请求参数的封装。fetch方法接口定义里面接收一个RequestInfo类型的输入,并不是固定的一个URL链接,因此是可以也可以接收一个Request对象作为输入。
Request对象接收的参数和fetch方法一致。
方式一:
let r1 = new Request('https://foo.com');
方式二:
let r1 = new Request('https://foo.com',{body: 'foobar'});
let r2 = new Request(r1, { method: 'POST'});
console.log(r1.method); // GET
console.log(r2.method); // POST
console.log(r1.bodyUsed); // true
console.log(r2.bodyUsed); // false
将一个Request对象传入进行拷贝。如果传入选项,则会覆盖传入对象对应选项值。第一个请求的请求体会被标记为“已使用”。如果源对象与创建的新对象不同源,则referrer属性会被清除。此外,如果源对象的mode为navigate,则会被转换为same-origin。
使用clone()方法,这个方法会创建一模一样的副本,任何值都不会被覆盖。如果请求对象的bodyUsed属性为true(即请求体已被读取),则不能传入Request对象进行拷贝,也不能调用clone方法进行克隆。
let r = new Request('https://foo.com');
r.clone();
new Request(r);
// 没有错误
r.text(); // 设置bodyUsed为true
r.clone();
// TypeError: Failed to execute 'clone' on 'Request': Request body is already used
new Request(r);
// TypeError: Failed to construct 'Request': Cannot construct a Request with a
Request object that has already been used.
let r = new Request('https://foo.com');
// 向foo.com发送GET请求
fetch(r);
// 向foo.com发送POST请求
fetch(r, { method: 'POST' });
fetch方法内部会将传入的Request对象拷贝。因此不能传入已经读取过body内容的Request对象。因为是拷贝,所以传入的Request对象请求体会被标记为已使用。也就是说,有请求体的Request只能在一次fetch中使用。(不包含请求体的请求不受此限制。)要想基于包含请求体的相同Request对象多次调用fetch(),必须在第一次发送fetch()请求前调用clone():
let r = new Request('https://foo.com',{ method: 'POST', body: 'foobar' });
// 3个都会成功
fetch(r.clone());
fetch(r.clone());
fetch(r);
大多数情况下,产生Response对象的主要方式是调用fetch(),也可以通过构造函数创建以及Response.redirect()和Response.error()创建。
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/"
// }
接收一个URL和一个重定向状态码(301、302、303、307或308)
console.log(Response.redirect('https://foo.com', 301));
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: false
// redirected: false
// status: 301
// statusText: ""
// type: "default"
// url: ""
// }
console.log(Response.error());
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: false
// redirected: false
// status: 0
// statusText: ""
// type: "error"
// url: ""
// }
网络错误的Response对象会导致fetch()产生的Promise对象被拒绝。
属性 | 值 |
---|---|
headers | 响应包含的Headers对象 |
ok | 布尔值,表示HTTP状态码的含义。200~299的状态码返回true,其他状态码返回false |
redirected | 布尔值,表示响应是否至少经过一次重定向 |
status | 整数,表示响应的HTTP状态码 |
statusText | 字符串,包含对HTTP状态码的正式描述。这个值派生自可选的HTTP Reason-Phrase字段,因此如果服务器以Reason-Phrase为由拒绝响应,这个字段可能是空字符串 |
type | 响应类型(1)basic:表示标准的同源响应(2)cors:表示标准的跨源响应(3)error:表示响应对象是通过Response.error()创建的(4)opaque:表示no-cors的fetch()返回的跨源响应(5)opaqueredirect:表示对redirect设置为manual的请求的响应 |
url | 包含响应URL的字符串。对于重定向响应,这是最终的URL,非重定向响应就是它产生的 |
Request和Response都使用了Fetch API的Body混入,这个混入为两个类型提供了只读的body属性(实现为ReadableStream)、只读的bodyUsed布尔值(表示body流是否已读)和一组方法,用于从流中读取内容并将结果转换为某种JavaScript对象类型。Body混入提供了5个方法,用于将ReadableStream转存到缓冲区的内存里,将缓冲区转换为某种JavaScript对象类型,以及通过Promise来产生结果。在解决之前,Promise会等待主体流报告完成及缓冲被解析。
Body.text()方法返回Promise,解决为将缓冲区转存得到的UTF-8格式字符串。
fetch('https://foo.com')
.then((response) => response.text())
.then(console.log);
Body.json()方法返回Promise,解决为将缓冲区转存得到的JSON。
fetch('https://foo.com/foo.json')
.then((response) => response.json())
.then(console.log);
Body.formData()方法返回Promise,解决为将缓冲区转存得到的FormData实例
fetch('https://foo.com/form-data')
.then((response) => response.formData())
.then((formData) => console.log(formData.get('foo'));
Body.arrayBuffer()方法返回Promise,解决为将缓冲区转存得到的ArrayBuffer实例。
fetch('https://foo.com')
.then((response) => response.arrayBuffer())
.then(console.log);
Body.blob()方法返回Promise,解决为将缓冲区转存得到的Blob实例。
fetch('https://foo.com')
.then((response) => response.blob())
.then(console.log);
因为Body混入是构建在ReadableStream之上的,所以主体流只能使用一次。这意味着所有主体混入方法都只能调用一次,再次调用就会抛出错误。作为Body混入的一部分,bodyUsed布尔值属性表示读取器是否已经在流上加了锁。这不一定表示流已经被完全读取。
从TCP/IP角度来看,传输的数据是以分块形式抵达端点的,而且速度受到网速的限制。接收端点会为此分配内存,并将收到的块写入内存。Fetch API通过ReadableStream支持在这些块到达时就实时读取和操作这些数据。ReadableStream暴露了getReader()方法,用于产生ReadableStreamDefaultReader,这个读取器可以用于在数据到达时异步获取数据块。数据流的格式是Uint8Array。
fetch('https://fetch.spec.whatwg.org/')
.then((response) => response.body)
.then(async function(body) {
let reader = body.getReader();
while(true) {
let { value, done } = await reader.read();
if (done) {
break;
}
console.log(value);
}
});
// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false }
// ...
上述代码当请求返回时,获取ReadableStreamDefaultReader类型对象的读取器。然后执行循环,一致等待异步数据到达,当数据未传输完成,就继续寻循环等待,直到数据传输完成。这种异步获取网络传输中的数据对于数据量较大的请求而言是很有效的。