Skip to content

网络请求与远程资源

XMLHttpRequest,实际上是 Web 过时规范的产物,应该只旧版浏览器中使用;实际开发中应该尽可能使用 fetch()

AJAX—asychronous javascript and xml

AJAX 不是新的编程语言,而是一种使用现有标准的新方法

AJAX 是与服务器交换数据并更新部分网页的艺术,在不重新加载整个页面的情况下

AJAX = 异步 JavaScript 和 XML

AJAX 是一种用于创建快速动态网页的技术

XMLHttpRequest 对象

所有的现代浏览器都通过 XMLHttpRequest 构造函数支持 XHR 对象;

variable = new XMLHttpRequest();

老版本的 IE5 和 IE6 使用 ActiveX 对象:variable = new ActiveXObject("Microsoft.XMLHTTP");

封装好适配函数:

javascript
function createAjax() {
  let xmlhttp;
  if (window.XMLHttpRequest) {
    // code for IE7+, Firefox, Chrome, Opera, Safari
    xmlhttp = new XMLHttpRequest();
  } else {
    // code for IE6, IE5
    xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
  }
  return xmlhttp;
}

let xmlhttp = createAjax();

XMLHttpRequest 请求

如需将请求发送到服务器,我们使用 XMLHttpRequest 对象的 open() 和 send() 方法

方法描述
open(method,url,async)规定请求的类型、URL 以及是否异步处理请求。 method:请求的类型;GET 或 POST url:文件在服务器上的位置 async:true(异步)或 false(同步)
send(string)将请求发送到服务器。 string:仅用于 POST 请求
get 请求数据
javascript
xmlhttp.open("GET", "demo_get.asp", true);
xmlhttp.send();
post 发送数据
javascript
xmlhttp.open("POST", "ajax_test.asp", true);
xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xmlhttp.send("fname=Bill&lname=Gates");
方法描述
setRequestHeader(header,value)向请求添加 HTTP 头。 header: 规定头的名称 value: 规定头的值
Async = true

当使用 async=true 时,请规定在响应处于 onreadystatechange 事件中的就绪状态时执行的函数:

javascript
xmlhttp.onreadystatechange = function () {
  if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
    document.getElementById("myDiv").innerHTML = xmlhttp.responseText;
  }
};
xmlhttp.open("GET", "test1.txt", true);
xmlhttp.send();
Async = false

请记住同步请求时,JavaScript 会等到服务器响应就绪才继续执行。如果服务器繁忙或缓慢,应用程序会挂起或停止

当您使用 async=false 时,请不要编写 onreadystatechange 函数 - 把代码放到 send() 语句后面即可:

javascript
xmlhttp.open("GET", "test1.txt", false);
xmlhttp.send();
document.getElementById("myDiv").innerHTML = xmlhttp.responseText;

XMLHttpRequest 响应

如需获得来自服务器的响应,请使用 XMLHttpRequest 对象的 responseText 或 responseXML 属性。

属性描述
responseText获得字符串形式的响应数据。
responseXML获得 XML 形式的响应数据。
responseXML

如果来自服务器的响应是 XML,而且需要作为 XML 对象进行解析,请使用 responseXML 属性:

javascript
xmlDoc = xmlhttp.responseXML;
txt = "";
x = xmlDoc.getElementsByTagName("ARTIST");
for (i = 0; i < x.length; i++) {
  txt = txt + x[i].childNodes[0].nodeValue + "<br />";
}
document.getElementById("myDiv").innerHTML = txt;

XMLHttpRequest readyState

当请求被发送到服务器时,我们需要执行一些基于响应的任务。

每当 readyState 改变时,就会触发 onreadystatechange 事件。

readyState 属性存有 XMLHttpRequest 的状态信息。

下面是 XMLHttpRequest 对象的三个重要的属性:

属性描述
onreadystatechange存储函数(或函数名),每当 readyState 属性改变时,就会调用该函数。
readyState存有 XMLHttpRequest 的状态。从 0 到 4 发生变化。 0: 请求未初始化 1: 服务器连接已建立 2: 请求已接收 3: 请求处理中 4: 请求已完成,且响应已就绪
status200: "OK" 404: 未找到页面

当 readyState 等于 4 且状态为 200 时,表示响应已就绪

测试 onreadystatechange
按正常情况下:
javascript
var xmlhttp = new XMLHttpRequest();
console.log(xmlhttp.readystate);
xmlhttp.onreadystatechange = function () {
    console.log(xmlhttp.readystate);
}
xmlhttp.open(.., .., true);
xmlhttp.send();
// 0 1 2 3 4

