Nvirya AdGuard 工作原理

理解 Nvirya AdGuard 能做什么、不能做什么,首先要明确用户脚本(Userscript)在浏览器架构中所处的位置,以及它与浏览器扩展和 VPN/DNS 过滤器的本质区别。

过滤技术对比#

不同的内容过滤工具工作在设备与浏览器的不同层级:

技术类型典型能力重要限制
用户脚本 (Nvirya AdGuard)运行页面级 JavaScript(@run-at document-start)、注入修饰性 CSS、监听并修改页面 DOM,并在受支持环境中包装 JavaScript API(fetch、XMLHttpRequest、sendBeacon、window.open)依赖脚本注入时机、用户脚本管理器能力、浏览器 API、内容安全策略 (CSP) 以及 WebKit 沙盒隔离;无法在 JavaScript 之外拦截浏览器底层网络流量
浏览器扩展 (Browser extension)可调用特权浏览器 API(如 declarativeNetRequest 或 webRequest),在网络请求进入页面上下文之前将其取消或重定向,同时支持内容脚本与样式注入依赖浏览器厂商支持(许多移动端浏览器不支持或受限)、扩展商店政策、Manifest 版本限制及授予的主机权限
VPN 或 DNS 过滤 (Shadowrocket、Surge、Loon、AdGuard DNS)在连接到达浏览器之前,在操作系统或网络接口层面对整个设备的域名解析或网络数据包进行拦截与重定向无法检查或修改页面 DOM、无法折叠空白广告占位框、无法解除页面滚动锁定、无法点击视频跳过按钮,也无法区分与主内容同域名分发的广告

作为用户脚本,Nvirya AdGuard 擅长页内 DOM 清理、交互式元素隐藏和 JavaScript 导航防护,但在处理底层网络请求时受限于浏览器的安全边界。

三阶段启动架构#

为了降低页面加载开销,Nvirya AdGuard v11.7.0 将初始化流程(init())划分为三个顺序阶段:

  1. 阶段 1 — 核心即时防护(document-start):
    • 在当前站点未被加入白名单或暂停时,注入主世界防护脚本(injectMainWorldShield)。
    • 为 fetch、XMLHttpRequest、navigator.sendBeacon、window.open、document.write、HTMLScriptElement.prototype.src 与 HTMLImageElement.prototype.src 安装 JavaScript API 包装钩子。
    • 安装捕获阶段(capture-phase)事件守卫,拦截隐形点击陷阱、未授权外部跳转、剪贴板写入及标签页可见性伪造。
    • 通过 history.replaceState(cleanTrackingQuery)移除地址栏中的已知跟踪参数(如 utm_*、fbclid、gclid、msclkid,YouTube 域名除外)。
    • 一旦 document.documentElement 或 document.head 可用,立即注入修饰性 CSS(installCosmeticCSS)与已保存的站点规则(installSiteRulesCSS)。
  2. 阶段 2 — 延迟 DOM 处理(DOMContentLoaded 或 document.body 就绪):
    • 在顶层窗口(window.self === window.top)的开放式 Shadow DOM 容器中挂载快捷悬浮按钮(FAB)。注意:完整底部控制面板 DOM(ensureSheetBuilt)会延迟最多 3 秒或等待浏览器空闲时再构建,以确保初始页面渲染流畅。
    • 启动批量 MutationObserver(startObserver)并执行 initialScan()(在 YouTube 域名上则调用 initYouTubeShield())。
    • 挂钩单页应用(SPA)路由变化(history.pushState、history.replaceState 与 popstate)。
  3. 阶段 3 — 空闲后台任务(requestIdleCallback 或 2,000 ms 回退):
    • 从存储中加载累计统计数据与 14 天活动历史。
    • 安排有界的反广告拦截回退扫描(600 ms、1,800 ms 与 4,200 ms)。
    • 注册用户脚本管理器菜单命令(GM_registerMenuCommand)。
    • 检查脚本更新(每 30 分钟一次)并同步远程域名列表(若已配置,每 24 小时一次)。

DOM 级过滤 vs. 网络级过滤#

DOM 级过滤#

DOM 级过滤针对已创建或插入文档树的 HTML 元素进行处理:

  • 修饰性 CSS 注入: <style data-nvirya-cosmetic> 样式表负责隐藏常见广告选择器([id^="google_ads_"]、ins.adsbygoogle、[id^="div-gpt-ad"]、[data-ad-slot]、已知广告 iframe 及 [data-nvirya-hidden]),以及针对 YouTube、Google 搜索、Bing 搜索、Reddit、X/Twitter、Quora 和 Facebook 移动端的域名作用域规则。
  • 批量 DOM 变动监听: startObserver() 不会在每次变动时全量扫描整个页面,而是将新增节点和属性变更(src、href、style、class、id、data-src、data-srcset)收集到有界队列(MAX_QUEUE = 800)中,并通过 processInChunks(每帧时间预算最多 8 ms)分片处理。在 Google 搜索与 Bing 搜索上,DOM 变动还会进行 150 ms 防抖(RE_SEARCH_DEBOUNCE_HOST),避免输入搜索词时发生布局抖动。
  • 遮罩、背景幕布与滚动锁定恢复: 当移除广告弹窗(removeTrackedAdNode)时,脚本会捕获周围遮罩上下文(captureOverlayRemovalContext),移除配对的全屏暗色背景(adjacentBackdropNodes),折叠空白底部悬浮广告栏(cleanupBottomStickyAdWrappers),并在没有合法弹窗打开时(hasVisibleLegitimateModal)恢复 <html> 与 <body> 的 overflow: auto 和 position: static(unlockPageScroll)。

