2019-11-07

效果圖:

這里啟動了四個客戶端進行測試

1. 登錄,以及獲取在線用戶列表

image

2. 私聊功能
image

3. 群聊功能
image

偶然發現了WebSocket, 發現這個可以實時通信,在線聊天,所以就做了一個聊天工具的demo,記錄一下

源碼

Socket.io

WebSocket是js原生自帶的,而Socket.io相當于是對WebSocket進行封裝的一個框架

官網說明:

介紹

Socket.io是一個WebSocket庫,包括了客戶端的js和服務器端的nodejs,它的目標是構建可以在不同瀏覽器和移動設備上使用的實時應用。它會自動根據瀏覽器從WebSocket、AJAX長輪詢、Iframe流等等各種方式中選擇最佳的方式來實現網絡實時應用,非常方便和人性化,而且支持的瀏覽器最低達IE5.5

socket.io特點

實時分析: 將數據推送到客戶端,這些客戶端會被表示為實時計數器,圖表或日志客戶。

實時通信和聊天: 只需幾行代碼便可寫成一個Socket.IO的”Hello,World”聊天應用。

二進制流傳輸: 從1.0版本開始,Socket.IO支持任何形式的二進制文件傳輸,例如:圖片,視頻,音頻等。

文檔合并: 允許多個用戶同時編輯一個文檔,并且能夠看到每個用戶做出的修改。

官方文檔中文版

官方文檔英文版

目錄結構

image

新建文件夾 -> 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

這里只是一個例子,介紹一下連接、斷開、接收消息,不包含在項目內

這兩個事件是框架本身的內置事件

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私聊

  1. 客戶端A的好友列表中點擊B, 出現聊天面板, 標記當前聊天對象,this.sendFriend = 當前聊天對象的id
  2. A輸入內容,點擊發送按鈕,使用window.socket.emit('sendMsg', data)發消息給服務器,并帶有接收此消息客戶端的id
客戶端 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)
  1. 服務器socket.on('sendMsg', (data) => {})接收到此消息,socket.to(data.id).emit('receiveMsg', data)
socket.on('sendMsg', (data) => {
    socket.to(data.id).emit('receiveMsg', data)
})
  1. 對應id的客戶端socket.on('receiveMsg', callback) 接收到私聊消息
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';
    }
})

群聊

創建群聊

  1. 點擊創建群聊按鈕 -> 出現所有在線用戶列表 -> 點擊選擇 -> 輸入群名稱 -> 確認創建 -> 客戶端發送創建群聊消息給服務器
客戶端 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 // 群成員,包含創建者
})
  1. 服務器接收到客戶端發送的創建群聊消息

    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)
    });
})
  1. 客戶端接收到chatGroupList消息,將數據加入群聊列表,并在頁面上繪制群聊列表;
客戶端 main.js

window.socket.on('chatGroupList', chatGroup => {
    this.chatGroupList.push(chatGroup);
    this.drawChatGroupList(); // 繪制群聊列表
})
  1. 客戶端接收到createChatGroup消息,給服務器發消息,說我要加入群聊socket.emit('joinChatGroup', data)
客戶端 main.js

window.socket.on('createChatGroup', (data) => {
    socket.emit('joinChatGroup', {
        id: this.id,
        userName: this.userName,
        info: data
    })
})
  1. 服務器接收到joinChatGroup消息,將當前客戶端加入群聊socket.join(data.info.roomId); 并通知此群聊中的所有人,xxx加入了群聊
服務器 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發送消息, 包括自己
})
  1. 客戶端接收到chatGrSystemNotice系統消息,將數據存儲起來,并繪制到頁面上
客戶端 main.js

window.socket.on('createChatGroup', (data) => {
    // 客戶端給服務器發消息,說我要加入群聊
    socket.emit('joinChatGroup', {
        id: this.id,
        userName: this.userName,
        info: data
    })
})

群聊聊天

流程同私聊相似,消息發送對象由個人id,變為了房間id

流程:

  1. 客戶端A在群聊列表中選擇了群聊B, 標記當前聊天的群聊roomId(this.sendChatGroup), 點擊打開聊天面板,輸入消息,點擊發送,window.socket.emit('sendMsgGroup', info)
  2. 服務器接收到sendMsgGroup
socket.on('sendMsgGroup', (data) => {
    socket.to(data.roomId).emit('receiveMsgGroup', data);
})
  1. 客戶端receiveMsgGroup事件
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';
    }
})

退出群聊

  1. 客戶端A在自己的群聊列表中選擇了群聊B點擊退出,客戶端發送消息
window.socket.emit('leave', {
    roomId: roomId,
    id: this.id,
    userName: this.userName
})
  1. 服務器處理leave退出群聊事件
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簽名的回調(如果有)。
斷開后房間自動關閉。
  1. 客戶端leaveChatGroup
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;

此項目還有些不完善,比如:

  1. 好友列表是直接獲取在線用戶的,可以做成像群聊那樣,申請添加好友,對方同意
  2. 用戶的群聊列表沒有存儲,刷新瀏覽器,就需要重新創建群聊
  3. 新消息沒做瀏覽器通知
  4. ......

github源碼

參考

官方文檔中文版

簡言 (YouChat) 感謝大佬

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 點擊查看原文 Web SDK 開發手冊 SDK 概述 網易云信 SDK 為 Web 應用提供一個完善的 IM 系統...
    layjoy閱讀 13,918評論 0 15
  • 入門 nodeJS net模塊 demo01 概述: TCP/IP 傳輸層協議,主要解決數據如何在網絡中傳輸 So...
    李冬杰閱讀 1,108評論 0 8
  • 1.JSONP JSONP是一種數據調用方式,是解決跨域交互的方法。網頁通過在script標簽里添加元素,向服務器...
    淺夢二十五夜閱讀 61評論 0 0
  • 今天早上和大姑洗寶寶的衣服,中午做飯吃,收了一堆堆快遞,整理二寶的衣服,滿滿曬了一竿,成就感十足。
    卓彤的美好時光閱讀 117評論 0 0
  • 原創:鳳溪思想盛宴3天前 曾經讀過唐朝怪才王梵志的詩“城外土饅頭,陷草在城里。一人吃一個,莫嫌沒滋味。” 后來又看...
    思想盛宴閱讀 323評論 0 9