Categories
程式開發

H5項目踩坑及出坑實踐


相比於PC項目只需要關注功能實現,H5項目兼容性似乎是前端開發和測試童鞋需要重點關注的問題。我做H5項目也有一段時間了,下面從自己項目中遇到的問題稍稍做一下复盤,回顧一下踩坑和出坑的過程。

1 iphoneX系列手機適配問題

表現

頭部劉海兩側區域或者底部區域,出現劉海遮擋文字遮擋、點擊區域,或者呈現黑底或白底空白區域。

產生原因

iPhoneX及以上版本手機都採用了狀態欄、圓弧展示角、傳感器槽、主屏幕指示器和屏幕邊緣手勢(具體名詞註釋看下圖)。頭部底部側邊欄都需要做特殊處理,使得content盡可能的處於安全區域內,適配iPhoneX系列手機的特殊性。

解決方案

設置安全區域,填充危險區域,危險區域不做操作和內容展示。何為安全區域(safe Area),顧名思義,安全區域即為正常顯示內容的區域,但該區域不受狀態欄和其它內容影響。當界面顯示在屏幕上時,安全區域即為導航欄、選項卡欄、工具欄和其他父視圖不覆蓋的屏幕視圖的一部分。

H5項目踩坑及出坑實踐 1

具體操作

第一步:視口擬合

viewport-fit meta 標籤設置為cover,獲取所有區域填充。判斷設備是否屬於iPhone X,給頭部底部增加適配層。 viewport-fit 有3 個值,分別為:

  • auto:此值不影響初始佈局視圖端口,並且整個web頁面都是可查看的。
  • contain:視圖端口按比例縮放,以適合顯示內嵌的最大矩形。
  • cover:視圖端口被縮放以填充設備顯示。強烈建議使用safe area inset變量,以確保重要內容不會出現在顯示之外。

viewport-fit meta標籤設置(cover時)

>

Step2:增加適配層

WebKit包含了新的CSS函數constant()和env(),以及一組四個預定義的常量:safe-area-inset-left, safe-area-inset-right, safe-area-inset-top和safe -area-inset-bottom。當合併一起使用時,允許樣式引用每個方面的安全區域的大小。

當我們設置viewport-fit:contain,也就是默認的時候時;設置safe-area-inset-left, safe-area-inset-right, safe-area-inset-top和safe-area-inset-bottom等參數時不起作用的。只有設為cover才可以用contant()和env()方法。

當我們設置viewport-fit:cover時,為了達到向前兼容ios11.2以前的版本向後兼容ios11.2以後版本的瀏覽器,需要同時用contant()和env()。設置如下:

body {
    padding-top: constant(safe-area-inset-top);
    padding-top: env(safe-area-inset-top);//为导航栏+状态栏的高度 88px
    padding-left: constant(safe-area-inset-left);
    padding-left: env(safe-area-inset-left); //如果未竖屏时为0 
    padding-right: constant(safe-area-inset-right);//如果未竖屏时为0
    padding-right: env(safe-area-inset-right);
    padding-bottom: constant(safe-area-inset-bottom));//距离底部圆弧的距离34px
    padding-bottom:  env(safe-area-inset-bottom));
}

通過上述設置,可以開闢出適配iPhoneX系列手機的安全區域。

在實際應用中,為了解決底部出現文字遮擋、fixed按鈕不可點擊,或者呈現黑底或白底空白區域的問題,同時適配不同的寬高比。結合媒體查詢分別適配X,XS MAX ,XR,給底部fixed的元素加一個適配底部小黑條和圓角的底部高度,如下面fixed-footer,會出現底部body超出底部fixed部分的問題,可以給body加一句

,使得每個X的屏幕都有一個div塊,把內容頂上去,防止出現底部透傳現象。

