整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Next.js+React聊天室|next.js桌面

Next.js+React聊天室|next.js桌面端聊天

近一直在捣鼓Next项目,Next.js是一个基于React.js的服务端ssr渲染框架,能够让你的react页面拥有SEO功能。

Next.js的star高达58.8K+。一款非常受开发者青睐的React SSR框架。

https://www.nextjs.cn/
https://github.com/vercel/next.js

项目简介

Next-WebChat 基于next.js+react+redux+antd+rlayer等技术开发的PC桌面端聊天实例。实现了发现消息/表情gif、图片/视频预览、弹窗菜单、红包/朋友圈等功能。

技术实现

  • 技术框架:Next.js+React.js+Redux
  • UI组件库:Antd(蚂蚁金服PC桌面端组件库)
  • 弹框组件:RLayer
  • 字体图标:阿里iconfont字体图标库
  • 动态图片:next-images(通过require动态引入本地图片)

效果预览

react自定义弹窗组件

项目中用到的所有弹窗功能均是自己开发的RLayer.js弹出框组件。

前段时间有分享过一篇关于react.js实现自定义弹窗组件,感兴趣的可以去看看。

React.js PC桌面端自定义弹出框组件

react自定义虚拟滚动条组件

项目中用到的滚动条均是基于react.js构建的轻量级美化滚动条组件RScroll。支持原生滚动条、是否自动隐藏、滚动条尺寸/层级/颜色等功能。

由于之前有过一篇分享文章,这里就不作过多的介绍了。

React.js轻量级虚拟滚动条组件

Next.js公共布局

next.js中自定义公共布局模板,在layouts目录下的index.js页面。

function Layout(props) {
    // console.log(props)
    const router=useRouter()

    // 登录验证拦截
    useEffect(()=> {
        // ...
    }, [])

    return (
    <>
        {/* 配置公共头部信息 */}
        <Head>
            <title>Next.js聊天室</title>
            <link rel="icon" href="/favicon.ico" />
            <meta name="keywords" content="Next.js|React.js|Next.js聊天室|Next.js仿微信|React聊天实例"></meta>
            <meta name="description" content="Next-WebChat 基于Next.js+React+Redux构建的服务端渲染聊天应用程序"></meta>
        </Head>

        <div className="next__container flexbox flex-alignc flex-justifyc">
            <div className={utils.classNames('next__wrapper', props.isWinMaximize&&'maximize')} style={{ backgroundImage: `url(${props.skin})` }}>
                <div className="next__board flexbox">
                    {/* 右上角按钮 */}
                    <WinBar {...props} />

                    {/* 侧边栏 */}
                    <Sidebar {...props} />

                    {/* 中间栏 */}
                    <Middle />

                    {/* 主体布局 */}
                    <div className="nt__mainbox flex1 flexbox flex-col">
                        {props.children}
                    </div>
                </div>
            </div>
        </div>
    </>
    )
}

Head组件里配置一些页面头部信息,如:标题、关键词、描述及图标等信息。

Next.js聊天模块

聊天编辑器区域单独抽离了一个组件,用来进行聊天输入处理。

// react实现contenteditable功能
return (
	<div 
		ref={editorRef}
		className="editor"
		contentEditable="true"
		dangerouslySetInnerHTML={{__html: state.editorText}}
		onClick={handleClicked}
		onInput={handleInput}
		onFocus={handleFocus}
		onBlur={handleBlur}
		style={{userSelect: 'text', WebkitUserSelect: 'text'}}>
	</div>
)

编辑器支持多行文本输入、光标处插入表情、粘贴截图发送、输入链接等功能。

如上图:视频播放是基于rlayer弹窗实现。

handlePlayVideo=(item, e)=> {
	rlayer({
		content: (
			<div className="flexbox flex-col" style={{height: '100%'}}>
				<div className="ntDrag__head" style={{color:'#696969',padding:'0 60px 0 15px',lineHeight:'42px'}}><i className="iconfont icon-bofang"></i> 视频预览</div>
				<div className="ntMain__cont flex1 flexbox flex-col">
					{/* 视频video */}
					<video className="vplayer" src={item.videosrc} poster={item.imgsrc} autoPlay preload="auto" controls
						x5-video-player-fullscreen="true"
						webkit-playsinline="true"
						x-webkit-airplay="true"
						playsInline
						x5-playsinline="true"
						style={{height: '100%', width: '100%', objectFit: 'contain', outline: 'none'}}
					/>
				</div>
			</div>
		),
		layerStyle: {background: '#f6f5ef'},
		opacity: .2,
		area: ['550px', '450px'],
		drag: '.ntDrag__head',
		resize: true,
		maximize: true,
	})
}

