هجرة فرانكشتاين: نهج الإطار الحيادي (الجزء 2)

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

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

حان الوقت لاختبار النظرية
حان الوقت لوضع النظرية على المحك. (معاينة كبيرة)

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

طريقة عرض افتراضية لتطبيق TodoMVC
طريقة عرض افتراضية لتطبيق TodoMVC (معاينة كبيرة)

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

ما هو هذا "الأكثر مرونة" الذي سنهاجر إليه بعد ذلك؟ لإظهار حالة عملية للغاية مفيدة في الحياة الواقعية ، كان علي الاختيار من بين أكثر إطاري عمل شيوعًا هذه الأيام: React و Vue. ومع ذلك ، أيهما سأختار ، فإننا سنفتقد بعض جوانب الاتجاه الآخر.

المزيد بعد القفز! أكمل القراءة أدناه ↓

لذلك في هذا الجزء ، سنستعرض كلا الأمرين التاليين:

  • ترحيل تطبيق jQuery إلى React و
  • ترحيل تطبيق jQuery إلى Vue .
أهدافنا: نتائج الترحيل إلى React و Vue
أهدافنا: نتائج الترحيل إلى React و Vue. (معاينة كبيرة)

مستودعات الكود

كل الأكواد المذكورة هنا متاحة للجمهور ، ويمكنك الوصول إليها متى شئت. هناك نوعان من المستودعات المتاحة لك للعب بها:

  • فرانكشتاين تودومفك
    يحتوي هذا المستودع على تطبيقات TodoMVC في أطر / مكتبات مختلفة. على سبيل المثال ، يمكنك العثور على فروع مثل vue و angularjs و react و jquery في هذا المستودع.
  • عرض فرانكشتاين
    يحتوي على العديد من الفروع ، يمثل كل منها اتجاه انتقال معين بين التطبيقات ، وهو متاح في المستودع الأول. هناك فروع مثل migration/jquery-to-react و migration/jquery-to-vue ، على وجه الخصوص ، سنغطيها لاحقًا.

كلا المستودعات قيد العمل ويجب إضافة الفروع الجديدة بتطبيقات جديدة واتجاهات الترحيل إليهما بانتظام. ( أنت حر في المساهمة أيضًا! ) سجل الالتزامات في فروع الترحيل منظم جيدًا ويمكن أن يكون بمثابة وثائق إضافية مع تفاصيل أكثر مما يمكنني تغطيته في هذه المقالة.

الآن ، دعونا نتسخ أيدينا! أمامنا طريق طويل ، لذا لا تتوقع أن تكون رحلة سلسة. الأمر متروك لك لتقرر كيف تريد متابعة هذه المقالة ، ولكن يمكنك القيام بما يلي:

  • قم باستنساخ فرع jquery من مستودع Frankenstein TodoMVC واتبع بدقة جميع الإرشادات أدناه.
  • بدلاً من ذلك ، يمكنك فتح فرع مخصص إما للترحيل إلى React أو الترحيل إلى Vue من مستودع Frankenstein Demo والمتابعة جنبًا إلى جنب مع سجل الالتزامات.
  • بدلاً من ذلك ، يمكنك الاسترخاء ومتابعة القراءة لأنني سأقوم بتسليط الضوء على الكود الأكثر أهمية هنا ، والأهم من ذلك بكثير فهم آليات العملية بدلاً من الكود الفعلي.

أود أن أذكر مرة أخرى أننا سنتبع بدقة الخطوات المقدمة في الجزء النظري الأول من المقالة.

دعنا نتعمق في!

  1. تحديد الخدمات المصغرة
  2. السماح بوصول مضيف إلى أجنبي
  3. اكتب خدمة / مكونًا غريبًا
  4. اكتب التفاف مكون الويب حول خدمة الكائنات الفضائية
  5. استبدل خدمة المضيف بمكون الويب
  6. اشطفها وكررها مع كل مكوناتك
  7. التبديل إلى أجنبي

1. تحديد الخدمات المصغرة

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

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

  1. حقل إدخال لإضافة عنصر جديد.
    يمكن أن تحتوي هذه الخدمة أيضًا على عنوان التطبيق ، بناءً على تحديد موضع هذه العناصر تمامًا.
  2. قائمة بالعناصر المضافة بالفعل.
    هذه الخدمة أكثر تقدمًا ، ومع القائمة نفسها ، تحتوي أيضًا على إجراءات مثل التصفية وإجراءات عنصر القائمة وما إلى ذلك.
تم تقسيم تطبيق TodoMVC إلى خدمتين صغيرتين مستقلتين
تم تقسيم تطبيق TodoMVC إلى خدمتين صغيرتين مستقلتين. (معاينة كبيرة)

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

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

2. السماح بوصول مضيف إلى أجنبي

اسمحوا لي أن أذكرك بإيجاز ما هي هذه.

  • مضيف
    هذا ما يسمى تطبيقنا الحالي. إنه مكتوب بالإطار الذي نحن على وشك الابتعاد عنه . في هذه الحالة بالذات ، تطبيق jQuery الخاص بنا.
  • كائن فضائي
    ببساطة ، هذه إعادة كتابة تدريجية للمضيف في إطار العمل الجديد الذي نحن على وشك الانتقال إليه . مرة أخرى ، في هذه الحالة بالذات ، إنه تطبيق React أو Vue.

القاعدة الأساسية عند تقسيم Host and Alien هي أنه يجب أن تكون قادرًا على تطوير ونشر أي منهما دون كسر الآخر - في أي وقت.

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

إضافة كائن فضائي كنموذج فرعي لمضيفك

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

يجب أن تبدو المبادئ العامة لبنية مشروعنا مع الوحدات الفرعية git كما يلي:

  • كل من Host و Alien مستقلان ويتم الاحتفاظ بهما في مستودعات git منفصلة ؛
  • يشير المضيف إلى Alien كوحدة فرعية. في هذه المرحلة ، يختار المضيف حالة معينة (التزام) من Alien ويضيفها ، كما يبدو ، كمجلد فرعي في بنية مجلد المضيف.
تمت إضافة React TodoMVC كوحدة git فرعية في تطبيق jQuery TodoMVC
تمت إضافة React TodoMVC كوحدة git فرعية في تطبيق jQuery TodoMVC. (معاينة كبيرة)

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

في المقتطفات أدناه ، نستخدم اتجاه React كمثال. لأي اتجاه ترحيل آخر ، استبدل react باسم فرع من Frankenstein TodoMVC أو اضبط القيم المخصصة عند الحاجة.

إذا تابعت استخدام تطبيق jQuery TodoMVC الأصلي:

 $ git submodule add -b react [email protected]:mishunov/frankenstein-todomvc.git react $ git submodule update --remote $ cd react $ npm i

إذا تابعت مع فرع migration/jquery-to-react (أو أي اتجاه ترحيل آخر) من مستودع Frankenstein Demo ، فيجب أن يكون تطبيق Alien موجودًا بالفعل كوحدة git submodule ، ويجب أن ترى مجلدًا خاصًا. ومع ذلك ، فإن المجلد فارغ افتراضيًا ، وتحتاج إلى تحديث الوحدات الفرعية المسجلة وتهيئتها.

من جذر مشروعك (مضيفك):

 $ git submodule update --init $ cd react $ npm i

