標籤:

Ajax 和 XML: 五種 Ajax 反模式

文檔選項

將此頁作為電子郵件發送

討論

拓展 Tomcat 應用

下載 IBM 開源 J2EE 應用伺服器 WAS CE 新版本 V1.1

級別: 中級

Jack D Herrington (jherr@pobox.com), 高級軟體工程師, Leverage Software Inc.

2007 年 4 月 20 日

通過理解錯誤的編碼方式,可以更好地了解如何正確地進行編碼。當然,編寫 Asynchronous JavaScript? + XML(Ajax)有正確的方法,也有錯誤的方法。本文將討論一些需要避免的常見編碼實踐。

如果人們在第一次就能夠將所有事情全部做對,那麼這個世界將變得完全不同。Ajax 也是如此。我做了大量的工作以支持 Ajax 開發人員(包括我自己),包括編碼、撰寫文章和演講。通過這些工作,我學到了很多關於正確和錯誤編寫 Ajax 的知識。在我的上一篇文章 「五種常見 Ajax 模式:可立即使用這些非常有用的 Ajax 設計模式」 中,我介紹了五種用於正確編寫 Ajax 應用程序的模式。在這篇文章中,我將介紹 Ajax 代碼中常見的五種反模式。

請訪問 Ajax 技術資源中心,這是有關 Ajax 編程模型信息的一站式中心,包括很多文檔、教程、論壇、blog、wiki 和新聞。任何 Ajax 的新信息都能在這裡找到。

您可能會問,什麼是反模式(anti-pattern)反模式 就是頻繁出現的應用程序設計缺陷,已經成為所有人都應該注意的問題。我在這裡將從較高的層次進行討論,而不涉及語法錯誤和鏈接問題。

大多數開發人員聽說過關於反模式的一個很好的例子:結構化查詢語言(Structured Query Language,SQL)的錯誤使用導致 Web 站點受到 SQL 注入攻擊。這種反模式使得公司損失慘重,並暴露了客戶記錄,而且不幸的是沒有一種編程語言可以倖免。因此,我們有必要了解這種模式發生的原理和原因,以及如何避免。

Ajax 反模式也是如此。我並不是說它們將造成公司損失數十億的收入,但是它們可以搞垮伺服器或者提供糟糕的用戶體驗,這種代價不僅昂貴,而且令人沮喪。

如果理解了發生錯誤的內容,您將學到很多知識。很多時候,人們僅僅把 Ajax 看作是一種在載入頁面後從伺服器取回 XML 的方式。這種觀點非常狹隘,並且如果被錯誤使用,將引發應用程序的性能問題。在本文中,我將解釋這種觀點之所以錯誤的原因,以及如何修復這種錯誤。

在沒有必要的時候輪詢計時器

我見到的很多 Ajax 問題都和濫用 JavaScript 語言內置的計時器功能有關。其中的關鍵方法是 window.setInterval()。只要看到這種方法,就需要稍微提高警惕;為什麼要使用一個計時器呢?當然,計時器有其用途 —— 比如,動畫。

window.setInterval() 方法告訴頁面以特定的時間間隔回調某個函數(比如每秒)。大多數瀏覽器對使用這些計時器總是說得多,做得少,主要是因為 JavaScript 語言是單線程的語言。如果您要求的時間間隔為 1 秒,那麼獲得的回調時間間隔可能是 1 秒、1.2 秒、9 秒或任何其他時間。

絕對不需要使用計時器的一種情況就是等待 Ajax 請求的完成。以 清單 1 為例。

清單 1. Antipat1a_polling.html

<html><script> var req = null; function loadUrl( url ) { if(window.XMLHttpRequest) { try { req = new XMLHttpRequest(); } catch(e) { req = false; } } else if(window.ActiveXObject) { try { req = new ActiveXObject(『Msxml2.XMLHTTP『); } catch(e) { try { req = new ActiveXObject(『Microsoft.XMLHTTP『); } catch(e) { req = false; } } } if(req) { req.open(『GET『, url, true); req.send(『『); } } window.setInterval( function watchReq() { if ( req != null && req.readyState == 4 && req.status == 200 ) { var dobj = document.getElementById( 『htmlDiv『 ); dobj.innerHTML = req.responseText; req = null; } }, 1000 ); var url = window.location.toString(); url = url.replace( /antipat1a_polling.html/, 『antipat1_content.html『 ); loadUrl( url ); </script><body> Dynamic content is shown between here:<br/> <div id="htmlDiv" style="border:1px solid black;padding:10px;"> </div>And here.</body></html>

