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.
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ı. Pnpm
yi denedikten sonra büyülendim: Yarn kadar hızlı, yerel monorepo desteği, npm
ye 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ş lerna
yi 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.json
da tutarken, pnpm
onları pnpm-lock.yaml
da 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-config
i 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):
- 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 (
- 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 parceljs
yi ş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:
- Repo'yu klonlayın
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.