Node.js(Express4.x)搭建聊天室3——完善网页

0.目标与前置条件

这一节,我将完全实现聊天室的全部页面显示和响应。

Socket.io聊天室Demo

这一节内容是在上两节完成的情况下进行的,请先参照第一节完成基本框架的搭建:
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


输入不同的用户昵称后,进入聊天室:

输入不同昵称

先进入的用户在其他用于进入时,会收到系统公告:

新用户加入的公告

如果用户昵称与之前的重名,将会:

昵称重名的情况

用户可以更改昵称,如果成功,会收到提示;其他用户也会通过公告的形式收到提醒。

更改昵称成功

如果失败,用户会收到提示

更改昵称失败

用户聊天时,在输入框中输入消息,点击发送后,在聊天记录面板中会有对应的显示。

Socket.io聊天室Demo

当一个用户离开聊天室了,其他用户会收到消息:

用户离开聊天室

所有的系统公告会保留在底部:

系统公告

结语

至此,一个相对饱满一些的聊天室就搭建好了。当然,即使“相对饱满”,依然是很简陋的聊天室。接下来如果要丰满这个聊天室、乃至集成到我们的其他应用中,还是有很多工作可以做的,比如:

  • 支持房间管理。用户可以创建房间,可以选择进入某一个房间
  • 用户管理。用户可以注册帐户、登录帐户,这个涉及到数据库
  • 聊天记录。保存聊天记录
  • 图片、文件发送。允许用户发送图片或其他文件
  • ...

要做好一个聊天室并不容易,但如果我们把它分解成一个个独立的分支,再逐一实现它,就不会那么茫然和不知所措了。

最后,欢迎fork或star我的项目:

https://github.com/KKDestiny/chatroom.git


原创文章,未经许可,请勿转载
作者:Mike的读书季
日期:2016.09.29

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容