Ext JS MVC Architecture

我們之前在之前的 Ext JS Architecture介紹的Ext JS 6的應用程序的結構,簡單介紹了MVC, 和MVVM兩種體系,這裏包括Controller, Model, View, ViewModel, ViewController等相關的知識。因爲Ext JS 6中,同樣支持MVC結構,而在之前的文章中,並沒有詳細的講到MVC, 更多的是新特性MVVM結構,本指南將詳細的講解MVC, Ext JS 4只有此結構,所以在Ext JS 4的文檔中找到此文章.

當前Ext JS 6中的Ext.app.Application中,關於application architecture的鏈接是錯誤的,它應該是此鏈接,在此糾正一下 http://docs.sencha.com/extjs/4.1.3/#!/guide/application_architecture

MVC Architecture

大型的客戶端程序都是很難寫,難以組織和維護。隨着功能和開發人員的增加,更加難以控制。Ext JS 4提供了一個新的應用程序架構,不僅能夠更好的組織你的代碼,還能減少代碼量。

我們的應用程序架構遵循 MVC-like模式,首次引入了Controller和Models. 當前有許多的MVC結構,其中大部分的定義都有略微的不同,以下是我們對MVC的定義:

  • Model是多個字段(field)和他們數據的集合(e.g. 一個User model包含username 和 password field). Models知道如何通過data package進行執久化, 也能夠通過association(關係)跟其它的model進行相連。Models的功能跟Ext JS 3中的Record class相同。並且跟Stores一起,用於grids和其它組件數據顯示
  • View 任何組件類型 - grids, trees,panel都稱之爲view.
  • Controllers 存放你的應用代碼邏輯的地方,包括是否渲染視圖,實例化Models, 或者其它的邏輯

在本指南中,我們將創建一個非常簡單的應用,來管理用戶數據。

應用程序架構主要是爲實際的類和框架代碼提供結構,並且保持代碼的一致性,它有以下作用:

  • 每個application的原理都是一致的,所以你只需要瞭解一遍,就能知道它的運行原理
  • 更容易在不同的應用之前共享代碼,因爲它們的工作方式是一樣的
  • 你可以使用我們的編譯工具,創建一個優化後的production版本

File Structure

Ext JS 4應用遵循統一的目錄結構,在MVC結構中,所有的類都存放在app/目錄下. 如下圖所示

這裏寫圖片描述

在這個例子中,我們將整個應程程序封裝在’account_manager’目錄中。 所需要的Ext JS 4 SDK文件,則保存在ext-4/目錄下。因此index.html文件的內容如下

<html>
<head>
    <title>Account Manager</title>

    <link rel="stylesheet" type="text/css" href="ext-4/resources/css/ext-all.css">

    <script type="text/javascript" src="ext-4/ext-debug.js"></script>

    <script type="text/javascript" src="app.js"></script>
</head>
<body></body>
</html>

Creating the application in app.js

Ext JS 應用通常是使用Ext.container.Viewport創建的單頁面應用。

一個應用由一個或者多個Views組件,一個view的行爲由相應的Ext.app.ViewController和Ext.app.ViewModel進行管理。

每一個 Ext JS 4 application都是從 Applcation類的實例開始, Application類包含你應用的全局設置(比如app的name, 創建一個全局的命名空間), 還包含app中要使用到的其它類的引用,比如models, views, controllers. 一個Application還包含一個launch 函數, 它在整個頁面都加載完成時,自動運行

讓我們創建一個簡單的Account Manager app,用來幫助我們管理用戶帳號。首先我們需要爲application選擇一個namespace. 所有的Ext JS 4應用都應該使用單個的全局變量,這樣,application中的所有類都在這個全局變量之下。
通常我們需要一個簡短的全局變量,我們在這裏使用 “AM”

Ext.application({
    requires: ['Ext.container.Viewport'],
    name: 'AM',

    appFolder: 'app',

    launch: function() {
        Ext.create('Ext.container.Viewport', {
            layout: 'fit',
            items: [
                {
                    xtype: 'panel',
                    title: 'Users',
                    html : 'List of users will go here'
                }
            ]
        });
    }
});

