O cerință cu care cel mai probabil mulți dintre noi am avut tangențe deja, sau dacă nu, cel mai probabil vom avea în viitor, este migrarea unor funcționalități deja existente spre o tehnologie de actualitate. În cele ce urmează vom prezenta acele funcționalități care pentru noi au făcut o astfel de migrare relevantă. Dar mai întâi să vedem care este contextul inițial. În cazul nostru, scopul a fost migrarea funcționalităților dintr-o aplicație monolit: componenta backend a migrat spre microservicii, iar cea frontend către microfrontenduri. Obiectivul nostru este acela de a prezenta provocările întâmpinate pe parcurs și în același timp soluțiile pe care le-am ales.
Conceptul de "aplicație monolit"- definit mai simplu - reprezintă o aplicație software în care funcționalitățile de frontend și cele de backend sunt dezvoltate în același codebase. În cazul nostru plecăm de la o aplicație Model - View - Controller (MVC) scrisă în PHP cu Laravel framework, partea de frontend fiind implementată cu blade templates și jQuery. Pentru a exemplifica cele anterior menționate, în diagrama de mai jos este reprezentată structura unei aplicații monolit clasice.
Conceptul de microserviciu este unul cu care majoritatea dintre noi cred că suntem familiarizați, însă când vine vorba de microfrontend lucrurile stau puțin diferit. Ideea de microfrontend a început să prindă putere în ultimii ani, dar încă nu este abordată la o scară largă. Practic, acest concept își propune să aducă beneficiile asociate microserviciilor într-o aplicație de frontend. Cel mai întâlnit mod de a dezvolta aplicații frontend în ultima perioadă este acela de a construi un single page application (SPA), dezvoltat de o echipă de frontend, specializată. Problemele care pot apărea dacă utilizăm acest mod de dezvoltare al aplicației sunt în general legate de dimensiunea codebase-ului care poate să devină greu de întreținut. O astfel de aplicație poate fi numită "frontend monolit". În ceea ce privește scopul nostru inițial, acesta este să folosim o tehnologie de actualitate și să ne îndepărtăm pe cât posibil de noțiunea de monolit. Important de menționat este faptul că echipa noastră este formată din aproximativ 100 de programatori care, până în momentul în care a fost luată în calcul rescrierea, lucrau simultan pe același codebase. Principiul care stă la baza microfrontend-urilor este modul de organizare al aplicației: fiecare echipă este responsabilă de câte un subdomeniu din businessul aplicației. Alte idei care stau la baza acestei abordări se referă la stackul de tehnologii folosit pentru dezvoltare. Practic, fiecare echipă are libertatea de a decide ce și cum folosește, fără a fi nevoie de o coordonare în prealabil cu celelalte echipe implicate în proiect, fiecare trebuind să dezvolte aplicații independente, atât pe partea de backend cât și pe partea de frontend, aplicații care nu se bazează de exemplu pe variabile globale, shared state etc. În figura de mai jos putem observa organizarea echipelor atunci când se lucrează cu microfrontenduri, precum și modul în care componentele comunică între ele.
Unul dintre principalele argumente în favoarea migrării dintr-un codebase monolitic într-o aplicație microfrontend îl reprezintă existența dependențelor vechi care nu se mai bucură de o mentenanță activă. Printre aceste dependențe putem, fără doar și poate, să includem librăria jQuery. Putem afirma faptul că folosirea jQuery este indicată în cazul proiectelor de dimensiuni mici, de prezentare, care nu necesită o manipulare complexă a DOM-ului sau care nu au o funcționalitate complexă astfel încât să nu fie nevoie de procesări costisitoare. În cazul nostru, proiectul a crescut într-un mod alert, iar odată cu el și dificultatea funcționalităților dezvoltate, motiv pentru care am început să simțim limitările stackului tehnologic existent. Astfel, migrarea către microfrontenduri bazate pe un framework de JavaScript, React în cazul nostru, ne ajută să eliminăm codul legacy și în același timp ne oferă mult mai multe posibilități pentru dezvoltarea funcționalităților noi, întrucât React se bucură de o popularitate tot mai mare și de o comunitate foarte activă. Dacă analizăm graficul următor, putem observa foarte clar tendința utilizatorilor de a renunța la jQuery ca dependență în proiecte. Graficul reprezintă popularitatea căutărilor pe Google a termenilor "jQuery" și "React", valoarea 100 reprezentând popularitatea maximă a termenului, iar valoarea 50 arătând că popularitatea a scăzut la jumătate din cea maximă înregistrată.
Un alt motiv pentru care migrarea dinspre monolit spre aplicații microfrontend poate fi benefică este reprezentat de dificultatea pe care o presupune mentenanța unui codebase monolitic. În cazul în care pe proiectul respectiv lucrează mai multe echipe, există un risc ridicat de apariție a codului duplicat deoarece specificațiile de design sunt cel mai probabil aceleași pentru toate echipele, fiind foarte greu de identificat existența unor elemente comune deja dezvoltate astfel încât acestea să poată fi reutilizate. În unele cazuri, chiar dacă un astfel de element comun este disponibil, modul în care codul este organizat poate face imposibilă utilizarea lui, fără a recurge la duplicare.
Practic, mutând una sau mai multe funcționalități în unul sau mai multe microfrontenduri, ne asigurăm că fiecare microfrontend are propria responsabilitate pentru o anumită parte din aplicație, funcționând independent de celelalte, ușurând astfel mentenanța. În cazul nostru, un alt element care s-a dovedit a fi benefic este reprezentat de dezvoltarea unei librării de componente reutilizabile care poate fi utilizată de toate echipele în microfrontendurile lor. Librăria respectivă cuprinde componente precum: dropdownuri, form fielduri, popupuri și alte componente necesare pentru implementarea funcționalităților în cadrul mai multor echipe.
Întrucât o aplicație monolit presupune existența unui număr mare de funcționalități, acestea trebuie acoperite de teste funcționale. În cazul nostru acestea lipseau cu desăvârșire, fapt ce constituie un risc, în special în situațiile în care aplicația se află într-un continuu proces de dezvoltare. Pentru noi, migrarea spre o tehnologie de actualitate a însemnat o foarte bună oportunitate pentru implementarea testelor funcționale și odată cu ele creșterea nivelului de încredere și siguranță din cadrul echipei.
Prima problemă de care ne-am lovit încă înainte de a decide care va fi stackul tehnologic spre care vom migra, a fost găsirea unui workflow care să ne satisfacă nevoile. Deoarece aplicația pe care lucrăm este live și are câteva zeci de mii de utilizatori activi în fiecare zi, suntem conștienți de faptul că, pe lângă migrarea funcționalităților, va trebui în continuare să asigurăm și mentenanța în monolit. Dar acest lucru nu ne va permite alocarea întregii echipe doar pe taskurile aferente migrării. Odată cu rescrierea funcționalităților era necesară și modificarea designului interfeței și uneori modificarea funcționalităților astfel încât să satisfacă cât mai bine nevoile utilizatorilor. Acest lucru a dus la decizia de a folosi feature flaguri pentru release-uri, pentru ca modificările în platforma live să poată fi efectuate gradual. Inițial, acestea se realizează doar pentru o parte din utilizatori, astfel încât să putem colecta feedback și să putem remedia posibilele probleme în cazul în care acestea apar înainte de a face release pentru 100% din utilizatori. Un alt element important în procesul de migrare a fost definirea ordinii în care funcționalitățile urmează a fi migrate, scopul final fiind migrarea codului în proporție de 100%. După cum am menționat anterior, în cazul nostru nu a fost posibilă alocarea întregii echipe doar pentru rescrierea funcționalităților. De aceea, am decis să începem progresiv prin implementarea unor mici părți funcționale și independente dintr-o pagină sau identificarea elementelor comune care apar pe mai multe pagini și construirea unor componente reutilizabile, care ulterior urmau să fie incluse în monolit.
Includerea unei componente din microfrontend în monolit a constat în adăugarea unui div în HTML-ul paginii în care avem nevoie, div pe care am adăugat după nevoie data-attributes pentru a face disponibile unele date care la momentul respectiv erau accesibile doar din monolit. Acest mod de a transmite date este unul temporar, dar necesar, și ne ajută să decuplăm frontendul de implementarea microserviciilor pe backend. Spre exemplu, în imaginea de mai jos avem un div cu id-ul "microfrontend-main-container" care va fi folosit pentru a identifica elementul în care urmează a fi randată componenta de React din microfrontend.
<div id="microfrontend-main-container"
data-position-name="{{$positionName}}"
data-language="{{$currentLocale}}"/>
Vă întrebați cum anume decide microfrontendul ce anume trebuie să randeze în divurile incluse în fișierele din monolit? În timpul buildului, microfrontendul generează un fișier JavaScript și un fișier CSS care urmează să fie incluse în monolit, în fișierele în care avem nevoie de componentele din microfrontend. În cazul nostru, în fișierele blade în care trebuie să includem microfrontendul, trebuie adăugate următoarele scripturi:
<link rel="stylesheet" href="{{$mfeCss}}">
<script src={{$mfeJs}}></script>
Astfel, fișierele generate de microfrontend sunt incluse în pagini și suntem cu un pas mai aproape de a avea componentele randate din microfrontend în monolit. În situația noastră, fișierele care se generează la build din microfrontend sunt încărcate în cloud și servite printr-un microserviciu de către monolit. Apoi acestea sunt incluse în pagini conform scripturilor anterior prezentate.
Totodată, pentru ca microfrontendul să decidă ce anume să randeze într-un anumit div, acesta folosește un sistem bazat pe events, practic din monolit se face dispatch la un custom event care conține o cheie pe baza căreia microfrontendul, care implementează event listenerul, randează componenta corespunzătoare. Spre exemplu, putem avea situații în care mesajul primit de microfrontend îi transmite acestuia că trebuie să randeze o anume componentă în cazul în care pagina nu este în totalitate migrată sau situația în care avem un mesaj generic iar microfrontendul va randa componentele pe baza rutei curente.
Poate cel mai mare beneficiu rezultat în urma rescrierii funcționalităților, pentru noi ca echipă, este nivelul de încredere pe care-l avem în codul dezvoltat. Începând cu un codebase pe care nu-l cunoșteam și care nu beneficia de teste funcționale, eram în punctul în care orice modificare însemna un risc mare de regresii, indiferent de cât de bine reușeam să ne testăm munca. În același timp, posibilitatea ca modificările noastre să influențeze unele funcționalități dezvoltate de către alte echipe, asupra cărora noi să nu avem vizibilitate, era ridicată. Prin migrarea spre microfrontenduri și microservicii am reușit să ajungem în punctul în care fiecare echipa care lucrează la proiect să devină independentă, iar acesta este un aspect foarte important mai ales când numărul echipelor este considerabil și se află într-o continuă creștere. Un alt mare beneficiu este reprezentat de faptul că am reușit să eliminăm librăriile outdated din proiect, acestea devenind un impediment de fiecare dată când era necesară dezvoltarea unor noi funcționalități. Utilizarea unei tehnologii up to date, care se bucură de mentenanță constantă și de o comunitate activă, ne oferă multiple posibilități în implementarea noilor funcționalități ceea ce reprezintă un factor important când vine vorba de gradul de satisfacție al membrilor echipei.
Un alt aspect, la fel de important ca cele deja menționate, îl reprezintă gradul de satisfacție al clientului care acum dispune de o aplicație mult mai sigură, rata de buguri raportate fiind într-o continuă scădere, direct proporțională cu numărul de funcționalități pe care reușim să le migrăm. Dar cel mai mare avantaj este că oferă clientului o aplicație mult mai ușor de dezvoltat pe viitor, fără constrângeri de ordin tehnic.
de Dan Sabadis
de Sorin Stan
de Ovidiu Mățan
de Ovidiu Mățan