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
- rest
- 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]
- article.rest.service.
- soap
- service
- rest.service.ts
- soap.service.ts
- api.module.ts
- interceptor
- core
- layout
- admin
- component
- footer
- footer.component.
[html|scss|spec.ts|ts]
- footer.component.
- header
- header.component.
[html|scss|spec.ts|ts]
- header.component.
- layout
- layout.component.
[html|scss|spec.ts|ts]
- layout.component.
- main
- main.component.
[html|scss|spec.ts|ts]
- main.component.
- footer
- component
- client
- component
- footer
- footer.component.
[html|scss|spec.ts|ts]
- footer.component.
- header
- header.component.
[html|scss|spec.ts|ts]
- header.component.
- layout
- layout.component.
[html|scss|spec.ts|ts]
- layout.component.
- main
- main.component.
[html|scss|spec.ts|ts]
- main.component.
- footer
- component
- admin
- core.module.ts
- core-routing.module.ts
- layout
- shared
- component
- modal
- modal.component.
[html|scss|spec.ts|ts]
- modal.component.
- button
- button.component.
[html|scss|spec.ts|ts]
- button.component.
- breadcrumb
- breadcrumb.component.
[html|scss|spec.ts|ts]
- breadcrumb.component.
- navbar
- navbar.component.
[html|scss|spec.ts|ts]
- navbar.component.
- modal
- directive
- pipe
- date-formatter.pipe.ts
- shared.module.ts
- component
- features
- auth
- component
- login-form
- login-form.component.
[html|scss|spec.ts|ts]
- login-form.component.
- logout-button
- logout-button.component.
[html|scss|spec.ts|ts]
- logout-button.component.
- login-form
- page
- register
- data-provider
- register.data-provider.ts
- model
- user.view.model.ts
- register.component.
[html|scss|spec.ts|ts]
- data-provider
- register
- service
- auth.module.ts
- auth-routing.module.ts
- component
- article
- component
- popular
- component
- item
- item.component.
[html|scss|spec.ts|ts]
- item.component.
- item
- data-provider
- articles.data-provider.ts
- model
- article.view.model.ts
- popular.
[html|scss|spec.ts|ts]
- component
- last-added
- component
- item
- item.component.
[html|scss|spec.ts|ts]
- item.component.
- item
- data-provider
- articles.data-provider.ts
- model
- article.view.model.ts
- last-added.component.
[html|scss|spec.ts|ts]
- component
- popular
- 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]
- data-provider
- show
- show.component.
[html|scss|spec.ts|ts]
- show.component.
- popular
- popular.component.
[html|scss|spec.ts|ts]
- popular.component.
- list
- component
- item
- component
- image
- image.component.
- service
- article.module.ts
- article-routing.module.ts
- component
- auth
- app.component.html
- app.component.ts
- app.module.ts
- app-routing.module.ts
- api
- config
- environments
- i18n
- pl
- core.json
- article.json
- en
- core.json
- article.json
- pl
- styles
- app
Podstawowe założenia
- 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.
- 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 katalogumodule-name/component/component-name/pipe
. Jeśli przydaje się w kilku komponentach danego modułu, umieszczamy go w katalogumodule-name/pipe
. Jeśli korzystamy z niego w całej aplikacji i jednocześnie ma związek z jej przeznaczeniem, umieszczamy wcore/pipe
, a jeśli korzystamy z niego niezależnie od tego, czym zajmuje się aplikacja, umieszczamy wshared/pipe
. - 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. - 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 modulecore
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.