因为 onreadystatechange 是在 readystate 改变的情况下触发,而 xmlhttp 的 readystate 一开始就为 0,所以我们要先打印

然后我们移动 onreadystatechange
javascript
var xmlhttp = new XMLHttpRequest();
console.log(xmlhttp.readystate);
xmlhttp.open(.., .., true);
xmlhttp.onreadystatechange = function () {
    console.log(xmlhttp.readystate);
}
xmlhttp.send();
// 0 2 3 4

var xmlhttp = new XMLHttpRequest();
console.log(xmlhttp.readystate);
xmlhttp.open(.., .., true);
console.log(xmlhttp.readystate);
xmlhttp.onreadystatechange = function () {
    console.log(xmlhttp.readystate);
}
xmlhttp.send();
// 0 1 2 3 4

这里我们发现 1 不见了所以 open()事件在 onreadystatechange 绑定前触发改变 readystate 状态

继续移动 onreadystatechange
javascript
var xmlhttp = new XMLHttpRequest();
console.log(xmlhttp.readystate);
xmlhttp.open(.., .., true);
console.log(xmlhttp.readystate);
xmlhttp.send();
xmlhttp.onreadystatechange = function () {
    console.log(xmlhttp.readystate);
}
// 0 1 2 3 4

这里我们发现 2 3 4 状态没有影响,这里可能是因为 send()异步造成的,因为状态改变成 2 需要传送数据,造成时间差,所以 onreadystatechange 事件在此之前能被绑定

为了保证浏览器兼容,应该在 open 之前给 onreadystatechange 事件赋值

onreadystatechange 事件处理程序没有 event 作为参数,所以只能使用 XHR 本身处理某些事物

在收到响应前可以使用 abort()方法取消异步请求

XHR.abort()

报错类型分析

500 报错会在 readyStatus 为 2、3、4 时触发

以此类推,其他报错发生了,但是 XHR 还是会进行到请求完成状态

HTTP 头部

每个 HTTP 请求和响应都会携带一些头部字段,这些字段可能对开发者有用,XHR 会通过一些方法暴露和请求头相关的一些信息

默认情况下,XHR 请求头会发送以下字段:

​ Accept:浏览器可以处理的内容类型

​ Accept-Charset:浏览器可以显示的字符集

​ Accept-Encoding:浏览器可以处理的压缩编码类型

​ Accept-Language:浏览器使用的语言

​ Connection:浏览器与服务器的连接类型

​ Cookie:页面中设置到 Cookie

​ Host:发送请求的页面所在的域

​ Referer:发送请求页面都 URI;这个字段在 HTTP 规范中就拼错了,所以为了兼容就将错就错(正确应该是 Refrrer)

​ User-Agent:浏览器的用户代理字符串

使用 setRequestHeader()方法可以设置请求头;为了请求头正确被发送,这个方法必须在 open 之后,send 之前调用;有些浏览器允许修改默认头部,有些不允许

可以使用 getResponseHeader()方法从 XHR 响应头获取信息;getAllResponseHeaders()方法可以获取包含所有响应头信息的字符串

GET 请求

get 请求的查询字符串都必须使用 encodeURIComponent()编码

javascript
function addURLParam(url, name, value) {
  url += url.indexOf("?") == -1 ? "?" : "&";
  url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
  return url;
}

POST 请求

浏览器向服务器发送较多的数据

XMLHttpRequest Level 2

并非所有浏览器都实现了 XMLHttpRequest Level 2 的所有部分,但都实现了其中部分功能

1、FormData 类型

该类型便于表单序列化

该类型实例有 append()方法,该方法接收两个值:键、值;可以使用该方法添加多个键值对

另外可以直接给构造函数传一个表单元素,可以自动将表单元素数据解析

最后可以将 FormData 实例直接通过 send 发送

2、超时

XHR 有一个 timeout 属性,用于表示发送请求后等待多少毫秒,如果响应不成功就中断请求,调用 ontimeout 事件处理程序

不过在超时后访问 status 属性则会发生错误,可以把检查 status 的语句放到 try/catch 中

3、overrideMimeType()方法

这个方法可以将响应返回的 MIME 类型覆盖,可以更好地让浏览器处理

必须在 send()之前调用 overrideMimeType()

进度事件

有 6 个相关的进度事件:

​ loadstart:在接收到响应的第一个字节时触发

​ progress:在接收响应期间反复触发

​ error:在请求出错时触发

​ abort:在调用 abort()时触发

​ load:在成功接收完响应时触发

​ loadend:在通信完成时,且在 error、abort、load 之后触发

load 事件

XHR 为了简化交互模式,增加了 load 事件,该事件在响应接收完成后立即触发,这样就不用检查 readystate 属性了

