Learn web development

从服务器获取数据

翻译正在进行中。

在现代网站和应用中另一个常见的任务是从服务端获取个别数据来更新部分网页而不用加载整个页面。 这看起来是小细节却对网站性能和行为产生巨大的影响。所以我们将在这篇文章介绍概念和技术使它成为可能,例如: XMLHttpRequest 和 Fetch API.

先决条件: JavaScript 基础(see first steps, building blocks, JavaScript objects), the basics of Client-side APIs
目标: 学会如何从服务器获取数据并使用它更新网页内容.

这里有什么问题?

最初加载页面很简单  -- 你为网站发送一个请求到服务器, 只要没有出错你将会获取资源并显示网页到你的电脑上。

A basic representation of a web site architecture

这个模型的问题是当你想更新网页的任何部分,例如显示一套新的产品或者加载一个新的页面,你需要再一次加载整个页面。这是非常浪费的并且导致了差的用户体验尤其是现在的页面越来越大且越来越复杂。

Enter Ajax

这导致了创建允许网页请求小块数据(例如 HTML, XML, JSON, 或纯文本) 和 仅在需要时显示它们的技术,从而帮助解决上述问题。

这是通过使用诸如 XMLHttpRequest 之类的API或者 — 最近以来的 Fetch API 来实现. 这些技术允许网页直接处理对服务器上可用的特定资源的 HTTP 请求,并在显示之前根据需要对结果数据进行格式化。

注意:在早期,这种通用技术被称为Asynchronous JavaScript and XML(Ajax), 因为它倾向于使用XMLHttpRequest 来请求XML数据。 但通常不是这种情况 (你更有可能使用 XMLHttpRequest 或 Fetch 来请求JSON), 但结果仍然是一样的,术语“Ajax”仍然常用于描述这种技术。

A simple modern architecture for web sites

Ajax模型包括使用Web API作为代理来更智能地请求数据,而不仅仅是让浏览器重新加载整个页面。让我们来思考这个意义:

  1. 去你最喜欢的信息丰富的网站之一,如亚马逊,YouTube,CNN等,并加载它。
  2. 现在搜索一些东西,比如一个新产品。 主要内容将会改变,但大部分周围的信息,如页眉,页脚,导航菜单等都将保持不变。

这是一件非常好的事情,因为:

  • 页面更新速度更快,您不必等待页面刷新,这意味着该网站体验感觉更快,响应更快。
  • 每次更新都会下载更少的数据,这意味着更少的浪费带宽。在宽带连接的桌面上这可能不是一个大问题,但是在移动设备和发展中国家没有无处不在的快速互联网服务是一个大问题。

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

A basic web app data flow architecture

本文不会涉及这种存储技术。我们稍后会在模块中讨论它。

基本的Ajax请求

让我们看看使用XMLHttpRequest 和 Fetch如何处理这样的请求. 对于这些例子,我们将从几个不同的文本文件中请求数据,并使用它们来填充内容区域。

这一系列文件将作为我们的假数据库; 在真正的应用程序中,我们更可能使用服务器端语言(如PHP,Python或Node)从数据库请求我们的数据。 不过,我们要保持简单,并专注于客户端部分。

XMLHttpRequest

