『AngularJS: Up and Running』読書メモ 第10章 ngRouteによるルーティング

AngularJS: Up and Running

AngularJSはngRouteというモジュールをオプションで提供している。これを使うと、AngularJSアプリケーションでルーティング(URLに応じた画面遷移)を実装することができる。

※本書が執筆された当時はngRouteが唯一の公式ルーターだったが、2015年にNew Routerというルーターが導入されている。このルーターはAngularJS の1.4以上と2系のいずれでも使えるという特徴がある。

シングルページアプリケーションのルーティング

シングルページアプリケーションにおけるルーティングにおいては、URLとは通常のURLではなく、シバンの追加されたURLである。通常のURLは以下のようなスタイルである。

  • http://example.com/first/page

これに対して、シングルページアプリケーションのURLは以下のようになる。

  • http://example.com/#/first/page
  • http://example.com/#!/first/page

このようになる理由は、ブラウザはハッシュを含むURLを、含まないURLとは違った形で解釈するからである。ブラウザは、#の後ろにある部分をサーバへのリクエストの際に無視する。そのため、#の後ろの部分が変化しても、サーバへのリクエストは行われない。

ngRouteを使う

AngularJSのルーティングモジュールを使う手順は以下のとおり。

1 モジュールのソースコードを読み込む

2 モジュールの依存関係を定義する

3 ページ内のどの部分をAngularJSが変更すべきか印をつける。ngRouteを使う場合、ng-viewディレクティブをHTML内で使う。

4 $routeProviderサービスを使ってconfigセクションでルーティングを定義する

$routeProviderを使うことで、各ルートをwhen()メソッドで定義できる。when()メソッドは、2つの引数をとる。

  • 第1引数はURL、またはこのルートを適用すべきURLの正規表現
  • 第2引数は設定オブジェクトで、そのルートで何が起きるべきかを指定する

otherwize()関数を使うことで、未定義のルートにユーザが遷移しようとした時の振る舞いを決めることができる。

ルーティングオプション

$routeProvider.whenメソッドの第2引数には以下のような形でオプションを渡すことができる。

  • url: URLまたはURLの正規表現。/user/:userId のようにプレースホルダーを設定することも可能
  • template: HTML文字列
  • templateUrl: テンプレートへのパス
  • controller: コントローラー名の文字列又はコントローラーを定義する関数・配列
  • controllerAs: テンプレート内でアクセスするためのコントローラーの別名
  • redirectTo: 別のルートにリダイレクトする
  • resolve: ルートが実行される前に実行される非同期の処理を記述する(認可、認証等に使う)

resolveによるルート実行前のチェック

resolveによって、ルートが読み込まれる前に実行される非同期の処理を定義することができる。

2番目のルート/protectedには、resolveが定義されている。連想配列にimmediateasyncというキーが定義されているが、キーには任意の値を使用できる(myKey1, myKey2でも何でもいい)。連想配列の値は、AngularJSのDIのシンタックスの配列になる。

ルーティングのresolveについて、AngularJSは以下を保証する。

  • resolveが値を返した場合、AngularJSはこの関数の実行を完了し、成功したresolveとして扱う
  • resolveがpromiseオブジェクトを返した場合、AngularJSはpromiseが値を返すのを待って、promiseの結果に応じて成功/失敗として扱う
  • 全てのresolve関数が完了するまで、ルートは読み込まれない
  • 全てのresolveは並列に実行される
  • いずれかのresolveがエラーに遭遇するか、promiseがrejectされたら、AngularJSはそのルートを読み込まない

上のサンプルコードでは、immediateのresolveは、定数を返すだけなので、毎回成功する。asyncはサーバとの通信を行い、成功すれば、ルートが読み込まれる。失敗すると、コントローラーは読み込まれず、対応するHTMLも表示されない。この場合、ユーザーは最後に表示されたページに留まる。

もう一点重要なことは、それぞれのresolveの戻り値は、それぞれのキーによってコントローラーに注入される、ということだ。上のサンプルコードでは、immediateは数値、asyncはpromiseオブジェクトになる。

$routeParamsサービスを使う

Single Page Application(SPA)では、ルートのコンテキストが必要になることが多い。たとえば、/users/1に対して、id=1のユーザの情報を表示する、といった処理が必要になる。

