[v1.3] 配合 1.3 scripting, 重构 `GM_addElement` (bug 修补 + 功能改进) by cyfung1031 · Pull Request #1233 · scriptscat/scriptcat
🔍 PR #1233 代码审查:重构 GM_addElement
📋 概述
此 PR 对 GM_addElement API 进行了重大重构,主要目的是:
- 修复 CSP/TTP 限制下无法插入元素的 bug — 统一由 content.js 处理元素插入
- 新增功能 — 支持
native: true原生创建元素、insertBefore式的refNode参数、更多属性如innerHTML/outerHTML/innerText/className/value - 移除旧逻辑 — 删除
scripting.ts中GM_addElement的 case 分支,不再走 background 消息中转
✅ 优点
- 架构改进明显:旧实现通过
syncSendMessage经 scripting 中转,新实现直接在 inject/content 端利用dispatchMyEvent同步事件通讯完成元素创建和插入,更加直接高效 - 更好的 CSP/TTP 兼容性:文字/数字类属性在 content.js 设置以避开 TrustedTypes 限制
- 功能扩展合理:
native: true、refNode、更多属性支持等都是实际需求驱动 - 注释详尽:代码中有充分的中文注释解释设计意图
- 测试脚本完备:新增了
example/tests/gm_add_element.js覆盖多种使用场景
⚠️ 问题和建议
1. 🔴 GM_addStyle 仍使用旧的 syncSendMessage 路径,而 scripting.ts 已删除 GM_addElement 的 case
GM_addStyle 仍使用旧的 syncSendMessage 路径,而 scripting.ts 已删除 GM_addElement 的 caseGM_addStyle(gm_api.ts 第 724–748 行)仍然通过 syncSendMessage 调用 scripting.ts 中的 GM_addElement case,但该 case 已在此 PR 中被删除。这将导致 GM_addStyle 完全失效!
// gm_api.ts — GM_addStyle 仍旧使用已删除的路径 const resp = (<CustomEventMessage>this.message).syncSendMessage({ action: `${this.prefix}/runtime/gmApi`, data: { uuid: this.scriptRes.uuid, api: "GM_addElement", // ← scripting.ts 中此 case 已被删除 params: [null, "style", { textContent: css }, isContent], }, });
建议:将 GM_addStyle 也重构为使用新的 GM_addElement 路径,例如直接调用 this.GM_addElement('style', { textContent: css })。
2. 🔴 随机 ID 碰撞风险
let randInt = Math.floor(Math.random() * 1147483647 + 1000000000); randInt -= randInt % 100; // 用此方法可以生成不重复的 id
注释说"可以生成不重复的 id",但 Math.random() 并不保证唯一性。如果两个脚本或同一脚本快速连续调用 GM_addElement,理论上可能产生相同的 randInt,导致 relatedTargetMap 中的数据被覆盖。
建议:使用递增计数器(类似现有的 relateId)或在现有 relateId 基础上扩展,确保 ID 唯一性。例如:
// 在类或模块级别维护一个递增计数器 const baseId = (relateId = relateId + 100); // 每次递增 100,留出 id0~id3 的空间
3. 🟡 eventFlag 的反推方式十分脆弱
const eventFlag = (this.message as CustomEventMessage).receiveFlag .split(`${DefinedFlags.outboundFlag}${DefinedFlags.domEvent}`)[0] .slice(0, -2);
通过字符串分割和截取来反推 eventFlag 依赖于 flag 的内部格式,如果 DefinedFlags 的格式发生变化,此处会无声地出错。PR 描述中也提到"目前未有直接取得 eventFlag 的方法"。
建议:在 CustomEventMessage 类中暴露一个 getter 方法(如 getEventFlag()),或将 eventFlag 作为构造参数保存,避免外部依赖内部格式。
4. 🟡 Native.ownFragment 作为单例的线程安全问题
// global.ts ownFragment: new DocumentFragment(),
Native.ownFragment 是一个全局共享的 DocumentFragment。如果在极端情况下(如事件监听器中触发了另一次 GM_addElement 调用),frag.lastChild 可能返回错误的元素。
建议:每次调用时创建新的 DocumentFragment,或者在使用后立即清空:
const frag = new DocumentFragment(); // 每次新建
5. 🟡 dispatchMyEvent 中 cancelable 检查可能导致误报错误
if (resFalse !== false && eventInitDict.cancelable === true) { throw new Error("Page Message Error"); }
如果 content 端的事件监听器尚未准备好(例如时序问题),preventDefault() 不会被调用,会导致抛出异常。注释说"通讯设置正确的话应不会发生",但在实际环境中初始化时序可能出问题。
建议:考虑添加 fallback 或更友好的错误提示,而不是直接 throw。或者增加重试机制。
6. 🟡 custom_event_message.ts 中 appendOrInsert 处理缺少错误检查
const el = <Element>this.getAndDelRelatedTarget(id1); const parent = <Node>this.getAndDelRelatedTarget(id2);
如果 getAndDelRelatedTarget 返回 undefined(例如 ID 碰撞导致数据被提前消费),后续的 el.setAttribute 和 parent.appendChild 会抛出不明确的运行时错误。
建议:添加空值检查并提供有意义的错误信息:
if (!el || !parent) throw new Error("GM_addElement: relatedTarget not found");
7. 🟡 innerHTML/outerHTML 的安全风险
在 content 环境中直接设置 innerHTML/outerHTML 可能绕过页面的 CSP 或 TrustedTypes 策略(这正是此 PR 的目的之一),但也意味着用户脚本可以注入任意 HTML。虽然用户脚本本身就有高权限,但仍需注意这可能被恶意脚本利用。
建议:在文档中明确说明此行为的安全影响,确保用户理解权限范围。
8. 🟢 小问题:dispatchMyEvent 函数命名
dispatchMyEvent 名称不够描述性,不太符合项目中其他函数的命名风格(如 pageDispatchEvent、pageDispatchCustomEvent)。
建议:考虑更具描述性的名称,如 dispatchBridgeEvent 或 dispatchRelatedTargetEvent。
9. 🟢 类型标注可以更精确
attrs: Record<string, string | number | boolean> | Node | null = {}
attrs 参数的类型联合了 Record<...> | Node | null,使得函数签名较为混乱。这是因为 GM_addElement 支持两种调用模式(有/无 parentNode),建议使用函数重载让类型更清晰。
📊 总结
| 维度 | 评价 |
|---|---|
| 代码正确性 | ⚠️ GM_addStyle 路径断裂是严重 bug |
| 架构设计 | ✅ 方向正确,但 eventFlag 反推和 ID 生成需改进 |
| 安全性 | 🟡 innerHTML/outerHTML 需注意,但在用户脚本场景下可接受 |
| 测试覆盖 | ✅ 有手动测试脚本,但缺少单元测试 |
| 代码风格 | 🟡 函数命名可改进,PR 作者自己也提到风格可能需重构 |
总体评价:功能设计思路好,解决了实际问题(CSP/TTP 绕过),但 GM_addStyle 的回归 bug 必须在合并前修复,且 ID 唯一性和 eventFlag 反推方式需要加固。建议修复关键问题后再合并。
🤖 Generated with Claude Code