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

تطبيق عملي لتعلم Express - الجزء الثالث: الوجهات والمتحكمات


Ola Abbas

سنُعِدّ في هذا المقال الوجهات Routes (شيفرة معالجة عناوين URL، ويسميها البعض بالموجهات) باستخدام دوال معالجة وهمية dummy لجميع النقاط النهائية للموارد التي سنحتاجها في موقع المكتبة المحلية LocalLibrary. سيكون لدينا في النهاية بنية معيارية لشيفرة معالجة الوجهات، والتي يمكننا توسيعها باستخدام دوال معالجة حقيقية في المقالات اللاحقة، وسيصبح لدينا أيضًا فهم جيد لكيفية إنشاء وجهات معيارية باستخدام إطار عمل Express.

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

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

أنشأنا النماذج مسبقًا، فالأشياء الرئيسية التي يجب إنشاؤها هي:

  • "الوجهات Routes" لتوجيه الطلبات المدعومة وأيّ معلومات مُشفَّرة في عناوين URL للطلب إلى دوال المتحكم المناسبة.
  • دوال المتحكم للحصول على البيانات المطلوبة من النماذج وإنشاء صفحة HTML تعرض البيانات وإعادتها إلى المستخدم لعرضها في المتصفح.
  • العروض (القوالب) التي تستخدمها المتحكمات لتقديم البيانات.

01 mvc express

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

يوفّر القسم الأول التالي نظرةً عامةً موجزة حول كيفية استخدام برمجية Express الوسيطة Router، ثم سنستخدم ذلك في الأقسام اللاحقة عند إعداد وجهات موقع المكتبة المحلية LocalLibrary.

مقدمة إلى الوجهات

الوجهة هي قسم من شيفرة Express التي تربط بين فعل HTTP، مثل GET و POST و PUT و DELETE وإلخ مع مسار أو نمط URL ودالة تُستدعى لمعالجة هذا النمط.

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

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

يوفر الجزء المتبقي من هذا القسم نظرةً عامة حول كيفية استخدام الكائن Router لتعريف الوجهات.

تعريف واستخدام وحدات وجهة منفصلة

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

أولًا، ننشئ وجهات لميزة الويكي wiki في وحدة بالاسم "wiki.js"، إذ تستورد هذه الشيفرة البرمجية كائن تطبيق Express، وتستخدمه للحصول على كائن Router، ثم تضيف بعض الوجهات إليه باستخدام التابع get()‎، ثم تصدّر الوحدةُ الكائنَ Router.

// ‫wiki.js: ‫وحدة وجهة ويكي Wiki

const express = require("express");
const router = express.Router();

// وجهة الصفحة الرئيسية
router.get("/", function (req, res) {
  res.send("Wiki home page");
});

// ‫وجهة صفحة About
router.get("/about", function (req, res) {
  res.send("About this wiki");
});

module.exports = router;

ملاحظة: عرّفنا في الشيفرة السابقة دوال رد النداء Callbacks الخاصة بمعالج الوجهة مباشرةً في دوال وجهة، وسنعرّف في موقع المكتبة المحلية LocalLibrary دوال رد النداء هذه في متحكم وحدة منفصل.

يمكن استخدام وحدة وجهة في ملف تطبيقنا الرئيسي من خلال طلب وحدة الوجهة (wiki.js) باستخدام الدالة require()‎، ثم استدعاء الدالة use()‎ في تطبيق Express لإضافة كائن Router إلى مسار معالجة البرمجية الوسيطة مع تحديد مسار URL الخاص بالويكي "wiki".

const wiki = require("./wiki.js");
// …
app.use("/wiki", wiki);

ويمكن بعد ذلك الوصول إلى الوجهتين المُعرَّفتين في وحدة وجهة wiki من /wiki/ و /wiki/about/.

دوال الوجهة

تعرّف الوحدة السابقة بعضًا من دوال الوجهة، إذ تُعرَّف الوجهة "about" (التي سنعيد إنتاجها فيما يلي) باستخدام التابع Router.get()‎، والذي يستجيب فقط لطلبات HTTP من النوع GET. الوسيط الأول لهذا التابع هو مسار URL والوسيط الثاني هو دالة رد نداء تُستدعَى عند تلقي طلب HTTP من النوع GET مع المسار.

router.get("/about", function (req, res) {
  res.send("About this wiki");
});

تأخذ دالة رد النداء ثلاثة وسائط تُسمَّى عادة req و res و next، والتي ستحتوي على كائن طلب HTTP واستجابة HTTP والدالة التالية next في سلسلة البرمجيات الوسيطة.