上面的代碼做了以下幾件事, 首先調用Ext.application創建一個新的Application實例,給這個實例傳遞的name ‘AM’. 它會爲我們自動創建一個全局變量AM, 並且在Ext.Loader中註冊這個namespace. 並且通過appFolder設置與namespace相應的文件夾路徑。我們也簡單的提供了一個launch函數,它僅創建一個 Viewport, Viewport創建好後,可以自動的渲染到doucment body。 一個頁面,只能創建一個Viewport.

這裏寫圖片描述

Defining a Controller

Controller相當於應用程序中的膠水,將應用程序中的元素(views, models)綁定到一起. 它真正做的是,監聽事件(監聽views中的事件),並做出相應的處理。接下來,我們創建一個app/controller/Users.js的控制器.

Ext.define('AM.controller.Users', {
    extend: 'Ext.app.Controller',

    init: function() {
        console.log('Initialized Users! This happens before the Application launch function is called');
    }
});

接着我們將新創建的Users 控制器添加到application配置中, 修改 app.js的以下代碼:

Ext.application({
    ...

    controllers: [
        'Users'
    ],

    ...
});

當我們在瀏覽器中查看index.html, Users controller會自動的被加載(因爲我們在application中有指定它), 並且它的init函數在application的launch之前被調用.

init函數是設置你的控制器與view交互的一個好地方,在init函數中,經常使用this.control函數,用來監聽view類的事件,並作出相應的行爲。接下來,我們更新Users controller的代碼,告訴我們,panel什麼時候被渲染

Ext.define("AM.controller.Users", {
    extend: "Ext.app.Controller",
    init: function(){
        console.log("'Initialized Users! This happens before the Application launch function is called ")
        this.control({
            'viewport > panel': {  //use Ext.ComponentQuery class
                render: this.onPanelRendered
            }
        })
    },
    onPanelRendered: function(){
        console.log('The panel was render');
    }
})

我們使用this.control來設置application中視圖的監聽器。control函數使用新的ComponentQuery 引擎來快速獲取一個頁面中組件的引用(xtype)。如果你還不熟悉ComponentQuery, 可以查看Component Query文檔, 簡單來說,它允許我們使用類似於CSS選擇器,來匹配一個頁面的組件(不是html節點, 而是Ext JS的Component, 可以是xtype, ID)

在我們的init函數中,我們使用了’viewport > panel’,它表示”viewport 下的直接Panel組件, 它返回一個數組,包含每個組件的引用”. 然後我們指定一個對像,對像中的鍵映射爲匹配到組件的事件名稱,它的值映射爲每個事件的處理函數。即,爲所有匹配到的組件,註冊事件監聽函數。

##Defining a View

接下來,我們想添加一個grid,用來顯示我們系統中的users.

其實View就是一個組件,它是Ext JS Component的一個子類。我們創建一個app/view/user/List.js文件,使用一個grid來顯示用戶列表

Ext.define('AM.view.user.List' ,{
    extend: 'Ext.grid.Panel',
    alias: 'widget.userlist',

    title: 'All Users',

    initComponent: function() {
        this.store = {
            fields: ['name', 'email'],
            data  : [
                {name: 'Ed',    email: '[email protected]'},
                {name: 'Tommy', email: '[email protected]'}
            ]
        };

        this.columns = [
            {header: 'Name',  dataIndex: 'name',  flex: 1},
            {header: 'Email', dataIndex: 'email', flex: 1}
        ];

        this.callParent(arguments);
    }
});

我們的View Class就是一個普通的類。在這裏,我們讓它繼承於Grid 組件,並且設置它的別名,這樣我們就可以用xtype的方式來使用它。我們也配置了一個store和columns, 這兩個配置是grid渲染時,需要使用到的數據

接下來,我們將view添加到User controller中。 因爲我們設置別名時,使用了特殊的’widget.’格式,所以可以使用’userlist’作爲xtype. 類似於之前使用的’panel’

