backbone-boilerplateではじめるHTML5アプリケーション開発 その4

backbone-boilerplateでTODOアプリを作成してみる

f:id:qualitas:20140521042907p:plain

これまで、backbone-boilerplateについて説明を書いてきましたが、
今回は、backbone-boilerplateを利用してサンプルアプリを作ってみます。

サンプルアプリの内容

今回は、既存のTodoMVC のBackbone.jsのTODOアプリをもとにして作ります。
Backbone.jsを利用した一般的なアプリと、backbone-boilerplateとの違いがわかると思います。

Backbone.jsのTODOアプリのサンプルはいくつかありますが、今回は、「Dependency Example」で紹介されているTODOアプリを使います。
https://github.com/tastejs/todomvc/tree/gh-pages/dependency-examples/backbone_require

また、アプリの構成は、github-viewerも参考にします。

backbone-boilerplateをもとに、サンプルアプリのプロジェクトをセットアップする

まず、backbone-boilerplate をcloneします

git clone --depth 1 https://github.com/backbone-boilerplate/backbone-boilerplate

プロジェクト名を変更します。今回はtodo-appとします

mv backbone-boilerplate todo-app

todo-appをカレントディレクトリにします

cd todo-app

grunt-cliとbowerをインストールします。この操作は一度のみでOKです。

npm install -g grunt-cli bower

backbone-boilerplateが利用するNode.jsのパッケージをインストールします。

npm install

backbone-boilerplateが利用するライブラリをインストールします

bower install

ライブラリを追加する

backbone-boilerplate自体には、github-viewerが利用していた以下のライブラリが、bower.json に設定されていません。

  1. backbone.layoutmanager
  2. lodash-template-loader

これらの2つのライブラリも、Backbone.jsのアプリを開発する際には有用なため、セットアップします。

以下のように、bowerでライブラリをインストールします。

bower install layoutmanager --save
bower install lodash-template-loader --save

また、Todoサンプルアプリは、backbone.localStorage, todomvc-common を利用しているため、インストールします。

bower install backbone.localStorage --save
bower install todomvc-common --save


bower.json に、インストールした3つのライブラリが追加されます。

{
  "name": "backbone-boilerplate",
  "dependencies": {
    "html5-boilerplate": "~4.3.0",
    "almond": "~0.2.9",
    "lodash": "~2.4.1",
    "backbone": "~1.1.1",
    "jquery": "~2.1.0",
    "requirejs": "~2.1.10",
    "qunit": "~1.14.0",
    "jasmine": "~2.0.0",
    "mocha": "~1.17.1",
    "chai": "~1.9.0",
    "sinon": "~1.8.2",
    "layoutmanager": "~0.9.5",
    "lodash-template-loader": "~0.1.5",
    "backbone.localStorage": "~1.1.7",
    "todomvc-common": "~0.1.4"
  }
}

RequireJSの設定を変更する

layoutmanagerと、lodash-template-loader、backbone.localStorage を利用するために、RequireJSの設定(config.js)を変更します。

まず、pathsの部分。github-viewerをもとにして、以下のようになりました。

