移动端H5应用,开发过程中主要遇到的问题:
1.适配不同手机
答:手淘方案(rem版本)
2.布局(固定位置、显示隐藏、栅栏)
答:使用position、visibility和display、flex
3.下拉刷新和上滑加载
答:touchstart、touchmove、touchend和scroll实现
4.缓存数据
答:使用localStorage
5.跳转与返回
答:location和history
6.输入与虚拟键盘
下面详细说说:
移动端头疼的一点,就是适配问题,这里关注的是手机分辨率,不同的手机分辨率不一样,苹果手机更是用上了视网膜屏,比如iphone6的DPR是2,6s的DPR是3。具体参考使用Flexible实现手淘H5页面的终端适配,当然,现在已经有更好的方法,那就是使用vw和vh。
手淘方案中用到的是rem这个CSS单位,1 rem等于body.fontSize
的大小,默认值是16px,最小值为12px,主要分成两步
1.计算设备实际分辨率,从而得出body.fontSize
(rem的基数)并动态写入。首先在网页上写入width=device-width
,声明页面宽度为设备宽度,对于Android手机,根据不同设备的分辨率,将宽度除以10就是该设备的rem基数,对于iOS,则还需要计算dpr,所以其实际分辨率为设备宽度*dpr,设备高度*dpr,rem基数设备宽度*dpr/10。
2.元素的大小使用rem为单位。通过上一步,将设备的实际分辨率宽度看作10rem,如果一个元素在一个设备中为1rem,那么在另一个设备中只要也是1rem,就能保证其相对大小是一样的(其实和等比例缩放差不多)。我们只需要计算UI图上元素的rem即可复用,如笔者项目中一级标题字体大小为0.33rem,间距是0.44rem等。
引用的方式如下:
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<script src="../common/framework/js/flexible-0.3.2.debug.js" type="text/javascript">script>
<link rel="stylesheet" type="text/css" href="../common/framework/css/flexible-0.3.2.debug.css">
有一点建议,引用该方案后可能需要添加div{font-size:0}
,不然Chromium中ipone模拟时div的上内边距会有一定空白(实际设备上没试过,可能不会有影响,但是浏览器上看着不爽就改了)。
position、visibility和display、flex都是CSS的属性。
固定区域滑动的实现需要滑动的内容为absolute,父级为fixed,而当父元素为relative而子元素为absolute(absoulte会找最近的父级realative直到为空)时可实现左右滑动。固定区域滑动实现如下
<div class="content">
<div class="ground-container">
<div class="ground-panel">
<div class="ground ground-red">
div>
<div class="ground ground-blue">
div>
div>
div>
div>
.ground-container{
position: fixed;
top: 10%;
bottom: 10%;
left: 0;
right: 0;
}
.ground-panel{
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: auto;
}
.ground{
width: 100%;
height: 720px;
}
.ground-red{
background-color: red;
}
.ground-blue{
background-color: blue;
}
元素的显示和隐藏是常见的事,一般在JQuery中使用$(id).show和$(id).hide()即可,但这种隐藏有可能不是想要的,这时就需要visibility了。
举个例子,一般登录校验的时候,密码错误会有相应的提示。假设这个提示本身是隐藏的,在密码输入栏和确定按钮的中间,只有校验出错的时候才显示。这是如果使用$(id).show和$(id).hide()会发现确定按钮会被挤下去,位移了一段距离。这是因为$(id).show和$(id).hide()实际上是封装了display:block和display:none的,display:none会将元素隐藏不可见,从渲染后的页面中也不会预留位置。而我们想要的隐藏只是不可见而已,元素还是在相应的位置的,这是就需要visibility了,即设置为visibility:visible或者是visibility:hidden使其显示或隐藏。笔者认为$(id).show和$(id).hide()比较适合弹窗和下拉导航一类的,visibility则适合提示一类。
相比较百分比设置,流式布局更适合使用flex。一旦遇到如三列布局这样的,需要自己计算各列的百分比是比较麻烦的,而flex就自动做好了这件事。同时还能有各种各样的显示方式。实现代码如下:
<div class="panel">
<div class="item">item1div>
<div class="item">item2div>
<div class="item">item3div>
div>
.panel{
display: -webkit-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
}
.item{
-webkit-box-flex:1;
-webkit-flex:1;
flex:1;
}
touchstart、touchmove、touchend和scroll
笔者使用的是jQuery WeUI、中的扩展组件很强大,但是有一点,其中的下拉刷新和上滑加载会冲突,当不处于顶部的时候,下滑仍然会触发刷新开发中就遇到这样的问题,触摸实现的下拉刷新和滑动事件冲突,倒是直接向下滑动时会触发下拉刷新,解决方案就是计算触摸移动的距离,判断是否下滑,这个在touchend中实现,同时在touchmove中根据移动距离判断是否阻止传递。这是因为在事件流touchstart-touchmove-touchend-click中,event.preventDefault()会阻止传递,同时position、z-index也会影响事件。
笔者项目中实现代码如下
<div class="container">
<div class="head_div">
<div class="head_title_2"><span > 标题spani>div>
<div class=" head_btn" onclick="add()" ><img src="../resources/img/add_gray.png">div>
div>
<div id="content" class="content weui-pull-to-refresh">
<div class="content_placeholder">
div>
<div class="weui-pull-to-refresh__layer">
<div class='weui-pull-to-refresh__arrow'>div>
<div class='weui-pull-to-refresh__preloader'>div>
<div class="down">下拉刷新div>
<div class="up">释放刷新div>
<div class="refresh">正在刷新div>
div>
<div id="tabContent" class="tab_content">
div>
<div class="footer_placeholder">
div>
div>
div>
$('.content').pullToRefresh().on("pull-to-refresh", function() {
pullDownFlag = true;
refreshPage();
});
var prev=0, current=0;
$(window).scroll(function() {
var bottomX = $(document).height() - $(window).height();
current = $(window).scrollTop();
if(current>prev){
console.log('下滑');
}
prev = current;
if(current<=0){
console.log("滚动条已经到达顶部为" + bottomX);
show();
}
if (current >= bottomX) {
console.log("滚动条已经到达底部为" + bottomX);
//if(lock) add();
show();add();
}
prev = current;
});
引用JQuery-weui
部分如下
/**
* jQuery WeUI V1.0.1
* By 言川
* http://lihongxun945.github.io/jquery-weui/
*/
/* global $:true */
/* global WebKitCSSMatrix:true */
(function($) {
"use strict";
$.touchEvents = {
start: $.support.touch ? 'touchstart' : 'mousedown',
move: $.support.touch ? 'touchmove' : 'mousemove',
end: $.support.touch ? 'touchend' : 'mouseup'
};
$.getTouchPosition = function(e) {
e = e.originalEvent || e; //jquery wrap the originevent
if(e.type === 'touchstart' || e.type === 'touchmove' || e.type === 'touchend') {
return {
x: e.targetTouches[0].pageX,
y: e.targetTouches[0].pageY
};
} else {
return {
x: e.pageX,
y: e.pageY
};
}
};
$.fn.join = function(arg) {
return this.toArray().join(arg);
}
})($);
/* ===============================================================================
************ Pull to refreh ************
=============================================================================== */
/* global $:true */
+function ($) {
"use strict";
var PTR = function(el) {
this.container = $(el);
this.distance = 50;
this.attachEvents();
}
PTR.prototype.touchStart = function(e) {
//if($(window).scrollTop()>100) return;
if(this.container.hasClass("refreshing")) return;
var p = $.getTouchPosition(e);
this.start = p;
this.diffX = this.diffY = 0;
};
PTR.prototype.touchMove= function(e) {
if(this.container.hasClass("refreshing")) return;
if(!this.start) return false;
var p = $.getTouchPosition(e);
this.diffX = p.x - this.start.x;
this.diffY = p.y - this.start.y;
if(this.diffY < 0) return;
this.container.addClass("touching");
//修改,与body内div的scroll兼容 start
if($(document.body).scrollTop()<this.distance){
e.preventDefault();
e.stopPropagation();
this.diffY = Math.pow(this.diffY, 0.8);
this.container.css("transform", "translate3d(0, "+this.diffY+"px, 0)");
}
//修改,与body内div的scroll兼容 end
if(this.diffY < this.distance) {
this.container.removeClass("pull-up").addClass("pull-down");
} else {
this.container.removeClass("pull-down").addClass("pull-up");
}
};
PTR.prototype.touchEnd = function() {
this.start = false;
if(this.diffY <= 0 || this.container.hasClass("refreshing")) return;
this.container.removeClass("touching");
this.container.removeClass("pull-down pull-up");
this.container.css("transform", "");
//新增判断,不再顶部一定范围内不可下拉刷新
if($(document.body).scrollTop()>this.distance || Math.abs(this.diffY) <= this.distance) {
}
else {
this.container.addClass("refreshing");
this.container.trigger("pull-to-refresh");
}
};
PTR.prototype.attachEvents = function() {
var el = this.container;
el.addClass("weui-pull-to-refresh");
el.on($.touchEvents.start, $.proxy(this.touchStart, this));
el.on($.touchEvents.move, $.proxy(this.touchMove, this));
el.on($.touchEvents.end, $.proxy(this.touchEnd, this));
};
var pullToRefresh = function(el) {
new PTR(el);
};
var pullToRefreshDone = function(el) {
$(el).removeClass("refreshing");
}
$.fn.pullToRefresh = function() {
return this.each(function() {
pullToRefresh(this);
});
}
$.fn.pullToRefreshDone = function() {
return this.each(function() {
pullToRefreshDone(this);
});
}
}($);
目前存储数据有几种,常见的是Cookies,而在H5中有sessionStorage和localStorage,顾名思义,前者的生命周期和session差不多,也就是当前窗口或标签页,一旦关闭就会被清除数据,而后者则是一直保存下去,需要主动清除才行。同时localStorage在Android中默认是关闭的,需要添加如下代码启用。
webSettings.setDomStorageEnabled(true);
感觉是不是sessionStorage比较省事,那么为什么最终选择了localStorage?
其实笔者一开始在项目中使用的是sessionStorage,但却遇到问题。项目是嵌入到其他app应用中的,有时需要调用原生页面(如摄像头的页面),然后再返回。项目本身是作为一个webview存在,这时sessionStorage会失效,所以选择了localStorage。
localStorage的使用方法和java的hashmap有点相似,但值得注意的是只能保存字符串,直接保存对象也只会是[object Object]
,笔者一般都是将对象(或数组)先通过JSON.stringify转换为字符串,然后使用的时候再通过JSON.parse转为对象使用的。sessionStorage和localStorage的方法一样,常用的有:
//设置一个键值
localStorage.setItem("second_list","");
//获取一个键值
localStorage.getItem("second_list");
//删除一个键值
localStorage.removeItem("second_list")
//获取指定下标的键的名称(如同Array)
localStorage.key(0);
//清空
localStorage.clear();
笔者的项目是嵌入到原生APP应用中的,在Android中打开后会启动webview(目前也就理解是个和浏览器差不多的)加载,所以网页间的跳转和返回也应该像在浏览器上的一样,跳转可以通过location.href='new.html'
,而返回可以通过H5提供的History API,即history.back()
或history.go()
。
笔者一开始是处理返回(在页面上的点击返回按钮)是直接打开新页面的,location.href='pre.html'
。这会导致一个问题,当(在Android中)使用物理按键返回的时候,原生APP是根据历史纪录返回的,这样会返回到其他的页面。举个例子,打开APP进入到webview中,现在在A页面中,打开了B页面,然后点击返回。这时webview的历史记录是A->B->A,而不是理想中只有的A,这种情况下在A按下物理按键的时候,本应该是直接退出的,但是会出现先返回到B,再返回到A才能退出的情况,这样是不能接受的。所以就需要使用History API。
当然也可以监听物理按键(Android可以,iOS好像不行)处理,不过感觉这样不友好,而且麻烦。
if (window.history && window.history.pushState) {
$(window).on('popstate', function () {
location.href='pre.html';
});
}
这里不得不提的是contenteditable
这个属性,真的是太棒了。元素添加这个就会变成可自动调整高度的文本框。值得注意的是,如果是在flex布局中使用,需要添加word-break: break-all;
才能实现自动换行。
当点击input
或者是含有contenteditable
属性的元素时,手机会自动弹出虚拟键盘,这时如果有元素是fixed且在底部的时候,需要输入的元素会被挡住,或者是跑到奇怪的位置上去,这时可以通过监听window是否变小,来判断虚拟键盘是否弹出,然后隐藏fixed的元素,变大则显示。
var h = $(window).height();
$(window).resize(function (){
if( $(window).height()'.footer_btn_panel').hide();
$('.footerButton').hide();
}else{
$('.footer_btn_panel').show();
$('.footerButton').show();
}
});
H5也提供了scrollIntoView实现该效果。
var element = document.getElementById("sb_form_q");
element.scrollIntoView(false);
使用虚拟键盘的搜索,需要通过form来实现。
<form action="#">
<input type="search" />
form>
$('form').on('submit', function(){
//search
document.activeElemnt.blur();
return false;
});