Chromeバージョンアップ後、jquery.PrintAreaで印刷できなくなった

f:id:qualitas:20140724235911p:plain

私が以前開発していたWebアプリには印刷機能があったのですが、先日の2014/7/17にChromeが36.0.1985.125へバージョンアップした途端、印刷ができなくなりました。

調べてみると、印刷に利用していたjquery.PrintAreaを最新のバージョン2.4.0にすると印刷できるようになるみたい・・。

jquery.PrintAreaは、2.3.3⇒2.4.0へのバージョンアップの際、以下のissueに対応している。そのおかげで直ってるみたい。
https://github.com/RitsC/PrintArea/issues/15
このissueは、IE11でiframeモードでの印刷ができない、というものだったが。

このissueでは、以下の変更が行われているけど、これでどうして印刷ができるようになるのだろう?

  • printメソッドの呼び出しをsetTimeoutで遅延実行するようにしている
setTimeout( function () { PrintArea.print( PrintAreaWindow ); }, 1000 );
  • iframeのスタイルを変更(left:0pxをright:0pxに変更している)
var iframeStyle = 'border:0;position:absolute;width:0px;height:0px;left:0px;top:0px;';

var iframeStyle = 'border:0;position:absolute;width:0px;height:0px;right:0px;top:0px;';
  • iframeのsrc属性の値を変更
$(iframe).attr({ style: iframeStyle, id: frameId, src: "" });

$(iframe).attr({ style: iframeStyle, id: frameId, src: "#" + new Date().getTime() });

とりあえずPrintAreaのバージョンアップで対応できたけど、正直、何が原因で印刷できなくなったのか、この修正でなんで直るのか分からなくて、かなりモヤモヤしている。


Chromeのバージョンアップで、ここまでアプリに影響が発生したのは初めて。
Chromeのラピッドリリースは利用ユーザにはメリットがあるけど、開発者にとっては大変だなと実感した。
Beta版でリリース前に動作確認しておかないといけないなぁ。

backbone-boilerplateがversion 2.0へ

このブログでは、backbone-boilerplateについていくつか解説を書いてきましたが、
このbackbone-boilerplateが、2014/6/24にバージョンアップされました。
https://github.com/backbone-boilerplate/backbone-boilerplate/pull/242

変更点は、以下の通り。

  • HTML5 BoilerplateをPure CSS (http://purecss.io/)へ変更
  • lodash-template-loader をbower.jsonに追加
  • Gruntfile.jsをCoffeeScriptに書き換え(Gruntfile.coffee)。また、以下の各タスクの設定を、build/tasks/配下に分割
    • build/tasks/clean.coffee
    • build/tasks/compress.coffee
    • build/tasks/copy.coffee
    • build/tasks/cssmin.coffee
    • build/tasks/jshint.coffee
    • build/tasks/karma.coffee
    • build/tasks/processhtml.coffee
    • build/tasks/requirejs.coffee
    • build/tasks/server.coffee
    • build/tasks/styles.coffee
  • grunt-bbb-requirejsの利用をやめ、grunt-contrib-requirejsを利用
  • grunt test でJSHintと単体テストが実行されるようにした(以前はできていたので、できるように戻した)
  • 依存ライブラリのバージョンをアップデート
  • Bower依存ライブラリの格納先ディレクトリを、vendor/bower から、bower_componentsに変更
  • Backbone.LayoutManagerを依存ライブラリに追加

バージョンアップ前のbackbone-boilerplateには、lodash-template-loaderやLayoutManagerが含まれていませんでしたが、今回のバージョンアップから標準で含まれるように変更されました。
※以前のboilerplateには、lodash-template-loaderやLayoutManagerが含まれていたので、元の構成に戻ったと言えます。

PureCSSというのは、今回初めて知ったのですが、Javascript不要なCSSフレームワークということで、Bootstrapとは違った特徴があり、興味深いです。

これまで本ブログで紹介したbackbone-boilerplateプロジェクトと構成が変更されているため(特に、gruntの部分)、読み替えが必要ですが、基本的な利用方法は変わらないと思います。
PureCSSや、gruntの構成変更に興味が無ければ、バージョンアップ前のbackbone-boilerplateを利用しても良いと思います。

バージョンアップ前のbackbone-boilerplateを利用したい場合、以下のようにして、バージョンアップ前のコミットのソースを取得できます。

git clone https://github.com/backbone-boilerplate/backbone-boilerplate
git checkout c698a5482b2ee7369d4972bfa3faed8f8709ea1b

また、以下のリポジトリはバージョンアップ前のbackbone-boilerplateです。
https://github.com/si-ro/backbone-boilerplate-ver1

なお、backbone-boilerplateのサンプルであるgithub-vierwerには、今回のバージョンアップの内容は反映されていませんでした。
引き続き、backbone-boilerplateをウォッチしていきたいと思います。

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

backbone.layoutmanagerを理解する

f:id:qualitas:20140521042907p:plain

前回にひきつづきBackbone-boilerplateでTODOアプリを作成していきます。

前回では、TodoMVCで公開されているBackboneのTodoアプリを、Backbone-boilerplateと、lodash-template-loaderを利用する形に置き換えました。
今回は、これに、backbone.layoutmanagerを適用したいと思います。

backbone.layoutmanagerとは

backbone.layoutmanagerは、BackboneのViewを利用しやすくするための機能を追加してくれるプラグインです。
Backbone.Marionetteと似ていますが、layoutManagerは、Viewを機能拡張することに特化しているのが特徴です。

BackboneのViewを使うとき、どのアプリでも同じように実装する必要のある処理というのがあると思います。
例えば、以下のような処理です

  • テンプレートの読み込みや展開
  • 一覧画面表示用のViewに、その一覧の各項目を表示するためのViewを追加する

LayoutManagerは、以下のような、Viewを利用する際にアプリが実装する必要のある基本的な処理を提供してくれます。

  • テンプレートの読み込み
  • 読み込んだテンプレートの内容を対象の要素へ展開
  • ヘッダ、コンテンツ表示部、フッタのようなViewのレイアウト
  • 階層構造のViewの表示
  • View表示前、表示後のイベントハンドラ

LayoutManager については以下のドキュメントに記載があります。
https://github.com/tbranyen/backbone.layoutmanager/wiki

backbone.layoutmanagerの使い方

Viewのレイアウト

backbone.layoutmanagerを利用してViewをレイアウトする簡単なサンプルを示します。

例えば、以下のように、メインとなるレイアウトと、ログイン画面のテンプレートの2つのViewを表示する場合。

  • メイン画面のテンプレート
<section class="content"></section>

<!-- Login template below will be injected here -->
<aside class="secondary"></aside>
  • ログイン画面のテンプレート
<form class="login">
    <p>
        <label for="user">Username</label><input type="text" name="user">
    </p>
    <p>
        <label for="pass">Password</label><input type="text" name="pass">
    </p>
    <p>
        <input class="loginBtn" type="submit" value="Login">
    </p>
</form>

Login画面を表示するためのViewは以下のように定義します。

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

    var Backbone = require("backbone");

    var LoginView = Backbone.Layout.extend({
        template: require("ldsh!/app/templates/login"),

    });

    module.exports = LoginView;
});

そして、メイン画面のレイアウトを以下のように定義します。

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

    var Backbone = require("backbone");
    var LoginView = require("modules/view/LoginView");
    
    var MainView = Backbone.Layout.extend({
        el: 'main',
        
        template: require("ldsh!/app/templates/main"),

        // In the secondary column, put a new Login View.
        views: {
          ".secondary": new LoginView()
        }
    });

    module.exports = MainView;
});

LayoutManagerのviewsプロパティに、レイアウト内のViewのインスタンスを指定します。
この例の場合、.secondary classが定義された要素に、ログイン画面のテンプレート内容が挿入されます。

画面の描画は、レイアウトを定義したメイン画面のViewのrenderメソッドを呼び出した際に行われます。

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

  // External dependencies.
  var Backbone = require("backbone");
  var Layout = require("layoutmanager");
  var MainView = require("modules/view/MainView");
  
  var Router = Backbone.Router.extend({
      initialize : function() {
          // Use main layout and set Views.
          this.mainView = new MainView();
          // Render to the page.
          this.mainView.render();
      },
      routes: {
          "": 'index'
      },

      index: function () {

      }
  });
  
  // Defining the application router.
  module.exports = Router;
});

この仕組みを利用して、ヘッダ、コンテンツ、フッタのような構造を持つ画面をレイアウトするようなことができます。

