
引言“Bug 无法复现建议关闭。”这是我上周在一个 issue 下面看到的回复。发 issue 的用户是个忠实用户他说我们网站的某个按钮点完后页面就白屏了但我们在测试环境、预发环境、甚至他的电脑上用无痕模式都试了一遍愣是没复现。就在我准备把这个 issue 标记为“无法复现关闭”的时候产品经理幽幽地说了一句“要不你试试装几个浏览器插件”我当时心想插件能影响我们代码那不至于吧。结果我装了 AdBlock、装了油猴脚本、装了某个购物比价插件刷新页面点击按钮——白屏了。那一刻我恍然大悟原来我们的代码一直生活在“无菌实验室”里而用户的浏览器是一个充满了各种“妖魔鬼怪”的丛林。今天我们就来聊聊那些躲在地址栏旁边的“凶手”——浏览器扩展Extensions以及它们如何悄悄地破坏你的网页。一、浏览器扩展用户的朋友开发者的噩梦浏览器扩展Chrome/Firefox/Edge 插件本质上是在用户浏览器里运行的第三方代码。它们拥有各种权限读取和修改当前页面的 DOM拦截和修改网络请求注入自己的 JS 和 CSS甚至操作本地存储、Cookie这些权限对用户来说是“增强功能”但对我们开发者来说就是一颗不知道什么时候会炸的雷。1.1 最常见的“作案手法”手法一往 DOM 里塞私货很多广告拦截插件会扫描页面里的广告位然后移除或隐藏它们。但如果你的代码恰好依赖某个被移除的 DOM 节点就会报错。// 你写的代码constadBannerdocument.getElementById(ad-banner);adBanner.addEventListener(click,trackAdClick);// 如果 adBanner 被插件删了这里就报错手法二修改全局变量有些插件会往window对象上挂东西比如window.web3、window.ethereum。如果插件代码有 bug或者覆盖了你自己的变量就会引发冲突。手法三拦截并修改网络请求某些比价插件会在页面加载时修改 fetch 或 XMLHttpRequest往请求里加参数、改返回值。如果你的代码对返回数据格式有严格校验就可能崩。手法四注入大量 CSS 导致样式错乱很多暗黑模式插件会强制给页面添加filter: invert(1)然后你的精心设计的渐变、阴影、图片全部变成鬼片现场。二、真实案例一次被插件坑到怀疑人生的经历去年有个用户反馈我们网站的一个下拉菜单点不开。我们团队三台电脑都试了没问题。后来让用户录屏发现他的浏览器右上角有一排插件图标大概七八个。我让用户把插件一个个关掉试试。关到第三个——广告拦截器——菜单能点了。后来排查发现那个广告拦截器有一条规则把我们的菜单按钮识别成了广告弹窗给它加上了display: none !important。解决方案我们在 CSS 里给菜单按钮加了一个更高优先级的规则并且改了 HTML 结构避开了那个插件的检测规则。从那以后我养成了一个习惯在调试“用户反馈但本地无法复现”的 bug 时先问一句“你装了哪些插件”三、常见的“凶手插件”类型类型典型代表可能引发的问题广告拦截器AdBlock, uBlock Origin移除 DOM 元素、阻止网络请求安全/隐私插件Privacy Badger, Ghostery屏蔽第三方脚本、修改 Cookie密码管理器LastPass, 1Password在密码框注入额外 UI可能破坏表单提交逻辑翻译插件谷歌翻译、沙拉查词修改 DOM 文本可能破坏依赖文本内容的前端逻辑暗黑模式插件Dark Reader注入全局 CSS可能导致样式错乱比价/购物助手各种返利插件修改商品价格、添加浮动按钮可能遮挡你的 UI油猴脚本Tampermonkey用户自定义脚本什么都能干什么都能坏四、如何检测和防范“插件污染”4.1 开发阶段用插件测试自己在开发时建议装几个常见的“破坏性”插件时不时开着它们测试一下自己的页面。你会发现很多之前没想过的问题。4.2 代码层面防御性编程操作 DOM 前检查元素是否存在consteldocument.getElementById(some-id);if(el){el.addEventListener(...);}使用!important时要谨慎插件经常用!important覆盖样式如果你的样式也用!important可能会变成“谁的!important更厉害”的军备竞赛。避免依赖全局变量如果一定要用先检查是否存在冲突if(typeofwindow.myGlobal!undefined!window.myGlobal.__MY_APP__){console.warn(全局变量 myGlobal 被第三方插件覆盖);}4.3 异常捕获与上报在代码里加上try-catch并上报错误信息。当用户反馈 bug 时可以从错误日志里看出蛛丝马迹window.addEventListener(error,(event){// 上报错误附带上用户安装了哪些插件如果能检测到的话reportError({message:event.message,filename:event.filename,// 可以尝试读取用户安装的插件虽然不能完全读取但部分插件会在 DOM 上留下痕迹extensions:detectExtensions()});});4.4 教用户“排除法”当用户反馈 bug 时可以提供一个标准操作打开无痕模式默认禁用大部分插件。如果无痕模式正常说明是插件的问题。一个一个关掉插件找出罪魁祸首。这比你在本地猜来猜去要高效得多。五、检测用户装了哪些插件有限但有用虽然你不能直接读取用户安装的所有插件隐私原因但你可以通过一些“痕迹”来推测functiondetectExtensions(){constdetected[];// AdBlock 检测if(document.querySelector(.adblock-warning)||typeofwindow.adblockDetector!undefined){detected.push(AdBlock (可能));}// 暗黑模式检测if(window.matchMedia((prefers-color-scheme: dark)).matches){// 这不一定是插件也可能是系统设置detected.push(暗黑模式);}// 某些插件会在 body 上加特定 classif(document.body.classList.contains(darkreader)){detected.push(Dark Reader);}returndetected;}六、总结拥抱不确定性浏览器插件是用户自主安装的我们无法禁止也不应该禁止。但我们可以通过防御性编程 异常监控 用户沟通让页面在面对这些“不速之客”时更加健壮。下次当你遇到“测试环境正常用户环境报错”的 bug 时别急着怀疑自己的代码先看看用户的地址栏旁边——可能有个小小的图标正在悄悄给你的页面使绊子。每日一问你遇到过最离谱的“插件导致 bug”是什么是广告拦截器把你的登录按钮给拦了还是翻译插件把你的代码注释翻译成了英文导致报错评论区分享你的“受害者”经历