10 أسطر من التعليمات البرمجية لتقليل ألم مشروع Vue الخاص بك

... أو الإلمام بمكونات Vue JS الإضافية كمثال لناقل الأحداث المتكامل


بضع كلمات حول ...


تحية للجميع! سأحجز على الفور. أنا حقًا أحب VueJS ، لقد كنت أكتب عليه بنشاط لأكثر من عامين ولا أعتقد أن تطويره يمكن أن يضر على الأقل إلى حد ما :)
من ناحية أخرى ، نحاول دائمًا إيجاد حلول عالمية تساعد على قضاء وقت أقل في العمل الميكانيكي والمزيد على ما هو مثير حقًا. في بعض الأحيان يكون الحل ناجحًا بشكل خاص. أريد أن أشارككم واحدة من هذه. تم إنشاء الخطوط العشرة التي سيتم مناقشتها (المفسد: في النهاية سيكون هناك المزيد) في عملية العمل على مشروع Cloud Blue - Connect ، وهو تطبيق كبير إلى حد ما مع أكثر من 400 مكون. تم دمج الحل الذي وجدناه بالفعل في نقاط مختلفة من النظام ولم يتطلب أكثر من نصف عام أي تصحيحات على الإطلاق ، لذلك يمكن اعتباره اختبارًا آمنًا بنجاح للاستقرار.

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

القليل عن كيفية تواصل مكونات Vue مع بعضها البعض


ربما يكون السؤال الأول الذي يطرحه الشخص الذي كتب أول مكون له هو كيف سيتلقى بيانات عن العمل وكيف ، بدوره ، سينقل البيانات التي تلقاها "بالخارج". يسمى مبدأ التفاعل المعتمد في إطار عمل Vue JS ...

دفق بيانات أحادي الاتجاه


باختصار ، يبدو هذا المبدأ مثل "الخصائص - أسفل ، الأحداث - لأعلى". أي ، لتلقي البيانات من الخارج ("من فوق") ، نقوم بتسجيل خاصية خاصة داخل المكون الذي يكتب فيه الإطار ، إذا لزم الأمر ، بياناتنا الواردة "من الخارج". من أجل نقل البيانات "لأعلى" ، داخل المكون في المكان الصحيح ، فإننا نطلق على طريقة إطار عمل $ emit الخاصة ، والتي تمرر بياناتنا إلى معالج المكون الرئيسي. في الوقت نفسه ، في Vue JS لا يمكننا فقط "بث" الحدث حتى عمق غير محدود (على سبيل المثال في Angular 1.x). انها "للملوثات العضوية الثابتة" مستوى واحد فقط ، إلى الوالد المباشر. الأمر نفسه ينطبق على الأحداث. لنقلهم إلى المستوى التالي ، لكل منهم ، تحتاج أيضًا إلى تسجيل واجهة خاصة - الخصائص والأحداث التي ستنقل "رسالتنا" بشكل أكبر.

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

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

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

فويكس (الجانب)


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

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

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

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

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

حافلة الحدث


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

استعارة ، في "بنايتنا" ، الحافلة هي محادثة شائعة في برنامج المراسلة. تشترك المكونات في "محادثة عامة" ترسل إليها مكونات أخرى رسائل. بمجرد ظهور "رسالة" في "الدردشة" التي اشترك فيها المكون ، سيبدأ المعالج.

هناك العديد من الطرق المختلفة لإنشاء ناقل حدث. يمكنك كتابتها بنفسك أو يمكنك استخدام حلول جاهزة - نفس RxJS ، والتي توفر وظائف ضخمة للعمل مع تدفقات كاملة من الأحداث. ولكن في أغلب الأحيان عند العمل مع VueJS يستخدمون ، الغريب ، VueJS نفسه. يوفر مثيل Vue الذي تم إنشاؤه من خلال المنشئ (new Vue ()) واجهة حدث جميلة وموجزة ، موضحة في الوثائق الرسمية.

هنا نقترب من السؤال التالي ...

ماذا نريد؟


ونريد بناء حافلة حدث في تطبيقنا. ولكن لدينا متطلبان إضافيان:

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

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

دعونا نسجل البرنامج المساعد أولا. للقيام بذلك ، مباشرة قبل نقطة التهيئة لتطبيق Vue (قبل استدعاء Vue. $ Mount ()) نضع الكتلة التالية:

Vue.use({   
  install(vue) { }, 
});

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

Vue.use({
  install(vue) {     
    vue.mixin({}); // <--
  }, 
});

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

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null; // <--
    vue.mixin({}); 
  }, 
});

نحتاج الآن إلى إنشاء الناقل نفسه ، ولكن أولاً ، دعنا نتذكر متطلبات التركيب ونفترض أنه في واصف المكون ، سنعلن عن الوحدة الجديدة مع الحقل "$ module" مع قيمة نصية (سنحتاج إليها بعد ذلك قليلاً). إذا تم تحديد حقل الوحدة النمطية $ في المكون نفسه ، فسننشئ ناقلًا جديدًا له ؛ وإذا لم يكن كذلك ، فسنمرر الرابط إلى الأصل عبر حقل $ الأصل. لاحظ أن حقول الواصف ستكون متاحة لنا من خلال حقل خيارات $.

سنضع إنشاء حافلتنا في أقرب مرحلة ممكنة - في خطاف beforeCreate.

Vue.use({
  install(vue) { 
    vue.prototype.$broadcast = null; 
    vue.mixin({
      beforeCreate() {  // <--
        if (this.$options.$module) {  // <--
         
 	} else if (this.$parent && this.$parent.$broadcast) {  // <--
         
        } 
      }, 
    }); 
  }, 
});

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

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null; 
    vue.mixin({
      beforeCreate() { 
        if (this.$options.$module) {
          this.$broadcast = new Vue();  // <--
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$broadcast = this.$parent.$broadcast;  // <--
        } 
      }, 
    }); 
  }, 
});

نتجاهل إعلان المكوّن الإضافي ، ونعتبر ... 1 ، 2 ، 3 ، 4 ... 10 خطوط ، كما وعدت!

هل يمكننا فعلها بشكل أفضل؟


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

// This one emits event  
this.$broadcast.$emit(‘my-event’, ‘PARAM_A’); 
// This is standard subscription inside module 
this.$broadcast.$on(‘my-event’, (paramA) => {…}); 
// This subscription will work for the same event 
this.$rootBroadcast.$on(‘my-event’, (module, paramA) => {…}); 
// This subscription will also work for the same event 
this.$rootBroadcast.$on(‘*’, (event, module, paramA) => {…});

دعونا نرى كيف يمكننا تحقيق ذلك:

أولاً ، قم بإنشاء حافلة واحدة ، والتي سيتم تنظيمها من خلال $ rootBroadcast ، والحقل نفسه برابط:

const $rootBus = new Vue(); // <--

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus; // <--
        if (this.$options.$module) {
          this.$broadcast = new Vue(); 
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$broadcast = this.$parent.$broadcast; 
        } 
      }, 
    }); 
  }, 
});

نحتاج الآن إلى عضوية الوحدة في كل مكون ، لذا دعنا نوسع تعريف الوحدات النمطية على النحو التالي:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;  // <--
          this.$broadcast = new Vue(); 
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;  // <--
          this.$broadcast = this.$parent.$broadcast; 
        } 
      }, 
    }); 
  }, 
});

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

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  // <--
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };  // <--
        } 
      }, 
    }); 
  }, 
});

وأخيرًا ، أضف أساليب الوكيل إلى الكائن - لأن حقل البث $ الآن لا يوفر الوصول المباشر إلى الناقل:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };
        } 
        // >>>
        this.$broadcast.$emit = (…attrs) => {
          this.$broadcast.$bus.$emit(…attrs);           
          const [event, …attributes] = attrs; 
          this.$rootBroadcast.$emit(event, this.$module, …attributes)); 
          this.$rootBroadcast.$emit(‘*’, event, this.$module, …attributes)
        };
        
        this.$broadcast.$on = (…attrs) => {           
          this.$broadcast.$bus.$on(…attrs);
        };
        // <<<
      }, 
    }); 
  }, 
});

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

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeDestroy() {                               // <--
        this.$broadcast.$off(this.$broadcastEvents);  // <--
      },

      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        this.$broadcastEvents = [];  // <--
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };
        } 

        this.$broadcast.$emit = (…attrs) => {
          this.$broadcastEvents.push(attrs[0]);   // <--
          this.$broadcast.$bus.$emit(…attrs);           
          const [event, …attributes] = attrs; 
          this.$rootBroadcast.$emit(event, this.$module, …attributes)); 
          this.$rootBroadcast.$emit(‘*’, event, this.$module, …attributes)
        };
        
        this.$broadcast.$on = (…attrs) => {           
          this.$broadcast.$bus.$on(…attrs);
        };

        this.$broadcast.$off =: (...attrs) => {  // <--
          this.$broadcast.$bus.$off(...attrs);   // <--
        };
      }, 
    }); 
  }, 
});

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

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

All Articles