在现代网站和应用中另一个常见的任务是从服务端获取个别数据来更新部分网页而不用加载整个页面。 这看起来是小细节却对网站性能和行为产生巨大的影响。所以我们将在这篇文章介绍概念和技术使它成为可能,例如: XMLHttpRequest 和 Fetch API.
先决条件: | JavaScript 基础(查看 第一步, 基础要件, JavaScript对象), 和客户端API基础 |
---|---|
目标: | 学会如何从服务器获取数据并使用它更新网页内容. |
这里有什么问题?
最初加载页面很简单 — 你为网站发送一个请求到服务器, 只要没有出错你将会获取资源并显示网页到你的电脑上。
这个模型的问题是当你想更新网页的任何部分,例如显示一套新的产品或者加载一个新的页面,你需要再一次加载整个页面。这是非常浪费的并且导致了差的用户体验尤其是现在的页面越来越大且越来越复杂。
Ajax开始
这导致了创建允许网页请求小块数据(例如 HTML, XML, JSON, 或纯文本) 和 仅在需要时显示它们的技术,从而帮助解决上述问题。
这是通过使用诸如 XMLHttpRequest
之类的API或者 — 最近以来的 Fetch API 来实现. 这些技术允许网页直接处理对服务器上可用的特定资源的 HTTP 请求,并在显示之前根据需要对结果数据进行格式化。
注意:在早期,这种通用技术被称为Asynchronous JavaScript and XML(Ajax), 因为它倾向于使用XMLHttpRequest
来请求XML数据。 但通常不是这种情况 (你更有可能使用 XMLHttpRequest
或 Fetch 来请求JSON), 但结果仍然是一样的,术语“Ajax”仍然常用于描述这种技术。
Ajax模型包括使用Web API作为代理来更智能地请求数据,而不仅仅是让浏览器重新加载整个页面。让我们来思考这个意义:
- 去你最喜欢的信息丰富的网站之一,如亚马逊,油管,CNN等,并加载它。
- 现在搜索一些东西,比如一个新产品。 主要内容将会改变,但大部分周围的信息,如页眉,页脚,导航菜单等都将保持不变。
这是一件非常好的事情,因为:
- 页面更新速度更快,您不必等待页面刷新,这意味着该网站体验感觉更快,响应更快。
- 每次更新都会下载更少的数据,这意味着更少地浪费带宽。在宽带连接的桌面上这可能不是一个大问题,但是在移动设备和发展中国家没有无处不在的快速互联网服务是一个大问题。
为了进一步提高速度,有些网站还会在首次请求时将资产和数据存储在用户的计算机上,这意味着在后续访问中,他们将使用本地版本,而不是在首次加载页面时下载新副本。 内容仅在更新后从服务器重新加载。
本文不会涉及这种存储技术。我们稍后会在模块中讨论它。
基本的Ajax请求
让我们看看使用XMLHttpRequest
和 Fetch如何处理这样的请求. 对于这些例子,我们将从几个不同的文本文件中请求数据,并使用它们来填充内容区域。
这一系列文件将作为我们的假数据库; 在真正的应用程序中,我们更可能使用服务器端语言(如PHP,Python或Node)从数据库请求我们的数据。 不过,我们要保持简单,并专注于客户端部分。
XMLHttpRequest
XMLHttpRequest
(通常缩写为XHR)现在是一个相当古老的技术 – 它是在20世纪90年代后期由微软发明的,并且已经在相当长的时间内跨浏览器进行了标准化。
- 为例子做些准备, 将 ajax-start.html 和四个文本文件 — verse1.txt, verse2.txt, verse3.txt, verse4.txt — 复制到你计算机上的一个新目录. 在这个例子中,我们将通过XHR在下拉菜单中选择一首诗(您可能会认识 — “如果谷歌翻译可以翻译的话”)加载另一首诗。
- 在
<script>
的内部, 添加下面的代码. 将<select>
和<pre>
元素的引用存储到变量中, 并定义一个onchange
事件处理函数,可以在select的值改变时, 将其值传递给updateDisplay()
函数作为参数。const verseChoose = document.querySelector('select'); const poemDisplay = document.querySelector('pre'); verseChoose.onchange = function() { const verse = verseChoose.value; updateDisplay(verse); };
Copy to Clipboard - 定义
updateDisplay()
函数. 首先,将下面的代码块放在之前代码块的下面 – 这是函数的空壳:function updateDisplay(verse) { }
Copy to Clipboard - 我们将通过构造一个 指向我们要加载的文本文件的相对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(); let url = verse + '.txt';
Copy to Clipboard - 要开始创建XHR请求,您需要使用
XMLHttpRequest()
的构造函数创建一个新的请求对象。 你可以把这个对象叫做你喜欢的任何东西, 但是我们会把它叫做request
来保持简单. 在之前的代码中添加以下内容:let request = new XMLHttpRequest();
Copy to Clipboard - 接下来,您需要使用
open()
方法来指定用于从网络请求资源的 HTTP request method , 以及它的URL是什么。我们将在这里使用GET
方法, 并将URL设置为我们的url
变量. 在你上面的代码中添加以下代码:request.open('GET', url);
Copy to Clipboard - 接下来,我们将设置我们期待的响应类型 — 这是由请求的
responseType
属性定义的 — 作为text
. 这并不是绝对必要的 — XHR默认返回文本 —但如果你想在以后获取其他类型的数据,养成这样的习惯是一个好习惯. 接下来添加:request.responseType = 'text';
Copy to Clipboard - 从网络获取资源是一个 asynchronous “异步” 操作, 这意味着您必须等待该操作完成(例如,资源从网络返回),然后才能对该响应执行任何操作,否则会出错,将被抛出错误。 XHR允许你使用它的
onload
事件处理器来处理这个事件 — 当onload
事件触发时(当响应已经返回时)这个事件会被运行。 发生这种情况时,response
数据将在XHR请求对象的响应属性中可用。在后面添加以下内容. 你会看到,在onload
事件处理程序中,我们将poemDisplay
(<pre>
元素 ) 的textContent
设置为request.response
属性的值。request.onload = function() { poemDisplay.textContent = request.response; };
Copy to Clipboard - 以上都是XHR请求的设置 — 在我们告诉它之前,它不会真正运行,这是通过
send()
完成的. 在你之前的代码下添加以下内容完成该函数:request.send();
Copy to Clipboard - 这个例子中的一个问题就是它首次加载时不会显示任何诗。 为了解决这个问题,在代码的底部添加以下两行 (正好在关闭的
</script>
标签之上) 默认加载第1节,并确保<select>
元素始终显示正确的值:updateDisplay('Verse 1'); verseChoose.value = 'Verse 1';
Copy to Clipboard
在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 !
- 复制您之前完成的示例目录. (如果您没有通过以前的练习,创建一个新的目录。, 然后复制 xhr-basic.html和这四个文件 — verse1.txt, verse2.txt, verse3.txt, and verse4.txt.)
- 在
updateDisplay()
里找到 XHR 那段代码:let request = new XMLHttpRequest(); request.open('GET', url); request.responseType = 'text'; request.onload = function() { poemDisplay.textContent = request.response; }; request.send();
Copy to Clipboard - 像这样替换掉所有关于XHR的代码:
fetch(url).then(function(response) { response.text().then(function(text) { poemDisplay.textContent = text; }); });
Copy to Clipboard - 在浏览器中加载示例(通过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()
promise解析的生文本。
在promise的函数内部,我们做的和在XHR版本中差不多— 设置 <pre>
元素的文本内容为text的值。
关于promises
当你第一次见到它们的时候,promises会让你有点困惑,但现在不要太担心这个。在一段时间之后,您将会习惯它们,特别是当您了解更多关于现代JavaScript api的时候——大多数现代的JavaScript api都是基于promises的。
让我们再看看上面的promises结构,看看我们是否能更清楚地理解它:
fetch(url).then(function(response) {
response.text().then(function(text) {
poemDisplay.textContent = text;
});
});
Copy to Clipboard
第一行是说‘’获取位于url里的资源(fetch(url)
)‘’和“然后当promise解析后运行指定的函数(.then(function() { ... })
)”。”解析”的意思是”在将来某一时刻完成指定的操作”。在本例中,指定的操作是从指定的URL(使用HTTP请求)获取资源,并返回对我们执行某些操作的响应。
实际上,传递给 then()
是一段不会立即执行的代码 — 而是当返回响应时代码会被运行。注意,你还可以选择把你的 promise 保存到一个变量里, 链接 .then()
在相同的位置。下面的代码会做相同的事情。
let myFetch = fetch(url);
myFetch.then(function(response) {
response.text().then(function(text) {
poemDisplay.textContent = text;
});
});
Copy to Clipboard
因为方法 fetch() 返回一个解析HTTP响应的promise, 你在 .then() 中定义的任何函数会被自动给与一个响应作为一个参数。你可以给这个参数取任何名字,以下的例子依然可以实现:(例子里把response参数叫做狗饼干—‘dogBiscuits’=狗饼干)
fetch(url).then(function(dogBiscuits) {
dogBiscuits.text().then(function(text) {
poemDisplay.textContent = text;
});
});
Copy to Clipboard
但是把参数叫做描述其内容的名字更有意义。
现在让我们来单独看一下函数:
function(response) {
response.text().then(function(text) {
poemDisplay.textContent = text;
});
}
Copy to Clipboard
response 对象有个 text()
方法, 获取响应主体中的原始数据a并把它转换成纯文本, 那是我们想要的格式。它也返回一个promise (解析结果文本字符串), 所以这里我们再使用 .then()
, 在里面我们再定义一个操作文本字符串的函数。我们设置诗歌的 <pre>
元素的 textContent
属性和这个文本字符串相同, 这样就非常简单地解决了。
值得注意的是你可以直接将promise块 (.then()
块, 但也有其他类型) 链接到另一个的尾部, 顺着链条将每个块的结果传到下一个块。 这使得promises非常强大。
下面的代码块和我们原始的例子做的是相同的事, 但它是不同的写法:
fetch(url).then(function(response) {
return response.text()
}).then(function(text) {
poemDisplay.textContent = text;
});
Copy to Clipboard
很多开发者更喜欢这种样式, 因为它更扁平并且按理说对于更长的promise链它更容易读 — 每一个promise(承诺)接续上一个promise,而不是在上一个promise的里面(会使得整个代码笨重起来,难以理解)。以上两种写法还有一个不同的地方是我们在response.text()
语句之前得包含一个 return
语句, 用来把这一部分的结果传向promise链的下一段。
你应该用哪种方法呢?
这完全取决于你正在干的项目是啥样。XHR已经面世非常之久,现在已经有了相当棒的跨浏览器支持。然而对于网页平台来说,Fetch和Promise是新近的产物,除了IE和Safari浏览器不支持,别的浏览器大多提供了支持。(现在Safari也即将为fetch和promise提供支持)。
如果你的项目需要支持年代久远的浏览器,那么使用XHR可能会更爽一些。如果你的项目比较激进而且你根本不管老版的浏览器吃不吃这套,那就选择Fetch吧老铁。
话说回来,咱倒真应该两者都学学——因为使用IE浏览器的人们在变少,Fetch会变得越来越流行(事实上IE已经没人管了,因为微软Edge浏览器的受宠),但在所有浏览器彻底支持Fetch之前,你可能还得和XHR纠缠一阵子。
一个更复杂的示例
为了使本文更详尽,我们将看一个稍微复杂一点的示例,它展示了Fetch的一些更有趣的用法。我们创建了一个名为Can Store的示例站点——它是一个虚构的超市,只出售罐头食品。你可以找到 example live on GitHub,并且 see the source code 。
默认情况下,站点会显示所有产品,但您可以使用左手列中的表单控件按类别或搜索词或两者进行筛选。
有很多复杂的代码处理按类别和搜索词过滤产品、操作字符串以便数据在UI中正确显示等等。我们不会在本文中讨论所有这些,但是您可以在代码中找到大量的注释 (see can-script.js)。
但是,我们会解释Fetch代码的含义。
第一个使用Fetch的块可以在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);
}
});
Copy to Clipboard
这看起来和我们之前看到的相似,只是第二个promise是在一个条件语句中。在条件下,我们检查返回的response是否成功 — response.ok
属性包含一个布尔变量,如果response是成功的 (e.g. 200 meaning “OK”),则是true
;如果失败了,则是false
。
如果response成功,我们运行第二个promise — 然而,这次我们使用 json()
(en-US),而不是text()
(en-US), 因为我们想要response返回的是一个结构化的JSON数据,而不是纯文本。
如果response失败,我们将输出一个错误到控制台,指出网络请求失败,该控制台将报告响应的网络状态和描述性消息(分别包含在response.status
和response.statusText
属性中)。 当然,一个完整的web站点可以通过在用户的屏幕上显示一条消息来更优雅地处理这个错误,也许还可以提供一些选项来补救这种情况。
您可以自己测试失败案例:
- 制作示例文件的本地副本(下载并解压the can-store ZIP file)
- 通过web服务器运行代码(如上所述,在 Serving your example from a server)
- 修改要获取的文件的路径,比如“produc.json'(确保你拼写的是错误的)
- 现在在你的浏览器上加载索引文件 (通过
localhost:8000
) 然后查看你的开发者控制台。 你将看到一条类似于“网络请求products.json失败,404:找不到文件”的消息
第二个Fetch块可以在fetchBlob()
找到:
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);
}
});
Copy to Clipboard
它的工作原理和前一个差不多, 除了我们放弃json()
(en-US)而使用blob()
(en-US) — 在本例中,我们希望以图像文件的形式返回响应,为此使用的数据格式是Blob — 这个词是“二进制大对象”的缩写,基本上可以用来表示大型文件类对象——比如图像或视频文件。
一旦我们成功地接收到我们的blob,我们就会使用它创建一个对象URL createObjectURL()
. 它返回一个指向浏览器中引用的对象的临时内部URL。这些不是很容易读懂,但是你可以通过打开Can Store看到,按Ctrl-/右键单击一个图像,然后选择“View Image(查看图像)”选项(根据您使用的浏览器可能略有不同)。 对象URL将在地址栏中可见,应该是这样的:
blob:http://localhost:7800/9b75250e-5279-e249-884f-d03eb1fd84f4
挑战:一个XHR版本的Can Store
我们希望您能尝试将Fetch版本转换为使用XHR,作为一个有用的实践。下载一份 copy of the ZIP file, 并适当修改下JavaScript。
一些有用的提示:
- 你可能会发现
XMLHttpRequest
作参考材料非常有用。 - 你基本上需要使用和你在前面的文章中看到的XHR-basic.html例子相同的模式。
- 但是,您将需要添加我们在Can Store的Fetch版本中显示的错误处理:
- 在
load
事件完毕后,response被用于request.response
而不是promise的then()
。 - Fetch的
response.ok
在XHR中的最佳等效为检查request.status
是否等于200或者request.readyState
是否等于4。 - 获取状态和状态信息的内容是一样的, 但是它们在
request
(XHR) 对象,而不是response
对象。
- 在
注意: 如果您在这方面有问题,请到Github将您的代码和完成的版本进行对比 ( see the source here, see also see it running live )。
概述
我们这篇关于从服务器那儿抓取数据的文章就到此为止了。现在你应该对如何使用XHR和Fetch有一些了解了吧?
请参阅
虽然本文中讨论了许多不同的主题,但是这些主题仅仅只是触及了表面。有关这些主题的更多详细信息,请尝试以下文章: