socket.io讓每一個人均可以開發屬於本身的即時通信

上一篇文章《WebSocket是時候展示你優秀的一面了》實際上是一個未完待續的讀物html

正因如此,答應了你們的東西仍是要兌現的,接下來的這篇文章裏,就讓咱們一塊兒來利用可愛的socket.io實現個聊天室的功能吧前端

友情提示: 聊天功能開發若是是第一次寫的話,確實會須要一段時間去咀嚼和消化,不過在你完整的敲過兩三遍後,你就會慢慢的理解和運用了,加油,Fighting!!!git

這裏放上該項目的地址,須要對照學習的,盡請拿走!github

聊天室的開發過程

其實這個過程從用戶的角度來講,其實無非就是鏈接上了,發送消息唄。web

然而實際上,從用戶的觀點看東西,也確實是這個樣子的,那就不繞圈子了,直接進入主題數據庫

創建鏈接

固然,沒錯,這絕對是全部奇妙玄學中的第一步,不創建鏈接,那還聊個球呢express

說到這裏,忽然想到應該先把html的結構給你們,否則還怎麼循序漸進的一塊兒敲呢bootstrap

先貼一張目錄的結構,下面的文件都對應目錄便可 數組

頁面結構

佈局樣式方面是直接使用bootstrap來搞的,方便快捷,主要就是讓你們看看樣子,這裏就不太浪費時間了, index.html文件地址bash

沒有任何功能,僅僅是頁面佈局,你們copy一下,看看樣子便可了

下面咱們來分別試着寫下客戶端和服務端的兩套建立鏈接的代碼,一塊兒敲敲敲吧

這纔是重要的東西,開擼

客戶端創建鏈接

// index.js文件
let socket = io();
// 監聽與服務端的鏈接
socket.on('connect', () => {
    console.log('鏈接成功'); 
});
複製代碼

socket.io用法簡單,方便上手,欲購從速,哈哈,繼續寫服務端的鏈接吧

服務端創建鏈接

服務端的搭建咱們仍是用以前使用的express來處理

// app.js文件

const express = require('express');
const app = express();
// 設置靜態文件夾,會默認找當前目錄下的index.html文件當作訪問的頁面
app.use(express.static(__dirname));

// WebSocket是依賴HTTP協議進行握手的
const server = require('http').createServer(app);
const io = require('socket.io')(server);
// 監聽與客戶端的鏈接事件
io.on('connection', socket => {
    console.log('服務端鏈接成功');
});
// ☆ 這裏要用server去監聽端口,而非app.listen去監聽(否則找不到socket.io.js文件)
server.listen(4000);
複製代碼

以上內容就是客戶端和服務端創建了websocket鏈接了,如此的so easy,那麼接下來繼續寫發送消息吧

發送消息

列表Ul輸入框按鈕這些都齊全了,那就開始發送消息吧

經過socket.emit('message')方法來發送消息給服務端

// index.js文件

// 列表list,輸入框content,按鈕sendBtn
let list = document.getElementById('list'),
    input = document.getElementById('input'),
    sendBtn = document.getElementById('sendBtn');

// 發送消息的方法
function send() {
    let value = input.value;
    if (value) {
        // 發送消息給服務器
        socket.emit('message', value);
        input.value = '';
    } else {
        alert('輸入的內容不能爲空!');
    }
}
// 點擊按鈕發送消息
sendBtn.onclick = send;
複製代碼

回車發送消息

每次都要點發送按鈕,也是夠反用戶操做行爲的了,因此仍是加上咱們熟悉的回車發送吧,看代碼,+號表示新增的代碼

// index.js文件
...省略

// 回車發送消息的方法
+ function enterSend(event) {
+    let code = event.keyCode;
+    if (code === 13)  send();
+ }
// 在輸入框onkeydown的時候發送消息
+ input.onkeydown = function(event) {
+    enterSend(event);  
+ };
複製代碼

前端已經把消息發出去了,接下來該服務端出馬了,繼續擼

服務端處理消息

// app.js文件
...省略

io.on('connection', socket => {
    // 監聽客戶端發過來的消息
+    socket.on('message', msg => {
         // 服務端發送message事件,把msg消息再發送給客戶端
+        io.emit('message', {
+            user: '系統',
+            content: msg,
+            createAt: new Date().toLocaleString()
+        });              
+    });
});
複製代碼

