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 实现服务器端向客户端主动发送消息的主要内容,如果未能解决你的问题,请参考以下文章

springboot+vue实现websocket

Springboot+vue3集成使用WebSocket

Springboot+vue3集成使用WebSocket

Springboot+vue3集成使用WebSocket

SpringBoot实现WebSocket发送接收消息 + Vue实现SocketJs接收发送消息

vue+websocket+Springboot实现的即时通信开源项目