XMLHttpRequest (通常缩写为XHR)现在是一个相当古老的技术 - 它是在20世纪90年代后期由微软发明的,并且已经在相当长的时间内跨浏览器进行了标准化。

  1. 为例子做些准备, 将 ajax-start.html 和四个文本文件 — verse1.txt, verse2.txt, verse3.txt,  verse4.txt — 复制到你计算机上的一个新目录. 在这个例子中,我们将通过XHR在下拉菜单中选择一首诗(您可能会认识 — "如果谷歌翻译可以翻译的话")加载另一首诗。

  2. <script> 的内部, 添加下面的代码. 将 <select><pre> 元素的引用存储到变量中, 并定义一个 onchange 事件处理函数,可以在select的值改变时, 将其值传递给 updateDisplay() 函数作为参数。 

    var verseChoose = document.querySelector('select');
    var poemDisplay = document.querySelector('pre');
    
    verseChoose.onchange = function() {
      var verse = verseChoose.value;
      updateDisplay(verse);
    };
  3. 定义 updateDisplay() 函数. 首先,将下面的代码块放在之前代码块的下面 - 这是函数的空壳:

    function updateDisplay(verse) {
    
    };
  4. 我们将通过构造一个  指向我们要加载的文本文件的相对URL  来启动我们的函数, 因为我们稍后需要它. 任何时候 <select> 元素的值都与所选的 <option> 内的文本相同 (除非在值属性中指定了不同的值) — 例如 "Verse 1". 相应的诗歌文本文件是 "verse1.txt", 并与HTML文件位于同一目录中, 因此只需要文件名即可。

    但是,Web服务器往往是区分大小写的,文件名没有空格。 要将“Verse 1”转换为“verse1.txt”,我们需要将V转换为小写,删除空格,并在末尾添加.txt。  这可以通过 replace(), toLowerCase(), 和 简单的 string concatenation 来完成. 在 updateDisplay() 函数中添加以下代码:

    verse = verse.replace(" ", "");
    verse = verse.toLowerCase();
    var url = verse + '.txt';
  5. 要开始创建XHR请求,您需要使用 XMLHttpRequest() 的构造函数创建一个新的请求对象。 你可以把这个对象叫做你喜欢的任何东西, 但是我们会把它叫做 request 来保持简单. 在之前的代码中添加以下内容:

    var request = new XMLHttpRequest();
  6. 接下来,您需要使用 open() 方法来指定用于从网络请求资源的 HTTP request method , 以及它的URL是什么。我们将在这里使用 GET 方法, 并将URL设置为我们的 url 变量. 在你上面的代码中添加以下代码:

    request.open('GET', url);
  7. 接下来,我们将设置我们期待的响应类型 —  这是由请求的 responseType 属性定义的 — 作为 text.  这并不是绝对必要的 — XHR默认返回文本 —但如果你想在以后获取其他类型的数据,养成这样的习惯是一个好习惯. 接下来添加:

    request.responseType = 'text';
  8. 从网络获取资源是一个 asynchronous "异步" 操作, 这意味着您必须等待该操作完成(例如,资源从网络返回),然后才能对该响应执行任何操作,否则会出错,将被抛出错误。 XHR允许你使用它的 onload 事件处理器来处理这个事件 — 当onload 事件触发时(当响应已经返回时)这个事件会被运行。 发生这种情况时, response 数据将在XHR请求对象的响应属性中可用。

    在后面添加以下内容.  你会看到,在 onload 事件处理程序中,我们将 poemDisplay  ( <pre> 元素 ) 的  textContent 设置为 request.response 属性的值。

    request.onload = function() {
      poemDisplay.textContent = request.response;
    };
  9. 以上都是XHR请求的设置 — 在我们告诉它之前,它不会真正运行,这是通过 send() 完成的. 在你之前的代码下添加以下内容完成该函数:

    request.send();
  10. 这个例子中的一个问题就是它首次加载时不会显示任何诗。 为了解决这个问题,在代码的底部添加以下两行 (正好在关闭的 </script> 标签之上) 默认加载第1节,并确保 <select> 元素始终显示正确的值:

    updateDisplay('Verse 1');
    verseChoose.value = 'Verse 1';

Serving your example from a server

如果只是从本地文件运行示例,一些浏览器(包括Chrome)将不会运行XHR请求。这是因为安全限制(更多关于web安全性的限制,请参阅Website security)。

为了解决这个问题,我们需要通过在本地web服务器上运行它来测试这个示例。要了解如何实现这一点,请阅读How do you set up a local testing server?

Fetch

Fetch API基本上是XHR的一个现代替代品——它是最近在浏览器中引入的,它使异步HTTP请求在JavaScript中更容易实现,对于开发人员和在Fetch之上构建的其他API来说都是如此。

让我们将最后一个示例转换为使用Fetch !

  1. 复制您之前完成的示例目录. (如果您没有通过以前的练习,创建一个新的目录。, 然后复制 xhr-basic.html和这四个文件 — verse1.txt, verse2.txt, verse3.txt, and verse4.txt.)

  2. 在 updateDisplay() 里找到 XHR 那段代码:

    var request = new XMLHttpRequest();
    request.open('GET', url);
    request.responseType = 'text';
    
    request.onload = function() {
      poemDisplay.textContent = request.response;
    };
    
    request.send();
  3. 像这样替换掉所有关于XHR的代码:

    fetch(url).then(function(response) {
      response.text().then(function(text) {
        poemDisplay.textContent = text;
      });
    });
  4. 在浏览器中加载示例(通过web服务器运行),它应该与XHR版本相同,前提是您运行的是一个现代浏览器。