onload 事件处理程序会接收到一个 event 对象,其 target 属性为 XHR 实例,但是并不是所有浏览器提供了 event 对象,所以还是直接使用上下文中的 xhr 对象会比较好

只要是从服务器收到响应,无论状态码是什么,都会触发 load 事件,所以要检查 status 属性

progress 事件

Mozilla 在 XHR 对象上添加了 progress 事件,在浏览器接收数据期间,这个事件会反复触发;每次触发时,处理程序都会收到 event 对象,其 target 属性也是 XHR 对象,而且包含三个额外属性:lengthComputable、position、totalSize;lengthComputable 是一个布尔值,表示进度是否可用;position 是收到的字节数;totalSize 是响应的 Content-Length 头部定义的总字节数

必须在 open 之前添加 onprogress 事件处理程序

跨域资源共享

通过 XHR 进行 Ajax 通讯的一个主要限制是跨源安全策略

默认情况,XHR 只能访问与发起请求的页面在同一个域内的资源

跨域资源共享(CORS,Cross-Origin Resource Sharing)定义了浏览器与服务器如何实现跨源通信;其基本思路是通过添加自定义 HTTP 头部允许浏览器与服务器进行信息传输,以确实请求或响应应该成功还是失败

对于简单的请求,例如 GET 或 POST 请求,没有自定义头部,而且请求体是 text/plain 类型,这样的请求会在发送时有一个额外的头部叫 Origin;Origin 头部包含发送请求页面的源(协议、域名、端口):Origin: http://www.hello.com

如果服务器决定响应请求,就应该发送 Access-Control-Allow-Origin 头部,包含相同的源,或者如果资源是公开的,就包含"*",例如:Access-Control-Allow-Origin: http://www.hello.com

如果没有这个头部或者有但不匹配,则表明不会响应浏览器请求,否则服务器就会处理请求;无论请求还是响应都不会包含 cookie 信息

现代浏览器 XHR 对象原生支持 CORS,在尝试访问不同源的资源时,这个行为就会被自动触发

跨域 XHR 对象有一些额外限制:

​ 不能使用 setRequestHeader()设置自定义头部

​ 不能发送和接受 cookie

​ getAllRequestHeader()方法始终返回空字符串

一般在访问本地资源时使用相对 URL,访问远程资源时使用绝对 URL

CORS 请求分为两种:

  • 简单请求

    • HTTP 方法是下列之一:HEAD、GET、POST
    • 不能自定义请求头 header,不得人为设置简单请求集合之外的其他首部字段。简单请求集合为:Accept、Accept-Language、Content-Language、Content-Type(但仅能是下列之一:application/x-www-form-urlencoded、multipart/form-data、text/plain)
    • 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问
    • 请求中没有使用 ReadableStream 对象
  • 复杂请求

    • 不满足简单请求的请求

允许获取头部

服务器响应头部Access-Control-Expose-Headers 列出了哪些首部可以作为响应的一部分暴露给外部

默认情况下,只有七种 simple response headers(简单响应首部)可以暴露给外部:

如果想要让客户端可以访问到其他的首部信息,可以将它们在Access-Control-Expose-Headers里面列出来

值使用逗号分隔

预检请求

CORS 通过一种预检请求(preflighted request)的服务器验证机制,允许使用自定义头部、除 GET 和 POST 之外的方法,以及不同请求体内容类型;在涉及发送前者描述的某种高级选项的请求时,会先向服务器发送一个“预检”请求;这个请求使用 OPTIONS 方法发送并包含以下头部:Origin、Access-Control-Request-Method、Access-Control-Request-Headers

这个请求发送后,服务器可以确定是否允许这种类型请求,服务器通过在响应中发送如下头部与浏览器沟通这些信息:

​ Access-Control-Allow-Origin:与简单请求相同

​ Access-Control-Allow-Methods:允许的方法(逗号分隔的列表)

​ Access-Control-Allow-Headers:服务器允许的头部(逗号分隔的列表)

​ Access-Control-Max-Age:缓存预检请求的秒数

预检请求返回后会按照响应头指定的时间来缓存一段时间

预请求的触发条件:复杂的 CORS 请求

凭据请求

默认请情况下,跨域请求不提供凭据(cookie、HTTP 认证和客户端 SSL 证书),可以通过将 withCredentials 属性设置为 true

如果服务器允许带凭证的请求,可以在响应头中添加 Access-Control-Allow-Credentials:true

如果响应头中没有这个头部,则浏览器不会把响应交给 js(responseText 是空字符串,status 是 0,onerror 被调用);服务器也可以在预请求响应头中发送这个头部,以表明这个源允许发送凭证请求

