MVCでExt.Directを使う

2011年9月17日 Ext JS / Sencha Perfect Day #007 でLTした時の資料です。Ext JS 4 のMVCアーキテクチャにおいて、Ext.Directを使う方法を解説しています。

このエントリーを書いた後に、別なアドバイスを @kotsutsumi さんから頂きました。 最後の部分もご覧ください。

はじめに

Ext.Directとは

とは、とかいって解説するような立場にはありませんので、@kotsutsumiさんの言葉を引用します。

  • RPC(Remote Procedure Call、リモートプロシージャコール)が行える便利な機能
  • サーバー通信は、Ext.Ajaxを利用するか、Ext.data.Store経由での通信のみで、サーバー処理との連動は以外とコストがかかる。
  • Ext.Directを使うとサーバーサイドの関数をJavaScriptの関数のように呼び出すことができて、可読性もよい。
  • Ext.Directを使うためには、Ext.Direct Packを使うかサーバーサイドを自分で実装する必要がある。

また、複数のリクエストをまとめてサーバーに送信するような仕組みが備わっているそうです。

サーバーサイドはxFrameworkPXで

現在開発が進んでいる NextJS でもExt.Directをサポートしていますが、 この稿を書いている段階では、まだ動かせていないので、今回は @kotsutsumiさんの開発したPHPフレームワーク xFrameworkPX を使います。

再び、@kotsutsumiさんの言葉。

  • xFrameworkPXにはxFrameworkPX_Controller_ExtDirectクラスが存在する。
  • モジュールを作成して、コントローラーに登録するだけでモジュール名でJS側からアクセスできる。
  • サーバーサイドのやりとり(api.phpとかrouter.phpの部分)は、ちゃんとオリジナルで実装して動作確認済。
  • MVCのModel部分をExt側から透過的に利用できるため、開発が高速。

僕は、実際に業務でExt3とxFrameworkPXの組み合わせで開発をしていますが、大変楽ちんです。 xFrameworkPX_Controller_ExtDirectクラスを一つ宣言するだけで、 作り上げたモジュール(Model)のpublicメソッドがそのままクライアントサイドから利用できるようになります。

なお、ここの解説の全ソースは、GitHubに登録してあります。

https://github.com/sunvisor/Ext4DirectWithMVC

Directを利用するための準備

最初にサーバー側のメソッドをコールするというのをやってみます。 PXのExtDirectコントローラーを用意し、そこでモジュールを読み込みます。

    class extdirect extends xFrameworkPX_Controller_ExtDirect
    {
        public $direct = array(
            'namespace' => 'AM'
        ); 
        public $modules = array(
            'users' => array( 'conn' => 'default' )
        );
    }

こんな感じです。namespaceによって、ダイレクトメソッドの名前空間も設定しています。 モジュールに次のメソッドを用意します。

    public function getHero()
    {
        return 'Thomas';
    }

APIを読み込みます

    <!DOCTYPE html>
    <html>
        <head>
            <title>Ext JS 4 RPC Sample</title>
            <meta charset="UTF-8" />
            <link href="ext4/resources/css/ext-all.css" rel="stylesheet">
            <script type="text/javascript" src="ext4/ext-all-debug.js"></script>
            <script type="text/javascript" src="direct.js"></script>
            <script type="text/javascript" src="extdirect.html"></script>
        </head>
        <body>

        </body>
    </html>

9行目の extdirect.html を読み込んでいるところが、APIの読み込みです。

Directプロバイダーを追加します。

    Ext.ns('Ext.app', 'AM');

    Ext.onReady(function() {

        Ext.direct.Manager.addProvider(Ext.app.REMOTING_API);

        AM.users.getHero(function(result){
            Ext.Msg.alert('主人公は' + result + 'です');
        });

    });

おなじみのExt.onReadyの中でプロバイダーを追加しています。 1行目では、名前空間を設定しています。 この例ではサーバーサイドのExtDirectコントローラーは、 ダイレクトメソッドの名前空間をAMとしていますし、 プロバイダの名前空間はExt.appになっていますので、その両方を設定しています。

そしてその直後に、リモートプロシージャーをコールしています。 みてください、こんなに簡単にサーバー側のメソッドをコールできるのです。 サーバーからの戻り値は、コールバック関数を用意してそこで読み取ります。

実行結果

http://www.sunvisor.net/~ext4direct/rpc.html

MVCアーキテクチャでDirectを使う

さて、これからが本題です。Ext JS 4 から導入された MVCアーキテクチャ の中で、Ext.Directを使う方法です。

MVCに限らず、Ext JS でデータを扱うときにはStoreを使いますね。 サーバーからのデータをStoreに格納して処理しますが、 それをExt.Directでどうやるのか、というところが本稿の本題です。

APIを読み込みます。

    <!DOCTYPE html>
    <html>
        <head>
            <title>My Ext JS 4 Application</title>
            <meta charset="UTF-8" />
            <link href="ext4/resources/css/ext-all.css" rel="stylesheet">
            <script type="text/javascript" src="ext4/ext-debug.js"></script>
            <script type="text/javascript" src="app.js"></script>
            <script type="text/javascript" src="extdirect.html"></script>
            <script type="text/javascript" src="ext-lang-ja.js"></script>
        </head>
        <body>

        </body>
    </html>

RPCのサンプルと同様です。 app.jsを読み込んだ後に、extdirect.htmlを読み込んでいます。

Directプロバイダーを追加します。

MVCでは、Ext.onReadyを使いません。 では、どこでプロバイダーの追加をすればいいのでしょうか。

    Ext.ns('Ext.app', 'AM');

    Ext.application({
        name: 'AM',
        autoCreateViewport: true,

        controllers: [
            'Users'
        ],

        launch: function() {
            
            // Ext.Direct プロバイダーの追加
            Ext.direct.Manager.addProvider(Ext.app.REMOTING_API);

        }
    });

上記は、app.jsのコードです。 MVCのアプリケーションでは、app.jsが起点になります。 Applicationのlaunchメソッドはアプリケーションが起動するときに実行されるメソッドです。 そのlaunchメソッドの中でプロバイダーの追加を行います。

StoreのAPIにDirectメソッドを設定します。

Ext.Directを使ったストアでは、directFnコンフィグかapiコンフィグに Ext.Directのメソッドを設定します。 単にデータを読み取るだけの用途ならばdirectFnを使い、 データの更新をする場合はapiを使うと思ってください。 そこで、Storeの定義において、次のように定義したくなりますがちょっと待ってください

    Ext.define('AM.store.Users', {
        extend: 'Ext.data.Store',
        model: 'AM.model.User',
        autoLoad: false,
        
        proxy: {
            type: 'direct',
            api: {
                create: AM.users.addRec,
                read: AM.users.getAll,
                update: AM.users.updateRec,
                destroy: AM.users.removeRec
            },
            reader: {
                type: 'json',
                root: 'data',
                successProperty: 'success'
            }
        }
    });

これだと、Storeが定義される時点ではまだダイレクトプロバイダーが設定されていないので エラーが発生します

定義の時点では次のようにapiは空にしておきます。

    Ext.define('AM.store.Users', {
        extend: 'Ext.data.Store',
        model: 'AM.model.User',
        autoLoad: false,
        
        proxy: {
            type: 'direct',
            directFn: Ext.emptyFn,  // リモートメソッドは空にしておく
            reader: {
                type: 'json',
                root: 'data',
                successProperty: 'success'
            }
        }
    });

当然ながらautoLoadコンフィグもfalseにしてあります。 そして、アプリケーションが起動してからStoreのAPIにDirectメソッドを設定します。 どこで? それは、コントローラーのonLaunchメソッドで行います。

    onLaunch: function () {
        var store = this.getUsersStore();

        // Proxyにサーバー関数をセット
        store.getProxy().api = {
            create: AM.users.addRec,
            read: AM.users.getAll,
            update: AM.users.updateRec,
            destroy: AM.users.removeRec
        };
        store.load();
    },

