了解如何使用 Asynchronous JavaScript? + XML (Ajax) 和 PHP 在 Web 應(yīng)用程序中建立聊天系統(tǒng)。您的客戶不需要下載或安裝任何專(zhuān)門(mén)的即時(shí)消息通訊軟件,就能和您及其他客戶討論網(wǎng)站的內(nèi)容。<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --><!--END RESERVED FOR FUTURE USE INCLUDE FILES-->
![]() |
|
Web 2.0 一詞出現(xiàn)以來(lái),開(kāi)發(fā)人員都在說(shuō)社區(qū)。不論您是否認(rèn)為這有點(diǎn)夸大其辭,但讓用戶或讀者能夠方便地實(shí)時(shí)討論頁(yè)面主題或者銷(xiāo)售的產(chǎn)品,這一想法還是很吸引人的。但是怎么辦呢?能否在推銷(xiāo)產(chǎn)品的頁(yè)面中加入聊天,而不必讓客戶安裝任何特殊的軟件包括 Adobe Flash Player 呢?當(dāng)然!實(shí)踐證明,用免費(fèi)的現(xiàn)成工具如 PHP、MySQL、動(dòng)態(tài) HTML (DHTML)、Ajax 和 Prototype.js 庫(kù)就能完全做到。
不再羅嗦了,讓我們立即開(kāi)始吧。
聊天首先要有一個(gè)身份標(biāo)識(shí)。這就需要一個(gè)簡(jiǎn)單的登錄頁(yè),如 清單 1 所示。
<html> <head><title>Chat Login</title></head> <body> <form action="chat.php" method="post"> Username: <input type="text" name="username"> <input type="submit" value="Login"> </form> </body> </html> |
該頁(yè)的顯示結(jié)果如 圖 1 所示。
注意: 該例中需要登錄窗口是因?yàn)槲蚁M勒l(shuí)在說(shuō)話。對(duì)于您的應(yīng)用程序,可能已經(jīng)存在一個(gè)登錄頁(yè)面,使用自己已有的用戶名即可。
![]() ![]() |
![]()
|
聊天系統(tǒng)實(shí)質(zhì)上就是一個(gè)字符串表格,每個(gè)字符串屬于一個(gè)發(fā)言者。最簡(jiǎn)單的模式如 清單 2 所示。
DROP TABLE IF EXISTS messages; CREATE TABLE messages ( message_id INTEGER NOT NULL AUTO_INCREMENT, username VARCHAR(255) NOT NULL, message TEXT, PRIMARY KEY ( message_id ) ); |
腳本中包含自動(dòng)增加的消息 ID、用戶名和消息本身。如果需要,還可以向每條消息增加時(shí)間戳以記錄發(fā)送的時(shí)間。
如果需要管理不同話題的多個(gè)會(huì)話,還需要建立一個(gè)表記錄不同的話題,并在消息表中增加相關(guān)的
topic_id
。為了盡量簡(jiǎn)化例子,我采用了最簡(jiǎn)單的模式。
建立數(shù)據(jù)庫(kù)和加載模式使用了下列命令:
% mysqladmin create chat % mysql chat < chat.sql |
根據(jù) MySQL 服務(wù)器的設(shè)置及其安全設(shè)定和口令,命令可能略有不同。
最基本的聊天用戶界面(UI)如 清單 3 所示。
<?php if ( array_key_exists( 'username', $_POST ) ) { $_SESSION['user'] = $_POST['username']; } $user = $_SESSION['user']; ?> <html> <head><title><?php echo( $user ) ?> - Chatting</title> <script src="prototype.js"></script> </head> <body> <div id="chat" style="height:400px;overflow:auto;"> </div> <script> function addmessage() { new Ajax.Updater( 'chat', 'add.php', { method: 'post', parameters: $('chatmessage').serialize(), onSuccess: function() { $('messagetext').value = ''; } } ); } </script> <form id="chatmessage"> <textarea name="message" id="messagetext"> </textarea> </form> <button onclick="addmessage()">Add</button> <script> function getMessages() { new Ajax.Updater( 'chat', 'messages.php', { onSuccess: function() { window.setTimeout( getMessages, 1000 ); } } ); } getMessages(); </script> </body> </html> |
在腳本的開(kāi)始部分中,您可從登錄頁(yè)面提交的參數(shù)中獲取用戶名并存儲(chǔ)在會(huì)話中。然后加載 Prototype.js JavaScript 庫(kù),它可以完成所有 Ajax 處理。
然后頁(yè)面提供了存放消息的位置。該區(qū)域由文件后面的
getMessages()
JavaScript 函數(shù)填寫(xiě)。
消息區(qū)域的下面是一個(gè)表單和用戶輸入消息文本的
textarea
。還有一個(gè)按鈕
Add
添加聊天消息。
頁(yè)面如 圖 2 所示。
請(qǐng)注意
getMessages()
函數(shù),頁(yè)面實(shí)際上每 1000 毫秒(1 秒)輪詢一次服務(wù)器,檢查是否有新消息,并把結(jié)果輸出到頁(yè)面上方的消息區(qū)域。本文
后面
還要詳細(xì)介紹輪詢,我想首先完成基本的實(shí)現(xiàn),messages.php 頁(yè)面返回當(dāng)前的消息列表。該頁(yè)如
清單 4
所示。
<table> <?php // Install the DB module using 'pear install DB' require_once 'DB.php'; $db =& DB::Connect( 'mysql://root@localhost/chat', array() ); if (PEAR::isError($db)) { die($db->getMessage()); } $res = $db->query('SELECT * FROM messages' ); while( $res->fetchInto( $row ) ) { ?> <tr><td><?php echo($row[1]) ?></td> <td><?php echo($row[2]) ?></td></tr> <?php } ?> </table> |
腳本的一開(kāi)始用 DB 庫(kù)連接到數(shù)據(jù)庫(kù),這個(gè)庫(kù)可從 PEAR 下載(請(qǐng)參閱 參考資料 )。如果還沒(méi)有安裝這個(gè)庫(kù),可通過(guò)下面的命令完成:
% pear install DB |
PEAR 安裝后,腳本可以查詢當(dāng)前的消息,檢索每一行,輸出用戶名和消息文本。
最后還有 add.php 腳本,從頁(yè)面上
addmessage()
函數(shù)的 Prototype.js Ajax 代碼中調(diào)用。該腳本從會(huì)話中取得消息文本和用戶名,然后在消息表中插入新的一行。代碼如
清單 5
所示。
<?php require_once("DB.php"); $db =& DB::Connect( 'mysql://root@localhost/chat', array() ); if (PEAR::isError($db)) { die($db->getMessage()); } $sth = $db->prepare( 'INSERT INTO messages VALUES ( null, ?, ? )' ); $db->execute( $sth, array( $_SESSION['user'], $_POST['message'] ) ); ?> <table> <?php $res = $db->query('SELECT * FROM messages' ); while( $res->fetchInto( $row ) ) { ?> <tr><td><?php echo($row[1]) ?></td> <td><?php echo($row[2]) ?></td></tr> <?php } ?> </table> |
add.php 腳本還返回當(dāng)前的消息列表,因?yàn)樵?yè)面中的 Ajax 代碼要從返回的 HTML 代碼更新聊天記錄。這樣用戶就能馬上看到添加到會(huì)話中的文本。
聊天系統(tǒng)的基本結(jié)構(gòu)就是這些。下一節(jié)說(shuō)明如何改進(jìn)輪詢的效率。
![]() ![]() |
![]()
|
這個(gè)原始的聊天系統(tǒng)中,頁(yè)面每秒請(qǐng)求一次對(duì)話的所有聊天記錄。雖然對(duì)于較短的對(duì)話影響不大,但是如果對(duì)話很長(zhǎng),性能問(wèn)題就顯現(xiàn)出來(lái)了。所幸的是解決起來(lái)很簡(jiǎn)單。每條消息都有
message_id
,這個(gè)數(shù)字自動(dòng)遞增。因此,如果知道已經(jīng)有了屬于某個(gè) ID 的消息,只需要請(qǐng)求出現(xiàn)在此 ID 之后的消息就可以。這樣可以大大降低消息傳遞的數(shù)量。多數(shù)請(qǐng)求很可能沒(méi)有新的消息,傳遞的包就會(huì)變小。
采用效率更高的設(shè)計(jì)需要稍微修改 chat.php 頁(yè)面,如 清單 6 所示。
<?php if ( array_key_exists( 'username', $_POST ) ) { $_SESSION['user'] = $_POST['username']; } $user = $_SESSION['user']; ?> <html> <head><title><?php echo( $user ) ?> - Chatting</title> <script src="prototype.js"></script> </head> <body> <div style="height:400px;overflow:auto;"> <table id="chat"> </table> </div> <script> function addmessage() { new Ajax.Request( 'add.php', { method: 'post', parameters: $('chatmessage').serialize(), onSuccess: function( transport ) { $('messagetext').value = ''; } } ); } </script> <form id="chatmessage"> <textarea name="message" id="messagetext"> </textarea> </form> <button onclick="addmessage()">Add</button> <script> var lastid = 0; function getMessages() { new Ajax.Request( 'messages.php?id='+lastid, { onSuccess: function( transport ) { var messages = transport.responseXML.getElementsByTagName( 'message' ); for( var i = 0; i < messages.length; i++ ) { var message = messages[i].firstChild.nodeValue; var user = messages[i].getAttribute('user'); var id = parseInt( messages[i].getAttribute('id') ); if ( id > lastid ) { var elTR = $('chat').insertRow( -1 ); var elTD1 = elTR.insertCell( -1 ); elTD1.appendChild( document.createTextNode( user ) ); var elTD2 = elTR.insertCell( -1 ); elTD2.appendChild( document.createTextNode( message ) ); lastid = id; } } window.setTimeout( getMessages, 1000 ); } } ); } getMessages(); </script> </body> </html> |
不再用 “chat”
<div>
標(biāo)記包含所有的消息,現(xiàn)在改為
<table>
標(biāo)記,收到新消息的時(shí)候動(dòng)態(tài)地追加一行。可以看到
getMessages()
函數(shù)中的相應(yīng)變化,和第一個(gè)版本相比長(zhǎng)了一些。
新版本的
getMessages()
預(yù)期 messages.php 頁(yè)面的結(jié)果是包含新消息的 XML 塊。messages.php 增加了一個(gè)參數(shù)
id
,即頁(yè)面顯示的最后一條消息的
message_id
。一開(kāi)始 ID 為 0,因而 messages.php 頁(yè)面返回所有的消息。此后則發(fā)送到目前為止顯示過(guò)的最后一條消息的 ID。
XML 響應(yīng)用
onSuccess
處理程序分解成元素,每個(gè)元素使用標(biāo)準(zhǔn) DHTML 文檔對(duì)象模型(DOM)函數(shù)添加到表格中,如
insertRow()
、
insertCell()
和
appendChild()
。
修改后的 messages.php 文件返回 XML 而不是 HTML,如 清單 7 所示。
<?php require_once("DB.php"); header( 'Content-type: text/xml' ); $id = 0; if ( array_key_exists( 'id', $_GET ) ) { $id = $_GET['id']; } $db =& DB::Connect( 'mysql://root@localhost/chat', array() ); if (PEAR::isError($db)) { die($db->getMessage()); } ?> <messages> <?php $res = $db->query( 'SELECT * FROM messages WHERE message_id > ?', $id ); while( $res->fetchInto( $row ) ) { ?> <message id="<?php echo($row[0]) ?>" user="<?php echo($row[1]) ?>"> <?php echo($row[2]) ?> </message> <?php } ?> </messages> |
圖 3 顯示了新的改進(jìn)后的版本。
圖 3. 經(jīng)過(guò)優(yōu)化的聊天窗口
從外觀上來(lái)說(shuō)沒(méi)有什么改變。但是和原來(lái)的相比效率要高得多。
![]() ![]() |
![]()
|
如果剛接觸 Ajax 或者僅對(duì)該領(lǐng)域有所了解,“輪詢” 的概念可能讓您感到害怕。不幸的是,輪詢是惟一的辦法。要在客戶機(jī)和服務(wù)器之間建立連續(xù)管道,同時(shí)又不需要在兩端安裝特定軟件,尚不存在可實(shí)現(xiàn)此目的的跨平臺(tái)、跨瀏覽器方法。即便這樣,可能還需要對(duì)防火墻進(jìn)行專(zhuān)門(mén)配置才行得通。因此,如果需要人人能用的一種簡(jiǎn)便辦法,Ajax 和輪詢是惟一的可能。
但是不斷宣傳和鼓吹的 “實(shí)時(shí)” 在哪兒呢?輪詢不可能是實(shí)時(shí)的。真的如此嗎?我認(rèn)為這取決于您對(duì) 實(shí)時(shí) 的定義。我過(guò)去編寫(xiě)電生理學(xué)數(shù)據(jù)檢索代碼時(shí), 實(shí)時(shí) 意味著毫秒。我相信地質(zhì)學(xué)家在某些情況下把分、日甚至年看作是 實(shí)時(shí) 。
如果查閱 Wikipedia,即會(huì)發(fā)現(xiàn)人類(lèi)的平均反應(yīng)時(shí)間大約在 200 到 270 毫秒之間。也就是擊一次球的時(shí)間。閱讀一條消息并形成答復(fù)的時(shí)間要長(zhǎng)得多,即使您非常投入。因此,等待聊天消息時(shí),200 毫秒左右(可能再長(zhǎng)一點(diǎn))的時(shí)間應(yīng)該足夠了。我設(shè)置為 1 秒,而且沒(méi)有感覺(jué)到不舒服。
作為 developerWorks Ajax 論壇(請(qǐng)參閱 參考資料 )的主持人, 輪詢 和 實(shí)時(shí) 的問(wèn)題每月至少遇到一次。我希望對(duì)于 Ajax 來(lái)說(shuō)已經(jīng)揭穿了輪詢和所謂 實(shí)時(shí) 的面具。建議在考慮某種極其復(fù)雜的實(shí)時(shí)解決方案之前嘗試一下輪詢。這樣至少可以知道嘗試自定義的解決方案之前使用現(xiàn)成的工具能夠做什么。
![]() ![]() |
![]()
|
希望本文為您提供了一個(gè)不錯(cuò)的起點(diǎn),以此為基礎(chǔ)在您的應(yīng)用程序中實(shí)現(xiàn)自己的聊天系統(tǒng)。下面是一些建議:
- 記錄用戶: 在聊天窗口的旁邊列出目前參加會(huì)談的人員。這樣可以告訴人們誰(shuí)參加了談話,什么時(shí)候來(lái)的,什么時(shí)候退出的。
- 允許多個(gè)會(huì)談: 允許多個(gè)關(guān)于不同話題的談話同時(shí)進(jìn)行。
-
支持表情字符:
將
:-)
這樣的字符組合翻譯成適當(dāng)?shù)男δ槇D像。 - 使用 URL 解析: 在客戶端 JavaScript 代碼中使用正則表達(dá)式發(fā)現(xiàn) URL 并轉(zhuǎn)化成超鏈接。
-
處理 Enter 鍵:
取消
Add
按鈕,通過(guò)檢查
textarea
的onkeydown
事件看看用戶是否按下了 Enter 或 Return 鍵。 - 顯示用戶輸入時(shí)間: 用戶開(kāi)始輸入的時(shí)候通知服務(wù)器,會(huì)談的其他人可以看到有人在回復(fù)。這樣如果有人打字慢可以將談話結(jié)束的感覺(jué)減到最低。
-
限制消息的大小:
保持談話順暢的另一個(gè)辦法是避免消息過(guò)長(zhǎng)。限制
textarea
中的最大字符數(shù) — 同樣通過(guò)捕獲onkeydown
— 有助于提高交談的速度。
這僅僅是修改上述代碼進(jìn)行改進(jìn)的部分想法。如果您這樣做了并且希望在社區(qū)中分享您的成果,請(qǐng)告訴我,我可以將其放到 下載 的源代碼中。
![]() ![]() |
![]()
|
我承認(rèn)我不大喜歡聊天。我從未打開(kāi)我的聊天客戶機(jī)。很長(zhǎng)時(shí)間內(nèi)僅使用過(guò)一次文本消息。我的聊天標(biāo)識(shí)符是 idratheryouemail 。夠嚴(yán)肅的。不過(guò)我發(fā)現(xiàn)結(jié)合當(dāng)前環(huán)境的聊天,比如本文所述的這種情況很吸引人。為什么?因?yàn)樗饕性诰W(wǎng)站有關(guān)的主題上,可以最大限度的避免關(guān)于最近 “TomKat” 新聞這類(lèi)的東拉西扯。
在您的 Web 應(yīng)用程序中嘗試這段代碼。看看能否讓您的讀者和客戶進(jìn)行實(shí)時(shí)交談,并通過(guò) developerWorks Ajax 論壇告訴我效果如何。希望能給您以驚喜。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫(xiě)作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
