Why's THE Design(爲何這麼設計) 是一系列關於計算機領域程序設計決策的文章(偏向於前端領域),在該系列會從不一樣的角度討論這種設計的優缺點、對具體實現形成的影響。由 Draveness 的《爲何這麼設計》 啓發javascript
在閱讀本文以前,須要你們先忘掉相似於 $.ajax()
和 axios
這類的庫方法或庫,迴歸到最原始的 XMLHttpRequest
,而後再去思考新設計的 fetch API
。所以閱讀本文以前,你須要一些簡單的前置知識:XMLHttpRequest
(後面以 XHR
簡稱) 和 fetch API
。html
首先,咱們給兩個概念下一個定義。XHR
和 fetch API
都是瀏覽器給上層使用者暴露出來的 API(相似於操做系統暴露系統 API 給瀏覽器這類應用同樣)。這兩套暴露的 API 給上層使用者提供了部分操做 http 包的能力。換句話說,這二者都是創建在 http 協議上的,咱們能夠將其當成具備部分功能的 http 客戶端。前端
XHR
出現時間很早,最開始在 Microsoft Exchange Server 2000 的 Outlook Web Access 中引入,隨後在 1999 年加入到 IE5 中,最後全部主流的瀏覽器都引入了該特性。也就是說,XHR
已經 21 歲高齡了(一種技術能存活如此之久,足夠證實其經典)。而 XHR
變得人盡皆知則是因爲 AJAX 架構的出現(在這裏須要提到的是,Ajax 更多的被認爲是一種 Web 應用架構,其最先出如今 Jesse James Carrett 於 2005年 發表的一篇 《Ajax: A New Approach to Web Applications》),各類著名應用都使用了 AJAX(好比 Google 的 Gmail)。無論是誰促進了誰,都足以證實 XHR
解決了當時很大的痛點問題,減小網絡延遲或損耗,提升用戶體驗,並加強了 JavaScript 在瀏覽器上操做 HTTP 的能力。java
用技術術語來說,XHR
在當時很好的解決了客戶端和服務器端的異步通訊。咱們想象下當時的情況,Web 體系增加很快,但web 應用的功能仍然是相對簡單的,更可能是對信息的展現,所以 XHR
設計者無需考慮過多的架構設計(仔細思考一下,當時沒有硬需求,並且過分設計一樣是個問題)。ios
以現代軟件工程的角度來看,XHR
的整個設計很是混亂,將 request、response 和事件監聽混在一個對象裏,而且須要經過實例化纔可以發送請求(後面咱們經過實際代碼來演示)。這帶來的問題是在實際使用過程當中配置和調用方式沒有組織和可維護性(注意,這在 web 應用比較簡單的時候不構成問題)。用架構的術語來說,XHR 不符合關注點分離原則(SOC),SOC 原則指望在設計系統時候可以將系統元素分離開來,儘可能保持各個元素的職責單一(好比 TCP/IP 協議簇的分層、經典 MVC 架構都是 SOC 原則的經典體現)。git
咱們來看一下最原始的 XHR
的使用:程序員
let xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';
xhr.onload = function() {
console.log(xhr.response);
}
xhr.onerror = function() {
console.log('error');
}
xhr.send();
複製代碼
從上面代碼能夠很明顯的看出,http request 、http response 和事件監聽都處於同一個 xhr
實例裏面。整個代碼組織缺乏語義化,而且可能陷入回調地獄的窘境。若是沒有各類包裝庫的實現(這也一樣是 fetch
API 出現後難以推廣的緣由之一,由於庫封裝的很好),手寫 xhr
絕對是個痛苦的事情。github
按照 jake archibald(Google chrome 成員,我的比較喜歡的一個技術專家 ) 的話來說:web
XHR was an ugly baby and time has not been kind to it. It's 16 now. In a few years it'll be old enough to drink, and it's enough of a pain in the arse when it's soberajax
從近二十年的歷史來看,Web 絕對是世界的中心(你看 JavaScript 設計得這麼潦草的語言仍然佔據很重要地位就能夠側面理解這句話的含義了)。Web 的發展表明着用戶羣體增多,也就意味着各類需求的增多(好比由文本傳遞到各類多媒體信息傳遞),各類技術方案、標準不斷被引入到 Web 體系中(好比著名的 WHATWG 組織和 ECMA International 組織),XHR
顯然到了該改變的時候,而改變每每有兩種大方向:
XHR
中有 XMLHttpRequest2
的方案發布(提供了操做二進制數據的能力)。到如今爲止,XHR
standard 仍然有少許的更新。fetch API
)。好處在於沒有歷史的束縛,而問題在於如何讓開發者社區來接受這一套新東西(尤爲是在舊方案仍然知足大部分需求的時候)標準編著者在設計方案時除了須要考慮使用者的方便,還要着眼於將來趨勢和是否與現有其餘議案產生衝突。從各種相關產品(這裏我將各類設計稱爲產品的目的是想從產品的角度思考問題)的時間線上來看:
$.ajax()
) 出如今 2011 年你會發現,這幾個時間線是很接近的,若是你再聯想到 JavaScript,當時 JavaScript 世界的最重大事件莫過於 ES2015 被一步步標準化(這也意味着 Promise 被正式引入標準,儘管 Promise
的理念早已經在程序世界家喻戶曉),所以上面的幾個產品不約而同地使用了 Promise
的方式來設計各自上層的 API(這也側面說明了回調這種異步寫法不太符合程序員線性順序處理思惟)。
咱們來看看 fetch API
在設計時主要考慮點在哪裏:
具體 fetch API
使用方式以下:
fetch(url)
.then((r) => r.json())
.then((data) => console.log(data))
.catch((e) => console.log('error'))
複製代碼
代碼組織比最開始的 XHR
更加清爽了不少,若是使用 async/await
語法則更加簡潔。
self.addEventListener('fetch', function (event) {
if (event.request.url === new URL('/', location).href) {
event.respondWith(
new Response('<h1>Hello!</h1>', {
headers: {'Content-Type', 'text/html'}
})
)
}
})
複製代碼
在上面的代碼中, event.request
是一個 Request
。這意味着能夠直接在客戶端實現 response
,而不是讓瀏覽器去請求網絡,這樣能夠結合 cache
實現某些靈活功能,這是 XHR
不能實現的。
可是一項新技術方案的出現,必定會引發業界的討論,甚至是爭議。很明顯,fetch API
儘管有很是先進的設計理念,但仍然帶來了很多的爭議(尤爲是 fetch API
很難實現某些 XHR
的功能時),這類爭議被我分爲兩類:
第一個誤解是(來自於一個 JavaScript 社區成員),「做爲平臺方,不該該在 XHR
基礎上添加 high level features,而是應該提供更加 low level 的 primitives」。
很明顯,上面的話陷入了一個誤區,「一個設計良好的、簡潔的 API 就是 high level的」。若是仔細看過規範的,你會發現, XHR 目前是創建在 fetch 的基礎上的,有規範爲證(在 XHR 的 send()
上):
Fetch req. Handle the tasks queued on the networking task source per below.
也就是說,fetch
API 實際上更加低階,也就會給上層開發者提供更加靈活的能力(指的普通前端開發者)。
第二個誤解是(也是社區常常抱怨的),認爲規範須要提供一個更加完善的東西,而不是一個半成品。
說實話我的認爲這是屬於開發者的「雙標」。當咱們做爲開發者時,咱們對於本身產品的要求是迭代式開發;而當咱們做爲消費者使用第三方技術或標準時,咱們的反應是 「how dare you present me with such incomplete imperfection」,這不是妥妥的雙標又是什麼呢?迭代發佈意味着規範方可以從實際使用中得到反饋進行改進,而且能夠指導將來的設計(若是你有看過 CSS 特性發布歷史的話你能夠理解這句話的含義 - 具體指的 CSS 各類前綴的濫用,致使尾大不掉)。jake archibald 的圖片很好的體現了這一點:
第三個誤解是,認爲 fetch API
對於某些 http 錯誤碼不會 reject(好比 400
、500
等)。可是我支持規範方,由於 fetch API
做爲一個更 low-level 的 API,無論是錯誤碼仍是正確碼都表示 http 客戶端有接受到服務器的 response,而網絡錯誤這類才真正表明着異常。
第一類爭議在通過解釋後每每會消失掉,而真正麻煩的是第二類 - 缺乏方便實現舊方案功能的特性(注意這裏指的是方便實現,而不是各類 hack 實現),具體到 fetch API
有如下幾個(不全,可是也能基本描述本文所想講的東西了):
XMLHttpRequest
單獨提供了 timeout
屬性用來處理超時中斷。XHR
提供了 onprogress
事件來幫助咱們來實現該功能,具體代碼以下:const xhr = new XMLHttpRequest();
xhr.open('GET', '/foo');
xhr.addEventListener('progress', (event) => {
const { lengthComputable, loaded, total } = event;
if (lengthComputable) {
console.log(`Downloaded ${loaded} of ${total} (${(loaded / total * 100).toFixed(2)}%)`);
} else {
console.log(`Downloaded ${loaded}`);
}
});
xhr.send();
複製代碼
首先看 request aborting 功能,在 XHR
中,中斷一個請求能夠直接調用 XHR
實例上的 abort()
方法,而且可使用事件監聽(onabort
事件),監聽請求中斷,做出相應的響應。 對於超時中斷,XHR
實例提供了 timeout
屬性來幫助咱們方便的實現功能,同時 XHR
還提供了 ontimeout
事件(這裏就不聊具體代碼如何寫了,Google 一下就行)
在最開始的 fetch API
並無提供上面的功能(這裏你能夠先思考一個問題,爲何這麼簡單的 API 實現,在 fetch API
中就成了很是激烈的討論?),具體的討論總共經歷了漫長的兩年之久,堪稱 fetch API
討論最激烈的特性了,我先給出討論的幾個階段(在這裏我不過多描述爭論的細節,只解釋幾個方案的 tradeoff,細節能夠看我給出的連接):
fetch
。在最開始的討論中,主要有兩種方案:
cancelable promise
方案(由 jake archibald 提出),也就是特別封裝一個 CancellablePromise
,具體使用以下:var p = fetch(url).then(r => r.json());
p.abort(); // cancels either the stream, or the request, or neither (depending on which is in progress)
var p2 = fetch(url).then(response => {
return new CancellablePromise(resolve => {
drainStream(
response.body.pipeThrough(new StreamingDOMDecoder())
).then(resolve);
}, { onCancel: _ => response.body.cancel() });
});
複製代碼
首先這種方案的第一個須要考慮的問題是,該叫什麼才能不產生歧義,abort
(和 XHR
保持一致)、terminate
仍是 cancel
。注意這也是很是重要的,由於一個新的 API 設計必定要考慮其語義性。
先把命名放一邊,你會發現這種方案對於上層開發者是否是特別友好,也比較符合原有 XHR
的寫法。
可是這種方案卻受到另外一方的強烈反對,主要表明是 getify(《you don't know js》 做者,我的認爲其水平很高,可是容易夾帶私貨)。他反對的點主要是更加深層次的設計,咱們來看下他主要的論點是什麼:
cancelable promise
的設計背離了 promise
的設計(若是後面看不懂,這個論點看到這裏就能夠了),若是咱們的設計將 abort 特性獨立在 promise 的創造上下文,那麼意味着 promise
會丟失可信賴性,影響對於內部的 promise 觀察(這種討論在最開始 promise
引入 ES6 時已經進行了好久,你仔細思考下 promise 的 resolve 和 reject 是否是在初始化時就已經肯定了,這也是顯示了一種不可變性)Abort != Promise Cancelation... Abort == async Cancel
。這句話的含義是 promise 的取消意味着 fetch
的終止,這是 back-pressure。這樣是否是會給 async/await
的某些設計帶來問題。規範在設計一個新的 API,也須要考慮是否會影響到的方案,由於各個方案參與者並不同(好比對於 streams API
是否有影響)到這裏,不一樣的討論方都相應給出了各自設計的考慮點,可是卻誰也說服不了誰,直到後來第一種方案出現可能的安全問題,才被直接否決。可是 abort
的需求仍然是存在的,儘管 controller + signal
的方案看起來沒有那麼的優雅,可是在第一種方案有重大問題時,其餘方案的缺點就顯得沒那麼重要了。最終第二種方案 controller + signal
被肯定下來(具體討論細節看上面的連接 3)。
到這裏,你是否是已經能體會到上面簡單的特性引入到底是怎麼涉及到深層次設計問題的。
正如我在 《爲何 setTimeout 的最小延遲是 4ms》 中一直在講的觀點是,「對於同一個需求,不一樣參與方的思考角度不同,會帶來不一樣的方案,也就會產生不少不一樣的 tradeoff。而如何思考、權衡,則是架構師的必備技能」
儘管該方案的討論時長過於的長,咱們最終仍是有了落地的方案(儘管不少人仍是不能接受,但有時候有了總比原地踏步要強些),具體代碼以下所示:
const controller = new AbortController();
const signal = controller.signal;
setTimeout(() => controller.abort(), 5000);
fetch(url, { signal }).then(response => {
return response.text();
}).then(text => {
console.log(text);
});
複製代碼
上面的代碼就是結合 controller + setTimeout
實現了超時中斷的功能。
可是,不幸的是 progress events 並無不少的進展。最開始提的幾個方案都被否決了,而且到筆者寫做時仍然處於停滯當中(2020 年 7 月)。我的認爲主要緣由仍是規範方精力有限,而該特性又不是優先級特別高。
可是,做爲上層應用者咱們仍然能夠結合 streams API
和 service worker
來實現上述需求(提一句,streams API
是很是優秀的功能,給了前端開發者更多操做底層網絡數據的能力)。在這裏,就不具體闡述實現方案了,給出我我的認爲實現很好的: fetch progress indicators
在正文部分,咱們詳細描述了 XHR
的設計問題和 fetch API
的設計理念,而且分析了這樣設計的優缺點。可是我仍然須要提到的是,這並非 fetch API
的所有,沒法在一篇文章裏面講完全部的細節。
誠然,對於上層開發者來說,理解這些設計理念和背後的各類方案、tradeoff,很是有助於開闊咱們的視野,以及更加深刻理解設計背後所涉及到的 computer science 概念和優秀實踐。可是,咱們仍然須要依託於現實世界的業務需求,畢竟需求每每是更加接近本身的。
換句話說,咱們須要考慮實際業務狀況來考慮使用舊的 XHR
, 仍是新的 fetch API
(看起來是廢話,可是是你須要的)。
我舉幾個你能夠權衡的點:
fetch API
, 那麼須要 polyfill(使用 XHR
polyfill)。streams API
和 service worker
。若是這兩個特性對於你的應用特別重要,那麼我推薦你使用 fetch API
。下面回答最後一個問題,除了兼容性,還有什麼遏制了 fetch API
替代了 XHR
?
那是由於各類優秀的庫(XHR
封裝)基本可以知足上層應用者大部分的功能需求,而且也沒有特別大的缺點,那麼爲何還要冒着風險使用新的 fetch API
呢?這是否是也體現了你已經默默作了技術選擇的 tradeoff 呢?
儘管有種種的問題,可是 fetch API
的將來仍然是光明的,npm 的 polyfill 包下載量也能簡單的說明問題:
whatwg-fetch
周下載量是 8,744,612
axios
周下載量是 10,041,206
二者的差距也並非太遠,不是嗎?
我是 BY,一個有趣的人,之後會帶來更多的原創文章。
本文章遵循 MIT 協議,轉載請聯繫做者。
有興趣能夠關注公衆號(百學原理)或者 Star GitHub repo.