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

Hassan Hedr

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

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

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

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

    38

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

  1. عند كل استدعاء لذلك التابع سيتم تعريف مصفوفة جديدة وإضافة عنصر واحد إليها ثم طباعتها، بعد انتهاء تنفيذ التابع سيمسح المفسّر قيمة المصفوفة من الذاكرة وذلك لأن المصفوفة لا يمكن الوصول إليها سوى من داخل التابع ولم يعد هناك حاجة لها، لذا للمحافظة على قيمتها يمكن تعريفها خارج التابع كالتالي: const yo =[]; // تعريف خارج التابع function Moneys(mo){ .. // إضافة للمصفوفة وطباعتها }
  2. يمكنك إضافة الخاصية title لعنصر القائمة ضمن HTML، وتمرير قيمة النص التي تريد أن تظهر عند وضع مؤشر الفأرة فوق العنصر كالتالي: <li title="الرئيسية"> ... </li>
  3. لا داعٍ لاستخدام مكتبة redux لإدارة حالة التطبيق ما دامت الحالة بسيطة، في مشروعك والسيناريو الذي ذكرته لا يوجد حالة للتطبيق يجب إدارتها، كل ما عليك هو تعريف التوجيه بشكل صحيح باستخدام مكتبة react-router ليتم توجيه الروابط إلى صفحاتها المطلوبة، ثم ذكر تلك الروابط ضمن المكونات المسؤولة عن عرض نوع المنتج والمنتج، الصعوبة في التطبيق قد يكون سببها اختيار طريقة معقدة لحل المشكلة، مع الوقت والتنفيذ العملي المتكرر والقراءة عن الحلول والإمكانيات وميزات المكتبات المستخدمة ستزيد مهارتك في حل المشكلة بأبسط الطرق، أنصحك بالتركيز فقط على مكتبة react-router لحل مشكلة التنقل بين الصفحات وتجنب تعيين متغيرات أو خطافات hooks مسؤولة عن تغيير الصفحة المعروضة بل فقط استخدم روابط عادية مثل href للمكون a، ومكتبة react-router ستتولى عرض الصفحة المطلوبة
  4. يمكنك التواصل مع مركز المساعدة وشرح مشكلتك لهم بالتفصيل مع إرفاق تلك الصورة لتوضيح المشكلة، وسيتابعون معك سبب المشكلة ويرشدونك لحلها، حاول إرسال شرح مفصل للخطوات التي قمت بها وما حدث وانتظر الرد منهم في أقرب وقت
  5. يتم تحديث الموقع دوريًا من قبل الفريق المختص، التحديث الأخير يركز على تحسين المظهر العام للموقع من كافة النواحي، كذلك أُضيف عدة أنواع من التفاعل والتعبير مع المشاركات، بالنسبة للدورات يتم دوريًا إضافة الدروس وتحديثها لاستخدام آخر المكتبات وأطر العمل المناسبة لمجال الدورة، يمكنك إذا أردت التواصل مع مركز المساعدة إذا كان لديك استفسارات اضافية
  6. يمكنك لذلك الاستفادة من رمز الخطأ عن طريق التابع getCode والذي يعيد رمز الخطأ الذي حدث، ثم التحقق من ذلك الرمز، فمثلًا في حال كان الرمز يبدأ بالرقم 08 فيعني أن الخطأ سببه مشكلة في الاتصال، في هذه الحالة يمكنك تنفيذ الأوامر الأخرى التي تريد كالتالي: catch(PDOException $e) { if(str_starts_with((string)$e->getCode(),'08')){ // خطأ في الاتصال ... } else { // خطأ آخر echo "Connection failed: " . $e->getMessage(); } }
  7. ضمن الدورات التي توفرها الأكاديمية يوجد فريق من المدربين لمساعدتك دومًا، في حال واجهت أي مشاكل أو لديك أي استفسارات أثناء متابعتك وتطبيقك لمحتوى الدروس يمكنك إضافة استفسارك أو شرح مشكلتك في قسم التعليقات أسفل فيديو الدرس، وسيتابع المدربين معك حل المشكلة والشرح لك ومساعدتك دومًا
  8. الخاصية color تؤثر على لون النص فقط، ولا تؤثر في لون محتوى العنصر path، فلتعديل لون هذا العنصر يمكن استخدام الخاصية fill element.style.fill = 'blue'; أيضًا لاستهداف العنصر المطلوب يجب أن يكون المُعرّف id له غير مكرر لأي عنصر آخر، أي يجب إزالة العنصر div الذي يحمل نفس قيمة المعرف
  9. يمثل المخزن المؤقت 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 واستراتيجيات التخزين المؤقت
  10. يعتمد ذلك على تطبيقات تتيح لك بناء صفحات الويب أو تطبيقات معينة أو الربط بين عدة خدمات معًا لبناء تطبيق دون الحاجة لمعرفة التقنيات البرمجية التي تُبنى بها، لكن ميزات التطبيق المنتج تكون محصورة بقدرات التطبيق الذي طور بها، قد لا تتيح لك تخصيص بعض الميزات التي تحتاجها وقد لا تدعمها، هدف تلك التطبيقات هي تجريد عملية بناء التطبيقات الأكثر استخدامًا والتي يمكن أتمتتها، أي إذا كنت تريد بناء منتج بسيط، كصفحة ويب لعرض بعض المعلومات فيمكنك بناءها بواسطة تلك التطبيقات دون الحاجة لمعرفة تقنية كبيرة، يمكنك الاستفادة من المقال التالي والذي يشرح ذلك تمامًا ويعرفك على بعض تلك الأدوات ومستقبل هذا المجال:
  11. الأمر طبيعي عند البدء بتعلم البرمجة، فكل الأفكار والشيفرات وبنيتها ومعانيها تكون جديدة، بداية ركز على الأساسيات ولا تنتقل إلى أفكار لاحقة قبل تمكين الأساسيات، والعامل الأكبر في تمكين الأفكار الجديدة هو التركيز على التطبيق العملي دومًا، ومع تكرار التطبيق العملي ستثبت الأساسيات وتصبح الشيفرات البرمجية مألوفة وتقرأها كما تقرأ أي لغة جديدة، ركز في شرح الأفكار الجديدة، ثم وظّف تلك الأفكار في مثال عملي لو كان بسيطًا، فالتطبيق العملي هو الأساس، وحاول القراءة بنفسك عن أي فكرة جديدة لتوسع فهمك لها قدر الحاجة، يمكنك مثلًا الرجوع لموسوعة حسوب ففيها شرح مفصل وموسع عن تفاصيل اللغات البرمجية، وأنصحك أيضًا بقراء المقالات التالية والتي تشرح لك النقاط التي يجب التركيز عليها عند تعلم البرمجة:
  12. يوجد عدة حلول لذلك ضمن 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 بنفس الطريقة، أنصحك بقراءة المقال التالي الذي يشرح الطرق المختلفة لضبط تموضع العناصر:
  13. يرجى تعديل السؤال وإرفاق الشيفرة ورسالة الخطأ التي تظهر نصًا بدلًا من الصورة مع تنسيقها بتنسيق الشيفرة كي نستطيع مساعدتك
  14. لاحظ أنك لم تمرر للمكون Notes الخاصية notes وهذا سبب المشكلة، يجب تمرير تلك الخاصية كالتالي: <Notes notes={...} .../> ^
  15. في الشيفرة المرفقة لا يوجد استخدام للمكون Notes تأكد من مكان استخدامه وطريقة تمرير تلك الخاصية، كما يرجى إرفاق الجزء من الشيفرة الخاص بذلك الاستخدام بدلًا من رابط خارجي
  16. في حال لم يظهر أي عنصر يعني أن قيمة notes لم تمرر للمكون Notes واستُخدمت القيمة الافتراضية وهي المصفوفة الفارغة، لذا تأكد أن قيمة notes تمرر بشكل صحيح للمكون وتحوي قيمة
  17. يمكنك تعيين قيمة افتراضية للمتغير notes تكون مصفوفة فارغة في حال لا يوجد بيانات، من داخل المكون يمكنك ذلك كالتالي: <div className='notes'> { (notes || []).map(note => <Note key={note.id} title={note.title} content={note.content}/>) } ... </div>
  18. ابدأ بتعلم أساسيات اللغة نفسها، من متغيرات وتعابير شرطية وحلقات ثم أنواع البيانات والمتغيرات، والمصفوفات وطرق التعامل معها، فالأساسيات ضرورية مهما كان مجال اختصاصك، بعدها ستتمكن من إنشاء النصوص البرمجية البسيطة في جافاسكريبت لتنفيذ المهام، ثم يمكنك تعلم طرق تثبيت واستيراد واستخدام المكتبات البرمجية الجاهزة وهي ضرورية لتكون جزءًا من برامجك تعلم ذلك أيضًا، هنا ستركز على تعلم كل مكتبة برمجية أو إطار عمل ستستخدمه على حدى لتحقيق المطلوب، أنصحك بقراءة المقال التالي والذي يوضح لك كيف تتعلم البرمجة واين تركز وكيف تبدأ: ويمكنك بدء تعلم البرمجة من سلسلة دروس python 101 باللغة العربية فهي تبدأ من الصفر وتصل إلى مستويات واسنخدامات متقدمة:
  19. يمكنك دومًا السؤال ضمن التعليقات أسفل الدروس وتوضيح مشكلتك وسيتم متابعتها من قبل فريق المدربين المختص ومتابعة المشاكل وحلها معك، الدورات يتم تحديثها باستمرار لتغطي آخر التقنيات وإصدارات المكتبات لذا ابقى على اطلاع وإلى حينها يمكنك متابعى الدورة وحتى من تلك المشاكل هي فرصة لتكسب خبرة في التعامل مع مشاكل الاعتماديات وطرق حلها وستستفيد من ذلك.
  20. لا يوجد متغير اسمه x ليتم تمريره إلى الخرج cout، إذا كنت تقصدين إحدى الخواص x من أحد الكائنين o1 أو o2 فيجب استخراج الخاصية منهما ثم طباعتها كالتالي: cout << o1.x; # أو cout << o2.x; يمكنك الاطلاع أكثر والاستفادة من قراءة المقالات التالية: يرجة تعديل نص السؤال واستبدال الشيفرة ضمن مربع تنسيق الشيفرة بتنسيقها الصحيح.
  21. يرجى توضيح الهدف والمراد من البرنامج، وإرفاق النتيجة الخاطئة أو رسالة الخطأ التي تظهر لتتوضح المشكلة ونتمكن من المساعدة
  22. يرجى إرفاق أي رسائل خطأ تظهر ضمن الطرفية console، وذكر لغة البرمجة وإطار العمل الذان يتم العمل بهما لتوضيح المشكلة
  23. رمز الاستجابة السريعة أو QR هو مجرد ترميز لنص ما على شكل صورة بمواصفات محددة، لتتمكن البرامج الأخرى من قراءتها بصريًا، سواء عبر تحليل صورة ثابتة أو تحليل ما تُظهره الكاميرة وقراءة ذلك النص ومعالجته، لذا يجب أولًا توليد رمز QR خاص برابط الموقع المُطلوب، ثم نشر تلك الصورة إما عبر طباعتها أو وضعها في المكان المطلوب ليتمكن المستخدمون من قراءتها والتوجه إلى الموقع، يمكنك البحث عبر الانترنت عن مواقع مختصة بتوليد تلك الرموز مجانًا، ويمكنك الاستفادة من الإجابات التالية:
  24. هل يمكنك توضيح ما العنصر الذي تواجه مشكلة في عرضه وأين، ففي الشيفرة المرفقة يوجد السطر التالي والذي يخفي إشارة التحميل بعد وصول رد الطلب: $('.spinner-border').remove(); ولا يوجد تعامل مع تلك الخاصية في مكان آخر ضمن الشيفرة، كذلك يرجى إرفاق أي رسائل خطأ تظهر لك ضمن console
  25. يرسل المتصفح عند استعراضك لصفحة ويب ما طلبًا إلى جهاز حاسوب آخر عبر الإنترنت وهو بدوره يرسل الصفحة المطلوبة كجواب لذلك الطلب، حيث ندعو جهاز الحاسوب الذي أُرسل إليه ذلك الطلب بخادم الويب 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
×
×
  • أضف...