Block Rockin’ Codes

back with another one of those block rockin' codes

localStorageをmockにしてjsonengineのクライアントを開発する

クライアントからAPI(HTTP + URI + JSON)を叩く形のアプリ開発を色々試しています。
その際、サーバ側は選択肢が色々あるんですが、最近はもっぱらjsonengine - Simple and ultra-scalable JSON storage on Google App Engine. No server-side coding required. - Google Project Hostingに取り組んでいます。

jsonengineはAPIの議論も一段落しつつあるので、そのうち記事をちゃんと書こうと思いますが、今回は先にこれを使って今やっていることを書きたいと思います。

やったのはサーバを起動するのが面倒くさかったので、jsonengineでの開発を代わりにlocalStorage(HTML5のWebStorage)でやるという試みです。

localStorageをmockにする

jsonengineはRESTful(を少し手軽にした感じ)なAPIでアクセスすることができるAPIを持ちます。
なので、JSでAjaxなWebAPクライアントを作る上でも非常に便利です。
サーバは限りなくノンコーディングで済むので、この場合書くのはHTML+JS+CSSが中心で、dev_appserver等で開発サーバを起動したら、あとの作業はエディタで書きながらブラウザを再起動して行くだけで進められます。サーバは起動しっぱなし。


しかし、次第に開発サーバをいちいち起動するのも面倒くさくなります。
取得するJSONをローカルのファイルに書いて置いて、そこにアクセスする形でも開発は出来ますが、実際やると色々面倒です。


どこかにデータを置く場所が確保出来れば、サーバは使わないで、クライアントだけで開発を完結できます。
そこで、このjsonengineサーバや、代替ファイルの代わりに、localStorageを使ってやれないかと思ったのでやってみました。

まずjsonengineのライブラリを作る。

localStorageはもちろんHTTPで操作出来るわけではないので、まずjsonengineのAPIに対する処理をJSのメソッド等でライブラリ化します。


jsonengineをJSからAjaxで叩く場合、例としてGETとPOSTの簡単な処理はjQueryを使えば大体下記のような感じになります。

 //GETの例
 $.ajax({
	 type : 'GET',
	 url:'/_je/docType/docId',
	 success : function(res){
		 callback(res);
	 }
 });

 //POSTの例
 $.ajax({
	 type : 'POST',
	 data : data,
	 url:'/_je/docType/',
	 success : function(res){
		 callback(res);
	 }
 });

この処理で必要なパラメータは、

  • docType(テーブル名のようなもの)
  • docId(リソースのID)
  • data(サーバに送るデータ)
  • callback(コールバック関数)

です。

なので、これを引数としたメソッドをjeというオブジェクトに作り、上の処理を少し抽象化して見ると以下の様になります。

var je = {
	 GET : function(docType,docId,fun){
		 $.ajax({
			 type : 'GET',
			 url : '/_je/'+docType+'/'+docId,
			 success : function(res){
				 fun(res);
			 }
		 });
	 },

	 POST : function(docType, data ,fun){
		 $.ajax({
			 type : 'post',
			 url : '/_je/'+docType,
			 data : data,
			 dataType : 'json',
			 success : function(res){
				 fun(res);
			 }
		 });
	 }
 };

特になんてことはないです。ライブラリという程簡単になっているわけでもないかもしれません。

次に、このメソッドと同じインターフェースで、しかし実行先がlocalStorageである以下のようなメソッドを、mockオブジェクトに実装してみます。

 var mock = {
	 GET : function(docType,docId,fun){
		 var text = JSON.parse(localStorage.getItem(docType));
		 fun(text[docId]);
	 },

	 POST : function(docType,data,fun){
		 var docId = mock.getDocId();//jsonengineと同じ方法で一意なidを生成
		 var now = mock.getCurrentTime();//unixtimeが取れる
		 //jsonengineが返すデータと同じものを作成する。
		 var doc = {
			 "_docId" : docId,
			 "_updatedAt" : now,
			 "_createdAt" : now,
			 "_updatedBy" : null,
			 "_createdBy" : null
		 };

		 for (var i in data){
			 doc[i] = data[i];
		 }
		 
		 var bucket = JSON.parse(localStorage.getItem(docType));
		 if(!bucket){
			 bucket = {};
		 }

		 bucket[docId] = doc;
		 localStrage.setItem(docType,JSON.stringify(bucket));

		 fun(doc);
	 },
 };

こうして作成した二つのオブジェクトをje→mockの順番で読んでおくと、開発の段階で、

 //je(本番用)をmock(開発用)で上書きしておく
 je=mock;

 $('#read').bind('click',function(){
	 je.GET(docType,docId,show);
 });

 $('#create').bind('click',function(){
	 var textData = $text.val();
	 var titleData = $title.val();
	 var data = {
		 "textData" : textData,
		 "titleData" : titleData
	 };
	 je.POST(docType,data,function(res){
		 docId=res._docId;
	 });
 });

と書いた場合、jsonengineを使ってるのと同じ感覚で、実際にはデータをlocalStrageとやりとりする。という形で開発が出来ます。
デプロイどころかdev_appserverを起動する必要すらありません。
AppengineSDKすらなくても、ブラウザとmockオブジェクトさえ有れば開発が始められます。

もちろんまだまだ挙動の差は大きいですが、開発初期でデータの簡単なやり取りだけならこれでもそれなりに進めて行けます。


ある程度の段階まで行ったら、mockでは限界が来ますので、最初に書いた下記を消します。

// この行を消す
// je = mock;

すると、オブジェクトは切り替わり、GETやPOSTはAjaxでサーバを見に行く様になります。


大したことはしてませんが、やってみたところ自分は楽になりました。
「サーバ側に疎いフロントエンドエンジニアにも、手軽に開発ができる」がjsonengineのコンセプトなので、
こういうのもありかなと思っています。


