0.目標(biāo)與前置條件
這一節(jié),我將完全實(shí)現(xiàn)聊天室的全部頁(yè)面顯示和響應(yīng)。

這一節(jié)內(nèi)容是在上兩節(jié)完成的情況下進(jìn)行的,請(qǐng)先參照第一節(jié)完成基本框架的搭建:
Node.js(Express4.x)搭建聊天室1——基本框架
并參照第二節(jié)添加幾個(gè)監(jiān)聽:
Node.js(Express4.x)搭建聊天室2——消息發(fā)送與監(jiān)聽
我把搭建聊天室的步驟分成了幾個(gè)部分,請(qǐng)按順序閱讀:
獲取代碼
Node.js(Express4.x)搭建聊天室1——基本框架
Node.js(Express4.x)搭建聊天室2——消息發(fā)送與監(jiān)聽
Node.js(Express4.x)搭建聊天室3——完善網(wǎng)頁(yè)
1.服務(wù)端
1.1 chatroom.js
在之前的幾節(jié)中,我們已經(jīng)搭建了chatroom的簡(jiǎn)易版,但如果進(jìn)入的用戶昵稱重復(fù)了,我們也不能作出判斷和處理;此外,當(dāng)用戶修改昵稱時(shí),也有可能出現(xiàn)用戶昵稱重復(fù)的情況。(在上一節(jié),我把它定義為了一個(gè)對(duì)象)
所以,在這一節(jié),我增加了一個(gè)數(shù)組來(lái)存儲(chǔ)用戶昵稱。
var userlist = new Array();
在用戶加入聊天時(shí),將昵稱存入該數(shù)組,如果用戶的昵稱已存在,則在此昵稱后增加一個(gè)隨機(jī)數(shù)來(lái)保證昵稱不同。
/* *************** 用戶emit消息"join"時(shí),響應(yīng) *************** */
socket.on('join', function (username) {
if (addedUser) return;
// 用戶信息存儲(chǔ)在socket會(huì)話中:在此之前,要檢查是否重復(fù)
for(var i=0; i<userlist.length; i++) {
if(userlist[i] == username) {
username = username+Math.ceil(Math.random()*10000);
break;
}
}
...
userlist.push(username) // 將昵稱加入數(shù)組
...
});
在用戶修改昵稱時(shí),在上一節(jié)是直接將socket.name替換為新的昵稱的。而現(xiàn)在,首先檢查數(shù)組中是否存在這個(gè)昵稱,如果沒(méi)有,則替換,否則提示用戶修改失敗。
/* *************** 更改昵稱 *************** */
socket.on('change_name', function (newname) {
if (addedUser) {
var oldname = socket.username;
// ************************** 這里開始本節(jié)更新 **************************
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;
}
}
// ************************** 這里結(jié)束本節(jié)更新 **************************
// 告知所有用戶
socket.broadcast.emit('name_changed', {
username: newname,
msg: "["+oldname+"] 改名為 ["+socket.username + "]",
type: "BROADCAST",
numUsers: guest_num
});
}
});
此外,為了維護(hù)昵稱數(shù)組,還需要在用戶離開時(shí),將離開的用戶剔除出昵稱數(shù)組。為了達(dá)到這個(gè)目的,我增加了一個(gè)函數(shù)來(lái)實(shí)現(xiàn):
// 移除數(shù)組元素
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 () {
...
// 將離開的用戶昵稱移出數(shù)組
userlist = removeArr(userlist, socket.username)
// 告知所有用戶
...
}
});
1.2 路由
在上一節(jié),我們直接就在index頁(yè)面進(jìn)行操作了。這一節(jié),我把index界面改為了一個(gè)輸入用戶昵稱的界面,然后跳轉(zhuǎn)到一個(gè)新界面other。要在routes/index.js中增加一個(gè)路由:
router.get('/other', function(req, res, next) {
res.render('other', { title: 'Express' });
});
2. 客戶端
2.1 更改index.jade頁(yè)面
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='進(jìn)入聊天室')
2.2 新增other.jade頁(yè)面
然后在views/index文件夾下創(chuàng)建一個(gè)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()") 發(fā)送
hr
form.form-inline
div.form-group
input.form-control(id='newnickname' placeholder='新昵稱')
a.btn.btn-danger(onclick="OL_ModifyNickName()") 修改昵稱
hr
h3 系統(tǒng)消息
div#history
script(src='/javascripts/jquery.min.js')
script(src='https://cdn.socket.io/socket.io-1.4.5.js')
這里我們引用了一個(gè)Bootstrap的css文件,請(qǐng)自行下載,并放入public/stylesheets文件夾中。
另外,我們還需要對(duì)css文件進(jìn)行一下替換:
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;
}
/* 滾動(dòng)條 */
.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頁(yè)面的js代碼
在other.jade頁(yè)面中,加入一些js代碼。
首先,加入基本功能函數(shù),用于此頁(yè)面的一些基礎(chǔ)功能
script.
// 基本功能函數(shù)
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));
}
發(fā)送聊天信息后,觸發(fā)的一些響應(yīng),包括發(fā)送消息、在聊天框中顯示、清空輸入框等。
script.
// 發(fā)送聊天信息
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();
}
修改昵稱后的響應(yīng)
script.
// 修改昵稱
function OL_ModifyNickName() {
var newnickname = document.getElementById("newnickname").value;
if(""==newnickname) {
alert("新昵稱不能為空!")
return -1;
}
change_name(newnickname);
document.getElementById("newnickname").value = "";
}
顯示系統(tǒng)公告
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
}
這部分是根據(jù)上一節(jié)index.jade的socket.io客戶端代碼進(jìn)行修改后的內(nèi)容:
script.
////////////////////////////////////////////////////////////////////
//啟動(dòng)
var socket = io.connect('http://127.0.0.1:3000');
//發(fā)送消息
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);
});
//監(jiān)聽
socket.on('login', function (data) {
console.log(data);
// 如果有重名的,要更改一個(gè)隨機(jī)名稱
G_Name = data.username;
document.getElementById("status").innerHTML = "歡迎您!"+G_Name;
document.getElementById("roomstatus").innerHTML = "當(dāng)前聊天有"+data.numUsers+"人";
});
socket.on('user_joined', function (data) {
console.log(data);
OL_ShowNotice(data.msg, 3);
document.getElementById("roomstatus").innerHTML = "當(dāng)前聊天有"+data.numUsers+"人";
});
socket.on('user_left', function (data) {
console.log(data);
OL_ShowNotice(data.msg, 3);
document.getElementById("roomstatus").innerHTML = "當(dāng)前聊天有"+data.numUsers+"人";
});
//修改昵稱
function change_name(name){
socket.emit('change_name', name, function (data) {
console.log(data);
});
}
// 監(jiān)聽修改昵稱后返回的消息
socket.on('name_changed', function (data) {
console.log(data);
document.getElementById("status").innerHTML = "歡迎您!"+G_Name;
OL_ShowNotice(data.msg, 3);
});
// 監(jiān)聽修改昵稱后返回給修改者的消息
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);
}
});
//發(fā)送消息
function send_msg(msg){
socket.emit('send_msg', msg, function (data) {
console.log(data);
});
}
// 監(jiān)聽消息
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.演示
運(yùn)行應(yīng)用(supervisor bin/www 或 node bin/www)
打開兩個(gè)瀏覽器,進(jìn)入127.0.0.1:3000
輸入不同的用戶昵稱后,進(jìn)入聊天室:

先進(jìn)入的用戶在其他用于進(jìn)入時(shí),會(huì)收到系統(tǒng)公告:

如果用戶昵稱與之前的重名,將會(huì):

用戶可以更改昵稱,如果成功,會(huì)收到提示;其他用戶也會(huì)通過(guò)公告的形式收到提醒。

如果失敗,用戶會(huì)收到提示

用戶聊天時(shí),在輸入框中輸入消息,點(diǎn)擊發(fā)送后,在聊天記錄面板中會(huì)有對(duì)應(yīng)的顯示。

當(dāng)一個(gè)用戶離開聊天室了,其他用戶會(huì)收到消息:

所有的系統(tǒng)公告會(huì)保留在底部:

結(jié)語(yǔ)
至此,一個(gè)相對(duì)飽滿一些的聊天室就搭建好了。當(dāng)然,即使“相對(duì)飽滿”,依然是很簡(jiǎn)陋的聊天室。接下來(lái)如果要豐滿這個(gè)聊天室、乃至集成到我們的其他應(yīng)用中,還是有很多工作可以做的,比如:
- 支持房間管理。用戶可以創(chuàng)建房間,可以選擇進(jìn)入某一個(gè)房間
- 用戶管理。用戶可以注冊(cè)帳戶、登錄帳戶,這個(gè)涉及到數(shù)據(jù)庫(kù)
- 聊天記錄。保存聊天記錄
- 圖片、文件發(fā)送。允許用戶發(fā)送圖片或其他文件
- ...
要做好一個(gè)聊天室并不容易,但如果我們把它分解成一個(gè)個(gè)獨(dú)立的分支,再逐一實(shí)現(xiàn)它,就不會(huì)那么茫然和不知所措了。
最后,歡迎fork或star我的項(xiàng)目:
原創(chuàng)文章,未經(jīng)許可,請(qǐng)勿轉(zhuǎn)載
作者:Mike的讀書季
日期:2016.09.29