惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

TaoSecurity Blog
TaoSecurity Blog
Jina AI
Jina AI
雷峰网
雷峰网
月光博客
月光博客
The GitHub Blog
The GitHub Blog
WordPress大学
WordPress大学
B
Blog RSS Feed
美团技术团队
C
CXSECURITY Database RSS Feed - CXSecurity.com
小众软件
小众软件
Security Latest
Security Latest
Microsoft Azure Blog
Microsoft Azure Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
Cybersecurity and Infrastructure Security Agency CISA
Last Week in AI
Last Week in AI
A
Arctic Wolf
Latest news
Latest news
Attack and Defense Labs
Attack and Defense Labs
I
Intezer
F
Fortinet All Blogs
罗磊的独立博客
MongoDB | Blog
MongoDB | Blog
Webroot Blog
Webroot Blog
S
Secure Thoughts
Help Net Security
Help Net Security
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
V
Visual Studio Blog
P
Proofpoint News Feed
博客园 - 【当耐特】
P
Privacy International News Feed
V
Vulnerabilities – Threatpost
Stack Overflow Blog
Stack Overflow Blog
Know Your Adversary
Know Your Adversary
云风的 BLOG
云风的 BLOG
Hacker News: Ask HN
Hacker News: Ask HN
L
LINUX DO - 最新话题
H
Help Net Security
爱范儿
爱范儿
酷 壳 – CoolShell
酷 壳 – CoolShell
S
SegmentFault 最新的问题
Forbes - Security
Forbes - Security
T
Tailwind CSS Blog
量子位
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Tenable Blog
Cloudbric
Cloudbric
N
News and Events Feed by Topic
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Hugging Face - Blog
Hugging Face - Blog

iconpik

向下歧视 脱落身心 勤劳但贫穷的中国人 愤怒是我的盔甲 剥离神性 白话心经 填志愿排除法 写作是私人的事 Ghost Mailgun替代方案 Vozinha 无力 只有一次的缘分 别人是什么人 高考没那么重要 永远有人年轻 一期一会 AI is going so faaarrr 人走灯灭 無心 放手也是一种执念 舒适区里什么都长不出来 SSH重置群晖双重验证 所有的路都通向大海 开源的意义 稀缺的执行力 构建复杂项目 至人无己 前后端黑话图鉴 从明文到双重哈希 当代中国政治结构性问题 似是故人來
如何浪费一天时间
王圆圆 · 2026-05-16 · via iconpik

如何浪费一天时间

昨天是个上线日,问题一个接一个,奋战到凌晨 4:44 才算告一段落。 收尾时还剩一个小尾巴——一个移动端的滚动 bug,看起来无足轻重。我安慰自己:睡一觉,早上半小时解决掉。 这篇日志记录的,就是这半小时是怎么变长的。

如何浪费一天时间
Photo by Amaury Gutierrez / Unsplash

问题

项目基于 React + Chakra,做了一个升级弹窗 VipUpgradeModal

桌面端表现完全正常。但在 iOS 的 不同浏览器上,弹窗内容无法滚动。

问题只出现在移动端,尤其是 iOS WebKit 浏览器Modal 内滚动失效。


答案

我开始专注在 VipUpgradeModal 组件内部 overflow、flex、scrollBehavior,但真正的问题根本不在这个组件。

真实原因是:这个弹窗有两个入口,行为不一样。

  • 从 Header 打开 → 正常 (后来发现)
  • 从个人中心的"VIP特权"按钮打开 → 无法滚动

两个入口用的是同一个组件,同一套代码,却有不同的表现。问题出在打开方式,而不是组件本身。

个人中心是一个全屏 Drawer。点击"VIP特权"时,Drawer 还开着,此时再叠一个 Modal——iOS WebKit 无法同时处理两层滚动锁,Modal 内部滚动直接失效。

从 Header 打开时,外层没有 Drawer,只有一层滚动锁,一切正常。

修复只需要两行:先关 Drawer,再延迟打开 Modal。

const handleOpenUpgradeModal = () => {
  onDrawerClose();
  setTimeout(() => {
    onUpgradeModalOpen();
  }, 250);
};

250ms 足够让 Drawer 的关闭动画和滚动锁释放完成。


但在找到这个真正原因之前,我们已经在组件内部前后折腾了 7 次。

下面完整记录这个过程——不只是为了分享最终答案,也为了记录这种"在错误方向上越走越深"的排查体验,以及最后如何跳出来的。


经过

最开始的方案很典型:

<Modal
  scrollBehavior={isMobile ? 'outside' : 'inside'}
  size={{ base: 'full', md: 'xl' }}
>
  <ModalContent
    maxH={isMobile ? '100vh' : '80vh'}
    display="flex"
    flexDirection="column"
  >
    <ModalHeader>...</ModalHeader>

    <ModalBody
      flex="1"
      overflowY={isMobile ? 'visible' : 'auto'}
    />

    <ModalFooter>...</ModalFooter>
  </ModalContent>
</Modal>

设计思路:

  • 桌面端:scrollBehavior="inside",ModalBody 内部滚动
  • 移动端:scrollBehavior="outside",整个弹窗滚动,overflow-y: visible

理论上没问题,但实际结果是:iOS 上弹窗完全无法向下滚动。


一次次次的尝试

尝试 1:把 overflow: clip 改成 overflow: hidden

怀疑 Chakra 在移动端给 ModalContent 注入的:

overflow: clip;

是罪魁祸首。iOS 对 clip 的支持历来诡异,于是改成:

overflow: hidden;

结果:❌ 无效。

cliphidden 本质上都会裁掉子元素的滚动区域,换汤不换药。


尝试 2:阻止背景滚动干扰

发现 blockScrollOnMount={!isMobile} 意味着移动端允许 body 滚动,而 iOS Safari 很容易把触摸事件透传给背景页面。于是改成:

blockScrollOnMount={true}

同时:

  • 高度从 100dvh 改回 100%
  • 去掉 h={0}
  • overscroll-behavior: contain 替代 touch-action: pan-y

结果:❌ ❌ 仍然无效。

背景滚动确实被阻止了,但弹窗内部还是无法滚动。


尝试 3:直接移除 overflow: hidden

既然 overflow: hidden 会裁掉滚动区域,那直接删掉。

结果:❌ ❌ ❌还是不行。

ModalContent 失去了高度约束,内容把容器直接撑开,maxH 失效,ModalBody 永远不会进入滚动状态。也就是说:overflow 必须存在,否则无法形成真正的滚动容器。


尝试 4:完全交给 Chakra

放弃手写布局,删除 display="flex"flex="1"minH={0}overflowY,只保留:

<Modal scrollBehavior="inside">
  <ModalContent maxH="100dvh">
    ...
  </ModalContent>
</Modal>

希望 Chakra 自己处理。

结果:❌ ❌ ❌ ❌ 依旧失败。

Chakra 的内部实现本质仍然是:

.ModalContent { overflow: hidden; }
.ModalBody    { overflow: auto;   }

父级 hidden + 子级 auto,还是嵌套 overflow,绕不过去。


尝试 5:去掉 scrollBehavior,恢复手动 flex 布局

意识到 Chakra 的滚动逻辑和手写 flex 布局是两套系统在互相冲突,于是:

  • 去掉 scrollBehavior
  • 恢复手动布局
<ModalContent display="flex" flexDirection="column">
  <ModalBody flex="1" minH={0} overflowY="auto" />
</ModalContent>

结果:❌ ❌ ❌ ❌ ❌ 还是失败。

只要父元素存在 overflow: hidden,iOS 就会把子元素的滚动区域裁掉。


尝试 6:overflow: auto 改成 overflow: scroll

怀疑 iOS 对 overflow: auto 的识别不稳定,改成 overflow-y: scroll

结果:❌ ❌ ❌ ❌ ❌ ❌ 无效。

问题根本不在 autoscroll 的区别,而在于父元素的 overflow 已经让子元素的滚动区域彻底失效,改什么值都没用。


尝试 7:单一滚动容器 + sticky

彻底换思路:

  • 不再使用嵌套滚动
  • 整个 ModalContent 自己滚动
  • Header / Footer 用 position: sticky 固定
  • ModalBody 不参与任何 overflow 计算
