akiyoko blog

akiyoko の IT技術系ブログです

Backbone.js を基礎からやってみよう

f:id:akiyoko:20150807012354j:plain

JavaScript製の MV*フレームワークと言えば、世間では「React.js」や「AngularJS」などの SPA (Single Page Application) が話題を集めていて、「Backbone.js」と言うと今更感がありますが、黒魔術の少ない薄いラッパーとしての Backbone.js は SPA の学習教材として最適なのではないかと考え、スキルセットの一つとして基礎からやってみようと思い立ちました。


・・と思い立ったのが、半年ほど前。
書き途中のまま止まっていたこの記事を最近発見したので、ひとまず最後まで書いてアップしてみることにしました。


基本は、

に沿って学習しました。
なお、古い情報がいろいろあったので、2013年以降の情報を中心にまとめています。





1. JavaScript

まずは、基礎中の基礎、JavaScriptの仕組みをしっかり理解するところから始めてみました。このスライドが非常に分かりやすかったです。


【学んだこと】

  • JavaScript にはクラスが無い(オブジェクトが全て)
  • プロトタイプチェイン
    • 自分が備えていない特性(機能や属性)を別のオブジェクトに委譲
var objA = {
    name: 'Hoge',
    say: function() {
        alert('My name is ' + this.name);
    }
}

var objB = {name: 'Fuga'};
objB.__proto__ = objA;

var objC = {};
objC.__proto__ = objB;

objC.say();
  • オブジェクトの生成例
// コンストラクタ関数の定義
var Person = function(name) {
    this.name = name;
}

// prototypeの拡張
Person.prototype.say = function() {
    alert('My name is ' + this.name);
}

// 利用
var person = new Person('Hoge');  // new演算子によって、person.__proto__ に Person.prototype が代入される!
person.say();
  • スコープチェイン(グローバルスコープとローカルスコープ)
    • 関数の中で作成された変数は、その関数の内側のみで参照できる
    • 関数の引数も、その関数の内側のみで参照できる
  • クロージャ
    • 引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする
    • 変数の隠蔽、別名の利用
  • this
    • this は呼び出し時に決定される
    • 関数呼び出し時に、その関数が所属していたオブジェクトが this
    • 関数はオブジェクトに束縛されない(呼び出し時に束縛される)
    • this を操ることができる call(), apply(), bind() メソッド(関数をオブジェクトに束縛できる)
person.sayHello();  // this => person
sayHello();  // this => global object



 

2. Backbone.js 概要

まずは、このスライドを流し見。



ついでに、「Backbone.js で MVCパターン」という記事が載っている本が手元にあったので、ざっと読んでおきました。

JavaScript徹底攻略 (WEB+DB PRESS plus)

JavaScript徹底攻略 (WEB+DB PRESS plus)

  • 作者: 沖林正紀,吾郷協,高橋征義,名村卓,桜井雅史,縣俊貴,太田昌吾,天野祐介,飯塚直,佐藤鉄平,冨田慎一,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2013/01/26
  • メディア: 大型本
  • 購入: 7人 クリック: 69回
  • この商品を含むブログ (6件) を見る

概要から始まり、Model, Collection, View までが 7ページほどで簡潔にまとめられていました。


 

【学んだこと】

  • Backbone.js は JavaScript製のクライアントサイド MV*フレームワーク
    • Controller の役割が薄く、View と Model が強く結びついている
  • イベント駆動でオブザーバ・パターンなクライアントアプリを作成できる
    • イベント駆動とは、「未来に起こるイベントに対して、処理を登録しておくプログラミング手法」
  • Backbone.js は Underscore.js に依存、部分的に jQuery に依存
  • Backbone なオブジェクトたちとその役割は下図

 

オブジェクト 役割
Model key-value 型のデータを保持。JSON API を介してデータの CRUD を処理
Collection モデルの集合
View モデルの変更や DOM のイベントを監視して、DOM を操作
Router URL の監視(ハッシュフラグメントの管理)
History Router の履歴監視


 

