2013年9月6日

Yeoman 1.0 がリリースされたので使ってみる

ついに Yeoman 1.0 がリリースされました!

Yeoman を使うことでアプリケーション開発を始めるときに設定ファイルやビルド定義ファイルを自動生成できるようになり、 各種ファイル置き場の共通化を図ることができます。

  • Gruntfile ってどう書くの?
  • JavaScript で使う HTML テンプレートファイルの管理ってどうやるの?
  • CoffeeScriptSass の導入って難しい?
  • 監視するファイルが多岐に渡ると LiveReload の設定が難しい ...
  • テストしてスクリプトを圧縮してパッケージに固めてサーバーに置くまでのコマンドが長い ...

こうした疑問や悩みを解決してくれるのが Yeoman の役割です。 Yeoman を使うことで Bower にも依存することになりますので、 jQuery のようなライブラリ/フレームワークを管理する方法も自然と身に付くでしょう。 また、JavaScript の MVC フレームワークを使う場合は、各レイヤーの定型ファイルを生成できます。

この記事では Backbone.js を使って Web アプリケーションのフロントエンドを作ってみます。 そもそも Yeoman って何? という場合は「 Yeomanについて 」というスライドが分かりやすいと思います。

Yeoman の準備

Yeoman のインストールは npm で一発です。 ただし、事前に Node の 0.8 以上、Ruby と Compass を使えるようにしておく必要があります。

$ node -v
v0.10.17

$ npm -v
1.3.8

$ compass -v
Compass 0.12.2 (Alnilam)
Copyright (c) 2008-2013 Chris Eppstein
Released under the MIT License.
Compass is charityware.
Please make a tax deductable donation for a worthy cause: http://umdf.org/compass

NPM から "yo", "grunt-cli", "bower" をインストールします。 -g オプションをつけてグローバル空間にインストールしてください。

$ npm install -g yo grunt-cli bower

どれかが古いバージョンだったりするときちんと動きませんので、 インストールできたバージョンを確認しておきましょう。

$ yo -v
1.0.4

$ bower -v
1.2.5

$ grunt --version
grunt-cli v0.1.9

Yeoman Generator の準備

Yeoman は用途に合わせて "generator" (ジェネレーター) があります。 MVC フレームワークに Backbone を採用する場合と Angular を採用する場合、Ember を採用する場合、 もしくはそれ以外の場合で、必要となるファイルは異なりますし、類似のものであっても配置ルールのベストプラクティスは違います。 ジェネレーターはフレームワークごとの差異を吸収してくれるもので、Yeoman のコアとは独立して開発が進められています。 そのため完成度も対象によってまちまちですから、今のところは開発レポジトリの説明文を見て判断するのが良いでしょう。

たとえば、Backbone のジェネレーターは generator-backbone です。 readme を読むと、簡単な使い方、ジェネレーターの一覧と典型的なワークフロー、それからオプションが記載されています。 貢献方法とライセンス条項 (BSD license) も明記されています。 なお、説明自体は AngularJS のジェネレーター (generator-angular) がもっとも丁寧かもしれません。

Backbone のジェネレーターは以下の5種類が定義されています。 これで Backbone が定義するそれぞれのレイヤーのファイルを生成していくことになります。

  • backbone:model
  • backbone:view
  • backbone:collection
  • backbone:router
  • backbone:all

Backbone 自体に関しては "Developing Backbone.js Applications" という書籍 (プレビュー版) がありますので、 ざーっと眺めてみると良いでしょう。

さて、ここでは "generator-backbone" をインストールします。

$ npm install -g generator-backbone

yo コマンドを実行してみると、Backbone のジェネレーターを実行する選択肢がありますね。 上下の矢印キーで選択肢を移動できますので、ジェネレーターが増えたときも安心です。

generator-backbone を使う

