久々にBackboneを使う機会があり、それならとMarionetteでダンスさせることにしました。
MarionetteはBackboneの冗長になりがちなBoilerPlate部分を吸収してくれるフレームワークです。
Backboneを使ったことがある人は、アプリケーションの構造として、どうあるべきか悩んだことはないでしょうか。
Marionetteには、アプリケーションクラス、モジュールクラスというアプリケーションの構造を表現するクラスがあります。
しかし、Marionetteでアプリケーションを作成する標準的な方法に関する情報も少なく、
クラスの仕様を理解するだけでは、使いこなすことが難しそうです。
今回は、Marionette.Moduleを使ってアプリケーションをモジュール化し、なおかつ、CommonJS準拠のモジュール管理をBrowserアプリでも実現できるBrowserifyを利用して、アプリケーションの骨格を考えてみます。
【お知らせ 15.09.10】
Backbone.Marionetteのモジュール機能がv3(現在はv2.4)から廃止されるようなので、次回の予定はなくなりました。また、モジュールバンドラもbrowserifyからwebpackの利用が増えているようなので、次回の代わりとして、以下のQiitaの記事を公開いたしました。ご参考まで。
目次
- CommonJSについて
- Backbone.Marionetteのモジュール
- サンプルアプリケーションビルド
- package.json
- Gruntfile.coffee
- Browserifyタスク
- Browserify:appタスク
- Browserify:vendorタスク
- Browserify:specタスク
- テスティング環境
- 次回へ
CommonJSについて
CommonJSの実装としては、Node.jsが有名です。Node.jsでは、Javascriptモジュールを読み込むのにrequireを利用します。
しかし、ブラウザで動作するJavascriptには、モジュールをロードする方法がなく、scriptタグで必要なJavascriptを読み込みます。
RequireJSを利用している方もいると思いますが、Browserifyは比較的簡単に利用できます。最近ではwebpackを使っている人もいるでしょう。
Backbone.Marionetteのモジュール
話を戻しますが、MarionetteのモジュールクラスはCommonJSに準拠したものではなく、あくまでMarionetteフレームワーク内のモジュールクラスです。
ちなみにMarionette.Moduleのドキュメントには以下のように紹介されています。
Marionette Modules allow you to create modular encapsulated logic. They can be used to split apart large applications into multiple files, and to build individual components of your app.
モジュールの役割としては、アプリケーションの分割(ファイル分割も含む)と隠蔽化ということだけで、どのように使うのは自由だという感じです。
たとえば、機能ごとにビユーとコントローラーをモジュールしましょうというような方針は特に見当たりません。結局、Backboneと同様に好きにしていいよという感じかなと思っています。
しかし、これでは進めようがないので、以下の書籍を参考にすることにしました。
ref. Backbone.Marionette.js: A Gentle Introduction by Devid Sulc
この書籍のチュートリアルを参考にしながら、Marionette.Moduleの使い方を探ってみたいと思います。
特に、この書籍の説明をするつもりはありませんので(全部、読んでないので…)、興味のある方は原書を読んでいただければと思います。
サンプルアプリケーションビルド
Browserifyの利用方法やテスティング環境の構築には、以下のエントリを参考にさせていただきました。Browserifyのバージョンが異なるためか解決できなかったこともありますが、大変参考になりました。
ref. browserify in Backbone.Marionette project
ref. Karma for JavaScript test runner
今回は、サンプルアプリケーションの構成の説明をかねて、Browserifyによるビルド方法をご紹介します。このエントリでは、Gruntを利用します。最初に必要なモジュールを準備します。
Browserifyのバンドルに関しては、qiitaのエントリにしましたので、こちらもあわせて参照してください。Gulpでもビルドしてみました。
Backbone.MarionetteアプリをBrowerifyでバンドルする(Grunt編)
Backbone.MarionetteアプリをBrowerifyでバンドルする(Gulp編)
package.json
利用するパッケージの内容と各バージョンは以下の通りです。
npmでインストールすると、バージョンが”^x.y.z”となりますが、あえて”~x.y.z”としています。
完全にバージョンを固定したい場合は、npm shrinkwrap
を実行するとnpm-shrinkwrap.jsonファイルを作成してくれます。これはGemfile.lockやcomposer.lockと同様のもののようです。
なんとなく、browserify,grunt-browserifyのメジャーバージョンのあがり方が急激な気がしますが、Semantic Versioning(http://semver.org/)への準拠ということなのかもしれません。
{ "name": "backbone.marionette", "version": "0.0.1", "description": "scaffold", "scripts": { "test": "karma start" }, "author": "kan", "devDependencies": { "matchdep": "~0.3.0", "grunt": "~0.4.5", "grunt-browserify": "~3.2.1", "handlebars": "~2.0.0", "power-assert": "~0.10.0", "hbsfy": "~2.2.1", "espowerify": "~0.10.0", "normalize-css": "~2.3.1", "jquery": "~2.1.1", "backbone": "~1.1.2", "underscore": "~1.7.0", "backbone.marionette": "~2.3.0", "mocha": "~2.0.1", "karma": "~0.12.28", "karma-mocha": "~0.1.10", "karma-mocha-debug": "~0.1.2", "karma-safari-launcher": "~0.1.1", "karma-html2js-preprocessor": "~0.1.0" } }
Backbone.Marionetteに関するパッケージは以下の通りです。
backbone.marionetteは、2.2.2から2.3.0へアップデートしました。主な変更点は最適化とリファクタリング、パフォーマンス向上しているそうです。
"jquery": "~2.1.1", "backbone": "~1.1.2", "underscore": "~1.7.0", "backbone.marionette": "~2.3.0", "handlebars": "~2.0.0",
最近はJavascriptやCSSもノードパッケージから取得するようにして、bowerを使わなくなりました。その方が管理が簡単ですね。
Browserifyに関するパッケージは、以下の通りです。
"grunt-browserify": "~3.2.1", "hbsfy": "~2.2.1", "espowerify": "~0.10.0",
テンプレートエンジンはHandelbarsを利用しています。hbsfyはHandlebarsの.hbsファイルのトランスフォームです。hbsファイルをプリコンパイルします。
espowerifyはpower-assertのためのBrowserifyトランスフォームです。これでテストコードをpower assert化します。BrowserifyはCoffeeScriptのソースもトランスパイルできますが、今回は利用していません。
テスティング関連のパッケージは、以下の通りです。
"power-assert": "~0.10.0", "mocha": "~2.0.1", "karma": "~0.12.28", "karma-mocha": "~0.1.10", "karma-mocha-debug": "~0.1.2", "karma-safari-launcher": "~0.1.1", "karma-html2js-preprocessor": "~0.1.0"
テストランナーはKarma、テストフレームワークにはMochaを、アサーションにはpower-assertを使います。テストブラウザはSafariを使っていますが、ヘッドレスのPhantomJSでもいいでしょう。
Gruntfile.coffee
最近では、gulpを使う人も増えているようですが、gruntも健在ですよね。
いままではGruntfile.jsとしてJavascriptで記述していたのですが、せめてここだけでもCoffeeScript使おうということで、今回はCoffeeScriptで記述しています。
単に、js2coffeeコマンドで変換して、少し修正しただけですけど、js2coffee(https://www.npmjs.com/package/js2coffee)も便利ですね。
Gruntfileを見るだけで、プロジェクトのディレクトリ構成など、なんとなくつかめるだろうと思いますが、簡単に説明しておきます。
ディレクトリ構成
package.json node_modules/ Gruntfile.coffee assets/ # ソースコード js/ /template # hbsファイル css/ specs/ # テストコード karma.conf.js # karma initで生成します。 public/ # 公開ディレクトリ assets/ js/ css/
Gruntfile
module.exports = (grunt) -> pkg = grunt.file.readJSON("package.json") remapify = require 'remapify' grunt.initConfig dir: lib: "./node_modules" src: "./assets" hbs: "./template" dst: "./public/assets" copy: csslib: files: [ expand: true cwd: "<%= dir.lib %>/normalize-css/" src: ["normalize.css"] dest: "<%= dir.dst %>/css/" ] browserify: options: preBundleCB: (b) -> b.plugin remapify, [ { cwd: "./assets/js" src: "*.js" expose: "nzkc" } ] app: files: "<%= dir.dst %>/js/app.js": [ "<%= dir.src %>/js/*.js" "<%= dir.hbs %>/*.hbs" ] options: transform: ["hbsfy"] ignore: ["<%= dir.src %>/js/vendor.js"] external: [ "jquery" "underscore" "backbone" "backbone.marionette" ] alias: [ "<%= dir.hbs %>/template1.hbs:proj/template/template1" "<%= dir.hbs %>/template2.hbs:proj/template/template2" "<%= dir.hbs %>/template3.hbs:proj/template/template3" ] vendor: files: "<%= dir.dst %>/js/vendor.js": ["<%= dir.src %>/js/vendor.js"] options: alias: [ "jquery" "underscore" "backbone" "backbone.marionette" ] spec: files: "./specs/spec.js": ["./specs/app-spec.js"] options: transform: ["hbsfy", "espowerify"] alias: [ "<%= dir.hbs %>/template1.hbs:proj/template/template1" "<%= dir.hbs %>/template2.hbs:proj/template/template2" "<%= dir.hbs %>/template3.hbs:proj/template/template3" ] external: [ "jquery" "underscore" "backbone" "backbone.marionette" ] watch: app: files: [ "<%= dir.src %>/js/*.js" "<%= dir.src %>/template/*.hbs" ] tasks: ["browserify"] spec: files: ["specs/*.js"] tasks: ["browserify:spec"] require("matchdep").filterDev("grunt-*").forEach(grunt.loadNpmTasks) grunt.registerTask "default", ["watch"]
Browserifyタスク
Browserifyタスクで、vendor.js,app.js,spec.jsを生成します。
前述のエントリーで大変参考になったのは、 browserifyの出力ファイルを分割するところです。
いままで、ひとつのファイルにまとめるしかないのだろうと思っていました。
また、相対パスでrequireのパスを記述するのが面倒なので、マッピングとエイリアスを利用します。
grunt-browserifyの2.x以前ではaliasMappingが使えたのですが、今では(3.2.1)deprecatedされてしまいました。2.x以降で同様なことを行うには、remapifyを利用すればできるようですが、思ったように動作しませんでした。
ref. https://github.com/jmreidy/grunt-browserify
A note on alias mappings, which was a functionality pre 2.0 that was removed: its behavior can be reproduced with @joeybaker's remapify plugin, as demonstrated in the code below:
具体的には、以下のようにbrowserifyタスクの共通optionsでremapifyにマッピング情報(.jsと.hbs)を設定したいのですが、引数のマッピング情報を二つ以上渡すと、Fatal error: Cannot read property '1' of null
となります。
これも微妙でcwdで指定したパスにファイルが存在しなければ、とりあえずエラーは回避できるという状況です。
remapify = require 'remapify' browserify: options: preBundleCB: (b) -> b.plugin remapify, [ { cwd: "./assets/js" src: "*.js" expose: "app" } { cwd: "./template" src: "*.hbs" expose: "app/template" } ]
仕方なく、hbsファイルに関してはaliasを併用して、動作するようにしましたが、ここは改善したいところです。
browserify: options: preBundleCB: (b) -> b.plugin remapify, [ { cwd: "./assets/js" src: "*.js" expose: "nzkc" } ] app: files: "<%= dir.dst %>/js/app.js": [ "<%= dir.src %>/js/*.js" ] options: transform: ["hbsfy"] ignore: ["<%= dir.src %>/js/vendor.js"] external: [ "jquery" "underscore" "backbone" "backbone.marionette" ] alias: [ "<%= dir.hbs %>/template1.hbs:proj/template/template1" "<%= dir.hbs %>/template2.hbs:proj/template/template2" "<%= dir.hbs %>/template3.hbs:proj/template/template3" ] vendor: files: "<%= dir.dst %>/js/vendor.js": ["<%= dir.src %>/js/vendor.js"] options: transform: ["hbsfy"] alias: [ "jquery" "underscore" "backbone" "backbone.marionette" "<%= dir.hbs %>/template1.hbs:proj/template/template1" "<%= dir.hbs %>/template2.hbs:proj/template/template2" "<%= dir.hbs %>/template3.hbs:proj/template/template3" ] spec: files: "./specs/spec.js": ["./specs/app-spec.js"] options: transform: ["hbsfy", "espowerify"] alias: [ "<%= dir.hbs %>/template1.hbs:proj/template/template1" "<%= dir.hbs %>/template2.hbs:proj/template/template2" "<%= dir.hbs %>/template3.hbs:proj/template/template3" ] external: [ "jquery" "underscore" "backbone" "backbone.marionette" ]
Browserify:appタスク
Browserifyタスクで生成するファイルは、vendor.js,app.js,spec.jsの3つです。
アプリケーションでは、vendor.jsとapp.jsを読み込みます。
vendorタスクでは、jquery,underscore,backbone,backbone.marionetteをエイリアスし、
appタスクでは、これをexternal宣言しておきます。
最初はfilesに<%= dir.hbs %>/*.hbs"
を追加していましたが、ビルドは正常だが、実行時にテンプレートがrequireできなくなりました。aliasのみとすると正常に動作します。この辺の理由もよくわかっていないです。
Browserify:vendorタスク
vendorタスクで読み込むソース(vendor.js)は以下のような単純なものです。
var Backbone = require('backbone'); Backbone.$ = require('jquery'); require('backbone.marionette');
Backboneをロードした後、Backbone.$にjqueryを読み込んでおく必要があるようです。
Browserifyが出力するファイル(vendor.js)は以下のような形になります(一部)。
require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ var Backbone = require('backbone'); Backbone.$ = require('jquery'); require('backbone.marionette'); },{"backbone":"backbone","backbone.marionette":"backbone.marionette","jquery":"jquery"}],2:[function(require,module,exports){ (以下、省略)
先頭はbrowserifyのおまじないコードでrequireでモジュールをロードするファンクションです。
Browserifyを利用したことがある人にとっては、なじみのコードです。初めて見る人はなんだこれ?って感じになりますね。
Browserifyを知らずに、初めてこのコードを見たとき、CommonJS準拠のローダを実装しているようだが、トリッキーなコードを書くなと思って、一生懸命追いかけました(笑)。
Browserifyを使うとNode.jsのモジュールもブラウザで利用できるようになります。これも魅力的なところでしょう。
Browserify:specタスク
specタスクでも、同様にexternal宣言をしておき、アプリケーション
(app.js,entities.js,module.js,.hbsファイル)モジュールも取り込みたいのですが、ここも、いまいちうまくいっていないところで、前述したようにaliasで無理矢理読み込んでいます。app.jsとvendor.jsのように分離することも難しいようですが、せめてファイルが増えてもGruntfileを触らなくてもいいようにしたいですね。
テスティング環境
karmaをインストール後、karma initを実行して、対話形式でkarma.conf.jsを作成しておきます。
$ npm install karma $ npm install -g karma-cli $ karma init
karma.conf.jsの設定ポイントは、以下のとおりです。今回修正したのは以下3点だけです。
frameworkにmochaとmocha-debugを設定し、必要なJavascriptとテスト時に必要なDOMを生成するためのfixture.htmlを用意し、browserにSafariを指定します。
karma.conf.js
// frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter frameworks: ['mocha-debug', 'mocha'], // list of files / patterns to load in the browser files: [ 'public/assets/js/vendor.js', 'specs/fixture.html', 'specs/setup.js', 'specs/spec.js' ], // start these browsers // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher browsers: ['Safari'],
karmaを起動し、gruntでwatchタスクを起動して、後は好きなエディタでJavascriptソースファイルを修正していきます。
npm test
とすると、package.jsonのscripts:testを実行できます。node_modules下のコマンドなら、フルパスを書く必要はありません。
"scripts": { "test": "karma start" },
$ npm test $ grunt watch
実際には、それぞれ別のターミナルで起動しています。
これでJavascrptファイルやhbsファイルに変更があれば、Browserifyタスクが自動的に実行され、Karmaのテストも自動的に再実行されるようになります。
次回へ
今回はサンプルアプリのJavaScriptソースファイルをBrowserifyでビルドし、テスティングするところまで説明しました。
次回は、本題であるBackbone.Marionetteのモジュールの簡単な説明と、Browerifyの利用を考慮したコーディング方法をご紹介します。