本文已經首發於InfoQ中文站,著作權,原文為《用Dojo實現Ajax請求:XHR、跨域、及其他》,如需轉載,請務必附帶本聲明,謝謝。
InfoQ中文站是一個面向中高端技術人員的線上獨立社區,為Java、.NET、Ruby、SOA、敏捷、架構等領域提供及時而有深度的資訊、高端技術大會如QCon 、線下技術交流活動QClub、免費迷你書下載如《架構師》等。
在任何瀏覽器上方便地實現Ajax請求是每一個Ajax架構的初衷。Dojo在這方面無疑提供了非常豐富的支援。除了XMLHttpRequest 之外,動態script、iframe、RPC也應有盡有,並且介面統一,使用方便,大多數情況下都只需要一句話就能達到目的,從而免除重複造輪子的麻煩。而且,Dojo一貫追求的概念完整性也在這裡有所體現,換句話說,在使用Dojo的Ajax工具的過程中不會感到任何的不自然,相反更容易有觸類旁通的感覺,因為API的模式是統一的,而且這裡涉及到的某些概念(如Deferred對象)也貫穿在整個Dojo之中。
Dojo的XHR函數
Dojo的XMLHttpRequest函數就叫dojo.xhr,除了把自己取名貨幣符號之外,這好像是最直接的辦法了。它定義在Dojo基本庫裡,所以不需要額外的require就能使用。它可以實現任何同域內的http請求。不過更常用的是dojo.xhrGet和dojo.xhrPost,它們只不過是對dojo.xhr函數的簡單封裝;當然根據REST風格,還有dojo.xhrPut和dojo.xhrDelete。
這些函數的參數都很統一。除了dojo.xhr的第一個參數是http方法名之外,所有的dojo.xhr*系列函數都接受同一種散列式的參數,其中包含請求的細節,例如url、是否同步、要傳給伺服器的內容(可以是普通對象、表單、或者純文字)、逾時設定、返回結果的類型(非常豐富且可擴充)、以及請求成功和失敗時的回調。所有dojo.xhr*函數(實際上是所有IO函數)傳回值也都一樣,都是一個Deferred對象,顧名思義,它能讓一些事情“延遲”發生,從而讓API用起來更靈活。
下面的兩個例子可能會帶來一點直觀感受:
dojo.xhrGet({ url: "something.html", load: function(response, ioArgs){ //用response幹一些事 console.log("xhr get success:", response); return response; //必須返回response }, error: function(response, ioArgs){ console.log("xhr get failed:", response); return response; //必須返回response }});//Deferred對象允許用同步調用的寫法寫非同步呼叫var deferredResult = dojo.xhrPost({ url: "something.html", form: formNode, //Dojo會自動將form轉成object timeout: 3000, //Dojo會保證逾時設定的有效性 handleAs: "json" //得到的response將被認為是JSON,並自動轉為object});//當響應結果可用時再調用回呼函數deferredResult.then(function(response){ console.log("xhr get success:", response); return response; //必須返回response});
首先解釋一下timeout。除了IE8之外,目前大多數XMLHttpRequest對象都沒有內建的timeout功能,因此必須用 setTimeout。當同時存在大量請求時,需要為每一個請求設定單獨的定時器,這在某些瀏覽器(主要是IE)會造成嚴重的效能問題。dojo的做法是只用一個單獨的setInterval,定時輪詢(間隔50ms)所有還未結束的請求的狀態,這樣就高效地解決了一切遠程請求(包括JSONP和 iframe)的逾時問題。
值得一提的還有handleAs參數,通過設定這個參數,可以自動識別伺服器的響應內容格式並轉換成對象或文本等方便使用的形式。根據文檔,它接受如下值:text (預設), json, json-comment-optional, json-comment-filtered, javascript, xml。
而且它還是可擴充的。其實handleAs只是告訴xhr函數去調用哪個格式轉換外掛程式,即dojo.contentHandlers對象裡的一個方法。例如 dojo.contentHandlers.json就是處理JSON格式的外掛程式。你可以方便地定製自己所需要的格式轉換外掛程式,當然,你也可修改現有外掛程式的行為:
dojo.contentHandlers.json = (function(old){ return function(xhr){ var json = old(xhr); if(json.someSignalFormServer){ doSomthing(json); delete json.someSignalFormServer; } return json; }})(dojo.contentHandlers.json);//一個小技巧,利用傳參得到原方法
如果要瞭解每個參數的細節,可以參考Dojo的文檔。
虛擬參數類
這裡特別提一下Dojo在API設計上的兩個特點。其一是虛擬參數“類”概念:通過利用javascript對象可以靈活擴充的特點,強行規定一個散列參數屬於某個“類”。例如dojo.xhr*系列函數所接受的參數就稱為dojo.__XhrArgs。這個“類”並不存在於實際代碼中(不要試圖用 instanceof驗證它),只停留在概念上,比抽象類別還抽象,因此給它加上雙底線首碼(Dojo習慣為抽象類別加單底線首碼)。這樣做看起來沒什麼意思,但實際上簡化了API,因為它使API之間產生了聯絡,更容易記憶也就更便於使用。這一點在對這種類做“繼承”時更明顯。例如 dojo.__XhrArgs繼承自dojo.__IoArgs,這是所有IO函數所必須支援的參數集合,同樣繼承自dojo.__IoArgs的還有 dojo.io.script.__ioArgs和dojo.io.iframe.__ioArgs,分別用於動態指令碼請求和iframe請求。子類只向父類添加少量的屬性,這樣繁多的參數就具有了樹形類結構。原本散列式參數是用精確的參數名代替了固定的參數順序,在增加靈活性和可擴充性的同時,實際上增加了記憶量(畢竟參數名不能拼錯),使得API都不像看起來那麼好用,有了參數類的設計就緩解了這個問題。
這種參數類的做法在Dojo裡隨處可見,讀源碼的話就會發現它們都是被正兒八經地以正常代碼形式聲明在一種特殊注釋格式裡的,像這樣:
/*=====dojo.declare("dojo.__XhrArgs", dojo.__IoArgs, { constructor: function(){ //summary: //... //handleAs: //... //...... } }); =====*/
這種格式可以被jsDoc工具自動提取成文檔,在文檔裡這些虛擬出來的類就像真的類一樣五髒俱全了。
Deferred對象
另一個API設計特點就是Deferred對象的廣泛使用。Dojo裡的Deferred是基於MochiKit實現稍加改進而成的,而後者則是受到 python的事件驅動網路工具包Twisted裡同名概念的啟發。概括來說的話,這個對象的作用就是將非同步IO中回呼函數的聲明位置與調用位置分離,這樣在一個非同步IO最終完成的地方,開發人員可以簡單地說“貨已經到了,想用的可以來拿了”,而不用具體地指出到底該調用哪些回呼函數。這樣做的好處是讓非同步IO的寫法和同步IO一樣(對資料的處理總是在取資料函數的外面,而不是裡面),從而簡化非同步編程。
具體做法是,非同步函數總是同步地返回一個代理對象(這就是Deferred對象),可以將它看做你想要的資料的代表,它提供一些方法以添加回呼函數,當資料可用時,這些回呼函數(可以由很多個)便會按照添加順序依次執行。如果在取資料過程中出現錯誤,就會調用所提供的錯誤處理函數(也可以有很多個);如果想要取消這個非同步請求,也可通過Deferred對象的cancel方法完成。
dojo.Deferred的核心方法如下:
then(callback, errback); //添加回呼函數callback(result); //表示非同步呼叫成功完成,觸發回呼函數errback(error); //表示非同步呼叫中產生錯誤,觸發錯誤處理函數cancel(); //取消非同步呼叫
Dojo還提供了一個when方法,使同步的值和非同步Deferred對象在使用時寫法一樣。例如:
//某個工具函數的實現var obj = { getItem: function(){ if(this.item){ return this.item; //這裡同步地返回資料 }else{ return dojo.xhrGet({ //這裡返回的是Deferred對象 url: "toGetItem.html", load: dojo.hitch(this, function(response){ this.item = response; return response; }) }); } }};//使用者代碼dojo.when(obj.getItem(), function(item){ //無論同步非同步,使用工具函數getItem的方式都一樣});
在函數閉包的協助下,Deferred對象的建立和使用變得更為簡單,你可以輕易寫出一個建立Deferred對象的函數,以同步的寫法做非同步事。例如寫一個使用store擷取資料的函數:
var store = new dojo.data.QueryReadStore({...});function getData(start, count){ var d = new dojo.Deferred(); //初始化一個Deferred對象 store.fetch({ start: start, count: count, onComplete: function(items){ //直接取用上層閉包裡的Deferred對象 d.callback(items); } }); return d; //把它當做結果返回}
用dojo.io.script跨域
dojo.xhr* 只是XmlHttpRequest對象的封裝,由於同源策略限制,它不能發跨域請求,要跨域還是需要動態建立