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

Hassan Hedr

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

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

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

  • عدد الأيام التي تصدر بها

    38

كل منشورات العضو Hassan Hedr

  1. ضمن الدورات التي توفرها الأكاديمية يوجد فريق من المدربين لمساعدتك دومًا، في حال واجهت أي مشاكل أو لديك أي استفسارات أثناء متابعتك وتطبيقك لمحتوى الدروس يمكنك إضافة استفسارك أو شرح مشكلتك في قسم التعليقات أسفل فيديو الدرس، وسيتابع المدربين معك حل المشكلة والشرح لك ومساعدتك دومًا
  2. الخاصية color تؤثر على لون النص فقط، ولا تؤثر في لون محتوى العنصر path، فلتعديل لون هذا العنصر يمكن استخدام الخاصية fill element.style.fill = 'blue'; أيضًا لاستهداف العنصر المطلوب يجب أن يكون المُعرّف id له غير مكرر لأي عنصر آخر، أي يجب إزالة العنصر div الذي يحمل نفس قيمة المعرف
  3. يمثل المخزن المؤقت buffer مساحة ما في الذاكرة RAM تحتوي على البيانات بالصيغة الثنائية binary، ويمكن لنود Node.js أن تتعامل مع هذه الذاكرة باستخدام الصنف ‎Buffer‎، حيث يمثل البيانات كسلسلة من الأعداد بطريقة مشابهة لعمل المصفوفات في جافاسكربت، إلا أن الفرق أن هذه البيانات لا يمكن التعديل على حجمها بعد إنشاء المخزن، وكثيرًا ما نتعامل مع المخازن المؤقتة عند تطوير البرامج ضمن بيئة نود دون أن نشعر، فمثلًا عند قراءة ملف ما باستخدام التابع ‎fs.readFile()‎ فسيمرر كائن من نوع مخزن مؤقت يحوي بيانات الملف الذي نحاول قراءته إلى تابع رد النداء callback أو كنتيجة للوعد Promise، وحتى عند إنشاء طلبات HTTP فالنتيجة هي مجرى stream من البيانات المخزنة مؤقتًا في مخزن مؤقت داخلي يساعد المستخدم على معالجة بيانات جواب الطلب على دفعات بدلًا من دفعة واحدة. ونستفيد من المخازن المؤقتة أيضًا عند التعامل مع البيانات الثنائية عند كتابة البرامج منخفضة المستوى مثل التي تتعامل مع إرسال واستقبال البيانات عبر الشبكة، كما توفر القدرة على التعامل مع البيانات على أخفض مستوى ممكن والتعديل عليها في الحالات التي نحتاج بها لذلك. سنتعرف في هذا الفصل على المخازن المؤقتة وطريقة إنشائها والقراءة والنسخ منها والكتابة إليها، وحتى تحويل البيانات الثنائية ضمنها إلى صيغ ترميز أخرى. المستلزمات هذا الفصل جزء من سلسلة دليل تعلم Node.js لذا يجب قبل قراءته: تثبيت بيئة Node.js على الجهاز، حيث استخدمنا في هذا المقال الإصدار رقم 10.19.0. معرفة التعامل مع حلقة REPL في نود، يمكنك الاطلاع على المقال الثاني من هذه السلسلة للتعرف أكثر على طريقة استخدام هذا الوضع. معرفة بأساسيات جافاسكربت وأنواع البيانات المتوفرة ضمن اللغة. إنشاء المخزن المؤقت سنتعرف في هذه الفقرة على طريقتين لإنشاء كائن التخزين المؤقت في نود، حيث يجب يجب أن نسأل أنفسنا دومًا في ما إذا كنا نريد إنشاء مخزن مؤقت جديد، أو استخراج مخزن مؤقت من بيانات موجودة مسبقًا، وعلى أساس ذلك سنحدد الطريقة المستخدمة لإنشائه، ففي حال أردنا تخزين بيانات غير موجودة ونتوقع أن تصل لاحقًا ففي تلك الحالة يجب إنشاء مخزن مؤقت جديد باستدعاء بالتابع ‎alloc()‎ من الصنف ‎Buffer‎، ولنوضح هذه الطريقة نبدأ بفتح جلسة جديدة من وضع حلقة REPL بتنفيذ الأمر ‎node‎ في سطر الأوامر كالتالي: $ node يظهر الرمز ‎>‎ في بداية السطر، ما يدل على استعداد هذا الوضع لتلقي التعليمات البرمجية وتنفيذها، حيث يقبل التابع ‎alloc()‎ تمرير عدد كمعامل أول إجباري يشير إلى حجم المخزن المؤقت الذي نود إنشاءه، أي يمثل هذا المعامل عدد البايتات التي ستُحجز في الذاكرة للمخزن المؤقت الجديد، فمثلًا لإنشاء مخزن مؤقت بسعة 1 كيلوبايت أي ما يعادل 1024 بايت يمكننا استخدام التابع السابق كالتالي: > const firstBuf = Buffer.alloc(1024); نلاحظ أن الصنف ‎Buffer‎ متاح بشكل عام في بيئة نود، ومنه يمكننا الوصول مباشرة إلى التابع ‎alloc()‎ لاستخدامه، ونلاحظ كيف مررنا القيمة ‎1024‎ كمعامل أول له لينتج لدينا مخزن مؤقت بسعة 1 كيلوبايت، حيث ستحوي المساحة المحجوزة للمخزن المؤقت الجديد مؤقتًا على أصفار افتراضيًا، وذلك ريثما نكتب البيانات ضمنه لاحقًا، وبإمكاننا تخصيص ذلك فإذا أردنا أن تحتوي تلك المساحة على واحدات بدلًا من الأصفار يمكننا تمرير هذه القيمة كمعامل ثاني للتابع ‎alloc()‎ كالتالي: > const filledBuf = Buffer.alloc(1024, 1); ينتج لدينا مخزنًا مؤقتًا بمساحة 1 كيلوبايت من الذاكرة المملوءة بالواحدات، ويجب التأكيد أن البيانات التي يمثلها المخزن المؤقت ستكون بيانات ثنائية binary مهما كانت القيمة التي نحددها له كقيمة أولية، حيث يمكن تمثيل العديد من صيغ البيانات بواسطة البيانات الثنائية، فمثلًا البيانات الثنائية التالية تمثل حجم 1 بايت: ‎01110110‎، ويمكن تفسيرها كنص بترميز ASCII باللغة الإنكليزية وبالتالي ستُعبّر عن الحرف ‎v‎، ويمكن أيضًا تفسير هذه البيانات بسياق آخر وترميز مختلف على أنها لون لبكسل واحد من صورة ما، حيث يمكن للحاسوب التعامل مع هذه البيانات ومعالجتها بعد معرفة صيغة ترميزها. ويستخدم المخزن المؤقت في نود افتراضيًا ترميز UTF-8 في حال كانت القيمة الأولية المخزنة ضمنه عند إنشاءه هي سلسلة نصية، حيث يمكن للبايت الواحد في ترميز UTF-8 أن يمثل حرفًا من أي لغة أو عددًا أو رمزًا ما، ويعتبر هذا الترميز توسعة لمعيار الترميز الأمريكي لتبادل البيانات أو ASCII والذي يقتصر على ترميز الأحرف الإنكليزية الكبيرة والصغيرة والأعداد وبعض الرموز القليلة الأخرى فقط، كعلامة التعجب "!" وعلامة الضم "&"، ويمكننا تحديد الترميز المستخدم من قبل المخزن المؤقت عبر تمريره كمعامل ثالث للتابع ‎alloc()‎، فمثلًا لو اقتصرت حاجة برنامج ما على التعامل مع محارف بترميز ASCII يمكننا تحديده كترميز للبيانات ضمن المخزن المؤقت كالتالي: > const asciiBuf = Buffer.alloc(5, 'a', 'ascii'); نلاحظ تمرير المحرف ‎a‎ كمعامل ثانِ وبذلك سيتم تخزينه ضمن المساحة الأولية التي ستُحجز للمخزن المؤقت الجديد، ويدعم نود افتراضيًا صيغ ترميز المحارف التالية: ترميز ASCII ويُمثّل بالسلسلة النصية ‎ascii‎. ترميز UTF-8 ويُمثّل بالسلسلة النصية ‎utf-8‎ أو ‎utf8‎. ترميز UTF-16 ويُمثّل بالسلسلة النصية ‎utf-16le‎ أو ‎utf16le‎. ترميز UCS-2 ويُمثّل بالسلسلة النصية ‎ucs-2‎ أو ‎ucs2‎. ترميز Base64 ويُمثّل بالسلسلة النصية ‎base64‎. الترميز الست عشري Hexadecimal ويُمثّل بالسلسلة النصية ‎hex‎. الترميز ISO/IEC 8859-1 ويُمثّل بالسلسلة النصية ‎latin1‎ أو ‎binary‎. حيث يمكن استخدام أي من أنواع الترميز السابقة مع أي تابع من الصنف ‎Buffer‎ يقبل ضمن معاملاته معاملًا بالاسم ‎encoding‎ لتحديد صيغة الترميز، ومن ضمنها التابع ‎alloc()‎ الذي تعرفنا عليه. قد نحتاج أحيانًا لإنشاء مخزن مؤقت يُعبر عن بيانات جاهزة موجودة مسبقًا، كقيمة متغير أو سلسلة نصية أو مصفوفة، حيث يمكننا ذلك باستخدام التابع ‎from()‎ الذي يدعم إنشاء مخزن مؤقت جديد من عدة أنواع من البيانات وهي: مصفوفة من الأعداد التي تتراوح قيمها بين ‎0‎ و ‎255‎،حيث يمثل كل عدد منها قيمة بايت واحد. كائن من نوع ‎ArrayBuffer‎ والذي يخزن داخله حجمًا ثابتًا من البايتات. سلسلة نصية. مخزن مؤقت آخر. أي كائن جافاسكربت يملك الخاصية ‎Symbol.toPrimitive‎ التي تُعبر عن طريقة تحويل هذا الكائن إلى بيانات أولية، مثل القيم المنطقية ‎boolean‎ أو ‎null‎ أو ‎undefined‎ أو الأعداد ‎number‎ أو السلاسل النصية ‎string‎ أو الرموز ‎symbol‎. لنختبر الآن طريقة إنشاء مخزن مؤقت جديد من سلسلة نصية باستخدام التابع from كالتالي: > const stringBuf = Buffer.from('My name is Hassan'); ينتج بذلك لدينا كائن مخزن مؤقت جديد يحتوي على قيمة السلسلة النصية ‎My name is Hassan‎، ويمكننا كما ذكرنا إنشاء مخزن مؤقت جديد من مخزن مؤقت آخر مثلًا كالتالي: > const asciiCopy = Buffer.from(asciiBuf); ينتج بذلك لدينا المخزن المؤقت ‎asciiCopy‎ والذي هو نسخة مطابقة من المخزن الأول ‎asciiBuf‎، وبذلك نكون قد تعرفنا على طرق إنشاء المخازن المؤقتة، وفي الفقرة التالية سنتعلم طرق قراءة البيانات منها. القراءة من المخزن المؤقت يوجد عدة طرق تمكننا من قراءة بيانات المخزن المؤقت، حيث يمكنن قراءة بايت واحد محدد فقط منه إذا أردنا، أو قراءة كل البيانات دفعة واحدة، ولقراءة بايت واحد فقط يمكن الوصول إليه عبر رقم ترتيب مكان هذا البايت ضمن المخزن المؤقت، حيث تُخزِّن المخازن المؤقتة البيانات بترتيب متتابع تمامًا كالمصفوفات، ويبدأ ترتيب أول مكان للبيانات داخلها من الصفر ‎0‎ تمامًا كالمصفوفات، ويمكن استخدام نفس صيغة الوصول إلى عناصر المصفوفة لقراءة البايتات بشكل مفرد من المخزن مؤقت. لنختبر ذلك نبدأ بإنشاء مخزن مؤقت جديد من سلسلة نصية كالتالي: > const hiBuf = Buffer.from('Hi!'); ونحاول قراءة أول بايت من هذا المخزن كالتالي: > hiBuf[0]; بعد الضغط على زر الإدخال ‎ENTER‎ وتنفيذ التعليمة السابقة سيظهر لنا النتيجة التالية: 72 حيث يرمز العدد ‎72‎ ضمن ترميز UTF-8 للحرف ‎H‎ وهو أول حرف من السلسلة النصية المُخزنة، حيث تقع قيمة أي بايت ضمن المجال من صفر ‎0‎ إلى ‎255‎، وذلك لأن البايت يتألف من 8 بتات أو bits، وكل بت بدوره يمثل إما صفر ‎0‎ أو واحد ‎1‎، فأقصى قيمة يمكن تمثيلها بسلسلة من ثمانية بتات تساوي 2⁸ وهو الحجم الأقصى للبايت الواحد، أي يمكن للبايت تمثيل قيمة من 256 قيمة ممكنة، وبما أن أول قيمة هي الصفر فأكبر عدد يمكن تمثيله في البايت الواحد هو 255، والآن لنحاول قراءة قيمة البايت الثاني ضمن المخزن كالتالي: > hiBuf[1]; سنلاحظ ظهور القيمة ‎105‎ والتي ترمز للحرف الصغير ‎i‎، والآن نحاول قراءة آخر بايت من هذا المخزن كالتالي: > hiBuf[2]; نلاحظ ظهور القيمة ‎33‎ والتي ترمز إلى إشارة التعجب ‎!‎، ولكن ماذا سيحدث لو حاولنا قراءة بايت غير موجود بتمرير قيمة لمكان خاطئ ضمن المخزن كالتالي: > hiBuf[3]; سنلاحظ ظهور القيمة التالية: undefined وهو نفس ما سيحدث لو حاولنا الوصول إلى عنصر غير موجود ضمن مصفوفة ما. والآن بعد أن تعرفنا على طريقة قراءة بايت واحد من البيانات ضمن المخزن مؤقت، سنتعرف على طريقة لقراءة كل البيانات المخزنة ضمنه دفعة واحدة. يوفر كائن المخزن مؤقت التابعين ‎toString()‎ و ‎toJSON()‎ والذي يعيد كل منهما البيانات الموجودة ضمن المخزن دفعة واحدة كل منهما بصيغة مختلفة، ونبدأ بالتابع ‎toString()‎ والذي يحول البايتات ضمن المخزن المؤقت إلى قيمة سلسلة نصية ويعيدها، لنختبر ذلك باستدعائه على المخزن المؤقت السابق ‎hiBuf‎ كالتالي: > hiBuf.toString(); سنلاحظ ظهور القيمة التالية: 'Hi!' وهي قيمة السلسلة النصية التي خزناها ضمن المخزن المؤقت عند إنشاءه، ولكن ماذا سيحدث لو استدعينا التابع ‎toString()‎ على مخزن مؤقت تم إنشاءه من بيانات من نوع مختلف؟ لنختبر ذلك بإنشاء مخزن مؤقت جديد فارغ بحجم ‎10‎ بايت كالتالي: > const tenZeroes = Buffer.alloc(10); ونستدعي التابع ‎toString()‎ ونلاحظ النتيجة: > tenZeroes.toString(); سيظهر ما يلي: '\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000' حيث تقابل السلسلة النصية ‎\u0000‎ المحرف في ترميز Unicode المقابل للقيمة ‎NULL‎، وهو ما يقابل قيمة الصفر ‎0‎، حيث يعيد التابع ‎toString()‎ ترميز UTF-8 للبايتات المخزنة في حال كانت البيانات ضمن المخزن المؤقت ليست من نوع سلسلة نصية، ويقبل التابع ‎toString()‎ معامل اختياري بالاسم ‎encoding‎ لتحديد ترميز البيانات المطلوب، حيث يمكن باستخدامه تعديل ترميز قيمة السلسلة النصية التي يعيدها التابع، فيمكن مثلًا قراءة نفس البيانات للمخزن ‎hiBuf‎ السابق لكن بالترميز الست عشري كالتالي: > hiBuf.toString('hex'); سنلاحظ ظهور النتيجة التالية: '486921' حيث تُعبر تلك القيمة عن الترميز الست عشري للبايتات التي تتألف منها السلسلة النصية ‎Hi!‎. ويُستفاد في نود من تلك الطريقة لتحويل ترميز بيانات ما من شكل لآخر، بإنشاء مخزن مؤقت جديد يحوي قيمة السلسلة النصية المراد تحويلها ثم استدعاء التابع ‎toString()‎ مع تمرير الترميز الجديد المرغوب به. أما وفي المقابل يعيد التابع ‎toJSON()‎ البيانات ضمن المخزن المؤقت كأعداد تمثل قيم البايتات المخزنة مهما كان نوعها، والآن لنختبر ذلك على كل من المخزنين السابقين ‎hiBuf‎ و ‎tenZeroes‎ ونبدأ بإدخال التعلمية التالية: > hiBuf.toJSON(); سنلاحظ ظهور القيمة التالية: { type: 'Buffer', data: [ 72, 105, 33 ] } يحوي الكائن الناتج من استدعاء التابع ‎toJSON()‎ على خاصية النوع ‎type‎ بالقيمة نفسها دومًا وهي ‎Buffer‎، حيث يُستفاد من هذه القيمة لتمييز نوع كائن JSON هذا عن الكائنات الأخرى، ويحتوي على خاصية البيانات ‎data‎ وهي مصفوفة من الأعداد التي تمثل البايتات المخزنة، ونلاحظ أنها تحتوي على القيم ‎72‎ و ‎105‎ و ‎33‎ بالترتيب وهي نفس القيم التي ظهرت لنا سابقًا عند محاولة قراءة البايتات المخزنة بشكل مفرد. والآن لنختبر استدعاء التابع ‎toJSON()‎ على المخزن الفارغ ‎tenZeroes‎: > tenZeroes.toJSON(); سنلاحظ ظهور النتيجة التالية: { type: 'Buffer', data: [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] } الخاصية ‎type‎ تحوي نفس القيمة السابقة، بينما البيانات في المصفوفة هي عشرة أصفار تمثل البايتات العشرة الفارغة التي يحويها المخزن المؤقت، وبذلك نكون قد تعلمنا طرق قراءة البيانات من المخازن المؤقتة، وفي الفقرة التالية سنتعلم طريقة التعديل على تلك البيانات ضمن المخزن المؤقت. التعديل على المخزن المؤقت يوجد عدة طرق للتعديل على البيانات ضمن المخزن المؤقت، وهي مشابهة لطريقة قراءة البيانات حيث يمكن إما تعديل قيمة بايت واحد مباشرة باستخدام نفس صيغة الوصول لعناصر المصفوفات، أو كتابة محتوى جديد وتبديل المحتوى المخزن مسبقًا. ولنبدأ بالتعرف على الطريقة الأولى لذلك سنستخدم المخزن السابق ‎hiBuf‎ الذي يحتوي على قيمة السلسلة النصية ‎Hi!‎ داخله، ولنحاول تعديل محتوى كل بايت منه على حدى إلى أن تصبح القيمة الجديدة هي ‎Hey‎، حيث نبدأ بتعديل الحرف الثاني من المخزن ‎hiBuf‎ إلى الحرف ‎e‎ كالتالي: > hiBuf[1] = 'e'; نتأكد من صحة التعديل السابق بقراءة محتوى المخزن المؤقت الجديد باستدعاء التابع ‎toString()‎ كالتالي: > hiBuf.toString(); نلاحظ ظهور القيمة التالية: 'H\u0000!' القيمة الغريبة التي ظهرت تدل على أن المخزن مؤقت يقبل فقط القيم العددية عند تخزينها داخله، لذا لا يمكن تمرير الحرف ‎e‎ كسلسلة نصية مباشرةً، بل يجب تمرير القيمة الثنائية المقابلة له كالتالي: > hiBuf[1] = 101; الآن يمكننا معاينة القيمة الجديدة والتأكد: > hiBuf.toString(); نحصل على القيمة التالية: 'He!' نعدل الحرف الأخير من هذه القيمة وهو العنصر الثالث ونضع القيمة الثنائية المقابلة للحرف ‎y‎ كالتالي: > hiBuf[2] = 121; نتأكد من المحتوى بعد التعديل: > hiBuf.toString(); نحصل على القيمة: 'Hey' ماذا سيحدث لو حاولنا تعديل قيمة بايت يقع خارج مجال بيانات المخزن المؤقت؟ سنلاحظ تجاهل المخزن لتلك العملية وتبقى القيمة المخزنة ضمنه كما هي، لنختبر ذلك بكتابة الحرف ‎o‎ إلى المحرف الرابع الغير موجود ضمن المخزن السابق كالتالي: > hiBuf[3] = 111; نعاين قيمة المخزن بعد ذلك التعديل: > hiBuf.toString(); ونلاحظ أن القيمة بقيت كما هي دون تعديل: 'Hey' الطريقة الأخرى للتعديل على محتوى المخزن تكون بكتابة عدة بايتات معًا باستخدام التابع ‎write()‎ الذي يقبل سلسلة نصية كمعامل له تعبر عن المحتوى الجديد للبيانات، لنختبر ذلك عبر تعديل محتوى المخزن ‎hiBuf‎ إلى محتواه السابق ‎Hi!‎ كالتالي: > hiBuf.write('Hi!'); نلاحظ أن تنفيذ التعليمة السابقة يعيد القيمة ‎3‎ وهي عدد البايتات التي تم تعديلها ضمن المخزن في تلك العملية، حيث يعبر كل بايت عن محرف واحد لأننا نستخدم الترميز UTF-8، وفي حال كان المخزن يستخدم ترميز آخر مثل UTF-16 ففيه يُمثَّل كل محرف على 2 بايت، عندها سيعيد تنفيذ تابع الكتابة ‎write()‎ بنفس الطريقة القيمة ‎6‎ للدلالة على عدد البايتات التي تمثل المحارف الثلاث المكتوبة. والآن لنتأكد من المحتوى الجديد بعد التعديل نستدعي‎toString()‎ كالتالي: > hiBuf.toString(); نحصل على القيمة: 'Hi!' هذه الطريقة أسرع من طريقة تعديل كل بايت على حدى، ولكن ماذا سيحدث لو كتبنا بيانات بحجم أكبر من حجم المخزن الكلي؟ سيقبل المخزن البيانات المقابلة لحجمه فقط ويهمل البقية، لنختبر ذلك بإنشاء مخزن مؤقت بحجم 3 بايت كالتالي: > const petBuf = Buffer.alloc(3); ونحاول كتابة سلسلة نصية بأربعة محارف مثلًا ‎Cats‎ كالتالي: > petBuf.write('Cats'); نلاحظ أن ناتج التعليمة السابقة هي القيمة ‎3‎ أي تم تعديل قيمة ثلاث بايتات فقط وتجاهل باقي القيمة المُمررة، لنتأكد من القيمة الجديدة كالتالي: > petBuf.toString(); نلاحظ القيمة الجديدة: 'Cat' حيث يُعدل التابع ‎write()‎ البايتات بالترتيب فعدّل أول ثلاث بايتات فقط ضمن المخزن وتجاهل البقية. والآن لنختبر ماذا سيحدث لو كتبنا قيمة بحجم أقل من حجم المخزن الكلي، لهذا نُنشئ مخزن مؤقت جديد بحجم 4 بايت كالتالي: > const petBuf2 = Buffer.alloc(4); ونكتب القيمة الأولية داخله كالتالي: > petBuf2.write('Cats'); ثم نكتب قيمة جديدة حجمها أقل من حجم المخزن الكلي كالتالي: > petBuf2.write('Hi'); وبما أن البيانات ستكتب بالترتيب بدئًا من أول بايت سنلاحظ نتيجة ذلك عند معاينة القيمة الجديدة للمخزن: > petBuf2.toString(); ليظهر القيمة التالية: 'Hits' تم تعديل قيمة أول بايتين فقط، وبقيت البايتات الأخرى كما هي دون تعديل. تكون البيانات التي نود كتابتها موجودة أحيانًا ضمن مخزن مؤقت آخر، حيث يمكننا في تلك الحالة نسخ محتوى ذلك المخزن باستدعاء التابع ‎copy()‎، لنختبر ذلك بداية بإنشاء مخزنين جديدين كالتالي: > const wordsBuf = Buffer.from('Banana Nananana'); > const catchphraseBuf = Buffer.from('Not sure Turtle!'); يحوي كل من المخزنين ‎wordsBuf‎ و ‎catchphraseBuf‎ على بيانات من نوع سلسلة نصية، فإذا أردنا تعديل قيمة المخزن ‎catchphraseBuf‎ ليحوي على القيمة ‎Nananana Turtle!‎ بدلًا من ‎Not sure Turtle!‎ يمكننا استدعاء تابع النسخ ‎copy()‎ لنسخ القيمة ‎Nananana‎ من المخزن ‎wordsBuf‎ إلى ‎catchphraseBuf‎، حيث نستدعي التابع ‎copy()‎ على المخزن الحاوي على المعلومات المصدر لنسخها إلى مخزن آخر، ففي مثالنا النص الذي نريد نسخه موجود ضمن المخزن ‎wordsBuf‎، لذا نستدعي تابع النسخ منه كالتالي: > wordsBuf.copy(catchphraseBuf); حيث يُعبّر معامل الوجهة ‎target‎ المُمرر له عن المخزن المؤقت الذي ستُنسخ البيانات إليه، ونلاحظ ظهور القيمة ‎15‎ كنتيجة لتنفيذ التعليمة السابقة وهي تعبر عن عدد البايتات التي تم كتابتها، ولكن بما أن النص ‎Nananana‎ مكوّن من ثمانية محارف فقط فهذا يدل على عمل مختلف نفذه تابع النسخ، لنحاول معرفة ماذا حدث ونعاين القيمة الجديدة باستخدام التابع ‎toString()‎ ونلاحظ النتيجة: > catchphraseBuf.toString(); نلاحظ القيمة الجديدة: 'Banana Nananana!' نلاحظ أن تابع النسخ ‎copy()‎ قد نسخ كامل المحتوى من المخزن ‎wordsBuf‎ وخزنه ضمن ‎catchphraseBuf‎، ولكن ما نريده هو نسخ قسم من تلك البيانات فقط وهي القيمة ‎Nananana‎، لنعيد القيمة السابقة للمخزن ‎catchphraseBuf‎ أولًا ثم نحاول تنفيذ المطلوب كالتالي: > catchphraseBuf.write('Not sure Turtle!'); يقبل التابع ‎copy()‎ عدة معاملات تمكننا من تحديد البيانات التي نرغب بنسخها إلى المخزن المؤقت الوجهة وهي: الوجهة ‎target‎ وهو المعامل الإجباري الوحيد، ويعبر عن المخزن المؤقت الوجهة لنسخ البيانات. ‎targetStart‎ وهو ترتيب أول بايت ستبدأ كتابة البيانات إليه ضمن المخزن الوجهة، وقيمته الافتراضية هي الصفر ‎0‎، أي بدء عملية الكتابة من أول بايت ضمن المخزن الوجهة. ‎sourceStart‎ وهو ترتيب أول بايت من البيانات التي نرغب بنسخها من المخزن المصدر. ‎sourceEnd‎ وهو ترتيب آخر بايت من البيانات الذي ستتوقف عملية النسخ عنده في المخزن المصدر، وقيمته الافتراضية هي الطول الكلي للبيانات ضمن المخزن المصدر. باستخدام تلك المعاملات يمكننا تحديد الجزء ‎Nananana‎ من المخزن ‎wordsBuf‎ ليُنسخ إلى المخزن ‎catchphraseBuf‎، حيث نمرر المخزن ‎catchphraseBuf‎ كمعامل الوجهة ‎target‎ كما فعلنا سابقًا، ونمرر القيمة ‎0‎ للمعامل ‎targetStart‎ لكتابة القيمة ‎Nananana‎ في بداية المخزن ‎catchphraseBuf‎، أما للقيمة ‎sourceStart‎ سنمرر ‎7‎ وهو ترتيب بداية أول محرف من القيمة ‎Nananana‎ ضمن المخزن ‎wordsBuf‎، وللقيمة ‎sourceEnd‎ نمرر الحجم الكلي للمخزن المصدر، ليكون الشكل النهائي لاستدعاء تابع النسخ بعد تخصيص المعاملات السابقة كالتالي: > wordsBuf.copy(catchphraseBuf, 0, 7, wordsBuf.length); سيظهر هذه المرة القيمة ‎8‎ كنتيجة لتلك العملية ما يعني أن القيمة التي حددناها فقط هي ما تم نسخه، ونلاحظ كيف استخدمنا الخاصية ‎wordsBuf.length‎ لتمرير حجم المخزن كقيمة للمعامل ‎sourceEnd‎، وهي نفس الخاصية ‎length‎ الموجودة ضمن المصفوفات، والآن لنعاين القيمة الجديدة للمخزن ‎catchphraseBuf‎ ونتأكد من النتيجة: > catchphraseBuf.toString(); نلاحظ القيمة الجديدة: 'Nananana Turtle!' بذلك نكون قد عدلنا البيانات ضمن المخزن ‎catchphraseBuf‎ عن طريق نسخ جزء محدد من بيانات المخزن ‎wordsBuf‎ إليه. والآن بعد أن انتهينا من تنفيذ الأمثلة في هذا الفصل يمكنك الخروج من جلسة REPL حيث ستُحذف كل المتغيرات السابقة التي عرفناها بعد عملية الخروج هذه، ولذلك ننفذ أمر الخروج كالتالي: > .exit ختامًا تعرفنا في هذا المقال على المخازن المؤقتة والتي تمثل مساحة محددة من الذاكرة محجوزة لتخزين البيانات بالصيغة الثنائية، وتعلمنا طرق إنشاء المخازن المؤقتة، سواء الجديدة أو التي تحتوي على بيانات موجودة مسبقًا، وتعرفنا بعدها على طرق قراءة تلك البيانات من المخزن سواء بقراءة كل بايت منه على حدى أو قراءة المحتوى كاملًا باستخدام التابعين ‎toString()‎ و ‎toJSON()‎، ثم تعرفنا على طرق الكتابة إلى المخازن لتعديل البيانات المخزنة ضمنها، سواء بكتابة كل بايت على حدى أو باستخدام التابعين ‎write()‎ و ‎copy()‎. يفتح التعامل مع المخازن المؤقتة في نود Node.js الباب للتعامل مع البيانات الثنائية مباشرة، فيمكن مثلًا دراسة تأثير صيغ الترميز المختلفة للمحارف على البيانات المخزنة، كمقارنة صيغ الترميز المختلفة مع الصيغتين UTF-8 و ASCII وملاحظة فرق الحجم بينها، كما يمكن مثلًا تحويل البيانات المخزنة من صيغة UTF-8 إلى صيغ الترميز الأخرى، ويمكنك الرجوع إلى التوثيق الرسمي العربي من نود للكائن ‎Buffer‎ للتعرف عليه أكثر. ترجمة -وبتصرف- للمقال Using Buffers in Node.js لصاحبه Stack Abuse. اقرأ أيضًا المقال السابق: إنشاء خادم ويب في Node.js باستخدام الوحدة HTTP مقدمة إلى Node.js أساسيات التخزين المؤقت للويب Web Caching: المصطلحات الأساسية أساسيات التخزين المؤقت للويب Web Caching: ترويسات HTTP واستراتيجيات التخزين المؤقت
  4. يعتمد ذلك على تطبيقات تتيح لك بناء صفحات الويب أو تطبيقات معينة أو الربط بين عدة خدمات معًا لبناء تطبيق دون الحاجة لمعرفة التقنيات البرمجية التي تُبنى بها، لكن ميزات التطبيق المنتج تكون محصورة بقدرات التطبيق الذي طور بها، قد لا تتيح لك تخصيص بعض الميزات التي تحتاجها وقد لا تدعمها، هدف تلك التطبيقات هي تجريد عملية بناء التطبيقات الأكثر استخدامًا والتي يمكن أتمتتها، أي إذا كنت تريد بناء منتج بسيط، كصفحة ويب لعرض بعض المعلومات فيمكنك بناءها بواسطة تلك التطبيقات دون الحاجة لمعرفة تقنية كبيرة، يمكنك الاستفادة من المقال التالي والذي يشرح ذلك تمامًا ويعرفك على بعض تلك الأدوات ومستقبل هذا المجال:
  5. الأمر طبيعي عند البدء بتعلم البرمجة، فكل الأفكار والشيفرات وبنيتها ومعانيها تكون جديدة، بداية ركز على الأساسيات ولا تنتقل إلى أفكار لاحقة قبل تمكين الأساسيات، والعامل الأكبر في تمكين الأفكار الجديدة هو التركيز على التطبيق العملي دومًا، ومع تكرار التطبيق العملي ستثبت الأساسيات وتصبح الشيفرات البرمجية مألوفة وتقرأها كما تقرأ أي لغة جديدة، ركز في شرح الأفكار الجديدة، ثم وظّف تلك الأفكار في مثال عملي لو كان بسيطًا، فالتطبيق العملي هو الأساس، وحاول القراءة بنفسك عن أي فكرة جديدة لتوسع فهمك لها قدر الحاجة، يمكنك مثلًا الرجوع لموسوعة حسوب ففيها شرح مفصل وموسع عن تفاصيل اللغات البرمجية، وأنصحك أيضًا بقراء المقالات التالية والتي تشرح لك النقاط التي يجب التركيز عليها عند تعلم البرمجة:
  6. يوجد عدة حلول لذلك ضمن CSS وذلك حسب الحالة، في حال أردت وضع زر في مكان ثابت ابدأ أولًا مثلًا بإضافة معرّف id له لاستهدافه ضمن تنسيقات CSS: <button id="my-button">...</button> والآن يمكنك في CSS استهدافه وتطبيق التنسيقات عليه كالتالي: #my-button { ... } لوضعه في مكان محدد نسبة إلى العنصر الحاول له يمكن تعيين الخاصية position للقيمة absolute وتحديد إحداثيات مكان الزر في الخواص top و left: #my-button { position: absolute; top: 50px; left: 40px; } ويمكن أيضًا استخدام الخواص right و bottom بنفس الطريقة، أنصحك بقراءة المقال التالي الذي يشرح الطرق المختلفة لضبط تموضع العناصر:
  7. يرجى تعديل السؤال وإرفاق الشيفرة ورسالة الخطأ التي تظهر نصًا بدلًا من الصورة مع تنسيقها بتنسيق الشيفرة كي نستطيع مساعدتك
  8. لاحظ أنك لم تمرر للمكون Notes الخاصية notes وهذا سبب المشكلة، يجب تمرير تلك الخاصية كالتالي: <Notes notes={...} .../> ^
  9. في الشيفرة المرفقة لا يوجد استخدام للمكون Notes تأكد من مكان استخدامه وطريقة تمرير تلك الخاصية، كما يرجى إرفاق الجزء من الشيفرة الخاص بذلك الاستخدام بدلًا من رابط خارجي
  10. في حال لم يظهر أي عنصر يعني أن قيمة notes لم تمرر للمكون Notes واستُخدمت القيمة الافتراضية وهي المصفوفة الفارغة، لذا تأكد أن قيمة notes تمرر بشكل صحيح للمكون وتحوي قيمة
  11. يمكنك تعيين قيمة افتراضية للمتغير notes تكون مصفوفة فارغة في حال لا يوجد بيانات، من داخل المكون يمكنك ذلك كالتالي: <div className='notes'> { (notes || []).map(note => <Note key={note.id} title={note.title} content={note.content}/>) } ... </div>
  12. ابدأ بتعلم أساسيات اللغة نفسها، من متغيرات وتعابير شرطية وحلقات ثم أنواع البيانات والمتغيرات، والمصفوفات وطرق التعامل معها، فالأساسيات ضرورية مهما كان مجال اختصاصك، بعدها ستتمكن من إنشاء النصوص البرمجية البسيطة في جافاسكريبت لتنفيذ المهام، ثم يمكنك تعلم طرق تثبيت واستيراد واستخدام المكتبات البرمجية الجاهزة وهي ضرورية لتكون جزءًا من برامجك تعلم ذلك أيضًا، هنا ستركز على تعلم كل مكتبة برمجية أو إطار عمل ستستخدمه على حدى لتحقيق المطلوب، أنصحك بقراءة المقال التالي والذي يوضح لك كيف تتعلم البرمجة واين تركز وكيف تبدأ: ويمكنك بدء تعلم البرمجة من سلسلة دروس python 101 باللغة العربية فهي تبدأ من الصفر وتصل إلى مستويات واسنخدامات متقدمة:
  13. يمكنك دومًا السؤال ضمن التعليقات أسفل الدروس وتوضيح مشكلتك وسيتم متابعتها من قبل فريق المدربين المختص ومتابعة المشاكل وحلها معك، الدورات يتم تحديثها باستمرار لتغطي آخر التقنيات وإصدارات المكتبات لذا ابقى على اطلاع وإلى حينها يمكنك متابعى الدورة وحتى من تلك المشاكل هي فرصة لتكسب خبرة في التعامل مع مشاكل الاعتماديات وطرق حلها وستستفيد من ذلك.
  14. لا يوجد متغير اسمه x ليتم تمريره إلى الخرج cout، إذا كنت تقصدين إحدى الخواص x من أحد الكائنين o1 أو o2 فيجب استخراج الخاصية منهما ثم طباعتها كالتالي: cout << o1.x; # أو cout << o2.x; يمكنك الاطلاع أكثر والاستفادة من قراءة المقالات التالية: يرجة تعديل نص السؤال واستبدال الشيفرة ضمن مربع تنسيق الشيفرة بتنسيقها الصحيح.
  15. يرجى توضيح الهدف والمراد من البرنامج، وإرفاق النتيجة الخاطئة أو رسالة الخطأ التي تظهر لتتوضح المشكلة ونتمكن من المساعدة
  16. يرجى إرفاق أي رسائل خطأ تظهر ضمن الطرفية console، وذكر لغة البرمجة وإطار العمل الذان يتم العمل بهما لتوضيح المشكلة
  17. رمز الاستجابة السريعة أو QR هو مجرد ترميز لنص ما على شكل صورة بمواصفات محددة، لتتمكن البرامج الأخرى من قراءتها بصريًا، سواء عبر تحليل صورة ثابتة أو تحليل ما تُظهره الكاميرة وقراءة ذلك النص ومعالجته، لذا يجب أولًا توليد رمز QR خاص برابط الموقع المُطلوب، ثم نشر تلك الصورة إما عبر طباعتها أو وضعها في المكان المطلوب ليتمكن المستخدمون من قراءتها والتوجه إلى الموقع، يمكنك البحث عبر الانترنت عن مواقع مختصة بتوليد تلك الرموز مجانًا، ويمكنك الاستفادة من الإجابات التالية:
  18. هل يمكنك توضيح ما العنصر الذي تواجه مشكلة في عرضه وأين، ففي الشيفرة المرفقة يوجد السطر التالي والذي يخفي إشارة التحميل بعد وصول رد الطلب: $('.spinner-border').remove(); ولا يوجد تعامل مع تلك الخاصية في مكان آخر ضمن الشيفرة، كذلك يرجى إرفاق أي رسائل خطأ تظهر لك ضمن console
  19. يرسل المتصفح عند استعراضك لصفحة ويب ما طلبًا إلى جهاز حاسوب آخر عبر الإنترنت وهو بدوره يرسل الصفحة المطلوبة كجواب لذلك الطلب، حيث ندعو جهاز الحاسوب الذي أُرسل إليه ذلك الطلب بخادم الويب web server، ووظيفته تلقي طلبات HTTP القادمة من العملاء كمتصفحات الويب، ويرسل بالمقابل رد HTTP يحتوي على صفحة HTML أو بيانات بصيغة JSON في حال كان دور الخادم تمثيل واجهة برمجية API، ولإرسال هذه البيانات ومعالجة الطلبات يحتاج خادم الويب لعدة برمجيات تقسم إلى صنفين أساسيين هما شيفرات الواجهات الأمامية Front-end code وهدفها عرض المحتوى المرئي للعميل مثل المحتوى وتنسيق الصفحة من ألوان مستخدمة أو خطوط، والواجهات الخلفية Back-end code وهدفها تحديد طرق تبادل البيانات ومعالجة الطلبات القادمة من المتصفح وتخزينها بالاتصال بقاعدة البيانات، والعديد من العمليات الأخرى. تتيح لنا بيئة نود Node.js كتابة شيفرات الواجهات الخلفية باستخدام لغة جافاسكربت، والتي كان سابقًا استخدامها محصورًا على تطوير الواجهات الأمامية فقط، وسهل استعمال بيئة نود استخدام لغة جافاسكربت لتطوير الواجهات الأمامية والخلفية معًا عملية تطوير خوادم الويب بدلًا من استعمال لغات أخرى لتطوير الواجهات الخلفية مثل لغة PHP، وهو السبب الأساسي في شهرة نود واستخدامها الواسع لتطوير شيفرات الواجهات الخلفية. سنتعلم في هذا المقال كيف نبني خادم ويب بالاستعانة بالوحدة البرمجية http التي توفرها نود يمكنه إعادة صفحات الويب بلغة HTML والبيانات بصيغة JSON وحتى ملفات البيانات بصيغة CSV. المستلزمات بيئة Node.js مثبتة على الجهاز حيث استخدمنا في هذا المقال الإصدار رقم 10.19.0. معرفة بأساسيات البرمجة ضمن بيئة نود، ويمكنك التعرف على ذلك أكثر بمراجعة المقال الأول من هذه السلسلة. سنستخدم البرمجة اللامتزامنة في إحدى الفقرات من هذا المقال، لذلك يمكنك التعرف على هذه الطريقة أكثر وعلى طريقة استعمال الوحدة fs للتعامل مع الملفات بمراجعة المقال الخامس من هذه السلسلة. إنشاء خادم HTTP بسيط في Node.js سنبدأ بإنشاء خادم ويب يعيد للمستخدم نصًا بسيطًا، لنتعلم بذلك أساسيات إعداد الخادم والتي سنعتمد عليها لتطوير خوادم أخرى ستعيد البيانات بصيغ متقدمة مثل صيغة JSON. نبدأ بإعداد البيئة البرمجية لتنفيذ التمارين ضمن هذا المقال فنُنشئ مجلدًا جديدًا بالاسم first-servers ثم ننتقل إليه: mkdir first-servers cd first-servers ونُنشئ الملف الرئيسي لشيفرة الخادم: touch hello.js نفتح الملف ضمن أي محرر نصوص سنستخدم في هذا المقال محرر نانو nano: nano hello.js نضيف إلى الملف السطر التالي لاستيراد الوحدة البرمجية http التي يوفرها نود افتراضيًا: const http = require("http"); تحوي وحدة http توابع لإنشاء الخادم سنستخدمها لاحقًا، ويمكنك التعرف أكثر على الوحدات البرمجية بمراجعة المقال الرابع من هذه السلسلة. والآن لنعرف ثابتين الأول هو اسم المضيف والثاني هو رقم المنفذ الذي سيستمع إليه الخادم: ... const host = 'localhost'; const port = 8000; كما ذكرنا سابقًا يستقبل الخادم الطلبات المرسلة إليه من متصفح العميل، ويمكن الوصول للخادم عبر عنوانه بإدخال اسم النطاق له والذي سيترجم لاحقًا إلى عنوان IP من قبل خادم DNS، ويتألف هذا العنوان من عدة أرقام متتالية مميزة لكل جهاز ضمن الشبكة مثل شبكة الإنترنت، واسم النطاق localhost هو عنوان خاص يشير به جهاز حاسوب إلى نفسه ويقابله عنوان IP التالي 127.0.0.1، وهو متاح فقط ضمن جهاز الحاسوب المحلي وليس متاحًا على أي شبكة موصول بها بما فيها شبكة الإنترنت. ويعبر رقم المنفذ port عن بوابة مميزة على الجهاز صاحب عنوان IP المحدد، حيث سنستخدم في حالتنا المنفذ رقم 8000 على الجهاز المحلي لخادم الويب، ويمكن استخدام أي رقم منفذ آخر غير محجوز، ولكن عادة ما نعتمد المنفذ رقم 8080 أو 8000 خلال مرحلة التطوير لخوادم HTTP، وبعد ربط الخادم على اسم المضيف ورقم المنفذ المحددين سنتمكن من الوصول إليه من المتصفح المحلي عبر العنوان http://localhost:8000. والآن لنضيف دالة مهمتها معالجة طلبات HTTP الواردة وإرسال رد HTTP المناسب لها، حيث تستقبل الدالة معاملين الأول req وهو كائن يمثل الطلب الوارد ويحوي البيانات الواردة ضمن طلب HTTP، والثاني res وهو كائن يحوي توابع مفيدة لبناء الرد المراد إرساله للعميل، حيث نستخدمه لإرسال رد HTTP من الخادم، وسنعيد بدايةً الرسالة "My first server!‎" لكل الطلبات الواردة إلى الخادم: ... const requestListener = function (req, res) { res.writeHead(200); res.end("My first server!"); }; يفضل إعطاء الدوال اسمًا واضحًا يدل على وظيفتها، فمثلًا إذا كان تابع الاستماع للطلب يعيد قائمة من الكتب المتوفرة فيفضل تسميته listBooks()‎، لكن في حالتنا وبما أننا نختبر ونتعلم فيمكننا تسميته بالاسم requestListener أي المستمع للطلب. تستقبل توابع الاستماع للطلبات request listener functions كائنين كمعاملات لها نسميهما عادةً req و res، حيث يُغلَّف طلب HTTP الوارد من المستخدم ضمن كائن الطلب في أول معامل req، ونبني الرد على ذلك الطلب بالاستعانة بكائن الرد في المعامل الثاني res. يعيِّن السطر الأول من تابع الاستماع السابق res.writeHead(200);‎ رمز الحالة لرد HTTP الذي سنرسله، والذي يحدد حالة معالجة الطلب من قبل الخادم، ففي حالتنا وبما أن الطلب سينجح ويكون صحيح دومًا نعين للرد رمز الحالة 200 والذي يعني إتمام الطلب بنجاح أو "OK"، وانظر مقال رموز الإجابة في HTTP للتعرف على أهم رموز الإجابة في طلبات HTTP. أما السطر الثاني من التابع res.end("My first server!");‎ فيرسل الرد للعميل الذي أرسل الطلب، ويمكن باستخدام ذلك التابع إرسال البيانات التي يجب أن يرسلها الخادم ضمن الرد وفي حالتنا هي إرسال نص بسيط. والآن أصبحنا جاهزين لإنشاء الخادم والاستفادة من تابع الاستماع السابق: ... const server = http.createServer(requestListener); server.listen(port, host, () => { console.log(`Server is running on http://${host}:${port}`); }); نحفظ الملف ونخرج منه، وفي حال كنت تستخدم محرر النصوص nano يمكنك الخروج بالضغط على الاختصار CTRL+X. في الشيفرة السابقة، أنشأنا في أول سطر كائن الخادم server باستخدام التابع createServer()‎ من الوحدة http، وظيفته استقبال طلبات HTTP وتمريرها إلى تابع الاستماع requestListener()‎، وبعدها نربط الخادم إلى عنوان الشبكة الذي سيستمع إليه باستخدام التابع server.listen()‎ ويمكن أن نمرر له رقم المنفذ port كمعامل أول، وعنوان الشبكة host كمعامل ثانِ، وفي النهاية دالة رد نداء callback تُستدعى عند بدء الاستماع من قبل الخادم للطلبات الواردة، وكل تلك المعاملات اختيارية لكن يفضل تمريرها وتحديد قيمها ليتضح عند قراءة الشيفرة على أي منفذ وعنوان سيستمع الخادم، ومن الضروري معرفة هذه الإعدادات للخادم عند نشر خادم الويب في بعض البيئات، خاصة التي تحتاج لإعداد موزع الحمل load balancing وإعداد الأسماء في خدمة DNS، ومهمة دالة رد النداء التي مررناها هناك طباعة رسالة إلى الطرفية تبين أن الخادم بدأ الاستماع مع توضيح عنوان الوصول إليه. يجب الملاحظة أنه حتى ولو لم نكن بحاجة لاستخدام كائن الطلب req ضمن تابع الاستماع، فمن الضروري تمريره كمعامل أول حتى نتمكن من الوصول لكائن الرد res كمعامل ثانِ بشكل صحيح. رأينا مما سبق سهولة إنشاء خادم ويب في نود حيث استطعنا بأقل من 15 سطرًا تجهيز خادم الويب، والآن لنشغله ونرى كيف يعمل بتنفيذ الأمر التالي: node hello.js سيظهر لنا الخرج التالي ضمن الطرفية: Server is running on http://localhost:8000 نلاحظ أن سطر الأوامر خرج من وضع الإدخال الافتراضي، لأن خادم الويب يعمل ضمن إجرائية طويلة لا تنتهي ليتمكن من الاستماع إلى الطلبات الواردة إليه في أي وقت، أما عند حدوث خطأ ما أو في حال أوقفنا الخادم يدويًا سيتم بذلك الخروج من تلك الإجرائية، لهذا السبب يجب اختبار الخادم من طرفية أخرى جديدة عبر التواصل معه باستخدام أداة تتيح إرسال واستقبال البيانات عبر الشبكة مثل cURL، وباستخدامها ننفذ الأمر التالي لإرسال طلب HTTP من نوع GET لخادم الويب السابق: curl http://localhost:8000 بعد تنفيذ الأمر سيظهر لنا رد الخادم ضمن الخرج كالتالي: My first server! نلاحظ ظهور الرد من طرف الخادم، ونكون بذلك قد أعددنا خادم الويب واختبرنا إرسال طلب إليه واستقبال الرد منه بنجاح، لكن لنفصّل أكثر في تلك عملية ونفهم ما حدث. عند إرسال طلب الاختبار إلى الخادم أرسلت الأداة cURL طلب HTTP من النوع GET إلى الخادم على العنوان http://localhost:8000، ثم استقبل خادم الويب الذي أنشأناه ذلك الطلب من العنوان الذي يستمع عليه ومرره إلى تابع الاستماع ومعالجة الطلبات المحدد requestListener()‎، وهو بدوره عيّن رمز الحالة بالرقم 200 وأرسل البيانات النصية ضمن الرد، ثم أرسل الخادم بعدها الرد إلى صاحب الطلب وهو الأداة cURL، والتي بدورها عرضت محتواه على الطرفية. نوقف الخادم الآن بالضغط على الاختصار CTRL+C ضمن الطرفية الخاصة به لإيقاف الإجرائية التي يعمل ضمنها ونعود بذلك إلى سطر الأوامر بحالته الافتراضية لاستقبال كتابة الأوامر وتنفيذها، ولكن ما طورناه يختلف عن خوادم الويب للمواقع التي نزورها عادة أو الواجهات البرمجية API التي نتعامل معها، فهي لا ترسل نصًا بسيطًا فحسب بل إما ترسل لنا صفحات مكتوبة بلغة HTML أو بيانات بصيغة JSON، لذلك في سنتعلم الفقرة التالية كيف يمكننا الرد ببيانات مكتوبة بتلك الصيغ الشائع استخدامها على شبكة الويب. الرد بعدة أنواع من البيانات يمكن لخادم الويب إرسال البيانات للعميل ضمن الرد بعدة صيغ منها HTML و JSON وحتى XML وصيغة CSV، كما يمكن للخوادم إرسال بيانات غير نصية مثل مستندات PDF أو الملفات المضغوطة وحتى الصوت أو الفيديو. سنتعلم في هذه الفقرة كيف نرسل بعض الأنواع من تلك البيانات وهي JSON و CSV وصفحات HTML وهي صيغ البيانات النصية الشائع استخدامها في الويب، حيث توفر العديد من الأدوات ولغات البرمجة دعمًا واسعًا لإرسال تلك الأنواع من البيانات ضمن ردود HTTP، فمثلًا يمكن إرسالها في نود باتباع الخطوات التالية: تعيين قيمة لترويسة Content-Type للرد في HTTP بقيمة تناسب نوع المحتوى المُرسل. تمرير البيانات بالصيغة الصحيحة للتابع res.end()‎ لإرسالها. سنطبق ذلك في عدة أمثلة لاحقة، حيث ستتشارك كل تلك الأمثلة في نفس طريقة إعداد الخادم كما فعلنا في الفقرة السابقة، والاختلاف بينها سيكون ضمن تابع معالجة الطلب فقط requestListener()‎، لذلك سنحضر ملفات تلك الأمثلة باستخدام قالب موحد لها جميعًا سنكتبه في البداية، لهذا نبدأ بإنشاء ملف جافاسكربت جديد بالاسم html.js سيحتوي على مثال إرسال الخادم لبيانات بصيغة HTML. نبدأ بكتابة الشيفرات المشتركة بين جميع الأمثلة ضمنه ثم ننسخ الملف إلى عدة نسخ لتجهيز ملفات الأمثلة الباقية: touch html.js نفتح الملف ضمن أي محرر نصوص: nano html.js ونضع داخله محتوى القالب لجميع الأمثلة اللاحقة كالتالي: const http = require("http"); const host = 'localhost'; const port = 8000; const requestListener = function (req, res) {}; const server = http.createServer(requestListener); server.listen(port, host, () => { console.log(`Server is running on http://${host}:${port}`); }); نحفظ الملف ونخرج منه وننسخه إلى ملفين جديدين الأول لمثال إرسال البيانات بصيغة CSV ضمن الرد كالتالي: cp html.js csv.js والآخر لإرسال البيانات بصيغة JSON: cp html.js json.js ونحضر الملفات التالية أيضًا والتي سنستخدمها للأمثلة في الفقرة اللاحقة: cp html.js htmlFile.js cp html.js routes.js بذلك نكون قد جهزنا جميع ملفات الأمثلة وبإمكاننا البدء بتضمينها، وسنبدأ في أول مثال بالتعرف على طريقة إرسال البيانات بصيغة JSON. إرسال البيانات بصيغة JSON صيغة ترميز كائنات جافاسكربت objects أو ما يعرف بصيغة JSON هي صيغة نصية لتبادل البيانات، وكما يشير اسمها فهي مشتقة من كائنات جافاسكربت ولكن يمكن التعامل معها من أي لغة برمجة أخرى تدعمها وقادرة على تحليل صيغتها، وهي تستخدم عادة في عمليات إرسال واستقبال البيانات من الواجهات البرمجية للتطبيقات API، ومن أسباب انتشارها صغر حجم البيانات عند إرسالها بهذه الصيغة مقارنة بالصيغ الأخرى مثل XML مثلًا، ومما يساعد في التعامل معها بكل سهولة هو توفر الأدوات لقراءة وتحليل هذه الصيغة. والآن نفتح ملف المثال json.js: nano json.js وبما أننا نريد إرسال البيانات بصيغة JSON لنعدل تابع معالجة الطلب requestListener()‎ ليعين قيمة الترويسة المناسبة لردود JSON كالتالي: ... const requestListener = function (req, res) { res.setHeader("Content-Type", "application/json"); }; ... يضيف التابع res.setHeader()‎ ترويسة HTTP إلى الرد توفر معلومات إضافية عن الطلب أو الرد المرسل، حيث يمرر له معاملين هما اسم الترويسة وقيمتها، حيث تصف قيمة الترويسة Content-Type صيغة البيانات أو نوع الوسائط media type المرفقة ضمن جسم الطلب، وفي حالتنا يجب تعيين قيمة الترويسة إلى application/json، ثم نعيد بعدها البيانات بصيغة JSON إلى المستخدم كالتالي: ... const requestListener = function (req, res) { res.setHeader("Content-Type", "application/json"); res.writeHead(200); res.end(`{"message": "This is a JSON response"}`); }; ... ضبطنا كما المثال السابق رمز الرد إلى القيمة 200 للدلالة على نجاح العملية، والفرق هنا أننا مررنا لتابع إرسال البيانات ضمن الرد response.end()‎ سلسلة نصية تحوي بيانات بصيغة JSON. والآن نحفظ الملف ونخرج منه ونشغل الخادم بتنفيذ الأمر التالي: node json.js ونفتح طرفية أخرى لتجربة إرسال طلب إلى الخادم باستخدام الأداة cURL كالتالي: curl http://localhost:8000 بعد إرسال الطلب والضغط على زر الإدخال ENTER نحصل على النتيجة التالية: {"message": "This is a JSON response"} نكون بذلك قد تعلمنا كيف يمكن إرسال رد يحوي بيانات بصيغة JSON مثل ما تفعل الواجهات البرمجية للتطبيقات API تمامًا. وبعد الاختبار نوقف الخادم بالضغط على الاختصار CTRL+C لنعود إلى سطر الأوامر مجددًا، حيث سنتعلم في الفقرة التالية كيف يمكن إرسال البيانات بصيغة CSV هذه المرة. إرسال البيانات بصيغة CSV شاع استخدام صيغة القيم المفصولة بفاصلة أو CSV عند التعامل مع البيانات المجدولة ضمن جداول، حيث يُفصل بين السجلات ضمن الجدول سطر جديد، وبين القيم على نفس السطر بفاصلة. والآن نفتح ملف المثال csv.js ضمن محرر النصوص ونعدل طريقة إرسال الطلب ضمن التابع requestListener()‎ كالتالي: ... const requestListener = function (req, res) { res.setHeader("Content-Type", "text/csv"); res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv"); }; ... نلاحظ كيف حددنا قيمة الترويسة Content-Type هذه المرة بالقيمة text/csv والتي تدل على أن البيانات المرسلة مكتوبة بصيغة CSV، وأضفنا هذه المرة ترويسة جديدة بالاسم Content-Disposition لتدل المتصفح على طريقة عرض البيانات المرسلة إليه، فإما أن تبقى ضمن المتصفح نفسه أو يتم حفظها في ملف خارجي، وحتى لو لم نعين قيمة للترويسة Content-Disposition فمعظم المتصفحات الحديثة ستُنزِّل البيانات وتحفظها ضمن ملف تلقائيًا في حال كانت بصيغة CSV، ويسمح تعيين قيمة لهذه الترويسة بتحديد اسم للملف الذي سيتم حفظه، والقيمة التي عيناها تخبر المتصفح أن البيانات المرسلة هي ملف مرفق بصيغة CSV يجب تنزيله وحفظه بالاسم oceanpals.csv. والآن لنرسل بيانات CSV ضمن الرد كالتالي: ... const requestListener = function (req, res) { res.setHeader("Content-Type", "text/csv"); res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv"); res.writeHead(200); res.end(`id,name,email\n1,Hassan Shark,shark@ocean.com`); }; ... حددنا كما العادة رمز الحالة 200 ضمن الرد للدلالة على نجاح العملية، ومررنا سلسلة نصية تحوي على بيانات بصيغة CSV إلى تابع إرسال البيانات res.end()‎، ونلاحظ كيف يفصل بين تلك القيم فواصل، وبين أسطر الجدول محرف ‎\n الذي يدل على سطر جديد، والبيانات التي أرسلناها تحوي سطران الأول فيه ترويسات الجدول والثاني يحوي البيانات الموافقة لها. والآن لنختبر عمل الخادم لذا نحفظ الملف ونخرج منه وننفذ أمر تشغيل الخادم كالتالي: node csv.js ونفتح طرفية أخرى لتجربة إرسال طلب إلى الخادم باستخدام الأداة cURL كالتالي: curl http://localhost:8000 يظهر لنا الرد التالي: id,name,email 1,Hassan Shark,shark@ocean.com إذا حاولنا الوصول للخادم من المتصفح عن طريق العنوان http://localhost:8000 نلاحظ كيف سيتم تنزيل ملف CSV المرسل وسيحدد تلقائيًا الاسم oceanpals.csv له. نوقف الخادم الآن لنعود إلى سطر الأوامر مجددًا. والآن بعد أن تعرفنا على طريقة إرسال البيانات بالصيغ JSON و CSV وهي أشيع الصيغ المستخدمة عند تطوير الواجهات البرمجية API، سنتعرف في الفقرة التالية على طريقة إرسال البيانات بحيث يمكن للمستخدم استعراضها ضمن المتصفح مباشرة. إرسال البيانات بصيغة HTML تعد لغة ترميز النصوص الفائقة HTML صيغة لترميز صفحات الويب والتي تتيح للمستخدم التفاعل مع الخادم مباشرةً من داخل المتصفح، ووظيفتها توصيف بنية محتوى الويب حيث تعتمد المتصفحات في عرضها لصفحات الويب على لغة HTML وعلى تنسيقها باستخدام CSS وهي تقنية أخرى من تقنيات الويب وظيفتها تجميل الصفحات وضبط طريقة عرضها. والآن نفتح ملف المثال لهذه الفقرة html.js ضمن محرر النصوص ونعدل طريقة إرسال الرد ضمن التابع requestListener()‎ بداية بتعيين قيمة مناسبة للترويسة Content-Type لتدل على صيغة HTML كالتالي: ... const requestListener = function (req, res) { res.setHeader("Content-Type", "text/html"); }; ... ونعيد بعدها البيانات بصيغة HTML إلى المستخدم بإضافة التالي: ... const requestListener = function (req, res) { res.setHeader("Content-Type", "text/html"); res.writeHead(200); res.end(`This is HTML`); }; ... كما العادة ضبطنا بداية رمز الحالة لرد HTTP، ثم أرسلنا بيانات بصيغة HTML بتمريرها كسلسلة نصية للتابع response.end()‎، وإذا اختبرنا الاتصال بالخادم عبر المتصفح ستظهر لنا صفحة HTML تحتوي على ترويسة بالنص "This is HTML". والآن نحفظ الملف ونخرج منه ونشغل الخادم لاختبار ذلك بتنفيذ الأمر التالي: node html.js نطلب بعد تشغيل الخادم عنوانه من المتصفح http://localhost:8000 لتظهر لنا الصفحة التالية: نوقف الخادم لنعود إلى سطر الأوامر مجددًا، وبذلك نكون تعلمنا طريقة إرسال صفحة HTML عبر كتابة محتواها يدويًا ضمن سلسلة نصية، ولكن عادة نخزن محتوى تلك الصفحات ضمن ملفات HTML منفصلة عن شيفرة الخادم، لذا سنتعرف في الفقرة التالية على طريقة تنفيذ ذلك. إرسال ملف صفحة HTML يمكن إرسال محتوى صفحات HTML عبر تمريرها مباشرة على شكل سلسلة نصية لتابع الإرسال كما فعلنا في الفقرة السابقة، ولكن يفضل تخزين محتوى صفحات HTML ضمن ملفات منفصلة وتخديم محتواها من قبل الخادم، حيث يمكن بذلك التعديل على محتواها بسهولة أكبر، ونكون قد فصلنا بذلك محتوى صفحات الويب عن شيفرات الخادم، وعملية الفصل هذه شائعة في معظم أطر العمل المشهورة لذا سيفيدنا معرفة الطريقة التي يتم بها تحميل وإرسال ملفات HTML. ولتخديم ملفات HTML من الخادم، يجب تحميل ملفاتها أولًا باستخدام الوحدة fs وكتابة محتوى الملف ضمن رد HTTP، لذا نُنشئ بداية ملف HTML الذي سيرسله الخادم كالتالي: touch index.html نفتح ملف الصفحة index.html ضمن محرر النصوص ونكتب صفحة HTML بسيطة تحتوي على خلفية باللون البرتقالي وعبارة ترحيب في المنتصف كالتالي: <!DOCTYPE html> <head> <title>My Website</title> <style> *, html { margin: 0; padding: 0; border: 0; } html { width: 100%; height: 100%; } body { width: 100%; height: 100%; position: relative; background-color: rgb(236, 152, 42); } .center { width: 100%; height: 50%; margin: 0; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-family: "Trebuchet MS", Helvetica, sans-serif; text-align: center; } h1 { font-size: 144px; } p { font-size: 64px; } </style> </head> <body> <div class="center"> <h1>Hello Again!</h1> <p>This is served from a file</p> </div> </body> </html> ستعرض الصفحة السابقة سطران هما "Hello Again!‎" و "This is served from a file"، في منتصف الصفحة فوق بعضهما بعضًا، والسطر الأول منها سيظهر بحجم خط أكبر من السطر الآخر، وستظهر النصوص باللون الأبيض وخلفية الصفحة باللون البرتقالي. والآن نحفظ الملف ونخرج منه ونعود إلى شيفرة الخادم حيث في هذا المثال سنستخدم الملف htmlFile.js الذي أعددناه سابقًا لتطوير الخادم، لذا نفتحه ضمن محرر النصوص ونبدأ أولًا باستيراد الوحدة fs بما أننا ننوي قراءة الملف السابق: const http = require("http"); const fs = require('fs').promises; ... سنستفيد من التابع readFile()‎ لتحميل محتوى ملف HTML، ونلاحظ كيف استوردنا نسخة التوابع التي تستعمل الوعود وذلك لتبسيط كتابة الشيفرات، حيث أنها أسهل بالقراءة من استخدام توابع رد النداء، والتي سيتم استيرادها افتراضيًا في حال استوردنا الوحدة fs فقط كالتالي require('fs')‎، ويمكنك الرجوع إلى المقال الخامس من هذه السلسلة للتعرف أكثر على البرمجة اللامتزامنة في جافاسكربت. والآن نبدأ بقراءة ملف HTML السابق عند وصول طلب من المستخدم، لهذا نعدل تابع معالجة الطلب requestListener()‎ كالتالي: ... const requestListener = function (req, res) { fs.readFile(__dirname + "/index.html") }; ... استدعينا التابع fs.readFile()‎ لتحميل الملف، ومررنا له القيمة ‎__dirname + "/index.html"‎ والتي يدل فيها المتغير الخاص ‎__dirname على المسار المطلق للمجلد الحاوي على ملف جافاسكربت الحالي، ونضيف إليه القيمة ‎/index.html للحصول على المسار المطلق الكامل لملف HTML الذي نريد إرساله، وبعد اكتمال تحميل الملف نضيف التالي: ... const requestListener = function (req, res) { fs.readFile(__dirname + "/index.html") .then(contents => { res.setHeader("Content-Type", "text/html"); res.writeHead(200); res.end(contents); }) }; ... سنرسل المحتوى الناتج عن نجاح تنفيذ الوعد الذي يعيده التابع fs.readFile()‎ أي قراءة الملف بنجاح كما فعلنا سابقًا وذلك ضمن التابع then()‎، حيث سيحتوي العامل contents على بيانات الملف بعد نجاح قراءته. وكما فعلنا سابقًا ضبطنا بدايةً قيمة الترويسة Content-Type إلى text/html للدلالة على إرسال محتوى بصيغة HTML، ثم ضبطنا رمز الحالة إلى 200 للدلالة على نجاح الطلب، ثم أرسلنا صفحة HTML التي حملناها إلى المستخدم وتحديدًا محتوى المتغير contents، لكن أحيانًا قد يفشل التابع fs.readFile()‎ في قراءة الملف لأي سبب كان، لذا يجب معالجة حالة الخطأ تلك بإضافة الشيفرة التالية ضمن التابع requestListener()‎: ... const requestListener = function (req, res) { fs.readFile(__dirname + "/index.html") .then(contents => { res.setHeader("Content-Type", "text/html"); res.writeHead(200); res.end(contents); }) .catch(err => { res.writeHead(500); res.end(err); return; }); }; ... نحفظ الملف ونخرج من محرر النصوص، ونلاحظ عندما يحدث خطأ ما أثناء تنفيذ الوعد سيتم رفضه، حيث يمكننا معالجة الخطأ باستخدام التابع catch()‎ والذي يُمرر إليه كائن الخطأ الذي يرميه استدعاء تابع قراءة الملف fs.readFile()‎، ونحدِّد فيه رمز حالة الرد بالقيمة 500 للدلالة على حدوث خطأ داخلي من طرف الخادم ونعيد الخطأ للمستخدم. والآن نشغل الخادم كالتالي: node htmlFile.js ونزور عنوانه http://localhost:8000 باستخدام المتصفح ستظهر لنا صفحة الويب كالتالي: وبذلك نكون قد أرسلنا صفحة HTML مُخزَّنة من ملف إلى المستخدم، والآن نوقف الخادم ونعود إلى الطرفية مجددًا. انتبه إلى أنَّ تحميل صفحة HTML بهذه الطريقة عند كل طلب HTTP يصل إلى الخادم يؤثر على الأداء، ومع أن الصفحة التي استخدمناها في مثالنا حجمها صغير وهو حوالي 800 بايت فقط، إلا أنه عند بناء التطبيقات قد يصل أحيانًا حجم الصفحات المستخدمة إلى رتبة الميجابايت، مما يؤدي لبطء في تحميلها وتخديمها للعميل، خصوصًا إذا كان من المتوقع ورود طلبات كثيرة إلى الخادم، لذا ولرفع الأداء يمكن تحميل محتوى الملفات مرة واحدة عند إقلاع الخادم وإرسال محتواها للطلبات الواردة، وبعد انتهاء عملية التحميل نخبر الخادم ببدء الاستماع للطلبات على العنوان المحدد له، وهذا ما سنتعلمه في الفقرة التالية حيث سنطور هذه الميزة في الخادم لرفع أداءه. رفع كفاءة تخديم صفحات HTML بدلًا من تحميل ملفات HTML عند كل طلب يرد إلى الخادم يمكننا تحميلها لمرة واحدة فقط في البداية، وبعدها نعيد تلك البيانات المخزنة مسبقًا لكل طلب سيرد لاحقًا إلى الخادم، لذلك نعود لملف المثال السابق htmlFile.js ونفتحه ضمن محرر النصوص ونضيف فيه متغيرًا جديدًا قبل إنشاء تابع معالجة الطلب requestListener()‎: ... let indexFile; const requestListener = function (req, res) { ... سيحتوي هذا المتغير على محتويات ملف HTML عند تشغيل الخادم، والآن نعدل على التابع requestListener()‎ وبدلًا من تحميل الملف داخله نعيد مباشرة محتوى المتغير indexFile: ... const requestListener = function (req, res) { res.setHeader("Content-Type", "text/html"); res.writeHead(200); res.end(indexFile); }; ... ونبدل مكان شيفرة تحميل الملف من داخل التابع requestListener()‎ إلى أعلى الملف في مكان إعداد الخادم ليصبح كالتالي: ... const server = http.createServer(requestListener); fs.readFile(__dirname + "/index.html") .then(contents => { indexFile = contents; server.listen(port, host, () => { console.log(`Server is running on http://${host}:${port}`); }); }) .catch(err => { console.error(`Could not read index.html file: ${err}`); process.exit(1); }); نلاحظ أن عملية قراءة الملف شبيهة جدًا بما نفذنا سابقًا، ولكن الفرق هنا أننا نحفظ بعد نجاح عملية قراءة الملف محتوياته ضمن المتغير العام indexFile، وبعد ذلك نشغل الخادم باستدعاء التابع listen()‎، حيث أن الخطوة الأساسية هي تحميل الملف لمرة واحدة قبل تشغيل الخادم، لنضمن بذلك أن التابع requestListener()‎ سيعيد محتوى الملف المخزن ضمن المتغير indexFile وأن قيمته ليست فارغة. وعدلنا أيضًا تابع معالجة الخطأ بحيث عند حدوث أي خطأ في عملية تحميل الملف سنطبع رسالة ضمن الطرفية توضح السبب ونخرج مباشرة من الخادم عبر استدعاء التابع exit()‎، وبذلك نستطيع معاينة سبب الخطأ الذي يمنع تحميل الملف ونعالج المشكلة أولًا ثم نعيد تشغيل الخادم بنجاح، فما الفائدة من تشغيل الخادم دون تحميل الملف المراد تخديمه. أنشأنا في الأمثلة السابقة عدة خوادم ويب تعيد كل منها المحتوى بصيغة مختلفة للمستخدم، ولم نستخدم حتى الآن أي بيانات من الطلب القادم إلى الخادم لتحديد ما يطلبه المستخدم تمامًا، حيث تفيدنا تلك البيانات في عملية التوجيه وإعداد عدة مسارات يستطيع خادم الويب الواحد تخديمها وهذا تمامًا ما سنتعلمه في الفقرة التالية. إدارة الوجهات Routes في الخادم معظم المواقع التي نزورها أو الواجهات البرمجية التي نتعامل معها تحوي عدة مسارات أو وجهات تسمح لنا بالوصول إلى عدد من الموارد على نفس الخادم، فمثلًا في نظام لإدارة الكتب في المكتبات على النظام أن يدير بيانات الكتب وبيانات أخرى مثل المؤلفين لهذه الكتب، وسيوفر خدمات أخرى مثل البحث والتصنيف، ومع أن بيانات الكتب والمؤلفين لها مرتبطة ببعضها لكن يمكن معاملتها كمَوردين مختلفين، وفي هذه الحالة يمكن أن نطور النظام ليخدّم كل نوع من تلك الموارد ضمن مسار محدد له، ليميز المستخدم الذي يتعامل مع الواجهة البرمجية API للنظام نوع المورد الذي يتعامل معه. لنطبق المثال ذاك ببناء خادم بسيط لنظام إدارة مكتبة سيحتوي على نوعين من البيانات، فعند طلب المستخدم المورد من المسار ‎/books سنرسل له قائمة بالكتب المتوفرة بصيغة JSON، أما عند طلب المسار ‎/authors سنرسل له قائمة بمعلومات حول المؤلفين بصيغة JSON أيضًا، ففي كل أمثلة خوادم الويب السابقة في هذا المقال كنا نرسل نفس الرد دومًا لكل الطلبات التي تصل إلى الخادم. لنختبر ذلك، علينا أولًا إرسال طلبات مختلفة للخادم ونلاحظ الرد المرسل على كل منها، لذا نعيد تشغيل خادم JSON الذي طورناه سابقًا بتنفيذ الأمر: node json.js وكالعادة في طرفية أخرى نرسل طلب HTTP باستخدام cURL كالتالي: curl http://localhost:8000 يعيد لنا الخادم الرد التالي: {"message": "This is a JSON response"} لنختبر الآن إرسال طلب على مسار مختلف للخادم كالتالي: curl http://localhost:8000/todos سنلاحظ ظهور نفس الرد السابق: {"message": "This is a JSON response"} ذلك لأن الخادم لا يعير اهتمامًا أبدًا عند معالجة الطلب داخل التابع requestListener()‎ للمسار الذي يطلبه المستخدم ضمن URL، لذا عندما أرسلنا طلبًا إلى المسار ‎/todos أعاد لنا الخادم نفس محتوى JSON الذي يعيده افتراضيًا، ولكن لبناء خادم نظام إدارة المكتبة يجب أن نفصل ونحدد نوع البيانات التي سنعيدها للمستخدم بناءً على المسار الذي يطلب الوصول إليه. والآن نوقف الخادم ونفتح الملف routes.js ونبدأ بتخزين بيانات JSON التي سيوفرها الخادم ضمن متغيرات قبل تعريف تابع معالجة الطلب requestListener()‎ كالتالي: ... const books = JSON.stringify([ { title: "The Alchemist", author: "Paulo Coelho", year: 1988 }, { title: "The Prophet", author: "Kahlil Gibran", year: 1923 } ]); const authors = JSON.stringify([ { name: "Paulo Coelho", countryOfBirth: "Brazil", yearOfBirth: 1947 }, { name: "Kahlil Gibran", countryOfBirth: "Lebanon", yearOfBirth: 1883 } ]); ... يحتوي المتغير books على سلسلة نصية بصيغة JSON فيها مصفوفة من الكائنات التي تمثل الكتب المتوفرة، ويحتوي كل كتاب منها على خاصية العنوان أو الاسم والمؤلف وسنة النشر، بينما يحتوي المتغير authors على سلسلة نصية بصيغة JSON أيضًا فيها مصفوفة من الكائنات التي تمثل المؤلفين ويملك كل مؤلف منها خاصية اسمه وبلد وسنة الولادة. وبعد أن جهزنا البيانات التي سنعيدها للمستخدم نبدأ بتعديل تابع معالجة الطلب requestListener()‎ ليعيد البيانات المناسبة منها بحسب المسار المطلوب، لذا نبدأ بتعيين قيمة الترويسة Content-Type لكل الطلبات التي سنرسلها، وبما أن جميع البيانات هي بصيغة JSON يمكننا تحديد قيمة الترويسة مباشرةً في البداية كالتالي: ... const requestListener = function (req, res) { res.setHeader("Content-Type", "application/json"); } ... والآن سنعيد بيانات JSON بحسب المسار المقابل ضمن عنوان URL الذي يحاول المستخدم طلبه، لذا نكتب تعليمة تبديل switch بحسب عنوان URL للطلب كالتالي: ... const requestListener = function (req, res) { res.setHeader("Content-Type", "application/json"); switch (req.url) {} } ... نلاحظ كيف يمكننا الوصول للمسار الذي يطلبه المستخدم من الخاصية url من كائن الطلب req، ونضيف بعدها حالات التوجيه للمسارات أو الوجهات المحددة ضمن تعليمة switch ونعيد بيانات JSON المناسبة لها، حيث توفر التعليمة switch في جافاسكربت طريقة للتحكم بالشيفرات التي ستنفَّذ بحسب القيمة أو التعبير البرمجي الممرر لها بين القوسين. والآن نضيف الحالة التي يطلب بها المستخدم قائمة الكتب باستخدام الكلمة case كالتالي: ... const requestListener = function (req, res) { res.setHeader("Content-Type", "application/json"); switch (req.url) { case "/books": res.writeHead(200); res.end(books); break } } ... نعين عندها رمز الحالة للطلب بالقيمة 200 للدلالة على نجاح الطلب ونعيد قيمة JSON الحاوية على قائمة الكتب المتاحة، ونضيف بعدها حالة case أخرى للرد على مسار طلب المؤلفين كالتالي: ... const requestListener = function (req, res) { res.setHeader("Content-Type", "application/json"); switch (req.url) { case "/books": res.writeHead(200); res.end(books); break case "/authors": res.writeHead(200); res.end(authors); break } } ... كما في الحالة السابقة نضبط أولًا رمز الحالة للرد بالقيمة 200 للدلالة على صحة الطلب، ونعيد قيمة JSON الحاوية على قائمة المؤلفين، وفي حال طلب المستخدم أي مسار آخر غير مدعوم سنرسل له خطأ، ولهذه الحالة يمكن إضافة الحالة الافتراضية default لالتقاط كل الحالات التي لا تطابق أي من الحالات المُعرّفة حيث نضبط فيها رمز الحالة إلى القيمة 404 للدلالة على أن المورد الذي يحاول المستخدم الوصول إليه غير موجود ونعيد رسالة خطأ للمستخدم ضمن كائن بصيغة JSON السابقة كالتالي: ... const requestListener = function (req, res) { res.setHeader("Content-Type", "application/json"); switch (req.url) { case "/books": res.writeHead(200); res.end(books); break case "/authors": res.writeHead(200); res.end(authors); break default: res.writeHead(404); res.end(JSON.stringify({error:"Resource not found"})); } } ... والآن لنشغل الخادم ونختبره من طرفية أخرى بإرسال طلب وصول إلى مسار الكتب المتاحة ونعاين الرد: curl http://localhost:8000/books لنحصل على الخرج: [{"title":"The Alchemist","author":"Paulo Coelho","year":1988},{"title":"The Prophet","author":"Kahlil Gibran","year":1923}] حصلنا على قائمة الكتب كما هو متوقع، وبالمثل نختبر مسار طلب المؤلفين ‎/authors كالتالي: curl http://localhost:8000/authors لنحصل على الخرج التالي: [{"name":"Paulo Coelho","countryOfBirth":"Brazil","yearOfBirth":1947},{"name":"Kahlil Gibran","countryOfBirth":"Lebanon","yearOfBirth":1883}] وأخيرًا نختبر الوصول إلى مسار غير مدعوم ونتأكد من أن تابع معالجة الطلب requestListener()‎ سيعيد لنا رسالة خطأ: curl http://localhost:8000/notreal سيعيد لنا الخادم رسالة الخطأ كالتالي: {"error":"Resource not found"} نوقف الخادم وبذلك نكون قد طورنا خادمًا يمكنه توجيه الطلب ضمن عدة مسارات مدعومة والرد عليها ببيانات مختلفة، وأضفنا إليه أيضًا ميزة إرسال رسالة خطأ عندما يحاول المستخدم الوصول لمسار غير مدعوم. ختامًا طورنا في هذا المقال عددًا من خوادم HTTP في بيئة نود، حيث بدأنا بإعادة نص بسيط ضمن الرد مرورًا بعدة أنواع من صيغ البيانات مثل JSON و CSV وصفحات HTML، وطورنا الخادم لتحميل صفحات HTML من ملفات خارجية مخصصة لها وتخديمها وإرسال محتواها إلى العميل، وأخيرًا طورنا واجهة برمجية API يمكنها الرد على طلب المستخدم بعدة أنواع من المعلومات بحسب معلومات من الطلب المُرسل للخادم. وبذلك تكون قد تعلمت طريقة إنشاء خوادم ويب يمكنها معالجة عدة أنواع من الطلبات والردود، والآن حاول مما تعلمت بناء خادم ويب يُخدّم عدة صفحات HTML للمستخدم بحسب المسارات المختلفة التي يطلبها، ويمكنك أيضًا بناء واجهة برمجة التطبيقات API الخاصة بك، ويمكنك الرجوع إلى التوثيق الرسمي للوحدة http من نود لتعلم المزيد عن خوادم الويب. ترجمة -وبتصرف- للمقال How To Create a Web Server in Node.js with the HTTP Module لصاحبه Stack Abuse. اقرأ أيضًا مقدمة إلى Node.js مدخل إلى خادم الويب مدخل إلى HTTP
  20. الاختبارات البرمجية Tests جزء مهم جدًا من عملية تطوير البرمجيات، وهي عبارة عن شيفرات برمجية مهمتها اختبار أجزاء التطبيق خلال مرحلة تطويره للتحقق من أدائها السليم خصوصًا بعد إضافة التطويرات والتعديلات عليه، ولتوفير الوقت عادة ما نؤتمت هذه الاختبارات إذ تمكننا سهولة هذه العملية من تنفيذ تلك الاختبارات باستمرار بعد كل إضافة لشيفرات جديدة على لتطبيق للتأكد من صحة تلك التغييرات وأن أي إضافة أو تعديل على جزء من الشيفرة لا تعطل عمل أي مزايا أخرى موجودة سابقًا، ما يمنح مطور التطبيق الثقة الكافية باعتماد التغييرات خصوصًا قبل مرحلة نشر التطبيق وإتاحته للاستخدام. ويأتي إطار عمل الاختبارات test framework لينظم طريقة إنشاء وتشغيل حالات الاختبار، ومن أشهر أطر عمل الاختبار تلك في جافاسكربت هو موكا Mocha، حيث تقتصر مهمته على إنشاء وتنظيم الاختبارات وليس تطبيق اختبارات التوكيد assertion testing على عمل الشيفرات، لذا لمطابقة القيم وتطبيق العديد من التوكيدات ضمن الاختبارات نستخدم وحدة برمجية أخرى يوفرها نود لنا افتراضيًا وهي assert. سنكتب في هذا المقال اختبارات لتطبيق قائمة مهام سنطوره ضمن بيئة نود، وسنُعد إطار عمل الاختبارات موكا Mocha له ونستخدمه لتنظيم الاختبارات، ثم سنستخدم الوحدة assert لكتابة تلك الاختبارات، أي سنستخدم Mocha لتخطيط الاختبارات والوحدة assert لتنفيذ التوكيدات ضمن الاختبار، وسيلزمك لتطبيق الأمثلة في هذا المقال تثبيت بيئة Node.js على جهازك، حيث سنستعمل في هذا المقال الإصدار رقم 10.16.0، وأيضًا معرفة بأساسيات لغة جافاسكربت. كتابة الوحدة البرمجية في نود نبدأ بكتابة وحدة برمجية سنختبرها لاحقًا وظيفتها إدارة قائمة من المهام وتوفر طريقة لاستعراض قائمة المهام التي نعمل عليها وإضافة مهام جديدة وتحديد المهام المكتملة منها، وستتيح أيضًا ميزة تصدير قائمة المهام هذه إلى ملف بصيغة CSV، وللتعرف أكثر على طرق كتابة وحدة برمجية باستخدام نود يمكنك مراجعة مقال إنشاء وحدات برمجية Modules في Node.js من هذه السلسلة. والآن نبدأ بتحضير بيئة العمل وننشئ مجلد باسم المشروع todos: mkdir todos ثم ندخل إلى المجلد: cd todos ونهيّئ ملف حزمة npm لاستخدامه لاحقًا لتنفيذ أوامر الاختبار: npm init -y سنحتاج لاعتمادية واحدة فقط وهي إطار عمل الاختبارات موكا Mocha لتنظيم وتشغيل الاختبارات التي سنكتبها لاحقًا، لذا ننفذ أمر تثبيتها ضمن المشروع كالتالي: npm i request --save-dev mocha نلاحظ أننا ثبتناها كاعتمادية تطوير لأننا لن نحتاج إليها في مرحلة الإنتاج بل ستستخدم خلال مرحلة التطوير فقط، ويمكنك التعرف أكثر على طرق إدارة الوحدات البرمجية في Node.js بمراجعة المقال الثالث من هذه السلسلة، والآن ننشئ الملف الأساسي لهذه الوحدة كالتالي: touch index.js ونفتحه ضمن أي محرر النصوص وليكن باستخدام محرر نانو nano: nano index.js نبدأ بتعريف الصنف Todos والذي سيحتوي على توابع سنستخدمها لإدارة قائمة المهام، لذا نضيف الأسطر التالية إلى ملف index.js: class Todos { constructor() { this.todos = []; } } module.exports = Todos; عرّفنا في بداية الملف الصنف Todos والتابع الباني له constructor()‎ بدون أي معاملات، حيث يمكننا إنشاء كائن جديد من هذا الصنف دون الحاجة لتمرير أي قيم له، ومهمته حاليًا إنشاء الخاصية todos وتعيين مصفوفة فارغة كقيمة لها، ثم صدّرنا هذا الصنف باستخدام الكائن modules في النهاية، كي تتمكن باقي الوحدات البرمجية من استيراد واستخدام الصنف Todos، فبدون ذلك لا يمكن لملف الاختبار الذي سنُنشئه لاحقًا استيراد واستخدام هذا الصنف، والآن نضيف تابعًا وظيفته إرجاع مصفوفة المهام المخزنة ضمن الكائن كالتالي: class Todos { constructor() { this.todos = []; } list() { return [...this.todos]; } } module.exports = Todos; يعيد التابع list()‎ نسخة من المصفوفة المخزنة ضمن الصنف باستخدام صيغة التفكيك في جافاسكربت لأن إعادة المتغير this.todos مباشرة يعني إعادة مؤشر إلى المصفوفة الأصلية ضمن الصنف Todos وبذلك نمنع الوصول إلى المصفوفة الأصلية وإجراء تعديلات عليها عن طريق الخطأ. ملاحظة: المصفوفات في جافاسكربت تُمرَّر بالمرجعية reference (وكذلك الكائنات objects أيضًا)، أي عند إسناد مصفوفة إلى متغير فإنه يحمل إشارة إلى تلك المصفوفة الأصلية وليس المصفوفة نفسها أي عند استعمال هذا المتغير لاحقًا أو تمريره كمعامل لتابع ما، فستشير جافاسكربت إلى المصفوفة الأصلية دومًا وستنعكس التعديلات عليها، فمثلًا إذا عند إنشاء مصفوفة تحوي ثلاث عناصر أسندناها إلى متغير x، ثم أنشأنا المتغير y وأسندنا له قيمة المصفوفة السابقة كالتالي y = x، فسيشير عندها كل من y و x إلى نفس المصفوفة وكل تغيير نقوم به على المصفوفة عن طريق المتغير y سيؤثر على المصفوفة التي يشير إليها المتغير x والعكس صحيح أي كلاهما يشيران إلى المصفوفة نفسها. والآن لنضيف التابع add()‎ ووظيفته إضافة مهمة جديدة إلى قائمة المهام الحالية: class Todos { constructor() { this.todos = []; } list() { return [...this.todos]; } add(title) { let todo = { title: title, completed: false, } this.todos.push(todo); } } module.exports = Todos; يأخذ التابع add()‎ معاملًا من نوع سلسلة نصية ويضعها ضمن خاصية العنوان title لكائن المهمة الجديدة، ويعين خاصية اكتمال هذه المهمة completed بالقيمة false افتراضيًا، ثم يضيف ذلك الكائن إلى مصفوفة المهام الحالية ضمن الكائن. ومن المهام الأخرى التي يجب أن يوفرها صنف مدير المهام هو تعيين مهمة كمهمة مكتملة، حيث سننفذ ذلك بالمرور على عناصر مصفوفة المهام todos والبحث عن عنصر المهمة التي يريد المستخدم تعيينها كمهمة مكتملة، وعند العثور عليها نعينها كمكتملة وإذا لم يُعثر عليها نرمي خطأ كإجراء احترازي، والآن نضيف هذا التابع الجديد complete()‎ كالتالي: class Todos { constructor() { this.todos = []; } list() { return [...this.todos]; } add(title) { let todo = { title: title, completed: false, } this.todos.push(todo); } complete(title) { let todoFound = false; this.todos.forEach((todo) => { if (todo.title === title) { todo.completed = true; todoFound = true; return; } }); if (!todoFound) { throw new Error(`No TODO was found with the title: "${title}"`); } } } module.exports = Todos; نحفظ الملف ونخرج من محرر النصوص، ونكون بذلك قد انتهينا من كتابة صنف مدير مهام بسيط سنستخدمه لاحقًا لتنفيذ الاختبارات عليه، حيث سنبدأ بالاختبارات اليدوية أولًا في الفقرة التالية لنتأكد من صحة عمله. اختبار الشيفرة يدويًا في هذه الفقرة سننفذ شيفرات التوابع السابقة لصنف إدارة المهام Todos يدويًا لنعاين ونتفحص خرجها ونتأكد من عملها كما هو متوقع منها أن تعمل، وتدعى هذه الطريقة بالاختبار اليدوي manual testing فهي أشيع طريقة يطبقها المطورون معظم الوقت حتى لو لكن يكن ذلك مقصودًا، وسنؤتمت لاحقًا تلك العملية باستخدام موكا Mocha لكن بدايةً سنختبر الشيفرات يدويًا لنتعرف على هذه الطريقة ونلاحظ ميزة استخدام إطار خاص لأتمتة الاختبارات. نبدأ بإضافة مهمتين جديدتين ونعيّن إحداهما كمكتملة، لذلك نبدأ جلسة REPL جديدة ضمن مجلد المشروع نفسه الحاوي على الملف index.js كالتالي: node ستلاحظ ظهور الرمز ‎>‎ في بداية السطر عند الدخول إلى وضع REPL التفاعلي، ويمكننا إدخال شيفرات جافاسكربت لتنفيذها كالتالي: const Todos = require('./index'); نحمل الوحدة البرمجية لمدير قائمة المهام باستخدام التابع require()‎ ونخزن قيمتها ضمن متغير بالاسم Todos، والذي صدرنا منه افتراضيًا الصنف Todos، والآن لنبدأ بإنشاء كائن جديد من ذلك الصنف كالتالي: const todos = new Todos(); يمكننا اختبار الوحدة باستخدام الكائن todos المشتق من الصنف Todos للتأكد من عمله وفق ما هو متوقع، فنبدأ بإضافة مهمة جديدة كالتالي: todos.add("run code"); لم يظهر إلى الآن مما نفذناه أي خرج ضمن الطرفية، ولنتأكد من تخزين المهمة السابقة بشكل سليم ضمن قائمة المهام نستدعي تابع عرض المهام الموجودة ونعاين النتيجة: todos.list(); سيظهر لنا الخرج التالي: [ { title: 'run code', completed: false } ] وهي النتيجة الصحيحة المتوقعة حيث تحتوي على عنصر وحيد وهو المصفوفة التي أضفناها سابقًا وحالة اكتمالها غير مكتملة، لنضيف الآن مهمة أخرى ونعدل المهمة الأولى لتصبح مكتملة كالتالي: todos.add("test everything"); todos.complete("run code"); نتوقع الآن وجود مهمتين ضمن الكائن todos، وهما "run code" و "test everything"، حيث يجب أن تكون المهمة الأولى "run code" مكتملة، ونتأكد من ذلك باستدعاء التابع list()‎: todos.list(); نحصل على الخرج: [ { title: 'run code', completed: true }, { title: 'test everything', completed: false } ] الخرج صحيح كما هو متوقع، والآن نخرج من جلسة REPL بتنفيذ الأمر التالي: .exit بذلك نكون قد تحققنا من عمل الوحدة البرمجية التي طورناها بشكل سليم، ولم نستخدم في ذلك أي ملفات اختبار مخصصة أو مكتبات اختبار، بل اعتمدنا فقط على الاختبار اليدوي، ولكن هذه الطريقة في الاختبار تأخذ وقتًا وجهدًا في كل مرة نضيف فيها تعديلات على الوحدة البرمجية، لذا سنؤتمت في الفقرة التالية عملية الاختبار هذه ونرى ما يمكن لإطار العمل موكا أن يساعدنا في ذلك. كتابة اختبارات Node.js باستخدام Mocha و Assert اختبرنا في الفقرة السابقة التطبيق يدويًا مع أن ذلك قد يفيد في بعض الحالات إلا أنه ومع تطوير الوحدة البرمجية التي نعمل عليها وزيادة حجمها والشيفرات المستخدمة ضمنها ستزداد تلك الطريقة صعوبة، وبينما نحن نختبر المزايا الجديدة التي أضفناها فيجب أيضًا اختبار المزايا السابقة جميعها مجددًا للتأكد أن التطويرات لم تؤثر على أي مزايا سابقة، وسيجبرنا ذلك اختبار كل ميزة ضمن التطبيق مرارًا وتكرارًا في كل مرة نعدل فيها الشيفرة ما سيأخذ الكثير من الوقت والجهد وقد نخطئ أو ننسى تنفيذ بعض الاختبارات خلال تلك المرحلة، والحل إعداد اختبارات مؤتمتة عبارة عن نصوص اختبار برمجية كأي برنامج عادي آخر، نمرر فيها بيانات محددة إلى التوابع ضمن التطبيق ونتأكد من سلامة عملها ووظيفتها كما هو متوقع، وكلما أضفنا ميزة للتطبيق أضفنا معها اختبارها، حيث عندما نكتب اختبارات مقابلة لكل ميزة في التطبيق سنتحقق بذلك من عمل الوحدة البرمجية كاملةً ودون حاجة لتذكر تنفيذ كل التوابع واختبار كل المزايا في كل عملية اختبار. وسنستخدم في كتابة الاختبارات إطار عمل مخصص للاختبار يدعى موكا Mocha مع وحدة assert البرمجية التي يوفرها نود كما أشرنا في بداية المقال، والآن نبدأ بإنشاء ملف جديد سنضع داخله شيفرات الاختبار كالتالي: touch index.test.js نفتحه ضمن أي محرر نصوص: nano index.test.js نبدأ بتحميل الوحدة البرمجية لمدير المهام كما فعلنا ضمن جلسة REPL في الفقرة السابقة، وبعدها نحمل الوحدة البرمجية assert لاستخدامها عند كتابة الاختبارات كالتالي: const Todos = require('./index'); const assert = require('assert').strict; تسمح الخاصية strict التي استخرجناها من الوحدة assert باستخدام معامل مساواة خاص منصوح باستخدامه ضمن بيئة نود ويوفر مزايا مفيدة أخرى لن ندخل في تفاصيلها. والآن قبل كتابة الاختبارات لنتعرف على طريقة موكا Mocha في تنظيم وترتيب شيفرات الاختبار، حيث تُكتب الاختبارات في Mocha بالصيغة التالية: describe([Test Group Name], function() { it([Test Name], function() { [Test Code] }); }); لاحظ وجود استدعاء لدالتين رئيسيين هما describe()‎ و it()‎، حيث تستخدم الدالة describe()‎ لتجميع الاختبارات المتشابهة معًا المكتوبة عبر it()‎، ولكن خطوة التجميع هذه غير ضرورية لكنها تسهل قراءة ملفات الاختبار وتزيد تنظيمها ويسهل لاحقًا التعديل على الاختبارات المتشابهة بسهولة أكبر، أما الدالة it()‎ فتحتوي على شيفرة الاختبار المراد تنفيذها للوحدة البرمجية المختبرة ونستخدم فيها مكتبة assert للتوكيد والتحقق من المخرجات. وهدفنا في هذه الفقرة استخدام موكا Mocha والوحدة assert لأتمتة عملية الاختبار أو حتى تنفيذها يدويًا كما فعلنا سابقًا، لذلك سنبدأ أولٌا بتعريف مجموعة اختبارات باستخدام التابع describe()‎ بإضافة الأسطر التالية لملف الاختبار بعد استيراد الوحدات البرمجية السابقة: ... describe("integration test", function() { }); بهذا نكون قد أنشأنا مجموعة اختبارات -سنكتبها لاحقًا- باسم integration test أي اختبار التكامل ووظيفته التحقق من عمل عدة توابع مع بعضها ضمن الوحدات البرمجية، على عكس اختبار الوحدة unit test الذي يختبر دالة واحدة في كل مرة، وعندما ينفذ موكا عملية اختبار التطبيق فسينفذ كل الاختبارات المعرفة ضمن التابع describe()‎ ضمن مجموعة بالاسم "integration test" التي عرفناها. والآن لنضيف اختبارًا باستخدام التابع it()‎ لاختبار جزء من التطبيق: ... describe("integration test", function() { it("should be able to add and complete TODOs", function() { }); }); نلاحظ كيف سمينا الاختبار باسم أجنبي واضح يصف عمله معناه بالعربية "يجب التمكن من إضافة مهمة ToDo وإكمالها"، لذا عند تنفيذ أي شخص للاختبارات سيعرف ما الجزء الذي نجح في الاختبار من تلك التي لم تنجح فيه، حيث يعتبر الاختبار الجيد لأي تطبيق توثيقًا جيدًا لعمله فتعتبر تلك الاختبارات كتوثيق تقني للتطبيق. والآن نبدأ بأول اختبار وهو إنشاء كائن من الصنف Todos جديد والتأكد بأنه لا يحتوي على أي عناصر: ... describe("integration test", function() { it("should be able to add and complete TODOs", function() { let todos = new Todos(); assert.notStrictEqual(todos.list().length, 1); }); }); أنشأنا في أول سطر من الاختبار كائنًا جديدًا من الصنف Todos كما فعلنا سابقًا ضمن REPL أو كما سنفعل عند استخدام هذا الصنف ضمن أي وحدة برمجية أخرى، واستخدمنا في السطر الثاني الوحدة assert وتحديدًا تابع اختبار عدم المساواة notStrictEqual()‎ والذي يأخذ معاملان وهما القيمة التي نريد اختبارها وتدعى القيمة الفعلية actual، والمعامل الثاني وهو القيمة التي نتوقع أن لا تساويها وتدعى القيمة المتوقعة expected، وفي حال تساوي القيمتين سيرمي التابع notStrictEqual()‎ خطئًا ويفشل هذا الاختبار. نحفظ الملف ونخرج منه، ونتوقع في هذه الحالة نجاح هذا الاختبار لأن طول المصفوفة سيكون 0 وهو غير مساوي للقيمة 1، ونتأكد من ذلك بتشغيل الاختبارات باستخدام موكا، لذا نعدل بدايةً على ملف الحزمة package.json ونفتحه ضمن محرر النصوص ونعدل النص البرمجي الخاص بتشغيل الاختبارات ضمن الخاصية scripts كالتالي: ... "scripts": { "test": "mocha index.test.js" }, ... بذلك نكون قد عدلنا الأمر test الخاص بالأداة npm، حيث عند تنفيذه كالتالي npm test سيتحقق npm من الأمر الذي أدخلناه ضمن ملف الحزمة package.json وسيبحث عن مكتبة موكا Mocha ضمن مجلد الحزم node_modules وينفذ الأمر mocha مُمرِّرًا له اسم ملف الاختبار للتطبيق. والآن نحفظ الملف ونخرج منه وننفذ أمر الاختبار السابق ونعاين النتيجة كالتالي: npm test نحصل على الخرج: > todos@1.0.0 test your_file_path/todos > mocha index.test.js integrated test ✓ should be able to add and complete TODOs 1 passing (16ms) يُظهر لنا الخرج السابق مجموعة الاختبارات التي جرى تنفيذها ويترك فراغًا قبل كل اختبار ضمن مجموعة الاختبار المعرفة، ونلاحظ ظهور اسم الاختبار كما مررناه للتابع it()‎ في ملف الاختبار، حيث تشير العلامة الظاهرة على يسار الاختبار أن الاختبار قد نجح، وفي الأسفل يظهر لنا خلاصة فيها معلومات عن كل الاختبارات التي نُفِّذت، وفي حالتنا هناك اختبار واحد ناجح واستغرقت عملية الاختبار كاملة 16 ميلي ثانية لتنفيذها، حيث يعتمد هذا التوقيت على أداء الجهاز الذي يُنفذ تلك الاختبارات. وكما لاحظنا أن الاختبارات التي نفذناها نجحت بالكامل ولكن مع ذلك فإن الاختبار الذي كتبناه قد يشير إلى حالة نجاح مغلوطة، وهي الحالة التي ينجح فيها الاختبار بينما في الحقيقة يجب أن يفشل، حيث أننا في هذا الاختبار نختبر فراغ مصفوفة المهام باختبار أن طولها لا يساوي الواحد، لذا نعدل الاختبار السابق ضمن ملف الاختبارات index.test.js ليصبح كالتالي: ... describe("integration test", function() { it("should be able to add and complete TODOs", function() { let todos = new Todos(); todos.add("get up from bed"); todos.add("make up bed"); assert.notStrictEqual(todos.list().length, 1); }); }); نحفظ الملف ونخرج منه ونلاحظ أننا أضفنا مهمتين جديدتين، لننفذ الاختبار ونلاحظ النتيجة: npm test سيظهر لنا التالي: ... integrated test ✓ should be able to add and complete TODOs 1 passing (8ms) نرى أن الاختبار قد نجح لأن طول المصفوفة ليس واحد كما هو متوقع، لكن ذلك يتعارض مع الاختبار السابق الذي أجريناه، حيث مهمته التحقق من أن الكائن الجديد من مدير المهام سيبدأ فارغًا دون أي مهام مخزنة ضمنه، لذا من الأفضل أن يتحقق الاختبار من ذلك في جميع الحالات. لنعدل الاختبار ونجعله ينجح فقط في حال عدم وجود أي مهام مخزنة ضمن الكائن ليصبح كالتالي: ... describe("integration test", function() { it("should be able to add and complete TODOs", function() { let todos = new Todos(); todos.add("get up from bed"); todos.add("make up bed"); assert.strictEqual(todos.list().length, 0); }); }); لاحظ استدعينا التابع strictEqual()‎ بدلًا من استدعاء التابع notStrictEqual()‎ الذي يتحقق من المساواة بين القيمة الحقيقية والمتوقعة الممررة له، بحيث يفشل عند عدم تساوي القيمتين. والآن نحفظ الملف ونخرج منه ونعيد تنفيذ أمر الاختبار لنرى النتيجة: npm test هذه المرة سيظهر لنا خطأ: ... integration test 1) should be able to add and complete TODOs 0 passing (16ms) 1 failing 1) integration test should be able to add and complete TODOs: AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B: + expected - actual - 2 + 0 + expected - actual -2 +0 at Context. (index.test.js:9:10) npm ERR! Test failed. See above for more details. سيفيدنا الخرج الظاهر في معرفة سبب الفشل وتصحيح الخطأ الحاصل، ونلاحظ عدم وجود علامة بجانب اسم الاختبار لأنه فشل، وأيضًا لم تعد خلاصة تنفيذ عملية الاختبار في الأسفل بل في الأعلى بعد قائمة الاختبارات المنفذة وحالتها: ... 0 passing (29ms) 1 failing ... والخرج الباقي يظهر بيانات متعلقة بالاختبارات الفاشلة، حيث يظهر أولًا الاختبارات التي فشلت: ... 1) integrated test should be able to add and complete TODOs: ... ثم سبب فشل تلك الاختبارات: ... AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B: + expected - actual - 2 + 0 + expected - actual -2 +0 at Context. (index.test.js:9:10) ... رُمي خطأ من النوع AssertionError عندما فشل اختبار التابع strictEqual()‎، حيث نلاحظ أن القيمة المتوقعة وهي 0 مختلفة عن القيمة الحقيقية لطول مصفوفة المهام وهي 2، ونلاحظ ذكر السطر الذي فشل عنده الاختبار ضمن ملف الاختبار وهو السطر رقم 10، وتفيد هذه المعلومات في حل المشكلة. نعدل الاختبار ونصحح المشكلة بتوقع القيمة الصحيحة لطول المصفوفة حتى لا يفشل الاختبار، وأولًا نفتح ملف الاختبارات: nano index.test.js ثم نزيل أسطر إضافة المهام باستخدام todos.add ليصبح الاختبار كالتالي: ... describe("integration test", function () { it("should be able to add and complete TODOs", function () { let todos = new Todos(); assert.strictEqual(todos.list().length, 0); }); }); نحفظ الملف ونخرج منه ونعيد تنفيذ الاختبار مجددًا ونتأكد من نجاحه في حالة صحيحة هذه المرة وليست مغلوطة: npm test نحصل على الخرج: ... integration test ✓ should be able to add and complete TODOs 1 passing (15ms) أصبح الاختبار الآن أقرب لما نريد، لنعود إلى اختبار التكامل مجددًا ونحاول اختبار إضافة مهمة جديدة ضمن الملف index.test.js كالتالي: ... describe("integration test", function() { it("should be able to add and complete TODOs", function() { let todos = new Todos(); assert.strictEqual(todos.list().length, 0); todos.add("run code"); assert.strictEqual(todos.list().length, 1); assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]); }); }); بعد استدعاء التابع add()‎ نتحقق من وجود مهمة واحدة ضمن كائن مدير المهام todos باستخدام تابع التوكيد strictEqual()‎، وأما الاختبار التالي فسيتحقق من البيانات الموجودة ضمن قائمة المهام todos بواسطة التابع deepStrictEqual()‎ والذي يختبر مساواة القيمة المتوقعة مع القيمة الحقيقية تعاوديًا بالمرور على كل الخصائص ضمن من القيمتين واختبار مساواتهما، ففي حالتنا سيختبر أن المصفوفتين يملك كل منها كائنًا واحدًا داخلها، ويتحقق من امتلاك كلا الكائنين لنفس الخواص وتساويها ففي حالتنا يجب أن يكون هنالك خاصيتين الأولى العنوان title ويجب أن تساوي قيمتها "run code"والثاني اكتمال المهمة completed وقيمتها تساوي false. نكمل كتابة الاختبار ليصبح كالتالي: ... describe("integration test", function() { it("should be able to add and complete TODOs", function() { let todos = new Todos(); assert.strictEqual(todos.list().length, 0); todos.add("run code"); assert.strictEqual(todos.list().length, 1); assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]); todos.add("test everything"); assert.strictEqual(todos.list().length, 2); assert.deepStrictEqual(todos.list(), [ { title: "run code", completed: false }, { title: "test everything", completed: false } ] ); todos.complete("run code"); assert.deepStrictEqual(todos.list(), [ { title: "run code", completed: true }, { title: "test everything", completed: false } ] ); }); }); أصبح الاختبار الآن مماثل تمامًا للاختبار اليدوي الذي نفذناها سابقًا، ولم نعد بحاجة للتحقق من الخرج يدويًا في كل مرة، فيكفي تنفيذ هذا الاختبار والتأكد من نجاحه ليدل على صحة عمل الوحدة البرمجية، حيث الهدف من الاختبار في النهاية التأكد من سلامة عمل الوحدة البرمجية كلها. والآن نحفظ الملف ونخرج منه وننفذ الاختبارات مرة أخرى ونتحقق من النتيجة: ... integrated test ✓ should be able to add and complete TODOs 1 passing (9ms) أعددنا بذلك اختبار تكامل باستخدام إطار الاختبارات موكا Mocha والوحدة assert. والآن لنتخيل بأننا شاركنا الوحدة البرمجية السابقة مع مطورين آخرين وأخبرنا العديد منهم بأنه يفضل رمي خطأ عند استدعاء التابع complete()‎ في حال لم يتم إضافة أي مهام بعد سابقًا، لذا لنضيف تلك الخاصية ضمن التابع complete()‎ ضمن الملف index.js: ... complete(title) { if (this.todos.length === 0) { throw new Error("You have no TODOs stored. Why don't you add one first?"); } let todoFound = false this.todos.forEach((todo) => { if (todo.title === title) { todo.completed = true; todoFound = true; return; } }); if (!todoFound) { throw new Error(`No TODO was found with the title: "${title}"`); } } ... نحفظ الملف ونخرج منه ثم نضيف اختبارًا جديدًا لتلك الميزة في ملف الاختبارات، حيث نريد التحقق من أن استدعاء التابع complete من كائن لا يحوي أي مهام بعد سيعيد الخطأ الخاص بحالتنا، لذا نعود لملف الاختبار ونضيف في نهايته الشيفرة التالية: ... describe("complete()", function() { it("should fail if there are no TODOs", function() { let todos = new Todos(); const expectedError = new Error("You have no TODOs stored. Why don't you add one first?"); assert.throws(() => { todos.complete("doesn't exist"); }, expectedError); }); }); استخدما التوابع describe()‎ و it()‎ كما فعلنا سابقًا، وبدأنا الاختبار بإنشاء كائن todos جديد، ثم عرّفنا الخطأ المتوقع عند استدعاء التابع complete()‎ واستخدمنا تابع توكيد رمي الأخطاء throws()‎ الذي توفره الوحدة assert لاختبار الأخطاء المرمية من قبل الشيفرة عند تنفيذها، حيث نمرر له كمعامل أول تابعًا يحتوي داخله على التابع الذي نتوقع منه رمي الخطأ، والمعامل الثاني هو الخطأ المتوقع رميه، والآن ننفذ أمر الاختبار npm test ونعاين النتيجة: ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs 2 passing (25ms) يتضح من الخرج السابق أهمية أتمتة الاختبارات باستخدام موكا والوحدة assert، حيث أنه وعند كل تنفيذ لأمر الاختبار npm test سيتم التحقق من نجاح كل الاختبارات السابقة، ولا حاجة لتكرار التحقق اليدوي أبدًا طالما أن الشيفرات الأخرى لا زالت تعمل وهذا ما تأكدنا منه عند نجاح بقية الاختبارات. وإلى الآن كل ما اختبرناه كان عبارة عن شيفرات متزامنة، وفي الفقرة التالية سنتعلم طرق اختبار والتعامل مع الشيفرات اللامتزامنة. اختبار الشيفرات اللامتزامنة سنضيف الآن ميزة تصدير قائمة المهام إلى ملف بصيغة CSV التي ذكرناها سابقًا، حيث سيحوي ذلك الملف كل المهام المخزنة مع تفاصيل حالة اكتمالها، لذا وللتعامل مع نظام الملفات سنحتاج لاستخدام وحدة fs التي توفرها نود لكتابة ذلك الملف. والجدير بالذكرأن عملية كتابة الملف عملية غير متزامنة ويمكن تنفيذها بعدة طرق كاستخدام دوال رد النداء callbacks مثلًا أو الوعود Promises أو عبر اللاتزامن والانتظار async/await كما رأينا في المقال السابق. سنتعلم في هذه الفقرة كيف يمكن كتابة الاختبارات للشيفرات اللامتزامنة التي تستخدم أي طريقة من تلك الطرق. الاختبار باستخدام دوال رد النداء تمُرر دالة رد النداء كمعامل إلى التابع اللامتزامن لتُستدعى عند انتهاء مهمة ذلك التابع، لنبدأ بإضافة التابع saveToFile()‎ للصنف Todos والذي سيمر على عناصر المهام ضمن الصنف ويبنى منها سلسلة نصية ويخزنها ضمن ملف بصيغة CSV، لذا نعود إلى ملف index.js ونضيف الشيفرات المكتوبة في نهايته: const fs = require('fs'); class Todos { constructor() { this.todos = []; } list() { return [...this.todos]; } add(title) { let todo = { title: title, completed: false, } this.todos.push(todo); } complete(title) { if (this.todos.length === 0) { throw new Error("You have no TODOs stored. Why don't you add one first?"); } let todoFound = false this.todos.forEach((todo) => { if (todo.title === title) { todo.completed = true; todoFound = true; return; } }); if (!todoFound) { throw new Error(`No TODO was found with the title: "${title}"`); } } saveToFile(callback) { let fileContents = 'Title,Completed\n'; this.todos.forEach((todo) => { fileContents += `${todo.title},${todo.completed}\n` }); fs.writeFile('todos.csv', fileContents, callback); } } module.exports = Todos; بداية نستورد الوحدة fs ثم نضيف التابع الجديد saveToFile()‎ إلى الصنف والذي يقبل كمعامل له دالة رد نداء تُستدعَى عند انتهاء عملية كتابة الملف، ونُنشئ ضمن التابع الجديد محتوى الملف ونخزنه ضمن المتغير fileContents، ونلاحظ كيف عيّنا القيمة الابتدائية له وهي عناوين الأعمدة للجدول في ملف CSV، ومررنا على كل مهمة مخزنة ضمن المصفوفة باستخدام التابع forEach()‎ وأضفنا لكل مهمة قيمة خاصية العنوان لها title وحالة الاكتمال completed، ثم استدعينا التابع writeFile()‎ من وحدة fs لكتابة الملف النهائي، ومررنا له اسم الملف الناتج todos.csv وكمعامل ثانِ مررنا محتوى ذلك الملف وهو قيمة المتغير fileContents السابق، وآخر معامل هو دالة رد النداء لمعالجة الخطأ الذي قد يحدث عند تنفيذ هذه العملية. والآن نحفظ الملف ونخرج منه ونكتب اختبارًا للتابع الجديد saveToFile()‎ يتحقق من وجود الملف المصدَّر ثم يتحقق من صحة محتواه، لذا نعود لملف الاختبار index.test.js ونبدأ بتحميل الوحدة fs في بداية الملف والتي سنستخدمها في عملية الاختبار: const Todos = require('./index'); const assert = require('assert').strict; const fs = require('fs'); ... ونضيف حالة الاختبار الجديدة في نهاية الملف: ... describe("saveToFile()", function() { it("should save a single TODO", function(done) { let todos = new Todos(); todos.add("save a CSV"); todos.saveToFile((err) => { assert.strictEqual(fs.existsSync('todos.csv'), true); let expectedFileContents = "Title,Completed\nsave a CSV,false\n"; let content = fs.readFileSync("todos.csv").toString(); assert.strictEqual(content, expectedFileContents); done(err); }); }); }); وبما أن هذا اختبار لميزة جديدة كليًا عن سابقاتها استخدمنا الدالة describe()‎ لتعريف مجموعة اختبارات جديدة متعلقة بهذه الميزة، ونلاحظ هذه المرة استخدام الدالة it()‎ بطريقة مختلفة، حيث نمرر لها عادةً دالة رد نداء تحوي داخلها الاختبار دون تمرير أي معامل لها، ولكن هذه المرة سنمرر لدالة رد النداء المعامل done والذي نحتاج إليه عند تنفيذ اختبار شيفرات لا متزامنة وخصوصًا التي تستخدم في عملها دوال رد النداء، حيث نستخدم دالة رد النداء done()‎ لإعلام موكا عند الانتهاء من اختبار عملية غير متزامنة، لهذا يجب علينا التأكد بعد اختبار دوال رد النداء استدعاء done()‎ حيث بدون ذلك الاستدعاء لن يعلم موكا أن الاختبار قد انتهى وسيبقى منتظرًا إشارة الانتهاء تلك. ونلاحظ أننا أنشأنا كائنًا جديدًا من الصنف Todos وأضفنا مهمة جديدة له بعدها استدعينا التابع الجديد saveToFile()‎ ومررنا له دالة لفحص كائن الخطأ الذي سيمرر لها إن وجد، ونلاحظ كيف وضعنا الاختبار لهذا التابع ضمن دالة رد النداء، لأن الاختبار سيفشل إن أجري قبل عملية كتابة الملف. أول توكيد تحققنا منه هو أن الملف todos.csv موجود: ... assert.strictEqual(fs.existsSync('todos.csv'), true); ... حيث يعيد التابع fs.existsSync()‎ القيمة الصحيحة true إذا كان الملف المحدد بالمسار الممر له موجودًا وإلا سيعيد قيمة خاطئة false. ملاحظة: يرجع العمل أن توابع الوحدة fs غير متزامنة افتراضيًا، ويوجد لبعض التوابع الأساسية منها نسخ متزامنة استخدمناها هنا لتبسيط الاختبار ويمكننا الاستدلال على تلك التوابع المتزامنة من اللاحقة "Sync" في نهاية اسمها، فلو استخدمنا النسخة المتزامنة ومررنا لها دالة رد نداء أيضًا فستصبح الشيفرة متداخلة وصعبة القراءة أو التعديل. أنشأنا بعد ذلك متغيرًا يحوي القيمة المتوقعة للملف todos.csv: ... let expectedFileContents = "Title,Completed\nsave a CSV,false\n"; ... واستخدمنا التابع المتزامن readFileSync()‎ من الوحدة fs لقراءة محتوى الملف كالتالي: ... let content = fs.readFileSync("todos.csv").toString(); ... حيث مررنا للتابع readFileSync()‎ مسار الملف todos.csv الذي جرى تصديره، وسيعيد لنا كائن تخزين مؤقت Buffer سيحوي بيانات الملف بالصيغة الثنائية، لذا نستدعي التابع toString()‎ منه للحصول على القيمة النصية المقروءة لتلك البيانات لمقارنتها مع القيمة المتوقعة لمحتوى الملف التي أنشأناها مسبقًا، ونستخدم لمقارنتهما تابع اختبار المساواة strictEqual من الوحدة assert: ... assert.strictEqual(content, expectedFileContents); ... وأخيرًا نستدعي التابع done()‎ لإعلام موكا بانتهاء الاختبار: ... done(err); ... نلاحظ كيف مررنا كائن الخطأ err عند استدعاء تابع الانتهاء done()‎ حيث سيفحص موكا تلك القيمة وسيفشل الاختبار إن احتوت على خطأ. والآن نحفظ الملف ونخرج منه وننفذ الاختبارات بتنفيذ التابع npm test كما العادة ونلاحظ النتيجة: ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (15ms) بذلك نكون قد اختبرنا تابعًا غير متزامنًا يستخدم دالة رد النداء، وبما أن تلك الطريقة لم تعد مستخدمة كثيرًا في وقتنا الحالي وتم استبدالها باستخدام الوعود كما شرحنا في المقال السابق من هذه السلسلة، سنتعلم في الفقرة القادمة كيف يمكن اختبار الشيفرات التي تستخدم الوعود في تنفيذ عملياتها اللامتزامنة باستخدام موكا. الاختبار باستخدام الوعود الوعد Promise هو كائن توفره جافاسكربت وظيفته إرجاع قيمة ما لاحقًا، وعندما تنفذ عمليته بنجاح نقول تحقق ذلك الوعد resolved، وفي حال حدث خطأ في تنفيذ عمليته نقول أنه قد فشل rejected. لنبدأ بتعديل التابع saveToFile()‎ ليستخدم الوعود بدلًا من دوال رد النداء، نفتح ملف index.js ونبدأ بتعديل طريقة استيراد الوحدة fs، حيث نعدل على عبارة الاستيراد باستخدام require()‎ لتصبح كالتالي: ... const fs = require('fs').promises; ... بذلك نكون قد استوردنا وحدة fs التي تستخدم الوعود بدلًا من التي تستخدم دوال رد النداء، ثم نعدل التابع saveToFile()‎ ليستخدم الوعود بشكل سليم كالتالي: ... saveToFile() { let fileContents = 'Title,Completed\n'; this.todos.forEach((todo) => { fileContents += `${todo.title},${todo.completed}\n` }); return fs.writeFile('todos.csv', fileContents); } ... نلاحظ أن التابع لم يعد يقبل معاملًا له، حيث يغنينا استخدام الوعود عن ذلك، ونلاحظ أيضًا تغيير طريقة كتابة التابع حيث نرجع منه الوعد الذي يرجعه التابع writeFile()‎. والآن نحفظ التغييرات على ملف index.js ثم نعدل على اختبار هذا التابع ليلائم استخدامه للوعود، لذا نعود لملف الاختبار index.test.js ونبدل اختبار التابع saveToFile()‎ ليصبح كالتالي: ... describe("saveToFile()", function() { it("should save a single TODO", function() { let todos = new Todos(); todos.add("save a CSV"); return todos.saveToFile().then(() => { assert.strictEqual(fs.existsSync('todos.csv'), true); let expectedFileContents = "Title,Completed\nsave a CSV,false\n"; let content = fs.readFileSync("todos.csv").toString(); assert.strictEqual(content, expectedFileContents); }); }); }); أول تعديل أجريناه هو إزالة معامل تابع الانتهاء done()‎ لأن بقاءه يعني انتظار موكا إشارة استدعائه حتى ينهي الاختبار وإلا سيرمي خطأ كالتالي: 1) saveToFile() should save a single TODO: Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/ubuntu/todos/index.test.js) at listOnTimeout (internal/timers.js:536:17) at processTimers (internal/timers.js:480:7) لهذا السبب عندما نستخدم الوعود ضمن الاختبار لا نمرر المعامل done()‎ إلى دالة رد النداء المُمررة لدالة تعريف الاختبار it()‎. ولاختبار الوعد نضع اختبارات التوكيدات ضمن استدعاء التابع then()‎، ونلاحظ كيف أننا نرجع الوعد من داخل تابع الاختبار وأننا لا نضيف استدعاء للتابع catch()‎ إليه لالتقاط الخطأ الذي قد يُرمى أثناء التنفيذ، وذلك حتى تصل أي أخطاء ترمى من داخل التابع then()‎ إلى الدالة الأعلى وتحديدًا إلى it()‎، حتى يعلم موكا بحدوث أخطاء أثناء التنفيذ وإفشال الاختبار الحالي، لذلك ولاختبار الوعود يجب أن نعيد الوعد المراد اختباره باستخدام return، وإلا سيظهر الاختبار على أنه ناجح حتى عند فشله في الحقيقة، ونحصل على نتيجة صحة مغلوطة، وأيضًا نتجاهل إضافة التابع catch()‎ لأن موكا يتحقق من الأخطاء المرمية بنفسه للتأكد من حالة فشل الوعد الذي يجب أن يؤدي بالمقابل إلى فشل الاختبار الذي يعطينا فكرة عن وجود مشكلة في عمل وحدة التطبيق. والآن وبعد أن عدلنا الاختبار نحفظ الملف ونخرج منه، وننفذ الأمر npm test لتنفيذ الاختبارات والتأكد من نجاحها: ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (18ms) بذلك نكون قد عدلنا على الشيفرة والاختبار المتعلق بها لتستخدم الوعود، وتأكدنا من أن الميزة لا زالت تعمل بشكل صحيح، والآن بدلًا من التعامل مع الوعود بتلك الطريقة سنستخدم في الفقرة التالية اللاتزامن والانتظار async/await لتبسيط العملية وإلغاء الحاجة لاستدعاء التابع then()‎ أكثر من مرة لمعالجة حالات نجاح التنفيذ ولتبسيط شيفرة الاختبار وتوضيحها. الاختبار باستخدام اللاتزامن والانتظار async/await تتيح الكلمتان المفتاحيتان async/await صيغة بديلة للتعامل مع الوعود، فعند تحديد تابع ما كتابع لا متزامن باستخدام الكلمة المفتاحية async يصبح بإمكاننا الحصول داخله مباشرةً على قيمة نتيجة أي وعد ننفذه عند نجاحه باستخدام الكلمة المفتاحية await قبل استدعاء الوعد، وبذلك نلغي الحاجة لاستدعاء التوابع then()‎ أو catch()‎ نهائيًا، وباستخدامها يمكننا تبسيط اختبار التابع saveToFile()‎ الذي يستخدم الوعود، لذا نعدله ضمن ملف الاختبارات index.test.js ليصبح كالتالي: ... describe("saveToFile()", function() { it("should save a single TODO", async function() { let todos = new Todos(); todos.add("save a CSV"); await todos.saveToFile(); assert.strictEqual(fs.existsSync('todos.csv'), true); let expectedFileContents = "Title,Completed\nsave a CSV,false\n"; let content = fs.readFileSync("todos.csv").toString(); assert.strictEqual(content, expectedFileContents); }); }); نلاحظ كيف أضفنا الكلمة async قبل تعريف دالة رد النداء المُمرر إلى it()‎، ما يسمح لنا باستخدام الكلمة await داخلها، ونلاحظ عند استدعاء التابع saveToFile()‎ إضافة الكلمة await قبل استدعائه بذلك لن يكمل نود تنفيذ الشيفرات في الأسطر اللاحقة وسينتظر لحين انتهاء تنفيذ هذا التابع، ونلاحظ أيضًا كيف أصبحت شيفرة الاختبار أسهل في القراءة بعد أن نقلنا شيفرات التوكيد من داخل التابع then()‎ مباشرة إلى جسم تابع الاختبار المُمرر إلى it()‎. والآن ننفذ الاختبارات بتنفيذ الأمر npm test لنحصل على الخرج: ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (30ms) بذلك نكون قد تعلمنا كيف يمكن اختبار التوابع اللامتزامنة مهما كان شكلها، سواء كانت تستخدم دوال رد النداء في عملها أو الوعود، وتعلمنا الكثير عن إطار عمل الاختبار موكا Mocha وكيفية استخدامه لاختبار التوابع اللامتزامنة، وسنتعرف في الفقرة التالية على خصائص أخرى يوفرها موكا Mocha ستحسن من طريقة كتابة الاختبارات، وسنتعرف تحديدًا على الخطافات hooks وكيف يمكنها التعديل على بيئة الاختبار. تحسين الاختبارات باستخدام الخطافات Hooks تسمح الخطافات في موكا Mocha بإعداد بيئة الاختبار قبل وبعد تنفيذ الاختبارات، حيث نستخدمها داخل التابع describe()‎ عادةً وتحوي على شيفرات تفيد في عملية الإعداد والتنظيف التي قد تحتاجها بعض الاختبارات، حيث يوفر موكا أربع خطافات رئيسية وهي: before: يُنفذ مرة واحدة قبل أول اختبار فقط. beforeEach: يُنفذ قبل كل اختبار. after: يُنفذ بعد تنفيذ آخر اختبار فقط. afterEach: يُنفذ بعد كل اختبار. تفيد تلك الخطافات عند اختبار تابع ما ضمن عدة اختبارات، وتسمح بفصل شيفرة الإعداد لها إلى مكان واحد منفصل عن مكان شيفرات التوكيد، كإنشاء الكائن todos في حالتنا مثلًا، ولنختبر فائدتها سنبدأ أولًا بإضافة اختبارات جديدة لمجموعة اختبارات التابع saveToFile()‎، فبعد أن تحققنا في الاختبار الماضي من صحة تصدير ملف المهام إلا أننا اختبرنا وجود مهمة واحدة فقط ضمنه، ولم نختبر الحالة التي تكون فيها المهمة مكتملة وهل سيتم حفظها ضمن الملف بشكل سليم أم لا، لذلك سنضيف اختبارات جديدة للتأكد من تلك الحالات وبالتالي التأكد من صحة عمل الوحدة البرمجية التي نطورها. لنبدأ بإضافة اختبار ثانِ للتأكد من حفظ المهام المكتملة بشكل سليم، لذا نفتح الملف index.test.js ضمن محرر النصوص ونضيف الاختبار الجديد كالتالي: ... describe("saveToFile()", function () { it("should save a single TODO", async function () { let todos = new Todos(); todos.add("save a CSV"); await todos.saveToFile(); assert.strictEqual(fs.existsSync('todos.csv'), true); let expectedFileContents = "Title,Completed\nsave a CSV,false\n"; let content = fs.readFileSync("todos.csv").toString(); assert.strictEqual(content, expectedFileContents); }); it("should save a single TODO that's completed", async function () { let todos = new Todos(); todos.add("save a CSV"); todos.complete("save a CSV"); await todos.saveToFile(); assert.strictEqual(fs.existsSync('todos.csv'), true); let expectedFileContents = "Title,Completed\nsave a CSV,true\n"; let content = fs.readFileSync("todos.csv").toString(); assert.strictEqual(content, expectedFileContents); }); }); يشبه هذا الاختبار ما سبقه، والفرق الوحيد هو استدعاء التابع complete()‎ قبل تصدير الملف باستخدام التابع saveToFile()‎، وأيضًا اختلاف محتوى الملف المتوقع ضمن المتغير expectedFileContents حيث يحوي القيمة true بدلًا من false عند حقل حالة الاكتمال للمهمة completed. والآن نحفظ الملف ونخرج منه وننفذ الاختبارات بتنفيذ الأمر: npm test سيظهر لنا التالي: ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO ✓ should save a single TODO that's completed 4 passing (26ms) نجحت الاختبارات كما هو متوقع، لكن يمكننا تحسين طريقة كتابتها وتبسيطها أكثر، حيث نلاحظ أن كلًا من الاختبارين يحتاجان إلى إنشاء كائن من الصنف Todos في بداية الاختبار، وفي حال إضافة اختبارات جديدة نحتاج لإنشاء هذا الكائن أيضًا لذا ستتكرر تلك العملية كثيرًا ضمن الاختبارات، وسينتج عن تنفيذ تلك الاختبارات في كل مرة ملفًا جديدًا يتم تصديره وحفظه. وقد يظن المستخدم لهذه الوحدة خطأً أن هذا الملف هو ملف مهام حقيقية وليس ملف ناتج عن عملية الاختبار، ولحل تلك المشكلة يمكننا حذف الملفات الناتجة بعد انتهاء الاختبار مباشرةً باستخدام الخطافات تلك، حيث نستفيد من الخطاف beforeEach()‎ لإعداد المهام قبل اختبارها، وهنا ضمن هذا الخطاف نُعد ونحضر عادةً أي بيانات سنستخدمها داخل الاختبارات، ففي حالتنا نريد إنشاء الكائن todos وبداخله مهمة جديدة نجهزها ونضيفها مسبقًا، وسنستفيد من الخطاف afterEach()‎ لحذف الملفات الناتجة بعد كل اختبار، لذلك نعدل مجموعة اختبارات التابع saveToFile()‎ ضمن ملف الاختبارات index.test.js ليصبح كالتالي: ... describe("saveToFile()", function () { beforeEach(function () { this.todos = new Todos(); this.todos.add("save a CSV"); }); afterEach(function () { if (fs.existsSync("todos.csv")) { fs.unlinkSync("todos.csv"); } }); it("should save a single TODO without error", async function () { await this.todos.saveToFile(); assert.strictEqual(fs.existsSync("todos.csv"), true); let expectedFileContents = "Title,Completed\nsave a CSV,false\n"; let content = fs.readFileSync("todos.csv").toString(); assert.strictEqual(content, expectedFileContents); }); it("should save a single TODO that's completed", async function () { this.todos.complete("save a CSV"); await this.todos.saveToFile(); assert.strictEqual(fs.existsSync('todos.csv'), true); let expectedFileContents = "Title,Completed\nsave a CSV,true\n"; let content = fs.readFileSync("todos.csv").toString(); assert.strictEqual(content, expectedFileContents); }); }); نلاحظ إضافة الخطاف beforeEach()‎ داخل مجموعة الاختبار: ... beforeEach(function () { this.todos = new Todos(); this.todos.add("save a CSV"); }); ... حيث أنشأنا كائنًا جديدًا من الصنف Todos سيكون متاحًا لكل الاختبارات ضمن هذه المجموعة، وذلك لأن موكا سيشارك قيمة الكائن this الذي أضفنا له خصائص ضمن الخطاف beforeEach()‎ مع جميع الاختبارات في توابع الاختبار it()‎، وقيمته ستكون واحدة ضمن مجموعة الاختبارات داخل describe()‎، حيث بالاستفادة من تلك الميزة يمكننا مشاركة بيانات مُعدة مسبقًا مع جميع الاختبارات. أما داخل الخطاف afterEach()‎، فقد حذفنا ملف CSV الناتج عن الاختبارات: ... afterEach(function () { if (fs.existsSync("todos.csv")) { fs.unlinkSync("todos.csv"); } }); ... في حال فشلت الاختبارات فلن يُنشأ ذلك الملف، لهذا السبب نختبر أولًا وجوده باستخدام التابع existsSync()‎ قبل تنفيذ عملية الحذف باستخدام التابع unlinkSync()‎، ثم بدلنا في باقي الاختبارات الإشارة إلى كائن المهام todos الذي كنا نُنشئه ضمن it()‎ مباشرةً، ليشير إلى الكائن الذي أعددناه ضمن الخطاف عن طريق this.todos، وحذفنا أسطر إنشاء الكائن todos ضمن تلك الاختبارات. والآن لننفذ تلك الاختبارات بعد التعديلات ونتأكد من نتيجتها بتنفيذ الأمر npm test لنحصل على التالي: ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO without error ✓ should save a single TODO that's completed 4 passing (20ms) نلاحظ أنه لا تغيير في نتائج الاختبار وجميعها نجحت، وأصبحت اختبارات التابع saveToFile()‎ أبسط وأسرع بسبب مشاركة الكائن مع جميع الاختبارات، وحللنا مشكلة ملف CSV الناتج عن تنفيذ الاختبارات. خاتمة كتبنا في هذا المقال وحدة برمجية لإدارة المهام في نود، واختبرنا عملها يدويًا في البداية داخل حلقة REPL التفاعلية، ثم أنشأنا ملف اختبار واستخدمنا إطار عمل الاختبارات موكا Mocha لأتمتة تنفيذ جميع الاختبارات على تلك الوحدة، واستخدمنا الوحدة assert للتوكيد والتحقق من عمل الوحدة التي طورناها، وتعلمنا كيف يمكن اختبار التوابع المتزامنة واللامتزامنة في موكا Mocha، واستعنا أخيرًا بالخطافات لتبسيط كتابة الاختبارات المرتبطة ببعضها وتسهيل قراءتها وزيادة سهولة التعديل عليها لاحقًا. والآن حاول عند تطوير برنامجك التالي كتابة الاختبارات لمزاياه، أو يمكنك البدء بكتابة الاختبارات له أولًا من خلال تحديد الدخل والخرج المتوقع من التوابع التي ستكتبها وكتابة اختبار لها على هذا الأساس ثم ابدأ ببنائها. ترجمة -وبتصرف- للمقال How To Test a Node.js Module with Mocha and Assert لصاحبه Stack Abuse. اقرأ أيضًا إنشاء وحدات برمجية Modules في Node.js تعرف على وحدات Node.js الأساسية إدارة الوحدات البرمجية في Node.js باستخدام npm وملف package.json
  21. يجب تعديل نوع زر إرسال النموذج ليكون submit بدلًا من button وذلك حتى يُطلق الحدث submit كالتالي: <input type="submit" value="Sign In" class="register-btn" />
  22. في حال كان النموذج form ضمن HTML لديك من الشكل التالي وبدون صنف class بالاسم form: <form> ... </form> فالاستعلام عنه يجب ألا يبدأ بنقطة كالتالي: let form = document.querySelector('form') وفقط في حال كان يملك الصنف form وأردت الاستعلام عنه باستخدام الصنف يجب إضافة النقطة كالتالي: <form class="form"> ... </form> let form = document.querySelector('.form')
  23. لتطوير الواجهات الأمامية لتطبيقات الويب والمواقع ثلاث لغات يجب تعلمها وهي HTML و CSS وجافاسكربت، بها يمكنك بناء المواقع الثابتة أو قوالب المواقع الديناميكية ذات المحتوى المتغير، أما للنظم الخلفية Backend يوجد العديد من اللغات التي يمكن تطوير تطبيقات الويب بها، منها PHP و جافاسكربت وبايثون، ولكل لغة برمجة أطر عمل تحوي المكونات الأساسية التي تحتاجها لبناء تطبيق ويب وتساعدك بناء تطبيقك بسرعة، منها لارافل في PHP و DJango في بايثون، أنصحك بقراءة المقال التالي ففيه تفصيل عن خريطة التعلم في مجال تطوير تطبيقات الويب وشرح عن كل مجال منها والدورات التي توفرها الأكاديمية بهذا الخصوص:
  24. التطبيق العملي هو الجزء الأكبر والأهم في تعلم البرمجة، ليس مطلوبًا منك حفظ التوابع أو كل تفاصيل اللغة، ركز على التطبيق العملي المتكرر لتطبيقات بسيطة مفيدة، بدءًا من طباعة رسالة بسيطة ضمن الطرفية Console، وصولًا إلى بناء تطبيق بسيط جدًا، خلال مرحلة التطبيق العملي ستتعامل مع مفاهيم اللغة مرارًا وتكرارًا، ومع الوقت ستحفظ الأجزاء الأكثر استخدامًا، وبدلًا من الحفظ ركز على مهارة البحث عن المعلومة أو المشكلة والقراءة عنها ضمن الأجوبة أو المراجع، البرمجة لا تُدرس نظريًا سوى لمفاهيم أولية بسيطة فقط، أنصحك بقراءة المقال التالي وفيه كل التفاصيل التي تحتاج معرفتها لرحلة تعلم البرمجة:
  25. حزمة لغات HTML و CSS و جافاسكربت هي اللغات الوحيدة التي يمكن لمتصفحات الويب التعامل معها، وبالتالي كل المواقع وتطبيقات الويب التي تتعامل معها مهما كانت طريقة بناءها فهي بالنهاية ستُنتج ملفات الموقع أو التطبيق ضمن تلك اللغات حصرًا، أي ما تعلمته يكفيك لبناء صفحات ومواقع الويب، يمكنك مثلًا تقديم خدمة بناء صفحات تعريفية عن الشركات أو الأشخاص، اهتم بجودة التصميم وجماليته والميزات التي توفرها ضمن الصفحات، أو يمكنك بناء قوالب المواقع المختلفة وبيعها، مثل قوالب الصفحات التعريفية، أو قوالب المتاجر الالكترونية لعرض البيانات، وغيرها، أنصحك بقراءة المقال التالي فهو يشرح بالتفصيل كل مجالات تعلم تطوير الويب، ويوضح خارطة طريق رحلة التعلم يمكنك اختيار ما تفضله منها، والمقال الآخر مقال عام يوجهك إلى طرق تعلم البرمجة والخدمات التي ستتمكن من تقديمها:
×
×
  • أضف...