io.emit()方法是向大廳和全部人房間內的人廣播

客戶端渲染消息

咱們繼續在index.js這裏寫,把服務端傳過來的消息接收並渲染出來

// index.js文件
...省略

// 監聽message事件來接收服務端發來的消息
+ socket.on('message', data => {
      // 建立新的li元素,最終將其添加到list列表
+     let li = document.createElement('li');
+     li.className = 'list-group-item';
+     li.innerHTML = `
        <p style="color: #ccc;">
            <span class="user">${data.user}</span>
            ${data.createAt}
        </p>
        <p class="content">${data.content}</p>`;
      // 將li添加到list列表中
+     list.appendChild(li);
      // 將聊天區域的滾動條設置到最新內容的位置
+     list.scrollTop = list.scrollHeight;
+ });
複製代碼

寫到這裏,發送消息的部分就已經完事了,執行代碼應該均可以看到以下圖的樣子了

看到上面的圖後,咱們應該高興一下,畢竟有消息了,離成功又近了一步兩步三四步

雖然上面的代碼還有瑕疵,不過不要方,讓咱們繼續完善它

根據圖片所示,全部的用戶都是「系統」,這根本就分不清誰是誰了,讓咱們來判斷一下,須要加個用戶名

建立用戶名

這裏咱們能夠知道,當用戶是第一次進來的時候,是沒有用戶名的,須要在設置以後纔會顯示對應的名字

因而乎,咱們就把第一次進來後輸入的內容看成用戶名

// app.js文件
...省略
// 把系統設置爲常量,方便使用
const SYSTEM = '系統';

io.on('connection', socket => {
    // 記錄用戶名,用來記錄是否是第一次進入,默認是undefined
+   let username;
    socket.on('message', msg => {
        // 若是用戶名存在
+       if (username) {
             // 就向全部人廣播
+            io.emit('message', {
+                user: username,
+                content: msg,
+                createAt: new Date().toLocaleString()
+            });
+       } else {  // 用戶名不存在的狀況
             // 若是是第一次進入的話,就將輸入的內容當作用戶名
+            username = msg;
             // 向除了本身的全部人廣播,畢竟進沒進入本身是知道的,不必跟本身再說一遍
+            socket.broadcast.emit('message', {
+                user: SYSTEM,
+                content: `${username}加入了聊天!`,
+                createAt: new Date().toLocaleString()
+            });
+        }
    });
});
複製代碼

☆️ socket.broadcast.emit,這個方法是向除了本身外的全部人廣播

沒錯,畢竟本身進沒進聊天室本身內心還沒數麼,哈哈

下面再看下執行的效果,請看圖

最基本的發消息功能已經實現了,下面咱們再接再礪,完成一個 私聊功能吧

添加私聊

在羣裏你們都知道@一下就表明這條消息是專屬被@的那我的的,其餘人是不用care的

如何實現私聊呢?這裏咱們採用,在消息列表list中點擊對方的用戶名進行私聊,因此廢話很少說,開寫吧

@一下

// index.js文件
...省略

  // 私聊的方法
+ function privateChat(event) {
+    let target = event.target;
     // 拿到對應的用戶名
+    let user = target.innerHTML;
     // 只有class爲user的纔是目標元素
+    if (target.className === 'user') {
         // 將@用戶名顯示在input輸入框中
+        input.value = `@${user} `;
+    }
+ }
  // 點擊進行私聊
+ list.onclick = function(event) {
+    privateChat(event);
+ };
複製代碼

客戶端已將@用戶名這樣的格式設置在了輸入框中,只要發送消息,服務端就能夠進行區分,是私聊仍是公聊了,下面繼續寫服務端的處理邏輯吧

服務端處理

首先私聊的前提是已經獲取到了用戶名了

而後正則判斷一下,哪些消息是屬於私聊的

最後還須要找到對方的socket實例,好方便發送消息給對方

那麼,看以下代碼

// app.js文件
...省略

// 用來保存對應的socket,就是記錄對方的socket實例
+ let socketObj = {};

