-
المساهمات
71 -
تاريخ الانضمام
-
تاريخ آخر زيارة
-
عدد الأيام التي تصدر بها
14
نوع المحتوى
ريادة الأعمال
البرمجة
التصميم
DevOps
التسويق والمبيعات
العمل الحر
البرامج والتطبيقات
آخر التحديثات
قصص نجاح
أسئلة وأجوبة
كتب
دورات
كل منشورات العضو محمد فواز عرابي
-
في سلسلة من عدة أجزاء سنناقش موضوعًا نظريًّا يُعتبر من أساسيّات هندسة البرامج، وهو أنماط التصميم (Design Patterns)، وسنعتمد لغة JavaScript في نقاشنا لتصاعد شعبيّتها ومرونتها التي تسمح لنا ببناء مشاريعنا وفق أنماط متنوّعة مما سيُسهّل علينا شرح موضوع السّلسلة نمط المُشيِّد (Constructor)يشيع استخدام المُشيّدات في اللغات الكائنيّة التّوجّه، حيث تُستخدم لإنشاء نُسخ (instances) من الأصناف (classes)، ومع أنّ JavaScript ليست لغةً كائنيّة التّوجّه بالمعنى التّقليديّ، إلّا أنّها تسمح بإنشاء نُسخ عن كائنات باستخدام بالمُشيّدات، ويمكن لأيّ دالّة أن تُستخدم كمُشيّد، وذلك بأن نُسبقها بالكلمة new، ولتوضيح هذا النّمط سنقوم بإنشاء مُنبّه (كالّذي تضبطه للاستيقاظ في هاتفك) يمكن ضبطه إلى تاريخ ووقت معيّنين ثمّ تفعيله أو تعطيله حسب الرّغبة: function Alarm(when) { this.setAt = when; this.enable = function() { var startAfter = new Date(this.setAt) - new Date; console.log("Alarm will wake you up after " + startAfter/1000 + " seconds"); this.timeout = setTimeout(function() { console.log("Wake up!"); }, startAfter); } this.disable = function() { if (this.timeout) { clearTimeout(this.timeout); delete this.timeout; console.log("Alarm diabled"); } } } var a = new Alarm("2015-03-19 5:58 PM"); a.enable() // Alarm will wake you up after 8.982 seconds // After a few seconds: // Wake up!في المثال السّابق نُسمّي الدّالة Alarm() مُشيّدًا (constructor)، والكائن a نُسخة (instance). لاحظ أنّ Alarm في المثال السّابق ليست سوى دالّة (function)، فهي ليست صنفًا كما في لغات أخرى مثل Java وC++، إذ تُعتبر الدّوال في JavaScript مكوّنًا من الدّرجة الأولى وتُعامل كما يُعامل أيّ كائن، وهكذا يمكن استخدامها كمشيّد لكائن آخر ممّا يسمح بمحاكاة مفهوم الأصناف الّذي لم يُضَف إلّا مؤخّرًا في JavaScript. من عيوب المِثال السّابق إسناد الدّوال الّتي ستعمل عمل الوظائف (methods) إلى النُسخة ذاتها عند إنشائها، وهذا يعني تكرار محتوى الدّوال في الذّاكرة مع كلّ نسخة جديدة من الكائن Alarm، بينما يمكننا توفير هذا الاستهلاك غير المُبرّر للذّاكرة بإسناد الدّوال إلى النّموذج البدئيّ للكائن (أي إلى Alarm.prototype) مما يسمح بمشاركتها بين كل نسخ الكائن، لتقوم الآلة الافتراضيّة بتنفيذ النّصّ البرمجيّ للدّالّة ذاتها بسياق النّسخة (instance context) الّتي استدعت الدّالة، أي إنّ this تُشير ضمن الدّالة عند تنفيذها إلى النُسخةَ المنشأة وليس الصّنف؛ بالطّبع ليس من المرغوب تطبيق الفكرة ذاتها على المُتغيّرات الأخرى مثل setAt، لأنّه من البديهيّ أن تختلف قيمتها بين نسخة وأخرى. لنُعد كتابة المثال السّابق بصورة أفضل: function Alarm(when) { this.setAt = when; } Alarm.prototype.enable = function() { var startAfter = new Date(this.setAt) - new Date; console.log("Alarm will wake you up after " + startAfter/1000 + " seconds"); this.timeout = setTimeout(function() { console.log("Wake up!"); }, startAfter); } Alarm.prototype.disable = function() { if (this.timeout) { clearTimeout(this.timeout); delete this.timeout; console.log("Alarm diabled"); } } var a = new Alarm("2015-03-19 6:21 PM"); a.enable(); // Alarm will wake you up after 30.243 seconds // After 30 seconds... // Wake up!هذا الأسلوب في إنشاء الأصناف شائع جدًّا، وهو يتطلّب فهمًا دقيقًا لآليّة الوراثة في JavaScript؛ إذ يُبنى كلّ كائنٍ فيها على كائن آخر يُسمّى النّموذج البدئيّ (prototype)، ويقوم هذا الكائن الأخير على كائن ثالث أعلى منه في السّلسلة هو نموذجه البدئيّ، وهكذا حتّى نصل إلى null الّذي ليس له نموذج بدئيّ بحسب تعريف اللّغة. في مثالنا السّابق الكائن Alarm.prototype هو النّموذج البدئيّ للكائن a، وهذا يعني أنّ كل الخواصّ المُسندة إلى Alarm.prototype وما فوقه ستكون مُتاحة للكائن a، ولو كتابنا برنامجًا مُشابهًا بـJava لقُلنا إنّ Alarm صنفٌ وإنّ a نُسخة عن هذا الصّنف (instance). عندما نحاول الوصول إلى الخاصّة a.setAt، فإنّ مُفسِّر JavaScript يبدأ بالبحث عن هذه الخاصّة من أدنى سلسلة الوراثة، أي من الكائن a ذاته، فإنّ وجدها قرأها وأعاد قيمتها، وإلّا تابع البحث صعودًا إلى النّموذج البدئيّ وهكذا... وطبيعة JavaScript هذه هي ما سمح لنا بإسناد الوظيفتين enable وdisable إلى Alarm.prototype مطمئنّين إلى أنّها ستكون مُتاحة عند قراءة a.enable() وa.disable(). يمكن التأكّد من النّموذج البدئيّ للكائن a كما يلي: Object.getPrototypeOf(a) == Alarm.prototype; // trueالأصناف في ECMAScript 6يُقدّم الإصدار الأحدث من JavaScript مفهوم الأصناف (classes) بصورته التّقليديّة المعروفة في اللّغات الأخرى، إلّا أنّه ليس سوى أسلوب آخر لصياغة النّماذج البدئيّة (أو ما يُسمّى syntactic sugar)، وهذا يعني أنّه نموذج الوراثة في JavaScript لم يتغيّر. يمكننا إعادة كتابة المثال السّابق بصياغة الأصناف في ES6 كما يلي: class Alarm { constructor(when) { this.startAt = when; } enable() { var startAfter = new Date(this.setAt) - new Date; console.log("Alarm will wake you up after " + startAfter/1000 + " seconds"); this.timeout = setTimeout(function() { console.log("Wake up!"); }, startAfter); } disable() { if (this.timeout) { clearTimeout(this.timeout); delete this.timeout; console.log("Alarm diabled"); } } }وستُسند الدّوال enable() وdisable() إلى Alarm.prototype تمامًا كما في المثال الذي سبقه. يُذكر أنّ استخدام new ليست الطّريقة الوحيدة لتشييد الكائنات، إذ يمكن استخدام الوظيفة Object.create() لتُعطي النّتيجة ذاتها: var a = Object.create(Alarm.prototype); Object.getPrototypeOf(a) == Alarm.prototype; // trueإسناد الخواصّ إلى الكائناتJavaScript لغة ديناميكية، وهذا يعني أنّه يمكن إضافة وحذف الخواصّ من الكائنات وتعديل نماذجها البدئيّة أثناء التّنفيذ، وهذا ما يمنحها القسم الأكبر من مرونتها ويجعلها مناسبة للاستخدام في بيئة مُعقّدة مثل بيئة الويب، وليس من الغرابة أن توفّر اللّغة وسائل متعدّدة لإسناد الخصائص إلى الكائنات لتلبية الحاجات المتنوّعة لتطبيقات الويب. ماذا لو أردنا تغيير قيمة المنبّه في مثالنا السّابق بعد تفعيله؟ لربّما ترادونا للوهلة الأولى إمكانيّة تغيير قيمة الخاصّة setAt بالطّريقة التّقليدية: a.setAt = "2016-03-03 03:03 PM"; // or a["setAt"] = "2016-03-03 03:03 PM";لكنّ نتيجة هذا الفعل لن تكون كما يُتوقّع، فلو عدنا للمثال السابق وتمعّنا في خواصّه، للاحظنا عيبًا في كيفيّة عمل المُنبّه، إذ إنّ الخاصّة setAt مكشوفة ويمكن تغيير قيمتها في أيّ وقت، حتى بعد تفعيل المُنبّه، إلّا أنّ تغييرها بعدئذٍ لن يغيّر اللّحظة الحقيقيّة الّتي سينطلق فيها المنبّه كما يتضّح لنا عند قراءة النّصّ البرمجيّ، ولذا فنحن هنا أمام حلّين: إمّا منع تغيير قيمة الخاصّة setAt وجعلها للقراءة فقط بعد إنشاء المُنبّه، أو إيقاف المنبّه وإعادة ضبطه في كلّ مرّة تُغيّر فيها قيمة الخاصّة setAt، وكلا الحلّين متاحان إذا كنّا على علم بأساليب إسناد الخصائص في JavaScript. توفّر اللّغة الوظيفة Object.defineProperty() الّتي تسمح بتعريف خواصّ لكائن ما مع إمكانيّة التّحكم بتفاصيل هذه الخاصّة، ومن هذه التّفاصيل: هل الخاصّة قابلة للكتابة؟ (writable)هل يجب المرور على هذه الخاصّة عند سرد خواصّ الكائن؟ (enumerable)ما الذي يحدث عند إسناد قيمة للخاصّة؟ (set)ما الذي يحدث عند قراءة قيمة الخاصّة؟ (get)وبهذا يمكننا بسهولة منع تغيير قيمة الخاصّة setAt بعد إسنادها: function Alarm(when) { Object.defineProperty(this, "setAt", { value: when, writable: false }) } Alarm.prototype.enable = function() { var startAfter = new Date(this.setAt) - new Date; console.log("Alarm will wake you up after " + startAfter/1000 + " seconds"); this.timeout = setTimeout(function() { console.log("Wake up!"); }, startAfter); } Alarm.prototype.disable = function() { if (this.timeout) { clearTimeout(this.timeout); delete this.timeout; console.log("Alarm diabled"); } } var a = new Alarm("2015-03-19 7:51 PM"); a.setAt = new Date("2016-03-19"); console.log(a.setAt); // "2015-03-19 7:51 PM"لاحظ أنّ قيمة setAt لم تتغيّر. هذا حلّ جيّد، لكن سيكون من الأفضل السّماح للمُستخدم بتعديل قيمة المنبّه، وعندها سنلجأ لإيقاف المنّبه وإعادة ضبطه كما يلي: function Alarm(when) { var _hidden_value = new Date(when); Object.defineProperty(this, "setAt", { set: function(newValue) { _hidden_value = new Date(newValue); if (this.timeout) { this.disable(); console.log("Alarm changed to " + newValue); console.log("You need to re-enable the alarm for changes to take effect"); } }, get: function() { return _hidden_value; } }) } Alarm.prototype.enable = function() { var startAfter = new Date(this.setAt) - new Date; console.log("Alarm will wake you up after " + startAfter/1000 + " seconds"); this.timeout = setTimeout(function() { console.log("Wake up!"); }, startAfter); } Alarm.prototype.disable = function() { if (this.timeout) { clearTimeout(this.timeout); delete this.timeout; console.log("Alarm diabled"); } } var a = new Alarm("2016-03-03 03:03 PM") a.enable(); // Alarm will wake you up after 30221933.66 seconds a.setAt = "2015-03-19 8:05 PM"; // Alarm changed to 2015-03-19 8:05 PM // You need to re-enable the alarm for changes to take effect a.enable() // Alarm will wake you up after 20.225 seconds // After 20 seconds... // Wake up!لاحظ أنّنا سنحتاج إلى مُتغيّر سرِّيِّ (_hidden_value) نُخزّن فيه القيمة الفعليّة لوقت التّنبيه. متى أستخدم هذا النّمط؟نمط المُشيّد لا يحتكر بنية مشروعك عند استخدامه؛ معنى هذا أنّه لا شيء يمنعك من استخدام نمط المُشيّد مع أي نمط آخر عند الحاجة لذلك، فيمكن (بل يشيع كثيرًا) استخدام المُشيّدات ضمن الوحدات (modules) ثمّ تصديرها لاستخدامها من موضع آخر في المشروع، ومثال ذلك أشياء مثل EventEmitter وStreams في Node.js. كما يمكن بناء أنماط أخرى سنتعرّف عليها لاحقًا على أساس المُشيِّدات مثل نمط الكائن المُتفرّد (Singleton) ونمط المُراقِب (Observer pattern). المصادر: شبكة مُطوّري موزيلّا: Inheritance and the prototype chainكتاب JavaScript Design Patterns لمؤلّفه Addy Osmani
- 1 تعليق
-
- 5
-
- constructor
- design patterns
-
(و 1 أكثر)
موسوم في:
-
في سلسلة من عدة أجزاء سنناقش موضوعًا نظريًّا يُعتبر من أساسيّات هندسة البرامج، وهو أنماط التصميم (Design Patterns)، وسنعتمد لغة JavaScript في نقاشنا لتصاعد شعبيّتها ومرونتها التي تسمح لنا ببناء مشاريعنا وفق أنماط متنوّعة مما سيُسهّل علينا شرح موضوع السّلسلة ما هي أنماط التصميم؟عندما تبدأ بتعلّم البرمجة، فغالبًا ما يكون اهتمامك مُنصبًّا على أن يكون البرنامج قادرًا على إنجاز المهمّة الّتي تريدها قبل كل شيء، أمّا بعد أن تتطوّر مهاراتك، فسينتقل اهتمامك إلى مواضيع أكثر عمقًا، وستبدأ بطرح بعض الأسئلة على نفسك، حتّى قبل أن تبدأ بكتابة البرنامج، من هذه الأسئلة: كيف أبني برنامجي بحيث يسهُل تحسينه فيما بعد؟كيف أتأكّد أن برنامجي سيبقى يؤدّي ما يُتوقّع منه حتّى وإن قمت بتعديل أجزاء منه بعد زيادة تعقيده؟كيف أبني برنامجي بحيث أستطيع إعادة استخدام أجزاء منه في برامج أخرى في المستقبل؟كيف أجعل برنامجي يستخدم أجزاء من مشاريع أخرى كتبها مطوّرون آخرون؟الإجابة على هذه الأسئلة هي واحدة دومًا: اختر نمط التصميم المناسب لمشروعك. لم نُعرِّف بعدُ مفهوم نمط التّصميم، لكنّنا بدأنا نُكوّن فكرة عنه. نمط التّصميم هو وصف لطريقة مُعيّنة في حلّ مشكلة برمجيّة ما، فالعديد من المُشكلات البرمجيّة يمكن حلّها بأكثر من طريقة، ولكلّ طريقة مساوئ ومحاسن، وبعضها قد يكون أكثر مُناسبةً للمشروع الحاليّ، واختيار نمط التصميم المُناسب سيضمن استمرار تطوّر المشروع بسهولة وربّما يُفيدنا في عزل أجزاء منه لإعادة استخدامها في مشاريع أخرى بحيث لا نُضطَّر لكتابتها مرارًا. أغلب الظنّ أنّك تستخدم واحدًا أو أكثر من أنماط التّصميم وإن لم تعرف ما هي أنماط التّصميم بمعناها النّظريّ، فإنشاء أصناف (classes) لمفاهيم مُجرّدة في اللّغات الكائنيّة التّوجّه (object-oriented) وإنشاء نُسخ عنها (instances) وتوزيع هذه الأصناف في ملفّات مستقلّة، هو في الواقع نمط من أنماط التّصميم. يُرجى الانتباه إلى أن التّطبيق العمليّ لأنماط التّصميم يفرض على المطوّر دمج أكثر من نمط معًا وشرحنا لأحدها لا يمكن أن يخلو من استخدام لأنماط أخرى كما سيتبيّن لك بعد انتهاء السّلسلة، إذ يمكن مثلًا إنشاء وحدة (module) تُصدِّر صنفًا (class) وهذا يعني أنّنا استخدمنا نمطين اثنين (نمط الوحدات، ونمط مُشيّد الكائنات constructor) في وقت واحد. نمط الوحدات (Module Pattern)بغرض تبسيط الأمور، سنبدأ بتوضيح أحد أنماط التّصميم الشّائعة في JavaScript، وهو ما يُعرف بنمط الوحدات (module pattern)، والتي ازدادت شعبيّة بعد ظهور Node.js وما أحدثته من تأثير انتقل حتّى إلى أساليب بناء وتصميم المكتبات البرمجيّة الّتي تستهدف المُتصفّحات. الوحدات هي أجزاء مُستقلّة ومعزولة من النّص البرمجيّ للمشروع توفّر مهمّة مُعيّنة، الأمر الّذي يجعل الهدف من كل وحدة واضحًا ومحدّدًا ويُجنّب المشروع الفوضى التي تنتج عن كتابة كامل النّصّ البرمجيّ في كتلة واحدة متداخلة يصعب معها تنقيحه وصيانته. فصل الأجزاء هذا ليس الفائدة الوحيدة الّتي يُقدّمها نمط الوحدات، إذ من خلاله يمكن مُحاكاة مفهوم المكوّنات السّرّيّة (private) والعلنيّة (public) وحماية بعض محتويات الوحدة من الوصول إليها من خارجها في JavaScript، وذلك بإخفائها ضمن الوحدة والامتناع عن تصديرها الأمر الذي يجعل الوصول إليها من خارج الوحدة مستحيلًا كما سنوضّح بعد قليل. تتوفّر في عالم JavaScript أشكال مختلفة لتصميم الوحدات، منها: الكائنات الحرفيّة (object literals)الدّوالّ المغلقة المجهولة (anonymous closures)وحدات CommonJSوحدات AMDوحدات ECMAScript 6الشّكل الأول ربّما هو أبسط الأشكال وأكثرها بدائيّة، وهو يعني ببساطة إنشاء كائن باستخدام صياغة القوسين المعكوفين {} يضمّ خصائص ووظائف متعلّقة بمهمّة واحدة لعزلها وتسهيل استخدامها: var userSettings = { preferences: { privacy: "strict", language: "ar", showEmail: false, available: true, }, updatePreferences: function(newPrefs) { this.preferences = newPrefs; } }بعض خبراء JavaScript لا يعتبرون هذا النّمط وحدةً حقيقيّة لبساطته الشّديدة، فمن الواضح أنّ هذا النّمط أبسط من حاجات التّطبيقات المعقّدة، فغالبًا ما يكون توزيع الوحدات على ملفّات منفصلة أمرًا مرغوبًا أثناء تطوير التّطبيقات ولهذا نحتاج إلى وسيلة لاستيراد هذه الملفّات وتصديرها بما يسمح باستخدام وحدة واقعة في ملفّ من ملفّ آخر، ولهذا الغرض طوّر مجتمع JavaScript خلال الأعوام الماضية أساليب قياسيّة اتّفق على استخدامها على الرّغم من أن اللّغة ذاتها لم تقدّم مفهوم الوحدات إلّا في الإصدار الأخير (ES6)، والذي استلهم من الأساليب السّابقة أصلًا؛ كما أنّنا قد نرغب بحماية كائن مثل preferences في مثالنا السّابق من تعديله بصورة مباشرة. الشّكل الثّاني هو أسلوب أكثر تطوّرًا لإنشاء الوحدات، ويحتاج فهمه إلى فهم معنى الدّوال المُغلقة (closures)، فإذا كانت لدينا دالّة تُعيد عند استدعائها دالّة أخرى، فإنّنا ندعو الدّالة الأخيرة دالّة مُغلقة، ويتاح لهذه الدّالة الوصول إلى المتّغيّرات الّتي كانت مفروضة في الدّالة الأولى حتّى عندما تُستدعى من خارجها: function addNumberToN(n) { return function(number) { return n + number; } } var addTo2 = addNumberToN(2); var five = addTo2(3); // 5هذه الخاصيّة في JavaScript تسمح لنا بعزل المتّغيّرات (encapsulation) ضمن الدّالة الخارجيّة مع الاحتفاظ بإمكانيّة الوصول إليها من الدّوال والكائنات الفرعيّة، الأمر الذي يحاكي مفهوم خصوصيّة المتغيّرات في لغات البرمجة الأخرى (access modifiers). لنفترض مثلًا أنّنا نريد أن نقوم بإنشاء عدّاد لعدد النّقرات على زرّ معيّن في تطبيقنا، ولا نريد الاحتفاظ بهذه القيمة في النّطاق العامّ لأنّ هذا قد يعرّضها للتّعارض من أسماء مُتغيّرات أخرى أو يجعلها قابلة للتّعديل من إضافات خارجيّة في المتصفّح، لهذا نقوم بإنشاء دالّة مُغلقة تُحيط بهذه القيمة: function() { var counter = 0; return { increaseCounter: function() { counter++; } } }وبهذا نكون قد قيّدنا إمكانيّة تعديل قيمة المتغيّر بزيادته فقط، وعبر الدّالة increaseCounter() فقط. لا يمكن استخدام الدّالّة increaseCounter() إلا بعد استدعاء الدّالة المجهولة (anonymous) الّتي تُحيط بها، ويتمّ هذا كما يلي: (function() { var counter = 0; return { increaseCounter: function() { counter++; } } })()لتُصبح الدّالة increaseCounter() مُتاحة في النّطاق العامّ، وبهذا نكون حصلنا على شكل بدائي لفكرة "تصدير الوحدات" (module exports). لنستعرض الآن الأساليب الأخرى لإنشاء الوحدات، ولعلّ أكثر هذه الأساليب شيوعًا أسلوب CommonJS المُعتمد في Node.js، والذي يُتيح استيراد الوحدات باستخدام الدّالّة require(): var UrlMaker = require("./url-maker"); var url_maker = new UrlMaker("Hello World!"); var url = url_maker.make(); console.log(url); // hello-world;وأمّا تصدير الوحدات لإتاحة استخدامها، فيتم بإسناد الخصائص إلى الكائن module.exports، كما في الملفّ url-maker.js الموجود في مسار العمل الحالي: module.exports = function UrlMaker(string) { return { make: function() { return string.toLowerCase().replace(/\s+/gi, "-").replace(/[!?*%$#@`]/gi, "") } } }لا يقتصر استخدام أسلوب CommonJS على بيئة Node.js، بل يمكن نقله إلى المتصفّحات باستخدام برامج مثل Browserify. من الأساليب الشائعة لإنشاء الوحدات كذلك نمط وحدات AMD (اختصارًا لـAsynchronous Module Definition) ولعلّه يُستخدم بكثرة مع مكتبة require.js في المتصفّحات والّتي تسمح بتحميل الوحدات (الموزّعة كلّ منها على ملفّ منفصل) عند الحاجة إليها، إذ يتمّ التّصريح عن كلّ وحدة وما تعتمد عليه من وحدات أخرى وتقوم require.js بتلبية هذه المتطلبات بتحميل ملفّات الوحدات المنفصلة. في المثال التّالي، نُصرّح عن حاجة موقعنا لمكتبتي jQuery وUnderscore ووحدة أخرى قمنا بإنشائها بأنفسنا: require(["jquery", "underscore", "user_profile"], function($, _, UserProfile) { var user = new UserProfile(); user.firstName = $("#form input[name='first']").text(); user.lastName = $("#form input[name='last']").text(); user.username = $("#form input[name='username']").text(); // ... })ويتمّ التّصريح عن الوحدة user_profile في ملفّ منفصل باسم موافق: define(function() { function Profile() { /* ... */ } return Profile; })يوفّر الإصدار الجديد من JavaScript (ES6) دعمًا أساسيًّا لتعريف الوحدات واستيرادها وتصديرها، وسنتطرّق له بالتّفصيل في الجزء القادم من سلسلة التّعريف بميزات ES6 الجديدة، لكنّ لا بأس من أن نتعرّف عليه سريعًا: import { jQuery as $ } from "/jquery.js"; import { Profile as UserProfile } from "/user.js"; var user = new UserProfile(); user.firstName = $("#form input[name='first']").text(); // ...وتُنشئ الوحدة وتُصدَّر في الملفّ user.js: class Profile { constructor() { // ... } validate() { // ... } } export { Profile };فوائد استخدام الوحداتلنُلخِّص إذن فوائد الوحدات: تنظيم النّصّ البرمجيّ للمشاريع الضّخمة بحيث يسهل فهم بنية البرنامج وتنقيحه ومتابعة صيانته في المستقبل.إدارة المُتطلّبات (dependencies): توضيح العلاقة بين مكوّنات المشروع، بحيث نستطيع إدارة ما تتطلّبه كلّ وحدة وتحميل هذه المتطلّبات آليًّا بالتّرتيب الصّحيح بدل الحاجة إلى التّصريح عن روابط المكتبات الخارجيّة في الصّفحة الرئيسيّة للموقع وإعادة ترتيبها كلّ ما تطلّب الأمر إضافة مكتبة جديدة تعتمد على أخرى.العزل (encapsulation): حماية أجزاء من المشروع من العبث بها سهوًا أو من نصوص برمجيّة خارجيّة، وذلك بعزلها ضمن نطاق فرعيّ خلافًا لتركها في النّطاق العامّ. ففي الحالة الطّبيعيّة تكون متغّيرات JavaScript المفروضة في النّطاق العامّ مُتاحة لأي دالّة، وأما عند فرض هذه المُتغيّرات ضمن الوحدات، فإنّ الوصول إليها يُصبح محدودًا بما هو داخل الوحدة ذاتها، ويتمّ ذلك باستغلال مفهوم النّطاقات (scopes) في JavaScript وإحاطة هذه المتغيّرات بدالّة تُغلّفها بنطاق فرعيّ ثمّ إعادة كائن يحوي فقط الخصائص الّتي نريد إتاحتها للعموم.متى أستخدم هذا النّمط؟يُنصح باستعمال هذا النّمط في المشاريع الضّخمة كتطبيقات الويب المُعقّدة الّتي تعمل في المتصفّح، إذ تكون الحاجة مُلحّة لتجزئة المشروع وتطوير كلّ جزء بصورة مستقلّة ثم ربط هذه الأجزاء مع بعضها إمّا بهدف تسهيل تطوير المشروع أو إدارة المتطلّبات بحيث تُجلَب عند الحاجة إليها نظرًا لكبر حجمها أو تأثيرها على أداء التّطبيق، كما يُنصح باستخدامه عند الحاجة لعزل تفاصيل الوحدة عن النّطاق العامّ. المصادر: كتاب JavaScript Design Patterns لمؤلّفه Addy OsmaniJavaScript Module Pattern: In-Depthوثائق require.jsوثائق Node.js
- 2 تعليقات
-
- 3
-
- modules
- design patterns
-
(و 1 أكثر)
موسوم في:
-
التفكيك Destructuringذكرنا في الجزء السابق أن اهتمامًا كبيرًا أُوليَ لتسهيل كتابة الشفرة وقراءتها في ECMAScript 6، والإسناد بالتفكيك (Destructuring assignment) لا يخرج عن هذا السياق، وهو ليس بالمفهوم الجديد في عالم البرمجة، فهو معروف في Python وفي Ruby. بعيدًا عن تعقيدات المصطلحات، إليك هذا المثال: var [a, b, c] = [1, 2, 3]; a == 1 // true b == 2 // true c == 3 // trueما الذي يحدث هنا؟ بكل بساطة تسمح ECMAScript 6 بصياغة جديدة للتعريف عن المتغيرات أو إسناد قيم جديدة إليها جُملةً واحدة من خلال جمعها ضمن قوسي مصفوفة (Array) وسيقوم مُفسّر اللغة بإسناد قيمة مقابلة لكل متغيّر من المصفوفة الواقعة على يمين مُعامل الإسناد (=). الأمر لا يقتصر على إسناد المصفوفات، بل يمكن أيضًا إسناد خصائص العناصر: let person = { firstName: "John", lastName: "Smith", Age: 42, Country: "UK" }; let { firstName, lastName } = person; console.log(`Hello ${ firstName } ${ lastName }!`); // Hello John Smith!في هذا المثال لدينا متغيّرات تتبع للنطاق العامّ firstName وlastName، وقد أسندنا لها قيمًا من خصائص الكائن person، حيث يبحث مفسّر اللّغة عن خصائص في الكائن person يماثل اسمها اسم المتغيّر المفروض ويُسندها إلى المُتغيّرات. يمكن توضيح المقصود بصورة أفضل إذا أعدنا كتابة الشفرة لتتوافق مع الإصدار الحالي من JavaScript: var person = { firstName: "John", lastName: "Smith", Age: 42, Country: "UK" }; var firstName = person.firstName; var lastName = person.lastName; console.log("Hello " + firstName + " " + lastName + "!"); // Hello John Smith! يشيع استخدام التفكيك في CoffeeScript (وهي لغة أقر Brendan Eich مُخترع JavaScript بأنّ الإصدار الأخير من JavaScript استوحى الكثير منها)، وخصوصًا عندما تُنظّم البرامج في وحدات كما في Node.js ويكون اهتمامًا مُقتصرًا على استيراد جزء مُحدّد من الوحدة المعنيّة: { EventEmitter } = require 'events' { EditorView } = require 'atom' { compile } = require 'coffee-script' compile('# coffeescript code here');عند تحويل هذا النص إلى JavaScript الحالية، سنحصل على: var EventEmitter = require('events').EventEmitter; var EditorView = require('atom').EditorView; var compile = require('events').compile; compile('# coffeescript code here');من الاستخدامات المفيدة للإسناد بالتفكيك التبديل بين قيمتي متغيّرين بصورة سهلة، سنقتبس المثال من توثيق CoffeeScript ونُعيد كتابته بـJavaScript: var theBait = 1000; var theSwitch = 0; [theBait, theSwitch] = [theSwitch, theBait];قبل ES6 كنا لنحتاج لكتابة مُتغيّر مؤقّت نخزن فيه قيمة إحدى المتغيّرين للاحتفاظ بها قبل التبديل بين القيمتين، وهو ما يفعله محوّل CoffeeScript بالفعل ليعطينا شفرة JavaScript متوافقة مع الإصدار الحالي (مع أنه يقوم بتخزين كلا القيمتين في مصفوفة، إلا أنّ الفكرة تبقى ذاتها): var theBait, theSwitch, _ref; theBait = 1000; theSwitch = 0; _ref = [theSwitch, theBait], theBait = _ref[0], theSwitch = _ref[1];المُكرِّرات (Iterators) وحلقة for... ofما من لغة برمجة تخلو من وسيلة للمرور على عدد من القيم وتكرار تنفيذ عمليّة معيّنة على هذه القيم، من أبسط هذه الوسائل حلقة for التقليديّة الشّهيرة، وفي JavaScript يشيع استخدام حلقة for... in إلى جانبها للمرور على أسماء خصائص العناصر، إذ يمكننا معرفة كل خصائص العنصر document بسطرين فقط: for (var propertyName in document) { console.log(propertyName); } // "body" // "addEventListener" // "getElementById" // ...لاحظ أن حلقة for... in تُعيد أسماء خصائص العنصر (كسلسة نصيّة String)، والأمر لا يستثني المصفوفات، فهي ليست سوى كائنات بأسماء خصائص توافق رقم الفهرس (Index): for (var i in [1, 2, 3]) { console.log(i); } // "0" // "1" // "2" من عيوب حلقة for... in أن لا شيء في تعريف اللغة يُجبر مُفسّر اللّغة على إخراج العناصر بترتيب ثابت بالضّرورة، وهذا يعني أنها تصبح مباشرة غير صالحة للمرور على المصفوفات - التي تستخدم لحفظ عناصر مُرتّبة - بطريقة بديهيّة، ويحلّ محلّها حلقة for التقليديّة عندئذٍ، وأمّا عند استخدامها للمرور على الكائنات، فإنّها لا تُعيد إلّا الخصائص الّتي تُعرّف على أنها قابلة للتعداد (enumerable)، وهو شيء يُحدّد عند تعريف الخاصّة، كما أنّها تُعيد الخصائص القابلة للتعداد التي ورثها الكائن عن "آباءه" ضمن سلسلة الوراثة، وهو تصرّف قد لا يكون مرغوبًا دومًا، وغالبًا سترى المطوّرين يُجرون فحصًا للخاصّة قبل متابعة تنفيذ الشفرة لمعرفة ما إذا كانت تخصّ العنصر ذاته أمّ أنّه ورثها: var obj = { a: 1, b: 2, c: 3 }; // كائن جديد لا يرث سوى النموذج Object for (var prop in obj) { console.log("o." + prop + " = " + obj[prop]); } // "o.a = 1" // "o.b = 2" // "o.c = 3"في هذا المثال (المنقول عن شبكة مطوّري موزيلا) فرضنا كائنًا جديدًا بثلاث خصائص، وعند المرور عليه بحلقة for... in فإنّنا حصلنا على النتيجة المتوقّعة، ولم نحصل على خصائص إضافيّة لأنّ الكائن الذي فرضناه لا يرث أي كائن آخر سوى Object (الذي ترثه كل الكائنات افتراضًا). أما في المثال التالي، فقد احتجنا لإجراء اختبار hasOwnProperty على العنصر الوارث لكي لا تظهر سوى الخاصة color التي يملكها بذاته ولم يرثها: var triangle = {a: 1, b: 2, c: 3}; function ColoredTriangle() { this.color = "red"; } ColoredTriangle.prototype = triangle; var obj = new ColoredTriangle(); for (var prop in obj) { if (obj.hasOwnProperty(prop)) { console.log("o." + prop + " = " + obj[prop]); } } // Output: // "o.color = red"حسنًا، لقد أطلنا الحديث عن حلقة for... in وهي ليست بالجديدة؛ لكنّنا أصبحنا نرى الحاجة لشيء جديد أكثر بساطة ومرونة، فهذا ما تبتغيه ES6 في النهاية، ولهذا نشأت فكرة المُكرّرات؛ التي تسمح لأي عنصر بأن يختار لنفسه الطّريقة التي يتصرّف بها عند المرور به في حلقة، ومع المُكرّرات لا بدّ من نوع جديد من الحلقات لتلبية هذه الحاجة والمحافظة على حلقة for... in للتوافق مع الإصدارات القديمة. من هنا نشأت حلقة for... of الجديدة. for (var num of [1, 2, 3]) { console.log(num); } // 1 // 2 // 3 for (var node of document.querySelectorAll('a')) { console.log(node); } // <a class="title" href="/"> // <a class="contact" href="/contact/">حصلنا في المثالين السابقين على قيمة الخاصّة وليس اسم الخاصّة، لكنّ هذا لا يعني أنّ حلقة for... of تُعيد قيم الخصائص دومًا، بل إنّها تستدعي مُكرّر الكائن (Iterator) وتطلب منه في كلّ دورة للحلقة تزويدها بشيء ما، وتترك للمُكرّر الحُريّة بإعادة أي قيمة يرغب بها، ولكن ولأنّنا نستدعي في مثالنا مصفوفة أولاً، وعنصر من نوع NodeList ثانيًا، وكلا النّوعين يُعيد مُكرُّرهما قيمَ العناصر في المصفوفة، فإنّنا نحصل على تلك النتيجة البديهيّة. بإمكاننا إنشاء أصناف بمُكرّرات خاصّة نُنشئها بأنفسنا، ولنفترض أن لدينا نوعًا لصفّ ضمن مدرسة ابتدائية، ونريد أن نحصل على تفاصيل الطلّاب على هيئة نص مُنسّق عند المرور على الصّفّ في حلقة for... of: function SchoolClass(students) { this.students = students; } SchoolClass.prototype[Symbol.iterator] = function*() { for (let i = 0; i < this.students.length; i++) { let student = this.students[i]; yield `#${i+1} ${student.name} (${student.age} years old)`; } } var ourClass = new SchoolClass([ { name: "Ahmed", age: 10 }, { name: "Alaa", age: 9 }/*, ...*/ ]); for (student of ourClass) { console.log(student); } // "#1 Ahmed (10 years old)" // "#2 Alaa (9 years old)" // "#3 ..." ...استخدمنا الرمز الخاص Symbol.iterator لإسناد دالّة مُولّد (Generator function) التي تُعطينا عند استدعائها نسخة من مُكرّر الصنف المُخصّص الذي أنشأناه. سنتعرف بعد قليل على المُولّدات (Generators) وكذلك على الرموز (Symbols) في وقت لاحق. لاحظ أنّنا استخدمنا حلقة for... of للمرور على محتويات ourClass. تذكّر أنّنا استخدمنا هذه الحلقة في الجزء السابق مع Array Comprehension، كما في المثال: let people = ["Samer", "Ahmed", "Khalid"]; console.log([`Hello ${person}` for (person of people)]); إن كانت الفقرة الأخيرة غامضة بعض الشيء فلا تقلق، سنتوسّع بشرح المولّدات بعد قليل. لكن دعونا نتوقّف قليلاً ولننتقل إلى الجانب الفلسفي لهذه الإضافات في JavaScript، قد تبدو للوهلة الأولى تعقيدات بلا طائل، خصوصًا وأنّ كثيرًا منها لا يهدف سوى للتسهيل، ولا يقدّم شيئًا يستحيل إنجازه بالإصدارات السابقة من اللغة؛ هنا يمكن الرّدّ بأنّ تطوّر اللغة متعلّق بكيفيّة استخدامها والخبرة التي تُكتسب مع مرور السّنين، حيث تظهر للمطوّرين حاجات جديدة وأفكار تطبّق مرارًا لدرجة أنها ترتقي لتصبح ضمن أساسات اللغة. سهولة كتابة الشفرة لم تعد رفاهية، بل هي ضرورة لإنجاز المشاريع الكبيرة لأنّها تتيح اختصار الوقت الذي كان سيضيع في كتابة متكرّرة ومُملّة، كما أنّها تُلبّي ما يتوقّعه المطوّرون من لغة أصبحت تؤخذ على محمل الجدّ وتُستخدم في تطوير تطبيقات ضخمة ومُعقّدة بعد أن كان جُلّ استخدامها تنفيذ بعض المهام البسيطة. المُولِّدات (Generators)المولدات (Generators) ببساطة هي دوال يمكن إيقافها والعودة إليها في وقت لاحق مع الاحتفاظ بسياقها دون تغيير، صياغة دوال المولدات لا تختلف كثيرًا عن صياغة الدوال التقليدية، كل ما عليك هو إضافة إشارة * بعد function واستخدام yield بدل return، المثال التالي سيوضح فكرة المولدات أكثر: function* getName() { let names = ['Muhammad', 'Salem', 'Abdullah']; for (name of names) { yield name; } } let nameGenerator = getName(); nameGenerator.next().value; // 'Muhammad' nameGenerator.next().value; // 'Salem' nameGenerator.next().value; // 'Abdullah' nameGenerator.next().value; // undefined } ما الذي يحدث هنا؟ فرضنا دالّة مولّد (Generator function) (والتي تُميّز بإشارة النجمة *) سمّيناها getName، وفيها صرحنا عن مصفوفة فيها أسماء، وظيفة هذه الدالة أن تعطينا عند استدعائها نسخة من مُكرّر (Iterator) (الذي شرحناه لتوّنا)، يزوّدنا بالأسماء بالترتيب في كل مرة نستدعيه فيها ليعطينا النتيجة التالية (next())، أولاً يجب حفظ نسخة المُكرّر ضمن متغير لكي نسمح له بحفظ حالته، ودون ذلك سيعطينا استدعاء دالّة المولد مباشرةً getName().next() دوماً النتيجة الأولى لأننا عملياً نُنشئ نسخة جديدة عنه في كل مرة نستدعيه، أما استدعاء نسخة عنه وحفظها في متغير مثل myGenerator فيسمح لنا باستدعاء .next() عليها كما هو متوقع. لا ترجع الدالة .next() القيمة التي نرسلها عبر yield فقط، بل ترجع كائناً يحوي القيمة المطلوبة ضمن الخاصة value، وخاصة أخرى done تسمح لنا بمعرفة ما إذا كان المولد قد أعطانا كل شيء. لنُعِدْ ترتيب أفكارنا: المولّدات تسمح بتوقّف تنفيذها مع الاحتفاظ بحالة التنفيذ (يحدث توقّف التنفيذ عند كلّ كلمة yield). فلو أنّنا كتبنا دالّة تقليديّة في المثال أعلاه مع return بدل yield لحصلنا في كلّ مرّة على الاسم الأول (Muhammad). وهذه الميزة في المولّدات يمكن استغلالها لإنشاء حلقات لا نهائية دون إعاقة متابعة البرنامج: function* numberGenerator() { for (let i = 0; true; i++) { yield i; } } let numGen = numberGenerator(); numGen.next(); // { value: 0, done: false } numGen.next(); // { value: 1, done: false } numGen.next(); // { value: 2, done: false } numGen.next(); // { value: 3, done: false } // ...دوالّ المولّدات تُعطي عند استدعاءها مُكرّرات، وهذا يعني إمكانيّة استخدامها في حلقة for... of (احذر من تطبيق مثال كهذا على مولّد غير منتهٍ كما في المثال السابق!): for (let name of getName()) { console.log(name); } // "Muhammad" // "Salem" // "Abdullah"لكلّ مُكرّر وظيفة .next() مهمّتها بدء تنفيذ الدّالّة أو متابعة تنفيذها ثم إيقافها مؤقّتًا عند كلّ كلمة yield. استدعاء next() على المُكرّر يعيد لنا في كلّ مرة كائنًا ذا خاصّتين: الأولى value وهي أيّ شيء نُعيده بكلمة yield، والثّانية done وهي قيمة منطقيّة (Boolean) تشير إلى حالة انتهاء تنفيذ الدّالة.تقبل الوظيفة .next() للمُكرّرات مُعاملاً اختياريًّا تستقبله وتُرسله لدالّة المولّد بعد متابعة التنفيذ، ويمكن استخدامها لإرسال رسائل لدالّة المولّد بحيث نؤثّر في تنفيذه: function* numberGenerator() { for (let i = 0; true; i++) { var reset = yield i; if (reset) i = -1; } } let numGen = numberGenerator(); numGen.next(); // { value: 0, done: false } numGen.next(); // { value: 1, done: false } numGen.next(); // { value: 2, done: false } numGen.next(); // { value: 3, done: false } numGen.next(true); // { value: 0, done: false }في هذا المثال مرّرنا القيمة true إلى الوظيفة .next() على المُكرّر، والذي بدوره يُرسلها لدالّة المولّد كنتيجة yeild i في الدّورة الموافقة للحلقة، لنقومَ بحفظها في متغيّر reset ونُجريَ فحصًا عند متابعة التنفيذ لإعادة تعيين قيمة i، التي ستزداد بمقدار واحد مع بدء الدورة التالية لحلقة for جاعلةً قيمة i مساوية للصّفر. خصائص المولّدات تجعلها مناسبة جدًا لكتابة شيفرة غير متزامنة بصورة أسهل تكاد تبدو فيها وكأنها شيفرة متزامنة خالية من الاستدعاءات الراجعة المتداخلة (Nested callbacks)؛ هذه الفكرة تحتاج إلى تركيز لأنها أساس لعدد من المكتبات مثل co وsuspend التي ظهرت مؤخّرًا وتصاعدت شعبيّتها بسرعة لأنّها تحلّ مشكلة جوهرية في استخدام JavaScript، ألا وهي التعامل مع الدوال غير المتزامنة (asynchronous functions) وذلك بالاعتماد كُليًّا على المُولّدات. لنفترض أنّ لدينا موقعًا لقراءة الكتب يعرض ملفّ المستخدم الشّخصيّ مع عدد الكتب التي قرأها وعنوان آخر كتاب مع تقييم المستخدم له: var list = document.querySelector("#book-list"); getJSON("http://reading-website.com/users/fwz.json", function(err, user) { if (err) return; // افعل شيئًا بما بخصوص الخطأ var num_books = user.books.length; var most_recent_book_id = user.books[num_books - 1]; getJSON("http://reading-website.com/users/fwz/ratings/" + most_recent_book_id + ".json", function(err, user_rating) { getJSON("http://reading-website.com/books/" + most_recent_book_id + ".json", function(err, book) { var fragment = document.createDocumentFragment(); var h2 = document.createElement("h2"); h2.textContent = user.full_name; var h3 = document.createElement("h3"); h3.textContent = "الكتب التي قرأها"; for (let book of books) { let li = document.createElement("li"); li.textContent = book.title + (book.id == most_recent_book_id ? " " + user_rating : ""); fragment.appendChild(li); } list.appendChild(fragment); }); }); })في المثال السابق احتجنا إلى إرسال 3 طلبات AJAX يعتمد أحدها على الآخر، ولأنّنا لا نستطيع إرسال طلب بتقييم المستخدم للكتاب قبل معرفة مُعرّف الكتاب، فلا بدّ من أن يرسل الطلب الخاصّ بتقييم الكتاب ضمن الاستدعاء الرّاجع لطلب معلومات المستخدم، ثمّ يمكن جلب عنوان الكتاب ضمن الاستدعاء الرّاجع للطلب السّابق، وهذا يعني زيادة تعقيد الشفرة مع تداخل الاستدعاءات الرّاجعة لتبدو أشبه بسباغيتي لا تُعرف بدايته من نهايته. تخيّلوا -لغرض التّخيّل- لو أمكننا كتابة هذه الشفرة (وهي غير متزامنة) لتبدو لقارئها وكأنها نص برمجي يسير بترتيب متزامن وبديهيّ... ألن يكون هذا أعظم شيء منذ اختراع JavaScript؟ var list = document.querySelector("#book-list"); try { var user = getJSON("http://reading-website.com/users/fwz.json"); var num_books = user.books.length; var most_recent_book_id = user.books[num_books - 1]; var user_rating = getJSON("http://reading-website.com/users/fwz/ratings/" + most_recent_book_id + ".json"); var book = getJSON("http://reading-website.com/books/" + most_recent_book_id + ".json"); var fragment = document.createDocumentFragment(); var h2 = document.createElement("h2"); h2.innerText = user.full_name; var h3 = document.createElement("h3"); h3.innerText = "الكتب التي قرأها"; for (let book of books) { let li = document.createElement("li"); li.innerText = book.title + (book.id == most_recent_book_id ? " " + user_rating : ""); fragment.appendChild(li); } list.appendChild(fragment); } catch (e) { // افعل شيئًا بما بخصوص الخطأ // Error } نحن نعلم أن الأمور لا يمكن أن تكون بهذه الروعة، وأنّ الشفرة أعلاه لن تعمل... نحن نعلم أن شيفرتنا تحتاج تفاصيل المستخدم للحصول على الكتب، وأننا نحتاج للكتاب لجلب عنوانه وتقييمه، وتنفيذ هذه المهمّات بشكل غير متزامن لا يعني أنّه ليس علينا انتظار المهمّة الأولى قبل إطلاق الثانية - بل يعني فقط أن المتصفح يمكنه تنفيذ رسم العناصر الأخرى وعرض الصفحات وإرسال طلبات أخرى في هذا الوقت. حسنًا، لدي خبر جيّد وآخر سيئ: أمّا الجيّد فهو أنّنا كتابة شيفرة شبيه بهذه أصبحت قريبة المنال مع الدّوالّ غير المتزامنة (Async Functions)، وأمّا الخبر السيّئ فهو أنّ علينا الانتظار إلى الإصدار 7 من ECMAScript لنستطيع كتابتها! (مع العلم أن المتصفّحات لم تنتهِ من تطبيق ES6!). لكن هذا لا يعني أن نقف مكتوفي الأيدي إلى أن تصدر ES7، بل بإمكاننا إيجاد حلّ وسط لهذه المشكلة؛ لماذا نضطّر إلى تعقيد الأمور بالاستدعاءات الرّاجعة المتداخلة؟ ألا يتوفّر في اللّغة بنية برمجيّة تسمح بإيقاف شيفرتنا ريثما يتمّ أمر ما غير متزامن (الانتظار لإكمال طلب AJAX) ثمّ المتابعة بعد انتهاءه؟ يبدو هذا الحديث مألوفًا! نعلم حتى الآن أننا بحاجة لاستخدام مولّد، ولذلك سنحيط شيفرتنا بدالّة مولّد كخطوة أولى: var list = document.querySelector("#book-list"); function* displayUserProfile() { // شيفرتنا هنا } الآن نحتاج لتنفيذ طلب AJAX الأوّل والانتظار إلى انتهاءه قبل الانتقال إلى الطّلب الثّاني، نعلم أنّ yield توقف تنفيذ المولّد: var list = document.querySelector("#book-list"); function* displayUserProfile() { yield getJSON("http://reading-website.com/users/fwz.json"); // ... } عظيم! لكن كيف نُخبر المولّد بأنّ عليه متابعة التنفيذ؟ var list = document.querySelector("#book-list"); function* displayUserProfile() { yield getJSON("http://reading-website.com/users/fwz.json", resume); // ... } سنمرر دالة اسمها resume للدّالة getJSON، وهذه الدالة ستُستدعى عند انتهاء جلب جواب الطّلب الذي أرسلناه، وهي فرصتنا لإخبار المولّد بمتابعة التنفيذ... فكيف سيكون محتواها؟ var list = document.querySelector("#book-list"); var resume = function(err, response) { displayIterator.next(response); } function* displayUserProfile() { var user = yield getJSON("http://reading-website.com/users/fwz.json", resume); var num_books = user.books.length; var most_recent_book_id = user.books[num_books - 1]; var user_rating = yield getJSON("http://reading-website.com/users/fwz/ratings/" + most_recent_book_id + ".json", resume); var book = yield getJSON("http://reading-website.com/books/" + most_recent_book_id + ".json", resume); var fragment = document.createDocumentFragment(); var h2 = document.createElement("h2"); h2.innerText = user.full_name; var h3 = document.createElement("h3"); h3.innerText = "الكتب التي قرأها"; for (let book of books) { let li = document.createElement("li"); li.innerText = book.title + (book.id == most_recent_book_id ? " " + user_rating : ""); fragment.appendChild(li); } list.appendChild(fragment); } var displayIterator = displayUserProfile(); displayIterator.next(); حفظنا نسخة عن المكرّر في متغيّر ثم استدعينا وظيفته next() في دالّة المتابعة، ممرّرين لها جواب الطّلب ليمكننا تخزينه ضمن المتغيّر user. الدّالة resume تستطيع الوصول إلى displayIterator لأنّه يكون معرّفًا قبل استدعاءها حتمًا، ولا ننسَ أن تعريف المتغيّرات في JavaScript يخضع لعملية الرّفع إلى أعلى النّطاق (variable hoisting) ممّا يجعل المتغيّر displayIterator موجودًا (وإن كان بلا قيمة) منذ بداية تنفيذ الشيفرة. للتأكّد من فهم هذه الشيفرة، سنعيد تحليلها خطوة بخطوة: في طلب AJAX الأوّل تستدعى الدالة resume ويمرّر إليها جواب الطّلب (response)، الذي يمرّر بدوره إلى المُكرّر ليُحفظ في المتغيّر user الذي سيُستخدم في الخطوة التّالية للمولّد لإرسال الطّلب الثّاني. تُكرّر العمليّة ذاتها للطلبين الآخرين ثمّ تُعرض النتائج في الصّفحة. الفائدة التي جنيناها من استخدام المولّدات هي التخلّص من تعقيد الاستدعاءات الرّاجعة نهائيًّا وتحويل شيفرة غير متزامنة وجعلها تبدو وكأنّها متزامنة. ذكرنا القليل عن مكتبات مثل co وsuspend، لكنّها باختصار تعمل بطريقة مماثلة جدًا لمثالنا الأخير: var suspend = require('suspend'), resume = suspend.resume; suspend(function*() { var data = yield fs.readFile(__filename, 'utf8', resume()); console.log(data); })();هذه المكتبات خطوة نحو مستقبل JavaScript، الذي بدأ يتشكل مع مشروع الدّوال غير المتزامنة باستخدام الكلمتين المفتاحيتين الجديدتين async وawait اللّتان ستتوفّران في الإصدار السّابع وتستندان في عملهما إلى أرضيّة الوعود (Promises) الّتي تتوفّر اليوم في ES6. سيكون بإمكاننا كتابة هذه الشيفرة بدل الاعتماد على المولّدات: async function displayUserProfile() { var user = await getJSON("http://reading-website.com/users/fwz.json"); var num_books = user.books.length; var most_recent_book_id = user.books[num_books - 1]; var user_rating = await getJSON("http://reading-website.com/users/fwz/ratings/" + most_recent_book_id + ".json"); var book = await getJSON("http://reading-website.com/books/" + most_recent_book_id + ".json"); var fragment = document.createDocumentFragment(); var h2 = document.createElement("h2"); h2.innerText = user.full_name; var h3 = document.createElement("h2"); h2.innerText = "الكتب التي قرأها"; for (let book of books) { let li = document.createElement("li"); li.innerText = book.title + (book.id == most_recent_book_id ? " " + user_rating : ""); fragment.appendChild(li); } list.appendChild(fragment); }في هذا المثال يجب على getJSON أن تُعيد وعدًا Promise ليستطيع مُفسّر اللّغة انتظاره إلى أن يُحقّق (resolve) أو يُرفض (reject)، والقيمة الّتي تُحقّق تُحفظ ضمن المُتغيّر user، وأما عند رفض الوعد يُرمى خطأ (throw) يمكن تلقّيه (catch) كما في الشيفرة غير المتزامنة. مُعامِل البقيّة (Rest parameter) والناشرة (Spread)بعد كلّ هذا الكلام المُعقّد عن الأشياء غير المتزامنة التي نريد جعلها تبدو متزامنة وما إلى ذلك، سنختم الجزء الثّاني بفكرتين بسيطتين أُضيفتا إلى ECMAScript في الإصدار السّادس وتحلّان مشكلتين شائعتين في كثير من اللّغات البرمجيّة: أمّا الأولى فهي الحاجة إلى تنفيذ نصّ برمجيّ ضمن دالة على عدد غير معروف من المُعاملات، فلنفترض أنّ لدينا دالة تجمع عددين: function add(n1, n2) { return n1 + n2; } add(1) // 1 add(1, 2) // 3ونظرًا لكوننا مبرمجين أذكياء فقد قرّرنا جعل الدّالة تقبل أي عددين أو ثلاثة أو أكثر... لنجعلها تقبل عددًا لا نهائيًّا من الأعداد؛ في الإصدار الحالي سنلجأ إلى استخدام الكائن الخاصّ arguments المتوفّر ضمن نطاق كلّ دالّة تلقائيًا: function add() { return [].reduce.call(arguments, function(memo, n) { return memo + n; }); } add(1) // 1 add(1, 2) // 3 add(1, 2, 3) // 6 حسنًا لقد اضطررنا إلى "استعارة" دالة الاختزال من مصفوفة فارغة لتطبيقها على الكائن الخاص arguments الذي يُعتبر "شبيه مصفوفة" ولا يملك ما تمتلكه المصفوفة من دوالّ، لماذا لا يمكننا كتابة هذا فحسب: function add(...numbers) { return numbers.reduce(function(memo, n) { return memo + n; }); } add(1) // 1 add(1, 2) // 3 add(1, 2, 3) // 6 وأمّا الفكرة الثّانية فهي تكاد تكون عكس السّابقة، فإذا كانت الأولى تجمع بقيّة المعاملات في كائن مُفرَد، فإنّ هذه "تَنشر" محتويات المصفوفة إلى عناصرها المكوّنة لها، ماذا لو لم نكن أذكياء وعجزنا عن الإتيان بدالة تجمع عددًا غير منتهٍ من الأرقام: function addThreeNumbers(n1, n2, n3) { return n1 + n2 + n3; } var myNumbers = [1, 2, 3]; addThreeNumbers(...myNumbers); // 6 لاحظ أن صياغة النشر (Spread) تطابق تمامًا صياغة البقيّة (Rest)، والاختلاف في السّياق فقط. لاحظ أيضًا أنّ معامل البقيّة، وكما يوحي اسمه، يمكن استخدامه لتجميع ما تبقى من مُعاملات الدّالة فقط: function addThreeOrMoreNumbers(n1, n2, ...numbers) { return n1 + n2 + numbers.reduce(function(memo, n) { return memo + n; }); } addThreeOrMoreNumbers(1, 2, 3); // 6في الجزء القادم سنتعرف بمشيئة الله على الوحدات (Modules) التي تُعتبر طريقة جديدة لتنظيم الشفرة اُستلهمَت من عالم Node.js وrequire.js، وسُنلقي نظرة على الأصناف (Classes)، المكوّن البرمجيّ الذي وجد طريقه أخيرًا إلى JavaScriptّ! المصادرشبكة مطوّري موزيلّاGoing Async With ES6 GeneratorsReplacing callbacks with ES6 Generators Iterators gonna iterate
-
مررنا جميعًا بموقف كهذا: بضع سطور JavaScript كُتبت لتؤدّي وظيفة صغيرة ثمّ تضخمت مع الوقت لتبلغ بضع عشرات من السطور، ثم مئات السطور... أصبحت تقبل معاملات أكثر، وتختبر شروطًا أكثر، ثم أتى ذلك اليوم: تقرير بعلّة، التطبيق لا يعمل كما ينبغي! وحينئذ يكون فكّ غموض هذه السطور المتشابكة مسؤوليّتنا. مع تزايد الأعباء الّتي نُلقيها على مشروعنا من جهة المتصفّح، يتّضح أمامنا أمران: أوّلهما: تجربة التطبيق بالنقر على الأزرار والروابط سريعًا ليس حلًّا عمليًّا، بل لا بدّ من الاختبارات المُؤتمتة حتّى نثق باستقرار مشروعنا؛ وثانيهما: علينا (غالبًا) أن نُغيّر كيفيّة كتابة برامجنا بما يُتيح لنا كتابة الاختبارات. هل أنا جادّة؟ نعم! لأنّنا وإن علمنا فائدة الاختبارات المؤتمتة، فإنّ أكثرنا اليوم لا يكتبون سوى اختبارات التكامل (integration tests). لا يُقلّل هذا من أهمّيّة اختبارات التكامل، فهي تُركّز على كيفيّة عمل أجزاء التّطبيق سويّةً، إلّا أنّها لا تستطيع إخبارنا إن كانت الوحدات المنفصلة لتطبيقنا تعمل كما ينبغي لها. هنا تبرز ضرورة اختبارات الوحدات (unit tests)، وستكون كتابة اختبارات الوحدات صعبة جدًّا إن لم يكن نصّ JavaScript الّذي كتبناه ملائمًا للاختبار. ما الفرق بين اختبارات الوحدات واختبارات التكامل؟كتابة اختبارات التكامل أمر بسيط عادةً: كل ما نفعله كتابة برنامج بسيط يصف كيف يتفاعل المُستخدم مع تطبيقنا، وما يجب أن يتوقّعه المستخدم عندئذٍ. إحدى برامج أتمتة المُتصفّحات الشّهيرة Selenium. وفي Ruby يُسهل Capybara التّعامل مع Selenium، وتتوفّر أدوات مشابهة كثيرة للغات الأخرى. فيما يلي اختبار تكامل لجزء من تطبيق بحث: def test_search fill_in('q', :with => 'cat') find('.btn').click assert( find('#results li').has_content?('cat'), 'Search results are shown' ) assert( page.has_no_selector?('#results li.no-results'), 'No results is not shown' ) end إذن فاختبار التكامل يهتمّ بتفاعل المستخدم مع التّطبيق، أمّا اختبار الوحدات فتركيزه ضيّق ويقتصر على جزء صغير من البرنامج: عندما أستدعي دالّةً (function) بمُدخلات معيّنة، فما المُخرجات المُتوقّعة؟ يصبح إخضاع التّطبيقات المكتوبة بأسلوب إجرائي (procedural) تقليديّ لاختبار الوحدات أمرًا شديد الصعوبة، وكذلك صيانتها وتنقيحها والبناء عليها. ولكن عندما نكتب نصوص برامجنا وفي ذهننا الحاجة لإجراء اختبارات الوحدات في المستقبل، فسنجد أنّ كتابة هذه الاختبارات أمر يسير، وسينعكس ذلك بالإيجاب على نصوصنا البرمجية كذلك. لنفهم ما أتحدّث عنه، دعنا نُلقِ نظرةً على تطبيق بحث بسيط: عندما يكتب المستخدم عبارة للبحث عنها، يُرسل التّطبيق طلب XMLHttpRequest للخادوم يطلب منه البيانات المُوافقة. يُجيبه الخادوم بالبيانات، مُصاغاة بـJSON، ليتلقّاها التطبيق ويعرضها في الصّفحة مُستخدمًا أدوات القولبة المُتوفّرة للمتصفّحات. يستطيع المستخدم أن ينقر على إحدى نتائج البحث ليُشير "بإعجابه" بها؛ وعندما يفعل ذلك، يُضاف اسم الشّخص الّذي أعجبه إلى قائمة "الإعجابات" على يمين الصّفحة. يمكن كتابة هذا البرنامج بأسلوب تقليدي كما يلي: var tmplCache = {}; function loadTemplate (name) { if (!tmplCache[name]) { tmplCache[name] = $.get('/templates/' + name); } return tmplCache[name]; } $(function () { var resultsList = $('#results'); var liked = $('#liked'); var pending = false; $('#searchForm').on('submit', function (e) { e.preventDefault(); if (pending) { return; } var form = $(this); var query = $.trim( form.find('input[name="q"]').val() ); if (!query) { return; } pending = true; $.ajax('/data/search.json', { data : { q: query }, dataType : 'json', success : function (data) { loadTemplate('people-detailed.tmpl').then(function (t) { var tmpl = _.template(t); resultsList.html( tmpl({ people : data.results }) ); pending = false; }); } }); $('<li>', { 'class' : 'pending', html : 'Searching …' }).appendTo( resultsList.empty() ); }); resultsList.on('click', '.like', function (e) { e.preventDefault(); var name = $(this).closest('li').find('h2').text(); liked.find('.no-results').remove(); $('<li>', { text: name }).appendTo(liked); }); }); يحلو لصديقنا Adam Sontag أن يُسمّي هذا الأسلوب "اختر مغامرتك الخاصّة"، ويحقّ له ذلك! ففي أيّ سطر ممّا سبق، قد نتعامل مع كيفيّة عرض البيانات، أو مع البيانات ذاتها، أو مع تفاعل المستخدم مع الصّفحة، أو مع حالة التّطبيق في لحظة معيّنة! من يدري!؟ هذا الأسلوب لا يجعل كتابة اختبارات التكامل صعبًا، ولكنّ اختبار المهامّ المختلفة للبرنامج تصبح معه غايةً في التعقيد. كيف ذلك؟ لنرَ: غياب كامل لبنية واضحة؛ فكلّ شيء يحدث ضمن $(document).ready() ثمّ في دوالّ مجهولة لا يمكن اختبارها لأنّها مُغلّفة ضمن الدّالة السّابقة ولا يمكن الوصول إليها.دوالّ معقدة؛ فعندما تتجاوز دالّة مُفردة 10 سطور، كما في دالّة إرسال النّموذج، فهذا يعني غالبًا أنّها تفعل أشياء أكثر مما ينبغي لها أن تفعل!حالة (state) مخفيّة أو مُشتركة؛ كالمتغيّر pending المُعرّف ضمن دالّة مُغلقة (closure)، لا سبيل لاختبار قيمته والتأكّد من كونها عُينت إلى قيمة صحيحة في لحظة ما.اعتماد شديد للأجزاء بعضها على بعض؛ فمثلاً: لا تحتاج دالّة نجاح طلب $.ajax وصولًا مباشرًا إلى عناصر الصّفحة.تنظيم النّصّ البرمجيّالخطوة الأولى لحلّ المشكلة السابقة هي إزالة هذا التّشابك، وفصل المهامّ السابقة إلى عدّة أجزاء: العرض والتفاعلإدارة البيانات وتخزينهاحالة التطبيق العامّةربط الأجزاء السّابقة مع بعضها لتعمل سويّةً!في الأسلوب التّقليديّ الّذي بيّناه سابقًا، تتشابك هذه المهمّات الأربع، ففي سطرٍ نتعامل مع العرض، وبعده ببضع سطور نتواصل مع الخادوم. يمكننا كتابة اختبار تكامل للبرنامج السابق بلا شكّ (بل ينبغي أن نفعل!)، ولكن كتابة اختبارات الوحدات أمرٌ صعب. في اختبارات التكامل، يمكننا افتراض عبارات من نحو "عندما يبحث المستخدم عن شيءٍ ما، يُتوقّع أن يرى النّتائج المُناسبة"، ولكن لا يمكننا التعمّق في التّفاصيل، وإن حدثت مشكلةٌ ما، ينبغي علينا تحديد موقع المشكلة بالضّبط، ولن تساعدنا اختبارات التكامل في ذلك. ولكنْ، إن أعدنا التفكير في كيفيّة كتابة نصّ البرنامج يمكننا كتابة اختبارات للوحدات تُسهِّل لنا تحديد موقع المشكلة، وتسمح بتسهيل صيانة النّصّ النهائي للبرنامج وتحسينه واستخدامه ثانيةً. سيتَّبع برنامجنا الجديد المبادئ التّالية: مثّل كلّ مهمّة ضمن كائن مُنفصل يقع تنصيفه في واحدة من المناطق الأربعة الّتي سبق ذكرها، دون أن يكون هذا الكائن في حاجة لمعرفة وجود الكائنات الأخرى. هذا يُجنّنبا تشابك النّص البرمجيّ.ادعم إمكانية الضّبط، بدلًا من كتابة قيم ثابتة. هذا سيُجنّبنا الحاجة لمحاكاة كامل بيئة HTML عند إجراء الاختبارات.أبقِ وظائف الكائنات بسيطة ومُوجزة. سيؤدّي هذا إلى نصّ برمجيّ أبسط وأسهل قراءةً.استخدم دوالّ مُشيّدة (constructors) لإنشاء نسخ من الكائنات، هذا يُتيح إنشاء نُسخ "نظيفة" من كلّ جزء من البرنامج لغرض الاختبار.بدايةً، علينا أن نكتشف كيف سنفصل تطبيقنا لعدّة أجزاء. سيكون لدينا 3 أجزاء مُخصّصة للعرض والتفاعل: نموذج البحث ونتائج البحث ومساحة الإعجابات. سيكون لدينا أيضًا جزء مُخصّص لجلب البيانات من الخادوم، وجزء آخر يربط بين تلك الأجزاء كلها. لنبدأ بالنّظر في واحد من أبسط أجزاء التطبيق: مساحة الإعجابات. في الإصدار السّابق من برنامجنا، كان النّصّ التّالي مسؤولًا عن تحديث هذه المساحة: var liked = $('#liked'); var resultsList = $('#results'); // ... resultsList.on('click', '.like', function (e) { e.preventDefault(); var name = $(this).closest('li').find('h2').text(); liked.find( '.no-results' ).remove(); $('<li>', { text: name }).appendTo(liked); });لاحظ كيف يتداخل جزء نتائج البحث في جزء مساحة الإعجابات وكيف يحتاج أن يعلم الكثير عن عناصره في الصّفحة. إن أردنا كتابة هذا الجزء بأسلوب أسهل اختبارًا، فعلينا إنشاء كائن لمساحة الإعجابات مسؤولٍ عن تعديل عناصر الصّفحة المُتعلّقة بقائمة الإعجابات: var Likes = function (el) { this.el = $(el); return this; }; Likes.prototype.add = function (name) { this.el.find('.no-results').remove(); $('<li>', { text: name }).appendTo(this.el); }; يوفّر هذا النّصّ دالّة تشييد (constructor function) تُعطي نسخة جديدة من مساحة الإعجابات. للنسخة المُنشأة وظيفة .add() يمكن استخدامها لإضافة نتائج جديدة. ولإثبات صحّة عملها، يمكننا كتابة بضعة اختبارات: var ul; setup(function(){ ul = $('<ul><li class="no-results"></li></ul>'); }); test('constructor', function () { var l = new Likes(ul); assert(l); }); test('adding a name', function () { var l = new Likes(ul); l.add('Brendan Eich'); assert.equal(ul.find('li').length, 1); assert.equal(ul.find('li').first().html(), 'Brendan Eich'); assert.equal(ul.find('li.no-results').length, 0); });ليس الأمر صعبًا كما ترى! استخدمنا Mocha كإطار عمل للاختبار وChai كمكتبة لإنشاء الافتراضات (assesrtions). Mocha توفّر test وsetup، وChai توفّر assert. تتوفّر أطر عمل ومكتبات للافتراضات كثيرة، ولكنّ السابقتين ملائمتان تمامًا للمُبتدئين. عليك اختيار ما يناسبك ويناسب مشروعك. اطّلع على QUnit وIntern (الأخيرة تبدو واعدة). يبدأ برنامج الاختبا بإنشاء عنصر في الصّفحة سنستخدمه لاحتواء مساحة الإعجابات، ثمّ ينطلق اختباران: الأول للتأكّد من إمكانيّة إنشاء مساحة الإعجابات، والآخر للتأكد من أن وظيفة .add() تعطينا النتيجة المرغوبة. بعد إنشاء هذين الاختبارين، يمكننا إعادة صياغة النصّ البرمجي لمساحة الإعجابات، مطمئنين إلى أنّ الأخطاء الّتي نرتكبها عندئذٍ ستُكتشف فورًا في الاختبار. يبدو تطبيقنا الآن كما يلي: var liked = new Likes('#liked'); var resultsList = $('#results'); // ... resultsList.on('click', '.like', function (e) { e.preventDefault(); var name = $(this).closest('li').find('h2').text(); liked.add(name); });جزء نتائج البحث أشدّ تعقيدًا من مساحة الإعجابات، لكن لنجرّب إعادة صياغته أيضًا. نريد أن نُنشئ وظائف للتفاعل مع نتائج البحث كما أنشأنا الوظيفة .add() لمساحة الإعجابات. نريد طريقةً لإضافة نتائج جديدة، وكذلك طريقة "لبثّ" التّغيرات الّتي تطرأ على النّتائج لبقيّة أجزا التّطبيق (كأن يُعجب المستخدم بنتيجة مثلاً). var SearchResults = function (el) { this.el = $(el); this.el.on( 'click', '.btn.like', _.bind(this._handleClick, this) ); }; SearchResults.prototype.setResults = function (results) { var templateRequest = $.get('people-detailed.tmpl'); templateRequest.then( _.bind(this._populate, this, results) ); }; SearchResults.prototype._handleClick = function (evt) { var name = $(evt.target).closest('li.result').attr('data-name'); $(document).trigger('like', [ name ]); }; SearchResults.prototype._populate = function (results, tmpl) { var html = _.template(tmpl, { people: results }); this.el.html(html); };الآن يمكن لنصّنا البرمجيّ القديم المسؤول عن إدارة التفاعلات بين نتائج البحث ومساحة الإعجابات أن يصبح كما يلي: var liked = new Likes('#liked'); var resultsList = new SearchResults('#results'); // ... $(document).on('like', function (evt, name) { liked.add(name); })وهذا أبسط وأقل تشابكًا، لأنّنا نستخدم document كممرّ عامّ للرّسائل المُتبادلة بين الأجزاء بحيث لا يضطرّ أحدها إلى أن يعلم وجود الآخر. (ملاحظة: في تطبيق حقيقيّ، ربّما يُستخدم شيء مثل Backbone أو مكتبة RSVP لإدارة الأحداث، استخدمنا document للتبسيط.) كذلك أخفينا كل التّفاصيل المُعفّدة (كإيجاد اسم الشّخص الّذي أُعجب المستخدم به) داخل كائن نتائج البحث، بدل أن نترك هذه التّفاصيل تُلوّث نصّ تطبيقنا. أفضل ما في الأمر هو أنّ بإمكاننا الآن كتابة اختبارات للتأكّد من عمل كائن نتائج البحث كما يجب: var ul; var data = [ /* fake data here */ ]; setup(function () { ul = $('<ul><li class="no-results"></li></ul>'); }); test('constructor', function () { var sr = new SearchResults(ul); assert(sr); }); test('display received results', function () { var sr = new SearchResults(ul); sr.setResults(data); assert.equal(ul.find('.no-results').length, 0); assert.equal(ul.find('li.result').length, data.length); assert.equal( ul.find('li.result').first().attr('data-name'), data[0].name ); }); test('announce likes', function() { var sr = new SearchResults(ul); var flag; var spy = function () { flag = [].slice.call(arguments); }; sr.setResults(data); $(document).on('like', spy); ul.find('li').first().find('.like.btn').click(); assert(flag, 'event handler called'); assert.equal(flag[1], data[0].name, 'event handler receives data' ); });التفاعل مع الخادم جزء آخر يجب إنشاؤه، وقد احتوى النّص البرمجيّ القديم على طلب $.ajax() فيه استدعاء راجع (callback) يتعامل مباشرةً مع عناصر الصّفحة: $.ajax('/data/search.json', { data : { q: query }, dataType : 'json', success : function( data ) { loadTemplate('people-detailed.tmpl').then(function(t) { var tmpl = _.template( t ); resultsList.html( tmpl({ people : data.results }) ); pending = false; }); } }); مُجدّدًا أقول: من الصعب كتابة اختبار وحدات للنّصّ السابق لأن أشياء كثيرةً تحدث في بضع سطور. يمكننا إعادة صياغة القسم المسؤول عن البيانات في صورة كائن مُنفصل: var SearchData = function () { }; SearchData.prototype.fetch = function (query) { var dfd; if (!query) { dfd = $.Deferred(); dfd.resolve([]); return dfd.promise(); } return $.ajax( '/data/search.json', { data : { q: query }, dataType : 'json' }).pipe(function( resp ) { return resp.results; }); };وعندها يمكن تعديل النّصّ البرمجيّ لجلب النّتائج إلى الصّفحة: var resultsList = new SearchResults('#results'); var searchData = new SearchData(); // ... searchData.fetch(query).then(resultsList.setResults); لاحظ كم بسّطنا نصّ تطبيقنا، وكيف عزلنا التّعقيدات ضمن كائن بيانات البحث، بدلًا من تركه حُرًّا ضمن نصّ تطبيقنا العامّ. قمنا أيضًا بجعل واجهة البحث قابلةً للاختبار. ولكن هناك بضعة أمور يجب النّظر فيها عندما نختبر جزءًا من برنامج يتفاعل مع الخواديم. أوّل هذه الأمور: لا نريد أنّ نتفاعل مع الخادوم حقيقةً، ففعل ذلك يعني العودة إلى عالم اختبارات التّكامل، ولأنّنا مُطوّرون أكفاء، فقد انتهينا من كتابة تلك، صحيح؟ ما نريده الآن هو "محاكاة" التّفاعل مع الخادوم، الأمر الّذي يمكن إنجازه بمكتبة Sinon. ثانيًا: يجب أيضًا اختبار المسارات غير المُتوقّعة للتطبيق، كعبارة بحث فارغة مثلًا. <code class="lang-javascript"> </code>test('constructor', function () { var sd = new SearchData(); assert(sd); }); suite('fetch', function () { var xhr, requests; setup(function () { requests = []; xhr = sinon.useFakeXMLHttpRequest(); xhr.onCreate = function (req) { requests.push(req); }; }); teardown(function () { xhr.restore(); }); test('fetches from correct URL', function () { var sd = new SearchData(); sd.fetch('cat'); assert.equal(requests[0].url, '/data/search.json?q=cat'); }); test('returns a promise', function () { var sd = new SearchData(); var req = sd.fetch('cat'); assert.isFunction(req.then); }); test('no request if no query', function () { var sd = new SearchData(); var req = sd.fetch(); assert.equal(requests.length, 0); }); test('return a promise even if no query', function () { var sd = new SearchData(); var req = sd.fetch(); assert.isFunction( req.then ); }); test('no query promise resolves with empty array', function () { var sd = new SearchData(); var req = sd.fetch(); var spy = sinon.spy(); req.then(spy); assert.deepEqual(spy.args[0][0], []); }); test('returns contents of results property of the response', function () { var sd = new SearchData(); var req = sd.fetch('cat'); var spy = sinon.spy(); requests[0].respond( 200, { 'Content-type': 'text/json' }, JSON.stringify({ results: [ 1, 2, 3 ] }) ); req.then(spy); assert.deepEqual(spy.args[0][0], [ 1, 2, 3 ]); }); });سأترك ما تبقّى من إعادة صياغة نموذج البحث لغرض الاختصار، وقد قمت بتبسيط بعض الاختبارات والنّصوص أعلاه مُجدّدًا، ويمكنك الاطّلاع على الإصدار النّهائي للتطبيق هنا إن كنت مُهتمًّا. عندما ننتهي من كتابة تطبيقنا بأسلوب يسمح بإجراء الاختبارات، سيكون مشروعنًا أكثر نظافةً ممّا بدأنا به: $(function() { var pending = false; var searchForm = new SearchForm('#searchForm'); var searchResults = new SearchResults('#results'); var likes = new Likes('#liked'); var searchData = new SearchData(); $(document).on('search', function (event, query) { if (pending) { return; } pending = true; searchData.fetch(query).then(function (results) { searchResults.setResults(results); pending = false; }); searchResults.pending(); }); $(document).on('like', function (evt, name) { likes.add(name); }); }); والأهمّ من ذلك أنّنا ننتهي ولدينا مشروعٌ أُخضع لاختبارات دقيقة، وهذا يعني أنّ بإمكاننا إعادة كتابة الأجزاء ونحن مطمئنون إلى أنّ ذلك لن يسيئ إلى عمل التطبيق. بإمكاننا أيضًا إضافة اختبارات جديدةً عندما نكتشف مشكلات جديدة، ثمّ كتابة النّصّ الّذي يجعل هذه الاختبار تنجح. اختبار البرامج يُسهّل حياتك على المدى البعيدمن السهولة أن تنظر إلى ما سبق وتقول لنفسك: "مهلًا! أتريدين منّي كتابة برامج أطول لأداء المهمّة ذاتها؟" عندما يتعلّق الأمر ببناء المشاريع على الإنترنت، فإنّه لا مفرّ من هذه الحقيقة: لا بدّ من وقتٍ كافٍ لتصميم منهجٍ لحلّ مشكلة. لا بدّ من تجربة الحلّ، سواءٌ كانت التّجربة يدويّة تقوم بها بنفسك في المتصفّح، أو مؤتمتة، أو أن تترك مستخدميك يجرّبوا مشروعك بعد نشره (فكرة مرعبة!). ستُغيّر نصوصك البرمجيّة، ثمّ سيستخدمها آخرون، وفي النّهاية ستبقى العلل موجودة مهما بلغ عدد الاختبارات الّتي تكتبها. صحيحٌ أنّ الاختبارات تتطلّب وقتًا عند كتابتها أوّل مرّة، ولكنّها ستوفّر وقتك في المدى البعيد، سترضى عن نفسك عندما يفشل أحد الاختبارات الّتي كتبتها كاشفًا عن علّة قبل أن يصل النّص البرمجيّ إلى مرحلة الإنتاج، وكذلك عندما يكون لديك نظامٌ يُثبت أنّ العلل قد حلّت بالفعل قبل أن تجد طريقها إلى المُستخدمين! مصادر إضافيّةكلّ ما كتبته أعلاه ليس إلّا أقلّ القليل من موضوع اختبارات JavaScript، فإن أردت المزيد: شاهد العرض الذي قدّمته في مؤتمر Full Frontal عام 2012 في برايتون في المملكة المتحدة.أو جرّب Grunt، وهي أداة تُساعدك في أتمتة عمليّة الاختبارات وأشياء أخرى كثيرة.أو اقرأ كتاب Test-Driven JavaScript Development لمؤلّفه Christian Johansen، صاحب مكتبة Sinon. هذا الكتاب مرجع ثمينٌ وضخم في موضوع اختبارات JavaScript ترجمة -وبتصرّف- للمقال Writing Testable JavaScript لصاحبته Rebecca Murphey
-
بعد أن أنشأنا نظام المستخدمين والجلسات، أصبحنا جاهزين الآن لبناء نظام التّعليقات، ثم إتاحة إمكانية إنشاء تدوينة جديدة وتعديل التّدوينات السّابقة. إتاحة التّعليقلحفظ التّعليقات نحتاج أولًا إلى جدول جديد يحفظ نصّ التعليق وتاريخه وكاتبه والتّدوينة التي أُضيف إليها، افتح صدفة 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.
-
لا قيمة لمدوّنتنا إن لم يكن باستطاعتنا كتابة التّدوينات الجديدة ونشرها، لذا علينا إنشاء صفحة تُتيح لنا (لنا فقط) كتابة تدوينة جديدة وحفظها في قاعدة البيانات. لكن أوّل ما يتبادر إلى الذّهن تساؤل عن الكيفيّة الّتي نستطيع أن نمنع فيها الزّائر من إضافة التّدوينات، إذ كيف يستطيع المتصفّح والخادوم التّفريق بين صاحب المدوّنة وزائرها؟ ملايين المواقع على الويب تقدّم محتوىً مخصّصًا لكلّ مستخدم، إذا عدنا لمثال فيس بوك وطرحنا السؤال ذاته: كيف يعرض فيس بوك نشرة أخبار خاصّة بكلّ مستخدم؟ بالطّبع لكلّ مستخدم حساب في الموقع محميّ بكلمة مرور، لكن ما الذي يحدث بين المتصفّح والخادوم ويجعل الخادوم يُرسل الصّفحة الخاصّة بالمستخدم إليه دون غيره؟ إن كنت سمعت من قبل بالكعكات (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 لتشاهد الملف الشّخصيّ (حسنًا لا يبدو عظيمًا جدًّا، لكنّنا سنحسّنه لاحقًا): تهانينا! لقد أنشأنا نظامًا للمستخدمين وأصبح بإمكاننا عرض محتوى مخصّص لكلّ مستخدم! في الجزء القادم سنتيح لأنفسنا كتابة التّدوينات، وللمستخدمين إضافة التعليقات، وستكون أعظم مدوّنة في التّاريخ!
-
في الدّرس السّابق قمنا بثتبيت 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 في التّفاهم بين الخادوم والمتصفّح. عظيم! لدينا الآن صفحة رئيسيّة منسّقة وصفحات مفردة للتّدوينات، في الدّرس القادم سنقوم بإنشاء نظام للمستخدمين تمهيدًا لإتاحة التّعليقات وكتابة تدوينات جديدة.
-
في الجزء السّابق أنشأنا الصّفحة الرئيسيّة للمدوّنة وصفحات مفردة لكلّ تدوينة بعد تهيئة المشروع وإنشاء قواعد البيانات، الآن سنقوم بإنشاء نظام للمستخدمين لنسمح للقرّاء بالتّعليق. إنشاء صفحة "حساب جديد" و"تسجيل الدّخول"نعلم إذًا أنّنا بحاجة أولاً إلى آلية لإنشاء الحسابات على خادومنا، وأوّل ما نقوم به إنشاء صفحة على الرّابط /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)، الّتي ستكون موضوع الدّرس القادم.
-
التّصميم الجيّد للمُنتجات يعني الاعتناء بالنواحي المهمّة للمنتج وإيلاءها اهتمامًا أكبر لتكونَ النتيجة واجهة استخدام جميلة ومُفيدة ومفهومة، لكن إياك أن تظنّ أنّ التصميم يقع على عاتق المُصمّمين فقط! التصميم مطلوب في النصوص البرمجية، ليس فقط في النصوص البرمجية المكتوبة لبناء الواجهات المرئيّة للمستخدم — بل المقصود تصميم النّصوص البرمجيّة ذاتها. النّصوص المُصمّمة بإتقان أسهل صيانةً وإمكانيّة تطويرها والبناء عليها أكبر، ممّا يجعل المُطوّرين أكثر كفاءة، لأنّها تسمح بتوجيه الاهتمام والجهد نحو بناء مُنتجات أفضل، والنتيجة شعورٌ عامّ بالراحة - راحة المطوّرين والمستخدمين وكلّ من يهمّه الأمر! لتصميم النّصوص البرمجيّة نواحٍ ثلاث عامّة غير مرتبطة بلغة برمجة مُعيّنة: بنية النظام: المُخطّط العامّ لمجموعة ملفّات المشروع، والقواعد الّتي تُحدد كيف تتواصل مكوّناته المختلفة فيما بينها، سواء كانت نماذج البيانات أم واجهات العرض أم المُتحكّمات.إمكانية الصيانة: إلى أي حدّ يمكن تحسين النصّ البرمجيّ والبناء عليه؟إعادة الاستخدام: هل يمكن إعادة استخدام مكوّنات المشروع؟ هل يسهُل تخصيص أحد هذه المكوّنات لإعادة استخدامه؟التصميم المُتقن للنّصوص البرمجية في اللّغات المرنة مثل JavaScript يفرض على المطوّر إلزام نفسه بمجموعة من القواعد، لأنّ بيئة JavaScript المُتسامحة قد تسمح للمشروع أن يعمل وإن كانت أجزاؤه مبعثرة هنا وهناك. لذلك فإن تأسيس بنية المشروع والالتزام بها منذ البداية يضمنان انسجامه حتّى تمامه. نمط الوحدات (module pattern) هو إحدى الطّرق المُجرّبة والّتي أثبتت فائدتها في تصميم البرمجيّات، إذ تتلاءم بنيتها القابلة للتوسيع مع ما يتطلّبه بناء مشروع برمجيّ متماسك وسهل الصّيانة. أعتمدُ هذا النّمط في بناء إضافات jQuery لأنّه يسمح بإعادة استخدام الوحدات وضبطها من خلال مجموعة من الخيارات عبر واجهة برمجيّة مُتقنة التّصميم. سنستعرض فيما يلي كيف يُكتبُ نصّ برمجيّ موزّع في مكوّنات يمكن استخدامها في مشاريع أخرى لاحقة. نمط الوحداتتكثر أنماط التّصميم، وتكثر معها مصادر تعلّمها. كتب Addy Osmani كتابًا ممتازًا (ومجّانيًّا!) عن أنماط التّصميم في JavaScript، أنصح كلّ المطوّرين من كلّ المستويات بالاطّلاع عليه. يٌسهّل نمط الوحدات تنظيم المشروع ويضمن بقاءه نظيفًا ومرتّبًا، والوحدة التي نقصدها ليست سوى كائن JavaScript حرفيّ تقليديّ (object literal) يتضمّن وظائف (methods) وخصائص (properties)، وهذه البنية البسيطة هي أفضل ما في نمط الوحدات لأنّها تسمح حتى لغير المتمرّسين ممّن يطّلعون على النّصّ البرمجيّ أن يقرؤوه ويفهموا بسرعة كيف يعمل. في التّطبيقات التي تتبنّى هذا النّمط، يكون لكلّ مكوّن فيه وحدة منفصلة تتبع له؛ فمثلاً، يمكن لحقل نصّ في الواجهة المرئيّة يسمح بالإكمال التلقائيّ للكلمات أن يُبنى من وحدتين، الأولى للحقل ذاته، والأخرى لقائمة الكلمات المُقترحَة. تتواصل الوحدتان فيما بينهما دون أن يتداخل نصّاهما البرمجيّان معًا. هذا العزل بين المكوّنات هو ما يجعلها مناسبة لإقامة بنية متماسكة للمشاريع، حيث تكون العلاقات بين أجزاء التطبيق محدّدة بدقّة؛ ويتولّى حقل النّصّ ما يرتبط به ضمن وحدته، ولا تُترك هذه الأمور مبعثرة بين الملفّات — لنحصل بالنّتيجة على نصّ برمجيّ واضح. توفّر البنية القائمة على الوحدات فائدة أخرى، إذ تسمح طبيعتها بتسهيل صيانة المشروع، وذلك بتطوير كلّ وحدة بصورة منفصلة دون أن تتأثّر بقيّة أجزاء التّطبيق. استخدمت نمط الوحدات لإنشاء إضافة jPanelMenu، وهي إضافة لـjQuery تسمح بإنشاء قائمة للموقع خارج الشاشة (off-canvas menu). سأستخدم هذه الإضافة كمثال عن بناء وحدة وفق هذا النمط. بناء الوحدةبدايةً سأفرض ثلاث وظائف وخاصّة واحدة تُستخدم جميعها للتفاعل مع القائمة. var jpm = { animated: true, openMenu: function( ) { … this.setMenuStyle( ); }, closeMenu: function( ) { … this.setMenuStyle( ); }, setMenuStyle: function( ) { … } };الفكرة هنا هي تجزئة النصّ إلى أصغر أجزاء مُفيدة ويمكن إعادة استخدامها. كان بإمكاننا كتابة وظيفة واحدة toggleMenu( ) لتبديل وضع القائمة بين الظهور والخفاء، ولكن إنشاء وظيفتين منفصلتين openMenu( ) وcloseMenu( ) يعطينا تحكّمًا أكبر ويسمح بإعادة استخدامهما ضمن الوحدة. لاحظ أنّ استدعاء وظائف الوحدة وخواصّها من ضمن الوحدة ذاتها (مثل استدعاء setMenuStyle( )) يُسبق بالكلمة المفتاحية this، فهذه هي طريقة وصول الوحدات إلى أعضائها. هذه هي البنية الأساسيّة لكلّ وحدة. يمكنك إضافة خواص ووظائف أخرى عند الحاجة إليها، ولكنّ الوحدة ستبقى بالبساطة ذاتها. بعد إقامة الأساسات السابقة، يمكن بناء طبقة فوقها لإعادة استخدام الوحدة، وذلك من خلال إتاحة ضبط خيارات الوحدة والتحكم بها. إضافات jQueryقد يكون الجانب الثالث للتصميم المُتقن هو الجانب الأهمّ: إعادة الاستخدام. هذه الفقرة يرافقها تحذير؛ فعلى الرغم من وجود طرق لكتابة مكوّنات يمكن إعادة استخدامها بـJavaScript "ساده"، إلّا أنّني أفضّل بناء الوحدة على صورة إضافة jQuery عندما تكون الأمور مُعقّدة، ولهذا أسباب عدّة. أهمّ هذه الأسباب: بناء الوحدة بصورة إضافة jQuery يوصل للمُطوّرين رسالة ضمنيّة بأنّ وحدتنا تعتمد على وجود jQuery، فهذا يجب أن يكون واضحًا منذ البداية لمن يُريد استخدامها. يُضاف إلى السّبب السّابق أنّ النّص البرمجيّ الّذي سنكتبه سيكون مُنسجمًا مع بقيّة أجزاء المشروع المُقامة على jQuery، وهذا جيّد لاعتبارات جماليّة أولًا، ولأنّه يسمح للمطوّرين بفهم استخدام الإضافة إلى حدٍّ ما دون كثير عناء، فهي إذًا طريقة أخرى لتحسين الواجهة البرمجيّة الّتي سيتخدمها المطوّرون. قبل البدء ببناء إضافة jQuery، تأكّد من أنّها لا تتعارض مع مكتبات JavaScript أخرى تستخدم الرّمز $. لا تقلق فالأمر أبسط ممّا يبدو، كلّ ما عليك هو إحاطة نصّ الإضافة بما يلي: (function($) { // نص الإضافة البرمجي هنا })(jQuery);بعد ذلك سنُعدُّ الإضافة ونُلقي بالوحدة الّتي كتبناها سابقًا ضمن الإضافة. إضافة jQuery ليس إلا وظيفة مُعرِّفة على الكائن $ (كائن jQuery). (function($) { $.jPanelMenu = function( ) { var jpm = { animated: true, openMenu: function( ) { … this.setMenuStyle( ); }, closeMenu: function( ) { … this.setMenuStyle( ); }, setMenuStyle: function( ) { … } }; }; })(jQuery);كل ما علينا لاستخدام الإضافة استدعاء الدّالة الّتي أنشأناها للتّوّ. var jpm = $.jPanelMenu( );الخياراتلا تكون الإضافة صالحةً لإعادة الاستخدام إن لم تُتِح تخصيصها وفق متطلّبات المشروع، فكلّ مشروع له أسلوبه الخاصّ وبنيته الخاصّة، وإتاحة ضبط الوحدة بالخيارات يسمح للإضافة بالتكيّف مع حاجات المشاريع المختلفة. تزويد الوحدة بقيم مبدئية للخيارات أمرٌ مفضَّل. أسهل طريقة لفعل ذلك استخدام وظيفة jQuery $.extend() الّتي تستقبل على الأقل مُعاملين اثنين (arguments). اجعل المُعامل الأول لهذه الوظيفة كائنًا يوفّر كل الخيارات المتاحة وقيهما المبدئيّة، واجعل المعامل الثاني الخيارات الّتي يُقدّمها المُطور الّذي يستخدم الإضافة. ستقوم الوظيفة بدمج الكائنين مع إحلال الخيارات الّتي قدّمها المّطوّر مكان مقابلاتها المبدئيّة. (function($) { $.jPanelMenu = function(options) { var jpm = { options: $.extend({ 'animated': true, 'duration': 500, 'direction': 'left' }, options), openMenu: function( ) { … this.setMenuStyle( ); }, closeMenu: function( ) { … this.setMenuStyle( ); }, setMenuStyle: function( ) { … } }; }; })(jQuery);إضافةً إلى توفير القيم المبدئيّة، تشرح الخيارات نفسها بنفسها، أي يستطيع من يقرأ النّصّ البرمجيّ رؤية كلّ الخيارات المُتاحة مباشرةً. أتِح أكبر عددٍ ممكن من الخيارات، فهذا قد يكون مُفيدًا في المستقبل، والمرونة لن تضرّ! الواجهة البرمجيّةالخيارات الّتي تتيحها الإضافة وسيلة رائعة لتخصيص عملها، وأمّا الواجهة البرمجيّة، فهي تتيح توسيع وظيفة الإضافة بكشف محتوى الإضافة من خصائص ووظائف ليستطيع المشروع الاستفادة منها. مع أنّ إتاحة أكبر ما يمكن من الوحدة عبر الواجهة البرمجيّة أمرٌ حسن، إلا أنّه ليس على من يستخدمها من الخارج الوصول إلى كلّ المكوّنات الدّاخليّة. أتِح في الواجهة البرمجيّة العناصر الّتي ستُستخدم فقط. في مثالنا، يجب على الإضافة أن تُتيح الوصول إلى وظائف فتح القائمة وإغلاقها فقط، أمّا الوظيفة الداخليّة setMenuStyle( ) فيجب أن تُستدعى عندما تُفتح القائمة أو تغلق، ولكن ليس على الأجزاء الخارجيّة من المشروع أن تصل إليها. لكشف الواجهة البرمجيّة، أعِد كائنًا يحوي الوظائف والخصائص المرغوب كشفها في نهاية نصّ الإضافة، بإمكانك أيضًا ربط الكائنات المُعادة مع تلك المُحتواة ضمن الوحدة؛ وفي هذا التنظيم يكمن جمال نمط الوحدات. (function($) { $.jPanelMenu = function(options) { var jpm = { options: $.extend({ 'animated': true, 'duration': 500, 'direction': 'left' }, options), openMenu: function( ) { … this.setMenuStyle( ); }, closeMenu: function( ) { … this.setMenuStyle( ); }, setMenuStyle: function( ) { … } }; return { open: jpm.openMenu, close: jpm.closeMenu, someComplexMethod: function( ) { … } }; }; })(jQuery);ستكون خصائص الواجهة ووظائفها متاحةً عبر هذا الكائن الّذي أُعيد بعد تهيئة الإضافة. var jpm = $.jPanelMenu({ duration: 1000, … }); jpm.open( );"صقل" واجهة المُطوّرينباتّباع الخطوط العامّة البسيطة والقليل من المفاهيم، أنشأنا لأنفسنا إضافة صالحةً لإعادة الاستخدام والتّوسعة، وهذا سيُسهّل عملنا كثيرًا، وككلّ ما نُنجزه تبقى التّجربة العامل الحاسم قبل اعتماد هذا النّمط على مستوى فريقك، وبما يُناسب سياق عملك. كلّ ما وجدت نفسي أكتب شيئًا يمكن إعادة استخدامه لاحقًا، بادرتُ لفصله في إضافة jQuery مبنيّة كوحدة. أفضل ما في هذا الأسلوب أنّه يجبرك على استخدام النّصّ الّذي تكتبه وتجربته، وعندما تستخدم شيئًا وأنت تُنشئه، يكون تحديد مواضع القوّة ومواضع الضّعف أسرع، الأمر الّذي يسمح لك بتغيير خطّة العمل باكرًا. هذه العمليّة تُعطي في النّهاية نصًّا برمجيًّا مُجرّبًا وجاهزًا لإتاحته كمشروع مفتوح المصدر يستقبل المساهمات، وقد قمت بالفعل بنشر إضافاتي (الّتي أعتبرها شبه ممتازة) على GitHub. وحتّى إن لم ترغب بنشر ما تكتبه للعموم، فإنّ تصميم النّصّ البرمجيّ أمر شديد الأهمّيّة، وستشكر نفسك بسببه في المستقبل! ترجمة -وبتصرّف- للمقال The Design of Code: Organizing JavaScript لصاحبه Anthony Colangelo
-
التعريف بالمشروع هذه سلسلة من الدّروس مُوجّهة للمبتدئين بتطوير الويب، تهدف إلى تعليم استخدام بيئة 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. نحن الآن جاهزون للعمل! في الدّرس القادم سنبدأ بإنشاء الصّفحة الرّئيسيّة لمدوّنتنا، والّتي ستعرض التّدوينات الّتي أضفناها لتوّنا.
-
كما هو عليه الحال كل بضع عقود، تطفو إلى السطح لغة برمجة ما ويعدنا المتعصبون لها بأنها ستفعل لنا كل شيء، بدءًا من تطبيقات الحاسوب مرورًا بالهواتف الذكية وليس انتهاءً بالتعامل مع الجمادات من حولنا بطرق رائعة كالتحكم بطائرة بلا طيّار باستخدام قبضة Xbox 360! لم يكن هذا الحماس يومًا أشدّ منه مع JavaScript، والأمر يعود لعدّة أسباب: كونها تعمل في المتصفح جعلها عابرة للمنصات، فكل حاسوب وكلّ هاتف ذكي (أو حتى متوسّط الذكاء!) اليوم يأتي مزوّدًا بمتصفّح قادر على تشغيل JavaScript،وبما أنّها لغة الويب الوحيدة التي يمكن استعمالها لبرمجة المواقع من جهة المتصفّح، فلك أن تتخيّل عدد مطوّري الويب الذي يتقنونها حول العالم!كذلك كون JavaScript بطبيعتها لغة فائقة المرونة لدرجة أن كل شيء فيها هو في الحقيقة كائن Object حتى الدّوال (functions)! ولا يوجد شيء اسمه أصناف (classes) بالمعنى التّقليديّ، وإنما توجد وراثة أنموذجية (Prototypal inheritance) فكل كائن يستطيع أن يرث أي كائن آخر، ولا أنواع محدّدة للكائنات، فما تفرضه في البداية كسلسلة نصيّة يمكنك أن تغيّره فيما بعد ليصبح رقمًا، وبإمكانك توسعة الأنماط البدئية. هذه المرونة التي يألفها من يبتدئ البرمجة بـJavaScript تجعله يُصاب بصدمة عندما ينتقل إلى لغة أخرى تفرض عليه قيودًا في التصريح عن الأنواع ووراثة الأصناف...الأمر الثالث الذي يجعل JavaScript متفوّقة هو التحسّن الممتاز في أدائها الذي لا يبدو أنه سيتوقّف عند حدّ ما قريبًا، منذ بضع سنوات عندما ظهر Google Chrome مع محرّك JavaScript الجديد V8 والذي اعتمد على JIT compilation بدأت ثورة في عالم التطوير للويب جعلت JavaScript موضع اهتمام وأخذ المطوّرون ينظرون في إمكانيّة استعمالها في تطوير "تطبيقات ويب" بدل "مواقع ويب"، ثم توسّع الأمر مع ظهور Node.js التي قامت على محرّك V8 ذاته لتصبح JavaScript مواطنًا من الدّرجة الأولى على الخواديم مثلها مثل Ruby on Rails وPHP. الأمور ليست ورديّة تمامًا لكنك تستطيع استيعاب سرعة التطوّر الذي تشهده JavaScript وخاصّة أنّه أصبح لدينا أنظمة تشغيل ليست سوى متصفّح ويب في حقيقتها (Chrome OS وFirefox OS). بإمكانك استضافة تطبيقات Node.js مجّانًا على منصّة Heroku أو OpenShift من RedHat مثلها مثل تطبيقات Java وPython وRuby.باختصار، تحوّلت JavaScript إلى لغة عامّة الأغراض (general-purpose) بعد أن كانت تستخدم بشكل بسيط لإضفاء القليل من التأثيرات السخيفة (Blink! Blink!) على صفحات الويب. الآن أريد أن أوضّح شيئًا، في الحقيقة أنا لست مُبرمجًا مُختصًّا، وJavaScript هي اللغة الوحيدة التي أزعم أنّني متوسّط إلى خبير بها، ومنذ عام أو أكثر لم أكتب تقريبًا أي شيء بلغة أخرى (بالطّبع CoffeeScript لا تُعتبر لغة مستقلّة، هي فقط لغة تُحوّل إلى JavaScript ومهمّتها تبسيط الكتابة)، مع أنّني بدأت تعلّم البرمجة مع PHP، إلا أنّني كرهت كل إشارات الدولار تلك ($variable) وعدم انسجام الواجهات البرمجيّة فيها. لا شيء يمنعك من استخدام PHP، وفي الحقيقة إطار العمل Laravel ممتاز ومنسّق بشكل جيّد، لكنّ ما يعيبها هو أنها تحاول أن تفعل كلّ شيء من الوصول لنظام الملفّات إلى دوال للتعامل مع مُعاملات طلبات HTTP إلخ... وهذا بالضّبط ما تحاول بيئات البرمجة الجديدة أن تتجنّبه، ففي Node.js، وعلى الرّغم من أنّه باستطاعتك أن تصل إلى نظام الملفّات وأن تُنشئ خادمًا يستمع إلى الطلبات على أحد المنافذ؛ إلّا أنّ كلّ شيء مُنظّم في وحدات (modules) مستقلّة وعليك أن تُصرح علانيًّة برغبتك باستعمال وحدة نظام الملفّات مثلاً، وكذلك الأمر بالنسبة للوحدات التي يكتبها مبرمجون آخرون. نظام الوحدات هذا والتصريح عنها ضمن ملفّ وعدم تلويث نطاق الأسماء العامّ (Global scope) هي بعضٌ من الأشياء التي نفّذها مُطوّرو Node.js على وجه صحيح، وأزعم أنه واحد من الأشياء التي جعلت JavaScript تُؤخذ على محمل الجدّ. هناك الكثير من المحاولات لتقليد هذا النظام بعد أن أثبت تفوّقه، انظر مثلاً إلى Composer بالنسبة لـPHP، وPip مع virtualenv في Python؛ لكنّ محاولة إدخال هذه الأنظمة على لغات ناضجة لا تبدو موفّقة كثيرًا، وأما في لغة Go فتعتبر الحُزم شيئًا من أساس اللغة. أيًّا يكن، لقد وصلنا إلى مرحلة يمكن بها إنجاز أيّة تطبيق بأيّة لغة، ويبقى الفارق هو التنظيم والسرعة والأمان وأنماط التّصميم المُتّبعة، وأهمّ من ذلك كلّه المجتمع الّذي يوفّر الدّعم والمساعدة للمبتدئين (في النّقطة الأخيرة لا شكّ أن JavaScript متفوّقة على كلّ اللّغات). لكن دعونا لا نُهين قدرتنا العقلية ونتجاهل وجود لغات برمجة أخرى فقط لأنّنا ألِفنا لغة برمجة واحدة، مهما كانت محبوبة! هناك أشياء في JavaScript لا يمكن التّغاضي عنها ولا يُمكن في أحسن الأحوال أن نعتبرها مزايا: فهي أوّلاً بطيئة رغم تحسّن أدائها بأضعاف ما كانت عليه منذ سنوات، وما تزال أبطأ بكثير من لغات أخرى. هناك من يجادل (وأنا أؤيّد هذا الرأي) أن السّرعة ليست كلّ شيء، فيكفي لتطبيقات الويب أن تكون سريعة بما يكفي، وليس عليها أن تكون خارقة السرعة، الفارق بين كتابة تطبيق بسيط يؤدي مهمّة محدّدة بـJavaScript وتوزيعه ليعمل على كلّ منصّات الهواتف الذكية أمر يستحق التضحية بالقليل (والقليل فقط، أي إلى حدّ معقول) من السّرعة في مقابل كتابة تطبيق منفصل بـJava (لن تتخيّل عدد السّطور المُرعب الذي تحتاجه!) وآخر بـObjective C (أو Swift) وآخر بلغة ما لـWindows Phone (إن كان هناك من يُطوّر لهذا النظام! ونعم أنا جاهل به لدرجة أنّني لا أعرف شيئًا عن اللّغة التي تُستخدم لتطوير تطبيقاته!). لكن بعد تجاوز هذا الحد المعقول من التضحية، يجدر بك أن تُعيد النظر في صلاحية هذه اللّغة إذا ما أردت تطبيقًا ينفّذ مهمّة تتطلّب سرعة فائقة، دعك من أنّ هناك تطبيقات تحتاج إلى التعامل مع النظام بطريقة لا توفّرها بيئة تطبيقات الويب على هذه الأجهزة.وثانيًا JavaScript هي عالم من الفوضى إن تغاضينا عن التنظيم الممتاز في Node.js ونظرنا إلى بيئة المتصفّحات... كم مرّة عانى مطوّرو الويب من عدم التوافق... المتصفّح الفلاني يوفّر الميزة الفلانية... رائع! هذا بالضبط ما أحتاجه! لكن للأسف المتصفح الآخر لا يوفّرها، وستحتاج إلى مكتبة بديلة (polyfill) لسدّ هذا الفراغ، حسنًا سأضيف هذا الـpolyfill وسيمكنني التطوير لكل المتصفّحات... نعم، باستثناء أنّ هذا الـpolyfill يسدّ الفراغ بنسخة قديمة من معيار هذه الميزة التي تحتاجها - فكما تعرف أعضاء منظّمة W3C لا يتوقّفون عن تغيير الواجهات البرمجيّة للأشياء التجريبية في المتصفّحات... ألم نُحذّرك من استخدام هذه الميزة غير المُستقرّة؟ كان يجدر بك أن تبحث عن حلّ بديل!... وهذا هو بالضّبط السّبب الذي يجعلني أكره التطوير للواجهات (front-end development) ولهذا قرّرت الاعتزال في عالم Node.js والتطوير للنهاية الخلفيّة (backend)! لا شيء من أحلامك الورديّة يتحقّق بسهولة في عالم المتصفّحات! أعرف أن الأمور في تحسّن دائم، وهناك الكثير من الأشياء الرائعة القادمة... مثل applicationCache وindexedDb وObject.observe وWeb Components وHTML Imports... لكنّ المشكلة أنها جميعها ليست مستقرّة أو غير مُتبنّاة في كلّ المتصفّحات بعد؛ ويبدو أن هذه الفوضى لن تنتهي يومًا، ولا حلّ لها سوى المزيد من فوضى المكتبات البديلة، هل قلت يومًا إنّ الويب هو مستقبل التطوير الموحّد لكلّ المنصّات؟ لقد كنت ساذجًا!هناك الكثير من عيوب التصميم في JavaScript، بعضها يعود لكونها لغة صُمّمت لإنجاز مهام بسيطة وعلى عجَلَ (في الحقيقة Brendan Eich صمّمها خلال 10 أيام لمتصفّح Netscape)، فمثلاً لا يُمكنك إنشاء خيوط (threads) لأنّها تعمل ضمن خيط واحد (single-threaded)، هناك الكثير من الحلول الالتفافيّة (workarounds) في Node.js والمتصفّحات تعدنا بحل محدود الفعاليّة (Web Workers) لكنّ إيّاك أن تُخبر مُبرمج Java بأنّه لا يُمكنك إنشاء threads في JavaScript ثمّ تدعي أنّها لغة قويّة! أيضًا لا تستغرب إن قضيت ساعات تشرح لمطوّري اللّغات الأخرى عن حلقة الأحداث (event loop) وكيف تعمل ولماذا عليك تمرير استدعاءات راجعة (callbacks) عندما تُنفّذ طلبات XMLHttpRequest وما هو جحيم الاستدعاءات (callback hell) ولماذا ظهرت بدائل عنها مثل الوعود (Promise) التي تحوّل جحيم الاستدعاءات إلى جحيم then!asynctask1(function(err1, data1) { if (err1) { throw err1; return; } asynctask2(data1, function(err2, data2) { if (err2) { throw err2; return; } asynctask3(data2, function(err3, data3) { // ... WELCOME TO JAVASCRIPT! }) }) });أخيرًا أرجوك لا تنسى بديهيّة أن JavaScript بحدّ ذاتها لغة تُفسّر بمحرّك مكتوب بـC++ أو لغة أخرى أعرق وأقوى أداءً، النّواة Linux مكتوبة بخليط من C وC++ وبرامج تشغيل الأجهزة (drivers) غالبًا تُكتب بـAssembly. نعم، صدّقني JavaScript ليست اللّغة الوحيدة ضمن المجموعة الشّمسيّة!ضحكت كثيرًا عندما شاهدت هذه الصورة منذ بضعة أسابيع، التي تُعبّر بالضّبط عن موضوع هذه التدوينة: تسخر هذه الصّورة بشدّة من تعصّب بعض مبرمجي JavaScript الذي يدعوهم إلى الظنّ بأنّ على كل تطبيق جديد في الكرة الأرضية أن يستخدم JavaScript من اليوم فصاعدًا، وأنّ لغات أخرى ستصبح طيّ النسيان ولا يستخدمها إلّا المبرمجون القدامى الذين عفى عليهم الدهر؛ الأمر ليس مقتصرًا على متعصّبي JavaScript وحدهم، فلكلّ لغة برمجة أنصارها ومتعصّبوها، لكن JavaScript بالذّات هي أكثر اللغات التي تترافق بهذه الظاهرة، للأسباب التي ذكرتها سابقًا. ما خُلاصة هذا الحديث؟ما أريد قوله من هذه التدوينة السّريعة هو أن أنصح المتعصّبين للغة برمجية أيًّا كانت أن يتوقّفوا عن إهانة قدراتهم العقليّة على التّعلّم وتقبّل الكتابة بلغة أخرى يرون أنّها للفاشلين فقط أو للقادمين من العصر الحجري... لم أتعلّم هذا إلا بالطّريقة الصّعبة، وأعتقد أنّ السّبب الّذي جعلني أتقبّل هذه الحقيقة هو كوني غير مختصّ، تعلّمي للبرمجة غير مرهون بعملٍ أو بربح مادّيٍّ، أنا فقط أُبرمج على سبيل التّسلية، وكلّ مشاريعي التي كتبتها (هذه المدوّنة بنيتها من الصّفر، وتطبيق aQuran، وتطبيق جديد أكتبه لتوليد خلاصات RSS...) كلّها كانت مجرّد تجربة ومحاولة لاستكشاف أنماط التصميم (design patterns) ومفاهيم برمجيّة أخرى. نعم تعلّمت الكثير عن البرمجة عمومًا من خلال JavaScript، لكنّها لن تكون اللّغة الوحيدة التي أكتب بها لبقيّة حياتي بالطّبع! يبدو أن هدفي التالي سيكون لغة Go حديثة العهد. البرمجة أوسع من صياغة اللّغة (syntax) لذا فلا يعتبر انتقالي لتعلّم لغة أخرى خسارة، والكثير من المفاهيم البرمجية كالبرمجة المُقادة بالاختبارات (test-driven development) والتّعامل مع الاستثناءات (exception handling) ومكوّنات اللغة كالأصناف (classes) والواجهات (interfaces) والدّوالّ (functions) هي أشياء توجد بعضها أو كلّها في كلّ اللّغات.
-
Harmony هو الاسم الرمزي لـ ECMAScript 6 وهي اللغة القياسية التي تقوم عليها JavaScript، والإصدار الجديد يأتي بميزات جديدة تتناول العديد من جوانب اللغة بما فيها الصياغة (syntax) وأسلوب البناء وأنواع جديدة من المكونات المدمجة في اللغة. في هذا المقال نتعرف على بعض من المميزات التي ستجعل كتابة شيفرة جافاسكربت أكثر اختصاراً وفعالية. متغيرات نطاقها القطعة البرمجية (Block-scoped Variables)في الإصدار الحالي من JavaScript، تُعامل كل المتغيرات المفروضة ضمن دالة (function) على أنها تابعة لهذه الدالة (Function-scoped) أي يمكن الوصول إليها من أي موقع ضمن هذه الدالة، حتى وإن كانت هذه المتغيرات قد فُرضِت ضمن قطعة برمجية فرعية ضمن هذه الدالة (كحلقة for أو جملة شرطية if)، وهذا يخالف ما تتبناه بعض من أشهر لغات البرمجة، وقد يسبب بعض الارتباك لمن لم يعتد عليه. لنوضح أكثر في هذا المثال: var numbers = [1, 2, 3]; var doubles = []; for (var i = 0; i < numbers.length; i++) { var num = numbers[i]; doubles[i] = function() { console.log(num * 2); } } for (var j = 0; j < doubles.length; j++) { doubles[j](); }عند تنفيذ هذا المثال، سنحصل على الرقم 6 ثلاث مرات، وهو أمر غير متوقع ما لم نكن على معرفة بطبيعة مجالات JavaScript، ولو طبق ما يشبه هذا المثال في لغة أخرى، لحصلنا على النتيجة 2 ثم 4 ثم 6، وهو ما يبدو النتيجة المنطقية لشيفرة كهذه. ما الذي يحدث هنا؟ يتوقع المبرمج أن المتغير num محصور ضمن حلقة for وعليه فإن الدالة التي ندخلها في المصفوفة doubles ستعطي عند استدعائها القيمة التي ورثتها عن مجال حلقة for إلا أن الحقيقة هي أن المتغير num يتبع للمجال العام، لأن حلقة for لا تُنشئ مجالًا فرعيًّا وعليه فإن القيمة العامة num تتغير ضمن حلقة for من 2 إلى 4 إلى 6 وعند استدعاء أي دالة ضمن المصفوفة doubles فإنها ستعيد إلينا القيمة العامة num، وبما أن الاستدعاء يحدث بعد إسناد آخر قيمة للمتغير num، فإن قيمته في أي لحظة بعد انتهاء الحلقة الأولى ستكون آخر قيمة أسندت إليه ضمن هذه الحلقة، وهي القيمة 6. يعطينا الإصدار القادم طريقة لحل هذا الارتباك باستخدام الكلمة المفتاحية let بدلاً عن var، وهي تقوم بخلق مجال ضمن القطعة البرمجية التي تُستخدم فيها، بمعنى آخر: ستكون let هي بديلنا عن var من الآن فصاعدًا، لأنها ببساطة تعطينا النتائج البديهية التي نتوقعها. لنُعِد كتابة المثال السابق باستبدال var num بـlet num: var numbers = [1, 2, 3]; var doubles = []; for (var i = 0; i < numbers.length; i++) { let num = numbers[i]; doubles[i] = function() { console.log(num * 2); } } for (var j = 0; j < doubles.length; j++) { doubles[j](); }عند تطبيق هذا المثال (يمكنك تطبيقه في Firefox وChrome لأن كلا المتصفحين شرعا في دعم let) سنحصل على النتيجة البديهية 2 ثم 4 ثم 6. بالطبع بإمكاننا تحسين الشيفرة باعتماد let عند التصريح عن كل المتغيرات السابقة، وهو الأمر الذي يجب أن تعتاد فعله من اليوم! شيفرة أقصر وأسهل للقراءةلعل أكثر ما أُحبّه في JavaScript مرونتها الفائقة، وبالذات القدرة على إمرار دوال مجهولة (Anonymous Functions) لدوال أخرى، الأمر الذي يسمح لنا بكتابة شيفرة ما كان من الممكن كتابتها بلغات أخرى إلا بضعفي عدد الأسطر وربما أكثر. لاحظ هذا المثال: var people = ['Ahmed', 'Samer', 'Khaled']; var greetings = people.map(function(person) { return 'Hello ' + person + '!'; }); console.log(greetings); // ['Hello Ahmed!', 'Hello Samer!', 'Hello Khaled!'];لو أردنا تنفيذ هذه المهمة في لغة أخرى، فلربما احتجنا إلى حلقة for لنمرّ من خلالها على كل عنصر ضمن المصفوفة ثم إدخال العبارات الجديدة ضمن مصفوفة أخرى، وهذا يعني أن مهمة يمكن كتابتها بسطرين في JavaScript قد تتطلب 5 سطور في لغة أخرى. لو لم تمتلك JavaScript القدرة على إمرار الدالة المجهولة function(person) {...} أعلاه، لفقدت جزءًا كبيرة من مرونتها. لكن الإصدار القادم من JavaScript تذهب أبعد من ذلك، وتختصر علينا كتابة الكثير من النص البرمجي. لُنعد كتابة المثال السابق: let people = ['Ahmed', 'Samer', 'Khaled']; let greetings = people.map(person => 'Hello ' + person + '!'); console.log(greetings); // ['Hello Ahmed!', 'Hello Samer!', 'Hello Khaled!'];في هذا المثال استخدمنا ما اصطلح على تسميته دوال الأسهم (Arrow Functions)، وهي طريقة أكثر اختصارًا لكتابة الدوال المجهولة، لن تحتاج لكتابة return، فهي ستضاف تلقائيًا عند التنفيذ. من الآن فصاعداً اعتمد دوال الأسهم عندما تريد تنفيذ دالة مجهولة بسيطة بسطر واحد. بمناسبة الحديث عن الشيفرة المختصرة... ما رأيكم لو جعلنا الشيفرة أعلاه أكثر اختصارًا؟! let people = ['Ahmed', 'Samer', 'Khaled']; let greetings = ['Hello ' + person + '!' for (person of people)]; console.log(greetings); // ['Hello Ahmed!', 'Hello Samer!', 'Hello Khaled!'];قد تبدو الصياغة غريبة بعض الشيء، لكنها تتيح لنا فهم النص بسهولة أكبر، وتغنينا عن الحاجة لدالة مجهولة (الأمر الذي قد يؤثر على الأداء، وإن كان بأجزاء من الثواني). الصياغة التي استخدمناها أعلاه تُسمى Array Comprehensions، وإن كنت قادرًا على ترجمتها إلى العربية بطريقة واضحة، فلا تبخل بها علينا! لكن... ألا ترون أنه يمكن تحسين هذه الشيفرة قليلاً؟ let people = ['Ahmed', 'Samer', 'Khaled']; let greetings = [`Hello ${ person }!` for (person of people)]; console.log(greetings); // ['Hello Ahmed!', 'Hello Samer!', 'Hello Khaled!']; هنا استبدلنا إشارات الاقتباس (' أو ") بالإشارة ` الأمر الذي أتاح لنا إحاطة المتغير person بقوسين معكوفين مسبوقين بإشارة $، وهذه الصياغة تدعى "السلاسل النصية المقولبة" أو Template Strings، والتي تسمح -بالإضافة إلى القولبة- بالعديد من الأشياء الرائعة، كالعبارات على عدة أسطر: let multilineString = `I am a multiline string`; console.log(multilineString); // I am // a multiline // string للأسف لن تعمل الشفرة السابقة في أي من المتصفحات الحالية، لأن السلاسل النصية المقولبة ما تزال غير معتمدة ضمن أي منها. تحديث: بدأ Firefox Nightly باعتمادها. من المميزات الجديدة كذلك إمكانية اختصار بناء الكائنات ذات الخصائص بالشكل التالي: حاليًا، نقوم بكتابة شيفرة مثل هذه: var createPerson = function(name, age, location) { return { name: name, age: age, location: location, greet: function() { console.log('Hello, I am ' + name + ' from ' + location + '. I am ' + age + '.'); } } }; var fawwaz = createPerson('Fawwaz', 21, 'Syria'); console.log(fawwaz.name); // 'Fawwaz' fawwaz.greet(); // "Hello, I am Fawwaz from Syria. I am 21." في الإصدار القادم، سيكون بالإمكان كتابة الشيفرة كالتالي:let createPerson = function(name, age, location) { return { name, age, location, greet() { console.log('Hello, I am ' + name + ' from ' + location + '. I am ' + age + '.'); } } }; let fawwaz = createPerson('Fawwaz', 21, 'Syria'); console.log(fawwaz.name); // 'Fawwaz' fawwaz.greet(); // "Hello, I am Fawwaz from Syria. I am 21."بما أن اسم المُعامل (parameter) يماثل اسم الخاصة (property)، فإن هذا يتم تفسيره على أن قيمة الخاصة توافق قيمة المعامل، بمعنى: name: name، بالإضافة إلى كتابة greet() {...} بدل greet: function() {...}. كذلك سيكون بإمكاننا تحسين هذا النص أكثر من ذلك باستخدام الأصناف (Classes)، نعم! سيكون لدينا أصناف أخيرًا! (سنستعرضها لاحقاً) الثوابت (Constants)سيداتي وسادتي... رحبوا بالثوابت... نعم إنها أخيرًا متوفرة في JavaScript، إحدى المكونات الأساسية لأي لغة برمجية التي لم تكن متوفرة في JavaScript، أصبحت الآن متوفرة. والآن نأتي للسؤال البديهي: لماذا أحتاج للثوابت؟ أليس بإمكاني التصريح عن متغير دون أن أغير قيمته بعد إعطاءه القيمة الأولية؟ نعم بالطبع بإمكانك ذلك، لكن هذا لا يعني بالضرورة أن المستخدم أو نصاً برمجيًا من طرف ثالث ليس بإمكانه تغيير قيمة هذا المتغير في سياق التنفيذ، وطالما أن المتغير "متغير" بطبيعته، فإننا دومًا بحاجة إلى شيء من أصل اللغة يحمينا من تغييره خطأ. عند التصريح عن ثابت فإننا نعطيه قيمة أولية ثم ستتولى الآلة البرمجية لـJavaScript حماية هذا الثابت من التغيير، وسُيرمى خطأ عند محاولة إسناد قيمة جديدة لهذا الثابت. const myConstant = 'Never change this!'; myConstant = 'Trying to change your constant'; // TypeError: redeclaration of const myConstant console.log(myConstant); // "Never change this!" المُعاملات الافتراضية (Default Parameters)غياب دعم المُعاملات الافتراضية في JavaScript واحد من أكثر الأشياء التي تزعجني، لأنها تجبرني على كتابة شيفرة مثل هذه: function SayHello (user) { if (typeof user == 'undefined') { user = 'User'; } console.log('Hello ' + user); } console.log(SayHello('Fawwaz')); // Hello Fawwaz! console.log(SayHello()); // Hello User!لو كان عندي 3 متغيرات غير إجبارية، فهذا يعني أنني سأحتاج 3 جمل شرطية، الأمر الذي يتطلب الكثير من الكتابة المُملة. بفضل الإصدار القادم من JavaScript، سيكون بالإمكان كتابة شيفرة أبسط بكثير: function SayHello (user='User') { console.log('Hello ' + user); } SayHello('Fawwaz'); // Hello Fawwaz! SayHello(); // Hello User! الوعود (Promises)الوعود هي الحل الذي تأتينا به JavaScript لحل مشكلة هرم الموت (Pyramid of Death) الذي نواجهه عند تنفيذ مهمات غير متزامنة تعتمد إحداها على الأخرى: function getFullPost(url, callback) { var getAuthor = function(post, callback) { $.ajax({ method: 'GET', url: '/author/' + post.author_id }, callback); }; var getRelatedPosts = function(post, callback) { $.ajax({ method: 'GET', url: '/related/' + post.id }, callback); }; $.ajax({ method: 'GET', url: url }, function(post) { getAuthor(post, function(res) { post.author = res.data.author; getRelatedPosts(post, function(res) { post.releated = res.data.releated; callback(post); }); }); }); } هل تلاحظ أن الشيفرة تتجه نحو اليمين؟ لو أردنا تنفيذ هذه المهمات غير المتزامنة واحدة بعد الأخرى وكان عددها 10 مثلًا فستصبح الشيفرة شديدة التعقيد، كما أن هذه الطريقة ليست بديهية، ولا يمكن لك أن تفهم ماذا تفعل هذه الدالة المجهولة (المعامل الثاني في كل دالة) ما لم تألفها. ماذا لو أمكننا كتابة هذه الشيفرة بصورة أفضل؟ function getFullPost(url) { var post = { }; var getPost = function(url) { return $http.get(url); }; var getAuthor = function(post) { return $http.get('/author/' + post.author_id).then(function(res) { post.author = res.data.author; }); }; var getRelatedPosts = function(post) { return $http.get('/related/' + post.id).then(function(res) { post.related = res.data.related; }); }; return getPost().then(getAuthor).then(getRelatedPosts).catch(function(err) { console.log('We got an error:', err); }); } في الجزء القادم سنتعرّف على المكوّنات الجديدة الأكثر إثارة، كالمولّدات التي ستجعلنا نغير من طريقة تعاملنا مع البيانات اللامتزامنة كليًّا!
-
ما هو AngularJS؟AngularJS هو إطار عمل JavaScript لطرف العميل يتبع بنية Model-View-Controller/Model-View-View-Model، ويعتبر اليوم مُهمًا لبناء تطبيقات ويب وحيدة الصفحة (SAP) أو حتى المواقع العادية. يُعتبر Angular JS قفزة كبيرة نحو مستقبل HTML وما يجلبه الإصدار الخامس منها (مع التطورات على صعيد JavaScript بالطبع!)، ويبعث الحياة من جديد في تعاملنا مع الويب الحديث. هذا المقال جولة شاملة في Angular JS مستخلصة من تجاربي ونصائح وممارسات تعلمتها خلال استخدامي. المُصطلحاتليس عليك أن تبذل جهداً كبيراً لتعلم Angular، وأهم ما يجب معرفته هو معاني المُصطلحات وتبني نمط MVC، وMVC هو اختصار لـModel-View-Controller، أي نموذج-طريقة عرض-مُتحكِّم. فيما يلي بعض المصطلحات والواجهات البرمجية الأساسية التي تزوّدنا بها Angular. MVCربما سمعت بهذا الاختصار من قبل، وهو مستخدم في لغات برمجة عديدة كوسيلة لبناء هيكل التطبيقات أو البرامج. وهاك تلخيص سريع لمعناه: النموذج (Model): بنية البيانات التي تقوم عليها أجزاء التطبيقات، غالبًا ما تُمثل بصيغة JSON. يفضل أن تكون لديك معرفة مسبقة بـJSON قبل تعلم Angular، لأنها ضرورية للتواصل بين الخادوم وطريقة العرض (سنأتي على شرحها في النقطة التالية). على سبيل المثال، لنفترض أنه لدينا مجموعة من المستخدمين، يمكن تمثيل بيانات تعريفهم كما يلي:{ "users" : [{ "name": "أحمد", "id": "82047392" },{ "name": "سامر", "id": "65198013" }] }عادة ما تُجلب هذه المعلومات من خادوم بطلب XMLHttpRequest، ويقابله في jQuery الإجراء $.ajax، وفي Angular الكائن $http. وقد تكون هذه المعلومات مكتوبة ضمن النص البرمجي أثناء تفسير الصفحة (من قاعدة بيانات أو مخزن بيانات). بعد ذلك يكون بإمكانك تعديل هذه المعلومات وإعادة إرسالها. طريقة العرض (View): وهو أمر سهل التفسير، فهي ببساطة المُخرج النهائي أو صفحة HTML التي تعرض البيانات (النموذج) على المستخدم مثلاً. باستخدام إطار عمل MVC، تُجلب البيانات من النموذج وتُعرض المعلومات المناسبة في صفحة HTML.المُتحكِّم (Controller): وله من اسمه نصيب، فهو يتحكم بالأشياء، ولكن أية أشياء؟ البيانات. المُتحكمات هي الطريقة التي تصل من خلالها بين الخادوم وطريقة العرض، فتسمح بتحديث البيانات سريعًا من خلال التواصل مع كلا الخادوم والعميل.إعداد مشروع Angular JS (الأساسيات)يجب أولاً تهيئة الأساسيات التي يقوم عليها مشروعنا. يجب أن نبدأ بتطبيق (ng-app) الذي يُعرِّف التطبيق (وng هي بادئة تعني Angular وتسبق عادة كل مكونات Angular JS)، ثم متحكم (Controller) ليتواصل مع طريقة العرض، ثم ربط DOM ولا ننسى تضمين Angular بالطبع. إليك الأساسيات: نص HTML مع تصريحات ng-*:<div ng-app="myApp"> <div ng-controller="MainCtrl"> <!-- محتويات المتحكم --> </div> </div>وحدة Angular مع متحكم:var myApp = angular.module('myApp', []); myApp.controller('MainCtrl', ['$scope', function ($scope) { // أوامر المتحكم }]);قبل أن نستبق الأمور، نحتاج لإنشاء وحدة Angular (أو Angular module)، التي ستتضمن كل النص البرمجي المُتعلق بالمشروع. هناك أكثر من طريقة للتصريح عن الوحدات، إحداها سَلسَلة كل النص البرمجي معًا (لا أفضل هذه الطريقة): angular.module('myApp', []) .controller('MainCtrl', ['$scope', function ($scope) {...}]) .controller('NavCtrl', ['$scope', function ($scope) {...}]) .controller('UserCtrl', ['$scope', function ($scope) {...}]);ولكن الطريقة التي أفضلها، والتي أثبتت أنها الأفضل لكل مشاريع Angular التي صمّمتها هي تعريف الوحدة العامة بشكل منفصل. الطريقة التي تعتمد على تسلسل التصريحات قد تجعلك تنسى إغلاق بعض الأقواس وتجعل قراءة النص البرمجي وتصحيحه أكثر تعقيدًا. لذا أُفضّل هذا الأسلوب: var myApp = angular.module('myApp', []); myApp.controller('MainCtrl', ['$scope', function ($scope) {...}]); myApp.controller('NavCtrl', ['$scope', function ($scope) {...}]); myApp.controller('UserCtrl', ['$scope', function ($scope) {...}]);بهذه الطريقة أُقسّم النص البرمجي على عدة ملفات، وفي كل ملف أربط مكوّناً من المكونات مع فضاء الأسماء myApp فيصبح تلقائيًا جزءًا من تطبيقي. نعم، الأمر كما فهمت، أفضل أن أنشئ ملفًا مستقلًا لكل متحكم ومُرشِد (directive) ومعمل (factory) وأي شيء آخر (ستشكرني على هذا). فيما بعد يمكنك دمجها معًا وتقليصها لتصبح ملفًا واحدًا (مستخدمًا مُدير مهام مثل Grunt أو Gulp) فتدفعَه إلى DOM. المُتحكّمات (Controllers)أصبحت تعرف الآن مفهوم MVC وتعلمت طريقة إعداد مشروع جديد، فلنطّلع الآن على الكيفية التي يُطبِّق فيها Angular JS العمل بالمتحكّمات. بناء على المثال السابق، بإمكاننا الآن أن نخطو خطوة بسيطة نحو عرض بعض البيانات ضمن طريقة العرض مستخدمين مُتحكّمًا. يستخدم Angular تركيب " handlebars" لقولبة HTML. ببساطة يعني هذا أنه بإمكان المتحكمات أن تعرض البيانات في صفحة HTML بأن تستبدل كل عبارة فيها مكتوبة ضمن الأقواس المزدوجة هكذا: {{ data }} قيمة يُعيّنها المُتحكم. في الحالة المثالية يجب أن لا تحوي صفحة HTML نصًا حقيقيًا أو قيمًا مُدرجة مسبقًا، ويجب أن تترك هذه المهمة لمتحكمات Angular. فيما يلي مثال يُبيّن كيف يمكن عرض نص أو سلسلة حروف String بسيطة ضمن الصفحة: <div ng-app="myApp"> <div ng-controller="MainCtrl"> {{ text }} </div> </div>في ملف JavaScript: var myApp = angular.module('myApp', []); myApp.controller('MainCtrl', ['$scope', function ($scope) { $scope.text = 'مرحباً بمعجبي Angular!'; }]);والناتج النهائي: رابط المثال أهم مفهوم هنا هو مفهوم النطاق ($scope) والذي ستربطه بكل الوظائف ضمن متُحكّم مُعيّن. يُشير $scope إلى العُنصر أو المنطقة الحالية في DOM (فهو لا يُساوي this ضمن النص البرمجي) وبهذا يخلق نطاقًا يُحيط بكل البيانات والوظائف ضمن العناصر (DOM elements)، ويعزلها عن العناصر الأخرى، فيبدو وكأنه ينقل مجالات JavaScript العامة/الخاصة إلى DOM، وهذا شيء رائع. قد يبدو مفهوم النطاق مخيفًا للوهلة الأولى، لكنه طريقك الواصل بين الخادوم (أو حتى البيانات المحلية) من جهة وDOM من الجهة الأخرى. يعطيك هذا المثال فكرة عن الطريقة التي "تُدفع" بها البيانات إلى DOM. لنٌلقِ نظرة على بيانات حقيقية نفترض أننا جلبناها من خادوم لنعرض تفاصيل تسجيل دخول المستخدم، سنكتفي في هذه المرحلة باستخدام بيانات جاهزة، وسنتعلم كيفية جلبها من الخادوم على هيئة JSON لاحقًا. أولاً سنكتب شفرة JavaScript: var myApp = angular.module('myApp', []); myApp.controller('UserCtrl', ['$scope', function ($scope) { // لنجعل معلومات المستخدم ضمن عنصر فرعي $scope.user = {}; $scope.user.details = { "username": "Todd Motto", "id": "89101112" }; }]);ثم ننتقل إلى DOM لعرض هذه البيانات <div ng-app="myApp"> <div ng-controller="UserCtrl"> <p class="username">Welcome, {{ user.details.username }}</p> <p class="id">User ID: {{ user.details.id }}</p> </div> </div>النتيجة من المهمّ أن تتذكر أن المتحكمات تستخدم فقط للبيانات ولإنشاء وظائف تتواصل مع الخادوم وتجلب أو ترسل بيانات JSON. لا تستخدم المتحكمات لمعالجة DOM (كأن تنقل عنصرًا ضمن الصفحة أو تخفيه أو تظهره...)، فمعالجة DOM مهمة المُرشِدات (directives)، وهي ما سنشرحه لاحقًا، المهم أن تتذكر أن موضع jQuery وغيرها من المكتبات التي تتعامل مع DOM ليس ضمن المتحكّمات. تنويه: خلال اطلاعك على وثائق Angular الرسمية، ستلاحظ أن الأمثلة المقدمة تعتمد الأسلوب التالي لإنشاء المتحكمات: var myApp = angular.module('myApp', []); function MainCtrl ($scope) { //... };لا تفعل ذلك. هذا سيجعل كل الوظائف المُصرّحة تابعةً للنطاق العامّ (global scope) ولا يربطها بشكل جيد مع التطبيق. هذا يعني كذلك أن عمليات التقليص للنص البرمجي والتجارب ستكون أكثر صعوبة. لا تلوّث فضاء الأسماء العام، بل اجعل المتحكمات ضمن التطبيق. المُرشِدات (Directives)الُمرشد في أبسط صوره هو نص HTML مٌقولَب، يفضل أن يكون استخدامه متكررًا ضمن التطبيق. توفر المرشدات طريقة سهلة لإدخال أجزاء DOM ضمن التطبيق دون عناء. تعلم استخدام المرشدات ليس أمرًا سهلًا على الإطلاق، وإتقانها يتطلب جهدًا، ولكن الفقرات التالية ستضعك على الطريق الصحيح. إذن، ما فائدة المُرشدات؟ إنها مفيدة في عدة أمور، منها إنشاء عناصر DOM، مثل علامات التبويب (tabs) وقوائم التصفح - ففائدتها تعتمد على ما يفعله تطبيقك في الواجهة. لتسهيل الشرح، سأقول ببساطة: إن كنت استعملت ng-show وng-hide من قبل، فقد استعملت المرشدات (حتى وإن كان هذان لا يُدرجان أية عناصر DOM). على سبيل التمرين، سنُنشئ نوعًا خاصًّا من الأزرار ونُسميه customButton، يُدرج هذا العنصر بعض العناصر الفرعية التي لا نريد كتابتها في كل مرة. تتنوع طرق التصريح عن المرشدات في DOM، وهي مبينة في النص البرمجي التالي: <!-- 1: تصريح عن مُرشد كخاصّة (attribute) --> <a custom-button>انقرني</a> <!-- 2: كعنصر مخصص (custom elements) --> <custom-button>انقرني</custom-button> <!-- 3: كصنف (class) (للتوافق مع النسخ القديمة من IE) --> <a class="custom-button">انقرني</a> <!-- 4: كتعليق (comment) (ليس ملائماً لهذا التمرين) --> <!-- directive: custom-button --> أفضّل استخدام المرشدات كخواصّ (attributes)، أما العناصر المُخصصة (custom elements) فقادمة في النسخ المستقبلية من HTML باسم Web Components، يوفر Angular ما يشبهها، ولكنها قد تنطوي على بعض العيوب والعلل في المتصفحات القديمة. الآن نعرف كيف نصرح عن المرشدات ضمن الصفحة، سننتقل إلى إنشائها ضمن JavaScript. لاحظ أنني سأربطه مع فضاء الأسماء العام myApp؛ في صيغته الأبسط يُكتب المرشد كما يلي: myApp.directive('customButton', function () { return { link: function (scope, element, attrs) { // هنا اكتب التعليمات التي تعالج DOM أو تتعامل مع أحداثه } }; });عرّفنا المرشد باستخدام الدالة .directive()، ومررنا إليها اسم المرشد 'customButton'. عندما تكتب حرفًا كبيرًا بالإنجليزية في اسم المُرشد، فإنه ينبغي استخدام اسم المرشد ضمن DOM بصيغته التي يُفصل بها باستخدام الشرطة (-) بين الحروف الكبيرة (كما في المثال السابق: استخدمنا 'customElement' في JavaScript و"custom-button" في HTML). يُرجع المُرشد كائنًا (Object) له عدد من الخصائص. أهم ما يجب تعلّمه منها: restrict وreplace وtransclude وtemplate وtemplateUrl وأخيرًا link. لنُضف بعضها إلى شفرتنا البرمجية: myApp.directive('customButton', function () { return { restrict: 'A', replace: true, transclude: true, template: '<a href="" class="myawesomebutton" ng-transclude>' + '<i class="icon-ok-sign"></i>' + '</a>', link: function (scope, element, attrs) { // هنا اكتب التعليمات التي تعالج DOM أو تتعامل مع أحداثه } }; });النتيجة تأكد من فحص العنصر (من الأمر Inspect element في المُتصفح) لرؤية العناصر الجديدة التي أُدخلت في الصفحة. اعلم أن الرمز لم يظهر ضمن العنصر الجديد، ببساطة لأنني لم أُضمّن Font Awesome ضمن المشروع، ولكن يمكنك فهم كيف تعمل المرشدات. لنتعرف الآن ما تعنيه كل واحدة من خصائص المرشد السابقة الذكر: الخاصية restrict: تُقيّد هذه الخاصة كيفية استخدام المُرشد، كيف نريد أن نستخدمه؟ إن كنت تبني مشروعًا يتطلب دعم النسخ القديمة من IE، فعليك استخدامه كخاصّة (attribute) أو صنف (class). القيمة 'A' تعني حصر استخدام المرشد بالخواص (attributes) فقط. 'E' تعني Element و'C' صنف و'M' تعليق. القيمة الافتراضية هي 'EA' (أي عنصر وخاصة). الخاصية replace: تعني استبدال HTML العنصر المصرّح عن المُرشد ضمن الصفحة بالقالب (template) الذي يُحدد في الخاصة template (مشروحة أدناه). الخاصةtransclude: تسمح بنسخ المحتوى الأصلي للعنصر المُصرّح عن المُرشد في الصفحة ودمجه ضمن المرشد (عند التنفيذ، ستلاحظ أن العبارة "انقرني" انتقلت إلى المُرشد). الخاصية template: قالب (كذلك المستخدم في المثال) يُدخل إلى الصفحة. يفضّل استخدام القوالب الصغيرة فقط. تُعالج القوالب وتبنى من قبل Angular مما يسمح باستخدام صيغة handlebars ضمنها. الخاصة templateUrl: مشابهة للسابقة، ولكنها تُجلب من ملف أو من وسم <script> بدل كتابتها ضمن تعريف المُرشد. كل ما عليك هو تعيين مسار الملف الذي يحوي القالب. يكون هذا الخيار مناسبًا عندما تريد الاحتفاظ بالقوالب خارج النص البرمجي لملفات JavaScript: myApp.directive('customButton', function () { return { templateUrl: 'templates/customButton.html' // directive stuff... });وضمن الملف، نكتب: <!-- inside customButton.html --> <a href="" class="myawesomebutton" ng-transclude> <i class="icon-ok-sign"></i> </a>ملاحظة: لا يخضع اسم الملف إلى أية قاعدة، وليس من الضروري أن يوافق اسمَ المُرشد. عند استخدام الأسلوب السابق، سيحتفظ المتصفح بنسخة مُخبأة (cached) من ملف HTML، وهو أمر رائع، الخيار البديل هو استخدام قالب ضمن وسم <script> وهنا لا تُخبأ نسخة منه في المتصفح: <script type="text/ng-template" id="customButton.html"> <a href="" class="myawesomebutton" ng-transclude> <i class="icon-ok-sign"></i> </a> </script>هنا أخبرنا Angular بأن وسم <script> هذا هو قالب (ng-template) وأعطيناه المُعرف. سيبحث Angular عن القالب أو عن ملف html، فاستخدم ما تراه مناسبًا. شخصيًا، أفضّل إنشاء ملفات html لسهولة تنظيمها ولتحسين الأداء وإبقاء DOM نظيفًا، فقد يستخدم مشروعك مع الوقت عشرات المُرشدات، وترتيبها في ملفات مستقلة يجعل مراجعتها أسهل. الخدمات (Services)كثيرًا ما تثير الخدمات في Angular ارتباك المطورين، ومن خبرتي وأبحاثي، أعتقد أن الخدمات وُضعت كنمط وأسلوب للتصميم أكثر من اختلافها بالوظيفة التي تؤديها. بعد التنقيب في مصدر Angular، وجدت أنها تُعالج وتبنى باستخدام المُجمّع (compiler) ذاته، وكذلك فهي تقدم العديد من الوظائف المشابهة. أنصح باستخدام الخدمات لبناء الكائنات المُتفرِّدة (singletons)، واستخدام المعامل (Factories) لبناء وظائف أكثر تعقيدًا كالكائنات الحرفيّة (Object Literals). فيما يلي مثال لاستخدام خدمة توجد ناتج الضرب لعددين: myApp.service('Math', function () { this.multiply = function (x, y) { return x * y; }; });يمكنك بعد هذا استخدامها ضمن مُتحكم كما يلي: myApp.controller('MainCtrl', ['$scope', function ($scope) { var a = 12; var b = 24; // الناتج: 288 var result = Math.multiply(a, b); }]);نعم بالطبع إيجاد ناتج الضرب سهل ولا يحتاج خدمة، لكننا نستخدمه لإيصال الفكرة فحسب. عندما ننشئ خدمة (أو معملاً) نحتاج إلى إخبار Angular عن متطلبات هذه الخدمة، وهو ما يسمى "حقن المتطلبات Dependency Injection" - إن لم تُصرّح عن المتطلبات فلن يعمل المتحكم المعتمد على الخدمة، وسيحدث خطأ عند التجميع. ربما لاحظت الجزء function ($scope) ضمن التصريح عن المتحكم أعلاه، وهذا هو ببساطة حقن المتطلبات. ستُلاحظ أيضًا [$scope] قبل الجزء function ($scope)، وهو ما سأشرحه لاحقًا. فيما يلي طريقة استخدام حقن المتطلبات لإخبار Angular أنك تحتاج إلى الخدمة التي أنشأتها للتو: // مرر الخدمة Math myApp.controller('MainCtrl', ['$scope', 'Math', function ($scope, Math) { var a = 12; var b = 24; // يُعطي 288 var result = Math.multiply(a, b); }]);المعامل (Factories)إيضاح فكرة المعامل سهل إذا كنت قد استوعبت فكرة الخدمات، بإمكاننا إنشاء كائن حرفي (Object Literal) ضمن المعمل أو عبر طرق أكثر تعقيدًا: function ($http) { return { get: function(url) { return $http.get(url); }, post: function(url) { return $http.post(url); }, }; }]);هنا أنشأنا مُغلفات (wrappers) مخصصة لخدمة $http في Angular المسؤولة عن طلبات XHR. بعد حقن المتطلبات ضمن المتحكم يمكننا استخدام هذا المعمل بسهولة: myApp.controller('MainCtrl', ['$scope', 'Server', function ($scope, Server) { var jsonGet = 'http://myserver/getURL'; var jsonPost = 'http://myserver/postURL'; Server.get(jsonGet); Server.post(jsonPost); }]);إذا أرت طلب التحديثات من الخادوم، بإمكانك إنشاء دالة Server.poll أو إن كنت تستخدم مقبسًا (ٍSocket)، فربما سترغب في إنشاء دالة Server.socket وهكذا... المعامل تسمح لك بتنظيم نصك البرمجي ضمن وحدات يمكن إدراجها ضمن المتحكمات منعًا لتكرار النص البرمجي فيها والحاجة المتكررة لصيانته. المُرشّحاتتستخدم المرشحات مع مصفوفات (arrays) من البيانات وخارج الحلقات (loops). إن احتجت للمرور على عناصر من مصفوفة بيانات والحصول على بعض منها فقط، فأنت في المكان الصحيح، يمكنك أيضًا استخدام المرشحات لتصفية ما يكتبه المستخدم ضمن حقل إدخال <input> مثلًا. هناك عدة طرق لاستخدام المُرشحات: ضمن متحكم أو دالة مُعرفة. فيما يلي الطريقة الأخيرة: myApp.filter('reverse', function () { return function (input, uppercase) { var out = ''; for (var i = 0; i < input.length; i++) { out = input.charAt(i) + out; } if (uppercase) { out = out.toUpperCase(); } return out; } }); // Controller included to supply data myApp.controller('MainCtrl', ['$scope', function ($scope) { $scope.greeting = 'Todd Motto'; }]);وفي HTML: <div ng-app="myApp"> <div ng-controller="MainCtrl"> <p>No filter: {{ greeting }}</p> <p>Reverse: {{ greeting | reverse }}</p> </div> </div>رابط المثال وهنا نستخدم المُرشح ضمن حلقة ng-repeat: <ul> <li ng-repeat="number in myNumbers |filter:oddNumbers">{{ number }}</li> </ul>مثال عن مُرشح ضمن متحكم: myApp.controller('MainCtrl', ['$scope', function ($scope) { $scope.numbers = [10, 25, 35, 45, 60, 80, 100]; $scope.lowerBound = 42; // Does the Filters $scope.greaterThanNum = function (item) { return item > $scope.lowerBound; }; }]);واستخدامه حلقة ng-repeat: <li ng-repeat="number in numbers | filter:greaterThanNum"> {{ number }} </li>رابط المثال كان هذا القسم الأكبر مما تحتاج لمعرفته عن AngularJS وواجهاتها البرمجية، ومع أن ما تعلمناه كافٍ لبناء تطبيق Angular، ولكننا إلى الآن لم نسكتشف أغوراها بعد. ربط البيانات ثنائي الاتجاهعندما سمعت للمرة الأولى عن ربط البيانات ثنائي الاتجاه لم أفهم ما يعنيه. باختصار يمكن القول إنه حلقة متصلة من البيانات المُزامنة: حدّث النموذج (Model) لتُحدَّث طريقة العرض (View)، أو حدّث طريقة العرض ليُحدَّث النموذج (Model). هذا يعني أن البيانات تبقى محدثة دومًا دون عناء. إن ربطت نموذج ng-model مع حقل إدخال <input> وكتبت فيه، فهذا يُنشئ (أو يُحدِّث) نموذجاً في الوقت ذاته. فيما يلي نقوم بإنشاء حقل <input> ونربطه بنموذج نسميه myModel، يمكنني الآن استخدام صياغة handlebars لعكس هذا النموذج وما يطرأ عليه من تحديثات في طريقة العرض في الوقت ذاته: <div ng-app="myApp"> <div ng-controller="MainCtrl"> <input type="text" ng-model="myModel" placeholder="Start typing..." /> <p>My model data: {{ myModel }}</p> </div> </div> myApp.controller('MainCtrl', ['$scope', function ($scope) { // Capture the model data // and/or initialise it with an existing string $scope.myModel = ''; }]); النتيجة طلبات XHR/Ajax/$http وربط JSONنعرف الآن كيف نرسل بيانات بسيطة ضمن المجال ($scope)، ونعرف ما يكفي عن كيفية عمل النماذج وربط البيانات ثنائي الجانب، والآن حان الوقت لمحاكاة طلبات XHR حقيقية للخادوم. ليس هذا ضروريًا لمواقع الويب العادية، لكنه مناسب جدًا لجلب البيانات في تطبيقات الويب. عندما تطور تطبيقك على جهازك المحلي، فغالبًا ما ستستخدم لغة مثل Java أو ASP.NET أو PHP أو غيرها لتشغيل خادوم محلي. وسواء كنا نتصل بقاعدة بيانات محلية أم بخادوم بعيد كواجهة برمجية، فإننا نتبع نفس الخطوات بالضبط. هنا يأتي دور $http، صديقك المخلص من اليوم فصاعدًا. الدالة $http هي مُغلّف wrapper تقدمه Angular للوصول إلى البيانات من الخادوم، وهو سهل الاستخدام للغاية ولا يحتاج لأية خبرة. فيما يلي مثال عن طلب GET لجلب البيانات من الخادوم. الصياغة مشابهة جدًا لصياغة jQuery، وهذا يُسهل الانتقال من الأخيرة إلى Angular: myApp.controller('MainCtrl', ['$scope', '$http', function ($scope, $http) { $http({ method: 'GET', url: '//localhost:9000/someUrl' }); }]);يُعيد Angular إلينا أمرًا يُصطلح على تسميته الوعد (Promise) ، وهو بديل أسهل استخدامًا من الاستدعاءات الراجعة (callbacks). يمكن تركيب الوعود في سلسلة باستخدام النقطة، ويمكننا ربطها مع مستقبلات النجاح والفشل: myApp.controller('MainCtrl', ['$scope', function ($scope) { $http({ method: 'GET', url: '//localhost:9000/someUrl' }) .success(function (data, status, headers, config) { // successful data retrieval }) .error(function (data, status, headers, config) { // something went wrong :( }); }]);سهلة الاستخدام والقراءة. هنا نربط طريقة العرض والخادوم بربط نموذج أو تحديثه. لنفترض أنه لدينا خادومًا مُعدًّا ولنقم بدفع اسم المستخدم إلى طريقة العرض عن طريق طلب AJAX. علينا --لو كنا حريصين على المثالية-- أن نصمم بيانات JSON التي نريدها أولاً. دعونا الآن نُبسط الأمور، ولندع هذا الأمر ليتولاه من يفهم في أمور النهاية الخلفية (backend)، ولكن لنقل أننا تفترض أن نستقبل بيانات مثل هذه: { "user": { "name": "Todd Motto", "id": "80138731" } }هذا يعني أننا سنحصل على كائن Object من الخادوم (سنسميه data، وسترى أنه يُمرر إلى مستقبلات الوعد الذي أنشأناه). علينا الآن أن نربط هذا الكائن بالخاصة data.user، وضمنها لدينا name وid. يمكن ببساطة الوصول إلى هذه القيم باستخدام data.user.name للحصول على "Todd Motto" مثلاً. فيما يلي الشفرة البرمجية (اطلع على التعليقات المضمنة): myApp.controller('UserCtrl', ['$scope', '$http', function ($scope, $http) { // create a user Object $scope.user = {}; // Initiate a model as an empty string $scope.user.username = ''; // نريد أن نرسل طلباً ونحصل على اسم المستخدم $http({ method: 'GET', url: '//localhost:9000/someUrlForGettingUsername' }) .success(function (data, status, headers, config) { // عند نجاح الطلب، نُسنِد الاسم إلى النموذج الذي أنشأناه $scope.user.username = data.user.name; }) .error(function (data, status, headers, config) { // وقع خطأ ما! :( }); }]);الآن ضمن الصفحة يمكننا ببساطة كتابة: <div ng-controller="UserCtrl"> <p>{{ user.username }}</p> </div>هذا سيعرض اسم المستخدم. لننتقل الآن إلى خطوة أبعد بفهم ربط البيانات التصريحي (Declarative data-binding) حيث تصبح الأمور أكثر إثارة. ربط البيانات التصريحيتقوم فلسفة Angular على إنشاء نصوص HTML ديناميكية قادرة على القيام بوظائف بنفسها لم نكن نتوقع أنها ممكنة ضمن المتصفح. هذه هي المهمة التي تقوم بها Angualar على خير وجه. لنتخيل أننا أرسلنا طلب AJAX لجلب قائمة بعناوين البريد الإلكتروني وسطر الموضوع فيها مع تاريخ إرسالها، ولنفترض أننا نريد عرضها ضمن الصفحة. في هذا المكان بالضبط تذهلنا Angular بقدراتها. لننشئ أولاً متحكماً بالبريد الإلكتروني: yApp.controller('EmailsCtrl', ['$scope', function ($scope) { // نُنشئ كائناً يدعاً `emails` $scope.emails = {}; // لنفترض أننا حصلنا على هذه البيانات من الخادوم // هذه **مصفوفة** من **الكائنات** $scope.emails.messages = [{ "from": "Steve Jobs", "subject": "I think I'm holding my phone wrong :/", "sent": "2013-10-01T08:05:59Z" },{ "from": "Ellie Goulding", "subject": "I've got Starry Eyes, lulz", "sent": "2013-09-21T19:45:00Z" },{ "from": "Michael Stipe", "subject": "Everybody hurts, sometimes.", "sent": "2013-09-12T11:38:30Z" },{ "from": "Jeremy Clarkson", "subject": "Think I've found the best car... In the world", "sent": "2013-09-03T13:15:11Z" }]; }]);علينا الآن دفعها ضمن الصفحة. هنا نستخدم الربط التصريحي لنُعلن عما سيفعله تطبيقنا: إنشاء أول جزء من عناصر HTML الحيوية. سنستخدم مُرشد ng-repeat المبني ضمن Angular، والذي سوف يمر على البيانات ويعرض الناتج دون عناء الاستدعاءات الرجعية أو تغيير الحالة، بهذه السهولة: <ul> <li ng-repeat="message in emails.messages"> <p>From: {{ message.from }}</p> <p>Subject: {{ message.subject }}</p> <p>{{ message.sent | date:'MMM d, y h:mm:ss a' }}</p> </li> </ul>النتيجة قمت أيضًا باستخدام مُرشّح التاريخ لأبين لك كيف يمكن أن تعرض تواريخ UTC. اطلع على المُرشدات التي توفرها Angular لتتعرف على القدرات الكاملة للربط التصريحي. بهذا نكون قد عرفنا كيف نصل البيانات بين الخادوم وطريقة العرض. وظائف المجال (Scope functions)تعتبر وظائف المجال الخطوة التالية في بناء وظائف التطبيق واستكمالاً للربط التصريحي. فيما يلي وظيفة بسيطة تحذف إحدى الرسائل: myApp.controller('MainCtrl', ['$scope', function ($scope) { $scope.deleteEmail = function (index) { $scope.emails.messages.splice(index, 1) }; }]);تنويه: من المهم أن تفكر في حذف البيانات من النموذج، لا تحذف العناصر أو أي شيئ من الصفحة، دع Angular يتولى هذا بربطه ثنائي الجانب للبيانات، فقط فكر بذكاء واكتب شفرة برمجية تستجيب لبياناتك. ربط الوظائف مع طريقة العرض يمر عبر المُرشدات، هذه المرة نستخدم مُرشد ng-click: <a ng-click="deleteEmail($index)">Delete email</a>هذا يختلف تمامًا عن مستقبلات النقر التقليدية في JavaScript، لأسباب عديدة سنشرحها لاحقًا. لاحظ أنني أيضًا أمرر فهرس العنصر $index، إذ يعرف Angualr ما العنصر المُراد حذفه (كم يوفر هذا من العناء؟) النتيجة دوال DOM التصريحيةننتقل الآن لدوال DOM، وهي أيضًا مُرشدات تؤدي وظيفة ضمن الصفحة بدونها كنا سنكتب شفرات برمجية طويلة. أحد الأمثلة المناسبة لإيضاح الفكرة هنا هو إظهار أو إخفاء قسم التنقل ضمن الصفحة باستخدام ng-show وng-click، لنرَ بساطة هذا: <a href="" ng-click="toggle = !toggle">Toggle nav</a> <ul ng-show="toggle"> <li>Link 1</li> <li>Link 2</li> <li>Link 3</li> </ul>هنا ندخل عالم MVVM (اختصار Model-View-View-Model)، لاحظ أننا لم نستخدم متحكمًا، وسنشرح فكرة MVVM بعد قليل. الناتج (جرب الإظهار والإخفاء): النتيجة التعابير (Expressions)من أفضل ما يقدمه Angular، يقدم بديلاً عن الحاجة لكتابة الكثير من JavaScript والنصوص المكررة. هل قمت يومًا ما بكتابة شيء كهذا؟ elem.onclick = function (data) { if (data.length === 0) { otherElem.innerHTML = 'No data'; } else { otherElem.innerHTML = 'My data'; } };ربما يكون هذا استدعاءً راجعًا عن طلب GET، ونحتاج لنغير محتوى في الصفحة بناء على البيانات، يقدم Angular هذا بدون الحاجة لكتابة حرف JavaScript! <p>{{ data.length > 0 && 'My data' || 'No data' }}</p>سيقوم هذا بتحديث الصفحة تلقائيًا دون استدعاءات عند وصول البيانات أو ما شابه. إن لم تتوفر البيانات، سيظهر هذا واضحًا، وإن وجدت فسيظهر كذلك. هناك حالات كثيرة جدًا يُمكن فيها لـAngular أن يتولاها عبر ربطه ثنائي الجانب للبيانات الذي يعمل بشكل رائع. النتيجة طرق العرض الديناميكية والتوجيه (Routing)هي ما تقوم عليه فلسفة تطبيقات الويب (والمواقع) أحادية الصفحة: لديك قسم الترويسة وقسم التذييل وشريط جانبي ثم المحتوى الذي يُحدث تلقائيًا بناءً على الرابط الحالي. يجعل Angular إعداد هذا الأمر في منتهى السهولة. تحقن طرق العرض الديناميكة دوالا مُعينة بناء على الرابط، عبر استخدام $routeProvider. فيما يلي إعداد بسيط: myApp.config(['$routeProvider', function ($routeProvider) { /** * $routeProvider */ $routeProvider .when('/', { templateUrl: 'views/main.html' }) .otherwise({ redirectTo: '/' }); }]);لاحظ أنه عندما (when) يكون الرابط / فقط (الصفحة الرئيسية)، ستعرض الصفحة main.html. من المفيد تسمية طريقة العرض الأساسية main.html وليس index.html لأنه سيكون لدينا مسبقاً الصفحة index.html وهي الصفحة التي تحوي طرق العرض الديناميكية وبقية الأجزاء. يمكن ببساطة إضافة المزيد من طرق العرض: myApp.config(['$routeProvider', function ($routeProvider) { /** * $routeProvider */ $routeProvider .when('/', { templateUrl: 'views/main.html' }) .when('/emails', { templateUrl: 'views/emails.html' }) .otherwise({ redirectTo: '/' }); }]);بإمكاننا الآن تحميل الصفحة emails.html ببساطة لتقوم بعرض قائمة الرسائل الإلكترونية. الخلاصة أننا استطعنا أن نبني تطبيقًا معقدًا للغاية بجهد ضئيل جدًا. توفر الخدمة $routerProvider المزيد من الخيارات، لكن ما تعلمناه عنها كافٍ في البداية. هناك أيضًا أمور أخرى مثل مُعترِضات $http التي تبدأ أحداثًا خلال مسير طلب AJAX، فتتيح لنا عرض مقدار التقدم على سبيل المثال أثناء جلب البيانات. البيانات الثابتة العامةفي تطبيق Gmail للويب، تُكتب بيانات كثيرة بصيغة JSON ضمن الصفحة (انقر باليمين واختر عرض المصدر في صفحة Gmail). إن قمنا بخطوة مشابهة، أي كتابة البيانات ضمن الصفحة، فهذا سيجعل وقت عرضها أقل وسيبدو التطبيق أسرع. عندما أطور تطبيقات مؤسستنا، تُدرج وسوم Java ضمن الصفحة وعندما تُعرض، تُرسل البيانات من الخادوم (لا خبرة لدي في Java لذا سأكتب فيما يلي تصريحات وهمية، يمكنك استخدام أي لغة على الخادوم إن أحببت). النصوص التالية توضح كيف يمكنك كتابة JSON ضمن الصفحة ثم تمريرها إلى المتحكم لربطها مباشرة: <!-- ضمن index.html (في نهاية الصفحة بالطبع) --> <script> window.globalData = {}; globalData.emails = <javaTagHereToGenerateMessages>; </script>سيُنشئ وسم Java الذي اختلقته البينات بينما سيعالج Angular الرسائل فورًا. كل ما عليك هو إعطاؤه البيانات عبر المتحكم: myApp.controller('EmailsCtrl', ['$scope', function ($scope) { $scope.emails = {}; // Assign the initial data! $scope.emails.messages = globalData.emails; }]);تقليص الملفات (Minification)سنتحدث قليلاً عن تقليص حجم الشفرات البرمجية التي كتبناها. ربما تكون قد جربت تقليص شفراتك البرمجية التي كتبتها لـAngular وصادفت خطأ. ليس هناك أمور خاصة يتطلبها تقليص حجم هذه الملفات، باستثناء الحاجة لإدراج أسماء المتطلبات ضمن مصفوفة قبل الدالة المُصرّح عنها، لنوضح أكثر: myApp.controller('MainCtrl', ['$scope', 'Dependency', 'Service', 'Factory', function ($scope, Dependency, Service, Factory) { // code }]);بعد التقليص: myApp.controller('MainCtrl', ['$scope', 'Dependency', 'Service', 'Factory', function (a,b,c,d) { // a = $scope // b = Dependency // c = Service // d = Factory // $scope alias usage a.someFunction = function () {...}; }]);عليك أن تحافظ على ترتيب المتطلبات المحقونة في المصفوفة ['_one', '_two'] وضمن مُعاملات الدالة function(_one, _two)، وإلا فإنك ستسبب لنفسك ولفريق العمل معك مشاكل كثيرة! الاختلافات بين MVC وMVVMسننهي مقالتنا العملاقة الآن بشرح سريع يشمل الفروق بين MVC وMVVM: MVC: التواصل يعتمد على المتحكم (Controller)، لذلك نقول Model-View-ControllerMVVM: يشمل ربط البيانات التصريحي الذي يتواصل، بالمفهوم التقني، مع نفسه؛ أي Model-View-View-Model. يتواصل النموذج مع طريقة العرض، وتتواصل هذه الأخيرة مع النموذج ثانية. يسمح هذا للبيانات بأن تبقى محدّثة على الجانبين دون الحاجة لفعل أي شيء. لا داعي هنا للمتحكم. مثال على ذلك: إنشاء حلقة ng-repeat دون أن نعتمد على بيانات يُرسلها متحكم:<li ng-repeat="number in [1,2,3,4,5,6,7,8,9]"> {{ number }} </li>يبدو هذا مناسبًا للتجارب السريعة، ولكنني أنصح دومًا باستخدام متحكم للحفاظ على تنظيم النص البرمجي. مثال مكونات الويب في HTML5قد تبدو هذه الفكرة مُكررّة، ولكنني سأعيدها هنا لنتحدث عن مكونات الويب. يسمح Angular بإنشاء عناصر (elements) مخصصة مثل: <myCustomElement></myCustomElement> في الحقيقة هذا أشبه ما يكون بمستقبل HTML5 التي تقدم فكرة جديدة تُسمى مكونّات الويب (Web Components)، والتي تتركب من عناصر مخصصة ضمن HTML مترافقة مع نص JavaScript ديناميكي، وهذا أمر مثيرٌ للغاية - والأكثر إثارة أنه أمر ممكن اليوم باستخدام Angular. فريق Angular بعيد النظر - شكرًا لكم! تعليقات المجال (Scope)أعتقد أن تعليقات المجال إضافة جميلة لسياق العمل، فبدل الحاجة لكتابة التعليقات ضمن HTML بالطريقة التالية: <!-- header --> <header> Stuff. </header> <!-- /header -->أصبحنا نتحدث عن طرق العرض والمجالات بدل الصفحة، البيانات ضمن مجال ما لا تُشارك مع مجالات أخرى إلا إن قمت بفعل ذلك عن عمدٍ. لنستغل هذا في تحسين كتابة النص البرمجي بتقسيمه إلى مناطق تسبقها تعليقات: !-- scope: MainCtrl --> <div class="content" ng-controller="MainCtrl"> </div> <!-- /scope: MainCtrl -->تصحيح العلل في Angularتتوفر إضافة جميلة للغاية لمتصفح Chrome ينصح بها فريق Angular، وتسمى Batarang. برمجة سعيدة! ترجمة -وبتصرف- للمقال: Ultimate guide to learning AngularJS in one day لصاحبه Todd Motto (الذي -بطبيعة الحال- يعود عليه ضمير المُتكلم في هذا المقال).