《JavaScript 高级程序设计》第二十四章 网络请求与远程资源 学习记录

1、XMLHttpRequest对象

let xhr = new XMLHttpRequest()

1、使用XHR

  • 首先要调用open()方法

    • 参数:请求类型、请求URL、请求是否异步的布尔值
    • 不会实际发送请求,只是为发送请求做好准备。
    • 只能访问同源URL,即域名相同、端口相同、协议相同。
  • 使用send()方法发送

    • 参数:作为请求体发送的数据,如果不需要请求体,传入null
    • 调用后请求就会发送到服务器
  • 同步请求,代码会等待服务器响应后再继续执行,XHR对象会增加以下属性

    • responseText 作为响应体返回的文本
    • responseXML 如果响应内容类型是”text/xml”或”application/xml”,那就是包含响应数据的XML DOM文档
    • status响应的HTTP状态
      • 需要首先检查
        • 2xx表示成功,此时responseTextresponseXML会有内容
        • 304表示资源未修改过,是从浏览器缓存中直接拿取的
    • statusText 响应的HTTP状态描述
  • 异步请求

    • XHR对象有一个readyState属性,表示当前处在请求响应过程中哪个阶段
      • 0: 未初始化,尚未调用open()方法
      • 1: 已打开,调用open(),尚未调用send()方法
      • 2: 已发送,调用send(),尚未收到响应
      • 3: 接收中,已经收到部分响应
      • 4: 完成,已经收到所有响应,可以使用
    • 每次值发生变化,就会触发readystatechange事件,一般只关心值为4。
    let xhr = new XMLHttpRequest()
    xhr.onreadystatechange = function() {
      if(xhr.readyState === 4) {
        if((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304){
          alert(xhr.responseText)
        }else {
          alert('error')
        }
      }
    }
    xhr.open("get", "test.txt", true)
    xhr.send(null)
    复制代码
    • 在收到请求前取消异步请求xhr.abort()
      • 调用后,XHR对象会停止触发事件,阻止访问这个对象上任何与响应相关的属性。

2、HTTP头部

  • 每个请求和响应都会携带一些头部字段,默认会发送以下头部字段
    • Accept:浏览器可以处理的内容类型
    • Accept-Charset:浏览器可以显示的字符集
    • Accept-Encoding: 浏览器可以处理的压缩编码类型
    • Accept-Language: 浏览器使用的语言
    • Connection: 浏览器与服务器的连接类型
    • Cookie: 页面中设置的Cookie
    • Host:发送请求的页面所在的域
    • Referer:发送请求的页面URI
    • User-Agent:浏览器用户代理字符串
  • 通过setRequsetHeader()方法设置额外的请求头部
    • 两个参数,头部字段的名称和值
    • 需在open()之后,send()之前调用
  • 使用getResponseHeader()方法从XHR对象中获取响应头部
    • 参数,获取头部的名称
    • 使用getAllResponseHeader()返回所有响应头部字段

3、GET请求

  • 最常用的查询方法,用于向服务器查询某些信息。
  • 必要时需要在GET请求的URL后面添加查询字符串参数。
  • 查询字符串的每个名和值都必须使用encodeURIComponent()编码
function addURLParam(url, name, value) {
  url += (url.indexOf("?") == -1 ? "?" : "&")
  url += encodeURIComponent(name) + "=" + encodeURIComponent(value)
  return url
}
复制代码

4、POST请求

  • 用于向服务器发送应该保存的数据。
  • 每个POST请求都应该在请求体中携带提交的数据
  • POST请求的请求体可以包含非常多的数据,而且数据可以是任意格式
  • 可以使用XHR模拟表单提交
    • Content-Type头部设置为application/x-www-formulencoded
    • 创建对应格式的字符串
  • POST请求比GET请求占用资源更多,发送相同数量的数据,GET请求比POST要快两倍

5、XMLHttpRequest Level 2

1、FormData 类型

  • 便于表单序列化,便于创建与表单类似格式的数据通过XHR发送
let data = new FormData()
data.append("name", "Nicholas")
复制代码
  • append() 接收两个参数,键和值,相当于表单字段名称和该字段的值。
  • 直接给FormData构造函数传入一个表单元素,也可以将表单中的数据作为键值对填充进去
  • 使用FormData不需要给XHR对象显式设置任何请求头部,能根据FormData实例传入的数据类型自动配置相应头部

2、超时

  • timeout属性,用来表示发送请求后等待多少毫秒,如果响应不成功就中断请求。
  • 超时后会触发timeout事件。
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
  if (xhr.readyState == 4) {
    try {
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
        alert(xhr.responseText);
      } else {
        alert("Request was unsuccessful: " + xhr.status);
      }
    } catch (ex) {
      // 假设由 ontimeout 处理
    }
  }
};
xhr.open("get", "timeout.php", true);
xhr.timeout = 1000; // 设置 1 秒超时
xhr.ontimeout = function () {
  alert("Request did not return in a second.");
};
xhr.send(null);
复制代码
  • 超时后readyState仍然会变成4,也会触发onreadystatechange事件处理程序,超时后访问status属性会发生错误,所以使用try/catch语句。

