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 — 核心即时防护(
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 — 延迟 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 — 空闲后台任务(
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 OKResponse对象(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 等平台上的视频广告机制截然不同:
- 共享媒体传输基础设施: 视频切片和播放器控制数据通常通过与正片视频完全相同的核心 API 端点(
/youtubei/v1/player、/youtubei/v1/next)和媒体 CDN(googlevideo.com、youtube.com/videoplayback)进行分发。 - 中断播放的风险: 如果脚本盲目拦截
googlevideo.com或丢弃/youtubei/v1/player响应,视频播放器就会卡死、黑屏或显示播放错误。 - 协同播放器状态处理: 因为 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秒,随后恢复您原本的音量与播放速度。
- 响应 JSON 裁剪(