JS中Fetch API替代传统Ajax的用法与注意事项

作者:小括号

时间:2026-11-04

浏览:278

深入讲解在JavaScript中使用现代标准Fetch API替代传统XMLHttpRequest(Ajax)的具体用法、两阶段解析机制、核心注意事项与避坑指南,包含完整代码示例与生产级封装方案。

文章目录
JS中Fetch API替代传统Ajax的用法与注意事项
技术演进总览:从 XMLHttpRequest 状态回调模型到标准 Fetch API 的现代化过渡
在 JavaScript 前端开发中,使用 Fetch API 替代传统的 XMLHttpRequest(Ajax),核心是用原生基于 Promise 的声明式管道替代繁重的事件回调状态机。这不仅能消除回调嵌套,还能与 async/await 语法深度结合。然而,直接替换并不能“无脑复制”代码,必须针对 HTTP 状态码不抛错、响应体流式两阶段解析、跨域 Cookie 默认不携带、请求取消改用 AbortController 以及 上传进度监听局限 这五大核心差异做好适配。本文将一步步拆解从传统 Ajax 平滑迁移至 Fetch 的完整做法与实操避坑要点。

一、传统 Ajax 与 Fetch API 的核心认知差异

长期以来,前端开发者依赖 XMLHttpRequest(XHR)对象来实现无需刷新页面的异步数据交互。很多早期教程习惯将 XHR 以及基于它的封装库(如 $.ajax)统称为“Ajax”。但从现代浏览器技术标准来看,XHR 的设计诞生较早,暴露出来的 API 具有浓厚的事件驱动与状态轮询色彩。

传统的 XMLHttpRequest 需要先实例化对象,通过 open() 初始化请求,再依次挂载 onreadystatechange 或 onload 回调函数,最后调用 send() 发出。在处理复杂的多级串行请求或动态条件请求时,容易产生冗长的嵌套代码。

相比之下,Fetch API 是现代浏览器内置的底层网络接口标准,具有以下几个本质特征:

  • 原生基于 Promise:Fetch 彻底告别了繁琐的回调监听,天然契合 .then() 链式调用和 async/await 语法糖,让异步请求的组织方式与同步代码同样直观。
  • 标准对象模型模块化:提供了专门的 Request、Response 和 Headers 原生类,开发者可以自由实例化、克隆和复用请求头与配置上下文,与 Service Worker 及现代构建工具高度协同。
  • 两阶段流式传输(Streams API):Fetch 将“网络响应到达(读取 Headers)”与“响应数据体消费(读取 Body)”分为两个独立阶段,支持大文件在下载过程中分段读取,避免了 XHR 一次性把所有数据缓存在内存中的性能瓶颈。
JS中Fetch API替代传统Ajax的用法与注意事项
图 1:XMLHttpRequest 与 Fetch API 核心机制对比矩阵

二、从 XMLHttpRequest 到 Fetch 的代码转换与迁移步骤

为了看清代码层面的实际变化,我们先以最常见的 GET 请求和 POST JSON 数据请求为例,对比新旧两套语法的对应关系。

1. 基础 GET 请求对照

在旧版 XHR 中,我们需要这样编写:

// 传统 XMLHttpRequest 发送 GET 请求
function getArticleOld(id, onSuccess, onError) {
  var xhr = new XMLHttpRequest();
  xhr.open('GET', '/api/articles/' + id, true);
  xhr.onreadystatechange = function () {
    if (xhr.readyState === 4) {
      if (xhr.status >= 200 && xhr.status < 300) {
        try {
          var data = JSON.parse(xhr.responseText);
          onSuccess(data);
        } catch (e) {
          onError(new Error('JSON 解析失败'));
        }
      } else {
        onError(new Error('请求失败,状态码: ' + xhr.status));
      }
    }
  };
  xhr.onerror = function () {
    onError(new Error('网络层通信异常'));
  };
  xhr.send();
}

切换到 Fetch API 后,配合 async/await,代码可以精简并扁平化为如下形式:

// 现代 Fetch API 发送 GET 请求
async function getArticleNew(id) {
  try {
    const response = await fetch(`/api/articles/${id}`);
    
    // 注意:必须显式检查 response.ok
    if (!response.ok) {
      throw new Error(`请求失败,状态码: ${response.status}`);
    }
    
    const data = await response.json();
    return data;
  } catch (error) {
    // 捕获网络失败或手动抛出的 HTTP 状态异常
    console.error('获取文章失败:', error.message);
    throw error;
  }
}

2. 提交 JSON 数据的 POST 请求对照

当向服务端提交数据时,XHR 需要手动调用 setRequestHeader 并对载荷进行 JSON.stringify 处理:

// 传统 XHR 发送 POST JSON
function createArticleOld(payload, callback) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/api/articles', true);
  xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
  xhr.onload = function () {
    if (xhr.status === 201 || xhr.status === 200) {
      callback(null, JSON.parse(xhr.responseText));
    } else {
      callback(new Error('保存失败: ' + xhr.status));
    }
  };
  xhr.send(JSON.stringify(payload));
}

而在 Fetch 中,所有配置集中在一个 options 参数对象内,结构更加清晰:

// Fetch API 发送 POST JSON
async function createArticleNew(payload) {
  const response = await fetch('/api/articles', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(payload)
  });

  if (!response.ok) {
    throw new Error(`提交失败,HTTP 状态码: ${response.status}`);
  }

  return await response.json();
}
JS中Fetch API替代传统Ajax的用法与注意事项
图 2:传统 Ajax 平滑迁移至 Fetch API 的标准五步流程

三、替代过程中必须掌握的五大核心注意事项

初学者或从传统 Ajax 刚转到 Fetch 的开发者,最常遇到几个不易察觉的隐蔽 Bug。理解这些机制的本质,是写出健壮代码的关键。

1. 注意事项一:HTTP 4xx/5xx 状态码不会触发 catch(最重要避坑点)

这是从 XHR 迁移到 Fetch 最容易犯的错误。在传统习惯或 Axios 等第三方库中,如果服务器返回了 404 Not Found 或 500 Internal Server Error,通常会直接被判为失败并进入错误回调。

但 Fetch API 的规范设计严格遵循网络通信原则:只要浏览器能够成功与远程服务器建立 TCP 连接并完整接收到了 HTTP 响应头,哪怕状态码是 404 或 500,Promise 也会被判定为 成功(Resolved),而不是被拒绝(Rejected)。

避坑口诀:只有当发生断网、DNS解析失败、跨域被浏览器CORS策略完全阻断等纯底层网络故障时,Fetch 的 Promise 才会走 catch。因此,每次调用 Fetch 之后,必须第一步检查 response.ok(即状态码位于 200~299 区间)。
// 正确的状态判断逻辑
const res = await fetch('/api/user/profile');

if (!res.ok) {
  // 服务器响应了,但状态码表示出错(如 401 未授权、404 不存在、500 服务异常)
  const errorInfo = await res.text();
  throw new Error(`业务错误 [${res.status}]: ${errorInfo}`);
}

// 状态码在 200~299 之间,安全提取数据
const profile = await res.json();
JS中Fetch API替代传统Ajax的用法与注意事项
图 3:Fetch 状态判定与两阶段响应流解析生命周期

2. 注意事项二:响应体采用流式读取,且只能消费一次

在 XHR 中,xhr.responseText 可以在同一个对象上反复多次读取。但在 Fetch API 中,response.body 是一个底层的 ReadableStream(可读流)。

当调用 response.json()、response.text()、response.blob() 或 response.formData() 时,流中的数据被逐块读取并组装到内存中。一旦流被读取完毕,就不能再次读取。如果尝试对同一个 response 既调用 .json() 又调用 .text(),控制台将直接抛出运行时错误:

// 常见错误示例:
const res = await fetch('/api/data');
const text = await res.text();
// 报错:TypeError: Already read
const json = await res.json(); 

// 正确做法:若需要多次消费或在错误时备份,使用 res.clone()
const res = await fetch('/api/data');
const resClone = res.clone(); // 复制流分支

try {
  const data = await res.json();
  console.log('JSON 成功:', data);
} catch (err) {
  // 解析 JSON 失败时,读取克隆的响应流排查纯文本内容
  const rawText = await resClone.text();
  console.warn('非标准 JSON 内容:', rawText);
}