3. Underscore.js入門 (全10回) – dotinstall

ドットインストールで基礎から学習。
まずは、Backbone.js が依存しているユーティリティライブラリの「Underscore.js」から。

【学んだこと】

  • Underscore.js は便利ツール
  • Backbone.js では template 等で利用している

 

4. Backbone.js入門 (全22回) – dotinstall

全22回と少し長いですが、Backbone.js の解説動画もひと通り見ておきます。

【学んだこと】

  • シンプルな Backbone.js アプリのハンズオン
  • Model, Collection, View の作り方(Router, History とかはやらない)


作成したコードはこちら ↓

$ tree backbonejs-dotinstall/
backbonejs-dotinstall/
├── index.html
└── js
    ├── app.js
    ├── backbone.js
    ├── jquery-2.1.1.min.js
    └── underscore.js

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8" />
    <title>Backbone.js</title>
    <style>
        .completed {
            text-decoration: line-through;
            color: gray;
        }
    </style>
</head>
<body>
    <h1>Tasks</h1>

    <form id="addTask">
        <input type="text" id="title">
        <input type="submit" value="add">
        <span id="error"></span>
    </form>

    <div id="tasks">
    </div>

    <p>Tasks left: <span id="count"></span></p>

    <script type="text/template" id="task-template">
        <input type="checkbox" class="toggle" <%= completed ? 'checked' : '' %>>
        <span class="<%= completed ? 'completed' : '' %>">
            <%- title %>
        </span>
        <span class="delete">[x]</span>
    </script>

    <script src="js/underscore.js"></script>
    <script src="js/jquery-2.1.1.min.js"></script>
    <script src="js/backbone.js"></script>
    <script src="js/app.js"></script>
</body>
</html>

js/app.js

(function() {

var Task = Backbone.Model.extend({
    defaults: {
        title: 'do something',
        completed: false
    },
    validate: function(attrs) {
        if (_.isEmpty(attrs.title)) {
            return 'Title must not be empty.';
        }
    },
    initialize: function() {
        this.on('invalid', function(model, error) {
            $('#error').html(error);
        });
    }
});
var Tasks = Backbone.Collection.extend({model: Task});

var TaskView = Backbone.View.extend({
    tagName: 'li',
    initialize: function() {
        this.model.on('destroy', this.remove, this);
        this.model.on('change', this.render, this);
    },
    events: {
        'click .delete': 'destroy',
        'click .toggle': 'toggle'
    },
    toggle: function() {
        this.model.set('completed', !this.model.get('completed'));
    },
    destroy: function() {
        if (confirm('Are you sure?')) {
            this.model.destroy();
        }
    },
    remove: function() {
        this.$el.remove();
    },
    template: _.template($('#task-template').html()),
    render: function() {
        var template = this.template(this.model.toJSON());
        this.$el.html(template);
        return this;
    }
});
var TasksView = Backbone.View.extend({
    tagName: 'ul',
    initialize: function() {
        this.collection.on('add', this.addNew, this);
        this.collection.on('change', this.updateCount, this);
        this.collection.on('destroy', this.updateCount, this);
    },
    addNew: function(task) {
        var taskView = new TaskView({model: task});
        this.$el.append(taskView.render().el);
        $('#title').val('').focus();
        this.updateCount();
    },
    updateCount: function() {
        var uncompletedTasks = this.collection.filter(function(task) {
            return !task.get('completed');
        });
        $('#count').html(uncompletedTasks.length);
    },
    render: function() {
        this.collection.each(function(task) {
            var taskView = new TaskView({model: task});
            this.$el.append(taskView.render().el);
        }, this);
        this.updateCount();
        return this;
    }
});

var AddTaskView = Backbone.View.extend({
    el: '#addTask',
    events: {
        'submit': 'submit'
    },
    submit: function(e) {
        e.preventDefault();
        //var task = new Task({title: $('#title').val()});
        var task = new Task();
        if (task.set({title: $('#title').val()}, {validate: true})) {
            this.collection.add(task);
            $('#error').empty();
        }
    }
});

var tasks = new Tasks([
    {
        title: 'task1',
        completed: true
    },
    {
        title: 'task2'
    },
    {
        title: 'task3'
    }
]);

var tasksView = new TasksView({collection: tasks});
var addTaskView = new AddTaskView({collection: tasks});

$('#tasks').html(tasksView.render().el);

})();


