Cómo reutilizar layouts comunes en Angular usando Router

La mayoría de las aplicaciones web en las que he trabajado hasta ahora, tenían un diseño en el que las diferentes páginas utilizan un layout común. Por ejemplo el layout que consta de cabecera, pie de página y sidebar, que son fijos para cada página, y el contenido que varía según la página. La idea lógica es tratar de extraer y reutilizar las partes comunes. Basándome en los documentos de Angular, los cursos de Pluralsight y otros materiales que encontré, se me ocurrieron dos posibles opciones. Para explicar mejor esas opciones, definamos primero el proyecto de ejemplo.

Proyecto de ejemploEnlace a esta sección

Digamos que tenemos una app sencilla que tiene 5 rutas/páginas diferentes (login, registro, dashboard, usuarios, configuración de cuenta) y dos layouts. Un diseño con contenido y pie de página, llamémoslo diseño 1, y diseño 2 con cabecera, pie de página, barra lateral y contenido. También digamos que las páginas de inicio de sesión y de registro tienen el diseño 1, mientras que las otras tienen el diseño 2.

Diseño 1 – diseño sólo a pie de página
Diseño 2 – diseño principal

El último, podemos decir que nuestras páginas son características separadas de la aplicación. Usando la estructura del proyecto por carpetas, cada una de nuestras características tendrá un módulo Angular separado con su correspondiente módulo de enrutamiento.

Opción 1Enlace a esta sección

(Puedes jugar con ella aquí)

El layout se define como un componente en un módulo separado, y lo usamos como componente padre en el módulo de enrutamiento de cada característica específica.

Primero en la plantilla del componente raíz (normalmente AppComponent) utiliza sólo <router-outlet> como:

<>Copia
<router-outlet></router-outlet>

Luego define el componente FooterOnlyLayoutComponent para el layout 1 con la siguiente plantilla:

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

Por último, para utilizar este layout para la ruta de inicio de sesión, hay que especificar la ruta como:

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

De esta forma, cuando el usuario navegue a/login, FooterOnlyLayoutComponent se renderizará en la «ranura del router» de AppComponent, mientras que LoginComponent se renderizará en la ranura del router de FooterOnlyLayoutComponent. Para que la página de registro utilice FooterOnlyLayoutComponent, defina la ruta de la misma manera, pero proporcionando la ruta de registro y el componente en lugar del inicio de sesión.

Para el componente de diseño 2 (MainLayoutComponent) tenemos la siguiente plantilla:

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

Para utilizar este diseño para la página del tablero de instrumentos, en el módulo de enrutamiento del tablero de instrumentos especifique la ruta así:

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

Ahora, cuando el usuario navegue a /dashboard, MainLayoutComponent se renderizará en la «ranura del router» de AppComponent, mientras que DashboardComponent se renderizará en la ranura del router de MainLayoutComponent. Para hacer que otras páginas utilicen esta disposición, especifique sus rutas de la misma manera en sus módulos de enrutamiento correspondientes.

Eso es todo. Ahora hemos sido capaces de reutilizar el diseño entre múltiples módulos. Las rutas de inicio de sesión y registro están usando el layout 1 (FooterOnlyLayoutComponent), mientras que las rutas de tablero, usuarios y configuración de cuenta están usando el layout 2 (MainLayoutComponent).

IssuesLink to this section

El problema con este enfoque es que el layout se recrea innecesariamente en cada cambio de ruta. Podemos comprobarlo poniendo registros de consola en los constructores del componente layout, header, footer y sidebar. Si primero vas a la página /dashboard, compruebas la consola, y luego vas a la /users, verás que los constructores son llamados dos veces.

Además de las implicaciones de rendimiento, esto trae otra capa de complejidad si hay algún estado que necesita ser persistido entre rutas. Digamos que nuestra cabecera tiene una entrada de búsqueda y el usuario escribió algo, cuando cambia a otra página, la cabecera será recreada y la entrada borrada. Por supuesto, esto puede ser manejado por la persistencia de estado a algunos de almacenamiento, pero eso sigue siendo una complejidad innecesaria.

Opción 2 – utilizar módulos cargados perezosamenteEnlace a esta sección

(Usted puede jugar con él aquí)

Defina el diseño como un componente en un módulo separado con enrutamiento. Llamemos a ese módulo LayoutModule. Defina todos los módulos de características como módulos hijos cargados perezosamente dentro de LayoutModule.

De nuevo, en la plantilla del componente raíz (AppComponent) utilice sólo <router-outlet>. Tanto el layout 1 (FooterOnlyLayoutComponent) como el layout 2 (MainLayoutComponent) tienen las mismas plantillas que en la opción 1.

No importar módulos de características en el AppModule. En su lugar, los importaremos perezosamente en el LayoutRoutingModule:

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

Por último, en el módulo de rutas de cada módulo de características sólo hay que utilizar la ruta vacía y el componente. Por ejemplo para el login, las rutas serían:

<>Copy
const routes: Routes = ;

mientras que para el dashboard es:

<>Copy
const routes: Routes = ;

y ya está.

De nuevo el login y el registro usan FooterOnlyLayoutComponent mientras que las otras rutas usan MainLayout. Sin embargo esta vez evitamos recrear el diseño, la cabecera, el pie de página y la barra lateral en cada cambio de ruta. Si vuelves a poner los registros de la consola en los constructores verás que ahora los layouts se recrean sólo cuando navegas entre rutas desde diferentes layouts. Así que si navegas de /dashboard a /users el diseño no se recreará, mientras que si vas de /dashboard a /login sí lo hará.

IssuesLink to this section

El problema más pequeño es que todos los módulos cargados perezosamente y sus rutas base tienen que ser definidos en LayoutRoutingModule por lo que puede llegar a ser un lío para los proyectos más grandes. El mayor problema es que tenemos que usar lazy loading mientras que a veces quizás no quieras hacerlo. Debería ser posible reutilizar los diseños de manera similar sin forzar los módulos de carga perezosa. Intenté evitar esto especificando loadChildren así:

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

pero esto sólo funciona si no se usa AOT, que es algo que definitivamente queremos usar en producción (https://github.com/angular/angular-cli/issues/4192).

Otra posible solución sería precargar todos los módulos cargados perezosamente especificando la estrategia de precarga en AppModule como:

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

pero con esto los módulos se agrupan por separado y terminas con múltiples archivos que el cliente necesita recuperar, lo cual es algo que quizás no quieres. Además, esto no es apropiado si usted quiere cargar perezosamente sólo algunos módulos específicos. En ese caso es posible que quieras escribir una estrategia de precarga personalizada, pero seguirás teniendo archivos para cada módulo.

Cómo se hizo esto con AngularJs y UI-RouterLink a esta sección

(Pruébalo aquí)

Esto fue mucho más fácil de lograr con AngularJs y UI-Router, utilizando vistas con nombre. Allí primero tenemos que definir el estado de diseño abstracto:

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

luego layout.html:

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

y luego cuando se define el estado para la página real es necesario utilizar el estado de diseño como padre y anular la(s) vista(s) con nombre específico. Así que el estado de inicio de sesión sería:

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

mientras que el estado del tablero sería:

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

Para definir el estado para el resto de las páginas sólo hay que seguir el mismo patrón.

Una vez hecho esto, vamos a añadir console.log al hook $onDestroy de cada componente y a navegar entre páginas. Podemos ver que la cabecera, la barra lateral y el pie de página no se destruyen al navegar entre /users y /dashboard. Incluso cuando navegamos entre la página con el diseño principal y la página con el diseño del pie de página nos daremos cuenta de que el pie de página se reutiliza.

ConclusiónEnlace a esta sección

Aunque es posible lograr algún tipo de reutilización del diseño con el router de Angular, como se ha descrito anteriormente, ambos enfoques parecen un poco «hacky» y doloroso. Es mucho más fácil conseguirlo con UI Router, donde incluso somos capaces de reutilizar componentes compartidos entre diferentes layouts, o el enrutamiento dinámico de React.

PS si conoces alguna forma mejor de manejar esto con el router de Angular, por favor comparte en los comentarios 🙂

EDIT:Enlace a esta sección

Opción 3Enlace a esta sección

Gracias a Alexander Carls y Lars Gyrup Brink Nielsen, que compartieron sus ideas en los comentarios, tenemos una opción 3 que resuelve todos los problemas mencionados anteriormente. La idea es suscribirse a los eventos del router y luego en cada evento NavigationEnd puedes mostrar/ocultar trozos del diseño dependiendo de la ruta. Ejemplos:

Ejemplo 1

Ejemplo 2 (con lazy loading)

Discute con la comunidad