那么Fetch代码中发生了什么呢?

首先,我们调用了fetch()方法,将我们要获取的资源的URL传递给它。这相当于现代版的XHR中的request.open(),另外,您不需要任何等效的send()方法。

然后,你可以看到.then()方法连接到了fetch()末尾-这个方法是Promises的一部分,是一个用于执行异步操作的现代JavaScript特性。fetch()返回一个promise,它将解析从服务器发回的响应。我们使用then()来运行一些后续代码,这是我们在其内部定义的函数。这相当于XHR版本中的onload事件处理程序。

fetch() promise 解析时,这个函数会自动将响应从服务器传递给参数。在函数内部,我们获取响应并运行其text()方法。这基本上将响应作为原始文本返回,这相当于在XHR版本中的responseType = 'text'

你会看到 text() 也返回了一个 promise, 所以我们链接另外一个 .then() 到它上面, 在其中我们定义了一个函数来接收 text() 承诺解析的生文本。

Aside on promises

当你第一次见到它们的时候,promises会让你有点困惑,但现在不要太担心这个。在一段时间之后,您将会习惯它们,特别是当您了解更多关于现代JavaScript api的时候——大多数现代的JavaScript api都是基于promises的。

让我们再看看上面的promises结构,看看我们是否能更清楚地理解它:

fetch(url).then(function(response) {
  response.text().then(function(text) {
    poemDisplay.textContent = text;
  });
});

第一行是说‘’获取位于url里的资源(fetch(url))‘’和“然后当promise resolves后运行指定的函数(.then(function() { ... }))”。"Resolve"的意思是"在将来某一时刻完成指定的操作"。在本例中,指定的操作是从指定的URL(使用HTTP请求)获取资源,并返回对我们执行某些操作的响应。

实际上,传递给 then() 是一段不会立即执行的代码 — 相反,当返回响应时代码会被运行。注意,你还可以选择把保存你的 promise 到一个变量里, 链 .then() 在相同的位置。下面的代码会做相同的事情。

var myFetch = fetch(url);

myFetch.then(function(response) {
  response.text().then(function(text) {
    poemDisplay.textContent = text;
  });
});

因为方法 fetch() 返回一个 promise ‘resolves’ 了 HTTP响应, 那么你在 .then() 中定义的任何函数会被自动给与一个响应作为一个参数。你可以给这个参数取任何名字,以下的例子依然可以实现:(例子里把response参数叫做狗饼干---'dogBiscuits'=狗饼干)

fetch(url).then(function(dogBiscuits) {
  dogBiscuits.text().then(function(text) {
    poemDisplay.textContent = text;
  });
});

但是把参数叫做描述其内容的名字更有意义。

现在让我们来单独看一下函数:

function(response) {
  response.text().then(function(text) {
    poemDisplay.textContent = text;
  });
}

response 对象有个 text()方法, which takes the raw data contained in the response body and turns it into plain text, which is the format we want it in. It also returns a promise (which resolves to the resulting text string), so here we use another .then(), inside of which we define another function that dictates what we want to do with that text string. We are just setting the textContent property of our poem's <pre> element to equal the text string, so this works out pretty simple.

It is also worth noting that you can directly chain multiple promise blocks (.then() blocks, but there are other types too) onto the end of one another, passing the result of each block to the next block as you travel down the chain. This makes promises very powerful.

The following block does the same thing as our original example, but is written in a different style:

fetch(url).then(function(response) {
  return response.text()
}).then(function(text) {
  poemDisplay.textContent = text;
});

Many developers like this style better, as it is flatter and arguably easier to read for longer promise chains — each subsequent promise comes after the previous one, rather than being inside the previous one (which can get unwieldy). The only other difference is that we've had to include a return statement in front of response.text(), to get it to pass its result on to the next link in the chain.

你应该用哪种方法呢?

This really depends on what project you are working on. XHR has been around for a long time now and has very good cross-browser support. Fetch and Promises, on the other hand, are a more recent addition to the web platform, although they're supported well across the browser landscape, with the exception of Internet Explorer and Safari (which at the time of writing has Fetch available in its latest technology preview).

If you need to support older browsers, then an XHR solution might be preferable. If however you are working on a more progressive project and aren't as worried about older browsers, then Fetch could be a good choice.

