MVC Application Architecture

前回に続いてSenchaのAPIドキュメンテーションにある MVC Application Architecture を読んでみました。 今回は訳している途中に本家の文章がアップデートされたりして大変でした。w

2012/02/29にコメントでのご指摘を反映して修正しました。


大きなクライアントサイド アプリケーションは、いつも書きにくくて、整理しにくくて、管理しにくいものです。 機能性や開発者をプロジェクトに追加するときに、急速に制御しきれなくなる傾向があります。 Ext JS4 には、コードを組織化するだけではなくコードを書く量を減少させる新しいアプリケーション アーキテクチャがあります。

このアプリケーション アーキテクチャは、初めて導入するModelsやControllersでMVCのようなパターンをフォローします。 多くのMVCアーキテクチャがありますが、その大部分は互いにさしたる違いはありません。 我々の定義は:

  • モデルは、フィールドとそのデータ(例えば、ユーザー名とパスワードの欄があるUserモデル)のコレクションです。 モデルは、データ パッケージを通して自分自身を存続する方法を知っていて、アソシエーションを使って他のモデルとリンクできます。 モデルはExt JS 3のレコードとよく似ていて、通常はストアと一緒使われ、グリッドや他のコンポーネントにデータを提供します。
  • ビューは全てのタイプのコンポーネントがそれです。グリッド、ツリー、パネルはみんなビューです。
  • コントローラーはアプリケーションを動作させるためのコード (ビューをレンダリングしたり、モデルをインスタンス化したり、などのアプリケーションロジック) を配置する特別な場所です。

このガイドではUserデータを管理する非常に簡単なアプリケーションを作成します。 最後まで読むと新しいExt JS 4アプリケーション アーキテクチャを使った簡単なアプリケーションの書き方がわかります。

  • 一度学んでしまえば、あらゆるアプリケーションが同じように動作します。
  • 同じように働いているので、簡単にアプリケーション間でコードを共有できます。
  • ビルドツールを使ってアプリケーションの最適化されたバージョンを作成できます。

ファイル構造

ExtJS4はすべてのアプリケーションで共通な統一されたディレクトリ構造に従います。 アプリケーションの基本的なファイル構造に関する詳細な説明は Getting Started guide を調べてください。 MVCレイアウトではすべてのクラスはappフォルダーに配置されます。 その中にはモデル、ビュー、コントローラー、ストアのサブフォルダがあります。 ここでのサンプル アプリケーションのフォルダ構造は、完成すると次のようになります:

Folder Structure

この例では’account_manager’というフォルダーの中に全体のアプリケーションをカプセル化しています。 Ext ?JS 4 SDKの必要なファイルは ext-4.0フォルダーの中にラップされます。index.htmlの内容は次のようになります。

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

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

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

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

app.js にアプリケーションを作成する

あらゆるExt JS4アプリケーションはApplicationのクラスのインスタンスから始まります。 Applicationはアプリケーションのグローバルセッティング(例えばアプリケーション名など)を保存し、 アプリケーションが使うモデルやビューやコントローラーのすべての参照を保持できます。 また、Applicationにはlaunchメソッドがあります。 このメソッドはすべてがロードされたときに自動的に実行されます。

ユーザー アカウントを管理するの簡単なAccount Manager アプリケーションを作成しましょう。 まず、このアプリケーションにグローバルなネームスペースを選択する必要があります。 すべてのExt JS 4アプリケーションではグローバル変数は一つだけにして、アプリケーションのクラスのすべてはその中にネストして格納します。 普通は短いグローバル大域変数がいいので、この例では「AM」を使用します:

Ext.application({
    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を呼び出し、「AM」という名前のApplicationのクラスのインスタンスを生成しています。 これで自動的にグローバル変数AMが設定され、appFolderコンフィグオプションで設定された’app’に対応するパスでExt.Loaderにネームスペースが登録されます。 次にUsersコントローラを設定しています (コントローラについては次のセクションで扱います)。 最後にスクリーンいっぱいを覆うPanelが入ったViewportを作成する簡単なlaunchメソッドを設定しました。

Initial view with a simple Panel

ほとんどのアプリケーションには、いくつかのコントローラ(通常はモデルごとに一つ)があります。 アプリケーションが使用しているコントローラを配列で指定すると、アプリケーションが起動する前に、 コントローラに対応するクラスが自動的にロードされ初期化されます。 例えばViewportにリスナーをセットアップするときなどに便利です。 この例では、Usersコントローラは AM.controller.Users クラスにマップされ、 そのファイルはapp/controller/User.jsに置かれます。

コントローラーの定義

コントローラーはアプリケーションを束ねる接着剤です。 何をするかというと、(主にビューからの)イベントをリッスンして、アクションを実行します。 次はUsersコントローラーの最初の形です:

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

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

ブラウザで index.html が表示されてアプリケーションがロードされたとき、Usersコントローラーは自動的にロードされます。 (Applicationの定義のときに指定しているからです) コントローラーのinit関数は、Applicationのlaunch関数が呼ばれる直前にコールされます。

initメソッドは コントローラーとビューとのやりとりのしかたを設定したり、他のコントローラー関数と連携するのに使われます。 controlメソッドを使うとビュークラスのイベントにハンドラを割り当てることが簡単にできます。 では、Panelが描画された時にconsole.logするようにUsersコントローラーを変更してみましょう。

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

    init: function() {
        this.control({
            'viewport > panel': {
                render: this.onPanelRendered
            }
        });
    },

    onPanelRendered: function() {
        console.log('The panel was rendered');
    }
});

this.controlを使って、アプリケーションのビューにリスナーをセットアップするようにinitメソッドを変更しました。 このcontrolメソッドは、新しいComponentQueryを使って、素早く簡単にページ上のコンポーネントへの参照を得ています。 まだComponentQueryになじみがなければ、このガイドのすべての説明をよく読んでください。 簡単に言うと、ページ上のコンポーネントを見つけるのにCSSセレクターのような構文が提供されるのです。

initメソッドでは、’viewport > panel’という文字が与えられています。 これは「Viewportの直接の子供である全てのPanelを探せ」という意味です。 次にイベント名(この場合は単にrender)に割り当てるハンドラ メソッドを指定しています。 セレクタに合致する全てのコンポーネントにおいてrenderイベントが発火し、onPanelRenderedが実行されます。

実行すると次のように表示されます。

Controller listener

エキサイティングなアプリケーションではありませんが、組織だったコードを使ってこんなに簡単に始められました。 では、グリッドを追加してみましょう。

ビューを定義する

ここまでに、二つのファイル(app.jsとapp/controller/Users.js)を作りました。 ではシステム内のすべてのユーザーを表示するグリッドを追加したいと思います。 ロジックより少し整理してビューを使い始める頃合いです。

ビューは単なるExt JSコンポーネントのサブクラスとして定義されたコンポーネントです。 views/user/List.jsという新しいファイルを作成し、そこに次のコードを書いて、ユーザーを表示するグリッドを作成しましょう。

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: 'ed@sencha.com'},
                {name: 'Tommy', email: 'tommy@sencha.com'}
            ]
        };

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

        this.callParent(arguments);
    }
});

このビュークラスはExtJSコンポーネントのサブクラスとして定義されるコンポーネント以上のものではありません。 この例ではGridPanelを拡張してこれをxtype(詳しくは後ほど)として使えるようにするためのaliasを設定してます。 storeとcolumnsのコンフィグで、グリッドがどのようにレンダリングされるかを指定してます。

次にUsersコントローラーにこのビューを追加します。 ‘widget.’形式でaliasを指定したので、’panel’などのように’userlist’というxtypeが使えるようになったからです。

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

    views: [
        'user.List'
    ],

    init: ...

    onPanelRendered: ...
});

次にこれをメインビューポートの中に描画するためにapp.jsを変更します。

Ext.application({
    ...

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

他に注目すべきなのはviewを’user.List’と指定している点です。 これはアプリケーションに対して、起動時にこれらのファイルを自動的にロードしろと伝えています。 このアプリケーションは、Ext JS 4の新しいダイナミックローディングシステムを使って、サーバーからこのファイルを自動的に引っ張ってきます。

Our first View

グリッドを制御する

onPanelRendered関数はいまでもコールされることに注意してください。 このグリッド クラスは ‘viewport > panel’ セレクターにマッチするからです。 というのは、このグリッド クラスはGridPanelを継承し、そのGridPanelはPanelを継承しているからです。

この状態ではビューポートの直接の子であるすべてのPanelやPanelのサブクラスに対して、リスナーが追加されてしまいます。 新しいxtypeに対してひもつけるように変更しましょう。 また、あとでユーザー情報を編集できるように、グリッド上でのダブルクリックをリッスンするようにします。

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'));
    }
});

この変更ではComponentQueryセレクターを(シンプルに’userlist’に)、イベント名を(’itemdblclick’に)、 ハンドラー関数の名前を(’editUser’に)変更していることに注目してください。

Double click handler

コンソールにログされるのもいいんですが、本当に欲しい機能はユーザー情報の編集です。app/view/user/Edit.js にビューを作ってみます。

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クラス)のサブクラスを定義します。 もう一度、initComponentを使ってオブジェクトitemsとbuttonsを指定します。 フォームをfitレイアウトで配置します。そのフォームには名前とメルアドを編集するフィールドがあります。 最後に、SaveとCloseの二つのボタンを作ってます。

このビューをレンダリングしてユーザー情報をその中にロードするようにコントローラーを使います。

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の力を借りてもう一度フォームへの参照を得ます。 Ext JS 4の全てのコンポーネントにはdownメソッドがあります。 downメソッドは全ての子コンポーネントをすばやく検索するために、ComponentQueryセレクターを受け取ります。

グリッドの行をダブルクリックするとウィンドウが表示されます。

Loading the form

モデルとストアの作成

編集フォームもできたので、そろそろユーザーを編集してその変更を保存するあたりを始めましょう。 始める前にちょっとだけコードを変更します。

AM.view.user.Listコンポーネントが生成される時、インラインで指定したストアが生成されます。 これはこれでちゃんと動きますが、このストアがアプリケーションのどこからでも参照できて、 その中のデータを更新できるようにしたいと思います。 ストアを別ファイル(app/store/Users.js)に出してしまいましょう。

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

二つだけ小さな変更をします。1つめはUsersコントローラーに対して、ロード時にこのストアを取り込むように指定することです。

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

次にストアをidで参照するように?app/view/user/List.js を変更します。

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

    //we no longer define the Users store in the `initComponent` method
    store: 'Users',

    ...
});

Usersコントローラが定義を管理するストアをインクルードすることによって、ストアは自動的にページにロードされ、storeIdが割り当てられます。 それによりビューから簡単に参照できる(この例だとシンプルに store:’Users’ とコンフィグに書くだけ)ようになります。

今のところストアの中にインラインでフィールドを定義しています。 これでもちゃんと動きますが、Ext JS 4にはパワフルなModelクラスがあり、ユーザー情報を編集する時にはアドバンテージをもらたします。 このセクションの最後に、ストアをapp/model/User.jsに配置されるモデルを使うようにリファクタリングします。

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

モデルを定義するのはこれだけ、フィールドをインラインで定義せずに、モデルを参照するようにストアを変更します。それにコントローラーにもモデルを参照させます。

//the Users controller will make sure that the User model is included on the page and available to our app
Ext.define('AM.controller.Users', {
    extend: 'Ext.app.Controller',
    stores: ['Users'],
    models: ['User'],
    ...
});
// we now reference the Model instead of defining fields inline
Ext.define('AM.store.Users', {
    extend: 'Ext.data.Store',
    model: 'User',

    data: [
        {name: 'Ed',    email: 'ed@sencha.com'},
        {name: 'Tommy', email: 'tommy@sencha.com'}
    ]
});

