跨终端 Web 之 Hybrid App

阅读数:6948 2015 年 5 月 26 日

话题:移动语言 & 开发架构

编者按:InfoQ 开设新栏目“品味书香”,精选技术书籍的精彩章节,以及分享看完书留下的思考和收获,欢迎大家关注。本文节选自徐凯著《跨终端 Web》第八章“Hybrid App”,主要讲述 Hybrid App 的发展现状以及技术实现,最后还介绍了两种主流 Hybrid 开发框架 PhoneGap/Cordova 和 Titanium。


Native App(以下简称 Native)和 Mobile Web(以下简称 Web)二者混合开发的产物被称为 Hybrid App(以下简称 Hybrid)。Hybrid 并不是什么新概念,最早可以追溯到 Symbian 时代,直到 iOS 和 Android 出现之后才充分展现出价值。

Hybrid 简史

1. 背景

Hybrid 既利用了 Native App 丰富的设备 API(Device API),又能拥有 Mobile Web 的跨平台、高效开发、快速发布的能力,对于相当庞大的应用场景而言都是适用的。

Hybrid 优势在于:

  • 跨平台

    Web 内容可以做到开发一次,所有平台生效,诸多产品需要这种能力。

  • 快速发布

    iOS 平台,Apple Store 平均审核周期 1~2 周不等,甚至更长,产品的发布周期从 2 周到 1 月,这对需要快速发布的产品而言难以接受。

    Android 平台,应用商店众多,发布过程烦琐。虽然可以应用内升级,但是带来的问题是新 App 需要通过应用商店,此外 APK 体积庞大,2G/3G 环境下体验差。

  • 高效开发

    Web 开发经过 20 年的发展,已经将结构(HTML)、表现(CSS)、行为(JavaScript)3 部分很好地分离开,在分工协作、开发效率上会具明显优势。

  • 丰富的 Device API

    Web(HTML5)强调通用性,受限于标准和浏览器实现,许多有用的系统功能未能得到支持(或部分支持)。而 Native 最大的优势在于设备 API 的调用能力,只要桥接 Native 和 Web,Web 也就能够拥有这种能力。

Hybrid 劣势表现为:

  1. CPU/GPU 密集类应用目前看更适合 Native,例如极品飞车这样的游戏。这种劣势是在不断弱化的,正如 “CSS Transform 3D”引入 GPU 大大缓解了 Web 动画不流畅的问题。
  2. 静态资源从服务器端加载导致的 UI 展示延迟问题。这个问题可以通过 Native 拦截 WebView 通信加载已打包的公共库来缓解。

2. 简史

  • 雏形

    雏形阶段大致为:

    • Symbian V3/5 时代已经有 Hybrid 雏形。
    • iOS 最初的 App 都是由 Objective-C 编写而成的,受限应用商店的发布周期,内容经常变化的部分开始通过使用内置浏览器控件(WebView)加载服务端页面来实现。
    • Android 出现并流行之后,可以将更多的 App 功能通过 Hybrid 来实现,这样在不同平台上就可以只维护一个版本。
  • 发展

    “跨平台”成了 Hybrid 最大的卖点,以 PhoneGap[1] 为首的 Hybrid 框架陆续出现,带来了诸多改变。

    • 访问设备功能。
      • Web(HTML5)不支持的功能可以让 Native 实现,再通过 Native 和 Web 之间通信,通过这种方式可以让 Web 获得和 Native 相同的设备 API 调用能力,这是 PhoneGap 这类 Hybrid 框架的基本工作原理。
      • 与此同时,将 Web 代码转为 Native 的 Hybrid 框架(如 Tianium[2])也出现了。
    • PhoneGap 子项目 weinre 是一种远程调试工具,极大地缓解了 Hybrid 难于调试的问题,进一步促进了 Hybrid 的发展。
    • Hybrid 框架提供了应用打包功能,开发者可以完全使用 HTML、CSS、JavaScript 开发 Native App。
  • 成熟

    随着 PhoneGap 这类 Hybrid 框架在全球的流行,一些问题暴露了出来,也正是这些问题的解决,让 Hybrid 走向成熟。

    • 开发体验提升。
      • weinre 这类调试工具仍属于插件性质,诸如“网络”、“本地资源”等高级调试功能无法支持,WebView 的原生调试需求越来越强烈。
      • iOS 6.0+ 已经支持原生的远程调试 [3]。
      • Chrome for Android 在原生远程调试上处于领先地位 [4]。
      • 从 Android 4.4 开始,WebView 也支持原生的远程调试 [5]。
    • 提升 WebView 性能的呼声日益增强。
    • 某些追求极致性能的功能转由 Native 实现,如转场(页面间切换)动画。
    • 静态资源本地化是理想状态,其他场景下 Native 拦截 WebView 的请求,并让公共资源重定向到 App 内置资源,同样能实现为 Web 提速。

