瞭解如何使用 Asynchronous JavaScript + XML (Ajax) 和 PHP 在 Web 應用程式中建立聊天系統。您的客戶不需要下載或安裝任何專門的立即訊息通訊軟體,就能和您及其他客戶討論網站的內容。
|
請訪問 Ajax 技術資源中心,這是有關 Ajax 編程模型資訊的一站式中心,包括很多文檔、教程、論壇、blog、wiki 和新聞。任何 Ajax 的新資訊都能在這裡找到。
|
|
Web 2.0 一詞出現以來,開發人員都在說社區。不論您是否認為這有點誇大其辭,但讓使用者或讀者能夠方便地即時討論頁面主題或者銷售的產品,這一想法還是很迷人的。但是怎麼辦呢?能否在推銷產品的頁面中加入聊天,而不必讓客戶安裝任何特殊的軟體包括 Adobe Flash Player 呢?當然!實踐證明,用免費的現成工具如 PHP、MySQL、動態超文字標記語言 (DHTML)、Ajax 和 Prototype.js 庫就能完全做到。
不再羅嗦了,讓我們立即開始吧。
登入
聊天首先要有一個身份標識。這就需要一個簡單的登入頁,如 清單 1 所示。
清單 1. index.html
<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> |
該頁的顯示結果如 圖 1 所示。
圖 1. 聊天登入視窗
注意:該例中需要登入視窗是因為我希望知道誰在說話。對於您的應用程式,可能已經存在一個登入頁面,使用自己已有的使用者名稱即可。
基本的聊天系統
聊天系統實質上就是一個字串表格,每個字串屬於一個發言者。最簡單的模式如 清單 2 所示。
清單 2. chat.sql
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 )); |
指令碼中包含自動增加的訊息 ID、使用者名稱和訊息本身。如果需要,還可以向每條訊息增加時間戳記以記錄發送的時間。
如果需要管理不同話題的多個會話,還需要建立一個表記錄不同的話題,並在訊息表中增加相關的 topic_id
。為了盡量簡化例子,我採用了最簡單的模式。
建立資料庫和載入模式使用了下列命令:
% mysqladmin create chat% mysql chat < chat.sql |
根據 MySQL 伺服器的設定及其安全設定和口令,命令可能略有不同。
最基本的聊天使用者介面(UI)如 清單 3 所示。
清單 3. chat.php
<?phpif ( 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> |
在指令碼的開始部分中,您可從登入頁面提交的參數中擷取使用者名稱並儲存在會話中。然後載入 Prototype.js JavaScript 庫,它可以完成所有 Ajax 處理。
然後頁面提供了存放訊息的位置。該地區由檔案後面的 getMessages()
JavaScript 函數填寫。
訊息地區的下面是一個表單和使用者輸入訊息文本的 textarea
。還有一個按鈕 Add 添加聊天訊息。
頁面如 圖 2 所示。
圖 2. 簡單的聊天視窗
請注意 getMessages()
函數,頁面實際上每 1000 毫秒(1 秒)輪詢一次伺服器,檢查是否有新訊息,並把結果輸出到頁面上方的訊息地區。本文 後面 還要詳細介紹輪詢,我想首先完成基本的實現,messages.php 頁面返回當前的訊息列表。該頁如 清單 4 所示。
清單 4. messages.php
<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> |
指令碼的一開始用 DB 庫串連到資料庫,這個庫可從 PEAR 下載(請參閱 參考資料)。如果還沒有安裝這個庫,可通過下面的命令完成:
PEAR 安裝後,指令碼可以查詢當前的訊息,檢索每一行,輸出使用者名稱和訊息文本。
最後還有 add.php 指令碼,從頁面上 addmessage()
函數的 Prototype.js Ajax 代碼中調用。該指令碼從會話中取得訊息文本和使用者名稱,然後在訊息表中插入新的一行。代碼如 清單 5 所示。
清單 5. add.php
<?phprequire_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 指令碼還返回當前的訊息列表,因為原頁面中的 Ajax 代碼要從返回的 HTML 程式碼更新聊天記錄。這樣使用者就能馬上看到添加到會話中的文本。
聊天系統的基本結構就是這些。下一節說明如何改進輪詢的效率。
一點改進
這個原始的聊天系統中,頁面每秒請求一次對話的所有聊天記錄。雖然對於較短的對話影響不大,但是如果對話很長,效能問題就顯現出來了。所幸的是解決起來很簡單。每條訊息都有 message_id
,這個數字自動遞增。因此,如果知道已經有了屬於某個 ID 的訊息,只需要請求出現在此 ID 之後的訊息就可以。這樣可以大大降低訊息傳遞的數量。多數請求很可能沒有新的訊息,傳遞的包就會變小。
採用效率更高的設計需要稍微修改 chat.php 頁面,如 清單 6 所示。
清單 6. chat.php(修改)
<?phpif ( 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>
標記包含所有的訊息,現在改為 <table>
標記,收到新訊息的時候動態地追加一行。可以看到 getMessages()
函數中的相應變化,和第一個版本相比長了一些。
新版本的 getMessages()
預期 messages.php 頁面的結果是包含新訊息的 XML 塊。messages.php 增加了一個參數 id
,即頁面顯示的最後一條訊息的 message_id
。一開始 ID 為 0,因而 messages.php 頁面返回所有的訊息。此後則發送到目前為止顯示過的最後一條訊息的 ID。
XML 響應用 onSuccess
處理常式分解成元素,每個元素使用標準 DHTML 文件物件模型(DOM)函數添加到表格中,如 insertRow()
、insertCell()
和 appendChild()
。
修改後的 messages.php 檔案返回 XML 而不是 HTML,如 清單 7 所示。
清單 7. messages.php
<?phprequire_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 顯示了新的改進後的版本。
圖 3. 經過最佳化的聊天視窗
從外觀上來說沒有什麼改變。但是和原來的相比效率要高得多。
“即時” 的秘密
如果剛接觸 Ajax 或者僅對該領域有所瞭解,“輪詢” 的概念可能讓您感到害怕。不幸的是,輪詢是惟一的辦法。要在客戶機和伺服器之間建立連續管道,同時又不需要在兩端安裝特定軟體,尚不存在可實現此目的的跨平台、跨瀏覽器方法。即便這樣,可能還需要對防火牆進行專門配置才行得通。因此,如果需要人人能用的一種簡便辦法,Ajax 和輪詢是惟一的可能。
但是不斷宣傳和鼓吹的 “即時” 在哪兒呢?輪詢不可能是即時的。真的如此嗎?我認為這取決於您對即時 的定義。我過去編寫電生理學資料檢索代碼時,即時 意味著毫秒。我相信地質學家在某些情況下把分、日甚至年看作是即時。
如果查閱 Wikipedia,即會發現人類的平均反應時間大約在 200 到 270 毫秒之間。也就是擊一次球的時間。閱讀一條訊息並形成回覆的時間要長得多,即使您非常投入。因此,等待聊天訊息時,200 毫秒左右(可能再長一點)的時間應該足夠了。我設定為 1 秒,而且沒有感覺到不舒服。
作為 developerWorks Ajax 論壇(請參閱 參考資料)的主持人,輪詢 和即時 的問題每月至少遇到一次。我希望對於 Ajax 來說已經揭穿了輪詢和所謂即時 的面具。建議在考慮某種極其複雜的即時解決方案之前嘗試一下輪詢。這樣至少可以知道嘗試自訂的解決方案之前使用現成的工具能夠做什麼。
此後的工作
希望本文為您提供了一個不錯的起點,以此為基礎在您的應用程式中實現自己的聊天系統。下面是一些建議:
- 記錄使用者:在聊天視窗的旁邊列出目前參加會談的人員。這樣可以告訴人們誰參加了談話,什麼時候來的,什麼時候退出的。
- 允許多個會談:允許多個關於不同話題的談話同時進行。
- 支援表情字元:將
:-)
這樣的字元組合翻譯成適當的笑臉映像。
- 使用 URL 解析:在用戶端 JavaScript 代碼中使用Regex發現 URL 並轉化成超連結。
- 處理 Enter 鍵:取消 Add 按鈕,通過檢查
textarea
的 onkeydown
事件看看使用者是否按下了 Enter 或 Return 鍵。
- 顯示使用者輸入時間:使用者開始輸入的時候通知伺服器,會談的其他人可以看到有人在回複。這樣如果有人打字慢可以將談話結束的感覺減到最低。
- 限制訊息的大小:保持談話順暢的另一個辦法是避免訊息過長。限制
textarea
中的最大字元數 — 同樣通過捕獲 onkeydown
— 有助於提高交談的速度。
這僅僅是修改上述代碼進行改進的部分想法。如果您這樣做了並且希望在社區中分享您的成果,請告訴我,我可以將其放到 下載 的原始碼中。
結束語
我承認我不大喜歡聊天。我從未開啟我的聊天客戶機。很長時間內僅使用過一次簡訊。我的聊天標識符是 idratheryouemail。夠嚴肅的。不過我發現結合當前環境的聊天,比如本文所述的這種情況很吸引人。為什嗎?因為它主要集中在網站有關的主題上,可以最大限度的避免關於最近 “TomKat” 新聞這類的東拉西扯。
在您的 Web 應用程式中嘗試這段代碼。看看能否讓您的讀者和客戶進行即時交談,並通過 developerWorks Ajax 論壇告訴我效果如何。希望能給您以驚喜。