معمارية الواجهات الأمامية المصغّرة: توسيع تطوير الواجهة

تنقل الواجهات الأمامية المصغّرة فكرة الخدمات المصغّرة إلى طبقة الواجهة. فبدل تطبيق واحد متراص، تُقسَّم الواجهة إلى تطبيقات أصغر مستقلة تُطوَّر وتُختبر وتُنشر على حدة. تملك كل واجهة مصغّرة مجالاً تجارياً متميزاً، ويمكن بناؤها بتقنية أخرى، وبفريق آخر، وبإيقاع إصدار مختلف.

نشأت هذه المعمارية من الضغوط نفسها التي أدّت إلى الخدمات المصغّرة على الخادم. فمع ازدياد تعقيد تطبيقات الوِب، صارت الواجهات المتراصة ثقيلة الصيانة، بطيئة البناء، محفوفة بالمخاطر عند النشر. كان أي تغيير في أي موضع من الشفرة يستلزم إعادة بناء التطبيق كله وإعادة نشره، ما يبطئ الفرق ويولّد احتكاكاً بين مجموعات تريد السير بسرعات مختلفة.

ما الواجهات الأمامية المصغّرة؟

تقسّم هذه المعمارية تطبيق الوِب إلى شرائح وظيفية، يملك كلاً منها فريق مستقل. ويتحمّل الفريق مسؤولية كل طبقات شريحته: مكوّنات الواجهة، ومنطق العمل، وجلب البيانات، والتكامل مع الخادم. يرى المستخدم تطبيقاً واحداً متماسكاً، لكنه خلف الكواليس مؤلَّف من عدة تطبيقات أصغر تعمل في نافذة المتصفح نفسها.

يتبع هذا التقسيم مبادئ التصميم الموجَّه بالمجال. فكل واجهة مصغّرة تقابل سياقاً محدوداً: حدّاً منطقياً حول قدرة عمل بعينها. وقد يكون إتمام الشراء، وفهرس المنتجات، والملف الشخصي، والبحث واجهات مصغّرة منفصلة تملكها فرق منفصلة.

الفارق الجوهري بين الواجهات المصغّرة ومجرد تقسيم الشفرة إلى وحدات هو أن الواجهات المصغّرة مستقلة في البناء والنشر. فلكل واحدة خطُّ إنتاجها ومستودعها واختباراتها وجدول إصدارها. وهذا الاستقلال هو ما يمنح المعمارية مزاياها، وهو نفسه ما يخلق صعوباتها.

الواجهة المصغّرة ليست مكتبة مكوّنات ولا مجموعة أدوات مشتركة. إنها تطبيق قائم بذاته يُركَّب في زمن التشغيل داخل تطبيق أكبر. وهذا التمييز جوهري لفهم قوة المعمارية وكلفتها معاً.

Module Federation مع Webpack 5

يُعدّ Module Federation النهج الأوسع انتشاراً في منظومتَي React وAngular. وقد ظهر في Webpack 5، ويتيح لتطبيق JavaScript أن يحمّل في زمن التشغيل شفرةً من تطبيق آخر. وتعمل الشفرة المحمَّلة في سياق التطبيق المضيف وتتشارك معه الاعتماديات، كي لا تتكرر مكتبات مثل React أو Vue.

تقوم الآلية على أن يكشف تطبيق بعيد وحدات بعينها ويستوردها المضيف. فالبعيد يعلن ما يتوافر من وحدات، والمضيف يشير إليها كأنها استيرادات محلية. ويتولّى Webpack التحميل غير المتزامن، وإزالة الاعتماديات المكررة، وحسم الإصدارات في وقت البناء.

تشارك الاعتماديات هو أهم الخصائص. فحين يستعمل المضيف والبعيد المكتبة نفسها، يضمن Webpack ألّا تُحمَّل في المتصفح إلا نسخة واحدة. وبذلك يُتفادى التدهور الناتج عن عدة نسخ من React أو Lodash أو غيرهما من المكتبات الكبيرة. كما يضمن نمط النسخة الواحدة أن تكون الحالة المشتركة — مخزن Redux أو سياق React — مشتركة فعلاً بين الواجهات المصغّرة.

// webpack.config.js - Remote application exposing a module
const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "checkout",
      filename: "remoteEntry.js",
      exposes: {
        "./CheckoutApp": "./src/bootstrap",
      },
      shared: {
        react: { singleton: true, requiredVersion: "^18.0.0" },
        "react-dom": { singleton: true, requiredVersion: "^18.0.0" },
      },
    }),
  ],
};

// webpack.config.js - Host application consuming the remote
const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "shell",
      remotes: {
        checkout: "checkout@http://localhost:3001/remoteEntry.js",
      },
      shared: {
        react: { singleton: true, requiredVersion: "^18.0.0" },
        "react-dom": { singleton: true, requiredVersion: "^18.0.0" },
      },
    }),
  ],
};

// Lazy loading the remote module in the host
const CheckoutApp = React.lazy(() => import("checkout/CheckoutApp"));

function Shell() {
  return (
    <Suspense fallback={<Spinner />}>
      <CheckoutApp />
    </Suspense>
  );
}

يحمّل المضيف ملف المدخل البعيد ما إن يصل المستخدم إلى مسار يحتاج واجهة إتمام الشراء. وهذا الملف شفرة JavaScript صغيرة يولّدها Webpack تلقائياً: تحمل فهرساً بكل الوحدات المكشوفة وأماكن العثور عليها. وحين يطلب المضيف وحدة بعينها، ينزّل Webpack الجزء الموافق وينفّذه في سياق الاعتماديات المشتركة.

من المهم أن يتفق المضيف والبعيد على إصدارات الاعتماديات المشتركة. فإن طلب البعيد React 18.2 ولم يكن لدى المضيف سوى React 18.0، أخفق نمط النسخة الواحدة ما لم تكن الإصدارات متوافقة. ويقبل الحقل requiredVersion نطاقات الإصدار الدلالي، ما يمنح فسحة ويحمي في الوقت نفسه من الكسر.

التكامل عبر الإطارات المضمّنة

قبل Module Federation والأساليب الحديثة، كان الإطار المضمَّن هو النهج الأصلي. فهو يضمّن في صفحة مستنداً كاملاً مستقلاً تماماً. ولهذا المستند سياق JavaScript خاص به، ونطاق CSS خاص به، وشجرة DOM خاصة به. وهذه العزلة هي قوة الأسلوب وضعفه في آن.

يقدّم الإطار المضمَّن أقوى ضمانات العزل بين كل الأساليب. فلا خطر لتضارب CSS، لأن لكلٍّ مستنده. ولا تصادم في JavaScript، لأن لكلٍّ نطاقه العام. ولا تستطيع واجهة مصغّرة أن تغيّر خطأً حالة أخرى، لأن شجرتَي DOM منفصلتان تماماً. وتسريب ذاكرة في إحداهما لا يُسقط الأخرى.

لكن ثمن هذه الضمانات باهظ. فالإطارات المضمّنة ثقيلة. يحمّل كلٌّ منها مستند HTML بأكمله بكل ما فيه من موارد CSS وJavaScript. ويعامل المتصفح كل واحد بوصفه سياق تصفّح منفصلاً، أي مزيداً من الذاكرة، ومزيداً من طلبات الشبكة، ومزيداً من عمل الرسم. فإن ضمّنت عشر واجهات مصغّرة في إطارات، حمّل المتصفح إحدى عشرة صفحة فعلياً.

ويستلزم التواصل بين الإطار والصفحة الحاوية له استعمال postMessage، وهو غير متزامن ومقصور على البيانات القابلة للتسلسل. فلا يمكن تمرير دوال أو نسخ أصناف أو إشارات إلى DOM عبر الحدّ. وهذا يجعل الإطارات غير صالحة لواجهات مصغّرة تحتاج تكاملاً وثيقاً — كنموذج دفع يحتاج حالة سلة الصفحة المضيفة مثلاً.

ويعاني الوصول كذلك. فقارئات الشاشة وسائر التقنيات المساعدة تجد عادةً صعوبة مع سياقات التصفّح المتداخلة. والتنقّل بلوحة المفاتيح عبر حدود الإطارات غير متسق. والبحث داخل الصفحة لا يعبر هذه الحدود، ويعامل سجلّ التصفّح كل انتقال داخلي بوصفه جلسة على حدة.

تصلح الإطارات المضمّنة في المقام الأول لتضمين محتوى طرف ثالث، حيث تتقدّم العزلة ويقلّ التكامل. وهي خيار رديء لتركيب تجربة متماسكة تحتاج فيها الواجهات المصغّرة إلى تشارك الحالة وتنسيق التنقّل وتكوين سطح بصري موحّد.

single-spa وسائر أُطر التنسيق

