Springboot2.x+Websocket+js实现实时在线文本协同编辑,并展示协同用户

news2024/12/27 10:51:41

文章目录

  • 诉求
  • 相关技术
  • 思路展开
  • 相关步骤
    • pom配置
    • 服务端相关配置
    • 文本信息、用户广播处理逻辑
    • 前端功能代码
    • 功能测试
  • 小结

诉求

       实现页面实时在线文本协同编辑,且显示当前同时编辑文本的用户。

相关技术

       Springboot(2.7.0)+Websocket+javascript

思路展开

  1. 页面展示当前登陆用户
  2. 页面有文本输入框(包含编辑、保存按钮)
  3. 页面展示编辑当前文本的用户
  4. 服务端广播处理文本信息的以及协同用户

相关步骤

pom配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>8</java.version>
        <java.encoding>UTF-8</java.encoding>
        <slf4j.version>1.7.30</slf4j.version>
    </properties>
    <dependencies>

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

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>

        <!-- 引入日志管理相关依赖-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-to-slf4j</artifactId>
            <version>2.14.0</version>
        </dependency>


        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.69</version>
        </dependency>

    </dependencies>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.1</version>
                    <configuration>
                        <target>${java.version}</target>
                        <source>${java.version}</source>
                        <encoding>${java.encoding}</encoding>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>2.6</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-release-plugin</artifactId>
                    <configuration>
                        <arguments>-Prelease</arguments>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-source-plugin</artifactId>
                    <version>2.1</version>
                    <configuration>
                        <attach>true</attach>
                    </configuration>
                    <executions>
                        <execution>
                            <phase>compile</phase>
                            <goals>
                                <goal>jar</goal>
                            </goals>
                        </execution>
                    </executions>

                </plugin>
            </plugins>
        </pluginManagement>
    </build>

</project>

服务端相关配置

       编写WebSocketConfig和WebSocketHandler配置类,实现对WebSocket的配置。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.*;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @author 
 * @date 2023年01月31日 14:21
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

//    @Override
//    public void configureMessageBroker(MessageBrokerRegistry registry) {
//        registry.enableSimpleBroker("/topic");
//        registry.setApplicationDestinationPrefixes("/app");
//    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/doc-collaboration").withSockJS();
    }

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * handler 
 * @date2023年01月31日 14:22
 */
@Component
public class WebSocketHandler extends TextWebSocketHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketHandler.class);

    private static final List<WebSocketSession> sessions = new ArrayList<>();

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) {
        LOGGER.info("Received message: {}", message.getPayload());
        for (WebSocketSession webSocketSession : sessions) {
            try {
                webSocketSession.sendMessage(message);
            } catch (IOException e) {
                LOGGER.error("Error: {}", e.getMessage());
            }
        }
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        sessions.add(session);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        sessions.remove(session);
    }
}

文本信息、用户广播处理逻辑

       定义 WebSocket 端点以处理来自客户端的传入消息。

/**
 * @author 
 * @date 2023年01月31日 11:19
 */

import com.alibaba.fastjson.JSON;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.*;

@ServerEndpoint("/doc-collaboration")
@Component
@Slf4j
public class DocWebSocketServer {

    private static Set<Session> sessions = new HashSet<>();
    private static Set<String> editingUsers = new HashSet<>();
    private static String content = "";

    @OnOpen
    public void onOpen(Session session) {
        sessions.add(session);
    }

    @OnClose
    public void onClose(Session session) {
        sessions.remove(session);
        String username = (String) session.getUserProperties().get("username");
        if (username != null) {
            editingUsers.remove(username);
            broadcastEditingUsers();
        }
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        Gson gson = new Gson();
        Map<String, Object> data = gson.fromJson(message, Map.class);
        String type = (String) data.get("type");
        log.info("Message type: {}, message data: {}", type, data);
        String jsonStr = "";

        switch (type) {
            case "connect":
                String username = (String) data.get("username");
                session.getUserProperties().put("username", username);
                jsonStr = JSON.toJSONString(new HashMap<String, Object>() {{
                    put("type", "update");
                    put("content", content);
                }});
                broadcast(jsonStr);
                break;
            case "update":
                content = (String) data.get("content");
                jsonStr = JSON.toJSONString(new HashMap<String, Object>() {{
                    put("type", "update");
                    put("content", content);
                }});
                broadcast(jsonStr);
                break;
            case "start-editing":
                username = (String) session.getUserProperties().get("username");
                editingUsers.add(username);
                broadcastEditingUsers();
                break;
            case "stop-editing":
                username = (String) session.getUserProperties().get("username");
                editingUsers.remove(username);
                broadcastEditingUsers();
                break;
            case "getUser":
                broadcastEditingUsers();
                break;
        }
    }

