浏览器特性深度解析:从兼容性到性能优化实战

FSD-云超 移动 阅读 1,284
赞 6 收藏
二维码
手机扫码查看
反馈

先看效果,再看代码

最近在搞一个移动端的滑动菜单,用户手指一划,侧边栏就弹出来。本来以为用个简单的 touchstart + touchmove 就搞定了,结果在 iOS Safari 上直接翻车——页面上下滚动被锁死了,手指一滑整个页面卡住不动。折腾了大半天才发现,是浏览器默认的触摸行为在捣鬼。

浏览器特性深度解析:从兼容性到性能优化实战

后来我改用 passive: false 配合 preventDefault(),才把问题解决。但你以为这就完了?不,不同浏览器对这个特性的支持还不一样,Chrome 和 Safari 的表现能让你怀疑人生。今天就来聊聊我在移动端处理浏览器触摸行为时踩过的坑,以及亲测有效的解决方案。

核心代码就这几行

先上最基础的代码,监听 touchmove 并阻止默认行为:

document.addEventListener('touchmove', (e) => {
  e.preventDefault();
}, { passive: false });

注意:必须加上 { passive: false },否则在 Chrome 56+ 和 iOS Safari 11.3+ 上,preventDefault() 会被直接忽略,毫无作用。我一开始就是漏了这个,调试到凌晨两点,差点以为是手机坏了。

但别急着全站加这个监听!如果你在页面任何地方都阻止 touchmove,默认的滚动、缩放、下拉刷新全都会失效。所以更合理的做法是:只在特定区域(比如弹窗、滑动菜单)里阻止默认行为。

比如下面这个侧滑菜单的实现:

<div id="sidebar" class="sidebar">
  <!-- 菜单内容 -->
</div>
const sidebar = document.getElementById('sidebar');

sidebar.addEventListener('touchmove', (e) => {
  // 只阻止 sidebar 内部的滚动穿透
  e.preventDefault();
}, { passive: false });

这样,用户在 sidebar 上滑动时不会触发页面滚动,但其他区域依然可以正常滚动。亲测有效,iOS 和 Android 都跑通了。

又踩坑了,touchmove滚动失效

但事情没那么简单。有一次我在一个 modal 弹窗里加了这个逻辑,结果发现:在 iOS 上,modal 内部的内容根本不能滚动!明明我只在 modal 外层容器上加了 preventDefault(),怎么连内部滚动都挂了?

查了半天才知道,iOS 的 WebKit 有个“智能”行为:一旦你阻止了某个 touchmove 事件,它会认为整个页面都不该滚动,连子元素的滚动也一并禁掉。这逻辑简直反人类。

解决方案是:只在用户试图“滚动穿透”时才阻止默认行为。判断方式很简单——如果当前滚动容器已经到顶或到底,再继续滑动就属于“穿透”,这时候才需要 preventDefault()

function preventScrollThrough(element) {
  element.addEventListener('touchmove', (e) => {
    const target = e.target;
    let scrollableParent = null;

    // 向上查找可滚动的父元素
    let current = target;
    while (current && current !== document.body) {
      const overflowY = window.getComputedStyle(current).overflowY;
      if (overflowY === 'auto' || overflowY === 'scroll') {
        scrollableParent = current;
        break;
      }
      current = current.parentElement;
    }

    if (!scrollable_parent) {
      e.preventDefault();
      return;
    }

    const { scrollTop, scrollHeight, clientHeight } = scrollableParent;
    const isAtTop = scrollTop === 0;
    const isAtBottom = scrollTop + clientHeight >= scrollHeight;

    const deltaY = e.deltaY;
    if ((isAtTop && deltaY < 0) || (isAtBottom && deltaY > 0)) {
      e.preventDefault();
    }
  }, { passive: false });
}

// 使用
const modal = document.querySelector('.modal');
preventScrollThrough(modal);

这段代码虽然有点长,但它能精准判断是否该阻止滚动。我在多个项目里用过,基本没再出问题。不过注意:性能上要小心,别在频繁触发的事件里做太多 DOM 查询,必要时可以缓存滚动容器。

踩坑提醒:这三点一定注意

  • 别全局加 preventDefault():除非你真的想禁用整个页面的滚动(比如全屏游戏),否则只在特定区域使用。否则用户会骂你。
  • Safari 的 passive 默认值是 true:这意味着如果你不显式设为 false,preventDefault() 在 Safari 上完全无效。Chrome 从 56 开始也这么干了,所以现在所有现代浏览器都得手动关 passive。
  • 别忘了移除监听器:如果你在 React/Vue 组件里加了监听,记得在组件卸载时移除,否则内存泄漏等着你。我之前就因为没移除,导致 modal 关闭后页面还是不能滚动。

谁更灵活?谁更省事?

其实除了手动监听 touchmove,还有更省事的办法。比如 CSS 的 overscroll-behavior

.sidebar {
  overscroll-behavior: contain;
}

这个属性可以让滚动在容器边界停止,不会“透传”到父级。亲测在 Chrome for Android 和新版 Safari 上有效,但 iOS 15 以下基本不支持。所以如果你的用户大部分是新机型,可以直接用这个,一行 CSS 搞定,比 JS 简单多了。

但现实很骨感——我们项目里还有不少 iOS 12 的用户,所以最后还是得 fallback 到 JS 方案。建议:优先用 overscroll-behavior,再用 JS 兜底。

if ('overscrollBehavior' in document.documentElement.style) {
  // 支持就用 CSS
  sidebar.style.overscrollBehavior = 'contain';
} else {
  // 不支持就用 JS
  preventScrollThrough(sidebar);
}

结尾:这个技术的拓展用法还有很多

其实浏览器的触摸行为控制还能用在很多地方:比如防止下拉刷新、禁用双指缩放、自定义手势识别等。我最近就在做一个画板应用,需要完全接管 touch 事件,连双指缩放都得自己实现。

以上是我踩坑后的总结,希望对你有帮助。如果你有更好的方案,或者遇到更奇葩的兼容性问题,欢迎评论区交流。这个技巧的拓展用法还有很多,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论