Backbone.Layout.extend({
  views: {
    "header": new HeaderView(),
    "section": new ContentView(),
    "footer": new FooterView()
  }
});

動的にViewを置き換えたい場合、setViewメソッドを利用します。

myLayout.setView("header", new HeaderView());

LayoutManagerによるrender処理の前にはbeforeRenderメソッドが呼び出されます。また、render処理の後は、afterRender メソッドが呼び出されます。これらのメソッドをオーバライドして、イベントに対する処理を記載することができます。

    var ListView = Backbone.Layout.extend({

        beforeRender : function() {

        },
        afterRender : function() {

一覧内の各項目のViewを追加する場合、insertViewメソッドを利用します。

Backbone.Layout.extend({
  beforeRender: function() {
    this.collection.each(function() {
      this.insertView(new ListItemView());
    }, this);
  }
});

この場合、LayoutManagerのbeforeRenderメソッド内でviewの挿入処理を行うと、全ての要素の挿入後に画面表示処理が行われ、効率的です。

beforeRenderメソッド以外のタイミングでViewの挿入を行う場合、明示的にrenderメソッドを呼び出す必要があります。

  afterRender: function() {
    this.collection.each(function() {
      this.insertView(new ListItemView()).render();
    }, this);
  }

この場合、1つの要素の挿入のたびに画面の描画が行われるため、表示速度が低下したり、画面ちらつきの原因となります。

テンプレートにパラメタを渡したい場合、serializeメソッドを実装します。

Backbone.Layout.extend({
        serialize : function() {
            return this.model.toJSON();
        },

今回作成したbackbone.layoutmanagerを利用したサンプルソースは以下で確認できます。
https://github.com/si-ro/backbone.layoutmanager-sample

Todoアプリでbackbone.layoutmanagerを利用する

TodoアプリのTodoViewをLayoutManagerを利用するように変更してみます。

app/app.js

layoutmanagerを利用するため、requireを追加します。

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

  // External dependencies.
  var _ = require("underscore");
  var $ = require("jquery");
  var Backbone = require("backbone");
  var Layout = require("layoutmanager");
TodoView.js

既存のrenderメソッドコメントアウトし、serialize ,beforeRender , afterRender メソッドを追加します。
renderメソッドの処理の一部はafterRenderに移します。
テンプレートの読み込みと表示の部分は、LayoutManagerが行います。

/*global define*/
define(function(require, exports, module) {
    'use strict';
    var Backbone = require("backbone");
    var Common = require("modules/Common");
    var TodoView = Backbone.Layout.extend({

        tagName:  'li',

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

        // The DOM events specific to an item.
        events: {
            'click .toggle':    'toggleCompleted',
            'dblclick label':   'edit',
            'click .destroy':   'clear',
            'keypress .edit':   'updateOnEnter',
            'blur .edit':       'close'
        },

        // The TodoView listens for changes to its model, re-rendering. Since there's
        // a one-to-one correspondence between a **Todo** and a **TodoView** in this
        // app, we set a direct reference on the model for convenience.
        initialize: function () {
            this.listenTo(this.model, 'change', this.render);
            this.listenTo(this.model, 'destroy', this.remove);
            this.listenTo(this.model, 'visible', this.toggleVisible);
        },
        serialize : function() {
            return this.model.toJSON();
        },
        beforeRender : function() {
            
        },
        afterRender : function() {
            this.$el.toggleClass('completed', this.model.get('completed'));

            this.toggleVisible();
            this.$input = this.$('.edit');
        },
        // Re-render the titles of the todo item.
//        render: function () {
//            this.$el.html(this.template(this.model.toJSON()));
//            this.$el.toggleClass('completed', this.model.get('completed'));
//
//            this.toggleVisible();
//            this.$input = this.$('.edit');
//            return this;
//        },
<省略>
    });

    module.exports = TodoView;
});

今回、Todoアプリの作り上、全てのViewをbackbone.layoutmanagerに置き換えるのが難しかったため、TodoViewのみLayoutManagerに変更してみました。
サンプルとしては物足りないですが、LayoutManagerが、テンプレートの読み込みと表示処理を行ってくれることがわかると思います。

今回、backbone.layoutmanagerを利用するように変更したTodoアプリのソースは以下で確認できます。
https://github.com/si-ro/todo-app2

backbone.layoutmanagerについては、以下のサイトも参考になります。
http://code.tutsplus.com/tutorials/make-backbone-better-with-extensions--net-30723

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で動作させる手順について記載したいと思います。

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

backbone-boilerplateが提供するビルド

f:id:qualitas:20140521042907p:plain

backbone-boilerplateの特徴の1つとして、ビルドプロセスがしっかり整備されていることがあげられます。今回は、backbone-boilerplateが用意しているビルドの内容について、見てみます。

backbone-boilerplateのビルドの内容を理解することで、Javascriptアプリケーションのビルドの方式について、ひととおり理解することができると思います。

backbone-boilerplateのビルドでやっていること

backbone-boilerplateはビルドにgruntを利用しており、デフォルトで、以下のタスクを実行するように設定されています。

Gruntfile.js

  // When running the default Grunt command, just lint the code.
  grunt.registerTask("default", [
    "clean",
    "jshint",
    "processhtml",
    "copy",
    "requirejs",
    "styles",
    "cssmin",
    //"compress",
  ]);
jshintタスク

JSHintを利用してappフォルダ配下のJavascriptソースの静的解析を実施します。
grunt からJSHintを実行するためのプラグイン grunt-contrib-jshint を利用しています。
gruntjs/grunt-contrib-jshint · GitHub


boilerplate内のjsファイルに対してjshintを行っても何もエラーはでませんが、ためしに、github-viewer/app/components/user/index.js に以下のようなコードを追記して grunt jshint を実行すると、

  var i = 0

以下のように、セミコロンがないことを指摘されます。

Running "jshint:0" (jshint) task
Linting app/components/user/index.js ...ERROR
[L3:C12] W033: Missing semicolon.
  var i = 0

Warning: Task "jshint:0" failed. Use --force to continue.

Aborted due to warnings.

実際の開発では、 .jshintrc を定義してJSHintのオプションを設定する必要があると思います。

processhtmlタスク

これは、htmlファイルの内容をリリース向けに変更するための処理を行うためのタスクです。
grunt-processhtml を利用して実行しています。

具体的には、index.html の内容を変更しています。
開発時にローカルPCから参照されるindex.html (例: github-viewer/index.html)は、以下のような内容になっています。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="viewport" content="width=device-width,initial-scale=1">

  <title>GitHub Viewer</title>

  <!-- Application styles. -->
  <!-- build:[href] /styles.min.css -->
  <link rel="stylesheet" href="/app/styles/index.css">
  <!-- /build -->
</head>

<body>
  <!-- Application container. -->
  <main role="main" id="main"></main>

  <!-- Application source. -->
  <!-- build:[src] /source.min.js -->
  <script data-main="/app/main" src="/vendor/bower/requirejs/require.js"></script>
  <!-- /build -->
</body>
</html>

これは、リリースビルド後の dist/index.html だと、以下のように変更されています。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <meta name="viewport" content="width=device-width,initial-scale=1">

  <title>GitHub Viewer</title>

  <!-- Application styles. -->
  <link rel="stylesheet" href="/styles.min.css">
</head>

<body>
  <!-- Application container. -->
  <main role="main" id="main"></main>

  <!-- Application source. -->
  <script data-main="/app/main" src="/source.min.js"></script>
</body>
</html>

/app/styles/index.css が、 /styles.min.css に変更されています。
また、 /vendor/bower/requirejs/require.js が、 /source.min.js になっています。
processhtmlが、index.htmlのコメントを参照して、ビルド時に変換してくれています。

  <!-- build:[href] /styles.min.css -->
  <link rel="stylesheet" href="/app/styles/index.css">

...

  <!-- build:[src] /source.min.js -->
  <script data-main="/app/main" src="/vendor/bower/requirejs/require.js"></script>

開発時とリリース時でindex.htmlの内容を変更したいことはよくあると思います。
場合によっては、index.htmlを2重管理したりとか。。
processhtmlを使えば、そういう問題に対応できます。

requirejsタスク

RequireJSを利用して分割したjsファイルを、1つのjsファイルに結合します。ソース内の改行や空白文字の除去(ソース圧縮)も実施されます。
これは、r.js を利用して行われます。また、AMD loaderとしてalmondが指定されています。

    // This task uses James Burke's excellent r.js AMD builder to take all
    // modules and concatenate them into a single file.
    requirejs: {
      release: {
        options: {
          mainConfigFile: "app/config.js",
          generateSourceMaps: true,
          include: ["main"],
          insertRequire: ["main"],
          out: "dist/source.min.js",
          optimize: "uglify2",

          // Since we bootstrap with nested `require` calls this option allows
          // R.js to find them.
          findNestedDependencies: true,

          // Include a minimal AMD implementation shim.
          name: "almond",

          // Setting the base url to the distribution directory allows the
          // Uglify minification process to correctly map paths for Source
          // Maps.
          baseUrl: "dist/app",

          // Wrap everything in an IIFE.
          wrap: true,

          // Do not preserve any license comments when working with source
          // maps.  These options are incompatible.
          preserveLicenseComments: false
        }
      }
    },

Lo-Dash Template Loader を利用してテンプレートファイルを呼び出しをしている箇所については、そのテンプレートファイルもjsファイル内に結合されます。

これらの処理は、backbone-boilerplateで用意されている、grunt-bbb-requirejs を利用して行われます。
grunt-bbb-requirejsは、grunt-contrib-requirejs と同じ目的をもつプラグインで、実際、grunt-contrib-requirejsからフォークされて開発されたプラグインですが、ソースは完全に異なっていて、処理内容にはいくつか違いがあるようです。

なお、Source Mapsファイル(source.min.js.map)も生成してくれます。
リリースビルドされたアプリを実行していても、Chromeデベロッパツールでは最適化前のソースの内容を確認することができます。

stylesタスク

style.css内の @import で指定された別のcssファイルの内容を、style.cssに結合します。

/*-- Bootstrap. -------------------------------------------------------------*/

@import "../../vendor/bower/bootstrap/dist/css/bootstrap.css";

/*-- Application stylesheets. -----------------------------------------------*/

@import "app.styl";

ビルド後、以下のように、bootstrap.cssの内容が埋め込まれた形になります。

article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
nav,
section,
summary {display: block;}
audio,
canvas,
video {display: inline-block;}
audio:not([controls]) {display: none; height: 0;}

...省略....

この処理は、grunt-bbb-styles で実施されています。

cssminタスク

style.css の内容を圧縮(改行、空白文字取り除き)して、style.min.css を作成します。


以上が、backbone-boilerplateのデフォルトのビルドで実行されるタスクです。
実際の開発では、他のタスクも必要になってくると思います(jsdocとか)。
Javascriptアプリケーションで必要となりそうな最低限のビルドが用意されており、仮にbackbone-boilerplateを利用しない場合でも、これらのビルドの考えた方は参考になると思います。

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

前回のブログの続きです。
http://qualitas.hatenablog.com/entry/2014/05/21/045427

backbone-boilerplate のサンプルアプリである、github-viewer を見てみます。
tbranyen/github-viewer · GitHub

セットアップ

1. githubからクローンします

git clone git://github.com/tbranyen/github-viewer.git
cd github-viewer

2. 依存ライブラリをインストールします

npm install -q

と、ここでそのままインストールを行うと、grunt-karmaのインストールに失敗してしまったので、github-viewer/package.json ファイルの内容を以下のように変更しました。
※backbone-boilerplateのpackage.jsonの内容と同じ。

  "devDependencies": {
    "grunt": "~0.4.1",
    "bbb": "~0.2.0",
    "grunt-contrib-jshint": "~0.8.0",
    "grunt-contrib-cssmin": "~0.7.0",
    "grunt-contrib-copy": "~0.5.0",
    "grunt-contrib-clean": "~0.5.0",
    "grunt-contrib-compress": "~0.6.0",
    "grunt-processhtml": "~0.3.0",
    "grunt-karma": "~0.8.2",
    "grunt-karma-coveralls": "~2.3.0",
    "karma-jasmine": "~0.1.0",
    "karma-mocha": "~0.1.0",
    "karma-qunit": "~0.1.0",
    "karma-phantomjs-launcher": "~0.1.0",
    "karma-coverage": "~0.1.0"
  },

変更後、改めてインストール。

npm install -q

3. インストール後、gruntでサーバ起動

grunt server

ブラウザから、http://127.0.0.1:8000 にアクセスすると、github-viewerの画面が表示されます。
f:id:qualitas:20140527034420j:plain

このサンプルアプリは、GitHubAPIを呼び出します。
最初の検索文字には、Organizationを指定します。bocoupや、gruntjsなど。
すると、そのOrganizationに所属しているMemberのリストを画面に表示します。
Memberを選択すると、その人のリポジトリ一覧を表示し、リポジトリを選択すると、そのコミットログを表示します。

サンプルアプリの構成

このgithub-viewerは、もちろんbackbone-boilerplateをもとに作成されていますが、他に2つ、重要なライブラリを利用しています。
1つは、Backbone LayoutManager。もうひとつは、Lo-Dash Template Loaderです。

Backbone LayoutManager

tbranyen/backbone.layoutmanager · GitHub
のドキュメントの説明がわかりやすいので、そちらを見てください
https://github.com/tbranyen/backbone.layoutmanager/wiki/Overview

LayoutManagerを利用することで、テンプレートファイル(html)とViewを分離することができます。また、ネストされたViewの表示をするためのsetView や insertView のようなメソッドを提供してくれます。
以下は、.user-listというclassが設定されているul要素にItemViewを追加するコードです。

  var Layout = Backbone.Layout.extend({
    template: require("ldsh!./template"),

    serialize: function() {
      return { users: this.collection };
    },

    beforeRender: function() {
      this.collection.each(function(user) {
        this.insertView(".user-list", new Item({
          model: user
        }));
      }, this);
    },

    afterRender: function() {
      // Only re-focus if invalid.
      this.$("input.invalid").focus();
    },

    initialize: function() {
      // Whenever the collection resets, re-render.
      this.listenTo(this.collection, "reset sync request", this.render);
    },

    events: {
      "submit form": "updateOrg"
    },

    updateOrg: function(ev) {
      app.router.go("org", this.$(".org").val());

      return false;
    }
  });

まぁ、、今だと、Backbone.Marionette を使うほうがいいかもしれませんけどね。。

Lo-Dash Template Loader

もうひとつは、Lo-Dash Template Loader で、Lo-Dash(Underscore)のテンプレートファイル(html)を読み込んで(fetchして)、Viewのテンプレートに設定するためのRequireJSプラグインです。
tbranyen/lodash-template-loader · GitHub
LayoutManagerと組み合わせることで効果を発揮します。
以下のように、ldsh!の後ろにテンプレートファイルのパスを指定します。※拡張子は除く

      // Use main layout and set Views.
      var Layout = Backbone.Layout.extend({
        el: "main",

        template: require("ldsh!./templates/main"),

        views: {
          ".users": new User.Views.List({ collection: this.users }),
          ".repos": new Repo.Views.List({ collection: this.repos }),
          ".commits": new Commit.Views.List({ collection: this.commits })
        }
      });

RequireJSのtextプラグインで、'text!templates/main.html' のようにするのと似ています。

textプラグインと同様、このLo-Dash Template Loaderも、ビルド時にテンプレートファイルを最適化してくれます。
つまり、ビルド時に最適化したjsファイルにテンプレートファイルの内容を埋め込んでくれます。
ただ、textプラグインと異なるのは、このLo-Dash Template Loaderは、テンプレートファイルをコンパイルした状態でjsファイルに埋め込んでくれるところです。※ _.template()を行った後のテンプレートファイルを埋め込みます。
そのため、backbone-boilerplateでリリースビルドしたアプリは、実行時のView表示のタイミングで、htmlファイルのfetchや、テンプレートのコンパイルを行う必要がなく、View表示の速度が向上するように工夫されています。

リリースビルド

最後に、github-viewerをリリースビルドして実行する方法について記載します。
ローカルPCでリリースビルドをして実行する方法は、以下のようにgruntタスクを実行します。

grunt default server:release

ただ、実行してみると、backbone.collectioncach が動作しようとするところでエラーになります。このサンプルアプリを動作させるのに、backbone.collectioncacheは特に不要なので、利用しないようにソースを変更します。
config.jsのbackbone.collectioncacheの設定をコメントアウト

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/jquery",
    "backbone": "../vendor/bower/backbone/backbone",
    "bootstrap": "../vendor/bower/bootstrap/dist/js/bootstrap",
    "layoutmanager": "../vendor/bower/layoutmanager/backbone.layoutmanager"
//    "collectionCache": "../vendor/backbone.collectioncache"
  },

  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"
    },

    // Backbone.CollectionCache depends on Backbone.
//    "collectionCache": ["backbone"],

    // Twitter Bootstrap depends on jQuery.
    "bootstrap": ["jquery"]
  }
});

