Backbone.jsでつくるModel-View-PresenterなUIパターンいろいろ

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

今日の目次

  • Backbone.jsの紹介
  • タブの実装を紹介

今日の目標

  • Backbone.jsで開発したことのない人の始める取っ掛かりになってもらえれば

Backbone.jsつかってますか?

Backbone.jsってなんですか?

クライアントサイドJavaScriptアプリケーションの構造を支えるMVPフレームワーク
シングルページアプリケーションとかで威力を発揮

Underscore.js もしくは Lo-Dash に強く依存。
json2.js、jQueryもしくはZeptoにも依存

Backbone.jsってなんですか?

mvp

Backbone.jsってなにがいいの?(特徴)

  • クラスベースのJavaScript(プロトタイプ継承)
      => テスト書きやすい、コードの見通しがいい

Backbone.jsってなにがいいの?(特徴)

  • クラスベースのJavaScript(プロトタイプ継承)
      => テスト書きやすい、コードの見通しがいい
  • ハッシュフラグメントのハンドリング(Backbone.Router)
      => 状態をブックマークできる

Backbone.jsってなにがいいの?(特徴)

  • クラスベースのJavaScript(プロトタイプ継承)
      => テスト書きやすい、コードの見通しがいい
  • ハッシュフラグメントのハンドリング(Backbone.Router)
      => 状態をブックマークできる
  • イベント駆動(Backbone.Events)
      => コードのメンテナンス性が高まる

Backbone.jsってなにがいいの?(特徴)

  • クラスベースのJavaScript(プロトタイプ継承)
      => テスト書きやすい、コードの見通しがいい
  • ハッシュフラグメントのハンドリング(Backbone.Router)
      => 状態をブックマークできる
  • イベント駆動(Backbone.Events)
      => コードのメンテナンス性が高まる
  • RESTful JSON interface ※今回これには触れません
      => Railsと相性いい

クラスベースのJavaScript

Rubyのクラス

class Person
  def initialize(name)
    @my_name = name
  end

  def greeting
    "Hello! I'm #{ @my_name }"
  end
end

yamagata = Person.new('yamagata')
yamagata.greeting # => Hello! I'm yamagata

クラスベースのJavaScript

JavaScriptでクラスをエミュレート

var Person = (function() {
  function Person(name) {
    this.my_name = name;
  }

  Person.prototype.greeting = function() {
    return "Hello! I'm " + this.my_name;
  };

  return Person;
})();

var yamagata = new Person('yamagata');
yamagata.greeting(); // => Hello! I'm yamagata

実はBackbone.jsではない機能

  • DOM操作 => jQuery
  • 配列やオブジェクトの操作 => jQuery, Underscore.js
  • プロトタイプ継承 => Underscore.js
  • this問題の解決 => Underscore.js
  • etc...

こんなのが作れます

タブ

スクリーンショット 2013-02-27 14.05.06

こんなのが作れます

タブ

ui-tab-components

実装を始める前に

<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="underscore.js"></script>
<script type="text/javascript" src="backbone.js"></script>

これらを読み込んでおきます

TabViewの仕様

  • テンプレート&データからhtmlの生成
  • 選択中にする(activate)
  • 非選択中にする(deactivate)

TabViewの実装

var TabView = Backbone.View.extend({
  btnTemplate: _.template($('#ui-tab-btn-template').html()),
  contentTemplate: _.template($('#ui-tab-content-template').html()),
});

スクリーンショット 2013-02-27 14.05.06

TabViewの実装

テンプレート(html内に記述)

タブボタンのテンプレート

<script type="text/template" id="ui-tab-btn-template">
  <li>
    <a href="#<%= name %>">
      <%= name[0].toUpperCase() + name.slice(1) %>
    </a>
  </li>
<script>

タブ内容のテンプレート

<script type="text/template" id="ui-tab-content-template">
  <div class="tab-content">
    <%= content %>
  </div>
<script>

TabViewの実装

テンプレートの例

var templateString = $('#ui-tab-btn-template').html();
var renderFunction = _.template(templateString);

var renderedHtml = renderFunction({ name: 'hoge' });
/* 実行結果
<li>
  <a href="#hoge">
    Hoge
  </a>
</li>
*/
var $li = $(renderedHtml);
$('ul').append($li);

TabViewの実装

インスタンス生成時にコンテナ要素と自身の名前をつけておく

initialize: function(options) {
  this.$container = $('#ui-tab');
  this.$btn_container = this.$container.find('.nav-tabs');
  this.$contents_container = this.$container.find('.tab-contents');
  this.name = this.options.name;
}