替代性跨源技术

CORS 出现前,实现跨源 Ajax 通信是有点麻烦的;开发者需要依赖能够执行跨源请求的 DOM 特性,在不使用 XHR 对象情况下发送某种类型的请求;这些技术至今还有可取之处,因为它们不需要修改服务器

图片探测

利用 img 标签实现跨域通信的最早的一种技术;可以动态创建图片,检测 onload 事件和 onerror 事件做出响应;这种技术经常用于图片探测(image pings)

图片探测是与服务器之间简单、跨域、单向的通信;数据通过查询字符串发送,响应可以随意设置,不过一般是位图图片或值为 204 的状态码;浏览器通过图片探测拿不到任何数据,但可以通过监听 onload 和 onerror 事件知道什么时候能接收到响应

图片探测频繁用于跟踪用户在页面上的点击操作或动态显示广告;缺点是只能发送 GET 请求和无法获取服务器响应内容

JSONP

JSONP(JSON with padding)是在 Web 上一种 JSON 的变体;JSONP 看起来和 JSON 一样,只是会包含在一个函数调用里:

callback({ "name": "lsn" });

JSONP 格式包含两个部分:回调和数据;回调是在页面接收到响应后应该调用的函数,回调函数的名称是通过请求来动态指定的;数据就是作为参数传给回调函数的 JSON 数据,一个典型的 JSONP 请求:

http://freegeoip.net/json/?callback=handleResponse

这个例子把回调函数的名字指定为 handleResponse()

JSONP 请求是通过动态创建 script 元素并为 src 属性指定跨域 URL 实现的;此时 script 与 img 元素类似,能够不限制的从其他域加载资源;因为 JSONP 是有效的 JavaScript,所以 JSONP 响应在被加载完成后会立即执行:

javascript
function handleResponse(response) {
  console.log(response.ip);
}
let script = document.createElement("script");
script.src = "http://freegeoip.net/json/?callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);

相比于图片探测,JSONP 可以访问响应,不过也有一些缺点:首相,JSONP 是从不同的域拉取可执行代码,如果这个域不可信,则可能在响应中加入恶意内容;在使用不受控的服务器时,一定要保证是可以信任的;第二是不好确定 JSONP 请求是否失败

Fetch API

Fetch API 能够执行 XMLHttpRequest 对象所有任务,但更容易使用,接口也更现代化,能够在 Web 工作线程等现代 Web 工具中使用

Fetch API 必须是异步,它是 WHATWG 的一个“活标准”,原文说:Fetch 标准定义请求、响应,以及绑定二者的流程:获取(fetch)

Fetch API 本身是使用 JavaScript 请求资源的优秀工具,同时这个 API 也能够应用在服务线程中,提供拦截、重定向、修改通过 fetch()生成的请求接口

基本用法

fetch()方法是暴露在全局作用域中的,包括主页面执行线程、模块和工作线程;调用这个方法,浏览器就会向给定的 URL 发送请求

1、分派请求

fetch()只接收一个参数,多数情况下是要获取资源的 URL,返回一个期约

javascript
let r = fetch("/bar");
console.log(r); //Promise <pending>

URL 格式和 XHR 一样,相对或绝对路径等

请求完成、资源可用时,期约会解决为一个 Response 对象;这个对象是 API 的封装,可以通过它取得相应资源,获取资源时要使用这个对象的属性和方法:

javascript
fetch("bar.txt").then((response) => {
  console.log(response);
});

//Response { type: "basic", url: ... }
2、读取响应

最简单的是取得纯文本格式内容,这要用到 text()方法;该方法返回一个期约,会解决为取得资源的完整内容

javascript
fetch("bar.txt").then((response) => {
  response.text().then((data) => {
    console.log(data);
  });
});

//bar.txt的内容

内容的结构通常是打平的:

javascript
fetch("bar.txt")
  .then((response) => response.text())
  .then((data) => console.log(data));

//bar.txt的内容
3、处理状态码和请求失败

通过 response 的 status 和 statusText(状态文本)属性检查响应状态;当然,200、404、500...状态都是在解决的期约中

可以显示的设置 fetch()在遇到重定向时的行为,不过默认行为是跟随重定向并返回状态码不是 300~399 的响应;跟随重定向时,响应对象的 redirected 属性会被设置为 true,状态码仍然是 200:

javascript
fetch("/permanent-redirect").then((response) => {
  //这个默认行为是跟随重定向直到最终URL
  //这个例子至少会出现两轮网络请求
  console.log(response.status); //200
  console.log(response.statusText); //OK
  console.log(response.redirected); //true
});

