移动端开发的几条总结

2,858 阅读16分钟
原文链接: segmentfault.com

1、移动端的字体

最开始的的方案是:

 body {
  font-family: 'Helvetica Neue', Helvetica, 'microsoft yahei', sans-serif;
 }

升级后的方案:

 body {
  font-family: -apple-system, BlinkMacSystemFont, "PingFang SC","Helvetica Neue",STHeiti,"Microsoft Yahei",Tahoma,Simsun,sans-serif;
 }

以前iOS版本降级使用 Helvetica。 中文字体设置为华文黑体STHeiTi, iOS 9+ 就开始支持 -apple-system 参数了, Chrome 使用 BlinkMacSystemFont ,兼容 iOS/MacOS。
现在很多设计师的字体都是PingFang,所以这里做了一个兼容。 顺便用"Microsoft Yahei"兼容了一下Window系统。
原生 Android 下中文字体与英文字体都选择默认的无衬线字体。 但是因为安卓系统可以去改系统的默认字体,而且每一个手机厂家也会内置字体,所以直接让他去选择默认的吧。不用单独去折腾安卓的字体了。

2、移动端的适配

移动端的适配方案各个厂商都一套方案,但是现在主流的方案是阿里的flexible,具体可以去看下这篇文章: 《使用Flexible实现手淘H5页面的终端适配》
阿里的方案我用过一段时间,但是对于每个device pixel ratio 都要写一个样式,虽然可以用sass、less的mixmin用法来处理,但是不同品尺寸屏幕下所显示的文字个数不一致的问题(如下图:商品的标题),往往会导致用户认为这是一个bug。
iPhone4的渲染图

rem-9.jpg

iPhone6的渲染图
rem-9.jpg

所以后面的开发就抛弃了这个方案,选中在以前项目中运用到的方案,思路是跟淘宝的思路大体上是一样的,根据750px的设计稿来换算成rem,1px == 0.01rem;

CSS单位rem
在W3C规范中是这样描述rem的:
font size of the root element.

也就是根节点的字体的大小,简单的理解,rem就是相对于根元素<html>的font-size来做计算。而我们的方案中使用rem单位,是能轻易的根据<html>的font-size计算出元素的盒模型大小。

具体怎么换算呢?把750px的设计稿 1px对应0.01rem即可

思路:

var html=document.querySelector("html");
html.style.fontSize=html.offsetWidth/7.5+"px"
window.onresize=function(){
   var a=document.querySelector("html");a.style.fontSize=a.offsetWidth/7.5+"px";
};

注意:并不是所有的地方都用rem来处理。
移动端的1px边框。
在处理移动端1px边框的时候有两种方案,其中一种方案就是将initial-scale=0.5还有一种方案就是通过伪类来处理。

父级 {
    positon: relative;
}
父级:after {
    content: " ";
    width: 200%;
    height: 200%;
    position: absolute;
    top: -1px;//之所要写成-1px而不是0是因为这个会将整个框下移1px,所以为了避免整个问题将元素上移1px
    left: 0;
    z-index: 1;
    border: 1px solid #ddd;
    border-bottom: 1px solid #ddd;
    -webkit-transform: scale(0.5);
    transform: scale(0.5);
    -webkit-transform-origin: 0 0;
    transform-origin: 0 0;
}

优化后的方案:

