Node.js与WebSocket实战:构建HiChat即时聊天应用
本文详解使用Node.js、Express和Socket.io构建网页即时聊天程序HiChat的全过程。涵盖环境搭建、基础HTTP服务、Socket.io通信机制、用户登录与在线统计、消息收发、图片与表情发送、自定义字体颜色及键盘快捷键等功能,并提供Heroku部署指南与常见问题分析。
Node.js与WebSocket实战:构建HiChat即时聊天应用
前端开发领域始终充满创新,不仅体现在富有创意的页面设计上,还在于令人惊叹的交互效果以及层出不穷的新技术。以Node.js为代表的后端技术打破了传统界限,将前端开发者的能力延伸至后端领域,实现了前后端能力的融合。开发者得以在前后端代码间自由切换,极大地提升了开发效率与掌控力。
本文将演示如何利用Node.js结合WebSocket协议开发一个网页即时聊天应用,项目命名为HiChat(中文意为“嗨聊”)。
开发过程中将主要使用express和socket.io两个核心模块,后续章节将详细介绍。
源码
完整源码可在页面末尾下载获取。
本地运行方法如下:
- 在命令行中执行npm install命令安装依赖。
- 模块下载完成后,运行node server命令启动服务器。
- 打开浏览器访问localhost地址即可。
下图展示了程序运行效果预览:

准备工作与Node基础
准备工作
本文示例基于Windows系统,Linux环境下Node安装与命令行操作略有差异,但程序实现逻辑基本跨平台通用。
Node相关
- 首先需在本机安装Node.js环境。
- 建议具备基础的Node.js知识,若未接触过,可参考相关入门教程。
接下来,我们将创建一个简单的HTTP服务器。
参考以下极简代码,它创建了一个监听系统80端口的HTTP服务器。
//node server example
//引入http模块
var http = require('http'),
//创建一个服务器
server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-Type': 'text/plain'
});
res.write('hello world!');
res.end();
});
//监听80端口
server.listen(80);
console.log('server started');
将代码保存为server.js文件,通过命令行执行node server或node server.js即可启动服务器。此时在浏览器地址栏输入localhost或本机IP 127.0.0.1即可访问,无需指定端口,因为服务器监听的是默认80端口。若80端口被占用,可改用其他端口如8080,访问时需显式指定端口号localhost:8080。