‏single-spa إطار عمل بلغة JavaScript ينسّق عدة واجهات مصغّرة في صفحة واحدة. يدير دورة حياة كل واحدة: يركّبها حين يصل المستخدم إلى مسار يحتاجها، ويفكّها حين يبتعد، ويبقي غير النشطة في الذاكرة ليعيد تركيبها بسرعة. وهو مستقل عن الأُطر ويدعم React وAngular وVue وSvelte وJavaScript الصرف.

تسجّل كل واجهة مصغّرة نفسها لدى single-spa بوصفها تطبيقاً له دوال دورة حياة: bootstrap وmount وunmount، واختيارياً update. ويتحكّم جذر single-spa بالتوجيه ويقرّر، عبر أنماط العناوين، أي التطبيقات نشط. وحين يطابق العنوان دالة نشاط تطبيق مسجَّل، يركّبه single-spa ويفكّ ما لم يعد نشطاً.

يحلّ single-spa واحدة من أشوك المسائل: تعايش الأُطر. فحين تُبنى واجهتان مصغّرتان بإصدارين مختلفين من الإطار نفسه، أو بإطارين مختلفين كلياً، يضمن single-spa تعايشهما في الصفحة نفسها بلا تضارب. ويبلغ ذلك بالتدخّل في مفاصل دورة الحياة وعزل الحالة العامة لكل تطبيق.

ومن النُّهج الأخرى Piral بنموذج امتداداته الذي تُحمَّل فيه الواجهات المصغّرة وحداتٍ من خدمة توزيع، وOpen Components الموجَّه إلى التركيب على الخادم. وثمة أيضاً درب مكوّنات الوِب، حيث تُغلَّف كل واجهة مصغّرة في عنصر مخصّص وتُسجَّل في واجهة المتصفح المقابلة. وتمنح مكوّنات الوِب تحديداً أصيلاً لنطاق HTML وCSS وJavaScript، وتتركّب تصريحياً في قوالب HTML.

يعتمد الاختيار على مجموعة تقنياتك، وبنية فرقك، ومتطلبات الأداء. فـ Module Federation خير خيار لمن هم أصلاً على Webpack. وsingle-spa معقول في المعماريات المختلطة ذات الأُطر المتباينة. ومكوّنات الوِب تناسب المؤسسات التي تريد فرض حدود مستقلة عن الأُطر. والإطارات المضمّنة لا تلائم إلا تضمين محتوى طرف ثالث.

مكتبات المكوّنات ونظم التصميم المشتركة

من الهواجس المتكررة فقدان التماسك البصري. فإن بنى كل فريق واجهته بمعزل عن غيره، صادف المستخدم أزراراً وخطوطاً وتخطيطات مختلفة وهو يتنقّل في التطبيق. والجواب مكتبة مكوّنات مشتركة أو نظام تصميم تستهلكه كل الواجهات المصغّرة.

تضمّ المكتبة المشتركة عادةً مكوّنات عرض: أزراراً وحقول إدخال ونوافذ حوارية وعناصر تنقّل. وهي بصرية محضة ولا تحمل منطق عمل. تقبل خصائص لتنويعات الشكل والعناوين ومعالجات الأحداث، لكنها لا تجلب بيانات ولا تدير حالة ولا تنفّذ سلوكاً خاصاً بالمجال. وهذا الفصل يُبقي المكتبة مستقرة ويمنعها من أن تصير عنق زجاجة أمام سرعة الفرق.

ويحتاج ترقيم إصداراتها انضباطاً. فحين تُنشر حزمةَ npm، تثبّت كل واجهة مصغّرة إصداراً وتحدّث على إيقاعها. وهذا يمنح استقلالاً، لكنه قد يولّد انجراف إصدارات يظهر فيه المكوّن نفسه مختلفاً بين واجهة وأخرى. وتساعد مجموعة اختبارات انحدار بصري على المكتبة في التقاط هذه الفوارق قبل الإنتاج.

وثمة خيار آخر: تقديم نظام التصميم اعتمادية زمن تشغيل. تُنشر المكتبة تطبيقاً قائماً بذاته وتحمّلها كل واجهة مصغّرة عند الإقلاع. فيستعمل الجميع دوماً أحدث إصدار من كل مكوّن ويزول الانجراف. وفي المقابل يصير تحديث النظام مستلزماً نشراً، وأي كسر يصيب كل الواجهات المصغّرة دفعة واحدة.

رموز التصميم قاسماً مشتركاً أصغر

رموز التصميم — وهي قيم مسمّاة للألوان والتباعد والخطوط والظلال — بديل أخفّ من مكتبة كاملة. فتنفّذ كل واجهة مصغّرة مكوّناتها بنفسها لكنها تشير إلى المجموعة نفسها من الرموز. وبذلك تُكسب حرية التنفيذ مع الحفاظ على التماسك البصري. ويمكن توزيع الرموز خصائصَ CSS مخصّصة، سهلةَ التجاوز ولا تستلزم أداة بناء البتة.

التواصل بين الواجهات المصغّرة

تحتاج الواجهات المصغّرة إلى التخاطب. فواجهة إتمام الشراء تحتاج أن تعرف ما وضعه المستخدم في السلة من الفهرس. وواجهة الترويسة تحتاج تحديث الشارة حين تتلقّى واجهة الإشعارات إشعاراً جديداً. وواجهة الدخول تحتاج إخبار سائر الواجهات حين يدخل المستخدم أو يخرج.

أبسط الآليات ناقل أحداث مشترك. تنشر كل واجهة مصغّرة أحداثاً في قناة عامة وتشترك في أحداث غيرها. ويُنفَّذ الناقل عادةً باعثَ أحداث معلَّقاً بكائن window أو محقوناً من القشرة. وتحمل الأحداث نوعاً وحمولة، ويرشّح المشتركون بحسب النوع.

// Shared event bus - shell application
// Each micro frontend receives this bus as part of its initialization.

type BusEvent = {
  type: string;
  payload: unknown;
};

type Listener = (event: BusEvent) => void;

class EventBus {
  private listeners: Map<string, Listener[]> = new Map();

  on(type: string, listener: Listener): () => void {
    if (!this.listeners.has(type)) {
      this.listeners.set(type, []);
    }
    this.listeners.get(type)!.push(listener);
    return () => this.off(type, listener);
  }

  off(type: string, listener: Listener): void {
    const listeners = this.listeners.get(type);
    if (listeners) {
      this.listeners.set(
        type,
        listeners.filter((l) => l !== listener)
      );
    }
  }

  emit(type: string, payload: unknown): void {
    this.listeners.get(type)?.forEach((listener) => {
      listener({ type, payload });
    });
  }
}

// Usage in a micro frontend
export function init(bus: EventBus) {
  bus.on("item:added-to-cart", (event) => {
    updateCartBadge(event.payload.quantity);
  });

  bus.emit("user:logged-in", { userId: "abc-123", name: "Jane" });
}

وللحاجات الأعقد، يكون مخزن حالة مشترك أفضل من ناقل الأحداث عادةً. فمخزن Redux أو Zustand مستضاف في القشرة يمكن حقنه في كل واجهة مصغّرة. تقرأ كل واحدة منه الحالة العابرة للمجالات، وتكتب في مخزنها المحلي ما يخصّ مجالها وحده. فيبقى الاقتران رخواً ويبقى مع ذلك مصدر حقيقة واحد للبيانات المشتركة: المستخدم الحالي، ومحتوى السلة، والتنقّل النشط.

ويجب تصميم طبقة التواصل وتوثيقها صراحةً. على الفرق أن تتفق على أسماء الأحداث وصيغ الحمولة والحدّ بين الحالة المشتركة والحالة المحلية. فمن دون هذا الاتفاق تنشئ الواجهات المصغّرة اعتماديات ضمنية على حالة بعضها الداخلية، فينتج متراص موزَّع بكل تعقيد الواجهات المصغّرة ومن دون شيء من مزاياها.

استراتيجيات التوجيه

على التوجيه أن يجيب عن سؤالين: لأي واجهة مصغّرة يعود المسار الحالي؟ وكيف يجري التنقّل بينها؟ والجوابان يتوقفان على استعمالك موجِّهاً مركزياً في القشرة، أو نهجاً موزَّعاً، أو استراتيجية مختلطة.

في التوجيه المركزي يملك تطبيق القشرة موجِّه المستوى الأول. فهو يعرّف خريطة المسارات، ويحدّد أي واجهة مصغّرة تُركَّب لكل نمط عنوان، ويمرّر إليها الوسائط. ولكل واجهة مصغّرة موجِّهها الداخلي للتنقّل داخل مجالها. فالقشرة تتولّى الانتقالات بين المجالات، والواجهات المصغّرة تتولّى المسارات الداخلية.

