Come riutilizzare i layout comuni in Angular usando Router

La maggior parte delle applicazioni web su cui ho lavorato finora, aveva un design in cui diverse pagine usano un layout comune. Per esempio il layout che consiste di intestazione, piè di pagina e barra laterale, che sono fissi per ogni pagina, e il contenuto che varia a seconda della pagina. L’idea logica è quella di cercare di estrarre e riutilizzare le parti comuni. Sulla base dei documenti di Angular, dei corsi di Pluralsight e di altri materiali che ho trovato, mi sono venute in mente due possibili opzioni. Per spiegare meglio queste opzioni, definiamo prima il progetto di esempio.

Progetto di esempioLink a questa sezione

Diciamo di avere una semplice applicazione che ha 5 diversi percorsi/pagine (login, registrazione, dashboard, utenti, impostazioni account) e due layout. Un layout con contenuto e piè di pagina, chiamiamolo layout 1, e il layout 2 con intestazione, piè di pagina, barra laterale e contenuto. Diciamo anche che le pagine di login e registrazione hanno il layout 1, mentre le altre hanno il layout 2.

Layout 1 – layout solo piè di pagina
Layout 2 – layout principale

L’ultimo, possiamo dire che le nostre pagine sono funzionalità separate dell’app. Usando la struttura del progetto cartella per caratteristica, ciascuna delle nostre caratteristiche avrà un modulo Angular separato con il modulo di routing corrispondente.

Opzione 1Link a questa sezione

(Puoi giocarci qui)

Il layout è definito come un componente in un modulo separato, e usarlo come componente padre nel modulo di routing di ogni specifica caratteristica.

Prima nel template del componente root (di solito AppComponent) usa solo <router-outlet> come:

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

Poi definisci FooterOnlyLayoutComponent componente per il layout 1 con il seguente template:

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

Infine, per utilizzare questo layout per il percorso di login, il percorso deve essere specificato come:

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

In questo modo, quando l’utente naviga verso/login, FooterOnlyLayoutComponent sarà reso nello “slot router” di AppComponent, mentre LoginComponent sarà reso nello slot router di FooterOnlyLayoutComponent. Per fare in modo che la pagina di registrazione usi FooterOnlyLayoutComponent, definisci la rotta allo stesso modo, ma fornendo il percorso e il componente di registrazione al posto del login.

Per il componente del layout 2 (MainLayoutComponent) abbiamo il seguente template:

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

Per utilizzare questo layout per la pagina del dashboard, nel modulo di routing del dashboard specificare la rotta in questo modo:

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

Ora, quando l’utente naviga verso /dashboard, MainLayoutComponent sarà reso nello “slot router” di AppComponent, mentre DashboardComponent sarà reso nello slot router di MainLayoutComponent. Per far sì che altre pagine usino questo layout, specificate i loro percorsi allo stesso modo nei loro corrispondenti moduli di routing.

Ecco fatto. Ora siamo stati in grado di riutilizzare il layout tra più moduli. I percorsi di login e registrazione stanno usando il layout 1 (FooterOnlyLayoutComponent), mentre i percorsi della dashboard, degli utenti e delle impostazioni dell’account stanno usando il layout 2 (MainLayoutComponent).

IssuesLink a questa sezione

Il problema con questo approccio è che il layout viene ricreato inutilmente ad ogni cambio di percorso. Possiamo verificarlo mettendo i log della console nei costruttori del componente layout, header, footer e sidebar. Se si va prima nella pagina /dashboard, si controlla la console, e poi si va nella /users, si vedrà che i costruttori vengono chiamati due volte.

Oltre alle implicazioni sulle prestazioni questo porta un altro livello di complessità se c’è qualche stato che deve essere persistito tra le rotte. Diciamo che la nostra intestazione ha un input di ricerca e l’utente ha digitato qualcosa, quando passa ad un’altra pagina, l’intestazione sarà ricreata e l’input cancellato. Naturalmente questo può essere gestito persistendo lo stato in qualche memoria, ma è comunque una complessità non necessaria.

Opzione 2 – usare moduli caricati pigramenteLink a questa sezione

(Potete giocarci qui)

Definire il layout come un componente in un modulo separato con il routing. Chiamiamo quel modulo LayoutModule. Definite tutti i moduli delle caratteristiche come moduli figli caricati pigramente all’interno di LayoutModule.

Anche in questo caso, nel template del componente principale (AppComponent) usate solo <router-outlet>. Sia il layout 1 (FooterOnlyLayoutComponent) che il layout 2 (MainLayoutComponent) hanno gli stessi template come nell’opzione 1.

Non importare i moduli delle caratteristiche nel AppModule. Invece, li importeremo pigramente nel LayoutRoutingModule:

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

Infine, nel modulo di routing di ogni modulo di funzionalità basta usare un percorso vuoto e il componente. Per esempio per il login, le rotte sarebbero:

<>Copy
const routes: Routes = ;

mentre per la dashboard è:

<>Copy
const routes: Routes = ;

e abbiamo finito. Tuttavia questa volta abbiamo evitato di ricreare layout, header, footer e sidebar ad ogni cambio di rotta. Se mettete di nuovo i log della console nei costruttori vedrete che ora i layout vengono ricreati solo quando si naviga tra le rotte da layout diversi. Quindi se navighi da /dashboard a /users il layout non verrà ricreato, mentre se vai da /dashboard a /login sì.

IssuesLink a questa sezione

Un problema minore è che tutti i moduli caricati pigramente e i loro percorsi di base devono essere definiti in LayoutRoutingModule quindi può diventare disordinato per progetti più grandi. Il problema più grande è che dobbiamo usare il caricamento pigro mentre a volte forse non si vuole. Dovrebbe essere possibile riutilizzare i layout in modo simile senza forzare i moduli caricati pigramente. Ho provato ad aggirare questo problema specificando loadChildren in questo modo:

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

ma questo funziona solo se non si usa AOT, che è qualcosa che vogliamo assolutamente usare in produzione (https://github.com/angular/angular-cli/issues/4192).

Un’altra possibile soluzione sarebbe quella di precaricare tutti i moduli caricati pigramente specificando la strategia di precarica in AppModule come:

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

ma con questo i moduli vengono impacchettati separatamente e si finisce per avere più file che il client deve recuperare, cosa che forse non si vuole. Inoltre questo non è appropriato se volete caricare pigramente solo alcuni moduli specifici. In questo caso potreste voler scrivere una strategia di precaricamento personalizzata, ma vi ritroverete comunque con un file per ogni modulo.

Come è stato fatto con AngularJs e UI-RouterLink a questa sezione

(Provalo qui)

Questo è stato molto più facile da realizzare con AngularJs e UI-Router, usando le viste con nome. Lì dobbiamo prima definire lo stato astratto del layout:

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

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

e poi quando si definisce lo stato per la pagina attuale è necessario utilizzare lo stato del layout come genitore e sovrascrivere le specifiche named view. Quindi lo stato di login sarebbe:

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

mentre lo stato di dashboard sarebbe:

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

Per definire lo stato per il resto delle pagine basta seguire lo stesso schema.

Una volta fatto questo, aggiungiamo console.log al gancio $onDestroy di ogni componente e navighiamo tra le pagine. Possiamo vedere che header, sidebar e footer non vengono distrutti quando si naviga tra /users e /dashboard. Anche quando navighiamo tra la pagina con il layout principale e la pagina con il solo layout del piè di pagina, noteremo che il piè di pagina viene riutilizzato.

ConclusioneLink a questa sezione

Anche se è possibile ottenere un certo tipo di riutilizzo del layout con Angular router, come descritto sopra, entrambi gli approcci sembrano un po’ “hacky” e dolorosi. È molto più facile ottenerlo con UI Router, dove siamo persino in grado di riutilizzare componenti condivisi tra diversi layout, o con il routing dinamico di React.

PS se conosci un modo migliore per gestire questo con Angular router, per favore condividi nei commenti 🙂

EDIT:Link a questa sezione

Opzione 3Link a questa sezione

Grazie ad Alexander Carls e Lars Gyrup Brink Nielsen, che hanno condiviso le loro idee nei commenti, abbiamo un’opzione 3 che risolve tutti i problemi di cui sopra. L’idea è di sottoscrivere gli eventi del router e poi su ogni evento NavigationEnd è possibile mostrare/nascondere pezzi del layout a seconda del percorso. Esempi:

Esempio 1

Esempio 2 (con lazy loading)

Discuti con la comunità