Laya 中使用 PureMVC

参考
PureMVC(AS3)剖析
我的FLASH情结2010 - 浅谈网页游戏与创业
菜鸟学PureMVC记
PureMVC(JS版)源码解析:总结
Flash务实主义(四)——Flash中的MVC

一、痛点

参考
android mvc mvp mvvm
PureMVC--一款多平台MVC框架

1.传统MVC的痛点

  • Controller:控制器,包含了项目的业务逻辑。但是也是被大家吐槽最多的一个,原因就是很多人,或者说大多数人,习惯于什么都往Controller里写,最后一个Controller超过1000行代码是司空见惯的事。所以关于传统MVC的第一个痛点就是,Controller过于臃肿。

  • Model:模型,包含了项目的数据模型。MVC定义之初,Model是核心,旨在使得同一个Model可以被复用到多个项目或者被复用到同一个项目的不同模块之中。但是在实际项目中,Model还承载着纯Model层内部的运算的工作,但是运算部分会项目的不同而有所区别,因此与项目的适配反而成为了Model可复用的枷锁。所以关于传统MVC的第二个痛点就是,Model变得不可复用。

  • View:视图,包含了项目所有的UI组件。视图本身没有什么好被大家诟病的,但是由于MVC中对于View和Controller界限的模糊界定造成了使用者在写代码的时候会觉得这部分代码放在View或者Controller里都可以的情况。例如事件的处理,组件的组合等。所以关于传统MVC的第三个痛点就是,View概念的模糊。

2.PureMVC到底如何解决了传统MVC的三个痛点?

  • Controller将操作逻辑细化为Command
    根据PureMVC的最佳实践,Controller实体不需要单独实现,且Controller内部将每一个操作分割为一个个Command,这从根本上解决了Controller越来越臃肿的问题,强制用户将Controller里每一个操作细粒度化,使得代码可读性更强,维护性更高。

  • Proxy负责域逻辑,DataObject负责数据模型
    PureMVC中,与域相关的逻辑和接口由Proxy来负责,后续的添加和修改接口只在Proxy中完成。而DataObject是完全对业务进行数据建模而产生的数据模型,与业务没有丝毫的关系,因此也保证了高可移植性。

  • ViewComponent只关注UI,其余的交给Mediator
    PureMVC规定了ViewComponent只负责UI的绘制,而其他事情,包括事件的绑定统统交给Mediator来做。这也就避免了ViewComponent内部代码定义模糊,更不会和Controller的代码进行混淆。

二、使用细节
PureMVC 工作流程图

1.Command需要侦听通知,使用registerCommand注册事件侦听
根据《pureMVC最佳实践》中的建议,我的做法是这样的,尽量使用Command,让Command成为Mediator与Proxy之间通讯的唯一桥梁,Mediator和Proxy中发出的Notification,接收者一定是某个Command,然后再由Command处理并将结果转发给真正的消息接收者,Command就算仅仅起一个转发作用,仅仅有不到10行代码,也要创建一个Command类。这样不仅使你的架构更加清晰,而且也更符合MVC思想,Command类的大量存在还使你架构的业务逻辑具有了更好的封装性和扩展性,可谓是一箭三雕,何乐而不为?唯一的负面影响可能是你需要创建和维护更多的Command类文件,但相对于优势而言,这点影响不算啥。

参考PureMVC(JS版)源码解析(六):MacroCommand类
在实际开发过程中,我们需要写一些复杂的逻辑处理单元(Command类),这写逻辑处理类要么继承SimpleCommand,要么继承MacroCommand类,哪什么时候继承MacroCommand类,什么时候继承SimpleCommand类,需要看我们逻辑的复杂度,如果一个逻辑单元可以拆分为多个子逻辑单元,那我们可以继承MacroCommand类,如果一个逻辑单元就可以处理,那我们只需要继承SimpleCommand类。继承MacroCommand类的子类需要重写initializeMacroCommand方法,不需要重写execute方法,继承SimpleCommand类的子类需要重写execute方法。

参考PureMVC(JS版)源码解析(十):Controller类
executeCommand()方法告诉我们, Command对象(SimpleCommand或者MacroCommand)是无状态的;只有在需要的时候(Controller收到相应的Notification)才会被创建,并且被执行(调用execute方法)。

2.xxxCanvasMediator需要侦听通知,需要在对应Mediator中使用listNotificationInterests注册,并重写handleNotification处理。

3.Proxy 集中程序的Domain Logic(域逻辑),并对外公布操作数据对象的API。它封装了所有对数据模型的操作,不管数据是客户端还是服务器端的。

