Cum să reutilizezi layout-uri comune în Angular folosind Router

Majoritatea aplicațiilor web la care am lucrat până acum, au avut un design în care diferite pagini folosesc un layout comun. De exemplu, layout-ul care constă în antet, footer și sidebar, care sunt fixe pentru fiecare pagină, iar conținutul care variază în funcție de pagină. Ideea logică este de a încerca să extragem și să reutilizăm părțile comune. Pe baza documentelor Angular, a cursurilor Pluralsight și a altor materiale pe care le-am găsit, am ajuns la două opțiuni posibile. Pentru a explica mai bine aceste opțiuni, să definim mai întâi proiectul de exemplu.

Proiect de exempluLink la această secțiune

Să spunem că avem o aplicație simplă care are 5 rute/pagini diferite (login, înregistrare, tablou de bord, utilizatori, setări cont) și două layout-uri. Un layout cu conținut și footer, să îl numim layout 1, și layout 2 cu header, footer, sidebar și conținut. De asemenea, să spunem că paginile de logare și înregistrare au layout 1, în timp ce celelalte au layout 2.

Layout 1 – layout doar de subsol
Layout 2 – layout principal

În cele din urmă, putem spune că paginile noastre sunt caracteristici separate ale aplicației. Folosind structura proiectului folder by feature, fiecare dintre caracteristicile noastre va avea un modul Angular separat cu modulul de rutare corespunzător.

Opțiunea 1Link to this section

(You can play with it here)

Layout-ul este definit ca o componentă într-un modul separat și se utilizează ca o componentă părinte în modulul de rutare al fiecărei caracteristici specifice.

În primul rând în șablonul componentei rădăcină (de obicei AppComponent) folosiți doar <router-outlet> ca:

<>Copiază
<router-outlet></router-outlet>

Apoi definiți componenta FooterOnlyLayoutComponent pentru aspectul 1 cu următorul șablon:

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

În cele din urmă, pentru a utiliza acest layout pentru ruta de conectare, trebuie specificată ruta ca:

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

În acest fel, atunci când utilizatorul navighează către/login, FooterOnlyLayoutComponent va fi redat în „slotul de router” al lui AppComponent, în timp ce LoginComponent va fi redat în slotul de router al lui FooterOnlyLayoutComponent. Pentru ca pagina de înregistrare să utilizeze FooterOnlyLayoutComponent, definiți traseul în același mod, dar furnizați calea de înregistrare și componenta în loc de login.

Pentru componenta layout 2 (MainLayoutComponent) avem următorul șablon:

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

Pentru a utiliza acest layout pentru pagina tabloului de bord, în modulul de rutare a tabloului de bord specificați ruta astfel:

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

Acum, atunci când utilizatorul navighează către /dashboard, MainLayoutComponent va fi redat în „slotul de rutare” al lui AppComponent, în timp ce DashboardComponent va fi redat în slotul de rutare al lui MainLayoutComponent. Pentru a face ca și alte pagini să folosească această dispunere, specificați rutele lor în același mod în modulele de rutare corespunzătoare.

Așa este. Acum am reușit să reutilizăm aspectul între mai multe module. Rutele de autentificare și înregistrare folosesc layout-ul 1 (FooterOnlyLayoutComponent), în timp ce rutele pentru tabloul de bord, utilizatori și setări de cont folosesc layout-ul 2 (MainLayoutComponent).

IssuesLink către această secțiune

Problema cu această abordare este că layout-ul este recreat în mod inutil la fiecare schimbare de rută. Putem verifica acest lucru punând jurnale de consolă în constructorii componentei layout, header, footer și sidebar. Dacă mergeți mai întâi la pagina /dashboard, verificați consola și apoi mergeți la /users, veți vedea că constructorii sunt apelați de două ori.

În afară de implicațiile asupra performanței, acest lucru aduce un alt nivel de complexitate dacă există o anumită stare care trebuie să fie persistată între rute. Să presupunem că antetul nostru are o intrare de căutare și utilizatorul a tastat ceva, atunci când trece la o altă pagină, antetul va fi recreat și intrarea va fi ștearsă. Bineînțeles că acest lucru poate fi gestionat prin persistența stării într-o anumită stocare, dar aceasta este încă o complexitate inutilă.

Opțiunea 2 – folosiți module cu încărcare leneșăLink to this section

(You can play with it here)

Defineți aspectul ca o componentă într-un modul separat cu rutare. Să numim acel modul LayoutModule. Definiți toate modulele de caracteristici ca module copii încărcate leneș în interiorul LayoutModule.

Din nou, în șablonul componentei rădăcină (AppComponent) folosiți doar <router-outlet>. Atât layout-ul 1 (FooterOnlyLayoutComponent), cât și layout-ul 2 (MainLayoutComponent) au aceleași șabloane ca în opțiunea 1.

Nu importați module de caracteristici în AppModule. În schimb, le vom importa leneș în LayoutRoutingModule:

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

În ultimul rând, în modulul de rutare al fiecărui modul de caracteristici folosiți doar calea goală și componenta. De exemplu, pentru login, rutele ar fi:

<>Copiază
const routes: Routes = ;

în timp ce pentru tabloul de bord este:

<>Copiază
const routes: Routes = ;

și am terminat.

Din nou, login și înregistrare folosesc FooterOnlyLayoutComponent în timp ce celelalte rute folosesc MainLayout. Cu toate acestea, de data aceasta am evitat să recreăm layout-ul, header-ul, footer-ul și sidebar-ul la fiecare schimbare de traseu. Dacă puneți din nou jurnalele de consolă în constructori, veți vedea că acum machetele sunt recreate din nou doar atunci când navigați între rute din machete diferite. Astfel, dacă navigați de la /dashboard la /users layout-ul nu va fi recreat, în timp ce dacă treceți de la /dashboard la /login va fi recreat.

IssuesLink către această secțiune

Problema mai mică este că toate modulele încărcate leneș și căile de bază ale acestora trebuie să fie definite în LayoutRoutingModule, astfel încât poate deveni dezordonat pentru proiectele mari. Problema mai mare este că trebuie să folosim încărcarea leneșă în timp ce uneori poate că nu doriți să o faceți. Ar trebui să fie posibilă refolosirea machetelor în mod similar fără a forța modulele lazy loaded. Am încercat să ocolesc acest lucru prin specificarea loadChildren astfel:

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

dar acest lucru funcționează numai dacă nu folosiți AOT, ceea ce este ceva ce vrem cu siguranță să folosim în producție (https://github.com/angular/angular-cli/issues/4192).

O altă soluție posibilă ar fi să preîncărcați toate modulele încărcate leneș prin specificarea strategiei de preîncărcare în AppModule, cum ar fi:

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

dar cu această soluție modulele sunt grupate separat și se ajunge la mai multe fișiere pe care clientul trebuie să le recupereze, ceea ce este ceva ce poate nu doriți. De asemenea, acest lucru nu este adecvat dacă doriți să încărcați leneș doar anumite module specifice. În acest caz, este posibil să doriți să scrieți o strategie de preîncărcare personalizată, dar tot veți ajunge să aveți un fișier pentru fiecare modul.

Cum s-a realizat acest lucru cu AngularJs și UI-RouterLink la această secțiune

(Încercați aici)

Acesta a fost mult mai ușor de realizat cu AngularJs și UI-Router, folosind vizualizări numite. Acolo trebuie mai întâi să definim starea abstractă a layout-ului:

<>Copiați
$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' } }});

apoi layout.html:

<>Copiați
<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>

și apoi, atunci când definiți starea pentru pagina reală, trebuie să folosiți starea layout-ului ca părinte și să suprascrieți vizualizarea (vizualizările) numită(e) specifică(e). Astfel, starea de conectare ar fi:

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

în timp ce starea tabloului de bord ar fi:

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

Pentru a defini starea pentru restul paginilor, urmați același model.

După ce acest lucru este făcut, să adăugăm console.log la cârligul $onDestroy al fiecărei componente și să navigăm între pagini. Putem vedea că header-ul, bara laterală și footer-ul nu sunt distruse atunci când navigăm între /users și /dashboard. Chiar și atunci când navigăm între pagina cu layout principal și pagina cu layout doar pentru footer vom observa că footer-ul este reutilizat.

ConcluzieLink către această secțiune

Chiar dacă este posibil să realizăm un anumit tip de reutilizare a layout-ului cu Angular router, așa cum am descris mai sus, ambele abordări par un pic „hacky” și dureroase. Este mult mai ușor de realizat cu UI Router, unde suntem chiar capabili să reutilizăm componente partajate între diferite layout-uri, sau cu rutarea dinamică a lui React.

PS dacă știți o modalitate mai bună de a gestiona acest lucru cu Angular router, vă rugăm să împărtășiți în comentarii 🙂

EDIT:Link către această secțiune

Opțiunea 3Link către această secțiune

Grație lui Alexander Carls și Lars Gyrup Brink Nielsen, care și-au împărtășit ideile în comentarii, avem o opțiune 3 care rezolvă toate problemele menționate mai sus. Ideea este să vă abonați la evenimentele routerului și apoi, la fiecare eveniment NavigationEnd, puteți afișa/ascunde bucăți din layout în funcție de traseu. Exemple:

Exemplu 1

Exemplu 2 (cu încărcare leneșă)

Discutați cu comunitatea