وفي التوجيه الموزَّع تعود المسارات إلى الواجهات المصغّرة نفسها. تظلّ القشرة تدير المطابقة العامة بين العنوان والواجهة المصغّرة، لكن كلاً منها تدير وحدها مساراتها الفرعية وتنقّلها الداخلي. وتنسّق مكتبة تنقّل في القشرة تغيّرات السجلّ وتضمن أن يعكس شريط العنوان حالة التطبيق كله لا حالة الواجهة النشطة وحدها.

وفي التوجيه بالأحداث ينسّق الناقلُ التنقّلَ. فحين تحتاج واجهة مصغّرة الذهاب إلى مسار يملكه غيرها، تبعث حدث تنقّل. وتسمعه القشرة فتنفّذ التغيير، فيُركَّب المقصد. وبذلك تخرج مسائل التوجيه من الواجهات المصغّرة ويتركّز المنطق في القشرة.

  • التوجيه المركزي: القشرة تملك خريطة المسارات والواجهات المصغّرة تتولّى التنقّل الداخلي وحده. مثالي للفرق الصغيرة والحدود البسيطة.
  • التوجيه الموزَّع: كل واجهة مصغّرة تدير مساراتها الفرعية. مثالي للفرق الكبيرة ذات المجالات المفصولة جيداً.
  • التوجيه بالأحداث: ينسَّق التنقّل عبر ناقل مشترك. مثالي للمعماريات المختلطة ذات الأُطر المتباينة.
  • التوجيه الهجين: القشرة تتولّى مسارات المجالات من المستوى الأول والواجهات المصغّرة تتولّى المسارات الفرعية. مثالي لأكثر تطبيقات الإنتاج.

النشر وترقيم الإصدارات

استقلال النشر هو الدافع الأول إلى تبنّي الواجهات المصغّرة. فينبغي أن يقدر كل فريق على نشر واجهته من دون تنسيق مع غيره ومن دون إضرار باستقرار الكل. وبلوغ ذلك يستلزم تصميماً متأنياً لخط الإنتاج، ولخطة الاستضافة، ولنظام الإصدارات.

أبسط النماذج الاستضافة المنفصلة. تُنشر كل واجهة مصغّرة على عنوانها أو في مساحة تخزينها. وتحمّلها القشرة من هناك في زمن التشغيل. ويمنح هذا النموذج أقصى استقلال، لأن كل فريق يتحكّم ببنيته التحتية وخط إنتاجه واستراتيجية تراجعه. ولا تحتاج القشرة إلى إعادة نشر حين تُحدَّث واجهة مصغّرة، لأنها تحمّل أحدث إصدار ديناميكياً.

وتجلب الاستضافة المنفصلة صعوبة جديدة: تنسيق عمليات النشر حفاظاً على التوافق مع القائم. فإن كانت واجهة إتمام الشراء تنتظر صيغة خصائص معينة من القشرة ثم غيّرتها نشرة القشرة التالية، انكسرت واجهة الشراء حتى تُحدَّث هي أيضاً. والمخرج أن يُرقَّم عقد التكامل — وهو عادةً الخصائص التي تمرّرها القشرة — وأن يُشار إلى الكسر بترقيم الإصدار الدلالي.

ويصير النشر التدريجي والتجريب على شريحة صغيرة من المستخدمين أسهل هنا، لأن كل واجهة مصغّرة تُنشر بذاتها. فيمكن لفريق أن يطلق إصداراً جديداً لخمسة في المئة من المستخدمين، ويراقب معدلات الخطأ ومؤشرات الأداء، ثم يوسّع تدريجياً إن سار كل شيء على ما يرام. وإن ظهرت مشكلة تراجع عن واجهته وحدها من دون مساس بالبقية.

وتثبيت الإصدار من القشرة هو مخرج الطوارئ. فإن أدخل نشرٌ كسراً أفلت من الاختبارات، أمكن للقشرة تثبيت الإصدار السابق واستعادة الاستقرار ريثما يعالج الفريق الأمر. وتكون هذه الآلية عادةً ملف إعدادات أو متغيّر بيئة يربط كل واجهة مصغّرة بإصدار منشور سلفاً.

مستودع واحد أم مستودعات منفصلة

بنية المستودعات موضوع خلافي في أوساط الواجهات الأمامية. ولحجج المستودع الواحد وحجج المستودعات المنفصلة وجاهتها، والاختيار الصائب يتوقف على حجم الفرق ونضج المؤسسة وتفضيلات الأدوات.

