لوحة المتصدرين
المحتوى الأكثر حصولًا على سمعة جيدة
المحتوى الأعلى تقييمًا في 01/19/21 في كل الموقع
-
سأشرح لك مبدأ عمل الدالة التراجعية وأجعلك تجيبين الأسئلة بنفسك، الأمر بسيط جداً فقط ركزي وإفهمي آلية عملها. لنفرض أن لدينا الدالة التالية : void arrayCount(int arr[]){ //... } void تعني أن الدالة لن ترجع لنا أي قيمة، لأننا في هذا المثال نريد طباعة عناصر المصفوفة بإستخدام الدالة التراجعية. في البداية نحتاج لمتغير من نوع int كـعداد يبدأ من الصفر وهو العنصر الأول في أي مصفوفة. إذا نضيف المتغير ونسميه counter أي العداد، فيصبح الكود كالتالي: int counter =0; void arrayCount(int arr[]){ //... } بعد ذلك نبدأ بكتابة الدالة arrayCount: ما هو الشرط الذي نستطيع من خلاله أن نعرف أننا وصلنا لآخر عنصر في المصفوفة ؟ ما رأيك بالخاصية Length الموجودة مسبقاً في المصفوفة والتي ترجع لنا طول المصفوفة؟ يمكننا الإستفادة من تلك الخاصية في تطبيق فكرتنا من خلال الشرط التالي: if( counter < arr.Length ){ //... } توضيح الشرط: طالما أن العداد الخاص بنا counter أقل من طول المصفوفة فهذا يعني أننا لم نصل لنهاية المصفوفة، ويتبقى عنصار يجب طباعتها. إذاً هيا بنا نضيف الشرط للكود الخاص بنا ليكون كالتالي: int counter =0; void arrayCount(int arr[]){ if( counter < arr.Length ) //... } } الآن يجب علينا طباعة عناصر المصفوفة من خلال: Console.WriteLine(); سنقوم بإضافة السطر الخاص بالطباعة في بداية الدالة التراجعية فيصبح الكود الخاص بنا كالتالي: int counter =0; void arrayCount(int arr[]){ Console.WriteLine(arr[counter]); if( counter < arr.Length ) //... } } يجب علينا أن نزيد العداد الخاص بنا counter في كل مرة تستدى به الدالة لضمان طباعة جميع العناصر، لاحظ آخر سطر في الدالة: int counter =0; void arrayCount(int arr[]){ Console.WriteLine(arr[counter]); if( counter < arr.Length ) //... } counter= counter+1; } ماذا تتوقعين أن يكون الناتج في حال تنفيذ الكود التالي وإستدعاء الدالة arrayCount ؟ سيتم طباعة أول عنصر في المصفوفة فقط, لماذا؟ لأننا ببساطة لم نقم بإستدعائها إلا مرة واحدة فقط وبالتالي سيطبع العنصر الأول فقط. سنقوم بإستدعاء الدالة من داخلها ( هذا هو مفهوم الدالة التراجعية أو recursion method/function ) في حال بقي عناصر يجب طباعتها ( الشرط الذي أضفناه ), لاحظ الكود الذي أضفناه: int counter =0; void arrayCount(int arr[]){ Console.WriteLine(arr[counter]); //طباعة عناصر المصفوفة if( counter < arr.Length )// في حال كان هناك عناصر أُخرى باقية سيتم إستدعاء الدالة مرة أخرى لحين الوصول إلى آخر المصفوفة arrayCount(arr); // يتم إستدعاء الدالة نفسها من داخلها لطباعة العناصر التالية } counter= counter+1; // زيادة العداد في كل مرة تستدعى فيها الدالة } وبهذا نكون قد فهما مبدأ الrecursion functions/methods. مرفق صورة توضيحية لآلية عمل الـrecursion functions/methods: أطيب الأمنيات بالتوفيق.2 نقاط
-
2 نقاط
-
سنتعلّم في هذا الدرس: فهم قوالب Vue.js الوصول إلى البيانات والتوابع من كائنات Vue.js الربط مع السمات Attributes كتابة شيفرة HTML خام التعامل مع الأحداث Events استخدام الربط ثنائي الاتجاه نتابع عملنا في هذا الدرس وهو الدرس الثاني من سلسلة دروس تعلّم Vue.js. سنتعلم هذه المرّة كيفية الوصول والتعامل مع DOM، حيث سنتعلّم كيف نستخدم موجّهات Vue.js مختلفة للوصول إلى بيانات كائن Vue.js والتفاعل معها، وسنتوسّع في التعامل مع الأحداث events بالإضافة إلى كيفية استخدام الربط ثنائي الاتجاه مع العناصر. فهم قوالب Vue.js تعاملنا في الدرس السابق مع تطبيقات استخدمت مزايا بسيطة من Vue.js، وإذا كنت تذكر أنّنا قد كتبنا شيفرة HTML بسيطة ومن ثمّ استخدمنا الاستبدال النصي '{{ message }}' ، واستخدمنا أيضًا موجّه 'v-on' للاستجابة إلى دخل المستخدم. ما يقوم به Vue.js من وراء الكواليس، هو أخذ نسخة عن شيفرة HTML وحفظها داخليًّا على شكل قالب template، بعد ذلك يتم إجراء عملية تصيير (rendering) على نسخة من القالب السابق، باستخدام الموجّهات وتعابير الاستبدال النصي (في حال وجودها ضمن القالب)، ثمّ بعد الانتهاء من التصيير يتم عرض الخرج النهائي على المستخدم. الآن، وعندما يُحدَث أي تغيير جديد في قيمة أي حقل مرتبط من كائن Vue.js، ستُجرى عملية تصيير جديدة على نسخة جديدة من القالب السابق المخزّن داخليًّا، ثم يُعرض الخرج النهائي من جديد على المستخدم. أي كما لو أنّنا أنشأنا ارتباطًا دائمًا بين كائن Vue.js وبين شيفرة HTML. وهذا ما رأيناه فعليًا في التطبيقات البسيطة التي تناولناها في الدرس السابق. ملاحظة من باب التذكير، نستخدم في هذه السلسلة الموقع jsfiddle.net بشكل افتراضي لتشغيل جميع التطبيقات التي نكتبها. ونحتاج بالطبع إلى إدراج ملف إطار العمل Vue.js لكي نستطيع تنفيذ هذه التطبيقات. إذا أردت أن تعرف كيف ذلك، يمكنك العودة إلى الدرس السابق. الوصول إلى البيانات والتوابع من كائنات Vue.js انظر إلى المثال التالي (مثل العادة، أول مقطع يمثّل شيفرة HTML وثاني مقطع يمثّل شيفرة JavaScript): <div id="app"> {{ title }} </div> var app = new Vue({ el: '#app', data: { title: 'Hello Vue!' } }) عندما نستخدم الاستبدال النصي '{{ title }}' كما وسبق أن فعلنا مسبقًا، لا نستخدم الكلمة 'this' قبل 'title' كما هو واضح. في الحقيقة أنّ أي كلمة تُشير إلى حقل (مثل 'title') موجودة ضمن حاضنة مزدوجة، سيتم اعتبارها على أنّها حقل ضمن القسم data في كائن Vue.js الموافق. الآن عند تنفيذ التطبيق السابق سيؤدي إلى ظهور الجملة Hello Vue! كما هو متوقّع. وبالمثل أيضًا، يمكننا في الواقع استخدام تابع مثل displayMessage() ليحقّق نفس الخرج السابق تمامًا، وبنفس الأسلوب تقريبًا. استخدم المثال التالي: <div id="app"> {{ displayMessage() }} </div> var app = new Vue({ el: '#app', data: { title: 'Hello Vue!' }, methods:{ displayMessage: function(){ return this.title; } } }) لاحظ أنّنا استخدمنا هذه المرة التابع displayMessage() ضمن الحاضنة المزدوجة {{ displayMessage() }} ومرّة أخرى لم نستخدم الكلمة this قبل اسم التابع. أي تابع يُكتب بهذه الطريقة سيُعتَبر افتراضيًّا على أنّه تابع موجود ضمن القسم methods من كائن Vue.js. ولكن هذا السلوك الافتراضي لا يسري على شيفرة JavaScript الموجودة ضمن كائن Vue.js حيث يجب استخدام الكلمة this في كلّ مرّة أردنا فيها الوصول إلى أحد أعضاء كائن Vue.js. الربط مع السمات Attributes لا يمكن استخدام تقنية الاستبدال النصي (باستخدام الحاضنة المزدوجة) لإدراج قيم ضمن سمات العناصر. لفهم هذا الموضوع بشكل جيّد، انظر معي إلى المثال التالي: <div id="app"> <p> {{ message }} - <a href='{{link}}'>Hsoub Academy</a> </p> </div> var app = new Vue({ el: '#app', data: { message: 'Hello Vue!', link: 'https://academy.hsoub.com/' } }) عند تنفيذ التطبيق السابق في jsfiddle.net ستحصل على الخرج التالي: لاحظ أنّه خرج منسّق كما هو متوقّع، ولكن إذا جربت النقر على الرابط لن ينقلك إلى موقع أكاديمية حسوب كما هو متوقّع، في الحقيقة سينقلك هذا الرابط إلى صفحة ضمن نفس موقع jsfiddle.net وهذه الصفحة بالطبع ستكون غير موجودة. السبب في ذلك أنّ تقنية الاستبدال النصّي تُعامِل محتويات الحقل link كنص مجرّد، يمكنك ملاحظة الرابط الناتج بعد النقر: https://fiddle.jshell.net/_display/{{title}} الحل لهذه المشكلة بسيط، ويتمثّل في تجنّب استخدام السمة href بهذا الشكل، إنّما ينبغي استخدام الموجّه v-bind مع الوسيط href على النحو التالي: v-bind:href = 'link' حيث link هو نفسه الحقل الموجود ضمن كائن Vue.js. استبدل بالتعبير السابق السمة href القديمة الموجودة ضمن شيفرة HTML الموجود في المثال السابق، بعد الاستبدال سيصبح شكل شيفرة HTML على النحو التالي: <div id="app"> <p> {{ message }} - <a v-bind:href = 'link'>Hsoub Academy</a> </p> </div> أعد تشغيل التطبيق مرّة أخرى، ستحصل على نفس الخرج، ولكن هذه المرّة إذا نقرت على الرابط ستنتقل إلى موقع أكاديمية حسوب. إذًا كخلاصة على ما سبق، إذا أردت أن تربط مع السمات فعليك استخدام موجّه مع الوسيط المناسب بدلًا من استخدام تقنية الاستبدال النصّي. سنتناول عددًا من هذه الموجّهات خلال هذه السلسلة، ويمكنك دومًا زيارة الصفحة الرسمية لإطار العمل Vue.js على الرابط رابط للاطلاع على جميع الموجّهات المتوفّرة. كتابة شيفرة HTML خام نحتاج في بعض الأحيان إلى كتابة شيفرة HTML خام مباشرةً في الصفحة. قد يتبادر إلى ذهنك أن تستخدم الاستبدال النصي مع الحاضنة المزدوجة، ولكن لن تنفع هذه التقنية في هذه الحالة. تأمّل معي المثال التالي لفهم أفضل حول هذه المشكلة: <div id="app"> {{ raw }} </div> var app = new Vue({ el: '#app', data: { raw: '<ul><li>First Item</li><li>Second Item</li><li>Third Item</li></ul>' } }) الهدف من التطبيق السابق هو عرض قائمة غير مرتّبة عن طريق العنصر 'ul' تُظهر ثلاثة عناصر فقط: First Item و Second Item و Third Item. ولكن عند التنفيذ لن تحصل على ما هو متوقع، ستحصل على الخرج التالي: First Item Second Item Third Item أي أنّك ستحصل على نص عادي دون أن يتعرّف عليه المتصفّح على أنّه شيفرة HTML. يمكن هذه المشكلة بسهولة بإجراء تعديل على شيفرة HTML فقط على النحو التالي: <div id="app"> <p v-html='raw'> </p> </div> التعديل الذي أجريته هو استخدام الموجّه v-html الذي يسمح في حالتنا هذه باستخدام محتويات الحقل raw كشيفرة HTML نظامية وليس مجرّد نص عادي (لاحظ أنّني قد تخلصت من الاستبدال النصّي {{ raw }}. أعد تنفيذ التطبيق، لتحصل على قائمة مُنسّقة بشكل صحيح. التعامل مع الأحداث (Events) للأحدث كما نعلم أهميّة عظيمة في تطوير التطبيقات التي تتفاعل مع المستخدم. تدعم Vue.js الأحداث بشكل جيّد، ولقد تعاملنا في الدرس السابق مع نوعين من الأحداث: حدث الإدخال ضمن مربّع النص، وحدث النقر بزر الفأرة على عنصر HTML. سنتناول في هذا الدرس الأحداث بشيء من التفصيل، حيث سنتعلّم كيف ننصت إلى أحداث الفأرة، بالإضافة إلى الإنصات إلى أحداث لوحة المفاتيح. الإنصات إلى أحداث الفأرة نبدأ بالتعامل مع أحداث الفأرة، لتُنعش ذاكرتك، انظر معي إلى التطبيق البسيط الموجود في الدرس السابق: <div id="app"> <input type='text' v-on:input="updateInfo"/> {{ message }} </div> var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' }, methods:{ updateInfo:function(event){ this.message = event.target.value; } } }) سبق وأن ذكرنا في الدرس السابق أنّه يتم الإنصات إلى أي حدث ضمن عنصر ما باستخدام الموجّهv-on، حيث يتم تغيير الوسيط المُمرّر لهذا الموجّه بتغيّر نوع الحدث المراد الإنصات له. بعد تحديد نوع الوسيط المراد تمريره للموجّه v-on يتم تحديد التابع الذي سيستجيب (سيعالج) هذا الحدث، وهو عبارة عن تابع ضمن القسم methods ضمن كائن Vue.js يتم استدعاؤه عند وقوع الحدث. بالنسبة للتابع المعالج للحدث (التابع updateInfo في المثال السابق) سيتم توليد كائن يحتوي على معلومات مهمّة حول الحدث الذي وقع، ويتم تمرير هذا الكائن بشكل تلقائي إلى التابع المعالج للحدث. على العموم، يمكن الاستغناء عن هذا السلوك التلقائي، وتمرير قيمة كيفيّة للتابع المعالج للحدث. انظر معي إلى المثال التالي: <div id='app'> <button v-on:click="increase(2)">Increase!</button> <p>{{counter}}</p> </div> var app = new Vue({ el: '#app', data: { counter: 0 }, methods:{ increase: function(value){ this.counter += value; } } }) الشيفرة السابقة مألوفة، مع ملاحظتين جديدتين. الأولى أنّنا لم نكتفي بكتابة اسم التابع المعالج للحدث ضمن الموجّه v-on:click فحسب، إنّما قد مرّرنا القيمة 2 كوسيط لهذا التابع على الشكل: increase(2) (لاحظ شيفرة HTML). بالمقابل، إذا تأملت شيفرة JavaScript ضمن تعريف التابع increase في القسم methods، فستلاحظ أنّنا نعامل الوسيط value كمتغيّر يحمل قيمة عددية وليس ككائن يحمل معلومات حول الحدث الذي وقع. أي أنّنا قد استطعنا تغيير السلوك الإفتراضي لعمليّة استدعاء التابع المعالج. بالنسبة للمثال السابق، فكما هو واضح، يعمل التطبيق على زيادة قيمة المتغيّر counter بمقدار القيمة value (في مثالنا السابق ستكون تساوي 2) في كل مرّة يتم فيها نقر الزر. في بعض الحالات قد نحتاج إلى تمرير كائن الحدث بالإضافة إلى تمرير قيمة كيفيّة بنفس الوقت. تدعم Vue.js هذا الأمر ببساطة من خلال تمرير الكلمة المحجوزة $event إلى التابع المعالج بالإضافة إلى القيمة الكيفية المراد تمريرها. إذا أردنا تطبيق ذلك على المثال الأخير فسيصبح تعريف الزر button على النحو التالي: <button v-on:click="increase(2, $event)">Increase!</button> وبالنسبة لتعريف التابع المعالج ضمن القسم methods فسيصبح على النحو التالي: increase: function(value, event){ this.counter += value; } أي مجرّد إضافة وسيط آخر. التعديل على كيفية الاستجابة للأحداث نحتاج في بعض الأحيان أن نُعدّل على كيفيّة الاستجابة للأحداث، فربما نحتاج في وقت ما إلى إيقاف الاستجابة لحدث ما لأحد العناصر دونًا عن العناصر الأخرى في HTML. لكي أضرب لك مثالًا جميلًا حول هذا الأمر، اسمح لي أولًا أن أقدّم لك حدث حركة الفأرة mousemove. يُولّد هذا الحدث عند مرور مؤشّر الفأرة فوق عنصر ما، ويتم استخدامه كما هو متوقّع مع الموجّه v-on. انظر إلى المثال البسيط التالي: <div id="app"> <p v-on:mousemove='updateCoordinates'> Mouse cursor at: ({{x}}, {{y}}) </p> </div> var app = new Vue({ el: '#app', data: { x:0, y:0 }, methods:{ updateCoordinates:function(event){ this.x = event.clientX; this.y = event.clientY; } } }) جرّب تنفيذ التطبيق البسيط السابق، ثمّ حرّك الفأرة فوق العنصر الوحيد الظاهر أمامك. ستحصل على خرج شبيه بما يلي: الجديد هنا هو استخدام الموجّه v-on:mousemove حيث أسندنا إليه التابع المعالج updateCoordinates المعرَّف بطبيعة الحال ضمن القسم methods في كائن Vue.js. لاحظ معي أيضًا كيف نحصل على الإحداثيات الحالية لمؤشّر الفأرة (الفاصلة x والتراتيب y) ضمن التابع updateCoordinates: this.x = event.clientX; this.y = event.clientY; الآن إذا أردنا أن ننشئ منطقة "ميتة" (ضمن عنصر span مثلًا) ضمن عنصر p الذي يعرض الإحداثيات، بحيث لايؤدّي مرور مؤشّر الفأرة فوق هذه المنطقة إلى توليد الحدث mousemove، فينبغي علينا عندها التعديل على الحدث mousemove كما يلي (سألوّن التعديلات الإضافية بالأخضر): <div id="app"> <p v-on:mousemove='updateCoordinates'> Mouse cursor at: ({{x}}, {{y}}) - <span v-on:mousemove='uncoveredArea'>Uncovered Area</span> </p> </div> var app = new Vue({ el: '#app', data: { x:0, y:0 }, methods:{ updateCoordinates:function(event){ this.x = event.clientX; this.y = event.clientY; }, uncoveredArea: function(event){ event.stopPropagation(); } } }) أضفت الموجّه v-on:mousemove إلى العنصر span وأسندت المعالج uncoveredArea له. بالنسبة للتابع uncoveredArea فقد أجريت تعديل على الحدث من خلال استدعاء التابع stopPropagation() من الكائن event. المعنى الحرفي لهذا التابع هو "إيقاف الانتشار" أي أنّنا سنمنع الإستجابة لهذا الحدث عندما يمر مؤشّر الفأرة فوق العنصر span. جرّب تنفيذ التطبيق السابق، ولاحظ التغيير الذي سيحدث عندما يمر مؤشّر الفأرة فوق عنصر span. إذا أردت الإحساس بالفرق، يمكنك أن تحذف التعليمة event.stopPropagation() ثم أعد تنفيذ التطبيق مرّة أخرى، لترى كيف أنّ الإحداثيات ستتغيّر عندما يمر مؤشّر الفأرة فوق عنصر span هذه المرة. يمكن استخدام صيغة أبسط للتعديل على الأحداث، فمن الممكن حذف التابع uncoveredArea بالكامل من قسم methods، والاكتفاء بالقسم الخاص بالموجّه على النحو التالي: v-on:mousemove.stop='' أي أنّنا قد استغنينا عن الشيفرة اللازمة لإيقاف انتشار الحدث mousemove. نسمي .stop هنا بمعدِّل الحدث (event modifiers). هناك عدّة معدّلات أحداث مفيدة سنستعرض بعضها منها خلال مسيرتنا في هذه السلسلة. الإنصات إلى أحداث لوحة المفاتيح يمكننا أحيانًا أن نحتاج إلى الإنصات أيضًا إلى الأحداث الناشئة من لوحة المفاتيح. والأسلوب المتبع هنا، يشبه إلى حدّ كبير ما كنّا نفعله مع أحداث الفأرة. إذا أردنا مثلًا الإنصات إلى حدث تحرير مفتاح من لوحة المفاتيح يمكن أن نستخدم الوسيط 'keyup' للموجّه v-on على النحو التالي: v-on:keyup='methodName' حيث 'methodName' هو اسم التابع المعالج للحدث 'keyup' والذي يجب أن يُوضَع ضمن القسم methods. دعنا الآن نوظّف ذلك في مثال بسيط: <div id="app"> <input type='text' v-on:keyup='keyIsUp' /> <p> {{message}} </p> </div> var app = new Vue({ el: '#app', data: { message: '' }, methods: { keyIsUp: function(event) { this.message = event.target.value; } } }) يعمل هذا التطبيق البسيط على تحديث الحقل 'message' كلّما تمّ تحرير مفتاح من لوحة المفاتيح، وبالتالي سيؤدّي ذلك إلى تحديث محتويات عنصر 'p' ضمن الواجهة. ولكن دعنا نتساءل، ماذا لو أردنا أن يستجيب المعالج 'keyIsUp' كلّما حُرِّر مفتاح المسافة (space) فقط، وليس عند أيّ مفتاح يُحرِّره المستخدم. الجواب ببساطة، هو في استخدام معدّل الحدث '.space' بعد 'keyup'. أضف فقط الكلمة '.space' إلى 'keyup' إلى المثال السابق. أي على النحو التالي: v-on:keyup.space = 'keyIsUp' أعد تنفيذ التطبيق لترى أنّ محتويات عنصر 'p' أصبحت لا تُحدَّث إلّا بعد تحرير المفتاح space. يوجد بالطبع العديد من المعدّلات التي تمثّل جميع المفاتيح على لوحة المفاتيح، فهناك مثلًا 'enter' و 'tab' و 'up' لمفتاح السهم العلوي، و 'down' لمفتاح السهم السفلي وهكذا. لمعدّلات أحداث لوحة المفاتيح الكثير من الفوائد، يتمثّل أبسطها في إرسال المحتوى الذي أدخله المستخدم بمجرد ضغطه للمفتاح Enter، أو إرسال البيانات مباشرةً بينما يكتبها المستخدم للحصول على مقترحات أثناء عملية الكتابة (كما يفعل محرّك البحث غوغل أثناء كتابة المستخدم للمفردات المراد البحث عنها). وغيرها الكثير من الاستخدامات. استخدام الربط ثنائي الاتجاه في معظم الأمثلة السابقة عمدنا إلى استخدام ربط باتجاه واحد، من الشيفرة إلى عنصر HTML. وفي بعض الحالات استطعنا أن نعكس هذا الأمر. أي استطعنا تعديل قيمة الحقل عن طريق الانصات إلى حدث الإدخال v-on:input وبالتالي معالج حدث مخصّص لهذه الغاية. ولكن توجد طريقة مباشرة وسهلة لإيجاد ربط ثنائي الاتجاه فعلي في Vue.js وذلك باستخدام الموجّه v-model وبدون الحاجة إلى معالج حدث، كمال في المثال التالي: <div id="app"> <input type='text' v-model='name'/> {{ name }} </div> var app = new Vue({ el: '#app', data: { name: 'Hello Vue!' } }) التطبيق السابق بسيط، وهو يعمل على إجراء ربط ثنائي الاتجاه بين الحقل name وبين عنصر مربّع النص، أي سيكون هناك ارتباط آني بين الحقل name وبين عنصر مربّع النص، فإذا حدث تغيّر لأحدهما سينعكس مباشرةً على الآخر. نفّذ التطبيق السابق وسترى مباشرةً الخرج التالي: لاحظ كيف أنّ محتوى مربّع النص قد تمّت تعبئته تلقائيًا بقيمة الحقل name، وبالمثل إذا حاولت الآن كتابة أي شيء ضمن مربّع النص سيتم تعديل قيمة الحقل name فورًا وفقًا له، وبالتالي سيُعدّل محتوى النص الموجود في الطرف الأيمن بسبب وجود الاستبدال النصي {{name}}. ختامًا تعلّمنا في هذا الدرس كيفيّة التعامل مع DOM، حيث تحدثنا عن القوالب، وكيف نصل إلى البيانات والتوابع بشيء من التفصيل، كما تعلّمنا كيفيّة الربط مع السمات، والتعامل مع الأحداث، وتعلّمنا كيفية الربط ثنائي الاتجاه الذي يسمح لنا بمزامنة البيانات بالاتجاهين. تمارين داعمة تمرين 1 يُطلب في هذا التمرين تطوير تطبيق الآلة الحاسبة البسيط الذي بنيناه في الدرس السابق. بحيث يسمح التطبيق الجديد بإجراء العمليات الحسابية الأربع بدلًا من عملية الجمع الوحيدة التي كان يدعمها التطبيق السابق. أقترح الواجهة التالية للتطبيق: لاحظ أنّني قد استخدمت عنصر 'select'، يمكنك استخدام أي طريقة أخرى لاختيار العمليات الحسابية الأربع. من الضروري أن يُوجِد التطبيق الناتج النهائي إذا حدث أحد الأمرين التاليين: تغيير قيمة أحد المعاملين على طرفي العملية الحسابية. تغيير العملية الحسابية عن طريق القائمة المنسدلة. أرجو أن يتمكن التطبيق من تمييز حالة القسمة على صفر، وإظهار رسالة مناسبة للمستخدم. تمرين 2 يُطلب في هذا التمرين إنشاء تطبيق يقدّم للمستخدم مقترحات نصيّة بينما يكتب المستخدم ضمن مربّع نص. سنحاكي عملية الاتصال مع خادوم بعيد عن طريق استخدام مصفوفة نصيّة ضمن الشيفرة. وسأعتبر أنّ المستخدم يحاول أن يُدخل اسم دولة عربية، فتظهر قائمة المقترحات بالأسفل بينما تتم عملية الإدخال. كما في الشكل التالي: ستحتاج بالطبع إلى التعامل مع أحداث لوحة المفاتيح. ولكي يكون الأمر أكثر سهولة بالنسبة إليك. يمكنك استخدام المصفوفة الجاهزة التالية كمصدر للبيانات التي يُفترَض أن تكون قادمة من الخادوم: [ 'السعودية', 'البحرين', 'مصر', 'السودان', 'ليبيا', 'الجزائر', 'المغرب', 'تونس', 'موريتانيا', 'العراق', 'سوريا', 'لبنان', 'قطر', 'الإمارات', 'الصومال', 'جزر القمر', 'الكويت', 'سلطنة عُمان', 'الأردن', 'اليمن', 'فلسطين' ] اقرأ أيضًا المقال التالي: الموجهات الشرطية والتكرارية في Vue.js المقال السابق: مقدمة إلى Vue.js النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js1 نقطة
-
قبل أن ننتقل إلى الموضوع الرئيسي في هذا الفصل، سنلقي نظرة على بعض الطرق المتبعة في تنقيح تطبيقات Node. تنقيح تطبيقات Node إن تنقيح التطبيقات المبنية باستخدام Node أصعب قليلًا من تنقيح شيفرة JavaScript التي تنفذ على المتصفح. لكن مع ذلك تبقى فكرة الطباعة على الطرفية وأسلوب المحاولة والخطأ طريقة فعالة في حل المشاكل. قد تجد من المطورين من يعتقد أن استخدام أساليب أكثر تطورًا هو أمر ضروري، لكن تذكر أن نخبة مطوري البرمجيات مفتوحة المصدر في العالم يستخدمون تلك الطريقة. برنامج Visual Studio Code ستجد أن المنقح المدمج ضمن هذا البرنامج ذو فائدة كبيرة في بعض الحالات. يمكنك أن تشغل البرنامج على وضع التنقيح كالتالي: ملاحظة: يمكن أن تجد التعليمة Run بدلًا من Debug في الإصدار الأحدث من Visual Studio Code. وقد يكون عليك أيضًا أن تهيئ ملف launch.json لتبدأ التنقيح. يمكن تنفيذ ذلك باختيار الأمر ...Add Configuration من القائمة المنسدلة الموجودة بجانب زر التشغيل الأخضر وفوق قائمة VARIABLES، ثم اختيار الأمر Run "npm start" in a debug terminal. يمكنك إيجاد المزيد من الإرشادات بالاطلاع على توثيق التنقيح لبرنامج Visual Studio Code. تذكر أن لا تشغل البرنامج ضمن أكثر من طرفية لأن ذلك سيحجز المنفذ مسبقًا ولن تتمكن من العمل. تعرض لقطة الشاشة التالية الطرفية وقد أوقفنا التنفيذ مؤقتًا في منتصف عملية حفظ الملاحظة الجديدة: توقَّف التنفيذ عندما وصلنا إلى نقطة التوقف التي وضعناها في السطر 63. يمكنك أن ترى في الطرفية قيمة المتغير note. وفي أعلى يسار النافذة ستجد بعض التفاصيل المتعلقة بحالة التطبيق. تُستخدم الأسهم في الأعلى للتحكم بترتيب عملية التنقيح. أدوات تطوير Chrome من الممكن أن نستخدم أدوات تطوير Chrome لتنقيح تطبيقات Node، وذلك بتشغيل التطبيق مستخدمين الأمر التالي: node --inspect index.js ستدخل إلى المنقح بالضغط على الأيقونة الخضراء (شعار Node) التي تظهر على طرفية تطوير Chrome: يعمل المنقح بنفس الطريقة التي يعمل بها عند تنقيح تطبيقات React. يمكنك استخدام النافذة Sources لزرع نقاط توقف في الشيفرة لإيقاف التنفيذ بشكل مؤقت عندها. ستظهر جميع الرسائل التي يطبعها الأمر console.log في النافذة Console من المنقح. كما يمكنك التحري عن قيم المتغيرات وتنفيذ شيفرة JavaScript إن أردت. تحقق من كل شيء قد يبدو لك تنقيح تطبيقات التطوير الشامل (واجهة خلفية وأمامية) محيّرًا في البداية. وقريبًا سيتواصل التطبيق مع قاعدة بيانات. وبالتالي سيزداد احتمال ظهور الأخطاء في أجزاء عدة من الشيفرة. عندما يتوقف التطبيق عن العمل، يجب علينا أولًا تصور المكان الذي قد تظهر فيه المشكلة. وعادة تظهر المشاكل في الأماكن غير المتوقعة وقد يستغرق إيجادها دقائق أو ساعات أو حتى أيامًا. مفتاح الحل هو تنظيم البحث. فالمشكلة قد تتواجد في أي مكان، لذلك تحقق من كل شيء واستبعد احتمالات المشكلة واحدًا تلو الآخر. ستساعدك الطباعة على شاشة الطرفية وكذلك برنامج Postman والمنقحات الأخرى، كما ستلعب الخبرة دورًا هامًا أيضًا. لا تتابع تطوير التطبيق إن لم تعثر على مصدر الخطأ، فهذه أسوأ استراتيجية. لأن ذلك سيسبب أخطاء أكثر وسيكون التنقيح أصعب. اتبع سياسة شركة Toyota لإنتاج الأنظمة (توقف وأصلح) فهي بالفعل سياسة مجدية جدًا في حالتنا. قاعدة البيانات MongoDB سنحتاج قطعًا إلى قاعدة بيانات لحفظ الملاحظات بشكل دائم. تتعامل معظم مناهج جامعة هلسينكي مع قواعد البيانات العِلاقيّة (Relational Databases)، لكننا سنتعامل في منهاجنا مع قاعدة البيانات MongoDB وهي من نمط قواعد البيانات المستقلة. تختلف قواعد البيانات المستقلة (أو التي تأتي على شكل مستندات منفصلة) عن العِلاقيّة في كيفية تنظيم البيانات ولغة الاستعلام التي تدعمها. وعادة ما تصنف قواعدة البيانات المستقلة تحت مظلة NoSQL أي التي لا تستخدم لغة الاستعلام SQL. اطلع على الفصلين collections و documents من دليل استخدام MongoDB لتتعلم أساسيات تخزين البيانات في قواعد البيانات المستقلة. يمكنك أن تُثبّت وتُشغّل MongoDB على حاسوبك. كما يمكنك الاستفادة من المواقع التي تقدم خدمات MongoDB على الإنترنت. سنتعامل في منهاجنا مع مزود الخدمة MongoDB Atlas. حالما تنشئ حسابًا على الموقع وتسجل الدخول، سينصحك الموقع بإنشاء عنقود: سنختار المزود AWS والمنطقة Frankfurt ثم ننشئ العنقود: انتظر حتى يكتمل العنقود ويصبح جاهزًا. قد يستغرق ذلك 10 دقائق. ملاحظة: لا تتابع قبل أن يصبح العنقود جاهزًا. سنستخدم نافذة database access لإنشاء معلومات التوثيق اللازمة لدخول قاعدة البيانات. وانتبه إلى أنها معلومات توثيق مختلفة عن تلك التي تستخدمها عند تسجيل الدخول، وسيستخدمها تطبيقك عندما يتصل بقاعدة البيانات. لنمنح المستخدم إمكانية القراءة والكتابة إلى قاعدة البيانات: ملاحظة: أبلغَ بعض المستخدمين عن عدم القدرة على الوصول إلى قاعدة البيانات بمعلومات التوثيق التي وضعوها بعد إنشاء القاعدة مباشرة. تريّث، فقد يستغرق الأمر دقائق حتى تفعّل هذه المعلومات. سنعرّف بعد ذلك عناوين IP التي يسمح لها بدخول قاعدة البيانات. ولتسهيل الأمر، سنسمح بالوصول إلى القاعدة من أي عنوان IP: أخيرًا أصبحنا جاهزين للاتصال بقاعدة البيانات، إبدأ بالنقر على connect: اختر Connect your application: سيظهر لك عنوان موقع MongoDB، وهو عنوان قاعدة البيانات التي أنشأناها والتي تزود مكتبة عميل MongoDB التي سنضيفها إلى تطبيقنا بالبيانات. يبدو العنوان كالتالي: mongodb+srv://fullstack:<PASSWORD>@cluster0-ostce.mongodb.net/test?retryWrites=true نحن الآن جاهزين لاستخدام قاعدة البيانات. يمكننا استخدام قاعدة البيانات مباشرة عبر شيفرة JavaScript بمساعدة المكتبة official MongoDb Node.js driver، لكن استخدامها مربك قليلًا. لذلك سنستخدم بدلًا منها مكتبة Mongoose التي تؤمن واجهة برمجية عالية المستوى. يمكن توصيف Mongoose بأنها رابط (Mapper) لكائنات من النوع document واختصارًا (ODM). وبالتالي سيكون حفظ كائنات JavaScript كمستندات Mongo مباشرًا باستخدام هذه المكتبة. لنثبت الآن Mongoose كالتالي: npm install mongoose --save لن نكتب أية شيفرات تتعامل مع Mongo في الواجهة الخلفية حاليًا، بل سننشئ تطبيقًا تدريبيًا ونضع في مجلده الجذري الملف mongo.js: const mongoose = require('mongoose') if (process.argv.length < 3) { console.log('Please provide the password as an argument: node mongo.js <password>') process.exit(1) } const password = process.argv[2] const url = `mongodb+srv://fullstack:${password}@cluster0-ostce.mongodb.net/test?retryWrites=true` mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true }) const noteSchema = new mongoose.Schema({ content: String, date: Date, important: Boolean, }) const Note = mongoose.model('Note', noteSchema) const note = new Note({ content: 'HTML is Easy', date: new Date(), important: true, }) note.save().then(result => { console.log('note saved!') mongoose.connection.close() }) ملاحظة: قد يختلف عنوان موقع MongoDB عن العنوان الذي عرضناه سابقًا بناء على المنطقة التي اخترتها عند إنشائك العنقود. تأكد من استخدامك العنوان الصحيح الذي حصلت عليه من MongoDB Atlas. ستفترض الشيفرة بأنها ستمرر كلمة المرور الموجودة ضمن معلومات التوثيق كمعامل لسطر أوامر. حيث يمكننا الوصول إلى معامل سطر الأوامر كالتالي: const password = process.argv[2] عندما تنفذ الشيفرة باستخدام الأمر node mongo.js password ستضيف Mongo ملفًا جديدًا إلى قاعدة البيانات. ملاحظة: استخدم كلمة السر التي اخترتها عند إنشاء قاعدة البيانات وليست كلمة سر الدخول إلى MongoDB Atlas. وانتبه أيضًا إلى الرموز الخاصة التي قد تضعها في كلمة مرورك فستحتاج عندها إلى تشفير الرموز في كلمة المرور عند كتابة عنوان الموقع. يمكنك الاطلاع على حالة قاعدة البيانات على MongoDB Atlas من Collections في النافذة Overview: وكما هو واضح، أضيف مستند يطابق الملاحظة إلى المجموعة notes في قاعدة البيانات التجريبية. توصي توثيقات Mongo بإعطاء أسماء منطقية لقواعد البيانات. ويمكننا تغيير اسم قاعدة البيانات من عنوان الموقع للقاعدة: سندمّر الآن قاعدة البيانات التجريبية بتغيير اسم قاعدة البيانات التي يشير إليها مؤسس الاتصال (connection string) إلى note-app بمجرد تعديل عنوان موقع القاعدة: mongodb+srv://fullstack:<PASSWORD>@cluster0-ostce.mongodb.net/note-app?retryWrites=true سننفذ الشيفرة الآن: لقد خُزِّنت الآن البيانات في القاعدة الصحيحة. يمكن باستخدام create database أن ننشئ قواعد بيانات مباشرة على موقع الويب MongoDB Atlas. لكن لا حاجة لذلك طالما أن الموقع ينشئ تلقائيًا قاعدة بيانات جديدة عندما يحاول التطبيق أن يتصل مع قاعدة بيانات غير موجودة. تخطيط قاعدة البيانات بعد تأسيس الاتصال مع قاعدة البيانات، سنعرف تخطيطًا (Schema) للملاحظة ونموذجًا (Model) مطابقًا للتخطيط: const noteSchema = new mongoose.Schema({ content: String, date: Date, important: Boolean, }) const Note = mongoose.model('Note', noteSchema) عرّفنا أولًا تخطيطًا للملاحظة وأسندناه إلى المتغيّر noteSchema. يخبر التخطيط Mongosse كيف سيُخزّن الكائن الذي يمثل الملاحظة في قاعدة البيانات ثم نعرّف نموذجًا باسم Note يقبل معاملين، الأول هو اسم النموذج المفرد. حيث يختلف الاسم المفرد عن اسم المجموعة بأن الأخير يحمل صيغة الجمع ويكتب بأحرف صغيرة "notes" فهذا عرف تتبعه Mongoose وتقوم به تلقائيًا بينما يشير التخطيط إلى الملاحظات بالاسم المفرد. المعامل الثاني كما هو واضح هو تخطيط الملاحظة. لا تملك قواعد البيانات المستقلة مثل Mongo أية تخطيطات (schemaless). ويعني ذلك أن قاعدة البيانات لا تهتم ببنية البيانات المخزنة فيها، حيث يمكن تخزين ملفات بتخطيطات مختلفة تمامًا ضمن نفس المجموعة. لكن الفكرة وراء منح Mongoose البيانات المخزنة في قاعدة البيانات تخطيطًا على مستوى التطبيق، هي تحديد شكل المستندات المخزنة في مجموعة ما. إنشاء وحفظ الكائنات ينشئ التطبيق في الشيفرة التالية كائن ملاحظة جديد بمساعدة النموذج Note: const note = new Note({ content: 'HTML is Easy', date: new Date(), important: false, }) تدعى النماذج "دوال البناء". فهي التي تنشئ الكائنات الجديدة في JavaScript بناء على المعاملات التي تملكها. وطالما أن الكائنات ستبنى باستخدام الدوال البانية للنماذج، ستمتلك كل خصائص النموذج بما فيها توابع حفظ الكائن في قاعدة البيانات. وتحفظ الكائنات في قواعد البيانات باستخدام التابع save الذي يُزوّد بمعالج حدث ضمن التابع then: note.save().then(result => { console.log('note saved!') mongoose.connection.close() }) بعدما يُحفظ الكائن في قاعدة البيانات، يُستدعَى معالج الحدث المعرّف ضمن التابع then. حيث يغلق معالج الحدث قناة الاتصال مع قاعدة البيانات باستخدام الأمر ()mongoose.connection.close. إن لم يُغلق الاتصال، فلن ينتهي البرنامج من تنفيذ العملية. تُخزّن نتيجة عملية الحفظ في المعامل result لمعالج الحدث، لكنها غير مهمة كثيرًا، خاصة أننا خزّنا كائنًا مفردًا في قاعدة البيانات. يمكنك طباعة الكائن على الطرفية إن أردت التمعن فيه أثناء عمل التطبيق أو أثناء تنقيحه. لنخزّن عدة ملاحظات أخرى في قاعدة البيانات. لكن علينا أولًا تعديل الشيفرة ثم إعادة تنفيذها. ملاحظة: لسوء الحظ، فإن توثيق mongoose ليس مستقرًا جدًا. لقد استخدمت الاستدعاءات في بعض الأجزاء عند عرض الأمثلة بينما استخدمت الأجزاء الأخرى أساليب أخرى. لذلك لا ننصحك بنسخ ولصق الشيفرة مباشرة من تلك التوثيقات. لا تخلط الوعود مع استدعاءات المدرسة التقليدية في نفس الشيفرة فهذا أمر غير محبذ. إحضار كائنات من قاعدة البيانات حَوِّل الشيفرة السابقة التي استخدمناها لإنشاء ملاحظة جديدة إلى تعليقات واستخدم الشيفرة التالية بدلًا منها: Note.find({}).then(result => { result.forEach(note => { console.log(note) }) mongoose.connection.close() }) عندما تُنفّذ الشيفرة سيطبع التطبيق كل الملاحظات الموجودة ضمن قاعدة البيانات: نحصل على الكائنات من قاعدة البيانات مستعملين التابع find العائد للنموذج Note. يقبل التابع السابق معاملًا على هيئة كائن يحتوي على معايير البحث. وطالما أن المعامل في الشيفرة السابقة كائن فارغ ({})، فسنحصل على جميع الملاحظات المخزنة في المجموعة notes. تخضع معايير البحث إلى قواعد الاستعلام في Mongo. ويمكننا تحديد البحث ليشمل مثلًا الملاحظات الهامة فقط على النحو التالي: Note.find({ important: true }).then(result => { // ... }) التمرين 3.12 3.12 قاعدة بيانات بسطر أوامر أنشئ قاعدة بيانات سحابية باستخدام MongoDB لتطبيق دليل الهاتف وذلك على موقع الويب MongoDb Atlas. أنشئ الملف mongo.js في مجلد المشروع، حيث تضيف الشيفرة في الملف مُدخلات إلى دليل الهاتف، وتشكيل قائمة بكل المُدخلات الموجودة في الدليل. ملاحظة: لا تضع كلمة المرور في الملف الذي سترفعه إلى GitHub. يجب أن يعمل التطبيق على النحو التالي: تمرير ثلاثة معاملات إلى سطر الأوامر عند تشغيل التطبيق، على أن تكون كلمة السر هي المعامل الأول كما في المثال التالي: node mongo.js yourpassword Anna 040-1234556 سيطبع التطبيق النتيجة التالية: added Anna number 040-1234556 to phonebook يُخزَّن المُدخل الجديد ضمن قاعدة البيانات. وانتبه إلى وضع الاسم الذي يحتوي على فراغات ضمن قوسي تنصيص مزدوجين: node mongo.js yourpassword "Arto Vihavainen" 045-1232456 إذا شغلت التطبيق بمعامل واحد فقط هو كلمة السر كما يلي: node mongo.js yourpassword على التطبيق عندها أن يعرض كل المُدخَلات في دليل الهاتف: phonebook: Anna 040-1234556 Arto Vihavainen 045-1232456 Ada Lovelace 040-1231236 يمكنك الحصول على معاملات سطر الأوامر من المتغيّر process.argv. ملاحظة: لا تغلق الاتصال في المكان غير المناسب. فلن تعمل على سبيل المثال الشيفرة التالية: Person .find({}) .then(persons=> { // ... }) mongoose.connection.close() سيُنفَّذ الأمر ()mongoose.connection.closeمباشرة بعد أن تبدأ العملية Person.find. أي ستغلق قاعدة البيانات مباشرة قبل أن تنتهي العملية السابقة وتستدعى دالة معالج الحدث. لذلك فالمكان الصحيح لإغلاق قاعدة البيانات سيكون في نهاية معالج الحدث: Person .find({}) .then(persons=> { // ... mongoose.connection.close() } ) ملاحظة: إذا سميت النموذج person، ستسمي mongoose المجموعة المقابلة people. ربط الواجهة الخلفية مع قاعدة البيانات لقد تعلمنا ما يكفي لبدء استخدام Mongo في تطبيقنا. لننسخ ونلصق القيم التي عرفناها في التطبيق التجريبي، ضمن الملف index.js: const mongoose = require('mongoose') // DO NOT SAVE YOUR PASSWORD TO GITHUB!! const url = 'mongodb+srv://fullstack:sekred@cluster0-ostce.mongodb.net/note-app?retryWrites=true' mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true }) const noteSchema = new mongoose.Schema({ content: String, date: Date, important: Boolean, }) const Note = mongoose.model('Note', noteSchema) لنغير معالج حدث إحضار كل الملاحظات إلى الشكل التالي: app.get('/api/notes', (request, response) => { Note.find({}).then(notes => { response.json(notes) }) }) يمكن أن نتحقق من عمل الواجهة الخلفية بعرض كل الملاحظات على المتصفح: يعمل التطبيق بشكل ممتاز الآن. تفترض الواجهة الأمامية أن لكل كائن معرفّا فريدًا id في الحقل id. وانتبه إلى أننا لا نريد إعادة حقل إصدار mongo إلى الواجهة الأمامية (v__). إحدى الطرق لنتحكم بصيغة الكائن الذي سنعيده، هي تعديل تخطيط الكائن باستخدام التابع toJSON والذي سنستعمله في كل النماذج التي تُنشأ اعتمادًا على هذا التخطيط. سيكون التعديل على النحو: noteSchema.set('toJSON', { transform: (document, returnedObject) => { returnedObject.id = returnedObject._id.toString() delete returnedObject._id delete returnedObject.__v } }) حتى لو بدت الخاصية (id__) لكائن Mongoose كسلسلة نصية فهي في الواقع كائن. لذلك يحولها التابع toJSON إلى سلسلة نصية حتى نتأكد أنها آمنة. إن لم نفعل ذلك ستظهر المشاكل أمامنا بمجرد أن نبدأ كتابة الاختبارات. ستجيب الواجهة الخلفية على طلب HTTP بقائمة من الكائنات التي أعيدت صياغتها باستخدام التابع toJSON: app.get('/api/notes', (request, response) => { Note.find({}).then(notes => { response.json(notes) }) }) أسندت مصفوفة الكائنات التي أعادتها Mongo إلى المتغير notes. وعندما ترسل الاستجابة بصيغة JSON، يستدعى التابع toJSON تلقائيًا من أجل كل كائن من المصفوفة باستخدام التابع JSON.stringify. تهيئة قاعدة البيانات في وحدة خاصة بها قبل أن نعيد كتابة بقية تطبيق الواجهة الخلفية، لنضع الشيفرة الخاصة بالتعامل مع Mongoose في وحدة مستقلة خاصة بها. لذلك سننشئ مجلدًا جديدًا للوحدة يدعى models ونضيف إليه ملفًا باسم note.js: const mongoose = require('mongoose') const url = process.env.MONGODB_URI console.log('connecting to', url) mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true }) .then(result => { console.log('connected to MongoDB') }) .catch((error) => { console.log('error connecting to MongoDB:', error.message) }) const noteSchema = new mongoose.Schema({ content: String, date: Date, important: Boolean, }) noteSchema.set('toJSON', { transform: (document, returnedObject) => { returnedObject.id = returnedObject._id.toString() delete returnedObject._id delete returnedObject.__v } }) module.exports = mongoose.model('Note', noteSchema) يختلف تعريف الوحدات في Node قليلًا عن الطريقة التي عرفنا فيها وحدات ES6 في القسم 2. فتُعرّف الواجهة العامة للوحدة بإسناد قيمة للمتغيّر module.exports. سنسند له إذًا النموذج Note. لن يتمكن مستخدم الوحدة من الوصول أو رؤية الأشياء المعرفة داخلها، كالمتغيرات وmonogose وعنوان موقع القاعدة url. يجري إدراج الوحدة بإضافة السطر التالي إلى الملف index.js: const Note = require('./models/note') وهكذا سيُسند المتغيّر Note إلى نفس الكائن الذي تعرّفه الوحدة. تغيّرت قليلًا الطريقة التي نجري بها الاتصال مع قاعدة البيانات: const url = process.env.MONGODB_URI console.log('connecting to', url) mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true }) .then(result => { console.log('connected to MongoDB') }) .catch((error) => { console.log('error connecting to MongoDB:', error.message) }) const noteSchema = new mongoose.Schema({ content: String, date: Date, important: Boolean, }) noteSchema.set('toJSON', { transform: (document, returnedObject) => { returnedObject.id = returnedObject._id.toString() delete returnedObject._id delete returnedObject.__v } }) module.exports = mongoose.model('Note', noteSchema) لا تكتب عنوان الموقع لقاعدة البيانات بشكل مسبق في الشيفرة فهذه فكرة سيئة. بل مرر العنوان إلى التطبيق عبر متغيّر البيئة MONGODB_URI. تقدم الطريقة التي اتبعناها في تأسيس الاتصال دالتين للتعامل مع حالتي نجاح الاتصال وفشله. حيث تطبع كلتا الدالتين رسائل إلى الطرفية لوصف حالة الاتصال. ستجد طرقًا عديدة لتعريف قيمة متغيّر البيئة، إحداها أن تعرّفه عندما تشغل التطبيق: MONGODB_URI=address_here npm run dev الطريقة الأخرى الأكثر تعقيدًا هي استخدام المكتبة dotenv التي يمكنك تثبيتها بتنفيذ الأمر: npm install dotenv --save عليك إنشاء ملف لاحقته env. عند جذر المشروع، ومن ثم تعرف متغيرات البيئة داخله. سيبدو الملف بالشكل التالي: MONGODB_URI='mongodb+srv://fullstack:sekred@cluster0-ostce.mongodb.net/note-app?retryWrites=true' PORT=3001 لاحظ أننا حددنا رقم المنفذ بشكل مسبق ضمن متغير البيئة PORT. ملاحظة تَجاهل الملف ذو اللاحقة "env."، لأننا لا نريد أن ننشر معلومات التوثيق الخاصة بنا في العلن. نستخدم متغيرات البيئة التي عرّفناها في الملف env. بكتابة العبارة ()require('dotenv').config، ثم يمكنك بعدها الإشارة إليهم في الشيفرة بالطريقة المعهودة process.env.MONGODB_URI. لنغيّر الملف index.js على النحو التالي: require('dotenv').config()const express = require('express') const app = express() const Note = require('./models/note') // .. const PORT = process.env.PORTapp.listen(PORT, () => { console.log(`Server running on port ${PORT}`) }) من المهم إدراج المكتبة dotenv قبل إدراج الوحدة note، لكي نضمن أن متحولات البيئة التي عرّفت داخلها ستكون متاحة للاستخدام ضمن كامل الشيفرة، وذلك قبل إدراج بقية الوحدات. استخدام قاعدة البيانات مع معالجات المسار لنغيّر بقية الوظائف في تطبيق الواجهة الخلفية ليتعامل مع قواعد البيانات. تضاف ملاحظة جديدة كالتالي: app.post('/api/notes', (request, response) => { const body = request.body if (body.content === undefined) { return response.status(400).json({ error: 'content missing' }) } const note = new Note({ content: body.content, important: body.important || false, date: new Date(), }) note.save().then(savedNote => { response.json(savedNote) }) }) تنشئ الدالة البانية للنموذج Note الكائنات التي تمثل الملاحظات. ثم ترسل الاستجابة داخل دالة استدعاء التابع save. يضمن هذا أن الاستجابة لن تُعاد إلى المرسل إن لم تنجح العملية. وسنناقش بعد قليل آلية التعامل مع الأخطاء. يحمل معامل دالة الاستدعاء savedNote الملاحظة الجديدة التي أنشئت. وتذكر أن البيانات التي أعيدت في الاستجابة قد أعيد تنسيقها باستعمال التابع toJSON. response.json(savedNote) تغيرت طريقة إحضار الملاحظات المفردة لتصبح على النحو: app.get('/api/notes/:id', (request, response) => { Note.findById(request.params.id).then(note => { response.json(note) }) }) التحقق من تكامل أداء الواجهتين الخلفية والأمامية من الجيد اختبار التطبيق الذي يعمل على الواجهة الخلفية عندما نضيف إليه وظائف جديدة، يمكن أن نستعمل في الاختبار برنامج Postman أو VS Code REST client أو المتصفح الذي تستخدمه. لننشئ ملاحظة جديدة ونخزّنها في قاعدة البيانات: بعد أن نتأكد من أن كل شيء يعمل على ما يرام في الواجهة الخلفية، نختبر تكامل الواجهة الأمامية مع الخلفية. فمن غير الكافي إطلاقًا اختبار الأشياء ضمن الواجهة الأمامية فقط. ربما عليك أن تدرس تكامل الوظائف بين الواجهتين وظيفة تلو الأخرى. فيمكننا أولًا إضافة الشيفرة التي تحضر كل البيانات من قاعدة البيانات ونختبرها على طرفية الواجهة الخلفية ضمن المتصفح. بعدها نتأكد أن الواجهة الأمامية تعمل جيدًا مع الشيفرة الجديدة للواجهة الخلفية. عندما يجري كل شيء بشكل جيد ننتقل إلى الوظيفة التالية. علينا تفقد الحالة الراهنة لقاعدة البيانات بمجرد بدأنا العمل معها. يمكننا القيام بذلك على سبيل المثال، عبر لوحة التحكم في MongoDB Atlas. كما ستفيدك أثناء التطوير بعض برامج Node الصغيرة، كالبرنامج mongo.js الذي كتبناه في هذا الفصل. ستجد شيفرة التطبيق بشكله الحالي في المستودع part3-4 على موقع GitHub. التمارين 3.13 - 3.14 يمثل التمرينين التاليين تطبيقًا مباشرًا وسهلًا لما تعلمناه. لكن إن لم تتكامل الواجهتين معًا أثناء العمل، فستكمن الأهمية في كيفية إيجاد ومعالجة الأخطاء التي سببت ذلك. 3.13 دليل هاتف بقاعدة بيانات: الخطوة 1 غيِّر طريقة إحضار جميع المُدخَلات لكي يتم ذلك من قاعدة بيانات. تأكد أن الواجهة الأمامية ستعمل بعد إجراء هذه التغييرات. سنتكتب شيفرة التعامل مع قاعدة البيانات MongoDB في وحدة خاصة بها خلال التمارين القادمة، كما فعلنا سابقًا في هذا الفصل (تهيئة قاعدة البيانات في وحدة خاصة بها). 3.14 دليل هاتف بقاعدة بيانات: الخطوة 2 غيّر في شيفرة الواجهة الخلفية بحيث تحفظ الأرقام في قاعدة البيانات، وتحقق أن الواجهة الأمامية ستعمل بشكل جيد بعد التغييرات في الواجهة الخلفية. يمكنك في هذه المرحلة أن تجعل المستخدمين يدخلون كل ما يشاؤون في دليل الهاتف. ولاحظ أن دليل الهاتف قد يحوي الاسم نفسه مكررًا مرات عدة. معالجة الأخطاء لو حاولنا الوصول إلى موقع ملاحظة بمعرّف id غير موجود. فستكون الإجابة Null (لا شيء). لنغير ذلك بحيث يستجيب الخادم على هذا الطلب برمز الحالة 404 (غير موجود). سنضيف أيضًا كتلة catch لتتعامل مع الحالات التي يُرفض فيها الوعد الذي يعيده التابع findById: app.get('/api/notes/:id', (request, response) => { Note.findById(request.params.id) .then(note => { if (note) { response.json(note) } else { response.status(404).end() } }) .catch(error => { console.log(error) response.status(500).end() }) }) إن لم يُعثر على تطابق في قاعدة البيانات، ستكون قيمة الكائن note هي null، وبالتالي ستنفذ كتلة else. سينتج عن ذلك استجابة برمز الحالة 404 (غير موجود). وأخيرًا، إن رفض الوعد الذي يعيده التابع findById، ستكون الاستجابة برمز الحالة 500 (خطأ داخلي في الخادم). ستعرض لك طرفية التطوير معلومات مفصلة أكثر عن الخطأ. بالإضافة إلى الخطأ الناتج عن عدم وجود ملاحظة، ستواجه حالة أخرى يتوجب عليك معالجتها. تتلخص هذه الحالة بمحاولة إحضار ملاحظة بمعرّف id من نوع خاطئ لا يطابق تنسيق Mongo للمعرفات IDs. فلو ارتكبنا خطأً كهذا، سنرى الرسالة التالية: Method: GET Path: /api/notes/someInvalidId Body: {} --- { CastError: Cast to ObjectId failed for value "someInvalidId" at path "_id" at CastError (/Users/mluukkai/opetus/_fullstack/osa3-muisiinpanot/node_modules/mongoose/lib/error/cast.js:27:11) at ObjectId.cast (/Users/mluukkai/opetus/_fullstack/osa3-muisiinpanot/node_modules/mongoose/lib/schema/objectid.js:158:13) ... إن إعطاء معرّف id بصيغة خاطئة، سيدفع التابع findById لإشهار خطأ يسبب رفضًا للوعد. كنتيجة لذلك ستُستدعى الدالة الموجودة في الكتلة catch. لنغيّر طريقة الاستجابة قليلًا في تلك الكتلة: app.get('/api/notes/:id', (request, response) => { Note.findById(request.params.id) .then(note => { if (note) { response.json(note) } else { response.status(404).end() } }) .catch(error => { console.log(error) response.status(400).send({ error: 'malformatted id' }) }) }) إن لم يكن تنسيق المعرف id صحيحًا، ستنتهي العملية بتنفيذ معالج الخطأ الموجود في catch. إن الاستجابة الملائمة لهذا الخطأ هو رمز الحالة 400 (طلب خاطئ) 400 Bad Request، لأن هذه الحالة تطابق تمامًا الوصف التالي: أضفنا أيضًا بعض البيانات إلى الاستجابة لنلقي الضوء على سبب الخطأ. يفضل دائمًا عند التعامل مع الوعود إضافة معالجات للأخطاء والاستثناءات، لأن إهمال ذلك سيدفعك لمواجهة أخطاء غريبة. ومن الجيد أيضًا طباعة الكائن الذي سبب الاستثناء على طرفية التطوير: .catch(error => { console.log(error) response.status(400).send({ error: 'malformatted id' }) }) قد يُستدعى معالج الخطأ التي زرعته، نتيجة لخطأ مختلف تمامًا عن الخطأ الذي تريد اعتراضه. فإن طبعت الخطأ على الطرفية ستوفر على نفسك عناء جلسات التنقيح المحبطة. وعلاوة على ذلك تزودك معظم الخدمات الحديثة بوسيلة ما لطباعة عمليات النظام، بحيث يمكنك الاطلاع عليها متى أردت والخادم Heroku مثال مهم عليها. وطالما أنك تعمل على الواجهة الخلفية فابق نظرك على الطرفية التي تظهر لك خرج العملية حتى لو عملت على شاشة صغيرة، سيلفت وقوع أي خطأ انتباهك. تحويل معالجات الأخطاء إلى أداة وسطية لقد كتبنا شيفرة ملاحقة الأخطاء ضمن بقية أجزاء الشيفرة، ويبدو الأمر معقولًا أحيانًا، لكن من الأفضل إضافة معالجات الأخطاء في مكان واحد. سيكون هذا الأمر مفيدًا إذا أردنا لاحقًا أن نقدم تقريرًا عن الأخطاء إلى منظومة تتبع أخطاء خارجية مثل Sentry. لنغيّر معالج المسار api/notes/:id/، بحيث يمرر الخطأ إلى الدالة next كمعامل، وتمثل هذه الدالة بدورها المعامل الثالث لمعالج المسار: app.get('/api/notes/:id', (request, response, next) => { Note.findById(request.params.id) .then(note => { if (note) { response.json(note) } else { response.status(404).end() } }) .catch(error => next(error))}) عندما تستدعى next دون معامل، سينتقل التنفيذ بكل بساطة إل المسار أو الأداة الوسطية التالية. بينما لو امتلكت هذه الدالة معاملًا فسيتابع التنفيذ إلى الأداة الوسطية لمعالجة الخطأ. معالجات أخطاء المكتبة Express هي أدوات وسطية تعرّف على شكل دالة تقبل أربع معاملات. سيبدو معالج الخطأ بالشكل التالي: const errorHandler = (error, request, response, next) => { console.error(error.message) if (error.name === 'CastError') { return response.status(400).send({ error: 'malformatted id' }) } next(error) } app.use(errorHandler) يتحقق المعالج من طبيعة الخطأ. فإن كان الاستثناء هو خطأ تحويل نوع (CastError)، سنتأكد أن مصدر الخطأ هو كائن id بتنسيق مخالف لقواعد Mongo. وعندها سيرسل معالج الخطأ استجابته إلى المتصفح عبر كائن الاستجابة response الذي يمرر كمعامل للمعالج. أما في بقية الاستثناءات، فسيمرر المعالج الخطأ إلى معالج الخطأ الافتراضي في express. تسلسل استخدام الأدوات الوسطية تنفذ الأدوات الوسطية بنفس تسلسل استخدامها في express، أي بنفس تسلسل ظهور الأمر app.use. لذلك ينبغي الانتباه أثناء تعريفها. سيكون التسلسل الصحيح للاستخدام كالتالي: app.use(express.static('build')) app.use(express.json()) app.use(logger) app.post('/api/notes', (request, response) => { const body = request.body // ... }) const unknownEndpoint = (request, response) => { response.status(404).send({ error: 'unknown endpoint' }) } // handler of requests with unknown endpoint app.use(unknownEndpoint) const errorHandler = (error, request, response, next) => { // ... } // handler of requests with result to errors app.use(errorHandler) يجب أن تضع الأداة الوسطية json-parser بين الأدوات الوسطية التي تعرّف أولًا. فلو كان الترتيب كالتالي: app.use(logger) // request.body is undefined! app.post('/api/notes', (request, response) => { // request.body is undefined! const body = request.body // ... }) app.use(express.json()) لن تكون البيانات المرسلة عبر طلب HTTP بصيغة JSON متاحة للاستخدام عبر الأداة الوسطية للولوج أو عبر معالج المسار POST، لأن الخاصية request.body ستكون غير محددة undefined في هذه المرحلة. ومن المهم جدًا أن تُعرّف الأداة الوسطية التي تعالج مشكلة المسارات غير المدعومة ضمن التعريفات الأخيرة، تمامًا قبل معالج الأخطاء. سيسبب الترتيب التالي على سبيل المثال مشكلة: const unknownEndpoint = (request, response) => { response.status(404).send({ error: 'unknown endpoint' }) } // handler of requests with unknown endpoint app.use(unknownEndpoint) app.get('/api/notes', (request, response) => { // ... }) لقد ظهر معالج النهاية غير المحددة (unknown endpoint) قبل معالج طلبات HTTP. وطالما أن معالج النهايات غير المحددة سيستجيب إلى كافة الطلبات بالرمز 404 unknown endpoint، فلن يُستدعى بعدها أي مسار أو أداة وسطية. ويبقى الاستثناء الوحيد هو معالج الخطأ الذي يجب أن يأتي أخيرًا بعد معالج النهايات غير المحددة. خيارات أخرى سنضيف الآن بعض الوظائف التي لم ندرجها بعد، بما فيها الحذف وتحديث ملاحظة مفردة. وأسهل الطرق لحذف ملاحظة من قاعدة البيانات هي استخدام التابع findByIdAndRemove: app.delete('/api/notes/:id', (request, response, next) => { Note.findByIdAndRemove(request.params.id) .then(result => { response.status(204).end() }) .catch(error => next(error)) }) سيستجيب الخادم في كلتا حالتي نجاح عملية الحذف برمز الحالة 202 (لا يوجد محتوى). ونقصد بالحالتين، حذف ملاحظة موجودة فعلًا، أو حذف ملاحظة غير موجودة. يمكن أن نستخدم المعامل result للتحقق من حذف المورد أم لا، وبالتالي سنتمكن من تحديد أي من حالتي النجاح قد أعيدت إن كنا بحاجة ماسة لذلك. ستمرر كل الاستثناءات إلى معالج الأخطاء. يمكن بسهولة تغيير أهمية الملاحظة باستخدام التابع findByIdAndUpdate: app.put('/api/notes/:id', (request, response, next) => { const body = request.body const note = { content: body.content, important: body.important, } Note.findByIdAndUpdate(request.params.id, note, { new: true }) .then(updatedNote => { response.json(updatedNote) }) .catch(error => next(error)) }) تسمح الشيفرة السابقة أيضًا بتعديل محتوى الملاحظة لكنها لا تدعم تغيير تاريخ الإنشاء. ولاحظ كيف يتلقى التابع findByIdAndUpdate معاملًا على هيئة كائن JavaScript نظامي، وليس كائن ملاحظة أنشئ بواسطة الدالة البانية للنموذج Note. يبقى لدينا تفصيل مهم يتعلق بالتابع findByIdAndUpdate. فمعامل معالج الحدث upDateNote سيستقبل الملف الأصلي للملاحظة بلا تعديلات. لذلك وضعنا المعامل الاختياري {new: true}، الذي يسبب استدعاء معالج الحدث بالنسخة المعدَّلة من الملاحظة بدلًا من الأصلية. بعد اختبار الواجهة الخلفية مباشرة مستخدمين Postman أو VS Code REST client، يمكننا التحقق من أنها تعمل بشكل صحيح. وكذلك التحقق من أن الواجهة الأمامية تتكامل مع الخلفية التي تستخدم قاعدة البيانات. لكن عندما نحاول تغيير أهمية ملاحظة، تستظهر على الطرفية رسالة الخطأ التالية: استخدم google للبحث عن سبب الخطأ وسيقودك إلى إرشادات لتصحيحه. واتباعًا للاقتراح الموجود في توثيق Mongoose، أضفنا السطر التالي للملف note.js: const mongoose = require('mongoose') mongoose.set('useFindAndModify', false) // ... module.exports = mongoose.model('Note', noteSchema) ستجد شيفرة التطبيق بشكله الحالي في المستودع part3-5 على موقع GitHub. التمارين 3.15 - 3.18 3.15 دليل هاتف بقاعدة بيانات: الخطوة 3 عدّل شيفرة الواجهة الخلفية لتحذف المُدخَلات مباشرة من قاعدة البيانات. تحقق أن الواجهة الأمامية تعمل بشكل صحيح بعد التعديلات. 3.16 دليل هاتف بقاعدة بيانات: الخطوة 4 حوّل معالج الخطأ في التطبيق إلى أداة وسطية جديدة لمعالجة الأخطاء. 3.17 دليل هاتف بقاعدة بيانات: الخطوة 5 * إذا حاول المستخدم إنشاء مُدخَل جديد إلى الدليل، وكان اسم الشخص موجودًا مسبقًا، ستحاول الواجهة الأمامية تحديث رقم الهاتف للمُدخَل الموجود بإرسال طلب HTTP-PUT إلى عنوان المُدخَل. عدّل الواجهة الخلفية لتدعم هذا الفعل، ثم تأكد أن الواجهة الأمامية ستعمل بعد التعديل. 3.18 دليل هاتف بقاعدة بيانات: الخطوة 6 * عدّل معالج المسار api/persons/:id ومعالج المسار api/persons/info ليستخدما قاعدة البيانات، ثم تحقق من أنهما يعملان جيدًا مستخدمًا المتصفح وPostman وVS Code REST client. سيبدو لك الأمر عند التحقق من مُدخَل فردي إلى دليل الهاتف كما في الشكل التالي: ترجمة -وبتصرف- للفصل saving data to MongoDB من سلسلة Deep Dive Into Modern Web Development1 نقطة
-
السلام عليكم أختي من النظرة الأولى السريعة ليس بطيئاً جداً قمت بالإطلاع على الكونسول في خطأ واحد وهو متعلق بالدالة isContentEditable لا أظن أن له تأثيراً كبيراً ولكنني انصحك بحله. ولكن عندما قمت بعمل website speed test وجدت ان سرعته على الجوال هي المشكلة حيث أن سرعته على الحاسوب تبلغ 85 وهي سرعة لا بأس بها على الرغم من حاجتها للتحسين أما سرعته في الجوال فتبلغ 32 وهي بطيئة جداً. و كانت نتائج الاختبار تعطي النصائح التالية: ازالة اكواد الجافاسكربت التي لا حاجة لها فهي لوحدها تستهلك 2.85 ثانية عند فتح الموقع بالجوال وهي تتمثل في الاتي : https://www.youtube.com/s/player/9f996d3e/player_ias.vflset/en_US/base.js وسيقوم بحفظ 363.7 KiB https://pagead2.googlesyndication.com/pagead/js/r20210112/r20190131/show_ads_impl_fy2019.js وسيقوم بحفظ 70KiB 2. تغيير الtheme المستخدم والذي يستهلك 0.58 ثانية للإستجابة. 3.ازالة أكواد ال CSS الغير مستخدمة . بعد القيام بذلك يمكنك إجراء اختبار لسرعة الموقع مرة أخرى من هنا1 نقطة
-
1 نقطة
-
1 نقطة
-
صفحات الويب التي تنتهي باللاحقة html تحتوي على كود يصف وينسق ويوفر التفاعل في الصفحة التي تظهر للمستخدم ولا يمكن تضمين قاعدة بيانات فيها. للتعامل مع قواعد البيانات والخادم تحتاج للغة أخرى مثل PHP أو Python. إذ أن لغة HTML تستخدم لهيكلة صفحات الويب و CSS لتنسيقها وأخيراً javascript لإضافة الديناميكية والتفاعل مع المستخدم. إليك الصورة التالية التي توضح سير عملية تصفح موقع إخباري مثلاً. يدخل المستخدم رابط الموقع الإخباري التالي www.example.com يقوم الweb server بإستقبال الطلب وتنفيذ كود PHP لجلب آخر الأخبار من قاعدة البيانات. يرسل الweb server ناتج تنفيذ ملف الPHP للمستخدم وهو عبارة عن كود HTML يفسره المتصفح ويعرضه للمستخدم. إذاً فالمسؤول هنا عن التعامل مع الخادم وقواعد البيانات هي لغة الـPHP وليس HTML إذ أن الأخيرة تستخدم فقط لهيكلة صفحات الويب.1 نقطة
-
السلام عليكم أخي اولاً من اين يمكنك تعلم الlaravel : من خلال التوثيق الرسمي الخاص بlaravel من هنا . او من خلال الدورات الخاصة باللارافيل. ثانياً : بالنسبة للطريقة الصحيحة لتعلم اللارافيل يجب عليك اولاً أن تكون على علم ودراية بلغة php. ثم لا تقم بتعلم الفريمورك كاملاً ثم البدء بممارسته عليك ان تمارس الجزئيات الصغيرة اولاً بأول حتى ترسخ عندك. احرص على قراءة اكواد كتبها غيرك فهذا يجعلك تتعلم بسرعة اكبر ويضيف لك خبرة وتجارب الاخرين. احرص على مساعدة الاخرين وتعليمهم ما تعلمت وبهذا ستكون قد رسخت معرفتك. بالنسبة الى أمر النسيان او العودة بعد الانشغال: لا عليك فأنت غير مطالب بأن تحفظ اي شيء ومراجعة بسيطة للقواعد الاساسية في نصف ساعة كفيلة بأن تعيد الى ذهنك ما تعلمته. لذلك احرص على التركيز على الفهم وليس الحفظ.1 نقطة
-
يعطيك العافية. لاحظت خطأ بسيط في المثال الذي الموضوع على الكلمة المفتاحية class حيث يجب كتابة: class ExampleClass بدلاً من: classExampleClass أقصد يجب إضافة مسافة فارغة فقط.1 نقطة
-
you can by looping throught the array and for every element by using one of the fallowing methods : .match or startwith() or look online for other methods and return the element that match the condition to a new array1 نقطة
-
السلام عليكم ورحمة الله نصيحتي هي أن تقرأي كتاب " كيف يتعلم المبرمج بشكل صحيح" للمؤلف :وجدي عصام عبد الرحيم الكتيب يتألف من 33 صفحة ، لكن غني بالنصائح والمعلومات المفيدة لبدأ تعلم البرمجة . بالتوفيق إن شاء الله1 نقطة
-
أظن أن قراءة كتاب خاص بالبرمجة شيء قديم وأمر ممل نسبيا, ففي مجال البرمجة هنالك لكل لغة برمجة موقع خاص بها لشرحها بكل تفاصيلها وهذا سيكون كمرجع لكي في حال نسيتي شيئا ما بعد أن تقومي بتعلم ما تريدين تعلمه وهو ايضا جيد للتعلم في حال قرأتي منه حيث أن مثلا الكود المشروح يمكنكي تطبيقه وتجربته والتعديل عليه في نفس الوقت. صحيح أن الكتب هي المصدر الأساسي للتعلم ولكن هنالك ما هو أسهل من ذلك في هذا الوقت حيث توجد وكما ذكرت أعلاه المواقع المخصصة للغة معينة أو الوثيقة الرسمية للغة البرمجة, الدورات ,المقالات ومجموعات مواقع التواصل الاجتماعي حيث ان هنالك العديد من الصفحات والمجموعات تقدم بعض المعلومات الجميلة والقيمة. وأنا أنصحك بدورة التي بعنوان CS50 المقدمة من جامعة هارفرد فهي تشرح العديد من الأمور اللازم تعلمها في مجال البرمجة ولكن هذه الدورة باللغة الانجليزية ولكن هنالك من قام بشرحها باللغة العربية. وبما أنكِ ذكرتِ أنكِ تعلمتي واجهات الويب يجب عليكي تطبيق بعض المشاريع في هذا المجال ثم يمكنك بعد ذلك الانتقال إلى تطوير الويب حتى تكتمل خبرتك في مجال الويب والتي تحتوي على البرمجة التي تبحثين عنها.1 نقطة
-
يوجد خطأ إملائي في العنوان: إضافة جانغو الموفرة للوقت: Django-extensions التصحيح: يجب تبديل كلمة "إضافة" بكلمة "إضافات".1 نقطة