アプリケーションでExt.Loaderを使う

この記事は、 Senchaの英文ブログ記事、 Using Ext Loader for Your Application の邦訳です。文中の図はSenchaのサイトから引っ張ってきてますから、画像がリンク切れになってたら、英語の原文のページがなくなっている可能性があります。

Ext JS4 の新機能クラスローダーシステムは、新しい依存関係システムを利用できるようにします。 これら二つのパワフルな新機能により、 ブラウザが必要なコードだけをダウンロードする、 拡張性の高いアプリケーションを作成できるようになります。

今日は、この新しいクラスローダのシステムを使った小さなアプリケーションを作成してお見せし、依存関係の管理システムの実習をします。 その道中でExt.Loaderシステムのさまざまな設定オプションを説明します。

始める前に、未来に早送りして結果を覗きみてみましょう。そうすることで、拡張するのに必要なクラスを識別することができます。

fig-1

この簡単なアプリには、お互いに連結しているUserGridPanelとUserFormPanelという名のグリッドパネルとフォームのパネルがあります。 UserGridPanelはその操作をサポートするためにModelとStoreを生成する事が必要になります。 UserGridPanelとUserFormPanelは Ext JSのWindowクラスを継承したUserEditorWindowの中にレンダリングされ管理されます。これら全てのクラスが、MyAppという名前空間のもとに置かれます。

コードを書き始める前に、ディレクトリ構造のレイアウトを設定しなくてはなりません。 この名前空間が管理するフォルダー構造は次のようになります。

fig-2

上記の図でわかるように、MyAppは、名前空間のグルーピングまたは”packeges”に応じて分割されています。その結果、アプリケーション全体が次の図のように機能する相互依存関係のモデルができあがります。

Ext JS4のMVCパターンをまねて構成してますけど、MVCパターンを使っているわけではありません。

fig-3

では、index.htmlの内容を満たすことから始めましょう。index.htmlはアプリケーションのルートにあって、アプリケーションを開始するのに必要な全てのものを含んでいます。

<!DOCTYPE HTML PUBLIC  "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
    <title>Ext 4 Loader</title>
    <link rel="stylesheet" type="text/css"  href="js/ext-4.0.1/resources/css/ext-all.css" />
    <script type="text/javascript" src="js/ext-4.0.1/ext-debug.js"></script>
    <script type="text/javascript" src="js/MyApp/app.js"></script>
</head>
<body>
</body>
</html>

index.html ファイルには、Ext JS4のCSSファイルへのリンクタグがあります。これは何度も見たことがあるでしょう、しかしext-debug.js というJavaScriptファイルをインクルードしている部分を見たら思わずなにこれって眉毛が上がるでしょう? きっとこれまで ext-all-debug.js は開発時にext-all.jsをリリース時に使っていたでしょうからね。

Ext JS にはすぐに使える選択肢がいくつかあり、それぞれに、利点と欠点があります。

それぞれのファイルの概説をします。

ext-all-debug-w.comments.js
フレームワーク全体のデバッグバージョン、コメント付きです。このファイルサイズは最大で、ブラウザに表示するときに最も処理時間が必要です。
ext-all-debug.js
上記と同じですが、コメントがありません。これも非常に大きいですが、ほとんどの場合ではデバッグに便利です。
ext-all.js
フレームワーク全体が連結・ミニファイされています。このファイルでのデバッグは不可能ですので、リリース時にのみ使います。
ext-degug.js
このファイルはExt JS の基本部分だけが入っています。このファイルでは、必要なすべてのExt JSをリモートロードでき、最良のデバッグ環境を提供します。トレードオフはこの方法が最も遅いです。
ext.js
ext-debug.js のミニファイバージョン。

さきほどのindex.htmlはext-debug.jsを使うように作られています。これはダイナミックローディングを動作させる最低限のシステムです。 後ほど、両方の長所が得られるext-allバージョンの使い方を紹介します。

UserGridPanelクラスがModelとデータStoreを必要とするので、まずそれらのサポートクラスを開発する必要があります。ModelとStoreを作成することから始めましょう。

Ext.define('MyApp.models.UserModel', {
    extend   : 'Ext.data.Model',
    fields   : [
        'firstName',
        'lastName',
        'dob',
        'userName'
    ]
});

上記のサンプルでは、Ext.data.Modelを拡張してUserModelクラスを生成しています。 こうすることによって、Ext JS は動的にdata.Modelクラスをロードした後UserModelクラスを生成し、data.Modelの依存関係をロードします。これがdata.Modelクラスを拡張する利点です。

次に、Ext.data.Storeを拡張してUserStoreクラスを生成します。

Ext.define('MyApp.stores.UserStore', {
    extend    : 'Ext.data.Store',
    singleton : true,
    requires  : ['MyApp.models.UserModel'],
    model     : 'MyApp.models.UserModel',
    constructor : function() {
        this.callParent(arguments);
        this.loadData([
            {
                firstName : 'Louis',
                lastName  : 'Dobbs',
                dob       : '12/21/34',
                userName  : 'ldobbs'
            },
            {
                firstName : 'Sam',
                lastName  : 'Hart',
                dob       : '03/23/54',
                userName  : 'shart'
            },
            {
                firstName : 'Nancy',
                lastName  : 'Garcia',
                dob       : '01/18/24',
                userName  : 'ngarcia'
            }
        ]);
    }
});

シングルトンのUserStoreクラスを生成するとき、UserStoreプロトタイプに新しいrequiresキーを追加します。 requiresキーは、クラスのインスタンスが作成される前にExt JSに必要なクラスをフェッチするように指示するリストです。 この場合は、UserModelクラスを必要なクラスとして指定しています。

ストアのmodelプロパティの中に定義されるUserModelクラスの利点により、 Ext JSはそれを自動的にロードします。 requiresキーにクラスをリストするということは、 必要なクラスを忘れないようにするために、自己文書化していることになると思います。

views.UserGridPanelの基礎となるクラスは生成されましたので、UserGridPanelそのものを作ることにします。

Ext.define('MyApp.views.UsersGridPanel', {
    extend   : 'Ext.grid.Panel',
    alias    : 'widget.UsersGridPanel',
    requires : ['MyApp.stores.UserStore'],
    initComponent : function() {
        this.store   = MyApp.stores.UserStore;
        this.columns = this.buildColumns();
        this.callParent();
    },
    buildColumns : function() {
        return [
            {
                header    : 'First Name',
                dataIndex : 'firstName',
                width     : 70
            },
            {
                header    : 'Last Name',
                dataIndex : 'lastName',
                width     : 70
            },
            {
                header    : 'DOB',
                dataIndex : 'dob',
                width     : 70
            },
            {
                header    : 'Login',
                dataIndex : 'userName',
                width     : 70
            }
        ];
    }
});

上記のコードを見るとき、requiresキーをよく見てください。どのようにUserStoreキーを追加しているかに注目してください。単に自前のグリッドパネルと自前のデータストアの間に依存関係を定義しているだけです。

次、フォームパネルを作ります。

Ext.define('MyApp.views.UserFormPanel', {
    extend      : 'Ext.form.Panel',
    alias       : 'widget.UserFormPanel',
    bodyStyle   : 'padding: 10px; background-color: #DCE5F0;' 
            + ' border-left: none;',
    defaultType : 'textfield',
    defaults    : {
        anchor     : '-10',
        labelWidth : 70
    },
    initComponent : function() {
        this.items = this.buildItems();
        this.callParent();
    },
    buildItems : function() {
        return [
            {
                fieldLabel : 'First Name',
                name       : 'firstName'
            },
            {
                fieldLabel : 'Last Name',
                name       : 'lastName'
            },
            {
                fieldLabel : 'DOB',
                name       : 'dob'
            },
            {
                fieldLabel : 'User Name',
                name       : 'userName'
            }
        ];
    }
});

UserFormクラスはこれまで開発してきたクラスを必要としませんので、requireディレクティブを追加する必要はありません。

ほとんど完成してきました。あとUserEditorWindowクラスとアプリケーションを起動するapps.jsを作る必要があります。 UserEditorWindowクラスのコードは下記のとおりです。 このクラスは グリッドとフォームパネルを連結するコードを含んでいるので、 少し長くなっていますが、辛抱してください。