ここでは、StoreのAPIにDirectメソッドを設定して、loadメソッドを実行しています。

別な方法(というかこちらがおすすめ)

この記事を書いてからしばらく後に、twitter で @kotsutsumi さんとやりとりをしていたら、

launchでaddProviderしてもいいんだけど、別途Ext.application定義しているファイルで、 Ext.onReadyで処理した方がよいですよ

と教えてもらいました。 その会話は次のtogetterを参照してください。

http://togetter.com/li/218847

initとonLaunchについて

ApplicationクラスのコンストラクタでExt.onReady() が作成されます。 そのonReadyの処理の流れは次のようになります。

  • 最初に各コントローラーのinitがコールされます。
  • その次にApplicationのonBeforeLaunchを呼び出します。onLaunchでは次の処理が実行されます。
    • QuickTipのinit
    • CreateViewport
    • Applicationのlaunchメソッドを実行
    • Applicationのlaunchイベントを発火
    • 各コントローラーのonLaunchをコール。

さきほど、Applicationのlaunchメソッドにおいて、ダイレクトプロバイダーを追加しましたので メソッドを設定するのは、その後ということになりますので、 initではなくonLaunchでメソッドを設定する必要があるわけです。

サーバーサイドの実装

Storeと通信をする際には、どんなデータが送られてくるのでしょうか。 またサーバー側ではどのようなデータを返したらよいのでしょうか。

Readアクション

データの読み取りの場合、 サーバー側には、paramOrderコンフィグで設定された、パラメータが送られてきます。 抽出条件や、ページングの情報などが渡されることが多いでしょう。 サーバー側で受け取るパラメータの仕様を決めたら、 Ext JS のparamOrderコンフィグにそれを設定します。

サーバーから返すデータは次のような形式で返します。

    {
        success: true,
        total: 2,
        data: [
            {
                id: 1,
                name: 'Thomas',
                email: 'thomas@sodo.com'
            }, {
                id: 2,
                name: 'Edword',
                email: 'edword@sodo.com'
            }
        ]
    }

この中の data という要素がレコードのデータになりますが、 このプロパティ名は、Readerのroot コンフィグで設定します。 total という要素にはレコードの総数がセットしますが、 このプロパティ名は、ReaderのtotalPropertyコンフィグで設定します。 デフォルトは’total’です。 successという要素には、成功したかどうかをセットします。 このプロパティ名は、ReaderのsuccessPropertyでセットします。 デフォルトは’success’です。

    public function getAll() 
    {
        $fields = $this->_getFields(true);
        $r = $this->get(
            'all',
            array(
                'fields' => $fields
            )
        );
        return array(
            'total' => count($r),
            'data' => $r,
            'success' => true
        );        
    }

更新系アクション

update, create, delete といった更新系のアクションでは、 対象となるデータがサーバーに送信されます。 Storeのsyncメソッドを実行すると、それまでにStoreに対して加えられた変更が 上記の3つのアクションに分解されて、それぞれのリクエストがサーバーに送信されます。

各アクションでは、 一件のデータであれば、一つのオブジェクトが、 複数のデータがあれば、オブジェクトの配列がサーバーに送信されます。 WriterのallowSingleコンフィグをfalseにすると、 1件の場合でも配列で送られるようになります。

