Blog
Elastyczna struktura projektu w Angularze 1.5 (architektura “fraktalna”) [“NG 1.5 z placu boju” 4/7]
Ten post to 4-ta część serii “Angular 1.5 z placu boju”, prezentującej architekturę opartej na komponentach, “gotowej na NG 2” aplikacji w Angularze 1.5, nad którą pracujemy.
Wprowadzona w Angularze 1.5 metoda
module.component()
może wydawać się kosmetycznym dodatkiem, jednak podejście, które ta metoda promuje (a które wykorzystujemy “do oporu”) owocuje zupełnie innym typem architektury niż opisywana w większości “klasycznych” tutoriali do Angulara, mam więc nadzieję, że uda Ci się na bazie naszych doświadczeń paru rzeczy dowiedzieć.Spis Treści:
- Czy opłaca się startować z nową aplikacją w Angularze 1.5?
- Aplikacja w Angularze 1.5 jako drzewo komponentów
- Komunikacja pomiędzy komponentami w Angularze 1.5
- Elastyczna struktura projektu w Angularze 1.5 (architektura “fraktalna”) [TEN POST]
- Pisanie projektu w Angularze 1.5 w ES6/ES2015 [WKRÓTCE]
- Testowanie jednostkowe komponentów Angulara 1.5 – szczegółowy przewodnik [WKRÓTCE]
- Testy E2E opartej na komponentach aplikacji w Angularze 1.5 [WKRÓTCE]
W poprzednich dwóch artykułach opisywałem koncepcyjną strukturę aplikacji w Angularze 1.5: jak podzielić aplikację na komponenty oraz jak komponenty się ze sobą komunikują. Teraz nadszedł czas na omówienie fizycznej organizacji aplikacji opartej na komponentach: struktury plików i katalogów.
Spojrzenie z góry: struktura aplikacji
istnieją dwa główne sposoby organizacji aplikacji:
- podział według warstw technicznych, z katalogami najwyższego poziomu takimi jak
components
,services
albofilters
- podział według funkcjonalności (czasami określany jako pod architecure), z katalogami najwyższego poziomu takimi jak
todos
,tags
albousers
struktura aplikacji zorganizowana według funkcjonalności ma kilka zalet:
- aplikację łatwiej zrozumieć
- łatwiej jest odszukać różne rzeczy
- łatwiej jest podzielić aplikację (np. na oddzielne serwisy)
Zobacz też doskonały post Boba Martina, który nazywa to “krzyczącą architekturą” (screaming architecture) i, w typowym dla siebie stylu, w niezwykle celny sposób podsumowuje zalety takiej architektury.
My także jesteśmy zwolennikami podziału aplikacji według funkcjonalności i w ten właśnie sposób porządkujemy naszą aplikację.
moduły Angulara świetnie przystają do funkcjonalności najwyższego poziomu
W naszej aplikacji, katalogi najwyższego poziomu odpowiadają naszym głównym obszarom funkcjonalnym. Każdy taki katalog jest oddzielnym, autonomicznym modułem Angulara:
- jawnie określa wszystkie swoje zależności (takie jak np.
ngRoute
,ngAnimate
czyngMessages
) - definiuje w pełni swoją konfigurację (swoje własne bloki
angular.module().config()
iangular.module().run()
) - zawiera swoją własną, wewnętrzną konfigurację routingu
Celem jest osiągnięcie takiej samowystarczalności modułów, żeby dało się minimalnym nakładem pracy wydzielić moduł do niezależnej aplikacji.
wewnątrz modułu stosujemy podział według warstw technicznych
Moduł zawiera następujące katalogi:
- osobne katalogi dla wszystkich głównych części składowych Angulara, wykorzystywanych w aplikacji opartej na komponentach:
components
,services
orazfilters
, plus dodatkowy katalogutils
na pomocnicze plain object-y i funkcje (importowane bezpośrednio lub wstrzykiwane poprzezangular.module().constant()
) - katalogi powiązane z konfiguracją aplikacji:
config
ienums
- katalog
spec_utils
, zawierający helpery powiązane z testami oraz customowe asercje
Oraz następujące pliki najwyższego poziomu:
- plik
index.config.js
, zawierający statyczną konfigurację modułu (uruchamianą poprzezangular.module().config()
) - plik
index.run.js
, zawierający runtime-ową konfigurację modułu (uruchamianą poprzezangular.module().run()
) - oddzielny plik
index.routes.js
, zawierający routing modułu (technicznie rzecz biorąc, routing jest częścią statycznej konfiguracji modułu, uruchamianej poprzezangular.module().config()
, jednak jest on na tyle koncepcyjnie niezależną częścią konfiguracji, że umieszczenie go w osobnym pliku jest uzasadnione) - główny plik
index.module.js
, określający zależności modułu oraz deklarujący komponenty, serwisy, filtry i stałe
Oczywiście nie wszystkie moduły muszą zawierać wszystkie wymienione pliki i katalogi. Powyższa lista to maksymalny zestaw, ale jak najbardziej możliwe są moduły nie zawierające np. filtrów (i w efekcie katalogu filters
) albo np. nie potrzebujące statycznej ani runtime-owej konfiguracji (i w efekcie nie zawierające plików index.config.js
ani index.run.js
).
pojedynczy moduł nadrzędny (app
) agreguje wszystkie moduły najwyższego poziomu
Nadrzędny katalog app
zawiera takie same 4 pliki jak w przypadku modułów (index.config.js
, index.run.js
, index.routes.js
oraz index.module.js
). Nie zawierają one jednak własnej logiki, działają jedynie jako agregatory:
- jedynymi zależnościami zdefiniowanymi w pliku
index.module.js
na poziomie aplikacji są inne moduły; nie definiuje on nigdy swoich własnych zewnętrznych zależności - jedyną route-ą, zdefiniowaną w pliku
index.routes.js
na poziomie aplikacji, jest route-a domyślna (.otherwise({ redirectTo: '/some_route' })
); wszystkie pozostałe route-y są zdefiniowane wewnątrz modułów - zazwyczaj na poziomie aplikacji nie jest w ogóle potrzebny plik
index.run.js
, a plikindex.config.js
na poziomie aplikacji zawiera jedynie konfigurację Angulara, taką jakdebugEnabled
, nie zawiera natomiast żadnej konfiguracji biznesowej
Ponadto, katalog app
nie zawiera żadnych pod-katalogów poza modułami (nie ma własnych komponentów, serwisów, config-u itp.).
kompletna struktura aplikacji wygląda jak poniżej:
[app]
[todos]
[components]
[config]
[enums]
[filters]
[services]
[spec_utils]
[utils]
index.config.js
index.module.js
index.routes.js
index.run.js
[tags]
...
[users]
...
[auth]
...
[shared]
...
index.config.js
index.module.js
index.routes.js
index.run.js
specjalny moduł shared
(i inne nie-funkcjonalne moduły)
W powyższym przykładzie kompletnej struktury aplikacji mogłeś oprócz modułów funkcjonalnych takich jak todos
albo tags
zauważyć również moduł shared
. W tym specjalnym, nie-funkcjonalnym module, przechowujemy kod reużywalny pomiędzy kilkoma modułami (Komponenty Atomowe, współdzielone helpery dla testów, niektóre uniwersalne serwisy i filtry itp.).
Katalog shared
jest najbardziej typowym przykładem modułu nie-funkcjonalnego, obecnym w praktycznie każdej aplikacji. Aplikacje mogą mieć jednak także inne tego typu moduły. Na przykład w naszej aplikacji mamy też moduł env
, w którym trzymamy konfigurację dla różnych środowisk na które deploy-ujemy naszą aplikację oraz logikę wykrywającą aktualne środowisko.
Co istotne, wszystkie moduły, zarówno funkcjonalne jak i nie-funkcjonalne, mają taką samą strukturę katalogów i plików i z czysto technicznego punktu widzenia są identyczne.
architektura “fraktalna”
Ponieważ wszystkie moduły mają zunifikowaną strukturę, są samowystarczalne i zaprojektowane do tego, by można je poziom wyżej agregować, otwiera się przed nami ciekawa opcja: możemy powielić taką samą strukturę także wewnątrz modułu, tak jak poniżej:
[app]
[module_with_submodules]
[sub_module_1]
[sub_module_2]
...
[sub_module_n]
[shared]
index.config.js
index.module.js
index.routes.js
index.run.js
[module_without_submodules]
[components]
[config]
[enums]
[filters]
[services]
[spec_utils]
[utils]
index.config.js
index.module.js
index.routes.js
index.run.js
[shared]
index.config.js
index.module.js
index.routes.js
index.run.js
Powyższy przykład pokazuje jedynie pojedynczy poziom sub-modułów, możemy jednak mieć tyle poziomów, ile tylko chcemy (sub-moduł może zawierać sub-sub-moduły itd.). Nazywamy to “architekturą fraktalną”. Taka architektura jest bardzo elastyczna i pozwala podzielić Twoją aplikację w taki sposób, w jaki tylko zechcesz.
Nie powinieneś jednak tego potencjału nadużywać. Naszym zdaniem najlepiej utrzymywać strukturę katalogów jak najbardziej płaską, wprowadzając sub-moduły jedynie gdy jakiś moduł staje się naprawdę ciężki do utrzymania (a nawet wtedy uważamy, że powinieneś w pierwszej kolejności rozważyć podział modułu na dwa lub więcej modułów najwyższego poziomu). Sądzimy też, że mimo iż jest możliwe by moduł zawierał równocześnie zarówno sub-moduły jak i niskopoziomowe elementy takie jak komponenty, moduł staje się łatwiejszy w utrzymaniu jeśli zawiera albo komponenty ALBO sub-moduły, nigdy jedne i drugie na raz.
Spojrzenie z dołu: struktura komponentu
W pierwszej części artykułu omówiłem jak podzielić aplikację na moduły. Moduły to jednak nie wszystko. Zawierają on także elementy niższego poziomu: komponenty, serwisy i filtry. Te elementy niższego poziomu są zazwyczaj rozbite na kilka plików. Jaki jest najlepszy sposób uporządkowania tych plików?
komponenty także powinny być samowystarczalne (tak jak moduły)
Istnieją dwa sposoby organizacji niskopoziomowych plików:
- można je umieścić w katalogach podzielonych względem typu plików (
templates
,styles
,specs
itd.) - można trzymać wszystkie pliki związane z danym komponentem w katalogu tego komponentu
Preferujemy drugą opcję: trzymanie wszystkich części składowych komponentu razem, w jednym katalogu. (Oczywiście odnosi się to także do serwisów, filtrów itd.)
Fragment takiej struktury może wyglądać jak poniżej:
[app]
[todos]
[components]
[todo_list]
todo_list.component.js
todo_list.html
todo_list.scss
todo_list.spec.js
[add_todo_form]
...
[services]
[todos_notifications]
todos_notifications.service.js
todos_notifications.spec.js
notification_formatter.js
...
Zwróć uwagę, że wszystkie powiązane pliki są trzymane razem – nie tylko standardowe elementy komponentu albo serwisu, ale także wszystkie pliki pomocnicze, takie jak np. notification_formatter.js
(“czysta” funkcja pomocnicza, służąca do generowania odpowiednio sformatowanych komunikatów powiadomień).
trzymanie wszystkiego razem ma dwie istotne zalety:
- łatwiej jest pracować nad komponentem (łatwiej jest wyszukiwać i nawigować po wszystkich powiązanych z komponentem plikach, mniej jest skakania pomiędzy katalogami)
- łatwiej jest przenieść komponent do innego modułu, uwspólnić go albo wydzielić do innej aplikacji
izolowanie komponentów także na poziomie CSS-a
Warto nadmienić, że idea samowystarczalnych komponentów może sięgać dalej niż tylko struktura plików i katalogów. Podobną izolację można (i powinno się) osiągnąć także na poziomie CSS-a, poprzez namespacing wszystkich klas CSS per komponent.
Jedną z popularnych metodyk, które to umożliwiają – i którą wykorzystujemy w naszym projekcie – jest metodyka BEM (Block Element Modifier). Komponentyzacja CSS-a to jednak szeroki temat, nie będę się więc w niego bardziej w tym poście zagłębiał.
Wnioski
Przedstawiona w tym artykule struktura projektu jest elastyczna, czysta i łatwo przeszukiwalna. Po kilku miesiącach rozwoju aplikacji w takiej architekturze, możemy z przekonaniem stwierdzić, że jest to właściwa droga i z całą pewnością wykorzystamy podobną architekturę w naszym kolejnym projekcie.
Jednakże, nauczyliśmy się też, że w pracy z “fraktalną” architekturą niezbędna jest właściwa równowaga. Możliwe jest zarówno wypracowanie zbyt płaskiej struktury (co czyni ją trudną do przeglądania) jak i struktury zbyt głębokiej (co czyni ją ciężką do refaktoryzacji).
Co Ty o tym sądzisz?
W jaki sposób Ty organizujesz swoje Angularowe (lub inne) aplikacje? Co sądzisz o strukturze opisanej powyżej? Podziel się swoją opinią w komentarzach poniżej!