为什么我的代码在测试环境跑得好好的,一到用户电脑就崩?原来凶手躲在地址栏旁边

0 阅读6分钟

引言

“Bug 无法复现,建议关闭。”

这是我上周在一个 issue 下面看到的回复。发 issue 的用户是个忠实用户,他说我们网站的某个按钮点完后页面就白屏了,但我们在测试环境、预发环境、甚至他的电脑上用无痕模式都试了一遍,愣是没复现。

就在我准备把这个 issue 标记为“无法复现,关闭”的时候,产品经理幽幽地说了一句:“要不你试试装几个浏览器插件?”

我当时心想:插件能影响我们代码?那不至于吧。

结果我装了 AdBlock、装了油猴脚本、装了某个购物比价插件,刷新页面,点击按钮——白屏了

那一刻我恍然大悟:原来我们的代码一直生活在“无菌实验室”里,而用户的浏览器,是一个充满了各种“妖魔鬼怪”的丛林。

今天,我们就来聊聊那些躲在地址栏旁边的“凶手”——浏览器扩展(Extensions),以及它们如何悄悄地破坏你的网页。

一、浏览器扩展:用户的朋友,开发者的噩梦

浏览器扩展(Chrome/Firefox/Edge 插件)本质上是在用户浏览器里运行的第三方代码。它们拥有各种权限:

  • 读取和修改当前页面的 DOM
  • 拦截和修改网络请求
  • 注入自己的 JS 和 CSS
  • 甚至操作本地存储、Cookie

这些权限对用户来说是“增强功能”,但对我们开发者来说,就是一颗不知道什么时候会炸的雷。

1.1 最常见的“作案手法”

手法一:往 DOM 里塞私货

很多广告拦截插件会扫描页面里的广告位,然后移除或隐藏它们。但如果你的代码恰好依赖某个被移除的 DOM 节点,就会报错。

// 你写的代码
const adBanner = document.getElementById('ad-banner');
adBanner.addEventListener('click', trackAdClick); // 如果 adBanner 被插件删了,这里就报错

手法二:修改全局变量

有些插件会往 window 对象上挂东西,比如 window.web3window.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 前检查元素是否存在

    const el = document.getElementById('some-id');
    if (el) {
      el.addEventListener(...);
    }
    
  • 使用 !important 时要谨慎:插件经常用 !important 覆盖样式,如果你的样式也用 !important,可能会变成“谁的 !important 更厉害”的军备竞赛。

  • 避免依赖全局变量:如果一定要用,先检查是否存在冲突:

    if (typeof window.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 时,可以提供一个标准操作:

  1. 打开无痕模式(默认禁用大部分插件)。
  2. 如果无痕模式正常,说明是插件的问题。
  3. 一个一个关掉插件,找出罪魁祸首。

这比你在本地猜来猜去要高效得多。

五、检测用户装了哪些插件(有限但有用)

虽然你不能直接读取用户安装的所有插件(隐私原因),但你可以通过一些“痕迹”来推测:

function detectExtensions() {
  const detected = [];
  
  // AdBlock 检测
  if (document.querySelector('.adblock-warning') || 
      typeof window.adblockDetector !== 'undefined') {
    detected.push('AdBlock (可能)');
  }
  
  // 暗黑模式检测
  if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
    // 这不一定是插件,也可能是系统设置
    detected.push('暗黑模式');
  }
  
  // 某些插件会在 body 上加特定 class
  if (document.body.classList.contains('darkreader')) {
    detected.push('Dark Reader');
  }
  
  return detected;
}

六、总结:拥抱不确定性

浏览器插件是用户自主安装的,我们无法禁止,也不应该禁止。但我们可以通过防御性编程 + 异常监控 + 用户沟通,让页面在面对这些“不速之客”时更加健壮。

下次当你遇到“测试环境正常,用户环境报错”的 bug 时,别急着怀疑自己的代码,先看看用户的地址栏旁边——可能有个小小的图标,正在悄悄给你的页面使绊子。


每日一问:你遇到过最离谱的“插件导致 bug”是什么?是广告拦截器把你的登录按钮给拦了?还是翻译插件把你的代码注释翻译成了英文导致报错?评论区分享你的“受害者”经历!