Ghid pentru Monorepos pentru codul front-end

Publicat: 2022-03-11

Monorepo-urile sunt un subiect fierbinte pentru o discuție. Au existat o mulțime de articole recent despre de ce ar trebui și nu ar trebui să utilizați acest tip de arhitectură pentru proiectul dvs., dar cele mai multe dintre ele sunt părtinitoare într-un fel sau altul. Această serie este o încercare de a aduna și explica cât mai multe informații posibil pentru a înțelege cum și când să folosiți monorepos.

Un Monorepository este un concept arhitectural, care conține practic toată semnificația din titlu. În loc să gestionați mai multe depozite, păstrați toate părțile de cod izolate într-un singur depozit. Țineți minte cuvântul izolat - înseamnă că monorepo nu are nimic în comun cu aplicațiile monolitice. Puteți păstra multe tipuri de aplicații logice într-un singur depozit; de exemplu, un site web și aplicația sa iOS.

Comparație între un monorepo, un singur repo și multi-repo

Acest concept este relativ vechi și a apărut cu aproximativ un deceniu în urmă. Google a fost una dintre primele companii care a adoptat această abordare pentru gestionarea bazelor de cod. Vă puteți întreba, dacă există de un deceniu, atunci de ce este un subiect atât de fierbinte abia acum? În mare parte, de-a lungul ultimilor 5-6 ani, multe lucruri au suferit schimbări dramatice. ES6, preprocesoare SCSS, manageri de activități, npm etc. — în prezent, pentru a menține o aplicație mică bazată pe React, trebuie să vă ocupați de pachete de proiecte, suite de testare, scripturi CI/CD, configurații Docker și cine știe ce altceva. Și acum imaginați-vă că, în loc de o aplicație mică, trebuie să mențineți o platformă uriașă constând din multe zone funcționale. Dacă vă gândiți la arhitectură, veți dori să faceți două lucruri principale: să separați preocupările și să evitați dupările de cod.

Pentru a face acest lucru, probabil că veți dori să izolați funcții mari în unele pachete și apoi să le utilizați printr-un singur punct de intrare în aplicația dvs. principală. Dar cum gestionați acele pachete? Fiecare pachet va trebui să aibă propria configurație a mediului de flux de lucru și asta înseamnă că de fiecare dată când doriți să creați un nou pachet, va trebui să configurați un nou mediu, să copiați toate fișierele de configurare și așa mai departe. Sau, de exemplu, dacă trebuie să schimbați ceva în sistemul dvs. de compilare, va trebui să treceți peste fiecare repo, să faceți o comitere, să creați o cerere de extragere și să așteptați fiecare construcție, ceea ce vă încetinește foarte mult. La acest pas, ne întâlnim cu monorepos.

În loc să avem o mulțime de depozite cu propriile configurații, vom avea o singură sursă de adevăr - monorepo: un ruler de suită de testare, un fișier de configurare Docker și o configurație pentru Webpack. Și mai aveți scalabilitate, oportunitatea de a separa preocupările, partajarea codului cu pachete comune și o mulțime de alți profesioniști. Sună bine, nu? Ei bine, este. Dar există și câteva dezavantaje. Să aruncăm o privire atentă asupra avantajelor și dezavantajelor exacte ale utilizării monorepo-ului în sălbăticie.

Avantaje Monorepo:

  • Un singur loc pentru a stoca toate configurațiile și testele. Deoarece totul se află într-un singur depozit, vă puteți configura CI/CD-ul și bundler-ul o dată și apoi reutilizați configurațiile pentru a construi toate pachetele înainte de a le publica la distanță. Același lucru este valabil și pentru testele unitare, e2e și de integrare - CI-ul dvs. va putea lansa toate testele fără a fi nevoit să se ocupe de configurații suplimentare.
  • Refactorizează cu ușurință caracteristicile globale cu comiteri atomice. În loc să faceți o cerere de extragere pentru fiecare repo, să vă dați seama în ce ordine să vă construiți modificările, trebuie doar să faceți o cerere de extragere atomică care va conține toate comitările legate de caracteristica cu care lucrați.
  • Publicarea simplificată a pachetelor. Dacă intenționați să implementați o nouă caracteristică în interiorul unui pachet care depinde de un alt pachet cu cod partajat, o puteți face cu o singură comandă. Este o funcție care necesită câteva configurații suplimentare, care vor fi discutate mai târziu într-o parte de revizuire a instrumentelor din acest articol. În prezent, există o selecție bogată de instrumente, inclusiv Lerna, Yarn Workspaces și Bazel.
  • Gestionare mai ușoară a dependenței. Un singur pachet.json . Nu este nevoie să reinstalați dependențele în fiecare repo ori de câte ori doriți să vă actualizați dependențele.
  • Reutilizați codul cu pachetele partajate, păstrându-le în continuare izolate. Monorepo vă permite să vă refolosiți pachetele din alte pachete, păstrându-le izolate unele de altele. Puteți utiliza o referință la pachetul de la distanță și le puteți consuma printr-un singur punct de intrare. Pentru a utiliza versiunea locală, puteți utiliza legături simbolice locale. Această caracteristică poate fi implementată prin scripturi bash sau prin introducerea unor instrumente suplimentare precum Lerna sau Yarn.