3、overrideMimeType()方法

  • 用来重写XHR响应的MIME类型。
  • send()之前设置。

2、进度事件

  • loadstart:在接收到响应的第一个字节时触发
  • progress:在接收响应期间反复触发
  • error:在请求出错时触发
  • abort:在调用abort()终止连接时触发
  • load:在成功接收完响应时触发
  • loadend:在通信完成时,且在error、abort、load之后触发

1、load事件

  • 为了简化交互模式,不用检查readyState属性

2、progress事件

  • 在接收数据期间反复触发,触发时会收到event对象,其属性
    • target XHR对象
    • lengthComputable 布尔值,表示进度信息是否可用
    • position 接收到的字节数
    • totalSize 响应的Content-Type头部定义的总字节数

3、跨源资源共享

  • 使用XHR进行Ajax通信主要限制是跨源安全策略,默认只能访问与发起请求的页面在同一个域内的资源,可以防止某些恶意行为,跨站资源共享(CORS)定义了浏览器与服务器如何实现跨源通信
  • 基本思路是使用自定义的HTTP头部允许浏览器和服务器相互理解,以确定请求或响应应该成功还是失败
  • 简单请求,没有自定义头部,请求体是text/plain类型,在发送时会有一个额外头部Origin,包含发送请求的页面的源(协议、域名、端口)以便服务器确定是否为其提供响应。如Origin: http://www.xxx.net
  • 如果服务器决定响应,那么就应该发送Access-Control-Allow-Origin头部,包含相同的源或*,如Access-Control-Allow-Origin: http://www.xxx.net
  • 如果没有这个头部,或者有源但不匹配,表面不会响应浏览器请求。无论请求还是响应都不会包含cookie信息。
  • XMLHttpRequest原生支持CORS,要访问不同域的源,传入绝对URL
  • 跨域XHR对象允许访问status和statusText属性,允许同步,但有额外限制
    • 不能使用setRequestHeader()设置自定义头部
    • 不能发送和接收cookie
    • getAllResponseHeaders()方法始终返回空字符串

1、预检请求

  • CORS通过预检请求的服务器验证机制,允许自定义头部,除GET和POST之外的方法,以及不同请求体内容类型。需要发送请求,请求使用OPTION方法包含以下头部
    • Origin 与简单请求相同
    • Access-Control-Request-Method 请求希望使用的方法
    • Access-Control-Request-Header (可选)要使用逗号分隔的自定义头部列表
  • 服务器可以确定是否允许这种类型请求,服务器会在响应中发送如下头部与浏览器沟通信息
    • Access-Control-Allow-Origin 与简单请求线条
    • Access-Control-Allow-Method 允许的方法(逗号分隔的列表)
    • Access-Control-Allow-Header 服务器允许的头部(逗号分隔的列表)
    • Access-Control-Max-Age 缓存预检请求的秒数
  • 预检请求返回后结果会按照返回的指定时间缓存一段时间。

2、凭据请求

  • 跨源请求默认不提供凭据,可以将withCredentials属性设置为true表明请求会发送凭据
  • 如果服务器允许带凭据请求,则会响应Access-Control-Allow-Credentials: true
  • 如果发送了凭据请求而服务器返回的响应中没有这个头部,则浏览器不会把响应交给JavaScript,即responseText是空字符串,status是0,onerror()被调用。
  • 服务器也可以在预检请求的响应中发送这个HTTP头部,表明这个源允许发送凭据请求。