ملاحظة: تُعَد دوال Router برمجيات Express وسيطة، مما يعني أنها يجب إما إكمال (أو الاستجابة) للطلب أو استدعاء الدالة next في السلسلة، إذ نكمل في حالتنا الطلب باستخدام التابع send()‎، لذلك لا يُستخدَم الوسيط next ونختار عدم تحديده.

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

تستدعي دالة رد النداء في مثالنا التابع send()‎ في الاستجابة لإعادة السلسلة النصية "About this wiki" عندما نتلقى طلب GET مع المسار ("‎/about"). هناك عدد من توابع الاستجابة الأخرى لإنهاء دورة الطلب/الاستجابة، فمثلًا يمكنك استدعاء التابع res.json()‎ لإرسال استجابة JSON أو التابع res.sendFile()‎ لإرسال ملف. يُعَد التابع render()‎ تابع الاستجابة الذي سنستخدمه غالبًا أثناء بناء المكتبة، والذي ينشئ ويعيد ملفات HTML باستخدام القوالب والبيانات (سنتحدث عن ذلك في مقالٍ لاحق).

أفعال HTTP

تستخدم أمثلة الوجهات السابقة التابع Router.get()‎ للاستجابة على طلبات HTTP من النوع GET مع مسار معين.

يوفّر الكائن Router أيضًا توابع وجهة لجميع أفعال HTTP الأخرى، والتي تُستخدَم غالبًا بنفس الطريقة تمامًا ومن هذه التوابع: post()‎ و put()‎ و delete()‎ و options()‎ و trace()‎ و copy()‎ و lock()‎ و mkcol()‎ و move()‎ و purge()‎ و propfind()‎ و proppatch()‎ و unlock()‎ و report()‎ و mkactivity()‎ و checkout()‎ و merge()‎ و m-search()‎ و notify()‎ و subscribe()‎ و unsubscribe()‎ و patch()‎ و search()‎ و connect()‎.

تتصرف الشيفرة البرمجية التالية مثلًا مثل وجهة ‎/about السابقة، ولكنها تستجيب فقط لطلبات HTTP من النوع POST:

router.post("/about", (req, res) => {
  res.send("About this wiki");
});

مسارات الوجهة

تعرِّف مساراتُ الوجهة النقاطَ النهائية التي يمكن إجراء الطلبات عندها، فالأمثلة التي رأيناها حتى الآن كانت مجرد سلاسل نصية، واُستخدِمت كما هو مكتوب تمامًا مثل: '/' و '‎/about' و '‎/book' و '‎/any-random.path'.

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

  • ?: يجب أن تحتوي النقطة النهائية endpoint على 0 أو 1 من المحرف السابق (أو المجموعة)، فمثلًا سيتطابق مسار الوجهة '‎/ab?cd' مع النقاط النهائية acd أو abcd.
  • +: يجب أن تحتوي النقطة النهائية على واحد أو أكثر من المحرف السابق (أو المجموعة)، فمثلًا سيتطابق مسار الوجهة '‎/ab+cd' مع النقاط النهائية abcd و abbcd و abbbcd وإلخ.
  • *: يمكن أن تحتوي النقطة النهائية على سلسلة نصية عشوائية في مكان المحرف *، فمثلًا سيتطابق مسار الوجهة '‎/ab*cd' مع النقاط النهائية abcd و abXcd و abSOMErandomTEXTcd وإلخ.
  • (): تجميع تطابق مجموعة من المحارف لإجراء عملية أخرى عليها، فمثلًا سينفّذ '‎/ab(cd)?e' تطابق ? على المجموعة (cd)، إذ سيتطابق مع abe و abcde.

يمكن أن تكون مسارات الوجهة أيضًا تعابير نمطية Regular Expressions في لغة جافا سكريبت JavaScript، فمثلًا سيتطابق مسار الوجهة التالي مع catfish و dogfish، ولكن لن يتطابق مع catflap و catfishhead وما إلى ذلك. لاحظ أن مسار التعبير النمطي يستخدم صياغة التعابير النمطية، وهي ليست سلسلة نصية بين علامات اقتباس كما في الحالات السابقة.

app.get(/.*fish$/, function (req, res) {
  // …
});

ملاحظة: ستستخدم معظم الوجهات في موقع المكتبة المحلية LocalLibrary سلاسلًا نصية ولن تستخدم تعابيرًا نمطية، وسنستخدم أيضًا معاملات الوجهات كما سنناقش في القسم التالي.

معاملات الوجهة

تُسمَّى معاملات الوجهة بأجزاء Segments عنوان URL المُستخدَمة لالتقاط القيم في مواضع محددة من عنوان URL، إذ تُسبَق هذه المقاطع بنقطتين ثم الاسم (مثل ‎/:your_parameter_name/‎). تُخزَّن القيم المُلتقَطة في كائن req.params من خلال استخدام أسماء المعاملات بوصفها مفاتيحًا، مثل req.params.your_parameter_name.

