この記事はTypeScript Advent Calendarの5日目の記事です。
最近Marionette.js+TypeScriptを自社開発のeラーニングシステムeden LMSにも少しずつ投入しはじめていて、なかなか良い感じだと思っています。この記事は、TypeScript+Marionette.jsのサンプルを示しつつ、簡単に使い方を説明していくという内容になっています。
そもそもMarionette.jsとは
Marionette.jsの知名度が心配なので簡単に補足しておきます。Marionette.jsは、JavaScriptのMVCフレームワークであるBackbone.jsを、さらに簡単に利用するためのライブラリ、といったところです。Backbone.jsはその名前の通り、良くも悪くも「背骨」にあたる機能を提供するフレームワークであり、実際の開発に使おうとすると、こまごまとした箇所を自分で書いてあげる必要があります。Marionette.jsは、Backbone.jsを使うときにたいてい必要とされるであろう「こまごまとした箇所」を、うまいことやってくれるライブラリと言えます。黒魔術などと罵倒されがちなAngularJSと比べると、Backbone.js、Marionette.jsともに非常に素直な作りになっており、裏側の処理をあまり意識せずに使えます。
Backbone.js+Marionette.jsのいいところ
Backbone.js+Marionette.jsを既存のシステムに投入するときに嬉しいポイントとして「使いたい部品から使える」ということがあります。Backbone.js+Marionette.jsは、Model、View、Controller、Router、Regionなどいろいろなコンポーネントから構成されていますが、極端な話ModelとViewだけ使うこともできたりします。あまりクリティカルでないところに対して、一部のコンポーネントから使っていって、徐々に利用範囲を増やしていくことができるわけです。また、素直な作りになっている分「JQueryを使ってガリガリ書いた過去のコード」を流用しやすいというあたりも、既存システムに取り入れて行く場合にありがたいところかなと思います。
Marionette.jsをTypeScriptで使う
Marionette.jsの話ばかりだとTypeScript Advent Calendarの記事ではなくなってしまうので、TypeScriptでのサンプルをいくつか示します。サンプルコードについては、 https://github.com/urushio/ts_marionette に放り込んでおきました。require.js+AMDの構成なので、tsc --module amd app.tsでコンパイルすれば、静的なHTMLが扱えるWebサーバー上でそのまま動くはずです。
モデルとビューでレンダリングするサンプル
まず比較的簡単なサンプルです。実行すると、「こんにちは、TypeScript!」と表示されます。
HTML側
「#divOutput」が結果を出力する領域、「#myTemplate」がテンプレートとなっている以外は無視してもらって大丈夫です。テンプレート内で、バインドされたモデルのmessageプロパティにアクセスしています。
<!DOCTYPE html>
<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 
		<title>TypeSciptサンプル</title>
	</head>
<body>
	<div id="divOutput">
	
	</div>
	
	<script id="myTemplate" type="text/html">
		こんにちは、<%= message %>!
	</script>
	
	<script>
	var require = {'baseUrl':"../lib"};
	</script>
	<script data-main="../helloworld/app.js" src="../lib/require.js"></script>
</body>
</html>
TypeScript側
Marionette.jsやBackbone.jsについての説明をし出すとキリがないので省略しますが、内容をざっくりと説明すると、次のような感じです。
・d.tsファイルへの参照とそれぞれのライブラリのimportを行う
・モデルのクラス(SampleModel)とビューのクラス(SampleView)のクラスをそれぞれ定義する。ビューでテンプレートとなる要素を指定する
・モデルのインスタンスを生成し、ビューのインスタンスと紐付けて、ビューのレンダリングを実行する
///<reference path="../lib/jquery/jquery.d.ts" /> 
///<reference path="../lib/backbone/backbone.d.ts" /> 
///<reference path="../lib/marionette/marionette.d.ts" /> 
import $ = require("jquery");
import Backbone = require("backbone");
import Marionette = require("backbone.marionette");
class SampleModel extends Backbone.Model{
};
class SampleView extends Marionette.ItemView<SampleModel>{
    template:string = "#myTemplate";
};
$(document).ready(()=>{
    var model= new SampleModel({message:"TypeScript"});
    var view = new SampleView({model:model,el:$("#divOutput")});
    view.render();
});
d.tsファイルの参照やインポートが正しく動作していること、「view.」まで入力するとIDE側で自動補完の候補を示してくれることなどが確認できます。
コレクションやイベントを扱うサンプル
次にもう少し複雑な例として、コレクションやイベントハンドラなどを扱う例を見てみます。実行すると、「こんにちは、TypeScript!」と「こんにちは、JavaScript!」の2つの要素を持つリストが表示されます。また、合わせて描画されているボタンをクリックすると、それぞれ「TypeScript」「JavaScript」という文字列をalertで表示します。
HTML側
HTML側はほとんど変わらず、テンプレート内にボタンを追加しただけです。
(略)
<script id="myTemplate" type="text/html">
	<li>
		こんにちは、<%= message %>!<input type="button" class="button" value="ボタン"/>
	</li>
