How to reuse common layouts in Angular using Router

私がこれまで取り組んできたほとんどの Web アプリは、異なるページで共通のレイアウトを使用する設計になっていました。 たとえば、ヘッダー、フッター、サイドバーからなるレイアウトはページごとに固定されており、コンテンツはページごとに異なります。 論理的には、共通部分を抽出して再利用しようとするものです。 AngularのドキュメントやPluralsightのコース、その他見つけた資料に基づいて、私は2つの可能な選択肢を思いつきました。 これらのオプションをよりよく説明するために、まずサンプル プロジェクトを定義します。

サンプル プロジェクトこのセクションへのリンク

たとえば、5 つのルート/ページ (ログイン、登録、ダッシュボード、ユーザー、アカウント設定) と 2 つのレイアウトがあるシンプルなアプリがあるとします。 コンテンツとフッターを持つレイアウト (これをレイアウト 1 と呼びます) と、ヘッダー、フッター、サイドバー、およびコンテンツを持つレイアウト 2 です。 また、ログインと登録ページにはレイアウト 1、その他にはレイアウト 2 があるとします。

Layout 1 – フッターのみのレイアウト
Layout 2 – メイン レイアウト

最後に、ページはアプリの個別の機能であると言うことができます。 機能ごとのプロジェクト構造を使用して、各機能は、対応するルーティング モジュールを備えた個別の Angular モジュールを持ちます。

Option 1このセクションへのリンク

(ここで遊ぶことができます)

Layout を個別のモジュール内のコンポーネントとして定義し、それを特定の機能ごとのルーティング モジュールの親コンポーネントとして使用します。

最初に、ルート コンポーネント テンプレート (通常は AppComponent) で、次のように <router-outlet> のみを使用します。

<>Copy
<div class="content" fxFlex> <router-outlet></router-outlet></div><app-footer></app-footer>

最後に、このレイアウトをログインルートに使用するには、次のようにルートを指定する必要があります。

<>Copy
...const routes: Routes = }];@NgModule({ imports: , exports: })export class LoginRoutingModule { }

この方法では、ユーザーが/login に移動すると、FooterOnlyLayoutComponentAppComponent の「ルーター スロット」に表示され、LoginComponentFooterOnlyLayoutComponent のルーター スロットに表示されることになります。 登録ページにFooterOnlyLayoutComponentを使用させるには、同じようにルートを定義し、ログインの代わりに登録パスとコンポーネントを指定します。

レイアウト 2 コンポーネント (MainLayoutComponent) では、次のテンプレートがあります:

<>Copy
<app-header fxLayout="column"></app-header><div fxLayout="row" fxFlex="100"> <app-sidebar fxLayout="column" fxFlex="300px"></app-sidebar> <div class="content" fxLayout="column" fxFlex> <router-outlet></router-outlet> </div></div><app-footer fxLayout="column"></app-footer>

ダッシュボード ページにこのレイアウトを使用するには、ダッシュボード ルーティング モジュールで次のようにルートを指定します。

<>Copy
...const routes: Routes = }];@NgModule({ imports: , exports: })export class DashboardRoutingModule { }

ここで、ユーザーが /dashboard に移動すると、MainLayoutComponentAppComponent の「ルーター スロット」に表示され、DashboardComponentMainLayoutComponent のルーター スロットに表示されるようになりました。 他のページでこのレイアウトを使用するには、対応するルーティングモジュールで同じようにルートを指定します。

以上です。 これで、複数のモジュール間でレイアウトを再利用できるようになりました。 ログインと登録のルートはレイアウト 1 (FooterOnlyLayoutComponent) を使用し、ダッシュボード、ユーザー、およびアカウント設定のルートはレイアウト 2 (MainLayoutComponent) を使用しています。

課題このセクションへのリンク

このアプローチの問題点は、ルートを変更するたびにレイアウトが不必要に再作成されることです。 レイアウト、ヘッダー、フッター、およびサイドバー コンポーネントのコンストラクターにコンソール ログを置くことで、それを確認することができます。 /dashboard ページに最初に行き、コンソールを確認し、/users ページに行くと、コンストラクターが 2 回呼び出されていることがわかります。

パフォーマンスへの影響以外に、ルート間で永続化する必要がある状態がある場合、これは別の層の複雑さをもたらします。 たとえば、ヘッダーに検索入力があり、ユーザーが何かを入力したとすると、ユーザーが別のページに切り替えたときに、ヘッダーが再作成され、入力がクリアされます。 もちろん、これは状態を何らかのストレージに永続化することで処理できますが、それでも不必要に複雑です。

Option 2 – use lazy loaded modulesこのセクションへのリンク

(You can play with it here)

ルーティングを備えた別のモジュールでコンポーネントとしてレイアウトを定義する。 そのモジュールを LayoutModule と呼ぶことにします。 すべての機能モジュールを LayoutModule 内で遅延ロードされる子モジュールとして定義します。

再び、ルートコンポーネントテンプレート (AppComponent) で <router-outlet> のみを使用します。 レイアウト1(FooterOnlyLayoutComponent)、レイアウト2(MainLayoutComponent)ともに、オプション1と同じテンプレートです。

機能モジュールをAppModuleでインポートしないようにします。 代わりに、LayoutRoutingModule:

<>Copy
…const routes: Routes = }, { path: '', component: FooterOnlyLayoutComponent, children: },];@NgModule({ imports: , exports: })export class LayoutRoutingModule { }

最後に、各機能モジュールのルーティングモジュールでは、空のパスとコンポーネントを使用するだけです。 たとえば、ログインの場合、ルートは次のようになります:

<>Copy
const routes: Routes = ;

一方ダッシュボードの場合:

<>Copy
const routes: Routes = ;

そして完了します。

他のルートがMainLayoutを使っているのにログインおよび登録は再びFooterOnlyLayoutComponentを使っていることになります。 しかし今回は、ルートを変更するたびにレイアウト、ヘッダー、フッター、サイドバーを再作成することは避けられました。 コンストラクタにコンソールログを再び置くと、異なるレイアウトからルート間を移動するときのみレイアウトが再作成されることがわかります。 /dashboard から /users に移動するとレイアウトは再作成されませんが、/dashboard から /login に移動すると再作成されます。

このセクションへの問題リンク

小さい問題は、すべての遅延ロード モジュールとそのベース パスを LayoutRoutingModule で定義しなければならず、大きなプロジェクトでは厄介になる可能性があることです。 より大きな問題は、遅延ロードを使用しなければならない一方で、使用したくない場合もあるということです。 遅延ロードを強制することなく、同様にレイアウトを再利用することが可能であるべきです。 私は、次のように loadChildren を指定することでこれを回避しようとしました:

<>Copy
...const routes: Routes = }, { path: '', component: FooterOnlyLayoutComponent, children: },];@NgModule({ imports: , exports: })export class LayoutRoutingModule { }

but this only works if you don’t use AOT, which is something we definitely want to use in production (https://github.com/angular/angular-cli/issues/4192).

別の可能な解決策は、次のように AppModule でプリロード戦略を指定することにより、すべての遅延ロード モジュールをプリロードすることです:

<>Copy
RouterModule.forRoot(, { preloadingStrategy: PreloadAllModules })

しかしこの方法ではモジュールが個別にバンドルされて、結局、複数のファイルがクライアントから取得する必要があり、おそらく望まないことでしょう。 また、これは、いくつかの特定のモジュールだけを遅延ロードしたい場合にも適切ではありません。 その場合、カスタムのプリロード戦略を書きたいかもしれませんが、各モジュールのファイルが残ってしまいます。

How this was done with AngularJs and UI-RouterLink to this section

(Try it out here)

これは AngularJs と UI-Router で、名前付きビューを使って簡単に達成できました。 そこで、まず抽象的なレイアウト状態を定義する必要があります。

<>Copy
$stateRegistry.register({ name: 'layout', abstract: true, views: { '@': { templateUrl: 'layout.html', }, 'header@layout': { component: 'header' }, 'sidebar@layout': { component: 'sidebar' }, 'content@layout': { template: '' }, 'footer@layout': { component: 'footer' } }});

次に layout.html:

<>Copy
<div class="flex-column" ui-view="header"></div><div class="flex-row flex-100"> <div class="flex-column" ui-view="sidebar"></div> <div class="flex-column flex" ui-view="content"></div></div><div class="flex-column" ui-view="footer"></app-footer>

そして実際のページの状態を定義するには、レイアウト状態を親にして特定の名前付きビュー(複数)をオーバーライドする必要があります。 したがって、ログイン状態は次のようになります:

<>Copy
$stateRegistry.register({ parent: 'layout', name: 'login', url: '/login', views: { 'content@layout': { component: 'login', }, 'header@layout': { component: '' }, 'sidebar@layout': { template: '' } }});

一方ダッシュボード状態は次のようになります:

<>Copy
$stateRegistry.register({ parent: 'layout', name: 'dashboard', url: '/dashboard', views: { 'content@layout': { component: 'dashboard', } }});

残りのページに対して状態を定義するには、同じパターンに従ってください。

それが終わったら、各コンポーネントの $onDestroy フックに console.log を追加して、ページ間を移動してみましょう。 /users/dashboardの間を移動しても、header、sidebar、footerが破壊されないことがわかる。 メイン レイアウトのページとフッターのみのレイアウトのページの間を移動する場合でも、フッターが再利用されていることがわかります。

Conclusion このセクションへのリンク

上記のように Angular ルーターで何らかのレイアウト再利用を実現できるとしても、両方のアプローチは少し「ハッキー」で痛みを伴うようです。 異なるレイアウト間で共有コンポーネントを再利用することさえ可能な UI ルーター、または React の動的ルーティングを使用すると、はるかに簡単にそれを達成することができます。

PS Angular ルーターでこれを処理する良い方法をご存知でしたら、コメントで共有してください 🙂

EDIT:Link to this section

Option 3Link to this section

コメントでアイデアを共有した Alexander Carls と Lars Gyrup Brink Nielsen に感謝して、上記の問題すべてを解決するオプション 3 を用意しました。 アイデアは、ルーター イベントをサブスクライブし、各 NavigationEnd イベントで、ルートに応じてレイアウトの一部を表示/非表示にすることです。 例:

例 1

例 2 (遅延ロードあり)

コミュニティで議論する