赞
踩
实现了一个简单的在线聊天室的前后端。前端用Vue实现,后端用Springboot实现。
在线聊天室的功能包括创建用户和显示在线用户列表、发送消息和显示消息列表、用户和消息列表实时更新这几点。以下是整体功能的活动图:
进入聊天室的用户需要有一个身份,为了简便,只需要一个唯一的id和一个用户名即可。用户名由用户自定义,id由服务端分配。
客户端通过将id和用户名记录在sessionStorage来保存用户信息,而服务端通过用户的id及session来区分用户,为此,服务端需要维护一个在线用户列表,来记录用户的信息。
如活动图所示,当用户第一次进入聊天室时,需要输入用户名,请求服务端分配id,服务端分配id后,客户端进行记录。而用户曾进入过聊天室时,可以直接从sessionStorage中获取id和用户名。
客户端和服务端的通信方式有两种,首先,在线聊天需要两端的全双工通信,因此需要建立websocket连接。通过websocket进行通信。
但是,如活动图所示,websocket的连接需要用到用户id,而用户在首次进入聊天室时还没有id,此时就需要客户端直接发送http请求到服务端,来获取一个id。
聊天室中的在线用户列表以及消息列表需要实时更新。此功能可以通过websocket实现,每当用户进入、退出聊天室,或发送消息时,客户端会通过websocket向服务端发送消息。服务端会维护一个在线用户列表和消息列表,当收到用户消息时,就会更新这个列表,并向所有用户发送更新消息,以达到实时更新的效果。
因为是一个在线聊天室,所以就参考了微信聊天的页面,左侧显示在线用户列表,右侧显示消息窗口。如下图:
如图所示,左侧用简单卡片列表的形式展示在线用户列表,右侧是消息列表,通过调整样式,使自己的消息靠右显示,别人的消息靠左显示。以下是部分代码:
- <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>
用户身份的实现方式在实现思路中已经给出,这里不再说明。此外,前端除了在sessionStorage中存储用户信息,还会利用Vuex存储用户信息。
前后端的通信有两种方式,首先是直接发送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();
- }
- }
- }
-
-
- }
聊天室需要保证用户列表与消息列表的实时更新,这里利用了Vue的响应式数据与Vuex,通过Vuex存储用户列表与消息列表,然后在Vue组件中监听它们的变化,每当有变化时,就更新自身的数据,从而使页面实时更新。
而Vuex中数据的变化,是通过WebSocket通信实现的,实现方式在实现思路部分已经给出,这里不再说明。以下是部分代码:
- 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,
- },
- },
- };
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。