移动端有哪些常见兼容问题?日期解析、点击延迟、点击穿透和滚动穿透怎么处理?
移动端高频兼容坑合集:'2026-10-30 12:30' 这种非标准日期字符串在部分浏览器里得到 Invalid Date;双击缩放带来的 300ms 点击延迟和由它引出的点击穿透;弹窗里滚到底后背景页面跟着滚;输入框被键盘遮挡。逐个讲现象、原因和官方推荐的改法。
简答
最常被问到的是日期。后端返回
'2026-10-30 12:30',直接new Date()在有的浏览器里能用,换个浏览器就是 Invalid Date。原因是规范只保证2026-10-30T12:30这种带T的格式,中间用空格的写法属于非标准格式,怎么解析由浏览器自己决定。稳妥的办法是自己拆出年月日时分,再用new Date(年, 月 - 1, 日, 时, 分)构造。第二类是点击。早期移动浏览器在
touchend之后要等大约 300 毫秒才发click,因为要看用户是不是在双击缩放。现在页面写了<meta name="viewport" content="width=device-width">,Chrome 32 起和 iOS 9.3 起都不再等。这个延迟还会引出点击穿透:在touchend里关掉遮罩,稍后补发的click落到了遮罩下面的元素上。第三类是滚动穿透:弹窗里的列表滚到底,再往下滑,后面的页面跟着滚了。这叫滚动链,可以给弹窗的滚动容器写
overscroll-behavior: contain截住。最后是输入框被键盘挡住,要按可视视口去调整布局。这些问题的表现和机型、浏览器版本有关,结论一定要在 iOS 和 Android 真机上验证。
为什么同一个日期字符串有的手机能解析,有的不行
ECMAScript 只规定了一种必须支持的日期时间格式,就是 toISOString() 输出的那种,例如 2026-10-30T12:30:00。MDN 写得很明确:带时间时,中间的 T 是必需的;不符合这个格式的字符串,引擎可以按任意方式解析,包括时区怎么算。所以 '2026-10-30 12:30'、'2026/10/30' 这类写法能不能解析、解析成哪个时刻,各个浏览器可以不一样,解析失败就得到 Invalid Date,getTime() 返回 NaN。
即使用了标准格式,还有一个时区的坑:只有日期、没写时区时按 UTC 解释,带了时间、没写时区时按本地时间解释。MDN 说这是一个历史上的规范错误,为了兼容已有网页没法改。下面两行是同一个时刻,因为纯日期被当成了 UTC 零点:
console.log(Date.parse('2019-01-01')); // 1546300800000
console.log(Date.parse('2019-01-01T00:00:00Z')); // 1546300800000
所以最省心的做法是不要把非标准字符串交给 Date 去猜。下面自己拆出各个数字,用多参数的 Date 构造函数创建本地时间,这条路径在所有环境里行为一致:
function parseLocalDateTime(text) {
const [datePart, timePart = '00:00'] = text.split(' ');
const [year, month, day] = datePart.split('-').map(Number);
const [hour, minute, second = 0] = timePart.split(':').map(Number);
return new Date(year, month - 1, day, hour, minute, second);
}
const time = parseLocalDateTime('2026-10-30 12:30');
console.log(time.getFullYear()); // 2026
console.log(time.getMonth() + 1); // 10
console.log(time.getHours()); // 12
读者可能会问月份为什么要减 1。Date 构造函数的月份从 0 开始,0 表示一月,所以传进去要减 1,读出来要加 1。另外,如果字符串表示的是服务器所在时区或 UTC 的时间,就不能当本地时间构造,最好让后端直接返回带时区的 ISO 字符串或时间戳。
300ms 点击延迟是怎么来的,现在还有吗
手机浏览器支持“双击放大文字”。用户点了一下,浏览器不知道第二下会不会马上跟来,只好在 touchend 之后等一会儿再发 click。Chrome 的文章说这段等待大约 300 到 350 毫秒,WebKit 的文章给出的是 350 毫秒。
后来浏览器的思路是:页面已经针对手机适配了,就不需要双击放大。Chrome 32 起,只要页面写了 width=device-width 的 viewport,就去掉这段延迟,同时保留双指缩放;WebKit 在 iOS 9.3 做了类似的修改,禁止缩放的页面同样不再延迟。如果没法改 viewport,可以给元素写 touch-action: manipulation,它只允许平移和双指缩放,关掉了双击缩放,浏览器也就不用等了。
<meta name="viewport" content="width=device-width, initial-scale=1">
所以今天遇到“点击慢半拍”,先检查 viewport 有没有写对,而不是急着引入 FastClick 这类旧库。Chrome 的文章也提到,给所有元素绑 touchstart 监听会拖慢滚动。
点击穿透是怎么发生的
点击穿透和上面的延迟是同一个来源。MDN 说明,只有一个触点时,浏览器通常会在触摸事件之后补发模拟的鼠标事件和 click。假设遮罩层在 touchend 里就把自己隐藏了,等浏览器补发 click 时,手指位置下面已经换成了遮罩后面的按钮或链接,于是它被意外点中了。
改法有两种。第一种是统一用 click 处理关闭,这样关闭和点击是同一个事件,不会再多出一个。第二种是确实要在触摸事件里处理时,调用 event.preventDefault(),MDN 说明这样能阻止浏览器补发模拟的鼠标事件。同时处理鼠标和触摸的交互,MDN 建议改用 Pointer Events,见《Pointer Events 为什么适合统一鼠标、触摸和触控笔交互?》。
弹窗里滚动,为什么后面的页面也跟着滚
MDN 把这种现象叫作滚动链:内层滚动容器滚到边界以后,继续滑动会交给外层容器,于是背景页面开始滚。overscroll-behavior 就是用来控制这一点的,contain 表示滚动不传到容器外面,但容器自己还会有回弹效果;none 连回弹也一起去掉。
.dialog-body {
overflow: auto;
overscroll-behavior: contain;
}
还有一个容易漏掉的情况:弹窗内容不够长、根本不能滚动时,MDN 说这样的容器始终被视为处在滚动边界上,滑动会直接交给外层。MDN 给出的做法是让遮罩成为 overflow: hidden 的滚动容器,同时设置 overscroll-behavior: contain,这样打开弹窗时背景就不会滚。MDN 目前把 overscroll-behavior 标为尚未进入 Baseline,所以目标浏览器的支持情况要查兼容表并真机测试。
横滑卡片和页面纵向滚动抢手势是另一类问题,见《移动端手势交互怎样避免和页面滚动冲突?》。
输入框被键盘挡住怎么办
键盘弹出时,多数移动浏览器只缩小可视视口,布局视口不变,固定在底部的元素就落到键盘下面。处理方法是监听 visualViewport 的变化,或者用 interactive-widget、VirtualKeyboard API,细节和各方案的支持范围见《移动端虚拟键盘出现时怎样避免布局被遮挡?》。
常见追问
为什么 new Date('2026-10-30') 在国内用户的手机上显示成早上 8 点?
因为只有日期、没有时区的标准格式按 UTC 零点解释。北京时间比 UTC 快 8 小时,换成本地时间显示就是早上 8 点。想要本地零点,要么写成带时间的
2026-10-30T00:00,要么用new Date(2026, 9, 30)。
现在还需要 FastClick 吗?
一般不需要。Chrome 32 起和 iOS 9.3 起,写了
width=device-widthviewport 的页面已经没有点击延迟。FastClick 靠给元素绑touchstart实现,Chrome 的文章提到这类监听会影响滚动性能,所以先检查 viewport 配置。
overscroll-behavior 的 contain 和 none 怎么选?
两者都能阻止滚动传到外层。区别在回弹:
contain保留容器自己的回弹效果,none连回弹也去掉。弹窗一般用contain就够了,需要完全禁止边缘回弹时才用none。