实现了一个简单的在线聊天室的前后端。前端用Vue实现,后端用Springboot实现。
一、项目描述
1. 整体功能描述
在线聊天室的功能包括创建用户和显示在线用户列表、发送消息和显示消息列表、用户和消息列表实时更新这几点。以下是整体功能的活动图:
2. 实现思路
用户身份
进入聊天室的用户需要有一个身份,为了简便,只需要一个唯一的id和一个用户名即可。用户名由用户自定义,id由服务端分配。
客户端通过将id和用户名记录在sessionStorage来保存用户信息,而服务端通过用户的id及session来区分用户,为此,服务端需要维护一个在线用户列表,来记录用户的信息。
如活动图所示,当用户第一次进入聊天室时,需要输入用户名,请求服务端分配id,服务端分配id后,客户端进行记录。而用户曾进入过聊天室时,可以直接从sessionStorage中获取id和用户名。
通信方式
客户端和服务端的通信方式有两种,首先,在线聊天需要两端的全双工通信,因此需要建立websocket连接。通过websocket进行通信。
但是,如活动图所示,websocket的连接需要用到用户id,而用户在首次进入聊天室时还没有id,此时就需要客户端直接发送http请求到服务端,来获取一个id。
实时更新
聊天室中的在线用户列表以及消息列表需要实时更新。此功能可以通过websocket实现,每当用户进入、退出聊天室,或发送消息时,客户端会通过websocket向服务端发送消息。服务端会维护一个在线用户列表和消息列表,当收到用户消息时,就会更新这个列表,并向所有用户发送更新消息,以达到实时更新的效果。
二、具体实现
1. 前端整体布局
因为是一个在线聊天室,所以就参考了微信聊天的页面,左侧显示在线用户列表,右侧显示消息窗口。如下图:
如图所示,左侧用简单卡片列表的形式展示在线用户列表,右侧是消息列表,通过调整样式,使自己的消息靠右显示,别人的消息靠左显示。以下是部分代码:
<template>
<div class="chatRoom">
<div class="personList">
<div class="title">
<h1>在线聊天室</h1>
</div>
<!-- 在线用户列表 -->
<div class="online-person">
<span class="title">在线用户</span>
<div class="person-cards-wrapper">
<div
class="personList"
v-for="personInfo in personList"
:key="personInfo.id"
>
<PersonCard :personInfo="personInfo"></PersonCard>
</div>
</div>
</div>
</div>
<!-- 聊天窗口 -->
<div class="chatContent">
<ChatWindow></ChatWindow>
</div>
</div>
</template>
<!-- 聊天窗口组件 -->
<template>
<div class="chat-window">
<div class="bottom">
<!-- 聊天信息列表 -->
<div class="chat-content" ref="chatContent">
<div class="chat-wrapper" v-for="item in chatList" :key="item.id">
<!-- 通过uid判断是自己的消息还是别人的 -->
<div class="chat-friend" v-if="item.uid != $store.state.id">
<div class="chat-text">
{{ item.msg }}
</div>
<div class="info-time">
<span>{{ item.name }}</span>
<span>{{ item.time }}</span>
</div>
</div>
<div class="chat-me" v-else>
<!-- 略 -->
</div>
</div>
</div>
<div class="chatInputs">
<input class="inputs" v-model="inputMsg" @keyup.enter="sendText" />
<div class="send box" @click="sendText">
<span class="sendText">发送</span>
</div>
</div>
</div>
</div>
</template>
2. 用户身份
用户身份的实现方式在实现思路中已经给出,这里不再说明。此外,前端除了在sessionStorage中存储用户信息,还会利用Vuex存储用户信息。
3. 前后端通信
前后端的通信有两种方式,首先是直接发送http请求,比如用户请求分配id时,这里是采用axios的方式像后端接口发送请求。
然后是websocket连接,前端需要始终与后端保持websocket连接。需要注意的是,建立连接的时机需要保证在用户得到id后。以下是部分代码:
前端:
<!-- 输入用户名的对话框 -->
<div v-if="showDialog" class="dialog-wrapper">
<div class="dialog">
<h2 class="dialog-title">请输入您的昵称</h2>
<el-input
v-model="inputName"
class="dialog-content"
placeholder="请输入内容"
maxlength="10"
></el-input>
<button @click="closeDialog" class="dialog-button">确定</button>
</div>
</div>
<script>
mounted() {
// 如果sessionStorage中无用户id,则需输入用户名,分配id
if (!sessionStorage.hasOwnProperty("id")) {
this.openDialog();
} else {
// 如果sessionStorage中有,则无需分配,直接用vuex存储
this.$store.dispatch("setUserId", sessionStorage.getItem("id"));
this.$store.dispatch("setUserName", sessionStorage.getItem("name"));
console.log("增加已有用户:" + this.$store.state.name);
addUser({ id: this.$store.state.id, name: this.$store.state.name }).then(
(res) => {
// 建立websocket连接,要保证addUser后服务端才发送更新消息
this.$store.dispatch("connect");
}
);
}
},
methods: {
openDialog() {
this.showDialog = true;
},
closeDialog() {
if (this.inputName.length == 0) {
this.$message({
message: "用户名不能为空",
type: "warning",
});
} else {
newUser(this.inputName).then((res) => {
// 将获取到的id用vuex存储
this.$store.dispatch("setUserId", res);
this.$store.dispatch("setUserName", this.inputName);
// 将数据存储到sessionStorage
sessionStorage.setItem("id", res);
sessionStorage.setItem("name", this.inputName);
// 建立websocket连接,需要用户id, 因此在分配id后进行
this.$store.dispatch("connect");
});
this.showDialog = false;
}
},
},
</script>
export const newUser = (param) => {
return axios({
method: 'post',
baseURL: `${baseUrl}/user/new/${param}`,
}).then(res => res.data)
}
export const addUser = (param) =>{
return axios({
method: 'post',
baseURL: `${baseUrl}/user/add`,
data: param,
}).then(res => res.data)
}
后端:
@RestController
@RequestMapping("user")
public class UserController {
// 用户第一次进入,分配id
@PostMapping("new/{name}")
public int newUser(@PathVariable String name) {
System.out.println(name);
User user = new User(++Storage.cur_id, name);
Storage.userList.put(user.getId(), user);
return Storage.cur_id;
}
// 用户再次进入,无需分配id
@PostMapping("add")
public void addUser(@RequestBody User user) {
System.out.println(user);
Storage.userList.put(user.getId(), user);
}
}
public class MyWebSocket {
private Session session;
private String userId;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static final CopyOnWriteArraySet<MyWebSocket> webSockets = new CopyOnWriteArraySet<>();
// 用来存在线连接用户信息
private static final ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<String, Session>();
// 连接成功调用的方法
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") String userId) {
this.session = session;
this.userId = userId;
webSockets.add(this);
sessionPool.put(userId, session); // 存储用户的session
// 有新用户连接后,需要通知所有用户更新在线用户列表
sendAllMessage(JSON.toJSONString(new SocketMessage(1, Storage.getUserList(), null)));
sendOneMessage(userId, JSON.toJSONString(new SocketMessage(2, null, Storage.getMsgList())));
}
// 连接关闭调用的方法
@OnClose
public void onClose() {
webSockets.remove(this);
sessionPool.remove(this.userId);
// 将用户从在线列表中删掉,并通知所有用户
Storage.userList.remove(Integer.parseInt(this.userId));
sendAllMessage(JSON.toJSONString(new SocketMessage(1, Storage.getUserList(), null)));
}
// 收到客户端消息后调用的方法
@OnMessage
public void onMessage(String message) {
Message msg = JSON.parseObject(message, Message.class);
Storage.msgList.add(msg);
sendAllMessage(JSON.toJSONString(new SocketMessage(2, null, Storage.getMsgList())));
}
// 单点消息
public void sendOneMessage(String userId, String message) {
Session session = sessionPool.get(userId);
if (session != null && session.isOpen()) {
log.info("【websocket消息】 单点消息:" + message);
session.getAsyncRemote().sendText(message);
}
}
// 广播消息
public void sendAllMessage(String message) {
for (MyWebSocket webSocket : webSockets) {
try {
if (webSocket.session.isOpen()) {
webSocket.session.getAsyncRemote().sendText(message);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
4. 发送消息与实时更新
聊天室需要保证用户列表与消息列表的实时更新,这里利用了Vue的响应式数据与Vuex,通过Vuex存储用户列表与消息列表,然后在Vue组件中监听它们的变化,每当有变化时,就更新自身的数据,从而使页面实时更新。
而Vuex中数据的变化,是通过WebSocket通信实现的,实现方式在实现思路部分已经给出,这里不再说明。以下是部分代码:文章来源:https://www.toymoban.com/news/detail-791637.html
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const state = {
socket: null,
id: "",
name: "",
userList: [],
msgList: [],
};
const mutations = {
setSocket(state, socket) {
state.socket = socket;
},
setId(state, id) {
state.id = id;
},
setName(state, name) {
state.name = name;
},
setUserList(state, userList) {
state.userList = userList;
},
setMsgList(state, msgList) {
state.msgList = msgList;
},
}
const actions = {
connect(context) {
const socket = new WebSocket('ws://localhost:8089/chat/' + context.state.id)
socket.onopen = () => {
console.log("建立websocket连接")
}
socket.onmessage = (event) => {
// 接收消息的情况有两种:更新用户列表和更新消息列表
console.log("收到服务端的消息:" + event.data);
let msg = JSON.parse(event.data);
if (msg.type == '1') {
console.log("更新用户列表: ");
context.commit("setUserList", msg.userList);
} else if (msg.type == '2') {
console.log("更新消息列表");
context.commit("setMsgList", msg.msgList);
}
}
context.commit("setSocket", socket)
},
setUserId(context, id) {
console.log("设置用户id为:" + id);
context.commit("setId", id);
},
setUserName(context, name) {
console.log("设置用户name为:" + name);
context.commit("setName", name);
},
sendMsg(context, msg) {
context.state.socket.send(msg);
}
};
export default new Vuex.Store({
actions,
mutations,
state,
});
export default {
data() {
return {
chatList: [],
inputMsg: "",
};
},
methods: {
//获取窗口高度并滚动至最底层
scrollBottom() {
this.$nextTick(() => {
const scrollDom = this.$refs.chatContent;
scrollDom.scrollTop = scrollDom.scrollHeight;
});
},
//发送文字信息
sendText() {
if (this.inputMsg) {
const now = new Date();
const year = now.getFullYear();
const month = ("0" + (now.getMonth() + 1)).slice(-2);
const day = ("0" + now.getDate()).slice(-2);
const hours = ("0" + now.getHours()).slice(-2);
const minutes = ("0" + now.getMinutes()).slice(-2);
const seconds = ("0" + now.getSeconds()).slice(-2);
const formattedTime =
year +
"-" +
month +
"-" +
day +
" " +
hours +
":" +
minutes +
":" +
seconds;
let msg = {
name: this.$store.state.name,
time: formattedTime,
msg: this.inputMsg,
uid: this.$store.state.id,
};
// 向websocket服务端发送消息
this.$store.dispatch("sendMsg", JSON.stringify(msg));
this.inputMsg = "";
} else {
this.$message({
message: "消息不能为空",
type: "warning",
});
}
},
},
computed: {
getChatMsgList() {
return this.$store.state.msgList;
},
},
watch: {
// 更新消息列表
getChatMsgList: {
handler: function (newVal, oldVal) {
console.log("更新chatList");
this.chatList = this.getChatMsgList;
this.scrollBottom();
},
deep: true,
},
},
};
三、代码仓库
ChatRoom: 简单的在线聊天网站的前后端文章来源地址https://www.toymoban.com/news/detail-791637.html
到了这里,关于在线聊天室(Vue+Springboot+WebSocket)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!