上一篇文章《WebSocket是時候展現你優秀的一面了》其實是一個未完待續的讀物
正因如此,答應了大家的東西還是要兌現的,接下來的這篇文章裡,就讓我們一起來利用可愛的socket.io實現個聊天室的功能吧
友情提示: 聊天功能開發如果是第一次寫的話,確實會需要一段時間去咀嚼和消化,不過在你完整的敲過兩三遍後,你就會慢慢的理解和運用了,加油,Fighting!!!
這裡放上該專案的地址,需要對照學習的,盡請拿走!
聊天室的開發過程
其實這個過程從使用者的角度來說,其實無非就是連線上了,傳送訊息唄。
然而實際上,從使用者的觀點看東西,也確實是這個樣子的,那就不繞圈子了,直接進入主題
建立連線
當然,沒錯,這絕對是所有奇妙玄學中的第一步,不建立連線,那還聊個球呢
說到這裡,突然想到應該先把html的結構給大家,不然還怎麼按部就班的一起敲呢
先貼一張目錄的結構,下面的檔案都對應目錄即可
頁面結構
佈局樣式方面是直接使用bootstrap來搞的,方便快捷,主要就是讓大家看看樣子,這裡就不太浪費時間了, index.html檔案地址
沒有任何功能,僅僅是頁面佈局,大家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,按鈕sendBtnlet 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;
+
});
複製程式碼
這樣就全部大功告成了,完成了最後的歷史訊息功能,如下圖所示效果
最後進行一個功能上的梳理吧,堅持到這裡的人,我已經不知道如何表達對你的敬佩了,好樣的
梳理一下
聊天室的功能完成了,看到這裡頭有點暈了,現在簡單回憶一下,實際都有哪些功能
- 建立客戶端與服務端的websocket通訊連線
- 客戶端與服務端相互傳送訊息
- 新增使用者名稱
- 新增私聊
- 進入/離開房間聊天
- 歷史訊息
小Tips
針對以上程式碼中常用的發訊息方法進行一下區分:
- socket.send()傳送訊息是為了給自己看的
- io.emit()傳送訊息是給所有人看的
- socket.broadcast.emit()傳送訊息除了自己都能看到
最後的最後,說下我的感受,這篇文章寫的有些難受
因為文章不能像親口敘述一樣表達的痛快,所以也在探索如何寫好技術類文章,望大家理解以及多提意見吧(新增程式碼部分如何寫的更一目瞭然),感謝大家辛苦的觀看了,再見了!!!