只要服务器返回了响应,fetch()期约都会解决,真正的“成功”请求,则需要在处理响应时再定义;通常状态码为 200 就会被认定为成功了,其他情况可以被认定为不成功;为了区分,可以在状态码为 200~299 时检查 response 对象的 ok 属性,成功则为 true,失败则相反

因为服务器没有响应而导致浏览器超时,这样会导致 fetch()期约被拒绝

javascript
fetch("/hello").then(
  (response) => console.log(response),
  (err) => console.log(err)
);

违反 CORS、无网络连、HTTPS 错配及其他浏览器/网络策略问题都会导致期约被拒绝

可以通过 url 属性检查通过 fetch()发送请求时使用的完整 URL

javascript
fetch("qux").then((res) => console.log(res.url));
// https://foo.com/bar/qux
4、自定义选项

只使用 URL 时,fetch()会发送 GET 请求,只包含最低限度的请求头;要进一步配置,需要传入可选的第二个参数 init 对象,要按照规定填充

可填充的属性有:body、cache、credentials、headers、integrity、keepalive、method、mode、redirect、referrer、referrerPolicy、signal

详情参阅红宝书 p725

常见的 Fetch 请求模式

1、发送 JSON 数据
javascript
fetch("/send", {
  method: "POST",
  body: JSON.stringify({ foo: "bar" }),
  headers: { "Content-Type": "application/json" },
});
2、在请求体中发送参数
javascript
fetch("/send", {
  method: "POST",
  body: "foo=bar&baz=qux",
  headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
});
3、发送文件

请求体支持 FormData 实现,所以 fetch()也可以序列化并发送文件字段中的文件

javascript
let data = new FormData();
let img = document.querySelector("input[type='file']");
data.append("image", img.files[0]);

fetch("xxx", {
  method: "POST",
  body: data,
});

//多个文件
let data = new FormData();
let imgs = document.querySelector("input[type='file'][multiple]");
for (let i = 0; i < imgs.files.length; ++i) {
  data.append("image", img.files[i]);
}

fetch("xxx", {
  method: "POST",
  body: data,
});
4、加载 Blob 文件

常见的一种做法是将图片文件加载到内存,然后将其添加到 HTML 图片元素

可以使用响应对象上暴露的 blob()方法;该方法返回一个期约,解决为一个 Blob 的实例;然后可以将这个实例传给 URL.createObjectUrl()以生成可以添加给图片元素 src 属性的值:

javascript
fetch("down")
  .then((response) => response.blob())
  .then((blob) => {
    img.src = URL.createObjectUrl(blob);
  });
5、发送跨源请求

从不同的源请求资源,响应要包含 CORS 头部才能保证浏览器收到响应;没有这些头部,跨源请求会失败并抛出错误

如果代码不需要访问响应,也可以发送 no-cors 请求;此时响应的 type 属性值为 opaque,无法读取响应内容,这种方式适合发送探测请求或者将响应缓存起来供以后使用

6、中断请求

通过 AbortController/AbortSignal 中断请求;调用 AbortController.abort()会中断所有网络传输,特别适合希望停止传输大型负载的情况;中断的 fetch 请求会包含错误的拒绝

Headers 对象

Headers 对象是所有外发请求和入站响应头部的容器,每个外发的 Request 都包含一个空的 Headers 实例,可以通过 Request.prototype.headers 访问,每个入站 Response 实例也可以通过 Response.prototype.headers 访问包含响应头部的 Headers 对象,这两个属性都是可修改属性

使用 new Headers()也可以创建新的实例

1、Headers 和 Map 的相似之处

因为 HTTP 头部本质上是序列化后的键值对,所以 Headers 和 Map 极为相似;它们的 JavaScript 表示则是中间接口;Headers 和 Map 类型都有 get()、set()、has()、delete()等实例方法

javascript
let headers = new Headers();
headers.set("foo", "bar");
headers.has("foo");
headers.get("foo");
headers.set("foo", "qux");
headers.delete("foo");

//它们都可以使用一个可迭代对象来初始化
let seed = [["foo", "bar"]];
let headers = new Headers(seed);

//它们也有相同的keys()、values()、entries()迭代接口
headers.keys();
headers.values();
headers.entries(); //['foo', 'bar']
2、Headers 独有特性

初始化时,Headers 可以使用键值对的对象初始化

javascript
let seed = { foo: "bar" };

还支持使用 append 方法添加多个值,如果键值相同,则值会添加到相应 value 中:

javascript
h.append("foo", "bar");
h.get("foo"); //'bar'
h.append("foo", "baz");
h.get("foo"); //'bar, baz'
3、头部护卫

