在上一篇Node.js 與 Socket.io – 即時聊天室文章,我就假設你看過了也做完了,我們有了一個「超級無敵簡單」的聊天室,但那實在是太簡單了、太陽春了。
身為一個聊天室,它沒有前人的紀錄,也沒有每人專屬的名稱,喔對,還不會紀錄名稱!這真是太令人失望的聊天室,實在是太失望,完全就只是個拿來作為教學用的超級無敵簡單版本。作者到底做了什麼鬼東西啊!
喔等等,作者好像就是我。
好吧,為了盡棄前嫌,我決定來改良改良,增加一些新功能。
請轉到Node.js 與 Socket.io – 即時聊天室實作收看。
第一個打算加入的是名稱,為什麼呢?因為比較簡單。
views/insdex.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);
});
views/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:3000
,試試吧!
這本篇教學中,我們已經成功地讓聊天訊息存在記憶體中,但重啟伺服器的話,這些存在記憶體中的暫時資料便會消失。為了解決每次重開伺服器聊天資料就被打掉重練的問題,我們可以為服務加入資料庫系統,有興趣的話可以繼續閱讀這篇文章:
index.js
const express = require('express');
const app = express();
const server = require('http').Server(app);
const io = require('socket.io')(server);
const records = require('./records.js');
const port = process.env.PORT || 3000;
// 加入線上人數計數
let onlineCount = 0;
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("Server Started. 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>My Chatroom</title>
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io();
</script>
<style>
html, body {
padding: 0;
margin: 0;
}
#container {
top: 50px;
width: 500px;
margin: 0 auto;
display: block;
position: relative;
}
#status-box {
text-align: right;
font-size: .6em;
}
#content {
width: 100%;
height: 350px;
border: 1px solid darkolivegreen;
border-radius: 5px;
overflow: auto;
}
#send-box {
width: 100%;
text-align: center;
}
#send-box input {
display: inline-block;
}
#send-box input.error {
border: 1px solid red;
}
input[name="name"] {
width: 15%;
}
input[name="msg"] {
width: 70%;
}
input[type="button"] {
width: 10%;
}
.msg {
width: 73%;
display: inline-block;
padding: 5px 0 5px 10px;
}
.msg > span {
width: 25%;
display: inline-block;
}
.msg > span::before {
color: darkred;
content: " { ";
}
.msg > span::after {
color: darkred;
content: " } ";
}
</style>
</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="暱稱">
<input type="text" name="msg" id="msg" placeholder="說點什麼?">
<input type="submit" value="送出">
</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>
Have Fun