Express模块应用
Express
首先通过npm进行安装。
- 在项目文件夹下打开命令行(提示:按住Shift键同时右击,可在右键菜单中选择“从此处打开命令行”选项)。
- 在命令行中输入npm install express并回车执行安装。
- 随后在server.js中通过require('express')引入该模块。
Express是Node.js中用于管理路由和响应请求的模块,可根据请求的URL返回相应的HTML页面。此处我们使用预先编写好的静态页面返回给客户端,只需通过Express指定返回页面的路径即可。若不使用此模块,需将HTML代码与后台JavaScript代码耦合在一起处理请求,较为不便。
//返回一个简单的HTML内容
server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-Type': 'text/html' //将返回类型由text/plain改为text/html
});
res.write('<h1>hello world!</h1>'); //返回HTML标签
res.end();
});
在与server.js同级目录下新建名为www的文件夹用于存放网页文件,包括图片及前端js文件等。假设已在www文件夹下编写好index.html文件(下一步将介绍,此步可先放置空HTML文件),则可通过以下方式使用Express将该页面返回至浏览器。相比初始版本,服务器代码已显著简化。
//使用express模块返回静态页面
var express = require('express'), //引入express模块
app = express(),
server = require('http').createServer(app);
app.use('/', express.static(__dirname + '/www')); //指定静态HTML文件的位置
server.listen(80);
界面包含四个按钮,分别为设置字体颜色、发送表情、发送图片和清除记录,后续将逐一介绍其实现方式。
Socket.io通信机制
socket.io
这是Node.js中用于实现Socket通信的包。利用它可以方便地建立服务器到客户端的Socket连接,发送事件并接收特定事件。
同样通过npm进行安装:npm install socket.io。安装后在node_modules文件夹下会生成socket.io文件夹,其中包含socket.io.js文件。将其引入HTML页面后,前端即可使用Socket.io与服务器进行通信。
<script src="/socket.io/socket.io.js"></script>
同时,在服务器端的server.js中,需通过require('socket.io')引入该模块,以便在服务器端使用Socket.io功能。
使用Socket.io时,前后端语法保持一致,即通过socket.emit()触发事件,通过socket.on()监听并处理对应事件。两者通过传递参数进行通信。具体工作模式可参考以下示例。
例如,在index.html中包含如下JavaScript代码(假设页面已放置ID为sendBtn的按钮):
<script type="text/javascript">
var socket=io.connect(),//与服务器进行连接
button=document.getElementById('sendBtn');
button.onclick=function(){
socket.emit('foo', 'hello');//发送一个名为foo的事件,并且传递一个字符串数据‘hello'
}
</script>
上述代码首先建立与服务器的连接,获取socket实例。随后,当页面上ID为sendBtn的按钮被点击时,通过该socket实例发起名为foo的事件,并向服务器传递字符串“hello”。
与此同时,我们需要在服务器端编写相应代码来处理该foo事件并接收传递的数据。
为此,在server.js中可编写如下代码:
//服务器及页面响应部分
var express = require('express'),
app = express(),
server = require('http').createServer(app),
io = require('socket.io').listen(server); //引入socket.io模块并绑定到服务器
app.use('/', express.static(__dirname + '/www'));
server.listen(80);
//socket部分
io.on('connection', function(socket) {
//接收并处理客户端发送的foo事件
socket.on('foo', function(data) {
//将消息输出到控制台
console.log(data);
})
});
现在按Ctrl+C关闭之前启动的服务器,再次输入node server启动服务器运行新代码查看效果。若一切正常,点击页面按钮后,在命令行窗口中将看到输出的“hello”字符串。
如前所述,Socket.io前后端语法一致,因此从服务器发送事件至客户端,并在客户端接收处理消息也是显而易见的操作。此处仅作简要介绍,后续将通过发送聊天消息进一步演示。
聊天界面与基础功能开发
基本页面
基于上述基础了解,接下来进入聊天程序功能的开发阶段。
首先构建主页面。鉴于这是大众化应用,界面设计无需过多纠结,脑海中已有大致雏形:包含一个显示消息的主窗体、一个输入消息的文本框以及一个发送消息按钮,这三者是必备元素。
此外,为实现以下四个功能,界面上还设有设置字体颜色、发送表情、发送图片和清除记录四个按钮。
最终页面即前文截图所示,代码如下:
www/index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="author" content="Wayou">
<meta name="description" content="hichat | a simple chat application built with node.js and websocket">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>hichat</title>
<link rel="stylesheet" href="styles/main.css">
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="icon" href="favicon.ico" type="image/x-icon">
</head>
<body>
<div class="wrapper">
<div class="banner">
<h1>HiChat :)</h1>
<span id="status"></span>
</div>
<div id="historyMsg">
</div>
<div class="controls" >
<div class="items">
<input id="colorStyle" type="color" placeHolder='#000' title="font color" />
<input id="emoji" type="button" value="emoji" title="emoji" />
<label for="sendImage" class="imageLable">
<input type="button" value="image" />
<input id="sendImage" type="file" value="image"/>
</label>
<input id="clearBtn" type="button" value="clear" title="clear screen" />
</div>
<textarea id="messageInput" placeHolder="enter to send"></textarea>
<input id="sendBtn" type="button" value="SEND">
<div id="emojiWrapper">
</div>
</div>
</div>
<div id="loginWrapper">
<p id="info">connecting to server...</p>
<div id="nickWrapper">
<input type="text" placeHolder="nickname" id="nicknameInput" />
<input type="button" value="OK" id="loginBtn" />
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="scripts/hichat.js"></script>
</body>
</html>
样式文件 www/styles/main.css
html, body {
margin: 0;
background-color: #efefef;
font-family: sans-serif;
}
.wrapper {
width: 500px;
height: 640px;
padding: 5px;
margin: 0 auto;
background-color: #ddd;
}
#loginWrapper {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(5, 5, 5, .6);
text-align: center;
color: #fff;
display: block;
padding-top: 200px;
}
#nickWrapper {
display: none;
}
.banner {
height: 80px;
width: 100%;
}
.banner p {
float: left;
display: inline-block;
}
.controls {
height: 100px;
margin: 5px 0px;
position: relative;
}
#historyMsg {
height: 400px;
background-color: #fff;
overflow: auto;
padding: 2px;
}
#historyMsg img {
max-width: 99%;
}
.timespan {
color: #ddd;
}
.items {
height: 30px;
}
#colorStyle {
width: 50px;
border: none;
padding: 0;
}
/*custom the file input*/
.imageLable {
position: relative;
}
#sendImage {
position: absolute;
width: 52px;
left: 0;
opacity: 0;
overflow: hidden;
}
/*end custom file input*/
#messageInput {
width: 440px;
max-width: 440px;
height: 90px;
max-height: 90px;
}
#sendBtn {
width: 50px;
height: 96px;
float: right;
}
#emojiWrapper {
display: none;
width: 500px;
bottom: 105px;
position: absolute;
background-color: #aaa;
box-shadow: 0 0 10px #555;
}
#emojiWrapper img {
margin: 2px;
padding: 2px;
width: 25px;
height: 25px;
}
#emojiWrapper img:hover {
background-color: blue;
}
.emoji{
display: inline;
}
footer {
text-align: center;
}
为保持项目良好的目录结构便于管理,在www文件夹下新建styles文件夹存放样式文件main.css,并新建scripts文件夹存放前端JS文件如hichat.js(所有前端JS代码集中于此),服务器JS文件server.js仍置于最外层。
同时新建content文件夹用于存放其他资源如图片等,其中content文件夹内再建emoji文件夹用于存储表情gif图,后续将用到。最终项目目录结构如下:
├─node_modules
└─www
├─content
│ └─emoji
├─scripts
└─styles
此时打开页面,将看到一个淡黑色遮罩层。接下来需实现用户昵称输入与服务器登录功能。该遮罩层用于显示连接服务器的状态信息,连接完成后,将出现输入框用于输入昵称。
前文HTML代码中已引入www/scripts/hichat.js文件,下面开始编写基本前端JS以实现连接功能。
定义一个全局变量HiChat用于整个程序开发,同时使用window.onload在页面加载完成后实例化HiChat,并调用其init方法运行程序。
www/scripts/Hichat.js
window.onload = function() {
//实例并初始化我们的hichat程序
var hichat = new HiChat();
hichat.init();
};
//定义我们的hichat类
var HiChat = function() {
this.socket = null;
};
//向原型添加业务方法
HiChat.prototype = {
init: function() {//此方法初始化程序
var that = this;
//建立到服务器的socket连接
this.socket = io.connect();
//监听socket的connect事件,此事件表示连接已经建立
this.socket.on('connect', function() {
//连接到服务器后,显示昵称输入框
document.getElementById('info').textContent = 'get yourself a nickname :)';
document.getElementById('nickWrapper').style.display = 'block';
document.getElementById('nicknameInput').focus();
});
}
};
上述代码定义了程序所需的HiChat类,后续处理消息、显示消息等业务逻辑均编写在此类中。
首先定义程序初始化方法,其中初始化Socket并监听连接事件。一旦连接到服务器,便显示昵称输入框。用户输入昵称后,服务器后台接收并进行下一步处理。
用户登录与昵称管理
设置昵称
要求连接用户首先设置昵称,且昵称必须唯一,不能与他人重名。这既便于用户区分,也有助于统计在线人数,并方便维护保存所有用户昵称的数组。
为此,在后台server.js中创建一个名为users的全局数组变量。当用户设置好昵称发送至服务器时,将昵称压入users数组。同时需注意,若用户断线离开,需相应地从users数组中移除,以保证数据正确性。
在前端,输入昵称点击OK提交后,需发起一个设置昵称的事件以便服务器监听。将以下代码添加至之前的init方法中。
www/scripts/hichat.js
//昵称设置的确定按钮
document.getElementById('loginBtn').addEventListener('click', function() {
var nickName = document.getElementById('nicknameInput').value;
//检查昵称输入框是否为空
if (nickName.trim().length != 0) {
//不为空,则发起一个login事件并将输入的昵称发送到服务器
that.socket.emit('login', nickName);
} else {
//否则输入框获得焦点
document.getElementById('nicknameInput').focus();
};
}, false);
server.js
//服务器及页面部分
var express = require('express'),
app = express(),
server = require('http').createServer(app),
io = require('socket.io').listen(server),
users=[];//保存所有在线用户的昵称
app.use('/', express.static(__dirname + '/www'));
server.listen(80);
//socket部分
io.on('connection', function(socket) {
//昵称设置
socket.on('login', function(nickname) {
if (users.indexOf(nickname) > -1) {
socket.emit('nickExisted');
} else {
socket.userIndex = users.length;
socket.nickname = nickname;
users.push(nickname);
socket.emit('loginSuccess');
io.sockets.emit('system', nickname); //向所有连接到服务器的客户端发送当前登陆用户的昵称
};
});
});
需解释的是,在connection事件的回调函数中,socket表示当前连接到服务器的客户端。因此代码socket.emit('foo')仅自己可接收该事件,而socket.broadcast.emit('foo')则表示向除自己外的所有人发送该事件。另外,代码中io表示服务器整个Socket连接,因此代码io.sockets.emit('foo')表示所有人都可收到该事件。
上述代码先判断接收到的昵称是否已存在于users中。若存在,则向自己发送一个nickExisted事件,前端接收到该事件后显示提示信息通知用户。
将以下代码添加至hichat.js的init方法中。
www/scripts/hichat.js
this.socket.on('nickExisted', function() {
document.getElementById('info').textContent = '!nickname is taken, choose another pls'; //显示昵称被占用的提示
});
若昵称未被其他用户占用,则将该昵称压入users数组,同时将其作为属性存至当前socket变量中。并将该用户在数组中的索引(因是数组最后一个元素,索引即为数组长度users.length)也作为属性保存至socket中,后续将用到。最后向自己发送一个loginSuccess事件,通知前端登录成功。前端接收到成功消息后移除灰色遮罩层,显示聊天界面。
将以下代码添加至hichat.js的init方法中。
www/scripts/hichat.js
this.socket.on('loginSuccess', function() {
document.title = 'hichat | ' + document.getElementById('nicknameInput').value;
document.getElementById('loginWrapper').style.display = 'none';//隐藏遮罩层显聊天界面
document.getElementById('messageInput').focus();//让消息输入框获得焦点
});
在线统计与系统消息
在线统计
此处实现显示在线用户数,并在聊天主界面中以系统身份显示用户连接与离开等信息。
上述server.js中,除loginSuccess事件外,还有一句代码通过io.sockets.emit向所有用户发送了一个system事件,传递了刚登录用户的昵称。所有人接收到该事件后,将在聊天窗口显示一条系统消息“某某加入了聊天室”。同时,考虑到前端无法得知用户是进入还是离开,故在该system事件中多传递一个数据以表明用户是进入还是离开。
将server.js中login事件更改如下:
server.js
socket.on('login', function(nickname) {
if (users.indexOf(nickname) > -1) {
socket.emit('nickExisted');
} else {
socket.userIndex = users.length;
socket.nickname = nickname;
users.push(nickname);
socket.emit('loginSuccess');
io.sockets.emit('system', nickname, users.length, 'login');
};
});
相比之前,多传递了一个login字符串。
同时再添加一个用户离开的事件,可通过socket.io自带的disconnect事件完成。当一个用户断开连接时,disconnect事件触发。在此事件中做两件事:一是将用户从users数组中删除,二是发送一个system事件通知所有人“某某离开了聊天室”。
将以下代码添加至server.js中connection的回调函数中。
server.js
//断开连接的事件
socket.on('disconnect', function() {
//将断开连接的用户从users中删除
users.splice(socket.userIndex, 1);
//通知除自己以外的所有人
socket.broadcast.emit('system', socket.nickname, users.length, 'logout');
});
上述代码通过JavaScript数组的splice方法将当前断开连接的用户从users数组中删除,此处可见之前保存的用户索引被使用。同时发送与用户连接时相同的system事件通知所有人“某某离开了”,为使前端知道是离开事件,故发送了一个“logout”字符串。
下面开始前端实现,即接收system事件。
在hichat.js中,将以下代码添加至init方法中。
www/scripts/hichat.js
this.socket.on('system', function(nickName, userCount, type) {
//判断用户是连接还是离开以显示不同的信息
var msg = nickName + (type == 'login' ? ' joined' : ' left');
var p = document.createElement('p');
p.textContent = msg;
document.getElementById('historyMsg').appendChild(p);
//将在线人数显示到页面顶部
document.getElementById('status').textContent = userCount + (userCount > 1 ? ' users' : ' user') + ' online';
});
现在运行程序,打开多个浏览器标签页,然后登录离开,即可看到相应的系统提示消息。
聊天消息发送功能
发送消息
用户连接与断开需显示系统消息,用户还需频繁发送聊天消息,故考虑将消息显示到页面这一功能单独编写为一个函数以便调用。为此向HiChat类中添加一个_displayNewMsg方法,它接收要显示的消息、消息来源以及颜色共三个参数。因希望系统消息区别于普通用户消息,故增加颜色参数。同时,该参数也便于后续实现让用户自定义文本颜色。
将以下代码添加至我的HiChat类中。
www/scripts/hichat.js
//向原型添加业务方法
HiChat.prototype = {
init: function() { //此方法初始化程序
//...
},
_displayNewMsg: function(user, msg, color) {
var container = document.getElementById('historyMsg'),
msgToDisplay = document.createElement('p'),
date = new Date().toTimeString().substr(0, 8);
msgToDisplay.style.color = color || '#000';
msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span>' + msg;
container.appendChild(msgToDisplay);
container.scrollTop = container.scrollHeight;
}
};
在_displayNewMsg方法中,还向消息添加了一个日期。同时判断了该方法调用时是否传递了颜色参数,未传递颜色的话默认使用#000即黑色。
同时修改我们在system事件中显示系统消息的代码,让其调用此_displayNewMsg方法。
www/scripts/hichat.js
this.socket.on('system', function(nickName, userCount, type) {
var msg = nickName + (type == 'login' ? ' joined' : ' left');
//指定系统消息显示为红色
that._displayNewMsg('system ', msg, 'red');
document.getElementById('status').textContent = userCount + (userCount > 1 ? ' users' : ' user') + ' online';
});
现在的效果如下:
有了此显示消息的方法后,下面开始实现用户间的聊天功能。
做法很简单,若掌握了前述emit发送事件、on接收事件,用户聊天消息的发送接收便轻车熟路。
首先为页面的发送按钮编写click事件处理程序,通过addEventListener监听此click事件。当用户点击发送时,先检查输入框是否为空,若不为空,则向服务器发送postMsg事件,将用户输入的聊天文本发送至服务器,由服务器接收并分发至除自己外的所有用户。
将以下代码添加至hichat.js的init方法中。
www/scripts/hichat.js
document.getElementById('sendBtn').addEventListener('click', function() {
var messageInput = document.getElementById('messageInput'),
msg = messageInput.value;
messageInput.value = '';
messageInput.focus();
if (msg.trim().length != 0) {
that.socket.emit('postMsg', msg); //把消息发送到服务器
that._displayNewMsg('me', msg); //把自己的消息显示到自己的窗口中
};
}, false);
在server.js中添加代码以接收postMsg事件。
server.js
io.on('connection', function(socket) {
//其他代码。。。
//接收新消息
socket.on('postMsg', function(msg) {
//将消息发送到除自己外的所有用户
socket.broadcast.emit('newMsg', socket.nickname, msg);
});
});
然后在客户端接收服务器发送的newMsg事件,并将聊天消息显示到页面。
将以下代码显示添加至hichat.js的init方法中。
this.socket.on('newMsg', function(user, msg) {
that._displayNewMsg(user, msg);
});
运行程序,现在可发送聊天消息了。
图片发送与显示处理
发送图片
上述已实现基本聊天功能,进一步,若希望用户可发送图片,程序将更完美。
图片不同于文字,但通过将图片转化为字符串形式后,便可像发送普通文本消息一样发送图片,只是在显示时将其还原为图片。
在此之前,我们已将图片按钮放置于页面,其实是一个文件类型的input,下面只需在其上做文章即可。
用户点击图片按钮后,弹出文件选择窗口供用户选择图片。随后可在JavaScript代码中使用FileReader将图片读取为base64格式的字符串形式进行发送。而base64格式的图片可直接指定为图片的src,从而将图片用img标签显示在页面。
为此我们监听图片按钮的change事件,一旦用户选择了图片,便显示到自己的屏幕上同时读取为文本发送至服务器。
将以下代码添加至hichat.js的init方法中。
www/scripts/hichat.js
document.getElementById('sendImage').addEventListener('change', function() {
//检查是否有文件被选中
if (this.files.length != 0) {
//获取文件并用FileReader进行读取
var file = this.files[0],
reader = new FileReader();
if (!reader) {
that._displayNewMsg('system', '!your browser doesn\'t support fileReader', 'red');
this.value = '';
return;
};
reader.onload = function(e) {
//读取成功,显示到页面并发送到服务器
this.value = '';
that.socket.emit('img', e.target.result);
that._displayImage('me', e.target.result);
};
reader.readAsDataURL(file);
};
}, false);
上述图片读取成功后,调用_displayNImage方法将图片显示在自己的屏幕同时向服务器发送了一个img事件。在server.js中,我们通过此事件来接收并分发图片至每个用户。同时也意味着还需在前端编写相应代码来接收。
此_displayNImage尚未实现,将在下面介绍。
将以下代码添加至server.js的socket回调函数中。
server.js
//接收用户发来的图片
socket.on('img', function(imgData) {
//通过一个newImg事件分发到除自己外的每个用户
socket.broadcast.emit('newImg', socket.nickname, imgData);
});
同时向hichat.js的init方法添加以下代码以接收显示图片。
this.socket.on('newImg', function(user, img) {
that._displayImage(user, img);
});
有个问题就是如果图片过大,会破坏整个窗口的布局,或者会出现水平滚动条,故我们对图片进行样式上的设置,让它最多只能以聊天窗口的99%宽度来显示,这样过大的图片就会自动缩小。
#historyMsg img {
max-width: 99%;
}
但考虑到缩小后的图片有可能失真,用户看不清,我们需要提供一种方法让用户可查看原尺寸大小的图片。故将图片用一个链接进行包裹,当点击图片时我们打开一个新的窗口页面,并将图片按原始大小呈现至这个新页面中供用户查看。
所以最后我们实现的_displayNImage方法应如下所示。
将以下代码添加至hichat.js的HiChat类中。
www/scripts/hichat.js
_displayImage: function(user, imgData, color) {
var container = document.getElementById('historyMsg'),
msgToDisplay = document.createElement('p'),
date = new Date().toTimeString().substr(0, 8);
msgToDisplay.style.color = color || '#000';
msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span> <br/>' + '<a href="' + imgData + '" target="_blank"><img src="' + imgData + '"/></a>';
container.appendChild(msgToDisplay);
container.scrollTop = container.scrollHeight;
}
再次启动服务器打开程序,我们可以发送图片了。
表情发送与混排实现
发送表情
文字往往难以表达说话时的面部表情,于是表情应运而生。
前文已介绍如何发送图片,严格来说,表情也是图片,但它有特殊之处,因为表情可穿插在文字中一并发送,故不能像处理图片那样处理表情。
根据以往经验,其他聊天程序将表情转为符号,例如我想发笑脸,并规定“:)”这个符号代表笑脸表情。数据传输过程中实际传输的是一个冒号加右括号的组合,当每个客户端接收到消息后,从文字中将这些表情符号提取出来,再用gif图片替换,这样呈现到页面便看到了表情加文字的混排效果。
你好,王尼玛[emoji:23]------>你好,王尼玛
上述形象展示了程序中表情的使用,可见我规定了一种格式来代表表情:[emoji:xx],中括号括起来然后“emoji”加个冒号,后面跟一个数字,该数字表示某个gif图片的编号。程序中,若点击表情按钮,然后呈现所有可用的表情图片,当用户选择一个表情后,生成对应的代码插入到当前待发送的文字消息中。发出去后,每个人接收到的也是代码形式的消息,只是在将消息显示到页面前,我们将表情代码提取出来,获取图片编号,然后用相应的图片替换。
首先需将所有可用的表情图片显示到一个小窗口,该窗口会在点击了表情按钮后显示如下图,在HTML代码中已添加好了此窗口,下面只需实现代码部分。
我们使用兔斯基作为聊天程序的表情包。可见,有很多张gif图,若手动编写,需花费不少功夫,不断写<img src='xx.gif'/>,故考虑将此工作交由代码自动完成,编写一个方法来初始化所有表情。
为此将以下代码添加至HiChat类中,并在init方法中调用此方法。
www/scripts/hichat.js
_initialEmoji: function() {
var emojiContainer = document.getElementById('emojiWrapper'),
docFragment = document.createDocumentFragment();
for (var i = 69; i > 0; i--) {
var emojiItem = document.createElement('img');
emojiItem.src = '../content/emoji/' + i + '.gif';
emojiItem.title = i;
docFragment.appendChild(emojiItem);
};
emojiContainer.appendChild(docFragment);
}
同时将以下代码添加至hichat.js的init方法中。
www/scripts/hichat.js
this._initialEmoji();
document.getElementById('emoji').addEventListener('click', function(e) {
var emojiwrapper = document.getElementById('emojiWrapper');
emojiwrapper.style.display = 'block';
e.stopPropagation();
}, false);
document.body.addEventListener('click', function(e) {
var emojiwrapper = document.getElementById('emojiWrapper');
if (e.target != emojiwrapper) {
emojiwrapper.style.display = 'none';
};
});
上面向页面添加了两个单击事件,一是表情按钮单击显示表情窗口,二是点击页面其他地方关闭表情窗口。
现在要做的就是,具体到某个表情被选中后,需获取被选中的表情,然后转换为相应的表情代码插入到消息框中。
为此我们再编写一个这些图片的click事件处理程序。将以下代码添加至hichat.js的init方法中。
www/scripts/hichat.js
document.getElementById('emojiWrapper').addEventListener('click', function(e) {
//获取被点击的表情
var target = e.target;
if (target.nodeName.toLowerCase() == 'img') {
var messageInput = document.getElementById('messageInput');
messageInput.focus();
messageInput.value = messageInput.value + '[emoji:' + target.title + ']';
};
}, false);
现在表情选中后,消息输入框中即可得到相应的代码了。
后续的发送与普通消息发送无异,因前文已实现了文本消息的发送,故此处无需再实现什么,只需更改之前用来显示消息的代码:首先判断消息文本中是否含有表情符号,若有,则转换为图片,最后再显示到页面。
为此我们编写一个方法接收文本消息为参数,用正则搜索其中的表情符号,将其替换为img标签,最后返回处理好的文本消息。
将以下代码添加至HiChat类中。
www/scripts/hichat.js
_showEmoji: function(msg) {
var match, result = msg,
reg = /\[emoji:\d+\]/g,
emojiIndex,
totalEmojiNum = document.getElementById('emojiWrapper').children.length;
while (match = reg.exec(msg)) {
emojiIndex = match[0].slice(7, -1);
if (emojiIndex > totalEmojiNum) {
result = result.replace(match[0], '[X]');
} else {
result = result.replace(match[0], '<img class="emoji" src="../content/emoji/' + emojiIndex + '.gif" />');
};
};
return result;
}
现在去修改之前我们显示消息的_displayNewMsg方法,让其显示消息前调用此_showEmoji方法。
_displayNewMsg: function(user, msg, color) {
var container = document.getElementById('historyMsg'),
msgToDisplay = document.createElement('p'),
date = new Date().toTimeString().substr(0, 8),
//将消息中的表情转换为图片
msg = this._showEmoji(msg);
msgToDisplay.style.color = color || '#000';
msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span>' + msg;
container.appendChild(msgToDisplay);
container.scrollTop = container.scrollHeight;
}
下面是实现后的效果:
主要功能已基本完成,为使程序更具人性与美观,可加入修改文字颜色的功能,以及键盘快捷键操作的支持,这也是一般聊天程序都有的功能,回车即可发送消息。
文字颜色与键盘快捷键
文字颜色
万幸,HTML5新增了一个专门用于颜色选取的input标签,且Chrome对其支持极佳,直接弹出系统的颜色拾取窗口。

