話題の記事

RequireJS+Backbone.jsでモジュール管理されたWebアプリ開発

2013.02.27

Todoアプリもどきを作る

今回はRequireJSの理解を深めるため、Backbone.jsと組み合わせてTodoを追加するだけの簡素なデモを作ってみました。
Backbone.jsについても触れているため、記事が少し長いです。
お急ぎの方はページ下部にデモとサンプルコードがあるので、そちらをさくっとご確認ください。

RequireJS:
http://requirejs.org/
RequireJS API:
http://requirejs.org/docs/api.html
Backbone.js:
http://backbonejs.org/

なにができるの?

Webアプリを制作する際に、RequireJSを使ってBackbone.jsで構成されるModelやViewをモジュール化することで、開発時の管理コスト削減を目指します。
なお、モジュール化するとファイル数が増えて読み込みのコストが上がるため、リリース時には 「 最適化(optimize) 」 を推奨です。
ここでいう最適化っていうのは、要するに「ちぢめる、まとめる、かためる(minify, combine, compress)」のことだと受け止めてください。

RequireJSって?

RequireJSについてはざっくりとではありますが、開発ブログで簡単なデモを作った記事を掲載しています。
全然知らないよって方は、素直にググるかこちらを参考にしてください。

Backbone.jsって?

Backbone.jsについては情報が古いものもありますが、こちらも開発ブログ内で何度か他のメンバーが触れています。
こっちも全然知らないよって方は、素直にググるかこちらを(ry

使ってみた

まだBackbone.jsに慣れてないよって方は、以下の記事がオススメです。
僕も今回のデモのベースとして、掲載されているサンプルコードを パクって 参考にしています。
※CoffeeScriptを使わずに書いているコードも記述されています。

構成など

  • RequireJS v2.1.4
  • Backbone.js v0.9.10
  • Underscore.js v1.4.4
  • jQuery v1.8.3
  • Twitter Bootstrap v2.2.2(bootstrap.min.cssのみ利用)

Backbone.jsがUndersocre.js( Lo-Dash )とjQuery( Zepto.js )に依存しています。

※Underscore.jsは"utility-belt library"と呼ばれる、便利な関数群を提供するライブラリです。
(ユーティリティベルトって言葉に馴染みがなかったので適当にググってみましたが、バットマンの画像がたくさん出てきました、、
「小さい便利グッズをまとめた1本のベルトみたいなライブラリだぜひゃっはー!」ってことなんでしょうか。
無理に日本語におきかえて認識するのはやめて"utility-belt"という言葉で認識するのが正しそうです。)
※Lo-DashはUndersocre.jsと互換性があり軽量化されたライブラリです。
※Zepto.jsはjQueryと互換性があり軽量化されたライブラリです。

ダウンロードページ

Download RequireJS:
http://requirejs.org/docs/download.html#requirejs
Backbone.js
http://backbonejs.org/
Underscore.js:
http://underscorejs.org/
Download jQuery | jQuery:
http://jquery.com/download/
Twitter Bootstrap:
http://twitter.github.com/bootstrap/

それぞれダウンロードし、今回はこのように配置します。一部必要に応じてリネームしています。
*印のついたファイルは新規作成したファイルです。

ファイル構成

ちょっと数が多いので、依存ライブラリも含めた「 ソース一式(zip) 」を置いておきます。

assets/
  js/
    libs/
      backbone-min.js
      jquery-1.8.3.min.js
      underscore-min.js

    collections/
      todos.js *

  models/
      todo.js *

    views/
      todo.js *
      todos.js *
      
    require.min.js
    app.js *

  css/
    bootstrap.min.css

index.html *

なお、index.htmlは「 Bootstrap, from Twitter 」に記載されているサンプルをベースに作成しました。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    ...
    <link href="assets/css/bootstrap.min.css" rel="stylesheet">...</head>
<body>
    <!-- navbar -->
    ...
    <!-- /navbar -->

    <!-- container -->
    <div class="container">
        <div id="todoListView">
            <form onSubmit="return false;">
                <input type="text" id="inputTodo" placeholder="TODOを入力">
                <button id="addTodo" style="display: none;">登録</button>
            </form>
            <ul id="todoList"></ul>
        </div>
    </div>
    <!-- /container -->

    <!-- requirejs -->
    <script type="text/javascript" src="assets/js/require.min.js" data-main="assets/js/app.js"></script>
    <!-- /requirejs -->
</body>
</html>

Backbone.jsのViewで利用するid属性の設定に注意します。

assets/model/todo.js

define(["backbone"], function() {
    /**
     * TodoのModel
     * Backbone.Modelを継承
     *
     * @class TodoModel
     */
    var TodoModel = Backbone.Model.extend({

        // 初期値の設定
        defaults: {},

        // インスタンス生成時に実行
        initialize: function() {
            console.dir("[Model]TodoModel::initialize()");
        },

        // バリデーションを定義
        validate: function(attrs) {
            console.dir("[Model]TodoModel::validate()");
            var ret = "";

            if(_.isEmpty(attrs.content)) {
                ret = "入力してください。";
            }

            return ret;
        }
    });

    // モジュールのModelを返す
    return TodoModel;
});

assets/collection/todos.js

define(["models/todo", "backbone"], function(TodoModel) {
    /**
     * TodoのCollection
     * Todoの集合を表すため、Backbone.Collectionを継承
     *
     * @class TodoCollection
     */
    var TodoCollection = Backbone.Collection.extend({
        // 依存関係で指定したTodoのModelを指定
        model: TodoModel
    });

    // モジュールのCollectionを返す
    return TodoCollection;
});

assets/views/todo.js

define(["backbone"], function() {
    /**
     * Todoを表示するためのView
     *
     * @class TodoView
     */
    var TodoView = Backbone.View.extend({
        // レンダリングの際に自動で挿入されるタグ(デフォルトは<div>)
        tagName: "li",

        // イベントの登録
        events: {},

        // インスタンス生成時に実行
        initialize: function() {
            console.log("[View]TodoView::initialize()");
        },

        // レンダリング
        render: function() {
            // この時点ではまだ画面には描画されない。
            $(this.el).html(this.model.get("content"));

            // 呼び出し元でメソッドチェーンが使えるようにthisを返す。
            return this;
        }
    });

    // モジュールのViewを返す。
    return TodoView;
});

assets/views/todos.js

define(["models/todo", "collections/todos", "views/todo", "backbone"], function(TodoModel, TodoCollection, TodoView) {
    /**
     * Todoリストを表示するためのView
     *
     * @class TodoListView
     */
    var TodoListView = Backbone.View.extend({

        // このViewで管理する要素を指定する。
        el: "#todoListView",

        // イベントの関連付け。
        // "el"で指定した要素内に対して行う。
        events: {
            // clickイベントを設定
            "click button#addTodo": "addTodo"
        },

        // インスタンス生成時に実行
        initialize: function() {
            console.dir("[View]TodoListView::initialize()");

            // Collectionのインスタンスを生成
            this.collection = new TodoCollection();

            // collectionに対し、addイベントが発生したらrenderを実行するよう設定
            this.collection.on("add", this.render);

            $("#addTodo").show();
        },

        // レンダリング
        render: function(todo) {
            console.dir("[View]TodoListView::render()");

            // 1つのTodoを表すViewのインスタンスを生成
            var view = new TodoView({
                model: todo
            });

            $("#todoList", this.el).append(view.render().el);
        },

        // 「登録」ボタンがクリックされた時に実行
        addTodo: function() {
            var todo, input;

            console.dir("[View]TodoListView::addTodo()");

            // modelのインスタンスを生成
            todo = new TodoModel();

            // バリデーション失敗時に"invalid"イベントが発生したらonErrorを実行するよう設定
            // v0.9.10では"error"ではなく"invalid"イベントが本体で設定されています。
            todo.on("invalid", this.onInvalid);

            input = $("#inputTodo");

            // 入力された内容をmodelにセット
            // {options}の指定でvalidateを有効に設定
            todo.set({
                content: input.val()
            }, {
                validate: true
            });

            console.dir('[Model]TodoListModel::isValid()');
            if(todo.isValid()) {
                // collectionにmodelを追加
                this.collection.add(todo);
            }

            // 入力内容をリセット
            input.val('');

            console.dir("--End--");
        },

        // バリデーションエラー
        onInvalid: function(model) {
            console.dir('[View]TodoListView::onInvalid()');
            alert(model.validationError);
        }
    });

    // モジュールのViewを返す
    return TodoListView;
});

assets/js/app.js

requirejs.config({
    // 読み込みのベースUrlを設定する。
    baseUrl: "assets/js",

    // pathsオプションの設定。"module/name": "path"を指定します。拡張子(.js)は指定しない。
    paths: {
        "jquery": "libs/jquery-1.8.3.min",
        "underscore": "libs/underscore-min",
        "backbone": "libs/backbone-min"
    },

    // shimオプションの設定。モジュール間の依存関係を定義。
    shim: {
        "underscore": {
            exports: "_"
        },
        "backbone": {
            deps: ["jquery", "underscore"],
            exports: "Backbone"
        }
    }
});

// require(["module/name", ...], function(params){ ... });
require(["views/todos", "backbone"], function(TodoListView) {

    console.dir("--Start--");

    // 最初にTodoリストのViewインスタンスを生成
    var app = new TodoListView();
});

デモ

テキスト入力欄に文字を入力し、 Enter キーか「登録」するボタンをクリックで下に追加されていきます。
なお、入力値チェックは空値のみになっていますがご容赦ください。
また、開発者ツールのコンソールで登録処理の流れを表示していますので参考になるかと思います。

僕自身、勉強しながら記事を書いているので、もっとこうした方がいいよ的な実装があればPull Requestなどでご指摘いただけるとうれしいです。もちろん日本語でOKです。

まとめ

それぞれが役割ごとにモジュール化されているため、規模に比例し再利用性メンテナンス性において有意義なものになりそうですね。
Backbone.js本体もあくまで1500行程度で書かれた枠組みと便利な関数を提供してくれているシンプルなライブラリなので、導入コストも低いかと思います。
とはいえ、どんな道具でも上手く使いこなすためにはそれなりに知識が必要ですよね。そこは素直に勉強あるのみです。

弊社でも近々イベント「 クラスメソッドブログ課外授業6日目 」でBackbone.jsについて触れる予定ですが、この記事を書いている2013年2月25日現在でキャンセル待ち状態です。
今後も様々なイベントが催されると思いますので、ぜひTwitterで @classmethod をフォローしてみてください(営業的な何か)

ついでに僕と秋葉原で一緒に勉強してくれる 大きい お友達も募集しています。

参考

下記、とても勉強になりました。ありがとうございました!