JavaScript 网络钩子(及其局限性)#

用户脚本无法访问浏览器的特权网络栈。因此,Nvirya AdGuard 在用户脚本上下文(installNetworkGuard)和主页面上下文(mainWorldShield)的 JavaScript API 层拦截请求:

  • Window.fetch (Fetch API): 当脚本向匹配 isAdUrl(u)(或开启深度网络过滤时匹配 RE_TRACKER)的地址发起 fetch(url) 请求时,Nvirya AdGuard 不会抛出容易触发反广告拦截检测的网络错误,而是通过 createFakeFetchResponse 返回一个合成的 200 OK Response 对象(JSON 请求返回 {"status":"ok","success":true,"data":[]},JS 请求返回 /* Nvirya Stub OK */ window.canRunAds = true;,纯文本返回空字符串)。
  • XMLHttpRequest: 当 xhr.open(method, url) 指向被拦截的广告或跟踪器 URL 时,脚本会将内部请求重定向到无害的 data:text/plain, URI,并在 xhr.send() 时将 readyState(4)、status(200)、statusText("OK")、responseText 和 response 覆盖为合成响应,同时派发 readystatechange、load 与 loadend 事件。
  • Navigator.sendBeacon: 被拦截的信标请求会立即返回 true,不会向网络发送数据。
  • 动态 <script> 与 <img> 属性: 将 HTMLScriptElement 的 .src 或 setAttribute('src', ...) 设为已知广告域名时,脚本会将其 type 设为 "javascript/blocked" 并触发合成 load 事件(triggerFakeScriptLoad)。将 HTMLImageElement 的 .src 设为广告 URL 时,脚本会伪造一个已加载完成的 32×32 图像(complete = true、naturalWidth = 32、naturalHeight = 32)。

JavaScript 钩子无法拦截的情况:

  • 声明式 HTML 子资源:在用户脚本钩子执行前已被浏览器预加载扫描器发起请求的静态 <img src="..."> 或 <iframe src="..."> 标签(但 DOM 过滤仍会在元素生成后将其隐藏或移除)。
  • CSS 网络请求:如 background-image: url(...) 或 @font-face 字体加载。
  • 服务端 HTTP 重定向(301、302、307、308 响应头)或用户脚本管理器未注入脚本的跨源 iframe 内部流量。
  • 受严格 内容安全策略 (CSP) 或 WebKit 沙盒限制的页面:当内联 <script> 注入(injectMainWorldShield)被阻止时,主世界钩子无法生效。

为什么流媒体广告不同于普通 DOM 广告#

传统的横幅或弹窗广告是加载自第三方广告域名的独立 DOM 元素(<iframe>、<div>、<ins> 或 <script>)。移除该元素或返回空响应通常就能干净地消除广告。

而 YouTube 等平台上的视频广告机制截然不同:

  1. 共享媒体传输基础设施: 视频切片和播放器控制数据通常通过与正片视频完全相同的核心 API 端点(/youtubei/v1/player、/youtubei/v1/next)和媒体 CDN(googlevideo.com、youtube.com/videoplayback)进行分发。
  2. 中断播放的风险: 如果脚本盲目拦截 googlevideo.com 或丢弃 /youtubei/v1/player 响应,视频播放器就会卡死、黑屏或显示播放错误。
  3. 协同播放器状态处理: 因为 Nvirya AdGuard 必须放行合法媒体流(RE_YOUTUBE_CDN_WHITELIST),它在 initYouTubeShield() 内部通过两层协同机制处理视频广告:
    • 响应 JSON 裁剪(installYouTubeJsonPruner): 拦截 ytInitialPlayerResponse、Response.prototype.json()、Response.prototype.text() 与 XMLHttpRequest 播放器响应,在播放器解析前清空 adPlacements、playerAds、adSlots、adBreakHeartbeatParams 和 adBreakRenderer。
    • 播放器状态加速回退(scheduleVideoAdScan): 当广告仍然进入 HTML5 <video> 元素(通过 #movie_player 上的 .ad-showing 或 .ad-interrupting 检测)时,脚本会自动点击可见的跳过按钮、将视频静音、将 playbackRate 提升至 16 倍,并在媒体时长元数据可用后立即跳转到 duration - 0.05 秒,随后恢复您原本的音量与播放速度。

有关移动端视频播放的更多说明,请参阅 已知限制 与 故障排除指南。