آليات القابلية للتوسعة في JavaScript

مرحبا الزملاء!

نذكرك أننا منذ وقت ليس ببعيد قد نشرنا الطبعة الثالثة من الكتاب الأسطوري "Expressive JavaScript " (Eloquent JavaScript) - طُبع باللغة الروسية لأول مرة ، على الرغم من وجود ترجمات عالية الجودة للإصدارات السابقة على الإنترنت.



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


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

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


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

المدودية قابلى المد

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

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

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

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

في صيغة أكثر عمومية ، يجب عليك التأكد من أنه يمكن دمج الإضافات ، حتى لو كانت "غير مدركة تمامًا" لبعضها البعض - وحتى لا تنشأ أي تعارضات أثناء تفاعلاتها.

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

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


في كثير من هذه الحالات ، يكون النظام مهمًا. أعني هنا الامتثال للترتيب الذي يتم به تطبيق التأثيرات ، يجب التحكم في هذا التسلسل وقابلية التنبؤ به.

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

نهج بسيط

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

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

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

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


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

تنظيم

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

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

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

التجميع وإلغاء البيانات المكررة

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

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

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

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

مشروع

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

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

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

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

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

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

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

ومع ذلك ، يبقى التعامل مع إلغاء البيانات المكررة وتوفير تحكم أفضل في الطلب.

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

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

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

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

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

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

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

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

Source: https://habr.com/ru/post/ar468219/


All Articles