从 XMLHttpRequest 到 Fetch API:前端网络请求演进与迁移指南
对比了 XMLHttpRequest 和 Fetch API 的技术差异,分析了 XHR 的回调地狱、错误处理模糊等缺陷,以及 Fetch 基于 Promise 的优势。文章提供了详细的迁移策略、兼容层实现代码、流式数据处理及请求重试机制,并探讨了浏览器兼容性处理和性能监控方案,旨在帮助开发者平滑过渡到现代前端网络请求标准。

对比了 XMLHttpRequest 和 Fetch API 的技术差异,分析了 XHR 的回调地狱、错误处理模糊等缺陷,以及 Fetch 基于 Promise 的优势。文章提供了详细的迁移策略、兼容层实现代码、流式数据处理及请求重试机制,并探讨了浏览器兼容性处理和性能监控方案,旨在帮助开发者平滑过渡到现代前端网络请求标准。

在前端开发领域,XMLHttpRequest (XHR) 长期统治着浏览器端的网络请求。然而,随着 Web 应用变得越来越复杂,XHR 的设计缺陷和局限性逐渐暴露。2015 年,Fetch API 作为更现代、更强大的替代方案出现在 Web 标准中,开启了前端网络请求的新时代。
本文将深入探讨从 XHR 迁移到 Fetch API 的技术细节、优势对比以及实际迁移策略,帮助你理解这一重要技术演进的背后逻辑。
// 典型的 XHR 请求代码
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
console.log('成功:', data);
} else {
console.error('请求失败:', xhr.status);
}
}
};
xhr.onerror = function() {
console.error('网络错误');
};
xhr.send();
回调地狱与复杂的状态管理
XHR 基于事件的回调模式导致代码嵌套层次深,错误处理分散,可读性差。
模糊的错误信息
XHR 的 readyState === 0 状态是最典型的例子 - 它只告诉开发者"请求未初始化",却不提供具体原因。这种模糊性使得调试变得异常困难。
API 设计不直观
需要管理多个事件监听器 (onreadystatechange, onerror, ontimeout),配置复杂,学习曲线陡峭。
功能限制
缺乏对现代 Web 特性(如流式处理、请求中断)的良好支持。
// 基础的 Fetch 请求
fetch('/api/data').then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}).then(data => console.log('成功:', data)).catch(error => console.error('失败:', error));
基于 Promise 的现代化 API
更精确的错误分类
fetch('/api/data').catch(error => {
// 明确的错误类型
if (error.name === 'TypeError') {
console.error('网络错误或 CORS 问题');
} else if (error.name === 'AbortError') {
console.error('请求被取消');
}
});
对现代 Web 特性的原生支持
XHR 的响应处理
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
// 所有完成状态都进入这里
if (xhr.status === 200) {
// 成功处理
} else {
// 所有错误状态统一处理
}
}
};
Fetch 的响应处理
fetch(url).then(response => {
// 关键区别:所有网络成功的请求都进入这里
// 包括 200、404、500 等状态码
if (response.ok) {
// 只有 200-299 状态码进入这里
return response.json();
} else if (response.status === 304) {
// 特殊处理缓存情况
return handleNotModified();
} else {
// 其他 HTTP 错误状态
throw new HttpError(response.status, response.statusText);
}
}).catch(error => {
// 只有网络层面的错误进入这里
// 如 CORS 错误、DNS 解析失败、网络断开等
});
response.ok 的真相
// Fetch 的 response.ok 行为
console.log(response.status); // 200 -> response.ok: true
console.log(response.status); // 201 -> response.ok: true
console.log(response.status); // 204 -> response.ok: true
console.log(response.status); // 304 -> response.ok: false // 注意!
console.log(response.status); // 404 -> response.ok: false
console.log(response.status); // 500 -> response.ok: false
这种设计让开发者能够更精确地处理不同的 HTTP 场景,特别是对 304 Not Modified 的特殊处理。
XHR 的请求控制
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.timeout = 5000;
xhr.ontimeout = function() {
console.log('请求超时');
};
// 但 XHR 无法真正中止一个超时请求
Fetch 的请求控制
const controller = new AbortController();
const signal = controller.signal;
// 设置超时
setTimeout(() => controller.abort(), 5000);
fetch('/api/data', { signal }).then(response => response.json()).catch(err => {
if (err.name === 'AbortError') {
console.log('请求被主动取消');
}
});
XHR 的 readyState === 0 表示请求甚至没有成功发送出去,常见原因包括:
open() 和 send() 之间发生异常async function robustFetch(url, options = {}) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), options.timeout || 30000);
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new HttpError(response.status, response.statusText);
}
return await response.json();
} catch (error) {
// Fetch 提供更明确的错误信息
if (error.name === 'AbortError') {
throw new Error('请求超时');
} else if (error.name === 'TypeError') {
throw new Error('网络错误或 CORS 配置问题');
} else {
throw error;
}
}
}
创建兼容层
class ApiClient {
constructor(baseURL = '') {
this.baseURL = baseURL;
this.useFetch = typeof fetch !== 'undefined';
}
async request(endpoint, options = {}) {
const url = this.baseURL + endpoint;
if (this.useFetch) {
return this.fetchRequest(url, options);
} else {
return this.xhrRequest(url, options);
}
}
async fetchRequest(url, options) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), options.timeout || 30000);
try {
const response = await fetch(url, {
method: options.method || 'GET',
headers: options.headers,
body: options.body,
: controller.,
:
});
(timeoutId);
(response.) {
:
:
.(response);
:
;
:
.(url);
:
();
:
();
:
();
:
();
:
(response.) {
.(response);
}
(response., response.);
}
} (error) {
(timeoutId);
.(error, url);
}
}
() {
(error. === ) {
{ : , : };
}
(error. === && error..()) {
{ : , : };
}
error;
}
}
流式数据处理
// 处理大文件或实时数据流
async function processLargeData(url) {
const response = await fetch(url);
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 处理数据块
console.log('接收到数据块:', value.length);
}
}
请求重试机制
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const result = await fetch(url, options);
return result;
} catch (error) {
lastError = error;
// 只在网络错误时重试
if (error.type === 'NETWORK_ERROR' || error.type === 'TIMEOUT') {
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
// HTTP 错误不重试
break;
}
}
throw lastError;
}
// 特性检测与降级方案
if (typeof fetch === 'function' && typeof AbortController === 'function') {
// 使用现代 Fetch API
module.exports = require('./modern-fetch-client');
} else {
// 降级到 XHR 或 polyfill
module.exports = require('./legacy-xhr-client');
}
拦截器模式
class FetchInterceptor {
constructor() {
this.requestInterceptors = [];
this.responseInterceptors = [];
}
use(requestHandler, responseHandler) {
if (requestHandler) this.requestInterceptors.push(requestHandler);
if (responseHandler) this.responseInterceptors.push(responseHandler);
}
async fetch(url, options = {}) {
// 处理请求拦截器
let processedOptions = options;
for (const interceptor of this.requestInterceptors) {
processedOptions = await interceptor(url, processedOptions);
}
let response = await fetch(url, processedOptions);
// 处理响应拦截器
for (const interceptor of this.responseInterceptors) {
response = await interceptor(response);
}
return response;
}
}
class MonitoredFetch {
static async fetch(url, options = {}) {
const startTime = performance.now();
const requestId = generateUniqueId();
try {
emitEvent('requestStart', { requestId, url, startTime });
const response = await fetch(url, options);
const endTime = performance.now();
emitEvent('requestEnd', {
requestId,
url,
duration: endTime - startTime,
status: response.status,
size: response.headers.get('content-length')
});
return response;
} catch (error) {
const endTime = performance.now();
emitEvent('requestError', {
requestId,
url,
duration: endTime - startTime,
error: error.message
});
throw error;
}
}
}
立即开始:
长期规划:
从 XMLHttpRequest 到 Fetch API 的迁移,不仅仅是技术方案的替换,更是前端开发理念的升级。Fetch API 代表了 Web 平台向更现代、更强大方向发展的趋势。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online