準備が整いましたのでプロジェクトを作成してみます。 ここでは、Web ページの URL をメモするブックマークを考えてみましょう。 まずはプロジェクト用のディレクトリを作成してそこに移動します。

$ mkdir bookmark-memo && cd $_

次に yo コマンドに "backbone" 引数を付けて実行します。

$ yo backbone

jQuery や Backbone などのライブラリは標準で入りますが、 Twitter Bootstrap と RequireJS を含めるかはオプションでの選択式になっています。 不要な場合は矢印キーで選択してスペースキーで ON/OFF を切り替えます。 エンターキーを押すとファイルの生成が始まり、ライブラリのインストールまで実行します。 ライブラリのインストールが不要な場合は yo コマンドを実行するときに --skip-install オプションを付けてください。

処理が終わると以下のファイル群が生成されます。 全容を Finder で見ると右図のようになります。

  • .bowerrc: Bower の設定ファイル、ライブラリを置くディレクトリを定義する
  • .editorconfig: エディタ設定、タブやインデントの定義など
  • .gitattributes: バージョン管理 (Git) で定義する属性
  • .gitignore: バージョン管理 (Git) に含めないファイルパターン
  • .jshintrc: JSHint の設定
  • Gruntfile.js: ビルド定義ファイル
  • app/: アプリケーション置き場
  • bower.json: フロントエンドのライブラリの依存関係を定義、Bower が解決
  • node_modules/: NPM がインストールしたツール群
  • package.json: ツール用のライブラリの依存関係を定義、NPM が解決
  • test/: テストコード置き場

続いて、Router を定義します。 先ほどは yo コマンドに "backbone" とだけ指定しましたが、今度は "backbone:router" と指定します。 アプリケーション全体の状態を管理するので、"app" という名前にします。 これで backbone ジェネレーターの router を生成する処理が実行され、 app/scripts/routers 配下に app.js が生成されます。

$ yo backbone:router app
   create app/scripts/routes/app.js

app/scripts/routes/app.js は次の通りです。 RequireJS の作法に則って処理が記載され、JSHint でエラーが出ないように define はグローバルに宣言されている、という コメントが盛り込まれていますね。 routes の部分はアプリケーションに合わせてハッシュフラグメントとのマッピング処理を記載してください。 ここでは特に何もしません。

/*global define*/

define([
    'jquery',
    'backbone'
], function ($, Backbone) {
    'use strict';

    var AppRouter = Backbone.Router.extend({
        routes: {
        }

    });

    return AppRouter;
});

先ほども記載しましたが、 generator-backbone には5つのターゲットがあります。 このうち、 "backbone:model", "backbone:view", "backbone:collection" の3つを使ってアプリケーションを構築していくことになります。 使い方は "backbone:router" と同様で、引数にモデルやビューの名称を与えると、それに則したファイルが生成されます。 次の章で順に見ていきましょう。

Model と Collection を定義する

URL をメモするブックマークを考えてみるので、 "bookmark" というモデルを定義しましょう。

$ yo backbone:model bookmark
   create app/scripts/models/bookmark.js

先ほどの router と同様ですが、 Backbone.Model を継承したクラスが定義されます。 プロパティとして "url", "title", "note" を持つようにしておきましょう。

/*global define*/

define([
    'underscore',
    'backbone'
], function (_, Backbone) {
    'use strict';

    var BookmarkModel = Backbone.Model.extend({
        defaults: {
            url: '',    // 手動で追加
            title: '',  // 手動で追加
            note: ''    // 手動で追加
        }
    });

    return BookmarkModel;
});

次に、モデルの集合を扱うコレクションを定義します。

$ yo backbone:collection bookmark
   create app/scripts/collections/bookmark.js

今度は Backbone.Collection を継承したクラスが定義され、名前付けのルールにより、 BookmarkModel と関連付いていることが分かります。

/*global define*/

define([
    'underscore',
    'backbone',
    'models/bookmark'
], function (_, Backbone, BookmarkModel) {
    'use strict';

    var BookmarkCollection = Backbone.Collection.extend({
        model: BookmarkModel
    });

    return BookmarkCollection;
});

RequireJS を使い始めるとファイル同士のパスの関係がややこしくなってしまうこともありますが、 Yeoman のジェネレーターに沿って定義していくことで余計なタイプミスを防止できます。 ただ、Backbone を使うとファイルが増えてしまう、というジレンマは解決できません。 本当に小さいアプリケーションではよく検討した方が良いでしょうし、 Backbone.Marionette のようなライブラリを被せることも検討に値します。 引き続き View の定義に進みますが、ここまででなんか複雑、、、という場合は generator-webapp から初めてみると良いでしょう。 こちらは MVC フレームワークを使わず、良い感じに Gruntfile などを生成してくれるものです。

View を定義する

ビューもモデルやコレクションと同じ要領で定義していくのですが、 名前付けのルールは少々注意した方が良いかもしれません。 "bookmark" という名前だとモデルのビューなのかコレクションのビューなのか紛らわしくなってしまいます。 そこで、モデルのビューは "bookmark-item", コレクションのビューは "bookmark-list" とします。

$ yo backbone:view bookmark-item
   create app/scripts/templates/bookmark-item.ejs
   create app/scripts/views/bookmark-item.js

$ yo backbone:view bookmark-list
   create app/scripts/templates/bookmark-list.ejs
   create app/scripts/views/bookmark-list.js

ビューを定義すると、JavaScript ファイルだけでなく HTML を記述するテンプレートファイルも生成されます。 オプションでテンプレートエンジンも選択できるのですが、デフォルトでは underscore.js の組み込みテンプレートを使います。

app/scripts/views/bookmark-item.js は以下のようになっています。 define の引数に "templates" が新しく加わり、 JST という変数で受け取っています。 JST には辞書形式でアクセスでき、テンプレートファイルのパスがキーになっています。

/*global define*/

define([
    'jquery',
    'underscore',
    'backbone',
    'templates'  // 自動生成されたファイルを参照
], function ($, _, Backbone, JST) {
    'use strict';

    var BookmarkItemView = Backbone.View.extend({
        template: JST['app/scripts/templates/bookmark-item.ejs']
    });

    return BookmarkItemView;
});

実は、 app/scripts/templates ディレクトリで拡張子が ".ejs" のファイルは "templates.js" としてまとめられます。 具体的には grunt-contrib-jst というタスクが実行されます。 Gruntfile.js には以下の定義が記載されており、 ".tmp" ディレクトリにまとめたスクリプトが自動生成されます。 livereload は app.tmp を同じように処理する設定になっていますので、RequireJS の define でアクセスできるのです。

jst: {
    options: {
        amd: true
    },
    compile: {
        files: {
            '.tmp/scripts/templates.js': ['<%= yeoman.app %>/scripts/templates/*.ejs']
        }
    }
},

grunt タスクの組み合わせを考えると、 ".ejs" ファイルの変更を検知すると jst タスクが実行され、 ファイルが生成されると livereload を通じて変更内容がブラウザに反映されます。 この一連の流れを手動で実現すると骨が折れると思いますので、Yeoman を使う利点のひとつと言えます。 (逆に、ピンと来ない場合は Yeoman を使うメリットは大きくないかもしれません。)

では、実際に View の動きを確認してみましょう。 アプリケーションのエントリーポイント (app/scripts/main.js) を編集します。 ( require.config の部分は省略)

require([
    'backbone',
    'views/bookmark-item'  // ビューのクラスを読み込む
], function (Backbone, BookmarkItemView) {
    Backbone.history.start();
    var view = new BookmarkItemView();  // インスタンス化
    console.log(view.template());       // ビューのテンプレートを文字列化
});

ターミナルで grunt コマンドからサーバーを起動します。ブラウザも自動で該当のページを開きます。 ( "open:server" のタスク)

