ch15-fetch

Fetch API

浏览器 API 与协议,第 15 章

Fetch API 是现代浏览器提供的底层网络请求接口,允许客户端通过 JavaScript 脚本化地执行数据传输。它继承了 XMLHttpRequest(XHR)的革命性遗产——XHR 曾在 IE 5 中首次亮相,成为异步 JavaScript 与 XML(AJAX)浪潮的核心技术,支撑起几乎所有现代 Web 应用的根基。

“XMLHTTP 改变了一切。它为 DHTML 赋予了真正的’D’(动态)。它让我们能够异步从服务器获取数据,同时在客户端保持文档状态……Outlook Web Access(OWA)团队渴望在浏览器中构建媲美 Win32 的富应用,这种需求推动了 IE 引入这项技术,最终使 AJAX 成为现实。”

—— Jim Van Eaton,《Outlook Web Access:Web 进化的催化剂》

在 XHR 出现之前,客户端与服务器之间的任何状态更新都需要刷新整个页面。XHR 让这一流程可以异步完成,并完全受应用 JavaScript 代码的控制。正是 XHR 让我们从”构建页面”跃迁到”在浏览器中构建交互式 Web 应用”。

Fetch API 继承了 XHR 的衣钵,但采用了基于 Promise 的现代设计,API 更加简洁、强大且灵活。与 XHR 一样,浏览器自动处理所有底层连接管理、协议协商、HTTP 请求格式化等繁琐事务:

  • 浏览器管理连接的建立、池化与终止
  • 浏览器自动选择最佳的 HTTP(S)传输协议(HTTP/1.1、HTTP/2、HTTP/3)
  • 浏览器处理 HTTP 缓存、重定向与内容类型协商
  • 浏览器强制执行安全、认证与隐私约束
  • 还有更多……

从繁琐的底层细节中解放出来,我们的应用可以专注于发起请求、管理进度、处理服务器返回数据的业务逻辑。简洁的 API 加上在所有现代浏览器中的普及,使 Fetch 成为浏览器网络请求的”瑞士军刀”。

因此,几乎所有的网络使用场景(脚本化下载、上传、流式传输,甚至实时通知)都可以基于 Fetch 构建。当然,这并不意味着 Fetch 在每个场景下都是最高效的传输方式——事实上,我们将会看到,在某些情况下它远非最优——但它依然是面向旧版客户端的可靠降级方案,这些客户端可能无法使用更新的浏览器网络 API。基于此,让我们深入探讨 Fetch 的最新能力、使用场景以及性能方面的注意事项。

对完整 Fetch API 及其能力的详尽分析超出了我们的讨论范围——我们的重点是性能!如需了解 Fetch API 的全貌,请参考官方 WHATWG 标准。


网络请求简史

尽管名字里还带着”XML”,XHR 从未被限定于 XML。这个前缀只是历史遗留——最初版本的 XHR 作为 MSXML 库的一部分随 Internet Explorer 5 发布:

“那是美好的旧时光,关键功能在发布前几天才匆忙加入……我意识到 IE 附带了 MSXML 库,而且我在 XML 团队有些不错的关系,他们可能会帮忙——我联系了当时负责该团队的 Jean Paoli,我们很快达成协议,将其作为 MSXML 库的一部分发布。这就是 XMLHTTP 这个名字的真正由来——这东西主要与 HTTP 有关,与 XML 没有特定联系,只是那是我能找到的最容易的发货借口,所以我得把 XML 塞进名字里。”

—— Alex Hopmann,《XMLHTTP 的故事》

Mozilla 参照微软的实现推出了自己的 XHR 版本,通过XMLHttpRequest接口暴露。Safari、Opera 及其他浏览器纷纷跟进,XHR 成为所有主流浏览器的事实标准——这个名字就此固定下来。事实上,XHR 的官方 W3C 工作草案直到 2006 年才发布,那时 XHR 早已广泛应用!

尽管 XHR popularity 高涨且在 AJAX 革命中扮演关键角色,早期版本的能力相当有限:仅支持文本数据传输、上传处理受限、无法处理跨域请求。为解决这些短板,“XMLHttpRequest Level 2”草案于 2008 年发布,新增了一系列特性:

  • 支持请求超时
  • 支持二进制与文本数据传输
  • 支持应用层覆盖响应的媒体类型与编码
  • 支持监控每个请求的进度事件
  • 支持高效的文件上传
  • 支持安全的跨源请求