模型部分在实际开发中除了存储数据,还有其他作用么?是的,其实它的实际职责非常多。它要给Command和Mediator提供接口,响应用户操作,进行数据操作或者请求远程数据服务,进行数据的序列化和反序列化,得到异步数据后可能还要检查数据合法化。但不管怎么样,它始终是在和数据打交道,同时也应该是你的主程序中唯一可以直接和数据打交道的管道,别的部分要想和数据有接触,首先要问问它同意不同意。模型处理完数据会以Notification的消息方式通知Command或者Mediator。但绝对不能在Proxy中直接调用Mediator,这是为了保证数据层的独立性、可移植性和重用性,也简化了你的架构思想。
参考PureMVC(JS版)源码解析(八):Proxy类
另外,和Mediator类一样,Proxy类也要在facade中注册,它也有onRegister()/onRemove()方法,这里就不在陈述,具体用法和Mediator类的onRegister()/onRemove()用法一样。
总结一下:Proxy类的结构很简单,我们只需要记住两点,一是它有一个data属性,可以用来存储它所管理的data model,二是它可以发送消息,不能接受消息。

4.应该避免Mediator与Proxy 直接交互。例子项目中遵从了这个规则,但实际上项目Mediator中不可避免需要获取Proxy数据,如果每次都通过一个Notification去获取数据,然后返回数据给Mediator,这样无形中增加了通信次数、带反馈数据的通信加重通信负担。所以可以适当是的在Mediator中facade.retrieveProxy获取Proxy然后拿到数据,而且从proxy直接拿数据,可以保证拿到最新数据。

5.永远不要把 Event 的名称定义在 Façade 类里。应该把 Event 名称常量定
义在那些发送事件的地方,或者就定义在 Event 类里。

6.参考Pure MVC使用的几点心得(模式和使用误区)
注册mediator/command/proxy的时机不合适和没有及时注销。当初所有command/proxy的注册我都是放在应用初始化的时候做,就是无论用户是否真的用到这些功能,都已经注册(占用客户端的内存)了。另外比较严重的一点是,根本没有注销!那样会怎样,客户端的内存会一直占用,直到退出浏览器,后果可想而知。正确的做法当然是有需求才去满足需求啦。当用户点击某个button,menu,下拉菜单时,才去注册mediator/command/proxy,用完之后及时注销。当然,如果有些功能模块是经常用到的,可以常驻内存,这样会提高一点效率。

7.参考Flash务实主义(四)——Flash中的MVC
是Mediator知道View的一切,View完全不知道Mediator,而不是相反
对于使用pureMVC的同僚们,我真是不明白你们到底是怎么把这个反过来理解成“View知道Mediator的一切,而Mediator完全不知道View”的,因为官方实例上写的很明白。估计是把mediator当成通信专用的模块类了吧。但是如果你放弃了mediator分离View代码的特性,只是用来通信的话,至少要保留原来的通信功能,就是让Mediator依然可以直接访问View。否则既然Mediator是用来通信的,它却不能操作View,结果还得设法和View再通信一次……

8.参考PureMVC的十个小提示 原作者杜增强
在某些情况下你不再使用一个Mediator和它的View Components. 你应该用facade.removeMediator(MyMediator.NAME)去掉这个Mediator同时用 destroy()来去掉包含所有listeners,timer,references的ViewComponent.以便更好的进行 垃圾回收.

9.参考PureMVC闲谈(五):pureMVC的QA

Q:Mediator可以handleNotification,Command也可以和notification关联,到底怎么来决定用谁处理呢?
A:还是回到纯·摸鱼吃,什么时候要军队自己去接通知,什么时候需要将军来接通知执行呢?通常情况下,“内部的小事”就由军队自己处理,比如骑兵一队的马饿了,发出一个通知“劳资饿了”,那么骑兵一队自己接这个通知然后去喂马就好了,这样做的好处就是保证了主要逻辑的简洁,不用劳烦将军再去操心这些小事。那么“关系到集体的大事”就得由将军去处理了,比如通知需要哪几只部队连夜突袭,发一个通知让将军来调度更加安全保险,这样做另外的好处就是重用性好 ,否则每个军队都得去接这个通知,然后看看是不是通知了自己,万一有些军队忘了接这个通知,打仗输了可就惨咯!(简单的逻辑交给Mediator处理)
Q:在registerMediator的时候,viewComponent怎么写?
A:写它要管理的视图对象
Q:我怎么看见有人写的 viewComponent是main,而不是管理的视图对象?
A:说明它要在内部创建视图对象,然后添加到main上面,所以传入了main。这样做就完全把那个视图对象 给保护了,还是那句话,这是很灵活的。
心得:
1.一个Command也可以关心多个通知,然后用一个Switch来判断处理。也可以给通知加上Type属性,在处理时进行分别处理。
2.多思考结构,积极重构,这会让你的程序后期维护和改进非常容易
3.建立一个AppMediator来管理整个main视图,这样就能提供他的viewComponent来获取main
4.建一个PublicNotification类来提供常用的通知名称
5.用onRemove()函数,及时清除对象释放内存
6.灵活处理,教科书只是参考,按你自己的方法也许更快更方便。
7.不要为了MVC而MVC,如果一个东西要彻底解耦是不太现实的,一些不会扩展的小东西完全可以怎么快怎么来。

10.参考总结PureMVC中Mediator,Command,Proxy的职责

image.png

11.参考PureMVC(AS3)剖析:吐槽
为了模块间解耦,Notification发布者应该不关心谁对这个消息感兴趣(谁来处理),感兴趣者自行注册(Mediator通过listNotificationInterests注册、Command通过facade.registerCommand()注册)。

