Hammer.js手势库在移动端项目中的实战应用与常见坑点总结
谁更灵活?谁更省事?
最近在做一个移动端拖拽排序组件,需求看着简单:手指按住卡片、拖动、松手后更新顺序。但实际一上手就发现——touch事件原生写起来真他妈累。preventDefault反复加、touchstart/touchmove/touchend状态管理混乱、iOS Safari里还容易触发页面滚动……我折腾了两天,最后还是决定拉几个方案出来对比一下,别再靠试错推进了。
这次主要对比三个方案:纯原生 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 event:
element.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 优化大量卡片排序性能,后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

暂无评论