4、替代性跨源技术

  • 过去跨源需要依赖DOM特性。

1、图片探测

  • 利用<img>标签实现跨与通信,任何页面都可以跨与加载图片而不用担心限制。可以动态创建图片,然后通过它们的onload和onerror事件处理程序得知何时收到响应。
  • 经常用于图片探测,是与服务器之间简单、跨域、单向的通信。数据通过查询字符串发送,响应可以随意设置,一般是位图图片或值为204的状态码。浏览器通过图片探测拿不到任何数据,但可以通过事件知道什么时候能接收到响应。
let img = new Image()
img.onload = img.onerror = function() {
  alert('Done!')
}
img.src = "http://www.example.com/test?name=Nicholas"
复制代码

2、JSONP

  • JSONP(JSON with padding),在web服务上流行的一种JSON变体。
  • 格式分为两部分:回调和数据
    • 回调是在页面接收到响应之后应该调用的函数,通常回调函数的名称是通过请求来动态指定的
    • 数据就是作为参数传给回调函数的JSON数据。
  • http://freegeoip.net/json/?callback=handleResponse,通常以字符串形式指定回调函数名称。
  • JSONP调用是通过动态创建<script>元素并为src属性指定跨域URL实现的。此时的<script><img>元素类似,能够不受限制地从其他域加载资源。
function handleResponse(response) {
  console.log(response)
}
let script = document.createElement("script")
script.src = "http://fxxx/json/?callback=handleResponse"
document.body.insertBefore(script, document.body.firstChild)
复制代码
  • 可以直接访问响应,实现浏览器与服务器的双向通信。缺点:
    • 是从不同的域拉取可执行代码,如果域不可信,可能在响应中加入恶意内容。
    • 不好确定JSONP请求是否失败,虽然HTML5规定了<script>元素的onerror事件处理程序,但还没有被任何浏览器实现。需要使用计时器来决定是否放弃等待响应。

5、Fetch API

  • 能够执行XMLHttpRequest对象的所有任务,更容易使用,接口更现代化,能够在Web工作线程等Web工具中使用。必须是异步的。

1、基本用法

  • 全局作用域下,包括主页面线程、模块和工作线程。

1、分派请求

  • fetch()必传参数input,一般是要获取资源的URL,返回一个期约

  • 请求完成、资源可用是,期约会解决为一个Response对象。

    fetch('bar.txt')
    	.then((response) => {
        console.log(response)
      })
    // Response {type: "basic", url: ...}
    复制代码

2、读取响应

  • 读取最简单方式是获取纯文本格式内容,text()方法。返回一个期约,会被解决为取得资源的完整内容:

    fetch('bar.txt')
    	.then((response) => {
        resonse.text().then((data)=> {
          console.log(data)
        })
      })
    
    fetch('bar.txt')
    	.then((response) => response.text())
    	.then((data) => console.log(data))
    复制代码

3、处理状态码和请求失败

  • 支持通过Response的status(状态码)和statusText(状态文本)属性检查响应状态。
    • 成功获取响应的请求200、请求不存在的资源404、请求的URL如果服务器抛出错误500
  • 可以显式设置fetch()在遇到重定向时的行为,默认行为是跟随重定向并返回状态码不是300~399的响应,响应对象的redirected属性会被设置为true,状态码仍是200
  • 请求失败的时候也是执行了期约的解决处理函数,只要服务器返回了响应,fetch()期约都会被解决
  • 通常返回200认为是成功,可以在非200~299时检查Response对象的ok属性
  • 因为服务器没有响应而导致浏览器超时,这样真正的fetch()失败会导致期约被拒绝
  • 违反CORS、无网络连接、HTTPS错配以及其他浏览器/网络策略问题都会导致期约拒绝。
  • 可以通过url属性检查通过fetch()发送请求时使用的完整URL