require.config({
  paths: {
      // Make vendor easier to access.
      "vendor": "../vendor",

      // Almond is used to lighten the output filesize.
      "almond": "../vendor/bower/almond/almond",

      // Opt for Lo-Dash Underscore compatibility build over Underscore.
      "underscore": "../vendor/bower/lodash/dist/lodash.underscore",

      // Map `lodash` to a valid location for the template loader plugin.
      "lodash": "../vendor/bower/lodash/dist/lodash",

      // Use the Lo-Dash template loader.
      "ldsh": "../vendor/bower/lodash-template-loader/loader",

      // Map remaining vendor dependencies.
      "jquery": "../vendor/bower/jquery/dist/jquery",
      "backbone": "../vendor/bower/backbone/backbone",
      "bootstrap": "../vendor/bower/bootstrap/dist/js/bootstrap",
      "layoutmanager": "../vendor/bower/layoutmanager/backbone.layoutmanager",
      "backboneLocalstorage": "../vendor/bower/backbone.localStorage/backbone.localStorage"
  },

ちなみに、jqueryのパスは、github-viewerでは "../vendor/bower/jquery/jquery" ですが、backbone-boilerlateの場合、 "../vendor/bower/jquery/dist/jquery"になります。
これは、github-viewerとbackbone-boilerlateで依存しているjqueryのバージョンが異なっていて、bowerでjqueryをインストールしたときに、インストール先のパスが若干異なってしまっている模様。※github-viewerはあまりメンテされてないようなので、基本的に依存ライブラリのバージョンはbackbone-boilerlateよりも少し古い。

また、pathsの下に、shimの設定を追記。

      "backboneLocalstorage": "../vendor/bower/backbone.localStorage/backbone.localStorage"
  },
  shim: {
      // This is required to ensure Backbone works as expected within the AMD
      // environment.
      "backbone": {
        // These are the two hard dependencies that will be loaded first.
        deps: ["jquery", "underscore"],

        // This maps the global `Backbone` object to `require("backbone")`.
        exports: "Backbone"
      },
      backboneLocalstorage: {
          deps: ['backbone'],
          exports: 'Store'
      }
    }

以上で、Todoアプリのプロジェクトの設定は完了。

Todoアプリもダウンロードします。

git clone https://github.com/tastejs/todomvc/tree/gh-pages/dependency-examples/backbone_require
cd backbone_require
bower install

次に、アプリの作成する前に、アプリの作成方針を考えたいと思います。

ソースファイルの分割方針を決める

これは、backbone-boilerplateとは直接関係ない話ですが、
コーディングを始める前に、ソースファイルの分割方針を決めたいと思います。

Javascriptのアプリであっても、jsソースファイルは、機能ごとに分けて作成するほうが、多人数で開発する際には都合が良いと思います。
そこで、今回のサンプルアプリでは、1つのViewやModelに対して、1つのjsソースファイルを作成する方針にします。
ソースファイル名は、そのViewやModelのクラス名(Javascriptだからクラスとは言わないかもしれませんが)と同一とします。たとえば、TodoView.jsのようにします。

また、jsファイルは、viewやmodelごとにフォルダを分けて保存することにします。
そのため、最初に、jsソースを格納するためのフォルダを新規に作ります。

ソースファイルのフォルダの分け方は開発者の好みがでそうなところですが、今回は、modulesフォルダの下に以下の3つのフォルダを作成します。

  • view
  • model
  • collection

フォルダには、以下のように、jsソースファイルを入れます。

  • view
    • AppView.js
    • TodoView.js
  • collection
    • TodosCollection.js
  • model
    • TodoModel.js

ファイル名は、TodoMVCのサンプルアプリの定義名に加えて、~View, ~Collection, ~Modelのように、
役割に応じてサフィックスをつけています。

モジュール定義の方法を決める

今回、TodoMVCのサンプルのソースを流用して作成しますが、
TODOアプリを移植する作業をしているときに、気づいたことがあります。
それは、RequireJSを利用したモジュール定義の方法が、TodoMVCのサンプルアプリと、github-viewerでは若干異なるということです。

ここで少し、RequireJSのモジュール定義の方法について考えてみます。

TodoMVCのサンプルアプリでは、以下のように、defineメソッドの第一引数にモジュール名の配列を指定しています。

/*global define*/
define([
	'underscore',
	'backbone',
	'backboneLocalstorage',
	'models/todo'
], function (_, Backbone, Store, Todo) {
	'use strict';

	var TodosCollection = Backbone.Collection.extend({

これは、RequireJSのドキュメントの「NAMED MODULES」に記載されている定義方法になります。
http://www.vipaq.com/rtfm/JavaScript/RequireJs/zh-cn/2.1.9/whyamd.html#namedmodules

一方、github-viewerでは、defineメソッドの呼び出しは、以下で統一されています。

define(function(require, exports, module) {
    'use strict';

...

    module.exports = XXX;

これは、RequireJSのドキュメントで、"simplified CommonJS wrapper"と呼ばれているモジュール定義方法です。
RequireJSのドキュメントに、この定義方法について記載があります。
http://www.vipaq.com/rtfm/JavaScript/RequireJs/zh-cn/2.1.9/whyamd.html#sugar
http://requirejs.org/docs/api.html#cjsmodule

ドキュメントの記載によると、この定義方法にする意味は2つあるようです。

1つは、「NAMED MODULES」の定義方法の場合、依存するモジュールが多い場合に、依存モジュール名と、functionの引数とで組み合わせを誤るリスクがあることです。

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",
         "oauth", "blade/jig", "blade/url", "dispatch", "accounts",
         "storage", "services", "widgets/AccountPanel", "widgets/TabButton",
         "widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",
         "jquery.textOverflow"],
function (require,   $,        object,         fn,         rdapi,
          oauth,   jig,         url,         dispatch,   accounts,
          storage,   services,   AccountPanel,           TabButton,
          AddAccount,           less,   osTheme) {

});

実際、僕が開発しているときにも、組み合わせを間違ったことが何度かあります。

これに対応する方法として、requireメソッドを利用する方法が記載されています。

define(function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

この方法であれば、対応を間違えにくくなります。

ちなみに、RequireJSは、Function.prototype.toString()を利用してメソッド内のrequire('')を解析し、内部的には上記ソースを以下のように変換しているようです。

define(['require', 'dependency1', 'dependency2'], function (require) {
    var dependency1 = require('dependency1'),
        dependency2 = require('dependency2');

    return function () {};
});

もうひとつの理由として、CommonJSのModules1.xとの互換性の考慮があります。
実際には、"simplified CommonJS wrapper"で定義したモジュールはCommonJSと完全に互換性があるわけではないですが、
少なくとも、wrapしたメソッド内の処理は動作する可能性があります。
つまり、モジュールを、Node.jsのようなサーバサイドJavascriptの実行環境で動作させたい場合、"simplified CommonJS wrapper"でモジュール定義したほうがいいだろうということのようです。

Javascriptのモジュール定義については、以下のサイトにも説明があります。
http://addyosmani.com/resources/essentialjsdesignpatterns/book/#modularjavascript

互換性を考慮する必要がないなら(ブラウザのみで動作させるのであれば)、「NAMED MODULES」の定義方法で良いと思います。
今回は、github-viewerの記述にならって、"simplified CommonJS wrapper"でモジュール定義をすることにします。

コーディングする

index.html

Todoアプリのindex.htmlの内容に置き換えます。

  <title>Backbone Boilerplate</title>

を、以下のように置き換えます。

  <title>Backbone.js + RequireJS • TodoMVC</title>
  <link rel="stylesheet" href="vendor/bower/todomvc-common/base.css">
  <script src="vendor/bower/todomvc-common/base.js"></script>

次に、

<main role="main" id="main"></main>

を、以下のように置き換えます。

  <section id="todoapp">
      <header id="header">
          <h1>todos</h1>
          <input id="new-todo" placeholder="What needs to be done?" autofocus>
      </header>
      <section id="main">
          <input id="toggle-all" type="checkbox">
          <label for="toggle-all">Mark all as complete</label>
          <ul id="todo-list"></ul>
      </section>
      <footer id="footer"></footer>
  </section>
  <footer id="info">
      <p>Double-click to edit a todo</p>
      <p>Written by <a href="http://addyosmani.github.com/todomvc/">Addy Osmani</a></p>
      <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
  </footer>
Model

/backbone_require/js/models/todo.js の内容を元に、app/modules/model/TodoModel.js を作成します。

以下のようになります。

define(function(require, exports, module) {
    'use strict';

    var Backbone = require("backbone");
    var TodoModel = Backbone.Model.extend({
        // Default attributes for the todo
        // and ensure that each todo created has `title` and `completed` keys.
        defaults: {
            title: '',
            completed: false
        },

        // Toggle the `completed` state of this todo item.
        toggle: function () {
            this.save({
                completed: !this.get('completed')
            });
        }
    });

    module.exports = TodoModel;
});

ポイントは、"simplified CommonJS wrapper"でモジュール定義をしている点と、
依存モジュールをrequireメソッドで参照している点です。
また、TodoModelをmodule.exportsに代入しています。

Collection

TodoModelと同様に、/backbone_require/js/collections/todos.js の内容をもとに、app/modules/collection/TodosCollection.js を作成します。

define(function(require, exports, module) {
    'use strict';

    var TodoModel = require("modules/model/TodoModel");
    var Store = require("backboneLocalstorage");
    
    var TodosCollection = Backbone.Collection.extend({
        // Reference to this collection's model.
        model: TodoModel,

・・・省略・・・

    module.exports = new TodosCollection();
});

ポイントはTodoModelと同じですが、module.exports には、new演算子で新規に作成されたTodoCollectionのオブジェクトを指定しています。このような定義にしていると、var Todos = require("modules/collection/TodosCollection") とした場合に、Todosにはnew TodosCollection()した結果が格納されます。そのため、Todos.trigger('filter'); のように、そのままメソッド呼び出しなどを行うことができます。通常、あまりこういう定義方法はしないのですが、Todoアプリはこのようにモジュール定義をしているため、それにそっています。※Todoアプリは、TodosCollectionをシングルトンオブジェクトのように扱うためにこのようにしていると思います。

View

Todoアプリには、Viewは2つあります。まず、app/modules/view/TodoView.js から。

TodoView

/backbone_require/js/views/todos.js をもとに作成します。

Viewは、Model/Collection を作成したときと違うところが1点あります。
それは、テンプレートファイルの読み出しに RequireJSのtextプラグインを利用している点です。

define([
	'jquery',
	'underscore',
	'backbone',
	'text!templates/todos.html',
	'common'
], function ($, _, Backbone, todosTemplate, Common) {
	'use strict';

	var TodoView = Backbone.View.extend({

		tagName:  'li',

		template: _.template(todosTemplate),

backbone-boilerplateには、テンプレートファイルの読み出しを行うためのlodash-template-loaderが用意されているため、これを利用する形に変更します。

define(function(require, exports, module) {
    'use strict';

    var Common = require("modules/Common");
    var TodoView = Backbone.View.extend({

        tagName:  'li',

        template: require("ldsh!/app/templates/todos"),

なお、modules/Common.js は、以下のように、/backbone_require/js/common.js をもとにして作成しておきます。

define(function(require, exports, module) {
    'use strict';
    return {
        // Which filter are we using?
        TodoFilter: '', // empty, active, completed

        // What is the enter key constant?
        ENTER_KEY: 13
    };
});
AppView

/backbone_require/js/views/app.js をもとに作成します。

define(function(require, exports, module) {
    'use strict';

    var Todos = require("modules/collection/TodosCollection");
    var TodoView = require("modules/view/TodoView");
    var Common = require("modules/Common");
    
    // Our overall **AppView** is the top-level piece of UI.
    var AppView = Backbone.View.extend({

        // Instead of generating a new element, bind to the existing skeleton of
        // the App already present in the HTML.
        el: '#todoapp',

        // Compile our stats template
        template: require("ldsh!/app/templates/stats"),

・・・省略・・・

    module.exports = AppView;
テンプレートファイル

/backbone_require/js/templatesフォルダ内の以下のテンプレートファイルを、app/templates フォルダにコピーします。

  • stats.html
  • todos.html
Router

/backbone_require/js/routers/router.js の内容を、app/router.jsに反映します。
以下のようになります。

define(function(require, exports, module) {
  "use strict";

  // External dependencies.
  var Backbone = require("backbone");
  var Common = require("modules/Common");
  var Todos = require("modules/collection/TodosCollection");
  
  var TodoRouter = Backbone.Router.extend({
      routes: {
          '*filter': 'setFilter'
      },

      setFilter: function (param) {
          // Set the current filter to be used
          Common.TodoFilter = param || '';

          // Trigger a collection filter event, causing hiding/unhiding
          // of the Todo view items
          Todos.trigger('filter');
      }
  });
  
  // Defining the application router.
  module.exports = TodoRouter;
});

main.js

/backbone_require/js/main.js の内容を、app/main.jsに反映します。
/backbone_require/js/main.jsには、RequireJSの設定が含まれているため、それは無視します。

以下のようになります。

// Break out the application running from the configuration definition to
// assist with testing.
require(["config"], function() {
  // Kick off the application.
  require(["app", "router", "modules/view/AppView"], function(app, Router, AppView) {
    // Define your master router on the application namespace and trigger all
    // navigation from this instance.
    app.router = new Router();

    // Trigger the initial route and enable HTML5 History API support, set the
    // root folder to '/' by default.  Change in app.js.
    Backbone.history.start({ pushState: true, root: app.root });
    
    // Initialize the application view
    new AppView();
  });
});

動作させてみる

todo-appフォルダをカレントにして、grunt serverタスクを実行し、http://127.0.0.1/ をブラウザで表示します。

cd todo-app
grunt server

以下のTodoMVCのBackbone.jsのTodoアプリと同様な動作をしていたら、うまくいっています。
http://todomvc.com/dependency-examples/backbone_require/

f:id:qualitas:20140611034946j:plain

今回作成したサンプルアプリのソースは、以下で確認できます。
https://github.com/si-ro/todo-app


ただ、今回のサンプルアプリでは、backbone.layoutmanagerを利用していません。
次回、Todoアプリをbackbone.layoutmanagerで動作させる手順について記載したいと思います。