移动端模态框的机制由于与PC的模态框机制一直有所区别,一直是许多新人很容易踩坑的地方,最近笔者做为一条老咸鱼也踩进了一个新坑中,真是平日里代码读得太粗略,故而写上几笔,以儆效尤。html
故事的原由是这样的,兄弟团的童鞋的页面出现了模块框内须要滚动元素的需求,可是实际状况是他调试了好久,却没有找到肯定的解决问题,这也引发了笔者的注意,虽然有现成的组件,可是由于相关代码是有一些历史的了,并无迁移,因而笔者就和他之前联调了一番。android
咱们知道常见的pc端模块框阻止滚动的方式是在html或者body标签上添加overflow:hidden,以及margin:0等来实例上将页面置为一个不可滚动的页面,而在移动端,则须要咱们手动的阻止相关dom的touchmove事件的冒泡,来达到目的,示意代码以下:git
/*html code*/ <div class='modal'> <div class="overlay" id="overlayId"></div> <div class='modal-content'id='YourModalContentId'></div> </div> /*js code*/ function addEvt(dom){ dom && dom.addEventListener('touchmove', onTouchMove); } function onTouchMove(e){ e.preventDefault(); } function fn(){ let overlayDom = document.querySelector('#overlayId'); let modalDom = document.querySelector('#YourModalContentId'); }
阻止全部手指能够碰触的元素的touchmove事件冒泡(以避免引发好比在微信中的view滚动,也可避免不能触发click事件,由于click事件不须要touchmove,只须要touchstart和touchend),这是其中的原理,而后实际例子稍微复杂了一点,由于实际场景须要modalcontent内部的dom滚动,通常作法是引入iscroll用touchmove事件来模拟滚动事件,可是这位童鞋作了常规操做以后获得了不一样的结论,里面的dom依然不能滚动,通过笔者和他仔细的比对以后,发现基本上只有一行代码的不一样:github
/*html code*/ <div class='modal'> <div class="overlay" id="overlayId"></div> <div class='modal-content'id='YourModalContentId'> <ul> ... </ul> </div> </div> /*js code*/ function addEvt(dom){ dom && dom.addEventListener('touchmove', onTouchMove); } function onTouchMove(e){ e.preventDefault(); e.stopPropagation(); } function fn(){ let overlayDom = document.querySelector('#overlayId'); let modalDom = document.querySelector('#YourModalContentId'); let scroller = new IScroll(modalDom, YourOptions); }
就是上面标红的那句话,可是正常状况下stopPropagation才是阻止事件冒泡,笔者开始也觉得应该是没有关系的,可是通过反复测试后发现。。没有那句话,内部dom的滚动没有任何问题,可是有了那句话以后,内部则不能滚动了。。细细思考以后,笔者觉着。。多半是iscroll内部的实现机制了。。web
因而读了下iscroll的源码,发现iscroll在initEvents时作了一个神奇的操做:微信
_initEvents: function(remove) { var eventType = remove ? utils.removeEvent : utils.addEvent, target = this.options.bindToWrapper ? this.wrapper : window; eventType(window, 'orientationchange', this); eventType(window, 'resize', this); if (this.options.click) { eventType(this.wrapper, 'click', this, true); } if (!this.options.disableMouse) { eventType(this.wrapper, 'mousedown', this); eventType(target, 'mousemove', this); eventType(target, 'mousecancel', this); eventType(target, 'mouseup', this); } if (utils.hasPointer && !this.options.disablePointer) { eventType(this.wrapper, 'MSPointerDown', this); eventType(target, 'MSPointerMove', this); eventType(target, 'MSPointerCancel', this); eventType(target, 'MSPointerUp', this); } if (utils.hasTouch && !this.options.disableTouch) { eventType(this.wrapper, 'touchstart', this); eventType(target, 'touchmove', this); eventType(target, 'touchcancel', this); eventType(target, 'touchend', this); } eventType(this.scroller, 'transitionend', this); eventType(this.scroller, 'webkitTransitionEnd', this); eventType(this.scroller, 'oTransitionEnd', this); eventType(this.scroller, 'MSTransitionEnd', this); }
在适用方没有强制绑定wrapper的状况下,touchstart、touchmove、touchend的target都是window!看到这里聪明的你也许已经反应过来了,这就是为何咱们日常写到touch事件的代码在移动出了dom的范围以后不能正常的释放,而iscroll的能够。。由于除了touchstart以外,其余的事件都是加在全局的window对象上的,而咱们遇到的这个实际问题又偏偏使用了touchmove事件,事件触发的层级关系变成了:antd
/*dom 示意*/ window //iscroll 处理touchmove -html -body --modal ---overlay //阻止事件冒泡 ---modal-content //阻止事件冒泡 ----iScrollElement
咱们须要等待事件冒泡到了window上,才能正常的处理iscrollElement的touchmove行为,看到这里。。笔者心里也是深感“这是一个何其大的大乌龙啊”。。不过也是由于平日中太过偏重解决问题,而没有仔细研究解决问题的方法的原理与机制。app
虽然各司其职是现代化大分工的基本诉求,可是有的时候知其因此然才能更有价值的提升咱们的工做效率,对于咱们解决实际问题,也是很有裨益的。框架
【2017.09.27】更新dom
最近笔者的android系统更新到了7.0...而后发现以上的通用移动端滚动模态窗的解决方案失效了。。问题彷佛出在preventDefault再也不按照咱们指望的方式工做了。。因而须要更新一下实现:
因为内部的滚动必定会传播到上层,那么解决思路就只能是将上层的滚动条件完全移除了,即须要在模态框展现的时候先标记当前的window.scrollY(这里咱们只考虑上层只有一个滚动条的状况),而后直接设置body的样式,将其overflow设置为hidden,物理上禁止上层容器的滚动;
而后,对于模态框上的滚动容器咱们再也不须要使用touch事件模拟滚动了,能够直接使用原生的滚动条;
最后,在模态框消失的时候,咱们须要手动还原window的滚动条为以前的状态(注意这里其实会有一些体验问题,可是咱们可使用先还原滚动条状态再隐藏模态框的hack来避免体验问题)。
【2017.11.08】更新
最近笔者在参考其余框架实现的时候发现,支付宝的antd-mobile并无笔者以前在android所遇到的问题,研究代码发现,由于antd引用了另外一个滚动的实现——scroller,再往下看了下实现,发现是由于iscroll的touch事件识别虽然是放在传入的容器上到,可是具体的行为为了体验的优化,却将touchmove和touchend事件添加到了window(如前文所述),而android 7彷佛对这种操做的支持并非很友好,致使了touchmove事件最早响应在了外层容器。。致使本来容器内部滚动的事件不能被正确处理。
其实,没想到iscroll原本一个为了优化体验而想出的小技巧,到了android 7时代,反而弄巧成拙。