البحث في الموقع
المحتوى عن 'مدخل إلى express'.
-
التعريف بالمشروع هذه سلسلة من الدّروس مُوجّهة للمبتدئين بتطوير الويب، تهدف إلى تعليم استخدام بيئة Node.js وإطار العمل Express من خلال بناء مدوّنة متكاملة تسمح للكُتَّاب بإضافة التّدوينات وتسمح للزوّار بإنشاء حسابات والتّعليق على التُدوينات. لن تقتصر هذه السّلسلة على شرح Express، بل ستقدّم شرحًا (نأمل أن يكون وافيًا) للعديد من المفاهيم المتعلّقة بتطوير الويب من جهة الخوادم (Server-side web development)، حيث سنتطرّق إلى تثبيت خادوم MySQL مع شرح استخدامه في Node.js بالإضافة إلى آليّة عمل بروتوكول HTTP ونظام إدارة المستخدمين وإنشاء الجلسات، في نهاية السّلسلة سنُلقي نظرة على مواضيع تحسين الأداء والأمان قبل نشر المشروع على الويب. في نهاية هذه السّلسلة من المفترض أن يكون المتعلّم قادرًا على التّعامل مع بيئة Node.js بسهولة ويمكنه إنشاء الخوادم وقواعد البيانات وإنشاء برامج تصل بين هذه الأجزاء. إذا كنت قادمًا من عالم PHP، فستكون هذه السّلسلة مناسبة لك أيضًا على حدٍّ سواء. Node.js لعلّك سمعت من قبل بـNode.js، لكنّها ما تزال غامضةً بشكل أو بآخر خصوصًا بين المطوّرين العرب، والسبب ربّما يكون ضعف الرّغبة في التّغيير أو صعوبة تأمين استضافة مشاريعها مقارنةً باستضافة مشاريع PHP أو غير ذلك من الأسباب. Node.js هي بيئة تطوير تسمح لنا بكتابة البرامج وتنفيذها باستخدام JavaScript، اللّغة الّتي كانت حتى وقت قريب حبيسة المتصفّح؛ لكنّها لم تعد كذلك بل أصبحت تُستخدم في كتابة مشاريع الويب وتطبيقات سطح المكتب وحتى التّطبيقات الّتي تعمل في الطّرفيّة، وربّما يعود الفضل في ذلك إلى Node.js ذاتها. إن كنت استخدمت PHP مع Apache لإنشاء موقع من قبل، فستجد أن Node.js تستطيع القيام بالمهمّة ذاتها وأكثر، وبأسلوب أبسط وأكثر تنظيمًا وتوفيرًا للوقت. Express لعلّ Express أهمّ مكوّنات مشروعنا، وعليه سينصبّ معظم شرحنا. Express باختصار هو إطار عمل لمشاريع الويب يعمل في بيئة Node.js، فهو بالنّسبة لـNode.js يقابل Laravel بالنّسبة لـPHP، وإن كان يختلف عنه كثيرًا في الفكرة الّتي يقوم عليها، إذ أنّ Express يلتزم بفلسفة Node.js الّتي تهدف إلى تجزئة المشاريع الضّخمة إلى وحدات، لن تجد في Express ذاته وحدات تتعامل مع قواعد البيانات أو تتولّى إدارة الجلسات وحماية كلمات المرور وما إلى ذلك، بل عليك أن تعتاد أنّ لكلّ شيء في Node.js وحدة مستقلّة يمكن استيرادها واستخدامها مع الوحدات الأخرى لنحصل على مشروع يسهل تطويره وصيانته دون الاعتماد على مكوّن ضخم من جهة مُطوّرة واحدة قد يكون مصير تطويره إلى الإهمال في المستقبل. يتولّى Express إدارة الرّوابط وتوجيهها فقط، ويمكن توسيعه باستخدام ما يُسمّى "البرامج الوسيطة" (middleware) التي تُشبه إلى حدّ ما إضافات المتصفّح التي تستخدمها، فهي تضيف المزيد من المزايا إلى الوظيفة الرئيسيّة لـExpress، وسنجد مثلاً برنامجًا وسيطًا يحفظ الجلسات باستخدام الكعكات (cookies) وآخر يخزّن نتائج الاستعلامات بشكل مؤقّت لتسريع استجابة الخادوم... من الواجب أن نذكر أنّ Express ليس إطار العمل الوحيد المتوفّر في Node.js، ولكنّه قد يكون الأشهر لبساطته الشّديدة وهيكليّته الممتازة. يبدو المستقبل واعدًا لمشاريع مثل Koa الّذي يستفيد من المزايا القادمة إلى الإصدارات المستقبليّة من JavaScript لتحسين كتابة الشّيفرة أكثر. يُشرف على تطوير Koa الفريق المطوّر لـExpress ذاته. MySQL سنستخدم لغة قواعد البيانات الشّهيرة SQL (بنكهة MySQL إن جاز التّعبير) لتخزين التّدوينات والتّعليقات ومعلومات المستخدمين، إن كنت لا تعرف الكثير عن SQL فلا بأس، لأنّ صياغتها غاية في السّهولة وتكاد تُشبه جمل اللغة الإنكليزيّة الحقيقيّة! تثبيت Node.js للحصول على آخر إصدار من Node.js: إذا كنت تستخدم Windows أو Mac فتوجّه إلى الصّفحة الرئيسية لموقع Node.js سيقوم الموقع بالتّعرّف على نظامك وبنيته وانتقاء برنامج التّثبيت المناسب، ليس عليك سوى الضّغط على زر Install لتنزيل برنامج التّثبيت ثم فتحه ومتابعة الخطوات. إذا كنت تستخدم Arch Linux، فستجد أحدث إصدار من Node.js ضمن مستودعات مستخدمي Arch (AUR). ويمكن تثبيته بالأمر التالي إذا كنت تستخدم yaourt (الأمر مشابه بالنّسبة لكلّ البرامج التي توفّر وصولاً إلى AUR? yaourt -S nodejs --noconfirm سيُطلب منك إدخال كلمة المرور إلى مستخدمك أو كلمة المرور إلى المستخدم الجذر (إن وُجدت). كما يمكن التّثبيت بالطّريقة المشروحة في الخيار التّالي. إذا كنت تستخدم توزيعة Linux أخرى، فقد تجد إصدارًا قديمًا من Node.js ضمن مستودعات توزيعتك، لذا يُنصح بتثبيت Node.js عن طريق مُدير إصدارات Node.js المتوفّر على GitHub، ويتمّ التثبيت بالطّريقة التالية: قم بتنزيل آخر نسخة من مدير إصدارات Node.js عن طريق زر Download ZIP في صفحة المشروع على GitHub. فكّ ضغط الملفّ الذي قمت بتنزيله انتقل بالطّرفيّة إلى مسار المجلد الناتج عن العمليّة السابقة، مثلاً: cd ~/Downloads/n-master ثم قم بتنفيذ أمر بناء المشروع: sudo make install قم بتثبيت آخر إصدار مستقرّ من Node.js مستخدمًا الأمر n الذي يوفّره مدير إصدارات Node.js: sudo n stable سيُطلب منك إدخال كلمة المرور إلى مستخدمك أو كلمة المرور إلى المستخدم الجذر إن وُجدت. من فوائد مُدير إصدارات Node.js إمكانيّة التبديل بشكل سريع بين عدّة إصدارات من Node.js، فقد ترغب أحيانًا بتجربة بعض المزايا المتوفّرة في إصدار غير مستقرّ (مثل v0.11 الذي يتضمّن بعضًا من مُكوّنات ECMAScript 6) مُستخدمًا الأمر n latest، ولكنّك ترغب بالعودة للعمل على مشاريع جادّة ضمن إصدار مستقرّ. ولهذا يمكنك استخدام الأمر n stable. يمكن أيضًا تثبيت الإصدارات الحديثة من Node.js على Ubuntu والتّوزيعات الأخرى باتّباع التّعليمات الرّسميّة المتوفّرة على صفحة Node.js على GitHub. للتحقّق من تثبيت Node.js اكتب الأمر التالي في الطّرفيّة (أو سطر أوامر Windows): node -v لتحصل على نتيجة برقم إصدار Node.js الذي قمت بتثبيته، مثل v0.10.33. إن كانت النتيجة تُفيد بعدم وجود الأمر مثل bash: node: command not found (على Linux) أو node is not recognized as an internal or external command... (على Windows)، فتحقّق من اتّباع الخطوات السّابقة مجدًدًا. إنشاء المشروع لنبدأ العمل بشكل نظيف، أنشئ مُجلّدًا جديدًا في مكان ما في جهازك وانتقل إليه باستخدام الطّرفيّة، سأقوم بإنشاء مجلّد ضمن مسار مُستخدمي /home/f/ وأسمّيه my-blog، ثم سأنتقل إليه باستخدام الأمر cd (اختصارًا لـchange directory) (الذي يتطابق في Linux وMac وWindows): cd /home/f/my-blog في Windows، قد يكون الأمر مُشابهًا لهذا: cd C:\Users\f\my-blog سنستخدم الأمر init الذي يوفّره مُدير حزم Node (npm) لإنشاء مشروع جديد، افتح الطّرفيّة (سطر الأوامر في Windows) واكتب الأمر التالي: npm init سيطرح البرنامج عليك مجموعة من الحقول لتعبئتها: name: اسم المشروع، ويقترح npm اسم المجلد الحالي كاسم للمشروع، ويمكنك الأخذ بالاقتراح بترك الحقل فارغًا وضغط Enter. version: إصدار المشروع (يمكنك تركه كما هو). description: وصف للمشروع. entry point: الملفّ الرئيسيّ الذي ينطلق منه المشروع، يمكنك تركه كما هو وإنشاء الملف index.js لاحقًا. test command: الأمر الذي يجب أن ينفّذه npm عندما يطلب منه تنفيذ الاختبارات على المشروع، أي عندما ينفذ الأمر npm test ضمن مجلد المشروع الرئيسيّ. سنتركه فارغًا الآن. git repository: مسار مستودع git الذي ستستخدمه لإدارة المشروع، يمكن أن يكون رابط http:// أو git:// ويمكن تعديله لاحقًا. keywords: الكلمات المفتاحية للمشروع مفصولة بفاصلة لاتينية (,)، أمثلة: blog, mysql, expressjs, tutorial. author: كاتب المشروع. license: رخصة المشروع، يمكن استخدام أي رخصة مثل GPLv2 أو MIT. بعد الانتهاء سيعرض البرنامج عليك المعلومات التي أدخلتها وهي جاهزة للكتابة إلى ملفّ package.json، اكتب yes لكتابة الملفّ. الملفّ package.json هو نقطة الانطلاق في كلّ مشاريع Node.js، ويُستخدم للتعريف بالمشروع ووصفه عند نشره في سجلّ حزم npm، والأهم من ذلك أنّه يستخدم لحفظ ما يعتمد عليه المشروع من حزم مع أرقام الإصدارات المطلوبة لكلّ حزم؛ ومن خلال هذا الملفّ يمكن إستنساخ المشروع وإعادة تثبيت المتطلّبات للتعديل عليه في وقت آخر أو من قبل أشخاص آخرين. تثبيت متطلّبات المشروع سيعتمد مشروعنا على إطار العمل Express كما هو واضح، بالإضافة إلى قواعد بيانات MySQL لتخزين التدوينات والمستخدمين وتعليقاتهم، سنحتاج أيضًا إلى بعض المتطلّبات الأخرى التي سنثبّتها في وقت الحاجة إليها. لتثبيت أحدث إصدار من express وحفظه كمتطلّب ضمن ملفّ package.json، نفّذ الأمر التّالي: npm install express --save ملاحظة: سنعتمد الإصدار الرّابع في هذه السلسّلة لكونه أحدث إصدار في وقت كتابتها، للتأكّد من تثبيت الإصدار الرّابع حتّى بعد صدور إصدارات أحدث، يمكنك استخدام الأمر: npm install express@4.10.* --save يحتاج تثبيت MySQL إلى خطوتين: الأولى تثبيت الخادوم الذي يوفّر قاعدة البيانات، ويُنجز بطريقة مختلفة لكل نظام تشغيل: في Windows وMac OS X، يمكن تثبيته بتنزيل برنامج التّثبيت المناسب لإصدار النّظام وبنيته من الموقع الرّسمي ثمّ اتّباع خطوات التّثبيت كما في تثبيت أي برنامج آخر. في Arch Linux، أنصح باستخدام MariaDB، وهي بديل مطابق تمامًا لـMySQL ويحلّ محلّه بدون الاضطرار لتعديل أي جزء من الشيفرة، ويمكن تثبيته من خلال مستودعات مستخدمي Arch بالأمر التّالي: yaourt -S mariadb --noconfirm في توزيعات Linux الأخرى مثل Ubuntu وFedora، فقد تتوفّر MariaDB وMySQL في المستودعات الرّسميّة ويمكن تثبيتها باستخدام apt-get وyum. الخطوة الأخرى تتضمّن تثبيت عميل MySQL (أو ما يسمى MySQL client)، وهو الجزء الذي سيتواصل مع الخادوم ليجلب نتائج الاستعلام من قاعدة البيانات ويوفّرها لمشروعنا، وفي حالتنا هذه ليس سوى وحدة Node.js يمكن تثبيتها بسهولة عبر npm install واستخدامها ضمن مشروعنا؛ تتوفّر العديد من الوحدات التي تقدّم إمكانية التواصل مع خادوم قواعد بيانات MySQL، ومن أفضلها الوحدة mysql التي يمكن تثبيتها بتنفيذ الأمر: npm install mysql --save نصيحة: يمكن تثبيت الحزمتين بأمر واحد: npm install express mysql --save بعد تثبيت الحزمتين، سنلاحظ إضافة حقل جديد في ملفّ package.json يوضّح متطلّبات المشروع التي حفظناها إلى الآن (قد تختلف أرقام الإصدارات لديك): "dependencies": { "express": "^4.10.6", "mysql": "^2.5.4" } إنشاء قاعدة البيانات وإدخال بعض التّدوينات كعيّنة وجود بعض التدوينات سيساعدنا في بناء الواجهة ورؤية التغييرات بسهولة أكبر، لذلك سنقوم بإنشاء قاعدة البيانات وإدخال بعض التّدوينات إليها قبل كلّ شيء. لإنشاء قاعدة البيانات وإدخال التدوينات، سنقوم بالتواصل مع الخادوم عبر الطّرفيّة، مستخدمين البرنامج mysql الذي يتمّ تثبيته تلقائيًّا عند تثبيت خادوم MySQL أو MariaDB. افتح الطّرفية ونفّذ الأمر: mysql -u root ملاحظة: إن اخترت اسمًا للمستخدم وكلمة مرور مختلفين أثناء التثبيت، فيمكن إدخالها بالطريقة الآتية (سيطلب منك إدخال كلمة المرور إلى هذا المستخدم): mysql -u username -p ستظهر شاشة مشابهة لهذه: ملاحظة: إن واجهتك مُشكلات في بدء البرنامج mysql، جرّب إعادة تشغيل النّظام. ملاحظة (2): يمكنك الاستغناء عن استعمال صدفة MySQL إذا كنت قد ثبّتت PHPMyAdmin على جهازك، حيث بإمكانك إدخال الأوامر ذاتها في مربّع الاستعلامات. في صدفة MySQL هذه يمكننا إدخال أوامر MySQL ليقوم الخادوم بتنفيذها على الفور. سنقوم بإنشاء قاعدة بيانات المدوّنة، وسنسمّيها myblog: CREATE DATABASE myblog; النتيجة التّالية دليل على نجاح التنفيذ: Query OK, 1 row affected (0.00 sec) اتّصل بقاعدة البيانات الجديدة بالأمر: connect myblog لنقم بإنشاء جدول للمستخدمين وآخر للتدوينات ثم لندخل مستخدمًا مع 4 تدوينات كتبها في أيام مختلفة، يمكنك نسخها ولصقها في صدفة MySQL فحسب: CREATE TABLE `users` (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50), password VARCHAR(500) NOT NULL, full_name VARCHAR(50), is_author BOOLEAN DEFAULT , UNIQUE INDEX (username)); INSERT INTO `users` (username, password, full_name, is_author) VALUES ("admin", "$2a$08$Z3FpAQwRgj7W0i71TtizFO7QDjpsIRNJfHh6mLgRJRJBtheKJh1Tu", "admin", 1); CREATE TABLE `posts` (id INT NOT NULL PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100), body LONGTEXT, date TIMESTAMP, author_id INT, slug VARCHAR(50), UNIQUE INDEX (slug), FOREIGN KEY (author_id) REFERENCES `users` (id)); INSERT INTO `posts` (title, body, date, author_id, slug) VALUES ("مرحبًا بالعالم!", "مرحبًا بكم في مدوّنتي المتواضعة!", "2014-12-29", 1, "hello-world"); INSERT INTO `posts` (title, body, date, author_id, slug) VALUES ("اقتباسات (1)", "الغني لو سئل عن تحسين العمل والحياة فسوف يقول: نحن نعرف أن البؤس غيرمفرح والواقع أن البؤس مادام بعيداً عنا فإننا نتسلح بفكرة أنه غير مفرح. ولكن لا تتوقع منا أن نفعل أي شيء بصدده. نحن آسفون لطبقاتكم الدنيا مثل مانحن آسفون لقطة جرباء...غير أننا سنقاتل كالمردة ضد أي تحسين لظرفكم. نحن نشعر انكم مأمونون أكثر وأنتم في حالكم هذا. إن الواقع الراهن يناسبنا ولسنا مستعدين لمخاطرة تحريركم حتى بساعة إضافية في اليوم هكذا يا إخوتي الأعزاء إن كان عليكم ان تعرقوا لدفع رحلاتنا إلى إيطاليا فلتعرقوا ولتحل عليكم اللعنة ― جورج أورويل، متشردًا في باريس ولندن", "2014-12-30", 1, "quotes-1"); INSERT INTO `posts` (title, body, date, author_id, slug) VALUES ("اقتباسات (2)", "التليفزيون يُغرقك في بحر من الأصوات والألوان بحيث لا تجد الوقت الكافي لتفكر أو تنتقد... إنه يقدم لك الأفكار الجاهزة ولا يسمح لك بالانتقاد الذي يسمح به الكتاب. ― راي برادبري، فهرنهايت 451", "2014-12-31", 1, "quotes-2"); INSERT INTO `posts` (title, body, date, author_id, slug) VALUES ("اقتباسات (3)", "أستطيع أن أقول لك يا بنيّ إنّ السّعادة ينبوع يتفجّر من القلب، لا غيث يهطل من السّماء، وأنّ النّفس الكريمة الرّاضية البريئة من أدران الرّذائل وأقذارها، ومطامع الحياة وشهواتها، سعيدة حيثما حلّت. [...] فمن أراد السّعادة فلا يسأل عنها المال والنّسب، وبين الفضّة والذّهب، والقصور والبساتين، والأرواح والرّياحين، بل يسأل عنها نفسه الّتي بين جنبيه فهي ينبوع سعادته وهنائه إن شاء، ومصدر شقائه وبلائه إن أراد. ― مصطفى لطفي المنفلوطي، الفضيلة", "2015-01-01", 1, "quotes-3"); لكلّ سطر في جدول التّدوينات الحقول التالية: العنوان title ونص التدوينة body وتاريخها date ومُعرّف الكاتب author_id الذي يُشير إلى أحد الكُتّاب المُسجلّين في جدول المستخدمين users، ثمّ slug وهو العنوان بالإنكليزية الملائم لاستخدامه ضمن رابط التّدوينة مثل hello-world في http://myblog/posts/hello-world. نحن الآن جاهزون للعمل! في الدّرس القادم سنبدأ بإنشاء الصّفحة الرّئيسيّة لمدوّنتنا، والّتي ستعرض التّدوينات الّتي أضفناها لتوّنا.
-
في الدّرس السّابق قمنا بثتبيت Node.js وخادوم MySQL وبقيّة متطلّبات المشروع، حان الوقت لنبدأ العمل الحقيقيّ! إنشاء صفحة المدوّنة الرئيسيّةأهمّ ما تعرضه الصّفحة الرئيسيّة لكلّ مدوّنة عادةً آخر التّدوينات بتاريخ كتابتها من الأحدث للأقدم، وسنركّز الآن على تطبيق هذا الجزء على أنّ نتوسّع في إضافة الميّزات في وقتٍ لاحق. أنشئ الملفّ index.js الّذي يُمثّل نقطة انطلاق مشروعنا، ولنبدأ باستيراد Express ضمنه: var express = require("express");لنُنشئ الآن تطبيق Express جديد، وهو يمثّل الخادوم الذي يُدير مدوّنتنا بالكامل، يتمّ إنشاء تطبيق Express ببساطة باستدعاء دالّة express الّتي أنشأناها لتوّنا: var express = require("express"); var app = express();تكون الصّفحة الرئيسيّة للمدوّنة على الرّابط الجذر للموقع عادةً، وهو ما نُعبّر عنه بـ/، سنطلب من تطبيقنا الاستجابة للطّلبات التي تصل إلى هذا الرّابط بعرض صفحة HTML تحوي آخر 10 تدوينات مرتّبة وفق تاريخ كتابتها من الأحدث إلى الأقدم: var express = require("express"); var app = express(); app.get("/", function(request, response) { // أرسل HTML });لندع إرسال الصّفحة جانبًا ولنفهم أسلوب استخدام Express، لكلّ تطبيق Express وظائف أربعة تُستخدم في استقبال وتوجيه الطّلبات، وهي get وpost وput وdel، وهذه الوظائف توافق أفعال HTTP الشّائعة. ولكن ما هي أفعال HTTP؟ كيف يعمل HTTP؟في كلّ مرّة تزور صفحة على الويب فإنّ متصفّحك يرسل للخادوم الذي يستضيف الموقع طلبًا بالحصول (GET) على المحتوى في الرابط الذي كتبته، يكون طلب HTTP هذا مشابهًا للمثال التّالي (المُبسَّط عمدًا): GET www.myblog.com/hello-world HTTP/1.1 Accept: text/html Accept-Language: ar-sy,ar; User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:34.0) Gecko/20100101 Firefox/34.0تُسمّى الحقول Accept وAccept-Language... بترويسات الطّلب (Request Headings)، ولكلّ ترويسة معنىً بالنّسبة للخادوم الذي يستقبل الطّلب، فمثلاً يقوم المتصفّح في الحقل User-Agent بالتّعريف عن نفسه، وهو ما يسمح للخادوم بإرسال جواب مخصّص لكلّ متصفّح مثلاً (إن شاء)، وفي الحقل Accept-Language يُرسِل المتصفّح اللّغات الّتي يرغب المستخدم برؤية الجواب بها، فيقوم الخادوم بإرسالة الصّفحة بالعربيّة (سوريا) ar-sy في حالتنا إن توفّرت لديه، أو بالعربية ar كخيار ثانٍ... وهكذا. يردّ الخادوم على الطّلب بجواب HTTP (HTTP Response) الذي يُشبه مثالنا هذا: HTTP/1.1 200 OK Content-Type: text/xml; charset=utf-8 Content-Length: 3918 <!DOCTYPE html> <html lang="ar"> <head> <title>مُدوّنتي - مرحبًا بالعالم!</title> </head> <body> مرحبًا بكم في مدوّنتي المتواضعة! </body> </html>السّطر الأوّل في الجواب يُسمّى سطر الحالة، ويتضمّن حالة الطّلب (حيث الرّقم 200 يعني أن الخادوم تلقّى الطّلب وردّ عليه بما هو متوقّع)، بقيّة السّطور هي ترويسات الجواب (Response Headings) الّتي تعني كلّ واحدة منها شيئًا ما لمستقبل الجواب (المتصفّح). يلي الترويسات متن الجواب (Response Body) الذي يحوي في حالتنا صفحة HTML الّتي سيقوم المتصفّح بعرضها على المستخدم. فعل GET الذي استخدمناه ليس وحيدًا، فهناك أفعال أخرى مثل POST الذي يُستخدم في المتصفّح لإرسال الحقول التي يُعبّئها المستخدم (كتعبئة حقل تسجيل الدّخول)، والفعل DELETE الذّي يستخدم ليطلب من الخادوم حذف محتوى ما (مثل حذف تدوينة من قبل المستخدم). الجدير بالذّكر أن الخادوم حرّ التّصرّف بالطّلبات التي يتلقّاها، والطّريقة التي شرحناها بهذه الأفعال مبنيّة على التّقاليد الشّائعة لاستخدامها، فلا شيء في الحقيقة يمنع الخادوم من حذف تدوينة عندما يتلقّى طلب GET بدلاً من DELETE وإنّما هو عُرف متّفق عليه. لنعد الآن إلى مثالنا السّابق، تقبل الوظيفة get مُعاملين أولهما الرّابط المطلوب التّعامل معه، والأخرى دالّة تقرأ الطّلب وتعدّل جوابه قبل إرسال الجواب للمُتصفّح، يمكن إرسال متن الجواب للمتصفّح من خلال الوظيفة send() للكائن response: var express = require("express"); var app = express(); app.get("/", function(request, response) { var html = "<!DOCTYPE html><html lang='ar'>" + "<head><title>مُدوّنتي!</title></head>" + "<body>" + posts.map(function(post) { return "<li>" + post.title + "</li>"; }).join("") + "</body></html>" response.send(html); });في الحالة الافتراضية سيكون جواب هذا الطّلب بالرّمز 200 OK مع متن يطابق محتوى المُتغيّر html. سنتعرّف فيما بعد على كيفيّة تغيير رموز الحالة بحيث نُرسل الرّمز الشّهير 404 Not Found عندما لا نجد تدوينة على الرّابط المطلوب. سيتوقّف البرنامج في هذه الحالة مُعطيًا خطأ بسبب كون posts غير معرّف، كلّ ما علينا الآن هو جلب التّدوينات من خلال قاعدة البيانات وتخزينها ضمن المُتغيّر posts، نحتاج إذًا لتنفيذ استعلام MySQL لجلب أحدث التدوينات، ولهذا سنقوم باستيراد وحدة mysql التي قمنا بتثبيتها وتأمين الاتصال بقاعدة البيانات: var express = require("express"); var mysql = require("mysql"); var connection = mysql.createConnection({ host: "localhost", user: "root", password: "", database: "myblog" }); connection.connect(); var app = express(); app.get("/", function(request, response) { connection.query( "SELECT * from `posts` ORDER BY date DESC LIMIT 10;", function(err, posts) { if (err) throw err; var html = "<!DOCTYPE html><html lang='ar'>" + "<head><title>مُدوّنتي!</title></head>" + "<body>" + posts.map(function(post) { return "<li>" + post.title + "</li>"; }).join("") + "</body></html>"; response.send(html); }); }); ملاحظة: لا تنسَ تغيير اسم المستخدم وكلمة المرور ليتوافقا مع ما اخترته أثناء تثبيت MySQL. تمتلك وحدة mysql وظيفة createConnection() الّتي تُعيد لنا نسخة من اتّصال بقاعدة البيانات الّتي حدّدناها، والذي يمكن بدؤه باستدعاء الوظيفة connect() ثم تّنفيذ الاستعلامات query() الّتي تتمّ بأسلوب غير متزامن (asynchronous) لتُعيد لنا الصّفوف النّاتجة عن الاستعلام ضمن المعامل الثّاني للدّالة (function(err, posts) { ... }) الّتي تُستدعى بعد انتهاء الاستعلام. بهذه السّطور القليلة التي يمكن فهمها بالقليل من الجهد تمكنّنا من إنشاء مدوّنة بسيطة، وهنا يبرز جمال Node.js الذي يسمح للمبتدئين بتطبيق أفكار قد تبدو بعيدة المنال وجعلها واقعًا ملموسًا! الآن حان وقت تجربة المشروع، نحتاج لإخبار Express بالإنصات إلى الطّلبات الّتي ترد على منفذ معيّن على جهازنا (localhost): var express = require("express"); var mysql = require("mysql"); var connection = mysql.createConnection({ host: "localhost", user: "root", password: "", database: "myblog" }); connection.connect(); var app = express(); app.get("/", function(request, response) { connection.query( "SELECT * from `posts` ORDER BY date DESC LIMIT 10;", function(err, posts) { if (err) throw err; var html = "<!DOCTYPE html><html lang='ar'>" + "<head><title>مُدوّنتي!</title></head>" + "<body>" + posts.map(function(post) { return "<li>" + post.title + "</li>"; }).join("") + "</body></html>"; response.send(html); }); }); app.listen(3000);لبدء البرنامج، افتح الطّرفيّة وانتقل إلى مجلّد المشروع، ثم نفّذ الأمر التّالي: node index.js ستتوقّف المؤشّر في الطّرفيّة عن الاستجابة بسبب انشغال هذه الطّرفيّة بتنفيذ البرنامج، اذهب إلى المتصفّح وانتقل إلى الرابط http://localhost:3000/ وشاهد النّتيجة: قد تبدو الصّفحة غاية في البساطة وخالية من أي عُنصر جماليّ، لكنّ ما يهمّنا الآن هو أنّنا قمنا بإنشاء خادوم يتواصل مع قاعدة بيانات ويعرض النّتائج على المستخدم... كلّ هذا في 16 سطرًا من JavaScript! لإنهاء البرنامج عُد إلى الطّرفيّة ذاتها واضغط Ctrl+C. تعرّف على لغة القوالب Jadeبعد أن تأكدنا من تنفيذ المكوّن الرئيسيّ لمشروعنا، سنعمل على تحسين شيفرتنا لجعلها أكثر بساطة وقابلة للتّطوير بسهولة فيما بعد. إذا ألقينا نظرةً على آخر ما كتبناه، سرعان ما نكتشف التّعقيد الذي ستصل إليه شيفرتنا إن أردنا إضافة المزيد من المزايا ضمن HTML، لأنّ هذا يعني إضافة المزيد من النّصّ إلى المتغيّر html بحيث يصبح طويلاً جدًّا وصعب القراءة؛ لا بدّ أن توجد طريقة أفضل من هذه! تتوفّر في كلّ اللّغات طريقة لتوليد صفحات HTML ديناميكيّة على الخادوم، بمعنى أنّه يمكن تغيير بعض محتوياتها وإدخال محتوى مُتغيّر فيها قبل إرسالها إلى المستخدم، هل تساءلت يومًا كيف يعرض فيس بوك لكلّ مستخدم صفحةً خاصّة به؟ بحيث يكون هيكلها متماثلاً لكلّ المستخدمين ولكن محتواها من الأخبار مختلف من مستخدم لآخر، الجواب هو باستخدام القوالب؛ لن نقوم بإنشاء فيس بوك جديد الآن، لكنّنا سنستفيد من ميزات القوالب الدّيناميكيّة لتوليد HTML بدلًا من كتابتها يدويًّا ضمن شيفرتنا! في عالم Node.js ستجد الكثير من لغات القولبة، لكنّ الامتداد الطّبيعيّ لاستخدام Express يكون باعتماد Jade كلغة قولبة كونها بدأت من المُطوّر ذاته، لنُعد كتابة HTML الصّفحة الرّئيسيّة لمدوّنتنا باستخدام Jade: doctype html html(lang="ar") head title "مُدوّنتي!" body for post in posts li #{ post.title }قارن بين نصّ HTML ونصّ Jade الأخير، أوّل ما نلاحظه في Jade هو بساطة صياغتها، فهي تلغي الوسوم النّهائيّة (مثل </head> و</body>) وتستعيض عن ذلك بكونها حسّاسة للمحاذاة، فكون الوسم title مُزاحًا إلى يمين head يعني أنّه محتوىً ضمنه، وكذلك الأمر بالنّسبة لـbody، نلاحظ كذلك دعم Jade للحلقات والمُتغيّرات، وهي من أبرز مزايا لغات القوالب، لأنها تسمح بتوليد عناصر متكرّرة دون الحاجة لكتابتها يدويًّا. سنحتاج أوّلًا لتثبيت Jade وحفظه في متطلّبات المشروع: npm install jade --save احفظ شيفرة Jade السابقة في ملفّ home.jade ضمن مجلّد جديد سمّه views داخل مُجلّد المشروع، ثمّ عُد للملفّ index.js، ولنقم باستخدام Jade عوضًا عن الأسلوب السابق: var express = require("express"); var mysql = require("mysql"); var connection = mysql.createConnection({ host: "localhost", user: "root", password: "", database: "myblog" }); connection.connect(); var app = express(); app.set("view engine", "jade"); app.get("/", function(request, response) { connection.query( "SELECT * from `posts` ORDER BY date DESC LIMIT 10;", function(err, posts) { response.render("home", { posts: posts }); }); }); app.listen(3000);ضبطنا الإعداد view engine في Express إلى القيمة "jade"، يستخدم Express هذا الإعداد عندما يُطلب منه عرض ملفّ ما باستخدام الوظيفة render التّابعة لكائن الجواب response، بحيث يبحث عن مُفسّر لغة القوالب (jade في حالتنا) ويطلب منه تحويل الملفّ "home" إلى HTML، مُمرّرًا له الكائن الذي يحوي المتغيّرات الّتي يحتاجها ({ posts: posts }). يبحث Express عن ملفّات العرض في المجلّد views بشكل افتراضيّ، وهو ما قمنا بإنشاءه للتّوّ.قم بتشغيل البرنامج مرّة أخرى باستخدام الأمر node index.js ثمّ زر الرّابط http://localhost:3000/. لم يتغيّر شيء ظاهر، لكنّنا انتقلنا إلى استخدام لغة قوالب وراء الكواليس، وسنستفيد من هذا بكتابة شيفرة أبسط وأكثر تنظيمًا. لنقم الآن بتعديل القالب home.jade ليبدو بشكل أجمل: doctype html html(lang="ar", dir="rtl") head title "مُدوّنتي!" body style :css body { font-family: Arial, sans-serif; } h1 مُدوّنتي hr for post in posts h2 #{ post.title } p #{ post.body } small بتاريخ #{ post.date }قمنا بتغيير اتّجاه النّص لجعله من اليمين إلى اليسار عبر الخاصة "dir"، ثمّ أدخلنا بعض التنسيق من خلال الوسم "<style>" في HTML، تسمح Jade بكتابة لغات أخرى ضمن القالب مثل كتابة CSS وCoffeeScript أو Markdown أو Sass عبر الصّياغة :language ليتم تحويلها إلى اللّغة المناسبة للمتصفّح إن تطلّب الأمر، وفي هذه الحالة أدخلنا CSS بسيط (الذي لا يحتاج للتّحويل) بكتابة :css قبل الشّيفرة. سنتعرّف على مزيد من مزايا Jade خلال عملنا. تبدو مدوّنتنا بشكل أجمل الآن، لكنّها بالتأكيد تحتاج المزيد من العمل! يمكننا تحسين عرض صيغة التّاريخ باستخدام مكتبة moment للتّعامل مع التّواريخ والوقت، سنحتاج أولاً إلى تثبيتها وحفظها في متطلّبات المشروع: npm install --save moment سنُدخل التّعديلات اللّازمة على الملفّين index.js وhome.jade: var express = require("express"); var mysql = require("mysql"); var moment = require("moment"); moment.locale("ar"); var formatDate = function(date) { return moment(new Date(date)).fromNow(); } var connection = mysql.createConnection({ host: "localhost", user: "root", password: "", database: "myblog" }); connection.connect(); var app = express(); app.set("view engine", "jade"); app.get("/", function(request, response) { connection.query( "SELECT * from `posts` ORDER BY date DESC LIMIT 10;", function(err, posts) { response.render("home", { posts: posts, formatDate: formatDate }); }); }); app.listen(3000);doctype html html(lang="ar", dir="rtl") head title "مُدوّنتي!" body style :css body { font-family: Arial, sans-serif; } h1 مُدوّنتي hr for post in posts h2 #{ post.title } p #{ post.body } small كُتِبَت #{ formatDate(post.date) }يمكن تمرير الدّوال (functions) إلى Jade كما نُمرّر المتغيّرات، وفي حالتنا قمنا بتعريف دالّة تقوم بتنسيق التّاريخ الذي تتلقاه بصياغة نسبيّة (منذ كذا يومًا، منذ ساعتين...) وذلك بالاستفادة من مكتبة moment التي استوردناها وعيّنّا لغة التّاريخ فيها إلى العربيّة. أجرينا التغييرات اللازمة في Jade مستخدمين الدّالة التي فرضناها وأصبحت متوفّرة ضمن القالب: إنشاء صفحة التدوينةمن المعتاد لصفحات التّدوينات أن تكون بهذه الهيئة: http://myblog.com/posts/hello-world، ويمكن أن نشاهد في مدوّنات أخرى روابط تحوي تاريخ كتابة التّدوينة أو رقمًا خاصًّا بها... إلخ، لكنّنا سندع الأمور بسيطة. لدينا حاليًّا 4 تدوينات، ستكون روابطها: /posts/hello-world/posts/quotes-1/posts/quotes-2/posts/quotes-3الثّابت بين هذه الرّوابط هو اعتمادها على الحقل slug الّذي أدخلناه في كلّ سطر في جدول التّدوينات. من غير المنطقيّ أن نُسجّل رابطًا لكلّ تدوينة على حدة في Express، وسيصبح هذا مستحيلاً مع إنشاء تدوينات جديدة. يوفّر Express آليّة للإجابة على الطّلبات الواردة على الروابط التي تطابق نمطًا معيّنًا، وهو في حالتنا /posts/ متبوعًا بحقل متغيّر slug، أو /posts/:slug بصياغة Express، سنضيف الشيفرة التالية إلى برنامجنا (قبل آخر سطر): app.get("/posts/:slug", function(request, response) { var slug = request.params.slug; connection.query("SELECT * from `posts` WHERE slug = ?", [ slug ], function(err, rows) { var post = rows[]; response.render("post", { post: post, formatDate: formatDate }); }); }) نطلب من Express الاستجابة لأي رابط يطابق النمط "/posts/:slug" بالبحث عن التدوينة التي تملك القيمة slug ضمن العمود الموافق في جدول التّدوينات، نلاحظ أنّ Express يوفّر لنا هذه القيمة المتغيّرة من خلال الكائن params التابع لكائن الطّلب request (كائن الطّلب يحوي كذلك ترويسات الطّلب الّتي تحدّثنا عنها في الجزء السّابق). من المهمّ أنّ نحمي قاعدة بياناتنا من العبث وذلك بتجنب هجمات حقن SQL، ولهذا توفّر وحدة mysql دالّة query() ذاتها لكن مع 3 معاملات بدل اثنين فقط، حيث يكون الثاني مصفوفة تحوي القيم الّتي نريد التأكّد من سلامتها (escape) قبل إحلالها محلّ إشارات الاستفهام في استعلاماتنا. هذا أسلوب شائع جدًا في استعلامات SQL، وهو أقلّ ما يمكننا فعله لحماية قاعدة البيانات. لم نقم بعد بإنشاء قالب صفحة التّدوينة، لننشئ ملفًا جديدًا اسمه post.jade ضمن مجلّد views: doctype html html(lang="ar", dir="rtl") head title مُدوّنتي! body style :css body { font-family: Arial, sans-serif; } h1 مُدوّنتي hr h2 #{ post.title } p #{ post.body } small كُتِبَت #{ formatDate(post.date) }لنبدأ برنامجنا، ونذهب إلى الصّفحة http://localhost:3000/posts/hello-world: لدينا الآن بعض المشكلات، ماذا يحدث لو أدخلنا رابطًا لتدوينة غير موجودة؟ جرّب مثلاً http://localhost:3000/posts/another-post: وقع خطأ في تفسير Jade سببه أن المتغيّر post الّذي وصله هو في الحقيقة غير معرّف undefined، لأنّه ما من تدوينة في قاعدة البيانات يطابق حقل slug فيها القيمة another-post، وعندما أجرينا الاستعلام أُعيدت لنا مصفوفة فارغة rows، وفي JavaScript فإنّ محاولة الوصول إلى خاصّة غير موجودة ("0") في عنصر مُعرّف (المصفوفة rows في حالتنا) تُرجع undefined. ما الذي كان علينا فعله لتجنب هذا الخطأ؟ أولاً يجب التأكّد قبل كلّ شيء أنّ الخطأ الذي يقع في مرحلة الاستعلام يتم التّعامل معه (handled) قبل الانتقال لما بعده، انتبه إلى أنّ الاستعلام الذي يتم بنجاح ويعيد مصفوفة فارغة لا يعتبر خطأ، لذا يجب التّعامل مع هذه الحالة أيضًا؛ مبدئيًا سنكتفي بإيقاف تنفيذ الدّالة: app.get("/posts/:slug", function(request, response) { var slug = request.params.slug; connection.query("SELECT * from `posts` WHERE slug = ?", [ slug ], function(err, rows) { if (err || rows.length == ) return; var post = rows[]; response.render("post", { post: post, formatDate: formatDate }); }); }) جرّب الآن إعادة تشغيل البرنامج وزيارة الصّفحة ذاتها... سيستمرّ المتصفّح بمحاولة تحميلها لوقت طويل قبل أن يفشل بسبب انتهاء مهلة الطّلب. لماذا يحدث هذا؟ علينا أن نفهم واحدًا من أهمّ المفاهيم في Express، وهو الكيفيّة التّي تسير بها عمليّة توجيه الرّوابط (routing)، في شيفرتنا الأخيرة سيتوقف Express عند return دون أن يعرف ما ينبغي فعله في الخطوة التّالية، وهذا يجعل البرنامج عالقًا في الفراغ، نحتاج لطريقة نخبر بها Express أن يتابع التّنفيذ ويفعل شيئًا ما عندما تنتهي إحدى وظائف التّعامل مع الرّوابط، ولهذا يعطينا Express دالّة next التي تتوفّر كمعامل ثالث للدالة التّي تتلقّى الرابط: app.get("/posts/:slug", function(request, response, next) { var slug = request.params.slug; connection.query("SELECT * from `posts` WHERE slug = ?", [ slug ], function(err, rows) { if (err || rows.length == ) return next(); var post = rows[]; response.render("post", { post: post, formatDate: formatDate }); }); }) أعد تشغيل البرنامج وزر الصّفحة مجدًّدا: هذا أفضل! لكن ما هي الدّالة التّالية التي استدعاها Express ليعرف أنّ صفحة على هذا الرّابط غير موجودة؟ الإجابة هي أنّ Express يحوي بشكل افتراضي دوالّ داخليّة يستدعيها عندما لا نزوّده بالدّالة التّالية، لكنّنا نستطيع فعل ذلك بسهولة: app.get("/posts/:slug", function(request, response, next) { var slug = request.params.slug; connection.query("SELECT * from `posts` WHERE slug = ?", [ slug ], function(err, rows) { if (err || rows.length == ) return next(); var post = rows[]; response.render("post", { post: post, formatDate: formatDate }); return; }); }) app.get("/posts/:slug", function(request, response) { response.send("التدوينة غير موجودة"); }) سجّلنا أكثر من دالّة تتعامل مع الرّابط ذاته، سينفّذها Express جميعًا بالتّرتيب ذاته، يمكن لكلّ دالّة أن تستدعي الدّالة التّالية أو أن توقف سلسلة الاستدعاءات بإرسال الطّلب للمتصفّح وإيقاف التّنفيذ. (إرسال الطّلب لا يعني بالضّرورة أنّ الدّوال التّالية لن تنفّذ، بل يجب إيقاف التّنفيذ صراحةً إن لم نرغب بهذا السّلوك). أعد تشغيل البرنامج ثم زُر الصّفحة ذاتها: حدث ما نتوقّعه بالضّبط، على سبيل التّأكد من كوننا لم نعبث بالوظيفة الرئيسيّة، جرّب زيارة تدوينة موجودة مثل http://localhost:3000/posts/quotes-1. كاختبار لك، قم بتعديل صفحة "التّدوينة غير موجودة" مستخدمًا قالبًا خاصًّا ولتجعله جميلاً! تصرّف براحتك! سأقوم بإدخال تعديل بسيط على الدّالة الثّانية، لجعلها ترسل الرّمز 404 (غير موجود) للمتصفّح بدل القيمة الافتراضيّة (200): app.get("/posts/:slug", function(request, response) { response.status(404); response.send("التدوينة غير موجودة"); })لن يغيّر هذا شيئًا في الظّاهر، لكنّه العرف المتّفق عليه، يمكن لبعض المتصفّحات أن تتعامل مع خطأ كهذا بعرض صفحة نتائج البحث على Google مثلاً (مع أنّه لا يوجد متصفّح يفعل ذلك)، لكنّها طريقة HTTP في التّفاهم بين الخادوم والمتصفّح. عظيم! لدينا الآن صفحة رئيسيّة منسّقة وصفحات مفردة للتّدوينات، في الدّرس القادم سنقوم بإنشاء نظام للمستخدمين تمهيدًا لإتاحة التّعليقات وكتابة تدوينات جديدة.
-
بعد أن أنشأنا نظام المستخدمين والجلسات، أصبحنا جاهزين الآن لبناء نظام التّعليقات، ثم إتاحة إمكانية إنشاء تدوينة جديدة وتعديل التّدوينات السّابقة. إتاحة التّعليقلحفظ التّعليقات نحتاج أولًا إلى جدول جديد يحفظ نصّ التعليق وتاريخه وكاتبه والتّدوينة التي أُضيف إليها، افتح صدفة MySQL واتصّل بقاعدة البيانات ثم نفّذ هذا الاستعلام: CREATE TABLE `comments` (id INT PRIMARY KEY AUTO_INCREMENT, post_id INT NOT NULL, user_id INT NOT NULL, body VARCHAR(500) NOT NULL, created TIMESTAMP, FOREIGN KEY (post_id) REFERENCES `posts` (id), FOREIGN KEY (user_id) REFERENCES `users` (id), INDEX (post_id));سنُعدّل قالب صفحة التّدوينة post.jade ونضيف حقلاً يسمح للمستخدم المُسجّل دخوله بإضافة التّعليق، ويعرض للزوّار إمكانيّة تسجيل الدّخول: doctype html html(lang="ar", dir="rtl") head title مُدوّنتي! body style :css body { font-family: Arial, sans-serif; } h1 مُدوّنتي hr h2 #{ post.title } p #{ post.body } small كُتِبَت #{ formatDate(post.date) } #comments h3 التعليقات if post.comments for comment in post.comments p يقول b #{ comment.full_name }: br | #{ comment.body } small #{ formatDate(comment.created) } hr else p لا تعليقات إلى الآن if user form(action="/posts/" + post.id + "/comments", method="POST") textarea(name="comment", placeholder="أكتب تعليقك") input(type="submit", value="أرسل التّعليق") else span لإضافة تعليقاتك، a(href="/login") سجّل دخولك | أو a(href="/signup") أنشئ حسابًا جديدًا لقد استبقنا الأمور قليلاً، سيرسل النّموذج الذي يشمل التّعليق إلى الرّابط /posts/:post_id/comments، لنقم بتوجيه هذا الرّابط: app.post("/posts/:post_id/comments", parseBody, function(request, response) { var body = request.body.comment; var user_id = request.user.id; var post_id = request.params.post_id; var created = new Date(); connection.query("INSERT INTO `comments` (post_id, user_id, body, created) VALUES (?, ?, ?, ?)", [ post_id, user_id, body, created ], function(err) { if (err) { response.status(500); response.send("تعذّرت إضافة التّعليق، حاول مجدّدًا."); return; } response.status(201); response.send("أُضيف التعليق"); }) }) ملاحظة: اخترنا أن يُرسل الرّابط إلى النّمط /posts/:post_id/comments بدل /posts/:slug/comments على سبيل تبسيط الأمور، لا يُميّز Express بين :slug و:post_id فكلاهما بالنّسبه له متُغيّران لا يخضعان لأيّة شروط، يمكننا التأكد من كون :post_id رقمًا باستخدام الوظيفة param() على التّطبيق والّتي نشترط بها أن يطابق المُتغيّر post_id التعبير النظامي (regular expression) التّالي: app.param('post_id', /^[0-9]+$/); وعندها لن تتلقّى هذا الدّالة سوى الرّوابط التي تحمل رقمًا في موقع :post_id. هذه الشّيفرة تكفي لإضافة التّعليق، لكنّنا بحاجة إلى تعديل دالّة توجيه رابط التّدوينة لإضافة التّعليقات إلى الصّفحة: app.get("/posts/:slug", function(request, response, next) { var slug = request.params.slug; connection.query("SELECT * from `posts` WHERE slug = ?", [ slug ], function(err, rows) { if (err || rows.length == 0) return next(); var post = rows[0]; connection.query("SELECT * FROM `comments` JOIN `users` ON comments.user_id=users.id WHERE post_id=?", [ post.id ], function(err, comments) { if (err) return next(err); post.comments = comments; response.render("post", { post: post, formatDate: formatDate, user: request.user }); }) }); })لنُجرّب أولًا زيارة صفحة التّدوينة http://localhost:3000/posts/hello-world دون تسجيل الدّخول وقبل إضافة أيّ تعليق: لنقم الآن بتسجيل الدّخول باسم admin وكلمة مرور 123456، ولنعُد للصفحة ونُحدّثها: لنجرّب إضافة تعليق: لنعُد لصفحة التّدوينة ونُعد تحميلها: رائع جدًا! لقد أنشأنا نظام تعليق بسيطًا بخطوات بسيطة بعد أن أتممنا القسم الأكبر من العمل عندما أنشأنا الجلسات ونظام المستخدمين. إتاحة كتابة التّدوينات وتعديلها لمدير المُدوّنةلنبدأ أوّلاً بصفحة إنشاء تدوينة جديدة، ولنُنشئ رابطًا جديدًا /new لعرض القالب views/post-editor.jade: doctype html html(lang="ar", dir="rtl") head title تدوينة جديدة body style :css body { font-family: Arial, sans-serif; } h1 مُدوّنتي hr h2 تسجيل الدخول form(action="/posts", method="POST") label(for="title") عنوان التدوينة: input(type="text", name="title", required) br label(for="slug") الرابط الفرعي: input(type="text", name="slug" required) br label(for="body") نص التّدوينة: textarea(name="body") br input(type="submit", value="أرسل التّدوينة")app.get("/new", function(request, response, next) { if (request.user && request.user.is_author) { response.render("post-editor", { user: request.user }); } else { response.status(403); response.send("ليس لديك صلاحيات إضافة تدوينة."); } }) app.post("/posts", parseBody, function(request, response) { if (request.user && request.user.is_author) { var title = request.body.title, body = request.body.body, date = new Date(), author_id = request.user.id, slug = request.body.slug; connection.query("INSERT INTO `posts` (title, body, date, author_id, slug) VALUES (?, ?, ?, ?, ?)", [ title, body, date, author_id, slug ], function(err) { if (err) { response.status(500); response.send("تعّذرت إضافة التّدوينة"); return; } response.status(201); response.send("أضيفت التّدوينة."); }) } else { response.status(403); response.send("ليس لديك صلاحيات إضافة تدوينة."); } })الجزء الأكثر أهمّيّة في شفرتنا هو التّحقق من كون المستخدم يحمل صلاحيات الكتابة، فإن لم يكن كذلك، نُرسل الرّمز 403 Forbidden (محظور). بإمكاننا تجاوز التّحقق قبل عرض القالب ونترك عرض الرسالة المناسب للقالب ذاته، لكنّ المهمّ هو إجراء التّحقّق عند إدخال التّدوينة في فاعدة البيانات. سنتيح تعديل التّدوينة على رابط التّدوينة ذاته متبوعًا بـ/edit: app.get("/posts/:slug/edit", function(request, response, next) { var user_id = request.user.id; var slug = request.params.slug; connection.query("SELECT * FROM `posts` WHERE author_id=? AND slug=?", [ user_id, slug ], function(err, rows) { if (!err && rows[0]) { response.render("post-editor", { post: post }); } else { response.status("401"); response.send("إمّا أن التّدوينة غير موجودة، أو أنّك لا تملك الصلاحيات للوصول إليها"); } }) })الجزء المهمّ من شفرتنا هو اشتراط أن يكون مؤلف التّدوينة المطلوب تعديلها هو صاحب الجلسة ذاته وهو ما كتبناه في استعلام MySQL، وإلّا سيكون بإمكان أن شخص أن يضيف /edit إلى نهاية التّدوينات ويجري ما يشاء من التغييرات عليها. لنقم بتعديل القالب post-editor.jade لجعله يتعامل مع التّدوينات الموجودة مسبقًا بالإضافة إلى التّدوينات الجديدة: doctype html html(lang="ar", dir="rtl") head title تدوينة جديدة body style :css body { font-family: Arial, sans-serif; } h1 مُدوّنتي hr - var editMode = post && post.id h2 #{editMode ? "تعديل التّدوينة" : "تدوينة جديدة" } form(action= editMode ? ("/posts/" + post.slug + "?_method=PUT") : "/posts", method="POST") label(for="title") عنوان التدوينة: input(type="text", name="title", required, value= editMode ? post.title : "") br if !editMode label(for="slug") الرابط الفرعي: input(type="text", name="slug" required) br label(for="body") نص التّدوينة: textarea(name="body") #{ editMode ? post.body : "" } br input(type="submit", value="أرسل التّدوينة")سيُرسل طلب التّعديل باستخدام الفعل PUT الّذي يُستخدم للطلب من الخادوم "تحديث" محتوى موجود لديه، خلافًا لـPOST المستخدم لإضافة محتوى جديد. المشكلة أنّ المتصفّحات لا تدعم استخدام سوى فعلين ضمن نماذج HTML هما POST وGET، ولذلك سنضطر إلى إيجاد طريقة "ملتوية" لتجاوز هذه المشكلة. استخدمنا الفعل POST ذاته في حالة التّعديل مع إضافة حقل إلى رابط action في النّموذج، سيستخدم هذا الحقل من قبل وحدة method-override الّتي تقرأه وتغيّر من فعل الطّلب إلى PUT ليمرّ عبر دالّة التّوجيه الّتي كتبناها: var methodOverride = require('method-override'); app.use(methodOverride('_method')); /* ... */ app.put("/posts/:slug", parseBody, function(request, response) { if (!requestest.user) { response.status(403); response.send("يجب تسجيل الدخول لتعديل التدوينات."); return; } var slug = request.params.slug; var new_title = request.body.title; var new_body = request.body.body; var user_id = request.user.id; connection.query("UPDATE `posts` SET title=?, body=? WHERE slug=? AND author_id=?", [ new_title, new_body, slug, user_id ], function(err) { if (err) { console.log(err); response.status("500"); response.send("حدث خطأ أثناء تعديل التدوينة"); return; } response.send("حُدِّثت التّدوينة."); }) })تأكد من اشتراطك كون مؤلّف التّدوينة هو ذاته صاحب الجلسة مرّة ثانية قبل إدخال البيانات. قلنا أنّ أفعال HTTP تستخدم استخدامًا دلاليًّأ (semantic) ولا شيء يُجبرك على استخدام PUT، بل يمكنك استخدام POST للحصول على نفس النّتيجة، لكنّه العرف المتّفق عليه، والذي ستعتاد على اتّباعه عندما تتقدّم في مستويات أعلى كبناء واجهة برمجيّة للمدوّنة (RESTful API) الّتي يتوقّع الطّرف الذي يتعامل معها هذا الأسلوب الدّلاليّ. يحقّ لنا الاحتفال الآن، فقد أنشأنا مدوّنة حقيقيّة من الصّفر! لنقم الآن بتحسين مظهرها وتنظيف شيفرتنا! تنظيف الشّيفرةحسنًا، قد تبدو شيفرتنا طويلة في الملفّ index.js طويلة بعض الشيء وفيها الكثير من التّكرار، وحالما نُشاهد سطورًا مكرّرة في شيفرة برمجيّة، نعلم أنّ بإمكاننا كتابة شيفرة أفضل. لقد أهملنا ذلك قليلًا لنحصل على برنامج يعمل بأسرع وقت ممكن، لكن علينا الآن أن نعود لنلقي نظرة أكثر إمعانًا في برنامجنا. تكرّر في كثير من المواضع استخدامنا للدّالة parseBody لتفسير متن طلبات POST، وحدة body-parser هي واحدة من البرامج الوسيطة التي يمكن استعمالها على مستوى التّطبيق أيضًا، لنقم بحذف عبارة parseBody من كل طلبات POST ولننقل تفسير متن الطّلب إلى مستوى التّطبيق، ذكرنا أنّه بإمكاننا استخدام app.use() لذلك: /* ... */ var app = express(); var parseBody = bodyParser.urlencoded({ extended: true }); app.use(session({ secret: "my top secret", resave: true, saveUninitialized: true })); app.use(parseBody); app.use(cookieParser()); // ...سيكون من المفيد بعد إضافة تدوينة جديدة أو تعديلها أو تعليق جديد على تدوينة العودة مجدًّدا إلى هذه التّدوينة بدل عرض رسالة تفيد بنجاح العمليّة فقط، يوفّر Express الوظيفة redirect() على الكائن response التي تُخبر المتصفّح بالانتقال إلى صفحة أخرى كجواب على الطّلب الّذي أُرسل. سأدع لك تنفيذ هذه المهمّات: عند كتابة تدوينة جديدة، انتقل إلى صفحة هذه التّدوينة.عند إضافة تعليق جديد، عُد إلى صفحة التّدوينة المعنيّة.عند إنشاء مستخدم جديد، انتقل إلى صفحة تسجيل الدّخول.عند تسجيل الدّخول، انتقل إلى صفحة الملفّ الشّخصيّ.في معظم دوالّ التّوجيه التي كتبناها، قمنا بالتّحقّق من الخطأ وإرسال رسالة مناسبة مع رمز حالة مثل 404 و403... من الأفضل أن نُصمّم صفحة خطأ خاصّة تتلقّى الخطأ ورسالته وتعرضه للمستخدم بأسلوب موحّد، سنحذف كلّ عبارات response.send() الّتي ترسل رسالة خطأ ونبدلها بالتّوجيه إلى الدّالة التّالية next() الّتي ستعرض قالب صفحة الخطأ views/error.jade: doctype html html(lang="ar", dir="rtl") head title مُدوّنتي! - خطأ body style :css body { font-family: Arial, sans-serif; } h1 مُدوّنتي hr h2 خطأ #{ response.statusCode } p #{ error.message || error.toString() }هذا مثال عن تعديل دالّة التّوجيه للرّابط /new: app.get("/new", function(request, response, next) { if (request.user && request.user.is_author) { response.render("post-editor", { user: request.user }); } else { response.status(403); return next(new Error("ليس لديك صلاحيات إضافة تدوينة.")); } })تقبل الدّالة next معاملاً اختياريًّا يشير إلى وجود خطأ، وهي الطّريقة المناسبة لتمرير الخطأ عبر دوالّ التّوجيه، عدم تمرير الخطأ يعني أنّ التّوجيه يسير من دالّة إلى أخرى بشكل سليم، يستفيد Express من وجود هذا المعامل لعرض الخطأ في حال انتهت عمليّة التّوجيه دون توفير دالّة تتعامل معه. سيلجأ Express إلى الدّالة التّالية الّتي يجب أن نُضيفها إلى نهاية شيفرتنا: app.use(function(err, request, response, next) { response.render("error", { error: err, statusCode: response.statusCode }) }) لاحظ أنّه خلافًا لدوالّ التّوجيه السّابقة، فقد استخدمنا 4 معاملات، قد تتساءل كيف يمكن لدالّة واحدة أن تتلقّى عددًا مختلفًا من المعاملات وتتصرّف بطريقة مختلفة، أو كيف تعرف الدّالة أن العنصر الأوّل هو كائن الخطأ وليس كائن الطّلب، الجواب هو أنّ Express يُجري تحقّقًا من عدد المعاملات في دالّة التّوجيه ويغيّر تصرّفه، وهذا الأمر متاح لأن JavaScript توفّر الكائن arguments بشكل تلقائيّ لكلّ الدّوالّ، والذي يمكن التّحقّق من طوله (length) بجملة شرطيّة وتغيير سلوك الدّالة. الهدف النّهائي من هذا أن يكون Express سهل الاستعمال وبديهيًّا، وهذا الأسلوب ستجده كثيرًا في Node.js. من الضّروري استخدام 4 معاملات ليستطيع Express التفريق بين: err, request, response وrequest, response, next. لنجرّب الآن زيارة بعض الصّفحات الّتي نتوقّع حدوث خطأ عندها: هذا أفضل! توحيد صفحة الخطأ سيجعلنا نفكّر في تقديم حلول لهذا الخطأ بناء على رمز الحالة، مثلاً نستطيع تقديم مربّع بحث في حال كان الرّمز 404 (غير موجود)، أو نستطيع أن نطلب من المستخدم تسجيل الدّخول في حال كان 403... إلخ. شيفرة JavaScript نظيفة الآن، لنلقِ نظرةً على القوالب الّتي أنشأناها، يتكرّر في معظمها استخدام ترويسة للصّفحة مع استخدام تنسيق موحّد، قد يبدو تضمين CSS في الصّفحة مقبولًا الآن، لكنّنا سنحتاج إلى نقله إلى ملفّ منفصل عندما نتوسّع في إضافة الأنماط لكي لا نحتاج لتكرارها في كلّ القوالب. لنُنشئ ملفًا للأنماط style.css في مجلّد جديد ضمن المشروع نُسمّيه public، ولننقل إليه شيفرة CSS من أحد القوالب: body { font-family: Arial, sans-serif; }لنحذف الآن شيفرة CSS من القوالب ونكتب بدلاً منها رابطًا لملفّنا: head title إنشاء مستخدم جديد link(rel="stylesheet", href="/style.css") body h1 مُدوّنتي //- ... حسنًا، لن يعثر الخادوم على الملفّ style.css عندما يطلبه المتصفّح، لأنّنا نحتاج لتوفيره صراحةً. من الشّائع استضافة كلّ الملفّات الثّابتة (static) مثل ملفّات CSS وJavaScript للمتصفّح ضمن مجلّد public، ثمّ توفير هذا المجلّد بكامل محتوياته على الخادوم. تتوفّر آليّة مُدمجة في Express للقيام بذلك: app.use(express.static(__dirname + '/public'));تتوفّر أيضًا وحدات خارجيّة يمكنها القيام بالمهمّة ذاتها وبخيارات أكثر مثل تحديد لواحق الملفّات وأذوناتها... مُلاحظة: المُتغيّر __dirname توفّره Node.js وهو يشير إلى المجلّد الذي يحوي الملفّ الحاليّ (index.js). سنستفيد من هذا المجلّد في استضافة ملفّات favicon وJavaScript الّتي تعمل في المتصفّح عندما نطوّر مدوّنتنا لتستخدم AJAX. تحسين مظهر المدوّنةسنحتاج إلى إجراء تغييرات في القوالب كإضافة بعض المُعرّفات (IDs) والأصناف (classess) لنقوم بتنسيقها وفق القواعد الّتي نكتبها في ملفّ style.css الّذي أنشأناه للتّوّ. يوفّر Express صياغتين مختصرتين للتّعبير عن الأصناف والمُعرّفات لكونهما شديدتي الشّيوع، لإضافة صنفين ومُعرّف على عنصر div ما، يمكن كتابة: #comments.post-comments.cardوهي مكافئة لكتابة: div(class="post-comments card", id="comments")والتي ستُترجم إلى HTML الّتالي: <div class="post-comments card" id="comments"></div>لاحظ أنّك لست بحاجة لكتابة div، لأنّ Jade يفهم المغزى من هذا الأسلوب على أنّه كائن div تلقائيًّا، لأنّه الكائن الأكثر استخدامًا لإضافة الأصناف والمعرّفات بهدف تنسيق مكوّنات الصّفحة. قد تلاحظ أثناء العمل الحاجة لتكرار أجزاء معيّنة من الشّيفرة في كلّ القوالب مثل إظهار عنوان المدوّنة وروابطها على المواقع الاجتماعيّة ومربّع البحث ضمن ترويسة (header) في كلّ الصّفحات، مع تذييل (footer) يحوي بعض الرّوابط الإضافيّة في نهاية كلّ صفحة، يمكنك التّخلصّ من عناء التّكرار باستخدام الكلمة المفتاحية extends لبناء قالب على قالب آخر، فلنقم ببناء قالب يتضمّن الهيكل العامّ لكلّ الصّفحات، ولنسمّه _layout.jade (اجعل اسمّ هذه الملفّات يبدأ بالرّمز _ لتستطيع فيما بعد تمييزها بسرعة بين ملفّات القوالب): doctype html html(lang="ar", dir="rtl") head title مُدوّنتي! link(rel="stylesheet", href="/style.css") body header h1#blog-title مُدوّنتي ul#blog-nav li: a(href="/") الرئيسية li: a(href="/login") تسجيل الدخول li: a(href="/signup") إنشاء حساب block content footer hr p جميع الحقوق غير محفوظةالعبارة block متبوعةً باسم نختاره نحن كما نشاء، تسمح لنا ببناء قوالب تشترك جميعها في الهيكل العّام لهذا الملفّ وتختلف فقط في هذا الجزء، مثلاً يمكننا الآن إعادة كتابة الصّفحة الرئيسيّة (home.jade) لتصبح: extends _layout block content for post in posts .post h2.post-title #{ post.title } p.post-body #{ post.body } small.post-date كُتِبَت #{ formatDate(post.date) }سيبحث Jade عن القطعة المُسمّاة content ويضيفها في المكان المناسب للقالب. لا داع لإرفاق لاحقة الملفّ فهي معروفة بالنّسبة لـJade. ملاحظة: التّعبير li: a(href=... هو اختصار تتيحه Jade للاستغناء عن الحاجة لكتابة الوسمين على سطرين. كثيرًا ما تحتاج إلى إدخال أجزاء متكرّرة من HTML مع إجراء بعض التّعديلات عليها، وهذا ما يمكن تنفيذه من خلال الدّوالّ في Jade والّتي تُسمّى mixins، وهي تشبه كثيرًا الدّوال في أي لغة برمجة، لتوضيح المفهوم أكثر، لنفترض أنّنا نريد توحيد مظهر التّدوينات بين صفحة التّدوينة والصّفحة الرئيسيّة، مع فارق بسيط هو جعل نصّ التّدوينة في الصّفحة الرئيسية محدودًا بمئتي حرف مثلاً، يمكن فعل هذا بنقل شيفرة التّدوينة المفردة إلى دالّة في ملفّ منفصل نُسمّيه _mixins.jade: mixin post(post, full) h2.post-title: a(href="/posts/" + post.slug) #{ post.title } p.post-body #{ full ? post.body : (post.body.substr(0, 199) + "...") } small.post-date كُتِبَت #{ formatDate(post.date) }لاحظ كم تشبه هذه الصّياغة صياغة الدّوالّ في لغات البرمجة، حيث يمكن إمرار مُعاملات لها بين قوسين. يمكن استدعاءها في قوالبنا بالرّمز + بعد تضمين الملفّ _mixins.jade بالكلمة المفتاحية include الّتي تشبه استدعاء وحدة خارجية بـrequire في Node.js: extends _layout include _mixins block content for post in posts .post +post(post, false)سأقوم الآن بتنسيق المُدوّنة بأسلوبي الخاصّ، وسأترك المجال لك لتفعل الأمر ذاته! إذا أردت استلهام بعض الأفكار، أنصحك بالاطّلاع على مواقع مثل Codrops. في الدّرس القادم سنطّلع على بعض المواضيع الّتي يجب أخذها بالحُسبان قبل نشر المدوّنة على الويب.
-
استضافة المشروع على الويب تختلف في كثير من نواحي عن البيئة المحلّيّة أثناء التّطوير، علينا أخذ عدّة أمور في عين الاعتبار قبل أن يصبح مشروعنا جاهزًا للنّشر (deployment). الأمان والحمايةعندما طوّرنا المشروع، لم نلقِ بالًا لمواضيع الحماية والأمان (سوى تجزئة كلمة المرور)، لأنّ الطّلبات في بيئة التّطوير تصدر وتعود للجهاز نفسه دون خوف من مرورها على جهات أخرى، أمّا على الويب فإنّ الطّلبات ستمرّ عبر عشرات ومئات بل ربّما آلاف الأجهزة قبل أن تصل للطّرف الآخر. ولذا علينا تنفيذ ما يلي كحدّ أدنى لحماية خادومنا والمستخدمين في وقت واحد: استخدام HTTPS: عند إنشاء المستخدم لحساب جديد أو تسجيله دخوله، يجب أن تنتقل كلمة المرور مُعمَّاة (encrypted) من المتصفّح للخادوم لكي لا تستطيع أطراف أخرى، مثل مزوّد خدمة الإنترنت أو مُخترقي الشّبكة، الاطّلاع عليها أثناء انتقالها، وهو ما يمكن تحقيقه باستخدام بروتوكول HTTPS، كيفيّة اعتماد HTTPS على الخادوم أمر خارج عن نطاق هذه السّلسلة. صلاحية الجلسات: ذكرنا أنّه من المُفضّل فرض حدّ على صلاحيّة الجلسة يتمّ بعدها إعادة طلب تسجيل الدّخول من المستخدم. إذا استطاع مخترق ما الوصول إلى جهاز المستخدم أو سرقة الكعكات من متصفّحه، فسيصبح بإمكانه انتحال شخصيّة هذا المستخدم والتّعليق باسمه أو إضافة التّدوينات إن كان الكاتب يملك هذا الإذن. التحقق من البريد الإلكتروني: لم نطلب من المستخدم عنوان بريده الإلكتروني عند إنشاء نموذج التّسجيل في الموقع لغرض تبسيط الدرس، ولكن من الأفضل طلبه مع التّحقّق من صحته بإرسال رسالة تحوي رابط التّحقّق وذلك لحماية المدوّنة من الهجمات الآلية الّتي تهدف إلى خلق ضغط على الخادوم أو إغراقه بالتّعليقات غير المرغوبة. الأداءالتخزين المؤقت (caching): الحواسيب اليوم سريعة للغاية، لا سيما الخواديم، معالجات بقدرات هائلة وأحجام كبيرة من الذاكرة وسعة التّخزين، لكن المشكلة الوحيدة الّتي لا تزال تؤثّر على أدائها، على الرّغم من كلّ التّطوّرات في السّنوات الأخيرة، ما تزال أقراص التّخزين، نعم هناك أنواع جديدة من الأقراص (SSD) تقدّم سرعات أكبر، لكنّها ما تزال مُكلفة وغير شائعة على الخوادم، في كلّ استعلام لقاعدة البيانات سيذهب خادوم MySQL إلى القرص ليجلب النّتائج، وهذا يعني أنّ كلّ صفحة يزورها كلّ مستخدم ستتطلّب على الأقل مرورًا واحدًا على القرص، وإذا كان القرص ميكانيكيًّا، وهي الأقراص الأكثر شيوعًا في الحواسيب إلى اليوم، فإنّ هذا سيستغرق وقتًا ملحوظًا. من الأفضل تخزين النّتائج للاستعلامات الشّائعة والّتي لا تتغيّر كثيرًا (كنصّ التّدوينة، ومعلومات المستخدمين) في الذّاكرة (RAM) لأنّها كبيرة الحجم عادةً، وأسرع بكثير من الأقراص الصّلبة. يمكننا تخزين هذه النّتائج بأسلوب بدائي بشكل كائن JavaScript ضمن متغيّر يبقى ضمن الذّاكرة، لكنّ هذا الحلّ ليس عمليًّا، لذلك وُجدت العديد من الحلول الّتي تعتمد بنية خادم/عميل مثل Redis وMemcached، ببساطة يمكن تخزين نتائج الاستعلامات في قاعدة بيانات Memcached الّتي تعمل في الذّاكرة، والخادوم يتولّى إدارة مساحة الذّاكرة وتوزيعها على عدّة حواسيب إن تطلّب الأمر، كما في المواقع الضّخمة. العديد من المواقع الكبيرة تستخدم Memcached، منها Google وTwitter وWikipedia.من الوسائل الأخرى لتسريع عمل الموقع أن نطلّب من المتصفّح تخزين الملفّات الّتي لا تتغيّر كثيرًا لفترة أطول قبل أن يجلبها من جديد، يمكن القيام بهذا من خلال ترويسات في جواب HTTP تُدعى بترويسات إدارة الذّاكرة المؤقتة (Cache Control Headers)، يمكن أن نطلب من المتصفّح على سبيل المثال أن يُخزّن ملفّ style.css لمدّة 10 أيّام على جهاز المتصفّح بحيث لا يضطّر لجلبه مجدّدًا لكلّ صفحة يزورها المستخدم. في Express يمكن استخدام خيار maxAge للوظيفة static() لتعيين المُدّة القصوى لتخزين الملفّات الثّابتة مُقدّرة بالميلي ثانية: app.use(express.static(__dirname + '/public', { maxAge: 60 * 60 * 24 * 1000 }));وأمّا لطلباتنا الأخرى، فيكمننا تعيين قيم ترويسات الجواب بالطّريقة التّالية: response.set("Cache-Control", "private, max-age=3600"); أو: response.set({ "Cache-Control": "private, max-age=3600, must-revalidate", "Expires": "Tue, 13 Jan 2015 21:49:10 GMT" }); إضافة الفهارس لقاعدة البيانات: قاعدة البيانات لدينا ما تزال صغيرة ولا تحتوي الكثير، لكنّنها ستتضخّم مع زيادة التّعليقات والتّدوينات والمستخدمين بلا شكّ، وعندها سيصبح من الضّروريّ إضافة الفهارس على الأعمدة الأكثر استعلامًا لتسريع جلب النّتائج. يمكن تشبيه الفهارس في قواعد البيانات بالفهارس في الكتب؛ إذا أردت الوصول إلى قسم معيّن في كتاب ضخم، فإنّك ستفضّل الاطّلاع على الفهرس، لأنّ ذلك أسرع من المرور على كلّ أقسام الكتاب، على الرّغم من أنّ الفهرس قد يتطلّب بضع صفحات إضافية من الكتاب، الّتي يمكن تشبيهها بمساحة تخزين إضافيّة على القرص. تقليص الملفات: قد يصبح حجم وعدد ملفّات المشروع الّتي ستصل للمتصفح مثل ملفّات CSS وJavaScript كبيرًا مع تطوّر المشروع، على الرّغم أنّنا لم نستخدم أيّة مكتبات JavaScript ضخمة أو خطوط ويب كبيرة الحجم، إلّا أنّنا سنفعل ذلك في المشاريع الواقعيّة غالبًا، عندها ستصبح الحاجّة لتقليص حجم هذه الملفّات وجمعها أمرًا له ما يُبرّره، المتصفّحات لا تهتمّ إن كانت ملفّات CSS الّتي تصلها خمسة أو واحدًا، ما يهمّها هو المحافظة على ترتيب سطور الشّيفرة، كما أنّها لا تهتم بالفواصل والمسافات في ملفّات JavaScript، ولا حتّى بأسماء المتغيّرات طالما استخدمت الأسماء نفسها، وهذا هو مبدأ التّقليص (minification) والجمع (concatenation)، لنأخذ نصّ JavaScript كهذا: var myVeryLongVariableName = "something"; function doSomething() { return myVeryLongVariableName; }بعد تقليص هذا الملفّ: var a="something";function doSomething(){return a;}بالطّبع سيصبح فارق الحجم بين الملفّين كبيرًا مع تضخّم الشّيفرة وزيادة تعقيدها. كلا الشّيفرتين تؤدّيان المهمّة ذاتها على المتصفّح، ولكن الأخيرة ستقلل من حجم طلب HTTP وهو ما ينعكس على سرعة تحميل الموقع وبالتّالي على قبول المستخدم للموقع وتجربته. تتوفّر في Node.js الكثير مع الوحدات الّتي تنفّذ مهمّات الجمع والتّقليص لمختلف لغات الويب، ولعلّ من أشهرها UglifyJS لنصوص JavaScript، وclean-css لملفّات CSS. أمّا كيفيّة استخدامها وأتمتتها فهو موضوع خارج عن نطاق هذه السّلسة. استخدام وسائل ضغط مثل gzip: وهي آليّة لضغط الصّفحات لتقليص حجمها ثمّ تعيين ترويسة Encoding في جواب HTTP لتُشير للمتصفّح بأنّ المحتوى المنقول مضغوط، وسيقوم المتصفّح الّذي يدعم هذه الآليّة بفكّ ضغطه ثمّ التعامل معه كأيّ جواب آخر، يتعرّف الخادوم على المتصفّحات الّتي تدعم gzip عبر ترويسة Accept-Encoding، أمّا المتصفّحات الأخرى فتُرسل لها الإجابة دون ضغط. معظم المتصفّحات الحديثة تدعم gzip، وتتوفّر لـExpress الوحدة compression للقيام بالمهمّة تلقائيًّا. ملفات المشروعللحصول على نسخة من ملفّات المشروع لتجربتها محلّيًّا والتّطوير عليها، يمكن عمل فرع عن المشروع أو استنساخه من صفحته على GitHub.
-
في الجزء السّابق أنشأنا الصّفحة الرئيسيّة للمدوّنة وصفحات مفردة لكلّ تدوينة بعد تهيئة المشروع وإنشاء قواعد البيانات، الآن سنقوم بإنشاء نظام للمستخدمين لنسمح للقرّاء بالتّعليق. إنشاء صفحة "حساب جديد" و"تسجيل الدّخول"نعلم إذًا أنّنا بحاجة أولاً إلى آلية لإنشاء الحسابات على خادومنا، وأوّل ما نقوم به إنشاء صفحة على الرّابط /signup تحوي نموذجًا يعبّئه المستخدم: doctype html html(lang="ar", dir="rtl") head title إنشاء مستخدم جديد body style :css body { font-family: Arial, sans-serif; } h1 مُدوّنتي hr h2 إنشاء مستخدم جديد form(action="/accounts", method="POST") label(for="name") اسمك: input(type="text", name="name", placeholder="الاسم كاملًا", required) br label(for="name") كلمة المرور: input(type="password", name="password", placeholder="اختر كلمة مرور قويّة", required) br label(for="username") اسم المستخدم: input(type="text", name="username", placeholder="حروف لاتينية، 50 حرفًا على الأكثر", required) br input(type="submit", value="أنشئ الحساب") احفظ النصّ السّابق في ملف signup.jade في مجلّد views؛ ثمّ أضف النّص التّالي إلى index.js قبل آخر سطر: app.get("/signup", function(request, response) { response.render("signup"); })زر الصّفحة http://localhost:3000/signup بعد تشغيل البرنامج وحاول إنشاء مستخدم جديد، سيرسلك Express إلى صفحة تفيد بعد إمكانية تنفيذ الفعل POST على الرّابط /accounts، وهو الرابط الذي اخترناه لتلقّي نماذج إنشاء المستخدمين بالنّموذج الّذي أنشأناه (لاحظ الخاصّتين action وmethod للنّموذج ضمن قالب Jade)، كلّ ما علينا الآن هو تسجيل دالّة تتعامل مع هذا الرّابط: app.post("/accounts", function(request, response) { // أنشئ الحساب }) إن كنت تتساءل لم استخدمنا POST بدلاً من GET، فالإجابة هي أنّ POST يستخدم للطّلب من الخادوم "إنشاء" الأشياء الجديدة (بينما يطلب GET "الحصول" عليها)، هذا أوّلًا، ثانيًا فإنّ إرسال الطّلب باستخدام GET، وعلى الرّغم من أنّه ممكن، إلّا أنّه قد يكشف كلمة المرور الّتي اختارها المستخدم، لأنّ محتويات النّموذج (بما فيها كلمة المرور) ستُرمّز ضمن الرّابط (URL-encoded)، وكلّ المتصفّحات تحتفظ بنسخة من سجلّ تصفّح المستخدم، وهذا قد يجعلها عرضة لأن يراها الآخرون. هذا مثال عن كيفية ترميز النّماذج في طلبات GET: http://myblog.com/signup?username=fwz&password=123456&full_name=فواز كما ترى، ليس هذا أفضل ما يمكننا فعله لإخفاء كلمة المرور! يرسل المتصفّح محتويات النّموذج بالفعل POST كمتن الطّلب، وعندما يتلقّاه الخادوم فإنّنا بحاجة إلى تحويله من نصّ مجرّد إلى صيغة كائن JavaScript، لا يقدّم Express هذه الإمكانيّة وحده، ولكنّه يوفّر وحدة منفصلة تُدعى body-parser للقيام بهذه المهمّة، قد يبدو هذا غريبًا للقادمين من PHP، لكنّها الطّريقة الّتي تسير بها الأمور في Node.js، ولهذا فوائده إذ يمكنك استبدال وحدة بوحدة أخرى تؤدّي الوظيفة ذاتها لكن قد تكون أسرع أو تقدّم وظائف أكثر، وكذلك يسمح هذا النّهج بتطوير الوحدات الصّغيرة بشكل أسرع دون الانتظار إلى صدور نسخة جديدة من إطار العمل كاملاً. كاختبار لك، قم بتثبيت body-parser وحفظه في متطلّبات المشروع. هل تذكر عندما تحدّثنا عن البرامج الوسيطة (middleware)؟ حسنًا، وحدة body-parser ليست سوى واحدة من هذه البرامج، ويأتي الاسم من كونها تتوسّط وظيفة Express لتوسّع خياراته بشكل منسجم مع سير توجيه الرّوابط. لإخبار Express برغبتنا باستخدام body-parser، علينا استيرادها ثمّ إدخالها كوسيط لعمليّة توجيه الرّابط /accounts: var express = require("express"); var bodyParser = require("body-parser"); /* ... */ var parseBody = bodyParser.urlencoded({ extended: true }); app.post("/accounts", parseBody, function(request, response) { console.log(request.body); }) app.listen(3000);هذه إحدى الطّرق لاستخدام البرامج الوسيطة على أحد الرّوابط، يمكن إدخال أي عدد من البرامج الوسيطة وسينفّذها Express واحدًا تلو الآخر حتّى يصل أخيرًا إلى دالّتنا الّتي تتعامل مع الرّابط. يمكننا أيضًا استخدام body-parser وأي برنامج وسيط آخر ليتدخّل في سير التّطبيق كاملاً (ليس على رابط واحد فقط)، وسنرى كيفيّة ذلك في وقتٍ لاحق. إن كانت صياغة السّطر var parseBody... غامضة فراجع صفحة توثيق وحدة body-parser، الأمر يتعلّق بأسلوب المطوّر الّذي أنشأ الوحدة، قد يكون أسلوب الوحدات الأخرى مختلفًا لكنّ ما يهمّك هو أن تتعلّم كيفيّة استخدام البرامج الوسيطة. في دالّة التّوجيه الأخيرة، سنقوم مبدئيًا بتسجيل محتويات النّموذج إلى الطّرفيّة الّتي تشغّل برنامجنا، يتوفّر الكائن request.body فقط لأنّنا استخدمنا body-parser قبل دالّتنا، والذي أتاح محتويات النّموذج في عنصر الطّلب. أعد تشغيل البرنامج وزُر الصّفحة ثم املأ الحقول واضغط "أنشئ الحساب"، عُد للطّرفيّة لتشاهد محتويات النّموذج وقد وصلت للخادوم: حسنًا لقد وصلَنا النّموذج وهو جاهز لإدخاله في قاعدة البيانات، لكن ليس قبل التّحقّق من محتوياته. القاعدة الرئيسيّة في حماية قواعد البيانات: لا تثق بما يُدخله المستخدم! تحقّق من سلامة كلّ حقل في النّموذج قبل إدخاله، ماذا لو أرسل المستخدم حقلاً إضافيًا is_author وجعل قيمته true، سيكون بإمكانه حينئذٍ كتابة التّدوينات دون أن نسمح له بذلك! app.post("/accounts", parseBody, function(request, response) { var username = request.body.username; var password = request.body.password; var full_name = request.body.name; if (!username || !password || username.length > 50) { response.status(400); response.send("تعذّر إنشاء الحساب، تحقّق من سلامة المُدخلات وأعد المحاولة"); return; } connection.query("INSERT INTO `users` (username, password, full_name) VALUES (?, ?, ?)", [username, password, full_name], function(err) { if (err) { response.status(500); response.send("وقع خطأ أثناء إنشاء الحساب، أعد المحاولة"); return; } response.status(201); response.send("أُنشئ الحساب، يمكنك الآن إنشاء المستخدم"); }); }) من المّهمّ ألا تأخذ الكائن response.body كاملاً وتلقيه مباشرة في قاعدة بياناتك، فقد يحتوي على حقول إضافية مثل is_author. قمنا بإجراء تحقّق بسيط من طول اسم المستخدم. تتوفّر في Node.js وحدات تُعطينا إمكانيّات أوسع للتحقّق من المدخلات بحسب أنواعها (نصّيّة، أرقام، عناوين البريد... إلخ)، سنستعرض إحداها لاحقًا. استخدمنا الرّمز 400 في حال الخطأ ومعناها Bad Request، تستخدم الأرقام ضمن 400-499 للإشارة إلى خطأ من جهة مُرسل الطّلب (خلافًا للفئة 5xx الّتي تعني أنّ الخطأ من جهة الخادوم). أمّا الرّمز 201 فيعني Created (أُنشئ). حماية كلمة المرور بدوال التّجزئة (Password hashing functions)التجزئة (hashing) موضوع معقّد للغاية، ويحتاج شرح مفاهيمه إلى سلسلة أطول من هذه! لكنّنا سنحاول توضيحه باختصار شديد للمبتدئين. بحسب ويكيبيديا، فإنّ دالّة التّجزئة التّشفيريّة: بالطّبع هذا التّعريف غامض جدًّا، والسّبب يعود إلى حدّ ما إلى غياب مصطلح عربيّ معتمد للكلمة hash، لعلّ الصّورة المرفقة مع التّعريف أعلاه تساعدنا في فهم المقصود: التّجزئة إذًا هي تحويل النّصوص المقروءة (كلمات المرور مثلاً) إلى تلك المجموعة من الحروف والأرقام الغامضة لنا، والغاية من ذلك الحصول على قيمة مميّزة للنصّ المُدخل دون الحاجة لمعرفة النّصّ ذاته، وبحيث يكون من المستحيل الحصول على نصّين مختلفين لهما قيمة مُجزّأة واحدة. إذا تمكّن شخصٌ ما من الحصول على القيم المجزّئة (يمين الصّورة) فلن يستطيع معرفة النّصّ الأصليّ (يسار الصّورة)، والطّريقة الوحيدة الّتي يمكن الاستفادة منها من القيمة المُجزّئة، هي إمكانية الإجابة على هذا السّؤال: هل النّصّ x يطابق تمامًا النّصّ y؟ يمكن الإجابة بنعم بالتّأكيد إذا كانت القيمة المُجزّئة لـx تطابق القيمة المُجزئّة لـy. نحفظ كلمة المرور مُجزّئة في قاعدة البيانات لأنّنا لا نهتمّ (ولا نرغب) بمعرفة كلمة المرور الّتي اختارها المستخدم. ما يهمّنا فقط هو أن نتحقّق من كون القيمة المُجزّئة المخزّنة في قاعدة البيانات تطابق ما يدخله المستخدم عند تسجيل دخوله بعد تجزئته بنفس الخوارزميّة، من المهمّ كذلك ألّا تتطابق القيمة المجزّئة لكلمتي مرور مختلفتين وإلّا سيتمكّن شخص محظوظ ما (أو ذكيّ) من تسجيل الدّخول باسم مستخدم آخر بكلمة مرور مختلفة! علينا أنّ نفرّق التّجزئة عن التعمية (encryption) والّتي هي تحويل نصّ مجرّد (plaintext) إلى نصّ مُشفّر (ciphertext) وفق عمليّة رياضيّة قابلة للعكس، بينما تهدف التّجزئة إلى تحويل البيانات المختلفة الحجم إلى قيمة ثابتة الطّول باتّجاه واحد فقط (one-way). في المثال السّابق أدخلنا كلمة المرور في قاعدة البيانات دون تجزئة، وهذا خطأ فادح لأنّه يسمح لمن يستطيع الوصول إلى جدول المستخدمين بالاطّلاع على كلمات مرورهم جميعًا، لعلّك تستخدم md5 أو sha1 في PHP لتجزئة كلمة المرور بشكل تقليدي، تتوفّر وحدات Node.js تسمح بتجزئة النّصوص بهذه الخوارزميات، لكنّنا سنستخدم خوارزميّة bcrypt الّتي تُعد أكثر أمانًا بمراحل من الخوارزميّتين سابقتي الذّكر: npm install bcrypt --save ملاحظة: تحتاج الوحدة bcrypt إلى إصدار متوافق من Python مثبّتًا على جهازك، راجع صفحة الوحدة على GitHub لمزيد من التّفاصيل. var bcrypt = require("bcrypt"); /* ... */ app.post("/accounts", parseBody, function(request, response) { var username = request.body.username; var password = request.body.password; var full_name = request.body.name; if (!username || !password || username.length > 50) { response.status(400); response.send("تعذّر إنشاء الحساب، تحقّق من سلامة المُدخلات وأعد المحاولة"); return; } bcrypt.hash(password, 8, function(err, hash) { if (err) { response.status(500); response.send("تعذّر إنشاء الحساب، تحقّق من سلامة المُدخلات وأعد المحاولة"); return; } connection.query("INSERT INTO `users` (username, password, full_name) VALUES (?, ?, ?)", [username, hash, full_name], function(err) { if (err) { response.status(500); response.send("وقع خطأ أثناء إنشاء الحساب، أعد المحاولة"); return; } response.send(201); response.send("أُنشئ الحساب، يمكنك الآن تسجيل الدخول"); }); }); }) // ... لنجرّب إنشاء مستخدم جديد الآن، شغّل البرنامج ثم انتقل إلى http://localhost:3000/signup، املأ الحقول بمُدخلات سليمة ثمّ اضغط "أنشئ الحساب": لنتأكد من وجود الحساب في قاعدة البيانات، افتح صدفة MySQL ونفّذ الاستعلام التّالي بعد الاتّصال بقاعدة البيانات: SELECT FROM `users` WHERE username="muhammad";بدّل اسم المستخدم بالاسم الذي ملأته في حقل "اسم المستخدم" عند إنشاء الحساب، ستحصل على نتيجة مشابهة لهذه: +----+----------+--------------------------------------------------------------+-----------+-----------+ | id | username | password | full_name | is_author | +----+----------+--------------------------------------------------------------+-----------+-----------+ | 2 | muhammad | $2a$08$6GFnpkKY6VQuB6/y4NCrg.AK9jI25XyfS6APz4rP8w1bpICKNR79G | محمد | 0 | +----+----------+--------------------------------------------------------------+-----------+-----------+ 1 row in set (0.00 sec)لاحظ كون كلمة المرور مُجزّئة ممّا يجعل معرفتها مستحيلة لمن يصل لجدول المستخدمين. تسجيل الدّخوللُننشئ صفحة تسجيل الدّخول على الرابط /login مع القالب views/login.jade: doctype html html(lang="ar", dir="rtl") head title تسجيل الدخول body style :css body { font-family: Arial, sans-serif; } h1 مُدوّنتي hr h2 تسجيل الدخول form(action="/sessions", method="POST") label(for="username") اسم المستخدم: input(type="text", name="username", required) br label(for="name") كلمة المرور: input(type="password", name="password" required) br input(type="submit", value="سجّل الدخول") سنضيف هذه الشيفرة للتّعامل مع تسجيل الدّخول: app.get("/login", function(request, response) { response.render("login"); }) app.post("/sessions", parseBody, function(request, response) { // ابحث عن المستخدم وتأكد من صحة كلمة المرور }) // ...الخطوة الأولى في تسجيل الدّخول تتضمّن التّحقّق من وجود اسم المستخدم ومقارنة كلمة المرور بعد تجزئتها (hashing) للكلمة المُجزئة في قاعدة البيانات. app.post("/sessions", parseBody, function(request, response) { var username = request.body.username; var password = request.body.password; if (!username || !password) { response.status(400); response.send("يجب توفير اسم المستخدم وكلمة المرور"); return; } connection.query("SELECT username, password FROM `users` WHERE username=?", [ username ], function(err, rows) { var user = rows[0]; if (!user) { response.status(400); response.send("لا يوجد مستخدم يطابق اسمه اسم المستخدم المطلوب"); return; } bcrypt.compare(password, user.password, function(err, result) { if (err) { response.status(500); response.send("وقع خطأ من جهة الخادم، حاول تسجيل الدخول لاحقًا"); return; } if (result == true) { // كلمتا المرور متطابقتان response.status(200); // احفظ الجلسة على المتصفّح } else { response.status(401); response.send("كلمة المرور التي أرسلتها خاطئة"); } }) }); })في البداية نبحث في قاعدة البيانات عن سطر يوافق حقل username فيه القيمة username الّتي أرسلها المتصفّح، إن وُجد هذا المستخدم فإنّنا نستخدم الوظيفة compare() الّتي توفّرها bcrypt لمقارنة كلمة المرور المُجزّئة مع كلمة المرور الّتي أرسلها المستخدم، إن كانت نتيجة المعامل result مساوية لـtrue، فهذا يعني أنّ كلمة المرور صحيحة. وإلّا فإنّنا نُرسل الرّمز 401 ويعني Unauthorized (غير مُصرّح له) مع رسالة مناسبة للدّلالة على فشل تسجيل الدّخول. لم ننتهِ بعد من تسجيل الدّخول، لكنّنا سنؤجّل الخطوة الثانية قليلاً، لأنّها تعتمد على فهمنا للجلسات (sessions)، الّتي ستكون موضوع الدّرس القادم.
-
لا قيمة لمدوّنتنا إن لم يكن باستطاعتنا كتابة التّدوينات الجديدة ونشرها، لذا علينا إنشاء صفحة تُتيح لنا (لنا فقط) كتابة تدوينة جديدة وحفظها في قاعدة البيانات. لكن أوّل ما يتبادر إلى الذّهن تساؤل عن الكيفيّة الّتي نستطيع أن نمنع فيها الزّائر من إضافة التّدوينات، إذ كيف يستطيع المتصفّح والخادوم التّفريق بين صاحب المدوّنة وزائرها؟ ملايين المواقع على الويب تقدّم محتوىً مخصّصًا لكلّ مستخدم، إذا عدنا لمثال فيس بوك وطرحنا السؤال ذاته: كيف يعرض فيس بوك نشرة أخبار خاصّة بكلّ مستخدم؟ بالطّبع لكلّ مستخدم حساب في الموقع محميّ بكلمة مرور، لكن ما الذي يحدث بين المتصفّح والخادوم ويجعل الخادوم يُرسل الصّفحة الخاصّة بالمستخدم إليه دون غيره؟ إن كنت سمعت من قبل بالكعكات (cookies) ولم تعرف ما علاقتها بالويب، فقد حان الوقت لنعرف ما هي وكيف تستخدم. الكعكات (Cookies)الكعكات هي أجزاء صغيرة من البيانات تخزّن في المتصفّح وتنتقل بينه وبين الخادوم مع كلّ طلب إلى هذا الخادوم (كترويسة Heading في طلب HTTP)، يمكن للخادوم أن يأمر المتصفّح بحفظ بيانات جديدة ضمن الكعكات، ويمكن للمتصفّح منع تخزين هذه البيانات بأمر المستخدم أو حذفها متى شاء. تُستخدم الكعكات بشكل شائع لتخزين "الجلسة" (session)، وهي الطّريقة الّتي يتذكّر بها الخادوم هذا المتصفّح بين الطّلب والآخر بحيث يستطيع تمييزه من بين الطّلبات الّتي تصله من حواسيب مختلفة حول العالم. من المهمّ أن نعرف أنّ طلبات HTTP هي طلبات مستقلّة بذاتها وعديمة الحالة (stateless)، بمعنى أن كلّ طلب يصدر من المتصفّح للخادوم نفسه لا يعرف أيّ شيء عن الطّلبات السّابقة أو اللاحقة، وكذلك الخادوم؛ إلا إذا تمّ إرفاق معرّف مُميّز (session ID) يتّفق عليه الطّرفان وينتقل مع كلّ طلب بين الجهتين. بدون الجلسات كنت ستحتاج إلى إدخال اسم مستخدمك وكلمة المرور في كلّ مرة تنتقل فيها إلى صفحة جديدة على فيس بوك! من المهمّ إذًا أن يكون معرّف الجلسة (session ID) مُميّزًا للمتصفّح ولا يتطابق مع معرّف جلسة أي متصفّح آخر، وهذا يتمّ بتوليد معرّف الجلسة بشكل عشوائي على الخادوم أولًا ثمّ إرساله للمتصفّح لحفظه ضمن الكعكات، وسيقوم المتصفّح بنقل الكعكات مع كلّ طلب، ممّا يسمح للخادوم بمعرفة تتابع الطّلبات من نفس المتصفّح. من الاستخدامات الأخرى للكعكات تتبّع المستخدمين بين المواقع عن طريق استضافة محتوى من طرف ثالث ضمن صفحة الموقع (third-party cookies) وهي حيلة تستخدم لمعرفة ذوق المستخدم وتوجّهه من خلال أنواع المواقع الّتي يزورها وبالتّالي استهدافه بالإعلانات أو مراقبة نشاطه. لا غرابة أن تعطينا المتصفّحات وسيلة لمنع كعكات الطّرف الثّالث، أو لمنع الكعكات بالكامل! لنلخّص الأمر: تسمح الجلسات بربط طلبات HTTP المتتالية بحيث يتعرّف الخادوم على كونها صادرة من جهة واحدة، مما يسمح له بتخصيص الإجابة على هذه السلسلة من الطّلبات دون غيرها، سنستفيد من هذا في حفظ تسجيل الدّخول المستخدم بحيث لا نطلب منه كلمة المرور عندما ينتقل من صفحة لأخرى. لنعد الآن إلى شيفرة تسجيل الدّخول ولنفكّر، ما الذي نحتاجه لحفظ الجلسة؟ عندما يُسجّل المستخدم دخوله للمرّة الأولى، نحتاج إلى حفظ مُعرّف الجلسة على الخادوم ليكون بإمكاننا مقارنته مع الطّلبات التّالية، وهذا يعني أنّنا بحاجة لوسيلة لحفظ معرّف الجلسة لكل مستخدم. في Express يتوفّر البرنامج الوسيط express-session الذي يتولّى هذه المهمّة كاملةً. قم بتثبيت هذه الوحدة، ثم لنقم باستيرادها: var express = require('express'); // ... var session = require('express-session');نريد استخدام express-session على مستوى التّطبيق بالكامل، ما يعني أنّنا نريد لها أن تتعقّب كلّ الطّلبات على جميع الرّوابط المسجّلة مما يسمح بمتابعة الجلسة، تسمّى هذه الوحدات البرامج الوسيطة على مستوى التّطبيق (Application-level middleware) بعكس الأسلوب الّذي استخدمناه في body-parser لتفسير متن الطّلب في نماذج إنشاء المستخدم وتسجيل الدّخول (برامج وسيطة على مستوى المُوجّه Router-level middleware). يمكن للوحدة أن تُستعمل بالأسلوبين. تستخدم الوحدات على مستوى التّطبيق باستدعاء الوظيفة use() لتطبيقنا: app.use(session({ secret: "my top secret", resave: false, saveUninitialized: true })); تستقبل وحدة session كائن الإعدادات الذي يتضمّن: secret: كلمة سرّيّة تسمح بتجزئة معرّف الجلسة لحمايته، ضع ما تشاء هنا.resave: هل يجب كتابة الجلسة مع كلّ طلب حتّى وإن لم تتغيّر؟saveUninitialized: هل يجب حفظ الجلسات الجديدة تلقائيًّا إلى الخادوم؟لا تقلق إن كانت هذه الإعدادات غامضة، ستتعرّف على فائدتها مع مرور الوقت. تذكّر أنّ ترتيب استدعاءات البرامج الوسيطة مهمّ، يجب أن نضيف الشّيفرة السّابقة قبل تسجيل الروابط لنتمكّن من متابعة الجلسة عبر كلّ الرّوابط المسجّلة. حسنًا من المفترض الآن أن يقوم المتصفّح بحفظ معرّف الجلسة ثم نقله مع كلّ طلب، لنتحقّق من هذا؛ شغّل البرنامج ثم زر الصفحة الرئيسية للمدوّنة، افتح أدوات المطوّرين (Ctrl + Shift + K في فيرفكس)، ثم انتقل إلى تبويب الشّبكة واضغط زر إعادة تحميل الصّفحة، سيبدأ المتصفّح بمراقبة الطّلبات، سيظهر طلب للصفحة الرئيسيّة مع بداية تحميلها، انقر عليه وابحث عن ترويسة Cookie في الجانب، لاحظ أنّها تحوي قيمة connect.sid، وهذا هو المعرّف المميّز الذي سينتقل بين الطّلبات، للتأكد من ذلك انتقل إلى صفحة أخرى مثل /posts/hello-world وكرّر العمليّة، ستجد أن معرّف الجلسة ثابت لا يتغيّر. عظيم! أصبح بإمكاننا الآن تمييز الطّلبات ومتابعتها، لكن ما الفائدة التي جنيناها إلى الآن! في الحقيقة لا شيء، نحتاج إلى الاستفادة من كون معرّف الجلسة مميّزًا بحيث نعلم أن المستخدم الذي تحمل طلباته هذا المعرّف قد سجّل دخوله فلا نطلب منه كلمة المرور في كلّ طلب، وسنستفيد من ذلك أيضًا في تخصيص محتوى الصّفحة وإتاحة وصول الكُتَّاب إلى صفحة إنشاء التّدوينات فيما بعد. نحتاج إلى حفظ معرّف الجلسة في جدول بقاعدة البيانات لنتمكّن من طلبه لاحقًا ومقارنته مع الطّلبات القادمة، لنُنشئ جدولاً يحفظ معرّفات الجلسات لكلّ مستخدم: CREATE TABLE `sessions` (session_id VARCHAR(100) NOT NULL PRIMARY KEY, username VARCHAR(50) NOT NULL, FOREIGN KEY (username) REFERENCES `users` (username), INDEX (username));من المهمّ أن نفهم أنّ معرّف الجلسة يحلّ محلّ كلمة المرور واسم المستخدم معًا، لهذا من الضّروري أن يكون مميّزًا (PRIMARY KEY) بحيث لا يتطابق معرّفان لمستخدمين مختلفين، من المهمّ، للسبب ذاته، حماية الجلسة وتوليدها بطريقة عشوائية، وهذا هو سبب استخدامنا للكلمة السّريّة secret في إعدادات express-session، من إجراءات الأمان الشّائعة إضافة مهلة تنتهي بعدها صلاحيّة الجلسة، وهذا هو السّبب الذي تطالبك لأجله بعض المواقع بإعادة تسجيل دخولك بعد فترة من الزّمن؛ لكنّنا لن نُشغل بالنا بهذه التّفاصيل الآن. حسنًا لدينا الآن معرّف الجلسة وجدول لحفظه، كل ما نحتاجه عند تسجيل الدّخول بشكل صحيح حفظ معرّف الجلسة إلى الجدول، لنعد إذًا إلى شفرة تسجيل الدّخول الّتي تركناها في الفقرة السّابقة: /* ... */ var cookieParser = require("cookie-parser"); app.use(cookieParser()); /* ... */ app.post("/sessions", parseBody, function(request, response, next) { var username = request.body.username; var password = request.body.password; if (!username || !password) { response.status(400); response.send("يجب توفير اسم المستخدم وكلمة المرور"); return; } connection.query("SELECT username, password FROM `users` WHERE username=?", [ username ], function(err, rows) { var user = rows[0]; if (!user) { response.status(400); response.send("لا يوجد مستخدم يطابق اسمه اسم المستخدم المطلوب"); return; } bcrypt.compare(password, user.password, function(err, result) { if (err) { response.status(500); response.send("وقع خطأ من جهة الخادم، حاول تسجيل الدخول لاحقًا"); return; } if (result == true) { connection.query("INSERT INTO `sessions` (session_id, username) VALUES (?, ?)", [ request.cookies["connect.sid"], username ], function(err) { if (err) return next(err); // تعامل مع الخطأ response.status(200); response.send("تم تسجيل الدّخول"); }) } else { response.status(401); response.send("كلمة المرور التي أرسلتها خاطئة"); } }) }); }) توفّر لنا express-session معرّف الجلسة ضمن الكعكة تحت الاسم connect.sid والذي يمكن تغييره بضبط القيمة name في إعدادات الوحدة. استخدمنا الوحدة cookie-parser التي تقوم بما يوحي به اسمها وتوفّر لنا الكعكات ضمن كائن الطّلب للحصول على معرّف الجلسة. نكاد ننتهي من إنشاء نظام المستخدمين، بقي علينا فقط إرفاق معلومات المستخدم مع كلّ دالّة توجيه لنتمكّن من عرض اسم المستخدم في الصّفحة وإتاحة تسجيل الخروج، بل يمكننا أيضًا توجيهه إلى صفحات خاصّة به أو منعه من الوصول إلى صفحات أخرى، سنقوم بإضافة دالّة توجيه تسبق جميع روابطنا وتقوم بإرفاق بيانات المستخدم (بعد جلبها من قاعدة البيانات) وإضافتها إلى كائن الطّلب: app.use(function(request, response, next) { var session_id = request.cookies["connect.sid"]; if (session_id) { connection.query("SELECT users.id, users.username, full_name, is_author FROM `users` JOIN `sessions` ON users.username=sessions.username WHERE session_id=?", [ session_id ], function(err, rows) { if (!err && rows[0]) { request.user = rows[0]; } next(); }) } else { next(); } }) في الحقيقة، ما كتبناه للتوّ ليس سوى برنامج وسيط، لا يختلف في شيء عن البرامج الوسيطة التي استعملناها مثل express-session عدا أنّ الأخيرة يوفّرها مطوّرون آخرون كحزمة على npm يمكن استيرادها. في دوال التّوجيه التّالية، سيتوفّر لدينا كائن request.user يتضمّن معلومات المستخدم الحالي، لنجرّب ذلك بإنشاء صفحة الملفّ الشّخصي للمستخدم (views/profile.jade): doctype html html(lang="ar", dir="rtl") head title الملف الشخصي body style :css body { font-family: Arial, sans-serif; } if (user) h1 #{ user.full_name } (#{ user.username }) hr else p لم تقم بتسجيل دخولك app.get("/profile", function(request, response) { response.render("profile", { user: request.user }) }) سنقوم بتوفير الكائن request.user للقالب، والذي سيكون غير معرّف إن لم يُوجد في قاعدة البيانات أو إن لم يسجل المستخدم دخوله، سيتولى القالب هذه الحالة ويعرض الرسالة المناسبة. لاحظ دعم Jade للجمل الشّرطيّة. حسنًا، لنجرّب الآن ما كتبناه، شغّل البرنامج ثم زر الصفحة http://localhost:3000/login، سجّل الدّخول باسم المستخدم admin وكلمة المرور 123456، من المفترض أن تنتقل إلى صفحة تخبرك بنجاح العملية، الآن انتقل إلى http://localhost:3000/profile لتشاهد الملف الشّخصيّ (حسنًا لا يبدو عظيمًا جدًّا، لكنّنا سنحسّنه لاحقًا): تهانينا! لقد أنشأنا نظامًا للمستخدمين وأصبح بإمكاننا عرض محتوى مخصّص لكلّ مستخدم! في الجزء القادم سنتيح لأنفسنا كتابة التّدوينات، وللمستخدمين إضافة التعليقات، وستكون أعظم مدوّنة في التّاريخ!