最速フレームワーク Mithril 入門

 みなさん初めまして、DeNA Games Osaka 技術編成部のさいです。DeNA って大阪にも拠点があるんですよ。今後ともよろしくおねがいしますね!

 

 さて、私は普段サーバーサイドのエンジニアをやっているんですけれども、フロントエンドの JavaScript も勉強中です。今日は、フロントエンドの JavaScript のフレームワーク、「Mithril」についてご紹介させていただきますね。

 

Mithril とは

 Mithril とはクライアントサイドのMVCフレームワークです。2014年にLeo Horie氏によって公開され、現在も絶賛開発が進められています。(2016/06 現在、バージョンは0.2.5 です)

 

Mithril の特徴

軽い

 Mithril のファイルサイズですが、バージョン0.2.5 時点で、約 20 KBです。React が約 147 KB(v15.1.0時点)、Angular 2が約 639 KB(v2.0.0-rc.2時点)なので、他のJavaScript フレームワークと比較しても圧倒的に小さいです。

高速

 他のフレームワークに比べて、圧倒的に処理速度/レンダリング速度共に速いです。(公式より引用)

※画像は公式サイトよりお借りしました。

画像は公式サイトよりお借りしました。

学習コストが低い

 Mithril フレームワークのAPI は 23 個ほどしかありません。主要なAPIは4個ほどです。これだけの API ですが、他のライブラリを使わずとも Mithril だけでほぼ完結できるほどの機能が一通りそろっています。

 またコード行数が1000行ほどと、JavaScript に不慣れな人でも、いざというときにフレームワークの実装を読んでいける量なのも良い点です。

MVC

React は View のみのライブラリですが、Mithril は一通りの MVC の機能を揃えています。 もちろん、コンポーネント機能もサポートしています。

仮想 DOM

 Mithril はレンダリングに仮想DOMを採用しています。これまで jQuery 等では$('ul').append('<li>ToDo</li>'); のようにDOMを追加/変更/削除する処理を記述することで、描画を行ってきましたが、Mithril においては完成品のあるべきDOMの姿を記述することで、変更があった差分だけ DOM を追加/変更/削除してくれます。

 Mithril を触ってみて驚いたのは、サーバーサイドのMVCと驚くほど親和性が高いことです。また、ファイルサイズが小さくて、描画も高速なので、スマートフォンなどの通信環境が貧弱で、ブラウザのパフォーマンスが気になる環境で有効です。

 注意点として、indexOf, map, keys などの関数が使われているため、古い Android 端末に対応するには、es5-shim.js(https://github.com/es-shims/es5-shim) などを読み込む必要があります。

 

ToDo アプリを作ってみる

 実際に ToDo アプリを作ってみて、Mithril の便利さを体感してみましょう!最終的にこんな感じの ToDo アプリを作ります。

まずこんな感じの HTML を用意します。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ToDo App by mithril.js</title>
</head>

<body>

<div id="root"></div>

<script src="./mithril.js"></script>
<script src="./app.js"></script>
</body>
</html>

mithril.js はあらかじめダウンロードしておいてください。app.js に ToDo アプリを書いていきましょう!

 

コンポーネントを作る

コンポーネントを作ります。

   // コンポーネント
    var ToDoComponent = {
        controller: ToDoController,
        view: ToDoView
    };

 Mithril におけるコンポーネントとは、コントローラーとビューの組み合わせです。これを m.mountすることによって先ほどのHTMLの <div id="root"></div> に紐付けます。

m.mount(document.getElementById("root"), ToDoComponent);

 これによって、ToDoComponent が描画した内容が、id="root" の中に描画されます。このままでは、ToDoController も ToDoView も実装していないので何も表示されません。引き続き、コントローラーとビューを作っていきましょう。

 

コントローラーを作る

コントローラーを作ります。以下のコードを ToDoComponent より前に記述してください。

   // コントローラー
    var ToDoController = function () {
        var self = this;
        // タスク一覧
        self.todo_list = [];
        // 入力中の新規タスク
        self.title = m.prop('');
    };

 Mithril におけるコントローラーは、インスタンスを作るクラスです(JavaScript は「クラス」という言語仕様が無いので正確な意味でのクラスとは異なるのですが……)

 ページ遷移してきた最初に Component に登録されたコントローラークラスから、Mithril によって勝手にコントローラーインスタンスが作成されます。以降は、何回再描画が発生しても、最初に作ったコントローラーインスタンスが使われます。

 ToDoController は2つのプロパティを持つことにします。todo_list は登録したタスク一つ一つを 配列に入れたものです。title はタスクを新規追加する際に、現在フォームに入力しているタスク名のことです。m.prop は getter/setter を作ってくれる Mithril の関数です。

 title プロパティについては、ユーザーの入力に合わせて中身も変わる(データバインディング)ので、また ToDoView を作成する際にご説明させていただきます!

 

ビューを作る

ビューを作ります。以下のコードを ToDoComponent より前に記述してください。

var ToDoView = function (ctrl) {
    return [
        m('form', {onsubmit: ctrl.add()},
            m('input', {onchange: m.withAttr("value", ctrl.title), value: ctrl.title()}),
            m('button[type=submit]', {onclick: ctrl.add()}, 'Add')
        )
    ];
};

 ビューはただの関数です。描画のたびに実行されます。描画のたびに実行されるので、リソースの初期化や、サーバーとの通信等の処理は、ビューではなくコントローラー側で行うようにしましょう。引数 ctrl はコントローラーインスタンスのことです。

 さて、m() というのが出てきました。Mithril は仮想DOMなので、DOMをJSで書きます。仮想DOMを生成してくれるのが、この m() という関数です。

 生成している仮想DOMの内容は下記と一緒ですね。

<form onsubmit="~">
<input onchange="~" value="~" />
<button type="submit" onclick="~">Add</button>
</form>

 ただ JavaScript で書く DOM ということで、ちょくちょく JavaScript のコードが混じってます。それでは1つずつ解説していきます。

onsubmit: ctrl.add()

 onsubmit した時に ctrl のインスタンスメソッドである add() によって生成された関数を実行します。add についてはまだ未実装なので、のちのちまた説明させていただきます!

m('button[type=submit]', {onclick: ctrl.add()}, 'Add'))

 こちらも onclick された時に ctrl.add() によって生成された関数を実行するということですね。