某些情况下,不是所有的 HTTP 头部都能被客户端修改,Headers 对象使用护卫来防止不被允许的修改;不同的护卫设置会改变 get、append、delete 的行为,违反护卫限制会抛出 TypeError

Headers 会因来源不同而展现不同的行为,由护卫来控制;js 可以决定 Headers 的护卫设置

护卫设置有如下几种:none、request、request-no-cors、response、immutable

详情查看红宝书 p732

Request 对象

这个对象是获取资源请求的接口,这个接口暴露了相关信息和方式

1、创建 Request 对象

通过构造函数初始化 Request 对象,传入一个参数,一般是 URL

构造函数也接收可选的第二个参数,一个 init 对象,这个对象和 fetch 中的 init 对象一样

2、克隆 Request 对象

可以使用 Request 构造函数或者使用 clone()方法创建 Request 对象副本

将一个 Request 对象作为第一个参数传给构造函数,会得到一个该请求的副本;如果再传入一个 init 对象,则会覆盖原来的 init 对象中同名的值;这种克隆方式并不总是能得到一模一样的副本(第一个请求的请求体会被标记为”已使用“,通过 Request 实例的 bodyUsed 属性可以访问);如果源对象与新对象不同源,则 referrer 属性会被清除;如果源对象的 mode 为 navigate,则会被转换为 same-origin

第二种克隆方式是使用 clone()函数,这个方法将会创建一模一样的副本,任何值都不会被覆盖,而且不会讲任何请求的请求体标记为”已使用“

如果对象的 bodyUsed 为 true(请求体已被读取),则上述两种方法都不能用来创建这个对象的副本,不然会抛出 TypeError

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

在调用 fetch 时可以传入已经创建好的 Request 实例而不是 URL,传给 fetch 的 init 对象会覆盖传入请求对象的值

fetch 会在内部克隆这个 Request 对象,将这个对象的请求体标记为“已使用”,所以 fetch 也不能使用请求体已经被使用过的 Request 对象

所以说,有请求体的请求只能在一次 fetch 中使用(不包含请求体的请求不受此限制)

如果想要多次使用一个请求,则需要在 fetch 之前使用 clone

Response 对象

该对象是获取资源响应的接口,暴露了相关信息和方法

1、创建 Response 对象

通过构造函数初始化一个对象,不需要传参数;此时响应值均为默认值,无实际意义

构造函数可以接收一个可选的 body 参数,可以是 null,等同于 fetch 参数 init 中的 body;还可以接收一个可选的 init 对象,可以包含下列键值:

headers必须是 Headers 对象实例或包含字符串键值对常规对象实例,默认为没有键值对的 Headers 对象
status表示 HTTP 响应状态码的整数,默认为 200
statusText表示 HTTP 响应状态的字符串。默认为空字符串
javascript
let response = new Response("foobar", {
  status: 418,
  statusText: "text",
});

大多数情况下,产生 Response 对象的主要方式是调用 fetch(),他返回一个最后会解决为 Response 对象的期约,这个 Response 对象代表实际的 HTTP 响应

Response 类还有两个静态方法可以生成 Response 对象:Response.redirect()和 Response.error();前者接收一个 URL 和一个重定向状态码(301、302、303、307、308),返回重定向的 Response 对象,提供的状态码必须对应重定向,否则会抛出错误;后者用于产生网络错误的 Response 对象(网络错误会导致 fetch 期约被拒绝)

2、读取响应状态信息

Response 对象包含一组只读属性,描述请求完成后的状态,这些属性有:headers、ok、redirected、status、statusText、type、url

相关说明查阅红宝书 p737

3、克隆 Response 对象

使用 clone()方法可以克隆一个一模一样的副本,不会覆盖任何值,不会将请求体标记为已使用

let r1 = r.clone();

如果响应的 bodyUsed 属性为 true(响应体已经被读取),则不能创建这个对象的副本,否则会抛出错误

有响应体的 Response 对象只能读取一次(不包含响应体的 Response 不受此限制),否则会抛出错误

在读取前使用 clone()可以将一个对象多次调用

也可以实现伪克隆操作:

javascript
let r1 = new Response("foo");
let r2 = new Response(r1.body);

r1.bodyUsed; //false
r2.bodyUsed; //false

Request、Response 及 Body 混入

Request 和 Response 都使用了 Fetch API 的 Body 混入,以实现两者承担有效载荷能力

这个混入为两个类型实现了只读的 body 属性(实现为 ReadableStream)、只读的 bodyUsed 布尔值(表示 body 是否已读)和一组方法,用于从流中读取内容并将结果转换为某种 JavaScript 对象类型