3. 现状

以上便是 Hybrid 的发展概述,从国内最新的资料可以看出,Hybrid 的趋势也是非常明显的。从图 8-1 可以看到越来越多的开发者决定使用 Hybrid(跨平台技术),最近两年的总量已经有 54%;而接近 60% 的开发者在 Hybrid 的技术方案上选择了 PhoneGap。

图 8-1 Hybrid 在国内的发展情况 [6]

  1. 在受访的 2309 个 Mobile 开发者中,到 2013 年 8 月为止完全使用 Native 开发的只有 8%,而剩余的 92% 都可以被认为使用的是 Hybrid,如图 8-2 所示。

    图 8-2 Hybrid 使用情况

  2. App 的跨平台特性成为一个重要的考虑,如图 8-3 所示。

    图 8-3 跨平台特性受关注

图 8-4 显示了 Hybrid 惊人的增长速度:2013 年无论是开发中、已发布的 Hybrid(或 HTML App)均相比于 2012 年出现了超过 125%~400% 的增长率 [8]。

图 8-4 Hybrid 增长迅猛 [9]

Hybrid 技术

无论 Android 还是 iOS,实现一个最简单的 Hybrid App 只需要几行代码:实例化 WebView、加载页面,之后便是页面自身的代码。要想实现更为复杂的、完整的 Hybrid 还需要不少知识。

  1. Mobile Web 开发基础:HTML、CSS、JavaScript。
  2. Native App 开发基础:Android、iOS。
  3. Native 与 Web 双向通信机制。

Mobile Web 开发基础可以参考本书第 2 章,Native App 开发基础已经超出本书的讨论范围,同样有很多可选择的书籍,本节来讲剩余的第 3 个问题 “Native 与 Web 双向通信机制”。

1. Native 调用 Web

无论 Android 还是 iOS ,Native 调用 Web(JavaScript) 都有很好的原生支持,如代码 8-1 和代码 8-2 所示。Android 中的调用方式如下,其中 webView 是 Webview 的实例。

代码 8-1 Android 调用 JavaScript

webView.loadUrl("javascript:(function(){ alert(‘ok’); })()”);

iOS 中的调用方式如下,其中 webView 是 UIWebview 的实例。

代码 8-2 iOS 调用 JavaScript

[webView stringByEvaluatingJavaScriptFromString: @"alert('ok')" ];

2. Web 调用 Native

“Native 调用 Web”本质上是 JavaScript 脚本的动态执行,在“Web 调用 Native”的场景下由于目前 Native 语言(Java 和 Objective-C)不容易像 JavaScript 那样便于动态执行,所以需要另辟蹊径。

2.1 Android

Android 上常见的方式有 3 种。

  1. 重写 WebViewClient.shouldOverrideUrlLoading(如代码 8-3 所示)。

    代码 8-3 重写 WebViewClient.shouldOverrideUrlLoading

    webView.setWebViewClient(new WebViewClient(){
    		@Override
    		public boolean shouldOverrideUrlLoading (WebView view, String url){
    			// TODO 解析 URL 并触发 Native 代码			
    return true;
    		}
    	});

    当页面内的 URL 发生变化时,如点击链接、执行 JavaScript(如location.href=http://等均会触发 WebViewClient.shouldOverrideUrlLoading,通过将 Web 调用 Native 的数据封装在 URL,再由 Native 解析数据并执行响应 Native 方法。

  2. 重写 WebChromeClient.onJsPrompt,或 onJsConfirm,或 onJsAlert,以 WebChromeClient.onJsPrompt 为例,如代码 8-4 所示。

    代码 8-4 重写 WebChromeClient.onJsPrompt

    	webView.setWebChromeClient(new WebChromeClient() {
    		public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
    		// TODO 解析 message 并触发 Native 代码			
    			result.confirm("");
    			return true;
    		}
    	});

    当执行“window.prompt(“{}”)”这样的 JavaScript 代码时,将会触发 WebChromeClient.onJsPrompt。

  3. WebView.addJavascriptInterface,这种方式和前两种都不同,通过将 Java Object(A) 映射为 JavaScript Object(B),从而调用 B.func1 时将会自动触发 A.func1,通过这种原生的方式实现了 “Web 调用 Native”,如代码 8-5 所示。

    代码 8-5 WebView.addJavascriptInterface

    	webView.addJavascriptInterface(new Object() {
    		public void func1() {
    		}
    
    		public void func2() {
    		}
    	}, "webViewObj");

以上 3 种方式,最常用的是方式 2;方式 2 相比方式 1 有内置的队列支持,不会出现高频访问数据丢失的情况;方式 3 是 Android 原生方式,但是不如前两种方式灵活。

2.2 iOS

iOS 中可用的方式类似 Android 中的 WebViewClient.shouldOverrideUrlLoading, 通过监控 WebView 的 URL 变化实现 Web 调用 Native,如代码 8-6 所示。

代码 8-6 shouldStartLoadWithRequest

- (BOOL)webView:(UIWebView *) webView shouldStartLoadWithRequest:
(NSURLRequest *)request 
navigationType: (UIWebViewNavigationType)navigationType {

}

3. Bridge

有了前两节的知识,可以实现一个通用模块(Bridge)来维护不同平台上的“Web 与 Native 双向通信机制”功能。如图 8-5 所示为 Web 调用 Native 的 Bridge 时序图。

图 8-5 Web 调用 Native 的 Bridge 时序图

Web 调用 Native 的实现原理如下。

  1. Web 端调用 Bridge.callByJS({name:’func1’, callback: function(){}, param:{}}),由 Bridge 根据特定“Web 调用 Native”方式通知 Native 执行相应方法(图 8-5 中的“func1”)。
  2. Native 执行完毕后通过“Native 调用 Web”的方式调用 Bridge. callByNative({token: ‘t1234’ })。如图 8-6 所示为 Native 调用 Web 时的 Bridge 时序图。
  3. 其中 JavaScript 回调函数会映射为字符串型的 token,通过这个方式来保证最终触发 JavaScript 的回调函数(包括匿名函数和通过闭包实现的私有函数)。

图 8-6 Native 调用 Web 时的 Bridge 时序图

可以看到,Bridge 实现“Native 调用 Web”是类似的。

  1. Native 端调用 Bridge.callByNative({token:’t1234’, script: ‘//todo’}),由 Bridge 根据特定“Native 调用 Web”方式通知 Web 执行相应脚本。
  2. Web 执行完毕后通过“Web 调用 Native”的方式调用 Bridge. callByJS({token: ‘t1234’ })。
  3. 如果 Bridge.callByNative 的 script 中执行了异步操作,需要在 script 主动调用 Bridge.callByJS,并且不需要传 token 参数。

笔者已经在 Android 上实现了完整的 Bridge[10],Bridge 由 JavaScript 实现可以运行在 Android 和 iOS 的 WebView 中,同时也非常容易扩展到 Windows Phone 等新平台,如代码 8-7 所示。

  1. Bridge 代码在产品环境下使用时请设置 DEBUG = false。
  2. 避免在 iOS 下快速变化 URL 时造成的数据丢失,可以考虑使用队列机制缓存命令。
  3. 扩展至 Windows Phone 等平台时 JavaScript 部分只需要扩展 invoke,Native 代码可以参考 Android 的实现。
  4. 目前 Bridge 单次通信后会删除回调函数,如果需要多次调用缓存的回调函数(如连续监控传感器数据),可以扩展 Bridge.callByNative。

代码 8-7 bridge.js

(function(window) {
    var DEBUG = true;
    var callbacks = {};
    var guid = 0;
    var ua = navigator.userAgent;
    // TODO 精确性待改进 
    var ANDROID = /android/i.test(ua);
    var IOS = /iphone|ipad/i.test(ua);
    var WP = /windows phone/i.test(ua);
    //ANDROID = 0; IOS = 1;

    /**
     * 方便在各个平台中看到完整的 log
     */
    function log() {
        if (DEBUG) {
            console.log.call(console, Array.prototype.join.call(arguments, ' '));
        }
    }

    /**
     * 平台相关的 Web 与 Native 单向通信方法 
     */
    function invoke(cmd) {
        log('invoke', cmd);
        if (ANDROID) {
            prompt(cmd);
        }
        else if (IOS) {
            location.href = 'bridge://' + cmd;
        }
        else if (WP) {
            // TODO ...
        }
    }

    var Bridge = {
        callByJS: function(opt) {
            log('callByJS', JSON.stringify(opt));
            var input = {};
            input.name = opt.name;
            input.token = ++guid;
            input.param = opt.param || {};
            callbacks[input.token] = opt.callback;

            invoke(JSON.stringify(input));
        },
        callByNative: function(opt) {
            log('callByNative', JSON.stringify(opt));
            var callback = callbacks[opt.token];
            var ret = opt.ret || {};
            var script = opt.script || '';

            // Native 主动调用 Web
            if (script) {
                log('callByNative script', script);
                try {
                    invoke(JSON.stringify({
                        token: opt.token,
                        ret: eval(script)
                    }));
                } catch (e) {
                    console.error(e);
                }
            }
            // Web 主动调用 Native,Native 被动响应 
            else if (callback) {
                callback(ret);
                try {
                    delete callback;
                    log(callbacks);
                } catch (e) {
                    console.error(e);
                }
            }

        }
    };

    window.Bridge = Bridge;
    window.__log = log;
})(window);

Hybrid 框架

目前一个 Hybrid 框架通常提供以下功能。

  1. Device API:封装 Native 的功能,跨平台提供一致的 Device API。
  2. App 打包:将 HTML5 编写的代码打包为 App(Titanium 会转换代码)。

PhoneGap 几乎成了 Hybrid 的代名词,Titanium 和 PhoneGap 的设计理念差异较大,图 8-7 形象地展示了 PhoneGap 和 Titanium 的组成部分。

图 8-7 Hybrid 框架 [11]

1. PhoneGap

1.1 PhoneGap 和 Cordova

PhoneGap 开发商 Notibi 2010 年将 PhoneGap 代码贡献给 Apache 软件基金(ASF),PhoneGap 核心引擎成为新的开源项目 Cordova,同时 PhoneGap 成了 Cordova 的一个发行版本 [12]。2011 年 10 月,Notibi 被 Adobe 收购 [13],但没有影响到 PhoneGap 和 Cordova 的开源性质。

1.2 原理

written once,run everywhere

如引文所述“一处编写,多处运行”,PhoneGap 主要的功能为:

  1. 提供 Hybrid API,可由 JavaScript 直接调用诸如加速度、摄像头、指南针、GPS、联系人等系统级 API,完整的 API 列表请访问 PhoneGap API Reference。
  2. 使用 Web(HTML、CSS、JavaScript)开发的内容经过 PhoneGap 编译打包为各个平台的 Native App,如图 8-8 所示。

图 8-8 PhoneGap 编译打包功能

1.3 经典案例

来自 PhoneGap Showcase[14] 和其他数据源的资料显示:

  • Facebook Mobile SDK[15] 和 SalesForce Mobile SDK[16] 均是基于 Cordova 的分支开发的。
  • Facebook 客户端中 Web 代码超过 90%[17]。
  • LinkedIn iPad 客户端中 Web 代码甚至超过 95%。
  • Wikipedia 更是直接用 PhoneGap 开发了自己的 iOS/Android Hybrid App[18],并将代码在 GitHub 上开源 [19]。

2. Titanium

Titanium 设计思路和 PhoneGap 有很大不同,Titanium 目的为移动开发提供一种跨平台的 JavaScript 运行时环境和 API。

2.1 设计思路

Titanium 设计的核心思路如下。

  1. 有一套核心的移动开发 API,它们可以跨平台进行规范,这些方面的重点应放在代码重用上。
  2. 有针对特定平台的 API、用户界面约定以及功能特性,开发者在针对该特定平台从事开发时采用,应该有针对特定平台的代码,以便这些用例提供最佳的用户体验。

Titanium 从设计理念上不追求“written once, run everywhere”,这是它的缺点,但同时它追求平台差异的更佳的用户体验,因而也受到一部分用户的追捧。Titanium 的另一个缺陷是插件难于扩展,要想支持新平台则更加困难。

2.2 工作流程

工作流程如下。Titanium 工作流如图 8-9 所示。

  1. 使用 Titanium SDK 在自带的 IDE(ALLOY)中开发。
  2. 使用工具编译为平台相关的 App。

图 8-9 Titanium 工作流

书籍简介

移动互联网不可阻挡地进入了我们的生活。作者将自己在百度和天猫期间的跨终端 Web 的开发实践转化为书中的技术方案和实现,呈现给各位读者。第 1 章提出了跨终端 Web 的概念以及实现跨终端 Web 的多重途径,第 2 章主要介绍 Mobile Web 的技术基础,第 3~7 章是全书的核心,按照开发流程组织逐步讲解了实现跨终端 Web 所需要的各类技术基础设施,第 8 章主要介绍了 Hybrid App 的发展历程、实现细节以及成熟的框架,第 9 章介绍的跨终端存储方案(Storage)是作者曾经的冠军作品,第 10 章完整介绍了如何通过脚本录制和回放来实现跨终端动作同步。

《跨终端 Web》讲解深入浅出,通畅易懂,适合有一定 PC Web 基础,希望迅速了解 Mobile Web,致力于 PC 和 Mobile Web 技术融合的读者。

作者简介

鬼道(原名徐凯),2011 年毕业于同济大学计算机系,模式识别方向硕士研究生。曾就职百度,现为天猫前端通用组技术 Leader。本书源于 2013 年 7 月在 D2 上的主题分享“移动优先的跨终端 Web”,2013 年 11 月在 W3CTECH 2013 做了第二次分享。


  • [1] PhoneGap 是主流 Hybrid 框架。
  • [2] http://www.appcelerator.com/titanium/
  • [3] http://www.36kr.com/p/117773.html
  • [4] http://www.html5rocks.com/en/tutorials/developertools/mobile/
  • [5] https://developers.google.com/chrome-developer-tools/docs/remote-debugging#debugging-webviews
  • [6] 摘自《友盟 2013 上半年报告》2013.09。
  • [7] http://www.kendoui.com/surveys/html5-native-debate-is-over.aspx
  • [8] http://www.businessinsider.com/html5-vs-native-apps-for-mobile-2013-6?op=1
  • [9] 此图来自 http://www.businessinsider.com/html5-vs-native-apps-for-mobile-2013-6?op=1。
  • [10] http://luics.github.io/cew/Bridge.zip
  • [11] 此图来自 http://www.businessinsider.com/html5-vs-native-apps-for-mobile-2013-6?op=1。
  • [12] http://phonegap.com/2012/03/19/phonegap-cordova-and-what%E2%80%99s-in-a-name/
  • [13] http://www.adobe.com/aboutadobe/pressroom/pressreleases/201110/AdobeAcquiresNitobi.html
  • [14] http://phonegap.com/app/
  • [15] https://developers.facebook.com/docs/guides/mobile/
  • [16] http://wiki.developerforce.com/page/Mobile_SDK
  • [17] http://www.geekpark.net/read/view/164456
  • [18] http://itunes.apple.com/us/app/wikipedia-mobile/id324715238?mt=8
  • [19] https://github.com/wikimedia/WikipediaMobile