Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2192487
  • 博文数量: 866
  • 博客积分: 14125
  • 博客等级: 上将
  • 技术积分: 10638
  • 用 户 组: 普通用户
  • 注册时间: 2007-07-27 16:53
个人简介

https://github.com/landuochong

文章分类

全部博文(866)

文章存档

2019年(3)

2018年(1)

2017年(10)

2015年(3)

2014年(8)

2013年(3)

2012年(70)

2011年(103)

2010年(360)

2009年(283)

2008年(22)

分类: Java

2012-07-24 18:02:18

一、Javascript的源码结构

提醒一下大家,PhoneGap的作者已经将PhoneGap的源码委托给了Apache基金会。PhoneGap的开源版本称为cordova

PhoneGap之于Cordova,正如OpenJDK之于JDK。两者基本上是差不多的。

cordova.android.js是一个build版本。本人对其做了反build工作,cordova.android.js展开的源码目录结构便如上所示。

从命名上其实已经可以看出,lib/android属于android平台的专用库。其余平台基本上是lib/windowsphone或者lib/ios。而除lib下的exec.jsplatform.js这两个是固定的。但是不同平台版本其实现是不一样的。除lib包外,其余所有的js文件都是跨平台通用的。(即使有差异应该也不大)


简单介绍一下各个目录和一些关键组件:

 

Java代码  收藏代码
  1. cordova.js:拦截DOM,Window事件,加入自定义cordova事件,管理回调Javascript。  
  2.   
  3. scripts/require.js:PhoneGap中模块化机制的基础框架。简单但是也不简单!  
  4.   
  5. scripts/bootstrap.js:负责cordova的启动。  
  6.   
  7. common/channel.js:PhoneGap中实现事件监听的基础。  
  8.   
  9. common/builder.js:具备定制化构造模块的能力。  
  10.   
  11. common/plugin:如名字所示。这里放置所有平台通用的插件接口。  
  12.   
  13. lib/exec.js:于上一篇解析中提到。是Javascript调用Native的入口。  
  14.   
  15. lib/platform.js:与平台实现有关的初始化。  
  16.   
  17. lib/android/plugin:与android平台紧密相关的插件。  

 

 

二、浅析PhoneGap中的模块化机制

也许是因为本人见过的Javascript代码太少,见到PhoneGap的模块化机制后便觉得非常的有趣和前卫。Pascal的作者沃斯曾写过一门叫做module的语言,其在语言级别做了模块化机制。我不知道PhoneGap模块化的思路是否也受;此影响。

废话不多说了,从require.js开始看起吧。它是模块化的基础。

 

Js代码  收藏代码
  1. var require,  
  2.   
  3.     define;  
  4.   
  5.    
  6.   
  7. (function () {  
  8.   
  9.     var modules = {};  
  10.   
  11.    
  12.   
  13.     function build(module) {  
  14.   
  15.         var factory = module.factory;  
  16.   
  17.         module.exports = {};  
  18.   
  19.         delete module.factory;  
  20.   
  21.         factory(require, module.exports, module);  
  22.   
  23.         return module.exports;  
  24.   
  25.     }  
  26.   
  27.    
  28.   
  29.     require = function (id) {  
  30.   
  31.         if (!modules[id]) {  
  32.   
  33.             throw "module " + id + " not found";  
  34.   
  35.         }  
  36.   
  37.         return modules[id].factory ? build(modules[id]) : modules[id].exports;  
  38.   
  39.     };  
  40.   
  41.    
  42.   
  43.     define = function (id, factory) {  
  44.   
  45.         if (modules[id]) {  
  46.   
  47.             throw "module " + id + " already defined";  
  48.   
  49.         }  
  50.   
  51.    
  52.   
  53.         modules[id] = {  
  54.   
  55.             id: id,  
  56.   
  57.             factory: factory  
  58.   
  59.         };  
  60.   
  61.     };  
  62.   
  63.    
  64.   
  65.     define.remove = function (id) {  
  66.   
  67.         delete modules[id];  
  68.   
  69.     };  
  70.   
  71.    
  72.   
  73. })();  
  74.   
  75.    
  76.   
  77. //Export for use in node  
  78.   
  79. if (typeof module === "object" && typeof require === "function") {  
  80.   
  81.     module.exports.require = require;  
  82.   
  83.     module.exports.define = define;  
  84.   
  85. }  

 

 