يحفظ المستودع الواحد كل الواجهات المصغّرة في مكان واحد منظَّمة في مجلدات. ولكل منها ملف package.json الخاص بها، وإعداد بنائها، ودليلها. ويستعمل هذا النهج أدوات مثل Turborepo أو Nx أو Lerna أو مساحات عمل pnpm لإدارة الاعتماديات وتشغيل عمليات البناء وتنسيق المهام بين الواجهات المصغّرة.

  • الإعداد المشترك للأدوات بسيط: ESLint واحد، وTypeScript واحد، وPrettier واحد للجميع.
  • تصير عمليات إعادة الهيكلة العابرة أيسر، لأن شفرة الجميع في مكان واحد.
  • إدارة الاعتماديات مركزية، ما يقلّل خطر تباين الإصدارات.
  • تصير الإيداعات الذرّية عبر عدة واجهات مصغّرة ممكنة حين يمسّ تغيير عدة مجالات.

أما نهج المستودعات المتعددة فيضع كل واجهة مصغّرة في مستودعها، بخط إنتاجها وإعداداتها. وتحتفظ الفرق باستقلال تام في مجموعة التقنيات وطريقة الإصدار. فالفريق الذي يريد الانتقال من React إلى Preact يستطيع ذلك بلا تنسيق، ما دامت واجهته تظهر على نحو صحيح في القشرة.

  • تملك الفرق استقلالاً كاملاً في سير عملها وأدواتها.
  • يبقى المستودع صغيراً، بعمليات استنساخ سريعة وخطوط إنتاج كفؤة.
  • لا حاجة إلى أدوات خاصة بالمستودع الواحد: تكفي مسارات Git المعتادة وسجلّات npm.
  • كسرٌ في واجهة مصغّرة لا يُسقط بناء أخرى.

يميل اتجاه القطاع إلى المستودع الواحد، خاصةً حيث يُتشارك نظام تصميم أو مجموعة أدوات. فكلفة تنسيق التغييرات بين عدة مستودعات ترجح عادةً على مكسب الاستقلال، لا سيما بين فرق متقاربة أصلاً وبمجموعات تقنيات متشابهة. ومع ذلك تبقى المستودعات المنفصلة الخيار الأفضل للمؤسسات ذات الفرق المتفرقة التي تستعمل أُطراً مختلفة.

اعتبارات الأداء

تحمل الواجهات المصغّرة كلفة أداء قياساً بتطبيق متراص. فكلٌّ منها يحمّل حزمة JavaScript خاصة به، وCSS خاصاً به، وربما بيئة تشغيل إطاره. وعلى المتصفح أن ينزّل شفرة أكثر ويحلّلها وينفّذها. فتزداد البايتات المنقولة ويطول زمن صيرورة الصفحة قابلة للاستعمال عند أول تحميل.

ويخفّف تشارك الاعتماديات في Module Federation هذه الكلفة بضمان تحميل المكتبة مرة واحدة. لكن شفرة التطبيق — مكوّنات كل واجهة مصغّرة وأدواتها ومنطق عملها — لا تُتشارك. فإن مرّ المستخدم في جلسة واحدة بثلاث واجهات مصغّرة، نزّل شفرة الثلاث، ولو لم يتعامل إلا مع واحدة.

وتقسيم الشفرة داخل كل واجهة مصغّرة لا غنى عنه. فعلى كل واحدة أن تحمّل تحميلاً كسولاً مساراتها ومكوّناتها الثقيلة ومكتبات الأطراف الثالثة. فواجهة لوحة إدارة تضمّ مكتبة رسوم بيانية ينبغي ألّا تحمّلها قبل أن يصل المستخدم إلى صفحة ترسم رسماً بيانياً. وهي الممارسة نفسها المتبعة في التطبيقات المتراصة، مطبَّقةً داخل كل حدّ.

والتحميل المسبق تحسين ثمين. فحين يدخل المستخدم مساراً من واجهة إتمام الشراء، يسع القشرة أن تتوقع انتقاله إلى تأكيد الطلب فتبدأ جلب موارده في الخلفية. وينبغي أن تستند هذه الاستراتيجية إلى بيانات استعمال حقيقية لا إلى ظنون عن أنماط التنقّل.

ويجب حماية المسار الحرج للرسم. فعلى القشرة أن تظهر بسرعة تطبيق متراص، أي أن تكون خفيفة: قليل من JavaScript، وقليل من CSS، وقليل من الاعتماديات. فهي المسؤولة عن أول رسم، ولا ينبغي للواجهات المصغّرة البطيئة أن تؤخّره. وينبغي تحميل كل واحدة تحميلاً غير متزامن وإظهار حالة تحميل حتى تجهز.

