实现了一个简单的在线聊天室的前后端。前端用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通信实现的,实现方式在实现思路部分已经给出,这里不再说明。以下是部分代码:

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: 简单的在线聊天网站的前后端

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