• 【js】再谈移动端的模态框实现


      移动端模态框的机制因为与PC的模态框机制一直有所区别,一直是许多新人很容易踩坑的地方,最近笔者作为一条老咸鱼也踩进了一个新坑中,真是平日里代码读得太粗略,故而写上几笔,以儆效尤。

      故事的起因是这样的,兄弟团的童鞋的页面出现了模块框内需要滚动元素的需求,但是实际情况是他调试了很久,却没有找到确定的解决问题,这也引起了笔者的注意,虽然有现成的组件,但是因为相关代码是有一些历史的了,并没有迁移,于是笔者就和他以前联调了一番。

      我们知道常见的pc端模块框阻止滚动的方式是在html或者body标签上添加overflow:hidden,以及margin:0等来实例上将页面置为一个不可滚动的页面,而在移动端,则需要我们手动的阻止相关dom的touchmove事件的冒泡,来达到目的,示意代码如下:

    /*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依然不能滚动,经过笔者和他仔细的比对之后,发现基本上只有一行代码的不同:

    /*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内部的实现机制了。。

      于是读了下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事件,事件触发的层级关系变成了:

    /*dom 示意*/
    window  //iscroll 处理touchmove
    -html
    -body
    --modal
    ---overlay //阻止事件冒泡
    ---modal-content //阻止事件冒泡
    ----iScrollElement

      我们需要等待事件冒泡到了window上,才能正常的处理iscrollElement的touchmove行为,看到这里。。笔者内心也是深感“这是一个何其大的大乌龙啊”。。不过也是因为平日中太过偏重解决问题,而没有仔细研究解决问题的方法的原理与机制。

      虽然各司其职是现代化大分工的基本诉求,但是有的时候知其所以然才能更有价值的提高我们的工作效率,对于我们解决实际问题,也是颇有裨益的。

    【2017.09.27】更新

      最近笔者的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时代,反而弄巧成拙。

  • 相关阅读:
    什么是内存(一):存储器层次结构
    关于跨平台的一些认识
    适合小白/外行的git与github最基础最浅显教程
    Android动画(二)-属性动画
    Android动画(一)-视图动画与帧动画
    View学习(四)-View的绘制(draw)过程
    View学习(三)- View的布局(layout)过程
    View学习(二)-View的测量(measure)过程
    View学习(一)-DecorView,measureSpec与LayoutParams
    wcf的诡异问题
  • 原文地址:https://www.cnblogs.com/mfoonirlee/p/7226613.html
Copyright © 2020-2023  润新知