io.on('connection', socket => {
    let username;
    socket.on('message', msg => {
        if (username) {
            // 正則判斷消息是否爲私聊專屬
+           let private = msg.match(/@([^ ]+) (.+)/);

+           if (private) {  // 私聊消息
                 // 私聊的用戶,正則匹配的第一個分組
+                let toUser = private[1];
                 // 私聊的內容,正則匹配的第二個分組
+                let content = private[2];
                 // 從socketObj中獲取私聊用戶的socket
+                let toSocket = socketObj[toUser];

+                if (toSocket) {
                     // 向私聊的用戶發消息
+                    toSocket.send({
+                        user: username,
+                        content,
+                        createAt: new Date().toLocaleString()
+                    });
+                }
            } else {    // 公聊消息
                io.emit('message', {
                    user: username,
                    content: msg,
                    createAt: new Date().toLocaleString()
                });
            }
        } else { // 用戶名不存在的狀況
            ...省略
            // 把socketObj對象上對應的用戶名賦爲一個socket
            // 如: socketObj = { '周杰倫': socket, '謝霆鋒': socket }
+           socketObj[username] = socket;
        }
    });
});
複製代碼

寫到這裏,咱們已經完成了公聊和私聊的功能了,可喜可賀,很是了不得了已經,可是不能傲嬌,咱們再完善一些小細節

如今全部用戶名和發送消息的氣泡都是一個顏色,其實這樣也很差區分用戶之間的差別

SO,咱們來改下顏色的部分

分配用戶不同的顏色

服務端處理顏色

// app.js文件
...省略
let socketObj = {};
// 設置一些顏色的數組,讓每次進入聊天的用戶顏色都不同
+ let userColor = ['#00a1f4', '#0cc', '#f44336', '#795548', '#e91e63', '#00bcd4', '#009688', '#4caf50', '#8bc34a', '#ffc107', '#607d8b', '#ff9800', '#ff5722'];

// 亂序排列方法,方便把數組打亂
+ function shuffle(arr) {
+    let len = arr.length, random;
+    while (0 !== len) {
        // 右移位運算符向下取整
+        random = (Math.random() * len--) >>> 0; 
        // 解構賦值實現變量互換
+        [arr[len], arr[random]] = [arr[random], arr[len]]; 
+    }
+    return arr;
+ }

io.on('connection', socket => {
     let username;
+    let color;  // 用於存顏色的變量

    socket.on('message', msg => {
        if (username) {
            ...省略
            if (private) {
                ...省略
                if (toSocket) {
                    toSocket.send({
                        user: username,
+                       color,
                        content: content,
                        createAt: new Date().toLocaleString()
                    });
                }
            } else {
                io.emit('message', {
                    user: username,
+                   color,
                    content: msg,
                    createAt: new Date().toLocaleString()
                });
            }
        } else { // 用戶名不存在的狀況
            ...省略
            // 亂序後取出顏色數組中的第一個,分配給進入的用戶
+           color = shuffle(userColor)[0];

            socket.broadcast.emit('message', {
                user: '系統',
+               color,
                content: `${username}加入了聊天!`,
                createAt: new Date().toLocaleString()
            });
        }
    });
});
複製代碼

服務端那邊給分配好了顏色,前端這邊再渲染一下就行了,接着寫下去,不要停

渲染顏色

在建立的li元素上,給對應的用戶名和內容分別在style樣式中加個顏色就能夠了,代碼以下

// index.js
... 省略

socket.on('message', data => {
    let li = document.createElement('li');
    li.className = 'list-group-item';
    // 給對應元素設置行內樣式添加顏色
+   li.innerHTML = `<p style="color: #ccc;"><span class="user" style="color:${data.color}">${data.user} </span>${data.createAt}</p>
                    <p class="content" style="background:${data.color}">${data.content}</p>`;
    list.appendChild(li);
    // 將聊天區域的滾動條設置到最新內容的位置
    list.scrollTop = list.scrollHeight;
});
複製代碼

寫完是寫完了,咱們看看效果吧

寫到這裏,看到這裏,是否疲倦了呢,年輕人不要放棄

Now,讓咱們來寫理論上的最最最後一個功能吧,進入某個羣裏聊天,該消息只有羣裏的人能夠看到

加入指定房間(羣)

咱們一直在上面的截圖中看到了兩個羣的按鈕,看到字面意思就能知道是幹嗎的,就是爲了這一刻而準備的