将 Request 和 Response 主体作为流来使用主要有两个原因;一个是有效载荷大小可能会导致网络延迟,另一个是流 API 本身在处理有效载荷方面有优势;除此之外最好是一次性获取资源主体

Body 混入提供了 5 个方法,用于将 ReadableStream 转存到缓冲区的内存里,将缓冲区转换为某种 JavaScript 对象类型,以及通过期约产生结果;再解决之前,期约会等待主体流报告完成及缓冲被解析;客户端必须等待响应的资源完全加载才能访问其内容

1、Body.text()

该方法返回期约,解决为将缓冲区转存得到的 UTF-8 格式字符串

2、Body.json()

该方法返回期约,解决为将缓冲区转存得到的 JSON

3、Body.formData()

浏览器可以将 FormData 对象序列化/反序列化为主体

该方法返回期约,解决为将缓冲区转存得到的 FormData 实例

4、Body.arrayBuffer()

有时候可能需要以原始二进制格式查看和修改主体

该方法可以将主题内容转换为 ArrayBuffer 实例,方法返回一个期约,解决为将缓冲区转存得到的 ArrayBuffer 实例

5、Body.blob()

有时候可能需要直接使用二进制格式主体,而不需要查看和修改

该方法可以将主体内容转换为 Blob 实例,方法返回一个期约,解决为将缓冲区转存得到的 Blob 实例

6、一次性流

因为 Body 混入是构建在 ReadableStream 之上的,所以主体流只能使用一次;否则会抛出错误

读取流的过程中,所有这些方法也会在它们被调用时给 ReadableStream 加锁,阻止其它读取器访问

bodyUsed 布尔值属性表示 ReadableStream 是否已分发(disturbed),意思是读取器是否已经在流上加了锁;不表示流已经被完全读取

7、使用 ReadableStream 主体

从 TCP/IP 角度看,传输的数据是以分块形式抵达端点的,而且速度受到网速的限制;Fetch API 通过 ReadableStream 支持在这些块到达时就实时读取和操作这些数据

Steam API 定义了,ReadableStream 暴露了 getReader()方法,用于产生 ReadableStreamDefaultReader,这个读取器可以用于在数据到达时异步获取数据块;数据流的格式是 Uint8Array

javascript
fetch('xxx')
	.then((res) => res.body)
	.then((body) => {
    	let reader = body.getReader();
    	reader.read()
    		.then(console.log);
});

// { value: Uint8Array{}, done: false }

//随着数据流到来取得整个有效载荷,可以递归调用read方法
fetch('xxx')
	.then((res) => res.body)
	.then((body) => {
    	let reader = body.getReader();
    	function processNextChunk({value, done}) {
            if (done) {
                return;
            }

            //....

            return reader.read()
            			.then(processNextChunk);
        }
    	reader.read()
    		.then(processNextChunk);
});

//可以使用async/await将上面的递归调用打平
fetch('xxx')
	.then((res) => res.body)
	.then((body) => {
    	let reader = body.getReader();
    	while (true) {
            let { value, done } = await reader.read();
            if (done) {
                break;
            }
            //....
        }
});

//使用Iterable接口,就可以使用for-await-of循环
fetch('xxx')
	.then((res) => res.body)
	.then((body) => {
    	let reader = body.getReader();
    	let asyncIterable = {
            [Symbol.asyncIterator]() {
                return {
                    next() {
                        return reader.read();
                    }
                }
            }
        }
        for await (chunk of asyncIterable) {
            //.....
        }
});

//通过将异步逻辑包装到一个生成器函数中,还可以进一步简化代码;如果流因为耗尽或错误而终止,读取器会释放锁,以允许不同的流读取器继续操作
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('xxxx')
	.then((res) => res.body)
	.then(async function(body) {
    	for await (chunk of streamGenerator(body)) {
            //.....
        }
})

当读取完 Uint8Array 块之后,浏览器会将其标记为可以被垃圾回收;对于需要在不连续内存中连续检查大量数据的情况,这样可以节省很多内存空间

缓冲区大小和浏览器是否等待缓冲区被填充后才将其推入流中,要根据 JavaScript 运行时实现

不同浏览器中分块的大小可能不同,这取决于带宽和网络延迟;浏览器如果决定不等待网络,也可以将部分填充的缓冲区发送到流,我们要准备处理以下情况:

​ 不同大小的 Uint8Array 块

​ 部分填充的 Uint8Array 块

​ 块到达的时间间隔不定

默认情况下,块是以 Uint8Array 格式抵达的;有时候会出现某些值作为多字节字符被分散到两个连续的块中,手动处理这种情况是很麻烦的,所以可以使用 Encoding API 的可插拔方案