    /**
     * 广播当前文本信息
     * @param message
     */
    private void broadcast(String message) {
        log.info("message   {}", message);
        for (Session session : sessions) {
            try {
                session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 广播当前正在编辑文本的用户
     */
    private void broadcastEditingUsers() {
        broadcast(
                JSON.toJSONString(
                        new HashMap<String, Object>() {{
                            put("type", "editing");
                            put("editingUsers", new ArrayList<>(editingUsers));
                        }}));
    }
}


前端功能代码

       创建一个 JavaScript 客户端,它与端点建立 WebSocket 连接并将更新发送到服务器。展示当前用户以及同时编辑文本的人员名称。

好久没写前端了,写起来有点费劲!😂

<head xmlns="http://www.w3.org/1999/html" xmlns="http://www.w3.org/1999/html">
    <style>
        .editing-users {
            background-color: lightgray;
            padding: 10px;
        }
    </style>
    <meta charset="UTF-8">
</head>
<body>
<div>
    <textarea id="content" readonly></textarea>
    </br>
    <button id="edit-button">编辑</button>
    <button id="save-button">保存</button>
</div>
</br>
<div>当前用户:</div><div id="user-name-label"> </div>
</br>
<div className="editing-users">
    <p id="editing-users-label">同时编辑的用户:</p>
    <ul id="editing-users-list"></ul>
</div>
<script>
    const socket = new WebSocket("ws://localhost:8080/doc-collaboration");
    const content = document.getElementById("content");
    const editButton = document.getElementById("edit-button");
    const saveButton = document.getElementById("save-button");
    const editingUsersLabel = document.getElementById("editing-users-label");
    const editingUsersList = document.getElementById("editing-users-list");
    const currentEditUserName = document.getElementById("user-name-label");
    socket.onopen = function () {
        const username = prompt("Enter your username");
        //用户创建登陆了
        socket.send(
            JSON.stringify({
                type: "connect",
                username: username
            })
        );
        //显示当前用户
        currentEditUserName.innerHTML=username;
        //获取当前文本同时编辑的用户
        socket.send(
            JSON.stringify({
                type: "getUser",
            })
        );
    };

    socket.onmessage = function (event) {
        const data = JSON.parse(event.data);

        if (data === null || typeof data.type === "undefined") {
            console.log("data:"+ data)
            return;
        }
        switch (data.type) {
            case "update":
                content.value = data.content;
                break;
            case "editing":
                editingUsersList.innerHTML = "";
                data.editingUsers.forEach(function (username) {
                    const li = document.createElement("li");
                    li.textContent = username;
                    editingUsersList.appendChild(li);
                });
                //可以选择没人编辑的时候隐藏当前列表
                // if (data.editingUsers.length === 0) {
                //     editingUsersLabel.style.display = "none";
                // } else {
                //     editingUsersLabel.style.display = "block";
                // }
                break;
        }
    };


    editButton.addEventListener("click", function () {
        content.removeAttribute("readonly");
        socket.send(
            JSON.stringify({
                type: "start-editing"
            })
        );
    });

    saveButton.addEventListener("click", function () {
    	//点击保存后输入框变为只读
        content.setAttribute("readonly", "true");
        socket.send(
            JSON.stringify({
                type: "stop-editing" 
            })
        );
    });
    content.addEventListener("input", function () {
        console.log("变动信息:" + content.value);
        socket.send(
            JSON.stringify({
                type: "update",
                content: content.value
            })
        );
    });
</script>
</body>
</html>

功能测试

       同时打开多个页面,当编辑信息时会显示到同时编辑的用户列表。

在这里插入图片描述
在这里插入图片描述

       当前用户点击保存时推出当前同时编辑的用户列表

在这里插入图片描述

小结

       上面实现为简易实现,仅供参考,可能并不适用一些业务场景。

       下面的是我的一些想法,在真实生产应用在线文档协同编辑有多个点

  • 实时协作编辑:多人同时在线编辑同一文档,显示协同编辑的人员,将信息更新为最新
  • 历史版本控制:记录并保存文档的历史版本,当出现不可修复的错误可会退或者前进版本,以及用户的一些修改轨迹
  • 讨论评论:在文档中添加评论和讨论功能,在一些文字或者图片附近可加以评论
  • 权限管理:控制团队成员对文档的访问和编辑权限.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/189053.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

NANK南卡护眼台灯Pro新品测评:旗舰级护眼,降低80%近视风险!

对于重度熬夜患者来说&#xff0c;对于家居用品是非常偏爱的&#xff0c;而最主要的就是光源问题&#xff0c;护眼台灯的发展迅速&#xff0c;不再局限于简单照明&#xff0c;在写作和办公、绘画的场景都适用&#xff0c;经常在电脑桌上的我&#xff0c;就看到了这款旗舰级专业…

9个 Intellij IDEA中使用Debug调试详解

有人说&#xff1a;一个人从1岁活到80岁很平凡&#xff0c;但如果从80岁倒着活&#xff0c;那么一半以上的人都可能不凡。 生活没有捷径&#xff0c;我们踩过的坑都成为了生活的经验&#xff0c;这些经验越早知道&#xff0c;你要走的弯路就会越少。 Debug用来追踪代码的运行流…

java数据输入

数据输入1.1数据输入概述1.2 Scanner使用的基本步骤导包创建对象接收数据

Windows内核--任务、进程和线程(2.4)

图片来自: Windows内核原理与实现 Windows内核有"任务"概念吗? 从技术术语来说&#xff0c;Windows内核并没有"任务"。"任务"一般被认为是抽象的需要执行的事情。在不同操作系统上&#xff0c;"任务"所代表的官方名称有所差异。 Linu…

树形表查询

树形表: 情况一:固定层级的树形表: 可以使用表的自连接查询,比如查询两级分类课程 selectone.id one_id,one.name one_name,one.parentid one_parentid,one.orderby one_orderby,one.label one_label,two.id two_id,two.name two_name,two.parentid two_parentid,two.orderby …

系统公告 | MemFire Cloud应用管理全新升级,实时数据库等你来体验~

“超能力”数据库&#xff5e;拿来即用&#xff0c;应用开发人员再也不用为撰写API而发愁。MemFire Cloud 为开发者提供了简单易用的云数据库&#xff08;表编辑器、自动生成API、SQL编辑器、备份恢复、托管运维&#xff09;&#xff0c;很大地降低开发者的使用门槛。 MemFire …

计算机视觉OpenCv学习系列:第九部分、视频读写

第九部分、视频读写第一节、视频读写处理1.视频标准与格式2.视频读写与函数3.代码练习与测试学习参考第一节、视频读写处理 1.视频标准与格式 视频标准与格式&#xff1a; SD(Standard Definition)标清480PHD(High Definition)高清720P/1080PUHD(Ultra High Definition)超高清…

100 个手把手、掏心窝子IDEA快捷键从入门到精通教程

有人说&#xff1a;一个人从1岁活到80岁很平凡&#xff0c;但如果从80岁倒着活&#xff0c;那么一半以上的人都可能不凡。 生活没有捷径&#xff0c;我们踩过的坑都成为了生活的经验&#xff0c;这些经验越早知道&#xff0c;你要走的弯路就会越少。 一、构建/编译 Ctrl F9&…

c++函数返回数组类型

c函数返回数组类型 首先&#xff0c;C 不允许返回一个完整的数组作为函数的参数。但是&#xff0c;可以通过指定不带索引的数组名来返回一个指向数组的指针。C 不支持在函数外返回局部变量的地址&#xff0c;除非定义局部变量为 static 变量。 方法一&#xff1a;将返回值定义…

完整的Apache+PHP8+MYSQL的配置

1.下载Apache和PHP下载Apache&#xff0c;地址&#xff1a;http://www.apachelounge.com/download/&#xff0c;如下图&#xff1a;将下载的压缩包解压到某个文件夹(比如&#xff1a;D:\software), 将解压后的文件夹重命名为Apache24下载PHP压缩包&#xff0c;地址&#xff1a;…

天云数据入选中国信通院《全球数据库产业图谱(2022)》

6月28日&#xff0c;由中国信息通信研究院&#xff08;以下简称“中国信通院”&#xff09;、中国通信标准化协会指导&#xff0c;中国通信标准化协会大数据技术标准推进委员会&#xff08;CCSA TC601&#xff09;主办的2022大数据产业峰会在京召开。 大会现场&#xff0c;魏凯…

黑马Pink老师JavaScript新教程上线啦~

今天是你开工的第几天&#xff1f; 是谁在回味假期的美好 是谁一上班就困困困 是谁身在上班心还在春节假 友情提示 再坚持4天&#xff0c;就到周末了 世间长假难有 但周末常在 无论是对于学习人还是打工人来说 周末都太美好了 和周末一样美好的 还有黑马的免费教程 …

11-Alibaba微服务流控组件Sentinel实战上

分布式系统遇到的问题 在一个高度服务化的系统中,我们实现的一个业务逻辑通常会依赖多个服务,比如:商品详情展示服务会依赖商品服务, 价格服务, 商品评论服务. 如图所示: 调用三个依赖服务会共享商品详情服务的线程池. 如果其中的商品评论服务不可用, 就会出现线程池里所有线程…

快来看啊,2023成都Java培训机构排行榜出来啦!

来啦&#xff0c;来啦&#xff01;我带着2023成都最新Java培训机构排行榜来啦。不知道怎么选择一个好的Java培训机构&#xff1f;停止寻觅&#xff0c;别再犹豫&#xff0c;看我这一篇就够啦&#xff01;一、成都动力节点动力节点&#xff0c;09年成立&#xff0c;14年来只专注…

C++语法复习笔记-3. C语言陷阱与C++改进

文章目录1.C语言常见语法问题1. 字符问题2.c语言的考虑3.C的思考2.C语言指针和数组问题1.问题表现传入参数会退化为指针2.问题原因3.C解决方案处理二维数组3. c语言移位问题1.问题表现逻辑右移还是算术右移移位操作位数的限制2.问题原因3.C中的解决方案4. C语言强制类型转换问题…

idea如何设置通过鼠标去改变字体的大小,idea如何进行作者署名以及时间显示

目录 一、idea如何设置通过鼠标去改变字体的大小 步骤&#xff1a; 1、缩小字体 2、放大字体 二、idea如何进行作者署名以及时间显示 步骤: 一、idea如何设置通过鼠标去改变字体的大小 步骤&#xff1a; 1、缩小字体 设置settings——>键盘设置keymap——>缩小字…

使用 cat 查看创建的软链接文件,出现 cat: 文件: 符号连接的层数过多

今天在使用 cat 查看创建的软链接文件时&#xff0c;出现了这样一个问题&#xff1a; 出现过程&#xff1a; 1、首先&#xff0c;在当前 home 目录下我有一个 2.txt 文件和 test 文件夹 &#xff1b; [wqlocalhost ~]$ ls 1.txt 2.txt Desktop Documents Downloads Musi…

如何将网站全部变成灰色的素装效果,瞧过来

最近&#xff0c;我们看到各个网站、app的页面全部变成了灰色或者黑白色。对于我们需要悼念的日子&#xff0c;使用这种素装&#xff0c;以表示我们对逝者或者英雄的悼念与缅怀&#xff01; 我们看到这种全站的内容全部为灰色&#xff0c;是不是很好奇呢&#xff1f;图片、按钮…

【香橙派4B】1、重刷系统

目录 准备材料 准备软件 准备系统镜像文件 下载镜像 解压镜像包 烧录镜像到SD卡 格式化SD卡 烧录镜像 SD卡烧录镜像到香橙派4B 买回来的香橙派4B&#xff0c;刚上电是安卓的系统&#xff0c;无法进行开发&#xff0c;所以需要进行重装系统。 准备材料 闪迪官方的SD卡&…

图例legend语法及设置

(1)设置图例位置 使用loc参数 plt.legend(loc‘lower left’) 0‘best’1‘upper right’2‘upper left’3‘lower left’4‘lower right’5‘right’6‘center left’7‘center right’8‘lower center’9‘upper center’10‘center’ (2)设置图例字体 #设置字体大小 fontsi…