-
Notifications
You must be signed in to change notification settings - Fork 310
重构 #1067 #1162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop/messaging-performance-boost2
Are you sure you want to change the base?
重构 #1067 #1162
Conversation
是重構的方式有問題嗎 |
src/content.ts
Outdated
| const EventFlag = randomMessageFlag(); | ||
|
|
||
| // 判断当前是否运行在 USER_SCRIPT 环境 (content环境) | ||
| const isContent = typeof chrome.runtime?.sendMessage === "function"; | ||
| const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject; | ||
| negotiateEventFlag(MessageFlag, EventFlag); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
怎麼這個跟 inject.ts 不一樣了?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
由content生成随机flag,scripting和inject获取
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
這樣不對
不是 1067 的設計
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1067
scripting 提供 EventFlag
inject.ts 和 content.ts 接收
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
因为我还是考虑chrome不要scripting,另外由谁提供EventFlag这个无所谓的
我本地跑 pnpm test 也跑不過 |
有坏掉的,但是有的是因为超时时间过短,又并发,导致处理不过来,加超时时间可以解决,坏掉的我明天看 |
我觉得不是超时问题。就是没成功呼叫 |
|
this.pageMessaging.onReady!(() => { 这个是需要的 考虑到 early-start 什么的会比握手更早呼叫 GM API 因为 GM API 都要跟 service_worker 溝通 |
packages/message/common.ts
Outdated
| export function getEventFlag(messageFlag: string): string { | ||
| let eventFlag = ""; | ||
| const EventFlagListener: EventListener = (ev) => { | ||
| if (!(ev instanceof CustomEvent)) return; | ||
| if (ev.detail?.action != "broadcastEventFlag") return; | ||
| eventFlag = ev.detail.EventFlag; | ||
| pageRemoveEventListener(messageFlag, EventFlagListener); | ||
| // 告知对方已收到 EventFlag | ||
| pageDispatchCustomEvent(messageFlag, { action: "receivedEventFlag" }); | ||
| }; | ||
|
|
||
| pageAddEventListener(messageFlag, EventFlagListener); | ||
|
|
||
| // 基于同步机制,判断是否已经收到 EventFlag | ||
| // 如果没有收到,则主动请求一次 | ||
| if (!eventFlag) { | ||
| pageDispatchCustomEvent(messageFlag, { action: "requestEventFlag" }); | ||
| } | ||
|
|
||
| return eventFlag; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@CodFrm
getEventFlag 好像是取得 inject <-> content 的 flag? 这不是1067的设计原意
1067 的设计是 inject 和 content 都是同级的
scripting才是上级
所以 inject 和 content 所有代码,除了 window.external 注入
都是一致的
const EventFlag = randomMessageFlag();
negotiateEventFlag(MessageFlag, EventFlag);不会在 content.js 搞
另外,根据执行次序,不一定马上会有 EventFlag
所以 1067 的 CustomEventMessage 里的 flag 一开始就不是固定
会有 onReady 等行为
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
确实目前 inject 和 content 除了 window.external 就没什么其它区别了,但是考虑到chrome并不需要scripting,以content为主也方便后续调整。对于协商flag这一块谁为主次无所谓的
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
這樣搞兩個版本會好混亂
直接統一成scripting不好嗎
不然日後bug report什麼的又會分兩種處理
我是想整個處理變得通用
不需要考慮chrome或firefox
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
你说得也有道理,两套不同的架构也加大维护成本
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
关于引入 scripting 和 使用 chrome.storage 作为广播 都挺纠结的,前者是因为Firefox妥协,后者是 方法的职责和性能问题,valueUpdate并不完全是广播
src/scripting.ts
Outdated
| // scripting <-> inject/content 的双向消息桥 | ||
| const scriptExecutorMsgTxIT = new CustomEventMessage(scriptExecutorMsgIT, true); // 双向:scripting <-> inject | ||
| const scriptExecutorMsgTxCT = new CustomEventMessage(scriptExecutorMsgCT, true); // 双向:scripting <-> content | ||
| const EventFlag = getEventFlag(MessageFlag); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1067
scripting 提供 EventFlag
inject.ts 和 content.ts 接收
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
由谁提供无所谓的,只要保持3个环境一致就行了
|
scripting.js 是主 假如完全没有 但整个概念要搞清楚主和客 (以往 content.js 做的都搬到 scripting.js 做) 另一个改变是 early-start 什么的是直接跑代码
这个本身就是异步,不影响 如果是同步的话,基于 inject/content 没有另一个脚本的执行,要考虑报错的可能性 好吧剛發現Brave的次序是 content->inject->scripting 那就做三方會談的EventFlag吧 |
|
三方会谈版 EventFlag // 避免页面载入后改动全域物件导致消息传递失败
const MouseEventClone = MouseEvent;
const CustomEventClone = CustomEvent;
const performanceClone = performance;
// 避免页面载入后改动 EventTarget.prototype 的方法导致消息传递失败
const pageDispatchEvent = performanceClone.dispatchEvent.bind(performanceClone);
const pageAddEventListener = performanceClone.addEventListener.bind(performanceClone);
const pageRemoveEventListener = performanceClone.removeEventListener.bind(performanceClone);
const detailClone = typeof cloneInto === "function" ? cloneInto : null;
const pageDispatchCustomEvent = (eventType, detail) => {
if (detailClone && detail) {
detail = detailClone(detail, performanceClone);
}
const ev = new CustomEventClone(eventType, {
detail,
cancelable: true,
});
return pageDispatchEvent(ev);
};
// flag 协商
function negotiateEventFlag(messageFlag, firstEventFlag, responsedCountMax = 3) {
const tag = `${messageFlag}_negotiate`;
let eventFlag = "";
let responsedCount = 0;
const EventFlagRequestHandler = (ev) => {
if (!(ev instanceof CustomEvent)) return;
switch (ev.detail && ev.detail.action) {
case "responseEventFlag":
if (ev.defaultPrevented) return;
if (eventFlag || !ev.detail || !ev.detail.EventFlag) return;
eventFlag = ev.detail.EventFlag;
if (eventFlag !== firstEventFlag) {
pageRemoveEventListener(tag, EventFlagRequestHandler);
}
break;
case "requestEventFlag":
if (ev.defaultPrevented) return;
responsedCount++;
if (responsedCount <= responsedCountMax) {
pageDispatchCustomEvent(tag, {
action: "responseEventFlag",
EventFlag: firstEventFlag,
});
ev.preventDefault();
ev.stopImmediatePropagation();
ev.stopPropagation();
} else {
pageRemoveEventListener(tag, EventFlagRequestHandler);
}
break;
}
};
pageAddEventListener(tag, EventFlagRequestHandler);
pageDispatchCustomEvent(tag, { action: "requestEventFlag" });
if (!eventFlag) {
console.error("negotiateEventFlag failed");
}
return eventFlag;
}
const mf = "amjmajsj1";
console.log(negotiateEventFlag(mf, "a123", 3)); // a123
console.log(negotiateEventFlag(mf, "a456", 3)); // a123
console.log(negotiateEventFlag(mf, "a689", 3)); // a123
console.log(negotiateEventFlag(mf, "a477", 3)); // a477
console.log(negotiateEventFlag(mf, "a488", 3)); // a477
console.log(negotiateEventFlag(mf, "a499", 3)); // a477
console.log(negotiateEventFlag(mf, "a511", 3)); // a511
console.log(negotiateEventFlag(mf, "a522", 3)); // a511
console.log(negotiateEventFlag(mf, "a533", 3)); // a511
|
并不需要,early-start的检测执行,在协商完成之后了,flag协商都没完成,是不会跑脚本的 |
|
啊,是我搞错了,我的代码有问题,我再看看,我以为我的 negotiateEventFlag/getEventFlag 和 scripting/content/inject 顺序无关的,确实需要个 onReady 如果 scripting, inject, content 顺序发生变化的话,会有你说的问题 我再看看 |
|
我又改动了一下,negotiateEventFlag 放在了scripting,不过现在放在任何环境/顺序都无所谓了 (debug信息故意保留,确定后再删除) |
你用我這個不就好了嗎?? 這個不用理誰先誰後 |
没有 onReady 的逻辑呀,会出现你说的问题,而且不好理解 |
|
关于单元测试,我把 --isolate=false 选项删除后,就可以通过了,看起来是有什么修改影响了。。。。 |
還是過不了呀 |
唉呀... |
src/content.ts
Outdated
| import { ScriptRuntime } from "./app/service/content/script_runtime"; | ||
| import { ScriptEnvTag } from "@Packages/message/consts"; | ||
|
|
||
| const MessageFlag = process.env.SC_RANDOM_KEY || "scriptcat-default-flag"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
這個fallback是多餘的
|
我只能說 1067 是花了一整個月想出來的東西 包括 early-start 不受握手程序的限制 老實說,一次性的代碼,也要做一個Class去 new 來 new 去,不建議 現代的JavaScript根本不是玩OOP 你看回我1067的代碼,可以很容易理解哪個部份是同步,哪個部份是異步 scriptingimport LoggerCore from "./app/logger/core";
import MessageWriter from "./app/logger/message_writer";
import { CustomEventMessage, createPageMessaging } from "@Packages/message/custom_event_message";
import { pageAddEventListener, pageDispatchCustomEvent } from "@Packages/message/common";
import { ScriptEnvTag, ScriptEnvType } from "@Packages/message/consts";
import { uuidv5 } from "./pkg/utils/uuid";
import { randomMessageFlag, makeBlobURL } from "@App/pkg/utils/utils";
import { ExtensionMessage } from "@Packages/message/extension_message";
import type { Message, MessageSend } from "@Packages/message/types";
import { Server, forwardMessage } from "@Packages/message/server";
import { RuntimeClient } from "@App/app/service/service_worker/client";
import type { Logger } from "@App/app/repo/logger";
import { MessageDelivery } from "./message-delivery";
const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY);
const uuids = new Map<string, ScriptEnvType>();
const senderToExt: Message = new ExtensionMessage(false);
const scriptExecutorMsgIT = createPageMessaging("");
const scriptExecutorMsgCT = createPageMessaging("");
const scriptExecutorMsgTxIT = new CustomEventMessage(scriptExecutorMsgIT, true); // 双向:scripting <-> inject
const scriptExecutorMsgTxCT = new CustomEventMessage(scriptExecutorMsgCT, true); // 双向:scripting <-> content
const loggerCore = new LoggerCore({
writer: new MessageWriter(senderToExt, "serviceWorker/logger"),
labels: { env: "scripting" },
});
const scriptingMessaging = createPageMessaging(""); // 对 inject / content 的 client 发出消息
const messageDeliveryToPage = new MessageDelivery();
const client = new RuntimeClient(senderToExt);
loggerCore.logger().debug("scripting start");
const requireScriptingToken = (): string => {
// ...
};
const setupDeliveryChannel = () => {
// ...
};
// ================================
// Server 构建与 service_worker 转发
// ================================
type GmApiPayload = { api: string; params: any; uuid: string };
const handleRuntimeGmApi = (
senderToInject: CustomEventMessage,
senderToContent: CustomEventMessage,
data: GmApiPayload
) => {
// ...
};
const prepareServer = (
server: Server,
senderToExt: MessageSend,
senderToInject: CustomEventMessage,
senderToContent: CustomEventMessage
) => {
// ...
};
// ================================
// 握手:MessageFlag 与 injectFlagEvt 协商
// ================================
const onMessageFlagReceived = (MessageFlag: string) => {
const executorEnvReadyKey = uuidv5("scriptcat-executor-ready", MessageFlag);
// 由 scripting 随机生成,用于 scripting <-> inject/content 的消息通道 token
const injectFlagEvt = randomMessageFlag();
// readyFlag 位运算:inject=1,content=2,凑齐 3 表示都 ready. ready 后设为 4 避免再触发
let readyFlag = 0;
const finalizeWhenReady = () => {
// ...
};
// 接收 inject/content 的 ready 回执
pageAddEventListener(`${injectFlagEvt}`, (ev) => {
// ...
});
// 向 inject/content 广播 injectFlagEvt(让它们知道后续用哪个 token 通信)
const submitTarget = () => {
// ...
};
// 处理“scripting 早于 content/inject 执行”的场景:
// content/inject 会先发一个 executorEnvReadyKey(detail 为空)来探测 scripting 是否在
pageAddEventListener(executorEnvReadyKey, (ev) => {
// ...
});
// 处理“scripting 晚于 content/inject 执行”的场景:
// scripting 启动后主动广播一次 executorEnvReadyKey,content/inject 立刻能收到 injectFlagEvt
submitTarget();
};
chrome.storage.local.onChanged.addListener((changes) => {
// ...
});
chrome.runtime.onMessage.addListener((message, _sender) => {
// ...
});
// ================================
// 启动流程
// ================================
onMessageFlagReceived(MessageFlag);
client.pageLoad().then((o) => {
// ...
});
inject.jsimport LoggerCore from "./app/logger/core";
import MessageWriter from "./app/logger/message_writer";
import { CustomEventMessage, createPageMessaging } from "@Packages/message/custom_event_message";
import { pageAddEventListener, pageDispatchCustomEvent, pageDispatchEvent } from "@Packages/message/common";
import { ScriptEnvTag } from "@Packages/message/consts";
import { uuidv5 } from "./pkg/utils/uuid";
import { initEnvInfo, ScriptExecutor } from "./app/service/content/script_executor";
import type { ValueUpdateDataEncoded } from "./app/service/content/types";
import type { TClientPageLoadInfo } from "./app/repo/scripts";
import type { Message } from "@Packages/message/types";
import { sendMessage } from "@Packages/message/client";
import { ExternalWhitelist } from "@App/app/const";
const MessageFlag = uuidv5(`${performance.timeOrigin}`, process.env.SC_RANDOM_KEY);
// ================================
// 常量与全局状态
// ================================
const isContent = typeof chrome.runtime?.sendMessage === "function";
const scriptEnvTag = isContent ? ScriptEnvTag.content : ScriptEnvTag.inject;
const executorEnvReadyKey = uuidv5("scriptcat-executor-ready", MessageFlag);
const scriptingMessaging = createPageMessaging(""); // injectFlagEvt
const pageMessaging = createPageMessaging(""); // `${injectFlagEvt}_${scriptEnvTag}`
const msg = new CustomEventMessage(pageMessaging, false);
const logger = new LoggerCore({
writer: new MessageWriter(msg, "scripting/logger"),
consoleLevel: "none",
labels: { env: "inject", href: window.location.href },
});
const scriptExecutor = new ScriptExecutor(msg);
let bindScriptingDeliveryOnce: (() => void) | null = null;
const requireScriptingToken = (): string => {
// ...
};
const resetMessagingTokens = () => {
// ...
};
const setMessagingTokens = (injectFlagEvt: string) => {
// ...
};
const acknowledgeScriptingReady = (injectFlagEvt: string) => {
// ...
};
// ================================
// 对外接口:external 注入
// ================================
// 判断当前 hostname 是否命中白名单(含子域名)
function isExternalWhitelisted(hostname: string) {
// ...
}
// 生成暴露给页面的 Scriptcat 外部接口
function createScriptcatExpose(pageMsg: Message) {
// ...
}
// 尝试写入 external,失败则忽略
function safeSetExternal<T extends object>(external: any, key: string, value: T) {
// ...
}
// 当 TM 与 SC 同时存在时的兼容处理:TM 未安装脚本时回退查询 SC
function patchTampermonkeyIsInstalled(external: any, scriptExpose: App.ExternalScriptCat) {
// ...
}
// inject 环境 pageLoad 后执行:按白名单对页面注入 external 接口
function onInjectPageLoaded(pageMsg: Message) {
// ...
}
// ================================
// 消息分发处理
// ================================
// 处理 scripting -> inject 的消息
const handleDeliveryMessage = (tag: string, value: any) => {
// ...
};
// ================================
// 页面通信绑定与握手
// ================================
// 监听 scripting 发来的 delivery 消息
const bindScriptingDeliveryChannel = () => {
// ...
};
// 建立 scripting <-> inject 的握手流程
const setupHandshake = () => {
// 准备一次性绑定函数
bindScriptingDeliveryOnce = () => {
// ...
};
// 等待 scripting 注入完成并发送 injectFlagEvt (仅调用一次)
pageAddEventListener(executorEnvReadyKey, (ev) => {
// ...
});
};
// ================================
// 启动流程
// ================================
// 检查 early-start 脚本
scriptExecutor.checkEarlyStartScript(scriptEnvTag, initEnvInfo);
// 建立握手与通信绑定
setupHandshake();
// 主动触发 ready 事件,请求 scripting 建立连接
pageDispatchEvent(new CustomEvent(executorEnvReadyKey));
|
单元测试的问题解决了
关于 这个 1162 的PR,令 early-start 不能直接跑的 "onReady", 你解决一下吧 還有 server.on 不能放在異步裡 我還是推以下寫法 /**
* 在同一个页面中,通过自定义事件「协商」出一个唯一可用的 EventFlag
*
* 设计目的:
* - 页面中可能同时存在多个实例
* - 需要确保最终只有一个 EventFlag 被选中并使用
*
* 协商思路(基于同步事件机制):
* 1. 先广播一次【不带 EventFlag 的询问事件】
* 2. 所有实例都会收到该事件,并根据收到的内容做判断:
* - 如果收到【已带 EventFlag 的事件】
* → 说明已有实例成功声明旗标,直接采用该值
* → 如果不是自己期望的旗标,立刻退出协商
* - 如果收到【不带 EventFlag 的事件】
* → 视为一次“空回应”
* → 在可接受次数内,主动声明自己的 preferredFlag
* 3. 若空回应次数超过上限仍未成功,则放弃协商
*
* 注意事项:
* - dispatchEvent 是同步执行的
* - 实例也会收到自己发出的事件
* - 只有一个实例时,通常立即采用 preferredFlag
* - 多实例并存时,先成功拦截并声明的实例胜出
*/
export function negotiateEventFlag(channelKey: string, preferredFlag: string, maxEmptyResponses: number = 3) {
/** 协商所使用的事件名称 */
const eventName = `${channelKey}_negotiate`;
/** 最终确认并采用的 EventFlag */
let finalFlag = "";
/** 已收到的“空事件”次数(不带 EventFlag) */
let emptyEventCount = 0;
/**
* 处理协商事件的核心监听函数
*/
const onNegotiationEvent: EventListener = (event) => {
if (!(event instanceof CustomEvent)) return;
if (event.defaultPrevented) return;
const receivedFlag = event.detail?.EventFlag;
// ───────────── 情况一:收到已声明 EventFlag 的事件 ─────────────
if (receivedFlag) {
// 只在尚未确定最终结果时处理
if (!finalFlag) {
finalFlag = receivedFlag;
// 若旗标不是自己期望的,说明其他实例已胜出
if (receivedFlag !== preferredFlag) {
pageRemoveEventListener(eventName, onNegotiationEvent);
}
}
return;
}
// ───────────── 情况二:收到不带 EventFlag 的空事件 ─────────────
emptyEventCount++;
if (emptyEventCount <= maxEmptyResponses) {
// 在允许范围内,主动声明自己的旗标
pageDispatchCustomEvent(eventName, {
EventFlag: preferredFlag,
});
// 阻止事件继续传播,避免被其他实例抢先处理
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation();
} else {
// 超过最大尝试次数,放弃协商
pageRemoveEventListener(eventName, onNegotiationEvent);
}
};
// 开始监听协商事件
pageAddEventListener(eventName, onNegotiationEvent);
// 发送第一次询问事件(不带 EventFlag)
pageDispatchCustomEvent(eventName, {});
if (!finalFlag) {
throw new Error("negotiateEventFlag: 未能成功协商出 EventFlag");
}
return finalFlag;
} |
概述 Descriptions
@cyfung1031 更简洁的flag协商方式,尽量不变动原来的结构,删除多余逻辑
你看看大概有没有什么问题,没有就先合并到你的分支上,再做调整
感觉这个 it.concurrent,机器配置不够的时候,总是会超时,并发也容易出问题,后续还是用普通的好了
变更内容 Changes
截图 Screenshots