Struktura aplikacji Angular

Kiedy zaczynałem swoją przygodę z frameworkiem Angular, tym co szczególnie przykuło moją uwagę podczas przeglądania różnorakich szkieletów aplikacji, był brak spójnej struktury plików i katalogów.

Czasem wszystkie komponenty dedykowane jednemu zadaniu były trzymane w jednym katalogu, czasem były rozdzielane na podkatalogi. To samo dotyczyło pipe’ów czy serwisów. Czasem były trzymane w jednym miejscu z komponentami, czasem wydzielane do osobnego folderu.

Pewne posunięcia tego typu można sobie wytłumaczyć, np. komponent listy artykułów i komponent pojedynczego artykułu są ze sobą ściśle powiązane, więc czemu nie trzymać ich razem. Problem był wtedy, gdy w tej samej aplikacji było mi dane znaleźć kilka koncepcji.

Postanowiłem więc zebrać swoje spostrzeżenia w tym zakresie i przygotować szkielet, który będzie wewnętrznie spójny.

Oto on:

  • src
    • app
      • api
        • interceptor
          • rest
            • jwt.interceptor.ts
            • request.interceptor.ts
            • response.interceptor.ts
            • response-logger.interceptor.ts
        • model
          • auth.api.model.ts
          • user.api.model.ts
          • article.api.model.ts
          • user-permission.api.model.ts
        • rest
          • article.rest.service.[ts|spec.ts]
          • auth.rest.service.[ts|spec.ts]
          • user.rest.service.[ts|spec.ts]
          • user-permission.service.[ts|spec.ts]
        • soap
        • service
          • rest.service.ts
          • soap.service.ts
        • api.module.ts
      • core
        • layout
          • admin
            • component
              • footer
                • footer.component.[html|scss|spec.ts|ts]
              • header
                • header.component.[html|scss|spec.ts|ts]
              • layout
                • layout.component.[html|scss|spec.ts|ts]
              • main
                • main.component.[html|scss|spec.ts|ts]
          • client
            • component
              • footer
                • footer.component.[html|scss|spec.ts|ts]
              • header
                • header.component.[html|scss|spec.ts|ts]
              • layout
                • layout.component.[html|scss|spec.ts|ts]
              • main
                • main.component.[html|scss|spec.ts|ts]
        • core.module.ts
        • core-routing.module.ts
      • shared
        • component
          • modal
            • modal.component.[html|scss|spec.ts|ts]
          • button
            • button.component.[html|scss|spec.ts|ts]
          • breadcrumb
            • breadcrumb.component.[html|scss|spec.ts|ts]
          • navbar
            • navbar.component.[html|scss|spec.ts|ts]
        • directive
        • pipe
          • date-formatter.pipe.ts
        • shared.module.ts
      • features
        • auth
          • component
            • login-form
              • login-form.component.[html|scss|spec.ts|ts]
            • logout-button
              • logout-button.component.[html|scss|spec.ts|ts]
          • page
            • register
              • data-provider
                • register.data-provider.ts
              • model
                • user.view.model.ts
              • register.component.[html|scss|spec.ts|ts]
          • service
          • auth.module.ts
          • auth-routing.module.ts
        • article
          • component
            • popular
              • component
                • item
                  • item.component.[html|scss|spec.ts|ts]
              • data-provider
                • articles.data-provider.ts
              • model
                • article.view.model.ts
              • popular.[html|scss|spec.ts|ts]
            • last-added
              • component
                • item
                  • item.component.[html|scss|spec.ts|ts]
              • data-provider
                • articles.data-provider.ts
              • model
                • article.view.model.ts
              • last-added.component.[html|scss|spec.ts|ts]
          • page
            • list - component - item - component - image - image.component.[html|scss|spec.ts|ts] - item.component.[html|scss|spec.ts|ts]
              • data-provider
                • articles.data-provider.ts
              • model
                • article.view.model.ts
              • list.component.[html|scss|spec.ts|ts]
            • show
              • show.component.[html|scss|spec.ts|ts]
            • popular
              • popular.component.[html|scss|spec.ts|ts]
          • service
          • article.module.ts
          • article-routing.module.ts
      • app.component.html
      • app.component.ts
      • app.module.ts
      • app-routing.module.ts
    • config
    • environments
    • i18n
      • pl
        • core.json
        • article.json
      • en
        • core.json
        • article.json
    • styles