ليكن لدينا مثلًا عنوان URL مُشفَّر يحتوي على معلومات حول المستخدمين والكتب: http://localhost:3000/users/34/books/8989، إذ يمكننا استخراج هذه المعلومات كما هو موضح فيما يلي باستخدام معاملات مسار userId و bookId:

app.get("/users/:userId/books/:bookId", (req, res) => {
  // ‫الوصول إلى userId باستخدام req.params.userId
  // الوصول إلى‫ bookId باستخدام req.params.bookId
  res.send(req.params);
});

يجب أن تتكون أسماء معاملات الوجهة من "محارف كلمات"، أي A-Z و a-z و0-9 و _.

ملاحظة: ستجري مطابقة عنوان "‎/book/create" مع وجهة مثل الوجهة ‎/book/:bookId، والتي ستستخرج قيمة "bookId" الخاصة بالإنشاء 'create'. ستُستخدَم الوجهة الأولى التي تطابق عنوان URL الوارد، لذلك إذا أردتَ معالجة عناوين ‎/book/create بصورة منفصلة، فيجب تعريف معالج الوجهة قبل الوجهة ‎/book/:bookId.

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

معالجة الأخطاء في دوال الوجهة

تحتوي جميع دوال الوجهة الموضحة سابقًا على وسائط req و res التي تمثل الطلب والاستجابة على التوالي. تُستدعَى دوال الوجهة أيضًا مع وسيط ثالث هو next، والذي يمكن استخدامه لتمرير الأخطاء إلى سلسلة برمجيات Express الوسيطة. توضح الشيفرة التالية كيفية إنجاز ذلك باستخدام مثال استعلام قاعدة البيانات الذي يأخذ دالة رد نداء ويعيد إما خطأ err أو بعض النتائج. تُستدعَى next مع خطأ err بوصفه قيمة معاملها الأول عند إعادة خطأ err، وسينتقل هذا الخطأ في النهاية إلى شيفرة معالجة الأخطاء العامة، وتُعاد البيانات المطلوبة ثم تُستخدَم في الاستجابة في حالة النجاح.

router.get("/about", (req, res, next) => {
  About.find({}).exec((err, queryResults) => {
      if (err) {
        return next(err);
      }
      // اعرض شيئًا ما في حالة النجاح
      res.render("about_view", { title: "About", list: queryResults });
    });
});

معالجة الاستثناءات في دوال الوجهة

يوضح القسم السابق كيف يتوقع إطار عمل Express أن تعيد دوال الوجهة أخطاءً، إذ صُمِّم إطار العمل للاستخدام مع الدوال غير المتزامنة التي تأخذ دالة رد نداء (مع خطأ وووسيط النتيجة)، والتي تُستدعَى عند اكتمال العملية. يُعَد ذلك مشكلة لأننا سنجري لاحقًا استعلامات قاعدة بيانات Mongoose التي تستخدم واجهات برمجة تطبيقات مستندة إلى الوعود promise، والتي يمكن أن تؤدي إلى رمي استثناءات في دوال الوجهة بدلًا من إعادة الأخطاء في دالة رد النداء.

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

ملاحظة: من المتوقع أن يعالج Express 5 التجريبي استثناءات جافا سكريبت بطريقة أصيلة.

ليكن لدينا المثال البسيط من القسم السابق مع وجود About.find().exec()‎ بوصفه استعلام قاعدة بيانات يعيد وعدًا، إذ يمكن كتابة دالة الوجهة ضمن كتلة try...catch كما يلي:

exports.get("/about", async function (req, res, next) {
  try {
    const successfulResult = await About.find({}).exec();
    res.render("about_view", { title: "About", list: successfulResult });
  } catch (error) {
    return next(error);
  }
});

يُعَد ذلك كمًا كبيرًا جدًا من الشيفرة البرمجية المتداولة لإضافتها إلى كل دالة، لذا سنستخدم بدلًا من ذلك وحدة express-async-handler التي تعرّف دالة تغليف تخفي كتلة try...catch والشيفرة البرمجية لتوجيه الخطأ، وبالتالي أصبح المثال نفسه الآن بسيطًا جدًا، لأننا نحتاج فقط إلى كتابة شيفرة برمجية للحالة التي نفترض فيها النجاح:

// استيراد الوحدة
const asyncHandler = require("express-async-handler");

exports.get(
  "/about",
  asyncHandler(async (req, res, next) => {
    const successfulResult = await About.find({}).exec();
    res.render("about_view", { title: "About", list: successfulResult });
  }),
);

