效果图:html
这里启动了四个客户端进行测试前端
1. 登陆,以及获取在线用户列表 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) 感谢大佬