0.目标与前置条件
这一节,我将完全实现聊天室的全部页面显示和响应。
这一节内容是在上两节完成的情况下进行的,请先参照第一节完成基本框架的搭建:
Node.js(Express4.x)搭建聊天室1——基本框架
并参照第二节添加几个监听:
Node.js(Express4.x)搭建聊天室2——消息发送与监听
我把搭建聊天室的步骤分成了几个部分,请按顺序阅读:
获取代码
Node.js(Express4.x)搭建聊天室1——基本框架
Node.js(Express4.x)搭建聊天室2——消息发送与监听
Node.js(Express4.x)搭建聊天室3——完善网页
1.服务端
1.1 chatroom.js
在之前的几节中,我们已经搭建了chatroom的简易版,但如果进入的用户昵称重复了,我们也不能作出判断和处理;此外,当用户修改昵称时,也有可能出现用户昵称重复的情况。(在上一节,我把它定义为了一个对象)
所以,在这一节,我增加了一个数组来存储用户昵称。
var userlist = new Array();
在用户加入聊天时,将昵称存入该数组,如果用户的昵称已存在,则在此昵称后增加一个随机数来保证昵称不同。
/* *************** 用户emit消息"join"时,响应 *************** */
socket.on('join', function (username) {
if (addedUser) return;
// 用户信息存储在socket会话中:在此之前,要检查是否重复
for(var i=0; i<userlist.length; i++) {
if(userlist[i] == username) {
username = username+Math.ceil(Math.random()*10000);
break;
}
}
...
userlist.push(username) // 将昵称加入数组
...
});
在用户修改昵称时,在上一节是直接将socket.name替换为新的昵称的。而现在,首先检查数组中是否存在这个昵称,如果没有,则替换,否则提示用户修改失败。
/* *************** 更改昵称 *************** */
socket.on('change_name', function (newname) {
if (addedUser) {
var oldname = socket.username;
// ************************** 这里开始本节更新 **************************
for(var i=0; i<userlist.length; i++) {
if(userlist[i] == newname) {
// 通知该用户修改成功
socket.emit('name_changed_msg', {
res: "failed",
error: "已有此用户:"+newname,
oldname: oldname,
newname: newname,
type: "RETURN"
});
return -1;
}
}
// 通知该用户修改成功
socket.emit('name_changed_msg', {
res: "success",
error: null,
oldname: oldname,
newname: newname,
msg: "["+oldname+"] 改名为 ["+socket.username + "]",
type: "RETURN",
numUsers: guest_num
});
for(var i=0; i<userlist.length; i++) {
if(userlist[i] == oldname) {
userlist[i] = newname;
socket.username = newname;
}
}
// ************************** 这里结束本节更新 **************************
// 告知所有用户
socket.broadcast.emit('name_changed', {
username: newname,
msg: "["+oldname+"] 改名为 ["+socket.username + "]",
type: "BROADCAST",
numUsers: guest_num
});
}
});
此外,为了维护昵称数组,还需要在用户离开时,将离开的用户剔除出昵称数组。为了达到这个目的,我增加了一个函数来实现:
// 移除数组元素
var removeArr = function(arr, ele) {
var new_arr = new Array();
for(var i=0; i<arr.length; i++) {
if(ele != arr[i]) {
new_arr.push(arr[i])
}
}
return new_arr;
}
用户离开聊天室:
/* *************** 用户离开 *************** */
socket.on('disconnect', function () {
...
// 将离开的用户昵称移出数组
userlist = removeArr(userlist, socket.username)
// 告知所有用户
...
}
});
1.2 路由
在上一节,我们直接就在index页面进行操作了。这一节,我把index界面改为了一个输入用户昵称的界面,然后跳转到一个新界面other。要在routes/index.js中增加一个路由:
router.get('/other', function(req, res, next) {
res.render('other', { title: 'Express' });
});
2. 客户端
2.1 更改index.jade页面
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
h1 欢迎使用socket.io聊天室
form(method='get' action='/other')
input(id='name' name='name' placeholder='输入您的名字')
input(type='submit' value='进入聊天室')
2.2 新增other.jade页面
然后在views/index文件夹下创建一个other.jade文件:
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
link(rel='stylesheet', href='/stylesheets/bootstrap.css')
body
h1 socket.io聊天室
p
span#status
span ,
span#roomstatus
p#notice.notice
a(href='/')
[退出] 聊天室
hr
div
h3 聊天记录
div.scrollbar#msg.msgbox
hr
div
textarea(id='msgsend' name='msgsend' placeholder='输入消息' rows='4').form-control
br
div
a.btn.btn-primary(onclick="OL_SendMsg()") 发送
hr
form.form-inline
div.form-group
input.form-control(id='newnickname' placeholder='新昵称')
a.btn.btn-danger(onclick="OL_ModifyNickName()") 修改昵称
hr
h3 系统消息
div#history
script(src='/javascripts/jquery.min.js')
script(src='https://cdn.socket.io/socket.io-1.4.5.js')
这里我们引用了一个Bootstrap的css文件,请自行下载,并放入public/stylesheets文件夹中。
另外,我们还需要对css文件进行一下替换:
body {
padding: 50px;
font: 14px "Microsoft Yahei", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}
.msgbox {
height:300px;
overflow-x:auto;
overflow-y:auto;
border:1px #ccc solid;
border-radius:5px;
background:#fff;
padding:14px 20px;
}
.notice {
color:#EF0000;
font-weight:bold;
}
.time {
float:right;
color:#999;
}
.mymsg {
color:#2289DB;
font-weight:bold;
}
/* 滚动条 */
.scrollbar::-webkit-scrollbar-track
{
background-color: #e1e1e1;
}
.scrollbar::-webkit-scrollbar
{
width: 10px;
background-color: #e1e1e1;
}
.scrollbar.shortscroll::-webkit-scrollbar
{
width: 8px;
background-color: #e1e1e1;
}
.scrollbar::-webkit-scrollbar-thumb
{
background-color: #888;
}
2.3 other.jade页面的js代码
在other.jade页面中,加入一些js代码。
首先,加入基本功能函数,用于此页面的一些基础功能
script.
// 基本功能函数
function ol_pad(num, n)
{
num = ""+num
var temp = num;
for(var i=0;i<(n-num.length);i++)
{
temp = "0"+temp
}
return temp
}
function GetRequest() {
var url = location.search; //获取url中"?"符后的字串
var theRequest = new Object();
if (url.indexOf("?") != -1) {
var str = url.substr(1);
strs = str.split("&");
for(var i = 0; i < strs.length; i ++) {
theRequest[strs[i].split("=")[0]]=unescape(strs[i].split("=")[1]);
}
}
return theRequest;
}
function GetDateTime() {
var obj = new Date();
return (obj.getFullYear()+"/"+ol_pad(obj.getMonth()+1, 2)+"/"+ol_pad(obj.getDate(), 2)+" "+ol_pad(obj.getHours(),2)+":"+ol_pad(obj.getMinutes(),2)+":"+ol_pad(obj.getSeconds(),2));
}
发送聊天信息后,触发的一些响应,包括发送消息、在聊天框中显示、清空输入框等。
script.
// 发送聊天信息
function OL_CleanInput() {
var obj = document.getElementById('msgsend');
obj.value = "";
}
function OL_ScrollChatWin() {
var obj = document.getElementById('msg');
obj.scrollTop = obj.scrollHeight;
}
function OL_SentAction() {
OL_ScrollChatWin();
OL_CleanInput();
}
function OL_CleanNotice() {
document.getElementById("notice").innerHTML = "";
}
function OL_SendMsg() {
var msg = document.getElementById("msgsend").value;
if(""==msg) {
alert("消息不能为空!")
return -1;
}
send_msg(msg);
document.getElementById("msg").innerHTML += "<p class='mymsg'>"+G_Name+": "+msg+"<span class='time'>"+GetDateTime()+"</span></p>";
OL_SentAction();
}
修改昵称后的响应
script.
// 修改昵称
function OL_ModifyNickName() {
var newnickname = document.getElementById("newnickname").value;
if(""==newnickname) {
alert("新昵称不能为空!")
return -1;
}
change_name(newnickname);
document.getElementById("newnickname").value = "";
}
显示系统公告
script.
// 通知
var NoticeTimer = null;
function OL_ShowNotice(msg, second) {
NoticeTimer = null;
document.getElementById("notice").innerHTML = "[消息] "+msg;
NoticeTimer = setTimeout("OL_CleanNotice()", second*1000)
var history = document.getElementById("history");
history.innerHTML = "<p>[消息] "+msg+"<span class='time'>"+GetDateTime()+"</span></p>" + history.innerHTML
}
这部分是根据上一节index.jade的socket.io客户端代码进行修改后的内容:
script.
////////////////////////////////////////////////////////////////////
//启动
var socket = io.connect('http://127.0.0.1:3000');
//发送消息
var Request = new Object();
Request = GetRequest();
var G_Name = Request["name"];
if(null==G_Name) {
G_Name = "访客"+Math.ceil(Math.random()*10000);
}
socket.emit('join', G_Name, function (data) {
console.log(data);
});
//监听
socket.on('login', function (data) {
console.log(data);
// 如果有重名的,要更改一个随机名称
G_Name = data.username;
document.getElementById("status").innerHTML = "欢迎您!"+G_Name;
document.getElementById("roomstatus").innerHTML = "当前聊天有"+data.numUsers+"人";
});
socket.on('user_joined', function (data) {
console.log(data);
OL_ShowNotice(data.msg, 3);
document.getElementById("roomstatus").innerHTML = "当前聊天有"+data.numUsers+"人";
});
socket.on('user_left', function (data) {
console.log(data);
OL_ShowNotice(data.msg, 3);
document.getElementById("roomstatus").innerHTML = "当前聊天有"+data.numUsers+"人";
});
//修改昵称
function change_name(name){
socket.emit('change_name', name, function (data) {
console.log(data);
});
}
// 监听修改昵称后返回的消息
socket.on('name_changed', function (data) {
console.log(data);
document.getElementById("status").innerHTML = "欢迎您!"+G_Name;
OL_ShowNotice(data.msg, 3);
});
// 监听修改昵称后返回给修改者的消息
socket.on('name_changed_msg', function (data) {
console.log(data);
if("success"==data.res) {
document.getElementById("status").innerHTML = "欢迎您!"+data.newname;
OL_ShowNotice(data.msg, 3);
}
else {
OL_ShowNotice("修改昵称失败!"+data.error, 3);
}
});
//发送消息
function send_msg(msg){
socket.emit('send_msg', msg, function (data) {
console.log(data);
});
}
// 监听消息
socket.on('msg_sent', function (data) {
console.log(data);
document.getElementById("msg").innerHTML += "<p>"+data.username+": "+data.msg+"<span class='time'>"+GetDateTime()+"</span></p>";
OL_ScrollChatWin();
});
3.演示
运行应用(supervisor bin/www 或 node bin/www)
打开两个浏览器,进入127.0.0.1:3000
输入不同的用户昵称后,进入聊天室:
先进入的用户在其他用于进入时,会收到系统公告:
如果用户昵称与之前的重名,将会:
用户可以更改昵称,如果成功,会收到提示;其他用户也会通过公告的形式收到提醒。
如果失败,用户会收到提示
用户聊天时,在输入框中输入消息,点击发送后,在聊天记录面板中会有对应的显示。
当一个用户离开聊天室了,其他用户会收到消息:
所有的系统公告会保留在底部:
结语
至此,一个相对饱满一些的聊天室就搭建好了。当然,即使“相对饱满”,依然是很简陋的聊天室。接下来如果要丰满这个聊天室、乃至集成到我们的其他应用中,还是有很多工作可以做的,比如:
- 支持房间管理。用户可以创建房间,可以选择进入某一个房间
- 用户管理。用户可以注册帐户、登录帐户,这个涉及到数据库
- 聊天记录。保存聊天记录
- 图片、文件发送。允许用户发送图片或其他文件
- ...
要做好一个聊天室并不容易,但如果我们把它分解成一个个独立的分支,再逐一实现它,就不会那么茫然和不知所措了。
最后,欢迎fork或star我的项目:
原创文章,未经许可,请勿转载
作者:Mike的读书季
日期:2016.09.29