Fetch API 自其诞生以来,已经成为现代 Web 开发中进行网络请求的主流方式。它以其简洁的 Promise 接口和对 HTTP 管道的清晰建模,取代了传统的 XMLHttpRequest,为开发者带来了更优雅、更强大的异步请求体验。然而,Fetch API 表面上的简洁背后,隐藏着一个复杂的内部状态机,它精确地协调着从请求发起、网络传输、响应接收到最终数据体(Body)读取的每一个异步步骤。
理解 Fetch API 的内部状态机,对于编写健壮、高效且具备良好错误处理机制的网络应用至关重要。我们将深入探讨这一机制,从请求的构建到响应体的消费,揭示 Fetch API 如何在各个阶段管理其异步状态。
Fetch API 的核心构成与异步基石
Fetch API 的核心由几个关键接口组成:
fetch()方法: 全局函数,用于发起网络请求。它返回一个Promise,该 Promise 在网络响应的头部被接收时解析为一个Response对象。Request接口: 表示一个请求资源的对象。你可以手动创建它来更精细地配置请求,例如设置方法、URL、头部、模式、缓存策略和请求体。Response接口: 表示对请求的响应的对象。它包含了响应的状态码、状态文本、头部信息,以及一个Body混入(Mixin),用于处理响应体。Headers接口: 允许你查询、添加或删除 HTTP 头部。Body混入 (Mixin):Request和Response对象都实现了Body混入,它提供了一系列异步方法(如json(),text(),blob(),arrayBuffer(),formData())来消费请求或响应的体数据。
Fetch API 的异步特性完全基于 JavaScript 的 Promise 机制。每次通过 fetch() 发起请求,都会得到一个 Promise。这个 Promise 将经历 pending(待定)、fulfilled(已兑现)或 rejected(已拒绝)三种状态。理解这些状态何时以及如何转换,是掌握 Fetch API 内部机制的关键。
Fetch API 内部状态机:高层概览
Fetch API 的网络请求生命周期可以被抽象为一个多阶段的状态机。它不像一个简单的函数调用那样立即返回结果,而是在后台通过一系列的异步操作来完成。
从高层来看,这个过程大致遵循以下步骤:
- 请求初始化: 客户端构建
Request对象(或由fetch()内部构建)。 - 请求调度与发送: 浏览器将请求发送到网络层。
- 网络传输: 请求在网络中传输,到达服务器。
- 服务器处理与响应: 服务器处理请求并生成响应。
- 响应头部接收: 浏览器接收到响应的头部信息。
- 响应体读取: 浏览器开始接收并读取响应体数据。
- 数据解析与交付: 响应体数据被解析成应用程序可用的格式。
Fetch API 的 Promise 机制将这些阶段划分为两个主要的异步操作:
- 第一个 Promise:
fetch()方法本身返回的 Promise。它在收到响应头部时解析,提供一个Response对象。 - 第二个 Promise:
Response对象上体读取方法(如json(),text()等)返回的 Promise。它在整个响应体被完全读取并解析后解析,提供最终的数据。
这种两阶段的 Promise 结构,正是 Fetch API 精妙之处,它允许开发者在知道请求是否成功(通过响应状态码)的同时,异步地处理潜在的大型响应体。
阶段一:请求的构建与调度
Fetch API 的旅程始于一个请求。这个请求可以是简单的 URL 字符串,也可以是一个经过详细配置的 Request 对象。
1.1 Request 对象的创建与配置
你可以通过 fetch() 函数直接传入 URL 和一个可选的 options 对象来发起请求:
// 最简单的 GET 请求
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
或者,为了更精细地控制请求,你可以显式地创建一个 Request 对象:
const requestUrl = 'https://api.example.com/users';
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_AUTH_TOKEN'
},
body: JSON.stringify({
name: 'Alice',
email: '[email protected]'
}),
mode: 'cors', // 跨域请求模式
cache: 'no-cache', // 不使用缓存
credentials: 'omit', // 不发送 cookies
signal: new AbortController().signal // 用于取消请求的信号
};
const myRequest = new Request(requestUrl, requestOptions);
fetch(myRequest)
.then(response => {
// ... 处理响应
})
.catch(error => {
// ... 处理错误
});
Request 对象的属性定义了请求的所有方面:
url: 请求的目标 URL。method: HTTP 方法(GET, POST, PUT, DELETE 等)。headers: 一个Headers对象,包含所有请求头。body: 请求体,可以是USVString(UTF-8 字符串),Blob,BufferSource,FormData,URLSearchParams,ReadableStream。mode: 请求模式(cors,no-cors,same-origin,navigate),影响 CORS 策略。cache: 缓存模式(default,no-store,reload,no-cache,force-cache,only-if-cached)。credentials: 凭证模式(omit,same-origin,include),控制是否发送 cookies。redirect: 重定向模式(follow,error,manual)。referrer: referrer 值。referrerPolicy: referrer 策略。integrity: 子资源完整性。keepalive: 允许请求在页面卸载时继续发送。signal:AbortSignal对象,用于取消请求。
1.2 fetch() 调用与 Promise 状态初始化
当你调用 fetch(myRequest) 或 fetch(url, options) 时,Fetch API 的内部状态机开始工作。
- Promise 返回:
fetch()函数会立即返回一个Promise对象。此时,这个Promise的状态是pending。 - 请求调度: 浏览器会将这个请求交给其底层的网络栈处理。这包括:
- DNS 解析: 将域名解析为 IP 地址。
- TCP 连接建立: 与服务器建立 TCP 连接(三次握手)。
- TLS 握手: 如果是 HTTPS 请求,进行 TLS 握手以建立加密通道。
- 发送请求行和头部: 构造 HTTP 请求行(方法、路径、协议版本)和所有请求头部,并通过网络发送。
- 发送请求体: 如果请求包含体(如 POST、PUT 请求),请求体数据会开始通过网络流式传输到服务器。
在此阶段,如果发生任何网络级别的错误(例如,DNS 解析失败、无法建立 TCP 连接、CORS 预检请求失败等),fetch() 返回的 Promise 将会立即进入 rejected 状态,并带有一个 TypeError(或更具体的错误,如 NetworkError)。
状态转换:
| 当前状态 | 事件/动作 | 下一状态 | 描述 |
|---|---|---|---|
| 初始 | fetch() 被调用 |
Request Pending | 返回一个 pending 状态的 Promise。 |
| Request Pending | 网络错误发生 | Rejected (网络错误) | 例如 DNS 失败、连接超时、CORS 预检失败。 |
fetch('https://nonexistent-domain-12345.com/data')
.then(response => console.log('This will not be logged'))
.catch(error => {
console.error('Fetch request failed:', error.name, error.message);
// 示例输出: "Fetch request failed: TypeError Failed to fetch"
});
阶段二:网络交互与响应头部接收
一旦请求成功调度,它就在网络中传输。服务器接收到请求,进行处理,并开始发送响应。
2.1 服务器处理与响应发送
服务器在接收到完整的请求(包括请求体)后,执行其业务逻辑,例如查询数据库、进行计算等,然后构建一个 HTTP 响应。这个响应包含:
- 状态行: HTTP 协议版本、状态码(如 200 OK, 404 Not Found, 500 Internal Server Error)和状态文本。
- 响应头部: 各种元数据,如
Content-Type、Content-Length、Cache-Control、Set-Cookie等。 - 响应体: 实际的数据负载(例如 JSON 字符串、HTML 文档、图片二进制数据)。
2.2 响应头部的接收与 Promise 解析
Fetch API 的一个关键设计点是:fetch() 返回的 Promise 会在浏览器接收到响应的 HTTP 头部时立即解析。这意味着在整个响应体数据到达之前,你就可以访问 Response 对象。
当 Promise 解析时,它会提供一个 Response 对象,该对象包含:
status: HTTP 状态码(例如 200, 404, 500)。statusText: 状态文本(例如 "OK", "Not Found", "Internal Server Error")。ok: 一个布尔值,表示响应是否成功(true表示状态码在 200-299 范围内)。这是判断 HTTP 级别成功与否的便捷方式。headers: 一个Headers对象,可以用于查询响应头。url: 响应的最终 URL(可能因重定向而改变)。type: 响应类型(例如basic,cors,opaque)。body: 一个ReadableStream对象,代表响应体的数据流。此时,响应体可能还未完全下载。
重要提示: 即使 HTTP 状态码表示错误(例如 404 Not Found 或 500 Internal Server Error),fetch() 返回的 Promise 仍然会解析(即进入 fulfilled 状态),而不是拒绝。你需要手动检查 response.ok 或 response.status 来判断请求的业务逻辑是否成功。
状态转换:
| 当前状态 | 事件/动作 | 下一状态 | 描述 |
|---|---|---|---|
| Request Pending | 服务器发送响应头部 | Response Headers Ready | fetch() Promise 解析,提供一个 Response 对象。 |
Response 对象包含状态码、头部等信息,但响应体尚未完全下载。 |
fetch('https://api.example.com/status/404') // 假设这是一个返回 404 的端点
.then(response => {
console.log('Response Headers Ready!');
console.log('Status:', response.status); // 404
console.log('Status Text:', response.statusText); // Not Found
console.log('OK:', response.ok); // false
console.log('Content-Type:', response.headers.get('Content-Type'));
if (!response.ok) {
// 即使是 404,Promise 也已解析,但我们仍需处理 HTTP 错误
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json(); // 继续处理响应体
})
.then(data => {
console.log('Data:', data);
})
.catch(error => {
console.error('Caught error:', error.message); // "Caught error: HTTP error! Status: 404"
});
fetch('https://api.example.com/data') // 假设这是一个返回 200 OK 的端点
.then(response => {
console.log('Response Headers Ready for 200 OK!');
console.log('Status:', response.status); // 200
console.log('OK:', response.ok); // true
return response.json(); // 继续处理响应体
})
.then(data => console.log('Data:', data))
.catch(error => console.error('Error:', error));
阶段三:响应体的读取与数据处理
一旦 fetch() Promise 解析并提供了 Response 对象,下一步就是读取其体数据。Response 对象实现了 Body 混入,提供了一系列方便的方法来处理不同类型的响应体。
3.1 Body 混入及其方法
Body 混入提供了以下异步方法,每个方法都返回一个 Promise:
response.json(): 将响应体解析为 JSON 对象。如果响应体不是有效的 JSON,返回的 Promise 将被拒绝。response.text(): 将响应体解析为纯文本字符串。response.blob(): 将响应体解析为Blob对象。适用于处理二进制数据,如图片、文件。response.arrayBuffer(): 将响应体解析为ArrayBuffer对象。适用于更底层的二进制数据操作。response.formData(): 将响应体解析为FormData对象。适用于处理表单数据。
这些方法有一个关键特性:它们只能被调用一次。一旦你调用了其中一个方法,响应体就被“消费”了。再次调用会导致错误。这是因为响应体是一个数据流,一旦读取,数据就流过了。
状态转换:
| 当前状态 | 事件/动作 | 下一状态 | 描述 |
|---|---|---|---|
| Response Headers Ready | 调用 response.json()/text() 等 |
Body Reading Pending | 返回一个新的 Promise,等待整个响应体被读取和解析。 |
| Body Reading Pending | 数据流开始传输 | Body Reading In Progress | 浏览器从网络中接收响应体数据块。 |
| Body Reading In Progress | 整个响应体接收完成并解析 | Fulfilled (数据可用) | 体读取 Promise 解析,提供解析后的数据(JSON, 文本等)。 |
| 解析错误发生 | Rejected (解析错误) | 例如,调用 json() 但响应体不是有效的 JSON。 |
|
| 网络断开/超时(在下载期间) | Rejected (网络错误) | 在下载响应体数据时发生网络中断。 |
3.2 异步体读取的详细流程
当你调用 response.json() 或其他体读取方法时,会发生以下情况:
- 返回新的 Promise: 该方法立即返回一个新的
Promise,其状态为pending。 - 启动体下载: 浏览器开始从网络连接中下载剩余的响应体数据。这个过程是异步进行的,可能需要一些时间,特别是对于大型文件。
- 数据缓冲与解析: 下载的数据被缓冲起来。一旦所有数据都被接收,并且根据所选的方法(
json(),text()等)进行了解析,体读取的 Promise 就会进入fulfilled状态,并将其解析后的数据作为值返回。 - 解析错误: 如果在解析过程中发生错误(例如,
response.json()尝试解析一个非 JSON 格式的响应),体读取的 Promise 将会进入rejected状态,并带有一个相应的错误。
fetch('https://api.example.com/users/1')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 此时 response.body 是一个 ReadableStream,数据还在下载中
return response.json(); // 返回一个新的 Promise,开始读取和解析 JSON
})
.then(userData => {
// 只有当整个 JSON 体被下载并成功解析后,这个 then 块才会被执行
console.log('User Data:', userData);
console.log('User Name:', userData.name);
})
.catch(error => {
console.error('Error fetching or parsing user data:', error);
});
3.3 处理不同类型的响应体
JSON 数据:
// POST 请求发送 JSON 并接收 JSON 响应
fetch('https://api.example.com/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: 'New Product', price: 99.99 })
})
.then(response => {
if (!response.ok) {
// 检查 HTTP 状态码,抛出错误以进入 catch 块
return response.json().then(err => { throw new Error(err.message || 'Server error'); });
}
return response.json(); // 解析响应体为 JSON
})
.then(product => {
console.log('Created product:', product);
})
.catch(error => {
console.error('Failed to create product:', error.message);
});
文本数据:
fetch('https://api.example.com/readme.txt')
.then(response => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.text(); // 解析响应体为纯文本
})
.then(textData => {
console.log('README content:', textData);
})
.catch(error => console.error('Error fetching text:', error));
二进制数据 (Blob):
// 假设 'https://api.example.com/image.jpg' 返回一张图片
fetch('https://api.example.com/image.jpg')
.then(response => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.blob(); // 解析响应体为 Blob
})
.then(imageBlob => {
const imageUrl = URL.createObjectURL(imageBlob);
const imgElement = document.createElement('img');
imgElement.src = imageUrl;
document.body.appendChild(imgElement);
console.log('Image loaded as Blob:', imageBlob);
// 记得在不再需要时释放 URL 对象
// URL.revokeObjectURL(imageUrl);
})
.catch(error => console.error('Error fetching image:', error));
流式处理 (使用 ReadableStream):
对于非常大的响应体,或者需要实时处理数据流的场景,可以直接使用 response.body 这个 ReadableStream 对象。这允许你分块读取数据,而不需要等待整个响应体下载完成。
async function fetchAndProcessStream() {
try {
const response = await fetch('https://api.example.com/large-data-stream'); // 假设此端点返回一个大文件或数据流
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let receivedLength = 0;
let chunks = [];
// 循环读取数据块
while (true) {
const { done, value } = await reader.read(); // value 是 Uint8Array
if (done) {
console.log('Stream reading complete.');
break;
}
chunks.push(value);
receivedLength += value.length;
console.log(`Received ${receivedLength} bytes. Current chunk size: ${value.length}`);
// 你可以在这里对每个数据块进行实时处理
// 例如,解码并显示部分文本:
// console.log('Partial data:', decoder.decode(value, { stream: true }));
}
// 将所有数据块合并并解码为最终字符串
let fullText = '';
for (const chunk of chunks) {
fullText += decoder.decode(chunk, { stream: true });
}
fullText += decoder.decode(new Uint8Array(), { stream: false }); // 刷新 decoder 缓冲区
console.log('Full stream content length:', fullText.length);
// console.log('Full stream content:', fullText); // 打印完整内容 (可能非常大)
} catch (error) {
console.error('Error fetching or processing stream:', error);
}
}
fetchAndProcessStream();
使用 ReadableStream 提供了更细粒度的控制,但同时也增加了代码的复杂性。对于大多数场景,json(), text() 等简便方法已足够。
阶段四:错误处理与请求取消
健壮的网络请求需要完善的错误处理。Fetch API 的 Promise 机制使得错误处理变得直观。
4.1 错误类型与处理
在 Fetch API 的整个生命周期中,可能遇到多种类型的错误:
-
网络错误 (
TypeError/NetworkError):- 发生在请求调度或网络传输阶段。
- 例如:DNS 解析失败、无网络连接、CORS 预检请求失败、请求被用户代理阻止、服务器无响应。
fetch()返回的 Promise 会直接进入rejected状态。- 错误对象通常是
TypeError或NetworkError。
fetch('https://nonexistent-domain-12345.com/data') .then(response => console.log('Success (should not happen)')) .catch(error => { console.error('Network-level error:', error.message); // "Network-level error: Failed to fetch" }); -
HTTP 错误 (4xx, 5xx 状态码):
- 发生在响应头部接收阶段。
- 例如:404 Not Found, 401 Unauthorized, 500 Internal Server Error。
fetch()返回的 Promise 不会拒绝,而是解析为一个Response对象。- 你需要手动检查
response.ok属性或response.status状态码来判断。
fetch('https://api.example.com/nonexistent-resource') .then(response => { if (!response.ok) { // 明确抛出错误,以便后续的 .catch 能够捕获 throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => console.log('Data:', data)) .catch(error => { console.error('Application-level HTTP error:', error.message); // "Application-level HTTP error: HTTP error! status: 404" }); -
解析错误:
- 发生在响应体读取和解析阶段。
- 例如:调用
response.json()尝试解析一个非 JSON 格式的响应体。 response.json()(或text(),blob()等) 返回的 Promise 会进入rejected状态。
// 假设这个端点返回纯文本 "Hello World" 而不是 JSON fetch('https://api.example.com/hello-text') .then(response => { if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return response.json(); // 尝试解析非 JSON 数据 }) .then(data => console.log('Data:', data)) .catch(error => { console.error('Parsing error:', error.name, error.message); // "Parsing error: SyntaxError Unexpected token 'H', "Hello World" is not valid JSON" }); -
请求取消错误 (
AbortError):- 当使用
AbortController取消正在进行的 Fetch 请求时发生。 fetch()返回的 Promise 会进入rejected状态,错误对象是DOMException,其name属性为'AbortError'。
- 当使用
4.2 使用 AbortController 取消请求
AbortController 接口提供了一种取消一个或多个 Web 请求的机制。它通过一个 AbortSignal 对象与 fetch() 请求关联。
- 创建
AbortController实例:const controller = new AbortController(); const signal = controller.signal; - 将
signal传递给fetch():fetch('https://api.example.com/long-task', { signal: signal }) .then(response => { /* ... */ }) .catch(error => { if (error.name === 'AbortError') { console.log('Fetch request was aborted.'); } else { console.error('Fetch error:', error); } }); - 调用
controller.abort()取消请求:// 假设 500ms 后取消请求 setTimeout(() => { controller.abort(); console.log('Attempting to abort fetch...'); }, 500);
完整的错误处理和取消示例:
const controller = new AbortController();
const signal = controller.signal;
async function performFetchWithCancellation() {
try {
console.log('Starting fetch request...');
const response = await fetch('https://api.example.com/data', { signal }); // 使用 signal
if (!response.ok) {
// 尝试读取服务器返回的错误信息,如果有的话
const errorBody = await response.text();
throw new Error(`HTTP error! Status: ${response.status}, Details: ${errorBody}`);
}
const data = await response.json();
console.log('Data received successfully:', data);
} catch (error) {
if (error.name === 'AbortError') {
console.warn('Fetch request was aborted by AbortController.');
} else if (error instanceof TypeError) { // 或 NetworkError
console.error('Network error (e.g., no internet, CORS):', error.message);
} else if (error instanceof SyntaxError) { // 例如 JSON 解析失败
console.error('Data parsing error:', error.message);
} else {
console.error('An unexpected error occurred:', error.message);
}
}
}
// 启动请求
performFetchWithCancellation();
// 模拟在请求进行中取消
setTimeout(() => {
controller.abort();
console.log('Request cancellation initiated.');
}, 200); // 假设 200ms 后取消
Fetch API 内部状态机:综合视图表格
为了更清晰地展示 Fetch API 的内部状态转换,我们可以用一个表格来概括:
| 阶段/状态 | 事件/动作 | 下一状态 | Promise 返回状态 | 描述 |
|---|---|---|---|---|
| 初始 | fetch() 被调用 |
Request Pending | Promise<Response> (pending) |
fetch() 函数被调用,立即返回一个 pending 状态的 Promise。浏览器开始构建和调度请求。 |
| Request Pending | 1. 网络连接建立成功 | Response Headers Ready | Promise<Response> (fulfilled) |
DNS 解析、TCP/TLS 握手、请求发送完成。服务器响应头部被接收。fetch() Promise 解析。 |
| 2. 网络错误发生 | Rejected (网络错误) | Promise<Response> (rejected) |
DNS 失败、连接超时、CORS 预检失败、无网络连接。fetch() Promise 被拒绝(TypeError/NetworkError)。 |
|
3. controller.abort() |
Rejected (AbortError) | Promise<Response> (rejected) |
请求被 AbortController 明确取消。fetch() Promise 被拒绝 (AbortError)。 |
|
| Response Headers Ready | 1. 业务逻辑检查 response.ok |
Body Reading Pending | Promise<DataType> (pending) |
fetch() Promise 已解析。开发者调用 response.json()/text() 等方法,返回一个新的 pending Promise。 |
2. 业务逻辑判断失败(如 !response.ok)并抛出错误 |
Rejected (HTTP Error) | Promise<DataType> (rejected) |
开发者手动检查 response.status 或 response.ok 后抛出自定义错误。 |
|
| Body Reading Pending | 1. 响应体数据开始传输 | Body Reading In Progress | Promise<DataType> (pending) |
浏览器开始从网络中接收响应体的第一个数据块。 |
| Body Reading In Progress | 1. 整个响应体接收完成并解析 | Fulfilled (数据可用) | Promise<DataType> (fulfilled) |
所有响应体数据已被下载,并成功解析为指定的数据类型。体读取 Promise 解析。 |
| 2. 解析错误发生 | Rejected (解析错误) | Promise<DataType> (rejected) |
响应体数据与所选的解析方法不匹配(如 json() 解析非 JSON)。体读取 Promise 被拒绝 (SyntaxError)。 |
|
| 3. 网络断开/超时(下载期间) | Rejected (网络错误) | Promise<DataType> (rejected) |
在下载响应体数据过程中发生网络中断或超时。体读取 Promise 被拒绝 (TypeError/NetworkError)。 |
|
| Fulfilled (数据可用) | 数据已准备好被应用程序使用 | 终端状态 | – | 应用程序可以安全地使用获取到的数据。 |
| Rejected (错误) | 错误已发生 | 终端状态 | – | 请求或数据处理失败,应用程序需要处理错误。 |
结论与展望
Fetch API 提供了一个强大且现代化的方式来处理 Web 请求。其内部状态机,通过 Promise 驱动的异步流程,将请求的发送、响应头部的接收和响应体的读取清晰地分离开来。这种分阶段处理机制,不仅提升了网络请求的灵活性和效率,也为开发者提供了细粒度的控制,从而能够构建出更具弹性、响应更快的 Web 应用程序。
理解 Fetch API 的状态转换和错误处理机制,是利用其全部潜力的基石。通过恰当的错误检查和请求取消策略,我们可以确保应用在面对各种网络条件和服务器响应时,都能保持稳定和用户友好。随着 Web 平台的不断演进,Fetch API 及其核心概念将继续作为构建高性能、高可靠性 Web 体验的关键组成部分。