vue中pc移動滾動穿透問題及解決
vue pc移動滾動穿透問題
上層無滾動(很簡單直接@touchmove.prevent)
<div @touchmove.prevent> 我是里面的內容 </div>
上層有滾動
如果上層需要滾動的話,那么固定的時候先獲取 body 的滑動距離,然后用 fixed 固定,用 top 模擬滾動距離;不固定的時候用獲取 top 的值,然后讓 body 滾動到之前的地方即可。
示例如下:
watch:{ statusShow(val){ if(val) { this.lockBody(); } else { this.resetBody(); } }, calendarShow(val){ if(val) { this.lockBody(); } else { this.resetBody(); } } }, methods: { lockBody() { const { body } = document; const scrollTop = document.body.scrollTop || document.documentElement.scrollTop; body.style.position = 'fixed'; body.style.width = '100%'; body.style.top = `-${scrollTop}px`; }, resetBody() { const { body } = document; const { top } = body.style; body.style.position = ''; body.style.width = ''; body.style.top = ''; document.body.scrollTop = -parseInt(top, 10); document.documentElement.scrollTop = -parseInt(top, 10); }, }
body是DOM對象里的body子節(jié)點,即 標簽;
documentElement 是整個節(jié)點樹的根節(jié)點root,即 標簽;
不同瀏覽器中,有的能識別document.body.scrollTop,有的能識別document.documentElement.scrollTop,有兼容性問題需要解決。
滑動穿透終極解決方案
問題描述
滑動穿透:浮層上的觸控會導致底層元素滑動。
問題探究
1、給body加overflow:hidden,pc端可以鎖scroll,移動端無效
pc端可以直接overflow:hidden解決
2、給body加overflow:hidden及絕對定位,背景會定位到頂部,如果是單屏頁面可以,長頁面不適用
如果彈出浮層時背景本來就沒有滾動距離,可以overflow:hidden加絕對定位解決
3、禁用touchmove事件,如@touchmove.prevent,對于彈層不需要的滑動的元素來說非常好用,因為scroll是touchmove觸發(fā)的,直接禁用就不會滑動穿透了,其實是直接就沒有系統(tǒng)滑動事件了。但是顯然不適合彈層需要滑動的情況
如果彈層時不需要滾動的,可以直接禁用touchmove就可以了
4、專門解決滑動穿透的第三方,存在巨大的兼容性問題。比如tua-body-scroll-lock,android可以完美解決,ios整個屏幕都不能滑動了。高星的body-scroll-lock據(jù)說android全掛,就沒有試了。
第三方有兼容性問題,可以自己判斷ua選用
5、終極解決方案:vant的popup
合理完美的解決方案,不存在兼容問題,適用于任何情況的popup。如果你不想為了鎖背景引入一個根本用不到的庫,可以一起來研究下popup的實現(xiàn)原理。
原理探究
如果不想看源碼想直接知道結論的話可以看這里:
因為常見會滑動穿透的場景都是:
- 子元素本來就不可滾動,在子元素上滑動引起背景滾動,
- 子元素可以滾動,但已經滾動到頂部或者底部,繼續(xù)滑動的話就會滑動穿透
所以如果子元素本身不可滾動,或者子元素氪滾動,但已經滾動到頂部或者底部時直接對touchmove進行默認事件阻止就可以阻止滑動穿透了。因為scroll事件是通過touchmove觸發(fā)的,禁止掉就不會觸發(fā)系統(tǒng)的scroll事件了。這樣就可以完美解決可滾動元素可以滾動但其背景在滑動時不為所動的效果了。
如果你想看看popup到底時如何做的可以來看看下面的源碼:
源碼分析:
src/popup/index.js文件中主要是參數(shù)及界面顯示的處理。
// src/popup/index.js import { createNamespace, isDef } from '../utils'; import { PopupMixin } from '../mixins/popup'; import Icon from '../icon'; const [createComponent, bem] = createNamespace('popup'); export default createComponent({ ? // 穿透處理的代碼在這里混入 ? mixins: [PopupMixin], ? props: { ? ? round: Boolean, ? ? duration: Number, ? ? closeable: Boolean, ? ? transition: String, ? ? safeAreaInsetBottom: Boolean, ? ? closeIcon: { ? ? ? type: String, ? ? ? default: 'cross' ? ? }, ? ? closeIconPosition: { ? ? ? type: String, ? ? ? default: 'top-right' ? ? }, ? ? position: { ? ? ? type: String, ? ? ? default: 'center' ? ? }, ? ? overlay: { ? ? ? type: Boolean, ? ? ? default: true ? ? }, ? ? closeOnClickOverlay: { ? ? ? type: Boolean, ? ? ? default: true ? ? } ? }, ? beforeCreate() { ? ? const createEmitter = eventName => event => this.$emit(eventName, event); ? ? this.onClick = createEmitter('click'); ? ? this.onOpened = createEmitter('opened'); ? ? this.onClosed = createEmitter('closed'); ? }, ? render() { ? ? if (!this.shouldRender) { ? ? ? return; ? ? } ? ? const { round, position, duration } = this; ? ? const transitionName = ? ? ? this.transition || ? ? ? (position === 'center' ? 'van-fade' : `van-popup-slide-${position}`); ? ? const style = {}; ? ? if (isDef(duration)) { ? ? ? style.transitionDuration = `${duration}s`; ? ? } ? ? return ( ? ? ? <transition ? ? ? ? name={transitionName} ? ? ? ? onAfterEnter={this.onOpened} ? ? ? ? onAfterLeave={this.onClosed} ? ? ? > ? ? ? ? <div ? ? ? ? ? vShow={this.value} ? ? ? ? ? style={style} ? ? ? ? ? class={bem({ ? ? ? ? ? ? round, ? ? ? ? ? ? [position]: position, ? ? ? ? ? ? 'safe-area-inset-bottom': this.safeAreaInsetBottom ? ? ? ? ? })} ? ? ? ? ? onClick={this.onClick} ? ? ? ? > ? ? ? ? ? {this.slots()} ? ? ? ? ? {this.closeable && ( ? ? ? ? ? ? <Icon ? ? ? ? ? ? ? role="button" ? ? ? ? ? ? ? tabindex="0" ? ? ? ? ? ? ? name={this.closeIcon} ? ? ? ? ? ? ? class={bem('close-icon', this.closeIconPosition)} ? ? ? ? ? ? ? onClick={this.close} ? ? ? ? ? ? /> ? ? ? ? ? )} ? ? ? ? </div> ? ? ? </transition> ? ? ); ? } });
根據(jù)mixins混入,可以看到核心部分應該在src/mixins/popup中,在這里針對lockscroll做出了兩種處理,綁定touchmove及touchstart并綁定class:van-overflow-hidden
// src/mixins/popup/index.js import { context } from './context'; import { TouchMixin } from '../touch'; import { PortalMixin } from '../portal'; import { on, off, preventDefault } from '../../utils/dom/event'; import { openOverlay, closeOverlay, updateOverlay } from './overlay'; import { getScrollEventTarget } from '../../utils/dom/scroll'; export const PopupMixin = { ? mixins: [ ? ? TouchMixin, ? ? PortalMixin({ ? ? ? afterPortal() { ? ? ? ? if (this.overlay) { ? ? ? ? ? updateOverlay(); ? ? ? ? } ? ? ? } ? ? }) ? ], ? props: { ? ? // whether to show popup ? ? value: Boolean, ? ? // whether to show overlay ? ? overlay: Boolean, ? ? // overlay custom style ? ? overlayStyle: Object, ? ? // overlay custom class name ? ? overlayClass: String, ? ? // whether to close popup when click overlay ? ? closeOnClickOverlay: Boolean, ? ? // z-index ? ? zIndex: [Number, String], ? ? // prevent body scroll ? ? lockScroll: { ? ? ? type: Boolean, ? ? ? default: true ? ? }, ? ? // whether to lazy render ? ? lazyRender: { ? ? ? type: Boolean, ? ? ? default: true ? ? } ? }, ? data() { ? ? return { ? ? ? inited: this.value ? ? }; ? }, ? computed: { ? ? shouldRender() { ? ? ? return this.inited || !this.lazyRender; ? ? } ? }, ? watch: { ? ? value(val) { ? ? ? const type = val ? 'open' : 'close'; ? ? ? this.inited = this.inited || this.value; ? ? ? this[type](); ? ? ? this.$emit(type); ? ? }, ? ? overlay: 'renderOverlay' ? }, ? mounted() { ? ? if (this.value) { ? ? ? this.open(); ? ? } ? }, ? /* istanbul ignore next */ ? activated() { ? ? if (this.value) { ? ? ? this.open(); ? ? } ? }, ? beforeDestroy() { ? ? this.close(); ? ? if (this.getContainer && this.$parent && this.$parent.$el) { ? ? ? this.$parent.$el.appendChild(this.$el); ? ? } ? }, ? /* istanbul ignore next */ ? deactivated() { ? ? this.close(); ? }, ? methods: { ? ? open() { ? ? ? /* istanbul ignore next */ ? ? ? if (this.$isServer || this.opened) { ? ? ? ? return; ? ? ? } ? ? ? // cover default zIndex ? ? ? if (this.zIndex !== undefined) { ? ? ? ? context.zIndex = this.zIndex; ? ? ? } ? ? ? this.opened = true; ? ? ? this.renderOverlay(); ? ? ? // 穿透處理的核心部分 ? ? ? if (this.lockScroll) { ? ? ? ? // 給touchstart及touchmove上綁定代碼 ? ? ? ? // 關于touchStart及ontouchmove的代碼在TouchMixin的引入中 ? ? ? ? on(document, 'touchstart', this.touchStart); ? ? ? ? on(document, 'touchmove', this.onTouchMove); ? ? ? ? if (!context.lockCount) { ? ? ? ? ? document.body.classList.add('van-overflow-hidden'); ? ? ? ? } ? ? ? ? context.lockCount++; ? ? ? } ? ? }, ? ? close() { ? ? ? if (!this.opened) { ? ? ? ? return; ? ? ? } ? ? ? if (this.lockScroll) { ? ? ? ? context.lockCount--; ? ? ? ? off(document, 'touchstart', this.touchStart); ? ? ? ? off(document, 'touchmove', this.onTouchMove); ? ? ? ? if (!context.lockCount) { ? ? ? ? ? document.body.classList.remove('van-overflow-hidden'); ? ? ? ? } ? ? ? } ? ? ? this.opened = false; ? ? ? closeOverlay(this); ? ? ? this.$emit('input', false); ? ? }, ? ? onTouchMove(event) { ? ? ? // 這個方法是touch文件中引入得,一會會看到 ? ? ? // 主要計算滑動得方向及距離 ? ? ? this.touchMove(event); ? ? ? // 方向計算 ? ? ? const direction = this.deltaY > 0 ? '10' : '01'; ? ? ? // 獲取滾動目標對象 ? ? ? const el = getScrollEventTarget(event.target, this.$el); ? ? ? // 滾動元素相關屬性賦值 ? ? ? const { scrollHeight, offsetHeight, scrollTop } = el; ? ? ? let status = '11'; ? ? ? /* istanbul ignore next */ ? ? ? if (scrollTop === 0) { ? ? ? ? // 沒有滾動的情況下,判定是否有滾動條 ? ? ? ? status = offsetHeight >= scrollHeight ? '00' : '01'; ? ? ? } else if (scrollTop + offsetHeight >= scrollHeight) { ? ? ? ? // 有滾動距離且滾動到底部 ? ? ? ? status = '10'; ? ? ? } ? ? ? /* istanbul ignore next */ ? ? ? if ( ? ? ? ? status !== '11' && ? ? ? ? this.direction === 'vertical' && ? ? ? ? !(parseInt(status, 2) & parseInt(direction, 2)) ? ? ? ) { ? ? ? ? // 有滾動條且有滾動距離且方向為垂直時,阻止默認事件,即阻止頁面滾動 ? ? ? ? // 所以原理其實是在可能會引起背景滑動穿透時禁止掉scroll事件 ? ? ? ? // 因為常見會滑動穿透的場景都是子元素不滾動引起背景滾動,或者子元素已經滾動到頂部或者底部,繼續(xù)滑動的話就會滑動穿透,如果發(fā)現(xiàn)已經滾動到頂部或者底部時直接禁止掉touchmove就可以阻止滑動穿透了 ? ? ? ? preventDefault(event, true); ? ? ? } ? ? }, ? ? renderOverlay() { ? ? ? if (this.$isServer || !this.value) { ? ? ? ? return; ? ? ? } ? ? ? this.$nextTick(() => { ? ? ? ? this.updateZIndex(this.overlay ? 1 : 0); ? ? ? ? if (this.overlay) { ? ? ? ? ? openOverlay(this, { ? ? ? ? ? ? zIndex: context.zIndex++, ? ? ? ? ? ? duration: this.duration, ? ? ? ? ? ? className: this.overlayClass, ? ? ? ? ? ? customStyle: this.overlayStyle ? ? ? ? ? }); ? ? ? ? } else { ? ? ? ? ? closeOverlay(this); ? ? ? ? } ? ? ? }); ? ? }, ? ? updateZIndex(value = 0) { ? ? ? this.$el.style.zIndex = ++context.zIndex + value; ? ? } ? } };
來看看touch的處理,可以看到給touchstart及touchmove綁定了滑動方向及距離得計算,touchmove這個方法會在ontouchmove中被調用,注意名稱,不要混淆。
import Vue from 'vue'; const MIN_DISTANCE = 10; function getDirection(x: number, y: number) { ? if (x > y && x > MIN_DISTANCE) { ? ? return 'horizontal'; ? } ? if (y > x && y > MIN_DISTANCE) { ? ? return 'vertical'; ? } ? return ''; } type TouchMixinData = { ? startX: number; ? startY: number; ? deltaX: number; ? deltaY: number; ? offsetX: number; ? offsetY: number; ? direction: string; }; export const TouchMixin = Vue.extend({ ? data() { ? ? return { direction: '' } as TouchMixinData; ? }, ? methods: { ? ? // touchstart獲取起始位置 ? ? touchStart(event: TouchEvent) { ? ? ? this.resetTouchStatus(); ? ? ? this.startX = event.touches[0].clientX; ? ? ? this.startY = event.touches[0].clientY; ? ? }, ? ? // touchmove算得移動后得位移差,用來計算方向和偏移量 ? ? touchMove(event: TouchEvent) { ? ? ? const touch = event.touches[0]; ? ? ? this.deltaX = touch.clientX - this.startX; ? ? ? this.deltaY = touch.clientY - this.startY; ? ? ? this.offsetX = Math.abs(this.deltaX); ? ? ? this.offsetY = Math.abs(this.deltaY); ? ? ? this.direction = this.direction || getDirection(this.offsetX, this.offsetY); ? ? }, ? ? resetTouchStatus() { ? ? ? this.direction = ''; ? ? ? this.deltaX = 0; ? ? ? this.deltaY = 0; ? ? ? this.offsetX = 0; ? ? ? this.offsetY = 0; ? ? } ? } });
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
vuejs+element-ui+laravel5.4上傳文件的示例代碼
本篇文章主要介紹了vuejs+element-ui+laravel5.4上傳文件的示例代碼,具有一定的參考價值,有興趣的可以了解一下2017-08-08vue-awesome-swiper 基于vue實現(xiàn)h5滑動翻頁效果【推薦】
說到h5的翻頁,很定第一時間想到的是swiper。但是我當時想到的卻是,vue里邊怎么用swiper。這篇文章主要介紹了vue-awesome-swiper - 基于vue實現(xiàn)h5滑動翻頁效果 ,需要的朋友可以參考下2018-11-11