效果圖:html
這裏啓動了四個客戶端進行測試前端
1. 登陸,以及獲取在線用戶列表
2. 私聊功能
3. 羣聊功能node
偶然發現了WebSocket, 發現這個能夠實時通訊,在線聊天,因此就作了一個聊天工具的demo,記錄一下git
源碼github
WebSocket是js原生自帶的,而Socket.io至關因而對WebSocket進行封裝的一個框架web
Socket.io是一個WebSocket庫,包括了客戶端的js和服務器端的nodejs,它的目標是構建能夠在不一樣瀏覽器和移動設備上使用的實時應用。它會自動根據瀏覽器從WebSocket、AJAX長輪詢、Iframe流等等各類方式中選擇最佳的方式來實現網絡實時應用,很是方便和人性化,並且支持的瀏覽器最低達IE5.5express
實時分析: 將數據推送到客戶端,這些客戶端會被表示爲實時計數器,圖表或日誌客戶。
實時通訊和聊天: 只需幾行代碼即可寫成一個Socket.IO的」Hello,World」聊天應用。
二進制流傳輸: 從1.0版本開始,Socket.IO支持任何形式的二進制文件傳輸,例如:圖片,視頻,音頻等。
文檔合併: 容許多個用戶同時編輯一個文檔,而且可以看到每一個用戶作出的修改。
npm
新建文件夾 -> npm init -y 生成package.json 可使用npm安裝插件後端
使用npm安裝express,socket.io
npm install express --save npm install socket.io --save
安裝完成的 package.json
{ "name": "websocketchat", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "express": "^4.17.1", "socket.io": "^2.3.0" } }
這兩個事件是框架自己的內置事件
connection 監聽客戶端鏈接
disconnect 監聽客戶端斷開
客戶端代碼
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <!-- 引入socket.io --> <script src="/socket.io/socket.io.js"></script> </head> <body></body> <script> window.socket = io(); socket.on('connect', () => { window.socket.on('success', data => { console.log(data) }) window.socket.on('quit', (id) => { console.log(`${id}鏈接斷開`) }) }) </script> </html>
服務器代碼
server.js
const fs = require('fs'); var express = require('express'); var app = express(); var http = require('http').Server(app); var io = require("socket.io")(http); // 路由爲/默認www靜態文件夾 app.use('/', express.static(__dirname + '/src')); io.on('connection', socket => { socket.emit('success', '鏈接到服務器') socket.on('disconnect', () => { io.emit('quit', socket.id) }) }) http.listen(3002, () => { console.log('http://localhost:3002/index.html') })
啓動服務器 運行node server.js
瀏覽器訪問 http://localhost:3002/index.html
瀏覽器控制檯輸出: 鏈接到服務器
注意編輯器的字符集設置,不然可能顯示亂碼
能夠開兩個瀏覽器,谷歌,火狐分別訪問http://localhost:3002/index.html
而後關掉火狐的訪問頁面,就能夠看到鏈接斷開的效果,谷歌控制檯輸出:TgkBeWIJK7G4hwlJAAAC鏈接斷開
服務器代碼中監聽disconnect事件,發送消息爲io.emit, 而不是socket.emit, 緣由以下:
io.emit() 給全部客戶端廣播消息 socket.emit() 給該socket的客戶端發送消息 火狐關掉訪問頁面,socket.emit('quit', socket.id)至關於給火狐這個客戶端發消息,可是這個頁面已經關掉了,天然是看不到的 io.emit('quit', socket.id) 給全部客戶端廣播消息,因此谷歌瀏覽器也能夠收到這條消息 或 使用socket.broadcast.emit('quit', socket.id); socket.broadcast.emit() 向全部的socket鏈接進行廣播,可是不包括髮送者自身
如下開始介紹此項目的功能及代碼
客戶端 main.js
function Chat() { this.userName // 當前登陸用戶名; this.userImg; // 用戶頭像 this.id; // 用戶socketId 每一個客戶端有一個本身的socket.id 經過此id能夠實現私聊 this.userList = []; // 好友列表 this.chatGroupList = []; // 羣聊列表 this.sendFriend = ''; // 當前正在聊天好友的用戶socketId this.sendChatGroup = ''; // 當前正在聊天的羣聊的roomId this.messageJson = {}; // 好友消息列表 this.msgGroupJson = {}; // 羣聊消息列表 this.tag = 0; // 0 個人好友面板 1 羣聊面板 }
Chat.prototype = { init() { this.userName = localStorage.getItem('userName'); this.userImg = localStorage.getItem('userImg'); this.selectClick(); // 註冊頁面按鈕點擊事件 this.setAllPorarait(); // 頁面添加頭像,圖片,表情包 // 緩存中有用戶名,頭像則不用再次輸入 if (this.userName && this.userImg) { $("#login-wrap").style.display = 'none'; this.login(this.userName, this.userImg); } else { $('.chat-btn').onclick = () => { let userName = $('.user-name').value; let userImg = $('.my-por').getAttribute('src'); this.login(userName, userImg); } } }, login(userName, userImg) { if (userName && userImg) { this.initSocket(userName, userImg); } }, initSocket(userName, userImg) { window.socket = io(); window.socket.on('connect', () => { $("#login-wrap").style.display = 'none'; $('.chat-panel').style.display = 'block'; this.userName = userName; this.userImg = userImg; this.id = window.socket.id; // 鏈接成功以後才能獲取到id,每刷新一次瀏覽器,都會獲取一個新的id let userInfo = { id: window.socket.id, userName: userName, userImg: userImg } // 獲取用戶名,頭像,以及socketid 設置緩存併發送給服務器 localStorage.setItem('userName', userName); localStorage.setItem('userImg', userImg); window.socket.emit('login', userInfo); }) window.socket.on('userList', (userList) => { this.userList = userList; // 返回當前全部在線用戶 this.drawUserList(); // 繪製好友列表 }) window.socket.on('quit', (id) => { this.userList = this.userList.filter(item => item.id != id) this.drawUserList(); }) } }
服務器 server.js
let userList = []; io.on('connection', (socket) => { // 前端socket.emit('login')發送消息,後端socket.on('login')接收 socket.on('login', (userInfo) => { userList.push(userInfo); io.emit('userList', userList); /* io.emit(給全部客戶端廣播消息) = socket.emit(給該socket的客戶端發送消息) + socket.broadcast.emit(發給因此客戶端,不包括本身) */ }) // 退出(內置事件) socket.on('disconnect', () => { userList = userList.filter(item => item.id != socket.id) io.emit('quit', socket.id) }) })
流程 客戶端A,客戶端B私聊
客戶端 main.js 消息發送按鈕點擊事件: let info = { sendId: this.id, // 發送者id id: this.sendFriend, // 接收者id userName: this.userName, // 發送者用戶名 img: this.userImg, // 發送者頭像 msg: $('.inp').innerHTML // 發送內容 } window.socket.emit('sendMsg', info)
socket.on('sendMsg', (data) => { socket.to(data.id).emit('receiveMsg', data) })
window.socket.on('receiveMsg', data => { this.setMessageJson(data); // 將此條消息加入消息列表數據中 // 判斷此條消息的sendId(發送者id) 是否是當前正在聊天的對象 // true 頁面繪製聊天消息 if (data.sendId === this.sendFriend) { this.drawMessageList(); // 頁面繪製聊天消息 } else { // false 好友頭像左上角顯示紅點,提示此好友發來了新消息 $('.me_' + data.sendId).innerHTML = parseInt($('.me_' + data.sendId).innerHTML) + 1; $('.me_' + data.sendId).style.display = 'block'; } })
客戶端 main.js window.socket.emit('createChatGroup', { masterId: chat.id, // 建立者id masterName: chat.userName, // 建立者用戶名 // 房間id:能夠本身設置房間id拼接規則 這個和用戶的socketid不一樣 // 用戶socketid是socket.id 拿到的, 房間id是本身自定義拼接的,只要保證不重複就可 roomId: 'room_' + chat.id + (Date.now()), chatGroupName: $('.chatGroupNameInput').value, // 羣名 member: chat.chatGroupArr // 羣成員,包含建立者 })
服務器接收到客戶端發送的建立羣聊消息
2.1 將此客戶端,也就是建立者加入羣聊socket.join(data.roomId);
2.2 存儲此羣聊數據chatGroupList[data.roomId] = data;
2.3 給羣聊的全部成員發送邀請加入羣聊的消息io.to(item.id).emit('chatGroupList', data)和當前羣聊數據的消息
io.to(item.id).emit('createChatGroup', data)
服務器 server.js let chatGroupList = {}; // 建立羣聊 socket.on('createChatGroup', data => { socket.join(data.roomId); chatGroupList[data.roomId] = data; // 羣聊列表數據 // 羣聊的每個成員發送chatGroupList(當前羣聊數據)、createChatGroup(建立羣聊)消息 data.member.forEach(item => { io.to(item.id).emit('chatGroupList', data) io.to(item.id).emit('createChatGroup', data) }); })
客戶端 main.js window.socket.on('chatGroupList', chatGroup => { this.chatGroupList.push(chatGroup); this.drawChatGroupList(); // 繪製羣聊列表 })
客戶端 main.js window.socket.on('createChatGroup', (data) => { socket.emit('joinChatGroup', { id: this.id, userName: this.userName, info: data }) })
服務器 server.js // 加入羣聊 socket.on('joinChatGroup', data => { socket.join(data.info.roomId); io.to(data.info.roomId).emit('chatGrSystemNotice', { roomId: data.info.roomId, msg: data.userName+'加入了羣聊!', system: true });//爲房間中的全部的socket發送消息, 包括本身 })
客戶端 main.js window.socket.on('createChatGroup', (data) => { // 客戶端給服務器發消息,說我要加入羣聊 socket.emit('joinChatGroup', { id: this.id, userName: this.userName, info: data }) })
流程同私聊類似,消息發送對象由我的id,變爲了房間id
流程:
socket.on('sendMsgGroup', (data) => { socket.to(data.roomId).emit('receiveMsgGroup', data); })
window.socket.on('receiveMsgGroup', (data) => { this.setMsgGroupJson(data); // 此條消息添加到聊天數據列表中 // 判斷收到的是否是當前羣聊的,不是就標記紅點,是就繪製聊天內容 if (data.roomId === this.sendChatGroup) { this.drawChatGroupMsgList(); // 繪製聊天內容 } else { $('.me_' + data.roomId).innerHTML = parseInt($('.me_' + data.roomId).innerHTML) + 1; $('.me_' + data.roomId).style.display = 'block'; } })
window.socket.emit('leave', { roomId: roomId, id: this.id, userName: this.userName })
socket.on('leave', data => { socket.leave(data.roomId, () => { let member = chatGroupList[data.roomId].member; let i = -1; // 向羣聊的每個成員發送xx離開的通知消息,包括離開者 // 而後在成員數組member中刪除離開的人 member.forEach((item, index) => { if (item.id === socket.id) { i = index; } io.to(item.id).emit('leaveChatGroup', { id: socket.id, // 退出羣聊人的id roomId: data.roomId, msg: data.userName+'離開了羣聊!', system: true }) }); if (i !== -1) { member.splice(i) } }); }) // socket.leave() 官網說明: socket.leave(room [, callback]) * room (串) * callback (功能) * Socket連接返回 從中刪除客戶端room,並可選地啓動帶有err簽名的回調(若是有)。 斷開後房間自動關閉。
window.socket.on('leaveChatGroup', data => { // 當前客戶端退出羣聊 if (data.id === this.id) { this.chatGroupList = this.chatGroupList.filter(item => item.roomId !== data.roomId) this.drawChatGroupList(); } else { // 其它成員離開了羣聊,這裏顯示消息通知 this.setMsgGroupJson(data); if (this.tag) { $('.me_' + data.roomId).innerHTML = parseInt($('.me_' + data.roomId).innerHTML) + 1; $('.me_' + data.roomId).style.display = 'block'; this.drawChatGroupMsgList(); } else { $('.me-group-chat-tab').innerHTML = parseInt($('.me-group-chat-tab').innerHTML) + 1; $('.me-group-chat-tab').style.display = 'block'; $('.me_' + data.roomId).innerHTML = parseInt($('.me_' + data.roomId).innerHTML) + 1; $('.me_' + data.roomId).style.display = 'block'; } } })
去掉input點擊輸入時出現的藍色邊框: outline: none; <div class="inp inp-box" contenteditable></div> 實現input的效果,並可指定寬高 瀏覽器通知: // 獲取權限 if (Notification && Notification.requestPermission){ Notification.requestPermission() } new Notification('新消息', { body: `${data.userName}: ${data.msg}`, icon: data.userImg }) 字體縮放 font-size: 12px; transform: scale(0.9); display: inline-block;
此項目還有些不完善,好比:
簡言 (YouChat) 感謝大佬