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

إنشاء خادم ويب في Node.js باستخدام الوحدة HTTP


Hassan Hedr

يرسل المتصفح عند استعراضك لصفحة ويب ما طلبًا إلى جهاز حاسوب آخر عبر الإنترنت وهو بدوره يرسل الصفحة المطلوبة كجواب لذلك الطلب، حيث ندعو جهاز الحاسوب الذي أُرسل إليه ذلك الطلب بخادم الويب 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، فمثلًا يمكن إرسالها في نود باتباع الخطوات التالية:

  1. تعيين قيمة لترويسة Content-Type للرد في HTTP بقيمة تناسب نوع المحتوى المُرسل.
  2. تمرير البيانات بالصيغة الصحيحة للتابع 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-response

نوقف الخادم لنعود إلى سطر الأوامر مجددًا، وبذلك نكون تعلمنا طريقة إرسال صفحة 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-file

وبذلك نكون قد أرسلنا صفحة 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.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...