IE及FF中均是一个普通的文本框,不过不影响使用,只是用户只能通过输入具体的颜色值来进行颜色设置,不如Chrome里面那么方便直观。
前文我们的_displayNewMsg方法可接收一个color参数,现在需做的是每次发送消息至服务器时,多加一个color参数即可。同时,在显示消息时调用_displayNewMsg时将此color传递过去。
下面是修改hichat.js中消息发送按钮代码的示例:
document.getElementById('sendBtn').addEventListener('click', function() {
var messageInput = document.getElementById('messageInput'),
msg = messageInput.value,
//获取颜色值
color = document.getElementById('colorStyle').value;
messageInput.value = '';
messageInput.focus();
if (msg.trim().length != 0) {
//显示和发送时带上颜色值参数
that.socket.emit('postMsg', msg, color);
that._displayNewMsg('me', msg, color);
};
}, false);
同时修改hichat.js中接收消息的代码,让其接收颜色值
this.socket.on('newMsg', function(user, msg, color) {
that._displayNewMsg(user, msg, color);
});
此处仅展示了发送按钮的修改,改动非常小,只是每次消息发送时获取一下颜色值,同时emit事件至服务器时也带上此颜色值,这样前端在显示时即可根据此颜色值为每个不同用户显示他们自己设置的颜色。剩下的就是按相同做法把发送图片时也加上颜色,此处省略。
最后效果:

按键操作
将以下代码添加至hichat.js的init方法中,这样在输入昵称后,按回车键即可登录,进入聊天界面后,回车键可发送消息。
document.getElementById('nicknameInput').addEventListener('keyup', function(e) {
if (e.keyCode == 13) {
var nickName = document.getElementById('nicknameInput').value;
if (nickName.trim().length != 0) {
that.socket.emit('login', nickName);
};
};
}, false);
document.getElementById('messageInput').addEventListener('keyup', function(e) {
var messageInput = document.getElementById('messageInput'),
msg = messageInput.value,
color = document.getElementById('colorStyle').value;
if (e.keyCode == 13 && msg.trim().length != 0) {
messageInput.value = '';
that.socket.emit('postMsg', msg, color);
that._displayNewMsg('me', msg, color);
};
}, false);
部署上线与云服务选择
部署上线
最后一步,当然就是将我们的辛勤结晶部署到实际站点。这应是最激动人心也是如释重负的一刻。但在此之前,让我们先添加一个Node.js程序通用的package.json文件,该文件中可指定我们的程序使用了哪些模块,这样他人获取到代码后,只需通过npm install命令即可自行下载安装程序中所需的模块,而无需我们将模块随源码一起发布。
添加package.json文件
将以下代码保存为package.json保存至与server.js相同的位置。
{
"name": "hichat",
"description": "a realtime chat web application",
"version": "0.4.0",
"main": "server.js",
"dependencies": {
"express": "3.4.x",
"socket.io": "0.9.x"
},
"engines": {
"node": "0.10.x",
"npm": "1.2.x"
}
}
云服务选择与部署
首先需选择一个支持Node.js同时又支持WebSocket协议的云服务器。因仅用于测试,空间内存限制等皆无所谓,只要免费即可。Node.js在GitHub的Wiki页面上列出了众多支持Node.js环境的云服务器,选来选去满足条件的只有Heroku。
若此前到Heroku部署过相关Node程序,必知其繁琐之处,且出错时极难调试。不过当我撰写此博客时,发现了一个利器Codeship,将其与你的GitHub绑定后,每次提交新代码它会自动部署至Heroku。无需任何额外操作!
代码更新、环境设置、编译部署,全部自动搞定,并提供详细的log信息及各步骤的状态信息。使用方法也很简单,注册后按提示,两三步搞定。鉴于本文已足够长,应创纪录了,此处不再赘述。