$ grunt server
Running "server" task

Running "clean:server" (clean) task

Running "coffee:dist" (coffee) task

Running "createDefaultTemplate" task

Running "jst:compile" (jst) task
File ".tmp/scripts/templates.js" created.

Running "compass:server" (compass) task
directory .tmp/styles/
   create .tmp/styles/main.css

Running "connect:livereload" (connect) task
Started connect web server on localhost:9000.

Running "open:server" (open) task

Running "watch" task
Waiting...

JavaScript コンソールを開くと bookmark-item.ejs の内容が出力されているはずです。 これで、HTML は ".ejs" ファイルで定義し、イベントは ".js" ファイルで処理する、という構図になります。

"Developing Backbone.js Applications" を参考にすると、 モデルのビュー、コレクションのビューは以下のように "events", "initialize", "render" を定義すると良さそうです。

モデルのビュー (app/scripts/views/bookmark-item.js)

var BookmarkItemView = Backbone.View.extend({

    template: JST['app/scripts/templates/bookmark-item.ejs'],

    tagName: 'tr',  // <table> タグでレイアウトする場合

    events: {
    },

    initialize: function() {
        this.model.on('change', this.render, this);
        this.model.on('destroy', this.remove, this);
    },

    close: function() {
        // unbind the events that this view is listening to
        this.stopListening();
        this.model.destroy();
    },

    render: function() {
        this.$el.html( this.template( this.model.toJSON() ) );
        return this;
    }

});

コレクションのビュー (app/scripts/views/bookmark-list.js)

var BookmarkListView = Backbone.View.extend({

    template: JST['app/scripts/templates/bookmark-list.ejs'],

    tagName: 'table',  // <table> タグでレイアウトする場合
    className: 'table',  // Twitter Bootstrap を使う場合

    events: {
    },

    initialize: function() {
        this.collection.on('add', this.addOne, this);
        this.collection.on('reset', this.reset, this);
    },

    close: function() {
        // unbind the events that this view is listening to
        this.stopListening();
    },

    addOne: function(model) {
        var v = new ItemView({model: model});  // RequireJS の define で 'views/bookmark-item' を読み込み ItemView として受け取る
        this.$('tbody').append( v.render().el );
    },
    reset: function() {
        this.$('tbody').empty();
    },

    render: function() {
        this.$el.html( this.template() );
        this.collection.each(this.addOne);
        return this;
    }
});

それぞれのビューに対するテンプレートは以下のようにしておきます。

モデルのテンプレート (app/scripts/templates/bookmark-item.ejs)

<td>
  <a href="<%= url %>"><%= title %></a>
</td>
<td>
  <%= note %>
</td>

コレクションのテンプレート (app/scripts/templates/bookmark-list.ejs)

<thead>
  <tr>
    <th>Bookmark</th>
    <th>Note</th>
  </tr>
</thead>
<tbody>
</tbody>

これでモデル、コレクション、ビューが一通り揃いました。

アプリケーションとしてまとめる

バラバラとクラスを定義してきましたが、これらをまとめる作業に移ります。 アプリケーション全体のビューを定義して、 main.js から読み込むようにします。 そして、 index.html に表示する部分を定義し、 main.scss で見た目を調整します。

まずは app というビューを定義します。

$ yo backbone:view app
   create app/scripts/templates/app.ejs
   create app/scripts/views/app.js

app/scripts/views/app.js ではコレクションとそのビューを読み込むようにします。 initialize でそれぞれのインスタンスを生成し、 render で描画します。

/*global define*/

