Hur man återanvänder gemensamma layouter i Angular med hjälp av Router

De flesta av de webbapplikationer som jag hittills arbetat med hade en design där olika sidor använde en gemensam layout. Till exempel layout som består av header, footer och sidebar, som är fasta för varje sida, och innehållet som varierar beroende på sida. Den logiska idén är att försöka extrahera och återanvända gemensamma delar. Baserat på Angular-dokumentationen, Pluralsight-kurserna och annat material som jag hittade kom jag fram till två möjliga alternativ. För att bättre förklara dessa alternativ ska vi först definiera exempelprojektet.

ExempelprojektLänk till det här avsnittet

Vi kan säga att vi har en enkel app som har fem olika vägar/sidor (inloggning, registrering, instrumentbräda, användare, kontoinställningar) och två layouter. En layout med innehåll och sidfot, låt oss kalla den layout 1, och layout 2 med rubrik, sidfot, sidebar och innehåll. Låt oss också säga att inloggnings- och registreringssidorna har layout 1, medan de andra har layout 2.

Layout 1 – endast layout för sidfot
Layout 2 – huvudlayout

Det sista kan vi säga att våra sidor är separata funktioner i appen. Med hjälp av en projektstruktur med mappar per funktion kommer var och en av våra funktioner att ha en separat Angular-modul med motsvarande routningsmodul.

Option 1Länk till det här avsnittet

(Du kan leka med det här)

Layout definieras som en komponent i en separat modul och används som en överordnad komponent i routningsmodulen för varje specifik funktion.

Först i rotkomponentmallen (vanligtvis AppComponent) används endast <router-outlet> som:

<>Copy
<router-outlet></router-outlet>

Därefter definieras FooterOnlyLayoutComponent komponent för layout 1 med följande mall:

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

För att använda den här layouten för inloggningsrutten måste rutten anges på följande sätt:

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

När användaren navigerar till /login kommer FooterOnlyLayoutComponent att visas i AppComponents ”router slot”, medan LoginComponent visas i FooterOnlyLayoutComponents router slot. Om du vill att registreringssidan ska använda FooterOnlyLayoutComponent definierar du rutten på samma sätt, men anger registreringssökväg och komponent i stället för inloggning.

För komponent i layout 2 (MainLayoutComponent) har vi följande mall:

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

För att använda den här layouten för instrumentbrädselssidan anger du i rutningsmodulen för instrumentbrädan rutten så här:

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

När användaren navigerar till /dashboard kommer MainLayoutComponent att visas i AppComponents ”router slot”, medan DashboardComponent visas i MainLayoutComponents router slot. Om du vill att andra sidor ska använda den här layouten anger du deras rutter på samma sätt i deras motsvarande routingmoduler.

Det var allt. Nu kunde vi återanvända layout mellan flera moduler. Rutter för inloggning och registrering använder layout 1 (FooterOnlyLayoutComponent), medan rutter för instrumentpanel, användare och kontoinställningar använder layout 2 (MainLayoutComponent).

SakerLänk till det här avsnittet

Problemet med det här tillvägagångssättet är att layouten återskapas i onödan vid varje ruttändring. Vi kan kontrollera detta genom att lägga in konsolloggar i konstruktörerna för komponenterna layout, header, footer och sidebar. Om du först går till sidan /dashboard, kontrollerar konsolen och sedan går till /users kommer du att se att konstruktörerna anropas två gånger.

Och förutom prestandakonsekvenserna medför detta ytterligare ett lager av komplexitet om det finns något tillstånd som måste bevaras mellan rutter. Låt oss säga att vår rubrik har en sökinmatning och att användaren har skrivit in något, när han byter till en annan sida kommer rubriken att återskapas och inmatningen rensas. Naturligtvis kan detta hanteras genom att persistera tillståndet till något lagringsutrymme, men det är fortfarande onödig komplexitet.

Option 2 – använd lazy loaded-modulerLänk till det här avsnittet

(Du kan leka med det här)

Definiera layout som en komponent i en separat modul med routing. Låt oss kalla den modulen LayoutModule. Definiera alla funktionsmoduler som lata laddade barnmoduler inuti LayoutModule.

I rotkomponentmallen (AppComponent) används endast <router-outlet>. Både layout 1 (FooterOnlyLayoutComponent) och layout 2 (MainLayoutComponent) har samma mallar som i alternativ 1.

Importera inte funktionsmoduler i AppModule. Istället kommer vi att importera dem slentrianmässigt i LayoutRoutingModule:

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

Sluttligen, i routingmodulen för varje funktionsmodul använder du bara tom sökväg och komponenten. Till exempel för inloggning skulle rutterna vara:

<>Copy
const routes: Routes = ;

men för instrumentpanelen är det:

<>Copy
const routes: Routes = ;

och vi är klara.

Inloggning och registrering använder FooterOnlyLayoutComponent medan andra rutter använder MainLayout. Den här gången undvek vi dock att återskapa layout, header, footer och sidebar vid varje ruttändring. Om du lägger in konsolloggar i konstruktörerna igen kommer du att se att nu återskapas layouter endast när du navigerar mellan rutter från olika layouter. Så om du navigerar från /dashboard till /users kommer layouten inte att återskapas, medan den kommer att återskapas om du går från /dashboard till /login.

SakerLänk till det här avsnittet

Ett mindre problem är att alla lata inlästa moduler och deras basvägar måste definieras i LayoutRoutingModule så det kan bli rörigt för större projekt. Större problem är att vi måste använda lazy loading medan man ibland kanske inte vill det. Det borde vara möjligt att återanvända layouter på liknande sätt utan att tvinga fram lazy loaded-moduler. Jag försökte kringgå detta genom att specificera loadChildren så här:

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

men detta fungerar bara om du inte använder AOT, vilket är något vi definitivt vill använda i produktion (https://github.com/angular/angular-cli/issues/4192).

En annan möjlig lösning skulle vara att förinläsa alla lata laddade moduler genom att ange förinläsningsstrategi i AppModule som:

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

men med detta paketeras modulerna separat och det slutar med att du får flera filer som klienten måste hämta, vilket är något som du kanske inte vill ha. Detta är inte heller lämpligt om du vill lazy load endast vissa specifika moduler. I det fallet kanske du vill skriva en anpassad förladdningsstrategi, men du kommer fortfarande att få en fil för varje modul.

Hur detta gjordes med AngularJs och UI-RouterLänk till det här avsnittet

(Prova här)

Det här var mycket lättare att uppnå med AngularJs och UI-Router, med hjälp av namngivna vyer. Där måste vi först definiera abstrakt layouttillstånd:

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

och sedan 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>

och sedan, när du definierar tillståndet för den faktiska sidan, måste du använda layouttillståndet som överordnad och åsidosätta specifika namngivna vyer. Så login state skulle vara:

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

medan dashboard state skulle vara:

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

För att definiera state för resten av sidorna följer du bara samma mönster.

När detta är gjort lägger vi till console.log till $onDestroy hook för varje komponent och navigerar mellan sidorna. Vi kan se att header, sidebar och footer inte förstörs när vi navigerar mellan /users och /dashboard. Även när vi navigerar mellan sidan med huvudlayout och sidan med endast footer-layout märker vi att footer återanvänds.

SlutsatsLänk till det här avsnittet

Även om det är möjligt att åstadkomma någon form av återanvändning av layouten med Angular-router, enligt beskrivningen ovan, verkar båda tillvägagångssätten lite ”hackiga” och smärtsamma. Det är mycket enklare att uppnå det med UI Router, där vi till och med kan återanvända delade komponenter mellan olika layouter, eller Reacts dynamiska routing.

PS om du vet något bättre sätt att hantera detta med Angular router, dela gärna med dig i kommentarerna 🙂

EDIT:Länka till det här avsnittet

Option 3Länka till det här avsnittet

Tack vare Alexander Carls och Lars Gyrup Brink Nielsen som delade med sig av sina idéer i kommentarerna, har vi nu ett alternativ 3 som löser alla de problem som nämns ovan. Idén är att prenumerera på routerhändelserna och sedan på varje NavigationEnd-händelse kan du visa/dölja delar av layouten beroende på rutten. Exempel:

Exempel 1

Exempel 2 (med lazy loading)

Diskutera med samhället