الوجهات اللازمة لموقع المكتبة المحلية

سنعرض فيما يلي عناوين URL التي سنحتاجها لصفحاتنا، إذ نضع اسم كل نموذج من نماذجنا (الكتاب ونسخة الكتاب ونوع الكتاب والمؤلف) مكان الكائن object، والكائنات objects هي جمع كائن، والمعرّف id هو نسخة فريدة من الحقل (‎_id) تُعطَى لكل نسخة من نموذج Mongoose افتراضيًا.

  • catalog/‏‎: الصفحة الرئيسية أو صفحة الفهرس.
  • catalog/<objects>/‏‎: قائمة بجميع الكتب أو نسخها أو أنواعها أو المؤلفين، مثل /catalog/books/ و /catalog/genres/ وغير ذلك.
  • catalog/<object>/<id>‎: صفحة التفاصيل لكتاب أو نسخة أو نوع أو مؤلف معين مع قيمة الحقل ‎_id المُحدَّدة، مثل ‎/catalog/book/584493c1f4887f06c0e67d37.
  • catalog/<object>/create: استمارة إنشاء كتاب أو نسخة أو نوع أو مؤلف جديد، مثل ‎/catalog/book/create.
  • catalog/<object>/<id>/update: استمارة لتحديث كتاب أو نسخة أو نوع أو مؤلف معين مع قيمة الحقل ‎_id المُحدَّدة، مثل ‎/catalog/book/584493c1f4887f06c0e67d37/update.
  • catalog/<object>/<id>/delete: استمارة لحذف كتاب أو نسخة أو نوع أو مؤلف معين مع قيمة الحقل ‎_id المُحدَّدة، مثل ‎/catalog/book/584493c1f4887f06c0e67d37/delete.

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

تُستخدَم عناوين URL الأخرى للعمل على نسخةٍ من مستند أو نموذج معين، وتشفّر هوية العنصر في عنوان URL (كما هو موضّح في العنصر <id> سابقًا). سنستخدم معاملات المسار لاستخراج المعلومات المشفرة وتمريرها إلى معالج الوجهة، وسنستخدمها في مقال لاحق لتحديد المعلومات التي نحصل عليها من قاعدة البيانات ديناميكيًا. نحتاج فقط إلى وجهة واحدة لكل مورد من نوع معين من خلال تشفير المعلومات في عنوان URL، مثل استخدام وجهة واحدة للتعامل مع عرض عنصر كتاب واحد.

ملاحظة: يسمح إطار عمل Express بإنشاء عناوين URL الخاصة بك بالطريقة التي تريدها، إذ يمكنك تشفير المعلومات في متن عنوان URL أو استخدام معاملات URL من النوع GET (مثل ‎/book/?id=6). يجب أن تظل عناوين URL نظيفة ومنطقية وقابلة للقراءة بغض النظر عن الطريقة التي تستخدمها.

سننشئ فيما يلي دوال رد نداء معالجة الوجهة وشيفرة الوجهة البرمجية لجميع عناوين URL السابقة.

إنشاء دوال رد النداء لمعالجة الوجهة

سننشئ أولًا جميع دوال رد النداء الوهمية أو الهيكلية التي ستُستدعَى لاحقًا قبل أن نعرّف الوجهات، وستُخزَّن دوال رد النداء في وحدات متحكمات منفصلة لكل من Book و BookInstance و Genre و Author، إذ يمكنك استخدام أي بنية ملفات أو وحدات، ولكن تبدو الطريقة التي سنستخدمها مناسبة ودقيقة لهذا المشروع.

ابدأ بإنشاء مجلدٍ للمتحكمات في جذر المشروع ‎/controllers، ثم أنشئ ملفات أو وحدات منفصلة للمتحكمات للتعامل مع كل نموذج كما يلي:

/express-locallibrary-tutorial  //the project root
  /controllers
    authorController.js
    bookController.js
    bookinstanceController.js
    genreController.js

ستستخدم المتحكمات الوحدة express-async-handler، لذا ثبّتها في المكتبة باستخدام npm قبل المتابعة كما يلي:

npm install --save express-async-handler

متحكم المؤلف Author

افتح الملف "‎/controllers/authorController.js" واكتب فيه الشيفرة البرمجية التالية:

const Author = require("../models/author");
const asyncHandler = require("express-async-handler");

// عرض قائمة بجميع المؤلفين
exports.author_list = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author list");
});

// عرض صفحة التفاصيل لمؤلف معين
exports.author_detail = asyncHandler(async (req, res, next) => {
  res.send(`NOT IMPLEMENTED: Author detail: ${req.params.id}`);
});

// عرض استمارة إنشاء م‫ؤلف باستخدام GET
exports.author_create_get = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author create GET");
});

