Türkçe
  • typescript
  • monorepo

TypeScript her şey dahil: Acıları ve kazanımlarıyla birlikte Monorepo

Bu makalede, felsefe hakkında olduğu için monorepo ve polyrepo'yu karşılaştırmayacağım. Bunun yerine, yapı inşa etme ve geliştirme deneyimine odaklanacağım ve JS/TS ekosistemine aşina olduğunuzu varsayacağım.

Gao
Gao
Founder

Giriş

Her zaman bir monorepo rüyam vardı.

Airbnb için çalışırken monorepo yaklaşımını gördüm, ancak bu sadece frontend içindi. JavaScript ekosistemi ve “mutlu” TypeScript geliştirme deneyimine olan derin bir sevgiyle, frontend ve backend kodunu aynı dilde hizalama sürecine yaklaşık üç yıl önce başladım. Bu, işe alım için harika idi, ancak projelerimiz hala birden çok repo'da dağılmış olduğu için geliştirme adına o kadar iyi değildi.

Denildiği gibi, “bir projeyi yeniden yapılandırmanın en iyi yolu yeni bir başlangıç yapmaktır”. Bu yüzden bir yıl önce startup'ımı başlatmaya başladığımda, toplam bir monorepo stratejisi kullanmaya karar verdim: frontend ve backend projelerini, hatta veritabanı şemalarını, tek bir repo'ya koydum.

Bu makalede, felsefe hakkında olduğu için monorepo ve polyrepo'yu karşılaştırmayacağım. Bunun yerine, yapı inşa etme ve geliştirme deneyimine odaklanacağım ve JS/TS ekosistemine aşina olduğunuzu varsayacağım.

Final sonucuna GitHub üzerinde ulaşılabilir.

Neden TypeScript?

Dürüst olmak gerekirse, ben bir JavaScript ve TypeScript hayranıyım. Esnekliği ve rigorozluğunun uyumluluğunu severim: unknown veya any ye geri dönebilir (her ne kadar kod tabanımızda any formunun her türlüsünü yasaklasak da) veya takım boyunca kod tarzını hizalayan süper katı bir lint kural setini kullanabilirsiniz.

“Fullstack” kavramı hakkında konuştuğumuzda, genellikle en az iki ekosistem ve programlama dili hayal ederiz: biri frontend, diğeri backend için.

Bir gün, daha basit olabileceğini aniden fark ettim: Node.js yeterince hızlıdır (inanın bana, çoğu durumda, kod kalitesi çalışma hızından daha önemlidir), TypeScript yeterince olgunlaşmıştır (büyük frontend projelerinde iyi çalışır) ve monorepo kavramı bir dizi ünlü takım tarafından uygulanmıştır (React, Babel, vb.) - o yüzden neden tüm kodu bir araya getirmiyoruz, frontend'den backend'e? Bu, mühendislerin aynı repo'da iş yapmasını ve bir özelliği (neredeyse) bir dilde uygulamalarını sağlar.

Paket yöneticisi seçmek

Bir geliştirici olarak, ve her zamanki gibi, kodlamaya başlamak için sabırsızlandım. Ama bu sefer, işler farklıydı.

Paket yöneticisinin seçimi, bir monorepo'da geliştirme deneyimi için kritiktir.

Eylemsizliğin acısı

Temmuz 2021'di. Uzun süre boyunca kullandığım [email protected] ile başladım. Yarn hızlıydı, ama çok geçmeden Yarn Workspace'lerle ilgili birkaç sorunla karşılaştım. Örneğin, bağımlılıkları doğru bir şekilde içerememe ve tonlarca sorun “modern'de düzeltilmiştir ” etiketiyle etiketlendi, bu beni v2'ye (berry) yönlendirdi.

“Tamam, şimdi yükseltiyorum.” V1 ile mücadeleyi bıraktım ve taşınmaya başladım. Ancak berry'nin uzun geçiş rehberi beni korkuttu ve birkaç başarısız denemeden sonra pes ettim.

Sadece işliyor

Dolayısıyla paket yöneticileri hakkında araştırmalar başladı. Pnpmyi denedikten sonra büyülendim: Yarn kadar hızlı, yerel monorepo desteği, npmye benzer komutlar, sert linkler vb. En önemlisi, sadece çalışıyor. Bir ürünle başlamak isteyen ancak bir paket yöneticisi geliştirmek istemeyen bir geliştirici olarak, birkaç bağımlılığı eklemek ve bir paket yöneticisinin nasıl çalıştığını veya başka herhangi bir havalı kavramı bilmeden projeye başlamak istedim.