</script>
(略)
TypeScript側
やっていることは次の通りです。
・先ほどのモデルとビューに加え、コレクションのクラス(SampleCollection)とコレクションビューのクラス(SampleCollectionView)を定義します。
・SampleView内で、ボタンクリック時の処理を実装します。
・モデルのインスタンスを2つ追加したコレクションを生成し、それとコレクションビューのインスタンスを関連づけてレンダリングします。
///<reference path="../lib/jquery/jquery.d.ts" /> 
///<reference path="../lib/backbone/backbone.d.ts" /> 
///<reference path="../lib/marionette/marionette.d.ts" /> 
import $ = require("jquery");
import Backbone = require("backbone");
import Marionette = require("backbone.marionette");
class SampleModel extends Backbone.Model{
};
class SampleCollection extends Backbone.Collection<SampleModel>{
};
class SampleView extends Marionette.ItemView<SampleModel>{
    template:string = "#myTemplate";
    events(){
        return {
            "click .button": "onButtonClick"
        };
    }
    onButtonClick(){
        alert(this.model.get("message"));
    }
};
class SampleCollectionView extends Marionette.CollectionView<SampleModel>{
};
$(document).ready(()=>{
    var collection = new SampleCollection();
    collection.add(new SampleModel({message:"TypeScript"}));
    collection.add(new SampleModel({message:"JavaScript"}));
    var view = new SampleCollectionView(
        {
            collection:collection
            ,el:$("#divOutput")
            ,childView:SampleView
        }
    );
    view.render();
});
コレクションクラスやコレクションビュークラスのコードを書いていく上で、自動補完や型チェックなどがだいたい動きます。もともとJavaScript用に作られたフレームワークなので、型が常に機能してくれるわけではないですが、それでもかなり助かる感じです。
サーバーサイドと連携するサンプル
最後にサーバーサイドとの連携サンプルを示します。サーバーサイドから取得したJSONを元にコレクションビューを描画します。また、削除ボタンをクリックするとそれぞれの要素を削除します。また、サーバーサイドのプログラムがないので、実際にDBなどにアクセスするわけではありませんが、ブラウザの開発者ツールで見てもらえば、保存と削除をクリックしたときに、それぞれサーバーにPOSTとDELETEのリクエストを投げていることが確認できます。
HTML側
テンプレート内のボタンを「保存」と「削除」の2つにしただけです。
(略)
<script id="myTemplate" type="text/html">
	<li>
		こんにちは、<%= message %>!
		<input type="button" class="save-button" value="保存"/>
		<input type="button" class="destroy-button" value="削除"/>
	</li>
</script>
(略)
サーバーサイド(ダミー)
とりあえずデータの取得だけでも動くように、静的なjsonファイルを置いておきます。
[
	{"id":1,"message":"TypeScript"}
	,{"id":2,"message":"JavaScript"}
	,{"id":3,"message":"ECMAScript"}
]
TypeScript側
基本的にはモデルとビュー、コレクションとコレクションビューのクラスを定義することはこれまでと同じです。異なるところは以下の通りです。
・コレクションクラスでurlプロパティを設定しました。これだけでfetchやsave、destroyなどのメソッド呼び出し時に、サーバーサイドのAPIに適切にアクセスしてくれるようになります。
・レンダリングについて、コレクションクラスのfetchメソッドの完了後に行うようにしました。
///<reference path="../lib/jquery/jquery.d.ts" /> 
///<reference path="../lib/backbone/backbone.d.ts" /> 
///<reference path="../lib/marionette/marionette.d.ts" /> 
import $ = require("jquery");
import Backbone = require("backbone");
import Marionette = require("backbone.marionette");
class SampleModel extends Backbone.Model{
};
class SampleCollection extends Backbone.Collection<SampleModel>{
    url:string = "./api.json";
};
class SampleView extends Marionette.ItemView<SampleModel>{
    template:string = "#myTemplate";
    events(){
        return {
            "click .save-button": "onSaveButtonClick"
            ,"click .destroy-button": "onDestroyButtonClick"
        };
    }
    onSaveButtonClick(){
        this.model.save();
    }
    onDestroyButtonClick(){
        this.model.destroy();
    }
};
class SampleCollectionView extends Marionette.CollectionView<SampleModel>{
};
$(document).ready(()=>{
    var collection = new SampleCollection();
    collection.fetch({
        success:()=>{
            var view = new SampleCollectionView(
                {
                    collection:collection
                    ,el:$("#divOutput")
                    ,childView:SampleView
                }
            );
            view.render();
        }
    });
});
サンプルは以上です。
まとめ
Marionette.jsとBackbone.jsの話とTypeScriptの話が混在しているので分かりにくかったかもしれませんが、とりあえずTypeScriptでもMarionette.jsが使えることは分かったと思います。初めに書いたように、Marionette.jsやBackbone.jsは素直な設計になっていて、JavaScriptでMVCをやるときのベストプラクティスといった感じで使えます。そういったフレームワークをTypeScriptを通して使うことで、よりしっかりとオブジェクト指向的な設計ができるようになるため、コードベースが増えていっても複雑さを一定に保ったままで開発が進めていけるのでは…と思います。