☰
Nora内容脚本注入原理全解:从MutationObserver到广告精准移除
2026/10/15 15:41:02 网站建设 项目流程

【免费下载链接】Nora

SNS browser. Facebook, Instagram, Reddit, Threads, X and more in a single app. No ads.

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

Nora 是一款无广告的 SNS 浏览器,把 Facebook、Instagram、Reddit、Threads、X 等社交平台装进同一个 App。它能"读懂"页面里的广告并精准移除,核心靠的是一段被注入到每个网页中的内容脚本(Content Script):脚本在页面加载瞬间执行,用 MutationObserver 监听 DOM 变化,再通过"网络拦截 + 按帧清扫"两条防线把广告干净利落地藏起来。本文带你从构建、注入到广告移除,完整看懂这套机制。

内容脚本从哪来:一条构建流水线

Nora 的内容脚本并不是手写的一大坨 JS,而是用 TypeScript 模块化编写,再由 esbuild 打包成单文件。构建命令定义在 package.json:

esbuild --bundle content/main.ts --outfile=assets/scripts/main.bjs

也就是说,整个注入逻辑的源码入口是 content/main.ts,产物main.bjs会被打进 App 安装包。

App 启动时,app/index.tsx 用Asset.loadAsync找到打包好的脚本,fetch读出完整文本后,通过scriptOnStart状态层层传给页面组件:

app/index.tsx → MainPage contentJs → ContentJsContext → NoraTab

其中 Context 的读写封装在 lib/hooks/useContentJs.ts。最后一步发生在 components/tab/NoraTab.tsx:每个标签页的 webview 加载时,Nora 把"广告屏蔽例外 + 匿名模式 + 内容脚本"拼成一段document-start脚本注入进去,保证脚本比网站自己的代码先跑。桌面端(Electron)同理,desktop/src/renderer/App.tsx 直接import脚本原文并复用同一套注入管线。

💡 关键设计:脚本在document-start时机执行,抢在网站发起第一个广告请求之前就位,这是"无广告体验"的前提。

脚本入口:先装广告拦截,再起 MutationObserver

打开 content/main.ts 可以看到入口逻辑非常克制:

blockAds() // 第一道防线:网络层拦截 window.Nora = initNora() // 建立与 App 的通信桥 initObserver() // 第二道防线:DOM 监听

initNora()定义在 content/nora.ts,负责设置页内开关(比如adBlockingEnabled)、暴露window.Nora给后续脚本调用。真正的"动态页面看门狗"在 content/main.ts:

const observer = new MutationObserver((mutations) => { hideAds(mutations) // 处理广告变化 handleDialogs() // 接管弹窗 scheduleAnonymousScan() // 匿名模式扫描 scheduleMorePostsScan() // "查看更多帖子"扫描 }) observer.observe(document.documentElement, { childList: true, subtree: true, })

为什么需要 MutationObserver?因为现代社交网站是 SPA:信息流帖子是滚动时才异步渲染进 DOM 的,广告也一样——它不会在onload时静静等着你,而是在你下滑的那一刻才"长"出来。Observer 监听整棵documentElement的子节点变化,任何一块新 DOM 插进来,都会触发上面的回调链。

广告精准移除:两条防线各管一半

所有广告相关逻辑集中在 content/ad.ts,按域名分工。

防线一:XHR 网络拦截——广告根本不落地

对 Instagram、Reddit、X,Nora 选择在数据到达前动手。content/ad.ts 的blockAds()会重写window.XMLHttpRequest:用一个继承自原生 XHR 的类替换它,在读取response/responseText时按 URL 判断是否要"改包"。

具体"怎么改"由站点服务类决定,注册表在 content/services/manager.ts。以 content/services/instagram.ts 为例:

transformResponse(res: string) { // 只有时间线请求才值得解析 if (!res.includes('xdt_api__v1__feed__timeline__connection')) return res const data = JSON.parse(res) data.data.xdt_api__v1__feed__timeline__connection.edges = before.filter((x) => !x.node.ad) // 直接滤掉广告节点 return JSON.stringify(data) }

Instagram 的 GraphQL 查询共用一个端点,所有响应都长得很像,所以先用子串探测"这是不是时间线数据",是才解析、过滤带ad字段的节点。广告在数据层就被剔除,DOM 里从头到尾不会渲染它——这是最彻底的移除方式。

这里还有一个工程细节:网站对同一请求的response可能读多次,而解析+重序列化整个 JSON 并不便宜,所以 content/ad.ts 用WeakMap按 XHR 实例缓存转换结果,同一个 body 只处理一次;open()时清缓存,防止复用的请求对象拿错结果。

防线二:MutationObserver + 按帧清扫——藏好已渲染的广告

Facebook 和 LinkedIn 的广告无法在网络层干净剥离,Nora 改用 DOM 手段。Observer 回调hideAds(mutations)(content/ad.ts)按域名走不同策略:

域名广告特征识别方式
m.facebook.com服务端直出的广告卡片节点带trackingDurationId属性 + 子树文本命中"赞助"多语言标签
www.facebook.com客户端渲染的帖子"Sponsored" 标签晚于帖子插入,先失效缓存判定,再统一重扫
www.linkedin.com.feed-item推广内容读取低强调度文案 span,匹配多语言"Promoted"列表

注意 www.facebook.com 的处理非常讲究性能:Facebook 一个批次能产生几百条 mutation 记录,且都指向同几篇帖子。如果在每条记录上都扫一次,同一帖子会被重复扫几百次,把页面脚本饿死(评论区会一直转圈)。所以 content/ad.ts 让记录只做"失效缓存判定"这件轻活,真正的清扫交给requestAnimationFrame合并到每帧一次,每个帖子每帧只扫一遍。LinkedIn 和多语言标签匹配(fbL10nSponsored、linkedinL10nPromoted)的判定逻辑分别在 content/services/facebook.ts 和 content/services/linkedin.ts。

隐藏 ≠ 简单 display:none:防止信息流"跳动"

把广告display: none会改变容器高度,滚动位置可能跳变。更棘手的是 Facebook Reels 的滚动吸附(scroll-snap)容器:Chromium 在滚动区重新布局时会重新执行 snap 选择,把画面"拽回"上一条 Reel。

content/ad.ts 的hideElement专门处理了这个问题:先向上找带scroll-snap-type的滚动容器,判断广告是否在视口上方——只有上方的内容收起才需要修正scrollTop,然后在同一帧内读scrollHeight强制布局、立即补偿偏移,让 snap 选择跑起来之前滚动位置已经对齐。广告消失,画面稳如泰山。

小结:一套克制的广告移除架构

把整条链路串起来看:

  1. 构建期:content/main.ts 模块化源码经 esbuild 打包为assets/scripts/main.bjs
  2. 注入期:app/index.tsx 读取脚本 → NoraTab.tsx 以document-start时机注入每个 webview
  3. 运行期:blockAds()在网络层拦截可解析的广告数据;MutationObserver 捕获 DOM 变化,hideAds()按帧清扫已渲染的广告,并用快照感知的hideElement保证滚动体验无跳变

这套设计对新手理解"浏览器插件 / 内容脚本"类架构也很有参考价值:能拦在网络层的绝不动 DOM,必须动 DOM 时也要把开销压到每帧一次——这正是 Nora 做到"无广告" yet 页面依然丝滑的秘密。

模块速查:入口 content/main.ts · 广告逻辑 content/ad.ts · 站点服务 content/services/ · 设置桥接 content/nora.ts · CSS 注入 content/css.ts

【免费下载链接】Nora

SNS browser. Facebook, Instagram, Reddit, Threads, X and more in a single app. No ads.

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

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

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

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

立即咨询