4、自定义选项

  • 只使用URL时,fetch()会发送GET请求,只包含最低限度的请求头,第二个参数配置如下:

  • body

    • 指定使用请求体时请求体的内容
    • 必须是Blob、BufferSource、FormData、URLSearchParams、ReadableStream或String的实例
  • cache

    • 用于控制浏览器与HTTP缓存的交互,要跟踪缓存的重定向,请求的redirect属性值必须是”follow”,必须符合同源策略限制,必须是下列值之一
      • default(默认)
        • fetch()返回命中的有效缓存,不发送请求
        • 命中无效缓存会发送条件式请求。如果响应发生改变,则更新缓存值,然后fetch()返回缓存的值,
        • 未命中缓存会发送请求,并缓存响应,然后fetch()返回响应
      • no-store
        • 浏览器不检查缓存,直接发送请求
        • 不缓存响应,直接通过fetch()返回
      • reload
        • 浏览器不检查缓存,直接发送请求
        • 缓存响应,在通过fetch()返回
      • no-cache
        • 无论命中有效缓存还是无效缓存都会发送条件式请求。如果响应已经改变,则更新缓存值,然后fetch()返回缓存的值,
        • 未命中缓存会发送请求,并缓存响应,然后fetch()返回响应
      • force-cache
        • 无论命中有效缓存还是无效缓存都通过fetch()返回,不发送请求
        • 未命中缓存会发送请求,并缓存响应,然后fetch()返回响应
      • only-if-cached
        • 只在请求模式为same-origin时使用缓存
        • 无论命中有效缓存还是无效缓存都通过fetch()返回,不发送请求
        • 未命中缓存返回状态码为504的响应
  • credentilas

    • 指定在外发送请求中如何包含cookie,类似withCredentials标签
      • omit
        • 不发送cookie
      • same-origin (默认)
        • 只在请求URL与发送fetch()请求的页面同源时发送cookie
      • include
        • 无论同源还是跨域都包含cookie
  • headers

    • 用于指定请求头部
      • 必须是Headers对象实例或者包含字符串格式键值对的常规对象
      • 默认值为不包含键值对的Headers对象,但请求会发送不可见的默认头部。
  • integrity

    • 强制子资源完整性
    • 必须是包含子资源完整性标志符的字符串
    • 默认空字符串
  • keepalive

    • 指示浏览器允许请求存在时间超出页面声明周期。适合报告事件或分析。
    • 布尔值,默认false
  • method

    • 指定HTTP请求方法
      • GET 默认
      • POST
      • PUT
      • PATCH
      • DELETE
      • HEAD
      • OPTIONS
      • CONNECT
      • TARCE
  • mode

    • 指定请求模式,决定来自跨源请求的响应是否有效,客户端可以读取多少响应。
      • cors
        • 允许遵循CORS协议的跨源请求
        • 响应的是“CORS过滤的响应”,即响应中可以访问的浏览器头部是经过浏览器强制白名单过滤的。
        • 手动创建Request实例时的默认值
      • no-cors
        • 允许不需要发送预检请求的跨源请求(HEAD、GET和织带有满足CORS请求头部的POST)
        • 响应的类型是opaque,即不能读取响应内容
        • 非手动创建Request实例时的默认值
      • same-origin
        • 任何跨源请求都不允许发送
      • navigate
        • 用于支持HTML导航,只在文档间导航使用
  • redirect

    • 用于指定如何重定向响应
    • 状态码为301、302、303、307或308
      • follow(默认)
        • 跟踪重定向请求,以最终非重定向URL的响应作为最终响应
      • error
        • 重定向请求会抛出错误
      • manual
        • 不跟踪重定向,而是返回opaquerrdirect类型的响应
        • 同时仍然暴露期望的重定向URL
        • 允许以手动方式跟踪重定向
  • referrer

    • 用于指定HTTP的Referer头部的内容
      • no-referrer
        • 以no-referrer作为值
      • client/about:client (默认)
        • 以当前URL或no-referrer(取决于来源策略referrerPolicy)作为值
      • <URL>
        • 以伪造URL作为值,伪造URL的源必须与执行脚本的源匹配
  • 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
  • signal

    • 用于支持AbortController中断进行中的fetch()请求
    • 必须是AbortSignal的实例
    • 默认为为关联控制器的AbortSignal的实例

2、常见的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、发送文件

  • 请求体支持FormData实现,所以fetch()也可以序列化并发送文件字段的文件
let imageFormData = new FormData();
let imageInput = document.querySelector("input[type='file']");
imageFormData.append('image', imageInput.files[0]);
fetch('/img-upload', {
  method: 'POST',
  body: imageFormData
});

// 这个 fetch() 实现可以支持多个文件:
let imageFormData = new FormData();
let imageInput = document.querySelector("input[type='file'][multiple]");
for (let i = 0; i < imageInput.files.length; ++i) {
  imageFormData.append('image', imageInput.files[i]);
}
fetch('/img-upload', {
  method: 'POST',
  body: imageFormData
});
复制代码

4、加载Blob文件

  • Fetch API能提供Blob类型的响应,而Blob可以兼容多种浏览器API。
  • 常见做法是明确将图片文件加载到内存,然后将其添加到HTML图片元素。
  • 可以使用响应对象暴露的blob方法。该方法返回一个期约,解决为一个Blob的实例。然后可以将这个实例传给URL.createObjectUrl()以生成可以添加图片元素src属性的值。
const imageElement = document.querySelector('img');
fetch('my-image.png')
  .then((response) => response.blob())
  .then((blob) => {
    imageElement.src = URL.createObjectURL(blob);
  });
复制代码

5、发送跨源请求

  • 响应头要包含CORS头部才能保证浏览器收到响应
  • 如果代码不需要访问请求,也可以发送no-cors请求。此时响应的type属性为opaque,因此无法读取响应内容。这中方式适合发送探测请求或将响应缓存起来供以后使用。
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);
// 已经中断 
复制代码

3、Headers对象

  • 外发请求和入站响应头部的容器。

  • 可以通过Request.prototype.headers访问

  • new Headers() 可以创建新实例

1、Headers与Map相似之处

  • 都有get()、set()、has()、delete()等方法
  • 都可以用一个可迭代对象初始化
  • 都有相同的keys()、values()、和entries()迭代器接口

2、Headers独有的特性

  • 可以用键值对的形式初始化
  • 支持使用append()方法添加多个值,在不存在的头部上调用相当于调用set(),后续调用会以逗号分隔符拼接

3、头部护卫

  • 并非所有HTTP头部都可以被客户端修改,使用护卫防止不被允许的修改。不同护卫设置会改变set、append、delete行为,违反护卫限制会抛出TypeError
  • 会因来源不同而展示不同行为,其行为由护卫控制。
护卫 情形 限制
none 在通过构造函数创建 Headers 实例时激活
request 在通过构造函数初始化 Request对象,且 mode 值为非 no-cors 时激活 不允许修改禁止修改的头部
request-no-cors 在通过构造函数初始化 Request对象,且 mode 不允许修改非简单头部
response 在通过构造函数初始化 Response 对象时激活 不允许修改禁止修改的响应头部
immutable 在通过 error()或 redirect()静态方法初始 化 Response 对象时激活 不允许修改任何头部

4、Request 对象

  • 获取资源请求的接口,暴露了请求的相关信息,也暴露了使用请求体的不同方式

1、创建Request对象

  • 构造函数初始化

    let r = new Request('https://foo.com'); 
    复制代码
  • 接收第二个参数,没有init对象中涉及的值会用默认值

    // 用所有默认值创建 Request 对象
    console.log(new Request(''));
    
    // Request {
    // 	bodyUsed: false
    // 	cache: "default"
    // 	credentials: "same-origin"
    // 	destination: ""
    // 	headers: Headers {}
    // 	integrity: ""
    // 	keepalive: false
    // 	method: "GET"
    // 	mode: "cors"
    // 	redirect: "follow"
    // 	referrer: "about:client"
    // 	referrerPolicy: ""
    // 	signal: AbortSignal {aborted: false, onabort: null}
    // 	url: "<current URL>"
    // }
    复制代码
  • 用指定的初始值创建