下面咱們再來,繼續擼,立刻就要完成大做了

客戶端-進出房間(羣)

// index.js文件
...省略

// 進入房間的方法
+ function join(room) {
+    socket.emit('join', room);
+ }
// 監聽是否已進入房間
// 若是已進入房間,就顯示離開房間按鈕
+ socket.on('joined', room => {
+    document.getElementById(`join-${room}`).style.display = 'none';
+    document.getElementById(`leave-${room}`).style.display = 'inline-block';
+ });

// 離開房間的方法
+ function leave(room) {
    socket.emit('leave', room);
+ }
// 監聽是否已離開房間
// 若是已離開房間,就顯示進入房間按鈕
+ socket.on('leaved', room => {
+    document.getElementById(`leave-${room}`).style.display = 'none';
+    document.getElementById(`join-${room}`).style.display = 'inline-block';
+ });
複製代碼

上面定義的joinleave方法直接在對應的按鈕上調用便可了,以下圖所示

下面咱們繼續寫服務端的代碼邏輯

服務端-處理進出房間(羣)

// app.js文件
...省略
io.on('connection', socket => {
    ...省略
    // 記錄進入了哪些房間的數組
+   let rooms = [];
    io.on('message', msg => {
        ...省略
    });
    // 監聽進入房間的事件
+   socket.on('join', room => {
+       // 判斷一下用戶是否進入了房間,若是沒有就讓其進入房間內
+       if (username && rooms.indexOf(room) === -1) {
            // socket.join表示進入某個房間
+           socket.join(room);
+           rooms.push(room);
            // 這裏發送個joined事件,讓前端監聽後,控制房間按鈕顯隱
+           socket.emit('joined', room);
            // 通知一下本身
+           socket.send({
+               user: SYSTEM,
+               color,
+               content: `你已加入${room}戰隊`,
+               createAt: new Date().toLocaleString()
+           });
+       }
+   });
    // 監聽離開房間的事件
+   socket.on('leave', room => {
        // index爲該房間在數組rooms中的索引,方便刪除
+       let index = rooms.indexOf(room);
+       if (index !== -1) {
+           socket.leave(room); // 離開該房間
+           rooms.splice(index, 1); // 刪掉該房間
            // 這裏發送個leaved事件,讓前端監聽後,控制房間按鈕顯隱
+           socket.emit('leaved', room);
            // 通知一下本身
+           socket.send({
+               user: SYSTEM,
+               color,
+               content: `你已離開${room}戰隊`,
+               createAt: new Date().toLocaleString()
+           });
+       }
+   });
});
複製代碼

寫到這裏,咱們也實現了加入和離開房間的功能,以下圖所示

既然進入了房間內,那麼很顯然,發言的內容只能是在房間內的人才能看到,這點咱們都懂

因此下面咱們再寫一下房間內發言的邏輯,繼續在app.js中開擼

處理房間內發言

// app.js文件
...省略
// 上來記錄一個socket.id用來查找對應的用戶
+ let mySocket = {};

io.on('connection', socket => {
    ...省略
    // 這是全部鏈接到服務端的socket.id
+   mySocket[socket.id] = socket;
    
    socket.on('message', msg => {
        if (private) {
            ...省略
        } else {
            // 若是rooms數組有值,就表明有用戶進入了房間
+           if (rooms.length) {
                // 用來存儲進入房間內的對應的socket.id
+               let socketJson = {};

+               rooms.forEach(room => {
                    // 取得進入房間內所對應的全部sockets的hash值,它即是拿到的socket.id
+                   let roomSockets = io.sockets.adapter.rooms[room].sockets;
+                   Object.keys(roomSockets).forEach(socketId => {
                        console.log('socketId', socketId);
                        // 進行一個去重,在socketJson中只有對應惟一的socketId
+                       if (!socketJson[socketId]) {
+                           socketJson[socketId] = 1;
+                       }
+                   });
+               });

                // 遍歷socketJson,在mySocket裏找到對應的id,而後發送消息
+               Object.keys(socketJson).forEach(socketId => {
+                   mySocket[socketId].emit('message', {
+                       user: username,
+                       color,
+                       content: msg,
+                       createAt: new Date().toLocaleString()
+                   });
+               });
            } else {
                // 若是不是私聊的,向全部人廣播
                io.emit('message', {
                    user: username,
                    color,
                    content: msg,
                    createAt: new Date().toLocaleString()
                });
            }
        }
    });
});
複製代碼