要将 Uint8Array 转换为可读文本,可以将缓冲区传给 TextDecoder,返回转换后的值;通过设置 stream:true,可以将之前的缓冲区保留在内存,从而让跨越两个块的内容可以被正确解码:

javascript
let decoder = new TextDecoder();

//.....
decoder.decode(chunk, { stream: true });

因为可以使用 ReadableStream 创建 Response 对象,所以就可以在读取流之后,将其通过管道导入另一个流;然后在这个新流上使用 Body 方法,如 text();这样就可以随着流的到达实时检查和操作流的内容:

javascript
fetch("xxx")
  .then((res) => res.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((res) => res.text())
  .then(console.log);

Beacon API

有些事务需要在浏览器 unload 事件发送网络请求,但是在 unload 事件处理程序中创建的任何异步请求都会被浏览器取消,所以 XMLHttpRequest 和 fetch 都不适合

为了解决这个问题,W3C 引入了补充性的 Beacon API;这个 API 给 navigator 对象增加了一个 sendBeacon()方法,接收一个 URL 和一个数据有效载荷参数,并会发送一个 POST 请求;可选的数据有效载荷参数有 ArrayBufferView、Blob、DOMString、FormData 实例;如果请求成功进入了最重要发送的任务队列,则这个方法返回 true、否则返回 false:

javascript
navigator.sendBeacon("xxxx", '{foo: "bar"}');

sendBeacon()可以在任何时候都能使用;调用 sendBeacon()后,浏览器会把请求添加到一个内部的请求队列,浏览器会主动地发送队列中的请求;浏览器保证在原始页面已经关闭的情况下也会发送请求;状态码、超时和其它网络原因造成的失败是完全不透明的,不能通过编程方式处理;信标(beacon)请求会携带调用 sendBeacon()时所有相关的 cookie

Web Socket

套接字的目标是通过一个长时连接实现与服务器全双工、双向的通信;在 js 创建 Web Socket 时,一个 HTTP 请求会发送到服务器以初始化连接;服务器响应后,连接使用 HTTP 的 Upgrade 头部从 HTTP 协议转换到 Web Socket 协议;所以必须使用支持该协议的专有服务器

因为 Web Socket 使用了自定义协议,所以 URL 方案稍有变化,要使用 ws://和 wss://;前者是不安全的连接,后者是安全连接

API

要创建一个新的 Web Socket,就要实例化一个 WebSocket 对象并传入提供连接的 URL:

let socket = new WebSocket("ws://www.example.com/server.php");

必须给 WebSocket 构造函数传入一个绝对 URL,同源策略不使用 WebSocket,因此可以打开到任意站点的连接;至于是否与来自特定源的页面通信,则完全取决于服务器

浏览器会在初始化 WebSocket 对象后立即创建连接;也有一个 readyState 属性表示当前状态:

​ 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

发送和接收数据

可以通过 send()方法并传入一个字符、ArrayBuffer、Blob 向服务器发送数据;发送消息时 WebSocket 对象上会触发 message 事件,可以通过事件的 event.data 属性访问到有效载荷,这个属性返回的数据可能是 ArrayBuffer 或 Blob,这由 WebSocket 对象的 binaryType 属性决定,该属性可能是“blob”或“arraybuffer”

其他事件

WebSocket 对象在连接生命周期中有可能触发 3 个其它事件:open、error、close

WebSocket 对象不支持 DOM Level 2 事件监听

这三个事件中只有 close 事件的 event 对象上有额外信息;这个对象上有三个额外属性:wasClean、code、reason,wasClean 是一个布尔值,表示连接是否干净的关闭,code 是一个来自服务器的数值状态码,reason 是一个字符串,包含服务器发来的信息

安全

通用层面上一般需要考虑几个问题

例如:/xxx/?id=23这样将 23 可以换成其他值,如果服务器不做权限处理就会导致用户信息被泄露;这样子在未授权系统可以访问某个资源时,可以将其视为跨站点请求伪造(CSRF,cross-site request forgery)攻击

Ajax 应用程序,无论大小,都会受到 CSRF 攻击的影响,包括无害漏洞验证攻击和恶意的数据盗窃或数据破坏攻击

关于安全防护 Ajax 相关 URL 的一般理论认为,需要验证请求发送者拥有对资源的访问权限,可以使用如下方式:

​ 要求通过 SSL 访问能够被 Ajax 访问的资源

​ 要求每个请求都发送一个按约定算法计算好的令牌(token)

以下手段对防护 CSRF 攻击是无效的

​ 要求 POST 请求而非 GET 请求

​ 使用来源 URL 验证来源(来源 URL 很容易伪造)

​ 基于 cookie 验证(同样很容易伪造)