SpringBoot+Vue+Websocket 实现服务器端向客户端主动发送消息
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot+Vue+Websocket 实现服务器端向客户端主动发送消息相关的知识,希望对你有一定的参考价值。
参考技术A 本文通过一个实际的场景来介绍在前后端分离的项目中通过 WebSocket 来实现服务器端主动向客户端发送消息的应用。主要内容如下Websocket 是一种在单个 TCP 连接上进行全双工通信的协议。WebSocket 连接成功后,服务端与客户端可以双向通信。在需要消息推送的场景,Websocket 相对于轮询能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
具体如下特点
在客户端的列表数据中有个 status 字段,服务器端需要花费较长的时间进行处理,处理完成后才会更新对应数据的 status 字段值,通过 Websocket 的处理流程如下:
通过注入 ServerEndpointExporter 类,用于在项目启动的时候自动将使用了 @ServerEndpoint 注解声明的 Websocket endpoint 注册到 WebSocketContainer 中。
为什么增加一个 ServerEndpointExporter Bean,并通过在一个类上增加 @ServerEndpoint 和 @Component 注解就可以实现服务器端 Websocket 功能,这里简单解析一下。
java 定义了一套 javax.servlet-api, 一个 HttpServlet 就是一个 HTTP 服务。java websocket 并非基于 servlet-api 简单扩展, 而是新定义了一套 javax.websocket-api。
一个 websocket 服务对应一个 Endpoint。与 ServletContext 对应, websocket-api 也定义了 WebSocketContainer, 而编程方式注册 websocket 的接口是继承自 WebSocketContainer 的 ServerContainer。
一个 websocket 可以接受并管理多个连接, 因此可被视作一个 server。主流 servlet 容器都支持 websocket, 如 tomcat, jetty 等。看 ServerContainer api 文档, 可从 ServletContext attribute 找到 ServerContainer。
springboot+websocket+vue+vuex实现在线聊天(客户端)
一、使用node代理ws请求
proxyObj['/ws'] = {
ws: true,
target: "ws://localhost:8081"
}
导入连接需要连接websocket的js
npm install sockjs-client
npm install stompjs
npm install sass-loader@8.0.2 --save //这个是css写法需要用到的 不加版本会报错
npm install node-sass@4.14.1 --save //安装sass-loader 需要有node-sass
二、创建聊天窗口
可以使用一个按钮来跳转到聊天页面
<el-button type="text" icon="el-icon-message-solid" style="color: black;margin-right: 5px" @click="goChat" ></el-button>
goChat(){
this.$router.push('/chat');
}
三、聊天页面绘制 (引入第三方的样式) 主页面
<template>
<div id="app">
<div class="sidebar">
<card></card>
<list></list>
</div>
<div class="main">
<message></message>
<usertext></usertext>
</div>
</div>
</template>
<script>
import card from "@/components/chat/card";
import list from "@/components/chat/list";
import message from "@/components/chat/message";
import usertext from "@/components/chat/usertext";
export default {
name: 'Chat',
data () {
return {
}
},
mounted:function() {
this.$store.dispatch('initData');
},
components:{
card,
list,
message,
usertext
}
}
</script>
<style lang="scss" scoped>
#app {
margin: 20px auto;
width: 800px;
height: 600px;
overflow: hidden;
border-radius: 10px;
.sidebar, .main {
height: 100%;
}
.sidebar {
float: left;
color: #f4f4f4;
background-color: #2e3238;
width: 200px;
}
.main {
position: relative;
overflow: hidden;
background-color: #eee;
}
}
</style>
分模块页面 聊天框自己头像
<template>
<div id="card">
<header>
<img class="avatar" :src="user.userface" :alt="user.name">
<p class="name">{{user.name}}</p>
</header>
<footer>
<input class="search" type="text" v-model="$store.state.filterKey" placeholder="search user...">
</footer>
</div>
</template>
<script>
export default {
name: 'card',
data () {
return {
user: JSON.parse(window.sessionStorage.getItem('user'))
}
}
}
</script>
<style lang="scss" scoped>
#card {
padding: 12px;
.avatar{
width: 40px;
height: 40px;
vertical-align: middle;/*这个是图片和文字居中对齐*/
}
.name {
display: inline-block;
padding: 10px;
margin-bottom: 15px;
font-size: 16px;
}
.search {
background: #26292E;
height: 30px;
line-height: 30px;
padding: 0 10px;
border: 1px solid #3a3a3a;
border-radius: 4px;
outline: none;/*鼠标点击后不会出现蓝色边框*/
color: #FFF;
}
}
</style>
聊天页面
<template>
<div id="message" v-scroll-bottom="sessions">
<ul v-if="checkUser" >
<li v-for="entry in sessions[user.username + '*' + checkUser.username]">
<p class="time">
<span>{{entry.date | time}}</span>
</p>
<div class="main" :class="{self:entry.self}">
<img class="avatar" :src="entry.self ? user.userface : checkUser.userface" alt="">
<p class="text">{{entry.content}}</p>
</div>
</li>
</ul>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'message',
data () {
return {
user:JSON.parse(window.sessionStorage.getItem('user'))
}
},
computed:mapState([
'sessions',
'checkUser'
]),
filters:{
time (date) {
if (date) {
date = new Date(date);
}
return `${date.getHours()}:${date.getMinutes()}`;
}
},
directives: {/*这个是vue的自定义指令,官方文档有详细说明*/
// 发送消息后滚动到底部,这里无法使用原作者的方法,也未找到合理的方法解决,暂用setTimeout的方法模拟
'scroll-bottom' (el) {
//console.log(el.scrollTop);
setTimeout(function () {
el.scrollTop+=9999;
},1)
}
}
}
</script>
<style lang="scss" scoped>
#message {
padding: 15px;
max-height: 68%;
overflow-y: scroll;
ul {
padding: 0px;
list-style-type: none;
li {
margin-bottom: 15px;
}
}
.time {
text-align: center;
margin: 7px 0;
> span {
display: inline-block;
padding: 0 18px;
font-size: 12px;
color: #FFF;
background-color: #dcdcdc;
border-radius: 2px;
}
}
.main {
.avatar {
float: left;
margin: 0 10px 0 0;
border-radius: 3px;
width: 30px;
height: 30px;
}
.text {
display: inline-block;
padding: 0 10px;
max-width: 80%;
background-color: #fafafa;
border-radius: 4px;
line-height: 30px;
}
}
.self {
text-align: right;
.avatar {
float: right;
margin: 0 0 0 10px;
border-radius: 3px;
width: 30px;
height: 30px;
}
.text {
display: inline-block;
padding: 0 10px;
max-width: 80%;
background-color: #b2e281;
border-radius: 4px;
line-height: 30px;
}
}
}
</style>
输入信息页面
<template>
<div id="uesrtext">
<textarea placeholder="按 Ctrl + Enter 发送" v-model="content" v-on:keyup="addMessage"></textarea>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'uesrtext',
data () {
return {
content:''
}
},
computed:mapState([
'checkUser'
]),
methods: {
addMessage (e) {
if (e.ctrlKey && e.keyCode ===13 && this.content.length) {
let msgObj = new Object();
msgObj.send = this.checkUser.username;
msgObj.content=this.content;
this.$store.state.stomp.send('/ws/chat',{},JSON.stringify(msgObj));
this.$store.commit('addMessage',msgObj);
this.content = '';
}
}
}
}
</script>
<style lang="scss" scoped>
#uesrtext {
position: absolute;
bottom: 0;
right: 0;
width: 100%;
height: 30%;
border-top: solid 1px #DDD;
> textarea {
padding: 10px;
width: 100%;
height: 100%;
border: none;
outline: none;
}
}
</style>
朋友列表栏
<template>
<div id="list">
<ul>
<li v-for="item in hrs" :class="{ active: checkUser?item.username === checkUser.username:false }" v-on:click="changeCurrentUser(item)"><!-- :class="[item.id === currentSessionId ? 'active':'']" -->
<img class="avatar" :src="item.userface" :alt="item.name">
<p class="name">{{item.name}}</p>
</li>
</ul>
</div>
</template>
<script>
import {mapState} from 'vuex'
export default {
name: 'list',
data () {
return {
}
},
computed: mapState([
'hrs',
'checkUser'
]),
methods:{
showHrs(){
console.log(this.$store.state.hrs);
},
changeCurrentUser:function (user) {
this.$store.commit('changeCurrentUser',user)
}
},mounted() {
this.showHrs()
}
}
</script>
<style lang="scss" scoped>
#list {
ul{
padding: 0;
}
li {
padding: 0px 15px;
border-bottom: 1px solid #292C33;
cursor: pointer;
&:hover {
background-color: rgba(255, 255, 255, 0.03);
}
}
li.active {/*注意这个是.不是冒号:*/
background-color: rgba(255, 255, 255, 0.1);
}
.avatar {
border-radius: 2px;
width: 30px;
height: 30px;
vertical-align: middle;
}
.name {
display: inline-block;
margin-left: 15px;
}
}
</style>
三、vuex 状态管理栏
import Vue from 'vue';
import Vuex from 'vuex';
import {getRequest} from "@/utils/config";
import SockJS from 'sockjs-client';
import Stomp from 'stompjs';
Vue.use(Vuex);
const store = new Vuex.Store({
state:{
routers:[],
sessions: {},
checkUser:null,
filterKey:'',
hrs:[],
stomp:null,
currentUser:null,
},
mutations:{
initRouters(state,data){
state.routers = data;
},
changeCurrentUser (state,user) {
state.checkUser = user;
},
init_currentUser(state,currentUser){
state.currentUser = currentUser;
},
addMessage (state,msg) {
let mess = state.sessions[state.currentUser.username+'*' + msg.send];
if (!mess){
Vue.set(state.sessions,state.currentUser.username+'*'+msg.send,[]);
}
state.sessions[state.currentUser.username+'*' + msg.send].push({
content:msg.content,
date: new Date(),
self:!msg.isSlef
})
},
INIT_DATA (state) {
let data = sessionStorage.getItem('vue-chat-session');
if (data) {
state.sessions = JSON.parse(data);
}
},
INIT_HR(state,data){
state.hrs = data;
}
},
actions: {
initData (context) {
context.commit('INIT_DATA');
getRequest('/chat/hr').then(resp=>{
if (resp){
context.commit('INIT_HR',resp)
}
});
context.state.stomp = Stomp.over(new SockJS('/ws/ep'));
context.state.stomp.connect({},success=>{
context.state.stomp.subscribe('/user/queue/chat',msg=>{
let message = JSON.parse(msg.body);
message.isSlef =true;
message.send = message.from;
context.commit('addMessage',message);
})
},error=>{
})
}
}
})
store.watch(function (state) {
return state.sessions
},function (val) {
sessionStorage.setItem('vue-chat-session', JSON.stringify(val));
},{
deep:true/*这个貌似是开启watch监测的判断,官方说明也比较模糊*/
})
export default store;
四、补充
vuex的mutations方法在页面刷新时,会重新加载vuex,所以此时获取到的数据会加载不到,如果用到获取属性时会报错,比如我在登录时获取用户数据,并commit到mutations,然后拿到用户名做操作,但是刷新时用户信息会没有,导致报错。解决方法如下
在APP.vue中
<script>
export default {
name: 'App',
created() {
//在页面加载时读取sessionStorage里的状态信息
if (sessionStorage.getItem('store')) {
this.$store.replaceState(Object.assign({}, this.$store.state, JSON.parse(sessionStorage.getItem('store'))));
}
//在页面刷新时将vuex里的信息保存到sessionStorage里
window.addEventListener('beforeunload', () => {
sessionStorage.setItem('store', JSON.stringify(this.$store.state));
});
}
}
</script>
<style>
五、效果展示
发消息
收消息
收发
以上是关于SpringBoot+Vue+Websocket 实现服务器端向客户端主动发送消息的主要内容,如果未能解决你的问题,请参考以下文章