socket实现聊天室
案例:实现聊天室
基于node+express+socket.io+vue+flex布局来实现简易的聊天室
实现功能
登录检测
系统提示在线人员状态(进入/离开)
接收和发送消息
自定义消息字体颜色
支持发送表情
支持发送图片
支持发送窗口震动
项目结构:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="stylesheet" href="style/index.css">
<link rel="stylesheet" href="style/font-awesome-4.7.0/css/font-awesome.min.css">
</head>
<body>
<div id="app">
<div class="name" v-if='isShow'>
<!-- <h2>请输入你的昵称</h2> -->
<input @keyup.enter='handleClick' type="text" id="name" placeholder="请输入昵称..." autocomplete="off"
v-model='username'>
<button id="nameBtn" @click='handleClick'>确 定</button>
</div>
<div class="main" :class='{shaking:isShake}'>
<div class="header">
<img src="image/logo.jpg">
❤️聊天室
</div>
<div id="container">
<div class="conversation">
<ul id="messages">
<li v-for='(user,index) in userSystem' :class='user.side'>
<div v-if='user.isUser'>
<img :src="user.img" alt="">
<div>
<span>{{user.name}}</span>
<p :style="{color: user.color}" v-html='user.msg'>
</p>
</div>
</div>
<p class='system' v-else>
<span>{{nowDate}}</span><br />
<span v-if='user.status'>{{user.name}}{{user.status}}了聊天室</span>
<span v-else>{{user.name}}发送了一个窗口抖动</span>
</p>
</li>
</ul>
<form action="">
<div class="edit">
<input type="color" id="color" v-model='color'>
<i title="自定义字体颜色" id="font" class="fa fa-font">
</i><i @click='handleSelectEmoji' @dblclick='handleDoubleSelectEmoji' title="双击取消选择" class="fa fa-smile-o" id="smile">
</i><i @click='handleShake' title="单击页面震动" id="shake" class="fa fa-bolt">
</i>
<input type="file" id="file">
<i class="fa fa-picture-o" id="img"></i>
<div class="selectBox" v-show='isEmojiShow'>
<div class="smile" id="smileDiv">
<p>经典表情</p>
<ul class="emoji">
<li v-for='(emojiSrc,i) in emojis' :id='i' :key='i'>
<img :src="emojiSrc" :alt="i+1" @click='handleEmojiImg(i+1)'>
</li>
</ul>
</div>
</div>
</div>
<!-- autocomplete禁用自动完成功能 -->
<textarea id="m" v-model='msgVal' autofocus @keyup.enter='handleSendMsg'></textarea>
<button class="btn rBtn" id="sub" @click='handleSendMsg'>发送</button>
<button class="btn" id="clear" @click='handleLogout'>关闭</button>
</form>
</div>
<div class="contacts">
<h1>在线人员(<span id="num">{{userInfo.length}}</span>)</h1>
<ul id="users">
<li v-for='(user,index) in userInfo'>
<img :src="user.img" alt="">
<span>{{user.username}}</span>
</li>
</ul>
<p v-if='userInfo.length==0'>当前无人在线哟~</p>
</div>
</div>
</div>
</div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
<script src='js/client.js'></script>
</body>
</html>
server.js
const express = require('express');
const app = express();1
const http = require('http').Server(app);
const io = require('socket.io')(http);
const port = process.env.PORT || 5000;
let users = [];//存储登录的用户
let userInfo = [];//存储用户姓名和头像
app.use('/',express.static(__dirname+'/static'));
app.get('/', function (req, res) {
res.sendFile(__dirname + '/index.html');
});
io.on('connection', function (socket) {
console.log('连接成功');
socket.on('login', function (user) {
const {username} = user;
console.log(users.indexOf(username))
if (users.indexOf(username)>-1){
socket.emit('loginError');
}else{
// 存储用户名
users.push(username);
userInfo.push(user);
io.emit('loginSuc');
socket.nickName = username;
// 系统通知
io.emit('system', {
name: username,
status: '进入'
})
// 显示在线人员
io.emit('disUser',userInfo);
console.log('一个用户登录');
}
});
// 发送窗口事件
socket.on('shake',()=>{
socket.emit('shake',{
name:'您'
})
// 广播消息
socket.broadcast.emit('shake',{
name:socket.nickName
})
});
// 发送消息事件
socket.on('sendMsg',(data)=>{
let img = '';
for(let i = 0; i < userInfo.length;i++){
if(userInfo[i].username === socket.nickName){
img = userInfo[i].img;
}
}
// 广播
socket.broadcast.emit('receiveMsg', {
name: socket.nickName,
img: img,
msg: data.msgVal,
color: data.color,
type: data.type,
side: 'left',
isUser:true
});
socket.emit('receiveMsg', {
name: socket.nickName,
img: img,
msg: data.msgVal,
color: data.color,
type: data.type,
side: 'right',
isUser: true
});
})
// 断开连接时
socket.on('disconnect',()=>{
console.log('断开连接');
let index = users.indexOf(socket.nickName);
if(index > -1){
users.splice(index,1);//删除用户信息
userInfo.splice(index,1);//删除用户信息
io.emit('system',{
name:socket.nickName,
status:'离开'
})
io.emit('disUser,userInfo'); //重新渲染
console.log('一个用户离开');
}
})
});
http.listen(3000, () => {
console.log('listen on 3000端口');
})
client.js
const vm = new Vue({
el: '#app',
data() {
return {
username: '',
msgVal: '',
isShow: true,
nowDate: new Date().toTimeString().substr(0, 8),
userHtml: '',
userInfo: [],
isShake: false,
timer: null,
userSystem: [],
color: '#000000',
emojis: [],
isEmojiShow: false
}
},
methods: {
handleClick() {
var imgN = Math.floor(Math.random() * 4) + 1; // 随机分配头像
if (this.username) {
this.socket.emit('login', {
username: this.username,
img: 'image/user' + imgN + '.jpg'
});
}
},
shake() {
this.isShake = true;
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.isShake = false;
}, 500);
},
handleShake(e) {
this.socket.emit('shake');
},
// 发送消息
handleSendMsg(e) {
e.preventDefault();
if (this.msgVal) {
this.socket.emit('sendMsg', {
msgVal: this.msgVal,
color: this.color,
type: 'text'
})
this.msgVal = '';
}
},
scrollBottom() {
this.$nextTick(() => {
const div = document.getElementById('messages');
div.scrollTop = div.scrollHeight;
})
},
initEmoji() {
for (let i = 0; i < 141; i++) {
this.emojis.push(`image/emoji/emoji (${i + 1}).png`);
}
},
// 点击微笑 弹出表情
handleSelectEmoji() {
this.isEmojiShow = true;
},
handleDoubleSelectEmoji() {
this.isEmojiShow = false;
},
// 用户点击发送表情
handleEmojiImg(index) {
this.isEmojiShow = false;
this.msgVal = this.msgVal + `[emoji${index}]`;
},
handleLogout(e) {
e.preventDefault();
this.socket.emit('disconnect')
}
},
created() {
const socket = io();
this.socket = socket;
this.socket.on('loginSuc', () => {
this.isShow = false;
});
this.socket.on('loginError', () => {
alert('用户名已存在,请重新输入');
this.username = '';
})
// 系统提示消息
this.socket.on('system', (user) => {
console.log(user);
this.userSystem.push(user);
this.scrollBottom()
})
// 显示在线人员
this.socket.on('disUser', (userInfo) => {
this.userInfo = userInfo;
})
//监听抖动事件
this.socket.on('shake', (user) => {
this.userSystem.push(user);
this.shake();
this.scrollBottom();
})
this.socket.on('receiveMsg', (obj) => {
let msg = obj.msg;
let content = ''
if (obj.type === 'img') {
}
// 提取文字中的表情加以渲染
while (msg.indexOf('[') > -1) { // 其实更建议用正则将[]中的内容提取出来
var start = msg.indexOf('[');
var end = msg.indexOf(']');
content += '<span>' + msg.substr(0, start) + '</span>';
content += '<img src="image/emoji/emoji%20(' + msg.substr(start + 6, end - start - 6) + ').png">';
msg = msg.substr(end + 1, msg.length);
}
content += '<span>' + msg + '</span>';
obj.msg = content;
this.userSystem.push(obj);
// 滚动条总是在最底部
this.scrollBottom();
})
// 渲染表情
this.initEmoji();
}
})