移動端模態框的機制由於與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時代,反而弄巧成拙。