JS 中有两种发起网络请求的方式,用来实现页面的局部刷新。分别是 XMLHttpRequest 和 Fetch API。
XMLHttpRequest 俗称“AJAX”,你或许已经听说过了,它是 Asynchronous JavaScript And XML 的首字母简写形式,不过这个名字很具有误导性,XMLHttpRequest 几乎能够处理包括 XML 在内的几乎所有数据格式,只不过因为历史原因,这种命名被一直保留着,没做修改。
Fetch API 是最近引入的一个 API,以 fetch() 方法的形式暴露,它使用更加现代也更加强大,因此我们从它开始讲起。
fetch() 方法
基本语法如下:
let promise = fetch(url, [options])
- url —— 请求地址
- options —— 配置参数。例如,设置请求方法、请求头等
如果不使用 options,默认发起 GET 请求。
得到的响应通常分两步处理。
fetch 函数返回的是一个 Promise 对象,使用内置的 Response 类实例作为 resolve 值,而且在服务器接收到响应头时就立即返回。
在这一阶段,我们可以检查 HTTP 状态码、查看请求是否成功,还可以检查响应头,不过还没有响应体数据。
需要注意的是,fetch 方法只有在无法触发 HTTP 请求(比如,网络中断了),或者请求地址不存在时,才会 reject,这时能用 catch 捕获错误。对返回的非正常状态码,类似 400 或 500 码,不会生成错误。
我们可以通过以下两个响应属性,查看 HTTP 状态:
- status —— HTTP 状态码,如 200
- ok —— 一个布尔值,当 HTTP 状态码介于 200~299 之间(包括 200 和 299)时,返回 true,否则返回 false。
举个例子:
let response = await fetch(url);
if (response.ok) { // if HTTP-status is 200-299
// get the response body (the method explained below)
let json = await response.json();
} else {
alert("HTTP-Error: " + response.status);
}
第二阶段,获得响应体,这一步我们需要额外的方法调用。
Response 上提供了很多基于 Promise 的方法实现,能够让我们以不同的形式获取响应体数据:
- response.text() —— 以文本的形式返回响应数据
- response.json() —— 以 JSON 对象的形式返回响应数据
- response.formData() —— 以 FormData 对象的形式返回响应数据
- response.blob() —— 以 Blob(二进制数据) 对象的形式返回响应数据
- response.arrayBuffer() —— 以 ArrayBuffer(二进制数据的底层表示) 对象的形式返回响应数据
- 另外,还能通过 response.body 属性获得响应数据,这是一个 ReadableStream 对象,允许分块读取(chunk-by-chunk)响应数据,之后会看到例子。
举个例子,从 Github 上获得最新的提交信息,以 JSON 对象的形式:
let url = 'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits';
let response = await fetch(url);
let commits = await response.json(); // read response body and parse as JSON
alert(commits[0].author.login);
或者不使用 await,使用纯 Promise 语法来写:
fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits')
.then(response => response.json())
.then(commits => alert(commits[0].author.login));
如果需要以文本形式取得数据,就不是使用 .json() 了,而要用 await response.text():
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');
let text = await response.text(); // read response body as text
alert(text.slice(0, 80) + '...');
下面再以 Fetch 规范 中出现的 logo 文件为例,展示如何读取二进制文件(查看 https://javascript.info/blob 一章,可以看到更加关于操作 Blob 对象的内容):
let response = await fetch('/article/fetch/logo-fetch.svg');
let blob = await response.blob(); // download as Blob object
// create <img> for it
let img = document.createElement('img');
img.style = 'position:fixed;top:10px;left:10px;width:100px';
document.body.append(img);
// show it
img.src = URL.createObjectURL(blob);
setTimeout(() => { // hide after three seconds
img.remove();
URL.revokeObjectURL(img.src);
}, 3000);
⚠️重要提示:对同一个 Response 对象,只能调用一次响应体读取(body-reading)方法
比如,你已经调用了 response.text(),再调用 response.json() 就不行了,因为响应体数据已经被处理过了。
let text = await response.text(); // response body consumed
let parsed = await response.json(); // fails (already consumed)
响应头
响应头数据可以通过 response.headers 获得。
response.headers 是一个类 Map 对象,提供了单独操作某个字段方法,还是一个可迭代对象:
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');
// get one header
alert(response.headers.get('Content-Type')); // application/json; charset=utf-8
// iterate over all headers
for (let [key, value] of response.headers) {
alert(`${key} = ${value}`);
}
下面展示了 response 的数据结构信息:
请求头
发送请求时,可以使用 headers 属性,配置请求头信息:
let response = fetch(protectedUrl, {
headers: {
Authentication: 'secret'
}
});
以下是我们 不能通过此种方式设置的 HTTP 头部字段列表:
- Accept-Charset, Accept-Encoding
- Access-Control-Request-Headers
- Access-Control-Request-Method
- Connection
- Content-Length
- Cookie, Cookie2
- Date
- DNT
- Expect
- Host
- Keep-Alive
- Origin
- Referer
- TE
- Trailer
- Transfer-Encoding
- Upgrade
- Via
- Proxy-*
- Sec-*
这是为了安全考虑,把这些字段的设置权限交给服务端,客户端不能控制。
POST 请求
如果发起的是 GET 请求之外的其他请求方法,就要使用 fetch 的配置对象了:
- method —— 请求方法,比如 POST
- body —— 请求体,以下类型之一
- 字符串(比如,经过 JSON.stringify 方法编码后的)
- FormData 对象,以 form/multipart 的形式发送数据
- Blob/BufferSource,发送二进制数据
- URLSearchParams,以 x-www-form-urlencoded 的形式发送数据,很少使用
多数时间,都是使用经过 JSON.stringify
方法编码后的字符串做请求体:
下例中,user 对象经 JSON 编码后跟随请求一起发送:
let user = {
name: 'John',
surname: 'Smith'
};
let response = await fetch('/article/fetch/post/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(user)
});
let result = await response.json();
alert(result.message);
注意,这里的请求体 body 是个字符串,默认的 Content-Type 字段值会是 text/plain;charset=UTF-8。
因为,我们传输的是 JSON 数据,所以头部 headers 里要把 Content-Type 的值设置成 application/json,这才是正确的表达传输数据是经 JSON 编码后的。
发送图片
还能使用 Blob 或 BufferSource 对象发送二进制数据。
下例中,鼠标在
<body style="margin:0">
<canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>
<input type="button" value="Submit" onclick="submit()">
<script>
canvasElem.onmousemove = function(e) {
let ctx = canvasElem.getContext('2d');
ctx.lineTo(e.clientX, e.clientY);
ctx.stroke();
};
async function submit() {
let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
let response = await fetch('/article/fetch/post/image', {
method: 'POST',
body: blob
});
// the server responds with confirmation and the image size
let result = await response.json();
alert(result.message);
}
</script>
</body>
注意,这里没有手动设置 Content-Type 头部信息,这是因为 Blob 有内置的类型信息(这里是 image/png,由 toBlob 方法生成)。在发送 Blob 对象时,内置类型会作为 Content-Type 的值使用。
当然,上述的方法还能使用纯 Promise 方法(不使用 async/await)改写成下面这样:
function submit() {
canvasElem.toBlob(function(blob) {
fetch('/article/fetch/post/image', {
method: 'POST',
body: blob
})
.then(response => response.json())
.then(result => alert(JSON.stringify(result, null, 2)))
}, 'image/png');
}
总结
fetch 请求通常分两步处理。await 形式:
let response = await fetch(url, options); // resolves with response headers
let result = await response.json(); // read body as json
纯 Promise 方法形式:
fetch(url, options)
.then(response => response.json())
.then(result => /* process result */)
响应属性:
- response.status – HTTP 状态码
- response.ok – HTTP 状态码介于 200-299 之间时,返回 true,否则返回 false
- response.headers – 以类 Map 对象形式返回的 HTTP 响应头信息
获得响应体的方法:
- response.text() —— 以文本的形式返回响应数据
- response.json() —— 以 JSON 对象的形式返回响应数据
- response.formData() —— 以 FormData 对象的形式返回响应数据
- response.blob() —— 以 Blob(二进制数据) 对象的形式返回响应数据
- response.arrayBuffer() —— 以 ArrayBuffer(二进制数据的底层表示) 对象的形式返回响应数据
到目前为止,学到的选项字段:
- method —— 请求方法
- headers —— 以对象形式表达的请求头(有部分字段不支持设置)
- body —— 发送的请求体数据,可以取 string、FormData、BufferSource、Blob 或 UrlSearchParams。
下章中,我们会看到更多的 fetch 选项以及它的使用用例。
(完)