使用 JavaScript 发出网络请求

在现代网站和应用程序中,另一个非常常见的任务是发出网络请求,从服务器检索单个数据项,以更新网页的各个部分,而无需加载整个新页面。这个看似微小的细节对网站的性能和行为产生了巨大的影响,因此在本文中,我们将解释这个概念并介绍使其成为可能的技术:特别是 Fetch API

预备知识 了解 HTMLCSS 基础,熟悉前面课程中介绍的 JavaScript 基础。
学习成果
  • 异步网络请求,这是 Web 上最常见的异步 JavaScript 用例。
  • 从网络获取的常见资源类型:JSON、媒体资产、来自 RESTful API 的数据。
  • 如何使用 fetch() 实现异步网络请求。

这里有什么问题?

一个网页由一个 HTML 页面和(通常)其他各种文件组成,例如样式表、脚本和图像。Web 上页面加载的基本模型是您的浏览器向服务器发出一个或多个 HTTP 请求,以获取显示页面所需的文件,服务器响应所请求的文件。如果您访问另一个页面,浏览器会请求新的文件,服务器会响应这些文件。

Traditional page loading

这种模型对许多网站都非常有效。但考虑一个数据驱动的网站。例如,像 温哥华公共图书馆 这样的图书馆网站。除此之外,您可以将这样的网站视为数据库的用户界面。它可能允许您搜索特定类型的书籍,或者根据您以前借阅过的书籍向您显示可能喜欢的书籍推荐。当您执行此操作时,它需要使用要显示的新书籍集来更新页面。但请注意,页面内容的大部分(包括页面标题、侧边栏和页脚等项目)保持不变。

传统模型的麻烦在于,即使我们只需要更新页面的一部分,我们也必须获取并加载整个页面。这效率低下,并可能导致糟糕的用户体验。

因此,许多网站不使用传统模型,而是使用 JavaScript API 从服务器请求数据并更新页面内容,而无需页面加载。因此,当用户搜索新产品时,浏览器只请求更新页面所需的数据——例如,要显示的新书籍集。

Using fetch to update pages

这里主要的 API 是 Fetch API。它使页面中运行的 JavaScript 能够向服务器发出 HTTP 请求以检索特定资源。当服务器提供这些资源时,JavaScript 可以使用这些数据来更新页面,通常是通过使用 DOM 操作 API。请求的数据通常是 JSON,这是一种传输结构化数据的好格式,但也可以是 HTML 或纯文本。

这是亚马逊、YouTube、eBay 等数据驱动网站的常见模式。使用此模型,

  • 页面更新速度更快,您无需等待页面刷新,这意味着网站感觉更快、响应更及时。
  • 每次更新下载的数据量更少,这意味着带宽浪费更少。这在宽带连接的台式机上可能不是一个大问题,但在移动设备和没有普及高速互联网服务的国家/地区,这是一个主要问题。

注意:在早期,这种通用技术被称为 异步 JavaScript 和 XML (AJAX),因为它倾向于请求 XML 数据。如今通常不是这样(您更可能请求 JSON),但结果仍然相同,“AJAX”一词仍然经常用于描述该技术。

为了进一步加快速度,一些网站在首次请求时还会将资产和数据存储在用户的计算机上,这意味着在后续访问中,它们会使用本地版本,而不是每次首次加载页面时都下载新的副本。内容仅在更新时才从服务器重新加载。

Fetch API

在本节中,我们将介绍几个 Fetch API 的示例。

以下示例具有一定的复杂性,并展示了如何在一些实际场景中使用 Fetch API。如果您以前从未使用过 fetch,您可能希望从 Scrimba 的 First fetch MDN 学习合作伙伴 交互式教程开始,它提供了一个非常简单的入门演练。

获取文本内容

对于此示例,我们将从几个不同的文本文件中请求数据,并使用它们来填充内容区域。

这一系列文件将充当我们虚假的数据库;在实际应用程序中,我们更可能使用像 PHP、Python 或 Node 这样的服务器端语言从数据库请求数据。但是,在这里,我们希望保持简单并专注于客户端部分。

要开始此示例,请在您的计算机上的新目录中本地复制 fetch-start.html 和四个文本文件 — verse1.txtverse2.txtverse3.txtverse4.txt。在此示例中,当在下拉菜单中选择时,我们将获取诗歌(您可能很熟悉)的不同诗节。

