淘宝彩票移动项目开发实践
作者按:如今越来越多的互联网产品开始在移动终端发力,终端产品越来越丰富。但是,平台差异带来的开发成本浪费很让人头疼。一段时间以来,淘宝彩票前端组也在努力寻求移动终端项目开发最佳实践,尽管诸多方面不甚成熟,但抛砖引玉,希望我们的总结整理会对大家有所启发。
淘宝彩票客户端产品目前有两条体系:
- 1、 原生应用:包括iPhone和Andoird上原生的客户端应用
2、 嵌入式应用:作为子应用的软件包形式提供,嵌入到第三方的客户端软件中,比如淘宝主站客户端以及支付宝客户端等
由于产品的共通性,这两类应用都是需要考虑较多复用性和开发成本的。在原生App中,有不少内容是可以用简单的HTML5来实现的,比如iPhone客户端中的“订单页”:
而在子应用的软件包里,则有更多的地方可以运用HTML5来实现,包括一些复杂的富交互。例如支付宝客户端中各彩种的实现:
App的形式
目前移动终端上的应用主要以Native App为主,这种应用的优势是:
- 1,性能快,体验好;
2,可访问本地资源,更有效的利用设备,节省流量;
3,并且已有一定规模(App Store),付费模式明朗。
缺点是:
- 1,开发成本高,移植性差;
2,所有发布需要经过App Store的审核。
当然,目前还存在一些Web App,用户可以直接通过浏览器来访问,例如Gmail。这类App的开发和维护的成本低,可以天然的在各种终端上执行,并且容易迭代更新,无需用户进行安装。不过就现阶段而言,无论从速度上还是交互上,用户体验与Native App的差距还是比较大的。既然两种App都各有利弊,那么我们就干脆将二者相结合:用原生控件做外壳和交互,保证流畅的用户体验和完整的推广渠道;使用HTML5来展示内容,保证内容的迅速迭代更新,即时响应用户需求。于是就诞生了Hybrid App,这也是目前最流行最合适的一种App形式。
对于前端工程师而言,在移动平台上没有了IE系列的困扰,HTML5的很多特性都可以大胆的运用。不同的是,我们需要设置一些针对移动终端的Meta属性,以及更合理的利用Media Query( 主流移动设备的分辨率信息)。
JS框架的选择
找到适合使用HTML5的场景,这时就需要考虑JS框架的选择。首先需要明确的是,类似jQuery和YUI这种“重型”的库,由于包含了很多处理兼容性方面的代码,且API设计过于细致而显得太过庞大,并不适合移动端。我们可以选取一些专业的用于Mobile开发的JS类库,例如jQuery Mobile,Sencha Touch等。jQuery Mobile是目前最流行的一个移动开发的框架,文档丰富,社区活跃,有很多的UI控件供我们使用,并且提供对多页面的支持(通过Ajax方式读取内容,并提供页面切换的过渡动画)。我认为jQuery Mobile的最强大之处就在于其UI方面的支持,但这一部分恰恰不是我所需要的。Sencha Touch是ExtJs的移动版,对于不熟悉ExtJs的人来说有一定的学习成本。
我们选择了 zepto.js作为底层库,使用 sea.js进行模块的管理和发布,原因是基于 CMD规范的模块文件最终需要打包入应用的软件包里, 或是打包后发布到线上;此外,我们使用 backbone.js为基础的MVC架构,用来剥离应用的数据部分;使用 underscore.js做为前端模板引擎(backbone重度依赖);使用 iScroll.js为我们提供模拟滚动的功能。这些都是一些专业的“小库”,很适合移动端的开发。当然,具体情况需要具体分析,没有万能的框架,只有万能的开发者。如果时间允许,也可以自己来定制一套满足自身需求的基础库。毕竟在移动端,一切以 “精简”为主。
应用架构的选择
首先抛出两个小问题:
- 1, OPOA近两年来在互联网得到了广泛应用,相比于之前的多页面切换和跳转的方式,这个方法也更优雅,用户体验更好。那么在移动端,OPOA是否依旧适用 呢?
2, HTML5最大的优势就是跨平台,只需要开发一套代码,就可以完全通用于不同的终端。但是,真的一套代码就能“吃”尽所有移动终端么?
先解答第一个问题。其实我们在开始尝试的时候,完全是参照PC端的OPOA来进行的,把很多的内容都放在一个Page里面,通过URL或Hash的方式来管理页面的显示。但实践表明,在移动端这样做是有很大弊端的。首先,在PC上显示区域比较大,我们可以先给用户展示一个框架,然后在分块的去显示内容,但是移动终端由于屏幕变小,往往同时只能展示一个区域,那么我们首先要根据URL判断显示哪个区域,然后再去加载这个区域的内容, 这样一来,让本来在移动端就变慢了许多的页面更加的雪上加霜。其次,为了减少请求,我们通常会把大量的前端模板都塞到这个仅有的Page中,不仅导致页面体积变大,性能降低,而且维护困难。因此,我认为,在移动端最好的方式还是传统的多页面开发。
那么肯定有人会问,这样子的话岂不是会损失页面切换的流畅性,让用户不爽么。这个问题我们是可以解决的。第一就是采用jQuery Mobile的方式(Web App中非常适用),切换时用Ajax去请求新的内容,然后再渲染到页面中。这种方法在某些特定条件下会有问题:我们仍然需要使用URL来进行历史的管理,如果将这些页面作为静态文件打包在客户端中的话,在一些Android系统的手机中,硬件会将这些带后缀的文件当成一个完整的文件去查找(例如 index.html?page=XXX),然后发生错误。第二个方法就是在Hybrid App中,我们可以借助客户端来帮助我们进行页面的切换。你只需要告诉客户端即将要跳转的URL路径(可以是网络请求,也可以是本地静态文件),然后由客户端进行跳转。这样就可以在客户端代码里面设置Webview切换的动画效果,使客户端的整体风格更加的统一。
针对第二个问题,我们刚开始的目标就是用一套代码来自适应所有的客户端。结果发现,在iPhone和Android上,尽管风格差别较大,但是整体的结构和布局是很相似的,那么只需要设置两份CSS文件,再提供一份API,分别在两个平台上做实现就可以了,维护起来很方便;但是在iPad 上,由于布局和交互变化比较大,为了复用,不得不在原代码上增加分支,不但破环了原代码的完整性,而且维护时会产生一些意想不到的错误。这样就得不偿失了。因此,如果手机客户端和平板电脑客户端差异很大的话,最好还是分开单独进行处理,而不是为了“复用”而复用,大家可以根据具体的情况制定自己的策略。例如彩票的iPad客户端,我们就是纯粹基于HTML5为pad量身定做的:
Webview及其与HTML5的通信
Webview是手机中内置的一个基于webkit内核的SDK,是搭载Web页面的容器,也是负责Web页面和Native App之间相互通信的桥梁。在不同平台上Webview的实现是有较大差异的。
在Android平台中是支持JS方法和Java方法的直接调用的,我们通过下面的代码即可实现互相调用。
//将JAVA对象绑定到JavaScript中 mWebView.addJavascriptInterface(new JsToJava(), 'stub'); //在JavaScript中调用Java方法 window.stub();
function invokedByJava(data){ //do something } //在Java中调用JavaScript方法 public void onClick(View v) { mWebView.loadUrl("javascript:invokedByJava('java_data')"); } //打开webview,调用页面 mWebView.loadUrl("file:///xxx.html");
但在iOS平台上,SDK没有原生提供JavaScript调用Native代码的API,只有反向调用的方法。要想在JavaScript中调用Native,一般有两种方法:一种是Phonegap采用的iframe方法,也是比较推荐的,如下所示;另一种是直接修改页面的location,在大部分情况是可用的,但是有时候会产生一些莫名其妙的错误。这两种方法的原理都是相同的:利用Webview去截获JS发起的特殊的网络请求,然后对其进行处理。
//Objective-C - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { NSURL * url = [request URL]; if ([[url scheme] isEqualToString:@"gap"]) { //在这里做js调用native的事情 //... //完成之后回调js //[webView stringByEvaluatingJavaScriptFromString: @"alert('done')"]; return NO; } return YES; } //通知iPhone UIWebView加载url对应的资源,url格式为: gap:something function loadURL(url) { var iFrame; iFrame = document.createElement('iframe'); iFrame.setAttribute('src', url); iFrame.setAttribute('style', 'display:none;'); iFrame.setAttribute('height', '0px'); iFrame.setAttribute('width', '0px'); iFrame.setAttribute('frameborder', '0'); document.body.appendChild(iFrame); //发起请求后将其从DOM上移除 iFrame.parentNode.removeChild(iFrame); iFrame = null; };
除此之外,不同平台的Webview还需要进行一些特殊的设置,才能让其和原生的浏览器的行为表现相一致。例如:在Android Webview中,JavaScript和localStrorage都是默认禁止的,需要提前启用;Android的物理后退键默认是会关闭当前的 Bebview的,而不是执行history.back();在iOS5.0以下的系统中,设置格式检查的Meta属性会偶尔失效,需要在 Webview中直接关闭其格式检查等等。在D2沙龙的 PPT中有详细的描述,大家有感兴趣的可以看下。
速度/性能
对于一个互联网产品的用户体验来说,速度和性能几乎是最重要的因素。但令人遗憾的是,这也是目前阻碍Web App发展的最大的障碍。原因主要有以下几个方面:
1 低端设备多,受硬件的限制。根据木桶原理,一个木桶的水量是受最短的板的限制的。与之类似,我们在衡量页面性能的时候,也需要看其在中低端机型上的表现,尤其是广大的Android千元智能机。如果Android所有的机型都能有Samsung Galaxy的表现,我们也就不用愁眉苦脸了,但是在更多时候,我们不得不为了在HTC野火上能够正常的展示而放弃在Samsung上看起来非常帅气的效果。
2 简化的浏览器实现。本来相比桌面版的浏览器,移动版的浏览器已经在功能上做了一些简化,性能上有些差距。但是,与原生的移动浏览器相比,Webview会更慢。下图为一个页面分别在Mobile Safari和Facebook的iPhone客户端中的运行结果,我们可以发现,UIWebView中的JS运行时间大概是Mobile Safari的3-4倍,这恐怕也是Facebook放弃HTML5转向原生应用的一个原因之一。
针对上述原因,我们总结出一些性能优化的小Tip:
- 1 用HTML5的离线存储和本地存储进行缓存,或者将页面直接打包到客户端中,减少在网络下载中的耗时。
2 减少DOM数量,尽可能少的使用position:relative,减少对DOM的操作
3 用CSS动画代替JS动画,在Android平台上可以平稳退化,放弃动画效果(包括CSS3动画)
4 避免GIF图片的使用(消耗内存)
5 如果只是在移动端使用的话,请使用iScroll-lite来代替iScroll(iScroll里面增加了很多额外的功能,比如在PC上模拟滚动),在允许的情况下,可以关闭滚动条(滚动条也是创建的DOM元素)
关于滚动,还有一些其他的想法,大家有兴趣可以一试:
- 1 将页面拆分成多个Webview,这样子在每个Webview中就都是原生的实现了,但会增加一些页面间互相通信的代价。
2 用Canvas来代替Scroll,但是文字的渲染可能会有些问题。
由于篇幅原因,这里就不赘述更详细的性能问题了~
移动终端的网络环境
移动应用最大的特点就是方便、随处可用。但无论是2G还是3G,我国目前的网络状况都难以满足移动应用的要求。相比于Native App,Web App除了要下载数据和图片外,还需要下载一些额外的js/css文件,这些动辄上百K的静态文件对于平均速度还不到10K的2G网络来说实在是太庞大了。 因此,目前最好的应用场景还是将页面打包到客户端中,版本变化时提醒用户让其主动进行更新。尽管这样会损失一部分web页面的灵活性。
调试
开发时的调试相对方便,可以基于浏览器进行。但开发环境和最终的执行环境还是有一些差异的,需要对终端的View中的“页面”进行调试,现在我们的做法是 “打点”,即使用一个Log代理控件给代码埋点,输出log,以此来辅助我们的调试。目前常用的工具有Weinre和JSconsole等。这些工具的原理是比较类似的:通过网络在其他机器上连接了一个调试的GUI,捕获待调试网页的JavaScript运行环境来查看代码输出或者对代码求值,并可以监控和修改调试目标的DOM和CSS样式。但是由于调试不是真正的发生在移动设备上,所以无法设置和捕捉断点进行debug。
在最新的iOS6中,Safari(仅限Mac桌面版)中自带了web检查器工具,可以让我们更方便的对iPhone/iPad上的Safari进行远程调试。
更多内容可以点击 下载我在7月28日D2沙龙(北京场)的分享。