f:id:akiyoko:20140814065219p:plain


 

5. 試して学ぶ Backbone.js 入門のまとめ


前半は ~4. までの復習として。後半は、これまであまり触れられてこなかったモデルのCRUD処理や Router 等を学習することができました。


 

【学んだこと】

  • CollectionにもUnderscore.jsの便利メソッドが使われている(each, find, filter, where とか)
  • ModelとCollectionの非同期RESTful JSONインタフェース
  • MongoDB使うときは、ModelのidAttributeを"_id"に
  • CRUDを順次実行したいときは、戻り値のDeferredオブジェクトのpipe、doneメソッドを使う
  • (その他)JSON.stringify(objs)すると、デバッグしやすい
  • Modelにon()でモデルに対する変更の監視(add, change, remove)
  • Viewは必ず一つのDOMと対応づける必要がある。
  • RouterはHistoryAPIをサポートしているブラウザでは”きれいな”URLを使うことができ、サポートしていないブラウザではhash fragmentsを使うという、どちらの状況にも対応したルーティング機能を提供


 

6. TODOアプリ(デモ)を読み解く

Backbone.js • TodoMVC
f:id:akiyoko:20150806020548p:plain
のデモを実際に触りながら、ソースコードを読み解いていきます。


ソースコードは GitHub でホスティングされています。


そのほか、Backbone.js でダウンロードしたパッケージに含まれているサンプルアプリのコードを読むのも勉強になると思います。



参考



 

7. 本を読む

Backbone.js の書籍と言えば、この「Backbone.jsガイドブック」一択しかないと思っていたのですが、

Backbone.jsガイドブック

Backbone.jsガイドブック


入門Backbone.js (Programmer's SELECTION)

入門Backbone.js (Programmer's SELECTION)

という本も出ていたのですね。知りませんでした。。



Backbone.jsガイドブック」の方は紙の本で購入していたのですが、まどろっこしい(長ったらしい?)解説が多くて途中でギブアップ。Amazon のレビューもあまり良くないようですね。


今なら、

などのブログ記事の方が効率的に学習できるかもしれません。




8. リファレンス

Backbone.js 公式サイト のリファレンスをチェックし、細かな知識をフォローします。

f:id:akiyoko:20150807004728p:plain



 

9. Yeomanで試す

ここからはまだ試せていませんが、Marionette.js や単体テスト用のプラグインを連携させる場合には、Yeoman を使ってプロジェクトを一気に作り上げてしまうというお手軽な方法もあります。


http://yeoman.io/generators/
f:id:akiyoko:20150807005825p:plain

このページで「backbone」と検索することで簡単に見つけられるのですが、Yeomanチーム謹製の「generator-backbone」のほか、

がワンセットになった「generator-marionette」というジェネレータもあるようです。



Marionette.js に関しては、こちらの記事(generator-marionette を使ったチュートリアル)が詳しくて分かりやすいです。

 

まとめ

Backbone.js は薄いラッパーであるが故にコードの見通しが良く、学習コストが低くなる分、簡単な動きを実装するだけでも冗長なコードを記述する必要が出てきます。それを良しとするかどうかはアプリの特性にも依るので置いておくとしても、SPA (Single Page Application) を理解するためのベースとしては大きな意義があるかと思います。