Hammer.js手势库在移动端项目中的实战应用与常见坑点总结

FSD-开心 交互 阅读 1,509
赞 19 收藏
二维码
手机扫码查看
反馈

谁更灵活?谁更省事?

最近在做一个移动端拖拽排序组件,需求看着简单:手指按住卡片、拖动、松手后更新顺序。但实际一上手就发现——touch事件原生写起来真他妈累。preventDefault反复加、touchstart/touchmove/touchend状态管理混乱、iOS Safari里还容易触发页面滚动……我折腾了两天,最后还是决定拉几个方案出来对比一下,别再靠试错推进了。

Hammer.js手势库在移动端项目中的实战应用与常见坑点总结

这次主要对比三个方案:纯原生 touch 事件Hammer.js(v2.0.8,目前最稳的稳定版)、Pointer Events + @use-gesture/react(React 场景下用)。没选 React DnD 或 Vue Draggable,因为它们太重,且底层还是封装 touch/pointer,不解决本质问题;也没提 iOS 的 WKWebView 兼容性——那个坑我早放弃了,单独开篇说。

原生 touch:能跑,但像手动拧螺丝

我第一版就是纯 touch 写的,代码看着干净:

let startY = 0;
let isDragging = false;

element.addEventListener('touchstart', (e) => {
  startY = e.touches[0].clientY;
  isDragging = true;
  e.preventDefault();
});

element.addEventListener('touchmove', (e) => {
  if (!isDragging) return;
  const deltaY = e.touches[0].clientY - startY;
  // 更新 transform
  element.style.transform = translateY(${deltaY}px);
  e.preventDefault(); // 必须加,否则 iOS 滚动会抢走
});

element.addEventListener('touchend', () => {
  isDragging = false;
  // 回弹 or 提交排序
});

问题在哪?就三处让我改了五次

  • iOS Safari 下,touchmove 里不加 e.preventDefault(),页面会跟着拖动;加了,又可能干掉 <input> 聚焦(比如你拖着拖着想点个搜索框);
  • 多个 touch 点位(比如误触另一根手指),e.touches[0] 突然变成 e.touches[1],坐标跳变;
  • 没有 velocity、angle、distance 这些基础手势数据,想加个“快速甩动排序”,得自己算 delta 和时间差,精度还不高。

结论:适合超轻量场景(比如单个按钮长按),但只要涉及拖拽、缩放、多点交互——原生写等于自虐。我后来把它删了,连 git 历史都 force push 掉了。

Hammer.js:老而弥坚,但有点“老”

我比较喜欢用 Hammer.js,不是因为它多新,而是它把所有坑都踩过一遍,文档里全写着怎么绕过去。v2 版本虽不维护了,但线上项目跑三年没出过手势逻辑 bug,这点比很多新库靠谱。

初始化就一行:

const mc = new Hammer.Manager(element);
mc.add(new Hammer.Pan({ threshold: 5, pointers: 1 }));
mc.add(new Hammer.Swipe({ direction: Hammer.DIRECTION_HORIZONTAL }));
mc.on('panstart panmove panend', handlePan);

关键优势有三点:

  • 自动处理 preventDefault 时机:它只在真正识别到 Pan 时才阻止默认行为,不会误杀 input;
  • 内置 velocity、direction、deltaX/Y,甩动排序一行 if (ev.velocityX > 1.5) submitSort() 就搞定;
  • 支持自定义 recognizer,比如我们加了个“双指垂直缩放识别器”,就抄官方 demo 改两行,不用从零造轮子。

但它有两个明显短板:

  • 不支持 Pointer Events,PC 端靠 mouse 事件模拟,偶尔有延迟感(尤其 Chrome 高刷屏);
  • 全局 touch-action 设置要手动配,比如 element.style.touchAction = 'none',漏了就会和浏览器滚动打架——这个我踩过三次坑,每次都要翻 issue 才想起来。

顺带一提,Hammer v2 的 CDN 是 https://cdn.jsdelivr.net/npm/hammerjs@2.0.8/hammer.min.js,别下错成 v3 beta,那个 Promise 化设计根本没法 debug。

@use-gesture/react:React 场景下的“懒人首选”

如果项目是 React,我一般选这个。不是因为它多先进,而是它把 Hammer 的能力用 React Hooks 封装得特别顺手,且天然兼容 pointer/touch/mouse

核心代码就这几行:

import { useDrag } from '@use-gesture/react';

function DraggableCard({ onDrop }) {
  const bind = useDrag(({ active, movement: [mx, my], last, velocity }) => {
    if (active) {
      element.style.transform = translate(${mx}px, ${my}px);
    }
    if (last && velocity > 0.5) {
      onDrop(mx > 100 ? 'right' : 'left');
    }
  });

  return <div {...bind()} className="card">拖我</div>;
}

优点太实在了:

  • 不用管 touch-action,它自动帮你设;
  • 鼠标、触控板、触摸屏一套逻辑跑通,测试成本直降;
  • 和 React 状态绑定自然,比如 useState 更新 drag position,不需要手动清理事件监听器。

缺点也有:非 React 项目没法用;文档例子偏少,遇到复杂嵌套手势(比如拖拽中双击放大),得翻源码看 recognizers 怎么配;另外它底层依赖 @react-spring/core,打包体积比 Hammer 大 12KB 左右——不过现在谁还在乎这 12KB?

我的选型逻辑

总结一下我的日常选择:

  • 纯静态页面 / Vue / 原生 JS 项目 → Hammer.js:稳定、小(7KB gzip)、文档全、社区 issue 丰富,遇到问题搜一下基本有解;
  • React 项目 → @use-gesture/react:开发体验好太多,Hooks 写法清爽,避免重复管理 ref 和 cleanup;
  • 只做 PC 端或对兼容性要求极低 → 原生 pointer eventelement.setPointerCapture() + gotpointercapture 监听,比 touch 简洁不少,但 iOS Safari 15 以下不支持,慎用。

至于 Hammer v3?我看了一眼,API 全 Promise 化,还强制 require TypeScript。我敬谢不敏——线上项目不是练手,能少一个 await 就少一个潜在卡顿点。

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

最后补三个我亲自踩过的雷,节省你半天时间:

  • Hammer 的 mc.destroy() 不会自动移除 DOM listener,必须手动 element.removeEventListener('touchstart', ...),否则内存泄漏;
  • position: fixed 元素上使用 Hammer,event.center.x/y 坐标不准,得自己减去 window.scrollX/Y
  • 如果用 Webpack 5+,Hammer 默认被 tree-shaking 干掉,要在 config 里加 new webpack.IgnorePlugin({ resourceRegExp: /^./gestures$/ }),或者直接 import 全量包。

以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,比如结合 IntersectionObserver 做拖拽吸附、配合 requestIdleCallback 优化大量卡片排序性能,后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

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

暂无评论