反向ajax实现
在過(guò)去的幾年中,web開發(fā)已經(jīng)發(fā)生了很大的變化。現(xiàn)如今,我們期望的是能夠通過(guò)web快速、動(dòng)態(tài)地訪問(wèn)應(yīng)用。在這一新的文章系列中,我們學(xué)習(xí)如何使用反向Ajax(Reverse Ajax)技術(shù)來(lái)開發(fā)事件驅(qū)動(dòng)的web應(yīng)用,以此來(lái)實(shí)現(xiàn)更好的用戶體驗(yàn)。客戶端的例子使用的是JQuery JavaScript庫(kù),在這首篇文章中,我們探索不同的反向Ajax技術(shù),使用可下載的例子來(lái)學(xué)習(xí)使用了流(streaming)方法和長(zhǎng)輪詢(long polling)方法的Comet。
前言
web開發(fā)在過(guò)去的幾年中有了很大的進(jìn)展,我們已經(jīng)遠(yuǎn)超了把靜態(tài)網(wǎng)頁(yè)鏈接在一起的做法,這種做法會(huì)引起瀏覽器的刷新,并且要等待頁(yè)面的加載。現(xiàn)在需要的是能夠通過(guò)web來(lái)訪問(wèn)的完全動(dòng)態(tài)的應(yīng)用,這些應(yīng)用通常需要盡可能的快,提供近乎實(shí)時(shí)的組件。在這一新的由五部分組成的文章系列中,我們學(xué)習(xí)如何使用反向Ajax(Reverse Ajax)技術(shù)來(lái)開發(fā)事件驅(qū)動(dòng)的web應(yīng)用。
在這第一篇文章中,我們要了解反向Ajax、輪詢(polling)、流(streaming)、Comet和長(zhǎng)輪詢(long polling),學(xué)習(xí)如何實(shí)現(xiàn)不同的反向Ajax通信技術(shù),并探討每種方法的優(yōu)點(diǎn)和缺點(diǎn)。你可以下載本文中例子的相應(yīng)源代碼。
Ajax、反向Ajax和WebSocket
異步的JavaScript和XML(Asynchronous JavaScript and XML,Ajax),一種可通過(guò)JavaScript來(lái)訪問(wèn)的瀏覽器功能特性,其允許腳本向幕后的網(wǎng)站發(fā)送一個(gè)HTTP請(qǐng)求而又無(wú)需重新加載頁(yè)面。Ajax的出現(xiàn)已經(jīng)超過(guò)了十年,盡管其名字中包含了XML,但你幾乎可以在Ajax請(qǐng)求中傳送任何的東西,最常用的數(shù)據(jù)是JSON,其與JavaScript語(yǔ)法很接近,且消耗更少帶寬。清單1給出了這樣的一個(gè)例子,Ajax請(qǐng)求通過(guò)某個(gè)地方的郵政編碼來(lái)檢索該地的名稱。
清單1. Ajax請(qǐng)求舉例
var?url?='http://www.geonames.org/postalCodeLookupJSON?postalcode='+?$('#postalCode').val()?+'&country='
+?$('#country').val()?+'&callback=?';
$.getJSON(url,?function(data) {
$('#placeName').val(data.postalcodes[0].placeName);
});
在本文可下載的源代碼中,你可在listing1.html中看到這一例子的作用。
反向Ajax(Reverse Ajax)本質(zhì)上則是這樣的一種概念:能夠從服務(wù)器端向客戶端發(fā)送數(shù)據(jù)。在一個(gè)標(biāo)準(zhǔn)的HTTP Ajax請(qǐng)求中,數(shù)據(jù)是發(fā)送給服務(wù)器端的,反向Ajax可以某些特定的方式來(lái)模擬發(fā)出一個(gè)Ajax請(qǐng)求,這些方式本文都會(huì)論及,這樣的話,服務(wù)器就可以盡可能快地向客戶端發(fā)送事件(低延遲通信)。
WebSocket技術(shù)來(lái)自HTML5,是一種最近才出現(xiàn)的技術(shù),許多瀏覽器已經(jīng)支持它(Firefox、Google Chrome、Safari等等)。WebSocket啟用雙向的、全雙工的通信信道,其通過(guò)某種被稱為WebSocket握手的HTTP請(qǐng)求來(lái)打開連接,并用到了一些特殊的報(bào)頭。連接保持在活動(dòng)狀態(tài),你可以用JavaScript來(lái)寫和接收數(shù)據(jù),就像是正在用一個(gè)原始的TCP套接口一樣。WebSocket會(huì)在這一文章系列的第二部分中談及。
反向Ajax技術(shù)
反向Ajax的目的是允許服務(wù)器端向客戶端推送信息。Ajax請(qǐng)求在缺省情況下是無(wú)狀態(tài)的,且只能從客戶端向服務(wù)器端發(fā)出請(qǐng)求。你可以通過(guò)使用技術(shù)模擬服務(wù)器端和客戶端之間的響應(yīng)式通信來(lái)繞過(guò)這一限制。
HTTP輪詢和JSONP輪詢
輪詢(polling)涉及了從客戶端向服務(wù)器端發(fā)出請(qǐng)求以獲取一些數(shù)據(jù),這顯然就是一個(gè)純粹的Ajax HTTP請(qǐng)求。為了盡快地獲得服務(wù)器端事件,輪詢的間隔(兩次請(qǐng)求相隔的時(shí)間)必須盡可能地小。但有這樣的一個(gè)缺點(diǎn)存在:如果間隔減小的話,客戶端瀏覽器就會(huì)發(fā)出更多的請(qǐng)求,這些請(qǐng)求中的許多都不會(huì)返回任何有用的數(shù)據(jù),而這將會(huì)白白地浪費(fèi)掉帶寬和處理資源。
圖1中的時(shí)間線說(shuō)明了客戶端發(fā)出了某些輪詢請(qǐng)求,但沒有信息返回這種情況,客戶端必須要等到下一個(gè)輪詢來(lái)獲取兩個(gè)服務(wù)器端接收到的事件。
圖1. 使用HTTP輪詢的反向Ajax
JSONP輪詢基本上與HTTP輪詢一樣,不同之處則是JSONP可以發(fā)出跨域請(qǐng)求(不是在你的域內(nèi)的請(qǐng)求)。清單1使用JSONP來(lái)通過(guò)郵政編碼獲取地名,JSONP請(qǐng)求通常可通過(guò)它的回調(diào)參數(shù)和返回內(nèi)容識(shí)別出來(lái),這些內(nèi)容是可執(zhí)行的JavaScript代碼。
要在JavaScript中實(shí)現(xiàn)輪詢的話,你可以使用setInterval來(lái)定期地發(fā)出Ajax請(qǐng)求,如清單2所示:
清單2. JavaScript輪詢
setInterval(function() {$.getJSON('events',?function(events) {
console.log(events);
});
},?2000);
文章源代碼中的輪詢演示給出了輪詢方法所消耗的帶寬,間隔很小,但可以看到有些請(qǐng)求并未返回事件,清單3給出了這一輪詢示例的輸出。
清單3. 輪詢演示例子的輸出
[client]?checking for events...[client]?no event
[client]?checking for events...
[client]2?events
[event]?At Sun Jun?0515:17:14?EDT?2011
[event]?At Sun Jun?0515:17:14?EDT?2011
[client]?checking for events...
[client]1?events
[event]?At Sun Jun?0515:17:16?EDT?2011
用JavaScript實(shí)現(xiàn)的輪詢的優(yōu)點(diǎn)和缺點(diǎn):
1. 優(yōu)點(diǎn):很容易實(shí)現(xiàn),不需要任何服務(wù)器端的特定功能,且在所有的瀏覽器上都能工作。
2. 缺點(diǎn):這種方法很少被用到,因?yàn)樗峭耆痪呱炜s性的。試想一下,在100個(gè)客戶端每個(gè)都發(fā)出2秒鐘的輪詢請(qǐng)求的情況下,所損失的帶寬和資源數(shù)量,在這種情況下30%的請(qǐng)求沒有返回?cái)?shù)據(jù)。
Piggyback
捎帶輪詢(piggyback polling)是一種比輪詢更加聰明的做法,因?yàn)樗鼤?huì)刪除掉所有非必需的請(qǐng)求(沒有返回?cái)?shù)據(jù)的那些)。不存在時(shí)間間隔,客戶端在需要的時(shí)候向服務(wù)器端發(fā)送請(qǐng)求。不同之處在于響應(yīng)的那部分上,響應(yīng)被分成兩個(gè)部分:對(duì)請(qǐng)求數(shù)據(jù)的響應(yīng)和對(duì)服務(wù)器事件的響應(yīng),如果任何一部分有發(fā)生的話。圖2給出了一個(gè)例子。
圖2. 使用了piggyback輪詢的反向Ajax
在實(shí)現(xiàn)piggyback技術(shù)時(shí),通常針對(duì)服務(wù)器端的所有Ajax請(qǐng)求可能會(huì)返回一個(gè)混合的響應(yīng),文章的下載中有一個(gè)實(shí)現(xiàn)示例,如下面的清單4所示。
清單4. piggyback代碼示例
$('#submit').click(function() {$.post('ajax',?function(data) {
var?valid?=?data.formValid;
//?處理驗(yàn)證結(jié)果
//?然后處理響應(yīng)的其他部分(事件)
processEvents(data.events);
});
});
清單5給出了一些piggyback輸出。
清單5. piggyback輸出示例
[client]?checking for events...[server]?form valid ? true
[client]4?events
[event]?At Sun Jun?0516:08:32?EDT?2011
[event]?At Sun Jun?0516:08:34?EDT?2011
[event]?At Sun Jun?0516:08:34?EDT?2011
[event]?At Sun Jun?0516:08:37?EDT?2011
你可以看到表單驗(yàn)證的結(jié)果和附加到響應(yīng)上的事件,同樣,這種方法也有著一些優(yōu)點(diǎn)和缺點(diǎn):
1. 優(yōu)點(diǎn):沒有不返回?cái)?shù)據(jù)的請(qǐng)求,因?yàn)榭蛻舳藢?duì)何時(shí)發(fā)送請(qǐng)求做了控制,對(duì)資源的消耗較少。該方法也是可用在所有的瀏覽器上,不需要服務(wù)器端的特殊功能。
2. 缺點(diǎn):當(dāng)累積在服務(wù)器端的事件需要傳送給客戶端時(shí),你卻一點(diǎn)都不知道,因?yàn)檫@需要一個(gè)客戶端行為來(lái)請(qǐng)求它們。
Comet
使用了輪詢或是捎帶的反向Ajax非常受限:其不具伸縮性,不提供低延遲通信(只要事件一到達(dá)服務(wù)器端,它們就以盡可能快的速度到達(dá)瀏覽器端)。Comet是一個(gè)web應(yīng)用模型,在該模型中,請(qǐng)求被發(fā)送到服務(wù)器端并保持一個(gè)很長(zhǎng)的存活期,直到超時(shí)或是有服務(wù)器端事件發(fā)生。在該請(qǐng)求完成后,另一個(gè)長(zhǎng)生存期的Ajax請(qǐng)求就被送去等待另一個(gè)服務(wù)器端事件。使用Comet的話,web服務(wù)器就可以在無(wú)需顯式請(qǐng)求的情況下向客戶端發(fā)送數(shù)據(jù)。
Comet的一大優(yōu)點(diǎn)是,每個(gè)客戶端始終都有一個(gè)向服務(wù)器端打開的通信鏈路。服務(wù)器端可以通過(guò)在事件到來(lái)時(shí)立即提交(完成)響應(yīng)來(lái)把事件推給客戶端,或者它甚至可以累積再連續(xù)發(fā)送。因?yàn)檎?qǐng)求長(zhǎng)時(shí)間保持打開的狀態(tài),故服務(wù)器端需要特別的功能來(lái)處理所有的這些長(zhǎng)生存期請(qǐng)求。圖3給出了一個(gè)例子。(這一文章系列的第2部分會(huì)更加詳細(xì)地解釋服務(wù)器端的約束條件)。
圖3. 使用Comet的反向Ajax
Comet的實(shí)現(xiàn)可以分成兩類:使用流(streaming)的那些和使用長(zhǎng)輪詢(long polling)的那些。
使用HTTP流的Comet
在流(streaming)模式中,有一個(gè)持久連接會(huì)被打開。只會(huì)存在一個(gè)長(zhǎng)生存期請(qǐng)求(圖3中的#1),因?yàn)槊總€(gè)到達(dá)服務(wù)器端的事件都會(huì)通過(guò)這同一連接來(lái)發(fā)送。因此,客戶端需要有一種方法來(lái)把通過(guò)這同一連接發(fā)送過(guò)來(lái)的不同響應(yīng)分隔開來(lái)。從技術(shù)上來(lái)講,兩種常見的流技術(shù)包括Forever Iframe(隱藏的IFrame),或是被用來(lái)在JavaScript中創(chuàng)建Ajax請(qǐng)求的XMLHttpRequest對(duì)象的多部分(multi-part)特性。
Forever Iframe
Forever Iframe(永存的Iframe)技術(shù)涉及了一個(gè)置于頁(yè)面中的隱藏Iframe標(biāo)簽,該標(biāo)簽的src屬性指向返回服務(wù)器端事件的servlet路徑。每次在事件到達(dá)時(shí),servlet寫入并刷新一個(gè)新的script標(biāo)簽,該標(biāo)簽內(nèi)部帶有JavaScript代碼,iframe的內(nèi)容被附加上這一script標(biāo)簽,標(biāo)簽中的內(nèi)容就會(huì)得到執(zhí)行。
1. 優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,在所有支持iframe的瀏覽器上都可用。
2. 缺點(diǎn): 沒有方法可用來(lái)實(shí)現(xiàn)可靠的錯(cuò)誤處理或是跟蹤連接的狀態(tài),因?yàn)樗械倪B接和數(shù)據(jù)都是由瀏覽器通過(guò)HTML標(biāo)簽來(lái)處理的,因此你沒有辦法知道連接何時(shí)在哪一端已被斷開了。
Multi-part XMLHttpRequest
第二種技術(shù),更可靠一些,是XMLHttpRequest對(duì)象上使用某些瀏覽器(比如說(shuō)Firefox)支持的multi-part標(biāo)志。Ajax請(qǐng)求被發(fā)送給服務(wù)器端并保持打開狀態(tài),每次有事件到來(lái)時(shí),一個(gè)多部分的響應(yīng)就會(huì)通過(guò)這同一連接來(lái)寫入,清單6給出了一個(gè)例子。
清單6. 設(shè)置Multi-part XMLHttpRequest的JavaScript代碼示例
var?xhr?=?$.ajaxSettings.xhr();xhr.multipart?=true;
xhr.open('GET',?'ajax',?true);
xhr.onreadystatechange?=?function() {
if?(xhr.readyState?==?4) {
processEvents($.parseJSON(xhr.responseText));
}
};
xhr.send(null);
在服務(wù)器端,事情要稍加復(fù)雜一些。首先你必須要設(shè)置多部分請(qǐng)求,然后掛起連接。清單7展示了如何掛起一個(gè)HTTP流請(qǐng)求。(這一系列的第3部分會(huì)更加詳細(xì)地談及這些API。)
清單7. 使用Servlet 3 API來(lái)在servlet中掛起一個(gè)HTTP流請(qǐng)求
protected?void?doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {
//?開始請(qǐng)求的掛起
AsyncContext asyncContext?=?req.startAsync();
asyncContext.setTimeout(0);
//?給客戶端發(fā)回多部分的分隔符
resp.setContentType("multipart/x-mixed-replace;boundary=\""
+?boundary?+"\"");
resp.setHeader("Connection",?"keep-alive");
resp.getOutputStream().print("--"+?boundary);
resp.flushBuffer();
//?把異步上下文放在列表中以被將來(lái)只用
asyncContexts.offer(asyncContext);
}
現(xiàn)在,每次有事件發(fā)生時(shí)你都可以遍歷所有的掛起連接并向它們寫入數(shù)據(jù),如清單8所示:
清單8. 使用Servlet 3 API來(lái)向掛起的多部分請(qǐng)求發(fā)送事件
for?(AsyncContext asyncContext : asyncContexts) {HttpServletResponse peer?=?(HttpServletResponse)
asyncContext.getResponse();
peer.getOutputStream().println("Content-Type: application/json");
peer.getOutputStream().println();
peer.getOutputStream().println(new?JSONArray()
.put("At?"+new?Date()).toString());
peer.getOutputStream().println("--"+?boundary);
peer.flushBuffer();
}
本文可下載文件的Comet-straming文件夾中的部分說(shuō)明了HTTP流,在運(yùn)行例子并打開主頁(yè)時(shí),你會(huì)看到只要事件一到達(dá)服務(wù)器端,雖然不同步但它們幾乎立刻會(huì)出現(xiàn)在頁(yè)面上。而且,如果打開Firebug控制臺(tái)的話,你就能看到只有一個(gè)Ajax請(qǐng)求是打開的。如果再往下看一些,你會(huì)看到JSON響應(yīng)被附在Response選項(xiàng)卡中,如圖4所示:
圖4. HTTP流請(qǐng)求的FireBug視圖
照例,做法存在著一些優(yōu)點(diǎn)和缺點(diǎn):
1. 優(yōu)點(diǎn):只打開了一個(gè)持久連接,這就是節(jié)省了大部分帶寬使用率的Comet技術(shù)。
2. 缺點(diǎn):并非所有的瀏覽器都支持multi-part標(biāo)志。某些被廣泛使用的庫(kù),比如說(shuō)用Java實(shí)現(xiàn)的CometD,被報(bào)告在緩沖方面有問(wèn)題。例如,一些數(shù)據(jù)塊(多個(gè)部分)可能被緩沖,然后只有在連接完成或是緩沖區(qū)已滿時(shí)才被發(fā)送,而這有可能會(huì)帶來(lái)比預(yù)期要高的延遲。
使用HTTP長(zhǎng)輪詢的Comet
長(zhǎng)輪詢(long polling)模式涉及了打開連接的技術(shù)。連接由服務(wù)器端保持著打開的狀態(tài),只要一有事件發(fā)生,響應(yīng)就會(huì)被提交,然后連接關(guān)閉。接下來(lái)。一個(gè)新的長(zhǎng)輪詢連接就會(huì)被正在等待新事件到達(dá)的客戶端重新打開。
你可以使用script標(biāo)簽或是單純的XMLHttpRequest對(duì)象來(lái)實(shí)現(xiàn)HTTP長(zhǎng)輪詢。
script標(biāo)簽
正如iframe一樣,其目標(biāo)是把script標(biāo)簽附加到頁(yè)面上以讓腳本執(zhí)行。服務(wù)器端則會(huì):掛起連接直到有事件發(fā)生,接著把腳本內(nèi)容發(fā)送回瀏覽器,然后重新打開另一個(gè)script標(biāo)簽來(lái)獲取下一個(gè)事件。
1. 優(yōu)點(diǎn):因?yàn)槭腔贖TML標(biāo)簽的,所有這一技術(shù)非常容易實(shí)現(xiàn),且可跨域工作(缺省情況下,XMLHttpRequest不允許向其他域或是子域發(fā)送請(qǐng)求)。
2. 缺點(diǎn):類似于iframe技術(shù),錯(cuò)誤處理缺失,你不能獲得連接的狀態(tài)或是有干涉連接的能力。
XMLHttpRequest長(zhǎng)輪詢
第二種,也是一種推薦的實(shí)現(xiàn)Comet的做法是打開一個(gè)到服務(wù)器端的Ajax請(qǐng)求然后等待響應(yīng)。服務(wù)器端需要一些特定的功能來(lái)允許請(qǐng)求被掛起,只要一有事件發(fā)生,服務(wù)器端就會(huì)在掛起的請(qǐng)求中送回響應(yīng)并關(guān)閉該請(qǐng)求,完全就像是你關(guān)閉了servlet響應(yīng)的輸出流。然后客戶端就會(huì)使用這一響應(yīng)并打開一個(gè)新的到服務(wù)器端的長(zhǎng)生存期的Ajax請(qǐng)求,如清單9所示:
清單9. 設(shè)置長(zhǎng)輪詢請(qǐng)求的JavaScript代碼示例
function?long_polling() {$.getJSON('ajax',?function(events) {
processEvents(events);
long_polling();
});
}
long_polling();
在后端,代碼也是使用Servlet 3 API來(lái)掛起請(qǐng)求,正如HTTP流的做法一樣,但你不需要所有的多部分處理代碼,清單10給出了一個(gè)例子。
清單10. 掛起一個(gè)長(zhǎng)輪詢Ajax請(qǐng)求
protected?void?doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {
AsyncContext asyncContext?=?req.startAsync();
asyncContext.setTimeout(0);
asyncContexts.offer(asyncContext);
}
在接收到事件時(shí),只是取出所有的掛起請(qǐng)求并完成它們,如清單11所示:
清單11. 在有事件發(fā)生時(shí)完成長(zhǎng)輪詢Ajax請(qǐng)求
while?(!asyncContexts.isEmpty()) {AsyncContext asyncContext?=?asyncContexts.poll();
HttpServletResponse peer?=?(HttpServletResponse)
asyncContext.getResponse();
peer.getWriter().write(
new?JSONArray().put("At?"?+?new?Date()).toString());
peer.setStatus(HttpServletResponse.SC_OK);
peer.setContentType("application/json");
asyncContext.complete();
}
在附帶的下載源文件中,comet-long-polling文件夾包含了一個(gè)長(zhǎng)輪詢示例web應(yīng)用,你可以使用 mvn jetty:run 命令來(lái)運(yùn)行它。
1. 優(yōu)點(diǎn):客戶端很容易實(shí)現(xiàn)良好的錯(cuò)誤處理系統(tǒng)和超時(shí)管理。這一可靠的技術(shù)還允許在與服務(wù)器端的連接之間有一個(gè)往返,即使連接是非持久的(當(dāng)你的應(yīng)用有許多的客戶端時(shí),這是一件好事)。它可用在所有的瀏覽器上;你只需要確保所用的XMLHttpRequest對(duì)象發(fā)送到的簡(jiǎn)單的Ajax請(qǐng)求就可以了。
2. 缺點(diǎn):相比于其他技術(shù)來(lái)說(shuō),不存在什么重要的缺點(diǎn),像所有我們已經(jīng)討論過(guò)的技術(shù)一樣,該方法依然依賴于無(wú)狀態(tài)的HTTP連接,其要求服務(wù)器端有特殊的功能來(lái)臨時(shí)掛起連接。
建議
因?yàn)樗鞋F(xiàn)代的瀏覽器都支持跨域資源共享(Cross-Origin Resource Share,CORS)規(guī)范,該規(guī)范允許XHR執(zhí)行跨域請(qǐng)求,因此基于腳本的和基于iframe的技術(shù)已成為了一種過(guò)時(shí)的需要。
把Comet做為反向Ajax的實(shí)現(xiàn)和使用的最好方式是通過(guò)XMLHttpRequest對(duì)象,該做法提供了一個(gè)真正的連接句柄和錯(cuò)誤處理。考慮到不是所有的瀏覽器都支持multi-part標(biāo)志,且多部分流可能會(huì)遇到緩沖問(wèn)題,因此建議你選擇經(jīng)由HTTP長(zhǎng)輪詢使用XMLHttpRequest對(duì)象(在服務(wù)器端掛起的一個(gè)簡(jiǎn)單的Ajax請(qǐng)求)的Comet模式,所有支持Ajax的瀏覽器也都支持該種做法。
結(jié)論
本文提供的是反向Ajax技術(shù)的一個(gè)入門級(jí)介紹,文章探索了實(shí)現(xiàn)反向Ajax通信的不同方法,并說(shuō)明了每種實(shí)現(xiàn)的優(yōu)勢(shì)和弊端。你的具體情況和應(yīng)用需求將會(huì)影響到你對(duì)最合適方法的選擇。不過(guò)一般來(lái)說(shuō),如果你想要在低延遲通信、超時(shí)和錯(cuò)誤檢測(cè)、簡(jiǎn)易性,以及所有瀏覽器和平臺(tái)的良好支持這幾方面有一個(gè)最好的折中的話,那就選擇使用了Ajax長(zhǎng)輪詢請(qǐng)求的Comet。
請(qǐng)繼續(xù)閱讀這一系列的第2部分:該部分將會(huì)探討第三種反向Ajax技術(shù):WebSocket。盡管還不是所有的瀏覽器都支持該技術(shù),但WebSocket肯定是一種非常好的反向Ajax通信媒介,WebSocket消除了所有與HTTP連接的無(wú)狀態(tài)特性相關(guān)的限制。第2部分還會(huì)談及由Comet和WebSocket技術(shù)帶來(lái)的服務(wù)器端約束。
代碼下載
reverse_ajaxpt1_source.zip
總結(jié)