從新運行app.js文件後,再進入房間聊天,會展現以下圖的效果,只有在同一個房間內的用戶,才能相互之間看到消息

麻雀雖小但五臟俱全,堅持寫到這裏的每一位都是贏家,不過我還想再完善最後一個小功能,就是展現一下 歷史消息

畢竟每次一進到聊天室都是空空如也的樣子也太蒼白了,仍是但願瞭解到以前的用戶聊了哪些內容的

那麼繼續加油,實現咱們最後一個功能吧

展現歷史消息

其實正確開發的狀況,用戶輸入的全部消息應該是存在數據庫中進行保存的,不過咱們這裏就不涉及其餘方面的知識點了,就直接用純前端的技術去模擬一下實現了

獲取歷史消息

這裏讓客戶端去發送一個getHistory的事件,在socket鏈接成功的時候,告訴服務器咱們要拿到最新的20條消息記錄

// index.js
...省略

socket.on('connect', () => {
    console.log('鏈接成功');
    // 向服務器發getHistory來拿消息
+   socket.emit('getHistory');
});
複製代碼

服務端處理歷史記錄並返回

// app.js
...省略

// 建立一個數組用來保存最近的20條消息記錄,真實項目中會存到數據庫中
let msgHistory = [];

io.on('connection', socket => {
    ...省略
    io.on('message', msg => {
        ...省略
        if (private) {
            ...省略
        } else {
            io.emit('message', {
                user: username,
                color,
                content: msg,
                createAt: new Date().toLocaleString()
            });
            // 把發送的消息push到msgHistory中
            // 真實狀況是存到數據庫裏的
+           msgHistory.push({
+               user: username,
+               color,
+               content: msg,
+              createAt: new Date().toLocaleString()
+          });
        }
    });
    
    // 監聽獲取歷史消息的事件
+   socket.on('getHistory', () => {
        // 經過數組的slice方法截取最新的20條消息
+       if (msgHistory.length) {
+           let history = msgHistory.slice(msgHistory.length - 20);
            // 發送history事件並返回history消息數組給客戶端
+           socket.emit('history', history);
+       }
+   });
});
複製代碼

客戶端渲染歷史消息

// index.js
...省略

// 接收歷史消息
+ socket.on('history', history => {
    // history拿到的是一個數組,因此用map映射成新數組,而後再join一下鏈接拼成字符串
+   let html = history.map(data => {
+       return `<li class="list-group-item">
            <p style="color: #ccc;"><span class="user" style="color:${data.color}">${data.user} </span>${data.createAt}</p>
            <p class="content" style="background-color: ${data.color}">${data.content}</p>
        </li>`;
+   }).join('');
+   list.innerHTML = html + '<li style="margin: 16px 0;text-align: center">以上是歷史消息</li>';
    // 將聊天區域的滾動條設置到最新內容的位置
+   list.scrollTop = list.scrollHeight;
+ });
複製代碼

這樣就所有大功告成了,完成了最後的歷史消息功能,以下圖所示效果

最後進行一個功能上的梳理吧,堅持到這裏的人,我已經不知道如何表達對你的敬佩了,好樣的

梳理一下

聊天室的功能完成了,看到這裏頭有點暈了,如今簡單回憶一下,實際都有哪些功能

  1. 建立客戶端與服務端的websocket通訊鏈接
  2. 客戶端與服務端相互發送消息
  3. 添加用戶名
  4. 添加私聊
  5. 進入/離開房間聊天
  6. 歷史消息

小Tips

針對以上代碼中經常使用的發消息方法進行一下區分:

  • socket.send()發送消息是爲了給本身看的
  • io.emit()發送消息是給全部人看的
  • socket.broadcast.emit()發送消息除了本身都能看到

最後的最後,說下個人感覺,這篇文章寫的有些難受

由於文章不能像親口敘述同樣表達的痛快,因此也在探索如何寫好技術類文章,望你們理解以及多提意見吧(新增代碼部分如何寫的更一目瞭然),感謝你們辛苦的觀看了,再見了!!!

相關文章
相關標籤/搜索