上一篇文章《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';
+ });
複製代碼
上面定義的join和leave方法直接在對應的按鈕上調用便可了,以下圖所示
// 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;
+ });
複製代碼
這樣就所有大功告成了,完成了最後的歷史消息功能,以下圖所示效果
聊天室的功能完成了,看到這裏頭有點暈了,如今簡單回憶一下,實際都有哪些功能
針對以上代碼中經常使用的發消息方法進行一下區分:
最後的最後,說下個人感覺,這篇文章寫的有些難受
由於文章不能像親口敘述同樣表達的痛快,因此也在探索如何寫好技術類文章,望你們理解以及多提意見吧(新增代碼部分如何寫的更一目瞭然),感謝你們辛苦的觀看了,再見了!!!