基于Vue的移动端h5项目总结

6,333 阅读12分钟

以前都是写pc,后来需要写h5移动端项目,会遇到一些自适应和兼容性等方面的问题,下面从自己写过的h5项目中稍稍做点总结。

一.自适应布局

1.1 vw与rem相结合实现适配

1.原理

开启一个移动端项目的基础,首先是想好如何在代码中实现移动端适配。之前没有经验,第一个项目里简单粗暴地采用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 / 7.5 = 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.优化

正巧今天跟同事有在讨论适配问题,他给我提了一个自己从来没有注意到的问题,就是只是像上面这样设计的话,会无限制放大,在大屏上很不好看,评论里也有优秀的童鞋提到。所以我把同事分享的Tingglelaoo 这位大佬写的通过限制最大最小宽度进行优化的方法分享给大家。 其实很简单,就是给根元素字体大小限制最大最小值,以及 body 也增加最大最小宽度限制,这样就可以改善用户体验了。

html {
    //设置根字体大小单位为vw,页面元素的尺寸单位都设为rem,搭配vw和rem,可实现布局根据视口变化而变化
    font-size: calc(100vw / 7.5);
    // 同时,通过Media Queries 限制根元素字体最大最小值
    @media screen and (min-width: 320px) {
        font-size: 64px;
    }
    @media screen and (max-width: 540px) {
        font-size: 108px;
    }
}

// body 也增加最大最小宽度限制,避免默认100%宽度的 block 元素跟随 body 而过大过小
body {
    max-width: 540px;
    min-width: 320px;
}

#app {
    font-size: initial;
}

2.2 弹性布局

典型应用场景:关键元素高宽和位置都不变,只有容器元素在做伸缩变换。 针对这种需求,记住一个大佬总结好的适配原则就好:文字流式,控件弹性,图片等比缩放

image

二.遇到的问题

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.forbidScroll();
            } else {
                this.allowScroll();
            }
        }
    }

缺点:

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:100%;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.fixedBody();
            } else {
                this.looseBody();
            }
        }
    }

优点

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: 100%;
        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: 100%;
        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: 100%;
        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 {
        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倍

image

优点: 全机型兼容,而且支持圆角

一般像这种项目里到处会用到的样式,最好把它提取成公共样式,比如在styles文件夹下建一个mixins.less文件,存放这些样式混合集,然后在用的组件里直接引用即可。 另外,还要进行媒体查询,兼容除iphone6以为的各种机型。 最终如下:

//mixins.less
//上边框
.setTopLine(@clolor) {
    content: " ";
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    height: 1px;
    background: @clolor;
    transform-origin: 0 0;
    /* 1.5倍屏 */
    @media (-webkit-min-device-pixel-ratio:1.5){
        transform: scaleY(0.6666);
    }
    /* 2倍屏 */
    @media (-webkit-min-device-pixel-ratio:2){
        transform: scaleY(0.5);
    }
    /* 3倍屏 */
    @media (-webkit-min-device-pixel-ratio:3){
        transform: scaleY(0.3333);
    }
}

//四条边框
.setAllLine(@clolor,@radius: 0) {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    transform-origin: 0 0;
    border: 1px solid @clolor;
    @media (-webkit-min-device-pixel-ratio: 1.5){
        width: 150%;
        height: 150%;
        transform: scale(0.66666);
        border-radius: @radius * 1.5;
    }
    @media (-webkit-min-device-pixel-ratio: 2){
        width: 200%;
        height: 200%;
        transform: scale(0.5);
        border-radius: @radius * 2;
    }
    @media (-webkit-min-device-pixel-ratio: 3){
        width: 300%;
        height: 300%;
        transform: scale(0.33333);
        border-radius: @radius * 3;
    }
}
//某组件
<style lang="less">
@import "~@/styles/mixins.less";
    .operate-wrap {
        height: .92rem;
        padding: 0 .32rem;
        position: relative;
        &::before {
            .setTopLine(#ebebf0);
        }
    }
</style>

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);//失焦后强制让页面归位即可
});

这种方法适用于只有一个输入框的场景,当有多个输入框时,会遇到输入框之间切换的情况。此时,每当切换,上一个聚焦元素会失焦,就会执行失焦事件处理函数,因为弹出键盘会让页面整体往上滚一点,执行了函数就会让页面归位掉下来,因此我们还需要去判断是输入框之间的切换,还是收起键盘

每次切换输入框,页面掉下来的问题效果图:

image

解决方法:

 let isReset = true;//是否归位
document.body.addEventListener('focusin', () => {
    isReset = false; //聚焦时键盘弹出,焦点在输入框之间切换时,会先触发上一个输入框的失焦事件,再触发下一个输入框的聚焦事件
});
document.body.addEventListener('focusout', () => {
    isReset = true;
    setTimeout(() => {
    //当焦点在弹出层的输入框之间切换时先不归位
        if (isReset) {
            window.scroll(0, 0);//确定延时后没有聚焦下一元素,是由收起键盘引起的失焦,则强制让页面归位
        }
    }, 300);
});

参考文章H5页面 ios 键盘收起后弹出层焦点错位

(2)ios点击延迟

暂时没遇到这个问题

(3)滚动卡顿

在滚动的容器上加上这句即可


 -webkit-overflow-scrolling: touch;

2.安卓问题

(1)当输入框在可视区域偏下位置时,弹出键盘,输入框部分会被遮挡,且编辑过程无法向下滑动

原因: 首先分析一下ios和安卓键盘弹起时的表现

  • IOS 软键盘弹起表现

在 IOS 上,输入框(input、textarea 或 富文本)获取焦点,键盘弹起,页面(webview)并没有被压缩,或者说高度(height)没有改变,只是页面(webview)整体往上滚了,且滚动高度(scrollTop)为软键盘高度。

  • Android 软键盘弹起表现

同样,在 Android上,输入框获取焦点,键盘弹起,但是页面(webview)高度会发生改变,一般来说,可视区高度会减小(原高度减去软键盘高度),除了因为页面内容被撑开可以产生滚动,webview 本身不能滚动。

问题效果如下:

image

由图可见,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);
    }
}

解决后的效果:

image

ios很正常,是这样的: 两张图分别为弹出键盘前和点击输入框弹出键盘后:

image

image

(2)当滚动遇到弹窗

对于背景页面有滚动,弹窗内部也有滚动区域的情况,点击弹窗里的输入框时,弹起键盘页面高度变小,导致此时可视区域呈现的正好都是弹窗内部滚动区域,因此只能滚动该区域内容,整个弹窗无法滑动显示全

问题图:

image
解决方案:

在弹起键盘时将遮罩高度变为现在视口的高度,因为高度变小,导致里面内容高度超出,此时手动加入滚动条即可滚动完整的弹窗,而不是只停留在弹窗内部有滚动条的区域

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按正常布局写到属于同一文档流中的页面最底下。