2011 年,“XMLHttpRequest Level 2”规范与原始 XHR 工作草案合并。因此,虽然你常能看到 XHR 1 版和 2 版的区分,这些区别今天已不再重要;现在只有一个统一的 XHR 规范。事实上,所有 XHR2 的新特性都通过相同的XMLHttpRequest API 提供:同一接口,更多功能。

新的 XHR2 特性现已获得所有现代浏览器支持;参见 caniuse.com/xhr2。因此,每当我们提到 XHR,我们隐式指的是 XHR2 标准。

然而,时代在前进。 随着 JavaScript 生态的发展,基于 Promise 的异步模式成为主流,Fetch API 应运而生。Fetch 于 2015 年左右开始在现代浏览器中落地,提供了一个更强大、更灵活的基础网络请求接口。今天,Fetch 已成为新代码的首选,而 XHR 主要作为遗留系统的维护接口存在。


跨源资源共享(CORS)

Fetch(以及其前辈 XHR)是浏览器级 API,自动处理无数底层细节:缓存、重定向处理、内容协商、认证等等。这具有双重目的。首先,它让应用 API 更易用,让我们能专注于业务逻辑。其次,它允许浏览器对应用代码进行沙箱隔离,强制执行一系列安全与策略约束。

Fetch 接口在每个请求上强制执行严格的 HTTP 语义:应用提供数据与 URL,浏览器格式化请求并处理每个连接的完整生命周期。类似地,虽然 Fetch API 允许应用添加自定义 HTTP 头(通过headers选项),但仍有许多受保护的头对应用代码不可触及:

  • Accept-CharsetAccept-EncodingAccess-Control-*
  • HostUpgradeConnectionRefererOrigin
  • CookieSec-*Proxy-*,以及数十个其他头……

浏览器会拒绝覆盖任何不安全头部,这保证了应用无法伪装虚假的用户代理、用户或请求来源。事实上,保护Origin头尤为重要,因为它是应用于所有 Fetch 请求的”同源策略”的关键组成部分。

“源”被定义为应用协议、域名与端口的三元组——例如(http, example.com, 80)(https, example.com, 443)被视为不同源。更多细节参见《Web Origin 概念》。

同源策略的动机很简单:浏览器存储用户数据,如认证令牌、Cookie 和其他私有元数据,这些不能在不同应用间泄露——例如,没有同源沙箱,example.com 上的任意脚本就能访问和操控 thirdparty.com 上的用户数据!

为解决这一特定问题,早期 XHR 被限制为仅同源请求,请求源必须与所请求资源的源匹配:从 example.com 发起的 XHR 只能请求来自同一 example.com 源的资源。或者,如果同源前提不满足,浏览器将直接拒绝发起 XHR 请求并报错。

然而,虽然必要,同源策略也对 XHR 的实用性施加了严格限制:如果服务器希望向运行在不同源的脚本提供资源怎么办?这就是”跨源资源共享”(CORS)的用武之地!CORS 为客户端跨源请求提供了一个安全的主动选择机制:

// 脚本来源:(http, example.com, 80)

// 同源请求
fetch('/resource.js')
  .then(response => response.text())
  .then(data => { ... });

// 跨源请求
fetch('http://thirdparty.com/resource.js')
  .then(response => response.text())
  .then(data => { ... });

CORS 请求使用相同的 Fetch API,唯一区别是所请求资源的 URL 与脚本执行来源不同:在上例中,脚本从(http, example.com, 80)执行,第二个 Fetch 请求访问的是(http, thirdparty.com, 80)的 resource.js。

CORS 请求的主动选择认证机制在较低层处理:当请求发出时,浏览器自动附加受保护的Origin HTTP 头,宣告请求来源。反过来,远程服务器能够检查Origin头,并通过在响应中返回Access-Control-Allow-Origin头来决定是否允许该请求:

=> 请求
GET /resource.js HTTP/1.1
Host: thirdparty.com
Origin: http://example.com
...

<= 响应
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://example.com
...
  • Origin头由浏览器自动设置
  • 选择加入头由服务器设置

