• typescript
  • monorepo

TypeScript kaikki yhdessä: Monorepo sen tuskat ja voitot

Tässä artikkelissa en vertaile monorepoa ja polyrepoa, koska kyse on filosofiasta. Sen sijaan keskityn rakentamiseen ja kehittymiskokemukseen ja oletan, että olet tuttu JS/TS-ekosysteemin kanssa.

Gao
Gao
Founder

Intro

Minulla on aina ollut unelma monoreposta.

Näin monorepo-lähestymistavan työskennellessäni Airbnb:llä, mutta se oli vain frontendille. Suuren rakkauden JavaScript-ekosysteemiin ja "onnelliseen" TypeScript-kehityskokemukseen ansiosta aloin yhdistää frontend- ja backend-koodia samaan kieleen noin kolme vuotta sitten. Se oli mahtavaa (rekrytointia varten), mutta ei niin mahtavaa kehittää, koska projektimme olivat edelleen hajallaan useissa repoissa.

Kuten sanotaan, "paras tapa refaktoroida projektia on aloittaa uusi". Joten kun aloitin startupini noin vuosi sitten, päätin käyttää täydellistä monorepo-strategiaa: yhdistä frontend- ja backend-projektit, jopa tietokantaskeemat, yhteen repoon.

Tässä artikkelissa en vertaile monorepoa ja polyrepoa, koska kyse on filosofiasta. Sen sijaan keskityn rakentamiseen ja kehittymiskokemukseen ja oletan, että olet tuttu JS/TS-ekosysteemin kanssa.

Lopputulos on saatavilla GitHubissa.

Miksi TypeScript?

Rehellisesti sanottuna olen JavaScriptin ja TypeScriptin fani. Rakastan sen joustavuuden ja tiukkuuden yhteensopivuutta: voit palata unknown tai any-tyyppeihin (vaikka koodipohjassamme olemme kieltäneet minkäänlaisen anyn), tai käyttää erittäin tiukkaa lint-sääntöjoukkoa yhdenmukaistaaksesi koodityyliä tiimin kesken.

Kun puhuimme "fullstack"-konseptista aiemmin, kuvittelimme yleensä vähintään kaksi ekosysteemiä ja ohjelmointikieltä: yksi frontendille ja yksi backendille.

Eräänä päivänä tajusin, että se voisi olla yksinkertaisempaa: Node.js on tarpeeksi nopea (usko minua, useimmissa tapauksissa koodin laatu on tärkeämpää kuin suoritusnopeus), TypeScript on tarpeeksi kypsä (toimii hyvin suurissa frontend-projekteissa), ja monorepo-konseptia ovat käyttäneet monet kuuluisat tiimit (React, Babel jne.) - joten miksemme yhdistäisi kaikkea koodia yhteen, frontendistä backendiin? Tämä voi tehdä insinöörien työstä helpompaa ilman kontekstinvaihtoa yhdessä repossa ja toteuttaa täydellinen ominaisuus (melkein) yhdellä kielellä.

Pakettimanagerin valinta

Kehittäjänä, ja kuten tavallisesti, en malttanut odottaa koodaamisen aloittamista. Mutta tällä kertaa asiat olivat erilaisia.

Pakettimanagerin valinta on kriittinen kehittäjäkokemukselle monorepossa.

Inertian tuska

Oli heinäkuu 2021. Aloitin [email protected]:llä, koska olen käyttänyt sitä pitkään. Yarn oli nopea, mutta pian kohtasin useita ongelmia Yarn Workspacesin kanssa. Esim. ei nostanut riippuvuuksia oikein, ja lukemattomia ongelmia on merkitty "korjattu modernissa", mikä ohjaa minut v2:een (berry).

"Okei, hienoa, päivitän nyt." Lopetin kamppailun v1:n kanssa ja aloin siirtyä. Mutta berryn pitkä migrationsopas pelotti minua, ja luovutin muutaman epäonnistuneen yrityksen jälkeen.

Se vain toimii

Joten pakettimanagerien tutkimus alkoi. Olin imarreltu pnpmistä kokeilun jälkeen: nopea kuten yarn, natiivimonorepotuki, samanlaiset komennot kuten npm, kiintolinkit jne. Tärkein asia on, että se vain toimii. Kehittäjänä, joka haluaa aloittaa tuotteen mutta EI kehittää pakettimanageria, halusin vain lisätä joitain riippuvuuksia ja aloittaa projektin ilman tietoa siitä, miten pakettimanageri toimii tai muita hienoja konsepteja.

Saman idean pohjalta valitsimme vanhan ystävän lerna komenn izvravljanje across пакaettien ja julkaisemisen työtilapaketteja.

Pakettialueiden määrittely

On vaikeaa selkeästi hahmottaa kunkin paketin lopullista laajuutta alussa. Aloita vain parhaalla yritykselläsi vallitsevan tilanteen mukaan ja muista, että voit aina refaktoroida kehityksen aikana.

Meidän alkurakenne sisältää neljä pakettia:

  • core: taustajärjestelmän monoliittipalvelu.
  • phrases: i18n avain → fraasi-resurssit.
  • schemas: tietokanta ja jaetut TypeScript-skeemat.
  • ui: web SPA, joka on vuorovaikutuksessa core kanssa.

Täydellinen teknologiapino

Koska omaksumme JavaScript-ekosysteemin ja käytämme TypeScriptiä pääohjelmointikielenä, monet valinnat ovat suoraviivaisia (perustuen mieltymyksiini 😊):

  • koajs taustajärjestelmän palvelulle (core): Minulla oli vaikea kokemus async/awaitn käytöstä express:ssä, joten päätin käyttää jotain natiivituen kanssa.
  • i18next/react-i18next i18n:lle (phrases/ui): sen API:n yksinkertaisuuden ja hyvän TypeScript-tuen vuoksi.
  • react SPA:lle (ui): henkilökohtainen mieltymys vain.

Entä skeemat?

Jotain puuttuu edelleen: tietokantajärjestelmä ja skeeman <-> TypeScript-määritelmän kartoitus.

Yleinen vs. mielipiteitä

Tuohon aikaan kokeilin kahta suosittua lähestymistapaa:

  • Käytä ORM:ää monilla dekoroinnilla.
  • Käytä kyselyrakentajaa kuten Knex.js.

Mutta molemmat tuottivat oudon tunteen aiemman kehityksen aikana:

  • ORM:lle: En pidä dekoroinnista, ja toinen abstrakti kerros tietokannasta aiheuttaa enemmän oppimispyrkimystä ja epävarmuutta tiimille.
  • Kyselyrakentajalle: Se on kuin kirjoittaisi SQL:ää joillakin rajoituksilla (hyvällä tavalla), mutta se ei ole todellista SQL:ää. Siksi meidän pitää käyttää .raw()-kyselyjä monissa tilanteissa.

Sitten näin tämän artikkelin: "Lopeta Knex.js:n käyttäminen: SQL-kyselyrakentajan käyttäminen on anti-pattern". Otsikko näyttää aggressiiviselta, mutta sisältö on loistavaa. Se muistutti minua vahvasti, että "SQL on ohjelmointikieli", ja tajusin, että voisin kirjoittaa suoraan SQL:ää (kuten CSS, miten voisin unohtaa tämän!) hyödyntääkseni natiivikieltä ja tietokantaominaisuuksia sen sijaan, että lisäisin toisen kerroksen ja vähentäisin voimaa.

Lopuksi päätin pysyä Postgresin ja Slonikin (avoin lähdekoodillinen Postgres-asiakas) kanssa, kuten artikkelissa todetaan:

…käyttäjälle annettavan tietokantadialektien valinnan hyöty on marginaalinen ja useille tietokantoille kehittämisen kustannukset ovat merkittävät.

SQL <-> TypeScript

Toinen etu SQL:n kirjoittamisessa on, että voimme käyttää sitä helposti TypeScript-määritelmien ainoana totuuden lähteenä. Kirjoitin koodigeneraattorin SQL-skeemojen muuntamiseksi TypeScript-koodiin, jota käytämme backendissämme, ja tulos ei näytä pahalta:

Voimme jopa yhdistää jsonb TypeScript-tyypin kanssa ja käsitellä tyypin validointia taustajärjestelmäpalvelussa tarvittaessa.

Tulokset

Lopullinen riippuvaisuusrakenne näyttää tältä:

Saatat huomata, että se on yksisuuntainen kaavio, mikä auttoi meitä suuresti pitämään selkeän arkkitehtuurin ja laajentamismahdollisuuden, kun projekti kasvaa. Lisäksi koodi on (pohjimmiltaan) kaikki TypeScriptissä.

Kehittäjäkokemus

Paketti ja konfiguraation jakaminen

