LoginSignup
16
14

More than 5 years have passed since last update.

StoreとProxyの蜜月な関係について

Last updated at Posted at 2012-12-13

この記事は、 Sencha Advent Calendar 2012 の12日目の記事です。
@kawanoshinobu さんが怖い人らしいので逆らわないようにしたいと思います。脅されてないです。がんばります。
ブログっぽいものデビューになります。よろしくおねがいしますm(_ _)m

はじめに

Ext JSを3.xから利用していますが、思い返せばExtでオブジェクト(クラス)指向を学んできたような気がします。
Ext JS(とSencha Touch)はそのリッチなUIに目を奪われがちですが、裏ではいろいろなデザインパターンが詰まっています。

  • Observer
  • Composite
  • Proxy
  • Singleton
  • などなど。

この中で、グリッドやツリーなどのデータセットの集まりを表示するUIで利用されている、Store(Proxy)の仕組みをおさらいしたいと思います。
Proxyパターンのいいところを紹介できればなぁ、と。

クラスの構成

さて下の画像はGridパネルクラスのサンプル画像です。
APIドキュメントから拝借してきました(^_^;)
GridPanel
APIドキュメント

このGridパネル、データの構造はどうなっているのでしょうか。
データ更新といった操作について、クラス構成を追いながら勉強してみませんか?

ほかのライブラリやフレームワークと同様に、Ext JSではビューとデータはきちんと分離されており、お互いはイベントを利用して連携しています。

先ほどのAPIドキュメントサンプルのソースを見てみると、大きく2つの部分が見えてきます。
データ(Ext.data.Store)とビュー(Ext.grid.Panel)です。
以降では、このデータ部分に注目していきます。

grid-example.js
Ext.create('Ext.data.Store', {
    storeId:'simpsonsStore',
    fields:['name', 'email', 'phone'],
    data:{'items':[
        {"name":"Lisa", "email":"lisa@simpsons.com", "phone":"555-111-1224"},
        {"name":"Bart", "email":"bart@simpsons.com", "phone":"555--222-1234"},
        {"name":"Homer", "email":"home@simpsons.com", "phone":"555-222-1244"},                        
        {"name":"Marge", "email":"marge@simpsons.com", "phone":"555-222-1254"}            
    ]},
    proxy: {
        type: 'memory',
        reader: {
            type: 'json',
            root: 'items'
        }
    }
});

Ext.create('Ext.grid.Panel', {
    title: 'Simpsons',
    store: Ext.data.StoreManager.lookup('simpsonsStore'),
    columns: [
        {header: 'Name',  dataIndex: 'name'},
        {header: 'Email', dataIndex: 'email', flex:1},
        {header: 'Phone', dataIndex: 'phone'}
    ],
    height: 200,
    width: 400,
    renderTo: Ext.getBody()
});

Gridパネルでのクラス構成は(たしか)下の図のようになっています。

StoreUML

ビューとイベントで結びついたStoreクラスが

  • データの取得
  • データの変換
  • モデルへの詰め込み

を処理していきます。
各処理では、Proxy、Reader/Writer、Modelという専用クラスを用いて作業を移譲しています。
コレクション型データに特化したコントローラといった位置づけですね。

追記)
@martini3ozからのご指摘で「Reader/WriterはStoreじゃなくてProxyにくっついてるよ」と教えてもらいました。
たしかにModel&Proxyだけで利用する場合に困りますもんね・・・

それではサンプルがてら

Proxyがどんなふうに使われるのかを見ていきましょう。
先ほどのサンプルをベースに以下のような動作を追加しておきます。

  • データの追加、削除用ボタン追加
  • データの更新用プラグイン追加(autoSyncでStore操作とProxyを自動連携)
  • Storeデータ表示用ボタン追加(メモリデータの動作確認用)
  • Reader/WriterはデフォルトのJSONを利用(指定しないだけ)

MemoryProxy(ブラウザメモリ上でのみデータ保持)

APIドキュメントのサンプルと同様に、データ置き場をブラウザメモリとするMemoryProxy利用版です。
初期データはスクリプト上に記述され、更新や削除などはページ内でのみ有効です。
追加したStoreデータ確認用ボタンで、追加、削除、更新、ソートなどによるメモリデータの変更が確認できます。

on-memoryproxy.js
var memoryData = [
    {"name":"Lisa", "email":"lisa@simpsons.com", "phone":"555-111-1224"},
    {"name":"Bart", "email":"bart@simpsons.com", "phone":"555--222-1234"},
    {"name":"Homer", "email":"home@simpsons.com", "phone":"555-222-1244"},
    {"name":"Marge", "email":"marge@simpsons.com", "phone":"555-222-1254"}
];

Ext.create('Ext.data.Store', {
    storeId:'simpsonsStore',
    autoSync : true,
    autoLoad : true,
    fields:['name', 'email', 'phone'],
    data : memoryData,
    proxy: {
        type: 'memory',
        id  : 'simpsons'
    }
});

