W ostatnim czasie przeglądając treści związane ze światem frontendu kilkukrotnie napotkałem na frazę „Mikrofrontend„. Postanowiłem nieco bardziej zgłębić temat i mam na ten temat nieco przemyśleń, którymi chciałbym się podzielić w tym wpisie. Chciałbym w tym miejscu wyraźnie zaznaczyć, że ten artykuł jest moją subiektywną opinią, z którą nie musisz się zgadzać. Ten wpis będzie skupiał się głównie na podejściu które zakłada wykorzystania kilku frameworków do budowy jednej aplikacji.
Aplikacja monolityczna
Na chwilę obecną praktycznie każda aplikacja, którą stworzyłem była monolitem, czyli jednym dużym kawałkiem kodu, w którym powstawały pewne zależności. Takie podejście genialnie sprawdza się w przypadku małych aplikacji.
Mikrofrontend – nowe podejście, ale czy lepsze?
Mikrofrontend to podejście, które jest bardzo podobne w swoich założeniach do mikroserwisów, które w WIELKIM skrócie zakłada, że zamiast monolitu tworzymy serwisy o określonych odpowiedzialnościach.
Myślę, że najlepszą odpowiedzią na zadane pytanie będzie poniższy film:
Plusy
Podział odpowiedzialności
Załóżmy, że tworzymy dashboard w którym mamy do dyspozycji kalendarz, komunikator oraz panel do tworzenia notatek i zarządzania nimi. Każdy z tych elementów możemy wyodrębnić, przez co każdy komponent (element aplikacji) będzie miał tylko jedną odpowiedzialność. Zwiększa to czytelność, ułatwia development, eliminuje nadmiarowe i niepożądane zależności.
Dowolność technologii
Mając już zlecenie na stworzenie dashboardu postanawiamy do każdego z elementów przydzielić zespół. Każdy z zespołów poproszono o wybór technologii, a zespoły udzieliły następujących odpowiedzi: React, React i Vue. No i mamy problem. Chyba, że skorzystamy z dobrodziejstwa mikrofrontendu i dwa elementy zostaną napisane w React’cie a jeden w Vue. Wilk syty i owca cała 🙂
Co więcej, oba zespoły Reactowe chcą pisać aplikację w różnych wersjach. Nic nie stoi na przeszkodzie, aby każdy z zespołów React’owych korzystał z tej, która jest dla nich bardziej przystępna.
To samo tyczy się oczywiście innych, bardziej szczegółowych rozwiązań. Załóżmy, że jeden z zespołów uparcie chce w celu komunikacji z API skorzystać z Axiosa, zaś drugi uważa, że Fetch API jest wystarczające. Tu po raz kolejny wilk syty i owca cała – każdy korzysta z tego co uważa za słuszne. Myślę, że tu nawet nie muszę wspominać, o mnogości wersji bibliotek.
Niezależny development
Załóżmy, że jeden z zespołów postanowił zaktualizować paczki, z których korzysta. Tak więc zrobił to i… konsola pokazuje masę błędów. Błędy są na tyle poważne, że aplikacja nie nadaje się do użycia. Na szczęście pozostałych zespołów to nie obchodzi – ich część aplikacji nadal działa i można nad nią bez przeszkód pracować. Do testowania spójności i integracji mogą korzystać ze starszej, działającej wersji uszkodzonego komponentu, a problemy innego zespołu nie przeszkadzają w ich pracy. Natomiast zespół, który naprawia swój komponent nie czuje presji czasu, że przez nich „zespół stoi w miejscu”.
W procesie niezależnego developmentu bardzo przydatne okazuje się też podejście multi-repo. Stosując to podejście, każdy komponent możemy developować w ramach osobnego repozytorium. W tym celu przydatne może okazać się narzędzie mrgit.
Niezależny deployment
Załóżmy, że pierwsza wersja omawianej przez nas aplikacji została wydana, natomiast klient miał do każdego z kompnentów pewne uwagi. Tylko okazało się, że jeden zespołów jest obecnie na urlopie. Dla developerów to nawet lepiej. Możemy niezależnie od siebie przeprowadzić deployment każdego komponentu z osobna. Niesie to ze sobą jeszcze jeden, ogromny plus. Deployment aplikacji komponent po komponencie bardzo mocno redukuje ryzyko wystąpienia komplikacji. Nawet jeśli komplikacje wystąpią to jesteśmy w stanie bardzo mocno je zredukować i szybko wyeliminować.
Minusy
Skoro plusy mamy już za sobą to teraz czas na minusy. A tych również kilka będzie…
Problemy ze spójnością
W tym punkcie można zawrzeć wszelkie problemy ze spójnością i utrzymaniem kodu. Przede wszystkim w projekcie zawarta jest duża ilość różnych technologii co już stanowi pewien problem sam w sobie. Może się zdarzyć tak, że pewne rozwiązania przestaną być z czasem wspierane co oczywiście może wystąpić w każdym projekcie. Natomiast liczba technologii znacznie zwiększa prawdopodobieństwo zaistnienia takiej sytuacji. Oczywiście spora część owych technologii dostaje aktualizacje, które wypadałoby instalować. Dokłada to kolejnej pracy i nie mam tu na myśli nawet samej liczby technologii do obsłużenia.
Załóżmy że w każdym elemencie aplikacji wykorzystujemy jedną bibliotekę, której kolejna wersja przyniosła znaczące zmiany i musimy nanieść znaczące zmiany w projekcie. Gdy każdy element aplikacji jest mówiąc kolokwialnie „z innej parafii” wykonujemy tę samą pracę kilkukrotnie, ponieważ każdy element aplikacji należy dostosować osobno. Niemniej jednak tu też można zaleźć zaletę. Aktualizacja kawałków aplikacji na pewno jest bezpieczniejsza niż jednorazowa aktualizacja całości. Co więcej – kto każe nam aktualizować wszystkie komponenty jednocześnie?
Kolejnym problemem jest to, że mając jedną aplikację zapewne developujemy ją w obrębie jednego projektu, co oznacza, że 100% kodu jest utrzymywane według jednolitego code style oraz są stosowane powtarzalne wzorce i praktyki, przez co osoba wdrażająca się w projekt łatwo może dostrzec powtarzalne elementy i schematy w naszej aplikacji co ułatwia i skraca czas wdrożenia się.
W mikrofrontendowym podejściu oczywiście jest to jak najbardziej wykonalne, ale gdy każdym komponentem aplikacji zajmuje się inny zespół to jest na pewno trudniejsze, niż gdy wszyscy „rzeźbią” w tym samym. Powiedziałbym nawet, że w tym przypadku wypracowanie wspólnych praktyk w różnych zespołach jest ważniejsze niż w przypadku klasycznej, monolitycznej aplikacji. Spójrzmy chociażby na style. Załóżmy, że każdy z zespołów owrapował swoją aplikację w kontener o klasie .container
. Do tego każdy zespół nałożył na kontener inne style. Powstaje bardzo ciekawy problem do rozwiązania 🙂
Oczywiście problem ten jest jak najbardziej do rozwiązania na wiele różnych sposobów, jak chociażby konwencje nazw tj. BEM, czy ukochane przeze mnie styled-components. Niemniej jednak myślę że problem jest warty odnotowania.
Największy, moim zdaniem, znak zapytania dotyczący utrzymania kodu zostawiłem na koniec. Załóżmy, że nad pewnym komponentem pracowała tylko jedna osoba. Na dodatek ten jeden komponent był stworzony w Vue a cała reszta projektu bazuje na React’cie. Ta osoba postanowiła się zwolnić i mamy tutaj typowy bus factor. Całą wiedzę o tym komponencie, były już pracownik, zabrał ze sobą. Taka sytuacja może spowodować niemały zamęt i chaos w firmie. Traci na tym nie tylko firma ale także klienci. Oczywiście można temu zapobiec przez budowanie takich zespołów, aby bus factor nie miał prawa zaistnieć ale myślę, że przy podejściu mikrofrontendowym o owe zjawisko jest nieco łatwiej.
Rozmiar projektu
Duża liczba bibliotek oznacza dużą liczbę kodu do pobrania przez przeglądarkę. Oczywiście żyjemy w czasach, gdzie możemy zastosować chociażby lazy loading, czy code splitting i pobrać tylko ten kod który nas interesuje. Niemniej jednak nie zmiania to faktu, że gdy zaistnieje potrzeba pobrania całego kodu aplikacji będzie on większy niż w przypadku tylko jednego frameworka.
Jeszcze jednym aspektem, który można podciągnąć pod rozmiar projektu jest to, że pewne powtarzalne elementy aplikacji tj. ikony, linki, inputy, buttony itp. należy w każdym komponencie zakodować i ostylować osobno.
Implementacja
Sposobów na implementację jest kilka. Najprostszym jaki udało mi się znaleźć to był najzwyklejszy iframe. Oczywiście można też stworzyć za pomocą JavaScriptu jakieś bardziej wyrafinowane narzędzie skrojone pod nasze potrzeby, ale można też skorzystać z gotowca. Ja właśnie posłużę się gotowcem, czyli Single SPA. Do naszej aplikacji wykorzystamy Vue oraz React’a.
Poza standardowymi paczkami tj. Vue i React potrzebujemy oczywiście single-spa
oraz dodatkowych paczek: single-spa-vue
i single-spa-react
.
Jeśli nie wiesz jak zainstalować owe paczki polecam zapoznać się z moim wpisem Podstawy pracy z npm. Kolejnym krokiem będzie utworzenie pliku z konfiguracją Single SPA. Owy plik należy jako entry point w pliku konfiguracyjnym Webpacka. Sam plik konfiguracyjny webpacka nie różni się niczym od plików z klasycznych projektów więc umieszczanie przykładowej konfiguracji w tym wpisie uważam za zbędne. Natomiast jeśli nie wiesz jak stworzyć taką konfigurację to zachęcam Cię do przeczytania wpisu Webpack – szybki start i pierwsza konfiguracja. Wracając jednak do Single SPA. Plik konfiguracyjny może wyglądać na przykład tak:
import { registerApplication, start } from 'single-spa'
registerApplication(
'vue',
() => import('./src/vue/vue.js'),
() => location.pathname !== "/react"
);
registerApplication(
'react',
() => import('./src/react/main.js'),
() => location.pathname !== "/vue"
);
start();
Przeanalizujmy co należy zawrzeć w takiej konfiguracji. Pierwszy parametr funkcji registerApplication()
to nazwa aplikacji, drugi z elementów to funkcja zwracająca import pliku wyjściowego dla aplikacji. Natomiast trzeci argument to funkcja która powinna zwracać true
lub false
w zależności od tego czy komponent ma być załadowany czy też nie.
Następnie należy to wszystko ze sobą połączyć. W pliku wyjściowym Vue należy umieścić następującą konfigurację:
import Vue from 'vue';
import singleSpaVue from 'single-spa-vue';
import App from './main.vue'
const vueLifecycles = singleSpaVue({
Vue,
appOptions: {
el: '#vue',
render: r => r(App)
}
});
export const bootstrap = [
vueLifecycles.bootstrap,
];
export const mount = [
vueLifecycles.mount,
];
export const unmount = [
vueLifecycles.unmount,
];
Analogicznie w pliku wyjściowym Reacta również musimy zamieścić podobną konfigurację:
import singleSpaReact from 'single-spa-react';
import App from './app.js';
function domElementGetter() {
return document.getElementById("react")
}
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: App,
domElementGetter,
})
export const bootstrap = [
reactLifecycles.bootstrap,
];
export const mount = [
reactLifecycles.mount,
];
export const unmount = [
reactLifecycles.unmount,
];
Ostatnim krokiem będzie stworzenie pliku index.html gdzie umieścimy nasze komponenty:
<html>
<head></head>
<body>
<div id="react"></div>
<div id="vue"></div>
<script src="/dist/single-spa.config.js"></script>
</body>
</html>
Oczywiście przedstawiłem tylko fragment konfiguracji związanej z Single SPA. Do pełnego działania potrzebujesz wcześniej wspomnianej konfiguracji Webpacka. Oprócz tego potrzebujesz również stworzone pliki wskazane w konfiguracji Reacta i Vue.
Co o tym sądzę?
Mikrofrontend to twór, do którego podchodzę dość sceptycznie. Oczywiście dostrzegam w tym podejściu cały szereg zalet, jest to coś nowego i coś ciekawego. Jednak nie widzę zastosowania tego w małych projektach – byłaby to armata na muchę. Natomiast przy dużych projektach problem utrzymania projektu może się bardzo szybko skalować. Być może brzmię jak typowy Polak, który narzeka, że „kiedyś to było gorzej i było lepiej„, ale na chwilę obecną widzę w tym wiele potencjalnych problemów, co tak jak już wielokrotnie zaznaczałem w tym artykule nie oznacza że nie widzę zalet.
A co ty sądzisz o mikrofrontendzie? Podoba ci się ta idea? Zgadzasz się ze mną, a może wręcz przeciwnie? Czy dostrzegasz wady lub zalety, które ja pominąłem? Zachęcam do dyskusji w komentarzach.
Źródła i materiały dodatkowe
- https://martinfowler.com/articles/micro-frontends.html
- https://blog.pragmatists.com/independent-micro-frontends-with-single-spa-library-a829012dc5be
- https://dev.to/dabit3/building-micro-frontends-with-react-vue-and-single-spa-52op
- https://www.npmjs.com/package/mrgit
- https://en.wikipedia.org/wiki/Bus_factor
- https://webpack.js.org/guides/code-splitting/
- https://medium.com/@benjamin.d.johnson/exploring-micro-frontends-87a120b3f71c
- https://micro-frontends.org/
- https://medium.com/@areai51/microfrontends-an-approach-to-building-scalable-web-apps-e8678e2acdd6
A czym się to różni tak po prawdzie od starej dobrej architektury opartej na eventach (np. Zakasa-Osmaniego)? Tam też mamy tak naprawdę całkowicie niezależne komponenty, które komunikują się ze sobą przy pomocy mechanizmu pub/sub. Jedyna różnica jest taka, że w mikrofrontendach separacja jest posunięta jeszcze dalej, bo niekoniecznie istnieje wspólny core.
Niemniej naiwną implementację mikrofrontendów (z założeniem, że każdy mikrofrontend może mieć własny stack technologiczny) należy odrzucić, bo z punktu widzenia wydajności jest to po prostu zabójcze. Dodatkowo mikrofrontendy nie do końca współgrają z architekturą OMT. IMO o wiele prościej zarządzać aplikacją, w której tylko komponenty UI są w pełni niezależne od siebie, ale logika biznesowa jest już jednolita. W innym wypadku i tak musielibyśmy dołożyć całą warstwę wczytującą poszczególne mikrofrontendy i koordynującą nimi (choreografia poprzez orchestrację? :P).