zoukankan      html  css  js  c++  java
  • 【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时代,反而弄巧成拙。

  • 相关阅读:
    软件配置管理的作用?软件配置包括什么?
    火火恍恍惚惚
    什么是软件测试?软件测试的目的与原则
    软件生存周期及其模型是什么?
    试述软件的概念和特点?软件复用的含义?构件包括哪些?
    一台客户端有三百个客户与三百个客户端有三百个客户对服务器施压,有什么区别?
    numpy的broadcast是怎么做的
    python到底是解释型语言还是需要编译的?
    python:删除类实例,仅仅只有动态属性会被删除,类属性不会被删除
    jupyter的kernel莫名其妙找不到,莫名其妙就中断
  • 原文地址:https://www.cnblogs.com/mfoonirlee/p/7226613.html
Copyright © 2011-2022 走看看