;(function(designWidth, rem2px, fixedWidth) {
    //容错
   designWidth = designWidth || 750;//传入设计稿的宽度
   rem2px = rem2px || 100;//rem to px 的关系
   fixedWidth = fixedWidth || 0;//固定最大宽度
   //获取当前html文档
   var docEl = document.documentElement;
   //获取body
   var body = document.querySelector("body");
   //设置固定宽度的大小
   if (!Number(fixedWidth)) {
       //不存在固定值,或者固定值为0
      body.style.maxWidth = designWidth / rem2px + 'rem';
   } else {
     body.style.maxWidth = fixedWidth / rem2px + 'rem';
   }
   body.style.margin = 'auto';
   //这里不设置body的position,为了底部存在可以让positon:absolute的可以吸在键盘上
   //屏幕的宽度
   var tempWidth = window.screen.width;
   var tempHeight = window.screen.height;
   //最小的宽度,以这个宽度来渲染,可以保证旋转的时候字体大小不变 为什么不用文档的宽度, 因为浏览器或者默认的app有时候会占用导航栏,
   //这个时候宽度和高度就会被裁剪一部分,但是这个时候屏幕的宽高是不会因为浏览器或者app的导航栏而被裁剪
   var minWidth = tempWidth > tempHeight ? tempHeight : tempWidth;
   //手机方向
   var orientation = window.orientation;
   //获取当前默认字体的大小,因为安卓可以设置默认字体的大小来进行计算
   var tempDom = window.document.createElement('div');
   tempDom.style.width = '1rem';
   tempDom.style.display = "none";
   var head = window.document.getElementsByTagName('head')[0];
   head.appendChild(tempDom);
   var defaultFontSize = parseFloat(window.getComputedStyle(tempDom, null).getPropertyValue('width'));
   tempDom.remove();
   //设置字体的大小
   window.onresize = function() {
       //延时是因为屏幕旋转获取新的高度需要一定的时间去重新获取高度和宽度
       setTimeout(function() {
            if (typeof(orientation) == "undefined") {
                //如果不支持orientation 那么就根据屏幕的宽高来判断
                var newWidth = window.screen.width;
                if (newWidth != tempWidth) {
                       tempWidth = newWidth
                       //如果屏幕的宽度值改变了
                      ReSetFontSize();
                }
           }
          else {
                 if (orientation != window.orientation) {
                      //设置最新的屏幕方向 为什么要延迟,因为文档去重新并且渲染高度是有一个时间段的
                      orientation = window.orientation;
                        ReSetFontSize();
           }
      }
    }, 100);
};
function ReSetFontSize() {
            //设置body的高度,body的高度不能直接设置成100%会导致重绘,同时为了防止fiex的bug(键盘弹出)
            body.style.height = docEl.clientHeight + "px";
            //设置字体大小
            docEl.style.fontSize = minWidth / designWidth * rem2px / defaultFontSize * 100 + "%";
}
ReSetFontSize();
document.body.classList.remove('vhidden');
})(750, 100, 750);

3、移动端的line-height

为什么这个要单独讲呢,因为这个问题在移动端出现的几率是100%,写习惯了PC端页面的开发者刚开始上手移动端经常会遇到文本垂直居中的问题,明明在谷歌模拟器里面看到文本是垂直居中的,但是为什么在手机上看到文字向上偏移的,而且安卓的问题比较大。transform虽然可以实现,但是感觉写起来却非常的繁琐。
提供两种方法,
1、padding

p{
    /*高度为90px*/
    font-size: .26rem;
    padding: 0.22rem;
}

虽然上面的方法可以实现,但是用起来的时候经常每次都要去计算高度(padding值等于设计高度减去font-size之后再/2),这样就比较麻烦,而且,在针对多行的时候还得计算,于是我又采用了下面的方式。利用 css3 flex布局的特性。

 p{  
    font-size: .26rem;
    height: 0.9rem;
    display: flex;
    display: -webkit-flex;
    -webkit-align-items:center;
    align-items:center;
    box-pack-align: center;
    -webkit-box-pack-align: center;
}

//同时水平居中也可以用下面的css3属性

box-pack: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
justify-content: center;

4、移动端的布局

移动端的布局情况有点多, 切记移动端最好不要使用postion:fixed,因为这个属性会在ios下产生很多bug。最终我根据之前的项目经验简单做了以下2种分类:

  1. 无固定栏页面

什么叫无固定项,所谓的无固定项页面就是整个网页从上到下没有没有固定在页面上的按钮,头部没有固定的按钮,底部没有固定的按钮,左右两侧也没有侧边栏。用户唯一的操作就是滑动页面到底部。这一类直接跟写PC一样的写法就行了。

  1. 固定项栏页面

基本上市面上所看到的移动端的页面都是固定头部和底部,少量的会加入侧边工具栏。这里主要讲固定头部和底部的。下面的例子主要把页面分为头部,内容,底部三部分。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>test</title>
    <meta name="keywords" content="test">
    <meta name="description" content="test">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
