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

小结

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

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

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

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

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

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


相关文章
|
6天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
19 2
|
6天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的宠物援助平台附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的宠物援助平台附带文章源码部署视频讲解等
14 4
|
6天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的宠物交易平台附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的宠物交易平台附带文章源码部署视频讲解等
17 4
|
6天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的大学生入伍人员管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的大学生入伍人员管理系统附带文章源码部署视频讲解等
22 4
|
6天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的小区物流配送系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的小区物流配送系统附带文章源码部署视频讲解等
22 3
|
6天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp宿舍管理系统的附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp宿舍管理系统的附带文章源码部署视频讲解等
18 3
|
6天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的家政平台附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的家政平台附带文章源码部署视频讲解等
19 3
|
6天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的宠物医院系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的宠物医院系统附带文章源码部署视频讲解等
12 2
|
6天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的实验室安全考试系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的实验室安全考试系统附带文章源码部署视频讲解等
16 2
|
6天前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的学生网课学习效果评价附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的学生网课学习效果评价附带文章源码部署视频讲解等
16 2