3. 注意事项三:Cookie 与凭据信息的传递规则

传统 XHR 在同源请求下默认会自动携带当前域名下的 Cookie;如果是跨域请求,只需将 xhr.withCredentials = true 开启即可。

而在 Fetch 规范中,凭据控制由 credentials 字段管理:

  • credentials: 'same-origin'(现代浏览器默认值):仅在同源请求时携带 Cookie 和认证头,跨域时不携带。
  • credentials: 'include':无论同源还是跨域,都显式携带凭证信息(跨域需要服务端响应 Access-Control-Allow-Credentials: true)。
  • credentials: 'omit':完全不发送任何凭据信息。

在前后端分离部署、前端页面与后端 API 位于不同子域(如 app.example.com 与 api.example.com)时,若接口依赖 Cookie/Session 进行登录态维护,切记在请求配置中显式声明:

const response = await fetch('https://api.example.com/v1/user', {
  method: 'GET',
  credentials: 'include' // 跨域必须开启,否则服务端无法收到鉴权 Cookie
});

4. 注意事项四:请求超时与取消的机制重构

旧版 XHR 原生提供了 xhr.timeout = 5000 属性与 xhr.ontimeout 事件,同时提供了 xhr.abort() 方法来立即中断正在进行的通信。

但 fetch() 的配置参数中并没有 timeout 属性!如果你写了 fetch(url, { timeout: 5000 }),它是完全不会生效的。在现代标准中,网络取消与超时完全由 AbortController 及其信号 AbortSignal 接管。

针对超时场景,现代浏览器支持两种标准写法:

// 方式 A:现代浏览器极简原生超时语法(AbortSignal.timeout)
try {
  const res = await fetch('/api/heavy-task', {
    // 超过 5 秒自动发出中断信号
    signal: AbortSignal.timeout(5000) 
  });
  const data = await res.json();
} catch (err) {
  if (err.name === 'TimeoutError') {
    console.warn('请求处理超时,已被浏览器熔断');
  } else {
    console.error('其他网络错误:', err);
  }
}

// 方式 B:通用手动取消控制器(支持兼容老环境或组件销毁取消)
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);

try {
  const res = await fetch('/api/heavy-task', {
    signal: controller.signal
  });
  clearTimeout(timeoutId); // 成功后清除定时器
  const data = await res.json();
} catch (err) {
  if (err.name === 'AbortError') {
    console.warn('请求被主动中止');
  }
}

5. 注意事项五:上传进度与边界情况(何时不该强行替代)

很多开发者试图将项目内所有网络请求一次性全部重构成 Fetch,但有一个场景需要特别警惕:带精细进度条的文件上传。

传统 XHR 提供了专门的 xhr.upload.onprogress 事件,能够精确监听上传字节数与总字节数,从而轻松渲染 0% ~ 100% 的上传进度条。然而,现行标准的 Fetch API 虽然对下载流(ReadableStream)支持完善,但对于上传流的细粒度进度监听,目前浏览器的支持程度和 API 便捷性仍然逊色于 XHR。

技术选型建议:对于常规的数据拉取、表单提交、JSON 交互、下载预览等 95% 以上的前端交互,Fetch API 是最佳选择;但如果有复杂的大文件分片上传且需精准展示每秒上传百分比的交互需求,保留原生的 XMLHttpRequest 或专业的上传 SDK 往往更加稳妥。

四、生产级实战:封装健壮的通用 Fetch 请求工具

在真实业务项目中,直接裸调 fetch() 会导致大量重复的 res.ok 校验、请求头设置与超时控制。我们可以将上述注意事项融会贯通,封装一个统一的通用请求客户端:

/**
 * 生产环境通用 HTTP 请求辅助工具
 * 集成:自动超时熔断、基础路径、默认 JSON 头、凭证配置与业务异常拦截
 */
class HttpClient {
  constructor(baseURL = '', defaultTimeout = 8000) {
    this.baseURL = baseURL;
    this.defaultTimeout = defaultTimeout;
  }

