Hello PureMVC!
第一次聽到 PureMVC 的時候大概是兩年前,那時候就對這套 MVC Framework 很感興趣,不過當時對於它的架構是有看沒有懂,就一直拖到了現在。
最近因為工作需要用到的關係,所以又再次學習它,經過閱覽無數前輩分享的文章之後,終於初步了解它的運作模式了,因此分享這邊學習心得筆記。
什麼是 MVC ?
一個設計方法(Design pattern 也稱設計模式)。
想初步了解的朋友可以參考維基百科的說明。
網路也有許多相關文章,大略內容就是將程式開發架構分為 Model、View 和 Controller 三個部份的開發方式,建議可以閱讀深入淺出設計模式這本書。
什麼是 PureMVC ?
一個跨多語言的 MVC Framework,其中我們要用到的是 Actionscript 3.0 版本,這是他們的官方網站。
下圖是他的架構圖,除了 MVC 外,它也用到不少其他 design pattern,第一次看可能會霧煞煞,沒關係,我們下面慢慢介紹。
Facade 可以看做是 MVC 之間的共同溝通管道,可以減少程式之間的耦合。
Command 就是扮演 Controller 的角色,PureMVC 寫出來的程式通常都會是以傳送(send)通知(Notification)的方式在進行動作,而 Command 就是負責執行它收到的通知所需要做的事情。
Proxy 從字面上看就是 Model 的代理者,不管是 C 還是 V 要取得使用資料,都必須經過它,這樣的好處是增加後端資料的可抽換性。
Mediator 可以看做是統一視覺元件的介面,透過它更新或取得視覺元件的狀態。
OK,大致上說明了 PureMVC 的概念之後,我們開始撰寫第一個程式吧,我參考了邦邦的部落格-初探pureMVC這篇文章,淺而易懂,是一個簡單的搜尋小程式,大致架構如下圖 :
範例執行結果 :
配置 PureMVC 環境 :
這裡我們使用 FlashDevelop 這套好用的免費軟體來開發,下載FlashDevelop。
接下來到 PureMVC 的網站,下載 PureMVC 的 library。
下載完可以看到 bin 資料夾裡面有個 swc 檔,待會要用的到它。
建立 Project :
安裝並開啟FlashDevelop,開啟新專案。
這裡我們選擇 AS3 Project 來開發。
專案建立完後的架構如下,之後新增的 as 檔都放在 src 資料夾下。
將剛剛下載的 swc 檔放到專案的 lib 資料夾底下。
然後右鍵加入 Library,這布記得要做唷,不然無法使用 PureMVC 的 API。
開始 Coding :
這個程式會有以下四種 Notification (通知) :
- STARTUP - 程式啟動
- INITED - 程式初始化結束
- SEARCH - 搜尋資料
- RESULT - 搜尋結果
還有三個視覺元件 :
- searchTxt - 搜尋字串的輸入欄位
- searchBtn - 送出搜尋字串的按鈕
- resultTxt - 顯示搜尋結果的欄位
一開始我們先寫把 Model 的部分,這個 class 主要負責提供搜尋所需的資料,為了簡化學習,我們只回傳一字串 " XXX Hello PureMVC! "。
DataProxy.as :
package puremvc.model
{
import org.puremvc.as3.interfaces.IProxy;
import org.puremvc.as3.patterns.proxy.Proxy;
import puremvc.ApplicationFacade;
/**
* DataProxy
*
* 儲存資料的 Model
*/
public class DataProxy extends Proxy implements IProxy
{
static public const PROXY_NAME:String = "DataProxy";
public function DataProxy()
{
super(PROXY_NAME, "");
}
public function search(query:String):void {
// 提供外部使用的搜尋 Method.
// 此方法並未真正實作任何搜尋程式.
// 暫時以 query + " Hello PureMVC!" 作為搜尋結果.
var result:String = query + " Hello PureMVC!";
var body:Object = { };
body["result"] = result;
// 將結果包裝成 body["result"] 後傳送 NOTIFICATION_RESULT 通知
this.facade.sendNotification(ApplicationFacade.NOTIFICATION_RESULT, body);
}
}
}
再來我們寫 View 的部分,MainMediator 主要負責接收 INITED 跟 RESULT 這兩個通知。
INITED 代表程式初始化結束,當初始化結束的通知發出後,MainMediator 就會需要偵聽事件的視覺元件加入偵聽。
RESULT 代表搜尋結果,當這個通知發出後,MainMediator 會將搜尋結果顯示到 resultTxt 這個 TextField元件上。
MainMediator.as :
package puremvc.view
{
import flash.events.MouseEvent;
import org.puremvc.as3.interfaces.IMediator;
import org.puremvc.as3.interfaces.INotification;
import org.puremvc.as3.patterns.mediator.Mediator;
import puremvc.ApplicationFacade;
/**
* MainMediator
*
* 主程式視覺介面
*/
public class MainMediator extends Mediator implements IMediator
{
static private const MEDIATOR_NAME:String = "MainMediator";
public function MainMediator(viewComponent:Object = null)
{
super(MEDIATOR_NAME, viewComponent);
}
override public function listNotificationInterests():Array {
// 此 Mediator 關注哪幾個 NOTIFICATION
return [
ApplicationFacade.NOTIFICATION_INITED,
ApplicationFacade.NOTIFICATION_RESULT
];
}
override public function handleNotification(notification:INotification):void {
// 當各 NOTIFICATION 產生通知時的動作
switch(notification.getName())
{
case ApplicationFacade.NOTIFICATION_INITED:
onInitedNotify(notification);
break;
case ApplicationFacade.NOTIFICATION_RESULT:
onResultNotify(notification);
break;
}
}
private function onInitedNotify(notification:INotification):void {
// 當主程式初始化結束的動作
setListener(true);
}
private function get app():Main {
return Main(this.viewComponent);
}
private function onResultNotify(notification:INotification):void {
// 將取得的 body["result"] 值顯示在 resultTxt 上
app.resultTxt.text = notification.getBody()["result"];
}
private function setListener(boolean:Boolean = false):void {
// 設定是否偵聽 searchBtn Click 事件
if (boolean)
{
app.searchBtn.addEventListener(MouseEvent.CLICK, onSearchBtnClick);
}else {
app.searchBtn.removeEventListener(MouseEvent.CLICK, onSearchBtnClick);
}
}
private function onSearchBtnClick(e:MouseEvent):void {
// 將 searchTxt 的值包裝成 body["keyword"] 後發送 NOTIFICATION_SEARCH 的通知
var body:Object = { };
body["keyword"] = app.searchTxt.text;
this.facade.sendNotification(ApplicationFacade.NOTIFICATION_SEARCH, body);
}
}
}
接下來我們開始寫需要用到的 Command,這裡我們先寫負責處理 SEARCH 通知的 SearchCommand。
SearchCommand.as :
package puremvc.controller
{
import org.puremvc.as3.interfaces.ICommand;
import org.puremvc.as3.interfaces.INotification;
import org.puremvc.as3.patterns.command.SimpleCommand;
import puremvc.model.DataProxy;
/**
* SearchCommand
*
* 接到搜尋通知後要做的事.
*/
public class SearchCommand extends SimpleCommand implements ICommand
{
public function SearchCommand()
{
super();
}
override public function execute(notification:INotification):void {
// 向 MainMediator 取得 body["keyword"] 對映的元件的值
var query:String = notification.getBody()["keyword"];
search(query);
}
private function search(query:String):void {
// 將要搜尋的值傳給 DataProxy 提供的 search Method
var dataProxy:DataProxy = DataProxy(this.facade.retrieveProxy(DataProxy.PROXY_NAME));
dataProxy.search(query);
}
}
}
再來是程式啟動 STARTUP 的 StartupCommand。
StartupCommand.as :
package puremvc.controller
{
import org.puremvc.as3.interfaces.ICommand;
import org.puremvc.as3.interfaces.INotification;
import org.puremvc.as3.patterns.command.SimpleCommand;
import puremvc.ApplicationFacade;
import puremvc.model.DataProxy;
import puremvc.view.MainMediator;
/**
* StartupCommand
*
* 程式啟動時需要做的事情
*/
public class StartupCommand extends SimpleCommand implements ICommand
{
public function StartupCommand()
{
super();
}
override public function execute(notification:INotification):void {
// 註冊 Model DataProxy
this.facade.registerProxy(new DataProxy());
// 註冊 View MainMediator
var app:Main = Main(notification.getBody());
this.facade.registerMediator(new MainMediator(app));
// 註冊接下來會用到的 Controller Command
this.facade.registerCommand(ApplicationFacade.NOTIFICATION_SEARCH, SearchCommand);
// 通知初始化結束
this.facade.sendNotification(ApplicationFacade.NOTIFICATION_INITED);
}
}
}
快結束了! 再來把程式的核心,ApplicationFacade 給完成就差不多了。
ApplicationFacade.as :
package puremvc
{
import org.puremvc.as3.interfaces.IFacade;
import org.puremvc.as3.patterns.facade.Facade;
import puremvc.controller.StartupCommand;
/**
* ApplicationFacade
*
* 所有程式之間唯一的溝通橋樑.
* 使用到 Facade 和 Singleton 設計模式.
*/
public class ApplicationFacade extends Facade implements IFacade
{
// 所有此程式會用到的列舉狀態
// - NOTIFICATION_STARTUP 程式啟動
// - NOTIFICATION_INITED 程式初始化結束
// - NOTIFICATION_SEARCH 發出搜尋的請求
// - NOTIFICATION_RESULT 產生結果的通知
static public const NOTIFICATION_STARTUP:String = "NOTIFICATION_STARTUP";
static public const NOTIFICATION_INITED:String = "NOTIFICATION_INITED";
static public const NOTIFICATION_SEARCH:String = "NOTIFICATION_SEARCH";
static public const NOTIFICATION_RESULT:String = "NOTIFICATION_RESULT";
static public function getInstance():ApplicationFacade {
if (instance == null) instance = new ApplicationFacade();
return ApplicationFacade(instance);
}
override protected function initializeController():void {
super.initializeController();
// 當 ApplicationFacade 被第一次呼叫的時候
// 會執行這個 initializeController()
// 所以在這裡我們註冊 NOTIFICATION_STARTUP 這個啟動 Command
this.registerCommand(NOTIFICATION_STARTUP, StartupCommand);
}
/**
* startup
*
* 程式啟動.
*
* @param app 主程式
*/
public function startup(app:Main):void {
this.sendNotification(NOTIFICATION_STARTUP, app);
}
}
}
最後,將用到的視覺元件加一加,然後寫上一行 ApplicationFacade.getInstance().startup(this)
就可以收工囉!!!
Main.as :
package
{
import flash.display.SimpleButton;
import flash.display.Sprite;
import flash.text.TextField;
import flash.text.TextFieldType;
import flash.events.Event;
import puremvc.ApplicationFacade;
/**
* Main
*
* 使用 PureMVC 來實作一個簡單的搜尋小程式.
*/
public class Main extends Sprite
{
public function Main():void
{
if (stage) init();
else addEventListener(Event.ADDED_TO_STAGE, init);
}
private function init(e:Event = null):void
{
removeEventListener(Event.ADDED_TO_STAGE, init);
// entry point
initComponents();
// pureMVC 的進入點.
// 使用唯一的 ApplicationFacade class 作為入口.
// 之後所有的元件之間溝通都需要經過此 facade.
ApplicationFacade.getInstance().startup(this);
}
public var searchTxt:TextField;
public var searchBtn:SimpleButton;
public var resultTxt:TextField;
private function initComponents():void {
// 初始化視覺元件(ViewCompoents)
// searchTxt
searchTxt = new TextField();
searchTxt.type = TextFieldType.INPUT;
searchTxt.border = true;
searchTxt.x = 50;
searchTxt.y = 50;
searchTxt.width = 200;
searchTxt.height = 20;
this.addChild(searchTxt);
// searchBtn
var buttonStyle:Sprite = new Sprite();
buttonStyle.graphics.beginFill(0xcccccc);
buttonStyle.graphics.drawRect(0, 0, 100, 20);
buttonStyle.graphics.endFill();
var buttonTxt:TextField = new TextField();
buttonTxt.text = "Search Button";
buttonTxt.width = 100;
buttonTxt.height = 20;
buttonStyle.addChild(buttonTxt);
searchBtn = new SimpleButton(buttonStyle, buttonStyle, buttonStyle, buttonStyle);
searchBtn.x = 260;
searchBtn.y = 50;
this.addChild(searchBtn);
// resultTxt
resultTxt = new TextField();
resultTxt.type = TextFieldType.DYNAMIC;
resultTxt.border = true;
resultTxt.x = 50;
resultTxt.y = 80;
resultTxt.width = 200;
resultTxt.height = 200;
this.addChild(resultTxt);
}
}
}
以上是我初步學習 PureMVC 後的心得,更進階一點的介紹可以參考官方的 Best Practice 或更多相關文章。
範例原始檔:
參考文章: