TypeScript الكل في واحد: Monorepo مع متاعبه وفوائده
في هذه المقالة، لن أقارن بين monorepo وpolyrepo لأن الأمر يتعلق بالفلسفة. بدلاً من ذلك، سأركز على تجربة البناء والتطور وأفترض أنك على دراية بنظام JS/TS الإيكولوجي.
مقدمة
لطالما كان لدي حلم بإنشاء monorepo.
رأيت نهج monorepo أثناء عملي في Airbnb، لكنه كان للواجهات الأمامية فقط. بحب عميق لنظام JavaScript الإيكولوجي وتجربة تطوير TypeScript "السعيدة"، بدأت في توحيد كود الواجهة الأمامية والخلفية في نفس اللغة منذ حوالي ثلاث سنوات. لقد كان أمراً رائعاً (للتوظيف) ولكنه لم يكن رائعاً للتطوير حيث كانت مشاريعنا لا تزال موزعة عبر مستودعات متعددة.
كما يُقال، "أفضل طريقة لإعادة هيكلة مشروع هي بدء مشروع جديد". لذلك عندما بدأت شركتي الناشئة منذ حوالي عام، قررت استخدام استراتيجية monorepo كاملة: وضع مشاريع الواجهة الأمامية والخلفية، حتى مخططات قاعدة البيانات، في مستودع واحد.
في هذه المقالة، لن أقارن بين monorepo وpolyrepo لأن الأمر يتعلق بالفلسفة. بدلاً من ذلك، سأركز على تجربة البناء والتطور وأفترض أنك على دراية بنظام JS/TS الإيكولوجي.
النتيجة النهائية متوفرة على GitHub.
لماذا TypeScript؟
بكل صراحة، أنا من محبي JavaScript وTypeScript. أحب توافقه بين المرونة والدقة: يمكنك الرجوع إلى unknown
أو any
(على الرغم من أننا منعنا أي شكل من أشكال any
في قاعدة الكود لدينا)، أو استخدام مجموعة قواعد صارمة بشكل كبير لتوحيد نمط الكود عبر الفريق.
عندما كنا نتحدث عن مفهوم "البرمجة الكاملة" سابقًا، كنا عادةً ما نتخيل وجود نظامين إيكولوجيين على الأقل ولغتين برمجيتين: واحدة للواجهة الأمامية وواحدة للواجهة الخلفية.
في يوم من الأيام، أدركت أنه يمكن أن يكون الأمر أبسط: Node.js سريع بما يكفي (صدقوني، في معظم الحالات، جودة الكود أكثر أهمية من سرعة التشغيل)، وTypeScript ناضج بما فيه الكفاية (يعمل بشكل جيد في المشاريع الكبيرة للواجهة الأمامية)، ومفهوم monorepo قد تم تطبيقه من قبل فرق مشهورة (React, Babel, etc.) - فلماذا لا نجمع كل الكود معًا، من الواجهة الأمامية إلى الخلفية؟ هذا يمكن أن يجعل المهندسين يقومون بالمهام دون تبديل السياق في مستودع واحد وتنفيذ ميزة كاملة (تقريبًا) في لغة واحدة.
اختيار مدير الحزم
بصفتي مطورًا، وكالعادة، لم أستطع الانتظار للبدء في الترميز. ولكن هذه المرة، كانت الأمور مختلفة.
اختيار مدير الحزم أمر حاسم لتجربة التطوير في monorepo.
ألم الجمود
كان ذلك في يوليو 2021. بدأت بـ [email protected]
لأنني كنت أستخدمه لفترة طويلة. كان Yarn سريعًا، لكن سرعان ما واجهت العديد من المشاكل مع Yarn Workspaces. على سبيل المثال، عدم تجميع التبعيات بشكل صحيح، والكثير من المشاكل تم وضع علامة عليها بـ "fixed in modern", والذي يوجهني إلى الإصدار الثاني (berry).
"حسنًا، سأقوم بالترقية الآن". توقفت عن الكفاح مع v1 وبدأت في الانتقال. لكن دليل الانتقال الطويل لـ berry أخافني، وتوقفت بعد عدة تجارب فاشلة.
إنها تعمل فقط
لذلك بدأت البحث عن مديري الحزم. استحوذ علي pnpm
بعد تجربة: سريع مثل yarn، دعم monorepo مدمج، أوامر مشابهة لـ npm
، الروابط الصلبة، إلخ. والأهم من ذلك، أنه يعمل فقط. كمطور يرغب في البدء بمنتج وليس تطوير مدير حزم، كنت أريد فقط إضافة بعض التبعيات والبدء في المشروع دون معرفة كيفية عمل مدير الحزم أو أي مفاهيم فاخرة أخرى.
بناءً على نفس الفكرة، اخترنا صديقنا القديم lerna
لتنفيذ الأوامر عبر الحزم ونشر حزم workspace.
تحديد نطاقات الحزم
من الصعب تحديد النطاق النهائي لكل حزمة بوضوح في البداية. فقط ابدأ بأفضل محاولة لك وفقًا للوضع الحالي، وتذكر أنك دائمًا يمكن إعادة هيكلة أثناء التطوير.
البنية الأولية تحتوي على أربع حزم:
core
: خدمة الشاملة للواجهة الخلفية.phrases
: الموارد الرئاسية لـ i18n المفتاحية.schemas
: مخططات قاعدة البيانات وTypeScript المشتركة.ui
: تطبيق ويب SPA يتفاعل معcore
.
التقنية لمكدس البرمجة الكاملة
نظرًا لأننا نتبنى نظام JavaScript الإيكولوجي ونستخدم TypeScript كلغتنا البرمجية الرئيسية، فإن الكثير من الخيارات تكون مباشرة (بناءً على تفضيلي 😊):
koajs
للخدمة الخلفية (core): واجهت صعوبة في استخدامasync/await
فيexpress
، لذلك قررت استخدام شيء مع دعم مدمج.i18next/react-i18next
لـ i18n (phrases/ui): أحب بساطة واجهات الـ APIs ودعم TypeScript الجيد.react
لـ SPA (ui): مجرد تفضيل شخصي.
ماذا عن المخططات؟
ما زال هناك شيء مفقود هنا: نظام قاعدة البيانات وتعريف المخطط <-> TypeScript.
عامة مقابل محددة الرأي الشخصي
في ذلك الوقت، جربت نهجين شائعين:
- استخدام ORM مع الكثير من المزخرفات.
- استخدام منشئ استعلام مثل Knex.js.
لكن كلاهما يخلق شعورًا غريبًا خلال التطوير السابق:
- بالنسبة لـ ORM: لست من محبي المزخرفات، وطبقة مجردة أخرى تتسبب في مزيد من الجهد التعليمي وعدم اليقين للفريق.
- بالنسبة لمنشئ الاستعلام: إنه يشبه كتابة SQL مع بعض القيود (بطريقة جيدة)، لكنه ليس SQL فعلي. لذلك نحتاج إلى استخدام
.raw()
للاستعلامات الخام في العديد من السيناريوهات.
ثم رأيت هذه المقالة: "كف عن استخدام Knex.js: استخدام منشئ استعلام SQL هو نمط مضاد". العنوان يبدو عدوانياً، لكن المحتوى رائع. يذكرني بقوة أن "SQL هي لغة برمجة"، وأدركت أنني أستطيع كتابة SQL مباشرةً (مثل CSS، كيف يمكنني أن أفتقد هذا!) لاستغلال اللغة الأصلية وميزات قاعدة البيانات بدلاً من إضافة طبقة أخرى والحد من القوة.
في الختام، قررت التمسك بـ Postgres و Slonik (عميل Postgres مفتوح المصدر)، كما تنص المقالة:
... الفائدة من السماح للمستخدم بالاختيار بين اللهجات المختلفة لقاعدة البيانات ضئيلة والتكلفة الزائدة للتطور لدعم قواعد بيانات متعددة في نفس الوقت كبيرة.
SQL <-> TypeScript
ميزة أخرى لكتابة SQL هي أننا يمكننا استخدامها بسهولة كمصدر واحد للحقيقة لتعريفات TypeScript. كتبت مولد كود لتحويل مخططات SQL إلى كود TypeScript الذي سنستخدمه في خلفيتنا، والنتيجة تبدو جيدة:
يمكننا حتى ربط jsonb
بنوع TypeScript ومعالجة التحقق من النوع في الخدمة الخلفية إذا لزم الأمر.
النتيجة
الهيكل النهائي للتبعيات يبدو كالتالي:
قد تلاحظ أنها مخطط باتجاه واحد، مما ساعدنا بشكل كبير في الحفاظ على هيكل واضح والقدرة على التوسع بينما ينمو المشروع. بالإضافة إلى ذلك، الكود هو (بشكل أساسي) كله في TypeScript.
تجربة التطوير
مشاركة الحزمة والتكوين
التبعيات الداخلية
pnpm
و lerna
يقومان بعمل رائع في تبعيات workspace الداخلية. نستخدم الأمر أدناه في جذر المشروع لإضافة حزم الزملاء:
سيضيف @logto/schemas
كحال استخدام لـ @logto/core
. بينما نحافظ على الإصدار الدلالي في package.json
لتبعياتك الداخلية، يمكن لـ pnpm
أيضًا ربطها بشكل صحيح في pnpm-lock.yaml
. ستبدو النتيجة كما يلي:
مشاركة التكوين
نعامل كل حزمة في monorepo كـ "مستقلة". لذلك يمكننا استخدام النهج القياسي لمشاركة التكوين، والذي يغطي tsconfig
، eslintConfig
، prettier
، stylelint
، وjest-config
. انظر هذا المشروع كمثال.
الترميز، اللنت، والالتزام
أستخدم VSCode للتطوير اليومي، وباختصار، لا شيء يختلف عندما يتم تكوين المشروع بشكل صحيح:
- تعمل ESLint و Stylelint بشكل طبيعي.
- إذا كنت تستخدم إضافة ESLint في VSCode، أضف إعدادات VSCode أدناه لجعلها تحترم تكوين ESLint لكل حزمة (استبدل قيمة
pattern
بالقيمة الخاصة بك):
- إذا كنت تستخدم إضافة ESLint في VSCode، أضف إعدادات VSCode أدناه لجعلها تحترم تكوين ESLint لكل حزمة (استبدل قيمة
- husky, commitlint, و lint-staged يعملون كما هو متوقع.
المترجم والوكيل
نستخدم مترجمات مختلفة للواجهة الأمامية والخلفية: parceljs
لـ UI (React) و tsc
لجميع حزم TypeScript البحتة الأخرى. أوصي بشدة بتجربة parceljs
إذا لم تفعل. إنها فعلاً مُترجم "صفر تكوين" يتعامل بمرونة مع أنواع الملفات المختلفة.
يستضيف Parcel خادم التطوير الأمامي الخاص به، ومخرجات الإنتاج هي فقط ملفات ثابتة. نظرًا لأننا نود ربط APIs و SPA تحت نفس الأصل لتجنب مشاكل CORS، فإن الاستراتيجية أدناه تعمل:
- في بيئة التطوير، استخدم وكيل HTTP بسيط لإعادة توجيه حركة المرور إلى خادم تطوير Parcel.
- في الإنتاج، قدم الملفات الثابتة مباشرةً.
يمكنك العثور على تنفيذ وظيفة واجهة التطوير الأمامية هنا.
وضع المراقبة
لدينا نص dev
في package.json
لكل حزمة يراقب التغييرات في الملفات ويعيد التحويل كما هو مطلوب. شكرًا لـ lerna
، أصبحت الأشياء سهلة باستخدام lerna exec
لتشغيل نصوص الحزم بالتوازي. سيكون النص الجذر كما يلي:
ملخص
بشكل مثالي، خطوتان فقط لمهندس/مساهم جديد للبدء:
- استنساخ المستودع
pnpm i && pnpm dev
ملاحظات ختامية
لقد كان فريقنا يطور تحت هذا النهج لمدة عام، ونحن سعداء به تمامًا. قم بزيارة مستودع GitHub الخاص بنا لرؤية شكل المشروع الحالي. لتلخيص:
المتاعب
- الحاجة إلى معرفة نظام JS/TS الإيكولوجي
- اختيار مدير الحزم الصحيح
- يتطلب بعض الإعدادات الإضافية لمرة واحدة
المكاسب
- تطوير وصيانة المشروع بأكمله في مستودع واحد
- تبسيط متطلبات المهارات البرمجية
- أساليب الكود المشتركة، المخططات، العبارات، والأدوات المساعدة
- تحسين كفاءة الاتصال
- لا مزيد من الأسئلة مثل: ما هو تعريف API؟
- جميع المهندسين يتحدثون نفس اللغة البرمجية
- سهولة CI/CD
- استخدم نفس مجموعة الأدوات للبناء، الاختبار، والنشر
تظل هذه المقالة تغفل عدة مواضيع: إعداد المستودع من الصفر، إضافة حزمة جديدة، الاستفادة من GitHub Actions لـ CI/CD، إلخ. سيكون طويل جدًا لهذه المقالة إذا قمت بتوسيع كل واحد منهم. لا تتردد في التعليق وإبلاغي بأي موضوع تود رؤيته في المستقبل.