Ext.define('MyApp.views.UserEditorWindow', {
    extend   : 'Ext.Window',
    requires : ['MyApp.views.UsersGridPanel','MyApp.views.UserFormPanel'],
    height : 200,
    width  : 550,
    border : false,
    layout : {
        type  : 'hbox',
        align : 'stretch'
    },
    initComponent : function() {
        this.items   = this.buildItems();
        this.buttons = this.buildButtons();
        this.callParent();
        this.on('afterrender', this.onAfterRenderLoadForm, this);
    },
    buildItems : function() {
        return [
            {
                xtype     : 'UsersGridPanel',
                width     : 280,
                itemId    : 'userGrid',
                listeners : {
                    scope     : this,
                    itemclick : this.onGridItemClick
                }
            },
            {
                xtype  : 'UserFormPanel',
                itemId : 'userForm',
                flex   : 1
            }
        ];
    },
    buildButtons : function() {
        return [
            {
                text    : 'Save',
                scope   : this,
                handler : this.onSaveBtn
            },
            {
                text    : 'New',
                scope   : this,
                handler : this.onNewBtn
            }
        ];
    },
    onGridItemClick : function(view, record) {
        var formPanel = this.getComponent('userForm');
        formPanel.loadRecord(record)
    },
    onSaveBtn : function() {
        var gridPanel  = this.getComponent('userGrid'),
            gridStore  = gridPanel.getStore(),
            formPanel  = this.getComponent('userForm'),
            basicForm  = formPanel.getForm(),
            currentRec = basicForm.getRecord(),
            formData   = basicForm.getValues(),
            storeIndex = gridStore.indexOf(currentRec),
            key;
        //loop through the record and set values
        currentRec.beginEdit();
        for (key in formData) {
            currentRec.set(key, formData[key]);
        }
        currentRec.endEdit();
        currentRec.commit();
        // Add and select
        if (storeIndex == -1) {
            gridStore.add(currentRec);
            gridPanel.getSelectionModel().select(currentRec)
        }
    },
    onNewBtn : function() {
        var gridPanel = this.getComponent('userGrid'),
            formPanel = this.getComponent('userForm'),
            newModel  = Ext.ModelManager.create({}, 
                              'MyApp.models.UserModel');
        gridPanel.getSelectionModel().clearSelections();
        formPanel.getForm().loadRecord(newModel)
    },
    onAfterRenderLoadForm : function() {
        this.onNewBtn();
    }
});

UserEditorWindowのコードにはUserGripPanelとUserFormPanelクラスの結合と生成を管理する多くのものが含まれています。Ext JS に以前に作成した二つのクラスをロードするように指示するために、requiresキーにリストしなければなりません。

app.jsファイルの中に全て一緒に結びつける準備ができました。 よくわかるように3つの段階を経ながら作成します。 最も単純な設定から初めて、徐々に増やしていきます。

Ext.Loader.setPath('MyApp', 'js/MyApp');
Ext.onReady(function() {
    Ext.create('MyApp.views.UserEditorWindow').show();
});

最初に、app.js ファイルはMyApp名前空間へのパスを追加するようにExt JSに指示します。 最初の引数に名前空間を、2つめの引数にページをロードする相対パスを渡して Ext.loader.setPathを呼び出します。

次に、Ext.createをコールする無名関数を渡して、Ext.onReadyをコールします。 Ext JS 4.0 でのExt.createは遅延インスタンス化をサポートします。 Ext.createには、インスタンス化するUserEditorWindowクラスの文字列表記を渡しています。 クラスのインスタンスを参照する必要はなく、すぐに表示したいので、showメソッドを連鎖呼び出ししています。

