前端若要與伺服端互動,現今的標準方案,有XMLHttpRequest(XHR)、Fetch、Server-Sent Event(SSE)與WebSocket,該採用哪個技術?

其實,這並不是方案的新舊或誰取代誰的問題,因為,使用XHR不見得是過時,使用WebSocket也不見得符合現代潮流。

基於請求回應

HTTP本身是個基於請求回應的模型,自然地,最為開發者熟悉的前端網路請求技術XHR,也是基於請求回應,雖說是古老技術,卻也是令JavaScript重生、前端技術開始蓬勃發展的要素之一,然而XHR的起源,卻是來自開發者眼中萬惡的IE──2000年左右,微軟為了處理與Exchange Server的互動,以ActiveX控制項的形式出現在IE5。

在IE5中,指定'Microsoft.XMLHTTP'建立的ActiveXObject物件,實現了IXMLHTTPRequest介面,而Mozilla在2002年的Gecko引擎中,仿造了類似的介面nsIXMLHttpRequest,並實現為XMLHttpRequest。

從歷史來看,XMLHttpRequest其實是仿造品,然而使用Gecko的瀏覽器,像是Firefox等,帶動XMLHttpRequest成為產業標準;後來,IE7也提供了XMLHttpRequest作為包裹器,同年W3C開始為XMLHttpRequest進行標準化;而到了2011年,才有了 XMLHttpRequest Level 1的正式規範。

不過,2014年HTML5標準正式釋出,其中包含了Fetch API,從而令XHR在不少開發者眼中,成了過時的技術之一。

有些開發者單純就真的只是因為XHR歷史悠久而覺得過時,偶而會有開發者在Ajax最後一個字母XML上作文章(相對於JSON),更常見的是在事件模型、API上探討,認為XHR撰寫麻煩而Fetch簡單易懂。其實兩者並非可以直接比較的東西,這部份可以參考先前專欄〈從XHR到Fetch〉

無論是XHR或是Fetch,都是基於請求回應模型,沒有請求,伺服端就不能發送資訊給客戶端。為了基於XHR、Fetch模擬伺服端推送,最基本的方式是輪詢(polling),客戶端週期性地發送請求以取得伺服端的回應,這種方式的好處是伺服端不用特別的支援,然而在伺服端真正狀態更新之前,這些發送的請求徒然耗費網路流量。

後來有了長輪詢(long polling),客戶端週期性地發送請求,伺服端接收請求後會先保留,在狀態有更新時,才予以回應,也就是說,客戶端可能發了第五次請求,伺服端才回應第一個請求,於是,就省去了不必要的回應。然而,若要這麼做,伺服端必須保持長時間連線,要有非同步伺服端技術的支援,否則光靠執行緒來持有連線,將會造成伺服端極大的負擔。

單向的Server-Sent Event

如果客戶端在發出一次請求後,伺服端保持不斷線,在每次狀態更新時都利用此一連線回應,也就是說,回應是個不中斷的資料流,客戶端在伺服端回應後,若能識別並取出當次的資料,就可以省去週期性地發出請求的負擔。

HTML5規範了Server-sent Events,主要由兩個部份組成:一個是客戶端與伺服端之間的協定,另一個是EventSource介面。在協定的基本部份,伺服端在不中斷的連線中要回應時,須包含ContentType: text/event-stream標頭,而每次的回應訊息,須以data:作為前置,並以\n\n結尾。

若瀏覽器支援SSE協定,使用XHR也能處理持續連線時回應的資料。由於連線不中斷,因此,要在XHR實例readyState為XMLHttpRequest.LOADING(常數3)處理,並且要自行記錄已接收的資料,從中切割出當次回應,例如,

if (xhr.readyState === XMLHttpRequest.
LOADING) {
let r = xhr.responseText.substring(received);
console.log(r);
received += r.length;
}

使用XHR必須自行處理這類細節,而且斷線後,必須自新重新建立XHR實例發出請求;若是使用SSE規範的EventSource介面,只要註冊EventSource實例的message事件,從Event實例的data特性就可以取得每次的回應,而且EventSource介面支援斷線重連,使用上方便許多;同樣地,由於必須保持長時間連線,想支援SSE的伺服端必須要有非同步伺服端技術的支援,才不會有效能的問題。

雙向的WebSocket協定

SSE基本上還是基於HTTP,脫離不了請求回應模型,連線建立之後,只能單向地從伺服端推送資料,客戶端、伺服端間無法雙向溝通。

為了解決這個問題,WebSocket於2011年標準化,在連線建立之後,客戶端與伺服端可以雙向互動,就算客戶端沒有請求,伺服端也可以主動發送訊息,能脫離請求回應模型的原因在於,WebSocket並非基於HTTP,是基於TCP的協定。

然而,WebSocket在建立連線時,確實仍基於HTTP,正確來說,是基於協定升級機制(https://mzl.la/2ZzD0yG),協定識別符號是ws、wss(相對於http、https),建立連線最初是透過HTTP發送Connection: Upgrade以及Upgrade: websocket標頭,以及一些特定於WebSocket的擴充標頭,若伺服端支援WebSocket,也會回應Connection、Upgrade,以及相對的WebSocket擴充標頭。

在經過協定升級機制、連線建立之後,客戶端和伺服器之間可以進行雙向的資料傳送,這是透過連接埠80或443完成。雖然WebSocket協定與HTTP無關,不過在客戶端,API與XHR、Fetch、SSE等類似,也是採用基於事件的模型,使用上並不困難;後來常見一種說法「WebSocket是為了取代SSE等伺服端推送的模擬而提出」有此一說的原因之一可能在於,IE與Edge不支援SSE,然而IE10之後的版本及Edge支援WebSocket。

如果不考慮IE與Edge,WebSocket用來取代SSE的說法也不完全正確,SSE基本上仍是基於HTTP,對於僅需要單向接受伺服端訊息的應用程式來說,使用SSE就足夠了。如果客戶端與伺服端需要雙向互動,而不單只是伺服端推送訊息,例如網路遊戲,才適合使用WebSocket。

如前所述,WebSocket要解決的是雙向溝通的問題,而不是單純地要取代SSE,當然,開發者確實也可以單純地將WebSocket用於伺服端推送訊息,儘管這並不能發揮WebSocket真正的優點。

不是取代的問題

在簡單的網路請求時,使用Fetch API需要的程式碼,比XHR來得簡潔;然而,Fetch也不是用來取代XHR,正如〈從XHR到Fetch〉中談過的,兩者並不衝突,開發者也可以將兩者互補的部份做進一步封裝,以符合應用程式需求。

方案的提出,總會有時間點上的差異,面對同一需求,也經常可以使用不同方案來解決,然而並非因新舊或可解決的需求重疊,而可斷言方案之間是取代的關係。

我們應該關注的是,哪個需求是某方案可簡單解決,而哪一方案得大費周章,忘了「取代」這兩個字,真正重要的是哪個方案可以解決需求!

專欄作者

熱門新聞

Advertisement