在上例中,thirdparty.com 通过返回适当的访问控制头,选择与 example.com 进行跨源资源共享。或者,如果它希望禁止访问,可以简单地省略Access-Control-Allow-Origin头,客户端浏览器将自动使已发送的请求失败。

如果第三方服务器不支持 CORS,客户端请求将失败,因为客户端始终验证选择加入头的存在。作为特例,CORS 也允许服务器返回通配符(Access-Control-Allow-Origin: *)表示允许任何来源访问。然而,在启用此策略前请三思!

至此,我们大功告成了吗?事实证明,并非如此,因为 CORS 采取了多项额外安全预防措施以确保服务器知晓 CORS:

  • CORS 请求默认省略用户凭证如 Cookie 和 HTTP 认证
  • 客户端仅限于发起”简单跨源请求”,这限制了允许的请求方法(GET、POST、HEAD)以及可发送和读取的 HTTP 头

要启用 Cookie 和 HTTP 认证,客户端必须在发起请求时设置额外选项(credentials: 'include'),服务器也必须响应适当的头(Access-Control-Allow-Credentials: true)以表明它明知允许应用包含私有用户数据。类似地,如果客户端需要写入或读取自定义 HTTP 头,或希望使用”非简单方法”请求,则必须首先通过发起预检请求获得第三方服务器的许可:

=> 预检请求
OPTIONS /resource.js HTTP/1.1
Host: thirdparty.com
Origin: http://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: My-Custom-Header
...

<= 预检响应
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: My-Custom-Header
...

(实际HTTP请求)
  • 预检 OPTIONS 请求用于验证权限
  • 来自第三方源的成功预检响应
  • 实际 CORS 请求

官方 W3C CORS 规范定义了何时何地必须使用预检请求:“简单”请求可以跳过,但多种条件会触发它,并增加至少一次完整的网络往返延迟来验证权限。好消息是,一旦发起预检请求,客户端可以缓存它以避免每次请求都进行相同验证。

CORS 获得所有现代浏览器支持;参见 caniuse.com/cors。如需深入了解各种 CORS 策略与实现,请参考官方 W3C 标准。


使用 Fetch 下载数据

Fetch 可以传输文本和二进制数据。事实上,浏览器为多种原生数据类型提供自动编码和解码,允许应用将这些类型直接传递给 Fetch 以进行适当编码,反之,浏览器也能自动解码这些类型:

类型描述
ArrayBuffer固定长度的二进制数据缓冲区
Blob不可变数据的二进制大对象
FormData编码的表单数据键值对
JSON表示简单数据结构的 JavaScript 对象
Text简单文本字符串

浏览器可以依赖 HTTP 内容类型协商推断适当的数据类型(例如将application/json响应解码为 JSON 对象),或者应用可以在发起 Fetch 请求时显式覆盖数据类型:

fetch('/images/photo.webp')
  .then(response => {
    if (!response.ok) throw new Error('Network response was not ok');
    return response.blob(); // 设置返回数据类型为blob
  })
  .then(blob => {
    const img = document.createElement('img');
    img.src = URL.createObjectURL(blob); // 从blob创建唯一对象URI并设为图片来源
    img.onload = () => {
      URL.revokeObjectURL(img.src); // 图片加载完成后释放对象URI
    };
    document.body.appendChild(img);
  });

注意,我们以原生格式传输图片资源,不依赖 base64 编码,并在页面中添加图片元素而不依赖 data URI。在 JavaScript 中处理接收到的二进制数据时,没有网络传输开销或编码开销!Fetch API 让我们能够从 JavaScript 脚本化地构建高效、动态的应用,无论数据类型如何。

Blob 接口是 HTML5 File API 的一部分,作为任意数据块(二进制或文本)的不透明引用。Blob 引用本身功能有限:你可以查询其大小、MIME 类型,并将其拆分为更小的 blob。然而,它的真正角色是作为各种 JavaScript API 之间的高效交换机制。


使用 Fetch 上传数据

通过 Fetch 上传数据对所有数据类型都同样简单高效。事实上,代码基本相同,唯一区别是在调用 fetch 时传入body选项。其余由浏览器处理:

// 上传简单文本字符串到服务器
fetch('/upload', {
  method: 'POST',
  body: 'text string'
})
.then(response => { ... });

// 通过FormData API创建动态表单
const formData = new FormData();
formData.append('id', 123456);
formData.append('topic', 'performance');

