🚢现代 JavaScript —— 杂项(网络请求)
00 分钟
2022-9-12
2022-9-15
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
通常,我们需要处理两部分:
  1. 一部分是通过在 controller.signal 上添加一个监听器,来执行可取消操作。
  1. 另一部分是触发取消:在需要的时候调用 controller.abort()
与 fetch 一起使用

Fetch 跨源请求

“CORS”:跨源资源共享(Cross-Origin Resource Sharing)

安全请求

有两种类型的跨源请求:
  1. 安全请求。
  1. 所有其他请求。
安全请求很简单,所以我们先从它开始。
如果一个请求满足下面这两个条件,则该请求是安全的:
  1. 安全的方法:GET,POST 或 HEAD
  1. 安全的 header —— 仅允许自定义下列 header:
      • Accept
      • Accept-Language
      • Content-Language
      • Content-Type 的值为 application/x-www-form-urlencodedmultipart/form-data 或 text/plain
任何其他请求都被认为是“非安全”请求。例如,具有 PUT 方法或 API-Key HTTP-header 的请求就不是安全请求。
本质区别在于,可以使用 <form> 或 <script> 进行安全请求,而无需任何其他特殊方法。

安全请求的CORS

如果一个请求是跨源的,浏览器始终会向其添加 Origin header。
服务器可以检查 Origin,如果同意接受这样的请求,就会在响应中添加一个特殊的 header Access-Control-Allow-Origin。该 header 包含了允许的源,或者一个星号 *。然后响应成功,否则报错
浏览器在这里扮演受被信任的中间人的角色:
  1. 它确保发送的跨源请求带有正确的 Origin
  1. 它检查响应中的许可 Access-Control-Allow-Origin,如果存在,则允许 JavaScript 访问响应,否则将失败并报错。
notion image

安全的 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 可以指定缓存此权限的秒数。因此,浏览器不是必须为满足给定权限的后续请求发送预检。
notion image
示例:
  • Step 1 预检请求(preflight request)
    • 在发送我们的请求前,浏览器会自己发送如下所示的预检请求:
    • 方法:OPTIONS
    • 路径 —— 与主请求完全相同:/service.json
    • 特殊跨源头:
      • Origin —— 来源。
      • Access-Control-Request-Method —— 请求方法。
      • Access-Control-Request-Headers —— 以逗号分隔的“非安全” header 列表。
  • Step 2 预检响应(preflight response)
    • 服务应响应状态 200 和 header:
    • Access-Control-Allow-Origin: https://javascript.info
    • Access-Control-Allow-Methods: PATCH
    • Access-Control-Allow-Headers: Content-Type,API-Key
    • 这将允许后续通信,否则会触发错误。
  • 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 组件的备忘单:

notion image

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)。

