この記事は、 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ドキュメントから拝借してきました(^_^;)
APIドキュメント
このGridパネル、データの構造はどうなっているのでしょうか。
データ更新といった操作について、クラス構成を追いながら勉強してみませんか?
ほかのライブラリやフレームワークと同様に、Ext JSではビューとデータはきちんと分離されており、お互いはイベントを利用して連携しています。
先ほどのAPIドキュメントサンプルのソースを見てみると、大きく2つの部分が見えてきます。
データ(Ext.data.Store)とビュー(Ext.grid.Panel)です。
以降では、このデータ部分に注目していきます。
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パネルでのクラス構成は(たしか)下の図のようになっています。
ビューとイベントで結びついたStoreクラスが
- データの取得
- データの変換
- モデルへの詰め込み
を処理していきます。
各処理では、Proxy、Reader/Writer、Modelという専用クラスを用いて作業を移譲しています。
コレクション型データに特化したコントローラといった位置づけですね。
追記)
@martini3ozからのご指摘で「Reader/WriterはStoreじゃなくてProxyにくっついてるよ」と教えてもらいました。
たしかにModel&Proxyだけで利用する場合に困りますもんね・・・
それではサンプルがてら
Proxyがどんなふうに使われるのかを見ていきましょう。
先ほどのサンプルをベースに以下のような動作を追加しておきます。
- データの追加、削除用ボタン追加
- データの更新用プラグイン追加(autoSyncでStore操作とProxyを自動連携)
- Storeデータ表示用ボタン追加(メモリデータの動作確認用)
- Reader/WriterはデフォルトのJSONを利用(指定しないだけ)
MemoryProxy(ブラウザメモリ上でのみデータ保持)
APIドキュメントのサンプルと同様に、データ置き場をブラウザメモリとするMemoryProxy利用版です。
初期データはスクリプト上に記述され、更新や削除などはページ内でのみ有効です。
追加したStoreデータ確認用ボタンで、追加、削除、更新、ソートなどによるメモリデータの変更が確認できます。
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処理が動作します。
AjaxProxy(サーバでデータ保持)
それではデータ置き場(Proxy)をサーバに移してみます。
先ほどのコードのうち、Proxy設定部分 のみ 変更します。
Proxyのtypeをajaxに変更し、apiで各インターフェースを設定します。
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
$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
// データベース処理など...
$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を利用したサンプルが以下になります。
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
しかしシンプソンズはやはり一般的なんだろうか。