Node.js(Express4.x)搭建聊天室3——完善網(wǎng)頁(yè)

0.目標(biāo)與前置條件

這一節(jié),我將完全實(shí)現(xiàn)聊天室的全部頁(yè)面顯示和響應(yīng)。

Socket.io聊天室Demo

這一節(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)的顯示。

Socket.io聊天室Demo

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

用戶離開聊天室

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

系統(tǒng)公告

結(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)目:

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


原創(chuàng)文章,未經(jīng)許可,請(qǐng)勿轉(zhuǎn)載
作者:Mike的讀書季
日期:2016.09.29

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容