この記事では、Web アプリケーション開発のための JavaScript フレームワークであるAngular
について説明します。
Angular とは
Angular(アンギュラー)は Google 社によって開発されているクライアントサイド向けJavaScriptフレームワーク
です。
フルスタック(全部乗せ)なフレームワークを指向しており、Web アプリケーション開発に必要な機能のほぼ全てをサポートしています。
Angular CLI
(Command Line Interface)を使って、プロジェクトの生成からビルド、デプロイまで実行することができます。
現在(2019 年 7 月)の最新バージョンは v8.0.0(メジャーバージョン)です。Google は半年ごとのバージョンアップを宣言しており、2019 年 10 月に v9.x、2020 年 5 月に v10.x のアップデートが予定されています。
Angular の開発初期(v1.x)では「AngularJS」というプロダクト名で開発されていましたが、v2.x にアップデートされたときにアーキテクチャの抜本的な見直しが図られ、名前も「Angular」に改められました。
AngularJS では JavaScript をベースとしたフレームワークでしたが、Angular では TypeScript ベースに改められ、文法も刷新されたため、AngularJS と Angular には互換性がありません。
※ネット上には AngularJS 時代の情報がたくさん転がっています。惑わされないように。。。
Angular は Node.js 環境下で開発を行います。これは Angular の開発サポート機能自体が TypeScript(をコンパイルした JavaScript)で記述されているためです。
また、Angular 自体は npm を使用してインストールすることができます。
Node.js は v8.x 以上、npm は v5.x 以上が必要です。バージョンが古いとエラーになるため注意が必要です。
インストール
Angular のインストールはすなわち Angular CLI のインストールです。
Angular CLI を利用することで、Angular プロジェクトを生成し、フレームワークの様々な恩恵を享受することができるようになります。
npm install @angular/cli -save
プロジェクトの生成
Angular プロジェクトを生成するには、Angular CLI から以下のコマンドを実行します。
コマンドを実行したカレントディレクトリに新しいサブディレクトリが作られ、その中にアプリケーションを構成するソースが格納されます。
このとき、Angular が依存する外部ライブラリも自動でインストールされます。
ng new <プロジェクト名>
※「ng」は Angular CLI を呼び出すコマンドですが、Angular を示すワードとして色々なところで頻出します。
生成されたプロジェクトフォルダには、Angular プロジェクトであることを示す angular.json に加え、TypeScript プロジェクトであることを示す tsconfig.json などが含まれます。
Angular 自身のモジュールや外部ライブラリなどは node_modules に格納されます。
実際にアプリケーションのソースとなるのは src フォルダです。
/
├ node_modules/
└ src/
├ app/
│ ├ app.component.html
│ ├ app.component.css
│ ├ app.component.ts
│ ├ app.component.spec.ts
│ └ app.module.ts
├ assets/
├ environments/
├ dist/
├ index.html
├ main.ts
├ styles.css
└ etc...
要素 | 機能 |
---|---|
node_modules/ | npm パッケージを格納するフォルダ。 |
src/ | プロジェクトの資源を格納するフォルダ。 |
src/index.html | メイン HTML ページ。 |
src/main.ts | アプリケーションのルートスクリプト。 |
src/styles.css | メインスタイルシート。 |
src/app/ | アプリケーションのソースを格納するフォルダ。 |
src/app/app.component.html | index.html から読み込まれるルートコンポーネント。 |
src/app/app.component.css | ルートコンポーネントのスタイルシート。 |
src/app/app.component.ts | ルートコンポーネントのスクリプト。 |
src/app/app.component.spec.ts | ルートコンポーネントのユニットテストスクリプト。 |
src/app/app.module.ts | root モジュールのスクリプト。 |
assets/ | 画像ファイル、外部ファイル等、コンパイルせずに使う資源を格納するフォルダ。 |
environments/ | 本番用、開発用等のビルド設定を格納するフォルダ。 |
dist/ | コンパイル済みプロジェクトを格納するフォルダ。 |
アプリケーションの実行
開発中の動作確認も Angular CLI からコマンドで簡単に行うことができます。
Angular プロジェクトのルートフォルダで実行する必要があります。
ng serve --open
--open
オプションを付けることで、ビルド完了後に自動でブラウザを開いてくれます。
ng serve の内部では webpack の開発用仮想サーバ機能(webpack-dev-server)を利用しており、ホットデプロイ(ソースの変更を検知して自動で再読み込みしてくれる機能)をサポートしているため、ソースの修正結果がそのままブラウザへ反映されます。
アプリケーションのビルド
Angular ソースを実際にアプリケーションとして運用する際には、コンパイルを行う必要があります。
以下のコマンドを実行すると、内部的に webpack の処理が走り、コンパイルおよびバンドルされたソースが生成されます。
Angular プロジェクトのルートフォルダで実行する必要があります。
ng build --configuration=production
--configuration
オプションでは、ビルドを行う環境設定を指定することができます。
一般的には、production、staging、development などでそれぞれ環境設定を定義しておきます。
production ビルドに限り、--prod
のショートハンドで指定することができます。
ビルド環境やソースの出力先などの設定については、angular.json 内で定義することができます。
コンポーネント
Angular はコンポーネント指向
のフレームワークです。
コンポーネントとは、特定の画面であったり、共通の部品であったり、何かしら「ある機能を持った一定のひとかたまり」のことを言います。
別々のコンポーネントを適宜組み合わせて画面を構成するのが Angular の Web アプリケーションです。
(例えば、ヘッダコンポーネントとナビゲーションコンポーネントとメインコンテンツコンポーネントを組み合わせて画面を構成するなど)
コンポーネントを作成するには、Angular CLI で以下のコマンドを実行します。
生成されるコンポーネントのソースは、同名のフォルダにまとめられた状態で作られます。
ng generate component hoge
コマンドを実行すると、ソースコードが生成されます。
CREATE src/app/hoge/hoge.component.html (19 bytes)
CREATE src/app/hoge/hoge.component.spec.ts (614 bytes)
CREATE src/app/hoge/hoge.component.ts (262 bytes)
CREATE src/app/hoge/hoge.component.css (0 bytes)
UPDATE src/app/app.module.ts (467 bytes)
「ng generate」はソースファイルを作るだけでなく、新しく作り出したコンポーネントをモジュールに登録してくれます。
Angular がコンポーネントを取り扱うためには、どこかのモジュールでコンポーネントを宣言しておく必要があります。
Angular におけるモジュールについては後の章で説明します。
└ app/
└ hoge/
└ hoge.component.html
└ hoge.component.css
└ hoge.component.ts
└ hoge.component.spec.ts
Angular は、Model-View-ViewModel からなるMVVMパターン
を踏襲したフレームワークです。
- Model アプリケーションのコアとなるドメインロジック(ユーザからは見えない)
- View ブラウザ上でユーザと対話するロジック(ユーザから見える)
- ViewModel Model と View の橋渡しを行うロジック
各コンポーネント単位は View-ViewModel レイヤーを担います。
生成されたファイルのうち、.html ファイルおよび.css ファイルが View、.ts ファイルが ViewModel にあたります。
コンポーネントの利用
生成したコンポーネントをアプリケーションに組み込むには、いくつか方法があります。
基本的なものは、以下の 3 種類です。
- HTML(ビュー)の中でコンポーネントセレクタを記述する
- ルーティングで遷移する
- アプリケーションのスクリプトで動的にコンポーネントを生成する
コンポーネントセレクタを記述する場合
Angular コンポーネントの TypeScript クラスファイル(.ts ファイル)には、@Component
デコレータを記述し、その中にコンポーネントのメタ情報(コンポーネント自身の情報)を記述します。
@Component({
selector: 'app-hoge',
templateUrl: './hoge.component.html',
styleUrls: ['./hoge.component.css']
})
要素 | 備考 |
---|---|
selector | コンポーネントセレクタ(Angular の View 上でそのコンポーネントを示す名前) |
templateUrl | コンポーネントの View 構造を記述したファイル(HTML) |
styleUrls | コンポーネントの View スタイルを記述したファイル(CSS) |
templateUrl、styleUrls は「template」「style」として HTML や CSS を.ts ファイル上に直接記述することもできます。
しかし、責務の分割の観点から、なるべく避けた方がよいでしょう。
上記の例のコンポーネントは、Angular の View において app-hoge タグを記述して利用することができます。
当然ながら、app-hoge タグは HTML 標準ではなく、本来ブラウザが解釈できるタグではありません。
あくまで Angular が独自に HTML を拡張したタグであり、実際のアプリケーションでは Angular によってブラウザが解釈できる形に変換されます。
<app-hoge></app-hoge>
ルーティングで遷移する場合
通常、Web ページ上のハイパーリンクをクリックなどすることで、別の Web ページに遷移できます。
同時に、ブラウザのヒストリにページの履歴が記録され、「戻る」ボタン、「進む」ボタンで移動することができます。
(Angular に限らずですが)SPA においては、「別の Web ページに遷移」という概念をほとんど持ちません。
リクエストされた URL に対応するコンポーネントを、現在表示しているコンポーネントと入れ替える
ことで別の Web ページに遷移したように見せています。
同時に、JavaScript でブラウザのヒストリを書き込んでいくことで、通常の画面遷移と同じように「戻る」ボタン、「進む」ボタンを使うことができます。
これをルーティング
といい、ルーティングを行うためのモジュールをルータ
といいます。
Angular Router はフレームワークの中でも重要な機能であるため、後の章で説明します。
動的にコンポーネントを生成する場合
スクリプトを記述することで動的にコンポーネントを生成することもできます。
(たとえば、ボタンを押したときに開くダイアログなどのユースケースが考えられます。)
コンポーネントの生成には、以下の 3 つのクラスを利用します。
-
ComponentFactoryResolver
コンポーネントクラスから ComponentFactory を特定するクラス -
ComponentFactory
コンポーネントを生成するクラス -
ViewContainerRef
View への参照を提供するクラス
コンポーネントクラスをブラウザで見える形に変換するのは ComponentFacroty の役目です。
しかしながら、Angular アプリケーションには無数のコンポーネントが含まれており、対応する ComponentFactory インスタンスも無数に存在しています。
その数多くの ComponentFactory インスタンスの中から、ほしいコンポーネントに対応した Factory を見つけ出すのが ComponentFactoryResolver の役目です。
そして、ComponentFactory によって生成されるコンポーネントは、View にセットされることでブラウザ上に現れます。
View へアクセスするためのメソッドを提供するのが ViewContainerRef です。
実際のコンポーネントを生成する際の流れは以下のようになります。
- ComponentFactoryResolver の
resolveComponentFactory
メソッドにコンポーネントクラスを渡して、対応する ComponentFactory を得る - ViewContainerRef の
createComponent
メソッドに ComponentFactory を渡して、View にコンポーネントを生成する
Factory 自体に触ることはありません。
ViewContainerRef 自体にコンポーネントを生成するメソッドがあり、そのまま Factory を渡してしまえば ViewContainerRef がすべてよしなにやってくれるのです。
ただし、動的なコンポーネントの生成はスクリプトロジックがやや煩雑になりがちです。
特に制約がなければ、Angular Material の MatDialogModule を利用するのがよいでしょう。
サービス
Angular は、特定のロジックをサービス
としてコンポーネントと切り分けることができます。
サービスとして別クラスに取り出した機能は、複数のコンポーネントから呼び出して共有することができます。
これにより、コンポーネントの責務を最小限にすることができ、保守性が向上します。
また、ビジネスロジックが特定のコンポーネントに結びつくことを防ぎ、再利用性を高めることができます。
Angular は依存性注入
(DI:Dependency Injection)をサポートする DI コンテナ
でもあります。
フレームワークにインジェクタ
という機構を持っており、サービスの利用者(コンポーネントクラスまたはサービスクラス)の要求に応じて、外部からサービスを注入(インジェクション)できる仕組みとなっています。
サービスの利用者は、使いたいサービスをコンストラクタで宣言しておけば、クラスの中でサービスをインスタンス化せずにサービスの機能を利用することができます。
サービスを作成するには、Angular CLI で以下のコマンドを実行します。
ng generate service hoge
CREATE src/app/hoge.service.spec.ts (323 bytes)
CREATE src/app/hoge.service.ts (133 bytes)
└ app/
└ hoge.service.ts
サービスの利用
サービスを利用するには、まずサービスを注入するインジェクタにサービスの作成方法を教える
必要があります。
インジェクタに教えるサービスの作成方法は、providers
メタ情報としてサービスごとに定義します。
下記の例では、import した HogeService を@Component デコレータの providers で指定することで、インジェクタが HogeService をインスタンス化して注入できるようになります。
src/app/hoge/hoge.component.ts
import { Component } from '@angular/core';
import { HogeService } from '../hoge.service';
@Component({
selector: 'app-hoge',
templateUrl: './hoge.component.html',
styleUrls: ['./hoge.component.css'],
providers: [HogeService]
})
export class HogeComponent {
constructor(private hoge: HogeService) {
this.hoge.out();
}
}
src/app/hoge.service.ts
import { Injectable } from '@angular/core';
@Injectable()
export class HogeService {
public out(): void {
console.log('this is HogeService.');
}
}
インジェクタはアプリケーションの階層ごとに存在します。
つまり、providers メタ情報をどこで定義するかによってサービスを利用できるスコープが変化します。
上記の例の場合はコンポーネントレベルで定義しているので、HogeService の作成方法を知っているインジェクタはこのコンポーネントのインジェクタのみとなり、他のコンポーネントは HogeService を利用することができません。
サービスを利用するレベルのインジェクタがサービスを注入できない場合、先祖のレベルのインジェクタへ遡って注入できるサービスを探します。
providers をアプリケーションのルートレベルで定義しておけば、そのアプリケーションのすべてのコンポーネントがサービスを利用できます。
特別な理由がない限り、ルートレベルで定義しておくのがよいでしょう。
providers をルートレベルにするには、以下のいずれかで可能です。
- root モジュール(app.module.ts)の providers にサービスを記述する
- サービスの@Injectable デコレータの providedIn メタ情報に「root」と記述する
providers で指定できるサービス作成方法にはいくつかの種類があります。
-
useClass
指定したクラスをインスタンス化(new)して注入する -
useValue
指定したインスタンスやオブジェクトを注入する -
useFactory
指定した関数の戻り値を注入する
上記の例で示したproviders: [HogeService]
は、以下の構文のシュガーシンタックスです。
providers: [{ provide: HogeService, useClass: HogeService }];
provide
プロパティはサービスインスタンスを識別する DI トークン(名前)です。
これにより、HogeService を指定するだけで、HogeService のインスタンスを特定して注入することができるようになります。
この通り、provider の正体は名前
とサービス作成方法
をセットで保持するオブジェクトです。
インジェクタはこれらの provider オブジェクトを頼りにサービスを作成し、作成されたサービス(インスタンスまたはオブジェクト)はそのインジェクタ内で共有されます。
バインディング
Angular では、View(.html ファイル)と ViewModel(.ts ファイル)が連携してコンポーネントを構成しています。
View と ViewModel の間で情報をやりとりする仕組みを、バインディング
といいます。
バインディングには以下の 3 種類があります。
-
イベントバインディング
View から ViewModel へ情報を渡す -
プロパティバインディング
ViewModel から View へ情報を渡す -
双方向バインディング
View と ViewModel で同期する
イベントバインディング
イベントバインディングは、View で発生した何らかのイベントを ViewModel に伝えることができます。
HTML の onClick、onLoad などをイメージすると理解しやすいでしょう。
HTML ボタンの click イベントにバインドするには、以下のように記述します。
<button (click)="hogeFnc()">Click Me!</button>
イベントバインディングによって、Web ページの操作からコンポーネントの関数を実行することができます。
関数に引数を渡す際は、テンプレート参照変数
を使うことができます。
View の要素に対して#
のプレフィクスとともに任意の名前をつけることで、要素の値にアクセスできるようになります。
<button (click)="hogeFnc(hogeForm.value)">Click Me!</button>
<input type="text" #hogeForm />
public hogeFnc(txt: string): void {
console.log(`hogeForm has value : ${txt}`);
}
プロパティバインディング
プロパティバインディングでは、ViewModel が持つ情報を View に埋め込むことができます。
HTML テキストフォームの value 属性にバインドするには、以下のように記述します。
<input type="text" [value]="hoge" />
上記の例では、ViewModel がもつ hoge 変数をテキストフォームの value にセットしています。
イベントバインディングとは括弧の形が異なることに注意してください。
- イベントバインディング :View から ViewModel へ ⇒
()丸括弧
- プロパティバインディング :ViewModel から View へ ⇒
[]角括弧
上記の例では、テキストフォームの値を変更しても ViewModel の hoge 変数の値は変化しません。
テキストフォームの値はあくまでも「hoge 変数の値をセットされただけ」であり、テキストフォームの変更を hoge 変数にフィードバックしないためです。
変更を hoge 変数へフィードバックするには、双方向バインディング
を使用します。(後述)
なお、どこかの HTML 要素にバインディングせずに、単純に変数の中身を出力したい場合は、以下のように記述できます。
{{ hoge }}
このように、{{ }}
で書く構文をInterpolation
(補間)といいます。
{{ }}
の中には、ただ変数を出力するだけではなく、式や関数を記述することもできます。
また、プロパティバインディングを利用して View のスタイルを変更することができます。
style 属性にバインドするスタイルバインディング
と class 属性にバインドするクラスバインディング
です。
スタイルバインディングでは、[style.CSS プロパティ名]
を記述し、対応する値をセットします。
クラスバインディングでは、[class.CSS クラス名]
を記述し、クラスを有効にするか無効にするかを boolean でセットします。
<p [style.color]="hogeStyle">hoge works!</p>
<p [class.hogeblue]="hogeClass">hoge works!</p>
public hogeStyle: string = 'red';
public hogeClass: boolean = true;
.hogeblue {
color: blue;
}
スタイルバインディングの場合、ViewModel にスタイル情報を持つ必要があります。
これは責務の分離の観点から好ましい状態ではない(本来、スタイルは CSS に集約するべき)ため、特別な理由がない限りクラスバインディングを利用するのがよいでしょう。
双方向バインディング
イベントバインディングとプロパティバインディングを組み合わせることで、View と ViewModel の同期を取ることができます。
以下は最も簡単な双方向バインディングの例です。
<input type="text" #hogeForm [value]="hoge" (input)="hoge = hogeForm.value" />
テキストフォームに hoge 変数の値がセットされ、テキストフォームの値が変更されればそのまま hoge 変数の値となります。
この方法でも双方向バインディングは実現できているのですが、あまりにも冗長です。
そのため、一般的にはNgModel
ディレクティブを使用します。
Angular には、フォームの操作を簡単に行うためのFormsModule
モジュールが用意されており、NgModel ディレクティブは FormsModule モジュールの一機能です。
NgModel ディレクティブを記述することで、フォームの要素をFormControl
インスタンスとして扱うことができます。
NgModel ディレクティブを使用するには、FormsModule モジュールをインポートし、View に NgModel ディレクティブを記述します。
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
~ 省略 ~
],
imports: [
FormsModule,
~ 省略 ~
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
<input type="text" [(ngModel)]="hoge" />
HTML の記述を[(ngModel)]
とし hoge 変数をセットすることで、このフォームの FormControl インスタンスは hoge 変数と結びつき、変更を即座に同期します。
イベントバインディングの記法は()丸括弧、プロパティバインディングの記法は[]角括弧でしたが、これはそれらを組み合わせた形[()]
となっています。(バナナボックス構文といいます)
ディレクティブ
Angular が解釈できる独自のキーワードを HTML に記述することで、View の出力結果を変化させることができます。
この仕組みをディレクティブ
といいます。
ディレクティブには大きく分けて 2 種類あります。
-
構造ディレクティブ
View の構造(レイアウト)そのものを変化させる -
属性ディレクティブ
View の属性を操作して見た目や振る舞いを変化させる
構造ディレクティブ
よく利用するのは、条件に応じて DOM 要素を追加・削除するNgIf
と、ループ処理で DOM 要素を生成していくNgFor
です。
<div *ngIf="hoge === fuga">あいうえお</div>
<div *ngFor="let item of ['AAAA','BBBB','CCCC']">{{ item }}</div>
NgIf の場合、指定した条件式(または boolean の変数)が true なら DOM 要素を生成し、false なら DOM 要素を生成しません。
一般的に、特定の要素を消す方法としては、CSS のdisplay: none;
などがありますが、CSS ではDOM要素を生成したうえで非表示
にします。
NgIf は DOM 要素の生成自体を行わないため、パフォーマンス面でメリットがあります。
NgFor の場合、指定した Iterable な値をループして DOM 要素を生成します。
基本的には for-of 構文で記述し、ループで取り出した値を仮変数に入れて利用します。
NgFor は、規定回数ループさせる(「let i=0; i < 10; i++」のような)処理はできないため、必ず Iterable な値を渡す必要があります。(ForEach と考えるとよいです)
属性ディレクティブ
よく利用するのは、CSS クラスを一括で適用するNgClass
と、CSS スタイルを一括で適用するNgStyle
です。
<div [ngClass]="hogeClass">あいうえお</div>
.classA {
color: red;
}
.classB {
font-weight: bold;
}
.classC {
background-color: yellow;
}
import { Component } from '@angular/core';
@Component({
selector: 'app-hoge',
templateUrl: './hoge.component.html',
styleUrls: ['./hoge.component.css']
})
export class HogeComponent {
public hogeClass = {
classA: true,
classB: true,
classC: true
};
}
NgClass では、CSS クラス名と一致する Key-Value を含んだオブジェクトを記述します。
Value が true ならクラスが適用され、false なら適用されません。
<div [ngStyle]="hogeStyle">あいうえお</div>
import { Component } from '@angular/core';
@Component({
selector: 'app-hoge',
templateUrl: './hoge.component.html',
styleUrls: ['./hoge.component.css']
})
export class HogeComponent {
public hogeStyle = {
color: 'blue',
'text-decoration': 'underline',
'font-size': '20pt'
};
}
NgStyle では、CSS プロパティ名と一致する Key-Value を含んだオブジェクトを記述します。
Value に指定した値に応じてスタイルが適用されます。
NgStyle の場合、ViewModel にスタイル情報を持つ必要があります。
これは責務の分離の観点から好ましい状態ではない(本来、スタイルは CSS に集約するべき)ため、特別な理由がない限り NgClass を利用するのがよいでしょう。
カスタムディレクティブ
Angular に標準で実装されている組込ディレクティブのほか、自分でディレクティブを定義することもできます。
コンポーネントやサービスを作るときと同様、Angular CLI から generate できます。
ng generate directive directive/mydirective
CREATE src/app/directive/mydirective.directive.spec.ts (244 bytes)
CREATE src/app/directive/mydirective.directive.ts (151 bytes)
UPDATE src/app/app.module.ts (689 bytes)
<div appMydirective>あいうえお</div>
import { Directive, ElementRef, OnInit } from '@angular/core';
@Directive({
selector: [appMydirective]
})
export class MydirectiveDirective implements OnInit {
constructor(private elRef: ElementRef) {}
ngOnInit() {
const el = this.elRef.nativeElement;
el.style.backgroundColor = 'green';
el.style.color = 'yellow';
}
}
上記の例では、ディレクティブセレクタをappMydirective
としています。
要素中にappMydirective
と記述することでディレクティブの効力が発揮され、要素のスタイルが変化します。
ディレクティブが適用された要素を参照するために、ElementRef クラスを使用しています。
パイプ
Angular では、View において表示内容を整形するための単純な関数(入力 → 整形 → 出力)の仕組みを持っており、これをパイプ
といいます。
一般的なコマンドラインインターフェースにおけるパイプライン演算子と同じように、|
でつなげて関数を呼び出すことで、データの加工をすることができます。
以下は代表的な組込パイプの例です。
<p>{{ 'This is Pipe' | uppercase }}</p>
<p>{{ 'This is Pipe' | lowercase }}</p>
<!-- 0文字目から4文字抽出 -->
<p>{{ 'This is Pipe' | slice:0:4 }}</p>
<p>{{ 10000 | currency }}</p>
<p>{{ 10000 | currency:'JPY' }}</p>
<p>{{ 10000 | number }}</p>
<!-- 整数桁10桁以上、小数点以下2桁 -->
<p>{{ 10000 | number:'10.2' }}</p>
<p>{{ 0.5 | percent }}</p>
<p>{{ dateObj | date }}</p>
<p>{{ dateObj | date:'yyyy年 MM月 dd日' }}</p>
※dateObj:日付型オブジェクト
カスタムパイプ
ディレクティブと同様に、パイプも自作することが可能です。
コンポーネントやサービスを作るときと同様、Angular CLI から generate が可能です。
ng generate pipe pipe/mypipe
CREATE src/app/pipe/mypipe.pipe.spec.ts (187 bytes)
CREATE src/app/pipe/mypipe.pipe.ts (205 bytes)
UPDATE src/app/app.module.ts (828 bytes)
{{ '吾輩は猫である。名前はまだ無い。' | mypipe }}
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'mypipe'
})
export class MypipePipe implements PipeTransform {
transform(value: string): string {
if (typeof value === 'string') {
return value.replace('猫', '犬');
} else {
return value;
}
}
}
上記の例では、パイプセレクタをmypipe
としています。
mypipe
にデータをパイプすることで、渡した文字列中のキーワードが置換されます。
パイプとしてデータの変換を行う正体はtransform
メソッドです。
Angular は、PipeTransform インタフェースとして実装された transform メソッドを呼び出すことでデータの変換を行います。
なお、パイプ処理は View の中で事あるごとに実行されるため、重い処理を行うのは避けた方がよいでしょう。
また、パイプの中の処理では、意図した型の引数が渡ってきているか必ずチェックしておくことが望ましいです。
ルータ
Angular ではコンポーネントを組み合わせて画面を構成しますが、何らかのアクション(ユーザのボタンタップ、function 起動等)に合わせて表示するコンポーネントを入れ替えることで、画面遷移を表現できます。
この機能をルーティング
と呼び、ルーティングはRouterModule
モジュールによって提供されます。
ルーティングを行うには以下のように準備を行う必要があります。
- base 要素を記述する
- AppRoutingModule モジュールを作成する
- ルートを設定する
- ルータアウトレットを記述する
- 遷移を行うトリガを記述する
base 要素を記述する
まず、アプリケーションの基点となるページがどこにあるかを示すために、<base>
タグを index.html の head タグに記述します。
href 属性を「/」とすることで、このページがアプリケーションのルートであることをルータへ示します。
なお、Angular CLI でアプリケーションの雛型を生成(ng new)した場合は、自動で index.html に書き込まれます。
<base href="/" />
AppRoutingModule モジュールを作成する
ルーティング機能を利用するために、Angular の RouterModule をインポートする必要があります。
root モジュールの中でそのままインポートすることもできますが、一般的には、ルーティング専用のモジュールを用意して root モジュールにインポートします。
RouterModule には初期設定作業を行う必要があるため、モジュールを分割することで root モジュールのコードが煩雑になるのを防ぎます。
モジュール名は任意の名前を付けられますが、慣例的に「AppRoutingModule」とするのが推奨されています。
ng generate module app-routing --flat --module=app
-
--flat
固有フォルダを作らず、src/app 直下に作成する -
--module
指定したモジュールのインポートに自動的に追加する
CREATE src/app/app-routing.module.ts (194 bytes)
UPDATE src/app/app.module.ts (1119 bytes)
ルーティングモジュールはアプリケーション全体で利用するモジュールであるため、AppModule(root モジュール)と同じフォルダに置くことが多いです。
また、上記の例では generate したモジュールをそのまま AppModule にインポートさせています。
module オプションを使用しない場合は、手作業でインポートする必要があります。
ルートを設定する
Angular に組み込まれている RouterModule モジュールは、そのままでは使えません。
ルーティングを行うための地図となるルート
(Route)を教え込む必要があります。
generate したばかりの AppRoutingModule モジュールは何も書かれていない空の状態です。
RouterModule モジュールをインポートし、ルーティングモジュールとしての機能を果たすよう設定をしていきます。
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; // RouterModuleとRoutes型をインポート
import { HogeComponent } from './hoge/hoge.component'; // サンプル用コンポーネント #1
import { FugaComponent } from './fuga/fuga.component'; // サンプル用コンポーネント #2
const routes: Routes = [
// https://example.jp/hoge にアクセスされたらHogeComponentを表示する
{ path: 'hoge', component: HogeComponent },
// https://example.jp/fuga にアクセスされたらFugaComponentを表示する
{ path: 'fuga', component: FugaComponent },
// 上記以外のURLにアクセスされたら https://example.jp/ に遷移する
{ path: '**', redirectTo: '/' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)], // ルートを読み込む
exports: [RouterModule]
})
export class AppRoutingModule {}
まず、RouterModule
モジュールとRoutes
型をインポートします。
Routes 型は Route
型(ルートの 1 エントリ)の配列です。
Route 型に指定できるプロパティには、一例として以下のようなものがあります。
プロパティ | 内容 |
---|---|
path | URL と比較する文字列。** を指定するとワイルドカードになる。 |
pathMatch | URL の比較ルール。 ・prefix(前方一致):デフォルト ・full(完全一致) |
component | path が一致した場合に表示するコンポーネント |
redirectTo | path が一致した場合にリダイレクトする URL |
canActivate | アクティブ化可能か判定するガード(後述)の配列 |
loadChildren | 遅延ロードを行うためのコールバック |
ルートは配列に指定された順番の通りに優先順位が決まります。
ワイルドカードを使用する際は、順番を誤るとすべてのアクセスがワイルドカードに拾われてしまうため、必ず最後に記述します。
作成したルート(routes 定数)を RouterModule モジュールの forRoot メソッドで読み込むことで、ルータとして機能させることができます。
ルータアウトレットを記述する
ルータによって入れ替わるコンポーネントのプレースホルダとなるものが、router-outlet
ディレクティブです。
View の任意の場所に記述することで、URL に対応したコンポーネントがルータによってバインドされます。
<router-outlet></router-outlet>
遷移を行うトリガを記述する
通常、Web 画面上のリンクやボタンを押下することで画面遷移を行います。
これらの遷移トリガを定義するには、以下のような方法があります。
-
routerLink
ディレクティブを記述する -
Router
のnavigate
メソッドを実行する
routerLink ディレクティブを使用する場合は、View の a タグまたは button タグにディレクティブを追加します。
<div>
<a routerLink="hoge">Go To Hoge</a>
<button routerLink="hoge">Go To Hoge</button>
</div>
<div>
<a routerLink="fuga">Go To Fuga</a>
<button routerLink="fuga">Go To Fuga</button>
</div>
<router-outlet></router-outlet>
上記の例では、リンクを押してもボタンを押しても、それぞれ「/hoge」「/fuga」に遷移します。
(下部の router-outlet に「/hoge」「/fuga」の内容が表示されます。)
スクリプトで何かしらの処理を行ったうえで遷移したい場合は、以下のように navigate メソッドを使用します。
<div>
<input type="text" #password />
<button (click)="goNext(password.value)">Go</button>
</div>
<router-outlet></router-outlet>
import { Component } from '@angular/core';
import { Router } from '@angular/router'; // 遷移を行うためのRouterをインポート
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'ngtest';
constructor(private router: Router) {}
goNext(pass: string): void {
if (pass === 'piyo') {
this.router.navigate(['hoge']); // 入力されたパスワードが「piyo」なら「/hoge」に遷移
} else {
console.log('NG! パスワードが誤っています');
}
}
}
ガード
ルータによって、ユーザはアプリケーション内のコンポーネントを自由に切り替えて参照することができるようになりました。
しかしながら、一般的なアプリケーションでは、特定のユーザにのみ参照を許可するような機能があります。(たとえばログイン機能などは最たる例です)
そのような特別な機能へのルーティングを制御するための方法として、Angular はガード
という機能を持っています。
ガードは、遷移先が利用可能かどうか判定するための関数といえます。NG の場合はルータは遷移をキャンセルします。
ng generate guard guard/auth
import { Injectable } from '@angular/core';
import {
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot
} from '@angular/router';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
// ここに何か処理を書く
return true;
}
}
上記の例では、ルータは CanActivate インタフェースのcanActivate
メソッドを呼び出すことで判定を行います。
作成したガードは、Route オブジェクトの中で指定します。
これにより、該当するパスへ遷移しようとしたときにガードのチェックが実行され、遷移の可否を判定します。
const routes: Routes = [
{ path: "hoge", component: HogeComponent, canActivate: [AuthGuard] }, // AuthGuardのチェックがOKなら参照可能
{ path: "fuga", component: FugaComponent, canActivate: [AuthGuard, UserGuard, DateGuard, ....]}, // すべてのガードのチェックがOKなら参照可能
{ path: "piyo", component: PiyoComponent }, // ガードのチェックなしで参照可能
{ path: "**", redirectTo: "/" }
];
チェックを行うガードは配列で指定します。
複数のガードを連ねた場合、すべてのガードの判定が OK だったら参照可能になります。
チェックの順番は配列で指定した順番となり、途中のガードで NG になった場合でもすべてのガードのチェックが実行されます。
HTTP クライアント
Angular では、サーバと非同期通信を行うための HTTP クライアントが提供されています。
HTTP クライアントを利用するには、HttpClientModule
をインポートします。
一般的には、HTTP クライアントはアプリケーション全体で必要になる機能であるため、root モジュールでインポートします。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [
BrowserModule,
// BrowserModule のあとに HttpClientModule をインポート
HttpClientModule
],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {}
HttpClientModule をインポートすることで、HttpClient
クラスが利用できるようになります。
実際の HTTP アクセスはこの HttpClient クラスのメソッドで行います。
なお、コンポーネントで HttpClient を使用するのは避けた方がよいとされています。
コンポーネントで HttpClient を使用してしまうと、コンポーネントの責務が肥大化しやすく管理が難しくなります。
一般的には、コンポーネントから独立した(カプセル化された)サービスクラスを用意して、HttpClient を使用します。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
const URL: string = 'https://hogehoge.jp/api/backend-fuga';
@Injectable()
export class ServerAccessService {
constructor(private http: HttpClient) {}
getRequest(): Observable<string> {
return this.http.get(URL);
}
postRequest(param: string): Observable<string> {
const httpOptions = {
'Content-Type': 'text/plain; charset=UTF-8'
};
return this.http.post(URL, param, httpOptions);
}
}
非同期で行われる通信のレスポンスを捕捉するために、HttpClient のメソッドの戻り値はObservable
型の値となっています。
これをsubscribe
(購読)することで、サーバからのレスポンスをアプリケーションが受け取ることができます。
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
private data: string;
constructor(private accessService: ServerAccessService) {}
getData() {
this.accessService.get().subscribe(
(data: string) => (this.data = data), // 通信成功時
error => console.log('error!!') //通信失敗時
);
}
}
subscribe の代わりに、View でasync
パイプを利用することもできます。
この場合、サーバからのレスポンスを受け取ると、Angular が自動でデータを読み出して View にバインドしてくれます。
subscribe の場合はどこかでunsubscribe
(購読解除)する必要があるのに対し、async パイプの場合は、コンポーネントの破棄時に Angular が自動で unsubscribe してくれます。
ただし、async パイプは複雑なデータ構造を持つレスポンスを扱うには不向きです。
また、レスポンスを受信するまではnull
とされ、View 上に何も表示されないことに注意が必要です。
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
private data$: Observable<string>; // Observable変数であることを示すために、慣例的に「$」を末尾に付与します
constructor(private accessService: ServerAccessService) {}
getData() {
this data$ = this.accessService.get();
}
}
<div>
<h1>サーバから受信した値</h1>
{{ data$ | async }}
</div>
RxJS
RxJS
は、非同期処理やイベント処理などの時間とともに変化する(いつ発生するかわからない)データ
の処理を簡潔に記述できるようにするライブラリです。
Angular はコア機能の依存ライブラリに RxJS を利用しており、Angular アプリケーションにおいても RxJS の考え方は避けて通れないものとなっています。
RxJS では、重要な概念として以下の 3 つが登場します。
名前 | 内容 |
---|---|
Observable (観測可能なもの) |
遅延プッシュされる複数の値のコレクション。 |
Observer (観測者) |
プッシュされた値の処理方法を定義したオブジェクト。 以下の 3 つの関数を持つ。 ・next(成功時の処理) ・error(エラー時の処理) ・complete(完了時の処理) |
Subscribe (購読) |
Observable と Observer を紐づけ、 Observable の観測を開始すること。 |
Producer(生産者)が断続的に提供するデータに反応して、Consumer(消費者)が何らかの処理を行う設計思想のことを、リアクティブプログラミング
といいます。
RxJS は「R
eactive Ex
tensions Library for J
avaS
cript」というように、このリアクティブプログラミングの考え方に即しています。
上記の表においては、Observable が Producer の役割、Observer が Consumer の役割にあたります。
Observable 型のデータに対してsubscribe
メソッドを通じて Observer を提供し、Observer が持つ処理方法に則ってプッシュされた値を処理します。
下記の例では、「1」「2」「3」の値を順にプッシュして完了する Observable に対して、別々の処理を行う Observer をそれぞれ観測させています。
import { Observable } from 'rxjs';
const observable = new Observable(observer => {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
});
const observer1 = {
next(data) {
console.log('subscription1 がデータを受信しました: ' + data);
},
error(err) {
console.error('subscription1 でエラーが発生しました: ' + err);
},
complete() {
console.log('subscription1 の購読が完了しました。');
}
};
const observer2 = {
next(data) {
console.log('subscription2 got new value: ' + data);
},
error(err) {
console.error('subscription2 got new error: ' + err);
},
complete() {
console.log('subscription2 finished.');
}
};
const subscription1 = observable.subscribe(observer1);
const subscription2 = observable.subscribe(observer2);
subscription1 がデータを受信しました: 1main.js:591:25
subscription1 がデータを受信しました: 2main.js:591:25
subscription1 がデータを受信しました: 3main.js:591:25
subscription1 の購読が完了しました。main.js:597:25
subscription2 got new value: 1 main.js:602:25
subscription2 got new value: 2 main.js:602:25
subscription2 got new value: 3 main.js:602:25
subscription2 finished. main.js:608:25
以下のように subscribe メソッドに直接 Observer を記述することもできます。
Observer オブジェクトを引数とする以外に、関数を 1 ~ 3 個渡すことでも subscribe が可能です。
その場合、引数として渡した順番に沿って next、error、complete と見なされます。
また、next 以外のメソッドは任意であるため、指定しなくても動作します。
const subscription1 = observable.subscribe({
next(data) {
console.log('subscription1 がデータを受信しました: ' + data);
},
error(err) {
console.error('subscription1 でエラーが発生しました: ' + err);
},
complete() {
console.log('subscription1 の購読が完了しました。');
}
});
const subscription2 = observable.subscribe(
data => console.log('subscription2 がデータを受信しました: ' + data),
err => console.log('subscription2 でエラーが発生しました: ' + err),
() => console.log('subscription2 の購読が完了しました。')
);
const subscription3 = observable.subscribe(data =>
console.log('subscription3 がデータを受信しました: ' + data)
);
subscription1 がデータを受信しました: 1main.js:591:25
subscription1 がデータを受信しました: 2main.js:591:25
subscription1 がデータを受信しました: 3main.js:591:25
subscription1 の購読が完了しました。main.js:597:25
subscription2 がデータを受信しました: 1main.js:600:68
subscription2 がデータを受信しました: 2main.js:600:68
subscription2 がデータを受信しました: 3main.js:600:68
subscription2 の購読が完了しました。main.js:600:182
subscription3 がデータを受信しました: 1main.js:601:68
subscription3 がデータを受信しました: 2main.js:601:68
subscription3 がデータを受信しました: 3main.js:601:68
complete メソッドまたは Error メソッドが呼ばれた時点で観測は終了しますが、complete メソッドを呼ばずに、観測されている限りデータを提供し続ける Observable もありえます。
その場合は、データの利用者側が任意のタイミングで観測を打ち切る必要があります。
観測を打ち切るには、Subscription
オブジェクトのunsubscribe
メソッドを実行します。
Subscription は観測行為そのものを表すオブジェクトです。(雑誌の購読契約みたいなものとイメージするとよいでしょう)
subscribe メソッドで観測を開始した際、その戻り値として返却されます。
// 観測の開始
const subscription = observable.subscribe({
next(data) {
// 何かの処理
},
error(err) {
// 何かの処理
},
complete() {
// 何かの処理
}
});
// 観測の終了
subscription.unsubscribe();
RxJS の特長として、断続的に提供されるデータを配列のように扱える
ことがあります。
たとえば、「1」「2」「3」を順番に提供する Observable があった場合、あたかも[1, 2, 3]
という配列であるかのように操作を加えることができます。
このデータ操作機能のことを、オペレータ
といいます。
以下の例では、 Observable が提供するデータのうち、奇数のみを取り出し、さらに 100 倍するような操作をしています。
import { Observable, pipe } from 'rxjs';
import { map, filter } from 'rxjs/operators';
const observable = new Observable<number>(observer => {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
});
const sub = observable
.pipe(
filter(data => data % 2 !== 0), // 奇数のみ取り出す
map(data => data * 100) // 100倍する
)
.subscribe(data => console.log(data));
100 main.js:591:232
300 main.js:591:232
上記の例のうち、filter
やmap
がオペレータです。
複数のオペレータを連ねるにはpipe
メソッドを使用します。
Angular Material
近年、Google が推奨しているデザイン手法がマテリアルデザイン
です。
「紙」と「インク」のイメージで構成された部品に反射光や影の効果を施すことで、より自然に「物体が存在する」ことの表現を目指したデザイン手法です。
このマテリアルデザインの原則に則って作られた UI 部品(ボタンやフォーム等)ライブラリが、Angular Material
です。
Angular の開発チームによって作成されたライブラリであり、公式の Angular ファミリーです。
そのため、Angular フレームワーク が View(.html ファイル)に作用する処理を阻害せず、高い親和性をもって動作する UI 部品を提供してくれます。
Angular Material は外部の npm パッケージとして公開されています。
npm install のほか、ng add
コマンドでもインストールが可能です。
ng add では、以下のような関連する初期設定まで簡潔に行えるため、公式では ng add を使うよう推奨されています。
- 依存ライブラリ(Component Dev Kit、Angular Animations)のインストール
- カラーテーマの設定
- HammerJS(ジェスチャー認識ライブラリ)のインストール
- BrowserAnimationsModule のインポート
- index.html への Roboto フォントの追加
- index.html へのマテリアルデザインアイコンフォントの追加
- root CSS の調整
- body の margin を 0 に
- html と body の height を 100%に
- デフォルトフォントの設定
ng add @angular/material
Angular Material に適用するカラーセットのことをテーマ
といいます。
テーマは自作することも可能ですが、Angular Material では組込テーマとして以下の 4 種類を用意しています。
- deeppurple-amber.css
- indigo-pink.css
- pink-bluegrey.css
- purple-green.css
使いたいテーマは、root CSS でインポートするほか、Angular.json のビルド設定(グローバルスタイル)に加えることでアプリケーションに適用できます。
(ng add でインストールした場合は後者の方法です。)
"build": {
~ 省略 ~
"styles": [
"./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css",
"src/styles.css"
],
~ 省略 ~
}
Angular Material の UI 部品(ボタンやチェックボックス等)はすべてモジュール化されているため、アプリケーション内で使用するにはまずモジュールをインポートする必要があります。
root モジュールにインポートしておくとアプリケーションの全体で利用可能になるため、特別な理由がない限り root モジュールにインポートしておくのがよいでしょう。
import { MatButtonModule, MatCheckboxModule } from '@angular/material';
@NgModule({
~ 省略 ~
imports: [
MatButtonModule,
MatCheckboxModule
],
~ 省略 ~
})
インポートした UI 部品を View に記述することで、Angular Material の UI 部品 を画面上に配置することができます。
以下は代表的な部品の例です。
もちろんこれらの部品に対しても、ディレクティブを使ったりバインディングしたりすることができます。
<div>
<button mat-raised-button>Button</button>
</div>
<div>
<mat-form-field>
<input matInput type="text" />
</mat-form-field>
</div>
<div>
<mat-checkbox>Checkbox label</mat-checkbox>
</div>
<div>
<mat-radio-group>
<mat-radio-button>Radio A</mat-radio-button>
<mat-radio-button>Radio B</mat-radio-button>
<mat-radio-button>Radio C</mat-radio-button>
</mat-radio-group>
</div>
<div>
<mat-icon>home</mat-icon>
</div>
スライドトグルやスライダーなど、ジェスチャー操作が可能な UI 部品を使用する場合は、HammerJS
ライブラリをインポートする必要があります。
ng add で Angular Material をインストールした場合は、HammerJS をインストールするかどうかも併せて確認されます。
ng add で HammerJS のインストールを拒否した場合や、npm install で Angular Material をインストールした場合などは、別途 npm からインストールすることで利用できます。
npm install --save hammerjs
インストールができたら、アプリケーションのエントリポイント(main.ts)でインポートします。
これにより、ジェスチャー操作を伴う UI 部品を利用できるようになります。
import 'hammerjs';
テスト
Angular では、以下の自動テストをサポートしています。
-
Jasmine
テストフレームワーク +Karma
テストランナーによる自動ユニットテスト -
Protractor
テストフレームワークによる自動 e2e(End-to-End)テスト
ユニットテスト
ユニットテストは Angular に組み込まれているJasmine
とKarma
によって実行されます。
テストコードを解釈し、実行するのは Jasmine の機能です。
Karma は、Jasmine の機能を利用してブラウザ上でテストを行い、結果をレポートします。
また、Karma が実行されている間、ソースコードの変更を検知して都度テストを再実行してくれます。
自動ユニットテストは Angular CLI のコマンドによって実行することができます。
以下のコマンドを実行するとテストが開始され、プロジェクト内に含まれる.spec.ts
ファイルに定義されたテストが実行されます。
ng test
$ ng test
Browserslist: caniuse-lite is outdated. Please run next command `npm update`
30% building 15/15 modules 0 active22 11 2019 15:36:30.973:WARN [karma]: No captured browser, open http://localhost:9876/
22 11 2019 15:36:31.026:INFO [karma-server]: Karma v4.1.0 server started at http://0.0.0.0:9876/
22 11 2019 15:36:31.027:INFO [launcher]: Launching browsers Chrome with concurrency unlimited
30% building 17/17 modules 0 active22 11 2019 15:36:31.089:INFO [launcher]: Starting browser Chrome
22 11 2019 15:36:35.035:WARN [karma]: No captured browser, open http://localhost:9876/
22 11 2019 15:36:35.132:INFO [Chrome 78.0.3904 (Windows 10.0.0)]: Connected on socket WxxEvvEsAy5bM4a2AAAA with id 10021485
Chrome 78.0.3904 (Windows 10.0.0): Executed 12 of 12 SUCCESS (2.265 secs / 2.225 secs)
TOTAL: 12 SUCCESS
TOTAL: 12 SUCCESS
TOTAL: 12 SUCCESS
=============================== Coverage summary ===============================
Statements : 100% ( 41/41 )
Branches : 100% ( 2/2 )
Functions: 100% ( 17/17 )
Lines: 100% ( 32/32 )
================================================================================
テストのコードカバレッジを出力するには、ng test コマンドに--code-coverage
オプションを付与するか、Angular.json において以下のように設定します。
"test": {
~ 省略 ~
"codeCoverage": true
~ 省略 ~
}
コードカバレッジを出力した場合、プロジェクト内にcoverage
フォルダが作成され、HTML 形式でレポートが作成されます。
また、上記の例のようにコンソールにもCoverage summary
が表示されます。
テストコード(.spec.ts)の構成は以下のようになっています。
- import
- describe
- beforeEach
- it
- expect
- afterEach
import では、テストに必要なモジュールやテスト対象のクラスを読み込みます。
describe は一区切りのテストシナリオを表します。describe の中に describe をネストすることもできます。
beforeEach、it、afterEach は describe の中に任意の数だけ記述します。
実際のテストケースを表すのは it です。
beforeEach、afterEach は、各 it の前後で毎回実施する初期化処理・終了処理などを記述します。
it の中では、想定結果の検証処理である expect を必ず記述します。
expect は任意の数だけ書くことができ、複数書いた場合、そのうちのひとつでも失敗すればその it は NG になります。
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('C-1: ルートコンポーネントのテスト', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [AppComponent]
}).compileComponents();
}));
it('C-1-1: コンポーネントが生成できること', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`C-1-2: タイトルが「test-sample」であること`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('test-sample');
});
it('C-1-3: H1見出しのテキストが「Welcome to test-sample!」であること', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain(
'Welcome to test-sample!'
);
});
});
expect に続くtoBeTruthy
、toEqual
、toContain
などはMatcher
と呼ばれる関数です。
expect の引数である検証対象と、Matcher の引数である比較対象を照らし合わせ、条件を満たしていれば expect は成功します。
Angular のユニットテストにおいては、TestBed
というテスト用の仮想 NgModule を利用します。
TestBed では、通常の NgModule と同様に declarations、imports、providers などを設定でき、モックサービスやモックコンポーネントを使用する場合はそれらを TestBed に読み込みます。
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TweetListComponent } from './tweet-list.component';
import { Component, Input } from '@angular/core';
import { By } from '@angular/platform-browser';
@Component({
selector: 'app-tweet',
template: '<div>Stub Component</div>'
})
class StubTweetComponent {
@Input() tweet: any;
}
describe('C-3: ツイートリストコンポーネントのテスト', () => {
let component: TweetListComponent;
let fixture: ComponentFixture<TweetListComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [TweetListComponent, StubTweetComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TweetListComponent);
component = fixture.componentInstance;
const stubValue = [
{
user: 'test 1',
content: `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`,
retweet: 12345,
like: 67890
}
];
component.tweetList = stubValue;
fixture.detectChanges();
});
it('C-3-1: ツイートリストがすべて表示されていること', () => {
const element: HTMLElement = fixture.debugElement.nativeElement;
const tweetComponents = element.querySelectorAll('app-tweet');
expect(tweetComponents.length).toBe(3);
});
it('C-3-2: リツイートイベントに応じてRT数が増加すること', () => {
let beforeCount: number;
const spy = spyOn(component, 'retweet').and.callThrough();
component.tweetList.forEach(tweet => {
beforeCount = tweet.retweet;
fixture.debugElement
.query(By.css('.tweet'))
.triggerEventHandler('retweet', tweet);
expect(spy).toHaveBeenCalled();
expect(tweet.retweet).toBe(beforeCount + 1);
});
});
});
Jasmine には、特定のオブジェクトメソッドや関数の実行を検知して、任意の動きに差し替えるスパイ
という機能があります。
上記の例(テストケース C-3-2)では、TweetListComponent インスタンスのretweet
メソッドをスパイし、callThrough
(スパイから本来のメソッドを呼び出す)しています。
これにより、expect においてスパイオブジェクトのtoHaveBeenCalled
を検査することで、そのメソッドが呼び出されたかどうか
をチェックしています。
上記の例はただスパイを経由しているだけですが、あらかじめ定めた戻り値をスパイから返させることで、モックやスタブを実現することができます。
e2e テスト
実際にアプリケーションをビルドしてブラウザ上で行うテストをe2e
(End-to-End)テストといいます。
ユニットテストではモックやスタブを用意してテスト対象の動作のみにフォーカスしたテストを行いますが、e2e テストではサーバサイドを含めた全体的な動きを検査することが目的です。(総合テスト段階に相当します)
e2e テストにおいてはProtractor
フレームワークを利用しますが、Protractor は内部的に Jasmine のテスト機能を利用しています。
したがって、テストコードの文法などはユニットテストと同じように書くことができます。
自動 e2e テストは Angular CLI のコマンドによって実行することができます。
以下のコマンドを実行すると、e2e
フォルダ内のapp.e2e-spec.ts
ファイル(ファイルは変更可能です)に定義されたテストコードを実行し、結果をレポートします。
ng e2e
$ ng e2e
[15:09:32] I/config_source - curl -oC:\Users\user\Desktop\DEV\test-sample\node_modules\protractor\node_modules\webdriver-manager\selenium\chrome-response.xml https://chromedriver.storage.googleapis.com/
[15:09:33] I/update - chromedriver: file exists C:\Users\user\Desktop\DEV\test-sample\node_modules\protractor\node_modules\webdriver-manager\selenium\chromedriver_78.0.3904.105.zip
[15:09:33] I/update - chromedriver: unzipping chromedriver_78.0.3904.105.zip
[15:09:34] I/update - chromedriver: chromedriver_78.0.3904.105.exe up to date
Browserslist: caniuse-lite is outdated. Please run next command `npm update`
Date: 2019-11-22T06:09:51.188Z
Hash: 13114b74aad04ac5cd11
Time: 8615ms
chunk {main} main.js, main.js.map (main) 28 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 248 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 6.08 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 16.7 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 3.94 MB [initial] [rendered]
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
i 「wdm」: Compiled successfully.
[15:09:53] I/launcher - Running 1 instances of WebDriver
[15:09:53] I/direct - Using ChromeDriver directly...
DevTools listening on ws://127.0.0.1:55696/devtools/browser/05988235-51e7-497a-920a-346cd5940efc
Jasmine started
workspace-project App
√ should display welcome message
Executed 1 of 1 spec SUCCESS in 1 sec.
[15:10:00] I/launcher - 0 instance(s) of WebDriver still running
[15:10:00] I/launcher - chrome #01 passed