実践入門 Ember.js

第8回開発ツール(Ember CLI)

前回Ember Dataを使って永続化されたデータを扱う方法を解説しました。今回は、Ember.jsを使った開発を支援する開発ツールを解説します。

今回紹介するEmber CLINode.js製のコマンドラインツールで、次の機能を提供しています。

  • コードの生成
  • JavaScriptのコンパイル
  • テストのサポート
  • デプロイ用のビルド

前準備

さっそく、Ember CLIをインストールして動かしてみましょう。

インストール

まずは前準備としてNode.jsとnpmをインストールします。Node.jsについては公式サイトを参考にインストールしてください。

次はNode.jsのパッケージマネージャであるnpmです。npmはNode.jsに標準添付されていますが、Ember CLIでは最新のnpmを使うことを推奨しているのでアップデートします。

$ npm install -g npm

では、いよいよEmber CLIをインストールします。

$ npm install -g ember-cli

インストールが完了したら、emberコマンドが使えるようになります。本稿の対象バージョンは次の通りです。

$ ember -v
version: 0.2.0
node: 0.12.0
npm: 2.7.1

ここで、Mac OSもしくはLinuxの場合は以下のようなメッセージが表示されることがあります。

Could not find watchman, falling back to NodeWatcher for file system events

このままでも特に動作に問題はありませんが、Ember CLIはファイルの更新検知にwatchmanを推奨しているので、これをインストールするとメッセージを非表示にできます。

以上でインストールが完了しました。ここからはEmber CLIを実際に使ってみましょう。

コードの生成

Ember CLIにはコードを自動生成する機能があり、アプリケーションのひな形を作成するためのコマンドが用意されています。ここではexample-appという名前でアプリケーションを作成することにします。

$ ember new example-app

生成されたファイルとディレクトリのうち特に重要な以下のものについて解説します。

app Ember.jsアプリケーションのクラスとテンプレートを配置するためのディレクトリです。ディレクトリ名、ファイル名には規則があります。これについては後述します。
bower.json Bower用の依存ライブラリのバージョン管理ファイルです。JavaScriptやCSSなど、フロントエンド用のライブラリを管理します。
config/environment.js アプリケーションの設定を記述するファイルです。
dist ビルドされたファイルが配置されるディレクトリです。詳しには後述します。
package.json Node.jsで利用するライブラリのバージョン管理ファイルです。主に、ビルドに関するライブラリとEmber.jsのアドオンのバージョンを記述します。
public ビルドが必要ない配布用のファイルを配置するためのディレクトリです。このディレクトリに置かれたファイルは、そのままdistディレクトリにコピーされます。
tests テストファイルを配置するためのディレクトリです。

生成されたファイルについてひと通りおさえたので、次はアプリケーションをブラウザで表示してみましょう。Ember CLIには動作検証を行うための開発用サーバを起動するコマンドが用意されています。

$ ember server

このコマンドで開発用のサーバが起動します。では、ブラウザで http://localhost:4200にアクセスしてみましょう。

次のメッセージが表示されるのが確認できたでしょうか?

Welcome to Ember.js

これでEmber CLIが生成したアプリケーションのひな形の動作を確認できたので、他の自動生成の機能を試してみましょう。

次はテンプレートを作成します。Ember CLIの自動生成コマンドember generateを利用してindexテンプレートを作成します。

$ ember generate template index

このコマンドを実行すると、app/templates/index.hbsというファイルが生成されます。

これまでの記事では、scriptタグを利用して以下のようにテンプレートを定義してきました。

<script type="text/x-handlebars" date-template-name="index">
</script>

ただこの方法だと複数のテンプレートを定義する場合、それらすべてをひとつにHTMLに埋め込む必要がありました。そのためアプリケーションを拡張していくとテンプレートを管理するのが困難になります。

一方Ember CLIでは、テンプレートはひとつひとつ独立したファイルとして管理されます。date-templateに対応するテンプレート名がファイル名になり、app/templatesディレクトリに保管されます[1]⁠。

では、このapp/templates/index.hbsを編集して表示を確認してみましょう。次の内容をテンプレートに記述してください。

Ember CLI example

そしてブラウザでアプリケーションにアクセスすると、次のように表示されるのが確認できます。

