文章目录
前言
Vue是一款流行的JavaScript框架,专注于构建用户界面。它采用了组件化的方式来构建Web应用程序。Express是一个基于Node.js的后端框架,用于构建网络应用程序和API。
WebSocket协议是一种用于实时双向通信的网络协议。它可以通过单个TCP连接在客户端和服务器之间进行实时数据传输,双方一旦握手成功,就可以在一个持久连接上传输数据,而无需反复建立和关闭连接。传输的数据支持文本或二进制格式。相较于传统的HTTP请求-响应模型,WebSocket具有更低的延迟和更高的实时性。因此非常适合构建实时聊天应用或数据推送服务。
实时聊天系统非常适合于使用WebSocket这种实时通信的能力使得WebSocket在许多应用中广泛使用,例如聊天应用、实时数据推送等。
本文使用Vue和Express分别构建在线群聊系统的前端和后端程序,通过ajax和websocket完成用户登录和聊天信息的管理,并使用mysql保存用户列表和历史消息。
代码仓库:https://gitee.com/wgzj/chat-group
一、Vue实现前端界面
前端主要分为登录页面和聊天页面两部分
1.localStorage的使用
在登录页面中,用户登录后向服务端发起请求验证登录信息是否正确。登录成功的话,将用户信息和过期时间保存到localStorage中(过期时间为当前时间加上24小时),之后利用vue提供的路由功能跳转到聊天页面。
//登录响应函数
const login = ()=>{
// 验证校验规则
formRef.value.validate().then(() => {
axios.post('http://localhost:3000/user/login',userInfo.value).then(res=>{
if(res.data.status === 1){
// 登录成功
// 将username存入localStorage
const expirationDate = new Date().getTime() + 24 * 60 * 60 * 1000;
const localInfo = JSON.stringify({ id:res.data.id, username: userInfo.value.username, expirationDate: expirationDate });
localStorage.setItem('localInfo',localInfo);
ElMessage.success('登录成功');
// 跳转到首页 路由完成跳转
router.push({
path:'chat'
})
}else{
ElMessage.warning('用户信息错误');
}
}).catch(err=>{
console.log(err);
ElMessage.error('服务异常');
});
}).catch(() => {
// 校验不通过,进行错误处理或提示用户
});
}
登录页面在DOM挂载完成后会检查localStorage中是否保存有用户信息,如果用户信息不为空则将当前时间与过期时间进行比较以判断数据是否过期,过期则清除,判断用户需要重新登录。否则不需要登录直接跳转到聊天页面。
onMounted(() => {
let localInfo = localStorage.getItem('localInfo');
if(localInfo){
localInfo = JSON.parse(localInfo);
if (new Date().getTime() > localInfo.expirationDate) {
// 数据已过期,清除
localStorage.removeItem('localInfo');
return;
}
router.push({
path:'chat'
})
}
});
聊天页面在加载后会检查localStorage中是否存储了个人信息,防止用户不正确的访问。
let localInfo = localStorage.getItem('localInfo');
if(localInfo){
localInfo = JSON.parse(localInfo);
currentUser.id = localInfo.id;
currentUser.username = localInfo.username;
if(currentUser.username){
// 连接服务器
socket.value = io('http://localhost:3000');
return;
}
}
// 用户名不存在
router.push({
path:'/'
})
2.websocket通信
客户端使用io(‘http://localhost:3000’)向服务端发起连接请求,返回值为socket对象,该对象提供了emit方法用于向服务端发送消息,on方法用于接受服务端的消息。两个方法的参数都为事件名称和回调函数,事件名称需要自定义。
'message-list’事件是服务端广播聊天消息的事件名称,客户端在建立连接后不断监听该事件来获取实时聊天消息。
socket.value.on('message-list', (data) => {
// console.log('Message from server:', data);
messageList.value = data;
// 等待DOM更新完成
nextTick(()=>{
// 设置滚动条滑到底部
scrollbar.value.setScrollTop(chatInterface.value.clientHeight);
});
});
客户端同时也需要接受最新用户列表,并更新用户数据响应到页面中。
socket.value.on('user-list', (data) => {
userList.value = data;
});
客户端能够主动将用户输入的消息发送给服务端,服务端将广播最新消息,客户端同步更新。
const send = ()=>{
const msg = sendContent.value.trim();
if(msg === ''){
ElMessage.error('发送内容为空!')
}else{
// 发送消息
socket.value.emit('user-message', {id:currentUser.id, msg:msg});
sendContent.value = '';
}
}
这里是用户退出的逻辑处理,socket对象的disconnect方法用于主动断开socket连接,处理完成后删除localStorage中存储的用户信息,最后路由到登录页面。
const exit = ()=>{
socket.value.disconnect();
localStorage.removeItem('localInfo');
router.push({
path:'/'
})
}
二、Express搭建后端服务
1.安装依赖
使用npm安装以下依赖
- body-parser 用于解析http请求数据转为js对象
- cors 用于处理跨域资源共享
- moment 是操作日期和时间的js库
- mysql2 连接mysql数据库
- socket.io 支持WebSocket通信的js库
npm install body-parser cors express moment mysql2 socket.io
2.创建服务器和mysql连接池
代码如下:
const app = require('express')();
const bodyParser = require('body-parser');
const cors = require('cors');
app.use(cors());
app.use(bodyParser.json());
const server = require('http').createServer(app);
const io = require('socket.io')(server, { cors: true });
const mysql = require('mysql2');
const moment = require('moment');
// 创建 MySQL 数据库连接池
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'root',
database: 'group_chat',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
后端需要操作数据库,进行管理用户、获取用户列表和查询实时消息的操作
// 获取用户
const getUserByName = (username)=>{
return new Promise((resolve, reject) => {
pool.query('SELECT * FROM user WHERE username = ?', username, (err, results, fields) => {
if (err) {
console.error('Error querying data:', err.stack);
reject(err);
return;
}
// 将结果传递给 Promise 的 resolve
resolve(results);
});
});
}
// 新增消息
const saveMessage = (data)=>{
return new Promise((resolve, reject) => {
const msg = {
content: data.msg,
user_id: data.id,
time: new Date()
}
pool.query('INSERT INTO message SET ?', msg, (err, insertResults) => {
if (err) {
console.error('Error inserting data:', err.stack);
reject(err);
return;
}
// 插入成功,获取最新记录
pool.query('SELECT username,content,time FROM message inner join user on message.user_id=user.id order by time', (err, selectResults) => {
if (err) {
console.error('Error querying data:', err.stack);
reject(err);
return;
}
// 修改日期格式
selectResults.forEach(item => {
item.time = moment(item.time).format('YYYY-MM-DD HH:mm:ss');
});
resolve(selectResults);
});
});
});
}
其他数据库操作与此类似
3.处理登录请求
判断用户是否是新用户,是则自动注册该用户。验证完成后返回用户的id。
// 处理登录请求
app.post('/user/login', (req, res) => {
const username = req.body.username;
const password = req.body.password;
console.log('\x1b[37m', '登录请求', req.body);
getUserByName(username).then((user) => {
if(user.length === 0){
// 新增用户
saveUser({username: username, password: password}).then((result) => {
res.json({id:result.insertId, status:1});
})
.catch((err) => {
console.error('Error getting message list:', err);
res.status(500).send('服务异常');
});
}else if(user[0].username === username && user[0].password === password){
// 验证已有用户
res.json({id:user[0].id, status:1});
}else{
res.json({id:-1, status:0});
}
})
.catch((err) => {
console.error('Error getting user:', err);
res.status(500).send('服务异常');
});
});
4.websocket监听消息
- 在socket.io中io.on方法用于监听不同的事件,第一个参数用于指定事件名称,第二个参数是事件触发时的回调函数。此处用于监听connection连接事件,并为每个建立的连接创建一个socket对象。客户端初次连接服务器时会触发该事件。
- socket.on用于给socket对象绑定不同的事件,这里监听客户端的登录消息、发送聊天的消息和断开连接的消息。
- 在’user-login’的消息事件中,保存每个socket连接的id和对应的用户信息,其次查询历史消息,使用socket.emit将消息列表返回给该socket连接的客户,之后获取用户列表并使用io.emit方法向所有客户端广播最新的用户列表。
- 在’user-message’事件中,获取到用户发送的消息后,将消息写入数据库并查询最新的消息列表,同样广播发送到每个客户端,以使所有用户的消息保持同步。
- 最后在用户断开连接的’disconnect’事件中,删除缓存中的socket连接的id。
io.on('connection', (socket) => {
console.log('\x1b[37m', 'a socket connected, id: ', socket.id);
// 监听登录消息
socket.on('user-login', (username) => {
console.log('\x1b[36m', username + '进入聊天室');
//保存用户名和对应id
users.set(socket.id, username);
//查询历史消息
getMessageList().then((messageList) => {
// 向客户端发送消息列表
socket.emit('message-list', messageList);
})
.catch((err) => {
console.error('Error getting message list:', err);
});
//查询用户列表
getUserList().then((userList) => {
// 向所有用户发送最新的用户列表
io.emit('user-list', userList);
})
.catch((err) => {
console.error('Error getting message list:', err);
});
});
// 监听聊天消息
socket.on('user-message', (data) => {
console.log('\x1b[37m', users.get(socket.id)+'发送:', data.msg);
saveMessage(data).then((messageList) => {
// 处理查询结果
// 向所有用户发送最新消息列表
io.emit('message-list', messageList);
})
.catch((err) => {
console.error('Error getting message list:', err);
});
});
// 监听断开连接
socket.on('disconnect', () => {
console.log('\x1b[31m', users.get(socket.id) + '离开聊天室');
//删除用户名和对应id
users.delete(socket.id);
});
});