Angular のコンポーネント分割について考えていることや実践していることをメモ。
Presentation Domain Separation
前提として Angular アプリケーションは Presentation Domain Separation を推している。(と、僕は考えている)
コンポーネント内のロジックはビューに必要なロジックだけに制限します。他のすべてのロジックはサービスに委譲する必要があります。
なので、コンポーネント(Presentation)とサービス(Domain)は適切に分離することがベストプラクティスとされている。 適切にとは何かやサービス側のさらなる分離についてはここでは言及しない。
コンポーネントの役割
ドメインロジックをサービス層へ委譲したとしてもコンポーネントにはまだ複数の役割がある。
- スタイリング
- ドメインロジックの呼び出し
- URL パスとの紐付け
- クエリパラメータやフラグメントへのアクセス
それぞれに名前をつけて責務を明確にすることで保守性が高まる。 具体的には単体テストなどで注力する部分が明確になりテストしやすくなるのではないか。
Presentational component
- 画面の表示に徹するコンポーネント
- 要素の配置とスタイリング
- 必要なデータは Input デコレータを使って親コンポーネントから受け取る
- 反対に親コンポーネントにイベントを伝搬させる必要があるときは Output デコレータを使う
Container component
- サービス層や store を通してデータのやり取りをするコンポーネント
- 他の Container component や Presentational component を配置する
- 基本的に表示に関することは何も気にしない
Page component
- AppRoutingModule により URL パスと紐付けられるコンポーネント
- router-outlet を通してのみ表示されるので selector の指定は不要
- ActivatedRoute を通してクエリパラメータやフラグメントへのアクセスが可能
- パスから取得したパラメータを保存するためにサービス層にアクセスすることがある
- 逆にサービス層から何かを得ることはない
- HTML は Containers component を置くだけでスタイルは管理しない
- styles や styleUrls の指定も不要
- xxxPageComponent という名前にするとエディタで探しやすい
例えば Tour of Heroes で /detail/:id
に対応する HeroDetailPageComponent を用意したとするとこのコンポーネントは spec を除くとこれくらいシンプルになる。(styles や styleUrls は不要で template もインラインで書けるくらいの量しかない)
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @Component({ template: `<app-hero-detail></app-hero-detail>`, changeDetection: ChangeDetectionStrategy.OnPush, }) export class HeroDetailPageComponent implements OnInit { constructor(private readonly _route: ActivatedRoute) {} ngOnInit(): void { // TODO } }
まとめ
上記を踏まえて図にしてみた。
フロントエンドのコンポーネント分割で Presentational component と Container component はよく見かけるが、URL パスと紐付けを考える Page component を置いている。 Container component はよくロジックに専念すると言われることが多い印象だが、実際にロジックを持つのはサービス層になるためあくまでそことやり取りをするコンポーネントとしている。
Tour of Heroes を題材に試行錯誤している GitHub リポジトリを貼っておく - https://github.com/kasaharu/angular-architecture