// معالجة إنشاء مؤلف باستخدام‫ POST
exports.author_create_post = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author create POST");
});

// عرض است‫مارة حذف المؤلف باستخدام GET
exports.author_delete_get = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author delete GET");
});

// معالجة ‫حذف المؤلف باستخدام POST
exports.author_delete_post = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author delete POST");
});

// عرض استمارة تحد‫يث المؤلف باستخدام GET
exports.author_update_get = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author update GET");
});

// معالجة تحديث‫ المؤلف باستخدام POST
exports.author_update_post = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Author update POST");
});

تطلب الوحدة أولًا نموذج المؤلف Author الذي سنستخدمه لاحقًا للوصول إلى بياناتنا وتحديثها، والمغلِّف asyncHandler الذي سنستخدمه لالتقاط الاستثناءات من دوال معالج الوجهة، ثم تصدِّر دوالًا لكل عنوان من عناوين URL التي نرغب في معالجتها. لاحظ أن عمليات الإنشاء والتحديث والحذف تستخدم الاستمارات Forms، وبالتالي تملك توابعًا إضافية لمعالجة طلبات الاستمارة من النوع Post، وسنناقش هذه التوابع في مقال لاحق.

تستخدم جميع الدوال الدالة المُغلِّفة Wrapper السابقة في معالجة الاستثناءات في دوال الوجهة مع وسائط للطلب والاستجابة والدالة التالية next، وتستجيب الدوال بسلسلة نصية تشير إلى أن الصفحة المرتبطة لم تُنشَأ بعد. إذا كان من المتوقع أن تتلقى دالة المتحكم معاملات المسار، فستكون هذه المعاملات هي الخرج الموجود في سلسلة الرسالة النصية (لاحظ req.params.id).

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

متحكم نسخة الكتاب BookInstance

افتح الملف ‎/controllers/bookinstanceController.js وانسخ الشيفرة البرمجية التالية التي تتبع نمطًا مطابقًا لوحدة المتحكم Author:

const BookInstance = require("../models/bookinstance");
const asyncHandler = require("express-async-handler");

// ‫عرض قائمة بجميع نسخ الكتب BookInstance
exports.bookinstance_list = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance list");
});

// ‫عرض صفحة تفاصيل نسخة كتاب BookInstance معينة
exports.bookinstance_detail = asyncHandler(async (req, res, next) => {
  res.send(`NOT IMPLEMENTED: BookInstance detail: ${req.params.id}`);
});

// ‫عرض استمارة إنشاء نسخة الكتاب BookInstance باستخدام GET
exports.bookinstance_create_get = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance create GET");
});

// ‫معالجة إنشاء نسخة كتاب BookInstance باستخدام POST
exports.bookinstance_create_post = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance create POST");
});

// ع‫رض استمارة حذف نسخة كتاب BookInstance باستخدام GET
exports.bookinstance_delete_get = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance delete GET");
});

// معالجة حذ‫ف نسخة كتاب BookInstance باستخدام POST
exports.bookinstance_delete_post = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance delete POST");
});

// عرض‫ استمارة تحديث نسخة كتاب BookInstance باستخدام GET
exports.bookinstance_update_get = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance update GET");
});

// معالجة‫ تحديث نسخة الكتاب باستخدام POST
exports.bookinstance_update_post = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: BookInstance update POST");
});

متحكم نوع الكتاب Genre

افتح الملف ‎/controllers/genreController.js والصق فيه النص التالي الذي يتبع نمطًا مطابقًا لملفات Author و BookInstance:

const Genre = require("../models/genre");
const asyncHandler = require("express-async-handler");

// عرض ق‫ائمة بجميع أنواع الكتب Genre
exports.genre_list = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre list");
});

// عرض صفحة ت‫فاصيل نوع كتاب Genre معين
exports.genre_detail = asyncHandler(async (req, res, next) => {
  res.send(`NOT IMPLEMENTED: Genre detail: ${req.params.id}`);
});

// عرض استم‫ارة إنشاء نوع الكتاب Genre باستخدام GET
exports.genre_create_get = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre create GET");
});

// معالجة إنشاء ‫نوع كتاب Genre باستخدام POST
exports.genre_create_post = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre create POST");
});

// عرض اس‫تمارة حذف نوع كتاب Genre باستخدام GET
exports.genre_delete_get = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre delete GET");
});

// معال‫جة حذف نوع كتاب Genre باستخدام POST
exports.genre_delete_post = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre delete POST");
});

// عرض استمارة تحد‫يث نوع كتاب Genre باستخدام GET
exports.genre_update_get = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre update GET");
});

