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.
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.
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.
“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.
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.
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.
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.
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'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.
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.
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:
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.
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.
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.
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:
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.