Robotlegs2中文教程-1使用MVCBundle
本系列全部文章:using robotlegs2
目的
本章使用Robotlegs2自带的MVCBundle实现一个简单的MVC实例。
Robotlegs2在架构设计上,框架仅实现了生命周期管理、Logger、消息调度、插件管理器、配置管理器等核心功能,其他功能全部使用插件实现。而MVCBundle,就是Robotlegs2提供的一个插件和配置集合,这个集合包含所有MVC需要的插件和功能。
本章不会研究Robotlegs2在结构上的设计,而是从最终用户的角度来使用MVCBundle。若希望了解Robotlegs2的架构,请关注本系列后续文章。
本章也不会详细介绍MVCBundle的所有用法,那样会导致本文篇幅过长。下一章会进行详细介绍。
Timer-MVCBundle-Sample概述
Timer-MVCBundle-Sample是本章的实例名称。这个实例实现了一个简单的定时器。看看定时器的3个状态:
- 初始状态,设置时、分,类名为TimerSetView。按Start按钮开始倒计时。
- 倒计时状态,类名为TimerActionView。按Cancel按钮取消倒计时,回到初始状态。
- 倒计时时间到,类名为AlertView。按Dismiss按钮回到初始状态。
项目依赖
- 本项目依赖 minimalcomps 组件库和 robotlegs 2.0.0b6。
- 本项目源码下载
项目结构
这里简单介绍一下项目中的包分类,以及各个部分的作用。介绍按照MVC分块进行。如果你性急,也可以跳过这部分直接看后面的项目详解。
View + Mediator
刚才看到的后缀为View的类,我叫它们“视图类”,所有的视图类,都位于 view
包中,并实现 ITimerView
接口。 ITimerView
中并没有包含任何方法签名。实现这个接口,是为了方便进行类型匹配。
视图类用于我们能看到的定时器的3个状态,每个状态对应一个视图。定时器运行的过程中所显示的状态,在这3个视图之间切换。上节 Timer-MCBundle-Sample
已经介绍了这3个状态以及对应的类名。
视图类都是显示对象的子类,3个视图也就是3个显示对象。在一个视图显示的时候,它会被加到舞台上,其他的视图则移出舞台。
在这个项目中,我使用了MinialComps组件。视图类继承其中的VBox或者HBox。3个视图类如下图:
每个视图类都有一个带有Mediator后缀的类,这是视图类的中介类。中介类负责管理视图事件,接收和传递系统事件等等。所有的中介类都继承 Robotlegs2 提供的 Mediator
类。如下图:
Model
此项目有2个Model:TimerModel
和 ViewModel
。
TimerModel
实现 ITimerModel
接口。由于项目过于简单,ITimerModel
中不包含任何需要具体实现的方法,但是这种编程习惯是Robotlegs所推荐的。它既方便了在Robotlegs中替换Model(例如你可以在开发和发布的时候使用不同的Model),也符合中的针对接口编程的OO原则。
TimerModel
中包含了一个 flash.util.Timer
实例,用它来实现定时器的核心功能。同时它还会发布定时器 工作的事件,并暴露一些方法来停止和启动定时器。
ViewModel
保存上面提到的3个视图类的实例,以此实现对视图实例的重用。同时它还提供一个 getView()
方法根据类或者类名来获取一个新的/重用的实例。你可以把 ViewModel
理解成一个对象池,虽然它并不是对象池。如下图:
Command + Event
此项目有3个Commnd: ChangeStateCmd
、 TimerStartCmd
和 TimerStopCmd
。
ChangeStateCmd
负责切换视图状态。它收到切换的命令,然后移除掉当前舞台上的视图实例,再从 ViewModel
中获取一个视图实例,将它加到舞台上。
TimerStartCmd
负责用户主动开始定时器运行的事件。它只是简单的更新 TimerModel
中的定时器流逝时间,然后开启定时器。
TimerStopCmd
负责用户主动停止定时器运行的事件。它停止 TimerModel
中的定时器,然后将视图切换到 TimerSetView
。
TEvent
是此项目中使用的系统事件类型。上面的几个Command均使用这个类型代表的事件来触发。如下图:
项目详解
初始化Robotlegs2
先来看看主类 Robotlegs2TimerExample
的全部内容(省略了package和import):
1[SWF(width=200,height=200)]
2public class Robotlegs2TimerExample extends Sprite
3{
4 public function Robotlegs2TimerExample()
5 {
6 Style.embedFonts = true;
7 Style.fontSize = 8;
8 Component.initStage(this.stage);
9 init();
10 }
11
12 private var _context:IContext;
13
14 private function init():void
15 {
16 _context = new Context()
17 .install(MVCSBundle)
18 .configure(AppConfig)
19 .configure(new ContextView(this));
20
21 _context.logLevel = LogLevel.DEBUG;
22 trace("init done");
23
24 //这里通过内置的事件框架来实现View的启动
25 (_context.injector.getInstance(IEventDispatcher) as IEventDispatcher).dispatchEvent(new TEvent(TEvent.CHANGE_STATE, TimerSetView));
26 }
27}
Style和Component都是 minimalcomps 中的类,具体用法可参考该组件源码。需要注意的是,如果需要显示中文,那么需要将 Style.embedFonts
设置为 false
,具体原因可参考 MinimalComps简介-一个超轻量级的纯AS组件库。
让我们来看 init
方法的具体内容。Context
类是Robotlegs2初始化的核心。在本项目的初始化当中,我们使用了 Context
的 install
和 configuare
方法。这两个方法都会返回 Context
自身,因此我们可以进行链式调用。
链式调用是JAVA和JavaScript中常用的调用方式,能减少代码量,让代码看起来更干净。当然,如果你不愿意用它,依然可以使用旧的方式。例如,上面的链式调用可以改成这样:
1_context = new Context();
2_content.install(MVCSBundle);
3_content.configure(AppConfig);
4_content.configure(new ContextView(this));
install()
的作用是安装一个 IExtension
或者 IBundle
。前者是一个扩展,后者是一个/一堆扩展和配置的集合。Robotlegs2根据我们的调用对 IExtension
和 IBundle
进行按需安装,这样可以节省资源。install()
支持无限参数,如果有多个 IBundle
,可以使用链式调用进行单个安装,也可以使用一个 install()
多个参数进行安装。
注意
为了描述的统一性和准确性,后文将不对
IExtension
和IBundle
进行翻译,而直接使用接口名。
MVCBundle
是一个包含许多 IExtension
的 IBundle
,它包含了MVC框架中的所有 IExtension
。除此以外,它还包含一些Logger系统等核心功能。在这个项目中,我们不需要其他的 IExtension
,一包足矣。
configuare()
的作用是对项目进行配置,通常将各种映射放在这里。与 install()
只能接收 IBundle
和 IExtension
不同, configuare()
可以接受任何类型的对象作为参数。同时,它也接受多个参数。例如上面的 configuare()
相关代码可以写成这样:
1_content.configure(AppConfig, new ContextView(this));
需要注意的是,对于ContextView的配置,必须放在所有配置的最后。具体原因,请关注本系列后续文章。
ContextView
是Robotlegs2提供的类,它没有实现任何接口,只是用于保存根显示对象的引用。我们可以将它注入到需要的地方,以方便访问根显示对象。
Injector
init()
的最后一句直接发布 TEvent.CHANGE_STATE
事件,ChangeStateCmd
会处理这个事件,它获取 TimerSetView
的实例,将其添加到舞台上。
注意这一句的作用:
1(_context.injector.getInstance(IEventDispatcher) as IEventDispatcher)....
最外层的括号中的内容,是为了获取一个 IEventDispatcher
的实例。IEventDispatcher
是AS3原生的用于发布事件的接口。 Robotlegs2中使用它作为 MVCBundle
的事件核心。
它以单例的形式存在,我们使用 injector.getInstance
方法,就可以得到这个单例。
injector是SwiftSuspenders提供的注入器。Robotlegs2使用这个注入器来实现注入,我们也可以使用它来获取Robotlegs2中注册过的各种资源。在 Context
中包含一个它的引用。我们既可以使用 _context.injector
的方式获取它,也可以使用注入的方式获取它。
例如,我们要得到 TimerModel
的单例,可以这样做(注意我使用的是接口):
1[Inject]
2public var injector:Injector;
3
4//获取到ITimerModel的单例,调用它的start方法让计时器开始运行
5(injector.getInstance(ITimerModel) as ITimerModel).start();
那么,既然 ITimerModel
已经在Robotlegs2中注册(这个注册是在AppConfig中完成的,后面会讲到),更简单的方法是这样的:
1[Inject]
2public var timerModel:ITimerModel;
3
4timerModel.start();
可是,我们为什么不在 Robotlegs2TimerSample
中直接注入 IEventDispatcher
,而要用 injector
来获取呢?例如像这样:
1[Inject]
2public var eventDispatcher:IEventDispatcher;
3
4private function init():void
5{
6 ................
7 eventDispatcher.dispatchEvent(new TEvent(TEvent.CHANGE_STATE, TimerSetView));
8}
这样的代码会出现运行时错误,原因是 eventDispatcher
的值为 null
。
这是因为在默认情况下, 只有被注入器初始化的类,才能被注入 。主类 Robotlegs2TimerSample
是无法被注入器初始化的,因此在主类中进行注入,在默认情况下是不会成功的。
AppConfig
让我们看看 AppConfig
的内容(同样省略了package和import)。
1public class AppConfig implements IConfig
2{
3 [Inject]
4 public var injector:Injector;
5
6 [Inject]
7 public var mediatorMap:IMediatorMap;
8
9 [Inject]
10 public var commandMap:IEventCommandMap;
11
12 [Inject]
13 public var logger:ILogger;
14
15 public function configure():void
16 {
17 models();
18 mediators();
19 commands();
20 logger.info("logger in AppConfig");
21 }
22
23 private function models():void
24 {
25 injector.map(ITimerModel).toSingleton(TimerModel);
26 injector.map(ViewModel).asSingleton();
27 }
28
29 private function mediators():void
30 {
31 mediatorMap.map(TimerSetView).toMediator(TimerSetMediator);
32 mediatorMap.map(TimerActionView).toMediator(TimerActionMediator);
33 mediatorMap.map(AlertView).toMediator(AlertViewMediator);
34 }
35
36 private function commands():void
37 {
38 commandMap.map(TEvent.TIMER_START, TEvent).toCommand(TimerStartCmd);
39 commandMap.map(TEvent.TIMER_STOP, TEvent).toCommand(TimerStopCmd);
40 commandMap.map(TEvent.CHANGE_STATE, TEvent).toCommand(ChangeStateCmd);
41 }
42}
AppConfig
实现了 IConfig
接口。Robotlegs2在对该接口进行配置的时候,会自动调用它的 configuare
方法进行注册。
我们来仔细看看这个类中注入的4个对象。
injector上面讲过一些,这里继续。使用它,可以进行单例映射。toSingleton()
方法和 asSingleton()
方法的不同之处在于,前者针对接口映射,我们可以方便在该方法的参数中替换具体实现;后者针对具体实现映射,它不能被替换。
如果你用过Robotlegs1,你会发现injector的映射语法改变了不少。它抛弃了原来使用参数来映射的方式,改用链式调用,在我看来,这样的改动让语法更加简洁,且容易记忆。
注意
本系列文章在必要的时候会对Robotlegs1和2进行比较,这是为了方便使用过Robotlegs1的读者进行更深入的理解。如果你以前没有使用过Robotlegs1,可以跳过这些内容。
例如,在Robotlegs1中,要实现本例中的两个单例映射,需要这样调用:
1injector.mapSingletonOf(ITimerModel, TimerModel);
2injector.mapSingleton(ViewModel);
mediatorMap用来实现视图类和Mediator的映射。以 mediatorMap.map(TimerSetView).toMediator(TimerSetMediator)
为例,这句实现了在 TimerSetView
被添加到舞台的时候, TimerSetMediator
会自动创建并完成初始化,同时进行需要的注入。
在Robotlegs1中,mediatorMap的调用方式也做了与injector类似的修改。这并不奇怪,因为mediatorMap的映射就是使用injector实现的。
例如,在Robotlegs1中,实现 TimerSetView
与 TimerSetMediator
的映射,需要这样调用:
1mediatorMap.mapView(TimerActionView, TimerActionMediator);
commandMap用来实现事件与Command的映射。以 commandMap.map(TEvent.TIMER_START, TEvent).toCommand(TimerStartCmd)
为例,这句实现了在 TEvent.TIMER_START
事件发生的时候, TimerStartCmd
被自动创建,同时进行需要的注入,然后执行其 execute
方法。
在Robotlegs1中,要实现 TEvent.TIMER_START
与 TimerStartCmd
的映射,需要这样调用:
1commandMap.mapEvent(TEvent.TIMER_START, TimerStartCmd);
logger提供一个全局的日志分析器,默认使用tarce实现。logger不但可以像上面的代码一样,直接输出文字,也可以方便的实现文本替换。例如:
1logger.info("logger in {0}, {1}", [this, "done"]);
2//输出内容
3//638 INFO Context-0-9f [class AppConfig] logger in [object AppConfig], done
AppConfig实际上是分担了一部分Context的功能。我们可以把不同模块,不同需求的Config进行分离,让程序之间的耦合更加松散。
在Robotlegs1中,我们一般把映射放在 Context
的 startup
方法中,这样在项目逐渐庞大的时候,Context就无可避免的庞大起来:
1..........
2//========================================
3// 注入Model和Service
4//========================================
5injector.mapSingletonOf(SocketServiceBase, SocketService);
6injector.mapSingleton(SocketDataDispatcherModel);
7injector.mapSingleton(StateModel);
8injector.mapSingleton(HTTPService2);
9injector.mapSingleton(UpdateService);
10
11//----界面信号
12signalCommandMap.mapSignalClass(FightEndSign, FightEndCmd);
13signalCommandMap.mapSignalClass(GuideAniEndSign, GuideAniEndCmd);
14signalCommandMap.mapSignalClass(GuideHelpSign, GuideHelpCmd);
15signalCommandMap.mapSignalClass(GuideCheckOpen8Sign, GuideCheckOpen8Cmd);
16signalCommandMap.mapSignalClass(CheckMausoleumSign, CheckMausoleumCmd);
17signalCommandMap.mapSignalClass(ExitMausoleumSign, ExitMausoleumCmd);
18
19//----其他不便分类的信号
20signalCommandMap.mapSignalClass(NecessarySocketInitDataDoneSign, NecessarySocketInitDataDoneCmd);
21signalCommandMap.mapSignalClass(SetDefaultPreferenceSign, SetDefaultPreferenceCmd);
22signalCommandMap.mapSignalClass(ActiveSign, ActiveCmd);
23signalCommandMap.mapSignalClass(ChargeInWebSign, ChargeInWebCmd);
24
25//========================================
26// 所有的子滑动界面注册
27//========================================
28//LoginModuleContent和RegisterModuleContent与接入商相关,因此在子Context类中注册
29mediatorMap.mapView(FightDeployModuleContent, FightDeployModuleContentMediator, null, false, false);
30mediatorMap.mapView(FightSpyModuleContent, FightSpyModuleContentMediator, null, false, false);
31mediatorMap.mapView(ChooseServerModuleContent, ChooseServerModuleContentMediator, null, false, false);
32mediatorMap.mapView(AccelerateCoolingModule,AccelerateCoolingModuleMediator,null,false,false);
33........
运行流程
1 这段代码是项目的界面入口
1(_context.injector.getInstance(IEventDispatcher) as IEventDispatcher).dispatchEvent(new TEvent(TEvent.CHANGE_STATE, TimerSetView));
IEventDispatcher
发布了一个事件 TEvent.CHANGE_STATE
,同时传递了一个视图类 TimerSetView
。由于我们前面已经将该事件映射到了 ChangeStateCmd
,因此,ChangeStateCmd
中的 execute()
方法被执行。
2 ChangeStateCmd做了什么?
让我们看看 ChangeStateCmd
的全部内容:
1public class ChangeStateCmd extends Command
2{
3 [Inject]
4 public var evt:TEvent;
5
6 [Inject]
7 public var logger:ILogger;
8
9 [Inject]
10 public var contextView:ContextView;
11
12 [Inject]
13 public var viewModel:ViewModel;
14
15 override public function execute():void
16 {
17 logger.debug(evt.info);
18 logger.debug(contextView.view.numChildren);
19 if(contextView.view.numChildren > 0)
20 {
21 var __currentView:ITimerView = contextView.view.getChildAt(0) as ITimerView;
22 if(__currentView)
23 {
24 contextView.view.removeChild(__currentView as DisplayObject);
25 }
26 }
27 var __newView:DisplayObject = viewModel.getView(evt.info) as DisplayObject;
28 center(__newView);
29 contextView.view.addChild(__newView);
30 logger.debug("get view:{0}", [__newView]);
31 }
32
33 private function center($view:DisplayObject):void
34 {
35 $view.x = (contextView.view.stage.stageWidth - $view.width) / 2;
36 $view.y = (contextView.view.stage.stageHeight - $view.height) / 2;
37 }
38}
在 ChangeStateCmd
中,我们需要得到 TEvent
传来的视图类,使用其创建视图实例,然后将实例添加到舞台上。
注入的 evt
实例,也就是前面 TEvent.CHANGE_STATE
事件传来的事件实例。在这次的调用这,evt.info
的值,就是 TimerSetView
。
execute()
方法先移除已存在的视图实例,然后根据 evt.info
的值获取一个新的实例,并将其添加到舞台。
所有的视图类,都实现了 ITimerView
接口。因此在这里可以通过判断 ITimerView
的实现情况,来了解实例的创建是否成功。
创建实例的工作在 ViewModel
中完成,可查看源码了解,此处不深入讲解。
TimerSetView
视图实例被添加到舞台的时候,TimerSetViewMediator
会自动创建。
3 TimerSetViewMediator
做了什么?
让我们来看看 TimerSetViewMediator
的全部内容:
1public class TimerSetMediator extends Mediator
2{
3 [Inject]
4 public var logger:ILogger;
5
6 [Inject]
7 public var v:TimerSetView;
8
9 public function TimerSetMediator()
10 {
11 }
12
13 override public function initialize():void
14 {
15 super.initialize();
16 logger.info("initialize");
17 v.startBtn.addEventListener(MouseEvent.CLICK, handler_start);
18 }
19
20 override public function destroy():void
21 {
22 v.startBtn.removeEventListener(MouseEvent.CLICK, handler_start);
23 super.destroy();
24 logger.info("destory");
25 }
26
27 private function handler_start(e:Event):void
28 {
29 logger.debug("click");
30 eventDispatcher.dispatchEvent(new TEvent(TEvent.TIMER_START, {minute:v.minute, second:v.second}));
31 eventDispatcher.dispatchEvent(new TEvent(TEvent.CHANGE_STATE, TimerActionView));
32 }
33}
v
被自动注入,它就是刚才我们创建的 TimerSetView
视图实例。
initialize()
方法在注入完成之后自动执行。因此,该方法适合进行 v
的视图侦听器。在本类中,加入了用户按下“Start”按钮时的鼠标事件侦听。
TimerSetView
的实例从舞台移除的时候, TimerSetViewMediator
被销毁。 destroy()
方法在此时执行。我们在这里移除“Start”按钮的鼠标事件侦听。
在单击“Start”按钮的时候,我们发布了两个系统事件 —— 1. TEvent.TIMER_START
,并传递当前选择的分、秒;2. TEvent.CHANGE_STATE
,切换到 TimerActionView
视图。
由于前面我们已经将 TEVent.TIMER_START
映射到了 TimerStartCmd
,因此 TimerStartCmd
中的 execute()
方法将被执行。
4 启动计时器
让我们来看看 TimerStartCmd
的全部内容:
1public class TimerStartCmd extends Command
2{
3 [Inject]
4 public var timerModel:ITimerModel;
5
6 [Inject]
7 public var evt:TEvent;
8
9 override public function execute():void
10 {
11 timerModel.minute = evt.info.minute;
12 timerModel.second = evt.info.second;
13 timerModel.start();
14 }
15
16}
在这里,我们更新了 TimerModel
中保存的分、秒,然后启动了计时器。
5 请继续
到此,我已经把主要流程讲完。至于后面的计时器时间到,或者中断计时器等操作,和前面的流程类似。大家可以继续在 TimerModel
和 TimerActionMediator
中找到它们的具体实现。
小结
本章使用一个简单的计时器例子,讲解了Robotlegs2中的MVCBundle的使用。拥有Robotlegs1使用经验的同学,现在应该已经可以转移到Robotlegs2上来了。
即使你从没有用过Robotlegs,那么也可以从本文开始,从头学习Robotlegs2。
下一章,我们将深入MVCBundle中,分析其中的Extension组成,并试图抛弃MVCBundle,使用更底层的方式来重新构架我们的计时器范例。
- 文章ID:1866
- 原文作者:zrong
- 原文链接:https://blog.zengrong.net/post/use_robotlegs2_1timermvcbundle/
- 版权声明:本作品采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可,非商业转载请注明出处(原文作者,原文链接),商业转载请联系作者获得授权。