// معالجة ‫تحديث نوع كتاب Genre باستخدام POST
exports.genre_update_post = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Genre update POST");
});

متحكم الكتاب Book

افتح الملف ‎/controllers/bookController.js وانسخ الشيفرة البرمجية التالية، إذ يتبع هذا الملف النمط المتبع نفسه في وحدات المتحكمات الأخرى، ولكنه يحتوي أيضًا على الدالة index()‎ لعرض صفحة الترحيب بالموقع:

const Book = require("../models/book");
const asyncHandler = require("express-async-handler");

exports.index = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Site Home Page");
});

// عرض قائمة بجميع الكتب
exports.book_list = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book list");
});

// عرض صفحة تفاصيل كتاب معين
exports.book_detail = asyncHandler(async (req, res, next) => {
  res.send(`NOT IMPLEMENTED: Book detail: ${req.params.id}`);
});

// عرض استما‫رة إنشاء كتاب باستخدام GET
exports.book_create_get = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book create GET");
});

// معال‫جة إنشاء كتاب باستخدام POST
exports.book_create_post = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book create POST");
});

// عرض است‫مارة حذف كتاب باستخدام GET
exports.book_delete_get = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book delete GET");
});

// معالج‫ة حذف كتاب باستخدام POST
exports.book_delete_post = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book delete POST");
});

// عرض استمارة تحدي‫ث كتاب باستخدام GET
exports.book_update_get = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book update GET");
});

// معالجة تحديث كت‫اب باستخدام POST
exports.book_update_post = asyncHandler(async (req, res, next) => {
  res.send("NOT IMPLEMENTED: Book update POST");
});

إنشاء دليل وحدات الوجهة

سننشئ الآن وجهات لجميع عناوين URL التي يحتاجها موقع المكتبة المحلية LocalLibrary، والتي ستستدعي دوال المتحكمات التي عرّفناها في الأقسام السابقة.

تحتوي البنية الهيكلية للموقع مسبقًا على المجلد "‎./routes" الذي يحتوي على وجهات لصفحة الفهرس index والمستخدمين users. أنشئ ملف وجهة آخر هو catalog.js ضمن هذا المجلد كما يلي:

/express-locallibrary-tutorial //the project root
  /routes
    index.js
    users.js
    catalog.js

افتح الملف ‎/routes/catalog.js وانسخ الشيفرة البرمجية التالية:

const express = require("express");
const router = express.Router();

// طلب وحدات المتحكمات
const book_controller = require("../controllers/bookController");
const author_controller = require("../controllers/authorController");
const genre_controller = require("../controllers/genreController");
const book_instance_controller = require("../controllers/bookinstanceController");

/// وجهات الكتاب ///

// الحصول على صفحة الدليل الرئيسية
router.get("/", book_controller.index);

// ط‫لب GET لإنشاء كتاب. يجب أن يأتي هذا الطلب قبل الوجهات Routes التي تعرض الكتاب (يستخدم المعرّف id)
router.get("/book/create", book_controller.book_create_get);

// طل‫ب POST لإنشاء كتاب
router.post("/book/create", book_controller.book_create_post);

// طل‫ب GET لحذف كتاب
router.get("/book/:id/delete", book_controller.book_delete_get);

// ط‫لب POST لحذف كتاب
router.post("/book/:id/delete", book_controller.book_delete_post);

// ‫طلب GET لتحديث كتاب
router.get("/book/:id/update", book_controller.book_update_get);

// ط‫لب POST لتحديث كتاب
router.post("/book/:id/update", book_controller.book_update_post);

// ط‫لب GET لكتاب واحد
router.get("/book/:id", book_controller.book_detail);

// ‫طلب GET لقائمة جميع عناصر الكتب
router.get("/books", book_controller.book_list);

/// وجهات المؤلف ///

// ط‫لب GET لإنشاء مؤلف Author. يجب أن يأتي هذا الطلب قبل وجهة المعرّف (مثل عرض مؤلف)
router.get("/author/create", author_controller.author_create_get);

// ط‫لب POST لإنشاء مؤلف
router.post("/author/create", author_controller.author_create_post);

// ط‫لب GET لحذف مؤلف
router.get("/author/:id/delete", author_controller.author_delete_get);

// طل‫ب POST لحذف مؤلف
router.post("/author/:id/delete", author_controller.author_delete_post);

// ط‫لب GET لتحديث مؤلف
router.get("/author/:id/update", author_controller.author_update_get);

// ط‫لب POST لتحديث مؤلف
router.post("/author/:id/update", author_controller.author_update_post);

// ط‫لب GET لمؤلف واحد
router.get("/author/:id", author_controller.author_detail);

// ط‫لب GET لقائمة جميع المؤلفين
router.get("/authors", author_controller.author_list);