聊天区域还支持拖拽发送图片功能。

handleDragEnter=(e)=> {
	e.stopPropagation()
	e.preventDefault()
}
handleDragOver=(e)=> {
	e.stopPropagation()
	e.preventDefault()
}
handleDrop=(e)=> {
	e.stopPropagation()
	e.preventDefault()
	console.log(e.dataTransfer)

	this.handleFileList(e.dataTransfer)
}
// 获取拖拽文件列表
handleFileList=(filelist)=> {
	let files=filelist.files
	if(files.length >=2) {
		rlayer.message({icon: 'error', content: '暂时支持拖拽一张图片', shade: true, layerStyle: {background:'#ffefe6',color:'#ff3838'}})
		return false
	}
	for(let i=0; i < files.length; i++) {
		if(files[i].type !='') {
			this.handleFileAdd(files[i])
		}else {
			rlayer.message({icon: 'error', content: '目前不支持文件夹拖拽功能', shade: true, layerStyle: {background:'#ffefe6',color:'#ff3838'}})
		}
	}
}
handleFileAdd=(file)=> {
	if(file.type.indexOf('image')==-1) {
		rlayer.message({icon: 'error', content: '目前不支持非图片拖拽功能', shade: true, layerStyle: {background:'#ffefe6',color:'#ff3838'}})
	}else {
		let reader=new FileReader()
		reader.readAsDataURL(file)
		reader.onload=function() {
			let img=this.result

			console.log(img)
		}
	}
}

大家如果感兴趣的话,可以试一试。

好了,今天就分享到这里。希望对大家有所帮助哈!

基于Nuxt.js+Vue仿微信App聊天实例

文地址:https://dwz.cn/Z1O56dnu

作者:Rude3Knife

前言

本文中搭建了一个简易的多人聊天室,使用了WebSocket的基础特性。

源代码来自老外的一篇好文:

https://www.callicoder.com/spring-boot-websocket-chat-example/

本文内容摘要:

  • 初步理解WebSocket的前后端交互逻辑
  • 手把手使用 SpringBoot + WebSocket 搭建一个多人聊天室Demo
  • 代码源码及其解释
  • 前端展示页面

此外,在下一篇文章中,我们将做到:

  • 对该WebSocket聊天室进行分布式改造,同时部署多台机器来作为集群,支撑高并发。
  • 保存用户session,并且在集群上实现session同步,比如实时展示当前在线的用户!

正文

WebSocket多人在线聊天室

本文工程源代码:

https://github.com/qqxx6661/springboot-websocket-demo


新建工程

我们新建一个SpringBoot2的项目工程,在默认依赖中,添加websocket依赖:

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>


WebSocket 配置

我们先来设置websocket的配置,新建config文件夹,在里面新建类WebSocketConfig

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
 @Override
 public void registerStompEndpoints(StompEndpointRegistry registry) {
 registry.addEndpoint("/ws").withSockJS();
 }
 @Override
 public void configureMessageBroker(MessageBrokerRegistry registry) {
 registry.setApplicationDestinationPrefixes("/app");
 registry.enableSimpleBroker("/topic");
 }
}


代码解释:

@EnableWebSocketMessageBroker用于启用我们的WebSocket服务器。

我们实现了WebSocketMessageBrokerConfigurer接口,并实现了其中的方法。

在第一种方法中,我们注册一个websocket端点,客户端将使用它连接到我们的websocket服务器。

withSockJS()是用来为不支持websocket的浏览器启用后备选项,使用了SockJS。