update アクションの場合は、更新されたレコードが送信されます。 WriterのwriteAllFieldsコンフィグがtrueの場合は、Storeの全てのフィールドが、 falseの場合は変更のあったフィールドのみが送信されます。 デフォルトはtrueです。

    public function updateRec($arg)
    {
        if( is_array($arg) ){
            foreach($arg as $rec){
                $this->_updateOne($rec);
            }
        } else {
            $this->_updateOne($arg);
        }
        
        return array(
            "success" => true,
            "data" => $arg
        );
    }

    private function _updateOne($arg)
    {
        $rec = (array)$arg;
        $fields = $this->_getFields(false);
        $id = $this->primaryKey;

        $bind = array();
        foreach($fields as $field){
            $bind[$field] = $rec[$field];
        }

        $param = array(
            'field' => $fields,
            'value' => $this->_getValues(false),
            'bind' => $bind,
            'where' => $id . '=' . $rec[$id]
        );

        $this->update($param);
    }

Ext3では、オブジェクトが渡されその中のrootコンフィグで設定した要素の中に レコードが入ってきましたが、 Ext4では、変更のあったレコードそのものがやってきます。

create アクションの場合も、追加されたレコードのデータが送信されます。

    public function addRec($arg)
    {
        $id = $this->primaryKey;

        if( is_array($arg) ){
            foreach($arg as &$rec){
                $rec->$id = $this->_addOne($rec);
            }
        } else {
            $arg->$id = $this->_addOne($arg);
        }

        return array(
            "success" => true,
            "data" => $arg
        );
    }

    private function _addOne($arg)
    {
        $rec = (array)$arg;

        $param = array(
            'field' => $this->_getFields(true),
            'value' => $this->_getValues(true),
            'bind' => $rec
        );
        $r = $this->insert($param, true);
        $this->insert($param);

        return $this->lastId();
    }

delete アクションの場合は、削除されたレコードのidだけが送信されます。

    public function removeRec($arg)
    {
        $id = $this->primaryKey;

        $conds = array();
        if( is_array($arg) ){ 
            foreach($arg as $rec){
                $conds[] = $rec->$id;
            }
        } else {
            $conds[] = $arg->$id; 
        }
        $this->remove($conds);
        
        return array(
            "success" => true,
            "data" => $arg
        );
    }

更新系アクションの戻り値

更新系アクションでは、

    {
        success: true,
        data: <送られてきたデータ>
    }

というデータを返します。dataのプロパティ名は例によって、 rootコンフィグで設定したプロパティ名です。 基本的に送られてきたデータと同じものを 返さないと、その後の処理がうまくいきません。

注意しなければならないのは、createアクションの場合です。 createアクションで送られてくるレコードのidフィールドは空になっています。 サーバー側では、それを登録した結果のidフィールドの値をセットしてから返さなければなりません。

Ext3では、サーバーに送られてきたデータが1つだけであっても、 戻り値は必ず配列で返す必要がありましたが、 Ext4では単独レコードの場合は配列でなくてもよいようです。

参考資料・関連ページ

メモ

以下は、この記事を書いたときに気づいたことのメモです。

getHogeStore

コントローラーで、getHogeStore()という関数を使うと、そのコントローラーが 保持するHogeというStoreを取得できる。

refs

コントローラーで、refsを定義するとコントロールの参照が簡単になる。

    refs: [
        { ref: 'hoge', selector: 'gridpanel' }
    ]

のように設定すると、

    this.getHoge();

でコントロールを参照できる。

3 thoughts on “MVCでExt.Directを使う

  1. ピンバック: Ext JS / Sencha Perfectday #007 講義メモ | Sunvisor Lab. Ext JS 別館

  2. ピンバック: addProvider呼び出しとデプロイ | Sunvisor Lab. Ext JS 別館

  3. Delphia

    Ich glaube, dass man als Atomkraftgegner ab sofort nur noch in unsreen Naledarcänbhrn, insbesondere Frankreich, demonstrieren sollte. In Deutschland ist der Ausstieg ja längst beschlossene Sache. Es ist nur eine Frage der Zeit. In Frankreich werden dagegen noch neue Kraftwerke gebaut – das ist doch eine ganz andere DImension. Die müssen ja dann noch 40-50 Jahre laufen, bis sie sich rentieren und man über eine Abschaltung nachdenken kann.

    返信

コメントを残す

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