運用 Ext JS 4 的 MVC 架構
Ext JS 4 簡介
Ext JS 4 目前是 Sencha 的產品,4.x 的正式版本號是 4.0.7。Ext JS 4 提供商業版本,但如果您的項目是開源的,則可以免費使用 Ext JS 4。Ext JS 的論壇目前非常活躍;Ext JS 還在不但地升級改進,據 Sencha 官方統計,使用 Ext JS 的開發者數目在一百萬以上。
Ext JS 4 與之前版本的比較
-
渲染效率提高
所有類都經過調優,包括最影響渲染效率的布局引擎重寫。
-
命名空間
命名空間是 Ext JS 4 的 MVC 的基礎,自此 Ext JS 類能按作用域分開存放了。.NET 或 Java 開發者應該熟悉命名空間帶來的好處:命名空間讓全類名映射到類文件路徑變得很容易,將類按作用域分文件夾存放使得類更容易管理。以 MVC 為例,Ext JS 類將按作用域:模型、視圖和控制器分為三類,分別存放于對應文件夾中。
-
按需加載類
清單 1. 按需加載類的例子Ext.define('MyNamespace.Cat', { requires: ['MyNamespace.BabyCat'], giveBirth: function() { // 實例化 BabyCat 之前,必須加載 BabyCat 的類定義。通過設置“requires”屬性,能實現類的按需加載 return new MyNamespace.BabyCat(); } });
這個特性其實是基于全新設計的類系統的,詳見下面的小結。不同于先前版本:即使用到 Ext JS 框架中很少一部分單元,Ext JS 也會加載所有的框架,按需加載只加載需要的類。因此按需加載類為 JS 優化和減少內存消耗提供了一個有效途徑。Sencha 為此還提供了 SDK 工具對 JS 代碼進行 Minify,在部署前運行 minify 對 JS 代碼最小化后,將得到一個最小 JS 集合。 -
全新的類系統 由于篇幅的限制,具體請參閱官方文檔 Class System,其中詳細描述了怎樣用 Ext JS 4 的方式定義類,以及錯誤處理和調試
-
MVC 架構 用 Ext JS 4 之前的版本寫大的客戶端應用,您會發現越來越“難”,您會發現有四難:難寫、難讀、難維護、難擴展。隨著越來越多的功能添加進來,代碼越來越失控,一個 JS 文件幾千行可能很普遍了,當然也不排除代碼組織得很好易于擴展的情況,但這些都需要開發者付出額外的開發代價去組織自己的架構。從 Ext JS 4 開始有了自己的 MVC 架構,開發者不必再付出這種額外的代價也能寫出漂亮的代碼。Ext JS 4 對 MVC 有自己的定義,以下定義來自 Sencha 官網的文檔:
- Model
:一組字段的集合以及它們對應的數據(例如:“User”類 model 有“username”和“password”字段),通過 data 包 (store,proxy 等 )Model 能序列化自己,并能通過關聯關系從一個 Model 導航到另一個 Model。Model 的工作原理類似 Ext JS 3 中的 Record 類,通常結合 Store 為表格控件或其它控件提供顯示數據。
- View
:任意組件,如 Grid, Tree 和 Panel 都是視圖。
- Controllers
:在這里寫所有的邏輯代碼:如渲染視圖、實例化模型、加載并初始化其它控制器等。
- Model
MVC 的概念很簡單,但實際項目中運用 MVC 模式將代碼組織起來會不會沒那么簡單?答案在后面的章節“介紹開發 Ext JS4 的利器 : Sencha Architect 2”中,該章節會詳細介紹怎樣用該工具開發 MVC 模式的 Ext JS 程序。
為什么要運用 MVC 架構
MVC 的概念
MVC 是一種成熟的軟件設計模式。MVC 模式的目的是實現一種動態的程序設計,使后續對程序的修改和擴展簡化,并且使程序某一部分的重復利用成為可能。除此之外,此模式通過對復雜度的簡化,使程 序結構更加直觀。軟件系統通過對自身基本部份分離的同時也賦予了各個基本部分應有的功能。專業人員可以通過自身的專長分組:
- 控制器 Controller - 負責轉發請求,對請求進行處理。
- 視圖 View - 界面設計人員進行圖形界面設計。
- 模型 Model - 程序員編寫程序應有的功能(實現算法等等)、數據庫專家進行數據管理和數據庫設計 ( 可以實現具體的功能 )。
MVC 模式的特點:
- 重用性
Ext JS 4 中,數據模型、視圖組件和存儲組件能重復使用,使用時只需將它們的名稱作為引用添加到特定的 controller 中。
- 職能分離
模型、視圖、控制器以及存儲之間的職能分離,使得每個 JS 文件的職能單一化、最小化。開發人員只需要引用這些必需的職能單元即可構建新的功能。
- 職責清晰
由于每一個 JS 單元文件的職責清晰,不同類型的“職責”被劃分為不同的組件。在集成開發工具中,開發人員很容易利用這些組件構建自己的應用。
- 復雜性
運用 MVC 模式時有一定的復雜性,因為開發者需要付出額外的努力去學習 Ext JS 4 的 MVC 框架,但在大型項目中這個付出是值得的。另外 Sencha 提供了相應的集成開發工具 Sencha Architect 協助基于 MVC 框架的開發,一定程度上減輕了因運用 MVC 模式帶給開發者的壓力。
運用 MVC 以前:
- 代碼組織凌亂
上面的小結中提到 Ext JS 4 之前的代碼通常會碰到四“難”,除此之外,代碼很難管理,由于沒有分離出類似控制器單元,導致“拷貝、粘貼”過多,應用邏輯代碼支離破碎,給調試和測試帶來很多不便。
- 缺乏整體邏輯
由于視圖等“靜態”代碼和事件監聽代碼混雜在一起,代碼功能職責混亂,很多本應作為公用的代碼也一起被混進去,導致可擴展性幾乎全無,因此為新功能編寫新的代碼也變得越來越不容易,修改代碼更是容易出錯。
運用 MVC 以后:
- 代碼層次結構清晰
圖 1. 文件結構
- JavaScript 腳本都按職責存放在不同的 JS 文件中。
- 不同的 JS 文件按 MVC 目錄命名約定"controller"、"store"、"view"、"model"分類存放。
- 整個工程里只有一個 HTML 文件,而且只作為入口文件,里面不寫任何腳本。請看下面入口文件的例子。
清單 2. 入口文件<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>Demo</title> <link rel="stylesheet" type="text/css" /> <script type="text/javascript" src="http://extjs.cachefly.net/ext-4.0.2a/ext-all-debug.js"></script> <link rel="stylesheet" type="text/css" href="style.css"/> <script type="text/javascript" src="demo.js"></script> </head> <body></body> </html>
整個工程僅此一個 HTML 文件 , 供十一行,其中 demo.js 是工程的啟動文件,里面包含了所有 UI widget 的構建,后面會具體介紹。 - 整體邏輯更加條理
入口定義在 Ext.application({...}) 中,這是 Ext JS 4 為開發 MVC 應用新增的函數,我們可以在里面引入初始時所需的 controller、view 或 model,在 launch 函數中編寫初始代碼,和以前在 Ext.onReady() 中一樣。因為 Ext 引擎會在 Ext.application({...}) 函數執行時創建一個全局 application 實例并能在 controller 中被引用得到 (this.application.*),所以我們也可以在 application 中定義一些公共函數,甚至注冊一些事件,方便其它 controller 調用。
"model"文件夾中只有模型相關的類定義,每個模型定義必須包含字段,還能定義字段校驗規則,不同模型之間的關聯,以及數據代理 ( 連接服務器存取數據 )。比如雇員\部門對應兩類模型,體現為兩個類文件"Employee"和"Department"。
"view"文件夾中定義了所有的 widget,每一個 widget 對應一個類文件。view 的代碼屬于靜態代碼,后面將提到怎樣用工具自動生成。
"controller"文件夾中的控制器按管理范圍的不同劃分為不同的類文件,其中每一個控制邏輯都包括初始化、組件事件監聽以及引 用等。比如,導航菜單的控制器會包含對主頁面菜單按鈕的動作監聽”click“,并在該事件中負責創建相應的子頁面;而子頁面對應的控制器只負責該頁面中 的組件的行為,如提交按鈕的點擊等。另外控制器本身能被動態加載,在下文的例子中我們還可以看到,不同的控制器被加載的時機和順序是由用戶行為(如點擊某 按鈕)控制的。其實這個特性是基于依賴于 Ext JS 4 的動態加載類的新特性的。詳見"按需加載類"一節。
由此可見,運用 MVC 后 HTML 里不再直接寫 JS 了,JS 按邏輯、職責分門別類存于不同的目錄,對應到不同的文件中。
開發 Ext JS4 的利器:Sencha Architect
Sencha Architect 是 Sencha 公司出品的一款輔助 ExtJS 開發的商業 IDE 軟件,能幫助 ExtJS 開發人員更加專注于核心 JS 代碼的開發,從而大大減少花費在編寫界面、組織代碼等反復性的工作上的時間。筆者寫作時的工具版本是 2.0.0 Build 412,此工具最大的特點是能幫助用戶管理符合 MVC 模式的代碼。例如,視圖類代碼能通過拖放組件結合屬性設置的方式完全自動生成,不用寫手一行代碼。其它如 Model、Store 和 Controller 的代碼能通過屬性設置,方法、事件設置自動生成。理想情況下,一個熟練的 ExtJS 開發者在使用 Sencha Architect 時,百分之九十以上的時間會花在 controller 的實現和自定義組件(包括 override 一些組件)的開發。這一點也不夸張,因為在 Sencha Architect 中開發界面實在太輕松了。感興趣的朋友可以從官網下載 Sencha Architect 的 30 天試用版嘗嘗鮮。
主要特性有 :
- 自動生成 view 代碼
通過拖放組件的方式生成復雜視圖。
圖 2. 視圖設計
- 代碼模式和設計模式切換
設計好的視圖,能方便切換到代碼模式下預覽,拷貝或導出。
圖 3. 模式切換
- 屬性設置面板
圖 4. 屬性面板
- 項目導航面板
圖 5. 導航面板
下面的示例程序是一個非常實用的集表單提交,表格應用和圖表顯示的綜合運用的例子。由于篇幅限制,本文只列舉主要的 JS 單元,感興趣的朋友請到本文末尾處下載完整的示例程序。
- 入口代碼
本示例使用了默認的 appFolder:app, 實際中用戶可以覆蓋此屬性,使用符合項目要求的路徑名。我在項目中傾向于 appFolder 中的所有類由 Sencha Architect 工具維護,通過配置 mvn,在 compile 時將 appFolder 中生成的類拷貝到 webApp 中。手工維護的 JS 文件放在獨立的命名空間中(稱其為擴展空間吧),并在入口中聲明,這樣能被 application 引用并加載,同時在擴展空間的類也能 require 到 application 對應命名空間中的類,這樣做的好處是,您能將 override 的代碼移出來放到擴展空間中,另外還能放一些項目中用到的插件。
清單 3. 入口代碼/* 動態加載依賴的前提 */ Ext.Loader.setConfig({ enabled: true, paths: { 'Extention': 'js'// 設置一個擴展命名空間,區分工具生成的代碼 } }); Ext.application({ requires: [ 'Extention.RandomGen'// 加載擴展命名空間中的類 ], views: [ 'MyViewport' ], autoCreateViewport: true, name: 'MyApp', controllers: [ // 引用初始頁面時,所需要的最小 controller 集合, // 其它的由主菜單按鈕觸發動態加載 'AppLaunchCtrl' ], /* 以下函數僅示意用戶能在 application 中定義自己的全局函數,具體實現請下載代碼后查看 */ findTab: function(tabPanel, record) {}, activateTab: function(tabPanel, targetTab) {}, widget: function(tabPanel, controllerName, widgetName, record, cfg) {} });
- 主菜單控制器
控制主菜單中三個按鈕的點擊事件,以及它們的狀態:如按下和浮起。
- 表單子頁面控制器
表單提交相關,本示例中,點擊提交按鈕后將彈出一個“Save”提示框。
- 表格子頁面控制器
通過查詢按鈕控制表格的 store 重新加載新數據。
- 圖表子頁面控制器
通過下拉選擇框控制圖表的刷新。
清單 4. 圖表子頁面控制器代碼
Ext.define('MyApp.controller.ChartCtrl', { extend: 'Ext.app.Controller', stores: [ 'MyChartStore', 'LatestMonths' ], refs: [ { ref: 'tabPanel', selector: '#tabPanel' }, { ref: 'latestMon', selector: '#comboLatest' } ], /* 請參看 init 中的事件綁定 */ onComboboxSelect: function(combo, records, options) { console.log('onComboboxSelect'); this.getMyChartStoreStore().load(); return false; }, init: function(application) { // 綁定月份下拉框的 select 事件 this.control({ "#comboLatest": { select: this.onComboboxSelect } }); /* 綁定 store 的 load 事件,完成動態數據效果(演示) 實際中項目不需要這樣, 應在 store 上定義 proxy 從服務器拿數據 */ this.getMyChartStoreStore().on( { 'load' : function(me, operation, eOpts) { me.loadData(generateData(8)); } } ); // 初始化月份下拉框的值為”1“ this.getLatestMon().setValue('1'); // 為圖表加載數據,此處顯示調用, // 是因為此 store 屬性 autoLoad 定義 false this.getMyChartStoreStore().load(); } });
- 運行結果
圖 6. 表單
圖 7. 表格
圖 8. 圖表
項目中的主要問題和解決辦法
- 關于 Sencha Architect 代碼的管理
- 此工具能生成幾乎所有必須的代碼,但對于手工編寫的部分 JS 代碼,本文推薦在 webApp 下建立獨立的目錄,并在入口文件中定義對應的路徑到命名空間的映射。
- 用編譯工具如 mvn 將生成目錄拷貝到 webApp 中。
- 關于 Controller
- 劃分好 controller 的范圍和生命周期。
劃分好頁面功能區,然后確定需要哪些 controller; 區分哪些是動態的,比如一個子窗口或新頁簽,對應 controller 的加載時機也應該是動態的。
- 將需要動態加載的 Controller 從入口文件定義中移除。
這樣做的一個顯而易見的好處是,避免初始頁面"過載";另外能避免因 init()調用太早導致事件綁定失敗,原因也顯而易見:需要綁定事件的組件還沒有被創建出來。
- 劃分好 controller 的范圍和生命周期。
- 關于 store 多實例
設置過 storeId 的 store 將在 storeManager 中注冊為單例;有時這是個限制,比如想在多個 view 實例中擁有獨立的 store 時,Sencha Architect 目前沒有好的辦法。辦法有二,一是 override view,讓每個 view 在 create 時擁有自己的 store;二是在 Model 中設置 proxy( 當然這時得移除 store 中的 proxy)。第二種方法來自 Sencha 的技術支持,但我還沒試過。
- 關于 TreePanel 對屬性設置的要求 我在使用 Sencha Architect 的時候碰到過這個問題,Architect 能顯示熱數據,即屬性設置好后,設計器能實時將數據作為預覽顯示出來;我一開始就碰到樹不能顯示的問題,接著是顯示了又無法展開下級。
- 設置好 store 的 proxy 中的 idProperty、root 屬性。
我一開始設置錯了 root(設置了一個不存在的 field),后來查了很久,找到了這個位置 , 問題解決。中間還懷疑是工具的 bug :-)。 瞧,一個很不起眼的地方,但很打擊士氣。
- 設置好樹節點所用到的 Model 中的 idProperty 屬性。
這個屬性決定點擊樹的節點往下鉆取時往服務器傳遞的參數。
- 設置好 store 的 proxy 中的 idProperty、root 屬性。
總結
Ext JS 從 4.0 以后有了很大的變化,特別是增加了對 MVC 開發模式的支持,給 Ext JS 開發注入了新的活力,也極大地方便了大型 WEB 項目的開發。本文通過對使用 MVC 前后的比較,透過一個很實用的 MVC 實例,演示了運用 Ext JS4 MVC 開發 Web 前端比用以前的版本要簡單很多;文章最后,本人根據自己在開發過程中的經歷提出了一些常見的困難以及解決辦法。希望讀者能從中得到一些啟發和幫助。
源碼下載: demo.zip
原文出處:IBM developerWorks