Architecting Your App in Ext JS 4, Part 2

SenchaのAPIドキュメンテーションにある Architecting Your App in Ext JS 4, Part 2 の翻訳をしました。

Ext JS 4 でアプリケーションを設計する Part 2

この前のアーティクルではExt JSを使ってPandoraスタイルのアプリケーションを設計する方法を調べました。 Medel-View-Controllerアーキテクチャについてと、複数のビューとモデルがある複雑なUIアプリケーションに提供する方法を調べました。 このアーティクルではアプリケーションの見た目の設計を越えて、 Ext.application とViewportクラスから始めて、どのようにデザインしコーディングするのかを調べます。

それではアプリケーションを書き始めましょう。

アプリケーションを定義する

Ext JS 3では Ext.onReady メソッドがアプリケーションのエントリーポイントで、 開発者はアプリケーションの構造を考えなければなりませんでした。 Ext JS 4はでMVCライクなパターンが導入されました。

このパターンはアプリケーションを作成するベストプラクティスに従うのに役立ちます。

新しいMVCパッケージで記述されたアプリケーションのエントリポイントは、 Ext.application メソッドを使います。 このメソッドは Ext.app.Application のインスタンスを生成し、ページの準備ができたらlaunchメソッドをコールします。 Ext.onReady を使う方法からこの方法にかわり、 自動的にビューポートを生成するとかネームスペースを設定するとかの新しい機能も追加されます。

app/Application.js

Ext.application({
    name: 'Panda',
    autoCreateViewport: true,
    launch: function() {
        // This is fired as soon as the page is ready
    }
});

nameコンフィグは新しいネームスペースを生成します。 すべてのビュー、モデル、ストア、コントローラーはこのネームスペースの中で有効です。 autoCreateViewportをtrueに設定すると、フレームワークはapp/view/Viewport.jsファイルをインクルードします。 このファイルには、Panda.view.Viewportという名前でクラスを定義します、 ネームスペースはアプリケーションのnameコンフィグによって指定されたものと一致します。

Viewportクラス

UIに必要なビューを見るとき、それぞれのパーツに注目します。 アプリケーションのViewportはこれらのパーツの接着剤として働きます。 必要なビューやアプリケーション全体のレイアウトをまとめるためのコンフィグレーション定義をロードします。 徐々にビューを定義しビューポートに追加するのが、UIの基本構造を生成する手っ取り早い方法だと思います。

このプロセスの中で重要なのはビューの骨組みでありそれぞれのビューそのものではありません。 これはほとんど彫刻のようです。 ビューの非常にラフなシェイプを作る事から始めて、あとから詳細を作っていきます。

ビルディングブロックの生成

前のアーティクルで行った作業を活用して、いくつかのビューを一度に定義してみます。

app/view/NewStation.js

Ext.define('Panda.view.NewStation', {
    extend: 'Ext.form.field.ComboBox',
    alias: 'widget.newstation',
    store: 'SearchResults',
    ... more configuration ...
});

app/view/SongControls.js

Ext.define('Panda.view.SongControls', {
    extend: 'Ext.Container',
    alias: 'widget.songcontrols',
    ... more configuration ...
});

app/view/StationsList

Ext.define('Panda.view.StationsList', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.stationslist',
    store: 'Stations',
    ... more configuration ...
});

app/view/RecentlyPlayedScroller.js

Ext.define('Panda.view.RecentlyPlayedScroller', {
    extend: 'Ext.view.View',
    alias: 'widget.recentlyplayedscroller',
    itemTpl: '<div></div>',
    store: 'RecentSongs',
    ... more configuration ...
});

app/view/SongInfo.js

Ext.define('Panda.view.SongInfo', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.songinfo',
    tpl: '<h1>About </h1><p></p>',
    ... more configuration ...
});

いくつかのコンポーネントコンフィグはこのアーティクルの範囲外なので載せていません。

上記のコンフィグの中で、三つのストアが設定されているのに気づくでしょう。 前のアーティクルで用意されたストア名にマップします。 この時点でストアを生成しましょう。

モデルとストア

サーバーサイドとして動作するモックデータの入った静的なjsonファイルで始めるのが便利です。 その後、動的なサーバーサイドを実装する際の参照としてこのファイルを使うことができます。

このアプリケーションではStatinとSongという2つのモデルを使うことにします。 またこれら2つのモデルを使う3つのストアが必要で、それがデータコンポーネントとバインドされます。 それぞれのストアはサーバーサイドからデータをロードします。 モックデータファイルは次のようなものになります。

静的データ

data/songs.json

{
    'success': true,
    'results': [
        {
            'name': 'Blues At Sunrise (Live)',
            'artist': 'Stevie Ray Vaughan',
            'album': 'Blues At Sunrise',
            'description': 'Description for Stevie',
            'played_date': '1',
            'station': 1
        },
        ...
    ]
}

data/stations.json

{
    'success': true,
    'results': [
        {'id': 1, 'played_date': 4, 'name': 'Led Zeppelin'},
        {'id': 2, 'played_date': 3, 'name': 'The Rolling Stones'},
        {'id': 3, 'played_date': 2, 'name': 'Daft Punk'}
    ]
}

data/searchresults.json

{
    'success': true,
    'results': [
        {'id': 1, 'name': 'Led Zeppelin'},
        {'id': 2, 'name': 'The Rolling Stones'},
        {'id': 3, 'name': 'Daft Punk'},
        {'id': 4, 'name': 'John Mayer'},
        {'id': 5, 'name': 'Pete Philly &amp; Perquisite'},
        {'id': 6, 'name': 'Black Star'},
        {'id': 7, 'name': 'Macy Gray'}
    ]
}

モデル

Ext JS 4のモデル(Model)はExt JS 3のレコード(Record)と非常に似ています。 一つの違いはモデルにプロキシーを指定できることです、またバリデーションやアソシエーションも指定できます。 このExt JS 4アプリケーションのSongモデルは次のようになります。

app/model/Song.js

Ext.define('Panda.model.Song', {
    extend: 'Ext.data.Model',
    fields: ['id', 'name', 'artist', 'album', 'played_date', 'station'],

    proxy: {
        type: 'ajax',
        url: 'data/recentsongs.json',
        reader: {
            type: 'json',
            root: 'results'
        }
    }
});

こうするとストアなしでもこのモデルのインスタンスをロードしたりセーブしたりできるので、 一般的にはこのようにする方法が良いと言えます。 また、同じモデルを使った複数のストアの場合、それぞれでプロキシーを再設定する必要がありません。

次にStationモデルも定義しましょう。

app/model/Station.js

Ext.define('Panda.model.Station', {
    extend: 'Ext.data.Model',
    fields: ['id', 'name', 'played_date'],

    proxy: {
        type: 'ajax',
        url: 'data/stations.json',
        reader: {
            type: 'json',
            root: 'results'
        }
    }
});

ストア

Ext JS 4では複数のストアが同じデータモデルを使うことができ、そのデータを違うソースからロードすることもできます。 このサンプルでは、StationモデルはSearchResultsとStationストアで使われていて、それぞれ別の場所からデータをロードします。 一つは検索結果をもう一つはユーザーのお気に入りのステーションを返します。 これをまとめるために、ストアの一つでモデルに定義されたプロキシーをオーバーライドする必要があります。

app/store/SearchResults.js

Ext.define('Panda.store.SearchResults', {
    extend: 'Ext.data.Store',
    requires: 'Panda.model.Station',
    model: 'Panda.model.Station',

    // Overriding the model's default proxy
    proxy: {
        type: 'ajax',
        url: 'data/searchresults.json',
        reader: {
            type: 'json',
            root: 'results'
        }
    }
});

app/store/Stations.js

Ext.define('Panda.store.Stations', {
    extend: 'Ext.data.Store',
    requires: 'Panda.model.Station',
    model: 'Panda.model.Station'
});

SearchResultsストア定義の中で、違うプロキシーコンフィグを指定することでStationモデルのプロキシー定義をオーバーライドしています。 ストアのloadメソッドをコールしたときに、モデル自身に定義されたプロキシーの代わりにストアのプロキシーが使われます。

サーバーサイドでは検索結果とユーザーのお気に入りのステーションの両方を取り出す一つのAPIを持たせるように実装することもできます。 この場合には、両方のストアはモデル上のデフォルトのプロキシー定義を使い、 ストアをロードするときに単に違うパラメータを渡すようにします。

最後にRecentSongストアを作りましょう。

app/store/RecentSongs.js

Ext.define('Panda.store.RecentSongs', {
    extend: 'Ext.data.Store',
    model: 'Panda.model.Song',

    // Make sure to require your model if you are
    // not using Ext JS 4.0.5
    requires: 'Panda.model.Song'
});

Ext JSの現在のバージョンではストアの’model’プロパティは自動的に依存関係を生成しないので、 モデルを動的にロードできるようにrequireを指定する必要がある点に注意してください。

また慣例的にストアの名前は複数形にし、モデル名は単数形にします。

アプリケーションにストアとモデルを追加する

モデルとストアを定義しましたので、アプリケーションに追加する番です。 Application.jsファイルを再度開きます。

app/Application.js

Ext.application({
    ...
    models: ['Station', 'Song'],
    stores: ['Stations', 'RecentSongs', 'SearchResults']
    ...
});

Ext JS 4 MVC パッケージを使うもう一つの利点はApplicationがstoreやmodelコンフィグに設定されたストアやモデルを自動的にロードしてくれることです。 次に、ロードされた各ストアのインスタンスを生成してその名前と同じstoreIdを設定します。 それにより、ストアをこの稿のビューでやったように、データコンポーネントにバインドするときにストアの名前を使うことができます。 例) store: ‘SearchResults’

接着剤を適用する

次はビュー、モデル、ストアを一緒に接着する番です。 ビューを一つひとつビューボートに追加することから始めましょう。 こうするとビューのコンフィグの不具合をデバッグするのが簡単になります。 それでは、できあがったPandaアプリケーションのビューポートを通して見てみましょう。

Ext.define('Panda.view.Viewport', {
    extend: 'Ext.container.Viewport',

Viewportクラスは通常 Ext.container.Viewport を拡張します。 これはブラウザウィンドウの利用可能なスペース全てを確保します。

    requires: [
        'Panda.view.NewStation',
        'Panda.view.SongControls',
        'Panda.view.StationsList',
        'Panda.view.RecentlyPlayedScroller',
        'Panda.view.SongInfo'
    ],

ビューボートでのビューの依存関係をセットアップします。 こうすることによって、前もってビューにaliasプロパティを使って設定してあるのでxtypeを使うことができるようになります。

    layout: 'fit',

    initComponent: function() {
        this.items = {
            xtype: 'panel',
            dockedItems: [{
                dock: 'top',
                xtype: 'toolbar',
                height: 80,
                items: [{
                    xtype: 'newstation',
                    width: 150
                }, {
                    xtype: 'songcontrols',
                    height: 70,
                    flex: 1
                }, {
                    xtype: 'component',
                    html: 'Panda<br>Internet Radio'
                }]
            }],
            layout: {
                type: 'hbox',
                align: 'stretch'
            },
            items: [{
                width: 250,
                xtype: 'panel',
                layout: {
                    type: 'vbox',
                    align: 'stretch'
                },
                items: [{
                    xtype: 'stationslist',
                    flex: 1
                }, {
                    html: 'Ad',
                    height: 250,
                    xtype: 'panel'
                }]
            }, {
                xtype: 'container',
                flex: 1,
                layout: {
                    type: 'vbox',
                    align: 'stretch'
                },
                items: [{
                    xtype: 'recentlyplayedscroller',
                    height: 250
                }, {
                    xtype: 'songinfo',
                    flex: 1
                }]
            }]
        };

        this.callParent();
    }
});

ViewportはContainerの拡張でContainerはdockedItemを持つことができないので、Viewportの唯一の子アイテムとしてPanelを追加します。 layoutにfitを指定してパネルのサイズをビューポートと同じにします。

アーキテクチャの面で最も重要なことの一つは、 実際のビューの中ではレイアウト指定のコンフィグを定義しないという事実にあります。 ビューの中でflex, width, height のようなプロパティを定義しないことによって、 アプリケーション全体のレイアウトを一箇所で簡単に調整でき、 アーキテクチャの保守性と柔軟性を追加します。

アプリケーションロジック

Ext JS 3では、 ボタンのハンドラーを使ったり、 サブコンポーネントのリスナーをバインドしたり、 ビューを拡張するためにメソッドをオーバーライドしたりと、 アプリケーションのロジックをビューに追加することがよくありました。 しかし、HTMLマークアップの中にインラインでCSSスタイルを定義するべきでないのと同じように、 アプリケーションロジックはビュー定義と分離する方が望ましいのです。 Ext JS 4では、MVCパッケージのコントローラーが提供されます。 ビューや他のコントローラーで発火したイベントをリスニングし、 これらのイベントを実行するためにアプリケーションロジックを実装する役割を果たします。 これにはいくつかの恩恵があります。

恩恵の一つは、アプリケーションロジックがビューのインスタンスにバインドしないと言うことです。 これは、アプリケーションロジックがデータの同期など他のことを処理し続けている間でも、 必要に応じてビューを廃棄したりインスタンス化したりすることができるということを意味します。

また、Ext JS 3ではそれぞれのアプリケーションロジックのレイヤーを追加する度に、 ビューをネストしなければならないことがありました。 アプリケーションロジックをコントローラーに移動することによって、 これらを中央集権化して保守や変更を簡単にすることができます。

最後にControllerベースクラスは アプリケーションロジックを実装するのを容易にする 多くの機能を提供します。

Controllersを生成する

ここまででUI、モデル、ストアのセットアップのための基本アーキテクチャができました。 次はアプリケーションをコントロールする番です。

app/controller/Station.js

Ext.define('Panda.controller.Station', {
    extend: 'Ext.app.Controller',
    init: function() {
        ...
    },
    ...
});

app/controller/Song.js

Ext.define('Panda.controller.Song', {
    extend: 'Ext.app.Controller',
    init: function() {
        ...
    },
    ...
});

アプリケーションにコントローラーをインクルードすると、 フレームワークは自動的にコントローラーをロードしそのinitメソッドをコールします。 initメソッドの中で、ビューやアプリケーションのイベントのリスナーを設定する必要があります。 大きなアプリケーションでは、実行時に追加のコントローラーをロードしたくなることもあります。 そんな時にはgetController()メソッドを使うことができます。

someAction: function() {
    var controller = this.getController('AnotherController');

    // Remember to call the init method manually
    controller.init();
}

実行時に追加のコントローラーをロードするときには、 手動でロードされたコントローラのinitメソッドをコールするのを忘れないようにしましょう。

サンプルアプリケーションの場合には、 アプリケーションの定義の中にcontrollers配列を追加して、 フレームワークにコントローラーのロードと初期化をさせています。

app/Application.js

Ext.application({
    ...
    controllers: ['Station', 'Song']
});

リスナーを設定する

コントローラーのinit関数のナックでcontrolメソッドを使って、UIのパーツをコントロールしてみましょう。

app/controller/Station.js

...
init: function() {
    this.control({
        'stationslist': {
            selectionchange: this.onStationSelect
        },
        'newstation': {
            select: this.onNewStationSelect
        }
    });
}
...

controlメソッドにはコンポーネントクエリーをキーとしたオブジェクトを渡します。 このサンプルではコンポーネントクエリーにはただ単にビューのxtypeを指定しています。 しかし、コンポーネントクエリーを使うと、UI上の特定のものを指定することができます。 コンポーネントクエリーについてより詳しく学ぶには、APIドキュメントを参照してください。

各クエリーはリスナー設定とバインドされます。 それぞれのリスナー設定の中で、リッスンしたいイベント名をキーにします。 利用可能なイベントはクエリーで特定したコンポーネントが提供するものになります。 この場合は、Gridのselectionchangeイベント(StationListビューからの)や ConboBoxのselectイベント(NewStationビューからの)を使います。 コンポーネントで利用できるイベントを探すには、各コンポーネントのAPIドキュメントのeventセクションを参照してください。

リスナー設定の値部分はイベントが発火したときに実行される関数です。 この関数のスコープは常にコントローラー自身になります。

Songコントローラーのリスナーをセットアップしましょう。

app/controller/Song.js

...
init: function() {
    this.control({
        'recentlyplayedscroller': {
            selectionchange: this.onSongSelect
        }
    });

    this.application.on({
        stationstart: this.onStationStart,
        scope: this
    });
}
...

RecentlyPlayedScrollerのselectionchangeイベントのリスナーとともに、 アプリケーションイベントのリスナーもここでセットアップします。 それをするためにアプリケーションインスタンスのonメソッドを使います。 各コントローラーからはthis.application参照を使ってアプリケーションのインスタンスにアクセスできます。

アプリケーションイベントは多くのコントローラーを持つイベントに非常に便利です。 それぞれのコントローラーで同じビューをリスニングする代わりに、 一つだけのコントローラーがビューのイベントをリッスンして、 他のコントローラーがリッスンできるアプリケーションワイドなイベントを発火します。 またコントローラー同士がお互いの存在に依存せずにコミュニケートすることができるようになります。

Songコントローラーは 新しいステーションが開始すると、曲のスクロールや曲の情報を更新する必要があるので、 その情報が必要です。

このstationstartアプリケーションイベントを発火する役目を持つ Stationコントローラーが、実際にどのように行うかを見てみましょう。

app/controller/Station.js

...
onStationSelect: function(selModel, selection) {
    this.application.fireEvent('stationstart', selection[0]);
}
...

selectionchangeイベントが提供する選択されたアイテムの一つを取得して、 stationstartイベントが発火されるときに、その引数として渡しています。

結論

このアーティクルではアプリケーションを校正する基本的なテクニックを見てきました。 もちろん、まだ沢山やることがあります。 このシリーズの次のパートではさらなるコントローラーのテクニックを紹介し、コントロールあーアクションを実装したりビューの詳細を追加したりしてPandaアプリケーションの続きを書きます。