做者: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次左右,挺苦的,還望支持,給點鼓勵