こんにちは、編集長の白石です。ぼくは、自社で運営と開発をしているTechFeedというエンジニア向けニュースアプリにAngular(とIonic)を採用していることもあり、Angularの行方には強く関心を抱いています。が、一方で、日々の忙しさにかまけて中々アップデートを追えていなかったりもします。
しかし、先月(2017年3月)にAngular 4.0がリリースされ、ng-conf(アメリカで開催されたAngularのカンファレンス)も終わった今は、Angularの最新情報を得るのに最適な時期!
そこで、ng-japanの代表であり、ng-confにも参加されていたlacoさんに、Angularの最新トピックとテクニックについてお聞きしてきました。初心者向けの記事ではありませんが、大変濃くて面白い記事になりました!Angular使いの皆さま、どうぞお楽しみください。
しかしあまりにロングなインタビューだったので、さすがに記事分けました。今回の記事は前編です。
⇒ 後編はこちら。
先日リリースされたばかりのAngular 4.0について教えてください!
白石 Angular 4.0が最近リリースされましたね。個々の技術的なトピックにはあとで突っ込んで聞かせていただくとして、まずは大まかに何が変わったのかを教えてください。
laco Angular 4.0で最も目玉の変更は、AOT(※)ビルドのサイズが大幅に減ったことですね。生成されるコードのサイズが60%に減ったケースもあるようです。
※AOT…Ahead of Time Compileのことで、Angularのテンプレートを事前にJavaScriptコードにコンパイルしておくこと。本番環境では必須。対して、実行時にテンプレートをコンパイルするモードのことをJIT (Just in Time)コンパイルと呼ぶ。
白石 それはすごいですね!
laco Angular 4.0のテーマは、「コードを小さく速くする」ことだったんです。なので、Angular 2.xで作ったコードをAngular 4.0でビルドするだけで、サイズがコンパクトになってスピードもアップする…ということを目標にしていて、それを達成したのが一番の成果ですね。
白石 コードサイズが小さくなれば、起動速度の短縮も期待できますね。「コードを小さく速く」が目標だったということは、今回のアップデートでは大きな機能追加はなかったんでしょうか。
laco 全くなかったというわけではありません。
4で入った一番大きな機能はサーバーサイド・レンダリング(SSR)ですね。これは、以前Angular Universalと呼ばれていたもので、Angularのコアチームとは別にコミュニティベースで開発されていた機能です。それをコアチームが引き取り、様々な変更を加えて今回のリリースに至りました。
白石 SSRは、Angularをサーバーサイドで実行して、その結果をクライアントにHTMLソースとして返す機能ですよね。SEOフレンドリーな機能として、Reactなどでもよく話題に上がる機能です。SSRについては、あとでじっくり聞かせてください(後編に掲載)。
今回は後方互換性を損なうような変更もありませんでしたか?
laco はい、ほとんどありませんでした。
Angularでは、削除予定の機能はまず非推奨になって、半年後のメジャー・アップグレードで削除されるというポリシーなんです。 だから後方互換性については、アップグレードに追随していればほとんど問題になることはないでしょう。
なので、後方互換性は保たれていますが、今回非推奨になった機能はいくつかあります。
例えば、Dependency Injection時に使用するOpaqueToken
がInjectionToken
になりました。この二つは、役目は全く変わりませんが、InjectionToken
はジェネリクスにより型パラメータを与えられます。そのおかげで、DIコンテナからオブジェクトをプログラマブルに取り出す(Injector.get()
)際、型キャストが不要になりました。
ほかには、template
タグがng-template
に代替されました。以前はtemplate
要素はAngularによって特別扱いされていたのですが、HTMLで一般的な要素が特殊な扱いを受けるのは問題があるということで、専用のng-template
が用意されました。
Angular CLIは使うべき
白石 lacoさんは、先日ng-conf(アメリカで開催されたAngularのカンファレンス)にも出席してらっしゃいましたよね。イベントの雰囲気はどんな感じでしたか?
laco Angular 4.0のリリースと、Angular CLI 1.0がリリースされた直後だったので、それをお祝いする雰囲気が強かったですね。
白石 お、Angular CLIについて全然知らないので教えてください。ぼくらが作っているTechFeedというサービスは、Angular CLIが使えるようになる前からAngularを採用していたので、Angular CLIを使っていないんです。
自分たちで作ったビルドスクリプトを利用している。そんな状態なのですが、Angular CLIに移行する価値はあると思いますか?
laco はい、あると思いますね。今後、ビルド速度をもっと速くしていこうという計画があるんですが、そうした成果もCLIに搭載されようとしている。CLIはどんどん進化していきますので、その進化に置いていかれないためにも、CLIの採用は考えたほうがいいと思います。
AOT by default
白石 ng-confでは、4の先のビジョン的なものも示されたのではないかと思うのですが、いかがですか?
laco とりあえず次期バージョンである5.0のテーマは「単純化」ですね。様々な機能を付け加える中で複雑化してきたAngularを一度シンプルにする。
その一つが、AOTコンパイルをデフォルトにしようとする動きです。
今のAOTはngcというコマンドによって実行されるわけですが、差分コンパイルができないんです。すべてを1からビルドし直すしかない。なので、差分コンパイルを可能にして、コードの変更をウォッチしてインクリメンタルなビルドを行うようにするのが目標です。
そして、普段からAOTを使って開発しようと。AOT by Defaultと呼ばれています。 そうすれば、「JITでは動くけどAOTでは動かない」などのバグも未然に防ぐ事ができますし、JITとAOTのコンパイラを一本化することもできる。
白石 なるほど。AOTのコンパイル速度を上げるというわけじゃなくて、普段からAOTを使って開発しようということですね。
ABCプロジェクトとは?
laco ちなみに、ABCというプロジェクトもあります。 ABCというのは、Angular with Bazel and Closureの略で、Googleが社内で使っていたものを公開したBazelというビルドツールと、昔からあるClosure Compilerを組み合わせようという計画です。
白石 そのプロジェクトからは、開発者はどんな恩恵を受けられるんでしょう?
laco Bazelというのは、Googleが作った言語に依存しないビルドツールで、速さと正確性を売りにしています。 Closure Compilerは昔からあるJavaScriptのミニファイアですが、その最適化度合いには定評があります。
これら二つのツールを使って、Angularのビルドを高速化し、さらに生成されるコードも小さくしようというのが、ABCプロジェクトの目標です。ただ、その成果が利用できるのは少し先の話です。来年のng-confで続報が聞けるかな、というくらい。
Web ComponentsとAngular
白石 5以降のビジョンは示されましたか?例えば、Web Componentsとの関係性とかはいかがでしょう。将来的なビジョンとか示されていたなら教えてください。
laco そう言われれば、Web Componentsの話はほとんどありませんでしたね。「Web標準と仲良くする」っていう件は、以前はホットなトピックだったのですが、もう決着がついていると言ってもいいんじゃないかと思います。
白石 Web Componentsと言えば、Angularってコンポーネントが生成するDOMをShadow DOM(※)にするというオプションがあったりしますよね。 そのオプションをデフォルトにするという話とかはあるんですか?
laco なかったですね。現在のShadow DOMの実装も、Shadow DOM v0を前提にしているので、そもそもChromeでしか動かないんです。 Shadow DOMモードを使うと、(属性を使った)エミュレーションを行う必要が無い(※)ので多少パフォーマンスがアップするのですが、とりあえず今のところはそちらをデフォルトにするという予定はないみたいですね。
※Shadow DOM…DOMをカプセル化し、外部の影響を受けにくくする仕組み。Web Components仕様の一部として標準化と実装が進んでいる
※(属性を使った)エミュレーション…Angularは、自動的にDOMに特殊な属性を付与して、その属性をCSSセレクタに追加するなどの処理を行い、Shadow DOMに近いカプセル化を実現している
コンポーネント設計、どうするのがベスト?
白石 ではここから、Angularの個々の機能やトピックについて、いろいろと深く伺っていきます。最初に取り上げたいトピックとしては、Angularのコンポーネントをどう設計するかについて。ng-confなどでもそういうトピックが取り上げられてたんじゃないかと思うのですが、Angularのコンポーネント設計で留意しておくべきこととかありますか?
laco まず、今主流のフレームワークやライブラリは、コンポーネントの実現方法として二種類に分けられます。
一つはカスタム要素型です。Angular、そしてWeb Comonentsがそうですが、新たな独自の要素に囲まれた形でDOMがレンダリングされるものです。もう一つはReactやVue.jsのように、カスタム要素はDOM内には出現せず、フラットなDOMにレンダリングされるものです。こちらはレンダラー型と呼ぶことにしましょう。
例えば<my-button>という要素を作ったとしたら、Angularは実際に<my-button>という要素をDOMに含めます。
1 2 3 |
<my-button> <!-- ここにコンポーネントのDOMがレンダリングされる --> </my-button> |
一方、ReactでMyButtonというコンポーネントを作ったとしても、その要素はDOMには表れません。
この違いが設計に影響を与えることがあるのは意識しておいたほうがいいと思います。特に、両方のフレームワークを使う必要がある人は要注意ですね。
白石 なるほど。具体的にはどのような設計上の違いになって表れてくるんでしょう。
laco カスタム要素が実際にDOMに表れ、コンポーネントを囲んでいるということは、CSSの影響範囲などにも違いが出てくるということです。例えば、生成されたカスタム要素はデフォルトではブロック要素になっていないので、:host
セレクタを使って要素自体に display: block;
を指定しなければならないこともあります。正直、これはかなり面倒です。
また、ユーザーエージェントはカスタム要素のセマンティクスを解釈できないので、アクセシビリティなどにも悪影響を及ぼす可能性があります。
なので、独自要素が現れないように、既知のHTML要素をコンポーネント化する手法も広まりつつあります。
@Component()
(Angularのコンポーネントに付与するデコレータ)に指定するselector
って、要素に限らず任意のCSSセレクタを指定できるので、そういうことが可能なんです(※)。
※例えばselector: '[my-button]'
のようにすれば、my-button
という属性を付与された要素がコンポーネントとして扱われる。
そのようにコンポーネントを設計しておくと、カスタム要素を使わずに、UAにとって既知の要素のみでDOMが組み立てられることになる。そうすればセマンティクスやアクセシビリティの問題も生じにくいですし、カスタム要素をブロック要素として指定する手間もなくなります。
さらに言えば、コンポーネントの外側を囲むカスタム要素がなくなれば、テンプレートも、そこから生成されるDOMもシンプルになります。テンプレートがシンプルになるということは、AOTビルドによって生成されるコードもサイズが小さくなります。
白石 なるほど、独自タグを使わないという手法が、今後存在感を増してくるかもしれないということですね。
プレゼンテーショナル・コンポーネントとコンテナー・コンポーネント
laco あと、コンポーネントの役割についても、プレゼンテーショナルなコンポーネントと、コンテナー・コンポーネントの二種類に分けて考えたほうがいいです。これはReactで研究されてきた設計手法で、Reactとの数少ない(笑)共通の話題ですね。
プレゼンテーショナル・コンポーネントは、シンプルに表示のみを担当します。複雑な動作はあまり行わず、属性(@Input()
)で与えられるデータのみを利用して、描画を行います。
コンテナー・コンポーネントは、自身はほとんど描画を行わず、ユーザーのイベントに応答したりAPI呼び出しを行ったりと、リッチでインテリジェントに振る舞います。ng-confではDecision Maker (意思決定者)と呼んでる人もいました。
白石 そういうふうに分けて考えることで、どんな利点があるんでしょうか?
laco コンポーネント指向の利点は再利用性ですが、実際にはすべてのコンポーネントが再利用可能なわけではありません。先ほどの二分法で考えると、再利用可能なのはプレゼンテーショナル・コンポーネントの方です。単純であればあるほど、再利用しやすい。
しかしそうすることによるトレードオフもあります。例えば、プレゼンテーショナル・コンポーネント内でイベントが発生したとして、そのイベントを処理するのがコンテナー・コンポーネントだとしたら、プレゼンテーショナル・コンポーネントで発生したイベントを、コンテナーにたどり着くまで外側に向かって伝播していくという処理を書かなくてはならない。イベントの「バケツリレー」が発生するわけです。
それを全部書いていくのは結構大変。
白石 ぼくも経験ありますが、確かに大変ですよね…どうやってそれを回避するのが一般的なんでしょうか?
laco サービス(※)を経由するのが普通ですね。サービスのプロパティとして用意したObservable
(※)型の変数を通じて、様々なイベントをやり取りするんです。
白石 コンポーネントがサービスのインスタンスをAngularにDIしてもらって、そのサービスが持つObservable
なプロパティを監視するんですね。
laco そうです。ですが、サービスは基本的にシングルトンなので、それより短寿命なコンポーネントがsubscribe()
するのであれば、コンポーネントが終了する時にunsubscribe()
しなくてはなりません。忘れるとメモリリークの原因になってしまいます。
そこで、Angularが標準で提供しているAsync Pipeを使って、テンプレート内でsubscribe()
すると、コンポーネントのインスタンスが破棄される時に自動的にunsubscribe()
してくれます。
unsubscribe()
を忘れる心配がないので、使える場合は使ったほうがいいと思います。
さらにAngular 4.0からは、ngIfにas構文が追加されて、ngIfの評価結果を変数として利用できるようになったおかげで、ngIfとasyncを組み合わせるのも簡単になりました。
1 2 3 4 5 6 |
<div *ngIf="userList | async as users; else loading"> <user-profile *ngFor="let user of users; count as count; index as i" [user]="user"> User {{i}} of {{count}} </user-profile> </div> <ng-template #loading>Loading...</ng-template> |
白石 ちょっとした改善ですが、気が利いてますね。
laco Angularをよりリアクティブにする「Reactive Angular」というプロジェクトが、Angularチーム内にあるんです。そこの成果物を取り込んだんですね。
※サービス…Angularによって生成され、コンポーネントにDIされるシングルトンのインスタンス ※Observable…Angularが依存しているRxJSというライブラリで、最も基本となるクラス。非同期の(リアクティブな)データストリームを提供する。
後編に続きます。