【Angular】Angularを1年間触ってみてたどり着いたコンポーネント設計: Page-Layout-Presenter構造

# Page-Layout-Presenter構造

フロントエンド開発において最適なコンポーネント設計について考えることは避けては通れないと考えています。
未熟ながら私もいくつか考えてみました。今回はその中で、ある程度形になったものを一つ提案したいと思います。

今回提案する設計では、チームでの開発を想定し、長期的に破綻せず運用でき、修正に強く、直感的であることを目指しました。
少しでもAngularの開発の手助けになればと思っています。

考えた経緯については別記事にまとめてあります。ご興味のある方は併せてご覧ください。
⇒ [【フロントエンド】最適なコンポーネント設計について考えてみた](https://qiita.com/oioigohan/items/fea2c23ee6cd221776f1)

## 設計の全体像

### 参照構造

詳細を説明する前に、大まかにでも全体を見ていただきたいので、実装した際のコンポーネントなどの参照構造をまずは図で紹介します。
赤色は`@NgModule`、黄色は`@Component`、水色は`@Injectable`を持つクラスを指します。
少し複雑に見えるかもしれませんが、詳細は後ほど説明します。

![Angular Page-Layout-Presenter architecture image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505645/f9f37da0-e361-3c4d-3cb7-ccd4523f0a96.png)


### ディレクトリ構造

次にディレクトリ構造を紹介します。
これについても詳細は後ほど説明します。

なお、拡張子が`.spec.ts`のファイルは表示を省略しています。
また、繰り返しなどは`...`で省略しています。
そして、次のようなコンポーネント内のファイルは`some/some.component.*`として省略して表記しています。

```:コンポーネント内のファイル
some/
　├ some.component.html
　├ some.component.scss
　├ some.component.spec.ts
　└ some.component.ts

=> some/some.component*
```

全体のディレクトリ構造は次です。

```:ディレクトリ構造の全体像
src/
　└ app/
　　├ app.module.ts
　　├ app-routing.module.ts
　　├ app.component.*
　　├ apis/
　　│ └ ...
　　├ services/
　　│ └ ...
　　├ directives/
　　│ ├ directives.module.ts
　　│ ├ no-tag.directive.ts
　　│ └ ...
　　├ components/
　　│ ├ material-design/
　　│ │ └ material-design.module.ts
　　│ └ presenters/
　　│ 　├ presenters.module.ts
　　│ 　├ buttons/
　　│ 　│ ├ buttons.module.ts
　　│ 　│ ├ button/button.component.*
　　│ 　│ ├ yellow-button/yellow-button.component.*
　　│ 　│ ├ white-button/white-button.component.*
　　│ 　│ └ ...
　　│ 　├ texts/
　　│ 　│ ├ texts.module.ts
　　│ 　│ └ ...
　　│ 　├ images/
　　│ 　│ ├ images.module.ts
　　│ 　│ └ ...
　　│ 　└ ...
　　└ features/
　　　├ feature1/
　　　│ ├ feature1.module.ts
　　　│ ├ feature1-routing.module.ts
　　　│ ├ services/
　　　│ │ └ feature1.service.ts
　　　│ └ components
　　　│ 　├ pages
　　　│ 　│ ├ some1-page/some1-page.component.*
　　　│ 　│ ├ some2-page/some2-page.component.*
　　　│ 　│ └ ...
　　　│ 　└ layouts
　　　│ 　　├ some1-layout/some1-layout.component.*
　　　│ 　　├ some2-layout/some2-layout.component.*
　　　│ 　　├ some3-layout/some3-layout.component.*
　　　│ 　　└ ...
　　　└ feature2/
　　　　└ ...
```

## 設計の詳細

今回提案する設計では、コンポーネントを適切に分割することに加え、**チームでの開発を想定**し、**長期的に破綻せず運用でき**、**修正に強く**、**直感的である**ことを目指しました。

そのために、コンポーネントの分割方法や、サービスやディレクティブ、スタイルシート、モジュールの作成などについて、総合的に考えました。ここではそれらについて大まかに説明します。

### ルーティングの参照構造

Angularにもルーティングの機能があり、クライアントから受け取ったURLを基に、表示するコンポーネントなどを指定することができます。

#### features

ここではアプリケーションがもつ機能を`feature`と呼ぶことにします。
例えば`login`や`todo`など、それらはアプリケーションによって異なります。
ルーティングを有効にする場合は、`https://localhost:4200/login`の`login`ように、アプリケーションのURLの直後の部分が`feature`にあたります。

これはつまり、各`feature`は`<feature>.module.ts`と`<feature>-routing.module.ts`を持ち、さらに、ルーティングの一番親である`app-routing.module.ts`から参照されるということです。

このルーティングの参照関係を図にすると次のようになります。

![routing module refference graph](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505645/46608ef5-e1ee-a29c-25b4-c88a338e4789.png)


`feature`同士は互いに関与しないため、並行して開発しても衝突しません。また、ある`feature`がバグを含んでいても、他の`feature`は正常に動かせます。なので、エンジニアが`feature`毎に分担するという開発方法が考えられます。

### コンポーネントの親子構造

コンポーネントには親子構造があり、次のような層構造を持たせます。

(親) => (子)
`App Component` => `Page Components` => `Layout Components` => `Presenter Components`

ここではそれぞれの層について説明します。

#### App Component

- アプリケーション新規作成時に自動で作成されます。
- アプリケーションに対し一つのみ存在し、親子関係の頂点になります。

#### Page Components

- これはウィンドウ一面に表示されるページを表すコンポーネントで、主に`Layout Components`で構成されます。
- `feature`毎にページを作成するため、`src/app/features/components`ディレクトリに配置します。
- URLに対し一つだけとし、使いまわしはしません。
- 状態管理に関するサービスは`Page Components`でのみ注入します。
  - アプリケーションの表示に関わる具体的なデータのことを「状態」と呼びます。
  - これは、[ほかの方の設計](https://speakerdeck.com/koumatsumot0/angular-webahurikesiyonfalsezui-xin-she-ji-shou-fa)などで見られる`Container Component`の役割と言えますが、`Container Component`はページに対して一つとされる場合が多いので、今回は冗長性を減らすために`Page Components`に取り込んでいます。
  - 状態管理には`NgRx`の利用が考えられますが、コストが高いため最初は`NgRx`を利用せず、サービスのみで開発するという方法も考えられます。`NgRx`や`Interceptor`については勉強中のため、今回は説明を省略させていただきます。
  - これについて図で表すと次のようになります。
![angular-architecture-overview2-Page-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505645/23892202-be2f-223c-b560-6a6f80af5476.png)
- 子コンポーネントから`@Output()`でイベントを受け取り、必要であれば状態を子コンポーネントの`@Input()`へ返します。
- `background-color`など、ページ全体に対するスタイルはこのコンポーネントに記述します。

#### Layout Components

- `Page Components`と同じく、`src/app/features/components`ディレクトリに配置します。
- `Presenter Components`と`Layout Components`で構成されます。
- `Layout Components`は他の`Layout Components`を参照可能なので、これにより複雑なレイアウトも表現可能です。
- `Layout Components`は状態を持ちません。
    - ただし、コンポーネント内で完結するような状態は除きます。
- 親コンポーネントから`@Input()`で受け取った状態を利用し、必要であれば子コンポーネントの`@Input()`へ返します。
- また、子コンポーネントから`@Output()`でイベントを受け取り、親コンポーネントへ`@Output()`します。
- `display`や`margin`など、レイアウトに関するスタイルはこのコンポーネントが担います。逆に、見た目に関するスタイルはここには記述しないようにします。

`features`と`Page Components`、`Layout Components`を図にすると次のようになります。

![angular-architecture-overview2-Page-9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505645/ddee19f4-4b44-3ee7-ac27-4a474fa1f0c1.png)

#### Presenter Component

- `Presenter Components`はページにおける最小パーツです。
- `Presenter Components`はアプリケーション全体で参照可能なコンポーネントです。
  - そのため、`src/app/features/components`ディレクトリではなく`src/app/components`ディレクトリに配置します。
- `Presenter Components`は一つの`PresentersModule`を持ちます。
- `Presenter Components`は複数の`Presenter`で構成されていて、`PresentersModule`は各`Presenter`が持つ`PresenterModule`をimportします。
- `Presenter`内でAngular Material Designを利用する場合はその`PresenterModule`へ`MaterialDesignModule`をimportします。
- `Presenter`は一つの`GUI Parts Componponent`とそれを継承する複数の`Styled GUI Parts Components`を含んでいます。

##### GUI Parts Component と Styled GUI Parts Components

- `Styled GUI Parts Components`とは、`GUI Parts Component`を継承し、その上で見た目に関するスタイルを記述したコンポーネントです。
  - 例えば`buttons`という`Presenter`の場合、`button`が`GUI Parts Component`、それ以外が`Styled GUI Parts Components`となります。この例のディレクトリ構造と図は次のようになります。

```
src/app/components/presenters/
　├ presenters.module.ts
　└ buttons/
　　├ buttons.module.ts
　　├ button/button.component.*                // GUI Parts Component
　　├ yellow-button/yellow-button.component.*  // Styled GUI Parts Component
　　├ white-button/white-button.component.*    // Styled GUI Parts Component
　　└ ...                                      // Styled GUI Parts Component
```

![angular-architecture-overview2-Page-5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505645/2a1a6ff1-b387-d5e3-32bf-31e0b6279bba.png)

- `Presenter Components`は状態を持ちません。
    - ただし、コンポーネント内で完結するような状態は除きます。
- `GUI Parts Component`にて、親コンポーネントから状態を受け取る`@Input()`と親コンポーネントへイベントを返す`@Output()`を記述し、`Styled GUI Parts Components`ではそれを継承します。
- `Presenter Components`は一度作ればほかのアプリケーションにも移植でき、その意味でも再利用性が高いです。

---

これらのことを踏まえると、コンポーネントの親子関係の図は次のようになります。

![angular-architecture-overview2-Page-6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505645/54060f03-ad3b-2f10-0260-1a24778cb558.png)

また、モジュールやサービスも含め、以上のことをまとめると、記事冒頭にある図と同じになります。

![Angular Page-Layout-Presenter architecture image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505645/f615ea16-498f-d352-94c2-e1056331745c.png)

<!--
## 構築手順

続いて、この設計におけるアプリケーションの構築手順を大まかに説明します。

### 1. appディレクトリ

#### 1.1. アプリケーションの新規作成

`ng new`コマンドでアプリケーションを新規作成します。
このとき`app`ディレクトリは次のような構造になっています。
ルーティングを有効にした場合は`app-routing.module.ts`モジュールも作成されます。

```
src/
　└ app/
　　├ app.module.ts
　　├ app-routing.module.ts
　　└ app.component.*
```

#### 1.2. tailwindcssのインストール

[こちらの記事](https://qiita.com/oioigohan/items/f4abfa20571895f8ac4b)に沿って`@ngneat/tailwind`をインストールします。

<details>
<summary>この設計では[tailwindcss](https://tailwindcss.com/)との相性が良いと考えています。</summary>
これは、レイアウトに関するスタイルの修正に強くなると考えているからです。この設計では、レイアウトに関するスタイルと見た目に関するスタイルを`Layout Components`と`Presenter Component`に分担させました。そのため、変更頻度が高いと思われるレイアウトに関するスタイルをテンプレートに直接書き込めることは作業効率向上の点で大きなメリットになると考えています。
</details>

#### 1.3. 必要なモジュールの作成

ここに、今回の設計で必要なモジュールを追加していきます。
追加するのは次です。

- MaterialDesignModule
- PresentersModule
- DirectivesModule

`MaterialDesignModule`は[Angular Material Design](https://material.angular.io/)を利用しない場合は不要ですが、今回は含めることにします。

これらモジュールは作成後、AppModuleにてimportします。

仮に`some-app`というアプリケーションを作成した場合、コマンドはそれぞれ次のようになります。

```shell:appディレクトリにモジュールを追加
some-app> ng g m components/material-design
some-app> ng g m components/presenters
some-app> ng g m directives
```

追加後のディレクトリ構造は次のようになります。

```
src/
　└ app/
　　├ app.module.ts
　　├ app-routing.module.ts
　　├ app.component.*
　　├ directives/
　　│ └ directives.module.ts  // 追加
　　└ components/
　　　├ material-design/
　　　│ └ material-design.module.ts  // 追加
　　　└ presenters/
　　　　└ presenters.module.ts  // 追加
```

#### 1.4. NoTagDirectiveを作成

[こちらの記事]()を参考に`src/app/directives`ディレクトリに`no-tag.directive.ts`を作成します。

<details>
<summary>`NoTagDirective`とは不要なタグをレンダリング時に除外するための自作のディレクティブです。</summary>
コンポーネントに親子関係があるとき、レンダリング時は子コンポーネントのタグも一緒に表示されます。そしてその`<app-hoge>`のようなコンポーネントのタグは、`<tbody>`タグと`<tr>`タグの間に入りこむなど、テーブルなどのコンポーネントの分割を阻害することがあります。そのような場合に利用します。（`<app-hoge>`などのタグはスタイルが適用されない上、DOM構造を見にくくするため、基本的に`app-`から始まるすべてのタグに`noTag`ディレクトリを付与するのがよいと考えています）
</details>

例えば次のようなテンプレートの場合、レンダリング後は`<tr>hoge</tr>`のみが残ります。

```html:some.component.html
<app-hoge noTag>
    <tr>hoge</tr>
</app-hoge>
```

#### 1.5. サービスなどのディレクトリを作成

さらに、アプリケーション全体で利用可能なサービスは`services`ディレクトリに作成し、
バックエンドとWebAPI連携する場合はそれ用のサービスを`apis`ディレクトリに作成します。
このようにして、最終的に`app`ディレクトリ下は次のようになります。

```
src/
　└ app/
　　├ app.module.ts
　　├ app-routing.module.ts
　　├ app.component.*
　　├ apis/                          // 追加
　　│ └ <some-api-name>-api.service.ts
　　├ services/                      // 追加
　　│ └ <some-common-service-name>.service.ts
　　├ directives/
　　│ ├ directives.module.ts         // 追加
　　│ └ no-tag.directive.ts          // 追加
　　└ components/
　　　├ material-design/
　　　│ └ material-design.module.ts  // 追加
　　　└ presenters/
　　　　└ presenters.module.ts       // 追加
```

### 2. featuresディレクトリ

#### 2.1. FeatureModuleとFeatureRoutingModuleの作成

次のコマンドで`FeatureModule`を作成します。

```
some-app> ng g m features/<feature-name> --routing
```

作成した`FeatureModule`のあるディレクトリに移動します。

#### 2.1. Page Components

`Pages`コンポーネントは`features/<feature>/components`ディレクトリ内に存在します。

```
src/app/features/
　└ <feature>/
　　├ <feature>.module.ts
　　├ <feature>-routing.module.ts
　　├ services/
　　│ └ <feature>.service.ts
　　└ components
　　　└ pages
　　　　├ some1-page
　　　　└ some2-page
```

`feature`には一つ以上のページが存在します。それらを`Pages`コンポーネントとして`pages`ディレクトリ内に作成します。



#### 2.2. Layoutsコンポーネント

#### 2.3. Presentersコンポーネント

### GUI Partsコンポーネント

### Styled GUI Parts コンポーネント

## その他

### MaterialDesignModule

### DirectivesModule

#### NoTagDirective
-->

## おわりに

今後は構築手順についても追記していきたいと考えています。
また、引き続きよりよい設計についても私なりに考えていきたいと思います。
一部でも参考になれば幸いです。