【實作筆記】利用Node.js及Socket.io製作簡易聊天室﹙中﹚

【實作筆記】利用Node.js及Socket.io製作簡易聊天室﹙中﹚

承接著上一篇的教學,假設大家已經看過而且也跟著一起做完了那個最簡單版本的聊天室,但那個所謂的聊天室也確實太過簡單了。身為一個聊天室,它沒有前人留下的記錄也沒有每個使用者專屬的個人暱稱。所以現在我們再來改良一下,並且增加一些新功能吧。

上回提要



首先第一個打算加入的功能是個人暱稱,因為這個功能比較簡單,不到十五分鐘就可以把它完成了。

views.index.html,我們先加入和修改以下的代碼:

// 加入 Cookies
function setCookie(cname, cvalue, exdays) {
    var d = new Date();
    d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
    var expires = "expires="+d.toUTCString();
    document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}

function getCookie(cname) {
    var name = cname + "=";
    var ca = document.cookie.split(';');
    for(var i = 0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == ' ') {
            c = c.substring(1);
        }
        if (c.indexOf(name) == 0) {
            return c.substring(name.length, c.length);
        }
    }
    return "";
}

// ...
var content = document.getElementById("content");
// 加入下面這些
var nameInputBox = document.getElementById("name");
var name = getCookie("name");

if (name) {
    nameInputBox.value = name;
}

// ....
if (ok) socket.emit("send", formData); // 這行替換成下面的程式片段

if (ok) {
    socket.emit("send", formData);
    setCookie("name", nameInputBox.value);
}

這部分我們新增了cookies的存取,以用來實現我們的個人暱稱記錄功能。這功能會在第一次成功送出時,將個人暱稱存入到cookies之中,之後用同一個瀏覽器進入聊天室,名稱輸入框內的名稱將會維持與先前所輸入的保持一樣。



聊天記錄

請加入新的檔案名為records.js

const {EventEmitter} = require("events");

let instance;
let data = [];
let MAX = 50;

class Records extends EventEmitter {
    constructor () {
        super();
    }

    push (msg) {
        data.push(msg);

        if (data.length > MAX) {
            data.splice(0, 1);
        }

        this.emit("new_message", msg);
    }

    get () {
        return data;
    }

    setMax (max) {
        MAX = max;
    }

    getMax () {
        return MAX;
    }
}

module.exports = (function () {
    if (!instance) {
        instance = new Records();
    }

    return instance;
})();

index.js修改以下的代碼:

const server = require('http').Server(app);
const io = require('socket.io')(server);
const records = require('./records.js'); // 新增這行

// ...

io.on('connection', (socket) => {
    // 有連線發生時增加人數
    onlineCount++;
    // 發送人數給網頁
    io.emit("online", onlineCount);
    socket.emit("maxRecord", records.getMax());   // 新增記錄最大值,用來讓前端網頁知道要放多少筆
    socket.emit("chatRecord", records.get());     // 新增發送紀錄

    socket.on("send", (msg) => {
        // 如果 msg 內容鍵值小於 2 等於是訊息傳送不完全
        // 因此我們直接 return ,終止函式執行。
        if (Object.keys(msg).length < 2) return;
        records.push(msg);
        //io.emit("msg", msg); // 這行刪除改由 Records 事件接手
    });

    socket.on('disconnect', () => {
        // 有人離線了,扣人
        onlineCount = (onlineCount < 0) ? 0 : onlineCount-=1;
        io.emit("online", onlineCount);
    });
});

// 新增 Records 的事件監聽器
records.on("new_message", (msg) => {
    // 廣播訊息到聊天室
    io.emit("msg", msg);
});

另外在index.html中,加入以下的代碼:

document.addEventListener("DOMContentLoaded", () => {
    var max_record; // 新增
    // ...

    // 加入新的事件監聽器  
    socket.on("chatRecord", function (msgs) {
        for (var i=0; i < msgs.length; i++) {
            (function () {
                addMsgToBox(msgs[i]);
            })();
        }
    });

    socket.on("maxRecord", function (amount) {
        max_record = amount;
    });

    // 修改 msg 事件監聽器
    socket.on("msg", addMsgToBox);

    // 新增兩個 function
    // 新增訊息到方框中
    function addMsgToBox (d) {
        var msgBox = document.createElement("div")
            msgBox.className = "msg";
        var nameBox = document.createElement("span");
            nameBox.className = "name";
        var name = document.createTextNode(d.name);
        var msg = document.createTextNode(d.msg);

        nameBox.appendChild(name);
        msgBox.appendChild(nameBox);
        msgBox.appendChild(msg);
        content.appendChild(msgBox);

        if (content.children.length > max_record) {
            rmMsgFromBox();
        }
    }

    // 移除多餘的訊息
    function rmMsgFromBox () {
        var childs = content.children;
        childs[0].remove();
    }
}

在這邊,我們新增了一個Records物件,它會將聊天記錄暫存於記憶體當中,而每當它接收到新訊息的時候便會通知伺服器主程式,然後伺服器再透過Websocket將訊息傳出去。此外,這個物件是全域型的物件,不過只會存在一個實體,是一種俗稱Singleton的一種程式設計方式。

而前端的調整主要是在第一次進入畫面,收到紀錄資料時的顯示方式,以及聊天筆數太多時要除掉舊的記錄這樣。

好了,啟動伺服器,連到http://localhost:80,大家再來測試吧!



這一篇教學就在這裡完結,如果你也有跟著時實做完文章內容,你應該會看到這樣的完整程式碼:

index.js

const express = require('express');
const app = express();
const server = require('http').Server(app);
const io = require('socket.io')(server);
const path = require('path');
const records = require('./records.js');
const port = process.env.PORT || 80;

// 加入線上人數計數
let onlineCount = 0;

app.use(express.static(path.join(__dirname, 'views')))

app.get('/', (req, res) => {
    res.sendFile( __dirname + '/views/index.html');
});

io.on('connection', (socket) => {
    // 有連線發生時增加人數
    onlineCount++;
    // 發送人數給網頁
    io.emit("online", onlineCount);
    // 發送紀錄最大值
    socket.emit("maxRecord", records.getMax());
    // 發送紀錄
    socket.emit("chatRecord", records.get());

    socket.on("greet", () => {
        socket.emit("greet", onlineCount);
    });

    socket.on("send", (msg) => {
        // 如果 msg 內容鍵值小於 2 等於是訊息傳送不完全
        // 因此我們直接 return ,終止函式執行。
        if (Object.keys(msg).length < 2) return;
        records.push(msg);
    });

    socket.on('disconnect', () => {
        // 有人離線了,扣人
        onlineCount = (onlineCount < 0) ? 0 : onlineCount-=1;
        io.emit("online", onlineCount);
    });
});

records.on("new_message", (msg) => {
    // 廣播訊息到聊天室
    io.emit("msg", msg);
});

server.listen(port, () => {
    console.log('System Message: server started on http://localhost:' port);
});



records.js

const {EventEmitter} = require("events");

let instance;
let data = [];
let MAX = 50;

class Records extends EventEmitter {
    constructor () {
        super();
    }

    push (msg) {
        data.push(msg);

        if (data.length > MAX) {
            data.splice(0, 1);
        }

        this.emit("new_message", msg);
    }

    get () {
        return data;
    }

    setMax (max) {
        MAX = max;
    }

    getMax () {
        return MAX;
    }
}

module.exports = (function () {
    if (!instance) {
        instance = new Records();
    }

    return instance;
})();



views/index.html

<!DOCTYPE html>
<html lang="zh-tw">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SimpleChat</title>
    <link rel="stylesheet" type="text/css" href="/css/style.css" />
    <script src="/socket.io/socket.io.js"></script>
    <script>
        var socket = io();
    </script>
</head>
<body>
    <div id="container">
        <div id="status-box">Server: <span id="status">-</span> / <span id="online">0</span> online.</div>
        <div id="content">
        </div>
        <div id="send-box">
            <form id="send-form">
                <input type="text" name="name" id="name" placeholder="Nickname">
                <input type="text" name="msg" id="msg" placeholder="Enter your message ...">
                <input type="submit" value="Send">
            </form>
        </div>
    </div>

    <script>
        document.addEventListener("DOMContentLoaded", () => {
            var max_record;

            var status = document.getElementById("status");
            var online = document.getElementById("online");
            var sendForm = document.getElementById("send-form");
            var content = document.getElementById("content");
            var nameInputBox = document.getElementById("name");
            var name = getCookie("name");

            if (name) {
                nameInputBox.value = name;
            }

            socket.on("connect", function () {
                status.innerText = "Connected.";
            });

            socket.on("disconnect", function () {
                status.innerText = "Disconnected.";
            });

            socket.on("online", function (amount) {
                online.innerText = amount;
            });

            socket.on("maxRecord", function (amount) {
                max_record = amount;
            });

            socket.on("chatRecord", function (msgs) {
                for (var i=0; i < msgs.length; i++) {
                    (function () {
                        addMsgToBox(msgs[i]);
                    })();
                }
            });

            socket.on("msg", addMsgToBox);

            sendForm.addEventListener("submit", function (e) {
                e.preventDefault();


                var ok = true;
                var formData = {
                    time: new Date().toUTCString()
                };
                var formChild = sendForm.children;

                for (var i=0; i< sendForm.childElementCount; i++) {
                    var child = formChild[i];
                    if (child.name !== "") {
                        var val = child.value;
                        if (val === "" || !val) {
                            ok = false;
                            child.classList.add("error");
                        } else {
                            child.classList.remove("error");
                            formData[child.name] = val;
                        }
                    }
                }

                if (ok) {
                    socket.emit("send", formData);
                    setCookie("name", nameInputBox.value);
                }
            });

            function addMsgToBox (d) {
                var msgBox = document.createElement("div")
                    msgBox.className = "msg";
                var nameBox = document.createElement("span");
                    nameBox.className = "name";
                var name = document.createTextNode(d.name);
                var msg = document.createTextNode(d.msg);

                nameBox.appendChild(name);
                msgBox.appendChild(nameBox);
                msgBox.appendChild(msg);
                content.appendChild(msgBox);

                if (content.children.length > max_record) {
                    rmMsgFromBox();
                }
            }

            function rmMsgFromBox () {
                var childs = content.children;
                childs[0].remove();
            }

            function setCookie(cname, cvalue, exdays) {
                var d = new Date();
                d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
                var expires = "expires="+d.toUTCString();
                document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
            }

            function getCookie(cname) {
                var name = cname + "=";
                var ca = document.cookie.split(';');
                for(var i = 0; i < ca.length; i++) {
                    var c = ca[i];
                    while (c.charAt(0) == ' ') {
                        c = c.substring(1);
                    }
                    if (c.indexOf(name) == 0) {
                        return c.substring(name.length, c.length);
                    }
                }
                return "";
            }
        });
    </script>
</body>
</html>



結語

在這篇教學中,我們已經成功地讓聊天訊息存在記憶體中,但重啟伺服器的話,這些存在記憶體中的暫時資料便會消失。為了解決每次重開伺服器聊天資料就被消失的問題,我們可以為服務加入資料庫系統,有興趣的話可以繼續閱讀這篇文章:

Modified by Alvin Au © 2023