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

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

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

+ Observer
+ Composite
+ Proxy
+ Singleton
+ などなど。

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

## クラスの構成
さて下の画像はGridパネルクラスのサンプル画像です。
APIドキュメントから拝借してきました(^_^;)
![GridPanel](http://docs.xenophy.com/ext-js/4-0/doc-resources/Ext.grid.Panel/Ext.grid.Panel1.png)
[APIドキュメント](http://docs.xenophy.com/ext-js/4-0/#/api/Ext.grid.Panel)

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

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

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

```javascript: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](http://farm9.staticflickr.com/8063/8260620981_05cf5a7202_b.jpg)

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

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

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

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

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

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

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

```javascript: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](http://farm9.staticflickr.com/8204/8260586155_e2b267717b.jpg)

* update処理
![update_action](http://farm9.staticflickr.com/8483/8261659522_e4171aacb2.jpg)

* Store(メモリ上の)データ表示
![store_data](http://farm9.staticflickr.com/8058/8260591361_8f59967eea_z.jpg)

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

```javascript: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は先ほどスクリプトで指定していた初期データを出力します。

```php: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);
```
```php: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の２種類あり、保持期間が異なります。
LocalStorageを利用したサンプルが以下になります。

```javascript: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](https://twitter.com/martini3oz) の [記事](http://extjs.sunvisor.net/646) にもあるように、このはなしはStoreだけでなくModelに直接適用もできるはずです。（試すひまがなかったのでどなたか・・・）

そのほかにも、

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

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


明日はSenchaの本を執筆されたきしださんですね。
後枠が空いているのでぜひご参加を！[Sencha Advent Calendar 2012](http://www.adventar.org/calendars/23)

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

## 謝謝
* [APIドキュメント](http://docs.xenophy.com/ext-js/4-0/#/api/Ext.grid.Panel)
：日本語版は毎度助けられてます。
* [Cacoo](https://cacoo.com/diagrams/)
：Web上でUML書けて便利ですねー。