<Modal
  blockScrollOnMount={true}
  size={{ base: 'full', md: 'xl' }}
>
  <ModalOverlay />
  <ModalContent
    maxH={isMobile ? '100dvh' : '80vh'}
    overflowY="auto"
    sx={{
      WebkitOverflowScrolling: 'touch',
      overscrollBehavior: 'contain',
    }}
  >
    <ModalHeader
      position="sticky"
      top={0}
      zIndex={1}
      bg={modalBg}
    >
      ...
    </ModalHeader>

    <ModalBody>
      {/* 普通内容,不参与滚动计算 */}
    </ModalBody>

    <ModalFooter
      position="sticky"
      bottom={0}
      zIndex={1}
      bg={modalBg}
    >
      ...
    </ModalFooter>
  </ModalContent>
</Modal>

结果:✅ 成功一半。iOS Safari 可以正常滚动。


分析

问题的本质是:iOS Safari 对嵌套 overflow 的支持非常差。

典型失败结构:

.parent { overflow: hidden; }
.child  { overflow: auto;   }

桌面浏览器完全正常,但在 iOS 上,父元素的 overflow: hidden 会直接裁掉子元素的滚动区域,overflow-y: scroll 也毫无作用——因为 iOS 根本识别不到子元素存在可滚动区域。

Chakra UI 的 scrollBehavior="inside" 底层就是这个结构,GitHub 上也有相关 issue(#5889#6131),本质是同一类嵌套 overflow 兼容问题。

方案的核心:只保留一个滚动容器。

.ModalContent { overflow-y: auto; }

Header / Footer 通过 position: sticky 实现固定,不再参与任何 overflow 计算。这样:

  • 没有嵌套 overflow
  • 没有 flex 高度竞争
  • 不依赖 iOS 对复杂滚动层级的兼容

这是目前移动 Web 上最稳定的方案之一,不只适用于 Chakra,对任何 UI 框架都通用。


根因:Drawer + Modal 双层滚动锁

多次试错之后,弹窗内滚动确实修好了——但只是修好了从 Header 打开的情况。

从个人中心的"VIP特权"按钮打开,依然无法滚动。

两个入口,同一个组件,不同结果。这说明问题不在组件内部。

排查发现:个人中心是一个全屏 Drawer,点击"VIP特权"时 Drawer 还处于打开状态。

iOS WebKit 的机制是:每次打开一个需要滚动锁的覆盖层(Drawer、Modal、Sheet),都会对 body 施加一次滚动锁。两层叠加时,iOS 无法正确识别内层 Modal 的可滚动区域,触摸事件被外层 Drawer 的滚动锁拦截,内部滚动完全失效。

从 Header 打开没有这个问题,因为外层没有 Drawer,只有一层滚动锁。

这才是真正的根因。前面 7 次在组件内部的折腾,方向全都错了——锅根本不在 VipUpgradeModal 本身。

修复方式:先关 Drawer,再延迟打开 Modal。

const handleOpenUpgradeModal = () => {
  onDrawerClose();
  setTimeout(() => {
    onUpgradeModalOpen();
  }, 250); // 等待 Drawer 关闭动画和 body 滚动锁释放
};

250ms 足够让 Drawer 的关闭动画和滚动锁释放完成。加上前面对 VipUpgradeModal 内部的 overflow 修复(单一滚动容器 + sticky),两个问题都彻底解决。


技术总结

尝试 方向 结果
1 overflow: clip → hidden
2 blockScrollOnMount={true}
3 移除 overflow: hidden
4 完全交给 Chakra scrollBehavior
5 去掉 scrollBehavior,恢复手动 flex
6 overflow: auto → scroll
7 单一滚动容器 + sticky ✅(但只解决了一半)
真正修复 先关 Drawer,再延迟打开 Modal
  1. iOS Safari 上,不要嵌套 overflow。 让滚动只发生在一层,用 sticky 固定头尾,内容区只负责展示。
  2. iOS 上不要叠加滚动锁。 Drawer 还开着就打开 Modal,两层滚动锁会让内部滚动完全失效。出现"同一组件不同入口表现不一样"时,先怀疑外层容器,而不是组件本身。