要查看真實環境的演示,請查看這個在線版本

antipat1a_polling.html.

在進行 setInterval 調用之前,所有一切看上去都工作得不錯。這個調用將設置監視請求狀態的計時器,然後使用下載的資源設置頁面內容。

我將展示一個更好的解決方案,用來計算出什麼時候請求能夠完成。同時,清單 2 展示了頁面正在請求的文件。

清單 2. Antipat1_content.html

<b>Hello there</b>

要查看真實環境的演示,請查看這個在線版本

antipat1_content.html.

同時 圖 1 顯示了在我的瀏覽器中看到的頁面。

圖 1. 放置在 HTML 文檔中的內容

所以,您可能會問自己,「它現在可以工作,不是嗎?如果沒有出現故障的話,為什麼要修復呢?」 實際上已經出現故障了,因為程序運行得非常慢。計時器將時間間隔設置為 1 秒,隨著時間的流逝,請求完全超過了時間間隔。所以,您將看到頁面首先出現一個空的框,然後再等待一秒鐘,忽然出現大量的內容。多麼糟糕!

如何解決呢?Ajax 天生就是非同步的。難道不需要進行輪詢循環就能查看何時完成請求嗎?

結果證明,並非如此。正如我在 清單 3 中展示的一樣,XMLHTTPRequest 對象所提供的全部內容是一個名為 onreadystatechange 的回調機制。(多麼好聽的名字,讓人想起了 VAX PDP/11s)。

清單 3. Antipat1a_fixed.html

<html><script> var req = null; function processReqChange() { if (req.readyState == 4 && req.status == 200 ) { var dobj = document.getElementById( 『htmlDiv『 ); dobj.innerHTML = req.responseText; } } function loadUrl( url ) { ... if(req) { req.onreadystatechange = processReqChange; req.open(『GET『, url, true); req.send(『『); } } var url = window.location.toString(); url = url.replace( /antipat1a_fixed.html/, 『antipat1_content.html『 ); loadUrl( url ); </script> ...

要查看真實環境的演示,請查看在線版本

antipat1a_fixed.html.

這個新代碼只是查看請求對象是否發生改變,以響應 onreadystatechange 回調。然後,在完成後更新頁面。

最後的結果是一個載入神速的頁面。頁面出現後,新的內容幾乎是立即填充了頁面框。為什麼呢?因為請求完成後就立即調用了代碼,然後填充頁面。沒有必要將時間浪費在無聊的計時器上。

輪詢反模式的另一個變體是:頁面反覆向伺服器發送請求,即使請求沒有發生變化。請看 清單 4 所示的搜索頁面。

清單 4. Antipat1b_polling.html

<html><script> var req = null; function processReqChange() { if (req.readyState == 4 && req.status == 200 ) { var dobj = document.getElementById( 『htmlDiv『 ); dobj.innerHTML = req.responseText; } } function loadUrl( url ) { ... } window.setInterval( function watchSearch() { var url = window.location.toString(); var searchUrl = 『antipat1_content.html?s=『+searchText.value; url = url.replace( /antipat1b_polling.html/, searchUrl ); loadUrl( url ); }, 1000 ); </script><body><form> Search <input id="searchText" type="text">:<br/> <div id="htmlDiv" style="border:1px solid black;padding:10px;"> </div></form></body></html>

要查看真實環境的演示,請查看在線版本

antipat1b_polling.html.

您可以看到瀏覽器中的頁面能夠發揮作用,如 圖 2 所示。

圖 2. 具有動態響應區域的搜索區域

多麼美妙。這樣看來,頁面非常合理。當我改變搜索文本時,顯示結果的區域也將根據新的搜索條件改變(也許並不完全如此,但是如果為請求安裝一個真正的搜索引擎,它就會這樣做)。

問題是 JavaScript 代碼使用 window.setInterval 來不斷地生成請求,即使搜索欄位沒有發生更改。這將消耗網路帶寬,並消耗伺服器時間。對於一個流行的站點來說,這可是一個致命的組合。

解決方法就是對搜索框使用事件回調,如 清單 5 所示。

清單 5. Antipat1b_fixed.html

<html><script> var req = null; function processReqChange() { ... } function loadUrl( url ) { ... } var seachTimer = null; function runSearch() { if ( seachTimer != null ) window.clearTimeout( seachTimer ); seachTimer = window.setTimeout( function watchSearch() { var url = window.location.toString(); var searchUrl = 『antipat1_content.html?s=『+searchText.value; url = url.replace( /antipat1b_fixed.html/, searchUrl ); loadUrl( url ); seachTimer = null; }, 1000 ); } </script><body><form> Search <input id="searchText" type="text" onkeyup="runSearch()">:<br/> <div id="htmlDiv" style="border:1px solid black;padding:10px;"> </div></form></body></html>