/// وجهات‫ نوع الكتاب GENRE ///

// طل‫ب GET لإنشاء نوع كتاب. يجب أن يأتي هذا الطلب قبل الوجهة التي تعرض نوع الكتاب (يستخدم المعرّف)
router.get("/genre/create", genre_controller.genre_create_get);

// طل‫ب POST لإنشاء نوع كتاب
router.post("/genre/create", genre_controller.genre_create_post);

// طل‫ب GET لحذف نوع كتاب
router.get("/genre/:id/delete", genre_controller.genre_delete_get);

// ط‫لب POST لحذف نوع كتاب
router.post("/genre/:id/delete", genre_controller.genre_delete_post);

// ط‫لب GET لتحديث نوع كتاب
router.get("/genre/:id/update", genre_controller.genre_update_get);

// طل‫ب POST لتحديث نوع كتاب
router.post("/genre/:id/update", genre_controller.genre_update_post);

// ‫طلب GET لنوع كتاب واحد
router.get("/genre/:id", genre_controller.genre_detail);

// طل‫ب GET لقائمة جميع أنواع الكتب
router.get("/genres", genre_controller.genre_list);

/// طر‫ق نسخ الكتب BOOKINSTANCE ///

// طل‫ب GET لإنشاء نسخة كتاب BookInstance. يجب أن يأتي هذا الطلب قبل الوجهة التي تعرض نسخة الكتاب (يستخدم المعرّف)
router.get(
  "/bookinstance/create",
  book_instance_controller.bookinstance_create_get,
);

// ط‫لب POST لإنشاء نسخة كتاب
router.post(
  "/bookinstance/create",
  book_instance_controller.bookinstance_create_post,
);

// ط‫لب GET لحذف نسخة كتاب
router.get(
  "/bookinstance/:id/delete",
  book_instance_controller.bookinstance_delete_get,
);

// ط‫لب POST لحذف نسخة كتاب
router.post(
  "/bookinstance/:id/delete",
  book_instance_controller.bookinstance_delete_post,
);

// ط‫لب GET لتحديث نسخة الكتاب
router.get(
  "/bookinstance/:id/update",
  book_instance_controller.bookinstance_update_get,
);

// ط‫لب POST لتحديث نسخة كتاب
router.post(
  "/bookinstance/:id/update",
  book_instance_controller.bookinstance_update_post,
);

// طل‫ب GET لنسخة كتاب واحدة
router.get("/bookinstance/:id", book_instance_controller.bookinstance_detail);

// ط‫لب GET لقائمة جميع نسخ الكتاب
router.get("/bookinstances", book_instance_controller.bookinstance_list);

module.exports = router;

تطلب الوحدة كائن Express ثم تستخدمه لإنشاء كائن Router، إذ تُضبَط جميع الوجهات في هذا الكائن الذي يُصدَّر لاحقًا. تُعرّف الوجهات إما باستخدام تابع ‎.get()‎ أو تابع ‎.post()‎ للكائن Router، وتُعرّف جميع المسارات باستخدام سلاسل نصية، إذ لا نستخدم أنماط سلاسل نصية أو تعابيرًا نمطية. تستخدم الوجهات التي تعمل على بعض الموارد المحددة (مثل الكتاب) معاملاتِ المسار للحصول على معرّف الكائن من عنوان URL، وتُستورَد جميع دوال المعالجة من وحدات المتحكمات التي أنشأناها في القسم السابق.

تحديث وحدة وجهة صفحة الفهرس

ضبطنا جميع وجهاتنا الجديدة، ولكن لا يزال لدينا الوجهة إلى الصفحة الأصلية، ولكن لنعيد التوجيه إلى صفحة الفهرس الجديدة التي أنشأناها في المسار '‎/catalog' بدلًا من ذلك.

افتح الملف ‎/routes/index.js وضع الدالة التالية بدلًا من الوجهة الحالية:

// الحصول على الصفحة الرئيسية
router.get("/", function (req, res) {
  res.redirect("/catalog");
});

ملاحظة: استخدمنا تابع الاستجابة redirect()‎ الذي يؤدي إلى إعادة التوجيه إلى الصفحة المحددة، مع إرسال رمز حالة HTTP الذي هو "302 Found" افتراضيًا، ويمكنك تغيير رمز الحالة المُعاد إن لزم الأمر وتوفير مسارات مطلقة أو نسبية. يمكنك الاطلاع على مقال رموز الإجابة في HTTP والمقال كيفية استكشاف وإصلاح رموز أخطاء HTTP الشائعةعلى أكاديمية حسوب لمزيدٍ من المعلومات حول رموز حالة HTTP.

تحديث الملف app.js

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

