How to reuse common layouts in Angular using Router

Die meisten der Webanwendungen, an denen ich bisher gearbeitet habe, hatten ein Design, bei dem verschiedene Seiten ein gemeinsames Layout verwenden. Zum Beispiel ein Layout, das aus Kopfzeile, Fußzeile und Seitenleiste besteht, die für jede Seite festgelegt sind, und der Inhalt, der je nach Seite variiert. Die logische Idee ist, zu versuchen, gemeinsame Teile zu extrahieren und wiederzuverwenden. Basierend auf den Angular-Dokumenten, Pluralsight-Kursen und anderen Materialien, die ich gefunden habe, habe ich zwei mögliche Optionen gefunden. Um diese Optionen besser zu erklären, definieren wir zunächst ein Beispielprojekt

BeispielprojektLink zu diesem Abschnitt

Angenommen, wir haben eine einfache App, die 5 verschiedene Routen/Seiten (Login, Registrierung, Dashboard, Benutzer, Kontoeinstellungen) und zwei Layouts hat. Ein Layout mit Inhalt und Fußzeile, nennen wir es Layout 1, und Layout 2 mit Kopfzeile, Fußzeile, Seitenleiste und Inhalt. Sagen wir auch, dass die Anmelde- und Registrierungsseiten Layout 1 haben, während die anderen Layout 2 haben.

Layout 1 – nur Fußzeile
Layout 2 – Hauptlayout

Das letzte können wir sagen, dass unsere Seiten separate Funktionen der App sind. Unter Verwendung der Projektstruktur „Ordner nach Features“ wird jedes unserer Features ein separates Angular-Modul mit dem entsprechenden Routing-Modul haben.

Option 1Link zu diesem Abschnitt

(Sie können hier damit spielen)

Layout wird als Komponente in einem separaten Modul definiert und als übergeordnete Komponente im Routing-Modul für jedes spezifische Feature verwendet.

Zuerst in der Root-Komponentenvorlage (normalerweise AppComponent) nur <router-outlet> wie folgt verwenden:

<>Kopieren
<router-outlet></router-outlet>

Dann die FooterOnlyLayoutComponent Komponente für Layout 1 mit folgender Vorlage definieren:

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

Um dieses Layout schließlich für die Login-Route zu verwenden, muss die Route wie folgt angegeben werden:

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

Auf diese Weise wird, wenn der Benutzer zu /login navigiert, FooterOnlyLayoutComponent im „Router-Slot“ von AppComponent dargestellt, während LoginComponent im Router-Slot von FooterOnlyLayoutComponent dargestellt wird. Damit die Registrierungsseite FooterOnlyLayoutComponent verwendet, definieren Sie die Route auf die gleiche Weise, wobei Sie den Registrierungspfad und die Komponente anstelle des Logins angeben.

Für die Komponente Layout 2 (MainLayoutComponent) haben wir die folgende Vorlage:

<>Kopieren
<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>

Um dieses Layout für die Dashboard-Seite zu verwenden, geben Sie im Dashboard-Routing-Modul die Route wie folgt an:

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

Wenn der Benutzer nun zu /dashboard navigiert, wird MainLayoutComponent im „Router-Slot“ von AppComponent gerendert, während DashboardComponent im Router-Slot von MainLayoutComponent gerendert wird. Damit auch andere Seiten dieses Layout verwenden, geben Sie deren Routen auf die gleiche Weise in den entsprechenden Routing-Modulen an.

Das war’s. Jetzt konnten wir das Layout zwischen mehreren Modulen wiederverwenden. Die Routen für die Anmeldung und Registrierung verwenden Layout 1 (FooterOnlyLayoutComponent), während die Routen für das Dashboard, die Benutzer und die Kontoeinstellungen Layout 2 (MainLayoutComponent) verwenden.

IssuesLink zu diesem Abschnitt

Das Problem bei diesem Ansatz ist, dass das Layout bei jeder Routenänderung unnötig neu erstellt wird. Wir können das überprüfen, indem wir Konsolenprotokolle in die Konstruktoren der Layout-, Header-, Footer- und Sidebar-Komponente einfügen. Wenn Sie zuerst die /dashboard-Seite aufrufen, die Konsole überprüfen und dann zur /users-Seite gehen, werden Sie sehen, dass die Konstruktoren zweimal aufgerufen werden.

Abgesehen von den Auswirkungen auf die Leistung bringt dies eine weitere Ebene der Komplexität mit sich, wenn es einen Zustand gibt, der zwischen den Routen erhalten bleiben muss. Nehmen wir an, unsere Kopfzeile hat eine Sucheingabe und der Benutzer hat etwas eingetippt. Wenn er zu einer anderen Seite wechselt, wird die Kopfzeile neu erstellt und die Eingabe gelöscht. Natürlich kann dies durch Persistieren des Zustands in einem Speicher gehandhabt werden, aber das ist immer noch unnötige Komplexität.

Option 2 – lazy loaded modulesLink zu diesem Abschnitt

(Sie können hier damit spielen)

Definieren Sie das Layout als eine Komponente in einem separaten Modul mit Routing. Nennen wir dieses Modul LayoutModule. Definieren Sie alle Feature-Module als „lazy loaded“ Kindermodule innerhalb von LayoutModule.

Auch in der Stammkomponentenvorlage (AppComponent) verwenden Sie nur <router-outlet>. Sowohl Layout 1 (FooterOnlyLayoutComponent) als auch Layout 2 (MainLayoutComponent) haben die gleichen Vorlagen wie in der Option 1.

Importieren Sie keine Feature-Module in der AppModule. Stattdessen importieren wir sie faul in der LayoutRoutingModule:

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

Zuletzt, im Routing-Modul jedes Feature-Moduls einfach leeren Pfad und die Komponente verwenden. Zum Beispiel für die Anmeldung wären die Routen:

<>Kopieren
const routes: Routes = ;

während für das Dashboard gilt:

<>Kopieren
const routes: Routes = ;

und wir sind fertig.

Auch hier verwenden Anmeldung und Registrierung FooterOnlyLayoutComponent, während andere Routen MainLayout verwenden. Diesmal haben wir jedoch vermieden, Layout, Kopfzeile, Fußzeile und Seitenleiste bei jeder Änderung der Route neu zu erstellen. Wenn Sie die Konsolenprotokolle erneut in die Konstruktoren einfügen, werden Sie sehen, dass die Layouts jetzt nur dann neu erstellt werden, wenn Sie zwischen Routen aus verschiedenen Layouts navigieren. Wenn man also von /dashboard zu /users navigiert, wird das Layout nicht neu erstellt, während es bei einem Wechsel von /dashboard zu /login neu erstellt wird.

IssuesLink zu diesem Abschnitt

Das kleinere Problem ist, dass alle „lazy loaded“ Module und ihre Basispfade in LayoutRoutingModule definiert werden müssen, was bei größeren Projekten unübersichtlich werden kann. Ein größeres Problem ist, dass wir „Lazy Loading“ verwenden müssen, obwohl man das vielleicht gar nicht will. Es sollte möglich sein, Layouts auf ähnliche Weise wiederzuverwenden, ohne lazy loaded Module zu erzwingen. Ich habe versucht, dies zu umgehen, indem ich loadChildren wie folgt spezifiziert habe:

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

aber das funktioniert nur, wenn man kein AOT verwendet, was wir in der Produktion auf jeden Fall verwenden wollen (https://github.com/angular/angular-cli/issues/4192).

Eine andere mögliche Lösung wäre es, alle „lazy loaded“ Module vorzuladen, indem man eine Vorladestrategie in AppModule angibt, wie:

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

aber damit werden die Module separat gebündelt und man hat am Ende mehrere Dateien, die der Client holen muss, was man vielleicht nicht will. Außerdem ist dies nicht geeignet, wenn Sie nur einige bestimmte Module nachladen wollen. In diesem Fall kann man eine eigene Preload-Strategie schreiben, aber man hat dann immer noch eine Datei für jedes Modul.

Wie dies mit AngularJs und UI-Router gemacht wurdeLink zu diesem Abschnitt

(Probieren Sie es hier aus)

Dies war mit AngularJs und UI-Router viel einfacher zu erreichen, indem man benannte Ansichten verwendet. Dort müssen wir zuerst einen abstrakten Layout-Status definieren:

<>Kopieren
$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' } }});

dann layout.html:

<>Kopieren
<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>

und dann, wenn wir den Status für die eigentliche Seite definieren, müssen wir den Layout-Status als Elternteil verwenden und spezifische benannte Ansichten überschreiben. Der Login-Status wäre also:

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

während der Dashboard-Status wäre:

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

Um den Status für den Rest der Seiten zu definieren, folgen Sie einfach demselben Muster.

Nachdem das erledigt ist, fügen wir console.log zum $onDestroy-Hook der einzelnen Komponenten hinzu und navigieren zwischen den Seiten. Wir können sehen, dass Kopfzeile, Seitenleiste und Fußzeile nicht zerstört werden, wenn wir zwischen /users und /dashboard navigieren. Selbst wenn wir zwischen der Seite mit dem Hauptlayout und der Seite mit dem reinen Fußzeilenlayout navigieren, werden wir feststellen, dass die Fußzeile wiederverwendet wird.

AbschlussLink zu diesem Abschnitt

Auch wenn es möglich ist, eine Art von Layout-Wiederverwendung mit dem Angular-Router zu erreichen, wie oben beschrieben, scheinen beide Ansätze ein wenig „hacky“ und schmerzhaft. Es ist viel einfacher, es mit UI Router zu erreichen, wo wir sogar in der Lage sind, gemeinsame Komponenten zwischen verschiedenen Layouts wiederzuverwenden, oder mit Reacts dynamischem Routing.

PS, wenn Sie einen besseren Weg kennen, dies mit Angular Router zu handhaben, teilen Sie bitte in den Kommentaren 🙂

EDIT:Link zu diesem Abschnitt

Option 3Link zu diesem Abschnitt

Dank Alexander Carls und Lars Gyrup Brink Nielsen, die ihre Ideen in den Kommentaren geteilt haben, haben wir eine Option 3, die alle oben genannten Probleme löst. Die Idee ist, die Router-Ereignisse zu abonnieren und dann bei jedem NavigationEnd-Ereignis Teile des Layouts in Abhängigkeit von der Route ein-/auszublenden. Beispiele:

Beispiel 1

Beispiel 2 (mit Lazy Loading)

Diskutieren Sie mit der Community