异步模式

  1. 创建 XMLHttpRequest
    1. 初始化它,通常就在 new XMLHttpRequest 之后:
      1. 此方法指定请求的主要参数:
        • method —— HTTP 方法。通常是 "GET" 或 "POST"
        • URL —— 要请求的 URL,通常是一个字符串,也可以是 URL 对象。
        • async —— 如果显式地设置为 false,那么请求将会以同步的方式处理,我们稍后会讲到它。
        • userpassword —— HTTP 基本身份验证(如果需要的话)的登录名和密码。
        请注意,open 调用与其名称相反,不会建立连接。它仅配置请求,而网络活动仅以 send 调用开启。
    1. 发送请求。
      1. 这个方法会建立连接,并将请求发送到服务器。可选参数 body 包含了 request body。
    1. 监听 xhr 事件以获取响应。
      1. 这三个事件是最常用的:
        • load —— 当请求完成(即使 HTTP 状态为 400 或 500 等),并且响应已完全下载。
        • error —— 当无法发出请求,例如网络中断或者无效的 URL。
        • progress —— 在下载响应期间定期触发,报告已经下载了多少。

    一旦服务器有了响应,我们可以在以下 xhr 属性中接收结果:
    status
    HTTP 状态码(一个数字):200404403 等,如果出现非 HTTP 错误,则为 0
    statusText
    HTTP 状态消息(一个字符串):状态码为 200 对应于 OK404 对应于 Not Found403 对应于 Forbidden
    response(旧脚本可能用的是 responseText
    服务器 response body。

    响应:

    我们可以使用 xhr.responseType 属性来设置响应格式:
    • ""(默认)—— 响应格式为字符串,
    • "text" —— 响应格式为字符串,
    • "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 对象。
    1. xhr.open('POST', ...) —— 使用 POST 方法。
    1. xhr.send(formData) 将表单发送到服务器。

    上传进度

    progress 事件仅在下载阶段触发。它专门用于跟踪上传事件:xhr.upload
    它会生成事件,类似于 xhr,但是 xhr.upload 仅在上传时触发它们:
    • loadstart —— 上传开始。
    • progress —— 上传期间定期触发。
    • abort —— 上传中止。
    • error —— 非 HTTP 错误。
    • load —— 上传成功完成。
    • timeout —— 上传超时(如果设置了 timeout 属性)。
    • loadend —— 上传完成,无论成功还是 error。
    示例:

    可恢复的文件上传

     

    长轮询

    长轮询是与服务器保持持久连接的最简单的方式,它不使用任何特定的协议,例如 WebSocket 或者 Server Sent Event。

    常规轮询

    从服务器获取新信息的最简单的方式是定期轮询。也就是说,定期向服务器发出请求:“你好,我在这儿,你有关于我的任何信息吗?”例如,每 10 秒一次。
    作为响应,服务器首先通知自己,客户端处于在线状态,然后 —— 发送目前为止的消息包。
    这可行,但是也有些缺点:
    1. 消息传递的延迟最多为 10 秒(两个请求之间)。
    1. 即使没有消息,服务器也会每隔 10 秒被请求轰炸一次,即使用户切换到其他地方或者处于休眠状态,也是如此。就性能而言,这是一个很大的负担。

    长轮询

    所谓“长轮询”是轮询服务器的一种更好的方式。
    它也很容易实现,并且可以无延迟地传递消息。
    其流程为:
    1. 请求发送到服务器。
    1. 服务器在有消息之前不会关闭连接。
    1. 当消息出现时 —— 服务器将对其请求作出响应。
    1. 浏览器立即发出一个新的请求。
    对于此方法,浏览器发出一个请求并与服务器之间建立起一个挂起的(pending)连接的情况是标准的。仅在有消息被传递时,才会重新建立连接。
    notion image
    实现:
     

    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。
    notion image
    请求的浏览器 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 是字符串或二进制格式,包括 BlobArrayBuffer 等。不需要额外的设置:直接发送它们就可以了。
    它是由 socket.binaryType 属性设置的,默认为 "blob",因此二进制数据通常以 Blob 对象呈现。

    限速

    我们可以反复地调用 socket.send(data)。但是数据将会缓冲(储存)在内存中,并且只能在网速允许的情况下尽快将数据发送出去。
    socket.bufferedAmount 属性储存了目前已缓冲的字节数,等待通过网络发送。
    我们可以检查它以查看 socket 是否真的可用于传输。
     

    Server Sent Events

    Server-Sent Events 规范描述了一个内建的类 EventSource,它能保持与服务器的连接,并允许从中接收事件。
    与 WebSocket 类似,其连接是持久的。
    两者之间有几个重要的区别:
    notion image
    使用它的主要原因:简单。在很多应用中,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 的 header Last-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: 通常排在最后。
     
     

    参考链接:
    1. 网络请求 (javascript.info)
     
    上一篇
    可恢复的文件上传 & 文件下载
    下一篇
    Tools Recommended —— Acid Tabs

    评论
    Loading...