var indexRouter = require("./routes/index");
var usersRouter = require("./routes/users");
const catalogRouter = require("./routes/catalog"); // استيراد الوجهات لمنطقة "الدليل" في الموقع

ضِف بعد ذلك وجهة الدليل catalog إلى مكدس البرمجيات الوسيطة بعد الوجهات الأخرى من خلال إضافة السطر الثالث التالي بعد السطرين الآخرين الموجودين مسبقًا في الملف:

app.use("/", indexRouter);
app.use("/users", usersRouter);
app.use("/catalog", catalogRouter); // إضافة وجهات الدليل إلى سلسلة البرمجيات الوسيطة

ملاحظة: أضفنا وحدة دليلنا في المسار '‎/catalog' الذي سيكون البادئة لجميع المسارات المحدَّدة في وحدة الدليل، فمثلًا سيكون عنوان URL هو /catalog/books/ للوصول إلى قائمة الكتب.

يجب أن يكون لدينا الآن مسارات ودوال هيكلية مُفعَّلة لجميع عناوين URL التي سندعمها في موقع المكتبة المحلية LocalLibrary.

اختبار الوجهات

أولًا، ابدأ تشغيل موقع الويب بالطريقة المعتادة، فالطريقة الافتراضية في أنظمة التشغيل هي:

// نظام ويندوز
SET DEBUG=express-locallibrary-tutorial:* & npm start

// ماك أو لينكس
DEBUG=express-locallibrary-tutorial:* npm start

ولكن إذا سبق لك إعداد nodemon، فيمكنك بدلًا من ذلك استخدام ما يلي:

npm run serverstart

انتقل بعد ذلك إلى عدد من عناوين URL الخاصة بموقع المكتبة المحلية LocalLibrary وتحقق من عدم ظهور صفحة خطأ (HTTP 404). إليك مجموعة صغيرة من عناوين URL:

الخلاصة

أنشأنا جميع الوجهات الخاصة بموقعنا، مع دوال المتحكمات الوهمية التي يمكننا ملؤها بتقديم كامل في مقالات لاحقة، وتعلمنا الكثير من المعلومات الأساسية حول وجهات Express والتعامل مع الاستثناءات وبعض الأساليب لبناء الوجهات والمتحكمات.

سننشئ في المقال التالي صفحة ترحيب مناسبة للموقع باستخدام العروض (القوالب) والمعلومات المخزنة في نماذجنا، وسننشئ استمارات HTML وشيفرة معالجة الاستمارات لبدء تعديل البيانات التي يخزنها الموقع.

ترجمة -وبتصرُّف- للمقال Express Tutorial Part 4: Routes and controllers.

اقرأ أيضًا


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

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

بتاريخ 1 ساعة قال احمد قابل هاشم ألصميدعي:

قمت بعمل جميع الخطوات اعلاه وعند اختبار الوجهات ظهر لي الخطا التالي 

Error: Route.post() requires a callback function but got a [object Undefined]

 

testl.zip 48.7 kB · 0 تنزيلات

لديك خطأ إملائي بسيط في ملف catalog.js في السطر 114 كتبت 

router.post('/bookinstance/:id/update' , book_instance_controller.bookinstane_update_post);

لاحظ .bookinstane_update_post الكلمة bookinstance كتبتها بالشكل التالي bookinstane لاحظ حرف ال c لذلك قم بتعديلها إلى 

router.post('/bookinstance/:id/update' , book_instance_controller.bookinstance_update_post);

 

رابط هذا التعليق
شارك على الشبكات الإجتماعية

بتاريخ 11 دقائق مضت قال عبدالباسط ابراهيم:

لديك خطأ إملائي بسيط في ملف catalog.js في السطر 114 كتبت 

router.post('/bookinstance/:id/update' , book_instance_controller.bookinstane_update_post);

لاحظ .bookinstane_update_post الكلمة bookinstance كتبتها بالشكل التالي bookinstane لاحظ حرف ال c لذلك قم بتعديلها إلى 

router.post('/bookinstance/:id/update' , book_instance_controller.bookinstance_update_post);

 

قمت بالتعديل وفعلا تم حل الخطا ولكن ظهر لي خطا اخر يقول Error: Route.get() requires a callback function but got a [object Undefined]

 

رابط هذا التعليق
شارك على الشبكات الإجتماعية

بتاريخ 4 دقائق مضت قال احمد قابل هاشم ألصميدعي:

قمت بالتعديل وفعلا تم حل الخطا ولكن ظهر لي خطا اخر يقول Error: Route.get() requires a callback function but got a [object Undefined]

 

اعتذر عن الالتباس تم حل المشكلة 

 

رابط هذا التعليق
شارك على الشبكات الإجتماعية



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...