要查看真實環境的演示,請查看在線版本

antipat1b_fixed.html.

這裡,我將 runSearch() 函數與搜索框的 onkeyup() 方法關聯起來。通過這樣做,當用戶在搜索框中輸入內容時,我將得到回調。

runSearch() 函數執行得非常漂亮。它為調用伺服器並實際運行搜索的另一個方法設置了一個超時。如果在設置之前還沒有超時,那麼將清除該超時。為什麼?因為這將允許用戶輸入大量的文本;然後,當用戶按下最後一個鍵時,第二種方法將運行搜索。通過這種方法,用戶將不再被不斷閃爍的顯示打擾。

回頁首

沒有檢查回調返回的結果

許多 Ajax 反模式源於對 XMLHTTPRequest 對象機制的誤解。我經常看到的一種情況就是,用戶沒有在回調中檢查對象的 readyStatestatus 欄位。請查看 清單 6 以理解我所說的含義。

清單 6. Antipat2_nocheck.html

<html><script> var req = null; function processReqChange() { var dobj = document.getElementById( 『htmlDiv『 ); dobj.innerHTML = req.responseText; } ... </code> <p>Everything looks okay. And on small requests, and on some browsers, it『s probably fine. But many requests are large enough to call several calls to the <code type="inline">onreadystatechange</code> handler before they finish. So, your callback might be working with incomplete data.</p> <p>The right way to do it is shown in <a href="#list7">Listing 7</a>.</p> <code type="section"> <heading refname="list7" type="code">Listing 7. Antipat2_fixed.html</heading> <html><script> var req = null; function processReqChange() { if (req.readyState == 4 && req.status == 200 ) { var dobj = document.getElementById( 『htmlDiv『 ); dobj.innerHTML = req.responseText; } } ...

要查看真實環境的演示,請查看在線版本

antipat2_nocheck.html.

沒有太多的代碼,並且它可以在所有瀏覽器上工作。

我注意到,和其他瀏覽器相比,這個問題在 Windows? Internet Explorer? 7 上尤為突出。Internet Explorer 7 對 onreadystatechange 進行多次回調 —— 我的意思是說即使對小的請求也多次進行回調。因此,需要正確編寫處理程序。

回頁首

在使用 HTML 更合適的時候卻傳送複雜的 XML

在我工作的一家公司中,所有的談論都是關於 「在網路的邊緣實現智能化」。關於這個簡單思想的另一個比較有趣的說法就是:通過在桌面上實現瀏覽器的智能化工作來替代伺服器中的全面處理。

但是在頁面中實現智能化意味著在其中使用大量的 JavaScript 代碼。這樣做有一個很大的弊端:瀏覽器兼容性。確實需要在每個流行的瀏覽器上測試 JavaScript 代碼中每個關鍵行 —— 或者至少,對客戶最可能使用的瀏覽器進行測試。所有這些都意味著大量的工作。以 清單 8 所示的複雜 Ajax 代碼為例。

清單 8. Antipat3_complex.html

<html><head><script> var req = null; function processReqChange() { if (req.readyState == 4 && req.status == 200 && req.responseXML ) { var dtable = document.getElementById( 『dataBody『 ); var nl = req.responseXML.getElementsByTagName( 『movie『 ); for( var i = 0; i < nl.length; i++ ) { var nli = nl.item( i ); var elYear = nli.getElementsByTagName( 『year『 ); var year = elYear.item(0).firstChild.nodeValue; var elTitle = nli.getElementsByTagName( 『title『 ); var title = elTitle.item(0).firstChild.nodeValue; var elTr = dtable.insertRow( -1 ); var elYearTd = elTr.insertCell( -1 ); elYearTd.innerHTML = year; var elTitleTd = elTr.insertCell( -1 ); elTitleTd.innerHTML = title; } } } function loadXMLDoc( url ) { if(window.XMLHttpRequest) { try { req = new XMLHttpRequest(); } catch(e) { req = false; } } else if(window.ActiveXObject) { try { req = new ActiveXObject(『Msxml2.XMLHTTP『); } catch(e) { try { req = new ActiveXObject(『Microsoft.XMLHTTP『); } catch(e) { req = false; } } } if(req) { req.onreadystatechange = processReqChange; req.open(『GET『, url, true); req.send(『『); } } var url = window.location.toString(); url = url.replace( /antipat3_complex.html/, 『antipat3_data.xml『 ); loadXMLDoc( url ); </script></head><body> <table cellspacing="0" cellpadding="3" ><tbody id="dataBody"> <tr> <th width_="20%">Year</th> <th width_="80%">Title</th> </tr> </tbody></table></body></html>

