Backbone.js のはなし

香川 周平 @ M3 Teck Talk #7

Backbone.js とは?

メリット

フロントエンドの JavaScript をきれいに書ける。

他のフレームワークとどう違う?

Size (min) Data Binding Components Testing Support
Backbone.js 6.3 kb
Angular.js 81 kb x x x
Ember.js 56 kb x x x

Backbone.js は圧倒的に軽量。機能は少ない。

フェラーリと自転車を比べるようなもの・・・。

Backbone なしでできないことが、Backbone でできるようになるわけではない。

主なコンポーネント

MVC?

それから・・・

Underscore.js

Backbone.js が依存しているユーティリティライブラリ。豊富なコレクション操作など、かゆいところに手が届く。

_.each([1, 2, 3], function (num, i) { console.log(num); });

_.map([1, 2, 3], function (num) { return num * num; });
// [1, 4, 9]

_.groupBy([18, 23, 34, 30, 12, 43], function (age) {
  return Math.floor(age / 10) * 10;
});
// { 10: [18, 12], 20: [23], 30: [34, 30], 40: [43] }

テンプレート関数もあり。

var compiled = _.template('Hello, <%= name %>!');
var result = compiled({ name: 'World' });
// Hello, World!

Backbone.Events 1

Object にイベント機能を付加する。

var obj = _.extend({}, Backbone.Events);
obj.on('greeting', function (message) {
  console.log([message, message, message].join(' ') + '!');
});
obj.trigger('greeting', 'hello');
// hello hello hello!

obj.off('greeting');
obj.trigger('greeting', 'ciao');
// ...

Backbone.Events 2

主従が逆のメソッドも。メモリーリーク対策にも。

var another = _.extend({
  reverse: function (message) {
    console.log(message.split('').reverse().join('')); 
  }
}, Backbone.Events);
another.listenTo(obj, 'greeting', another.reverse); // bind いらず!
// obj.on('greeting', _.bind(another.reverse, another));

obj.trigger('greeting', 'Bonjour');
// ruojnoB

another.stopListening();
// obj から another.reverse への参照を能動的に消すことができる。

Backbone.Model 1

何を入れる?

Backbone.Model 2

var Background = Backbone.Model.extend({
  promptColor: function () {
    var cssColor = prompt('CSS 色を入力してください。');
    this.set('color', cssColor);
  },
  turnBlack: function () {
    this.set('color', '#000');
  }
});
var bg = new Background();
bg.on('change:color', function (model, color) {
  $('section').css('backgroundColor', color);
});

$('#prompt-color').on('click', _.bind(bg.promptColor, bg));
$('#turn-black').on('click', _.bind(bg.turnBlack, bg));

Backbone.Collection 1

Model の集合を管理する。追加、削除時にイベントを発火。

var Book = Backbone.Model.extend({});
var Library = Backbone.Collection.extend({
  model: Book
});

var books = new Library();
books.on('add', function (book) {
  console.log('新しく「' + book.get('title') + '」が到着しました。');
});
books.add([
  { title: '羅生門 2', author: '芥川龍之介', published: false },
  { title: '坊ちゃん', author: '夏目漱石', published: true }
]);
// 新しく「羅生門 2」が到着しました。
// 新しく「坊ちゃん」が到着しました。

Backbone.Collection 2

Underscore.js の collection メソッドが利用可能。.

var authors = books.map(function (book) {
  return book.get('author');
});
// ["芥川龍之介", "夏目漱石"]

var publishedBooks = books.filter(function (book) {
  return book.get('published') === true;
});
// '「坊ちゃん」だけ'

Backbone.Sync

Model/Collection をバックエンドと同期する仕組み。Backbone.sync を書き換えれば、REST API や local storage など、好きなバックエンドを選択できる。

var book = new Backbone.Model({
  title: 'The Rough Riders',
  author: 'Theodore Roosevelt'
});

book.save();
// create: {"title":"The Rough Riders","author":"Theodore Roosevelt"}

book.save({author: 'Teddy'});
// update: {"title":"The Rough Riders","author":"Teddy"}

Backbone.View 1

コードというより、規約。

DOM と Model/Collection をつなぐ

DOM 関連のコードを整理する

Backbone.View 2

