1. 项目概述:Fetch API 不是“另一个 AJAX 库”,而是浏览器原生的网络通信新范式
你有没有在调试一个前端页面时,发现控制台里反复出现
XMLHttpRequest
的 deprecated 警告?或者在写一个简单的数据加载逻辑时,被 jQuery 的
$.ajax()
那堆 success/error/complete 回调嵌套绕得头晕?又或者,在用 Axios 时突然意识到——等等,我到底为什么要为一个 GET 请求引入 12KB 的第三方库?这些不是偶然的困惑,而是 JavaScript 网络请求演进到临界点的真实信号。
Fetch API
就是在这个节点上被 W3C 标准化、被所有现代浏览器原生支持的核心能力。它不是语法糖,不是封装层,而是浏览器直接暴露给开发者的、基于 Promise 的底层网络抽象。标题里那个看似平平无奇的 “Cómo usar la API Fetch de JavaScript para obtener datos”(如何使用 JavaScript 的 Fetch API 获取数据),背后其实是一场从“模拟 HTTP”到“真正操作 HTTP”的范式迁移。它解决的远不止是“怎么发个请求”这个表层问题,而是彻底重构了前端开发者与服务器对话的方式:用声明式代替命令式,用链式处理代替回调地狱,用标准化的 Request/Response 对象代替黑盒式的字符串拼接。适合谁?如果你还在用
XMLHttpRequest
手动设置
open()
、
setRequestHeader()
、监听
onreadystatechange
,或者你只是把
fetch()
当成
axios.get()
的简写版来用,那这篇就是为你写的。它不预设你熟悉 Promise 或 async/await,但会带你从第一行
fetch('https://api.example.com/users')
开始,拆开每一个
.then()
的内部齿轮,告诉你为什么
.catch()
捕获不到 404 错误,为什么
response.json()
本身就是一个 Promise,以及为什么一个看似简单的 GET 请求,背后牵扯着 CORS、重定向、缓存策略、流式响应等一整套 Web 基础设施。这不是一份 API 文档的翻译,而是一个有十年全栈经验的老手,在真实项目中踩过上百次 fetch 坑之后,把那些藏在 MDN 文档角落里的“注意事项”和“实操陷阱”,全部摊开在你面前。
2. 核心设计思路与方案选型:为什么 Fetch 是当前最合理的选择,而非历史的简单重复
2.1 从 XMLHttpRequest 到 Fetch:一次底层抽象的升维
理解 Fetch 的价值,必须先看清它的前任——
XMLHttpRequest
(XHR)。很多人以为 XHR 是“老古董”,但它的设计哲学至今仍有深刻影响。XHR 的核心是一个状态机:
UNSENT → OPENED → HEADERS_RECEIVED → LOADING → DONE
。你必须手动调用
open()
初始化连接,
send()
发送数据,然后通过监听
onreadystatechange
事件,在
readyState === 4
且
status >= 200 && status < 300
时,才敢去读取
responseText
。这个过程充满了“仪式感”,但也带来了巨大的认知负担和出错空间。比如,忘记检查
status
就直接解析 JSON,结果服务端返回 500 错误,前端却抛出
SyntaxError: Unexpected token < in JSON at position 0
(因为返回的是 HTML 错误页);又或者,在
send()
之前忘了调用
setRequestHeader()
设置
Content-Type
,导致后端无法正确解析 POST 数据。Fetch API 的设计,本质上是对这一整套流程的“语义升维”。它不再让你操作一个状态机,而是让你构造一个
Request 对象
(描述“我要做什么”)和一个
Response 对象
(描述“我得到了什么”)。
fetch()
函数本身只做一件事:接收一个 Request,返回一个 Promise,这个 Promise 最终 resolve 为一个 Response。这个设计剥离了所有与“如何传输”相关的细节(如连接复用、DNS 缓存、TCP 握手),将开发者完全聚焦于“请求什么”和“如何处理响应”这两个核心问题上。这就像从手动挡汽车(XHR)切换到自动挡(Fetch):你不再需要关心离合器和换挡时机,只需专注油门和方向盘。这种抽象带来的直接好处,就是代码的可读性和可维护性呈指数级提升。一个典型的 XHR GET 请求可能需要 15 行代码,而等价的 Fetch 只需 3 行,并且逻辑清晰无比。
2.2 为什么不是 Axios 或其他封装库?一个关于“责任边界”的严肃讨论
看到这里,你可能会问:既然 Axios 这样的库已经如此成熟,为什么还要费劲去学原生 Fetch?这是一个极其关键的问题,答案关乎一个前端工程师的职业素养。Axios 等库的价值毋庸置疑,它们提供了拦截器、请求/响应转换、自动 JSON 序列化、取消请求等高级功能。但这些功能,恰恰是 Fetch 的“责任之外”。Fetch 的定位非常明确:它是一个
符合 WHATWG Fetch 标准的、浏览器原生的、最小可行的网络请求原语
。它的职责边界就是:发送 HTTP 请求,接收 HTTP 响应,将原始字节流包装成结构化的 Response 对象。任何超出这个边界的逻辑——比如自动将
data
对象序列化为
application/json
并设置
Content-Type
头,或者在响应体是 JSON 时自动调用
response.json()
——都是由上层应用或封装库来完成的。这带来了一个根本性的优势:
可控性与可预测性
。当你在生产环境遇到一个诡异的网络问题时,如果使用的是 Axios,你首先要排查的是 Axios 的拦截器是否篡改了请求头,它的默认超时设置是否干扰了你的业务逻辑,它的错误处理机制是否掩盖了真实的网络异常。而如果你直接使用 Fetch,整个调用栈是透明的、扁平的:
fetch() -> 浏览器内核 -> 网络层
。没有中间商赚差价,没有黑盒逻辑干扰。我在一个金融类后台系统中就曾遇到过这样的案例:用户反馈某个报表导出接口偶尔失败,但日志显示后端一切正常。最终排查发现,是 Axios 的默认
timeout
设置为 0(无限等待),而某个 CDN 节点在特定条件下会静默丢弃连接,导致前端永远卡在
pending
状态。换成原生 Fetch 后,我们显式设置了
signal
和
AbortController
,问题立刻暴露并得到解决。因此,学习 Fetch 并非为了取代 Axios,而是为了掌握那个“不可替代的底层”。它让你在需要极致控制、需要深度调试、或者需要构建自己专属请求框架时,拥有绝对的话语权。
2.3 Promise 作为基石:为什么异步处理模型决定了整个生态的走向
Fetch API 的另一个革命性设计,是它
原生拥抱 Promise
。这绝非一个简单的语法糖替换。Promise 的引入,从根本上改变了 JavaScript 异步编程的范式。在 XHR 时代,错误处理是割裂的:网络错误(如 DNS 失败、连接超时)会触发
onerror
事件,而 HTTP 错误(如 404、500)则需要你在
onload
里手动检查
status
。这种割裂导致错误处理逻辑分散、难以统一。Fetch 将这一切收束到 Promise 的
reject
和
resolve
两条路径上。一个
fetch()
调用,只有在网络层面完全失败(如域名无法解析、连接被拒绝)时,才会 reject 一个
TypeError
。而只要请求成功发出并收到了服务器的响应(哪怕状态码是 500),Promise 就会 resolve,返回一个
Response
对象。这个设计看似反直觉,实则精妙。它强制开发者将“网络可达性”和“业务逻辑正确性”这两个不同维度的问题分开处理。前者是基础设施问题,后者是应用层问题。这为后续的错误分类、监控埋点、用户体验优化(如区分“服务器挂了”和“数据不存在”)提供了坚实的基础。更重要的是,Promise 的链式调用(
.then().catch().finally()
)天然支持函数式编程思想。你可以轻松地将“请求 -> 解析 JSON -> 过滤数据 -> 渲染 UI”这一系列操作,写成一条清晰的数据流,而不是嵌套的回调。这种表达力,是 XHR 时代无法想象的。当然,Promise 也带来了新的挑战,比如“
.catch()
捕获不到 404”这个经典误区,这正是我们需要在后续章节深入剖析的核心细节。
3. 核心细节解析与实操要点:GET 与 POST 的完整实现,以及那些文档里不会明说的坑
3.1 GET 请求:从最简形式到生产环境的完整配置
最基础的 GET 请求,一行代码就能搞定:
fetch('https://jsonplaceholder.typicode.com/posts/1')
但这行代码在生产环境中是“危险”的。它隐含了太多未经声明的默认行为。让我们把它展开,逐层剖析:
第一步:理解默认行为
-
method: 默认为'GET',无需显式指定。 -
headers: 默认为空对象{},这意味着Accept头是浏览器默认值(通常是*/*),Content-Type头不存在(GET 请求本就不该有请求体)。 -
mode: 默认为'cors',这是 Fetch 的安全基石。它要求服务器必须在响应头中包含Access-Control-Allow-Origin,否则浏览器会阻止 JavaScript 读取响应内容。这是对同源策略的强化,而非削弱。 -
cache: 默认为'default',它会遵循 HTTP 缓存规则(如Cache-Control和Expires头)。对于一个获取用户信息的接口,这通常是期望的行为;但对于一个实时股票价格接口,你可能需要cache: 'no-store'来强制跳过所有缓存。
第二步:添加查询参数——不要手动拼接! 新手最容易犯的错误,就是像这样拼接 URL:
// ❌ 危险!容易产生 XSS 和编码错误
const userId = "123&name=test";
const url = `https://api.example.com/user?id=${userId}`;
fetch(url);
正确的做法是使用
URLSearchParams
:
// ✅ 安全、标准、可读性强
const params = new URLSearchParams();
params.append('id', '123');
params.append('name', 'test');
params.append('sort', 'date');
const url = `https://api.example.com/user?${params.toString()}`;
// 结果: https://api.example.com/user?id=123&name=test&sort=date
fetch(url);
URLSearchParams
会自动对所有值进行
encodeURIComponent()
,确保特殊字符(如空格、
&
、
=
)被正确转义,从根本上杜绝了 URL 注入风险。
第三步:处理响应——
.json()
是一个 Promise!
这是 Fetch 学习中最大的认知拐点。
response.json()
方法本身
返回一个 Promise
,而不是直接返回解析好的 JSON 对象。
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => {
console.log(response.status); // 200
console.log(response.ok); // true (status 在 200-299 范围内)
console.log(response.headers.get('content-type')); // application/json; charset=utf-8
// ❌ 错误!response.json() 返回的是 Promise,不是数据
// const data = response.json();
// ✅ 正确!必须再次 .then() 或 await
return response.json();
})
.then(data => {
console.log(data); // { id: 1, title: "...", ... }
});
提示:
response.ok是一个布尔值,比手动检查response.status >= 200 && response.status < 300更简洁、更语义化。它应该成为你处理响应状态的第一道判断。
3.2 POST 请求:从表单提交到 JSON API 的全流程
POST 请求比 GET 复杂得多,因为它涉及请求体(body)的构造和序列化。Fetch 对此提供了高度的灵活性,但也要求你承担更多的责任。
场景一:提交 HTML 表单(
application/x-www-form-urlencoded
)
这是传统 Web 表单的提交方式。
const formData = new FormData();
formData.append('username', 'john_doe');
formData.append('email', 'john@example.com');
fetch('/login', {
method: 'POST',
// ⚠️ 关键:不要手动设置 Content-Type!
// 浏览器会根据 FormData 自动设置为 multipart/form-data 或 application/x-www-form-urlencoded
body: formData
})
.then(response => response.json())
.then(data => console.log(data));
注意:当
body是FormData时, 绝对不要 手动设置headers: { 'Content-Type': '...' }。浏览器会根据FormData的内容自动选择最合适的Content-Type(可能是multipart/form-data,也可能是application/x-www-form-urlencoded),并生成相应的boundary。手动设置会破坏这个自动机制,导致后端无法解析。
场景二:调用 RESTful JSON API(
application/json
)
这是现代前后端分离架构中最常见的场景。
const userData = {
username: 'john_doe',
email: 'john@example.com',
preferences: { theme: 'dark', notifications: true }
};
fetch('/api/users', {
method: 'POST',
// ✅ 必须显式设置 headers
headers: {
'Content-Type': 'application/json'
},
// ✅ 必须将对象序列化为 JSON 字符串
body: JSON.stringify(userData)
})
.then(response => {
if (!response.ok) {
// ✅ 这里处理 HTTP 错误(400, 401, 500 等)
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => console.log('User created:', data))
.catch(error => {
// ✅ 这里处理网络错误(TypeError)和上面 throw 的错误
console.error('Fetch failed:', error);
});
提示:
JSON.stringify()是一个强大的工具,但它也有局限。例如,它会忽略undefined值、函数和Symbol。如果你的userData对象中包含了这些,你需要一个更健壮的序列化方案,比如使用JSON.stringify(userData, (key, value) => value === undefined ? null : value)进行预处理。
3.3 错误处理的黄金法则:网络错误、HTTP 错误与业务错误的三层防御
Fetch 的错误处理是其最易混淆的部分,也是最能体现你是否真正理解它的试金石。我们必须建立一个清晰的三层防御模型:
| 层级 | 触发条件 | 如何捕获 | 典型错误 |
|---|---|---|---|
| L1:网络错误 | 请求根本未发出(DNS 失败、连接被拒、SSL 证书错误) |
.catch()
|
TypeError: Failed to fetch
|
| L2:HTTP 错误 | 请求已发出,服务器返回了非成功状态码(4xx, 5xx) |
if (!response.ok) { throw ... }
|
HTTP error! status: 404
|
| L3:业务错误 |
服务器返回了 200,但响应体中的业务字段表明操作失败(如
{ success: false, message: "余额不足" }
)
|
data.success === false
|
"余额不足"
|
一个健壮的 Fetch 封装函数,必须同时覆盖这三层:
async function safeFetch(url, options = {}) {
try {
const response = await fetch(url, options);
// L2: HTTP 错误
if (!response.ok) {
const errorData = await response.json(); // 尝试读取错误详情
throw new HttpError(
`HTTP ${response.status} ${response.statusText}`,
response.status,
errorData
);
}
// L3: 业务错误(假设 API 返回 { code: 0, data: {...} } 格式)
const data = await response.json();
if (data.code !== 0) {
throw new BusinessError(data.message || '未知业务错误', data.code);
}
return data.data;
} catch (error) {
// L1: 网络错误,或上面 throw 的任何错误
if (error instanceof TypeError) {
// 网络错误
console.error('网络连接失败,请检查您的网络设置。');
} else if (error instanceof HttpError) {
// HTTP 错误
console.error(`服务器返回错误:${error.message}`);
} else if (error instanceof BusinessError) {
// 业务错误
console.error(`操作失败:${error.message}`);
} else {
// 其他未预期错误
console.error('发生未知错误:', error);
}
throw error; // 重新抛出,让调用者决定如何处理
}
}
// 使用示例
safeFetch('/api/payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 100 })
})
.then(result => console.log('支付成功!'))
.catch(() => console.log('支付流程已终止'));
4. 实操过程与核心环节实现:从零开始构建一个可复用的 Fetch 工具库
4.1 构建基础请求函数:
request()
的骨架设计
一个生产级的 Fetch 工具库,其核心是一个灵活、可扩展的
request()
函数。它不应该只是一个
fetch()
的简单包装,而应该是一个“请求工厂”,能够根据传入的配置,动态生成符合需求的 Request 对象,并处理通用的生命周期逻辑。
/**
* @typedef {Object} RequestOptions
* @property {string} url - 请求 URL
* @property {string} [method='GET'] - HTTP 方法
* @property {Object} [headers={}] - 请求头
* @property {any} [body] - 请求体(会被自动序列化)
* @property {string} [contentType='json'] - 请求体类型 ('json', 'form', 'text', 'blob')
* @property {number} [timeout=10000] - 超时时间(毫秒)
* @property {boolean} [credentials=true] - 是否携带 Cookie
*/
/**
* 核心请求函数
* @param {RequestOptions} options
* @returns {Promise<any>}
*/
async function request(options) {
const {
url,
method = 'GET',
headers = {},
body,
contentType = 'json',
timeout = 10000,
credentials = true
} = options;
// Step 1: 构造请求头
const finalHeaders = { ...headers };
if (contentType === 'json' && method !== 'GET' && method !== 'HEAD') {
finalHeaders['Content-Type'] = 'application/json';
} else if (contentType === 'form' && method !== 'GET' && method !== 'HEAD') {
// 对于 form 类型,我们不设置 Content-Type,让浏览器自动处理
}
// Step 2: 序列化请求体
let finalBody = body;
if (body !== undefined && body !== null) {
if (contentType === 'json') {
finalBody = JSON.stringify(body);
} else if (contentType === 'form') {
finalBody = new URLSearchParams(body).toString();
// 如果 body 是一个 FormData 对象,则保持原样
if (body instanceof FormData) {
finalBody = body;
}
}
}
// Step 3: 创建 AbortController 用于超时和取消
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
method,
headers: finalHeaders,
body: finalBody,
credentials: credentials ? 'include' : 'omit',
signal: controller.signal
});
clearTimeout(timeoutId);
// Step 4: 基础响应检查
if (!response.ok) {
const errorData = await response.json();
throw new Error(`HTTP ${response.status}: ${response.statusText}. ${JSON.stringify(errorData)}`);
}
// Step 5: 根据响应头自动解析响应体
const contentTypeHeader = response.headers.get('content-type') || '';
if (contentTypeHeader.includes('application/json')) {
return await response.json();
} else if (contentTypeHeader.includes('text/')) {
return await response.text();
} else if (contentTypeHeader.includes('image/') || contentTypeHeader.includes('application/octet-stream')) {
return await response.blob();
} else {
return await response.text(); // 默认回退
}
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`请求超时(${timeout}ms)`);
}
throw error;
}
}
4.2 封装常用方法:
get()
,
post()
,
put()
,
delete()
的语义化
基于
request()
这个强大内核,我们可以轻松地封装出语义清晰、开箱即用的快捷方法。这不仅能提升开发效率,更能增强代码的可读性。
// GET 请求:通常用于获取资源
function get(url, options = {}) {
return request({
url,
method: 'GET',
...options
});
}
// POST 请求:通常用于创建资源
function post(url, data, options = {}) {
return request({
url,
method: 'POST',
body: data,
contentType: 'json',
...options
});
}
// PUT 请求:通常用于完全更新资源
function put(url, data, options = {}) {
return request({
url,
method: 'PUT',
body: data,
contentType: 'json',
...options
});
}
// DELETE 请求:通常用于删除资源
function del(url, options = {}) {
return request({
url,
method: 'DELETE',
...options
});
}
// 专门用于表单提交的 POST
function postForm(url, formData, options = {}) {
return request({
url,
method: 'POST',
body: formData,
contentType: 'form',
...options
});
}
// 使用示例:现在代码变得像自然语言一样清晰
get('/api/users')
.then(users => console.log('用户列表:', users));
post('/api/users', { name: 'Alice', email: 'alice@example.com' })
.then(newUser => console.log('新用户:', newUser));
del(`/api/users/${userId}`)
.then(() => console.log('用户已删除'));
4.3 高级功能集成:拦截器、请求重试与全局错误处理
一个真正成熟的工具库,必须具备企业级应用所需的高级特性。我们将为
request()
添加拦截器(Interceptor)和重试(Retry)机制。
拦截器:在请求发出前和响应返回后注入自定义逻辑
// 全局拦截器存储
const requestInterceptors = [];
const responseInterceptors = [];
// 添加请求拦截器
function addRequestInterceptor(fn) {
requestInterceptors.push(fn);
}
// 添加响应拦截器
function addResponseInterceptor(fn) {
responseInterceptors.push(fn);
}
// 修改 request() 函数以支持拦截器
async function request(options) {
// ... [前面的代码保持不变] ...
// 在发起请求前,执行所有请求拦截器
let processedOptions = { ...options };
for (const interceptor of requestInterceptors) {
processedOptions = await interceptor(processedOptions) || processedOptions;
}
try {
const response = await fetch(/* ... */);
// 在处理响应前,执行所有响应拦截器
let processedResponse = response;
for (const interceptor of responseInterceptors) {
processedResponse = await interceptor(processedResponse) || processedResponse;
}
// ... [后续的响应处理逻辑] ...
} catch (error) {
// ... [错误处理逻辑] ...
}
}
// 示例:添加一个自动添加认证 Token 的请求拦截器
addRequestInterceptor(async (options) => {
const token = localStorage.getItem('auth_token');
if (token && !options.headers?.Authorization) {
options.headers = {
...options.headers,
Authorization: `Bearer ${token}`
};
}
return options;
});
// 示例:添加一个统一的错误日志记录响应拦截器
addResponseInterceptor(async (response) => {
if (!response.ok) {
console.warn(`API 请求失败: ${response.url} (${response.status})`);
}
return response;
});
重试机制:优雅地应对瞬时网络抖动
/**
* 带重试的请求函数
* @param {RequestOptions} options
* @param {Object} retryOptions
* @param {number} [retryOptions.maxRetries=3] - 最大重试次数
* @param {number} [retryOptions.baseDelay=1000] - 基础延迟(毫秒)
*/
async function requestWithRetry(options, retryOptions = {}) {
const { maxRetries = 3, baseDelay = 1000 } = retryOptions;
for (let i = 0; i <= maxRetries; i++) {
try {
return await request(options);
} catch (error) {
// 只对网络错误和 5xx 错误进行重试
if (
(error.name === 'TypeError' && error.message.includes('fetch')) ||
(error.message.includes('HTTP 5') && !error.message.includes('HTTP 501'))
) {
if (i === maxRetries) throw error; // 最后一次尝试失败,抛出错误
// 指数退避:第一次重试等待 1s,第二次 2s,第三次 4s...
const delay = baseDelay * Math.pow(2, i);
console.log(`请求失败,${delay}ms 后重试... (${i + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
// 其他错误(如 400, 401, 业务错误)不重试,直接抛出
throw error;
}
}
}
}
5. 常见问题与排查技巧实录:那些让你抓耳挠腮的 Fetch “幽灵问题”
5.1 经典问题速查表:高频故障与一键修复方案
| 问题现象 | 根本原因 | 诊断方法 | 修复方案 | 我的实操心得 |
|---|---|---|---|---|
TypeError: Failed to fetch
| 网络层完全失败(跨域、HTTPS/HTTP 混合、CORS 头缺失、本地文件协议) |
查看浏览器 Network 面板,看请求是否出现在列表中;检查 Console 中是否有更详细的错误信息(如
Blocked by CORS policy
)
|
1. 确保前后端协议一致(都用 HTTPS);2. 后端正确配置
Access-Control-Allow-Origin
;3. 本地开发时,用
http-server
或
live-server
启动,避免
file://
协议
| 这个错误信息极其笼统,90% 的时间它都不是网络真的断了,而是 CORS 配置问题。养成第一时间打开 Network 面板的习惯,比瞎猜快十倍。 |
SyntaxError: Unexpected token < in JSON at position 0
| 服务端返回了 HTML(通常是 404 或 500 错误页),而非预期的 JSON | 在 Network 面板中点击该请求,查看 Preview 或 Response 标签页,确认返回的内容 |
1. 在
.then()
中先检查
response.ok
;2. 如果
!response.ok
,再调用
response.text()
查看原始 HTML;3. 修复后端路由或逻辑错误
|
这是我职业生涯中见过最多次的错误。它完美诠释了为什么不能跳过
response.ok
检查。记住:
response.json()
是信任的,但
response
本身是不可信的。
|
| POST 请求,后端收不到数据 |
Content-Type
头设置错误,或
body
未正确序列化
|
在 Network 面板中查看该请求的 Headers 和 Payload 标签页,确认
Content-Type
头和请求体内容
|
1. 对于 JSON,确保
headers: { 'Content-Type': 'application/json' }
且
body: JSON.stringify(data)
;2. 对于表单,
不要
设置
Content-Type
,直接传
FormData
或
URLSearchParams
|
曾在一个项目中,后端同事坚持说“前端没传数据”,我花了半小时证明
FormData
是对的,最后发现是后端 Nginx 配置限制了
client_max_body_size
。所以,永远要和后端一起看 Network 面板。
|
| GET 请求被缓存,总是返回旧数据 |
浏览器或中间代理(CDN)根据
Cache-Control
头进行了缓存
|
在 Network 面板中查看该请求的 Size 列,如果是
(from disk cache)
或
(from memory cache)
,说明被缓存了
|
1. 在 URL 后添加时间戳参数
?t=${Date.now()}
(简单粗暴);2. 在
fetch
选项中设置
cache: 'no-store'
;3. 后端设置
Cache-Control: no-cache
|
在开发阶段,我习惯性地在所有 GET 请求的
fetch
选项里加上
cache: 'no-store'
,上线后再根据业务需求调整。这能避免 99% 的“为什么我的新代码没生效”的问题。
|
response.json()
报错
Unexpected end of JSON input
|
服务端返回了空响应体(
""
),或响应体是纯文本而非 JSON
| 在 Network 面板中查看 Response 标签页,确认内容是否为空或格式错误 |
1. 在调用
response.json()
前,先用
response.text()
获取原始字符串,打印出来看;2. 后端确保在所有分支下都返回了有效的 JSON
|
这个错误通常意味着后端逻辑有缺陷,比如某个
if
分支里忘了
return res.json({...})
。前端的防御性编程是必要的,但根源还是在后端。
|
5.2 深度排查实战:一个真实线上 Bug 的完整复盘
问题背景 :一个电商 App 的“加入购物车”按钮,在 iOS Safari 上点击后没有任何反应,但在 Chrome 和 Android 上一切正常。控制台没有任何报错。
排查过程 :
-
初步验证
:首先,在 Safari 的开发者工具中,确认
fetch()调用确实被执行了。通过在fetch()前加console.log('About to fetch'),确认代码执行到了这里。 -
Network 面板观察
:在 Safari 的 Network 面板中,发现该 POST 请求根本没有出现在列表中。这说明请求甚至没有发出,问题出在
fetch()调用本身。 -
聚焦错误
:仔细检查
fetch()的参数。发现body是一个普通的 JavaScript 对象:{ productId: 123, quantity: 1 }。而在 Safari 14 之前的版本中,fetch()的body参数 不接受普通对象 ,它只接受Blob,BufferSource,FormData,URLSearchParams,USVString(字符串)或ReadableStream。Chrome 和较新版本的 Safari 会自动将对象转换为字符串,但老 Safari 不会,它会直接抛出一个静默的TypeError,并且这个错误不会进入catch块,因为fetch()函数本身在解析参数时就失败了,根本没返回 Promise。 -
验证与修复
:在
fetch()调用前,添加一个console.log(typeof body),在 Safari 中输出object,证实了猜想。修复方案很简单:将body显式地JSON.stringify()。// ❌ 在老 Safari 中会静默失败 fetch('/cart', { method: 'POST', body: { productId: 123 } }); // ✅ 正确 fetch('/cart', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ productId: 123 }) });
经验总结
:这个 Bug 教会我三件事:第一,永远不要假设
fetch()
的参数在所有浏览器中表现一致,尤其是涉及到
body
这种“非标准”类型的参数时;第二,当遇到“完全没有网络请求”的情况时,问题极有可能出在
fetch()
调用的参数校验阶段,而不是网络传输阶段;第三,
console.log()
是最朴实无华,但也是最有效的调试工具,尤其是在移动端这种调试不便的环境下。
5.3 性能与体验优化:让 Fetch 不仅“能用”,更要“好用”
Fetch API 本身是高性能的,但如何用它构建出流畅的用户体验,是一门艺术。
1. 加载状态管理:避免“假死” 一个常见的反模式是,用户点击按钮后,界面没有任何反馈,直到请求完成。这会让用户怀疑应用是否卡死。
// ❌ 反模式:没有加载状态
button.addEventListener('click', () => {
fetch('/api/data').then(...);
});
// ✅ 正确:提供即时视觉反馈
button.addEventListener('click', async () => {
button.disabled = true;
button.textContent = '加载中...';
try {
const data = await fetch('/api/data').then(r => r.json());
renderData(data);
} catch (error) {
alert('加载失败,请重试');
} finally {
button.disabled = false;
button.textContent = '加载数据';
}
});
2. 并发请求控制:避免“请求雪崩”
当一个页面需要同时加载多个资源(如用户信息、订单列表、通知)时,盲目地并发
fetch()
可能会压垮后端或导致客户端性能下降。
// 使用 Promise.allSettled() 来并发请求,并能分别处理每个请求的成功与失败
const [userRes, ordersRes, notificationsRes] = await Promise.allSettled([
fetch('/api/user'),
fetch('/api/orders'),
fetch('/api/notifications')
]);
// 分别处理
const user = userRes.status === 'fulfilled' ? await userRes.value.json() : null;
const orders = ordersRes.status === 'fulfilled' ? await ordersRes.value.json() : [];
const notifications = notificationsRes.status === 'fulfilled' ? await notificationsRes.value.json() : [];
3. 流式响应处理:为大文件下载和实时日志提供支持
Fetch 的
Response.body
是一个
ReadableStream
,这使得我们可以实现真正的流式处理,而不需要等待整个响应体下载完毕。
// 示例:实现一个带进度条的大文件下载
async function downloadFile(url, filename)


被折叠的 条评论
为什么被折叠?



