2016年3月2日
Angular 2で作った最初のアプリの事後分析 : 使ってわかったAngular 2の長所と短所
(2016-01-19)by Mike Ryan
本記事は、原著者の許諾のもとに翻訳・掲載しております。
最近、Angular 2を使ってWebアプリ作成の手伝いをする機会がありました。このフレームワークの複雑さに戸惑いもありましたが、Angular 2は間違いなくすばらしいものです。この事後分析では私がAngular 2のフレームワークを使った時に感じたいい面と悪い面の両方を掘り下げていくことにします。
Angular 2
Angularは携帯電話やデスクトップのWebアプリケーションを作成するための開発プラットホーム。
https://angular.io/
Angular 2+リアクティブプログラミング=❤
私はすでに、Angular 1アプリケーション ではRxJSをかなり使っていました。特に、サービス間のメッセージ・バスを行うアプリを作成する時です。Angular 2はObservableを非常によくサポートしているので、アプリ全体にリアクティブプログラミングを取り入れることができ、とても興奮しました。
ReactiveX (リアクティブエクステンション)/ RxJS
RxJSは、JavaScriptのリアクティブプログラミングのライブラリである。
github.com
私は、 すごく人気のあるReduxライブラリ で動く Fluxアーキテクチャ を活用していました。Angular 2のインジェクタとTypeScriptの型システムをきれいに統合させるために、私はRxJS のObservableを使ってReduxをラップするクラスベースのラッパーを書き、状態の変更の通知を受け取れるようにしました。コンシューマは、それらのObservableを購読し、状態の変更のアクションをディスパッチするストアクラスを使うことができます。André Staltz氏はリアクティブプラグラミングに関する すばらしいガイドライン を書いています。さらに、Angular 2やRxJSと一緒にReduxを使う方法を学びたい人には、以下のサイトを参照することをお勧めします。
Angular 2- Redux入門
Angular 2アプリケーションでReduxをどのように使うか
medium.comngrx/store
ストア- Reduxに影響を受けた、RxJSを使ったAngular 2アプリのための状態管理機能。
github.com
リアクティブプログラミングの良い所は、XmlHttpRequest をラップするObservableベースのラッパーとして機能する、パワフルな新しいHTTPサービスです。さらに、この新しいルータは、Reduxストアと同期したルート状態を保持するために私が活用したObservableとして、ルートの変更を公開してくれます。全てがつなげたら、 タイムトラベルするデバッガ を活用することができるReduxの開発ツールをラップする薄いAngular 2ラッパーが取得できました。これは、Reactの開発者がしばらくの間持っていた特権です。
Angular 2を使ったリアクティブプログラミングの欠点は、フレームワークが、イベントを操作したりインプットの変更をリッスンするのに、従来のコールバックを使用していることです。イベントリスナーをテンプレートで設定したり、subjectにプッシュするのにコールバックを使ったりするのを、自分で設定するのは難しくありませんし、むしろインプット変更と同じようなことをするAngular 2のOnChangeのライフサイクルフックを使うのが妥当でしょう。それでも、Angular 2が、 これらのパターンをサポート していたら、リアクティブプログラミングのフレームワークがより自然になっていたでしょう。
非同期パイプはすばらしい
RxJS+Reduxを選択する一番の利点は、状態の変更をAngular 2の変更検知のサイクルと結びつけるのがとても容易であることにあります。これは非同期パイプのおかげです。Angular 2パイプは、データ変換を扱う時のテンプレートの式でよく使われます。Angular 2パイプはAngular 1からのフィルタや、昔ながらのUnixパイプと似ています。非同期パイプはObservableや、それに渡されるPromiseを購読することができ、直接テンプレートで値を出すことができます。
非同期パイプ
非同期パイプはObservableやプロミスを購読することができ、最新の値を戻し、検出することができる。新しい値が出されると、非同期パイプは変更が確認されたコンポーネントをマークする。
よくあるTodoアプリの例で見てみましょう。以下は、ngrx/storeと非同期パイプを使ったコードのサンプルです。
パイプを非同期に呼び出すのは、間違えやすいので気を付けましょう。なぜなら、ngrx/storeが状態を BehaviorSubject を使って吐き出すためです。つまり、非同期パイプは最新の値を直ちに受け取り、Todoリストで作成される今後の全ての変更を非同期に通知されます。すばらしい点もありますが、最善ではありません。Angular 2はコンポーネントの中で無駄な変化検出をし続けることになってしまうからです。
Reactのベストプラクティスから、私はコンポーネントを2つのカテゴリーに分類しました。1つ目はRedux storeに接続する役割を担うラッパーコンポーネント。2つ目はインプットプロパティで状態を受け取り、イベントでアウトプットを吐き出す純粋なコンポーネントです。これにより、Redux Storeが変更をディスパッチした時に発生する変更を、コンポーネントが検知するという戦略を修正することが、Angular 2でできるようになりました。以下は、上記の例をリファクタしたものです。
Todoリストの 変化検出戦略 をOnPushに設定することで、もしTodoリストのインプットが変更されたらAngular 2に変化検出をさせます。インプットは非同期パイプを使っているRedux Storeと結びついているので、Redux StoreがアップデートされるとTodoリストも一緒にアップデートされます。
これだけでも、成功と言えるでしょう。Angular 2を使えば、少しのコードで、明確で効率の良い、リアクティブなUIが作成できるのです。
依存性注入(DI)は非常によくなっている
Angular 1の欠点の1つは、文字列ベースの依存性注入です。名前衝突を避けるために厳しい規約が課せされたり、コードの圧縮化の安全を守る特別なツールを必要になったりします。しかし、Angular 2では、依存性注入はこれらの面倒な欠点を克服でき、かなり改善されました。
Angular 1開発者がまず気付くであろう一番大きな違いは、DIが文字列の代わりにトークンを使用している点でしょう。プロバイダトークンに文字列を使うこともできますが、以下のやり方で型をバインドする方が良いでしょう。
TypeScriptコンパイラのオプションでemitDecoratorMetadataを指定すると、クラスコンストラクタ上での型付けがクラスのメタデータに吐き出されます。Angular 2ではこの型情報が使用され、依存性注入に自動で接続されます。実際のリフレクションAPIと言語のための典型的な性能です。
また、この新しいインジェクタは階層的です。Angular 2は、コンパイルするそれぞれのコンポーネントごとに新しい子インジェクタを生成し、結果としてコンポーネントの階層とマッチした注入階層ができあがります。これにより、グローバル衝突を避け、開発者は上手にカプセル化された、設定の一部として必須のプロバイダを宣言するコンポーネントを書くことができるようになるのです。
このインジェクタの一番の欠点は、インジェクタそのものではなく、コンパイルしている間にTypeScriptによって吐き出される型情報があまりに少ないことにあります。TypeScriptのインターフェースやジェネリック型に対応するJavaScriptが欠けています。つまり、Angular 2のインジェクタが使用可能な実行時の型情報がないということです。
以下のTypeScriptのコードを見てみましょう。
TypeScriptの試験的なReflect APIを使って、MyServiceに吐き出されるパラメータ型メタデータは以下のようになると思っていました。
[[ ILights ], [ Camera<string> ], [ Action[] ]
しかし、実際には以下のようになりました。
[[ Object ], [ Camera ], [ Array ]]
TypeScriptコンパイラは私のILightsインターフェースを一般的なオブジェクト型に変換して、Cameraクラスのジェネリック型に必要な正確さを損ない、Action []配列の特徴をなくしてただの配列にしてしまいました。このように、型の通りの正確さを保てないとなると、Angular2のプロバイダ間で実際に使える型の種類が制限されます。
この型システムにおけるもうひとつの重大な欠点は、共通ライブラリの型定義に、本当はインターフェースの宣言をするつもりなのに誤って型の宣言をしているものが時々あることです。例えば、ReduxにRxJSでラッパーを構築する際、私はファクトリを使ってReduxのストアクラスを提供しようとしました。
一見すると有効そうですが、Reducerは実のところただのインターフェースで、JavaScriptと同じようには使えません。幸いTypeScriptはよくできているので、インターフェースが使用された時は検知して下図のように知らせてくれます。
私はReducerを提供するために文字列トークンを使ってコードのリファクタリングを行いました。
上記のコードは問題なくコンパイルできますが動作しません。その理由は、Reduxの共通の型定義をが、ストアをエクスポートされたクラスとして宣言しているからですが、実際にはReduxにそのようなエクスポートはありません。そのせいで実行時にストアが未定義という結果になるのです。Reducerとストアの両方を提供するために、私は型の代わりに文字列トークンを使ってもう一度コードのリファクタリングをしなければなりませんでした。
この問題をデバッグすることは難しく、私のいらいらはさらに募りました。型定義のファイルが実際のライブラリと一致しないことをTypeScriptが検知できる方法などなかったからです。ストアクラスのプロバイダを登録しようとすると、Angular 2はふざけているのか、意味不明なエラー、「トークンを定義してください! 」と「型エラー: インジェクタは定義されていません」を投げてきました。私は結局、Reduxの名前付きエクスポートと型定義ファイルを比較して問題を特定しました。
これは素晴らしい経験だったとは言えませんし、他の開発者が共通の型定義ファイルを使用してインジェクタに好きなライブラリを関連付けようとしたら、何らかのトラブルに見舞われるだろうと予想がつきます。また、TypeScriptが 忠実性のより高いランタイム型 を将来的に提供する見込みはなさそうで、Angular 2の依存性注入は当面の間、多少不自由なままにしておかれる可能性があります。
新しいルータは有望
Angular 2にはComponent Routerと呼ばれる新しいルータが同梱されています。それはreact-routerとEmberのRouterを足して2で割ったようなものです。私のプロジェクトのルーティング要件が少なかったとはいえ、大まかにはComponent Routerは“十分使える”とわかりました。
もっと大きいプロジェクトで使うとすれば、新しいルータには重要な機能がいくつか欠けています。例えばルーティングのエラーを処理したり、ルートが見つからない時に簡単に404ページへリダイレクトしたり、ルートに入る前にデータを解決したりする機能は必要でしょう。私はすべてのルート設定にワイルドカードを付けたルートを追加し、これらのワイルドカード付きのルートが404 のコンポーネントを指すようにすることで404エラーを処理できました。また、ルータの @CanActivateデコレータを悪用することでルートに入る前に非同期要求を実行し、データの解決を一時的に行うことができました。
これらの問題に、ある解決策を用いて成功した開発者を何度か見ました。その解決策とは、ルータのRouterOutletコンポーネントをサブクラス化し、新機能によるルータのライフサイクルフックを手動で強化することです。これは推奨される解決策という扱いで終わるという可能性はあるものの、継承より構築を好むフレームワークだと主張しているのに、フレームワークのコンポーネントをサブクラス化するしか手がないというのは、よく練りあげられた対策とはとても思えません。
フレームワークのリリースが近づくにつれて、より多くのルーティングの選択肢が提示されることを私は願っています。Angular 2のための ui-router に関しては初期の簡易な試行がすでに行われていて、わくわくします。
ES5、ES2015、それともTypeScript?
Angular 2向けのES5のシンタックスは使用に耐えないとわかりましたし、ES2015に関する標準的なドキュメントは存在しません。手間をかけずにTypeScriptを(お好みならDartを)使いましょう。そうお勧めする最大の理由は、Angular 2で頻繁に使われているデコレータの中の、パラメータに使用するデコレータについては、ES2017のデコレータ企画案に現在のところ入っていないからです。
メタデータを関連付けることは自分でできましたが、ドキュメントがなく時間も長くかかる苦行となりました。
ES5で記述してみるともっとめちゃくちゃになるとわかりました。
Angular 2チームは ES5とTypeScriptとDartのドキュメント作成に優先順位をつけようとしており 、ES2015/ES2016を使用したい開発者に対しては「TypeScriptの糖衣構文を書き下す方法を習得せよ」と要請しています。TypeScriptを優先するという決断は確かに理解できます(ほとんどのフレームワークは1つの言語についてさえ適切なドキュメントを備えていません)が、これで事実上、Angular 2のアプリケーションをTypeScript以外の言語で書くのは不適切ということになりました。
Angular 2は重い
私がAngular 2は重いと言うのには、2つの意味があります。1つは複雑さの点で重いということで、もう1つはAngular 2のペイロードのサイズが大きいということです。
Angular 1でよくある不満は、習得が難しいことと新しい用語の幅が広すぎることでした。私は正直Angular 2で、これらの課題が大いに解消されたとは思いませんし、多くの場合、より複雑になったと思います。Angular 2の利点は、Angular 1とは異なり、たくさんの最良の事例が、前のバージョンよりもはるかに安全に動作するフレームワークに組み込まれていることです。
より複雑だが、より安全だという良い例は、議論の的になっているAngular 2の新しいテンプレート言語です。私はその品質について断言するつもりはありませんが、新しい言語はAngular 1と比較してより複雑になった一方、新しいテンプレート言語はもっと予測可能なコードになると言えるでしょう。例えば、 ngIfと ngForを同じ要素に適用することができないということは、Angular 1に存在していたディレクティブの優先度を処理する必要がなくなります。それはまた、期待される結果についてもっと明示的に書くことを開発者に強要することでもあります。
見た目は悪いですが明示的なので、私は、Angular 1のテンプレート言語よりこちらのほうが好きです。
新しいテンプレート言語では、実際にコンポーネントのテンプレートを読み、さまざまな属性のバインディングの違いを見分けることができました。Angular 1のテンプレートとそのすばらしい属性のバインディングにとっては、この点は特に泣き所だったものです。新しいテンプレート言語には多少の慣れが必要でしたが、このテンプレート言語のほうが、Angular 1よりも生産性が高いです。
実際のサイズの点では、前に開発した30,000行を超えるAngular 1のアプリよりも、コード分割した小さなAngular 2のアプリのほうが、最初のページロードがかなり重いです。今年の末までに単純なAngular 2のアプリを10キロバイトまで小さくするという大胆な計画がささやかれていますが、今後数カ月でどれだけ改善されるかとても気になります。短期的にはAngular 2の開発チームは、テンプレートをコンパイルし、実行時にこの改善を行うのではなくビルドステップの一部としてインジェクタのプロバイダを解決するためのツールを提供していくでしょう。これによって、エンドユーザに送信するために必要なコードが減り、アプリケーションの実行速度は速くなるはずです。
Angular 2は強力で複雑
私は以前Angular 1を採用したであろうプロジェクトにAngular 2を採用するでしょうか? 絶対採用します! 私は、最新のAngular 2を使った経験ではAngular 1よりはるかに優ると圧倒的に確信しています。しかも、素晴らしいHTTPクライアント、強固なテストストーリー、簡単にできるフォームのような、前バージョンの決定的な機能は依然として存在したままです。これらの機能は、Angularになくてはならない機能として常に際立っていましたので、Angular 2でもそのまま重要な機能として残ったのはうれしいことです。
Angular 2は初期のベータ版でありながら、完成したアプリはかなり安定しています。私が遭遇した荒削りな部分の多くは、わかりにくいエラーメッセージ、不完全なドキュメント、及びツールの不可解なエッジケースに起因していました。これらは、フレームワークが成長し、成熟していくにつれて、時間をかけて徐々に改善されるでしょう。
Angular 2が複雑で習得するのが難しいことは、いまだに否定できません。開発していく中で、「落とし穴」がいくつもありました。しかし、このフレームワークを完全に却下するのが当然だと言うほどひどくはありません。さらに、私が対処してきた欠点の多くは、すでに積極的に改善されています。
Angular 2を試してみたいですか? 今のところは、注意しながらも楽観的に試してみましょう。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa