第十章 应用Moduler方式重构应用
不同于导航式容器如ViewStack等容器加载方式,从Flex2开始,Flex标准实现中包含了一种名称叫做ModuleLoader的容器。这个容器可以在需要的时候加载已经预编译过的各种Module模块,而不需要在容器初始化时强制地一次性加载所有容器模块。Module加载是Adobe解决Flex系统应用初始化时较大的下载负载而设计的一种折中方案。在后续的章节中我们会了解到在现实的企业开发中,尤其是大型的系统应用中,Module加载方式和Cairngorm架构的结合,基本上可以解决Flex应用的负载平衡问题。本章将就以下几个方面详细介绍Module加载方式的来龙去脉。
1. Module加载方式概览
2. Module的创建、编译、加载、卸载方法。
3. ModuleLoader事件处理
4. Module模块间的数据共享和传输
5. Module和Cairngorm整合前瞻。
10.1 Module加载方式概览
在理解这种加载方式之前,我们应该首先知道什么是Module模块。Module 实际上是一个预编译的SWF文件。虽然是SWF格式的文件,但是这个文件不能独立运行,并且只能被ModuleLoader加载后才能显示。逻辑上它是一个容器,可以像一般的容器一样包含别的容器,组件,甚至是别的Module模块。根据需要,预编译的Module模块可以被应用加载和卸载。不同的应用可以共享这些Module模块。Flex应用可以被分割为若干个预编译的Module模块,可以在需要时分别加载这些模块,避免在系统初始化时加载全部子容器。采用这种方式的Flex应用从设计上分隔了逻辑相对独立的模块,减少了系统初始化时的加载时间。
10.2 Module的创建、编译、加载和卸载方法
我们有两种方式创建一个Module模块,一是利用MXML标签<mx:Module>创建Module类;令一种方式采用ModuleManager类在ActionScript中创建Module模块类。模块类创建后将被编译成SWF文件。如前面章节所述我们可以利用mxmlc编译器手动编译或者在Flex Builder3集成开发环境提供的工具自动编译Module类为SWF文件。这里介绍采用Flex Builder3集成开发环境提供的方式创建一个Module类。
10.2.1 在FlexBuilder3中创建基于MXML标签的Module模块
创建基于MXML标签的Module模块,需要扩展mx.modules.Module.
1. 第一步,在Flex Builder3集成开发环境中创建一个Flex项目Moduler。
2. 第二步,选择FileàNewàMXML Module
3. 第三步,输入Module文件名字为MXMLDemoModule,设置Module的高度和宽度,选择Module容器的布局方式为absolute.,选择默认的预编译后优化的SWF选项,单击Finish按钮。
4. Module文件被创建,编辑Module文件,在此例中我们可以假定是一个登陆Module界面,
其源代码为,
<?xml version="1.0" encoding="utf-8"?>
<mx:Module xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="400" height="300">
<mx:Panel width="300" height="220" layout="absolute" horizontalCenter="0"
verticalCenter="0" backgroundAlpha="0.5">
<mx:Form horizontalCenter="0" verticalCenter="0" paddingBottom="0" paddingLeft="0" paddingRight="0"
paddingTop="0">
<mx:FormItem label="Account" indicatorGap="5">
<mx:TextInput borderStyle="solid" id="account" text="admin"/>
</mx:FormItem>
<mx:FormItem label="Password" indicatorGap="5">
<mx:TextInput borderStyle="solid" id="password" text="admin" displayAsPassword="true"/>
</mx:FormItem>
<mx:FormItem direction="horizontal" >
<mx:Button id="logined" label="Login" styleName="blueButton"/>
<mx:Button id="reset" label="Reset" styleName="greenButton"/>
</mx:FormItem>
</mx:Form>
</mx:Panel>
<mx:Label text="CopyRight 2008 demo Solutions" horizontalCenter="0" verticalCenter="252.5"/>
</mx:Module>
集成登陆Module模块到主应用文件ModulerApp.mxml,
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute">
<mx:ModuleLoader url="MXMLDemoModule.swf"/>
</mx:Application>
10.2.2 创建基于ActionScript的Module模块
在ActionScript中创建的Module模块类大都继承了mx.modules.Module或者mx.modules.ModuleBase基础类。我们都知道,MXML标签<mx:Module>实际上是mx.modules.Module类的另一种表现方式,这也就不难理解无论在MXML标签中还是在ActionScript中创建Module模块其实都是相通的。自定义Module类继承mx.modules.Module和继承mx.modules.ModuleBase两个基类的不同是,继承自前者的或者基于<mx:Module>标签的自定义Module组件将被加入到框架可视化显示列表,后者则没有。参考示例
package {
import mx.modules.ModuleBase;
public class MyModule extends ModuleBase {
public function MyModule() {
trace("MyModule was created!");
}
}
}
10.2.3 编译Module模块
前面已经提及我们有两种方式来编译Module模块文件。像编译主应用文件一样,一种是在命令行手动编译,最简单的情形 可以利用这个命令
Mxmlc MyModule.mxml
另一种是在Flex Builder3中提供的自动编译工具编译。编译的结果是一个可以被装载的SWF文件,与编译后的SWF主应用文件最大的不同就是,模块SWF文件不能独立运行,只能在被装载后才能够和其宿主共同运行。
10.2.4 怎样装载和卸载Module模块
总体来说,有几种方式可以实现Module模块的装载和卸载。
- 利用ModuleLoader:ModuleLoader类提供了一系列高层处理Module的编程接口。
- 利用ModuleManager: ModuleManager类提供了低层次的处理Module的装载卸载以及事件响应等的变成接口。
下面将详细地讲解这两种处理方式。
我们可以利用ModuleLoader类在主应用文件或者别的Module模块中加载任意预编译的Module对象。最简单的方式是采用ModuleLoader类的标签形式<mx:ModuleLoader>在主应用的MXML文件中显式地加载Module,然后只需要设置这个标签的url属性为预编译Module的SWF文件位置,可以是相对路径,也可以是绝对路径。参考实例,
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute">
<mx:ModuleLoader url="MXMLDemoModule.swf"/>
</mx:Application>
编程时我们可以根据需要替换此url所指向的模块位置,实现不同模块的更迭显示。当ModuleLoader类初始化时或者每次更改这个url属性值时,这个ModuleLoader类的loadModule()方法被触发。如果设定url属性为空字符串,ModuleLoader卸载当前加载的Module模块。
ModuleLoader其实是一种特殊的导航式容器。和一般导航式容器如ViewStack不同的是,ModuleLoader不必在初始化时携带加载所有的孩子组件。了解这一点,我们可以猜想到,在一个Flex主应用中可以包含甚至嵌套包含多个ModuleLoader实例,以下示例展示在一个ViewStack容器中包含多个ModuleLoader的示例。
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Panel
title="Multiple Modules Demo "
height="100%"
width="100%"
paddingTop="0"
paddingLeft="0"
paddingRight="0"
paddingBottom="0"
>
<mx:VBox id="v1" label="Module1">
<mx:Label id="l1" text="Module1.swf"/>
<mx:ModuleLoader url="Module1.swf"/>
</mx:VBox>
<mx:VBox id="v2" label="Module2">
<mx:Label id="l2" text="Module2.swf"/>
<mx:ModuleLoader url="Module2.swf"/>
</mx:VBox>
<mx:VBox id="v3" label="Module3">
<mx:Label id="l3" text="Module3.swf"/>
<mx:ModuleLoader url="Module3.swf"/>
</mx:VBox>
</mx:TabNavigator>
</mx:Panel>
</mx:Application>
同时我们可以利用ModuleLoader类提供的loadModule()和unloadModule()方法动态地指定被加载的Module模块。这两个方法没有输入形参,调用时分别加载和卸载当前ModuleLoader实例url属性所指向的Module模块。下面示例展示了在TabNavigator容器中,按钮点击后分别加载卸载不同的Module模块
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
import mx.modules.*;
public function moduleLoader(m:ModuleLoader, s:String):void {
if (!m.url) {
m.url = s;
return;
}
m.loadModule();
}
public function moduleUnloader(m:ModuleLoader):void {
m.unloadModule();
}
]]>
</mx:Script>
<mx:Panel title="Dynamically load/unload multiple Modules"
height="100%"
width="100%"
paddingTop="0"
paddingLeft="0"
paddingRight="0"
paddingBottom="0"
>
<mx:TabNavigator id="navigator"
width="100%"
height="100%"
creationPolicy="all"
>
<mx:VBox id="vb1" label="Module1">
<mx:Button label="Load" click="moduleLoader(moduleLoader, l1.text)" />
<mx:Button label="Unload" click="moduleUnloader(moduleLoader)" />
<mx:Label id="l1" text="Module1.swf"/>
<mx:ModuleLoader id="moduleLoader"/>
</mx:VBox>
<mx:VBox id="vb2" label="Module2">
<mx:Button label="Load" click="moduleLoader(moduleLoader2, l2.text)"/>
<mx:Button label="Unload" click="removeModule(moduleLoader2)" />
<mx:Label id="l2" text="Module2.swf"/>
<mx:ModuleLoader id="moduleLoader2"/>
</mx:VBox>
<mx:VBox id="vb3" label="Module3">
<mx:Button label="Load" click="moduleLoader(moduleLoader3, l3.text)"/>
<mx:Button label="Unload" click="removeModule(moduleLoader3)" />
<mx:Label id="l3" text="Module3.swf"/>
<mx:ModuleLoader id="moduleLoader3"/>
</mx:VBox>
</mx:TabNavigator>
</mx:Panel>
</mx:Application>
我们当然也可以利用ModuleManager类来加载Module模块。这种方式比起纯粹的ModuleLoader方式稍微复杂一点,但是ModuleManager提供了比ModuleLoader更加强大的能力来管理Module模块的加载过程。具体操作可以分成以下几步
- 通过ModuleManager实例的getModule()方法拿到Module模块的一个索引,索引类型为IModuleInfo。
- 调用这个索引的load()方法。
- 利用这个接口的factory属性拿到它相关连的Module工厂,调用此工厂的create()方法,并将返回值强制转换成当前的Module类型。
参考示例
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" creationComplete="init()">
<mx:Script>
<![CDATA[
import mx.events.ModuleEvent;
import mx.modules.ModuleManager;
import mx.modules.IModuleInfo;
public var info:IModuleInfo;
private function init():void {
info = ModuleManager.getModule("Module1.swf");
info.addEventListener(ModuleEvent.READY, moduleEventHandler);
info.load();
}
private function moduleEventHandler(e:ModuleEvent):void {
vb.addChild(info.factory.create() as Module1);
}
]]>
</mx:Script>
<mx:VBox id="vb"/>
</mx:Application>
理论上说,Flex应用第一次启动时初始化下载的大小,集成了Module模块的应用相对比没有集成Module模块的相似应用要小一些。这减少了初始化页面的等待时间。甚至在Module模块的SWF文件还没有下载完毕的时候,主应用文件也可以顺利显示。Module模块第一次加载时将被缓存在客户端IE, 已缓存的Module模块被再次加载时,FlashPlayer将直接在缓存中加载Module实例,减少了加载时间,提高了用户体验。当然这有一个前提就是,客户端IE没有被清空。所以在实际操作中,你可以在SWF模块真正的被使用之前将其预加载到客户端缓存。方法是调用ModuleManager.getModule("Module1.swf").load()语句。在调用create()方法之前并没有创建这个模块实例,而只是纯粹地将模块SWF文件加载到客户端内存。下面实例展示了在系统初始化时预先加载一个名字叫做Module1.swf的模块,当此模块需要显示时才创建这个Module模块实例。
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="modulePreloader()">
<mx:Script>
<![CDATA[
import mx.events.ModuleEvent;
import mx.modules.ModuleManager;
import mx.modules.IModuleInfo;
private function modulePreloader():void {
var info:IModuleInfo = ModuleManager.getModule("Module1.swf");
info.addEventListener(ModuleEvent.READY, moduleEventHandler);
info.load();
}
private function moduleEventHandler(e:ModuleEvent):void {
trace('' + e.type);
}
]]>
</mx:Script>
<mx:Panel title="Module Preloader" height="100%" width="100%" >
<mx:TabNavigator id="tn" width="100%" height="100%" creationPolicy="all" >
<mx:VBox id="vb1" label="Module1">
<mx:ModuleLoader url="Module1.swf"/>
</mx:VBox>
</mx:TabNavigator>
</mx:Panel>
</mx:Application>
10.3 怎样处理ModuleLoader事件
我们不仅可以以上述的方式加载卸载预编译Module模块,而且更进一步,也可以捕捉和处理模块在加载卸载过程中可能会触发的各种事件。这些事件由ModuleLoader触发,分别有setup,ready,loading,unload,progress,error和urlChanged等。很好的利用这些内置的事件可以使我们更加精细地跟踪模块加载卸载过程,实现更加强大的功能。下述示例展示了一个自定义的ModuleLoader组件CustomModuleLoader,这个组件会跟踪当模块实例被主应用加载时所有触发的事件。
<?xml version="1.0" encoding="iso-8859-1"?>
<mx:ModuleLoader xmlns:mx="http://www.adobe.com/2006/mxml" xmlns="*"
creationComplete="init()">
<mx:Script>
<![CDATA[
public function init():void {
addEventListener("urlChanged", onUrlChanged);
addEventListener("loading", onLoading);
addEventListener("progress", onProgress);
addEventListener("setup", onSetup);
addEventListener("ready", onReady);
addEventListener("error", onError);
addEventListener("unload", onUnload);
standin = panel;
removeChild(standin);
}
public function onUrlChanged(event:Event):void {
if (url == null) {
if (contains(standin))
removeChild(standin);
} else {
if (!contains(standin))
addChild(standin);
}
progress.indeterminate=true;
unload.enabled=false;
reload.enabled=false;
}
public function onLoading(event:Event):void {
progress.label="Loading module " + url;
if (!contains(standin))
addChild(standin);
progress.indeterminate=true;
unload.enabled=false;
reload.enabled=false;
}
public function onProgress(event:Event):void {
progress.label="Loaded %1 of %2 bytes...";
progress.indeterminate=false;
unload.enabled=true;
reload.enabled=false;
}
public function onSetup(event:Event):void {
progress.label="Module " + url + " initialized!";
progress.indeterminate=false;
unload.enabled=true;
reload.enabled=true;
}
public function onReady(event:Event):void {
progress.label="Module " + url + " successfully loaded!";
unload.enabled=true;
reload.enabled=true;
if (contains(standin))
removeChild(standin);
}
public function onError(event:Event):void {
progress.label="Error loading module " + url;
unload.enabled=false;
reload.enabled=true;
}
public function onUnload(event:Event):void {
if (url == null) {
if (contains(standin))
removeChild(standin);
} else {
if (!contains(standin))
addChild(standin);
}
progress.indeterminate=true;
progress.label="Module " + url + " was unloaded!";
unload.enabled=false;
reload.enabled=true;
}
public var standin:DisplayObject;
]]>
</mx:Script>
<mx:Panel id="panel" width="100%">
<mx:ProgressBar width="100%" id="progress" source="{this}"/>
<mx:HBox width="100%">
<mx:Button id="unload"
label="Unload Module"
click="unloadModule()"
/>
<mx:Button id="reload"
label="Reload Module"
click="unloadModule();loadModule()"
/>
</mx:HBox>
</mx:Panel>
</mx:ModuleLoader>
相应主应用文件
<?xml version="1.0"?>
<mx:Application xmlns="*" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
[Bindable]
public var selectedItem:Object;
]]>
</mx:Script>
<mx:ComboBox
width="215"
labelField="label"
close="selectedItem=ComboBox(event.target).selectedItem"
>
<mx:dataProvider>
<mx:Object label="Select Coverage"/>
<mx:Object
label="Life Insurance"
module="insurancemodules/LifeInsurance.swf"
/>
<mx:Object
label="Auto Insurance"
module="insurancemodules/AutoInsurance.swf"
/>
<mx:Object
label="Home Insurance"
module="insurancemodules/HomeInsurance.swf"
/>
</mx:dataProvider>
</mx:ComboBox>
<mx:Panel width="100%" height="100%">
<CustomModuleLoader id="mod"
width="100%"
url="{selectedItem.module}"
/>
</mx:Panel>
<mx:HBox>
<mx:Button label="Unload" click="mod.unloadModule()"/>
<mx:Button label="Nullify" click="mod.url = null"/>
</mx:HBox>
</mx:Application>
10.3.1 应用error事件
使用ModuleLoader的error事件可以允许开发者在当不知什么原因Module模块没有被加载或卸载成功的时候,做一些必要的动作。参考以下示例
<?xml version="1.0"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
import mx.events.ModuleEvent;
import mx.modules.*;
import mx.controls.Alert;
private function errorHandler(e:ModuleEvent):void {
Alert.show("There was an error loading the module." +
" Please contact the Help Desk.");
trace(e.errorText);
}
public function createModule():void {
if (chartModuleLoader.url == ti1.text) {
// If they are the same, call loadModule.
chartModuleLoader.loadModule();
} else {
// If they are not the same, then change the url,
// which triggers a call to the loadModule() method.
chartModuleLoader.url = ti1.text;
}
}
public function removeModule():void {
chartModuleLoader.unloadModule();
}
]]>
</mx:Script>
<mx:Panel title="Module Example"
height="90%"
width="90%"
paddingTop="10"
paddingLeft="10"
paddingRight="10"
paddingBottom="10"
>
<mx:HBox>
<mx:Label text="URL:"/>
<mx:TextInput width="200" id="ti1" text="ColumnChartModule.swf"/
>
<mx:Button label="Load" click="createModule()"/>
<mx:Button label="Unload" click="removeModule()"/>
</mx:HBox>
<mx:ModuleLoader id="chartModuleLoader" error="errorHandler(event)"/
>
</mx:Panel>
</mx:Application>
10.3.2 应用progress事件
使用ModuleLoader的progress事件可以让你跟踪模块实例加载的进度信息。参考示例,
<?xml version="1.0"?>
<!-- modules/SimpleProgressEventHandler.mxml -->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
import mx.events.ModuleEvent;
import flash.events.ProgressEvent;
import mx.modules.*;
[Bindable]
public var progBar:String = "";
[Bindable]
public var progMessage:String = "";
private function progressEventHandler(e:ProgressEvent):void {
progBar += ".";
progMessage =
"Module " +
Math.round((e.bytesLoaded/e.bytesTotal) * 100) +
"% loaded";
}
public function createModule():void {
chartModuleLoader.loadModule();
}
public function removeModule():void {
chartModuleLoader.unloadModule();
progBar = "";
progMessage = "";
}
]]>
</mx:Script>
<mx:Panel title="Module Example"
height="90%"
width="90%"
paddingTop="10"
paddingLeft="10"
paddingRight="10"
paddingBottom="10"
>
<mx:HBox>
<mx:Label id="l2" text="{progMessage}"/>
<mx:Label id="l1" text="{progBar}"/>
</mx:HBox>
<mx:Button label="Load" click="createModule()"/>
<mx:Button label="Unload" click="removeModule()"/>
<mx:ModuleLoader
id="chartModuleLoader"
url="ColumnChartModule.swf"
progress="progressEventHandler(event)"
/>
</mx:Panel>
</mx:Application>
10.4 怎样共享和传输Module模块间数据
Module模块是一个容器,每个独立的模块对象都相当于一个自定义组件。有以下几种方式可以实现模块-模块、模块-主应用、主应用-模块、模块-一般自定义组件间的数据传输和通信。
- 利用ModuleLoader的child、ModuleManager的factory、以及Application的parentApplication属性存取Module模块和主应用文件对象索引。
- 因为Module模块在ModuleLoader中通常用url属性来指定,所以我们可以通过在url上面拼GET参数,然后在Module模块中解析这些拼上去的参数的方式来传输数据。
- 通过ActionScript接口方式。你可以定义一个ActionScript接口,这个接口定义了一系列方法和属性。Module模块和主应用Application都可以访问这些属性。从而实现数据共享。
10.4.1 在主应用Application中访问Module模块对象
我们可以在主应用Application中访问子Module模块对象中定义的方法和属性。下面示例展示了通过ModuleLoader的child属性拿到子Module模块对象索引,然后调用其定义的公共方法getTitle().
<?xml version="1.0"?>
<!-- modules/ParentApplication.mxml -->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script><![CDATA[
[Bindable]
private var s:String;
private function getTitle():void {
s = (m1.child as ChildModule1).getModTitle();
}
]]></mx:Script