了解如何使用 Asynchronous JavaScript™ + XML (Ajax) 和 PHP 在 Web 应用程序中建立聊天系统。您的客户不需要下载或安装任何专门的即时消息通讯软件,就能和您及其他客户讨论网站的内容。
Web 2.0
一
词
出
现
以来,
开发
人
员
都在
说
社区。不
论
您是否
认为这
有点夸大其辞,但
让
用
户
或
读
者能
够
方便地
实时讨论页
面主
题
或者
销
售的
产
品,
这
一想法
还
是很吸引人的。但是怎
么办
呢?能否在推
销产
品的
页
面中加入聊天,而不必
让
客
户
安装任何特殊的
软
件包括
Adobe Flash Player
呢?当然!
实
践
证
明,用免
费
的
现
成工具如
PHP
、
MySQL
、
动态
HTML (DHTML)
、
Ajax
和
Prototype.js
库
就能完全做到。
不再
罗嗦
了,
让
我
们
立即
开
始吧。
登
录
清 单 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>
|
注意:该
例中需要登
录
窗口是因
为
我希望知道
谁
在
说话
。
对
于您的
应
用程序,可能已
经
存
在一个登
录页
面,使用自己已有的用
户
名即可。
基本的聊天系
统
清 单 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
服
务
器的
设
置及其安全
设
定和口令,命令可能略有不同。
清 单 3. chat.php
<?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 οnclick="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
添加聊天消息。
请
注意
getMessages()
函数,
页
面
实际
上
每
1000
毫秒(
1
秒)
轮询
一次服
务
器,
检查
是否有新消息,并把
结
果
输
出到
页
面上方的消息区域。本文
后面
还
要
详细
介
绍轮询
,我想首先完成基本的
实现
,
messages.php
页
面返回当前的消息列表。
该页
如
清单 4
所示。
清 单 4. messages.php
<table>
<?php
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>
|
% pear install DB
|
PEAR
安装后,脚本可以
查询
当前的消息,
检
索
每
一行,
输
出用
户
名和消息文本。
最后
还
有
add.php
脚本,从
页
面上
addmessage()
函数的
Prototype.js Ajax
代
码
中
调
用。
该
脚本从会
话
中取得消息文本和用
户
名,然后在消息表中插入新的一行。代
码
如
清单 5
所示。
清 单 5. add.php
<?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
脚本
还
返回当前的消息列表,因
为
原
页
面中的
Ajax
代
码
要从返回的
HTML
代
码
更新聊天
记录
。
这样
用
户
就能
马
上看到添加到会
话
中的文本。
聊天系
统
的基本
结
构就是
这
些。下一
节说
明如何改
进轮询
的效率。
一点改
进
这
个原始的聊天系
统
中,
页
面
每
秒
请
求一次
对话
的所有聊天
记录
。
虽
然
对
于
较
短的
对话
影响不大,但是如果
对话
很
长
,性能
问题
就
显现
出来了。所幸的是解决起来很
简单
。
每
条消息都有
message_id
,
这
个数字自
动递
增。因此,如果知道已
经
有了属于某个
ID
的消息,只需要
请
求出
现
在此
ID
之后的消息就可以。
这样
可以大大降低消息
传递
的数量。多数
请
求很可能没有新的消息,
传递
的包就会
变
小。
清 单 6. chat.php(修改)
<?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 οnclick="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()
。
<?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>
|
从外
观
上来
说
没有什
么
改
变
。但是和原来的相比效率要高得多。
如果
刚
接触
Ajax
或者
仅对该领
域有所了解,
“
轮询
”
的概念可能
让
您感到害怕。不幸的是,
轮询
是惟一的
办
法。要在客
户
机和服
务
器之
间
建立
连续
管道,同
时
又不需要在两端安装特定
软
件,尚不存在可
实现
此目的的跨平台、跨
浏览
器方法。即便
这样
,可能
还
需要
对
防火
墙进
行
专门
配置才行得通。因此,如果需要人人能用的一
种简
便
办
法,
Ajax
和
轮询
是惟一的可能。
但是不断宣
传
和鼓吹的
“
实时
”
在哪儿呢?
轮询
不可能是
实时
的。真的如此
吗
?我
认为这
取决于您
对实时
的定
义
。我
过
去
编
写
电
生理学数据
检
索代
码时
,
实时
意味着毫秒。我相信地
质
学家在某些情况下把分、日甚至年看作是
实时
。
如果
查阅
Wikipedia
,即会
发现
人
类
的平均反
应时间
大
约
在
200
到
270
毫秒之
间
。也就是
击
一次球的
时间
。
阅读
一条消息并形成答
复
的
时间
要
长
得多,即使您非常投入。因此,等待聊天消息
时
,
200
毫秒左右(可能再
长
一点)的
时间应该
足
够
了。我
设
置
为
1
秒,而且没有感
觉
到不舒服。
作
为
developerWorks Ajax
论坛
(
请
参
阅
参考资料
)的主持人,
轮询
和
实时
的
问题每
月至少遇到一次。我希望
对
于
Ajax
来
说
已
经揭
穿了
轮询
和所
谓实时
的面具。建
议
在考
虑
某
种
极其
复杂
的
实时
解决方案之前
尝试
一下
轮询
。
这样
至少可以知道
尝试
自定
义
的解决方案之前使用
现
成的工具能
够
做什
么
。
此后的工作
希望本文
为
您提供了一个不
错
的起点,以此
为
基
础
在您的
应
用程序中
实现
自己的聊天系
统
。
下面是一些建
议
:
- 记录用户:在聊天窗口的旁边列出目前参加会谈的人员。这样可以告诉人们谁参加了谈话,什么时候来的,什么时候退出的。
- 允许多个会谈:允许多个关于不同话题的谈话同时进行。
- 支持表情字符:将:-)这样的字符组合翻译成适当的笑脸图像。
- 使用 URL 解析:在客户端 JavaScript 代码中使用正则表达式发现 URL 并转化成超链接。
- 处理 Enter 键:取消 Add 按钮,通过检查textarea的onkeydown事件看看用户是否按下了 Enter 或 Return 键。
- 显示用户输入时间:用户开始输入的时候通知服务器,会谈的其他人可以看到有人在回复。这样如果有人打字慢可以将谈话结束的感觉减到最低。
- 限制消息的大小:保持谈话顺畅的另一个办法是避免消息过长。限制textarea中的最大字符数 — 同样通过捕获onkeydown — 有助于提高交谈的速度。
结
束
语
我承
认
我不大喜
欢
聊天。我从未打
开
我的聊天客
户
机。很
长时间
内
仅
使用
过
一次文本消息。我的聊天
标识
符是
idratheryouemail
。
够严肃
的。不
过
我
发现结
合当前
环
境的聊天,比如本文所述的
这种
情况很吸引人。
为
什
么
?因
为
它主要集中在网站有
关
的主
题
上,可以最大限度的避免
关
于最近
“TomKat”
新
闻这类
的
东
拉西扯。
在您的
Web
应
用程序中
尝试这
段代
码
。
看看能否
让
您的
读
者和客
户进
行
实时
交
谈
,并通
过
developerWorks Ajax
论坛
告
诉
我效果如何。希望能
给
您以惊喜。
学
习
- 您可以参阅本文在 developerWorks 全球网站上的 英文原文。
- 订阅 Ajax 和 XML 系列 RSS 提要。
- Ajax 技术资源中心:developerWorks 上所有有关 Ajax 的问题都可以在这里找到解答。
- 订阅 Ajax 相关文章和教程的 RSS 提要:获得即将发表的 Ajax 相关文章和教程的通知(查看 developerWorks 内容 RSS 提要 了解更多的信息)。
- PHP 主页:为 PHP 程序员提供了大量参考资料。
- Prototype 库:这个 JavaScript Framework 可以简化动态 Web 应用程序的开发。
- Scriptaculous JavaScript 库:这个基于 Prototype 的框架提供的显示帮助器和特效可以改善您的网站。
- Prototype.js 文档:关于 Prototype JavaScript 库的其他信息,包括到官方 Prototype blog 和很多其他资源的链接。
- jQuery:提供了和 Prototype.js 功能类似的另一个 JavaScript 库。
- Yahoo! User Interface Library:看看 Yahoo! 的 Ajax 工具箱。
- developerWorks XML 专区:通过 developerWorks XML 专区了解 XML 的方方面面。
- IBM XML 认证:了解如何才能成为一名 IBM 认证的 XML 及相关技术的开发人员。
- XML 技术文档库:developerWorks XML 专区提供了大量技术文章和技巧、教程、标准以及 IBM 红皮书。
- developerWorks 技术事件和网络广播:随时关注技术的最新进展。