跳到主要内容

移动端 tap 与 click

移动端最经典的坑是"一次点击执行了两遍"。 这一页讲清楚 Konva 在这件事上帮你挡掉了什么、什么情况下会漏过来。

click 和 tap 不会同时触发​

先说结论,因为这和很多老文章讲的不一样:

Konva 把 pointerclick 按输入类型映射成 click(鼠标) 或 tap(触摸),只会得到其中一个。

实测一次鼠标点击与一次手指轻点收到的事件序列:

鼠标: pointerdown → mousedown → pointerup → pointerclick → mouseup → click
触摸: pointerdown → touchstart → pointerup → pointerclick → touchend → tap

末尾一个是 click,另一个是 tap,没有任何一次交互同时产生两者。

所以下面这种写法是安全的,桌面和移动端都只会执行一次:

shape.on('click tap', handler); // 空格分隔,注册两个事件名

Konva 默认替你调用了 preventDefault​

浏览器有一套"兼容性鼠标事件"机制:手指轻点之后, 浏览器会补发一组合成的 mousedown / mouseup / click, 好让只写了鼠标逻辑的老页面在手机上也能用。 这正是"双重触发"的历史来源。

Konva 默认把它挡掉了。源码里在处理按下事件时:

// only call preventDefault if the shape is listening for events
const isTouch = evt.type.indexOf('touch') >= 0;
if (shape.preventDefault() && evt.cancelable && isTouch) {
evt.preventDefault();
}

节点的 preventDefault 属性默认是 true, 所以只要图形在监听事件,触摸事件就会被 preventDefault(), 浏览器的合成点击不再产生。

这意味着你把 preventDefault 关掉之后,双重触发会回来:

shape.preventDefault(false); // 合成 click 不再被抑制

而关掉它是有正当理由的——preventDefault() 会一并阻止页面滚动, 如果你的画布是页面的一部分、用户需要在画布上滑动来滚动页面, 就必须关掉它。这时候就得小心事件重复,见 移动端滚动。

dragDistance 影响轻触的判定​

Konva.dragDistance 默认 3(像素)。手指按下后移动超过这个距离, Konva 判定为拖拽,tap 就不会触发了。

手指远不如鼠标稳,按下的瞬间偏移三五个像素很正常。 默认值对触摸设备偏小,表现为"明明点了一下,却没有反应"。

Konva.dragDistance = 8; // 全局
shape.dragDistance(8); // 或者只改某个节点

调大的副作用是拖拽启动会有一点"迟钝感", 需要在"点不中"和"拖不动"之间权衡。移动端优先的应用一般设 6–10。

常见问题​

还有 300ms 点击延迟吗?​

现代浏览器基本没有了。这个延迟原本是为了等待判断是不是双击缩放, 当页面声明了 <meta name="viewport" content="width=device-width"> (即已适配移动端、不可缩放或由浏览器管理缩放)之后, Chrome 和 Safari 都已移除。

所以第一件事是确认你的页面有这个 meta 标签。

仍可能有延迟的场景是老旧的 WebView—— 一些 App 内置浏览器的内核版本很旧。如果确实遇到, 用 touchstart 代替 tap 能规避,但要自己处理"按下后滑走不应算点击"的逻辑。

为什么我在手机上点不中小按钮?​

两个原因通常同时存在:

命中区域太小。 手指的接触面积远大于鼠标指针。 细线要设 hitStrokeWidth,小图标要用 hitFunc 把命中区扩大到 至少 44×44 的 CSS 像素(这是 iOS 人机界面指南的建议值), 见自定义命中区域。

dragDistance 太小。 见上面一节。

双击在移动端怎么写?​

dbltap,对应桌面的 dblclick:

shape.on('dblclick dbltap', handler);

映射关系和单击一致——pointerdblclick 按输入类型映射成 dblclick 或 dbltap 之一。

长按怎么实现?​

Konva 没有内置长按事件,自己用定时器实现:

let timer = null;

shape.on('pointerdown', () => {
timer = setTimeout(() => { timer = null; onLongPress(); }, 500);
});
shape.on('pointerup pointercancel pointerout', () => {
clearTimeout(timer);
timer = null;
});
shape.on('click tap', () => {
if (timer === null) return; // 已经当作长按处理了,别再当点击
});

pointercancel 必须清理定时器,否则来电或系统手势打断之后, 长按回调仍然会在半秒后触发。

国内环境注意事项​

微信内置浏览器是必测环境。 安卓端微信用的是 X5 内核(基于较旧的 Chromium), 在触摸事件、字体回退、preventDefault 的生效范围上和标准 Chrome 都有差异。 iOS 端微信用的是系统 WKWebView,行为更接近 Safari。 桌面浏览器的移动端模拟器复现不出这些差异,必须真机测。

调试手段上,微信里看不到控制台,实用的办法是引入 vConsole:

<script src="https://unpkg.com/vconsole/dist/vconsole.min.js"></script>
<script>new VConsole();</script>

排查"执行了两次"这类问题时,直接在处理函数里打印 e.type 和 e.evt.pointerType,一眼就能看出是哪个事件重复了。

各家 App 的 WebView 内核版本差异很大。 如果你的画布要嵌进 第三方 App(小程序的 web-view、各类资讯 App 的内置浏览器), 不要假设它们支持指针事件。保险的写法是用 click tap 这类传统事件名, Konva 内部会把指针事件映射过来,两边都能工作。