做者:valentinogagliardi
译者:前端小智
来源:github
阿里云服务器很便宜火爆,今年比去年便宜,10.24~11.11购买是1年86元,3年229元,能够点击 下面连接进行参与:
https://www.aliyun.com/1111/2...javascript
若是你问我,我会说 JS 挺强大的。做为一种在浏览器中运行的脚本语言,它能够作全部相似的事情:html
等等。在第 8
章中,我们从数组开始构建了一个 HTML 表格。 硬编码数组是一个同步数据源,也就是说,能够直接在我们的代码中使用它,无需等待。 可是大多数时候,数据都是从后台请求过来的。网络请求始终是异步操做,而不是同步数据源:请求数据,服务器会有必定的延迟后才响应。前端
JS 自己没有内置的异步性:它是“宿主”环境(浏览器或 Node.j),为处理耗时的操做提供了外部帮助。在第3章中,我们看到了setTimeout
和 setInterval
,这两个属于 Web API
的。浏览器提供了不少 API,其中还有一个叫XMLHttpRequest
,专门用于网络请求。java
事实上,它来自于以 XML 数据格式的时代。如今 JSON
是最流行的用于在 Web 服务之间移动数据的通讯“协议”,但 XMLHttpRequest
这个名称最终被保留了下来。ios
XMLHttpRequest
也是 AJAX
技术的一部分,它是 “异步JavaScript和XML” 的缩写。AJAX 就是为了在浏览器中尽量灵活地处理网络请求而诞生的。它的做用是可以从远程数据源获取数据,而不会致使页面刷新。当时这个想法几乎是革命性的。随着 XMLHttpRequest (大约13年前)的引入,我们可使用它来进行异步请求。git
var request = new XMLHttpRequest(); request.open('GET', "https://academy.valentinog.com/api/link/"); request.addEventListener('load', function() { console.log(this.response); }) request.send();
在上述的示例中:github
XMLHttpRequest
对象XMLHttpRequest 是基于DOM事件的,我们可使用 addEventListener
或 onload
来监听“load
”事件,该事件在请求成功时触发。对于失败的请求(网络错误),我们能够在“error
”事件上注册一个侦听器:web
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
。编程
从 REST API 提取数据后,我们将构建一个简单的 HTML 列表。 新建一个名为 build-list.html 的文件:json
<!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 接口请求数据。
这里我们使用 ES6 类的方法来构建,还会使用私有类字段(在撰写本文时,Firefox不支持该字段)。在编写任何代码以前,都要思考一下,别人会“如何使用个人类”? 例如,另外一个开发人员可使用我们的代码并经过传入来调用该类:
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"; } }
你可能不喜欢语法,可是私有类字段能够完成其工做。 这种方式,咱们就不能从外部访问 url
和 target
:
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。如今,我想:
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并在浏览器控制台中再次查看 Scopes 。response
是 undefined
,由于咱们要访问的 this
是 List。这与箭头函数和类的行为一致(类默认为严格模式)。那么如今有什么解决办法吗?
从第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
。
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); });
Fetch
比 XMLHttpRequest
更方便、更干净,但它有不少特性。例如,必须特别注意检查响应中的错误。在下一节中,我们将了解关于它的更多信息,同时从头从新构建 Fetch
。
为了更好的理解 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(); }); }
被拒绝的 Promise 由 catch
处理:
fetch("https://acdemy.valentinog.com/api/link/") .then(function(response) { console.log(response); }) .catch(function(error) { console.log(error); });
如今,若是 url
是错误的,会打印具体的错误信息到控制台。若是 url
正确,则打印请求到数据:
上述实现方式还不够完善。首先,我们须要实现一个返回 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.ok
为 true
:
重构 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
的参数。而后根据该参数,咱们能够构造一个新的请求对象:
body
、method
和 headers
首先,建立一个带有一些名为 body
,method
和 headers
的属性的新 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
提供的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
的逻辑能够独立存在于方法上。
此外,还有很大的空间能够添加新特性:支持 PUT 和 DELETE 以及更多以不一样格式返回响应的函数。若是你想看到更复杂的获取 API polyfill,请查看来自 Github的 工程师的 whatwg-fetch。你会发现与我们的 polyfill
有不少类似之处。
AJAX 让咱们有机会构建流畅的、用户友好的界面,从而改变了咱们构建 web 的方式。经典页面刷新的日子已经一去不复返了。
如今,我们能够构建优雅的 JS 应用程序并在后台获取所需的数据。XMLHttpRequest 是用于发出HTTP 请求的优秀的旧遗留的 API,今天仍在使用,但其形式有所不一样: Fetch API。
代码部署后可能存在的BUG无法实时知道,过后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给你们推荐一个好用的BUG监控工具Fundebug。
原文:https://github.com/valentinogagliardi/Little-JavaScript-Book/blob/v1.0.0/manuscript/chapter9.md
阿里云最近在作活动,低至2折,有兴趣能够看看:https://promotion.aliyun.com/...
干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。
https://github.com/qq449245884/xiaozhi
由于篇幅的限制,今天的分享只到这里。若是你们想了解更多的内容的话,能够去扫一扫每篇文章最下面的二维码,而后关注我们的微信公众号,了解更多的资讯和有价值的内容。
每次整理文章,通常都到2点才睡觉,一周4次左右,挺苦的,还望支持,给点鼓励