وينبغي تحديد ميزانيات الأداء لكل واجهة مصغّرة لا للتطبيق كله فحسب. فعلى كل فريق أن يعرف الحد الأقصى لحجم حزمته، والحد الأقصى لزمن صيرورتها قابلة للاستعمال، والحد الأقصى لبصمة ذاكرتها. وتجاوزها ينبغي أن يُسقط خط الإنتاج، كما كان ليحدث في تطبيق متراص.

اختبار الواجهات المصغّرة

يجري اختبار هذه المعمارية على ثلاثة مستويات: كل واجهة مصغّرة بمعزل، والتكامل بينها، والتطبيق المركَّب ككل. ولكل مستوى أدواته وأهدافه ومسؤولوه.

وتتبع اختبارات الوحدات والمكوّنات داخل كل واجهة مصغّرة الأنماط نفسها المتبعة في أي تطبيق واجهة. فكل فريق يختبر واجهته، وتجري الاختبارات في خط إنتاجه. والفارق الوحيد أن من الحكمة الاختبار بافتراض أن القشرة وسائر الواجهات المصغّرة غير جديرة بالثقة: أي التحقّق احتياطاً من أن واجهتك تتدهور بلطف حين تمرّر القشرة خصائص غير متوقّعة أو حين لا يصل حدث منتظَر.

والتكامل أصعب المستويات. فعلى الاختبار أن يحمّل القشرة وعدة واجهات مصغّرة في آن، ويحاكي تفاعلات تعبر الحدود، ويتحقّق من صحة السلوك المركَّب. ويتفوّق Cypress وPlaywright هنا، لأنهما يعملان في متصفح حقيقي ويتعاملان مع التطبيق كاملاً.

ومن الاستراتيجيات الناجعة الاحتفاظ في بيئة الاختبار بنشرة شبيهة بنشرة الإنتاج: كل واجهة مصغّرة على عنوانها والقشرة مضبوطة لتحميلها. وتجري المجموعة على هذه النشرة وتقطع رحلات حقيقية تعبر الحدود. فتظهر بذلك مشكلات لا تُرى في الاختبارات المعزولة: إخفاقات توقيت حين لا تكون واجهة قد أنهت إقلاعها وأخرى تحاول مخاطبتها، أو انحدارات في الشكل حين يزيح تغيير CSS في واحدة تخطيط أخرى.

واختبارات الانحدار البصري مهمة هنا خصوصاً. فينبغي أن يكون لنظام التصميم المشترك مجموعته الخاصة. وأن يكون لكل واجهة مصغّرة مجموعتها لصفحاتها. وأن يكون للتطبيق المركَّب مجموعته للرحلات الحرجة. ويندمج Percy أو Chromatic أو Loki في خط الإنتاج ويلتقط الفوارق تلقائياً.

متى لا تستعمل الواجهات المصغّرة

الواجهات المصغّرة ليست المعمارية الصائبة لكل مشروع. فالتعقيد الذي تجلبه — كلفة تشغيلية، وكلفة أداء، وحاجة إلى تنسيق، وصعوبة في التقصّي — لا تبرّره إلا حاجات تنظيمية بعينها. وتطبيقها على مشروع لا يحتاجها يولّد احتكاكاً ويبطئ التطوير بلا مردود.

فريق صغير يبني تطبيقاً واحداً لا يحتاجها. فقد صُمّمت هذه المعمارية لمؤسسات ذات فرق عدة، يملك كلٌّ منها مجالاً متميزاً. وإن كانت واجهتك كلها في مقدور ثلاثة أو أربعة أشخاص، فتطبيق متراص بوحدات منظَّمة جيداً سيكون أوفر إنتاجاً وأسرع بناءً وأسهل صيانةً.

والتطبيق ذو المجالات شديدة الاقتران مرشّح رديء. فإن كانت كل خاصية متشابكة مع سائر الخصائص، وإن كان الفهرس والسلة وإتمام الشراء متداخلة إلى حدّ يمنع فصلها نظيفاً، فإن تقسيمها سيلزمك ببناء طبقات تواصل معقّدة تحاكي الوصول المباشر الذي كان في المتراص. وهذا هو النمط المضاد المسمّى بالمتراص الموزَّع.

