NHN Cloud Meetup 編集部
JavaScriptフレームワークの概要2 – Angular 2
2016.11.03
942
これから4回にわたり、JavaScript(フロントエンド)フレームワークについて紹介したいと思います。以下のような内容で連載する予定です。
- Cycle.js
- Angular 2
- Vue.js
- React
Angular 2
はじめに
Angularは、拡張されたHTML文法をベースにしたWebアプリケーションフレームワークで開発されました。データバインディング、テンプレート文法などの便利な機能を提供し、Webアプリケーションのメンテナンス、開発のスピードを大幅に向上させ、数多くの開発者から高い人気を得ています。彼らのコミュニティも活発で、今後発展が期待されるフレームワーク(あるいはプラットフォーム)に数えられています。
Framework to Platform
もはや「AngularJS」ではない。「Angular」だ。
「AngularJS 1」は、Webアプリ開発の生産性に重点を置きましたが、「Angular 2」では、生産性はもちろん様々なプラットフォームに合わせて開発できるように開発範囲を拡大することにも重点を置いています。
(https://angular.io)
ng-confのビデオ資料から、Angular 2のウェブ開発、テスト、プログレッシブウェブアプリ、サーバーサイドレンダリング、ネイティブアプリ、マテリアルデザイン、サードパーティプログラムなど、Angular 2のプラットフォームを間接的に体験できます。
TypeScript
(TypeScript:Angular 2’s Secret Weapon)
Angular 2は、既存のJavaScriptではなく、TypeScriptを主言語として推奨しています。初めて目にするTypeScriptについてプレッシャーを感じるかもしれませんが、JavaScriptをよく理解していれば、TypeScriptの学習は難しくないでしょう。
TypeScriptはJavaScriptの拡張言語で、すべてのJavaScript文法を採用しています。そのため、jsからtsに拡張子だけ変更しても問題なく動作します。TypeScriptの拡張された言語的機能によって、ユーザーは便利なコードツーリング機能が使用でき、開発の生産性も大幅に向上することでしょう。詳しい機能の説明は公式ホームページを確認してみよう。
開発環境
開発フレームワークを導入する際、開発環境を整える部分が大きな障壁だと感じることはありませんか?ビルドツール、ビルド設定、静的アナライザ(Linter)、トランスコンパイラ(Transpiler)、テスト環境、配布、デバッグなど整備すべき環境があまりにも多いからです。「Angular 2(以下Angular)プロジェクトを開始するときの最大のハードルは何ですか?」というアンケート調査でも「開発環境の設定」が26%で1位を占めました。
(https://twitter.com/Brocco/status/713374344823640064)
開発環境を準備して、単純に「app works!」フレーズをブラウザに表示するだけでも、次のような過程が必要です。
- NPMパッケージの設定とディペンデンシー – package.json作成
- タイプスクリプト
- tsconfig.json作成
- typgins.json作成
- tslint.json作成
- モジュラー/バンドラー設定(SystemJSまたはWepback…)
- パッケージインストール
- フォルダ構造作成、ルートモジュール、ルートコンポーネント、main(entry)スクリプトの作成
- index.html作成
- npm start
- テスト環境設定(Karma、Jasmine、Protractor…)
このような繰り返しの設定とパッケージのインストールを、3〜4回のcommandだけで簡単に処理できると、とても楽になりますね。
まさに、Angular CLIがその役割をしてくれます。
Angular-CLIはYeomanと類似したプログラムで、初期の開発環境スキャフォールディング機能を提供し、この他にもコンポーネントの追加/削除、テスト、構築、追加ライブラリの登録など、ほぼすべての基盤が一緒に提供されます。このような機能で、私たちは迅速かつ容易にアプリケーションを開発できます。1〜2分で基本的な開発環境を構成して、実際に「app works!」をブラウザに表示できるのです。
ブラウザのサポート範囲
IE9以上など、広く対応しています。
Chrome | Firefox | Edge | IE | Safari | iOS | Android | IE mobile |
---|---|---|---|---|---|---|---|
latest | latest | 14 | 11 | 10 | 10 | Marshmallow(6.0) | 11 |
13 | 10 | 9 | 9 | Lollipop(5.0、5.1) | |||
9 | 8 | 8 | KitKat(4.4) | ||||
7 | 7 | Jellybean(4.1、4.2、4.3) |
(https://angular.io/docs/ts/latest/guide/browser-support.html)
アーキテクチャ
Angularアプリケーションは、基本的にテンプレートとプロパティ(データ)/イベントバインディングで動作します。このようなテンプレートとの結合、アプリケーションロジックを合わせて、画面の部分を構成するものをコンポーネント(Component)と呼びます。
また、AngularはNgModulesと呼ばれるモジュールシステムを保有しています。すべてのAngularアプリケーションは、少なくとも1つ以上のモジュールを保有しており、慣例上、ルートモジュールは「AppModule」と呼ばれ、このルートモジュールにBootstrappingを実行して、アプリケーションを起動します。このようなモジュールシステムでアプリケーションを機能やドメインなどで組織化し、それぞれに必要なAngularのコンポーネント、サービス、ディレクティブ(Directives)などを統合して管理します。これにより、重複コードを回避したり、モジュールの遅延ロード(Lazy loading)を防止したりできます。
モジュールシステムのように、この記事で詳しく説明していない内容は、ガイドドキュメントで幅広くカバーされているので、Angular開発を始めるなら、必ず読まれることをおすすめします。
コンポーネント(Component)
(https://angular.io/docs/ts/latest/guide/architecture.html)
Angularで最も重要な部分はコンポーネントです。コンポーネントは、画面を構成する1つの単位です。基本的には、テンプレート + メタデータ + (コンポーネント)クラスの組み合わせで構成され、アプリケーションロジックを定義します。テンプレートはHTMLで、ここにAngularだけのテンプレート文法(Syntax)を追加して、データやイベントをバインドします。そして、メタデータを使って単純なクラスをテンプレートに結び付けて、Angularにコンポーネントとして登録します。Angularは、このデータをコンパイルして、実際のオブジェクトも作成しながらレンダリングをします。このようなコンポーネントは、それぞれ独自のライフサイクル(Lifecycle)を持っており、Hookからより洗練された細かい動作を定義できます。
// app.component.ts import { Component } from '@angular/core'; @Component({ // Metadata selector: 'app-root', templateUrl: './app.component.html', // Template styleUrls: ['./app.component.css'] }) export class AppComponent { // Class title = 'app works!'; }
前述したように、コンポーネントは画面全体を構成する1つの単位ですが、この単位は分割もできます。このように分割したコンポーネントでツリーを構成して、全体的に1つのモジュールまたはページを作り上げます。
(https://angular.io/docs/ts/latest/guide/architecture.html)
ツリーで構成されたコンポーネントは@Inputと@Outputを用いて互いに通信できます。Angular-Universityで提供しているサンプルコードが参考になるでしょう。
データバインディング(Data Binding)
Angularのデータバインディングは、コンポーネントとDOM間のデータ通信方式を意味しますが、コード上ではコンポーネントのクラスとテンプレート間のデータ通信と考えるとよいでしょう。
(https://angular.io/docs/ts/latest/guide/architecture.html)
次のバインディング方式をみてみよう。
<li></li> <hero-detail [hero]="selectedHero"></hero-detail> <li (click)="selectHero(hero)"></li>
- 文法は単純な出力(Interpolation)で、通常クラスが保有しているプロパティをそのまま出力する。
- []はプロパティバインディング(Property Binding)に対応するコンポーネントやDOMのプロパティに値を指定する。
- ()はイベントのバインディング(Event Binding)に対応するコンポーネントやDOMでイベントが発生したときに実行されるハンドラを指定する。
ここは見逃しやすいところですが、バインディング文法が文字列で作成されても、Angularはこれをコードで認識してEvaluationします。
[hero]="selectedhero" (js) heroDetail.hero = "selectedHero"; (x, 文字列のままで動作しない) (js) heroDetail.hero = this.selectedHero; (o, 文字列をEvaluationして動作する)
では次に、双方向データバインディング(Two-way data binding)方式を見てみましょう。
<input [(ngModel)]="hero.name">
双方向データバインディングは、プロパティとイベントを同時にバインドする方法で、ngModeldirectiveと[()]構文を使用します。この場合、hero.nameが変わるとinput.valueが変わり、input.value
が変わるとhero.name
も変わります。そのため、それぞれ別々にイベントとプロパティをバインドする必要がなく、簡単な文法で処理できます。
詳しい内容や、バイディング時のHTML属性とDOMプロパティの違い、単純な出力とプロパティバインディングの使用方法などについては、Angularのテンプレートバインディング構文ページから確認できます。
ディレクティブ(Directives)
Angularは、DOMを扱うすべての方法をディレクティブに表現します。つまり、コンポーネントもディレクティブの一種類であるということです。しかし、コンポーネント自体も非常に重要な概念であるため、ディレクティブと分離して考えています。広い意味でディレクティブがコンポーネントを含み、実質的に私たちが指す狭い意味でのディレクティブは、コンポーネントを除いた属性ディレクティブと構造ディレクティブを意味します。
属性ディレクティブ(Attribute Directives)
属性ディレクティブは、DOMの見た目や動作を変更します。理想的な属性ディレクティブは、コンポーネントと関係がなく、詳細実装に束縛されない形で動作します。代表的なものとして、AngularのngStyle、ngClassディレクティブなどが属性ディレクティブに属します。基本的にProperty Bindingと同じように動作します。バインディング自体がディレクティブに属すると考えてもよいでしょう。
<p [style.background]="'lime'">I am green with envy!</p> <!-- 参考: styleとngStyleを一緒に使用した場合は、一緒に適用される。 --> <p [style.color]="'white'" [ngStyle]="{background: myColor}">Background Color: </p>
構造ディレクティブ(Structural Directives)
構造ディレクティブは、コンポーネントやDOMを追加/削除して、どのように画面に表示させるかを表現します。つまり、実際のDOMツリーに直接関連があった場合に、見ることができます。代表的なものとして、Angularの*ngFor、*ngIfディレクティブなどが構造ディレクティブに属します。
<li *ngFor="let hero of heroes"></li> <hero-detail *ngIf="selectedHero"></hero-detail>
*ngForの場合、コンポーネントのheroesリストにあるhero数の<li>DOMを複製します。
*ngIfは、選択されたheroがある場合にのみ、HeroDetailComponent
をレンダリングします。
サービス(Service)
(https://angular.io/docs/ts/latest/guide/architecture.html)
サービスオブジェクトは、値、関数、アプリケーションに必要な機能など、広い範囲で使用されます。実際にほとんどがサービスオブジェクトになることができます。Loggerクラスや、HTTPモジュールからAPIを呼び出すクラス、複雑な計算ロジックを持っているクラスなど非常に多様で、特別に定義するような特徴はありません。
そのため、サービスオブジェクトはAngularが特別に定義して提供するような基本的な要素ではありません。とはいえ、私たちがサービスオブジェクトを使わずに、Angularが提供する機能だけでアプリケーションのすべてのロジックを作成して実装するのは困難でしょう。サーバーからデータを受け取ったり、検証したり、コンソールに出力したりするのは、コンポーネントの責任ではないからです。コンポーネントは、ユーザーとの相互作用、ビューとアプリケーションロジックの伝達に対する役割だけで、良いコンポーネントは、データバインディングの属性/メソッドのみを表現します。コンポーネントと関連しない作業を「サービス」という名前で処理します。サービスオブジェクトがないと、良いコンポーネントは実装できないでしょう。
このようなサービスオブジェクトは、AngularのDI(Dependency Injection)からコンポーネントと相互作用します。
テスト
Angularのロジックをテストするには様々な方法がありますが、Angular基準のテスト方法をみてみると、モジュール、コンポーネント、サービス、パイプ、ディレクティブ、非同期、イベント、ルータ、end-to-endのテストなど、それぞれ必要なテストが非常に多いようです。幸いにも各テストの原理や手法は、Angularのテストガイドで詳しく説明されていますので、テストを作成する際には必ず参考にしましょう。
テスト環境
JSテストは、複数のフレームワークとライブラリの組み合わせで行えますが、Angularは通常、Karma – Jasmineの組み合わせでテストを実行します。KarmaはAngularテストを容易にするために作成されており、拡張性や使用性が良く、他のJSテストにも広く使われています。
ただし、いくらテストツールを使って簡単に作成できたとしても、実際にその環境に合わせるには、多くの努力とノウハウが必要です。そこで、Angular-CLIを用いたテスト環境の構築を推奨します。
コンポーネント・テスト
Angularで最も重要なテストを挙げるとすると、おそらくコンポーネントテストになるでしょう。コンポーネント自体がAngularにおいて非常に重要な概念であるだけに、コンポーネントテストも非常に重要です。Isolatedテスト、Shallowテスト、統合(Integration)テストの3つがあります。
Isolatedテスト
Isolatedテストは、複雑なロジックまたはレンダリングとは関係なく、ロジックをテストするときに実行します。
Formがあるコンポーネントをテストする場合を考えてみよう。コンポーネントをあえてレンダリングしなくてもFormのonSubmitなどのハンドラを直接呼び出して、値の有効性や発行するActionを検証することができます。つまり、ユーザーによって入力された「どのようなイベントが発生してステータス値が変更された」というような仮定の下では、メソッドや動作をテストすることだけに集中します。一般的な関数やメソッドのユニットテストと類似しています。
Shallowテスト
たまに、メソッドや関数がDOMに依存することがあり、実際にレンダリングせずにテストを実行するのが難しいケースがあります。かといって、アプリケーション全体をレンダリングするには、テストに対するコストが非常に大きくなるため、通常は当該コンポーネントのみをレンダリングするShallowテストを実行します。Shallowテストは、各ケースに該当するコンポーネントのテンプレートだけをレンダリングし、子コンポーネントのテンプレートはレンダリングしません。テストを1つのコンポーネントとして独立させることができます。
NO_ERRORS_SCHEMAというスキーマをコンパイラに渡すと、コンパイラは認識されていない要素と属性を無視します。このため、不要なコンポーネントやディレクティブを宣言する必要がなくなります。
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { AppComponent } from './app.component'; import { RouterLinkStubDirective } from '../testing'; //... beforeEach( async(() => { TestBed.configureTestingModule({ declarations: [ AppComponent, RouterLinkStubDirective ], schemas: [ NO_ERRORS_SCHEMA ] }) .compileComponents() .then(() => { fixture = TestBed.createComponent(AppComponent); comp = fixture.componentInstance; }); })); it('....', () => {....});
ただし、Shallowテストをするときは、コンパイラがエラーを教えてくれません。したがって、開発者は、入力ミスや誤って使用されているコンポーネント、ディレクティブの問題も通知がこないということに注意が必要です。
統合テスト
最後に、統合テストはモジュールを基準にテストします。Shallowテストは、テストに必要なオブジェクトを除いた残りの依存性を完全に模倣しますが、統合テストは、実際の依存性をそのまま使用します。よってShallowテストを入念に作成したとしても、最終的に必要なすべてのコンポーネント、サービスなどの実装が有機的に動作するとは限りません。Shallowテストが対象コンポーネントが設計した通りに動作するかに重点を置くとすると、統合テストはデータの正確性に重点を置いて検証します。
通常はTestBed.configureTestingModuleで、テストしたいモジュールをimports経由で構成して、テストを実行します。
End-To-Endテスト
通常、Angularのe2eテストは、Protractorを使用します。Protractorは、Angular用のend-to-end(以下e2e)テストフレームワークで、NodeJSプログラムです。SeleniumのWebDriverIO(WebDriverJS)を利用して、e2eテストをサポートします。
(http://www.protractortest.org/#/infrastructure)
アプリケーションをブラウザ、モバイルに駆動させて、実際のユーザーと同じ入力とアクションをシミュレーションし、アプリケーション全体を行為を中心にテストできます。また、JasmineとMochaをサポートするため、一貫性のあるスタイルのテストコードを作成できます。
パフォーマンス
ng-conf 2016のKeynoteによると、Angular 2は前作よりも、ランニングタイム、レンダリングのパフォーマンスが約5倍速くなっています。コンパイラから、遅延ロード、サーバーレンダリング、ウェブウォーカー、ウェブコンポーネントなど、数多くの技術を積極的に導入し、最適化した結果でしょう。
中でも、前作のDirty checkingとは異なる他方式の変更検知が、性能向上において最も大きな影響を与えたのではないかと言われています。
変更検知(Change detection)
変化検知は、アプリケーションのビュー(画面)にあるDOM、あるいは内部データモデルで変化が生じたときに検知することを言います。
(http://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html)
例えば、削除ボタンを押して項目を削除したり、追加ボタンで項目を増やすと、DOMの変更が発生します。逆に、内部コードによって制限時間が経過して記事が削除されたときは、フレームワークのデータモデルの変更が発生します。このような場合は、両方ともビューの変更が発生し、これをRerenderingと呼びます。では、レンダリングの対象が何か、どのようにして分かるのでしょうか?DOMツリーにアクセスするコストは常に大きいので、どこを更新すべきかを把握し、可能な限りDOMツリーへのアクセス回数を減らさなければなりません。
フレームワーク毎に異なる方法でこの問題を解決しましたが、Angularは一部のイベントに集中します。
ビューの更新時期
Angularはビューの更新が必要か、どうやって分かるのでしょうか?
@Component({ template: ` <h1> </h1> <button (click)="changeName()">Change name</button> ` }) class MyApp { firstname:string = 'Pascal'; lastname:string = 'Precht'; changeName() { this.firstname = 'Brad'; this.lastname = 'Green'; } }
ボタンをクリックすると、コンポーネントの状態値を変更するハンドラが実行されます。コンポーネントの状態値が変わる瞬間が、ビューを更新しなければならない瞬間です。
他のケースもあります。
@Component() class ContactsApp implements OnInit{ contacts:Contact[] = []; constructor(private http: Http) {} ngOnInit() { this.http.get('/contacts') .map(res => res.json()) .subscribe(contacts => this.contacts = contacts); } }
コンポーネントが初期化されるとき、HTTPリクエストを送信しますが、要請に対する応答が届く度にリストは更新されます。この時点で、アプリケーションの状態が変わり、ビューを更新する必要があります。
結論として、アプリケーションの状態変更は、次のような非同期的な動作で発生します。
- DOMイベント
- XHR
- Timer
視点を変えると、どのような非同期処理が実行されるとアプリケーションの状態が変更される可能性が高くなるか、ということです。この非同期の時点が、まさにAngularにビューを更新しなければならないと通知する時点です。しかし、上記のコードでは、私たちはビューを更新させるコードを実行していませんね。Angularでは、Zoneがビューを更新するように指示してくれます。
Zoneは、Angularとは関係なく、JS自体の非同期APIをモンキーパッチしてHookingするライブラリであり、AngularではこのZoneを外部依存性として積極的に活用しています。AngularはNgZoneと呼ばれる固有のZoneを保有します。Angularのコード内にApplicationRefと呼ばれるものがありますが、NgZonesとonTurnDone
イベントの変更を検知しています。両方のイベントのうち一方が発生すると、tick()を実行します。
Angularは、全コンポーネントの有機的な変化を、どのように処理するのでしょうか?Angularの各コンポーネントには、変化検知する独自のディテクタ(Change Detector)があります。この部分が変更発生時、各コンポーネントに独立して変更を検知できるようにします。コンポーネントツリー内で非同期動作が実行されると、Zoneは自分に与えられたハンドラを処理し、Angularに変更を知らせます。Angularは、固有のディテクタを用いて、実際のバインディングを確認し、更新します。ツリーでは、データは常にTop-Downで流れます。このような一方向のデータフローが循環構造よりも予測しやすいでしょう。
(http://pascalprecht.github.io/slides/angular-2-change-detection-explained/#/59)
最適化?
Angularのコンポーネントツリー自体が単純な構造で、コード自体もVMに優しいコードを作り出すので、すべてのコンポーネントをイベント毎に確認しても高速で処理できます。例えば、イベントが発生した場合、数分間で数十万個のコンポーネントを確認することもできます。
各コンポーネントに検知器がありますが、とはいえ定型化されたディテクタを各コンポーネントで個別に扱うというわけではありません。
Angularは、各コンポーネントの構造に合わせて、実行時にディテクタのクラスを定義して作成するため、コンポーネントに適切な検知器を生成しながらも、VMに優しい最適化されたコードを生成します。よって私たちはパフォーマンスについて深く考える必要はなく、Angularが自動でサポートしてくれます。
Immutable&Observable
いかなる非同期イベントであれ、一旦発生するとアプリケーションの状態が変化する可能性があります。
都度コンポーネントの変更有無を確認しなければならないのは、非常にストレスになりますね。変更を検知する時間を簡単に表現すると、このようになります。
全体変更検知時間 = 1つのバインディングを確認する時間 * バインディング個数
1つの結合を確認する時間は、動的なディテクターを生成して最適化しました。残っているのは、全体の結合数を調整する部分ですが、実際にアプリケーションの絶対的な全体結合の数を減らすことは難しいです。代わりに変更検知したときに、全体の結合をすべて確認するのではなく、変更された結合のみを確認できるようにします。ImmutableとObservableを使うことで変更有無が分かり、Angularをより高速化できます。
通常、このような最適化は、Angularの変更検知戦略(Change Detection Strategy)をOnPush設定して処理します。
Immutable
Immutableオブジェクトは内部のプロパティ値を変更するのではなく、完全に新しいオブジェクトを作成して内容を補充する方法で値を変更します。
そのため、修正を加えても、以前のオブジェクトとは異なる別オブジェクトを指すようになります。
Angularの状態が変更されると、状態を管理するオブジェクト自体が別オブジェクトとして生成され、深さを比較せずにリファレンスを比較するだけで状態の変化を検知できます。
(http://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html)
Angularは変更検知の過程で、変更がないサブツリーをスキップします。このように実行回数を全体から一部に減らすことで、パフォーマンスを向上させることができます。
Observable
ObservableはImmutableとは別の方法で、変更が発生したことを保証します。Immutableがオブジェクトのレファレンスを保証すると、Observableは変更点のイベントを発生させて反応します。ところが、ObservableとOnPush戦略を一緒に使用すると、Observableオブジェクトのリファレンスが変更されないため、サブツリーの更新は発生しません。変更が発生したコンポーネントのみ更新が必要な場合は問題ありませんが、サブコンポーネントの変更も更新が必要な場合、問題になります。このような場合は、ディテクターのmarkForCheck()というAPIから変更が発生したコンポーネントのサブツリーまでを更新させる方法があります。
(http://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html)
この変更検知の範囲を調整して、パフォーマンスを向上できます。
Web components
開発者が直接HTML要素を作成する技術のことを言います。詳細は、公式ホームページから確認できます。Angular 2は内部的にWeb componentsを借用してNative Elementを利用するため、既存のJavaScriptコンポーネントを利用するよりもはるかに速くなりました。
まとめ
Angularは、多くの部分が更新されています。
AngularJSにはなかったサーバーレンダリングも可能になり、パフォーマンスの向上、主要言語の入れ替えなど、欠点と思われていた部分がなくなり、新しくなって戻ってきました。比較対象とされるReactとは異なり、テンプレートコードを別ファイルに分離して管理することができ、非開発者とのコラボレーションもしやすくなっています。
また、Angular 2はフロントエンドのフレームワークであるため、Reactよりも自由度が比較的低いですが、熟練の開発者でなくても全体的な設計負担を軽減してくれます。さらに、ユーザーに基本的なパフォーマンスの最適化を保証するため、パフォーマンスについて深く心配する必要がありません。欠点としては、Angular 2独自のコードサイズが大きいという問題が残っていますが、実際に使用するコードだけ抜いてバンドルすることで解消できます。
Reference
- 公式ホームページ- https://angular.io
- ng-conf 2016 – https://www.youtube.com/playlist?list=PLOETEcp3DkCq788xapkP_OU-78jhTf68j
- ANGULAR 2 CHANGE DETECTION EXPLAINED – http://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
- Three Ways to Test Angular 2 Components – https://vsavkin.com/three-ways-to-test-angular-2-components-dcea8e90bd8d#.h75hdex2u