fetch('/upload', {
  method: 'POST',
  body: formData // 上传multipart/form-data对象到服务器
})
.then(response => { ... });

// 创建无符号8位整数类型的数组(ArrayBuffer)
const uInt8Array = new Uint8Array([1, 2, 3]);
fetch('/upload', {
  method: 'POST',
  body: uInt8Array.buffer // 上传字节块到服务器
})
.then(response => { ... });

Fetch 的body选项接受DOMStringBlobBufferSourceFormDataURLSearchParamsReadableStreamUSVString对象,自动执行适当编码,设置适当的 HTTP 内容类型,并分发请求。需要发送二进制 blob 或上传用户提供的文件?很简单:获取对象引用并传给 Fetch。事实上,稍加工作,我们还可以将大文件拆分为小块:

const blob = ...; // 任意数据blob(二进制或文本)
const BYTES_PER_CHUNK = 1024 * 1024; // 设置块大小为1MB
const SIZE = blob.size;

let start = 0;
let end = BYTES_PER_CHUNK;

while(start < SIZE) { // 以1MB增量遍历提供的数据
  fetch('/upload', {
    method: 'POST',
    headers: {
      'Content-Range': `${start}-${end}/${SIZE}` // 宣告上传的数据范围(起始-结束/总计)
    },
    body: blob.slice(start, end) // 通过Fetch上传1MB数据切片
  })
  .then(response => { ... });

  start = end;
  end = start + BYTES_PER_CHUNK;
}

Fetch 支持请求流(通过ReadableStream),这为我们提供了真正的上传流能力。然而,上述分块上传示例展示了一种简单的应用层解决方案:文件通过多个 Fetch 请求分块上传。这种模式绝不是真正请求流 API 的替代品,但对于某些应用来说仍是一个可行的解决方案。

对大型文件上传进行切片是一种在连接不稳定或间歇性环境下提供更健壮 API 的好技术——例如,如果某个块因连接断开而失败,应用可以重试,或稍后恢复上传,而不是从头开始完整传输。


监控下载与上传进度

网络连接可能是间歇性的,延迟和带宽变化很大。那么我们如何知道 Fetch 请求是成功、超时还是失败?Fetch API 通过ReadableStreamResponse对象提供了监控进度的能力,但对于简单的进度事件,我们可能需要借助XMLHttpRequest的进度事件,或使用新的fetchAbortController结合的模式。然而,对于上传进度,现代 Fetch 结合XMLHttpRequest风格的进度监控仍是最实用的方案。

实际上,对于需要详细进度监控的场景,许多开发者仍会选择 XHR,或者使用新的fetch上传跟踪提案(仍在标准化中)。以下是使用 XHR 进行进度监控的示例(因为 Fetch 在此领域仍在追赶):

const xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.timeout = 5000; // 设置请求超时为5000毫秒(默认:无超时)

xhr.upload.addEventListener('progress', (event) => { // 注册上传进度事件回调
  if (event.lengthComputable) {
    const progress = (event.loaded / event.total) * 100; // 计算传输进度
    console.log(`Upload progress: ${progress}%`);
  }
});

xhr.addEventListener('load', () => { ... }); // 注册请求成功回调
xhr.addEventListener('error', () => { ... }); // 注册请求失败回调

xhr.send(formData);

loaderror事件将触发一次以指示 XHR 传输的最终状态,而progress事件可以触发多次,提供跟踪传输状态的便利 API:我们可以比较loaded属性与total来估算已传输数据量。

要估算传输数据量,服务器必须在响应中提供内容长度:我们无法估算分块传输的进度,因为根据定义,响应的总大小未知。

另外,XHR 请求没有默认超时,这意味着请求可能”进行中”无限期。作为最佳实践,始终为你的应用设置有意义的超时并处理错误!

对于 Fetch,我们可以使用AbortController实现超时:

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时

fetch('/resource', { signal: controller.signal })
  .then(response => {
    clearTimeout(timeoutId);
    return response;
  })
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('Request timed out');
    }
  });

流式数据传输

在某些情况下,应用可能需要或希望增量处理数据流:在客户端数据可用时立即上传到服务器,或在数据从服务器到达时立即处理下载的数据。Fetch API 通过ReadableStream为这一重要用例提供了原生支持:

