第 24 章 网络请求与远程资源
第 24 章 网络请求与远程资源
24.1 XMLHttpRequest 对象
所有现代浏览器都通过 XMLHttpRequest
构造函数原生支持 XHR 对象:
let xhr = new XMLHttpRequest()
24.1.1 使用 XHR
使用 XHR 对象首先要调用 open()
方法,这个方法接收 3 个参数:请求类型(“get
”、"post
"等)、请求 URL,以及表示请求是否异步的布尔值。调用 open()
不会实际发送请求,只是为发送请求做好准备。
xhr.open('get', 'example.php', false)
注意
只能访问同源 URL,也就是域名相同、端口相同、协议相同。如果请求的 URL 与发送请求的页面在任何方面有所不同,则会抛出安全错误。
要发送定义好的请求,必须像下面这样调用 send()
方法:
xhr.open('get', 'example.txt', false)
xhr.send(null)
send()
方法接收一个参数,是作为请求体发送的数据。如果不需要发送请求体,则必须传 null
,因为这个参数在某些浏览器中是必需的。调用 send()
之后,请求就会发送到服务器。
因为这个请求是同步的,所以 JavaScript 代码会等待服务器响应之后再继续执行。收到响应后,XHR
对象的以下属性会被填充上数据。
❑ responseText:作为响应体返回的文本。
❑ responseXML:如果响应的内容类型是"text/xml"或"application/xml",那就是包含响应数据的 XML DOM 文档。
❑ status:响应的 HTTP 状态。
❑ statusText:响应的 HTTP 状态描述。
XHR
对象有一个 readyState
属性,表示当前处在请求/响应过程的哪个阶段。这个属性有如下可能的值。
❑ 0:未初始化(Uninitialized)。尚未调用 open()方法。
❑ 1:已打开(Open)。已调用 open()方法,尚未调用 send()方法。
❑ 2:已发送(Sent)。已调用 send()方法,尚未收到响应。
❑ 3:接收中(Receiving)。已经收到部分响应。
❑ 4:完成(Complete)。已经收到所有响应,可以使用了。
每次 readyState
从一个值变成另一个值,都会触发 readystatechange
事件。可以借此机会检查 readyState
的值。一般来说,我们唯一关心的 readyState
值是 4,表示数据已就绪。为保证跨浏览器兼容,onreadystatechange
事件处理程序应该在调用 open()
之前赋值。
let xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {if (xhr.readyState == 4) {if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {console.log(xhr.responseText)} else {console.log('Request was unsuccessful: ' + xhr.status)}}
}
xhr.open('get', 'example.txt', true)
xhr.send(null)
在收到响应之前如果想取消异步请求,可以调用 abort()
方法:
xhr.abort()
调用这个方法后,XHR 对象会停止触发事件,并阻止访问这个对象上任何与响应相关的属性。中断请求后,应该取消对 XHR 对象的引用。由于内存问题,不推荐重用 XHR 对象。
24.1.2 HTTP 头部
每个 HTTP 请求和响应都会携带一些头部字段,这些字段可能对开发者有用。XHR 对象会通过一些方法暴露与请求和响应相关的头部字段。
默认情况下,XHR 请求会发送以下头部字段。
❑ Accept:浏览器可以处理的内容类型。
❑ Accept-Charset:浏览器可以显示的字符集。
❑ Accept-Encoding:浏览器可以处理的压缩编码类型。
❑ Accept-Language:浏览器使用的语言。
❑ Connection:浏览器与服务器的连接类型。
❑ Cookie:页面中设置的 Cookie。
❑ Host:发送请求的页面所在的域。
❑ Referer:发送请求的页面的 URI。注意,这个字段在 HTTP 规范中就拼错了,所以考虑到兼容性也必须将错就错。(正确的拼写应该是 Referrer。)
❑ User-Agent:浏览器的用户代理字符串。
虽然不同浏览器发送的确切头部字段可能各不相同,但这些通常都是会发送的。如果需要发送额外的请求头部,可以使用 setRequestHeader()
方法。这个方法接收两个参数:头部字段的名称和值。为保证请求头部被发送,必须在 open()
之后、send()
之前调用 setRequestHeader()
。
let xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {if (xhr.readyState == 4) {if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {console.log(xhr.responseText)} else {console.log('Request was unsuccessful: ' + xhr.status)}}
}
xhr.open('get', 'example.php', true)
xhr.setRequestHeader('MyHeader', 'MyValue')
xhr.send(null)
可以使用 getResponseHeader()
方法从 XHR 对象获取响应头部,只要传入要获取头部的名称即可。如果想取得所有响应头部,可以使用 getAllResponseHeaders()
方法,这个方法会返回包含所有响应头部的字符串。
let myHeader = xhr.getResponseHeader('MyHeader')
let allHeaders = xhr.getAllResponseHeaders()/*** getAllResponseHeaders()方法通常返回类似如下的字符串:* 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*/
24.1.3 GET 请求
最常用的请求方法是 GET 请求,用于向服务器查询某些信息。发送 GET 请求最常见的一个错误是查询字符串格式不对。查询字符串中的每个名和值都必须使用 encodeURIComponent()
编码,所有名/值对必须以和号(&)分隔。对 XHR 而言,查询字符串必须正确编码后添加到 URL 后面,然后再传给 open()
方法。
function addURLParam(url, name, value) {url += url.indexOf('? ') == -1 ? '? ' : '&'url += encodeURIComponent(name) + '=' + encodeURIComponent(value)return url
}
24.1.4 POST 请求
第二个最常用的请求是 POST 请求,用于向服务器发送应该保存的数据。每个 POST 请求都应该在请求体中携带提交的数据,而 GET 请求则不然。POST 请求的请求体可以包含非常多的数据,而且数据可以是任意格式。
function submitData() {let xhr = new XMLHttpRequest()xhr.onreadystatechange = function () {if (xhr.readyState == 4) {if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {console.log(xhr.responseText)} else {console.log('Request was unsuccessful: ' + xhr.status)}}}xhr.open('post', 'postexample.php', true)xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')letform = document.getElementById('user-info')xhr.send(serialize(form))
}
注意
POST 请求相比 GET 请求要占用更多资源。从性能方面说,发送相同数量的数据,GET 请求比 POST 请求要快两倍。
24.1.5 XMLHttpRequest Level 2
1.FormData 类型
现代 Web 应用程序中经常需要对表单数据进行序列化,因此 XMLHttpRequest Level 2 新增了 FormData
类型。FormData
类型便于表单序列化,也便于创建与表单类似格式的数据然后通过 XHR 发送。
append()
方法接收两个参数:键和值,相当于表单字段名称和该字段的值。可以像这样添加任意多个键/值对数据。此外,通过直接给 FormData
构造函数传入一个表单元素,也可以将表单中的数据作为键/值对填充进去。
let data = new FormData()
data.append('name', 'Nicholas')let data = new FormData(document.forms[0])
let xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {if (xhr.readyState == 4) {if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {console.log(xhr.responseText)} else {console.log('Request was unsuccessful: ' + xhr.status)}}
}
xhr.open('post', 'postexample.php', true)
let form = document.getElementById('user-info')
xhr.send(new FormData(form))
2.超时
let xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {if (xhr.readyState == 4) {try {if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {console.log(xhr.responseText)} else {console.log('Request was unsuccessful: ' + xhr.status)}} catch (ex) {// 假设由ontimeout处理}}
}
xhr.open('get', 'timeout.php', true)
xhr.timeout = 1000 //设置1秒超时
xhr.ontimeout = function () {console.log('Request did not return in a second.')
}
xhr.send(null)
这个例子演示了使用 timeout
设置超时。给 timeout
设置 1000 毫秒意味着,如果请求没有在 1 秒钟内返回则会中断。此时则会触发 ontimeout
事件处理程序,readyState
仍然会变成 4,因此也会调用 onreadystatechange
事件处理程序。不过,如果在超时之后访问 status
属性则会发生错误。为做好防护,可以把检查 status
属性的代码封装在 try/catch
语句中。
3.overrideMimeType()方法
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)
24.2 进度事件
❑ loadstart:在接收到响应的第一个字节时触发。
❑ progress:在接收响应期间反复触发。
❑ error:在请求出错时触发。
❑ abort:在调用 abort()终止连接时触发。
❑ load:在成功接收完响应时触发。
❑ loadend:在通信完成时,且在 error、abort 或 load 之后触发。
24.2.1 load 事件
Firefox 最初在实现 XHR 的时候,曾致力于简化交互模式。最终,增加了一个 load 事件用于替代 readystatechange
事件。load
事件在响应接收完成后立即触发,这样就不用检查 readyState
属性了。onload
事件处理程序会收到一个 event
对象,其 target
属性设置为 XHR 实例,在这个实例上可以访问所有 XHR 对象属性和方法。不过,并不是所有浏览器都实现了这个事件的 event
对象。考虑到跨浏览器兼容,还是需要像下面这样使用 XHR 对象变量:
let xhr = new XMLHttpRequest()
xhr.onload = function () {if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {console.log(xhr.responseText)} else {console.log('Requestwasunsuccessful: ' + xhr.status)}
}
xhr.open('get', 'altevents.php', true)
xhr.send(null)
只要是从服务器收到响应,无论状态码是什么,都会触发 load
事件。这意味着还需要检查 status
属性才能确定数据是否有效。Firefox、Opera、Chrome 和 Safari 都支持 load 事件。
24.2.2 progress 事件
Mozilla 在 XHR 对象上另一个创新是 progress
事件,在浏览器接收数据期间,这个事件会反复触发。每次触发时,onprogress
事件处理程序都会收到 event
对象,其 target
属性是 XHR
对象,且包含 3 个额外属性:lengthComputable
、position
和 totalSize
。其中,lengthComputable
是一个布尔值,表示进度信息是否可用;position
是接收到的字节数;totalSize
是响应的 Content-Length 头部定义的总字节数。有了这些信息,就可以给用户提供进度条了。
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 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', 'altevents.php', true)
xhr.send(null)
24.3 跨源资源共享
跨源资源共享(CORS, Cross-Origin Resource Sharing)定义了浏览器与服务器如何实现跨源通信。CORS 背后的基本思路就是使用自定义的 HTTP 头部允许浏览器和服务器相互了解,以确实请求或响应应该成功还是失败。
对于简单的请求,比如 GET 或 POST 请求,没有自定义头部,而且请求体是 text/plain 类型,这样的请求在发送时会有一个额外的头部叫 Origin。Origin 头部包含发送请求的页面的源(协议、域名和端口),以便服务器确定是否为其提供响应。
如果服务器决定响应请求,那么应该发送 Access-Control-Allow-Origin 头部,包含相同的源;或者如果资源是公开的,那么就包含"*"。
let xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {if (xhr.readyState == 4) {if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {console.log(xhr.responseText)} else {console.log('Request was unsuccessful: ' + xhr.status)}}
}
xhr.open('get', 'http://www.somewhere-else.com/page/', true)
xhr.send(null)
跨域 XHR 对象允许访问 status
和 statusText
属性,也允许同步请求。出于安全考虑,跨域 XHR 对象也施加了一些额外限制。
❑ 不能使用 setRequestHeader()设置自定义头部。
❑ 不能发送和接收 cookie。
❑ getAllResponseHeaders()方法始终返回空字符串。
24.3.1 预检请求
CORS 通过一种叫预检请求(preflighted request)的服务器验证机制,允许使用自定义头部、除 GET 和 POST 之外的方法,以及不同请求体内容类型。在要发送涉及上述某种高级选项的请求时,会先向服务器发送一个“预检”请求。这个请求使用 OPTIONS 方法发送并包含以下头部。
❑ Origin:与简单请求相同。
❑ Access-Control-Request-Method:请求希望使用的方法。
❑ Access-Control-Request-Headers:(可选)要使用的逗号分隔的自定义头部列表。
Origin: http://www.nczonline.net
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ
在这个请求发送后,服务器可以确定是否允许这种类型的请求。服务器会通过在响应中发送如下头部与浏览器沟通这些信息。
❑ Access-Control-Allow-Origin:与简单请求相同。
❑ Access-Control-Allow-Methods:允许的方法(逗号分隔的列表)。
❑ Access-Control-Allow-Headers:服务器允许的头部(逗号分隔的列表)。
❑ Access-Control-Max-Age:缓存预检请求的秒数。
预检请求返回后,结果会按响应指定的时间缓存一段时间。换句话说,只有第一次发送这种类型的请求时才会多发送一次额外的 HTTP 请求。
24.3.2 凭据请求
默认情况下,跨源请求不提供凭据(cookie、HTTP 认证和客户端 SSL 证书)。可以通过将 withCredentials 属性设置为 true 来表明请求会发送凭据。如果服务器允许带凭据的请求,那么可以在响应中包含如下 HTTP 头部:
Access-Control-Allow-Credentials: true
如果发送了凭据请求而服务器返回的响应中没有这个头部,则浏览器不会把响应交给 JavaScript(responseText
是空字符串,status
是 0, onerror()
被调用)。注意,服务器也可以在预检请求的响应中发送这个 HTTP 头部,以表明这个源允许发送凭据请求。
24.4 替代性跨源技术
24.4.1 图片探测
任何页面都可以跨域加载图片而不必担心限制,因此这也是在线广告跟踪的主要方式。可以动态创建图片,然后通过它们的 onload
和 onerror
事件处理程序得知何时收到响应。
let img = new Image()
img.onload = img.onerror = function () {console.log('Done! ')
}
img.src = 'http://www.example.com/test?name=Nicholas'
24.4.2 JSONP
JSONP 是“JSON with padding”的简写,是在 Web 服务上流行的一种 JSON 变体。
JSONP 格式包含两个部分:回调和数据。回调是在页面接收到响应之后应该调用的函数,通常回调函数的名称是通过请求来动态指定的。而数据就是作为参数传给回调函数的 JSON 数据。
callback({ "name": "Nicholas" })
http://freegeoip.net/json/?callback=handleResponse
JSONP 调用是通过动态创建<script>
元素并为 src
属性指定跨域 URL 实现的。此时的<script>
与<img>
元素类似,能够不受限制地从其他域加载资源。因为 JSONP 是有效的 JavaScript,所以 JSONP 响应在被加载完成之后会立即执行。
function handleResponse(response) {console.log(`You're at IP address ${response.ip}, which is in${response.city}, ${response.region_name}`)
}
let script = document.createElement('script')
script.src = 'http://freegeoip.net/json/? callback=handleResponse'
document.body.insertBefore(script, document.body.firstChild)
JSONP 也有一些缺点。
首先,JSONP 是从不同的域拉取可执行代码。如果这个域并不可信,则可能在响应中加入恶意内容。此时除了完全删除 JSONP 没有其他办法。在使用不受控的 Web 服务时,一定要保证是可以信任的。
第二个缺点是不好确定 JSONP 请求是否失败。虽然 HTML5 规定了<script>
元素的 onerror
事件处理程序,但还没有被任何浏览器实现。为此,开发者经常使用计时器来决定是否放弃等待响应。这种方式并不准确,毕竟不同用户的网络连接速度和带宽是不一样的。
24.5 Fetch API
24.5.1 基本用法
1.分派请求
fetch()
只有一个必需的参数 input
。多数情况下,这个参数是要获取资源的 URL。这个方法返回一个期约。
let r = fetch('/bar')
console.log(r) // Promise <pending>fetch('bar.txt').then((response) => {console.log(response)
})
// Response { type: "basic", url: ... }
请求完成、资源可用时,期约会解决为一个 Response
对象。这个对象是 API 的封装,可以通过它取得相应资源。获取资源要使用这个对象的属性和方法,掌握响应的情况并将负载转换为有用的形式。
2.读取响应
读取响应内容的最简单方式是取得纯文本格式的内容,这要用到 text()
方法。
fetch('bar.txt').then((response) => {response.text().then((data) => {console.log(data)})
})fetch('bar.txt').then((response) => response.text()).then((data) => console.log(data))
// bar.txt的内容
3.处理状态码和请求失败
Fetch API 支持通过 Response
的 status
(状态码)和 statusText
(状态文本)属性检查响应状态。成功获取响应的请求通常会产生值为 200 的状态码,请求不存在的资源通常会产生值为 404 的状态码,请求的 URL 如果抛出服务器错误会产生值为 500 的状态码。
fetch('/bar').then((response) => {console.log(response.status) // 200console.log(response.statusText) // OK
})fetch('/does-not-exist').then((response) => {console.log(response.status) // 404console.log(response.statusText) // NotFound
})fetch('/throw-server-error').then((response) => {console.log(response.status) // 500console.log(response.statusText) // InternalServerError
})
可以显式地设置 fetch()
在遇到重定向时的行为(本章后面会介绍),不过默认行为是跟随重定向并返回状态码不是 300~399 的响应。跟随重定向时,响应对象的 redirected
属性会被设置为 true
,而状态码仍然是 200。
fetch('/permanent-redirect').then((response) => {// 默认行为是跟随重定向直到最终URL// 这个例子会出现至少两轮网络请求// <origin url>/permanent-redirect -> <redirect url>console.log(response.status) // 200console.log(response.statusText) // OKconsole.log(response.redirected) // true
})
通常状态码为 200 时就会被认为成功了,其他情况可以被认为未成功。为区分这两种情况,可以在状态码非 200~299 时检查 Response
对象的 ok
属性。
fetch('/bar').then((response) => {console.log(response.status) //200console.log(response.ok) //true
})
fetch('/does-not-exist').then((response) => {console.log(response.status) //404console.log(response.ok) //false
})
可以通过 url
属性检查通过 fetch()
发送请求时使用的完整 URL。
// foo.com/bar/baz发送的请求
console.log(window.location.href) // https://foo.com/bar/baz
fetch('qux').then((response) => console.log(response.url))
// https://foo.com/bar/qux
fetch('/qux').then((response) => console.log(response.url))
// https://foo.com/qux
fetch('//qux.com').then((response) => console.log(response.url))
// https://qux.com
fetch('https://qux.com').then((response) => console.log(response.url))
// https://qux.com
4.自定义选项
只使用 URL 时,fetch()
会发送 GET 请求,只包含最低限度的请求头。要进一步配置如何发送请求,需要传入可选的第二个参数 init 对象。init 对象要按照下表中的键/值进行填充。
键 | 值 |
---|---|
body | 指定使用请求体时请求体的内容 必须是 Blob、BufferSource、FormData、URLSearchParams、ReadableStream 或 String 的实例 |
cache | 用于控制浏览器与 HTTP 缓存的交互。要跟踪缓存的重定向,请求的 redirect 属性值必须是 “follow”,而且必须符合同源策略限制。必须是下列值之一 Default ❑ fetch() 返回命中的有效缓存。不发送请求 ❑ 命中无效(stale)缓存会发送条件式请求。如果相应已经改变,则更新缓存的值。然后 fetch() 返回缓存的值 ❑ 未命中缓存会发送请求,并缓存响应。然后 fetch() 返回响应 no-store ❑ 浏览器不检查缓存,直接发送请求 ❑ 不缓存响应,直接通过 fetch() 返回 reload ❑ 浏览器不检查缓存,直接发送请求 ❑ 缓存响应,再通过 fetch() 返回 no-cache ❑ 无论命中有效缓存还是无效缓存都会发送条件式请求。如果响应已经改变,则更新缓存的值。然后 fetch() 返回缓存的值 ❑ 未命中缓存会发送请求,并缓存响应。然后 fetch() 返回响应 force-cahce ❑ 无论命中有效缓存还是无效缓存都通过 fetch() 返回。不发送请求 ❑ 未命中缓存会发送请求,并缓存响应。然后 fetch() 返回响应 only-if-cache ❑ 只在请求模式为 same-origin 时使用缓存 ❑ 无论命中有效缓存还是无效缓存都通过 fetch() 返回。不发送请求 ❑ 未命中缓存返回状态码为 504(网关超时)的响应 默认为 default |
credentials | 用于指定在外发送请求中如何包含 cookie。与 XMLHttpRequest 的 widthCredentials 标签类似必须是下列字符串值之一 ❑ omit:不发送 cookie ❑ same-origin:只在请求 URL 与发送 fetch() 请求的页面同源时发送 cookie ❑ include:无论同源还是跨源都包含 cookie 在支持 Credential Management API 的浏览器中,也可以是一个 FederatedCredential 或 PasswordCredential 的实例 默认为 same-origin |
headers | 用于指定请求头部 必须是 Header 对象实例或包含字符串格式键/值对的常规对象 默认值为不包含键/值对的 Headers 对象。这不意味着请求不包含任何头部,浏览器仍然会随请求发送一些头部。虽然这些头部对 JavaScript 不可见,但浏览器的网络检查器可以观察到 |
integrity | 用于强制资源完整性 必须是包含子资源完整性标识符的字符串 默认为空字符串 |
keepalive | 用于指示浏览器允许请求存在时间超出页面生命周期。适合报告事件或分析,比如页面在 fetch() 请求后很快卸载。设置 keepalive 标志的 fetch() 请求可用于替代 Navigator.sendBeacon() 必须是布尔值 默认为 false |
method | 用于指定 HTTP 请求方法 基本上就是如下字符串值: ❑ GET ❑ POST ❑ PUT ❑ PATCH ❑ DELETE ❑ HEAD ❑ OPTIONS ❑ CONNECT ❑ TRACE 默认为 GET |
mode | 用于指定请求模式。这个模式决定来自跨源请求的响应是否有效,以及客户端可以读取多少响应。违反这里指定模式的请求会抛出错误 必须是下列字符串值之一 ❑ cors:允许遵守 CORS 协议的跨源请求。响应是 “CORS 过滤的响应”,意思是响应中可以访问浏览器头部是经过浏览器强制白名单过滤的 ❑ no-cors:允许不需要发送预检请求的跨源请求(HEAD、GET 和只带有满足 CORS 请求头部的 POST)。响应类型是 opaque,意思是不能读取响应内容 ❑ same-origin:任何跨源请求都不允许发送 ❑ navigate:用于支持 HTML 导航,只在文档间导航时使用。基本用不到 在通过构造函数手动创建 Request 实例时,默认为 cors;否则,默认为 no-cors |
redirect | 用于指定如何处理重定向响应(状态码为 301、302、303、307 或 308) 必须是下列字符串值之一 ❑ follow:跟踪重定向请求,以最终非重定向 URL 的响应作为最终响应 ❑ error:重定向请求会抛出错误 ❑ manual:不跟踪重定向请求,而是返回 opaqueredirect 类型的响应,同时仍然暴露期望的重定向 URL。允许手动方式跟踪重定向 默认为 follow |
referrer | 用于指定 HTTP 的 Referer 头部的内容 必须是下列字符串值之一 ❑ no-referrer:以 no-referrer 作为值 ❑ client/about:client:以当前 URL 或 no-referrer(取决于来源策略 referrerPolicy)作为值 ❑ <URL>:以伪造 URL 作为值。伪造 URL 的源必须与执行脚本的源匹配 默认为 client/about:client |
referrerPolicy | 用于指定 HTTP 的 Referer 头部 必须是下列字符串值之一 no-referrer ❑ 请求中不包含 Referer 头部 no-referrer-when-downgrade ❑ 对于从安全 HTTPS 上下文发送到 HTTP URL 的请求时,不包含 Referer 头部 ❑ 对于所有其他请求,将 Referer 设置为完整 URL origin ❑ 对于所有请求,将 Referer 设置为只包含源 same-origin ❑ 对于跨源请求,不包含 Referer 头部 ❑ 对于同源请求,将 Referer 设置为完整 URl strict-origin ❑ 对于从安全 HTTPS 上下文发送到 HTTP URL 的请求,不包含 Referer 头部 ❑ 对于所有其他请求,将 Referer 设置为只包含源 origin-when-cross-origin ❑ 对于跨源请求,将 Referer 设置为只包含源 ❑ 对于同源请求,将 Referer 设置为完整 URL strict-origin-when-cross-origin ❑ 对于从安全 HTTPS 上下文发送到 HTTP URL 的请求,不包含 Referer 头部 ❑ 对于所有其他跨源请求,将 Referer 设置为只包含源 ❑ 对于同源请求,将 Referer 设置为完整 URL unsafe-url ❑ 对于所有请求,将 Referer 设置为完整 URL 默认为 no-referrer-when-downgrade |
signal | 用于支持通过 AbortController 中断进行中的 fetch() 请求 必须是 AbortSignal 的实例 默认为未关联控制器的 AbortSignal 实例 |
24.5.2 常见 Fetch 请求模式
与 XMLHttpRequest
一样,fetch()
既可以发送数据也可以接收数据。使用 init
对象参数,可以配置 fetch()
在请求体中发送各种序列化的数据
1.发送 JSON 数据
let payload = JSON.stringify({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=UTF-8'
})
fetch('/send-me-params', {method: 'POST', // 发送请求体时必须使用一种HTTP方法body: payload,headers: paramHeaders
})
3.发送文件
let imageFormData = new FormData()
let imageInput = document.querySelector("input[type='file']")
imageFormData.append('image', imageInput.files[0])
fetch('/img-upload', {method: 'POST',body: imageFormData
})let imageFormData = new FormData()
let imageInput = document.querySelector("input[type='file'][multiple]")
for (leti = 0; i < imageInput.files.length; ++i) {imageFormData.append('image', imageInput.files[i])
}
fetch('/img-upload', {method: 'POST',body: imageFormData
})
4.加载 Blob 文件
const imageElement = document.querySelector('img')
fetch('my-image.png').then((response) => response.blob()).then((blob) => {imageElement.src = URL.createObjectURL(blob)})
5.发送跨源请求
fetch('//cross-origin.com')
// TypeError: Failed to fetch
// No 'Access-Control-Allow-Origin' header is present on the requested resource.fetch('//cross-origin.com', { method: 'no-cors' }).then((response) => console.log(response.type))
// opaque
6.中断请求
Fetch API 支持通过 AbortController/AbortSignal
对中断请求。调用 AbortController.abort()
会中断所有网络传输,特别适合希望停止传输大型负载的情况。中断进行中的 fetch()
请求会导致包含错误的拒绝。
let abortController = new AbortController()
fetch('wikipedia.zip', { signal: abortController.signal }).catch(() => console.log('aborted! '))
// 10 毫秒后中断请求
setTimeout(() => abortController.abort(), 10)
// 已经中断
24.5.3 Headers 对象
Headers
对象是所有外发请求和入站响应头部的容器。每个外发的 Request
实例都包含一个空的 Headers
实例,可以通过 Request.prototype.headers
访问,每个入站 Response
实例也可以通过 Response.prototype.headers
访问包含着响应头部的 Headers
对象。这两个属性都是可修改属性。另外,使用 new Headers()
也可以创建一个新实例。
1.Headers 与 Map 的相似之处
// Headers 与 Map 类型都有 get()、set()、has()和 delete()等实例方法
let h = new Headers()
let m = new Map()
// 设置键
h.set('foo', 'bar')
m.set('foo', 'bar')
// 检查键
console.log(h.has('foo')) // true
console.log(m.has('foo')) // true
console.log(h.has('qux')) // false
console.log(m.has('qux')) // false
// 获取值
console.log(h.get('foo')) // bar
console.log(m.get('foo')) // bar
// 更新值
h.set('foo', 'baz')
m.set('foo', 'baz')
// 取得更新的值
console.log(h.get('foo')) // baz
console.log(m.get('foo')) // baz
// 删除值
h.delete('foo')
m.delete('foo')
// 确定值已经删除
console.log(h.get('foo')) // undefined
console.log(m.get('foo')) // undefined// 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()迭代器接口
let seed = [['foo', 'bar'],['baz', 'qux']
]
let h = new Headers(seed)
let m = new Map(seed)
console.log(...h.keys()) // foo, baz
console.log(...m.keys()) // foo, baz
console.log(...h.values()) // bar, qux
console.log(...m.values()) // bar, qux
console.log(...h.entries()) // ['foo', 'bar'], ['baz', 'qux']
console.log(...m.entries()) // ['foo', 'bar'], ['baz', 'qux']
2.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()
方法支持添加多个值。在 Headers
实例中还不存在的头部上调用 append()
方法相当于调用 set()
。后续调用会以逗号为分隔符拼接多个值。
let h = new Headers()
h.append('foo', 'bar')
console.log(h.get('foo')) // "bar"
h.append('foo', 'baz')
console.log(h.get('foo')) // "bar, baz"
3.头部护卫
某些情况下,并非所有 HTTP 头部都可以被客户端修改,而 Headers
对象使用护卫来防止不被允许的修改。不同的护卫设置会改变 set()
、append()
和 delete()
的行为。违反护卫限制会抛出 TypeError
。
Headers
实例会因来源不同而展现不同的行为,它们的行为由护卫来控制。JavaScript 可以决定 Headers
实例的护卫设置。下表列出了不同的护卫设置和每种设置对应的行为。
护卫 | 适用情形 | 限制 |
---|---|---|
none | 在通过构造函数创建 Header 实例时激活 | 无 |
request | 在通过构造函数初始化 Request 对象,且 mode 值为非 no-cors 时激活 | 不允许修改禁止修改的头部 |
request-no-cors | 在通过构造函数初始化 Request 对象,且 mode 值为 no-cors 时激活 | 不允许修改费简单头部 |
response | 在通过构造函数初始化 Response 对象时激活 | 不允许修改禁止修改的响应头部 |
immutable | 在通过 error()或 redirect()静态方法初始化 Response 对象时激活 | 不允许修改任何头部 |
24.5.4 Request 对象
1.创建 Request 对象
可以通过构造函数初始化 Request
对象。为此需要传入一个 input
参数,一般是 URL:
let r = new Request('https://foo.com')
console.log(r)
// Request {...}
Request
构造函数也接收第二个参数——一个 init
对象。这个 init
对象与前面介绍的 fetch()
的 init
对象一样。没有在 init
对象中涉及的值则会使用默认值。
2.克隆 Request 对象
使用 Request
构造函数
let r1 = new Request('https://foo.com', { method: 'POST', body: 'foobar' })
let r2 = new Request(r1, { method: 'PUT' })
console.log(r1.method) // 'POST'
console.log(r2.method) // 'PUT'// 这种克隆方式并不总能得到一模一样的副本
// 第一个请求的请求体会被标记为“已使用”
console.log(r1.bodyUsed) // true
console.log(r2.bodyUsed) // false
使用 clone()
方法。如果请求对象的 bodyUsed
属性为 true
(即请求体已被读取),那么上述任何一种方式都不能用来创建这个对象的副本。在请求体被读取之后再克隆会导致抛出 TypeError
。
let r1 = new Request('https://foo.com', { method: 'POST', body: 'foobar' })
let r2 = r1.clone()
console.log(r1.url) // 'https://foo.com/'
console.log(r2.url) // 'https://foo.com/'
console.log(r1.bodyUsed) // false
console.log(r2.bodyUsed) // falsenew Request(r1)
console.log(r1.bodyUsed) // true
new Request(r1) // Cannot construct a Request with a Request object that has already been used.
r1.clone() // unusable
3.在 fetch()中使用 Request 对象
let r = new Request('https://foo.com')
// 向foo.com发送GET请求
fetch(r)
// 向foo.com发送POST请求
fetch(r, { method: 'POST' })
fetch()
会在内部克隆传入的 Request
对象。与克隆 Request
一样,fetch()
也不能拿请求体已经用过的 Request
对象来发送请求:
let r = new Request('https://foo.com', { method: 'POST', body: 'foobar' })
r.text()
fetch(r)
// TypeError: Cannot construct a Request with a Request object that has already been used.
关键在于,通过 fetch
使用 Request
会将请求体标记为已使用。要想基于包含请求体的相同 Request
对象多次调用 fetch()
,必须在第一次发送 fetch()
请求前调用 clone()
。
let r = new Request('https://foo.com', { method: 'POST', body: 'foobar' })
// 3 个都会成功
fetch(r.clone())
fetch(r.clone())
fetch(r)
fetch(r)
24.5.5 Response 对象
1.创建 Response 对象
let r = new Response()
console.log(r)
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: true
// redirected: false
// status: 200
// statusText: "OK"
// type: "default"
// url: ""
// }
Response
构造函数接收一个可选的 body 参数。这个 body
可以是 null
,等同于 fetch()
参数 init
中的 body
。还可以接收一个可选的 init
对象,这个对象可以包含下表所列的键和值。
键 | 值 |
---|---|
headers | 必须是 Headers 对象实例或包含字符串键/值对的常规对象实例 默认为没有键/值对的 Headers 对象 |
status | 表示 HTTP 相应状态码的整数 默认为 200 |
statusText | 表示 HTTP 响应状态的字符串 默认为空字符串 |
let r = new Response('foobar', {status: 418,statusText: "I'm a teapot"
})
console.log(r)
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: false
// redirected: false
// status: 418
// statusText: "I'mateapot"
// type: "default"
// url: ""
// }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 对象:
console.log(Response.redirect('https://foo.com', 301))
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: false
// redirected: false
// status: 301
// statusText: ""
// type: "default"
// url: ""
// }// 提供的状态码必须对应重定向,否则会抛出错误:
Response.redirect('https://foo.com', 200)
// RangeError: Failed to execute 'redirect' on 'Response': Invalid status code
Response.error()
用于产生表示网络错误的 Response
对象(网络错误会导致 fetch()期约被拒绝)。
console.log(Response.error())
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: false
// redirected: false
// status: 0
// statusText: ""
// type: "error"
// url: ""
// }
2.读取响应状态信息
Response
对象包含一组只读属性,描述了请求完成后的状态,如下表所示。
属性 | 值 |
---|---|
headers | 响应包含的 Headers 对象 |
ok | 布尔值,表示 HTTP 状态码的含义。200~299 的状态码返回 true,其他状态码返回 false |
redirected | 布尔值,表示响应是否至少经过一次重定向 |
status | 整数,表示响应的 HTTP 状态码 |
statusText | 字符串,包含对 HTTP 状态码的正式描述。这个值派生自可选的 HTTP Reason-Phrase 字段,因此如果服务器以 Reason-Phrase 为由拒绝响应,这个字段可能是空字符串 |
type | 字符串,包含响应类型。可能是下列字符串值之一 ❑ basic:表示标准的同源响应 ❑ cors:表示标准的跨源响应 ❑ error:表示响应对象是通过 Response.error() 创建的 ❑ opaque:表示 no-cors 的 fetch()返回的跨源响应 ❑ opaqueredirect:表示对 redirect 设置为 manual 的请求的响应 |
url | 包含响应 URL 的字符串。对于重定向响应,这是最终的 URL,非重定向响应就是它产生的 |
fetch('//foo.com').then(console.log)
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: true
// redirected: false
// status: 200
// statusText: "OK"
// type: "basic"
// url: "https://foo.com/"
// }fetch('//foo.com/redirect-me').then(console.log)
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: true
// redirected: true
// status: 200
// statusText: "OK"
// type: "basic"
// url: "https://foo.com/redirected-url/"
// }fetch('//foo.com/does-not-exist').then(console.log)
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: false
// redirected: true
// status: 404
// statusText: "NotFound"
// type: "basic"
// url: "https://foo.com/does-not-exist/"
// }fetch('//foo.com/throws-error').then(console.log)
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: false
// redirected: true
// status: 500
// statusText: "InternalServerError"
// type: "basic"
// url: "https://foo.com/throws-error/"
// }
3.克隆 Response 对象
克隆 Response
对象的主要方式是使用 clone()
方法,这个方法会创建一个一模一样的副本,不会覆盖任何值。这样不会将任何请求的请求体标记为已使用。
let r1 = new Response('foobar')
let r2 = r1.clone()
console.log(r1.bodyUsed) // false
console.log(r2.bodyUsed) // false
如果响应对象的 bodyUsed
属性为 true
(即响应体已被读取),则不能再创建这个对象的副本。在响应体被读取之后再克隆会导致抛出 TypeError
。
let r = new Response('foobar')
r.clone()
// 没有错误
r.text() // 设置bodyUsed为true
r.clone() // Response.clone: Body has already been consumed.
有响应体的 Response
对象只能读取一次。(不包含响应体的 Response
对象不受此限制。)要多次读取包含响应体的同一个 Response
对象,必须在第一次读取前调用 clone()
。
let r = new Response('foobar')
r.clone().text().then(console.log) // foobar
r.clone().text().then(console.log) // foobar
r.text().then(console.log) // foobar
r.text().then(console.log) // Body is unusable: Body has already been read
此外,通过创建带有原始响应体的 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) // Body is unusable: Body has already been read
24.5.6 Request、Response 及 Body 混入
Request
和 Response
都使用了 Fetch API 的 Body
混入,以实现两者承担有效载荷的能力。这个混入为两个类型提供了只读的 body
属性(实现为 ReadableStream
)、只读的 bodyUsed
布尔值(表示 body
流是否已读)和一组方法,用于从流中读取内容并将结果转换为某种 JavaScript 对象类型。
通常,将 Request
和 Response
主体作为流来使用主要有两个原因。一个原因是有效载荷的大小可能会导致网络延迟,另一个原因是流 API 本身在处理有效载荷方面是有优势的。除此之外,最好是一次性获取资源主体。
Body 混入提供了 5 个方法,用于将 ReadableStream
转存到缓冲区的内存里,将缓冲区转换为某种 JavaScript 对象类型,以及通过期约来产生结果。在解决之前,期约会等待主体流报告完成及缓冲被解析。这意味着客户端必须等待响应的资源完全加载才能访问其内容。
1.Body.text()
Body.text()
方法返回期约,解决为将缓冲区转存得到的 UTF-8 格式字符串。
fetch('https://foo.com').then((response) => response.text()).then(console.log)
// <!doctype html><html lang="en">
// <head>
// <meta charset="utf-8">
// ...let request = new Request('https://foo.com', { method: 'POST', body: 'barbazqux' })
request.text().then(console.log) // 'barbazqux'
2.Body.json()
Body.json()
方法返回期约,解决为将缓冲区转存得到的 JSON。
fetch('https://foo.com/foo.json').then((response) => response.json()).then(console.log) // {"foo": "bar"}let request = new Request('https://foo.com', { method: 'POST', body: JSON.stringify({ bar: 'baz' }) })
request.json().then(console.log) // { bar: 'baz' }
3.Body.formData()
浏览器可以将 FormData
对象序列化/反序列化为主体。
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()
有时候,可能需要以原始二进制格式查看和修改主体。为此,可以使用 Body.arrayBuffer()
将主体内容转换为 ArrayBuffer
实例。Body.arrayBuffer()
方法返回期约,解决为将缓冲区转存得到的 ArrayBuffer
实例。
fetch('https://foo.com').then((response) => response.arrayBuffer()).then(console.log)
// ArrayBuffer(...) {}let request = new Request('https://foo.com', { method: 'POST', body: 'abcdefg' })
// 以整数形式打印二进制编码的字符串
request.arrayBuffer().then((buf) => console.log(new Int8Array(buf)))
// Int8Array { [Iterator] 0: 97, 1: 98, 2: 99, 3: 100, 4: 101, 5: 102, 6: 103 }
5.Body.blob()
有时候,可能需要以原始二进制格式使用主体,不用查看和修改。为此,可以使用 Body.blob()
将主体内容转换为 Blob
实例。Body.blob()
方法返回期约,解决为将缓冲区转存得到的 Blob
实例。
fetch('https://foo.com').then((response) => response.blob()).then(console.log)
// Blob { size: 8358, type: 'text/html;charset=utf-8' }let request = new Request('https://foo.com', { method: 'POST', body: 'abcdefg' })
request.blob().then(console.log)
// Blob { size: 7, type: 'text/plain;charset=utf-8' }
6.一次性流
因为 Body
混入是构建在 ReadableStream
之上的,所以主体流只能使用一次。这意味着所有主体混入方法都只能调用一次,再次调用就会抛出错误。
fetch('https://foo.com').then((response) => response.blob().then(() => response.blob())) // Body is unusable: Body has already been readlet request = new Request('https://foo.com', { method: 'POST', body: 'foobar' })
request.blob().then(() => request.blob()) // Body is unusable: Body has already been read
即使是在读取流的过程中,所有这些方法也会在它们被调用时给 ReadableStream
加锁,以阻止其他读取器访问
fetch('https://foo.com').then((response) => {response.blob() // 第一次调用给流加锁response.blob() // 第二次调用再次加锁会失败 Body is unusable: Body has already been read
})let request = new Request('https://foo.com', { method: 'POST', body: 'foobar' })
request.blob() // 第一次调用给流加锁
request.blob() // 第二次调用再次加锁会失败 Body is unusable: Body has already been read
作为 Body 混入的一部分,bodyUsed
布尔值属性表示 ReadableStream
是否已摄受(disturbed),意思是读取器是否已经在流上加了锁。这不一定表示流已经被完全读取。
let request = new Request('https://foo.com', { method: 'POST', body: 'foobar' })
let response = new Response('foobar')
console.log(request.bodyUsed) // false
console.log(response.bodyUsed) // false
request.text().then(console.log) // foobar
response.text().then(console.log) // foobar
console.log(request.bodyUsed) // true
console.log(response.bodyUsed) // true
7.使用 ReadableStream 主体
ReadableStream
暴露了 getReader()
方法,用于产生 ReadableStream-DefaultReader
,这个读取器可以用于在数据到达时异步获取数据块。数据流的格式是 Uint8Array。
fetch('https://fetch.spec.whatwg.org/').then((response) => response.body).then((body) => {let reader = body.getReader()console.log(reader) // ReadableStreamDefaultReader {}reader.read().then(console.log)})
// { value: Uint8Array{}, done: false }fetch('https://fetch.spec.whatwg.org/').then((response) => response.body).then((body) => {let reader = body.getReader()function processNextChunk({ value, done }) {if (done) {return}console.log(value)return reader.read().then(processNextChunk)}return reader.read().then(processNextChunk)})
// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false }
// ...
异步函数非常适合这样的 fetch()
操作。可以通过使用 async/await
将上面的递归调用打平:
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)}})
另外,read()
方法也可以真接封装到 Iterable
接口中。因此就可以在 for-await-of
循环中方便地实现这种转换:
fetch('https://fetch.spec.whatwg.org/').then((response) => response.body).then(async function (body) {let reader = body.getReader()let asyncIterable = {[Symbol.asyncIterator]() {return {next() {return reader.read()}}}}for await (chunk of asyncIterable) {console.log(chunk)}})
如果流因为耗尽或错误而终止,读取器会释放锁,以允许不同的流读取器继续操作:
async function* streamGenerator(stream) {const reader = stream.getReader()try {while (true) {const { value, done } = await reader.read()if (done) {break}yield value}} finally {reader.releaseLock()}
}fetch('https://fetch.spec.whatwg.org/').then((response) => response.body).then(async function (body) {for await (chunk of streamGenerator(body)) {console.log(chunk)}})
要将 Uint8Array
转换为可读文本,可以将缓冲区传给 TextDecoder
,返回转换后的值。通过设置 stream: true
,可以将之前的缓冲区保留在内存,从而让跨越两个块的内容能够被正确解码:
let decoder = new TextDecoder()
async function* streamGenerator(stream) {const reader = stream.getReader()try {while (true) {const { value, done } = await reader.read()if (done) {break}yield value}} finally {reader.releaseLock()}
}
fetch('https://fetch.spec.whatwg.org/').then((response) => response.body).then(async function (body) {for await (chunk of streamGenerator(body)) {console.log(decoder.decode(chunk, { stream: true }))}})
// <!doctype html><html lang="en"> ...
// whether a <a data-link-type="dfn" href="#concept-header" ...
// result to <var>rangeValue</var>. ...
// ...
因为可以使用 ReadableStream
创建 Response
对象,所以就可以在读取流之后,将其通过管道导入另一个流。然后在这个新流上再使用 Body
的方法,如 text()
。这样就可以随着流的到达实时检查和操作流内容。
fetch('https://fetch.spec.whatwg.org/').then((response) => response.body).then((body) => {const reader = body.getReader()// 创建第二个流return new ReadableStream({async start(controller) {try {while (true) {const { value, done } = await reader.read()if (done) {break}// 将主体流的块推到第二个流controller.enqueue(value)}} finally {controller.close()reader.releaseLock()}}})}).then((secondaryStream) => new Response(secondaryStream)).then((response) => response.text()).then(console.log)
// <!doctype html><html lang="en"><head><meta charset="utf-8"> ...
24.6 Beacon API
为了把尽量多的页面信息传到服务器,很多分析工具需要在页面生命周期中尽量晚的时候向服务器发送遥测或分析数据。因此,理想的情况下是通过浏览器的 unload
事件发送网络请求。这个事件表示用户要离开当前页面,不会再生成别的有用信息了。
在 unload
事件触发时,分析工具要停止收集信息并把收集到的数据发给服务器。这时候有一个问题,因为 unload
事件对浏览器意味着没有理由再发送任何结果未知的网络请求(因为页面都要被销毁了)。例如,在 unload
事件处理程序中创建的任何异步请求都会被浏览器取消。为此,异步 XMLHttpRequest
或 fetch()
不适合这个任务。分析工具可以使用同步 XMLHttpRequest
强制发送请求,但这样做会导致用户体验问题。浏览器会因为要等待 unload
事件处理程序完成而延迟导航到下一个页面。
为解决这个问题,W3C 引入了补充性的 Beacon API。这个 API 给 navigator
对象增加了一个 sendBeacon()
方法。这个简单的方法接收一个 URL 和一个数据有效载荷参数,并会发送一个 POST 请求。可选的数据有效载荷参数有 ArrayBufferView
、Blob
、DOMString
、FormData
实例。如果请求成功进入了最终要发送的任务队列,则这个方法返回 true
,否则返回 false
。
可以像下面这样使用这个方法:
// 发送POST请求
// URL: 'https://example.com/analytics-reporting-url'
// 请求负载:'{foo: "bar"}'
navigator.sendBeacon('https://example.com/analytics-reporting-url', '{foo: "bar"}')
这个方法虽然看起来只不过是 POST 请求的一个语法糖,但它有几个重要的特性。
❑ sendBeacon()并不是只能在页面生命周期末尾使用,而是任何时候都可以使用。
❑ 调用 sendBeacon()后,浏览器会把请求添加到一个内部的请求队列。浏览器会主动地发送队列中的请求。
❑ 浏览器保证在原始页面已经关闭的情况下也会发送请求。
❑ 状态码、超时和其他网络原因造成的失败完全是不透明的,不能通过编程方式处理。
❑ 信标(beacon)请求会携带调用 sendBeacon()时所有相关的 cookie。
24.7 Web Socket
Web Socket(套接字)的目标是通过一个长时连接实现与服务器全双工、双向的通信。在 JavaScript 中创建 Web Socket 时,一个 HTTP 请求会发送到服务器以初始化连接。服务器响应后,连接使用 HTTP 的 Upgrade 头部从 HTTP 协议切换到 Web Socket 协议。这意味着 Web Socket 不能通过标准 HTTP 服务器实现,而必须使用支持该协议的专有服务器。
因为 Web Socket 使用了自定义协议,所以 URL 方案(scheme)稍有变化:不能再使用 http://或 https://,而要使用 ws://和 wss://。前者是不安全的连接,后者是安全连接。在指定 Web Socket URL 时,必须包含 URL 方案,因为将来有可能再支持其他方案。
使用自定义协议而非 HTTP 协议的好处是,客户端与服务器之间可以发送非常少的数据,不会对 HTTP 造成任何负担。使用更小的数据包让 Web Socket 非常适合带宽和延迟问题比较明显的移动应用。使用自定义协议的缺点是,定义协议的时间比定义 JavaScript API 要长。Web Socket 得到了所有主流浏览器支持。
24.7.1 API
要创建一个新的 Web Socket,就要实例化一个 WebSocket
对象并传入提供连接的 URL:
let socket = new WebSocket('ws://www.example.com/server.php')
注意
必须给WebSocket
构造函数传入一个绝对 URL。同源策略不适用于 Web Socket,因此可以打开到任意站点的连接。至于是否与来自特定源的页面通信,则完全取决于服务器。(在握手阶段就可以确定请求来自哪里。)
浏览器会在初始化 WebSocket
对象之后立即创建连接。与 XHR
类似,WebSocket
也有一个 readyState
属性表示当前状态。不过,这个值与 XHR
中相应的值不一样。
❑ WebSocket.OPENING(0):连接正在建立。
❑ WebSocket.OPEN(1):连接已经建立。
❑ WebSocket.CLOSING(2):连接正在关闭。
❑ WebSocket.CLOSE(3):连接已经关闭。
WebSocket
对象没有 readystatechange
事件,而是有与上述不同状态对应的其他事件。readyState
值从 0 开始。
何时候都可以调用 close()
方法关闭 Web Socket 连接:
socket.close()
调用 close()
之后,readyState
立即变为 2(连接正在关闭),并会在关闭后变为 3(连接已经关闭)。
24.7.2 发送和接收数据
打开 Web Socket 之后,可以通过连接发送和接收数据。要向服务器发送数据,使用 send()
方法并传入一个字符串、ArrayBuffer
或 Blob
。
let socket = new WebSocket('ws://www.example.com/server.php')
let stringData = 'Hello world! '
let arrayBufferData = Uint8Array.from(['f', 'o', 'o'])
let blobData = new Blob(['f', 'o', 'o'])
socket.send(stringData)
socket.send(arrayBufferData.buffer)
socket.send(blobData)
服务器向客户端发送消息时,WebSocket
对象上会触发 message
事件。这个 message
事件与其他消息协议类似,可以通过 event.data
属性访问到有效载荷,event.data
返回的数据也可能是 ArrayBuffer
或 Blob
。这由 WebSocket
对象的 binaryType
属性决定,该属性可能是"blob
"或"arraybuffer
"。
socket.onmessage = function (event) {let data = event.data// 对数据执行某些操作
}
24.7.3 其他事件
WebSocket 对象在连接生命周期中有可能触发 3 个其他事件。
❑ open:在连接成功建立时触发。
❑ error:在发生错误时触发。连接无法存续。
❑ close:在连接关闭时触发。
WebSocket
对象不支持 DOM Level 2 事件监听器,因此需要使用 DOM Level 0 风格的事件处理程序来监听这些事件:
let socket = new WebSocket('ws://www.example.com/server.php')
socket.onopen = function () {console.log('Connection established.')
}
socket.onerror = function () {console.log('Connection error.')
}
socket.onclose = function () {console.log('Connection closed.')
}
在这些事件中,只有 close
事件的 event
对象上有额外信息。这个对象上有 3 个额外属性:wasClean
、code
和 reason
。其中,wasClean
是一个布尔值,表示连接是否干净地关闭;code
是一个来自服务器的数值状态码;reason
是一个字符串,包含服务器发来的消息。
24.8 安全
关于安全防护 Ajax 相关 URL 的一般理论认为,需要验证请求发送者拥有对资源的访问权限。可以通过如下方式实现。
❑ 要求通过 SSL 访问能够被 Ajax 访问的资源。
❑ 要求每个请求都发送一个按约定算法计算好的令牌(token)。
注意,以下手段对防护 CSRF 攻击是无效的。
❑ 要求 POST 而非 GET 请求(很容易修改请求方法)。
❑ 使用来源 URL 验证来源(来源 URL 很容易伪造)。
❑ 基于 cookie 验证(同样很容易伪造)。
相关文章:
第 24 章 网络请求与远程资源
第 24 章 网络请求与远程资源 24.1 XMLHttpRequest 对象 所有现代浏览器都通过 XMLHttpRequest 构造函数原生支持 XHR 对象: let xhr new XMLHttpRequest()24.1.1 使用 XHR 使用 XHR 对象首先要调用 open()方法,这个方法接收 3 个参数:请…...
k8s dashboard离线部署步骤
确定k8s版本,以1.23为例。 部署metrics-server服务,最好用v0.5.2。 用v0.6.0,可能会报以下错误: nodekubemaster:~/Desktop/metric$ kubectl top nodes Error from server (ServiceUnavailable): the server is currently unabl…...
热备份路由HSRP及配置案例
✍作者:柒烨带你飞 💪格言:生活的情况越艰难,我越感到自己更坚强;我这个人走得很慢,但我从不后退。 📜系列专栏:网路安全入门系列 目录 一,HSRP的相关概念二,…...
【文本分类】bert二分类
import os import torch from torch.utils.data import DataLoader, Dataset from transformers import BertTokenizer, BertForSequenceClassification, AdamW from sklearn.metrics import accuracy_score, classification_report from tqdm import tqdm# 自定义数据集 class…...
计算机网络 (30)多协议标签交换MPLS
前言 多协议标签交换(Multi-Protocol Label Switching,MPLS)是一种在开放的通信网上利用标签引导数据高速、高效传输的新技术。 一、基本概念 MPLS是一种第三代网络架构技术,旨在提供高速、可靠的IP骨干网络交换。它通过将IP地址映…...
【Springer斯普林格出版,Ei稳定,往届快速见刊检索】第四届电子信息工程、大数据与计算机技术国际学术会议(EIBDCT 2025)
第四届电子信息工程、大数据与计算机技术国际学术会议(EIBDCT 2025)将于2025年2月21-23日在中国青岛举行。该会议主要围绕电子信息工程、大数据、计算机技术等研究领域展开讨论。会议旨在为从事相关科研领域的专家学者、工程技术人员、技术研发人员提供一…...
C# 修改项目类型 应用程序程序改类库
初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的,可以在任何平台上使用。 源码指引:github源…...
[开源]自动化定位建图系统
系统状态机: 效果展示: 1、 机器人建图定位系统-基础重定位,定位功能演示 2、 机器人建图定位系统-增量地图构建,手动回环检测演示 3、… 开源链接: https://gitee.com/li-wenhao-lwh/lifelong-backend Qt人机交互…...
OSPF使能配置
OSPF路由协议是用于网际协议(ip)网络的链路状态的路由协议。该协议使用链路状态路由算法的内部网关协议(IGP),在单一自治系统(AS)内部工作。适用于IPV4的OSPFv2协议定义于RFC 2328,R…...
ES_如何设置ElasticSearch 8.0版本的匿名访问以及https_http模式的互相切换
总结: 设置匿名访问,只需要设置xpack.security.authc.anonymous.username和xpack.security.authc.anonymous.roles参数就行,设置好后,可以匿名访问也可以非匿名访问,但是非匿名访问的情况下必须保证用户名和密码正确 取…...
web移动端UI框架
文章目录 Vant简介主要特点和功能适用场景和用户评价 Mint UI简介主要特点和功能 cube-ui简介特性 iView Weapp简介 uni-app简介 Vant 使用vue3版本官网:https://vant-ui.github.io/vant/#/zh-CN/ 适用vue2版本官网:https://vant-ui.github.io/vant/v2/…...
数据库高安全—角色权限:权限管理权限检查
目录 3.3 权限管理 3.4 权限检查 书接上文数据库高安全—角色权限:角色创建角色管理,从角色创建和角色管理两方面对高斯数据库的角色权限进行了介绍,本篇将从权限管理和权限检查方面继续解读高斯数据库的角色权限。 3.3 权限管理 &#x…...
spring boot controller放到那一层
在 Spring Boot 应用程序中,Controller 层通常被放置在应用程序的 表示层(Presentation Layer) 或 用户界面层(UI Layer) 中。Controller 层的主要职责是处理用户的 HTTP 请求,并将请求转发给服务层进行业务…...
报错 - cannot import name ‘ExportOptions‘ from ‘torch.onnx._internal.exporter‘
调用库时出现错误: ImportError: cannot import name ‘ExportOptions’ from ‘torch.onnx._internal.exporter’ 尝试更新 onnx, onnxscript,diffusers 均没有解决问题 将 torch 升级(从 2.1.0 到 2.5.1)后解决了 具…...
恒压恒流原边反馈控制芯片 CRE6289F
CRE6289F 系列产品是一款内置高压 MOS 功率开关管的高性能多模式原边控制的开关电源芯片。较少的外围元器件、较低的系统成本设计出高性能的交直流转换开关电源。CRE6289F 系列产品提供了极为全面和性能优异的智能化保护功能,包括逐周期过流保护、软启动、芯片过温保…...
ffmpeg视频抽帧和合成
FFMPEG 抽取视频场景转换帧 ffmpeg -i input.mp4 -vf "selectgt(scene,0.4),showinfo" -vsync vfr output_%04d.jpg ffmpeg -i input.mp4 -vf "selectgt(scene,0.4),scale1280:720" -vsync vfr output_%03d.jpg # -vsync 已经弃用,最新版本不…...
七、Hadoop环境搭建之安装JDK
文章目录 一、卸载自带JDK二、传输jdk到服务器中三、解压四、配置JDK环境变量 一、卸载自带JDK 注意:安装JDK前,一定确保提前删除了虚拟机自带的JDK。 以下操作,请切换至root权限进行操作 输入:rpm -qa | grep jdk 会查询出系统…...
RocketMQ消息积压问题如何解决?
大家好,我是锋哥。今天分享关于【RocketMQ消息积压问题如何解决?】面试题。希望对大家有帮助; RocketMQ消息积压问题如何解决? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 RocketMQ 消息积压问题通常是指消息队列中的消息堆积过多&…...
【Java基础】进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
1. 进程和线程 1.1 进程 几乎所有的操作系统都支持进程的概念,所有运行中的任务通常对应一个进程(Process)。 当一个程序进入内存运行时,即变成一个进程。 进程是处于运行过程中的程序,并且具有一定的独立功能&…...
CDN防御如何保护我们的网络安全?
在当今数字化时代,网络安全成为了一个至关重要的议题。随着网络攻击的日益频繁和复杂化,企业和个人都面临着前所未有的安全威胁。内容分发网络(CDN)作为一种分布式网络架构,不仅能够提高网站的访问速度和用户体验&…...
深度学习驱动的蛋白质设计技术与实践
通过设计特定的蛋白质结构,可以实现预期的生物功能,如催化特定化学反应、识别和结合特定分子、调控生物信号传导等,为生物医学、药物研发、生物技术等领域提供重要工具和解决方案。传统的蛋白质设计方法主要依赖于已知蛋白质结构的同源建模、…...
Centos7使用yum工具出现 Could not resolve host: mirrorlist.centos.org
在 CentOS 7 中使用 yum 工具时,出现 "Could not resolve host: mirrorlist.centos.org" 的错误,一般情况是因为默认的镜像源无法访问。 以下是一些常用的解决方法: 检查网络连接:首先使用 ping 命令测试网络连接是否…...
iOS - 原子操作
在 Objective-C 运行时中,原子操作主要通过以下几种方式实现: 1. 基本原子操作 // 原子操作的基本实现 #if __has_feature(c_atomic)#define OSAtomicIncrement32(p) __c11_atomic_add((_Atomic(int32_t) *)(p), 1, __ATOMIC_RELAXED) #define …...
PHP二维数组去除重复值
Date: 2025.01.07 20:45:01 author: lijianzhan PHP二维数组内根据ID或者名称去除重复值 代码示例如下: // 假设 data数组如下 $data [[id > 1, name > Type A],[id > 2, name > Type B],[id > 1, name > Type A] // 重复项 ];// 去重方法 $dat…...
【Flink部署】-- 部署 flink 1.19.1 standalone 集群
目录 1、环境准备 2、部署步骤 2.1、下载 flink 二进制包 2.2、配置全局环境变量 2.3、验证 flink 版本 2.4、配置 config.yaml 2.5、启动和停止本地 standalone 集群 2.6、提交测试作业 2.7、查询作业 3、参考 1、环境准备 jdk 11apache flink 1.19.1 安装包2、部署…...
关于物联网的基础知识(二)——物联网体系结构分层
成长路上不孤单😊😊😊😊😊😊 【14后😊///计算机爱好者😊///持续分享所学😊///如有需要欢迎收藏转发///😊】 今日分享关于物联网的基础知识(二&a…...
使用Paddledetection进行模型训练【Part2:数据准备+模型训练】
目录 写在前面 数据准备 模型优化 1. 使用自定义数据 2. 加载预训练模型 3. 启动训练 写在前面 在目标检测算法产业落地过程中,常常会出现需要额外训练以满足实际使用的要求,项目迭代过程中也会出先需要修改类别的情况。本文档详细介绍如何使用Pa…...
使用最广泛的FastAPI项目结构推荐,命名规范等
使用最广泛的FastAPI项目结构推荐,命名规范等 一、FastAPI项目结构如下:二、组件管理:使用依赖注入三、命名约定四、建议分层架构的设计五、文档和测试六、版本控制和持续集成七、环境和配置管理工具八、性能优化与权限安全 一、FastAPI项目结…...
【JAVA】java中将一个list进行拆解重新组装
一、使用场景 1、当需要对一个list中的元素属性进行重新赋值,比如一个list中存储了订单数据,我们需要改变list中每个订单的id,然后再重新输出订单list if(CollectionUtils.isNotEmpty(orderList)){ orderList.forEach(p->{ …...
怎样修改el-table主题样式
起因:el-table有主题样式,部分需要单独设置 环境:ideanodejs插件谷歌浏览器 第一步:找到scss文件: 谷歌浏览器打开表格页面,ctrlshifti打开开发者工具,点击后鼠标移动到表格单元格上单击一下…...
【Docker项目实战】使用Docker部署gallery轻量级图片管理系统
【Docker项目实战】使用Docker部署gallery轻量级图片管理系统 一、SFPG介绍1.1 应用简介1.2 主要特点1.3 主要使用场景二、本次实践规划2.1 本地环境规划2.2 本次实践介绍三、本地环境检查3.1 检查Docker服务状态3.2 检查Docker版本3.3 检查docker compose 版本四、下载gallery…...
缓存-Redis-常见问题-缓存击穿-永不过期+逻辑过期(全面 易理解)
缓存击穿(Cache Breakdown) 是在高并发场景下,当某个热点数据在缓存中失效或不存在时,瞬间大量请求同时击中数据库,导致数据库压力骤增甚至崩溃的现象。为了解决这一问题,“永不过期” “逻辑过期” 的策略…...
【0x006D】HCI_Write_LE_Host_Support命令详解
目录 一、命令概述 二、命令格式及参数说明 2.1. HCI_Write_LE_Host_Support命令格式 2.2. LE_Supported_Host 三、生成事件及参数 3.1. HCI_Command_Complete 事件 3.2. Status 四、命令执行流程 4.1. 命令发起阶段(主机端) 4.2. 命令处理阶段…...
尚硅谷· vue3+ts 知识点学习整理 |14h的课程(持续更ing)
vue3 主要内容 核心:ref、reactive、computed、watch、生命周期 常用:hooks、自定义ref、路由、pinia、miit 面试:组件通信、响应式相关api ----> 笔记:ts快速梳理;vue3快速上手.pdf 笔记及大纲 如下ÿ…...
【JAVA】Java开发小游戏 - 简单的2D平台跳跃游戏 基本的2D平台跳跃游戏框架,适合初学者学习和理解Java游戏开发的基础概念
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默, 忍不住分享一下给大家。点击跳转到网站 学习总结 1、掌握 JAVA入门到进阶知识(持续写作中……) 2、学会Oracle数据库入门到入土用法(创作中……) 3、手把…...
C++ constexpr(八股总结)
答:这个关键字用于指示一个变量、函数或者对象可以在编译时求值(即它们是常量表达式)。这意味着当程序编译时,编译器会尝试计算出该表达式的值,而不是在程序运行时计算它,从而提高了程序的性能。 常量表达…...
继承(5)
大家好,今天我们继续来学习继承的相关知识,来看看子类构造方法(也叫做构造器)是如何做的。 1.6 子类构造方法 父子父子,先有父再有子,即:子类对象构选时,需要先调用基类构造方法,然后执行子类的构造方法 ★此时虽然执行了父类的…...
Oracle Dataguard(主库为 RAC 双节点集群)配置详解(1):安装 Oracle11g RAC 双节点集群
Oracle Dataguard(主库为 RAC 双节点集群)配置详解(1):安装 Oracle11g RAC 双节点集群 目录 Oracle Dataguard(主库为 RAC 双节点集群)配置详解(1):安装 Orac…...
基于Matlab的变压器仿真模型建模方法(13):单相升压自耦变压器的等效电路和仿真模型
1.单相升压自耦变压器的基本方程和等效电路 单相升压自耦变压器的接线原理图如图1所示。在建立自耦变压器的基本方程时,仍然把它看成是从双绕组变压器演变而来。在图1中,设节点a到节点b部分的绕组的匝数为,对应于双绕组变压器的原边绕组;节点c到节点a部分的绕组的绕组匝数为…...
ffmpeg 常用命令
更详细请参考ffmpeg手册,下载ffmpegrelease版后在doc中就有,主页面。video filter 参考ffmpeg-filters.html -version -formats -demuxers -protocols -muxers -filters -devices —pix_fmts -codecs -sample_fmts -decoders -layouts -encoders -colors…...
c#集成itext7导出pdf,包含表格
在Nuget解决方案中搜索itext7,进行安装 同时还要安装 itext7.bouncy-castle-adapter。 否则 PdfWriter writer new PdfWriter(pdfOutputPath);执行时会报错unknown PdfException,然后生成一个空白的pdf,且显示已损坏。 捕获异常发现 ex.In…...
STM32-WWDG/IWDG看门狗
WWDG/IWDG一旦开启不能关闭,可通过选项字节在上电时启动硬件看门狗,看门狗计数只能写入不能读取。看门狗启用时,T6bit必须置1,防止立即重置。 一、原理 独立看门狗-超时复位 窗口看门狗-喂狗(重置计数器,…...
后台管理系统引导功能的实现
引导是软件中经常见到的一个功能,无论是在后台项目还是前台或者是移动端项目中。 那么对于引导页而言,它是如何实现的呢?通常情况下引导页是通过 聚焦 的方式,高亮一块视图,然后通过文字解释的形式来告知用户该功能的作…...
Flink如何设置合理的并行度
一个Flink程序由多个Operator组成(source、transformation和 sink)。 一个Operator由多个并行的Task(线程)来执行, 一个Operator的并行Task(线程)数目就被称为该Operator(任务)的并行度(Parallel)。即并行度就是相对于Operator来说的。 合理设置并行度可以有效提高Flink作业…...
【JMeter】单接口
1. HTTPS接口 前置准备: 下载被测网站的证书在cmd利用java工具keytool.exe重新生成store证书 先进入存放该程序的盘 使用命令: keytool -import -alias "xxxx.store" -file "D:\xxx.cer" -keystore xx.store 根据提示信任证书 在JMeter菜…...
安科瑞Acrel-1000DP分布式光伏监控系统在浙江安吉成3234.465kWp分布式光伏发电项目中的应用
摘 要:分布式光伏发电站是指将光伏发电组件安装在用户的建筑物屋顶、空地或其他适合的场地上,利用太阳能进行发电的一种可再生能源利用方式,与传统的大型集中式光伏电站相比,分布式光伏发电具有更灵活的布局、更低的建设成本和更高…...
电子应用设计方案87:智能AI收纳箱系统设计
智能 AI 收纳箱系统设计 一、引言 智能 AI 收纳箱系统旨在为用户提供更高效、便捷和智能的物品收纳与管理解决方案,通过融合人工智能技术和创新设计,提升用户的生活品质和物品整理效率。 二、系统概述 1. 系统目标 - 实现物品的自动分类和整理…...
C++泛型编程:多参数函数模版、动态数组类
在多参数函数模版中,有以下几点: 第一点是函数模版支持多个类型参数 第二点是一旦有类型不能推导,就会导致编译失败 第三点是返回值类型无法作为推导依据 其中第二点和第三点,在下面代码中有体现,首先是第三点&…...
点亮一个esp32 的led
最近入了一个ESP32 兄弟们,这玩意还可以,买来肯定是给它点亮啊对吧 我就是点灯侠🎇 😭千万不要不接天线啊,不然你会一直找不到你的wifi 1.点灯第一步你得有IDE Arduino 就是这个绿东西 可是怎么下载安装呢ÿ…...
【计算机视觉】单目深度估计模型-Depth Anything-V2
概述 本篇将简单介绍Depth Anything V2单目深度估计模型,该模型旨在解决现有的深度估计模型在处理复杂场景、透明或反射物体时的性能限制。与前一代模型相比,V2版本通过采用合成图像训练、增加教师模型容量,并利用大规模伪标签现实数据进行学…...