✏️ Написано от Майкъл Соломон и Йони Голдбърг

Какво гледаме

Пазарът Monorepo е горещ като огън. Странно, но сега, когато търсенето на Monoreps експлодира, една от водещите библиотеки — Lerna- току-що се пенсионира. При внимателно вглеждане може да не е просто съвпадение – с толкова много разрушителни и блестящи функции, въведени от нови доставчици, Lerna не успя да се справи с темпото и да остане актуална. Този разцвет на нови инструменти обърква мнозина — Кой е правилният избор за следващия ми проект? Какво трябва да гледам, когато избирам инструмент Monorepo? Тази публикация цели да подреди това претоварване с информация, да обхване новите инструменти, да наблегне на това, което е важно, и накрая да сподели някои препоръки. Ако сте тук за инструменти и функции, вие сте на правилното място, въпреки че може да се окажете на дълбоко търсещо пътешествие към желания от вас работен поток за разработка.

Тази публикация се занимава само с бекенд и Node.js. Той също така обхваща типични бизнес решения. Ако сте Google/FB разработчик, който се сблъсква с 8000 пакета - съжаляваме, имате нужда от специално оборудване. Следователно чудовищните инструменти на Monorepo като Bazel са пропуснати. Тук ще разгледаме някои от най-популярните инструменти Monorepo, включително Turborepo, Nx, PNPM, работно пространство Yarn/npm и Lerna (въпреки че всъщност вече не се поддържа — това е добра база за сравнение).

Да започваме? Когато хората използват термина Monorepo, те обикновено се отнасят до един или повече от следните 4 слоя по-долу. Всеки един от тях може да донесе стойност на вашия проект, всеки има различни последствия, инструменти и функции:

Слой 1: Обикновени стари папки, за да останете над вашия код

С нулев инструментариум и само като разполага всички Microservice и библиотеки заедно в една и съща главна папка, разработчикът получава страхотни предимства за управление и тонове стойност: навигация, търсене в компоненти, незабавно изтриване на библиотека, отстраняване на грешки, бързо добавяне на нови компоненти. Обмислете алтернативата с подхода с множество хранилища — добавянето на нов компонент за модулност изисква отваряне и конфигуриране на ново хранилище на GitHub. Не само караница, но и по-големи шансове разработчиците да изберат краткия път и да включат новия код в някакъв полуподходящ съществуващ пакет. С прости думи, Monorepos с нулев инструмент може да увеличи модулността.

Този слой често се пренебрегва. Ако вашата кодова база не е огромна и компонентите са силно отделени (повече за това по-късно) - това може да е всичко, от което се нуждаете. Виждали сме шепа успешни решения на Monorepo без никакви специални инструменти.

Като се има предвид това, някои от по-новите инструменти допълват това изживяване с интересни функции:

  • И „Turborepo“, и „Nx“, и също „Lerna“ предоставят визуално представяне на зависимостите на пакетите
  • „Nx позволява „правила за видимост““, което се отнася до налагането кой какво може да използва. Помислете за библиотека за „плащане“, която трябва да се използва само от „микроуслугата за поръчка“ — отклонението от това ще доведе до неуспех по време на разработката (не при прилагане по време на изпълнение)

  • Nx генератор на работно пространство позволява отделяне на компоненти. Всеки път, когато член на екипа трябва да създаде нов контролер/библиотека/клас/микроуслуга, той просто извиква CLI команда, която кодира продукти въз основа на шаблон на общност или организация. Това налага последователност и споделяне на най-добри практики

Слой 2: Задачи и конвейер за ефективно изграждане на вашия код

Дори в един свят на автономни компоненти има задачи за управление, които трябва да се прилагат групово, като прилагане на корекция за сигурност чрез актуализация на npm, провеждане на тестове на множество компоненти, които са били засегнати от промяна, публикуване 3 свързани библиотеки, за да назовем няколко примера. Всички инструменти на Monorepo поддържат тази основна функционалност за извикване на някаква команда над група пакети. Например Lerna, Nx и Turborepo го правят.

