深入讲解在JavaScript中使用现代标准Fetch API替代传统XMLHttpRequest(Ajax)的具体用法、两阶段解析机制、核心注意事项与避坑指南,包含完整代码示例与生产级封装方案。
JS中Fetch API替代传统Ajax的用法与注意事项
时间:2026-11-04
浏览:278
文章目录
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 一次性把所有数据缓存在内存中的性能瓶颈。
二、从 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();
}
三、替代过程中必须掌握的五大核心注意事项
初学者或从传统 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)。
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();
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。
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 新实践' });
五、全文总结
Fetch API 替代传统 XMLHttpRequest 绝不仅仅是“少写几行代码”的语法糖升级,而是 JavaScript 网络编程模型向现代化、模块化与流式规范的重要演进。在项目实际落地中,只要牢牢把握以下几项原则,就能顺利完成网络层的平滑过渡:
- 牢记响应校验防线:绝不能省略
if (!response.ok)状态码检查,HTTP 错误绝不会自动抛入catch。 - 理解两阶段读取与流单次性:把读取响应头与调用
res.json()解析响应体视为两个独立环节,严禁重复消费同一响应流。 - 超时由信号驱动:弃用已无效的
timeout属性,借助AbortController或AbortSignal.timeout()实现规范的请求熔断。 - 按需配置跨域凭证:在跨域且依赖 Session/Cookie 场景下,明确配置
credentials: 'include'。 - 尊重上传进度边界:针对对上传进度条有极高实时性要求的功能模块,理性保留 XHR。
将这些规则融入通用的请求封装库中,既能充分享受标准 Fetch API 带来的极简代码与良好扩展性,又能保障应用在生产环境中的鲁棒性与异常自愈能力。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。