type
status
date
slug
summary
tags
category
icon
password
Edited
Sep 15, 2022 01:54 PM
Created
Sep 15, 2022 01:47 PM
Fetch
url
—— 要访问的 URL。
options
—— 可选参数:method,header 等。
没有
options
,这就是一个简单的 GET 请求,下载 url
的内容。响应的属性:
response.status
—— response 的 HTTP 状态码,
response.ok
—— HTTP 状态码为 200-299,则为true
。
response.headers
—— 类似于 Map 的带有 HTTP header 的对象。
获取 response body 的方法:
response.text()
—— 读取 response,并以文本形式返回 response,
response.json()
—— 将 response 解析为 JSON 对象形式,
response.formData()
—— 以FormData
对象(multipart/form-data
编码,参见下一章)的形式返回 response,
response.blob()
—— 以 Blob(具有类型的二进制数据)形式返回 response,
response.arrayBuffer()
—— 以 ArrayBuffer(低级别的二进制数据)形式返回 response。
第一阶段,当服务器发送了响应头(response header),
fetch
返回的 promise
就使用内建的 Response class 对象来对响应头进行解析。第二阶段,为了获取 response body,我们需要使用一个其他的方法调用。
设置请求头:
获取响应头:
Post:
method
—— HTTP 方法,例如POST
,
body
—— request body,其中之一:- 字符串(例如 JSON 编码的),
FormData
对象,以multipart/form-data
形式发送数据,Blob
/BufferSource
发送二进制数据,- URLSearchParams,以
x-www-form-urlencoded
编码形式发送数据,很少使用。
FormData
FormData 对象表示 HTML 表单数据的对象。
如果提供了 HTML
form
元素,它会自动捕获 form
元素字段。FormData
的特殊之处在于网络方法(network methods),例如 fetch
可以接受一个 FormData
对象作为 body。它会被编码并发送出去,带有 Content-Type: multipart/form-data
。方法:
我们可以使用以下方法修改
FormData
中的字段:formData.append(name, value)
—— 添加具有给定name
和value
的表单字段,
formData.append(name, blob, fileName)
—— 添加一个字段,就像它是<input type="file">
,第三个参数fileName
设置文件名(而不是表单字段名),因为它是用户文件系统中文件的名称,
formData.delete(name)
—— 移除带有给定name
的字段,
formData.get(name)
—— 获取带有给定name
的字段值,
formData.has(name)
—— 如果存在带有给定name
的字段,则返回true
,否则返回false
。
.set
移除所有具有给定 name
的字段,然后附加一个新字段formData.set(name, value)
,
formData.set(name, blob, fileName)
。
表单始终以
Content-Type: multipart/form-data
来发送数据,这个编码允许发送文件。因此 <input type="file">
字段也能被发送Fetch 下载进度
要跟踪下载进度,我们可以使用
response.body
属性。它是 ReadableStream
—— 一个特殊的对象,它可以逐块(chunk)提供 body。await reader.read()
调用的结果是一个具有两个属性的对象:done
—— 当读取完成时为true
,否则为false
。
value
—— 字节的类型化数组:Uint8Array
。
示例:
Fetch 中止(Abort)
控制器是一个极其简单的对象。
- 它具有单个方法
abort()
,
- 和单个属性
signal
,我们可以在这个属性上设置事件监听器。
当
abort()
被调用时:controller.signal
就会触发abort
事件。
controller.signal.aborted
属性变为true
。
通常,我们需要处理两部分:
- 一部分是通过在
controller.signal
上添加一个监听器,来执行可取消操作。
- 另一部分是触发取消:在需要的时候调用
controller.abort()
。
与 fetch 一起使用
Fetch 跨源请求
“CORS”:跨源资源共享(Cross-Origin Resource Sharing)
安全请求
有两种类型的跨源请求:
- 安全请求。
- 所有其他请求。
安全请求很简单,所以我们先从它开始。
如果一个请求满足下面这两个条件,则该请求是安全的:
- 安全的方法:GET,POST 或 HEAD
- 安全的 header —— 仅允许自定义下列 header:
Accept
,Accept-Language
,Content-Language
,Content-Type
的值为application/x-www-form-urlencoded
,multipart/form-data
或text/plain
。
任何其他请求都被认为是“非安全”请求。例如,具有
PUT
方法或 API-Key
HTTP-header 的请求就不是安全请求。本质区别在于,可以使用
<form>
或 <script>
进行安全请求,而无需任何其他特殊方法。安全请求的CORS
如果一个请求是跨源的,浏览器始终会向其添加
Origin
header。服务器可以检查
Origin
,如果同意接受这样的请求,就会在响应中添加一个特殊的 header Access-Control-Allow-Origin
。该 header 包含了允许的源,或者一个星号 *
。然后响应成功,否则报错浏览器在这里扮演受被信任的中间人的角色:
- 它确保发送的跨源请求带有正确的
Origin
。
- 它检查响应中的许可
Access-Control-Allow-Origin
,如果存在,则允许 JavaScript 访问响应,否则将失败并报错。
安全的 response header
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
列表中没有
Content-Length
header!要授予 JavaScript 对任何其他 response header 的访问权限,服务器必须发送
Access-Control-Expose-Headers
header。它包含一个以逗号分隔的应该被设置为可访问的非安全 header 名称列表。”非安全“请求
首先,它会先发送一个初步的、所谓的“预检(preflight)”请求,来请求许可。
预检请求使用
OPTIONS
方法,它没有 body,但是有三个 header:Access-Control-Request-Method
header 带有非安全请求的方法。
Access-Control-Request-Headers
header 提供一个以逗号分隔的非安全 HTTP-header 列表。
如果服务器同意处理请求,那么它会进行响应,此响应的状态码应该为 200,没有 body,具有 header:
Access-Control-Allow-Origin
必须为或进行请求的源(例如
https://javascript.info
)才能允许此请求。
Access-Control-Allow-Methods
必须具有允许的方法。
Access-Control-Allow-Headers
必须具有一个允许的 header 列表。
- 另外,header
Access-Control-Max-Age
可以指定缓存此权限的秒数。因此,浏览器不是必须为满足给定权限的后续请求发送预检。
示例:
- Step 1 预检请求(preflight request)
- 方法:
OPTIONS
。 - 路径 —— 与主请求完全相同:
/service.json
。 - 特殊跨源头:
Origin
—— 来源。Access-Control-Request-Method
—— 请求方法。Access-Control-Request-Headers
—— 以逗号分隔的“非安全” header 列表。
在发送我们的请求前,浏览器会自己发送如下所示的预检请求:
- Step 2 预检响应(preflight response)
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Allow-Methods: PATCH
Access-Control-Allow-Headers: Content-Type,API-Key
。
服务应响应状态 200 和 header:
这将允许后续通信,否则会触发错误。
- Step 3 实际请求(actual request)
预检成功后,浏览器现在发出主请求。这里的过程与安全请求的过程相同。
- Step 4 实际响应(actual response)
服务器不应该忘记在主响应中添加
Access-Control-Allow-Origin
。成功的预检并不能免除此要求Fetch API
这是所有可能的
fetch
选项及其默认值(注释中标注了可选值)的完整列表:URL 对象
内建的 URL 类提供了用于创建和解析 URL 的便捷接口。
url
—— 完整的 URL,或者仅路径(如果设置了 base),
base
—— 可选的 base URL:如果设置了此参数,且参数url
只有路径,则会根据这个base
生成 URL。
下面这两个 URL 是一样的:
URL 组件的备忘单:
SearchParams
url.searchParams
,URLSearchParams 类型的对象。它为搜索参数提供了简便的方法:
append(name, value)
—— 按照name
添加参数,
delete(name)
—— 按照name
移除参数,
get(name)
—— 按照name
获取参数,
getAll(name)
—— 获取相同name
的所有参数(这是可行的,例如?user=John&user=Pete
),
has(name)
—— 按照name
检查参数是否存在,
set(name, value)
—— set/replace 参数,
sort()
—— 按 name 对参数进行排序,很少使用,
- ……并且它是可迭代的,类似于
Map
。
编码(encoding)
RFC3986 标准定义了 URL 中允许哪些字符,不允许哪些字符。
那些不被允许的字符必须被编码,例如非拉丁字母和空格 —— 用其 UTF-8 代码代替,前缀为
%
,例如 %20
(由于历史原因,空格可以用 +
编码,但这是一个例外)。好消息是
URL
对象会自动处理这些。我们仅需提供未编码的参数,然后将 URL
转换为字符串编码字符串
如果使用字符串,则需要手动编码/解码特殊字符。
下面是用于编码/解码 URL 的内建函数:
encodeURI
—— 编码整个 URL。
decodeURI
—— 解码为编码前的状态。
encodeURIComponent
—— 编码 URL 组件,例如搜索参数,或者 hash,或者 pathname。
decodeURIComponent
—— 解码为编码前的状态。
区别:
encodeURI
仅编码 URL 中完全禁止的字符。
encodeURIComponent
也编码这类字符,此外,还编码#
,$
,&
,+
,,
,/
,:
,;
,=
,?
和@
字符。
所以,对于一个 URL 整体,我们可以使用
encodeURI
而对于 URL 参数,我们应该改用
encodeURIComponent
URL
和 URLSearchParams
基于最新的 URL 规范:RFC3986,而 encode*
函数是基于过时的 RFC2396。XMLHttpRequest
XMLHttpRequest 有两种执行模式:同步(synchronous)和异步(asynchronous)。
异步模式
- 创建
XMLHttpRequest
:
- 初始化它,通常就在
new XMLHttpRequest
之后: method
—— HTTP 方法。通常是"GET"
或"POST"
。URL
—— 要请求的 URL,通常是一个字符串,也可以是 URL 对象。async
—— 如果显式地设置为false
,那么请求将会以同步的方式处理,我们稍后会讲到它。user
,password
—— HTTP 基本身份验证(如果需要的话)的登录名和密码。
此方法指定请求的主要参数:
请注意,
open
调用与其名称相反,不会建立连接。它仅配置请求,而网络活动仅以 send
调用开启。- 发送请求。
这个方法会建立连接,并将请求发送到服务器。可选参数
body
包含了 request body。- 监听
xhr
事件以获取响应。 load
—— 当请求完成(即使 HTTP 状态为 400 或 500 等),并且响应已完全下载。error
—— 当无法发出请求,例如网络中断或者无效的 URL。progress
—— 在下载响应期间定期触发,报告已经下载了多少。
这三个事件是最常用的:
一旦服务器有了响应,我们可以在以下
xhr
属性中接收结果:status
HTTP 状态码(一个数字):
200
,404
,403
等,如果出现非 HTTP 错误,则为 0
。statusText
HTTP 状态消息(一个字符串):状态码为
200
对应于 OK
,404
对应于 Not Found
,403
对应于 Forbidden
。response
(旧脚本可能用的是 responseText
)服务器 response body。
响应:
我们可以使用
xhr.responseType
属性来设置响应格式:""
(默认)—— 响应格式为字符串,
"text"
—— 响应格式为字符串,
"arraybuffer"
—— 响应格式为ArrayBuffer
(对于二进制数据,请参见 ArrayBuffer,二进制数组),
"blob"
—— 响应格式为Blob
(对于二进制数据,请参见 Blob),
"document"
—— 响应格式为 XML document(可以使用 XPath 和其他 XML 方法)或 HTML document(基于接收数据的 MIME 类型)
"json"
—— 响应格式为 JSON(自动解析)。
readyState:
XMLHttpRequest
的状态(state)会随着它的处理进度变化而变化。可以通过 xhr.readyState
来了解当前状态。XMLHttpRequest
对象以 0
→ 1
→ 2
→ 3
→ … → 3
→ 4
的顺序在它们之间转变。每当通过网络接收到一个数据包,就会重复一次状态 3
。我们可以使用
readystatechange
事件来跟踪它们调用
xhr.abort()
终止请求同步请求
如果在
open
方法中将第三个参数 async
设置为 false
,那么请求就会以同步的方式进行。换句话说,JavaScript 执行在
send()
处暂停,并在收到响应后恢复执行。这有点儿像 alert
或 prompt
命令。HTTP 配置
header:
XMLHttpRequest
允许发送自定义 header,并且可以从响应中读取 header。HTTP-header 有三种方法:
setRequestHeader(name, value)
使用给定的
name
和 value
设置 request header。XMLHttpRequest
是不能撤销 setRequestHeader
。getResponseHeader(name)
获取具有给定
name
的 header(Set-Cookie
和 Set-Cookie2
除外)getAllResponseHeaders()
返回除
Set-Cookie
和 Set-Cookie2
外的所有 response header。header 之间的换行符始终为
"\r\n"
获取所有header:
POST & FormData:
可以使用内建的 FormData 对象。
xhr.open('POST', ...)
—— 使用POST
方法。
xhr.send(formData)
将表单发送到服务器。
上传进度
progress
事件仅在下载阶段触发。它专门用于跟踪上传事件:xhr.upload
。它会生成事件,类似于
xhr
,但是 xhr.upload
仅在上传时触发它们:loadstart
—— 上传开始。
progress
—— 上传期间定期触发。
abort
—— 上传中止。
error
—— 非 HTTP 错误。
load
—— 上传成功完成。
timeout
—— 上传超时(如果设置了timeout
属性)。
loadend
—— 上传完成,无论成功还是 error。
示例:
可恢复的文件上传
长轮询
长轮询是与服务器保持持久连接的最简单的方式,它不使用任何特定的协议,例如 WebSocket 或者 Server Sent Event。
常规轮询
从服务器获取新信息的最简单的方式是定期轮询。也就是说,定期向服务器发出请求:“你好,我在这儿,你有关于我的任何信息吗?”例如,每 10 秒一次。
作为响应,服务器首先通知自己,客户端处于在线状态,然后 —— 发送目前为止的消息包。
这可行,但是也有些缺点:
- 消息传递的延迟最多为 10 秒(两个请求之间)。
- 即使没有消息,服务器也会每隔 10 秒被请求轰炸一次,即使用户切换到其他地方或者处于休眠状态,也是如此。就性能而言,这是一个很大的负担。
长轮询
所谓“长轮询”是轮询服务器的一种更好的方式。
它也很容易实现,并且可以无延迟地传递消息。
其流程为:
- 请求发送到服务器。
- 服务器在有消息之前不会关闭连接。
- 当消息出现时 —— 服务器将对其请求作出响应。
- 浏览器立即发出一个新的请求。
对于此方法,浏览器发出一个请求并与服务器之间建立起一个挂起的(pending)连接的情况是标准的。仅在有消息被传递时,才会重新建立连接。
实现:
WebSocket
在 RFC 6455 规范中描述的
WebSocket
协议,提供了一种在浏览器和服务器之间建立持久连接来交换数据的方法。数据可以作为“数据包”在两个方向上传递,而无需中断连接也无需额外的 HTTP 请求。要打开一个 WebSocket 连接,我们需要在 url 中使用特殊的协议
ws
创建 new WebSocket
同样也有一个加密的
wss://
协议。类似于 WebSocket 中的 HTTPS。始终使用
wss://
wss://
协议不仅是被加密的,而且更可靠。一旦 socket 被建立,我们就应该监听 socket 上的事件。一共有 4 个事件:
open
—— 连接已建立,
message
—— 接收到数据,
error
—— WebSocket 错误,
close
—— 连接已关闭。
WebSocket 方法:
socket.send(data)
,
socket.close([code], [reason])
。
连接状态
要获取连接状态,可以通过带有值的
socket.readyState
属性:0
—— “CONNECTING”:连接还未建立,
1
—— “OPEN”:通信中,
2
—— “CLOSING”:连接关闭中,
3
—— “CLOSED”:连接已关闭。
深入
当
new WebSocket(url)
被创建后,它将立即开始连接。在连接期间,浏览器(使用 header)问服务器:“你支持 WebSocket 吗?”如果服务器回复说“我支持”,那么通信就以 WebSocket 协议继续进行,该协议根本不是 HTTP。
请求的浏览器 header 示例:
Origin
—— 客户端页面的源,例如https://javascript.info
。WebSocket 对象是原生支持跨源的。没有特殊的 header 或其他限制。旧的服务器无法处理 WebSocket,因此不存在兼容性问题。但Origin
header 很重要,因为它允许服务器决定是否使用 WebSocket 与该网站通信。
Connection: Upgrade
—— 表示客户端想要更改协议。
Upgrade: websocket
—— 请求的协议是 “websocket”。
Sec-WebSocket-Key
—— 浏览器随机生成的安全密钥。
Sec-WebSocket-Version
—— WebSocket 协议版本,当前为 13。
如果服务器同意切换为 WebSocket 协议,服务器应该返回响应码 101:
这里
Sec-WebSocket-Accept
是 Sec-WebSocket-Key
,是使用特殊的算法重新编码的。浏览器使用它来确保响应与请求相对应。扩展和子协议
WebSocket 可能还有其他 header,
Sec-WebSocket-Extensions
和 Sec-WebSocket-Protocol
,它们描述了扩展和子协议。例如:
Sec-WebSocket-Extensions: deflate-frame
表示浏览器支持数据压缩。扩展与传输数据有关,扩展了 WebSocket 协议的功能。Sec-WebSocket-Extensions
header 由浏览器自动发送,其中包含其支持的所有扩展的列表。
Sec-WebSocket-Protocol: soap, wamp
表示我们不仅要传输任何数据,还要传输 SOAP 或 WAMP(“The WebSocket Application Messaging Protocol”)协议中的数据。WebSocket 子协议已经在 IANA catalogue 中注册。因此,此 header 描述了我们将要使用的数据格式。
这个可选的 header 是使用
new WebSocket
的第二个参数设置的。它是子协议数组,例如,如果我们想使用 SOAP 或 WAMP:数据传输
WebSocket 通信由 “frames”(即数据片段)组成,可以从任何一方发送,并且有以下几种类型:
- “text frames” —— 包含各方发送给彼此的文本数据。
- “binary data frames” —— 包含各方发送给彼此的二进制数据。
- “ping/pong frames” 被用于检查从服务器发送的连接,浏览器会自动响应它们。
- 还有 “connection close frame” 以及其他服务 frames。
在浏览器里,我们仅直接使用文本或二进制 frames。
WebSocket
.send()
方法可以发送文本或二进制数据。socket.send(body)
调用允许 body
是字符串或二进制格式,包括 Blob
,ArrayBuffer
等。不需要额外的设置:直接发送它们就可以了。它是由
socket.binaryType
属性设置的,默认为 "blob"
,因此二进制数据通常以 Blob
对象呈现。限速
我们可以反复地调用
socket.send(data)
。但是数据将会缓冲(储存)在内存中,并且只能在网速允许的情况下尽快将数据发送出去。socket.bufferedAmount
属性储存了目前已缓冲的字节数,等待通过网络发送。我们可以检查它以查看 socket 是否真的可用于传输。
Server Sent Events
Server-Sent Events 规范描述了一个内建的类
EventSource
,它能保持与服务器的连接,并允许从中接收事件。与
WebSocket
类似,其连接是持久的。两者之间有几个重要的区别:
使用它的主要原因:简单。在很多应用中,WebSocket
有点大材小用。
使用
要开始接收消息,我们只需要创建
new EventSource(url)
即可。浏览器将会连接到
url
并保持连接打开,等待事件。服务器响应状态码应该为 200,header 为
Content-Type: text/event-stream
,然后保持此连接并以一种特殊的格式写入消息,就像这样:data:
后为消息文本,冒号后面的空格是可选的。
- 消息以双换行符
\n\n
分隔。
- 要发送一个换行
\n
,我们可以在要换行的位置立即再发送一个data:
(上面的第三条消息)。
在实际开发中,复杂的消息通常是用 JSON 编码后发送。换行符在其中编码为
\n
,因此不需要多行 data:
消息。对于每个这样的消息,都会生成
message
事件重新连接
创建之后,
new EventSource
连接到服务器,如果连接断开 —— 则重新连接。这非常方便,我们不用去关心重新连接的事情。
每次重新连接之间有一点小的延迟,默认为几秒钟。
服务器可以使用
retry:
来设置需要的延迟响应时间(以毫秒为单位)。- 如果服务器想要浏览器停止重新连接,那么它应该使用 HTTP 状态码 204 进行响应。
- 如果浏览器想要关闭连接,则应该调用
eventSource.close()
:
当连接最终被关闭时,就无法“重新打开”它。如果我们想要再次连接,只需要创建一个新的
EventSource
。消息 id
每条消息都应该有一个
id
字段当收到具有
id
的消息时,浏览器会:- 将属性
eventSource.lastEventId
设置为其值。
- 重新连接后,发送带有
id
的 headerLast-Event-ID
,以便服务器可以重新发送后面的消息。
把
id:
放在 data:
后请注意:
id
被服务器附加到 data
消息后,以确保在收到消息后 lastEventId
会被更新。连接状态:readyState
EventSource
对象有 readyState
属性,该属性具有下列值之一:对象创建完成或者连接断开后,它始终是
EventSource.CONNECTING
(等于 0
)。事件
默认情况下
EventSource
对象生成三个事件:message
—— 收到消息,可以用event.data
访问。
open
—— 连接已打开。
error
—— 无法建立连接,例如,服务器返回 HTTP 500 状态码。
服务器可以在事件开始时使用
event: ...
指定另一种类型事件。要处理自定义事件,我们必须使用
addEventListener
而非 onmessage
:Node端 简单使用
服务器响应格式:
服务器发送由
\n\n
分隔的消息。一条消息可能有以下字段:
data:
—— 消息体(body),一系列多个data
被解释为单个消息,各个部分之间由\n
分隔。
id:
—— 更新lastEventId
,重连时以Last-Event-ID
发送此 id。
retry:
—— 建议重连的延迟,以 ms 为单位。无法通过 JavaScript 进行设置。
event:
—— 事件名,必须在data:
之前。
一条消息可以按任何顺序包含一个或多个字段,但是
id:
通常排在最后。参考链接:
- 作者:JinSo
- 链接:https://jinso365.top/article/modern-javascript-others-network
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。