H5项目踩坑及出坑实践

相比于PC项目只需要关注功能实现,H5项目兼容性似乎是前端开发和测试童鞋需要重点关注的问题。我做H5项目也有一段时间了,下面从自己项目中遇到的问题稍稍做一下覆盘,回顾一下踩坑和出坑的过程。

1 iphoneX系列手机适配问题

表现

头部刘海两侧区域或者底部区域,出现刘海遮挡文字遮挡、点击区域,或者呈现黑底或白底空白区域。

产生原因

iPhoneX及以上版本手机都采用了状态栏、圆弧展示角、传感器槽、主屏幕指示器和屏幕边缘手势(具体名词注释看下图)。头部底部侧边栏都需要做特殊处理,使得content尽可能的处于安全区域内,适配iPhoneX系列手机的特殊性。

解决方案

设置安全区域,填充危险区域,危险区域不做操作和内容展示。何为安全区域(safe Area),顾名思义,安全区域即为正常显示内容的区域,但该区域不受状态栏和其它内容影响。当界面显示在屏幕上时,安全区域即为导航栏、选项卡栏、工具栏和其他父视图不覆盖的屏幕视图的一部分。

具体操作

Step1:viewport-fit

viewport-fit meta 标签设置为 cover,获取所有区域填充。判断设备是否属于iPhone X,给头部底部增加适配层 。viewport-fit 有 3 个值,分别为:

  • auto:此值不影响初始布局视图端口,并且整个web页面都是可查看的。
  • contain:视图端口按比例缩放,以适合显示内嵌的最大矩形。
  • cover:视图端口被缩放以填充设备显示。强烈建议使用safe area inset变量,以确保重要内容不会出现在显示之外。

viewport-fit meta标签设置(cover时)

><meta name="viewport" content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=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加一句<div class=“footer”></div>

,使得每个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效果差别很大。

穿透:点击蒙层,蒙层消失后发现触发了蒙层下层元素点击事件。或者点击页内按钮跳转至新页,发现新页的对应位置的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. 禁止缩放

<meta name="viewport" content="width=device-width, user-scalable=no">// 关键是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

知道了设备像素比,我们就大概知道了1px线变粗的原因。简单来说就是手机屏幕分辨率越来越高了,同样大小的一个手机,它的实际物理像素数更多了。因为不同的移动设备有不同的像素密度,所以我们所写的1px在不同的移动设备上等于这个移动设备的1px。现在做移动端开发时一般都要加上一句话:

&lt;meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

这句话定义了本页面的viewport的宽度为设备宽度,初始缩放值和最大缩放值都为1,并禁止了用户缩放。

viewport的设置和屏幕物理分辨率是按比例而不是相同的,移动端window对象有个devicePixelRatio属性,它表示设备物理像素和css像素的比例,在retina屏的iphone手机上,这个值为2或3, css里写的1px长度映射到物理像素上就有2px或3px。通过设置viewport,可以改变css中的1px用多少物理像素来渲染,设置了不同的viewport,当然1px的线条看起来粗细不一致。

解决方案

a. 在公共样式里面定义一个类,使用伪元素+绝对定位+scale,优点:兼容性较好,缺点:input元素不支持伪元素

<div class="wrap">
    内容区域
</div>

设置四周的边框:

.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,<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%'><line x1='0' y1='100%' x2='100%' y2='100%' stroke='#dcdcdc' stroke-width='1'/></svg>");
}

4 position fixed和sticky兼容性

表现

在如下图所示的图中,当页面滑动到搜索框下面,二手房tab会自动吸顶,但是在某些安卓机的原生浏览器中没有吸顶这个动作。

产生原因

吸顶的动作是用position:sticky完成的,但是Caniuse上显示sticky的兼容性如下:

Sticky的作用相当于relative和fixed的结合体,当修饰的目标节点再屏幕中时表现为relative,当要超出的时候是fixed的形式展现。但是由于兼容性问题,在安卓端没有很好地兼容。且它的活动范围只能在父元素内,滚动超过父元素的话,它一样不能吸顶。

解决方案

react解决方案:使用react-sticky,通过计算 <Sticky> 组件相对于<StickyContai ner>组件的位置进行工作,如果他出现在视口的外面,将其附加到屏幕的顶部所需要的样式作为参数传递给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项目踩坑及出坑实践

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章