كيف رأينا متراصة. الجزء 3 ، مدير الإطار بدون إطارات

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



نظرًا لحقيقة أن التطبيقات يتم تحميلها في إطار iframe ، فهناك مشاكل في التخطيط ، والإضافات لا تعمل بشكل صحيح ، ولا يزال العملاء ينزلون حزمتين مع Angular ، حتى لو كانت إصدارات Angular في التطبيق و Frame Manager هي نفسها. ويبدو استخدام iframe في عام 2020 سلوكًا سيئًا. ولكن ماذا لو تخلينا عن الإطارات وحملنا جميع التطبيقات في نافذة واحدة؟



اتضح أن هذا ممكن ، والآن سأخبرك بكيفية تنفيذه.







الحلول الممكنة



سبا فردي : "موجه جافا سكريبت للخدمات المصغرة الأمامية" - كما هو موضح في موقع المكتبة على الويب. يتيح لك تشغيل التطبيقات المكتوبة في أطر مختلفة في نفس الصفحة في نفس الوقت. لم ينجح الحل بالنسبة لنا: لم تكن هناك حاجة إلى معظم الوظائف ، كما أن أداة التحميل System.js المستخدمة فيه في بعض الحالات تخلق مشاكل عند الإنشاء باستخدام حزمة الويب. ولا يبدو أن استخدام أداة تحميل وحدة مع حزمة الويب هو الحل الأفضل.



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



تنفيذ مدير الإطار



دعونا نرى كيف يتم تنفيذ تحميل التطبيقات بدون إطارات في مدير الإطارات باستخدام مثال.