//iphone X
@media only screen and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) {
    .fixed-footer{
    bottom: constant(safe-area-inset-bottom) ;
    bottom:  env(safe-area-inset-bottom);
    }
    .footer {
    position: fixed;
    bottom: 0;
    width: 100%;
    height: constant(safe-area-inset-bottom)
    height: env(safe-area-inset-bottom)
    background-color: #fff;
}
}
//iphone Xs Max
@media only screen and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio:3) {
    .fixed-footer{
    bottom:  constant(safe-area-inset-bottom)
    bottom: env(safe-area-inset-bottom)
    }
    .footer {
    position: fixed;
    bottom: 0;
    width: 100%;
    height: constant(safe-area-inset-bottom)
    height:  env(safe-area-inset-bottom)
    background-color: #fff;
    }
}
//iphone XR
@media only screen and (device-width: 414px) and (device-height:     896px) and (-webkit-device-pixel-ratio:2) {
    .fixed-footer{
    bottom: constant(safe-area-inset-bottom)
    bottom: env(safe-area-inset-bottom)
    }
    .footer {
        position: fixed;
        bottom: 0;
        width: 100%;
        height:  constant(safe-area-inset-bottom)
        height:  env(safe-area-inset-bottom)
        background-color: #fff;
     }
}

2 click點擊延遲與穿透問題

表現

延時:點擊某個滾動的動畫(如圖所示),交互中動畫會停止,出現下一步操作。但是在IOS系統中,點擊沒有反應,與Android效果差別很大。

H5項目踩坑及出坑實踐 2

穿透:點擊蒙層,蒙層消失後發現觸發了蒙層下層元素點擊事件。或者點擊頁內按鈕跳轉至新頁,發現新頁的對應位置的click事件被觸發了。

產生原因

為什麼會出現click延時?

iOS 中的safari,為了實現雙擊縮放操作,在單擊300ms之後,如果未進行第二次點擊,則執行click單擊操作。也就是說來判斷用戶行為是否為雙擊縮放產生的。後來其他的瀏覽器都效仿safari,實現了雙擊縮放功能,導致在大部分app中無論是否需要雙擊縮放這種行為,click單擊都會產生300ms延遲。

為什麼會出現點擊透傳?

當點擊移動設備的屏幕時, 可以分解成多個事件,順序依次為:touchstart — touchmove — touchend — click, 這些事件是按順序依次觸發的。雙層元素疊加時,在上層元素上綁定touch 事件,下層元素綁定click 事件。由於click 發生在touch之後,點擊上層元素,元素消失,此時事件只進行到touchend,300ms後下層元素會觸發click事件,由此產生了點擊穿透的效果。當然對於跨頁麵點擊穿透問題,和上述原理差不多,同時滿足了touch,跳轉新頁面,click事件,三者缺一不可。

解決方案

解決click延時:

a. 禁止縮放

// 关键是user-scalable=no

但是在iOS10下面及部分UC瀏覽器中為了提高網站的輔助功能那個,屏蔽了Meta下的user-scalable=no功能。就算加上user-scalable=no,瀏覽器也能支持手動縮放。可以用js加監聽事件來阻止手動縮放。代碼如下:

   window.onload=function () {  
        document.addEventListener('touchstart',function (event) {  
            if(event.touches.length>1){  
                event.preventDefault();  
            }  
        })  
        var lastTouchEnd=0;  
        document.addEventListener('touchend',function (event) {  
            var now=(new Date()).getTime();  
            if(now-lastTouchEnd<=300){  
                event.preventDefault();  
            }  
            lastTouchEnd=now;  
        },false)  
    } 

b. 用touch事件替代click事件

原理就是:

  • 當我們手指觸摸屏幕,記錄當前觸摸時間
  • 當我們手指離開屏幕,用離開的時間減去觸摸的時間
  • 如果時間小於150ms,並且沒有滑動過屏幕,那麼我們就定義為點擊
//封装tap,解决click 300ms延时
function tap(obj,callback){
  var flag = false;
  var startTime = 0;  //记录触摸时候的时间变量
  obj.addEventListener('touchstart',function(e){
   startTime = Date.now()
 });
  obj.addEventListener('touchmove',function(e){
   flag = true; //看看是否有滑动,有滑动算拖拽,不算点击
 });
  obj.addEventListener('touchend',function(e){
    if(!flag && (Date.now() - startTime) < 150){ //如果手指触摸和离开时间小于150ms算点击
      callback && callback() //执行回调函数
   }
   flag = false;  //取反,重置
   stratTime = 0;
 });
}

c. 引用faskclick插件庫

使用faskclick庫以後,click延時和穿透問題都可以解決了。至於faskclick插件庫的實現原理,以後再做總結。

d. vue項目可以安裝vue-tap插件

