相比于PC项目只需要关注功能实现,H5项目兼容性似乎是前端开发和测试童鞋需要重点关注的问题。我做H5项目也有一段时间了,下面从自己项目中遇到的问题稍稍做一下复盘,回顾一下踩坑和出坑的过程。
表现
头部刘海两侧区域或者底部区域,出现刘海遮挡文字遮挡、点击区域,或者呈现黑底或白底空白区域。
产生原因
iPhoneX及以上版本手机都采用了状态栏、圆弧展示角、传感器槽、主屏幕指示器和屏幕边缘手势(具体名词注释看下图)。头部底部侧边栏都需要做特殊处理,使得content尽可能的处于安全区域内,适配iPhoneX系列手机的特殊性。
解决方案
设置安全区域,填充危险区域,危险区域不做操作和内容展示。何为安全区域(safe Area),顾名思义,安全区域即为正常显示内容的区域,但该区域不受状态栏和其它内容影响。当界面显示在屏幕上时,安全区域即为导航栏、选项卡栏、工具栏和其他父视图不覆盖的屏幕视图的一部分。
具体操作
Step1:viewport-fit
viewport-fit meta 标签设置为 cover,获取所有区域填充。判断设备是否属于iPhone X,给头部底部增加适配层 。viewport-fit 有 3 个值,分别为:
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;
}
}
表现
延时:点击某个滚动的动画(如图所示),交互中动画会停止,出现下一步操作。但是在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事件
原理就是:
//封装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库源码。
表现
在做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。现在做移动端开发时一般都要加上一句话:
<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>");
}
表现
在如下图所示的图中,当页面滑动到搜索框下面,二手房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解决方案差不多。
表现
在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)恢复成原来的视口
H5项目有的坑远不止这些,出坑解决方案更是个人有个人的偏好。后续会持续输出相关踩坑出坑方案。生命不息,踩坑不止…
领取专属 10元无门槛券
私享最新 技术干货