والمؤسسة التي تنقصها النضج التشغيلي ستعاني. فهذه المعمارية تستلزم ممارسات تشغيل جيدة، واختبارات مؤتمتة، ومراقبة، واستجابةً للحوادث. وعلى كل فريق أن يقدر على النشر بنفسه والتراجع سريعاً. فإن غابت هذه الأسس فالأولى الاستثمار فيها أولاً في سياق تطبيق متراص.

والتطبيقات الحرجة في الأداء، ذات المتطلبات الصارمة لزمن التحميل، قد لا تحتمل هذه الكلفة. فإن كانت كل جزء من الألف من الثانية مهماً — كما في لوحة تداول لحظية أو واجهة فيديو — فقد تُخرجك طلبات الشبكة الإضافية وزمن تحليل JavaScript الزائد عن ميزانيتك.

  • فريق الواجهة لديك أقل من أربعة أشخاص.
  • في التطبيق أقل من خمسة حدود مجال متمايزة بوضوح.
  • لا خبرة للفريق بالخدمات المصغّرة أو الأنظمة الموزَّعة.
  • لا تملك المؤسسة تكاملاً مستمراً ولا مراقبةً مؤتمتين.
  • للتطبيق ميزانيات أداء صارمة يصعب الوفاء بها أصلاً.
  • الخصائص شديدة الاقتران ولا تنفصل نظيفاً إلى مجالات.

الواجهات المصغّرة قرار تنظيمي يتبدّى في هيئة معمارية تقنية. فإن لم تكن تحتاج الاستقلال التنظيمي الذي تقدّمه — فرقاً مختلفة، وإيقاعات مختلفة، وخيارات تقنية مختلفة — فالكلفة التقنية لا تستحق.

خاتمة

الواجهات المصغّرة نمط قوي للمؤسسات التي تحتاج توسيع تطوير الواجهة بين فرق عدة. وتجلب المعمارية مزايا حقيقية: نشراً مستقلاً، واستقلالاً للفرق، وحرية تقنية، وإمكان الإطلاق بإيقاعات متباينة. وهذه المزايا ملموسة وقابلة للقياس حين تُطبَّق المعمارية على المشكلة الصائبة.

ومفتاح النجاح أن تُعامَل حلاً تنظيمياً أولاً وحلاً تقنياً ثانياً. فهذه المعمارية قائمة لتجعل الفرق مستقلة، لا لتنتج شفرة أنيقة الفكّ. وينبغي لكل قرار تقني — Module Federation أم إطارات مضمّنة، مستودع واحد أم عدة مستودعات، ناقل أحداث أم مخزن مشترك — أن يصوّب نحو رفع سرعة الفرق من دون فقدان تجربة متماسكة.

ابدأ صغيراً. انطلق من واجهة مصغّرة واحدة ذات حدّ مجال واضح وفريق متحمّس لقيادتها بنفسه. أظهِر أن خط الإنتاج يعمل، وأن الأداء يصمد، وأن الفريق يسلّم أسرع. ثم وسّع تدريجياً. فليست المسألة كل شيء أو لا شيء: النهج الهجين — واجهة أو اثنتان داخل تطبيق متراص في سائره — يكون غالباً خير نقطة انطلاق.

وأشيع الإخفاقات التجريد السابق لأوانه. تبني الفرق طبقات تنسيق متقنة، ونظم حالة مشتركة، وبنية تحتية عابرة، قبل أن تفهم حاجاتها الحقيقية. ابدأ بأبسط تكامل ممكن — قشرة تحمّل الواجهات المصغّرة عبر وسم script أو عبر Module Federation — ولا تضف تعقيداً إلا حين يثبت عجز البسيط. فدرجة التجريد الصائبة تتكشّف في الاستعمال الحقيقي لا في التصميم المسبق.

لقد جعل Module Federation الواجهات المصغّرة في متناول اليد أكثر من أي وقت مضى، لكن الأسئلة العميقة تبقى تنظيمية. أتحتاج فرقك إلى النشر المستقل؟ أتحتمل مؤسستك التعقيد التشغيلي؟ أتنفصل مجالاتك نظيفاً؟ من أجاب بنعم وجدها محوِّلة. ومن أجاب بلا وجدها مزعجة. والمعمارية بذاتها محايدة: قيمتها رهن السياق الذي تُطبَّق فيه بالكامل.

See what your own repository can account for.

Thirty minutes on a repository you choose, including the part the record cannot attribute.