第24章 网络请求和远程资源
第24章 网络请求和远程资源
Ajax,即Asynchronous JavaScript+XML的缩写。一种在不卸载网页的情况下向服务器请求数据的技术。
XHR对象的API被认为难以使用,并且自推出以来,Fetch API已成为XHR的现代化替代品。它对promises和service workers的支持使其成为一个非常强大的Web开发工具。
XMLHttpRequest是过时的JavaScript规范的产物,尽可能使用fetch()。
基础
HTTP头
每个HTTP请求和响应都会发送一组头信息,开发人员可能感兴趣也可能不感兴趣。XHR对象通过多种方法公开两种类型的头:请求头和响应头。
默认情况下,发送XHR请求时会发送以下头:
Accept 浏览器可以处理的内容类型。
Accept-Charset 浏览器可以显示的字符集。
Accept-Encoding 浏览器处理的压缩编码。
Accept-Language 浏览器运行的语言。
Connection 浏览器与服务器建立的连接类型。
Cookie 在页面上设置的任何cookie。
Host 发出请求的页面的域名。
Referer 发出请求的页面的URI。请注意,此标头在HTTP规范中拼写错误,因此出于兼容性目的必须拼写错误。(这个词的正确拼写是“referrer”。)
User-Agent 浏览器的用户代理字符串。
GET请求
GET请求是HTTP协议中最常见的请求方法之一,用于从服务器获取数据。当浏览器请求一个网页时,通常会使用GET请求来获取页面中的内容、图片、样式表等资源。
GET请求的特点:
幂等性 :GET请求是幂等的,即多次请求同一资源返回的结果应该相同。这意味着对于相同的GET请求,不应该对服务器端产生副作用。
可缓存性 :由于GET请求的幂等性,响应结果通常可以被缓存,以提高性能并减少对服务器的负载。
数据传输 :GET请求通过URL传递数据,数据会附加在URL的末尾,以
?
开头,参数之间用&
连接,例如:http://example.com/page?param1=value1¶m2=value2
。安全性 :GET请求的数据会暴露在URL中,因此不适合传输敏感信息,如密码等。
GET请求的工作流程:
客户端(如浏览器)向服务器发送GET请求,请求中包含要访问的资源的URL。
服务器接收到GET请求后,根据请求中的URL找到对应的资源,并将资源作为响应返回给客户端。
客户端接收到服务器的响应后,解析响应内容,并将其展示给用户。
GET请求的示例:
假设要获取一个名为example.html
的网页,可以通过以下GET请求来实现:
GET /example.html HTTP/1.1
Host: www.example.com
GET请求的优缺点:
优点 :
- 简单易用,适合获取静态资源。
- 可以被缓存,提高性能。
- 可以被书签保存和分享,方便用户访问特定页面。
缺点 :
- 传输数据量有限,受URL长度限制。
- 不适合传输敏感信息。
- 不适合提交大量数据,如表单提交。
总的来说,GET请求是HTTP中常用的请求方法,适合用于获取非敏感、静态的资源,以及需要被缓存的情况。
POST请求
POST 请求是一种在万维网上请求服务器接受并存储数据的一种方法。在 HTTP 协议中,POST 方法被用来向服务器提交数据,这些数据通常是通过表单填写的。与 GET 请求不同,POST 请求将数据放在请求的主体部分,而不是放在 URL 中。
以下是 POST 请求的一些关键特点和用法:
数据传输方式 :
- POST 请求将数据放在请求的主体部分,而不是像 GET 请求一样放在 URL 中。这意味着 POST 请求可以传输更多数据,并且更安全,因为数据不会像在 URL 中那样暴露给第三方。
数据长度限制 :
- POST 请求通常没有严格的数据长度限制,这使得它适合传输大量数据,如上传文件或表单提交。
安全性 :
- POST 请求比 GET 请求更安全,因为用户提交的数据不会直接暴露在 URL 中,而是放在请求主体中。这可以减少敏感数据被恶意拦截的风险。
幂等性 :
- POST 请求不是幂等的,这意味着多次发送相同的 POST 请求可能会导致不同的结果。这与 GET 请求不同,GET 请求是幂等的,多次发送相同的 GET 请求会产生相同的结果。
用途 :
- POST 请求通常用于提交表单数据、上传文件、创建资源等需要向服务器发送数据的操作。
示例 :
- 下面是一个简单的使用 POST 请求的示例:在这个示例中,客户端向服务器发送了一个包含名字和邮箱的表单数据,服务器将这些数据存储或处理后返回相应的结果。
POST /submit_form HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded name=John&[email protected]
- 下面是一个简单的使用 POST 请求的示例:
FormData类型
FormData
类型是 JavaScript 中用于创建表单数据对象的接口,常用于通过 AJAX 请求将表单数据发送到服务器。使用 FormData
对象,可以方便地构建键值对形式的数据集,用于发送到服务器端。
以下是关于 FormData
类型的详细介绍:
创建 FormData
对象
可以通过以下方式创建一个空的 FormData
对象:
const formData = new FormData();
向 FormData
对象添加数据
- 使用
append()
方法添加数据: 可以使用append()
方法向FormData
对象添加键值对数据。例如:
formData.append('username', 'john_doe');
formData.append('email', '[email protected]');
- 通过表单元素创建
FormData
对象: 可以通过将一个表单元素传递给FormData
构造函数来创建FormData
对象,这将自动包含表单中的所有字段。
const formElement = document.getElementById('myForm');
const formData = new FormData(formElement);
发送 FormData
对象
可以使用 AJAX 请求(如 fetch
或 XMLHttpRequest
)将 FormData
对象发送到服务器。例如,使用 fetch
发送 FormData
对象:
fetch('submit_form.php', {
method: 'POST',
body: formData
})
.then(response => {
// 处理响应
})
.catch(error => {
// 处理错误
});
文件上传
FormData
对象也可以用于上传文件。通过将文件对象添加到 FormData
中,可以将文件作为表单数据发送到服务器。
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
formData.append('file', file);
获取 FormData
中的数据
可以使用 get()
方法从 FormData
对象中获取特定字段的值,也可以使用 entries()
、keys()
和 values()
方法来迭代 FormData
对象中的键值对。
for (const pair of formData.entries()) {
console.log(pair[0] + ', ' + pair[1]);
}
注意事项
FormData
对象可以处理文本数据和文件数据。- 当使用
FormData
对象发送数据时,请求的Content-Type
头会自动设置为multipart/form-data
,适用于发送表单数据。 FormData
对象通常用于 POST 请求,以便将数据发送到服务器。
使用 FormData
对象可以简化在前端处理表单数据并将其发送到服务器的过程,特别是在需要上传文件或发送复杂数据时非常有用。
跨域资源共享
跨域资源共享(Cross-Origin Resource Sharing,CORS)是一种用于在浏览器和服务器之间进行跨域通信的机制。在Web开发中,由于同源策略的限制,浏览器通常会阻止从一个源加载的网页去请求另一个源的资源。同源策略是浏览器的一种安全特性,用于防止恶意网站通过脚本访问用户的敏感数据。
CORS允许服务器指定哪些源(域、协议、端口的组合)可以访问其资源。这样,即使请求发起源与资源所在源不同,只要服务器允许跨域访问,浏览器就会允许这种跨域请求。
以下是一些关键概念和工作原理:
同源策略(Same-Origin Policy) :同源策略是浏览器的一项安全功能,限制一个源(域、协议、端口的组合)的文档或脚本与另一个源进行交互。这意味着如果两个资源的协议、域名或端口有任何一个不同,就被认为是跨域请求。
简单请求(Simple Request) :对于特定类型的请求(比如GET、POST以及部分HEAD请求),浏览器会执行一个预检请求(preflight request)来确认服务器是否允许实际的请求。如果服务器返回正确的响应头,浏览器才会继续发送实际的请求。
预检请求(Preflight Request) :当请求为非简单请求时(比如使用自定义头部、使用PUT、DELETE等方法时),浏览器会先发送一个OPTIONS方法的预检请求到服务器,以获取服务器允许的方法、头部等信息。
CORS头(CORS Headers) :服务器通过设置HTTP响应头来指示浏览器是否允许跨域请求。常用的CORS头包括
Access-Control-Allow-Origin
(指定允许访问的源)、Access-Control-Allow-Methods
(指定允许的HTTP方法)、Access-Control-Allow-Headers
(指定允许的请求头)、Access-Control-Allow-Credentials
(指定是否允许发送凭据信息)等。实现方法 :要启用CORS,服务器端需要在响应中添加相应的CORS头。通常可以通过在服务器端代码中设置这些响应头或者通过服务器配置来实现。前端开发人员也可以通过XMLHttpRequest对象或Fetch API来发送跨域请求,并根据服务器返回的响应头进行处理。
简单请求
在网络通信中,简单请求(Simple Request)是指符合一定条件的跨域请求。根据同源策略(Same-Origin Policy),浏览器限制了从一个源(域名、协议、端口)向另一个源发起的跨域请求。简单请求是指符合以下条件的跨域请求:
使用以下方法之一:
- GET
- HEAD
- POST
Content-Type 头部只能是以下三种之一:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
请求中的头部信息限制在以下几种常见的字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于上述三种值
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
如果一个跨域请求符合以上条件,那么浏览器会将其视为简单请求,直接发起跨域请求。否则,就会被视为复杂请求,需要进行预检(Preflight Request)以获得服务器的允许。
预检请求
CORS通过一种称为预检请求(preflighted request)的服务器验证机制,允许使用自定义头、除GET和POST之外的方法,以及不同主体内容类型。当尝试使用其中一个高级选项发出请求时,会向服务器发出“预检”请求。此请求使用OPTIONS方法并发送以下头:
- Origin : 与简单请求相同。
- Access-Control-Request-Method : 请求想要使用的方法。
- Access-Control-Request-Headers : (可选)正在使用的自定义头的逗号分隔的列表。
以下是一个假设POST请求带有名为NCZ的自定义头的示例:
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://example.com/api', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('NCZ', 'custom-value');
xhr.send();
在此请求期间,服务器可以确定是否允许此类请求。服务器通过在响应中发送以下头将其传达给浏览器:
- Access-Control-Allow-Origin : 与简单请求相同。
- Access-Control-Allow-Methods : 以逗号分隔的允许的方法的列表。
- Access-Control-Allow-Headers : 以逗号分隔的服务器允许的头列表。
- Access-Control-Max-Age : 缓存此预检请求的时间(以秒为单位)。
例如,在服务器端可以这样设置响应头:
res.setHeader('Access-Control-Allow-Origin', 'https://example.com');
res.setHeader('Access-Control-Allow-Methods', 'POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, NCZ');
res.setHeader('Access-Control-Max-Age', '3600');
一旦发出预检请求,结果将在响应中指定的时间段内缓存;只会在第一次发出这种类型的请求时产生额外的HTTP请求成本。
认证请求
默认情况下,跨域请求不提供凭据(cookie、HTTP身份验证和客户端SSL证书)。可以通过将 withCredentials
属性设置为 true
来指定请求应发送凭据。如果服务器允许凭据请求,则它会使用以下HTTP头进行响应:
如果发送了有凭据的请求并且此头未作为响应的一部分发送,则浏览器不会将响应传递给JavaScript(responseText
是一个空字符串,status
为 0,onerror()
被调用)。请注意,服务器还可以将此HTTP头作为预检响应的一部分发送,以指示允许源发送凭据请求。
示例代码:
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.withCredentials = true;
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
// 请求成功
console.log(xhr.responseText);
} else {
// 请求失败
console.log('请求失败');
}
};
xhr.send();
在上面的示例中,我们使用 XMLHttpRequest
对象发送了一个带有凭据的 GET 请求,并在服务器端设置了允许跨域请求发送凭据。
跨域技术
- CORS(Cross-Origin Resource Sharing) CORS是一种官方推荐的跨域解决方案。服务器设置相应的HTTP响应头,允许特定的外域请求资源。
前端示例代码 :
// 使用XMLHttpRequest发送CORS请求
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://example.com/api/data', true);
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 400) {
// 请求成功
var data = JSON.parse(xhr.responseText);
console.log(data);
} else {
// 服务器达到请求,但返回错误状态码
}
};
xhr.onerror = function() {
// 发生网络错误
};
xhr.send();
后端示例代码 (以Node.js为例):
JavaScript
// 使用Express.js设置CORS
const express = require('express');
const app = express();
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*'); // 允许所有域名跨域
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next();
});
app.get('/api/data', (req, res) => {
res.json({ data: 'Hello World' });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
- JSONP(JSON with Padding) JSONP是一种老旧的跨域技术,通过
<script>
标签的src属性绕过同源策略。
前端示例代码 :
HTML
<!-- HTML中动态创建script标签 -->
<script>
function jsonpCallback(data) {
console.log(data);
}
var script = document.createElement('script');
script.src = 'http://example.com/api/data?callback=jsonpCallback';
document.body.appendChild(script);
</script>
后端示例代码 (以PHP为例):
PHP
<?php
// 服务器端响应JSONP请求
$callback = $_GET['callback'];
$data = array('data' => 'Hello World');
echo $callback . '(' . json_encode($data) . ')';
?>
- 服务器代理 通过在同源服务器上设置代理,将请求转发到目标服务器,从而绕过浏览器的同源策略。
前端示例代码 :
JavaScript
// 与本地代理服务器通信
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true); // 请求本地代理服务器
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 400) {
// 请求成功
var data = JSON.parse(xhr.responseText);
console.log(data);
} else {
// 服务器达到请求,但返回错误状态码
}
};
xhr.onerror = function() {
// 发生网络错误
};
xhr.send();
后端示例代码 (以Node.js为例):
JavaScript
// 本地代理服务器
const express = require('express');
const app = express();
app.get('/api/data', (req, res) => {
// 转发请求到目标服务器
// ...
res.json({ data: 'Hello World' });
});
app.listen(3000, () => {
console.log('Proxy server running on port 3000');
});
- WebSocket WebSocket是一种全双工通信协议,可以在不同域之间建立持久连接。
前端示例代码 :
JavaScript
// 建立WebSocket连接
var socket = new WebSocket('ws://example.com/socket');
socket.onopen = function(event) {
console.log('WebSocket连接已建立');
// 发送数据
socket.send('Hello from client');
};
socket.onmessage = function(event) {
console.log('接收到服务器消息:', event.data);
};
socket.onclose = function(event) {
console.log('WebSocket连接已关闭');
};
后端示例代码 (以Node.js为例):
JavaScript
// WebSocket服务器
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
console.log('WebSocket连接已建立');
// 接收客户端消息
ws.on('message', (message) => {
console.log('接收到客户端消息:', message);
// 处理消息并发送响应
// ...
ws.send('Hello from server');
});
});
FETCH API
Fetch API可以执行与XMLHttpRequest对象相同的所有任务,但更易于使用,具有更现代的接口,并且能够被现代Web工具(如Web Workers)使用。XMLHttpRequest的异步是可选的,但Fetch API发送的所有请求都是严格异步的。
Fetch API本身是用于在JavaScript中请求资源的优秀工具,但该API在服务工作者领域也很重要,因为它提供了一个接口,用于拦截、重定向和更改通过fetch()发出的请求。
基础API用法
fetch()方法可在任何全局作用域内使用,包括主页面执行、模块和内部worker。调用它将指示浏览器向提供的URL发送请求。
发送请求
fetch()
方法接受一个参数input
,通常是你想要获取资源的URL。这个方法返回一个Promise对象,这意味着你可以使用.then()
和.catch()
来处理响应或捕获错误。
// 发送GET请求
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error('网络响应错误');
}
return response.json(); // 解析JSON数据
})
.then(data => {
console.log(data); // 处理数据
})
.catch(error => {
console.error('获取数据失败:', error);
});
// 发送POST请求
fetch('https://api.example.com/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
key: 'value'
})
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应错误');
}
return response.json();
})
.then(data => {
console.log('数据提交成功:', data);
})
.catch(error => {
console.error('提交数据失败:', error);
});
在上面的代码中,我们看到了如何使用fetch()
方法发送GET和POST请求。对于GET请求,我们直接传递了URL。对于POST请求,我们还需要提供一个配置对象,其中包括请求方法、请求头和请求体。
当请求完成并且资源可用时,Promise将解决为一个Response对象。这个Response对象封装了获取的资源,并提供了许多属性和方法来处理这些资源。例如,.json()
方法可以将响应体解析为JSON对象。
读取响应
读取响应内容的最简单方法是使用text()方法。此方法返回一个promise:
fetch('https://api.example.com/data')
.then(response => response.text())
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Error:', error);
});
处理状态码和请求失败
Fetch API允许检查Response对象的状态码和状态文本,可分别通过status和statusText属性访问。
成功获取资源通常会产生响应码 200:
fetch('https://api.example.com/data')
.then(response => {
if (response.ok) {
console.log('Request successful');
} else {
console.log('Request failed with status:', response.status);
}
})
.catch(error => {
console.error('Error:', error);
});
请求一个不存在的资源通常会产生一个响应码 404:
fetch('https://api.example.com/nonexistent')
.then(response => {
if (response.ok) {
console.log('Request successful');
} else {
console.log('Request failed with status:', response.status);
}
})
.catch(error => {
console.error('Error:', error);
});
请求一个抛出服务器错误的资源URL通常会产生一个响应码 500:
fetch('https://api.example.com/error')
.then(response => {
if (response.ok) {
console.log('Request successful');
} else {
console.log('Request failed with status:', response.status);
}
})
.catch(error => {
console.error('Error:', error);
});
fetch()与重定向相关的行为可以显式设置(本章稍后详述),但默认行为是跟随重定向并返回响应码不为300-399的响应。执行fetch时,响应对象上的redirected属性设置为true,但响应码仍为200。
在所有这些示例中,请注意fetch返回的promise的resolved handler正在执行,即使请求可能被视为失败,例如状态500。如果服务器发送任何类型的响应,fetch()的promise将变成已解决。这种行为是有道理的:系统级网络协议已经成功完成了一次往返消息传输。什么是“成功”的请求应该在如何处理响应中定义。
通常,响应码200被认为是成功的,其他任何事情都被认为是不成功的。为了区分这些,Response对象的ok属性会标识响应代码在200-299之间:
fetch('https://api.example.com/data')
.then(response => {
if (response.ok) {
console.log('Request successful');
} else {
console.log('Request failed with status:', response.status);
}
})
.catch(error => {
console.error('Error:', error);
});
真正的fetch()失败,如浏览器超时,没有服务器响应等将拒绝:
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error('Request failed');
}
return response.json();
})
.catch(error => {
console.error('Error:', error);
});
请求promise拒绝的原因包括CORS违规、缺乏网络连接、HTTPS违规和其他浏览器/网络策略违规。
当使用url属性发送请求时可以通过fetch()检查整个URL:
fetch('https://api.example.com/data', {
method: 'GET'
})
.then(response => response.json())
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Error:', error);
});
自定义fetch选项
当仅与URL一起使用时,fetch()将使用最少的请求头发送GET请求 。要配置请求的发送方式,可以将init对象作为可选的第二个参数传递给fetch()。init对象应填充下表中的任意数量的键和相应的值:
cache
用于控制浏览器在执行fetch时如何与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(网关超时) 的响应。
默认为 default
。
credentials
用于指定是否以及如何将cookie包含在传出(outgoing)请求中。这类似于XMLHttpRequest withCredentials标志。必须是以下字符串值之一:
omit
: 不发送cookie。same-orgin
: 仅当请求URL的域与执行fetch脚本的域匹配时才发送cookie。include
: Cookie包含在同域和跨域请求中。
也可以是支持Credential Management API的浏览器中的FederatedCredential实例或PasswordCredential实例。默认为same-origin
。
heders
用于指定请求头。必须是Headers对象实例或包含键值字符串对头的常规对象实例。默认为没有键值对的Headers对象。这并不意味着请求将在没有头的情况下发送;当请求正式发送时,浏览器可能仍会添加头。这种差异对JavaScript来说是不可见的,但仍然可以在浏览器的网络检视器中观察到。
integrity
属性用于强制执行子资源完整性。它必须是包含子资源完整性标识符的字符串。如果未提供完整性标识符,则应该将其设置为 null
而不是空字符串。
keepalive
用于指示浏览器允许请求存在于页面生命周期之外。这对于在发送 fetch
后不久可能发生页面卸载时向服务器报告事件或分析指标非常有用。带有 keepalive
标志的 fetch
可以用作 Navigator.sendBeacon()
的替代品。keepalive
的值必须是布尔值,默认为 false
。
method
用于指定请求的HTTP方法。几乎总是以下字符串值之一:
- GET
- POST
- PUT
- PATCH
- DELETE
- HEAD
- OPTIONS
- CONNECT
- TRACE
默认为GET。
mode
用于指定请求的模式。该模式确定来自跨域请求的响应是否有效以及客户端可以读取多少响应。违反指定模式的请求将引发错误。必须是以下字符串值之一:
- cors: 允许符合CORS协议的跨域请求。响应将是“CORS过滤的响应”,这意味着响应中可访问的头由浏览器强制白名单过滤。
- no-cors: 允许不需要预检请求的跨域请求(仅具有CORS-safelisted请求头的 HEAD、GET 和 POST)。响应类型将是不透明的,这意味着无法读取响应的内容。
- same-origin: 不允许任何类型的跨域请求。
- navigate: 旨在支持 HTML 导航,仅在文档之间导航时创建。可能永远不需要使用此模式。
当通过其构造函数手动创建请求实例时,默认为 cors。否则,默认为 no-cors。
redirect
用于指定应如何处理重定向的响应(定义为301、302、303、307或308的响应状态码)。必须是以下字符串值之一:
follow
: 重定向的请求将被跟踪,并且具有非重定向响应的最终 URL 将作为最终响应返回。error
: 重定向的请求将引发错误。manual
: 重定向的请求不会跟随重定向,而是返回类型为opaqueredirect
的响应,同时仍然公开预期的重定向 URL。这允许手动跟踪重定向。
默认为 follow
。
referrer
用于指定应作为 HTTP Referer 头发送的内容。必须是以下字符串值之一:
no-referrer
: 发送no-referrer
作为 HTTP Referer 值。client/about:client
: 发送当前 URL 或no-referrer
(由 referrer 策略确定)作为实际的 HTTP Referer 值。<URL>
: 欺骗作为 HTTP Referer 发送的 URL。欺骗 URL 的来源必须与执行脚本的来源相匹配。
默认为 client/about:client
。
referrer-Policy
用于指定 HTTP Referer 头。必须是以下字符串值之一:
no-referrer
: 从请求中完全删除 Referer 头。no-referrer-when-downgrade
: 对于从安全 HTTPS 环境发送到 HTTP URL 的请求,Referer 头被省略。对于其他请求,Referer 头设置为完整 URL。origin
: 对于所有请求,Referer 头仅设置为 origin。same-origin
: 对于跨域请求,Referer 头被省略。对于同源请求,Referer 头设置为完整 URL。strict-origin
: 对于从安全 HTTPS 环境发送到 HTTP URL 的请求,Referer 头被省略。对于其他请求,Referer 头仅设置为 origin。strict-origin-when-cross-origin
: 对于从安全 HTTPS 环境发送到 HTTP URL 的跨域请求,Referer 头被省略。对于同源请求,Referer 头设置为完整 URL。对于所有其他跨域请求,Referer 头仅设置为 origin。unsafe-url
: 对于所有请求,Referer 头设置为完整 URL。
默认情况下,referrer-Policy
的值为 no-referrer-when-downgrade
。
signal
signal
用于启用通过关联的 AbortController
中止正在进行的 fetch
请求的能力。必须是 AbortSignal
实例。默认情况下,它是未关联的 AbortSignal
实例。
缓存命中(cache hit):当应用程序或软件请求数据时,会首先发生缓存命中。首先,中央处理单元(CPU)在其最近的内存位置(通常是主缓存)中查找数据。如果在缓存中找到请求的数据,则将其视为缓存命中。
常见的Fetch模式
与XMLHttpRequest一样,fetch()既用于获取数据,也用于发送数据。使用init
对象,可以将fetch()配置为在请求主体中发送各种可序列化的数据类型。
发送JSON数据
一个简单的JSON字符串可以通过以下示例发送到服务器:
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ key: 'value' })
})
在请求主体中发送参数
由于请求主体支持任何字符串值,因此将参数作为序列化的主体字符串发送也很容易:
fetch(url, {
method: 'POST',
body: 'param1=value1¶m2=value2'
})
发送文件
因为主体支持FormData实例,所以fetch()会很高兴地序列化并发送从文件选择器表单输入中提取的文件:
const formData = new FormData();
formData.append('file', fileInputElement.files[0]);
fetch(url, {
method: 'POST',
body: formData
})
这样的fetch()实现也可以支持多个文件:
const formData = new FormData();
formData.append('file1', file1InputElement.files[0]);
formData.append('file2', file2InputElement.files[0]);
fetch(url, {
method: 'POST',
body: formData
})
将文件加载为Blob
Fetch API能够以Blob的形式提供响应,从而与多个浏览器API兼容。一种常见的表现形式是将图像文件显示加载到内存中并将其添加到HTML图像元素。这样,响应对象公开一个blob()
方法,该方法返回一个解决为Blob实例的promise。这反过来可以传递给URL.createObjectUrl()
为图像元素的src属性生成有效值:
fetch(url)
.then(response => response.blob())
.then(blob => {
const imageUrl = URL.createObjectURL(blob);
// 将imageUrl赋值给img元素的src属性
});
发送跨域请求
请求来自不同域的资源需要响应具有CORS头以供浏览器接受。如果没有,跨域请求将失败并抛出错误:
fetch(url, {
mode: 'cors'
})
.then(response => {
// 处理响应
})
.catch(error => {
console.error('Fetch error:', error);
});
如果代码不需要访问响应,则可以发送no-cors
的fetch。在这种情况下,响应的type
属性将是opaque
,因此会阻止检查它。此策略可用于发送pings或仅可缓存响应以供以后使用的情况:
fetch(url, {
mode: 'no-cors'
})
.then(() => {
// 请求成功,不需要访问响应
})
.catch(error => {
console.error('Fetch error:', error);
});
中断请求
Fetch API支持通过AbortController
的signal/abort
对中止请求。调用AbortController.abort()
终止所有网络传输,因此当停止传输大负载时非常有用。中止运行中的fetch()将导致它因错误而被拒绝:
const controller = new AbortController();
fetch(url, {
signal: controller.signal
})
.then(response => {
// 处理响应
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('Request aborted');
} else {
console.error('Fetch error:', error);
}
});
// 中止请求
controller.abort();
以上是关于Fetch API的常见用法示例,希望对您有所帮助。如果您有任何疑问或需要进一步的帮助,请随时告诉我。
Headers 对象
Headers 对象用作所有传出请求和传入响应头的容器。每个传出 Request 实例都包含一个可通过 Request.prototype.headers
访问的空 Headers 实例。每个传入的 Response 实例都包含一个通过 Response.prototype.headers
访问的填充的 Headers 实例,这两个都是可变属性。还可以通过 new Headers()
构造函数创建一个新实例。
探索 Headers 与 Map 相似性
Headers 对象与 Map 对象的重叠度很高,因为 HTTP 头本质上是序列化的键值对。Headers 和 Map 类型共享许多实例方法:get()
、set()
、has()
和 delete()
,如下所示:
let h = new Headers();
let m = new Map();
// Set key
h.set('foo', 'bar');
m.set('foo', 'bar');
// Check for key
console.log(h.has('foo')); // true
console.log(m.has('foo')); // true
console.log(h.has('qux')); // false
console.log(m.has('qux')); // false
// Get value
console.log(h.get('foo')); // bar
console.log(m.get('foo')); // bar
// Replace value
h.set('foo', 'baz');
m.set('foo', 'baz');
// Get replaced value
console.log(h.get('foo')); // baz
console.log(m.get('foo')); // baz
// Remove value
h.delete('foo');
m.delete('foo');
// Check that value is removed
console.log(h.get('foo')); // undefined
console.log(m.get('foo')); // undefined
这些类型都可以用可迭代对象初始化,如下所示:
let seed = [['foo', 'bar']];
let h = new Headers(seed);
let m = new Map(seed);
console.log(h.get('foo')); // bar
console.log(m.get('foo')); // bar
它们还有相同的功能:keys()
, values()
, entries()
和迭代接口:
let seed = [['foo', 'bar'], ['baz', 'qux']];
let h = new Headers(seed);
let m = new Map(seed);
console.log(...h.keys()); // foo, baz
console.log(...m.keys()); // foo, baz
console.log(...h.values()); // bar, qux
console.log(...m.values()); // bar, qux
console.log(...h.entries()); // ['foo', 'bar'], ['baz', 'qux']
console.log(...m.entries()); // ['foo', 'bar'], ['baz', 'qux']
Headers 对象的独有功能
Headers 对象不是 Map 的复制品。初始化时,一个 Headers 对象可以用有键值对的对象来初始化,而一个 Map 不能:
let seed = {
foo: 'bar',
a: "666"
};
let h = new Headers(seed);
console.log(h.get('foo')); // bar
console.log(h.get('a')); //666
// TypeError: object is not iterable
可以使用 append()
方法为单个 HTTP 头分配多个值;当与 Headers 实例中尚不存在的头一起使用时,append()
的行为与 set()
方法相同。后续使用将连接以逗号分隔的头值:
let h = new Headers();
h.append('foo', 'bar');
console.log(h.get('foo')); // "bar"
h.append('foo', 'baz');
console.log(h.get('foo')); // "bar, baz"
h.append('a','666'); // 同set()方法
console.log(h.get('a'));
Header 保护
并非所有 HTTP 头都可以由客户端更改,这需要 Headers 对象启用保护(guards)来执行。不同的保护设置将改变 set()
、append()
和 delete()
的行为方式。违反保护限制将引发 TypeError
。
Headers 实例将根据其出处表现不同,这种行为是由 guards 管理的。在 JavaScript 中无法确定 Headers 实例的保护设置。下表描述了各种可能的保护设置以及每种设置的行为含义。
保护 | 应用场景 | 限制 |
---|---|---|
none | 当Headers实例通过其构造函数创建时激活。 | 无 |
request | 当Request对象通过其构造函数以任何非no-cors模式实例化时激活。 | 不允许修改具有禁止头名称的头 |
request-no-cors | 当Request对象通过其构造函数以no-cors模式实例化时激活。 | 不允许修改不是简单头的头 |
cors | 当Response对象通过其构造函数实例化时激活。 | 不允许修改带有禁止响应头名称的头 |
immutable | 当Response对象通过error()或redirect() 静态方法实例化时激活。 | 不允许修改头 |
Request对象
顾名思义,Request对象是对获取的资源的请求的接口。此接口公开有关请求性质的信息以及使用请求体的不同方式。
创建Request对象
Request对象可以通过构造函数实例化。它需要一个输入参数,通常是一个URL:
const myRequest = new Request('https://bilibili.com/data');
Request构造函数还接受第二个可选参数,一个init对象。该init对象与fetch()的init对象相同。未在init中指定的值将在Request实例中分配其默认值:
//使用默认值
console.log(new Request(''));
// bodyUsed: false
// cache: "default"
// credentials: "same-origin"
// destination: ""
// headers: Headers {}
// integrity: ""
// isHistoryNavigation: false
// keepalive: false
// method: "GET"
// mode: "cors"
// redirect: "follow"
// referrer: "about:client"
// referrerPolicy: ""
// signal: AbortSignal {aborted: false, onabort: null}
// url: "http://127.0.0.1:8848/WWW/PRO%20Javascript/new_file.html"
// __proto__: Request
//使用init值:
console.log(new Request('https://foo.com', {method: 'POST'}));
// bodyUsed: false
// cache: "default"
// credentials: "same-origin"
// destination: ""
// headers: Headers {}
// integrity: ""
// isHistoryNavigation: false
// keepalive: false
// method: "POST"
// mode: "cors"
// redirect: "follow"
// referrer: "about:client"
// referrerPolicy: ""
// signal: AbortSignal {aborted: false, onabort: null}
// url: "https://foo.com/"
// __proto__: Request
克隆Request对象
Fetch API提供了两种不同的方法来复制Request对象:使用Request构造函数和使用clone()方法。
将Request实例作为输入参数传递给Request构造函数将复制该请求:
let r1 = new Request('https://foo.com');
let r2 = new Request(r1);
console.log(r2.url); // https://foo.com/
init对象内的值将覆盖源对象的值:
let r1 = new Request('https://foo.com');
let r2 = new Request(r1, {method: 'POST'});
console.log(r1.method); // GET
console.log(r2.method); // POST
这种复制策略不会总是产生精确的副本。最值得注意的是,它会将第一个请求主体标记为已用:
let r1 = new Request('https://foo.com',{ method: 'POST', body: 'foobar' });
let r2 = new Request(r1);
console.log(r1.bodyUsed); // true
console.log(r2.bodyUsed); // false
如果源对象与创建的新对象位于不同的域,则referrer属性将被清除。此外,如果源对象的mode值为 navigate,它将被转换为 same-origin。
克隆Request对象的第二种方法是使用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为false,则上面两种方法都不能克隆请求,这意味着主体尚未读取。读取主体后,尝试克隆将引发TypeError。
let r = new Request('https://foo.com');
r.clone();
new Request(r); //正常
r.text(); //设置bodyUsed字段为false
r.clone(); //异常
new Request(r); //异常
通过fetch()使用请求对象
fetch()和Request构造函数具有相同的函数签名这一事实并非偶然。调用fetch()时,可以传递一个已经创建的Request实例而不是URL。与Request构造函数一样,fetch()的init对象中提供的值将覆盖提供的请求的值:
fetch(myRequest)
.then(response => {
// Handle response
})
.catch(error => {
// Handle error
});
与克隆请求一样,无法使用具有使用过的主体的请求来发送fetch:
let r = new Request('https://foo.com',{ method: 'POST', body: 'foobar' });
r.text();
fetch(r);//异常
重要的是,在fetch中使用Request还会将主体标记为已使用。因此,对于具有主体的请求只能执行一次fetch。(不包含主体的请求不受此限制):
let r = new Request('https://foo.com',{ method: 'POST', body: 'foobar' });
fetch(r);
fetch(r);//异常
使用fetch()调用包含主体的相同请求对象多次时,必须为第一个fetch()中的请求调用clone():
let r = new Request('https://foo.com',{ method: 'POST', body: 'foobar' });
fetch(r.clone());
fetch(r.clone());
fetch(r);
Response对象
顾名思义,Response对象是对所获取资源的响应的接口。此接口公开有关响应的信息以及使用响应主体的不同方式。
创建响应对象
可以通过构造函数实例化Response对象。不使用参数时,它的属性将使用默认值填充,因为此实例不代表实际的HTTP响应:
let r = new Response();
console.log(r);
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: true
// redirected: false
// status: 200
// statusText: ""
// type: "default"
// url: ""
// __proto__: Response
// }
Response构造函数接受第一个可选参数,即body。该body可以为null,与init的body相同。第二个可选参数是init对象,应该用下表中的任意数量的键和相应的值填充。
键 | 值 |
---|---|
headers | 必须是Headers对象实例或包含头键值字符串对的常规对象实例。 |
status | 指示HTTP响应状态码的整数。默认为200。 |
statusText | 描述HTTP响应状态的字符串。默认为空字符串。 |
body和init可用于构建响应,如下所示:
let r = new Response('foobar', {
status: 418,
statusText: 'I\'m a teapot'
});
console.log(r);
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: false
// redirected: false
// status: 418
// statusText: "I'm a teapot"
// type: "default"
// url: ""
// __proto__: Response
// }
对于大多数应用程序,生成Response对象的最常见方法是调用fetch();这将返回一个promise,该promise解决为代表实际HTTP响应的Response对象。参考如下伪代码:
fetch('https://foo.com')
.then((response) => {
console.log(response);
});
Response类还具有两个用于生成Response对象的静态方法,即Response.redirect()和Response.error()。Response.redirect()接受一个URL和重定向状态码(301、302、303、307或308)并返回一个重定向的Response对象:
console.log(Response.redirect('https://foo.com', 301));
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: false
// redirected: false
// status: 301
// statusText: ""
// type: "default"
// url: ""
// }
提供的状态码必须符合重定向条件,否则抛出错误:
Response.redirect('https://foo.com', 200);
// RangeError: Failed to execute 'redirect' on 'Response': Invalid status code
也可以使用Response.error()。此静态方法会产生期望从网络错误中得到的响应,这会导致fetch()返回的promise变成已拒绝:
console.log(Response.error());
// Response {
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: false
// redirected: false
// status: 0
// statusText: ""
// type: "error"
// url: ""
// __proto__: Response
// }
读取响应状态信息
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。 |
下面是返回200、302、404和500状态码的典型响应的伪代码示例:
fetch('//foo.com').then(console.log);
// Response {
// body: (...),
// bodyUsed: false,
// headers: Headers {},
// ok: true,
// redirected: false,
// status: 200,
// statusText: "OK",
// type: "basic",
// url: "https://foo.com/"
// }
fetch('//foo.com/redirect-me').then(console.log);
// Response {
// body: (...),
// bodyUsed: false,
// headers: Headers {},
// ok: true,
// redirected: true,
// status: 200,
// statusText: "OK",
// type: "basic",
// url: "https://foo.com/redirected-url/"
// }
fetch('//foo.com/does-not-exist').then(console.log);
// Response {
// body: (...),
// bodyUsed: false,
// headers: Headers {},
// ok: false,
// redirected: true,
// status: 404,
// statusText: "Not Found",
// type: "basic",
// url: "https://foo.com/does-not-exist/"
// }
fetch('//foo.com/throws-error').then(console.log);
// Response {
// body: (...),
// bodyUsed: false,
// headers: Headers {},
// ok: false,
// redirected: true,
// status: 500,
// statusText: "Internal Server Error",
// type: "basic",
// url: "https://foo.com/throws-error/"
// }
克隆响应对克隆响应对象
克隆 Response 对象的主要方法是使用 clone()
方法,它创建一个精确的副本,没有机会覆盖任何值。这不会将任何响应的主体标记为已使用:
let r1 = new Response('foobar');
let r2 = r1.clone();
console.log(r1.bodyUsed); // false
console.log(r2.bodyUsed); // false
如果响应属性 bodyUsed
为 true
,则不允许克隆 Response,这意味着尚未读取主体。读取主体后,尝试克隆将引发 TypeError
:
let r = new Response('foobar');
console.log(r.bodyUsed); // false
r.clone();
console.log(r.bodyUsed); // false
// No error
r.text(); // 设置 bodyUsed 为 true
console.log(r.bodyUsed); // true
r.clone();
// TypeError: Failed to execute 'clone' on 'Response': Response body is already used
只有具有主体的 Response 才能执行主体读取(不包含主体的响应不受此限制)。
为了读取包含主体的相同 Response 对象的主体多次,必须在执行第一次读取之前调用 clone()
:
let r = new Response('foobar');
r.clone().text().then(console.log); // foobar
r.clone().text().then(console.log); // foobar
r.text().then(console.log); // foobar
也可以通过使用原响应的主体创建新的 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
请求、响应和Body mixin
在面向对象的编程语言中,mixin 是一个包含供其他类使用的方法的类,而不必是这些类的父类。Request
和 Response
都包含 Fetch API 的 Body
mixin。这个 mixin 赋予每种类型一个只读 body
(实现为 ReadableStream
),一个只读 bodyUsed
,指示是否读取了 body 流,以及一些读取流并将结果转换为特定 JavaScript 对象类型的方法。
通常,将 Request
或 Response
主体作为流使用有两个主要原因:一是负载的大小导致网络延迟,二是流 API 本身对于处理负载非常有用。在其他情况下,当一次性消耗掉获取的资源的主体时会非常有用。
Body
mixin 提供了五种不同的方法,它们将 ReadableStream
刷新到内存中的单个缓冲区中,将缓冲区转换为特定的 JavaScript 对象类型,并在 promise 中生成它。此 promise 将等到 body 流报告已完成并且在解决之前解析缓冲区。这意味着必须等待获取的资源在客户端上完全加载,然后才能访问其内容。
Body.text()
Body.text()
方法返回一个 promise,并解决为一个 USVString
对象。以下展示了 Body.text()
与 Response
对象的使用:
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
Body.json()
Body.json()
方法返回一个 promise,并将缓冲区数据解码为 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'}
Body.formData()
Body
对象中的 formData()
方法将 Response
对象中所携带的数据流读取并封装为一个对象,该方法将返回一个 Promise 对象,该对象将产生一个 FormData
对象。浏览器能够将 FormData
对象序列化/反序列化为主体。以下展示了 FormData
实例:
let myFormData = new FormData();
myFormData.append('foo', 'bar');
使用 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
Body.arrayBuffer()
可能会需要检查和修改 body 负载为原始二进制文件。对于这样的任务,可以使用 Body.arrayBuffer()
将主体转换为 ArrayBuffer
实例。此方法返回一个 promise,并将缓冲区暴露为 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]
Body.blob()
可能会需要将 body 负载用作原始二进制文件,而无需检查或修改。对于这样的任务,可以使用 Body.blob()
将 body 用作 Blob
实例。此方法返回一个 promise,并将缓冲区暴露为一个 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"}
一次性流
因为 Body mixin 是建立在 ReadableStream 之上的,这意味着 body 流只能被读取一次。因此所有的 Body mixin 方法只能被调用一次;随后尝试调用 mixin 方法将引发错误:
fetch('https://foo.com')
.then((response) => response.blob().then(() => response.blob()));
// TypeError: Failed to execute 'blob' on 'Response': body stream is locked
let request = new Request('https://foo.com', {
method: 'POST',
body: 'foobar'
});
request.blob().then(() => request.blob());
// TypeError: Failed to execute 'blob' on 'Request': body stream is locked
即使流只是在被读取的过程中,所有这些方法都会在调用后立即锁定 ReadableStream 并防止第二个读取器访问流:
fetch('https://foo.com')
.then((response) => {
response.blob(); // 第一次调用锁定流
response.blob(); // 第二次调用尝试锁定流,但是失败了
});
// TypeError: Failed to execute 'blob' on 'Response': body stream is locked
let request = new Request('https://foo.com', {
method: 'POST',
body: 'foobar'
});
request.blob();
request.blob();
// TypeError: Failed to execute 'blob' on 'Request': body stream is locked
作为 Body mixin 的一部分,bodyUsed 布尔属性指示 ReadableStream 是否被干扰,这意味着读取器已经给流上了锁。这并不一定表示流已干枯。此属性在此处演示:
let request = new Request('https://foo.com', {
method: 'POST',
body: 'foobar'
});
let response = new Response('foobar');
console.log(request.bodyUsed); // false
console.log(response.bodyUsed); // false
request.text().then(console.log); // foobar
response.text().then(console.log); // foobar
console.log(request.bodyUsed); // true
console.log(response.bodyUsed); // true
使用 ReadableStream Body 许多 JavaScript 编程将网络视为原子操作;请求被一次性创建和发送,响应被公开为统一的数据负载,可以一次性全部变成可用。这种约定隐藏了底层的混乱,使涉及网络的代码更易于编写。由于 TCP/IP 的本质,传输的数据以块 (chunks) 的形式到达终端,并且只有网络可以这么快的传送这些块。接收端分配内存并写入通过网络接收的内容。Fetch API 允许读取和操作通过 ReadableStream 实时到达的数据。
注意:本节中的示例将获取 Fetch 规范的 HTML,可在 https://fetch.spec.whatwg.org/ 找到。这个页面有大约 1MB,这是一个足够大的有效负载,本节中的流示例将在多个块中到达。
Stream API 中定义的 ReadableStream 公开了一个 getReader() 方法,该方法生成一个 ReadableStreamDefaultReader,该方法可用于在主体块到达时异步获取它们。主体流的每个块都作为 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 }
// ...
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 块超出作用域后,浏览器会将其标记为适合垃圾回收。这允许在适合串行 (serially) 和离散段检查大型有效负载的场景中潜在地节省大量内存。
缓冲区的大小以及浏览器在将其推送到流之前是否等待它被填充取决于 JavaScript 运行时的实现。浏览器对这样一个事实很敏感,即在可能的情况下等待并填充分配的缓冲区是理想的,但同时通过尽可能频繁地发送 (有时未填充) 缓冲区来保持流充满。
浏览器可能会根据带宽或网络延迟等因素改变块缓冲区的大小。此外,如果浏览器决定不等待网络,它可能会决定向流发送部分填充的缓冲区。最终,代码应该准备好处理以下内容:
- 可改变大小的 Uint8Array 块
- Uint8Array 块被部分填充
- 块以不可预测的间隔到达
默认情况下,块将以 Uint8Array 格式到达。由于块的终止不考虑编码内容,因此可能存在诸如多字节字符之类的值在两个单独的连续块之间拆分。手动解决这个问题的方法很麻烦,但在许多情况下,编码 API 有即插即用 (plug-and-play) 的解决方案。
要将 Uint8Array 转换为可读文本,可以向 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();
// create secondary stream
return new ReadableStream({
async start(controller) {
try {
while (true) {
const {
value,
done
} = await reader.read();
if (done) {
break;
}
// Push the body stream's chunk onto the secondary stream
controller.enqueue(value);
}
} finally {
controller.close();
reader.releaseLock();
}
}
})
})
.then((secondaryStream) => new Response(secondaryStream))
.then(response => response.text())
.then(console.log);
// <!doctype html><html lang="en"><head><meta charset="utf-8"> ...
Beacon API
为了最大限度地传输有关页面的信息,许多分析工具需要尽可能晚地将遥测(telemetry)或分析数据发送到服务器。因此,最佳模式是在浏览器的 unload
事件上发送网络请求。此事件表示正在发生页面分离,并且不会在该页面上生成更多有用信息。当 unload
事件被触发时,分析工具希望停止收集信息并尝试将他们拥有的信息发送到服务器。这带来了一个问题,因为 unload
事件对浏览器意味着几乎没有理由分派任何挂起的网络请求(因为页面无论如何都会被丢弃)。例如,在 unload
处理程序中创建的任何异步请求都将被浏览器取消。因此,异步 XMLHttpRequest
或 fetch()
不适合此任务。分析工具可以使用同步 XMLHttpRequest
来强制传递请求,但这样做会导致用户体验问题,因为浏览器会暂停等待请求返回,从而延迟导航到下一页。
为了解决这个问题,W3C引入了一个补充的 Beacon API。该 API 向 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"}');
这种方法可能看起来只是 POST
请求的语法糖,但该方法有几个值得注意的特性:
sendBeacon()
不受页面生命周期结束的限制,它可以随时使用。- 调用
sendBeacon()
后,浏览器会将请求添加到内部请求队列中。浏览器会急切地尝试发送队列中的请求。 - 浏览器保证它会尝试发送请求,即使浏览器已经拆除了原始页面。
- 响应代码、超时和任何其他网络故障是完全不透明的,无法以编程方式处理。
- 在最初调用
sendBeacon()
时,beacon 请求与所有相关 cookie 一起发送。
Web Sockets
Web Sockets的目标是通过单个持久连接提供与服务器的全双工双向通信。在JavaScript中创建Web Socket时,会向服务器发送HTTP请求以启动连接。当服务器响应时,连接使用HTTP Upgrade头从HTTP切换到 Web Socket协议。这意味着Web Sockets无法使用标准HTTP服务器来实现,必须使用支持该协议的专用服务器才能正常工作。
由于Web Sockets使用自定义协议,因此URL方案略有不同。不使用http://
或https://
方案,而是使用 ws://
表示不安全连接,使用 wss://
表示安全连接。指定Web Sockets URL时,必须包含该方案,因为将来可能会支持其他方案。
使用自定义协议而不是HTTP的优点是可以在客户端和服务器之间发送非常少量的数据,不受HTTP字节开销的影响。使用较小的数据包使Web Sockets成为带宽和延迟有问题的移动应用程序的理想选择。使用自定义协议的缺点是定义协议需要比JavaScript API更长的时间。所有主要浏览器都支持Web Sockets。
API
要创建一个新的Web Socket,可实例化一个WebSocket对象并传入将提供连接的URL:
let socket = new WebSocket("ws://www.example.com/server.php");
必须将绝对URL传递给WebSocket构造函数。同域策略不适用于Web Sockets,因此可以打开与任何站点的连接。是否与来自特定域的页面进行通信完全取决于服务器(它可以使用握手中的信息确定请求的来源)。
一旦WebSocket对象被实例化,浏览器就会尝试创建连接。与XHR类似,WebSocket有一个readyState
属性,用于指示当前状态。但是,这些值与XHR的值不同:
WebSocket.OPENING (0)
: 正在建立连接。WebSocket.OPEN (1)
: 连接已经建立。WebSocket.CLOSING (2)
: 连接开始关闭。WebSocket.CLOSE (3)
: 连接已关闭。
WebSocket没有readystatechange
事件。但是,有其他事件对应于不同的状态。readyState
总是从0开始。
可以随时使用close()
方法关闭Web Sockets连接:
socket.close();
调用close()
后,readyState
立即更改为2(closing),并在完成时转换为3。
发送/接受数据
打开 Web Socket 后,可以通过连接发送数据,也可以从连接接收数据。要将数据发送到服务器,请使用 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;
// do something with data
};
类似于通过 send()
发送到服务器的数据,event.data
中返回的数据可以作为 ArrayBuffer 或 Blob 获取。这由 WebSocket 对象的 binaryType
控制,可以是“blob”或“arraybuffer”。
其他事件
WebSocket 对象还有三个在连接生命周期内触发的事件:
open
:当连接成功时触发。error
:发生错误时触发。连接无法持续。close
:当连接关闭时触发。
WebSocket 对象不支持 DOM2 事件侦听器,因此需要为每个对象使用 DOM0 方式事件处理程序:
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
对象的额外信息。event
对象还有三个附加属性:wasClean
,一个布尔值,指示连接是否干净地关闭;code
,从服务器发送的数字状态码;reason
,一个包含从服务器发送的消息的字符串。可能希望使用此信息向用户显示或记录分析:
socket.onclose = function(event) {
console.log(`Was clean? ${event.wasClean} Code=${event.code} Reason=${event.reason}`);
};
安全
已经发表了很多关于 Ajax 安全的文章。事实上,有整本专门针对该主题的书籍。大型 Ajax 应用程序的安全考虑非常广泛,但总体而言,有一些关于 Ajax 安全的基本知识需要了解。
首先,任何可以通过 Ajax 访问的 URL 也可以被浏览器或服务器访问。如以下 URL:
/getuserinfo.php?id=23
如果对此 URL 发出请求,它可能会返回关于 ID 为 23 的用户的一些数据。没有什么可以阻止某人将 URL 用户 ID 更改为 24 或 56 或任何其他值。getuserinfo.php
文件必须知道请求者是否真的可以访问被请求的数据。否则,服务器将门户大开。
当未经授权的系统能够访问资源时,它被视为跨站点请求伪造(CSRF)攻击。未经授权的系统使处理请求的服务器看起来是合法的。Ajax 应用程序,无论大小,都受到 CSRF 攻击的影响,从良性的漏洞证明(proof-of-vulnerability)攻击到恶意的数据窃取或数据破坏攻击。
如何保护通过 Ajax 访问的 URL 的流行理论是验证发送者是否有权访问资源。这可以通过以下方式完成:
- 需要 SSL 才能访问可以通过 Ajax 请求的资源。
- 要求计算令牌与每个请求一起发送。
请注意,以下对 CSRF 攻击无效:
- POST 替换为 GET — 这很容易改变。
- 使用 referrer 确定来源 — Referrers 很容易伪造。
- 根据 cookie 信息进行验证 — 也很容易伪造。