使用 Fetch API
The Fetch API provides a JavaScript interface for making HTTP requests and processing the responses.
Fetch 是 XMLHttpRequest
的现代替代品:与使用回调的 XMLHttpRequest
不同,Fetch 是基于 Promise 的,并与现代 Web 的功能集成在一起,例如 服务工作者 和 跨域资源共享 (CORS)。
使用 Fetch API,您可以通过调用 fetch()
来发出请求,该函数在 window
和 worker
上下文中都可用作全局函数。您可以向其传递一个 Request
对象或包含要获取的 URL 的字符串,以及可选参数来配置请求。
fetch()
函数返回一个 Promise
,该 Promise 用表示服务器响应的 Response
对象来完成。然后,您可以通过调用响应上的适当方法来检查请求状态并提取响应正文,包括文本和 JSON。
以下是一个使用 fetch()
从服务器检索 JSON 数据的最小函数
async function getData() {
const url = "https://example.org/products.json";
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const json = await response.json();
console.log(json);
} catch (error) {
console.error(error.message);
}
}
我们声明一个包含 URL 的字符串,然后调用 fetch()
,传递 URL 而不使用任何额外选项。
fetch()
函数将在某些错误上拒绝 Promise,但如果服务器以错误状态响应(例如 404
),则不会拒绝。因此,我们还会检查响应状态,如果它不是 OK,则抛出异常。
否则,我们通过调用 Response
的 json()
方法来获取响应正文内容作为 JSON,并记录其值之一。请注意,与 fetch()
本身一样,json()
是异步的,与访问响应正文内容的所有其他方法一样。
在本文档的其余部分,我们将更详细地介绍此过程的不同阶段。
发出请求
设置方法
设置正文
请求正文是请求的有效负载:它是客户端发送给服务器的内容。您不能在 GET
请求中包含正文,但它对于将内容发送到服务器的请求(例如 POST
或 PUT
请求)非常有用。例如,如果您想将文件上传到服务器,您可能会发出 POST
请求并将文件作为请求正文包含在内。
要设置请求正文,请将其作为 body
选项传递
const response = await fetch("https://example.org/post", {
body: JSON.stringify({ username: "example" }),
// ...
});
您可以将正文提供为以下任何类型的实例
请注意,与响应正文一样,请求正文是流,发出请求将读取流,因此如果请求包含正文,您就不能两次发出该请求
const request = new Request("https://example.org/post", {
method: "POST",
body: JSON.stringify({ username: "example" }),
});
const response1 = await fetch(request);
console.log(response1.status);
// Will throw: "Body has already been consumed."
const response2 = await fetch(request);
console.log(response2.status);
相反,您需要在发送请求之前 创建该请求的克隆
const request1 = new Request("https://example.org/post", {
method: "POST",
body: JSON.stringify({ username: "example" }),
});
const request2 = request1.clone();
const response1 = await fetch(request1);
console.log(response1.status);
const response2 = await fetch(request2);
console.log(response2.status);
有关更多信息,请参阅 锁定和干扰的流。
设置报头
请求报头向服务器提供有关请求的信息:例如,Content-Type
报头告诉服务器请求正文的格式。许多报头由浏览器自动设置,脚本无法设置:这些称为 禁止的报头名称。
要设置请求报头,请将它们分配给 headers
选项。
您可以在此处传递一个包含 header-name: header-value
属性的对象字面量
const response = await fetch("https://example.org/post", {
headers: {
"Content-Type": "application/json",
},
// ...
});
或者,您可以构造一个 Headers
对象,使用 Headers.append()
向该对象添加报头,然后将 Headers
对象分配给 headers
选项
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
const response = await fetch("https://example.org/post", {
headers: myHeaders,
// ...
});
如果 mode
选项设置为 no-cors
,则您只能设置 CORS 安全列表中的请求报头。
发出 POST 请求
我们可以结合 method
、body
和 headers
选项来发出 POST 请求
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
const response = await fetch("https://example.org/post", {
method: "POST",
body: JSON.stringify({ username: "example" }),
headers: myHeaders,
});
发出跨域请求
是否可以发出跨域请求取决于 mode
选项的值。它可以取三个值之一:cors
、no-cors
或 same-origin
。
- 默认情况下,
mode
设置为cors
,这意味着如果请求是跨域的,则它将使用 跨域资源共享 (CORS) 机制。这意味着- 如果请求是 简单请求,则始终会发送请求,但服务器必须以正确的
Access-Control-Allow-Origin
报头进行响应,否则浏览器不会与调用者共享响应。 - 如果请求不是简单请求,则浏览器将发送 预检请求 以检查服务器是否了解 CORS 并允许该请求,并且只有在服务器以适当的 CORS 报头响应预检请求时才会发送实际请求。
- 如果请求是 简单请求,则始终会发送请求,但服务器必须以正确的
- 将
mode
设置为same-origin
将完全禁止跨域请求。 - 将
mode
设置为no-cors
意味着请求必须是简单请求,这会限制可以设置的报头,并将方法限制为GET
、HEAD
和POST
。
包含凭据
凭据是 Cookie、TLS 客户端证书或包含用户名和密码的身份验证报头。
要控制浏览器是否发送凭据,以及浏览器是否遵守任何 Set-Cookie
响应报头,请设置 credentials
选项,它可以取以下三个值之一
omit
:从不向请求中发送凭据,也不在响应中包含凭据。same-origin
(默认值):只为同源请求发送和包含凭据。include
:始终包含凭据,即使是跨域的。
请注意,如果 Cookie 的 SameSite
属性设置为 Strict
或 Lax
,则即使 credentials
设置为 include
,也不会跨站点发送该 Cookie。
在跨域请求中包含凭据会使网站容易受到 CSRF 攻击,因此,即使 credentials
设置为 include
,服务器也必须通过在其响应中包含 Access-Control-Allow-Credentials
报头来同意包含凭据。此外,在这种情况下,服务器必须在 Access-Control-Allow-Origin
响应报头中明确指定客户端的来源(即,不允许 *
)。
这意味着,如果 credentials
设置为 include
并且请求是跨域的,则
- 如果请求是 简单请求,则会使用凭据发送请求,但服务器必须设置
Access-Control-Allow-Credentials
和Access-Control-Allow-Origin
响应报头,否则浏览器将返回网络错误给调用者。如果服务器确实设置了正确的报头,则响应(包括凭据)将传递给调用者。 - 如果请求不是简单请求,则浏览器将发送一个预检请求,不包含凭据,服务器必须设置
Access-Control-Allow-Credentials
和Access-Control-Allow-Origin
响应头,否则浏览器将向调用方返回网络错误。如果服务器设置了正确的头,则浏览器将继续执行真正的请求,包括凭据,并将真正的响应(包括凭据)传递给调用方。
创建Request
对象
Request()
构造函数采用与fetch()
本身相同的参数。这意味着,您无需将选项传递给fetch()
,而是可以将相同的选项传递给Request()
构造函数,然后将该对象传递给fetch()
。
例如,我们可以使用以下代码通过将选项传递给fetch()
来发出POST请求
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
const response = await fetch("https://example.org/post", {
method: "POST",
body: JSON.stringify({ username: "example" }),
headers: myHeaders,
});
但是,我们可以将其重写为将相同的参数传递给Request()
构造函数
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
const myRequest = new Request("https://example.org/post", {
method: "POST",
body: JSON.stringify({ username: "example" }),
headers: myHeaders,
});
const response = await fetch(myRequest);
这也意味着您可以从另一个请求创建请求,同时使用第二个参数更改其某些属性
async function post(request) {
try {
const response = await fetch(request);
const result = await response.json();
console.log("Success:", result);
} catch (error) {
console.error("Error:", error);
}
}
const request1 = new Request("https://example.org/post", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username: "example1" }),
});
const request2 = new Request(request1, {
body: JSON.stringify({ username: "example2" }),
});
post(request1);
post(request2);
取消请求
要使请求可取消,请创建一个AbortController
,并将它的AbortSignal
分配给请求的signal
属性。
要取消请求,请调用控制器的abort()
方法。fetch()
调用将拒绝承诺,并引发AbortError
异常。
const controller = new AbortController();
const fetchButton = document.querySelector("#fetch");
fetchButton.addEventListener("click", async () => {
try {
console.log("Starting fetch");
const response = await fetch("https://example.org/get", {
signal: controller.signal,
});
console.log(`Response: ${response.status}`);
} catch (e) {
console.error(`Error: ${e}`);
}
});
const cancelButton = document.querySelector("#cancel");
cancelButton.addEventListener("click", () => {
controller.abort();
console.log("Canceled fetch");
});
如果fetch()
调用已完成但响应主体尚未读取时取消请求,则尝试读取响应主体将拒绝并引发AbortError
异常。
async function get() {
const controller = new AbortController();
const request = new Request("https://example.org/get", {
signal: controller.signal,
});
const response = await fetch(request);
controller.abort();
// The next line will throw `AbortError`
const text = await response.text();
console.log(text);
}
处理响应
一旦浏览器从服务器接收到了响应状态和头(可能在接收响应主体本身之前),由fetch()
返回的承诺将使用Response
对象完成。
检查响应状态
由fetch()
返回的承诺将在某些错误(例如网络错误或错误的方案)上被拒绝。但是,如果服务器以404
之类的错误进行响应,则fetch()
将使用Response
完成,因此我们必须在读取响应主体之前检查状态。
Response.status
属性告诉我们数字状态码,而Response.ok
属性在状态处于200 范围内时返回true
。
常见的模式是检查ok
的值,如果为false
则抛出异常
async function getData() {
const url = "https://example.org/products.json";
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
// ...
} catch (error) {
console.error(error.message);
}
}
检查响应类型
响应具有type
属性,它可以是以下之一
basic
:请求是同源请求。cors
:请求是跨源 CORS 请求。opaque
:请求是使用no-cors
模式发出的跨源简单请求。opaqueredirect
:请求将redirect
选项设置为manual
,服务器返回了重定向状态。
类型决定了响应的可能内容,如下所示
- 基本响应排除禁止响应头名称列表中的响应头。
- CORS 响应仅包含CORS 安全列表响应头列表中的响应头。
- 不透明响应和不透明重定向响应的
status
为0
,头列表为空,主体为null
。
检查头
与请求一样,响应也具有headers
属性,它是一个Headers
对象,其中包含所有对脚本公开的响应头,但会根据响应类型进行排除。
此功能的常见用例是在尝试读取主体之前检查内容类型
async function fetchJSON(request) {
try {
const response = await fetch(request);
const contentType = response.headers.get("content-type");
if (!contentType || !contentType.includes("application/json")) {
throw new TypeError("Oops, we haven't got JSON!");
}
// Otherwise, we can read the body as JSON
} catch (error) {
console.error("Error:", error);
}
}
读取响应主体
Response
接口提供了一些方法来以多种不同格式检索整个主体内容
这些都是异步方法,返回一个Promise
,该承诺将使用主体内容完成。
在此示例中,我们获取图像并将其读取为Blob
,然后可以使用它来创建对象 URL
const image = document.querySelector("img");
const url = "flowers.jpg";
async function setImage() {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const blob = await response.blob();
const objectURL = URL.createObjectURL(blob);
image.src = objectURL;
} catch (e) {
console.error(e);
}
}
如果响应主体不是适当的格式,则该方法将抛出异常:例如,如果在无法解析为 JSON 的响应上调用json()
。
流式传输响应主体
请求和响应主体实际上是ReadableStream
对象,每当您读取它们时,您都在流式传输内容。这对内存效率很有利,因为浏览器不必在调用方使用诸如json()
之类的 方法检索内容之前将整个响应缓冲在内存中。
这也意味着调用方可以在接收内容时增量地处理内容。
例如,考虑一个GET
请求,它获取一个大型文本文件并以某种方式处理它,或将其显示给用户
const url = "https://www.example.org/a-large-file.txt";
async function fetchText(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const text = await response.text();
console.log(text);
} catch (e) {
console.error(e);
}
}
如果我们使用Response.text()
(如上所述),我们必须等到整个文件都接收完毕才能处理任何部分。
如果我们改为流式传输响应,我们可以在从网络接收内容时处理内容的块
const url = "https://www.example.org/a-large-file.txt";
async function fetchTextAsStream(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const stream = response.body.pipeThrough(new TextDecoderStream());
for await (const value of stream) {
console.log(value);
}
} catch (e) {
console.error(e);
}
}
在此示例中,我们异步迭代流,在内容到达时处理每个块。
请注意,当您直接访问主体时,您会获得响应的原始字节,并且必须自己对其进行转换。在本例中,我们调用ReadableStream.pipeThrough()
将响应通过TextDecoderStream
传递,该流将 UTF-8 编码的主体数据解码为文本。
逐行处理文本文件
在下面的示例中,我们获取文本资源并逐行处理它,使用正则表达式来查找换行符。为简单起见,我们假设文本为 UTF-8,并且不处理获取错误
async function* makeTextFileLineIterator(fileURL) {
const response = await fetch(fileURL);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let { value: chunk, done: readerDone } = await reader.read();
chunk = chunk || "";
const newline = /\r?\n/gm;
let startIndex = 0;
let result;
while (true) {
const result = newline.exec(chunk);
if (!result) {
if (readerDone) break;
const remainder = chunk.substr(startIndex);
({ value: chunk, done: readerDone } = await reader.read());
chunk = remainder + (chunk || "");
startIndex = newline.lastIndex = 0;
continue;
}
yield chunk.substring(startIndex, result.index);
startIndex = newline.lastIndex;
}
if (startIndex < chunk.length) {
// Last line didn't end in a newline char
yield chunk.substring(startIndex);
}
}
async function run(urlOfFile) {
for await (const line of makeTextFileLineIterator(urlOfFile)) {
processLine(line);
}
}
function processLine(line) {
console.log(line);
}
run("https://www.example.org/a-large-file.txt");
锁定和干扰流
请求和响应主体为流的结果是
- 如果使用
ReadableStream.getReader()
将读取器附加到流,则该流将被锁定,并且任何其他内容都无法读取该流。 - 如果从流中读取了任何内容,则该流将被干扰,并且任何其他内容都无法从该流中读取。
这意味着无法多次读取相同的响应(或请求)主体
async function getData() {
const url = "https://example.org/products.json";
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const json1 = await response.json();
const json2 = await response.json(); // will throw
} catch (error) {
console.error(error.message);
}
}
如果您确实需要多次读取主体,则必须在读取主体之前调用Response.clone()
async function getData() {
const url = "https://example.org/products.json";
try {
const response1 = await fetch(url);
if (!response1.ok) {
throw new Error(`Response status: ${response1.status}`);
}
const response2 = response1.clone();
const json1 = await response1.json();
const json2 = await response2.json();
} catch (error) {
console.error(error.message);
}
}
这是在使用服务工作者实现离线缓存时常见的模式。服务工作者希望将响应返回给应用程序,但也希望缓存响应。因此,它克隆响应,返回原始响应,并缓存克隆
async function cacheFirst(request) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open("MyCache_1");
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
return Response.error();
}
}
self.addEventListener("fetch", (event) => {
if (precachedResources.includes(url.pathname)) {
event.respondWith(cacheFirst(event.request));
}
});