لاحظ أنه في كلتا الحالتين نقوم بتثبيت التبعيات لتطبيق Alien ، ولكن هذه التبعيات تصبح محمية في المجلد الفرعي ولن تلوث مضيفنا.

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

3. اكتب خدمة / مكونًا غريبًا

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

تحتوي فروع مستودع Frankenstein TodoMVC على مكون ناتج يمثل الخدمة الأولى "حقل الإدخال لإضافة عنصر جديد" كمكون رأس:

  • مكون الرأس في React
  • مكون الرأس في Vue

كتابة المكونات في إطار العمل الذي تختاره خارج نطاق هذه المقالة وليست جزءًا من Frankenstein Migration. ومع ذلك ، هناك بعض الأشياء التي يجب وضعها في الاعتبار أثناء كتابة مكون Alien.

استقلال

بادئ ذي بدء ، يجب أن تتبع المكونات في Alien نفس مبدأ الاستقلال ، الذي تم إعداده مسبقًا من جانب المضيف: يجب ألا تعتمد المكونات على المكونات الأخرى بأي شكل من الأشكال.

التوافقية

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

كمثال ، ألق نظرة على ملف js/storage.js الذي يعد قناة الاتصال الأساسية لمكونات jQuery الخاصة بنا:

 ... fetch: function() { return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); }, save: function(todos) { localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); var event = new CustomEvent("store-update", { detail: { todos } }); document.dispatchEvent(event); }, ...

هنا ، نستخدم localStorage (لأن هذا المثال ليس حرجًا من الناحية الأمنية) لتخزين عناصر المهام الخاصة بنا ، وبمجرد تسجيل التغييرات على التخزين ، نرسل حدث DOM مخصصًا على عنصر document الذي يمكن لأي مكون الاستماع إليه.

في الوقت نفسه ، من جانب الكائن الفضائي (دعنا نقول React) ، يمكننا إعداد اتصال معقد لإدارة الحالة كما نريد. ومع ذلك ، ربما يكون من الذكاء الاحتفاظ بها للمستقبل: لدمج مكون Alien React الخاص بنا بنجاح في Host ، يتعين علينا الاتصال بنفس قناة الاتصال التي يستخدمها Host. في هذه الحالة ، يكون localStorage . لتبسيط الأمور ، قمنا فقط بنسخ ملف تخزين Host's إلى Alien وربطنا مكوناتنا به:

 import todoStorage from "../storage"; class Header extends Component { constructor(props) { this.state = { todos: todoStorage.fetch() }; } componentDidMount() { document.addEventListener("store-update", this.updateTodos); } componentWillUnmount() { document.removeEventListener("store-update", this.updateTodos); } componentDidUpdate(prevProps, prevState) { if (prevState.todos !== this.state.todos) { todoStorage.save(this.state.todos); } } ... }

الآن ، يمكن لمكونات Alien الخاصة بنا التحدث بنفس اللغة مع مكونات المضيف والعكس صحيح.

4. كتابة غلاف مكون الويب حول خدمة الكائنات الفضائية

على الرغم من أننا الآن في الخطوة الرابعة فقط ، فقد حققنا الكثير:

  • لقد قمنا بتقسيم تطبيق المضيف الخاص بنا إلى خدمات مستقلة جاهزة للاستبدال بخدمات Alien ؛
  • لقد أنشأنا Host and Alien ليكونا مستقلين تمامًا عن بعضهما البعض ، ولكننا متصلان جيدًا عبر git submodules ؛
  • لقد كتبنا أول مكون كائن فضائي باستخدام إطار العمل الجديد.

حان الوقت الآن لإنشاء جسر بين Host و Alien حتى يعمل مكون Alien الجديد في المضيف.

تذكير من الجزء 1 : تأكد من أن مضيفك لديه حزمة حزم متاحة. في هذه المقالة ، نعتمد على Webpack ، لكن هذا لا يعني أن التقنية لن تعمل مع Rollup أو أي حزمة أخرى من اختيارك. ومع ذلك ، أترك التعيين من Webpack إلى تجاربك.

اصطلاح التسمية

كما ذكرنا في المقالة السابقة ، سنستخدم Web Components لدمج Alien في Host. من جانب المضيف ، نقوم بإنشاء ملف جديد: js/frankenstein-wrappers/Header-wrapper.js . (سيكون غلاف فرانكشتاين الأول لدينا.) ضع في اعتبارك أنه من الجيد تسمية أغلفةك بنفس اسم المكونات الخاصة بك في تطبيق Alien ، على سبيل المثال فقط عن طريق إضافة لاحقة " -wrapper ". سترى لاحقًا سبب اعتبار هذه فكرة جيدة ، ولكن في الوقت الحالي ، دعنا نتفق على أن هذا يعني أنه إذا كان المكون Alien يسمى Header.js (في React) أو Header.vue (في Vue) ، فإن الغلاف المقابل في يجب تسمية جانب المضيف باسم Header-wrapper.js .

في غلافنا الأول ، نبدأ بالنموذج الأساسي لتسجيل عنصر مخصص:

 class FrankensteinWrapper extends HTMLElement {} customElements.define("frankenstein-header-wrapper", FrankensteinWrapper);

بعد ذلك ، علينا تهيئة Shadow DOM لهذا العنصر.

يرجى الرجوع إلى الجزء 1 للحصول على سبب لاستخدام Shadow DOM.

 class FrankensteinWrapper extends HTMLElement { connectedCallback() { this.attachShadow({ mode: "open" }); } }

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

 import React from "../../react/node_modules/react"; import ReactDOM from "../../react/node_modules/react-dom"; import HeaderApp from "../../react/src/components/Header"; ...

هنا علينا أن نتوقف للحظة. لاحظ أننا لا نستورد تبعيات Alien من node_modules . كل شيء يأتي من الكائن الفضائي نفسه الموجود في react/ مجلد فرعي. هذا هو السبب في أن الخطوة 2 مهمة للغاية ، ومن الأهمية بمكان التأكد من أن المضيف لديه حق الوصول الكامل إلى أصول Alien.

الآن ، يمكننا عرض مكون Alien الخاص بنا داخل Shadow DOM الخاص بمكون الويب:

 ... connectedCallback() { ... ReactDOM.render(<HeaderApp />, this.shadowRoot); } ...

ملاحظة : في هذه الحالة ، لا تحتاج React إلى أي شيء آخر. ومع ذلك ، لتصيير مكون Vue ، تحتاج إلى إضافة عقدة التفاف لاحتواء مكون Vue الخاص بك مثل ما يلي:

 ... connectedCallback() { const mountPoint = document.createElement("div"); this.attachShadow({ mode: "open" }).appendChild(mountPoint); new Vue({ render: h => h(VueHeader) }).$mount(mountPoint); } ...

والسبب في ذلك هو الاختلاف في كيفية عرض مكونات React و Vue: تقوم React بإلحاق المكون بعقدة DOM المشار إليها ، بينما يستبدل Vue عقدة DOM المشار إليها بالمكون. وبالتالي ، إذا فعلنا .$mount(this.shadowRoot) لـ Vue ، فإنه يستبدل Shadow DOM بشكل أساسي.

هذا كل ما يتعين علينا القيام به للغلاف الخاص بنا في الوقت الحالي. يمكن العثور على النتيجة الحالية لبرنامج تضمين Frankenstein في كل من اتجاهات الترحيل jQuery-to-React و jQuery-to-Vue هنا:

  • غلاف فرانكشتاين لمكون التفاعل
  • غلاف فرانكشتاين لمكون Vue