方法名中的STOMP是来自Spring框架STOMP实现。STOMP代表简单文本导向的消息传递协议。它是一种消息传递协议,用于定义数据交换的格式和规则。为啥我们需要这个东西?因为WebSocket只是一种通信协议。它没有定义诸如以下内容:如何仅向订阅特定主题的用户发送消息,或者如何向特定用户发送消息。我们需要STOMP来实现这些功能。

在configureMessageBroker方法中,我们配置一个消息代理,用于将消息从一个客户端路由到另一个客户端。

第一行定义了以“/app”开头的消息应该路由到消息处理方法(之后会定义这个方法)。

第二行定义了以“/topic”开头的消息应该路由到消息代理。消息代理向订阅特定主题的所有连接客户端广播消息。

在上面的示例中,我们使用的是内存中的消息代理。

之后也可以使用RabbitMQ或ActiveMQ等其他消息代理。


创建 ChatMessage 实体

ChatMessage用来在客户端和服务端中交互

我们新建model文件夹,创建实体类ChatMessage。

实体中,有三个字段:

  • type:消息类型
  • content:消息内容
  • sender:发送者

类型有三种:

  • CHAT: 消息
  • JOIN:加入
  • LEAVE:离开


创建Controller来接收和发送消息

创建controller文件夹,在controller文件夹添加类ChatController

代码解释:

我们在websocket配置中,从目的地以/app开头的客户端发送的所有消息都将路由到这些使用@MessageMapping注释的消息处理方法。

例如,具有目标/app/chat.sendMessage的消息将路由到sendMessage()方法,并且具有目标/app/chat.addUser的消息将路由到addUser()方法


添加WebSocket事件监听

完成了上述代码后,我们还需要对socket的连接和断连事件进行监听,这样我们才能广播用户进来和出去等操作。

创建listener文件夹,新建WebSocketEventListener类

代码解释:

我们已经在ChatController中定义的addUser()方法中广播了用户加入事件。因此,我们不需要在SessionConnected事件中执行任何操作。

在SessionDisconnect事件中,编写代码用来从websocket会话中提取用户名,并向所有连接的客户端广播用户离开事件。


创建前端聊天室页面

我们在src/main/resources文件下创建前端文件,结构类似这样:

static
 └── css
 └── main.css
 └── js
 └── main.js
 └── index.html 


1. HTML文件 index.html

HTML文件包含用于显示聊天消息的用户界面。它包括sockjs和stomp 两个js库。

SockJS是一个WebSocket客户端,它尝试使用本机WebSockets,并为不支持WebSocket的旧浏览器提供支持。STOMP JS是javascript的stomp客户端。

笔者在文件里使用了国内的CDN源

2. JavaScript main.js

添加连接到websocket端点以及发送和接收消息所需的javascript。

代码解释:

connect()函数使用SockJS和stomp客户端连接到我们在Spring Boot中配置的/ws端点。

成功连接后,客户端订阅/topic/public,并通过向/app/chat.addUser目的地发送消息将该用户的名称告知服务器。

stompClient.subscribe()函数采用一种回调方法,只要消息到达订阅主题,就会调用该方法。

其它的代码用于在屏幕上显示和格式化消息。

3. CSS main.css

整个项目结构如下:

启动

启动SpringBoot项目

效果入下:

补充:使用RabbitMQ代替内存作为消息代理

添加依赖:

然后将WebSocketConfig类中configureMessageBroker方法改为使用RabbitMq,完成!

如此一来,便可以通过RabbitMq进行消息的订阅。

前两周经常有大学生小伙伴私信给我,问我可否有偿提供毕设帮助,我说暂时没有这个打算,因为工作实在太忙,现阶段无法投入到这样的领域内,其中有两个小伙伴又问到我websocket该怎么使用,想给自己的项目中加入这样的技术。

刚好我所在的公司有做问诊服务,里面就使用了websocket实现聊天通讯,就在闲暇之余专门把部分代码摘取出来,做了一个简单的demo分享给他们了,之后想想这块可以再丰富一下,就花时间又做了一个更完整的小项目出来,且加了详细的注释说明,分享给对websocket感兴趣的小伙伴们。

案例展示


技术栈