m('input', {onchange: m.withAttr("value", ctrl.title), value: ctrl.title()})

 こちらですが、まず value の方を見てみましょう。先ほど ToDoController に title プロパティを実装したと思いますが、そちらの中身を ` の value に定義しています。

 onchange のところで、 m.withAttr("value", ctrl.title) というのを設定しています。これは、onchange するたびに input タグの value 属性の値と ctrl.title の値を同期させるという意味です。データバインディングですね。

 これによって、ユーザーがこの input タグの value の値が変更すると、ctrl.title の値が自動で変更され、またプログラムが ctrl.title の値を変更すると、ユーザーが見える input タグの value の値も自動で変更されるようになりました。

 この時点ではまだ何も表示されないかと思います。 add メソッドを実装していきましょう!

 

add メソッドの実装

 先ほど実装していなかった、ToDoController の add メソッドを実装します。

   ToDoController.prototype.add = function() {
        var self = this;
        return function (e) {
            // submit 実行を無効
            e.preventDefault();

            // ユーザーの入力した値を取得
            var val = self.title();
            if (!val) return;

            // ToDoを追加
            var new_todo = new ToDoModel({title: val});
            self.todo_list.push(new_todo);

            // ユーザーの入力した値をクリア
            self.title('');
        };
    };

 add メソッドは、関数を返す関数です!実際にユーザーが add ボタンを押した時に実行される関数は、 return している function(e) { ~ } です。

 return している function の中を見ていきましょう。

           // ユーザーの入力した値を取得
            var val = self.title();

 こちらでユーザーが入力した値を取得しています。ToDoView の中の m.withAttr のおかげで、ユーザーの入力した値と self.title() の値が自動で紐付いています。

           // ToDoを追加
            var new_todo = new ToDoModel({title: val});
            self.todo_list.push(new_todo);

 新しいToDoModel を一つ生成して、ToDoController インスタンスの todo_list に追加しています。そして最後にユーザーが入力した値をクリアします。

 さてここまで実装すると、HTML にフォームが表示されるようになったと思います。

ToDoModel はまだ未実装でしたね。次でご説明しましょう。

 

ToDoModel

   var ToDoModel = function(data) {
        // タスク名
        this.title    = m.prop(data.title);
    };

 ToDoModel はインスタンスを作るクラスです。インスタンス1つ = タスク1つというふうにしましょう。ToDoModel インスタンスはプロパティを2つ持ちます。タスク名の title と、タスクが完了したか否かの is_done ですね。m.prop は、先ほどToDoController のところでも解説しました、getter/setter を作る関数です。

 これで add ボタンを押下すると、タスクが追加されるようになりました。しかし追加したタスクを描画する処理を実装していません。

 

追加したタスクを描画

   var ToDoView = function (ctrl) {
        return [
            m('form', {onsubmit: ctrl.add()},
                m('input', {onchange: m.withAttr("value", ctrl.title), value: ctrl.title()}),
                m('button[type=submit]', {onclick: ctrl.add()}, 'Add')),
            // ここが追加したところ
            m('table', [ctrl.todo_list.map(function(todo) {
                return m('tr', [
                    m('td', todo.title())
                ]);
            })])
        ];
    };

 先ほど実装した ToDoView を上記のように修正します。ToDoController インスタンスの todo_list 配列の中を、 map を使って走査しています。todo は ToDoModel のインスタンスのことですね。

 これで新規タスクを入力して add を押すと、タスクが追加されるようになったと思います。

タスクの完了/未完了も実装

タスク一つ一つに、タスクが完了したのかまだ未完了なのか設定できるようにしたいと思います。

   // モデル
    var ToDoModel = function(data) {
        this.title    = m.prop(data.title);
        this.is_done  = m.prop(data.is_done); // 追加
    };

まずは ToDoModel のプロパティにタスクが完了/未完了なのかを表す is_done プロパティを追加します。

   var ToDoView = function (ctrl) {
        return [
            m('form', {onsubmit: ctrl.add()},
                m('input', {onchange: m.withAttr("value", ctrl.title), value: ctrl.title()}),
                m('button[type=submit]', {onclick: ctrl.add()}, 'Add')),
            m('table', [ctrl.todo_list.map(function(todo) {
                return m('tr', [
                    // ここが修正部分
                    m('td', [
                        m('input[type=checkbox]', {onclick: ctrl.update_is_done(todo), checked: todo.is_done()})
                    ]),
                    m('td', {style: {textDecoration: todo.is_done() ? 'line-through' : 'none'}}, todo.title())
                ]);
            })])
        ];
    };

 ToDoView にチェックボックスを追加します。style を使って、todo.is_done() の値によって、取り消し線を入れるかどうか設定しています。

 ToDoController の update_is_done メソッドを下記のように実装します。

   ToDoController.prototype.update_is_done = function(todo) {
        var self = this;
        return function (e) {
            var val = todo.is_done();

            // is_done の true/false を反転
            todo.is_done(!val);
        };
    };

 update_is_done メソッドも、add メソッドと同じように関数を返す関数です。実際にチェックボックスがユーザーによってチェックされた時はこの、return された関数が実行されます。

 これによって、チェックボックスを選択すると、自動でタスクに取り消し線が引かれ、チェックボックスを解除すると、取り消し線も削除されるようになったと思います。

完成

 最終的なコードです

'use strict';
// コントローラー
var ToDoController = function () {
    var self = this;
    // タスク一覧
    self.todo_list = [];
    // 入力中の新規タスク
    self.title = m.prop('');
};
ToDoController.prototype.add = function() {
    var self = this;
    return function (e) {
        // submit 実行を無効
        e.preventDefault();

        // ユーザーの入力した値を取得
        var val = self.title();
        if (!val) return;

        // ToDoを追加
        var new_todo = new ToDoModel({title: val});
        self.todo_list.push(new_todo);

        // ユーザーの入力した値をクリア
        self.title('');
    };
};
ToDoController.prototype.update_is_done = function(todo) {
    var self = this;
    return function (e) {
        var val = todo.is_done();

        // is_done の true/false を反転
        todo.is_done(!val);
    };
};
// モデル
var ToDoModel = function(data) {
    this.title    = m.prop(data.title);
    this.is_done  = m.prop(data.is_done); // 追加
};

var ToDoView = function (ctrl) {
    return [
        m('form', {onsubmit: ctrl.add()},
            m('input', {onchange: m.withAttr("value", ctrl.title), value: ctrl.title()}),
            m('button[type=submit]', {onclick: ctrl.add()}, 'Add')),
        m('table', [ctrl.todo_list.map(function(todo) {
            return m('tr', [
                // ここが修正部分
                m('td', [
                    m('input[type=checkbox]', {onclick: ctrl.update_is_done(todo), checked: todo.is_done()})
                ]),
                m('td', {style: {textDecoration: todo.is_done() ? 'line-through' : 'none'}}, todo.title())
            ]);
        })])
    ];
};  

// コンポーネント
var ToDoComponent = {
    controller: ToDoController,
    view: ToDoView
};

m.mount(document.getElementById("root"), ToDoComponent);

まとめ

 100行にも満たないコードでToDoアプリを実装することができました。何より仮想DOMのおかげで、表示したいことのあるべき姿だけ、View に実装すればよくなったのが、すごく設計しやすいと思います。

皆さんもぜひ、一度 Mithril を触ってみてください!