Aynı fikir üzerine, paketler arasında komutları çalıştırmak ve iş alanı paketlerini yayınlamak için eski bir arkadaş lernayi seçtik.

Paket kapsamlarını tanımlama

Her paketin nihai kapsamını baştan belirlemek zordur. Durumun gerektirdiği şekilde en iyi denemenizle başlayın ve geliştirme sırasında her zaman yeniden yapılandırma yapabileceğinizi unutmayın.

Başlangıçtaki yapımız dört paket içeriyor:

  • core: backend monolit hizmet.
  • phrases: i18n anahtar → cümle kaynakları.
  • schemas: Databıase ve paylaşılan TypeScript şemaları.
  • ui: core ile etkileşimde bulunan bir web SPA.

Fullstack için teknoloji yığını

JavaScript ekosistemini benimsediğimiz ve ana programlama dilimiz olarak TypeScript'i kullandığımız için, birçok seçim açıkçı (benim tercihime dayanarak 😊):

  • Backend hizmeti (çekirdek) için koajs': expresste async / await` kullanırken zor bir deneyimim oldu, bu yüzden yerel desteğe sahip bir şey kullanmaya karar verdim.
  • i18n (phrases/ui) için i18next/react-i18next: API'lerin basitliğini ve iyi TypeScript desteğini beğeniyorum.
  • SPA (ui) için react: Sadece kişisel tercih.

Şemalar hakkında ne dersiniz?

Burada hala bir şey eksik: veritabanı sistemi ve şema <-> TypeScript tanımı eşleme.

Genel v.s. tercihli

Bu noktada, iki popüler yaklaşımı denedim:

  • Çok fazla dekoratörle birlikte ORM kullanın.
  • Knex.js gibi bir sorgu oluşturucu kullanın.

Ancak ikisi de önceki geliştirmede garip bir duygu yarattı:

  • ORM için: Dekoratörlerin hayranı değilim ve veritabanının başka bir soyut katmanı ekibin daha fazla öğrenme çabasına ve belirsizliğine neden olur.
  • Sorgu oluşturucu için: İyi bir şekilde bazı kısıtlamalarla SQL yazmanız gibidir, ancak gerçek SQL değildir. Bu yüzden birçok durumda ham sorgular için .raw() kullanmamız gerekiyor.

Daha sonra bu makaleyi gördüm: “Knex.js kullanmayı bırakın: SQL sorgu oluşturucu kullanmak bir anti-kalıptır”. Başlık agresif görünüyor, ama içerik harika. Bana güçlü bir şekilde “SQL bir programlama dilidir” hatırlattı, ve SQL'i doğrudan yazabileceğimi (CSS gibi, bunu nasıl kaçırdığım!) Anladım, yerel dili ve veritabanı özelliklerini kullanmak yerine başka bir katman eklemek ve gücü azaltmak yerine.

Sonuç olarak, aynı makalenin belirttiği gibi, Postgres ve Slonik (açık kaynaklı bir Postgres istemcisi) ile tutunmayı karar verdim:

… farklı veritabanı lehçeleri arasında seçim yapma yararı marjinaldir ve birden çok veritabanı için aynı anda geliştirme yapmanın maliyeti önemlidir.

SQL <-> TypeScript

SQL'i yazmanın bir başka avantajı, onu TypeScript tanımlarının tek gerçeği olarak kolayca kullanabilmemizdir. SQL şemalarını backend'de kullanacağımız TypeScript koduna dönüştüren bir kod üreteci yazdım ve sonuç fena görünmüyor:

Gerekirse `jsonb'yi bir TypeScript tipine bağlayabilir ve backend servisinde tip doğrulamayı gerçekleştirebiliriz.

Sonuç

Final bağımlılık yapısı şöyle görünüyor:

Bir yönlü bir diyagram olduğunu fark edersiniz, bu da bize açık bir mimariyi koruma ve projenin büyüdükçe genişleme yeteneğini kazanma konusunda büyük yardımcı oldu. Ayrıca, kod (temelde) tümüyle TypeScript'te.

Geliştirme deneyimi

Paket ve yapılandırma paylaşımı

Dahili bağımlılıklar

Pnpm ve lerna, iç iş alanı bağımlılıklarında muhteşem bir iş çıkarıyor. Proje kökünde kardeş paketleri eklemek için aşağıdaki komutu kullanırız:

Bu, @logto/scheması @logto/coreın bir bağımlılığı olarak ekler. Dahili bağımlılıklarınızın semantik sürümünü package.jsonda tutarken, pnpm onları pnpm-lock.yamlda doğru bir şekilde bağlayabilir. Sonuç şöyle görünür:

Yapılandırma paylaşımı

Monorepo’daki her paketi “bağımsız” olarak görürüz. Bu nedenle, tsconfig, eslintConfig, prettier, stlyelint ve jest-configi kapsayan standart yapılandırma paylaşımı yöntemini kullanabiliriz. Örnek için bu projeye bakın.

Kod, lint ve commit

Günlük geliştirme için VSCode kullanıyorum ve kısa bir şekilde, proje düzgün bir şekilde yapılandırılmış olduğunda bir şey farklı değil:

  • ESLint ve Stylelint normal çalışır.
    • VSCode ESLint eklentisini kullanıyorsanız, kişi başına ESLint yapılandırmasını onurlandırması için aşağıdaki VSCode ayarlarını ekleyin (pattern değerini kendi değerinizle değiştirin):
  • husky, commitlint ve lint-staged beklenildiği gibi çalışır.

Derleyici ve proxy

Frontend ve backend için farklı derleyiciler kullanıyoruz: UI (React) için parceljs ve tüm diğer saf TypeScript paketleri için tsc. Denediyseniz parceljsyi şiddetle tavsiye ederim. Bu, farklı dosya türlerini zarif bir şekilde ele alan gerçek bir “sıfır yapılandırma” derleyicidir.

Parcel kendi frontend geliştirme sunucusuna sahip ve üretim çıktısı yalnızca statik dosyalardır. CORS sorunlarını önlemek için API'ları ve SPA'yı aynı kökene bağlamak istediğimizden, aşağıdaki strateji işe yarar:

  • Geliştirme ortamında, trafiği Parcel geliştirme sunucusuna yönlendirmek için basit bir HTTP proxy kullanın.
  • Üretimde, statik dosyaları doğrudan sunun.

Frontend middleware işlev uygulamasını burada bulabilirsiniz.

İzleme modu

Her paket için package.json'da gerektiğinde yeniden derleyen dosya değişikliklerini izleyen bir devscriptimiz var.Lernaya teşekkürler, lerna exec` kullanarak paket scriptlerini paralel olarak çalıştırma işleri kolaylaştırıyor. Kök script şöyle görünür:

Özet

İdeal olarak, yeni bir mühendis/katkıda bulunan kişi başlamak için yalnızca iki adım gerektirir:

  1. Repo'yu klonlayın
  2. pnpm i && pnpm dev

Son notlar

Takımımız bu yaklaşım altında bir yıl boyunca geliştirdi ve bundan oldukça memnunuz. Projemizin son halini görmek için GitHub repo'muzu ziyaret edin. Özetlemek gerekirse:

Acılar

  • JS/TS ekosistemine aşina olmanız gerekiyor
  • Doğru paket yöneticisini seçmeniz gerekiyor
  • Ek bazı bir seferlik kurulumlar gerektirir

Kazanımlar

  • Tüm projeyi bir repo'da geliştirme ve bakım yapma
  • Basitleştirilmiş kodlama beceri gereksinimleri
  • Paylaşılan kod stilleri, şemalar, ifadeler ve yardımcı programlar
  • İletişim verimliliğinin artması
    • API tanımı nedir gibi sorular artık yok
    • Tüm mühendisler aynı programlama dilinde konuşuyor
  • Kolay CI/CD
    • Derleme, test etme ve yayınlamak için aynı araç zincirini kullanma

Bu makale, repo'yu sıfırdan kurmak, yeni bir paket eklemek, GitHub Actions'ı CI/CD için kullanma vb. birçok konuyu ele almıyor. Her birini genişletirsem bu makale için çok uzun olurdu. Yorum yapmaktan çekinmeyin ve gelecekte hangi konuyu görmek istediğinizi bana bildirin.