考虑到不同群体对vue等前端技术的接受程度,本案例采用了HTML+CSS+JQuery来实现,代码直接复制到vue项目中也是一样的,只是赋值和取值的方式改变而已,很多Java程序员其实对于一门简单案例的学习不喜欢牵扯太多前端技术,而是单纯学习想知道的这门技术就好,太多其他的引入反而影响跟踪调试,而原始的HTML+JS方式更有利于我们学习和理解,只需要右键HTML页面在浏览器打开进行F12调试即可。

技术

版本

Java

1.8

SpringBoot

2.3.12.RELEASE

WebSocket

2.3.12.RELEASE

Hutools

5.8.0.M1

SockJS

1.6.0

StompJS

1.7.1


实现过程

1、引入依赖

<!-- websocket -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!-- Hutools工具类 -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.0.M1</version>
</dependency>
复制代码


2、订阅常量类

后面的websocket配置类会用到这几个常量

stomp端点地址: 连接websocket时的后缀地址,比如127.0.0.1:8888/websocket。

websocket前缀:前端调服务端消息接口时的URL都加上了这个前缀,比如默认是/send,变成/app/send。

点对点代理地址:如果websocket配置类中设置了代理路径,一般点对点订阅路径喜欢用/queue。

广播代理地址:如果websocket配置类中设置了代理路径,一般广播订阅路径喜欢用这个/topic。

package com.simple.ws.constants;

/**
 * <p>
 * websocket常量
 * </p>
 *
 * @author 福隆苑居士,公众号:【Java分享客栈】
 * @since 2022-04-02 10:11
 */
public class WsConstants {

   // stomp端点地址
   public static final String WEBSOCKET_PATH="/websocket";

   // websocket前缀
   public static final String WS_PERFIX="/app";

   // 消息订阅地址常量
   public static final class BROKER {
      // 点对点消息代理地址
      public static final String BROKER_QUEUE="/queue/";
      // 广播消息代理地址
      public static final String BROKER_TOPIC="/topic";
   }
}
复制代码


3、WebSocket配置类

核心内容讲解:

1)、@EnableWebSocketMessageBroker:用于开启stomp协议,这样就能支持@MessageMapping注解,类似于@requestMapping一样,同时前端可以使用Stomp客户端进行通讯;

2)、registerStompEndpoints实现:主要用来注册端点地址、开启跨域授权、增加拦截器、声明SockJS,这也是前端选择SockJS的原因,因为spring项目本身就支持;

3)、configureMessageBroker实现:主要用来设置客户端订阅消息的路径(可以多个)、点对点订阅路径前缀的设置、访问服务端@MessageMapping接口的前缀路径、心跳设置等;

package com.simple.ws.config;

import com.simple.ws.constants.WsConstants;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;

/**
 * <p>
 * websocket核心配置类
 * </p>
 *
 * @author 福隆苑居士,公众号:【Java分享客栈】
 * @since 2022/4/1 22:57
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {

   /**
    * 注册stomp端点
    *
    * @param registry stomp端点注册对象
    */
   @Override
   public void registerStompEndpoints(StompEndpointRegistry registry) {
      registry.addEndpoint(WsConstants.WEBSOCKET_PATH)
            .setAllowedOrigins("*")
            .withSockJS();
   }

   /**
    * 配置消息代理
    *
    * @param registry 消息代理注册对象
    */
   @Override
   public void configureMessageBroker(MessageBrokerRegistry registry) {

      // 配置服务端推送消息给客户端的代理路径
      registry.enableSimpleBroker(WsConstants.BROKER.BROKER_QUEUE, WsConstants.BROKER.BROKER_TOPIC);
      
      // 定义点对点推送时的前缀为/queue
      registry.setUserDestinationPrefix(WsConstants.BROKER.BROKER_QUEUE);
      
      // 定义客户端访问服务端消息接口时的前缀
      registry.setApplicationDestinationPrefixes(WsConstants.WS_PERFIX);
   }
}
复制代码

特别说明:如果对于配置类中这几个路径的设置看不明白,没关系,后面的前端部分你一看就懂了。


4、消息接口

说明:

1)、消息接口使用@MessageMapping注解,前面讲的配置类@EnableWebSocketMessageBroker注解开启后才能使用这个;