</head>
<style>
* {
    margin: 0;
    padding: 0;
    line-height: 1;
    border: 0;
    tap-highlight-color: transparent;
    -webkit-tap-highlight-color: transparent;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
}

body {
    font-size: 0;
    overflow: hidden;
    background-color: #f2f2f2;
}

.vhidden {
    visibility: hidden;
}

.flex-box {
    display: flex;
    display: -webkit-flex;
}

.vertical-center {
    box-pack: center;
    -webkit-box-pack: center;
    -webkit-justify-content: center;
    justify-content: center;
}

.horizontal-center {
    -webkit-box-align: center;
    align-items: center;
}

input {
    display: block;
    height: 0.88rem;
    font-size: 0.26rem;
    border: none;
    width: 100%;
    text-align: center;
}

input:focus {
    border: none;
    outline: none;
}

header {
    height: 1rem;
    position: relative;
    z-index: 1;
    background-color: #fff;
}

header div {
    box-flex: 1;
    -webkit-box-flex: 1;
    font-size: 0.26rem;
    width: 100%;
}

main {
    -webkit-overflow-scrolling: touch;
    height: calc(100% - 2rem);
    overflow-y: scroll;
    overflow-x: hidden;
    position: relative;
    z-index: 1;
}

main::-webkit-scrollbar {
    display: none;
}

main p {
    padding: 0.2rem;
    font-size: 0.26rem;
    color: #333;
}

footer {
    height: 1rem;
    position: relative;
    z-index: 1;
    background-color: #fff;
}

footer div {
    height: 0.88rem;
    font-size: 0.26rem;
}

footer.bottom-input {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 2;
}

.test1 {
    font-size: .26rem;
    padding: 0.22rem;
    text-align: center;
    background-color: #fff;
    margin: 0.1rem auto;
}

.test2 {
    font-size: .26rem;
    height: 0.9rem;
    display: flex;
    display: -webkit-flex;
    -webkit-box-align: center;
    align-items: center;
    box-pack: center;
    -webkit-box-pack: center;
    -webkit-justify-content: center;
    justify-content: center;
    background-color: #fff;
    margin: 0.1rem auto;
}
</style>

<body class="vhidden">
    <header class="flex-box">
        <div class="flex-box vertical-center horizontal-center">导航栏一</div>
        <div class="flex-box vertical-center horizontal-center">导航栏二</div>
    </header>
    <main>
        <div class="test1">
            这是内容部分1
        </div>
        <div class="test2">
            这是内容部分2
        </div>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <input class="flex-box vertical-center horizontal-center" type="text" name="" id="Input" placeholder="输入点什么">
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
        <p>这是内容部分</p>
    </main>
    <!-- <footer class="flex-box vertical-center horizontal-center">
        <div class="flex-box vertical-center horizontal-center" id="tt">底部按钮</div>
    </footer> -->
    <!-- <footer class="flex-box vertical-center horizontal-center bottom-input">
        <input class="flex-box vertical-center horizontal-center" type="text" name="" id="Input" placeholder="输入点什么">
    </footer> -->