要查看真實環境的演示,請查看在線版本

antipat3_complex.html.

這段代碼從 清單 9 所示的 XML 文件中讀取數據,然後將它變為表格格式。

清單 9. Antipat3_data.xml

<movies> <movie> <year>1993</year> <title>Jurassic Park</title> </movie> <movie> <year>1997</year> <title>The Lost World: Jurassic Park</title> </movie> <movie> <year>2001</year> <title>Jurassic Park III</title> </movie> </movies>

可以看到如 圖 3 所示的結果。

圖 3. 複雜的電影清單頁面

這其實不是糟糕的代碼。只不過是用大量的代碼執行一個實際上相當簡單的任務。產生的頁面一點兒都不複雜。它不能在客戶端對頁面進行排序和搜索。事實上,幾乎沒有理由對 XML 和 HTML 進行複雜的轉換。

難道不能像 清單 10 那樣讓伺服器返回 HTML 而不是 XML,從而變得更簡單點兒嗎?

清單 10. Antipat3_fixed.html

<html><script> var req = null; function processReqChange() { if (req.readyState == 4 && req.status == 200 ) { var dobj = document.getElementById( 『tableDiv『 ); dobj.innerHTML = req.responseText; } } function loadUrl( url ) { ... } var url = window.location.toString(); url = url.replace( /antipat3_fixed.html/, 『antipat3_content.html『 ); loadUrl( url ); </script><body><div id="tableDiv"></div></body></html>

要查看真實環境的演示,請查看在線版本

antipat3_fixed.html.

事實上,這樣更加簡單。所有創建複雜錶行和單元格的代碼被替換為頁面中 <div> 標記的一組簡單的 innerHTML。 Voilà!

從伺服器返回的 HTML 如 清單 11 所示。

清單 11. Antipat3_content.html

<table cellspacing="0" cellpadding="3" > <tbody id="dataBody"> <tr> <th width_="20%">Year</th> <th width_="80%">Title</th> </tr> <tr> <td>1993</td> <td>Jurassic Park</td> </tr> <tr> <td>1997</td> <td>The Lost World: Jurassic Park</td> </tr> <tr> <td>2001</td> <td>Jurassic Park III</td> </tr> </tbody> </table>

要查看真實環境的演示,請查看在線版本

antipat3_content.html.

對於所有任務,選擇是在伺服器上處理,還是在客戶機上處理取決於任務的需求。本文的例子相當簡單:提供電影表。如果任務更複雜的話 —— 可能會進行分類、搜索、添加、刪除或動態交互(單擊電影名將出現更多信息)—— 那麼可以在客戶端使用更加複雜的代碼。事實上,在本文的結尾我將演示在客戶機上進行排序,從而反面論證在伺服器上施加大量負載的情形。

也許所有示例中最好的一個就是 Google Maps。Google Maps 執行了很好的任務 —— 將富客戶端的代碼與伺服器端的智能映射引擎結合了起來。我將使用這個服務作為例子,說明如何確定在哪裡執行什麼樣的處理。

回頁首

在應該傳送 JavaScript 代碼的時候卻傳送 XML

所有關於使 Web 瀏覽器讀取 XML 數據源並動態呈現它們的誇大其辭,可能讓您覺得這是惟一可用的方法。然而,這種想法是錯誤的,因為非常聰明的工程師已經使用過 Ajax 傳送技術來發送 JavaScript 代碼而不是 XML。請看 清單 12 所示的電影表示例。

清單 12. Antipat4_fixed.html

<html><head><script> var req = null; function processReqChange() { if (req.readyState == 4 && req.status == 200 ) { var dtable = document.getElementById( 『dataBody『 ); var movies = eval( req.responseText ); for( var i = 0; i < movies.length; i++ ) { var elTr = dtable.insertRow( -1 ); var elYearTd = elTr.insertCell( -1 ); elYearTd.innerHTML = movies[i].year; var elTitleTd = elTr.insertCell( -1 ); elTitleTd.innerHTML = movies[i].name; } } } function loadXMLDoc( url ) { ... } var url = window.location.toString(); url = url.replace( /antipat4_fixed.html/, 『antipat4_data.js『 ); loadXMLDoc( url ); </script></head><body> <table cellspacing="0" cellpadding="3" > <tbody id="dataBody"><tr> <th width_="20%">Year</th> <th width_="80%">Title</th> </tr></tbody></table></body></html>