Dezavantaje Monorepo:

  • Nu există nicio modalitate de a restricționa accesul doar la anumite părți ale aplicației. Din păcate, nu puteți partaja doar o parte din monorepo - va trebui să acordați acces la întreaga bază de cod, ceea ce ar putea duce la unele probleme de securitate.
  • Performanță Git slabă atunci când lucrați la proiecte la scară largă. Această problemă începe să apară doar pe aplicații uriașe cu mai mult de un milion de comiteri și sute de dezvoltatori care își fac munca simultan în fiecare zi în același depozit. Acest lucru devine deosebit de supărător, deoarece Git utilizează un grafic aciclic direcționat (DAG) pentru a reprezenta istoria unui proiect. Cu un număr mare de comiteri, orice comandă care parcurge graficul poate deveni lentă pe măsură ce istoricul se adâncește. Performanța încetinește și din cauza numărului de referințe (adică, ramuri sau etichete, care pot fi rezolvate prin eliminarea referințelor de care nu mai aveți nevoie) și a cantității de fișiere urmărite (precum și greutatea acestora, chiar dacă problema fișierelor grele poate fi rezolvată folosind Git LFS).

    Notă: în zilele noastre, Facebook încearcă să rezolve problemele cu scalabilitatea VCS prin corecțiile Mercurial și, probabil, în curând, aceasta nu va fi o problemă atât de mare.

  • Timp de construcție mai mare. Deoarece veți avea o mulțime de cod sursă într-un singur loc, va dura mult mai mult timp pentru ca CI să ruleze totul pentru a aproba fiecare PR.

Revizuirea instrumentului

Setul de instrumente pentru gestionarea monorepourilor este în continuă creștere, iar în prezent, este foarte ușor să te pierzi în toată varietatea de sisteme de construcție pentru monorepos. Puteți fi întotdeauna la curent cu soluțiile populare folosind acest repo. Dar, deocamdată, să aruncăm o privire rapidă asupra instrumentelor care sunt utilizate intens în zilele noastre cu JavaScript:

  • Bazel este sistemul de compilare Google orientat spre monorepo. Mai multe despre Bazel: awesome-bazel
  • Yarn este un instrument de gestionare a dependenței JavaScript care acceptă monorepos prin spații de lucru.
  • Lerna este un instrument pentru gestionarea proiectelor JavaScript cu pachete multiple, construit pe Yarn.

Majoritatea instrumentelor folosesc o abordare cu adevărat similară, dar există câteva nuanțe.

Ilustrație a procesului CI/CD al depozitului git monorepo

Ne vom aprofunda în fluxul de lucru Lerna, precum și în celelalte instrumente din partea 2 a acestui articol, deoarece este un subiect destul de amplu. Deocamdată, să obținem doar o imagine de ansamblu a ceea ce se află în interior:

Lerna

Acest instrument vă ajută într-adevăr atunci când vă ocupați de versiunile semantice, configurați fluxul de lucru, împingeți pachetele, etc. Ideea principală din spatele Lerna este că proiectul dvs. are un folder de pachete, care conține toate părțile de cod izolate. Și pe lângă pachete, aveți o aplicație principală, care, de exemplu, poate locui în folderul src. Aproape toate operațiunile din Lerna funcționează printr-o regulă simplă - repetați toate pachetele și faceți unele acțiuni asupra lor, de exemplu, creșteți versiunea pachetului, actualizați dependența tuturor pachetelor, construiți toate pachetele etc.

Cu Lerna, aveți două opțiuni despre cum să vă folosiți pachetele:

  1. Fără a le împinge la distanță (NPM)
  2. Împingeți pachetele la distanță

În timp ce utilizați prima abordare, puteți utiliza referințe locale pentru pachetele dvs. și, practic, nu vă pasă de legăturile simbolice pentru a le rezolva.

Dar dacă utilizați a doua abordare, sunteți forțat să vă importați pachetele de la distanță. (de exemplu, import { something } from @yourcompanyname/packagename; ), ceea ce înseamnă că veți obține întotdeauna versiunea de la distanță a pachetului dvs. Pentru dezvoltarea locală, va trebui să creați legături simbolice în rădăcina folderului dvs. pentru a face bundler-ul să rezolve pachetele locale în loc să le folosească pe cele care se află în interiorul node_modules/ . De aceea, înainte de a lansa Webpack sau bundler-ul tău preferat, va trebui să lansezi lerna bootstrap , care va lega automat toate pachetele.

