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