再也不学AJAX了!(二)使用AJAX ② Fetch API

「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第三篇,最近更新于 2023 年 1 月。

您可能会觉得惊讶,在上一篇文章中我向您介绍的 XMLHttpReuqest 对象是 Microsoft 公司在 1999 年提出的,并且是 IE5 的一部分。而在 2014 年,依然是 Microsoft 公司,提出了我们如今耳熟能详的 Promise API,并迅速被 TC39 纳入 JavaScript 标准。随后,W3C 公布了一种更加简洁,优雅的 AJAX 请求 API,这便是本篇文章的主角:「Fetch API」。

1. 什么是 Fetch API

和 XMLHttpRequest 对象一样,Fetch API 是由浏览器提供的,用于获取或操作网络资源的 JavaScript API。它允许开发者通过 JavaScript 代码发送和接收 HTTP 请求和响应。Fetch API 现如今已经有广泛的浏览器支持度,在绝大多数情况下都成为 XMLHttpRequest 对象的替代品。

和 XMLHttpRequest 在 Node.js 中需要使用第三方包使用的命运不同,Node 在 2022 年 2 月 1 日将 Fetch API 的 PR 合并, 在 17.5 版本以上的 Node.js 中,Fetch API 都可以直接使用。

2. 上手 Fetch API

2.1 Fetch API 的结构

在上一篇文章中,我们详细地介绍了如何通过 XHRHttpRequest 对象发送 AJAX 请求。在本章中,我们将对比 XHRHttpRequest 与 Fetch API 的 API 设计,从而理解为什么 Fetch API 的设计更加优雅和现代。

通过下方的图片,结合上一篇文章讲解的内容,您应该很容易理解 XHRHttpRequest API 的整体结构。

再也不学AJAX了!(二)使用AJAX ② Fetch API_第1张图片

Promise 还没有诞生的年代,XHRHttpRequest 对象主要通过事件订阅/派发机制,解决请求/响应的异步问题。此时所有的必须属性和扩展属性都挂载在 XHRHttpRequest 对象上,并且提供的方法和属性非常底层,很多功能,例如监听响应状态,都需要开发者手动实现。这就催生了例如 axios 等三方包提供语法糖减少编码的复杂性。

而通过下方的图片,您可以看到 Fetch API 的整体设计要更加简洁,容易理解:

再也不学AJAX了!(二)使用AJAX ② Fetch API_第2张图片

由图中可见,Fetch API 基于 Promise 提供了直观的流式处理方法 .fetch().then().catch()。并且将 AJAX 请求所需的所有信息合理分装在三个类中:RequestHeadersResponse。这种清晰的代码组织方式,使开发者能够非常轻松的理解和使用 Fetch API 的相关特性。

此外,Fetch API 还利用 Promise 特性(.catch() 方法),将错误处理显示的通过接口方法暴露给用户,这使得开发者能够编写出更加稳健的代码。

总之,Fetch API 通过结合最新的 JS 异步处理方案 Promise,良好的组织 AJAX 请求数据,方法之间的关系,让 JavaScript 开发者的日子变得更加轻松,并迫使 XHRHttpRequest API 与一系列提供语法糖的第三方库逐渐退出历史舞台。

2.2 Fetch API 提供的属性和方法

在了解 Fetch API 的整体设计后,我们来快速浏览一遍 Fetch API 提供的核心属性和方法。非常直观的,Fetch API 在全局提供了 fetch() 方法,该方法接收一个 Request 类的实例,或者两个参数:

  1. resource:表示资源获取地址,包含任何字符串化后有效的 URL 地址,是必须的;
  2. options:包含了所有除资源地址外其他自定义 HTTP 请求参数,是可选的,比较常用的属性有:

    • method:HTTP 请求方法,默认为 GET
    • headers:在此添加请求头部信息;值可以是一个包含有效属性的对象,或是一个 Headers 对象的实例(注意这些头部属性无法被添加);
    • body:任何要向服务端发送的数据,数据类型不限;
    • mode:请求的模式,涉及跨域,在后面的文章中我们会提到;
    • credentials:指定浏览器是否应该在请求中包含凭证,也和跨域有关,凭证包含 Cookie 和 HTTP 认证,有三个可选值:

      • omit:不包含任何凭证;
      • same-origin:仅在请求同源地址时,包含凭证(默认值);
      • include:应始终在请求中包含凭证;
    • cache:指定了本次请求与浏览器 HTTP 缓存的关系;
    • redirect:指定了如何处理包含重定向的响应;

