移動端Modal元件開發雜談

有贊前端發表於2019-03-03

Vant 是有贊開發的一套基於 Vue 2.0Mobile 元件庫,在開發的過程中也踩了很多坑,今天我們就來聊一聊開發一個移動端 Modal 元件(在有贊該元件被稱為 Popup )需要注意的一些

在任何一個合格的UI元件庫中,Modal 元件應該是必備的元件之一。它一般用於使用者處理事物,但又不希望跳轉頁面時,可以使用 Modal 在當前頁面中開啟一個浮層,承載對應的操作。相比PC端,移動端的 Modal 元件坑會更多,比如滾動穿透問題就不像PC端在 body 上新增 overflow: hidden 那麼簡單。

目錄

一、API定義
二、水平垂直居中的方案
三、可惡的滾動穿透
四、position: fixed 失效

一、API定義

任何一個元件開始編碼前都需要首先將API先定義好,才好根據API來提供對應的功能。Modal 元件提供了以下API:

移動端Modal元件開發雜談

更具體的 Api 介紹可以訪問該連結檢視:Popup

二、水平垂直居中方案

垂直居中的方案網上谷歌一下就能找到很多種,主流的方案有:

  1. absolute(fixed) + 負邊距
  2. absolute(fixed) + transform
  3. flex
  4. table + vertical-align

首先說一下我們選擇的是第二種:absolute(fixed) + transform,它是以上方案中最簡單最方便的方案,程式碼實現量也很少。實現程式碼如下:

.modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
複製程式碼

但是 transform 會導致一個巨大的,這個的具體細節會在下面的章節中詳細講到。

說完了我們選擇的方案,再來說說為啥不選擇其他的方案呢?

absolute(fixed) + 負邊距

只能適合定高的場景,果斷拋棄。如果要實現不定高度就要通過JS來計算了,增加了實現的複雜度。

flex

flex 佈局一是在某些老版本的安卓瀏覽器上還不是很相容,還有就是需要包裹一個父級才能水平垂直居中。

table + vertical-middle

CSS2 時代用這個方案來實現垂直居中是比較常見的方案,不足的地方就是程式碼實現量相對較大。

三、可惡的滾動穿透

開發過移動端UI元件的都知道,在移動端有個可惡的滾動穿透問題。這個問題可以描述為:在彈窗上滑動會導致下層的頁面跟著滾動。

網上谷歌一下滾動穿透關鍵字其實可以發現很多種解決方案,每個方案也各有優缺點,但我們選擇的解決方案是團隊的一姐一篇移動端體驗優化的博文中得到的啟示(博文地址:花式提升移動端互動體驗 | TinySymphony)。

具體的思路是:當容器可以滑動時,若已經在頂部,禁止下滑;若在底部,禁止上滑;容器無法滾動時,禁止上下滑。實現的方式就是在 document 上監聽 touchstarttouchmove 事件,如滑動時,祖先元素並沒有可滑動元素,直接阻止冒泡即可;否則判斷手指滑動的方向,若向下滑動,判斷是否滑動到了滑動元素的底部,若已經到達底部,阻止冒泡,向上滑動也類似。具體的程式碼實現可以看下面的程式碼:

const _ = require('src/util')
export default function (option) {
  const scrollSelector = option.scroll || '.scroller'
  const pos = {
    x: 0,
    y: 0
  }

  function stopEvent (e) {
    e.preventDefault()
    e.stopPropagation()
  }

  function recordPosition (e) {
    pos.x = e.touches[0].clientX
    pos.y = e.touches[0].clientY
  }

  function watchTouchMove (e) {
    const target = e.target
    const parents = _.parents(target, scrollSelector)
    let el = null
    if (target.classList.contains(scrollSelector)) el = target
    else if (parents.length) el = parents[0]
    else return stopEvent(e)
    const dx = e.touches[0].clientX - pos.x
    const dy = e.touches[0].clientY - pos.y
    const direction = dy > 0 ? '10' : '01'
    const scrollTop = el.scrollTop
    const scrollHeight = el.scrollHeight
    const offsetHeight = el.offsetHeight
    const isVertical = Math.abs(dx) < Math.abs(dy)
    let status = '11'
    if (scrollTop === 0) {
      status = offsetHeight >= scrollHeight ? '00' : '01'
    } else if (scrollTop + offsetHeight >= scrollHeight) {
      status = '10'
    }
    if (status !== '11' && isVertical && !(parseInt(status, 2) & parseInt(direction, 2))) return stopEvent(e)
  }
  document.addEventListener('touchstart', recordPosition, false)
  document.addEventListener('touchmove', watchTouchMove, false)
}
複製程式碼

四、position: fixed 失效

在前端工程師的世界觀裡,position: fixed 一直是相對瀏覽器視口來定位的。有一天,你在固定定位元素的父元素上應用了 transform 屬性,當你重新整理瀏覽器想看看最新的頁面效果時,你竟然發現固定定位的元素竟然相對於父元素來定位了。是不是感覺人生觀都崩塌了。

這個問題,目前只在Chrome瀏覽器/FireFox瀏覽器下有。也有人給 Chrome 提bug:Fixed-position element uses transformed ancestor as the container,但至今尚未解決。

例如下面的程式碼:

<style>
  body {
    padding: 50px;
  }
  .demo {
    background: #ccc;
    height: 100px;
    transform: scale(1);
  }
  .fixed-box {
    position: fixed;
    top: 0;
    left: 0;
    width: 100px;
    height: 100px;
    background: red;
  }
</style>

<div class="demo">
  <div class="fixed-box"></div>
</div>
複製程式碼

垂直居中方案 position: fixed + transform 的選擇導致了 Modal 元件使用上的一個坑。當我們在 Modal 元件裡面巢狀了一個 Modal 時,內層的Modal 就是相對外層的 Modal 來定位,而不是瀏覽器的 viewport。這也限制了我們 Modal 的使用場景,如果你想實現巢狀的 Modal,就要選擇其他的垂直居中方案了,有舍必有得嘛。

關於 position: fixed 失效的更多細節可以參考以下幾篇博文:

總結

開發元件庫不易,開發移動端元件庫更不易。移動端元件庫相對PC端會有更多的奇葩的坑。當遇到坑,肯定是要選擇跨越它,而不是逃避它,因此也才有了我們這篇文章,後續我們也還會有一些介紹 Vant 元件庫開發過程中遇到的坑,或者一些優化相關的文章,敬請期待。

如果覺得這篇文章講的還不夠,完整原始碼實現請移步Github:popup

本文首發於有贊技術部落格

相關文章