<script> 元素内部,添加以下代码。这存储了对 <select><pre> 元素的引用,并向 <select> 元素添加了一个监听器,以便当用户选择一个新值时,新值作为参数传递给名为 updateDisplay() 的函数。

js
const verseChoose = document.querySelector("select");
const poemDisplay = document.querySelector("pre");

verseChoose.addEventListener("change", () => {
  const verse = verseChoose.value;
  updateDisplay(verse);
});

让我们定义 updateDisplay() 函数。首先,将以下内容放在您之前的代码块下面——这是函数的空壳。

js
function updateDisplay(verse) {

}

我们将通过构造一个指向我们要加载的文本文件的相对 URL 来开始我们的函数,因为我们稍后会需要它。任何时候 <select> 元素的值都与选定 <option> 内部的文本相同(除非您在值属性中指定了不同的值)——例如“Verse 1”。对应的诗节文本文件是“verse1.txt”,并且与 HTML 文件在同一目录中,因此只需文件名即可。

然而,Web 服务器往往区分大小写,并且文件名中没有空格。要将“Verse 1”转换为“verse1.txt”,我们需要将“V”转换为小写,删除空格,并在末尾添加“.txt”。这可以通过 replace()toLowerCase()模板字面量 来完成。在 updateDisplay() 函数中添加以下行

js
verse = verse.replace(" ", "").toLowerCase();
const url = `${verse}.txt`;

最后,我们准备好使用 Fetch API

js
// Call `fetch()`, passing in the URL.
fetch(url)
  // fetch() returns a promise. When we have received a response from the server,
  // the promise's `then()` handler is called with the response.
  .then((response) => {
    // Our handler throws an error if the request did not succeed.
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    // Otherwise (if the response succeeded), our handler fetches the response
    // as text by calling response.text(), and immediately returns the promise
    // returned by `response.text()`.
    return response.text();
  })
  // When response.text() has succeeded, the `then()` handler is called with
  // the text, and we copy it into the `poemDisplay` box.
  .then((text) => {
    poemDisplay.textContent = text;
  })
  // Catch any errors that might happen, and display a message
  // in the `poemDisplay` box.
  .catch((error) => {
    poemDisplay.textContent = `Could not fetch verse: ${error}`;
  });

这里有很多值得深入探讨的地方。

首先,Fetch API 的入口点是一个名为 fetch() 的全局函数,它将 URL 作为参数(它接受另一个可选参数用于自定义设置,但我们这里不使用)。

接下来,fetch() 是一个异步 API,它返回一个 Promise。如果您不知道那是什么,请阅读有关 异步 JavaScript 的模块,特别是有关 promise 的课程,然后回到这里。您会发现该文章也谈到了 fetch() API!

因此,由于 fetch() 返回一个 promise,我们将一个函数传递给返回 promise 的 then() 方法。当 HTTP 请求从服务器收到响应时,将调用此方法。在处理程序中,我们检查请求是否成功,如果失败则抛出错误。否则,我们调用 response.text(),以文本形式获取响应正文。

结果发现 response.text() 也是异步的,所以我们返回它返回的 promise,并将一个函数传递到这个新 promise 的 then() 方法中。当响应文本准备好时,将调用此函数,并在其中使用文本更新我们的 <pre> 块。

最后,我们在末尾链上一个 catch() 处理程序,以捕获我们调用或其处理程序中的任何异步函数抛出的任何错误。

这个例子目前存在的一个问题是它在首次加载时不会显示任何诗歌。为了解决这个问题,在代码底部(紧邻结束的 </script> 标签上方)添加以下两行,以默认加载第一节,并确保 <select> 元素始终显示正确的值

js
updateDisplay("Verse 1");
verseChoose.value = "Verse 1";

从服务器提供您的示例

如果您只是从本地文件运行示例,现代浏览器将不会运行 HTTP 请求。这是由于安全限制(有关 Web 安全的更多信息,请阅读 网站安全)。

为了解决这个问题,我们需要通过运行本地 Web 服务器来测试示例。要了解如何执行此操作,请参阅 如何设置本地测试服务器?

罐头店

在此示例中,我们创建了一个名为 The Can Store 的示例网站——它是一个只销售罐头食品的虚构超市。您可以在 GitHub 上查看此示例,并查看源代码

A fake e-commerce site showing search options in the left hand column, and product search results in the right-hand column.

默认情况下,该网站显示所有产品,但您可以使用左侧栏中的表单控件按类别、搜索词或两者兼有来筛选它们。

