文章目录
- 诉求
- 相关技术
- 思路展开
- 相关步骤
- pom配置
- 服务端相关配置
- 文本信息、用户广播处理逻辑
- 前端功能代码
- 功能测试
- 小结
诉求
实现页面实时在线文本协同编辑,且显示当前同时编辑文本的用户。
相关技术
Springboot(2.7.0)+Websocket+javascript
思路展开
- 页面展示当前登陆用户
- 页面有文本输入框(包含编辑、保存按钮)
- 页面展示编辑当前文本的用户
- 服务端广播处理文本信息的以及协同用户
相关步骤
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>
功能测试
同时打开多个页面,当编辑信息时会显示到同时编辑的用户列表。
当前用户点击保存时推出当前同时编辑的用户列表
小结
上面实现为简易实现,仅供参考,可能并不适用一些业务场景。
下面的是我的一些想法,在真实生产应用在线文档协同编辑有多个点
- 实时协作编辑:多人同时在线编辑同一文档,显示协同编辑的人员,将信息更新为最新
- 历史版本控制:记录并保存文档的历史版本,当出现不可修复的错误可会退或者前进版本,以及用户的一些修改轨迹
- 讨论评论:在文档中添加评论和讨论功能,在一些文字或者图片附近可加以评论
- 权限管理:控制团队成员对文档的访问和编辑权限.