Page-Layout-Presenter構造
フロントエンド開発において最適なコンポーネント設計について考えることは避けては通れないと考えています。
未熟ながら私もいくつか考えてみました。今回はその中で、ある程度形になったものを一つ提案したいと思います。
今回提案する設計では、チームでの開発を想定し、長期的に破綻せず運用でき、修正に強く、直感的であることを目指しました。
少しでもAngularの開発の手助けになればと思っています。
考えた経緯については別記事にまとめてあります。ご興味のある方は併せてご覧ください。
⇒ 【フロントエンド】最適なコンポーネント設計について考えてみた
設計の全体像
参照構造
詳細を説明する前に、大まかにでも全体を見ていただきたいので、実装した際のコンポーネントなどの参照構造をまずは図で紹介します。
赤色は@NgModule
、黄色は@Component
、水色は@Injectable
を持つクラスを指します。
少し複雑に見えるかもしれませんが、詳細は後ほど説明します。
ディレクトリ構造
次にディレクトリ構造を紹介します。
これについても詳細は後ほど説明します。
なお、拡張子が.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
から参照されるということです。
このルーティングの参照関係を図にすると次のようになります。
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
でのみ注入します。- アプリケーションの表示に関わる具体的なデータのことを「状態」と呼びます。
- これは、ほかの方の設計などで見られる
Container Component
の役割と言えますが、Container Component
はページに対して一つとされる場合が多いので、今回は冗長性を減らすためにPage Components
に取り込んでいます。 - 状態管理には
NgRx
の利用が考えられますが、コストが高いため最初はNgRx
を利用せず、サービスのみで開発するという方法も考えられます。NgRx
やInterceptor
については勉強中のため、今回は説明を省略させていただきます。 - これについて図で表すと次のようになります。
- 子コンポーネントから
@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
を図にすると次のようになります。
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
-
Presenter Components
は状態を持ちません。- ただし、コンポーネント内で完結するような状態は除きます。
-
GUI Parts Component
にて、親コンポーネントから状態を受け取る@Input()
と親コンポーネントへイベントを返す@Output()
を記述し、Styled GUI Parts Components
ではそれを継承します。 -
Presenter Components
は一度作ればほかのアプリケーションにも移植でき、その意味でも再利用性が高いです。
これらのことを踏まえると、コンポーネントの親子関係の図は次のようになります。
また、モジュールやサービスも含め、以上のことをまとめると、記事冒頭にある図と同じになります。
おわりに
今後は構築手順についても追記していきたいと考えています。
また、引き続きよりよい設計についても私なりに考えていきたいと思います。
一部でも参考になれば幸いです。