• typescript
  • monorepo

TypeScript all-in-one: Monorepo z jego bólami i zyskami

W tym artykule nie porównam monorepo i polyrepo, ponieważ to wszystko jest kwestią filozofii. Zamiast tego, skupię się na budowaniu i ewoluującym doświadczeniu i zakładam, że znasz ekosystem JS / TS.

Gao
Gao
Founder

Wprowadzenie

Zawsze marzyłem o monorepo.

Zobaczyłem podejście monorepo podczas pracy dla Airbnb, ale dotyczyło to tylko frontendu. Z głęboką miłością do ekosystemu JavaScript i „szczęśliwego” doświadczenia z rozwojem TypeScriptu, zacząłem dopasowywać kod frontendu i backendu w tym samym języku od około trzech lat temu. Było to fantastyczne (pod względem zatrudnienia), ale nie tak wspaniałe podczas tworzenia, ponieważ nasze projekty wciąż były rozproszone po wielu repozytoriach.

Jak mówi przysłowie, „najlepszym sposobem na restrukturyzację projektu jest rozpoczęcie nowego”. Więc kiedy roku temu zaczynałem swój startup, postanowiłem zastosować totalną strategię monorepo: umieścić projekty frontendu i backendu, a nawet schematy bazy danych, w jednym repozytorium.

W tym artykule nie porównuję monorepo i polyrepo, ponieważ wszystko to jest kwestią filozofii. Zamiast tego skupię się na budowie i ewolucji i założę, że znasz ekosystem JS/TS.