يبدو الإعداد الأولي كما يلي: لدينا تطبيق رئيسي - main. يتم تحميله دائمًا أولاً ويجب تحميل تطبيقات أخرى داخل نفسه - التطبيق 1 والتطبيق 2. لنقم بإنشاء ثلاثة تطبيقات باستخدام الأمر ng new <app-name> . بعد ذلك ، سنقوم بتكوين البروكسيات بحيث يتم إرسال ملفات html و js للتطبيق المطلوب إلى طلبات مثل /<app-name>/*.js ، /<app-name>/*.html ، ويتم إرسال إحصائيات التطبيق الرئيسي إلى جميع الطلبات الأخرى.



proxy.conf.js
const cfg = [
  {
    context: [
      '/app1/*.js',
      '/app1/*.html'
    ],
    target: 'http://localhost:3001/'
  },
  {
    context: [
      '/app2/*.js',
      '/app2/*.html'
    ],
    target: 'http://localhost:3002/'
  }
];

module.exports = cfg;




بالنسبة للتطبيقات app-1 و app-2 ، سنحدد baseHref في angular.json app1 و app2 على التوالي. سنقوم أيضًا بتغيير محددات مكونات الجذر إلى app-1 و app-2.



هذا ما يبدو عليه التطبيق الرئيسي




أولاً ، دعنا نحمل تطبيقًا فرعيًا واحدًا على الأقل. للقيام بذلك ، تحتاج إلى تحميل جميع ملفات js المحددة في index.html.



اكتشف عناوين url لملفات js: قم بتقديم طلب http لـ index.html ، وقم بتحليل السلسلة باستخدام DOMParser وحدد جميع علامات البرنامج النصي. دعنا نحول كل شيء إلى مصفوفة ونرسمه إلى مجموعة من العناوين. ستحتوي العناوين التي تم الحصول عليها بهذه الطريقة على location.origin ، لذلك نستبدلها بسلسلة فارغة:



private getAppHTML(): Observable<string> {
  return this.http.get(`/${this.currentApp}/index.html`, {responseType: 'text'});
}

private getScriptUrls(html: string): string[] {
  const appDocument: Document = new DOMParser().parseFromString(html, 'text/html');
  const scriptElements = appDocument.querySelectorAll('script');

  return Array.from(scriptElements)
    .map(({src}) => src.replace(this.document.location.origin, ''));
}


هناك عناوين ، تحتاج الآن إلى تحميل البرامج النصية:

private importJs(url: string): Observable<void> {
  return new Observable(sub => {
    const script = this.document.createElement('script');

    script.src = url;
    script.onload = () => {
      this.document.head.removeChild(script);

      sub.next();
      sub.complete();
    };
    script.onerror = e => {
      sub.error(e);
    };

    this.document.head.appendChild(script);
  });
}


تضيف الشفرة عناصر البرنامج النصي مع src الضرورية إلى DOM ، وبعد تنزيل البرامج النصية ، يتم حذف هذه العناصر - وهو حل قياسي إلى حد ما ، يتم تحميله في webpack ويتم تنفيذ system.js بالمثل.



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



تحميل حزم webpack



يستخدم Angular حزمة الويب لتحميل الوحدات. في التكوين القياسي ، يقسم webpack الكود إلى الحزم التالية:



  • main.js - كل كود العميل ؛
  • polyfills.js - polyfills.
  • styles.js - الأنماط ؛
  • vendor.js - جميع المكتبات المستخدمة في التطبيق ، بما في ذلك Angular ؛
  • runtime.js - وقت تشغيل webpack ؛
  • <module-name> .module.js - الوحدات البطيئة.


إذا فتحت أيًا من هذه الملفات ، يمكنك في البداية رؤية الرمز:



(window["webpackJsonp"] = window["webpackJsonp"] || []).push([/.../])


وفي runtime.js:



var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);


وهي تعمل على النحو التالي: عندما يتم تحميل الحزمة ، فإنها تنشئ مصفوفة webpackJsonp ، إذا لم تكن موجودة بالفعل ، وتدفع محتوياتها فيها. يتجاوز وقت تشغيل webpack وظيفة الدفع لهذه المصفوفة بحيث يمكنك لاحقًا تحميل حزم جديدة ، ومعالجة كل شيء موجود بالفعل في المصفوفة.



كل هذا ضروري حتى لا يهم الترتيب الذي يتم تحميل الحزم به.



وبالتالي ، إذا قمت بتحميل تطبيق Angular ثانٍ ، فسيحاول إضافة وحداته النمطية إلى وقت تشغيل webpack الموجود بالفعل ، والذي سيؤدي في أفضل الأحوال إلى إعادة تهيئة التطبيق الرئيسي.



قم بتغيير اسم webpackJsonp



لتجنب التعارضات ، تحتاج إلى تغيير اسم مصفوفة webpackJsonp. تستخدم Angular CLI تهيئة webpack الخاصة بها ، ولكن يمكن تمديدها إذا رغبت في ذلك. للقيام بذلك ، تحتاج إلى تثبيت حزمة بناء الزاوي / حزمة الويب المخصصة:



npm i -D @ angular-builders / custom-webpack.



ثم ، في ملف angular.json في تكوين المشروع ، استبدل architecture.build.builder بـ @ angular-builders / custom-webpack: المتصفح وأضف إلى architecture.build.options :



"customWebpackConfig": {
  "path": "./custom-webpack.config.js"
}


تحتاج أيضًا إلى استبدال architecture.serve.builder بـ @ angular-builders / custom-webpack: dev-server لكي يعمل هذا محليًا مع خادم dev.



أنت الآن بحاجة إلى إنشاء ملف تكوين webpack ، والذي تم تحديده أعلاه في customWebpackConfig: custom-webpack.config.js



إنه يحدد الإعدادات المخصصة ، يمكنك قراءة المزيد في الوثائق الرسمية .



نحن مهتمون بـ jsonpFunction .



يمكنك تعيين مثل هذا التكوين في جميع التطبيقات المحملة لتجنب التعارضات (إذا استمرت التعارضات بعد ذلك ، فمن المرجح أنك قد تعرضت لعن):



module.exports = {
 output: {
   jsonpFunction: Math.random().toString()
 },
};


الآن ، إذا حاولنا تحميل جميع البرامج النصية بالطريقة الموضحة أعلاه ، فسنرى خطأ:



التطبيق المحدد 1 لا يطابق أي عناصر



قبل تحميل التطبيق ، تحتاج إلى إضافة عنصر الجذر الخاص به إلى DOM:



private addAppRootElement(appName: string) {  
  const rootElementSelector = APP_CFG[appName].rootElement;
  this.appRootElement = this.document.createElement(rootElementSelector);
  this.appContainer.nativeElement.appendChild(this.appRootElement);
}


دعنا نحاول مرة أخرى - يا هلا ، تم تحميل التطبيق!







التبديل بين التطبيقات



نقوم بإزالة التطبيق السابق من DOM ويمكننا التبديل بين التطبيقات:



destroyApp () {
  if (!this.currentApp) return;
  this.appContainer.nativeElement.removeChild(this.appRootElement);
}


ولكن هناك بعض العيوب: عندما ننتقل إلى app-1 → app-2 → app-1 ، نعيد تحميل حزم js لتطبيق app-1 وننفذ التعليمات البرمجية الخاصة بها. بالإضافة إلى ذلك ، نحن لا ندمر التطبيقات التي تم تحميلها مسبقًا ، مما يؤدي إلى تسرب الذاكرة واستهلاك الموارد بشكل غير ضروري.



إذا لم تقم بإعادة تنزيل حزم التطبيق ، فلن يتم تنفيذ عملية التمهيد من تلقاء نفسها ولن يتم تحميل التطبيق. تحتاج إلى تفويض عملية بدء تشغيل التمهيد للتطبيق الرئيسي.



للقيام بذلك ، دعنا نعيد كتابة ملف main.ts للتطبيقات المحملة:



const BOOTSTRAP_FN_NAME = 'ngBootstrap';
const bootstrapFn = (opts?) => platformBrowserDynamic().bootstrapModule(AppModule, opts);

window[BOOTSTRAP_FN_NAME] = bootstrapFn;


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



لتدمير التطبيق وإصلاح تسرب الذاكرة ، تحتاج إلى استدعاء طريقة التدمير الخاصة بوحدة تطبيق الجذر (AppModule). تُرجع الدالة platformBrowserDynamic (). BootstrapModule ارتباطًا إليها ، مما يعني أن وظيفة الغلاف الخاصة بنا:



this.getBootstrapFn$().subscribe((bootstrapFn: BootstrapFn) => {
  this.zone.runOutsideAngular(() => {
    bootstrapFn().then(m => {
      this.ngModule = m;  //    
    });
  });
});

this.ngModule.destroy(); //   


بعد استدعاء التدمير () على الوحدة النمطية الجذر ، سيتم استدعاء أساليب ngOnDestroy () لجميع الخدمات ومكونات التطبيق (إذا تم تنفيذها).



كل شيء يعمل. ولكن إذا كان التطبيق المحمّل يحتوي على وحدات نمطية كسولة ، فلن يتمكنوا من التحميل: يمكنك







أن ترى أن مسار التطبيق مفقود في العنوان (يجب أن يكون هناك /app2/lazy-lazy-module.js ). لحل هذه المشكلة ، تحتاج إلى مزامنة href الأساسي للتطبيق الرئيسي والمحمّل:



private syncBaseHref(appBaseHref: string) {
  const base = this.document.querySelector('base');

  base.href = appBaseHref;
}


الآن كل شيء يعمل كما ينبغي.



النتيجة



لنرى كم من الوقت يستغرق تحميل تطبيق فرعي عن طريق وضع console.time () قبل تحميل البرامج النصية في التطبيق الرئيسي و console.timeEnd () في مُنشئ المكون الجذر للتطبيق الرئيسي.



عندما يتم تحميل تطبيقات app-1 و app-2 لأول مرة ، نرى شيئًا مثل هذا:







سريع جدًا. ولكن إذا عدت إلى التطبيق الذي تم تنزيله مسبقًا ، يمكنك رؤية الأرقام التالية:







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



مدير الإطار بدون إطارات



يتم تنفيذ الحل الموضح أعلاه في مدير الإطار ، والذي يدعم تحميل التطبيقات مع أو بدون إطارات مضمنة. يتم الآن تحميل حوالي ربع جميع التطبيقات في Tinkoff Business بدون إطارات ، ويتزايد عددها باستمرار.



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



مستودع مع رمز عينة



All Articles