接下來,我們需要在Users controller中添加這個view, 否則沒有辦法在app.js中使用 xtype對它渲染。

Ext.define('AM.controller.Users', {
    extend: 'Ext.app.Controller',

    views: [
        'user.List'
    ],

    init: ...

    onPanelRendered: ...
});

然後,我們可以在main viewport中,對它進行浸染

Ext.application({
    ...

    launch: function() {
        Ext.create('Ext.container.Viewport', {
            layout: 'fit',
            items: {
                xtype: 'userlist'
            }
        });
    }
});

需要注意的一個事件是,我們在views數組中,指定了’user.List’, 它是告訴應用程序自動加載這些文件,這樣我們才能在application launch中使用這些組件。application使用了Ext JS 4中新的動態加載系統,可以自動從服務器上加載。
這裏寫圖片描述

Controlling the grid

當前onPanelRendered函數依然可以執行,這是因爲grid類 繼承於 Panel類,符合’viewport > panel’選擇器。

所以我們需要使用一個新的xtype,用來縮窄選擇的範圍。我們可以添加以下代碼,用來監聽grid中每一行的雙擊事件

Ext.define('AM.controller.Users', {
    extend: 'Ext.app.Controller',

    views: [
        'user.List'
    ],

    init: function() {
        this.control({
            'userlist': {
                itemdblclick: this.editUser
            }
        });
    },

    editUser: function(grid, record) {
        console.log('Double clicked on ' + record.get('name'));
    }
});

效果如下圖所示

這裏寫圖片描述

接下來,我們創建一個app/view/user/Edit.js,用來真真的編輯用戶信息,而不是簡單的console log

Ext.define('AM.view.user.Edit', {
    extend: 'Ext.window.Window',
    alias: 'widget.useredit',

    title: 'Edit User',
    layout: 'fit',
    autoShow: true,

    initComponent: function() {
        this.items = [
            {
                xtype: 'form',
                items: [
                    {
                        xtype: 'textfield',
                        name : 'name',
                        fieldLabel: 'Name'
                    },
                    {
                        xtype: 'textfield',
                        name : 'email',
                        fieldLabel: 'Email'
                    }
                ]
            }
        ];

        this.buttons = [
            {
                text: 'Save',
                action: 'save'
            },
            {
                text: 'Cancel',
                scope: this,
                handler: this.close
            }
        ];

        this.callParent(arguments);
    }
});

我們再次定義了已存在組件(這次爲Ext.window.Window)的一個子類. 佈局使用 ‘fit’,它適用於一個組件的容器,它的子組件,隨着容器大小的改變而改變。

接下來,我們在controller中添加新創建的view. 並且渲染它

Ext.define('AM.controller.Users', {
    extend: 'Ext.app.Controller',

    views: [
        'user.List',
        'user.Edit'
    ],

    init: ...

    editUser: function(grid, record) {
        var view = Ext.widget('useredit');

        view.down('form').loadRecord(record);
    }
});

首先,我們通過便捷的方法Ext.widget創建了一個視圖。它等於 Ext.create(‘widget.useredit’); 然後,我們利用了ComponentQuery,來查找form 組件。每一個Component都有一個down函數,它接收一個ComponentQuery選擇器,可以快迅的查找一個子組件。

這裏寫圖片描述

Creating a Model and a Store

當前我們的是以內聯的方法創建的store. 雖然這樣也可以,但我們想要在應用程序的任何地方引用這個store, 並更新它的數據,那就需要將它提取出來,存放到app/store/Users.js中

Ext.define('AM.store.Users', {
    extend: 'Ext.data.Store',
    fields: ['name', 'email'],
    data: [
        {name: 'Ed',    email: '[email protected]'},
        {name: 'Tommy', email: '[email protected]'}
    ]
});

然後修改controller,

Ext.define('AM.controller.Users', {
    extend: 'Ext.app.Controller',
    stores: [
        'Users'
    ],
    ...
});

然後,我們就可以在app/view/user/List.js中,通過id來引用這個store.