また、router.js の collectionCache の読み込み処理をコメントアウト

  var Commit = require("components/commit/index");
  var User = require("components/user/index");
  var Repo = require("components/repo/index");

  // require("collectionCache");

これでリリースビルドしたサンプルアプリが動作するようになります。

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

JavascriptをベースにしたHTML5アプリケーションを初めて開発するとき、まず、アプリケーションの基盤をどのように構築すればよいか悩む人もいるのではないでしょうか。

かくいう私も悩みました。

プロジェクトの構成はどうするか、フレームワーク(MVCとか)、テストとか、ビルド、CIなど‥

 

もし、MVCフレームワークにBackbone.jsの利用を考えているなら、backbone-boilerplateを利用してみるのが良いと思います。

https://github.com/backbone-boilerplate/backbone-boilerplate

f:id:qualitas:20140521042907p:plain

これは、Backbone.jsを利用したアプリケーションテンプレートで、Backbone, Underscore(Lo-Dash), jQuery, RequireJS などのJavascriptフレームワークを組み込んだアプリケーションの雛形を提供してくれます。

さらに、gruntを利用したビルドや、Mocha + Chai, Jasmineのようなテストフレームワークを利用したテストも組み込まれています。

また、HTML5サイトを構築するためのテンプレートである html5-boilerplate をベースにしています。

 