define([
    'jquery',
    'underscore',
    'backbone',
    'templates',
    'collections/bookmark',  // コレクションの定義を読み込む
    'views/bookmark-list'    // モデルの定義を読み込む
], function ($, _, Backbone, JST, BookmarkCollection, BookmarkListView) {
    'use strict';

    var AppView = Backbone.View.extend({

        template: JST['app/scripts/templates/app.ejs'],

        initialize: function() {
            this.collection = new BookmarkCollection();
            this.view = new BookmarkListView({collection: this.collection});
        },

        render: function() {
            this.$el.html( this.template() );
            this.$el.append( this.view.render().el );  // サブビューも描画する
            return this;
        }

    });

    return AppView;
});

テンプレートファイル (app/scripts/templates/app.ejs) は空白にします。 アプリケーションにメニューを設置したい、ヘッダーやフッターも動的に変更したい、 そんな場合はこのテンプレートファイルでレイアウトを調整することになるでしょう。

次に、アプリケーションのエントリーポイント (app/scripts/main.js) を編集します。 ここではテストデータを与えるようにしてみましょう。 ( require.config の部分は省略)

Backbone.history.start();

var app = new AppView({el: '.container'});
app.render();

// Add test data
app.collection.add({
    title: 'Google',
    url: 'http://google.com',
    note: '検索エンジン'
});
app.collection.add({
    title: 'Yahoo!',
    url: 'http://yahoo.com',
    note: 'ポータルサイト'
});
app.collection.add({
    title: 'Facebook',
    url: 'http://facebook.com',
    note: 'SNS'
});

そもそもの表示領域は app/index.html で定義します。 ジェネレーターで生成されたボイラープレートは以下のようになっていますが、 <div class="container"> の子要素は削除してしまいましょう。

<div class="container">
    <div class="hero-unit">     ← ここから削除
        <h1>'Allo, 'Allo!</h1>
        <p>You now have</p>
        <ul>
            <li>HTML5 Boilerplate</li>
            <li>jQuery</li>
            <li>Backbone.js</li>
            <li>Underscore.js</li>
            <li>RequireJS</li>
            <li>Twitter Bootstrap</li>
        </ul>
        <p>installed.</p>
        <h3>Enjoy coding! - Yeoman</h3>
    </div>                      ← ここまで削除
</div>

最後に SCSS のスタイルシート (app/styles/main.scss) を編集します。 ちょっと余白を調整してみましょう。

@import 'sass-bootstrap/lib/bootstrap';

.container {
    margin: 50px auto 0 auto;
    width: 600px;
}

以上でブックマークっぽいものが出来ました。 入力用のビューも作成し、テストデータとして記述した部分をサーバーサイドやローカルストレージと連動させることで、 もう少し「動く」ものになると思います。

リリース用にビルドする

実装が一通り終わったら、リリース用のビルドします。 テストケースを流して、各種ファイルのミニフィケーションと結合を進めます。 index.html にコメントで記載されたビルド設定に従いファイル名の調整まで実施してくれます。

一連の流れは Gruntfile.js に記述されていますので、やるべきことは非常に簡単です。

$ grunt

コンソールにしばらくメッセージが流れ、各タスクの処理時間が表示されるはずです。 ビルドが終わると dist ディレクトリに成果物が保存されます。 後はこれをアーカイブにまとめるなり rsync で転送するなりして、ターゲット環境に転送します。 それもまとめて! という場合は grunt-rsync などがありますので、自分で組込んでみると良いでしょう。

終わりに

Yeoman 1.0 がリリースされたので使ってみました。 よく考えられたワークフローと、急速な広がりを見せるエコシステムを組み合わせることで、 アプリケーションの雛形をスムーズに構築できます。この記事では触れていませんが、 grunt のタスクには CoffeeScript のコンパイルも含まれています。

Github の Wiki にはいくつかのブログ記事 (英語) へのリンクがあります。 Adobe でも記事を書いている人がいますので、HTML5 プラットフォーム上ではアプリ開発の立ち上げ方法として有力な選択肢になるかもしれません。

Yeoman に関するこれまでの記事はこちらから。 プロジェクトが発足してからはもう一年になるんですね。

コメントを投稿