Node.js - Express + MongoDB + Socket.IO (以聊天室為範例) -[2]

express 搭配 mongodb 建立聊天室,後半段。

 

有鑑於先前發過相關系列文章:

 

如果沒有安裝過 MongoDB,可以參考此篇:

 

 

 

一、前言

由於內容太多了,本系列將分成上下篇說明。

範例專案放置在 Github 上,可以直接 clone 下來試試,

本篇示範的簡易專案放在 SimpleExample 裡面,可以直接查看。

上篇:Node.js - Express + MongoDB + Socket.IO (以聊天室為範例) -[1]

 

上篇利用 socket.io 進行事件廣播

本篇將利用 mongodb,把訊息存進資料庫裡。

 

 

 

二、資料庫連線與訊息讀寫

由於本範例資料庫的存取,只有在 socket 訊息交換時候,

因此在設計上將此部分抽離出來。

 

建立資料夾名為 socket,檔案名為 index.js

[ socket / index.js ]

建立一個 SocketHander 類別:

class SocketHander {
    constructor() {
        this.db;
    }
}

 

還記得我們前面建立了 Schema() 嗎?

將該模型引入,而時間部份使用 moment.js,方便我們進行操作。

npm install -S moment

 

[ socket / index.js ]

const Messages = require('../models/Messages');
const moment = require('moment');

class SocketHander {
    constructor() {
        this.db;
    }
}

 

[ socket / index.js ]

接著建立資料庫「連線」、「儲存」和「取出」方法。

const Messages = require('../models/Messages');
const moment = require('moment');
class SocketHander {

    constructor() {
        this.db;
    }

    connect() {
        this.db = require('mongoose').connect('mongodb://localhost:27017/chat');
        this.db.Promise = global.Promise;
    }

    getMessages() {
        return Messages.find();
    }

    storeMessages(data) {

        console.log(data);
        const newMessages = new Messages({
            name: data.name,
            msg: data.msg,
            time: moment().valueOf(),
        });

        const doc = newMessages.save();
    }
}

module.exports = SocketHander;

 

 

 

三、Socket.io 事件建立

[ app.js ]

引入 socket / index.js 。

const SocketHander = require('./socket/index');

通訊成功時,連線資料庫。

io.on('connection', (socket) => {

  console.log('a user connected');

  socketHander = new SocketHander();

  socketHander.connect();

});

改寫 message 廣播事件,當有人傳送訊息時,將訊息寫入資料庫。

socket.on("message", (obj) => {
  socketHander.storeMessages(obj);
  io.emit("message", obj);
});

 

[ public / javascripts / app.js ]

到前端頁面發送訊息,測試是否被寫入資料庫內。

let data = {
    name: 'Robby',
    msg: 'Hi~',
};

socket.emit('message', data);

 

頁面重整後,可以看到後端收到訊息,並且資料庫也確實有寫入。

而用戶端也有收到訊息的廣播:

 

 

 

四、實作【極簡】聊天系統 - 介面設計

 

本實作介面使用了 Tocas UI 提供的 example 進行修改。

由於修改的部分眾多,非本文章系列重點,

因此在文章上,僅以簡易的介面方式呈現。

 

以下為簡易版所寫的樣式

 

[ views / index.ejs ]

首先刻化好聊天系統的表單,大致簡易了一下:

<body>
  <div class="chats">
    <div class="chat">
      <div class="group">
        <div class="name">Robby:</div>
        <div class="msg">Hi~</div>
      </div>
      <div class="time">11分鐘前</div>
    </div>
    <div class="chat">
      <div class="group">
        <div class="name">Robby:</div>
        <div class="msg">Hola~</div>
      </div>
      <div class="time">10分鐘前</div>
    </div>
  </div>
  <div class="message">
    <input id="name" type="text" placeholder="your name" />
    <input id="msg" type="text" placeholder="input the message" />
    <button type="button">送出</button>
  </div>
</body>

 

[ public / stylesheets / style.css ]

body {
  padding: 50px;
  font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}

a {
  color: #00B7FF;
}

.chats {
  padding: 10px 5px;
  height: 200px;
  width: 500px;
  background: #eee;
  overflow-y: scroll;
}

.chats .chat {
  display: flex;
  margin: 5px 0;
}

.chats .chat .group {
  display: inherit;
  word-wrap: normal;
}

.chats .chat .group .name {
  padding: 0 10px;
  font-weight: bold;
}

.chats .chat .time {
  padding-left: 10px;
  font-size: 10px;
  line-height: 18px;
  color: #aaa;
}

.message {
  margin-top: 10px;
}

.message input {
  padding: 0 5px;
  height: 25px;
  width: 300px;
}

.message input:first-child {
  width: 80px;
}

.message button {
  height: 32px;
  width: 100px;
}

 

