🥢 JavaScript Fetch
# fetch
的使用
fetch()
方法是一种现代通用网络请求的方法:
let promise = fetch(url, [options])
url
:请求网络路径;options
:可选参数(方法、请求头等)。没有可选参数,默认请求方法get
。
使用 fetch
发送网络请求后,获取来自服务器的响应需要经过两个阶段:
第一阶段:当服务器发送了响应头,
fetch
返回的promise
就使用内建的Response
类对象来 对响应头进行解析。可以在这个阶段,检查响应头,检查 HTTP 状态确定请求是否成功了,这个阶段还没有响应体。
- 如果
fetch
无法建立一个 HTTP 请求。例如,网络问题;请求网络地址不存在,此时promise
就会reject
。 - 异常的 HTTP 状态,例如 404 或 500,不会导致出现 error。
请求 HTTP 状态可以通过两个属性查看:
status
:HTTP 状态码;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); }
1
2
3
4
5
6
7
8- 如果
第二阶段:获取
response
响应体,使用一个其他的方法调用。Response
提供了多种基于promise
的方法,来以不同的格式访问 resposne 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
。
🌰 例子 / 获取响应的 JSON 对象:
let url = 'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits';
let response = await fetch(url);
let commits = await response.json();
console.log(commits[0].author.login)
2
3
4
5
或者使用纯 promise
:
fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits')
.then(response => response.json())
.then(commits => alert(commits[0].author.login));
2
3
注意
一般只能选择一种读取 body 的方法。例如,如果已经使用了 response.text()
来获取 response
, 那么再用 response.json()
不会生效。因为响应体内容已经被处理过了。
# 响应头 Response header
Response header 是位于 response.headers
中的一个类似于 Map 的 header 对象。(不是真正的 Map 对象) 可以通过 名字 获取各个 header
,或者迭代。
🌰 例子 / 获取响应头 Content-type
:
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}`);
}
2
3
4
5
6
7
8
9
# 请求头 Request header
可以通过在使用 fetch
时,在选择参数中配置 headers
选项。
🌰 例子:
let response = fetch(protectedUrl, {
headers: {
Authentication: 'secret'
}
})
2
3
4
5
为了保证 HTTP 的正确性和安全性,存在不能控制的请求头:https://fetch.spec.whatwg.org/#forbidden-header-name。仅有浏览器控制。
# 请求方法 method
要使用 默认方法 GET
以外的请求方法,通过 method
选项配置 / 或者配置 body
(request body, 常用 JSON)。
🌰 例子 / 以 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()
console.log(result.message)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如果请求的 body
是字符串,则 Content-Type
会默认设置为 text/plain;charset=UTF-8
。所以如果要发送 JSON,需要设置请求头 Content-type
控制编码类型。
🌰 例子 / 获取 GitHub 用户信息。
- 创建一个异步函数
getUsers(names)
,该函数接受 GitHub 登录名数组作为输入,查询 GitHub 以获取有关这些用户的信息,并返回 GitHub 用户数组。- GitHub 查询用户信息的 API:https://api.github.com/users/USERNAME
async function getUsers(names) {
let jobs = [];
for (let name of names) {
let job = fetch(`https://api.github.com/users/${name}`).then(
response => {
if (response.status != 200) return null;
else return response.json();
},
error => {
return null;
}
);
jobs.push(job)
}
let results = await Promis.all(jobs)
return results;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 对于 每一个用户名都有一个
fetch
请求。求不应该相互等待。以便能够尽快获取到数据。如果任何一个请求失败了,或者没有这个用户,则函数应该返回null
到结果数组中。.then
调用紧跟在fetch
后面,这样当收到响应时,它不会等待其他的fetch
,而是立即开始读取.json()
。通过将.json()
直接添加到每个fetch
中,就能确保每个 fetch 在收到响应时都会立即开始以 JSON 格式读取数据,而不会彼此等待。- 使用
await Promise.all
将会等到所有的fetch
都获取到响应数据才开始解析。
# Fetch
的应用
# FormData
用于发送 HTML 表单数据的类型对象。构造器使用如下:
let formData = new FormData([form])
[form]
是选择性提供的。如果提供了 HTMLform
元素,就会自动捕获该from
元素字段。
FormData
的特殊之处在于网络方法。例如 fetch
可以接受一个 FormData
对象作为 body。它会被编码并发送出去,带有 Content-Type: multipart/form-data
。
对于服务器,只是普通的表达数据提交。
🌰 例子 / 提交简单的表单数据:
<form id="formElem">
<input type="text" name="name" value="John">
<input type="text" name="surname" value="Smith">
<input type="submit">
</form>
2
3
4
5
let formElem = document.querySelector("#formElem");
formElem.onsubmit = async (e) => {
e.preventDefault
let response = await fetch('/article/formdata/post/user', {
method: 'POST',
body: new FormData(formElem)
});
let result = await response.json()
console.log(result.message)
}
2
3
4
5
6
7
8
9
10
11
12
FormData
提供的方法修改其中的字段:
formData.append(name, value)
:添加具有给定name
和value
的表单字段;formData.append(name, blob, fileName)
:添加一个相当于<input type="file">
输入类型的字段。第三个参数fileName
设置文件名(而不是表单字段名),因为它是用户文件系统中文件的名称;formData.delete(name)
:移除带有给定name
的字段;formData.get(name)
:获取带有给定name
的字段值;formData.has(name)
:如果存在带有给定name
的字段,则返回true
,否则返回false
。
技术上,可以拥有多个 相同名称
name
的字段。因此,多次调用append
将会添加多个具有相同名称的字段。使用set
方法,确保该字段只有一个唯一的值(移除之前存在的name
字段)。
formData.set(name, value)
formData.set(name, blob, fileName)
与使用
append
的方法相同。
可以使用 for ... of
循环迭代 formData
中的字段与值:
for (let [name, value] of formData) {
console.log(`${name} = ${value}`)
}
2
3
# FormData
表单上传文件
FormData
表单类型始终带有请求头 Content-type: multipart/form-data
发送数据,这个编码允许发送文件 ( <input type="file">
类型的输入框)。
🌰 例子 / 上传图片:
<form id="formElem">
<input type="text" name="firstName" value="John">
Picture: <input type="file" name="picture" accept="image/*">
<input type="submit">
</form>
2
3
4
5
formElem.onsubmit = async (e) => {
e.preventDefault();
let response = await fetch('/article/formdata/post/user-avatar', {
method: 'POST',
body: new FormData(formElem)
});
let result = await response.json();
alert(result.message);
};
2
3
4
5
6
7
8
9
10
11
12
# Fetch
跨源请求
跨源请求(跨域) (跨源资源请求 Cross-Origin Resource Sharing / CORS ):发送到其他域、协议或者端口的请求。
允许跨源请求的两种请求类型:
- 安全请求;
- 所有的其他请求。
安全的请求需要满足两个条件:
- 安全的请求方法:
GET
、POST
、HEAD
; - 安全的请求头 header 仅允许自定义下列的 header:
Accept
,Accept-Language
,Content-Language
,Content-Type
的值为application/x-www-form-urlencoded
,multipart/form-data
或text/plain
。
除了安全的请求,没有符合上述条件的请求都为非安全的请求。例如,使用
PUT
请求方法,或者具有除了上述的自定义的请求头部。安全的请求与非安全的请求的本质区别:
可以使用
<form>
/script
进行安全请求,无需特殊的方法。使用安全的请求能兼容较旧的服务器,不用进行特殊的处理。
而发送非安全的请求,无法进行同样的工作。
旧的服务器可能会认为此类请求来自具有特权的来源。当尝试发送一个非安全请求时,浏览器会发送一个特殊的预检请求到服务器,询问服务是否接受此类跨源请求吗。
服务器需要明确通过 header 确认,否则非安全的请求无法发送。
对于安全的请求 CORS:
🌰 例子:
从 https://somewhere.page
请求 https://anywhere.com/request
,发送的请求头为:
GET /request
Host: anywhere.com
Origin: https://somewhere.page
2
3
请求头的 Origin 会包含确切的请求来源(域名、协议、端口),没有路径。服务器检查 Origin ,如果同意接受请求,就会在响应中添加一个特殊的 header
Access-Control-Allow-Origin
。该 header 包含了允许的源,或者一个星号*
。然后响应成功,否则报错。浏览器作为中间人:确保了发送的跨源请求带有正确的 Origin;检查响应头中是否带有许可
Access-Control-Allow-Origin
,如果存在,则允许 JavaScript 访问响应,否则将失败并报错。
响应的请求头:
200 OK
Content-Type:text/html; charset=UTF-8
Access-Control-Allow-Origin: *
2
3
对于响应头 (Response Header)处理跨源请求时,JavaScript 只能访问 安全的响应头:
Cache-Control
Content-Language
Content-Type
Expires
Last-Modified
Pragma
对于其他的响应头都会导致出错。
要授予 JavaScript 对任何其他请求的访问权限,服务器必须发送
Access-Control-Expose-Headers
header。它包含一个以逗号分隔的应该被设置为可访问的非安全 header 名称列表。
对于非安全的请求:
为了使用任何的 HTTP 请求方法(不仅 GET / POST,还有可能是 PATCH、DELETE 或者其他)。
浏览器发送预检请求,来请求许可:
预检请求使用
OPTIONS
方法,它没有 body,但是有三个 header:Access-Control-Request-Method
header 带有非安全请求的方法。Access-Control-Request-Headers
header 提供一个以逗号分隔的非安全 HTTP-header 列表。
如果服务器同意处理请求,那么会进行响应以下响应头信息:
Access-Control-Allow-Origin
必须为*
或进行请求的源才能允许此请求。Access-Control-Allow-Methods
必须具有允许的方法。Access-Control-Allow-Headers
必须具有一个允许的 header 列表。Access-Control-Max-Age
可以指定缓存此权限的秒数。因此,浏览器不是必须为满足给定权限的后续请求发送预检。
🌰 例子:
发送预检请求 /preflight request:
OPTIONS /service.json Host: site.com Origin: https://somewhere.info Access-Control-Request-Method: PATCH Access-Control-Request-Headers: Content-Type,API-Key
1
2
3
4
5Origin
: 来源。
Access-Control-Request-Method
: 请求方法。Access-Control-Request-Headers
: 以逗号分隔的非安全 header 列表。
服务器响应预检响应 /preflight response:
Access-Control-Allow-Origin: https://somewhere.page Access-Control-Allow-Methods: PUT,PATCH,DELETE Access-Control-Allow-Headers: Content-Type,API-Key Access-Control-Max-Age: 86400
1
2
3
4这将允许后续通信,否则会触发错误。如果服务器将来需要其他方法和 header,则可以通过将这些方法和 header 添加到列表中来预先允许它们。
可以看到
PATCH
在Access-Control-Allow-Methods
中,Content-Type,API-Key
在列表Access-Control-Allow-Headers
中,因此它将发送主请求。Access-Control-Max-Age
带有一个表示秒的数字,则在给定的时间内,预检权限会被缓存。上面的响应将被缓存 86400 秒,也就是一天。在此时间范围内,后续请求将不会触发预检。假设它们符合缓存的配额,则将直接发送它们。发送实际请求:
最后的请求头:
PATCH /service.json Host: site.com Content-Type: application/json API-Key: secret Origin: https://javascript.info
1
2
3
4
5响应头应该添加
Access-Control-Allow-Origin
。例如:
Access-Control-Allow-Origin: https://somewhere.pages
1
# Fetch 请求凭证
默认情况下,由 JavaScript 代码发起的跨源请求,不会带任何凭据(cookies 或者 HTTP 网络认证)。
因为具有凭据的请求比没有凭据的请求要强大得多。如果被允许,它会使用它们的凭据授予 JavaScript 代表用户行为和访问敏感信息的全部权力。
所以要在 跨源请求 必须显式地带有允许请求的凭据和附加 header。
例子:要在 fetch
中发送凭据,需要添加 credentials: "include"
选项:
fetch('http://another.com', {
credentials: "include"
});
2
3
现在,
fetch
将把源自another.com
的 cookie 和我们的请求发送到该网站。
如果服务器同意接受 带有凭据 的请求,则除了 Access-Control-Allow-Origin
外,服务器还应该在响应中添加 header Access-Control-Allow-Credentials: true
。
响应头如下:
200 OK
Access-Control-Allow-Origin: https://somewhere.pages
Access-Control-Allow-Credentials: true
2
3
注意:对于具有凭据的请求,禁止 Access-Control-Allow-Origin
使用星号 *
。如上所示,它必须有一个确切的源。这是另一项安全措施,以确保服务器真的知道它信任的发出此请求的是谁。