Commit 19aa1f99 authored by ligaowei's avatar ligaowei

refactor(websocket): 优化DomSyncHandler及JWT握手拦截器实现

- 引入JwtHandshakeInterceptor组件用于WebSocket连接的JWT认证
- 使DomSyncWebSocketConfig支持通过依赖注入使用JwtHandshakeInterceptor
- 移除旧版内联JWT拦截器实现,改用单独组件类
- 优化DomSyncHandler,针对iframe模式简化DOM同步逻辑
- 移除DomSyncData类,合并相关字段简化数据传输结构
- 实现HTML内容转义以避免JSON序列化错误
- 取消传输样式和脚本,仅同步完整DOM和增量DOM变化
- 增加获取完整DOM时的重试机制以提升稳定性
- 修改大消息广播方案,添加消息大小检查与分片发送逻辑
- 删除不必要的压缩功能,改用纯文本消息分片传输
- 删除部分WebSocket、SSE及控制台消息监听,减少复杂度
- 改进Playwright初始化和页面导航监听逻辑,避免阻塞
- 精简WebSocket命令处理,去除对容器元素的特定限制
- 增加详细日志输出,方便定位和跟踪用户操作及错误
- 优化异常处理,确保错误信息及时反馈给客户端
- 调整客户端连接管理,实现异常时安全移除无效会话
parent b2158e16
...@@ -4,6 +4,8 @@ import org.springframework.context.annotation.Bean; ...@@ -4,6 +4,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer; import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
...@@ -11,6 +13,7 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry ...@@ -11,6 +13,7 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
import org.springframework.web.socket.server.HandshakeInterceptor; import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import pangea.hiagent.utils.JwtUtil;
import pangea.hiagent.websocket.DomSyncHandler; import pangea.hiagent.websocket.DomSyncHandler;
import java.util.Map; import java.util.Map;
...@@ -22,6 +25,12 @@ import java.util.Map; ...@@ -22,6 +25,12 @@ import java.util.Map;
@EnableWebSocket @EnableWebSocket
public class DomSyncWebSocketConfig implements WebSocketConfigurer { public class DomSyncWebSocketConfig implements WebSocketConfigurer {
private final JwtHandshakeInterceptor jwtHandshakeInterceptor;
public DomSyncWebSocketConfig(JwtHandshakeInterceptor jwtHandshakeInterceptor) {
this.jwtHandshakeInterceptor = jwtHandshakeInterceptor;
}
// 注入DomSyncHandler,交由Spring管理生命周期 // 注入DomSyncHandler,交由Spring管理生命周期
@Bean @Bean
public DomSyncHandler domSyncHandler() { public DomSyncHandler domSyncHandler() {
...@@ -32,45 +41,45 @@ public class DomSyncWebSocketConfig implements WebSocketConfigurer { ...@@ -32,45 +41,45 @@ public class DomSyncWebSocketConfig implements WebSocketConfigurer {
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(domSyncHandler(), "/ws/dom-sync") registry.addHandler(domSyncHandler(), "/ws/dom-sync")
// 添加握手拦截器用于JWT验证 // 添加握手拦截器用于JWT验证
.addInterceptors(new JwtHandshakeInterceptor()) .addInterceptors(jwtHandshakeInterceptor)
// 生产环境:替换为具体域名,禁止使用* // 生产环境:替换为具体域名,禁止使用*
.setAllowedOrigins("*"); .setAllowedOrigins("*");
} }
}
/** /**
* JWT握手拦截器,用于WebSocket连接时的认证 * JWT握手拦截器,用于WebSocket连接时的认证
*/ */
public static class JwtHandshakeInterceptor implements HandshakeInterceptor { @Component
@Override class JwtHandshakeInterceptor implements HandshakeInterceptor {
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, private final JwtUtil jwtUtil;
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// 首先尝试从请求头中获取JWT Token
String token = request.getHeaders().getFirst("Authorization");
// 如果请求头中没有,则尝试从查询参数中获取 public JwtHandshakeInterceptor(JwtUtil jwtUtil) {
if (token == null) { this.jwtUtil = jwtUtil;
String query = request.getURI().getQuery();
if (query != null) {
UriComponentsBuilder builder = UriComponentsBuilder.newInstance().query(query);
token = builder.build().getQueryParams().getFirst("token");
}
} }
if (token != null && token.startsWith("Bearer ")) { @Override
token = token.substring(7); // 移除"Bearer "前缀 public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
} WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
String token = extractTokenFromRequest(request);
if (token != null && !token.isEmpty()) { if (StringUtils.hasText(token)) {
try { try {
// 简单检查token是否包含典型的JWT部分 // 验证token是否有效
String[] parts = token.split("\\."); boolean isValid = jwtUtil.validateToken(token);
if (parts.length == 3) { if (isValid) {
// 基本格式正确,接受连接 // 获取真实的用户ID
String userId = jwtUtil.getUserIdFromToken(token);
if (userId != null) {
attributes.put("token", token); attributes.put("token", token);
// 使用token的一部分作为用户标识(实际应用中应该解析JWT获取用户ID) attributes.put("userId", userId);
attributes.put("userId", "user_" + token.substring(0, Math.min(8, token.length()))); System.out.println("WebSocket连接认证成功,用户ID: " + userId);
System.out.println("WebSocket连接认证成功,Token: " + token);
return true; return true;
} else {
System.err.println("无法从token中提取用户ID");
}
} else {
System.err.println("JWT验证失败,token可能已过期或无效");
} }
} catch (Exception e) { } catch (Exception e) {
System.err.println("JWT验证过程中发生错误: " + e.getMessage()); System.err.println("JWT验证过程中发生错误: " + e.getMessage());
...@@ -89,5 +98,29 @@ public class DomSyncWebSocketConfig implements WebSocketConfigurer { ...@@ -89,5 +98,29 @@ public class DomSyncWebSocketConfig implements WebSocketConfigurer {
WebSocketHandler wsHandler, Exception exception) { WebSocketHandler wsHandler, Exception exception) {
// 握手后处理,这里不需要特殊处理 // 握手后处理,这里不需要特殊处理
} }
/**
* 从请求头或参数中提取Token
* 复用JwtAuthenticationFilter中的逻辑
*/
private String extractTokenFromRequest(ServerHttpRequest request) {
// 首先尝试从请求头中提取Token
String authHeader = request.getHeaders().getFirst("Authorization");
if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
return token;
}
// 如果请求头中没有Token,则尝试从URL参数中提取
String query = request.getURI().getQuery();
if (query != null) {
UriComponentsBuilder builder = UriComponentsBuilder.newInstance().query(query);
String token = builder.build().getQueryParams().getFirst("token");
if (StringUtils.hasText(token)) {
return token;
}
}
return null;
} }
} }
\ No newline at end of file
package pangea.hiagent.dto; package pangea.hiagent.websocket;
import com.alibaba.fastjson2.annotation.JSONField; import com.alibaba.fastjson2.annotation.JSONField;
...@@ -6,11 +6,11 @@ import com.alibaba.fastjson2.annotation.JSONField; ...@@ -6,11 +6,11 @@ import com.alibaba.fastjson2.annotation.JSONField;
* DOM同步的数据传输对象 * DOM同步的数据传输对象
*/ */
public class DomSyncData { public class DomSyncData {
// 消息类型:init(初始化完整DOM)、update(增量DOM更新)、style(样式)、script(脚本) // 消息类型:init(初始化完整DOM)、update(增量DOM更新)、style(样式)、script(脚本)、fragment(分片消息)
@JSONField(name = "type") @JSONField(name = "type")
private String type; private String type;
// DOM内容(完整/增量) // DOM内容(完整/增量/分片
@JSONField(name = "dom") @JSONField(name = "dom")
private String dom; private String dom;
......
...@@ -5,17 +5,15 @@ import com.microsoft.playwright.*; ...@@ -5,17 +5,15 @@ import com.microsoft.playwright.*;
import com.microsoft.playwright.options.LoadState; import com.microsoft.playwright.options.LoadState;
import org.springframework.web.socket.*; import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler; import org.springframework.web.socket.handler.TextWebSocketHandler;
import pangea.hiagent.dto.DomSyncData;
import java.io.ByteArrayOutputStream;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.zip.GZIPOutputStream;
/** /**
* DOM同步的WebSocket处理器 * 优化的DOM同步WebSocket处理器
* 针对前端iframe显示特点进行了简化和优化
*/ */
public class DomSyncHandler extends TextWebSocketHandler { public class DomSyncHandler extends TextWebSocketHandler {
// 存储连接的前端客户端(线程安全) // 存储连接的前端客户端(线程安全)
...@@ -27,10 +25,6 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -27,10 +25,6 @@ public class DomSyncHandler extends TextWebSocketHandler {
private Page page; private Page page;
private BrowserContext context; private BrowserContext context;
// 压缩和分片配置
private static final int CHUNK_SIZE = 64 * 1024; // 64KB分片大小
private static final boolean ENABLE_COMPRESSION = true; // 是否启用压缩
// 连接数限制 // 连接数限制
private static final int MAX_CONNECTIONS_PER_USER = 5; // 每用户最大连接数 private static final int MAX_CONNECTIONS_PER_USER = 5; // 每用户最大连接数
private static final int MAX_COMMANDS_PER_SECOND = 10; // 每秒最大指令数 private static final int MAX_COMMANDS_PER_SECOND = 10; // 每秒最大指令数
...@@ -50,6 +44,60 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -50,6 +44,60 @@ public class DomSyncHandler extends TextWebSocketHandler {
messageCounters.merge(counterName, 1L, Long::sum); messageCounters.merge(counterName, 1L, Long::sum);
} }
/**
* 转义HTML内容中的特殊字符,防止JSON序列化错误
*
* @param content 待转义的HTML内容
* @return 转义后的内容
*/
private String escapeHtmlContent(String content) {
if (content == null || content.isEmpty()) {
return content;
}
StringBuilder sb = new StringBuilder(content.length() * 2); // 预分配空间以提高性能
for (int i = 0; i < content.length(); i++) {
char c = content.charAt(i);
switch (c) {
case '"':
sb.append("\\\"");
break;
case '\\':
sb.append("\\\\");
break;
case '/':
sb.append("\\/");
break;
case '\b':
sb.append("\\b");
break;
case '\f':
sb.append("\\f");
break;
case '\n':
sb.append("\\n");
break;
case '\r':
sb.append("\\r");
break;
case '\t':
sb.append("\\t");
break;
default:
// 处理控制字符
if (c < 0x20) {
sb.append(String.format("\\u%04x", (int) c));
} else {
sb.append(c);
}
break;
}
}
return sb.toString();
}
// 获取计数器值 // 获取计数器值
public static long getCounter(String counterName) { public static long getCounter(String counterName) {
return messageCounters.getOrDefault(counterName, 0L); return messageCounters.getOrDefault(counterName, 0L);
...@@ -80,10 +128,7 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -80,10 +128,7 @@ public class DomSyncHandler extends TextWebSocketHandler {
"--no-sandbox", // 服务器端必须,否则Chrome启动失败 "--no-sandbox", // 服务器端必须,否则Chrome启动失败
"--disable-dev-shm-usage", // 解决Linux下/dev/shm空间不足的问题 "--disable-dev-shm-usage", // 解决Linux下/dev/shm空间不足的问题
"--disable-gpu", // 禁用GPU,服务器端无需 "--disable-gpu", // 禁用GPU,服务器端无需
"--remote-allow-origins=*", // 允许跨域请求(处理外部样式/脚本) "--remote-allow-origins=*"))); // 允许跨域请求
"--disable-web-security", // 禁用网络安全检查(便于测试)
"--allow-running-insecure-content" // 允许不安全内容
)));
// 创建浏览器上下文(隔离环境) // 创建浏览器上下文(隔离环境)
context = browser.newContext(new Browser.NewContextOptions() context = browser.newContext(new Browser.NewContextOptions()
...@@ -99,71 +144,51 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -99,71 +144,51 @@ public class DomSyncHandler extends TextWebSocketHandler {
} catch (Exception e) { } catch (Exception e) {
System.err.println("Playwright初始化失败: " + e.getMessage()); System.err.println("Playwright初始化失败: " + e.getMessage());
e.printStackTrace(); e.printStackTrace();
// 尝试重新初始化
try {
System.out.println("尝试重新初始化Playwright...");
destroy(); // 先清理现有资源
Thread.sleep(1000); // 等待1秒
playwright = Playwright.create();
browser = playwright.chromium().launch(new BrowserType.LaunchOptions()
.setHeadless(true)
.setArgs(java.util.Arrays.asList(
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
"--remote-allow-origins=*")));
context = browser.newContext(new Browser.NewContextOptions()
.setViewportSize(1920, 1080));
page = context.newPage();
page.setDefaultTimeout(10000); // 10秒超时
System.out.println("Playwright重新初始化成功");
} catch (Exception re) {
System.err.println("Playwright重新初始化失败: " + re.getMessage());
re.printStackTrace();
}
} }
} }
/** /**
* 初始化页面监听事件(核心:捕获DOM、样式、脚本变化) * 初始化页面监听事件(核心:捕获DOM变化)
* 针对iframe显示特点进行了优化,简化了监听逻辑
*/ */
private void initPageListener() { private void initPageListener() {
// 初始化统计计数器 // 初始化统计计数器
messageCounters.put("domChanges", 0L); messageCounters.put("domChanges", 0L);
messageCounters.put("websocketMessages", 0L); messageCounters.put("websocketMessages", 0L);
messageCounters.put("sseEvents", 0L);
messageCounters.put("errors", 0L); messageCounters.put("errors", 0L);
// 1. 页面加载完成后,推送完整的DOM、样式、脚本(初始化) // 1. 页面加载完成后,推送完整的DOM(初始化)
page.onLoad(page -> { page.onLoad(page -> {
System.out.println("页面加载完成: " + page.url()); System.out.println("页面加载完成: " + page.url());
incrementCounter("pageLoads"); incrementCounter("pageLoads");
sendFullDomAndResourceToClients(); // 发送完整的DOM内容到客户端
sendFullDomToClientsWithRetry();
}); });
// 2. 监听DOM变化(使用MutationObserver),推送增量更新 // 2. 监听DOM变化(使用MutationObserver),推送增量更新
// 针对iframe特点优化:只监听body区域的变化
page.evaluate("() => {\n" + page.evaluate("() => {\n" +
" // 创建MutationObserver监听DOM变化\n" + " // 创建MutationObserver监听DOM变化\n" +
" const observer = new MutationObserver((mutations) => {\n" + " const observer = new MutationObserver((mutations) => {\n" +
" // 将变化的DOM节点转为字符串,发送给Playwright\n" + " // 将变化的DOM节点转为字符串,发送给Playwright\n" +
" const changes = mutations.map(mutation => ({\n" + " const changes = mutations.map(mutation => ({\n" +
" type: mutation.type,\n" + " type: mutation.type,\n" +
" target: mutation.target.outerHTML,\n" + " target: mutation.target.outerHTML || mutation.target.textContent,\n" +
" addedNodes: Array.from(mutation.addedNodes).map(node => node.outerHTML || ''),\n" + " addedNodes: Array.from(mutation.addedNodes).map(node => node.outerHTML || node.textContent || ''),\n" +
" removedNodes: Array.from(mutation.removedNodes).map(node => node.outerHTML || '')\n" + " removedNodes: Array.from(mutation.removedNodes).map(node => node.outerHTML || node.textContent || ''),\n" +
" attributeName: mutation.attributeName,\n" +
" oldValue: mutation.oldValue\n" +
" }));\n" + " }));\n" +
" // 调用Playwright的暴露函数,传递DOM变化数据\n" + " // 调用Playwright的暴露函数,传递DOM变化数据\n" +
" window.domChanged(JSON.stringify(changes));\n" + " window.domChanged(JSON.stringify(changes));\n" +
" });\n" + " });\n" +
" // 配置监听:监听所有节点的添加/删除、属性变化、子节点变化\n" + " // 配置监听:监听body节点的添加/删除、属性变化、子节点变化\n" +
" observer.observe(document.body, {\n" + " observer.observe(document.body, {\n" +
" childList: true,\n" + " childList: true,\n" +
" attributes: true,\n" + " attributes: true,\n" +
" subtree: true,\n" + " subtree: true,\n" +
" characterData: true\n" + " characterData: true,\n" +
" attributeOldValue: true\n" +
" });\n" + " });\n" +
"}"); "}");
...@@ -187,164 +212,18 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -187,164 +212,18 @@ public class DomSyncHandler extends TextWebSocketHandler {
return null; return null;
}); });
// 4. 监听WebSocket连接 // 4. 监听页面导航事件,导航后重新初始化
page.onWebSocket(webSocket -> {
System.out.println("检测到WebSocket连接: " + webSocket.url());
// 监听WebSocket的消息接收事件
webSocket.onFrameReceived(frame -> {
try {
incrementCounter("websocketMessages");
// 封装WebSocket消息,推送给前端
DomSyncData wsData = new DomSyncData(
"ws", // WebSocket类型
webSocket.url(), // WebSocket地址
frame.text(), // 消息内容
"",
getCurrentPageUrl()
);
String jsonData = JSON.toJSONString(wsData);
broadcastMessage(jsonData);
} catch (Exception e) {
String errorMsg = "处理WebSocket消息失败: " + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
incrementCounter("errors");
sendErrorToClients(errorMsg);
}
});
// 监听WebSocket关闭事件
webSocket.onClose(closedWebSocket -> {
try {
String closeInfo = "WebSocket连接已关闭: " + closedWebSocket.url();
System.out.println(closeInfo);
DomSyncData wsCloseData = new DomSyncData(
"ws-close", // WebSocket关闭类型
closedWebSocket.url(), // WebSocket地址
closeInfo, // 关闭信息
"",
getCurrentPageUrl()
);
String jsonData = JSON.toJSONString(wsCloseData);
broadcastMessage(jsonData);
} catch (Exception e) {
String errorMsg = "处理WebSocket关闭事件失败: " + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
incrementCounter("errors");
sendErrorToClients(errorMsg);
}
});
// 监听WebSocket帧发送事件
webSocket.onFrameSent(frame -> {
try {
// 记录发送的WebSocket帧(可用于调试)
System.out.println("WebSocket发送帧到: " + webSocket.url() + ", 内容长度: " +
(frame.text() != null ? frame.text().length() : 0));
} catch (Exception e) {
// 静默处理,避免影响主流程
System.err.println("记录WebSocket发送帧失败: " + e.getMessage());
}
});
// 注意:Playwright Java WebSocket API 可能不支持 onError 回调
// 如果需要错误处理,可以通过其他方式实现
});
// 5. 监听SSE(Server-Sent Events)连接
page.onResponse(response -> {
try {
// 检查是否为SSE响应
String contentType = response.headers().get("content-type");
if (contentType != null && contentType.contains("text/event-stream")) {
System.out.println("检测到SSE响应: " + response.url());
incrementCounter("sseResponses");
// 添加页面错误监听器来捕获SSE相关的错误
response.frame().page().onPageError(error -> {
try {
String errorMsg = "页面错误 (可能与SSE相关): " + error;
System.err.println(errorMsg);
incrementCounter("errors");
// 发送错误信息给客户端
DomSyncData errorData = new DomSyncData(
"sse-error", // SSE错误类型
response.url(), // SSE地址
errorMsg, // 错误信息
"",
getCurrentPageUrl()
);
broadcastMessage(JSON.toJSONString(errorData));
} catch (Exception e) {
System.err.println("处理SSE页面错误失败: " + e.getMessage());
e.printStackTrace();
incrementCounter("errors");
}
});
// 尝试通过控制台消息捕获SSE数据
response.frame().page().onConsoleMessage(consoleMessage -> {
try {
// 检查是否可能是SSE相关的消息
String text = consoleMessage.text();
if (text != null && (text.contains("event:") || text.contains("data:") || text.contains("id:"))) {
System.out.println("检测到可能的SSE数据: " + text);
incrementCounter("sseEvents");
// 处理SSE数据
DomSyncData sseDataObj = new DomSyncData(
"sse", // SSE类型
response.url(), // SSE地址
text, // SSE内容
"",
getCurrentPageUrl()
);
broadcastMessage(JSON.toJSONString(sseDataObj));
}
} catch (Exception e) {
System.err.println("处理SSE控制台消息失败: " + e.getMessage());
e.printStackTrace();
incrementCounter("errors");
}
});
// 尝试通过请求完成事件来捕获SSE数据
response.frame().page().onRequestFinished(request -> {
try {
// 检查是否是SSE请求
if (request.url().equals(response.url())) {
System.out.println("SSE请求完成: " + request.url());
// 可以在这里添加额外的处理逻辑
}
} catch (Exception e) {
System.err.println("处理SSE请求完成事件失败: " + e.getMessage());
e.printStackTrace();
incrementCounter("errors");
}
});
}
} catch (Exception e) {
String errorMsg = "检查SSE响应失败: " + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
incrementCounter("errors");
sendErrorToClients(errorMsg);
}
});
// 6. 监听页面导航事件,导航后重新初始化
page.onFrameNavigated(frame -> { page.onFrameNavigated(frame -> {
System.out.println("检测到页面导航: " + frame.url()); System.out.println("检测到页面导航: " + frame.url());
incrementCounter("navigations"); incrementCounter("navigations");
// 导航完成后,等待页面加载 // 异步处理导航完成后的DOM发送,避免阻塞
java.util.concurrent.CompletableFuture.runAsync(() -> {
try { try {
page.waitForLoadState(LoadState.LOAD); // 等待页面达到网络空闲状态,确保内容稳定
page.waitForLoadState(LoadState.NETWORKIDLE);
System.out.println("页面加载完成: " + frame.url()); System.out.println("页面加载完成: " + frame.url());
// 发送更新后的DOM内容到客户端 // 发送更新后的DOM内容到客户端
sendFullDomAndResourceToClients(); sendFullDomToClientsWithRetry();
} catch (Exception e) { } catch (Exception e) {
String errorMsg = "页面加载状态等待失败: " + e.getMessage(); String errorMsg = "页面加载状态等待失败: " + e.getMessage();
System.err.println(errorMsg); System.err.println(errorMsg);
...@@ -353,8 +232,9 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -353,8 +232,9 @@ public class DomSyncHandler extends TextWebSocketHandler {
sendErrorToClients(errorMsg); sendErrorToClients(errorMsg);
} }
}); });
});
// 7. 监听页面错误事件 // 5. 监听页面错误事件
page.onPageError(error -> { page.onPageError(error -> {
try { try {
String errorMsg = "页面错误: " + error; String errorMsg = "页面错误: " + error;
...@@ -366,49 +246,30 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -366,49 +246,30 @@ public class DomSyncHandler extends TextWebSocketHandler {
e.printStackTrace(); e.printStackTrace();
} }
}); });
// 8. 监听控制台消息事件
page.onConsoleMessage(message -> {
try {
String consoleMsg = "控制台消息 [" + message.type() + "]: " + message.text();
System.out.println(consoleMsg);
// 如果是错误类型的消息,也发送给客户端
if ("error".equals(message.type())) {
incrementCounter("consoleErrors");
sendErrorToClients(consoleMsg);
}
} catch (Exception e) {
System.err.println("处理控制台消息事件失败: " + e.getMessage());
e.printStackTrace();
}
});
} }
/** /**
* 推送完整的DOM、样式、脚本给所有客户端(初始化时调用) * 推送完整的DOM给所有客户端(初始化时调用)
* 针对iframe显示特点进行了优化,不再传输样式和脚本
*/ */
private void sendFullDomAndResourceToClients() { private void sendFullDomToClients() {
try { try {
// 1. 获取页面完整DOM(包含所有节点) // 1. 获取页面完整DOM(包含所有节点)
String fullDom = page.content(); String fullDom = page.content();
// 2. 获取页面所有样式(内联样式+外部样式表内容) // 2. 对HTML内容进行转义处理,防止JSON序列化错误
String fullStyle = getPageAllStyles(); String escapedDom = escapeHtmlContent(fullDom);
// 3. 获取页面所有脚本(内联脚本+外部脚本的URL,前端自行加载)
String fullScript = getPageAllScripts();
// 4. 封装数据 // 3. 封装数据(简化版,不传输样式和脚本)
DomSyncData data = new DomSyncData( DomSyncData data = new DomSyncData(
"init", // 初始化类型 "init", // 初始化类型
fullDom, escapedDom,
fullStyle, "", // 不再传输样式
fullScript, "", // 不再传输脚本
getCurrentPageUrl() // 当前页面URL getCurrentPageUrl() // 当前页面URL
); );
// 5. 序列化为JSON并推送 // 4. 序列化为JSON并推送
String jsonData = JSON.toJSONString(data); String jsonData = JSON.toJSONString(data);
broadcastMessage(jsonData); broadcastMessage(jsonData);
...@@ -419,77 +280,81 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -419,77 +280,81 @@ public class DomSyncHandler extends TextWebSocketHandler {
} }
/** /**
* 获取页面所有样式(内联+外部 * 推送完整的DOM给所有客户端(带重试机制
* 注意:外部样式表通过Playwright请求获取内容,避免前端跨域问题 * 针对页面导航过程中的不稳定状态增加了重试机制
*/ */
private String getPageAllStyles() { private void sendFullDomToClientsWithRetry() {
try { sendFullDomToClientsWithRetry(3, 500); // 默认重试3次,每次间隔500毫秒
Object result = page.evaluate("async () => {" +
"let styleText = '';" +
"// 1. 获取内联样式(<style>标签)" +
"document.querySelectorAll('style').forEach(style => {" +
" styleText += style.textContent + '\\n';" +
"});" +
"// 2. 获取外部样式表(<link rel=\"stylesheet\">)" +
"const linkElements = document.querySelectorAll('link[rel=\"stylesheet\"]');" +
"for (let link of linkElements) {" +
" try {" +
" const response = await fetch(link.href);" +
" if (response.ok) {" +
" const cssText = await response.text();" +
" styleText += cssText + '\\n';" +
" }" +
" } catch (e) {" +
" console.error('加载外部样式表失败:', link.href, e);" +
" }" +
"}" +
"return styleText;" +
"}");
return result != null ? result.toString() : "";
} catch (Exception e) {
System.err.println("获取页面样式失败: " + e.getMessage());
e.printStackTrace();
return "";
}
} }
/** /**
* 获取页面所有脚本(内联脚本+外部脚本URL,前端按需加载) * 推送完整的DOM给所有客户端(带重试机制)
* 外部脚本不直接获取内容,避免体积过大,前端通过URL加载(需处理跨域) *
* @param maxRetries 最大重试次数
* @param retryDelay 重试间隔(毫秒)
*/ */
private String getPageAllScripts() { private void sendFullDomToClientsWithRetry(int maxRetries, long retryDelay) {
Exception lastException = null;
for (int i = 0; i < maxRetries; i++) {
try { try {
Object result = page.evaluate("() => {" + // 1. 获取页面完整DOM(包含所有节点)
"let scriptData = {" + String fullDom = page.content();
" inline: [], // 内联脚本内容" +
" external: [] // 外部脚本URL" + // 2. 对HTML内容进行转义处理,防止JSON序列化错误
"};" + String escapedDom = escapeHtmlContent(fullDom);
"// 1. 获取内联脚本(<script>标签,无src属性)" +
"document.querySelectorAll('script:not([src])').forEach(script => {" + // 3. 封装数据(简化版,不传输样式和脚本)
" scriptData.inline.push(script.textContent);" + DomSyncData data = new DomSyncData(
"});" + "init", // 初始化类型
"// 2. 获取外部脚本URL(<script>标签,有src属性)" + escapedDom,
"document.querySelectorAll('script[src]').forEach(script => {" + "", // 不再传输样式
" scriptData.external.push(script.src);" + "", // 不再传输脚本
"});" + getCurrentPageUrl() // 当前页面URL
"return JSON.stringify(scriptData);" + );
"}");
return result != null ? result.toString() : "{}"; // 4. 序列化为JSON并推送
String jsonData = JSON.toJSONString(data);
broadcastMessage(jsonData);
// 成功发送,直接返回
return;
} catch (Exception e) { } catch (Exception e) {
System.err.println("获取页面脚本失败: " + e.getMessage()); lastException = e;
e.printStackTrace(); System.err.println("第" + (i+1) + "次获取完整DOM失败: " + e.getMessage());
return "{}";
// 如果不是最后一次重试,则等待一段时间再重试
if (i < maxRetries - 1) {
try {
Thread.sleep(retryDelay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
sendErrorToClients("获取完整DOM被中断:" + ie.getMessage());
return;
}
}
}
}
// 所有重试都失败了
if (lastException != null) {
lastException.printStackTrace();
sendErrorToClients("获取完整DOM失败(已重试" + maxRetries + "次):" + lastException.getMessage());
} }
} }
/** /**
* 推送增量DOM变化给所有客户端(DOM更新时调用) * 推送增量DOM变化给所有客户端(DOM更新时调用)
* 针对iframe显示特点进行了优化
*/ */
private void sendIncrementalDomToClients(String changes) { private void sendIncrementalDomToClients(String changes) {
try { try {
// 对变化数据进行转义处理
String escapedChanges = escapeHtmlContent(changes);
DomSyncData data = new DomSyncData( DomSyncData data = new DomSyncData(
"update", // 增量更新类型 "update", // 增量更新类型
changes, // DOM变化数据 escapedChanges, // DOM变化数据
"", // 样式无增量,空字符串 "", // 样式无增量,空字符串
"", // 脚本无增量,空字符串 "", // 脚本无增量,空字符串
getCurrentPageUrl() getCurrentPageUrl()
...@@ -503,29 +368,19 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -503,29 +368,19 @@ public class DomSyncHandler extends TextWebSocketHandler {
} }
/** /**
* 广播消息给所有客户端(带压缩和分片 * 广播消息给所有客户端(增加消息大小检查和分片处理
*/ */
private void broadcastMessage(String message) { private void broadcastMessage(String message) {
if (ENABLE_COMPRESSION && message.getBytes().length > CHUNK_SIZE) { // 检查消息大小,如果过大则进行分片处理
// 启用压缩且消息较大时,进行压缩和分片 final int MAX_MESSAGE_SIZE = 64 * 1024; // 64KB限制
try { try {
byte[] compressedData = compressData(message); if (message == null || message.isEmpty()) {
sendChunkedData(compressedData); return;
} catch (Exception e) {
e.printStackTrace();
// 压缩失败时,直接发送原始数据
sendRawMessage(message);
}
} else {
// 不启用压缩或消息较小时,直接发送
sendRawMessage(message);
}
} }
/** // 如果消息小于最大限制,直接发送
* 发送原始消息(不分片) if (message.length() <= MAX_MESSAGE_SIZE) {
*/
private void sendRawMessage(String message) {
TextMessage textMessage = new TextMessage(message); TextMessage textMessage = new TextMessage(message);
for (WebSocketSession client : clients.keySet()) { for (WebSocketSession client : clients.keySet()) {
try { try {
...@@ -533,52 +388,70 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -533,52 +388,70 @@ public class DomSyncHandler extends TextWebSocketHandler {
client.sendMessage(textMessage); client.sendMessage(textMessage);
} }
} catch (Exception e) { } catch (Exception e) {
System.err.println("发送消息给客户端失败: " + e.getMessage());
e.printStackTrace(); e.printStackTrace();
// 发送失败时移除客户端 // 发送失败时移除客户端
clients.remove(client); clients.remove(client);
} }
} }
} else {
// 消息过大,需要分片处理
System.out.println("消息过大,正在进行分片处理,总长度: " + message.length());
sendFragmentedMessage(message, MAX_MESSAGE_SIZE);
}
} catch (Exception e) {
System.err.println("广播消息时发生异常: " + e.getMessage());
e.printStackTrace();
} }
/**
* 压缩数据(使用GZIP)
*/
private byte[] compressData(String data) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gzipOut = new GZIPOutputStream(baos);
gzipOut.write(data.getBytes("UTF-8"));
gzipOut.close();
return baos.toByteArray();
} }
/** /**
* 发送分片数据 * 分片发送大消息
*
* @param message 完整消息
* @param maxFragmentSize 每个片段的最大大小
*/ */
private void sendChunkedData(byte[] data) { private void sendFragmentedMessage(String message, int maxFragmentSize) {
try { try {
int totalChunks = (int) Math.ceil((double) data.length / CHUNK_SIZE); int totalLength = message.length();
int fragmentCount = (int) Math.ceil((double) totalLength / maxFragmentSize);
for (int i = 0; i < totalChunks; i++) {
int start = i * CHUNK_SIZE; System.out.println("消息将被分为 " + fragmentCount + " 个片段发送");
int end = Math.min(start + CHUNK_SIZE, data.length);
byte[] chunk = java.util.Arrays.copyOfRange(data, start, end); for (int i = 0; i < fragmentCount; i++) {
int start = i * maxFragmentSize;
// 构造分片消息 int end = Math.min(start + maxFragmentSize, totalLength);
DomSyncData chunkData = new DomSyncData( String fragment = message.substring(start, end);
"chunk", // 分片类型
String.valueOf(i), // 当前分片索引 // 创建分片消息
String.valueOf(totalChunks), // 总分片数 DomSyncData fragmentData = new DomSyncData(
java.util.Base64.getEncoder().encodeToString(chunk), // 分片内容(Base64编码) "fragment", // 分片类型
"" fragment, // 分片内容
"", // 样式
"", // 脚本
getCurrentPageUrl() + "?fragment=" + i + "&total=" + fragmentCount // URL附加分片信息
); );
String jsonData = JSON.toJSONString(chunkData); String jsonData = JSON.toJSONString(fragmentData);
sendRawMessage(jsonData); TextMessage textMessage = new TextMessage(jsonData);
// 发送分片消息
for (WebSocketSession client : clients.keySet()) {
try {
if (client.isOpen()) {
client.sendMessage(textMessage);
}
} catch (Exception e) {
System.err.println("发送分片消息给客户端失败: " + e.getMessage());
e.printStackTrace();
// 发送失败时移除客户端
clients.remove(client);
}
}
} }
} catch (Exception e) { } catch (Exception e) {
System.err.println("发送分片数据失败: " + e.getMessage()); System.err.println("分片发送消息时发生异常: " + e.getMessage());
e.printStackTrace(); e.printStackTrace();
sendErrorToClients("发送分片数据失败: " + e.getMessage());
} }
} }
...@@ -603,7 +476,6 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -603,7 +476,6 @@ public class DomSyncHandler extends TextWebSocketHandler {
*/ */
@Override @Override
public void afterConnectionEstablished(WebSocketSession session) { public void afterConnectionEstablished(WebSocketSession session) {
// 生产环境:此处添加Token认证逻辑(参考之前的权限控制部分)
// 从会话属性中获取用户ID // 从会话属性中获取用户ID
String userId = (String) session.getAttributes().get("userId"); String userId = (String) session.getAttributes().get("userId");
if (userId == null) { if (userId == null) {
...@@ -628,7 +500,6 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -628,7 +500,6 @@ public class DomSyncHandler extends TextWebSocketHandler {
System.out.println("客户端连接成功:" + session.getId() + ",用户:" + userId); System.out.println("客户端连接成功:" + session.getId() + ",用户:" + userId);
// 连接建立后不立即推送DOM,而是等待客户端发送navigate指令 // 连接建立后不立即推送DOM,而是等待客户端发送navigate指令
// 这样可以避免在WebSocket连接建立但iframe还未准备好时推送数据
System.out.println("WebSocket连接已建立,等待客户端发送导航指令..."); System.out.println("WebSocket连接已建立,等待客户端发送导航指令...");
} }
...@@ -639,6 +510,10 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -639,6 +510,10 @@ public class DomSyncHandler extends TextWebSocketHandler {
protected void handleTextMessage(WebSocketSession session, TextMessage message) { protected void handleTextMessage(WebSocketSession session, TextMessage message) {
String payload = message.getPayload(); String payload = message.getPayload();
// 记录接收到的消息
System.out.println("收到WebSocket消息: " + payload);
incrementCounter("websocketMessages");
// 从会话属性中获取用户ID // 从会话属性中获取用户ID
String userId = (String) session.getAttributes().get("userId"); String userId = (String) session.getAttributes().get("userId");
if (userId == null) { if (userId == null) {
...@@ -654,7 +529,9 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -654,7 +529,9 @@ public class DomSyncHandler extends TextWebSocketHandler {
if (currentTime - lastTime < 1000) { if (currentTime - lastTime < 1000) {
// 同一秒内 // 同一秒内
if (commandCount >= MAX_COMMANDS_PER_SECOND) { if (commandCount >= MAX_COMMANDS_PER_SECOND) {
sendErrorToClients("指令执行过于频繁,请稍后再试"); String errorMsg = "指令执行过于频繁,请稍后再试";
System.err.println("用户 " + userId + " " + errorMsg);
sendErrorToClients(errorMsg);
return; return;
} }
commandCounts.put(userId, commandCount + 1); commandCounts.put(userId, commandCount + 1);
...@@ -667,13 +544,17 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -667,13 +544,17 @@ public class DomSyncHandler extends TextWebSocketHandler {
try { try {
// 检查Playwright实例是否有效 // 检查Playwright实例是否有效
if (!isPlaywrightInstanceValid()) { if (!isPlaywrightInstanceValid()) {
sendErrorToClients("Playwright实例未初始化或已关闭,请刷新页面重试"); String errorMsg = "Playwright实例未初始化或已关闭,请刷新页面重试";
System.err.println("用户 " + userId + " " + errorMsg);
sendErrorToClients(errorMsg);
return; return;
} }
// 检查WebSocket连接状态 // 检查WebSocket连接状态
if (!session.isOpen()) { if (!session.isOpen()) {
sendErrorToClients("WebSocket连接已关闭"); String errorMsg = "WebSocket连接已关闭";
System.err.println("用户 " + userId + " " + errorMsg);
sendErrorToClients(errorMsg);
return; return;
} }
...@@ -691,6 +572,8 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -691,6 +572,8 @@ public class DomSyncHandler extends TextWebSocketHandler {
param = ""; param = "";
} }
System.out.println("处理指令: " + command + ", 参数: " + param + " (用户: " + userId + ")");
switch (command) { switch (command) {
case "navigate": case "navigate":
// 导航到指定URL // 导航到指定URL
...@@ -751,12 +634,7 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -751,12 +634,7 @@ public class DomSyncHandler extends TextWebSocketHandler {
case "click": case "click":
// 点击指定选择器的元素(如#su) // 点击指定选择器的元素(如#su)
try { try {
// 检查选择器是否为容器元素本身
if ("#dom-view".equals(param)) {
sendErrorToClients("不能对容器元素 #dom-view 执行点击操作");
} else {
page.locator(param).click(); page.locator(param).click();
}
} catch (Exception e) { } catch (Exception e) {
String errorMsg = "点击元素失败:" + e.getMessage(); String errorMsg = "点击元素失败:" + e.getMessage();
System.err.println(errorMsg); System.err.println(errorMsg);
...@@ -767,12 +645,7 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -767,12 +645,7 @@ public class DomSyncHandler extends TextWebSocketHandler {
case "dblclick": case "dblclick":
// 双击指定选择器的元素 // 双击指定选择器的元素
try { try {
// 检查选择器是否为容器元素本身
if ("#dom-view".equals(param)) {
sendErrorToClients("不能对容器元素 #dom-view 执行双击操作");
} else {
page.locator(param).dblclick(); page.locator(param).dblclick();
}
} catch (Exception e) { } catch (Exception e) {
String errorMsg = "双击元素失败:" + e.getMessage(); String errorMsg = "双击元素失败:" + e.getMessage();
System.err.println(errorMsg); System.err.println(errorMsg);
...@@ -783,13 +656,7 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -783,13 +656,7 @@ public class DomSyncHandler extends TextWebSocketHandler {
case "hover": case "hover":
// 悬停在指定选择器的元素上 // 悬停在指定选择器的元素上
try { try {
// 检查选择器是否为容器元素本身,避免对容器元素执行hover操作
if ("#dom-view".equals(param)) {
// 忽略对容器元素本身的hover操作,不报错但也不执行
System.out.println("忽略对容器元素 #dom-view 的hover操作");
} else {
page.locator(param).hover(); page.locator(param).hover();
}
} catch (Exception e) { } catch (Exception e) {
String errorMsg = "悬停元素失败:" + e.getMessage(); String errorMsg = "悬停元素失败:" + e.getMessage();
System.err.println(errorMsg); System.err.println(errorMsg);
...@@ -797,47 +664,10 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -797,47 +664,10 @@ public class DomSyncHandler extends TextWebSocketHandler {
sendErrorToClients(errorMsg); sendErrorToClients(errorMsg);
} }
break; break;
case "mousedown":
// 鼠标按下指定选择器的元素
try {
// 检查选择器是否为容器元素本身
if ("#dom-view".equals(param)) {
sendErrorToClients("不能对容器元素 #dom-view 执行鼠标按下操作");
} else {
page.mouse().down(); // 使用page.mouse()而不是locator.mouse()
}
} catch (Exception e) {
String errorMsg = "鼠标按下失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "mouseup":
// 鼠标释放指定选择器的元素
try {
// 检查选择器是否为容器元素本身
if ("#dom-view".equals(param)) {
sendErrorToClients("不能对容器元素 #dom-view 执行鼠标释放操作");
} else {
page.mouse().up(); // 使用page.mouse()而不是locator.mouse()
}
} catch (Exception e) {
String errorMsg = "鼠标释放失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "focus": case "focus":
// 聚焦到指定元素 // 聚焦到指定元素
try { try {
// 检查选择器是否为容器元素本身
if ("#dom-view".equals(param)) {
sendErrorToClients("不能对容器元素 #dom-view 执行聚焦操作");
} else {
page.locator(param).focus(); page.locator(param).focus();
}
} catch (Exception e) { } catch (Exception e) {
String errorMsg = "聚焦元素失败:" + e.getMessage(); String errorMsg = "聚焦元素失败:" + e.getMessage();
System.err.println(errorMsg); System.err.println(errorMsg);
...@@ -848,12 +678,7 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -848,12 +678,7 @@ public class DomSyncHandler extends TextWebSocketHandler {
case "blur": case "blur":
// 失去焦点 // 失去焦点
try { try {
// 检查选择器是否为容器元素本身
if ("#dom-view".equals(param)) {
sendErrorToClients("不能对容器元素 #dom-view 执行失去焦点操作");
} else {
page.locator(param).blur(); page.locator(param).blur();
}
} catch (Exception e) { } catch (Exception e) {
String errorMsg = "失去焦点失败:" + e.getMessage(); String errorMsg = "失去焦点失败:" + e.getMessage();
System.err.println(errorMsg); System.err.println(errorMsg);
...@@ -928,14 +753,8 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -928,14 +753,8 @@ public class DomSyncHandler extends TextWebSocketHandler {
if (colonIndex != -1 && colonIndex < param.length() - 1) { if (colonIndex != -1 && colonIndex < param.length() - 1) {
String selector = param.substring(0, colonIndex); String selector = param.substring(0, colonIndex);
String content = param.substring(colonIndex + 1); String content = param.substring(colonIndex + 1);
// 检查选择器是否为容器元素本身
if ("#dom-view".equals(selector)) {
sendErrorToClients("不能对容器元素 #dom-view 执行输入操作");
} else {
Locator inputLocator = page.locator(selector); Locator inputLocator = page.locator(selector);
inputLocator.fill(content); // 使用fill替代type,因为type方法已被弃用 inputLocator.fill(content);
}
} }
} catch (Exception e) { } catch (Exception e) {
String errorMsg = "输入内容失败:" + e.getMessage(); String errorMsg = "输入内容失败:" + e.getMessage();
...@@ -952,14 +771,8 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -952,14 +771,8 @@ public class DomSyncHandler extends TextWebSocketHandler {
if (colonIndex != -1 && colonIndex < param.length() - 1) { if (colonIndex != -1 && colonIndex < param.length() - 1) {
String selector = param.substring(0, colonIndex); String selector = param.substring(0, colonIndex);
String content = param.substring(colonIndex + 1); String content = param.substring(colonIndex + 1);
// 检查选择器是否为容器元素本身
if ("#dom-view".equals(selector)) {
sendErrorToClients("不能对容器元素 #dom-view 执行输入操作");
} else {
Locator inputLocator = page.locator(selector); Locator inputLocator = page.locator(selector);
inputLocator.fill(content); // 使用fill替代type,支持更丰富的输入 inputLocator.fill(content);
}
} }
} catch (Exception e) { } catch (Exception e) {
String errorMsg = "输入内容失败:" + e.getMessage(); String errorMsg = "输入内容失败:" + e.getMessage();
...@@ -1101,12 +914,9 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -1101,12 +914,9 @@ public class DomSyncHandler extends TextWebSocketHandler {
stats.put("userConnections", new HashMap<>(userConnections)); stats.put("userConnections", new HashMap<>(userConnections));
stats.put("domChanges", getCounter("domChanges")); stats.put("domChanges", getCounter("domChanges"));
stats.put("websocketMessages", getCounter("websocketMessages")); stats.put("websocketMessages", getCounter("websocketMessages"));
stats.put("sseEvents", getCounter("sseEvents"));
stats.put("errors", getCounter("errors")); stats.put("errors", getCounter("errors"));
stats.put("pageLoads", getCounter("pageLoads")); stats.put("pageLoads", getCounter("pageLoads"));
stats.put("navigations", getCounter("navigations")); stats.put("navigations", getCounter("navigations"));
stats.put("consoleErrors", getCounter("consoleErrors"));
stats.put("sseResponses", getCounter("sseResponses"));
stats.put("timestamp", System.currentTimeMillis()); stats.put("timestamp", System.currentTimeMillis());
if (page != null) { if (page != null) {
...@@ -1126,7 +936,7 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -1126,7 +936,7 @@ public class DomSyncHandler extends TextWebSocketHandler {
*/ */
public void resetAllCounters() { public void resetAllCounters() {
try { try {
String[] counters = {"domChanges", "websocketMessages", "sseEvents", "errors", "pageLoads", "navigations", "consoleErrors", "sseResponses"}; String[] counters = {"domChanges", "websocketMessages", "errors", "pageLoads", "navigations"};
for (String counter : counters) { for (String counter : counters) {
resetCounter(counter); resetCounter(counter);
} }
...@@ -1164,5 +974,4 @@ public class DomSyncHandler extends TextWebSocketHandler { ...@@ -1164,5 +974,4 @@ public class DomSyncHandler extends TextWebSocketHandler {
return ""; return "";
} }
} }
} }
\ No newline at end of file
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
> >
<template #append> <template #append>
<el-button @click="navigateToUrl">访问</el-button> <el-button @click="navigateToUrl">访问</el-button>
<el-button @click="clearLogs">清空日志</el-button> <el-button @click="clearLogs">清空</el-button>
</template> </template>
</el-input> </el-input>
</div> </div>
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from 'vue' import { ref, onMounted, onUnmounted, nextTick, defineExpose, type Ref } from 'vue'
import { init, classModule, propsModule, styleModule, eventListenersModule, h } from 'snabbdom' import { init, classModule, propsModule, styleModule, eventListenersModule, h } from 'snabbdom'
// 添加pako库用于解压gzip数据 // 添加pako库用于解压gzip数据
import pako from 'pako' import pako from 'pako'
...@@ -66,32 +66,43 @@ const isConnected = ref<boolean>(false) ...@@ -66,32 +66,43 @@ const isConnected = ref<boolean>(false)
const connectWebSocket = () => { const connectWebSocket = () => {
// 避免重复连接 // 避免重复连接
if (websocket.value && websocket.value.readyState === WebSocket.OPEN) { if (websocket.value && websocket.value.readyState === WebSocket.OPEN) {
return addLog('WebSocket连接已存在且处于打开状态', 'info');
return;
} }
// 如果已有连接但处于其他状态,先关闭它 // 如果已有连接但处于其他状态,先关闭它
if (websocket.value) { if (websocket.value) {
try { try {
addLog(`WebSocket当前状态: ${websocket.value.readyState}`, 'info');
// 检查连接状态,避免在连接过程中关闭 // 检查连接状态,避免在连接过程中关闭
if (websocket.value.readyState === WebSocket.CONNECTING) { if (websocket.value.readyState === WebSocket.CONNECTING) {
addLog('WebSocket正在连接中,等待连接完成后再处理...', 'info'); addLog('WebSocket正在连接中,等待连接完成后再处理...', 'info');
// 等待连接完成后再决定是否关闭 // 等待连接完成后再决定是否关闭
websocket.value.onopen = () => { websocket.value.onopen = () => {
addLog('WebSocket连接已完成,现在关闭旧连接', 'info'); addLog('WebSocket连接已完成,现在关闭旧连接', 'info');
try {
websocket.value?.close(); websocket.value?.close();
} catch (closeError) {
addLog('关闭WebSocket连接时出错: ' + (closeError as Error).message, 'error');
}
websocket.value = null; websocket.value = null;
// 重新调用连接函数 // 重新调用连接函数
connectWebSocket(); setTimeout(() => connectWebSocket(), 100);
}; };
websocket.value.onerror = () => { websocket.value.onerror = () => {
addLog('WebSocket连接出错,关闭旧连接', 'info'); addLog('WebSocket连接出错,关闭旧连接', 'info');
try {
websocket.value?.close(); websocket.value?.close();
} catch (closeError) {
addLog('关闭WebSocket连接时出错: ' + (closeError as Error).message, 'error');
}
websocket.value = null; websocket.value = null;
// 重新调用连接函数 // 重新调用连接函数
connectWebSocket(); setTimeout(() => connectWebSocket(), 100);
}; };
return; return;
} else { } else {
addLog('关闭现有的WebSocket连接', 'info');
websocket.value.close(); websocket.value.close();
} }
} catch (e) { } catch (e) {
...@@ -104,68 +115,127 @@ const connectWebSocket = () => { ...@@ -104,68 +115,127 @@ const connectWebSocket = () => {
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
// 动态获取WebSocket连接地址,适配不同部署环境 // 动态获取WebSocket连接地址,适配不同部署环境
// 注意:前端开发服务器运行在5174端口,但后端服务运行在8080端口
// 在生产环境中,前端和后端通常运行在同一端口上
const isDev = import.meta.env.DEV
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const host = window.location.host
// 确保在开发环境中连接到正确的后端端口
let host;
if (isDev) {
// 在开发环境中,前端运行在5174端口,后端运行在8080端口
host = window.location.hostname + ':8080'
} else {
// 在生产环境中,使用当前主机和端口
host = window.location.host
}
const wsUrl = `${protocol}//${host}/ws/dom-sync${token ? '?token=' + token : ''}` const wsUrl = `${protocol}//${host}/ws/dom-sync${token ? '?token=' + token : ''}`
addLog('正在连接WebSocket: ' + wsUrl, 'info'); addLog('正在连接WebSocket: ' + wsUrl, 'info');
try { try {
const ws = new WebSocket(wsUrl) const ws = new WebSocket(wsUrl);
websocket.value = ws websocket.value = ws;
// 设置连接超时 // 设置连接超时
const connectionTimeout = setTimeout(() => { const connectionTimeout = setTimeout(() => {
if (ws.readyState === WebSocket.CONNECTING) { if (ws.readyState === WebSocket.CONNECTING) {
addLog('WebSocket连接超时,正在关闭连接...', 'error'); addLog('WebSocket连接超时,正在关闭连接...', 'error');
try {
ws.close(); ws.close();
} catch (closeError) {
addLog('关闭WebSocket连接时出错: ' + (closeError as Error).message, 'error');
}
isConnected.value = false; isConnected.value = false;
// 尝试重连 // 尝试重连
if (reconnectAttempts.value < maxReconnectAttempts.value) { if (reconnectAttempts.value < maxReconnectAttempts.value) {
reconnectAttempts.value++ reconnectAttempts.value++;
addLog(`WebSocket连接超时,正在尝试第${reconnectAttempts.value}次重连...`, 'error') addLog(`WebSocket连接超时,正在尝试第${reconnectAttempts.value}次重连...`, 'error');
const delay = Math.min(reconnectDelay.value * Math.pow(2, reconnectAttempts.value - 1), 30000) const delay = Math.min(reconnectDelay.value * Math.pow(2, reconnectAttempts.value - 1), 30000);
setTimeout(connectWebSocket, delay) setTimeout(() => connectWebSocket(), delay);
} else { } else {
addLog('WebSocket连接超时,达到最大重连次数,停止重连', 'error') addLog('WebSocket连接超时,达到最大重连次数,停止重连', 'error');
} }
} }
}, 10000); // 10秒超时 }, 30000); // 增加到30秒超时,给连接更多时间
// 连接打开事件 // 连接打开事件
ws.onopen = () => { ws.onopen = () => {
clearTimeout(connectionTimeout); // 清除超时定时器 clearTimeout(connectionTimeout); // 清除超时定时器
isConnected.value = true isConnected.value = true;
reconnectAttempts.value = 0 reconnectAttempts.value = 0;
addLog('WebSocket连接已建立', 'info') addLog('WebSocket连接已建立', 'info');
// WebSocket连接建立后,检查iframe是否已经加载完成 // WebSocket连接建立后,检查iframe是否已经加载完成
// 如果iframe已经加载完成,重新添加事件监听器以确保连接状态正确 // 如果iframe已经加载完成,重新添加事件监听器以确保连接状态正确
if (domViewRef.value && domViewRef.value.contentDocument) { if (domViewRef.value && domViewRef.value.contentDocument) {
addIframeEventListeners() addLog('WebSocket连接建立后检测到iframe已就绪,绑定事件监听器', 'info');
} // 延迟绑定以确保iframe完全就绪
setTimeout(() => {
removeIframeEventListeners();
addIframeEventListeners();
}, 300);
} else {
addLog('WebSocket连接建立后iframe尚未就绪,等待iframe加载完成', 'info');
} }
};
// 接收消息事件 // 接收消息事件
ws.onmessage = (event) => { ws.onmessage = (event) => {
try { try {
const data = JSON.parse(event.data) // 验证数据是否为空
if (!event.data) {
addLog('接收到空数据', 'error');
return;
}
// 检查数据是否为字符串
if (typeof event.data !== 'string') {
addLog('接收到非字符串数据: ' + typeof event.data, 'error');
return;
}
// 检查数据长度
if (event.data.length > 10 * 1024 * 1024) { // 10MB限制
addLog('接收到的数据过大: ' + event.data.length + ' 字节', 'error');
return;
}
// 尝试解析JSON
const data = JSON.parse(event.data);
// 处理分片数据 // 处理分片数据
if (data.type === 'chunk') { if (data.type === 'chunk') {
handleChunkData(data) handleChunkData(data);
} else { } else {
handleDomSyncData(data) handleDomSyncData(data);
} }
} catch (e) { } catch (e) {
addLog('解析数据失败:' + (e as Error).message, 'error') addLog('解析数据失败:' + (e as Error).message, 'error');
addLog('原始数据:' + event.data.substring(0, 100) + '...', 'error') addLog('原始数据长度:' + (event.data ? event.data.length : 0) + ' 字节', 'error');
// 显示部分原始数据用于调试,但限制长度
if (event.data) {
const previewLength = Math.min(200, event.data.length);
addLog('原始数据预览:' + event.data.substring(0, previewLength) + (event.data.length > previewLength ? '...' : ''), 'error');
// 检查是否包含常见的JSON格式错误
const lastBraceIndex = event.data.lastIndexOf('}');
const lastBracketIndex = event.data.lastIndexOf(']');
const lastValidIndex = Math.max(lastBraceIndex, lastBracketIndex);
if (lastValidIndex > 0 && lastValidIndex < event.data.length - 1) {
addLog('数据可能被截断,最后有效字符位置: ' + lastValidIndex + ', 总长度: ' + event.data.length, 'error');
}
} }
} }
};
// 连接关闭事件 // 连接关闭事件
ws.onclose = (event) => { ws.onclose = (event) => {
clearTimeout(connectionTimeout); // 清除超时定时器 clearTimeout(connectionTimeout); // 清除超时定时器
isConnected.value = false isConnected.value = false;
addLog(`WebSocket连接已关闭,代码: ${event.code}, 原因: ${event.reason}`, 'info'); addLog(`WebSocket连接已关闭,代码: ${event.code}, 原因: ${event.reason}`, 'info');
// 检查关闭原因 // 检查关闭原因
...@@ -173,44 +243,60 @@ const connectWebSocket = () => { ...@@ -173,44 +243,60 @@ const connectWebSocket = () => {
addLog('WebSocket连接异常关闭,可能是网络问题或服务器未响应', 'error'); addLog('WebSocket连接异常关闭,可能是网络问题或服务器未响应', 'error');
} }
// 只有在非正常关闭的情况下才尝试重连
if (event.code !== 1000) { // 1000表示正常关闭
if (reconnectAttempts.value < maxReconnectAttempts.value) { if (reconnectAttempts.value < maxReconnectAttempts.value) {
reconnectAttempts.value++ reconnectAttempts.value++;
addLog(`WebSocket连接已断开,正在尝试第${reconnectAttempts.value}次重连...`, 'error') addLog(`WebSocket连接已断开,正在尝试第${reconnectAttempts.value}次重连...`, 'error');
// 指数退避重连策略 // 指数退避重连策略
const delay = Math.min(reconnectDelay.value * Math.pow(2, reconnectAttempts.value - 1), 30000) const delay = Math.min(reconnectDelay.value * Math.pow(2, reconnectAttempts.value - 1), 30000);
setTimeout(connectWebSocket, delay) // 最大延迟30秒 setTimeout(() => connectWebSocket(), delay); // 最大延迟30秒
} else { } else {
addLog('WebSocket连接已断开,达到最大重连次数,停止重连', 'error') addLog('WebSocket连接已断开,达到最大重连次数,停止重连', 'error');
// 重置重连次数,以便用户手动重新连接
reconnectAttempts.value = 0;
} }
} else {
addLog('WebSocket连接正常关闭,无需重连', 'info');
} }
};
// 连接错误事件 // 连接错误事件
ws.onerror = (error) => { ws.onerror = (error) => {
clearTimeout(connectionTimeout); // 清除超时定时器 clearTimeout(connectionTimeout); // 清除超时定时器
isConnected.value = false isConnected.value = false;
addLog('WebSocket错误:' + (error as any).message, 'error') addLog('WebSocket错误:' + (error as any).message, 'error');
// 检查网络连接状态
if (!navigator.onLine) {
addLog('网络连接不可用,请检查您的网络连接', 'error');
}
// 如果连接失败,尝试重新连接 // 如果连接失败,尝试重新连接
if (reconnectAttempts.value < maxReconnectAttempts.value) { if (reconnectAttempts.value < maxReconnectAttempts.value) {
reconnectAttempts.value++ reconnectAttempts.value++;
addLog(`WebSocket连接错误,正在尝试第${reconnectAttempts.value}次重连...`, 'error') addLog(`WebSocket连接错误,正在尝试第${reconnectAttempts.value}次重连...`, 'error');
// 指数退避重连策略 // 指数退避重连策略
const delay = Math.min(reconnectDelay.value * Math.pow(2, reconnectAttempts.value - 1), 30000) const delay = Math.min(reconnectDelay.value * Math.pow(2, reconnectAttempts.value - 1), 30000);
setTimeout(connectWebSocket, delay) setTimeout(() => connectWebSocket(), delay);
} else { } else {
addLog('WebSocket连接错误,达到最大重连次数,停止重连', 'error') addLog('WebSocket连接错误,达到最大重连次数,停止重连', 'error');
} addLog('提示:请检查网络连接或后端服务是否正常运行', 'info');
// 重置重连次数,以便用户手动重新连接
reconnectAttempts.value = 0;
} }
};
} catch (e) { } catch (e) {
addLog('创建WebSocket连接失败: ' + (e as Error).message, 'error'); addLog('创建WebSocket连接失败: ' + (e as Error).message, 'error');
// 如果创建连接失败,也尝试重连 // 如果创建连接失败,也尝试重连
if (reconnectAttempts.value < maxReconnectAttempts.value) { if (reconnectAttempts.value < maxReconnectAttempts.value) {
reconnectAttempts.value++ reconnectAttempts.value++;
addLog(`WebSocket连接创建失败,正在尝试第${reconnectAttempts.value}次重连...`, 'error') addLog(`WebSocket连接创建失败,正在尝试第${reconnectAttempts.value}次重连...`, 'error');
const delay = Math.min(reconnectDelay.value * Math.pow(2, reconnectAttempts.value - 1), 30000) const delay = Math.min(reconnectDelay.value * Math.pow(2, reconnectAttempts.value - 1), 30000);
setTimeout(connectWebSocket, delay) setTimeout(() => connectWebSocket(), delay);
} else { } else {
addLog('WebSocket连接创建失败,达到最大重连次数,停止重连', 'error') addLog('WebSocket连接创建失败,达到最大重连次数,停止重连', 'error');
addLog('提示:请检查网络连接或后端服务是否正常运行', 'info');
} }
} }
} }
...@@ -218,8 +304,14 @@ const connectWebSocket = () => { ...@@ -218,8 +304,14 @@ const connectWebSocket = () => {
// iframe加载完成事件 // iframe加载完成事件
const onIframeLoad = () => { const onIframeLoad = () => {
addLog('iframe加载完成', 'info') addLog('iframe加载完成', 'info')
// 使用nextTick确保DOM完全更新后再添加事件监听器
nextTick(() => { // 使用更稳健的方式确保iframe内容文档可用
const checkAndBindListeners = (attempt = 1) => {
const maxAttempts = 10;
// 检查iframe是否已准备好
if (domViewRef.value && domViewRef.value.contentDocument) {
try {
// 移除旧的事件监听器(如果有的话) // 移除旧的事件监听器(如果有的话)
removeIframeEventListeners(); removeIframeEventListeners();
// 添加事件监听器到iframe内容文档 // 添加事件监听器到iframe内容文档
...@@ -232,7 +324,27 @@ const onIframeLoad = () => { ...@@ -232,7 +324,27 @@ const onIframeLoad = () => {
if (isConnected.value && websocket.value && websocket.value.readyState === WebSocket.OPEN) { if (isConnected.value && websocket.value && websocket.value.readyState === WebSocket.OPEN) {
addIframeEventListeners() addIframeEventListeners()
} }
})
addLog(`iframe事件监听器绑定成功,尝试次数: ${attempt}`, 'info');
} catch (e) {
addLog(`iframe事件监听器绑定失败 (尝试 ${attempt}): ${e.message}`, 'error');
// 如果还有重试机会,继续重试
if (attempt < maxAttempts) {
setTimeout(() => checkAndBindListeners(attempt + 1), 200);
}
}
} else if (attempt < maxAttempts) {
// 如果iframe还未准备好,继续重试
addLog(`iframe内容文档尚未准备就绪 (尝试 ${attempt}),继续等待...`, 'warn');
setTimeout(() => checkAndBindListeners(attempt + 1), 300);
} else {
addLog('iframe内容文档准备超时,无法绑定事件监听器', 'error');
}
};
// 启动检查和绑定过程
checkAndBindListeners();
} }
// 监听iframe内部的导航事件 // 监听iframe内部的导航事件
...@@ -241,17 +353,17 @@ const monitorIframeNavigation = () => { ...@@ -241,17 +353,17 @@ const monitorIframeNavigation = () => {
const iframe = domViewRef.value as HTMLIFrameElement; const iframe = domViewRef.value as HTMLIFrameElement;
if (!iframe || !iframe.contentWindow) { if (!iframe || !iframe.contentWindow) {
addLog('无法访问iframe内容窗口', 'error') addLog('无法访问iframe内容窗口', 'error');
return return;
} }
try { try {
// 监听iframe内部的beforeunload事件 // 监听iframe内部的beforeunload事件
const beforeUnloadHandler = () => { const beforeUnloadHandler = () => {
addLog('iframe即将导航到新页面', 'info') addLog('iframe即将导航到新页面', 'info');
// 在页面卸载前移除事件监听器 // 在页面卸载前移除事件监听器
removeIframeEventListeners(); removeIframeEventListeners();
} };
// 使用函数调用方式解决类型错误 // 使用函数调用方式解决类型错误
const contentWindow = iframe.contentWindow; const contentWindow = iframe.contentWindow;
...@@ -263,15 +375,16 @@ const monitorIframeNavigation = () => { ...@@ -263,15 +375,16 @@ const monitorIframeNavigation = () => {
// 监听iframe内部的popstate事件(浏览器前进后退) // 监听iframe内部的popstate事件(浏览器前进后退)
const popStateHandler = () => { const popStateHandler = () => {
addLog('iframe历史状态改变', 'info') addLog('iframe历史状态改变', 'info');
// 页面状态改变后重新绑定事件监听器 // 页面状态改变后重新绑定事件监听器
setTimeout(() => { setTimeout(() => {
if (domViewRef.value && domViewRef.value.contentDocument) { if (domViewRef.value && domViewRef.value.contentDocument) {
removeIframeEventListeners(); removeIframeEventListeners();
addIframeEventListeners() addIframeEventListeners();
} addLog('iframe历史状态改变后重新绑定事件监听器完成', 'info');
}, 1500)
} }
}, 1500);
};
if (contentWindow) { if (contentWindow) {
contentWindow.addEventListener('popstate', popStateHandler as EventListener); contentWindow.addEventListener('popstate', popStateHandler as EventListener);
...@@ -281,15 +394,16 @@ const monitorIframeNavigation = () => { ...@@ -281,15 +394,16 @@ const monitorIframeNavigation = () => {
// 监听hashchange事件 // 监听hashchange事件
const hashChangeHandler = () => { const hashChangeHandler = () => {
addLog('iframe哈希值改变', 'info') addLog('iframe哈希值改变', 'info');
// 哈希改变后重新绑定事件监听器 // 哈希改变后重新绑定事件监听器
setTimeout(() => { setTimeout(() => {
if (domViewRef.value && domViewRef.value.contentDocument) { if (domViewRef.value && domViewRef.value.contentDocument) {
removeIframeEventListeners(); removeIframeEventListeners();
addIframeEventListeners() addIframeEventListeners();
} addLog('iframe哈希值改变后重新绑定事件监听器完成', 'info');
}, 1500)
} }
}, 1500);
};
if (contentWindow) { if (contentWindow) {
contentWindow.addEventListener('hashchange', hashChangeHandler as EventListener); contentWindow.addEventListener('hashchange', hashChangeHandler as EventListener);
...@@ -303,10 +417,11 @@ const monitorIframeNavigation = () => { ...@@ -303,10 +417,11 @@ const monitorIframeNavigation = () => {
// 页面重新可见时重新绑定事件监听器 // 页面重新可见时重新绑定事件监听器
setTimeout(() => { setTimeout(() => {
removeIframeEventListeners(); removeIframeEventListeners();
addIframeEventListeners() addIframeEventListeners();
}, 1000) addLog('页面重新可见时重新绑定事件监听器完成', 'info');
} }, 1000);
} }
};
// 使用setTimeout来延迟添加事件监听器,避免类型问题 // 使用setTimeout来延迟添加事件监听器,避免类型问题
setTimeout(() => { setTimeout(() => {
...@@ -316,8 +431,25 @@ const monitorIframeNavigation = () => { ...@@ -316,8 +431,25 @@ const monitorIframeNavigation = () => {
}, 0); }, 0);
// 保存引用以便移除 // 保存引用以便移除
(iframe as any).__visibilityChangeHandler = visibilityChangeHandler; (iframe as any).__visibilityChangeHandler = visibilityChangeHandler;
// 监听iframe加载完成事件
const loadHandler = () => {
addLog('iframe内容加载完成', 'info');
// 确保事件监听器已正确绑定
setTimeout(() => {
addIframeEventListeners();
}, 500);
};
if (contentWindow) {
contentWindow.addEventListener('load', loadHandler as EventListener);
// 保存引用以便移除
(iframe as any).__loadHandler = loadHandler;
}
addLog('iframe导航事件监听器设置完成', 'info');
} catch (e) { } catch (e) {
addLog('设置iframe导航监听失败: ' + (e as Error).message, 'error') addLog('设置iframe导航监听失败: ' + (e as Error).message, 'error');
} }
} }
...@@ -359,8 +491,18 @@ const cleanupIframeListeners = () => { ...@@ -359,8 +491,18 @@ const cleanupIframeListeners = () => {
delete (iframe as any).__visibilityChangeHandler; delete (iframe as any).__visibilityChangeHandler;
} }
// 移除load事件监听器
if ((iframe as any).__loadHandler) {
if (iframe.contentWindow) {
iframe.contentWindow.removeEventListener('load', (iframe as any).__loadHandler);
}
delete (iframe as any).__loadHandler;
}
// 移除iframe内的事件监听器 // 移除iframe内的事件监听器
removeIframeEventListeners(); removeIframeEventListeners();
addLog('iframe监听器清理完成', 'info');
} catch (e) { } catch (e) {
addLog('清理iframe监听器失败: ' + (e as Error).message, 'error'); addLog('清理iframe监听器失败: ' + (e as Error).message, 'error');
} }
...@@ -369,38 +511,80 @@ const cleanupIframeListeners = () => { ...@@ -369,38 +511,80 @@ const cleanupIframeListeners = () => {
// 为iframe内容添加事件监听器 // 为iframe内容添加事件监听器
const addIframeEventListeners = () => { const addIframeEventListeners = () => {
if (!domViewRef.value || !domViewRef.value.contentDocument) { if (!domViewRef.value || !domViewRef.value.contentDocument) {
addLog('无法访问iframe内容文档', 'error') addLog('无法访问iframe内容文档', 'error');
return return;
} }
const iframeDoc = domViewRef.value.contentDocument const iframeDoc = domViewRef.value.contentDocument;
try { try {
// 先移除已存在的监听器,避免重复绑定 // 先移除已存在的监听器,避免重复绑定
removeIframeEventListeners(); removeIframeEventListeners();
// 添加点击事件监听器 // 添加点击事件监听器
iframeDoc.addEventListener('click', handleIframeClick, { passive: true }) iframeDoc.addEventListener('click', handleIframeClick, { passive: true });
// 添加输入事件监听器(带防抖) // 添加输入事件监听器(带防抖)
const debouncedInputHandler = debounce(handleIframeInput, 500); const debouncedInputHandler = debounce(handleIframeInput, 500);
(iframeDoc as any).__debouncedInputHandler = debouncedInputHandler; // 保存引用以便移除 (iframeDoc as any).__debouncedInputHandler = debouncedInputHandler; // 保存引用以便移除
iframeDoc.addEventListener('input', debouncedInputHandler, { passive: true }) iframeDoc.addEventListener('input', debouncedInputHandler, { passive: true });
// 添加表单提交事件监听器 // 添加表单提交事件监听器
iframeDoc.addEventListener('submit', handleIframeSubmit) iframeDoc.addEventListener('submit', handleIframeSubmit);
// 添加滚动事件监听器(带节流) // 添加滚动事件监听器(带节流)
const throttledScrollHandler = throttle(handleIframeScroll, 200); const throttledScrollHandler = throttle(handleIframeScroll, 200);
(iframeDoc as any).__throttledScrollHandler = throttledScrollHandler; // 保存引用以便移除 (iframeDoc as any).__throttledScrollHandler = throttledScrollHandler; // 保存引用以便移除
iframeDoc.addEventListener('scroll', throttledScrollHandler, { passive: true }) iframeDoc.addEventListener('scroll', throttledScrollHandler, { passive: true });
// 添加键盘事件监听器 // 添加键盘事件监听器
iframeDoc.addEventListener('keydown', handleIframeKeyDown, { passive: true }) iframeDoc.addEventListener('keydown', handleIframeKeyDown, { passive: true });
// 添加DOMContentLoaded事件监听器,确保文档完全加载
const domContentLoadedHandler = () => {
addLog('iframe内容文档DOM加载完成', 'info');
};
(iframeDoc as any).__domContentLoadedHandler = domContentLoadedHandler;
iframeDoc.addEventListener('DOMContentLoaded', domContentLoadedHandler);
// 添加MutationObserver监控DOM变化
const mutationObserver = new MutationObserver((mutations) => {
// 当DOM发生变化时,重新绑定事件监听器以确保新元素能响应事件
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// 对于新增节点,延迟重新绑定事件监听器
setTimeout(() => {
addIframeEventListeners();
}, 100);
}
});
});
addLog('已为iframe添加事件监听器', 'info') mutationObserver.observe(iframeDoc.body, {
childList: true,
subtree: true
});
(iframeDoc as any).__mutationObserver = mutationObserver; // 保存引用以便移除
// 添加页面可见性变化监听器
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
// 页面重新可见时重新绑定事件监听器
setTimeout(() => {
removeIframeEventListeners();
addIframeEventListeners();
addLog('页面重新可见时重新绑定事件监听器完成', 'info');
}, 1000);
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
(iframeDoc as any).__visibilityChangeListener = handleVisibilityChange; // 保存引用以便移除
addLog('已为iframe添加事件监听器', 'info');
} catch (e) { } catch (e) {
addLog('为iframe添加事件监听器失败: ' + (e as Error).message, 'error') addLog('为iframe添加事件监听器失败: ' + (e as Error).message, 'error');
} }
} }
...@@ -433,6 +617,26 @@ const removeIframeEventListeners = () => { ...@@ -433,6 +617,26 @@ const removeIframeEventListeners = () => {
// 移除键盘事件监听器 // 移除键盘事件监听器
iframeDoc.removeEventListener('keydown', handleIframeKeyDown); iframeDoc.removeEventListener('keydown', handleIframeKeyDown);
// 移除DOMContentLoaded事件监听器
if ((iframeDoc as any).__domContentLoadedHandler) {
iframeDoc.removeEventListener('DOMContentLoaded', (iframeDoc as any).__domContentLoadedHandler);
delete (iframeDoc as any).__domContentLoadedHandler;
}
// 移除MutationObserver
if ((iframeDoc as any).__mutationObserver) {
(iframeDoc as any).__mutationObserver.disconnect();
delete (iframeDoc as any).__mutationObserver;
}
// 移除页面可见性变化监听器
if ((iframeDoc as any).__visibilityChangeListener) {
document.removeEventListener('visibilitychange', (iframeDoc as any).__visibilityChangeListener);
delete (iframeDoc as any).__visibilityChangeListener;
}
addLog('已移除iframe事件监听器', 'info');
} catch (e) { } catch (e) {
addLog('移除iframe事件监听器失败: ' + (e as Error).message, 'error'); addLog('移除iframe事件监听器失败: ' + (e as Error).message, 'error');
} }
...@@ -463,34 +667,45 @@ const handleIframeSubmit = (event: Event) => { ...@@ -463,34 +667,45 @@ const handleIframeSubmit = (event: Event) => {
event.preventDefault() // 阻止默认提交行为 event.preventDefault() // 阻止默认提交行为
const selector = getElementSelector(target) const selector = getElementSelector(target)
sendCommand('submit', selector) sendCommand('submit', selector)
addLog('已发送表单提交指令', 'info') addLog('已发送表单提交指令: ' + selector, 'info')
// 对于表单提交,我们需要更智能地处理后续的事件监听器重新绑定 // 对于表单提交,我们需要更智能地处理后续的事件监听器重新绑定
const retryBindListeners = () => { const retryBindListeners = () => {
let attempts = 0; let attempts = 0;
const maxAttempts = 10; const maxAttempts = 20; // 增加尝试次数
const interval = setInterval(() => { const interval = setInterval(() => {
attempts++; attempts++;
if (domViewRef.value && domViewRef.value.contentDocument) { if (domViewRef.value && domViewRef.value.contentDocument) {
removeIframeEventListeners();
addIframeEventListeners(); addIframeEventListeners();
clearInterval(interval); clearInterval(interval);
addLog('表单提交后重新绑定事件监听器成功', 'info'); addLog(`表单提交后重新绑定事件监听器成功,尝试次数: ${attempts}`, 'info');
} else if (attempts >= maxAttempts) { } else if (attempts >= maxAttempts) {
clearInterval(interval); clearInterval(interval);
addLog('表单提交后重新绑定事件监听器失败,达到最大尝试次数', 'error'); addLog('表单提交后重新绑定事件监听器失败,达到最大尝试次数', 'error');
} }
}, 500); }, 200); // 缩短间隔时间
}; };
// 立即尝试一次 // 立即尝试一次
setTimeout(() => { setTimeout(() => {
if (domViewRef.value && domViewRef.value.contentDocument) { if (domViewRef.value && domViewRef.value.contentDocument) {
removeIframeEventListeners();
addIframeEventListeners(); addIframeEventListeners();
} }
}, 500); }, 50); // 缩短首次尝试的延迟
// 启动重试机制 // 启动重试机制
retryBindListeners(); retryBindListeners();
// 添加一个超时保护,确保即使重试机制失败也能最终绑定监听器
setTimeout(() => {
if (domViewRef.value && domViewRef.value.contentDocument) {
removeIframeEventListeners();
addIframeEventListeners();
addLog('表单提交后最终保护性绑定事件监听器', 'info');
}
}, 8000); // 8秒后最终尝试
} }
} }
...@@ -640,6 +855,8 @@ const doSendCommand = (command: string, param: string) => { ...@@ -640,6 +855,8 @@ const doSendCommand = (command: string, param: string) => {
} }
} else { } else {
addLog('WebSocket连接已断开,无法发送指令', 'error') addLog('WebSocket连接已断开,无法发送指令', 'error')
// 尝试重新连接
connectWebSocket()
} }
} }
...@@ -765,8 +982,45 @@ const handleChunkData = (data: any) => { ...@@ -765,8 +982,45 @@ const handleChunkData = (data: any) => {
} }
// 验证JSON格式 // 验证JSON格式
try {
const parsedData = JSON.parse(jsonData); const parsedData = JSON.parse(jsonData);
handleDomSyncData(parsedData); handleDomSyncData(parsedData);
} catch (parseError) {
addLog('解析完整分片数据失败:' + (parseError as Error).message, 'error');
addLog('数据长度:' + jsonData.length + ' 字符', 'error');
// 显示部分数据用于调试
const previewLength = Math.min(200, jsonData.length);
addLog('数据预览:' + jsonData.substring(0, previewLength) + (jsonData.length > previewLength ? '...' : ''), 'error');
// 检查常见问题
if (jsonData.includes('\0')) {
addLog('警告:数据中包含空字符,可能导致解析失败', 'error');
}
// 尝试修复常见的JSON问题
try {
let fixedData = jsonData;
// 移除可能的BOM标记
if (fixedData.charCodeAt(0) === 0xFEFF) {
fixedData = fixedData.substring(1);
}
// 移除末尾的空字符
fixedData = fixedData.replace(/[\0]+$/, '');
// 重新尝试解析
const reParsedData = JSON.parse(fixedData);
addLog('修复后数据解析成功', 'info');
handleDomSyncData(reParsedData);
return;
} catch (fixError) {
addLog('修复后仍无法解析数据: ' + (fixError as Error).message, 'error');
}
throw parseError;
}
handleDomSyncData(parsedData);
} catch (e) { } catch (e) {
addLog('解析完整分片数据失败:' + (e as Error).message, 'error'); addLog('解析完整分片数据失败:' + (e as Error).message, 'error');
addLog('数据内容预览:' + fullContent.substring(0, 100) + '...', 'error'); addLog('数据内容预览:' + fullContent.substring(0, 100) + '...', 'error');
...@@ -825,14 +1079,50 @@ const renderFullDomInIframe = (data: any) => { ...@@ -825,14 +1079,50 @@ const renderFullDomInIframe = (data: any) => {
const doc = domViewRef.value.contentDocument; const doc = domViewRef.value.contentDocument;
// 清空原有内容 // 使用更安全的方式设置内容,避免使用document.write
// 检查传入的数据是否是完整的HTML文档
if (data.dom.startsWith('<!DOCTYPE html>') || data.dom.startsWith('<html')) {
// 如果是完整HTML文档,使用现代DOM API方式设置内容
// 先清空现有文档
doc.open(); doc.open();
doc.write(data.dom); doc.write(data.dom);
doc.close(); doc.close();
} else {
// 如果不是完整HTML文档,假定是body内容
// 使用更安全的innerHTML设置方式
try {
// 先清空body内容
while (doc.body.firstChild) {
doc.body.removeChild(doc.body.firstChild);
}
// 创建临时容器来解析HTML
const tempContainer = doc.createElement('div');
tempContainer.innerHTML = data.dom;
// 将解析后的元素逐个移动到body中
while (tempContainer.firstChild) {
doc.body.appendChild(tempContainer.firstChild);
}
} catch (innerError) {
// 如果上面的方法失败,回退到直接设置innerHTML
addLog('使用安全DOM操作失败,回退到innerHTML方式: ' + (innerError as Error).message, 'warn');
// 在设置innerHTML之前,先移除所有现有的子元素
while (doc.body.firstChild) {
doc.body.removeChild(doc.body.firstChild);
}
doc.body.innerHTML = data.dom;
}
}
// 添加样式(如果有) // 添加样式(如果有)
if (data.style) { if (data.style) {
// 先移除已存在的样式
const existingStyles = doc.querySelectorAll('style[data-dom-sync-style]');
existingStyles.forEach(style => style.remove());
const styleTag = doc.createElement('style'); const styleTag = doc.createElement('style');
styleTag.setAttribute('data-dom-sync-style', 'true');
styleTag.textContent = data.style; styleTag.textContent = data.style;
doc.head.appendChild(styleTag); doc.head.appendChild(styleTag);
} }
...@@ -845,7 +1135,7 @@ const renderFullDomInIframe = (data: any) => { ...@@ -845,7 +1135,7 @@ const renderFullDomInIframe = (data: any) => {
// 重新绑定事件监听器 // 重新绑定事件监听器
setTimeout(() => { setTimeout(() => {
addIframeEventListeners(); addIframeEventListeners();
}, 1000); }, 500);
} catch (e) { } catch (e) {
addLog('在iframe中渲染完整DOM失败:' + (e as Error).message, 'error'); addLog('在iframe中渲染完整DOM失败:' + (e as Error).message, 'error');
addLog('DOM数据预览:' + (data.dom ? data.dom.substring(0, 100) + '...' : '无'), 'error'); addLog('DOM数据预览:' + (data.dom ? data.dom.substring(0, 100) + '...' : '无'), 'error');
...@@ -869,13 +1159,38 @@ const executeScriptsInIframe = (scriptJson: string, doc: Document) => { ...@@ -869,13 +1159,38 @@ const executeScriptsInIframe = (scriptJson: string, doc: Document) => {
return; return;
} }
// 验证脚本数据结构
if (!scriptData || typeof scriptData !== 'object') {
addLog('脚本数据格式无效', 'error');
return;
}
if (!Array.isArray(scriptData.inline)) {
scriptData.inline = [];
}
if (!Array.isArray(scriptData.external)) {
scriptData.external = [];
}
// 执行内联脚本 // 执行内联脚本
if (scriptData.inline && Array.isArray(scriptData.inline)) { if (scriptData.inline && Array.isArray(scriptData.inline)) {
scriptData.inline.forEach((scriptText: string) => { scriptData.inline.forEach((scriptText: string) => {
if (scriptText && scriptText.trim() !== '') { if (scriptText && scriptText.trim() !== '') {
try {
// 创建script标签并执行
const scriptTag = doc.createElement('script'); const scriptTag = doc.createElement('script');
scriptTag.textContent = scriptText; scriptTag.textContent = scriptText;
doc.body.appendChild(scriptTag); // 添加安全检查,防止危险脚本执行
if (isScriptSafe(scriptText)) {
// 将脚本添加到head而不是body,以避免重复执行
doc.head.appendChild(scriptTag);
} else {
addLog('检测到不安全的内联脚本,已阻止执行', 'warn');
}
} catch (execError) {
addLog('执行内联脚本失败:' + (execError as Error).message, 'error');
}
} }
}); });
} }
...@@ -884,12 +1199,17 @@ const executeScriptsInIframe = (scriptJson: string, doc: Document) => { ...@@ -884,12 +1199,17 @@ const executeScriptsInIframe = (scriptJson: string, doc: Document) => {
if (scriptData.external && Array.isArray(scriptData.external)) { if (scriptData.external && Array.isArray(scriptData.external)) {
scriptData.external.forEach((scriptUrl: string) => { scriptData.external.forEach((scriptUrl: string) => {
if (scriptUrl && scriptUrl.trim() !== '') { if (scriptUrl && scriptUrl.trim() !== '') {
try {
const scriptTag = doc.createElement('script'); const scriptTag = doc.createElement('script');
scriptTag.src = scriptUrl; scriptTag.src = scriptUrl;
scriptTag.crossOrigin = 'anonymous'; scriptTag.crossOrigin = 'anonymous';
scriptTag.onload = () => addLog('外部脚本加载完成:' + scriptUrl, 'info'); scriptTag.onload = () => addLog('外部脚本加载完成:' + scriptUrl, 'info');
scriptTag.onerror = () => addLog('外部脚本加载失败:' + scriptUrl, 'error'); scriptTag.onerror = (error) => addLog('外部脚本加载失败:' + scriptUrl + ' 错误: ' + (error as any).message, 'error');
doc.body.appendChild(scriptTag); // 添加到head而不是body,这样更符合标准
doc.head.appendChild(scriptTag);
} catch (loadError) {
addLog('添加外部脚本标签失败:' + (loadError as Error).message, 'error');
}
} }
}); });
} }
...@@ -917,21 +1237,31 @@ const updateIncrementalDomInIframe = (data: any) => { ...@@ -917,21 +1237,31 @@ const updateIncrementalDomInIframe = (data: any) => {
change.addedNodes.forEach((nodeHtml: string) => { change.addedNodes.forEach((nodeHtml: string) => {
if (nodeHtml) { if (nodeHtml) {
try { try {
// 使用更安全的方式插入节点
const tempDiv = doc.createElement('div'); const tempDiv = doc.createElement('div');
tempDiv.innerHTML = nodeHtml; tempDiv.innerHTML = nodeHtml;
const newChild = tempDiv.firstChild as HTMLElement;
if (newChild) { // 获取所有子节点而不是仅第一个
const nodesToAdd = Array.from(tempDiv.childNodes);
if (nodesToAdd.length > 0) {
// 查找目标父节点 // 查找目标父节点
const parentSelector = change.target ? change.target : 'body'; const parentSelector = change.target ? change.target : 'body';
const parentElement = doc.querySelector(parentSelector); let parentElement = doc.querySelector(parentSelector);
if (parentElement) {
parentElement.appendChild(newChild); // 如果找不到指定的父节点,使用body作为后备
} else { if (!parentElement) {
doc.body.appendChild(newChild); parentElement = doc.body;
addLog('未找到指定父节点,使用body作为后备: ' + parentSelector, 'warn');
} }
// 将所有节点添加到父节点中
nodesToAdd.forEach(node => {
parentElement!.appendChild(node.cloneNode(true));
});
} }
} catch (e) { } catch (e) {
addLog('添加节点失败:' + (e as Error).message, 'error'); addLog('添加节点失败:' + (e as Error).message, 'error');
addLog('节点HTML预览:' + (nodeHtml ? nodeHtml.substring(0, 100) + '...' : '无'), 'error');
} }
} }
}); });
...@@ -939,49 +1269,64 @@ const updateIncrementalDomInIframe = (data: any) => { ...@@ -939,49 +1269,64 @@ const updateIncrementalDomInIframe = (data: any) => {
// 处理移除的节点 // 处理移除的节点
if (change.removedNodes && Array.isArray(change.removedNodes)) { if (change.removedNodes && Array.isArray(change.removedNodes)) {
change.removedNodes.forEach((nodeHtml: string) => { change.removedNodes.forEach((nodeSelector: string) => {
if (nodeHtml) { if (nodeSelector) {
try { try {
const tempDiv = doc.createElement('div'); // 直接使用选择器查找要移除的元素
tempDiv.innerHTML = nodeHtml; const targetElement = doc.querySelector(nodeSelector);
const nodeToRemove = tempDiv.firstChild as HTMLElement;
if (nodeToRemove) {
// 构建更精确的选择器
const selector = getElementSelector(nodeToRemove);
const targetElement = doc.querySelector(selector);
if (targetElement) { if (targetElement) {
targetElement.remove(); targetElement.remove();
} else { } else {
// 如果精确选择器找不到,尝试使用其他方式 addLog('未找到要移除的元素: ' + nodeSelector, 'warn');
const fallbackSelector = nodeToRemove.id ? '#' + nodeToRemove.id :
nodeToRemove.tagName.toLowerCase();
const fallbackElement = doc.querySelector(fallbackSelector);
if (fallbackElement) {
fallbackElement.remove();
}
}
} }
} catch (e) { } catch (e) {
addLog('移除节点失败:' + (e as Error).message, 'error'); addLog('移除节点失败:' + (e as Error).message, 'error');
addLog('节点选择器:' + nodeSelector, 'error');
}
}
});
}
// 处理属性更新
if (change.attributes && Array.isArray(change.attributes)) {
change.attributes.forEach((attrChange: any) => {
try {
const targetElement = doc.querySelector(attrChange.selector);
if (targetElement) {
if (attrChange.value === null || attrChange.value === undefined) {
targetElement.removeAttribute(attrChange.name);
} else {
targetElement.setAttribute(attrChange.name, attrChange.value);
}
} }
} catch (e) {
addLog('更新属性失败:' + (e as Error).message, 'error');
addLog('属性变更详情:' + JSON.stringify(attrChange), 'error');
} }
}); });
} }
}); });
// 更新完成后重新绑定事件监听器
setTimeout(() => {
addIframeEventListeners();
}, 100);
} catch (e) { } catch (e) {
addLog('在iframe中增量更新DOM失败:' + (e as Error).message, 'error'); addLog('在iframe中增量更新DOM失败:' + (e as Error).message, 'error');
addLog('变更数据预览:' + (data.dom ? data.dom.substring(0, 200) + '...' : '无'), 'error');
} }
} }
// 导航到指定URL // 导航到指定URL
const navigateToUrl = () => { const navigateToUrl = (url?: string) => {
if (urlInput.value) { const targetUrl = url || urlInput.value
if (targetUrl) {
// 验证URL格式 // 验证URL格式
if (!isValidUrl(urlInput.value)) { if (!isValidUrl(targetUrl)) {
addLog('无效的URL格式: ' + urlInput.value, 'error') addLog('无效的URL格式: ' + targetUrl, 'error')
return return
} }
sendCommand('navigate', urlInput.value) sendCommand('navigate', targetUrl)
} }
} }
...@@ -994,46 +1339,121 @@ const isValidUrl = (url: string): boolean => { ...@@ -994,46 +1339,121 @@ const isValidUrl = (url: string): boolean => {
// 检查是否以http://或https://开头 // 检查是否以http://或https://开头
if (!url.toLowerCase().startsWith('http://') && !url.toLowerCase().startsWith('https://')) { if (!url.toLowerCase().startsWith('http://') && !url.toLowerCase().startsWith('https://')) {
// 如果没有协议前缀,自动添加https:// // 如果没有协议前缀,自动添加https://
urlInput.value = 'https://' + url url = 'https://' + url
url = urlInput.value
} }
// 使用正则表达式进行基本的URL格式验证 // 确保URL末尾没有多余的斜杠(除了域名根路径)
const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/ if (url.endsWith('/') && url.length > 8) { // 8是'http://a'的长度
return urlPattern.test(url) url = url.slice(0, -1);
}
// 使用URL构造函数验证URL格式
try {
new URL(url);
return true;
} catch (e) {
return false;
}
}
// 检查脚本是否安全执行
const isScriptSafe = (scriptText: string): boolean => {
// 简单的安全检查,防止明显的恶意脚本执行
const unsafePatterns = [
/document\.cookie/i,
/localStorage/i,
/sessionStorage/i,
/indexedDB/i,
/navigator\.sendBeacon/i,
/XMLHttpRequest/i,
/fetch\s*\(/i,
/eval\s*\(/i,
/Function\s*\(/i,
/setTimeout\s*\([^,]+,[^,]+\)/i,
/setInterval\s*\([^,]+,[^,]+\)/i,
/location\s*=\s*/i,
/window\.open/i,
/document\.write/i
];
// 检查是否存在不安全模式
for (const pattern of unsafePatterns) {
if (pattern.test(scriptText)) {
return false;
}
}
return true;
} }
// 添加一个方法来初始化页面 // 添加一个方法来初始化页面
const initializePage = () => { const initializePage = () => {
addLog('开始初始化页面...', 'info');
// 设置初始iframe源为about:blank // 设置初始iframe源为about:blank
iframeSrc.value = 'about:blank' iframeSrc.value = 'about:blank';
// 添加一个小延迟确保iframe初始化完成后再连接WebSocket
setTimeout(() => {
// 连接WebSocket // 连接WebSocket
connectWebSocket() connectWebSocket();
addLog('WebSocket连接已启动', 'info');
}, 500);
// 添加一个备用机制,如果iframe长时间未加载则强制重新加载
const iframeLoadTimeout = setTimeout(() => {
if (!domViewRef.value || !domViewRef.value.contentDocument) {
addLog('iframe加载超时,尝试重新初始化...', 'warn');
// 重新设置iframe源以触发重新加载
iframeSrc.value = 'about:blank';
// 再次尝试连接WebSocket
setTimeout(() => {
connectWebSocket();
}, 1000);
}
}, 10000); // 增加到10秒超时
// 保存超时ID以便在组件卸载时清除
(window as any).__iframeLoadTimeout = iframeLoadTimeout;
} }
// 添加日志 // 添加日志
const addLog = (message: string, type: string) => { const addLog = (message: string, type: string) => {
const logArea = document.getElementById('log-area') try {
const logArea = document.getElementById('log-area');
if (logArea) { if (logArea) {
const logItem = document.createElement('div') const logItem = document.createElement('div');
logItem.className = 'log-' + type logItem.className = 'log-' + type;
logItem.textContent = `[${new Date().toLocaleTimeString()}] ${message}` logItem.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logArea.appendChild(logItem)
// 添加数据属性以便于调试
logItem.setAttribute('data-log-type', type);
logItem.setAttribute('data-timestamp', new Date().toISOString());
logArea.appendChild(logItem);
// 限制日志数量,避免内存泄漏 // 限制日志数量,避免内存泄漏
while (logArea.children.length > 100) { while (logArea.children.length > 100) {
logArea.removeChild(logArea.firstChild!) logArea.removeChild(logArea.firstChild!);
} }
// 使用nextTick确保DOM更新后再滚动 // 使用nextTick确保DOM更新后再滚动
nextTick(() => { nextTick(() => {
// 检查用户是否在查看历史日志 // 检查用户是否在查看历史日志
const isScrolledToBottom = logArea.scrollHeight - logArea.clientHeight <= logArea.scrollTop + 10 const isScrolledToBottom = logArea.scrollHeight - logArea.clientHeight <= logArea.scrollTop + 10;
if (isScrolledToBottom) { if (isScrolledToBottom) {
// 自动滚动到最新日志 // 自动滚动到最新日志
logArea.scrollTop = logArea.scrollHeight logArea.scrollTop = logArea.scrollHeight;
} }
}) });
} else {
// 如果无法找到日志区域,使用console作为后备
console.log(`[${type.toUpperCase()}][${new Date().toLocaleTimeString()}] ${message}`);
}
} catch (e) {
// 即使日志记录失败,也不要影响主流程
console.error('日志记录失败:', e);
} }
} }
...@@ -1048,10 +1468,30 @@ const clearLogs = () => { ...@@ -1048,10 +1468,30 @@ const clearLogs = () => {
// 组件挂载时初始化页面 // 组件挂载时初始化页面
onMounted(() => { onMounted(() => {
initializePage() initializePage()
// 监听页面可见性变化,当页面重新获得焦点时检查连接状态
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
// 如果WebSocket未连接,尝试重新连接
if (!isConnected.value || !websocket.value || websocket.value.readyState !== WebSocket.OPEN) {
addLog('页面重新激活,检查WebSocket连接状态...', 'info')
connectWebSocket()
}
}
}
document.addEventListener('visibilitychange', handleVisibilityChange)
// 组件卸载时移除事件监听器
onUnmounted(() => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
})
}) })
// 组件卸载时关闭WebSocket // 组件卸载时关闭WebSocket
onUnmounted(() => { onUnmounted(() => {
addLog('组件正在卸载,清理资源...', 'info');
// 清理iframe相关的事件监听器 // 清理iframe相关的事件监听器
cleanupIframeListeners(); cleanupIframeListeners();
...@@ -1059,6 +1499,7 @@ onUnmounted(() => { ...@@ -1059,6 +1499,7 @@ onUnmounted(() => {
if (websocket.value) { if (websocket.value) {
try { try {
websocket.value.close() websocket.value.close()
addLog('WebSocket连接已关闭', 'info');
} catch (e) { } catch (e) {
addLog('关闭WebSocket连接时出错: ' + (e as Error).message, 'error'); addLog('关闭WebSocket连接时出错: ' + (e as Error).message, 'error');
} }
...@@ -1069,11 +1510,27 @@ onUnmounted(() => { ...@@ -1069,11 +1510,27 @@ onUnmounted(() => {
if (chunkTimeoutId.value) { if (chunkTimeoutId.value) {
clearTimeout(chunkTimeoutId.value); clearTimeout(chunkTimeoutId.value);
chunkTimeoutId.value = null; chunkTimeoutId.value = null;
addLog('分片超时定时器已清除', 'info');
}
// 清除iframe加载超时定时器
if ((window as any).__iframeLoadTimeout) {
clearTimeout((window as any).__iframeLoadTimeout);
(window as any).__iframeLoadTimeout = null;
addLog('iframe加载超时定时器已清除', 'info');
} }
// 重置状态 // 重置状态
isConnected.value = false; isConnected.value = false;
reconnectAttempts.value = 0; reconnectAttempts.value = 0;
addLog('组件资源清理完成', 'info');
})
// 暴露给父组件的方法
defineExpose({
urlInput,
navigateToUrl
}) })
</script> </script>
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
<div class="page-content"> <div class="page-content">
<div class="left-panel"> <div class="left-panel">
<DomSyncViewer /> <DomSyncViewer ref="domSyncViewerRef" />
</div> </div>
<div class="right-panel"> <div class="right-panel">
...@@ -56,11 +56,20 @@ ...@@ -56,11 +56,20 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import DomSyncViewer from '@/components/DomSyncViewer.vue' import DomSyncViewer from '@/components/DomSyncViewer.vue'
// 定义DomSyncViewer组件实例的类型
interface DomSyncViewerInstance {
urlInput: Ref<string>
navigateToUrl: () => void
}
// 响应式数据 // 响应式数据
const targetUrl = ref('https://www.baidu.com') const targetUrl = ref('https://www.baidu.com')
const connectionStatus = ref('disconnected') const connectionStatus = ref('disconnected')
const currentPage = ref('未连接') const currentPage = ref('未连接')
// DomSyncViewer组件引用
const domSyncViewerRef = ref<DomSyncViewerInstance | null>(null)
// 导航到指定URL // 导航到指定URL
const navigateToUrl = () => { const navigateToUrl = () => {
// 验证URL格式 // 验证URL格式
...@@ -74,8 +83,13 @@ const navigateToUrl = () => { ...@@ -74,8 +83,13 @@ const navigateToUrl = () => {
return; return;
} }
// 这里可以通过某种方式通知DomSyncViewer组件导航到指定URL // 通过ref调用DomSyncViewer组件的navigateToUrl方法来导航到指定URL
console.log('导航到:', targetUrl.value) if (domSyncViewerRef.value) {
// 直接传递URL给DomSyncViewer组件的navigateToUrl方法
domSyncViewerRef.value.navigateToUrl(targetUrl.value)
} else {
console.error('DomSyncViewer组件引用未找到')
}
} }
// 组件挂载时的处理 // 组件挂载时的处理
......
...@@ -21,6 +21,11 @@ export default defineConfig({ ...@@ -21,6 +21,11 @@ export default defineConfig({
target: 'http://localhost:8080', target: 'http://localhost:8080',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api') rewrite: (path) => path.replace(/^\/api/, '/api')
},
'/ws': {
target: 'http://localhost:8080',
ws: true, // 启用WebSocket代理
changeOrigin: true
} }
} }
}, },
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment