大型的應用在開發和運維上都存在着困難。應用功能的調整和開發人員的調動都會影響對項目的掌控。ExtJS4帶來了一種新的應用結構。這種結構不止用於組織代碼,也能有效的減小必要的代碼量。javascript
此次ExtJS4的應用結構採用了MVC的形式。在這種形式下,Models和Controllers第一次被引入了ExtJS。目前已經有了許多MVC式的結構,這些結構大部分是大同小異。這裏是咱們定義的MVC結構:css
在這篇文章中咱們將創建一個簡單的用戶數據管理應用。經過這個應用能夠對ExtJS4的MVC結構有初步的瞭解。html
ExtJS4的MVC結構提供了一套結構性和一致性的規範。在開發中會發現,Ext的MVC應用是由自定義類和框架代碼構成的。按慣例,先說下采用MVC結構開發的好處:java
使用ExtJS的MVC模式開發應用須要使用統一的文件結構 。應用全部的類都放在app目錄下。按MVC結構,在app目錄下須要創建model、view、store和controller等子目錄。下圖爲咱們的一個示例應用開發完成後的目錄結構:ajax
在這個例子中咱們封裝了一個應用程序稱爲「account_manager」,ExtJS4的sdk環境置於ext-4.0目錄中。以下爲index.html的代碼:數據庫
1: <html>
2: <head>
3: <title>Account Manager</title>
4: <link rel="stylesheet" type="text/css" href="ext-4.0/resources/css/ext-all.css">
5: <script type="text/javascript" src="ext-4.0/ext-debug.js"></script>
6: <script type="text/javascript" src="app.js"></script>
7: </head>
8: <body></body>
9: </html>
每一個ExtJS4的MVC應用都是經過一個Appliaction類的實例啓動。在Application中包含應用的全局定義(好比應用名稱),以及應用中所使用到的模型、視圖和控制器的引用。此外,Application類還有一個launch函數。launch函數在頁面加載完成後執行。json
接下來咱們將建立一個account_manager應用來管理用戶帳戶信息。首先須要爲這個應用定義一個全局的命名空間。全部ExtJS4的MVC應用應該只有一個全局命名空間,應用中全部的類都置於這個命名空間下。通常咱們會給這個命名空間定義一個較短的名稱,這裏咱們使用「AM」。api
1: Ext.application({
2: name: 'AM',
3:
4: appFolder: 'app',
5:
6: launch: function() {
7: Ext.create('Ext.container.Viewport', {
8: layout: 'fit',
9: items: [
10: {
11: xtype: 'panel',
12: title: 'Users',
13: html : 'List of users will go here'
14: }
15: ]
16: });
17: }
18: });
這裏實現的功能很簡單。首先,咱們調用 Ext.application建立了一個Application類的實例,並將之命名爲「AM」。同時,這裏自動建立了一個全局變量「AM」,併爲Ext.Loader註冊了一個命名空間。而後咱們設置appFolder屬性爲app目錄。最後咱們設置了launch函數,並在函數中建立了包含一個panel的Viewport來填充整個屏幕。數組
能夠說控制器(controller)是把整個應用綁定在一塊兒的膠水。它們(控制器)執行的工做是事件監聽(一般來自view)並作出相應處理。瀏覽器
咱們繼續完成account_manager應用,接下來咱們將建立一個控制器。新建一個js文件app/controller/Users.js,代碼以下:
1: Ext.define('AM.controller.Users', {
2: extend: 'Ext.app.Controller',
3:
4: init: function() {
5: console.log('Initialized Users! This happens before the Application launch function is called');
6: }
7: });
將咱們新建的控制器Users添加到應用中,爲app.js添加屬性以下:
1: Ext.application({
2: ...
3:
4: controllers: [
5: 'Users'
6: ],
7:
8: ...
9: });
當咱們經過index.html在瀏覽器中加載應用時,控制器Users也會被自動加載(由於咱們在上面的應用定義中作了設置),而後Users的init函數會被調用——在Application類的launch函數執行以前。
在控制器與視圖(view)的交互中,init函數的做用十分重要,一般它會和控制器的另外一個函數control一塊兒使用。使用control函數比較容易實現對視圖事件的監聽和響應。
咱們調整下Users控制器,經過control函數來看看panel何時被render(渲染):
1: Ext.define('AM.controller.Users', {
2: extend: 'Ext.app.Controller',
3:
4: init: function() {
5: this.control({
6: 'viewport > panel': {
7: render: this.onPanelRendered
8: }
9: });
10: },
11:
12: onPanelRendered: function() {
13: console.log('The panel was rendered');
14: }
15: });
在上面的代碼中,咱們調整了users控制器的init函數,使用this.control爲應用中的視圖創建了監聽。control函數使用新的ComponentQuery引擎簡單快速的實現了對頁面上的組件的引用。(關於ComponentQuery的內容請參看文檔。簡單地說,就是使用css式的選擇器實現了對頁面上組件的快速匹配)。
在上面的init函數中咱們使用了「viewport > panel」這樣的語句。「viewport > panel」是一個選擇器,它的含義是「找到Viewport的每一個直接子Panel」。隨後咱們提供了一個對象,在這個對象中包含事件名稱(上例中便是render)及相應的處理函數。實現的效果是任何一個匹配咱們定義的選擇器的組件觸發了render事件後即會調用onPanelRendered函數。
如今運行下應用看看效果:
並非一個很炫的應用,但它已經展現了使用MVC管理代碼的好處。接下來咱們給這個應用添加一個表格,使它變得豐滿些。
目前爲止咱們的應用只有兩個文件、極少的幾行代碼。如今咱們想給這個應用添加一個表格來展現系統中的用戶。是時候使用視圖了。
視圖只是一個組件,是繼承自ExtJS現有組件的類。咱們將定義一個用戶信息列表類,新建文件app/view/user/List.js,代碼以下:
1: Ext.define('AM.view.user.List' ,{
2: extend: 'Ext.grid.Panel',
3: alias : 'widget.userlist',
4:
5: title : 'All Users',
6:
7: initComponent: function() {
8: this.store = {
9: fields: ['name', 'email'],
10: data : [
11: {name: 'Ed', email: 'ed@sencha.com'},
12: {name: 'Tommy', email: 'tommy@sencha.com'}
13: ]
14: };
15:
16: this.columns = [
17: {header: 'Name', dataIndex: 'name', flex: 1},
18: {header: 'Email', dataIndex: 'email', flex: 1}
19: ];
20:
21: this.callParent(arguments);
22: }
23: });
如上,這個視圖類也只是一個簡單的普通類。咱們只是繼承了Grid組件,定義了一個別名,經過這個別名咱們可使用xtype調用這個類(通常是這麼用,還有別的用處)。同時咱們還添加了列表須要使用的store和columns信息。
接下來咱們須要將這個視圖添加到Users控制器中。咱們已經使用'widget.'形式設置了類的別名,因此能夠直接使用「userlist」做xtype,就跟之前使用「xtype: ‘panel’」同樣。
1: Ext.define('AM.controller.Users', {
2: extend: 'Ext.app.Controller',
3:
4: views: [
5: 'user.List'
6: ],
7:
8: init: ...
9:
10: onPanelRendered: ...
11: });
而後咱們在應用的Viewport佈局中調用這個類。這須要在app.js中修改launch函數:
1: Ext.application({
2: ...
3:
4: launch: function() {
5: Ext.create('Ext.container.Viewport', {
6: layout: 'fit',
7: items: {
8: xtype: 'userlist'
9: }
10: });
11: }
12: });
這裏還有一點須要注意,在控制器的views數組中,咱們設置了'user.List'
。這告訴了應用去調用視圖文件view/user/List.js,這樣在應用加載時就能夠直接使用這個文件了。這裏使用了ExtJS4新增的的動態調用服務以從服務器中獲取文件。如今再看看咱們的acount_manager應用:
請注意,代碼更新後onPanelRendered函數仍然被調用了。這表名咱們自定義的列表類仍然匹配「viewport > panel」選擇器。由於咱們自定義的userlist類繼承自Grid類,Grid類繼承自Panel類。
此時咱們添加到選擇器中的監聽仍然被Viewport直屬的Panel類或其子類所調用,咱們能夠利用這點對咱們的用戶信息列表的功能作些強化。仍是使用init函數,不過要改成監聽用戶信息記錄上的雙擊事件,使雙擊後能夠對用戶信息進行編輯。調整控制器:
1: views: [
2: 'user.List'
3: ],
4:
5: init: function() {
6: this.control({
7: 'userlist': {
8: itemdblclick: this.editUser
9: }
10: });
11: },
12:
13: editUser: function(grid, record) {
14: console.log('Double clicked on ' + record.get('name'));
15: }
對控制器Users咱們作了以下調整:
修改了ComponentQuery的選擇器,如今只是使用‘userlist’;修改監聽的事件爲「itemdblclick」;調整事件處理函數爲「editUser」。再次運行應用,雙擊記錄時會在控制檯上輸出相應的信息:
應用運行的很好。可是咱們不會知足於簡單的在控制檯上輸出信息,咱們但願能真正的編輯用戶信息。這裏要建立一個新的視圖:app/view/user/Edit.js
:
1: Ext.define('AM.view.user.Edit', {
2: extend: 'Ext.window.Window',
3: alias : 'widget.useredit',
4:
5: title : 'Edit User',
6: layout: 'fit',
7: autoShow: true,
8:
9: initComponent: function() {
10: this.items = [
11: {
12: xtype: 'form',
13: items: [
14: {
15: xtype: 'textfield',
16: name : 'name',
17: fieldLabel: 'Name'
18: },
19: {
20: xtype: 'textfield',
21: name : 'email',
22: fieldLabel: 'Email'
23: }
24: ]
25: }
26: ];
27:
28: this.buttons = [
29: {
30: text: 'Save',
31: action: 'save'
32: },
33: {
34: text: 'Cancel',
35: scope: this,
36: handler: this.close
37: }
38: ];
39:
40: this.callParent(arguments);
41: }
42: });
咱們再次定義了一個ExtJS組件的子類。此次是繼承了Ext.window.Window。咱們仍須要使用initComponent函數爲編輯視圖Edit添加表單元素和按鈕。佈局採用了「fit」形式,窗體下只有一個form面板,在form面板中有兩個文本框用以編輯姓名和郵件地址。最後還定義了兩個按鈕用以關閉窗體和保存更改。
而後咱們要作的就是把這個編輯視圖添加到控制器,並將用戶信息添加到編輯視圖。見代碼:
1: Ext.define('AM.controller.Users', {
2: extend: 'Ext.app.Controller',
3:
4: views: [
5: 'user.List',
6: 'user.Edit'
7: ],
8:
9: init: ...
10:
11: editUser: function(grid, record) {
12: var view = Ext.widget('useredit');
13:
14: view.down('form').loadRecord(record);
15: }
16: });
重點看一下editUser函數。在這個函數中,咱們使用了函數’Ext.widget’,其功能相似於Ext.create('widget.useredit')。而後咱們再次使用ComponentQuery創建了到編輯視圖的映射。每一個ExtJS4的組件都有一個down函數以接收ComponentQuery的選擇器並快速的查找選擇器對應的直屬子元素。
雙擊列表中的行,效果以下圖:
如今咱們已經作好了編輯視圖,也差很少能夠進行編輯和保存了。可是在作這些工做以前,咱們還須要對咱們的代碼進行一次重構。
如今的AM.view.user.List組件中,store是寫在List類中。雖然如今應用也運行的很好,可是咱們更傾向於將store獨立出來,這樣也便於咱們對store中的數據進行處理。如今咱們開始將store中從List視圖中拆分出來放到app/store/Users.js中:
1: Ext.define('AM.store.Users', {
2: extend: 'Ext.data.Store',
3: fields: ['name', 'email'],
4: data: [
5: {name: 'Ed', email: 'ed@sencha.com'},
6: {name: 'Tommy', email: 'tommy@sencha.com'}
7: ]
8: });
而後咱們還須要作兩處小的調整。
首先在Users控制器中添加對store的調用:
1: Ext.define('AM.controller.Users', {
2: extend: 'Ext.app.Controller',
3: stores: [
4: 'Users'
5: ],
6: ...
7: });
而後更新app/view/user/List.js,添加對store的引用:
1: Ext.define('AM.view.user.List' ,{
2: extend: 'Ext.grid.Panel',
3: alias : 'widget.userlist',
4:
5: //we no longer define the Users store in the `initComponent` method
6: store: 'Users',
7:
8: ...
9: });
在控制器中引用store的目的是動態的調用store定義頁,併爲調用的store賦上一個簡短的storeId。這使在視圖中引用store更加方便(在上例中只是添加了一個屬性 store: 'Users')。
如今咱們是將字段(name和email)定義在store中。這樣也能夠工做了,可是ExtJS4提供了一個頗有用的類Ext.data.Model來幫助咱們。咱們將使用Model再次重構咱們的代碼,Model定義於app/model/User.js中:
1: Ext.define('AM.model.User', {
2: extend: 'Ext.data.Model',
3: fields: ['name', 'email']
4: });
完成model的定義後,須要在控制器和store中添加對Model的引用,並從store去掉對應字段的聲明:
1: //the Users controller will make sure that the User model is included on the page and available to our app
2: Ext.define('AM.controller.Users', {
3: extend: 'Ext.app.Controller',
4: stores: ['Users'],
5: models: ['User'],
6: ...
7: });
8:
9: // we now reference the Model instead of defining fields inline
10: Ext.define('AM.store.Users', {
11: extend: 'Ext.data.Store',
12: model: 'AM.model.User',
13:
14: data: [
15: {name: 'Ed', email: 'ed@sencha.com'},
16: {name: 'Tommy', email: 'tommy@sencha.com'}
17: ]
18: });
咱們所作的重構工做不會立刻產生效果,不過會有助於下一環節的工做。再次刷新頁面運行應用會發現並沒有任何變化。不過是時候完成編輯功能了。
如今咱們有一個用戶信息列表了,也能夠雙擊記錄打開編輯窗口了。接下來須要實現的任務就是保存用戶編輯結果。如今咱們的用戶信息編輯窗口有一個表單和一個保存按鈕「save」。接下來咱們須要調整控制器的init函數,添加一個對「save」按鈕的監聽:
1: Ext.define('AM.controller.Users', {
2: init: function() {
3: this.control({
4: 'viewport > userlist': {
5: itemdblclick: this.editUser
6: },
7: 'useredit button[action=save]': {
8: click: this.updateUser
9: }
10: });
11: },
12:
13: updateUser: function(button) {
14: console.log('clicked the Save button');
15: }
16: });
咱們在控制器中添加了第二個ComponentQuery選擇器:‘'useredit button[action=save]'’。它和第一個選擇器工做模式相同,首先根據xtype‘useredit’找到咱們定義的用戶信息編輯窗體,而後在窗體中查找action屬性爲‘save’的按鈕。在咱們定義用戶信息編輯窗體類時,咱們給「save」按鈕設置action:’save’屬性就給咱們提供了一個能夠快速定位到這個按鈕的方法。
咱們很高興看到在咱們點擊save按鈕時updateUser函數被調用並執行了。
咱們已經將save按鈕點擊事件的監聽與處理完成了。接下來要作的就是完善updateUser這個函數,真正實現用戶信息的編輯。在
updateUser函數中咱們須要從表單中獲取信息並保存到用戶信息store中。讓咱們看看功能是怎樣實現的:
1: updateUser: function(button) {
2: var win = button.up('window'),
3: form = win.down('form'),
4: record = form.getRecord(),
5: values = form.getValues();
6:
7: record.set(values);
8: win.close();
9: }
這裏須要打斷一下作些說明。經過按鈕的點擊事件咱們獲取了對‘save’按鈕的引用,可是咱們真正須要的是包含數據的表單及用戶信息編輯窗體。爲了更快的解決問題咱們再次使用了ComponentQuery的選擇器,首先使用button.up('window')獲取了用戶信息編輯窗體,然後使用win.down('form')獲取了表單。
這以後咱們要作的工做就很簡單了,獲取加載到表單中的record,並用用戶編輯後的信息更新record。最後關閉窗體,回到用戶信息列表上。下圖是應用運行後的結果,咱們將第一條記錄的姓名改成了「Ed Spencer」:
剛剛的工做很簡單吧。咱們還須要完成最後一步——實現與服務器端的交互。以前咱們只是生硬地將用戶信息寫在store中,接下來咱們將嘗試使用Ajax獲取用戶信息:
1: Ext.define('AM.store.Users', {
2: extend: 'Ext.data.Store',
3: model: 'AM.model.User',
4: autoLoad: true,
5:
6: proxy: {
7: type: 'ajax',
8: url: 'data/users.json',
9: reader: {
10: type: 'json',
11: root: 'users',
12: successProperty: 'success'
13: }
14: }
15: });
這裏咱們去掉了data屬性,取而代之的是proxy屬性。proxy是Extjs4中獲取和保存數據的一種方式。ExtJS4中有多種形式的proxy,這裏咱們使用了比較簡單的ajax proxy,咱們告訴它從'data/users.json'中獲取信息。
從上面的代碼中看到,咱們給proxy提供了一個reader對象。reader的做用是解碼服務器端反饋的數據集信息。此次咱們使用的是JSON Reader,並設置了它的root和successProperty屬性。最後咱們還須要建立一個'data/users.json'文件,將咱們之前使用的數據複製進去:
1: {
2: success: true,
3: users: [
4: {id: 1, name: 'Ed', email: 'ed@sencha.com'},
5: {id: 2, name: 'Tommy', email: 'tommy@sencha.com'}
6: ]
7: }
在store中的另外一處改動是將autoLoad屬性設置爲true。這意味着stroe會主動向proxy發出請求加載數據。如今咱們刷新頁面從新加載應用並不能看到任何變化。可是實際上獲取數據的方式已經不同了。
咱們要作的最後一件事就是將對數據所作的改變返回到服務器上。在咱們這個例子中咱們是使用靜態的JSON文件在服務器端存儲數據,因此咱們不能看到任何數據庫的變化。可是至少咱們能夠驗證應用是在正常工做的。首先咱們須要對store的proxy作出一點小的調整告訴它將更新信息發送到另外一個url:
1: proxy: {
2: type: 'ajax',
3: api: {
4: read: 'data/users.json',
5: update: 'data/updateUsers.json'
6: },
7: reader: {
8: type: 'json',
9: root: 'users',
10: successProperty: 'success'
11: }
12: }
咱們仍然會從users.json中獲取數據,可是全部的更新都會發送到updateUsers.json。這隻會返回一個虛擬的相應,讓咱們知道應用在正常運行。updateUsers.json中的內容是{"success": true}
。而後還要作的就是告訴Store在編輯完成後同步一次記錄,爲此咱們還要在updateUser函數中添加一行代碼:
1: updateUser: function(button) {
2: var win = button.up('window'),
3: form = win.down('form'),
4: record = form.getRecord(),
5: values = form.getValues();
6:
7: record.set(values);
8: win.close();
9: this.getUsersStore().sync();
10: }
如今咱們的這個示例應用已經完成了。咱們再運行這個應用一次,編輯一條記錄,點擊保存按鈕,而後查看request是否正確的發送給了updateUser.json文件:
示例應用account_manager的源碼可在ExtJS4的文檔中找到,所在目錄是examples/app/simple。
做者注:本文譯自Extjs4.0文檔中的《MVC Architecture》一文。限於在下的英文水平及對Ext的理解,因此不免有些不足之處。不過是抱着本身學習也方便你們的心思,拋磚引玉勉強翻譯了全文。若是有不通之處還請及時指正。