HTTP进化:从XHR到Fetch

一、XMLHttpRequest:沉默的基石

深夜的办公室里,只有键盘敲击声与空调的低鸣。林峰盯着屏幕上那段缠绕如藤蔓的代码,眉头紧锁。

javascript 复制代码
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);
      // 处理数据...
    } else {
      console.error('请求失败:', xhr.status);
    }
  }
};
xhr.send();

“这已经是第几次写这样的代码了?”他喃喃自语。

XHR——XMLHttpRequest,这个从IE5时代就存在的老将,支撑着整个前端异步通信的世界。林峰记得自己第一次使用它时的新奇:原来页面可以不刷新就获取数据。但十年过去,这份新奇早已被繁琐的API设计磨平。

二、回调地狱与Promise曙光

项目进入快速迭代期,接口调用越来越复杂。一个用户操作可能需要连续调用三四个接口,每个接口都依赖上一个的结果。

javascript 复制代码
// 回调地狱的典型场景
xhr1(function(data1) {
  xhr2(data1.id, function(data2) {
    xhr3(data2.token, function(data3) {
      xhr4(data3.list, function(data4) {
        // 四层嵌套后,代码已难以维护
      });
    });
  });
});

团队新来的实习生小杨看着这样的代码直摇头:“林哥,这‘金字塔’看着头晕。”

林峰苦笑。直到Promise的出现,才让这片黑暗有了第一缕光。

“试试这个。”他演示着新的写法:

javascript 复制代码
function fetchUser(id) {
  return new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', '/api/user/' + id);
    xhr.onload = function() {
      if (xhr.status === 200) {
        resolve(JSON.parse(xhr.responseText));
      } else {
        reject(new Error(xhr.statusText));
      }
    };
    xhr.onerror = function() {
      reject(new Error('Network Error'));
    };
    xhr.send();
  });
}

// 链式调用,清晰多了
fetchUser(123)
  .then(user => fetchOrders(user.id))
  .then(orders => processOrders(orders))
  .catch(error => console.error('出错:', error));

小杨眼睛一亮:“这个好!像在搭积木,一层一层很清楚。”

三、Fetch API:优雅的变革

2015年,Fetch API作为现代浏览器原生支持的HTTP客户端横空出世。林峰第一次在技术博客上看到它时,就被其简洁的API设计吸引。

javascript 复制代码
// 同样的请求,用Fetch写
fetch('/api/data')
  .then(response => {
    if (!response.ok) {
      throw new Error('网络响应异常');
    }
    return response.json();
  })
  .then(data => {
    // 处理数据
  })
  .catch(error => {
    console.error('请求失败:', error);
  });

“没有冗长的readyState判断,没有手动创建XHR对象,甚至JSON解析都内置了。”林峰在技术分享会上向团队介绍,“Fetch基于Promise设计,天然支持异步流控制。”

但变革从来不是一帆风顺的。

四:兼容性的阵痛

第一个坑很快出现。项目需要支持IE11,而Fetch在IE上根本不存在。

“林哥,页面在IE上白屏了!”测试同事急匆匆地跑来。

林峰一拍额头:“忘了polyfill。”他迅速引入whatwg-fetch填充库,但问题接踵而至。

第二个坑是关于错误处理。Fetch与XHR有个关键区别:Fetch只在网络故障时reject,HTTP错误状态(如404、500)仍然resolve

javascript 复制代码
// 这个404请求不会进入catch块!
fetch('/api/not-exist')
  .then(response => {
    console.log(response.status); // 404
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return response.json();
  })
  .catch(error => {
    console.log('这里才会捕获404错误');
  });

团队成员小李因此踩了坑:“我以为是网络问题,调试了半天才发现是接口返回了500。”

五:超时控制的缺失

第三个坑更隐蔽:Fetch没有内置超时控制。

“用户反馈上传大文件时,有时会一直卡住。”产品经理拿着用户反馈记录。

XHR有timeout属性,Fetch却没有。林峰查阅资料后,给出了解决方案:

javascript 复制代码
function fetchWithTimeout(url, options = {}, timeout = 10000) {
  return Promise.race([
    fetch(url, options),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('请求超时')), timeout)
    )
  ]);
}

“用Promise.race竞速,让超时Promise和Fetch Promise赛跑。”林峰解释道。

六:请求中断的难题

第四个坑出现在文件上传功能。用户需要能够取消上传。

“XHR有abort()方法,Fetch怎么取消?”小杨问道。

林峰展示了相对较新的AbortController API:

javascript 复制代码
const controller = new AbortController();
const signal = controller.signal;

fetch('/api/upload', {
  method: 'POST',
  body: formData,
  signal: signal // 传入信号
});

// 需要取消时
controller.abort(); // Fetch请求会被中断

“不过注意兼容性,老浏览器不支持。”林峰补充道。

七:进度监控的局限

第五个坑关乎用户体验。文件上传时需要显示进度条。

“XHR有upload.onprogress,Fetch有吗?”小李在做上传功能时遇到了瓶颈。

林峰摇头:“Fetch API本身不提供进度事件。对于大文件上传,要么用XHR,要么分片上传。”

团队最终选择了分片方案,将大文件切成小块,每块上传后更新进度。

八:拦截器与全局配置

在大型项目中,经常需要对所有请求做统一处理:添加认证头、统一错误处理、请求响应日志等。

“Axios有拦截器,Fetch怎么实现类似功能?”架构评审会上有人提问。

林峰分享了自己的封装方案:

javascript 复制代码
class HttpService {
  constructor(baseURL) {
    this.baseURL = baseURL;
    this.interceptors = {
      request: [],
      response: []
    };
  }

  async request(url, options = {}) {
    // 请求拦截
    let requestOptions = { ...options };
    for (const interceptor of this.interceptors.request) {
      requestOptions = await interceptor(requestOptions);
    }

    // 添加基础URL
    const fullUrl = url.startsWith('http') ? url : `${this.baseURL}${url}`;

    // 发送请求
    let response = await fetch(fullUrl, requestOptions);

    // 响应拦截
    for (const interceptor of this.interceptors.response) {
      response = await interceptor(response);
    }

    return response;
  }

  addRequestInterceptor(interceptor) {
    this.interceptors.request.push(interceptor);
  }

  addResponseInterceptor(interceptor) {
    this.interceptors.response.push(interceptor);
  }
}

// 使用示例
const http = new HttpService('/api');
http.addRequestInterceptor(options => {
  options.headers = {
    ...options.headers,
    'Authorization': `Bearer ${localStorage.getItem('token')}`
  };
  return options;
});

九:Streaming Response的惊喜

在开发实时日志查看功能时,林峰发现了Fetch的一个强大特性:流式响应。

javascript 复制代码
const response = await fetch('/api/logs/stream');
const reader = response.body.getReader();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  
  // value是Uint8Array,可以实时处理数据块
  const text = new TextDecoder().decode(value);
  console.log('新日志:', text);
}

“服务器可以持续推送数据,客户端可以实时处理,不用等整个响应完成。”林峰兴奋地向团队演示,“这对大文件下载、实时监控场景特别有用。”

十:进化中的思考

技术分享会的最后,林峰在白板上画了一条时间轴:

XHR(1999) → jQuery.ajax(2006) → Fetch(2015)

“技术的进化不是简单的替换,而是适应场景的选择。”他总结道:

  1. 简单场景用Fetch:现代应用,优先选择Fetch,代码更简洁
  2. 需要进度监控用XHR:大文件上传下载,XHR更合适
  3. 复杂项目考虑封装:无论是Fetch还是XHR,良好的封装都能提升开发体验
  4. 兼容性永远是第一关:了解你的用户,选择合适的技术方案

会后,小杨感慨:“以前觉得XHR老土,现在明白每个技术都有它的时代使命。”

林峰点头:“是啊,Fetch不是终点。你看,现在已经有更现代的axios、ky这些库,未来还会有新的技术。我们前端开发者要做的,不是追逐每一个新名词,而是理解背后的思想,解决真实的问题。”

窗外,夜幕已深,但办公室的灯还亮着。在这条不断进化的技术道路上,每一次对旧坑的跨越,都是为了更好地走向未来。

而HTTP通信的进化史,不过是前端十年征途中,一个缩影罢了。