このリファクタリングによって次のセクションが簡単になりますが、アプリケーションの現在の振る舞いには影響はありません。 ページをリロードして行をダブルクリックすると予想通りユーザー編集ウィンドウがまだ表示されます。ここらで編集機能を終わります。

Loading the form

モデルでデータを保存する

これでユーザーのグリッドにデータをロードして行をダブルクリックしたときには編集ウィンドウが開くようになりましたので、 ユーザーが施した変更を保存したいと思います。 前に定義したユーザー編集ウィンドウには(名前とメルアドのフィールドがある)フォームとsaveボタンがありました。 saveボタンのクリックをリッスンするようにコントローラーのinitメソッドを変更しましょう。

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');
    }
});

二つ目のComponentQueryセレクター(今回は ‘useredit button[action=save]’)を追加しました。 最初のセレクター(’useredit’というxtypeを使ってユーザー編集ウィンドウにフォーカスするために定義しました)と同じように動作します。 次にそのウィンドウにある’save’アクションのあるボタンを探します。 ユーザー編集ウィンドウを定義したときsaveボタンに {action: ‘save’} というコンフィグを設定すると、対象のボタンを特定するのが簡単になります。

これでsaveボタンが押されたときにupdateUserメソッドがコールされます。

Seeing the save handler

これで、ハンドラーが正しくsaveボタンにアタッチされたことがわかりました、ではupdateUserメソッドに実際のロジックを書いていきます。 このメソッドでは、フォームからデータを取り出し、それでユーザーのデータを更新し、ユーザーストアに保存する必要があります。 どのようにするか見てみましょう。

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

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

何が行われているのか解析してみましょう。 クリックイベントはユーザーがクリックしたボタンへの参照を渡してくれます。 でも本当に欲しいのはデータやウィンドウ自身を含むフォームへのアクセスです。 これらを簡単に入手するにはまた、ComponentQueryを使えばいいのです。 最初に、button.up(‘windows’) でユーザー編集ウィンドウを取得しています。 次にwin.down(‘form’)でフォームを取得しています。

次に、現在フォームにロードされているレコードをとって来て、ユーザがフォームにタイプしたものでアップデートします。 最後にウィンドウを閉じてフォーカスをグリッドに戻します。次は、名前を’Ed Spencer’に変更してsaveボタンをクリックしたときに表示される画面です。

The record in the grid has been updated

サーバーに保存する

とても簡単です。サーバーサイドと連携させて終わりにしよう。今のところ、二つのレコードをストアにハードコーディングしてますが、かわりにAJAXを使って読み込むようにしましょう。

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に変えています。 プロキシはExt JS 4のストアやモデルのデータを読み書きする方法です。 AJAX、JSON-P、HTML5のローカルストレージなどのプロキシがあります。 ここでは、data/users.jsonからデータをロードするように設定したシンプルなAJAXプロキシを使ってます。

次にreaderを設定しています。 readerの役割はストアが理解できるフォーマットにサーバーレスポンスをデコードすることです。 この例では?JSON reader を使用って、root と successProperty コンフィグを指定しました (コンフィグに関する詳しい情報については、Json Readerのドキュメントを見てください)。 最後に、data/users.jsonファイルを作りその中に前のデータをペーストします。

{
    success: true,
    users: [
        {id: 1, name: 'Ed',    email: 'ed@sencha.com'},
        {id: 2, name: 'Tommy', email: 'tommy@sencha.com'}
    ]
}

他の修正点はautoLoadをtrueに設定していることです、これはストアがデータを直ちにデータをロードするようにプロキシに依頼することを意味します。 ページをリフレッシュすると以前と同じ結果が表示されますが、アプリケーションにデータがハードコーディングされていません。

最後にすることは変更をサーバーに返すことです。 このサンプルではサーバーサードの静的なJSONファイルを使っているだけなので、データベースの変更を見ることはできませんが、 少なくとも全てが正しく接続されているか確認することはできます。 最初に違うurlにアップデートを送り返すようにプロキシをちょっと変更します。

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

これまで通りusers.jsonからデータを読み込んでいますが、更新はupdateUsers.jsonに送られます。 ここではダミーレスポンスを返ってくるだけですが、動作確認はできます。 このupdateUsers.jsonファイルは{success:true}と書かれているだけです。 次の変更点は、編集後にストアに対して同期されたと伝えることです。 そのためにupdateUserメソッドにもう1行追加します。

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

    record.set(values);
    win.close();
    this.getUsersStore().sync();
}

これで、すべてのサンプルを実行しちゃんと動くことを確認しました。 行を編集しsaveボタンをクリックしてリクエストが正しくupdateUser.jsonに送られていることを確認します。

The record in the grid has been updated

デプロイメント

新たに導入されたSencha SDK Toolsで、Ext JS4アプリケーションの展開もこれまで以上に簡単になります。 このツールで、JSB3(JSBuilderファイル形式)形式で、すべての依存関係のマニュフェストを生成して、 必要な部分だけの最小限のビルドを数分以内に作成します。

操作の詳細については、Getting Started を参照してください。

Next Steps

私たちはUserデータを管理して、更新をサーバに送り返す、非常に簡単なアプリケーションを作りました。 私たちは、簡単な状態で始めて、それをよりきれいで組織化されるように徐々にコードをリファクタリングしました。 この時点で、スパゲッティコードを作らないで、多くの機能を追加することは簡単です。 Ext JS 4 SDKをダウンロードすると、examples/app/simple folder にこのアプリケーションの完全なソースコードがあります。

次のガイドでは、アプリケーションのコードをより小さくし、保守しやすくすることができる、Controllerの高度な使い方やパターンを紹介します。

MVC Application Architecture」への9件のフィードバック

  1. ピンバック: Ext JS4 MVC でのViewport.jsって?| Sunvisor Lab. Ext JS 別館

  2. mario

    記述の誤り  誤り         正しい model: ‘User’,   model: ‘AM.model.User’,

    返信
  3. mario

    modelの指定は以下の正しいのでは? 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'
        }
    }
    

    });

    返信
    1. sunvisor 投稿作成者

      このコードって,「サーバーに保存する」の直下と同じコードじゃないんでしょうか。

      返信
  4. ピンバック: MVCでExt.Directを使う | Sunvisor Lab. Ext JS 別館

  5. ピンバック: SenchaTouch2[4]-MVC[0]-パターンとスタートポイント « cross HVN

  6. mashiki

    このコードって,「サーバーに保存する」の直下と同じコードじゃないんでしょうか。

    marioさんの最初の指摘の通り、  誤: model: ‘User’,  正: model: ‘AM.model.User’, の部分が異なってますね。

    他、 ・一番最初のapp.jsで余計な定義「controllers:…」がある。(あると動かない。) ・コントローラの定義で「ブラウザで index.html が表示されて…」の前に下記のソースがない(もしかしたら翻訳中に本家が変えた部分かも知れませんね) Ext.application({ ...

    controllers: [
        'Users'
    ],
    
    ...
    

    });

    ・’viewport > panel’の不等号が>になっている ・「モデルを定義するのはこれだけ,フィールドをインラインで定義せずに,モデルを参照するようにストアを変更します。それにコントローラーにもモデルを参照させます。」の所のソースでcontroller.Usersとstore.Usersが1つのファイルの様に見えており紛らわしい。

    今日、人に教えるのにこのサイトの翻訳が有って助かりました。marioさんと同じところ含め上記5点でコピペで失敗するので、修正されると日本中のこれからの開発者が助かると思います。

    こちらは是非修正をという訳では有りませんが、音読したとききになったので確認いただければ。「現在でもても」の前後、「結果がひょじ」、「ロード時ににこの」、「グローバル大域変数」。もっと有った気がするのですが、黙読だと思い出せるのはこれくらい。

    返信
    1. mashiki

      不等号のところ「&gt;」になっていると、指摘したかったのですが、>に変換されてしまいました。TT

      返信

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です