理想的には、コントローラーとルートは、独立してアクセス可能であるべきである。これらのURLパラメータには、$routeParamsサービスを使ってアクセスできる。

このルートにおける、:detIdはプレースホルダーである。ここにある値は$routeParams.detIdで取得できる。

※ngRouteにはURLに制約を付ける機能がないため、/detail/1だけでなく/detail/fooでもアクセスできる。後者の場合、detIdは’foo’になる。

また、URLに含まれるクエリストリングも取得できる。/detail/1?q=fooにアクセスすると、以下のようなデータが取得できる。

注意すべきこと

空のテンプレート

ngRouteは、それぞれのルートが空ではないtemplateまたはtemplateUrlに関連付けられていることを要求する。テンプレートが存在しない場合、ngRouteはこのルートを無視する。空文字列のtemplateも、テンプレートが存在しないとみなされるので注意が必要である。

resolveによるコントローラーへの注入

resolveを使ってコントローラーに値を注入する際は、ng-controllerディレクティブを使用せず、ルーティング定義の一部としてコントローラーを定義すること。そうしないと、AngularJSは依存している値が取得できない。

$routeParamsの値の型

$routeParamsから取得した値は、デフォルトでは全て文字列になる。

1つのアプリケーションにng-viewは1つだけ

ngRouteでは、ng-viewは1つだけ使用可能である。複数のng-viewや、ネストしたng-viewは使用できない。

追加の設定

HTML5モード

ハッシュの含まれるURLはSPAでは一般的なものであるが、ハッシュが含まれない「普通の」URLを使うこともできる。

AngularJSのHTML5モードは、ブラウザのpushState APIを使用する。http://example.com/#/user/1というURLは、HTML5モードではhttp://example.com/user/1となる。

HTML5モードを有効化するには、サーバ側のサポートも必要である。ユーザがhttp://example.com/user/1にアクセスしたとき、サーバのドキュメントルート配下の/user/1ではなく、index.htmlが返るようにする必要がある。

HTML5モードを有効化するには、以下の3つが必要である。

(1) AngularJSのconfigでHTML5モードを有効化する

HTML5モードを有効化するには、$locationProviderを使用する。hashPrefixに’!’を設定することが、SEO上の観点からは推奨される。

(2) <base />タグ

index.html<head>の中に、href属性を含んだ<base />タグを追加する必要がある。これによって、ブラウザに、画像やCSS等の静的リソースをどこから取得すべきかを教える。

たとえば、アプリケーションがhttp://example.com/appというURLで提供されていてる場合、HTML5モードが有効になっていると、URLはhttp://example.com/app/user/1といった形になる。このとき、サーバはindex.htmlを返すが、ブラウザのパスは/app/user/1なので、相対パスではファイルを取得できない。以下のように設定されていれば、この問題を解決できる。

これによって、URLに関わらず、相対パスが/appを起点として解決されるようになる。

(3) サーバサイド

/first/page/second/pageといったルートに対するアクセスの場合にも、index.htmlが返されるようにする必要がある。

以下はNodeJSの場合の実装例。

AngularJSとSEO

検索エンジンについて気に留めておくべき点は以下のとおり。

  • GoogleやBingのような検索エンジンは、SPAに対応したパターンを有している。これらは、hashbangを使用したURLの使用を前提にしている(#ではなく、#!を使用したURL)
  • 検索エンジンがクロールする際、#!?escaped_fragment=に置き換えて、サーバにリクエストを送る

さらに、サーバーが検索エンジンからのリクエストを判別して、以下のような手段を使用してレスポンスを返す必要がある。

  • HTMLのスナップショットを作成して返す: AngularJSのドキュメントはこの方法を使用している
  • HTMLをリアルタイムにレンダリングして返す: PhantomJSのようなヘッドレスブラウザをサーバで使用してHTMLをレンダリングする

angular-seoのようなツールの使用も検討すべきである。

AngularJSと分析サービス

Google Analyticsのような分析サービスは、SPAでは簡単には動かない。分析サービスは、ページの読み込み時のイベントにフックして処理を行っているからである。SPAでは、Google Analyticsを手動で呼び出してルートの変更を教える必要がある。これには、Angularyticsのようなライブラリが使用できる。

代替となるルーター

ngRouteで、70-80%のニーズは満たすことができる。SPAのURLは一般的にシンプルなものになるからである。

しかし、URLに複雑な要求があったり、URLに応じてUIを細かに制御したいような場合には、ngRouteの機能では対応が難しい。

このような場合には、ui-routerの使用を検討すべきである。

※2016年3月現在では、New Routerも代替候補の1つになる。

『AngularJS: Up and Running』読書メモ 第5章 サービス

AngularJS: Up and Running

AngularJSのサービス

AngularJSのサービスは、アプリケーション全体に渡って振る舞いや状態を保持できる関数、あるいはオブジェクトである。それぞれのAngularJSのサービスは一度だけインスタンス化されるので、アプリケーションは常に同じインスタンスにアクセスする。AngularJSのサービスで実装することができるものは、繰り返し行う処理、状態の共有、キャッシュ、ファクトリなどである。

なぜAngularJSのサービスが必要なのか

AngularJSのコントローラーは、以下のような仕事に優れている。

  • 取得するモデルを決定し、それをHTMLに表示する
  • ユーザーとのインタラクション
  • 表示ロジック

コントローラーはステートフルだが、短命である。シングルページアプリケーションにおいて、コントローラーは何度も破棄と再生成が繰り返される。

サービス VS コントローラー

以下は、コントローラーの役割の違いを示したものである。

|コントローラー|サービス|
|表示ロジック|ビジネスロジック|
|ビューと直接結びつく|ビューからは独立している|
|UIを動かす|アプリケーションを動かす|
|特定の画面専用|再利用可能|

コントローラーは、データの取得・表示、ユーザインタラクション、UIのスタイリングと表示に責任を持つ。

サービスは、サーバの呼び出し、共通のバリデーションロジック、アプリケーションレベルの保存処理や、再利用可能なビジネスロジックに責任を持つ。

AngularJSの「$」

AngularJSによって提供されるすべてのサービスには、「$」という接頭辞がついている。たとえば、$log、$http、$windowなどである。これによって、サービスの名前を見たときに、AngularJSの組み込みのものかがすぐにわかる。逆に言えば、独自のサービスを作る際には、それらに「$」という接頭辞をつけてはいけない。組み込みのサービスか否かの判別が難しくなるからである。

組み込みのAngularJSサービスを利用する

サービスを使うコントローラーの定義方法は以下のようになる。

最初の定義方法では、AngularJSは無名関数の仮引数の情報を使用して依存するオブジェクトを調べる。この方法の場合、JavaScriptのminifyによって$logの名前が書き換えられた際に動作しなくなる。

第2の定義方法では、controller() の第1引数にはコントローラー名の文字列を、第2引数には配列を渡している。配列の最初の引数は依存するサービス名の文字列で、第2引数の無名関数では引数に$logを指定している。この方法の場合、文字列の’$log’はminifyされても変わらないので、minifyを行っても正常に動作する。

冗長ではあるが、第2の定義方法が推奨される。

注入の順番

依存するオブジェクトを文字列で定義すると、文字列の記述の順番にオブジェクトが渡される。

文字列によって依存オブジェクトを指定している場合、AngularJSは無名関数の仮引数の情報を無視して、文字列の並んでいる順番に変数を渡す。したがって、上記コードでは、$logが$windoに、$windowが$logに渡される。

依存オブジェクトの指定文字列と、無名関数の仮引数の順番は一致している必要がある。

一般的なAngularJSのサービス

$window

グローバルなwindowオブジェクトのラッパー。グローバルな状態を避けるために用意され、特にテストにおいて重要。テストでは$windowサービスを簡単にモックできるので、windowではなく$windowを使うべき。

$location

ブラウザのURLの操作を行う。
(ただし、Single Page Applicationとしての範囲内でのURLの操作であり、location.hrefの書き換えのような別ページの遷移はできない)。

$http

HTTPリクエストを扱う。

独自のサービスを作成する

機能をサービスとして実装すべきか決めるには、以下のような基準に基いて決めるとよい。

  • 再利用性があるか
  • アプリケーションレベルの状態保持を行うものか
  • ビューから独立しているか
  • サードパーティのサービス(SocketIO等)と結びつくか
  • キャッシュやファクトリか

上記基準のいずれかに該当するようであれば、サービスとして実装する価値がある。

シンプルなAngularJSのサービスを作る

AngularJSは以下を保証する。

  • サービスは必要になるまで実体化されない
  • サービス定義関数は一度だけ実行され、インスタンスはそのまま保持される(サービスはシングルトンである)

Factory, Service, Providerの違い

関数型のプログラミングスタイルを実践しているか、関数とオブジェクトを返すことを好むなら、module.factory()を使うべきである。

クラスベースのオブジェクト指向のスタイルを好むなら、service()を使うべきである。

providerは、複雑な初期化処理が必要な場合に使用する。

config関数は、AngularJSのアプリケーションが起動する前に実行される。

『AngularJS: Up and Running』読書メモ 第4章 フォーム

AngularJS: Up and Running

ng-submit

フォームの送信には、submitボタンのng-clickを使う方法と、ng-submitを使う方法がある。ng-submitの利点は、submitボタンのクリック外に、テキストフィールドに入力後のreturnキーを押した場合にも発火すること。

FormController

formに名前を付けると、AngularJSは自動的にFormControllerを作成する。FormControllerにはフォームの名前でアクセスできる。

上のサンプルコードの場合、myForm.$invalid でmyFormのFormControllerの$invalidプロパティにアクセスしている。FormControllerには以下のプロパティがある。

$invalid: フォームのバリデーションが1つでも通らない場合にtrue
$valid: フォームの全てのバリデーションが通る場合にtrue
$pristine: ユーザの入力がまだ無い場合にtrue(pristineは「きれいな」という意味)
$dirty: ユーザ入力が行われた場合にtrue
$error: それぞれのフィールドにおけるエラーの詳細を保持する

エラーメッセージの表示

それぞれのフィールドに応じたエラーを取得するには、フィールドのname属性を設定する必要がある。以下では、name=”uname”のinputフィールドを作成している。

{フォーム名}.{フィールド名}.$error.{バリデーション名}で、各バリデーションを通過しているか確認できる。

状態に応じたフォームのスタイリング

AngularJSは、フォームの状態に応じて以下のCSSクラスを追加する。

  • $invalid: ng-invalid
  • $valid: ng-valid
  • $pristine: ng-pristine
  • $dirty: ng-dirty

この他にもinputフィールドの状態に応じてCSSクラスが自動的に追加される(例えば、requiredが設定されているフィールドにはng-valid-requiredまたはng-invalid-requiredが追加される)。

ngModelOptions

AngularJS 1.3以降では、以下のオプションによってng-modelの挙動を制御できる。

updateOn

ng-modelの更新が行われるイベント名を指定する。
defaultならinputフィールドに固有のイベント、blurとすればinputフィールドからフォーカスが外れた時に更新が行われる。
複数のイベントの組み合わせも可能。

debounce

ユーザーが入力をやめてからモデルの値を更新するまでの待ち時間をミリ秒単位で指定する。
イベント毎に指定する場合は{“default”: 500, “blur”: 0}のようなオブジェクトを設定する。

allowInvalid

デフォルトではfalseになっており、AngularJSは、バリデーションに失敗している間はモデルに値を設定しない。allowInvalidをtrueにすると、バリデーションが失敗していてもモデルに値を設定する。

getterSetter

trueを設定すると、ng-modelの式を変数ではなくgetter/setterとして扱えるようになる。

ng-formによるフォームのネスト

通常のHTMLのformはネストすることができないが、AngularJSのng-formディレクティブはネストすることができる。
以下がネストしたフォームの例。profileをグループ化している。

『AngularJS: Up and Running』読書メモ 第3章 AngularJSでのユニットテスト

AngularJS: Up and Running

コードが意図した通りに動いているか確認するのは書く人間の義務である。ユニットテストによって、コードがどのように振る舞うべきか定義することができる。また、ユニットテストを自動的に実行することで、振る舞いが意図せず変わっていないか確認することができる。

ユニットテストとは何か、何故必要なのか

ユニットテストとは、1つの関数や、コードの1部分を取り上げて、それが意図したとおりに動作しているか確認するテストを書く、というコンセプトである。

以下に、JavaScriptベースのクライアントサイドアプリケーションでユニットテストを書くべき理由を5つ挙げる。

(1) 正しさの証明

ユニットテストが無いと、手動テストを盲目的に信頼するしかない。ユニットテストは、意図したとおりに動作することの証明になる。

(2) コンパイラが無い

JavaScriptにはコンパイラが無いため、エラーは実行時にしかわからない。ブラウザで実行する前にユニットテストを走らせれば、エラーを検知できる。

(3) エラーを早く見つけられる

ユニットテストによって、実際にアプリケーションを実行するよりも早く以上に気付ける。

(4) エンバグを防ぐ

ユニットテストを書くことで、将来的に既存機能を壊してバグを作りこんでしまう可能性を低下させることができる。

(5) 仕様になる

コメントは実装と対応していない可能性があるが、ユニットテストは実装と対応しており、アプリケーションの仕様ともいえる。

初めてのTDD

テスト駆動開発(Test Driven Development, TDD)とは、アジャイルの方法論で、開発のスタイルを反転させる。TDDでは、コードを実装する前に、テストを書く。

TDDの信条は以下のとおり。

  • 失敗しているテストがコードを要求している場合にのみ、コードは書かれる
  • テストをパスするのに必要最小限のコードが書かれる
  • 重複は全てのステップで取り除かれる
  • 全てのテストが通ったら、次に必要な機能のための失敗するテストが追加される

これらのシンプルなルールによって、以下のことが保証される。

  • コードは有機的に成長し、コードの各行には目的がある
  • コードは高度にモジュール化され、凝集性があり、再利用可能である(テストを書くためにはこれらの要素が必要である)
  • 将来的なコードの毀損やバグを防ぐ包括的なテストが提供される
  • テストは仕様やドキュメンテーションとしても機能する

Karma入門

Karmaは高速なテストランナーである。NodeJSとSocketIOを利用して、複数のブラウザで高速にテストを実行している。

Karmaはテストを可能な限りシンプルで高速にすることを目指している。

以下がKarmaのインストール方法である。

  1. NodeJSをインストールする
  2. Karma CLIをインストールする(sudo npm install -g karma-cli)
  3. Karmaをテストを行うフォルダにインストールする(npm install karma)
  4. 使用するKarmaプラグインをインストールする(npm install karma-jasmine 等)

Karmaはプラグインシステムを採用しており、使用するテスティングフレームワークや各ブラウザ向けのテストランナーはプラグインでインストールする。以下のコマンドを実行すると、Jasmineと、Chromeのランチャー(テスト実行時にブラウザを自動で起動するスクリプト)がインストールされる。

Karma及びプラグインのインストールは、テスト対象のプロジェクト内で実行する必要がある点に注意が必要。

テストランナーとテスティングフレームワーク

テストランナーとテスティングフレームワークは混同されがちである。これは、同じライブラリが両方の仕事をすることが多いからと思われる。

JavaScript(そしてAngularJS)では、それぞれの目的に2つの分離されたツールを利用する。Karmaはテストランナーで、全てのユニットテストを探してきて、ブラウザを開き、テストを実行して、結果を取得することに責任を持つ。テストの書き方には関知しない。

Jasmineはテスティングフレームワークである。テストを書くための文法、API、アサーションの書き方を定義する。テスティングフレームワークにはJasmine以外の選択肢(mocha、qunit等)もある。

Karmaプラグイン

Karmaプラグインは、以下の種類に分けられる。

  • ブラウザランチャー:テスト実行時にブラウザを自動的に起動する
  • テスティングフレームワーク:テストを書く際に使用するフレームワーク(Jasmine, mocha, qunit等)
  • レポーター:Karmaの出力を制御する
  • インテグレーション:既存のJavaScriptライブラリ・ツールとの統合(Closure, RuquireJS等)

Karmaの設定の解説

Karmaを使用するには、設定ファイル(karma.conf.js)が必要である。以下は設定ファイルの例である。

以下、設定ファイルの解説。

  • basePath: テスト対象の全てのファイルと、テストが必要とするファイルの読み込みのための起点となるパス。Karmaの設定ファイルからの相対パス。
  • frameworks: 読み込むフレームワークのリスト。jasmine, mocha, qunit等。
  • files: 読み込むファイルのリスト。AngularJSの場合、angular.jsを最初に読み込んで、その後テストヘルパーを含んだangular-mock.jsを読み込む。その後、アプリケーションのソースコードを読み込み、最後にユニットテストのファイルを読み込む。
  • exclude: 除外するファイルのリスト
  • port: Karmaのサーバがどのポートを使うか。デフォルトは8080。
  • logLevel: Karmaがブラウザから取得するログのレベル(全出力・エラーのみ出力等)
  • autoWatch: trueにすると、files設定に含まれる全てのファイルを監視して、変更があったら際に自動的に全てのテストが実行される
  • browsers: Karma起動時に開くブラウザ。実際の起動には、それぞれのブラウザに対応したプラグインが必要。
  • singleRun: 1つのユニットテストの終了後に、サーバーを停止するか否か。CI(Continuous Integration, Jenkins等)環境ではtrueに、それ以外ではfalseにすべき。

Karma設定の詳細はhttp://karma-runner.github.io/0.13/config/configuration-file.htmlを参照。

Karma設定の生成

以下のコマンドを実行することで、設定ファイルを生成できる。

これによって、対話シェルが起動する。一連の質問に応えることで、ニーズに応じたkarma.conf.jsファイルが生成される。

Jasmine: テストスペックのスタイル

Jasmineは振る舞い駆動のテストの書式を提供する。

振る舞い駆動のテストでは、大量のアサーションを書く代わりに、振る舞いと期待する状態を記述する。

Jasmineの文法

  • describe: 一連のテスト(テストツイート)を作成する。これは複数のユニットテストのコンテナである。describeの対象はコントローラーやサービスである。また、describeのネストも可能である。
  • beforeEach: それぞれのテストの実行前に実行される処理を記述する
  • afterEach: それぞれのテストの実行後に実行される処理を記述する
  • it: ユニットテストの単位である。itブロックは、他のitブロックから独立しているべきである。状態のセットアップと、機能の実行、戻り値のチェックが含まれる。
  • expect: それぞれのexpectは値を取り、その後にマッチャーで期待する値を記述する。

Jasmineのマッチャー

全てのマッチャーは、expect(value)の後に続けて使用する。以下は代表的なマッチャーの例。

  • toEqual: 2つのオブジェクトの同一性を比較する
  • toBe: 参照をチェックする。2つの変数が同じオブジェクトを参照している場合はtrue。
  • toBeTruthy: JavaScriptでtrueとして評価される値か調べる
  • toBeFalsy: JavaScriptでfalseとして評価される値か調べる
  • toBeDefined: expectに渡されたリファレンスが定義済みか調べる
  • toBeUndefined: expectに渡されたリファレンスが未定義か調べる
  • toBeNull: expectに渡されたリファレンスがnullか調べる
  • toContain: expectに渡された配列がマッチャーに指定した要素を含むか調べる
  • toMatch: 正規表現によって渡された文字列にマッチするか調べる

※JavaScriptでfalseとして評価される値
– nullのオブジェクト
– 空の文字列
– 数値の0
– Booleanのfalse

コントローラーのユニットテストの例

以下のようなコントローラに対するコードを書く(書籍のサンプルコードを一部割愛)

テストは以下のようになる。

以下は、angular.mock.jsで定義されている、テスト用ヘルパーメソッド。

  • module(): 指定したモジュールを初期化する
  • inject(): AngularJSのサービスを関数に注入する

$controllerはテスト専用ではなく、AngularJSのサービスの1つ。

ユニットテストの実行

ユニットテストの実行には、以下のコマンドを実行する。

このコマンドは現在のディレクトリから自動的にkarma.conf.jsを見つけ出す。Karmaの設定ファイルが別の名前だったり、別の場所にある場合には、パスを指定する必要がある。

karmaコマンドはテスト用のサーバを立ち上げる。設定で記述したそれぞれのブラウザが自動的に起動する。自動的に起動しなかったブラウザも、手動で起動してページにアクセスすることでテストを実行できる。

テストの実行結果はコンソールに表示される。ブラウザには何も表示されない。

autoWatchがtrueになっている場合、karmaのサーバが起動した状態で、監視対象のファイルを書き換えて保存すると、テストが自動的に再実行される。

おわりに

AngularJSのユニットテストで注目すべき点の1つは、DI(Dependency Injection, 依存性の注入)が効果的に使われていることである。ユニットテストにおいては、テスト対象のモジュールが何に依存しているかを気にする必要がない。オブジェクトの初期化は全てAngularJSがやってくれる。

『AngularJS: Up and Running』読書メモ 第2章 コントローラーとディレクティブ

AngularJS: Up and Running

AngularJSのモジュール

最初に学ぶべき概念は「モジュール」である。
AngularJSでは、モジュールによって、関連するコードを1つにパッケージする。
AngularJSのモジュールには2つの部品がある。

  • モジュールは自身のコントローラー、サービス、ファクトリ、ディレクティブを定義することができる
    • これらにはモジュールを通してアクセスできる
  • モジュールは別のモジュールに依存することができる
    • モジュールの初期化処理において依存モジュールを指定する

モジュールは、関連するJavaScriptの容れ物であるのと同時に、AngularJSがアプリケーションを開始するのに使用するものでもある。
ng-appディレクティブで、アプリケーションのエントリポイントを指定できる。

angular.moduleメソッドの第1引数はモジュール名、第2引数は依存しているモジュール名の配列になる。
第2引数を渡さない場合、myAppという名前のモジュールを探して返す。

コントローラーの作成

AngularJSにおけるコントローラーの役割は以下のようになる。

  • サーバから現在のUIに合わせたデータを取得する
  • ユーザにどのデータを見せるか決める
  • 表示のロジック
  • ユーザーとのやりとり

UIに関係のないコントローラーは存在しない(UIに関係ないビジネスロジックはサービスに書くべき)。
コントローラーは以下のように定義する。

AngularJSのモジュールのcontrollerメソッドを使って、コントローラーを定義することができる。
controllerメソッドは第1引数にコントローラー名、第2引数に配列を取る。
第2引数の配列には、依存しているモジュール名の文字列を入れ、配列の最後の要素にコントローラーとなる関数を入れる。
(依存モジュールが無い場合は関数だけを入れる)

なお、第2引数には配列でなく関数を渡すこともできるが、JavaScriptコードを圧縮すると動作しなくなるため非推奨。

また、従来のサンプルコードでよく使われていた、グローバルに関数を定義してコントローラーとする方法は、1.3以降では動かない。

ng-controllerディレクティブを使うことで、コントローラーを指定できる。

ControllerAs記法はAngularJS1.2で導入された機能で、コントローラー自身のプロパティにビューからアクセスできるようにすることができる。
上記サンプルでは、ng-controllerのControllerAs記法でMainCtrlに別名をつけ、MainCtrlに定義されたプロパティにアクセスしている。

コントローラーがネストされた複雑なUIでは、ビューの変数がどこから来ているか判別するのが難しいことがある。
ControllerAs記法を使うと、コントローラーの名前によって変数の出処がはっきりする。

配列を扱う

ng-repeatディレクティブを使うことで、配列を表示することができる。

ng-repeatディレクティブを使って、MainCtrl.notesを表示するコードは以下。

ここで、ng-repeatの付いた要素と、その中身の要素は、繰り返しの際のテンプレートになる。
上記コードの場合、中にspanを含むdivが3つ生成される。

ここでは、ビューで{{}}の代わりにng-bindディレクティブを使っている。
{{}}を使うと、AngularJSアプリケーションの読み込みが完了するまで{{}}が表示されてしまうことがあるが、ng-bindではこの問題を回避できる。
この問題は、ng-cloakディレクティブを使うことでも回避可能である。

AngularJSのロードが完了するのを待つ

angular.js(又はangular.min.js)を読み込むと、以下のCSSが自動的に定義される。

[ng-cloak]は、ng-cloakという属性を持つ要素に対するセレクタなので、このCSSルールでは、ng-cloak属性のついた要素を非表示にする。
ロードが完了すると、AngularJSはng-cloak属性を要素から取り除くため、要素が画面上に表示される。
bodyタグにng-cloakをつけることもできるが、細かなパーツごとにつけていった方がユーザビリティが良い。

angular.jsのソースコードの読み込みが完了するまで、上記CSSは定義されていないことに注意が必要。
※一般に、CSSは先に、JavaScriptは後に読み込むほうがページ表示のパフォーマンスが良くなる。
上記CSSルールを自前のCSSファイルの中で定義しておくことで、より確実に非表示にすることができる。

AngularJSの処理の流れ

  1. HTMLの読み込みが完了し、JavaScriptの読み込みが始まる
  2. ドキュメント全体の読み込みが完了し、AngularJSの動作が始まる
  3. ng-appディレクティブを見つけると、関連するモジュールを探して読み込む
  4. ng-app配下のDOMを探索し、ディレクティブやバインド文を探す
  5. ng-controller又はng-repeatディレクティブを見つけたら、その都度scopeを作成する
  6. HTMLからアクセスされる変数にウォッチャーとリスナーを登録する(2way-binding)
  7. AngularJSはモデルのデータが変わった場合にのみ、UIの更新を行う

ワンタイムバインディングによるngBindの最適化

AngularJS1.3以上では、ワンタイムバインディングを利用することでngBindのパフォーマンスを最適化できる。

以下のようなデータがあると想定する。

通常のngBindと、ワンタイムバインドを使った構文の例は以下。

ワンタイムバインディングの方では、変数の前に「::」をつけている。
この記号をつけると、変数は初回表示の時にだけ使用され、その後更新はされなくなる。
ページ内で双方向バインディングされている変数の数が多くなるほど、更新処理のオーバーヘッドは大きくなる。
ワンタイムバインディングを使うことで、更新処理のオーバーヘッドを最小化することができる。

その他のディレクティブ

ng-show/ng-hide

ng-show/ng-hideディレクティブを使うと、要素の表示非表示を制御することができる。

ng-show/hideの条件には式を渡すこともできる。

ng-show/hideで真として評価される値は、Booleanのtrue、空白でない文字列、0でない数値、nullでないJavaScriptのオブジェクトである。

ng-class

ng-classディレクティブはCSSクラスを付け外しするのに使用される。
ng-classディレクティブには、適用するクラスを文字列として渡す使い方と、オブジェクトを渡す使い方がある。

オブジェクトを渡す方法の場合、渡すオブジェクトの値がtrueである要素のキーがクラスとして適用される。
実際のコードでは以下のような実装になる。

以下はビュー。itemという変数にstatusというプロパティがあるとする。

上記コードの場合、item.statusがtrueであれば.doneが、falseであれば.pendingが適用される。

ng-repeatを扱う

ng-repeatは繰り返し行うディレクティブである。

オブジェクトをng-repeatする

以下のようなオブジェクトがあるとする。

このオブジェクトをng-repeatで回す場合は、ビューを以下のように書く。

ng-repeatはオブジェクトをループする場合、アルファベット順で、大文字の方が先になる。そのため、上の例ではMisko, brad, shyamの順に表示される。

ng-repeatは、value in Array又は(key, value) in Objectという形をとる。配列の場合には、ng-repeatは配列の先頭から順に表示する。

ng-repeatのヘルパー変数

ng-repeatには、HTMLテンプレートで利用できるヘルパー変数が存在する。

Track by ID

デフォルトでは、ng-repeatは繰り返しの度に新しいDOM要素を作る。
しかし、パフォーマンス最適化のため、作成されるオブジェクトが同じであればDOMのキャッシュ又は再利用を行う。
(同一性の検証にはオブジェクトのハッシュ値を用いている)

オブジェクトの同一性に関係なく、同じDOM要素を再利用して欲しいこともある。
この場合には、AngularJSにトラッキングのための情報を渡すことができる。

track by にトラッキングのための変数を設定すると、この変数の値が同じであればDOM要素が再利用される。

$$ で始まる変数を使ってはいけない

$$ で始まる変数を、アプリケーションのコードで使ってはいけない。
AngularJSは $$ で始まる変数をAngularJS内のプライベート変数を示すものとして使用しており、これらの変数が将来に渡っても使える保証はない。

複数のHTML要素にまたがったng-repeat

テーブルを2行ごと(

2つごと)に繰り返す、といった操作が必要になることがある。
ng-repeat-startとng-repeat-endディレクティブを使うことで、AngularJSに、ng-repeatの開始点と終了点を知らせることができる。