畫面大致上如下:

 

 

 

五、實作【極簡】聊天系統 - 事件撰寫

訊息發送功能

[ public / javascripts / app.js ]

取得表單上的名稱和內容,並利用 socket.emit 發送訊息。

document.querySelector('button').addEventListener('click', () => {
    Send();
});

function Send() {

    let name = document.querySelector('#name').value;
    let msg = document.querySelector('#msg').value;
    if (!msg && !name) {
        alert('請輸入大名和訊息');
        return;
    }
    let data = {
        name: name,
        msg: msg,
    };
    socket.emit('message', data);
    document.querySelector('#msg').value = '';
}

 

 

訊息接收功能

[ public / javascripts / app.js ]

在 message 監聽訊息,當接收資料時,呼叫 appendData 方法。

socket.on('message', (obj) => {
    console.log(obj);
    appendData([obj]);
});

function appendData(obj) {
}

回顧一下前面,這是每個人的訊息元素組成架構:

<div class="chat">
  <div class="group">
    <div class="name">Robby:</div>
    <div class="msg">Hi~</div>
  </div>
  <div class="time">11分鐘前</div>
</div>

當有新訊息時,要 append 進去,在此可以使用 innerHTML 新增元素。

function appendData(obj) {

    let el = document.querySelector('.chats');
    let html = el.innerHTML;

    obj.forEach(element => {
        html +=
            `
            <div class="chat">
                <div class="group">
                    <div class="name">${element.name}:</div>
                    <div class="msg">${element.msg}</div>
                </div>
                <div class="time">${element.time}</div>
            </div>
            `;
    });
    el.innerHTML = html.trim();
}

測試發送訊息看看,你可能會疑惑,時間為何是 undefined

因為當初在 [ socket / index.js ] 裡面我們是存入 moment().valueOf()。

他的格式為將時間轉成 Unix時間戳(毫秒),在此用意是為了使用 fromNow()

 

[ views / index.ejs ]

引入 moment.js ( 後端的是npm安裝,客戶端無法讀取 )

<script src='https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/moment.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.20.1/locale/zh-tw.js'></script>

 

[ public / javascripts / app.js ]

因此我們把時間的部分改成:

${moment(element.time).fromNow()}

發送訊息就可以看到「幾秒前」。

 

 

取得歷史訊息

[ app.js ]

首先要建立一個歷史訊息的廣播。

io.on('connection', async (socket) => {

  console.log('a user connected');

  socketHander = new SocketHander();

  socketHander.connect();

  const history = await socketHander.getMessages();

  io.emit('history', history);

  // 其他省略 ...

});
  • 由於讀取資料庫使用了 Promise ,在這邊我們使用異步函數 asnyc、await
  • 建立廣播事件「history」 

 

[ public / javascripts / app.js ]

客戶端的部分則是監聽「history」即可。

socket.on('history', (obj) => {
    if (obj.length > 0) {
        appendData(obj);
    }
});
  • 為了確定是否有讀取到歷史訊息,使用 length 判斷

 

實際測試,可以發現一載入畫面就有先前的訊息。

 

 

為何別人載入的時候我會跳出歷史訊息?

如果你已經測試過兩人以上的連線測試,會發現這個問題。

同時開兩個頁面即可多人連線。

  • 當右邊有新連線加入,左邊原本視窗會跟著載入歷史訊息
  • 這是因為廣播的時候,是針對所有人

 

[ app.js ]

我們要將歷史訊息的廣播對象鎖定為當前用戶

// 原本舊的
// io.emit('history', history);

const socketid = socket.id;
io.to(socketid).emit('history', history);
  • 由於連線時 socket 會自動配給 id,利用 socket.id 就可取得
  • io.to(sockerid):可以將訊息廣播給鎖定的 id

 

如此一來就可以解決重複載入的問題。

 

 

如何讓卷軸自動捲到底?

[ public / javascripts / app.js ]

使用 scrollTo()scrollHeight 就可捲動到底。

function scrollWindow() {
    let h = document.querySelector('.chats');
    h.scrollTo(0, h.scrollHeight);
}

然後在 appendData() 結尾處補上即可。

function appendData(obj) {

    // 以上省略
    el.innerHTML = html.trim();
    scrollWindow();
}

重新整理畫面後,就可以看到卷軸自動捲到底囉~

 

 

 

六、後記

聊天的功能想必不只有訊息傳送,

也許未來可以試著增加檔案傳送、圖片顯示、超連結顯示等等,

甚至也可仿造 Discord,建立出語音平台。

 

範例專案放置在 Github 上,可以直接 clone 下來試試,

本篇示範的簡易專案放在 SimpleExample 裡面,可以直接查看。

 

大家也一起來打造屬於自己的 LINE 吧!

 

有勘誤之處,不吝指教。ob'_'ov