Ostateczny wynik jest dostępny na [GitHub] (https://github.com/logto-io/logto).

Dlaczego TypeScript?

Szczerze mówiąc, jestem fanem JavaScriptu i TypeScriptu. Kocham kompatybilność jego elastyczności i rygorystyczności: możesz wrócić do unknown lub any (chociaż zabroniliśmy jakiejkolwiek formy any w naszym repozytorium kodu), lub użyć super-ścisłego zestawu reguł lint do dopasowania stylu kodu w całym zespole.

Kiedy rozmawialiśmy o koncepcji „fullstack” wcześniej, zwykle wyobrażaliśmy sobie co najmniej dwa ekosystemy i języki programowania: jeden dla frontendu i jeden dla backendu.

Pewnego dnia nagle zdałem sobie sprawę, że może być prościej: Node.js jest wystarczająco szybki (wierz mi, w większości przypadków jakość kodu jest ważniejsza niż prędkość biegu), TypeScript jest na tyle dojrzały (sprawdza się w dużych projektach frontendowych), a koncepcja monorepo została wypróbowana przez wiele znanych zespołów (React, Babel, itp.) - więc dlaczego by nie połączyć cały kod razem, od frontendu do backendu? To pozwoli inżynierom wykonywać prace bez zmiany kontekstu w jednym repozytorium i implementować kompletną funkcję w (prawie) jednym języku.

Wybór menedżera pakietów

Jako deweloper, jak zwykle, nie mogłem się doczekać, kiedy zacznę kodować. Ale tym razem było inaczej.

Wybór menedżera pakietów ma kluczowe znaczenie dla doświadczenia dewelopera w monorepo.

Ból inercji

To był lipiec 2021 roku. Zacząłem od [email protected], ponieważ używałem go od dawna. Yarn był szybki, ale wkrótce napotkałem kilka problemów z Yarn Workspaces. Na przykład [nieprawidłowe wynoszenie zależności] (https://github.com/yarnpkg/yarn/issues/7572), a tony problemów są oznaczone jako „[naprawione we współczesnym] (https://github.com/yarnpkg/yarn/issues?q=label%3Afixed-in-modern+) ”, które przekierowują mnie do wersji 2 ([berry] (https: //github.com/yarnpkg/berry)).

„Dobrze, dobrze, teraz się aktualizuję.” Przestałem się zmagając z wersją 1 i zacząłem migrować. Ale długi [poradnik migracji] (https://yarnpkg.com/getting-started/migration) berry przestraszył mnie i poddałem się po kilku nieudanych próbach.

Po prostu działa

Więc badania nad menedżerami pakietów się zaczęły. Zostałem pochłonięty przez pnpm po próbie: szybki jak yarn, natywne wsparcie monorepo, podobne polecenia do npm, twarde linki itp. Co najważniejsze, to po prostu działa. Jako deweloper, który chce zacząć z produktem, ale NIE rozwija menedżera pakietów, chciałbym tylko dodać kilka zależności i rozpocząć projekt bez wiedzy, jak działa menedżer pakietów lub jakieś inne fancy koncepcje.

Na tej samej zasadzie wybraliśmy starych przyjaciół lerna do wykonywania poleceń w pakietach i publikowania pakietów przestrzeni roboczej.

Definiowanie zakresów pakietów

Trudno jest dokładnie określić ostateczny zakres każdego pakietu na początku. Po prostu zacznij od swojego najlepszego podejścia według aktualnego status quo i pamiętaj, że zawsze możesz przeprowadzić refaktoryzację podczas rozwoju.

Nasza [początkowa struktura] (https://github.com/logto-io/logto/tree/af7e6ccd83723d623555dafa4650e115fa795838/packages) zawiera cztery pakiety:

  • core: monolityczna usługa backendowa.
  • phrases: klucz i18n → zasoby fraz.
  • schemas: baza danych i wspólne schematy TypeScript.
  • ui: web SPA, który współpracuje z core.

Tech stack do fullstack

Ponieważ przyjmujemy ekosystem JavaScript i używamy TypeScript jako naszego głównego języka programowania, wiele wyborów jest oczywistych (na podstawie mojego gustu 😊):

  • koajs dla usługi backendowej (core): miałam trudne doświadczenia z użyciem async / await w express, więc zdecydowałam się na coś z natywnym wsparciem.
  • i18next / react-i18next dla i18n (phrases / ui): podoba mi się jego prostota interfejsów API i dobre wsparcie TypeScript.
  • react dla SPA (ui): Po prostu osobiste preferencje.

Co z schematami?

Coś tutaj jeszcze brakuje: system bazy danych i mapowanie schematu <-> Definicja TypeScript.

Ogólne kontra uprzedzenie

W tym momencie próbowałem dwóch popularnych podejść:

  • Użyj ORM z wieloma dekoratorami.
  • Użyj budowniczego zapytań, takiego jak Knex.js.

Ale oba wywołały dziwne uczucie podczas poprzedniego rozwoju:

  • Dla ORM: Nie jestem fanem dekoratorów, a kolejna abstrakcyjna warstwa bazy danych powoduje większy wysiłek uczenia się i niepewność dla zespołu.
  • Dla budowniczego zapytań: To jak pisanie SQL z pewnymi ograniczeniami (w dobry sposób), ale to nie jest prawdziwe SQL. Stąd musimy używać .raw () do surowych zapytań w wielu scenariuszach.

Wtedy zobaczyłem ten artykuł: „[Przestań używać Knex.js: Używanie budowniczego zapytań SQL to antywzorzec] (https://gajus.medium.com/stop-using-knex-js-and-earn-30-bf410349856c)”. Tytuł wygląda agresywnie, ale treść jest świetna. Przypomina mi mocno, że „SQL to język programowania”, a zdałem sobie sprawę, że mogę pisać SQL bezpośrednio (tak jak CSS, jak mogłem to przegapić!) Aby wykorzystać natywny język i funkcje bazy danych, zamiast dodawać inną warstwę i zmniejszać moc.

Podsumowując, zdecydowałem się trzymać Postgresa i [Slonik] (https://github.com/gajus/slonik) (otwarty klient Postgres), jak stwierdza artykuł:

... korzyść z pozwalania użytkownikowi na wybór między różnymi dialektami baz danych jest marginalna, a nakład pracy związany z jednoczesnym rozwijaniem dla wielu baz danych jest znaczny.

SQL <-> TypeScript

Inną zaletą pisania SQL jest to, że możemy łatwo używać go jako pojedynczego źródła prawdy definicji TypeScript. Napisałem [generator kodu] (https://github.com/logto-io/logto/tree/af7e6ccd83723d623555dafa4650e115fa795838/packages/schemas/src/gen) do transpilowania schematów SQL na kod TypeScript, który będziemy używać w naszym backendzie, a wynik wygląda nieźle:

Możemy nawet połączyć jsonb z typem TypeScript i przeprowadzić walidację typu w usłudze backendu, jeśli to potrzebne.

Wynik

Ostateczna struktura zależności wygląda tak:

Możesz zauważyć, że jest to diagram w jednym kierunku, który bardzo nam pomógł utrzymać przejrzystą architekturę i zdolność do rozwijania projektu. Plus, kod jest (zasadniczo) wszystko w TypeScript.

Doświadczenie dewelopa

Udostępnianie pakietów i konfiguracji

Wewnętrzne zależności

pnpm i lerna robią niesamowitą robotę na wewnętrznych zależnościach przestrzeni roboczej. Używamy poniższego polecenia w głównym katalogu projektu, aby dodać pakiety rodzeństwa:

Doda to @logto/schemas jako zależność do @logto/core. Podczas zachowania semantycznej wersji w package.json twoich wewnętrznych zależności, pnpm może też poprawnie je łączyć w pnpm-lock.yaml. Wynik będzie wyglądał tak:

Udostępnianie konfiguracji

Traktujemy każdy pakiet w monorepo jako „niezależny”. Dlatego możemy używać standardowego podejścia do udostępniania konfiguracji, które obejmuje tsconfig, eslintConfig, prettier, stlyelint i jest-config. Zobacz [ten projekt] (https://github.com/logto-io/logto/tree/6327eb6c577cdf36c8f44b55bac8195f7d6a6335/packages/console) na przykład.

Kody, fleki i zobowiązanie

Używam VSCode do codziennego rozwoju, a w skrócie, nic nie jest inne, gdy projekt jest poprawnie skonfigurowany:

  • ESLint i Stylelint działają normalnie.
    • Jeżeli używasz wtyczki ESLint dla VSCode, dodaj poniższe ustawienia VSCode, aby zmusić go do przestrzegania konfiguracji ESLint na pakiet (zastąp wartość pattern swoją własną):
  • husky, commitlint i lint-staged działają zgodnie z oczekiwaniami.

Kompilator i proxy

Używamy różnych kompilatorów dla frontendu i backendu: parceljs dla UI (React) i tsc dla wszystkich innych czystych pakietów TypeScript. Gorąco polecam wypróbować parceljs, jeśli jeszcze tego nie zrobiłeś. To prawdziwy „kompilator zerowej konfiguracji”, który z wdziękiem obsługuje różne typy plików.

Parcel obsługuje własny serwer dev frontendu, a wyjście produkcyjne to po prostu pliki statyczne. Ponieważ chcielibyśmy zamontować API i SPA pod tym samym pochodzeniem, aby uniknąć problemów z CORS, strategia poniżej działa:

  • W środowisku deweloperskim używaj prostego serwera proxy HTTP, aby przekierować ruch do serwera dev Parcel.

  • W produkcji, bezpośrednio zarządzaj plikami statycznymi.

Możesz znaleźć implementację funkcji middleware frontendu [tutaj] (https://github.com/logto-io/logto/blob/6327eb6c577cdf36c8f44b55bac8195f7d6a6335/packages/core/src/middleware/koa-spa-proxy.ts).

Tryb watch

Mamy skrypt dev w package.json dla każdego pakietu, który obserwuje zmiany w plikach i ponownie kompiluje, gdy to potrzebne. Dzięki lerna, rzeczy stają się łatwe za pomocą lerna exec do uruchamiania skryptów pakietów równolegle. Główny skrypt będzie wyglądał tak:

Podsumowanie

Idealnie, tylko dwa kroki dla nowego inżyniera / kontrybutora, aby zacząć:

  1. Sklonuj repo
  2. pnpm i && pnpm dev

Uwagi końcowe

Nasz zespół rozwijał się w ten sposób przez rok i jesteśmy z niego bardzo zadowoleni. Odwiedź nasze [repo na GitHubie] (https://github.com/logto-io/logto), aby zobaczyć najnowszy kształt projektu. Podsumowując:

Bóle

  • Potrzeba znajomości ekosystemu JS / TS
  • Musisz wybrać odpowiedniego menedżera pakietów
  • Wymaga pewnych dodatkowych jednorazowych ustawień

Zyski

  • Tworzenie i utrzymanie całego projektu w jednym repozytorium
  • Uproszczone wymagania dotyczące umiejętności kodowania
  • Wspólne style kodowania, schematy, frazy i narzędzia
  • Poprawiona efektywność komunikacji
    • Nie ma już pytań typu: Co to jest definicja API?
    • Wszyscy inżynierowie rozmawiają w tym samym języku programowania
  • Łatwe CI / CD
    • Użyj tego samego zestawu narzędzi do budowania, testowania i publikowania

Ten artykuł pozostawia kilka tematów niepokrytych: Ustawianie repozytorium od początku, dodawanie nowego pakietu, wykorzystanie GitHub Actions do CI / CD itp. Byłoby za długo na ten artykuł, gdybym rozwinął każdy z nich. Śmiało komentuj i daj mi znać, który temat chciałbyś zobaczyć w przyszłości.