当调用 fetch() 方法后,它会立即返回一个 Promsie 对象,该 Promise 将在接收到响应头时切换为 resolved,并返回一个 Response 对象的实例。

注意,也许和你想的不同,fetch() 方法返回的 Promise 对象只有在网络出现问题时,才会变更状态为 rejected。也就是说,开发者需要手动检查响应的 HTTP 状态码:

fetch()
    .then((res) => {
        if (res.ok) {
            console.log("ok")
            return res.json()
        } else {
            console.log("error")
        }
    })
    .then()
    .catch((err) => {
        console.log(err)
    })

2.3 Fetch API 实战

在了解到 Fetch API 的基本属性和方法后,我将通过四个经典的示例来介绍如何在项目中运用 Fetch API。

2.3.1 发送 POST 请求

使用 Fetch API 发送 POST 请求非常简单,只需要设置 method 属性为 POST,并将请求数据 stringfiy 后添加到 body 属性中即可。

fetch("/api/submit", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "John", age: 30 })
})
  .then((res) => {
   if (res.ok) {
        return res.json()
    } else {
        console.log("error")
    }
  })
  .then(data => {
    console.log("Success:", data);
  })
  .catch(error => {
    console.error("Error:", error);
  });

2.3.2 处理大文件

可以通过流式处理的方式,处理大文件响应,这使得我们可以节约更多内存并获得更高性能:

fetch("large-file.jpg")
  .then(response => {
    const reader = response.body.getReader();
    return new ReadableStream({
      start(controller) {
        function push() {
          reader.read().then(({ done, value }) => {
            if (done) {
              controller.close();
              return;
            }
            controller.enqueue(value);
            push();
          });
        }
        push();
      }
    });
  })
  .then(stream => new Response(stream))
  .then(response => response.blob())
  .then(myBlob => {
    var objectURL = URL.createObjectURL(myBlob);
    // do something with objectURL
  });

这个例子中,使用 fetch() 方法请求了一个大图片文件,使用 response.body.getReader() 方法获取了一个可读数据流。然后利用 ReadableStream API 将响应数据从流中一段一段的读取出来,并在读取过程中进行处理,这样就可以避免加载整个大文件到内存中,从而解决内存爆炸的问题。

2.3.3 跨域发送请求

使用 Fetch API 发送跨域请求的代码如下:

fetch("https://example.com/data", {
  mode: "cors",
  credentials: "include"
})
  .then((res) => {
    if (res.ok) {
        return res.json()
    } else {
        console.log("error")
    }
  })
  .then(data => {
    console.log("Success:", data);
  })
  .catch(error => {
    console.error("Error:", error);
  });

请注意,当您通过向 .fetch() 方法传入的值是 Request 的实例时,mode 的默认值为 cors。当您不需要向服务端传递 cookie 时,可以不设置 credentials 属性。

3. Fetch API vs XHRHttpRequest 对象

在完整的介绍完 Fetch API 的内容后,是时候站在功能的角度上思考 Fetch API 与 XHRHttpRequest 对象的使用时机了。虽然我之前给出了一个简单的标准:「当不考虑老版本浏览器兼容的情况下,始终使用 Fetch API!」但是其实这两者之间,还存在一些功能上的差异。

3.1 Fetch API 特性

3.1.1 更方便的缓存控制

使用 XHRHttpRequest 时,我们需要编码指定请求头部信息 Cache-Control 控制浏览器的资源缓存行为。但是在 Fetch API 中,我们可以直接通过在 options 中传入 cache 属性实现这一点,该属性包含如下字段:

  • default:优先使用浏览器有效的缓存资源,当缓存资源过期后,向服务器进行条件查询,如果资源过期再发送新的请求;
  • no-store:永远不使用浏览器缓存,直接发送请求,获得的响应不去更新浏览器缓存;
  • reload:永远不使用浏览器缓存,直接发送请求,但是获得响应后更新缓存;
  • no-cache: 注意这里并不意味着「不使用缓存」,而是指每次检查浏览器缓存资源时,无论资源是否过期,都向服务器进行条件查询;
  • force-cache:强制使用浏览器缓存,不管资源是否过期,如果没有缓存资源,再发送请求;
  • only-if-cached:同 force-cache,但是当没有缓存资源时,由浏览器响应 504 状态(该模式仅在 mode: "same-origin" 配置下生效);