2、克隆Request()对象

  • 使用构造函数

    let r1 = new Request('https://foo.com');
    let r2 = new Request(r1);
    console.log(r2.url); // https://foo.com/ 
    复制代码
    • 传入init对象会覆盖同名的值
    • 不能得到一摸一样的副本,第一个请求的请求体会被标记为“已使用” (bodyUsed)
    • 如果源对象与创建的新对象不同源,则referrer属性会被清除,如果源对象mode为navigate则会转为same-origin
  • clone()方法

    • 创建一摸一样的副本,任何值不会覆盖,不会标记为已使用
    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); // false 
    复制代码
    • bodyUsed属性为true的对象,不能再创建这个对象的副本
    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. 
    复制代码

3、在fetch()中使用Request对象

  • 在调用fetch()时,可以传入已经创建好的Request实例。
  • 传给fetch()的init对象会覆盖传入请求对象的值
let r = new Request('https://foo.com');
// 向 foo.com 发送 GET 请求
fetch(r);
// 向 foo.com 发送 POST 请求
fetch(r, { method: 'POST' }); 
复制代码
  • fetch()会在内部克隆传入的Request对象,不能拿请求体已用过的Request对象来发送请求
  • 由请求体的Request只能在一次fetch中使用
  • 要想多次调用,需要在发送请求前调用clone()
let r = new Request('https://foo.com',
 { method: 'POST', body: 'foobar' });

// 3 个都会成功
fetch(r.clone());
fetch(r.clone());
fetch(r); 
复制代码

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: ""
    // } 
    复制代码
  • 接收一个可选的body参数,该参数可以是null。等同于fetch()参数init中的body。

  • 还可以接收一个可选的init对象

    • headers
      • 必须是 Headers 对象实例或包含字符串键/值对的常规对象实例
      • 默认为没有键/值对的 Headers 对象
    • status
      • status 表示 HTTP 响应状态码的整数
      • 默认为 200
    • statusText
      • 表示 HTTP 响应状态的字符串
      • 默认为空字符串
  • 生成Response对象主要是调用fetch(),它返回一个最后会解决为Response对象的期约,这个Response对象代表实际的HTTP响应

  • Response类还有两个用于生成Response对象的方法

    • Response.redirect()
      • 前者接收一个 URL 和一个重定向状态码(301、302、303、307 或 308)
      • 返回重定向的 Response 对象
      • 提供的状态码必须对应重定向,否则会抛出错误
    • Response. error()
      • 用于产生表示网络错误的 Response 对象(网络错误会导致 fetch()期约被拒绝)

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,非重定向响应就是它产生的

3、克隆Response对象

  • clone()

    • 这个方法会创建一个一模一样的副本

    • 不会覆盖任何值。这样不会将任何请求的请求体标记为已使用

    • 如果响应对象的 bodyUsed 属性为 true(即响应体已被读取),则不能再创建这个对象的副本。在 响应体被读取之后再克隆会导致抛出 TypeError

    • 有响应体的 Response 对象只能读取一次。(不包含响应体的 Response 对象不受此限制。)

    • 要多次读取包含响应体的同一个 Response 对象,必须在第一次读取前调用 clone():

    • 通过创建带有原始响应体的 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);
    // TypeError: Failed to execute 'text' on 'Response': body stream is locked
    复制代码

6、Request、Response及Body混入

  • Request 和 Response 都使用了 Fetch API 的 Body 混入,以实现两者承担有效载荷的能力。
  • 这个 混入为两个类型提供了只读的 body 属性(实现为 ReadableStream)、只读的 bodyUsed 布尔值(表 示 body 流是否已读)和一组方法,用于从流中读取内容并将结果转换为某种 JavaScript 对象类型。
  • 作为流使用的原因:
    • 有效载荷的大小可能会导致网络延迟
    • 流API本身在处理有效载荷方面是有优势的
  • 最好是一次性获取资源主体
  • 在解决之前,期约会等待主体流报告完成及缓冲被解析。这意味着客户端必须等待响应的资源完全加载才能访问其内容。

1、Body.text()

  • 返回期约,解决为缓冲区转存得到的UTF-8字符串
// 下面的代码展示了在 Response 对象上使用 Body.text():
fetch('https://foo.com')
	.then((response) => response.text())
	.then(console.log);
// <!doctype html><html lang="en">
// <head>
// 	<meta charset="utf-8">
// ...