代码行数的确非常短。其定义了requiredefine两个函数。首先define函数用于声明一个模块。其中id表示模块名称,这必须是唯一的,而factory便是构造模块的工厂方法。require函数使用懒加载的方式获得已define过的对应id的模块。

来看一个使用其的简单示例吧:

 

Java代码  收藏代码
  1. define("cordova/plugin/android/app", function(require, exports, module) {  
  2.   
  3. var exec = require('cordova/exec');  
  4.   
  5.    
  6.   
  7. module.exports = {  
  8.   
  9.   /** 
  10.  
  11.    * Clear the resource cache. 
  12.  
  13.    */  
  14.   
  15.   clearCache:function() {  
  16.   
  17.     exec(nullnull"App""clearCache", []);  
  18.   
  19.   },  
  20.   
  21.    
  22.   
  23.   /** 
  24.  
  25.    * Load the url into the webview or into new browser instance. 
  26.  
  27.    * 
  28.  
  29.    * @param url           The URL to load 
  30.  
  31.    * @param props         Properties that can be passed in to the activity: 
  32.  
  33.    *      wait: int                           => wait msec before loading URL 
  34.  
  35.    *      loadingDialog: "Title,Message"      => display a native loading dialog 
  36.  
  37.    *      loadUrlTimeoutValue: int            => time in msec to wait before triggering a timeout error 
  38.  
  39.    *      clearHistory: boolean              => clear webview history (default=false) 
  40.  
  41.    *      openExternal: boolean              => open in a new browser (default=false) 
  42.  
  43.    * 
  44.  
  45.    * Example: 
  46.  
  47.    *      navigator.app.loadUrl("http://server/myapp/index.html", {wait:2000, loadingDialog:"Wait,Loading App", loadUrlTimeoutValue: 60000}); 
  48.  
  49.    */  
  50.   
  51.   loadUrl:function(url, props) {  
  52.   
  53.     exec(nullnull"App""loadUrl", [url, props]);  
  54.   
  55.   },  
  56.   
  57.    
  58.   
  59.   /** 
  60.  
  61.    * Cancel loadUrl that is waiting to be loaded. 
  62.  
  63.    */  
  64.   
  65.   cancelLoadUrl:function() {  
  66.   
  67.     exec(nullnull"App""cancelLoadUrl", []);  
  68.   
  69.   },  
  70.   
  71.    
  72.   
  73.   /** 
  74.  
  75.    * Clear web history in this web view. 
  76.  
  77.    * Instead of BACK button loading the previous web page, it will exit the app. 
  78.  
  79.    */  
  80.   
  81.   clearHistory:function() {  
  82.   
  83.     exec(nullnull"App""clearHistory", []);  
  84.   
  85.   },  
  86.   
  87.    
  88.   
  89.   /** 
  90.  
  91.    * Go to previous page displayed. 
  92.  
  93.    * This is the same as pressing the backbutton on Android device. 
  94.  
  95.    */  
  96.   
  97.   backHistory:function() {  
  98.   
  99.     exec(nullnull"App""backHistory", []);  
  100.   
  101.   },  
  102.   
  103.    
  104.   
  105.   /** 
  106.  
  107.    * Override the default behavior of the Android back button. 
  108.  
  109.    * If overridden, when the back button is pressed, the "backKeyDown" JavaScript event will be fired. 
  110.  
  111.    * 
  112.  
  113.    * Note: The user should not have to call this method.  Instead, when the user 
  114.  
  115.    *       registers for the "backbutton" event, this is automatically done. 
  116.  
  117.    * 
  118.  
  119.    * @param override        T=override, F=cancel override 
  120.  
  121.    */  
  122.   
  123.   overrideBackbutton:function(override) {  
  124.   
  125.     exec(nullnull"App""overrideBackbutton", [override]);  
  126.   
  127.   },  
  128.   
  129.    
  130.   
  131.   /** 
  132.  
  133.    * Exit and terminate the application. 
  134.  
  135.    */  
  136.   
  137.   exitApp:function() {  
  138.   
  139.     return exec(nullnull"App""exitApp", []);  
  140.   
  141.   }  
  142.   
  143. };  
  144.   
  145. });  

 

 

这是phonegap模块化编程的典型写法。首先在头部定义依赖的模块组件,再次通过设置module.exports 向外部暴露出对应的方法。

         由于Javascript中在语法级别没有私有访问符。因此往往解决之道是:a.模仿C风格,命名使用_开头的一律表示是私有;b.新建一个private对象,将其私有方法放置其中,起到命名空间的作用;c.将私有部分用function{}套住,return返回公开部分。这种做法充分发挥了javascript闭包的优势,但是可读性比较差。

         phoneGapmodule机制使用了第三种做法。但是通过将构造和依赖分离开来,使得可读性大大增加。代码清晰好懂,依赖性也一目了然。

         也许有朋友要问。这种模块机制碰到循环依赖的情况怎么办?例如Adefine阶段requireBBdefine阶段又requireA。很遗憾,这种循环依赖的情况 依照phoneGap的模块化思路是无法实现的。因此对于关键组件的编制,phoneGap总会小心翼翼地处理其依赖顺序。

         通过阅读源码,发现大致的模块依赖顺序是这样子的(被依赖模块到依赖模块):utils.js->channel.js|builder.js->cordova.js->exec.js|polling.js->callback.js->platform.js->bootstrap.js

三、PhoneGap中的事件处理机制

common/channel.jsPhoneGap事件处理机制的基础。每个事件类型,都会包装成一个Channel对象。

既然是事件,那么就得支持基础的观察者模式吧?Channelprototype定义了事件的一些关键方法。subscribe用于注册一个监听器,并给监听器一个guidguid类似cordova.js中的callbackId,只是一个流水标识。但Channel中的guid稍有些不同,指定确切的guid可以对监听器做覆盖操作。

utils.close是个很有趣的方法。Javascript中的调用不当引起this不对,这是新手常见的错误。常见的做法会通过封装applydelegate。而close这个方法是绝了,它通过闭包包装了一个指向确定this,调用确定function,使用确定实参的final函数。不管在什么样的环境下调用,这个方法总能正确执行。

subscribeOnce类似于YUI或者jquery中的one。只会收到一次监听。(若事件已经触发过,则在注册阶段立即回调监听)

unsubscribefire分别用来注销监听器和触发事件。触发事件将会引起监听器的广播操作。可选的fireArgs用于保证subscribeOnce在事件已触发的情况下能获得正确的广播参数。

Channel本身还有一个监听注册/注销的事件拦截。分别是onSubscribeonUnSubscribe。在common\plugin\battery.js中,我们可以看到。battery.js便是利用这个注册监听回调,来对Plugin服务做懒加载和卸载工作。

作为模块暴露公有部分的channel对象比较有意思。join这个工具方法类似subscribeOnce,它的第二个参数是个Channel数组。当且仅当所有的Channel事件都被fire,join的监听才会被回调。这个方法还是挺有用的

create是个构造工厂方法。新构造的Channel事件会被放置在channel对象中。使用上会方便点。在channel.create('onCordovaReady');,便可以便捷的通过channel[‘onCordovaReady’]来方便的访问对应类型的Channel对象了。

deviceReadyMap,deviceReadyArray,waitForInitialization,initializeComplete这四者紧密相关。它们决定了onDeviceReady事件在何时被触发。于common/bootstrap.js中我们看到下面一段代码。

 

Java代码  收藏代码
  1. channel.join(function() {  
  2.   
  3. channel.onDeviceReady.fire();  
  4.   
  5. }, channel.deviceReadyChannelsArray);  

 

         waitForInitialization用于添加onDeviceReady的等待Channel事件。initializeComplete用于触发指定的等待Channel事件。如果想要增加onDeviceReady的条件,我们只需要在onCordovaReady之前添加waitForInitialization即可。事实上,在lib/android/plugin/storage.js中我们便可以看到一个绝佳的例子。cupcakeStorage利用本地Plugin为不支持localStorage APIWebView提供了一个备选方案。在本地建立好备用的sqlite数据库后,cupcakeStorage的等待时间便结束完毕。

四、启动与PhoneGap自定义事件

首先上图。


上图为本人整理的启动事件序列,待会儿大家便能从源码中看到了。

一、PhoneGap的启动

近期因为赶项目和犯懒,所以一直没有更新,希望朋友们见谅。

前面介绍过phoneGap的模块化机制。其通过requiredefine,巧妙的将模块的定义和依赖关系隔离开来。



如上图所示,在定义模块机制后。后面统统都是做define。那么,这些被定义的模块最终何时被实例化呢?


cordova.js的末尾我们找到了这一句。第一个被请求实例化的模块是cordova(adobe版本是phonegap),接着在实例化cordova模块后,cordova依赖的模块也纷纷被实例化出来了。(channelutils)

cordova模块仅是个最基础的顶层通信模块。除了cordova模块外,其余数十个phonegap模块都还在沉睡之中。事实上,这一步走完其实phonegap已经完成了。Phonegap用户只需要require(‘cordova/plugin/contacts’)这样,即可访问到phonegap的功能。

但是,这样做还不够好。第一,require对于phonegap使用者来说并不友好。第二,虽然可以通过require得到构建模块的实例,可这并不代表Native/Javascript的通信关系已经建立。phonegap用户还不知道何时可以安全正确的使用这些功能。

基于上面两点,phonegap构建了一个bootstrap函数。在上图的后一句,大家可以看到bootstrap的封闭调用。下面我来详解一下bootstrap

二、Bootstrap详解

首先贴上代码。

Java代码  收藏代码
  1. (function (context) {  
  2.   
  3.     var channel = require("cordova/channel"),  
  4.   
  5.         _self = {  
  6.   
  7.             boot: function () {  
  8.   
  9.                 /** 
  10.  
  11.                  * Create all cordova objects once page has fully loaded and native side is ready. 
  12.  
  13.                  */  
  14.   
  15.                 channel.join(function() {  
  16.   
  17.                     var builder = require('cordova/builder'),  
  18.   
  19.                         base = require('cordova/common'),  
  20.   
  21.                         platform = require('cordova/platform');  
  22.   
  23.    
  24.   
  25.                     // Drop the common globals into the window object, but be nice and don't overwrite anything.  
  26.   
  27.                     builder.build(base.objects).intoButDontClobber(window);  
  28.   
  29.    
  30.   
  31.                     // Drop the platform-specific globals into the window object  
  32.   
  33.                     // and clobber any existing object.  
  34.   
  35.                     builder.build(platform.objects).intoAndClobber(window);  
  36.   
  37.    
  38.   
  39.                     // Merge the platform-specific overrides/enhancements into  
  40.   
  41.                     // the window object.  
  42.   
  43.                     if (typeof platform.merges !== 'undefined') {  
  44.   
  45.                         builder.build(platform.merges).intoAndMerge(window);  
  46.   
  47.                     }  
  48.   
  49.    
  50.   
  51.                     // Call the platform-specific initialization  
  52.   
  53.                     platform.initialize();  
  54.   
  55.    
  56.   
  57.                     // Fire event to notify that all objects are created  
  58.   
  59.                     channel.onCordovaReady.fire();  
  60.   
  61.    
  62.   
  63.                     // Fire onDeviceReady event once all constructors have run and  
  64.   
  65.                     // cordova info has been received from native side.  
  66.   
  67.                     channel.join(function() {  
  68.   
  69.                         channel.onDeviceReady.fire();  
  70.   
  71.                     }, channel.deviceReadyChannelsArray);  
  72.   
  73.    
  74.   
  75.                 }, [ channel.onDOMContentLoaded, channel.onNativeReady ]);  
  76.   
  77.             }  
  78.   
  79.         };  
  80.   
  81.    
  82.   
  83.     // boot up once native side is ready  
  84.   
  85.     channel.onNativeReady.subscribeOnce(_self.boot);  
  86.   
  87.    
  88.   
  89.     // _nativeReady is global variable that the native side can set  
  90.   
  91.     // to signify that the native code is ready. It is a global since  
  92.   
  93.     // it may be called before any cordova JS is ready.  
  94.   
  95.     if (window._nativeReady) {  
  96.   
  97.         channel.onNativeReady.fire();  
  98.   
  99.     }  
  100.   
  101.    
  102.   
  103. }(window));  
  104.   
  105.    
 

