传送门:
讨论q群:248908795
一、 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.js 和 platform.js 这两个是固定的。但是不同平台版本其实现是不一样的。除 lib包外,其余所有的 js 文件都是跨平台通用的。 ( 即使有差异应该也不大 )
简单介绍一下各个目录和一些关键组件 :
- cordova.js:拦截DOM,Window事件,加入自定义cordova事件,管理回调Javascript。
- scripts/require.js:PhoneGap中模块化机制的基础框架。简单但是也不简单!
- scripts/bootstrap.js:负责cordova的启动。
- common/channel.js:PhoneGap中实现事件监听的基础。
- common/builder.js:具备定制化构造模块的能力。
- common/plugin:如名字所示。这里放置所有平台通用的插件接口。
- lib/exec.js:于上一篇解析中提到。是Javascript调用Native的入口。
- lib/platform.js:与平台实现有关的初始化。
- lib/android/plugin:与android平台紧密相关的插件。
二、 浅析 PhoneGap 中的模块化机制
也许是因为本人见过的 Javascript 代码太少,见到 PhoneGap 的模块化机制后便觉得非常的有趣和前卫。Pascal 的作者沃斯曾写过一门叫做 module 的语言,其在语言级别做了模块化机制。我不知道 PhoneGap 模块化的思路是否也受 ; 此影响。
废话不多说了,从 require.js 开始看起吧。它是模块化的基础。
- var require,
- define;
- (function () {
- var modules = {};
- function build(module) {
- var factory = module.factory;
- module.exports = {};
- delete module.factory;
- factory(require, module.exports, module);
- return module.exports;
- }
- require = function (id) {
- if (!modules[id]) {
- throw "module " + id + " not found";
- }
- return modules[id].factory ? build(modules[id]) : modules[id].exports;
- };
- define = function (id, factory) {
- if (modules[id]) {
- throw "module " + id + " already defined";
- }
- modules[id] = {
- id: id,
- factory: factory
- };
- };
- define.remove = function (id) {
- delete modules[id];
- };
- })();
- //Export for use in node
- if (typeof module === "object" && typeof require === "function") {
- module.exports.require = require;
- module.exports.define = define;
- }
代码行数的确非常短。其定义了 require 和 define 两个函数。首先 define 函数用于声明一个模块。其中 id 表示模块名称,这必须是唯一的,而 factory 便是构造模块的工厂方法。 require 函数使用懒加载的方式获得已define 过的对应 id 的模块。
来看一个使用其的简单示例吧 :
- define("cordova/plugin/android/app", function(require, exports, module) {
- var exec = require('cordova/exec');
- module.exports = {
- /**
- * Clear the resource cache.
- */
- clearCache:function() {
- exec(null, null, "App", "clearCache", []);
- },
- /**
- * Load the url into the webview or into new browser instance.
- *
- * @param url The URL to load
- * @param props Properties that can be passed in to the activity:
- * wait: int => wait msec before loading URL
- * loadingDialog: "Title,Message" => display a native loading dialog
- * loadUrlTimeoutValue: int => time in msec to wait before triggering a timeout error
- * clearHistory: boolean => clear webview history (default=false)
- * openExternal: boolean => open in a new browser (default=false)
- *
- * Example:
- * navigator.app.loadUrl("http://server/myapp/index.html", {wait:2000, loadingDialog:"Wait,Loading App", loadUrlTimeoutValue: 60000});
- */
- loadUrl:function(url, props) {
- exec(null, null, "App", "loadUrl", [url, props]);
- },
- /**
- * Cancel loadUrl that is waiting to be loaded.
- */
- cancelLoadUrl:function() {
- exec(null, null, "App", "cancelLoadUrl", []);
- },
- /**
- * Clear web history in this web view.
- * Instead of BACK button loading the previous web page, it will exit the app.
- */
- clearHistory:function() {
- exec(null, null, "App", "clearHistory", []);
- },
- /**
- * Go to previous page displayed.
- * This is the same as pressing the backbutton on Android device.
- */
- backHistory:function() {
- exec(null, null, "App", "backHistory", []);
- },
- /**
- * Override the default behavior of the Android back button.
- * If overridden, when the back button is pressed, the "backKeyDown" JavaScript event will be fired.
- *
- * Note: The user should not have to call this method. Instead, when the user
- * registers for the "backbutton" event, this is automatically done.
- *
- * @param override T=override, F=cancel override
- */
- overrideBackbutton:function(override) {
- exec(null, null, "App", "overrideBackbutton", [override]);
- },
- /**
- * Exit and terminate the application.
- */
- exitApp:function() {
- return exec(null, null, "App", "exitApp", []);
- }
- };
- });
这是 phonegap 模块化编程的典型写法。首先在头部定义依赖的模块组件 , 再次通过设置 module.exports 向外部暴露出对应的方法。
由于 Javascript 中在语法级别没有私有访问符。因此往往解决之道是: a. 模仿 C 风格,命名使用 _ 开头的一律表示是私有 ;b. 新建一个 private 对象 , 将其私有方法放置其中 , 起到命名空间的作用 ;c. 将私有部分用 function{} 套住 , 用 return 返回公开部分。这种做法充分发挥了 javascript 闭包的优势,但是可读性比较差。
phoneGap 的 module 机制使用了第三种做法。但是通过将构造和依赖分离开来,使得可读性大大增加。代码清晰好懂,依赖性也一目了然。
也许有朋友要问。这种模块机制碰到循环依赖的情况怎么办?例如 A 在 define 阶段 requireB , B 在 define阶段又 requireA 。很遗憾,这种循环依赖的情况 依照 phoneGap 的模块化思路是无法实现的。因此对于关键组件的编制 ,phoneGap 总会小心翼翼地处理其依赖顺序。
通过阅读源码,发现大致的模块依赖顺序是这样子的 ( 被依赖模块到依赖模块 ):utils.js->channel.js|builder.js->cordova.js->exec.js|polling.js->callback.js->platform.js->bootstrap.js 。
三、 PhoneGap 中的事件处理机制
common/channel.js 是 PhoneGap 事件处理机制的基础。每个事件类型,都会包装成一个 Channel 对象。
既然是事件,那么就得支持基础的观察者模式吧? Channel 的 prototype 定义了事件的一些关键方法。subscribe 用于注册一个监听器,并给监听器一个 guid 。 guid 类似 cordova.js 中的 callbackId ,只是一个流水标识。但 Channel 中的 guid 稍有些不同,指定确切的 guid 可以对监听器做覆盖操作。
utils.close 是个很有趣的方法。 Javascript 中的调用不当引起 this 不对,这是新手常见的错误。常见的做法会通过封装 apply 做 delegate 。而 close 这个方法是绝了 , 它通过闭包包装了一个指向确定 this ,调用确定function, 使用确定实参的 final 函数。不管在什么样的环境下调用 , 这个方法总能正确执行。
subscribeOnce 类似于 YUI 或者 jquery 中的 one 。只会收到一次监听。 ( 若事件已经触发过,则在注册阶段立即回调监听 )
unsubscribe 和 fire 分别用来注销监听器和触发事件。触发事件将会引起监听器的广播操作。可选的 fireArgs用于保证 subscribeOnce 在事件已触发的情况下能获得正确的广播参数。
Channel 本身还有一个监听注册 / 注销的事件拦截。分别是 onSubscribe 和 onUnSubscribe 。在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 中我们看到下面一段代码。
- channel.join(function() {
- channel.onDeviceReady.fire();
- }, channel.deviceReadyChannelsArray);
waitForInitialization 用于添加 onDeviceReady 的等待 Channel 事件。 initializeComplete 用于触发指定的等待 Channel 事件。如果想要增加 onDeviceReady 的条件,我们只需要在 onCordovaReady 之前添加waitForInitialization 即可。事实上,在 lib/android/plugin/storage.js 中我们便可以看到一个绝佳的例子。cupcakeStorage 利用本地 Plugin 为不支持 localStorage API 的 WebView 提供了一个备选方案。在本地建立好备用的 sqlite 数据库后 ,cupcakeStorage 的等待时间便结束完毕。
四、 启动与 PhoneGap 自定义事件
首先上图。
上图为本人整理的启动事件序列,待会儿大家便能从源码中看到了。
待续。。。。