</body>
<script>
(function(designWidth, rem2px, fixedWidth) {
    //容错
    designWidth = designWidth || 750;//传入设计稿的宽度
    rem2px = rem2px || 100;//rem to px 的关系
    fixedWidth = fixedWidth || 0;//是否固定最大宽度,固定宽度

    //如果没有确定就默认设计稿的宽度
    //获取当前html文档
    var docEl = document.documentElement;
    //获取body
    var body = document.querySelector("body");
    //设置固定宽度的大小
    if (!Number(fixedWidth)) {
        //不存在固定值,或者固定值为0
        body.style.maxWidth = designWidth / rem2px + 'rem';
    } else {
        body.style.maxWidth = fixedWidth / rem2px + 'rem';
    }
    body.style.margin = 'auto';
    //这里不设置body的position,为了底部存在可以让positon:absolute的可以吸在键盘上
    //屏幕的宽度
    var tempWidth = window.screen.width;
    var tempHeight = window.screen.height;
    //最小的宽度,以这个宽度来渲染,可以保证旋转的时候字体大小不变 为什么不用文档的宽度, 因为浏览器或者默认的app有时候会占用导航栏,
    //这个时候宽度和高度就会被裁剪一部分,但是这个时候屏幕的宽高是不会因为浏览器或者app的导航栏而被裁剪
    var minWidth = tempWidth > tempHeight ? tempHeight : tempWidth;
    //手机方向
    var orientation = window.orientation;
    //获取当前默认字体的大小,因为安卓可以设置默认字体的大小来进行计算
    var tempDom = window.document.createElement('div');
    tempDom.style.width = '1rem';
    tempDom.style.display = "none";
    var head = window.document.getElementsByTagName('head')[0];
    head.appendChild(tempDom);
    var defaultFontSize = parseFloat(window.getComputedStyle(tempDom, null).getPropertyValue('width'));
    tempDom.remove();
    //设置字体的大小
    window.onresize = function() {
        //延时是因为屏幕旋转获取新的高度需要一定的时间去重新获取高度和宽度
        setTimeout(function() {
            if (typeof(orientation) == "undefined") {
                //如果不支持orientation 那么就根据屏幕的宽高来判断
                var newWidth = window.screen.width;
                if (newWidth != tempWidth) {
                    tempWidth = newWidth
                    //如果屏幕的宽度值改变了
                    ReSetFontSize();
                }
            } else {
                if (orientation != window.orientation) {
                    //设置最新的屏幕方向 为什么要延迟,因为文档去重新并且渲染高度是有一个时间段的
                    orientation = window.orientation;
                    ReSetFontSize();
                }
            }
        }, 100);
    };
    function ReSetFontSize() {
        //设置body的高度,body的高度不能直接设置成100%会导致重绘,同时为了防止fiex的bug(键盘弹出)
        body.style.height = docEl.clientHeight + "px";
        //设置字体大小
        docEl.style.fontSize = minWidth / designWidth * rem2px / defaultFontSize * 100 + "%";
    }
    ReSetFontSize();
    document.body.classList.remove('vhidden');
})(750, 100, 750);
</script>

</html>

Phone手机在滑动overflow-y: scroll的元素上滑动的时候会顿卡,需要加入如下的css代码就可以了

-webkit-overflow-scrolling:touch;

上面的demo在中间部门有输入框并且在底部的时候去点击输入框,弹出的键盘会把输入框盖住,只有在输入部分内容之后输入框才会出现在视窗中。遇到这种情况需要加入如下代码。

var element = document.getElementById("box");
element.scrollIntoView();
//element.scrollIntoView(false);
//scrollIntoView 可选参数是 true false,默认是true
//true 元素的顶端将和其所在滚动区的可视区域的顶端对齐。
//false 元素的底端将和其所在滚动区的可视区域的底端对齐。

5、移动端的bfc

这个bfc不是格式化上下文,而是back forward cache(往返缓存),这个特性最早出现在Firefox和Opera浏览器上,可以在用户使用浏览器的“后退”和“前进”按钮时加快页面的转换速度。
原理就是浏览器会把之前访问的页面缓存到浏览器内存里面,当用在进行“后退”和“前进”操作的时候,浏览器会直接从缓存里面把页面展现到前台,从而不去刷新页面。
但是这样会导致用户在子页面与上一个页面之前存在管理的时候,操作后返回上个页面的状态并没有更改。
这个时候我们就需要去检测页面是不是从缓存里面读取出来的页面。

$(window).on('pageshow', function(event) {
    if (event.persisted) {
        location.reload(true);
    }
});

6、移动端与客户端的交互

现在内嵌H5开发的页面越来越多,前端跟客户端的交互也就越来越多,现在运用得最多的方式是用JSBridge来通信,其主要原理就是就是通过某种方式触发一个url(iframe)的改变,原生捕获到url,进行分析,得到自己想要的数据信息。
之所以要考虑用JSBridge来通信是考虑到
Android4.2以下,addJavascriptInterface方式有安全漏掉
iOS7以下,无法用到ios提供给前端最新的api messageHandlers
因为现有的手机基本上都是4.2以上以及iOS7以上,所以我们可以放心大胆使用上面两个属性。