有很多复杂的代码处理按类别和搜索词筛选产品,操作字符串以便数据在 UI 中正确显示等。我们不会在文章中讨论所有这些,但您可以在代码中找到详细的注释(参见 can-script.js)。

但是,我们将解释 Fetch 代码。

使用 Fetch 的第一个代码块可以在 JavaScript 的开头找到

js
fetch("products.json")
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.json();
  })
  .then((json) => initialize(json))
  .catch((err) => console.error(`Fetch problem: ${err.message}`));

fetch() 函数返回一个 promise。如果它成功完成,第一个 .then() 块中的函数将包含从网络返回的 response

在这个函数中,我们

  • 检查服务器是否没有返回错误(例如 404 Not Found)。如果返回了,我们抛出错误。
  • 对响应调用 json()。这将以 JSON 对象 的形式检索数据。我们返回 response.json() 返回的 promise。

接下来,我们将一个函数传递到该返回 promise 的 then() 方法中。该函数将获得一个包含 JSON 形式的响应数据的对象,我们将其传递给 initialize() 函数。正是 initialize() 启动了在用户界面中显示所有产品的过程。

为了处理错误,我们在链的末尾链接了一个 .catch() 块。如果 promise 因某种原因失败,它就会运行。在其中,我们包含一个作为参数传递的函数,一个 err 对象。此 err 对象可用于报告发生的错误的性质,在此示例中我们使用简单的 console.error() 来完成。

然而,一个完整的网站会通过在用户的屏幕上显示消息并提供补救情况的选项来更优雅地处理此错误,但我们不需要比简单的 console.error() 更多的东西。

您可以自己测试失败案例

  1. 制作示例文件的本地副本。
  2. 通过 Web 服务器运行代码(如上文 从服务器提供您的示例 中所述)。
  3. 修改要获取的文件的路径,例如将其更改为“produc.json”(确保拼写错误)。
  4. 现在在浏览器中加载索引文件(通过 localhost:8000)并查看浏览器开发人员控制台。您将看到一条类似于“Fetch problem: HTTP error: 404”的消息。

第二个 Fetch 块可以在 fetchBlob() 函数中找到

js
fetch(url)
  .then((response) => {
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }
    return response.blob();
  })
  .then((blob) => showProduct(blob, product))
  .catch((err) => console.error(`Fetch problem: ${err.message}`));

这与前一个工作方式大致相同,不同之处在于我们使用 blob() 而不是 json()。在这种情况下,我们希望以图像文件形式返回响应,我们为此使用的数据格式是 Blob(该术语是“Binary Large Object”的缩写,基本上可以用来表示大型文件状对象,例如图像或视频文件)。

一旦我们成功接收到 blob,我们就会将其传递给我们的 showProduct() 函数,该函数会显示它。

XMLHttpRequest API

有时,尤其是在旧代码中,您会看到另一个名为 XMLHttpRequest(通常缩写为“XHR”)的 API 用于发出 HTTP 请求。这早于 Fetch,并且是第一个广泛用于实现 AJAX 的 API。我们建议您尽可能使用 Fetch:它是一个更简单的 API,并且比 XMLHttpRequest 具有更多功能。我们不会介绍使用 XMLHttpRequest 的示例,但我们将向您展示我们第一个罐头店请求的 XMLHttpRequest 版本会是什么样子

js
const request = new XMLHttpRequest();

try {
  request.open("GET", "products.json");

  request.responseType = "json";

  request.addEventListener("load", () => initialize(request.response));
  request.addEventListener("error", () => console.error("XHR error"));

  request.send();
} catch (error) {
  console.error(`XHR error ${request.status}`);
}

这分为五个阶段

  1. 创建一个新的 XMLHttpRequest 对象。
  2. 调用其 open() 方法进行初始化。
  3. 为其 load 事件添加一个事件监听器,该事件在响应成功完成时触发。在监听器中,我们使用数据调用 initialize()
  4. 为其 error 事件添加一个事件监听器,该事件在请求遇到错误时触发
  5. 发送请求。

我们还必须将整个事情包装在 try...catch 块中,以处理 open()send() 抛出的任何错误。

希望您认为 Fetch API 比这有所改进。特别是,请注意我们必须在两个不同的地方处理错误。

总结

本文展示了如何开始使用 Fetch 从服务器获取数据。

另见

然而,本文讨论了许多不同的主题,这只是触及了皮毛。有关这些主题的更多详细信息,请尝试以下文章