余白

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

持続可能な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を使いこなすよりも、プリミティブに書く