使用HBuilder基于HTML5编写新闻客户端APP的一些实验

1、一些基本概念javascript

一、HBuilder
css

按照个人理解,HBuilder是一个基于eclipse二次开发的IDE,主要对HTML5的开发作了许多优化,刚发布没多久,目前基本稳定,可是仍然有一些BUG。html

用HBuilder开发移动APP的好处是能够链接手机实时调试APP(对于Android其实也就是用adb跟DDMS),链接手机后IDE的界面上会出现你所链接的手机,对项目中的文件修改回实时同步到手机上,下图是ADT和HB的对比:java

二、HTML5 Plusnode

其实就是HBuildeBuild发团队推出的一种框架,尽管官方说这是一个开放性的HTML5的扩展标准,可是从目前其提供的资源以及行为来看,我跟人不太认同官方的说法。目前也只有HBuilder能打包基于HTML5+的项目,一样地,HBuilder开发移动应用用的就是这个框架。jquery

目前市场上可见的、基于此框架作的应用有CSDN的新闻客户端,基于此框架能够调用许多手机的原生服务。如下分别是CSDN的客户端及HBuilder提供的示例程序,HBuilder中有这两个项目的源代码:android

2、实验目标(需求分析)web

一、目标算法

首先,是肯定一下用HB和H5+能不能作出原生手机应用的效果,其次是看看开发的复杂度。chrome

在此,我定下一个目标:仿照网易新闻的APP,用HB作一个新闻客户端的雏形,重点是作出几个手机客户端常见的手势操做以及一些动画效果。

二、分析

下图是对网易新闻客户端使用过程的几个截图,也是此次要实现的内容:

①、首页(主页面),包含一个LOGO、一个菜单按钮、一个用户按钮、一个导航栏、一个新闻标题列,标题列的顶部是一个图片轮播栏;

②、在首页中拖动页面,页面可渐渐缩放以呈现出菜单,点击菜单按钮也是一样的效果;

③、在首页左右拖动可在头条、推荐、娱乐等导航间进行切换;

④、点击新闻列表进入新闻内容页,在内容页中拖动页面可返回。

第一步就是实现上面的内容,其中大部分手势操做在许多原生应用中都是司空见惯的,可是对于H5应用来讲仍是比较罕见,也没有几个案例能够参考,所以就蛮力开撸了。


3、正文

一、大概思路

既然是基于HTML5的,那么就必须发挥HTML5的优点。其一就是兼容性,一次开发便可在多个平台上使用,这就意味着一样须要在代码中进行设备检测,也要进行屏幕自适应,这些都是移动端网站用得比较多的手段了,也再也不详述。官方也说了建议别用各类JS库,那么全部动画效果、页面布局都须要本身来写。

固然,必要的偷懒仍是得有的,用个jQuery,在不影响性能的地方使用也无妨。

按照上面的分析,目前要作的是两个页面:第一个页面就是新闻列表页,包含那个缩放菜单;第二个页面是新闻内容的查看页面。

看完HTML5+的文档(虽然内容很少,可是该有的内容都有,仍是不错的),大概明白了新闻列表页中的全部效果都得基于HTML5来写,也就是说,写这个页面能够用PC浏览器来进行调试。

进入新闻内容页的时候,能够用H5+提供的窗体接口,此页面中拖动返回之类的操做能够利用修改窗体的位置来实现。

页面中的图标均可以在FontAwesome这个图标字体里边找到,能够省下一大笔时间。

那么,接下来就是开撸了。

二、代码结构

代码列表以下:

index是新闻列表页,view是新闻内容查看页,pic是图片浏览页(点击图片会进到这个页面,先放置);js目录中,frame.js中放置一些公用代码,main.js是处理index逻辑的代码,main.XXX的几个js文件则是头条、推荐等导航的逻辑分管。

三、DOM操做及模板机制

从图片轮播到新闻列表,均可以先作好一个模板而后给予模板动态生成,因此,首先,我写了几个模板:

<section data-node="tpl" style="display:none;">
    <div class="content-imghead" data-node="imghead">
        <div class="content-imghead-container" data-node="container">
        </div>
    </div> 
    <div class="content-imgtitle" data-node="imgtitle">
        <div class="content-imgtitle-desc" data-node="desc"></div> 
        <div class="content-imgtitle-words" data-node="words"></div>
        <div class="conntent-imgtitle-dot i1" data-node="i1"></div>
        <div class="conntent-imgtitle-dot i2" data-node="i2"></div> 
        <div class="conntent-imgtitle-dot i3" data-node="i3"></div>
        <div class="conntent-imgtitle-dot i4" data-node="i4"></div>
    </div>
    <div class="content-horitem" data-node="horitem"> 
        <img class="content-horitem-img" src="img/news_default_320_160.png" data-node="img"></img>
        <div class="content-horitem-title" data-node="title">新闻标题新闻标题新闻标题新闻标题新闻标题</div> 
        <div class="content-horitem-desc" data-node="desc">副标题描述副标题描述副标题描述副标题描述</div>
        <div class="content-horitem-icon" data-node="icon"><i class="icon-facetime-video"></i></div>
    </div>
</section>

上面的代码中,从上而下分别是图片轮播的轮播页、图片轮播的标题页、新闻标题列表项的模板。HTML代码中有data-node这个属性,这是后面用来获取DOM用的,data-node的相关JS代码以下:

var F = window['F'] = {
    getNodes : function(obj){
        var i;
        if(!obj.childNodes || obj.childNodes.length == 0){
            return obj;
        }
        for(i = 0; i < obj.childNodes.length; i ++){
            if(obj.childNodes[i].dataset && obj.childNodes[i].dataset.node){
                obj[obj.childNodes[i].dataset.node] = F.getNodes(obj.childNodes[i]);
            }
        }
        return obj;
    }
};

也就是一个简易的DOM获取的方式,经过这句代码:

_doc = F.getNodes($('body')[0]);

能够分层遍历获取body下全部带了data-node的节点,譬如上面的模板,能够经过_doc.tpl.imghead来取得图片轮播的模板。

四、页面

调界面的过程是痛苦的,持续三四个小时的眼镜干涩的调整过程不提,总而言之最终写好的页面长这样:

仿真度也足够了吧?在此感谢FontAwesome,否则画图标可能还要花上一两个小时……

(其实作完界面以后我就下班了,那天是4.29,就快五一了,心情激动啊)

最终首页的布局以下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>...</title>
    <link rel="stylesheet" type="text/css" href="css/main.css"/>
    <link rel="stylesheet" type="text/css" href="css/font-awesome.css"/>
    <script src="js/jquery.js" type="text/javascript" charset="utf-8"></script>
    <script src="js/frame.js" type="text/javascript" charset="utf-8"></script>
    <script src="js/main.top.js" type="text/javascript" charset="utf-8"></script>
    <script src="js/main.recommend.js" type="text/javascript" charset="utf-8"></script>
    <script src="js/main.entertainment.js" type="text/javascript" charset="utf-8"></script>
    <script src="js/main.sport.js" type="text/javascript" charset="utf-8"></script>
    <script src="js/main.finance.js" type="text/javascript" charset="utf-8"></script>
    <script src="js/main.js" type="text/javascript" charset="utf-8"></script>
    <style type="text/css"></style>
