vue組件庫之popup彈窗組件

業務背景

在作活動時常常須要實現各類各樣的彈窗,有一些常見的問題須要處理,包含:javascript

  • 滑動穿透問題:滑動彈窗元素致使背景元素滾動
  • 多彈窗層級問題:當有多彈窗時,最新的彈窗永遠在最上層
  • 出現/消失過渡動畫

實現

已發佈npm包歡迎使用反饋和star~html

npm install jdc-popup -S
複製代碼

github: github.com/jinglecjy/j…
demo: jinglecjy.github.io/jdc-popup/d…
vue

demo

滑動穿透問題

初始的解決方案

打開浮層時fixed底部元素,同時爲了保持body的位置與打開浮層前一致,設置top偏移爲當前scrollTop;
關閉浮層時恢復底部元素狀態和滾動高度;java

controlledBgScrolled() {
    let bgEle = document.getElementById('app');
    // 打開浮層
    if (this.showPopup) {
        let top = document.documentElement.scrollTop
          || window.pageYOffset
          || document.body.scrollTop;
        this.scrollTop = top;
        bgEle.style.position = 'fixed';
        bgEle.style.top = `-${top}px`;
        bgEle.style.height = '100%';
    } 
    else {
        bgEle.style.position = 'relative';
        bgEle.style.top = '';
        bgEle.style.height = '100%';
        document.documentElement.scrollTop = this.scrollTop; 
        window.pageYOffset = this.scrollTop;
        document.body.scrollTop = this.scrollTop;
    }
}
複製代碼

這個方案的缺點是:android

  1. 打開/關閉彈窗瞬間,能夠看到有閃動
  2. 在APP內嵌頁面,與APP原生有概率發生衝突

使用庫實現

測試提出bug的時間比較緊急,因此直接引入了已有的庫解決了該問題,通過測試,ios8+,android4.4+下沒有發現問題。
庫的源碼比較清晰,以前的我的的思路都是想要一套代碼兼容各端,這個庫是將問題細分,對不一樣端進行了不一樣的處理,更容易去兼容。這三種方案都沒法完美兼容全部端,詳細能夠查看參考[2]。ios

PC端

PC端實現比較簡單,經過在body設置overflow: hidden就OK了。git