このやり方はjsonengineもlocalStorageも基本的にはスキーマレスで、JSONと相性がいいという共通点があるためにやりやすいですね。
あとAPIがRESTfulに出来ていれば、GET/POST/PUT/DELETE的なメソッドを作る方向は変わらないはずなので、ある程度の使い回しが出来ると思います。
サーバが別のもの(CouchDB,sinatra,flask,JSONIC etc)になったら、mockとjeのメソッドの実装をAPIに合わせて書き換えてあげれば同じ考え方でいけそうです。
また、ステートレスな処理が前提なので、メソッド同士が変に呼び合ったりすることもなくシンプルに再現が出来るはずです。


以前書いた記事は、「APIを決めるまでのmock」という意味で書きましたが、今回は「APIが決まっている時のmock」という感じです。(そもそも mock であってるのか?)



これに先立って、localStorageそのものがどの程度行けそうか調べたのが前回の記事です。


localStorageの挙動と簡単なラッパー - Block Rockin’ Codes


localStorageは便利ですが、今は文字列しか格納出来ません。またブラウザごとの差も結構あることが分かりました。
この時、格納・取得・削除・ループだけですが簡単なラッパーを作っているので、それも合わせると以下の様になります。

 // 前回作ったlocalStorageの簡易ラッパー
 var db ={
	 set : function(key, obj){
		 localStorage.setItem(key, JSON.stringify(obj));
	 },

	 get : function(key){
		 var tmp = localStorage.getItem(key);
		 return (tmp === undefined)?  null :  JSON.parse(tmp);
	 },

	 each : function(fun){
		 try{
			 for (var i=0; i<localStorage.length; i++){
				 var k = localStorage.key(i);
				 fun(k,db.get(k));
			 }
		 }catch(e){
			 for (var key in localStorage){
				 if(key === 'key') continue;
				 fun(k,db.get(k));
			 }
		 }
	 },

	 del : function(key){
		 delete localStorage[key];
	 }
 };
 var je = {
     baseURI : '/_je/',

     POST : function(docType, data ,fun){
		 $.ajax({
			 type : 'post',
			 url : '/_je/'+docType,
			 data : data,
			 dataType : 'json',
			 success : function(res){
				 fun(res);
			 }
		 });
     },

     DELETE : function(docType,fun){
		 $.ajax({
			 type: 'DELETE',
			 url: '/_je/'+docType,
			 success : function(res){
				 fun(res);
			 }
		 });
     },

     GET : function(docType,docId,fun){
		 $.ajax({
			 type : 'GET',
			 url:'/_je/'+docType+'/'+docId,
			 success : function(res){
				 fun(res);
			 }
		 });
     },

     PUT : function(params){
		 var docType = params.docType;
		 var docId = params.docId;
		 var data = params.data;
		 var success = params.success;

		 $.ajax({
			 type : 'PUT',
			 url:'/_je/'+docType+'/'+docId,
			 data : data,
			 success : function(res){
				 success(res);
			 },
			 error : function(xhr){
				 if(xhr.status === 409){
					 je.PUT(params);
				 }
			 }
		 });
	 }
 };
 var mock = {
     ALNUMS : "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
     UUID_DIGITS : 32,

     // docIdを生成
     getDocId : function(){
		 var docId=''; 
		 var ALNUMS = mock.ALNUMS;
		 var digits=mock.UUID_DIGITS;
		 
		 for(var i=0; i < digits; i++){
			 docId += ALNUMS[Math.floor(Math.random() * (digits+1))]; 
		 };
		 return docId;
     },

     // UNIXtimeを取得
     getCurrentTime : function(){
		 return parseInt((new Date)/1000);
     },

     POST : function(docType,data,fun){
		 // jsonengineと同じ仕様のJSONを生成
		 var docId = mock.getDocId();
		 var now = mock.getCurrentTime();
		 var doc = {
			 "_docId" : docId,
			 "_updatedAt" : now,
			 "_createdAt" : now,
			 "_updatedBy" : null,
			 "_createdBy" : null
		 };

		 for (var i in data){
			 doc[i] = data[i];
		 }
		 
		 var bucket = db.get(docType);
		 if(!bucket){
			 bucket = {};
		 }

		 bucket[docId] = doc;
		 db.set(docType,bucket);

		 fun(doc);

     },

     GET : function(docType,docId,fun){
		 var text = db.get(docType);
		 fun(text[docId]);
     },

     PUT : function(params){
		 var docType = params.docType;
		 var docId = params.docId;
		 var data = params.data;
		 var success = params.success;

		 var res = db.get(docType)[docId];

		 for (var k in data){
			 res[k] = data[k];
		 }
		 res._updatedAt = mock.getCurrentTime();
		 
		 success(res);
     },

     DELETE : function(docType,fun){
		 db.del(docType);
		 fun(localStorage);
     }
 };

とりあえず今はここまでです。


なお、上のライブラリは古くて、今はjsonengineのAPIが変更になっている部分があるので、このままでは使えません。
でもこのやり方もいけそうなので、新しいAPIの合わせて書き直したら公開しようかなと思っています。

メモ

  • ステータスコードやら例外系の処理も組める様に改善。
  • localStorageのイベントを使う必要が出てくると思うが、イベントはもっと実装の差があるそうなので、まずそこも調査する必要が有る。
  • localStorageに文字列以外も入る様になると色々できそう。
  • IndexedDBならもっと色々なことが出来そう。実装に期待。
  • 必要が有ればjQueryプラグインにして、もっと使いやすい仕組みにする。
  • 結構色々な場面で使えそうなので、なるべく汎用的に保って使い回せないか、色々試してみる。