2)、这里稍微提一下,真正线上项目都是把websocket服务做成单独的网关形式,提供rest接口给其他服务调用,达到共用的目的,本项目因为不涉及任何数据库交互,所以直接用@MessageMapping注解,后续完整IM项目接入具体业务后会做一个独立的websocket服务,敬请关注哦!

package com.simple.ws.controller;

import com.simple.ws.constants.WsConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * <p>
 * 消息接口
 * </p>
 *
 * @author 福隆苑居士,公众号:【Java分享客栈】
 * @since 2022-04-02 12:00
 */
@RestController
@RequestMapping("/api")
@Slf4j
public class MsgController {

   private final SimpMessagingTemplate messagingTemplate;

   public MsgController(SimpMessagingTemplate messagingTemplate) {
      this.messagingTemplate=messagingTemplate;
   }

   /**
    * 发送广播消息
    * -- 说明:
    *       1)、@MessageMapping注解对应客户端的stomp.send('url');
    *       2)、用法一:要么配合@SendTo("转发的订阅路径"),去掉messagingTemplate,同时return msg来使用,return msg会去找@SendTo注解的路径;
    *       3)、用法二:要么设置成void,使用messagingTemplate来控制转发的订阅路径,且不能return msg,个人推荐这种。
    *
    * @param msg 消息
    */
   @MessageMapping("/send")
   public void sendAll(@RequestParam String msg) {

      log.info("[发送消息]>>>> msg: {}", msg);

      // 发送消息给客户端
      messagingTemplate.convertAndSend(WsConstants.BROKER.BROKER_TOPIC, msg);
   }
   
复制代码


5、前端项目结构

很简单,就是HTML+CSS和几个js文件,sockjs和stompjs就是和服务端通信的实现,可以从GitHub官网下载,而websocket.js是我们自己封装的和服务端通信的内容。

6、Stomp客户端使用

页面结构样式这里就省略不讲了,直接开始正文。

stompjs,是对websocket原生使用的一层封装,提供了更简单的调用方法。

这里先看看我们自己封装的websocket.js的实现:

1)、声明URL

也就是服务端配置类的端点地址

var url="http://127.0.0.1:8888/websocket"; // 改成自己的服务端地址
复制代码


2)、建立websocket连接

stompClient=Stomp.over(socket)就是覆盖了sockjs使用自己的客户端来操作ws;

进入stompClient.connect()中就代表连接成功,可以进行自己的业务处理比如广播通知某人上线等等,重要的是连接成功后要声明订阅列表,这样服务端转发的消息才会根据这些订阅地址发送过来,否则收不到;

最后就是有一个回调可以捕获异常情况,在里面可以做一些操作比如重连等等。

/**
 * 连接
 */
function connect() {
    userId=GetUrlParam("userId");
    var socket=new SockJS(url, null, { timeout: 15000});
    stompClient=Stomp.over(socket); // 覆盖sockjs使用stomp客户端
    stompClient.connect({}, function (frame) {

        console.log('frame: ' + frame)

        // 连接成功后广播通知
        sendNoticeMsg(userId, "in");

        /**
         * 订阅列表,订阅路径和服务端发消息路径一致就能收到消息。
         * -- /topic: 服务端配置的广播订阅路径
         * -- /queue/: 服务端配置的点对点订阅路径
         */
        stompClient.subscribe("/topic", function (response) {
                showMsg(response.body);
        });

        stompClient.subscribe("/queue/" + userId + "/topic", function (response) {
                showMsg(response.body);
        });

        // 异常时进行重连
        }, function (error) {
            console.log('connect error: ' + error)
            if (reConnectCount > 10) {
                    console.log("温馨提示:您的连接已断开,请退出后重新进入。")
                    reConnectCount=0;
            } else {
                    wsReconnect && clearTimeout(wsReconnect);
                    wsReconnect=setTimeout(function () {
                            console.log("开始重连...");
                            connect();
                            console.log("重连完毕...");
                            reConnectCount++;
                    }, 1000);
            }
        }
    )
}
复制代码


3)、断开websocket连接

断开很简单,但要注意一点,不要根据关闭窗口或浏览器的事件来控制断开,这是一个误区,首先浏览器兼容性差异较大,传统的js在监听窗口关闭事件的兼容性上是很差的,这个可以自己试验就知道了,有些浏览器可以有些不可以;