使用方法類vue的指令,在本次問題中用的就是這種方案解決的。

解決點擊穿透:

a. 經常用的就是不要混用touch和click,把所有的click事件替換成click事件,但是需要特別注意a標籤,a標籤的href也是click,需要去掉換成js控制的跳轉,或者直接改成span+tap控制跳轉。如果要求不高,不在乎滑走或者滑進來觸發事件的話,span+touchend就可以了,畢竟tap需要引入第三方庫。當然對交互要求不高的情況下可以全部用click事件,但是要想好這300ms的後果。

b. 應用pointer-events。常言道能用css解決的問題就不要用js。 pointer-events是CSS3的一個屬性,支持的值非常多,其中大部分都是和SVG有關。對於點擊穿透了解一個none就可以了。

pointer-events: none;//让鼠标点击事件失效。

蒙層隱藏後,給按鈕下面元素添上pointer-events: none;樣式,讓click穿過去,300ms後去掉這個樣式,恢復響應即可。但是要注意蒙層消失後的的300ms內,用戶可以看到按鈕下面的元素點擊沒反應,如果用戶手速很快的話一定會發現,不推薦使用。

c. 比較推薦的方式如果不介意多加載幾KB的話,可以嘗試上述解決點擊延時的fastclick庫。這裡不多加解釋說明,具體可以去看fastclick庫源碼。

3 1px 問題

表現

在做H5頁面時,有時候UI稿會出現邊框寬度為1px,如果簡單粗暴的寫border:1px solid #eee,UI在審查的時候也常常會覺得分割線或者邊框線太粗了,要更細一點,但是看代碼發現也寫了1px。這個時候想改成0.5px,會發現在很多IOS7及以下以及一些Android機型上不支持0.5px。為了解決1px變粗問題,我們就要找到一種實現0.5px的方案。

產生原因

要知道問題的原因首先要了解一下幾個概念:

(1) 物理像素(physical pixel)

一個物理像素是顯示器(手機屏幕)上最小的物理顯示單元(像素顆粒),在操作系統的調度下,每一個設備像素都有自己的顏色值和亮度值。如:iPhone6上就有750*1334個物理像素顆粒。

(2) 設備獨立像素(density-independent pixel)

設備獨立像素,也叫密度無關像素,可以認為是計算機坐標系統中得一個點,這個點代表一個可以由程序使用的虛擬像素(比如:css像素),有時我們也說成是邏輯像素。然後由相關係統轉換為物理像素。所以說,物理像素和設備獨立像素之間存在著一定的對應關係,這就是接下來要說的設備像素比。

(3) 設備像素比(device pixel ratio )

簡稱dpr設備像素比(簡稱dpr)定義了物理像素和設備獨立像素的對應關係。它的值可以按如下的公式的得到:

設備像素比(dpr)=物理像素/邏輯像素(px) // 在某一方向上,x方向或者y方向,下圖dpr=2

H5項目踩坑及出坑實踐 3

知道了設備像素比,我們就大概知道了1px線變粗的原因。簡單來說就是手機屏幕分辨率越來越高了,同樣大小的一個手機,它的實際物理像素數更多了。因為不同的移動設備有不同的像素密度,所以我們所寫的1px在不同的移動設備上等於這個移動設備的1px。現在做移動端開發時一般都要加上一句話:


這句話定義了本頁面的viewport的寬度為設備寬度,初始縮放值和最大縮放值都為1,並禁止了用戶縮放。

viewport的設置和屏幕物理分辨率是按比例而不是相同的,移動端window對像有個devicePixelRatio屬性,它表示設備物理像素和css像素的比例,在retina屏的iphone手機上,這個值為2或3 , css裡寫的1px長度映射到物理像素上就有2px或3px。通過設置viewport,可以改變css中的1px用多少物理像素來渲染,設置了不同的viewport,當然1px的線條看起來粗細不一致。

解決方案

a. 在公共樣式裡面定義一個類,使用偽元素+絕對定位+scale,優點:兼容性較好,缺點:input元素不支持偽元素

内容区域

設置四周的邊框:

