اذهب إلى المحتوى

Ola Abbas

الأعضاء
  • المساهمات

    189
  • تاريخ الانضمام

  • تاريخ آخر زيارة

كل منشورات العضو Ola Abbas

  1. نَمت قوة الحوسبة بوتيرة سريعة دون ظهور أيّ علامات على التباطؤ كما توقّع قانون مور Moore، فليس مألوفًا أن تحتوي أيّ خوادم عالية الجودة على وحدة معالجة مركزية واحدة فقط مع إمكانية تحقيق ذلك باستخدام عدد من الأساليب المختلفة. المعالجة المتعددة المتماثلة Symmetric Multi-Processing تُعَدّ المعالجة المتعددة المتماثلة Symmetric Multi-Processing -أو SMP اختصارًا- الإعداد الأكثر شيوعًا حاليًا لتضمين وحدات المعالجة المركزية CPU المتعددة في نظام واحد، ويشير المصطلح متماثل Symmetric إلى حقيقة أنّ جميع وحدات المعالجة المركزية في النظام هي نفسها من حيث المعمارية وسرعة الساعة مثلًا، كما توجد في نظام SMP معالجات متعددة تشترك في جميع موارد النظام الأخرى مثل الذاكرة والقرص الصلب وغير ذلك. ترابط الذواكر المخبئية Cache Coherency تعمل وحدات المعالجة المركزية في النظام بصورة مستقلة عن بعضها بعضًا، فلكل منها مجموعته الخاصة من المسجلات وعدّاد البرنامج وغير ذلك، ولكن يوجد مكوِّن واحد يتطلب تزامنًا صارمًا بالرغم من تشغيل وحدات المعالجة المركزية بصورة منفصلة عن بعضها بعضًا، وهذا المكوِّن هو الذاكرة المخبئية Cache الخاصة بوحدة المعالجة المركزية. تذكّر أنّ الذاكرة المخبئية هي مساحة صغيرة من الذاكرة يمكن الوصول إليها بسرعة وتعكس القيم المخزّنة في ذاكرة النظام الرئيسية، فإذا عدّلت إحدى وحدات المعالجة المركزية البيانات في الذاكرة الرئيسية وكان لدى وحدة معالجة مركزية أخرى نسخة قديمة من تلك الذاكرة في ذاكرتها المخبئية، فلن يكون النظام في حالة متناسقة، ولاحظ أنّ هذه المشكلة تحدث عندما تكتب المعالجات في الذاكرة فقط، إذ ستكون البيانات متناسقةً إذا كانت القيمة للقراءة فقط. يستخدِم نظام SMP عملية التنصت Snooping لتنسيق الحفاظ على ترابط الذواكر المخبئية على جميع المعالجات، إذ يُعَدّ التنصت العمليةَ التي يستمع فيها المعالج إلى ناقل تتصل به جميع المعالجات لمعرفة أحداث الذاكرة المخبئية، ثم يحدّث الذاكرة المخبئية وفقًا لذلك. يمكن تحقيق ذلك باستخدام بروتوكول واحد هو بروتوكول MOESI الذي يرمز إلى الكلمات مُعدَّل Modified ومالك Owner وحصري Exclusive ومشارَك Shared وغير صالح Invalid التي تمثّل الحالة التي يمكن أن يكون فيها خط الذاكرة المخبئية على معالج في النظام، كما توجد بروتوكولات أخرى لذلك، ولكن تشترك جميعها في مفاهيم متشابهة، وسنوضح فيما يلي بروتوكول MOESI. إذا طلب المعالج قراءة خط ذاكرة مخبئية من الذاكرة الرئيسية، فيجب عليه أولًا التنصت على جميع المعالجات الأخرى في النظام لمعرفة ما إذا كانت تعرف حاليًا أيّ شيء عن تلك المنطقة من الذاكرة مثل تخزينها في الذاكرة المخبئية، فإذا لم تكن موجودةً في أيّ عملية أخرى، فيمكن للمعالج تحميل الذاكرة في الذاكرة المخبئية وتمييزها على أنها حصرية Exclusive، ويغير الحالة إلى معدَّلة Modified عند الكتابة في الذاكرة المخبئية. تلعب هنا تفاصيل الذاكرة المخبئية دورًا أساسيًا، إذ ستعيد بعض الذواكر المخبئية مباشرةً كتابة الذاكرة المخبئية المعدَّلة إلى ذاكرة النظام المعروفة باسم الذاكرة المخبئية من النوع Write-through، لأن عمليات الكتابة تنتقل إلى الذاكرة الرئيسية، في حين لن تفعل ذلك الذواكر المخبئية الأخرى، بل ستترك القيمة المُعدَّلة في الذاكرة المخبئية فقط حتى التخلص منها عندما تمتلئ الذاكرة المخبئية مثلًا. الحالة الأخرى هي المكان الذي يطبّق فيه المعالج عملية التنصت ويكتشف أن القيمة موجودة في ذاكرة مخبئية خاصة بمعالجات أخرى، فإذا كانت هذه القيمة مميَّزةً بوصفها مُعدَّلةً Modified، فسينسخ المعالج البيانات في ذاكرته المخبئية ويميّزها على أنها مشتركة Shared، كما سيرسل رسالةً إلى المعالج الآخر الذي حصلنا على البيانات منه لتمييز خط ذاكرته المخبئية بوصفه المالك Owner، لنفترض الآن أن معالجًا ثالثًا في النظام يريد استخدام تلك الذاكرة أيضًا، فسيتنصت ويبحث عن نسخة مشتركة ونسخة مالكة، وبالتالي سيأخذ قيمته من قيمة المالك. تقرأ جميع المعالجات الأخرى القيمة فقط، ولكن يبقى خط الذاكرة المخبئية مشتركًا في النظام، فإذا احتاج معالج ما تحديث القيمة، فإنه يرسل رسالة إلغاء صلاحية Invalidate عبر النظام، كما يجب على أيّ معالج له هذا الخط الخاص بالذاكرة المخبئية تمييزه بوصفه غير صالح Invalid لأنه لم يَعُد يعكس القيمة الحقيقية، ويميز المعالج خط الذاكرة المخبئية بوصفه معدَّلًا في ذاكرته المخبئية عندما يرسل رسالة إلغاء الصلاحية وستميّزه المعالجات الأخرى على أنه غير صالح. لاحظ أنه إذا كان خط الذاكرة المخبئية حصريًا، فسيعلم المعالج أنه لا يوجد معالج آخر يعتمد عليه، لذا يمكنه تجنّب إرسال رسالة إلغاء صلاحية، وتبدأ بعدها العملية من جديد، وبالتالي يتحمل أيُّ معالج له القيمة المعدّلة مسؤوليةَ كتابة القيمة الحقيقية مرةً أخرى إلى الذاكرة RAM عند التخلص منها من الذاكرة المخبئية، ولاحظ أنّ هذا البروتوكول يضمن تناسق خط الذاكرة المخبئية بين المعالجات. هناك العديد من المشاكل في هذا النظام عند زيادة عدد المعالجات، إذ يمكن التحكم في تكلفة التحقق من وجود معالج آخر يحتوي على خط ذاكرة مخبئية (التنصت على عملية القراءة) أو إلغاء صلاحية البيانات في كل معالج آخر (إلغاء عملية التنصت) عند استخدام عدد قليل من المعالجات، ولكن تزداد حركة النواقل مع زيادة عدد المعالجات وهذا هو السبب في أن أنظمة SMP تصل إلى حوالي 8 معالجات فقط. يعطي وجود جميع المعالجات في الناقل نفسه مشاكل فيزيائية أيضًا، إذ تسمح خصائص الأسلاك الفيزيائية بوضعها على مسافات معينة من بعضها بعضًا وتسمح بأن يكون لها أطوال معينة فقط، وتبدأ سرعة الضوء في أن تصبح أحد الجوانب التي يجب مراعاتها في المدة التي تستغرقها الرسائل للتنقل في النظام مع المعالجات التي تعمل بسرعة مقدَّرةً بالجيجاهيرتز. لاحظ أنّ برمجيات النظام ليس لها أيّ جزء من هذه العملية، بالرغم من أنّ المبرمجين يجب أن يكونوا على دراية بما يطبّقه العتاد استجابةً للبرمجيات التي يصمّمونها لزيادة الأداء إلى الحد الأقصى. حصرية الذاكرة المخبئية في أنظمة SMP شرحنا في مقال سابق الذواكر المخبئية الشاملة Inclusive والحصرية Exclusive، إذ تكون الذواكر المخبئية L1 شاملةً، أي أن جميع البيانات الموجودة في الذاكرة المخبئية L1 موجودة في الذاكرة المخبئية L2، وتعني الذاكرة المخبئية L1 الشاملة أنّ الذاكرة المخبئية L2 يجب أن تتنصت حركة مرور الذاكرة للحفاظ على ترابطها في نظام متعدد المعالجات، إذ ستضمن L1 عكس أيّ تغييرات في الذاكرة L2، مما يقلل من تعقيد ذاكرة L1 ويفصله عن عملية التنصت، وبالتالي سيسمح لها بأن تكون أسرع. تحتوي معظم المعالجات الحديثة المتطورة مثل المعالجات التي ليست مدمَجة على سياسة كتابة الذاكرة المخبئية L1 من النوع Write-through وسياسة الكتابة من النوع Write-back في الذواكر المخبئية ذات المستوى الأدنى، وهناك عدة أسباب لذلك، فبما أنّ ذواكر L2 المخبئية في هذا الصنف من المعالجات تكون حصريةً تقريبًا على الشريحة وسريعةً جدًا عمومًا، فليست العقوبات المفروضة على كتابة الذاكرة المخبئية L1 من النوع Write-through الأمر الرئيسي، كما يمكن أن تتسبّب مجمّعات البيانات المكتوبة التي لا يُحتمَل قراءتها في المستقبل في تلوث مورد L1 المحدود لأن أحجام L1 صغيرة. ليس هناك داع للقلق بشأن الكتابة في L1 من النوع Write-through إذا احتوت على بيانات متسخة معلَّقة، وبالتالي يمكن أن تمرّر منطق الترابط الإضافي إلى ذاكرة L2 التي لديها دور أكبر تلعبه في ترابط الذاكرة المخبئية. تقنية خيوط المعالجة الفائقة Hyperthreading يمكن أن يقضي المعالج الحديث كثيرًا من وقته في انتظار أجهزة أبطأ بكثير في تسلسل الذواكر الهرمي لتقديم البيانات للمعالجة، وبالتالي فإن إستراتيجيات الحفاظ على خط أنابيب المعالج ممتلئًا لها أهمية قصوى، وتتمثل إحدى الإستراتيجيات في تضمين عدد كاف من المسجلات ومنطق الحالة بحيث يمكن معالجة مجريين من التعليمات في الوقت نفسه، مما يجعل وحدة معالجة مركزية واحدة تبحث عن جميع النوايا والأهداف المطلوبة كأنها وحدتان CPU. تحتوي كل وحدة معالجة مركزية على مسجلاتها الخاصة، ولكن يجب عليها مشاركة منطق المعالج الأساسي والذاكرة المخبئية وحيز النطاق التراسلي للإدخال والإخراج من وحدة المعالجة المركزية إلى الذاكرة، لذا يمكن أن يحافظ مجريان من التعليمات على المنطق الأساسي للمعالج أكثر انشغالًا، ولكن لن تكون زيادة الأداء كبيرةً بسبب وجود وحدتَي CPU منفصلتين فيزيائيًا، ويكون تحسين الأداء أقل من 20%، ولكن يمكن أن يكون أفضل أو أسوأ كثيرًا اعتمادًا على الحِمل. الأنوية المتعددة Multi Core أصبح وضع معالِجَين أو أكثر في الحزمة الفيزيائية نفسها ممكنًا مع زيادة القدرة على احتواء مزيد من الترانزستورات على شريحة واحدة، ولكن المعالجات الأكثر شيوعًا هي المعالجات ثنائية النواة، إذ توجد نواتان للمعالج على الشريحة نفسها، وتُعَدّ هذه الأنوية -على عكس تقنية خيوط المعالجة الفائقة Hyperthreading- معالجات كاملةً، وبالتالي تبدو على أنها معالجات منفصلة فيزيائيًا مثل نظام SMP. تحتوي المعالجات على ذاكرة L1 المخبئية الخاصة بها، ولكن يجب عليها مشاركة الناقل المتصل بالذاكرة الرئيسية وأجهزة أخرى، وبالتالي لن يكون الأداء جيدًا مثل نظام SMP الكامل، ولكنه أفضل بكثير من نظام خيوط المعالجة الفائقة، ويمكن لكل نواة تطبيق تقنية خيوط المعالجة الفائقة لتحسين إضافي. تتمتع المعالجات متعددة الأنوية ببعض المزايا التي لا تتعلق بالأداء، كما أنّ للناقلات الفيزيائية الخارجية بين المعالجات حدود فيزيائية، ولكن يمكن حل بعض هذه المشاكل من خلال احتواء المعالجات على قطعة السيليكون نفسها بحيث تكون قريبةً جدًا من بعضها بعضًا. تُعَدّ متطلبات الطاقة للمعالجات متعددة الأنوية أقل بكثير من المعالجات المنفصلة عن بعضها بعضًا، وهذا يعني أن هناك حاجة أقل لتبريد الحرارة والتي يمكن أن تكون ميزةً كبيرةً في تطبيقات مراكز البيانات حيث تُجمَّع الحواسيب مع وجود حاجة كبيرة للتبريد، كما يجعل وجود الأنوية في الحزمة الفيزيائية نفسها المعالجةَ المتعددة عمليةً في التطبيقات التي لن تكون فيها كذلك مثل الحواسيب المحمولة، كما يُعَدّ إنتاج شريحة واحدة بدلًا من شريحتين أرخص بكثير. العناقيد Clusters تتطلب العديد من التطبيقات أنظمةً أكبر بكثير من عدد المعالجات التي يمكن لنظام SMP التوسع إليها، وتُعَدّ العناقيد Clusters إحدى الطرق لتوسيع النظام أكثر، وهي عدد من الحواسيب التي لديها بعض القدرة على التواصل مع بعضها بعضًا، كما لا تعرف الأنظمة بعضها بعضًا على مستوى العتاد، إذ تُترَك مهمة ربط هذه الحواسيب للبرمجيات. تسمح البرمجيات مثل MPI للمبرمجين بكتابة برامجهم ثم وضع أجزاء منها على حواسيب أخرى في النظام مثل تمثيل حلقة تُنفَّذ عدة آلاف من المرات وتطبّق إجراءً مستقلًا، أي لا يوجد تكرار للحلقة يؤثر على أيّ تكرار آخر، ويمكن للبرمجيات جعل كل حاسوب يشغّل 250 حلقة لكل منها مع وجود أربعة حواسيب في العنقود. يختلف الترابط بين الحواسيب، إذ يمكن أن يكون بطيئًا مثل روابط شبكة الإنترنت أو سريعًا مثل الناقلات المخصَّصة والخاصة مثل روابط إنفيني باند Infiniband، ومهما كان هذا الترابط، فسيبقى في المستوى الأخفض من تسلسل الذواكر الهرمي وسيكون أبطأ بكثير من الذاكرة RAM، وبالتالي لن يقدّم العنقود أداءً جيدًا في الموقف الذي تتطلب فيه كل وحدة معالجة مركزية الوصول إلى البيانات المُخزَّنة في الذاكرة RAM الخاصة بحاسوب آخر، إذ ستحتاج البرمجيات في كل مرة أن تطلب نسخةً من البيانات من الحاسوب الآخر، وتنسخها عبر الرابط البطيء إلى الذاكرة RAM المحلية قبل أن يتمكن المعالج من إنجاز أيّ عمل. لا تتطلب العديد من التطبيقات هذا النسخ المستمر بين الحواسيب، وأحد الأمثلة الشائعة عن ذلك هو SETI@Home، إذ تُحلَّل البيانات التي جرى جمعها من هوائي راديو بحثًا عن علامات على وجود كائن فضائي، ويمكن توزيع كل حاسوب لبضع دقائق للحصول على البيانات لتحليلها ويعطي تقريرًا ملخصًا لما وجده، إذ يُعَدّ SETI@Home عنقودًا مخصَّصًا وكبيرًا جدًا. يوجد تطبيق آخر هو تطبيق تصيير الصور Rendering of Images الذي يُستخدَم خاصةً للتأثيرات الخاصة في الأفلام، إذ يُسلَّم كل حاسوب إطارًا واحدًا من الفيلم يحتوي على نماذج إطارات شبكية وخامات Textures ومصادر إضاءة يجب دمجها أو تصييرها في التأثيرات الخاصة المذهلة التي نحصل عليها، كما يُعَدّ كل إطار ساكنًا، لذلك لا يحتاج الحاسوب بمجرد حصوله على الدخل الأولي لمزيد من الاتصال حتى يصبح الإطار النهائي جاهزًا لإرساله ودمجه في الحركة، فقد كان لفيلم سيد الخواتم مثلًا تأثيرات خاصة مصيَّرة على عنقود ضخم يعمل بنظام لينكس. الوصول غير الموحد للذاكرة Non-Uniform Memory Access يُعَدّ الوصول غير الموحد للذاكرة Non-Uniform Memory Access -أو NUMA اختصارًا- عكس نظام العناقيد السابق تقريبًا، ولكنه -كما هو الحال في نظام العنقود- يتكون من عقد فردية مرتبطة ببعضها بعضًا، إلا أنّ الارتباط بين العقد شديد التخصص ومكلف، ولا يمتلك العتاد أيّ معرفة بالربط بين العقد في نظام العنقود، في حين لا تمتلك البرمجيات في نظام NUMA معرفةً جيدةً أو تمتلك معرقةً أقل حول تخطيط النظام، إذ يطبّق العتاد كل العمل لربط العقد مع بعضها بعضًا. يأتي مصطلح الوصول غير الموحّد إلى الذاكرة من حقيقة أن الذاكرة RAM ليست محلية بالنسبة لوحدة المعالجة المركزية، وبالتالي يمكن أن هناك حاجة لأن تصل عقدة على بعد مسافة ما إلى البيانات، إذ يستغرق ذلك وقتًا أطول على النقيض من معالج واحد أو نظام SMP حيث يمكن الوصول إلى الذاكرة RAM مباشرةً، ويستغرق ذلك دائمًا وقتًا ثابتًا أو موحّدًا. تخطيط نظام NUMA يُعَدّ تقليل المسافة بين العقد أمرًا بالغ الأهمية مع وجود العديد من العقد التي تتواصل مع بعضها في النظام، إذ يُفضَّل أن يكون لكل عقدة رابط مباشر بكل عقدة أخرى لأنه يقلّل المسافة التي تحتاجها أيّة عقدة للعثور على البيانات، لكن لا يُعَدّ ذلك موقفًا عمليًا عندما ينمو عدد العقد إلى المئات والآلاف كما هو الحال مع الحواسيب العملاقة الكبيرة، فالأساس في هذا النمو هو مجموعة مؤلفة من عقدتين تتواصلان مع بعضهما بعضًا ثم ستنمو إلى n!/2*(n-2)!‎. تُستخدَم التخطيطات البديلة لمقايضة المسافة بين العقد مع الوصلات المطلوبة بهدف التقليل من هذا النمو الأسي، فأحد هذه التخطيطات الشائعة في معماريات NUMA الحديثة هو المكعب الفائق Hypercube الذي يحتوي على تعريف رياضي صارم، ويكون المكعب الفائق هو نظير رباعي الأبعاد للمكعب الذي هو نظير ثلاثي الأبعاد للمربع. مثال عن المكعب الفائق Hypercube الذي يوفر مقايضةً جيدةً بين المسافة بين العقد وعدد الوصلات المطلوب. يمكننا أن نرى في الشكل السابق أن المكعب الخارجي يحتوي على 8 عقد، والحد الأقصى لعدد المسارات المطلوبة لأي عقدة للتواصل مع عقدة أخرى هو 3، فإذا وضعنا مكعبًا آخر داخل هذا المكعب، فسيكون لدينا ضعف عدد المعالجات ولكن زادت التكلفة القصوى للمسار إلى 4، مما يعني نمو تكلفة المسار القصوى خطيًا فقط عند نمو عدد المعالجات بمقدار 2‎n‎‎‎. ترابط الذاكرة المخبئية Cache Coherency لا يزال الحفاظ على ترابط الذاكرة المخبئية في نظام NUMA ممكنًا، إذ يشار إلى ذلك باسم نظام NUMA مع ترابط الذاكرة المخبئية Cache Coherent NUMA System أو ccNUMA اختصارًا، ولا يتوسّع المخطط القائم على البث الإذاعي المُستخدَم للحفاظ على ترابط ذاكرة المعالج المخبئية في نظام SMP إلى مئات أو حتى آلاف المعالجات في نظام NUMA كبير. يشار إلى أحد المخططات الشائعة لترابط الذاكرة المخبئية في نظام NUMA باسم النموذج المستند إلى الدليل Directory Based Model الذي تتصل فيه المعالجات الموجودة في النظام بعتاد دليل الذاكرة المخبئية، إذ يحافظ عتاد الدليل على صورة متناسقة لكل معالج، كما يخفي هذا التجريد عمل نظام NUMA عن المعالج. يحتفظ المخطط المستند إلى الدليل لصاحبيه Censier و Feautrier بدليل مركزي، إذ تحتوي كل كتلة ذاكرة على بِت راية يُعرَف بالبِت الصالح Valid Bit لكل معالج وبِت واحد يُسمَّى بالبِت المتسخ Dirty Bit، ويضبط الدليل البِت الصالح للمعالج الذي يقرأ الذاكرة إلى ذاكرته المخبئية. إذا أراد المعالج الكتابة إلى خط الذاكرة المخبئية، فيجب أن يضبِط الدليل البِتَّ المتسخ لكتلة الذاكرة من خلال إرسال رسالة إلغاء صلاحية إلى تلك المعالجات التي تستخدِم خط الذاكرة المخبئية والمعالجات التي جرى ضبط رايتها فقط بهدف تجنب حركة مرور البث broadcast traffic. يجب بعد ذلك أن يحاول أيّ معالج آخر قراءة كتلة الذاكرة، وسيجد الدليل ضبط البِت المتسخ، كما يجب أن يحصل الدليل على خط الذاكرة المخبئية المُحدَّث من المعالج مع البِت الصالح المضبوط حاليًا، ويعيد كتابة البيانات المتسخة إلى الذاكرة الرئيسية ثم إعادة هذه البيانات إلى المعالج المطلوب، مما يؤدي إلى ضبط البِت الصالح للمعالج الطالب في هذه العملية، ولاحظ أنّ هذا الأمر واضح للمعالج الطالب ويمكن أن يحتاج الدليل الحصول على تلك البيانات من مكان قريب جدًا أو من مكان بعيد جدًا. لا يمكن أن يتوسع المخطط المؤلَّف من آلاف المعالجات التي تتصل بدليل واحد بصورة جيدة، إذ تتضمن توسّعات المخطط وجود تسلسل هرمي من الدلائل التي تتواصل فيما بينها باستخدام بروتوكول منفصل، كما يمكن أن تستخدِم الدلائل شبكة اتصالات ذات أغراض أعم للتواصل فيما بينها بدلًا من ناقل وحدة المعالجة المركزية، مما يسمح بالتوسع إلى أنظمة أكبر بكثير. تطبيقات NUMA تُعَدّ أنظمة NUMA الأنسب لأنواع المشاكل التي تتطلب قدرًا كبيرًا من التفاعل بين المعالج والذاكرة، فمن المصطلحات الشائعة في محاكاة الطقس مثلًا هو تقسيم البيئة إلى صناديق صغيرة تستجيب بطرق مختلفة، بحيث تعكس المحيطات والأرض أو تخزن كميات مختلفة من الحرارة مثلًا، ويجب تغذية الاختلافات الصغيرة لمعرفة النتيجة الإجمالية أثناء تشغيل عمليات المحاكاة. يؤثر كل صندوق على الصناديق المحيطة، إذ يعني وجود الشمس أكثر قليلًا مثلًا أنّ صندوقًا معينًا ينشر مزيدًا من الحرارة مما يؤثر على الصناديق المجاورة له، ولكن سيكون هناك الكثير من الاتصالات على عكس إطارات الصور الفردية في عملية التصيير Rendering التي لا تؤثر على بعضها، كما يمكن أن تحدث عمليةً مماثلةً إذا أردت تصميم نموذج لحادث سيارة، حيث سيُطوى كل صندوق صغير من السيارة التي تحاكيها بطريقة ما وسيمتص قدرًا من الطاقة. ليس للبرمجيات معرفة مباشرة بأن النظام الأساسي هو نظام NUMA، ولكن يجب أن يتوخّى المبرمجون الحذر عند البرمجة لهذا النظام للحصول على أفضل أداء، وسيؤدي الاحتفاظ بالذاكرة بالقرب من المعالج الذي سيستخدِمها إلى أفضل أداء، ولكن يجب أن يستخدِم المبرمجون تقنيات مثل التشخيص Profiling لتحليل مسارات الشيفرة البرمجية المتّبَعة والعواقب التي تسببها الشيفرة البرمجية للنظام لاستخراج أفضل أداء. ترتيب الذاكرة وقفلها تجلب الذاكرة المخبئية متعددة المستويات والمعمارية متعددة المعالجات الفائقة بعض المشاكل المتعلقة بكيفية رؤية المبرمج لشيفرة المعالج البرمجية التي تكون قيد التشغيل. لنفترض أنّ شيفرة البرنامج البرمجية تعمل على معالجَين في الوقت نفسه، وأنّ كلا المعالجين يشتركان بفعالية في منطقة واحدة كبيرة من الذاكرة، فإذا أصدر أحد المعالجَين تعليمات تخزين لوضع قيمة مسجّل في الذاكرة، فلا بد أنك تتساءل عن الوقت الذي يمكن فيه التأكد من أن المعالج الآخر يحمّل تلك الذاكرة التي سيرى قيمتها الصحيحة. يمكن للنظام في أبسط الحالات أن يضمن أنه في حالة تنفيذ أحد البرامج لتعليمات التخزين، وبالتالي سترى أيّ تعليمات تحميل لاحقة هذه القيمة، إذ يُشار إلى ذلك باسم ترتيب الذاكرة الصارم Strict Memory Ordering، لأن القواعد لا تسمح بأيّ مجال للحركة، كما يجب أن تدرك أنّ هذا النوع من الأشياء يُعَدّ عائقًا خطيرًا أمام أداء النظام. لا يُطلَب من ترتيب الذاكرة أن يكون صارمًا جدًا في كثير من الأحيان، إذ يمكن للمبرمج تحديد النقاط التي يحتاجها للتأكد من رؤية جميع العمليات المُعلَّقة بطريقة عامة، ولكن يمكن أن يكون هناك العديد من التعليمات من بين هذه النقاط حيث لا تكون الدلالات Semantics مهمة، ولنفترض الموقف التالي مثلًا الذي يمثل ترتيب الذاكرة: typedef struct { int a; int b; } a_struct; /* * مرّر مؤشرًا لتخصيصه بوصفه بنيةً جديدةً */ void get_struct(a_struct *new_struct) { void *p = malloc(sizeof(a_struct)); /* لا نهتم بترتيب التعليمتين التاليتين * اللتين ستُنفَّذان في النهاية */ p->a = 100; p->b = 150; /* .لكن يجب أن تُنفَّذا قبل التعليمة التالية * p وإلّا فسيتمكن معالج آخر ينظر إلى قيمة * .من أن يجدها تؤشّر إلى بنية قيمها غير مملوءة */ new_struct = p; } لدينا في هذا المثال عمليتَي تخزين يمكن تطبيقهما بأيّ ترتيب معيّن بما يناسب المعالج، ولكن يجب في الحالة الأخيرة تحديث المؤشر فقط بمجرد التأكد من اكتمال عمليتَي التخزين السابقتين، وإلّا فيمكن أن ينظر معالج آخر إلى قيمة p ويتبع المؤشر إلى الذاكرة ويحمّلها ويحصل على قيمة غير صحيحة تمامًا، لذا يجب أن تحتوي عمليات التحميل والتخزين على دلالات تصف سلوكها. توصَف دلالات الذاكرة من حيث الأسوار Fences التي تحدّد كيفية إعادة ترتيب عمليات التحميل والتخزين، كما يمكن افتراضيًا إعادة طلب عملية التحميل أو التخزين في أيّ مكان، ويشبه اكتساب الدلالات Acquire Semantics السورَ الذي يسمح فقط لعمليات التحميل والتخزين بالتحرك للأسفل عبره، أي يمكنك ضمان أنّ أيّ عملية تحميل أو تخزين لاحقة سترى القيمة -لأنه لا يمكن نقلها فوقها- عند اكتمال هذا التحميل أو التخزين. يُعَدّ تحرير الدلالات Release Semantics عكس ذلك، أي يسمح السور بأي عملية تحميل أو تخزين أن تكتمل قبله -أي التحرك للأعلى-، ولكن لا يوجد شيء قبلها للتحرك للأسفل. وبالتالي يمكنك تخزين أيّ عملية تحميل أو تخزين سابقة مكتملة عند معالجة التحميل أو التخزين باستخدام تحرير الدلالات. رسم توضيحي يمثّل عمليات إعادة الترتيب الصالحة للعمليات باستخدام اكتساب الدلالات وتحريرها سور الذاكرة الكامل full memory fence هو مزيج من اكتساب الدلالات وتحريرها، حيث لا يمكن إعادة ترتيب عمليات التحميل أو التخزين في أيّ اتجاه حول عملية التحميل أو التخزين الحالية، كما يستخدِم نموذج الذاكرة الأكثر صرامة سور ذاكرة كامل لكل عملية، في حين سيترك النموذج الأضعف كل عملية تحميل وتخزين على أساس تعليمات عادية قابلة لإعادة الترتيب. المعالجات ونماذج الذاكرة تطبّق المعالجات المختلفة نماذج ذاكرة مختلفة، إذ يحتوي معالج x86 ومعالج AMD64 على نموذج ذاكرة صارم تمامًا، حيث تحتوي جميع عمليات التخزين على تحرير دلالات، أي يجب أن ترى أيّة عملية تحميل أو تخزين لاحقة نتيجةَ عملية التخزين، ولكن جميع عمليات التحميل لها دلالات عادية، كما تعطي بادئة القفل سورًا للذاكرة، في حين يسمح المعالج إيتانيوم Itanium لجميع عمليات التحميل والتخزين بأن تكون عاديةً ما لم يُجرَى إخباره صراحةً بغير ذلك. القفل ليست معرفة متطلبات ترتيب الذاكرة لكل معمارية عمليةً ومناسبةً لجميع المبرمجين وسيجعل ذلك نقل البرامج وتنقيحها عبر أنواع المعالجات المختلفة أمرًا صعبًا، إذ يستخدِم المبرمجون مستوًى أعلى من التجريد يسمى القفل Locking للسماح بالتشغيل المتزامن للبرامج عندما يكون هناك وحدات معالجة مركزية متعددة، كما لا يمكن لأيّ معالج آخر الحصول على القفل حتى يُحرَّر عندما يحصل برنامج ما عليه لجزء من شيفرة برمجية، كما يجب أن يحاول المعالج أخذ القفل قبل أيّ أجزاء مهمة من الشيفرة البرمجية، فإذا لم يستطع الحصول عليه، فلن يستمر في عمله. يمكنك رؤية كيف أنّ ذلك مقيَّد بتسمية دلالات ترتيب الذاكرة الموضَّحة سابقًا، كما نريد التأكد من أنه لن يُعاد طلب أيّ عمليات يجب أن يحميها القفل قبل الحصول عليه، وهذه هي الطريقة التي تعمل بها عملية اكتساب الدلالات، في حين يجب التأكد من أنّ كل عملية طبّقناها أثناء احتفاظنا بالقفل مكتملة عندما نحرره مثل مثال تحديث المؤشر الموضَّح سابقًا، وهذا ما يسمى بتحرير الدلالات. هناك العديد من المكتبات البرمجية المتاحة التي تسمح للمبرمجين بعدم القلق بشأن تفاصيل دلالات الذاكرة واستخدام المستوى الأعلى من تجريد القفل lock()‎ وإلغاء القفل unlock()‎. صعوبات الأقفال تجعل أنظمة القفل البرمجة أكثر تعقيدًا، إذ يمكنها أن تؤدي إلى تعطيل البرامج، ولنفترض أنّ معالجًا ما يحتفظ بقفل على بعض البيانات، وينتظر قفلًا على بيانات أخرى حاليًا، فإذا انتظر معالج آخر البيانات التي يحتفظ بها المعالج الأول وكان قبل ذلك وقبل دخوله في حالة قفل يحتفظ ببيانات يريدها المعالج الأول ذاك لفك قفله، فسنواجه حالة تعطل تام، بحيث ينتظر كل معالج المعالج الآخر ولا يمكن لأيّ منهما الاستمرار بدون قفل المعالج الآخر. ينشأ هذا الموقف بسبب حالة التسابق Race Condition في أغلب الأحيان التي تُعَدّ إحدى أصعب الأخطاء التي يمكن تعقّبها، فإذا كان هناك معالِجان يعتمدان على عمليات تحدث بترتيب معيّن في الوقت، فهناك دائمًا احتمال حدوث حالة تسابق، كما يمكن أن تصطدم أشعة جاما المنبعثة من نجم متفجر في مجرة أخرى بأحد المعالجات، مما يؤدي إلى الخروج عن ترتيب العمليات، ثم ستحدث حالة تعطل تام كما رأينا سابقًا، لذا يجب ضمان ترتيب البرامج باستخدام الدلالات وليس عبر الاعتماد على سلوكيات محددة لمرة واحدة. يوجد وضع مماثل يسمى المنع Livelock وهو عكس التعطل Deadlock، إذ يمكن أن تكون إحدى الاستراتيجيات لتجنب التعطل أن يكون لديك قفل مؤدب Polite يرفض إعطاء القفل لكل مَن يطلبه، وقد يتسبب هذا القفل المؤدّب في جعل خيطين Threads يمنحان بعضهما القفل باستمرار دون الحاجة إلى أخذ القفل لفترة كافية لإنجاز العمل المهم والانتهاء من القفل، إذ يمكن أن يكون هناك وضع مشابه في الحياة الواقعية لشخصين يلتقيان عند الباب في الوقت نفسه، ويقول كلاهما: "لا، أنت أولًا، أنا أصر على ذلك" دون المرور عبر الباب نهائيًا. استراتيجيات القفل هناك العديد من الاستراتيجيات المختلفة لتطبيق سلوك الأقفال، إذ يُشار إلى القفل البسيط الذي يحتوي ببساطة على حالتين -مقفل Locked أو غير مقفل Unlocked- على أنه كائن مزامنة Mutex، وهو اختصار للاستبعاد المتبادل Mutual Exclusion الذي يعني أنه إذا كان لدى شخص ما قفلًا، فلا يمكن لشخص آخر الحصول عليه، وهناك عدد من الطرق لتطبيق قفل كائن المزامنة، إذ لدينا في أبسط الحالات ما يسمى بالقفل الدوار Spinlock إذ يبقى المعالج ضمن حلقة في انتظار أخذ القفل مثل طفل صغير يطلب من والديه شيئًا ويقول "هل يمكنني الحصول عليه الآن؟" باستمرار. تكمن مشكلة هذه الاستراتيجية في أنها تضيع الوقت، إذ لا ينفّذ المعالج أيّ عملٍ مفيد بينما يكون متوقفًا ويطلب القفل باستمرار، وقد يكون ذلك مناسبًا للأقفال التي يُحتمَل أن تُقفَل لفترة قصيرة جدًا من الوقت فقط، ولكن يمكن أن يكون مقدار الوقت الذي يستغرقه القفل أطول بكثير في كثير من الحالات. الاستراتيجية الأخرى هي السكون Sleep، حيث إذا لم يتمكن المعالج من الحصول على القفل، فسينفّذ بعض الأعمال الأخرى في انتظار إشعار بأن القفل متاح للاستخدام، وسنرى في المقالات القادمة كيف يمكن لنظام التشغيل تبديل العمليات وإعطاء المعالج مزيدًا من العمل لتنفيذه. يُعَدّ كائن المزامنة حالةً خاصةً من متغير تقييد الوصول Semaphore الذي اخترعه عالم الحاسوب الهولندي ديكسترا Dijkstra، إذ يمكن ضبط متغير تقييد الوصول Semaphore لحساب عدد مرات الوصول إلى الموارد في حالة توفر العديد منها، في حين يكون لديك كائن المزامنة Mutex في الحالة التي يكون فيها عدد الموارد يساوي واحدًا فقط. لكن لا تزال أنظمة القفل هذه تواجه بعض المشاكل، إذ يرغب معظم الأشخاص في قراءة البيانات التي تُحدَّث في حالات نادرة فقط. يمكن أن يؤدي وجود جميع المعالجات التي ترغب في قراءة البيانات فقط التي تتطلب قفلًا إلى تنازع القفل حيث يُنجَز القليل من العمل لأن الجميع ينتظر الحصول على القفل نفسه لبعض البيانات. ترجمة -وبتصرُّف- للقسم Small to big systems من الفصل Computer Architecture من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال التالي: دور نظام التشغيل وتنظيمه في معمارية الحاسوب المقال السابق: الأجهزة الطرفية Peripherals ونواقلها Buses في معمارية الحاسوب وحدة المعالجة المركزية المدخل الشامل لتعلم علوم الحاسوب اختيار العتاد والبرامج في العالم الرقمي
  2. الأجهزة الطرفية peripherals هي مجموعة الأجهزة الخارجية التي تتصل بحاسوبك، ويجب أن يكون للمعالج طريقة ما للتواصل مع هذه الأجهزة الطرفية لجعلها مفيدة، وتسمى قناة الاتصال بين المعالج والأجهزة الطرفية بالناقل Bus. المفاهيم الخاصة بنواقل الأجهزة الطرفية يتطلب الجهاز عمليات إدخال وإخراج ليكون مفيدًا، ويوجد هناك عدد من المفاهيم الشائعة المطلوبة للتواصل المفيد مع الأجهزة الطرفية التي سنستعرضها فيما يلي. المقاطعات Interrupts تسمح المقاطعة للجهاز بمقاطعة المعالج حرفيًا بما تعنيه الكلمة للإشارة إلى بعض المعلومات، فمثلًا تُنشَأ مقاطعة لتسليم حدث الضغط على مفتاح إلى نظام التشغيل عند الضغط عليه، إذ تسنِد تركيبة من نظام التشغيل وبيوس BIOS مقاطعةً لكل جهاز. ترتبط الأجهزة عمومًا بمتحكم المقاطعة القابل للبرمجة Programmable Interrupt Controller أو PIC اختصارًا، وهو شريحة منفصلة تُعَدّ جزءًا من اللوحة الأم التي تخزّن معلومات المقاطعة مؤقتًا وتنقلها إلى المعالج الرئيسي، كما يحتوي كل جهاز على خط مقاطعة فيزيائي بينه وبين أحد خطوط PIC التي يوفرها النظام، فإذا أراد الجهاز مقاطعة المعالج، فسيعدّل الجهد على هذا الخط. هناك وصف واسع جدًا لدور متحكم PIC وهو أنه يتلقى هذه المقاطعة ويحولها إلى رسالة ليستخدمها المعالج الرئيسي، كما يختلف هذا الإجراء حسب المعمارية، ولكن المبدأ العام هو أن يضبط نظام التشغيل جدول واصف المقاطعات Interrupt Descriptor Table الذي تربط فيه كل مقاطعة محتمَلة بعنوان شيفرة برمجية للانتقال إليها عند تلقي المقاطعة كما هو موضح في الشكل الآتي. كتابة معالج المقاطعة Interrupt Handler هو عمل مطور برنامج تشغيل الجهاز بالتزامن مع نظام التشغيل. نظرة عامة على معالجة المقاطعة: يرفع الجهاز المقاطعة إلى متحكم المقاطعة، حيث تمرِّر هذه المقاطعة المعلومات إلى المعالج. ينظر المعالج إلى جدول واصف مقاطعاته الذي يملؤه نظام التشغيل للعثور على الشيفرة البرمجية التي تعالج الخطأ. تقسّم معظم المشغّلات معالجة المقاطعات إلى نصفين سفلي وعلوي، إذ يتعرف النصف السفلي على المقاطعة ويضع الإجراءات في رتل للمعالجة ويعيد المعالج إلى ما كان يفعله سابقًا بسرعة، في حين سيُشغَّل النصف العلوي لاحقًا عندما تكون وحدة المعالجة المركزية متاحةً، وسينفّذ المعالجة الإضافية، كما يؤدي ذلك إلى وقف المقاطعة التي تعطل وحدة المعالجة المركزية بأكملها. حفظ الحالة بما أنّ المقاطعة يمكن أن تحدث في أيّ وقت، فيجب أن تتمكن من العودة إلى العملية الجارية عند الانتهاء من معالجة المقاطعة، كما أنّ مهمة نظام التشغيل هي التأكد من أنه يحفظ أيّ حالة State عند الدخول إلى معالج المقاطعة، أي يسجلها ويستعيدها عند العودة من معالج المقاطعة، وتكون بذلك المقاطعة واضحةً تمامًا في كل ما يحدث في ذلك الوقت بغض النظر عن الوقت الضائع. المقاطعات Interrupts والمصائد Traps والاستثناءات Exceptions ترتبط المقاطعة عمومًا بحدث خارجي من جهاز فيزيائي، ولكن تُعَدّ الآلية نفسها مفيدةً للتعامل مع عمليات النظام الداخلية، فإذا اكتشف المعالج مثلًا حالات مثل الوصول إلى ذاكرة غير صالحة أو محاولة القسمة على صفر أو تعليمات غير صالحة، فيمكنه داخليًا رفع استثناء ليعالجه نظام التشغيل، كما تُستخدَم هذه الآلية ليلتقط نظام التشغيل استدعاءات النظام ولتطبيق الذاكرة الوهمية virtual memory، في حين تبقى مبادئ مقاطعة الشيفرة البرمجية المُشغَّلة بطريقة غير متزامنة كما هي بالرغم من إنشائها داخليًا وليس من مصدر خارجي. أنواع المقاطعات هناك طريقتان رئيسيتان لإصدار إشارات إلى المقاطعات على الخط هما المستوى level والحافة edge المُنبَّهة، إذ تحدّد المقاطعات ذات المستوى المُنبَّه جهد خط المقاطعة الذي يُحتفَظ به مرتفعًا للإشارة إلى وجود مقاطعة معلَّقة، في يحن تكتشف المقاطعات ذات الحافة المُنبَّهة الانتقالات في الناقل عندما ينتقل جهد الخط من منخفض إلى مرتفع، ويكتشف متحكم المقاطعة PIC نبضة الموجة المربعة باستخدام المقاطعة ذات الحافة المنبَّهة عند إصدار الإشارة ورفع المقاطعة. يظهر الفرق عندما تشترك الأجهزة في خط مقاطعة، إذ سيكون خط المقاطعة مرتفعًا في نظام المقاطعة ذي المستوى المنبَّه حتى معالجة جميع الأجهزة التي رفعت المقاطعة وإلغاء تأكيد مقاطعتها، كما تشير النبضة الموجودة على الخط إلى متحكم المقاطعة PIC الذي تنشئه المقاطعة في نظام المقاطعة ذي الحافة المنبَّهة، وستصدر هذه النبضة إشارةً إلى نظام التشغيل لمعالجة المقاطعة في حالة ظهور نبضات أخرى على الخط المؤكَّد مسبقًا من جهاز آخر. تكمن مشكلة المقاطعات ذات المستوى المنبَّه في أنها يمكن أن تتطلب قدرًا كبيرًا من الوقت لمعالجة مقاطعة أحد الأجهزة، إذ يظل خط المقاطعة مرتفعًا أثناء هذا الوقت ولا يمكن تحديد ما إذا تسبّب أيّ جهاز آخر في حدوث مقاطعة على الخط، وهذا يعني أنه يمكن أن يكون هناك زمن تأخير كبير وغير متوقع في خدمة المقاطعات. يمكن ملاحظة المقاطعة طويلة الأمد ووضعها في رتل انتظار في المقاطعات ذات الحافة المُنبَّهة، ولكن لا يزال بإمكان الأجهزة الأخرى التي تشترك في الخط الانتقال -وبالتالي رفع المقاطعات- أثناء حدوث ذلك، ويؤدي ذلك إلى حدوث مشاكل جديدة، إذ يمكن تفويت أحد المقاطعات في حالة مقاطعة جهازين في الوقت نفسه أو يمكن أن يؤدي التشويش البيئي أو غيره إلى حدوث مقاطعة زائفة يجب تجاهلها. المقاطعات غير القابلة للتقنع أو الإخفاء Non-maskable Interrupts يجب أن يكون النظام قادرًا على إخفاء المقاطعات أو منعها في أوقات معينة، ويمكن وضع المقاطعات لتكون قيد الانتظار، لكن هناك صنف معيّن من المقاطعات يسمى المقاطعات غير القابلة للتقنّع أو الإخفاء Non-maskable Interrupts أو NMI اختصارًا، إذ تُعَدّ هذه المقاطعات استثناءً من هذه القاعدة مثل مقاطعة إعادة الضبط reset. يمكن أن تكون مقاطعات NMI مفيدةً لتطبيق أشياء مثل مراقبة النظام، حيث تُرفَع مقاطعة NMI دوريًا وتضبِط بعض الرايات التي يجب أن يقرّ بها نظام التشغيل، فإذا لم يظهر هذا الإقرار قبل مقاطعة NMI الدورية التالية، فيمكن عَدّ النظام أنه لا يحرز أيّ تقدم، كما يمكن استخدام مقاطعات NMI لتشخيص Profiling النظام، إذ يمكن رفع مقاطعات NMI الدورية واستخدامها لتقييم الشيفرة البرمجية التي يعمل بها المعالج حاليًا، مما يؤدي بمرور الوقت إلى إنشاء ملف تعريف للشيفرة البرمجية التي تعمل والحصول على رؤية مفيدة للغاية حول أداء النظام. فضاء الإدخال والإخراج IO يجب أن يتصل المعالج بالجهاز الطرفي عبر عمليات الإدخال والإخراج IO، ويُطلَق على الشكل الأكثر شيوعًا من عمليات IO عمليات الإدخال والإخراج المرتبطة بالذاكرة Memory Mapped IO، إذ ترتبط المسجلات الموجودة على الجهاز مع الذاكرة، وما عليك سوى القراءة أو الكتابة في عنوان محدد من الذاكرة للتواصل مع الجهاز. الوصول المباشر للذاكرة DMA بما أن سرعة الأجهزة أقل بكثير من سرعة المعالجات، فيجب أن يكون هناك طريقة ما لتجنب انتظار وحدة المعالجة المركزية للبيانات من الأجهزة. يُعَدّ الوصول المباشر للذاكرة Direct Memory Access -أو DMA اختصارًا- طريقةً لنقل البيانات مباشرةً بين الجهاز الطرفي وذاكرة RAM الخاصة بالنظام، ويمكن لمشغّل الجهاز إعداده لإجراء نقل باستخدام طريقة الوصول DMA من خلال إعطائه منطقةً من ذاكرة RAM لوضع بياناته فيها، ثم يمكنه بدء نقل DMA والسماح لوحدة المعالجة المركزية بمواصلة تنفيذ المهام الأخرى. سيرفع الجهاز المقاطعة بعد الانتهاء ويرسل لمشغّل الجهاز إشارةً باكتمال النقل، ثم ستكون البيانات القادمة من الجهاز مثل ملف من قرص صلب أو إطارات من بطاقة التقاط الفيديو موجودةً في الذاكرة وجاهزةً للاستخدام. نواقل أخرى تصل نواقل أخرى بين ناقل PCI والأجهزة الخارجية مثل ناقل USB الذي سنتعرف عليه فيما يلي. USB يُعَدّ جهاز USB من وجهة نظر نظام التشغيل أنه مجموعة من نقاط النهاية المجمَّعة معًا في واجهة ما، إذ يمكن أن تكون نقطة النهاية إما نقطة إدخال أو إخراج، بحيث تنقل نقطة النهاية البيانات باتجاه واحد فقط، كما يمكن أن تحتوي نقاط النهاية على عدد من الأنواع المختلفة هي: نقاط نهاية خاصة بعمليات التحكم Control End-points: مخصصة لإعداد الجهاز وغير ذلك. نقاط نهاية خاصة بالمقاطعات Interrupt End-points: تُستخدَم لنقل كميات صغيرة من البيانات، ولديها أولوية عليا. نقاط النهاية المجمَّعة Bulk End-points: تنقل كميات كبيرة من البيانات ولكنها لا تحصل على قيود زمنية مضمونة. عمليات النقل المتزامنة Isochronous Transfers: هي عمليات نقل ذات أولوية عالية في الوقت الحقيقي، ولكن إذا جرى تفويتها، فلن يعاد تجربتها، وتُستخدَم لبيانات البث مثل الفيديو أو الصوت حيث لا توجد فائدة من إرسال البيانات مرةً أخرى. يمكن أن يكون هناك العديد من الواجهات المكونة من نقاط نهاية متعددة، وتُجمَّع الواجهات ضمن إعدادات Configurations، ولكن معظم الأجهزة لها إعداد واحد فقط. نظرة عامة على متحكم UCHI (مأخوذة من توثيق إنتل Intel) يوضح الشكل السابق نظرة عامة على واجهة متحكم المضيف العامة Universal Host Controller Interface أو UHCI اختصارًا، إذ ويوفر نظرةً عامةً حول كيفية نقل بيانات USB خارج النظام عن طريق مجموعة من العتاد والبرمجيات، كما تضبط البرمجيات قالب بيانات بتنسيق محدد لمتحكم المضيف لقراءته وإرساله عبر ناقل USB. يحتوي المتحكم بدءًا من أعلى يسار الشكل السابق على مسجل إطارات مع عدّاد يُزاد دوريًا في كل ميلي ثانية، إذ تُستخدَم هذه القيمة للفهرسة ضمن قائمة إطارات تنشئها البرمجيات، ويؤشّر كل إدخال في هذا الجدول إلى رتل واصفات النقل Transfer Descriptors، كما تضبط البرمجيات هذه البيانات في الذاكرة ويقرؤها المتحكم المضيف الذي يُعَدّ شريحةً منفصلةً تشغّل ناقل USB، ويجب أن تجدول البرمجيات أرتال العمل بحيث يُمنَح 90% من وقت الإطار للبيانات المتزامنة ويُمنَح 10% المتبقية لبيانات المقاطعة والتحكم والبيانات المُجمَّعة. تعني الطريقة التي تُربَط بها البيانات أنّ واصفات النقل للبيانات المتزامنة ترتبط بمؤشر إطار معيّن واحد فقط -أي فترة زمنية معينة واحدة فقط- ثم ستُهمَل، لكن تُوضَع جميع بيانات المقاطعة والتحكم والبيانات المُجمَّعة ضمن رتل انتظار بعد البيانات المتزامنة، وبالتالي إذا لم تُرسَل في إطار واحد -أو فترة زمنية واحدة- فسيجري ذلك في المرة التالية. تتواصل طبقات USB عبر كتل طلبات USB أو URB اختصارًا، إذ تحتوي كتل URB على معلومات حول نقطة النهاية التي يرتبط بها هذا الطلب والبيانات وأي معلومات أو سمات ذات صلة ودالة رد نداء call-back function تُستدعَى عند اكتمال كتلة URB، كما ترسِل مشغّلات USB كتل URB بتنسيق ثابت إلى مركز USB الذي يديرها بالتنسيق مع متحكم مضيف USB على النحو الوارد أعلاه، وتُرسَل بياناتك إلى جهاز USB عبر مركز USB، ثم تُشغَّل دالة رد النداء. ترجمة -وبتصرُّف- للقسم Peripherals and buses من الفصل Computer Architecture من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال التالي: أنظمة المعالجات في معمارية الحاسوب المقال السابق: نظرة عميقة على تسلسل الذواكر الهرمي والذاكرة المخبئية في معمارية الحاسوب المدخل الشامل لتعلم علوم الحاسوب فهم عملية التخبئة (Caching) في معمارية الحاسوب
  3. يمكن لوحدة المعالجة المركزية جلب التعليمات والبيانات مباشرةً من الذاكرة المخبئية Cache Memory الموجودة على شريحة المعالج فقط، لذا يجب تحميل الذاكرة المخبئية من ذاكرة النظام الرئيسية، أي ذاكرة الوصول العشوائي Random Access Memory -أو RAM اختصارًا-، ولكن تحتفظ الذاكرة RAM بمحتوياتها فقط عند الوصل بمصدر طاقة، لذلك يجب تخزينها على مساحة تخزين دائمة وغير متطايرة. تسلسل الذواكر الهرمي نطلق على طبقات الذواكر التالية اسم تسلسل الذواكر الهرمي Memory Hierarchy: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } السرعة الذاكرة الوصف الأسرع الذاكرة المخبئية Cache الذاكرة المخبئية هي ذاكرة مضمَّنة في وحدة المعالجة المركزية، وهي ذاكرة سريعة جدًا وتستغرق دورة واحدة فقط للوصول إليها، ولكن هناك حد لحجمها لأنها مُدمَجة مباشرةً في وحدة المعالجة المركزية، كما توجد هناك عدة مستويات فرعية من الذاكرة المخبئية تسمى L1 و L2 و L3 بسرعات متزايدة قليلًا عن بعضها البعض. الذاكرة RAM يجب أن تأتي جميع التعليمات وعناوين التخزين الخاصة بالمعالج من الذاكرة RAM، وتستغرق وحدة المعالجة المركزية بعض الوقت للوصول إلى الذاكرة RAM يسمى زمن التأخير Latency بالرغم من أنها ذاكرة سريعة جدًا، كما تُخزَّن الذاكرة RAM في شرائح منفصلة ومخصصة متصلة باللوحة الأم، مما يعني أنها أكبر بكثير من الذاكرة المخبئية. الأبطأ القرص الصلب Disk جميعنا على دراية بالبرامج التي تصلنا على قرص مرن floppy disk أو قرص مضغوط، ونعلم كيفية حفظ ملفاتنا على القرص الصلب، ونعلم الوقت الطويل الذي يمكن أن يستغرقه البرنامج للتحميل من القرص الصلب، إذ يعني وجود آليات فيزيائية مثل الأقراص الدوارة والرؤوس المتحركة أن الأقراص الصلبة هي أبطأ وسيلة من وسائل التخزين، ولكنها أكبرها حجمًا. النقطة المهمة التي يجب معرفتها حول تسلسل الذواكر الهرمي هي المقايضات بين السرعة والحجم على حساب بعضهما البعض، فكلما كانت الذاكرة أسرع، كان حجمها أصغر. سبب فعالية الذواكر المخبئية هو أنّ شيفرة الحاسوب البرمجية تعرض شكلَين من أشكال المحلية Locality هما: تشير المحلية المكانية Spatial Locality إلى احتمالية الوصول إلى البيانات الموجودة ضمن الكتل مع بعضها بعضًا. تشير المحلية الزمانية Temporal Locality إلى أن البيانات المستخدَمة مؤخرًا يُحتمَل أن تُستخدَم مرة أخرى قريبًا. يعني ذلك أنه يمكن الاستفادة من تنفيذ أكبر قدر ممكن من عمليات الوصول السريعة إلى الذاكرة أي المحلية الزمانية وتخزين كتل صغيرة من المعلومات ذات الصلة أي المحلية المكانية. الذاكرة المخبئية تُعَدّ الذاكرة المخبئية أحد أهم عناصر معمارية وحدة المعالجة المركزية، إذ يجب على المطورين فهم كيفية عمل الذاكرة المخبئية في أنظمتهم لكتابة شيفرة برمجية فعالة، كما تُعَدّ نسخةً سريعةً جدًا من ذاكرة النظام الرئيسية الأبطأ، وهي أصغر بكثير من الذواكر الرئيسية لأنها مضمنة داخل شريحة المعالج جنبًا إلى جنب مع المسجلات ومنطق المعالج، وهناك حدود اقتصادية ومادية لأقصى حجم لها. تجد الشركات المصنعة مزيدًا من الطرق لحشر مزيد من الترانزستورات على الشريحة، مما يؤدي إلى زيادة أحجام الذواكر المخبئية بصورة كبيرة، ولكن يُقدَّر حجم حتى أكبر الذواكر المخبئية بعشرات الميجابايتات بعكس حجم الذاكرة الرئيسية المقدَّر بالجيجابايتات أو حجم القرص الصلب المقدَّر بالتيرابايتات. تتكون الذاكرة المخبئية من قطع صغيرة تعكس محتوى أجزاء من الذاكرة الرئيسية، إذ يُطلَق على حجم هذه القطع بحجم الخط Line Size، ويساوي تقريبًا 32 أو 64 بايتًا، ومن الشائع التحدث عن حجم الخط أو خط الذاكرة المخبئية عند الحديث عن الذاكرة المخبئية، والذي يشير إلى قطعة واحدة تعكس محتوى قطعة من الذاكرة الرئيسية، كما يمكن للذاكرة المخبئية فقط تحميل وتخزين الذاكرة بأحجام مضاعفة من خط الذاكرة المخبئية. تحتوي الذواكر المخبئية على تسلسلها الهرمي الخاص، ويطلق عليه عادةً L1 و L2 و L3، إذ تُعَدّ الذاكرة المخبئية L1 هي الأسرع والأصغر و L2 أكبر وأبطأ منها و L3 هي الأكبر والأبطأ، كما تُقسَم الذاكرة المخبئية L1 إلى ذواكر مخبئية خاصة بالتعليمات وأخرى بالبيانات، وتُعرف باسم معمارية هارفارد Harvard Architecture بعد أن قدمها حاسوب Harvard Mark-1 القائم على المُرحّلات Relay. تساعد الذواكر المخبئية المقسمة على تقليل الاختناقات في خطوط الأنابيب، حيث تشير مراحل خط الأنابيب السابقة إلى تعليمات الذاكرة المخبئية وتشير المراحل اللاحقة إلى بيانات الذاكرة المخبئية، كما يسمح توفير ذاكرة مخبئية منفصلة للتعليمات بإجراء تطبيقات بديلة تستفيد من طبيعة مجرى التعليمات بغض النظر عن فائدة تقليل التنازع على مورد مشترك، إذ تكون الذاكرة المخبئية الخاصة بالتعليمات للقراءة فقط، أي لا تحتاج إلى ميزات باهظة الثمن على الشريحة مثل تعدد المنافذ، ولا تحتاج إلى التعامل مع عمليات قراءة الكتل الفرعية لأن مجرى التعليمات يستخدِم عمومًا عمليات وصول ذات أحجام أكثر انتظامًا. ترابط الذاكرة المخبئية: يمكن أن يجد خط ذاكرة مخبئية معيّن مكانًا صالحًا في أحد الإدخالات المظللة. يطلب المعالج باستمرار من الذاكرة المخبئية أثناء التشغيل العادي التحققَ من تخزين عنوان معيّن في الذاكرة المخبئية، لذلك تحتاج الذاكرة المخبئية لطريقة ما لمعرفة ما إذا كان لديها خط صالح أم لا، فإذا أمكن تخزين عنوان معيّن في أيّ مكان ضمن الذاكرة المخبئية، فيجب البحث في كل خط من الذاكرة المخبئية في كل مرة يُنشَأ فيها مرجع لتحديد وصول صحيح أو خاطئ، كما يمكن الاستمرار في البحث السريع من خلال إجرائه على التوازي في عتاد الذاكرة المخبئية، ولكن يكون البحث في كل إدخال مكلفًا للغاية بحيث يتعذر تطبيقه في ذاكرة مخبئية ذات حجم معقول، لذا يمكن جعل الذاكرة المخبئية أبسط من خلال فرض قيود على مكان وجود عنوان معيّن. يُعَدّ ذلك مقايضةً، فالذاكرة المخبئية أصغر بكثير من ذاكرة النظام، لذا يجب أن تحمل بعض العناوين أسماء بديلة Alias للعناوين الأخرى، فإذا جرى تحديث عنوانَين يحملان أسماء بديلةً لبعضهما البعض باستمرار، فسيقال أنهما يتنازعان على خط الذاكرة المخبئية، كما يمكننا تصنيف الذواكر المخبئية إلى ثلاثة أنواع عامة كما هو موضح في الشكل السابق وهي: الذواكر المخبئية المربوطة مباشرةً Direct mapped Caches التي تسمح لخط الذاكرة المخبئية بالتواجد فقط في إدخال واحد في الذاكرة المخبئية، ويُعَدّ ذلك أبسط تطبيق في العتاد، ولكن -كما هو موضح في الشكل السابق- لا توجد إمكانية لتجنب استخدام الأسماء البديلة لأن العنوانَين المظلَّلين يجب عليهما التشارك في خط الذاكرة المخبئية نفسه. الذواكر المخبئية الترابطية بالكامل Fully Associative Caches التي تسمح بوجود خط الذاكرة المخبئية في أيّ إدخال منها، مما يؤدي إلى تجنّب مشكلة الأسماء البديلة، لأن أيّ إدخال يكون متاحًا للاستخدام، لكن يُعَدّ تطبيق ذلك في العتاد مكلفًا للغاية لأنه يجب البحث عن كل موقع محتمَل في الوقت نفسه لتحديد ما إذا كانت القيمة موجودةً في الذاكرة المخبئية. الذواكر المخبئية التجميعية Set Associative Caches التي تُعَدّ عبارةً عن مزيج من الذواكر المخبئية المربوطة مباشرةً والذواكر المخبئية الترابطية بالكامل، وتسمح بوجود قيمة معينة للذاكرة المخبئية في بعض المجموعات الفرعية من الخطوط الموجودة ضمن هذه الذاكرة المخبئية، كما تُقسَم الذاكرة المخبئية إلى مناطق تسمَّى طرقًا Ways، ويمكن وجود عنوان معيّن في أيّ طريق، وبالتالي ستسمح الذاكرة المخبئية التجميعية المؤلفة من مجموعة من الطرق عددها n لخط الذاكرة المخبئية بالتواجد ضمن مجموعة الإدخالات التي عددها يساوي باقي قسمة مجموعة الكتل الإجمالية ذات الحجم المحدد على n، ويظهِر الشكل السابق عينةً من ذاكرة تجميعية مؤلفة من 8 عناصر و 4 طرق، إذ يكون للعنوانَين أربعة مواقع محتملة، مما يعني أنه يجب البحث عن نصف الذاكرة المخبئية فقط في كل عملية بحث، وكلما زاد عدد الطرق، زادت المواقع الممكنة ونقصت الأسماء البديلة، مما يؤدي إلى أداء أفضل. يجب أن يتخلص المعالِج من الخط بمجرد امتلاء الذاكرة المخبئية لإفساح المجال لخط جديد، وهناك العديد من الخوارزميات التي يمكن للمعالج من خلالها اختيار الخط الذي سيتخلص منه مثل خوارزمية الأقل استخدامًا مؤخرًا Least Recently Used -أو LRU اختصارًا- والتي تُعَدّ خوارزميةً يجري فيها التخلص من أقدم خط غير مستخدَم لإفساح المجال للخط الجديد. ليس هناك داع لضمان التوافق مع الذاكرة الرئيسية عندما تكون البيانات للقراءة فقط من الذاكرة المخبئية، لكن يحتاج المعالج لاتخاذ بعض القرارات حول كيفية تحديث الذاكرة الرئيسية الأساسية عندما يبدأ في الكتابة في خطوط الذاكرة المخبئية، إذ ستكتب طريقة التخزين الخاصة بالذاكرة المخبئية التي تُسمَّى Write-through Cache التغييرات مباشرةً في ذاكرة النظام الرئيسية عندما يحدّث المعالج الذاكرة المخبئية، ويُعَدّ ذلك أبطأ لأن عملية الكتابة في الذاكرة الرئيسية أبطأ، في حين تؤخر طريقة التخزين الخاصة بالذاكرة المخبئية التي تُسمَّى Write-back Cache كتابةَ التغييرات على الذاكرة RAM حتى الضرورة القصوى، والميزة الواضحة لذلك هي أنّ الوصول إلى الذاكرة الرئيسية مطلوب عند كتابة إدخالات الذاكرة المخبئية. يُشار إلى خطوط الذاكرة المخبئية المكتوبة دون وضعها في الذاكرة على أنها متسخة Dirty، فعيبها هو أنه يمكن أن يتطلب الأمر وصولَين إلى الذاكرة أحدهما لكتابة بيانات الذاكرة الرئيسية المتسخة والآخر لتحميل البيانات الجديدة عند التخلص من إدخال معيّن من الذاكرة المخبئية. إذا كان الإدخال موجودًا في كل من الذاكرة المخبئية ذات المستوى الأعلى والمستوى الأدنى في الوقت نفسه، فإننا نسمّي الذاكرة المخبئية ذات المستوى الأعلى بالشاملة Inclusive. بينما إذا أزالت الذاكرة المخبئية ذات المستوى الأعلى التي تحتوي على خط معيّن إمكانيةَ احتواء ذاكرة مخبئية ذات مستوى أقل على هذا الخط، فإننا نقول أنها حصرية Exclusive وسنناقش ذلك لاحقًا. عنونة الذاكرة المخبئية لم نناقش حتى الآن كيف تقرر الذاكرة المخبئية ما إذا كان عنوان معيّن موجودًا في الذاكرة المخبئية أم لا، إذ يجب أن تحتفظ الذواكر المخبئية بمجلد للبيانات الموجودة حاليًا في خطوط الذاكرة المخبئية، ويمكن وضع مجلد وبيانات الذاكرة المخبئية على المعالج معًا، ولكن يمكن أن يكونا منفصلَين أيضًا كما في حالة المعالج POWER5 الذي يحتوي على مجلد ذاكرة L3 على المعالج، ولكن يتطلب الوصول إلى البيانات اجتياز ناقل L3 للوصول إلى ذاكرة خارجية ليست على المعالج، ويمكن أن يسهّل هذا الترتيب معالجة عمليات الوصول الصحيحة أو الخاطئة بصورة أسرع دون التكاليف الأخرى للاحتفاظ بالذاكرة المخبئية بالكامل على المعالج. وسوم الذاكرة المخبئية Cache Tags: يجب التحقق من الوسوم على التوازي للحفاظ على وقت الاستجابة منخفضًا، إذ يتطلب المزيدُ من بتات الوسوم (أي ارتباطات مجموعات أقل) عتادًا أكثر تعقيدًا لتحقيق ذلك. بينما تعني ارتباطاتُ المجموعات الأكثر وسومًا أقل، ولكن يحتاج المعالج الآن إلى عتاد لمضاعفة خرج العديد من المجموعات التي يمكن أن تضيف زمن تأخير أيضًا. يمكن تحديد ما إذا كان العنوان موجودًا في الذاكرة المخبئية بسرعة من خلال فصله إلى ثلاثة أجزاء هي الوسم Tag والفهرس Index والإزاحة Offset. تعتمد بتات الإزاحة على حجم خط الذاكرة المخبئية، إذ يمكن استخدام خط بحجم 32 بايت مثلًا آخر 5 بتات أي 2‎5‎ من العنوان بوصفه إزاحةً في الخط، ويُعَدّ الفهرس خط ذاكرة مخبئية معيّن يمكن أن يتواجد فيه الإدخال، فلنفترض أنه لدينا ذاكرة مخبئية تحتوي على 256 إدخالًا مثلًا، فإذا كانت هذه الذاكرة هي ذاكرة مخبئية مربوطة مباشرةً، فيمكن أن تكون البيانات موجودة في خط واحد محتمَل فقط، لذا تصف 8 بتات التالية (2‎8‎) بعد الإزاحة الخط المراد التحقق منه بين 0 و 255. لنفترض الآن أنّ الذاكرة المخبئية المكونة من 256 عنصرًا مقسمة إلى طريقين، وهذا يعني أنّ هناك مجموعتين مؤلفتين من 128 خط، ويمكن أن يقع العنوان المحدد في أيّ من هاتين المجموعتين، وبالتالي فإن المطلوب هو 7 بتات فقط على أساس فهرس للإزاحة في الطرق المؤلفة من 128 إدخالًا، كما نخفّض عدد البتات المطلوبة على أساس فهرس لأن كل طريق يصبح أصغر عندما نزيد عدد الطرق بالنسبة إلى حجم ذاكرة مخبئية معيّن. لا يزال مجلد الذاكرة المخبئية بحاجة إلى التحقق مما إذا كان العنوان المخزن في الذاكرة المخبئية هو العنوان الذي يريده، وبالتالي فإن البتات المتبقية من العنوان هي بتات الوسوم التي يتحقق مجلد الذاكرة المخبئية منها مقابل بتات وسم العنوان الواردة لتحديد ما إذا كان هناك عملية وصول صحيحة أم لا، وهذه العلاقة موضحة في الصورة السابقة. إذا كان هناك طرق متعددة، فيجب إجراء هذا التحقق على التوازي في كل طريق، ثم تُمرَر النتيجة بعد ذلك إلى معدد إرسال Multiplexor ينتج عنه نتيجة وصول صحيحة hit أو خاطئة miss، وكلما كانت الذاكرة المخبئية أكثر ارتباطًا، قل عدد البتات المطلوبة للفهرس وزاد عدد البتات المطلوبة للوسم، حتى الوصول إلى أقصى حد للذاكرة المخبئية الترابطية بالكامل حيث لا تُستخدَم بتات كبتات للفهرس، كما تُعَدّ المطابقة على التوازي لبتات الوسوم مكونًا باهظًا لتصميم الذاكرة المخبئية وهي عمومًا العامل المحدّد لعدد الخطوط -أي حجمها- التي يمكن أن تنمو إليها الذاكرة المخبئية. ترجمة -وبتصرُّف- للقسم Memory من الفصل Computer Architecture من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال التالي: الأجهزة الطرفية Peripherals ونواقلها Buses في معمارية الحاسوب المقال السابق: تعرف على وحدة المعالجة المركزية وعملياتها في معمارية الحاسوب الذاكرة وأنواعها فهم عملية التخبئة (Caching) في معمارية الحاسوب المدخل الشامل لتعلم علوم الحاسوب
  4. تنفّذ وحدة المعالجة المركزية التعليمات على القيم الموجودة في المسجّلات Registers، إذ يوضّح المثال الآتي أولًا ضبط R1 على القيمة 100 وتحميل القيمة من موقع الذاكرة 0x100 إلى R2 وجمع القيمتين، ثم وضع النتيجة في R3، وأخيرًا تخزين القيمة الجديدة 110 في R4. يتكون الحاسوب من وحدة معالجة مركزية Central Processing Unit -أو CPU اختصارًا- متصلة بالذاكرة، إذ توضّح الصورة السابقة المبدأ العام لجميع عمليات الحاسوب، كما تنفّذ وحدة المعالجة المركزية التعليمات المقروءة من الذاكرة، وهناك نوعان من هذه التعليمات هما: التعليمات التي تحمّل القيم من الذاكرة إلى المسجلات وتخزّن القيم من المسجلات إلى الذاكرة. التعليمات التي تُشغَّل على القيم المخزَّنة في المسجّلات مثل جمع أو طرح أو ضرب أو قسمة قيمتين موجودتين في مسجلين، أو إجراء العمليات الثنائية and و or و xor وغيرها، أو إجراء عمليات حسابية أخرى، مثل الجذر التربيعي و sin و cos و tan وغيرها. لذا نجمع في مثالنا ببساطة العدد 100 مع قيمة مُخزَّنة في الذاكرة ونخزّن النتيجة الجديدة في الذاكرة. التفريع Branching يُعَدّ التفريع عمليةً مهمةً لوحدة المعالجة المركزية، وذلك بغض النظر عن عمليتي التحميل أو التخزين، إذ تحتفظ وحدة المعالجة المركزية داخليًا بسجل للتعليمة التالية التي ستنفَّذ في مؤشر التعليمات Instruction Pointer، بحيث يُزاد هذا المؤشر ليؤشّر إلى التعليمة التالية تسلسليًا، إذ ستتحقق التعليمة الفرعية مما إذا كان لمسجل معيّن القيمة صفر، أو تتحقق من وجود من ضبط راية flag ما. فإذا كان الأمر كذلك، فسيُعدَّل المؤشر ليؤشّر إلى عنوان مختلف، وبالتالي ستكون التعليمة التالية للتنفيذ من جزء مختلف من البرنامج، وهذه هي الطريقة التي تعمل بها الحلقات وتعليمات القرار. يمكن مثلًا تنفيذ التعليمة if (x==0)‎ من خلال إيجاد ناتج تطبيق عملية or على اثنين من المسجلات، أحدهما يحمل القيمة x والآخر يحمل القيمة صفر، فإذا كانت النتيجة صفرًا، فستكون المقارنة صحيحة، أي أنّ جميع بتات x أصفار ويجب تنفيذ جسم التعليمة، وإلّا فستتجاوز التعليمة الفرعية هذه الشيفرة. الدورات جميعنا على دراية بسرعة الحاسوب المعطاة بالميجاهرتز أو الجيجاهرتز التي تقابل ملايين أو آلاف الملايين من الدورات في الثانية، ويسمى ذلك بسرعة الساعة Clock Speed لأنها السرعة التي تنبض بها ساعة الحاسوب الداخلية، إذ تُستخدَم النبضات ضمن المعالج لإبقائه متزامنًا داخليًا، ويمكن البدء بعملية أخرى في كل لحظة أو نبضة. جلب التعليمة وفك تشفيرها وتنفيذها وتخزين نتيجتها يتكون تنفيذ تعليمة واحدة من دورة معينة من الأحداث، وهي الجلب وفك التشفير والتنفيذ والتخزين، إذ يجب على وحدة المعالجة المركزية تطبيق الخطوات التالية لتنفيذ تعليمة add السابقة مثلًا: الجلب Fetch: الحصول على التعليمات من الذاكرة إلى المعالج. فك التشفير Decode: فك تشفير ما يجب أن تفعله داخليًا، أي الجمع في هذه الحالة. التنفيذ Execute: أخذ القيم من المسجلات وجمعها. التخزين Store: تخزين النتيجة في مسجل آخر، كما يمكن رؤية مصطلح انتهاء Retiring التعليمة. نظرة داخلية إلى وحدة المعالجة المركزية تحتوي وحدة المعالجة المركزية داخليًا على العديد من المكونات الفرعية المختلفة التي تطبّق كلًا من الخطوات المذكورة سابقًا، كما يمكن أن تحدث جميعها بصورة مستقلة عن بعضها البعض، وهي مشابهة لخط الإنتاج في المصانع، حيث توجد العديد من المحطات ولكل خطوة مَهمة معينة لأدائها، ثم يمكنه تمرير النتائج إلى المحطة التالية وأخذ مدخلات جديدة للعمل عليها. تتكون وحدة المعالجة المركزية من العديد من المكونات الفرعية المختلفة، وتطبّق كل منها مهمةً مُخصَّصةً. توضِّح الصورة السابقة مخططًا بسيطًا لبعض الأجزاء الرئيسية لوحدة المعالجة المركزية الحديثة، حيث يمكنك رؤية التعليمات تأتي ثم يفك المعالج تشفيرها؛ كما تحتوي وحدة المعالجة المركزية على نوعين رئيسيين من المسجّلات، هما مسجلات العمليات الحسابية الخاصة بالأعداد الصحيحة ومسجلات العمليات الحسابية الخاصة بالأعداد العشرية. تُعَدّ الأعداد العشرية Floating Point طريقةً لتمثيل الأعداد ذات المنزلة العشرية بصيغة ثنائية، ويجري التعامل معها بطريقة مختلفة ضمن وحدة المعالجة المركزية، كما تُعَدّ المسجلات MMX (توسع الوسائط المتعددة Multimedia Extension) و SSE (مجرى بيانات متعددة لتعليمة مفردة Streaming Single Instruction Multiple Data) أو Altivec مسجلات مماثلة للمسجلات الخاصة الأعداد العشرية. يُعَدّ ملف المسجلات Register File اسمًا يجمع جميع المسجلات الموجودة ضمن وحدة المعالجة المركزية، وتوجد ضمنه أجزاء وحدة المعالجة المركزية التي تنفّذ كل العمل، إذ تحمّل المعالجات أو تخزّن قيمةً في مسجل أو من مسجل إلى الذاكرة، أو تنفّذ بعض العمليات على القيم الموجودة في المسجلات كما قلنا سابقًا. تُعَدّ وحدة الحساب والمنطق Arithmetic Logic Unit -أو ALU اختصارًا- قلب عمليات وحدة المعالجة المركزية، إذ تأخذ القيم من المسجلات وتنفّذ أيًا من العمليات المتعددة التي تستطيع وحدة المعالجة المركزية تنفيذها، كما تحتوي جميع المعالجات الحديثة على عدد من وحدات ALU، بحيث يمكن لكل منها العمل بصورة مستقلة، وتحتوي المعالجات مثل المعالج بنتيوم Pentium على وحدات ALU سريعة ووحدات ALU بطيئة، إذ تكون الوحدات السريعة أصغر حجمًا، لذا يمكنك استخدام المزيد منها على وحدة المعالجة المركزية، ولكن يمكنك تنفيذ العمليات الأكثر شيوعًا فقط؛ أما وحدات ALU البطيئة، فيمكنها تنفيذ جميع العمليات ولكنها تكون أكبر حجمًا. تعالِج وحدة إنشاء العناوين Address Generation Unit -أو AGU اختصارًا- التواصل مع الذاكرة المخبئية Cache Memory والذاكرة الرئيسية لجلب القيم إلى المسجلات لكي تعمل وحدة ALU، ثم استعادة القيم من المسجلات إلى الذاكرة الرئيسية، كما تحتوي مسجلات الأعداد العشرية على المفاهيم نفسها، ولكنها تستخدِم مصطلحات مختلفةً قليلًا لمكوناتها. استخدام خط الأنابيب تُعَدّ عملية وحدة ALU التي تجمع قيم المسجلات منفصلةً تمامًا عن عملية وحدة AGU التي تكتب القيم في الذاكرة، إذ لا يوجد سبب يمنع وحدة المعالجة المركزية من تطبيق هاتين العمليتين معًا في وقت واحد، كما توجد عدة وحدات ALU في النظام والتي يمكن أن تعمل كل منها على تعليمات منفصلة. يمكن لوحدة المعالجة المركزية تنفيذ بعض عمليات الأعداد العشرية باستخدام منطق الأعداد العشرية أثناء تشغيل تعليمات الأعداد الصحيحة أيضًا، إذ تسمى هذه العملية باستخدام خط الأنابيب Pipelining، ويشار إلى المعالج الذي يمكنه تطبيق هذه العملية بأن له معمارية عددية فائقة Superscalar Architecture، إذ تُعَدّ جميع المعالجات الحديثة معالجات عدديةً فائقةً، ويحتوي أيّ معالج حديث على أكثر من أربع مراحل يمكنه استخدامها ضمن خط أنابيب، وكلما زاد عدد المراحل التي يمكن تنفيذها في الوقت نفسه، زاد عمق خط الأنابيب. يمكن تشبيه خط الأنابيب بأنبوب مملوء بكرات زجاجية، باستثناء أن هذه الكرات هي تعليمات وحدة المعالجة المركزية، إذ ستضع الكرات الزجاجية في نهاية واحدة، بحيث تضعها واحدةً تلو الأخرى -أي كرة لكل نبضة ساعة- حتى تملأ الأنبوب، وستنتقل كل كرة زجاجية -أو تعليمة- تدفعها للداخل إلى الموضع التالي بمجرد أن يمتلئ الأنبوب مع سقوط كرة في النهاية التي تمثّل النتيجة. تؤدي التعليمات الفرعية إلى إحداث فوضى في هذا النموذج، إذ يمكن أن تتسبب أو لا تتسبب في بدء التنفيذ من مكان مختلف، فإذا أردت استخدام خط الأنابيب، فسيتعين عليك تخمين الاتجاه الذي ستتجه فيه التعليمة الفرعية حتى تعرف التعليمات التي يجب إحضارها إلى خط الأنابيب، فإذا خمّنت وحدة المعالجة المركزية ذلك بصورة صحيحة، فسيسير كل شيء على ما يرام، إذ تستخدِم المعالجات مثل معالج بنتيوم ذاكرة تخزين مؤقت Trace Cache لتعقب مسار التعليمات الفرعية، حيث يمكن في كثير من الأحيان أن تخمّن الطريق الذي ستذهب إليه التعليمة الفرعية من خلال تذكر نتائجها السابقة، فإذا تذّكرتَ نتيجة التعليمة الفرعية الأخيرة في حلقة تتكرر 100 مرة مثلًا، فستكون على صواب 99 مرة، لأن المرة الأخيرة فقط ستستمر في البرنامج فعليًا؛ بينما إذا جرى تخمين المعالج بطريقة غير صحيحة، فهذا يعني أنّ المعالج قد أهدر كثيرًا من الوقت ويجب عليه مسح خط الأنابيب والبدء من جديد. يشار إلى هذه العملية عادةً باسم تفريغ خط الأنابيب Pipeline Flush وهي مماثلة للحاجة إلى التوقف وإفراغ كل الكرات من الأنبوب، كما تتكوّن عملية تخمين التعليمة الفرعية Branch Prediction من تفريغ خط الأنابيب وأخذ التخمين أو عدم الأخذ به وفتحات تأخير التعليمة الفرعية branch delay slots. إعادة الترتيب إذا كانت وحدة المعالجة المركزية هي الأنبوب، فسنكون لك الحرية في إعادة ترتيب الكرات ضمنه طالما أنها تخرج من نهايته بالترتيب نفسه الذي وضعتَها فيه، إذ نسمي ذلك بترتيب البرنامج Program Order لأنه ترتيب التعليمات المُعطَى في البرنامج الحاسوبي، كما يمكنك الاطلاع على المثال التالي الذي يمثل إعادة ترتيب المخزن المؤقت Buffer: 1: r3 = r1 * r2 2: r4 = r2 + r3 3: r7 = r5 * r6 4: r8 = r1 + r7 افترض مجرى التعليمات الموضح سابقًا، إذ يجب على التعليمة 2 انتظار اكتمال التعليمة 1 قبل أن تبدأ، وهذا يعني أنّ خط الأنابيب يجب عليه التوقف أثناء انتظار القيمة المراد حسابها، كما تعتمد التعليمتان 3 و 4 على قيمة r7، ولكن التعليمتان 2 و 3 لا تعتمدان على بعضهما البعض أبدًا، وهذا يعني أنهما يعملان في مسجلات منفصلة تمامًا، فإذا بدّلنا بين التعليمتين 2 و 3، فسنحصل على ترتيب أفضل لخط الأنابيب، إذ يمكن أن ينفّذ المعالج عملًا مفيدًا بدلًا من انتظار اكتمال خط الأنابيب للحصول على نتيجة التعليمة السابقة. يمكن أن تتطلب التعليمات بعض الأمان حول كيفية ترتيب العمليات عند كتابة شيفرة منخفضة المستوى، إذ نطلق على هذا المتطلب دلالات الذاكرة Memory Semantics، فإذا أردت اكتساب الدلالات Acquire Semantics، فهذا يعني أنه يجب عليك التأكد من إكمال نتائج جميع التعليمات السابقة للتعليمة الحالية، وإذا أردت تحرير الدلالات Release Semantics، فهذا يعني أنّ جميع التعليمات بعد هذه التعليمة يجب أن ترى النتيجة الحالية. توجد دلالات أخرى أكثر صرامة وهي حاجز الذاكرة Memory Barrier أو سور الذاكرة Memory Fence الذي يتطلب أن تكون العمليات مرتبطةً بالذاكرة قبل المتابعة، كما يضمن المعالج هذه الدلالات في بعض المعماريات، بينما يجب أن تحددها بصورة صريحة في المعماريات الأخرى، ولا يحتاج معظم المبرمجين إلى القلق بشأنها على الرغم من أنك قد تصادفها. معمارية CISC ومعمارية RISC يمكن تقسيم معماريات الحاسوب إلى معمارية حاسوب مجموعة التعليمات المعقدة Complex Instruction Set Computer -أو CISC اختصارًا- ومعمارية حاسوب مجموعة التعليمات المُخفَّضة Reduced Instruction Set Computer أو RISC اختصارًا. لاحظ أننا في المثال الأول من مقالنا حمّلنا القيم صراحةً في المسجلات وأجرينا عملية الجمع، ثم خزّنا القيمة الناتجة المحفوظة في مسجل آخر في الذاكرة، إذ يُعَدّ ذلك مثالًا عن نهج RISC للحوسبة الذي يشمل تنفيذ العمليات على القيم الموجودة في المسجلات وتحميل القيم وتخزينها بصورة صريحة من الذاكرة وإليها، كما يمكن أن يكون نهج CISC مجرد تعليمات مفردة تأخذ قيمًا من الذاكرة وتنفذ عملية الجمع داخليًا ثم تكتب النتيجة، وهذا يعني أنّ التعليمات يمكن أن تستغرق عدة دورات، ولكن كلا النهجين يحققان في النهاية الهدف نفسه. تُعَدّ جميع المعماريات الحديثة معماريات RISC حتى معمارية إنتل بنتيوم Intel Pentium الأكثر شيوعًا والتي تهدم التعليمات داخليًا إلى تعليمات فرعية بأسلوب RISC داخل الشريحة قبل التنفيذ، بالرغم من وجود مجموعة تعليمات مصنَّفة على أنها CISC، وهناك عدة أسباب لذلك وهي: تجعل معمارية RISC البرمجة بلغة التجميع Assembly أكثر تعقيدًا، نظرًا لأن جميع المبرمجين تقريبًا يستخدِمون لغات عالية المستوى ويتركون العمل الشاق لإنتاج شيفرة التجميع للمصرّف Compiler، وبالتالي ستتفوق المزايا الأخرى على هذا العيب. بما أنّ التعليمات الموجودة في معالج RISC أبسط، فهناك مساحة أكبر ضمن شريحة المسجلات، إذ تُعَدّ المسجلات أسرع أنواع الذواكر كما نعلم من تسلسل الذواكر الهرمي، ويجب في النهاية تنفيذ جميع التعليمات على القيم المحفوظة في المسجلات، لذا ستؤدي زيادة عدد المسجلات إلى أداء أعلى عند تكافؤ جميع الأشياء الأخرى. بما أنّ جميع التعليمات تُنفَّذ في الوقت نفسه، فسيكون استخدام خطوط الأنابيب ممكنًا، وكما نعلم أنّ استخدام خط الأنابيب يتطلب تدفقات من التعليمات باستمرار إلى المعالج، لذلك إذا استغرقت بعض التعليمات وقتًا طويلًا جدًا دون أن تتطلب التعليمات الأخرى ذلك، فسيصبح خط الأنابيب معقدًا ليكون فعّالًا. معمارية EPIC يُعَدّ معالج إيتانيوم Itanium مثالًا على معمارية معدَّلة تسمى الحوسبة الصريحة للتعليمات الفرعية Explicitly Parallel Instruction Computing. ناقشنا سابقًا كيف أنّ المعالجات الفائقة لها خطوط أنابيب بها العديد من التعليمات في الوقت نفسه ضمن أجزاء مختلفة من المعالج، إذ يمكن تحقيق ذلك من خلال إعطاء التعليمات للمعالج بالترتيب الذي يمكن أن يحقق أفضل استفادة من العناصر المتاحة في وحدة المعالجة المركزية، وقد كان تنظيم مجرى التعليمات الواردة تقليديًا مهمة العتاد، إذ يصدر البرنامج التعليمات بطريقة تسلسلية، ويجب أن ينظر المعالج إلى الأمام ويحاول اتخاذ قرارات حول كيفية تنظيم التعليمات الواردة. الفكرة وراء معمارية EPIC هي أنّ هناك مزيد من المعلومات المتاحة على مستويات أعلى والتي يمكن أن تجعل هذه القرارات أفضل مما يفعله المعالج، ويؤدي تحليل مجرًى من تعليمات لغة التجميع -كما تفعل المعالجات الحالية- إلى فقدان الكثير من المعلومات التي قدّمها المبرمج في الشيفرة البرمجية الأصلية. فكر في الأمر على أنه الفرق بين دراسة مسرحية لشكسبير وقراءة نسخة ملاحظات الجرف Cliff's Notes من المسرحية نفسها، فكلاهما يمنحك النتيجة نفسها، ولكن النسخة الأصلية تحتوي على جميع أنواع المعلومات الإضافية التي تحدد المشهد وتعطيك فهمًا جيدًا للشخصيات، وبالتالي يمكن نقل منطق ترتيب التعليمات من المعالج إلى المصرّف، وهذا يعني أنّ مطوِّري المصرّفات يجب أن يكونوا أذكى في محاولة العثور على أفضل ترتيب للشيفرة البرمجية للمعالج، كما يجب تبسيط المعالج كثيرًا، إذ نُقِل الكثير من عمله إلى المصرِّف. يوجد مصطلح آخر غالبًا ما يُستخدَم مع معمارية EPIC وهو عالم التعليمات الطويلة جدًا Very Long Instruction World -أو VLIW اختصارًا-، إذ تُوسَّع كل تعليمة للمعالج لإخباره بالمكان الذي يجب أن ينفّذ فيه التعليمة في وحداته الداخلية، وتكمن مشكلة هذا الأسلوب في أنّ الشيفرة البرمجية تعتمد كليًا على طراز المعالج الذي صُرِّفت الشيفرة البرمجية من أجله، كما تُجري الشركات دائمًا مراجعات على العتاد، وتجعل العملاء يعيدون تصريف تطبيقاتهم في كل مرة، مما جعل صيانة مجموعة من الشيفرات البرمجية الثنائية المختلفة أمرًا غير عملي. تحل معمارية EPIC هذه المشكلة بطريقة علوم الحاسوب المعتادة من خلال إضافة طبقة من التجريد، كما تنشئ معمارية EPIC عرضًا مبسطًا مع بعض الوحدات الأساسية مثل الذاكرة ومسجّلات الأعداد الصحيحة والعشرية بدلًا من التحديد الصريح للجزء الدقيق من المعالج الذي يجب أن تنفّذ التعليمات عليه. ترجمة -وبتصرُّف- للقسم The CPU من الفصل Computer Architecture من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال التالي: نظرة عميقة على تسلسل الذواكر الهرمي والذاكرة المخبئية في معمارية الحاسوب المقال السابق: تمثيل الأنواع والأعداد في الأنظمة الحاسوبية وحدة المعالجة المركزية المدخل الشامل لتعلم علوم الحاسوب
  5. اقتربنا من نهاية سلسلة إطار العمل Vue.js، إذ يجب الآن تعلّم كيفية الوصول إلى العناصر وإدارتها وتعديلها مثل إدارة التركيز أو كيف يمكننا تحسين الشمولية أو إمكانية الوصول لمستخدمي لوحة المفاتيح في تطبيقنا، إذ سنتعرّف على كيفية استخدام خاصيات ref في إطار العمل Vue للتعامل مع إدارة التركيز التي تُعَدّ ميزةً متقدمةً تتيح الوصول المباشر إلى عقد DOM الأساسية أسفل نموذج DOM الافتراضي أو الوصول المباشر من أحد المكوّنات إلى بنية DOM الداخلية الخاصة بمكوِّن ابن، كما سنوفِّر مزيدًا من الموارد لتعلّم إطار عمل Vue للاستزادة منها. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ تُكتَب مكوّنات Vue بوصفها مجموعةً من كائنات جافاسكربت التي تدير بيانات التطبيق وصيغة القوالب المستنِدة إلى لغة HTML المرتبطة مع بنية DOM الأساسية، كما ستحتاج إلى طرفية مثبَّتٌ عليها node و npm لتثبيت Vue ولاستخدام بعض ميزاته الأكثر تقدمًا مثل مكونات الملف المفرد Single File Components أو دوال التصيير Render. الهدف: تعلّم كيفية التعامل مع إدارة التركيز باستخدام خاصيات ref في إطار العمل Vue. مشكلة إدارة التركيز لدينا وظيفة تعديل تعمل بصورة جيدة في تطبيقنا، إلا أننا لم نقدّم تجربةً رائعةً للمستخدِمين الذين لا يستخدِمون الفأرة، فإذا فعّل المستخدِم زر "التعديل Edit"، فإننا نزيل هذا الزر من نموذج DOM دون نقل تركيز المستخدِم إلى أيّ مكان آخر، لذلك سيختفي التركيز، ويمكن أن يكون هذا مربكًا لمستخدِمي لوحة المفاتيح والمستخدِمين غير المبصرين. طبقّ ما يلي لفهم ما يحدث حاليًا: أعد تحميل صفحتك ثم اضغط على مفتاح Tab، ويجب أن تشاهد إطارًا يمثل التركيز حول حقل الإدخال لإضافة عناصر مهام جديدة. اضغط على مفتاح Tab مرةً أخرى، ويجب أن ينتقل التركيز إلى زر "الإضافة Add". اضغط على مفتاح Tab مرةً أخرى وسينتقل التركيز إلى مربع الاختيار الأول، ثم يجب أن ينتقل التركيز إلى زر "التعديل Edit" الأول. فعّل زر "التعديل Edit" بالضغط على مفتاح Enter، وبالتالي سيُستبدَل مربع الاختيار بمكوِّن التعديل، ولكن سيختفي إطار التركيز. يمكن أن يكون هذا السلوك مربكًا، ويختلف ما يحدث عند الضغط على مفتاح Tab مرةً أخرى تبعًا للمتصفح الذي تستخدِمه، فإذا حفظت التعديل أو ألغيته، فسيختفي التركيز مرةً أخرى عندما تعود إلى العرض الذي لا يتضمن تعديلًا. يمكن منح المستخدِمين تجربةً أفضل من خلال إضافة شيفرة برمجية للتحكّم في التركيز بحيث يُضبَط على حقل التعديل عند عرض نموذج التعديل، وسنعيد وضع التركيز على زر "التعديل Edit" عندما يلغي المستخدِم تعديله أو يحفظه، وبالتالي يجب فهم المزيد حول كيفية عمل إطار Vue داخليًا بهدف ضبط التركيز. نموذج DOM الافتراضي وخاصيات ref يستخدِم إطار العمل Vue مثل بعض أطر العمل الأخرى نموذج DOM الافتراضي Virtual DOM -أو VDOM اختصارًا- لإدارة العناصر، وهذا يعني أنّ إطار Vue يحتفظ في الذاكرة بتمثيلٍ لجميع العقد في تطبيقنا، كما يمكن إجراء أيّ تحديثات أولًا على العقد الموجودة في الذاكرة، ثم تُزامَن جميع التغييرات التي يجب إجراؤها على العقد الفعلية على الصفحة دفعةً واحدةً. بما أن قراءة وكتابة عقد DOM الفعلية تكون مستهلكة أكثر للأداء من العقد الافتراضية في أغلب الأحيان، فسيؤدي ذلك إلى أداء أفضل، ولكنه يعني أيضًا أنه لا يجب تعديل عناصر HTML مباشرةً من خلال واجهات برمجة تطبيقات المتصفح الأصيلة Native مثل Document.getElementById عند استخدام أطر العمل، لأنه سيؤدي إلى عدم مزامنة نموذج VDOM و DOM الفعلي، فإذا كنت بحاجة إلى الوصول إلى عقد DOM الأساسية مثل حالة ضبط التركيز، فيمكنك استخدام خاصيات ref في إطار Vue، كما يمكنك بالنسبة لمكّونات Vue المخصَّصة استخدام خاصيات ref للوصول مباشرةً إلى البنية الداخلية لمكوّن ابن، ولكن يجب تطبيق ذلك بحذر لأنه قد يصعّب فهم الشيفرة البرمجية. يمكن استخدام خاصيات ref في أحد المكونات من خلال إضافة السمة ref إلى العنصر الذي تريد الوصول إليه مع معرِّف من نوع سلسلة نصية لقيمة هذه السمة، إذ يجب أن تكون السمة ref فريدةً في المكون، ولا ينبغي أن يكون هناك عنصران يصيّّران في الوقت نفسه ولهما السمة ref نفسها. إضافة الخاصية ref إلى التطبيق لنضِف السمة ref إلى زر "التعديل Edit" في المكوِّن ToDoItem.vue كما يلي: <button type="button" class="btn" ref="editButton" @click="toggleToItemEditForm"> Edit <span class="visually-hidden">{{label}}</span> </button> يمكن الوصول إلى القيمة المرتبطة بالسمة ref من خلال استخدام الخاصية ‎$refs المتوفِّرة في نسخة المكوِّن، لذا أضِف التابع console.log()‎ إلى التابع toggleToItemEditForm()‎ كما يلي لمعرفة قيمة السمة ref عند النقر على زر "التعديل Edit": toggleToItemEditForm() { console.log(this.$refs.editButton); this.isEditing = true; } إذا فعّلتَ زر "التعديل Edit" الآن، فيُفترَض أن ترى عنصر الزر <button> في HTML مشارًا إليه في طرفيتك. التابع ‎$nextTick()‎ في إطار العمل Vue يجب الآن التركيز على زر "التعديل Edit" عندما يحفظ المستخدِم التعديل أو يلغيه، لذلك يجب التعامل مع التركيز باستخدام التابعَين itemEdited()‎ و editCancelled()‎ في المكوِّن ToDoItem. أنشئ تابعًا جديدًا لا يأخذ أيّ وسيط بالاسم focusOnEditButton()‎ وأسند السمة ref إلى متغير، ثم استدعِ التابع focus()‎ على هذه السمة. focusOnEditButton() { const editButtonRef = this.$refs.editButton; editButtonRef.focus(); } أضِف بعد ذلك استدعاءً إلى this.focusOnEditButton()‎ في نهاية التابعَين itemEdited()‎ و editCancelled()‎ كما يلي: itemEdited(newItemName) { this.$emit("item-edited", newItemName); this.isEditing = false; this.focusOnEditButton(); }, editCancelled() { this.isEditing = false; this.focusOnEditButton(); }, جرّب تعديل عنصر مهمة ثم حفظ التعديل أو إلغاءه باستخدام لوحة المفاتيح وستلاحظ عدم ضبط التركيز، لذلك لا تزال لدينا مشكلة ويجب حلها، فإذا فتحتَ طرفيتك، فسترى ظهور الخطأ الذي يبدو غريبًا: "can't access property "focus", editButtonRef is undefined" إذ جرى تعريف المرجع ref الخاص بالزر عند تفعيل زر "التعديل Edit"، ولكنه ليس كذلك الآن، وتذكّر أننا لم نعُد نصيّر قسم المكوّن الذي يحتوي على زر "التعديل Edit" عند تغيير قيمة isEditing إلى true، وبالتالي لا يوجد عنصر لربط المرجع به، لذلك يصبح غير مُعرَّف. لكننا نضبط قيمة isEditing على false قبل محاولة الوصول إلى المرجع، فهل يجب أن يعرض الموجّه v-if الزر الآن؟ حسنًا، يجب الآن استخدام نموذج DOM الافتراضي، وبما أنّ Vue يحاول تحسين وإصلاح التغييرات، فلن يحدّث نموذج DOM مباشرةً عند ضبط قيمة isEditing على false، لذلك فإنّ زر "التعديل Edit" لم يُصيَّر بعد عند استدعاء التابع focusOnEdit()‎، لذا يجب الانتظار حتى يجتاز إطار Vue دورة تحديث نموذج DOM التالية، إذ تحتوي مكونات Vue على تابع خاص يسمَّى ‎$nextTick()‎ الذي يقبل دالة رد نداء Callback Function تنفّذ بعد تحديث نموذج DOM. يمكننا تغليف جسم الدالة الحالية ضمن استدعاء الدالة ‎$nextTick()‎ لأنه يجب استدعاء التابع focusOnEditButton()‎ بعد تحديث نموذج DOM. focusOnEditButton() { this.$nextTick(() => { const editButtonRef = this.$refs.editButton; editButtonRef.focus(); }); } إذا فعّلتَ زر "التعديل Edit" ثم ألغيت التغييرات أو حفظتها باستخدام لوحة المفاتيح، فيجب إعادة التركيز إلى زر "التعديل Edit". توابع دورة حياة إطار عمل Vue تتمثَّل الخطوة التالية في نقل التركيز إلى العنصر <input> في نموذج التعديل عند النقر على زر "التعديل Edit"، ولكن لا يمكننا فقط وضع التركيز ضمن معالِج حدث النقر على زر "التعديل Edit" لأن نموذج التعديل موجود في مكوِّن مختلف عن هذا الزر، لذا يمكننا إزالة المكوِّن ToDoItemEditForm ونعيد تحميله كلما نقرنا على زر "التعديل Edit" للتعامل مع هذا الأمر. تجتاز مكوّنات Vue سلسلةً من الأحداث تُعرَف باسم دورة الحياة Lifecycle التي تمتد من قبل إنشاء العناصر وإضافتها إلى نموذج VDOM أي تثبيتها، حتى إزالتها من نموذج VDOM أي تدميرها. يتيح إطار عمل Vue تشغيل توابع في مراحل مختلفة من دورة الحياة باستخدام توابع دورة الحياة Lifecycle Methods المفيدة لأشياء متعددة مثل جلب البيانات، إذ يمكن أن تحتاج إلى الحصول على بياناتك قبل تصيير مكوِّنك أو بعد تغيير الخاصيات، وتمثِّل القائمة التالية قائمة توابع دورة الحياة حسب الترتيب الذي تُطلَق فيه: beforeCreate()‎: يُشغَّل قبل إنشاء نسخة من مكوِّنك، وليست البيانات والأحداث متاحةً بعد. created()‎: يُشغَّل بعد تهيئة مكوِّنك ولكن قبل إضافته إلى نموذج VDOM، ويُعَدّ المكان الذي يحدث فيه جلب البيانات. beforeMount()‎: يُشغَّل بعد تصريف Compile قالبك وقبل تصيير مكوِّنك إلى نموذج DOM الفعلي. mounted()‎: يُشغَّل بعد تثبيت مكوِّنك في نموذج DOM، ويمكن هنا الوصول إلى المراجع refs. beforeUpdate()‎: يُشغَّل كلما تغيرت البيانات في مكوِّنك وقبل تصيير التغييرات إلى نموذج DOM. updated()‎: يُشغَّل كلما تغيرت البيانات في مكوِّنك وبعد تصيير التغييرات إلى نموذج DOM. beforeDestroy()‎: يُشغَّل قبل إزالة أحد المكوّنات من نموذج DOM. destroyed()‎: يُشغَّل بعد إزالة أحد المكونات من نموذج DOM. activated()‎: يُستخدَم فقط في المكونات المغلَّفة بوسم keep-alive الخاص، ويعمل بعد تفعيل المكون. deactivated()‎: يُستخدَم فقط في المكونات المغلَّفة بوسم keep-alive الخاص، ويعمل بعد إلغاء تفعيل المكون. ملاحظة: يمكنك الاطلاع على مخطط رائع يشرح وقت حدوث هذه التوابع في توثيق Vue. لنستخدِم الآن تابعًا من توابع دورة الحياة لبدء التركيز عند تثبيت المكوِّن ToDoItemEditForm، لذا أضِف ref="labelInput"‎ إلى العنصر <input> في المكوِّن ToDoItemEditForm.vue كما يلي: <input :id="id" ref="labelInput" type="text" autocomplete="off" v-model.lazy.trim="newName" /> أضف بعد ذلك الخاصية mounted()‎ إلى كائن المكوِّن مباشرةً، إذ لا ينبغي وضعها ضمن الخاصية methods، وإنما يجب وضعها في مستوى تسلسل props و data()‎ و methods الهرمي نفسه، فتوابع دورة الحياة هي توابع خاصة بمفردها ولا توضَع مع التوابع التي يعرّفها المستخدِم ولا تأخذ أيّ مدخلات، ولاحظ أنه لا يمكنك استخدام دالة سهمية هنا لأننا نحتاج الوصول إلى this للوصول إلى المرجع labelInput. mounted() { } أسنِد المرجع labelInput إلى متغير ضمن التابع mounted()‎، ثم استدعِ الدالة focus()‎ الخاصة بالمرجع، ولست مضطرًا إلى استخدام ‎$nextTick هنا لأن المكوِّن مُضاف بالفعل إلى نموذج DOM عند استدعاء التابع mounted()‎. mounted() { const labelInputRef = this.$refs.labelInput; labelInputRef.focus(); } إذا فعّلتَ زر "التعديل Edit" الآن باستخدام لوحة المفاتيح، فيجب أن ينتقل التركيز إلى تعديل العنصر <input> مباشرةً. التعامل مع التركيز عند حذف عناصر المهام إذا نقرت على زر "التعديل Edit"، فسيكون نقل التركيز إلى مربع نص تعديل الاسم والعودة إلى زر "التعديل Edit" عند الإلغاء أو الحفظ من نموذج التعديل أمرًا منطقيًا، لكن ليس لدينا موقع واضح لنقل التركيز إليه عند حذف عنصر، ونحتاج إلى طريقة لتزويد مستخدِمي التقنيات المساعدة بالمعلومات التي تؤكّد حذف عنصر. نتعقّب فعليًا عدد العناصر في عنوان القائمة أي العنصر <h2> في المكوِّن App.vue، وهو مرتبط بقائمة عناصر المهام، مما يجعله مكانًا مناسبًا لنقل التركيز إليه عند حذف عقدة. يجب أولًا إضافة مرجع إلى عنوان القائمة ثم إضافة السمة tabindex="-1"‎ إليه، مما يجعل العنصر قابلًا للتركيز برمجيًا، أي يمكن التركيز عليه باستخدام شيفرة جافاسكربت إن لم يكن كذلك افتراضيًا. عدِّل العنصر <h2> في المكوِّن App.vue كما يلي: <h2 id="list-summary" ref="listSummary" tabindex="-1">{{listSummary}}</h2> ملاحظة: تُعَدّ السمة tabindex أداةً قويةً للتعامل مع بعض مشاكل الشمولية Accessibility، ولكن يجب استخدامها بحذر، إذ يمكن أن يتسبّب الإفراط في استخدام السمة tabindex="-1"‎ في حدوث مشاكل لجميع أنواع المستخدِمين، لذلك استخدمها فقط في المكان الذي تحتاجها فيه فقط، كما يجب عدم استخدام السمة tabindex > = 0، إذ يمكن أن تسبب مشاكل للمستخدِمين لأنها يمكن أن تؤدي إلى عدم التطابق مع ترتيب انتقال التركيز باستخدام مفتاح Tab في نموذج DOM، و / أو إضافة عناصر غير تفاعلية إلى هذا الترتيب، كما يكون ذلك مربكًا للمستخدِمين خاصةً أولئك الذين يستخدِمون قارئات الشاشة والتقنيات المساعدة الأخرى. أصبح لدينا مرجع ref وأعلَمنا المتصفحات أنه يمكننا التركيز برمجيًا على العنصر <h2>، ويجب الآن التركيز عليه. استخدم المرجع listSummary في نهاية التابع deleteToDo()‎ لضبط التركيز على العنصر <h2>، وبما أنّ العنصر <h2> يُصيَّر دائمًا في التطبيق، فليس هناك داع للقلق بشأن استخدام ‎$nextTick مع توابع دورة الحياة للتعامل مع التركيز عليه. deleteToDo(toDoId) { const itemIndex = this.ToDoItems.findIndex(item => item.id === toDoId); this.ToDoItems.splice(itemIndex, 1); this.$refs.listSummary.focus(); } إذا حذفتَ عنصرًا من قائمتك الآن، فيجب نقل التركيز إلى عنوان القائمة الذي يجب أن يوفِّر تجربة تركيز جيدة لجميع المستخِدمين. تهانينا، انتهينا الآن من إنشاء تطبيقنا باستخدام إطار العمل Vue، وسنتعرّف الآن على بعض الموارد الإضافية لتعلم إطار العمل Vue. ملاحظة: إذا كنت بحاجة إلى التحقق من شيفرتك مقابل نسختنا، فيمكنك العثور على نسخة نهائية من شيفرة تطبيق Vue في مستودع todo-vue، كما يمكنك الحصول على إصدار حي مباشر قيد التشغيل. موارد إضافية لتعلم إطار العمل Vue سنختتم الآن هذا القسم من سلسلة تعلم تطوير الويب الذي يشمل إطار العمل Vue من خلال إعطائك قائمة بالموارد التي يمكنك استخدامها في مسيرة تعلمك، بالإضافة إلى بعض النصائح المفيدة الأخرى. من الموارد التي يجب أن تتطلع عليها لمعرفة المزيد عن إطار العمل Vue: كتاب أساسيات إطار العمل Vue .js من أكاديمية حسوب الذي يشرح مفهوم إطار العمل Vue. مقالات عن Vue: مقالات عربية من أكاديمية حسوب عن إطار العمل Vue.js. قسم الأسئلة البرمجية: إن أردت الحصول على مساعدة من مبرمجين عرب. منتدى البرمجة العربية: المنتدى البرمجي العربي للحصول على مساعدة في إطار العمل Vue أو أي شيء متعلق بالبرمجة. توثيق Vue: يحتوي موقع Vue الرسمي على توثيق شامل بما في ذلك الأمثلة والكتب والموارد المرجعية. مستودع Vue Github: شيفرة Vue البرمجية نفسها حيث يمكنك فيه الإبلاغ عن المشاكل والمساهمة مباشرةً في شيفرة Vue الأساسية، كما يمكن أن تساعدك دراسة شيفرة Vue البرمجية على فهم كيفية عمل إطار العمل وكتابة شيفرة أفضل. توثيق Vue CLI: يحتوي على معلومات حول تخصيص وتوسيع الخرج الذي تنشئه باستخدام واجهة سطر الأوامر CLI. NuxtJS: هو إطار عمل من Vue من طرف الخادم مع بعض الآراء المعمارية التي يمكن أن تكون مفيدةً لإنشاء تطبيقات قابلة للصيانة، حتى إن لم تستخدِم أيًا من ميزات التصيير من طرف الخادم Server Side Rendering التي يوفرها، كما يوفِّر توثيقًا مفصلًا حول استخدام NuxtJS. بناء ونشر تطبيق Vue توفِّر واجهة CLI في Vue أدوات لإعداد تطبيقنا للنشر على الويب كما يلي: إذا كان خادمك المحلي لا يزال قيد التشغيل، فيمكن إنهاؤه بالضغط على الاختصار Ctrl + C في الطرفية. شغّل الأمر npm run build أو الأمر yarn build في الطرفية. مما يؤدي إلى إنشاء مجلد dist جديد يحتوي على جميع ملفات الإنتاج الجاهزة. يمكنك نشر موقعك على الويب من خلال نسخ محتويات هذا المجلد إلى بيئة استضافتك. ملاحظة: يتضمن توثيق Vue CLI دليلًا حول كيفية نشر تطبيقك على العديد من أنظمة الاستضافة الشائعة. إصدار Vue 3 يُعَدّ Vue 3 إصدارًا رئيسيًا من Vue مع الكثير من التغييرات الرئيسية، وقد دخل هذا الإصدار مرحلة الإصدار التجريبي النشط في شهر 4 من عام 2020، كما يُعَدّ أكبر تغيير فيه هو واجهة Composition API الجديدة التي تعمل بوصفها بديلًا لواجهة API الحالية القائمة على الخاصيات، وتُستخدَم دالة setup()‎ واحدة مع المكوِّن في هذه الواجهة الجديدة، حيث يتوفر فقط ما تعيده من هذه الدالة في عناصر القوالب <template>. كما يجب أن تكون واضحًا بشأن الخاصيات التفاعلية عند استخدام هذه الواجهة، إذ يعالج إطار العمل Vue ذلك نيابةً عنك باستخدام الواجهة Options API، مما يجعل واجهة برمجة التطبيقات الجديدة حالة استخدام أكثر تقدمًا. كما توجد بعض التغييرات الأخرى، بما في ذلك التغيير في كيفية تهيئة التطبيقات في Vue، ويمكنك الاطلاع عليها في توثيق Vue.js الرسمي. ترجمة -وبتصرّف- للمقالَين Focus management with Vue refs وVue resources. اقرأ أيضًا المقال السابق: العرض الشرطي في إطار العمل Vue.js إضافة تنسيق للمكونات واستعمال الخاصية computed في تطبيق Vue.js استخدام Vue.js للتعامل مع DOM
  6. حان الوقت الآن لإضافة الوظائف التي تمكّننا من تعديل عناصر المهام الموجودة مسبقًا، لذلك سنستفيد من إمكانات التصيير الشرطي Conditional Rendering في إطار العمل Vue مثل v-if و v-else للسماح بالتبديل بين عرض عناصر المهام الموجودة مسبقًا وعرض التعديل حيث يمكنك تعديل عناوين أو تسميات labels عناصر المهام، كما سنتعرّف على إضافة وظيفة لحذف عناصر المهام. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ تُكتَب مكونات Vue بوصفها مجموعةً من كائنات جافاسكربت التي تدير بيانات التطبيق وصيغة القوالب المستنِدة إلى لغة HTML المرتبطة مع بنية DOM الأساسية، كما ستحتاج إلى طرفية مثبَّت عليها node و npm لتثبيت Vue ولاستخدام بعض ميزاته الأكثر تقدمًا مثل مكوّنات الملف المفرد Single File Components أو دوال التصيير Render. الهدف: تعلّم كيفية استخدام التصيير الشرطي في إطار العمل Vue. إنشاء مكون التعديل يمكننا البدء بإنشاء مكوِّن منفصل للتعامل مع وظيفة التعديل، لذا أنشئ ملفًا جديدًا بالاسم ToDoItemEditForm.vue في المجلد components وانسخ الشيفرة التالية في هذا الملف: <template> <form class="stack-small" @submit.prevent="onSubmit"> <div> <label class="edit-label">Edit Name for "{{label}}"</label> <input :id="id" type="text" autocomplete="off" v-model.lazy.trim="newLabel" /> </div> <div class="btn-group"> <button type="button" class="btn" @click="onCancel"> Cancel <span class="visually-hidden">editing {{label}}</span> </button> <button type="submit" class="btn btn__primary"> Save <span class="visually-hidden">edit for {{label}}</span> </button> </div> </form> </template> <script> export default { props: { label: { type: String, required: true }, id: { type: String, required: true } }, data() { return { newLabel: this.label }; }, methods: { onSubmit() { if (this.newLabel && this.newLabel !== this.label) { this.$emit("item-edited", this.newLabel); } }, onCancel() { this.$emit("edit-cancelled"); } } }; </script> <style scoped> .edit-label { font-family: Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: #0b0c0c; display: block; margin-bottom: 5px; } input { display: inline-block; margin-top: 0.4rem; width: 100%; min-height: 4.4rem; padding: 0.4rem 0.8rem; border: 2px solid #565656; } form { display: flex; flex-direction: row; flex-wrap: wrap; } form > * { flex: 0 0 100%; } </style> ملاحظة: تصفَّح الشيفرة السابقة ثم اقرأ الوصف التالي للتأكد من فهمك لكل شيء يطبّقه المكوِّن قبل المضي قدمًا، إذ تُعَدّ هذه الطريقة مفيدةً للمساعدة في تعزيز ما تعلمته حتى الآن. تضبط الشيفرة السابقة أساس وظيفة التعديل، إذ ننشئ نموذجًا يحتوي على حقل إدخال <input> لتعديل اسم المهمة، ويوجد زر "حفظ Save" وزر "إلغاء Cancel": إذا نقرت على زر "الحفظ Save"، فسيصدِر المكوِّن التسمية الجديدة باستخدام الحدث item-edited. إذا نقرت على زر "الإلغاء Cancel"، فسيشير المكوِّن إلى ذلك عن طريق إصدار الحدث edit-cancelled. تعديل المكون ToDoItem يجب إجراء بعض التعديلات على المكوِّن ToDoItem قبل التمكّن من إضافة المكوِّن ToDoItemEditForm إلى تطبيقنا، إذ يجب إضافة متغير لتعقّب تعديل العنصر، وزر لتبديل هذا المتغير، كما سنضيف زر حذف Delete لأن الحذف وثيق الصلة بالتعديل، أي عدّل قالب المكون ToDoItem كما يلي: <template> <div class="stack-small"> <div class="custom-checkbox"> <input type="checkbox" class="checkbox" :id="id" :checked="isDone" @change="$emit('checkbox-changed')" /> <label :for="id" class="checkbox-label">{{label}}</label> </div> <div class="btn-group"> <button type="button" class="btn" @click="toggleToItemEditForm"> Edit <span class="visually-hidden">{{label}}</span> </button> <button type="button" class="btn btn__danger" @click="deleteToDo"> Delete <span class="visually-hidden">{{label}}</span> </button> </div> </div> </template> أضفنا عنصر <div> الذي يغلّف القالب بأكمله لأغراض التنسيق، وأضفنا زرَّي "تعديل Edit" و "حذف Delete": إذا نقرت على زر "التعديل Edit"، فسيبدّل عرض مكوِّن ToDoItemEditForm لنتمكن من استخدامه لتعديل عنصر المهام باستخدام دالة معالج حدث تسمى toggleToItemEditForm()‎ التي ستضبط الراية isEditing على القيمة true، لكن يجب أولًا تعريف هذه الدالة ضمن الخاصية data()‎. إذا نقرت على زر "الحذف Delete"، فسيُحذَف عنصر المهام باستخدام دالة معالج حدث تسمى deleteToDo()‎، إذ سنصدِر في هذا المعالج الحدث item-deleted إلى المكوِّن الأب، مما يؤدي إلى تحديث القائمة. لنعرِّف الآن معالجات النقرات والراية isEditing، لذا أضِف الخاصية isEditing بعد الخاصية isDone كما يلي: data() { return { isDone: this.done, isEditing: false }; } أضِف الآن توابعك ضمن الخاصية methods وبعد الخاصية data()‎ مباشرةً: methods: { deleteToDo() { this.$emit('item-deleted'); }, toggleToItemEditForm() { this.isEditing = true; } } عرض المكونات شرطيا باستخدام v-if و v-else لدينا الآن الراية isEditing التي يمكننا استخدامها للإشارة إلى أن العنصر مُعدَّل أم لا، فإذا كانت للراية isEditing القيمة true، فيجب استخدام هذه الراية لعرض المكوِّن ToDoItemEditForm بدلًا من مربع الاختيار، إذ سنستخدِم الموجِّه v-if في إطار العمل Vue. لن يصيِّر الموجِّه v-if كتلةً ما إلا إذا كانت القيمة المُمرَّرة إليه true، وهذا مشابه لكيفية عمل التعليمة if في لغة جافاسكربت، إذ يحتوي الموجّه v-if على موجّهات مقابلة هي v-else-if و v-else لتوفير ما يعادلها في لغة جافاسكربت مثل else if و else ضمن قوالب Vue. يجب أن تكون كتل v-else و v-else-if الشقيق الأول لكتل v-if و v-else-if، وإلا فلن يتعرَّف Vue عليها، كما يمكنك استخدام الموجّه v-if مع الوسم <template> إذا كنت بحاجة إلى تصيير قالب كامل شرطيًا. أخيرًا، يمكنك استخدام الموجّهَين v-if و v-else مع جذر المكوِّن لعرض إحدى الكتل فقط أو الكتلة الأخرى، لأنّ Vue لن يصيّر سوى كتلةً واحدةً من هذه الكتل في كل مرة، لذا سنطبّق ذلك في تطبيقنا مما يسمح باستبدال الشيفرة التي تعرض عنصر المهمة في نموذج التعديل. أضِف v-if="!isEditing"‎ إلى عنصر <div> الجذر في المكون ToDoItem كما يلي: <div class="stack-small" v-if="!isEditing"> أضف بعد ذلك السطر التالي بعد وسم إغلاق <div>: <to-do-item-edit-form v-else :id="id" :label="label"></to-do-item-edit-form> كما يجب استيراد المكوِّن ToDoItemEditForm وتسجيله لنتمكّن من استخدامه ضمن هذا القالب، لذا أضف السطر التالي قبل العنصر <script>: import ToDoItemEditForm from "./ToDoItemEditForm"; أضِف الخاصية components قبل الخاصية props ضمن كائن المكوِّن: components: { ToDoItemEditForm }, إذا انتقلتَ الآن إلى تطبيقك ونقرت على زر "تعديل Edit" عنصر المهام، فيجب أن ترى مربع الاختيار مستبدَلًا بنموذج التعديل. لكن لا توجد طريقة حاليًا للعودة، لذلك يجب إضافة مزيد من معالجات الأحداث إلى المكوِّن. الرجوع من وضع التعديل يجب أولًا إضافة التابع itemEdited()‎ إلى الخاصية methods في المكوِّن ToDoItem، إذ يأخذ هذا التابع عنوان label العنصر الجديد بوصفه وسيطًا ويرسل الحدث itemEdited إلى المكوِّن الأب ويضبط isEditing على القيمة false، لذا أضِف هذا التابع الآن بعد التوابع الحالية كما يلي: itemEdited(newLabel) { this.$emit('item-edited', newLabel); this.isEditing = false; } سنحتاج بعد ذلك إلى التابع editCancelled()‎، ولا يأخذ هذا التابع أي وسائط ويعمل فقط على ضبط isEditing مرةً أخرى على القيمة false، لذا أضِف هذا التابع بعد التابع السابق: editCancelled() { this.isEditing = false; } أخيرًا، سنضيف معالِجات الأحداث للأحداث الصادرة من المكوِّن ToDoItemEditForm، وسنربط التوابع المناسبة لكل حدث، لذا عدِّل الاستدعاء <to-do-item-edit-form> ليبدو كما يلي: <to-do-item-edit-form v-else :id="id" :label="label" @item-edited="itemEdited" @edit-cancelled="editCancelled"> </to-do-item-edit-form> تعديل وحذف عناصر المهام يمكننا الآن التبديل بين نموذج التعديل ومربع الاختيار، ولكن لم نعالِج تحديث المصفوفة ToDoItems مرةً أخرى في المكوِّن App.vue، ويمكن إصلاح ذلك من خلال الاستماع إلى الحدث item-edited وتحديث القائمة وفقًا لذلك، كما يجب التعامل مع حدث الحذف لنتمكّن من حذف عناصر المهام. أضف التوابع الجديدة التالية إلى كائن مكوِّن App.vue بعد التوابع الموجودة مسبقًا ضمن الخاصية methods: deleteToDo(toDoId) { const itemIndex = this.ToDoItems.findIndex(item => item.id === toDoId); this.ToDoItems.splice(itemIndex, 1); }, editToDo(toDoId, newLabel) { const toDoToEdit = this.ToDoItems.find(item => item.id === toDoId); toDoToEdit.label = newLabel; } سنضيف بعد ذلك مستمعي الأحداث للحدثين item-deleted و item-edited، بحيث: يجب تمرير item.id إلى التابع بالنسبة للحدث item-deleted. يجب تمرير item.id والمتغير الخاص ‎$event بالنسبة للحدث item-edited، إذ يُعَدّ المتغير ‎$event هو متغير خاص بإطار العمل Vue ويُستخدَم لتمرير بيانات الحدث إلى التوابع، فإذا استخدمتَ أحداث HTML الأصيلة Native مثل الحدث click، فسيمرّر هذا المتغير كائن الحدث الأصيل إلى تابعك. عدّل الاستدعاء <to-do-item></to-do-item> ضمن قالب App.vue ليبدو كما يلي: <to-do-item :label="item.label" :done="item.done" :id="item.id" @checkbox-changed="updateDoneStatus(item.id)" @item-deleted="deleteToDo(item.id)" @item-edited="editToDo(item.id, $event)"> </to-do-item> يجب أن تكون الآن قادرًا على تعديل العناصر وحذفها من القائمة. إصلاح خطأ باستخدام الحالة isDone يبدو كل شيء رائعًا حتى الآن، ولكننا أحدثنا خطأً عن طريق إضافة وظيفة التعديل، لذا جرّب تنفيذ ما يلي: تحديد أو إلغاء تحديد أحد مربعات اختيار المهام. الضغط على زر "التعديل Edit" لعنصر المهام نفسه. إلغاء التعديل بالضغط على زر "الإلغاء Cancel". لاحظ حالة مربع الاختيار بعد الضغط على زر الإلغاء، فلم ينسَ التطبيق حالة مربع الاختيار فقط، وإنما أحدث خللًا أيضًا في حالة الاكتمال done لعنصر المهام المقابل، فإذا حاولت تحديده أو إلغاء تحديده مرةً أخرى، فسيتغير عدد المهام المكتملة بعكس ما تتوقعه لأنّ الخاصية isDone ضمن data تُعطَى القيمة this.done عند تحميل المكوِّن، ويمكن إصلاح ذلك عن طريق تحويل عنصر بيانات isDone إلى الخاصية computed، إذ سنستخدِم ميزةً أخرى للخاصيات computed هي أنها تحافظ على التفاعل، مما يعني أنّ حالتها تُحفَظ عندما يتغير القالب. أزِل السطر التالي من الخاصية data()‎: isDone: this.done, أضِف الكتلة التالية بعد كتلة data() { }‎: computed: { isDone() { return this.done; } }, ستجد أنّ المشكلة قد حُلَّت الآن عند الحفظ وإعادة التحميل، إذ سيجري الاحتفاظ بحالة مربع الاختيار عند التبديل بين قوالب عناصر المهام. فهم تشابك الأحداث يُعَدّ تشابكُ الأحداث القياسية والمخصَّصة التي استخدمناها لتحفيز التفاعل في تطبيقنا أحد أكثر الأجزاء المربكة، إذ يمكننا فهم ذلك بصورة أفضل من خلال كتابة مخطط تدفقي أو وصف أو رسم توضيحي لمكان إصدار الأحداث ومكان الاستماع إليها وما يحدث نتيجة إطلاقها. فمثلًا في الملف App.vue: يستمع العنصر <to-do-form> إلى: الحدث todo-added الصادر عن التابع onSubmit()‎ ضمن المكوِّن ToDoForm عند إرسال النموذج، والنتيجة هي استدعاء التابع addToDo()‎ لإضافة عنصر مهمة جديد إلى المصفوفة ToDoItems. يستمع العنصر <to-do-item> إلى: الحدث checkbox-changed الصادر عن مربع الاختيار في العنصر <input> ضمن المكوِّن ToDoItem عند تحديده أو إلغاء تحديده، والنتيجة هي استدعاء التابع updateDoneStatus()‎ لتحديث حالة الاكتمال done لعنصر المهمة المقابل. الحدث item-deleted الصادر عن التابع deleteToDo()‎ ضمن المكوِّن ToDoItem عند الضغط على زر "الحذف Delete"، والنتيجة هي استدعاء التابع deleteToDo()‎ لحذف عنصر المهمة المقابل. الحدث item-edited الصادر عن التابع itemEdited()‎ ضمن المكوِّن ToDoItem عند الاستماع بنجاح إلى الحدث item-edited باستخدام التابع onSubmit()‎ ضمن المكوِّن ToDoItemEditForm، إذ يمثّل ذلك سلسلةً من حدثَي item-edit مختلفين، والنتيجة هي استدعاء التابع editToDo()‎ لتحديث عنوان label عنصر المهمة المقابل. في الملف ToDoForm.vue: يستمع العنصر <form> إلى الحدث submit، والنتيجة هي استدعاء التابع onSubmit()‎ الذي يتحقَّق من أنّ العنوان label الجديد ليس فارغًا، ثم يصدر الحدث todo-added الذي يمكن الاستماع إليه لاحقًا ضمن الملف App.vue كما ذكرنا سابقًا، ثم يمسح عنوان العنصر <input> الجديد. في الملف ToDoItem.vue: يستمع عنصر مربع الاختيار checkbox في العنصر <input> إلى الحدث change، والنتيجة هي إصدار الحدث checkbox-changed عند تحديد أو إلغاء تحديد مربع الاختيار الذي يمكن الاستماع إليه لاحقًا في المكوِّن App.vue كما ذكرنا سابقًا. يستمع زر <button> "التعديل Edit" إلى الحدث click، والنتيجة هي استدعاء التابع toggleToItemEditForm()‎ الذي قيمة this.isEditing مبدَّلة إلى true والتي تعرض بدورها قالب تعديل عنصر المهام عند إعادة التصيير. يستمع زر <button> "الحذف Delete" إلى الحدث click، والنتيجة هي استدعاء التابع deleteToDo()‎ الذي يصدر الحدث item-deleted، والذي يمكن الاستماع إليه لاحقًا ضمن المكوِّن App.vue كما ذكرنا سابقًا. يستمع المكوِّن <to-do-item-edit-form> إلى: الحدث item-edited الصادر عن التابع onSubmit()‎ ضمن المكوِّن ToDoItemEditForm عند إرسال النموذج بنجاح، والنتيجة هي استدعاء التابع itemEdited()‎ الذي يصدر الحدث item-edited الذي يمكن الاستماع إليه لاحقًا في المكوِّن App.vue كما ذكرنا سابقًا، كما يعيد ضبط this.isEditing على القيمة false، وبالتالي لن يظهر نموذج التعديل عند إعادة التصيير. الحدث edit-cancelled الصادر عن التابع onCancel()‎ ضمن المكوِّن ToDoItemEditForm عند النقر على زر "الإلغاء Cancel"، والنتيجة هي استدعاء التابع editCancelled()‎ الذي يعيد ضبط this.isEditing على القيمة false، وبالتالي لن يظهر نموذج التعديل عند إعادة التصيير. في الملف ToDoItemEditForm.vue: يستمع العنصر <form> إلى الحدث submit، والنتيجة هي استدعاء التابع onSubmit()‎ الذي يتحقق مما إذا كانت قيمة العنوان الجديدة غير فارغة وليست مماثلةً للقيمة القديمة، فإذا كان الأمر كذلك، فسيصدر الحدث item-edited الذي يمكن الاستماع إليه لاحقًا ضمن المكوِّن ToDoItem.vue كما ذكرنا سابقًا. يستمع زر <button> "الإلغاء Cancel" إلى الحدث click، والنتيجة هي استدعاء التابع onCancel()‎ الذي يصدر الحدث edit-cancelled الذي يمكن الاستماع إليه لاحقًا في المكون ToDoItem.vue كما ذكرنا سابقًا. الخلاصة أصبح لدينا الآن وظائف التعديل والحذف في تطبيقنا، وبالتالي سنقترب من نهاية سلسلة Vue، إذ يجب الآن تعلّم كيفية إدارة التركيز أو كيف يمكننا تحسين الشمولية لمستخدمي لوحة المفاتيح في تطبيقنا. ترجمة -وبتصرّف- للمقال Vue conditional rendering: editing existing todos. اقرأ أيضًا المقال السابق: إضافة تنسيق للمكونات واستعمال الخاصية computed في تطبيق Vue.js عرض مكونات Vue.js استخدام Vue.js للاتصال بالإنترنت
  7. لقد حان الوقت أخيرًا لجعل تطبيقنا يبدو أجمل قليلًا، إذ سنتعرّف في هذا المقال على الطرق المختلفة لتنسيق مكونات إطار العمل Vue باستخدام لغة CSS، كما سنضيف عدّادًا يعرض عدد عناصر المهام المكتملة باستخدام ميزة في إطار عمل Vue تسمّى الخاصية computed، إذ تعمل هذه الخاصية بصورة مشابهة للتوابع، ولكن يُعاد تشغيلها فقط عندما تتغير إحدى اعتمادياتها. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية، إذ تُكتَب مكونات Vue بوصفها مجموعةً من كائنات جافاسكربت التي تدير بيانات التطبيق وصيغة القوالب المستنِدة إلى لغة HTML المرتبطة مع بنية DOM الأساسية، كما ستحتاج إلى طرفية مثبَّت عليها node و npm لتثبيت Vue ولاستخدام بعض ميزاته الأكثر تقدمًا مثل مكونات الملف المفرد Single File Components أو دوال التصيير Render. الهدف: التعرف على مكونات التنسيق وتعلّم كيفية استخدام خاصيات computed في Vue. يجب إضافة شيفرة CSS الأساسية إلى تطبيقنا لجعله يبدو أفضل قبل إضافة مزيد من الميزات المتقدمة إليه، إذ يمتلك إطار العمل Vue ثلاث طرق شائعة لتنسيق التطبيقات: ملفات CSS الخارجية. التنسيقات العام في مكونات الملف المفرد، أي الملفات ذات اللاحقة ‎.vue. التنسيقات على مستوى المكونات في مكونات الملف المفرد. سنستخدِم مزيجًا من الطرق الثلاثة لإضفاء مظهر وتنسيق أفضل على تطبيقنا للتعرف على هذه الطرق. التنسيق باستخدام ملفات CSS الخارجية يمكنك تضمين ملفات CSS الخارجية وتطبيقها بطريقة عامة على تطبيقك، لذا أنشئ ملفًا بالاسم reset.css في المجلد src/assets، إذ تُعالَج الملفات الموجودة في هذا المجلد باستخدام Webpack، وبالتالي يمكننا استخدام معالجات CSS المسبقَة مثل SCSS أو معالجات ملحقَة مثل PostCSS. لن نستخدِم في هذا المقال مثل هذه الأدوات، ولكن يجب معرفة أنه ستُعالَج الشيفرة تلقائيًا عند تضمينها في المجلد assets. أضِف المحتويات التالية في الملف reset.css: /*reset.css*/ /* إعادة الضبط */ *, *::before, *::after { box-sizing: border-box; } *:focus { outline: 3px dashed #228bec; } html { font: 62.5% / 1.15 sans-serif; } h1, h2 { margin-bottom: 0; } ul { list-style: none; padding: 0; } button { border: none; margin: 0; padding: 0; width: auto; overflow: visible; background: transparent; color: inherit; font: inherit; line-height: normal; -webkit-font-smoothing: inherit; -moz-osx-font-smoothing: inherit; -webkit-appearance: none; } button::-moz-focus-inner { border: 0; } button, input, optgroup, select, textarea { font-family: inherit; font-size: 100%; line-height: 1.15; margin: 0; } button, input { /* 1 */ overflow: visible; } input[type="text"] { border-radius: 0; } body { width: 100%; max-width: 68rem; margin: 0 auto; font: 1.6rem/1.25 "Helvetica Neue", Helvetica, Arial, sans-serif; background-color: #f5f5f5; color: #4d4d4d; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; } @media screen and (min-width: 620px) { body { font-size: 1.9rem; line-height: 1.31579; } } /*نهاية إعادة الضبط*/ استورد بعد ذلك الملف reset.css في الملف src/main.js كما يلي: import './assets/reset.css'; سيؤدي ذلك إلى نقل الملف أثناء خطوة البناء وإضافته تلقائيًا إلى موقعنا. يجب تطبيق التنسيقات المُعاد ضبطها على التطبيق الآن، إذ تُظهر الصور التالية مظهر التطبيق قبل وبعد تطبيق إعادة الضبط. قبل إعادة ضبط التنسيق: بعد إعادة ضبط التنسيق: تشمل التغييرات الملحوظة إزالة نقط تعداد للقائمة وتغييرات لون الخلفية والتغييرات في الزر الأساسي وتنسيقات حقل الإدخال. إضافة التنسيقات العامة إلى مكونات الملف المفرد أعدنا ضبط شيفرة CSS لتكون موحدةًَ في المتصفحات ويجب الآن تخصيص التنسيقات، فهناك بعض التنسيقات التي نريد تطبيقها على مستوى المكونات في تطبيقنا من خلال إضافة التنسيقات إلى وسوم <style> في الملف App.vue بدلًا من إضافتها مباشرةً إلى ملف التنسيقات reset.css. توجد مسبقًا بعض التنسيقات في الملف App.vue، فلنحذفها ونستبدلها بالتنسيقات التالية التي تضيف تنسيقًا إلى الأزرار وحقول الإدخال وتخصّص العنصر ‎#app وأبناءه، لذا عدِّل العنصر <style> في الملف App.vue بحيث يبدو كما يلي: <style> /* التنسيقات العامة */ .btn { padding: 0.8rem 1rem 0.7rem; border: 0.2rem solid #4d4d4d; cursor: pointer; text-transform: capitalize; } .btn__danger { color: #fff; background-color: #ca3c3c; border-color: #bd2130; } .btn__filter { border-color: lightgrey; } .btn__danger:focus { outline-color: #c82333; } .btn__primary { color: #fff; background-color: #000; } .btn-group { display: flex; justify-content: space-between; } .btn-group > * { flex: 1 1 auto; } .btn-group > * + * { margin-left: 0.8rem; } .label-wrapper { margin: 0; flex: 0 0 100%; text-align: center; } [class*="__lg"] { display: inline-block; width: 100%; font-size: 1.9rem; } [class*="__lg"]:not(:last-child) { margin-bottom: 1rem; } @media screen and (min-width: 620px) { [class*="__lg"] { font-size: 2.4rem; } } .visually-hidden { position: absolute; height: 1px; width: 1px; overflow: hidden; clip: rect(1px 1px 1px 1px); clip: rect(1px, 1px, 1px, 1px); clip-path: rect(1px, 1px, 1px, 1px); white-space: nowrap; } [class*="stack"] > * { margin-top: 0; margin-bottom: 0; } .stack-small > * + * { margin-top: 1.25rem; } .stack-large > * + * { margin-top: 2.5rem; } @media screen and (min-width: 550px) { .stack-small > * + * { margin-top: 1.4rem; } .stack-large > * + * { margin-top: 2.8rem; } } /* نهاية التنسيقات العامة */ #app { background: #fff; margin: 2rem 0 4rem 0; padding: 1rem; padding-top: 0; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1); } @media screen and (min-width: 550px) { #app { padding: 4rem; } } #app > * { max-width: 50rem; margin-left: auto; margin-right: auto; } #app > form { max-width: 100%; } #app h1 { display: block; min-width: 100%; width: 100%; text-align: center; margin: 0; margin-bottom: 1rem; } </style> إذا اختبرت التطبيق، فسترى أنّ قائمة المهام موجودة ضمن قسم خاص به الآن مع بعض التنسيقات الأفضل لعناصر المهام كما يلي: إضافة أصناف CSS في إطار العمل Vue سنطبّق أصناف CSS على عنصر الزر <button> في المكوِّن ToDoForm، وبما أنّ قوالب Vue تستخدِم لغة HTML، فسنستخدِم الطريقة نفسها عن طريق إضافة السمة class=""‎ إلى العنصر، لذا أضِف السمة class="btn btn__primary btn__lg"‎ إلى العنصر <button> في نموذجك: <button type="submit" class="btn btn__primary btn__lg"> Add </button> هناك تغيير آخر يمكن إجراؤه، فبما أنّ النموذج يشير إلى قسم معيّن من الصفحة، فيمكن استخدام العنصر <h2>، ولكن يشير العنصر label إلى الغرض من النموذج مسبقًا، لذلك يجب تجنب التكرار من خلال تغليف العنصر label ضمن العنصر <h2>، كما يمكننا إضافة بعض تنسيقات CSS العامة الأخرى، إذ سنضيف الصنف input__lg إلى العنصر <input>، لذا عدِّل قالب المكوّن ToDoForm بحيث يبدو كما يلي: <template> <form @submit.prevent="onSubmit"> <h2 class="label-wrapper"> <label for="new-todo-input" class="label__lg"> What needs to be done? </label> </h2> <input type="text" id="new-todo-input" name="new-todo" autocomplete="off" v-model.lazy.trim="label" class="input__lg" /> <button type="submit" class="btn btn__primary btn__lg"> Add </button> </form> </template> كما سنضيف الصنف stack-large إلى الوسم <ul> في الملف App.vue كما يلي، مما يساعد في تحسين التباعد بين عناصر المهام: <ul aria-labelledby="list-summary" class="stack-large"> إضافة التنسيقات ذات النطاق المحدد نريد الآن تنسيق المكوِِّن ToDoItem، إذ يمكننا إضافة العنصر <style> ضمنه لتكون تعريفات التنسيقات قريبةً من المكوِّن، لكن إذا غيرت هذه التنسيقات أشياءً خارج هذا المكوِّن، فسيكون تعقّب التنسيقات المسؤولة عن ذلك وإصلاح المشكلة أمرًا صعبًا. سنستخدِم السمة scoped حيث يرتبط محدِّد سمة HTML الفريدة data مع جميع التنسيقات، مما يمنعها من التعارض على المستوى العام، كما يمكنك استخدام معدِّل scoped من خلال إنشاء العنصر <style> ضمن المكوِّن ToDoItem.vue، ثم منحه السمة scoped كما يلي: <style scoped> </style> انسخ بعد ذلك شيفرة CSS التالية والصقها في العنصر <style> الذي أنشأناه للتو: .custom-checkbox > .checkbox-label { font-family: Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-weight: 400; font-size: 16px; font-size: 1rem; line-height: 1.25; color: #0b0c0c; display: block; margin-bottom: 5px; } .custom-checkbox > .checkbox { font-family: Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-weight: 400; font-size: 16px; font-size: 1rem; line-height: 1.25; box-sizing: border-box; width: 100%; height: 40px; height: 2.5rem; margin-top: 0; padding: 5px; border: 2px solid #0b0c0c; border-radius: 0; -webkit-appearance: none; -moz-appearance: none; appearance: none; } .custom-checkbox > input:focus { outline: 3px dashed #fd0; outline-offset: 0; box-shadow: inset 0 0 0 2px; } .custom-checkbox { font-family: Arial, sans-serif; -webkit-font-smoothing: antialiased; font-weight: 400; font-size: 1.6rem; line-height: 1.25; display: block; position: relative; min-height: 40px; margin-bottom: 10px; padding-left: 40px; clear: left; } .custom-checkbox > input[type="checkbox"] { -webkit-font-smoothing: antialiased; cursor: pointer; position: absolute; z-index: 1; top: -2px; left: -2px; width: 44px; height: 44px; margin: 0; opacity: 0; } .custom-checkbox > .checkbox-label { font-size: inherit; font-family: inherit; line-height: inherit; display: inline-block; margin-bottom: 0; padding: 8px 15px 5px; cursor: pointer; touch-action: manipulation; } .custom-checkbox > label::before { content: ""; box-sizing: border-box; position: absolute; top: 0; left: 0; width: 40px; height: 40px; border: 2px solid currentColor; background: transparent; } .custom-checkbox > input[type="checkbox"]:focus + label::before { border-width: 4px; outline: 3px dashed #228bec; } .custom-checkbox > label::after { box-sizing: content-box; content: ""; position: absolute; top: 11px; left: 9px; width: 18px; height: 7px; transform: rotate(-45deg); border: solid; border-width: 0 0 5px 5px; border-top-color: transparent; opacity: 0; background: transparent; } .custom-checkbox > input[type="checkbox"]:checked + label::after { opacity: 1; } @media only screen and (min-width: 40rem) { label, input, .custom-checkbox { font-size: 19px; font-size: 1.9rem; line-height: 1.31579; } } يجب الآن إضافة أصناف CSS إلى القالب لربط التنسيقات، لذا انتقل إلى العنصر <div> الجذر ثم أضِف الصنف custom-checkbox، وأضف الصنف checkbox إلى العنصر <input> ثم أضف الصنف إلى العنصر <label>، وبالتالي سيحتوي التطبيق على مربعات اختيار مخصَّصة، كما يجب أن يبدو تطبيقك الآن كما يلي: انتهينا من تنسيق تطبيقنا، وسنعود الآن إلى إضافة مزيد من الوظائف إليه، أي استخدام الخاصية computed لإضافة عدد عناصر المهام المكتملة إلى تطبيقنا. استخدام الخاصية computed في إطار العمل Vue نريد إضافة عدّاد counter يلخّص عدد العناصر في قائمة المهام، مما يساعد التقنيات المساعدة في معرفة عدد المهام المكتملة، فإذا كان لدينا عنصران مكتملان من أصل خمسة في قائمة المهام، فسنرى العبارة "2‎ items completed out of 5" كما يلي: <h2>{{ToDoItems.filter(item => item.done).length}} out of {{ToDoItems.length}} items completed</h2> سيُعاد حساب هذا العدد في كل عملية تصيير، ويمكن أن يكون ذلك غير مهم بالنسبة إلى تطبيق صغير مثل تطبيقنا، ولكنه سيتسبب في مشكلة خطيرة في الأداء بالنسبة للتطبيقات الأكبر حجمًا أو عندما يكون التعبير أكثر تعقيدًا، لذا يكون الحل الأفضل هو استخدام خاصيات computed في إطار العمل Vue، إذ تعمل هذه الخاصيات بصورة مشابهة للتوابع، ولكن يُعاد تشغيلها فقط عندما تتغير إحدى اعتمادياتها، إذ سيُعاد تشغيلها في حالتنا عندما تتغير المصفوفة ToDoItems فقط. يمكن إنشاء الخاصية computed من خلال إضافتها إلى كائن المكوِّن مثل الخاصية methods التي استخدمناها سابقًا. إضافة عداد أضِف الشيفرة البرمجية التالية إلى كائن المكوِّن App بعد الخاصية methods، إذ يأخذ التابع listSummary()‎ عدد عناصر ToDoItems المنتهية، ويعيد سلسلةً نصيةً تمثّل ذلك. computed: { listSummary() { const numberFinishedItems = this.ToDoItems.filter(item =>item.done).length return `${numberFinishedItems} out of ${this.ToDoItems.length} items completed` } } يمكننا الآن إضافة {{listSummary}} مباشرةً إلى نموذجنا، إذ سنضيفه ضمن العنصر <h2> قبل العنصر <ul> مباشرةً، كما سنضيف السمة id والسمة aria-labelledby لتكون محتويات العنصر <h2> عنوانًا label للعنصر <ul>، لذا أضِف العنصر <h2> وعدّل العنصر <ul> ضمن قالب المكون App كما يلي: <h2 id="list-summary">{{listSummary}}</h2> <ul aria-labelledby="list-summary" class="stack-large"> <li v-for="item in ToDoItems" :key="item.id"> <to-do-item :label="item.label" :done="item.done" :id="item.id"></to-do-item> </li> </ul> يجب أن تشاهد الآن ملخص القائمة في تطبيقك مع تحديث عدد العناصر الإجمالي كلما أضفت مزيدًا من عناصر المهام، ولكن إذا حاولت تحديد بعض العناصر ثم إلغاء تحديدها، فسيظهر خطأ لأننا لم نتعقّب بعد البيانات "المكتملة" فعليًا، لذلك لن يتغير عدد العناصر المكتملة. تعقب تغيرات عدد العناصر المكتملة يمكننا استخدام الأحداث لالتقاط تحديث مربع الاختيار وإدارة القائمة وفقًا لذلك، كما يمكننا ربط معالج الحدث ‎@change مع كل مربع اختيار بدلًا من استخدام الموجِّه v-model لأننا لا نعتمد على ضغط الزر لبدء التغيير. عدّل العنصر <input> في المكون ToDoItem.vue ليبدو كما يلي، كما يمكننا تضمين ‎$emit()‎ لأنّ كل ما يجب فعله هو إرسال أن مربع الاختيار محدَّد فقط: <input type="checkbox" class="checkbox" :id="id" :checked="isDone" @change="$emit('checkbox-changed')" /> أضف تابعًا جديدًا بالاسم updateDoneStatus()‎ في المكوِّن App.vue بعد التابع addToDo()‎، إذ يجب أن يأخذ هذا التابع معامِلًا واحدًا هو معرّف عنصر المهمة، كما نريد العثور على العنصر ذي المعرّف id المطابق وتحديث حالته done لتكون معاكسةً لحالته الحالية: updateDoneStatus(toDoId) { const toDoToUpdate = this.ToDoItems.find(item => item.id === toDoId) toDoToUpdate.done = !toDoToUpdate.done } يجب تشغيل هذا التابع كلما أصدر المكوِّن ToDoItem الحدث checkbox-changed، وتمرير معرّفه item.id بوصفه معاملًا، لذا عدّل الاستدعاء <to-do-item></to-do-item> كما يلي: <to-do-item :label="item.label" :done="item.done" :id="item.id" @checkbox-changed="updateDoneStatus(item.id)"> </to-do-item> إذا اختبرت العنصر ToDoItem الآن، فيجب أن تشاهد تحديث العدّاد الملخِّص بطريقة صحيحة. الخلاصة تعرّفنا في هذا المقال على تنسيق مكوّنات إطار العمل Vue باستخدام لغة CSS، واستخدمنا الخاصية computed لإضافة ميزة مفيدة إلى تطبيقنا وهي حساب عدد عناصر المهام المكتملة، وسنتعرّف في المقال التالي على التصيير أو العرض الشرطي Conditional Rendering وكيفية استخدامه لإظهار نموذج التعديل عندما نريد تعديل عناصر المهام الحالية. ترجمة -وبتصرّف- للمقالين Styling Vue components with CSS وUsing Vue computed properties. اقرأ أيضًا المقال السابق: إنشاء المكونات في تطبيق Vue.js عرض مكونات Vue.js شرح مفهوم الأحداث والتوابع والنماذج في Vue.js
  8. لدينا الآن عينة من البيانات وحلقة تأخذ كل جزء من هذه البيانات وتصيّره أو تعرضه Render ضمن المكوِّن ToDoItem في تطبيقنا، كما نريد السماح لمستخدِمينا بإدخال عناصر مهامهم في التطبيق، لذلك سنحتاج إلى نص إدخال <input> وحدث يُطلَق عند إرسال البيانات وتابع لإطلاق الإرسال بهدف إضافة البيانات وتصيير القائمة ونموذج للتحكم في البيانات، وهذا هو موضوعنا في هذا المقال. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ تُكتَب مكوّنات Vue بوصفها مجموعةً من كائنات جافاسكربت التي تدير بيانات التطبيق وصيغة القوالب المستنِدة إلى لغة HTML المرتبطة مع بنية DOM الأساسية، وستحتاج إلى طرفية مثبَّت عليها node و npm لتثبيت Vue ولاستخدام بعض ميزاته الأكثر تقدمًا مثل مكوّنات الملف المفرد Single File Components أو دوال التصيير Render. الهدف: تعلّم كيفية التعامل مع الاستمارات Forms في Vue والأحداث Events والنماذج Models والتوابع Methods المرتبطة بها. إنشاء استمارة مهام جديدة لدينا الآن تطبيق يعرض قائمةً بعناصر المهام، ولكن لا يمكننا تعديل قائمة العناصر دون تغيير شيفرتنا البرمجية يدويًا، إذًا لنصلح ذلك ولْننشئ مكوِّنًا جديدًا يسمح لنا بإضافة عنصر مهام جديد. أنشئ ملفًا جديدًا يسمى ToDoForm.vue في مجلد المكوّنات، ثم أضِف عنصر <template> فارغًا ووسم <script> كما يلي: <template></template> <script> export default {}; </script> أضِف استمارة HTML الذي يتيح إدخال عنصر مهام جديد وإرساله إلى التطبيق، كما نحتاج إلى العنصر <form> مع العناصر <label> و<input> و<button>، لذا عدِّل قالبك ليبدو كما يلي: <template> <form> <label for="new-todo-input"> What needs to be done? </label> <input type="text" id="new-todo-input" name="new-todo" autocomplete="off" /> <button type="submit"> Add </button> </form> </template> لدينا الآن مكوِّن استمارة يمكننا من خلاله إدخال عنوان عنصر مهام جديد والذي سيصبح عنوانًا أو تسمية label للمكوِّن ToDoItem المقابل عند تصييره لاحقًا، ولنحمِّل هذا المكوِّن في تطبيقنا، لذا ارجع إلى الملف App.vue وأضف تعليمة الاستيراد import التالية بعد تعليمة الاستيراد السابقة مباشرةً ضمن العنصر <script>: import ToDoForm from './components/ToDoForm'; يجب تسجيل المكوِّن الجديد في المكون App من خلال تعديل الخاصية components الخاصة بكائن المكوِّن بحيث تبدو كما يلي: components: { ToDoItem, ToDoForm } صيّر المكوِّن ToDoForm ضمن تطبيقك من خلال إضافة العنصر <to-do-form /‎> إلى العنصر <template> ضمن المكوِّن App كما يلي: <template> <div id="app"> <h1>My To-Do List</h1> <to-do-form></to-do-form> <ul> <li v-for="item in ToDoItems" :key="item.id"> <to-do-item :label="item.label" :done="item.done" :id="item.id"></to-do-item> </li> </ul> </div> </template> إذا شغّلت موقعك الآن، فيجب أن ترى الاستمارة الجديدة كما يلي: إذا ملأت الاستمارة ونقرت على زر "الإضافة Add"، فستسرسل الصفحة الاستمارة مرةً أخرى إلى الخادم، ولكننا لا نريد ذلك، وإنما نريد تشغيل تابع على الحدث submit الذي سيضيف المهام الجديدة إلى قائمة بيانات ToDoItem المُعرَّفة ضمن المكوِّن App، لذلك يجب إضافة تابع إلى نسخة المكوِّن. إنشاء تابع وربطه بحدث باستخدام الموجه v-on يمكن إتاحة تابع للمكوِّن ToDoForm من خلال إضافته إلى كائن المكوِّن ضمن الخاصية methods التي تشبه الخاصيات data()‎ و props وما إلى ذلك، إذ تحتوي الخاصية methods على أيّ تابع قد نحتاجه لاستدعاء مكوننا. كما تُشغَّل جميع التوابع عند استدعاء هذه الخاصية، لذلك لا يُعَدّ استخدامها لعرض المعلومات ضمن القالب أمرًا جيدًا، وبالتالي يجب استخدام الخاصية computed -التي سنتحدث عنها لاحقًا- لعرض البيانات الناتجة عن العمليات الحسابية. يجب إضافة التابع onSubmit()‎ إلى الخاصية methods ضمن كائن المكوِّن ToDoForm، إذ سنستخدِم هذا التابع للتعامل مع إجراء الإرسال، لذا أضف هذا التابع كما يلي: export default { methods: { onSubmit() { console.log('form submitted') } } } يجب بعد ذلك ربط التابع بمعالج حدث الإرسال submit للعنصر <form>، ويشبه ذلك إلى حد كبير كيفية استخدام إطار العمل Vue لصيغة v-bind بهدف ربط السمات، إذ يمتلك Vue موجِّهًا خاصًا لمعالجة الأحداث وهو v-on الذي يعمل باستخدام الصيغة v-on:event="method"‎، كما توجد صيغة مختصرة هي ‎@event="method"‎، إذ سنستخدِم الصيغة المختصرة في هذا المقال، وبالتالي أضف معالج حدث الإرسال submit إلى العنصر <form> كما يلي: <form @submit="onSubmit"> إذا شغّلنا معالج حدث الإرسال، فلا يزال التطبيق يرسل البيانات إلى الخادم مما يتسبب في تحديث الصفحة، وبما أننا نطبّق كل عمليات المعالجة على العميل، فلا يوجد خادم ليتعامل مع إعادة الإرسال وسنفقد جميع الحالات المحلية عند تحديث الصفحة، إذ يمكن منع المتصفح من الإرسال إلى الخادم من خلال إيقاف إجراء الحدث الافتراضي أثناء انتشاره للأعلى Bubbling Up في الصفحة باستخدام التابع Event.preventDefault()‎ في لغة جافاسكربت الصرفة Vanilla JavaScript مثلًا. كما يحتوي إطار عمل Vue على صيغة خاصة تُسمَّى معدِّلات الأحداث Event Modifiers التي يمكنها معالجة هذا الأمر مباشرةً في قالبنا، في حين تُضاف المُعدِّلات إلى نهاية الحدث مع نقطة بالصورة ‎@event.modifier، ونوضِّح فيما يلي قائمةً بمعدِّلات الأحداث: ‎.stop: يوقِف انتشار الحدث ويكافئ التابع Event.stopPropagation()‎ في أحداث جافاسكربت العادية. ‎.prevent: يمنع سلوك الحدث الافتراضي ويكافئ التابع Event.preventDefault()‎. ‎.self: يؤدي إلى تشغيل المعالج فقط إذا أُرسِل الحدث من هذا العنصر المُحدَّد. {‎.key}: يؤدي إلى تشغيل معالج الأحداث باستخدام مفتاح محدَّد فقط، ويحتوي موقع MDN على قائمة بقيم المفاتيح الصالحة، ولكن يجب تحويل المفاتيح متعددة الكلمات إلى حالة الأحرف التي تسمى نمط أسياخ الشواء Kebab Case مثل page-down. ‎.native: يستمع إلى الحدث الأصيل Native في عنصر الجذر أو الغلاف الخارجي Outer-most Wrapping لمكوِّنك. ‎.once: يستمع إلى الحدث حتى تشغيله لمرة واحدة فقط لا أكثر. ‎.left: يشغِّل المعالج باستخدام حدث زر الفأرة الأيسر فقط. ‎.right: يشغِّل المعالج باستخدام حدث زر الفأرة الأيمن فقط. ‎.middle: يشغِّل المعالج باستخدام حدث زر الفأرة الأوسط فقط. ‎.passive: يكافئ استخدام المعامِل { passive: true } عند إنشاء مستمع حدث في لغة جافاسكربت الصرفة Vanilla JavaScript باستخدام التابع addEventListener()‎. سنستخدِم في حالتنا المعالِج ‎.prevent لإيقاف إجراء الإرسال الافتراضي للمتصفح، لذا أضِف ‎.prevent إلى معالج الإرسال ‎@submit في قالبك كما يلي: <form @submit.prevent="onSubmit"> إذا حاولت إرسال الاستمارة الآن، فستلاحظ عدم إعادة تحميل الصفحة، وإذا فتحت الطرفية، فيمكنك رؤية نتائج التابع console.log()‎ التي أضفناها ضمن التابع onSubmit()‎. ربط البيانات مع الدخل باستخدام الموجه v-model نحتاج الآن إلى طريقة للحصول على القيمة الموجودة في حقل الإدخال <input> من الاستمارة لنتمكّن من إضافة عنصر المهام الجديد إلى قائمة بيانات ToDoItems، إذ يكون أول شيء نحتاجه هو الخاصية data في استمارتنا لتعقّب قيمة المهمة. أضِف التابع data()‎ إلى كائن مكون ToDoForm الذي يعيد الحقل label، إذ يمكننا ضبط قيمة الحقل label الأولية بوصفها سلسلة نصية فارغة، ويجب أن يبدو كائن المكوِّن الآن كما يلي: export default { methods: { onSubmit() { console.log("form submitted"); } }, data() { return { label: "" }; } }; نحتاج الآن إلى طريقة ما لربط قيمة حقل الإدخال <input> ذي المعرِّف new-todo-input بالحقل label، إذ يمتلك إطار العمل Vue موجِّهًا خاصًا لذلك وهو v-model الذي يرتبط مع خاصية البيانات التي ضبطتها عليه ويبقيها متزامنةً مع حقل الإدخال <input>، في حين يعمل الموجّه v-model مع جميع أنواع حقول الإدخال المختلفة بما في ذلك مربعات الاختيار Checkboxes وأزرار الانتقاء Radios وحقول الاختيار Select Inputs، كما يُستخدَم هذا الموجِّه من خلال إضافة سمة بالصورة v-model="variable"‎ إلى العنصر <input> كما يلي: <input type="text" id="new-todo-input" name="new-todo" autocomplete="off" v-model="label" /> ملاحظة: يمكنك مزامنة البيانات مع قيم العنصر <input> باستخدام تركيبة من الأحداث مع سمات v-bind وهذا ما يفعله الموجّه v-model، كما تختلف تركيبة الأحداث والسمات وفقًا لنوع حقل الإدخال وستتطلب شيفرةً برمجيةً أكبر من مجرد استخدام صيغة v-model المختصرة. لنختبر استخدام الموجِّه v-model عن طريق تسجيل قيمة البيانات المقدَّمة في التابع onSubmit()‎، إذ يمكن الوصول إلى سمات البيانات في المكوّنات باستخدام الكلمة this، وبالتالي يمكن الوصول إلى الحقل label بالصورة this.label، لذا عدّل التابع onSubmit()‎ ليبدو كما يلي: methods: { onSubmit() { console.log('Label value: ', this.label); } }, عد الآن إلى تطبيقك المُشغَّل، وأضِف نصًا في الحقل <input> ثم انقر على زر "الإضافة Add"، إذ يجب أن ترى القيمة التي أدخلتها مسجلةً في الطرفية كما يلي على سبيل المثال: Label value: My value تغيير سلوك الموجه v-model باستخدام المعدلات يمكننا إضافة معدِّلات Modifiers لتغيير سلوك الموجِّه v-model بطريقة مماثلة لمعدِّلات الأحداث، ومن أبزر هذه المعدِّلات: ‎.trim: يزيل المسافة الفارغة الموجودة قبل نص الإدخال أو بعده، ويمكننا إضافة هذا المُعدِّل إلى تعليمة v-model بالصورة v-model.trim="label"‎ ‎.lazy: يتغير هذا المعدِّل عندما يتزامن v-model مع قيمة نص حقل الإدخال، كما يمكن مزامنة الموجِّه v-model عن طريق تحديث المتغير الذي يستخدم الأحداث، بينما تحدُث هذه المزامنة مع نص حقل الإدخال باستخدام الحدث input، ويعني ذلك أنّ إطار عمل Vue يزامن البيانات بعد كل ضغطة مفتاح، في حين يتسبب المعدِّل ‎.lazy في أن يستخدِم الموجّه v-model الحدث change بدلًا من ذلك، وهذا يعني أنّ Vue لن يزامن البيانات إلّا عندما يفقد حقل الإدخال التركيز أو عند إرسال الاستمارة، وهذا منطقي أكثر لأننا نحتاج إلى البيانات النهائية فقط. ملاحظة: يمكن استخدام المعدِّلَين ‎.trim و‎.lazy مع بعضهما البعض من خلال استخدامهما بالشكل v-model.lazy.trim="label"‎. عدّل السمة v-model إلى سلسلة lazy و trim كما هو موضَّح أعلاه، ثم اختبر تطبيقك مرةً أخرى، وجرّب إرسال قيمة بمسافة فارغة في كل نهاية مثلًا. تمرير البيانات إلى العناصر الآباء مع الأحداث المخصصة يجب الآن تمرير عنصر المهام الذي أنشأناه إلى المكوِّن App من خلال جعل المكوِّن ToDoForm يصدر حدثًا مخصَّصًا يمرِّر البيانات، وجعل المكون App يستمع إلى هذا الحدث، وتعمل هذه الطريقة بصورة مشابهة جدًا للأحداث الأصيلة مع عناصر HTML، إذ يمكن للمكوِّن الابن إصدار حدث يمكن الاستماع إليه باستخدام الموجِّه v-on. لنضِف الحدث todo-added إلى الحدث onSubmit الخاص بالمكوِّن ToDoForm، كما يمكن إصدار الأحداث المخصَّصة بالصورة this.$emit("event-name")‎، ويجدر بالذكر أنّ معالجات الأحداث حساسة لحالة الأحرف ولا يمكن أن تتضمن مسافات، وتُحوَّل قوالب Vue إلى أحرف صغيرة، مما يعني أنها لا تستطيع الاستماع إلى الأحداث المُسمَّاة بأحرف كبيرة. ضَع ما يلي بدلًا من التابع console.log()‎ الموجود في التابع onSubmit()‎: this.$emit("todo-added"); ارجع بعد ذلك إلى المكوِّن App.vue وأضِف الخاصية methods إلى كائن المكوِّن الذي يحتوي على التابع addToDo()‎ كما هو موضّح أدناه، إذ يمكن لهذا التابع فقط إظهار العبارة To-do added على الطرفية حاليًا. export default { name: 'app', components: { ToDoItem, ToDoForm }, data() { return { ToDoItems: [ { id:uniqueId('todo-'), label: 'Learn Vue', done: false }, { id:uniqueId('todo-'), label: 'Create a Vue project with the CLI', done: true }, { id:uniqueId('todo-'), label: 'Have fun', done: true }, { id:uniqueId('todo-'), label: 'Create a to-do list', done: false } ] }; }, methods: { addToDo() { console.log('To-do added'); } } }; أضِف بعد ذلك مستمع حدث إلى الحدث todo-added في العنصر <to-do-form></to-do-form>، إذ يستدعي هذا المستمع التابع addToDo()‎ عند إطلاق الحدث، وسيبدو المستمع بالصورة ‎@todo-added="addToDo"‎ باستخدام الاختصار @: <to-do-form @todo-added="addToDo"></to-do-form> يجب أن ترى سجل الطرفية من التابع addToDo()‎ عند إرسال المكوِّن ToDoForm، ويُعَدّ ذلك أمرًا جيدًا، لكننا ما زلنا لا نعيد أيّ بيانات إلى المكوِّن App.vue، ويمكن ذلك عن طريق إعادة وسائط إضافية إلى الدالة this.$emit()‎ في المكوِّن ToDoForm، إذ نريد في حالتنا تمرير بيانات العنصر label مع الحدث عند إطلاقه من خلال تضمين البيانات التي نريد تمريرها بوصفها معاملًا آخرًا في التابع ‎$emit()‎ بالصورة this.$emit("todo-added", this.label)‎، وهذا مشابه لكيفية تضمين أحداث جافاسكربت الأصيلة للبيانات باستثناء أنّ أحداث Vue المخصَّصة التي لا تتضمن أيّ كائن حدث افتراضيًا، وبالتالي سيتطابق الحدث المنطلق مع أيّ كائن ترسله مباشرةً، كما سيكون كائن الحدث في حالتنا سلسلةً نصيةً فقط، لذا عدِّل التابع onSubmit()‎ كما يلي: onSubmit() { this.$emit('todo-added', this.label) } يمكن التقاط هذه البيانات ضمن المكوِّن App.vue من خلال إضافة معامِل إلى التابع addToDo()‎ الذي يتضمن عنصر label خاص بعنصر المهام الجديد، لذا ارجع إلى المكوِّن App.vue وعدّله ليبدو كما يلي: methods: { addToDo(toDoLabel) { console.log('To-do added:', toDoLabel); } } إذا اختبرت استمارتك مرةً أخرى، فسترى أنّ أيّ نص تدخله مسجَّل في الطرفية عند الإرسال، كما يمرِّر Vue تلقائيًا الوسائط بعد أن يكون اسم الحدث في this.$emit()‎ هو معالِج الأحداث خاصتك. إضافة المهام الجديدة إلى بياناتنا يجب الآن إضافة عنصر يمثِّل بيانات المكوِّن ToDoForm التي أصبحت متوفرةً في المكوِّن App.vue في المصفوفة ToDoItems، ويمكن ذلك عن طريق دفع كائن عنصر مهام جديد إلى المصفوفة التي تحتوي على البيانات الجديدة. عدِّل التابع addToDo()‎ كما يلي: addToDo(toDoLabel) { this.ToDoItems.push({id:uniqueId('todo-'), label: toDoLabel, done: false}); } اختبر الاستمارة مرةً أخرى، إذ يُفترَض أن ترى عناصر مهام جديدة تُلحَق بنهاية القائمة، وإذا أرسلتَ الاستمارة وحقل الإدخال فارغ، فستُضاف عناصر المهام التي لا تحتوي على نص إلى القائمة، ويمكن إصلاح ذلك من خلال منع تشغيل الحدث todo-added عندما يكون الاسم فارغًا، وبما أنّ الاسم قد أزيل باستخدام الموجِّه ‎.trim، فيجب اختبار السلسلة النصية الفارغة فقط، لذا ارجع إلى المكوِّن ToDoForm وعدِّل التابع onSubmit()‎ كما يلي، وإذا كانت قيمة الحقل label فارغةً، فيجب عدم إصدار الحدث todo-added. onSubmit() { if(this.label === "") { return; } this.$emit('todo-added', this.label); } جرّب استمارتك مرةً أخرى، إذ لن تتمكّن الآن من إضافة عناصر فارغة إلى قائمة المهام. استخدام الموجه v-model لتحديث قيمة حقل الإدخال لا يزال العنصر <input> يحتوي على القيمة القديمة بعد الإرسال، ويمكن إصلاح ذلك لأننا نستخدم الموجّه v-model لربط البيانات بالعنصر <input> في المكوِّن ToDoForm، فإذا ضبطنا معامِل الاسم name ليكون سلسلة نصية فارغة، فسيُحدَّث حقل الإدخال. عدِّل التابع onSubmit()‎ الخاص بالمكوِّن ToDoForm كما يلي: onSubmit() { if(this.label === "") { return; } this.$emit('todo-added', this.label); this.label = ""; } إذا نقرتَ الآن على زر "الإضافة Add"، فسيمسح حقل إدخال المهمة الجديدة "new-todo-input" نفسه. الخلاصة يمكننا الآن إضافة عناصر المهام إلى استمارتنا، وبالتالي بدأ تطبيقنا الآن يصبح تفاعليًا، ولكننا تجاهلنا شكله وتنسيقه تمامًا، وسنركز في المقال التالي على إصلاح ذلك، كما سنتعرف على الطرق المختلفة التي يوفرها إطار العمل Vue لمكوّنات التنسيق. ترجمة -وبتصرّف- للمقال Adding a new todo form: Vue events, methods, and models. اقرأ أيضًا عرض مكونات Vue.js إنشاء المكونات في تطبيق Vue.js الموجهات الشرطية والتكرارية في Vue.js
  9. لدينا حتى الآن مكوِّن يعمل بنجاح، ونحن الآن جاهزون لإضافة عدة مكوّنات ToDoItem إلى تطبيقنا، إذ سنتعلّم في هذا المقال كيفية إضافة مجموعة من بيانات عناصر المهام إلى المكوِّن App.vue التي سنكرّرها ونعرضها ضمن المكونات ToDoItem باستخدام الموجّه v-for. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ تُكتَب مكوِّنات Vue بوصفها مجموعةً من كائنات جافاسكربت التي تدير بيانات التطبيق وصيغة القوالب المستنِدة إلى لغة HTML المرتبطة مع بنية DOM الأساسية، وستحتاج إلى طرفية مثبَّت عليها node و npm لتثبيت Vue ولاستخدام بعض ميزاته الأكثر تقدمًا مثل مكوّنات الملف المفرد Single File Components أو دوال التصيير Render. الهدف: تعلّم كيفية تكرار مصفوفة من البيانات وتصييرها أو عرضها وإخراجها في مكوّنات متعددة. عرض القوائم باستخدام الموجه v-for يجب أن نكون قادرين على تصيير عناصر مهام متعددة باستخدام الموجِّه v-for في Vue لكي تكون قائمة المهام فعّالةً، إذ يسمح هذا الموجِّه المبني مسبقًا بتضمين حلقة ضمن قالبنا مع تكرار تصيير ميزة القالب لكل عنصر في المصفوفة، إذ سنستخدِمه للتكرار ضمن مصفوفة من عناصر المهام وعرضها في تطبيقنا ضمن مكوّنات ToDoItem منفصلة. إضافة بعض البيانات لعرضها يجب أولًا الحصول على مصفوفة من عناصر المهام من خلال إضافة الخاصية data إلى كائن المكوّن App.vue، وتحتوي هذه الخاصية على الحقل ToDoItems الذي تكون قيمته مصفوفةً من عناصر المهام، كما سنضيف لاحقًا آليةً لإضافة عناصر مهام جديدة، ولكن يمكننا البدء ببعض عناصر المهام المقلِّدة Mock Items، إذ سيُمثَّل كل عنصر من عناصر المهام بكائن له الخاصيتان name و done. أضِف بعض نماذج عناصر المهام كما يلي، وبالتالي تكون لديك بعض البيانات المتاحة للتصيير باستخدام الموجّه v-for: export default { name: 'app', components: { ToDoItem }, data() { return { ToDoItems: [ { label: 'Learn Vue', done: false }, { label: 'Create a Vue project with the CLI', done: true }, { label: 'Have fun', done: true }, { label: 'Create a to-do list', done: false } ] }; } }; يمكننا الآن بعد أن أصبح لدينا قائمة بالعناصر، استخدام الموجِّه v-for لعرض هذه العناصر، إذ تُطبَّق الموجَّهات Directives على العناصر مثل السمات الأخرى، كما يمكنك في حالة الموجِّه v-for استخدام صيغة خاصة مشابهة لحلقة for...in في جافاسكربت هي v-for="item in items"‎، إذ تُعَدّ items هي المصفوفة التي نريد تكرار عناصرها؛ أما item، فهو مرجع للعنصر الحالي في المصفوفة. يرتبط الموجِّه v-for بالعنصر الذي تريد تكراره، ويصيّر ذلك العنصر وأبناؤه، كما نريد في حالتنا عرض العنصر <li> لكل عنصر مهمةً ضمن المصفوفة ToDoItems، ثم نريد تمرير البيانات من كل عنصر مهمة إلى المكوِّن ToDoItem. السمة key هناك جزء آخر من الصيغة المُستخدَمة مع الموجِّه v-for يجب معرفته وهو السمة key، إذ يحاول Vue تحسين تصيير العناصر في القائمة من خلال تصحيح عناصر القائمة حتى لا يعيد إنشاءها في كل مرة تتغير فيها القائمة، لذلك يحتاج إلى "مفتاح key" فريد للعنصر نفسه الذي يرتبط مع الموجِّه v-for للتأكد من أنه يعيد استخدام عناصر القائمة بطريقة مناسبة. يجب أن تكون قيم السمات key سلسلةً نصيةً أو قيمًا عدديةً للتأكد من أنّ Vue يمكنه الموازنة بين هذه السمات بدقة، كما يمكن أن يكون استخدام حقل الاسم name أمرًا رائعًا، ولكن يتحكّم به إدخال المستخدِم، مما يعني أنه لا يمكننا ضمان أن تكون الأسماء فريدةً، كما يمكننا استخدام التابع lodash.uniqueid()‎ كما فعلنا في المقال السابق. استورد أولًا lodash.uniqueid إلى المكوِّن App بالطريقة نفسها التي استخدمناها مع المكوِّن ToDoItem كما يلي: import uniqueId from 'lodash.uniqueid'; أضِف بعد ذلك حقل المعرّف id لكل عنصر في المصفوفة ToDoItems، وأسند القيمة uniqueId('todo-')‎ لكل معرِّف، إذ يجب أن تبدو محتويات العنصر <script> في App.vue الآن كما يلي: import ToDoItem from './components/ToDoItem.vue'; import uniqueId from 'lodash.uniqueid' export default { name: 'app', components: { ToDoItem }, data() { return { ToDoItems: [ { id: uniqueId('todo-'), label: 'Learn Vue', done: false }, { id: uniqueId('todo-'), label: 'Create a Vue project with the CLI', done: true }, { id: uniqueId('todo-'), label: 'Have fun', done: true }, { id: uniqueId('todo-'), label: 'Create a to-do list', done: false } ] }; } }; أضِف الموجِّه v-for والسمة key إلى العنصر <li> في قالب App.vue كما يلي: <ul> <li v-for="item in ToDoItems" :key="item.id"> <to-do-item label="My ToDo Item" :done="true"></to-do-item> </li> </ul> إذا أجرينا هذا التغيير، فيمكن لكل تعبير جافاسكربت بين وسوم <li> الوصول إلى قيمة item وإلى سمات المكوِّن الأخرى، وهذا يعني أنه يمكننا تمرير حقول كائنات العنصر إلى المكوِّن ToDoItem وتذكّر استخدام صيغة v-bind، ويُعَدّ ذلك مفيدًا حقًا لأننا نريد أن تعرض عناصر المهام خاصيات label بوصفها تسميةً أو عنوانًا لها وليس عنوانًا ثابتًا "My Todo Item"، كما نريد أن تعكس حالة التحديد الخاصيات done بحيث لا تُضبَط دائمًا على القيمة done="false"‎. عدِّل السمة label="My ToDo Item"‎ إلى ‎:label="item.label"‎ والسمة ‎:done="false"‎ إلى ‎:done="item.done"‎ كما يلي: <ul> <li v-for="item in ToDoItems" :key="item.id"> <to-do-item :label="item.label" :done="item.done"></to-do-item> </li> </ul> إذا نظرت إلى تطبيقك المُشغَّل الآن، فسيُظهِر عناصر المهام بأسمائها الصحيحة، وإذا فحصت الشيفرة البرمجية، فسترى أنّ جميع المدخلات لها معرِّفات فريدة مأخوذة من الكائن في المكوِّن App. إجراء بعض التعديلات يمكنك إجراء عملية إعادة البناء Refactoring هنا، إذ يمكننا تحويل المعرِّف إلى خاصية بدلًا من إنشاء معرِّفات لمربعات الاختيار ضمن المكوِّن ToDoItem، وبالرعم من أنّ ذلك لا يُعَدّ ضروريًا، إلّا أنه يسهّل علينا إدارته نظرًا لأنه يجب إنشاء معرِّف فريد لكل عنصر مهمة على أية حال. أضف خاصية id الجديدة إلى المكون ToDoItem. اجعل هذه الخاصية إجباريةً واجعل نوعها String. احذف الحقل id من السمة data لمنع تعارض الأسماء. لم نَعُد نستخدِم التابع uniqueId، لذلك يجب إزالة السطر التالي، وإلا فسيعطي تطبيقك خطأً. import uniqueId from 'lodash.uniqueid';‎ يجب أن تبدو محتويات العنصر <script> في المكوِّن ToDoItem كما يلي: export default { props: { label: {required: true, type: String}, done: {default: false, type: Boolean}, id: {required: true, type: String} }, data() { return { isDone : this.done, } }, } مرّر item.id الآن في المكون App.vue بوصفه خاصيةً إلى المكوِّن ToDoItem، إذ يجب أن يبدو قالب App.vue الآن كما يلي: <template> <div id="app"> <h1>My To-Do List</h1> <ul> <li v-for="item in ToDoItems" :key="item.id"> <to-do-item :label="item.label" :done="item.done" :id="item.id"></to-do-item> </li> </ul> </div> </template> إذا نظرت إلى موقعك المُصيَّر الآن، فيجب أن يبدو كما هو، ولكن تعني عملية إعادة البناء أن معرّفنا id مأخوذ من البيانات الموجودة ضمن App.vue وهو مُمرَّر إلى المكون ToDoItem بوصفه خاصية Prop مثل أي شيء آخر، لذلك أصبحت الأمور الآن منطقيةً ومتناسقةً أكثر. الخلاصة لدينا الآن عينة من البيانات وحلقة تأخذ كل جزء من هذه البيانات وتصيّره ضمن المكوِّن ToDoItem في تطبيقنا، إذ يجب الآن السماح لمستخدمينا بإدخال عناصر مهامهم في التطبيق، ولذلك سنحتاج إلى نص حقل الإدخال <input> وحدث يُطلَق عند إرسال البيانات وتابع للإطلاق بهدف إضافة البيانات وتصيير القائمة بالإضافة إلى نموذج للتحكم في البيانات، وهذا هو موضوعنا في المقال التالي. ترجمة -وبتصرّف- للمقال Rendering a list of Vue components. اقرأ أيضًا المقال السابق: إنشاء المكونات في تطبيق Vue.js مدخل إلى التعامل مع المكونات في Vue.js النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js
  10. حان الوقت الآن للتعمق أكثر في إطار العمل Vue وإنشاء مكوّن مخصَّص، إذ سنبدأ بإنشاء مكوِّن لتمثيل كل عنصر في قائمة المهام، كما سنتعرّف على بعض المفاهيم المهمة مثل استدعاء المكونات ضمن مكونات أخرى وتمرير البيانات إليها باستخدام الخاصيات Props وحفظ حالة البيانات. ملاحظة: إذا كنت بحاجة إلى التحقق من شيفرتك مقابل نسخة شيفرتنا، فيمكنك العثور على نسخة نهائية من نموذج شيفرة تطبيق Vue في المستودع todo-vue أو يمكنك الحصول على إصدار حي وقيد التشغيل منه. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ تُكتَب مكونات Vue بوصفها مجموعة من كائنات جافاسكربت التي تدير بيانات التطبيق وصيغة القوالب المستنِدة إلى لغة HTML المرتبطة مع بنية DOM الأساسية، وستحتاج إلى طرفية مثبَّت عليها node و npm لتثبيت Vue ولاستخدام بعض ميزاته الأكثر تقدمًا مثل مكونات الملف المفرد Single File Components أو دوال التصيير Render. الهدف: تعلّم كيفية إنشاء مكوِّن Vue وتصييره ضمن مكوِّن آخر، وتمرير البيانات إليه باستخدام الخاصيات، وحفظ حالته. إنشاء المكون ToDoItem لننشئ مكوننا الأول الذي سيعرض عنصر مهمة واحدًا، إذ سنستخدمه لبناء قائمة المهام. أنشئ ملفًا جديدًا بالاسم ToDoItem.vue في المجلد moz-todo-vue/src/components، ثم افتح الملف في محرّر الشيفرات. أنشئ قسم قالب المكوِّن من خلال إضافة العنصر <template></template> في أعلى الملف. أنشئ القسم <script></script> بعد قسم القالب، ثم أضف فيه كائن تصدير افتراضي export default {}‎، وهو كائن مكوِّنك. يجب أن يبدو ملفك الآن كما يلي: <template> </template> <script> export default {}; </script> يمكننا الآن إضافة محتوى فعلي إلى الملف ToDoItem، إذ يُسمَح حاليًا لقوالب Vue بإضافة عنصر جذر واحد فقط، كما يجب استخدام عنصر واحد لتغليف كل شيء ضمن قسم القالب، ولكن سيتغير ذلك في الإصدار رقم 3 من Vue،إذ سنستخدِم العنصر <div> لهذا العنصر الجذر. أضف عنصر <div> فارغ ضمن قالب المكوِّن. أضِف مربع اختيار وعنوانًا label مقابلًا ضمن العنصر <div>. أضِف السمة id إلى مربع الاختيار، والسمة for التي تربط مربع الاختيار مع العنوان label كما هو موضّح فيما يلي: <template> <div> <input type="checkbox" id="todo-item" /> <label for="todo-item">My Todo Item</label> </div> </template> استخدام المكون TodoItem ضمن تطبيقك لم نضِف المكوِّن إلى تطبيقنا حتى الآن، لذلك لا توجد طريقة لاختباره ومعرفة ما إذا كان كل شيء على ما يرام أم لا، إذًا لنضفه الآن. افتح الملف App.vue مرة أخرى. أضِف السطر التالي لاستيراد المكون ToDoItem في أعلى الوسم <script>: import ToDoItem from './components/ToDoItem.vue'; أضِف الخاصية components ضمن كائن المكوِّن ثم أضِف ضمنها المكوِّن ToDoItem لتسجيله. يجب أن تبدو محتويات الوسم <script> الآن كما يلي: import ToDoItem from './components/ToDoItem.vue'; export default { name: 'app', components: { ToDoItem } }; هذه هي الطريقة نفسها التي سُجِّل بها المكوِّن HelloWorld بواسطة واجهة CLI الخاصة بإطار العمل Vue سابقًا. يمكنك تصيير المكوِّن ToDoItem فعليًا في التطبيق من خلال الانتقال إلى العنصر <template> واستدعائه بوصفه عنصر بالشكل: <to-do-item></to-do-item> ولاحظ أنّ اسم ملف المكوِّن وتمثيله في جافاسكربت يكون دائمًا في حالة أحرف باسكال PascalCase مثل ToDoList، ويكون العنصر المخصّص المكافئ دائمًا في نمط أحرف أسياخ الشواء Kebab-case مثل <to-do-list>. أنشئ قائمةً غير مرتبة <ul> بعد العنصر <h1> تحتوي على عنصر قائمة واحد <li>. أضِف العنصر <to-do-item></to-do-item> ضمن عنصر القائمة. يجب أن تبدو محتويات العنصر <template> في الملف App.vue الآن كما يلي: <div id="app"> <h1>To-Do List</h1> <ul> <li> <to-do-item></to-do-item> </li> </ul> </div> إذا تحقّقتَ من تطبيقك المُصيَّر مرةً أخرى، فيجب أن ترى الآن العنصر ToDoItem المُصيَّر الذي يتكون من مربع اختيار وعنوان label. إضافة خاصيات للمكونات لا يزال المكوِّن ToDoItem غير مفيد للغاية لأنه يمكننا تضمينه مرةً واحدة فقط في الصفحة، إذ يجب أن تكون المعرّفات فريدةً، وليس لدينا طريقة لضبط نص العنوان ولا يُعَدّ ذلك ديناميكيًا. ما نحتاجه الآن هو حالة المكوِّن التي يمكن تحقيقها من خلال إضافة الخاصيات Props إلى المكوِّن، إذ تشبه الخاصيات مدخلات دالة ما، وتعطي قيمة الخاصية للمكونات حالة أولية تؤثر على عرضها. تسجيل الخاصيات هناك طريقتان لتسجيل الخاصيات في Vue هما: الطريقة الأولى هي سرد الخاصيات بوصفها مصفوفةً من السلاسل النصية فقط، إذ تقابل كل مدخلة في المصفوفة اسم خاصية. الطريقة الثانية هي تعريف الخاصيات بوصفها كائنات، إذ يقابل كل مفتاح اسم الخاصية، كما يسمح سرد الخاصيات بوصفها كائنات بتحديد القيم الافتراضية وتحديد الخاصيات بأنها إجبارية وتطبيق أنواع الكائن الأساسية (أنواع جافاسكربت الأولية تحديدًا)، بالإضافة إلى إجراء تحقق بسيط من صحة الخاصية. ملاحظة: لا يحدث التحقق من صحة الخاصية إلا في وضع التطوير، لذلك لا يمكنك الاعتماد عليها بصورة صارمة في عملية الإنتاج، كما تُستدعَى دوال التحقق من صحة الخاصية قبل إنشاء نسخة من المكوِّن، لذلك لا يمكنها الوصول إلى حالة المكوِّن أو الخاصيات الأخرى، وسنستخدِم تابع تسجيل الكائن بالنسبة لهذا المكوِّن. ارجع إلى الملف ToDoItem.vue، ثم أضِف الخاصية props ضمن كائن التصدير default {}‎ الذي يحتوي على كائن فارغ، وبعدها أضف ضمن هذا الكائن خاصيتين مع المفتاحين label و done، وتذكَّر أنّ قيمة المفتاح label يجب أن تكون كائنًا مع خاصيتن (أو Props كما يطلق عليها في سياق توفرهما للمكوّنات) وهما: الأولى هي الخاصية required التي ستكون لها القيمة trueمما يخبر Vue أننا نتوقع أن يكون لكل نسخة من هذا المكوِّن حقل عنوان أو تسمية label، وسيعطي Vue تحذيرًا إذا لم يتضمن المكوِّن ToDoItem على حقل عنوان. الثانية هي خاصية النوع type. اضبط قيمة هذه الخاصية على نوع جافاسكربت String (لاحظ الحرف الكبير "S")، وهذا يخبر Vue أننا نتوقع أن تكون قيمة هذه الخاصية سلسلةً نصيةً. أما بالنسبة للخاصية done، فأضِف أولًا الحقل default مع القيمة false، وهذا يعني أنّ الخاصية done ستكون لها القيمة false عند عدم تمريرها إلى المكوِّن ToDoItem، وضَع في الحسبان أنّ هذا ليس مطلوبًا، إذ نحتاج فقط إلى الحقل default مع الخاصيات غير المطلوبة، وأضف بعد ذلك حقل النوع type مع القيمة Boolean، وهذا يخبر Vue أننا نتوقع أن يكون لخاصية القيمة value النوع المنطقي boolean في جافاسكربت. يجب أن يبدو كائن المكوِّن الآن كما يلي: <script> export default { props: { label: { required: true, type: String }, done: { default: false, type: Boolean } } }; </script> استخدام الخاصيات المسجلة يمكننا الآن بعد تعريف هذه الخاصيات ضمن كائن المكوِّن استخدام هذه القيم المتغيرة في قالبنا ولنبدأ بإضافة الخاصية label إلى قالب المكوِّن. ضع مكان محتويات العنصر <label> القيمة {{label}} في عنصر القالب <template>، إذ تُعَدّ {{}} صيغة قوالب خاصة في Vue تتيح طباعة نتيجة تعابير جافاسكربت المُعرَّفة في الصنف Class ضمن القالب، بما في ذلك القيم والتوابع، كما يُعرَض المحتوى ضمن {{}} بوصفه نصًا وليس شيفرة HTML، وبالتالي سنطبع قيمة الخاصية label في هذه الحالة. يجب أن يبدو قسم قالب المكوِّن الآن كما يلي: <template> <div> <input type="checkbox" id="todo-item" /> <label for="todo-item">{{label}}</label> </div> </template> ارجع إلى متصفحك وسترى عنصر المهام مُصيَّرًا كما كان سابقًا، ولكن بدون العنصر label، وانتقل إلى أدوات التطوير DevTools في متصفحك وسترى تحذيرًا في الطرفية كما يلي: [Vue warn]: Missing required prop: "label" found in ---> <ToDoItem> at src/components/ToDoItem.vue <App> at src/App.vue <Root> يكون هذا بسبب أننا ميّزنا الخاصية label بوصفها خاصيةً مطلوبةً أو إجباريةً، لكننا لم نعطِ المكوِّن هذه الخاصية مطلقًا، إذ حدّدنا المكان الذي نريد استخدامه ضمن القالب، لكننا لم نمرّره إلى المكوِّن عند استدعائه، إذًا لنصلح ذلك. أضف الخاصية label إلى المكوِّن <to-do-item> في الملف App.vue مثل سمة HTML العادية تمامًا: <to-do-item label="My ToDo Item"></to-do-item> سترى الآن العنصر label في تطبيقك دون تحذير في الطرفية مرةً أخرى. كائن الخاصية data في Vue إذا عدّلتَ قيمة الخاصية label المُمرَّرة إلى المكون <to-do-item> في تطبيقك، فيجب أن تراها مُحدَّثةً. لدينا الآن مربع اختيار مع عنصر label قابل للتحديث، ولكننا لا نطبّق حاليًا أيّ شيء باستخدام الخاصية done، إذ يمكننا تحديد مربعات الاختيار في واجهة المستخدِم، ولكن لا يوجد مكان في التطبيق نسجل فيه ما إذا كان عنصر المهام المطلوب قد اكتمل فعليًا أم لا. يمكنك تحقيق ذلك من خلال ربط الخاصية done الخاصة بالمكوِّن مع السمة checked في العنصر <input>، بحيث يمكن أن تكون بمثابة سجل لما إذا كان مربع الاختيار محددًا أم لا، إذ يجب أن تعمل الخاصيات بوصفها رابط بيانات أحادي الاتجاه، ويجب ألّا يغير المكوِّن قيمة خاصياته أبدًا، إذ يمكن أن تجعل خاصيات تعديل المكونات تنقيح الأخطاء تحديًا، فإذا مُرِّرت قيمةً إلى عدة أبناء، فيمكن أن يكون تتبُّع مصدر تغييرات هذه القيمة أمرًا صعبًا، كما يمكن أن يؤدي تغيير الخاصيات إلى إعادة تصيير المكوّنات، لذا فسيؤدي تغيّر خاصيات المكوِّن إلى إعادة تصييره، مما يؤدي بدوره إلى حدوث التغيّر مرةً أخرى. يمكننا حل هذه المشكلة من خلال إدارة الحالة done باستخدام الخاصية data في Vue، إذ تُعَدّ الخاصية data المكان الذي يمكنك من خلاله إدارة حالة المكوِّن المحلية، فهي توجد ضمن كائن المكوِّن جنبًا إلى جنب مع الخاصية props ولها البنية التالية: data() { return { key: value } } لاحظ أنّ الخاصية data هي دالة للحفاظ على قيم البيانات فريدة لكل نسخة من المكوِّن في وقت التشغيل، إذ تُستدعَى الدالة بصورة منفصلة لكل نسخة من المكوِّن. فإذا عرّفتَ الخاصية data بوصفها كائن فقط، فستشترك جميع نسخ هذا المكوِّن في القيم نفسها، وهذا أحد الآثار الجانبية التي لا نريدها للطريقة التي يسجِّل بها Vue المكوّنات، كما يمكنك استخدام this للوصول إلى خاصيات المكوّن والخاصيات الأخرى من data وسنرى مثالًا عن ذلك لاحقًا. ملاحظة: بما أن الطريقة التي يعمل بها this يمكن استخدامها في الدوال السهمية Arrow Function أو الارتباط بسياق الآباء، فلن تتمكن من الوصول إلى أيّ من السمات الضرورية من data إذا استخدمتَ هذه الدالة، لذلك لا تستخدِمها مع الخاصية data. لنضِف الخاصية data إلى المكوِّن ToDoItem، إذ سيعيد ذلك كائنًا يحتوي على خاصية واحدة سنسميها isDone التي تكون قيمتها this.done. عدّل كائن المكوِّن كما يلي: export default { props: { label: { required: true, type: String }, done: { default: false, type: Boolean } }, data() { return { isDone: this.done }; } }; يربط Vue جميع خاصياتك بنسخة المكوِّن مباشرةً، لذلك لا يتعين علينا استدعاء this.props.done، ويربط السمات الأخرى مثل data و methods و computed وغيرها مباشرةً بنسخة المكوِّن لجعلها متاحةً لقالبك، لكن يجب الاحتفاظ بالمفاتيح فريدة لهذه السمات، وهذا هو السبب في أننا أطلقنا على سمة data الاسم isDone عوضًا عن done. يجب الآن ربط الخاصية isDone بالمكوِّن، إذ يملك Vue بنية صيغة لربط تعابير جافاسكربت بعناصر ومكونات HTML بطريقة مشابهة لكيفية استخدام Vue لتعابير {{}} لعرض تعابير جافاسكربت ضمن القوالب، وهذه الصيغة هي v-bind التي تبدو كما يلي: v-bind:attribute="expression" وبالتالي ستسبق العبارة v-bind:‎ أيّ سمة أو خاصية تريد ربطها، ويمكنك استخدام اختصار للخاصية v-bind في أغلب الأحيان، وهذا الاختصار هو استخدام تقطتين قبل السمة أو الخاصية، لذلك تعمل العبارة ‎:attribute="expression"‎ بطريقة مشابهة للعبارة v-bind:attribute="expression"‎، لذلك يمكننا استخدام v-bind لربط الخاصية isDone مع السمة checked في العنصر <input> في حالة استخدام مربع اختيار ضمن المكوِّن ToDoItem، كما أنّ الأمرَين التاليَين متكافئان: <input type="checkbox" id="todo-item" v-bind:checked="isDone" /> <input type="checkbox" id="todo-item" :checked="isDone" /> أنت حر في استخدام النمط الذي تريده، ولكن يُفضَّل الحفاظ على كل شيء متسقًا قدر الإمكان، وبما أنّ استخدام صيغة الاختصار أكثر شيوعًا، فسنلتزم بهذا النمط في هذا المقال. عدّل العنصر <input> الآن ليشمل ‎:checked="isDone"‎. اختبر مكوِّنك عن طريق تمرير ‎:done="true"‎ إلى استدعاء المكوِّن ToDoItem في الملف App.vue، ولاحظ أنك تحتاج إلى استخدام صيغة v-bind، لأنّ القيمة true ستُمرَّر بوصفها سلسلةً نصيةً بخلاف ذلك، كما يجب تحديد مربع الاختيار المعروض. <template> <div id="app"> <h1>My To-Do List</h1> <ul> <li> <to-do-item label="My ToDo Item" :done="true"></to-do-item> </li> </ul> </div> </template> حاول تغيير القيمة true إلى false، ثم أعِد تحميل تطبيقك لترى كيفية تغيّر الحالة. إعطاء المهام معرفا فريدا رائع، أصبح لدينا الآن مربع اختيار يعمل بنجاح، إذ يمكننا ضبط الحالة برمجيًا، لكن يمكننا حاليًا إضافة مكوِّن ToDoList واحد فقط إلى الصفحة لأن المعرّف id ثابت، مما يؤدي إلى حدوث أخطاء في التقنية المساعدة لأننا بحاجة هذا المعرّف لربط العناوين أو التسميات إلى مربعات الاختيار المقابلة لها بصورة صحيحة، ويمكن إصلاح هذا الخطأ من خلال ضبط المعرّف id برمجيًا في بيانات المكوِّن. يمكننا استخدام التابع uniqueid()‎ الخاص بحزمة Lodash للمساعدة في إبقاء الفهرس فريدًا، إذ تصدِّر هذه الحزمة دالةً تأخذ سلسلةً نصيةً وتضيف عددًا صحيحًا فريدًا إلى نهاية البادئة، وسيكون هذا كافيًا لإبقاء معرِّفات المكوّنات فريدة. يجب إضافة هذه الحزمة إلى مشروعنا باستخدام npm، لذا أوقف خادمك وأدخِل الأمر التالي في طرفيتك: npm install --save lodash.uniqueid ملاحظة: إذا أردت استخدام yarn، فيمكنك كتابة الأمر yarn add lodash.uniqueid. يمكننا الآن استيراد هذه الحزمة إلى المكوِّن ToDoItem، لذا أضِف السطر التالي قبل العنصر <script> في الملف ToDoItem.vue: import uniqueId from 'lodash.uniqueid'; أضِف بعد ذلك الحقل id إلى الخاصية data بحيث يبدو كائن المكوِّن بالصورة التالية، إذ يعيد التابع uniqueId()‎ البادئة todo-‎ مع سلسلة نصية فريدة ملحقَة بها: import uniqueId from 'lodash.uniqueid'; export default { props: { label: { required: true, type: String }, done: { default: false, type: Boolean } }, data() { return { isDone: this.done, id: uniqueId('todo-') }; } }; اربط بعد ذلك المعرِّف id مع كل سمة id الخاصة بمربع الاختيار والسمة for الخاصة بالعنوان أو التسمية، وعدِّل السمات id و for الحالية كما يلي: <template> <div> <input type="checkbox" :id="id" :checked="isDone" /> <label :for="id">{{label}}</label> </div> </template> الخلاصة لدينا حتى الآن المكوِّن ToDoItem الذي يعمل بنجاح، ويمكنه تمرير عنوان أو تسمية لعرضها، كما سيخزّن حالته المحدَّدة، وسيُصيَّر مع معرِّف id فريد في كل مرة يُستدعَى فيها، كما يمكنك التحقق مما إذا كانت المعرّفات الفريدة تعمل عن طريق إضافة المزيد من استدعاءات المكونات <to-do-item> مؤقتًا في الملف App.vue ثم التحقق من خرجها المُصيَّر باستخدام أدوات التطوير DevTools في متصفحك. نحن الآن جاهزون لإضافة عدة مكونات ToDoItem إلى تطبيقنا، إذ سنتعلّم في المقال التالي كيفية إضافة مجموعة من بيانات عناصر المهام إلى المكوِّن App.vue، والتي سنكرّرها ونعرضها ضمن المكوّنات ToDoItem باستخدام الموجّه v-for. ترجمة -وبتصرّف- للمقال Creating our first Vue component. اقرأ أيضًا المقال السابق: مدخل إلى إطار العمل Vue.js مدخل إلى التعامل مع المكونات في Vue.js التعامل مع دخل المستخدم عن طريق نماذج الإدخال في Vue.js النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js
  11. سنلقي في هذا المقال نظرةً على خلفية إطار العمل Vue.js، وسنتعلمّ كيفية تثبيته وإنشاء مشروع جديد ودراسة بنية المشروع بأكمله مع مكوّناته، بالإضافة إلى كيفية تشغيله محليًا وتجهيزه للبناء. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ تُكتَب مكونات Vue بوصفها مجموعةً من كائنات جافاسكربت التي تدير بيانات التطبيق وصيغة القوالب المستنِدة إلى لغة HTML المرتبطة مع بنية نموذج DOM الأساسية، كما ستحتاج إلى طرفية مثبَّتٌ عليها node و npm لتثبيت Vue ولاستخدام بعض ميزاته الأكثر تقدمًا مثل مكوّنات الملف المفرد Single File Components أو دوال التصيير Render. الهدف: إعداد بيئة تطوير Vue المحلية وإنشاء تطبيق بسيط وفهم أساسيات كيفية عمله. يُعَدّ Vue.js إطار عمل جافاسكربت حديث يوفِّر تسهيلات مفيدةً للتحسين التدريجي على عكس العديد من أطر العمل الأخرى، إذ يمكنك استخدام إطار عمل Vue لتحسين شيفرة HTML الحالية، وبالتالي يمكنك استخدامه بوصفه بديلًا مؤقتًا لمكتبة ما مثل مكتبة jQuery. كما يمكنك استخدام Vue لكتابة تطبيقات الصفحة الواحدة Single Page Applications -أو SPA اختصارًا-، مما يتيح لك إنشاء شيفرة توصيف يديرها Vue بالكامل، كما يمكن أن يحسّن ذلك من تجربة المطوِّر وأدائه عند التعامل مع التطبيقات المعقّدة، كما يتيح لك الاستفادة من المكتبات للتوجيه من طرف العميل وإدارة الحالة عندما تحتاج ذلك، إذ يتخذ Vue نهجًا وسطيًا لاستخدام الأدوات مثل التوجيه من طرف العميل وإدارة الحالة، في حين يحتفظ فريق Vue الأساسي بالمكتبات المقترحة لهذه الوظائف، إلا أنها ليست مجمعةً مباشرةً ضمنه، مما يتيح اختيار مكتبة إدارة توجيه أو حالة مختلفة إذا ناسبت تطبيقك أكثر. كما يوفِّر Vue نهجًا تقدميًا لكتابة شيفرة التوصيف، ويتيح مثل معظم الإطارات العمل الأخرى إنشاء كتل قابلة لإعادة الاستخدام من شيفرة التوصيف باستخدام المكونات، كما تُكتَب مكونات Vue باستخدام صيغة قوالب HTML خاصةً، وإذا احتجت إلى تحكِّم أكبر مما تسمح به صيغة HTML، فيمكنك كتابة دوال JSX أو جافاسكربت الصرفة لتعريف مكوناتك. يمكن أن ترغب لاحقًا في إبقاء كتاب أساسيات إطار العمل Vue.js من أكاديمية حسوب وتوثيق API مفتوحَين في تبويبات أخرى، بحيث يمكنك الرجوع إليهما إذا أردتَ مزيدًا من المعلومات حول أيّ موضوع فرعي، كما يمكنك الاطلاع على موازنة جيدة بين Vue والعديد من الأطر الأخرى في توثيق Vue، ولكن يُحتمَل أن تكون هذه الموازنة منحازة. هذه المقالة جزء من سلسلة تمهيدية حول إطار العمل Vue.js وإليك كامل مقالات السلسلة: مدخل إلى إطار العمل Vue.js إنشاء المكونات في تطبيق Vue.js عرض مكونات Vue.js شرح مفهوم الأحداث والتوابع والنماذج في Vue.js إضافة تنسيق للمكونات واستعمال الخاصية computed في تطبيق Vue.js العرض الشرطي في إطار العمل Vue.js إدارة العناصر باستخدام خاصيات ref في إطار العمل Vue.js تثبيت Vue.js يمكنك استخدام إطار عمل Vue في موقع قائم من خلال وضع أحد عناصر <script> التالية في الصفحة، مما يتيح بدء استخدام Vue على المواقع الحالية، لذلك يفتخر Vue بكونه إطار عمل تقدمي، إذ يُعَدّ ذلك خيارًا رائعًا عند نقل مشروع موجود مسبقًا باستخدام مكتبة مثل مكتبة jQuery إلى Vue، وبالتالي يمكنك استخدام الكثير من ميزات Vue الأساسية مثل السمات والمكونات المُخصَّصة وإدارة البيانات. سكربت التطوير Development Script: غير مُحسَّن، ولكنه يتضمن تحذيرات الطرفية، وهذا رائع لعملية التطوير. <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> سكربت الإنتاج Production Script: إصدار مُحسَّن، ويتضمّن الحد الأدنى من تحذيرات الطرفية، كما يوصَى بتحديد رقم الإصدار عند تضمين Vue في موقعك بحيث لا تؤدي أيّ تحديثات لإطار العمل إلى تعطيل موقعك المباشر دون علمك. <script src="https://cdn.jsdelivr.net/npm/vue@2"></script> لكن هذا النهج له بعض القيود، إذ ستحتاج إلى استخدام حزمة NPM الخاصة بإطار عمل Vue لإنشاء تطبيقات أكثر تعقيدًا، مما سيتيح استخدام ميزات Vue المتقدمة والاستفادة من الحزم مثل WebPack، كما توجد واجهة سطر الأوامر CLI لتبسيط عملية التطوير، مما يسهّل إنشاء التطبيقات باستخدام Vue، إذ ستحتاج إلى ما يلي لاستخدام حزمة npm وواجهة CLI: تثبيت Node.js 8.11+‎. npm أو yarn. ملاحظة: إن لم تثبّت ما سبق مسبقًا، فتعرف على المزيد حول تثبيت npm و Node.js. شغّل الأمر التالي في طرفيتك لتثبيت CLI: npm install --global @vue/cli أو استخدم yarn: yarn global add @vue/cli يمكنك بعد ذلك فتح الطرفية في المجلد الذي تريد إنشاء المشروع فيه، وتشغيل الأمر vue create <project-name>‎، ثم ستعطيك واجهة CLI قائمةً بإعدادات المشروع التي يمكنك استخدامها، وهناك عدد قليل منها مُعَدّ مسبقًا، ويمكنك إعدادها بنفسك، إذ تتيح لك هذه الخيارات إعداد أشياء مثل شيفرة TypeScript وكشف الأخطاء المحتمَلة Linting والتوجيه vue-router والاختبار وغير ذلك. تهيئة مشروع جديد سننشئ نموذجًا لتطبيق قائمة المهام لاستكشاف ميزات Vue المختلفة، إذ سنبدأ باستخدام واجهة سطر أوامر Vue لإنشاء إطار عمل تطبيق جديد لبناء تطبيقنا فيه، لذا اتبع الخطوات التالية: اكتب الأمر cd في الطرفية للانتقال إلى المكان الذي تريد فيه إنشاء تطبيقك، ثم شغّل الأمر vue create moz-todo-vue. استخدم مفاتيح الأسهم ومفتاح Enter لتحديد خيار "تحديد الميزات يدويًا Manually select features". تسمح لك القائمة الأولى التي ستظهر لك باختيار الميزات التي تريد تضمينها في مشروعك، وتأكد من تحديد "Babel" و "Linter / Formatter"، فإذا لم تكن مُحدَّدةً، فاستخدم مفاتيح الأسهم ومفتاح المسافة للتبديل فيما بينها، ثم اضغط Enter للمتابعة. ستحدِّد بعد ذلك إعدادًا للميزة "linter / formatter"، لذا انتقل إلى الخيار "Eslint مع منع الأخطاء فقط Eslint with error prevention only" واضغط على Enter مرةً أخرى، إذ سيساعدنا ذلك على اكتشاف الأخطاء الشائعة دون مبالغة في الصرامة. سيُطلَب منك بعد ذلك ضبط نوع كشف الأخطاء المحتمَلة linting الآلي الذي تريده، لذا حدِّد الخيار "فحص الأخطاء عند الحفظ Lint on save"، مما سيؤدي إلى التحقق من وجود أخطاء عند حفظ ملف ضمن المشروع، ثم اضغط على Enter للمتابعة. ستحدِّد الآن كيف تريد إدارة ملفات الإعداد الخاصة بك، لذا سيضع الخيار "In dedicated config files" إعدادات الضبط الخاصة بك لأشياء مثل ESLint في ملفاتها المخصَّصة، وسيضع الخيار الآخر "In package.json" جميع إعدادات الضبط الخاصة بك في الملف package.json الخاص بالتطبيق، لذا حدِّد الخيار "In dedicated config files" واضغط على Enter. أخيرًا، ستُسأَل عن الحفظ على أساس إعداد مسبق للخيارات المستقبلية، فإذا أردتَ استخدام هذه الإعدادات وإلغاء الإعدادات المسبقة الحالية وتريد استخدامها مرةً أخرى، فاكتب y؛ وإلّا فاكتب n. ستبدأ واجهة CLI الآن في إنشاء الشيفرة المساعدة Scaffolding لمشروعك، وتثبيت كل اعتمادياتك. إذا لم تشغّل واجهة CLI الخاصة بإطار العمل Vue سابقًا، فسيظهر سؤال آخر، إذ سيُطلَب منك اختيار مدير الحزم، ويمكنك استخدام مفاتيح الأسهم لتحديد مدير الحزم الذي تفضله ليكون مدير الحزم الافتراضي من الآن فصاعدًا، فإذا احتجتَ استخدام مدير حزم مختلف بعد ذلك، فيمكنك تمرير الراية التالية عند تشغيل الأمر vue create: ‎--packageManager=<package-manager>‎ وبالتالي إذا أردت إنشاء مشروع moz-todo-vue باستخدام مدير الحزم npm واخترت yarn سابقًا، فيمكنك تشغيل الأمر التالي: vue create moz-todo-vue --packageManager=npm بنية المشروع إذا سار كل شيء على ما يرام، فيُفترَض إنشاء سلسلة من الملفات والمجلدات لمشروعك أهمها ما يلي: ‎.eslintrc.js: ملف إعداد أداة ESLint الذي يمكنك استخدامه لإدارة قواعد كشف الأخطاء المحتملة. babel.config.js: ملف إعداد Babel الذي يحوِّل ميزات جافاسكربت الحديثة المستخدَمة في شيفرة التطوير إلى صيغة أقدم أكثر توافقًا مع المتصفحات في شيفرة الإنتاج، ويمكنك تسجيل إضافات Babel الإضافية في هذا الملف. ‎.browserslistrc: هو إعداد لقائمة المتصفحات، ويمكنك استخدامه للتحكُّم في المتصفحات التي تعمل أدواتك على تحسينها. public: يحتوي هذا المجلد على الأصول الساكنة المنشورة، ولكن لم يعالجها Webpack أثناء البناء باستثناء الملف index.html الذي يحصل على بعض المعالجة. favicon.ico: الرمز أو الأيقونة المفضلة Favicon لتطبيقك، وهو شعار Vue حاليًا. index.html: هو قالب تطبيقك، حيث يُشغَّل تطبيق Vue من صفحة HTML هذه، ويمكنك استخدام صيغة قوالب Lodash لتعديل القيم فيها، ولاحظ أنه لا يُستخدَم هذا القالب لإدارة تخطيط تطبيقك، بل هو مخصَّص لإدارة ملفات HTML الساكنة الموجودة خارج تطبيق Vue، إذ يُعدَّل هذا الملف في حالات الاستخدام المتقدِّمة فقط. src: يحتوي هذا المجلد على مركز تطبيق Vue. main.js: هو نقطة الدخول إلى تطبيقك، إذ يهيّئ هذا الملف حاليًا تطبيق Vue ويشير إلى عنصر HTML في الملف index.html الذي يجب ربطه مع تطبيقك، كما يُعَدّ هذا الملف المكانَ الذي تسجِّل فيه المكونات العامة أو مكتبات Vue الإضافية في أغلب الأحيان. App.vue: هو مكون المستوى الأعلى في تطبيق Vue. components: يُعَد هذا المجلد المكان الذي تحتفظ فيه بمكوناتك، ويحتوي حاليًا على مثال لمكوِّن واحد فقط. assets: هذا المجلد مخصص لتخزين الأصول الساكنة مثل ملفات CSS والصور، وبما أنّ هذه الملفات موجودة في المجلد المصدر، فيمكن أن يعالجها Webpack، وهذا يعني أنه يمكنك استخدام المعالِجات المسبقة مثل Sass/SCSS أو Stylus. ملاحظة: يمكن وجود مجلدات أخرى بناءً على الخيارات التي تحددها عند إنشاء مشروع جديد، فإذا اخترت موجّهًا مثلًا، فسيكون لديك أيضًا المجلد views. ملفات ‎.vue مكونات الملف المفرد تُعَدّ المكونات جزءًا أساسيًا من إنشاء التطبيقات في Vue كما هو الحال في العديد من أطر عمل الواجهة الأمامية، إذ تسمح هذه المكونات بتقسيم تطبيق كبير إلى كتل بناء منفصلة يمكن إنشاؤها وإدارتها، ونقل البيانات بين بعضها البعض كما هو مطلوب. يمكن أن تساعدك هذه الكتل الصغيرة في معرفة ما تفعله شيفرتك البرمجية واختبارها. يمكن أن تشجعك بعض أطر العمل على فصل شيفرات القالب والمنطق والتصميم إلى ملفات منفصلة، ولكن يتبع Vue النهج المعاكس، إذ يتيح لك باستخدام مكونات الملف المفرد Single File Components تجميع قوالبك والسكربتات المقابلة وشيفرة CSS معًا في ملف واحد ينتهي باللاحقة ‎.vue، وتعالج أداة بناء JS -مثل Webpack- هذه الملفات، مما يعني أنه يمكنك الاستفادة من أدوات وقت البناء في مشروعك، وبالتالي يمكنك استخدام أدوات مثل Babel و TypeScript و SCSS وغيرها لإنشاء مكونات أكثر تعقيدًا. تُضبَط المشاريع المُنشَأة باستخدام واجهة CLI الخاصة بإطار عمل Vue لاستخدام ملفات ‎.vue مع Webpack، فإذا نظرت ضمن المجلد src في المشروع الذي أنشأناه باستخدام CLI، فسترى أول ملف ‎.vue وهو App.vue. App.vue افتح الملف App.vue وسترى أنه يتكون من ثلاثة أجزاء هي: <template> و <script> و <style> التي تحتوي على معلومات قالب المكوِّن وسكربتاته وتنسيقه، وتشترك كافة مكونات الملف المفرد في البنية الأساسية نفسها، كما يحتوي عنصر القالب <template> على بنية شيفرة التوصيف ومنطق عرض مكونك، كما يمكن أن يحتوي قالبك على أيّ شيفرة HTML صالحة، بالإضافة إلى بعض الصيغ الخاصة بإطار عمل Vue التي سنشرحها لاحقًا. ملاحظة: يمكنك استخدام صيغة قالب Pug بدلًا من لغة HTML القياسية من خلال ضبط السمة lang في الوسم <template> بالصورة <template lang="pug"‎>، إذ سنلتزم باستخدام لغة HTML القياسية في هذا المقال، ولكن يجب أن تعرف أنّ ذلك ممكن. يحتوي العنصر <script> على المنطق الذي لا يُعرَض من المكوِّن، إذ يجب أن يحتوي الوسم <script> على كائن جافاسكربت JS افتراضي مُصدَّر، ويُعَدّ هذا الكائن المكان الذي تسجِّل فيه المكونات محليًا، وتعرِّف مدخلات المكوّن (الخاصيات Props)، وتتعامل مع الحالة المحلية، وتعرِّف التوابع وغير ذلك، كما ستعمل خطوة البناء على معالجة هذا الكائن وتحويله مع قالبك إلى مكوّن Vue باستخدام الدالة render()‎. يضبط التصدير الافتراضي في حالة App.vue اسم المكوّن على App ويسجّل المكوّن HelloWorld من خلال إضافته إلى الخاصية components، فإذا سجّلتَ أحد المكوّنات بهذه الطريقة، فهذا يعني أنك تسجله محليًا، ولا يمكن استخدام المكوّنات المسجلة محليًا إلا ضمن المكوّنات التي تسجلها، لذلك يجب استيرادها وتسجيلها في كل ملف مكوّن يستخدمها. يمكن أن يكون هذا مفيدًا لتقسيم الحزم أو تقنية هز الشجرة Tree Shaking، إذ لا تحتاج كل صفحة في تطبيقك إلى جميع المكوّنات بالضرورة. import HelloWorld from './components/HelloWorld.vue'; export default { name: 'App', components: { // ‫يمكنك تسجيل المكونات محليًا هنا. HelloWorld } }; ملاحظة: إذا أردت استخدام صيغة لغة TypeScript، فيجب ضبط السمة lang في الوسم <script> للإشارة إلى المصرِّف الذي تستخدمه لغة TypeScript بالصورة: <script lang="ts"‎>. يُعَدّ العنصر <style> المكان الذي تكتب فيه شيفرة CSS الخاصة بالمكوّن، فإذا أضفتَ السمة scoped بالصورة <style scoped>، فسيحدِّد إطار العمل Vue نطاق التنسيقات لمحتويات مكوّن الملف المفرد SFC، إذ يعمل هذا العنصر بطريقة مشابهة لحلول CSS في JS، ولكنه يسمح لك فقط بكتابة شيفرة CSS عادية. ملاحظة: إذا اخترتَ معالج CSS مسبَق عند إنشاء المشروع باستخدام واجهة سطر الأوامر CLI، فيمكنك إضافة السمة lang إلى الوسم <style> بحيث يمكن معالجة المحتويات باستخدام Webpack في وقت البناء مثل الوسم <style lang="scss"‎> الذي يسمح باستخدام صيغة SCSS في معلومات التنسيق. تشغيل التطبيق محليا تأتي واجهة CLI الخاصة بإطار العمل Vue مع خادم تطوير مضمَّن، مما يتيح تشغيل تطبيقك محليًا لتتمكّن من اختباره بسهولة دون الحاجة إلى إعداد خادم بنفسك، إذ تضيف CLI الأمر serve إلى الملف package.json الخاص بالمشروع بوصفه سكربت npm بحيث يمكنك تشغيله بسهولة. شغّل الأمر npm run serve في طرفيتك أو الأمر yarn serve إذا أردت استخدام yarn، إذ يجب أن ينتج شيء يشبه ما يلي: INFO Starting development server... 98% after emitting CopyPlugin DONE Compiled successfully in 18121ms App running at: - Local: <http://localhost:8080/> - Network: <http://192.168.1.9:8080/> Note that the development build is not optimized. To create a production build, run npm run build. إذا انتقلت إلى العنوان المحلي في تبويب متصفح جديد مثل العنوان http://localhost:8080 أو يمكن أن يختلف بناءً على إعداداتك، فيجب أن ترى تطبيقك، إذ يجب أن يحتوي التطبيق حاليًا على رسالة ترحيب، ورابطًا إلى توثيق Vue، وروابطًا إلى الإضافات التي أضفتها عند تهيئة التطبيق باستخدام CLI، بالإضافة إلى بعض الروابط المفيدة الأخرى إلى مجتمع Vue ونظامه البيئي. إجراء بعض التعديلات على التطبيق التعديل الأول الذي سنجريه على التطبيق هو حذف شعار Vue، لذا افتح الملف App.vue واحذف العنصر <img> من قسم القالب: <img alt="Vue logo" src="./assets/logo.png"> إذا كان خادمك قيد التشغيل، فيُفترَض أن ترى الشعار محذوفًا من الموقع المُصيَّر مباشرةً تقريبًا، فلنحذف الآن المكوّن HelloWorld من القالب. أولًا احذف السطر التالي: <HelloWorld msg="Welcome to Your Vue.js App"/> إذا حفظت الملف App.vue الآن، فسيعطي التطبيق المُصيَّر خطأً لأننا سجَّلنا المكوّن ولكننا لم نستخدِمه، إذ يجب أيضًا إزالة الأسطر الموجودة ضمن العنصر <script> الذي يستورِد المكوّن ويسجله، لذا احذف الأسطر التالية: import HelloWorld from './components/HelloWorld.vue' components: { HelloWorld } يجب ألّا يعرض تطبيقك المُصيَّر أيّ خطأ حاليًا، وإنما سيعرض صفحةً فارغةً فقط، إذ لا يوجد حاليًا محتوًى مرئيًا ضمن عنصر القالب <template>. لنضِف عنصر <h1> جديد ضمن العنصر <div id="app"‎>، وبما أننا سننشئ تطبيق قائمة المهام، فلنضبط نص العنوان ليكون "To-Do List" كما يلي: <template> <div id="app"> <h1>To-Do List</h1> </div> </template> سيعرض التطبيق العنوان كما هو متوقع. الخلاصة تعلّمنا في هذا المقال بعض أفكار إطار العمل Vue وأنشأنا شيفرة تطبيقنا المساعدة وفحصناه وأجرينا بعض التعديلات الأولية؛ أما الآن فسنذهب إلى أبعد من ذلك وننشئ تطبيق قائمة المهام الأساسي الذي يسمح بتخزين قائمة بالعناصر وإلغاء تحديدها عند الانتهاء وترشيح جميع المهام والمهام المكتملة والمهام غير المكتملة في القائمة، كما سنبني في المقال التالي أول مكوّن مخصَّص، وسنتعرّف على بعض المفاهيم المهمة مثل تمرير الخاصيات وحفظ حالة البيانات. ترجمة -وبتصرّف- للمقال Getting started with Vue. اقرأ أيضًا مدخل إلى التعامل مع المكونات في Vue.js التعامل مع دخل المستخدم عن طريق نماذج الإدخال في Vue.js النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js
  12. سنتعرف في هذا المقال على التوجيه Routing أو الترشيح المستند إلى عنوان URL كما يشار إليه في بعض الأحيان، والذي سنستخدمه لتوفير عنوان URL فريد لكل عرض من عروض المهام الثلاثة: "جميع المهام All" و"المهام النشطة Active" و"المهام المكتملة Completed"، كما سنتطرق إلى كيفية استكشاف الأخطاء وإصلاحها. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ يُعَدّ فهم ميزات جافاسكربت الحديثة مثل الأصناف Classes والوحدات Modules وما إلى ذلك مفيدًا للغاية، لأن إطار العمل Ember يستخدمها بكثرة. الهدف: التعرف على كيفية تطبيق التوجيه في إطار العمل Ember، وتوفير مزيد من الموارد لتعلم إطار عمل Ember ومعلومات استكشاف الأخطاء وإصلاحها. الترشيح المستند إلى عنوان URL يأتي إطار عمل Ember مع نظام التوجيه ذي التكامل الوثيق مع عنوان URL الخاص بالمتصفح، إذ يجب عند كتابة تطبيقات الويب تمثيل الصفحة بعنوان URL بحيث إذا كانت الصفحة بحاجة إلى تحديث، فلن يُفاجَأ المستخدِم بحالة تطبيق الويب، وسيتمكّن من الانتقال مباشرةً إلى عروض التطبيق المهمة. لدينا حاليًا صفحة "جميع المهام All" إذ لن نطبّق ترشيحًا فيها في الوقت الحالي، لكن يجب إعادة تنظيمها قليلًا للتعامل مع عرض مختلف للمهام النشطة والمهام المكتملة، كما يحتوي تطبيق Ember على مسار تطبيق افتراضي مرتبط بالقالب app/templates/application.hbs الذي يُعَدّ نقطة الدخول إلى تطبيق قائمة المهام، لذلك يجب إجراء بعض التغييرات للسماح بالتوجيه Routing. إنشاء المسارات لننشئ ثلاثة مسارات جديدة هي: "Index" و "Active" و "Completed" من خلال إدخال الأوامر التالية في الطرفية ضمن المجلد الجذر لتطبيقك: ember generate route index ember generate route completed ember generate route active لا ينتج عن الأمرَين الثاني والثالث ملفات جديدةً فحسب، وإنما يُعدَّل الملف app/router.js الموجود مسبقًا الذي يحتوي على المحتويات التالية: import EmberRouter from '@ember/routing/router'; import config from './config/environment'; export default class Router extends EmberRouter { location = config.locationType; rootURL = config.rootURL; } Router.map(function() { this.route('completed'); this.route('active'); }); يتصرّف الملف router.js بوصفه خريطة موقع sitemap للمطورين ليتمكنوا من رؤية كيفية تنظيم التطبيق بأكمله بسرعة، كما أنه يخبر إطار العمل Ember بكيفية التفاعل مع مسارك عند تحميل بيانات عشوائية أو التعامل مع الأخطاء أثناء تحميل تلك البيانات أو تفسير الأجزاء الآلية لعنوان URL مثلًا، وبما أنّ بياناتنا ساكنة، فلن نصل إلى أيّ من هذه الميزات الرائعة، لكننا سنظل نتأكد من أنّ المسار يوفِّر الحد الأدنى من البيانات المطلوبة لعرض الصفحة. لم يضِف إنشاء المسار "Index" سطر تعريف مسار إلى الملف router.js، لأن "Index" هي كلمة خاصة تشير إلى المسار الافتراضي للتصيير والتحميل وغير ذلك كما هو الحال مع التنقل باستخدام عنوان URL وتحميل وحدة جافاسكربت JavaScript. يمكنك تعديل طريقتنا القديمة في تصيير تطبيق قائمة المهام من خلال استبدال الاستدعاء {{outlet}} باستدعاء المكون TodoList من قالب التطبيق ب، مما يعني تصيير أيّ مسار فرعي في المكان نفسه دون الانتقال إلى تبويب جديد. انتقل إلى الملف todomvc/app/templates/application.hbs وضَع مكان السطر التالي: <TodoList /> ما يلي: {{outlet}} يمكننا الآن إدخال استدعاء المكوّن TodoList في قوالب index.hbs و completed.hbs و active.hbs الموجودة أيضًا في مجلد القوالب. ضَع مكان السطر التالي: {{outlet}} ما يلي: <TodoList /> إذا جرّبت التطبيق مرةً أخرى وزرتَ أيًّا من المسارات الثلاثة: localhost:4200 localhost:4200/active localhost:4200/completed فسترى الشيء نفسه بالضبط، إذ سيصيَّر القالب الذي يتوافق مع المسار المحدد ("Active" أو "Completed" أو "Index") في كل عنوان URL للمكون <TodoList /‎>. يُحدَّد الموقع في الصفحة حيث يُصيَّر المكوّن <TodoList /‎> باستخدام {{ outlet }} في المسار الأب وهو في هذه الحالة application.hbs، ولدينا الآن مساراتنا في مكانها الصحيح، لكننا بحاجة إلى طريقة للتمييز بين كل من هذه المسارات لتظهِر ما يفترض منها أن تظهِره. ارجع مرةً أخرى إلى الملف todo-data.js الذي يحتوي مسبقًا على تابع جالب getter يعيد جميع المهام وتابع جالب آخر يعيد المهام غير المكتملة، ولكنه لا يحتوي على تابع جالب يعيد المهام المكتملة فقط، لذا لنضفه بعد التوابع الجالبة الموجودة مسبقًا: get completed() { return this.todos.filter(todo => todo.isCompleted); } النماذج يجب الآن إضافة نماذج Models إلى مسارات ملفات جافاسكربت للسماح بسهولة بإعادة مجموعات بيانات معينة لعرضها في تلك النماذج، إذ يُعَدّ النموذج model بأنه خطاف Hook دورة حياة تحميل بيانات، ولكن ليست قدرات النموذج مهمةً لنا في تطبيق TodoMVC، كما سنوفر الوصول إلى الخدمة كما فعلنا مع المكوّنات. نموذج المسار index عدِّل أولًا الملف todomvc/app/routes/index.js ليبدو كما يلي: import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; export default class IndexRoute extends Route { @service('todo-data') todos; model() { let todos = this.todos; return { get allTodos() { return todos.all; } } } } يمكننا الآن تعديل الملف todomvc/app/templates/index.hbs بحيث إذا تضمّن المكوّن <TodoList /‎>، فإنه يفعل ذلك صراحة مع النموذج المتاح، ويستدعي التابع الجالب allTodos()‎ للتأكد من ظهور جميع المهام. عدِّل السطر التالي في هذا الملف: <TodoList /> إلى ما يلي: <TodoList @todos={{ @model.allTodos }}/> نموذج المسار completed عدِّل الملف todomvc/app/routes/completed.js ليبدو كما يلي: import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; export default class CompletedRoute extends Route { @service('todo-data') todos; model() { let todos = this.todos; return { get completedTodos() { return todos.completed; } } } } يمكننا الآن تعديل الملف todomvc/app/templates/completed.hbs بحيث إذا تضمّن المكوّن ‎<TodoList /‎>‎، فإنه يفعل ذلك صراحةً مع النموذج المتاح، ويستدعي التابع الجالب completedTodos()‎ للتأكد من ظهور المهام المكتملة فقط. عدِّل السطر التالي في هذا الملف: <TodoList /> إلى ما يلي: <TodoList @todos={{ @model.completedTodos }}/> نموذج المسار active عدِّل الملف todomvc/app/routes/active.js ليبدو كما يلي: import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; export default class ActiveRoute extends Route { @service('todo-data') todos; model() { let todos = this.todos; return { get activeTodos() { return todos.incomplete; } } } } يمكننا الآن تعديل الملف todomvc/app/templates/active.hbs بحيث إذا تضمّن المكوّن <TodoList /‎>، فإنه يفعل ذلك صراحةً مع النموذج المتاح، ويستدعي التابع الجالب activeTodos()‎ للتأكد من ظهور المهام النشطة أو غير المكتملة فقط. عدِّل السطر التالي في هذا الملف: <TodoList /> إلى ما يلي: <TodoList @todos={{ @model.activeTodos }}/> لاحظ أننا نعيد كائنًا له تابع جالب بدلًا من كائن ساكن أو قائمة مهام ساكنة مثل this.todos.completed في خطافات نماذج المسار، والسبب في ذلك هو أننا نريد أن يكون للقالب مرجع آلي لقائمة المهام، فإذا أعدنا القائمة مباشرةً، فلن يُعاد حساب البيانات أبدًا، مما يؤدي إلى ظهور تنقلات فاشلة وعدم ترشيح فعلي، ويمكن إعادة البحث عن المهام من خلال تحديد تابع جالب في كائن الإعادة من بيانات النموذج بحيث تُمثَّل التعديلات التي أجريناها على قائمة المهام في القائمة المُصيَّرة. تشغيل روابط التذييل أصبحت وظائف المسار الآن في مكانها الصحيح، لكن لا يمكننا الوصول إليها من تطبيقنا، لذا لنفعِّل روابط التذييل بحيث يؤدي النقر عليها إلى الانتقال إلى المسارات المطلوبة. ارجع إلى الملف todomvc/app/components/footer.hbs وابحث عمّا يلي: <a href="#">All</a> <a href="#">Active</a> <a href="#">Completed</a> وعدِّله إلى ما يلي: <LinkTo @route='index'>All</LinkTo> <LinkTo @route='active'>Active</LinkTo> <LinkTo @route='completed'>Completed</LinkTo> يُعَدّ <LinkTo> بأنه مكوّن Ember مبني مسبقًا يعالِج جميع تغييرات الحالة عند التنقل بين المسارات، ويضبط الصنف active على أيّ رابط يطابق عنوان URL في حالة وجود رغبة في تنسيقه تنسيقًا مختلفًا عن الروابط غير النشطة. تحديث عرض المهام ضمن قائمة المهام أحد الأشياء الصغيرة الأخيرة التي نحتاج إلى إصلاحها هو أننا كنا ندخل سابقًا إلى خدمة todo-data مباشرةً ونكرّر جميع المهام ضمن الملف todomvc/app/components/todo-list.hbs كما يلي: {{#each this.todos.all as |todo| }} بما أننا نريد أن يعرض مكوّن قائمة المهام قائمةً مُرشَّحةً، فيجب تمرير وسيط إلى مكوِّن قائمة المهام يمثِّل قائمة المهام الحالية من todos كما يلي: {{#each @todos as |todo| }} يجب أن يحتوي تطبيقك الآن على روابط فعّالة في التذييل تعرض مسارات "Index" أو المسارات الافتراضية و"النشطة Active" و"المكتملة Completed". هناك الكثير مما يجب تنفيذه قبل أن يتطابق ما نفّذناه حتى الآن مع تطبيق TodoMVC الأصلي مثل تعديل المهام وحذفها واستمرارها عبر عمليات إعادة تحميل الصفحة، كما يمكنك مشاهدة تطبيق Ember المكتمل من خلال التحقق من مجلد التطبيق النهائي في مستودع شيفرة تطبيقنا للحصول عليها أو شاهد الإصدار المباشر. ادرس الشيفرة لمعرفة المزيد حول إطار عمل Ember، وراجع الفقرة التالية التي توفِّر مزيدًا من الموارد وبعض نصائح استكشاف الأخطاء وإصلاحها. استكشاف الأخطاء العامة وإصلاحها وبنى gotcha والمفاهيم الخاطئة كان آخر تحديث للقائمة التالية في شهر 6 من عام 2020. كيف يمكنني تنقيح الأخطاء التي يظهرها إطار العمل؟ توجد الإضافة ember-inspector بالنسبة للأشياء الخاصة بإطار العمل والتي تسمح بفحص ما يلي: المسارات Routes والمتحكِّمات Controllers. المكوِنات. الخدمات. الوعود Promises. البيانات وهي بيانات من واجهة برمجة تطبيقات بعيدة مثل ember-data افتراضيًا. معلومات الإهمال Deprecation Information. تصيير الأداء. اطّلع على أدوات المطوِّر التي تفيدك في تنقيح الأخطاء، وتعرّف على كيفية استخدام أدوات المطوِّر DevTools في كروم Chrome. بوجد ملفان رئيسيان من ملفات جافاسكربت هما: vendor.js و ‎{app-name}.js في أيِّ مشروع Ember افتراضي، ويُنشَآن هذان الملفان باستخدام خرائط الشيفرة البرمجية Sourcemaps، لذلك ستُحمَّل خريطة الشيفرة البرمجية وستُوضَع نقطة التوقف في شيفرة مُترجَمة مسبقًا لتسهيل الارتباط بشيفرة مشروعك باستخدام منقّح أخطاء عند فتح الملف vendor.js أو الملف ‎{app-name}.js للبحث عن الشيفرة البرمجية ذات الصلة. هل سأحتاج إلى واجهة ember-data المثبتة مسبقا؟ لن تحتاجها على الإطلاق، إذ تحل واجهة ember-data المشاكل الأكثر شيوعًا التي سيعمل بها أيّ تطبيق يتعامل مع البيانات بحيث يمكن تشغيل عميل بيانات واجهتك الأمامية، وهناك بديل شائع لأيّ عميل بيانات واجهة أمامية كامل الميزات هو Fetch API، إذ سيبدو المسار Route باستخدام fetch()‎ كما يلي باستخدام أنماط التصميم التي يوفرها إطار العمل: import Route from '@ember/routing/route'; export default class MyRoute extends Route { async model() { let response = await fetch('some/url/to/json/data'); let json = await response.json(); return { data: json }; } } لماذا لا يمكنني استخدام جافاسكربت فقط؟ هذا هو السؤال الأكثر شيوعًا الذي يسمعه مجتمع Ember من الأشخاص الذين لديهم خبرة سابقة في React، إذ يمكن استخدام صيغة JSX أو أيّ شكل آخر من أشكال إنشاء نموذج DOM، ولكن ليس هناك شيء قوي مثل نظام قوالب Ember، حيث يفرض الحد الأدنى منه قرارات معينة، ويسمح بشيفرة برمجية أكثر تناقسًا مع الحفاظ على القالب أكثر هيكلية بدلًا من ملئه بشيفرة برمجية حسب الرغبة. ما هي حالة المساعد mut؟ انتقل المساعد mut مع إطار العمل Ember عند انتقاله من البيانات ذات الاتجاهين إلى مجرى البيانات الأكثر شيوعًا والأسهل ذي الاتجاه الواحد، إذ يمكن عدّ المساعدmut بمثابة تحويل وقت البناء الذي يغلّف وسيطه بتابع ضابط setter، إذ يسمح استخدام المساعدmut بالتصريح عن دوال إعدادات القالب فقط كما يلي: <Checkbox @value={{this.someData}} @onToggle={{fn (mut this.someData) (not this.someData)}} /> بينما ستكون هناك حاجة إلى صنف مكوّن بدون استخدام المساعدmut: import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; export default class Example extends Component { @tracked someData = false; @action setData(newValue) { this.someData = newValue; } } سيُستدعَى هذا الصنف بعد ذلك في القالب كما يلي: <Checkbox @data={{this.someData}} @onChange={{this.setData}} /> يمكن أن يكون استخدام mut مرغوبًا نظرًا لاختصاره، ولكنه يحتوي على دلالات غريبة يمكن أن تتسبّب في الكثير من الارتباك لمعرفة المقصود منها. كانت هناك بعض الأفكار الجديدة التي وُضِعت معًا في شكل إضافات تستخدِم واجهات برمجة تطبيقات ember-set-helper و ember-box، ويحاول كلاهما حل مشاكل mut من خلال تقديم مفاهيم أوضح، وتجنب تحولات وقت البناء وسلوك آلة Glimmer الافتراضية الضمني. باستخدام ember-set-helper: <Checkbox @value={{this.someData}} @onToggle={{set this "someData" (not this.someData)}} /> باستخدام ember-box: {{#let (box this.someData) as |someData|}} <Checkbox @value={{unwrap someData}} @onToggle={{update someData (not this.someData)}} /> {{/let}} لا تُعَدّ أيّ من هذه الحلول شائعةً بين أعضاء المجتمع، ولا يزال الأشخاص يحاولون اكتشاف واجهة برمجة تطبيقات مريحة وبسيطة لضبط البيانات في سياق القالب فقط دون أصناف جافاسكربت الداعمة. ما هو الغرض من المتحكمات؟ المتحكِّمات Controllers هي أنماط مفردة Singletons يمكن أن تساعد في إدارة تصيير السياق الخاص بالمسار النشط، وتعمل إلى حد كبير مثل أصناف جافاسكربت الداعمة للمكوّن، والمتحكِّمات هي -من شهر 1 عام 2020- الطريقة الوحيدة لإدارة محدّدات استعلام عناوين URL، إذ يجب أن تكون المتحكِّمات خفيفةً إلى حد ما في مسؤولياتها وفي تفويض المكوّنات والخدمات حيثما أمكن ذلك. ما هو الغرض من المسارات؟ يمثِّل المسار Route جزءًا من عنوان URL عندما ينتقل المستخدِم من مكان إلى آخر في التطبيق، فالمسار له المسؤوليات التالية فقط: تحميل الحد الأدنى من البيانات المطلوبة لتصيير المسار أو عرض الشجرة الفرعية. يُعَدّ بوابة الوصول إلى المسار وإعادة التوجيه إذا لزم الأمر. التعامل مع حالات التحميل والخطأ في الحد الأدنى من البيانات المطلوبة. يحتوي المسار على ثلاثة خطّافات لدورة الحياة فقط وجميعها اختيارية وهي: beforeModel: بوابة الوصول إلى المسار. model: مكان تحميل البيانات. afterModel: للتحقق من الوصول. يتمتع المسار بالقدرة على التعامل مع الأحداث الشائعة الناتجة عن إعداد الخطاف model: loading: ما يجب تطبيقه عند تحميل الخطّاف model. error: ما يجب فعله عند حدوث خطأ في الخطّاف model. يمكن لكل من loading و error تصيير القوالب الافتراضية بالإضافة إلى القوالب المخصَّصة المحددة في مكان آخر في التطبيق، مما يوحِّد حالات التحميل أو حالات الخطأ، كما يمكن العثور على مزيد من المعلومات حول ما يمكن للمسار تنفيذه في توثيق واجهة برمجة التطبيقات API. أخيرًا، إذا احتجت إلى أي مساعدة، فأضف سؤالك ضمن قسم الأسئلة والأجوبة في أكاديمية حسوب أو ضمن مجتمع البرمجة في حسوب IO وستصل إلى إجابتك من مجتمع المطورين العرب. ترجمة -وبتصرّف- للمقالين Routing in Ember وEmber resources and troubleshooting. اقرأ أيضًا المقال السابق: تنفيذ التفاعل في تطبيق Ember: وظيفة التذييل والعرض الشرطي مقدمة إلى إطار العمل Ember بنية تطبيق إطار العمل Ember وتقسيمها إلى مكونات
  13. حان الوقت الآن لمعالجة وظيفة التذييل Footer في تطبيقنا، إذ سنحدِّث عدّاد المهام لإظهار العدد الصحيح للمهام التي يجب إكمالها، وسنطبّق التنسيق بصورة صحيحة على المهام المكتملة من خلال تحديد مربع الاختيار، كما سنفعِّل زر "مسح المهام المكتملة Clear completed"، وسنتعرّف على استخدام التصيير أو العرض الشرطي Conditional Rendering في قوالبنا (التصيير والعرض والإخراج هي مترادفات لكلمة rendering). المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ يُعَدّ فهم ميزات جافاسكربت الحديثة مثل الأصناف Classes والوحدات Modules وما إلى ذلك مفيدًا للغاية، لأن إطار العمل Ember يستخدمها بكثرة. الهدف: مواصلة تعلّم أصناف المكوّنات من خلال التعرّف على التصيير الشرطي وتفعيل بعض وظائف التذييل. توصيل السلوك بالتذييل يجب تطبيق الوظائف الثلاث التالية لكي يعمل التذييل: عدّاد المهام المعلَّقة. مرشحات لجميع المهام والمهام النشطة والمهام المكتملة. زر لمسح المهام المكتملة. أولًا، يجب إنشاء صنف Class للتذييل بما أننا نحتاج إلى الوصول إلى الخدمة من مكوّن التذييل، لذلك أدخِل الأمر التالي في الطرفية: ember generate component-class footer ثانيًا، ابحث بعد ذلك عن الملف todomvc/app/components/footer.js وعدّله إلى ما يلي: import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; export default class FooterComponent extends Component { @service('todo-data') todos; } ثالثًا، يجب الآن العودة إلى الملف todo-data.js وإضافة بعض الوظائف التي ستسمح بإعادة عدد المهام غير المكتملة لمعرفة عدد المهام المتبقية، ووظيفة مسح المهام المكتملة من القائمة التي يحتاجها زر "مسح المهام المكتملة Clear completed"، لذلك أضِف في الملف todo-data.js التابع الجالب Getter التالي بعد الجالب all()‎ الموجود سابقًا لتحديد المهام غير المكتملة: get incomplete() { return this.todos.filterBy('isCompleted', false); } يمكننا باستخدام التابع Array.Proxy.filterBy()‎ في Ember ترشيح الكائنات في المصفوفة بسهولة بناءً على شروط مساواةٍ بسيطة، إذ نريد في جزء الشيفرة السابق الحصول على جميع عناصر المهام عندما تكون الخاصية isCompleted مساوية للقيمة false، وسيُعاد حساب هذا التابع الجالب عندما تتغير قيمة الكائن في المصفوفة لأن الخاصية isCompleted مُميَّزة بالمزخرِف ‎@tracked في الكائن Todo. رابعًا، أضف بعد ذلك ما يلي بعد الدالة add(text)‎ الموجودة مسبقًا: @action clearCompleted() { this.todos = this.incomplete; } يُعَدّ ذلك رائعًا لمسح المهام، إذ نحتاج فقط إلى ضبط المصفوفة todos لتساوي قائمة المهام غير المكتملة. خامسًا، أخيرًا، يجب الاستفادة من هذه الوظيفة الجديدة في قالب footer.hbs، لذلك انتقل إلى هذا الملف الآن. سادسًا، ضَع أولًا مكان السطر التالي: <strong>0</strong> todos left ما يلي، إذ يُملَأ عدد المهام غير المكتملة بطول المصفوفة incomplete: <strong>{{this.todos.incomplete.length}}</strong> todos left سابعًا، ثم ضع مكان السطر التالي: <button type="button" class="clear-completed"> ما يلي: <button type="button" class="clear-completed" {{on 'click' this.todos.clearCompleted}}> إذا نقرتَ على الزر الآن، فسيُشغَّل الإجراء clearCompleted()‎ الذي أضفناه سابقًا، ولكن إذا حاولت النقر على زر "مسح المهام المكتملة Clear Completed"، فلن يبدو التطبيق أنه يفعل أيّ شيء بسبب عدم وجود طريقة لإكمال المهام حاليًا، كما يجب توصيل القالب todo.hbs بالخدمة، بحيث يؤدي تحديد مربع الاختيار المتعلق به إلى تغيير حالة كل مهمة. مشكلة كتابة todos بدلا من todo لدينا مشكلة صغيرة أخرى نتعامل معها، إذ تشير العبارة "todos left" إلى وجود عدد من المهام المتبقية بالرغم من وجود مهمة واحدة متبقية أحيانًا، وهذا سيء قواعديًا. يمكن حل هذه المشكلة من خلال تعديل هذا الجزء من القالب ليحتوي على التصيير الشرطي، إذ يمكنك في Ember تصيير أجزاء من القالب شرطيًا باستخدام المحتوى الشرطي مثل الكتلة البسيطة التالية: {{#if this.thingIsTrue}} Content for the block form of "if" {{/if}} ضع مكان الجزء التالي من footer.hbs: <strong>{{this.todos.incomplete.length}}</strong> todos left ما يلي: <strong>{{this.todos.incomplete.length}}</strong> {{#if this.todos.incomplete.length === 1}} todo {{else}} todos {{/if}} left سيؤدي ذلك إلى إعطاء خطأ، ولكن لا تستطيع عبارات if البسيطة هذه في Ember حاليًا اختبار تعبير معقد مثل الموازنة، وإنما تستطيع اختبار قيمة الصواب أو الخطأ فقط، لذلك يجب إضافة جالب getter إلى الملف todo-data.js لإعادة النتيجة this.incomplete.length === 1 ثم استدعاؤها في القالب. أضف الجالب الجديد الآتي إلى الملف todo-data.js بعد التوابع الجالبة الموجودة مسبقًا مباشرةً، ولاحظ أننا نحتاج إلى this.incomplete.length وليس this.todos.incomplete.length لأننا نطبّق ذلك ضمن الخدمة حيث يتوفر الجالب incomplete()‎ مباشرةً، كما أنّ محتويات الخدمة متوفرة في القالب مثل المهام todos عبر التعليمة ‎@service('todo-data') todos;‎ ضمن صنف التذييل، وبالتالي سيكون this.todos.incomplete.length هناك. get todoCountIsOne() { return this.incomplete.length === 1; } ارجع بعد ذلك إلى footer.hbs وعدِّل قسم القالب السابق الذي عدّلناه إلى ما يلي: <strong>{{this.todos.incomplete.length}}</strong> {{#if this.todos.todoCountIsOne}} todo {{else}} todos {{/if}} left احفظ الملف واختبره، وسترى الكلمة الصحيحة المُستخدَمة عندما يكون لديك عنصر واحد لتنفيذه. لاحظ صيغة كتلة if في Ember، ويمكنك استخدام الشكل المضمَّن التالي: {{if this.todos.todoCountIsOne "todo" "todos"}} استكمال المهام يجب استخدام صنف للوصول إلى الخدمة كما هو الحال مع المكونات الأخرى. إنشاء الصنف todo أولًا، شغّل الأمر التالي في الطرفية: ember generate component-class todo ثانيًا، انتقل الآن إلى الملف todomvc/app/components/todo.js وعدّل محتوياته لتبدو كما يلي لمنح المكوّن todo إمكانية الوصول إلى الخدمة: import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; export default class TodoComponent extends Component { @service('todo-data') todos; } ثالثًا، ارجع مرةً أخرى إلى ملف الخدمة todo-data.js وأضف الإجراء التالي بعد الإجراءات السابقة مباشرةً، مما سيسمح بتبديل حالة الاكتمال لكل مهمة: @action toggleCompletion(todo) { todo.isCompleted = !todo.isCompleted; } تحديث القالب لإظهار الحالة المكتملة أخيرًا، سنعدّل القالب todo.hbs بحيث ترتبط قيمة مربع الاختيار بالخاصية isCompleted في المهمة، حيث يُستدعَى التابع toggleCompletion()‎ في خدمة المهمة عند التعديل. أولًا، ابحث أولًا عن السطر التالي في الملف todo.hbs: <li> وضَع مكانه ما يلي، إذ ستلاحظ أننا نستخدم المحتوى الشرطي لإضافة قيمة الصنف إذا كان ذلك مناسبًا: <li class="{{ if @todo.isCompleted 'completed' }}"> ثانيًا، ابحث بعد ذلك عمّا يلي: <input aria-label="Toggle the completion of this todo" class="toggle" type="checkbox" > وضَع مكانه ما يلي: <input class="toggle" type="checkbox" aria-label="Toggle the completion of this todo" checked={{ @todo.isCompleted }} {{ on 'change' (fn this.todos.toggleCompletion @todo) }} > ملاحظة: يستخدِم جزء الشيفرة السابق كلمةً مفتاحيةً جديدةً خاصةً بإطار عمل Ember هي fn التي تسمح بالتطبيق الجزئي Partial Application، وهو مشابه للتابع bind لكنه لا يغير سياق الاستدعاء، ويكافئ استخدام التابع bind مع الوسيط الأول null. أعِد تشغيل خادم التطوير وانتقل إلى المضيف المحلي localhost:4200 مرةً أخرى، وسترى أنه لدينا عدّاد "المهام المتبقية todos left" وزر "المسح Clear": يمكن أن تسأل نفسك لماذا لا نطبّق التبديل على المكوِّن فقط؟ نظرًا لأن الدالة قائمة بذاتها ولا تحتاج على الإطلاق إلى أيّ شيء من الخدمة، وبما أننا في النهاية سنرغب في الاستمرار أو مزامنة جميع تغييرات قائمة المهام مع التخزين المحلي (اطّلع على الإصدار الأخير من التطبيق)، فستكون جميع عمليات تغيير الحالة المستمرة في المكان نفسه. الخلاصة يمكننا الآن وضع علامة على المهام المكتملة ومسحها أيضًا، والشيء الوحيد المتبقي لتفعيل التذييل هو عمليات ترشيح "جميع المهام All" و"المهام النشطة Active" و"المهام المكتملة Completed"، إذ سنطبّق ذلك في المقال التالي باستخدام التوجيه Routing. ترجمة -وبتصرّف- للمقال Ember Interactivity: Footer functionality, conditional rendering. اقرأ أيضًا المقال السابق: تنفيذ التفاعل في تطبيق Ember: الأحداث والأصناف والحالة بنية تطبيق إطار العمل Ember وتقسيمها إلى مكونات مقدمة إلى إطار العمل Ember
  14. سنبدأ في هذا المقال بإضافة بعض التفاعل إلى تطبيقنا مما يوفر القدرة على إضافة وعرض عناصر مهام جديدة، كما سنتعرّف على استخدام الأحداث في إطار العمل Ember، وإنشاء أصناف مكوّنات لتحتوي على شيفرة جافاسكربت JavaScript للتحكم بالميزات التفاعلية وإعداد خدمة لتتبّع حالة بيانات تطبيقنا. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ يُعَدّ فهم ميزات جافاسكربت الحديثة مثل الأصناف Classes، والوحدات Modules، وما إلى ذلك مفيدًا للغاية لأن إطار العمل Ember يستخدمها بكثرة. الهدف: معرفة كيفية إنشاء أصناف المكوّنات واستخدام الأحداث للتحكم في التفاعل وتتبع حالة التطبيق باستخدام خدمة. إضافة التفاعل لدينا الآن نسخة من تطبيق قائمة المهام todo app مقسَّمة إلى مكوّنات ومُعاد بناؤها، ولنتعرَّف على كيفية إضافة التفاعل الذي نحتاجه ليعمل التطبيق، إذ يجب التصريح عن أهداف ومسؤوليات كل مكوّن عند البدء في التفكير في التفاعل كما سنوضّح فيما يلي، ثم سنوجّهك لتتعلم كيفية تنفيذ ذلك. إنشاء المهام نريد أن نتمكن من إرسال المهمة المكتوبة في حقل إدخال المهام عندما نضغط على مفتاح Enter لتظهر في قائمة المهام، ويجب أن نكون قادرين على التقاط النص المكتوب في حقل الإدخال لتعرف شيفرة جافاسكربت ما كتبناه، كما يمكننا حفظ مهامنا وتمرير هذا النص إلى مكوّن قائمة المهام لعرضه، ويمكننا التقاط الحدث keydown باستخدام المُعدِّل on الذي يُعَدّ صيغةً مبسَّطةً في Ember للتابعَين addEventListener و removeEventListener. أضف السطر الجديد الموضَّح أدناه إلى الملف header.hbs: <input class='new-todo' aria-label='What needs to be done?' placeholder='What needs to be done?' autofocus {{on 'keydown' this.onKeyDown}} > توضَع هذه السمة الجديدة بين أقواس مزدوجة معقوصة، ويدل ذلك على أنها جزء من صيغة قالب آلي في إطار عمل Ember، فالوسيط الأول المُمرَّر إلى on هو نوع الحدث الذي يجب الاستجابة له keydown، والوسيط الأخير هو معالِج الحدث أي الشيفرة المُشغَّلة استجابةً لإطلاق الحدث keydown، في حين تشير الكلمة المفتاحية this إلى سياق Context أو نطاق Scope المكوّن الذي سيختلف بين مكوّن وآخر. يمكننا تحديد ما هو متاح ضمن this من خلال إنشاء صنف مكوّن ليتماشى مع مكوّنك، وهو صنف بلغة جافاسكربت الصرفة Vanilla JavaScript وليس له معنًى خاصًا في إطار عمل Ember باستثناء وراثة أو توسعة الصنف الأب Component. يمكنك إنشاء الصنف header ليتوافق مع مكوّن الترويسة من خلال كتابة الأمر التالي في الطرفية: ember generate component-class header مما يؤدي إلى إنشاء ملف الصنف الفارغ التالي في الملف todomvc/app/components/header.js: import Component from '@glimmer/component'; export default class HeaderComponent extends Component { } سننفّذ في هذا الملف شيفرة معالج الحدث، لذلك عدّل محتواه إلى ما يلي: import Component from '@glimmer/component'; import { action } from '@ember/object'; export default class HeaderComponent extends Component { @action onKeyDown({ target, key }) { let text = target.value.trim(); let hasValue = Boolean(text); if (key === 'Enter' && hasValue) { alert(text); target.value = '' } } } يُعَدّ المزخرِف ‎@action الشيفرة الوحيدة الخاصة بإطار العمل Ember هنا بغض النظر عن وراثة الصنف الأب Component والعناصر الخاصة بإطار العمل Ember التي نستوردها باستخدام صيغة وحدة جافاسكربت، بينما باقي الملف مكتوب بلغة جافاسكربت الصرفة Vanilla JavaScript ويمكن أن يعمل في أيّ تطبيق آخر، كما يصرِّح المزخرِف ‎@action عن أن الدالة هي إجراء action، مما يعني أنها نوع من الدوال التي ستُستدعَى من حدث وقع في القالب، كما أنّ ‎@action يربط this الخاص بالدالة بنسخة من الصنف. ملاحظة: يُعَدّ المزخرِف Decorator دالة تغليف تغلِّف وتستدعي دوالًا أو خاصيات أخرى مما يوفر وظائفًا إضافيةً، إذ يشغّل المزخرِف ‎@tracked مثلًا الشيفرة المُطبَّقة عليه، ويتتبّعه ويحدّث التطبيق تلقائيًا عند تغيير القيم. عُد إلى تبويب متصفحك مع تشغيل التطبيق، إذ يمكننا كتابة ما نريد وستظهر رسالة تنبيه تخبرنا بما كتبناه بالضبط عندما نضغط على مفتاح Enter. نحتاج الآن لمكان لتخزين المهام لتتمكن المكوّنات الأخرى من الوصول إليها. تخزين المهام باستخدام خدمة يملك إطار عمل Ember إدارة حالة مبنية مسبقًا على مستوى التطبيق يمكننا استخدامها لإدارة تخزين مهامنا، والسماح لكل مكوّن من مكوّناتنا الوصول إلى البيانات من تلك الحالة على مستوى التطبيق، إذ يستدعي إطار Ember خدمات البناء التي تبقى قيد التشغيل طوال عُمر الصفحة، إذ سيؤدي تحديث الصفحة إلى مسحها. شغّل الأمر التالي في الطرفية لإنشاء خدمة لتخزين بيانات قائمة المهام: ember generate service todo-data يعطي تشغيل الأمر السابق خرجًا يشبه الخرج التالي: installing service create app/services/todo-data.js installing service-test create tests/unit/services/todo-data-test.js مما يؤدي إلى إنشاء الملف todo-data.js ضمن المجلد todomvc/app/services لاحتواء خدمتنا، كما يحتوي هذا الملف في البداية على عبارة استيراد وصنف فارغ كما يلي: import Service from '@ember/service'; export default class TodoDataService extends Service { } يجب أولًا تتبّع كل من نص المهمة وما إذا كانت مكتملة أم لا، لذا أضف تعليمة الاستيراد التالية بعد تعليمة الاستيراد الموجودة مسبقًا: import { tracked } from '@glimmer/tracking'; أضف الصنف التالي بعد السطر السابق الذي أضفته: class Todo { @tracked text = ''; @tracked isCompleted = false; constructor(text) { this.text = text; } } يمثل الصنف السابق مهمَّة، لأنه يحتوي على الخاصية ‎@tracked text التي تحتوي على نص المهمَّة، ويحتوي على الخاصية ‎@tracked isCompleted التي تحدِّد ما إذا كانت المهام مكتملةً أم لا، فإذا أنشأنا نسخةً من هذا الصنف، فسيكون للكائن Todo قيمة text أولية تساوي النص المعطَى له عند إنشائه، وتُعطَى الخاصية isCompleted القيمة false. الجزء الوحيد الخاص بإطار عمل Ember من هذا الصنف هو المزخرِف ‎@tracked الذي يرتبط بنظام التفاعل ويسمح لإطار Ember بتحديث ما تراه في تطبيقك تلقائيًا إذا تغيرت الخاصيات المُتتبَّعة tracked، وحان الوقت الآن لإضافة شيء ما إلى جسم الخدمة، لذا أضِف أولًا تعليمة استيراد import أخرى بعد التعليمة السابقة لإتاحة الإجراءات actions ضمن الخدمة: import { action } from '@ember/object'; عدّل الكتلة export default class TodoDataService extends Service { … }‎ كما يلي: export default class TodoDataService extends Service { @tracked todos = []; @action add(text) { let newTodo = new Todo(text); this.todos = [...this.todos, newTodo]; } } ستحتفظ الخاصية todos في الخدمة بقائمة مهامنا الموجودة ضمن مصفوفة، وسنميّزها بالمزخرِف ‎@tracked لأننا نريد تحديث واجهة المستخدِم أيضًا عند تحديث قيمة الخاصية todos. يُضاف المزخرِف ‎@action إلى الدالة add()‎ التي يستدعيها القالب لربطها بنسخة الصنف. يمكن أن يكون ذلك مألوفًا في لغة جافاسكربت، ولكن لاحظ استدعاء التابع pushObject()‎ في المصفوفة todos، والسبب أن إطار عمل Ember يوسّع نموذج المصفوفات في لغة جافاسكربت افتراضيًا، مما يمنحنا توابعًا ملائمة لضمان معرفة نظام التعقّب في إطار Ember بهذه التغييرات. هناك العشرات من هذه التوابع مثل pushObject()‎ أو insertAt()‎ أو popObject()‎ التي يمكن استخدامها مع أيّ نوع وليس مع الكائنات فقط. يمنحنا التابع ArrayProxy الخاص بإطار عمل Ember توابعًا سهلة الاستخدام مثل isAny()‎ وfindBy()‎ وfilterBy()‎ لتسهيل الأمور. استخدام الخدمة من مكون الترويسة يمكننا الآن بعد أن تحديد طريقة لإضافة المهام التفاعل مع هذه الخدمة من مكوّن الإدخال في الملف header.js لبدء إضافتها فعليًا، إذ يجب أولًا حقن الخدمة في القالب باستخدام المزخرِف ‎@inject الذي سنعيد تسميته إلى ‎@service. أضف تعليمة الاستيراد التالية إلى الملف header.js بعد تعليمتَي الاستيراد الموجودتَين مسبقًا: import { inject as service } from '@ember/service'; يمكننا الآن توفير الخدمة todo-data ضمن الصنف HeaderComponent عبر الكائن todos باستخدام المزخرِف ‎@service، لذلك أضف السطر التالي بعد سطر التصدير export‎ الأول مباشرةً: @service('todo-data') todos; يمكن الآن استبدال سطر النص البديل alert(text);‎ باستدعاء الدالة add()‎ الجديدة كما يلي: this.todos.add(text); إذا جربنا ذلك في تطبيق المهام في متصفحنا من خلال كتابة الأمر npm start ثم الانتقال إلى المضيف المحلي localhost:4200، فلن يحدث أيّ شيء بعد الضغط على مفتاح Enter، إذ يُعَدّ بناء التطبيق بدون أيّ أخطاء علامةً جيدةً، ولكن يمكننا رؤية إضافة مهامنا باستخدام الفاحص Ember Inspector كما يلي: عرض المهام يجب أن تكون هناك طريقة لوضع المهام التي ننشئها فعليًا مكان مهامنا الساكنة "Buy Movie Tickets"، إذ يجب في المكوّن TodoList إخراج المهام من الخدمة وتصيير Render المكوّن Todo لكل مهمة، كما يمكن استعادة المهام من الخدمة، ولكن يحتاج المكوّن TodoList أولًا إلى صنف داعم للمكوّن لاحتواء هذه الوظيفة، لذلك اضغط على الاختصار Ctrl + C لإيقاف خادم التطوير وأدخِل الأمر التالي في الطرفية: ember generate component-class todo-list يؤدي ذلك إلى إنشاء صنف المكوّن الجديد في المجلد todomvc/app/components/todo-list.js. املأ هذا الملف بالشيفرة التالية بحيث يمكن لقالبنا الوصول إلى الخدمة todo-data باستخدام الخاصية todos، ويمكن الوصول إليها باستخدام this.todos ضمن كل من الصنف والقالب: import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; export default class TodoListComponent extends Component { @service('todo-data') todos; } تتمثّل إحدى المشاكل هنا في أن الخدمة تسمَّى todos وتُسمَّى قائمة المهام todos أيضًا، لذلك يمكننا حاليًا الوصول إلى البيانات باستخدام this.todos.todos. لا يُعَد هذا أمرًا سهلًا، لذلك سنضيف تابعًا جالبًا getter إلى الخدمة this.todos.todos بالاسم all سيمثِّل جميع المهام. ارجع إلى الملف todo-data.js وأضِف ما يلي بعد التعليمة ‎@tracked todos = [];‎: get all() { return this.todos; } يمكننا الآن الوصول إلى البيانات باستخدام this.todos.all، إذ يُعَدّ ذلك أسهل. انتقل إلى المكوّن todo-list.hbs، وضع مكان استدعاءات المكوّن الساكنة التالية: <Todo /> <Todo /> كتلة ‎#each الآلية، وهي صيغة مُبسطَّة من تابع جافاسكربت forEach()‎، إذ تنشئ كتلة ‎#each المكوّنَ <Todo /‎> لكل مهمة متوفرة في قائمة المهام التي يعيدها التابع الجالب all()‎ الخاص بالخدمة: {{#each this.todos.all as |todo|}} <Todo @todo={{todo}} /> {{/each}} لنتعرّف على محتويات الشيفرة السابقة: this: سياق التصيير أو نسخة المكوّن. todos: خاصية this التي عرّفناها في المكوّن todo-list.js باستخدام التعليمة ‎@service('todo-data') todos;‎، وهي مرجع إلى الخدمة todo-data، مما يسمح بالتفاعل مع نسخة الخدمة مباشرةً. all: جالب الخدمة todo-data الذي يعيد جميع المهام. جرّب تشغيل الخادم مرةً أخرى وانتقل إلى التطبيق، وستجده يعمل، ولكن كلما أدخلت عنصر مهمة جديد، فسيظهر عنصر قائمة جديد تحت حقل إدخال النص، وستظهر العبارة "Buy Movie Tickets" دائمًا للأسف، لأن عنوان النص في كل عنصر قائمة مضمَّنٌ في ذلك النص كما هو موضَّح في الملف todo.hbs: <label>Buy Movie Tickets</label> عدِّل السطر السابق كما يلي لاستخدام الوسيط ‎@todo الذي سيمثل المهمة التي مررناها إلى المكوّن عند استدعائه ضمن الملف todo-list.hbs في السطر <Todo @todo={{todo}} /‎>: <label>{{@todo.text}}</label> جرّبه مرةً أخرى، ويجب أن تجد الآن أنّ النص المُرسَل من حقل الإدخال <input> يظهر بصورة صحيحة في واجهة المستخدِم كما يلي: الخلاصة يمكننا الآن إضافة عناصر المهمات إلى تطبيقنا، كما يمكن تتبّع حالة البيانات باستخدام الخدمة، وسننتقل في المقال التالي إلى تشغيل وظائف التذييل Footer بما في ذلك عدّاد المهام، كما سنتعرّف على التصيير الشرطي وتصميم المهام بصورة صحيحة عند تحديدها، وسنفعّل زر "مسح المهام المكتملة Clear completed". ترجمة -وبتصرّف- للمقال Ember interactivity: Events, classes and state. اقرأ أيضًا المقال السابق: بنية تطبيق إطار العمل Ember وتقسيمها إلى مكونات مقدمة إلى إطار العمل Ember
  15. سنخطط في هذا المقال بنية تطبيق TodoMVC في إطار العمل Ember، وسنضيف توصيف HTML له، ثم سنقسّم بنية HTML إلى مكونات. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية. يُعَد فهم ميزات جافاسكربت الحديثة مثل الأصناف Classes والوحدات Modules وما إلى ذلك مفيدًا للغاية، لأن إطار العمل Ember يستخدِمها بكثرة. الهدف: تعلّم كيفية إنشاء بنية تطبيق Ember، ثم تقسيم هذه البنية إلى مكونات. التخطيط لتصميم تطبيق TodoMVC أنشأنا في المقال السابق مشروع Ember جديد ثم أضفنا وضبطنا أنماط CSS، وسنضيف الآن توصيف HTML وسنخطط بنية ودلالات تطبيق TodoMVC. يُعَدّ توصيف HTML لصفحة هبوط تطبيقنا مُعرَّفًا في المسار app/templates/application.hbs، وهو موجود مسبقًا وتبدو محتوياته حاليًا كما يلي: ‏{{!-- يعرض المكون التالي رسالة ترحيب‫ Ember الافتراضية. --}} <WelcomePage /> {{!-- لا تتردد في إزالتها --}} {{outlet}} المكوّن <WelcomePage /‎> هو مكون توفّره إضافة Ember التي تصيِّر Render صفحة الترحيب الافتراضية التي رأيناها في المقال السابق عندما انتقلنا لأول مرة إلى الخادم على المضيف المحلي localhost:4200، لكننا لا نريد ذلك، وإنما نريد أن يحتوي هذا المكون على بنية تطبيق TodoMVC، لذلك احذف محتويات الملف application.hbs واستبدلها بما يلي: <section class="todoapp"> <h1>todos</h1> <input class="new-todo" aria-label="What needs to be done?" placeholder="What needs to be done?" autofocus > </section> احفظ الملف application.hbs، إذ سيَعيد خادم التطوير الذي شغّلته سابقًا بناء التطبيق وتحديث المتصفح تلقائيًا، أي يجب أن يبدو الخرج المُصيَّر الآن كما يلي: لا يتطلب الأمر كثيرًا من الجهد لجعل توصيف HTML يبدو مثل تطبيق قائمة مهام كامل الميزات. عدِّل الملف application.hbs مرةً أخرى ليكون محتواه على النحو التالي: <section class="todoapp"> <h1>todos</h1> <input class="new-todo" aria-label="What needs to be done?" placeholder="What needs to be done?" autofocus > <section class="main"> <input id="mark-all-complete" class="toggle-all" type="checkbox"> <label for="mark-all-complete">Mark all as complete</label> <ul class="todo-list"> <li> <div class="view"> <input aria-label="Toggle the completion of this todo" class="toggle" type="checkbox" > <label>Buy Movie Tickets</label> <button type="button" class="destroy" title="Remove this todo" ></button> </div> <input autofocus class="edit" value="Todo Text"> </li> <li> <div class="view"> <input aria-label="Toggle the completion of this todo" class="toggle" type="checkbox" > <label>Go to Movie</label> <button type="button" class="destroy" title="Remove this todo" ></button> </div> <input autofocus class="edit" value="Todo Text"> </li> </ul> </section> <footer class="footer"> <span class="todo-count"> <strong>0</strong> todos left </span> <ul class="filters"> <li> <a href="#">All</a> <a href="#">Active</a> <a href="#">Completed</a> </li> </ul> <button type="button" class="clear-completed"> Clear Completed </button> </footer> </section> يجب أن يكون الخرج المُصيَّر الآن على النحو التالي: يبدو التطبيق مكتملًا، ولكن تذكّر أنه الآن مجرد نموذج أولي ساكن، لذلك يجب تقسيم توصيف HTML إلى مكونات آلية، إذ سنحوّله لاحقًا إلى تطبيق تفاعلي بالكامل. إذا نظرنا إلى الشيفرة الموجودة بجوار تطبيق المهام المُصيَّر في الشكل التالي، فهناك طرق متعددة يمكن من خلالها تحديد كيفية تقسيم واجهة المستخدِم UI، ولكن لنخطّط الآن لتقسيم توصيف HTML إلى المكونات التالية: مجموعات المكونات هي كما يلي: حقل الإدخال الرئيسي new-todo: باللون الأحمر في الشكل السابق. الجسم Body الذي يحتوي على قائمة المهام والزر الذي يضع علامة على جميع المهام المكتملة mark-all-complete: باللون الأرجواني في الشكل السابق. الزر mark-all-complete المميز بوضوح للأسباب التي سنوضّحها لاحقًا: باللون الأصفر في الشكل السابق. كل مهمة هي مكوّن لوحده: باللون الأخضر في الشكل السابق. التذييل Footer: باللون الأزرق في الشكل السابق. لاحظ أنّ مربع الاختيار mark-all-complete المميَّز باللون الأصفر يُصيَّر بجوار حقل الإدخال new-todo أثناء وجوده في القسم main لأن تنسيقات CSS الافتراضية تضع مربع الاختيار والعنصر label بقيم سالبة للإحداثيات العلوية واليسارية لتحريكهما بجوار حقل الإدخال، عوضًا عن وضعهما في القسم main. استخدام واجهة سطر الأوامر CLI لإنشاء المكونات نريد إنشاء أربعة مكونات لتمثيل تطبيقنا وهذه المكونات هي: الترويسة Header. القائمة List. عنصر المهمة Todo. التذييل Footer. استخدِم الأمر ember generate component متبوعًا باسم المكوّن لإنشائه، ولننشئ مكوّن الترويسة أولًا باتباع الخطوات التالية: أوقف تشغيل الخادم بالانتقال إلى الطرفية والضغط على الاختصار Ctrl + C. أدخِل الأمر التالي في الطرفية: ember generate component header سينشئ هذا الأمر بعض الملفات الجديدة، كما هو موضّح في ناتج الطرفية النهائي التالي: installing component create app/components/header.hbs skip app/components/header.js tip to add a class, run `ember generate component-class header` installing component-test create tests/integration/components/header-test.js يُعَدّ الملف header.hbs بأنه ملف القالب الذي سيتضمّن بنية HTML لمكوّن الترويسة فقط، وسنضيف لاحقًا الوظائف الآلية المطلوبة مثل روابط البيانات والاستجابة لتفاعل المستخدِم وما إلى ذلك. يُعَدّ الملف header-test.js ملفًا مخصَّصًا لكتابة الاختبارات الآلية للتأكد من أن تطبيقنا يستمر في العمل بمرور الوقت أثناء الترقية وإضافة الميزات وإعادة البناء وما إلى ذلك، ولن نتطرّق للاختبار في هذا المقال، ولكن يجب تطبيق الاختبار أثناء عملية التطوير وليس بعده، إذ يمكن أن تنساه لاحقًا. لننشئ شيفرة مساعِدة Scaffolding للمكوّنات الأخرى قبل إضافة أيّ شيفرة مكوّن، وبالتالي أدخِل الأوامر التالية في طرفيتك واحدًا تلو الآخر: ember generate component todo-list ember generate component todo ember generate component footer سترى الآن ما يلي في المجلد todomvc/app/components: بما أنه أصبح لدينا الآن جميع ملفات بنية المكونات، فيمكننا قص ولصق توصيف HTML لكل مكوّن من الملف application.hbs إلى كل مكوّن من هذه المكوّنات، ثم تعديل الملف application.hbs ليمثّل تجريداتنا الجديدة. أولًا، يجب تعديل الملف header.hbs ليحوي ما يلي: <input class="new-todo" aria-label="What needs to be done?" placeholder="What needs to be done?" autofocus > ثانيًا، كما يجب تعديل الملف todo-list.hbs ليحوي ما يلي: <section class="main"> <input id="mark-all-complete" class="toggle-all" type="checkbox"> <label for="mark-all-complete">Mark all as complete</label> <ul class="todo-list"> <Todo /> <Todo /> </ul> </section> ثالثًا، أضف ما يلي إلى الملف todo.hbs: <li> <div class="view"> <input aria-label="Toggle the completion of this todo" class="toggle" type="checkbox" > <label>Buy Movie Tickets</label> <button type="button" class="destroy" title="Remove this todo" ></button> </div> <input autofocus class="edit" value="Todo Text"> </li> رابعًا، يجب تعديل الملف footer.hbs بحيث يحوي ما يلي: <footer class="footer"> <span class="todo-count"> <strong>0</strong> todos left </span> <ul class="filters"> <li> <a href="#">All</a> <a href="#">Active</a> <a href="#">Completed</a> </li> </ul> <button type="button" class="clear-completed"> Clear Completed </button> </footer> خامسًا، أخيرًا، يجب تعديل محتويات الملف application.hbs بحيث تُستدَعى المكونات المناسبة كما يلي: <section class="todoapp"> <h1>todos</h1> <Header /> <TodoList /> <Footer /> </section> سادسًا، شغِّل الأمر npm start في الطرفية مرةً أخرى بعد إجراء هذه التعديلات، ثم توجَّه إلى المضيف المحلي http://localhost:4200 للتأكد من أنّ تطبيق المهام لا يزال يبدو كما كان قبل إعادة البناء. لاحظ كيف يعرض كلا عنصري المهام العبارة "Buy Movie Tickets" بسبب استدعاء المكوّن نفسه مرتين، ونَص المهمة ثابت فيه. الخلاصة يبدو كل شيء كما ينبغي، إذ نجحنا في إعادة بناء توصيف HTML إلى مكوّنات، وسنبدأ في المقال التالي بالتعرّف على إضافة التفاعل إلى تطبيق Ember. ترجمة -وبتصرّف- للمقال Ember app structure and componentization. اقرأ أيضًا المقال السابق: مقدمة إلى إطار العمل Ember
  16. سنلقي نظرةً في هذا المقال على إطار العمل Ember وآلية عمله وفوائده، وكيفية تثبيت سلسلة أدواته محليًا وإنشاء تطبيق نموذجي ثم إجراء إعداد أولي لتجهيز التطبيق للتطوير. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، كما يُعَدّ فهم ميزات جافاسكربت الحديثة مثل الأصناف Classes والوحدات Modules وما إلى ذلك مفيدًا للغاية، لأن إطار العمل Ember يستخدِمها بكثرة. الهدف: معرفة كيفية تثبيت إطار عمل Ember وإنشاء تطبيق بسيط. يُعَدّ Ember إطار عمل من النوع خدمة-مكونات Component-service يركّز على عملية تطوير تطبيقات الويب وإضفاء تجربة مميزة على واجهاتها ويقلل من الاختلافات بين التطبيقات، ويُعَدّ طبقةً حديثةً وخفيفةً فوق طبقة جافاسكربت الأصيلة Native، كما يتمتع بتوافق كبير مع الإصدارات السابقة واللاحقة لمساعدة الشركات على مواكبة أحدث إصدارات Ember وأحدث الاتفاقيات التي يقودها المجتمع. تُعَدّ المكونات حزمًا من شيفرات السلوك والتنسيق Style والتوصيف Markup التي تشبه إلى حد كبير ما توفره أطر عمل الواجهة الأمامية الأخرى مثل React و Vue و Angular، كما يوفر جانب الخدمة حالةً مشتركةً طويلة الأمد وسلوكًا وواجهةً للتكامل مع المكتبات أو الأنظمة الأخرى، إذ يُعَدّ الموجِّه Router الذي سنشرحه لاحقًا خدمةً مثلًا، وتشكّل المكونات والخدمات القسم الأكبر من تطبيق EmberJS. هذا المقال جزء من سلسلة تقديمية حول إطار العمل Ember وإليك فهرس كامل السلسلة: مقدمة إلى إطار العمل Ember بنية تطبيق إطار العمل Ember وتقسيمها إلى مكونات تنفيذ التفاعل في تطبيق Ember: الأحداث والأصناف والحالة تنفيذ التفاعل في تطبيق Ember: وظيفة التذييل والعرض الشرطي التوجيه Routing في إطار العمل Ember حالات الاستخدام يناسب إطار عمل EmberJS بناء التطبيقات التي تستهدف إحدى السمتين التاليتين أو كلتيهما: تطبيقات الصفحة الواحدة، بما في ذلك تطبيقات الويب الشبيهة بالتطبيقات الأصيلة وتطبيقات الويب التقدمية Progressive Web Apps أو PWA اختصارًا. يعمل إطار عمل Ember بصورة أفضل عندما يشكِّل الواجهة الأمامية الكاملة لتطبيقك. زيادة التماسك بين العديد من تقنيات الفرق البرمجية. تتيح أفضل الممارسات التي يدعمها المجتمع سرعة تطوير أكبر على المدى الطويل. يملك إطار عمل Ember اصطلاحات أو اتفاقيات واضحة ومفيدة لفرض التناسق ومساعدة أعضاء الفريق على العمل بسرعة. إضافات إطار عمل Ember يمتلك إطار العمل EmberJS معمارية الإضافات Plugin، مما يعني أنه يمكن تثبيت الإضافات Add-ons وتوفير وظائف إضافية دون كثير من الإعداد إذا وُجِد، ومن هذه الإضافات: PREmber: تصيير Rendering موقع ويب ساكن للمدونات أو المحتوى التسويقي. FastBoot: تصيير من طرف الخادم، بما في ذلك تحسين محركات البحث SEO، أو تحسين أداء التصيير الأولي لصفحات الويب المعقدة عالية التفاعل. empress-blog: تأليف منشورات المدونات باستخدام لغة ماركداون Markdown مع تحسين محركات البحث SEO باستخدام الإضافة PREmber. ember-service-worker: إعداد تطبيق ويب تقدمي PWA بحيث يمكن تثبيت التطبيق على الأجهزة المحمولة مثل تثبيته من متجر تطبيقات الجهاز نفسه. تطبيقات الجوال الأصيلة يمكن استخدام إطار عمل Ember مع تطبيقات الأجهزة المحمولة الأصيلة مع جسر بين تطبيقات الهاتف المحمول الأصيلة ولغة جافاسكربت مثل الجسر الذي توفره واجهة Corber. الآراء والأعراف يُعَدّ إطار العمل EmberJS أحد أكثر أطر عمل الواجهة الأمامية تشبثًا برأيه، وتُعَدّ الآراء في Ember مجموعةً من الاتفاقيات أو الأعراف التي تساعد على زيادة كفاءة المطورين، ولكن يجب تعلّم تلك الاتفاقيات، إذ تساعد الآراء التي تدعم الاتفاقيات في تقليل الاختلافات بين التطبيقات مثل اللغات المُستخدَمة والنظام المجتمعي بفضل تعريف هذه الاتفاقيات ومشاركتها، وهو هدف مشترك بين جميع الأطر المتشبثة برأيها، ويصبح المطورون بعدها أكثر قدرةً على التبديل بين المشاريع والتطبيقات دون الحاجة إلى إعادة تعلم المعمارية والأنماط والمصطلحات وما إلى ذلك، وستلاحظ خلال هذا المقال آراء إطار العمل Ember مثل اصطلاحات تسمية ملفات المكونات. ارتباط إطار عمل Ember مع لغة جافاسكربت الصرفة Vanilla JavaScript بُني إطار عمل Ember على تقنيات جافاسكربت، ويُعَدّ طبقةً رقيقةً فوق البرمجة التقليدية كائنية التوجه، مع السماح للمطورين باستخدام تقنيات البرمجة الوظيفية. يستخدِم Ember صيغتين رئيسيتين هما: جافاسكربت أو لغة TypeScript اختياريًا. لغة قوالب Ember الخاصة التي تعتمد على لغة Handlebars. تُستخدَم لغة القوالب Templating Language لتحسين عملية البناء ووقت التشغيل، وهي مجموعة شاملة من لغة HTML، وبالتالي يمكن لأيّ شخص يعرف لغة HTML تقديم مساهمات مهمة لأيّ مشروع من مشاريع Ember، كما يمكن للمصممين وغيرهم من غير المطورين المساهمة في قوالب الصفحات دون أي معرفة بلغة جافاسكربت، ثم يمكن إضافة التفاعل لاحقًا، كما تتيح لغة القوالب هذه حمولات أصول أخف نظرًا لتصريف القوالب في شيفرة ثنائية Byte Code يمكن تحليلها بسرعة أكبر من تحليل شيفرة جافاسكربت. كل شيء آخر في Ember هو جافاسكربت وخاصةً أصناف جافاسكربت التي تُعَدّ المكان الذي تعمل فيه معظم أجزاء إطار العمل حيث توجد الأصناف الأبناء، ويكون لكل نوع من الأشياء غرض مختلف وموقع متوقع مختلف ضمن مشروعك. يوضِّح الشكل التالي تأثير Ember على جافاسكربت في المشاريع النموذجية، إذ يوضِّح كيف أن أقل من 20% من شيفرة JS المكتوبة خاصةً بإطار العمل Ember. بدء استخدام Ember سننشئ أولًا نسخةً من نموذج تطبيق TodoMVC التقليدي لتعلُّم كيفية استخدام أساسيات إطار عمل Ember، إذ يُعَدّ تطبيق TodoMVC تطبيقًا أساسيًا لتتبّع المهام، ويُستخدَم في العديد من التقنيات المختلفة، وإليك نسخةً مكتملةً منه ليكون مرجع لك. إنشاء تطبيق جديد في Ember يحتوي مشروع TodoMVC على بعض المشاكل من حيث الالتزام بممارسات الويب التي يمكن الوصول إليها افتراضيًا، وهناك نوعان من مشاكل جيت هاب GitHub المفتوحة حول هذا الموضوع في مجموعة مشاريع TodoMVC هي: إضافة وصول مستخدِمي لوحة المفاتيح إلى العروض التوضيحية. إعادة تفعيل المخطط حول العناصر القابلة للتركيز. يهتم إطار Ember بموضوع إمكانية الوصول accessibility كثيرًا وأن تكون التطبيقات المبنية فيه سهلة الوصول لكامل المستخدمين حتى بمن فيهم أي إعاقة وقد وفر دليلًا شاملًا عنه، ولكن بما أنّ هذا المقال يركِّز على جانب جافاسكربت في إنشاء تطبيق ويب صغير، فإنّ قيمة TodoMVC تأتي من توفير ملفات CSS المبنيّة مسبقًا وبنية HTML الموصَى بها، مما يلغي الاختلافات بين التطبيقات، كما يسمح بإجراء موازنة أسهل بينها، وسنركِّز لاحقًا على إضافة شيفرة إلى تطبيقنا لإصلاح بعض أكبر أخطاء تطبيق TodoMVC. تثبيت أدوات Ember يستخدِم Ember واجهة سطر الأوامر CLI لبناء أجزاء من تطبيقك وإنشاء شيفرتها المساعدة Scaffolding. أولًا، ستحتاج إلى تثبيت أداة node ومدير الحزم npm قبل تمكّنك من تثبيت أداة ember-cli، كما يمكنك الانتقال إلى مقال دليل استخدام سطر الأوامر في عملية تطوير الويب من طرف العميل لمعرفة كيفية تثبيت node و npm إذا لم تكن مثبّتةً لديك مسبقًا. ثانيًا، اكتب الأمر التالي في طرفيتك لتثبيت أداة ember-cli: npm install -g ember-cli توفِّر هذه الأداة برنامج ember في طرفيتك، وتُستخدَم لإنشاء وبناء وتطوير واختبار تطبيقك وإنشاء شيفرته المساعِدة، كما يمكنك تشغيل الأمر ember --help للحصول على قائمة كاملة بالأوامر وخياراتها. ثالثًا، يمكنك إنشاء تطبيق جديد تمامًا من خلال كتابة الأمر التالي في طرفيتك، مما يؤدي إلى إنشاء مجلد جديد له الاسم todomvc ضمن المجلد الذي أنت فيه حاليًا، بحيث يحتوي على أدوات لبناء تطبيق Ember جديد، وتأكد من الانتقال إلى مكان مناسب في الطرفية مثل مجلد "سطح المكتب" أو مجلد "المستندات" لتعثر على مجلد تطبيقك بسهولة قبل تشغيل الأمر التالي: ember new todomvc أو شغّل الأمر التالي على نظام ويندوز: npx ember-cli new todomvc يؤدي هذا الأمر إلى إنشاء بيئة تطوير تطبيقات جاهزة للإنتاج تتضمن افتراضيًا الميزات التالية: خادم التطوير مع إعادة التحميل المباشر. معمارية الإضافات التي تسمح لحزم الطرف الثالث بتحسين تطبيقك. أحدث إصدار من جافاسكربت متكامل مع Babel و Webpack. بيئة اختبار آلية تدير اختباراتك في المتصفح، مما يتيح لك الاختبار مثل المستخدِم العادي. عملية التحويل Transpilation والتصغير Minification لكل من شيفرات CSS وجافاسكربت الخاصة بعمليات البناء للإنتاج. اتباع العرف السائد في كتابة الشيفرة مما يقلل الاختلافات بين التطبيقات ويسمح بتبديل السياق بسهولة. الاستعداد لبناء مشروع Ember ستحتاج إلى محرِّر شيفرات قبل الاستمرار بمشروعك الجديد، فإذا لم يكن لديك محرّر مُعَدّ مسبقًا، فإنّ Ember Atlas لديه بعض الإرشادات حول كيفية إعداد المحرّرات المختلفة. تثبيت الأصول المشتركة لمشاريع TodoMVC لا يُعَدّ تثبيت الأصول أو ملفات المشروع المشتركة خطوةً مطلوبةً للمشاريع الجديدة، لكنه يسمح باستخدام ملفات CSS المشتركة الحالية حتى لا نخمّن ما هو ملف CSS المطلوب لإنشاء أنماط أو تنسيقات مشروع TodoMVC. أولًا، انتقل أولًا إلى المجلد todomvc في الطرفية باستخدام الأمر cd todomvc في نظامَي macOS أو لينكس Linux مثلًا. ثانيًا، شغّل الأمر التالي لوضع ملف CSS المشترك الخاص بمشروع todomvc ضمن تطبيقك: npm install --save-dev todomvc-app-css todomvc-common ثالثًا، ابحث بعد ذلك عن الملف ember-cli-build.js في المجلد todomvc الموجود في المجلد الجذر، وافتحه في محرر الشيفرة الذي اخترته، إذ يُعَدّ الملف ember-cli-build.js مسؤولًا عن إعداد التفاصيل حول كيفية بناء مشروعك بما في ذلك تجميع كل ملفاتك مع بعضها البعض وتصغير الأصول وإنشاء خرائط الشيفرة البرمجية، لذلك ليس هناك داع للقلق بشأن هذا الملف، كما سنضيف سطورًا إلى الملف ember-cli-build.js لاستيراد ملفات CSS المشتركة، بحيث تصبح جزءًا من عملية البناء دون الحاجة إلى استيرادها ‎@import صراحةً في الملف app.css، إذ سيتطلب ذلك إعادة كتابة عنوان URL في وقت البناء وبالتالي سيكون أقل كفاءة وأكثر تعقيدًا في الإعداد. رابعًا، ابحث عن الشيفرة التالية في الملف ember-cli-build.js: let app = new EmberApp(defaults, { // أضف خيارات هنا }); خامسًا، أضف الأسطر التالية بعد ذلك قبل حفظ الملف: app.import('node_modules/todomvc-common/base.css'); app.import('node_modules/todomvc-app-css/index.css'); سادسًا، أخيرًا، ابحث عن الملف app.css الموجود في المسار app/styles/app.css، والصق ما يلي فيه: :focus, .view label:focus, .todo-list li .toggle:focus + label, .toggle-all:focus + label { outline: #d86f95 solid !important; } يعدِّل ملف CSS بعض الأنماط التي توفرها حزمة npm والتي هي todomvc-app-css، مما يسمح بظهور تركيز لوحة المفاتيح ويؤدي إلى حد ما إلى إصلاح أحد عيوب الشمولية الرئيسية لتطبيق TodoMVC الافتراضي. بدء تشغيل خادم التطوير يمكنك بدء تشغيل التطبيق في وضع التطوير عن طريق كتابة الأمر التالي في الطرفية أثناء وجودك ضمن المجلد todomvc: ember server ويجب أن يظهر لديك خرج مشابه لما يلي: Build successful (190ms) – Serving on http://localhost:4200/ Slowest Nodes (totalTime >= 5%) | Total (avg) -----------------------------------------+----------- BroccoliMergeTrees (17) | 35ms (2 ms) Package /assets/vendor.js (1) | 13ms Concat: Vendor Styles/assets/vend... (1) | 12ms يُشغَّل خادم التطوير على المضيف المحلي http://localhost:4200، والذي يمكنك زيارته في متصفحك للتحقق من عملك حتى الآن. إذا كان كل شيء على ما يرام، فسترى صفحةً تشبه الصفحة التالية: الخلاصة وصلنا إلى النقطة التي يمكننا فيها البدء في بناء نموذجنا لتطبيق TodoMVC في إطار عمل Ember، وسنتعرّف في المقال التالي على بناء بنية شيفرة توصيف تطبيقنا بوصفها مجموعة من المكونات المنطقية. ترجمة -وبتصرّف- للمقال Getting started with Ember. اقرأ أيضًا مقارنة بين أطر الواجهات الأمامية: Angular و React و Vue
  17. سنزوّدك من خلال هذا المقال بقائمة من مصادر React التي يمكنك استخدامها للمضي قدمًا في مسار تعلّمك تطوير الواجهات الأمامية وبناء تطبيقات الويب. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية. الهدف: توفير مصادر إضافية لمعرفة المزيد عن مكتبة React. التنسيقات على مستوى المكون تُعرِّف العديد من تطبيقات React تنسيقاتها على أساس كل مكوِّن بدلًا من أن تكون في صفحة تنسيقات واحدة متجانسة، إذ تتيح الأداة create-react-app استيراد ملفات CSS إلى وحدات جافاسكربت، بحيث تُرسَل شيفرة CSS إلى المستخدِم فقط عند تصيير Render المكوِّن المقابل، وقد كان بإمكاننا في تطبيقنا مثلًا كتابة ملف Form.css مخصّص ليحتوي على تنسيقات تلك المكونات، ثم استيراد التنسيقات إلى وحداتها كما يلي: import Form from './Form'; import './Form.css' تسهِّل هذه الطريقة تحديد وإدارة شيفرة CSS المُخصَّصة لمكوِّن معيَّن، ولكنها تؤدي إلى تجزئة ملف التنسيقات عبر قاعدة شيفرتك البرمجية، كما يمكن ألّا تكون هذه التجزئة مفيدةً، إذ يُعَدّ الحدّ من مقدار الشيفرة التي ليست ذات فائدة والمُرسَلة إلى المستخدِم، أمرًا منطقيًا بالنسبة للتطبيقات الأكبر حجمًا، والتي تحتوي على مئات من العروض الفريدة والكثير من الأجزاء المتحركة، إذ يُحتمَل أن تكون لديك تنسيقات على مستوى التطبيق وتنسيقات مكونات محدَّدة مبنية فوقها. أدوات تطوير React استخدمنا التابع console.log()‎ للتحقق من حالة تطبيقنا وخاصياته Props، إذ سترى بعض التحذيرات المفيدة ورسائل الخطأ التي تعطيها React في واجهة سطر الأوامر CLI وطرفية جافاسكربت في المتصفح. ولكن هناك المزيد الذي نستطيع إجراؤه هنا. تتيح الأداة المساعدة React DevTools فحص الأجزاء الداخلية لتطبيق React مباشرةً في المتصفح، إذ تضيف لوحةً جديدةً إلى أدوات مطور متصفحك، كما يمكنك بواسطتها فحص حالة المكوّنات المختلفة وخاصياتها، وتعديل الحالة والخاصيات لإجراء تغييرات فورية على تطبيقك، إذ تُظهر لقطة الشاشة التالية تطبيقنا النهائي كما يظهر في الأداة React DevTools: نرى على اليسار جميع المكونات التي يتألف منها تطبيقنا بما في ذلك بعض المفاتيح الفريدة للأشياء المُصيَّرة من المصفوفات، في حين نرى على اليمين الخاصيات والخطّافات Hooks التي يستخدِمها المكوِّن App، ولاحظ أنّ للمكوّنات Form وFilterButton وTodo مسافةً بادئةً إلى اليمين، وهذا يشير إلى أنّ المكوِّن App هو المكوِّن الأب لها، لذا يُعَدّ هذا العرض رائعًا في التطبيقات الأكثر تعقيدًا لفهم العلاقات بين الأبناء والآباء بسهولة، كما تتوفر الأداة React DevTools في عدد من الأشكال مثل: امتداد متصفح كروم Chrome. امتداد متصفح فايرفوكس Firefox. امتداد متصفح Chromium Edge (سيُتاح قريبًا). تطبيق مستقل يمكنك تثبيته باستخدام NPM أو Yarn. جرّب تثبيت إحدى هذه الأدوات، ثم استخدمها لفحص التطبيق الذي أنشأته للتو. واجهة برمجة تطبيقات السياق Context API استخدَم التطبيق الذي أنشأناه خاصيات المكوِّنات لتمرير البيانات من المكوِّن App إلى المكوِّنات الأبناء التي تحتاجها، إذ تُعَدّ الخاصيات طريقةً مناسبةً لمشاركة البيانات في معظم الأحيان، ولكنها ليست الأفضل دائمًا بالنسبة للتطبيقات المعقَّدة والمتداخلة كثيرًا. توفِّر React واجهة برمجة تطبيقات السياق Context API بوصفها طريقةً لتوفير البيانات للمكوّنات التي تحتاجها دون تمرير الخاصيات إلى أسفل شجرة المكونات، كما يوجد الخطّاف useContext الذي يسهِّل ذلك. أصناف المكونات يمكن بناء مكونات React باستخدام أصناف ES6 التي تُسمَّى بأصناف المكونات Class Components، إذ كانت أصناف ES6 قبل ظهور الخطّافات الطريقة الوحيدة لجلب الحالة إلى المكوّنات أو إدارة آثار التصيير الجانبية، ولا تزال الطريقة الوحيدة للتعامل مع بعض الميزات الأخرى في الحالات الطارئة، وهي شائعة جدًا في مشاريع React القديمة، كما يمكنك الاطلاع على المصادر التالية: حالة ودورة حياة المكونات في توثيق React. توثيق React على موسوعة حسوب. تعلّم لغة جافاسكربت من خلال توثيقها على موسوعة حسوب. الاختبار توفِّر create-react-app بعض الأدوات لاختبار تطبيقك، كما يغطي توثيق create-react-app بعض أساسيات الاختبار. التوجيه يُعالَج التوجيه تقليديًا باستخدام خادم وليس باستخدام تطبيق على حاسوب المستخدِم، ولكن يمكن ضبط تطبيق ويب لقراءة موقع المتصفح وتحديثه، وتصيير واجهات مستخدِم معيّنة، وهذا ما يسمى بالتوجيه من طرف العميل Client-side Routing، كما يمكن إنشاء العديد من المسارات الفريدة لتطبيقك مثل ‎/home أو ‎/dashboard أو login/‎. أنتج مجتمع React مكتبتين رئيسيتين للتوجيه من طرف العميل هما React Router وReach Router. تُعَدّ مكتبة React Router مناسبةً للتطبيقات ذات احتياجات التوجيه المعقَّدة، كما أنها تلبي بعض الحالات الطارئة بطريقة أفضل من مكتبة Reach Router، ولكن تُعَدّ React Router مكتبةً أكبر. تُعَدّ مكتبة Reach Router مناسبةً للتطبيقات الأبسط، وتدير التركيز تلقائيًا أثناء تنقل المستخدِم من صفحة إلى أخرى. تُعَدّ إدارة التركيز أمرًا ضروريًا في التوجيه من طرف العميل، إذ يمكن وقوع مستخدِمي لوحة المفاتيح في مأزق التركيز، ويمكن ألّا يكون لدى مستخدِمي قارئ الشاشة أيّ فكرة عن انتقالهم إلى صفحة جديدة، في حين تُعَدّ مكتبة Reach Router مكانًا جيدًا للبدء، لأنها أفضل من حيث إمكانية الوصول، لكن سيُدمَج هذان المشروعان في المستقبل القريب، وستكون حينها مكتبة React Router هي المشروع الباقي مع إضافة ميزات إدارة التركيز الخاصة بمكتبة Reach. أخيرًا، لا تنسى الرجوع إلى توثيق React على موسوعة حسوب فضع هذا المرجع في جعبتك (بالإضافة إلى المراجع الأخرى التي توفرها الموسوعة) والذي ستحتاج إلى الرجوع إليه بين الحين والآخر في رحلة سيرك مع مكتبة React لبناء واجهات المواقع والتطبيقات. ترجمة -وبتصرُّف- للمقال React resources. اقرأ أيضًا مكونات React الأساسية (React Components) المصطلحات المستخدمة في React تعلم البرمجة
  18. سنركِّز في هذا المقال على الشمولية Accessibility (أو تترجم إلى سهولة وصول أيضًا) بما في ذلك إدارة التركيز Focus Management في React التي يمكنها تحسين قابلية الاستخدام وتقليل الارتباك لكل من مستخدمي لوحة المفاتيح فقط وقارئات الشاشة. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية. الهدف: تعلّم كيفية تنفيذ إمكانية وصول مستخدِمي لوحة المفاتيح في React. مستخدمو لوحة المفاتيح أنجزنا حتى الآن جميع الميزات التي أردنا تنفيذها، إذ أصبح بإمكان المستخدِم إضافة مهمة جديدة، وتحديد المهام، وإلغاء تحديدها، وحذف المهام، وتعديل أسماء المهام، وترشيح قائمة المهام جميعها أو المهام النشطة أو المهام المكتملة، إذ يمكن للمستخدِمين تنفيذ جميع هذه المهام باستخدام الفأرة، ولكن لا يمكن لمستخدِمي لوحة المفاتيح فقط الوصول إلى هذه الميزات بسهولة. استكشاف مشكلة قابلية استخدام لوحة المفاتيح انقر على حقل الإدخال الموجود أعلى التطبيق كما لو أنك تريد إضافة مهمة جديدة، إذ سترى خطًا سميكًا يمثل حدود العنصر outline حول حقل الإدخال، إذ تُعَدّ هذه الحدود المؤشر المرئي على تركيز المتصفح على هذا العنصر حاليًا، لذا اضغط على مفتاح Tab من لوحة المفاتيح، وسترى ظهور المخطط حول زر الإضافة Add تحت حقل الإدخال، إذ يدل ذلك على انتقال تركيز المتصفح. اضغط على مفتاح Tab عدة مرات، وسترى مؤشر التركيز المتقطع ينتقل بين أزرار الترشيح، واستمر في الضغط على مفتاح Tab إلى أن يصبح مؤشر التركيز حول زر التعديل Edit الأول، ثم اضغط على مفتاح Enter، إذ سيبدّل المكوِّن <Todo /‎> بين القوالب كما صمّمنا، وسترى نموذجًا يتيح تعديل اسم المهمة. ولكن قد تتساءل عن مكان وجود مؤشر التركيز حاليًا، إذ تُزال تمامًا العناصر التي كانت موجودةً سابقًا لاستبدال شيء آخر بها عندما نبدِّل بين القوالب في المكوِّن <Todo /‎>، وهذا يعني اختفاء العنصر الذي ركّزنا عليه سابقًا، ولا يوجد شيء نركّز عليه حاليًا، فقد يؤدي ذلك إلى إرباك مجموعة كبيرة من المستخدِمين، وخاصةً المستخدِمين الذين يعتمدون على لوحة المفاتيح أو الذين يستخدِمون قارئ الشاشة، كما يمكن تحسين تجربة مستخدِمي لوحة المفاتيح وقارئ الشاشة من خلال إدارة تركيز المتصفح بأنفسنا. التركيز بين القوالب إذا بدَّل المستخدِم قالب <Todo/‎> من قالب العرض إلى قالب التعديل، فيجب علينا التركيز على العنصر <input> المستخدَم لإعادة تسميته، ويجب علينا إعادة التركيز مرةً أخرى إلى زر التعديل Edit عند التبديل مرةً أخرى من قالب التعديل إلى قالب العرض. استهداف العناصر يجب علينا إخبار React بالعنصر الذي نريد التركيز عليه في نموذج DOM وكيفية العثور عليه، ويساعدنا في ذلك الخطّاف useRef في React الذي ينشئ كائنًا له الخاصية current، إذ يمكن أن تكون هذه الخاصية مرجعًا لأيّ شيء نريده، ثم يمكننا البحث عن هذا المرجع لاحقًا، وهذا مفيد للإشارة إلى عناصر DOM، لذا عدِّل تعليمة الاستيراد import في الجزء العلوي من الملف Todo.js لتتضمن الخطّاف useRef كما يلي: import React, { useRef, useState } from "react"; أنشئ بعد ذلك ثابتَين جديدين بعد الخطّافات في الدالة Todo()‎، إذ يجب أن يكون أحد هذين الثابتين مرجعًا لزر التعديل Edit في قالب العرض والآخر لحقل التعديل في قالب التعديل. const editFieldRef = useRef(null); const editButtonRef = useRef(null); تملك هذه المراجع قيمةً افتراضيةً هي null لأنها ستكون بلا قيمة حين ربطها بالعناصر الخاصة بها من خلال إضافة الخاصية ref لكل عنصر، وضبط قيمها على كائنات ref المسماة بأسماء مناسبة، ويجب تعديل مربع النص <input> في قالب التعديل كما يلي: <input id={props.id} className="todo-text" type="text" value={newName} onChange={handleChange} ref={editFieldRef} /> يجب أن يكون زر التعديل Edit في قالب العرض كما يلي: <button type="button" className="btn" onClick={() => setEditing(true)} ref={editButtonRef} > Edit <span className="visually-hidden">{props.name}</span> </button> التركيز على عناصر ref باستخدام الخطاف useEffect يمكننا استخدام عناصر ref للغرض المقصود منها عن طريق استيراد خطّاف React آخر هو useEffect()‎ الذي سُمِّي بهذا الاسم لأنه يعمل بعد أن تصيِّر React مكونًا معينًا، وسيشغّل هذا الخطّاف أيّ أمور إضافية نريد إضافتها إلى عملية التصيير، والتي لا يمكننا تشغيلها ضمن جسم الدالة الرئيسية، إذ يُعَدّ الخطّاف useEffect()‎ مفيدًا في الوضع الحالي لأننا لا نستطيع التركيز على عنصر إلا بعد تصيير المكوِّن <Todo /‎> ومعرفة React مكان عناصر ref، لذا عدِّل تعليمة الاستيراد import في الملف Todo.js مرةً أخرى لإضافة الخطّاف useEffect كما يلي: import React, { useEffect, useRef, useState } from "react"; يأخذ الخطّاف useEffect()‎ دالةً على أساس وسيط له، إذ تُنفَّذ هذه الدالة بعد تصيير المكوِّن، لذا ضع استدعاء الخطّاف useEffect()‎ التالي قبل تعليمة return مباشرةً في جسم الدالة Todo()‎، ومرّر إليه دالةً تسجّل العبارة "side effect" في الطرفية: useEffect(() => { console.log("side effect"); }); أضف تابع console.log()‎ آخر، لتوضيح الفرق بين عملية التصيير الرئيسية وتشغيل الشيفرة ضمن الخطّاف useEffect()‎، أي ضع التعليمة التالية بعد الإضافة السابقة: console.log("main render"); افتح الآن التطبيق في متصفحك، إذ يجب أن ترى كلتا الرسالتين في الطرفية مع تكرار كل منهما ثلاث مرات، ولاحظ ظهور عبارة التصيير الرئيسي main render أولًا، ثم عبارة "side effect" ثانيًا، على الرغم من وجود عبارة "side effect" أولًا في الشيفرة. main render (3) Todo.js:100 side effect (3) Todo.js:98 احذف تعليمة console.log("main render")‎ الآن، ولننتقل إلى تطبيق إدارة التركيز. التركيز على حقل التعديل تذكّر أننا نريد التركيز على حقل التعديل عندما ننتقل إلى قالب التعديل، لذا عدِّل الخطّاف useEffect()‎ ليصبح كما يلي: useEffect(() => { if (isEditing) { editFieldRef.current.focus(); } }, [isEditing]); إذا كانت قيمة isEditing هي true، فستقرأ React قيمة مرجع editFieldRef الحالية وتنقل تركيز المتصفح إليه، كما سنمرِّر مصفوفةً إلى الخطّاف useEffect()‎ على أساس وسيط ثان، إذ تُعَدّ هذه المصفوفة قائمةً من القيم التي يجب أن يعتمد عليها الخطّاف useEffect()‎، فإذا ضمّنا هذه القيم، فلن يُشغَّل الخطاف useEffect()‎ إلا عند تغيير إحدى هذه القيم، إذ نريد تغيير التركيز عندما تتغير قيمة isEditing فقط، لذا انتقل إلى متصفحك الآن، وسترى انتقال التركيز إلى العنصر <input> المقابل لزر التعديل Edit عند النقر عليه. نقل التركيز إلى زر التعديل مرة أخرى يمكن أن يبدو نقل React للتركيز مرةً أخرى إلى زر التعديل Edit عند حفظ التعديل أو إلغائه أمرًا سهلًا للوهلة الأولى، فلنجرّب إضافة شرط إلى الخطّاف useEffect للتركيز على زر التعديل إذا كانت قيمة الحالة isEditing هي false مثلًا، لذا عدِّل استدعاء الخطّاف useEffect()‎ كما يلي: useEffect(() => { if (isEditing) { editFieldRef.current.focus(); } else { editButtonRef.current.focus(); } }, [isEditing]); عُد إلى متصفحك وسترى انتقال تركيزك بين تعديل العنصر <input> وزر التعديل Edit مع بدء التعديل وإنهائه، ولكن يركِّز الزر Edit في المكوِّن <Todo /‎> الأخير مباشرةً على تحميل الصفحة قبل تفاعلنا مع التطبيق، وهنا يتصرّف الخطّاف useEffect()‎ كما صممناه تمامًا، فهو يعمل بمجرد تصيير المكوِّن، ويرى أن قيمة الحالة isEditing هي false، كما يركِّز على الزر Edit، وبما أنه يوجد ثلاث نسخ من المكوِّن <Todo /‎>، فإننا نرى التركيز على الزر Edit الأخير، إذ يجب إعادة التفكير ليتغير التركيز عندما تتغير قيمة الحالة isEditing فقط. إدارة التركيز الصارمة يجب معرفة قيمة الحالة isEditing ومتى تتغير هذه القيمة، لذا يجب أن نكون قادرين على قراءة قيمة isEditing السابقة، ويجب أن يكون منطق العمل كما يلي باستخدام الشيفرة الوهمية التالية: if (wasNotEditingBefore && isEditingNow) { focusOnEditField() } if (wasEditingBefore && isNotEditingNow) { focusOnEditButton() } ناقش فريق React طرقًا للحصول على حالة المكوِّن السابقة، وقدّم مثالًا عن الخطّاف المُخصَّص الذي يمكننا استخدامه لذلك. الصق الشيفرة التالية بالقرب من أعلى الملف Todo.js قبل الدالة Todo()‎: function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; } سنعرِّف الآن الثابت wasEditing بعد الخطّافات في الجزء العلوي من الدالة Todo()‎، إذ نريد أن يتتبع هذا الثابت قيمة isEditing السابقة، لذلك سنستدعي الدالة usePrevious مع isEditing على أساس وسيط كما يلي: const wasEditing = usePrevious(isEditing); يمكننا باستخدام هذا الثابت تحديث الخطّاف useEffect()‎ لتطبيق الشفرة الوهمية التي ناقشناها سابقًاـ لذا عدِّل هذه الشيفرة لتصبح كما يلي: useEffect(() => { if (!wasEditing && isEditing) { editFieldRef.current.focus(); } if (wasEditing && !isEditing) { editButtonRef.current.focus(); } }, [wasEditing, isEditing]); لاحظ اعتماد منطق useEffect()‎ الآن على الثابت wasEditing، لذلك سنقدِّمه ضمن مصفوفة الاعتماديات، وحاول مرةً أخرى استخدام زرَي التعديل Edit والإلغاء Cancel للتبديل بين قوالب المكوِّن <Todo /‎>، إذ سترى مؤشر تركيز المتصفح يتحرك بطريقة مناسبة دون ظهور المشكلة التي ناقشناها سابقًا. التركيز عندما يحذف المستخدم مهمة هناك مشكلة واحدة أخيرة في تجربة لوحة المفاتيح، وهي اختفاء التركيز عندما يحذف المستخدِم مهمةً من القائمة، لذا سنتبع نمطًا مشابهًا للتعديلات السابقة، إذ سننشئ مرجعًا جديدًا، وسنستخدِم الخطّاف usePrevious()‎، لنتمكّن من التركيز على عنوان القائمة عندما يحذف المستخدِم مهمةً. يكون المكان الذي نرغب في إرسال تركيزنا إليه واضحًا في بعض الأحيان، إذ كانت لدينا نقطة أصل للرجوع إليها وهي زر التعديل Edit عند التبديل بين قوالب <Todo /‎>، لكن لا يوجد مكان للرجوع إليه في حالتنا، لأننا نزيل العناصر تمامًا من نموذج DOM، فعنوان القائمة هو أفضل خيار لأنه قريب من عنصر القائمة الذي سيحذفه المستخدِم، كما سيخبر التركيز عليه المستخدِم بعدد المهام المتبقية. إنشاء المرجع استورد الخطّافَين useRef()‎ وuseEffect()‎ إلى الملف App.js كما يلي: import React, { useState, useRef, useEffect } from "react"; صرّح بعد ذلك عن مرجع جديد ضمن الدالة App()‎ قبل تعليمة return مباشرةً كما يلي: const listHeadingRef = useRef(null); تحضير العنوان لا تكون عناصر العنوان مثل <h2> قابلةً للتركيز عادةً، ولكن لا يُعَدّ ذلك مشكلة، إذ يمكننا جعل أيّ عنصر قابلًا للتركيز برمجيًا عن طريق إضافة السمة tabindex="-1"‎ إليه، أي يمكن التركيز عليه باستخدام لغة جافاسكربت فقط، كما لا يمكنك الضغط على مفتاح Tab للتركيز على عنصر له السمة tabindex="-1"‎ باستخدام الطريقة نفسها التي يمكنك اتباعها مع عناصر <button> أو <a>، إذ يمكن تطبيق ذلك باستخدام السمة tabindex="0"‎، ولكنه لا يُعَدّ مناسبًا في هذه الحالة. لنضِف السمة tabindex المكتوبة بالصورة tabIndex في صيغة JSX إلى العنوان الموجود أعلى قائمة المهام مع المرجع headingRef كما يلي: <h2 id="list-heading" tabIndex="-1" ref={listHeadingRef}> {headingText} </h2> ملاحظة: تُعَدّ السمة tabindex رائعةً لحالات إمكانية الوصول الطارئة، ولكن يجب عليك الحرص على عدم الإفراط في استخدامها، إذ يمكنك تطبيقها على عنصر فقط عندما تكون متأكدًا تمامًا من أنّ جعله قابلًا للتركيز سيفيد المستخدِم بطريقة ما، كما يجب عليك استخدام العناصر التي يمكن التركيز عليها طبيعيًا مثل الأزرار وعناصر الروابط وحقول الإدخال، في حين يمكن أن يكون لاستخدام السمة tabindex غير المسؤول تأثيرًا سلبيًا عميقًا على مستخدِمي لوحة المفاتيح وقارئات الشاشة. الحصول على الحالة السابقة نريد التركيز على العنصر المرتبط بالمرجع باستخدام السمة ref عندما يحذف المستخدِم مهمةً من القائمة فقط، إذ سيتطلب ذلك الخطّاف usePrevious()‎ الذي استخدمناه سابقًا، لذا أضف هذا الخطّاف في أعلى الملف App.js بعد عبارات الاستيراد مباشرةً كما يلي: function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; } أضف الآن ما يلي قبل تعليمة return ضمن الدالة App()‎: const prevTaskLength = usePrevious(tasks.length); استدعينا الخطّاف usePrevious()‎ لتتبع طول حالة المهام. ملاحظة: بما أننا نستخدِم الآن الخطّاف usePrevious()‎ في ملفين، فستتمثّل إعادة البناء الفعالة في نقل الدالة usePrevious()‎ إلى ملفها، وتصديرها من هذا الملف، واستيرادها حيث تريدها. استخدام الخطاف useEffect()‎ للتحكم في التركيز الرئيسي يمكننا الآن إعداد الخطّاف useEffect()‎ لتشغيله عندما يتغير عدد المهام، والذي سيركِّز على العنوان إذا كان عدد المهام أقل مما كان عليه سابقًا، أي أننا حذفنا مهمةً. أضف ما يلي إلى جسم الدالة App()‎ بعد الإضافات السابقة مباشرةً: useEffect(() => { if (tasks.length - prevTaskLength === -1) { listHeadingRef.current.focus(); } }, [tasks.length, prevTaskLength]); نحاول التركيز فقط على عنوان القائمة إذا كانت لدينا مهام أقل مما كانت عليه سابقًا، إذ تضمن الاعتماديات Dependencies االمُمرَّرة في هذا الخطّاف محاولة إعادة التشغيل فقط عندما تتغير أيّ من هذه القيم، أي عدد المهام الحالية أو عدد المهام السابقة، وسترى الآن ظهور مخطط التركيز المنقط حول العنوان أعلى القائمة عندما تحذف مهمةً في متصفحك. الخلاصة انتهينا من إنشاء تطبيق React من الألف إلى الياء، وستكون المهارات التي تعلمتها أساسًا للبناء عليها لتواصل العمل مع React، كما يمكنك أن تكون مساهمًا فعالًا في مشروع React حتى إذا كان كل ما تفعله هو التفكير مليًا في المكونات وحالتها وخاصياتها، وتذكّر أن تكتب دائمًا أفضل شيفرة HTML ممكنة. يُعَدّ الخطّافان useRef()‎ وuseEffect()‎ ميزات متقدمةً إلى حد ما، ويجب أن تفخر بنفسك لاستخدامها، وابحث عن فرص لممارستها أكثر، مما سيسمح لك بإنشاء تجارب شاملة للمستخدِمين، إذ يمكن تعذُّر الوصول إلى تطبيقنا لمستخدِمي لوحة المفاتيح بدونهما. ملاحظة: إذا كنت بحاجة إلى التحقق من شيفرتك مقابل نسختنا من الشيفرة، فيمكنك العثور على نسخة نهائية من نموذج شيفرة تطبيق React في المستودع todo-reaction. كما يمكنك الحصول على إصدار مباشر قيد التشغيل من خلال مراجعة todo-react-build. سنقدِّم في المقال التالي قائمةً بموارد React التي يمكنك استخدامها للمضي قدمًا في مسار تعلّمك. ترجمة -وبتصرُّف- للمقال Accessibility in React. اقرأ أيضًا تنفيذ التفاعل في تطبيق React: التعديل والترشيح والتصيير الشرطي إنشاء تطبيق قائمة مهام باستخدام React تقسيم تطبيق React إلى مكونات أساسيات بناء تطبيقات الويب
  19. سنضيف في هذا المقال اللمسات الأخيرة على وظائف تطبيق قائمة المهام Todo List الرئيسية -الذي بنيناه في المقالات السابقة- من خلال السماح بتعديل المهام الحالية، وترشيح Filtering قائمة المهام جميعها والمهام المكتملة وغير المكتملة فقط، كما سنتعرّف على التصيير الشرطي Conditional Rendering لواجهة المستخدِم UI. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية. الهدف: التعرف على التصيير الشرطي في React وتطبيق ترشيح القائمة وتعديل واجهة مستخدِم في تطبيقنا. تعديل اسم المهمة لا توجد لدينا واجهة مستخدِم لتعديل اسم المهمة حتى الآن، ولكن يمكننا على الأقل تنفيذ الدالة editTask()‎ في الملف App.js حاليًا، والتي تشبه الدالة deleteTask()‎ لأنها ستأخذ معرِّفًا id للعثور على الكائن الهدف، وستأخذ الخاصية newName التي تحتوي على الاسم الذي سنعدّل اسم المهمة إليه، في حين أننا سنستخدِم التابع Array.prototype.map()‎ بدلًا من التابع Array.prototype.filter()‎ لأننا نريد إعادة مصفوفة جديدة مع بعض التعديلات بدلًا من حذف شيء منها، لذا أضف الدالة editTask()‎ ضمن المكوِّن App مع الدوال الأخرى كما يلي: function editTask(id, newName) { const editedTaskList = tasks.map(task => { // إذا كانت هذه المهمة لها معرّف المهمة المعدّلة نفسه if (id === task.id) { // return {...task, name: newName} } return task; }); setTasks(editedTaskList); } مرِّر editTask إلى مكونات <Todo /‎> بوصفها خاصيةً Prop بالطريقة نفسها التي مررنا بها الخاصية deleteTask كما يلي: const taskList = tasks.map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} toggleTaskCompleted={toggleTaskCompleted} deleteTask={deleteTask} editTask={editTask} /> )); افتح الآن الملف Todo.js، إذ سنُجري إعادة بناء. واجهة مستخدم التعديل يجب توفير واجهة مستخدِم للمستخدِمين للسماح لهم بتعديل مهمة، لذا استورد أولًا الخطّاف useState إلى المكوِّن Todo كما فعلنا سابقًا مع المكوِّن App عن طريق تعديل تعليمة الاستيراد الأولى إلى ما يلي: import React, { useState } from "react"; سنضبط الآن الحالة isEditing التي يجب أن تكون قيمتها الافتراضية false، لذا أضف السطر التالي في الجزء العلوي من تعريف المكون Todo(props) { … }‎: const [isEditing, setEditing] = useState(false); كما يجب إعادة التفكير في المكوِّن <Todo /‎> من الآن فصاعدًا، إذ نريد منه عرض أحد القالبَين التاليين بدلًا من القالب الوحيد المستخدَم حتى الآن: قالب العرض View عند عرض المهام فقط، وهو ما استخدمناه حتى الآن. قالب التعديل Editing عند تعديل المهام، وسننشئه بعد قليل. انسخ الشيفرة التالية في الدالة Todo()‎ بعد الخطّاف useState()‎ وقبل التعليمة return: const editingTemplate = ( <form className="stack-small"> <div className="form-group"> <label className="todo-label" htmlFor={props.id}> New name for {props.name} </label> <input id={props.id} className="todo-text" type="text" /> </div> <div className="btn-group"> <button type="button" className="btn todo-cancel"> Cancel <span className="visually-hidden">renaming {props.name}</span> </button> <button type="submit" className="btn btn__primary todo-edit"> Save <span className="visually-hidden">new name for {props.name}</span> </button> </div> </form> ); const viewTemplate = ( <div className="stack-small"> <div className="c-cb"> <input id={props.id} type="checkbox" defaultChecked={props.completed} onChange={() => props.toggleTaskCompleted(props.id)} /> <label className="todo-label" htmlFor={props.id}> {props.name} </label> </div> <div className="btn-group"> <button type="button" className="btn"> Edit <span className="visually-hidden">{props.name}</span> </button> <button type="button" className="btn btn__danger" onClick={() => props.deleteTask(props.id)} > Delete <span className="visually-hidden">{props.name}</span> </button> </div> </div> ); أصبح لدينا الآن بنيتا قوالب مختلفتين -"Edit" و"View"- معرّفتان ضمن ثابتين منفصلين، وهذا يعني أنّ التعليمة return الخاصة بالمكوِّن <Todo /‎> مكررة، فهي تحتوي على تعريف قالب العرض View أيضًا، ويمكن تنظيف هذا التكرار باستخدام التصيير الشرطي Conditional Rendering لتحديد القالب الذي يعيده المكوِّن، وبالتالي يُصيَّر في واجهة المستخدِم. التصيير الشرطي يمكننا في صيغة JSX استخدام شرط لتغيير ما يصيّره المتصفح، إذ يُكتَب الشرط في صيغة JSX باستخدام معامِل ثلاثي Ternary Operator، فالشرط في حالة المكوِّن <Todo /‎> هو "هل تُعدَّل هذه المهمة؟"، لذا عدِّل التعليمة return ضمن الدالة Todo()‎، بحيث تصبح كما يلي: return <li className="todo">{isEditing ? editingTemplate : viewTemplate}</li>; يجب أن يصيّر متصفحك جميع مهامك كما كانت سابقًا، ويمكن مشاهدة قالب التعديل من خلال تغيير القيمة الافتراضية للحالة isEditing من false إلى true في شيفرتك حاليًا، إذ سنجعل زر التعديل Edit يبدِّل هذه القيمة لاحقًا. التبديل بين قوالب سنجعل الآن ميزة التعديل تفاعليةً، إذ يجب أولًا استدعاء الدالة setEditing()‎ مع القيمة true عندما يضغط المستخدِم على زر التعديل Edit في قالب العرض viewTemplate لنتمكّن من التبديل بين القوالب، لذا عدِّل زر التعديل Edit في قالب العرض viewTemplate كما يلي: <button type="button" className="btn" onClick={() => setEditing(true)}> Edit <span className="visually-hidden">{props.name}</span> </button> سنضيف الآن السمة onClick نفسها إلى زر الإلغاء Cancel في قالب التعديل editingTemplate، ولكن سنضبط الحالة isEditing هذه المرة على القيمة false لتعيدنا إلى قالب العرض، لذا عدِّل زر الإلغاء Cancel في قالب التعديل editingTemplate كما يلي: <button type="button" className="btn todo-cancel" onClick={() => setEditing(false)} > Cancel <span className="visually-hidden">renaming {props.name}</span> </button> يجب أن تكون قادرًا الآن على الضغط على زرَي التعديل Edit والإلغاء Cancel في عناصر المهام للتبديل بين القالبين. الخطوة التالية هي جعل وظيفة التعديل تعمل فعليًا. التعديل من واجهة المستخدم ما سنعمل عليه تاليًا مماثل لما عملنا عليه مع المكون Form (انظر المقال السابق تقسيم تطبيق React إلى مكونات). إذا كتب المستخدِم شيئًا في حقل الإدخال الجديد، فيجب تتبّع النص الذي يدخله، ويجب استخدام خاصية رد النداء Callback Prop بمجرد إرسال النموذج لتحديث الحالة باسم المهمة الجديد، إذ سنبدأ بإنشاء خطّاف Hook جديد لتخزين وضبط الاسم الجديد، لذا ضع ما يلي أسفل الخطّاف الموجود مسبقًا في الملف Todo.js: const [newName, setNewName] = useState(''); أنشئ بعد ذلك الدالة handleChange()‎ التي تضبط الاسم الجديد، لذا ضع ما يلي بعد الخطافات وقبل القوالب: function handleChange(e) { setNewName(e.target.value); } سنعدِّل الآن الحقل <input /‎> الخاص بقالب التعديل editingTemplate بضبط السمة value على newName وربط الدالة handleChange()‎ مع الحدث onChange كما يلي: <input id={props.id} className="todo-text" type="text" value={newName} onChange={handleChange} /> يجب أخيرًا إنشاء دالة تعالِج الحدث onSubmit الخاصة بنموذج التعديل، لذا أضف ما يلي مباشرةً بعد الدالة السابقة التي أضفتها: function handleSubmit(e) { e.preventDefault(); props.editTask(props.id, newName); setNewName(""); setEditing(false); } تذكّر أنّ خاصية رد النداء editTask()‎ تحتاج إلى معرِّف المهمة التي نعدّلها بالإضافة إلى اسمها الجديد. اربط الدالة handleSubmit()‎ مع حدث إرسال submit النموذج عبر إضافة معالج الحدث onSubmit التالي إلى عنصر النموذج <form> الخاص بقالب التعديل editingTemplate: <form className="stack-small" onSubmit={handleSubmit}> يجب الآن أن تكون قادرًا على تعديل مهمة في متصفحك. العودة إلى أزرار الترشيح اكتملت الآن ميزاتنا الرئيسية، وبالتالي يمكننا التفكير في أزرار الترشيح التي تكرّر العنوان "All" حاليًا بدون وظائف تطبّقها، إذ سنعيد تطبيق بعض المهارات التي استخدمناها في المكوِّن <Todo /‎> من أجل ما يلي: إنشاء خطّاف لتخزين المرشِّح Filter النشط. تصيير مصفوفة من عناصر <FilterButton /‎> التي تسمح للمستخدِمِين بتغيير المرشِّح النشط بين جميع المهام والمهام المكتملة والمهام غير المكتملة. إضافة خطاف ترشيح أضف خطّافًا جديدًا إلى الدالة App()‎ التي تقرأ وتضبط المرشِّح، إذ نريد أن يكون المرشّح الافتراضي هو All لأنه يجب عرض جميع المهام في البداية. const [filter, setFilter] = useState('All'); تعريف المرشحات هدفنا الآن هو: يجب أن يكون لكل مرشِّح اسم فريد. يجب أن يكون لكل مرشِّح سلوك فريد. يُعَدّ كائن JavaScript طريقةً رائعةً لربط الأسماء بالسلوكيات، فالمفتاح هو اسم المرشِّح، والخاصية هي السلوك المرتبط بهذا الاسم. أضِف كائنًا بالاسم FILTER_MAP في الجزء العلوي من الملف App.js بعد تعليمات الاستيراد وقبل الدالة App()‎ كما يلي: const FILTER_MAP = { All: () => true, Active: task => !task.completed, Completed: task => task.completed }; تُعَدّ قيم الكائن FILTER_MAP دوالًا سنستخدِمها لترشيح مصفوفة بيانات المهام tasks كما يلي: يعرِض المرشِّح All جميع المهام، لذلك يعيد القيمة true لجميع المهام. يعرِض المرشِّح Active المهام التي يكون فيها للخاصية completed القيمة false. يعرِض المرشِّح Completed المهام التي يكون فيها للخاصية completed القيمة true. أضف ما يلي بعد الذي أضفناه منذ قليل، إذ سنستخدِم التابع Object.keys()‎ لتجميع مصفوفة FILTER_NAMES: const FILTER_NAMES = Object.keys(FILTER_MAP); ملاحظة: نعرِّف هذه الثوابت خارج الدالة App()‎ لأنها إذا عُرِّفت ضمنها، فسيُعاد حسابها في كل مرة يعاد فيها تصيير المكوِّن <App /‎>، ولا نريد حصول ذلك، فلن تتغير هذه المعلومات أبدًا بغض النظر عمّا يفعله تطبيقنا. تصيير المرشحات أصبح لدينا الآن مصفوفة FILTER_NAMES، وبالتالي يمكننا استخدامها لتصيير المرشِّحات الثلاثة، إذ يمكننا إنشاء ثابت يسمى filterList ضمن الدالة App()‎، إذ سنستخدِم هذا الثابت لربط مصفوفة الأسماء وإعادة المكوِّن <FilterButton /‎>، وتذكّر أننا هنا بحاجة إلى مفاتيح أيضًا، لذا أضف ما يلي بعد التصريح عن الثابت taskList: const filterList = FILTER_NAMES.map(name => ( <FilterButton key={name} name={name}/> )); سنستبدل الآن الثابت filterList بالمكونات <FilterButton /‎> الثلاثة المكرَّرة في الملف App.js، أي بدلًا مما يلي: <FilterButton /> <FilterButton /> <FilterButton /> ضع التالي: {filterList} لن يعمل ذلك الآن، إذ لدينا المزيد لعمله أولًا. المرشحات التفاعلية يمكنك جعل أزرار المرشِّحات تفاعليةً من خلال تحديد الخاصيات التي تحتاج إلى استخدامها. نعلم أن المكوِّن <FilterButton /‎> يجب أن يبلّغ عمّا إذا كان مضغوطًا حاليًا، ويجب الضغط عليه إذا تطابق اسمه مع القيمة الحالية لحالة المرشِّح. نعلم أن المكوِّن <FilterButton /‎> يحتاج إلى دالة رد نداء لضبط المرشِّح النشط، كما يمكننا الاستفادة مباشرةً من الخطّاف setFilter. عدِّل الثابت filterList كما يلي: const filterList = FILTER_NAMES.map(name => ( <FilterButton key={name} name={name} isPressed={name === filter} setFilter={setFilter} /> )); كما يجب الآن تعديل الملف FilterButton.js لاستخدام الخاصيات التي قدمناها له بالطريقة نفسها التي طبّقناها سابقًا مع المكوِّن <Todo /‎>، لذا طبّق ما يلي وتذكَّر استخدام الأقواس المعقوصة لقراءة هذه المتغيرات: ضع all مكان الخاصية {props.name}. اضبط قيمة السمة aria-pressed على الخاصية {props.isPressed}. أضف السمة onClick التي تستدعي الخطّاف props.setFilter()‎ مع اسم المرشِّح. يجب أن تكون الآن الدالة FilterButton()‎ كما يلي: function FilterButton(props) { return ( <button type="button" className="btn toggle-btn" aria-pressed={props.isPressed} onClick={() => props.setFilter(props.name)} > <span className="visually-hidden">Show </span> <span>{props.name}</span> <span className="visually-hidden"> tasks</span> </button> ); } انتقل إلى متصفحك مرةً أخرى، إذ يجب أن ترى تسمية الأزرار المختلفة بأسمائها الخاصة، فإذا ضغطتَ على زر الترشيح Filter، فيجب أن ترى نَص الزر يأخذ تخطيطًا جديدًا، إذ يخبرك هذا بأنه مُحدَّد، فإذا ألقيت نظرةً على فاحص صفحة أدوات التطوير DevTool’s Page Inspector أثناء النقر على الأزرار، فسترى أنّ قيم السمة aria-pressed تتغير وفقًا لذلك. لكن لا تزال الأزرار لا ترشِّح المهام في واجهة المستخدِم. ترشيح المهام في واجهة المستخدم يربط الثابت taskList في الدالة App()‎ حاليًا حالة المهام ويعيد مكوِّن <Todo /‎> جديدًا لكل منها، ولكننا لا نريد ذلك، إذ يجب تصيير المهمة فقط إذا كانت مُضمَّنةً في نتائج تطبيق المرشِّح المحدَّد، وبالتالي يجب ترشيح حالة المهام باستخدام التابع Array.prototype.filter()‎ قبل ربطها لإزالة الكائنات التي لا نريد تصييرها، لذا عدِّل الثابت taskList كما يلي: const taskList = tasks .filter(FILTER_MAP[filter]) .map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} toggleTaskCompleted={toggleTaskCompleted} deleteTask={deleteTask} editTask={editTask} /> )); يمكن الوصول إلى قيمة في المصفوفة FILTER_MAP التي تتوافق مع مفتاح حالة المرشِّح لتحديد دالة رد النداء التي يجب استخدامها في التابع Array.prototype.filter()‎، فإذا كان المرشِّح هو All مثلًا، فسيُقيَّم العنصر FILTER_MAP[filter]‎ على ‎() => true، كما يؤدي اختيار المرشِّح في متصفحك الآن إلى إزالة المهام التي لا تفي بمعاييره، في حين سيتغير العدد الموجود في العنوان أعلى القائمة ليمثِّل القائمة. الخلاصة اكتمل تطبيقنا الآن، ولكن يمكننا إجراء بعض التحسينات لضمان إمكانية استخدام مجموعة أكبر من المستخدِمين له، كما يتناول المقال التالي تضمين إدارة التركيز Focus Management في React التي يمكنها تحسين قابلية الاستخدام وتقليل الارتباك لكل من مستخدِمي لوحة المفاتيح فقط وقارئات الشاشة. ترجمة -وبتصرُّف- للمقال React interactivity: Editing, filtering, conditional rendering. اقرأ أيضًا تنفيذ التفاعل في تطبيق React: الأحداث والحالة إنشاء تطبيق قائمة مهام باستخدام React تقسيم تطبيق React إلى مكونات أساسيات بناء تطبيقات الويب
  20. حان الوقت الآن لتعديل تطبيقنا من واجهة مستخدِم ثابتة تمامًا إلى واجهة مستخدِم تسمح لنا بالتفاعل معها وتعديلها بعد وضع خطة للمكوّنات من خلال البحث عن الأحداث Events والحالة State، إذ ينتج في النهاية تطبيق يمكننا من خلاله إضافة المهام وحذفها بنجاح، ووضع علامة على المهام المكتملة. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية. الهدف: التعرف على كيفية التعامل مع الأحداث والحالة في React، واستخدامها لإنشاء دراسة حالة تطبيق تفاعلي. معالجة الأحداث إذا استخدَمت لغة جافاسكربت الصرفة Vanilla JavaScript سابقًا، فلا بدّ أنك معتاد على وجود ملف جافاسكربت منفصل، إذ يمكنك الاستعلام عن بعض عُقد DOM وربط المستمعين بها كما يلي: const btn = document.querySelector('button'); btn.addEventListener('click', () => { alert("hi!"); }); يمكننا في React كتابة معالِجات الأحداث مباشرةً للعناصر الموجودة في JSX كما يلي: <button type="button" onClick={() => alert("hi!")} > Say hi! </button> يمكن أن يُعَدّ ذلك أمرًا غير مناسب لنصائح أفضل الممارسات التي تميل إلى عدم استخدام معالِجات الأحداث المُضمَّنة في HTML، ولكن تذكر أنّ صيغة JSX هي جزء من لغة جافاسكربت. أضفنا في المثال السابق السمة onClick إلى عنصر الزر <button>، وقيمة هذه السمة هي دالة تشغّل تنبيهًا بسيطًا، إذ تملك السمة onClick معنًى خاصًا هنا، فهي تخبر React بتشغيل دالة معيّنة عندما ينقر المستخدِم على الزر، كما يجب ملاحظة بعض الأشياء الأخرى، وهي: تُعَدّ طبيعة اسم السمة onClick ذات حالة الجَمل Camel-cased مهمةً، إذ لن تتعرف صيغة JSX على السمة onclick، لأن هذه الكلمة محجوزة في لغة جافاسكربت، وتُستخدَم لغرض مختلف يمثل خاصيات معالِج الحدث onclick المعيارية. تتبع جميع أحداث المتصفح هذا التنسيق في صيغة JSX باستخدام الجزء on متبوعًا باسم الحدث. لنطبّق هذه الملاحظات على تطبيقنا بدءًا من المكوِّن Form.js. معالجة إرسال النموذج أنشئ دالةً بالاسم handleSubmit()‎ في الجزء العلوي من دالة المكوِّن Form()‎، إذ يجب على هذه الدالة منع سلوك الحدث submit الافتراضي، ثم يجب إطلاق تنبيه alert()‎ بالذي تريده كما يلي: function handleSubmit(e) { e.preventDefault(); alert('Hello, world!'); } يمكنك استخدام هذه الدالة من خلال إضافة السمة onSubmit إلى العنصر <form>، وضبط قيمتها على الدالة handleSubmit كما يلي: <form onSubmit={handleSubmit}> إذا عدت إلى متصفحك ونقرت على زر "الإضافة Add"، فسيعرض المتصفح مربع حوار تنبيه يحتوي على الرسالة "Hello, world!‎" أو أيّ شيء آخر اخترته. خاصيات رد النداء يندر اقتصار التفاعل على مكوِّن واحد فقط في تطبيقات React، إذ ستؤثِّر الأحداث التي تحدث في أحد المكوِّنات على أجزاء أخرى من التطبيق، فإذا كان لدينا القدرة على إنشاء مهام جديدة مثلًا، فستؤثِّر الأشياء التي تحدث في المكوِّن <Form /‎> على القائمة المُصيَّرة في المكوِّن <App /‎>. نريد أن تساعدنا الدالة handleSubmit()‎ في إنشاء مهمة جديدة، لذلك سنحتاج إلى طريقة لتمرير المعلومات من المكوِّن <Form /‎> إلى المكوِّن <App /‎>، فلا يمكننا تمرير البيانات من الابن إلى الأب بالطريقة نفسها التي نمرر بها البيانات من الأب إلى الابن باستخدام الخاصيات Props المعيارية، إذ يمكننا بدلًا من ذلك كتابة دالة في المكوِّن <App /‎> تتوقع بعض البيانات من نموذجنا بوصفها دخلًا لها، ثم تمرير هذه الدالة إلى المكوِّن <Form /‎> بوصفها خاصيةً، وتُسمَّى هذه الدالة التي تُعامَل على أنها خاصية بخاصية رد النداء Callback Prop، إذ يمكننا استدعاء خاصية رد النداء ضمن المكوِّن <Form /‎> لإرسال البيانات الصحيحة إلى المكوِّن <App /‎>. معالجة إرسال النموذج باستخدام خاصيات رد النداء أنشئ دالةً بالاسم addTask()‎ في الجزء العلوي من دالة المكوِّن App()‎، بحيث تحتوي هذه الدالة على معامِل واحد هو name: function addTask(name) { alert(name); } سنمرِّر بعد ذلك الدالة addTask()‎ إلى المكوِّن <Form /‎> بوصفها خاصيةً، إذ يمكن أن تحمل الخاصية أيّ اسم تريده، ولكن اختر اسمًا تفهمه لاحقًا مثل الاسم addTask الذي يتطابق مع اسم الدالة ومع ما ستفعله، وهنا يجب تعديل استدعاء المكوِّن <Form /‎> كما يلي: <Form addTask={addTask} /> أخيرًا، يمكنك استخدام هذه الخاصية ضمن الدالة handleSubmit()‎ في المكوِّن <Form /‎> كما يلي: function handleSubmit(e) { e.preventDefault(); props.addTask("Say hello!"); } سيؤدي النقر على زر "الإضافة Add" في متصفحك إلى إثبات عمل دالة رد النداء addTask()‎، لكن سيكون جيدًا أن نحصل على تنبيه لإظهار ما نكتبه في حقل الإدخال، وهذا ما سنفعله لاحقًا. ملاحظة: سمّينا خاصية رد النداء بالاسم addTask لتسهيل فهم ما ستفعله هذه الخاصية، ومن الاصطلاحات الشائعة الأخرى التي قد تصادفها في شيفرة React هي أن تُسبَق أسماء خاصيات رد النداء بالكلمة on متبوعةً باسم الحدث الذي سيؤدي إلى تشغيلها، إذ يمكننا مثلًا تسمية خاصية بالاسم onSubmit مع القيمة addTask. الحالة والخطاف useState استخدَمنا حتى الآن الخاصيات لتمرير البيانات عبر المكوّنات، ولكننا نحتاج إلى شيء آخر عند تعاملنا مع دخل المستخدِم وتحديثات البيانات. تأتي الخاصيات من أب المكوِّن، فلن يرث المكوِّن <Form /‎> مثلًا اسمًا جديدًا للمهمة، إذ يتواجد العنصر <input /‎> مباشرةً ضمن المكوِّن <Form /‎>، لذا سيكون هذا المكوِّن مسؤولًا مباشرةً عن إنشاء هذا الاسم الجديد، كما لا يمكننا الطلب من المكوِّن <Form /‎> إنشاء خاصياته تلقائيًا، ولكن يمكننا أن نطلب منه تتبع بعض بياناته، إذ تسمى هذه البيانات التي يمتلكها المكوِّن نفسه بالحالة State، والتي تُعَدّ أداةً قويةً أخرى من React، لأن المكوّنات لا تمتلك الحالة فحسب، وإنما يمكنها تحديثها لاحقًا، في حين لا يمكن تحديث الخاصيات التي يتلقاها المكوِّن، فهي للقراءة فقط. توفِّر React مجموعةً متنوعةً من الدوال الخاصة التي تسمح لنا بتوفير إمكانات جديدة للمكوّنات مثل الحالة، إذ تُسمَّى هذه الدوال بالخطّافات Hooks، والخطّاف useState -كما يوحي اسمه- هو بالضبط الذي نحتاجه لإعطاء مكوِّننا حالةً، وهنا يجب استيراد خطّاف React من الوحدة react لاستخدامه، لذا عدِّل السطر الأول في الملف Form.js ليصبح كما يلي: import React, { useState } from "react"; يسمح لنا ذلك باستيراد الدالة useState()‎، واستخدامها في أيّ مكان من هذا الملف، كما تنشئ هذه الدالة حالةً لمكوِّن، ويحدِّد معامِلها الوحيد القيمة الأولية لتلك الحالة، كما تُعيد هذه الدالة الحالةَ ودالةً يمكن استخدامها لتحديث الحالة لاحقًا، ولنجرب ذلك أولًا من خلال إنشاء الحالة name ودالةً لتحديث هذه الحالة، لذا اكتب السطر التالي قبل الدالة handleSubmit()‎ ضمن الدالة Form()‎: const [name, setName] = useState('Use hooks!'); يحدُث ما يلي في السطر السابق: ضبط قيمة الحالة name الأولية على القيمة "Use hooks!‎". تعريف دالة بالاسم setName()‎ وظيفتها تعديل الحالة name. تعيد الدالة useState()‎ الشيئين السابقين، لذا فإننا نستخدِم عملية هدم المصفوفات Array Destructuring لإسنادهما إلى متغيرَين منفصلين. حالة القراءة يمكنك رؤية الحالة name قيد التشغيل مباشرةً، لذا أضِف السمة value إلى العنصر input في النموذج، واضبط قيمتها لتكون name، إذ سيصيِّر متصفحك التعليمة "Use hooks!‎" في العنصر input. <input type="text" id="new-todo-input" className="input input__lg" name="text" autoComplete="off" value={name} /> عدِّل بعد ذلك التعليمة "Use hooks!‎" إلى سلسلة نصية فارغة، فهذا ما نريده لحالتنا الأولية كما يلي: const [name, setName] = useState(''); قراءة دخل المستخدم يجب التقاط دخل المستخدِم الذي يكتبه قبل تمكننا من تغيير قيمة الحالة nameمن خلال الاستماع إلى الحدث onChange، فلنكتب الدالة handleChange()‎، ونستمع إليها على الوسم <input /‎>. // قُرب أعلى المكوِّن ‫`Form` function handleChange(e) { console.log("Typing!"); } // بعد تعليمة‫ `return` <input type="text" id="new-todo-input" className="input input__lg" name="text" autoComplete="off" value={name} onChange={handleChange} /> لن تتغير قيمة الدخل أثناء الكتابة حاليًا، ولكن سيطبع متصفحك الكلمة "Typing!" على طرفية جافاسكربت (نافذة console)، وبالتالي سنعلم أنّ مستمع الحدث متصل بالدخل، كما يمكنك تغيير قيمة الدخل من خلال استخدام الدالة handleChange()‎ لتحديث الحالة name. يمكن قراءة محتويات حقل الإدخال عند تغييرها من خلال الوصول إلى الخاصية value الخاصة بالعنصر input عن طريق قراءة القيمة e.target.value ضمن الدالة handleChange()‎، إذ يمثِّل e.target العنصر الذي أَطلق الحدث change، وهو العنصر input، وبالتالي تكون الخاصية value هي النص الموجود ضمنها، كما يمكنك تنفيذ التابع console.log()‎ على هذه القيمة لرؤيتها في طرفية متصفحك كما يلي: function handleChange(e) { console.log(e.target.value); } حالة التحديث يجب تخزين حالة name المحدَّثة مع تغير قيمة الدخل، لذا غيِّر التابع console.log()‎ إلى setName()‎ كما يلي: function handleChange(e) { setName(e.target.value); } يجب الآن تعديل الدالة handleSubmit()‎ لتستدعي الخاصية props.addTask مع الحالة name على أساس وسيط، مما يؤدي إلى إرسال المهمة مرةً أخرى إلى المكوِّن App، لنتمكن من إضافتها إلى قائمة المهام لاحقًا، كما يجب مسح الدخل بعد إرسال النموذج، لذلك سنستدعي الدالة setName()‎ مرةً أخرى مع سلسلة نصية فارغة. function handleSubmit(e) { e.preventDefault(); props.addTask(name); setName(""); } أخيرًا، يمكنك كتابة شيء ما في حقل الإدخال في متصفحك والنقر فوق زر "Add"، وبالتالي سيظهر كل ما كتبته في مربع حوار التنبيه، والآن يجب أن يكون الملف Form.js كما يلي: import React, { useState } from "react"; function Form(props) { const [name, setName] = useState(""); function handleChange(e) { setName(e.target.value); } function handleSubmit(e) { e.preventDefault(); props.addTask(name); setName(""); } return ( <form onSubmit={handleSubmit}> <h2 className="label-wrapper"> <label htmlFor="new-todo-input" className="label__lg"> What needs to be done? </label> </h2> <input type="text" id="new-todo-input" className="input input__lg" name="text" autoComplete="off" value={name} onChange={handleChange} /> <button type="submit" className="btn btn__primary btn__lg"> Add </button> </form> ); } export default Form; ملاحظة: هناك شيء واحد ستلاحظه، وهو أنه يمكنك إرسال مهام فارغة عند الضغط على زر الإضافة Add دون إدخال اسم المهمة، لذلك يجب منع إضافة المهام الفارغة من خلال إضافة تحقق ما إلى الدالة handleSubmit()‎ على سبيل المثال. إضافة مهمة أصبحنا الآن جاهزين لكتابة دوال تسمح للمستخدِم بإضافة مهمة جديدة من متصفحه بعد أن تدربنا على الأحداث وخاصيات رد النداء والخطّافات. استخدام المهام بوصفها حالات استورِد الخطّاف useState إلى الملف App.js لتتمكن من تخزين المهام في حالة ما من خلال تعديل سطر استيراد React إلى ما يلي: import React, { useState } from "react"; نريد تمرير الخاصية props.tasks إلى الخطّاف useState()‎، إذ ستحتفظ هذه الخاصية بحالة الخطّاف الأولية، لذا أضف السطر التالي في الجزء العلوي من تعريف الدالة App()‎: const [tasks, setTasks] = useState(props.tasks); يمكننا الآن تغيير ربط قائمة المهام taskList لتكون نتيجةً لربط tasks بدلًا من props.tasks، إذ يجب أن يبدو تصريح الثابت taskList كما يلي: const taskList = tasks.map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} /> ) ); إضافة مهمة لدينا الآن الخطّاف setTasks الذي يمكننا استخدامه في الدالة addTask()‎ لتحديث قائمة المهام، ولكن هناك مشكلة أنه لا يمكننا فقط تمرير الوسيط name الخاص بالدالة addTask()‎ إلى الخطاف setTasks، لأنّ tasks هي مصفوفة من الكائنات؛ أما الوسيط name، فهو سلسلة نصية، وبالتالي ستكون السلسلة النصية مكان المصفوفة. يجب أولًا وضع الوسيط name في كائن له بنية مهامنا الحالية نفسها، إذ سننشئ ضمن الدالة addTask()‎ الكائن newTask لإضافته إلى المصفوفة، ويجب بعد ذلك إنشاء مصفوفة جديدة مع إضافة هذه المهمة الجديدة إليها، ثم تحديث حالة بيانات المهام إلى هذه الحالة الجديدة من خلال استخدام صيغة الانتشار Spread Syntax لنسخ المصفوفة الحالية، وإضافة الكائن في النهاية، ثم تمرير هذه المصفوفة إلى الدالة setTasks()‎ لتحديث الحالة، وبالتالي يجب أن تصبح الدالة addTask()‎ كما يلي: function addTask(name) { const newTask = { id: "id", name: name, completed: false }; setTasks([...tasks, newTask]); } يمكنك الآن استخدام المتصفح لإضافة مهمة إلى بياناتنا، لذا اكتب أيّ شيء تريده في النموذج، وانقر زر الإضافة Add أو اضغط على مفتاح Enter من لوحة المفاتيح، إذ سترى عنصر المهام الجديد يظهَر في واجهة المستخدِم، لكن هناك مشكلة أخرى تتمثّل بإعطاء الدالة addTask()‎ المعرّف id نفسه لكل مهمَّة، إذ يُعَدّ ذلك أمرًا سيئًا لإمكانية الوصول، كما يجعل التمييز بين المهام المستقبلية أمرًا مستحيلًا على React باستخدام الخاصية key، إذ ستعطيك React تحذيرًا في طرفية أدوات التطوير DevTools مثل رسالة التحذير التالية: "Warning: Encountered two children with the same key…‎". يجب إصلاح هذه المشكلة، كما يُعَدّ إنشاء معرّفات فريدة مشكلةً صعبةً، وهي مشكلة كتَب لها مجتمع جافاسكربت بعض المكتبات المفيدة من أجلها، إذ سنستخدِم حاليًا المكتبة nanoid لأنها صغيرة الحجم وتعمل جيدًا، لذا تأكّد من أنك في المجلد الجذر لتطبيقك وشغّل الأمر التالي في طرفيتك: npm install nanoid وفي ملاحظة مهمة، إذا أردتَ استخدام مدير الحزم yarn، فيجب كتابة الأمر: yarn add nanoid. يمكننا الآن استيراد المكتبة nanoid في الجزء العلوي من الملف App.js لنتمكّن من استخدامها لإنشاء معرِّفات فريدة لمهامنا الجديدة، لذا ضمّن أولًا سطر الاستيراد التالي في أعلى الملف App.js: import { nanoid } from "nanoid"; لنحدّث الآن الدالة addTask()‎ بحيث يصبح كل معرِّف مهمة مؤلفًا من البادئة todo-‎ بالإضافة إلى سلسلة نصية فريدة تنشئها المكتبة nanoid، لذا عدِّل تصريح الثابت newTask إلى ما يلي: const newTask = { id: "todo-" + nanoid(), name: name, completed: false }; احفظ كل شيء، وجرب تطبيقك مرةً أخرى، إذ يمكنك الآن إضافة المهام دون تلقِّي هذا التحذير بشأن المعرِّفات المُكرَّرة. عد المهام يمكننا الآن إضافة مهام جديدة، ولكن هناك مشكلة تتمثَّل بقراءة العنوان ثلاث مهام متبقية، بغض النظر عن عدد المهام، إذ يمكن إصلاح ذلك عن طريق حساب طول قائمة المهام taskList وتغيير نص العنوان وفقًا لذلك، لذا أضف ما يلي ضمن تعريف الدالة App()‎ قبل تعليمة return: const headingText = `${taskList.length} tasks remaining`; هذا صحيح تقريبًا باستثناء أنه إذا احتوت قائمتنا على مهمة واحدة، فسيظل العنوان يستخدِم الكلمة "tasks"، ولذلك يجب أن نجعلها متغيرةً، لذا عدِّل الشيفرة التي أضفتها للتو كما يلي: const tasksNoun = taskList.length !== 1 ? 'tasks' : 'task'; const headingText = `${taskList.length} ${tasksNoun} remaining`; يمكنك الآن استبدال المتغير headingText بمحتوى نص عنوان القائمة، لذا عدِّل العنصر <h2> ليصبح كما يلي: <h2 id="list-heading">{headingText}</h2> إكمال مهمة لاحظ أنّ مربع الاختيار يُحدَّد ويُلغَى تحديده بطريقة مناسبة عند النقر عليه، كما تُعَدّ معرفة المتصفح لكيفية تذكّر مدخلات مربعات الاختيار المُحدَّدة أو المُلغَى تحديدها دون مساعدتنا ميزةً في لغة HTML، ولكن تخفي هذه الميزة مشكلةً، إذ لا يغيِّر تحديد مربع الاختيار أو عدم تحديده الحالة في تطبيق React، مما يعني عدم تزامن المتصفح مع التطبيق، لذلك يجب كتابة شيفرتنا لإعادة المتصفح متزامنًا مع التطبيق. إثبات الخطأ لنكتب الدالة toggleTaskCompleted()‎ في المكوِّن App()‎، إذ سيكون لهذه الدالة المعامِل id، لكننا لن نستخدِمها حاليًا، إذ سنسجِّل الآن المهمة الأولى في المصفوفة في الطرفية، وسنفحص ما يحدث عندما نحدِّدها أو نلغي تحديدها في متصفحنا، لذا أضف ما يلي قبل التصريح عن الثابت taskList مباشرةً: function toggleTaskCompleted(id) { console.log(tasks[0]) } سنضيف بعد ذلك الخاصية toggleTaskCompleted إلى خاصيات كل مكوّن من مكوّنات <Todo /‎> المُصيَّرة ضمن taskList كما يلي: const taskList = tasks.map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} toggleTaskCompleted={toggleTaskCompleted} /> )); انتقل إلى المكوِّن Todo.js وأضف معالِج الحدث onChange إلى العنصر <input /‎> الذي يجب أن يستخدِم دالةً مجهولةً لاستدعاء الخاصية props.toggleTaskCompleted()‎ مع المعامِل props.id، إذ يجب أن يكون العنصر <input /‎> الآن كما يلي: <input id={props.id} type="checkbox" defaultChecked={props.completed} onChange={() => props.toggleTaskCompleted(props.id)} /> احفظ كل شيء وعُد إلى متصفحك ولاحظ تحديد المهمة الأولى Eat، ثم افتح طرفية جافاسكربت، ثم انقر على مربع الاختيار الموجود بجوار الخيار Eat، وبالتالي فإنّ مربع الاختيار هذا غير محدَّد كما توقّعنا، ولكن ستعطي طرفية جافاسكربت الخاصة بك شيئًا كما يلي: Object { id: "task-0", name: "Eat", completed: true } يُلغَى تحديد مربع الاختيار في المتصفح، لكن تخبرنا الطرفية بأن المهمَّة Eat لا تزال مكتملةً، وسنصلح ذلك لاحقًا. مزامنة المتصفح مع بياناتنا لنَعُد إلى الدالة toggleTaskCompleted()‎ في الملف App.js، إذ نريدها أن تغيِّر الخاصية completed للمهمَّة التي أُلغِي تحديدها فقط، وترك المهام الأخرى كما هي، لذلك سنطبّق التابع map()‎ على قائمة المهام وسنغيّر القائمة التي أكملناها فقط، لذا عدِّل الدالة toggleTaskCompleted()‎ إلى ما يلي: function toggleTaskCompleted(id) { const updatedTasks = tasks.map(task => { // إذا كان لهذه المهمة معرِّف المهمة المُعدَّلة نفسه if (id === task.id) { // استخدم انتشار الكائن لإنشاء كائن جديد // ‫عُدِّلت الخاصية `completed` الخاصة به return {...task, completed: !task.completed} } return task; }); setTasks(updatedTasks); } عرّفنا الثابت updatedTasks الذي يمر على عناصر المصفوفة tasks الأصلية، فإذا طابقت خاصية معرّفُ id المهمة المعرّفَ id المقدَّم للدالة، فسنستخدِم صيغة انتشار الكائن Object Spread Syntax لإنشاء كائن جديد، وسنبدّل إلى الخاصية checked لهذا الكائن قبل إعادته؛ أما إذا لم يتطابقا، فسنعيد الكائن الأصلي. نستدعي بعد ذلك الدالة setTasks()‎ مع هذه المصفوفة الجديدة لتحديث الحالة. حذف مهمة سيتَّبع حذف مهمة نمطًا مشابهًا لتبديل حالتها المكتملة، إذ يجب تعريف دالة لتحديث الحالة، ثم تمرير هذه الدالة إلى المكوِّن <Todo /‎> بوصفها خاصيةً، واستدعاؤها عند حدوث الحدث الصحيح. خاصية رد النداء deleteTask سنكتب الدالة deleteTask()‎ في المكوِّن App، إذ ستأخذ هذه الدالة المعامِل id مثل الدالة toggleTaskCompleted()‎، وسنسجّل هذا المعرِّف في الطرفية، لذا أضف ما يلي بعد الدالة toggleTaskCompleted()‎: function deleteTask(id) { console.log(id) } أضف بعد ذلك خاصية رد نداء أخرى إلى مصفوفة مكوّنات <Todo /‎> كما يلي: const taskList = tasks.map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} toggleTaskCompleted={toggleTaskCompleted} deleteTask={deleteTask} /> )); يجب استدعاء الدالة props.deleteTask()‎ في الملف Todo.js عند الضغط على زر الحذف Delete، كما تحتاج الدالة deleteTask()‎ إلى معرفة معرِّف المهمَّة التي ستستدعيها لتتمكّن من حذف المهمَّة الصحيحة من الحالة، لذا عدِّل زر الحذف Delete ضمن الملف Todo.js كما يلي: <button type="button" className="btn btn__danger" onClick={() => props.deleteTask(props.id)} > Delete <span className="visually-hidden">{props.name}</span> </button> إذا نقرتَ الآن على أيّ من أزرار الحذف Delete في التطبيق، فيجب أن تسجِّل طرفية المتصفح معرّف المهمَّة المرتبطة به. حذف المهام من الحالة وواجهة المستخدم يمكننا الآن استدعاء الخطّاف setTasks()‎ في الدالة deleteTask()‎ استدعاءً صحيحًا لحذف هذه المهمة فعليًا من حالة التطبيق وحذفها مرئيًا من واجهة مستخدِم التطبيق، وبما أنّ الخطّاف setTasks()‎ يتوقع مصفوفةً بوصفها وسيطًا له، فيجب تزويده بمصفوفة جديدة تنسخ المهام الحالية باستثناء المهمة التي يتطابق معرِّفها مع معرِّف المهمَّة المُمرَّرة إلى الدالة deleteTask()‎. يمكننا الآن استخدام التابع Array.prototype.filter()‎، إذ يمكننا اختبار كل مهمة، واستبعاد مهمة من المصفوفة الجديدة إذا تطابقت خاصيتها id مع المعامِل id المُمرَّر إلى الدالة deleteTask()‎، لذا عدِّل الدالة deleteTask()‎ ضمن الملف App.js كما يلي: function deleteTask(id) { const remainingTasks = tasks.filter(task => id !== task.id); setTasks(remainingTasks); } جرِّب تطبيقك مرةً أخرى، إذ يجب أن تكون قادرًا على حذف مهمَّة من تطبيقك. الخلاصة قدّمنا في هذا المقال معلومات حول كيفية تعامل React مع الأحداث والحالة، وتنفيذ وظائف إضافة المهام وحذفها ووضع علامة على المهام المكتملة، كما سنطبّق في المقال التالي وظيفة تعديل المهام الحالية وترشيح قائمة المهام جميعها والمهام المكتملة وغير المكتملة فقط، كما سنطّلع على التصيير الشرطي لواجهة المستخدِم UI. ترجمة -وبتصرُّف- للمقال React interactivity: Events and state. اقرأ أيضًا إنشاء تطبيق قائمة مهام باستخدام React تقسيم تطبيق React إلى مكونات أساسيات بناء تطبيقات الويب
  21. يُعَدّ تطبيقنا الذي عملنا عليه في المقال السابق وحدةً متراصةً، لذلك يجب تقسيمه إلى مكوّنات يمكن وصفها وإدارتها قبل تمكننا من جعل تطبيقنا يفعل شيئًا ما، إذ لا تحتوي مكتبة React على قواعد صارمة لتحديد ما يُعَدّ مكوّنًا Component، فالأمر متروك لك، وسنعرض في هذا المقال طريقةً معقولةً لتقسيم تطبيقنا إلى مكونات. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية. الهدف: إظهار طريقة لتقسيم تطبيق قائمة المهام إلى مكوّنات. تحديد المكون الأول قد يبدو تحديد أحد المكوّنات أمرًا صعبًا إلى حين حصولك على بعض الخبرة العملية، ولكن الأمور المهمة هي: إذا كان أحد الأشياء جزءًا واضحًا من تطبيقك، فيُحتمَل أن يكون مكوّنًا. إذا أُعيد استخدام أحد الأشياء كثيرًا، فيُحتمَل أن يكون مكوّنًا. تُعَدّ النقطة الثانية ذات قيمة خاصة، إذ يتيح إنشاء مكوّن من عناصر واجهة المستخدِم تعديلَ شيفرتك البرمجية في مكان واحد ورؤية تلك التعديلات في جميع الأماكن التي يُستخدَم فيها هذا المكوّن، إذ ليس عليك تقسيم كل شيء إلى مكوِنات مباشرةً، ولنستخدِم النقطة الثانية لإنشاء مكوّن من أكثر الأجزاء المُعاد استخدامها والأكثر أهميةً في واجهة المستخدِم وهو عنصر قائمة المهام. إنشاء المكون <Todo /‎> يجب علينا إنشاء ملف جديد للمكوّن قبل إنشائه، ويجب إنشاء مجلد لهذه المكونات، إذ تنشئ الأوامر التالية المجلد components وملفًا ضمنه يُسمَّى Todo.js، ولكن تأكّد من وجودك في جذر تطبيقك قبل تشغيل هذه الأوامر: mkdir src/components touch src/components/Todo.js إنّ ملف Todo.js الجديد فارغ حاليًا، لذا افتحه واكتب فيه السطر الأول التالي: import React from "react"; سننشئ مكوّنًا يسمّى Todo، لذلك يمكننا إضافة شيفرتنا إلى الملف Todo.js على النحو التالي، إذ سنعرِّف الدالة Todo()‎ ونصدّرها على السطر نفسه كما يلي: export default function Todo() { return ( ); } كل شيء جيد حتى الآن، لكن يجب أن يعيد المكون شيئًا ما، لذا ارجع إلى الملف src/App.js، وانسخ أول عنصر <li> من القائمة غير المرتبة، والصقه في الملف Todo.js بحيث يصبح كما يلي: export default function Todo() { return ( <li className="todo stack-small"> <div className="c-cb"> <input id="todo-0" type="checkbox" defaultChecked={true} /> <label className="todo-label" htmlFor="todo-0"> Eat </label> </div> <div className="btn-group"> <button type="button" className="btn"> Edit <span className="visually-hidden">Eat</span> </button> <button type="button" className="btn btn__danger"> Delete <span className="visually-hidden">Eat</span> </button> </div> </li> ); } ملاحظة: يجب أن تعيد المكونات شيئًا ما دائمًا، فإذا حاولت لاحقًا تصيير Render مكون لا يعيد شيئًا، فستعرِض React خطأً في متصفحك. أصبح المكوّن Todo مكتملًا حاليًا، وبالتالي يمكننا استخدامه، والآن أضف السطر التالي في الملف App.js بالقرب من أعلى الملف لاستيراد المكوّن Todo: import Todo from "./components/Todo"; يمكنك مع استيراد هذا المكون وضع استدعاءات المكوّن <Todo /‎> مكان جميع عناصر <li> في الملف App.js، ويجب أن يصبح العنصر <ul> كما يلي: <ul role="list" className="todo-list stack-large stack-exception" aria-labelledby="list-heading" > <Todo /> <Todo /> <Todo /> </ul> إذا نظرت إلى متصفحك، فستلاحظ شيئًا مؤسفًا، إذ تُكرِّر قائمتك المهمة الأولى ثلاث مرات كما يلي: لا نريد الأكل فقط، إذ لدينا مهام أخرى يجب تنفيذها، وسنوضّح فيما يلي كيفية إجراء استدعاءات لمكوّنات مختلفة تصيّر محتوًى فريدًا. إنشاء مكون <Todo /‎> فريد تُعَدّ المكونات مهمةً لأنها تتيح إعادة استخدام أجزاء من واجهة المستخدِم، والإشارة إلى مكان ما ليكون مصدرًا لواجهة المستخدِم تلك، ولكن تكمن المشكلة في أننا لا نريد إعادة استخدام جميع المكونات، وإنما نريد إعادة استخدام معظم الأجزاء، وتعديل أجزاء صغيرة، لذا يجب استخدام الخاصيات Props. الخاصية name إذا أردنا تتبّع أسماء المهام التي نريد إكمالها، فيجب علينا التأكد من أنّ كل مكوّن <Todo /‎> يصيِّر اسمًا فريدًا، لذا امنح كل مكوّن <Todo /‎> في الملف App.js خاصية الاسم name، ولنستخدم أسماء المهام التي كانت لدينا سابقًا كما يلي: <Todo name="Eat" /> <Todo name="Sleep" /> <Todo name="Repeat" /> إذا حدّثتَ متصفحك، فسترى الشيء السابق نفسه بالضبط، إذ أعطينا المكوّن <Todo /‎> بعض الخاصيات، لكننا لم نستخدِمها بعد، فلنَعُد الآن إلى الملف Todo.js ونصلح كل شيء. عدّل أولًا تعريف الدالة Todo()‎ بحيث تأخذ الخاصيات props على أساس معامِل، كما يمكنك تطبيق التابع console.log()‎ على الخاصيات props كما فعلنا سابقًا إذا أردت التحقق من استلام المكوّن للخاصيات استلامًا صحيحًا، ثم يمكنك وضع خاصية الاسم name مكان تكرارات المهمة Eat، وتذكّر استخدام الأقواس المعقوصة لحقن قيمة متغير في تعابير JSX، إذ يجب أن تكون الدالة Todo()‎ بعد ذلك كما يلي: export default function Todo(props) { return ( <li className="todo stack-small"> <div className="c-cb"> <input id="todo-0" type="checkbox" defaultChecked={true} /> <label className="todo-label" htmlFor="todo-0"> {props.name} </label> </div> <div className="btn-group"> <button type="button" className="btn"> Edit <span className="visually-hidden">{props.name}</span> </button> <button type="button" className="btn btn__danger"> Delete <span className="visually-hidden">{props.name}</span> </button> </div> </li> ); } يجب أن يعرض متصفحك الآن ثلاث مهام فريدة، ولكن لا تزال جميع هذه المهام مُحدَّدةً افتراضيًا. الخاصية completed حُدِّدت المهمة Eat فقط في القائمة الثابتة الأصلية، إذ نريد إعادة استخدام معظم واجهة المستخدِم التي تشكل المكوِّن <Todo /‎‎‎> مع تعديل شيء واحد من خلال منح كل استدعاء للمكوّن <Todo /‎‎‎> في الملف App.js الخاصية completed الجديدة، كما يجب أن يكون للخاصية completed التابعة للمكوّن الأول الذي اسمه Eat القيمة true، وللمكونات الأخرى القيمة false كما يلي: <Todo name="Eat" completed={true} /> <Todo name="Sleep" completed={false} /> <Todo name="Repeat" completed={false} /> يجب علينا العودة إلى الملف Todo.js لاستخدام هذه الخاصيات، لذا عدّل السمة defaultChecked للعنصر <input /‎> بحيث تساوي قيمتها الخاصية completed، ثم يكون عنصر <input /‎> الخاص بالمكون Todo على النحو التالي: <input id="todo-0" type="checkbox" defaultChecked={props.completed} /> حدّث متصفحك لإظهار تحديد المكوِّن Eat فقط كما يلي: إذا عدّلت كل خاصيات completed الخاصة بالمكوّن <Todo /‎>، فسيحدِّد متصفحك أو يلغي تحديد مربعات الاختيار المكافئة والمُصيَّرة وفقًا لذلك. الخاصية id يعطي المكوّن <Todo /‎> لكل مهمة السمة id بالقيمة todo-0، وهذا خطأ في HTML لأن سمات id يجب أن تكون فريدةً، إذ تستخدِمها لغات جافاسكربت وCSS وغيرها بوصفها معرّفات فريدةً لأجزاء المستند، وبالتالي يجب أن نعطي المكون الخاصية id التي تأخذ قيمةً فريدةً لكل مكوّن Todo. إذًا لنمنح كل نسخة من المكوِّن <Todo /‎> معرّفًا باستخدام التنسيق todo-i، إذ تزيد قيمة i بمقدار واحد في كل مرة كما يلي: <Todo name="Eat" completed={true} id="todo-0" /> <Todo name="Sleep" completed={false} id="todo-1" /> <Todo name="Repeat" completed={false} id="todo-2" /> عُد الآن إلى الملف Todo.js واستفد من الخاصية id، إذ يجب تعديل قيمة السمة id للعنصر <input /‎> وقيمة السمة htmlFor الخاصة بالعنصر label كما يلي: <div className="c-cb"> <input id={props.id} type="checkbox" defaultChecked={props.completed} /> <label className="todo-label" htmlFor={props.id}> {props.name} </label> </div> كل شيء جيد حتى الآن، ولكن تُعَدّ شيفرتنا مكرَّرةً، فالأسطر الثلاثة التي تصيّر المكوّن <Todo /‎> متطابقة تقريبًا مع اختلاف واحد فقط هو قيمة كل خاصية، كما يمكننا تنظيف شيفرتنا باستخدام إحدى ميزات جافاسكربت الأساسية وهي التكرار Iteration، ولكن يجب أولًا إعادة التفكير في المهام لاستخدام هذه الميزة. بيانات المهام تحتوي كل مهمة من مهامنا حاليًا على ثلاثة أجزاء من المعلومات، وهي اسمها، وما إذا كانت مُحدَّدة، ومعرّفها الفريد، كما تُترجَم هذه البيانات إلى كائن Object، وبما أنه لدينا أكثر من مهمة، فسنستخدِم مصفوفةً من الكائنات لتمثيل هذه البيانات، لذا أنشئ ثابتًا const جديدًا بعد تعليمة الاستيراد الأخيرة في الملف src/index.js وقبل التابع ReactDOM.render()‎ كما يلي: const DATA = [ { id: "todo-0", name: "Eat", completed: true }, { id: "todo-1", name: "Sleep", completed: false }, { id: "todo-2", name: "Repeat", completed: false } ]; سنمرِّر بعد ذلك الثابت DATA إلى المكوّن <App /‎> بوصفه خاصيةً تُسمَّى tasks، إذ يجب أن يكون السطر الأخير من الملف src/index.js كما يلي: ReactDOM.render(<App tasks={DATA} />, document.getElementById("root")); أصبحت هذه المصفوفة متاحةً الآن للمكون App بالصورة props.tasks، كما يمكنك استخدام التابع console.log()‎ للتحقق من ذلك. التصيير مع التكرار يمكننا تصيير مصفوفة الكائنات من خلال تحويل كل منها إلى المكون <Todo /‎>، إذ تمنحنا لغة جافاسكربت تابع مصفوفة لتحويل البيانات إلى شيء آخر، وهو Array.prototype.map()‎، لذا أنشئ ثابتًا const جديدًا يسمى taskList قبل تعليمة return الخاصة بالدالة App()‎، واستخدِم التابع map()‎ لتحويله، ولنحوّل مجموعة مهامنا إلى شيء بسيط يتمثّل باسم name كل مهمة كما يلي: const taskList = props.tasks?.map(task => task.name); لنحاول وضع الثابت taskList مكان جميع أبناء العنصر <ul> كما يلي: <ul role="list" className="todo-list stack-large stack-exception" aria-labelledby="list-heading" > {taskList} </ul> يمنحنا ذلك طريقةً لإظهار جميع المكوّنات مرةً أخرى، إذ يصيّر المتصفح حاليًا اسم كل مهمة بوصفه نصًا دون بنية معينة، كما ينقصنا حاليًا بنية HTML مثل عنصر <li> ومربعات الاختيار والأزرار. يمكننا إصلاح ذلك من خلال إعادة المكوّن <Todo /‎> من التابع map()‎، وتذكَّر أنّ صيغة JSX تسمح لنا بخلط بنى جافاسكربت مع اللغات التوصيفية Markup، فلنجرب ما يلي بدلًا مما لدينا حاليًا: const taskList = props.tasks.map(task => <Todo />); انظر مرةً أخرى إلى تطبيقك، إذ تبدو مهامنا الآن كما كانت سابقًا، لكنها تفتقد إلى أسماء المهام نفسها، وتذكَّر أنّ كل مهمة نطبّق عليها التابع map()‎ لها الخاصيات id وname وchecked، والتي نريد تمريرها إلى المكوّن <Todo /‎>، وبالتالي سنحصل على الشيفرة التالية: const taskList = props.tasks.map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} /> )); يبدو التطبيق الآن كما كان سابقًا، ولكن أصبحت شيفرتنا أقل تكرارًا. خاصيات key الفريدة يجب أن تتعقّب React المهام لتصييرها بصورة صحيحة بعد أن صيّرت هذه المهام من مصفوفة، إذ تستخدِم React التخمين لتتبع الأشياء، ولكن يمكننا مساعدتها عن طريق تمرير الخاصية key لمكونات <Todo /‎>، إذ تُعَدّ key خاصيةً خاصةً تديرها React، ولا يمكنك استخدام الكلمة key لأيّ غرض آخر، وبما أنّ الخاصية key يجب أن تكون فريدةً، فسنعيد استخدام خاصية id الخاصة بكل كائن مهمة على أساس مفتاح له، لذا عدِّل الثابت taskList كما يلي: const taskList = props.tasks.map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} /> ) ); يجب عليك دائمًا تمرير مفتاح فريد لأيّ شيء تُصيّره مع التكرار، ولن يتغير شيء واضح في متصفحك، ولكن إذا لم تستخدِم مفاتيح فريدةً، فستعطي React تحذيرات في الطرفية console ويمكن أن يتصرف تطبيقك بطريقة غريبة. تقسيم أجزاء التطبيق المتبقية إلى مكونات يمكننا الآن تحويل باقي التطبيق إلى مكونات بعد فرزِنا المكوّن الأكثر أهميةً، وتذكَّر أنّ المكونات هي إما أجزاء واضحة من واجهة المستخدِم، أو أجزاء معاد استخدامها من واجهة المستخدِم، أو كليهما، كما يمكننا إنشاء مكونين آخرين هما: <Form/‎> <FilterButton/‎> بما أننا نعلم بحاجتنا لهذين المكوِنين، فيمكننا تجميع أوامر إنشاء الملفات في أمر واحد في الطرفية، لذا شغّل الأمر التالي في طرفيتك، مع الانتباه إلى أنك في المجلد الجذر لتطبيقك: touch src/components/Form.js src/components/FilterButton.js المكون <Form/‎> افتح الملف components/Form.js ونفِّذ ما يلي: استورد مكتبة React في أعلى الملف كما فعلنا في الملف Todo.js. أنشئ المكوِّن Form()‎ الجديد باستخدام بنية Todo()‎ الأساسية نفسها، ثم صدِّر هذا المكوِّن. انسخ وسوم <form> وما يوجد بينها من الملف App.js، والصقها ضمن تعليمة return الخاصة بالمكوِّن Form()‎. صدّر المكوِّن Form في نهاية الملف. يجب أن يكون الملف Form.js الآن كما يلي: import React from "react"; function Form(props) { return ( <form> <h2 className="label-wrapper"> <label htmlFor="new-todo-input" className="label__lg"> What needs to be done? </label> </h2> <input type="text" id="new-todo-input" className="input input__lg" name="text" autoComplete="off" /> <button type="submit" className="btn btn__primary btn__lg"> Add </button> </form> ); } export default Form; المكون <FilterButton/‎> كرِّر الأمور نفسها التي نفّذتها لإنشاء الملف Form.js على الملف FilterButton.js، ولكن استدعِ المكوِّن FilterButton()‎ وانسخ جزء HTML للزر الأول الموجود ضمن العنصر <div> ذو الصنف filters من الملف App.js في تعليمة return، إذ يجب أن يكون الملف الآن كما يلي: import React from "react"; function FilterButton(props) { return ( <button type="button" className="btn toggle-btn" aria-pressed="true"> <span className="visually-hidden">Show </span> <span>all </span> <span className="visually-hidden"> tasks</span> </button> ); } export default FilterButton; استيراد جميع المكونات أضف بعض تعليمات الاستيراد import في الجزء العلوي من الملف App.js لاستيراد هذه المكوّنات الجديدة، ثم عدّل تعليمة return الخاصة بالمكوِّن App()‎ لتصيير المكونات، إذ يجب أن يكون الملف ‎‎App.js كما يلي: import React from "react"; import Form from "./components/Form"; import FilterButton from "./components/FilterButton"; import Todo from "./components/Todo"; function App(props) { const taskList = props.tasks.map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} /> ) ); return ( <div className="todoapp stack-large"> <h1>TodoMatic</h1> <Form /> <div className="filters btn-group stack-exception"> <FilterButton /> <FilterButton /> <FilterButton /> </div> <h2 id="list-heading">3 tasks remaining</h2> <ul role="list" className="todo-list stack-large stack-exception" aria-labelledby="list-heading" > {taskList} </ul> </div> ); } export default App; نكون بذلك جاهزين تقريبًا للتعامل مع التفاعل في تطبيق React الخاص بنا. الخلاصة تعمّقنا في كيفية تقسيم التطبيق إلى مكوّنات وتصييرها بكفاءة، إذ سننتقل الآن إلى إلقاء نظرة على كيفية التعامل مع الأحداث في React وإضافة التفاعل. ترجمة -وبتصرُّف- للمقال Componentizing our React app. اقرأ أيضًا أساسيات بناء تطبيقات الويب مكونات React الأساسية (React Components)
  22. لنفترض أننا نريد توضيح مفهوم React من خلال إنشاء تطبيق يسمح للمستخدِمين بإضافة المهام التي يريدون العمل عليها وتعديلها وحذفها، وكذلك وضع علامة على المهام المكتملة دون حذفها، إذ سنوجّهك من خلال هذا المقال لوضع بنية المكوّن App الأساسية وتصميمه في المكان الصحيح، وتعريف المكوّنات الفردية والتفاعلية التي سنضيفها لاحقًا. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية. الهدف: تقديم دراسة حالة تطبيق قائمة المهام، ووضع بنية المكوّن App الأساسية وتصميمه في المكان الصحيح. قصص مستخدم التطبيق تُعَدّ قصة المستخدِم في تطوير البرمجيات هدفًا قابلًا للتنفيذ من منظور المستخدِم، إذ سيساعدنا تحديد قصص المستخدِمين قبل البدء في تركيز عملنا، ويجب على تطبيقنا تحقيق القصص التالية، إذ يمكن للمستخِدم تنفيذ ما يلي: قراءة قائمة المهام. إضافة مهمة باستخدام الفأرة أو لوحة المفاتيح. وضْع علامة على المهام المكتملة باستخدام الفأرة أو لوحة المفاتيح. حذف أيّ مهمة باستخدام الفأرة أو لوحة المفاتيح. تعديل أيّ مهمة باستخدام الفأرة أو لوحة المفاتيح. عرض مجموعة فرعية محدَّدة من المهام: جميع المهام، أو المهمة النشطة فقط، أو المهام المكتملة فقط. تجهيز المشروع الأولي أنشأتْ الأداة create-react-app بعض الملفات التي لن نستخدِمها مطلقًا في مشروعنا، إذ لن نضيف ملف تنسيق سابق لعرض المكونات، لذا احذف أولًا استيراد App.css من أعلى الملف App.js، كما أننا لن نستخِدم الملف logo.svg، لذا أزِل استيراده أيضًا، ثم انسخ والصق بعد ذلك الأوامر التالية في طرفيتك لحذف بعض الملفات غير الضرورية، وتأكّد من أنك تبدأ من المجلد الجذر للتطبيق: # ‫انتقل إلى المجلد src الخاص بمشروعك cd src # احذف بعض الملفات rm -- App.test.js App.css logo.svg serviceWorker.js setupTests.js # انتقل احتياطيًا إلى جذر المشروع cd .. ملاحظتان: هناك ملفان من الملفات التي حذفناها مخصَّصان لاختبار التطبيق، وبالتالي لن نغطّي الاختبار في مثالنا. إذا أوقفت خادمك لتنفيذ المهام السابقة في الطرفية، فيجب تشغيله مرةً أخرى باستخدام الأمر npm start. شيفرة المشروع الأساسية سنقدِّم فيما يلي شيفرة الدالة App()‎ وشيفرة CSS لتنسيق تطبيقك لتستخدمها بدلًا من الشيفرة التي لديك الآن. صيغة JSX انسخ مقتطف الشيفرة التالي إلى مفكرتك، ثم الصقه في الملف App.js بحيث يحل محل دالة App()‎ الحالية: function App(props) { return ( <div className="todoapp stack-large"> <h1>TodoMatic</h1> <form> <h2 className="label-wrapper"> <label htmlFor="new-todo-input" className="label__lg"> What needs to be done? </label> </h2> <input type="text" id="new-todo-input" className="input input__lg" name="text" autoComplete="off" /> <button type="submit" className="btn btn__primary btn__lg"> Add </button> </form> <div className="filters btn-group stack-exception"> <button type="button" className="btn toggle-btn" aria-pressed="true"> <span className="visually-hidden">Show </span> <span>all</span> <span className="visually-hidden"> tasks</span> </button> <button type="button" className="btn toggle-btn" aria-pressed="false"> <span className="visually-hidden">Show </span> <span>Active</span> <span className="visually-hidden"> tasks</span> </button> <button type="button" className="btn toggle-btn" aria-pressed="false"> <span className="visually-hidden">Show </span> <span>Completed</span> <span className="visually-hidden"> tasks</span> </button> </div> <h2 id="list-heading"> 3 tasks remaining </h2> <ul role="list" className="todo-list stack-large stack-exception" aria-labelledby="list-heading" > <li className="todo stack-small"> <div className="c-cb"> <input id="todo-0" type="checkbox" defaultChecked={true} /> <label className="todo-label" htmlFor="todo-0"> Eat </label> </div> <div className="btn-group"> <button type="button" className="btn"> Edit <span className="visually-hidden">Eat</span> </button> <button type="button" className="btn btn__danger"> Delete <span className="visually-hidden">Eat</span> </button> </div> </li> <li className="todo stack-small"> <div className="c-cb"> <input id="todo-1" type="checkbox" /> <label className="todo-label" htmlFor="todo-1"> Sleep </label> </div> <div className="btn-group"> <button type="button" className="btn"> Edit <span className="visually-hidden">Sleep</span> </button> <button type="button" className="btn btn__danger"> Delete <span className="visually-hidden">Sleep</span> </button> </div> </li> <li className="todo stack-small"> <div className="c-cb"> <input id="todo-2" type="checkbox" /> <label className="todo-label" htmlFor="todo-2"> Repeat </label> </div> <div className="btn-group"> <button type="button" className="btn"> Edit <span className="visually-hidden">Repeat</span> </button> <button type="button" className="btn btn__danger"> Delete <span className="visually-hidden">Repeat</span> </button> </div> </li> </ul> </div> ); } افتح الآن الملف public/index.html وعدّل نص العنصر <title> ليصبح TodoMatic، بحيث يطابق العنصر <h1> الموجود أعلى تطبيقنا. <title>TodoMatic</title> يجب أن ترى ما يلي عند تحديث متصفحك: لا يبدو هذا التطبيق جميلًا ولا يعمل بعد، لكنه جيد حاليًا، وضَع في بالك شيفرة JSX، وكيفية توافقها مع قصص المستخدِمين: لدينا عنصر <form> مع العنصر <input type="text"‎> لكتابة مهمة جديدة، وزر لإرسال النموذج. لدينا مجموعة من الأزرار التي سنستخدِمها لمهامنا. لدينا عنوان heading يخبرنا عن عدد المهام المتبقية. لدينا ثلاث مهام مرتبة ضمن قائمة غير مرتبة، إذ تُعَدّ كل مهمة أنها عنصر قائمة <li>، كما تحتوي على أزرار لتعديلها وحذفها، بالإضافة إلى مربع اختيار لإيقاف تشغيلها. سيسمح النموذج بإنشاء المهام، إذ تسمح الأزرار بترشيح المهام، ويُعَدّ العنوان والقائمة طريقةً لقراءتها، إذ ليست واجهة المستخدِم المُستخدَمة لتعديل مهمة موجودةً في الوقت الحالي، ولكن سنكتبها لاحقًا. ميزات الشمولية قد تلاحظ بعض السمات غير العادية هنا مثل: <button type="button" className="btn toggle-btn" aria-pressed="true"> <span className="visually-hidden">Show </span> <span>all</span> <span className="visually-hidden"> tasks</span> </button> تخبر السمة aria-pressed التقنيات المساعدة مثل قارئات الشاشة أنه قد يكون الزر في إحدى حالتين وهما مضغوط pressed أو غير مضغوط unpressed، إذ تمثِّلان حالة التشغيل on والإيقاف off، ويعني تعيين القيمة true أنّ الزر مضغوط افتراضيًا. ليس للصنف visually-hidden أيَّ تأثير حتى الآن، لأننا لم نضمِّن شيفرة CSS، فإذا وضعنا التنسيقات في مكانها الصحيح، فسيُخفَى أيّ عنصر في هذا الصنف عن المستخدِمين المبصرين، وسيظل متاحًا لمستخدِمي قارئ الشاشة، لأن هذه الكلمات لا يحتاجها المستخدِمون المبصرون، إذ تُستخدَم لتقديم المزيد من المعلومات حول ما يفعله الزر لمستخدِمي قارئ الشاشة الذين ليس لديهم القدرة البصرية الإضافية لمساعدتهم، كما يمكنك العثور على العنصر <ul> التالي: <ul role="list" className="todo-list stack-large stack-exception" aria-labelledby="list-heading" > تساعد السمة role التقنيات المساعِدة في توضيح نوع العنصر الذي يمثِّله الوسم، إذ يجري التعامل مع العنصر <ul> بوصفه قائمةً افتراضيًا، ولكن ستؤدي التنسيقات التي نضيفها إلى تعطيل هذه الوظيفة، في حين ستؤدي السمة role إلى استعادة القائمة التي تعني العنصر <ul>. تخبر السمة aria-labelledby التقنيات المساعِدة بتعاملنا مع عنوان قائمتنا بوصفه العنوان الذي يصف الغرض من القائمة الموجودة تحته، مما يساعد مستخدِمي قارئ الشاشة على فهم الغرض منها بصورة أفضل، وأخيرًا، فتملك عناصر label وinput في عناصر القائمة بعض السمات الفريدة الخاصة بصيغة JSX، وهي: <input id="todo-0" type="checkbox" defaultChecked={true} /> <label className="todo-label" htmlFor="todo-0"> Eat </label> السمة defaultChecked في الوسم <input /‎> تخبر React بتحديد مربع الاختيار مبدئيًا، فإذا أردنا استخدام السمة checked كما نفعل في HTML، فستعرِض React بعض التحذيرات المتعلقة بمعالجة أحداث مربع الاختيار في طرفية متصفحك (نافذة console)، والتي يجب تجنبها، ولا تقلق كثيرًا بشأن ذلك في الوقت الحالي، إذ سنغطي ذلك لاحقًا عندما نبدأ باستخدام الأحداث. تتوافق السمة htmlFor مع السمة for المُستخدَمة في لغة HTML، ولا يمكننا استخدام الكلمة for بوصفها سمةً في صيغة JSX لأنها كلمة محجوزة، لذلك تستخدِم React السمة htmlFor بدلًا من ذلك. ملاحظتان: يمكنك استخدام القيم المنطقية -أي true وfalse- في سمات JSX من خلال إحاطة هذه القيم بأقواس معقوصة، فإذا كتبت السمة defaultChecked="true"‎ مثلًا، فستكون "true" هي قيمة السمة defaultChecked، والتي تُعَدّ سلسلةً حرفيةً String Literal، لأنها لغة جافاسكربت وليست لغة HTML. تملك السمة aria-pressed القيمة "true" في مثالنا لأنّ aria-pressed ليست سمةً منطقيةً حقيقيةً بالطريقة التي تستخدِمها السمة checked بها. تنفيذ التنسيقات الصق شيفرة CSS التالية في الملف src/index.css لتحُل محل ما هو موجود حاليًا: /* إعادة الضبط */ *, *::before, *::after { box-sizing: border-box; } *:focus { outline: 3px dashed #228bec; outline-offset: 0; } html { font: 62.5% / 1.15 sans-serif; } h1, h2 { margin-bottom: 0; } ul { list-style: none; padding: 0; } button { border: none; margin: 0; padding: 0; width: auto; overflow: visible; background: transparent; color: inherit; font: inherit; line-height: normal; -webkit-font-smoothing: inherit; -moz-osx-font-smoothing: inherit; -webkit-appearance: none; } button::-moz-focus-inner { border: 0; } button, input, optgroup, select, textarea { font-family: inherit; font-size: 100%; line-height: 1.15; margin: 0; } button, input { overflow: visible; } input[type="text"] { border-radius: 0; } body { width: 100%; max-width: 68rem; margin: 0 auto; font: 1.6rem/1.25 Arial, sans-serif; background-color: #f5f5f5; color: #4d4d4d; } @media screen and (min-width: 620px) { body { font-size: 1.9rem; line-height: 1.31579; } } /* نهاية إعادة الضبط */ /* التنسيقات العامة */ .form-group > input[type="text"] { display: inline-block; margin-top: 0.4rem; } .btn { padding: 0.8rem 1rem 0.7rem; border: 0.2rem solid #4d4d4d; cursor: pointer; text-transform: capitalize; } .btn.toggle-btn { border-width: 1px; border-color: #d3d3d3; } .btn.toggle-btn[aria-pressed="true"] { text-decoration: underline; border-color: #4d4d4d; } .btn__danger { color: #fff; background-color: #ca3c3c; border-color: #bd2130; } .btn__filter { border-color: lightgrey; } .btn__primary { color: #fff; background-color: #000; } .btn-group { display: flex; justify-content: space-between; } .btn-group > * { flex: 1 1 49%; } .btn-group > * + * { margin-left: 0.8rem; } .label-wrapper { margin: 0; flex: 0 0 100%; text-align: center; } .visually-hidden { position: absolute !important; height: 1px; width: 1px; overflow: hidden; clip: rect(1px 1px 1px 1px); clip: rect(1px, 1px, 1px, 1px); white-space: nowrap; } [class*="stack"] > * { margin-top: 0; margin-bottom: 0; } .stack-small > * + * { margin-top: 1.25rem; } .stack-large > * + * { margin-top: 2.5rem; } @media screen and (min-width: 550px) { .stack-small > * + * { margin-top: 1.4rem; } .stack-large > * + * { margin-top: 2.8rem; } } .stack-exception { margin-top: 1.2rem; } /* نهاية التنسيقات العامة */ .todoapp { background: #fff; margin: 2rem 0 4rem 0; padding: 1rem; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1); } @media screen and (min-width: 550px) { .todoapp { padding: 4rem; } } .todoapp > * { max-width: 50rem; margin-left: auto; margin-right: auto; } .todoapp > form { max-width: 100%; } .todoapp > h1 { display: block; max-width: 100%; text-align: center; margin: 0; margin-bottom: 1rem; } .label__lg { line-height: 1.01567; font-weight: 300; padding: 0.8rem; margin-bottom: 1rem; text-align: center; } .input__lg { padding: 2rem; border: 2px solid #000; } .input__lg:focus { border-color: #4d4d4d; box-shadow: inset 0 0 0 2px; } [class*="__lg"] { display: inline-block; width: 100%; font-size: 1.9rem; } [class*="__lg"]:not(:last-child) { margin-bottom: 1rem; } @media screen and (min-width: 620px) { [class*="__lg"] { font-size: 2.4rem; } } .filters { width: 100%; margin: unset auto; } ‏/* تنسيقات عناصر‫ Todo */ .todo { display: flex; flex-direction: row; flex-wrap: wrap; } .todo > * { flex: 0 0 100%; } .todo-text { width: 100%; min-height: 4.4rem; padding: 0.4rem 0.8rem; border: 2px solid #565656; } .todo-text:focus { box-shadow: inset 0 0 0 2px; } /* تنسيقات مربعات الاختيار */ .c-cb { box-sizing: border-box; font-family: Arial, sans-serif; -webkit-font-smoothing: antialiased; font-weight: 400; font-size: 1.6rem; line-height: 1.25; display: block; position: relative; min-height: 44px; padding-left: 40px; clear: left; } .c-cb > label::before, .c-cb > input[type="checkbox"] { box-sizing: border-box; top: -2px; left: -2px; width: 44px; height: 44px; } .c-cb > input[type="checkbox"] { -webkit-font-smoothing: antialiased; cursor: pointer; position: absolute; z-index: 1; margin: 0; opacity: 0; } .c-cb > label { font-size: inherit; font-family: inherit; line-height: inherit; display: inline-block; margin-bottom: 0; padding: 8px 15px 5px; cursor: pointer; touch-action: manipulation; } .c-cb > label::before { content: ""; position: absolute; border: 2px solid currentColor; background: transparent; } .c-cb > input[type="checkbox"]:focus + label::before { border-width: 4px; outline: 3px dashed #228bec; } .c-cb > label::after { box-sizing: content-box; content: ""; position: absolute; top: 11px; left: 9px; width: 18px; height: 7px; transform: rotate(-45deg); border: solid; border-width: 0 0 5px 5px; border-top-color: transparent; opacity: 0; background: transparent; } .c-cb > input[type="checkbox"]:checked + label::after { opacity: 1; } احفظ الملف وألقِ نظرةً على المتصفح، إذ يجب أن يتمتع تطبيقك الآن بتنسيق مقبول. الخلاصة يبدو تطبيق قائمة المهام الآن أشبه بتطبيق حقيقي، ولكنه لا يفعل أيّ شيء فعليًا، إذ سنبدأ في إصلاح ذلك لاحقًا في المقالات القادمة. ترجمة -وبتصرُّف- للمقال Beginning our React todo list. اقرأ أيضًا أساسيات بناء تطبيقات الويب مكونات React الأساسية (React Components) إنشاء تطبيق قائمة مهام بسيط باستخدام Laravel 5
  23. سنلقي في هذا المقال نظرةً على مكتبة React، إذ سنطّلع على بعض التفاصيل حول خلفيتها وحالات استخدامها، وسننشئ سلسلة أدوات React الأساسية وتطبيقًا بسيطًا بحيث نتعلم كيفية عمل React. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية، إذ تستخدِم React صيغة لغة HTML ضمن جافاسكربت HTML-in-JavaScript، والتي تسمى JSX، أي JavaScript وXML، كما سيساعدك التعرف على كل من لغة HTML وجافاسكربت على تعلّم صيغة JSX، وتحديد ما إذا كانت الأخطاء في تطبيقك مرتبطةً بجافاسكربت أو بمجال أكثر تحديدًا من React. الهدف: إعداد بيئة تطوير React المحلية، وإنشاء تطبيق بسيط، وفهم أساسيات عمله. تُعَدّ React مكتبةً لبناء واجهات المستخدِم، ولا تُعَدّ إطار عمل، فهي ليست حصريةً للويب، كما تُستخدَم مكتبة React مع المكتبات الأخرى للتصيير Render إلى بيئات معينة، إذ يمكن استخدام إطار عمل React Native لبناء تطبيقات الهاتف المحمول، لكن يستخدِم المطورون مكتبة React جنبًا إلى جنب مع ReactDOM للبناء للويب، إذ تُستخدَم React و ReactDOM في المجالات نفسها ولحل المشكلات نفسها التي تستخدِمها أطر تطوير الويب الحقيقية الأخرى، لذلك نشير إلى React بوصفها إطار عمل Framework. تهدف React إلى تقليل الأخطاء التي تحدث عندما يبني المطورون واجهات المستخدِم من خلال استخدام المكوّنات Components، والتي تُعَدّ أجزاءً من الشيفرة البرمجية المنطقية والمستقلة ذاتيًا والتي تصف جزءًا من واجهة المستخدِم، إذ يمكن تكوين هذه المكونات مع بعضها البعض لإنشاء واجهة مستخدِم كاملة، كما تجرِّّد React كثيرًا من أعمال التصيير، وبالتالي تجعلك تركِّز على تصميم واجهة المستخدِم. حالات الاستخدام Use cases لا تفرض React قواعد صارمةً حول اصطلاحات الشيفرة أو تنظيم الملفات خلاف أطر العمل Frameworks الأخرى، مما يتيح لفرق العمل تحديد الاصطلاحات التي تناسبها بصورة أفضل، واستخدام مكتبة React بالطريقة التي ترغب بها، إذ يمكن لمكتبة React معالجة زر واحد أو أجزاء من الواجهة أو واجهة المستخدِم للتطبيق بأكمله، فإذا أردت استخدام React لأجزاء صغيرة من الواجهة، فلا يُعَدّ ذلك سهلًا مثل بناء تطبيق باستخدام مكتبة مثل jQuery أو إطار عمل مثل Vue، إذ يكون استخدام مكتبة React أسهل عند إنشاء تطبيقك بالكامل باستخدامها. كما تتطلب العديد من مزايا تجربة المطوِّر لتطبيق React مثل كتابة الواجهات باستخدام صيغة JSX، عملية تصريف Compilation، في حين تبطّئ إضافة مصرِّف مثل Babel إلى موقع ويب الشيفرة الموجودة عليه، لذلك يُعِدّ المطورون مثل هذه الأدوات باستخدام خطوة بناء، إذ يمكن القول أنّ React لها متطلبات أدوات كثيرة، ولكن يمكن تعلّمها، كما سيركِّز هذا المقال على حالة استخدام React لتصيير واجهة المستخدِم بالكامل لتطبيق ما باستخدام الأدوات التي توفرها أداة create-react-app الخاصة بفيسبوك. كيفية استخدام React للغة جافاسكربت تستخدِم React ميزات لغة جافاسكربت الحديثة للعديد من أنماطها، ولكن يأتي أكبر تحوّل لها عن جافاسكربت عند استخدام صيغة JSX التي توسِّع صيغة جافاسكربت، بحيث يمكن أن تكون الشيفرة البرمجية التي تشبه HTML جنبًا إلى جنب معها، وإليك المثال التالي: const heading = <h1>Mozilla Developer Network</h1>; يُعرَف الثابت heading السابق بتعبير JSX، ويمكن لمكتبة React استخدامه لتصيير الوسم <h1> في التطبيق، ولنفترض أننا أردنا تغليف العنوان heading بوسم <header> لأسباب دلالية، إذ تتيح صيغة JSX بتداخل العناصر ضمن بعضها بعضًا كما نفعل مع لغة HTML كما يلي: const header = ( <header> <h1>Mozilla Developer Network</h1> </header> ); ملاحظة: لا تُعَدّ الأقواس في المقتطف السابق خاصةً بصيغة JSX، وليس لها أيّ تأثير على تطبيقك، وإنما تُعَدّ إشارةً لك ولحاسوبك بأن الأسطر المتعددة من الشيفرة البرمجية الموجودة ضمنها هي جزء من التعبير نفسه، كما يمكنك كتابة تعبير header كما يلي: const header = <header> <h1>Mozilla Developer Network</h1> </header> لا يمكن لمتصفحك قراءة صيغة JSX بدون مساعدة، إذ سيبدو التعبير header كما يلي عند تصريفه باستخدام أداة Babel أو Parcel: const header = React.createElement("header", null, React.createElement("h1", null, "Mozilla Developer Network") ); يمكن تخطي خطوة التصريف واستخدام التابع React.createElement()‎ لكتابة واجهة المستخدِم بنفسك، ولكنك تفقد بذلك ميزة JSX التصريحية، وتصبح قراءة شيفرتك أصعب، إذ يُعَدّ التصريف خطوةً إضافيةً في عملية التطوير، في حين يعتقد العديد من المطورين في مجتمع React أنّ قابلية قراءة JSX تستحق العناء، كما تجعل الأدوات الشائعة تصريف صيغة JSX إلى جافاسكربت جزءًا من عملية الإعداد، ولا يتعين عليك إعداد التصريف بنفسك إلّا إذا أردت ذلك. تُعَدّ صيغة JSX مزيجًا من لغتَي HTML وجافاسكربت، لذلك يجدها بعض المطورين سهلة التعلم، ويجدها آخرون مربكةً بسبب طبيعتها الممزوجة، ولكنها ستسمح لك ببناء واجهات مستخدِم بسرعة وبسهولة إذا أتقنتها، كما ستسمح للآخرين بفهم قاعدة شيفرتك البرمجية فهمًا أفضل وبسرعة، كما يمكنك الاطلاع على صفحة شرح JSX بالتفصيل من توثيق React في موسوعة حسوب لقراءة المزيد عن JSX. إعداد تطبيق React الأول هناك العديد من الطرق لاستخدام React، لكننا سنستخدِم create-react-app وهي أداة واجهة سطر الأوامر -أو CLI اختصارًا-، إذ تسرّع هذه الأداة عملية تطوير تطبيق React عن طريق تثبيت بعض الحزم وإنشاء بعض الملفات، والتعامل مع الأدوات الموضَّحة سابقًا، كما يمكن إضافة React إلى موقع ويب دون استخدام الأداة create-react-app عن طريق نسخ بعض عناصر <script> في ملف HTML، ولكن تُعَدّ الأداة create-react-app نقطة بداية شائعة لتطبيقات React، إذ سيسمح لك استخدامها بقضاء المزيد من الوقت في بناء تطبيقك ووقت أقل في التفكير في الإعداد. المتطلبات يجب تثبيت Node.js من أجل استخدام create-react-app، كما يوصى باستخدام إصدار الدعم طويل الأمد Long-term Support -أو LTS اختصارًا-، إذ يتضمن Node مدير الحزم npm ومشغّل الحزم npx، كما يمكنك استخدام مدير الحزم Yarn، لكننا سنفترض أنك تستخدِم npm في هذا المقال، وهنا يمكنك الاطلاع على مقال أساسيات إدارة الحزم لمزيد من المعلومات حول npm وYarn. إذا استخدمت نظام ويندوز Windows، فستحتاج إلى تثبيت بعض البرامج التي تمنحك التكافؤ مع طرفية نظام يونيكس Unix أو نظام ماك macOS لاستخدام أوامر الطرفية التي سنستخدِمها، إذ يُعَدّ كل من Gitbash الذي يكون جزءًا من مجموعة أدوات git لنظام ويندوز أو نظام ويندوز الفرعي للينكس Windows Subsystem for Linux -أو WSL اختصارًا- مناسبَين، كما يمكنك الاطلاع على دليل استخدام سطر الأوامر للحصول على مزيد من المعلومات حول هذه الأوامر وحول أوامر الطرفية بصفة عامة. ضع في بالك أنّ React و ReactDOM ينتجان تطبيقات تعمل فقط على مجموعة حديثة إلى حد ما من المتصفحات مثل IE9+‎ باستخدام تعويض نقص دعم المتصفحات Polyfill، كما يوصَى باستخدام متصفح حديث مثل فايرفوكس Firefox أو مايكروسوفت إيدج Microsoft Edge أو سفاري Safari أو كروم Chrome. تهيئة التطبيق تأخذ الأداة create-react-app وسيطًا واحدًا هو الاسم الذي ترغب في منحه لتطبيقك، وتستخدِمه لإنشاء مجلد جديد، ثم تنشئ الملفات الضرورية بداخله، وتأكد من تطبيق الأمر cd على المكان الذي تريد أن يكون فيه تطبيقك على القرص الصلب، ثم شغّل الأمر التالي في الطرفية: npx create-react-app moz-todo-react يؤدي تشغيل الأمر السابق إلى إنشاء المجلد moz-todo-response، مع تنفيذ الأمور التالية ضمنه: تثبيت بعض حزم npm الأساسية لعمل التطبيق. كتابة سكربتات لبدء التطبيق وتنفيذه. إنشاء بنية من الملفات والمجلدات التي تحدِّد معمارية التطبيق الأساسية. تهيئة المجلد بوصفه مستودع جيت git إذا كان جيت مثبتًا على حاسوبك. ملاحظة: إذا كان مدير الحزم yarn مثبتًا لديك، فستُستخدَم أداة create-react-app افتراضيًا لاستخدام yarn بدلًا من npm، وإذا كان كل من مديرَي الحزم مثبَّتَين لديك وتريد استخدام npm صراحةً، فيمكنك إضافة الراية ‎--use-npm عند تشغيل create-react-app: npx create-react-app moz-todo-react --use-npm ستعرِض create-react-app عددًا من الرسائل في الطرفية أثناء عملها، وهذا أمر طبيعي، إذ يمكن أن يستغرق ذلك بضع دقائق، فالوقت مناسب الآن لتحضير كوب من الشاي. غيّر المسار الحالي إلى المجلد moz-todo-react باستخدام الأمر cd عند اكتمال العملية، ثم شغّل الأمر npm start، إذ سيبدأ تشغيل السكربتات المُثبَّتة باستخدام الأداة create-react-app على خادم محلي على المضيف المحلي localhost الذي هو 3000، وافتح التطبيق في تبويب جديد من المتصفح، إذ سيعرِض متصفحك ما يلي: بنية التطبيق تمنحنا أداة create-react-app كل ما نحتاجه لتطوير تطبيق React، إذ تبدو بنية الملفات الأولية الخاصة به كما يلي: moz-todo-react ├── README.md ├── node_modules ├── package.json ├── package-lock.json ├── .gitignore ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg ├── reportWebVitals.js └── setupTests.js يُعَدّ المجلد src بأنه المكان الذي سنقضي فيه معظم وقتنا، فهو مكان وجود شيفرة تطبيقنا البرمجية، كما يحتوي المجلد public على ملفات سيقرأها متصفحك أثناء تطوير التطبيق وأهمها index.html، إذ تحقن React شيفرتك البرمجية في هذا الملف ليتمكّن متصفحك من تشغيلها، وهناك بعض الوسوم الأخرى التي تساعد الأداة create-react-app في عملها، لذا احرص على عدم تعديلها إلا إذا كنت متأكدًا مما تفعله، ولكن يجب عليك تغيير النص الموجود داخل العنصر <title> في هذا الملف ليعكس عنوان تطبيقك، وعناوين الصفحات الدقيقة مهمة من أجل إمكانية الوصول. سيُنشَر أيضًا المجلد public عند إنشاء ونشر إصدار الإنتاج من تطبيقك، إذ لن نغطّي مرحلة النشر في هذا المقال، ولكن يجب أن تكون قادرًا على استخدام حل مشابه لذلك الموضَّح في مقال نشر التطبيق، في حين يحتوي الملف package.json على معلومات حول مشروعنا، والتي يستخدِمها كل من Node.js وnpm لإبقائه منظمًّا، كما لا يُعَدّ هذا الملف خاصًا بتطبيقات React، ولا تحتاج إلى فهم هذا الملف على الإطلاق لإكمال هذا المقال، ولكن إذا أردتَ معرفة المزيد عنه، فيمكنك قراءة مقال أساسيات إدارة الحزم. استكشاف مكون React الأول يُعَدّ المكوّن Component في React وحدةً قابلةً لإعادة الاستخدام والتي تصيّر جزءًا من التطبيق، كما يمكن أن تكون هذه الأجزاء كبيرةً أو صغيرةً، لكنها تكون عادةً محددةً بوضوح، فهي تخدم غرضًا واحدًا واضحًا، ولنفتح الملف src/App.js، لأنّ متصفحنا يطالبنا بتعديله، إذ يحتوي هذا الملف على المكوِّن الأول App وعدد قليل من سطور الشيفرة البرمجية الأخرى: import React from 'react'; import logo from './logo.svg'; import './App.css'; function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> </div> ); } export default App; يتكون ملف App.js من ثلاثة أجزاء رئيسية وهي كما يلي، إذ تتبع معظم مكونات React هذا النمط: بعض تعليمات الاستيراد import في الأعلى. المكوِّن App في المنتصف. تعليمة تصدير export في الأسفل. تعليمات الاستيراد Import تسمح تعليمات الاستيراد الموجودة في أعلى الملف App.js باستخدام الشيفرة المُعرَّفة في مكان آخر، وهذه التعليمات هي: import React from 'react'; import logo from './logo.svg'; import './App.css'; تستورِد التعليمة الأولى مكتبة React التي تحوِّل صيغة JSX التي نكتبها إلى التابع React.createElement()‎، ويجب على جميع مكونّات React استيراد وحدة React، فإذا تخطيت هذه الخطوة، فسيعطي تطبيقك خطأً، في حين تستورِد التعليمة الثانية صورة شعار Logo من الملف '‎./logo.svg'، ولاحظ استخدام /. في بداية المسار، والامتداد ‎.svg في نهايته، إذ يدل ذلك على أن الملف محلي وأنه ليس ملف جافاسكربت، ويوجد الملف logo.svg في مجلدنا المصدر، ولا نكتب مسارًا أو امتدادًا عند استيراد وحدة React، لأنه لا يُعَدّ ملفًا محليًا، وإنما يُدرَج بوصفه اعتماديةً Dependency في الملف package.json. تستورِد التعليمة الثالثة ملف CSS المتعلق بالمكوّن App، ولاحظ عدم وجود اسم متغير والموجِّه from، إذ لا تُعَدّ هذه الصيغة أصيلةً Native في صيغة وحدة جافاسكربت، وإنما تأتي من أداة Webpack وهي الأداة التي تستخدِمها create-react-app لتجميع جميع ملفات جافاسكربت مع بعضها بعضًا وتقديمها إلى المتصفح. المكون App توجد دالة تسمَّى App بعد تعليمات الاستيراد، إذ يفضِّل مجتمع جافاسكربت استخدام الأسماء بحالة الجَمل Camel-case مثل helloWorld، في حين تستخدِم مكونات React أسماء المتغيرات بحالة باسكال Pascal-case مثل HelloWorld لتوضيح أنّ عنصر JSX المحدَّد هو مكون React وليس وسم HTML عادي، فإذا أردت إعادة تسمية الدالة App لتصبح app، فسيعطي متصفحك خطأً. function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> </div> ); } تعيد الدالة App تعبير JSX الذي يحدِّد ما يصيّره متصفحك على DOM في النهاية، كما تحتوي بعض العناصر في هذا التعبير على سمات Attributes مكتوبةً كما تُكتَب في لغة HTML تمامًا باتباع النمط attribute="value"‎، في حين يحتوي وسم الفتح <div> على السمة className في السطر الثالث، وهي الخاصية class نفسها في لغة HTML، ولكن لا يمكننا استخدام الكلمة class، لأنّ صيغة JSX هي لغة جافاسكربت وهي كلمة محجوزة فيها، مما يعني أنّ لغة حافاسكربت تستخدِمها مسبقًا لغرض معيَّن، وقد يتسبّب استخدامها في شيفرتنا في حدوث مشاكل، كما تُكتَب بعض سمات HTML الأخرى بطريقة مختلفة في JSX عن تلك الموجودة في لغة HTML للسبب ذاته. عدِّل الوسم <p> في السطر السادس، بحيث يصبح "Hello, world!‎"، ثم احفظ ملفك، إذ ستلاحظ أن هذا التعديل سيُصيَّر مباشرةً في خادم التطوير الذي يعمل على المضيف المحلي http://localhost:3000 في متصفحك، ثم احذف بعد ذلك الوسم <a> واحفظ الملف، مما يؤدي إلى اختفاء رابط "Learn React"، إذ يجب أن يبدو المكوّن App الآن كما يلي: function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Hello, World! </p> </header> </div> ); } تعليمات التصدير تجعل تعليمة التصدير export default App في الجزء السفلي من الملف App.js المكوّنَ App متاحًا للوحدات الأخرى. الملف index.js لنفتح الملف src/index.js الذي يُعَدّ المكان الذي يُستخدَم فيه المكوّن App، وهو نقطة الدخول إلى تطبيقنا، إذ يبدو في البداية كما يلي: import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); // إذا أردت أن يعمل تطبيقك في وضع عدم الاتصال وأن يُحمَّل بسرعة، فيمكنك تغيير // ‫unregister()‎ إلى register()‎ في الأسفل، ولاحظ أنّ هذا يأتي مع بعض المخاطر. serviceWorker.unregister(); يبدأ الملف index.js باستيراد جميع وحدات JS والملفات الأخرى التي يحتاجها للعمل كما هو الحال مع الملف App.js، ويحتفظ الملف src/index.css بالتنسيقات العامة المطبَّقة على تطبيقنا بالكامل، كما يمكننا رؤية المكوّن App الذي استوردناه، فهو متاح للاستيراد بفضل تعليمة التصدير export في أسفل الملف App.js، في يستدعي السطر السابع الدالة ReactDOM.render()‎ مع وسيطين هما: المكوِّن الذي نريد تصييره، وهو <App /‎> في هذه الحالة. عنصر DOM الذي نريد تصيير المكوِّن ضمنه، وهو العنصر ذو المعرِّف root في هذه الحالة، فإذا نظرت ضمن الملف public/index.html، فستجد أن هذا العنصر هو <div> ضمن العنصر <body>. وهذا يعني أننا نريد تصيير تطبيق React الخاص بنا مع المكوِّن App بوصفه الجذر أو المكوِّن الأول. ملاحظة: يجب أن تحتوي مكونات React وعناصر HTML على شرطات إغلاق مائلة في صيغة JSX، إذ ستؤدي كتابة المكوّن <App> فقط أو الوسم <img> فقط إلى حدوث خطأ. تُعَدّ عمّال الخدمة Service workers أجزاءً مثيرةً من الشيفرة البرمجية التي تحسّن أداء التطبيق وتسمح لميزات تطبيقات الويب بالعمل في وضع عدم الاتصال، لكننا لن نتحدّث عنها في هذا المقال، إذ يمكنك حذف السطر الخامس ومعظم الشيفرة الموجودة أسفله، وهنا يجب أن يبدو ملف index.js النهائي كما يلي: import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; ReactDOM.render(<App />, document.getElementById('root')); المتغيرات والخاصيات سنستخدِم فيما يلي بعضًا من مهارات جافاسكربت لنتمكّن من تعديل المكوّنات والتعامل مع البيانات في React، إذ سنتحدث عن كيفية استخدام المتغيرات في JSX، وسنشرح الخاصيات Props التي تُعَدّ طريقةً لتمرير البيانات إلى المكوّن الذي يمكن الوصول إليه بعد ذلك باستخدام المتغيرات. المتغيرات في JSX لنركّز على السطر التاسع في الملف App.js: <img src={logo} className="App-logo" alt="logo" /> وُضِعت قيمة السمة src الخاصة بالوسم <img /‎> ضمن أقواس معقوصة، وهي الطريقة التي تتعرف بها صيغة JSX على المتغيرات، إذ تشير القيمة {logo} إلى استيراد الشعار logo في السطر الثاني من التطبيق، ثم استرداد ملف الشعار وتصييره، ولنحاول إنشاء متغير خاص بنا من خلال إضافة التعليمة const subject = 'React';‎ قبل تعليمة return في الدالة App، إذ يجب أن يبدو المكوّن App الآن كما يلي: function App() { const subject = "React"; return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Hello, World! </p> </header> </div> ); } غيّر السطر الثامن لاستخدام المتغير subject بدلًا من االكلمة "world" كما يلي: function App() { const subject = "React"; return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Hello, {subject}! </p> </header> </div> ); } يجب أن يعرض المتصفح التعليمة "Hello, React!‎" بدلًا من التعليمة "Hello, world!‎" عند الحفظ، ولا يستفيد المتغير الذي ضبطناه للتو استفادةً كبيرةً من ميزات React، لذلك نحتاج إلى استخدام الخاصيات Props. خاصيات المكون الخاصية هي البيانات الممرَّرة إلى مكوِّن React، كما تشبه الخاصيات إلى حد ما سمات HTML، ولكن تمتلك عناصر HTML سمات وتمتلك مكونات React خاصيات، إذ تُكتَب الخاصيات ضمن استدعاءات المكوِّن، وتستخدِم الصيغة نفسها التي تستخدمها سمات HTML وهي prop="value"‎، كما يكون تدفّق البيانات أحادي الاتجاه في React، إذ يمكن تمرير الخاصيات من المكوّنات الآباء إلى المكوّنات الأبناء فقط، وتكون الخاصيات للقراءة فقط، فلنفتح الملف index.js ونمنح المكوّن <App/‎> استدعاءه الأول، ثم أضف الخاصية subject إلى استدعاء المكوِّن <App/‎> مع القيمة Clarice، إذ يجب أن تبدو شيفرتك البرمجية كما يلي: ReactDOM.render(<App subject="Clarice" />, document.getElementById('root')); لنفتح الملف App.js ولننتقل إلى الدالة App()‎ التي يجب أن تكون كما يلي مع اختصار تعليمة return للإيجاز: function App() { const subject = "React"; return ( // ‫تعليمة return ); } عدّل الدالة App بحيث تقبل الخاصيات props على أساس معامِل لها، واحذف الثابت subject، كما يمكنك وضع الخاصيات props في التابع console.log()‎ لطباعتها على طرفية المتصفح كما يلي: function App(props) { console.log(props); return ( // ‫تعليمة return ); } احفظ ملفك وتحقق من طرفية جافاسكربت (نافذة console) في متصفحك، إذ يجب أن ترى شيئًا يشبه ما يلي: Object { subject: "Clarice" } تتوافق خاصية الكائن subject مع الخاصية subject التي أضفناها إلى استدعاء المكون <App /‎>، كما تتوافق سلسلة Clarice النصية مع قيمتها، إذ تُجمَع خاصيات المكوِّن في React دائمًا ضمن كائنات بهذه الطريقة، ولنستخدِم الخاصية subject في الملف App.js، لذا غيّر الثابت subject لقراءة قيمة props.subject بدلًا من تعريفه على أنه سلسلة React، كما يمكنك حذف التابع console.log()‎ إذا أردت ذلك. function App(props) { const subject = props.subject; return ( // تعليمة‫ return ); } يجب أن يعرض التطبيق عبارة "Hello, Clarice!‎" عند الحفظ، فإذا عدت إلى الملف index.js وعدّلت قيمة subject ثم حفظته، فسيتغيّر النص. الخلاصة تعرّفنا في هذا المقال على مكتبة React، بما في ذلك كيفية تثبيتها محليًا، وإنشاء تطبيق بسيط، وكيفية عمل الأساسيات؛ أما في المقال التالي، فسنبدأ بإنشاء أول تطبيق مناسب وهو تطبيق قائمة المهام، لكن لنلخّص بعض الأشياء التي تعلمناها حتى الآن. في React: يمكن للمكونات استيراد الوحدات التي تحتاجها ويجب أن تصدِّر نفسها في الجزء السفلي من ملفاتها. تُسمَّى دوال المكوِّن باستخدام حالة باسكال PascalCase. يمكنك قراءة متغيرات JSX بوضعها بين أقواس معقوصة مثل {so}. تختلف بعض سمات JSX عن سمات HTML بحيث لا تتعارض مع كلمات جافاسكربت المحجوزة، إذ تُترجَم class في لغة HTML إلى className في JSX مثلًا، ولاحظ أنّ السمات متعددة الكلمات تُسمَّى باستخدام حالة الجَمل camel-cased. تُكتَب الخاصيات تمامًا مثل السمات ضمن استدعاءات المكوِّن وتُمرَّر إلى المكوّنات. ترجمة -وبتصرُّف- للمقال Getting started with React. اقرأ أيضًا مدخل إلى React.js - مكتبة تطوير الواجهات الرسومية من فيس بوك مدخل إلى استعمال المكتبة React-Router اختبار تطبيقات React باستعمال Jest ومكتبة React Testing Library المصطلحات المستخدمة في React
  24. يملك كل إطار عمل جافاسكربت رئيسي نهجًا مختلف لتحديث نموذج كائن المستند DOM، ومعالجة أحداث المتصفح، وتوفير تجربة مطوِّر ممتعة، إذ سنستكشف في هذا المقال الميزات الرئيسية لأطر عمل "الأربعة الكبار"، وكيفية عمل هذه الأطر، والاختلافات بينها. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت. الهدف: فهم ميزات شيفرة أطر العمل Frameworks الرئيسية. لغات المجال المحدد Domain-specific Languages سنشغّل جميع أطر العمل التي سنناقشها في هذا المقال باستخدام لغة جافاسكربت، إذ ستتيح جميعها استخدام لغات المجال المحدد Domain-specific Languages -أو DSLs اختصارًا- لبناء التطبيقات، كما تستخدم مكتبة React صيغة JSX لكتابة مكوناتها، في حين يستخدِم إطار عمل Ember لغة Handlebars، كما تعرف هذه اللغات كيفية قراءة متغيرات البيانات على عكس لغة HTML، ويمكن استخدام هذه البيانات لتبسيط عملية كتابة واجهة المستخدِم؛ أما تطبيقات Angular، فتستخدِم لغة TypeScript التي لا تهتم بكتابة واجهات المستخدِم ولكنها لغة مجال محدد، وتختلف كثيرًا عن لغة جافاسكربت الصرفة Vanilla JavaScript. لا يستطيع المتصفح قراءة لغات DSL مباشرةً، لذلك يجب تحويلها إلى لغة جافاسكربت أو HTML أولًا، إذ يُعَدّ التحويل خطوةً إضافيةً في عملية التطوير، ولكن تتضمن أدوات إطار العمل الأدوات المطلوبة لمعالجة هذه الخطوة، أو يمكن تعديلها لتضمين هذه الخطوة، كما يمكن إنشاء تطبيقات إطار عمل دون استخدام هذه اللغات، لكن سيبسّط استخدامها عملية التطوير ويسهّل العثور على المساعدة من مجتمعات تلك الأطر. صيغة JSX يرمز الاختصار JSX إلى لغتي جافاسكربت و XML، ويُعَدّ امتدادًا للغة جافاسكربت، إذ يضيف صيغةً تشبه لغة HTML إلى بيئة جافاسكربت، وقد اخترع فريق React صيغة JSX لاستخدامها في تطبيقات React، ولكن يمكن استخدامها لتطوير تطبيقات أخرى مثل تطبيقات Vue، وإليك مثال بسيط لصيغة JSX: const subject = "World"; const header = ( <header> <h1>Hello, {subject}!</h1> </header> ); يمثِّل التعبير السابق عنصر <header> في لغة HTML وبداخله عنصر <h1>، إذ تخبر الأقواس المعقوصة حول subject في السطر الرابع التطبيق بقراءة قيمة الثابت subject وإدخاله في العنصر <h1>، في حين ستُصرَّف صيغة JSX من جزء الشيفرة السابق عند استخدامها مع إطار عمل React إلى ما يلي: var subject = "World"; var header = React.createElement("header", null, React.createElement("h1", null, "Hello, ", subject, "!") ); سينتج جزء الشيفرة السابق ما يلي في لغة HTML عندما يصيّره المتصفح في النهاية: <header> <h1>Hello, World!</h1> </header> لغة Handlebars لا تُعَدّ لغة القوالب Handlebars لغةً خاصةً بتطبيقات Ember، ولكنها تُستخدَم بكثرة معها، إذ تشبه شيفرة Handlebars لغة HTML، ولكن لديها خيار سحب البيانات من مكان آخر، إذ يمكن استخدام هذه البيانات للتأثير على ملفات HTML التي يبنيها التطبيق في النهاية، كما تستخدِم لغة Handlebars -مثل صيغة JSX- الأقواس المعقوصة لحقن قيمة متغير، ولكنها تستخدِم زوجًا مزدوجًا من الأقواس المعقوصة بدلًا من زوج واحد، وإليك قالب Handlebars التالي: <header> <h1>Hello, {{subject}}!</h1> </header> والبيانات التالية: { subject: "World" } ستبني لغة Handlebars جزء HTML التالي: <header> <h1>Hello, World!</h1> </header> لغة TypeScript تُعَدّ لغة TypeScript مجموعةً شاملةً من جافاسكربت، أي أنها توسّعها، إذ تُعَدّ كل شيفرات جافاسكربت صالحةً للغة TypeScript، ولكن العكس ليس صحيحًا، كما تُعَدّ لغة TypeScript مفيدةً للصرامة التي تسمح للمطورين بفرضها على شيفرتهم البرمجية مثل دالة add()‎ التي تأخذ الأعداد الصحيحة a وb وتعيد ناتج جمعهما، ويمكن كتابة هذه الدالة في لغة جافاسكربت على النحو التالي: function add(a, b) { return a + b; } قد تكون هذه الشيفرة بسيطةً جدًا بالنسبة لشخص اعتاد على استخدام جافاسكربت، ولكنها يمكن أن تكون أوضح، إذ تتيح لنا لغة جافاسكربت استخدام المعامل + لربط السلاسل مع بعضها بعضًا، لذلك ستعمل هذه الدالة أيضًا إذا كان a وb عبارة عن سلاسل نصية Strings، وبالتالي قد لا تمنحك النتيجة التي تتوقعها، فإذا أردنا السماح فقط بتمرير الأعداد إلى هذه الدالة، فستجعل لغة TypeScript ذلك ممكنًا كما يلي: function add(a: number, b: number) { return a + b; } يخبر النوع ‎: number المكتوب بعد كل معامِل في لغة TypeScript أنّ كلا المعامِلَين a وb يجب أن يكونا عددَين، فإذا أردنا استخدام هذه الدالة وتمرير القيمة '2' إليها بوصفها وسيطًا، فستعطي لغة TypeScript خطأً أثناء التصريف Compilation، وبالتالي سنضطر إلى إصلاح هذا الخطأ، كما يمكننا كتابة شيفرة جافاسكربت الخاصة بنا والتي تعطينا هذه الأخطاء، إلا أنها ستجعل شيفرتنا البرمجية أكثر تفصيلًا، إذ يمكن أن يكون السماح للغة TypeScript بمعالجة مثل هذه الفحوصات نيابةً عنا أمرًا منطقيًا. كتابة المكونات تحتوي معظم أطر العمل على نموذج مكونات، إذ يمكن كتابة مكونات React باستخدام صيغة JSX، ومكونات Ember باستخدام لغة Handlebars، ومكونات Angular وVue باستخدام صيغة القوالب التي توسّع لغة HTML قليلًا، كما توفِّر مكونات كل إطار عمل -بغض النظر عن كيفية كتابة المكونات- طريقةً لوصف الخاصيات الخارجية التي قد تحتاجها، والحالة الداخلية التي يجب أن يديرها المكوِّن، والأحداث التي يمكن للمستخدِم أن أن يتفاعل بها مع المكوِّن، كما سنعطي في هذا المقال أمثلةً من مقتطفات شيفرة React والتي ستُكتَب باستخدام صيغة JSX. الخاصيات Properties تُعَدّ الخاصيات Properties -أو props اختصارًا- بيانات خارجية يحتاجها المكوِّن من أجل تصييرها Render، ولنفترض أنك تنشئ موقعًا إلكترونيًا لمجلة على الإنترنت، وتحتاج إلى التأكُّد من أن كل كاتب مساهم يُنسَب له عمله، فيمكنك إنشاء مكوِّن AuthorCredit لكل مقال، إذ يحتاج هذا المكوِّن إلى عرض صورة شخصية للمؤلف وسطر قصير يحتوي على بعض المعلومات عنه، لذلك يحتاج المكوِّن AuthorCredit إلى قبول بعض الخاصيات من أجل معرفة الصورة المراد تصييرها والسطر القصير المطلوب طباعته، إذ يمكن أن يبدو تمثيل React للمكوِّن AuthorCredit كما يلي: function AuthorCredit(props) { return ( <figure> <img src={props.src} alt={props.alt} /> <figcaption>{props.byline}</figcaption> </figure> ); } تمثِّل {props.src} و{props.alt} و{props.byline} المكان الذي ستُدرَج فيه الخاصيات ضمن المكوِّن، إذ يمكن تصيير هذا المكوِّن من خلال كتابة الشيفرة التالية في المكان الذي نريده، والذي سيكون على الأرجح ضمن مكوِّن آخر: <AuthorCredit src="./assets/zelda.png" alt="Portrait of Zelda Schiff" byline="Zelda Schiff is editor-in-chief of the Library Times." /> مما يؤدي في النهاية إلى تصيير عنصر <figure> التالي في المتصفح مع بنيته المحدَّدة في المكوِّن AuthorCredit، ومحتواه المحدَّد في الخاصيات المدرجة في استدعاء المكوِّن AuthorCredit: <figure> <img src="assets/zelda.png" alt="Portrait of Zelda Schiff" > <figcaption> Zelda Schiff is editor-in-chief of the Library Times. </figcaption> </figure> الحالة State يُعَدّ وجود آلية قوية للتعامل مع الحالة مفتاحًا لإطار عمل فعّال، وقد يحتوي كل مكوِّن على بيانات يجب التحكم بحالتها، إذ ستستمر هذه الحالة بطريقة ما طالما أنّ المكوِّن قيد الاستخدام، ويمكن استخدام الحالة مثل الخاصيات للتأثير على كيفية تصيير المكوِّن، ولنفترض مثلًا وجود زر يحسب عدد مرات النقر فوقه، إذ يجب أن يكون هذا المكوِّن مسؤولًا عن تتبّع حالة العد count الخاصة به، ويمكن كتابته كما يلي: function CounterButton() { const [count] = useState(0); return ( <button>Clicked {count} times</button> ); } يُعَدّ useState()‎ خطاف React الذي سيتتبع قيمة بيانات أولية أثناء تحديثها عند إعطائه تلك القيمة، وستُصيَّر الشيفرة بدايةً كما يلي في المتصفح: <button>Clicked 0 times</button> يتتبّع استدعاء الخطاف useState()‎ الحالة count بطريقة قوية عبر التطبيق دون الحاجة إلى كتابة شيفرة لتنفيذ ذلك بنفسك. الأحداث Events تحتاج المكونات إلى طرق للاستجابة لأحداث المتصفح من أجل أن تكون تفاعلية، وبالتالي ستتمكن تطبيقاتنا من الاستجابة للمستخدِمين، إذ يوفّر كل إطار من أطر العمل صيغته الخاصة للاستماع إلى أحداث المتصفح، والتي تشير إلى أسماء أحداث المتصفح الأصيلة المكافِئة، إذ يتطلب الاستماع إلى حدث النقر click خاصيةً خاصةً هي onClick في React، ولنحدّث شيفرة CounterButton السابقة للسماح لها بحساب عدد النقرات كما يلي: function CounterButton() { const [count, setCount] = useState(0); return ( <button onClick={() => setCount(count + 1)}>Clicked {count} times</button> ); } استخدمنا دالة useState()‎ إضافية لإنشاء دالة setCount()‎ خاصة يمكن استدعاؤها لتحديث قيمة count، إذ نستدعي هذه الدالة في السطر الرابع، ونضبط قيمة count على قيمتها الحالية مع إضافة 1 إليها. مكونات التنسيق Styling components يوفِّر كل إطار من أطر العمل طريقةً لتحديد تنسيقات لمكوناتك أو للتطبيق كله، إذ توفِّر جميعها طرقًا متعددةً لتعريف تنسيقات المكوِّن على الرغم من اختلاف نهج كل إطار عن الآخر، ويمكنك تصميم تطبيقات إطار العمل باستخدام Sass أو Less، أو تحويل Transpile ملفات تنسيقات CSS باستخدام PostCSS مع إضافة بعض الوحدات المساعِدة. التعامل مع الاعتماديات توفِّر جميع الأطر الرئيسية آليات للتعامل مع الاعتماديات Dependencies باستخدام مكوِّنات ضمن مكوّنات أخرى وبمستويات هرمية متعددة في بعض الأحيان، وستختلف آليات هذه الإطارات عن بعضها بعضًا، ولكن النتيجة النهائية هي نفسها كما هو الحال مع الميزات الأخرى، كما تميل المكوّنات إلى استيراد مكوّنات في مكوّنات أخرى باستخدام صيغة وحدة جافاسكربت المعيارية أو شيء آخر مشابه. مكونات ضمن مكونات أخرى تتمثَّل إحدى الفوائد الرئيسية لبنية واجهة المستخدِم القائمة على المكوّنات في أنه يمكن تكوين المكوّنات مع بعضها بعضًا، إذ يمكنك استخدام مكونات ضمن مكونات أخرى لبناء تطبيق ويب مثل كتابة وسوم HTML ضمن بعضها بعضًا لإنشاء موقع ويب، كما يتيح لك كل إطار عمل بكتابة مكوّنات تستخدِم وتعتمد على مكوّنات أخرى، كما يمكن استخدام مكوِّن React الذي هو AuthorCredit ضمن المكوِّن Article مثلًا، وهذا يعني حاجة المكوِّن Article إلى استيراد المكوِّن AuthorCredit. import AuthorCredit from "./components/AuthorCredit"; يمكن بعد ذلك استخدام المكوِّن AuthorCredit ضمن المكوِّن Article كما يلي: ... <AuthorCredit /> … حقن الاعتماديات تشتمل التطبيقات الواقعية على بنى مكونات ذات مستويات متعددة من التداخل Nesting في أغلب الأحيان، وقد يحتاج مكوِّن AuthorCredit المتداخل بعمق في العديد من المستويات لسبب ما إلى بيانات من المستوى الجذر لتطبيقنا، ولنفترض تنظيم موقع المجلة الذي نبنيه على النحو التالي: <App> <Home> <Article> <AuthorCredit {/* props */} /> </Article> </Home> </App> يحتوي المكوِّن App على البيانات التي يحتاجها المكوِّن AuthorCredit، كما يمكننا إعادة كتابة المكوِّن Home وArticle لمعرفة كيفية تمرير الخاصيات، ولكن قد يكون ذلك مملًا إذا كان هناك العديد من المستويات بين أصل ووجهة البيانات، كما قد يؤثر ذلك على الأداء، فلا يستخدِم المكوِّنان Home وArticle صورة المؤلف أو السطر القصير الذي يعطي معلومات مختصَرةً عن المؤلف، ولكن إذا أردنا الحصول على هذه المعلومات في المكوِّن AuthorCredit، فينحتاج إلى تغيير المكوِّنَين Home وArticle لإضافتها. تُسمَّى مشكلة تمرير البيانات عبر العديد من طبقات المكوّنات بتمرير الخاصيات Prop Drilling التي لا تُعَدّ مثاليةً للتطبيقات الكبيرة، إذ يمكن التحايل على هذه المشكلة من خلال توفير أطر العمل وظيفة تُعرَف باسم حقن الاعتمادية Dependency Injection، وهي طريقة لإعطاء بيانات معينة مباشرةً إلى المكوّنات التي تحتاجها دون تمريرها عبر المستويات المتداخلة، إذ ينفّذ كل إطار عمل عملية حقن الاعتمادية تحت اسم مختلف وبطريقة مختلفة، ولكن التأثير هو نفسه في النهاية. يسمّي إطار العمل Angular هذه العملية حقن الاعتمادية، في حين يمتلك إطار العمل Vue توابع المكوّنات provide()‎ وinject()‎؛ أما React، فيحتوي على واجهة برمجة تطبيقات السياق Context API، بينما يشارك إطار عمل Ember الحالة من خلال خدمات. دورة الحياة Life Cycle تُعَدّ دورة حياة المكوّن في سياق إطار العمل مجموعةً من المراحل التي يمر بها المكوِّن من وقت إلحاقه بنموذج DOM ثم تصييره بواسطة المتصفح -والذي يدعى بالتركيب Mounting في أغلب الأحيان- إلى وقت إزالته من نموذج DOM -والذي يطلَق عليه التفكيك Unmounting في أغلب الأحيان، كما يسمّي كل إطار عمل مراحل دورة الحياة هذه بطريقة مختلفة، ولا تمنح جميعها المطورين الوصول إلى المراحل نفسها، كما تتبع جميع الأطر النموذج العام نفسه، إذ تسمح للمطورين بتنفيذ إجراءات معينة عند تركيب Mount المكوِّن، وعند تصييره، وعند تفكيكه Unmount، وفي عدة مراحل بينها. تُعَدّ مرحلة التصيير Render المرحلة الأهم، لأنها تتكرر عندما يتفاعل المستخدِم مع تطبيقك، وتُشغَّل في كل مرة يحتاج فيها المتصفح إلى تصيير شيء جديد، سواءً كانت هذه المعلومات الجديدة إضافةً إلى ما هو موجود في المتصفح أو حذفه أو تعديله، كما يمكنك الاطلاع على هذا الرسم البياني لدورة حياة مكون React الذي يوضِّح هذا المفهوم. تصيير العناصر تتخِذ أطر العمل أساليبًا مختلفةً ولكنها متشابهة لتصيير التطبيقات، وتتعقّب جميعها الإصدار الحالي المُصيَّر من DOM في متصفحك، كما تتخذ كل منها قرارات مختلفةً قليلًا حول كيفية تغيير نموذج DOM مثل إعادة تصيير المكوّنات في تطبيقك، وبما أنّ أطر العمل تتخِذ هذه القرارات نيابةً عنك، فهذا يعني أنك لا تتفاعل مع نموذج DOM بنفسك، كما يُعَدّ هذا التجريد البعيد عن نموذج DOM أكثر تعقيدًا واستهلاكًا للذاكرة من تحديثه بنفسك، ولكن لا يمكن لأطر العمل بدونه السماح لك بالبرمجة بالطريقة التصريحية المعروفة بها. يُعَدّ نموذج DOM الافتراضي Virtual DOM نهجًا يمكن من خلاله تخزين معلومات حول نموذج DOM في متصفحك ضمن ذاكرة جافاسكربت، إذ يحدّث تطبيقك هذه النسخة من DOM، ثم يوازنها مع DOM الحقيقي المصيَّر لمستخدِميك فعليًا لتحديد ما سيُصيَّر، كما ينشئ التطبيق اختلافًا Diff لموازنة الاختلافات بين DOM الافتراضي المُحدَّث و DOM المُصيَّر حاليًا، إذ يُستخدَم هذا الاختلاف لتطبيق التحديثات على نموذج DOM الحقيقي، كما يستخدِم كل من React وVue نموذج DOM الافتراضي، لكنهما لا يطبِّقان المنطق نفسه بالضبط عند تطبيق الاختلاف Diffing أو التصيير Rendering، ويمكنك قراءة المزيد عن DOM الافتراضي في توثيق React على موسوعة حسوب. يشبه نموذج DOM التزايدي Incremental DOM نموذج DOM الافتراضي Virtual DOM في أنه ينشئ اختلافًا في نموذج DOM لتحديد ما سيُصيَّر، إلا أنه يختلف في عدم إنشائه نسخةً كاملةً من DOM في ذاكرة جافاسكربت، وهو يتجاهل أجزاء DOM التي لا تحتاج إلى تغيير، فإطار العمل Angular هو الإطار الوحيد الذي ناقشناه حتى الآن والذي يستخدِم نموذج DOM التزايدي، كما يمكنك قراءة المزيد حول نموذج DOM التزايدي على مدونة Auth0. آلة Glimmer الافتراضية خاصة بإطار عمل Ember، ولا تُعَدّ نموذج DOM افتراضي أو DOM تزايدي، وإنما هي عملية منفصلة يمكن من خلالها تحويل قوالب Ember إلى نوع من شيفرة ثنائية Byte Code تكون أسهل وأسرع في القراءة من جافاسكربت. التوجيه Routing يُعَدّ التوجيه جزءًا مهمًا من تجربة الويب، إذ يوفِّر كل إطار عمل مكتبةً أو أكثر بحيث تساعد المطورين على تنفيذ التوجيه من جانب العميل في تطبيقاتهم، لتجنّب تجربة معطَّلة في التطبيقات المعقدة ذات المشاهدات الكثيرة. الاختبار Testing تستفيد جميع التطبيقات من تغطية الاختبار التي تضمن استمرار برنامجك في التصرف بالطريقة التي تتوقعها، إذ يوفر النظام المجتمعي لكل إطار عمل الأدوات التي تسهّل كتابة الاختبارات، ولا تُضمَّن أدوات الاختبار في الأطر نفسها، ولكن تمنحك أدوات واجهة سطر الأوامر المُستخدَمة لإنشاء تطبيقات إطار العمل، الوصول إلى أدوات الاختبار المناسبة، إذ يحتوي كل إطار على أدوات واسعة النطاق في نظامه المجتمعي مع إمكانات اختبار الوحدة والتكامل على حد سواء. تُعَدّ مكتبة الاختبار Testing Library مجموعةً من أدوات الاختبار المساعِدة التي تحتوي على أدوات للعديد من بيئات جافاسكربت بما في ذلك React وVue وAngular، ويغطي توثيق Ember اختبار تطبيقاته، وإليك اختبار سريع للمكوِن CounterButton مكتوب بمساعَدة مكتبة اختبار React، إذ يختبر هذا الاختبار عددًا من الأشياء مثل وجود الزر وما إذا كان الزر يعرض النص الصحيح بعد النقر عليه 0 و1 و2 مرة: import React from "react"; import { render, fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import CounterButton from "./CounterButton"; it("renders a semantic with an initial state of 0", () => { const { getByRole } = render(<CounterButton />); const btn = getByRole("button"); expect(btn).toBeInTheDocument(); expect(btn).toHaveTextContent("Clicked 0 times"); }); it("Increments the count when clicked", () => { const { getByRole } = render(<CounterButton />); const btn = getByRole("button"); fireEvent.click(btn); expect(btn).toHaveTextContent("Clicked 1 times"); fireEvent.click(btn); expect(btn).toHaveTextContent("Clicked 2 times"); }); الخلاصة يجب أن يكون لديك الآن مزيدًا من الأفكار حول اللغات والميزات والأدوات الفعلية التي ستستخدِمها أثناء إنشاء التطبيقات باستخدام أطر العمل، ولا بدّ أنك متحمس للبدء بكتابة الشيفرة، وهذا ما ستفعله لاحقًا، ولكن يمكنك الآن اختيار إطار العمل الذي ترغب في بدء تعلمه أولًا مثل: React Ember Vue Svelte Angular ترجمة -وبتصرُّف- للمقال Framework main features. اقرأ أيضًا مقدمة إلى أطر عمل تطوير الويب من طرف العميل فهم أدوات تطوير الويب من طرف العميل بناء نموذج كامل لسلسلة أدوات تطوير الويب من طرف العميل
  25. سنبدأ مقالنا بإلقاء نظرة على تاريخ لغة جافاسكربت JavaScript وأطر العمل Frameworks، وسبب وجود هذه الأطر وفوائدها، وكيفية اختيار إطار عمل، وما هي البدائل المتاحة لأطر العمل من طرف العميل. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML و CSS و جافاسكربت. الهدف: فهم أطر عمل جافاسكربت من طرف العميل والمشاكل التي تحلها وبدائلها وكيفية اختيارها. لمحة تاريخية ظهرت لغة جافاسكربت لأول مرة في عام 1996 والتي أضافت تفاعلًا إلى الويب الذي احتوى على مستندات ثابتة سابقًا، وبالتالي أصبح الويب مكانًا لفعل أشياء، وليس مجرد مكان لقراءة تلك الأشياء، كما زادت شعبية جافاسكربت، كما كتب المطورون الذين عملوا معها، أدوات لحل المشاكل التي واجهوها، وجمّعوها في حزم قابلة لإعادة الاستخدام سميّت بمكتبات Libraries ليتمكنوا من مشاركة حلولهم مع بعضهم البعض، إذ ساعد هذا النظام المجتمعي المشترك للمكتبات في تشكيل نمو الويب. تُعَدّ لغة جافاسكربت الآن جزءًا أساسيًا من الويب، وتُستخدَم في 95% من مواقع الويب، كما أصبح المستخدِمون يكتبون أوراقًا papers، ويديرون ميزانياتهم، ويستمعون إلى الموسيقى، ويشاهدون الأفلام، ويتواصلون مع بعضهم البعض عن بعد فوريًا من خلال الدردشة النصية أو الصوتية أو المرئية، وبالتالي أتاح الويب تنفيذ أمور كانت ممكنةً سابقًا فقط في التطبيقات الأصيلة المثبَّتة على حواسيبنا، إذ يُشار إلى هذه المواقع الحديثة والمعقَّدة والتفاعلية باسم تطبيقات الويب Web Applications. أدى ظهور أطر عمل جافاسكربت الحديثة إلى تسهيل إنشاء تطبيقات تفاعلية وديناميكية، فإطار العمل Framework هو عبارة عن مكتبة تقدّم آراءً لبناء البرمجيات، إذ تسمح هذه الآراء بإمكانية التنبؤ والتجانس في التطبيق، فالقدرة على التنبؤ تسمح للبرمجيات بالتوسع إلى حجم هائل مع بقائها قابلةً للصيانة، إذ تُعَدّ القدرة على التنبؤ وقابلية الصيانة أمران ضروريان لصحة البرمجيات وطول عمرها. تعمل أطر عمل جافاسكربت على تشغيل الكثير من البرمجيات الرائعة على الويب الحديث، بما في ذلك العديد من مواقع الويب التي يُحتمَل استخدامها كل يوم، إذ تستخدِم صفحة توثيق الويب في MDN مثلًا إطار عمل React/ReactDOM لتشغيل واجهتها الأمامية، وتوجد هناك أطر عمل متعددة، ولكن "الأربعة الكبار" هي: إمبر Ember أُصدِر إطار عمل Ember في ديسمبر كانون الأول عام 2011 على أساس استمرار للعمل الذي بدأ في مشروع SproutCore، ويُعَدّ Ember إطار عمل قديم به عدد مستخدِمين أقل من البدائل الحديثة مثل React وVue، لكنه لا يزال يتمتع بقدر لا بأس به من الشعبية نظرًا لاستقراره ودعم المجتمع وبعض مبادئ البرمجة الذكية. Angular هو إطار عمل لتطبيق ويب مفتوح المصدر بقيادة فريق أنجولار Angular في جوجل ومجتمع من الأفراد والشركات، إذ نتج Angular عن إعادة كتابة AngularJS بالكامل من الفريق نفسه الذي بناه وقد أُصدِر أنجولار رسميًا في 14 سبتمبر أيلول من عام 2016، وهي إطار عمل قائم على المكونات، وتستخدِم قوالب HTML التصريحية Declarative، كما يترجم مصرّف إطار العمل القوالب إلى تعليمات جافاسكربت محسَّنة في وقت البناء وبشفافية عن المطورين، تستخدِم أنجولار لغة TypeScript، وهي مجموعة شاملة من لغة جافاسكربت التي سنلقي نظرةً عليها بمزيد من التفصيل لاحقًا. Vue أصدَر إيفان يو Evan You لأول مرة إطار عمل Vue في عام 2014 بعد العمل والتعلم من مشروع AngularJS الأصلي، إذ يُعَدّ Vue الأصغر بين الأربعة الكبار، لكنه تمتَّع مؤخرًا بشعبية متزايدة، كما يوسّع إطار عمل Vue مثل AngularJS لغة HTML بشيفرته، وهو يعتمد بصورة أساسية على لغة جافاسكربت المعيارية الحديثة. React أصدَرت شركة فيسبوك مكتبة React في عام 2013، إذ استخدِمت React قبل ذلك في حل العديد من مشاكلها داخليًا، ولا تُعَدّ React نفسها إطار عمل، وإنما مكتبةً لتصيير Rendering مكونات واجهة المستخدِم، كما تُستخدَم React جنبًا إلى جنب مع المكتبات الأخرى لإنشاء التطبيقات، إذ تُمكِّن React وReact Native المطورين من إنشاء تطبيقات للهاتف المحمول، في حين تمكِّن React وReactDOM المطورين من إنشاء تطبيقات الويب، وتُعرَف React بوصفها إطار عمل جافاسكربت نظرًا لاستخدام React وReactDOM معًا في كثير من الأحيان، كما توسّع React لغة جافاسكربت بصيغة تشبه لغة HTML، إذ تُعرَف هذه الصيغة باسم JSX. سبب وجود أطر العمل ناقشنا البيئة التي ألهمت إنشاء أطر العمل، ولكننا لم نناقش السبب الحقيقي لحاجة المطورين إلى إنشائها، إذ يتطلب استكشاف الأسباب فحص تحديات تطوير البرمجيات أولًا. ضع في الحسبان نوعًا شائعًا من التطبيقات، وهو مُنشئ قائمة المهام To-do List Creator التي سننفِّذها باستخدام مجموعة متنوعة من أطر العمل لاحقًا، إذ يجب أن يسمح هذا التطبيق للمستخدِمين بتطبيق بعض الأمور مثل عرض قائمة المهام وإضافة مهمة جديدة وحذف مهمة، كما يجب أن ينفّذ ذلك أثناء تتبّع وتحديث بيانات التطبيق الأساسية بصورة موثوقة، إذ تُعرَف هذه البيانات الأساسية بالحالة State في تطوير البرمجيات. هذه الأهداف بسيطة من الناحية النظرية بمعزل عن أهداف الأخرى، إذ يمكننا تكرار البيانات لتصييرها، كما يمكننا إضافة كائن لعمل مهمة جديدة، واستخدام معرِّف Identifier للعثور على مهمة أو تعديلها أو حذفها، إذ يجب أن يسمح التطبيق للمستخدِم بتطبيق كل هذه الأشياء من خلال المتصفح، ولكن قد تبدأ بعض المشاكل في الظهور، إلا أنّ المشكلة الحقيقية هي الحاجة إلى تحديث واجهة المستخدِم المناسبة في كل مرة نغيّر فيها حالة تطبيقنا، إذ يمكننا فحص صعوبة هذه المشكلة من خلال النظر إلى ميزة واحدة فقط من تطبيق قائمة مهام وهي عرض قائمة المهام. تغيرات DOM المطولة يستغرق إنشاء عناصر HTML وتصييرها في المتصفح في الوقت المناسب قدرًا كبيرًا من الشيفرة، ولنفترض أنّ حالتنا هي مصفوفة من الكائنات كما يلي: const state = [ { id: 'todo-0', name: 'Learn some frameworks!' } ] يمكنك التساؤل عن كيفية عرض إحدى هذه المهام للمستخدِم، إذ نريد تمثيل كل مهمة بعنصر قائمة، أي العنصر <li> في لغة HTML ضمن عنصر القائمة غير المرتبة <ul> كما يلي: function buildTodoItemEl(id, name) { const item = document.createElement('li'); const span = document.createElement('span'); const textContent = document.createTextNode(name); span.appendChild(textContent); item.id = id; item.appendChild(span); item.appendChild(buildDeleteButtonEl(id)); return item; } استخدمنا التابع document.createElement()‎ لإنشاء العنصر <li> والعديد من أسطر الشيفرة لإنشاء الخصائص والعناصر الأبناء التي يحتاجها، في حين يشير جزء الشيفرة التالي إلى دالة بناء أخرى هي buildDeleteButtonEl()‎، والتي تتبع نمطًا مشابهًا للنمط الذي استخدمناه لبناء عنصر القائمة: function buildDeleteButtonEl(id) { const button = document.createElement('button'); const textContent = document.createTextNode('Delete'); button.setAttribute('type', 'button'); button.appendChild(textContent); return button; } لا ينفِّذ هذا الزر أيّ شيء حتى الآن، ولكنه سينفِّذ شيئًا ما لاحقًا عندما نقرِّر تنفيذ ميزة الحذف، كما يمكن أن تقرأ الشيفرة التي ستصيّر العناصر على الصفحة شيئًا كما يلي: function renderTodoList() { const frag = document.createDocumentFragment(); state.tasks.forEach(task => { const item = buildTodoItemEl(task.id, task.name); frag.appendChild(item); }); while (todoListEl.firstChild) { todoListEl.removeChild(todoListEl.firstChild); } todoListEl.appendChild(frag); } لدينا الآن أكثر من ثلاثين سطرًا من الشيفرة المخصَّصة لواجهة المستخدِم فقط -أي إلى خطوة تصيير شيء ما في DOM- دون إضافة أصناف Classes التي يمكننا استخدامها لاحقًا لتصميم عناصر القائمة، كما يتطلب العمل مباشرةً مع نموذج DOM فهم أشياء كثيرة حول كيفية عمله مثل كيفية إنشاء العناصر، وتغيير خصائصها، وكيفية وضع العناصر ضمن بعضها البعض، والحصول عليها على الصفحة، فلا تعالج هذه الشيفرة تفاعلات المستخدِم أو إضافة مهمة أو حذفها، فإذا أضفنا هذه الميزات، فيجب علينا تذكّر تحديث واجهة المستخدِم في الوقت المناسب وبالطريقة الصحيحة. أُنشِئت أطر عمل جافاسكربت لتسهيل هذا النوع من العمل، إذ أُوجِدت لتوفير تجربة مطوِّر أفضل، فهي لا تضيف ميزات جديدة إلى جافاسكربت، وإنما تمنحك وصولًا أسهل لميزاتها لتتمكّن من بناء تطبيقات ويب بطريقة عصرية، فإذا أردت رؤية نماذج شيفرة هذا المقال عمليًا، فيمكنك التحقق من إصدار عامل من التطبيق على CodePen الذي يسمح للمستخدِمين بإضافة مهام جديدة وحذفها. طريقة أخرى لبناء واجهات المستخدم توفّر إطارات عمل جافاسكربت طريقةً لكتابة واجهات المستخدِم بطريقة تصريحية، أي أنها تسمح بكتابة الشيفرة التي توضِّح كيف يجب أن تبدو واجهة المستخدِم، كما يحقّق إطار العمل ذلك ضمن نموذج DOM في الخلفية، وكان فهم منهج جافاسكربت الصرفة Vanilla JavaScript لإنشاء عناصر DOM جديدة بطريقة تكرارية في لمح البصر أمرًا صعبًا، لكن يوضِّح الجزء التالي من الشيفرة الطريقة التي يمكنك من خلالها استخدام إطار عمل Vue لوصف قائمة من المهام: <ul> <li v-for="task in tasks" v-bind:key="task.id"> <span>{{task.name}}</span> <button type="button">Delete</button> </li> </ul> يختصر جزء الشيفرة السابق ما يقرب من اثنين وثلاثين سطرًا من الشيفرة في ستة أسطر، فإذا كانت الأقواس المعقوصة وسمات v-‎ غير مألوفة لك، فلا بأس بذلك، إذ سنتعرّف على الصيغة التي يستخدِمها إطار عمل Vue لاحقًا، كما تشبه الشيفرة السابقة واجهة المستخدِم التي تمثِّلها، في حين لا تشبه شيفرة جافاسكربت الصرفة ذلك. يمكن عدم كتابة دوالنا لبناء واجهة المستخدم بفضل إطار عمل Vue الذي سيتعامل مع ذلك بطريقة مثلى وفعالة، فدورنا الوحيد هو شرحنا لإطار عمل Vue الشكل الذي يجب أن يبدو عليه كل عنصر، كما يمكن للمطورين الذين هم على دراية بإطار عمل Vue الانضمام إلى مشروعنا والعمل بسرعة على ما يجري، فاستعمال إطار العمل Vue -وأي إطار عمل آخر- يحسّن كفاءة الفريق وأعضائه. يمكن تطبيق أشياء مشابهة في لغة جافاسكربت الصرفة، إذ تسهّل سلاسل القالب الحرفية Template literal strings كتابة سلاسل HTML التي تمثِّل الصورة التي سيبدو عليها العنصر الأخير، وقد يكون ذلك فكرةً مفيدةً لشيء بسيط مثل تطبيق قائمة المهام، ولكنه ليس قابلًا للصيانة للتطبيقات الكبيرة التي تتعامل مع آلاف سجلات البيانات، كما يمكن تصيير العديد من العناصر الفريدة في واجهة المستخدِم. فوائد أطر العمل الأخرى لنلقِ نظرةً على بعض المزايا الأخرى التي تمنحنا إياها أطر العمل، كما يمكن تحقيق مزايا الأطر في لغة جافاسكربت الصرفة، ولكن يزيح استخدام إطار العمل عبء ضرورة حل هذه المشاكل بأنفسنا. الأدوات يمتلك كل إطار من أطر العمل مجتمع مطورين كبير ونشط، لذلك يوفِّر النظام المجتمعي لكل إطار الأدوات التي تعمل على تحسين تجربة المطوِّر، إذ تسهّل هذه الأدوات إضافة أشياء مثل الاختبار للتأكد من عمل تطبيقك كما ينبغي، أو تدقيق الصياغة Linting للتأكد من خلو شيفرتك البرمجية من الأخطاء متجانسها من حيث الأسلوب. التجزئة Compartmentalization تشجع معظم الأطر الرئيسية المطورين على تجريد الأجزاء المختلفة من واجهات المستخدِم إلى مكونات Components، إذ تُعَدّ هذه المكونات أجزاءً من شيفرة برمجية قابلة للصيانة وإعادة الاستخدام ويمكنها التواصل مع بعضها بعضًا، كما يمكن وضْع الشيفرة المتعلقة بمكوّن معيّن في ملف واحد أو ملفين محدَّدين، بحيث يعرف المطوِّر بالضبط إلى أين يذهب لإجراء تغييرات على هذا المكوّن، في حين سيتعيّن عليك في تطبيق مكتوب بلغة جافاسكربت الصرفة إنشاء مجموعة اصطلاحات لتحقيق ذلك بطريقة فعالة وقابلة للتوسيع، كما يمكن انتهاء المطاف بالعديد من مطوري جافاسكربت بنشر الشيفرة البرمجية المتعلقة بجزء واحد من واجهة المستخدِم في جميع أنحاء الملف أو في ملف آخر تمامًا. التوجيه Routing يتيح الويب للمستخدمين التنقل من صفحة إلى أخرى، إذ يُعَدّ شبكةً من الوثائق المترابطة، فإذا ضغطتَ على رابط في موقع الويب هذا، فسيتصل متصفحك بخادم ما ويجلب محتوًى جديدًا لعرضه لك، وبالتالي سيتغير عنوان URL في شريط العنوان، ويمكنك حفظ عنوان URL الجديد والعودة إلى الصفحة لاحقًا، أو مشاركته مع الآخرين ليتمكنوا من العثور على الصفحة نفسها بسهولة، كما يتذكر متصفحك سجل التنقل ويسمح لك بالتنقل ذهابًا وإيابًا، وهذا ما يسمى بالتوجيه من طرف الخادم Server Side Routing. لا تجلب تطبيقات الويب الحديثة عادةً ملفات HTML الجديدة لتصييرها، وإنما تحمّل صفحة HTML واحدةً وتحدِّث نموذج DOM ضمنها باستمرار -ويشار إليها باسم تطبيقات الصفحة الواحدة Single Page Apps أو SPAs اختصارًا- دون انتقال المستخدِمين إلى عناوين جديدة على الويب، كما يطلَق عادةً على كل صفحة ويب وهمية Pseudo-Webpage جديدة اسم عرض View دون إجراء أيّ توجيه. إذا كان تطبيق SPA معقدًا بدرجة كافية ويصيِّر عددًا كافيًا من العروض الفريدة، فيجب إدخال وظائف التوجيه في تطبيقك، وقد اعتاد الناس على القدرة على الارتباط بصفحات معينة في التطبيقات، والانتقال ذهابًا وإيابًا في سجل التنقّل وما إلى ذلك، ولكن تتأثر تجربتهم عند تعطل ميزات الويب المعيارية هذه، فإذا تعامل تطبيق عميل مع التوجيه بهذه الطريقة، فإنه يسمى بالتوجيه من طرف العميل Client Side Routing، كما يمكن إنشاء موجّه باستخدام إمكانيات جافاسكربت والمتصفح الأصيلة، لكن الأطر الشائعة والمطوّرة بطريقة نشطة لها مكتبات مرافقة تجعل التوجيه جزءًا أسهل في عملية التطوير. أمور يجب مراعاتها عند استخدام الأطر يفضِّل مطور الويب الفعال استخدام أنسب الأدوات لكل عمل، كما تسهّل أطر عمل جافاسكربت تطوير التطبيقات الأمامية، لكنها ليست حلًا سحريًا لحل جميع المشاكل، إذ سنوضِّح فيما يلي الأمور التي يجب مراعاتها عند استخدام الأطر، وضَع في الحسبان أنك قد لا تحتاج إلى إطار عمل إطلاقًا. معرفة كيفية استخدام الأداة تستغرق الأطر وقتًا للتعلم تمامًا مثل لغة جافاسكربت الصرفة، لذلك تأكد من امتلاكك الوقت لتعلّم ما يكفي من ميزات إطار العمل قبل أن تقرر استخدامه لمشروع ما ليكون مفيدًا لك بدلًا من أن يعيقك، وتأكد من راحة زملائك في الفريق في التعامل معه أيضًا. الهندسة الفائقة Overengineering إذا كان مشروع تطوير الويب ملفًا شخصيًا يتكون من بضع صفحات، وكانت هذه الصفحات ذات قدرة تفاعلية قليلة أو معدومة، فقد لا يكون إطار العمل وجافاسكربت ضروريين إطلاقًا، إذ لا تُعَدّ أطر العمل وحدةً مترابطةً، فبعضها أكثر ملاءمةً للمشاريع الصغيرة من غيرها، إذ كتبت سارة دراسنر Sarah Drasner في مقال لمجلة Smashing Magazine عن كيفية استبدال Vue بـ jQuery بوصفها أداةً لجعل أجزاء صغيرة من صفحة ويب تفاعلية. قاعدة شيفرة أكبر وتجريد أكبر تسمح لك أطر العمل بكتابة المزيد من الشيفرة البرمجية التصريحية -وأحيانًا مقدار أقل من الشيفرة البرمجية- من خلال التعامل مع تفاعلات DOM نيابةً عنك في الخلفية، ويُعَدّ هذا التجريد رائعًا لتجربتك بوصفك مطورًا، ولكنه ليس مجانيًا، إذ يجب على أطر العمل تشغيل شيفرتها البرمجية لترجمة ما تكتبه إلى تغييرات DOM، والتي بدورها تجعل الجزء الأخير من البرنامج أكبر وأكثر تكلفةً. تُعَدّ الشيفرة البرمجية الإضافية أمرًا لا مفر منه، وسيسمح لك إطار العمل الذي يدعم تقنية هز الشجرة Tree-Shaking -أي إزالة أي شيفرة غير مُستخدَمة فعليًا في التطبيق أثناء عملية البناء- بالحفاظ على تطبيقاتك صغيرة، ولكن ذلك لا يزال عاملًا يجب وضعه في الحسبان عند التفكير في أداء تطبيقك، خاصةً على الأجهزة المقيَّدة بالشبكة أو بالتخزين مثل الهواتف المحمولة. لا يؤثِّر تجريد الأطر على شيفرة جافاسكربت فحسب، وإنما يؤثِّر على علاقتك بطبيعة الويب ذاتها، فنتيجة تطبيقك النهائية أو الطبقة التي يتفاعل معها المستخدِمون في النهاية هي HTML بغض النظر عن كيفية بنائك لتطبيق الويب، إذ يمكن أن تجعلك كتابة تطبيقك بالكامل باستخدام لغة جافاسكربت غافلًا عن HTML والغرض من وسومها المختلفة، وتقودك إلى إنتاج مستند HTML غير دلالي Un-semantic ولا يمكن الوصول إليه، إذ يمكن كتابة تطبيق هش يعتمد كليًا على جافاسكربت ولن يعمل بدونه. ليست الأطر مصدر مشاكلنا، إذ يمكن أن يكون أيّ تطبيق هشًا ومتضخمًا ولا يمكن الوصول إليه مع وجود أولويات خاطئة، ولكن يُعَدّ تضخم أطر العمل من أولويات المطورين، فإذا كانت أولوياتك هي إنشاء تطبيق ويب معقَّد، فيمكنك تنفيذ ذلك بسهولة، في حين إذا كانت أولوياتك هي عدم حماية الأداء وإمكانية الوصول، فستزيد أطر العمل من هشاشة تطبيقك وتضخمه وعدم إمكانية الوصول إليه، وقد أدّت أولويات المطور الحديثة التي ضخَّمتها أطر العمل، إلى قلب بنية الويب في مواضع مختلفة، إذ يضع الويب الآن جافاسكربت أولًا وتجربة المستخدِم أخيرًا في أغلب الأحيان بدلًا من إنشاء شبكة مستندات قوية وتعتمد على المحتوى. إمكانية الوصول على شبكة ويب مقادة بأطر العمل تتطلب إمكانية الوصول إلى واجهات المستخدِم بعض التفكير والجهد دائمًا، ويمكن أن تؤدي الأطر إلى تعقيد هذه العملية، إذ يجب استخدام واجهات برمجة تطبيقات إطار عمل متقدمة في أغلب الأحيان للوصول إلى ميزات المتصفح الأصيلة مثل مناطق ARIA الحية أو إدارة التركيز. تخلق تطبيقات إطار العمل في بعض الحالات حواجز وصول غير موجودة في المواقع التقليدية مثل التوجيه من طرف العميل كما ذكرنا سابقًا، في حين تكون للتنقل عبر الويب باستخدام التوجيه التقليدي من طرف الخادم نتائج يمكن التنبؤ بها، إذ يعرف المتصفح كيفية ضبط التركيز على الجزء العلوي من الصفحة وستعلن التقنيات المساعدة عن عنوان الصفحة، إذ تحدث هذه الأمور في كل مرة تنتقل فيها إلى صفحة جديدة. لا يحمّل متصفحك صفحات ويب جديدة باستخدام التوجيه من طرف العميل، لذلك لا يعرف أنه يجب عليه ضبط التركيز تلقائيًا أو الإعلان عن عنوان صفحة جديد، وقد كرّس مؤلفو أطر العمل وقتًا وعملًا هائلَين لكتابة شيفرة جافاسكربت التي تعيد إنشاء هذه الميزات، ولكن لم يطبّق أيّ إطار عمل ذلك بطريقة مثالية، وبذلك يجب عليك التفكير في إمكانية الوصول منذ بداية كل مشروع ويب، وضع في الحسبان أنه من المرجَّح معاناة قواعد الشيفرة المجردة التي تستخدِم الأطر من مشاكل الوصول الرئيسية إذا لم تفعل ذلك. كيفية اختيار إطار العمل يتّخذ كل إطار من أطر العمل التي ناقشناها سابقًا مناهج مختلفةً لتطوير تطبيقات الويب، إذ يتحسّن كل منها أو يتغير بانتظام، ولكل منها إيجابياته وسلبياته، كما يُعَدّ اختيار إطار العمل الصحيح عمليةً تعتمد على الفريق والمشروع، إذ يجب عليك إجراء بحث للكشف عمّا يناسب احتياجاتك، ولكننا حدّدنا بعض الأسئلة التي يمكنك طرحها من أجل البحث في خياراتك بفعالية أكبر، وهي كما يلي: ما المتصفحات التي يدعمها إطار العمل؟ ما اللغات الخاصة بالنطاق التي يستخدمها إطار العمل؟ هل يحتوي الإطار على مجتمع قوي وتوثيق جيد ودعم متاح؟ يوفِّر الجدول الموجود في هذا المقال ملخصًا سريعًا لدعم المتصفح الحالي الذي يقدِّمه كل إطار عمل، بالإضافة إلى لغات المجال المحدَّد Domain-specific Languages -أو DSLs اختصارًا- التي يمكن استخدامها، إذ تُعَدّ لغات المجال المحدَّد لغات برمجة مرتبطةً بمجالات محدَّدة من تطوير البرمجيات، كما تُعَدّ في سياق أطر العمل أنها أنواع من لغات جافاسكربت أو HTML التي تسهّل التطوير باستخدام هذا الإطار. لا يتطلب أيّ إطار من أطر العمل مطوِّرًا لاستخدام لغة DSL معينة، ولكن صُمِّمت جميعها تقريبًا مع وضع لغة DSL محدَّدة في الحسبان، إذ يعني اختيار عدم استخدام لغة DSL المفضلة لإطار العمل أنك ستفقد الميزات التي من شأنها تحسين تجربة المطوِّر، كما يجب عليك التفكير بجدية في مصفوفة الدعم ولغات DSL الخاصة بإطار العمل عند اختيارك لأيّ مشروع جديد، إذ يمكن أن يكون دعم المتصفح غير المتطابق عائقًا أمام المستخدِمين، ويمكن أن يكون دعم لغة DSL غير المناسب عائقًا أمامك وأمام زملائك في الفريق. table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } إطار العمل دعم المتصفح لغة DSL المفضلة لغات DSL المدعومة إطار العمل Angular المتصفح IE9+ لغة TypeScript لغات HTML-based وTypeScript إطار العمل React متصفح IE9+ الحديث مع تعويض نقص دعم المتصفحات Polyfill صيغة JSX صيغة JSX ولغة TypeScript إطار العمل Vue المتصفح IE9+ لغة HTML-based لغات HTML-based وJSX وPug إطار العمل Ember متصفح IE9+ الحديث في إصدار Ember رقم 2.18 لغة Handlebars لغات Handlebars وTypeScript وفي ملاحظة مهمة، تجدر الإشارة إلى أن لغات DSL التي وصفناها بأنها " لغات تستند إلى HTML أو HTML-based" لا تمتلك أسماءً رسميةً، فهي ليست لغات DSL حقيقية، ولكنها لغة HTML غير معيارية، لذا وجب الإشارة إلى هذه النقطة. قد يكون مجتمع إطار العمل المقياس الأصعب في القياس، لأن حجم المجتمع لا يرتبط مباشرةً بالأعداد التي يسهل الوصول إليها، ويمكنك التحقق من عدد نجوم مشروع جيت هاب GitHub أو التنزيلات الأسبوعية من npm للحصول على فكرة عن شعبيته، ولكن أفضل ما يمكنك فعله في بعض الأحيان هو البحث في عدد قليل من المنتديات أو التحدث إلى مطوِّرين آخرين، إذ لا يتعلق الأمر بحجم المجتمع فحسب، وإنما يتعلق بشموليته ومدى جودة التوثيق المتاح. هناك مناقشات كثيرة في جميع أنحاء الويب حول إطار العمل الأفضل، فقد اختارت مؤسسة ويكيميديا Wikimedia مؤخرًا استخدام إطار العمل Vue لواجهتها الأمامية، ونشرت طلبًا للتعليقات Request For Comments -أو RFC اختصارًا- حول اعتماد هذا الإطار، وقد استغرق إريك جاردنر Eric Gardner مؤلف RFC وقتًا لتوضيح احتياجات مشروع ويكيميديا وسبب كون بعض أطر العمل اختيارات جيدة للفريق، إذ يُعَدّ طلب التعليقات هذا مثالًا رائعًا لنوع البحث الذي يجب عليك تطبيقه بنفسك عند التخطيط لاستخدام إطار عمل للواجهة الأمامية. يُعَدّ استبيان حالة جافاسكربت مجموعةً مفيدةً من ملاحظات مطوري جافاسكربت، كما يغطّي العديد من الموضوعات المتعلقة بجافاسكربت بما في ذلك البيانات حول استخدام أطر العمل ورأي المطورين بها، وهناك حاليًا مجموعة من البيانات المتاحة على مدى عدة سنوات، مما يسمح لك بالتعرف على شعبية إطار العمل، كما وازن فريق Vue بين Vue وأطر العمل الشائعة الأخرى، إذ قد يكون هناك بعض التحيز في هذه الموازنة، لكنها تُعَدّ موردًا قيّمًا. بدائل لأطر العمل من طرف العميل إذا أردت البحث عن أدوات لتسريع عملية تطوير الويب، وعلمتَ أنّ مشروعك لن يتطلب شيفرة جافاسكربت مكثفةً من طرف العميل، فيمكنك الوصول إلى أحد الحلول القليلة لبناء الويب مثل: نظام إدارة المحتوى Content Management System. التصيير من طرف الخادم Server-side Rendering. مولّد موقع ساكن Static Site Generator. أنظمة إدارة المحتوى تُعَدّ أنظمة إدارة المحتوى Content Management Systems -أو CMSes اختصارًا- أدوات تسمح للمستخدِم بإنشاء محتوى للويب دون كتابة الشيفرة البرمجية مباشرةً، كما تُعَدً حلًا جيدًا للمشاريع الكبيرة وخاصةً المشاريع التي تتطلب مدخلات من كتّاب المحتوى الذين لديهم قدرةً محدودةً على كتابة شيفرة برمجية، أو للمبرمجين الذين يرغبون في توفير الوقت، إلا أنها تتطلب قدرًا كبيرًا من الوقت لإعدادها. يعني استخدام نظام CMS أنك تتخلى على الأقل عن قدر من التحكم في ناتج موقعك النهائي على الويب، فإذا لم يؤلِّف نظام إدارة المحتوى الذي اخترته محتوًى يمكن الوصول إليه افتراضيًا على سبيل المثال، فسيكون تحسين ذلك أمرًا صعبًا في أغلب الأحيان، وتشمل الأمثلة المستخدَمة حاليًا ووردبريس Wordpress وجوملا Joomla ودروبال Drupal. التصيير من طرف الخادم يُعَدّ التصيير من طرف الخادم Server-side Rendering -أو SSR اختصارًا- بنية تطبيقات تكون مهمة الخادم فيها تصيير تطبيق مؤلف من صفحة واحدة، وهو عكس التصيير من طرف العميل Client-side Rendering، كما يُعَدّ الطريقة الأكثر شيوعًا والأكثر مباشرةً لبناء تطبيق جافاسكربت، إذ يكون التصيير من طرف الخادم أسهل على جهاز العميل، لأنك ترسل ملف HTML المُصيَّر إليه فقط، ولكن يمكن أن يكون إعداده صعبًا بالموازنة مع التطبيق المُصيَّر من طرف العميل. تدعم جميع أطر العمل التي ذكرهانا في هذا المقال التصيير من طرف الخادم والتصيير من طرف العميل، كما يمكنك الاطلاع على Next.js لإطار العمل React وNuxt.js لإطار العمل Vue وFastBoot لإطار العمل Ember وAngular Universal لإطار العمل Angular. مولدات الموقع الساكنة تُعَدّ مولِّدات المواقع الساكنة Static Site Generators برامج تنشئ ديناميكيًا جميع صفحات الويب الخاصة بموقع متعدِّد الصفحات -بما في ذلك شيفرة جافاسكربت أو CSS ذات الصلة، بحيث يمكن نشرها في أماكن متعددة، كما يمكن أن يكون مضيف النشر فرعًا من صفحات جيت هاب أو مثيل Netlify أو أيّ خادم خاص من اختيارك مثلًا، ولهذا النهج مزايا متعددة فيما يتعلق بالأداء، فلا يبني جهاز المستخدِم الخاص بك الصفحة باستخدام جافاسكربت، فهو مكتمل فعليًا، والأمان، إذ تمتلك الصفحات الساكنة عددًا أقل من متجهات الهجوم، كما لا يزال بإمكان هذه المواقع استخدام شيفرة جافاسكربت حيثما يحتاجون إليها، لكنها لا تعتمد عليها، وتستغرق مولّدات المواقع الساكنة وقتًا لتعلّمها مثل أيّ أداة أخرى، كما يمكن أن تكون عائقًا أمام عملية التطوير. يمكن أن تحتوي المواقع الساكنة على صفحات فريدة قليلة أو كثيرة حسبما تريد، كما تمكّنك أطر العمل من كتابة تطبيقات جافاسكربت من طرف العميل بسرعة، وتتيح لك مولّدات المواقع الساكنة طريقةً لإنشاء ملفات HTML بسرعة، كما تسمح مولّدات المواقع الساكنة للمطورين بكتابة المكوّنات التي تحدِّد الأجزاء المشتركة من صفحات الويب الخاصة بك، وتكوين هذه المكونات معًا لإنشاء صفحة نهائية، إذ تسمَّى هذه المكونات ضمن سياق مولّدات الموقع الساكنة قوالبًا Templates، ويمكن أن تكون صفحات الويب التي أنشأها مولِّد المواقع الساكنة موطنًا لتطبيقات إطار العمل إذا أردت صفحةً واحدةً محدَّدةً من موقع الويب المُولَّد بطريقة ساكنة لتشغيل تطبيق React عندما يزوره المستخدِم مثلًا. مولّدات المواقع الساكنة موجودة منذ فترة طويلة، لكنها شهدت بعض التجدّد في تاريخ الويب الحديث، وتتوفر الآن منها مجموعة من الخيارات القوية مثل Hugo وJekyll وEleventy وGatsby، فإذا أردت معرفة المزيد حول مولّدات المواقع الساكنة، فراجع دليل تاتيانا ماك Tatiana Mac للمبتدئين في Eleventy، إذ تشرح في المقال الأول من السلسلة ما هو مولِّد الموقع الساكن، وكيفية ارتباطه بالوسائل الأخرى لنشر محتوى الويب. الخلاصة لم نعلّمك كتابة أيّ شيفرة برمجية حتى الآن، ولكن نأمل أننا قدّمنا لك خلفيةً مفيدةً حول سبب استخدامك لأطر العمل في المقام الأول وكيفية البدء في الاختيار، كما يبحث مقالنا القادم في أنواع محدَّدة من ميزات أطر العمل، وسبب عملها بطريقة معينة. ترجمة -وبتصرُّف- للمقال Introduction to client-side frameworks. اقرأ أيضًا دليل استخدام سطر الأوامر في عملية تطوير الويب من طرف العميل بناء نموذج كامل لسلسلة أدوات تطوير الويب من طرف العميل أساسيات إدارة الحزم في تطوير الويب من طرف العميل
×
×
  • أضف...