TabViewの実装

アクティブ化と非アクティブ化

activate: function() {
  this.$btn.addClass('active');
  this.$content.addClass('active');
},
 
deactivate: function() {
  this.$btn.removeClass('active');
  this.$content.removeClass('active');
}

TabViewの実装

var TabView = Backbone.View.extend({
  btnTemplate: _.template($('#ui-tab-btn-template').html()),
  contentTemplate: _.template($('#ui-tab-content-template').html()),
 
  initialize: function(options) {
    this.$container = $('#ui-tab');
    this.$btn_container = this.$container.find('.nav-tabs');
    this.$contents_container = this.$container.find('.tab-contents');
    this.name = this.options.name;
  },
   
  render: function() {
    this.$btn = $(this.btnTemplate(this.options.data));
    this.$content = $(this.contentTemplate(this.options.data));
    this.$btn_container.append(this.$btn);
    this.$contents_container.append(this.$content);
  }
   
  activate: function() {
    this.$btn.addClass('active');
    this.$content.addClass('active');
  },
 
  deactivate: function() {
    this.$btn.removeClass('active');
    this.$content.removeClass('active');
  }
});

TabAppの仕様

  • データを元に各TabViewを生成できる
  • htmlを1回だけ描画できる
  • タブを切り替えるイベント change を発行できる

TabAppの実装

var TabApp = _.extend({}, Backbone.Events);

Backbone.Eventsはイベント系関数をもったモジュールなので、プロトタイプ継承できない。

TabAppの実装

初期化関数

views: [], // TabView格納用
tab_names: [], // タブ名格納用
 
initialize: function(data) {
  var self = this;
  _.each(data, function(_data) {
    var tabView = new TabView({ data: _data });
 
    // TabViewがchangeイベントに一斉に反応できるようにしておく
    tabView.listenTo(self, 'change', function(name) {
      // 自分の名前と一致していたらactivate、でなければdeactivate
      var method = tabView.name === name ? 'activate' : 'deactivate'
      tabView[method]();
    });
    
    // メンバーに格納
    self.views.push(tabView);
    self.tab_names.push(_data.name);
  });
   
  return this;
}

TabAppの実装

htmlの描画を一度だけ実行。実行された場合のCallbackを受け付ける。

wasRendered: false,
 
renderAll: function(callback) {
  // 一度だけ実行する
  if (!this.wasRendered) {
    this.wasRendered = true;
 
    _.each(this.views, function(view) {
      view.render();
    });
 
    if (callback) callback();
  }
}

TabAppの実装

タブの切替

TabRouterからタブ名を受け取る。TabView全てを操作するのではなく'change'イベントの発火を行う。

changeTo: function(name) {
  if (_.indexOf(this.tab_names, name) >= 0) {
    this.trigger('change', name);
  }
}

TabAppの実装

タブの切替

TabRouterからタブ名を受け取る。TabView全てを操作するのではなく'change'イベントの発火を行う。

スクリーンショット 2013-03-05 11.37.11

TabRouterの実装

ハッシュフラグメントの切り替わりで反応できる。

var TabRouter = Backbone.Router.extend({});

データを受け取りTabAppを初期化する

initialize: function(options) {
  this.options = options;
  this.app = TabApp.initialize(this.options.data);
}

TabRouterの実装

ルートを定義してアクションに紐付ける

routes: {
  '*tab': 'defaultAction'
},
 
defaultAction: function(tab) {
  var self = this;
  this.app.renderAll(function() {
    self.navigate(self.options.data[0].name, { trigger: true });
  });
 
  if (tab.length) {
    this.app.changeTo(tab);
  }
}

※navigateのtriggerオプション重要

アプリケーションをスタートさせる

これまで書いてきたのは定義だけなのでそのままでは動けない
html側にスタートさせる記述を書く

<script type="text/javascript">
  jQuery(function() {
    var data = {
      { name: 'apple', content: 'commodo consequat id interdum rhoncus Maecenas...' },
      { name: 'google', content: 'commodo consequat id interdum rhoncus Maecenas...' },
      { name: 'classmethod', content: 'commodo consequat id interdum rhoncus Maecenas...' }
    };
 
    new TabRouter(data);
    Backbone.history.start();
  });
</script>

まとめ

各機能をクラスで分けて定義することで、アプリーケーションをクラス間のやり取りで表せるようになり、コードの移植性や拡張性が高まる。

あとあまり大掛かりなものじゃなくても使えます

https://dev.classmethod.jp/series/