前端

拦截弹窗代码实现指南:原理、多平台技术与实战案例

TRAE AI 编程助手

01|弹窗拦截技术全景:从浏览器到内核的攻防战

谢邀,人在工位,刚关第 37 个"限时领取 888 元券"的弹窗。
今天把压箱底的多平台拦截方案一次性开源,顺便秀一波 TRAE IDE 的"边写边测"能力,让弹窗无处遁形。


02|技术原理:弹窗到底在哪一层"出生"?

层级代表 API拦截点关键数据结构
浏览器window.open重写+事件代理WindowProxy
系统 WebViewWKWebView / WebViewClient代理回调WKUIDelegate
桌面系统CreateWindowEx钩子+事件过滤CBTProc / EVENT_OBJECT_CREATE
内核驱动NtUserCreateWindowEx回调注册ObRegisterCallbacks

一句话总结:
"弹窗"= 用户态 API → 内核态句柄 → 消息循环 → 界面绘制;在哪一层插入钩子,就决定拦截的粒度与稳定性。


03|Web 端:一行代码拦截 90% 弹窗

3.1 重写原生 API(浏览器插件/油猴脚本通用)

// userscript = true
// ==UserScript==
// @name         Universal Pop-blocker
// @match        *://*/*
// @run-at       document-start
// ==/UserScript==
 
(() => {
  const open = window.open;
  window.open = function (...args) {
    console.warn('[PopBlocker] 拦截可疑调用:', args);
    // 白名单:主域相同且带 _blank 的放行
    const [url, target] = args;
    if (target === '_blank' && new URL(url).hostname.endsWith(location.hostname)) {
      return open.apply(this, args);
    }
    // 其余一律拦截
    return null;
  };
})();

3.2 事件代理拦截"伪弹窗"——div 盖板

// 监听节点插入,秒杀"遮罩层+关闭按钮"套路
new MutationObserver(() => {
  document.querySelectorAll('div[style*="fixed"]').forEach(n => {
    if (n.offsetWidth / window.innerWidth > 0.8) {
      n.remove();          // 暴力移除
      console.log('[PopBlocker] 已干掉一个全屏遮罩');
    }
  });
}).observe(document.body, { childList: true, subtree: true });

TRAE 实时调试技巧
在 TRAE IDE 里装 Tampermonkey Snippets 插件,脚本改动后 ⇧⌘R 立即热重载,无需刷新页面即可看效果,比传统"改→F5→找弹窗"快 5 倍。


04|移动端:双端 WebView 拦截方案

4.1 Android:重写 WebChromeClient#onCreateWindow

class PopBlockWebChromeClient : WebChromeClient() {
    override fun onCreateWindow(
        view: WebView, isDialog: Boolean, isUserGesture: Boolean, resultMsg: Message?
    ): Boolean {
        // 用户手势触发 + 白名单域名校验
        if (isUserGesture && allowList.contains(view.url?.host)) {
            return super.onCreateWindow(view, isDialog, isUserGesture, resultMsg)
        }
        // 拦截:不给新窗口句柄
        resultMsg?.sendToTarget()
        return true
    }
}

4.2 iOS:利用 WKUIDelegate 拒绝新窗口

func webView(_ webView: WKWebView,
             createWebViewWith configuration: WKWebViewConfiguration,
             for navigationAction: WKNavigationAction,
             windowFeatures: WKWindowFeatures) -> WKWebView? {
    guard navigationAction.targetFrame == nil,
          let url = navigationAction.request.url,
          allowList.contains(url.host ?? "") else {
        return nil   // 直接返回 nil,弹窗胎死腹中
    }
    // 允许打开新 VC 承载
    let popup = WKWebView(frame: .zero, configuration: configuration)
    viewController.present(PopupVC(popup), animated: true)
    return popup
}

05|桌面端:全局级钩子,让弹窗"生"不出来

5.1 Windows CBT 钩子(用户态)

// MinHook 示例,拦截窗口创建
#include <Windows.h>
#include <MinHook.h>
 
static HWND(WINAPI *TrueCreateWindowEx)(
    DWORD, LPCWSTR, LPCWSTR, DWORD, int, int, int, int, HWND, HMENU, HINSTANCE, LPVOID
) = CreateWindowExW;
 