Welcome to Ember.js
Ember CLI example

では続いてコントローラを作成してみましょう。

$ ember generate controller index

このコマンドを実行すると、次のファイルが生成されます。

app/controllers/index.js
import Ember from 'ember';

export default Ember.Controller.extend({
});

ここで初めて登場したimport/exportについて解説します。

JavaScriptのコンパイル

この記述は、次世代のECMAScript(JavaScriptの仕様)として仕様が策定されているES6 module syntaxです。import文で外部のモジュールを取得し、export文でAPIを外部に公開します。とは言っても、現在のブラウザはこの構文をサポートしていないため、Ember CLIはこの構文をAMD形式に変換して現在のブラウザで動作するようにコンパイルします[2]⁠。

Ember CLIはモジュールのファイル名を手がかりに必要なクラスを探索します。この仕組みについては後ほど詳しく解説します。

まずはほかのクラスも生成しつつ全体の流れを掴んでみましょう。次はRouteです。

$ ember generate route index

Routeを生成する際はあわせてtemplateも生成されます。app/templates/index.hbsを上書きするか確認されるので、yをタイプしてください。

[?] Overwrite app/templates/index.hbs? Yes, overwrite

生成したファイルを次のように変更してください。

app/routes/index.js
import Ember from 'ember';

export default Ember.Route.extend({
  model: function() {
    return {
      name: 'Tomster'
    };
  }
});
app/controllers/index.js
import Ember from 'ember';

export default Ember.Controller.extend({
  hello: function() {
    return 'Hello, ' + this.get('model.name');
  }.property('model.name')
});
app/templates/index.hbs
{{hello}}!

ここまで準備ができたら、ブラウザからアクセスしてみましょう。次のように表示されたでしょうか[3]⁠。

Welcome to Ember.js

Hello, Tomster!

ここまでの動きから、Ember.jsがアプリケーションの命名規約が、これまで取り扱ってきたクラス名ではなくファイル名を元にしていることが確認できました[4]⁠。

module syntaxのメリット

モジュールをコンパイルするという仕組みは一見複雑に見えるかもしれませんが、実は大きなメリットがあります。

1. クラスがアプリケーション名に依存しなくなる

App.IndexRouteだとAppというアプリケーションのインスタンスが存在するまでクラスを定義できません。そして、アプリケーション名が異なる別のアプリケーションで再利用できません。

2. クラスの定義順序を気にしなくてよくなる

あるクラスを継承して別のクラスを定義したい場合、これまでの仕組みだと必ず親クラスの定義が先に記述される必要がありました。しかしローダを利用した仕組みではimportによってクラスが必要になった際に読み込まれるため、クラスがファイルのどの部分で定義されているかというのは重要ではなくなります。

そのため、自分で作成したクラスをライブラリとして再利用したり、サードパティのライブラリを取得することも容易になります。Ember CLIではEmber Addonsというアドオン機構があり、コマンドから必要なアドオンをインストールできます。

$ ember install:addon <アドオン名>

ここまでで、今まで行ってきた開発をEmber CLIの仕組みの上で行えるようになりました。次は開発したアプリケーションをテストする方法を見ていきましょう。

テストのサポート

Ember CLIはテストの実行をサポートしています。

テスト実行はテスト支援ツールであるtestemによって行われます。これには次の利点があります。

  • 仮想ブラウザ/実ブラウザを含む複数のブラウザでテストが実行できる
  • ソースコードの変更を検知してテストが自動で実行される

また、テストの実行方法は次の2種類が用意されています。

ember test

シェル経由でテストを実行しTAP形式ででテスト結果を出力します。CI環境向けの実行方法です。

ember test --server

テスト用のサーバを起動して、ファイルの変更を検知して自動でテストを実行します。手元で開発しながらテストを実行する際に適した実行方法です。

Ember CLIのデフォルトの設定では、仮想ブラウザであるPhantomJSが利用されます。もしインストールしていなければ次のコマンドを実行してインストールしてください。

$ npm install -g phantomjs

では、次はテストコードを見ていきましょう。

Unitテスト

実はさきほどクラスのひな形を生成した際、Unitテストのひな形も生成されていました。tests/unitディレクトリの中に、クラス毎のテストが生成されています。

ここではtests/unit/routes/index-test.jsを見てみましょう。

import {
  moduleFor,
  test
} from 'ember-qunit';

moduleFor('route:index', {
  // Specify the other units that are required for this test.
  // needs: ['controller:foo']
});

test('it exists', function(assert) {
  var route = this.subject();
  assert.ok(route);
});

新しく登場した部分を解説します。

ember-qunit

QUnitをベースにEmber用のヘルパーが追加されたライブラリです[5]⁠。

moduleFor

ember-qunitが提供するヘルパーです。引数で指定したモジュールをセットアップします。ここではroutes/indexをセットップし、テスト実行中にthis.subject()で取得できるようにします。

また、次のオプションを指定できます。

  • beforeEach:テスト実行前に行う処理を記述する関数を設定します。
  • afterEach:テストを実行後に行う処理を記述する関数を設定します。
  • needs:依存するモジュールを記述すると、テスト実行時にメインのモジュールと一緒にセットップされます。

クラスを拡張していく際、各クラスの振る舞いを検証したくなった際はここにテストを追加していくことになるでしょう。Unitテストについては公式ドキュメントに詳しく紹介されているため、ぜひこちらにも目を通してみることをお勧めします。

Acceptanceテスト

次は、Acceptanceテストを見ていきましょう。Ember CLIのAcceptanceテストは、ユーザが操作するであろうシナリオごとにテストケースを作成します。そしてJavaScriptでアプリケーションを操作しながら検証を行います。

例えば、ブログシステムに対して「記事を投稿できること」というテストケースを想定してみましょう。

まずはテストのひな形を作成します。

$ ember generate acceptance-test post-an-article

このコマンドを実行すると、次のファイルが生成されます。

// tests/acceptance/post-an-article-test.js
import Ember from 'ember';
import {
  module,
  test
} from 'qunit';
import startApp from 'example-app/tests/helpers/start-app';

var application;

module('Acceptance: PostAnArticle', {
  beforeEach: function() {
    application = startApp();
  },

  afterEach: function() {
    Ember.run(application, 'destroy');
  }
});

test('visiting /post-an-article', function(assert) {
  visit('/post-an-article');

  andThen(function() {
    assert.equal(currentPath(), 'post-an-article');
  });
});

ポイントは次の通りです。

  • beforeEachでアプリケーションを生成afterEachでアプリケーションを破棄することで、テストケース毎に新しいアプリケーションを利用しています。

  • 実際にユーザが行うであろう操作をエミュレートしています。

    ここではvisit()メソッドを使って、画面遷移を行っています。このメソッドはEmber Test Helpersによって提供されています。

Ember Test Helpersが提供するメソッドは次の通りです。

「アプリケーションに対して操作を行うメソッド」

次のメソッドは非同期処理で実行されます。これは、アニメーションや画面遷移など時間のかかる操作が挟まった場合でも、メインスレッドをブロックすることなく指定した操作を順番を行うための工夫です。

visit(url) 引数で指定したurlに遷移します。
fillIn(selector, text) selectorで指定したDOMに対して、value属性にtextを設定します。フォームを埋める際に利用します。
click(selector) selectorで指定したDOMに対してクリックイベントを発火させます。リンクやボタンをクリックする際に利用します。
keyEvent(selector, type, keyCode) selectorで指定したDOMに対してtype(mousemoveやkeypressなどの)キー操作のイベントを発火させます。その際、keyCodeを指定可能です。
triggerEvent(selector, type, options) selectorで指定したDOMに対してtypeのイベントを発火させます。イベントはjQery.Eventオブジェクトとして発火します。optionsjQery.Eventにそのまま渡されます。
「アプリケーションに対して確認を行うメソッド」

次のメソッドは同期処理で実行されます。アプリケーションに対しての確認は何かをブロックすることはないため、同期的にメソッドが実行されます。

find(selector, context) selectorで指定したDOMを取得します。contextを指定すると、特定のDOMの子要素からのみ探索されます。
currentPath() 現在のURLのパスを取得します。
currentRouteName() 現在のRoute名を取得します。
currentURL() 現在のURLを取得します。

同期実行のメソッドと非同期実行のメソッドを混在させたテストシナリオを作成する場合、どのようの扱えばよいのでしょうか? 特に気にせず処理を混在させてしまうと、 操作が完了する前に検証を行ってしまいテストに失敗する場合があります。そのため、同期処理で確認を行う際は以下のように非同期処理が完了するのを待ちます。

andThen(function() {
  // ここで確認を行う
});

andThenはスケジュールされている非同期処理の完了を待ってからコールバックを実行する関数です。

以上を踏まえて、⁠記事を投稿する」というテストケースを組み立ててみると次のようになります。

// tests/acceptance/post-an-article-test.js
import Ember from 'ember';
import {
  module,
  test
} from 'qunit';
import startApp from 'example-app/tests/helpers/start-app';

var application;

module('Acceptance: PostAnArticle', {
  beforeEach: function() {
    application = startApp();
  },

  afterEach: function() {
    Ember.run(application, 'destroy');
  }
});

test('Post an article', function(assert) {
  visit('/post/new');
  fillIn('input.title', 'Ember CLIについて');
  fillIn('input.body', '今回はAcceptanceテストの紹介です。');
  click('input.submit');

  andThen(function() {
    assert.equal(find('.alert-success').text(), '記事を作成しました');
  });
});

以上、Acceptanceテストの解説でした。

デプロイ用のビルド

アプリケーションの開発とテストができるようになったので、次はいよいよデプロイを踏まえたビルドを見ていきましょう。

Ember CLIはソースコードを本番環境へのデプロイするためにビルドする機能があります。ビルドによってアプリケーションのために必要なファイルはdistディレクトリに配置されるので、あとはそのファイルを本番環境にアップロードするとデプロイが完了です。

ソースコードのビルド

ビルドは次のコマンドで実行します。

$ ember build --environment=production

dist/assetsディレクトリには次のファイルが生成されました。

$ ls dist/assets
example-app-1a34084e096955fac1190e92a12cbe92.js  vendor-8ddd9c2582308c7a3a851e4d8333d4f0.js
example-app-d41d8cd98f00b204e9800998ecf8427e.css vendor-d41d8cd98f00b204e9800998ecf8427e.css

ここで何が起こっているのかを見ていきましょう。

コンパイル

ソースコードがコンパイルされ、dist/assetsに配置されます。ここ最近では、JavaScript/CSSを直接記述せずCoffeeScriptSassでソースコードを記述するプロジェクトも増えてきているのではないでしょうか。Ember CLIはこのような言語のコンパイルにも対応しています。また、先ほど解説したmodule syntaxのコンパイルもこのタイミングで行われます。

結合

app以下のファイルはアプリケーション名example-app⁠、app以外から読み込んでいるファイルはvendorという名前のファイルに結合されます。これは、ソースコードをひとつひとつ配布するよりも、一つのファイルに結合した方がHTTPリクエストのオーバヘッドを節約できるためです[6]⁠。

圧縮

配布するファイルは極力サイズを小さくすることでファイル転送にかかるコストを下げられます。Ember CLIはファイルから空白文字や改行コードの削除、さらにはソースコードの最適化を行って極力ファイルサイズを小さくします。また、設定によっては画像ファイルについても圧縮を行えます。

ハッシュ値の付与

生成されたファイルにはファイルの内容のmd5チェックサムが付与されます。これはファイルをWebサーバに配置した際にキャッシュを有効にすることを意図したものです。もしファイルの中身に変更があればハッシュ値付きのファイル名も変更されます。そうすることで、意図しないキャッシュが効いてしまうことを防いでいます。もし不要である場合はオプションで無効にできます[7]⁠。また、CSSの中から参照している画像のパスもハッシュ値が付与されたものに書き換えられます。

このビルドの仕組みはBroccoliというフロントエンドのためのビルドツールを利用しています。ビルドの設定を変更したい場合、Broccoli.jsに設定を記述します。

以上、ビルドについての解説でした。

最後に

今回は開発ツールであるEmer CLIを解説しました。実は今回で「実践入門 Ember.js」は最終回です。

今後もEmber.jsは2.0、そしてさらにその先に向けて進化を続けていくはずです。その進化とともに、みなさんもEmber.jsを使った開発を楽しんでいただければ幸いです。

それでは短い間でしたがお付き合いいただきありがとうございました。

おすすめ記事

記事・ニュース一覧