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

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

诉求

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

相关技术

      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>

功能测试

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

73d8c9be8b2a4960a39693770de0ac9a.png

73d8c9be8b2a4960a39693770de0ac9a.png

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

73d8c9be8b2a4960a39693770de0ac9a.png

小结

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

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

实时协作编辑:多人同时在线编辑同一文档,显示协同编辑的人员,将信息更新为最新

历史版本控制:记录并保存文档的历史版本,当出现不可修复的错误可会退或者前进版本,以及用户的一些修改轨迹

讨论评论:在文档中添加评论和讨论功能,在一些文字或者图片附近可加以评论

权限管理:控制团队成员对文档的访问和编辑权限.


相关文章
|
2月前
|
开发框架 前端开发 网络协议
Spring Boot结合Netty和WebSocket,实现后台向前端实时推送信息
【10月更文挑战第18天】 在现代互联网应用中,实时通信变得越来越重要。WebSocket作为一种在单个TCP连接上进行全双工通信的协议,为客户端和服务器之间的实时数据传输提供了一种高效的解决方案。Netty作为一个高性能、事件驱动的NIO框架,它基于Java NIO实现了异步和事件驱动的网络应用程序。Spring Boot是一个基于Spring框架的微服务开发框架,它提供了许多开箱即用的功能和简化配置的机制。本文将详细介绍如何使用Spring Boot集成Netty和WebSocket,实现后台向前端推送信息的功能。
617 1
|
3月前
|
存储 JavaScript 前端开发
webSocket+Node+Js实现在线聊天(包含所有代码)
文章介绍了如何使用WebSocket、Node.js和JavaScript实现在线聊天功能,包括完整的前端和后端代码示例。
245 0
|
1月前
|
JavaScript 前端开发 Java
springboot解决js前端跨域问题,javascript跨域问题解决
本文介绍了如何在Spring Boot项目中编写Filter过滤器以处理跨域问题,并通过一个示例展示了使用JavaScript进行跨域请求的方法。首先,在Spring Boot应用中添加一个实现了`Filter`接口的类,设置响应头允许所有来源的跨域请求。接着,通过一个简单的HTML页面和jQuery发送AJAX请求到指定URL,验证跨域请求是否成功。文中还提供了请求成功的响应数据样例及请求效果截图。
springboot解决js前端跨域问题,javascript跨域问题解决
|
4月前
|
JavaScript 前端开发 网络协议
Vue.js 与 WebSocket 的惊世联姻!实时数据通信的震撼变革,你敢错过?
【8月更文挑战第30天】在现代Web开发中,实时数据通信至关重要。Vue.js作为流行前端框架,结合WebSocket技术,实现了高效实时的数据交互。本文简要介绍了WebSocket原理及其在Vue.js项目中的应用方法,包括建立连接、监听事件及数据处理等步骤,展示了如何利用二者结合轻松应对实时聊天、股票更新等多种场景,为开发者提供了实用指南。希望本文能帮助您更高效地实现Web应用的实时通信功能。
218 0
|
2月前
|
JavaScript Java PHP
快速对比:Django、Spring Boot、Node.js 和 PHP
快速对比:Django、Spring Boot、Node.js 和 PHP
113 7
|
2月前
|
前端开发 Java C++
RSocket vs WebSocket:Spring Boot 3.3 中的两大实时通信利器
本文介绍了在 Spring Boot 3.3 中使用 RSocket 和 WebSocket 实现实时通信的方法。RSocket 是一种高效的网络通信协议,支持多种通信模式,适用于微服务和流式数据传输。WebSocket 则是一种标准协议,支持全双工通信,适合实时数据更新场景。文章通过一个完整的示例,展示了如何配置项目、实现前后端交互和消息传递,并提供了详细的代码示例。通过这些技术,可以大幅提升系统的响应速度和处理效率。
|
3月前
|
JavaScript 前端开发 开发工具
五子棋小游戏(JS+Node+Websocket)可分房间对战
本文介绍了通过JS、Node和WebSocket实现的五子棋游戏,支持多人在线对战和观战功能。
105 1
五子棋小游戏(JS+Node+Websocket)可分房间对战
|
2月前
|
Java BI API
spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
这篇文章介绍了如何在Spring Boot项目中整合iTextPDF库来导出PDF文件,包括写入大文本和HTML代码,并分析了几种常用的Java PDF导出工具。
675 0
spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
|
2月前
|
缓存 Java 程序员
Java|SpringBoot 项目开发时,让 FreeMarker 文件编辑后自动更新
在开发过程中,FreeMarker 文件编辑后,每次都需要重启应用才能看到效果,效率非常低下。通过一些配置后,可以让它们免重启自动更新。
44 0
|
2月前
|
JavaScript 前端开发 API
Node.js 中的 WebSocket 底层实现
Node.js 中的 WebSocket 底层实现
82 0