一、前言
本文將基於 SpringBoot
+ Vue
+ WebSocket
實現一個簡單的在線聊天功能
頁面如下:
在線體驗地址:http://www.zhengqingya.com:8101
二、SpringBoot
+ Vue
+ WebSocket
實現在線聊天
1、引入websocket依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2、websocket 配置類
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3、websocket 處理類Controller
@Slf4j
@Component
@ServerEndpoint("/groupChat/{sid}/{userId}")
public class WebSocketServerController {
/**
* 房間號 -> 組成員信息
*/
private static ConcurrentHashMap<String, List<Session>> groupMemberInfoMap = new ConcurrentHashMap<>();
/**
* 房間號 -> 在線人數
*/
private static ConcurrentHashMap<String, Set<Integer>> onlineUserMap = new ConcurrentHashMap<>();
/**
* 收到消息調用的方法,群成員發送消息
*
* @param sid:房間號
* @param userId:用戶id
* @param message:發送消息
*/
@OnMessage
public void onMessage(@PathParam("sid") String sid, @PathParam("userId") Integer userId, String message) {
List<Session> sessionList = groupMemberInfoMap.get(sid);
Set<Integer> onlineUserList = onlineUserMap.get(sid);
// 先一個群組內的成員發送消息
sessionList.forEach(item -> {
try {
// json字符串轉對象
MsgVO msg = JSONObject.parseObject(message, MsgVO.class);
msg.setCount(onlineUserList.size());
// json對象轉字符串
String text = JSONObject.toJSONString(msg);
item.getBasicRemote().sendText(text);
} catch (IOException e) {
e.printStackTrace();
}
});
}
/**
* 建立連接調用的方法,群成員加入
*
* @param session
* @param sid
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid, @PathParam("userId") Integer userId) {
List<Session> sessionList = groupMemberInfoMap.computeIfAbsent(sid, k -> new ArrayList<>());
Set<Integer> onlineUserList = onlineUserMap.computeIfAbsent(sid, k -> new HashSet<>());
onlineUserList.add(userId);
sessionList.add(session);
// 發送上線通知
sendInfo(sid, userId, onlineUserList.size(), "上線了~");
}
public void sendInfo(String sid, Integer userId, Integer onlineSum, String info) {
// 獲取該連接用戶信息
User currentUser = ApplicationContextUtil.getApplicationContext().getBean(UserMapper.class).selectById(userId);
// 發送通知
MsgVO msg = new MsgVO();
msg.setCount(onlineSum);
msg.setUserId(userId);
msg.setAvatar(currentUser.getAvatar());
msg.setMsg(currentUser.getNickName() + info);
// json對象轉字符串
String text = JSONObject.toJSONString(msg);
onMessage(sid, userId, text);
}
/**
* 關閉連接調用的方法,群成員退出
*
* @param session
* @param sid
*/
@OnClose
public void onClose(Session session, @PathParam("sid") String sid, @PathParam("userId") Integer userId) {
List<Session> sessionList = groupMemberInfoMap.get(sid);
sessionList.remove(session);
Set<Integer> onlineUserList = onlineUserMap.get(sid);
onlineUserList.remove(userId);
// 發送離線通知
sendInfo(sid, userId, onlineUserList.size(), "下線了~");
}
/**
* 傳輸消息錯誤調用的方法
*
* @param error
*/
@OnError
public void OnError(Throwable error) {
log.info("Connection error");
}
}
4、websocket 消息顯示類
@Data
@ApiModel(description = "websocket消息內容")
public class MsgVO {
@ApiModelProperty(value = "用戶id")
private Integer userId;
@ApiModelProperty(value = "用戶名")
private String username;
@ApiModelProperty(value = "用戶頭像")
private String avatar;
@ApiModelProperty(value = "消息")
private String msg;
@ApiModelProperty(value = "在線人數")
private int count;
}
5、前端頁面
溫馨小提示:當用戶登錄成功之后,可以發起websocket連接,存在store中...
下面只是單頁面的簡單實現
<template>
<div class="chat-box">
<header>聊天室 (在線:{{count}}人)</header>
<div class="msg-box" ref="msg-box">
<div
v-for="(i,index) in list"
:key="index"
class="msg"
:style="i.userId == userId?'flex-direction:row-reverse':''"
>
<div class="user-head">
<img :src="i.avatar" height="30" width="30" :title="i.username">
</div>
<div class="user-msg">
<span :style="i.userId == userId?' float: right;':''" :class="i.userId == userId?'right':'left'">{{i.content}}</span>
</div>
</div>
</div>
<div class="input-box">
<input type="text" ref="sendMsg" v-model="contentText" @keyup.enter="sendText()" />
<div class="btn" :class="{['btn-active']:contentText}" @click="sendText()">發送</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
ws: null,
count: 0,
userId: this.$store.getters.id, // 當前用戶ID
username: this.$store.getters.name, // 當前用戶昵稱
avatar: this.$store.getters.avatar, // 當前用戶頭像
list: [], // 聊天記錄的數組
contentText: "" // input輸入的值
};
},
mounted() {
this.initWebSocket();
},
destroyed() {
// 離開頁面時關閉websocket連接
this.ws.onclose(undefined);
},
methods: {
// 發送聊天信息
sendText() {
let _this = this;
_this.$refs["sendMsg"].focus();
if (!_this.contentText) {
return;
}
let params = {
userId: _this.userId,
username: _this.username,
avatar: _this.avatar,
msg: _this.contentText,
count: _this.count
};
_this.ws.send(JSON.stringify(params)); //調用WebSocket send()發送信息的方法
_this.contentText = "";
setTimeout(() => {
_this.scrollBottm();
}, 500);
},
// 進入頁面創建websocket連接
initWebSocket() {
let _this = this;
// 判斷頁面有沒有存在websocket連接
if (window.WebSocket) {
var serverHot = window.location.hostname;
let sip = '房間號'
// 填寫本地IP地址 此處的 :9101端口號 要與后端配置的一致!
var url = 'ws://' + serverHot + ':9101' + '/groupChat/' + sip + '/' + this.userId; // `ws://127.0.0.1/9101/groupChat/10086/聊天室`
let ws = new WebSocket(url);
_this.ws = ws;
ws.onopen = function(e) {
console.log("服務器連接成功: " + url);
};
ws.onclose = function(e) {
console.log("服務器連接關閉: " + url);
};
ws.onerror = function() {
console.log("服務器連接出錯: " + url);
};
ws.onmessage = function(e) {
//接收服務器返回的數據
let resData = JSON.parse(e.data)
_this.count = resData.count;
_this.list = [
..._this.list,
{ userId: resData.userId, username: resData.username, avatar: resData.avatar, content: resData.msg }
];
};
}
},
// 滾動條到底部
scrollBottm() {
let el = this.$refs["msg-box"];
el.scrollTop = el.scrollHeight;
}
}
};
</script>
<style lang="scss" scoped>
.chat-box {
margin: 0 auto;
background: #fafafa;
position: absolute;
height: 100%;
width: 100%;
max-width: 700px;
header {
position: fixed;
width: 100%;
height: 3rem;
background: #409eff;
max-width: 700px;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
color: white;
font-size: 1rem;
}
.msg-box {
position: absolute;
height: calc(100% - 6.5rem);
width: 100%;
margin-top: 3rem;
overflow-y: scroll;
.msg {
width: 95%;
min-height: 2.5rem;
margin: 1rem 0.5rem;
position: relative;
display: flex;
justify-content: flex-start !important;
.user-head {
min-width: 2.5rem;
width: 20%;
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
background: #f1f1f1;
display: flex;
justify-content: center;
align-items: center;
.head {
width: 1.2rem;
height: 1.2rem;
}
// position: absolute;
}
.user-msg {
width: 80%;
// position: absolute;
word-break: break-all;
position: relative;
z-index: 5;
span {
display: inline-block;
padding: 0.5rem 0.7rem;
border-radius: 0.5rem;
margin-top: 0.2rem;
font-size: 0.88rem;
}
.left {
background: white;
animation: toLeft 0.5s ease both 1;
}
.right {
background: #53a8ff;
color: white;
animation: toright 0.5s ease both 1;
}
@keyframes toLeft {
0% {
opacity: 0;
transform: translateX(-10px);
}
100% {
opacity: 1;
transform: translateX(0px);
}
}
@keyframes toright {
0% {
opacity: 0;
transform: translateX(10px);
}
100% {
opacity: 1;
transform: translateX(0px);
}
}
}
}
}
.input-box {
padding: 0 0.5rem;
position: absolute;
bottom: 0;
width: 100%;
height: 3.5rem;
background: #fafafa;
box-shadow: 0 0 5px #ccc;
display: flex;
justify-content: space-between;
align-items: center;
input {
height: 2.3rem;
display: inline-block;
width: 100%;
padding: 0.5rem;
border: none;
border-radius: 0.2rem;
font-size: 0.88rem;
}
.btn {
height: 2.3rem;
min-width: 4rem;
background: #e0e0e0;
padding: 0.5rem;
font-size: 0.88rem;
color: white;
text-align: center;
border-radius: 0.2rem;
margin-left: 0.5rem;
transition: 0.5s;
}
.btn-active {
background: #409eff;
}
}
}
</style>