Ch24 网络请求与远程资源
现代JavaScript网络请求方案主要有两种
- 基于XMLHttpRequest(XHR)
- 基于Fetch
24.1 XMLHttpRequest对象
使用构造函数声明一个XHR对象:let xhr = new XMLHttpRequest();
24.1.1 使用XHR
使用一个XHR有以下几步:
同步请求:
发送请求:
- 调用open方法,为发送请求做好准备
- 调用send方法,将请求发送到服务器
// open接收三个参数:请求类型,请求URL,是否异步请求
xhr.open("get", "example.txt", false);
// send接收一个参数,是作为请求体发送的数据
xhr.send();
收到响应后:
-
检查status属性判断是否响应成功返回
- status状态码2xx表示响应成功,此时,responseText 或 responseXML(如果内容类型正确)属性中会有内容。
- status状态码304表示资源未修改过,从浏览器缓存中拿去。
异步请求:
XHR对象有个readyState属性,表示当前处在请求/响应的哪个阶段。
readyState每变化一次,都会触发readystatechange事件。一般来说,我们唯一关心的 readyState 值是 4,表示数据已就绪。
let xhr = new XMLHttpRequest();
xhr.onreadychange = 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); // true代表异步请求
xhr.send(null);
24.1.2 HTTP头部
每个 HTTP 请求和响应都会携带一些头部字段。XHR 对象会通过一些方法暴露与请求和响应相关的头部字段。
请求头部默认字段包括:Accept、Accept-Charset等。通过调用setRequestHeader()方法可以自定义请求头部,(必须在open之后,send之前调用!)
响应头部默认字段包括:Date、Server等。可以使用 getResponseHeader()方法从 XHR 对象获取响应头部,只要传入要获取头部的名称即 可。如果想取得所有响应头部,可以使用 getAllResponseHeaders()方法,这个方法会返回包含所 有响应头部的字符串。
24.1.3 GET请求
最常用的请求方法是 GET 请求,用于向服务器查询某些信息。必要时,需要在 GET 请求的 URL 后面添加查询字符串参数。
例子:
function addURLParam(url, name, value) {
url += (url.indexOf("?") == -1 ? "?" : "&");
url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
return url;
}
let url = "example.php";
// 添加参数
url = addURLParam(url, "name", "Nicholas");
url = addURLParam(url, "book", "Professional JavaScript");
// 初始化请求
xhr.open("get", url, false);
24.1.4 POST请求
第二个最常用的请求是 POST 请求,用于向服务器发送应该保存的数据。每个 POST 请求都应该在 请求体中携带提交的数据。
let xhr = new XMLHttpRequest();
xhr.open("POST", "example.php", true);
xhr.setRequestHeader("Content-Type", "applicatioin/x-www-form-urlencoded");
let data = document,getElementById("user-info");
xhr.send(serialize(data));
注意 POST 请求相比 GET 请求要占用更多资源。从性能方面说,发送相同数量的数据, GET 请求比 POST 请求要快两倍。
24.1.5 XMLHttpRequest Level 2
XMLHttpRequest Level 2 又进一步发展了 XHR 对象。
1. FormData 类型
FormData 类型便于表单序列化,也便于创建与表单类似格式的数据然后通过 XHR 发送。下面的代码创建了一个 FormData 对象,并填充了一些数据:
let data = new FormData();
data.append("name", "Nicholas");
append()方法接收两个参数:键和值,相当于表单字段名称和该字段的值。
使用 FormData 的另一个方便之处是不再需要给 XHR 对象显式设置任何请求头部了。XHR 对象能 够识别作为 FormData 实例传入的数据类型并自动配置相应的头部。
xhr.open("post", "postexample.php", true);
let form = document.getElementById("user-info");
xhr.send(new FormData(form));
2. 超时
超时用于表示发送请求后等待多少毫秒,如果响应不成功就中断请求。
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);
这个例子演示了使用 timeout 设置超时。给 timeout 设置 1000 毫秒意味着,如果请求没有在 1 秒钟内返回则会中断。此时则会触发 ontimeout 事件处理程序,readyState 仍然会变成 4,因此也会调用 onreadystatechange 事件处理程序。不过,如果在超时之后访问 status 属性则会发生错误。 为做好防护,可以把检查 status 属性的代码封装在 try/catch 语句中。
24.2 进度事件
Progress Events 定义了客户端服务器端通信。
24.3 跨域资源共享
默认情况下,XHR 只能访问与发起请求的页面在同一个域内的资源。这个安全限制可以防止某些恶意行为。不过,浏览器也需要支持合法跨源访问的能力。
CORS 背后的基本思路就是使用自定义的 HTTP 头部允许浏览器和服务器相互了解,请求在发送时会有一个额外的头部叫 Origin。Origin 头部包含发送请求的页面的源(协议、 域名和端口),以便服务器确定是否为其提供响应。
Origin: http://www.nczonline.net
如果服务器决定响应请求,那么应该发送 Access-Control-Allow-Origin 头部,包含相同的源;
Access-Control-Allow-Origin: http://www.nczonline.net
默认情况下,跨源请求不提供凭据(cookie、HTTP 认证和客户端 SSL 证书)。可以通过将 withCredentials 属性设置为 true 来表明请求会发送凭据。如果服务器允许带凭据的请求,那么可 以在响应中包含如下 HTTP 头部: Access-Control-Allow-Credentials: true
24.4 替代性跨源技术
- 图片探测
- JSONP
24.5 Fetch
Fetch API 能够执行 XMLHttpRequest 对象的所有任务,接口也更现代化,能够在 Web 工作线程等现代 Web 工具中使用。 Fetch API 则必须是异步。
24.5.1 基本用法
fetch()方法是暴露在全局作用域中的,包括主页面执行线程、模块和工作线程。
-
分派请求
fetch只有一个必须的参数input,多数情况下是目标url,而fetch返回一个Promise
let r = fetch('/bar');
console.log(r); // Promise <pending>
-
读取相应
读取响应内容的最简单方式是取得纯文本格式的内容,这要用到 text()方法。这个方法返回一个Promise,会解决为取得资源的完整内容:
fetch('bar.txt')
.then((response) => {
response.text().then((data) => {
console.log(data);
});
});
// bar.txt 的内容
fetch('bar.txt')
.then((response) => response.text())
.then((data) => console.log(data));
// bar.txt 的内容
-
处理状态码和请求失败
Fetch API 支持通过 Response 的 status(状态码)和 statusText(状态文本)属性检查响应状 态。成功获取响应的请求通常会产生值为 200 的状态码。违反 CORS、无网络连接、HTTPS 错配及其他浏览器/网络策略问题都会导致期约被拒绝。
fetch('/bar')
.then((response) => {
console.log(response.status); // 200
console.log(response.statusText); // OK
});
// 请求不存在的资源通常会产生值为 404 的状态码:
fetch('/does-not-exist')
.then((response) => {
console.log(response.status); // 404
console.log(response.statusText); // Not Found
});
// 请求的 URL 如果抛出服务器错误会产生值为 500 的状态码:
fetch('/throw-server-error')
.then((response) => {
console.log(response.status); // 500
console.log(response.statusText); // Internal Server Error
});
-
自定义选项
只传1个参数的默认情况下,是发送get请求,如果要进一步配置如何发送请求,需要传入可选的第二个参数,该参数是一个对象,里面包含若干可选属性。
- body
- cache
- credentials
- headers
- integrity
- keepalive
- method
- ...
24.5.2 常见Fetch请求模式
- 发送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
});
- 在请求体中发送参数
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
});
- 发送文件
let imageFormData = new FormData();
let imageInput = document.querySelector("input[type='file']");
imageFormData.append('image', imageInput.files[0]);
fetch('/img-upload', {
method: 'POST',
body: imageFormData
});
//多文件
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
});
- 中断请求
let abortController = new AbortController();
fetch('wikipedia.zip', { signal: abortController.signal })
.catch(() => console.log('aborted!');
// 10 毫秒后中断请求
setTimeout(() => abortController.abort(), 10);
// 已经中断
Ch25 客户端存储
25.1 cookie
HTTP cookie 通常也叫作 cookie,最初用于在客户端存储会话信息。这个规范要求服务器在响应 HTTP 请求时,通过发送 Set-Cookie HTTP 头部包含会话信息。
浏览器会存储这些会话信息,并在之后的每个请求中都会通过 HTTP 头部 cookie 再将它们发回服务器。
25.1.1 限制
cookie 是与特定域绑定的。设置 cookie 后,它会与请求一起发送到创建它的域。这个限制能保证 cookie 中存储的信息只对被认可的接收者开放,不被其他域访问。 因为 cookie 存储在客户端机器上,所以为保证它不会被恶意利用,浏览器会施加限制。同时,cookie 也不会占用太多磁盘空间。
- 不超过 300 个 cookie;
- 每个 cookie 不超过 4096 字节;
- 每个域不超过 20 个 cookie;
- 每个域不超过 81 920 字节。
25.1.2 cookie构成
cookie 在浏览器中是由以下参数构成的:
- 名称:唯一标识。不区分大小写
- 值:cookie里的字符串值
- 域:cookie的有效域
- 路径:请求URL中包含这个路径才会把cookie发送到服务器。
- 过期时间:表示何时删除cookie。
- 安全标志:设置之后,只在使用SSL安全连接的情况下才会把cookie发送到服务器。
25.1.3 JavaScript中的cookie
在 JavaScript 中处理 cookie 比较麻烦,因为接口过于简单,只有 BOM 的 document.cookie 属性。 要使用该属性获取值时,document.cookie 返回包含页面中所有有效 cookie 的字符串(根据域、路径、过期时间和安全设置),以分号分隔,如下面的例子所示: name1=value1;name2=value2;name3=value3
所有名和值都是 URL 编码的,因此必须使用 decodeURIComponent()解码。
25.2 Web Storage
Web Storage 的目的是解决通过客户端存储不需要频繁发送回服务器的数据时使用 cookie 的问题。
Web Storage 的第 2 版定义了两个对象:localStorage 和 sessionStorage。
25.2.1 Storage类型
Storage 类型用于保存名/值对数据,直至存储空间上限(由浏览器决定)。Storage 的实例与其他 对象一样,但增加了以下方法:
- clear:删除所有值
- getItem:取得指定name的值
- key:取得给定数值位置的名称
- removeItem:删除某个键值对
- setItem:设置一个键值对
25.2.2 sessionStorage
sessionStorage 对象只存储会话数据,这意味着数据只会存储到浏览器关闭。
25.2.3 localStorage
localStorage 对象作为在客户端持久存储数据的机制。要访问同一个 localStorage 对象,页面必须来自同一个域(子域不可以)、在相同的端 口上使用相同的协议。
存储在 localStorage 中的数据会保留到通过 JavaScript 删除或者用户清除浏览器缓存。localStorage 数据不受页面刷新影响,也不会因关闭窗口、标签页或重新启动浏览 器而丢失。