Podstawowe założenia

  1. Nie jesteśmy w stanie zdefiniować, jak głęboka będzie struktura zagnieżdżeń komponentów, więc każdy komponent powinien mieć taką samą strukturę jak moduł. Krótko mówiąc, jeśli komponent posiada dedykowany mu serwis, w katalogu komponentu umieszczamy katalog service i dopiero wewnątrz niego umieszczamy klasę serwisu. W ten sposób tworzymy coś na kształt struktury drzewa, której wzorcem jest układ katalogów modułu.
  2. Zgodnie z powyższym, umiejscowienie danej klasy zależy od jej roli w aplikacji. Jeśli jakiś pipe użytkowany jest wyłącznie przez jeden komponent, jego miejsce jest w katalogu module-name/component/component-name/pipe. Jeśli przydaje się w kilku komponentach danego modułu, umieszczamy go w katalogu module-name/pipe. Jeśli korzystamy z niego w całej aplikacji i jednocześnie ma związek z jej przeznaczeniem, umieszczamy w core/pipe, a jeśli korzystamy z niego niezależnie od tego, czym zajmuje się aplikacja, umieszczamy w shared/pipe.
  3. Oddzielamy komponenty bez powiązania z routingiem, od komponentów z nim powiązanych (podobnie jak ma to miejsce w VueJS). W tym celu tworzymy dodatkowy katalog page. Jego zawartością są oczywiście komponenty (dekorator @Component), ale umieszczamy tu tylko te związane z routingiem. Chodzi tu wyłącznie o naszą wygodę w przeglądaniu i rozumieniu znaczenia tych komponentów.
  4. Nazwy katalogów (tam gdzie możemy), zapisujemy w liczbie pojedynczej. Jest to założenie dyktowane moimi osobistymi preferencjami, więc możesz to olać i zrobić po swojemu.

Moduły aplikacji

Moduł api

Jak sama nazwa wskazuje, moduł ma za zadanie obsługę komunikacji z zewnętrznymi API. Mogą to być zarówno API rest, ale także webservice’y, połączenia socket itp.

Wyszedłem z założenia, że struktura danych zwracanych przez API jest stała, niezależnie od tego gdzie te dane wykorzystujemy. Dopiero konkretna sytuacja w której te dane wykorzystujemy, determinuje ich docelową strukturę. Tą strukturę budujemy w miejscu użycia, czyli w modułach funkcjonalnych.

Przykładowe elementy modułu:

  • modele wysyłane do API
  • modele odbierane z API
  • serwisy do komunikacji
  • interceptory wspierające komunikację z API

Moduł shared

Celem modułu shared, jest dostarczenie reużywalnych elementów aplikacji, jak np. kontrolki UI, dyrektywy, pipe’y etc. Zawartość shared nie powinna mieć żadnego związku z założeniami funkcjonalnymi i biznesowymi aplikacji. Ma natomiast związek z wykorzystywanymi rozszerzeniami do UI, np. komponent button może wykorzystywać klasy css z biblioteki bootstrap.

Przykładowe elementy modułu:

  • komponent kontenera dla okna modalnego
  • komponent do obsługi komunikatów wyświetlanych w aplikacji
  • pipe do wyświetlenia stringu w liczbie mnogiej
  • pipe do wyświetlenia daty zmodyfikowanej o strefę czasową
  • dyrektywa wyświetlająca błędy walidacji formularza
  • walidator numeru NIP

Moduł core

Moduł core to bazowy moduł naszej aplikacji. Zawiera elementy kluczowe dla jej funkcjonowania i jest łącznikiem pomiędzy bazowym AppModule, ApiModule oraz modułami funkcjonalnymi.

Przykładowe elementy modułu:

  • serwis generujący menu aplikacji na podstawie uprawnień użytkownika
  • layout aplikacji
  • guard weryfikujący możliwość dostępu do aplikacji (jeśli moduły features wymagają specyficznej dla nich kontroli dostępu, wtedy guard w module core powinien jedynie dostarczać interfejs dla guard’ów zaimplementowanych w tych modułach).

Moduły funkcjonalne (features)

Moduły features to implementacje konkretnych działów aplikacji. Jedynie moduły features mogą posiadać ścieżki routingu.

Dla zachowania spójnej struktury katalogów w ramach aplikacji, dobrze jest katalogować elementy modułu, nawet jeśli sam moduł zawiera jedynie jeden komponent i jeden serwis http.

Katalogi wewnątrz modułów

component

Miejsce składowania komponentów niepowiązanych ze ścieżkami routingu.

page

Miejsce składowania komponentów podpiętych pod ścieżki routingu. Jeśli taki komponent zawiera wewnątrz inne, mniejsze komponenty, trzymamy je albo w katalogu component (jeśli są używane także przez inne komponenty) lub w tym samym katalogu, co komponent rodzica (jeśli tylko on z nich korzysta).

W rozumieniu Angular, mamy do czynienia z typowym komponentem. Miejsce przechowywania w innym katalogu służy jedynie w celu odseparowania komponentów związanych z ścieżkami routingu, od pozostałych komponentów. Upraszcza też implementację w sytuacji, gdy np. najpopularniejsze artykuły zechcemy wyświetlić jednocześnie jako część innej podstrony, jak i odrębną zakładkę w własnym adresem URL. Wystarczy wtedy jedynie komponent popular zagnieździć wewnątrz strony popular.

component i page

Oba te katalogi można rozszerzać tak, jak ma to miejsce w przypadku modułów, tj. do komponentu można dostarczyć dedykowany pipe lub service. Może też posiadać wewnątrz inne komponenty.

data-provider

Serwisy, których celem jest pobranie danych z serwisów modułu ApiModule i przy pomocy biblioteki class-transformer opisanej przeze mnie tutaj, zwrócić dane w postaci przydatnej dla widoku (view model).

model

Znajdują się w nim modele zwracane przez data-provider. Często łączą w sobie wyniki kilku requestów do API, np. obiekt użytkownika z kolekcją obiektów jego uprawnień.

service

Serwisy przechowujące stan UI, np. serwis przechowujący stan kroków w formularzu składającym się z kilku mniejszych formularzy.

directive, interceptor, pipe

To chyba nie wymaga wyjaśnień. Dokumentacja Angular szczegółowo wyjaśnia ich przeznaczenie.

Inne katalogi aplikacji

i18n

Katalog ze zmiennymi języka, wykorzystywany przez bibliotekę ngx-translate. Nazwą pliku z tłumaczeniami, jest nazwa modułu w Angular.

config

Pliki konfiguracyjne aplikacji. Część z nich, jest używana przez pliki z katalogu environments.

Podsumowanie

W tym miejscu może pojawić się pytanie, czemu layout trzymać w core, a nie shared. Przecież to częsta praktyka. Owszem, ale w moim modelu łamie ona zasadę, że shared nie dotyka logiki aplikacji. Layout jej dotyka, często korzysta ze specyficznych dla aplikacji rozwiązań, np. wyświetla menu w zależności od roli użytkownika lub samego faktu bycia zalogowanym, natomiast moduł shared nie powinien importować komponentów z modułu core i modułów funkcjonalnych.

To by było na tyle. Jeśli któryś z fragmentów był niejasno przeze mnie opisany lub masz coś do dodania, proszę o komentarz.