2012/1月頃に登場したboilerplateですが、頻繁に更新されていて、Javascriptアプリケーション開発の最新のノウハウが入っていると思います。

 

セットアップ

利用するためには、node.jsが必要なので、あらかじめインストールします。

 

backbone-boilerplateのREADMEに記載のとおりに、コマンドを実施します。

 

1. githubからクローンするか、zipをダウンロードして展開

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

 

【2014/7/14追記】

※2014/6/24 にbackbone-boilerplateがアップデートされました。

本記事執筆時のbackbone-boilerplateを取得する場合、以下のようにしてください。

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

git checkout c698a5482b2ee7369d4972bfa3faed8f8709ea1b

 

2. フォルダ名を変更

mv backbone-boilerplate my-project

3. カレントディレクトリを移動

cd my-project

4. 以下のようにコマンド入力

npm install -g grunt-cli bower

npm install -g coveralls

npm install

bower install

 

利用方法

アプリケーションをローカルPC上で動作させるには、以下のように、gruntタスクを実行します。

grunt server

ブラウザで、http://127.0.0.1:8000/ にアクセスするとアプリが動作します。

ただ、デフォルトだと、以下のように、index.html内にコンテンツが無いので、画面には何も表示されませんが・・

<body>
  <!-- Application container. -->
  <main role="main" id="main"></main>
 
  <!-- Application source. -->
  <!-- build:[src] /source.min.js -->
  <script data-main="/app/main" src="/vendor/bower/requirejs/require.js"></script>
  <!-- /build -->
