Fetch API 原理解析:从 Promise 网络请求到生产级错误处理

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 上一切正常。控制台没有任何报错。

排查过程

  1. 初步验证 :首先,在 Safari 的开发者工具中,确认 fetch() 调用确实被执行了。通过在 fetch() 前加 console.log('About to fetch') ,确认代码执行到了这里。
  2. Network 面板观察 :在 Safari 的 Network 面板中,发现该 POST 请求根本没有出现在列表中。这说明请求甚至没有发出,问题出在 fetch() 调用本身。
  3. 聚焦错误 :仔细检查 fetch() 的参数。发现 body 是一个普通的 JavaScript 对象: { productId: 123, quantity: 1 } 。而在 Safari 14 之前的版本中, fetch() body 参数 不接受普通对象 ,它只接受 Blob , BufferSource , FormData , URLSearchParams , USVString (字符串)或 ReadableStream 。Chrome 和较新版本的 Safari 会自动将对象转换为字符串,但老 Safari 不会,它会直接抛出一个静默的 TypeError ,并且这个错误不会进入 catch 块,因为 fetch() 函数本身在解析参数时就失败了,根本没返回 Promise。
  4. 验证与修复 :在 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)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值