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 種類になる。
- Angular の API をブラックボックス化したユーティリティライブラリ
- 便利なディレクティブを提供する UI ライブラリ
- 外部ライブラリをラップしたアダプタライブラリ
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 の内部ドメインに精通している必要があるので少し難しい。 とはいえ https://lacolaco.hatenablog.com/entry/2018/04/20/080000 でも述べたように、Angular HttpClient を使わなければいけない理由などどこにもないので、標準の ApolloClient を使ってしまうのもよいだろう。このあたりはそのライブラリのメンテナンス状況と信頼度に応じて考えるべきところ。
Angular 用に作られたサードパーティライブラリを採用できる条件は次のようになる。
- その機能は Angular に依存するか => しないなら非依存の npm パッケージを探す
- その機能はユーティリティ(いくつかの Angular 機能のショートハンド)に過ぎないか => そうなら使わない
- そのライブラリはこまめにメンテナンスされているか => そうでないなら使わない
Angular に依存する部分と、そうでない部分を明確にする
アプリケーションの設計のなかで、Angular に依存すべき部分とそうでない部分を明確に分けておくことが大事。 ビュー層はどうしようもなく Angular の領域なので、コンポーネント、ディレクティブ、パイプの中にドメイン固有のロジックを持たない、ビューとしての仕事だけに専念させる。
ドメインロジックをまとめたサービスクラスへの@Injectable()
は特に内部に Angular が侵入するわけではないので、大きな問題にはならない。 ただし、ドメインロジックの中でも Angular API が必要になることがある。たとえばタイトルを変えるためのTitle
サービスや、現在の URL を扱うためのLocation
サービス、あるいは HttpClient や Router など。 そういった部分は、アプリケーションのインフラ層に逃し、やはりドメインロジックからは排除したい。下の画像の真ん中のロジック部分からは Angular を排除する。 ドメインロジックの変更を Angular に邪魔されないために、また、Angular のアップデートをドメインロジックで邪魔しないために。
ロジック内部の設計は自由だが、Angular に依存する部分とそうでない部分を明確にすると、自然と最低でもこの 3 層はできあがるはず。
プリミティブに書く
Angular も RxJS も、凝ろうと思えばいくらでもテクニック重視の書き方ができる。たとえば RxJS のpipe
だけでほとんどの処理を済ませてしまうとか、たくさんのパイプを繋げてテンプレート内で処理を済ませてしまうとか。
書いてる間は気持ちがいいが、同僚や数ヶ月後の自分を困らせることになるのは誰の目にも明らか。なるべく素の TypeScript でプリミティブに書けないかどうかを考えたい。 ライブラリのコードの最適化はライブラリが頑張るしかないが、TypeScript で素直に書いておけばコンパイル時の最適化や実行時のエンジンでの最適化も受けやすい。async/await がそのいい例。 Angular や RxJS などのアップデートでよく提供されるマイグレーション CLI も、使われている箇所が複雑になると適用漏れが生まれやすい。
テンプレート自体にも要注意で、*ngIf
、*ngFor
、ngSwitch
などでビューの構造を操作しまくるテンプレートが大きくなると読みづらい。 こういった構造ディレクティブの内側は、ある程度スコープが切られた小さなテンプレートになるはずなので、その単位でコンポーネント分割するとけっこう見通しが良くなる。
こういったテンプレートから
<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 を使いこなすよりも、プリミティブに書く