لتلخيص آليات غلاف فرانكشتاين:

  1. إنشاء عنصر مخصص ،
  2. بدء Shadow DOM ،
  3. قم باستيراد كل ما هو مطلوب لعرض مكون Alien ،
  4. عرض المكون الفضائي داخل Shadow DOM الخاص بالعنصر المخصص.

ومع ذلك ، هذا لا يجعل لدينا Alien in Host تلقائيًا. يتعين علينا استبدال ترميز Host الحالي بغلاف Frankenstein الجديد.

اربطوا أحزمة الأمان ، فقد لا يكون الأمر مباشرًا كما يتوقع المرء!

5. استبدال خدمة المضيف بمكون الويب

دعنا نكمل ونضيف ملف Header-wrapper.js الجديد إلى index.html واستبدل علامة الرأس الحالية بالعنصر المخصص <frankenstein-header-wrapper> الذي تم إنشاؤه حديثًا.

 ... <!-- <header class="header">--> <!-- <h1>todos</h1>--> <!-- <input class="new-todo" placeholder="What needs to be done?" autofocus>--> <!-- </header>--> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script type="module" src="js/frankenstein-wrappers/Header-wrapper.js"></script>

لسوء الحظ ، لن يعمل هذا بهذه البساطة. إذا فتحت مستعرضًا وتحققت من وحدة التحكم ، فهناك Uncaught SyntaxError في انتظارك. اعتمادًا على المتصفح ودعمه لوحدات ES6 ، سيكون إما مرتبطًا بواردات ES6 أو بالطريقة التي يتم بها عرض مكون Alien. في كلتا الحالتين ، يتعين علينا القيام بشيء حيال ذلك ، ولكن يجب أن تكون المشكلة والحل مألوفين وواضحين لمعظم القراء.

5.1 قم بتحديث Webpack و Babel عند الحاجة

يجب أن نشرك بعض سحر Webpack و Babel قبل دمج غلاف Frankenstein الخاص بنا. إن مناقشة هذه الأدوات خارج نطاق المقالة ، ولكن يمكنك إلقاء نظرة على الالتزامات المقابلة في مستودع Frankenstein Demo:

  • التكوين للترحيل إلى React
  • التكوين للترحيل إلى Vue

بشكل أساسي ، قمنا بإعداد معالجة الملفات بالإضافة إلى نقطة دخول جديدة فرانكشتاين في تكوين Webpack frankenstein كل ما يتعلق بأغلفة فرانكشتاين في مكان واحد.

بمجرد أن يعرف Webpack in Host كيفية معالجة مكون Alien ومكونات الويب ، فنحن على استعداد لاستبدال ترميز Host ببرنامج تضمين Frankenstein الجديد.

5.2 استبدال المكون الفعلي

يجب أن يكون استبدال المكون مباشرًا الآن. في index.html الخاص بالمضيف ، قم بما يلي:

  1. استبدل <header class="header"> عنصر DOM بـ <frankenstein-header-wrapper> ؛
  2. إضافة برنامج نصي جديد frankenstein.js . هذه هي نقطة الدخول الجديدة في Webpack التي تحتوي على كل ما يتعلق بأغلفة Frankenstein.
 ... <!-- We replace <header class="header"> --> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script src="./frankenstein.js"></script>

هذا هو! أعد تشغيل الخادم إذا لزم الأمر وشاهد سحر مكون Alien المدمج في Host.

ومع ذلك ، يبدو أن هناك شيئًا ما مفقودًا. لا يبدو مكون Alien في سياق المضيف بنفس الطريقة التي يظهر بها في سياق تطبيق Alien المستقل. إنه ببساطة غير منظم.

مكون Alien React غير منظم بعد دمجه في المضيف
مكون Alien React غير المصمم بعد دمجه في المضيف (معاينة كبيرة)

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

5.3 معلومات عامة عن تصميم المكون الفضائي

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

الأنماط العالمية

نحن جميعًا على دراية بهذه الأمور: يمكن توزيع الأنماط العامة (وعادة ما يتم توزيعها) بدون أي مكون معين ويتم تطبيقها على الصفحة بأكملها. تؤثر الأنماط العامة على جميع عُقد DOM ذات المحددات المطابقة.

بعض الأمثلة على الأنماط العامة هي علامات <style> و <link rel="stylesheet"> الموجودة في index.html . بدلاً من ذلك ، يمكن استيراد ورقة أنماط عامة إلى بعض وحدات JS الجذرية بحيث يمكن لجميع المكونات الوصول إليها أيضًا.

مشكلة تطبيقات التصميم بهذه الطريقة واضحة: الحفاظ على أوراق أنماط متجانسة للتطبيقات الكبيرة يصبح صعبًا للغاية. أيضًا ، كما رأينا في المقالة السابقة ، يمكن للأنماط العامة بسهولة كسر المكونات التي يتم عرضها مباشرة في شجرة DOM الرئيسية كما هو الحال في React أو Vue.

الأنماط المجمعة

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

لماذا يمكن أن تكون الأنماط المحددة النطاق هشة؟

في الجزء 1 ، عند تبرير استخدام Shadow DOM في Frankenstein Migration ، قمنا بتغطية موضوع تحديد النطاق مقابل التغليف) وكيف يختلف تغليف Shadow DOM عن أدوات تصميم النطاق. ومع ذلك ، لم نوضح لماذا توفر أدوات تحديد النطاق مثل هذا التصميم الهش لمكوناتنا ، والآن ، عندما واجهنا مكون Alien غير المصمم ، أصبح ضروريًا للفهم.

تعمل جميع أدوات تحديد النطاق للأطر الحديثة بشكل مشابه:

  • أنت تكتب أنماطًا لمكونك بطريقة ما دون التفكير كثيرًا في النطاق أو التغليف ؛
  • تقوم بتشغيل مكوناتك باستخدام أوراق أنماط مستوردة / مضمنة من خلال بعض أنظمة التجميع ، مثل Webpack أو Rollup ؛
  • تقوم أداة التجميع بإنشاء فئات CSS فريدة أو سمات أخرى ، مما يؤدي إلى إنشاء وحقن محددات فردية لكل من HTML وأوراق الأنماط المقابلة ؛
  • يقوم المجمع بإدخال <style> في <head> للمستند الخاص بك ويضع أنماط المكونات الخاصة بك مع محددات مختلطة فريدة من نوعها.

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

دعنا نلقي نظرة على المضيف الحالي باستخدام DevTools. عند فحص غلاف Frankenstein المُضاف حديثًا باستخدام مكون Alien React ، على سبيل المثال ، يمكننا رؤية شيء مثل هذا:

غلاف فرانكشتاين مع مكون أجنبي بداخله. لاحظ فئات CSS الفريدة على عقد Alien.
غلاف فرانكشتاين مع مكون أجنبي بداخله. لاحظ فئات CSS الفريدة على عقد Alien. (معاينة كبيرة)

لذلك ، يقوم Webpack بإنشاء فئات CSS فريدة لمكوننا. رائعة! أين الأنماط إذن؟ حسنًا ، الأنماط هي بالضبط حيث تم تصميمها لتكون - في <head> الخاص بالمستند.