var ua = navigator.userAgent.toLowerCase();
window.isAndroid = /android/.test(ua);
window.HtmlWebviewCallNative = function(par) {
    if (/客户端ua标识/.test(ua)) {
 //判断是否在客户端打开的页面
        if (isAndroid) {
            //Android这个是安卓向浏览器注入的对象,这个看安卓客户端给的是什么
            Android.HTMLCallNative(JSON.stringify(par));
        } else {
            window.webkit.messageHandlers.HTMLCallNative.postMessage(par);
        }
    } else {
        console.log(JSON.stringify(par))
    }
};

//调用方法eg

HTMLCallNative({
   functionName: 'callPhone',
   params: ['13883785984', '18323270482'],
   callback: 'callbackFunctionName'
});

原理以及参数说明
1.通过向window注册一个名字为HTMLCallNative的对象,以后每次向这个函数传递要通信的函数名和函数所需的参数即可;安卓是通过addJavascriptInterface直接注入页面,ios是通过WKWebView的新特性MessageHandler来这个对象来实现JS调用原生方法。
2.约定HTMLCallNative这个方法名为app中默认用来接受新交互规则的入口函数,安卓和ios分别拿到HTMLCallNative传过来的function名字和参数。
3.客户端通过反射机制,查找字符串函数名对应的函数并执行函数,此时通信成功。
4.functionName: 必为字符串,驼峰式命名,这个字符串为真正调用的方法,需要前端跟客户端共同来定义。
5.params:方法需要的参数,无需对参数进行encodeURIencodeURIComponent, 支持字符串,arrayobject
6.callback: 有回调函数时,传入这个参数,只需要传入函数名称即可,若回调函数需要传入参数,app端在调用的时候传入即可,跟当时业务相关,这里就不约定格式了。

相比JSBridge的优点:
1.在JS中写起来简单,不用再用创建iframe然后触发URL的方式那么麻烦了。
2.JS传递参数更方便。使用拦截URL的方式传递参数,只能把参数拼接在后面,如果遇到要传递的参数中有特殊字符,如&、=、?等,必须得转换,否则参数解析肯定会出错。
例如传递的url是这样的:
www.baidu.com/share/op...
使用拦截URL 的JS调用方式

loadURL("firstClick://shareClick?title=分享的标题&content=分享的内容&url=链接地址&imagePath=图片地址"); }

将上面的url 放入链接地址这里后,根本无法区分share_uuid是其他参数,还是url里附带的参数。
但是使用MessageHandler 就可以避免特殊字符引起的问题。

7、移动端唤起手机app

首先,我们看下安卓的配置文件和Scheme

<activity android:name = ".MainActivity">
    <intent-filter>
        <action android:name = "android.intent.action.MAIN" />
        <category android:name = "android.intent.category.LAUNCHER" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:host="xxx.com" android:scheme="abraham"/>
    </intent-filter>
</activity>

重点关<data android:host="xxx.com" android:scheme="abraham"/>,前端就需要根据这个字来唤起app

<a href="abraham:/xxx.com/?pid=1">打开app</a>

schema拼接协议的格式:[scheme]://[host]/[path]?[query]
当然ios的也有自己的协议,通常在写唤起app之前需要跟客户端的同事进行对接一下,拿到他们的协议。

注意schema协议要小写,否则会有不能响应的异常!

当然我们可以整合一下代码,把ios的也加进来:

var u = navigator.userAgent;var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; //android终端或者uc浏览器var isiOS2 = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端if (isAndroid) {
    window.location.href = "安卓提供协议url";
    /***打开app的协议,有安卓同事提供***/
    window.setTimeout(function() {
        window.location.href = '下载的地址';
    }, 2000);
} else if (isiOS2) {
    window.location.href = "IOS提供协议url";
    /***打开app的协议,有ios同事提供***/
    window.setTimeout(function() {
        window.location.href = '下载的地址';
    }, 2000);
} else {
    window.location.href = '下载的地址';
}

简单的唤起方法没有解决在其他应用唤起的bug,可以通过下面的唤起 [https://github.com/sunhaikuo/js-arouse-app][4]