//以下代码展示了在 Request 对象上使用 Body.text():
let request = new Request('https://foo.com',
 { method: 'POST', body: 'barbazqux' });
request.text()
	.then(console.log);
// barbazqux 
复制代码

2、Body.json()

  • 返回期约,解决为将缓冲区转存得到的JSON
// 下面的代码展示了在 Response对象上使用 Body.json():
fetch('https://foo.com/foo.json')
	.then((response) => response.json())
	.then(console.log);
// {"foo": "bar"}

// 以下代码展示了在 Request 对象上使用 Body.json():
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 对象序列化/反序列化为主体
  • Body.formData()方法返回期约,解决为将缓冲区转存得到的 FormData 实例
// 下面的代码展示了在 Response 对象上使用 Body.formData():
fetch('https://foo.com/form-data')
	.then((response) => response.formData())
	.then((formData) => console.log(formData.get('foo'));
// bar
        
// 以下代码展示了在 Request 对象上使用 Body.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()

  • 返回期约,解决为将缓冲区转存得到 的 ArrayBuffer 实例
// 下面的代码展示了在 Response 对象上使用 Body.arrayBuffer():
fetch('https://foo.com')
	.then((response) => response.arrayBuffer())
	.then(console.log);
// ArrayBuffer(...) {}

// 以下代码展示了在 Request 对象上使用 Body.arrayBuffer():
let request = new Request('https://foo.com',
 { method:'POST', body: 'abcdefg' });

// 以整数形式打印二进制编码的字符串
request.arrayBuffer()
	.then((buf) => console.log(new Int8Array(buf)));
// Int8Array(7) [97, 98, 99, 100, 101, 102, 103]
复制代码

5、Body.blob()

  • 返回期约,解决为将缓冲区转存得到的 Blob 实例
// 下面的代码展示了在 Response 对象上使用 Body.blob():
fetch('https://foo.com')
	.then((response) => response.blob())
	.then(console.log);
// Blob(...) {size:..., type: "..."}

// 以下代码展示了在 Request 对象上使用 Body.blob():
let request = new Request('https://foo.com',
 { method:'POST', body: 'abcdefg' });
request.blob()
	.then(console.log);
// Blob(7) {size: 7, type: "text/plain;charset=utf-8"} 
复制代码

6、一次性流

  • Body 混入是构建在 ReadableStream 之上的,所以主体流只能使用一次。

  • 这意味着所有主 体混入方法都只能调用一次,再次调用就会抛出错误

  • 作为 Body 混入的一部分,bodyUsed 布尔值属性表示 ReadableStream 是否已摄受

7、使用 ReadableStream 主体

  • 从 TCP/IP 角度来看,传输的数据是以分块形式抵达端点的,而且速度受到网速的限制。
  • 接收端点 会为此分配内存,并将收到的块写入内存。
  • Fetch API 通过 ReadableStream 支持在这些块到达时就实时读取和操作这些数据。
  • ReadableStream暴露了getReader()方法,用于产生ReadableStreamDefaultReader,这个读取器可以用于在数据到达时异步获取数据块。数据流的格式是 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 }
复制代码
  • 在随着数据流的到来取得整个有效载荷,可以递归调用 read()方法
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);
    }
  });
// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false } 
// ...
复制代码
  • 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)
    }
})
// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false }
// { value: Uint8Array{}, done: false } 
// ...
复制代码
  • 进一步将异步逻辑包装到生成器函数
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 块之后,浏览器会将其标记为可以被垃圾回收。对于需要在 不连续的内存中连续检查大量数据的情况,这样可以节省很多内存空间
  • 缓冲区的大小,以及浏览器是否等待缓冲区被填充后才将其推到流中,要根据 JavaScript 运行时的实现
  • 不同浏览器中分块大小可能不同,取决于带宽和网络延迟,不同情况
    • 不同大小的 Unit8Array块
    • 部分填充的Unit8Array块
    • 块到达的时间间隔不确定
  • Unit8Array转为可读文本,通过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
      }));
    }
  });
复制代码
  • 因为可以使用 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);
复制代码

6、Beacon API

  • 解决页面销毁时unload事件无法发送异步请求。
  • 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"}'); 