.wrap
        height: 40px;
        position: relative;
        &::after 
            content: "";
            position: absolute;
            top: 0;
            left: 0;
            width: 200%;
            height: 200%;
            transform-origin: 0 0; -webkit-transform-origin: 0 0; -moz-transform-origin: 0 0; -o-transform-origin: 0 0;
            transform: scale(.5); -webkit-transform: scale(.5); -moz-transform: scale(.5); -o-transform: scale(.5);
            border: 1px solid #ebebf0;

b.使用rem 改進

使用rem作為單位,這樣可以更好地去實現移動端的響應式像素以及Retina屏幕上的表現。優點是實現簡單,缺點是部分機型還是不兼容。

c. css中引入svg 改進

具體思路是為元素加上background-image,然後把svg置為圖片類型,因為svg上的1px 就是實實在在的只佔1個物理像素。實現很簡單,代碼如下:

input 
{
  background-image:url(
"data:image/svg+xml;base64,");
}

4 position fixed和sticky兼容性

表現

在如下圖所示的圖中,當頁面滑動到搜索框下面,二手房tab會自動吸頂,但是在某些安卓機的原生瀏覽器中沒有吸頂這個動作。

H5項目踩坑及出坑實踐 4

產生原因

吸頂的動作是用position:sticky完成的,但是Caniuse上顯示sticky的兼容性如下:

H5項目踩坑及出坑實踐 5

Sticky的作用相當於relative和fixed的結合體,當修飾的目標節點再屏幕中時表現為relative,當要超出的時候是fixed的形式展現。但是由於兼容性問題,在安卓端沒有很好地兼容。且它的活動範圍只能在父元素內,滾動超過父元素的話,它一樣不能吸頂。

解決方案

react解決方案:使用react-sticky,通過計算組件相對於組件的位置進行工作,如果他出現在視口的外面,將其附加到屏幕的頂部所需要的樣式作為參數傳遞給render callback,作為child傳遞的函數。

JS解決方案:通過cssSupport判斷瀏覽器的支持情況,如果瀏覽器支持sticky,則不做處理,否則通過自定義滾動事件的監聽,根據top的改變來實現tab層fixed和absolute的轉換。

vue解決方案:可以直接使用vue-sticky組件,vue-sticky實現原理大致與JS解決方案差不多。

5 軟鍵盤將頁面頂起來、收起未回落問題

表現

在Android手機中,點擊input框時,鍵盤彈出,將頁面頂起來,導致頁面樣式錯亂。失去焦點時,鍵盤收起,鍵盤區域空白,未回落。

產生原因

我們在app佈局中會有個固定的底部。在Android一些版本中,輸入鍵盤彈出來,會將解壓absolute和fixed定位的元素。導致可視區域變小,佈局錯亂。

解決方案

軟鍵盤將頁面頂起來的解決方案,主要是通過監聽頁面高度變化,強制恢復成彈出前的高度。

// 记录原有的视口高度
const originalHeight = document.body.clientHeight || document.documentElement.clientHeight;
window.onresize = function(){
  var resizeHeight = document.documentElement.clientHeight || document.body.clientHeight;
  if(resizeHeight < originalHeight ){
    // 恢复内容区域高度
    // const container = document.getElementById("container")
    // 例如 container.style.height = originalHeight;
  }
}

鍵盤不能回落問題出現在iOS12+和wechat6.7.4+中,而在微信H5開發中是比較常見的Bug。兼容原理:1.判斷版本類型2.更改滾動的可視區域。

const isWechat = window.navigator.userAgent.match(/MicroMessenger/([d.]+)/i);
if (!isWechat) return;
const wechatVersion = wechatInfo[1];
const version = (navigator.appVersion).match(/OS (d+)_(d+)_?(d+)?/);


 // 如果设备类型为iOS12+和wechat6.7.4+,恢复成原来的视口
if (+wechatVersion.replace(/./g, '') >= 674 && +version[1] >= 12) {
  window.scrollTo(0, Math.max(document.body.clientHeight, document.documentElement.clientHeight));
}
window.scrollTo(x-coord, y-coord),其中window.scrollTo(0, clientHeight)恢复成原来的视口

6 總結

H5項目有的坑遠不止這些,出坑解決方案更是個人有個人的偏好。後續會持續輸出相關踩坑出坑方案。生命不息,踩坑不止…

7 推薦文章

本文轉載自公眾號(ID:)。

原文鏈接

H5項目踩坑及出坑實踐