Gitee源码:https://gitee.com/wfeng0/CSDN
GitHub源码:GitHub - wf0/CSDN: vue-socket.io实现即时聊天
视频介绍:vue-socket.io实现即时聊天_哔哩哔哩_bilibili
目录
1.项目说明:
2.登录页面实现:
3. 连接socket.io:
3.1 下载vue-socket.io:
3.2 引用:
4. Node服务器的开发:
4.1新建一个文件夹
4.2 安装依赖
4.3 新建入口文件index.js
4.4 启动服务,测试连接
4.5 监听用户登录
4.6 将用户名加到socket实例的属性上,便于我们处理私聊。
4.7 监听服务器是否存在用户:
5. 动态渲染用户列表
5.1 使用vuex处理用户列表
5.2 vuex管理socket事件
5.3 默认群聊的实现
5.4 服务器管理在线人数(用户列表)
6. 群聊的实现
6.1 发送消息到服务器:
6.2 渲染聊天列表:
7. 私聊的实现
8. 实现效果
9. 总结:
9.1 前端UI设计
9.2 socket服务器的设计
9.3 群聊的实现
9.4 私聊的实现
9.5 项目难点
技术讨论群【522121825】
我们主要通过这个目,练习一下vue-socket.io的群聊和私聊两个功能,至于房间Rooms,有兴趣的伙伴可以自己去研究啦。我是将整个打代码、思考的过程写了下来,如果只想要结果的,可以直接看我的总结部分,或者到我的GitHub主页上看源码。还是希望大家好好看,相信会有收获的。好了,直接开始吧。
那我们的群聊是怎么实现的呢?我们会设计一个登录页面,要求输入用户的用户名,作为聊天应用的唯一标识。群聊是登录上来就有一个默认的群,而每个用户都能触发私聊。就这个思路,下面来实现。
//vuex
state: {
/* 记录登录状态 */
isLogin:false,
}
//App.vue
computed:{
isLogin(){
return store.state.isLogin;
},
//我们通过获取vuex的数据,判断用户是否登录
使用 v-if v-else实现登录的控制:
效果图如下:
用户输入用户名,并选择头像后,点击登录进入系统;具体的代码如下(我就不分解详细说了,因为页面的样式每个人都有自己的风格,如果对我的代码不懂的,可以留言讨论呢。)
登录
data() {
return {
activeName:'first',
username:'',
choosed:'',
avatarList:[
'http://img.mp.itc.cn/upload/20170808/5861bc790e654d56bc9289c567b44875_th.jpg',
'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
'http://gss0.baidu.com/-fo3dSag_xI4khGko9WTAnF6hhy/zhidao/pic/item/30adcbef76094b36ba49777aa5cc7cd98c109d49.,jpg',
'http://img.52z.com/upload/news/image/20180111/20180111085521_86389.jpg',
'http://img.mp.itc.cn/upload/20170808/5861bc790e654d56bc9289c567b44875_th.jpg',
'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
'http://gss0.baidu.com/-fo3dSag_xI4khGko9WTAnF6hhy/zhidao/pic/item/30adcbef76094b36ba49777aa5cc7cd98c109d49.,jpg',
'http://img.52z.com/upload/news/image/20180111/20180111085521_86389.jpg'
],
}
},
css:
.choosed{
border: solid 1px red;
}
.login{
width: 50%;
margin-left: 25%;
border: solid 1px rgb(228, 231, 237);
padding: 30px;
}
.login .avatar{
margin-top: 20px;
}
.login .avatar .el-avatar{
cursor: pointer;
}
也可以自己写这个登录页,确保用户输入用户名和头像信息。
业务就是将选中的头像赋给choosed;就能应用css了:(被选中的头像加了边框)
这里注意啊,这个信息是你!你!!所以,要将这个信息告诉vuex,修改myInfo的数据。(是登录按钮的事件处理,同时,还要连接socket.io)
//vuex:
setMyInfo(state,data){
state.myInfo=data;
state.isLogin=true;
},
//App login事件:
login(){
if(this.username&&this.choosed){
/* 告诉vuex修改个人信息 */
store.commit('setMyInfo',{
img:this.choosed,
name:this.username,
});
/* 连接socket */
}
},
现在能跳过去,但是我们的信息并没有修改,原因是我们的数据是定死的,现在要监听vuex数据;
如下:
!-- 我的信息 -->
//监听vuex数据
computed:{
myInfo(){
return store.state.myInfo;
},
},
现在就能实现了:
终于到了我们的关键技术了!前面的所有都是铺垫,为了页面好看些。
npm i vue-socket.io --s
(这里要关闭自动连接!不然造成node服务器资源浪费。服务器地址我们后面再写)
import VueSocketIO from 'vue-socket.io'
//我直接use在后面了,你也可以新起一行
Vue.use(ElementUI).use(
new VueSocketIO({
debug: true, // debug调试,生产建议关闭
connection: "url",
options: { //Optional options,
autoConnect:false, //关闭自动连接,在用户登录后在连接。
}
})
);
跟你的项目同级,不建议将服务器的文件夹建在vue项目里面,这样代码容易混乱。(空的,就是普通的文件夹)
npm i socket.io http express file --s
所需的依赖是:socket.io、http、express、file(用空格隔开,就能安装多个)
等你装完,目录就自己有了!
这个名字可以随意。
服务器连接的代码可以参考我的这篇文章:Vue 使用 Vue-socket.io 实现即时聊天应用(连接篇)
包括请求跨域,400错误的解决办法等;下面是简单的代码,(这里可以打开自动连接,看能不能连接上。我们现在只需要socket.io服务器,所以我没有写http的服务,http的服务,是有app.get(....)的。要区分socket服务和http服务)
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http,{
allowEIO3: true,
cors: {
origin: "http://localhost:8080",
methods: ["GET", "POST"],
credentials: true
}
});
io.on('connection', function(socket){
console.log('a user connected');
});
http.listen(3000, function(){
console.log('listening on *:3000');
});
这个步骤是一定要做的,测试连接,建议先打开自动连接,没问题了关闭。我的没问题:
(以后的事件,都定义在io中,我就不再全部写了)
io.on('connection', function (socket) {
/* 监听用户登录事件 */
socket.on('login',data=>{
console.log('用户登录:',data);
});
console.log('a user connected');
});
App.vue:记得main.js中关闭自动连接啊啊啊啊!!
login(){
if(this.username&&this.choosed){
/* 告诉vuex修改个人信息 */
store.commit('setMyInfo',{
img:this.choosed,
name:this.username,
});
/* 连接socket */
this.$socket.connect();
this.$socket.emit('login',{name:this.username});
}
},
现在应该是监听了,但是还没有连接,用户登录了再连接成功:
可以参考我的文章:Vue 使用 Vue-socket.io 实现即时聊天应用(通讯篇)(服务器的console.log对用户影响不大,但是对我们调试程序却很有用,不建议删除)
/* 监听用户登录事件,并将数据放到socket实例的属性上 */
socket.on('login',data=>{
console.log('用户登录:',data);
socket.name=data.name;
});
思考:socket.name应该唯一吗?既然是私聊的唯一标识,一定是唯一的!!不然私聊找到两个人了。因此,登录的时候,不是简单的连接就行了,还要判断服务器上是否存在已有该用户名!这个才是关键!建议使用最简单的回调函数解决。
App.vue:(不能写具名函数,使用箭头函数)
this.$socket.emit('login',{name:this.username},(result)=>{
console.log(result)
});
node.js:
/* 监听用户登录事件,并将数据放到socket实例的属性上 */
socket.on('login',(data,callback)=>{
console.log('用户登录:',data);
socket.name=data.name;
callback(123);
});
接收到回调的数据:
可以看官网的描述:
如上所诉,需要监听服务器是否存在用户,使用回调函数处理,是最简单的。App根据返回的数据,决定登录成功还是给出错误提示:
/* 监听用户登录事件,并将数据放到socket实例的属性上 */
socket.on('login',(data,callback)=>{
/* 遍历服务器连接对象 */
var islogin=true;
io.sockets.sockets.forEach(iss => {
if(iss.name==data.name){
islogin=false;
}
});
if(islogin){
console.log('用户登录成功:',data);
socket.name=data.name;
callback(true);
}else{
console.log('用户登录失败!:',data);
callback(false);
}
});
App根据返回的数据做判断:
哈哈哈,一个登录就这么多事!坚持下去啊,后面就很快了。
现在实现动态渲染用户列表:
我们的数据,现在还是Aside管理,现在改为vuex管理,放在userList中,同时!还要在从vuex取出来渲染。
state: {
//....
/* 用户列表 */
userList:[
{
name: '王小虎',
img: 'http://img.mp.itc.cn/upload/20170808/5861bc790e654d56bc9289c567b44875_th.jpg'
}, {
name: '郑泷',
img: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
}, {
name: '小蛮',
img: 'http://gss0.baidu.com/-fo3dSag_xI4khGko9WTAnF6hhy/zhidao/pic/item/30adcbef76094b36ba49777aa5cc7cd98c109d49.jpg'
}, {
name: '张云',
img: 'http://img.52z.com/upload/news/image/20180111/20180111085521_86389.jpg'
}
],
},
在Aside中,不需要定义数据了,而是直接计算出来;(就是原来tableDate是data数据,现在直接用computed)
确保用户列表渲染不变!!
动态渲染,用户登录上来要更新列表:
使用vuex管理:(可以结合Vue 使用 Vue-socket.io 实现即时聊天应用(vuex管理)_~朴:shu的博客-CSDN博客 这篇博客看看)
Vue.use(ElementUI).use(
new VueSocketIO({
debug: true, // debug调试,生产建议关闭
connection: "http://localhost:3000",
vuex: {
store,
actionPrefix: 'SOCKET_',
mutationPrefix: 'SOCKET_'
},
options: { //Optional options,
autoConnect:false, //关闭自动连接,在用户登录后在连接。
}
})
);
vuex:
/* 渲染用户列表 */
SOCKET_login(state,data){
console.log('vuex',data)
},
服务器:
if(islogin){
console.log('用户登录成功:',data);
socket.name=data.name;
callback(true);
io.emit('login',data)
}else{
console.log('用户登录失败!:',data);
callback(false);
}
都是login事件,大家区分清楚啊!建议:可以将socket的生产提示关了。
收到的数据还差头像信息!!在login事件中传过去:
默认为空的,收到数据后,push到userList中
/* 渲染用户列表 */
SOCKET_login(state,data){
state.userList.push(data)
},
通知所有连接的用户,有人连接上来了,不然没有效果嘿嘿!通知vuex说有人连接上来,应该用的广播才行!!!
socket.emit('login',data)=>io.emit('login',data) 一个是连接实例,一个是广播!
实现效果:
当二号用户连接上来的时候,一号会监听vuex的login事件(是广播的!!)
所以,他的页面是这样:
已经成功一半了!坚持呀。
加一个默认的群聊,
/* 用户列表 */
userList:[
{
name:'默认群聊',
img:'https://pic1.zhimg.com/50/v2-adfacac8307b48531d4e341a6090aa03_hd.jpg?source=1940ef5c'
}
],
效果如下:
保证每个人进来,都有一个默认的群聊。(同时,你还可以优化一下,使用 数据过滤实现判断该用户是不是你,是你就不显示)
数据过滤:(为空字符不过滤 || 过滤数据中include(包含)keyword) && 过滤掉自己(取非了)
这样子好多了!
但是不知道大家有没有发现一个问题!为什么后面登录的就没有前面的人的消息?1号是先登录的,2号是后登录的,每次登录进系统,都会从vuex获取用户列表,列表都是空的,原因在这里!解决办法是通过socket服务器返回在线人数是最准确,最及时的。
/* 接收在线人数,传给前端,保证在线人数是最新的 */
var userList=[];
//...
if(islogin){
console.log('用户登录成功:',data);
userList.push(data);
socket.name=data.name;
callback(true);
io.emit('login',userList);
}else{
console.log('用户登录失败!:',data);
callback(false);
}
传过去数组,就不用push啦,直接在vuex中赋值:
/* 渲染用户列表 */
SOCKET_login(state,data){
state.userList=data;
},
在服务器端添加默认群聊:
/* 接收在线人数,传给前端,保证在线人数是最新的 */
var userList=[
{
name:'默认群聊',
img:'https://pic1.zhimg.com/50/v2-adfacac8307b48531d4e341a6090aa03_hd.jpg?source=1940ef5c'
}
];
同时,对于掉线的用户,我们直接从数组中删除,然后再传给前端即可:(注意啊,一定要添加条件,不然返回 -1,会把默认群聊也删除了)
/* 用户掉线 */
socket.on('disconnect',()=>{
console.log('用户离开')
/* 删除用户 */
let index=userList.findIndex(i=>i.name==socket.name);
if(index){
userList.splice(index,1);
/* 通知前端 */
io.emit('login',userList);
}
});
效果如下:
到这里,已经可以实现群聊和私聊了!
实现群聊需要两个步骤,1.发送消息到服务器,并广播给所有连接实例,2. 渲染聊天列表(难点);
send(){
/* 发送消息 */
/* 先判断是群聊还是私聊 */
this.$socket.emit('groupChat',{});
this.$socket.emit('privateChat',{});
console.log(this.input);
/* 清空输入框 */
this.input='';
},
所以,Footer组件需要一个数据,根据Aside点击的是群聊还是私聊。使用vuex管理这个数据:(vuex文件)
/* 聊天类型 */
chatType:'',
/* 修改聊天类型 */
changeChatType(state,data){
state.chatType=data;
},
我们通过Aside列表来渲染用户名,所以,在这里修改聊天类型:
setUserInfo(row, column, event){
if(row.name=='默认群聊'){
store.commit('changeChatType','group');
}else{
store.commit('changeChatType','private');
}
store.commit('setUserInfo',{name:row.name,img:row.img});
},
Footer组件监听:
methods: {
send(){
/* 发送消息 */
/* 先判断是群聊还是私聊 */
if(this.chatType=='group'){
this.$socket.emit('groupChat',{});
console.log('群聊');
}else{
this.$socket.emit('privateChat',{});
console.log('私聊');
}
console.log(this.input);
/* 清空输入框 */
this.input='';
},
},
computed:{
chatType(){
return store.state.chatType;
},
},
能识别后,就触发不同的事件即可。
if(this.chatType=='group'){
this.$socket.emit('groupChat',{
username: this.userInfo.name,
list:{
type: "my",//标记是我发的信息,但是通过服务器转发,必须是user,变成别人的,才能让别人渲染成功,不然所有人发送,都是my,就都在右边了
time: time.toLocaleString( ), //获取日期与时间,
msg: this.input,
}
});
}
一定要注意这个type。要服务器发给(除了自己)的客户端,并且修改类型为user!自己的信息就push到数组中就行了:
还是回到我们上一篇写的最后的难点:数据结构
/* 聊天记录 */
chatMessageList:[{
username: "默认群聊",
list: [
{
type: "my",
time: "",
msg: "你好啊",
},
{
type: "user",
time: "",
msg: "你好啊",
},
{
type: "user",
time: "",
msg: "你好啊",
},
],
},
],
我发的消息对于别人是 user,对于自己是my,不然没有左右的效果,我们在上一篇的时候,没有考虑群聊,我们应该在每一条消息中,都加入用户头像信息才行:
因此,修改数据传输,数据都是vuex监听得到的(computed):
/* 先判断是群聊还是私聊 */
if(this.chatType=='group'){
var data={
username: this.userInfo.name,
list:{
name:this.myInfo.name,
img:this.myInfo.img,
type: "my",//标记是我发的信息,但是通过服务器转发,必须是user,变成别人的,才能让别人渲染成功,不然所有人发送,都是my,就都在右边了
time: time.toLocaleString( ), //获取日期与时间,
msg: this.input,
}
}
this.$socket.emit('groupChat',data);
/* 自己的信息直接push到数组中 */
store.commit('SOCKET_updateChatMessageList',data);
}
vuex处理数据:(这个也是难理解的)
/* 聊天记录的修改,这里我们使用vuex监听 */
SOCKET_updateChatMessageList(state,data){
var finduser=false;
/* 数据处理:先找到自己的聊天记录 */
state.chatMessageList.forEach(list=>{
if(list.username==data.username){
finduser=true;
list.list.push(data.list)
}
});
if(!finduser){
state.chatMessageList.push({
username:data.username,
list:[data.list]
});
}
},
//为什么要自己封装这个数据,因为传过来的list是对象,需要封装为数据,便于下次直接push进去
主要是数据结构理解了就好了
服务器监听群聊事件,现在是没有做处理的,直接转发,会有一个严重的问题:
/* 监听群聊事件 */
socket.on('groupChat',data=>{
// 发送给所有客户端,除了发送者
socket.broadcast.emit('updateChatMessageList',data);
});
我群聊发的消息,在你页面显示是你的消息!为啥?因为我们定义type是my!所以服务器转发,需要处理这个小问题。
/* 监听群聊事件 */
socket.on('groupChat',data=>{
// 发送给所有客户端,除了发送者
/* 修改源数据的属性 */
data.list.type='user';
socket.broadcast.emit('updateChatMessageList',data);
});
这样,我发的,你就是收的。还有头像没有处理呢,在数据传输的时候加上头像信息。修改main循环的内容:
群聊实现了! 主要理解数据结构,怎么处理数据就好了。(后面的私聊消息的处理,也是这个数据结构)
我们还是直接使用io.sockets.sockets找人,你也可以修改代码,将socket.id放在在线人数的数组中。
实现了群聊,私聊的代码非常简单:
/* 监听私聊事件 */
socket.on('privateChat',data=>{
/* 找到对应的私聊对象 */
io.sockets.sockets.forEach(iss=>{
if(iss.name==data.username){
data.list.type='user';
io.to(iss.id).emit('updateChatMessageList',data);
}
});
});
//我们把data定义到外面了,数据结构相同
/* 先判断是群聊还是私聊 */
if(this.chatType=='group'){
this.$socket.emit('groupChat',data);
/* 自己的信息直接push到数组中 */
store.commit('SOCKET_updateChatMessageList',data);
}else{
this.$socket.emit('privateChat',data);
/* 自己的信息直接push到数组中 */
store.commit('SOCKET_updateChatMessageList',data);
}
我给你发,你收不到,你发我也收不到!问题出在我们的数据结构!我们循环遍历的是聊天对象名字,2号给1号发消息,看头上是1!但是,1号想收到消息,头上是2!!
为了让大家明白这个,我先把过滤关了:
现在是两个用户,22给1发消息,注意头上的标记:
但是没有收到!因为数据到头顶是1的人那里了:
这数据结构设计有问题!!! 再认真看一下我们的数据结构:
chatMessageList:[{
username: "默认群聊",
list: [
{
type: "my",
time: "",
msg: "你好啊",
},
{
type: "user",
time: "",
msg: "你好啊",
},
{
type: "user",
time: "",
msg: "你好啊",
},
],
}
],
现在问题应该清晰了,username不能直接用用户名做标记。默认群聊是因为所有人都是这个名字!(要求,私聊双方看到的是一样的!使用房间可以解决,但是很浪费资源,不推荐。)
因此数据结构设计不合理?百度找办法,看看别人怎么设计的......(根据发送者与接收者作为区分,群聊的话,固定接收者是’默认群聊‘即可)
chatMessageList:[{
sender:'',//发送者id
receiver: '',//接收方id
time:'',//发送时间
msg:''//消息内容
}],
因为数据结构变了,需要改变的地方挺多哦,我一一说:
循环遍历的地方:
//Mian.vue
{{list.msg}}
{{list.msg}}
条件就是发送者是我,接收者是用户,或者接收者是我,发送者是用户,这样就能包括所有可能了。
那传到服务器的数据也要变啦:(群聊默认receive是'默认群聊')
send(){
var time = new Date();
/* 发送消息 */
/* 先判断是群聊还是私聊 */
if(this.chatType=='group'){
let data={
type:'my',
sender:this.myInfo.name,//发送者id
senderimg:this.myInfo.img,//发送者的img
receiver: '默认群聊',//接收方id
time:time.toLocaleString( ),//发送时间
msg: this.input,//消息内容
}
this.$socket.emit('groupChat',data);
/* 自己的信息直接push到数组中 */
store.commit('SOCKET_updateChatMessageList',data);
}else{
let data={
type:'my',
sender:this.myInfo.name,//发送者id
senderimg:this.myInfo.img,//发送者的img
receiver:this.userInfo.name,//接收方id
time:time.toLocaleString( ),//发送时间
msg: this.input,//消息内容
}
this.$socket.emit('privateChat',data);
/* 自己的信息直接push到数组中 */
store.commit('SOCKET_updateChatMessageList',data);
}
/* 清空输入框 */
this.input='';
},
vuex监听变简单了,因为类型就是对象,所以直接push:
/* 聊天记录的修改,这里我们使用vuex监听 */
SOCKET_updateChatMessageList(state,data){
state.chatMessageList.push(data);
},
服务器监听事件注意属性变化:data.list.type变成data.type ,找人的name变了:变成receive:
/* 监听私聊事件 */
socket.on('privateChat',data=>{
/* 找到对应的私聊对象 */
io.sockets.sockets.forEach(iss=>{
if(iss.name==data.receiver){
data.type='user';
io.to(iss.id).emit('updateChatMessageList',data);
}
});
});
8. 实现效果
我打开了3个窗口:
开始群聊:
私聊:
3号的聊天窗口是空的,模拟1号与3号聊天,2号是空的。完全符合:
9. 总结:
终于将这个难关攻克下来了!过程虽然一直出错,但是我想将这个过程分享给大家,而不是写完美的结果,不然我踩过的坑,大家有可能继续,而且也想引发大家的思考。通过这个小项目,对我印象深刻!让我对vue-socket.io的使用以及期间的连接、管理等多维度的知识有了一定掌握,希望对大家有帮助!总结一下这个项目吧。
9.1 前端UI设计
前端UI设计我就不多说了,每个人都有不同的风格,但是我们要模拟登录页面,也是练习了socket.io的连接问题(autoConnect:false);同时,通过用户输入的username,绑定socket实例,便于我们私聊找人。
9.2 socket服务器的设计
现在看来,socket服务器的设计应该是最简单的。无非就是在需要的时候触发事件,在需要的时候监听事件就好了,不涉及难点。这只是小项目,真正的开发,可能需要node结合数据库,将聊天内容保存下来。(大家可以试试)
9.3 群聊的实现
这个项目中用到了两处广播:一个是用户列表的管理。通过服务器管理列表是最准确的,同时对前端也更加友好,不需要我们去处理更多的用户数据。还有的就是群聊,群聊就是广播了。不知道大家注意到没有,用户列表使用的是 io.emit('xxx',data) 而我们群聊发送消息是用的socket.broadcast.emit('broadcast', 'hello friends!');( // 发送给所有客户端,除了发送者),因为群聊设计左右的消息布局,我们在开始设计的时候,就这样啦。所以我们还需要手动的 store.commit()去修改自己的消息。其实,还有更好的办法解决。后面,我们的数据结构变了之后,可以直接根据sender是不是自己,来判断放在右边。这样,可以直接使用 io.emit(),省去很多麻烦。
9.4 私聊的实现
私聊的设计,主要是数据结构卡了一下,其实私聊还是比较简单的。主要是服务器如何找到对应私聊的那个人。我们使用了数组接收在线人数,其实也应该直接将socket.id放在数组中,这个倒是问题不大。两个方式实现难度不大。主要是你对原理理解多少啦。
9.5 项目难点
整个项目的难点,就是聊天数据结构的设计。一个好的数据结构,可以让你的项目简单好几倍!!!我深刻感受到。
9.6 项目的改进
这个项目目前已经有雏形了,但是还不是很完善,比如聊天信息自动置底,聊天应该加名称等等,有兴趣的小伙伴可以在项目基础上修改,欢迎大家提建议。
总之,这个项目做完啦,记录下来,希望对大家有所帮助。
真心不易,快两万字了。呜呜~~