var DocumentView = Backbone.View.extend({
  tagName: 'li',
  className: 'document-row',
  events: {
    'click .icon'         : 'open',
    'click .button.edit'  : 'openEditDialog',
    'click .button.delete': 'destroy'
  },
  initialize: function() {
    this.listenTo(this.model, 'change', this.render);
  }
  render: function() { /* ... */ });
});
var doc = new Document({ title: 'Hello, World!', content: '...' });
var docView = new DocumentView({ model: doc });

テンプレート

まずは Underscore.js 標準のものが簡単。JST や Hanlderbars.js もよく使われる。

<script type="text/template" id="book-template">
  <div class="book">
    <span class="book-name"><%= title %></span>
    by <span class="book-author"><%= author %></span>
  </div>
</script>
var BookView = Backbone.View.extend({
  render: function () {
    var template = $('#book-template').html();
    this.$el.html(template(this.model.attributes));
    return this;
  }
});

部品化 1

jQuery のセレクタとコールバックの羅列。何がしたいのかよくわからない・・・。

// Foo
$('.foo').on('click', function (e) { /* ... */ });
$('.foo bar').on('click', function (e) { /* ... */ });
$('.foo bar').on('mouseover', function (e) { /* ... */ });

// Baz
$('.baz').on('click', function (e) { /* ... */ });

部品ごとにコードを一カ所に集め、それぞれ Backbone.View を継承したクラスにすると・・・。

部品化 2

var FooView = Backbone.View.extend({
  events: {
    'click' : 'doSomething',
    'click .bar' : 'toggleBarState',
    'mouseover .bar' : 'makeBarRed'
  },
  doSomething: function (e) { /* ... */ },
  toggleBarState: function (e) { /* ... */ },
  makeBarRed: function (e) { /* ... */ }
});
var BazView = Backbone.View.extend({
  events: {
    'click' : 'postData'
  },
  postData: function (e) { /* ... */ }
});

var fooView = new FooView({ el: $('.foo') });
var bazView = new BazView({ el: $('.baz') });

名前がつく

jQuery セレクタ/コールバックの羅列に比べて、どこで何をしているかわかりやすくなる。

Event Delegation

events を使うと、担当する DOM 要素とその子要素へのイベントハンドラーが簡単に登録できる。this もバインド済。

var FooView = Backbone.View.extend({
  greet: 'Hello!'
  events: {
    'mouseover'           : 'smile',
    'click .hello-button' : 'sayHello'
  },
  smile: function () {
  },
  sayHello: function () {
    // this は View のインスタンス!
    console.log(this.greet);
  }
});

Event Delegation 補足

jQuery だけでも、委譲はできる。

.hello-button は最初からある必要はない。親要素にイベントリスナーを仕掛けておき、子から bubbling してくるイベントを監視。

$('.foo').on('click', '.hello-button', function (e) {
  var buttonText = $(e.currentTarget).text();
  alert(buttonText + ' was clicked.');
});
$('<button />').addClass('hello-button').text('Hello!').appendTo('.foo');
$('<button />').addClass('hello-button').text('Hi!').appendTo('.foo');

古い IE で submit など、できないイベントもあり。

うちはうち、よそはよそ。

グローバルの $ を使うと、View の中だけでなく、画面中どこでも操作できてしまう。 this.$el/this.$ で、this.el またはその子供だけを操作するようにすると、責任範囲が明確になる。

// View のメソッドの中で・・・

this.$el.css('color', '#f00');
// $(this.el).css('color', '#f00'); と同じ

this.$('li').hide();
// $(this.el).find('li').hide(); と同じ。

別の View と連動させたい時は?

Backbone.Router

URL とアクションをマッピング。

var Router = Backbone.Router.extend({
  routes: {
    'help':                 'help',    // #help
    'search/:query':        'search',  // #search/kiwis
    'search/:query/p:page': 'search'   // #search/kiwis/p7
  },
  help: function() { /* ... */ },
  search: function(query, page) { /* ... */ }
});
var router = new Router();
Backbone.history.start(); // { pushStat: true } なら進む・戻る

router.navigate('help', {trigger: true});

まずは View から

既存システムでクライアントサイドに Model を導入するには、けっこうな変更が必要。REST API を用意して、クライアントでテンプレート使ってレンダリングして・・・。

Router も Single Page Application でないとあまり・・・。

まずは View から使ってみましょう。

リソース