要查看真實環境的演示,請查看在線版本

antipat4_fixed.html.

這個示例沒有從伺服器讀取 XML,它讀取的是 JavaScript 代碼。然後使用 JavaScript 代碼中的 eval() 函數獲取數據,然後再使用這些數據快速構建表。

清單 13 展示了 JavaScript 代碼。

清單 13. Antipat4_data.js

[ { year: 1993, name: 『Jurassic Park『 }, { year: 1997, name: 『The Lost World: Jurassic Park『 }, { year: 2001, name: 『Jurassic Park III『 } ]

這個功能要求伺服器使用 JavaScript 語言進行通信。不過這通常不是什麼大問題。大多數流行的 Web 語言已經支持 JavaScript Object Notation(JSON)輸出。

優勢是明顯的。在這個示例當中,通過使用 JavaScript 語言,下載到客戶機的數據減少了 52%。同樣,性能也得到了提升。讀取 JavaScript 代碼的速度快了 9%。9% 可能看上去不是很大,但是要記住這是個非常基礎的示例。更大的數據塊或者更複雜的結構需要更多 XML 解析代碼,而所需的 JavaScript 代碼數量不會變。

回頁首

伺服器負載過重

在伺服器上執行很少的任務的反面論證是在其上執行大量的操作。正如我在前面提到的,這是一個需要權衡的問題。但是,我想說明的是如何在客戶機上對電影表執行排序,從而為伺服器減輕負載。

清單 14 顯示了可排序的電影表。

清單 14. Antipat5_sort.html

<html><head><script> var req = null; var movies = null; function processReqChange() { if (req.readyState == 4 && req.status == 200 ) { movies = eval( req.responseText ); runSort( 『year『 ); } } function runSort( key ) { if ( key == 『name『 ) movies.sort( function( a, b ) { if ( a.name < b.name ) return -1; if ( a.name > b.name ) return 1; return 0; } ); else movies.sort( function( a, b ) { if ( a.year < b.year ) return -1; if ( a.year > b.year ) return 1; return 0; } ); var dtable = document.getElementById( 『dataBody『 ); while( dtable.rows.length > 1 ) dtable.deleteRow( 1 ); for( var i = 0; i < movies.length; i++ ) { var elTr = dtable.insertRow( -1 ); var elYearTd = elTr.insertCell( -1 ); elYearTd.innerHTML = movies[i].year; var elTitleTd = elTr.insertCell( -1 ); elTitleTd.innerHTML = movies[i].name; } } function loadXMLDoc( url ) { ... } var url = window.location.toString(); url = url.replace( /antipat5_sort.html/, 『antipat4_data.js『 ); loadXMLDoc( url ); </script></head><body> <table cellspacing="0" cellpadding="3" > <tbody id="dataBody"><tr> <th width_="20%"><a href="javascript: void runSort(『year『)">Year</a></th> <th width_="80%"><a href="javascript: void runSort(『name『)">Title</a></th> </tr></tbody></table></body></html>

要查看真實環境的演示,請查看在線版本

antipat5_sort.html.

這是一個相當簡單的示例。它無法處理那些很可能需要好幾頁顯示的特別長的列表。但是它確實說明了創建一個能夠快速排序的表非常簡單,並且無需刷新頁面,也不需要伺服器來執行麻煩無聊的排序工作。

回頁首

結束語

我針對 Ajax 編寫了大量文章,並做了大量 Ajax 工作,同時主持 IBM developerWorks Ajax 論壇,所以我了解一些關於 Ajax 的知識,以及其正確和錯誤的用法。最常見的情況就是開發人員低估了 Ajax 的複雜性,他們認為它只不過是向瀏覽器發送 XML、JavaScript 或 HTML 代碼而已。我將 Ajax 平台視作完整的瀏覽器;實際上,是完整的流行瀏覽器集,因為您必須了解所有這些瀏覽器的特殊要求。

所有這些都歸結到一點:有大量的有關 Ajax 的知識要學習,在這個過程中還會發生很多錯誤。我希望這篇文章能夠幫助您避免一些這樣的陷阱,或者在落入這樣的圈套後幫助您解決麻煩。總之,雖然可以從成功的經驗中學到很多知識,然而通常可以從錯誤中學到更多的東西。

你的讚賞是我堅持原創的動力

讚賞共 0 人讚賞
推薦閱讀:

AJAX速覽
Ajax簡單實例
Ajax 進行Post傳值和Get傳值

TAG:Ajax | 模式 |