在维护基于 React Native Web 的 h5 项目时,遇到这样的一个 bug,在部分安卓 h5 页面一些点击事件会稳定触发两边,这样的问题在 iOS 和 pc 调试 h5 都是好的,甚至同一机型只要另外换一个浏览器可能也会有不同的效果。
在调试阶段,发现只要把 TouchableHighlight
等组件都干掉,换成 div,点击事件也从 onPress
改到 onClick
,就全部是好的,不过由于是三端项目,需要 TouchableHighlight
这些 React Native 组件,当然不能这么改了了事,不过也给我定位到可能是 React Native Web Touchable
类组件的 bug
定位到是这个问题,很快可以找到对应问题 issue 以及 RNW 作者的回复和解决 链接
根据 issue 里大家的讨论结合后面作者 necolas 的修复改动,我们可以加一些 console 来看下引起问题的原因
首先在有问题的 onPress 函数里添加 console.log('触发 onPress 函数')
然后在 node_modules/react-native-web/dist/modules/ResponderEventPlugin/index.js
找到以下的代码(在备注的地方加入console)
ResponderEventPlugin.extractEvents = function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
var hasActiveTouches = ResponderTouchHistoryStore.touchHistory.numberActiveTouches > 0;
var eventType = nativeEvent.type;
// 在这加 console
console.log('responder >>> ', topLevelType, Date.now() - lastActiveTouchTimestamp)
var shouldSkipMouseAfterTouch = false;
if (eventType.indexOf('touch') > -1) {
lastActiveTouchTimestamp = Date.now();
} else if (lastActiveTouchTimestamp && eventType.indexOf('mouse') > -1) {
var now = Date.now();
shouldSkipMouseAfterTouch = now - lastActiveTouchTimestamp < 250;
}
if (
// Filter out mousemove and mouseup events when a touch hasn't started yet
(eventType === 'mousemove' || eventType === 'mouseup') && !hasActiveTouches ||
// Filter out events from wheel/middle and right click.
nativeEvent.button === 1 || nativeEvent.button === 2 ||
// Filter out mouse events that browsers dispatch immediately after touch events end
// Prevents the REP from calling handlers twice for touch interactions.
// See #802 and #932.
shouldSkipMouseAfterTouch) {
return;
}
var normalizedEvent = normalizeNativeEvent(nativeEvent);
return originalExtractEvents.call(ResponderEventPlugin, topLevelType, targetInst, normalizedEvent, nativeEventTarget);
};
然后我们看一下效果
可以看到 onPress 确实触发了两次,且第一次在 touchend
之后,第二次在 mouseup
之后,所以我们可以合理推测 React Native Web 的 Touchable 组件将 onPress 应该就是在 touchend
和 mouseup
都绑定上了
那为何只有特定安卓 h5 会有这个问题呢?照上面的推论,那应该每个设备都有这个问题
我们可以换至不会触发问题的浏览器下看一下效果(在我的 onePlus 6T 下只有微信环境稳定复现,其他浏览器都不会有问题)
似乎差不多不过就是没有触发两次 onPress,让我们再来看一下源码的实现
简而言之就是,now - lastActiveTouchstamp < 250
时,就可以使 shouldSkipMouseAfterTouch = true
,从而跳过绑定在 mouse 上的 onPress 事件,从而避免触发两次的情况
而我们在前面 console 后面加上的数字就是 now - lastActiveTouchstamp
的值,可以看到问题浏览器下,该值均在 320+,不在 250ms 的安全阈值内,无法跳过绑定在 mouse 上的 onPress 事件,因此触发两次 onPress。
找到引起问题的原因,就可以需要找到一些解决方法了,下面我抛砖引玉,提供三种解决方案的思路
在 0.11.7+ 的版本,RNW 作者将安全阈值更新到了 1000ms,这就可以解决绝大多数的浏览器问题。(当然了在公司某台巨卡测试机上 仍有问题,根据上面的调试,测出来的相距时间达到了 1200ms+)
commit 信息
不过目前项目已经很难做依赖库升级的情况下,我们可以利用 npm 的 postinstall 钩子函数,写一些脚本在 npm i
后执行,人为修改本地 node_modules
里的文件
举个例子,可以利用 sed
命令来处理
sed -i '' "s/250/1500/g" ./node_modules/react-native-web/dist/modules/ResponderEventPlugin/index.js
这种方式很灵活,可以灵活改动安全阈值,比如我就把 250 改到了 1500
移動端瀏覽器 :當 Touch Event 與 Mouse Event 同時存在的時候
从上面的文章可以知道,我们可以在 touchend
handler 中执行 event.preventDefault()
去取消 mouse event 的发送
也就是说,我们可以在有问题的 onPress 里加上 event.preventDefault()
就可以了(上面文章打不开的,网上还有其他很多优质博客,这里不再赘述)