В някои проекти извикването на каскадна команда е всичко, от което се нуждаете. Най-вече ако всеки пакет има автономен жизнен цикъл и процесът на изграждане обхваща един пакет (повече за това по-късно). В някои други типове проекти, където работният процес изисква тестване/пускане и публикуване/внедряване на много пакети заедно — това ще завърши с ужасно бавно изживяване. Помислете за решение със стотици пакети, които са транспилирани и групирани - човек може да изчака минути, за да се изпълни широк тест. Въпреки че не винаги е добра практика да разчитате на широки/E2E тестове, това е доста често срещано в дивата природа. Това е точно мястото, където блести новата вълна от инструменти Monorepo — задълбочено оптимизиране на процеса на изграждане. Трябва да кажа това на глас: Тези инструменти носят красиви и иновативни оптимизации на изграждане:

  • Паралелизиране —Ако две команди или пакети са ортогонални един на друг, командите ще се изпълняват в две различни нишки или процеси. Обикновено вашият контрол на качеството включва тестване, облицовка, проверка на лицензи, проверка на CVE — защо не паралелизирате?
  • Интелигентен план за изпълнение —Освен паралелизирането, оптимизираният ред за изпълнение на задачите се определя въз основа на много фактори. Помислете за компилация, която включва A, B, C, където A, C зависят от B - наивно система за компилация би изчакала B да се компилира и едва след това да изпълни A & C. Това може да бъде оптимизирано, ако стартираме на A & C изолирани единични тестове докатоизграждате B, а не след това. Чрез паралелното изпълнение на задача възможно най-рано, общото време за изпълнение се подобрява - това има забележително въздействие най-вече при хостване на голям брой компоненти. Вижте по-долу пример за визуализация на подобрение на тръбопровода

  • Открийте кой е засегнат от промяна — Дори в система с високо свързване между пакетите обикновено не е необходимо да се изпълняват всичкипакети, а не само тези, които са засегнати от промяна . Какво точно е „засегнати“? Пакети/микроуслуги, които зависят от друг променен пакет. Някои от инструментите могат да игнорират незначителни промени, които е малко вероятно да нарушат други. Това не е голямо усилване на производителността, но също така и невероятна функция за тестване — разработчиците могат да получат бърза обратна връзка дали някой от техните клиенти е повреден. И Nx, и Turborepo поддържат тази функция. Lerna може да каже само кой от пакета Monorepo е променен
  • Подсистеми (т.е. проекти) —Подобно на „засегнатите“ по-горе, съвременните инструменти могат да реализират части от графиката, които са взаимно свързани (проект или приложение), докато други не са достъпни от компонента в контекст (друг проект), така че да знаят да включват само пакети от съответната група
  • Кеширане —Това е сериозен ускорител на скоростта: Nx и Turborepo кешират резултата/изхода от задачите и избягват повторното им изпълнение при последващи компилации, ако е ненужно. Например, помислете за продължителни тестове на микроуслуга, когато давате команда за повторно изграждане на тази микроуслуга, инструментите може да осъзнаят, че нищо не се е променило и тестът ще бъде пропуснат. Това се постига чрез генериране на хеш карта на всички зависими ресурси — ако някой от тези ресурси не се е променил, тогава хеш картата ще бъде същата и задачата ще бъде пропусната. Те дори кешират stdout на командата, така че когато стартирате кеширана версия, тя действа като истинско нещо — помислете за провеждане на 200 теста, виждане на всички оператори в журнала на тестовете, получаване на резултати през терминала за 200 ms, всичко действа като ' реално тестване, докато всъщност тестовете изобщо не се изпълняват, а кеша!
  • Отдалечено кеширане —Подобно на кеширането, само чрез поставяне на хеш картите и резултата на задачата на глобален сървър, така че по-нататъшните изпълнения на компютрите на други членове на екипа също ще пропуснат ненужните задачи. В огромни проекти на Monorepo, които разчитат на E2E тестове и трябва да изградят всички пакети за разработка, това може да спести много време

Слой 3: Повдигнете вашите зависимости, за да подобрите инсталацията на npm

Оптимизациите на скоростта, които бяха описани по-горе, няма да са от полза, ако тясното място е голямата кал, която се нарича „npm install“ (без да критикувам, просто е трудно по природа). Вземете типичен сценарий като пример, предвид десетки компоненти, които трябва да бъдат изградени, те биха могли лесно да задействат инсталирането на хиляди подзависимости. Въпреки че използват доста сходни зависимости (напр. същия регистратор, същия ORM), ако версията на зависимостта не е равна, тогава npm ще дублира („проблемът с двойниците на NPM“) инсталирането на тези пакети, което може да доведе до дълъг процес.