You should really learn both — Fetch will become more popular as Internet Explorer declines in usage (IE is no longer being developed, in favor of Microsoft's new Edge browser), but you might need XHR for a while yet.

一个更复杂的示例

To round off the article, we'll look at a slightly more complex example that shows some more interesting uses of Fetch. We have created a sample site called The Can Store — it's a fictional supermarket that only sells canned goods. You can find this example live on GitHub, and see the source code.

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

By default, the site displays all the products, but you can use the form controls in the left hand column to filter them by category, or search term, or both.

There is quite a lot of complex code that deals with filtering the products by category and search terms, manipulating strings so the data displays correctly in the UI, etc. We won't discuss all of it in the article, but you can find extensive comments in the code (see can-script.js).

We will however explain the Fetch code.

The first block that uses Fetch can be found at the start of the JavaScript:

fetch('products.json').then(function(response) {
  if(response.ok) {
    response.json().then(function(json) {
      products = json;
      initialize();
    });
  } else {
    console.log('Network request for products.json failed with response ' + response.status + ': ' + response.statusText);
  }
});

This looks similar to what we saw before, except that the second promise is inside a conditional statement. In the condition we check to see if the response returned has been successful — the response.ok property contains a Boolean that is true if the response was OK (e.g. 200 meaning "OK"), or false if it was unsuccessful.

If the response was successful, we run the second promise — this time however we use json(), not text(), as we want to return our response as structured JSON data, not plain text.

If the response was not successful, we print out an error to the console stating that the network request failed, which reports the network status and descriptive message of the response (contained in the response.status and response.statusText properties, respectively). Of course a complete web site would handle this error more gracefully, by displaying a message on the user's screen and perhaps offering options to remedy the situation.

You can test the fail case yourself:

  1. Make a local copy of the example files (download and unpack the can-store ZIP file)
  2. Run the code through a web server (as described above, in Serving your example from a server)
  3. Modify the path to the file being fetched, to something like 'produc.json' (i.e. make sure it is mispelled)
  4. Now load the index file in your browser (e.g. via localhost:8000) and look in your browser developer console. You'll see a message along the lines of "Network request for products.json failed with response 404: File not found"

The second Fetch block can be found inside the fetchBlob() function:

fetch(url).then(function(response) {
  if(response.ok) {
    response.blob().then(function(blob) {
      objectURL = URL.createObjectURL(blob);
      showProduct(objectURL, product);
    });
  } else {
    console.log('Network request for "' + product.name + '" image failed with response ' + response.status + ': ' + response.statusText);
  }
});

This works in much the same way as the previous one, except that instead of using json(), we use blob() — in this case we want to return our response as an image file, and the data format we use for that is Blob — the term is an abbreviation of "Binary Large Object", and can basically be used to represent large file-like objects — such as images or video files.

Once we've successfully received our blob, we create an object URL out of it, using createObjectURL(). This returns a temporary internal URL that points to an object referenced inside the browser. These are not very readable, but you can see what one looks like by opening up the Can Store app, Ctrl-/Right-clicking on an image, and selecting the "View image" option (which might vary slightly depending on what browser you are using). The object URL will be visible inside the address bar, and should be something like this:

blob:http://localhost:7800/9b75250e-5279-e249-884f-d03eb1fd84f4

Challenge: An XHR version of the Can Store

We'd like you to have a go at converting the Fetch version of the app to use XHR, as a useful bit of practice. Take a copy of the ZIP file, and try modifying the JavaScript as appropriate.

Some helpful hints:

  • You might find the XMLHttpRequest reference material useful.
  • You will basically need to use the same pattern as you saw earlier in the XHR-basic.html example.
  • You will, however, need to add the error handling we showed you in the Fetch version of the Can Store:
    • The response is found in request.response after the load event has fired, not in a promise then().
    • About the best equivalent to Fetch's response.ok in XHR is to check whether request.status is equal to 200, or if request.readyState is equal to 4.
    • The properties for getting the status and status message are the same, but they are found on the request (XHR) object, not the response object.

Note: If you have trouble with this, feel free to check your code against the finished version on GitHub (see the source here, and also see it running live).

概述

That rounds off our article on fetching data from the server. By this point you should have an idea of how to start working with both XHR and Fetch.

请参阅

There are however a lot of different subjects discussed in this article, which has only really scratched the surface. For a lot more detail on these subjects, try the following articles:

文档标签和贡献者

 最后编辑者: Mr.ma,