Sådan genbruges fælles layouts i Angular ved hjælp af Router

De fleste af de webapps, jeg har arbejdet på indtil videre, havde et design, hvor forskellige sider bruger et fælles layout. For eksempel layout, som består af header, footer og sidebar, som er fast for hver side, og indholdet, som varierer fra side til side. Den logiske idé er at forsøge at udtrække og genbruge fælles dele. Baseret på Angular docs, Pluralsight-kurser og andre materialer, jeg fandt, kom jeg frem til to mulige muligheder. For bedre at forklare disse muligheder, lad os først definere eksempelprojektet.

EksempelprojektLink til dette afsnit

Lad os sige, at vi har en simpel app, som har 5 forskellige ruter/sider (login, registrering, dashboard, brugere, kontoindstillinger) og to layouts. Et layout med indhold og sidefod, lad os kalde det layout 1, og layout 2 med header, sidefod, sidebar og indhold. Lad os også sige, at login- og registreringssiderne har layout 1, mens de andre har layout 2.

Layout 1 – kun footer-layout
Layout 2 – hovedlayout

Det sidste kan vi sige, at vores sider er separate funktioner i appen. Ved hjælp af projektstruktur med mappe efter funktion vil hver af vores funktioner have et separat Angular-modul med et tilsvarende routingmodul.

Mulighed 1Link til dette afsnit

(Du kan lege med det her)

Layout defineres som en komponent i et separat modul, og brug det som en overordnet komponent i routingmodulet for hver specifik funktion.

Først i rodkomponentskabelonen (normalt AppComponent) bruges kun <router-outlet> som:

<>Kopier
<router-outlet></router-outlet>

Dernæst defineres FooterOnlyLayoutComponent komponent til layout 1 med følgende skabelon:

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

Finalt skal ruten angives som følger for at bruge dette layout til login-ruten:

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

Finalt skal ruten angives som følger:

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

Sådan vil FooterOnlyLayoutComponent blive gengivet i AppComponents “router slot”, når brugeren navigerer til/login, mens LoginComponent vil blive gengivet i FooterOnlyLayoutComponents router slot. Hvis du vil have registreringssiden til at bruge FooterOnlyLayoutComponent, skal du definere ruten på samme måde, mens du angiver registreringssti og komponent i stedet for login.

For layout 2-komponenten (MainLayoutComponent) har vi følgende skabelon:

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

For at bruge dette layout til dashboard-siden skal du i dashboard-routingmodulet angive rute på følgende måde:

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

Nu, når brugeren navigerer til /dashboard, vil MainLayoutComponent blive gengivet i AppComponents “router slot”, mens DashboardComponent vil blive gengivet i MainLayoutComponents router slot. Hvis du vil have andre sider til at bruge dette layout, skal du angive deres ruter på samme måde i deres tilsvarende routing-moduler.

Det var det. Nu var vi i stand til at genbruge layout mellem flere moduler. Ruterne login og registrering bruger layout 1 (FooterOnlyLayoutComponent), mens ruterne dashboard, brugere og kontoindstillinger bruger layout 2 (MainLayoutComponent).

SpørgsmålLink til dette afsnit

Problemet med denne fremgangsmåde er, at layoutet unødigt genskabes ved hver ruteændring. Vi kan kontrollere det ved at indsætte konsollogfiler i konstruktørerne for layout-, header-, footer- og sidebar-komponenten. Hvis du først går til /dashboard-siden, tjekker konsollen og derefter går til /users, vil du se, at konstruktørerne kaldes to gange.

Afhængig af konsekvenserne for ydeevnen medfører dette endnu et lag af kompleksitet, hvis der er noget tilstand, der skal persisteres mellem ruter. Lad os sige, at vores header har et søgeinput, og brugeren har indtastet noget, når han skifter til en anden side, vil header blive genskabt og input ryddet. Selvfølgelig kan dette håndteres ved at persistere tilstand til noget lager, men det er stadig unødvendig kompleksitet.

Mulighed 2 – brug lazy loaded modulerLink til dette afsnit

(Du kan lege med det her)

Definér layout som en komponent i et separat modul med routing. Lad os kalde dette modul LayoutModule. Definer alle funktionsmoduler som lazy loaded børnemoduler inden for LayoutModule.

I rodkomponentskabelonen (AppComponent) skal du igen kun bruge <router-outlet>. Både layout 1 (FooterOnlyLayoutComponent) og layout 2 (MainLayoutComponent) har samme skabeloner som i mulighed 1.

Importer ikke funktionsmoduler i AppModule. I stedet importerer vi dem dovent i LayoutRoutingModule:

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

Sidst, i routingmodulet for hvert funktionsmodul skal du blot bruge tom sti og komponenten. For eksempel for login ville ruterne være:

<>Copy
const routes: Routes = ;

men for dashboardet er det:

<>Copy
const routes: Routes = ;

og vi er færdige.

Login og registrering bruger FooterOnlyLayoutComponent, mens andre ruter bruger MainLayout. Denne gang undgik vi dog at genskabe layout, header, footer og sidebar ved hver ruteændring. Hvis du sætter konsollogs ind i konstruktørerne igen, vil du se, at nu genskabes layouts kun, når du navigerer mellem ruter fra forskellige layouts. Så hvis du navigerer fra /dashboard til /users vil layout ikke blive genskabt, mens det vil det, hvis du går fra /dashboard til /login.

IssuesLink til dette afsnit

Mindre problem er, at alle lazy loaded moduler og deres basisstier skal defineres i LayoutRoutingModule, så det kan blive rodet for større projekter. Større problem er, at vi er nødt til at bruge lazy loading, mens man nogle gange måske ikke ønsker det. Det burde være muligt at genbruge layouts på samme måde uden at tvinge lazy loaded moduler. Jeg forsøgte at gå udenom dette ved at specificere loadChildren på denne måde:

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

men det virker kun, hvis du ikke bruger AOT, hvilket er noget, vi helt sikkert ønsker at bruge i produktion (https://github.com/angular/angular-cli/issues/4192).

En anden mulig løsning ville være at forloade alle lazy loaded moduler ved at angive preload-strategi i AppModule som:

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

men med dette bliver modulerne bundlet separat, og du ender med flere filer, som klienten skal hente, hvilket er noget, du måske ikke ønsker. Dette er heller ikke hensigtsmæssigt, hvis du kun ønsker at lazy load kun nogle specifikke moduler. I det tilfælde kan du måske skrive en brugerdefineret preload-strategi, men du vil stadig ende op med en fil for hvert modul.

Sådan blev det gjort med AngularJs og UI-RouterLink til dette afsnit

(Prøv det her)

Dette var meget nemmere at opnå med AngularJs og UI-Router, ved hjælp af navngivne visninger. Der skal vi først definere abstrakt layouttilstand:

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

derpå layout.html:

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

og derefter, når du definerer tilstand for den faktiske side, skal du bruge layouttilstand som overordnet og tilsidesætte specifikke navngivne visning(er). Så login state ville være:

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

men dashboard state ville være:

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

For at definere state for resten af siderne skal du bare følge det samme mønster.

Når det er gjort, skal vi tilføje console.log til $onDestroy hooket for hver komponent og navigere mellem siderne. Vi kan se, at header, sidebar og footer ikke bliver ødelagt, når vi navigerer mellem /users og /dashboard. Selv når vi navigerer mellem side med hovedlayout og side med kun footer-layout, vil vi bemærke, at footer genbruges.

KonklusionLink til dette afsnit

Selv om det er muligt at opnå en form for genbrug af layout med Angular-router, som beskrevet ovenfor, virker begge tilgange lidt “hacky” og smertefulde. Det er meget nemmere at opnå det med UI Router, hvor vi endda er i stand til at genbruge delte komponenter mellem forskellige layouts, eller React’s dynamiske routing.

PS hvis du kender en bedre måde at håndtere dette med Angular router, så del venligst i kommentarerne 🙂

EDIT:Link til dette afsnit

Mulighed 3Link til dette afsnit

Takket være Alexander Carls og Lars Gyrup Brink Nielsen, som delte deres ideer i kommentarerne, har vi en mulighed 3, som løser alle de problemer, der er nævnt ovenfor. Idéen er at abonnere på routerbegivenhederne, og så kan du på hver NavigationEnd-begivenhed vise/skjule dele af layoutet afhængigt af ruten. Eksempler:

Eksempel 1

Eksempel 2 (med lazy loading)

Diskuter med fællesskabet