const $body = document.querySelector('body')
const bodyStyle = { ...$body.style }
const scrollBarWidth = window.innerWidth - document.body.clientWidth
// 打開浮層時
$body.style.overflow = 'hidden'
$body.style.boxSizing = 'border-box'
$body.style.paddingRight = `${scrollBarWidth}px`
// 關閉浮層時,恢復原始設置
['overflow', 'boxSizing', 'paddingRight']
.forEach((x: OverflowHiddenPcStyleType) => {
    $body.style[x] = bodyStyle[x] || ''
}
複製代碼

Android端

Android端的實現與我的的方案思路基本一致,打開彈層時將底部元素fixed並設置top偏移,關閉浮層時恢復現場,注意到底部元素必須同時設置html和body。github

const scrollTop = $html.scrollTop || $body.scrollTop
const htmlStyle = { ...$html.style }
const bodyStyle = { ...$body.style }

// 打開浮層時,fixed底部
$html.style.height = '100%'
$html.style.overflow = 'hidden'
$body.style.top = `-${scrollTop}px`
$body.style.width = '100%'
$body.style.height = 'auto'
$body.style.position = 'fixed'
$body.style.overflow = 'hidden'

// 關閉浮層時,恢復現場
$html.style.height = htmlStyle.height || ''
$html.style.overflow = htmlStyle.overflow || ''
['top', 'width', 'height', 'overflow', 'position']
.forEach((x: OverflowHiddenMobileStyleType) => {
  $body.style[x] = bodyStyle[x] || ''
})

window.scrollTo(0, scrollTop)
複製代碼

iOS端

iOS端在打開浮層時,禁用了底部的touchmove事件,若是彈窗內部元素須要可滾動,則經過另外的函數自行處理。關閉浮層時移除全部事件監聽。經測試,該方案在Android下彈窗滾動到邊界時,底部元素有概率出現滾動。該庫在iPhone 6p下初次打開沒法滾動。chrome

/*** 打開浮層時,處理浮層和底部元素的滾動事件 ***/
// 1. targetElement爲須要滾動的元素容器,處理其滾動
if (targetElement && lockedElements.indexOf(targetElement) === -1) {
    targetElement.ontouchstart = (event) => {
        initialClientY = event.targetTouches[0].clientY
    }
    targetElement.ontouchmove = (event) => {
        if (event.targetTouches.length !== 1) return
        // 手動處理滾動
        handleScroll(event, targetElement)
    }
    // 記錄可滾動元素
    lockedElements.push(targetElement)
}
const handleScroll = (event, targetElement) => {
    const clientY = event.targetTouches[0].clientY - initialClientY
    if (targetElement) {
        const { scrollTop, scrollHeight, clientHeight } = targetElement
        // 向上滾動時 且 已經到達頂部
        const isOnTop = clientY > 0 && scrollTop === 0
        // 當向下滾動 且 已經到達底部
        const isOnBottom = clientY < 0
                          && scrollTop + clientHeight + 1 >= scrollHeight
        if (isOnTop || isOnBottom) {
            return preventDefault(event)
        }
    }
    event.stopPropagation()
    return true
}

// 2. 禁止document的touchMove事件
if (!documentListenerAdded) {
    document.addEventListener(
      'touchmove',
      preventDefault,
      eventListenerOptions)
    documentListenerAdded = true
}
/*** 關閉浮層時,移除時間監聽 ***/
if (targetElement) {
    const index = lockedElements.indexOf(targetElement)
    if (index !== -1) {
        targetElement.ontouchmove = null
        targetElement.ontouchstart = null
        lockedElements.splice(index, 1)
    }
}
if (documentListenerAdded) {
    document.removeEventListener(
      'touchmove',
      preventDefault,
      eventListenerOptions)
    documentListenerAdded = false
}
複製代碼

ui組件庫都是怎麼實現的?

用庫能夠較好的完成這個需求,可是對於滑動穿透這樣的小問題,每次都引入庫解決有點小題大作。基於上述考慮,選取了京東的nutui/有讚的vant/餓了麼的mintui對比其實現方案,對好比下:npm

組件庫 實現思路 實現形式
vant touch事件處理 mixins,抽取了複雜複用邏輯
mint touch事件處理 mixins,抽取了複雜邏輯,但實現方案上依賴組件結構,複用性不強
nut fixed底層背景 組件內部函數,簡潔易讀,複用性不強

對比了一下三個方案,並實際測試(iOS8+/Android4+),仍是沒法兼容全部機型,最終的仍是按照庫的基本思路包裝了一下組件。
另外加入overscroll-behavior雖然兼容性不佳,可是安卓原生瀏覽器和chrome瀏覽器仍然有部分支持。

// 對於半透明蒙層阻止滾動
mask.addEventListener(
  'touchmove', 
  this.preventDefault,
  { capture: false, passive: false },
  false);
// 對可滾動元素容器手動處理滾動
onTouchMove(event, targetElement) {
    ...
    if (targetElement) {
        const {
            scrollTop,
            scrollHeight,
            clientHeight
        } = targetElement
        // 向上滾動時 且 已經到達頂部
        const isOnTop = this.deltaY > 0 && scrollTop === 0
        // 當向下滾動 且 已經到達底部
        const isOnBottom = this.deltaY < 0
                          && scrollTop + clientHeight + 1 >= scrollHeight
        if (isOnTop || isOnBottom) {
            this.preventDefault(event)
        }
    }
    event.stopPropagation()
    return true
}
// 關閉彈窗時,移除全部事件
...
複製代碼

多彈窗層級問題

當先後打開兩個彈窗,用戶的預期是按照打開的前後順序,越後打開的彈窗在越上層,簡而言之就是新彈窗永遠在最上層。能夠經過記錄當前出現過的最大zIndex,新彈窗zIndex = zIndex+1。 另外滑動穿透問題在多彈窗狀況下也須要處理,對於非當前最高層級彈窗,不該當收到滾動影響。

this.$el.style.zIndex = context.zIndex + 1; 
context.zIndex += 1;
複製代碼

擴展

業務上的彈窗樣式通常比較複雜,若是隻是簡單通用的彈窗樣式,想要解決滑動穿透問題,能夠經過Vue.extend擴展,將彈窗組件直接掛載到document下(element-ui中就使用了相似的作法),不會和主體內容互相影響,調用起來也更加靈活。

參考

[1] tua-scroll-body-lock:tuateam.github.io/tua-body-sc…
[2] 滑動穿透(鎖body)終極探索]:juejin.im/post/5ca481…
[3] vant-popup]:blog.csdn.net/riddle1981/…
[4] NUTUI:github.com/jdf2e/nutui

相關文章
相關標籤/搜索