Sisäiset riippuvuudet

pnpm ja lerna tekevät upeaa työtä sisäisistä työtilariippuvuuksista. Käytämme alla olevaa komentoa projektin juuresta lisätäksesi sisaruspakkauksia:

Se lisää @logto/schemas riippuvuudeksi @logto/core:lle. Kun pidät semanttisen version sisäisiä riippuvuuksia package.jsonissa, pnpm voi myös linkittää ne oikein pnpm-lock.yamlissa. Tulokset näyttävät tältä:

Konfiguraation jakaminen

Kohtelemme jokaista pakettia monorepossa "itsenäisesti". Näin voimme käyttää standardimenetelmää konfiguraation jakamiseen, mikä kattaa tsconfig, eslintConfig, prettier, stylelint ja jest-config. Katso tätä projektia esimerkkinä.

Koodaus, linttaus ja sitoutuminen

Käytän VSCodea päivittäiseen kehitykseen, ja lyhyesti sanottuna, mikään ei ole erilaista, kun projekti on oikein konfiguroitu:

  • ESLint ja Stylelint toimivat normaalisti.
    • Jos käytät VSCode ESLint -laajennusta, lisää alla oleva VSCode-asetus, jotta se kunnioittaa per-paketti ESLint-konfiguraatiota (korvaa "pattern" omaan arvoosi):
  • husky, commitlint ja lint-staged toimivat odotetusti.

Kääntäjä ja välityspalvelin

Käytämme erilaisia kääntäjiä frontendille ja backendille: parceljs UI:lle (React) ja tsc kaikille muille puhtaille TypeScript-paketeille. Suosittelen vahvasti, että kokeilet parceljs:ää, jos et ole vielä tehnyt niin. Se on todellinen "nollakonfiguraatiokääntäjä", joka käsittelee erilaisia tiedostotyyppejä hienosti.

Parcel isännöi omaa frontend-kehityspalvelinta, ja tuotantolähtö on vain staattisia tiedostoja. Koska haluaisimme liittää APIt ja SPA:n saman alkuperän alle välttääksemme CORS-ongelmia, alla oleva strategia toimii:

  • Kehitysympäristössä käytä yksinkertaista HTTP-välityspalvelinta ohjataksesi liikennettä Parcel-kehityspalvelimelle.
  • Tuotannossa palvele staattiset tiedostot suoraan.

Voit löytää frontend-ohjelmointifunktioiden toteutuksen täältä.

Katselutila

Meillä on dev-skripti package.json:ssa jokaiselle paketille, joka seuraa tiedostomuutoksia ja kompilee uudelleen tarvittaessa. Kiitos lerna:lle, asiat tulevat helpoiksi käyttämällä lerna execiä pakettiskriptien suorittamiseen rinnakkain. Juuren skripti näyttää tältä:

Yhteenveto

Ihanteellisesti vain kaksi vaihetta uudelle insinöörille/osallistujalle aloittamiseen:

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

Päättelyhuomautuksia

Tiimimme on kehittänyt tämän lähestymistavan alla vuoden ajan, ja olemme melko tyytyväisiä siihen. Vieraile GitHub-repossamme, jotta näet projektin viimeisimmän muodon. Tiivistääkseen:

Tuskat

  • Tarvitaan JS/TS-ekosysteemin tuntemusta
  • Tarvitaan oikean pakettimanagerin valitseminen
  • Vaatii jonkin verran lisäyksiä kertaluonteiseen asentamiseen

Voitot

  • Koko projektin kehittäminen ja ylläpito yhdessä repossa
  • Yksinkertaistetut koodausosaamisen vaatimukset
  • Jaetut koodityylit, skeemat, fraasit ja työkalut
  • Parantunut kommunikaation tehokkuus
    • Ei enää kysymyksiä, kuten: Mikä on API-määritelmä?
    • Kaikki insinöörit puhuvat samaa ohjelmointikieltä
  • CI/CD:n helppous
    • Käytä samaa työkaluketjua rakentamiseen, testaamiseen ja julkaisemiseen

Tämä artikkeli jättää useita aiheita kattamatta: Repon asettaminen alusta alkaen, uuden paketin lisääminen, GitHub-toimintojen hyödyntäminen CI/CD:lle jne. Se olisi liian pitkä tälle artikkelille, jos laajennan jokaista niistä. Kommentoithan vapaasti ja kerro, mistä aiheesta haluaisit kuulla tulevaisuudessa.