其次,可以参考QQ,你自己在退出一个群聊的时候实际上你就单纯是关闭了,并没有离线,而是你退出QQ时才真正离线,所以真正控制这个断开方法的位置应该是点击退出按钮时,这一点不要理解错了。

/**
 * 断开
 */
function disconnect() {
    if (stompClient !=null) {
        // 断开连接时进行广播通知
        sendNoticeMsg(userId, "out");
        // 断开连接
        stompClient.disconnect(function(){
                // 有效断开的回调
                console.log(userId + "断开连接....")
        });
    }
}
复制代码


4)、消息滚动到底部

这个没什么说的,在进入页面以及发送消息后渲染页面时使用即可。

// 消息窗口滚动到底部
function scrollBotton(){
    var div=document.getElementById("content");
    div.scrollTop=div.scrollHeight;
}
复制代码


5)、聊天消息渲染到页面

这里就是单纯的JQuery操作了,注意的一点是这里加了个type判断是系统消息还是聊天消息,在本案例中,系统消息就是某人上下线的提示,聊天消息就是发送出来的内容。

在vue这样的框架中,这部分的操作其实会很简单。

/**
 * 聊天消息渲染到页面中
 */
function showMsg(obj) {
    obj=JSON.parse(obj);
    var userId=obj.userId;
    var sendTime=obj.sendTime;
    var info=obj.info;
    var type=obj.type;

    if (1===type) {
        // 聊天消息
        console.log("聊天消息...")
        var msgHtml="<div class=\"msg\" id=\"msg\">" + 
                          "  <div class=\"first-line\">" + 
                          "	   <div class=\"userName\" id=\"userName\">" + userId + "</div>" +  
                          "	   <div class=\"sendTime\" id=\"sendTime\">" + sendTime + "</div>" + 
                          "  </div>" + 
                          "  <div class=\"second-line\">" + 
                          "    <div class=\"sendMsg\" id=\"sendMsg\">" + info + "</div>" + 
                          "  </div>" + 
                          "</div>";

        // 渲染到页面	
        $("#content").html($("#content").html() + "\n" + msgHtml);

} else if (2===type) {
        // 系统消息
        console.log("系统消息...")
        var msgHtml="<div class=\"notice\">" + 
                                "<div class=\"notice-info\">" + info + "</div>" + 
                          "</div>";

        // 渲染到页面	
        $("#content").html($("#content").html() + "\n" + msgHtml);
    }

    // 消息窗口滚动到底部
    scrollBotton();
}
复制代码


6)、发送群聊消息

这里传递的obj定义了一个消息体,就是一个对象,真正项目中也是这般使用,而不是单纯传递一个文本;

stompClient.send中的url,其中/app是服务端配置类中设置的ApplicationDestinationPrefixes,而/send就是controller接口中@MessageMapping("/send")的路径,两个加在一起就是这里前端发送的路径,少一个或多一个斜杠都会导致服务端收不到消息。

/**
* 发送群聊消息
* -- 这里我们传递消息体对象 
* 	{
*         "userId": userId, // 发送者
*         "sendTime": sendTime, // 发送时间
*         "info": info, // 发送内容
*         "type": 1  // 消息类型,1-聊天消息,2-系统消息
*       }
*/
function sendAll(obj) {
    stompClient.send("/app/send", {}, JSON.stringify(obj));
}
复制代码


7)、发送系统消息

就是传递type=2即可,info做了下判断返回不同的消息内容。

/**
 * 发送系统通知消息
 * @param userId 用户id 
 */
function sendNoticeMsg(userId, action) {
    var obj={
        "userId": userId,
        "sendTime": new Date().Format("HH:mm:ss"),
        "info": "in"===action ? userId + "进入房间" : userId + "离开房间",
        "type": 2
    }
    sendAll(obj);
}
复制代码


7、聊天页发消息

index.html就是聊天主页面,直接调用我们前面封装好的websocket.js方法即可。

主要步骤为:进入页面时建立websocket连接 --> 获取登录用户信息 --> 监听按钮点击事件和键盘事件 --> 发送websocket消息 --> 清空文本框内容

这样,一旦发送消息成功,服务端就可以看到接收到的消息体并根据发送路径进行转发,前端websocket.js中订阅列表中的路径一旦和服务端转发的路径匹配上,就会收到消息,我们把消息渲染到页面上即可。

这个过程其实也就是websocket全双工通信的原理

<script>

    $(function() {
        // 启动websocket
        connect();

        // 获取用户信息
        getUser();

        // 消息窗口滚动到底部
        scrollBotton();

        // 监听键盘Enter键,要用keyup,否则无法清除换行符。
        $("#send-info").keyup(function(e) {
            var eCode=e.keyCode ? e.keyCode : e.which ? e.which : e.charCode;
            if (eCode==13){
                $("#send-btn").click();
            }
        });

        // 监听发送按钮点击事件
        $("#send-btn").click(function() {
            send();
        });

        // 监听退出按钮点击事件
        $("#exit-btn").click(function() {
            layer.confirm('你确定要退出吗?', {
                time: 0, // 不自动关闭
                btn: ['确定退出', '再玩玩'],
                yes: function(index){
                    layer.close(index);
                    disconnect();
                    window.location.href='login.html';
                }
            });
        });


    });

    // 获取用户信息
    function getUser() {
        var userId=GetUrlParam("userId");
        $("#userId").text(userId);
    }

    // 发送广播消息,这里定义一个type:1-聊天消息,2-系统消息。
    function send() {
        var userId=$("#userId").text().trim();
        var sendTime=new Date().Format("HH:mm:ss");
        var info=$("#send-info").val().replace("\n", "");
        var msg={
            "userId": userId,
            "sendTime": sendTime,
            "info": info,
            "type": 1
        }
        // 发送消息
        sendAll(msg);

        // 清空文本域内容
        $("#send-info").val("");
    }

</script>
复制代码


避坑指南

1)、版本问题,经本人专门花时间测试,SpringBoot2.4.0以下版本才能整合SockJS和StompJS成功,以上的版本都不行,会报 Main site uses: "1.6.0", the iframe: "1.0.0" 这样的错误,将StompJS换成低版本也不行,所以这里整合时用了SpringBoot2.3.12.RELEASE版本,但这个没关系,websocket服务一般都是单独做成一个服务的,如果是微服务,你的其他业务服务使用高版本的SpringBoot就行了;

2)、监听窗口关闭事件不可取,这个在前面已经讲过了,浏览器兼容性差,我试过好几个浏览器监听效果都各不相同甚至完全无效,其次本身这样操作也不合理,我们只要保证退出时触发断开事件即可,无需在这样的事情上浪费时间,可以参考QQ;

3)、服务端编写消息接口时推荐使用SimpMessagingTemplate来控制发送,而不是@SendTo注解,因为前者更符合程序员开发思路,后期独立websocket服务暴露rest接口时也更简单;

4)、配置类中其实还有很多其他配置项,比如心跳配置、拦截器配置等,本案例没有加入进来,因为我自己公司的项目中其实使用过心跳,但后来又去掉了,因为对这块了解不深入的话贸然使用容易出现稀奇古怪的问题。

讲个趣事,我们前端工程师当初就因为心跳这块调试了挺久,上线后依然会出现时好时坏的情况,因为他之前也没做过websocket都是现学的,而且线上环境和测试环境差异难明,包含程序缺陷、网络环境因素等等,后来我们决定去掉心跳检测,之后两年也没出任何问题。

所以有时候保证项目稳定性反而更有用,但处于学习的角度而言,心跳检测是一定需要的,否则所有的socket框架也不会专门提供这样的方案了。


总结

SpringBoot+websocket的实现其实不难,你可以使用原生的实现,也就是websocket本身的OnOpen、OnClosed等等这样的注解来实现,以及对WebSocketHandler的实现,类似于netty的那种使用方式,而且原生的还提供了对websocket的监听,服务端能更好的控制及统计。

但根据我个人的经验而言,真实项目中还是使用Stomp实现的居多,因为独立服务更方便,便于后期搭建集群环境做横向扩展,且内置的方法也很简单,既然如此,我们还是以主流实现方式为准来学习吧。


原文链接:https://juejin.cn/post/7083020131412115464