☰
Sona源码解析二:InjectorManager如何实现Ember刷新不掉的DOM自愈注入
2026/10/17 15:19:13 网站建设 项目流程

【免费下载链接】sona

基于Pengu loader的英雄联盟客户端增强插件;LOL助手;sona助手;LOL插件;A LOL plugin base on Pengu loader; sona plugin;

项目地址:https://gitcode.com/gh_mirrors/sona4/sona
点击查看免费下载

Sona 是一个基于 Pengu Loader 的英雄联盟客户端增强插件,它为客户端注入入口按钮、自定义旗帜、英雄 T 级展示等大量自定义 UI。但客户端底层是 Ember.js 框架,一次页面刷新就可能把注入的元素整个"洗掉"。本文深入解析 Sona 的 InjectorManager——用一个永不掉线的全局 MutationObserver + 帧级节流,实现"被刷掉就自动补回"的 DOM 自愈注入机制。

为什么需要"自愈":Ember 会随时洗掉你的 DOM

把自定义元素插入客户端 DOM 后,下面这些情况都会让它消失:

  • 玩家从主页切到"召唤师峡谷"等页面再切回,Ember 重建了整个路由视图;
  • 客户端热更新、数据刷新导致容器节点被替换;
  • 某些页面卸载时,连带子树一起被移除。

传统做法是每个功能自己写一个轮询setTimeout反复检查,结果是:注入点一多,几十个轮询互相打架,性能差、时序乱。Sona 的解法是把所有注入点收敛到一个管理器、一个 Observer里统一调度。

核心设计:一个 Observer 守护所有注入任务

InjectorManager 的全部状态只有三个:

成员作用
tasks: Set<InjectTask>所有注册进来的注入任务集合
observer: MutationObserver唯一的全局 DOM 观察者
isThrottled: boolean帧级节流标志位

注入任务:一个"尽力而为"的布尔函数

任务类型只有一行契约(InjectorManager.ts#L16):

type InjectTask = () => boolean
  • 返回true:注入成功,或元素已存在(本次无事可做);
  • 返回false:锚点元素还没出现,等下一次 DOM 变动再试。

这个"幂等 + 无副作用失败"的契约非常关键:它让任务可以被同一帧内重复执行、任意次重复执行而不会重复插元素。例如入口按钮任务 tryInjectSonaButton 每次执行先查#sona-entry-btn是否还在,不在才找 Play 按钮左侧重新插入。

永不断线的 MutationObserver

InjectorManager.start() 启动全局守护时做了一个刻意的"反常"决定:从不调用disconnect()。它监听整个document.body的子树变化:

this.observer.observe(document.body, { childList: true, subtree: true, })

于是自愈链路自然成立:Ember 删掉了注入元素 → DOM 变动触发 Observer 回调 → 重跑所有任务 → 元素被补回。整个闭环不依赖任何轮询,且因为只有一个 Observer,开销是常数级的。

requestAnimationFrame 节流:一帧最多检查一次

Ember 一次刷新可能触发成百上千个 mutation 事件。如果每次都重跑任务,主线程会被塞爆。start() 里的节流逻辑是:

if (this.isThrottled) return this.isThrottled = true requestAnimationFrame(() => { for (const task of this.tasks) { /* 重跑全部任务 */ } this.isThrottled = false })

含义是:一帧之内无论发生多少次 DOM 变动,任务只统一执行一次。既保证了"下一帧一定恢复"的自愈速度,又不会因为 Ember 的高频渲染而卡顿。每个任务还包在try/catch里,单个任务抛错不会影响其他任务。

实战走读:入口按钮如何"复活"

以 injections.ts 中的入口按钮为例,完整自愈过程只有四步:

  1. 启动时注册:registerAllInjections() 调用injector.register(tryInjectSonaButton),注册后立即执行一次;
  2. 首次注入:找到.play-button-container的父节点,把按钮insertBefore到 Play 按钮左侧;
  3. Ember 刷新:玩家切换页面,按钮连同父节点一起被移除,DOM 变动触发 Observer;
  4. 自动补回:下一帧任务重跑,按钮不存在 → 重新插入,全程无需人工干预。

同样的模式覆盖了插件里几乎所有注入点:自定义旗帜按钮 插到身份定制页标题旁、英雄选择 T 级徽章 插到选英雄界面、在线状态菜单接管 用data-sona-hijacked属性标记已接管的节点防止重复绑定。

连 Sona 自己的 React 主界面也受它保护:tryGuardContainer 检查#sona-root容器是否还连着 DOM,脱离就自动重新挂载——宿主界面刷新后,面板"失而复得"。

功能开关如何与注入器协作

任务不是永远注册的,而是跟随功能开关动态增删,这对应 InjectorManager.register / unregister 两个 API:

  • 开启"自定义旗帜"时:updateCustomBanner() 调用injector.register(tryInjectCustomBannerButton),并顺手执行一次立即生效;
  • 关闭时:injector.unregister(...)从集合中移除,任务不再被重跑,同时cleanupCustomBanner()移除已注入的元素和补丁。

也就是说,开关关闭 = 任务从自愈集合中摘除,Observer 会继续工作,但不再为这个功能浪费检查。选英雄 T 级展示的 registerTierInjection / unregisterTierInjection 也是同样的套路,进选英雄界面注册、退场时清理。

与 Ember Hook 的分工:DOM 层 vs 组件层

Sona 的注入体系其实是双层结构:

机制文件作用层级解决什么
InjectorManagersrc/lib/InjectorManager.tsDOM 层注入的元素被刷新掉后自动补回
Ember Hooksrc/lib/ember-hook.ts组件类层通过 RCPpostInit劫持Ember.Component.extend,在组件"类"创建时注入 Mixin 或劫持方法

Ember Hook 管的是"组件长什么样",InjectorManager 管的是"页面刷新后元素还在不在"。两者互补,前者负责在正确的时机改造组件,后者负责在混乱的 DOM 生命周期里兜底。

小结

InjectorManager 用一个不到百行的单例文件,回答了客户端插件最常见的难题:

  • 永不掉线:Observer 从不断开,任何注入点被刷掉后,下一次 DOM 变动自动补回;
  • 绝对流畅:全局唯一 Observer +requestAnimationFrame节流,一帧最多检查一次,Ember 高频渲染也不会卡顿;
  • 易于扩展:新增注入点只需写一个幂等的tryInjectXxx(): boolean,然后injector.register(task)一行搞定(见 injections.ts 顶部注释的开发指引)。

对想给自己的 Pengu Loader 插件注入 UI 的开发者来说,这套"单一 Observer + 幂等任务 + 帧级节流"的模板,几乎是照搬就能用的最佳实践。

【免费下载链接】sona

基于Pengu loader的英雄联盟客户端增强插件;LOL助手;sona助手;LOL插件;A LOL plugin base on Pengu loader; sona plugin;

项目地址:https://gitcode.com/gh_mirrors/sona4/sona
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询