3.1.2 更方便的 CORS 控制

在下一章我们会深入讨论跨域问题,在 Web 世界,要想访问不同源下的资源,需要客户端和服务端的全面配合。与缓存控制类似,Fetch API 为我们提供了 modecredentials 两个配置属性,让开发者能够更轻松的指定请求是否跨域,以及 cookie 等身份信息是否伴随请求发送。

credentials 属性包含三个可选值:

  • omit:永远不发送或接收 cookie;
  • same-origin:仅当请求地址与当前地址同源时,发送 cookie(默认);
  • include:无论是否同源,均发送 cookie;

3.1.3 更方便的重定向控制

在 XHRHttpRequest 对象时,想要拒绝服务器响应的重定向命令,需要通过 xhr.followRedirect = false 指令实现。而在 Fetch API 中,这一项也被设计为一个配置选项 redirect,该属性有三个可选值:

  • follow:遵从服务器意见,进行重定向;
  • error:直接抛错,进入 .catch() 流程;
  • manual:拒绝重定向,返回服务器信息,由开发者自己控制;

3.1.4 支持更多数据格式

XMLHttpRequest 和 Fetch API 在返回响应实体时都提供了多种数据格式:

  • XMLHttpRequest 提供了几种常用的数据格式,例如:

    • responseText:返回字符串格式的响应实体;
    • responseXML:返回 XML 格式的响应实体;
    • response:返回 ArrayBuffer, Blob 或 FormData 格式的响应实体;
  • Fetch API 提供了更多数据格式,例如:

    • arrayBuffer():返回 ArrayBuffer 格式的响应实体;
    • blob():返回 Blob 格式的响应实体;
    • formData():返回 FormData 格式的响应实体;
    • json():返回 JSON 格式的响应实体;
    • text():返回字符串格式的响应实体;

3.1.5 支持服务端使用

正如前面提到过的,当 Node 版本大于 17.5 时,就可以直接使用 Fetch API。

3.2 XHRHttpRequest 专属特性

相信看到这里,您已经能够通过对比上一篇文章介绍的 XHRHttpRequest 对象总结出 Fetch API 所缺失的特性了,没错,相较于 XHRHttpRequest 对象,Fetch API:

  1. 无法查询请求进度;
  2. 无法指定请求超时时间;
  3. 无法中止请求;
  4. 无法获得更详尽的响应信息;
  5. 在低版本浏览器中不支持;

然而需要注意,虽然 Fetch API 缺少这些功能,但这并不意味着我们无法通过其他手段实现,例如对于废弃请求而言,Fetch API 实际上提供了另一个全局类:AbortController() 来实现这一点:

const controller = new AbortController();

fetch("/service", {
  method: "GET",
  signal: controller.signal,
})
  .then((res) => res.json())
  .then((json) => console.log(json))
  .catch((error) => console.error("Error:", error));

controller.abort();

4. 总结

在本篇文章中,我向您介绍了 Fetch API 方方面面的知识,并通过对比上一篇文章中介绍的 XHRHttpRequest 对象,为您展示了这两个 AJAX API 在设计,内容,编码和功能上的差异。总的来说 XHRHttpRequest 的 API 设计比较原始,开放了更底层的能力,但有良好的浏览器支持性,而 Fetch API 则基于 Promise 对内容做了良好的封装,并对用户暴露了众多实用的方法和属性

至此,您应该有能力从容地使用 JavaScript 发送 AJAX 请求,并在未来的 API 设计中运用一些 Fetch API 的设计思想。很高兴您能一路看到这里,在下一篇文章中,我将为您介绍 AJAX 技术的最后一个,但也是最至关重要的主题:「如何跨域请求资源」,敬请期待。

5. 参考资料

你可能感兴趣的:(再也不学AJAX了!(二)使用AJAX ② Fetch API)