1. 首页

【JS 小书】第 10 章:使用异步 JavaScript

REST API 和 XMLHttpRequest

如果你问我,我会说 JS 挺强大的。作为一种在浏览器中运行的脚本语言,它可以做所有类似的事情:

  • 动态创建元素
  • 添加互动性

等等。在第 8 章中,咱们从数组开始构建了一个 HTML 表格。 硬编码数组是一个同步数据源,也就是说,可以直接在咱们的代码中使用它,无需等待。 但是大多数时候,数据都是从后台请求过来的。网络请求始终是异步操作,而不是同步数据源:请求数据,服务器会有一定的延迟后才响应。

JS 本身没有内置的异步性:它是“宿主”环境(浏览器或 Node.j),为处理耗时的操作提供了外部帮助。在第3章中,咱们看到了setTimeoutsetInterval,这两个属于 Web API 的。浏览器提供了很多 API,其中还有一个叫XMLHttpRequest,专门用于网络请求。

事实上,它来自于以 XML 数据格式的时代。现在 JSON 是最流行的用于在 Web 服务之间移动数据的通信“协议”,但 XMLHttpRequest 这个名称最终被保留了下来。

XMLHttpRequest 也是 AJAX 技术的一部分,它是 “异步JavaScript和XML” 的缩写。AJAX 就是为了在浏览器中尽可能灵活地处理网络请求而诞生的。它的作用是能够从远程数据源获取数据,而不会导致页面刷新。当时这个想法几乎是革命性的。随着 XMLHttpRequest (大约13年前)的引入,咱们可以使用它来进行异步请求。


var request = new XMLHttpRequest(); request.open('GET', "https://academy.valentinog.com/api/link/"); request.addEventListener('load', function() { console.log(this.response); }) request.send();

在上述的示例中:

  • 创建一个新的 XMLHttpRequest 对象
  • 通过提供方法和网址来打开请求
  • 注册事件监听器
  • 发送请求

XMLHttpRequest 是基于DOM事件的,咱们可以使用 addEventListeneronload 来监听“load”事件,该事件在请求成功时触发。对于失败的请求(网络错误),咱们可以在“error”事件上注册一个侦听器:


var request = new XMLHttpRequest(); request.open("GET", "https://academy.valentinog.com/api/link/") request.onload = function() { console.log(this.response) } request.onerror = function() { // 处理错误 } request.send();

有了这些知识,咱们就更好地使用 XMLHttpRequest

通过 XMLHttpRequest 请求数据,构建 HTML 列表

REST API 提取数据后,咱们将构建一个简单的 HTML 列表。 新建一个名为 build-list.html 的文件:


<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>XMLHttpRequest</title> </head> <body> </body> <script src="xhr.js"></script> </html>

接下来,在同一个文件夹中创建一个名为 xhr.js 的文件。在这个文件中,创建一个新的 XHR 请求:


"use strict"; const request = new XMLHttpRequest();

上面的调用(构造函数方式)创建了一个 XMLHttpRequest 类型的新对象。与 setTimeout 等异步函数相反,我们把回调作为参数:


setTimeout(callback, 10000); function callback() { console.log("hello timer!"); }

XMLHttpRequest 基于 DOM 事件,处理程序回调注册在 onload 对象上。当请求成功时,load 事件触发。


"use strict"; const request = new XMLHttpRequest(); request.onload = callback; function callback() { console.log("Got the response!"); }

注册回调之后,我们可以使用 open() 打开请求。它接受一个 HTTP 方法


"use strict"; const request = new XMLHttpRequest(); request.onload = callback; function callback() { console.log("Got the response!"); } request.open("GET", "https://academy.valentinog.com/api/link/");

最后,我们可以使用 send() 发送实际的请求


"use strict"; const request = new XMLHttpRequest(); request.onload = callback; function callback() { console.log("Got the response!"); } request.open("GET", "https://academy.valentinog.com/api/link/"); request.send();

在浏览器中打开 build-list.html,在控制台中会看到“Got the response!”,说明请求成功。如果你还记得第6章,每个常规 JS 函数都有一个对其宿主对象的引用。因为回调在 XMLHttpRequest 对象中运行,所以可以通过 this.response 获取服务器返回的数据。


"use strict"; const request = new XMLHttpRequest(); request.onload = callback; function callback() { // this refers to the new XMLHttpRequest // response is the server's response console.log(this.response); } request.open("GET", "https://academy.valentinog.com/api/link/"); request.send();

保存文件并刷新 build-list.html。在控制台可以看到返回的数据,数据格式是字符串,有两种方法可以把它变成 JSON 格式:

  • 方法一:在 XMLHttpRequest 对象上配置响应类型
  • 方法二:使用 JSON.parse()

方法一


"use strict"; const request = new XMLHttpRequest(); request.onload = callback; function callback() { // this refers to the new XMLHttpRequest // response is the server's response console.log(this.response); } // configure the response type request.responseType = "json"; // request.open("GET", "https://academy.valentinog.com/api/link/"); request.send();

方法二 比较推荐,也符合咱们现在的编程习惯:


"use strict"; const request = new XMLHttpRequest(); request.onload = callback; function callback() { const response = JSON.parse(this.response); console.log(response); } request.open("GET", "https://academy.valentinog.com/api/link/"); request.send();

再次刷新build-list.html,会看到一个 JS 对象数组,每个对象都具有相同的结构:


[ // { title: "JavaScript Engines: From Call Stack to Promise, (almost) Everything You Need to Know", url: "https://www.valentinog.com/blog/engines/", tags: ["javascript", "v8"], id: 3 } // ]

这次,咱们没有像第8章那样手工创建数组,而是通过 REST API 接口请求数据。

使用 JS 构建 HTML 列表(和调试类)

这里咱们使用 ES6 类的方法来构建,还会使用私有类字段(在撰写本文时,Firefox不支持该字段)。在编写任何代码之前,都要思考一下,别人会“如何使用我的类”? 例如,另一个开发人员可以使用咱们的代码并通过传入来调用该类:

  • 用于获取数据的 URL
  • 要将列表附加到的 HTML 元素

const url = "https://academy.valentinog.com/api/link/"; const target = document.body; const list = new List(url, target);

有了这些要求,咱们就可以开始编写类代码了。目前,它应该接受构造函数中的两个参数,并拥有一个获取数据方法


class List { constructor(url, target) { this.url = url; this.target = target; } getData() { return "stuff"; } }

软件开发中的普遍观点是,除非有充分的理由要做相反的事情,否则不能从外部访问类成员和方法。 在 JS 中,除非使用模块,否则没有隐藏方法和变量的原生方法(第2章)。 即使是 class 也不能幸免于信息泄漏,但是有了私有字段,就能大概率避免这类问题。 JS 私有类字段的目前还没有成标准,但大部分浏览器已经支持了,它用 # 来表示,重写上面的类:


class List { #url; #target; constructor(url, target) { this.#url = url; this.#target = target; } getData() { return "stuff"; } }

你可能不喜欢语法,但是私有类字段可以完成其工作。 这种方式,我们就不能从外部访问 urltarget


class List { #url; #target; constructor(url, target) { this.#url = url; this.#target = target; } getData() { return "stuff"; } } const url = "https://academy.valentinog.com/api/link/"; const target = document.body; const list = new List(url, target); console.log(list.url); // undefined console.log(list.target); // undefined

有了这个结构,咱们就可以将数据获取逻辑移到 getData 中。


"use strict"; class List { #url; #target; constructor(url, target) { this.#url = url; this.#target = target; } getData() { const request = new XMLHttpRequest(); request.onload = function() { const response = JSON.parse(this.response); console.log(response); }; request.open("GET", this.#url); request.send(); } } const url = "https://academy.valentinog.com/api/link/"; const target = document.body; const list = new List(url, target);

现在,为了显示数据,咱们在 getData 之后添加一个名为 render 的方法。render 将为我们创建一个 HTML 列表,从作为参数传递的数组开始:


"use strict"; class List { #url; #target; constructor(url, target) { this.#url = url; this.#target = target; } getData() { const request = new XMLHttpRequest(); request.onload = function() { const response = JSON.parse(this.response); console.log(response); }; request.open("GET", this.#url); request.send(); } // The new method render(data) { const ul = document.createElement("ul"); for (const element of data) { const li = document.createElement("li"); const title = document.createTextNode(element.title); li.appendChild(title); ul.appendChild(li); } this.#target.appendChild(ul); } }

注意 document.createElement()document.createTextNode()appendChild()。咱们在第8章讲DOM 操作的时候见过。this.#target 私有字段将 HTML 列表附加到 DOM。现在,我想:

  • 获取 JSON 响应后调用 render
  • 当用户创建一个新的列表“实例”时立即调用 getData

为此,咱们在 request.onload 回调内部调用 render


getData() { const request = new XMLHttpRequest(); request.onload = function() { const response = JSON.parse(this.response); // Call render after getting the response this.render(response); }; request.open("GET", this.#url); request.send(); }

另一方面,getData 应该在构造函数中运行:


constructor(url, target) { this.#url = url; this.#target = target; // Call getData as soon as the class is used this.getData(); }

完整代码:


"use strict"; class List { #url; #target; constructor(url, target) { this.#url = url; this.#target = target; this.getData(); } getData() { const request = new XMLHttpRequest(); request.onload = function() { const response = JSON.parse(this.response); this.render(response); }; request.open("GET", this.#url); request.send(); } render(data) { const ul = document.createElement("ul"); for (const element of data) { const li = document.createElement("li"); const title = document.createTextNode(element.title); li.appendChild(title); ul.appendChild(li); } this.#target.appendChild(ul); } } const url = "https://academy.valentinog.com/api/link/"; const target = document.body; const list = new List(url, target);

尝试一下:在浏览器中刷新 build-list.html 并查看控制台


Uncaught TypeError: this.render is not a function

this.render 不是函数! 会是什么呢? 此时,你可能想要达到第6章或更高版本,可以调试代码。 在 getData 中的 this.render(response) 之后,添加 debugger 指令:


getData() { const request = new XMLHttpRequest(); request.onload = function() { const response = JSON.parse(this.response); debugger; this.render(response); }; request.open("GET", this.#url); request.send(); }

debugger 加了一个所谓的断点,执行将停止在那里。现在打开浏览器控制台并刷新build-list.html。下面是将在 Chrome 中看到的:

仔细查看“Scopes”选项卡。getData 中确实有一个 this,但它指向 XMLHttpRequest。 换句话说,我们试图在错误的对象上访问 this.render

为什么 this 不匹配? 这是因为传递给 request.onload 的回调在 XMLHttpRequest 类型的宿主对象中运行,调用 const request = request = new XMLHttpRequest() 的结果。解决方法,在前几章中已经提到过了,可以使用 箭头函数


getData() { const request = new XMLHttpRequest(); // The arrow function in action request.onload = () => { const response = JSON.parse(this.response); debugger; this.render(response); }; request.open("GET", this.#url); request.send(); }

刷新 build-list.html 并检查它


Uncaught SyntaxError: Unexpected token u in JSON at position 0

很好,前面的错误消失了,但是现在 JSON.parse 出现了一个问题。我们很容易想象它与 this 有关。将debugger 向上移动一行


getData() { const request = new XMLHttpRequest(); request.onload = () => { debugger; const response = JSON.parse(this.response); this.render(response); }; request.open("GET", this.#url); request.send(); }

刷新build-list.html并在浏览器控制台中再次查看 Scopesresponseundefined ,因为我们要访问的 thisList。这与箭头函数和类的行为一致(类默认为严格模式)。那么现在有什么解决办法吗?

第8章 DOM 和 events 中了解到,作为事件监听器传递的每个回调都可以访问 event 对象。在该 event 对象中还有一个名为 target 的属性,指向触发事件的对象。吃准可以通过 event.target.response 获取响应回来的数据。


getData() { const request = new XMLHttpRequest(); request.onload = event => { const response = JSON.parse(event.target.response); this.render(response); }; request.open("GET", this.#url); request.send(); }

完整代码:


"use strict"; class List { #url; #target; constructor(url, target) { this.#url = url; this.#target = target; this.getData(); } getData() { const request = new XMLHttpRequest(); request.onload = event => { const response = JSON.parse(event.target.response); this.render(response); }; request.open("GET", this.#url); request.send(); } render(data) { const ul = document.createElement("ul"); for (const element of data) { const li = document.createElement("li"); const title = document.createTextNode(element.title); li.appendChild(title); ul.appendChild(li); } this.#target.appendChild(ul); } } const url = "https://academy.valentinog.com/api/link/"; const target = document.body; const list = new List(url, target);

接着,继续探索 XMLHttpRequest 的发展:Fetch

异步演变:从 XMLHttpRequest 到 Fetch

Fetch API 是一种用于发出 AJAX 请求的原生浏览器方法,它常常被诸如 Axios 之类的库所忽视。Fetch 与ES6 和新的 Promise 对象一起诞生于 2015 年。

另一方面,AJAX 从 1999 年开始就有了一套在浏览器中获取数据的技术。现在我们认为 AJAX 和 Fetch 是理所当然的,但是很少有人知道 Fetch 只不过是 XMLHttpRequest 的 “美化版”。Fetch 比典型的 XMLHttpRequest 请求更简洁,更重要的是基于 Promise。这里有一个简单的事例:


fetch("https://academy.valentinog.com/api/link/").then(function(response) { console.log(response); });

如果在浏览器中运行它,控制台将打印一个响应对象。根据请求的内容类型,需要在返回数据时将其转换为JSON


fetch("https://academy.valentinog.com/api/link/").then(function(response) { return response.json(); });

与你可能认为的相反,仅仅调用并没有返回实际的数据。由于response.json()也返回一个 Promise ,因此需要进一步才能获得 JSON 有效负载:


fetch("https://academy.valentinog.com/api/link/") .then(function(response) { return response.json(); }) .then(function(json) { console.log(json); });

FetchXMLHttpRequest 更方便、更干净,但它有很多特性。例如,必须特别注意检查响应中的错误。在下一节中,咱们将了解关于它的更多信息,同时从头重新构建 Fetch

从头开始重新构建 Fetch API

为了更好的理解 Fetch 原理,咱们重写 fetch 方法。首先,创建一个名为fetch.html的新文件,内容如下:


<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Building Fetch from scratch</title> </head> <body> </body> <script src="fetch.js"></script> </html>

然后在相同的文件夹中创建另一个名为 fetch.js 的文件,内容如下:


"use strict"; window.fetch = null;

在第一行中,咱们确保处于严格模式,在第二行中,“取消”原始的Fetch API。现在咱们可以开始构建自己的 Fetch API 了。fetch 的工作方式非常简单。它接受一个 url 并针对它发出一个 GET 请求:


fetch("https://academy.valentinog.com/api/link/").then(function(response) { console.log(response); });

当带有 then 的函数说明该函数是“可链”的,这意味着它返回一个 Promise。因此,在 fetch.js 中,咱们创建一个名为 fetch 的函数,它接受一个 url 并返回一个新的 Promise。创建 Promise,可以调用Promise 构造函数,并传入一个回调函数来解析和拒绝:


function fetch(url) { return new Promise(function(resolve, reject) { // do stuff }); }

完善代码:


"use strict"; window.fetch = fetch; function fetch(url) { return new Promise(function(resolve, reject) { resolve("Fake response!"); }); } fetch("https://academy.valentinog.com/api/link/").then(function(response) { console.log(response); });

在控制台中得到“Fake response!” 。当然,这仍然是一个无用的 fetch ,因为没有从 API 返回任何东西。让咱们在 XMLHttpRequest 的帮助下实现真正的行为。咱们已经知道了 XMLHttpRequest 创建请求方式。接着,将XMLHttpRequest 封装到咱们的 Promise


function fetch(url) { return new Promise(function(resolve, reject) { const request = new XMLHttpRequest(); request.open("GET", url); request.onload = function() { resolve(this.response); }; request.onerror = function() { reject("Network error!"); }; request.send(); }); }

被拒绝的 Promisecatch 处理:


fetch("https://acdemy.valentinog.com/api/link/") .then(function(response) { console.log(response); }) .catch(function(error) { console.log(error); });

现在,如果 url 是错误的,会打印具体的错误信息到控制台。如果 url 正确,则打印请求到数据:

Js中文网 一个帮助开发者成长的社区,你想要的,在这里都能找到

上述实现方式还不够完善。首先,咱们需要实现一个返回 JSON 的函数。实际的 Fetch API 生成一个响应,可以稍后将其转换为 JSON、blob 或 文本,如下所示(对于本练习的范围,我们只实现 JSON 函数)


fetch("https://academy.valentinog.com/api/link/") .then(function(response) { return response.json(); }) .then(function(json) { console.log(json); })

实现该功能应该很容易。 似乎 “response” 可能是一个单独带有 json() 函数的实体。 JS 原型系统非常适合构建代码(请参阅第5章)。 咱们创建一个名为 Response 的构造函数和一个绑定到其原型的方法(在fetch.js中):


function Response(response) { this.response = response; } Response.prototype.json = function() { return JSON.parse(this.response); };

就这样,咱们我们可以在 Fetch 中使用 Response:


window.fetch = fetch; function Response(response) { this.response = response; } Response.prototype.json = function() { return JSON.parse(this.response); }; function fetch(url) { return new Promise(function(resolve, reject) { const request = new XMLHttpRequest(); request.open("GET", url); request.onload = function() { // 前面: // resolve(this.response); // 现在: const response = new Response(this.response); resolve(response); }; request.onerror = function() { reject("Network error!"); }; request.send(); }); } fetch("https://academy.valentinog.com/api/link/") .then(function(response) { return response.json(); }) .then(function(json) { console.log(json); }) .catch(function(error) { console.log(error); });

上面的代码在浏览器的控制台中打印一个对象数组。现在咱们来处理误差。Fetch 的真实版本比我们的 polyfill 复杂得多,但是实现相同的行为并不困难。Fetch 中的响应对象有一个属性,一个名为“ok”的布尔值。该布尔值在请求成功时设置为 true,在请求失败时设置为 false。开发人员可以通过引发错误来检查布尔值并更改 Promise 处理程序。 这是使用实际 Fetch 检查状态的方法:


fetch("https://academy.valentinog.com/api/link/") .then(function(response) { if (!response.ok) { throw Error(response.statusText); } return response.json(); }) .then(function(json) { console.log(json); }) .catch(function(error) { console.log(error); });

如你所见,还有一个 "statusText" 。 在 Response 对象中似乎容易实现 "ok""statusText"。 当服务器响应成功,response.oktrue

  • 状态码等于或小于200
  • 状态码小于 300

重构 Response 方法,如下所示:


function Response(response) { this.response = response.response; this.ok = response.status >= 200 && response.status < 300; this.statusText = response.statusText; }

这里不需要创建 “statusText“,因为它已经从原始 XMLHttpRequest 响应对象返回了。这意味着在构造自定义响应时只需要传递整个响应


function fetch(url) { return new Promise(function(resolve, reject) { const request = new XMLHttpRequest(); request.open("GET", url); request.onload = function() { // 前面: // var response = new Response(this.response); // 现在: pass the entire response const response = new Response(this); resolve(response); }; request.onerror = function() { reject("Network error!"); }; request.send(); }); }

但是现在咱们的 polyfill 有问题。 它接受单个参数 “url“,并且仅对其发出 GET 请求。修复这个问题应该很容易。首先,我们可以接受第二个名为requestInit的参数。然后根据该参数,我们可以构造一个新的请求对象:

  • 默认情况下,发出 GET 请求
  • 如果提供,则使用 requestInit 中的 bodymethodheaders

首先,创建一个带有一些名为 bodymethodheaders 的属性的新 Request 函数,如下所示:


function Request(requestInit) { this.method = requestInit.method || "GET"; this.body = requestInit.body; this.headers = requestInit.headers; }

但在此之上,我们可以为设置请求头添加一个最小的逻辑


function fetch(url, requestInit) { return new Promise(function(resolve, reject) { const request = new XMLHttpRequest(); const requestConfiguration = new Request(requestInit || {}); request.open(requestConfiguration.method, url); request.onload = function() { const response = new Response(this); resolve(response); }; request.onerror = function() { reject("Network error!"); }; // 设置 headers for (const header in requestConfiguration.headers) { request.setRequestHeader(header, requestConfiguration.headers[header]); } // more soon }); }

setRequestHeader 可以在 XMLHttpRequest 对象上直接使用。 headers 对于配置 AJAX 请求很重要。 大多数时候,你可能想在 headers 中设置 application/json 或身份验证令牌。

  • 如果没有 body,则为空请求
  • 带有一些有效负载的请求是 body 提供的

function fetch(url, requestInit) { return new Promise(function(resolve, reject) { const request = new XMLHttpRequest(); const requestConfiguration = new Request(requestInit || {}); request.open(requestConfiguration.method, url); request.onload = function() { const response = new Response(this); resolve(response); }; request.onerror = function() { reject("Network error!"); }; // Set headers on the request for (const header in requestConfiguration.headers) { request.setRequestHeader(header, requestConfiguration.headers\[header\]); } // If there's a body send it // If not send an empty GET request requestConfiguration.body && request.send(requestConfiguration.body); requestConfiguration.body || request.send(); }); }

下面是完整的代码:


"use strict"; window.fetch = fetch; function Response(response) { this.response = response.response; this.ok = response.status >= 200 && response.status < 300; this.statusText = response.statusText; } Response.prototype.json = function() { return JSON.parse(this.response); }; function Request(requestInit) { this.method = requestInit.method || "GET"; this.body = requestInit.body; this.headers = requestInit.headers; } function fetch(url, requestInit) { return new Promise(function(resolve, reject) { const request = new XMLHttpRequest(); const requestConfiguration = new Request(requestInit || {}); request.open(requestConfiguration.method, url); request.onload = function() { const response = new Response(this); resolve(response); }; request.onerror = function() { reject("Network error!"); }; for (const header in requestConfiguration.headers) { request.setRequestHeader(header, requestConfiguration.headers[header]); } requestConfiguration.body && request.send(requestConfiguration.body); requestConfiguration.body || request.send(); }); } const link = { title: "Building a Fetch Polyfill From Scratch", url: "https://www.valentinog.com/fetch-api/" }; const requestInit = { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(link) }; fetch("https://academy.valentinog.com/api/link/create/", requestInit) .then(function(response) { if (!response.ok) { throw Error(response.statusText); } return response.json(); }) .then(function(json) { console.log(json); }) .catch(function(error) { console.log(error); });

真正的 Fetch API 实现要复杂得多,并且支持高级特性。我们只是触及了表面。可以改进代码,例如,添加 headers 的逻辑可以独立存在于方法上。

此外,还有很大的空间可以添加新特性:支持 PUTDELETE 以及更多以不同格式返回响应的函数。如果你想看到更复杂的获取 API polyfill,请查看来自 Github的 工程师的 whatwg-fetch。你会发现与咱们的 polyfill 有很多相似之处。

总结

AJAX 让我们有机会构建流畅的、用户友好的界面,从而改变了我们构建 web 的方式。经典页面刷新的日子已经一去不复返了。

现在,咱们可以构建优雅的 JS 应用程序并在后台获取所需的数据。XMLHttpRequest 是用于发出HTTP 请求的优秀的旧遗留的 API,今天仍在使用,但其形式有所不同: Fetch API

往期推荐

作者:valentinogagliardi
链接:https://segmentfault.com/a/1190000020589899

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:【JS 小书】第 10 章:使用异步 JavaScript

链接:https://www.javascriptc.com/3381.html

« JavaScript它如何运行:深入V8引擎&编写优化代码的5个技巧
iOS 暗黑模式适配的完美解决方案»
Flutter 中文教程资源

相关推荐

QR code