// 流式下载示例
const response = await fetch('/stream');
const reader = response.body.getReader();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  // 处理新接收的数据块(value是Uint8Array)
  console.log(`Received ${value.length} bytes`);
}

与 XHR 不同,Fetch 从一开始就将流式传输作为一等公民设计。ReadableStream接口允许我们增量读取响应体,而不必等待整个下载完成。这是处理大型响应或实时数据流的理想方案。

对于上传流,Fetch 同样支持ReadableStream作为请求体:

const stream = new ReadableStream({
  start(controller) {
    // 可以随时间推送数据到流
    controller.enqueue(new TextEncoder().encode('Hello '));
    setTimeout(() => {
      controller.enqueue(new TextEncoder().encode('World'));
      controller.close();
    }, 1000);
  }
});

fetch('/upload', {
  method: 'POST',
  body: stream,
  headers: { 'Content-Type': 'text/plain' }
});

这代表了相比 XHR 的重大进步。XHR 从未将流式传输作为官方用例,导致实现效率低下且受限。Fetch 与 Streams API 的结合为浏览器中的高效网络请求流式传输铺平了道路。

Streams API 现已获得所有现代浏览器支持;参见 caniuse.com/streams。


实时通知与消息推送

Fetch 提供了一种简单高效的方式来同步客户端与服务器的更新:必要时,客户端分发 Fetch 请求更新服务器上的适当数据。然而,反过来——服务器如何通知客户端数据已更新——则困难得多。

HTTP 没有提供服务器向客户端发起新连接的方式。因此,要接收实时通知,客户端必须要么轮询服务器获取更新,要么利用流式传输允许服务器在更新可用时推送新通知。幸运的是,如我们在上一节所见,Fetch 支持流式下载,这为我们提供了新的可能性。

“实时”对不同应用有不同含义:有些应用要求亚毫秒级开销,而其他应用可能接受以分钟为单位的延迟。要确定最佳传输方式,首先为你的应用定义明确的延迟和开销目标!


轮询策略

从服务器获取更新的最简单策略之一是让客户定期进行检查:客户端可以在定期间隔(轮询服务器)发起后台 Fetch 请求以检查更新。如果服务器有新数据,则在响应中返回,否则响应为空。

轮询实现简单,但通常效率很低。轮询间隔的选择至关重要:长轮询间隔意味着更新交付延迟,而短间隔则导致客户端和服务器的不必要流量和高开销。让我们考虑最简单的例子:

function checkUpdates(url) {
  fetch(url)
    .then(response => response.json())
    .then(data => { ... }); // 处理从服务器接收的更新
}

setInterval(() => { checkUpdates('/updates') }, 60000); // 每60秒发起一次Fetch请求
  • 每个 Fetch 请求都是独立的 HTTP 请求,平均而言,HTTP 会产生约 800 字节的开销(不含 HTTP Cookie)用于请求/响应头。
  • 定期检查在数据以可预测间隔到达时工作良好。不幸的是,可预测的到达率是例外而非常态。因此,定期轮询会引入消息在服务器可用与其交付给客户端之间的额外延迟。
  • 除非经过仔细考虑,轮询通常在无线网络上成为昂贵的性能反模式;参见《消除定期且低效的数据传输》。唤醒无线电消耗大量电池电量!

最优轮询间隔是什么? 没有唯一答案。频率取决于应用需求,在效率与消息延迟之间存在固有权衡。因此,轮询适合轮询间隔较长、新事件以可预测速率到达且传输负载较大的应用。这种组合抵消了额外 HTTP 开销,并最小化消息交付延迟。


长轮询技术

定期轮询的挑战在于可能存在许多不必要且为空的检查。考虑到这一点,如果我们对轮询流程稍作修改:当没有更新可用时,不返回空响应,而是保持连接空闲直到更新可用会怎样?

利用长期保持的 HTTP 请求(“挂起的 GET”)允许服务器向浏览器推送数据的技术通常被称为”Comet”。然而,你也可能遇到其他名称,如”反向 AJAX”、“AJAX 推送”和”HTTP 推送”。

通过保持连接打开直到更新可用(长轮询),数据可以在服务器上一旦可用就立即发送给客户端。因此,长轮询为消息延迟提供了最佳场景,它还消除了空检查,减少了 Fetch 请求数量和轮询的整体开销。一旦更新交付,长轮询请求即完成,客户端可以发起另一个长轮询请求并等待下一条可用消息:

async function checkUpdates(url) {
  try {
    const response = await fetch(url);
    const data = await response.json();
    ... // 处理接收的更新
  } catch (error) {
    console.error('Long-poll failed:', error);
  }
  checkUpdates('/updates'); // 发起下一个更新的长轮询请求(永远循环)
}

checkUpdates('/updates'); // 发起初始长轮询Fetch请求

至此,长轮询是否总是比定期轮询更好的选择?除非消息到达率已知且恒定,长轮询总是提供更好的消息延迟。如果这是主要标准,长轮询是赢家。

另一方面,开销讨论需要更细致的观点。首先,注意每条交付的消息仍产生相同的 HTTP 开销;每条新消息都是一个独立的 HTTP 请求。然而,如果消息到达率很高,长轮询将比定期轮询发起更多请求!

长轮询通过最小化消息延迟动态适应消息到达率,这是你可能想要或不想要的行为。如果对消息延迟有一定容忍度,定期轮询可能是更高效的传输方式——例如,如果更新率很高,定期轮询提供了一种简单的”消息聚合”机制,可以减少请求数量并改善移动设备上的电池寿命。

在实践中,并非所有消息都有相同的优先级或延迟要求。因此,你可能希望考虑混合策略:在服务器上聚合低优先级更新,并触发高优先级更新的立即交付;参见《Nagle 算法与高效服务器推送》。

Facebook Chat 通过长轮询

在实践中,长轮询已成为通过 XHR/Fetch 交付实时通知最广泛使用的方法之一。虽然它可能不是最高效的传输方式,但它简单、健壮,且任何支持 Fetch 的浏览器都支持它。像 Facebook Chat 这样的流行产品于 2008 年首次通过这种方法部署:

“我们选择的将文本从一个用户传递到另一个用户的方法涉及在每个 Facebook 页面加载一个 iframe,并让该 iframe 的 JavaScript 通过持久连接发起 HTTP GET 请求,该连接直到服务器有数据给客户端时才返回。如果连接中断或超时,请求会重新建立。这绝不是新技术:它是 Comet 的变种,特别是 XHR 长轮询,和/或 BOSH。”

—— Facebook Chat,Facebook 工程博客

今天,我们可以通过 Server-Sent Events 和 WebSocket 更高效地交付相同功能。话虽如此,长轮询仍是许多实时框架的常见降级策略。如果其他方法都失败了,长轮询来救场!


Fetch 使用场景与性能

XMLHttpRequest 让我们从构建页面跃迁到在浏览器中构建交互式 Web 应用。首先,它实现了浏览器内的异步通信,但同样重要的是,它也让这个过程变得简单。分发和控制脚本化 HTTP 请求只需几行 JavaScript 代码,浏览器处理其余一切:

  • 浏览器格式化 HTTP 请求并解析响应
  • 浏览器强制执行相关安全(同源)策略
  • 浏览器处理内容协商(如 gzip)
  • 浏览器处理请求和响应缓存
  • 浏览器处理认证、重定向等等……

因此,对于遵循 HTTP 请求-响应周期的任何传输,XHR 是一个多功能且高性能的传输方式。需要获取需要认证的资源、传输时应压缩、并应缓存以备将来查找?浏览器处理所有这些及更多,让我们专注于应用逻辑!

Fetch 继承了所有这些能力,同时提供了更现代的基于 Promise 的 API,支持流式传输,并在 Service Workers 中可拦截。然而,Fetch 也有其局限性。正如我们所见,虽然 Fetch 支持流式传输,但低级别的进度监控(特别是上传进度)仍不如 XHR 成熟。

类似地,使用 Fetch 交付实时更新也没有一种最佳策略。定期轮询产生高开销和消息延迟。长轮询提供低延迟,但仍有每条消息的相同开销;每条消息都是自己的 HTTP 请求。要同时拥有低延迟和低开销,我们需要真正的服务器推送能力!

因此,虽然 Fetch 是”实时”交付的流行机制,但它可能不是该工作的最佳性能传输方式。现代浏览器支持更简单、更高效的选项,如 Server-Sent Events 和 WebSocket。因此,除非你有特定原因需要长轮询,否则请使用它们。