Como reutilizar layouts comuns em Angular usando Router

A maior parte das aplicações web em que trabalhei até agora, tinha um design onde páginas diferentes estão usando layout comum. Por exemplo, um layout que consiste em cabeçalho, rodapé e barra lateral, que são fixos para cada página, e o conteúdo que varia de acordo com a página. A idéia lógica é tentar extrair e reutilizar partes comuns. Com base nos documentos Angulares, cursos Pluralsight e outros materiais que encontrei, encontrei duas opções possíveis. Para melhor explicar essas opções, vamos primeiro definir o exemplo project.

Example ProjectLink para esta secção

Vamos dizer que temos uma aplicação simples que tem 5 rotas/páginas diferentes (login, registo, dashboard, utilizadores, definições de conta) e dois layouts. Um layout com conteúdo e rodapé, vamos chamá-lo de layout 1, e o layout 2 com cabeçalho, rodapé, barra lateral e conteúdo. Digamos também que as páginas de login e registro têm layout 1, enquanto outras têm layout 2.

Layout 1 – apenas rodapé layout
Layout 2 – layout principal

O último, podemos dizer que nossas páginas são características separadas do aplicativo. Usando pasta por estrutura de projeto de característica, cada uma de nossas características terá um módulo angular separado com módulo de roteamento correspondente.

Opção 1Link para esta seção

(Você pode jogar com ele aqui)

Layout é definido como um componente em um módulo separado, e usá-lo como um componente pai no módulo de roteamento de cada característica específica.

Primeiro no template do componente raiz (geralmente AppComponent) use apenas <router-outlet> like:

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

Então defina FooterOnlyLayoutComponent componente para o layout 1 com o seguinte template:

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

Finalmente, para usar este layout para a rota de login, a rota tem de ser especificada como:

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

Desta forma, quando o usuário navegar para /login, FooterOnlyLayoutComponent será renderizado no “slot do roteador” de AppComponent, enquanto LoginComponent será renderizado no “slot do roteador” de FooterOnlyLayoutComponent. Para fazer a página de registo use FooterOnlyLayoutComponent, defina a rota da mesma forma enquanto fornece o caminho e componente do registo em vez do login.

Para o layout 2 componente (MainLayoutComponent) temos o seguinte template:

<>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 usar este layout para a página do painel, no módulo de roteamento do painel especifique a rota desta forma:

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

Agora, quando o usuário navegar para /dashboard, MainLayoutComponent será renderizado no “slot do roteador” de AppComponent, enquanto DashboardComponent será renderizado no “slot do roteador” de MainLayoutComponent. Para que outras páginas usem este layout, especifique suas rotas da mesma forma em seus módulos de roteamento correspondentes.

É isso aí. Agora nós conseguimos reutilizar o layout entre vários módulos. As rotas de login e registro estão usando o layout 1 (FooterOnlyLayoutComponent), enquanto o painel de controle, usuários e configurações de conta estão usando o layout 2 (MainLayoutComponent).

IssuesLink para esta seção

Problema com esta abordagem é que o layout é desnecessariamente recriado em cada mudança de rota. Podemos verificar isso colocando os logs do console nos construtores do layout, cabeçalho, rodapé e componente da barra lateral. Se você primeiro for a página /dashboard, verificar console, e depois ir para /users, você verá que construtores são chamados duas vezes.

Além das implicações de desempenho, isto traz outra camada de complexidade se houver algum estado que precisa ser persistido entre as rotas. Digamos que nosso cabeçalho tem uma entrada de busca e o usuário digita algo, quando ele muda para outra página, o cabeçalho será recriado e a entrada será limpa. Claro que isto pode ser tratado persistindo o estado para algum armazenamento mas isso ainda é complexidade desnecessária.

Opção 2 – use módulos carregados preguiçososLink para esta secção

(Pode jogar com ele aqui)

Definir layout como um componente num módulo separado com roteamento. Vamos chamar esse módulo de LayoutModule. Defina todos os módulos de características como módulos filhos preguiçosos dentro de LayoutModule.

Again, no modelo do componente raiz (AppComponent) use apenas <router-outlet>. Ambos layout 1 (FooterOnlyLayoutComponent) e layout 2 (MainLayoutComponent) têm os mesmos templates como na opção 1.

Não importar módulos de características no AppModule. Em vez disso, vamos importá-los preguiçosamente no módulo LayoutRoutingModule:

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

Lastly, no módulo de roteamento de cada módulo de recurso basta usar o caminho vazio e o componente. Por exemplo, para login, as rotas seriam:

<>Copiar
const routes: Routes = ;

enquanto que para o painel é:

<>Copiar
const routes: Routes = ;

e já está.

Again login and registration are using FooterOnlyLayoutComponent enquanto que outras rotas estão usando MainLayout. No entanto desta vez evitamos recriar layout, cabeçalho, rodapé e barra lateral em cada mudança de rota. Se você colocar os logs do console nos construtores novamente você verá que agora os layouts são recriados somente quando você navega entre rotas de diferentes layouts. Então se você navegar de /dashboard a /users layout não será recriado, enquanto que se você for de /dashboard a /login irá.

IssuesLink a esta seção

Smaller problema é que todos os módulos preguiçosos carregados e seus caminhos base têm que ser definidos em LayoutRoutingModule para que possa se tornar confuso para projetos maiores. O maior problema é que nós temos que usar carregamento preguiçoso enquanto às vezes talvez você não queira. Deve ser possível reutilizar layouts de forma similar sem forçar módulos carregados preguiçosamente. Eu tentei contornar isso especificando loadChildren like this:

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

mas isso só funciona se você não usar AOT, que é algo que definitivamente queremos usar na produção (https://github.com/angular/angular-cli/issues/4192).

Outra solução possível seria pré-carregar todos os módulos carregados preguiçosamente especificando a estratégia de pré-carga em AppModule como:

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

mas com estes módulos são empacotados separadamente e você acaba com múltiplos arquivos que o cliente precisa buscar, o que é algo que você talvez não queira. Isto também não é apropriado se você quiser carregar preguiçosamente apenas alguns módulos específicos. Nesse caso você pode querer escrever uma estratégia de pré-carga personalizada, mas você ainda vai acabar com um arquivo para cada módulo.

Como isso foi feito com AngularJs e UI-RouterLink para essa seção

(Experimente aqui)

Isso foi muito mais fácil de conseguir com AngularJs e UI-Router, usando vistas nomeadas. Lá nós precisamos primeiro definir o estado do layout abstrato:

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

then layout.html:

<>Copiar
<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 então ao definir o estado para a página real, você precisa usar o estado do layout como um pai e sobrepor a(s) vista(s) nomeada(s) específica(s). Então o estado de login seria:

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

enquanto o estado do painel de controle seria:

<>Copy

Para definir o estado para o resto das páginas basta seguir o mesmo padrão.

Após isso ser feito, vamos adicionar console.log ao $onDestroy gancho de cada componente e navegar entre páginas. Podemos ver que o cabeçalho, barra lateral e rodapé não são destruídos ao navegar entre /users e /dashboard. Mesmo quando navegamos entre página com layout principal e página com apenas rodapé, notaremos que o rodapé é reutilizado.

ConclusionLink para esta seção

Even embora seja possível conseguir algum tipo de reutilização de layout com o roteador Angular, como descrito acima, ambas as abordagens parecem um pouco “hacky” e dolorosas. É muito mais fácil consegui-lo com o UI Router, onde somos capazes até de reutilizar componentes compartilhados entre diferentes layouts, ou o roteamento dinâmico do React.

PS se você conhece alguma maneira melhor de lidar com isso com o roteador Angular, por favor compartilhe nos comentários 🙂

EDIT:Link para esta seção

Option 3Link para esta seção

Obrigado a Alexander Carls e Lars Gyrup Brink Nielsen, que compartilharam suas idéias nos comentários, nós temos uma opção 3 que resolve todos os problemas mencionados acima. A ideia é subscrever os eventos do router e depois em cada evento NavigationEnd pode mostrar/ocultar peças do layout dependendo da rota. Exemplos:

Exemplo 1

Exemplo 2 (com carregamento preguiçoso)

Discutir com a comunidade