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
- Mulighed 1Link til dette afsnit
- SpørgsmålLink til dette afsnit
- Mulighed 2 – brug lazy loaded modulerLink til dette afsnit
- IssuesLink til dette afsnit
- Sådan blev det gjort med AngularJs og UI-RouterLink til dette afsnit
- KonklusionLink til dette afsnit
- EDIT:Link til dette afsnit
- Mulighed 3Link til dette afsnit
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.
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 AppComponent
s “router slot”, når brugeren navigerer til/login
, mens LoginComponent
vil blive gengivet i FooterOnlyLayoutComponent
s 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 AppComponent
s “router slot”, mens DashboardComponent
vil blive gengivet i MainLayoutComponent
s 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:
<>Copyconst routes: Routes = ;
men for dashboardet er det:
<>Copyconst 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:
<>CopyRouterModule.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