无法知道Notification的源头。然而这点可以通过在消息体body中,增加字段标识,如:

sendNotification(ApplicationConstants.UPDATE_LEVEL_DATA, 
{ "noticeSource": this, "levelData": m_levelData } );

noticeSource标识消息来源,如果您还想要知道消息传递层次,可以用数组表示,顺序插入传递者。

三、Egret中使用PureMVC

参考
使用egret开发2048
关于使用pureMvc开发2048,出现 Cannot find name 'puremvc'.问题
如何在egret中使用pureMVC

我们只需要他提供的一个js文件和一个.d.ts文件。 然后复制到对应位置。改一下index.html和release.html如果要打包成native还要改一下native_loader.js,具体怎么改你看GitHub上对应的文件 。 这里还有一点没有说清楚的是 pureMVC的作者给的那个js不要用,他多了一点define的东西,我把他去掉了就可以在ts项目里面用了。

引用了第三方类库之后要先 build -e 一次的。

四、Laya 中使用 PureMVC

1.同Egret,把define那部分注释掉。
2.注意,如果下载了multicore版本,写法有一点点区别。大多数语言都支持两个版本的框架,一个是标准版,不支持多模块,另一个是multicore版,支持多模块。参考pureMVC,多核和单核的区别,多核可以有多个Facade的实例,单核只有一个

class AppFacade extends puremvc.Facade {
    constructor() {
        super();
    }
    public static readonly STARTUP: string = "startup";
    public static readonly MOUSE_CLICK: string = "MOUSE_CLICK";

    public static getInstance(): AppFacade {
        if (this.instance == null) this.instance = new AppFacade();
        return <AppFacade>(this.instance);
    }

    public initializeController(): void {
        super.initializeController();
        this.registerCommand(AppFacade.STARTUP, StartupCommand);
        this.registerCommand(AppFacade.MOUSE_CLICK, MouseClickCommand);
    }

    /**
     * 启动PureMVC,在应用程序中调用此方法,并传递应用程序本身的引用
     * @param   rootView    -   PureMVC应用程序的根视图root,包含其它所有的View Componet
     */
    public startUp(rootView: any): void {
        this.sendNotification(AppFacade.STARTUP, rootView);
        this.removeCommand(AppFacade.STARTUP); //PureMVC初始化完成,注销STARUP命令
    }
}

class StartupCommand extends puremvc.SimpleCommand {

    public constructor() {
        super();
    }
    public execute(notification: puremvc.INotification): void {
        this.facade.registerProxy(new MouseClickProxy());
        var stage: Laya.Stage = notification.getBody() as Laya.Stage;
        this.facade.registerMediator(new StageMediator(stage));
    }
}

class MouseClickProxy extends puremvc.Proxy implements puremvc.IProxy {
    public static NAME: string = "MouseClickProxy";
    public constructor() {
        super(MouseClickProxy.NAME);
    }
}

class StageMediator extends puremvc.Mediator implements puremvc.IMediator{
        public static NAME:string = "StageMediator";
        
        public constructor(viewComponent:Laya.Stage){
            super(StageMediator.NAME, viewComponent);
            //注册监听,从而获取UI上的事件
            viewComponent.on(Laya.Event.CLICK, this,this.mouseClick);
        }

        private mouseClick(event:Laya.Event):void
        {
            this.sendNotification(AppFacade.MOUSE_CLICK, event.stageX);
        }
}

class MouseClickCommand extends puremvc.SimpleCommand {

    public constructor() {
        super();
    }

    public execute(notification: puremvc.INotification): void {
        var stageX: number = <number>notification.getBody();
        console.log("click pos:",stageX);
    }
}

// 程序入口
class GameMain{
    constructor()
    {
        Laya.init(600,400);
        AppFacade.getInstance().startUp(Laya.stage);
    }
}
new GameMain();

multicore版本部分代码

    public static BG_NAME:string = "BGH5Video";
    public static readonly STARTUP: string = "startup";
    public static readonly MOUSE_CLICK: string = "MOUSE_CLICK";

    // public static getInstance(): AppFacade {
    //     if (this.instance == null) this.instance = new AppFacade();
    //     return <AppFacade>(this.instance);
    // }

        public static getInstance():AppFacade {
            if( !AppFacade.instanceMap[ AppFacade.BG_NAME ] )
                AppFacade.instanceMap[ AppFacade.BG_NAME ] = new AppFacade( );
            return AppFacade.instanceMap[ AppFacade.BG_NAME ];
        }

class StartupCommand extends puremvc.SimpleCommand {

    public constructor() {
        super();
    }
    public execute(notification: puremvc.INotification): void {
        this.facade().registerProxy(new MouseClickProxy());
        var stage: Laya.Stage = notification.getBody() as Laya.Stage;
        this.facade().registerMediator(new StageMediator(stage));
    }
}
五、puremvc 场景切换

使用 PureMVC 构建游戏项目 Part IV 之场景管理
注意,如果关闭某个窗口,只是把它从舞台移除,但是没有removeMediator时。此mediator通过listNotificationInterests侦听的事件仍然会执行
end

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容