小言_互联网的博客

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

410人阅读  评论(0)

诉求

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

相关技术

       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>

 

功能测试

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


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

小结

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

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

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

转载:https://blog.csdn.net/asd1358355022/article/details/128820043
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场