Ext.create('Ext.grid.Panel', {
    title: 'Simpsons',
    store: Ext.data.StoreManager.lookup('simpsonsStore'),
    columns: [
        {header: 'Name',  dataIndex: 'name', editor: 'textfield'},
        {header: 'Email', dataIndex: 'email', flex:1, editor: 'textfield'},
        {header: 'Phone', dataIndex: 'phone', editor: 'textfield'}
    ],
    height: 200,
    width: 400,
    selType: 'rowmodel',
    plugins: [Ext.create('Ext.grid.plugin.RowEditing')],
    renderTo: Ext.getBody(),

    buttons: [{
        text : "Look the store.",
        handler : function() {
            var st = Ext.data.StoreManager.lookup('simpsonsStore'),
                tpl = new Ext.XTemplate(
                    '<tpl for=".">',
                        '<p>{idx}. {name}, {email}, {phone}</p>',
                    '</tpl>'
                ).compile(),
                msg = "";
            Ext.each(st.getRange(), function(item, idx) {
                msg += tpl.applyTemplate(Ext.apply(item.data, {
                    idx : idx+1
                }));
            }, this);
            Ext.Msg.alert("store data", msg);
        }
    }, {
        text : 'add.',
        handler : function() {
            Ext.data.StoreManager.lookup('simpsonsStore').add({}); // add empty data
        }
    }, {
        text : 'delete.',
        handler : function() {
            Ext.data.StoreManager.lookup('simpsonsStore').removeAt(0); // remove Row No.0
        }
    }]
});

こんな感じでCRUD処理が動作します。

  • read処理での一覧表示
    read_action

  • update処理
    update_action

  • Store(メモリ上の)データ表示
    store_data

AjaxProxy(サーバでデータ保持)

それではデータ置き場(Proxy)をサーバに移してみます。
先ほどのコードのうち、Proxy設定部分 のみ 変更します。
Proxyのtypeをajaxに変更し、apiで各インターフェースを設定します。

on-ajax.js
Ext.create('Ext.data.Store', {
    storeId:'simpsonsStore',
    autoSync : true,
    autoLoad : true,
    fields:['name', 'email', 'phone'],
    proxy: {
        type: 'ajax',
        api : {
            read    : "read.php",
            update  : "update.php",
            create  : "create.php",
            destroy : "destroy.php",
        },
        id  : 'simpsons'
    }
});

対応するサーバCGIを準備しておきます。
サンプルなので、read以外は成功を示す{success:true}を返すのみです。
readは先ほどスクリプトで指定していた初期データを出力します。

read.php
<?php
$ret = array(
    array(
        "name"=>"Lisa", "email"=>"lisa@simpsons.com", "phone"=>"555-111-1224"
    ),
    array(
        "name"=>"Bart", "email"=>"bart@simpsons.com", "phone"=>"555--222-1234"
    ),
    array(
        "name"=>"Homer", "email"=>"home@simpsons.com", "phone"=>"555-222-1244"
    ),
    array(
        "name"=>"Marge", "email"=>"marge@simpsons.com", "phone"=>"555-222-1254"
    )
);
echo json_encode($ret);
create.php
<?php
// データベース処理など...
$ret = array(
    'success' => true,
    'debug' => $GLOBALS['HTTP_RAW_POST_DATA'] // for debug
);
echo json_encode($ret);

(update.php, destroy.phpも同様)

メモリ版と動作は変わりませんが、Proxyをすげ替えることでデータ置き場が変わりました。

これが今回の本題ですが、 Proxyによってデータ置き場が隠蔽されており、ビューやモデルは意識しなくてよい のです。
自分は初めてこの実装を理解できたとき、少し感動しました。

WebStorageProxy(ブラウザストレージ)

そのほかに、ブラウザ側データ置き場として、WebStorageがあります。
LocalStorage、SessionStorageの2種類あり、保持期間が異なります。
LocalStorageを利用したサンプルが以下になります。

on-sessionstorage.js
Ext.create('Ext.data.Store', {
    storeId:'simpsonsStore',
    autoSync : true,
    autoLoad : true,
    fields:['name', 'email', 'phone'],
    proxy: {
        type: 'localstorage',
        id  : 'simpsons'
    }
});

LocalStorageは、ブラウザに依存しますがブラウザ上でデータを永続保持できます。
これでサーバ不要のCRUDアプリになっちゃいます。
(画面リロードで前回保存データがきちんと表示されます)

まとめ

Proxyをすげ替えることで、データの所在を意識しない実装が可能になりますよ。
というはなしでした。
@martini3oz記事 にもあるように、このはなしはStoreだけでなくModelに直接適用もできるはずです。(試すひまがなかったのでどなたか・・・)

そのほかにも、

  • ネットにつながっているときはAjaxProxy
  • ネットにつながってないときはLoaclStorageProxy

みたいなMultiProxyみたいのがあったらと思うと、夢が広がりますね〜。
どっかに落ちてないかな。

明日はSenchaの本を執筆されたきしださんですね。
後枠が空いているのでぜひご参加を!Sencha Advent Calendar 2012

しかしシンプソンズはやはり一般的なんだろうか。

謝謝

16
14
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
14