بينما يكون مكون Alien داخل غلاف Frankenstein ، فإن أنماطه موجودة في رأس المستند.
أثناء وجود مكون Alien داخل غلاف Frankenstein ، تكون أنماطه في المستند <head> . (معاينة كبيرة)

لذلك كل شيء يعمل كما ينبغي ، وهذه هي المشكلة الرئيسية. نظرًا لأن مكون Alien الخاص بنا موجود في Shadow DOM ، وكما هو موضح في الجزء رقم 1 ، يوفر Shadow DOM تغليفًا كاملاً للمكونات من بقية الصفحة والأنماط العامة ، بما في ذلك أوراق الأنماط التي تم إنشاؤها حديثًا للمكون الذي لا يمكنه عبور حدود الظل و الحصول على مكون الغريبة. ومن ثم ، يُترك المكون الفضائي غير منظم. ومع ذلك ، الآن ، يجب أن تكون تكتيكات حل المشكلة واضحة: يجب علينا بطريقة ما وضع أنماط المكون في Shadow DOM نفسه حيث يوجد المكون الخاص بنا (بدلاً من <head> الخاص بالمستند).

5.4. تحديد أنماط المكون الفضائي

حتى الآن ، كانت عملية الترحيل إلى أي إطار عمل واحدة. ومع ذلك ، تبدأ الأمور في الاختلاف هنا: فلكل إطار عمل توصياته حول كيفية تصميم المكونات ، وبالتالي تختلف طرق معالجة المشكلة. هنا ، نناقش الحالات الأكثر شيوعًا ، ولكن إذا كان إطار العمل الذي تستخدمه يستخدم طريقة فريدة لتصميم مكونات ، فعليك أن تضع في اعتبارك التكتيكات الأساسية مثل وضع أنماط المكون في Shadow DOM بدلاً من <head> .

في هذا الفصل ، نغطي إصلاحات لـ:

  • الأنماط المجمعة مع وحدات CSS في Vue (تكتيكات Scoped CSS هي نفسها) ؛
  • الأنماط المجمعة مع المكونات ذات الأنماط في React ؛
  • وحدات CSS العامة والأنماط العامة. أقوم بدمجها لأن CSS Modules ، بشكل عام ، تشبه إلى حد بعيد أوراق الأنماط العامة ويمكن استيرادها بواسطة أي مكون مما يجعل الأنماط منفصلة عن أي مكون معين.

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

الأنماط المجمعة في Vue And Shadow DOM

إذا كنت تكتب تطبيق Vue ، فأنت على الأرجح تستخدم مكونات ملف واحد. إذا كنت تستخدم Webpack أيضًا ، فيجب أن تكون على دراية بجهازين للتحميل vue-loader و vue-style-loader . يسمح لك الأول بكتابة مكونات الملف الفردي تلك بينما يقوم الأخير بحقن CSS للمكون ديناميكيًا في مستند كعلامة <style> . بشكل افتراضي ، vue-style-loader يضخ أنماط المكوّن في <head> الخاص بالمستند. ومع ذلك ، تقبل كلتا الحزمتين خيار shadowMode في التكوين مما يسمح لنا بسهولة بتغيير السلوك الافتراضي وأنماط الحقن (كما يوحي اسم الخيار) في Shadow DOM. دعونا نراه في العمل.

تكوين حزمة الويب

كحد أدنى ، يجب أن يحتوي ملف تكوين Webpack على ما يلي:

 const VueLoaderPlugin = require('vue-loader/lib/plugin'); ... module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { shadowMode: true } }, { test: /\.css$/, include: path.resolve(__dirname, '../vue'), use: [ { loader:'vue-style-loader', options: { shadowMode: true } }, 'css-loader' ] } ], plugins: [ new VueLoaderPlugin() ] }

في تطبيق حقيقي ، سيكون test: /\.css$/ الكتلة أكثر تعقيدًا (ربما يتضمن قاعدة oneOf ) لحساب تكوينات المضيف والأجنبي. ومع ذلك ، في هذه الحالة ، تم تصميم jQuery باستخدام <link rel="stylesheet"> بسيط في index.html ، لذلك لا نبني أنماطًا للمضيف عبر Webpack ، ومن الآمن تلبية احتياجات Alien فقط.

تكوين الغلاف

بالإضافة إلى تكوين Webpack ، نحتاج أيضًا إلى تحديث غلاف Frankenstein الخاص بنا ، مع توجيه Vue إلى Shadow DOM الصحيح. في Header-wrapper.js ، يجب أن يتضمن عرض مكون Vue الخاصية shadowRoot التي تؤدي إلى shadowRoot Frankenstein الخاص بنا:

 ... new Vue({ shadowRoot: this.shadowRoot, render: h => h(VueHeader) }).$mount(mountPoint); ...

بعد تحديث الملفات وإعادة تشغيل الخادم ، يجب أن تحصل على شيء مثل هذا في DevTools الخاص بك:

تم وضع الأنماط المجمعة مع مكون Alien Vue داخل غلاف Frankenstein مع الاحتفاظ بجميع فئات CSS الفريدة.
تم وضع الأنماط المجمعة مع مكون Alien Vue داخل غلاف Frankenstein مع الاحتفاظ بجميع فئات CSS الفريدة. (معاينة كبيرة)

أخيرًا ، توجد أنماط مكون Vue داخل Shadow DOM. في الوقت نفسه ، يجب أن يبدو طلبك كما يلي:

يبدأ مكون الرأس في الظهور كما ينبغي. ومع ذلك ، لا يزال هناك شيء مفقود.
يبدأ مكون الرأس في الظهور كما ينبغي. ومع ذلك ، لا يزال هناك شيء مفقود. (معاينة كبيرة)

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

الأنماط المجمعة في React and Shadow DOM

نظرًا لوجود العديد من الطرق التي يمكن للمرء من خلالها تصميم مكون React ، فإن الحل الخاص لإصلاح مكون Alien في Frankenstein Migration يعتمد على طريقة تصميم المكون في المقام الأول. دعنا نغطي بإيجاز البدائل الأكثر استخدامًا.

مكونات على غرار

المكونات المصممة هي إحدى الطرق الأكثر شيوعًا لتصميم مكونات React. بالنسبة لمكون Header React ، فإن المكونات المصممة هي بالضبط الطريقة التي نصممها بها. نظرًا لأن هذا نهج CSS-in-JS كلاسيكي ، فلا يوجد ملف بامتداد مخصص يمكننا ربطه بمجمعنا كما نفعل مع ملفات .css أو .js ، على سبيل المثال. لحسن الحظ ، تسمح المكونات المصممة بإدخال أنماط المكون في عقدة مخصصة (Shadow DOM في حالتنا) بدلاً من head المستند بمساعدة مكون المساعدة StyleSheetManager . إنه مكون محدد مسبقًا ، مثبت مع حزمة styled-components الذي يقبل الخاصية target ، ويحدد "عقدة DOM بديلة لإدخال معلومات الأنماط". بالضبط ما نحتاجه! علاوة على ذلك ، لا نحتاج حتى إلى تغيير تكوين Webpack الخاص بنا: كل شيء يعود إلى غلاف Frankenstein الخاص بنا.

يجب علينا تحديث Header-wrapper.js الذي يحتوي على مكون React Alien بالأسطر التالية:

 ... import { StyleSheetManager } from "../../react/node_modules/styled-components"; ... const target = this.shadowRoot; ReactDOM.render( <StyleSheetManager target={target}> <HeaderApp /> </StyleSheetManager>, appWrapper ); ...

هنا ، نستورد مكون StyleSheetManager (من Alien ، وليس من Host) ونلف مكون React به. في الوقت نفسه ، نرسل الخاصية target للإشارة إلى shadowRoot بنا. هذا هو. إذا قمت بإعادة تشغيل الخادم ، فيجب أن ترى شيئًا كهذا في DevTools الخاص بك:

تم وضع الأنماط المجمعة مع مكون React Alien داخل غلاف Frankenstein مع الاحتفاظ بجميع فئات CSS الفريدة.
تم وضع الأنماط المجمعة مع مكون React Alien داخل غلاف Frankenstein مع الاحتفاظ بجميع فئات CSS الفريدة. (معاينة كبيرة)

الآن ، أنماط المكون لدينا موجودة في Shadow DOM بدلاً من <head> . وبهذه الطريقة ، فإن عرض تطبيقنا يشبه الآن ما رأيناه في تطبيق Vue سابقًا.

بعد نقل الأنماط المجمعة إلى غلاف Frankenstein ، يبدأ مكون Alien React في الظهور بشكل أفضل. ومع ذلك ، فإننا لم نصل إلى هناك بعد.
بعد نقل الأنماط المجمعة إلى غلاف Frankenstein ، يبدأ مكون Alien React في الظهور بشكل أفضل. ومع ذلك ، نحن لم نصل إلى هناك بعد. (معاينة كبيرة)

القصة نفسها: المكونات المصممة هي المسؤولة فقط عن الجزء المجمّع من أنماط مكون React ، والأنماط العامة تدير البتات المتبقية. نعود إلى الأنماط العالمية بعد قليل من مراجعة نوع آخر من مكونات التصميم.

وحدات CSS

إذا ألقيت نظرة فاحصة على مكون Vue الذي قمنا بإصلاحه سابقًا ، فقد تلاحظ أن وحدات CSS هي بالضبط الطريقة التي نصمم بها هذا المكون. However, even if we style it with Scoped CSS (another recommended way of styling Vue components) the way we fix our unstyled component doesn't change: it is still up to vue-loader and vue-style-loader to handle it through shadowMode: true option.

When it comes to CSS Modules in React (or any other system using CSS Modules without any dedicated tools), things get a bit more complicated and less flexible, unfortunately.

Let's take a look at the same React component which we've just integrated, but this time styled with CSS Modules instead of styled-components. The main thing to note in this component is a separate import for stylesheet:

 import styles from './Header.module.css'

The .module.css extension is a standard way to tell React applications built with the create-react-app utility that the imported stylesheet is a CSS Module. The stylesheet itself is very basic and does precisely the same our styled-components do.

Integrating CSS modules into a Frankenstein wrapper consists of two parts:

  • Enabling CSS Modules in bundler,
  • Pushing resulting stylesheet into Shadow DOM.

I believe the first point is trivial: all you need to do is set { modules: true } for css-loader in your Webpack configuration. Since, in this particular case, we have a dedicated extension for our CSS Modules ( .module.css ), we can have a dedicated configuration block for it under the general .css configuration:

 { test: /\.css$/, oneOf: [ { test: /\.module\.css$/, use: [ ... { loader: 'css-loader', options: { modules: true, } } ] } ] }

Note : A modules option for css-loader is all we have to know about CSS Modules no matter whether it's React or any other system. When it comes to pushing resulting stylesheet into Shadow DOM, however, CSS Modules are no different from any other global stylesheet.

By now, we went through the ways of integrating bundled styles into Shadow DOM for the following conventional scenarios:

  • Vue components, styled with CSS Modules. Dealing with Scoped CSS in Vue components won't be any different;
  • React components, styled with styled-components;
  • Components styled with raw CSS Modules (without dedicated tools like those in Vue). For these, we have enabled support for CSS modules in Webpack configuration.

However, our components still don't look as they are supposed to because their styles partially come from global styles . Those global styles do not come to our Frankenstein wrappers automatically. Moreover, you might get into a situation in which your Alien components are styled exclusively with global styles without any bundled styles whatsoever. So let's finally fix this side of the story.

Global Styles And Shadow DOM

Having your components styled with global styles is neither wrong nor bad per se: every project has its requirements and limitations. However, the best you can do for your components if they rely on some global styles is to pull those styles into the component itself. This way, you have proper easy-to-maintain self-contained components with bundled styles.

Nevertheless, it's not always possible or reasonable to do so: several components might share some styling, or your whole styling architecture could be built using global stylesheets that are split into the modular structure, and so on.

So having an opportunity to pull in global styles into our Frankenstein wrappers wherever it's required is essential for the success of this type of migration. Before we get to an example, keep in mind that this part is the same for pretty much any framework of your choice — be it React, Vue or anything else using global stylesheets!

Let's get back to our Header component from the Vue application. Take a look at this import:

 import "todomvc-app-css/index.css";

This import is where we pull in the global stylesheet. In this case, we do it from the component itself. It's only one way of using global stylesheet to style your component, but it's not necessarily like this in your application.

Some parent module might add a global stylesheet like in our React application where we import index.css only in index.js , and then our components expect it to be available in the global scope. Your component's styling might even rely on a stylesheet, added with <style> or <link> to your index.html . It doesn't matter. What matters, however, is that you should expect to either import global stylesheets in your Alien component (if it doesn't harm the Alien application) or explicitly in the Frankenstein wrapper. Otherwise, the wrapper would not know that the Alien component needs any stylesheet other than the ones already bundled with it.

Caution . If there are many global stylesheets to be shared between Alien components and you have a lot of such components, this might harm the performance of your Host application under the migration period.

Here is how import of a global stylesheet, required for the Header component, is done in Frankenstein wrapper for React component:

 // we import directly from react/, not from Host import '../../react/node_modules/todomvc-app-css/index.css'

Nevertheless, by importing a stylesheet this way, we still bring the styles to the global scope of our Host, while what we need is to pull in the styles into our Shadow DOM. كيف نفعل ذلك؟

Webpack configuration for global stylesheets & Shadow DOM

First of all, you might want to add an explicit test to make sure that we process only the stylesheets coming from our Alien. In case of our React migration, it will look similar to this:

 test: /\.css$/, oneOf: [ // this matches stylesheets coming from /react/ subfolder { test: /\/react\//, use: [] }, ... ]

In case of Vue application, obviously, you change test: /\/react\// with something like test: /\/vue\// . Apart from that, the configuration will be the same for any framework. Next, let's specify the required loaders for this block.

 ... use: [ { loader: 'style-loader', options: { ... } }, 'css-loader' ]

Two things to note. First, you have to specify modules: true in css-loader 's configuration if you're processing CSS Modules of your Alien application.

Second, we should convert styles into <style> tag before injecting those into Shadow DOM. In the case of Webpack, for that, we use style-loader . The default behavior for this loader is to insert styles into the document's head. Typically. And this is precisely what we don't want: our goal is to get stylesheets into Shadow DOM. However, in the same way we used target property for styled-components in React or shadowMode option for Vue components that allowed us to specify custom insertion point for our <style> tags, regular style-loader provides us with nearly same functionality for any stylesheet: the insert configuration option is exactly what helps us achieve our primary goal. Great news! Let's add it to our configuration.

 ... { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }

However, not everything is so smooth here with a couple of things to keep in mind.

أوراق الأنماط العامة insert خيار style-loader

إذا قمت بالتحقق من الوثائق الخاصة بهذا الخيار ، فلاحظت أن هذا الخيار يأخذ محددًا واحدًا لكل تكوين. هذا يعني أنه إذا كان لديك العديد من مكونات Alien التي تتطلب أنماطًا عامة تم سحبها في غلاف Frankenstein ، فيجب عليك تحديد style-loader لكل من أغلفة Frankenstein. في الممارسة العملية ، هذا يعني أنه ربما يتعين عليك الاعتماد على قاعدة oneOf في كتلة التكوين الخاصة بك لتعمل على جميع الأغلفة.

 { test: /\/react\//, oneOf: [ { test: /1-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '1-frankenstein-wrapper' } }, `css-loader` ] }, { test: /2-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '2-frankenstein-wrapper' } }, `css-loader` ] }, // etc. ], }

ليس مرنًا جدًا ، أوافق. ومع ذلك ، فهي ليست مشكلة كبيرة طالما لم يكن لديك مئات المكونات للترحيل. وإلا ، فقد يؤدي ذلك إلى صعوبة الحفاظ على تكوين Webpack الخاص بك. لكن المشكلة الحقيقية هي أننا لا نستطيع كتابة محدد CSS لـ Shadow DOM.

في محاولة لحل هذه المشكلة ، قد نلاحظ أن خيار insert يمكن أن يأخذ أيضًا وظيفة بدلاً من المحدد العادي لتحديد منطق أكثر تقدمًا للإدراج. باستخدام هذا ، يمكننا استخدام هذا الخيار لإدراج أوراق الأنماط مباشرة في Shadow DOM! في شكل مبسط ، قد يبدو مشابهًا لما يلي:

 insert: function(element) { var parent = document.querySelector('frankenstein-header-wrapper').shadowRoot; parent.insertBefore(element, parent.firstChild); }

مغر ، أليس كذلك؟ ومع ذلك ، لن يعمل هذا مع السيناريو الخاص بنا أو سيعمل بعيدًا عن المستوى الأمثل. إن <frankenstein-header-wrapper> بنا متاح بالفعل من index.html (لأننا أضفناه في الخطوة 5.2). ولكن عندما يعالج Webpack جميع التبعيات (بما في ذلك أوراق الأنماط) لمكون Alien أو غلاف Frankenstein ، لم تتم تهيئة Shadow DOM بعد في غلاف Frankenstein: تتم معالجة عمليات الاستيراد قبل ذلك. ومن ثم ، فإن توجيه insert مباشرة إلى shadowRoot سيؤدي إلى حدوث خطأ.

هناك حالة واحدة فقط يمكننا فيها ضمان تهيئة Shadow DOM قبل أن يعالج Webpack تبعية ورقة الأنماط الخاصة بنا. إذا لم يستورد مكون Alien ورقة أنماط بحد ذاته وأصبح الأمر متروكًا لبرنامج تضمين Frankenstein لاستيراده ، فقد نستخدم الاستيراد الديناميكي واستيراد ورقة الأنماط المطلوبة بعد إعداد Shadow DOM:

 this.attachShadow({ mode: "open" }); import('../vue/node_modules/todomvc-app-css/index.css');

سيعمل هذا: مثل هذا الاستيراد ، جنبًا إلى جنب مع تكوين insert أعلاه ، سيجد بالفعل Shadow DOM الصحيح وإدراج علامة <style> فيه. ومع ذلك ، فإن الحصول على ورقة الأنماط ومعالجتها سيستغرق وقتًا ، مما يعني أن المستخدمين الذين لديهم اتصال بطيء أو أجهزة بطيئة قد يواجهون لحظة من المكون غير المصمم قبل أن تصل ورقة الأنماط الخاصة بك إلى مكانها داخل Shadow DOM الخاص بالغلاف.

يتم عرض مكون Alien غير المصمم قبل استيراد ورقة الأنماط العامة وإضافتها إلى Shadow DOM.
يتم عرض مكون Alien غير المصمم قبل استيراد ورقة الأنماط العامة وإضافتها إلى Shadow DOM. (معاينة كبيرة)

لذلك بشكل عام ، على الرغم من أن insert يقبل الوظيفة ، للأسف ، هذا لا يكفي بالنسبة لنا ، وعلينا الرجوع إلى محددات CSS العادية مثل frankenstein-header-wrapper . هذا لا يضع أوراق الأنماط في Shadow DOM تلقائيًا ، ومع ذلك ، فإن أوراق الأنماط موجودة في <frankenstein-header-wrapper> خارج Shadow DOM.

يضع محمل النمط ورقة أنماط مستوردة في غلاف Frankenstein ، ولكن خارج Shadow DOM.
يضع style-loader ورقة أنماط مستوردة في غلاف Frankenstein ، ولكن خارج Shadow DOM. (معاينة كبيرة)

نحتاج قطعة أخرى من اللغز.

تكوين الغلاف لأوراق الأنماط العامة و Shadow DOM

لحسن الحظ ، يكون الإصلاح واضحًا تمامًا من جانب الغلاف: عندما تتم تهيئة Shadow DOM ، نحتاج إلى التحقق من أي أوراق أنماط معلقة في الغلاف الحالي وسحبها إلى Shadow DOM.

الحالة الحالية لاستيراد ورقة الأنماط العامة هي كما يلي:

  • نقوم باستيراد ورقة أنماط يجب إضافتها إلى Shadow DOM. يمكن استيراد ورقة الأنماط إما في مكون Alien نفسه أو بشكل صريح في غلاف Frankenstein. في حالة الترحيل إلى React ، على سبيل المثال ، تتم تهيئة الاستيراد من الغلاف. ومع ذلك ، في الترحيل إلى Vue ، يستورد المكون المماثل نفسه ورقة الأنماط المطلوبة ، ولا يتعين علينا استيراد أي شيء في الغلاف.
  • كما أشرنا أعلاه ، عندما يعالج Webpack عمليات استيراد .css لمكوِّن Alien ، بفضل خيار insert أداة تحميل style-loader ، يتم حقن أوراق الأنماط في غلاف Frankenstein ، ولكن خارج Shadow DOM.

التهيئة المبسطة لـ Shadow DOM في غلاف Frankenstein ، يجب أن تبدو حاليًا (قبل سحب أي أوراق أنماط) مشابهة لما يلي:

 this.attachShadow({ mode: "open" }); ReactDOM.render(); // or `new Vue()`

الآن ، لتجنب وميض المكون غير المصمم ، ما نحتاج إلى القيام به الآن هو سحب جميع أوراق الأنماط المطلوبة بعد تهيئة Shadow DOM ، ولكن قبل عرض مكون Alien.

 this.attachShadow({ mode: "open" }); Array.prototype.slice .call(this.querySelectorAll("style")) .forEach(style => { this.shadowRoot.prepend(style); }); ReactDOM.render(); // or new Vue({})

لقد كان تفسيرًا طويلاً مع الكثير من التفاصيل ، ولكن بشكل أساسي ، كل ما يتطلبه الأمر لسحب أوراق الأنماط العالمية إلى Shadow DOM:

  • في تكوين Webpack ، أضف style-loader مع خيار insert الذي يشير إلى غلاف Frankenstein المطلوب.
  • في الغلاف نفسه ، اسحب أوراق الأنماط "المعلقة" بعد تهيئة Shadow DOM ، ولكن قبل عرض مكون Alien.

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

  • غلاف فرانكشتاين لمكون React
  • غلاف Frankenstein لمكون Vue

يمكنك أيضًا إلقاء نظرة على تهيئة Webpack في هذه الخطوة من الترحيل:

  • الهجرة إلى التفاعل مع المكونات المصممة
  • الهجرة للتفاعل مع وحدات CSS
  • الهجرة إلى Vue

وأخيرًا ، تبدو مكوناتنا تمامًا كما أردنا أن تبدو.

نتيجة ترحيل مكون الرأس المكتوب باستخدام Vue و React. لا تزال قائمة عناصر المهام هي تطبيق jQuery.
نتيجة ترحيل مكون الرأس المكتوب باستخدام Vue و React. لا تزال قائمة عناصر المهام هي تطبيق jQuery. (معاينة كبيرة)

5.5 ملخص أنماط التثبيت لمكون Alien

هذه لحظة رائعة لتلخيص ما تعلمناه في هذا الفصل حتى الآن. قد يبدو أنه كان علينا القيام بعمل هائل لإصلاح تصميم المكون الفضائي ؛ ومع ذلك ، يتلخص كل ذلك في:

  • يعد إصلاح الأنماط المجمعة المطبقة مع المكونات المصممة في وحدات React أو CSS و Scoped CSS في Vue أمرًا بسيطًا مثل سطرين في غلاف Frankenstein أو تكوين Webpack.
  • تبدأ أنماط التثبيت ، التي يتم تنفيذها باستخدام وحدات CSS النمطية ، بسطر واحد فقط في تكوين css-loader . بعد ذلك ، يتم التعامل مع وحدات CSS على أنها ورقة أنماط عامة.
  • يتطلب إصلاح أوراق الأنماط العامة تكوين حزمة style-loader مع خيار insert في Webpack ، وتحديث غلاف Frankenstein لسحب أوراق الأنماط إلى Shadow DOM في اللحظة المناسبة من دورة حياة الغلاف.

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

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

5.6 أحداث React و JS في Shadow DOM

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

كما رأيت أثناء إصلاح تصميم مكون Alien الخاص بنا ، على عكس Vue حيث تتلاءم الأشياء مع مكونات الويب تقريبًا خارج الصندوق ، فإن React ليست جاهزة لمكونات الويب. في الوقت الحالي ، لدينا فهم لكيفية جعل مكونات React تبدو جيدة على الأقل داخل Web Components ، ولكن هناك أيضًا وظائف وأحداث JavaScript يجب إصلاحها.

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

لحسن الحظ ، أعد الأشخاص الأذكياء حلاً لنا. قدمjosephnvu أساس الحل ، وقام Lukas Bombach بتحويله إلى react-shadow-dom-retarget-events npm. حتى تتمكن من تثبيت الحزمة ، واتباع الإرشادات الموجودة على صفحة الحزم ، وتحديث كود الغلاف الخاص بك وسيبدأ مكون Alien الخاص بك في العمل بطريقة سحرية:

 import retargetEvents from 'react-shadow-dom-retarget-events'; ... ReactDOM.render( ... ); retargetEvents(this.shadowRoot);

إذا كنت ترغب في جعلها أكثر أداءً ، فيمكنك عمل نسخة محلية من الحزمة (يسمح ترخيص MIT بذلك) والحد من عدد الأحداث للاستماع إليها كما هو الحال في مستودع Frankenstein Demo. في هذا المثال ، أعرف الأحداث التي أحتاجها لإعادة استهدافها وتحديدها فقط.

مع هذا ، انتهينا أخيرًا (أعلم أنها كانت عملية طويلة) بالترحيل المناسب لأول مكون أجنبي مصمم وكامل الوظائف. احصل على مشروب جيد. تستحقها!

6. شطف وكرر لجميع المكونات الخاصة بك

بعد أن قمنا بترحيل المكون الأول ، يجب أن نكرر العملية لجميع مكوناتنا. في حالة Frankenstein Demo ، هناك واحد فقط متبقي ، ومع ذلك: الشخص المسؤول عن عرض قائمة عناصر المهام.

أغلفة جديدة للمكونات الجديدة

لنبدأ بإضافة غلاف جديد. باتباع اصطلاح التسمية ، الذي تمت مناقشته أعلاه (نظرًا لأن مكون React يسمى MainSection.js ) ، يجب تسمية الغلاف المقابل في الترحيل إلى React باسم MainSection-wrapper.js . في الوقت نفسه ، يُطلق على مكون مشابه في Vue اسم Listing.vue ، ومن ثم يجب تسمية الغلاف المقابل في الترحيل إلى Vue باسم Listing-wrapper.js . ومع ذلك ، بغض النظر عن اصطلاح التسمية ، فإن الغلاف نفسه سيكون متطابقًا تقريبًا مع الغلاف الذي لدينا بالفعل:

  • غلاف لقائمة التفاعل
  • غلاف لقائمة Vue

يوجد شيء واحد مثير للاهتمام قدمناه في هذا المكون الثاني في تطبيق React. في بعض الأحيان ، لهذا السبب أو لسبب آخر ، قد ترغب في استخدام بعض المكونات الإضافية jQuery في مكوناتك. في حالة مكون React الخاص بنا ، قدمنا ​​شيئين:

  • البرنامج المساعد Tooltip من Bootstrap الذي يستخدم jQuery ،
  • تبديل لفئات CSS مثل .addClass() و .removeClass() .

    ملاحظة : استخدام jQuery لإضافة / حذف الفئات هو توضيحي بحت. من فضلك لا تستخدم jQuery لهذا السيناريو في المشاريع الحقيقية - اعتمد على JavaScript عادي بدلاً من ذلك.

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

ومع ذلك ، تكمن المشكلة في أنه حتى إذا تأكدت من أن هذا المكون يعمل بشكل جيد في سياق تطبيق Alien الخاص بك ، فعند وضعه في Shadow DOM ، لن تعمل مكونات jQuery الإضافية والتعليمات البرمجية الأخرى التي تعتمد على jQuery.

jQuery في Shadow DOM

دعنا نلقي نظرة على التهيئة العامة لملحق jQuery العشوائي:

 $('.my-selector').fancyPlugin();

بهذه الطريقة ، ستتم معالجة جميع العناصر ذات .my-selector بواسطة fancyPlugin . يفترض هذا الشكل من التهيئة أن .my-selector موجود في DOM العام. ومع ذلك ، بمجرد وضع مثل هذا العنصر في Shadow DOM ، تمامًا كما هو الحال مع الأنماط ، تمنع حدود الظل jQuery من التسلل إليه. نتيجة لذلك ، لا يمكن لـ jQuery العثور على عناصر داخل Shadow DOM.

الحل هو توفير معلمة ثانية اختيارية للمحدد الذي يعرّف العنصر الجذر لـ jQuery للبحث منه. وهذا هو المكان الذي يمكننا فيه توفير shadowRoot بنا.

 $('.my-selector', this.shadowRoot).fancyPlugin();

بهذه الطريقة ، ستعمل محددات jQuery ، ونتيجة لذلك ، الإضافات على ما يرام.

ضع في اعتبارك أن الغرض من مكونات Alien هو استخدامها في كل من: Alien بدون ظل DOM ، وفي Host داخل Shadow DOM. ومن ثم فإننا بحاجة إلى حل أكثر توحيدًا لا يفترض وجود Shadow DOM افتراضيًا.

عند تحليل مكون MainSection في تطبيق React الخاص بنا ، وجدنا أنه يحدد خاصية documentRoot .

 ... this.documentRoot = this.props.root? this.props.root: document; ...

لذلك ، نتحقق من خاصية root التي تم تمريرها ، وإذا كانت موجودة ، فهذا ما نستخدمه كجذر documentRoot . وإلا فإننا نعود إلى document .

فيما يلي تهيئة المكون الإضافي تلميح الأدوات الذي يستخدم هذه الخاصية:

 $('[data-toggle="tooltip"]', this.documentRoot).tooltip({ container: this.props.root || 'body' });

كمكافأة ، نستخدم نفس خاصية root لتحديد حاوية لحقن تلميح الأداة في هذه الحالة.

الآن ، عندما يكون المكون Alien جاهزًا لقبول خاصية root ، نقوم بتحديث عرض المكون في غلاف Frankenstein المقابل:

 // `appWrapper` is the root element within wrapper's Shadow DOM. ReactDOM.render(<MainApp root={ appWrapper } />, appWrapper);

وهذا كل شيء! المكون يعمل بشكل جيد في Shadow DOM كما هو الحال في DOM العام.

تكوين Webpack لسيناريو الأغلفة المتعددة

يحدث الجزء المثير في تكوين Webpack عند استخدام عدة أغلفة. لا شيء يتغير للأنماط المجمعة مثل وحدات CSS في مكونات Vue ، أو المكونات المصممة في React. ومع ذلك ، يجب أن تحصل الأنماط العالمية على القليل من التغيير الآن.

تذكر ، قلنا أن style-loader (المسؤول عن حقن أوراق الأنماط العامة في Shadow DOM الصحيح) غير مرن لأنه لا يتطلب سوى محددًا واحدًا في كل مرة لخيار insert الخاص به. هذا يعني أنه يجب علينا تقسيم قاعدة .css في Webpack للحصول على قاعدة فرعية واحدة لكل غلاف باستخدام قاعدة oneOf أو قاعدة مشابهة ، إذا كنت تستخدم مجمعًا بخلاف Webpack.

من الأسهل دائمًا شرح ذلك باستخدام مثال ، لذلك دعونا نتحدث عن المثال من الترحيل إلى Vue هذه المرة (ومع ذلك ، فإن المثال الموجود في الترحيل إلى React متطابق تقريبًا):

 ... oneOf: [ { issuer: /Header/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }, ... ] }, { issuer: /Listing/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-listing-wrapper' } }, ... ] }, ] ...

لقد استبعدت css-loader لأن تكوينه هو نفسه في جميع الحالات. دعنا نتحدث عن style-loader ذلك. في هذا التكوين ، نقوم بإدراج علامة <style> إما في *-header-* أو *-listing-* ، اعتمادًا على اسم الملف الذي يطلب ورقة الأنماط هذه (قاعدة issuer في Webpack). لكن علينا أن نتذكر أن ورقة الأنماط العامة المطلوبة لعرض مكون Alien قد يتم استيرادها في مكانين:

  • المكون الأجنبي نفسه ،
  • غلاف فرانكشتاين.

وهنا ، يجب أن نقدر اصطلاح التسمية للأغلفة ، الموصوف أعلاه ، عندما يتطابق اسم مكون Alien مع غلاف مطابق. إذا كان لدينا ، على سبيل المثال ، ورقة أنماط ، تم استيرادها في مكون Vue يسمى Header.vue ، فسيتم تصحيح *-header-* المجمع. في الوقت نفسه ، إذا قمنا ، بدلاً من ذلك ، باستيراد ورقة الأنماط في الغلاف ، فإن ورقة الأنماط هذه تتبع بالضبط نفس القاعدة إذا كان الغلاف يسمى Header-wrapper.js دون أي تغييرات في التكوين. نفس الشيء بالنسبة لمكوِّن Listing.vue وما Listing-wrapper.js . باستخدام اصطلاح التسمية هذا ، نقوم بتقليل التكوين في الحزمة الخاصة بنا.

بعد ترحيل جميع المكونات الخاصة بك ، حان الوقت للخطوة الأخيرة من الترحيل.

7. التبديل إلى أجنبي

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

على سبيل المثال ، يبدو جزء المحتوى من index.html في تطبيق jQuery - بعد ترحيل كلتا الخدمتين المصغرتين - على النحو التالي:

 <section class="todoapp"> <frankenstein-header-wrapper></frankenstein-header-wrapper> <frankenstein-listing-wrapper></frankenstein-listing-wrapper> </section>

في هذه اللحظة ، لا فائدة من الاحتفاظ بتطبيق jQuery: بدلاً من ذلك ، يجب أن ننتقل إلى تطبيق Vue وننسى كل أغلفةنا ، Shadow DOM وتكوينات Webpack الفاخرة. للقيام بذلك ، لدينا حل أنيق.

لنتحدث عن طلبات HTTP. سأذكر تكوين Apache هنا ، ولكن هذا مجرد تفصيل تنفيذ: إجراء التبديل في Nginx أو أي شيء آخر يجب أن يكون تافهًا كما هو الحال في Apache.

تخيل أن موقعك قد تم عرضه من المجلد /var/www/html على الخادم الخاص بك. في هذه الحالة ، يجب أن يحتوي httpd.conf أو httpd-vhost.conf على إدخال يشير إلى هذا المجلد مثل:

 DocumentRoot "/var/www/html"

لتبديل تطبيقك بعد ترحيل Frankenstein من jQuery إلى React ، كل ما عليك فعله هو تحديث إدخال DocumentRoot إلى شيء مثل:

 DocumentRoot "/var/www/html/react/build"

قم ببناء تطبيق Alien الخاص بك ، وأعد تشغيل الخادم الخاص بك ، وسيتم تقديم تطبيقك مباشرة من مجلد Alien: تطبيق React الذي يتم تقديمه من مجلد react/ المجلد. ومع ذلك ، ينطبق الشيء نفسه على Vue ، بالطبع ، أو أي إطار عمل آخر قمت بترحيله أيضًا. هذا هو السبب في أنه من الأهمية بمكان الحفاظ على Host and Alien مستقلين تمامًا وعاملين في أي وقت لأن الكائن الفضائي الخاص بك يصبح مضيفك في هذه الخطوة.

يمكنك الآن إزالة كل شيء حول مجلد Alien الخاص بك بأمان ، بما في ذلك جميع Shadow DOM وأغلفة Frankenstein وأي عنصر آخر متعلق بالترحيل. لقد كان مسارًا صعبًا في لحظات ، لكنك قمت بترحيل موقعك. تهانينا!

خاتمة

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

بعد التحول إلى Alien ، يمكن أن يتقاعد Frankenstein.
بعد التحول إلى Alien ، يمكن أن يتقاعد Frankenstein. (معاينة كبيرة)

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