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