O ilustrare a spației de nume a modulelor dvs. în interiorul unui pachet cu un singur nod

Fire

Yarn este inițial un manager de dependență pentru pachetele NPM, care nu a fost construit inițial pentru a suporta monorepos. Dar în versiunea 1.0, dezvoltatorii Yarn au lansat o caracteristică numită Workspaces . La momentul lansării, nu era atât de stabil, dar după un timp, a devenit utilizabil pentru proiecte de producție.

Workspace este practic un pachet, care are propriul package.json și poate avea anumite reguli de construire specifice (de exemplu, un tsconfig.json separat dacă utilizați TypeScript în proiectele dvs.). De fapt, puteți gestiona cumva fără Yarn Workspaces folosind bash și aveți exact aceeași configurație, dar acest instrument ajută la ușurarea procesului de instalare și actualizare a dependențelor per pachet.

Dintr-o privire, Yarn cu spațiile sale de lucru oferă următoarele caracteristici utile:

  1. Dosarul unic node_modules în rădăcină pentru toate pachetele. De exemplu, dacă aveți packages/package_a și packages/package_b — cu propriul package.json — toate dependențele vor fi instalate numai în rădăcină. Aceasta este una dintre diferențele dintre modul în care funcționează Yarn și Lerna.
  2. Legături simbolice de dependență pentru a permite dezvoltarea pachetelor locale.
  3. Un singur fișier de blocare pentru toate dependențele.
  4. Actualizare concentrată a dependențelor în cazul în care doriți să reinstalați dependențe pentru un singur pachet. Acest lucru se poate face folosind steag-ul -focus .
  5. Integrare cu Lerna. Puteți face cu ușurință ca Yarn să se ocupe de toată instalarea/legăturile simbolice și să lăsați Lerna să se ocupe de publicare și de controlul versiunilor. Aceasta este cea mai populară configurație de până acum, deoarece necesită mai puțin efort și este ușor de lucrat.

Link-uri utile:

  • Spații de lucru cu fire
  • Cum se construiește proiectul mono-repo TypeScript

Bazel

Bazel este un instrument de compilare pentru aplicații la scară largă, care poate gestiona dependențe în mai multe limbi și poate suporta o mulțime de limbi moderne (Java, JS, Go, C++ etc.). În cele mai multe cazuri, utilizarea Bazel pentru aplicații JS mici și mijlocii este exagerată, dar la scară largă, poate oferi o mulțime de beneficii datorită performanței sale.

Prin natura sa, Bazel arată similar cu Make, Gradle, Maven și alte instrumente care permit construirea proiectelor pe baza fișierului care conține o descriere a regulilor de construire și a dependențelor proiectului. Același fișier din Bazel se numește BUILD și se află în spațiul de lucru al proiectului Bazel. Fișierul BUILD folosește Starlark, un limbaj de compilare de nivel înalt, care poate fi citit de om, care seamănă mult cu Python.

De obicei, nu veți avea de-a face cu BUILD pentru că există o mulțime de boilerplate care pot fi găsite cu ușurință pe web și care sunt deja configurate și gata de dezvoltare. Ori de câte ori doriți să vă construiți proiectul, Bazel face, practic, următoarele:

  1. Încarcă fișierele BUILD relevante pentru țintă.
  2. Analizează intrările și dependențele acestora, aplică regulile de construcție specificate și produce un grafic de acțiune.
  3. Execută acțiunile de construire asupra intrărilor până când sunt produse ieșirile finale de construcție.

Link-uri utile:

  • JavaScript și Bazel – Documente pentru configurarea unui proiect Bazel pentru JS de la zero.
  • Reguli JavaScript și TypeScript pentru Bazel – Boilerplate pentru JS.

Concluzie

Monorepo-urile sunt doar un instrument. Există o mulțime de argumente dacă are sau nu viitor, dar adevărul este că în unele cazuri, acest instrument își face treaba și se ocupă de el într-o manieră eficientă. Pe parcursul ultimilor ani, acest instrument a evoluat, a câștigat mult mai multă flexibilitate, a depășit o mulțime de probleme și a eliminat un strat de complexitate în ceea ce privește configurația.

Mai sunt o mulțime de probleme de rezolvat, cum ar fi performanța Git slabă, dar sperăm că acest lucru va fi rezolvat în viitorul apropiat.

Dacă doriți să învățați să construiți o conductă CI/CD robustă pentru aplicația dvs., vă recomand Cum să construiți o conductă de implementare inițială eficientă cu GitLab CI .

Înrudit: Flux Git îmbunătățit explicat