初探PureMVC:使用PHP+MySQL+Flex结合PureMVC框架做了个Flex留言本
PureMVC框架听也听得多了,但自己一直没有着手去弄过。最近有必要学习它了,于是在各种搜索引擎找了一些资料,现在总算对这个框架有所了解。眼高手低是不行的,所以自己动手去实践一下了。OK!弄一个留言本试试看,然后自己再详细地分析了一下,希望对PureMVC框架有更深的一层了解。我写的不是教程哦,是总结。所以哪里写得不好,请原谅,也恳请您能指出哪里不好。下面只介绍了这个留言本在开始时从服务端获取数据的工作流程,先看下面那个流程图,接着奉上详细的代码以及注释,最后提供源文件下载。
Main.mxml
- <?xml version="1.0" encoding="utf-8"?>
- <!--
- 初探PureMVC, 学习理论后, 实践最重要. 如果发现一些不恰当的地方, 请到以下地方指出来:
- My Blog : riaoo.com
- My Email: y_boy@126.com & riahome.cn@gmail.com
- 谢谢!
- 注意: 本例子使用了 PHP + MySQL, 为了测试成功, 请在虚拟环境下测试. 测试前, 请先创建数据库, 需要创建的数据库在 "sql" 文件夹里了.
- 欢迎交流!
- -->
- <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
- xmlns:UI="cn.riahome.guestbook.puremvc.view.UI.*"
- creationComplete="facade.startup( this )" fontSize="12">
- <!--
- 目前分析途径: Main.mxml
- 完整分析途径: Main.mxml -> ApplicationFacade.as -> StartupCommand.as -> ListTopicProxy.as -> ListPanelMediator.as
- 整个pureMVC框架就是从上面那个 startup() 函数开始了, 正如其名, 它启动了整个框架.
- 请按着 Ctrl 键点击这个函数, 进入去看代码.
- -->
- <mx:Style>
- .errorTip{
- fontSize:12;
- }
- </mx:Style>
- <mx:Script>
- <![CDATA[
- import cn.riahome.guestbook.puremvc.ApplicationFacade;
- private var facade:ApplicationFacade = ApplicationFacade.getInstance();
- ]]>
- </mx:Script>
- <UI:ListPanel id="listPanel" x="71" y="10" width="555" height="498"/>
- <UI:InsertPanel id="insertPanel" x="634" y="318" width="400" height="190"/>
- <UI:DetailPanel id="detailPanel" x="634" y="10" width="400" height="300"/>
- </mx:Application>
ApplicationFacade.as
- package cn.riahome.guestbook.puremvc
- {
- import cn.riahome.guestbook.puremvc.controller.StartupCommand;
- import org.puremvc.as3.interfaces.IFacade;
- import org.puremvc.as3.patterns.facade.Facade;
- public class ApplicationFacade extends Facade implements IFacade
- {
- /**
- *
- * 目前分析途径: Main.mxml -> ApplicationFacade.as
- * 完整分析途径: Main.mxml -> ApplicationFacade.as -> StartupCommand.as -> ListTopicProxy.as -> ListPanelMediator.as
- *
- * 来到这里, 有必要说一下 MVC, 即 Model, View, Controller:
- *
- * Model:
- * model => 数据! 本人觉得在 pureMVC 里, model 里有两个主角: VO(Value Object) 和 Proxy(代理).
- * VO 是数据的结构, 存储数据的容器. 一条留言(TopicVO)就有ID值(id), 留言时间(addTime), 昵称(username), 内容(content)
- * Proxy 是负责获得数据的. 获得数据的方式有很多种, 可以获得本地数据(swf本身里的数据), 也可以从互联网上获得数据, 当然从服务器上获得数据也是常发生的事情.
- * 而从非本地获得数据可以是: http, remote...
- *
- * View:
- * view => 显示! 顾名思义, view 就是显示的东西. 一切要显示的东西都在这里了. 通常, 它也会有两个主角: UI 和 Mediator(中介器)
- * UI 就是那些要显示的东西, 例如一个显示留言的界面(就是一个component), 一个填写留言的界面(也是一个component)
- * Mediator 最最最重要的任务是处理有关 UI 的逻辑. 比如说更新 UI 上显示的数据, 或者是提交数据, 又或者是验证用户输入的数据
- *
- * Controller:
- * controller => 逻辑! controller, 里头都是一个命令(Command), 一些算法, 一些逻辑就在这里头完成.
- * Model 的 Proxy 获得数据后, 可能需要把这些数据进行一些处理, 那就交由 Controller 里的那些 Command 处理吧.
- * Model 的 Proxy 只负责着获得数据, 具体的数据处理交给 Controller 的 Command 吧
- * 例如: Proxy 获得的数据可能是 变量/值 配对格式的数据, 而我需要的是 xml 格式的, 那就需要实现转化了. 转化过程就交给 command 了.
- *
- * 总的来说:
- * View 用于显示东西给用户看的, 显示的数据由 Model 提供. 有时候 Model 获得的数据不一定就合 View 的胃口,
- * 那么 Model 先把数据交给 Controller 处理好, 处理好后再交给 View 显示出来.
- *
- * 那么, Model 是在什么时候把数据交给 View 层呢? 又是怎样来交给 View 呢? 就是通过发布 "通知" 来实现的, 这个通知携带着数据. 这是 pureMVC 的消息机制.
- *
- * 无论您有没有弄懂以上所说的, 都请您先把它记住!
- *
- * 以下定义了一些通知, 当这个通知发布出去时, 对这个通知感兴趣的 Command 或者 Mediator 会接收这个通知.
- *
- **/
- public static const STARTUP:String = "startup";
- public static const GET_ALL_TOPIC_COMPLETE:String = "getAllTopicComplete"; // ListPanel 对这个通知感兴趣
- public static const SELECT_TOPIC:String = "selectTopic"; // DetailPanel 对这个通知感兴趣
- public static const INSERT_TOPIC_COMPLETE:String = "insertTopicComplete"; // ListPanel 对这个通知感兴趣
- /**
- * 以下这个函数是采用单例模式, 也就是整个 swf 就只有它一个
- **/
- public static function getInstance():ApplicationFacade
- {
- if( instance == null ) instance = new ApplicationFacade();
- return instance as ApplicationFacade;
- }
- /**
- * 下面就是启动整个 pureMVC 的函数
- * sendNotification() 函数用来发布通知的, 这份通知书里装着数据的.
- * 第一个参数是通知书的标题(是一个唯一的标识符), 第二个参数是携带的数据(是MVC三者之间传递的数据).
- **/
- public function startup( app:Object ):void
- {
- sendNotification( STARTUP, app );
- }
- /**
- * 重写这个函数, 您也看到了, 使用 registerCommand() 函数来用注册 command 的.
- * 何谓 "注册 Command" 呢? 就是使 "通知" 跟 command 对应起来.
- * 下面就是把通知名 STARTUP 跟 StartupCommand 对应起来.
- * 在任何时候任何地方, STARTUP 通知被发布了, StarupCommand 就会被执行.
- * 每一个 command 里头都有一个 execute() 函数的, execute() 函数的参数由谁来充当呢?
- * 就是通知所携带的数据, 也就是上面 sendNotification() 函数的第二个参数.
- *
- * 好了, 从 Main.mxml 文件里的 creationComplete="facade.startup( this )" 语句中走到这里了,
- * 现在得从 registerCommand( STARTUP, StartupCommand ) 语句中走到 StartupCommand 里了.
- * 请您按着 Ctrl 键点击一下 StartupCommand.
- **/
- override protected function initializeController():void
- {
- super.initializeController(); // 先调用父类的 initializeController() 方法, 看 pureMVC 源代码可知, 调用这个方法会创建一个单例的 controller, 具体自己看了.
- registerCommand( STARTUP, StartupCommand );
- }
- }
- }
StartupCommand.as
- package cn.riahome.guestbook.puremvc.controller
- {
- import cn.riahome.guestbook.puremvc.model.InsertTopicProxy;
- import cn.riahome.guestbook.puremvc.model.ListTopicProxy;
- import cn.riahome.guestbook.puremvc.view.DetailPanelMediator;
- import cn.riahome.guestbook.puremvc.view.InsertPanelMediator;
- import cn.riahome.guestbook.puremvc.view.ListPanelMediator;
- import org.puremvc.as3.interfaces.ICommand;
- import org.puremvc.as3.interfaces.INotification;
- import org.puremvc.as3.patterns.command.SimpleCommand;
- /**
- *
- * 目前分析途径: Main.mxml -> ApplicationFacade.as -> StartupCommand.as
- * 完整分析途径: Main.mxml -> ApplicationFacade.as -> StartupCommand.as -> ListTopicProxy.as -> ListPanelMediator.as
- *
- * 一旦收到通知 STARTUP 后, 就会执行这个 StartupCommand.
- * 收到的通知里头携带着数据, 这个数据作为参数传递给下面那个 execute() 函数了.
- *
- * 在这里, 主要的功能是注册 Model 里的 Proxy(代理) 和 Controller 里的 Mediator(中介器)
- * 看看 execute() 函数里的代码, 无论是注册 Proxy 还是 Mediator, 都是由 facade 对象的 registerProxy() 方法或 registerMediator() 方法完成的
- * facade 对象是本类内部的一个家伙, 用来管理 Proxy 和 Mediator 的. 在后面的代码里, 想要重新获得 Proxy 或者 Mediator 都可以通过这个 facade 家伙.
- * 是不是对这个 facade 对象感到很奇怪呢!? 如果想知道更多, 就得看 pureMVC 框架的源代码了. 在这里简要说一下:
- * facade 是一个单例对象, 也就是说整个 swf 只有孤独的它一个. 在你写的 Proxy, Mediator 以及 Command 里都会有它的存在. 用它来管理 Proxy 和 Mediator 的.
- * 就像最下面那行代码 ( facade.retrieveProxy( ListTopicProxy.NAME ) as ListTopicProxy ).getAllTopic();
- * 可以通过使用 retrieveProxy() 方法来找回相应的 Proxy, 要找回某个 Proxy, 就要传递那个 Proxy 的名字进去.
- * 类似地, 也有 retrieveMediator() 方法来找回 Mediator.
- * 在 facade 内部是使用数组来存放这些 Proxy 和 Mediator 的. 为什么能找到指定的 Proxy 或 Mediator 呢?
- * 那是因为每一个 Proxy 或 Mediator 都有它自己的一个名字, 那个数组存储 Proxy 或 Mediator 时, 是使用它们自身的名字来作为键(Key)进行存储的.
- * 要找到指定的 Proxy 或 Mediator, 只需知道它的名字就可以了.
- **/
- public class StartupCommand extends SimpleCommand implements ICommand
- {
- /**
- * 这个函数要重写啊.
- **/
- override public function execute(note:INotification):void
- {
- /**
- * 下面两个 Proxy 是用来与服务端通讯的. 它们负责着数据的 获取 或 提交.
- * InsertTopicProxy 是用来向服务端提交数据的, ListTopicProxy 是用来获取数据的.
- *
- * 您可以按着 Ctrl 键点击 ListTopicProxy, 看看它的内部如何
- **/
- facade.registerProxy( new InsertTopicProxy() );
- facade.registerProxy( new ListTopicProxy() );
- /**
- * 记住, Mediator 是负责 UI(就是那些 component)的逻辑部分.
- * 不管是数据验证, 更新 UI 所显示的数据, 还是其它乱七八糟的东西, 都由每块UI(就是每块 Component)所对应的 Mediator 来处理的.
- * 例如: InsertPanelMediator 负责把对应的 InsertPanel(这是一个 component) 里的数据进行检验(对用户输入的数据进行检验正确与否),
- * 检验无误后, 就把数据交给 InsertTopicProxy 写入数据库. 而 UI(一个component) 本身不进行任何的数据处理或逻辑分析等等.
- **/
- var app:Main = note.getBody() as Main;
- facade.registerMediator( new InsertPanelMediator( app.insertPanel ) );
- /**
- * 完整分析途径: Main.mxml -> ApplicationFacade.as -> StartupCommand.as -> ListTopicProxy.as -> ListPanelMediator.as
- * 先去分析 ListTopicProxy.as 再来看以下那个 ListPanelMediator.
- **/
- facade.registerMediator( new ListPanelMediator( app.listPanel ) );
- facade.registerMediator( new DetailPanelMediator( app.detailPanel ) );
- /**
- * 有些数据是在一开始的时候就需要的, 所以在这里就命令 ListTopicProxy 调用它的方法 getAllTopic() 来取得服务端上的数据.
- * 通过 facade 对象的 retrieveProxy() 方法来找回指定的 Proxy.
- * 在这里找回了 ListTopicProxy, 因为我把这个 ListTopicProxy 的名字作为参数传递进 retrieveProxy() 方法了.
- *
- * 以下代码可以拆分为:
- * var proxy:ListTopicProxy = facade.retrieveProxy( ListTopicProxy.NAME ) as ListTopicProxy;
- * proxy.getAllTopic();
- **/
- ( facade.retrieveProxy( ListTopicProxy.NAME ) as ListTopicProxy ).getAllTopic();
- }
- }
- }
ListTopicProxy.as
- package cn.riahome.guestbook.puremvc.model
- {
- import cn.riahome.guestbook.puremvc.ApplicationFacade;
- import cn.riahome.guestbook.puremvc.model.vo.TopicVO;
- import flash.net.URLVariables;
- import mx.collections.ArrayCollection;
- import mx.controls.Alert;
- import mx.messaging.messages.HTTPRequestMessage;
- import mx.rpc.events.FaultEvent;
- import mx.rpc.events.ResultEvent;
- import mx.rpc.http.HTTPService;
- import org.puremvc.as3.interfaces.IProxy;
- import org.puremvc.as3.patterns.proxy.Proxy;
- public class ListTopicProxy extends Proxy implements IProxy
- {
- /**
- *
- * 目前分析途径: Main.mxml -> ApplicationFacade.as -> StartupCommand.as -> ListTopicProxy.as
- * 完整分析途径: Main.mxml -> ApplicationFacade.as -> StartupCommand.as -> ListTopicProxy.as -> ListPanelMediator.as
- *
- * 为这个 Proxy(这里为ListTopicProxy) 起一个名字. 这个名字是唯一的, 且应该定义为静态的常量.
- **/
- public static const NAME:String = "ListTopicProxy";
- private var httpService:HTTPService;
- public function ListTopicProxy()
- {
- /**
- * 每一个 Proxy 都有一个 data 属性, 用来存储需要传递的数据, 就是用来携带数据的.
- * 这里必须先调用父类的构造函数, 把自己的名字和携带的 数据/数据类型 传进父类的构造函数
- * 下面传递的第二个参数是一个 ArrayCollection, 当执行 super() 后, data(Object类型)属性就会变为一个 ArrayCollection.
- **/
- super(NAME, new ArrayCollection() );
- /**
- * 在每个 Proxy 里面, 一定要做的事情就是上面的两项: 为自己起一个名字 以及 先调用父类的构造函数
- * 然后具体要做的事情, 就根据需要而定了.
- * 这里要做的事是: 从服务端获取留言数据, 当获取数据完成后, 就发布一个通知来告诉大家:"我完成了向服务端获取留言数据了,我拥有这些数据".
- * 您看看下面的 onResult() 函数, 该函数里有一条语句: sendNotification( ApplicationFacade.GET_ALL_TOPIC_COMPLETE, data ).
- * 第一个参数就是通知消息, 以静态常量的方式在 ApplicationFacade 类里定义了.
- **/
- httpService = new HTTPService();
- httpService.method = mx.messaging.messages.HTTPRequestMessage.GET_METHOD;
- httpService.resultFormat = HTTPService.RESULT_FORMAT_XML;
- httpService.url = "php/listTopic.php"; // 这里的 url 根据您的虚拟目录不同而不同
- httpService.addEventListener( ResultEvent.RESULT, onResult );
- httpService.addEventListener( FaultEvent.FAULT, onFault);
- }
- private function onResult( event:ResultEvent ):void
- {
- var arr:ArrayCollection = new ArrayCollection();
- var result:XMLList = XML( event.result ).children();
- for( var i:uint = 0; i < result.children().length(); i++)
- {
- var o:TopicVO = new TopicVO( result[i].@id, result[i].@addTime, result[i].@username, result[i] );
- arr.addItem( o );
- }
- data = arr;
- /**
- * 通知: 各单位注意, 我完成了向服务端获取留言数据了,我拥有这些数据, 有意者请接收通知!
- * 在这个例子里, 希望得到这些数据的当然是 ListPanel.mxml 了.
- * 但, 之前已经说过, 取得数据这类的工作并不用 UI(也说是component) 来完成, 这些工作由 UI 的中介器(Mediator)来完成.
- * 在这里, ListPanel.mxml 的中介器是 ListPanelMediator.
- * 现在, 请您打开 ListPanelMediator.as 文件, 或者在 "分析途经" 路线上退回上一级(也就是 StartupCommand.as 文件),
- * 然后找到 facade.registerMediator( new ListPanelMediator( app.listPanel ) ) 这一句,
- * 按着 Ctrl 键点击 ListPanelMediator, 进入去看看源码.
- *
- * 目前 分析途经: Main.mxml -> ApplicationFacade.as -> StartupCommand.as -> ListTopicProxy.as
- *
- **/
- sendNotification( ApplicationFacade.GET_ALL_TOPIC_COMPLETE, data );
- }
- private function onFault( event:FaultEvent ):void
- {
- Alert.show( event.message.toString(), "提示");
- }
- public function getAllTopic():void
- {
- httpService.send( new URLVariables("ran="+Math.random()) ); // 给一个随机参数, 避免缓存
- }
- }
- }
ListPanelMediator.as
- package cn.riahome.guestbook.puremvc.view
- {
- import cn.riahome.guestbook.puremvc.ApplicationFacade;
- import cn.riahome.guestbook.puremvc.model.ListTopicProxy;
- import cn.riahome.guestbook.puremvc.view.UI.ListPanel;
- import mx.collections.ArrayCollection;
- import mx.events.ListEvent;
- import org.puremvc.as3.interfaces.IMediator;
- import org.puremvc.as3.interfaces.INotification;
- import org.puremvc.as3.patterns.mediator.Mediator;
- public class ListPanelMediator extends Mediator implements IMediator
- {
- /**
- *
- * 目前分析途经: Main.mxml -> ApplicationFacade.as -> StartupCommand.as -> ListTopicProxy.as -> ListPanelMediator.as
- * 完整分析途径: Main.mxml -> ApplicationFacade.as -> StartupCommand.as -> ListTopicProxy.as -> ListPanelMediator.as
- *
- * 以下是给自己一个唯一的名字, 通过 facade 对象的 registerMediator() 方法找回自己时, 就得用这个唯一的名字了.
- * **/
- public static const NAME:String = "ListPanelMediator";
- public function ListPanelMediator( viewComponent:Object )
- {
- /**
- * 调用父类的构造函数, 第一个参数传入自己的名字, 上面只是起了名字, 还没写入户口薄或身份证哩, 调用 super() 后才算是写入户口薄.
- * 如果您是按着 "分析途经" 看过来的话, 那您就应该在 StartupCommand.as 里看过, 应该知道每个 UI(component) 都有一个对应的 Mediator.
- * 在这里, 这个 ListPanelMediator 就是为那个 ListPanel.mxml 服务的. super() 函数的第二个参数就是指明为哪个 UI(component) 服务.
- * 那么参数 viewComponent 是在哪里开始传入的呢? 大家回顾 StartupCommand.as 文件, 在注册 ListPanelMediator 时就已经传入了 app.listPanel.
- * app 是 主文件(Main.mxml)的引用, listPanel 是 app 里的一个 UI(component).
- * 也就是说, 从一开始在 StartupCommand.as 里注册自己(ListPanelMediator)时, 就已经指明了自己是为哪个 UI(component) 服务的.
- *
- * 看 "分析途经", 上一级的 ListTopicProxy.as 成功获取服务端的数据后, ListPanel.mxml 急切地想得到那些数据,
- * 但 取得数据 这项工作是由它的中介器(就是本类 ListPanelMediator)来完成的.
- *
- * 上一级的 ListTopicProxy.as 成功获取服务端的数据后, 发布通知, 说已经成功获取数据了.
- * 然后到了这里, 在这里是怎样接收那个通知的呢? 又是怎样为对应的 UI(component) 完成上述工作的呢?
- * 请看下面的 listNotificationInterests()函数 和 handleNotification()函数. 第一个实现如何接收通知, 第二个实现相应工作.
- **/
- super( NAME, viewComponent );
- listPanel.dataGrid.addEventListener( ListEvent.ITEM_CLICK, onSelect );
- }
- // 通知: 各单位注意, 用户现已选择了一条留言, 需要接收通知的单位请马上接收.
- private function onSelect( event:ListEvent ):void
- {
- sendNotification( ApplicationFacade.SELECT_TOPIC, listPanel.dataGrid.selectedItem );
- }
- // 获得对应的 UI(component)
- public function get listPanel():ListPanel
- {
- return viewComponent as ListPanel;
- }
- /**
- * 在下面的函数里列出了所以感兴趣的通知, 包括: GET_ALL_TOPIC_COMPLETE 和 INSERT_TOPIC_COMPLETE.
- * 上一级的 ListTopicProxy.as 成功获取服务端的数据后, 发布这个通知: GET_ALL_TOPIC_COMPLETE.
- * 因为本类对这个通知感兴趣, 所以当通知(GET_ALL_TOPIC_COMPLETE)一旦被发布了(不管发布是谁), 本类马上接收了该通知,同时取得该通知所携带的数据.
- * 接收到通知后, 就进入最下面的 handleNotification() 函数了.
- *
- * (只要把感兴趣的通知放入下面函数的数组里, pureMVC 内部就会把通知从发布者送到接收者手上了. 有兴趣可以打开 pureMVC 的源代码看看内部机制)
- **/
- override public function listNotificationInterests():Array
- {
- return [
- ApplicationFacade.GET_ALL_TOPIC_COMPLETE,
- ApplicationFacade.INSERT_TOPIC_COMPLETE
- ];
- }
- /**
- * 以下函数的参数 note 就是那个通知了. 姑且把那个通知叫做通知书, 所以在这个通知书里有两个重要信息: 通知书名称(通知类型) 和 通知书里的内容(通知书所携带的数据).
- * 如何分别取得通知(note)的类型和它所携带的数据呢? 可以这样:
- * 通知类型: note.getName()
- * 通知携带的数据: note.getBody()
- *
- * 以下函数就是根据接收到的通知类型而采取不同措施. 比如说, 接收到 ListTopicProxy 发布的 GET_ALL_TOPIC_COMPLETE 通知, 于是执行 switch 语句的第一个分支.
- * switch 语句的第一个分支的功能是: 更新对应的 UI(这里是 listPanel, 一个component) 里的数据.
- * 在 listPanel 里先定义好一个 topicData 变量, 给中介器操作.
- * 相当于在 UI 里提供一个接口给中介器操作, 这样就把 表现层(UI) 跟 逻辑层(Mediator) 分开了, 伟人说这是 松偶合 !
- *
- * 如何取得对应的 UI(component) 呢? 就在上面的那个 getter 函数取得: public function get listPanel():ListPanel{}
- *
- * 到此为止, 一个完整的 pureMVC 工作原理在 "分析途径" 上算是表现出来了.
- * 来到这里, 如果你通过本例子弄懂了 pureMVC 工作原理, 那恭喜您! 也恭喜我! 您是聪明的, 我也是聪明的:)
- *
- * 欢迎一起交流技术:
- * My Blog : riaoo.com
- * My Email: y_boy@126.com & riahome.cn@gmail.com
- **/
- override public function handleNotification(note:INotification):void
- {
- switch( note.getName() )
- {
- case ApplicationFacade.GET_ALL_TOPIC_COMPLETE:
- listPanel.topicData = note.getBody() as ArrayCollection;
- break;
- case ApplicationFacade.INSERT_TOPIC_COMPLETE:
- ( facade.retrieveProxy( ListTopicProxy.NAME ) as ListTopicProxy ).getAllTopic();
- break;
- }
- }
- }
- }
源文件下载:PureMVC_GuestBook.zip