以前都是写pc,后来需要写h5移动端项目,会遇到一些自适应和兼容性等方面的问题,下面从自己写过的h5项目中稍稍做点总结。
一.自适应布局
1.rem布局基础
开启一个移动端项目的基础,首先是想好如何在代码中实现移动端适配。之前没有经验,个参与的项目里简单粗暴地采用px写死的方法,觉得不好,本项目采用的是rem布局,因为它可以自适应不同屏幕尺寸的设备。
这里我们要用到两种单位:
- vw: viewport width,相对于视口的宽度
1vw为视口宽度的1%,100vw为设备的宽度
- rem: 相对于根元素html的字体大小的单位
比如2rem=2倍的根字体大小
rem布局非常简单,其基本原理就是根据屏幕不同的分辨率,动态修改根字体的大小,让所有的用rem单位的元素跟着屏幕尺寸一起缩放,从而达到自适应的效果。
拿我的项目来举例:我们的设计稿是按照iphone6来设计的(iphone6实际宽度 375px),而设计稿上的宽度是750px,之前是直接把所有尺寸/2,现在我会这样实现自适应:
html {
font-size: calc(100vw / 7.5);//除以的7.5是根据设计稿的屏幕宽度来定的,这样750px宽度下根元素字体大小则为750px/7.5=100px=1rem
}
其中,100vw是设备宽度deviceWidth,这样就实现了不同设备宽度下,动态修改根字体font-size的大小,比如:
deviceWidth = 320,font-size = 320 / 7.5 = 42.6667px //iphone5
deviceWidth = 375,font-size = 375 / 7.5 = 50px //iphone678 X
deviceWidth = 414,font-size = 414 / 6.4 = 55.2px //iphone678 plus
所以设计思路就是,根据设计稿将html的font-size设置为100px。比如750的设计稿,就除以7.5。
这样设计的原因是:实现适配只要在代码里把宽高直接将设计稿的尺寸除以100即可,换算很方便。
比方设计稿上宽高300px、96px的元素,就可以在代码中这么设置宽高
.test {
width: 3rem
height: .96rem
}
//反过来验证下,iphone6,显示宽度为3*50px=150px ok
但是我们又不能改变默认字体大小的展示,因此还要加一句#app的字体大小重置
html {
font-size: calc(100vw / 7.5);
}
#app {
font-size: initial; //重置页面字体大小恢复为浏览器默认16px,否则就显示成50px了
}
以上设计思路的大优点就是:方便计算。
2.弹性布局
典型应用场景:关键元素高宽和位置都不变,只有容器元素在做伸缩变换。针对这种需求,记住一个大佬总结好的适配原则就好:文字流式,控件弹性,图片等比缩放。
二.遇到的问题
2.1 弹窗遇到滚动穿透(高频问题)
1.什么是滚动穿透?
移动端弹出fixed弹窗的话,若底部背景页面存在滚动条,则滑动弹窗会导底部的背景页面跟着滚动,称为“滚动穿透”。但是这种情况在pc上是不会出现的。
若弹窗下层(背景页面上层)还有fixed定位的一遮罩,此时滑动遮罩背景页面也会跟着一起滚动。
2.解决方案
(1)添加样式:overflow: hidden;
打开弹窗时,给背景页面内容超出自身高度的div添加样式:
overflow:hidden
关闭弹窗时,移除样式,或设
overflow:auto
e.g.
watch: {
'showModal'(val) {
let ele = document.querySelector('内容超出自身高度的div');
if(val) {
ele.style.overflow = 'hidden';
} else {
ele.style.overflow = 'auto';
}
}
}
本项目目前用的就是这个方案
缺点:
1.滚动位置会丢失,页面会回到顶部 : 无论打开弹窗前页面背景滚动到什么位置,打开弹窗时,页面背景都会回到顶部。因此只能适用触发弹窗出现的按钮位于屏中的情况。
(2)阻止移动端的touchmove事件
methods:{
preventDefault:function(e){e.preventDefault();},
//禁止背景页面滚动
forbidScroll(){
document.body.addEventListener('touchmove',
this.preventDefault,{passive:false});//阻止默认事件
},
//解除背景页面禁止滚动
allowScroll(){
document.body.removeEventListener('touchmove',
this.preventDefault,{passive:false});//打开默认事件
},
},
watch: {
'showModal'(val) {
if(val) {
this.fixedBody();
} else {
this.looseBody();
}
}
}
缺点:
1.若弹窗内部有滚动,就无法滚动了
优点:
1.解决了上面的问题:弹窗打开时,背景页面处在打开弹窗前滚动到的位置,并不是顶点处
(3)body position:fixed定位,并记录背景页面滚动位置,关闭弹窗时还原滚动位置
methods:{
//body fixe定位,把当前的滚动位置赋值给css的top属性
fixedBody () {
let scrollTop = document.body.scrollTop || document.documentElement.scrollTop
document.body.style.cssText += 'position:fixed;width:;top:-' + scrollTop + 'px;'
},
//清除fixed固定定位和top值;并恢复打开弹窗前滚动位置
looseBody () {
let body = document.body
body.style.position = 'static'
let top = body.style.top
document.body.scrollTop = document.documentElement.scrollTop = -parseInt(top)
body.style.top = ''
}
},
watch: {
'showModal'(val) {
if(val) {
this.forbidScroll();
} else {
this.allowScroll();
}
}
}
优点
1.底部背景页面和有滚动条的弹窗都可以滚动
2.可以记录背景页面滚动位置
缺点
1.当弹窗内部滚动到底部or顶部时,再去滑动背景页面,再回头滚动弹窗内部,又无法滚动了。
然而看别人却似乎没有遇到过这个问题,所以暂时还没找到解决方案……
2.2 移动端的1px问题
1.1px问题
设计稿上的1px边框,我们再iphone6上应为0.5px,因为设计稿宽度为750px,iphone6宽度为375px。而简单粗暴的写0.5px在ios8+上支持,但安卓不支持
2.解决方案
直接列我经常用的比较完美的方案:
使用伪元素+定位+transform
<div class="wrap">
内容区域
</div>
(1)设置border-top
.wrap {
width: ;
height: .8rem;
padding: .24rem .32rem;
position: relative;
&::after {
content: " ";
position: absolute;
left: 0;
top: 0;
right: 0;
height: 1px;
background: #ebebf0;
transform-origin: 0 0;
transform: scaleY(.5);
}
}
(2)设置border-bottom
.wrap {
width: ;
height: .8rem;
padding: .24rem .32rem;
//div相对定位
position: relative;
//伪元素定位
&::after {
content: " ";
position: absolute;
left: 0;
bottom: 0;
right: 0;
height: 1px;
background: #ebebf0;
transform-origin: 0 0;
transform: scaleY(.5);
}
}
(3)设置border-left
.wrap {
width: ;
height: .8rem;
padding: .24rem .32rem;
margin-left: .32rem;
position: relative;
&::after {
content: " ";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 1px;
background: #ebebf0;
transform-origin: 0 0;
transform: scaleX(.5);
}
}
(4)四周的边框都设置
.wrap {
//width: ;
height: .8rem;
padding: .24rem .32rem;
margin-left: .32rem;
margin-right: .32rem;
position: relative;
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 200%;
height: 200%;
transform-origin: 0 0;
transform: scale(.5);
border: 1px solid #ebebf0;
}
}
原理:将伪元素的长和宽先放大2倍,然后再设置一个边框,以左上角为中心,缩放到原来的0.5倍
优点:
全机型兼容,而支持圆角
2.3 弹出数字键盘,只能输入数字
<input type="number" pattern="[0-9]*" @input="onInput($event.target.value)"v-model="number" />
若还需要限制数字位数,只能自己手动js控制,因为maxlength属性在type为number情况下不生效
onInput(val) {
if (val >= 999) {
this.number = val.slice(0,3);
}
}
2.4 各种兼容性问题:
1.ios问题
(1)ios12以上,收起键盘页面卡死,造成点击错乱
因为键盘收起时输入框会失焦,因此,监听失焦事件即可
document.body.addEventListener('focusout', () => {
window.scroll(0, 0);//失焦后强制让页面归位即可
});
这种方法适用于只有一个输入框的场景,当有多个输入框时,会遇到输入框之间切换的情况。此时,每当切换,上一个聚焦元素会失焦,就会执行失焦事件处理函数,因为弹出键盘会让页面整体往上滚一点,执行了函数就会让页面归位掉下来,因此我们还需要去判断是输入框之间的切换,还是收起键盘。
每次切换输入框,页面掉下来的问题效果图:
解决方法:
let isReset = true;//是否归位
document.body.addEventListener('focusin', () => {
isReset = false; //聚焦时键盘弹出,焦点在输入框之间切换时,会先触发上一个输入框的失焦事件,再触发下一个输入框的聚焦事件
});
document.body.addEventListener('focusout', () => {
isReset = true;
setTimeout(() => {
//当焦点在弹出层的输入框之间切换时先不归位
if (isReset) {
window.scroll(0, 0);//确定延时后没有聚焦下一元素,是由收起键盘引起的失焦,则强制让页面归位
}
}, 300);
});
(2)ios点击延迟
暂时没遇到这个问题
(3)滚动卡顿
在滚动的容器上加上这句即可
-webkit-overflow-scrolling: touch;
2.安卓问题
(1)当输入框在可视区域偏下位置时,弹出键盘,输入框部分会被遮挡,且编辑过程无法向下滑动
原因:
首先分析一下ios和安卓键盘弹起时的表现
IOS 软键盘弹起表现
在 IOS 上,输入框(input、textarea 或 富文本)获取焦点,键盘弹起,页面(webview)并没有被压缩,或者说高度(height)没有改变,只是页面(webview)整体往上滚了,且滚动高度(scrollTop)为软键盘高度。
Android 软键盘弹起表现
同样,在 Android上,输入框获取焦点,键盘弹起,但是页面(webview)高度会发生改变,一般来说,可视区高度会减小(原高度减去软键盘高度),除了因为页面内容被撑开可以产生滚动,webview 本身不能滚动。
问题效果如下:
由图可见,fixed定位的底部footer正好遮住textarea的底部
解决方法:
//window.onresize 监听页面大小变化,该方法只会在安卓执行
window.onresize = function () {
if (document.activeElement.tagName === "INPUT" || document.activeElement.tagName === "TEXTAREA") {
let ele = document.activeElement;
setTimeout(()=>{
ele.scrollIntoView();//焦点元素滚到可视区域的问题
},0);
}
}
解决后的效果:
ios很正常,是这样的:两张图分别为弹出键盘前和点击输入框弹出键盘后:
(2)当滚动遇到弹窗
对于背景页面有滚动,弹窗内部也有滚动区域的情况,点击弹窗里的输入框时,弹起键盘页面高度变小,导致此时可视区域呈现的正好都是弹窗内部滚动区域,因此只能滚动该区域内容,整个弹窗无法滑动显示全
问题图:
解决方案:
在弹起键盘时将遮罩高度变为现在视口的高度,因为高度变小,导致里面内容高度超出,此时手动加入滚动条即可滚动完整的弹窗,而不是只停留在弹窗内部有滚动条的区域
let originHeight = document.documentElement.clientHeight || document.body.clientHeight;
//ios不会触发resize事件
window.onresize = function () {
let resizeHeight = document.documentElement.clientHeight || document.body.clientHeight;
if (resizeHeight < originHeight) {
//键盘弹起
setTimeout(() => {
document.querySelector('弹窗遮罩').style.height = document.body.offsetHeight + 'px';
document.querySelector('弹窗遮罩').style.overflow = 'scroll';
},0);
} else {
//键盘收起
document.querySelector('弹窗遮罩').style.height = 517 + 'px'; //517为弹窗原先高度
}
}
另外还要备注一条:
之前弹窗底部按钮的footer部分用的是fixed定位,bottom为0,导致键盘弹出后,视口高度变小,footer改为相对现在的视口底部(也就是键盘顶部)fixed定位,会遮挡住弹窗内容一部分。因此,还要同时取消使用fixed定位,改为将footer写到页面底下。
作者:Wowoy
链接:https://juejin.im/post/5de72b1f51882512360d3910
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。