Това е мястото, където линията от инструменти за работно пространство (напр. работно пространство на Yarn, работни пространства npm, PNPM) започва и въвежда известна оптимизация - Вместо да инсталира зависимости във всяка папка „NODE_MODULES“ на компонента, тя ще създаде една централизирана папка и ще свърже всички зависимости през там. Това може да покаже огромно увеличение на времето за инсталиране за огромни проекти. От друга страна, ако винаги се фокусирате върху един компонент в даден момент, инсталирането на пакетите на една микроуслуга/библиотека не трябва да бъде проблем.

Както Nx, така и Turborepo могат да разчитат на пакетния мениджър/работното пространство, за да предоставят този слой оптимизации. С други думи, Nx и Turborepo са слоят над пакетния мениджър, който се грижи за оптимизираната инсталация на зависимости.

Освен това Nx въвежда още една нестандартна, може би дори противоречива техника: може да има само ЕДИН package.json в основната папка на цялото Monorepo. По подразбиране, когато създавате компоненти с помощта на Nx, те няма да имат свой собствен package.json! Вместо това всички ще споделят root package.json. По този начин всички микросервизи/библиотеки споделят своите зависимости и времето за инсталиране се подобрява. Забележка: Възможно е да се създадат „публикуеми“ компоненти, които имат package.json, просто не е по подразбиране.

Тук съм загрижен. Споделянето на зависимости между пакети увеличава свързването, какво ще стане, ако Microservice1 иска да пренасочи версията dependency1, но Microservice2 не може да направи това в момента? Освен това package.json е част от времето за изпълнение на Node.js и изключването му от корена на компонента губи важни функции като главното поле на package.json или ESM експорти (като казва на клиентите кои файлове са изложени). Пуснах малко POC с Nx миналата седмица и се оказах блокиран — библиотека B беше запушена, опитах се да я импортирам от библиотека A, но не можах да получа оператора „import“, за да укажа правилното име на пакета. Естественото действие беше да отворим package.json на B и да проверим името, но няма Package.json… Как да определя името му? Nx документите са страхотни, най-накрая намерих отговора, но трябваше да отделя време за изучаване на нова „рамка“.

Спрете за секунда: Всичко опира до вашия работен процес

Ние се занимаваме с инструменти и функции, но всъщност е безсмислено да оценяваме тези опции, преди да определим дали предпочитаният от вас работен процес е синхронизиран или независим(ще обсъдим това след няколко секунди). Това предварително фундаментално решение ще промени почти всичко.

Разгледайте следния пример с 3 компонента: Библиотека 1 въвежда някои основни и критични промени, Microservice1 и Microservice2 зависят от Library1 и трябва да реагират на тези критични промени. как?

Вариант А — Синхронизираният работен процес-Следвайки този стил на разработка, всичките три компонента ще бъдат разработени и внедрени в едно парче заедно. На практика разработчикът ще кодира промените в Library1, ще тества libray1 ​​и също така ще проведе тестове за широка интеграция/e2e, които включват Microservice1 и Microservice2. Когато са готови, версията на всички компоненти ще бъде увеличена. Накрая те ще бъдат разположени заедно.

При този подход разработчикът има шанса да види пълния поток от гледна точка на клиента (Microservice1 и 2), тестовете обхващат не само библиотеката, но и през очите на клиентите, които действително я използват. От друга страна, той изисква актуализиране на всички зависещи компоненти (може да са десетки), като това увеличава радиуса на взрива на риска, тъй като повече единици са засегнати и трябва да се обмисли преди разгръщането. Освен това работата върху голяма единица работа изисква изграждане и тестване на повече неща, което ще забави изграждането.

Вариант Б — Независим работен процес-Този стил е за работа единица по единица, една хапка наведнъж и внедряване на всеки компонент независимо въз основа на неговите лични бизнес съображения и приоритет. Ето как става: Разработчик прави промените в Library1, те трябва да бъдат тествани внимателно в обхвата на Library1. След като е готова, SemVer се пренасочва към нова специалност и библиотеката се публикува в регистър на мениджър на пакети (напр. npm). Какво ще кажете за клиентските микроуслуги? Е, екипът на Microservice2 сега е супер зает с други приоритети и пропуснете тази актуализация за сега (същото, което всички забавяме много от нашите npm актуализации). Microservice1 обаче е много заинтересован от тази промяна — Екипът трябва проактивно да актуализира тази зависимост и да вземе най-новите промени, да изпълни тестовете и когато са готови, днес или следващата седмица — да я внедри.

Следвайки независимия работен процес, авторът на библиотеката може да се движи много по-бързо, защото не е необходимо да взема под внимание 2 или 30 други компонента — някои са кодирани от различни екипи. Този работен процес също я принуждава да пише ефективни тестове срещу библиотеката — това е единствената й предпазна мрежа и вероятно ще завърши с автономни компоненти, които имат слабо свързване с други. От друга страна, тестването в изолация без гледната точка на клиента губи някакво измерение на реализъм. Освен това, ако един разработчик трябва да актуализира 5 единици — публикуването на всяка отделно в регистъра и след това актуализирането във всички зависимости може да бъде малко досадно.

За илюзията за синхрон

В разпределените системи не е възможно да се постигне 100% синхронност - вярването в противен случай може да доведе до грешки в дизайна. Помислете за критична промяна в Microservice1, сега неговият клиент Microservice2 се адаптира и е готов за промяната. Тези две микроуслуги се внедряват заедно, но поради естеството на микроуслугите и разпределеното време за изпълнение (напр. Kubernetes) внедряването само на Microservice1 е неуспешно. Сега кодът на Microservice2 не е съобразен с продукцията на Microservice1 и сме изправени пред производствена грешка. Тази поредица от неуспехи може да се управлява до известна степен и със синхронизиран работен процес — внедряването трябва да организира внедряването на всяка единица, така че всяка да се внедрява наведнъж. Въпреки че този подход е осъществим, той увеличава шансовете за връщане назад с голям обхват и увеличава страха от внедряване.

Това фундаментално решение, синхронизирано или независимо, ще определи толкова много неща - дали производителността е проблем или изобщо не (когато работите върху единична единица), повдигане на зависимости или оставяне на специални node_modules във всяка папка на пакета и дали да се създаде локален връзка между пакети, която е описана в следващия параграф.

Слой 4: Свържете вашите пакети за незабавна обратна връзка

Когато имате Monorepo, винаги има неизбежната дилема как да свържете компонентите:

Опция 1: Използване на npm —Всяка библиотека е стандартен npm пакет и неговият клиент го инсталира чрез стандартните npm команди. Като се има предвид Microservice1 и Library1, това ще завърши с две копия на Library1: едното вътре в Microservices1/NODE_MODULES (т.е. локалното копие на консумиращата Microservice), а второто е папката за разработка, където екипът кодира Library1.

Опция 2: Просто обикновена папка —С това Library1 не е нищо друго освен логически модул в папка, която Microservice1,2,3 просто импортира локално. NPM не е включен тук, това е просто код в специална папка. Ето как например са представени модулите Nest.js.

С опция 1 екипите се възползват от всички страхотни предимства на мениджъра на пакети — SemVer(!), инструменти, стандарти и т.н. Въпреки това, ако някой актуализира Library1, промените няма да бъдат отразени в Microservice1, тъй като той грабва своето копие от npm регистъра и промените все още не са публикувани. Това е фундаментална болка с Monorepo и мениджърите на пакети - човек не може просто да кодира множество пакети и да тества/пуска промените.

С опция 2 екипите губят всички предимства на мениджъра на пакети: всяка промяна се разпространява незабавно до всички потребители.

Как да носим доброто и от двата свята (вероятно)? Използване на свързване. Lerna, Nx, различните работни пространства за управление на пакети (Yarn, npm и т.н.) позволяват използването на npm библиотеки и в същото време връзка между клиентите (напр. Microservice1) и библиотеката. Под капака те създадоха символична връзка. В режим на разработка промените се разпространяват незабавно, по време на разгръщане — копието се грабва от регистъра.

Ако изпълнявате синхронизирания работен процес, всичко е готово. Само сега всяка рискована промяна, въведена от Library3, трябва да се обработи СЕГА от 10-те микроуслуги, които я консумират.

Ако предпочитате независимия работен процес, това разбира се е голямо безпокойство. Някои може да нарекат този стил на директно свързване „монолитен монорепо“ или може би „монолито“. Въпреки това, когато не се свързва, е по-трудно да се отстранят грешки при малък проблем между Microservice и библиотеката npm. Това, което обикновено правя, е временно свързване(с npm връзка) между пакетите, отстраняване на грешки, код, след което накрая премахвам връзката.

Nx използва малко по-разрушителен подход — използва „пътеки на TypeScript“ за свързване между компонентите. Когато Microservice1 импортира Library1, за да избегне пълния локален път, той създава съпоставяне на TypeScript между името на библиотеката и пълния път. Но изчакайте малко, няма TypeScript в производство, така че как може да работи? Е, във времето за сервиране/пакет той пакетира и свързва компонентите заедно. Не е много стандартен начин за работа с Node.js.

Затваряне: Какво трябва да използвате?

Всичко опира до вашия работен процес и архитектура – ​​огромен невиждан кръстопът стои пред решението за инструменти Monorepo.

Сценарий A —Ако вашата архитектура диктува синхронизиран работен поток, при който всички пакети се внедряват заедно или поне се разработват в сътрудничество — тогава има силна нужда от богат инструмент за управление това свързване и повишаване на производителността. В този случай Nx може да е чудесен избор.

Например, ако вашата микроуслуга трябва да поддържа същото управление на версиите или ако екипът е наистина малък и едни и същи хора актуализират всички компоненти, или ако вашата модулация не е базирана на мениджър на пакети, а по-скоро на модули, притежаващи рамка (напр. Nest. js), ако правите интерфейс, където компонентите по същество са публикувани заедно, или ако вашата стратегия за тестване разчита най-вече на E2E — за всички тези случаи и други, Nx е инструмент, който е създаден, за да подобри изживяването при кодиране на много относителносвързани компоненти заедно. Това е страхотно захарно покритие върху системи, които неизбежно са големи и свързани.

Ако системата ви не е голяма по своята същност или не е предназначена да синхронизира внедряването на пакети, фантастичните функции на Monorepo може да увеличат свързването между компонентите. Пирамидата Monorepo по-горе очертава линия между основните функции, които осигуряват стойност без свързващи компоненти, докато други слоеве идват с архитектурна цена, която трябва да се вземе предвид. Понякога изкачването към върха си заслужава последствията, просто вземете това решение съзнателно.

Сценарий B—Ако сте в независим работен процескъдето всеки пакет се разработва, тества и внедрява (почти) независимо — тогава по същество няма нужда да си представяте инструменти за оркестрирайте стотици пакети. През повечето време има само един пакет на фокус. Това изисква избор на по-икономичен и по-прост инструмент — Turborepo. Следвайки този път, Monorepo не е нещо, което засяга вашата архитектура, а по-скоро инструмент с обхват за по-бързо изпълнение на компилация. Един конкретен инструмент, който насърчава независимия работен процес, е „Bilt“ от Gil Tayar, той все още не е спечелил достатъчно популярност, но скоро може да нарасне и е чудесен източник да научите повече за тази философия на работа.

Във всеки сценарий помислете за работни пространства —Ако се сблъскате с проблеми с производителността, които са причинени от инсталиране на пакет, тогава различните инструменти за работно пространство Yarn/npm/PNPM могат значително да минимизират това натоварване с малък отпечатък. Въпреки това, ако работите в автономен работен процес, шансовете да се сблъскате с подобни проблеми са по-малки. Не използвайте само инструменти, освен ако не изпитвате болка.

Опитахме се да покажем красотата на всеки и къде блести. Ако ни е позволено да завършим тази статия с убеден избор: Ние силно вярваме в независим и автономен работен процес, при който случаен разработчик на пакет може да кодира и внедрява безстрашно, без да се забърква с десетки други чужди пакети. Поради тази причина Turborepo ще бъде нашият любим инструмент за следващия сезон. Обещаваме да ви кажем как върви.

Бонус: Сравнителна таблица

Вижте по-долу подробна таблица за сравнение на различните инструменти и функции:

Само визуализация, пълната таблица може да бъде намерена тук