Conversation
There was a problem hiding this comment.
Pull request overview
此 PR 修复了 document.write 导致消息监听器被重置的问题(#1049)。当页面调用 document.write 时,整个文档会被替换,导致所有通过 window.addEventListener 注册的事件监听器丢失。
主要变更:
- 新增
DocumentReloadObserver类,监听document.documentElement的变化,在文档重新加载时自动重新注册事件监听器 - 修复了
ExtensionContentMessageSend中的逻辑错误(||改为&&) - 改进了 inject 环境的日志处理,通过 content 环境统一输出
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/message/custom_event_message.ts | 新增 DocumentReloadObserver 类来监听文档重载;修改 CustomEventMessage 构造函数以支持文档重载时重新注册监听器 |
| src/inject.ts | 创建并使用 DocumentReloadObserver;配置日志通过 content 环境输出(consoleLevel: "none") |
| src/content.ts | 创建并共享 DocumentReloadObserver 给所有消息通道;新增日志转发处理器 |
| src/app/service/content/content.ts | 新增 logger 消息处理器,将 inject 环境的日志转发到 content 环境输出 |
| packages/message/server.test.ts | 更新测试代码以使用 DocumentReloadObserver |
| packages/message/extension_message.ts | 修复逻辑错误:将 ` |
| src/app/service/service_worker/value.ts | 添加空行以改进代码格式 |
| export class DocumentReloadObserver { | ||
| callbacks = new Set<() => void>(); | ||
|
|
||
| constructor() { | ||
| let lastDocumentElement = document.documentElement; | ||
| const observer = new MutationObserver(() => { | ||
| if (lastDocumentElement !== document.documentElement) { | ||
| lastDocumentElement = document.documentElement; | ||
| this.callbacks.forEach((cb) => cb()); | ||
| } | ||
| }); | ||
|
|
||
| observer.observe(document, { | ||
| childList: true, // 监听子节点变化 | ||
| }); | ||
| } | ||
|
|
||
| on(callback: () => void): void { | ||
| this.callbacks.add(callback); | ||
| } | ||
|
|
||
| off(callback: () => void): void { | ||
| this.callbacks.delete(callback); | ||
| } | ||
| } |
There was a problem hiding this comment.
新增的 DocumentReloadObserver 类缺少单元测试覆盖。这是一个核心功能,用于解决 document.write 导致的监听器重置问题,应该添加测试来验证:
- 当
document.documentElement改变时,回调是否被正确触发 - 多个回调是否都能被调用
on/off方法是否正常工作
建议在 packages/message/ 目录下创建 custom_event_message.test.ts。
| const observer = new MutationObserver(() => { | ||
| if (lastDocumentElement !== document.documentElement) { | ||
| lastDocumentElement = document.documentElement; | ||
| this.callbacks.forEach((cb) => cb()); |
There was a problem hiding this comment.
回调执行时缺少错误处理。如果某个回调函数抛出异常,可能会阻止其他回调的执行。
建议添加 try-catch 保护:
this.callbacks.forEach((cb) => {
try {
cb();
} catch (e) {
console.error("DocumentReloadObserver callback error:", e);
}
});| this.callbacks.forEach((cb) => cb()); | |
| this.callbacks.forEach((cb) => { | |
| try { | |
| cb(); | |
| } catch (e) { | |
| console.error("DocumentReloadObserver callback error:", e); | |
| } | |
| }); |
| constructor() { | ||
| let lastDocumentElement = document.documentElement; | ||
| const observer = new MutationObserver(() => { | ||
| if (lastDocumentElement !== document.documentElement) { | ||
| lastDocumentElement = document.documentElement; | ||
| this.callbacks.forEach((cb) => cb()); | ||
| } | ||
| }); | ||
|
|
||
| observer.observe(document, { | ||
| childList: true, // 监听子节点变化 | ||
| }); | ||
| } | ||
|
|
There was a problem hiding this comment.
DocumentReloadObserver 的 MutationObserver 没有提供清理机制。当不再需要观察时,observer 应该被断开连接以避免潜在的内存泄漏。
建议添加一个 disconnect() 方法:
export class DocumentReloadObserver {
callbacks = new Set<() => void>();
private observer: MutationObserver;
constructor() {
let lastDocumentElement = document.documentElement;
this.observer = new MutationObserver(() => {
if (lastDocumentElement !== document.documentElement) {
lastDocumentElement = document.documentElement;
this.callbacks.forEach((cb) => cb());
}
});
this.observer.observe(document, {
childList: true,
});
}
disconnect(): void {
this.observer.disconnect();
}
// ... 其他方法
}| constructor() { | |
| let lastDocumentElement = document.documentElement; | |
| const observer = new MutationObserver(() => { | |
| if (lastDocumentElement !== document.documentElement) { | |
| lastDocumentElement = document.documentElement; | |
| this.callbacks.forEach((cb) => cb()); | |
| } | |
| }); | |
| observer.observe(document, { | |
| childList: true, // 监听子节点变化 | |
| }); | |
| } | |
| private observer: MutationObserver; | |
| constructor() { | |
| let lastDocumentElement = document.documentElement; | |
| this.observer = new MutationObserver(() => { | |
| if (lastDocumentElement !== document.documentElement) { | |
| lastDocumentElement = document.documentElement; | |
| this.callbacks.forEach((cb) => cb()); | |
| } | |
| }); | |
| this.observer.observe(document, { | |
| childList: true, // 监听子节点变化 | |
| }); | |
| } | |
| disconnect(): void { | |
| this.observer.disconnect(); | |
| } |
|
还没细看 |
content环境的 window.addEventListener 也会因为 document.write 被清除 |
|
@CodFrm 我发现了 navigation.addEventListener 不会被 document.write 清除
類似的還有 |
确实,他们都可以,performance的兼容性更好,但是不知道有什么差异 |
|
@CodFrm 在我这边用 navigation 代替 window 后,实际用脚本测试,document.write 也没被清除
// ==UserScript==
// @name iframe窗口获取变更后的存储值
// @namespace https://docs.scriptcat.org/
// @version 0.1.0
// @description 顶级窗口改变存储值,iframe窗口会获取不到变更后的值
// @author You
// @grant GM_addValueChangeListener
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_log
// @match *://*/*
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const KEY = "TEST_VALUE_CHANGE";
console.log("script start");
console.log('123');
// iframe 获取改变后的值,正常应该全部次数都弹,实际只会弹3-4次
GM_addValueChangeListener(KEY, (_, oldValue, newValue) => {
console.log('value change', "info", { oldValue, newValue, host: location.host });
});
for (let i = 1; i < 30; i++) {
setTimeout(() => {
console.log("set value");
GM_setValue(KEY, `测试数据 ${Date.now()} i=${i}`)
}, i * 1000)
}
})(); |
同样顺利执行 |
|
你测试看看吧。要不就改用 |
测试确实可以,传递element也没问题 // ==UserScript==
// @name iframe窗口获取变更后的存储值
// @namespace https://docs.scriptcat.org/
// @version 0.1.0
// @description 顶级窗口改变存储值,iframe窗口会获取不到变更后的值
// @author You
// @grant GM_addValueChangeListener
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_log
// @match *://*/*
// @run-at document-end
// @inject-into content
// @grant GM_addElement
// ==/UserScript==
(function () {
'use strict';
const KEY = "TEST_VALUE_CHANGE";
GM_log("script start");
console.log('123');
// iframe 获取改变后的值,正常应该全部次数都弹,实际只会弹3-4次
GM_addValueChangeListener(KEY, (_, oldValue, newValue) => {
GM_log('value change', "info", { oldValue, newValue, host: location.host });
});
for (let i = 1; i < 1000; i++) {
setTimeout(() => {
GM_log("set value");
GM_setValue(KEY, `测试数据 ${Date.now()} i=${i}`)
}, i * 1000)
}
let el = GM_addElement(document.body, "a", {
textContent: "hahahahah"
})
console.log(el)
})(); |
|
我看它做成 EventTarget 就是为了这个 event |
|
你大概改一下我再改吧。 |
| sendMessage<T = any>(data: TMessage): Promise<T> { | ||
| return new Promise((resolve) => { | ||
| if (!this.options?.documentId || this.options?.frameId) { | ||
| if (!this.options?.documentId && !this.options?.frameId) { |
There was a problem hiding this comment.
我在排查这个问题的时候发现,顺手改的,这里有个逻辑错误
There was a problem hiding this comment.
这个也是排查的时候发现的问题,一起改的,可以一起,没问题
Co-Authored-By: 王一之 <yz@ggnb.top>







概述 Descriptions
fix #1049
变更内容 Changes
截图 Screenshots