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
      • core
        • http
          • user.service.[ts|spec.ts]
          • user-permission.service.[ts|spec.ts]
        • interceptor
          • request.interceptor.ts
          • jwt.interceptor.ts
          • response.interceptor.ts
          • responnse-logging.interceptor.ts
        • 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]
        • model
          • user.model.ts
          • user-permission.model.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
        • http
          • rest.service.[ts|spec.ts]
          • soap.service.[ts|spec.ts]
        • 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]
          • http
            • auth.service.[ts|spec.ts]
          • model
            • auth.model.ts
          • page
            • register
              • register.component.[html|scss|spec.ts|ts]
          • service
          • auth.module.ts
          • auth-routing.module.ts
        • article
          • component
            • popular
              • component
                • popular-item
                  • popular-item.component.[html|scss|spec.ts|ts]
              • popular.[html|scss|spec.ts|ts]
            • last-added
              • component
                • last-added-item
                  • last-added-item.component.[html|scss|spec.ts|ts]
              • last-added.component.[html|scss|spec.ts|ts]
          • http
            • article.service.[ts|spec.ts]
          • model
            • article.model.ts
          • page
            • list
              • list.component.[html|scss|spec.ts|ts]
              • list-item.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 może 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). 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ł 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, a modułami funkcjonalnymi.

Przykładowe elementy modułu:

  • interceptory wyzwalane przy requestach do api
  • layout aplikacji
  • serwisy http wykorzystywane wewnątrz więcej niż jednego modułu funkcjonalnego lub wszystkie serwisy aplikacji, jeśli mamy ich niewiele
  • modele zwracane przez serwisy http zaimplementowane w module
  • 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.

http

Serwisy zajmujące się komunikacją z API.

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.