私たちのページ( [ http://moduscreate.com/senchaarticles/01/pass1.html )で表示すると、UIがレンダリングするのが見えますが、かなり遅いですし、Firebugの中でExt JSからのメッセージが表示されます。

fig-4

最適な方法でローダーシステムを使っていないので、Ext JSに怒鳴られてしまいました。 2番目にこの問題を取り上げます。 しかしながら、これは活用すべきすばらしい学習の機会です。

Netタブに切り替えなくても見えるように、 すべてのXHRリクエストをコンソールの中に表示するようにFireBugを設定します。 これで、すべてのExt JSクラスをロードしたとしても、それをフィルタリングすることができるようになり、 クラス依存システムの動作を見ることができます。

FireBugのコンソールフィルターに”User”とタイプするだけで、次のように表示されます。

fig-5

見たとおり、UserEditorWindowクラスが最初にロードされ、UserGridPanelをrequireしてます。UserGridPanelはUserStoreとUserModelをrequireしています。最後に、UserFormPanlクラスがロードされます。

私はさきほど、最適な方法でローダーシステムを使っていないので、Ext JSに怒鳴られてしまったと言いました。 これは、Ext.onReadyが起動した後に特定の依存関係が同期XHRでロードされるためです。これは最も効率的な方法ではありませんし、デバッグも容易ではありません。

この問題を解決するには、パフォーマンスがあってデバッグが容易な方法でクラスをロードするようにExt JSに指示するように、app.jsを更新しなければなりません。

Ext.Loader.setPath('MyApp', 'js/MyApp');
Ext.require('MyApp.views.UserEditorWindow');
Ext.onReady(function() {
    Ext.create('MyApp.views.UserEditorWindow').show();
});

より速いローディングを可能にして、 よりよいデバッグ環境を手に入れるには、 Ext.onReadyの前に単にExt.requireの呼び出しを追加するだけです。 それでExt JSにUserEditorWindowクラスをrequireするように指示します。 こうすると、Ext.onReadyが実行される前にロードされるように、ドキュメントのHEADにスクリプトタグを挿入することができます。

http://moduscreate.com/senchaarticles/01/pass2.html にアクセスしてこの動きを確認してください。 このページをロードし終わってもExt JS が警告メッセージをコンソールに吐き出さなくなったでしょう?

ここでやったことはExt JSフレームワークとアプリケーションクラスの遅延ローディングを有効にすることです。 これはデバッグの時にはよいのですが、ページのレンダリング時間はそれと引き替えに非常に痛いことになっています。 なぜかって?

簡単に言うと、ロードされたリソースの数が膨大だからです。 この例では、Ext JSはJavaScriptのリソースだけで全部で193のリクエストをWebサーバーに対して投げています。ほとんどはキャッシュされています。

fig-6

これまで6つのJavaScriptファイル(5つのクラスとapp.js)を作りました。ということは必要とするExt JS ファイルをロードするために、ブラウザが187のリクエストをしなければならなかったということになります。 このソリューションは、使えるには使えますが多くの場合望ましいとはいえません。ですが、ローカルの開発環境での開発には最適です。

この問題を解決するために、ハイブリッドな状況を作り出すことができます。ext-all-debugを使ってフレームワーク全体をロードして、自作のクラスを動的にロードします。こうするためには、2つのファイルを更新します。

最初は、index.thmlをext-debug.jsのかわりにext-all-debug.jsをインクルードするように変更します。

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

次に、app.js を変更して、Ext.Loaderを有効にします。

(function() {
    Ext.Loader.setConfig({
        enabled : true,
        paths   : {
            MyApp : 'js/MyApp'
        } 
    });
 
    Ext.require('MyApp.views.UserEditorWindow');
 
    Ext.onReady(function() {
        Ext.create('MyApp.views.UserEditorWindow').show();
    });
})();

enebledプロパティをtrueにセットし名前空間と相対パスをpathsに設定したオブジェクトを渡して、Loader.setConfigを呼び出してExt Loaderを有効にしています (訳注: Ext.Loaderはext-debug.jsで使うときにはデフォルトでenabled: true に設定されていますが、この場合には明示的にtrueをセットしてやらなければなりません)。 app.jsを変更することによって、ローカル開発環境ではほんの数秒でアプリケーションをロード時表示することができるようになります。

fig-7

ついにできました!Ext JSクラス依存関係システムとダイナミックローディングを使うことができる簡単なアプリケーションを作成しました。 これらのファイルは http://moduscreate.com/senchaarticles/01/files.zip からダウンロードできます。

1 thought on “アプリケーションでExt.Loaderを使う

  1. ピンバック: scriptタグで読み込むファイルとデプロイについて | Sunvisor Lab. Ext JS 別館

コメントを残す

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