已知问题与总结展望
已知问题
部署测试后,发现一些本地未出现的问题,主要有以下几点:
- 首次连接过慢,有时会失败出现503错误。经查Heroku文档,官方表示程序首次接入时受资源限制确实会很慢,这是使用免费套餐的必然结果。不过用于线上测试这点尚能忍受。
- 发送表情时,Chrome会向服务器重新请求已下载至客户端的gif图片,而IE和FF无此问题,导致在Chrome里表情会有延迟,进而出现聊天主信息窗口滚动不及时的现象。
- 用户未活动一定时间后会与服务器失连,Socket自动断开。不知是Socket.io内部机制还是又是Heroku所致。
总结展望
经过上述一番折腾,一个基本的聊天程序便打造完毕。可完善之处仍有许多,例如利用CSS3动画,完全可以制作出窗口抖动功能。听起来很不错吧。同时利用HTML5的Audio API,要实现类似微信的语音消息也并非不可能,够震撼吧。甚至还有Geolocation API,我们便可联想到实现同城功能。利用Webcam可打造出视频对聊,但此方面WebRTC已做得非常出色。
PS:做程序员前有两个想法,一是写个播放器,一是写个聊天程序,现在圆满了。
源码下载:demo
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
在 NestJS 中使用管道验证数据与守卫控制访问权限
2026-11-05 12:43:41
Node.js后端生成高强度随机字符串的方法
2026-11-04 16:29:54
Node.js环境中怎么使用axios或http模块发送GET请求
2026-11-03 21:57:15
Node.js环境下怎么实现JS反混淆自动化?常用库与CLI工具推荐
2026-11-03 21:15:54
解决JS逆向常见报错:缺少DOM环境导致代码运行失败
2026-11-03 20:41:59