复制代码
  • sendBeacon() 任何时候都可以使用
  • sendBeacon()后,浏览器会把请求添加到一个内部的请求队列。浏览器会主动地发送队 列中的请求。
  • 浏览器保证在原始页面已经关闭的情况下也会发送请求
  • 状态码、超时和其他网络原因造成的失败完全是不透明的,不能通过编程方式处理
  • 信标(beacon)请求会携带调用 sendBeacon()时所有相关的 cookie

7、Web Socket

  • 目标是通过一个长时连接实现与服务器全双工、双向的通信
  • 在 JavaScript 中创建 Web Socket 时,一个 HTTP 请求会发送到服务器以初始化连接。
  • 服务器响应后,连接使用 HTTP 的 Upgrade 头部从 HTTP 协议切换到 Web Socket 协议。
  • 这意味着 Web Socket 不能通过标准 HTTP服务器实现,而必须使用支持该协议的专有服务器。
  • 要使用 ws://和 wss://。前者是不安全的连接,后者是安全连接。
  • 客户端与服务器之间可以发送非常少的数据,不会对 HTTP 造成任何负担。

1、API

  • 创建必须传入绝对URL
  • 同源策略不适用于 Web Socket,因此可以 打开到任意站点的连接。至于是否与来自特定源的页面通信,则完全取决于服务器
let socket = new WebSocket("ws://www.example.com/server.php"); 
复制代码
  • 浏览器会在初始化 WebSocket 对象之后立即创建连接。与 XHR 类似,WebSocket 也有一个 readyState 属性表示当前状态。
    • WebSocket.OPENING(0):连接正在建立。
    • WebSocket.OPEN(1):连接已经建立。
    • WebSocket.CLOSING(2):连接正在关闭。
    • WebSocket.CLOSE(3):连接已经关闭。
  • 任何时候都可以调用 close()方法关闭 Web Socket 连接,
socket.close()
复制代码
  • 调用 close()之后,readyState 立即变为 2(连接正在关闭),并会在关闭后变为 3(连接已经关闭)。

2、发送和接收数据

  • 通过连接发送和接收数据,要向服务器发送数据,使用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属性访问到有效载荷
socket.onmessage = function(event) {
	let data = event.data;
	// 对数据执行某些操作
}; 
复制代码
  • event.data返回的数据也可能是 ArrayBuffer 或 Blob。 这由 WebSocket 对象的 binaryType属性决定,该属性可能是”blob”或”arraybuffer”

3、其他事件

  • open:在连接成功建立时触发。
  • error:在发生错误时触发。连接无法存续。
  • close:在连接关闭时触发。
let socket = new WebSocket("ws://www.example.com/server.php");
socket.onopen = function () {
  alert("Connection established.");
};
socket.onerror = function () {
  alert("Connection error.");
};
socket.onclose = function () {
  alert("Connection closed.");
};
复制代码
  • 只有 close 事件的 event 对象上有额外信息。这个对象上有 3 个额外属性
    • wasClean 布尔值,表示连接是否干净地关闭
    • code 来自服务器的数值状态码
    • reason 字符串,包含服务器发来的消息
socket.onclose = function (event) {
  console.log(`as clean? ${event.wasClean} Code=${event.code} Reason=${event.reason}`);
};
复制代码

8、安全

  • 任何 Ajax 可以访问的 URL,也可以通过浏览器或服务器访问
  • 在未授权系统可以访问某个资源时,可以将其视为跨站点请求伪造(CSRF)攻击
  • 未授权系统会按照处理请求的服务器的要求伪装自己。
  • Ajax 应用程序,无论大小,都会受到 CSRF 攻击的影响,包括无害的漏洞验证攻击和恶意的数据盗窃或数据破坏攻击
  • 措施
    • 要求通过 SSL 访问能够被 Ajax 访问的资源
    • 要求每个请求都发送一个按约定算法计算好的令牌(token)
  • 注意,以下手段对防护 CSRF 攻击是无效的
    • 要求 POST 而非 GET 请求(很容易修改请求方法)
    • 使用来源 URL 验证来源(来源 URL 很容易伪造)
    • 基于 cookie 验证(同样很容易伪造)
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享