Jak ponownie wykorzystać wspólne układy w Angular używając Routera

Większość aplikacji internetowych, nad którymi pracowałem do tej pory, miała projekt, gdzie różne strony używają wspólnego układu. Na przykład układ, który składa się z nagłówka, stopki i paska bocznego, które są stałe dla każdej strony, oraz treść, która różni się w zależności od strony. Logicznym pomysłem jest próba wyodrębnienia i ponownego użycia wspólnych części. W oparciu o Angular docs, kursy Pluralsight i inne materiały, które znalazłem, wymyśliłem dwie możliwe opcje. Aby lepiej wyjaśnić te opcje, najpierw zdefiniujmy przykładowy projekt.

Przykładowy projektLink do tej sekcji

Powiedzmy, że mamy prostą aplikację, która ma 5 różnych tras/stron (login, rejestracja, dashboard, użytkownicy, ustawienia konta) i dwa układy. Jeden layout z treścią i stopką, nazwijmy go layout 1, oraz layout 2 z nagłówkiem, stopką, sidebarem i treścią. Powiedzmy też, że strony logowania i rejestracji mają layout 1, a pozostałe layout 2.

Layout 1 – layout tylko ze stopką
Layout 2 – layout główny

Ostatnio możemy powiedzieć, że nasze strony są osobnymi funkcjonalnościami aplikacji. Używając struktury projektu według cech, każda z naszych cech będzie miała osobny moduł Angulara z odpowiednim modułem routingu.

Opcja 1Link do tej sekcji

(Możesz się tym pobawić tutaj)

Layout jest zdefiniowany jako komponent w osobnym module, i użyj go jako komponentu nadrzędnego w module routingu każdej konkretnej cechy.

Najpierw w szablonie komponentu głównego (zwykle AppComponent) użyj tylko <router-outlet> jak:

<>Kopiuj
<router-outlet></router-outlet>

Następnie zdefiniuj FooterOnlyLayoutComponent komponent dla układu 1 z następującym szablonem:

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

Na koniec, aby użyć tego layoutu dla trasy logowania, należy określić trasę w następujący sposób:

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

W ten sposób, gdy użytkownik przejdzie do/login, FooterOnlyLayoutComponent zostanie wyrenderowany w „slocie routera” AppComponent, natomiast LoginComponent zostanie wyrenderowany w slocie routera FooterOnlyLayoutComponent. Aby strona rejestracji korzystała z FooterOnlyLayoutComponent, zdefiniuj trasę w ten sam sposób, podając jednak ścieżkę rejestracji oraz komponent zamiast loginu.

Dla komponentu layout 2 (MainLayoutComponent) mamy następujący szablon:

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

Aby użyć tego layoutu dla strony dashboardu, w module routingu dashboardu określ trasę w następujący sposób:

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

Teraz, gdy użytkownik przejdzie do /dashboard, MainLayoutComponent zostanie wyrenderowane w „slocie routera” AppComponent, natomiast DashboardComponent zostanie wyrenderowane w slocie routera MainLayoutComponent. Aby inne strony mogły korzystać z tego układu, należy określić ich trasy w ten sam sposób w odpowiednich modułach routingu.

To wszystko. Teraz byliśmy w stanie ponownie wykorzystać układy pomiędzy wieloma modułami. Trasy logowania i rejestracji używają layoutu 1 (FooterOnlyLayoutComponent), podczas gdy trasy pulpitu nawigacyjnego, użytkowników i ustawień konta używają layoutu 2 (MainLayoutComponent).

ZagadnieniaLink do tej sekcji

Problem z tym podejściem polega na tym, że layout jest niepotrzebnie odtwarzany przy każdej zmianie trasy. Możemy to sprawdzić umieszczając logi konsolowe w konstruktorach komponentów layout, header, footer i sidebar. Jeśli najpierw przejdziesz na stronę /dashboard, sprawdzisz konsolę, a następnie przejdziesz do /users, zobaczysz, że konstruktory są wywoływane dwukrotnie.

Oprócz implikacji wydajnościowych, przynosi to kolejną warstwę złożoności, jeśli istnieje jakiś stan, który musi być utrzymywany pomiędzy trasami. Załóżmy, że nasz nagłówek ma wejście wyszukiwania i użytkownik wpisał coś, kiedy przełączy się na inną stronę, nagłówek zostanie odtworzony, a wejście wyczyszczone. Oczywiście można to obsłużyć przez przechowywanie stanu w jakimś magazynie, ale to wciąż niepotrzebna złożoność.

Opcja 2 – użycie leniwie ładowanych modułówLink do tej sekcji

(Możesz się tym pobawić tutaj)

Zdefiniuj layout jako komponent w oddzielnym module z routingiem. Nazwijmy ten moduł LayoutModule. Zdefiniuj wszystkie moduły funkcji jako leniwie ładowane moduły potomne wewnątrz LayoutModule.

Ponownie, w szablonie komponentu głównego (AppComponent) użyj tylko <router-outlet>. Zarówno layout 1 (FooterOnlyLayoutComponent), jak i layout 2 (MainLayoutComponent) mają takie same szablony jak w opcji 1.

Nie importuj modułów funkcyjnych w AppModule. Zamiast tego, zaimportujemy je leniwie w LayoutRoutingModule:

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

Na koniec, w module routingu każdego modułu funkcyjnego po prostu użyj pustej ścieżki i komponentu. Na przykład dla logowania, trasy byłyby:

<>Copy
const routes: Routes = ;

natomiast dla dashboardu jest to:

<>Copy
const routes: Routes = ;

i gotowe.

Znowu logowanie i rejestracja używają FooterOnlyLayoutComponent podczas gdy inne trasy używają MainLayout. Tym razem jednak uniknęliśmy ponownego tworzenia layoutu, nagłówka, stopki i sidebara przy każdej zmianie trasy. Jeśli ponownie umieścisz logi konsoli w konstruktorach, zobaczysz, że teraz layouty są ponownie tworzone tylko wtedy, gdy nawigujesz pomiędzy trasami z różnych layoutów. Jeśli więc przejdziesz z /dashboard do /users, layout nie zostanie odtworzony, podczas gdy jeśli przejdziesz z /dashboard do /login, zostanie.

Problemy Link do tej sekcji

Mniejszym problemem jest to, że wszystkie leniwie ładowane moduły i ich ścieżki bazowe muszą być zdefiniowane w LayoutRoutingModule, więc może to stać się niechlujne dla większych projektów. Większym problemem jest to, że musimy używać leniwego ładowania, podczas gdy czasami może nie chcesz tego robić. Powinno być możliwe ponowne wykorzystanie układów w podobny sposób bez wymuszania leniwego ładowania modułów. Próbowałem obejść to, określając loadChildren w ten sposób:

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

ale to działa tylko wtedy, gdy nie używasz AOT, co jest czymś, czego zdecydowanie chcemy używać w produkcji (https://github.com/angular/angular-cli/issues/4192).

Innym możliwym rozwiązaniem byłoby wstępne załadowanie wszystkich leniwie ładowanych modułów przez określenie strategii preload w AppModule jak:

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

ale w tym przypadku moduły są pakowane oddzielnie i kończysz z wieloma plikami, które klient musi pobrać, co jest czymś, czego być może nie chcesz. Również to nie jest odpowiednie, jeśli chcesz leniwie ładować tylko niektóre określone moduły. W takim przypadku możesz chcieć napisać niestandardową strategię preload, ale nadal będziesz miał plik dla każdego modułu.

Jak to zostało zrobione z AngularJs i UI-RouterLink do tej sekcji

(Wypróbuj tutaj)

To było dużo łatwiejsze do osiągnięcia z AngularJs i UI-Router, używając nazwanych widoków. Tam najpierw musimy zdefiniować abstrakcyjny stan layoutu:

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

następnie 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>

i następnie podczas definiowania stanu dla aktualnej strony musimy użyć stanu layoutu jako rodzica i nadpisać konkretny nazwany widok(y). Tak więc stanem logowania będzie:

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

natomiast stanem pulpitu nawigacyjnego będzie:

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

Aby zdefiniować stan dla pozostałych stron wystarczy postępować według tego samego schematu.

Jak już to zrobimy, dodajmy console.log do haka $onDestroy każdego komponentu i nawigujmy pomiędzy stronami. Widzimy, że header, sidebar i footer nie są niszczone podczas nawigacji pomiędzy /users a /dashboard. Nawet gdy będziemy nawigować pomiędzy stroną z głównym układem a stroną z samym układem stopki, zauważymy, że stopka jest ponownie używana.

ZakończenieLink do tej sekcji

Mimo że możliwe jest osiągnięcie pewnego rodzaju ponownego użycia układu za pomocą routera Angulara, jak opisano powyżej, oba podejścia wydają się nieco „hacky” i bolesne. Dużo łatwiej jest to osiągnąć za pomocą UI Routera, gdzie jesteśmy nawet w stanie ponownie wykorzystać współdzielone komponenty pomiędzy różnymi layoutami, lub za pomocą dynamicznego routingu Reacta.

PS jeśli znasz jakiś lepszy sposób na obsługę tego w Angularowym routerze, podziel się nim w komentarzach 🙂

EDIT:Link do tej sekcji

Opcja 3Link do tej sekcji

Dzięki Alexandrowi Carlsowi i Larsowi Gyrupowi Brink Nielsenowi, którzy podzielili się swoimi pomysłami w komentarzach, mamy opcję 3, która rozwiązuje wszystkie wyżej wymienione problemy. Idea polega na subskrybowaniu zdarzeń routera, a następnie na każdym zdarzeniu NavigationEnd można pokazać/ukryć fragmenty układu w zależności od trasy. Przykłady:

Przykład 1

Przykład 2 (z leniwym ładowaniem)

Dyskutuj ze społecznością