从服务器获取数据

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

先决条件 JavaScript 基础知识(参见 入门构建块JavaScript 对象),以及 客户端 API 基础知识
目标 学习如何从服务器获取数据并使用它来更新网页内容。

这里的问题是什么?

网页由 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 的几个示例。

获取文本内容

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

这系列文件将充当我们的假数据库;在实际应用中,我们更有可能使用 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> 内部的文本相同(除非您在 value 属性中指定了不同的值)——例如“诗歌节 1”。相应的诗歌节文本文件为“verse1.txt”,并且与 HTML 文件位于同一目录中,因此只需使用文件名即可。

但是,Web 服务器往往区分大小写,并且文件名中没有空格。要将“诗歌节 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> 标记上方)添加以下两行,以默认加载诗歌节 1,并确保 <select> 元素始终显示正确的值

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

从服务器提供您的示例

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

要解决此问题,我们需要通过本地 Web 服务器运行示例来对其进行测试。要了解如何执行此操作,请阅读 我们有关设置本地测试服务器的指南

罐头商店

在此示例中,我们创建了一个名为“罐头商店”的示例网站——这是一家仅销售罐装商品的虚构超市。您可以在 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 未找到)。如果返回了错误,我们将抛出该错误。
  • 在响应上调用 json()。这将以 JSON 对象 的形式检索数据。我们返回 response.json() 返回的 Promise。

接下来,我们将一个函数传递给返回的 Promise 的 then() 方法。此函数将接收一个包含响应数据(以 JSON 格式)的对象,我们将此对象传递给 initialize() 函数。此函数启动在用户界面中显示所有产品的过程。

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

但是,一个完整的网站会更优雅地处理此错误,例如在用户的屏幕上显示一条消息,并可能提供解决问题的方法,但我们只需要一个简单的 console.error() 即可。

您可以自己测试失败的情况

  1. 创建示例文件的本地副本。
  2. 通过 Web 服务器运行代码(如上所述,在 从服务器提供示例 中)。
  3. 修改正在获取的文件的路径,例如将其更改为 'produc.json'(确保拼写错误)。
  4. 现在在浏览器中加载 index 文件(通过 localhost:8000),并在浏览器开发者控制台中查看。您将看到类似“Fetch 问题:HTTP 错误: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}`));

它的工作方式与前一个非常相似,除了不使用 json(),而是使用 blob()。在这种情况下,我们希望将响应作为图像文件返回,并且我们为此使用的数据格式是 Blob(该术语是“二进制大对象”的缩写,基本上可以用来表示大型文件类对象,例如图像或视频文件)。

成功接收 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 从服务器获取数据。

另请参阅

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