在作活動時常常須要實現各類各樣的彈窗,有一些常見的問題須要處理,包含:javascript
已發佈npm包歡迎使用反饋和star~html
npm install jdc-popup -S
複製代碼
github: github.com/jinglecjy/j…
demo: jinglecjy.github.io/jdc-popup/d…
vue
打開浮層時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
測試提出bug的時間比較緊急,因此直接引入了已有的庫解決了該問題,通過測試,ios8+,android4.4+下沒有發現問題。
庫的源碼比較清晰,以前的我的的思路都是想要一套代碼兼容各端,這個庫是將問題細分,對不一樣端進行了不一樣的處理,更容易去兼容。這三種方案都沒法完美兼容全部端,詳細能夠查看參考[2]。ios
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端的實現與我的的方案思路基本一致,打開彈層時將底部元素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端在打開浮層時,禁用了底部的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
}
複製代碼
用庫能夠較好的完成這個需求,可是對於滑動穿透這樣的小問題,每次都引入庫解決有點小題大作。基於上述考慮,選取了京東的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