js温故(四):Ajax—XHR、fetch、websocket
一、XMLHttpRequest 对象
所有现代浏览器都支持通过XMLHttpRequest构造函数来原生支持XHR对象:const xhr = new XMLHttpRequest()。
1. 使用XHR
使用XHR第一步便是调用open()方法,其接收三个参数:请求的类型(如get),url,以及一个布尔值表示请求是否为异步。例如,xhr.open('get', 'www.baidu.com', false) 表示将要向www.baidu.com发送一个同步的get请求。有两点需要注意:
- 这里得第二个参数
url一般是相对于当前页面的,不过也可以使用绝对URL; - 调用
open()方法只是进行初始化,并不会立即发送请求,而是为发送请求做准备,真正开始发送请求还需要调用send()方法。
send()方法接收一个参数,表示作为请求体发送的数据。xhr.send({username: 'cc', pwd: 'lovecake'})。如果不需要请求体,则必须传入null:xhr.send(null)。调用send()方法后,请求就会发送到服务器。由于在open()方法里将这个请求设置为同步的,因此在调用send()方法后,后续的js代码会暂停执行,等到服务器响应后方才恢复执行。当收到相应后,xhr对象的一些属性会被填充上响应数据:
responseText:作为相应体返回的文本;responseXML:如果响应的内容类型是text/xml或application/xml,则此属性为包含响应数据的XML DOM文档;status:响应的HTTP状态;statusText:响应的HTTP状态描述。
当收到响应后,首先应该检查状态码status来判断响应是否成功返回。一般来说,**状态码为2xx**则表示成功。此外,状态码为304,表示服务器资源未修改,直接用缓存中的资源,这也意味着响应有效。其它状态码则表示请求失败。
const xhr = new XMLHttpRequest()
let url = 'www.baidu.com'
xhr.open('get', url, false)
xhr.send(null)
// 检查status
if(xhr.status >= 200 && xhr.status<300 || xhr.status === 304){
// 请求成功
console.log('request success!!')
}else{
// 请求失败
console.log('request failed:' + xhr.status)
}
尽管statusText中也包含状态描述信息,但是它在跨浏览器时并不可靠,因此还是应该用status来检查。对于不同的响应类型,responseText始终保存响应体的内容,而responseXML只在相应类型为XML数据时为有效数据,否则其值为null。
由于设置为同步请求,会阻塞之后的js代码,因此,最好使用异步请求。
const xhr = new XMLHttpRequest()
let url = 'www.baidu.com'
// 第三个参数设置为true,即异步请求
xhr.open('get', url, true)
xhr.send(null)
异步请求需要用到xhr的readyState属性,该属性表示当前请求处于哪个阶段,其可能的值如下:
- 0 :未初始化,即尚未调用
open()方法; - 1 :已打开。即调用了
open()方法,但还未调用send(); - 2 :已发送。已经调用
send()方法,但还未接收到响应; - 3 :接收中,即已接受到部分响应;
- 4 :完成,即已接收到所有数据,可以使用。
可以看到,我们最需要的就是readyState的值为4。readyState的值每次变化时,都会触发readystatechange事件,因此,只需要监听xhr对象的readystatechange事件,该事件没有event对象,因此我们判断readyState值为4时,即请求完成,可进行后续操作。 为保证跨浏览器兼容,对readystatechange事件的监听应在open()方法调用之前进行。 对于不想继续的请求,可以使用abort()方法取消,并断开对xhr对象的引用。由于内存问题,不推荐复用xhr对象,最好每个请求都重新创建一个。
const xhr = new XMLHttpRequest()
let url = 'www.baidu.com'
// 监听readystatechange事件,应在open()方法调用之前
// 不推荐使用this来指代xhr对象,在某些浏览器中在这里使用this可能会导致错误
xhr.onreadystatechange = () => {
// 请求完成
if(xhr.readyState === 4){
// 检查status
if(xhr.status >= 200 && xhr.status<300 || xhr.status === 304){
// 请求成功
console.log('request success!!')
// 执行其它操作
// ...
}else{
// 请求失败
console.log('request failed:' + xhr.status)
// 处理失败
// ...
}
}
}
xhr.open('get', url, true)
xhr.send(null)
// 由于某些原因可能想取消该请求
xhr.abort()
xhr = null
2. HTTP头部
每个HTTP请求和响应都携带有一些头部信息。XHR对象通过一些方法暴露请求和响应相关的头部字段。
默认情况下,XHR请求会发送一些头部字段:
Accept:浏览器能够处理的内容类型;Accept-Charset:浏览器能够显示的字符集;Accept-Encoding:浏览器能够处理的压缩编码类型;Accept-Language:浏览器使用的语言;Connection:浏览器与服务器的连接类型;Cookie:页面中设置的Cookie;Host:发送请求的页面所在的域;Referer:发送请求的页面的URL(这个字段本应是Referrer,但是在HTTP规范中就拼错了,因此将错就错);User-Agent:浏览器的用户代理字符串。
如果需要发送额外的头部字段,可以使用setRequestHeader()方法,这个方法应该在open()方法之后、send()方法之前调用。
// 创建xhr对象
const xhr = new XMLHttpRequest()
// 监听readystatechange事件
xhr.onreadystatechange = () => {
// 请求完成
if(xhr.readyState === 4){
// 检查status
if(xhr.status >= 200 && xhr.status<300 || xhr.status === 304){
// 请求成功
console.log('request success!!')
// ...
}else{
// 请求失败
console.log('request failed:' + xhr.status)
}
}
}
// 初始化请求
xhr.open('get', 'www.baidu.com', true)
// 添加额外的头部字段,最好区别于默认头部字段
xhr.setRequestHeader('custom-header-field', 'custom-value')
// 发送请求
xhr.send(null)
要读取相应头的信息,可以使用getResponseHeader()方法,另外,也可以使用getAllResponseHeaders()方法来获取所有的响应头,其值为包含所有响应头部的字符串。
const myHeader = xhr.getResponseHeader('my-header'),
allHeaders = xhr.getAllResponseHeaders()
// getAllResponseHeaders()一般返回如下多行字符串
console.log(allHeaders)
/*
Date: Sun, 14 Nov 2004 18:04:03 GMT
Server: Apache/1.3.29 (Unix)
Vary: Accept
X-Powered-By: PHP/4.3.8
Connection: close
Content-Type: text/html; charset=iso-8859-1
*/
3. GET请求
GET请求的查询参数一般都拼接在URL后面。对于XHR而言,拼接的参数应该经过encodeURLComponent()方法来正确编码并添加到URL后面,然后传给open()方法。如下函数可以将查询字符串参数添加到URL后面。
function appendURLParams(url, name, value){
const symbol = url.indexOf('?') !== -1 ? '?' : '&',
addedContent = `${encodeURLComponent(name)}=${encodeURLComponent(value)}`;
return url + symbol + addedContent;
}
4. POST请求
POST请求在请求体中携带数据。XHR最初主要设计用来发送XML数据的,也可以发送字符串。对于服务器而言, POST请求与表单提交是不一样的。可以将请求头中的Content-Type设置为与表单提交一致,即application/x-www-formurlencoded,并且创建对应格式的字符串,来模拟表单提交。
5. XMLHTTPRequest Level 2
XMLHTTPRequest Level 2 进一步发展了XHR。
(1) FormData 类型
XMLHTTPRequest Level 2 中新增了FormData类型,以便对表单数据进行序列化,或创建与表单类似格式的数据然后通过XHR发送。
const fd = new FormData()
// 添加数据
fd.append('uname', 'cc')
使用append()方法来添加数据(键可以重复),delete()方法来删除所有对应键的数据,set()来设置不重复的键,get()方法来获取该键的第一条数据,getAll()方法得到包含该键对应的所有数据的数组。
(2) 超时
给XHR对象的timeout属性设置一个时间(毫秒),如果超过该时间还未收到响应,则XHR对象会触发timeout事件,并调用ontimeout处理程序。触发timeout事件后,readyState的值也会变成4,但是此时访问xhr.status会出错。因此,检查status的语句应该放在try/catch语句中,当捕获到错误则说明请求超时。
// 创建xhr对象
const xhr = new XMLHttpRequest()
// 监听readystatechange事件
xhr.onreadystatechange = () => {
try{
// 请求完成
if(xhr.readyState === 4){
// 检查status
if(xhr.status >= 200 && xhr.status<300 || xhr.status === 304){
// 请求成功
console.log('request success!!')
// ...
}else{
// 请求失败
console.log('request failed:' + xhr.status)
}
}
}catch(e){
// 超时,一般由ontimeout来处理
}
}
// 初始化请求
xhr.open('get', 'www.baidu.com', true)
// 超时处理
xhr.timeout = 2000 // 设置超时时间2秒钟
xhr.ontimeout = () => {
console.log('request timeout!!')
// 其它处理
// ...
}
// 添加额外的头部字段,最好区别于默认头部字段
xhr.setRequestHeader('custom-header-field', 'custom-value')
// 发送请求
xhr.send(null)
(3) overrideMimeType()方法
overrideMimeType()方法用于重写XHR响应的MIME类型。响应返回的MIME类型决定了XHR对象如何处理响应。例如,如果服务器实际上发送了XML数据,但是响应头设置的MIME类型是text/plain,则XHR对象不会将其当作XML类型来处理,导致xhr.responseXML的值为null。此时调用overrideMimeType()方法可以强制将响应当成XML来处理当然,这个方法应该在调用send()方法之前调用。
const xhr = new XMLHttpRequest();
xhr.open("get", "www.baidu.com", true);
// 将响应头的MIME类型设置为XML
xhr.overrideMimeType("text/xml");
xhr.send(null);
二、进度事件 progress
进度事件一开始只针对于XHR,后来也推广到了其它类似API。一般来说,有如下6个进度相关的事件:
loadstart:在接收到第一个响应字节时触发;progress:在接收响应期间反复出发;error:在请求出错时触发;abort:在请求终止连接时触发;load:在成功接收完响应时触发;loadend:在通信完成时,且在error、abort、load之后触发。
这些事件都比较容易理解,主要说明一下load和progress:
1. load事件
onload事件处理程序相当于之前的readyState的值为4,简化了操作。它接收一个event对象,其target为对应的xhr对象,但是不是所有浏览器都实现了这个事件的event对象,因此,考虑到兼容性,还是应该像之前一样使用xhr,而不是event.target。当然,只要接收到完响应,就会触发load事件,这不受状态码status的影响。因此,我们仍然需要检查status。
2. progress事件
在浏览器接收数据期间,progress事件会反复触发。每次触发,onprogress事件处理程序都会接收一个event对象,它的target属性是xhr对象,且拥有额外的三个属性:lengthComputable、position、totalSize 。其中,lengthComputable是一个布尔值,表示进度信息是否可用;position表示当前接收到的字节数;totalSize表示总字节数。用这些信息就可以展示进度条了。另外,onprogress事件处理程序应该在open()事件之前添加。
let xhr = new XMLHttpRequest();
xhr.onload = function(event) {
if ((xhr.status >= 200 && xhr.status < 300) ||
xhr.status == 304) {
console.log(xhr.responseText);
} else {
console.log("Request failed: " + xhr.status);
}
};
// 监控进度
xhr.onprogress = function(event) {
let divProgress = document.getElementById("progress");
if (event.lengthComputable) {
divProgress.innerHTML = "请求完成: " + (event.position/event.totalSize*100).toFixed(2) + "%"
}
};
xhr.open("get", "www.baidu.com", true);
xhr.send(null);
三、 跨域资源共享
由于XHR受跨域安全策略限制,默认情况下,XHR只能访问与发起请求的页面同在一个域内的资源。跨域资源共享(CORS)定义了浏览器与服务器如何进行跨源通信。CORS背后的基本思路是使用自定义的HTTP头部,允许浏览器与服务器相互了解,来确定请求应该成功或失败。
对于GET、POST这些简单的请求,没有请求头,且请求体为text/plain类型,这样的请求在发送时会额外有一个头部,叫做Origin,它包含发送请求的页面的源(协议、域名、端口),从而让服务器确定是否为其提供响应。如果服务器决定响应,就会发送 Access-Control-Allow-Origin头部,包含相同的源(协议、域名、端口),或者为*,表示资源是公开的。
如果没有这个头部,或者有但是源不匹配,则表示不会响应浏览器请求。无论请求还是响应,都没有Cookie信息。跨域XHR对象允许访问status和responseText,也允许同步请求,但是处于安全考虑也做了一些限制:
- 不允许使用
setRequestHeader()来设置自定义头部; - 不能接收与发送
cookie; getAllResponseHeaders()始终返回空字符串;
1. 预检请求
CORS通过预检请求的服务器验证机制,允许使用自定义头部、除GET、POST以外的方法、以及不同请求体的内容类型。在发送这些里面的某种高级选项的请求时,会先向服务器发送一个预检请求,该请求使用OPTIONS方法发送并包含如下头部:
Origin:与简单请求相同,为发送请求的页面的源;Access-Control-Request-Method:请求希望使用的方法;Access-Control-Request-Headers:用逗号分隔的请求头部列表(可选)。
例如,假设一个POST请求的预检请求的头部如下:
Origin: http://www.baidu.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: custom-header
在该请求发送后,服务器判断是否允许这种类型的请求,并在响应中包含如下头部,来与浏览器沟通这些信息:
Access-Control-Allow-Origin:与简单请求相同,允许访问的源,或者*表示资源公开;Access-Control-Allow-Methods:允许访问的方法,以逗号分隔的列表;Access-Control-Allow-Headers:服务器允许的头部,以逗号分隔的列表;Access-Control-Max-Age:缓存预检请求的秒数。预检请求响应后,其结果会按照这个秒数被缓存一段时间,期间再次发送这种类型的请求无需再发送预检请求。
2. 凭据请求
默认情况下,跨源请求不提供凭据(cookie、HTTP认证、客户端SSL证书等)。可以将withCredentials设置为true来表明请求会发送凭据。如果服务器允许带凭据的请求,则可以在响应头中包含如下HTTP头部:
Access-Control-Allow-Credentials: true
如果发送了凭据请求,但是服务器的响应中没有这个头部,则浏览器不会把响应的内容交给JavaScript。当然,服务器也可以在预检请求的响应中包含这个头部,来表明这个源允许发送凭据请求。
四、 替代性跨源技术
1. 图片探测
图片探测是与服务器之间简单、跨域、单向的通信,只能通过设置src属性来发送GET请求,且无法获取服务器返回的数据。通过动态创建图片,监听它们的onload和onerror事件处理程序来得知何时收到响应。数据可以通过查询字符串来发送,响应可以随意设置,毕竟浏览器无法得到任何数据,只能通过onload和onerror来获悉什么时候接收到响应。
2. JSONP
JSONP看起来和JSON一样,但是它被包含在函数里。JSONP格式包含 回调 和 数据 两部分。回调是在页面接收到响应之后应该调用的函数,回调函数的名称通常是通过请求来动态指定的。而数据就是作为参数传给回调函数的JSON数据。JSONP服务通常支持用查询字符串来指定回调函数的名称。
<script>标签不受跨源安全策略的限制,因此通过动态生成<script>标签并指定其src属性来调用JSONP。
function handleResponse(response) {
console.log(response)
}
let script = document.createElement("script");
// 通过查询字符串来指定回调函数名称
script.src = "http://www.baidu.com?callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);
五、 Fetch API
Fetch API可以执行所有XMLHttpRequest的任务,且更便于使用,接口也更现代化,能够在工作者线程中使用。Fetch API必须是异步的。
1. 基本使用
fetch()方法暴露在全局作用域中,包括主页面线程、模块和工作者线程。
(1) 分派请求
fetch()方法只有一个必需的参数,即获取资源的URL,可以是相对路径或绝对路径。返回一个期约(Promise 实例)。
let response = fetch('www.baidu.com')
console.log(response) // Promise <pending>
当请求完成且资源可用时,该期约会解决为一个Response对象,该对象是API的封装,可以通过它的某些方法来获取相应的资源。
(2) 读取响应
最快的读取响应的方法是读取纯文本内容,这需要调用Response对象的text()方法,该方法返回一个期约,解决为取得的资源的内容。
fetch('test.txt')
.then(resp => resp.text())
.then(data => console.log(data))
(3) 处理 状态码 和 失败请求
可以检查Response对象的status和statusText来确定相应状态。成功获取响应的请求一般会得到值为200的状态码;请求不存在的资源一般会得到值为404的状态码;服务器错误一般是值为500的状态码;可以显示地定义fetch()遇到重定向时的行为,但是默认行为是跟随重定向,此时响应对象的redirected属性会被设置为true,而状态码仍然是200。
哪怕请求看起来失败了(如status值为500),期约也会被解决,我们需要判断状态码来确定请求是否成功。而当请求超时时,期约才会执行拒绝程序。此外,无网络连接、违反CORS、HTTPS错配等也会导致期约被拒绝。
通过Response对象的url属性可以检查发起请求的完整URL。
fetch('/test')
.then(resp => console.log(resp.url))
// https://www.baidu.com/test
(4) 自定义选项
只使用URL参数时,fetch()默认为GET请求,且只使用最低限度的请求头。如果徐奥进一步配置,比如使用POST方法、自定义请求头等操作时,就需要传入可选的第二个参数:init对象。下面列举一些常用的配置:
-
body:请求体的内容,必须是Blob,BufferSource、FormData、URLSearchParams、ReadableStream或String的实例; -
cache:用于控制浏览器与HTTP缓存的交互。如需跟踪缓存的重定向,则请求的redirect属性应为follow,且不能违反CORS。cache的值应为以下之一:Default:默认值。命中有效的缓存,则fetch()返回该缓存,不发送请求;命中无效的缓存,则发送条件式请求,若响应已经改变,则更新缓存的值,且fetch()返回新的缓存值;若没有命中缓存,则发送请求并缓存响应,然后fetch()返回响应。no-store:浏览器不检查缓存,直接发送请求;不缓存响应,直接fetch()返回;reload:浏览器不检查缓存,直接发起请求;之后缓存响应并fetch()返回响应;no-cache:未命中缓存则发送请求,并缓存响应,然后fetch()返回响应;否则发送条件式请求,若响应已经改变,则更新缓存值然后fetch()返回缓存的值;force-cache:未命中缓存则发起请求,缓存响应并fetch()返回响应;否则,无论命中有效缓存还是无效缓存,都直接返回缓存的值,不发起请求;only-if-cached:只有请求模式为same-origin时使用缓存;无论命中有效缓存还是无效缓存时,都返回缓存的值,不发送请求;没有命中缓存则返回状态码为504 (网关超时) 的响应。
-
credentials:用于指定在外派请求时如何包含cookie,和XMLHttpRequest的withCredentials标签类似。值为以下字符串之一:omit:不发送cookie;same-origin:默认值。只在同源请求时发送cookie;include:无论同源还是跨源请求,都发送cookie。
headers:用于指定请求头部,必须是Headers实例或包含字符串格式的键值对对象。默认为空的Headers实例,但是这不意味着不发送请求头,浏览器仍然会跟随请求发送一些头部信息,但是这些请求头对JavaScript不可见,但浏览器的网络检查器可以检查到。
-
keepalive:指示浏览器是否允许在页面卸载后请求仍然存在,适合向服务器发送报告事件与分析。值为布尔值,默认为false。 -
method:指定请求的方法,值为以下字符串之一:GET,默认值;POST;PUT;PATCH;DELETE;HEAD;OPTIONS;CONNECT;TARCE。
-
mode:指定请求模式,决定来自跨源请求的响应是否有效。必须是以下字符串之一:cors:通过构造函数手动创建Request实例时默认值为此,允许遵守CORS跨源请求;no-cors:其它情况默认值为此,允许不需要发送预检请求的跨源请求,如HEAD、GET和只带有满足CORS请求头部的POST请求;响应类型是opaque,意思是能读取响应内容;same-origin:只允许同源请求;navigate:用于支持HTML导航,一般用不到。
-
redirect:用于指定如何处理重定向响应。follow:默认值,跟踪重定向请求,以最终非重定向的URL的响应作为最终响应;error:遇到重定向时抛出错误;manual:不跟踪重定向,但是返回opaqueredirect类型的响应,并暴露期望重定向的URL。允许以手动方式跟踪重定向。
-
referrer:用于指定HTTP的Referer头部的内容,值为以下字符串之一:no-referrer:以no-referrer作为值;client/about:client:以当前URL或no-referrer作为值;<URL>:以伪造的URL作为值。伪造URL的源必须与执行脚本的源匹配。
2. 常见的Fetch请求模式
与XMLHttpRequest类似,Fetch既能够接收数据,也能够发送数据。
(1) 发送JSON数据
let data = JSON.stringify({
uname: 'cc',
age: 18
})
// 初始化请求头
let jsonHeaders = new Headers({
'Content-Type': 'application/json'
})
fetch('/send-json-data', {
method: 'POST',
header: jsonHeaders,
body: data
})
(2) 在请求体中发送参数
请求体可以是String实例,这意味着它可以是任意字符串,可以用来发送请求参数。
let params = 'uname=cc&age=18'
let paramHeaders = new Headers({
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
});
fetch('/send-params', {
method: 'POST',
header: paramHeaders,
body: params
})
(3) 发送文件
请求体可以是FormData实例,因此fetch()可以序列化并发送文件字段中的文件。
// 初始化一个FormData实例
let formdata = new FormData()
// 选择单文件输入器(假设已经选择了文件)
let fileInput = document.querySelector('input[type="file"]')
// 给formdata添加一个文件
formdata.append('file1', fileInput.files[0])
// 发送文件
fetch('/send-file', {
method: 'POST',
body: formdata
})
当然,也可以给FormData实例添加多个文件,然后通过fetch()发送多个文件。
// 初始化一个FormData实例
let formdata = new FormData()
// 选择多文件输入器(假设已经选择了文件)
let fileInput = document.querySelector('input[type="file"][multiple]')
// 给formdata添加多个文件
for(let i = 0; i < fileInput.files.length; i++){
formdata.append(`file${i + 1}`, fileInput.files[i])
}
// 发送文件
fetch('send-files', {
method: 'POST',
body: formdata
})
(4) 加载Blob文件
Fetch API可以提供Blob类型的响应,而Blob类型又兼容多种浏览器API。以图片为例,可以将图片文件加载到内存,然后将其添加到HTML元素上。我们可以使用Response对象上的blob()方法,该方法返回一个期约,解决为一个Blob实例。将这个Blob实例传给URL.createObjectUrl()方法,可以生成添加给图片元素src属性的值:
const img = document.createElement('img')
fetch('/cc.jpg')
.then(resp => resp.blob())
.then(blob => {
img.src = URL.createObjectUrl(blob)
document.body.appenChild(img)
})
(5) 发送跨源请求
发送跨源请求,响应头需要包含CORS头部才能保证浏览器收到响应。若没有这些头部,则跨源请求会失败并抛出错误。如果不需要访问响应,也可以发送no-cors请求,此时响应的类型为opaque,意思是无法读取响应,这种请求适合作为探测请求。
(6) 中断请求
Fetch API可以通过 AbortController/AbortSignal 对来中断请求。调用AbortController.abort()方法会中断所有网络传输,尤其适合传输大型负载的情况 (如长视频) 。中断的fetch()请求会导致包含错误的拒绝。
const abortController = new AbortController()
fetch('/cc-play-game.mp4', { signal: abortController.signal })
.catch(() => {
console.log('请求已中断!')
})
// 50毫秒后中断请求
setTimeout(abortController.abort, 50)
// 请求已中断!
3. Headers对象
Headers是所有请求和响应的头部的容器。每个外派的Request请求和入站的Response对象都有一个Headers实例,可以通过Request.prototype.header和Response.prototype.header来访问。这两个属性都可以修改,此外,也可以通过new Headers()来创建一个Headers实例。
(1) Headers 与 Map
这两者极其相似,都有get(),set(),delete(),has()等方法。都可以使用一个可迭代对象来初始化。
let seed = [['name', 'cc']]
let header = new Headers(seed)
let map = new Map(seed)
console.log(header.get('name')) // cc
console.log(map.get('name')) // cc
都有相同的keys(),values(),entries() 迭代器接口。
(2) Headers独有的特性
Headers在初始化时,也可以使用键值对的形式,而Map不可以。Headers实例的一个字段,可以通过append()方法添加多个值,后续的值会以逗号分隔,拼接在原来的值后面。
// 以键值对形式初始化
const header = new Headers({
name: 'cc'
})
console.log(header.get('name')) // cc
// 给同一个字段追加值
header.append('name', 'yy')
console.log(header.get('name')) // cc,yy
(3) 头部护卫
头部护卫根据Request对象的来源不同而不同,它能保护相关头部字段不被修改,违反护卫限制则会抛出TypeError。护卫只能是以下几种之一:
none:在通过构造函数创建Headers实例时激活,不会限制字段的修改;request:在通过构造函数初始化Headers实例,且mode值不为no-cors时激活,不允许修改禁止修改的请求头部;request-no-cors:在通过构造函数初始化Headers实例,且mode值为no-cors时激活,不允许修改非简单头部;response:在通过构造函数初始化Response对象时激活,不允许修改禁止修改的响应头部;immutable:在通过error()或redirect()静态方法初始化Response对象时激活,不允许修改任何头部。
4. Request对象
(1) 创建Request对象
可以通过构造函数来创建,需要传入一个input参数,一般是URL。
const req = new Request('https://www.baidu.com')
也接收第二个参数,一个init对象。与fetch()接收的参数一样,不在赘述。
(2) 克隆Request对象
- 使用构造函数来克隆;
- 使用
clone()方法来克隆。
将一个Request实例传给构造函数,可以得到该请求对象的一个副本。如果同时传入了init对象,则init对象的值会覆盖原来的同名字段。使用这种方法克隆请求对象之后,源请求对象会被标记为 已使用 ,即其bodyUsed属性会变为true。如果源对象与新对象不同源,则新对象的referrer属性会被清除。此外,如果源对象的mode属性为navigate,则新对象的mode属性会被转换为same-origin。
第二中克隆方式是使用clone()方法,这个方法会得到一模一样的实例。且不会把任何请求的请求体标记为已使用。
需要注意的是,不论使用哪种方式,只有bodyUsed为false的请求对象才可以被克隆,否则会抛出TypeError。
let req1 = new Request('https://www.baidu.com')
let req11 = req1.clone(req1)
new Request(req1)
// 没有错误
req1.text() // 使用req1,会将其bodyUsed标记为true
req1.clone() // TypeError
new Request(req1) // TypeError
(3) 在fetch()中使用Request对象
给fetch()传入Request实例,相当于在fetch()内部使用Request构造函数克隆了一份传入的Request实例。同样的,给fetch()对象传入的第二个参数init的值会覆盖Request实例上的同名字段;请求体被标记为已使用的Request实例传给fetch()也会抛出TypeError;被fetch()使用过的请求对象,其请求体会被标记为已使用,无法再次传给fetch()使用,当然,没有请求体的Request实例不受此限制,因此,如要复用Request实例,则每次传给fetch()的应该是使用clone()方法克隆出来的实例副本。
5. Response对象
(1) 创建Response对象
可以通过构造函数创建Response对象,且无需任何参数。此时创建的实例各属性均为默认值,因为它并不代表实际的HTTP响应。
(2) 克隆 Response对象
主要使用clone()方法,创建一个一模一样的实例副本,且不会将bodyUsed属性标记为true。
6. Request、Response、Body混入
Request和Response对象都使用了Fetch API的Body混入,这个混入为两种类型提供了只读的body属性 (实现为ReadableStream) 、bodyUsed (标记body流是否已读),以及一组方法,用于读取body流的内容并转换为某种类型的JavaScript对象类型。
Body混入提供了5种方法,用于读取流内容并转换为对应的JavaScript对象类型。
(1) Body.text()
返回一个期约,解决为utf-8格式的字符串。如下例演示了在Response对象上使用text()。
fetch('/cc-intro.com')
.then(resp => resp.text())
.then(txt => console.log(txt))
// <!doctype html><html lang="en">
// <head>
// <meta charset="utf-8">
// ...
(2) Body.json()
返回一个期约,解决为JSON。
fetch('/cc-intro.json')
.then(resp => resp.json())
.then(json => console.log(json))
// {name: "cc", age: 18}
(3) Body.formdata()
返回一个期约,解决为FormData实例。
fetch('https://cc.com/form-data')
.then((response) => response.formData())
.then((formData) => console.log(formData.get('name'));
// cc
(4) Body.arrayBuffer()
返回期约,解决为ArrayBuffer实例。
(5) Body.blob()
返回期约,解决为Blob实例。
(6) 一次性流
Body混入是建立在ReadableStream的基础之上的,因此主体流只能使用一次,这意味着以上五种方法只能选择其中的一种调用一次,再次调用以上任何方法则会报错。
(7) 使用ReadableStream主体
由于对流的理解尚为浅薄,此处暂时就不班门弄斧来介绍相关要点了。
六、 Beacon API
在页面关闭(unload)事件触发时,分析工具应停止收集信息,并将收集到的信息发送给服务器。异步XMLHttpRequest或fetch()都不太适合这个任务,因为浏览器会将unload事件处理程序中的网络请求取消,毕竟对浏览器而言,没有任何理由需要在页面关闭后还继续发送请求。虽然同步XMLHttpRequest可以完成这个任务,但是会造成用户关闭浏览器的延迟,影响用户体验。(实际上,设置fetch()中的init参数对象的keepalive为true,也可以允许在页面关闭后维持请求的生命周期。)
为此,W3C引入了Beacon API来解决这个问题。这个API给navigator对象增加了一个sendBeacon()方法,发送一个POST请求。此处不作详细介绍。
七、 Web Socket
Web Socket(套接字)的目标是通过一个长时连接实现与服务器全双工、双向的通信。使用自定义协议:ws://、wss://,前者是非安全连接,后者是安全连接,而不再使用http://、https://。使用自定义协议,允许客户端和服务器之间发送非常少的数据,不会给HTTP造成任何负担。
1. API
要创建一个新的Web Socket,就要提供一个绝对URL链接来实例化一个WebSocket对象。浏览器同源策略不适用于Web Socket,因此传入的URL可以是打开到任意站点的链接。而是否与特定源的页面通信,就完全取决于服务器了。
const ws = new WebSocket('ws://baidu.com')
浏览器在初始化Web Socket后会立即简历连接。与XHR类似,Web Socket也有一个readyState属性表示连接状态,但是其取值与XHR不一样。
WebSocket.OPENING(0):正在连接;WebSocket.OPEN(1):连接已经建立;WebSocket.CLOSING(2):正在关闭连接;WebSocket.CLOSE(3):连接已关闭。
WebSocket对象没有readystatechange事件,不过有与各个状态的其它事件。readyState的值从0开始。
任何时候都可以调用close()方法来关闭连接:ws.close()。调用关闭方法之后,readyState的值立即变为2,并在关闭完成之后变为3。
2. 发送 与 接收数据
要向服务器发送数据,可以使用send()方法,传入一个字符串、ArrayBuffer 或者 Blob 。
const ws = new WebSocket('wss://www.baidu.com');
let strData = 'I am cc',
arrayBuffer = int8Array.from(['f', 'o', 'o']),
blobData = new Blob(['c', 'c', 18]);
// 发送字符串数据
ws.send(strData);
// 发送ArrayBuffer数据
ws.send(arrayBuffer);
// 发送Blob数据
ws.send(blobData);
当服务器向客户端发送消息时,WebSocket对象会触发message事件,可以在onmessage事件处理程序中进行处理。该事件与其它消息协议类似,接收一个event事件对象,且可以通过event.data来访问到有效载荷:
ws.onmessage = (event) => {
console.log(event.data)
// 其它处理
// ...
}
与send()方法相似,接收到的event.data也可能是ArrayBuffer或Blob。这由WebSocket对象的binaryType决定,该属性可能是blob或arraybuffer。
3. 其它事件
在WebSocket的连接生命周期中,有可能会触发其它三个事件:
open:在成功建立连接时触发;error:在发生错误时触发,触发后连接无法存续;close:在成功关闭连接时触发。
WebSocket事件不支持DOM LEVEL 2事件监听(即addEventListener()方式),因此需要使用DOM LEVEL 0风格的事件处理程序来监听:
const ws = new WebSocket('wss://www.baidu.com')
ws.onopen = () => console.log('连接成功!')
ws.onclose = (e) => conssole.log('连接已断开!')
ws.onerror = () => console.log('连接发生错误!!')
这三个事件中,只有close事件的event对象上有额外信息。该对象上有3个额外属性:
wasClean:布尔值,表示连接是否干净地关闭;code:来自服务器的数值状态码;reason:字符串,包含服务器发来的消息。
可以将这些信息显示给用户,或者记录到日志里。