在 1988 年约翰・卡朋特的 cult 电影《They Live》中,主角 Nada 发现一副太阳镜能够揭露广告牌与建筑物上隐藏的信息 —— 那些肉眼不可见却无处不在的控制符号四十年后,这个隐喻被开发者重新诠释:浏览器中同样充斥着肉眼可见却隐匿深处的广告元素与追踪脚本,而这一次我们用代码这副 “墨镜” 去揭示并清除它们。GitHub 上出现了一个名为 they-live 的项目,其核心思路正是利用 MutationObserver API 动态观测页面 DOM 变化,配合 CSS 选择器注入实现无需安装浏览器扩展的内容净化。
MutationObserver 的核心监听机制
MutationObserver 是浏览器原生提供的 DOM 变化监听接口,相比早期已废弃的 MutationEvents,它具有更高的性能与更精细的配置能力。一个典型的监听器需要三步初始化:首先通过 new MutationObserver(callback) 创建观察者实例,其中回调函数接收包含所有变化的 MutationRecord 数组;其次调用 observe(target, options) 将监听器绑定到目标节点并配置监听选项;最后在不再需要时调用 disconnect() 释放资源。项目实现的监听逻辑通常以 document.body 或 document.documentElement 为根节点,配置 childList: true 与 subtree: true 以捕获所有层级的节点增删,这是应对现代单页应用动态渲染广告的关键。
监听回调中需要区分有益变化与无意义变化。常见的优化策略是在回调开头设置防抖计时器,将连续触发的多个变化合并为单次处理,例如使用 200 毫秒的 setTimeout 延迟执行实际清除逻辑。此外,应当维护一个已处理节点集合 Set,通过 WeakSet 或标记属性避免对同一元素重复操作。当检测到新增节点时,首先判断其是否属于已知广告容器类名如 ad-container、sponsored-content、promotion-banner,对于符合模式的节点直接应用隐藏规则。
CSS 选择器注入的批量隐藏策略
CSS 注入是实现批量隐藏的高效手段,相比逐个 DOM 操作,它利用浏览器的样式引擎一次性完成匹配。注入方式有两种主流路径:一是通过创建 <style> 标签并写入 document.head.appendChild(styleEl) 将规则添加到页面;二是使用 CSSStyleSheet.insertRule() 操作已存在的样式表。对于需要动态更新规则列表的场景,前者更易维护 —— 每次广告拦截规则变更时清空并重新写入 <style> 标签内容即可。
规则列表的组织建议按选择器特异性分组。最基础的是标签名与类名组合,例如 [class*="ad-"]、[id^="google_ads"]、[data-ad] 等模式覆盖主流广告联盟的命名规范。更激进的规则可针对包含特定文本内容的元素,如 :containsSponsored 或 :has-text("Advertisement"),这类选择器需要依赖浏览器对 content 文本节点的支持情况。项目代码中通常会维护一个规则数组,包含正则表达式用于动态匹配不断变化的广告容器命名,例如 /(ad-|banner-|promo-)/i 可覆盖 ad-banner、AD-PROMO-Container 等变体。
隐藏规则本身推荐使用 display: none !important 而非 visibility: hidden 或 opacity: 0,原因是后者仍会触发网络请求与脚本执行,而前者能够彻底阻断资源加载。对于需要保留布局占位但视觉消除的场景,可使用 visibility: hidden 配合固定宽高,这在某些响应式布局中可避免页面跳变。
DOM 节点替换与内容净化
纯粹的隐藏无法阻止恶意脚本的执行,某些广告系统会检测元素可见性并触发备选行为。更高强度的净化策略涉及 DOM 替换或节点删除。替换的核心思路是将可疑容器替换为安全的空壳元素,同时保留占位防止布局崩溃。具体实现为先通过 document.createElement('div') 创建净化容器,再使用 element.replaceWith(cleanElement) 完成原子替换,最后对净化容器应用最小化样式以确保不影响页面流。
内容净化的另一个维度是属性剥离。广告节点通常携带事件监听器与数据属性,克隆节点后仅保留标签与文本内容可以有效隔离副作用。克隆操作使用 cloneNode(false) 仅复制自身属性而不递归复制子节点,然后手动转移必要的内容子节点。完成后通过 adElement.replaceWith(safeElement) 插入净化版本,原节点失去引用后将被垃圾回收。
性能与边界情况处理
MutationObserver 在高动态页面中可能产生大量触发,需要设置观察器配置对象的 attributes: false 与 characterData: false 仅监听结构变化以减少回调频率。对于已确认安全的动态区域如聊天窗口、通知面板,可通过 observer.unobserve(element) 取消对这些子树的监听以聚焦真正需要净化的区域。
边界情况包括页面 Frame 的处理。部分广告通过 <iframe> 嵌入第三方内容,跨域限制导致主页面 MutationObserver 无法感知 Frame 内部变化。解决方案是遍历 document.querySelectorAll('iframe'),对同源 Frame 递归应用相同的监听与净化逻辑;对于跨域 Frame 则通过 postMessage 与 Frame 内脚本通信,或在 Content Security Policy 允许时注入脚本。
另一个常见问题是 SPA 路由切换导致的重复监听。页面组件卸载时若未调用 observer.disconnect(),旧的监听器将继续存在并在组件重新挂载后与新的监听器叠加。通过在 React useEffect 清理函数或 Vue onBeforeUnmount 钩子中显式断开监听,可以避免事件处理的指数增长。此外,MutationObserver 实例本身持有的引用会阻止节点被垃圾回收,使用 WeakRef 或在卸载时清空引用数组是必要的内存管理实践。
规则更新与持久化机制
广告发布者的类名与结构策略持续演进,静态规则列表的有效性会随时间衰减。动态更新方案可采用远程规则拉取:在页面加载时从可信源 fetch 最新规则 JSON,包含正则模式与对应 CSS 选择器,缓存在 localStorage 并设置过期时间,下次页面加载时优先使用缓存并在后台静默更新。这种机制既保证了规则的时效性,又避免了每次请求的网络开销。
规则的本地持久化还支持用户自定义白名单与黑名单。白名单通过存储特定域名的例外规则避免误伤关键内容,黑名单则允许高级用户添加页面特有的广告模式。存储格式推荐使用 JSON Schema 校验以防止恶意规则破坏页面,同时提供导入导出功能便于配置迁移。
现代广告系统越来越多地使用 Web Components 与 Shadow DOM 封装广告组件,这些组件的 DOM 结构对主页面 MutationObserver 不可见。需要在检测到可疑元素时,通过 element.shadowRoot 访问 Shadow DOM 并在其中应用相同的监听与净化逻辑。对于使用 closed 模式的 Shadow Root,则无法直接访问,此时只能通过 CSS 隔离或依赖浏览器原生的广告拦截能力。
工程实践与监控指标
生产环境中,DOM 拦截的有效性需要量化监控。建议在净化逻辑执行后向控制台或监控服务上报指标:拦截次数、命中规则类型、处理耗时。对于持续高频触发的规则匹配失败情况(如新出现的广告类名未被覆盖),自动生成告警并触发远程规则更新流程。
可用性测试是不可或缺的环节。项目应维护一个包含主流新闻网站、电商平台、视频站点的测试用例套件,通过自动化脚本模拟页面加载并验证广告元素数量是否降至预期阈值。每次规则更新后触发回归测试,确保新规则不会误伤正常功能。测试框架推荐使用 Playwright 或 Puppeteer 的 page.evaluate() 在页面上下文执行广告统计脚本。
技术实现之外,这一方案的局限性也值得正视:依赖客户端 JavaScript 执行的拦截无法阻止 DNS 层面的广告请求,也无法拦截服务 worker 注册的离线缓存广告。对于追求极致隐私与性能的用户,仍需结合 Hosts 文件修改或网络层代理等多层防御手段。但对于作为浏览器扩展之外的轻量补充、或在特定内网环境中部署内容净化脚本,基于 MutationObserver 与 CSS 注入的方案提供了无需权限申请、开箱即用的技术路径。
资料来源:MDN Web Docs - MutationObserver API 概述了观察者配置选项与回调机制的技术细节,GeeksforGeeks 的教程则展示了如何基于该 API 构建广告检测与响应逻辑的典型实现模式。
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。