</body>

テストを実行する

以下のように実行します。

grunt karma:run

テストが実施され、test/coverageフォルダにカバレッジレポートが出力されます。

カバレッジレポートは、デフォルトでは、lcovの形式になっていて、以下のパスに、レポート参照用のindex.htmlが出力されます。

test/coverage/PhantomJS 1.9.7 (Windows 7)/lcov-report/index.html

 

JenkinsのようなCI環境でレポートをカバレッジレポートを表示したい場合は、coberturaの形式でカバレッジレポートを出力させることもできます。

Gruntfile.jsファイルの以下の箇所を、'lcov'から'cobertura'に変更します。

        coverageReporter: {
          type: "cobertura",
          dir: "test/coverage"
        },

以下のパスに、cobertura-coverage.xml が出力されます。

test/coverage/PhantomJS 1.9.7 (Windows 7)/cobertura-coverage.xml

 

backbone-boilerplateを利用することで、JavascriptベースのHTML5アプリケーションに慣れていない人でも、プロジェクト構成やフレームワーク、ビルドなどを、ひととおりおさえることができます。

 

ただ、このbackbone-boilerplateには、アプリケーションのサンプルソースが含まれていません。

このboilerplateを利用したサンプルアプリは、別途githubで公開されているため、そちらを参照する必要があります。

https://github.com/tbranyen/github-viewer

 

backbone-boilerplateについては、今後もブログを書いていきたいと思います。