Mikrofrontend - okładka

Mikrofrontend

Opublikowano Kategorie FrontendCzas czytania 10min

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. Monolit jest 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.

Jak każde podejście tak również i to ma wady i zalety. Pokrótce omówię plusy i minusy, które udało mi się dostrzec.

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, a 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 komponentó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ć.

Mikrofrontend jest jak puzzle

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 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. Zwróćmy choćby uwagę 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. Myślę jednak, że to jest problem 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. 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 to 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ć np. 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.

Mikrofrontend - implementacja

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-vuesingle-spa-react.

Kolejnym krokiem będzie utworzenie pliku z konfiguracją Single SPA. 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 jest zbędne. Jeśli nie wiesz jak stworzyć taką konfigurację, to zachęcam Cię do przeczytania wpisu wprowadzającego do Webpacka.


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 umieszczone zostaną 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 sporo 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 Janusz, który narzeka, że „kiedyś to było gorzej i było lepiej„, ale na chwilę obecną widzę w tym wiele potencjalnych problemów. Wielokrotnie zaznaczałem w tym artykule jednak, że widzę też wiele zalet. Czas i praktyka pokaże, czy miałem rację.

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

Dominik Szczepaniak

Zawodowo Senior Software Engineer w CKSource. Prywatnie bloger, fan włoskiej kuchni, miłośnik jazdy na rowerze i treningu siłowego.

Inne wpisy, które mogą Cię zainteresować

Zapisz się na mailing i odbierz e-booka

Odbierz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer i realnie zwiększ swoje szanse na rozmowie rekrutacyjnej! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.

Subscribe
Notify of
guest

1 Komentarz
Most Voted
Newest Oldest
Inline Feedbacks
View all comments
Comandeer
5 years ago

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).