Ext.define('AM.view.user.List' ,{
    extend: 'Ext.grid.Panel',
    alias: 'widget.userlist',
    title: 'All Users',

    //我們不需要initComponent中定義User store.
    store: 'Users',

    initComponent: function() {

        this.columns = [
        ...
});

我們在Users controller中包含stores,是用來告訴應用程序自動加載這個store文件,並且根據store類的定義,給定一個storeid, 這樣就可以在view中,通過id引用相應的store.

當前我們只在store中定義了’name’和’email’字段。雖然這也可以滿足我們的需求,但在Ext JS 4中,我們有一個更強大的Ext.data.Model類

Ext.define('AM.model.User', {
    extend: 'Ext.data.Model',
    fields: ['name', 'email']
});

接下來,我們替換store中的內聯field

Ext.define('AM.store.Users', {
    extend: 'Ext.data.Store',
    model: 'AM.model.User',

    data: [
        {name: 'Ed',    email: '[email protected]'},
        {name: 'Tommy', email: '[email protected]'}
    ]
});

然後,我們也需要告訴User controller獲得User model的引用

Ext.define('AM.controller.Users', {
    extend: 'Ext.app.Controller',
    stores: ['Users'],
    models: ['User'],
    ...
});

Saving data with the Model

首先讓我們監聽save button

Ext.define('AM.controller.Users', {
    ...
    init: function() {
        this.control({
            'viewport > userlist': {
                itemdblclick: this.editUser
            },
            'useredit button[action=save]': {
                click: this.updateUser
            }
        });
    },
    ...
    updateUser: function(button) {
        console.log('clicked the Save button');
    }
    ...
});

現在我們可以看到,點擊save後,將日誌出輸到控制檯,接下來,我們更新User

updateUser: function(button) {
    var win    = button.up('window'),
        form   = win.down('form'),
        record = form.getRecord(),
        values = form.getValues();

    record.set(values);
    win.close();
}

Client 事件給了我們一個button的引用,但我們實際上是需要訪問 form 組件(它包含數據) 和window 組件。所以我們再次使用了ComponentQuery,先通過button.up(‘window’)獲得Edit User window, 然後通過win.down(‘form’)獲得 form.

Saving to the server

Ext.define('AM.store.Users', {
    extend: 'Ext.data.Store',
    model: 'AM.model.User',
    autoLoad: true,

    proxy: {
        type: 'ajax',
        url: 'data/users.json',
        reader: {
            type: 'json',
            root: 'users',
            successProperty: 'success'
        }
    }
});

在上面的代碼中,我們刪除了’data’屬性,而用 一個Proxy代替。 Proixs可以用於store和model中,用來加載和保存數據,類型有AJAX, JSON-P 以及HTML5 localStorage. 在這裏,我們使用 AJAX代理,加載服務器的’data/users.json’.

我們在代理中定義了一個Reader, 它用於解析服務器響應到Store可以理解的格式。這裏,我們使用了JSON Reader, 並且指定了JSON中數據保存的位置root, 以及successProperty. 以下是
data/user.json文件

{
    "success": true,
    "users": [
        {"id": 1, "name": 'Ed',    "email": "[email protected]"},
        {"id": 2, "name": 'Tommy', "email": "[email protected]"}
    ]
}

在store中,我們還有一個autoLoad=true, 它的意思是Store將要求Proxy立即加載數據。如果我們刷新網頁,將看到跟之前的效果是一樣的。但現在我們卻可以不用將數據硬編碼在application。

接下來,我們需要將改變的數據返回到服務器。

proxy: {
    type: 'ajax',
    api: {
        read: 'data/users.json',
        update: 'data/updateUsers.json'
    },
    reader: {
        type: 'json',
        root: 'users',
        successProperty: 'success'
    }
}

我們依然從user.json中讀取數據,但是任何的更新,都將發送到updateUser.json.

接下來,我們需要告訴Store 跟服務器同步

updateUser: function(button) {
    var win    = button.up('window'),
        form   = win.down('form'),
        record = form.getRecord(),
        values = form.getValues();

    record.set(values);
    win.close();
    // synchronize the store after editing the record
    this.getUsersStore().sync();
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章