最近业界关于移动开发中的究竟使用原生代码还是HTML 5代码的争论依然持续。目前来说有三种方式去进行移动开发
• 原生代码,
• hybrid(混合型)移动应用
• 移动Web app(使用HTML5)
使用HTML 5开发应用比起为每一种不同的平台从头开始编写代码,是一种能跨不同平台而代码量较少的一种方式。在这种情况下,大部分的用户界面,甚至全部的界面都可以通过HTML 实现。
“Hybrid应用”一词,指的是移动应用大部分是用HTML 5去编写界面,而部分需要访问特定设备功能的则使用原生代码。大部分这些原生代码并不是自然生成可视的,而仅仅通过特定的转换将数据返回给应用的HTML 5界面,再经过渲染返回给用户。PhoneGap这个框架就提供了这样的功能。
大部分关于HTML 5的争论并不是着眼于讨论它是否能胜任移动应用中的用户体验。Facebook创始人 Mark Zuckerberg在他的对Facebook在使用HTML 5中遇到的困难一文中作了相关评论。Facebook的一些问题可以通过由Sencha Touch框架进行解决(参考SenchaTouch自己的一个例子Fastbook),也可以通过LinkedIn在其infinite scrolling post一文中解决。此外,LinkedIn随后还提供了使用更多的原生代码解决关于转换方向的问题。
为何不进行整合?
与其使用全部的原生代码或者HTML 5代码去开发,为什么不将两者进行整合呢?使用混合型开发移动应用,可以同时利用原生和HTML 5代码开发用户界面。这就能让开发者在设计用户界面的时候能使用最适合的工具。
很明显,对于开发用户应用界面使用两种或者更多的技术是有其缺点的。最重要的是必须让开发者既掌握原生的开发又掌握HTML 5。使用原生代码开发的用户界面不大容易在其他平台使用,并需要重新开发。由于这些技术需要更广泛的知识面和上面提到的困难,为什么还有人尝试努力使用这它?
HTML 富文档界面
可能你的用户体验告诉你文档可以以富文本格式进行显示更为精彩。虽然在iOS上可以使用NSAttributedString进行富本文格式化,但这种文档的结构并不适合迁移到其他设备平台中,也不能完整由HTML再现。如果文档中需要使用如表格进行显示,则象NSAttributedString的解决方案是不能奏效的。考虑下面的一家基因检测公司的iPad应用,如下图,是一个混合型界面的例子:
请注意看上面的图,很难从表面上分辨这到底哪部分是用原生代码编写的,哪部分是用HTML 5编写的。这个界面中的右边部分是可变的长度。表格是包含可扩展的行,完全使用CSS3动画效果,而页面中其他文字部分都是相同的高度。
Documents such as this are what HTML does best. Trying to create something like this in native code would have entailed considerably greater effort, with little benefit to the user. In this application, the HTML5 is generated with a mustache template, via theGRMustache library for iOS.
像这样的页面使用HTML 5实现是最好的。如果尝试使用原生代码去实现则要花费很大功夫,而且对用户好处不多。在这个应用中,通过使用为iOS而设的GRMustache类库去生成相关的HTML 5模板。下图中用颜色区别表明了哪个部分是使用HTML5,哪个部分使用原生代码。
注意当用户移动滚动条的时候,在右侧顶部的文档头部会动态改变大小以显示更多的内容,从而说明用户界面之间是如何互动的。用户可以点在HTML 5实现部分的连接和按钮,以弹出一个内置的Web浏览器以加载相关内容。在本文接下来的部分,会看到在原生代码的界面和HTML 5的界面之间互相连接是很容易的事情。
两全其美的解决方法
正如我们所看到的,可以在同一个应用的屏幕中整合两种不同的技术以提供无缝的体验。你可能希望在iOS上使用UINavigationController和与之相关的 UINavigationBar控件,或者在Android应用中使用action bar,而剩下的部分则使用HTML 5渲染。
下面是一个使用HTML 5设计的购物车应用体验,但依然有保留原生的导航控件和用于页面导航的tab工具条:
这里,HTML 5内容实际上是从服务端加载的,它在整个用户体验中控制着流程。有相关的API允许服务端生成的HTML去修改导航控件中的内容以适应浏览的过程。
你可能会发现原生的控件,如iOS中的MapKit等都提供了比HTML 5控件更好的性能体验,在这种情况下,应该使用HTML 5代码去和iOS原生代码层通信,让其返回内容并呈现。
上面的两个图描述了一个综合运用原生和HTML 5的购物应用。用户首先在地图中选择了商店地址,然后数据将已HTML 5的方式呈现,用户并继续购物。
一旦用户完成了和地图的交互,结果则会马上传递到HTML 5页面中以作进一步处理。原生“控件”可以在任何平台(iOS、Android、Windows等)中实现,这取决于应用是如何架构,并且能从HTML5应用中调用。使用这个策略,HTML 5代码将提供对整个应用流程的控制,而原生代码则在后台运行等到需要的时候提供数据,如果处理恰当,用户是很难区分应用的哪一个部分是使用HTML5,哪一个部分是使用原生代码。
更容易的应用更新
大多数人都知道为了在iOs或者Android应用中更新应用,都必须重新通过相应平台正常的渠道去重新提交。对于Ios,可能需要等待七天去获得苹果的批准,这就很难在当今移动应用的狂潮中满足用户的迫切需要。如果通过HTML和Javascript的方法去给用户移动体验,则意味着可以通过Web去动态满足用户的需要。
正如前文说的,由于可以通过Web的方法调用应用中的原生代码,则看上去这其实是一种”桥梁”的机制。PhoneGap就是通过PhoneGap plugin interface去实现。之前的购物商店的例子就是使用这个方法。使用这个方法可以使得购物的流程如果改变后不再需要走繁琐的原生应用的发布流程。如果一个设计良好的界面可以使得页面和原生容器之间的通讯变为可能。
关于内容方面
对于来自Web的内容,通常最好能限制其通过这种桥接技术的流量。类似PhoneGap的内置的功能提供了很多设备的信息,其中一些可能会远远超出应用程序的所需要。如果由于某种原因Web内容被泄露,比如跨站点脚本攻击,你可能会突然发现这些内容会以某种方式调用本地代码。
基于这个原因,当需要使用混合型移动应用的时候,应该使用你自己的桥接代码,以严格控制哪些代码可以动态加载什么样的内容。一些平台有自己的控制方法去限制从Web viewer中对原生代码的调用。在Android中,类必须明确自己注册通过JavaScript访问。在Android 4.2中,可以进一步通过在需要从Web viewer中访问的方法前添加@ JavaScriptInterface注解去限制其调用。
需要考虑的要点
• 如果用户在线,直接从Web服务器容是很好的。请记住,移动应用的网络连接问题。如果网络连接中断,会发生什么事? 有的时候可以通过AppCache实现一些离线功能,但这需要规划。另一种方法在稍后介绍,使用的是本地存储。
• 托管在Web服务器上的内容需要一定的基础设施。现在有更多的需要被视为移动应用组成部分的“组件”。它不再仅仅是一些通过AppStore部署的原生代码和一些后端的服务。必须小心的是在网站更新的时候,不要移动应用程序与现有的接口间发生冲突。请记住,网站的内容发生了变化,则必须更新移动应用程序本身。出于这个原因,可能需要对网站的内容进行必要的版本标记以区分其内容。
•
苹果公司对仅仅包裹在原生应用中的Web view这样类型的应用持悲观看法。如果你只是在一个原生移动应用中,仅仅包含一些Web内容,则可能在提交iOS的AppStore审批时不会得到批准。苹果公司希望你的应用程序将使用的一些设备自身的功能,或者好歹使用下Mobile Safari。。想想看,你的应用程序其实可以集成相机功能、联系人列表、本地数据存储,离线操作等,还要记得,这些Web view中提供的内容必须和原来应用提交的样式等保持一致。
如果你创建了一个词搜索游戏,然后突然开始在webview浏览器中有一些风马牛不相及的内容,这将违反苹果的条款,并可能在AppStore中下架。根据iOS开发者协议,每次的更新不能改变在提交appStore审核时阐述的该应用的主要功能。
体验应用更新过程
正如上文提到的,从web服务器加载内容再刷新本地的内容。在这种方法中,只要web内容更新,服务器上的Web内容(HTML,CSS和JavaScript)会下载到移动应用程序客户端。更新代码就可以改变用户的界面,应用流程等,这无需开发人通过使用iOS App Store或Google Play去处理。
注意的是只能更新HTML、CSS和Javascript,是不能更新原生代码的。苹果公司允许只要在iOs应用中在UIWebView控件的上下文中运行就可以下载Javascript。
上图的应用是运行在Android中。HTML的内容是下载到用户的移动设备中并存储的。注意应用提供了导航条的用户体验。下图则是运行在iOs的情景,使用的是UINavigationController :
除了加快更新过程,这种传输应用程序的方式能确保用户获得最新的代码。在传统的方法中,应用程序的更新是通过应用程序商店分发,并通知用户有可用的更新,尽管他们并不需要安装最新的更新。不是每个人都定期去更新应用程序。而在用户浏览应用的时候,通过更新本地的HTML,CSS和JavaScript正好解决这个问题。
动态更新本地内容的功能,实际上是商业混合型移动应用开发平台“Trigger.IO”的一部分。 Trigger.IO功能允许更新你移动应用的HTML部分。需要注意的是原生代码不能以这种方式更新。但是,如果你应用程序的流程控制使用的JavaScript和增强的本机代码,那么应用程序流程作发生变化的时候同步更新是可能的。
动态更新内容对开发和测试的好处
能动态更新一个移动应用,也可以加速开发进程。 iOS和Android的原生开发过程一般需要在桌面计算机上对应用程序代码进行编译,然后转移到测试设备中去。一些发开发者项目,如Icenium、LiveSync或Ion则支持编译应用的更新到云端,然后只需下载已经在设备上运行的应用程序,这就完成了更新。
当代码进行了更改,并保存到服务器上,只需要一个简单的三个手指头的手势动作就可以更新iOS设备上的应用程序。 Icenium也有类似的功能并可用于Android。 Adobe公司的PhoneGap通过一个被称为Hydration的特性让更新变得可能。在这种情况下,开发人员可以通过他们的IDE整合到各种设备上,而无需物理连接到这些移动设备,就可以推送新的代码。
当Hydration功能启用的时候,每次启动应用程序时,应用程序将检查PhoneGap构建服务器去看是否有新版本可用。如果有,用户将被提示升级到新的版本。这可以使你的应用程序的质量得到保证,以及更容易使用。你将不必担心签署代码更新、配置和测试等问题。
注意事项
虽然动态更新应用的功能很强,但记住并确保凡是来源于网络的内容必须不能随意调用设备的原生代码。如果不这样做,可能会导致你的用户数据暴露给第三方,因此违反与苹果或Google的协议。此外,动态更新内容不应该改变应用原来的功能。否则会可能被苹果或者Google下架应用。
沟通两个世界的桥梁
iOS和Android都提供了能在原生应用中使用的Web view控件,接下来的例子中将讲解如何在Web view中使用Javascript调用原生代码以及原生代码如何调用在Web view中的Javascript。
首先你必须使用PhoneGap(Apache Cordova),可以使用PhoneGap 1.4 for iOS 和PhoneGap 1.9 for Android。本文并不从PhoneGap的基础开始讲解,因此读者可能需要到PhoneGap官网了解一些相关知识。对于iOs可以将CordovaWebView 放到原生的viewController控件中,而Android中也可以使用。这允许你的web代码能访问整个PhoneGap API并且访问任何PhoneGap插件。当然可以根据PhoneGap plugin API 去将应用的原生代码暴露给CordovaWebView。
假设你不需要使用PhoneGap的全部功能而只是需要在原生和HTML5两个世界中使用它们部分的功能,则也可以自己去编写代码是实现这个桥梁。本文之前提到的三个应用都是使用“私有API”的方式去实现编码的。
研究iOS中的UIWebView
在iOS中的UIWebView和相关的UIWebViewDelegate提供了相关的机制,并允许原生代码和Javascript之间互相调用,下面准备了一个简单的例子进行讲解:
注意屏幕中的上半部分,浅灰色部分使用的是UIWebView进行渲染,而下半部分白色的使用的是原生代码声称的。这个应用只是从Web view中传递JSON对象到原生代码中,反之亦然,代码和Android版本的可以在 这个地址下载:
https://github.com/ptraeg/html5-in-mobile-apps
从Javascript中调用原生代码
从Javascript中去调用原生代码很简单,只是在方法中带有command和以JSON格式要传递的参数,代码如下:
- var nativeBridge = {
- invoke: function (commandName, args) {
- console.log(commandName + ": " + JSON.stringify(args, null, 2));
- window.location = 'js-call:' + commandName + ':' +
- encodeURIComponent(JSON.stringify(args));
- }
- };
上面的代码尝试去将浏览器导航到一个新的URL,但并不是用传统的http://或file://的URL。这里使用的是自定义协议js-call:,而在Objective-C方面,代码如下:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType (UIWebViewNavigationType)navigationType
在这个调用的委托中,可以查询传入的URL是否是来自Javascript的调用:
- NSString *requestURLString = [[request URL] absoluteString];
- f ([requestURLString hasPrefix:@"js-call:"]) {
接下来是判断command的名字和JSON参数字符串:
- NSArray *components = [requestURLString componentsSeparatedByString:@":"];
- NSString *commandName = (NSString*)[components objectAtIndex:1];
- NSString *argsAsString = [ (NSString*)[components objectAtIndex:2] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding ];
既然我们将参数作为字符串,则可以将其转换为NSDictionary对象:
- NSError *error = nil;
- NSData *argsData = [argsAsString dataUsingEncoding:NSUTF8StringEncoding];
- NSDictionary *args = (NSDictionary*)[NSJSONSerialization JSONObjectWithData:argsData options:kNilOptions error:&error];
最后,是时候调用正确的原生方法以处理Javascript调用的参数并且刷新原生UI:
- if ([commandName isEqualToString:@"updateNames"]) {
- [self updateNativeNameValuesWithFirstName:[args objectForKey:@"fname"]
- LastName:[args objectForKey:@"lname"]];
- }
记得在通过js-call调用方法的时候,必须从委托中返回boolean值为no,以防止Web view尝试访问URL。
iOS原生代码中访问Javascript
这种方法比较简单,因为我们没有使用委托方法。我们可以简单通过传入要调用的JavaScript函数名和需要的参数作为字符串,传给UIWebView的stringByEvaluatingJavaScriptFromString方法就可以了
在这里,我们从本地视图中获得值并且通过NSDictionary将其变成一个JSON对象。
- NSDictionary *namesDict = @{@"fname": self.fNameTextField.text,
- @"lname": self.lNameTextField.text};
- NSError *error;
- NSData *namesData = [NSJSONSerialization dataWithJSONObject:namesDict
- options:0
- error:&error];
- NSString *namesJSON = [ [NSString alloc] initWithData:namesData
- encoding:NSUTF8StringEncoding] ;
既然已经将JSON作为字符串传递,只需要格式化命令并传递到Web view中就可以了。
- NSString *jsCommand = [NSString stringWithFormat:@"setNames(%@)", namesJSON];
- [self.webView stringByEvaluatingJavaScriptFromString:jsCommand];
在Android中使用Webview
同样,下图是使用Android的Webview实现上面iOS例子的效果:
同样,上部分灰色的是由Web View生成的,下半部分的是原生代码生成。
从Javascript代码中调用原生代码
Android Web VIEW有个好处就是能将全部的Java类暴露给Javascript,这就不用象iOS那么麻烦了。首先只需要对Android Web view进行注册设置就可以了,代码如下:
- @Override
- public void onStart() {
- super.onStart();
- WebSettings webSettings = webview.getSettings();
- webSettings.setJavaScriptEnabled(true);
- webview.loadUrl("file:///android_asset/webviewContent.html");
- webview.addJavaScriptInterface(new WebViewInterface(this), "Android");
- }
注意上面最后一行代码,注册了一个特殊的类名为WebViewInterface,它可以通过Javacript去访问,此外,该类还通过字符串“Android”指出其命名空间,还可以测试是否注册了Android命名空间,代码为:
- if (window.Android) {
- Android.updateNames(JSON.stringify(nameData));
- }
而在Java服务端,WebViewInterface是一个标准的Java类,如果使用的是Android 4.2 SDK,则必须使用@JavaScriptInterface去注解要暴露给Javascript的方法。
- public class WebViewInterface {
- Context mContext;
- /** Instantiate the interface and set the context */
- WebViewInterface(Context c) {
- mContext = c;
- }
- @JavaScriptInterface
- public void updateNames(String namesJsonString) {
- Log.d(getPackageName(), "Sent from webview: " + namesJsonString);
- try {
- JSONObject namesJson = new JSONObject(namesJsonString);
- final String firstName = namesJson.getString("fname");
- final String lastName = namesJson.getString("lname");
- // When invoked from JavaScript, this is executed on a thread
- // other than the UI thread.
- // Because we want to update the native UI controls, we must
- // create a runnable for the main UI thread.
- runOnUiThread(new Runnable() {
- public void run() {
- fnameEditText.setText(firstName);
- lnameEditText.setText(lastName);
- }
- });
- } catch (JSONException e) {
- Log.e(getPackageName(),
- "Failed to create JSON object from Web view data");
- }
- }
- }
注意当Javascript调用这个类中的方法的时候,它们并不是在主UI 线程中执行的,如果我们的目的是更新UI部分,则必须创建新的runnable线程并且传递给主UI线程执行:
- runOnUiThread(new Runnable() {
- public void run() {
- fnameEditText.setText(firstName);
- lnameEditText.setText(lastName);
- }
- });
而在原生代码中调用Javascript则比较简单。将数据用JSON的形式存放到字符串中然后就可以给Javascript调用,注意要使用的是Javascript:协议:
- public void sendNamesToWebView() {
- JSONObject namesJson = new JSONObject();
- try {
- namesJson.put("fname", fnameEditText.getText().toString());
- namesJson.put("lname", lnameEditText.getText().toString());
- webview.loadUrl( "JavaScript:setNames(" + namesJson.toString() + ")" );
- } catch (JSONException e) {
- Log.e(getPackageName(),
- "Failed to create JSON object for Web view");
- }
- }
WebView的缺点分析
和软件开发的大多数情况一样,我们必须审视WebView的一些缺点,特别是在制作混合移动应用时要注意:
• 不是所有的Web view都渲染成同样的效果 在过去的两年中,Web view的功能有明显提高。然而,你的用户群可能不会运行Android或iOS的最新和最优秀的版本。 Android平台在Web view方面已普遍落后于iOS。Google似乎在重新努力来改善这种情况,但那些停留在旧版本的Android(尤其是Ice Cream Sandwich 4.x版本前)的用户可能会发现Web view的性能尤其是复杂的表格,其效果和在iOS上呈现的并不一样。开发人员需要在Android 2.2,2.3和4.x上测试HTML5代码以确保内容呈现的一致。
• 性能问题
Web view的性能通常是比不上原生代码。然而,在许多情况下,你可能会发现性能是完全可以接受的,特别是在当前的硬件飞速发展的情况下。请记住,原生代码和代码Web view部分调用的衔接性能会有一定的影响。但一般来说,不会要求在毫秒级完成调用,对于大多数应用,这是不是一个问题。
• 滚动条问题
用户可能无法区分Web view的滚动条滚动方式和应用程序原生代码滚动方式的不同。这其中的一些问题可以通过在新版本的WebKit中使用CSS3的-webkit-overflow-scrolling:touch属性去解决。你可能也想看看解决方案,如FTScroller 和 iScroll。
• 复杂性
正如前面提到的,如果将内容从Web服务器中下载到移动设备,必须确保拥有必要的基础设施来支持。因为现在应用程序既有原生代码,也有Web服务器和潜在的后端服务参与,这会存在更多的故障点。你必须针对早期部署版本的应用程序做好兼容性测试,以确保当有应用更新的时候,现有版本的应用程序仍然运行。
• 遵守开发者协议
开发者的你必须熟悉每一类移动应用商店对web方面内容的限制。正如本文所提到的,特别是在iOS平台上要额外注意,否则会不允许发布甚至下架。
• 安全性
如前所述对于所有来源于web的内容都应该持有怀疑的态度,尤其是如果将和你的本机原生代码交互的。确保你只暴露你需要使用的原生代码部分。如果不这样做,可能会导致一些Web的恶意代码能调用本机代码。必须确保将Web的安全实践考虑进来以确保Web代码的安全调用。
鉴于一个混合型移动应用所带来的灵活性,如果你发现Web view的性能在交互方面不够优秀,则可以在应用中编写原生代码。当然,编写更多的本机代码,则在平台迁移的时候需要更多的重新改写,这是一种利弊权衡。总的来说,混合型移动应用的解决方案能让你最大限度掌控应用的性能和灵活性。
小结
移动应用开发中使用HTML5目前是个有点争议性的话题。然而,重要的是要作为开发者,必须了解每一种解决方案的优缺点,了解什么样的策略是适合你的应用程序,这些都是由需求决定的。尽管移动HTML5的解决方案在最近几个月已经有一些负面的宣传,但本文还是讲解了使用HTML5的好处。
无论采用什么方法开发移动应用,必须保持性能的高效和安全性,并且尽早测试,采用多种设备测试。在这篇文章中,我们已经看到,HTML5带来了许多好处,特别是采用混合HTML 5和原生代码开发,则给开发者更高的自由度选择。