HWND WINAPI HookCreateWindowEx(
    DWORD exStyle, LPCWSTR cls, LPCWSTR title, DWORD style,
    int X, int Y, int W, int H, HWND parent, HMENU menu, HINSTANCE inst, LPVOID param
) {
    if (wcsstr(title, L"限时抢购") || wcsstr(cls, L"Chrome_WidgetWin_2")) {
        SetLastError(ERROR_ACCESS_DENIED);
        return nullptr;          // 直接返回空句柄
    }
    return TrueCreateWindowEx(exStyle, cls, title, style, X, Y, W, H, parent, menu, inst, param);
}
 
// 注入目标进程后安装钩子
MH_Initialize();
MH_CreateHook(&CreateWindowExW, &HookCreateWindowEx, (LPVOID*)&TrueCreateWindowEx);
MH_EnableHook(&CreateWindowExW);

5.2 macOS:Swift + Accessibility API

import ApplicationServices
 
func blockPopUps() {
    let options = [kAXTrustedCheckOptionPrompt.takeRetainedValue(): true] as CFDictionary
    guard AXIsProcessTrustedWithOptions(options) else { return }
 
    NSWorkspace.shared.notificationCenter.addObserver(
        forName: NSWorkspace.didLaunchApplicationNotification,
        object: nil,
        queue: nil
    ) { noti in
        guard let app = noti.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication,
              let name = app.localizedName
        else { return }
        if name.contains("广告") || name.contains("Popup") {
            app.forceTerminate()
        }
    }
}

06|实战案例:10 分钟给 Electron 应用加上"弹窗防火墙"

场景
内部 IM 工具基于 Electron,但偶尔被第三方 SDK 弹窗骚扰。要求:

  1. 不修改 SDK 源码
  2. 支持热开关
  3. 日志可追踪

实现
主进程预加载脚本:

// blocker.js
const { session } = require('electron');
 
session.defaultSession.webRequest.onBeforeRequest(
  { urls: ['*://ads.example.com/*'] },          // 1. 网络层先过滤
  (details, callback) => callback({ cancel: true })
);
 
// 2. 渲染层再兜底
const blocker = `
  window.open = () => null;
  document.addEventListener('DOMNodeInserted', e => {
    const n = e.target;
    if (n.tagName === 'DIV' && n.innerHTML.includes('立即领取')) {
      n.remove();
      console.log('[Blocker] 移除营销节点');
    }
  });
`;
webContents.on('dom-ready', () => webContents.executeJavaScript(blocker));

TRAE 一键打包 & 热更新
TRAE 内置 Electron Forge 模板,⌘⇧P → Electron: Package 即可出绿色安装包;
打开 Auto-updater 插件,把 blocker.js 放 resources/dynamic/ 目录,云端改规则,客户端 30 s 内无感热更新,无需重新发版。


07|常见坑与调试技巧

症状排查命令/思路
HTTPS 页面 Mixed-content拦截脚本不生效DevTools→Console 看 CSP 报错;加 // @grant unsafeWindow
iOS 13+ 弹窗仍跳出WKWebView 白名单未同步用 Safari Web Inspector 远程调试,看 targetFrame
Windows 钩子崩溃目标进程闪退VS 附加→异常设置→勾选 Win32 访问冲突;用 TRAE 远程 GDB 联调
Electron 更新后失效webRequest 监听被清空主进程 console 打印 webContents.id 确认生命周期,重注册监听

08|性能与体验权衡

  • Web 油猴脚本 ≈ 0 额外内存,但能被页面反屏蔽
  • 移动端 WebView 代理 稳定、省电,需发版才能改规则
  • 桌面全局钩子 最彻底,但需管理员权限,升级成本高

结论:"网络层+渲染层"双保险 是当前 ROI 最高的组合;
用 TRAE IDE 的 多进程调试器 同时看主进程 & 渲染进程日志,定位哪一层漏拦,比单步 printf 快 10 倍。


09|思考题

  1. 如果页面用 window.open 打开 data:text/html,<script>alert(1)</script>,上述脚本还能拦截吗?
  2. 在 macOS 11+ 上,Accessibility API 需要用户手动授权,如何引导用户才能最大化通过率?
  3. 尝试用 TRAE 的 MCP 脚本 把"拦截规则"做成可拖拽的 JSON 配置,让运营同学也能 5 分钟上线新规则。

10|参考资料


把代码搬进 TRAE,按 ⌘↵ 直接跑,弹窗拦截效果"所见即所得"。
如果文章帮到你,点个赞或在看,我们评论区继续盘逻辑!

(此内容由 AI 辅助生成,仅供参考)