  async request(endpoint, options = {}) {
    const {
      method = 'GET',
      headers = {},
      body,
      timeout = this.defaultTimeout,
      credentials = 'same-origin',
      ...customConfig
    } = options;

    const fullUrl = this.baseURL + endpoint;

    // 组织 Headers
    const finalHeaders = new Headers(headers);
    let finalBody = body;

    // 若提交对象数据且未设置 Content-Type,自动转为 JSON 字符串
    if (body && typeof body === 'object' && !(body instanceof FormData) && !(body instanceof Blob)) {
      if (!finalHeaders.has('Content-Type')) {
        finalHeaders.set('Content-Type', 'application/json');
      }
      finalBody = JSON.stringify(body);
    }

    // 配置超时控制器
    const controller = new AbortController();
    let timeoutTimer = null;
    if (timeout > 0) {
      timeoutTimer = setTimeout(() => controller.abort(), timeout);
    }

    try {
      const response = await fetch(fullUrl, {
        method,
        headers: finalHeaders,
        body: finalBody,
        credentials,
        signal: controller.signal,
        ...customConfig
      });

      // 清理超时计时器
      if (timeoutTimer) clearTimeout(timeoutTimer);

      // 第一道防线:检测 HTTP 状态码
      if (!response.ok) {
        let errorMessage = `HTTP Error ${response.status}`;
        try {
          // 尝试读取后端返回的错误信息
          const errData = await response.json();
          errorMessage = errData.message || errorMessage;
        } catch (_) {
          // 若不是 JSON,尝试读取文本
          try {
            errorMessage = await response.text();
          } catch (__) {}
        }

        // 统一处理鉴权失效(如 401 自动跳转登录页)
        if (response.status === 401) {
          console.warn('登录凭据已过期,触发重定向');
          // window.location.href = '/login';
        }

        const err = new Error(errorMessage);
        err.status = response.status;
        throw err;
      }

      // 处理 204 No Content
      if (response.status === 204) {
        return null;
      }

      // 第二道防线:智能解析响应体
      const contentType = response.headers.get('content-type') || '';
      if (contentType.includes('application/json')) {
        return await response.json();
      }
      return await response.text();

    } catch (error) {
      if (timeoutTimer) clearTimeout(timeoutTimer);

      // 区分中止错误与通用网络错误
      if (error.name === 'AbortError') {
        throw new Error(`请求超时(限制 ${timeout}ms),已终止连接`);
      }
      throw error;
    }
  }

  get(endpoint, options) {
    return this.request(endpoint, { ...options, method: 'GET' });
  }

  post(endpoint, data, options) {
    return this.request(endpoint, { ...options, method: 'POST', body: data });
  }

  put(endpoint, data, options) {
    return this.request(endpoint, { ...options, method: 'PUT', body: data });
  }

  delete(endpoint, options) {
    return this.request(endpoint, { ...options, method: 'DELETE' });
  }
}

// 使用示例:
// const api = new HttpClient('https://api.example.com');
// const user = await api.get('/user/1001');
// const result = await api.post('/articles', { title: 'Fetch 新实践' });
JS中Fetch API替代传统Ajax的用法与注意事项
图 4:Fetch API 替代传统 Ajax 全景技术选型与避坑总结

五、全文总结

Fetch API 替代传统 XMLHttpRequest 绝不仅仅是“少写几行代码”的语法糖升级,而是 JavaScript 网络编程模型向现代化、模块化与流式规范的重要演进。在项目实际落地中,只要牢牢把握以下几项原则,就能顺利完成网络层的平滑过渡:

  1. 牢记响应校验防线:绝不能省略 if (!response.ok) 状态码检查,HTTP 错误绝不会自动抛入 catch。
  2. 理解两阶段读取与流单次性:把读取响应头与调用 res.json() 解析响应体视为两个独立环节,严禁重复消费同一响应流。
  3. 超时由信号驱动:弃用已无效的 timeout 属性,借助 AbortController 或 AbortSignal.timeout() 实现规范的请求熔断。
  4. 按需配置跨域凭证:在跨域且依赖 Session/Cookie 场景下,明确配置 credentials: 'include'。
  5. 尊重上传进度边界:针对对上传进度条有极高实时性要求的功能模块,理性保留 XHR。

将这些规则融入通用的请求封装库中,既能充分享受标准 Fetch API 带来的极简代码与良好扩展性,又能保障应用在生产环境中的鲁棒性与异常自愈能力。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章