我们先来详细关注下,phonegap是如何提供回调通知,来告诉用户NativeJavascript之间成功建立联系的呢?

首先,bootstrap通过channel模块注册监听了onNativeReady事件。这个事件由Native层触发,用来表示Native层准备完毕(可以接受plugin调用)。在Android平台上面,onNativeReady是在WebViewonPageFinished回调中触发的。


 为了安全的启动,boot等待onNativeReadyonDOMContentLoaded完毕后才执行。那么Phonegapboot都做了些什么呢?

boot做了两件事情。首先是实例化和发布模块来给phonegap用户使用。其次是广播onCordovaReady来通知phonegapboot完毕,用户可以放心使用phonegap的功能。

通过builder,模块的实例化和发布工作变得很艺术。builder模块提供三种发布方式,分别是intoButDontClobber(若发布目标中已存在同名模块,则不允许做覆盖),intoAndClobber(若发布目标中已存在同名模块,则强制覆盖),intoAndMerge(若发布目标中已存在实例模块,则尝试对两者进行合并,合并的优先级是欲发布模块比发布模块高)

解释一下“发布”这个词。其实就是根据定义id实例化(require)模块,然后把它作为某个object的属性。从boot的源码中,我们可以看到。在common.jsplatform.js中,分别定义了objects对象。objects定义了各个模块的id和层次关系。父子关系通过children字段表明。

Java代码  收藏代码
  1. objects: {  
  2.   
  3.     cordova: {  
  4.   
  5.         path: 'cordova',  
  6.   
  7.         children: {  
  8.   
  9.             exec: {  
  10.   
  11.                 path: 'cordova/exec'  
  12.   
  13.             }  
  14.   
  15.         }  
  16.   
  17.     },  
 

         根据上面的定义,cordova模块下将挂一个exec模块。即通过builder,用户可以直接通过cordova.exec来访问exec模块。

         当然,发布的target都是windowcommon下的发布定义(objects),所使用的发布策略是不覆写原有属性。之所以这样做,是考虑到之后浏览器加强html5支持后,有些模块将会原生提供。

platform下的发布定义(objects)所使用的发布策略是覆写所有属性。其定义的是与平台密切相关的模块,因此会做强制覆写。

注意platform下的发布定义,除了objects外。还提供了merge定义。这里的merge作用在于“增强”。为原有的通用模块,增加一些平台相关的其它方法。在android/platform.jsmerges定义便是一个例子。最终发布的notification将既包含commonnotification.jsalert方法,也包含platformnotification.jsactivityStart方法。

发布模块后会执行platform.initialize方法。这个方法用于做platform初始化工作,将与平台特性紧耦合,尤其会与NativeJavascript互相通信相关。在android平台版本中,initialize包含了polling/xhr的初始化,android的按键事件、可兼容web数据库apilocalstorage。当这些工作完毕后,将会广播onCordovaReady事件。此后便可以安全的使用phonegap所提供的所有功能了。

onCordovaReady后会去准备onDeviceReady。关于onDeviceReady的详细描述,我在上篇解析的channel中提过了,在此也就不重复了。

三、总结

至此phonegapNativeJavascript层源码解析完毕。有关如何扩展phonegap插件,各位可以参照github上面的这个开源项目:https://github.com/purplecabbage/phonegap-plugins

需要注意的是。其中的addPlugin写法,phonegap已经不再推荐使用了。并表示该方法将在2.0版本中移除。

阅读(1732) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~
评论热议
请登录后评论。

登录 注册