</head>
<body>
	<div class="side" data-node="side_left">
		<div class="side-list" data-node="list">
			<div class="side-list-item selected"><i class="icon-list-alt"></i>新闻</div> 
			<div class="side-list-item"><i class="icon-rss"></i>订阅</div>
			<div class="side-list-item"><i class="icon-picture"></i>图片</div>
			<div class="side-list-item"><i class="icon-film"></i>视频</div>
			<div class="side-list-item"><i class="icon-comments-alt"></i>跟帖</div>
			<div class="side-list-item"><i class="icon-headphones"></i>电台</div>
		</div> 
	</div>
	<div class="page" data-node="page_main"> 
		<div class="touchcover" data-node="cover"></div>
		<div class="header" data-node="header"> 
			<div class="header-btn-left" data-node="btn_left_menu"><i class="icon-reorder"></i></div>
			<div class="header-btn-right"><i class="icon-user"></i></div>
		</div>
		<div class="navbar" data-node="navbar">
			<div class="navi selected" data-node="navi0">头条</div> 
			<div class="navi" data-node="navi1">推荐</div>
			<div class="navi" data-node="navi2">娱乐</div> 
			<div class="navi" data-node="navi3">体育</div>
			<div class="navi" data-node="navi4">财经</div>
			<div class="naviselector" data-node="selector"></div> 
		</div>
		<div class="naviswitch"><i class="icon-angle-down"></i></div> 
		<div class="container" data-node="container">
			<div class="content" data-node="content1"> 

			</div>
			<div class="content" data-node="content2">
 
			</div>
			<div class="content" data-node="content3">

			</div>
			<div class="content" data-node="content4">

			</div>
			<div class="content" data-node="content5">

			</div> 
		</div>
	</div> 
	<section data-node="tpl" style="display:none;">
		<div class="content-imghead" data-node="imghead">
			<div class="content-imghead-container" data-node="container">
			</div>
		</div> 
		<div class="content-imgtitle" data-node="imgtitle">
			<div class="content-imgtitle-desc" data-node="desc"></div> 
			<div class="content-imgtitle-words" data-node="words"></div>
			<div class="conntent-imgtitle-dot i1" data-node="i1"></div>
			<div class="conntent-imgtitle-dot i2" data-node="i2"></div> 
			<div class="conntent-imgtitle-dot i3" data-node="i3"></div>
			<div class="conntent-imgtitle-dot i4" data-node="i4"></div>
		</div>
		<div class="content-horitem" data-node="horitem"> 
			<img class="content-horitem-img" src="img/news_default_320_160.png" data-node="img"></img>
			<div class="content-horitem-title" data-node="title">新闻标题新闻标题新闻标题新闻标题新闻标题</div> 
			<div class="content-horitem-desc" data-node="desc">副标题描述副标题描述副标题描述副标题描述</div>
			<div class="content-horitem-icon" data-node="icon"><i class="icon-facetime-video"></i></div>
		</div>
	</section>
</body>
</html>

新闻内容页的布局以下:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
		<title></title>
		<link rel="stylesheet" type="text/css" href="css/view.css"/>
		<link rel="stylesheet" type="text/css" href="css/font-awesome.css"/>
		<script src="js/frame.js" type="text/javascript" charset="utf-8"></script>
		<script src="js/jquery.js" type="text/javascript" charset="utf-8"></script>
		<script src="js/view.js" type="text/javascript" charset="utf-8"></script>
		<style type="text/css"></style>
	</head> 
	<body>
		<div class="page" data-node="page_view">
			<div class="header" data-node="header">
				<div class="header-btn-left" data-node="btn_back">
					<i class="icon-angle-left"></i>
				</div> 
				<div class="header-tip" data-node="tip">1245</div>
			</div>
			<div class="content" data-node="content">
				<h3 class="title">新闻标题</h3>
				<p class="note">副标题&nbsp;02-02 00:00</p>
				      
				<p>新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容</p>
				<p>新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容</p>
				<p>新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容</p>
				<p>新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容</p>
				<p>新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容</p>
				<p>新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容</p>
				<p>新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容</p>
				<p>新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容新闻内容</p>
			</div> 
			<div class="footer" data-node="footer">
				<input type="text" class="footer-reply" placeholder="写跟帖" data-node="input" /> 
				<div class="footer-btn-favor" data-node="btn_favor"><i class="icon-star-empty"></i></div>
				<div class="footer-btn-share" data-node="btn_share"><i class="icon-share"></i></div>
			</div>
		</div>
	</body>
</html>

五、屏幕自适应

作这个的时候,手头上的测试机有:iPhone 五、Note 三、MI 三、MI 2S

其实移动端的页面自适应,最关键的仍是devicePixelRatio这个值,看iPhone4的分辨率比3GS高了四倍,其实对于网页来讲仍是同样的分辨率,看Note 3的1080P多高,其实在页面上比起肾果来也多不了多少……

所以,我可耻地用了最省事的自适应方法:在页面加载完后一次insertRule来修改样式!

这样作的好处是省事省资源(生下来的资源能够作不少事啊……),坏处是面对屏幕翻转、页面尺寸更改等事件会表现得一塌糊涂。

首先,我用了这么一坨代码来获取页面的尺寸属性以及设定一些属性

G = {};
// 屏幕宽度
G.WIDTH                = $(window).width(),
// 屏幕高度
G.HEIGHT               = $(window).height(),
// 首页滑到这个位置则断定用户要打开左侧菜单
G.LIMIT_SLIDE_PAGESIDE = G.WIDTH / 4;
// 菜单打开时,首页滑到这个位置则认为用户要关闭菜单
G.LIMIT_SLIDE_PAGEBACK = G.WIDTH / 4 * 3;
// 菜单打开时首页页面滑到这个位置
G.SITE_X_PAGESIDE      = G.WIDTH / 3 * 2;
// 页面缩放最小值
G.SCALE_MAX_PAGESIDE   = 0.2;
// 菜单打开时页面缩放的比率
G.SCALE_PAGESIDE       = 1 - G.SITE_X_PAGESIDE / G.WIDTH * G.SCALE_MAX_PAGESIDE;
// 菜单滑动的动画时间
G.TIME_SLIDE_ANIMATE   = 200;
// 页面导航切换的动画时间
G.TIME_SPAGE_ANIMATE   = 200;
// 菜单打开时,菜单的顶部对齐缩小后的页面顶部
G.SITE_SCALE_BOTTOM    = (G.HEIGHT - G.SCALE_PAGESIDE * G.HEIGHT) / 2;

而后,我又用了这么个方式来让一些页面元素自适应:

style = document.styleSheets[document.styleSheets.length - 1];
style.insertRule('.container{height:' + (G.HEIGHT - 80) + 'px;}', 0);
style.insertRule('.content{height:'   + (G.HEIGHT - 80) + 'px; width:' + G.WIDTH + 'px;}', 0);
style.insertRule('.content-imghead{height:'   + (G.WIDTH / 2 - 1) + 'px; width:' + G.WIDTH + 'px;}', 0);
style.insertRule('.content-imghead-img{height:'   + (G.WIDTH / 2 - 1) + 'px; width:' + G.WIDTH + 'px;}', 0);
style.insertRule('.content-imghead-container{height:'   + (G.WIDTH / 2 - 1) + 'px;}', 0);
style.insertRule('.content-horitem{width:'   + (G.WIDTH - 24) + 'px;}', 0);

CSS的代码很枯燥,在此就不提了。

六、手势动画

这实际上是APP最重要的部分,手势的合理与否很大程度上决定了应用的生死(这是一个伏笔)。

左右滑动能够经过设置style.left的值、能够用translateX来实现,缩放效果则只能用transform的scale函数来实现(这也是一个伏笔)。

我定义了一堆变量用来存放动画过程的一些参数:

// 运行时参数
R = {};
R.isIOS = navigator.userAgent.match(/iPhone/i) || navigator.userAgent.match(/iPod/i) || navigator.userAgent.match(/iPad/i);
R.PAGE_S    = 'main',
R.container = _doc.page_main.container;
R.navIndex  = 0;
R.navCount  = 5;
R.animating = false;
R.scaleRate = 1;
// 页面从哪里开始滑
R.fromX     = 0;
// 页面滑动的步进
R.step_t    = 64;
// 页面缩放的步进
R.step_s    = (1 - G.SCALE_PAGESIDE) / G.SITE_X_PAGESIDE * 64;

其中,isIOS这个是直接抄CSDN的APP的,在写完这个以后我还作了对chrome、ASOP自带浏览器等的判断,最终仍是删掉了。

在滑动页面开关菜单的过程当中,最重要的是保证动做的合理与流畅,譬如用户滑动到必定程度后松手时,页面是回弹(操做取消)仍是自动滑到另外一边(操做生效)、自动滑动的过程当中滑动速率跟用户手动滑动的部分速率一致、动画效果不卡帧等。

所以,我在新闻列表页中定义了五套touch事件处理函数来分别处理不一样的操做,分别是:

①、没有任何操做时(路由)

②、动做多是滑到左侧列表菜单

③、左侧菜单打开时的响应

④、多是左侧菜单滑回页面

⑤、多是 主页横向左侧翻页

⑥、 主页横向右侧翻页

断定逻辑以下:

T.on(_doc, 'start', function(e){
    var sx, sy, cx, cy, rt, fx, ft, step_t, step_s, an = false;
    switch(e.touches.length){
    // 触摸事件
    // 单点手势
    case 1:
        sx = e.touches[0].clientX;
        sy = e.touches[0].clientY;
        switch(R.PAGE_S){
        // 在主屏幕中,判断是否切换到菜单
        case 'main':
            T.on(_doc.page_main, 'move'  , m0);
            T.on(_doc.page_main, 'end'   , e0);
            T.on(_doc.page_main, 'calcel', c0);                 
            break;
        // 在左侧菜单中,判断是否切换回主屏幕
        case 'side-l':
            T.on(_doc.page_main.cover, 'move'  , m2);
            T.on(_doc.page_main.cover, 'end'   , e2);
            T.on(_doc.page_main.cover, 'calcel', c2);   
            break;
        }
        break;
    default:
        break;
    }
    /* ... */
}

(由于是第一次用H5作APP,对其不熟悉,也许其中有很多多余的部分)

T.on是另外封装的一个兼容函数,相关代码以下:

var T = window['T'] = {
    on : function(dom, type, func){
        if(("ontouch" + type) in dom){
            dom["ontouch" + type] = func;
        }else{
            dom.addEventListener("touch" + type, func);
        }
    },
    off : function(dom, type, func){
        if(("ontouch" + type) in dom){
            dom["ontouch" + type] = null;
        }else{
            dom.removeEventListener("touch" + type, func);
        }
    },
    offNormal : function(dom, func1, func2, func3){
        if('ontouchstart' in dom){
            dom.ontouchmove   = null;
            dom.ontouchcancel = null;
            dom.ontouchend    = null;
        }else{
            dom.removeEventListener('touchmove',   func1);
            dom.removeEventListener('touchend',    func2);
            dom.removeEventListener('touchcancel', func3);
        }
    }
};

图片轮播则用了另外的一套touch事件,绑定在imghead上。

提及拖动的原理,在个人理解中,关键就是两个坐标:开始拖动时的横坐标SX、当前拖动位置的横坐标CX,拖动距离就是CX - SX,那么只须要把页面的位移设置为CX - SX就完事了。

当触摸事件结束以后,该如何自动完成剩下的动画?最初,我是直接用了CSS3的transition属性,当触摸结束后,给元素加一个transition:linear;之类的样式,动画效果结束后再去掉这个样式,后来,我发现这个样式在android平台下存在一个略微严重的问题:卡。

也不知道ASOP自带的浏览器是怎么回事,也不知道为何HBuilder在打包的时候要封装Android的自带浏览器,这个浏览器的性能真的很糟糕,不管多好的手机,只要用这个浏览器,其表现跟若干年前的单核手机没什么区别。

其实说到这,这篇文章已经能够结束了,事实上,作到这,我已经没有什么激情再把这个应用原型写下去了,由于我预感目前在Android上H5+是玩不下去了。

最后我用了这么一个方式来兼顾动画效果:

function menuSlideIn(){
    if(R.animating){
        return;
    }
    R.PAGE_S =  'side-l';
    if(R.isIOS){
        _menuSlideIn();
    }else{
        $(_doc.page_main.cover).show();
        _doc.page_main.style.left = G.SITE_X_PAGESIDE + 'px';
    }
}
function _menuSlideIn(){
    R.animating                          =  true;
    R.scaleRate                          -= R.step_s;
    R.fromX                              += R.step_t;
    _doc.page_main.style.left            =  R.fromX + 'px';
    _doc.page_main.style.webkitTransform =  'scale(' + R.scaleRate + ', ' + R.scaleRate + ')';
    if(R.fromX >= G.SITE_X_PAGESIDE){
        $(_doc.page_main.cover).show();
        R.animating                          = false;
        _doc.page_main.style.left            = G.SITE_X_PAGESIDE + 'px';
        _doc.page_main.style.webkitTransform = 'scale(' + G.SCALE_PAGESIDE + ', ' + G.SCALE_PAGESIDE + ')';
    }else{
        requestAnimationFrame(_menuSlideIn);
    }
}

对于左侧的菜单,在非IOS上禁用缩放效果。甚至在用户拖动完毕后不显示动画效果而直接让菜单闪到那个位置,卡帧的动画还不如不播,所以我干脆用了requestAnimationFrame这个Android上没有的函数。

最后是新闻内容页,用户点击新闻时,调用这么个方法打开新页面:

plus.ui.createWindow("view.html").show('slide-in-right', 200);

万幸,H5+自带了一系列的窗口打开、关闭动画效果。

由于这个页面是在新窗口的,所以要实现拖动返回的话,就再也不是设置DOM的style.left属性了,而是调用这么个方法:

W.setOption({left : ox});

来设置窗体的位置。

相应地,设置窗体位置就要换一种算法了,由于窗体移动后,每次检测到的X位置所相对的位置都改变了。

假设页面一开始的偏移是OX = 0,用户触摸屏幕的位置是SX,用户第一次移动时的位置是CX,那么,把OX设置为CX - SX,而后把页面的位置设置为OX;

第二次以及以后的移动事件中,OX = OX + CX - SX,而后把页面的偏移设置为OX;

结束触摸以后,假如当前页面的偏移达到了返回的断定点,那么调用:

plus.ui.closeWindow(W, 'slide-out-right', 200);

关掉这个窗口,不然让页面动画返回左边缘。

在IOS上,以上的动画效果很流畅完美地实现了,可是,不知道是否是个人使用方式不对,在Android下这段代码的表现是悲剧的。

在拖动过程当中,页面不断地闪动,拖着拖着,页面就消失了!

因此,这段代码,我是这么写的:

function m1(e){
    cx = e.touches[0].clientX;
    cy = e.touches[0].clientY;
    _doc.page_view.header.tip.innerHTML = cx;
    if(R.isIOS){
        if(lx !== null){
            ox = ox + cx - sx;
            W.setOption({
                left : ox
            });
        }else{
            ox = cx - sx;
            W.setOption({
                left : ox
            });
        }
    }
}

是的,对于非IOS的系统,我直接取消掉了这个效果。

至此,文章真的要结束了,由于在Android上,这个应用雏形的用户体验已经低到了极致。


4、总结

实验结果是:H5+能够作APP,不太能作APK。

我衷心但愿这篇文章里出现的Android下的糟糕表现是个人技术及实现思路太糟糕而形成的,由于我真心渴望能够直接用HTML5写出媲美原生的应用的那一天的到来。

也许代码中还有不少的能够优化的地方,也许进行极致的优化以后应用能够运行得比较流畅,但这倒是不是我所想看到的,须要作得这么完美才能实用的技术,是没法推广的。

对这个半成品都算不上的应用感兴趣的话,能够移步http://www.lxrmido.com/webcv2/看看实际效果

对代码感兴趣的同窗能够把svn://www.lxrmido.com/webcv2这个目录checkout下来看看,但愿对你有所帮助

最后的最后,但愿你们能够告诉我是否是真的哪里想歪了,总感受HBuilder不该该那么脆弱……

相关文章
相关标签/搜索