TypeScript الكل في واحد: Monorepo مع متاعبه وفوائده
في هذه المقالة، لن أقارن بين monorepo وpolyrepo لأن الأمر يتعلق بالفلسفة. بدلاً من ذلك، سأركز على تجربة البناء والتطور وأفترض أنك على دراية بنظام JS/TS الإيكولوجي.
في هذه المقالة، لن أقارن بين monorepo وpolyrepo لأن الأمر يتعلق بالفلسفة. بدلاً من ذلك، سأركز على تجربة البناء والتطور وأفترض أنك على دراية بنظام JS/TS الإيكولوجي.
لطالما كان لدي حلم بإنشاء monorepo.
رأيت نهج monorepo أثناء عملي في Airbnb، لكنه كان للواجهات الأمامية فقط. بحب عميق لنظام JavaScript الإيكولوجي وتجربة تطوير TypeScript "السعيدة"، بدأت في توحيد كود الواجهة الأمامية والخلفية في نفس اللغة منذ حوالي ثلاث سنوات. لقد كان أمراً رائعاً (للتوظيف) ولكنه لم يكن رائعاً للتطوير حيث كانت مشاريعنا لا تزال موزعة عبر مستودعات متعددة.
كما يُقال، "أفضل طريقة لإعادة هيكلة مشروع هي بدء مشروع جديد". لذلك عندما بدأت شركتي الناشئة منذ حوالي عام، قررت استخدام استراتيجية monorepo كاملة: وضع مشاريع الواجهة الأمامية والخلفية، حتى مخططات قاعدة البيانات، في مستودع واحد.
في هذه المقالة، لن أقارن بين monorepo وpolyrepo لأن الأمر يتعلق بالفلسفة. بدلاً من ذلك، سأركز على تجربة البناء والتطور وأفترض أنك على دراية بنظام JS/TS الإيكولوجي.
النتيجة النهائية متوفرة على GitHub.
بكل صراحة، أنا من محبي 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.
في ذلك الوقت، جربت نهجين شائعين:
لكن كلاهما يخلق شعورًا غريبًا خلال التطوير السابق:
.raw() للاستعلامات الخام في العديد من السيناريوهات.ثم رأيت هذه المقالة: "كف عن استخدام Knex.js: استخدام منشئ استعلام SQL هو نمط مضاد". العنوان يبدو عدوانياً، لكن المحتوى رائع. يذكرني بقوة أن "SQL هي لغة برمجة"، وأدركت أنني أستطيع كتابة SQL مباشرةً (مثل CSS، كيف يمكنني أن أفتقد هذا!) لاستغلال اللغة الأصلية وميزات قاعدة البيانات بدلاً من إضافة طبقة أخرى والحد من القوة.
في الختام، قررت التمسك بـ Postgres و Slonik (عميل Postgres مفتوح المصدر)، كما تنص المقالة:
... الفائدة من السماح للمستخدم بالاختيار بين اللهجات المختلفة لقاعدة البيانات ضئيلة والتكلفة الزائدة للتطور لدعم قواعد بيانات متعددة في نفس الوقت كبيرة.
ميزة أخرى لكتابة 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 للتطوير اليومي، وباختصار، لا شيء يختلف عندما يتم تكوين المشروع بشكل صحيح:
pattern بالقيمة الخاصة بك):نستخدم مترجمات مختلفة للواجهة الأمامية والخلفية: parceljs لـ UI (React) و tsc لجميع حزم TypeScript البحتة الأخرى. أوصي بشدة بتجربة parceljs إذا لم تفعل. إنها فعلاً مُترجم "صفر تكوين" يتعامل بمرونة مع أنواع الملفات المختلفة.
يستضيف Parcel خادم التطوير الأمامي الخاص به، ومخرجات الإنتاج هي فقط ملفات ثابتة. نظرًا لأننا نود ربط APIs و SPA تحت نفس الأصل لتجنب مشاكل CORS، فإن الاستراتيجية أدناه تعمل:
يمكنك العثور على تنفيذ وظيفة واجهة التطوير الأمامية هنا.
لدينا نص dev في package.json لكل حزمة يراقب التغييرات في الملفات ويعيد التحويل كما هو مطلوب. شكرًا لـ lerna، أصبحت الأشياء سهلة باستخدام lerna exec لتشغيل نصوص الحزم بالتوازي. سيكون النص الجذر كما يلي:
بشكل مثالي، خطوتان فقط لمهندس/مساهم جديد للبدء:
pnpm i && pnpm devلقد كان فريقنا يطور تحت هذا النهج لمدة عام، ونحن سعداء به تمامًا. قم بزيارة مستودع GitHub الخاص بنا لرؤية شكل المشروع الحالي. لتلخيص:
المتاعب
المكاسب
تظل هذه المقالة تغفل عدة مواضيع: إعداد المستودع من الصفر، إضافة حزمة جديدة، الاستفادة من GitHub Actions لـ CI/CD، إلخ. سيكون طويل جدًا لهذه المقالة إذا قمت بتوسيع كل واحد منهم. لا تتردد في التعليق وإبلاغي بأي موضوع تود رؤيته في المستقبل.