JavaScript 可以将网络请求发送到服务器,并在需要时加载新信息。
例如,我们可以使用网络请求来:
- 提交订单,
- 加载用户信息,
- 从服务器接收最新的更新,
- ……等。
……所有这些都没有重新加载页面!
对于来自 JavaScript 的网络请求,有一个总称术语 “AJAX”(Asynchronous JavaScript And XML 的简称)。但是,我们不必使用 XML:这个术语诞生于很久以前,所以这个词一直在那儿。
有很多方式可以向服务器发送网络请求,并从服务器获取信息。
fetch()
方法是一种现代通用的方法,那么我们就从它开始吧。旧版本的浏览器不支持它(可以 polyfill),但是它在现代浏览器中的支持情况很好。
基本语法:
let promise = fetch(url, [options])
url
—— 要访问的 URL。options
—— 可选参数:method,header 等。
没有 options
,那就是一个简单的 GET 请求,下载 url
的内容。
浏览器立即启动请求,并返回一个该调用代码应该用来获取结果的 promise
。
获取响应通常需要经过两个阶段。
第一阶段,当服务器发送了响应头(response header),fetch
返回的 promise
就使用内建的 Response class 对象来对响应头进行解析。
在这个阶段,我们可以通过检查响应头,来检查 HTTP 状态以确定请求是否成功,当前还没有响应体(response body)。
如果 fetch
无法建立一个 HTTP 请求,例如网络问题,亦或是请求的网址不存在,那么 promise 就会 reject。异常的 HTTP 状态,例如 404 或 500,不会导致出现 error。
我们可以在 response 的属性中看到 HTTP 状态:
status
—— HTTP 状态码,例如 200。ok
—— 布尔值,如果 HTTP 状态码为 200-299,则为true
。
例如:
let response = await fetch(url);
if (response.ok) { // 如果 HTTP 状态码为 200-299
// 获取 response body(此方法会在下面解释)
let json = await response.json();
} else {
alert("HTTP-Error: " + response.status);
}
第二阶段,为了获取 response body,我们需要使用一个其他的方法调用。
Response
提供了多种基于 promise 的方法,来以不同的格式访问 body:
response.text()
—— 读取 response,并以文本形式返回 response,response.json()
—— 将 response 解析为 JSON,response.formData()
—— 以FormData
对象(在 下一章 有解释)的形式返回 response,response.blob()
—— 以 Blob(具有类型的二进制数据)形式返回 response,response.arrayBuffer()
—— 以 ArrayBuffer(低级别的二进制数据)形式返回 response,- 另外,
response.body
是 ReadableStream 对象,它允许你逐块读取 body,我们稍后会用一个例子解释它。
例如,我们从 GitHub 获取最新 commits 的 JSON 对象:
let url = 'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits';
let response = await fetch(url);
let commits = await response.json(); // 读取 response body,并将其解析为 JSON
alert(commits[0].author.login);
也可以使用纯 promise 语法,不使用 await
:
fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits')
.then(response => response.json())
.then(commits => alert(commits[0].author.login));
要获取响应文本,可以使用 await response.text()
代替 .json()
:
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');
let text = await response.text(); // 将 response body 读取为文本
alert(text.slice(0, 80) + '...');
作为一个读取为二进制格式的演示示例,让我们 fetch 并显示一张 “fetch” 规范 中的图片(Blob
操作的有关内容请见 Blob):
let response = await fetch('/article/fetch/logo-fetch.svg');
let blob = await response.blob(); // 下载为 Blob 对象
// 为其创建一个 <img>
let img = document.createElement('img');
img.style = 'position:fixed;top:10px;left:10px;width:100px';
document.body.append(img);
// 显示它
img.src = URL.createObjectURL(blob);
setTimeout(() => { // 3 秒后将其隐藏
img.remove();
URL.revokeObjectURL(img.src);
}, 3000);
重要:
我们只能选择一种读取 body 的方法。
如果我们已经使用了 response.text()
方法来获取 response,那么如果再用 response.json()
,则不会生效,因为 body 内容已经被处理过了。
let text = await response.text(); // response body 被处理了
let parsed = await response.json(); // 失败(已经被处理过了)
Response header
Response header 位于 response.headers
中的一个类似于 Map 的 header 对象。
它不是真正的 Map,但是它具有类似的方法,我们可以按名称(name)获取各个 header,或迭代它们:
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');
// 获取一个 header
alert(response.headers.get('Content-Type')); // application/json; charset=utf-8
// 迭代所有 header
for (let [key, value] of response.headers) {
alert(`${key} = ${value}`);
}
Request header
要在 fetch
中设置 request header,我们可以使用 headers
选项。它有一个带有输出 header 的对象,如下所示:
let response = fetch(protectedUrl, {
headers: {
Authentication: 'secret'
}
});
……但是有一些我们无法设置的 header(详见 forbidden HTTP headers):
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-*
这些 header 保证了 HTTP 的正确性和安全性,所以它们仅由浏览器控制。
POST 请求
要创建一个 POST
请求,或者其他方法的请求,我们需要使用 fetch
选项:
method
—— HTTP 方法,例如POST
,body
—— request body,其中之一:- 字符串(例如 JSON 编码的),
FormData
对象,以form/multipart
形式发送数据,Blob
/BufferSource
发送二进制数据,- URLSearchParams,以
x-www-form-urlencoded
编码形式发送数据,很少使用。
JSON 形式是最常用的。
例如,下面这段代码以 JSON 形式发送 user
对象:
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
选项来发送 application/json
,这是 JSON 编码的数据的正确的 Content-Type
。
发送图片
我们同样可以使用 Blob
或 BufferSource
对象通过 fetch
提交二进制数据。
例如,这里有一个 <canvas>
,我们可以通过在其上移动鼠标来进行绘制。点击 “submit” 按钮将图片发送到服务器:
<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
});
// 服务器给出确认信息和图片大小作为响应
let result = await response.json();
alert(result.message);
}
</script>
</body>
请注意,这里我们没有手动设置 Content-Type
header,因为 Blob
对象具有内建的类型(这里是 image/png
,通过 toBlob
生成的)。对于 Blob
对象,这个类型就变成了 Content-Type
的值。
可以在不使用 async/await
的情况下重写 submit()
函数,像这样:
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); // 解析 response header
let result = await response.json(); // 将 body 读取为 json
或者以 promise 形式:
fetch(url, options)
.then(response => response.json())
.then(result => /* process result */)
响应的属性:
response.status
—— response 的 HTTP 状态码,response.ok
—— HTTP 状态码为 200-299,则为true
。response.headers
—— 类似于 Map 的带有 HTTP header 的对象。
获取 response body 的方法:
response.text()
—— 读取 response,并以文本形式返回 response,response.json()
—— 将 response 解析为 JSON 对象形式,response.formData()
—— 以FormData
对象(form/multipart 编码,参见下一章)的形式返回 response,response.blob()
—— 以 Blob(具有类型的二进制数据)形式返回 response,response.arrayBuffer()
—— 以 ArrayBuffer(低级别的二进制数据)形式返回 response。
到目前为止我们了解到的 fetch 选项:
method
—— HTTP 方法,headers
—— 具有 request header 的对象(不是所有 header 都是被允许的)body
—— 要以string
,FormData
,BufferSource
,Blob
或UrlSearchParams
对象的形式发送的数据(request body)。
在下一章,我们将会看到更多 fetch
的选项和用例。
任务
从 GitHub fetch 用户信息
创建一个异步函数 getUsers(names)
,该函数接受 GitHub 登录名数组作为输入,查询 GitHub 以获取有关这些用户的信息,并返回 GitHub 用户数组。
带有给定 USERNAME
的用户信息的 GitHub 网址是:https://api.github.com/users/USERNAME
。
沙箱中有一个测试用例。
重要的细节:
- 对每一个用户都应该有一个
fetch
请求。 - 请求不应该相互等待。以便能够尽快获取到数据。
- 如果任何一个请求失败了,或者没有这个用户,则函数应该返回
null
到结果数组中。
解决方案
要获取一个用户,我们需要:fetch('https://api.github.com/users/USERNAME')
.
如果响应的状态码是 200
,则调用 .json()
来读取 JS 对象。
否则,如果 fetch
失败,或者响应的状态码不是 200
,我们只需要向结果数组返回 null
即可。
代码如下:
async function getUsers(names) {
let jobs = [];
for(let name of names) {
let job = fetch(`https://api.github.com/users/${name}`).then(
successResponse => {
if (successResponse.status != 200) {
return null;
} else {
return successResponse.json();
}
},
failResponse => {
return null;
}
);
jobs.push(job);
}
let results = await Promise.all(jobs);
return results;
}
请注意:.then
调用紧跟在 fetch
后面,这样,当我们收到响应时,它不会等待其他的 fetch,而是立即开始读取 .json()
。
如果我们使用 await Promise.all(names.map(name => fetch(...)))
,并在 results
上调用 .json()
方法,那么它将会等到所有 fetch 都获取到响应数据才开始解析。通过将 .json()
直接添加到每个 fetch
中,我们就能确保每个 fetch 在收到响应时都会立即开始以 JSON 格式读取数据,而不会彼此等待。
这个例子表明,即使我们主要使用 async/await
,低级别的 Promise API 仍然很有用。