Ant Design Table 横向滚动条神秘消失?我是如何一步步找到真凶的

起因

项目中有一个设备管理页面,使用了 Ant Design 的 Table 组件,配置了横向和纵向滚动:

css 复制代码
<Table
  scroll={{
    x: "100%",
    y: "calc(100vh - 300px)",
  }}
  // ... 其他属性
/>

某天测试同学反馈了一个诡异的问题:表格的滚动条会莫名其妙地消失

更离谱的是,滚动条虽然看不见了,但鼠标放在原来滚动条的位置仍然可以拖动一个"隐形"的滚动条!


第一步:确认复现路径

首先,我需要搞清楚滚动条在什么情况下会消失。经过反复测试,终于找到了稳定的复现路径:

  1. 在标签页 A 中打开设备管理页面,Table 正常显示横向和纵向滚动条 ✅
  2. 点击某个设备进入详情页,右键点击二维码,在新标签页 B 中打开手机端页面
  3. 在标签页 B 中按 F12 打开开发者工具,切换视图之后
  4. 切回标签页 A → 滚动条消失了!

关键发现:问题只在"标签页 B 切换设备仿真"后才会出现。如果不切换设备仿真,滚动条一直正常。

这说明问题跟 Chrome DevTools 的设备仿真有关。但为什么呢?设备仿真只影响当前标签页 B,为什么会影响到标签页 A?


第二步:排除 CSS 原因

我的第一反应是:是不是 CSS 样式污染了?

项目里有一个 device-details-mgmt.css,里面用全局的 ::-webkit-scrollbar 把所有滚动条设成了 5px 宽、浅灰色:

ruby 复制代码
::-webkit-scrollbar {
    width: 5px;
    height: 5px;
}
::-webkit-scrollbar-thumb {
    background: #c1c1c1;  /* 浅灰滑块 */
}
::-webkit-scrollbar-track {
    background: #f1f1f1;  /* 浅灰轨道 */
}

5px 宽 + 浅灰色,在浅色背景下确实不太看得清。我试着把这些样式限定到设备详情容器内,避免影响 Table。

结果:滚动条照样消失。

这说明 CSS 不是根因。但我还是不死心,又试了几种 CSS 方案:

尝试的方案 结果
overflow: scroll !important 强制显示滚动条 ❌ 无效
scrollbar-gutter: stable 保留滚动条空间 ❌ 无效
scrollbar-color + scrollbar-width 标准属性 ❌ 无效

所有 CSS 方案全部无效!

这让我意识到,问题不在 CSS 层面,而是更底层的原因。


第三步:排除 JS 原因

既然 CSS 搞不定,那是不是 JS 的问题?

我怀疑的方向有:

怀疑 1react-full-screen 组件的跨标签页事件干扰

设备详情页用了 react-full-screen,设备仿真可能触发了全屏变化事件。我在 onChange 中加了 document.fullscreenElement 检查,只允许当前标签页的全屏事件生效。

结果:无效。全屏事件根本没有被触发。

怀疑 2vh 单位被设备仿真重新计算

Table 的 scroll.y 用了 calc(100vh - 300px),设备仿真可能改变了 vh 的值。我改用 useRef + getBoundingClientRect() 动态计算高度。

结果:无效。高度计算完全正确,滚动条消失不是因为高度问题。

怀疑 3:标签页切回时需要强制重渲染

我监听了 visibilitychange 事件,当标签页重新可见时,通过临时切换 overflow 属性强制浏览器重新渲染滚动条。

结果:无效。重新渲染后滚动条仍然是透明的。

JS 方案也全部无效!


第四步:换个思路------为什么 B 标签页能影响 A 标签页?

CSS 和 JS 都试过了,问题依然存在。我不得不重新审视一个最基本的问题:

为什么标签页 B 的操作,能影响到标签页 A?

在正常的认知中,浏览器的每个标签页是相互隔离的。一个标签页的 JS、CSS、DOM 不应该影响另一个标签页。

但事实摆在眼前:B 的设备仿真确实影响了 A 的滚动条。

这说明 A 和 B 之间存在某种共享。那共享的是什么?


第五步:认识 Chrome 渲染进程

我开始研究 Chrome 的多进程架构,发现了一个关键知识点:

Chrome 会将具有 opener 关系的标签页分配到同一个渲染进程(Renderer Process)中。

什么是 opener 关系?当你用 window.open(url, '_blank') 打开新标签页时,新标签页可以通过 window.opener 访问原标签页。Chrome 为了性能优化,会将这样的两个标签页放在同一个渲染进程中。

而我们的代码正是这样写的:

javascript 复制代码
// DownloadSvgQRCode.js
window.open(
  `${window.location.origin}/#/ScanDeviceQRCode?device_id=${device_id}`,
  '_blank'
  // 没有第三个参数!
);

没有 noopener,所以 A 和 B 共享同一个渲染进程!


第六步:理解设备仿真对渲染进程的影响

那设备仿真又是怎么影响渲染进程的呢?

当你在 DevTools 中切换设备仿真时,Chrome 通过 CDP(Chrome DevTools Protocol) 发送命令:

php 复制代码
Emulation.setScrollbarsHidden({ hidden: true })
Emulation.setDeviceMetricsOverride({ mobile: true, ... })

关键在于 setScrollbarsHidden------它的效果是修改渲染进程级别的滚动条模式,将经典滚动条(Classic Scrollbar)切换为覆盖式滚动条(Overlay Scrollbar)。

而 Overlay 滚动条的特点是:半透明、自动隐藏。这就是为什么滚动条看起来"消失"了,但拖动区域还在------滚动条其实还在,只是变成了透明的 overlay 模式!

因为 A 和 B 共享同一个渲染进程,所以 B 的设备仿真修改了进程级滚动条模式,A 也被影响了!


第七步:验证------noopener 分离渲染进程

既然根因是共享渲染进程,那解决方案就是让 A 和 B 使用独立的渲染进程

方法很简单:给 window.open 添加 noopener 参数:

javascript 复制代码
// 修改前
window.open(url, '_blank');

// 修改后
window.open(url, '_blank', 'noopener');

noopener 做了两件事:

  1. 断开 opener 关系 :新标签页的 window.opener 变为 null
  2. 强制分离渲染进程:Chrome 不再需要维护 opener 通信通道,新标签页被分配到独立渲染进程

修改后测试:✅ 问题完美解决! B 标签页的设备仿真不再影响 A 标签页的滚动条。


原因总结

用一张图说清楚整个因果链:

css 复制代码
window.open('_blank') 没有加 noopener
        │
        ▼
A 和 B 标签页建立 opener 关系
        │
        ▼
Chrome 将 A 和 B 分配到同一个渲染进程
        │
        ▼
B 标签页切换设备仿真
        │
        ▼
CDP 发送 Emulation.setScrollbarsHidden({ hidden: true })
        │
        ▼
渲染进程级别的滚动条模式从 Classic 切换为 Overlay
        │
        ▼
A 标签页的滚动条也变成 Overlay 模式(半透明、自动隐藏)
        │
        ▼
A 标签页的滚动条"消失"了!

修复 :添加 noopener,让 B 使用独立渲染进程,B 的设备仿真不再影响 A。


延伸知识

Chrome 渲染进程与标签页的关系

打开方式 是否共享渲染进程
window.open(url, '_blank') ✅ 共享(同一站点)
window.open(url, '_blank', 'noopener') ❌ 独立
用户手动 Ctrl+T 打开新标签页 ❌ 独立
从书签栏打开 ❌ 独立

两种滚动条模式的区别

Classic(经典) Overlay(覆盖式)
外观 始终可见 半透明,自动隐藏
布局 占据空间 浮在内容上方
CSS ::-webkit-scrollbar ✅ 有效 无效
scrollbar-gutter: stable ✅ 有效 无效
触发条件 桌面模式(默认) 移动端 / DevTools 设备仿真

CDP 命令的影响范围

CDP 命令 影响范围
Emulation.setDeviceMetricsOverride 仅当前标签页
Emulation.setScrollbarsHidden ⚠️ 整个渲染进程
Emulation.setTouchEmulationEnabled 仅当前标签页

如何确认标签页是否共享渲染进程

  • 方法 1 :按 Shift+Esc 打开 Chrome 任务管理器,查看是否有多个标签页共用同一个进程 ID
  • 方法 2 :地址栏输入 chrome://process-internals,查看每个标签页的进程信息
  • 方法 3 :在 Console 中执行 console.log(window.opener),如果不为 null,说明可能共享渲染进程

最终修复

javascript 复制代码
// DownloadSvgQRCode.js

// 修改前
window.open(
  `${window.location.origin}/#/ScanDeviceQRCode${device_id ? `?device_id=${device_id}` : ''}`,
  '_blank'
);

// 修改后 ------ 只加了第三个参数 'noopener'
window.open(
  `${window.location.origin}/#/ScanDeviceQRCode${device_id ? `?device_id=${device_id}` : ''}`,
  '_blank',
  'noopener'
);

一行代码,问题解决。noopener 不仅是安全最佳实践(防止 tabnapping 攻击),还能避免渲染进程级别的副作用。

相关推荐
GreenTea1 小时前
DeepSeek-V4 技术报告深度分析:基础研究创新全景
前端·人工智能·后端
河阿里2 小时前
HTML5标准完全教学手册
前端·html·html5
吴声子夜歌2 小时前
Vue3——新语法
前端·javascript·vue.js
jiayong232 小时前
第 36 课:任务详情抽屉快捷改状态
开发语言·前端·javascript·vue.js·学习
FFF_634560232 小时前
通用 vue 页面 js 下载任何文件的方法
开发语言·前端·javascript
光影少年2 小时前
中级前端需要会的东西都有那些?
前端·学习·前端框架
琢磨先生TT2 小时前
为什么很多后台系统功能不少,看起来却还是很廉价?
前端·vue.js·设计
ekuoleung2 小时前
量化平台中的 DSL 设计与实现:从规则树到可执行策略
前端·后端
小研说技术3 小时前
实时通信对比,一场MCP协议的技术革命
前端·后端·面试