本章内容
使用 XMLHttpRequest 对象
处理 XMLHttpRequest 事件
源域 Ajax 限制
Fetch API
Streams API
把 Ajax 推到历史舞台上的关键技术是 XMLHttpRequest(XHR)对象。XHR 为发送服务器请求和获取响应提供了合理的接口。这个接口可以实现异步从服务器获取额外数据,意味着用户点击不用页面刷新也可以获取数据。XHR 对象的 API 被普遍认为比较难用,而 Fetch API自从诞生以后就迅速成为了 XHR 更现代的替代标准。Fetch API 支持期约(promise)和服务线程(service worker),已经成为极其强大的 Web 开发工具
文章目录
1 XMLHttpRequest 对象
所有现代浏览器都通过 XMLHttpRequest 构造函数原生支持 XHR 对象:
let xhr = new XMLHttpRequest();
1.1 使用 XHR
只能访问同源 URL,也就是域名相同、端口相同、协议相同。
xhr.open("get", "example.txt", false); //为发送请求做好准备。
xhr.send(null); // 发送定义好的请求,如果不需要发送请求体,则必须传 null,
因为这个请求是同步的,所以 JavaScript 代码会等待服务器响应之后再继续执行。收到响应后,XHR对象的以下属性会被填充上数据。
responseText:作为响应体返回的文本。
responseXML:如果响应的内容类型是"text/xml"或"application/xml",那就是包含响应数据的 XML DOM 文档。
status:响应的 HTTP 状态。
statusText:响应的 HTTP 状态描述。
收到响应后,第一步要检查 status 属性以确保响应成功返回。一般来说,HTTP 状态码为 2xx 表示成功。如果 HTTP状态码是 304,则表示资源未修改过,是从浏览器缓存中直接拿取的。最好检查 status 而不是 statusText 属性,因为后者已经被证明在跨浏览器的情况下不可靠。
虽然可以像前面的例子一样发送同步请求,但多数情况下最好使用异步请求,这样可以不阻塞JavaScript 代码继续执行。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) {
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.open("get", "example.txt", true);
xhr.send(null);
在收到响应之前如果想取消异步请求,可以调用 abort()方法:
xhr.abort();
1.2 HTTP 头部
默认情况下,XHR 请求会发送以下头部字段。
Accept:浏览器可以处理的内容类型。
Accept-Charset:浏览器可以显示的字符集。
Accept-Encoding:浏览器可以处理的压缩编码类型。
Accept-Language:浏览器使用的语言。
Connection:浏览器与服务器的连接类型。
Cookie:页面中设置的 Cookie。
Host:发送请求的页面所在的域。
Referer:发送请求的页面的 URI。注意,这个字段在 HTTP 规范中就拼错了,所以考虑到兼容性也必须将错就错。(正确的拼写应该是 Referrer。)
User-Agent:浏览器的用户代理字符串。
如果需要发送额外的请求头部,可以使用 setRequestHeader()方法。必须在 open()之后、send()之前调用 setRequestHeader():
xhr.open("get", "example.php", true);
xhr.setRequestHeader("MyHeader", "MyValue");
xhr.send(null);
1.3 GET 请求
xhr.open("get", "example.php?name1=value1&name2=value2", true);
发送 GET 请求最常见的一个错误是查询字符串格式不对。查询字符串中的每个名和值都必须使用encodeURIComponent()编码。
1.4 POST 请求
xhr.open("post", "example.php", true);
注意 POST 请求相比 GET 请求要占用更多资源。从性能方面说,发送相同数量的数据,GET 请求比 POST 请求要快两倍。
1.5 XMLHttpRequest Level 2
1. FormData 类型
现代 Web 应用程序中经常需要对表单数据进行序列化,因此新增了FormData 类型。FormData 类型便于表单序列化,也便于创建与表单类似格式的数据然后通过 XHR发送。下面的代码创建了一个 FormData 对象,并填充了一些数据:
let data = new FormData();
data.append("name", "Nicholas"); // append()方法接收两个参数:键和值,相当于表单字段名称和该字段的值。
使用 FormData 的另一个方便之处是不再需要给 XHR 对象显式设置任何请求头部了。XHR 对象能够识别作为 FormData 实例传入的数据类型并自动配置相应的头部。
2. 超时
在给 timeout 属性设置了一个时间且在该时间过后没有收到响应时,XHR 对象就会触发 timeout 事件,调用 ontimeout 事件处理程序。如果响应不成功就中断请求。
xhr.timeout = 1000; // 设置 1 秒超时
xhr.ontimeout = function() {
alert("Request did not return in a second.");
};
xhr.send(null);
3. overrideMimeType()方法
假设服务器实际发送了 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);
这个例子强制让 XHR 把响应当成 XML 而不是纯文本来处理。为了正确覆盖响应的 MIME 类型,必须在调用 send()之前调用 overrideMimeType()。
2 进度事件
Progress Events 是 W3C 的工作草案,定义了客户端服务器端通信。这些事件最初只针对 XHR,现在也推广到了其他类似的 API。有以下 6 个进度相关的事件。
loadstart:在接收到响应的第一个字节时触发。
progress:在接收响应期间反复触发。
error:在请求出错时触发。
abort:在调用 abort()终止连接时触发。
load:在成功接收完响应时触发。
loadend:在通信完成时,且在 error、abort 或 load 之后触发。
2.1 load 事件
Firefox 最初在实现 XHR 的时候,曾致力于简化交互模式。最终,增加了一个 load 事件用于替代readystatechange 事件。load 事件在响应接收完成后立即触发,这样就不用检查 readyState 属性了。onload 事件处理程序会收到一个 event 对象,其 target 属性设置为 XHR 实例,在这个实例上可以访问所有 XHR 对象属性和方法。不过,并不是所有浏览器都实现了这个事件的 event 对象。
let xhr = new XMLHttpRequest();
xhr.onload = function() {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
};
xhr.open("get", "altevents.php", true);
xhr.send(null);
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) {
alert(xhr.responseText);
} else {
alert("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);
为了保证正确执行,必须在调用 open()之前添加 onprogress 事件处理程序。在前面的例子中,每次触发 progress 事件都会更新 HTML 元素中的信息。假设响应有 Content-Length 头部,就可以利用这些信息计算出已经收到响应的百分比。
3 跨源资源共享
跨源资源共享(CORS,Cross-Origin Resource Sharing)定义了浏览器与服务器如何实现跨源通信。CORS 背后的基本思路就是使用自定义的 HTTP 头部允许浏览器和服务器相互了解,以确实请求或响应应该成功还是失败。
对于简单的请求,比如 GET 或 POST 请求,没有自定义头部,而且请求体是 text/plain 类型,这样的请求在发送时会有一个额外的头部叫 Origin。Origin 头部包含发送请求的页面的源(协议、域名和端口),以便服务器确定是否为其提供响应。下面是 Origin 头部的一个示例:
Origin: http://www.nczonline.net
如果服务器决定响应请求,那么应该发送 Access-Control-Allow-Origin 头部,包含相同的源;或者如果资源是公开的,那么就包含"*"。比如
Access-Control-Allow-Origin: http://www.nczonline.net
如果没有这个头部,或者有但源不匹配,则表明不会响应浏览器请求。否则,服务器就会处理这个请求。注意,无论请求还是响应都不会包含 cookie 信息。
现代浏览器通过 XMLHttpRequest 对象原生支持 CORS。在尝试访问不同源的资源时,这个行为会被自动触发。要向不同域的源发送请求,可以使用标准 XHR对象并给 open()方法传入一个绝对 URL,比如:
xhr.open("get", "http://www.somewhere-else.com/page/", true);
xhr.send(null);
出于安全考虑,跨域 XHR对象也施加了一些额外限制。
不能使用 setRequestHeader()设置自定义头部。
不能发送和接收 cookie。
getAllResponseHeaders()方法始终返回空字符串。
3.1 预检请求
CORS 通过一种叫预检请求(preflighted request)的服务器验证机制,允许使用自定义头部、除 GET和 POST 之外的方法,以及不同请求体内容类型。在要发送涉及上述某种高级选项的请求时,会先向服务器发送一个“预检”请求。这个请求使用 OPTIONS 方法发送并包含以下头部。
Origin:与简单请求相同。
Access-Control-Request-Method:请求希望使用的方法。
Access-Control-Request-Headers:(可选)要使用的逗号分隔的自定义头部列表。
下面是一个假设的 POST 请求,包含自定义的 NCZ 头部:
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:缓存预检请求的秒数。
Access-Control-Allow-Origin: http://www.nczonline.net
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000
预检请求返回后,结果会按响应指定的时间缓存一段时间。换句话说,只有第一次发送这种类型的请求时才会多发送一次额外的 HTTP 请求。
3.2 凭据请求
默认情况下,跨源请求不提供凭据(cookie、HTTP 认证和客户端 SSL 证书)。可以通过将withCredentials 属性设置为 true 来表明请求会发送凭据。如果服务器允许带凭据的请求,那么可以在响应中包含如下 HTTP 头部:
Access-Control-Allow-Credentials: true
如果发送了凭据请求而服务器返回的响应中没有这个头部,则浏览器不会把响应交给 JavaScript(responseText 是空字符串,status 是 0,onerror()被调用)。注意,服务器也可以在预检请求的响应中发送这个 HTTP 头部,以表明这个源允许发送凭据请求。
4 替代性跨源技术
CORS 出现之前,实现跨源 Ajax 通信是有点麻烦的。开发者需要依赖能够执行跨源请求的 DOM 特性,在不使用 XHR 对象情况下发送某种类型的请求。虽然 CORS 目前已经得到广泛支持,但这些技术仍然没有过时,因为它们不需要修改服务器。
4.1 图片探测
图片探测是利用img标签实现跨域通信的最早的一种技术。任何页面都可以跨域加载图片而不必担心限制,因此这也是在线广告跟踪的主要方式。可以动态创建图片,然后通过它们的 onload 和onerror 事件处理程序得知何时收到响应。
这种动态创建图片的技术经常用于图片探测(image pings)。图片探测是与服务器之间简单、跨域、单向的通信。数据通过查询字符串发送,响应可以随意设置,不过一般是位图图片或值为 204 的状态码。浏览器通过图片探测拿不到任何数据,但可以通过监听 onload 和 onerror 事件知道什么时候能接收到响应。下面看一个例子:
let img = new Image();
img.onload = img.onerror = function() {
alert("Done!");
};
// 设置完 src 属性之后请求就开始了,这个例子向服务器发送了一个 name值。
img.src = "http://www.example.com/test?name=Nicholas";
图片探测频繁用于跟踪用户在页面上的点击操作或动态显示广告。当然,图片探测的缺点是只能发送 GET 请求和无法获取服务器响应的内容。这也是只能利用图片探测实现浏览器与服务器单向通信的原因。
4.2 JSONP
JSONP 是“JSON with padding”的简写,是在 Web 服务上流行的一种 JSON 变体。JSONP 看起来跟 JSON 一样,只是会被包在一个函数调用里,比如:
callback({ "name": "Nicholas" });
JSONP 格式包含两个部分:回调和数据。回调是在页面接收到响应之后应该调用的函数,通常回调函数的名称是通过请求来动态指定的。而数据就是作为参数传给回调函数的 JSON 数据。下面是一个典型的 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 是从不同的域拉取可执行代码。如果这个域并不可信,则可能在响应中加入恶意内容。此时除了完全删除 JSONP没有其他办法。在使用不受控的 Web 服务时,一定要保证是可以信任的。
- 第二个缺点是不好确定 JSONP 请求是否失败。虽然 HTML5 规定了
5 Fetch API
Fetch API 能够执行 XMLHttpRequest 对象的所有任务,但更容易使用,接口也更现代化,能够在Web 工作线程等现代 Web 工具中使用。XMLHttpRequest 可以选择异步,而 Fetch API 则必须是异步。Fetch API 是 WHATWG 的一个“活标准”(living standard),用规范原文说,就是“Fetch 标准定义请求、响应,以及绑定二者的流程:获取(fetch)”。
5.1 基本用法
fetch()方法是暴露在全局作用域中的,包括主页面执行线程、模块和工作线程。调用这个方法,浏览器就会向给定 URL 发送请求。
1. 分派请求
fetch()只有一个必需的参数 input。多数情况下,这个参数是要获取资源的 URL。这个方法返回一个期约:
fetch('bar.txt')
.then((response) => {
console.log(response);
});
// Response { type: "basic", url: ... }
2. 读取响应
读取响应内容的最简单方式是取得纯文本格式的内容,这要用到 text()方法。这个方法返回一个期约,会解决为取得资源的完整内容:
fetch('bar.txt')
.then((response) => {
response.text().then((data) => {
console.log(data);
});
});
3. 处理状态码和请求失败
Fetch API 支持通过 Response 的 status(状态码)和 statusText(状态文本)属性检查响应状态。成功获取响应的请求通常会产生值为 200 的状态码,如下所示:
fetch('/bar')
.then((response) => {
console.log(response.status); // 200
console.log(response.statusText); // OK
});
4. 自定义选项
只使用 URL 时,fetch()会发送 GET 请求,只包含最低限度的请求头。要进一步配置如何发送请求,需要传入可选的第二个参数 init 对象。init 对象要按照下表中的键/值进行填充。
键 | 值 |
---|---|
body | 指定使用请求体时请求体的内容,Blob、BufferSource、FormData、URLSearchParams等 |
cache | 用于控制浏览器与 HTTP缓存的交互。要跟踪缓存的重定向,请求的 redirect 属性值必须是"follow" |
headers | 用于指定请求头部,必须是 Headers 对象实例或包含字符串格式键/值对的常规对象 |
略 |
5.2 常见 Fetch 请求模式
与 XMLHttpRequest 一样,fetch()既可以发送数据也可以接收数据。使用 init 对象参数,可以配置 fetch()在请求体中发送各种序列化的数据。
1. 发送 JSON 数据
可以像下面这样发送简单 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
});
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);
// 已经中断
5.3 Headers 对象
Headers 对象是所有外发请求和入站响应头部的容器。每个外发的 Request 实例都包含一个空的Headers 实例,可以通过 Request.prototype.headers 访问,每个入站 Response 实例也可以通过Response.prototype.headers 访问包含着响应头部的 Headers 对象。这两个属性都是可修改属性。另外,使用 new Headers()也可以创建一个新实例。
1. Headers 与 Map 的相似之处
Headers 对象与 Map 对象极为相似。这是合理的,因为 HTTP 头部本质上是序列化后的键/值对,它们的 JavaScript 表示则是中间接口。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
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()方法支持添加多个值。
let h = new Headers();
h.append('foo', 'bar');
console.log(h.get('foo')); // "bar"
3. 头部护卫
某些情况下,并非所有 HTTP 头部都可以被客户端修改,而 Headers 对象使用护卫来防止不被允许的修改。
护卫 | 适用情形 | 限 制 |
---|---|---|
request | 在通过构造函数初始化 Request对象,且 mode值为非 no-cors 时激活 | 不允许修改禁止修改的头部 |
response | 在通过构造函数初始化 Response 对象时激活 | 不允许修改禁止修改的响应头部 |
5.4 Request 对象
Request 对象是获取资源请求的接口。这个接口暴露了请求的相关信息,也暴露了使用请求体的不同方式。
1. 创建 Request 对象
let r = new Request('https://foo.com');
console.log(r);
// Request {...}
2. 克隆 Request 对象
Fetch API 提供了两种不太一样的方式用于创建 Request 对象的副本:使用 Request 构造函数和使用 clone()方法。
let r1 = new Request('https://foo.com',{ method: 'POST', body: 'foobar' });
let r2 = new Request(r1);
// 第一个请求的请求体会被标记为“已使用”
console.log(r1.bodyUsed); // true
另一种方法会创建一模一样的副本,任何值都不会被覆盖。与第一种方式不同,这种方法不会将任何请求的请求体标记为“已使用”
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
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 中使用。
let r = new Request('https://foo.com',
{ method: 'POST', body: 'foobar' });
fetch(r);
fetch(r);
// TypeError: Cannot construct a Request with a Request object that has already
been used.
要想基于包含请求体的相同 Request 对象多次调用 fetch(),必须在第一次发送 fetch()请求前调用 clone():
let r = new Request('https://foo.com',
{ method: 'POST', body: 'foobar' });
// 3 个都会成功
fetch(r.clone());
fetch(r.clone());
fetch(r);
5.5 Response 对象
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 对象的主要方式是调用 fetch(),它返回一个最后会解决为Response 对象的期约,这个 Response 对象代表实际的 HTTP 响应。下面的代码展示了这样得到的Response 对象:
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/"
// }
2. 读取响应状态信息
Response 对象包含一组只读属性,描述了请求完成后的状态
type: 字符串,包含响应类型。可能是下列字符串值之一
basic:表示标准的同源响应
cors:表示标准的跨源响应
error:表示响应对象是通过 Response.error()创建的
opaque:表示 no-cors 的 fetch()返回的跨源响应
opaqueredirect:表示对 redirect 设置为 manual 的请求的响应
3. 克隆 Response 对象
克隆 Response 对象的主要方式是使用 clone()方法
let r1 = new Response('foobar');
let r2 = r1.clone();
console.log(r1.bodyUsed); // false
console.log(r2.bodyUsed); // false
5.6 Request、Response 及 Body 混入
Request 和 Response 都使用了 Fetch API 的 Body 混入,以实现两者承担有效载荷的能力。这个混入为两个类型提供了只读的 body 属性(实现为 ReadableStream)、只读的 bodyUsed 布尔值(表示 body 流是否已读)和一组方法,用于从流中读取内容并将结果转换为某种 JavaScript 对象类型。
通常,将 Request 和 Response 主体作为流来使用主要有两个原因。一个原因是有效载荷的大小可能会导致网络延迟,另一个原因是流 API 本身在处理有效载荷方面是有优势的。除此之外,最好是一次性获取资源主体。
1. Body.text()
Body.text()方法返回期约,解决为将缓冲区转存得到的 UTF-8 格式字符串。下面的代码展示了在 Response 对象上使用 Body.text():
.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()
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()
let myFormData = new FormData();
myFormData.append('foo', 'bar');
在通过 HTTP 传送时,WebKit 浏览器会将其序列化为下列内容:
------WebKitFormBoundarydR9Q2kOzE6nbN7eR
Content-Disposition: form-data; name="foo"bar
------WebKitFormBoundarydR9Q2kOzE6nbN7eR--
4. Body.arrayBuffer()
有时候,可能需要以原始二进制格式查看和修改主体。为此,可以使用 Body.arrayBuffer()将主体内容转换为 ArrayBuffer 实例。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()
fetch('https://foo.com')
.then((response) => response.blob())
.then(console.log);
// Blob(...) {size:..., type: "..."}
6. 一次性流
因为 Body 混入是构建在 ReadableStream 之上的,所以主体流只能使用一次。这意味着所有主体混入方法都只能调用一次,再次调用就会抛出错误
fetch('https://foo.com')
.then((response) => response.blob().then(() => response.blob()));
// TypeError: Failed to execute 'blob' on 'Response': body stream is locked
7. 使用 ReadableStream 主体
JavaScript 编程逻辑很多时候会将访问网络作为原子操作,比如请求是同时创建和发送的,响应数据也是以统一的格式一次性暴露出来的。这种约定隐藏了底层的混乱,让涉及网络的代码变得很清晰。
从 TCP/IP 角度来看,传输的数据是以分块形式抵达端点的,而且速度受到网速的限制。接收端点会为此分配内存,并将收到的块写入内存。Fetch API 通过 ReadableStream 支持在这些块到达时就实时读取和操作这些数据。
ReadableStream暴露了getReader()方法,用于产生ReadableStream-DefaultReader,这个读取器可以用于在数据到达时异步获取数块。数据流的格式是 Uint8Array。下面的代码调用了读取器的 read()方法,把最早可用的块打印了出来:
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 }
// ...
在这些例子中,当读取完 Uint8Array 块之后,浏览器会将其标记为可以被垃圾回收。对于需要在不连续的内存中连续检查大量数据的情况,这样可以节省很多内存空间。
6 Beacon API
为了把尽量多的页面信息传到服务器,很多分析工具需要在页面生命周期中尽量晚的时候向服务器发送遥测或分析数据。为解决这个问题,W3C 引入了补充性的 Beacon API。这个 API 给 navigator 对象增加了一个sendBeacon()方法。这个简单的方法接收一个 URL 和一个数据有效载荷参数,并会发送一个 POST请求。
// 发送 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
Web Socket(套接字)的目标是通过一个长时连接实现与服务器全双工、双向的通信。在 JavaScript中创建 Web Socket 时,一个 HTTP 请求会发送到服务器以初始化连接。服务器响应后,连接使用 HTTP的 Upgrade 头部从 HTTP 协议切换到 Web Socket 协议。这意味着 Web Socket 不能通过标准 HTTP 服务器实现,而必须使用支持该协议的专有服务器。要使用 ws://和 wss://。前者是不安全的连接,后者是安全连接。
使用自定义协议而非 HTTP 协议的好处是,客户端与服务器之间可以发送非常少的数据,不会对HTTP 造成任何负担。使用更小的数据包让Web Socket 非常适合带宽和延迟问题比较明显的移动应用。使用自定义协议的缺点是,定义协议的时间比定义 JavaScript API 要长。Web Socket 得到了所有主流浏览器支持。
7.1 API
let socket = new WebSocket("ws://www.example.com/server.php");
WebSocket 也有一个readyState 属性表示当前状态。不过,这个值与 XHR 中相应的值不一样。
WebSocket.OPENING(0):连接正在建立。
WebSocket.OPEN(1):连接已经建立。
WebSocket.CLOSING(2):连接正在关闭。
WebSocket.CLOSE(3):连接已经关闭。
7.2 发送和接收数据
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);
socket.onmessage = function(event) {
let data = event.data;
// 对数据执行某些操作
};
7.3 其他事件
WebSocket 对象在连接生命周期中有可能触发 3 个其他事件。
open:在连接成功建立时触发。
error:在发生错误时触发。连接无法存续。
close:在连接关闭时触发。
8 安全
/getuserinfo.php?id=23
请求这个 URL,可以假定返回 ID 为 23 的用户信息。访问者可以将 23 改为 24 或 56,甚至其他任何值。getuserinfo.php 文件必须知道访问者是否拥有访问相应数据的权限。否则,服务器就会大门敞开,泄露所有用户的信息。
在未授权系统可以访问某个资源时,可以将其视为跨站点请求伪造(CSRF,cross-site request forgery)攻击。未授权系统会按照处理请求的服务器的要求伪装自己。Ajax 应用程序,无论大小,都会受到 CSRF攻击的影响,包括无害的漏洞验证攻击和恶意的数据盗窃或数据破坏攻击。
关于安全防护 Ajax 相关 URL 的一般理论认为,需要验证请求发送者拥有对资源的访问权限。可以通过如下方式实现:
要求通过 SSL 访问能够被 Ajax 访问的资源。
要求每个请求都发送一个按约定算法计算好的令牌(token)。
注意,以下手段对防护 CSRF 攻击是无效的:
要求 POST 而非 GET 请求(很容易修改请求方法)。
使用来源 URL 验证来源(来源 URL 很容易伪造)。
基于 cookie 验证(同样很容易伪造)