余白

https://blog.lacolaco.net/ に移転しました

FlutterのBLoCパターンをAngularで理解する

この記事ではAngularDart/Flutterの文脈で新しいコンポーネント設計パターンとして広まりつつあるBLoCパターンを、Angularの語彙で理解し、実装する方法を紹介する。

BLoCパターンとは

BLoCとは、Business Logic Componentの略である。 BLoCを使ったアプリケーションの実装パターンをBLoCパターンと呼ぶ。

まず誤解を招きそうなポイントとして、この"Component"はReactやAngularなどでいうところのビューを構築する"コンポーネント"ではない。 一般的な単語としての、アプリケーションを構成するひとかたまりの要素という意味の"Component"なので誤解しないこと。 対比するレベルとしては、"UI Component" vs "Business Logic Component"のようになる。

BLoCは複数の環境向けにアプリケーションを開発するときのコードシェアカバレッジを高めるための、リファクタリング指針のようなものだ。 具体的には、以下の指針を与える。

  1. BLoCの入力・出力インターフェースはすべてStream/Sinkである
  2. BLoCの依存は必ず注入可能で、環境に依存しない
  3. BLoC内に環境ごとの条件分岐は持たない
  4. 以上のルールに従う限り実装は自由である

詳しくはBLoCパターンの初出であるこのセッションを見るとよい。

Flutter / AngularDart – Code sharing, better together (DartConf 2018) - YouTube

AngularにおけるBLoCパターン

AngularにおいてBLoCパターンの恩恵がどれほどあるのかは議論の余地があるが、 持続可能なAngularアプリケーション開発のために大事なこと - lacolaco でも述べたようにフレームワークに依存しない部分を明確に分ける、というのは設計指針として重要である。 サーバーサイドでの実行やNativeScript、Ionic、あるいはReact/Vueなどへの換装など考えても、BLoCパターンはアプリケーションのAngular依存度を適切に保つために良いルールに思える。

さて、さっそくAngularでBLoCを実装してみよう。 Dartには言語標準のStreamとSinkがあるが、JavaScriptにはまだ存在しないため、非標準の実装が必要である。 幸運にもAngularはRxJSと相互運用可能なので、RxJSのObservableをStreamに見立ててBLoCを実装することができる。

まずはUIコンポーネントビジネスロジックを持ってしまった状態の例を以下に挙げる。

https://stackblitz.com/edit/angular-bloc-example-1?file=src%2Fapp%2Fapp.component.ts

@Component({
  selector: 'my-app',
  template: `
  <div cdkTrapFocus [cdkTrapFocusAutoCapture]="false">
    <mat-form-field appearance="outline" style="width: 80%;">
      <input 
        matInput
        placeholder="Search for..." 
        ngModel (ngModelChange)="onInputChange($event)">
    </mat-form-field>
  </div>

  <span> {{preamble}} </span>

  <ul>
    <li *ngFor="let result of results">
      {{ result }}
    </li>
  </ul>
  `,
})
export class AppComponent {
  private query = '';
  results: string[] = [];

  get preamble() {
    return this.query == null || this.query.length == 0 ? '' : `Results for ${this.query}`;
  }

  constructor(private repository: SearchRepository) {}

  onInputChange(query: string) {
    this.query = query;
    this.executeSearch(query);
  }

  private async executeSearch(query: string) {
    const results = await this.repository.search(query);
    this.results = results;
  }
}

UIコンポーネントAPIの呼び出しや状態の保持などさまざまなビジネスロジックを持っているので、もしこのアプリケーションを別プラットフォームにも展開したくなってもコードが共有できない。

BLoCの作成

BLoCはポータビリティを考えると、ほとんどの場合は単なるクラスとして宣言される。 ここではSearchBlocクラスを作成する。 もともとAppComponentが持っていたビジネスロジックをすべてSearchBlocに移動すると次のようになる。

class SearchBloc {
  private query = '';
  results: string[] = [];


  get preamble() {
    return this.query == null || this.query.length == 0 ? '' : `Results for ${this.query}`;
  }

  constructor(private repository: SearchRepository) {
  }

  async executeSearch(query: string) {
    this.query = query;
    const results = await this.repository.search(query);
    this.results = results;
  }
}

そしてAppComponentSearchBlocに依存して次のようになる。

@Component({
  selector: 'my-app',
  template: `
  <div cdkTrapFocus [cdkTrapFocusAutoCapture]="false">
    <mat-form-field appearance="outline" style="width: 80%;">
      <input 
        matInput
        placeholder="Search for..." 
        ngModel (ngModelChange)="bloc.executeSearch($event)">
    </mat-form-field>
  </div>

  <span> {{ bloc.preamble }} </span>

  <ul>
    <li *ngFor="let result of bloc.results">
      {{ result }}
    </li>
  </ul>
  `,
})
export class AppComponent {
  bloc: SearchBloc;

  constructor(private repository: SearchRepository) {
    this.bloc = new SearchBloc(this.repository);
  }
}

https://stackblitz.com/edit/angular-bloc-example-2?file=src/app/app.component.ts

Observableへのリファクタリング

先述のとおり、BLoCパターンではBLoCのすべてのインターフェースはStreamでなければならない。 これはFlutterのStatefulWidgetやAngularDartのChange Detectionの間で、データの変更に対するUIのリアクションのアプローチが違うからだ。 同期的な状態の管理ではプラットフォームごとに特別な処理が必要になる。

一方StreamであればFlutterはStreamBuilderでStreamからデータが流れてくるたびに再描画する仕組みをもっており、AngularDartもasyncパイプにより同様の反応機構をもっている。 プラットフォームに依存せず非同期的な値を描画するために、DartBLoCパターンではStreamを活用する。

Angularの場合はRxJSがBLoCの実装を助けてくれる。

DartのStreamをObservable、SinkをObserverに置き換えると、SearchBlocは次のようになる。

class SearchBloc {
  private _results$: Observable<string[]>
  get results$(): Observable<string[]> {
    return this._results$;
  }

  private _preamble$: Observable<string>
  get preamble$(): Observable<string> {
    return this._preamble$;
  }

  private _query$ = new BehaviorSubject<string>('');
  get query(): Observer<string> {
    return this._query$;
  }

  constructor(private repository: SearchRepository) {
    this._results$ = this._query$
      .pipe(
      switchMap(query => observableFrom(this.repository.search(query)))
      );
    this._preamble$ = this.results$.pipe(
      withLatestFrom(this._query$, (_, q) => q ? `Results for ${q}` : '')
    );
  }

  dispose() {
    this._query$.complete();
  }
}

results: string[]results$: Observable<string[]>になり、preamble: stringpreamble$: Observable<string>となった。 これらはqueryの変更に反応して変化する非同期的な値として表現される。

queryObserver<string>インターフェースを外部に公開し、新しい値の追加をUIに許可する。 SearchBlocの内部では_query$: BehaviorSubject<string>を実体として持ち、コンストラクタでは_query$に反応する_results$_preamble$が宣言されている。

これをAppComponentから使うと次のようになる。テンプレート中でasyncパイプを使い、Observableの変更に反応してビューの再描画が実行されるようになる。

@Component({
  selector: 'my-app',
  template: `
  <div cdkTrapFocus [cdkTrapFocusAutoCapture]="false">
    <mat-form-field appearance="outline" style="width: 80%;">
      <input 
        matInput
        placeholder="Search for..." 
        ngModel (ngModelChange)="bloc.query.next($event)">
    </mat-form-field>
  </div>

  <span> {{ bloc.preamble$ | async }} </span>

  <ul>
    <li *ngFor="let result of bloc.results$ | async">
      {{ result }}
    </li>
  </ul>
  `,
})
export class AppComponent {
  bloc: SearchBloc;

  constructor(private repository: SearchRepository) {
    this.bloc = new SearchBloc(this.repository);
  }

  ngOnDestroy() {
    this.bloc.dispose();
  }
}

https://stackblitz.com/edit/angular-bloc-example-3?file=src/app/app.component.ts

これでBLoCの実装が完了した。

考察

まとめ

皮肉屋とシステム

ここ数年で一番好きな映画は「マネーショート」(原題 "The Big Short") だ。

映画館でも2回観たし、Netflixにあったから頻繁に見てたんだけどなくなってしまった。今はGoogle Playで買って見ている。

役者もストーリーも演出も全部好きなんだけど、中でもひとつ心に残っていつも頭の片隅から離れないセリフがある。

元々バカなシステムなんだ (中略) 自分を見ろ 皮肉屋を気取ってもどこかでシステムを信じてる

ローンのデフォルトが増えているのに債権の価格が高騰するという、合理的に考えればありえないことが起きていた。 格付け機関も債権のランクを下げることなく、詐欺的なシステムが稼働していた。

作中のマーク・バウムはなぜ合理的に考えればありえない詐欺がまかり通っているのかと、ドイツ銀行のジャレド・ベネットを問い詰めた。その時にジャレドが返したセリフ。

ウォール街のシステムの崩壊を予期し、破綻に賭けて空売りを仕掛けた悲観主義の皮肉屋の彼らは、それでも格付け機関や銀行が正しく機能して正しく債権が暴落し、空売りが成功すると信じていたわけだ。 システムを疑いながらもどこかでシステムを信じている、この二律背反は気づかないうちに自分にも当てはまっていることが多い。


www3.nhk.or.jp

2週間、宿泊費自己負担で8万人のボランティアを集めるためにやりがいをPR、どうかしている。 システムが狂っている。まともに考えたらこんなのでうまくいくわけがない。

最初はこう思うわけだが、この まともに考えたら ってのは、社会の合理性をどこかで信じていることの現れじゃないかと考えてる。 社会は まともに 運営されて稼働している、という観念が根強く響いてきている。 マイナンバー、新元号、まともじゃないものはいままで何度も見てきているのに、やはりどこかで信じてしまっている。

非合理的な社会はそこに確かに存在していて、だからこういった非合理的なシステムが生まれようとしていて、元になった社会自体が非合理なのだから、多分これは"まかり通る"んだろう。 なんだかんだ、詐欺的なシステムの上で、東京オリンピックは終了し、さまざまな形で負債を残していくんだと思う。祭りが終わるまでは誰も現実を見ないんだろう。

特に何か結論があるわけでもない話。おしまい。

危機感の話

常に危機感がある。これは自分が博士号も修士号も持っておらず、第三者から観測可能な価値を持っていないどころか、コンピューターサイエンスの教育を受けたことがないくせにソフトウェアエンジニアとして専門職に就いて生計を立てているのが根底にあるかもしれない。 プログラマーとしてインターネット上で活動しはじめたころからずっとアカデミーに対して劣等感がある。 自分がやっていることなんて高度な教育を受けた人間がちょっと参入してくればあっという間に淘汰されるだろうし、常に風前の灯火っていう感じがしてる。

正直なところ1年後に自分に仕事があるかどうかまったく自信がない。半年後すら曖昧だ。3年後なんてまったく想像もできない。 何をしても足りてない気がするから、毎日生き急いでいるような気がする。

本当は価値がないかもしれないけども、少なくとも価値があると錯覚してもらうために、常に可能な限り優れた成果を出してそれをアピールして生きてる。 そういうセルフブランディングだけで生きているので、それをやめた瞬間すべてが止まってしまうという危惧がある。

これは克服できるのか、すべきなのか、なんもわからん。

持続可能なAngularアプリケーション開発のために大事なこと

Webにかぎらず、アプリケーションというのは作って終わりではなく、その後も継続して改修・改善されていくケースが多い。受託で開発して納品して終わりというケースでも、納品した先にメンテナンスする人がいる。

この記事では、Angularアプリケーションの開発において、いかにメンテナンス性を維持して、持続可能なプロジェクトを構成するかについての個人的な見解をまとめる。

フレームワークを邪魔しない

Angularアプリケーションのメンテナンスにおいて、いちばん重要なことはいかにAngularのアップデートを阻害しないかという点に尽きる。 これはAngularに限った話ではなくフレームワークと呼ばれるものを使うなら常に必要なことであるし、 アップデートが定期的に降ってくることが決まっているAngularであればなおさらである。 アプリケーションの一番根幹となる部分の鮮度が落ちれば、その他の部分はそれに引きづられて腐ってしまう。

なので、Angularの新しいバージョンがリリースされたときにアップデートのブロッカーとなるものはなるべく作らないのが第一のポイント。 具体的には、ngx-**系のサードパーティ製のAngular用ライブラリは、極力減らす。 とはいえすべて使わない、というのはある程度の規模になると現実的ではないので、 利用するときは、そのライブラリのメンテナンスが頻繁に行われているかどうかをしっかりチェックするべき。

何か機能を実装するためにライブラリが必要だと思ったときには、まずフレームワークに依存しない実装のnpmモジュールを探す。 そのnpmモジュールとAngularとのインテグレーション部分は、自分でアダプターやブリッジを実装するのがベスト。

例: Google Analytics

例として、AngularアプリケーションでGoogle Analyticsと連携したいと思った時、angular google analyticsのように検索すると当然Angularのエコシステム上には専用のライブラリが出てくる。 例えば有名所で言えば https://github.com/angulartics/angulartics2 のような。

しかしこれはAngularのバージョンアップデートを阻害するリスクを冒してまで導入するべきものかどうか、個人的にはNoである。

Angularの基本的な機能を理解さえしていれば、ルーティングでURLパスが変わるたびにpageviewイベントをGAに送信するなど造作もない。たった5行のコードで書ける。

@Component({...})
export AppComponent {
  constructor(private router: Router) {}

  ngOnInit() {
    this.router.events.subscribe(e => {
      if (e instanceof NavigationEnd) {
        window.gtag('config', 'UA-*******-**', { page_path: e.urlAfterRedirects })
      }
    });
  }
}

クリックイベントの送信だって、(click) でイベントを受け取ったあとコンポーネント側で処理できる。 テンプレート上だけで簡単に済ませようと思うのは間違っていないが、Angularのバージョンアップデートを阻害するリスクと天秤にかけて考えるべき。 ディレクティブにしたって自分で書けばいい話だ。

Angularとサードパーティライブラリ

Angular用のサードパーティライブラリは、基本的には次の3種類になる。

  1. AngularのAPIブラックボックス化したユーティリティライブラリ
  2. 便利なディレクティブを提供するUIライブラリ
  3. 外部ライブラリをラップしたアダプタライブラリ

AngularのAPIブラックボックス化したユーティリティライブラリは極力避けよう。これは数行の手間を惜しむことで数ヶ月後の自分の首を締めることになる。特にこういう小さなユーティリティ系は作者が作って公開しておしまいになるケースが多く、そうして結局あとで自分で書くことになる。

UIライブラリも要注意。たくさんのコンポーネントのテンプレート内に散らばり、ライブラリを差し替えようとなったときにも一苦労になる。Angular MaterialやClarity、Ignite-UIなど、企業レベルでメンテナンスされていると安心して使える。

アダプタライブラリは、細かく分けると2種類ある。その違いはアダプタされた先のコア部分がAngularに依存するかどうか。 例えば、先ほどのGoogle AnalyticsをラップしたAngularticsやAngularFireなどは、コア部分は普通のJavaScriptなのでラップするのは自分でも簡単にできる。 よくあるのはFirebaseに新機能が入ったがAngularFireに反映されてなくて使えない、みたいなケースで、そういうリスクもあるので注意が必要である。 Firebaseのラッパーくらいは自分で書ける範囲。

一方でApollo-Angularは少し毛色が違い、Apollo-AngularはApollo Clientのラッパーというよりは、AngularのHttpClientをベースにApollo Clientを初期化するためのブリッジのようなもの。 この場合はAngular HttpClientとのブリッジを自分で書くのはApollo Clientの内部ドメインに精通している必要があるので少し難しい。 とはいえ Angularとaxiosを使ったHTTP通信 - lacolaco でも述べたように、Angular HttpClientを使わなければいけない理由などどこにもないので、標準のApolloClientを使ってしまうのもよいだろう。このあたりはそのライブラリのメンテナンス状況と信頼度に応じて考えるべきところ。

Angular用に作られたサードパーティライブラリを採用できる条件は次のようになる。

  1. その機能はAngularに依存するか => しないなら非依存のnpmパッケージを探す
  2. その機能はユーティリティ(いくつかのAngular機能のショートハンド)に過ぎないか => そうなら使わない
  3. そのライブラリはこまめにメンテナンスされているか => そうでないなら使わない

Angularに依存する部分と、そうでない部分を明確にする

アプリケーションの設計のなかで、Angularに依存すべき部分とそうでない部分を明確に分けておくことが大事。 ビュー層はどうしようもなくAngularの領域なので、コンポーネント、ディレクティブ、パイプの中にドメイン固有のロジックを持たない、ビューとしての仕事だけに専念させる。

ドメインロジックをまとめたサービスクラスへの@Injectable() は特に内部にAngularが侵入するわけではないので、大きな問題にはならない。 ただし、ドメインロジックの中でもAngular APIが必要になることがある。たとえばタイトルを変えるためのTitleサービスや、現在のURLを扱うためのLocationサービス、あるいはHttpClientやRouterなど。 そういった部分は、アプリケーションのインフラ層に逃し、やはりドメインロジックからは排除したい。下の画像の真ん中のロジック部分からはAngularを排除する。 ドメインロジックの変更をAngularに邪魔されないために、また、Angularのアップデートをドメインロジックで邪魔しないために。

f:id:lacolaco:20180515113943p:plain

ロジック内部の設計は自由だが、Angularに依存する部分とそうでない部分を明確にすると、自然と最低でもこの3層はできあがるはず。

プリミティブに書く

AngularもRxJSも、凝ろうと思えばいくらでもテクニック重視の書き方ができる。たとえばRxJSのpipeだけでほとんどの処理を済ませてしまうとか、たくさんのパイプを繋げてテンプレート内で処理を済ませてしまうとか。

書いてる間は気持ちがいいが、同僚や数ヶ月後の自分を困らせることになるのは誰の目にも明らか。なるべく素のTypeScriptでプリミティブに書けないかどうかを考えたい。 ライブラリのコードの最適化はライブラリが頑張るしかないが、TypeScriptで素直に書いておけばコンパイル時の最適化や実行時のエンジンでの最適化も受けやすい。async/awaitがそのいい例。 AngularやRxJSなどのアップデートでよく提供されるマイグレーションCLIも、使われている箇所が複雑になると適用漏れが生まれやすい。

テンプレート自体にも要注意で、*ngIf*ngForngSwitch などでビューの構造を操作しまくるテンプレートが大きくなると読みづらい。 こういった構造ディレクティブの内側は、ある程度スコープが切られた小さなテンプレートになるはずなので、その単位でコンポーネント分割するとけっこう見通しが良くなる。

こういったテンプレートから

<ng-container *ngFor="let user of users">
  <div class="user">
    <div>{{user.name}}</div>
  </div>
</ng-container>

このように切り出すのがよい。

<ng-container *ngFor="let user of users">
  <user-list-item [user]="user"></user-list-item>
</ng-container>

結果的に、構造を担うコンポーネントと、表示を担うコンポーネントが分かれていくので、コンポーネント設計としてもオススメの考え方である。

まとめ

持続可能なメンテナンス性の高いAngularアプリケーションを開発するために重要なことは以下の点。

  • Angularのバージョンアップデートを邪魔しない
    • Angularの特定バージョンに依存するサードパーティライブラリは極力減らす
    • Angularにない機能にはまずプレーンなnpmライブラリを探し、アダプターが必要なら自分で書くようにする
  • コードベース中で、Angularに依存する領域とそうでない領域を明確にする
  • 複雑なAPIを使いこなすよりも、プリミティブに書く

Angular v2からv6までの変化をまとめてみた

rdlaboさんがしっかりGW明けにIonicの記事書いてくれたので、僕もAngularのv2からv6まで、3年弱の変遷についてまとめます。

Ionic 2 から 4 への、この2年間の進化を振り返る

前Angular v2時代

現行Angularの歴史を話し始めるとAngularDart 0.x-1.xからの話になります。 実はAngularDart v1は、停滞したAngularJSの改革をおこなうための、仮説検証の場でした。 僕がAngularDart 0.xから1.xを使って開発していたのが2014年末〜2015年あたりなので、だいたい3年半くらい前の話です。

ここで生まれた基本的な概念は今でもしっかりとAngularに残っています。例えば次のようなもの。

これらはDartの言語機能やWeb Components仕様の整備により実装できた新しいAngularの形で、これをAngularJSの次世代バージョンとして実現するために必要な言語機能の仕様が AtScript として誕生し、これをもとにAngular 2の開発が進められました。

結局その後は紆余曲折ありAtScriptは必要なくTypeScript+デコレータで実現することになりました。

Angular v2

2015年12月に、Angular v2はbeta.0をリリースし、2016年9月14日に2.0.0がリリースされました。

ちゃんと掘り下げると無限に長くなるので、この時期に導入された重要な概念をおさらいするだけに留めます。

オフラインコンパイル

現在ではAngularのAoTコンパイルは当たり前になっていますが、当時はじめて"オフラインコンパイル"というものが発表されたときは困惑しました。 あらためて当時のプロポーザルを見ると、まさにこれって今実装されようとしているngIvyそのものじゃないかと思いました。 これまでのAoTではまだ不十分でしたが、ついに3年越しの成就となろうとしてます。感慨深い。っていうかTobias氏が頭良すぎる。

lacolaco.hatenablog.com

Angular 2 offline template compilation - Google ドキュメント

https://ng2-info.github.io/2016/01/angular2-in-2016/#even-faster-angular-2-tobias

実際にAoTコンパイルが使えるようになったのはrc.2からでした。2016年5月のことなので、ちょうど2年前。ngcちゃんも2歳になりました。

AngularJSへの .component 逆輸入

AngularJSからAngular 2への移行をスムーズにするため、マインドセットを統一するためのAPI逆輸入がおこなわれました。 コンポーネントベースの設計になることで、コンポーネントルーターを起点としたマイグレーションが可能になる、という構想でした。

当時は angular-component-router 自体は失敗しましたが、考え方自体は今のngUpgrade APIと同じなので、やっぱりAngularコアチームは頭良いですね。

Animation API

この頃、Web Animations APIを元にしたAnimation APIを作ることが決まりました。今でもしっかり続いています。

Language Service

テンプレート内でコンポーネントの型情報を元に型チェックや入力補完をおこなう機能の提案がなされたのが実はbeta.5のころです。 実際に実装されて使えるようになったのは2.3リリースの頃ですが、いまでは開発中に無くてはならないものになっています。

https://ng2-info.github.io/2016/02/beta-5-has-released/#template-services-plan

Angular CLIとスタイルガイド

John Papa氏とMinko Gechev氏のふたりが草案を作ったAngular Style Guideは、いまでも公式ドキュメントとしてメンテナンスされつづけています。 そしてAngular CLIではそのスタイルガイドに沿ったコード生成をおこなっています。

SystemJSからwebpackへ

アルファ版のころから続いていたSystemJSベースのセットアップが、webpackベースに変わりました。 v6になった今でもAngular CLIはwebpackを利用していて、Tree Shakingなどいろいろな最適化ができているのでナイスな決断だったといえます。

Forms APIの改革

RCの中でもっとも大きく変わったAPIのひとつがFormsですね。テンプレート駆動フォームと、リアクティブフォームが明確に分割され、選択できるようになりました。 この頃はテンプレート駆動がメインで、リアクティブがオプショナルという立ち位置でしたが、いまやリアクティブがメインで、テンプレート駆動がオプショナルとなりつつあります。 パラダイムの変遷を感じます。

Routerの改革

RCの中でもっとも大きく変わったAPIその2、Routerです。当時はてんやわんやでした。

AngularJS: Improvements Coming for Routing in Angular

Victor Savkin氏により実装されたRouter v3はその形をほとんど変えないまま今のv6まで続いています。 Victor Savkin氏はAngularコアチームの中でもとびきり頭が良い一人ですね。

NgModuleの導入

Angular 2のRCといえばNgModuleというくらい悪名高い破壊的変更です。いまでは当たり前ですが、これも当時は衝撃でした。

https://ng2-info.github.io/2016/07/preparing-for-ngmodule/

v6でも現役な、Angularの根幹のAPIですが、そのあり方は変わろうとしています。詳しくはv6のセクションで述べます。

Custom Elements対応

rc.6で追加されたのが CUSTOM_ELEMENT_SCHEMA です。Angularのテンプレート内でAngularが知らないカスタム要素を使うための機能です。 これによりWeb Components仕様に則って作られたCustom Elementsとの協調が可能となりました。 いまでも現役な重要機能です。

v4

半年後の2017年3月23日にv4.0.0がリリースされました。

AngularJS: Angular, version 2: proprioception-reinforcement

AngularJS: Angular 4.0.0 Now Available

v2.xからv4の中で変わっていったものたちを振り返っていきましょう。

Reactive Angularに向けた変化

この頃からリアクティブプログラミングをサポートするための変更が進み始めます。 *ngIf*ngForasync as 記法で非同期データと値を保持できるようになったのがv4のことです。今ではこれ無しのテンプレートは考えられないです。

<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>

Angular Universalの刷新

ベータ版のころからサードパーティとして続いていたAngular Universalのコア部分をAngularコアチームが回収し、@angular/platform-serverとして再実装しました。 複雑になっていたAPIを整理し、renderModuleだけを提供するシンプルなパッケージに生まれ変わりました。 platform-serverはこの頃と今でもあま変わっていませんが、platform-serverを元にした新しいAngular Universalは独自に進化を続けています。

https://github.com/angular/universal

f:id:lacolaco:20180508103229p:plain

angular.ioのAngular化

v4.2のリリースにあわせて、ようやく公式ドキュメンテーションサイトのangular.ioがAngularで再実装されました。 いまでもコアチームによるAngularのPWA実装の例として活発にメンテナンスされています。

AngularJS: Angular 4.2 Now Available

Animation APIの改革

stagger()group() など複雑なアニメーションを実現するための新しいAPIがv4.2で導入されました。

A New Wave of Animation Features in Angular - yearofmoo.com

HttpClientの導入

@angular/common/http パッケージが公開され、新しいHTTPクライアントが使えるようになったのがv4.3です。

Angular 4.3 Now Available – Angular Blog

v5

v4から半年後、2017年11月2日に2回目のメジャーアップデートであるv5.0.0がリリースされました。

Version 5.0.0 of Angular Now Available – Angular Blog

Build Optimizer

v5の頃から、Angularは機能の追加よりもバンドルサイズの改善に注力する傾向が強まります。 Build Optimizerはwebpackのプラグインとして提供され、Angularのアプリケーションのバンドル時に様々な最適化をおこない、バンドルサイズを削減します。

AngularコンパイラのTypeScript Transformer化

AngularのテンプレートコンパイラがTypeScriptの標準拡張機能に準拠することにより、デコレータのコンパイル処理に介入できるようになりました。 これはテンプレートの最適化をおこなうための足がかりでもあり、テンプレート中の無駄な空白を除去してバンドルサイズを削減することなどが可能になりました。

また、AoTコンパイルの軽量化により、ng serve --aot が可能になったのもこの時です。

さらに、デコレータ内でアロー関数を使ってもAoTコンパイルできるようになったのもv5です。v2から長くやっている方はもしかすると無意識に避けてしまっているのではないでしょうか。

Component({
  provider: [{provide: SOME_TOKEN, useFactory: () => null}]
})
export class MyClass {}

ロケール依存パイプの刷新

v5で最も衝撃の強かった変更はdatenumberなど、ロケールに依存する組み込みパイプの全面刷新です。 それまではWeb標準仕様のI18n APIに準拠して実装されていましたが、バグが多くブラウザ間の実装の差異に悩まされていたため、それをやめてAngular内部にロケールデータを持つことになりました。

https://github.com/angular/angular/blob/master/CHANGELOG.md#i18n-pipes

Zone.js非依存化への歩み

v5では、Angularアプリケーションの実行時にZone.jsを使わない選択肢が選べるようになりました。

platformBrowserDynamic()
  .bootstrapModule(AppModule, {ngZone: 'noop'});

バンドルサイズの軽量化や、エキスパートな開発者による独自のパフォーマンスチューニングを可能にするための機能です。

Angular MaterialとCDK

ついにようやくAngular MaterialがStableになり、そのコアであるComponent Dev Kit(CDK)も公開されました。

Angular 5.1 & More Now Available – Angular Blog

Angular CLIのPWAサポート

Angular CLIがService Workerの生成や、Angular Universalを利用したApp Shellのレンダリングなどをサポートし、Angular for PWAの風が吹いてきました。 v6では ng add @angular/pwa だけでPWA化が済んでしまうほどになりました。

Angular Progressive Web App Guide

v6

そして2018年5月4日、6.0.0がリリースされました。

Version 6 of Angular Now Available – Angular Blog

Angular Elements

v6といえばAngular Elementsです。 まだまだ実験的なAPIではありますが、AngularのコンポーネントをCustom Elements化できる機能が公開されています。

https://angular.io/guide/elements

アプリケーションのためのフレームワークだったAngularから、WWWの世界へ進出していくための大きな一歩です。

Angular CLIの大改革

Angular CLIがv1.7からv6へアップデートし、かなり大きな進化を遂げました。

アプリケーションだけでなくライブラリ開発も可能になり、ng updateコマンドやng addコマンドなど新しい機能もたくさん追加されました。

新しいDI機構

NgModuleの導入以来ずっと変化のなかったDI機能に、新しい仕組みが導入されました。

Tree Shakable Providers と呼ばれる機能により、DIでプロバイドされる@InjectableなクラスをNgModuleのprovidersに渡さなくてもよくなります。 代わりに、@Injectable({providedIn}) という新しいAPIで、自身がプロバイドされるモジュールのスコープを持つようになります。

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class MyService {
  constructor() { }
}

Angular v6で導入されるTree-Shakable DIの紹介 - lacolaco

https://angular.io/guide/dependency-injection#tree-shakable-providers

静的な参照がNgModuleから消えるため、Tree Shakingによって実際にコンストラクタの型パラメータとして参照されているクラスだけがバンドルに含まれるようになります。

RxJS v6

Angular v6が依存するRxJSのバージョンもv6にアップデートされました。 RxJSのメンテナであるBen Lesh氏もAngularコアチームに入ったことによりスムーズに連携できるようになった印象です。

RxJS v6はTree Shakingサポートによってバンドルサイズを大きく減らすことができるため、Angularにとってもかなり嬉しいアップデートです。


まとめ

結構長くなりました。 v2以前からv6まで変化の流れを追ってみましたが、今あるAngularの機能の大半はv2のころのままですね。 新し目の機能についても構想自体はv2以前からのものもあり、2.0.0以降の変化は主に開発者支援や、バンドルサイズ削減に貢献するものがほとんどだなというのがわかります。

  • v2: フレームワークとしてのAngularの完成
  • v4: フレームワークのブラッシュアップ
  • v5: プラットフォームとしてのAngular: Material, CDK, PWAサポート
  • v6: プラットフォームの拡大: Angular Elements

といった感じでしょうか。

今後もAngularはフレームワークとしてだけでなくプラットフォームやエコシステムとして広がっていくこと間違い無しなので、マクロな視点で見ていくのが大事ですね。