乐趣区

jsnodejssocketio实现聊天功能私聊创建群聊

效果图:

这里启动了四个客户端进行测试

1. 登录,以及获取在线用户列表

2. 私聊功能

3. 群聊功能

偶然发现了 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 支持任何形式的二进制文件传输,例如:图片,视频,音频等。

文档合并: 允许多个用户同时编辑一个文档,并且能够看到每个用户做出的修改。

官方文档中文版

官方文档英文版

目录结构

新建文件夹 -> 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) 感谢大佬

退出移动版