0.目標與前置條件
這一節(jié)琅绅,我將完全實現(xiàn)聊天室的全部頁面顯示和響應(yīng)奏赘。
這一節(jié)內(nèi)容是在上兩節(jié)完成的情況下進行的,請先參照第一節(jié)完成基本框架的搭建:
Node.js(Express4.x)搭建聊天室1——基本框架
并參照第二節(jié)添加幾個監(jiān)聽:
Node.js(Express4.x)搭建聊天室2——消息發(fā)送與監(jiān)聽
我把搭建聊天室的步驟分成了幾個部分蛮浑,請按順序閱讀:
獲取代碼
Node.js(Express4.x)搭建聊天室1——基本框架
Node.js(Express4.x)搭建聊天室2——消息發(fā)送與監(jiān)聽
Node.js(Express4.x)搭建聊天室3——完善網(wǎng)頁
1.服務(wù)端
1.1 chatroom.js
在之前的幾節(jié)中迎献,我們已經(jīng)搭建了chatroom的簡易版,但如果進入的用戶昵稱重復(fù)了值朋,我們也不能作出判斷和處理叹侄;此外,當(dāng)用戶修改昵稱時昨登,也有可能出現(xiàn)用戶昵稱重復(fù)的情況趾代。(在上一節(jié),我把它定義為了一個對象)
所以丰辣,在這一節(jié)撒强,我增加了一個數(shù)組來存儲用戶昵稱。
var userlist = new Array();
在用戶加入聊天時笙什,將昵稱存入該數(shù)組飘哨,如果用戶的昵稱已存在,則在此昵稱后增加一個隨機數(shù)來保證昵稱不同得湘。
/* *************** 用戶emit消息"join"時杖玲,響應(yīng) *************** */
socket.on('join', function (username) {
if (addedUser) return;
// 用戶信息存儲在socket會話中:在此之前,要檢查是否重復(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ù)組
...
});
在用戶修改昵稱時淘正,在上一節(jié)是直接將socket.name替換為新的昵稱的摆马。而現(xiàn)在,首先檢查數(shù)組中是否存在這個昵稱鸿吆,如果沒有囤采,則替換,否則提示用戶修改失敗惩淳。
/* *************** 更改昵稱 *************** */
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
});
}
});
此外蕉毯,為了維護昵稱數(shù)組乓搬,還需要在用戶離開時,將離開的用戶剔除出昵稱數(shù)組代虾。為了達到這個目的进肯,我增加了一個函數(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頁面進行操作了棉磨。這一節(jié)江掩,我把index界面改為了一個輸入用戶昵稱的界面,然后跳轉(zhuǎn)到一個新界面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文件夾下創(chuàng)建一個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')
這里我們引用了一個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代碼火本。
首先,加入基本功能函數(shù)蓄喇,用于此頁面的一些基礎(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客戶端代碼進行修改后的內(nèi)容:
script.
////////////////////////////////////////////////////////////////////
//啟動
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_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īng)用(supervisor bin/www 或 node bin/www)
打開兩個瀏覽器,進入127.0.0.1:3000
輸入不同的用戶昵稱后禀苦,進入聊天室:
先進入的用戶在其他用于進入時蔓肯,會收到系統(tǒng)公告:
如果用戶昵稱與之前的重名,將會:
用戶可以更改昵稱振乏,如果成功蔗包,會收到提示;其他用戶也會通過公告的形式收到提醒慧邮。
如果失敗调限,用戶會收到提示
用戶聊天時舟陆,在輸入框中輸入消息,點擊發(fā)送后耻矮,在聊天記錄面板中會有對應(yīng)的顯示秦躯。
當(dāng)一個用戶離開聊天室了,其他用戶會收到消息:
所有的系統(tǒng)公告會保留在底部:
結(jié)語
至此淘钟,一個相對飽滿一些的聊天室就搭建好了宦赠。當(dāng)然,即使“相對飽滿”颗圣,依然是很簡陋的聊天室芬迄。接下來如果要豐滿這個聊天室、乃至集成到我們的其他應(yīng)用中,還是有很多工作可以做的萎坷,比如:
- 支持房間管理。用戶可以創(chuàng)建房間僧著,可以選擇進入某一個房間
- 用戶管理既绩。用戶可以注冊帳戶、登錄帳戶丐谋,這個涉及到數(shù)據(jù)庫
- 聊天記錄芍碧。保存聊天記錄
- 圖片、文件發(fā)送号俐。允許用戶發(fā)送圖片或其他文件
- ...
要做好一個聊天室并不容易泌豆,但如果我們把它分解成一個個獨立的分支,再逐一實現(xiàn)它吏饿,就不會那么茫然和不知所措了踪危。
最后,歡迎fork或star我的項目:
原創(chuàng)文章猪落,未經(jīng)許可贞远,請勿轉(zhuǎn)載
作者:Mike的讀書季
日期:2016.09.29