-
المساهمات
189 -
تاريخ الانضمام
-
تاريخ آخر زيارة
نوع المحتوى
ريادة الأعمال
البرمجة
التصميم
DevOps
التسويق والمبيعات
العمل الحر
البرامج والتطبيقات
آخر التحديثات
قصص نجاح
أسئلة وأجوبة
كتب
دورات
كل منشورات العضو Ola Abbas
-
حان الوقت الآن للتعمق أكثر في إطار العمل Vue وإنشاء مكوّن مخصَّص، إذ سنبدأ بإنشاء مكوِّن لتمثيل كل عنصر في قائمة المهام، كما سنتعرّف على بعض المفاهيم المهمة مثل استدعاء المكونات ضمن مكونات أخرى وتمرير البيانات إليها باستخدام الخاصيات Props وحفظ حالة البيانات. ملاحظة: إذا كنت بحاجة إلى التحقق من شيفرتك مقابل نسخة شيفرتنا، فيمكنك العثور على نسخة نهائية من نموذج شيفرة تطبيق Vue في المستودع todo-vue أو يمكنك الحصول على إصدار حي وقيد التشغيل منه. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ تُكتَب مكونات Vue بوصفها مجموعة من كائنات جافاسكربت التي تدير بيانات التطبيق وصيغة القوالب المستنِدة إلى لغة HTML المرتبطة مع بنية DOM الأساسية، وستحتاج إلى طرفية مثبَّت عليها node و npm لتثبيت Vue ولاستخدام بعض ميزاته الأكثر تقدمًا مثل مكونات الملف المفرد Single File Components أو دوال التصيير Render. الهدف: تعلّم كيفية إنشاء مكوِّن Vue وتصييره ضمن مكوِّن آخر، وتمرير البيانات إليه باستخدام الخاصيات، وحفظ حالته. إنشاء المكون ToDoItem لننشئ مكوننا الأول الذي سيعرض عنصر مهمة واحدًا، إذ سنستخدمه لبناء قائمة المهام. أنشئ ملفًا جديدًا بالاسم ToDoItem.vue في المجلد moz-todo-vue/src/components، ثم افتح الملف في محرّر الشيفرات. أنشئ قسم قالب المكوِّن من خلال إضافة العنصر <template></template> في أعلى الملف. أنشئ القسم <script></script> بعد قسم القالب، ثم أضف فيه كائن تصدير افتراضي export default {}، وهو كائن مكوِّنك. يجب أن يبدو ملفك الآن كما يلي: <template> </template> <script> export default {}; </script> يمكننا الآن إضافة محتوى فعلي إلى الملف ToDoItem، إذ يُسمَح حاليًا لقوالب Vue بإضافة عنصر جذر واحد فقط، كما يجب استخدام عنصر واحد لتغليف كل شيء ضمن قسم القالب، ولكن سيتغير ذلك في الإصدار رقم 3 من Vue،إذ سنستخدِم العنصر <div> لهذا العنصر الجذر. أضف عنصر <div> فارغ ضمن قالب المكوِّن. أضِف مربع اختيار وعنوانًا label مقابلًا ضمن العنصر <div>. أضِف السمة id إلى مربع الاختيار، والسمة for التي تربط مربع الاختيار مع العنوان label كما هو موضّح فيما يلي: <template> <div> <input type="checkbox" id="todo-item" /> <label for="todo-item">My Todo Item</label> </div> </template> استخدام المكون TodoItem ضمن تطبيقك لم نضِف المكوِّن إلى تطبيقنا حتى الآن، لذلك لا توجد طريقة لاختباره ومعرفة ما إذا كان كل شيء على ما يرام أم لا، إذًا لنضفه الآن. افتح الملف App.vue مرة أخرى. أضِف السطر التالي لاستيراد المكون ToDoItem في أعلى الوسم <script>: import ToDoItem from './components/ToDoItem.vue'; أضِف الخاصية components ضمن كائن المكوِّن ثم أضِف ضمنها المكوِّن ToDoItem لتسجيله. يجب أن تبدو محتويات الوسم <script> الآن كما يلي: import ToDoItem from './components/ToDoItem.vue'; export default { name: 'app', components: { ToDoItem } }; هذه هي الطريقة نفسها التي سُجِّل بها المكوِّن HelloWorld بواسطة واجهة CLI الخاصة بإطار العمل Vue سابقًا. يمكنك تصيير المكوِّن ToDoItem فعليًا في التطبيق من خلال الانتقال إلى العنصر <template> واستدعائه بوصفه عنصر بالشكل: <to-do-item></to-do-item> ولاحظ أنّ اسم ملف المكوِّن وتمثيله في جافاسكربت يكون دائمًا في حالة أحرف باسكال PascalCase مثل ToDoList، ويكون العنصر المخصّص المكافئ دائمًا في نمط أحرف أسياخ الشواء Kebab-case مثل <to-do-list>. أنشئ قائمةً غير مرتبة <ul> بعد العنصر <h1> تحتوي على عنصر قائمة واحد <li>. أضِف العنصر <to-do-item></to-do-item> ضمن عنصر القائمة. يجب أن تبدو محتويات العنصر <template> في الملف App.vue الآن كما يلي: <div id="app"> <h1>To-Do List</h1> <ul> <li> <to-do-item></to-do-item> </li> </ul> </div> إذا تحقّقتَ من تطبيقك المُصيَّر مرةً أخرى، فيجب أن ترى الآن العنصر ToDoItem المُصيَّر الذي يتكون من مربع اختيار وعنوان label. إضافة خاصيات للمكونات لا يزال المكوِّن ToDoItem غير مفيد للغاية لأنه يمكننا تضمينه مرةً واحدة فقط في الصفحة، إذ يجب أن تكون المعرّفات فريدةً، وليس لدينا طريقة لضبط نص العنوان ولا يُعَدّ ذلك ديناميكيًا. ما نحتاجه الآن هو حالة المكوِّن التي يمكن تحقيقها من خلال إضافة الخاصيات Props إلى المكوِّن، إذ تشبه الخاصيات مدخلات دالة ما، وتعطي قيمة الخاصية للمكونات حالة أولية تؤثر على عرضها. تسجيل الخاصيات هناك طريقتان لتسجيل الخاصيات في Vue هما: الطريقة الأولى هي سرد الخاصيات بوصفها مصفوفةً من السلاسل النصية فقط، إذ تقابل كل مدخلة في المصفوفة اسم خاصية. الطريقة الثانية هي تعريف الخاصيات بوصفها كائنات، إذ يقابل كل مفتاح اسم الخاصية، كما يسمح سرد الخاصيات بوصفها كائنات بتحديد القيم الافتراضية وتحديد الخاصيات بأنها إجبارية وتطبيق أنواع الكائن الأساسية (أنواع جافاسكربت الأولية تحديدًا)، بالإضافة إلى إجراء تحقق بسيط من صحة الخاصية. ملاحظة: لا يحدث التحقق من صحة الخاصية إلا في وضع التطوير، لذلك لا يمكنك الاعتماد عليها بصورة صارمة في عملية الإنتاج، كما تُستدعَى دوال التحقق من صحة الخاصية قبل إنشاء نسخة من المكوِّن، لذلك لا يمكنها الوصول إلى حالة المكوِّن أو الخاصيات الأخرى، وسنستخدِم تابع تسجيل الكائن بالنسبة لهذا المكوِّن. ارجع إلى الملف ToDoItem.vue، ثم أضِف الخاصية props ضمن كائن التصدير default {} الذي يحتوي على كائن فارغ، وبعدها أضف ضمن هذا الكائن خاصيتين مع المفتاحين label و done، وتذكَّر أنّ قيمة المفتاح label يجب أن تكون كائنًا مع خاصيتن (أو Props كما يطلق عليها في سياق توفرهما للمكوّنات) وهما: الأولى هي الخاصية required التي ستكون لها القيمة trueمما يخبر Vue أننا نتوقع أن يكون لكل نسخة من هذا المكوِّن حقل عنوان أو تسمية label، وسيعطي Vue تحذيرًا إذا لم يتضمن المكوِّن ToDoItem على حقل عنوان. الثانية هي خاصية النوع type. اضبط قيمة هذه الخاصية على نوع جافاسكربت String (لاحظ الحرف الكبير "S")، وهذا يخبر Vue أننا نتوقع أن تكون قيمة هذه الخاصية سلسلةً نصيةً. أما بالنسبة للخاصية done، فأضِف أولًا الحقل default مع القيمة false، وهذا يعني أنّ الخاصية done ستكون لها القيمة false عند عدم تمريرها إلى المكوِّن ToDoItem، وضَع في الحسبان أنّ هذا ليس مطلوبًا، إذ نحتاج فقط إلى الحقل default مع الخاصيات غير المطلوبة، وأضف بعد ذلك حقل النوع type مع القيمة Boolean، وهذا يخبر Vue أننا نتوقع أن يكون لخاصية القيمة value النوع المنطقي boolean في جافاسكربت. يجب أن يبدو كائن المكوِّن الآن كما يلي: <script> export default { props: { label: { required: true, type: String }, done: { default: false, type: Boolean } } }; </script> استخدام الخاصيات المسجلة يمكننا الآن بعد تعريف هذه الخاصيات ضمن كائن المكوِّن استخدام هذه القيم المتغيرة في قالبنا ولنبدأ بإضافة الخاصية label إلى قالب المكوِّن. ضع مكان محتويات العنصر <label> القيمة {{label}} في عنصر القالب <template>، إذ تُعَدّ {{}} صيغة قوالب خاصة في Vue تتيح طباعة نتيجة تعابير جافاسكربت المُعرَّفة في الصنف Class ضمن القالب، بما في ذلك القيم والتوابع، كما يُعرَض المحتوى ضمن {{}} بوصفه نصًا وليس شيفرة HTML، وبالتالي سنطبع قيمة الخاصية label في هذه الحالة. يجب أن يبدو قسم قالب المكوِّن الآن كما يلي: <template> <div> <input type="checkbox" id="todo-item" /> <label for="todo-item">{{label}}</label> </div> </template> ارجع إلى متصفحك وسترى عنصر المهام مُصيَّرًا كما كان سابقًا، ولكن بدون العنصر label، وانتقل إلى أدوات التطوير DevTools في متصفحك وسترى تحذيرًا في الطرفية كما يلي: [Vue warn]: Missing required prop: "label" found in ---> <ToDoItem> at src/components/ToDoItem.vue <App> at src/App.vue <Root> يكون هذا بسبب أننا ميّزنا الخاصية label بوصفها خاصيةً مطلوبةً أو إجباريةً، لكننا لم نعطِ المكوِّن هذه الخاصية مطلقًا، إذ حدّدنا المكان الذي نريد استخدامه ضمن القالب، لكننا لم نمرّره إلى المكوِّن عند استدعائه، إذًا لنصلح ذلك. أضف الخاصية label إلى المكوِّن <to-do-item> في الملف App.vue مثل سمة HTML العادية تمامًا: <to-do-item label="My ToDo Item"></to-do-item> سترى الآن العنصر label في تطبيقك دون تحذير في الطرفية مرةً أخرى. كائن الخاصية data في Vue إذا عدّلتَ قيمة الخاصية label المُمرَّرة إلى المكون <to-do-item> في تطبيقك، فيجب أن تراها مُحدَّثةً. لدينا الآن مربع اختيار مع عنصر label قابل للتحديث، ولكننا لا نطبّق حاليًا أيّ شيء باستخدام الخاصية done، إذ يمكننا تحديد مربعات الاختيار في واجهة المستخدِم، ولكن لا يوجد مكان في التطبيق نسجل فيه ما إذا كان عنصر المهام المطلوب قد اكتمل فعليًا أم لا. يمكنك تحقيق ذلك من خلال ربط الخاصية done الخاصة بالمكوِّن مع السمة checked في العنصر <input>، بحيث يمكن أن تكون بمثابة سجل لما إذا كان مربع الاختيار محددًا أم لا، إذ يجب أن تعمل الخاصيات بوصفها رابط بيانات أحادي الاتجاه، ويجب ألّا يغير المكوِّن قيمة خاصياته أبدًا، إذ يمكن أن تجعل خاصيات تعديل المكونات تنقيح الأخطاء تحديًا، فإذا مُرِّرت قيمةً إلى عدة أبناء، فيمكن أن يكون تتبُّع مصدر تغييرات هذه القيمة أمرًا صعبًا، كما يمكن أن يؤدي تغيير الخاصيات إلى إعادة تصيير المكوّنات، لذا فسيؤدي تغيّر خاصيات المكوِّن إلى إعادة تصييره، مما يؤدي بدوره إلى حدوث التغيّر مرةً أخرى. يمكننا حل هذه المشكلة من خلال إدارة الحالة done باستخدام الخاصية data في Vue، إذ تُعَدّ الخاصية data المكان الذي يمكنك من خلاله إدارة حالة المكوِّن المحلية، فهي توجد ضمن كائن المكوِّن جنبًا إلى جنب مع الخاصية props ولها البنية التالية: data() { return { key: value } } لاحظ أنّ الخاصية data هي دالة للحفاظ على قيم البيانات فريدة لكل نسخة من المكوِّن في وقت التشغيل، إذ تُستدعَى الدالة بصورة منفصلة لكل نسخة من المكوِّن. فإذا عرّفتَ الخاصية data بوصفها كائن فقط، فستشترك جميع نسخ هذا المكوِّن في القيم نفسها، وهذا أحد الآثار الجانبية التي لا نريدها للطريقة التي يسجِّل بها Vue المكوّنات، كما يمكنك استخدام this للوصول إلى خاصيات المكوّن والخاصيات الأخرى من data وسنرى مثالًا عن ذلك لاحقًا. ملاحظة: بما أن الطريقة التي يعمل بها this يمكن استخدامها في الدوال السهمية Arrow Function أو الارتباط بسياق الآباء، فلن تتمكن من الوصول إلى أيّ من السمات الضرورية من data إذا استخدمتَ هذه الدالة، لذلك لا تستخدِمها مع الخاصية data. لنضِف الخاصية data إلى المكوِّن ToDoItem، إذ سيعيد ذلك كائنًا يحتوي على خاصية واحدة سنسميها isDone التي تكون قيمتها this.done. عدّل كائن المكوِّن كما يلي: export default { props: { label: { required: true, type: String }, done: { default: false, type: Boolean } }, data() { return { isDone: this.done }; } }; يربط Vue جميع خاصياتك بنسخة المكوِّن مباشرةً، لذلك لا يتعين علينا استدعاء this.props.done، ويربط السمات الأخرى مثل data و methods و computed وغيرها مباشرةً بنسخة المكوِّن لجعلها متاحةً لقالبك، لكن يجب الاحتفاظ بالمفاتيح فريدة لهذه السمات، وهذا هو السبب في أننا أطلقنا على سمة data الاسم isDone عوضًا عن done. يجب الآن ربط الخاصية isDone بالمكوِّن، إذ يملك Vue بنية صيغة لربط تعابير جافاسكربت بعناصر ومكونات HTML بطريقة مشابهة لكيفية استخدام Vue لتعابير {{}} لعرض تعابير جافاسكربت ضمن القوالب، وهذه الصيغة هي v-bind التي تبدو كما يلي: v-bind:attribute="expression" وبالتالي ستسبق العبارة v-bind: أيّ سمة أو خاصية تريد ربطها، ويمكنك استخدام اختصار للخاصية v-bind في أغلب الأحيان، وهذا الاختصار هو استخدام تقطتين قبل السمة أو الخاصية، لذلك تعمل العبارة :attribute="expression" بطريقة مشابهة للعبارة v-bind:attribute="expression"، لذلك يمكننا استخدام v-bind لربط الخاصية isDone مع السمة checked في العنصر <input> في حالة استخدام مربع اختيار ضمن المكوِّن ToDoItem، كما أنّ الأمرَين التاليَين متكافئان: <input type="checkbox" id="todo-item" v-bind:checked="isDone" /> <input type="checkbox" id="todo-item" :checked="isDone" /> أنت حر في استخدام النمط الذي تريده، ولكن يُفضَّل الحفاظ على كل شيء متسقًا قدر الإمكان، وبما أنّ استخدام صيغة الاختصار أكثر شيوعًا، فسنلتزم بهذا النمط في هذا المقال. عدّل العنصر <input> الآن ليشمل :checked="isDone". اختبر مكوِّنك عن طريق تمرير :done="true" إلى استدعاء المكوِّن ToDoItem في الملف App.vue، ولاحظ أنك تحتاج إلى استخدام صيغة v-bind، لأنّ القيمة true ستُمرَّر بوصفها سلسلةً نصيةً بخلاف ذلك، كما يجب تحديد مربع الاختيار المعروض. <template> <div id="app"> <h1>My To-Do List</h1> <ul> <li> <to-do-item label="My ToDo Item" :done="true"></to-do-item> </li> </ul> </div> </template> حاول تغيير القيمة true إلى false، ثم أعِد تحميل تطبيقك لترى كيفية تغيّر الحالة. إعطاء المهام معرفا فريدا رائع، أصبح لدينا الآن مربع اختيار يعمل بنجاح، إذ يمكننا ضبط الحالة برمجيًا، لكن يمكننا حاليًا إضافة مكوِّن ToDoList واحد فقط إلى الصفحة لأن المعرّف id ثابت، مما يؤدي إلى حدوث أخطاء في التقنية المساعدة لأننا بحاجة هذا المعرّف لربط العناوين أو التسميات إلى مربعات الاختيار المقابلة لها بصورة صحيحة، ويمكن إصلاح هذا الخطأ من خلال ضبط المعرّف id برمجيًا في بيانات المكوِّن. يمكننا استخدام التابع uniqueid() الخاص بحزمة Lodash للمساعدة في إبقاء الفهرس فريدًا، إذ تصدِّر هذه الحزمة دالةً تأخذ سلسلةً نصيةً وتضيف عددًا صحيحًا فريدًا إلى نهاية البادئة، وسيكون هذا كافيًا لإبقاء معرِّفات المكوّنات فريدة. يجب إضافة هذه الحزمة إلى مشروعنا باستخدام npm، لذا أوقف خادمك وأدخِل الأمر التالي في طرفيتك: npm install --save lodash.uniqueid ملاحظة: إذا أردت استخدام yarn، فيمكنك كتابة الأمر yarn add lodash.uniqueid. يمكننا الآن استيراد هذه الحزمة إلى المكوِّن ToDoItem، لذا أضِف السطر التالي قبل العنصر <script> في الملف ToDoItem.vue: import uniqueId from 'lodash.uniqueid'; أضِف بعد ذلك الحقل id إلى الخاصية data بحيث يبدو كائن المكوِّن بالصورة التالية، إذ يعيد التابع uniqueId() البادئة todo- مع سلسلة نصية فريدة ملحقَة بها: import uniqueId from 'lodash.uniqueid'; export default { props: { label: { required: true, type: String }, done: { default: false, type: Boolean } }, data() { return { isDone: this.done, id: uniqueId('todo-') }; } }; اربط بعد ذلك المعرِّف id مع كل سمة id الخاصة بمربع الاختيار والسمة for الخاصة بالعنوان أو التسمية، وعدِّل السمات id و for الحالية كما يلي: <template> <div> <input type="checkbox" :id="id" :checked="isDone" /> <label :for="id">{{label}}</label> </div> </template> الخلاصة لدينا حتى الآن المكوِّن ToDoItem الذي يعمل بنجاح، ويمكنه تمرير عنوان أو تسمية لعرضها، كما سيخزّن حالته المحدَّدة، وسيُصيَّر مع معرِّف id فريد في كل مرة يُستدعَى فيها، كما يمكنك التحقق مما إذا كانت المعرّفات الفريدة تعمل عن طريق إضافة المزيد من استدعاءات المكونات <to-do-item> مؤقتًا في الملف App.vue ثم التحقق من خرجها المُصيَّر باستخدام أدوات التطوير DevTools في متصفحك. نحن الآن جاهزون لإضافة عدة مكونات ToDoItem إلى تطبيقنا، إذ سنتعلّم في المقال التالي كيفية إضافة مجموعة من بيانات عناصر المهام إلى المكوِّن App.vue، والتي سنكرّرها ونعرضها ضمن المكوّنات ToDoItem باستخدام الموجّه v-for. ترجمة -وبتصرّف- للمقال Creating our first Vue component. اقرأ أيضًا المقال السابق: مدخل إلى إطار العمل Vue.js مدخل إلى التعامل مع المكونات في Vue.js التعامل مع دخل المستخدم عن طريق نماذج الإدخال في Vue.js النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js
-
سنلقي في هذا المقال نظرةً على خلفية إطار العمل Vue.js، وسنتعلمّ كيفية تثبيته وإنشاء مشروع جديد ودراسة بنية المشروع بأكمله مع مكوّناته، بالإضافة إلى كيفية تشغيله محليًا وتجهيزه للبناء. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ تُكتَب مكونات Vue بوصفها مجموعةً من كائنات جافاسكربت التي تدير بيانات التطبيق وصيغة القوالب المستنِدة إلى لغة HTML المرتبطة مع بنية نموذج DOM الأساسية، كما ستحتاج إلى طرفية مثبَّتٌ عليها node و npm لتثبيت Vue ولاستخدام بعض ميزاته الأكثر تقدمًا مثل مكوّنات الملف المفرد Single File Components أو دوال التصيير Render. الهدف: إعداد بيئة تطوير Vue المحلية وإنشاء تطبيق بسيط وفهم أساسيات كيفية عمله. يُعَدّ Vue.js إطار عمل جافاسكربت حديث يوفِّر تسهيلات مفيدةً للتحسين التدريجي على عكس العديد من أطر العمل الأخرى، إذ يمكنك استخدام إطار عمل Vue لتحسين شيفرة HTML الحالية، وبالتالي يمكنك استخدامه بوصفه بديلًا مؤقتًا لمكتبة ما مثل مكتبة jQuery. كما يمكنك استخدام Vue لكتابة تطبيقات الصفحة الواحدة Single Page Applications -أو SPA اختصارًا-، مما يتيح لك إنشاء شيفرة توصيف يديرها Vue بالكامل، كما يمكن أن يحسّن ذلك من تجربة المطوِّر وأدائه عند التعامل مع التطبيقات المعقّدة، كما يتيح لك الاستفادة من المكتبات للتوجيه من طرف العميل وإدارة الحالة عندما تحتاج ذلك، إذ يتخذ Vue نهجًا وسطيًا لاستخدام الأدوات مثل التوجيه من طرف العميل وإدارة الحالة، في حين يحتفظ فريق Vue الأساسي بالمكتبات المقترحة لهذه الوظائف، إلا أنها ليست مجمعةً مباشرةً ضمنه، مما يتيح اختيار مكتبة إدارة توجيه أو حالة مختلفة إذا ناسبت تطبيقك أكثر. كما يوفِّر Vue نهجًا تقدميًا لكتابة شيفرة التوصيف، ويتيح مثل معظم الإطارات العمل الأخرى إنشاء كتل قابلة لإعادة الاستخدام من شيفرة التوصيف باستخدام المكونات، كما تُكتَب مكونات Vue باستخدام صيغة قوالب HTML خاصةً، وإذا احتجت إلى تحكِّم أكبر مما تسمح به صيغة HTML، فيمكنك كتابة دوال JSX أو جافاسكربت الصرفة لتعريف مكوناتك. يمكن أن ترغب لاحقًا في إبقاء كتاب أساسيات إطار العمل Vue.js من أكاديمية حسوب وتوثيق API مفتوحَين في تبويبات أخرى، بحيث يمكنك الرجوع إليهما إذا أردتَ مزيدًا من المعلومات حول أيّ موضوع فرعي، كما يمكنك الاطلاع على موازنة جيدة بين Vue والعديد من الأطر الأخرى في توثيق Vue، ولكن يُحتمَل أن تكون هذه الموازنة منحازة. هذه المقالة جزء من سلسلة تمهيدية حول إطار العمل Vue.js وإليك كامل مقالات السلسلة: مدخل إلى إطار العمل Vue.js إنشاء المكونات في تطبيق Vue.js عرض مكونات Vue.js شرح مفهوم الأحداث والتوابع والنماذج في Vue.js إضافة تنسيق للمكونات واستعمال الخاصية computed في تطبيق Vue.js العرض الشرطي في إطار العمل Vue.js إدارة العناصر باستخدام خاصيات ref في إطار العمل Vue.js تثبيت Vue.js يمكنك استخدام إطار عمل Vue في موقع قائم من خلال وضع أحد عناصر <script> التالية في الصفحة، مما يتيح بدء استخدام Vue على المواقع الحالية، لذلك يفتخر Vue بكونه إطار عمل تقدمي، إذ يُعَدّ ذلك خيارًا رائعًا عند نقل مشروع موجود مسبقًا باستخدام مكتبة مثل مكتبة jQuery إلى Vue، وبالتالي يمكنك استخدام الكثير من ميزات Vue الأساسية مثل السمات والمكونات المُخصَّصة وإدارة البيانات. سكربت التطوير Development Script: غير مُحسَّن، ولكنه يتضمن تحذيرات الطرفية، وهذا رائع لعملية التطوير. <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> سكربت الإنتاج Production Script: إصدار مُحسَّن، ويتضمّن الحد الأدنى من تحذيرات الطرفية، كما يوصَى بتحديد رقم الإصدار عند تضمين Vue في موقعك بحيث لا تؤدي أيّ تحديثات لإطار العمل إلى تعطيل موقعك المباشر دون علمك. <script src="https://cdn.jsdelivr.net/npm/vue@2"></script> لكن هذا النهج له بعض القيود، إذ ستحتاج إلى استخدام حزمة NPM الخاصة بإطار عمل Vue لإنشاء تطبيقات أكثر تعقيدًا، مما سيتيح استخدام ميزات Vue المتقدمة والاستفادة من الحزم مثل WebPack، كما توجد واجهة سطر الأوامر CLI لتبسيط عملية التطوير، مما يسهّل إنشاء التطبيقات باستخدام Vue، إذ ستحتاج إلى ما يلي لاستخدام حزمة npm وواجهة CLI: تثبيت Node.js 8.11+. npm أو yarn. ملاحظة: إن لم تثبّت ما سبق مسبقًا، فتعرف على المزيد حول تثبيت npm و Node.js. شغّل الأمر التالي في طرفيتك لتثبيت CLI: npm install --global @vue/cli أو استخدم yarn: yarn global add @vue/cli يمكنك بعد ذلك فتح الطرفية في المجلد الذي تريد إنشاء المشروع فيه، وتشغيل الأمر vue create <project-name>، ثم ستعطيك واجهة CLI قائمةً بإعدادات المشروع التي يمكنك استخدامها، وهناك عدد قليل منها مُعَدّ مسبقًا، ويمكنك إعدادها بنفسك، إذ تتيح لك هذه الخيارات إعداد أشياء مثل شيفرة TypeScript وكشف الأخطاء المحتمَلة Linting والتوجيه vue-router والاختبار وغير ذلك. تهيئة مشروع جديد سننشئ نموذجًا لتطبيق قائمة المهام لاستكشاف ميزات Vue المختلفة، إذ سنبدأ باستخدام واجهة سطر أوامر Vue لإنشاء إطار عمل تطبيق جديد لبناء تطبيقنا فيه، لذا اتبع الخطوات التالية: اكتب الأمر cd في الطرفية للانتقال إلى المكان الذي تريد فيه إنشاء تطبيقك، ثم شغّل الأمر vue create moz-todo-vue. استخدم مفاتيح الأسهم ومفتاح Enter لتحديد خيار "تحديد الميزات يدويًا Manually select features". تسمح لك القائمة الأولى التي ستظهر لك باختيار الميزات التي تريد تضمينها في مشروعك، وتأكد من تحديد "Babel" و "Linter / Formatter"، فإذا لم تكن مُحدَّدةً، فاستخدم مفاتيح الأسهم ومفتاح المسافة للتبديل فيما بينها، ثم اضغط Enter للمتابعة. ستحدِّد بعد ذلك إعدادًا للميزة "linter / formatter"، لذا انتقل إلى الخيار "Eslint مع منع الأخطاء فقط Eslint with error prevention only" واضغط على Enter مرةً أخرى، إذ سيساعدنا ذلك على اكتشاف الأخطاء الشائعة دون مبالغة في الصرامة. سيُطلَب منك بعد ذلك ضبط نوع كشف الأخطاء المحتمَلة linting الآلي الذي تريده، لذا حدِّد الخيار "فحص الأخطاء عند الحفظ Lint on save"، مما سيؤدي إلى التحقق من وجود أخطاء عند حفظ ملف ضمن المشروع، ثم اضغط على Enter للمتابعة. ستحدِّد الآن كيف تريد إدارة ملفات الإعداد الخاصة بك، لذا سيضع الخيار "In dedicated config files" إعدادات الضبط الخاصة بك لأشياء مثل ESLint في ملفاتها المخصَّصة، وسيضع الخيار الآخر "In package.json" جميع إعدادات الضبط الخاصة بك في الملف package.json الخاص بالتطبيق، لذا حدِّد الخيار "In dedicated config files" واضغط على Enter. أخيرًا، ستُسأَل عن الحفظ على أساس إعداد مسبق للخيارات المستقبلية، فإذا أردتَ استخدام هذه الإعدادات وإلغاء الإعدادات المسبقة الحالية وتريد استخدامها مرةً أخرى، فاكتب y؛ وإلّا فاكتب n. ستبدأ واجهة CLI الآن في إنشاء الشيفرة المساعدة Scaffolding لمشروعك، وتثبيت كل اعتمادياتك. إذا لم تشغّل واجهة CLI الخاصة بإطار العمل Vue سابقًا، فسيظهر سؤال آخر، إذ سيُطلَب منك اختيار مدير الحزم، ويمكنك استخدام مفاتيح الأسهم لتحديد مدير الحزم الذي تفضله ليكون مدير الحزم الافتراضي من الآن فصاعدًا، فإذا احتجتَ استخدام مدير حزم مختلف بعد ذلك، فيمكنك تمرير الراية التالية عند تشغيل الأمر vue create: --packageManager=<package-manager> وبالتالي إذا أردت إنشاء مشروع moz-todo-vue باستخدام مدير الحزم npm واخترت yarn سابقًا، فيمكنك تشغيل الأمر التالي: vue create moz-todo-vue --packageManager=npm بنية المشروع إذا سار كل شيء على ما يرام، فيُفترَض إنشاء سلسلة من الملفات والمجلدات لمشروعك أهمها ما يلي: .eslintrc.js: ملف إعداد أداة ESLint الذي يمكنك استخدامه لإدارة قواعد كشف الأخطاء المحتملة. babel.config.js: ملف إعداد Babel الذي يحوِّل ميزات جافاسكربت الحديثة المستخدَمة في شيفرة التطوير إلى صيغة أقدم أكثر توافقًا مع المتصفحات في شيفرة الإنتاج، ويمكنك تسجيل إضافات Babel الإضافية في هذا الملف. .browserslistrc: هو إعداد لقائمة المتصفحات، ويمكنك استخدامه للتحكُّم في المتصفحات التي تعمل أدواتك على تحسينها. public: يحتوي هذا المجلد على الأصول الساكنة المنشورة، ولكن لم يعالجها Webpack أثناء البناء باستثناء الملف index.html الذي يحصل على بعض المعالجة. favicon.ico: الرمز أو الأيقونة المفضلة Favicon لتطبيقك، وهو شعار Vue حاليًا. index.html: هو قالب تطبيقك، حيث يُشغَّل تطبيق Vue من صفحة HTML هذه، ويمكنك استخدام صيغة قوالب Lodash لتعديل القيم فيها، ولاحظ أنه لا يُستخدَم هذا القالب لإدارة تخطيط تطبيقك، بل هو مخصَّص لإدارة ملفات HTML الساكنة الموجودة خارج تطبيق Vue، إذ يُعدَّل هذا الملف في حالات الاستخدام المتقدِّمة فقط. src: يحتوي هذا المجلد على مركز تطبيق Vue. main.js: هو نقطة الدخول إلى تطبيقك، إذ يهيّئ هذا الملف حاليًا تطبيق Vue ويشير إلى عنصر HTML في الملف index.html الذي يجب ربطه مع تطبيقك، كما يُعَدّ هذا الملف المكانَ الذي تسجِّل فيه المكونات العامة أو مكتبات Vue الإضافية في أغلب الأحيان. App.vue: هو مكون المستوى الأعلى في تطبيق Vue. components: يُعَد هذا المجلد المكان الذي تحتفظ فيه بمكوناتك، ويحتوي حاليًا على مثال لمكوِّن واحد فقط. assets: هذا المجلد مخصص لتخزين الأصول الساكنة مثل ملفات CSS والصور، وبما أنّ هذه الملفات موجودة في المجلد المصدر، فيمكن أن يعالجها Webpack، وهذا يعني أنه يمكنك استخدام المعالِجات المسبقة مثل Sass/SCSS أو Stylus. ملاحظة: يمكن وجود مجلدات أخرى بناءً على الخيارات التي تحددها عند إنشاء مشروع جديد، فإذا اخترت موجّهًا مثلًا، فسيكون لديك أيضًا المجلد views. ملفات .vue مكونات الملف المفرد تُعَدّ المكونات جزءًا أساسيًا من إنشاء التطبيقات في Vue كما هو الحال في العديد من أطر عمل الواجهة الأمامية، إذ تسمح هذه المكونات بتقسيم تطبيق كبير إلى كتل بناء منفصلة يمكن إنشاؤها وإدارتها، ونقل البيانات بين بعضها البعض كما هو مطلوب. يمكن أن تساعدك هذه الكتل الصغيرة في معرفة ما تفعله شيفرتك البرمجية واختبارها. يمكن أن تشجعك بعض أطر العمل على فصل شيفرات القالب والمنطق والتصميم إلى ملفات منفصلة، ولكن يتبع Vue النهج المعاكس، إذ يتيح لك باستخدام مكونات الملف المفرد Single File Components تجميع قوالبك والسكربتات المقابلة وشيفرة CSS معًا في ملف واحد ينتهي باللاحقة .vue، وتعالج أداة بناء JS -مثل Webpack- هذه الملفات، مما يعني أنه يمكنك الاستفادة من أدوات وقت البناء في مشروعك، وبالتالي يمكنك استخدام أدوات مثل Babel و TypeScript و SCSS وغيرها لإنشاء مكونات أكثر تعقيدًا. تُضبَط المشاريع المُنشَأة باستخدام واجهة CLI الخاصة بإطار عمل Vue لاستخدام ملفات .vue مع Webpack، فإذا نظرت ضمن المجلد src في المشروع الذي أنشأناه باستخدام CLI، فسترى أول ملف .vue وهو App.vue. App.vue افتح الملف App.vue وسترى أنه يتكون من ثلاثة أجزاء هي: <template> و <script> و <style> التي تحتوي على معلومات قالب المكوِّن وسكربتاته وتنسيقه، وتشترك كافة مكونات الملف المفرد في البنية الأساسية نفسها، كما يحتوي عنصر القالب <template> على بنية شيفرة التوصيف ومنطق عرض مكونك، كما يمكن أن يحتوي قالبك على أيّ شيفرة HTML صالحة، بالإضافة إلى بعض الصيغ الخاصة بإطار عمل Vue التي سنشرحها لاحقًا. ملاحظة: يمكنك استخدام صيغة قالب Pug بدلًا من لغة HTML القياسية من خلال ضبط السمة lang في الوسم <template> بالصورة <template lang="pug">، إذ سنلتزم باستخدام لغة HTML القياسية في هذا المقال، ولكن يجب أن تعرف أنّ ذلك ممكن. يحتوي العنصر <script> على المنطق الذي لا يُعرَض من المكوِّن، إذ يجب أن يحتوي الوسم <script> على كائن جافاسكربت JS افتراضي مُصدَّر، ويُعَدّ هذا الكائن المكان الذي تسجِّل فيه المكونات محليًا، وتعرِّف مدخلات المكوّن (الخاصيات Props)، وتتعامل مع الحالة المحلية، وتعرِّف التوابع وغير ذلك، كما ستعمل خطوة البناء على معالجة هذا الكائن وتحويله مع قالبك إلى مكوّن Vue باستخدام الدالة render(). يضبط التصدير الافتراضي في حالة App.vue اسم المكوّن على App ويسجّل المكوّن HelloWorld من خلال إضافته إلى الخاصية components، فإذا سجّلتَ أحد المكوّنات بهذه الطريقة، فهذا يعني أنك تسجله محليًا، ولا يمكن استخدام المكوّنات المسجلة محليًا إلا ضمن المكوّنات التي تسجلها، لذلك يجب استيرادها وتسجيلها في كل ملف مكوّن يستخدمها. يمكن أن يكون هذا مفيدًا لتقسيم الحزم أو تقنية هز الشجرة Tree Shaking، إذ لا تحتاج كل صفحة في تطبيقك إلى جميع المكوّنات بالضرورة. import HelloWorld from './components/HelloWorld.vue'; export default { name: 'App', components: { // يمكنك تسجيل المكونات محليًا هنا. HelloWorld } }; ملاحظة: إذا أردت استخدام صيغة لغة TypeScript، فيجب ضبط السمة lang في الوسم <script> للإشارة إلى المصرِّف الذي تستخدمه لغة TypeScript بالصورة: <script lang="ts">. يُعَدّ العنصر <style> المكان الذي تكتب فيه شيفرة CSS الخاصة بالمكوّن، فإذا أضفتَ السمة scoped بالصورة <style scoped>، فسيحدِّد إطار العمل Vue نطاق التنسيقات لمحتويات مكوّن الملف المفرد SFC، إذ يعمل هذا العنصر بطريقة مشابهة لحلول CSS في JS، ولكنه يسمح لك فقط بكتابة شيفرة CSS عادية. ملاحظة: إذا اخترتَ معالج CSS مسبَق عند إنشاء المشروع باستخدام واجهة سطر الأوامر CLI، فيمكنك إضافة السمة lang إلى الوسم <style> بحيث يمكن معالجة المحتويات باستخدام Webpack في وقت البناء مثل الوسم <style lang="scss"> الذي يسمح باستخدام صيغة SCSS في معلومات التنسيق. تشغيل التطبيق محليا تأتي واجهة CLI الخاصة بإطار العمل Vue مع خادم تطوير مضمَّن، مما يتيح تشغيل تطبيقك محليًا لتتمكّن من اختباره بسهولة دون الحاجة إلى إعداد خادم بنفسك، إذ تضيف CLI الأمر serve إلى الملف package.json الخاص بالمشروع بوصفه سكربت npm بحيث يمكنك تشغيله بسهولة. شغّل الأمر npm run serve في طرفيتك أو الأمر yarn serve إذا أردت استخدام yarn، إذ يجب أن ينتج شيء يشبه ما يلي: INFO Starting development server... 98% after emitting CopyPlugin DONE Compiled successfully in 18121ms App running at: - Local: <http://localhost:8080/> - Network: <http://192.168.1.9:8080/> Note that the development build is not optimized. To create a production build, run npm run build. إذا انتقلت إلى العنوان المحلي في تبويب متصفح جديد مثل العنوان http://localhost:8080 أو يمكن أن يختلف بناءً على إعداداتك، فيجب أن ترى تطبيقك، إذ يجب أن يحتوي التطبيق حاليًا على رسالة ترحيب، ورابطًا إلى توثيق Vue، وروابطًا إلى الإضافات التي أضفتها عند تهيئة التطبيق باستخدام CLI، بالإضافة إلى بعض الروابط المفيدة الأخرى إلى مجتمع Vue ونظامه البيئي. إجراء بعض التعديلات على التطبيق التعديل الأول الذي سنجريه على التطبيق هو حذف شعار Vue، لذا افتح الملف App.vue واحذف العنصر <img> من قسم القالب: <img alt="Vue logo" src="./assets/logo.png"> إذا كان خادمك قيد التشغيل، فيُفترَض أن ترى الشعار محذوفًا من الموقع المُصيَّر مباشرةً تقريبًا، فلنحذف الآن المكوّن HelloWorld من القالب. أولًا احذف السطر التالي: <HelloWorld msg="Welcome to Your Vue.js App"/> إذا حفظت الملف App.vue الآن، فسيعطي التطبيق المُصيَّر خطأً لأننا سجَّلنا المكوّن ولكننا لم نستخدِمه، إذ يجب أيضًا إزالة الأسطر الموجودة ضمن العنصر <script> الذي يستورِد المكوّن ويسجله، لذا احذف الأسطر التالية: import HelloWorld from './components/HelloWorld.vue' components: { HelloWorld } يجب ألّا يعرض تطبيقك المُصيَّر أيّ خطأ حاليًا، وإنما سيعرض صفحةً فارغةً فقط، إذ لا يوجد حاليًا محتوًى مرئيًا ضمن عنصر القالب <template>. لنضِف عنصر <h1> جديد ضمن العنصر <div id="app">، وبما أننا سننشئ تطبيق قائمة المهام، فلنضبط نص العنوان ليكون "To-Do List" كما يلي: <template> <div id="app"> <h1>To-Do List</h1> </div> </template> سيعرض التطبيق العنوان كما هو متوقع. الخلاصة تعلّمنا في هذا المقال بعض أفكار إطار العمل Vue وأنشأنا شيفرة تطبيقنا المساعدة وفحصناه وأجرينا بعض التعديلات الأولية؛ أما الآن فسنذهب إلى أبعد من ذلك وننشئ تطبيق قائمة المهام الأساسي الذي يسمح بتخزين قائمة بالعناصر وإلغاء تحديدها عند الانتهاء وترشيح جميع المهام والمهام المكتملة والمهام غير المكتملة في القائمة، كما سنبني في المقال التالي أول مكوّن مخصَّص، وسنتعرّف على بعض المفاهيم المهمة مثل تمرير الخاصيات وحفظ حالة البيانات. ترجمة -وبتصرّف- للمقال Getting started with Vue. اقرأ أيضًا مدخل إلى التعامل مع المكونات في Vue.js التعامل مع دخل المستخدم عن طريق نماذج الإدخال في Vue.js النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js
-
سنتعرف في هذا المقال على التوجيه Routing أو الترشيح المستند إلى عنوان URL كما يشار إليه في بعض الأحيان، والذي سنستخدمه لتوفير عنوان URL فريد لكل عرض من عروض المهام الثلاثة: "جميع المهام All" و"المهام النشطة Active" و"المهام المكتملة Completed"، كما سنتطرق إلى كيفية استكشاف الأخطاء وإصلاحها. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ يُعَدّ فهم ميزات جافاسكربت الحديثة مثل الأصناف Classes والوحدات Modules وما إلى ذلك مفيدًا للغاية، لأن إطار العمل Ember يستخدمها بكثرة. الهدف: التعرف على كيفية تطبيق التوجيه في إطار العمل Ember، وتوفير مزيد من الموارد لتعلم إطار عمل Ember ومعلومات استكشاف الأخطاء وإصلاحها. الترشيح المستند إلى عنوان URL يأتي إطار عمل Ember مع نظام التوجيه ذي التكامل الوثيق مع عنوان URL الخاص بالمتصفح، إذ يجب عند كتابة تطبيقات الويب تمثيل الصفحة بعنوان URL بحيث إذا كانت الصفحة بحاجة إلى تحديث، فلن يُفاجَأ المستخدِم بحالة تطبيق الويب، وسيتمكّن من الانتقال مباشرةً إلى عروض التطبيق المهمة. لدينا حاليًا صفحة "جميع المهام All" إذ لن نطبّق ترشيحًا فيها في الوقت الحالي، لكن يجب إعادة تنظيمها قليلًا للتعامل مع عرض مختلف للمهام النشطة والمهام المكتملة، كما يحتوي تطبيق Ember على مسار تطبيق افتراضي مرتبط بالقالب app/templates/application.hbs الذي يُعَدّ نقطة الدخول إلى تطبيق قائمة المهام، لذلك يجب إجراء بعض التغييرات للسماح بالتوجيه Routing. إنشاء المسارات لننشئ ثلاثة مسارات جديدة هي: "Index" و "Active" و "Completed" من خلال إدخال الأوامر التالية في الطرفية ضمن المجلد الجذر لتطبيقك: ember generate route index ember generate route completed ember generate route active لا ينتج عن الأمرَين الثاني والثالث ملفات جديدةً فحسب، وإنما يُعدَّل الملف app/router.js الموجود مسبقًا الذي يحتوي على المحتويات التالية: import EmberRouter from '@ember/routing/router'; import config from './config/environment'; export default class Router extends EmberRouter { location = config.locationType; rootURL = config.rootURL; } Router.map(function() { this.route('completed'); this.route('active'); }); يتصرّف الملف router.js بوصفه خريطة موقع sitemap للمطورين ليتمكنوا من رؤية كيفية تنظيم التطبيق بأكمله بسرعة، كما أنه يخبر إطار العمل Ember بكيفية التفاعل مع مسارك عند تحميل بيانات عشوائية أو التعامل مع الأخطاء أثناء تحميل تلك البيانات أو تفسير الأجزاء الآلية لعنوان URL مثلًا، وبما أنّ بياناتنا ساكنة، فلن نصل إلى أيّ من هذه الميزات الرائعة، لكننا سنظل نتأكد من أنّ المسار يوفِّر الحد الأدنى من البيانات المطلوبة لعرض الصفحة. لم يضِف إنشاء المسار "Index" سطر تعريف مسار إلى الملف router.js، لأن "Index" هي كلمة خاصة تشير إلى المسار الافتراضي للتصيير والتحميل وغير ذلك كما هو الحال مع التنقل باستخدام عنوان URL وتحميل وحدة جافاسكربت JavaScript. يمكنك تعديل طريقتنا القديمة في تصيير تطبيق قائمة المهام من خلال استبدال الاستدعاء {{outlet}} باستدعاء المكون TodoList من قالب التطبيق ب، مما يعني تصيير أيّ مسار فرعي في المكان نفسه دون الانتقال إلى تبويب جديد. انتقل إلى الملف todomvc/app/templates/application.hbs وضَع مكان السطر التالي: <TodoList /> ما يلي: {{outlet}} يمكننا الآن إدخال استدعاء المكوّن TodoList في قوالب index.hbs و completed.hbs و active.hbs الموجودة أيضًا في مجلد القوالب. ضَع مكان السطر التالي: {{outlet}} ما يلي: <TodoList /> إذا جرّبت التطبيق مرةً أخرى وزرتَ أيًّا من المسارات الثلاثة: localhost:4200 localhost:4200/active localhost:4200/completed فسترى الشيء نفسه بالضبط، إذ سيصيَّر القالب الذي يتوافق مع المسار المحدد ("Active" أو "Completed" أو "Index") في كل عنوان URL للمكون <TodoList />. يُحدَّد الموقع في الصفحة حيث يُصيَّر المكوّن <TodoList /> باستخدام {{ outlet }} في المسار الأب وهو في هذه الحالة application.hbs، ولدينا الآن مساراتنا في مكانها الصحيح، لكننا بحاجة إلى طريقة للتمييز بين كل من هذه المسارات لتظهِر ما يفترض منها أن تظهِره. ارجع مرةً أخرى إلى الملف todo-data.js الذي يحتوي مسبقًا على تابع جالب getter يعيد جميع المهام وتابع جالب آخر يعيد المهام غير المكتملة، ولكنه لا يحتوي على تابع جالب يعيد المهام المكتملة فقط، لذا لنضفه بعد التوابع الجالبة الموجودة مسبقًا: get completed() { return this.todos.filter(todo => todo.isCompleted); } النماذج يجب الآن إضافة نماذج Models إلى مسارات ملفات جافاسكربت للسماح بسهولة بإعادة مجموعات بيانات معينة لعرضها في تلك النماذج، إذ يُعَدّ النموذج model بأنه خطاف Hook دورة حياة تحميل بيانات، ولكن ليست قدرات النموذج مهمةً لنا في تطبيق TodoMVC، كما سنوفر الوصول إلى الخدمة كما فعلنا مع المكوّنات. نموذج المسار index عدِّل أولًا الملف todomvc/app/routes/index.js ليبدو كما يلي: import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; export default class IndexRoute extends Route { @service('todo-data') todos; model() { let todos = this.todos; return { get allTodos() { return todos.all; } } } } يمكننا الآن تعديل الملف todomvc/app/templates/index.hbs بحيث إذا تضمّن المكوّن <TodoList />، فإنه يفعل ذلك صراحة مع النموذج المتاح، ويستدعي التابع الجالب allTodos() للتأكد من ظهور جميع المهام. عدِّل السطر التالي في هذا الملف: <TodoList /> إلى ما يلي: <TodoList @todos={{ @model.allTodos }}/> نموذج المسار completed عدِّل الملف todomvc/app/routes/completed.js ليبدو كما يلي: import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; export default class CompletedRoute extends Route { @service('todo-data') todos; model() { let todos = this.todos; return { get completedTodos() { return todos.completed; } } } } يمكننا الآن تعديل الملف todomvc/app/templates/completed.hbs بحيث إذا تضمّن المكوّن <TodoList />، فإنه يفعل ذلك صراحةً مع النموذج المتاح، ويستدعي التابع الجالب completedTodos() للتأكد من ظهور المهام المكتملة فقط. عدِّل السطر التالي في هذا الملف: <TodoList /> إلى ما يلي: <TodoList @todos={{ @model.completedTodos }}/> نموذج المسار active عدِّل الملف todomvc/app/routes/active.js ليبدو كما يلي: import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; export default class ActiveRoute extends Route { @service('todo-data') todos; model() { let todos = this.todos; return { get activeTodos() { return todos.incomplete; } } } } يمكننا الآن تعديل الملف todomvc/app/templates/active.hbs بحيث إذا تضمّن المكوّن <TodoList />، فإنه يفعل ذلك صراحةً مع النموذج المتاح، ويستدعي التابع الجالب activeTodos() للتأكد من ظهور المهام النشطة أو غير المكتملة فقط. عدِّل السطر التالي في هذا الملف: <TodoList /> إلى ما يلي: <TodoList @todos={{ @model.activeTodos }}/> لاحظ أننا نعيد كائنًا له تابع جالب بدلًا من كائن ساكن أو قائمة مهام ساكنة مثل this.todos.completed في خطافات نماذج المسار، والسبب في ذلك هو أننا نريد أن يكون للقالب مرجع آلي لقائمة المهام، فإذا أعدنا القائمة مباشرةً، فلن يُعاد حساب البيانات أبدًا، مما يؤدي إلى ظهور تنقلات فاشلة وعدم ترشيح فعلي، ويمكن إعادة البحث عن المهام من خلال تحديد تابع جالب في كائن الإعادة من بيانات النموذج بحيث تُمثَّل التعديلات التي أجريناها على قائمة المهام في القائمة المُصيَّرة. تشغيل روابط التذييل أصبحت وظائف المسار الآن في مكانها الصحيح، لكن لا يمكننا الوصول إليها من تطبيقنا، لذا لنفعِّل روابط التذييل بحيث يؤدي النقر عليها إلى الانتقال إلى المسارات المطلوبة. ارجع إلى الملف todomvc/app/components/footer.hbs وابحث عمّا يلي: <a href="#">All</a> <a href="#">Active</a> <a href="#">Completed</a> وعدِّله إلى ما يلي: <LinkTo @route='index'>All</LinkTo> <LinkTo @route='active'>Active</LinkTo> <LinkTo @route='completed'>Completed</LinkTo> يُعَدّ <LinkTo> بأنه مكوّن Ember مبني مسبقًا يعالِج جميع تغييرات الحالة عند التنقل بين المسارات، ويضبط الصنف active على أيّ رابط يطابق عنوان URL في حالة وجود رغبة في تنسيقه تنسيقًا مختلفًا عن الروابط غير النشطة. تحديث عرض المهام ضمن قائمة المهام أحد الأشياء الصغيرة الأخيرة التي نحتاج إلى إصلاحها هو أننا كنا ندخل سابقًا إلى خدمة todo-data مباشرةً ونكرّر جميع المهام ضمن الملف todomvc/app/components/todo-list.hbs كما يلي: {{#each this.todos.all as |todo| }} بما أننا نريد أن يعرض مكوّن قائمة المهام قائمةً مُرشَّحةً، فيجب تمرير وسيط إلى مكوِّن قائمة المهام يمثِّل قائمة المهام الحالية من todos كما يلي: {{#each @todos as |todo| }} يجب أن يحتوي تطبيقك الآن على روابط فعّالة في التذييل تعرض مسارات "Index" أو المسارات الافتراضية و"النشطة Active" و"المكتملة Completed". هناك الكثير مما يجب تنفيذه قبل أن يتطابق ما نفّذناه حتى الآن مع تطبيق TodoMVC الأصلي مثل تعديل المهام وحذفها واستمرارها عبر عمليات إعادة تحميل الصفحة، كما يمكنك مشاهدة تطبيق Ember المكتمل من خلال التحقق من مجلد التطبيق النهائي في مستودع شيفرة تطبيقنا للحصول عليها أو شاهد الإصدار المباشر. ادرس الشيفرة لمعرفة المزيد حول إطار عمل Ember، وراجع الفقرة التالية التي توفِّر مزيدًا من الموارد وبعض نصائح استكشاف الأخطاء وإصلاحها. استكشاف الأخطاء العامة وإصلاحها وبنى gotcha والمفاهيم الخاطئة كان آخر تحديث للقائمة التالية في شهر 6 من عام 2020. كيف يمكنني تنقيح الأخطاء التي يظهرها إطار العمل؟ توجد الإضافة ember-inspector بالنسبة للأشياء الخاصة بإطار العمل والتي تسمح بفحص ما يلي: المسارات Routes والمتحكِّمات Controllers. المكوِنات. الخدمات. الوعود Promises. البيانات وهي بيانات من واجهة برمجة تطبيقات بعيدة مثل ember-data افتراضيًا. معلومات الإهمال Deprecation Information. تصيير الأداء. اطّلع على أدوات المطوِّر التي تفيدك في تنقيح الأخطاء، وتعرّف على كيفية استخدام أدوات المطوِّر DevTools في كروم Chrome. بوجد ملفان رئيسيان من ملفات جافاسكربت هما: vendor.js و {app-name}.js في أيِّ مشروع Ember افتراضي، ويُنشَآن هذان الملفان باستخدام خرائط الشيفرة البرمجية Sourcemaps، لذلك ستُحمَّل خريطة الشيفرة البرمجية وستُوضَع نقطة التوقف في شيفرة مُترجَمة مسبقًا لتسهيل الارتباط بشيفرة مشروعك باستخدام منقّح أخطاء عند فتح الملف vendor.js أو الملف {app-name}.js للبحث عن الشيفرة البرمجية ذات الصلة. هل سأحتاج إلى واجهة ember-data المثبتة مسبقا؟ لن تحتاجها على الإطلاق، إذ تحل واجهة ember-data المشاكل الأكثر شيوعًا التي سيعمل بها أيّ تطبيق يتعامل مع البيانات بحيث يمكن تشغيل عميل بيانات واجهتك الأمامية، وهناك بديل شائع لأيّ عميل بيانات واجهة أمامية كامل الميزات هو Fetch API، إذ سيبدو المسار Route باستخدام fetch() كما يلي باستخدام أنماط التصميم التي يوفرها إطار العمل: import Route from '@ember/routing/route'; export default class MyRoute extends Route { async model() { let response = await fetch('some/url/to/json/data'); let json = await response.json(); return { data: json }; } } لماذا لا يمكنني استخدام جافاسكربت فقط؟ هذا هو السؤال الأكثر شيوعًا الذي يسمعه مجتمع Ember من الأشخاص الذين لديهم خبرة سابقة في React، إذ يمكن استخدام صيغة JSX أو أيّ شكل آخر من أشكال إنشاء نموذج DOM، ولكن ليس هناك شيء قوي مثل نظام قوالب Ember، حيث يفرض الحد الأدنى منه قرارات معينة، ويسمح بشيفرة برمجية أكثر تناقسًا مع الحفاظ على القالب أكثر هيكلية بدلًا من ملئه بشيفرة برمجية حسب الرغبة. ما هي حالة المساعد mut؟ انتقل المساعد mut مع إطار العمل Ember عند انتقاله من البيانات ذات الاتجاهين إلى مجرى البيانات الأكثر شيوعًا والأسهل ذي الاتجاه الواحد، إذ يمكن عدّ المساعدmut بمثابة تحويل وقت البناء الذي يغلّف وسيطه بتابع ضابط setter، إذ يسمح استخدام المساعدmut بالتصريح عن دوال إعدادات القالب فقط كما يلي: <Checkbox @value={{this.someData}} @onToggle={{fn (mut this.someData) (not this.someData)}} /> بينما ستكون هناك حاجة إلى صنف مكوّن بدون استخدام المساعدmut: import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; export default class Example extends Component { @tracked someData = false; @action setData(newValue) { this.someData = newValue; } } سيُستدعَى هذا الصنف بعد ذلك في القالب كما يلي: <Checkbox @data={{this.someData}} @onChange={{this.setData}} /> يمكن أن يكون استخدام mut مرغوبًا نظرًا لاختصاره، ولكنه يحتوي على دلالات غريبة يمكن أن تتسبّب في الكثير من الارتباك لمعرفة المقصود منها. كانت هناك بعض الأفكار الجديدة التي وُضِعت معًا في شكل إضافات تستخدِم واجهات برمجة تطبيقات ember-set-helper و ember-box، ويحاول كلاهما حل مشاكل mut من خلال تقديم مفاهيم أوضح، وتجنب تحولات وقت البناء وسلوك آلة Glimmer الافتراضية الضمني. باستخدام ember-set-helper: <Checkbox @value={{this.someData}} @onToggle={{set this "someData" (not this.someData)}} /> باستخدام ember-box: {{#let (box this.someData) as |someData|}} <Checkbox @value={{unwrap someData}} @onToggle={{update someData (not this.someData)}} /> {{/let}} لا تُعَدّ أيّ من هذه الحلول شائعةً بين أعضاء المجتمع، ولا يزال الأشخاص يحاولون اكتشاف واجهة برمجة تطبيقات مريحة وبسيطة لضبط البيانات في سياق القالب فقط دون أصناف جافاسكربت الداعمة. ما هو الغرض من المتحكمات؟ المتحكِّمات Controllers هي أنماط مفردة Singletons يمكن أن تساعد في إدارة تصيير السياق الخاص بالمسار النشط، وتعمل إلى حد كبير مثل أصناف جافاسكربت الداعمة للمكوّن، والمتحكِّمات هي -من شهر 1 عام 2020- الطريقة الوحيدة لإدارة محدّدات استعلام عناوين URL، إذ يجب أن تكون المتحكِّمات خفيفةً إلى حد ما في مسؤولياتها وفي تفويض المكوّنات والخدمات حيثما أمكن ذلك. ما هو الغرض من المسارات؟ يمثِّل المسار Route جزءًا من عنوان URL عندما ينتقل المستخدِم من مكان إلى آخر في التطبيق، فالمسار له المسؤوليات التالية فقط: تحميل الحد الأدنى من البيانات المطلوبة لتصيير المسار أو عرض الشجرة الفرعية. يُعَدّ بوابة الوصول إلى المسار وإعادة التوجيه إذا لزم الأمر. التعامل مع حالات التحميل والخطأ في الحد الأدنى من البيانات المطلوبة. يحتوي المسار على ثلاثة خطّافات لدورة الحياة فقط وجميعها اختيارية وهي: beforeModel: بوابة الوصول إلى المسار. model: مكان تحميل البيانات. afterModel: للتحقق من الوصول. يتمتع المسار بالقدرة على التعامل مع الأحداث الشائعة الناتجة عن إعداد الخطاف model: loading: ما يجب تطبيقه عند تحميل الخطّاف model. error: ما يجب فعله عند حدوث خطأ في الخطّاف model. يمكن لكل من loading و error تصيير القوالب الافتراضية بالإضافة إلى القوالب المخصَّصة المحددة في مكان آخر في التطبيق، مما يوحِّد حالات التحميل أو حالات الخطأ، كما يمكن العثور على مزيد من المعلومات حول ما يمكن للمسار تنفيذه في توثيق واجهة برمجة التطبيقات API. أخيرًا، إذا احتجت إلى أي مساعدة، فأضف سؤالك ضمن قسم الأسئلة والأجوبة في أكاديمية حسوب أو ضمن مجتمع البرمجة في حسوب IO وستصل إلى إجابتك من مجتمع المطورين العرب. ترجمة -وبتصرّف- للمقالين Routing in Ember وEmber resources and troubleshooting. اقرأ أيضًا المقال السابق: تنفيذ التفاعل في تطبيق Ember: وظيفة التذييل والعرض الشرطي مقدمة إلى إطار العمل Ember بنية تطبيق إطار العمل Ember وتقسيمها إلى مكونات
-
حان الوقت الآن لمعالجة وظيفة التذييل Footer في تطبيقنا، إذ سنحدِّث عدّاد المهام لإظهار العدد الصحيح للمهام التي يجب إكمالها، وسنطبّق التنسيق بصورة صحيحة على المهام المكتملة من خلال تحديد مربع الاختيار، كما سنفعِّل زر "مسح المهام المكتملة Clear completed"، وسنتعرّف على استخدام التصيير أو العرض الشرطي Conditional Rendering في قوالبنا (التصيير والعرض والإخراج هي مترادفات لكلمة rendering). المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ يُعَدّ فهم ميزات جافاسكربت الحديثة مثل الأصناف Classes والوحدات Modules وما إلى ذلك مفيدًا للغاية، لأن إطار العمل Ember يستخدمها بكثرة. الهدف: مواصلة تعلّم أصناف المكوّنات من خلال التعرّف على التصيير الشرطي وتفعيل بعض وظائف التذييل. توصيل السلوك بالتذييل يجب تطبيق الوظائف الثلاث التالية لكي يعمل التذييل: عدّاد المهام المعلَّقة. مرشحات لجميع المهام والمهام النشطة والمهام المكتملة. زر لمسح المهام المكتملة. أولًا، يجب إنشاء صنف Class للتذييل بما أننا نحتاج إلى الوصول إلى الخدمة من مكوّن التذييل، لذلك أدخِل الأمر التالي في الطرفية: ember generate component-class footer ثانيًا، ابحث بعد ذلك عن الملف todomvc/app/components/footer.js وعدّله إلى ما يلي: import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; export default class FooterComponent extends Component { @service('todo-data') todos; } ثالثًا، يجب الآن العودة إلى الملف todo-data.js وإضافة بعض الوظائف التي ستسمح بإعادة عدد المهام غير المكتملة لمعرفة عدد المهام المتبقية، ووظيفة مسح المهام المكتملة من القائمة التي يحتاجها زر "مسح المهام المكتملة Clear completed"، لذلك أضِف في الملف todo-data.js التابع الجالب Getter التالي بعد الجالب all() الموجود سابقًا لتحديد المهام غير المكتملة: get incomplete() { return this.todos.filterBy('isCompleted', false); } يمكننا باستخدام التابع Array.Proxy.filterBy() في Ember ترشيح الكائنات في المصفوفة بسهولة بناءً على شروط مساواةٍ بسيطة، إذ نريد في جزء الشيفرة السابق الحصول على جميع عناصر المهام عندما تكون الخاصية isCompleted مساوية للقيمة false، وسيُعاد حساب هذا التابع الجالب عندما تتغير قيمة الكائن في المصفوفة لأن الخاصية isCompleted مُميَّزة بالمزخرِف @tracked في الكائن Todo. رابعًا، أضف بعد ذلك ما يلي بعد الدالة add(text) الموجودة مسبقًا: @action clearCompleted() { this.todos = this.incomplete; } يُعَدّ ذلك رائعًا لمسح المهام، إذ نحتاج فقط إلى ضبط المصفوفة todos لتساوي قائمة المهام غير المكتملة. خامسًا، أخيرًا، يجب الاستفادة من هذه الوظيفة الجديدة في قالب footer.hbs، لذلك انتقل إلى هذا الملف الآن. سادسًا، ضَع أولًا مكان السطر التالي: <strong>0</strong> todos left ما يلي، إذ يُملَأ عدد المهام غير المكتملة بطول المصفوفة incomplete: <strong>{{this.todos.incomplete.length}}</strong> todos left سابعًا، ثم ضع مكان السطر التالي: <button type="button" class="clear-completed"> ما يلي: <button type="button" class="clear-completed" {{on 'click' this.todos.clearCompleted}}> إذا نقرتَ على الزر الآن، فسيُشغَّل الإجراء clearCompleted() الذي أضفناه سابقًا، ولكن إذا حاولت النقر على زر "مسح المهام المكتملة Clear Completed"، فلن يبدو التطبيق أنه يفعل أيّ شيء بسبب عدم وجود طريقة لإكمال المهام حاليًا، كما يجب توصيل القالب todo.hbs بالخدمة، بحيث يؤدي تحديد مربع الاختيار المتعلق به إلى تغيير حالة كل مهمة. مشكلة كتابة todos بدلا من todo لدينا مشكلة صغيرة أخرى نتعامل معها، إذ تشير العبارة "todos left" إلى وجود عدد من المهام المتبقية بالرغم من وجود مهمة واحدة متبقية أحيانًا، وهذا سيء قواعديًا. يمكن حل هذه المشكلة من خلال تعديل هذا الجزء من القالب ليحتوي على التصيير الشرطي، إذ يمكنك في Ember تصيير أجزاء من القالب شرطيًا باستخدام المحتوى الشرطي مثل الكتلة البسيطة التالية: {{#if this.thingIsTrue}} Content for the block form of "if" {{/if}} ضع مكان الجزء التالي من footer.hbs: <strong>{{this.todos.incomplete.length}}</strong> todos left ما يلي: <strong>{{this.todos.incomplete.length}}</strong> {{#if this.todos.incomplete.length === 1}} todo {{else}} todos {{/if}} left سيؤدي ذلك إلى إعطاء خطأ، ولكن لا تستطيع عبارات if البسيطة هذه في Ember حاليًا اختبار تعبير معقد مثل الموازنة، وإنما تستطيع اختبار قيمة الصواب أو الخطأ فقط، لذلك يجب إضافة جالب getter إلى الملف todo-data.js لإعادة النتيجة this.incomplete.length === 1 ثم استدعاؤها في القالب. أضف الجالب الجديد الآتي إلى الملف todo-data.js بعد التوابع الجالبة الموجودة مسبقًا مباشرةً، ولاحظ أننا نحتاج إلى this.incomplete.length وليس this.todos.incomplete.length لأننا نطبّق ذلك ضمن الخدمة حيث يتوفر الجالب incomplete() مباشرةً، كما أنّ محتويات الخدمة متوفرة في القالب مثل المهام todos عبر التعليمة @service('todo-data') todos; ضمن صنف التذييل، وبالتالي سيكون this.todos.incomplete.length هناك. get todoCountIsOne() { return this.incomplete.length === 1; } ارجع بعد ذلك إلى footer.hbs وعدِّل قسم القالب السابق الذي عدّلناه إلى ما يلي: <strong>{{this.todos.incomplete.length}}</strong> {{#if this.todos.todoCountIsOne}} todo {{else}} todos {{/if}} left احفظ الملف واختبره، وسترى الكلمة الصحيحة المُستخدَمة عندما يكون لديك عنصر واحد لتنفيذه. لاحظ صيغة كتلة if في Ember، ويمكنك استخدام الشكل المضمَّن التالي: {{if this.todos.todoCountIsOne "todo" "todos"}} استكمال المهام يجب استخدام صنف للوصول إلى الخدمة كما هو الحال مع المكونات الأخرى. إنشاء الصنف todo أولًا، شغّل الأمر التالي في الطرفية: ember generate component-class todo ثانيًا، انتقل الآن إلى الملف todomvc/app/components/todo.js وعدّل محتوياته لتبدو كما يلي لمنح المكوّن todo إمكانية الوصول إلى الخدمة: import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; export default class TodoComponent extends Component { @service('todo-data') todos; } ثالثًا، ارجع مرةً أخرى إلى ملف الخدمة todo-data.js وأضف الإجراء التالي بعد الإجراءات السابقة مباشرةً، مما سيسمح بتبديل حالة الاكتمال لكل مهمة: @action toggleCompletion(todo) { todo.isCompleted = !todo.isCompleted; } تحديث القالب لإظهار الحالة المكتملة أخيرًا، سنعدّل القالب todo.hbs بحيث ترتبط قيمة مربع الاختيار بالخاصية isCompleted في المهمة، حيث يُستدعَى التابع toggleCompletion() في خدمة المهمة عند التعديل. أولًا، ابحث أولًا عن السطر التالي في الملف todo.hbs: <li> وضَع مكانه ما يلي، إذ ستلاحظ أننا نستخدم المحتوى الشرطي لإضافة قيمة الصنف إذا كان ذلك مناسبًا: <li class="{{ if @todo.isCompleted 'completed' }}"> ثانيًا، ابحث بعد ذلك عمّا يلي: <input aria-label="Toggle the completion of this todo" class="toggle" type="checkbox" > وضَع مكانه ما يلي: <input class="toggle" type="checkbox" aria-label="Toggle the completion of this todo" checked={{ @todo.isCompleted }} {{ on 'change' (fn this.todos.toggleCompletion @todo) }} > ملاحظة: يستخدِم جزء الشيفرة السابق كلمةً مفتاحيةً جديدةً خاصةً بإطار عمل Ember هي fn التي تسمح بالتطبيق الجزئي Partial Application، وهو مشابه للتابع bind لكنه لا يغير سياق الاستدعاء، ويكافئ استخدام التابع bind مع الوسيط الأول null. أعِد تشغيل خادم التطوير وانتقل إلى المضيف المحلي localhost:4200 مرةً أخرى، وسترى أنه لدينا عدّاد "المهام المتبقية todos left" وزر "المسح Clear": يمكن أن تسأل نفسك لماذا لا نطبّق التبديل على المكوِّن فقط؟ نظرًا لأن الدالة قائمة بذاتها ولا تحتاج على الإطلاق إلى أيّ شيء من الخدمة، وبما أننا في النهاية سنرغب في الاستمرار أو مزامنة جميع تغييرات قائمة المهام مع التخزين المحلي (اطّلع على الإصدار الأخير من التطبيق)، فستكون جميع عمليات تغيير الحالة المستمرة في المكان نفسه. الخلاصة يمكننا الآن وضع علامة على المهام المكتملة ومسحها أيضًا، والشيء الوحيد المتبقي لتفعيل التذييل هو عمليات ترشيح "جميع المهام All" و"المهام النشطة Active" و"المهام المكتملة Completed"، إذ سنطبّق ذلك في المقال التالي باستخدام التوجيه Routing. ترجمة -وبتصرّف- للمقال Ember Interactivity: Footer functionality, conditional rendering. اقرأ أيضًا المقال السابق: تنفيذ التفاعل في تطبيق Ember: الأحداث والأصناف والحالة بنية تطبيق إطار العمل Ember وتقسيمها إلى مكونات مقدمة إلى إطار العمل Ember
-
سنبدأ في هذا المقال بإضافة بعض التفاعل إلى تطبيقنا مما يوفر القدرة على إضافة وعرض عناصر مهام جديدة، كما سنتعرّف على استخدام الأحداث في إطار العمل Ember، وإنشاء أصناف مكوّنات لتحتوي على شيفرة جافاسكربت JavaScript للتحكم بالميزات التفاعلية وإعداد خدمة لتتبّع حالة بيانات تطبيقنا. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ يُعَدّ فهم ميزات جافاسكربت الحديثة مثل الأصناف Classes، والوحدات Modules، وما إلى ذلك مفيدًا للغاية لأن إطار العمل Ember يستخدمها بكثرة. الهدف: معرفة كيفية إنشاء أصناف المكوّنات واستخدام الأحداث للتحكم في التفاعل وتتبع حالة التطبيق باستخدام خدمة. إضافة التفاعل لدينا الآن نسخة من تطبيق قائمة المهام todo app مقسَّمة إلى مكوّنات ومُعاد بناؤها، ولنتعرَّف على كيفية إضافة التفاعل الذي نحتاجه ليعمل التطبيق، إذ يجب التصريح عن أهداف ومسؤوليات كل مكوّن عند البدء في التفكير في التفاعل كما سنوضّح فيما يلي، ثم سنوجّهك لتتعلم كيفية تنفيذ ذلك. إنشاء المهام نريد أن نتمكن من إرسال المهمة المكتوبة في حقل إدخال المهام عندما نضغط على مفتاح Enter لتظهر في قائمة المهام، ويجب أن نكون قادرين على التقاط النص المكتوب في حقل الإدخال لتعرف شيفرة جافاسكربت ما كتبناه، كما يمكننا حفظ مهامنا وتمرير هذا النص إلى مكوّن قائمة المهام لعرضه، ويمكننا التقاط الحدث keydown باستخدام المُعدِّل on الذي يُعَدّ صيغةً مبسَّطةً في Ember للتابعَين addEventListener و removeEventListener. أضف السطر الجديد الموضَّح أدناه إلى الملف header.hbs: <input class='new-todo' aria-label='What needs to be done?' placeholder='What needs to be done?' autofocus {{on 'keydown' this.onKeyDown}} > توضَع هذه السمة الجديدة بين أقواس مزدوجة معقوصة، ويدل ذلك على أنها جزء من صيغة قالب آلي في إطار عمل Ember، فالوسيط الأول المُمرَّر إلى on هو نوع الحدث الذي يجب الاستجابة له keydown، والوسيط الأخير هو معالِج الحدث أي الشيفرة المُشغَّلة استجابةً لإطلاق الحدث keydown، في حين تشير الكلمة المفتاحية this إلى سياق Context أو نطاق Scope المكوّن الذي سيختلف بين مكوّن وآخر. يمكننا تحديد ما هو متاح ضمن this من خلال إنشاء صنف مكوّن ليتماشى مع مكوّنك، وهو صنف بلغة جافاسكربت الصرفة Vanilla JavaScript وليس له معنًى خاصًا في إطار عمل Ember باستثناء وراثة أو توسعة الصنف الأب Component. يمكنك إنشاء الصنف header ليتوافق مع مكوّن الترويسة من خلال كتابة الأمر التالي في الطرفية: ember generate component-class header مما يؤدي إلى إنشاء ملف الصنف الفارغ التالي في الملف todomvc/app/components/header.js: import Component from '@glimmer/component'; export default class HeaderComponent extends Component { } سننفّذ في هذا الملف شيفرة معالج الحدث، لذلك عدّل محتواه إلى ما يلي: import Component from '@glimmer/component'; import { action } from '@ember/object'; export default class HeaderComponent extends Component { @action onKeyDown({ target, key }) { let text = target.value.trim(); let hasValue = Boolean(text); if (key === 'Enter' && hasValue) { alert(text); target.value = '' } } } يُعَدّ المزخرِف @action الشيفرة الوحيدة الخاصة بإطار العمل Ember هنا بغض النظر عن وراثة الصنف الأب Component والعناصر الخاصة بإطار العمل Ember التي نستوردها باستخدام صيغة وحدة جافاسكربت، بينما باقي الملف مكتوب بلغة جافاسكربت الصرفة Vanilla JavaScript ويمكن أن يعمل في أيّ تطبيق آخر، كما يصرِّح المزخرِف @action عن أن الدالة هي إجراء action، مما يعني أنها نوع من الدوال التي ستُستدعَى من حدث وقع في القالب، كما أنّ @action يربط this الخاص بالدالة بنسخة من الصنف. ملاحظة: يُعَدّ المزخرِف Decorator دالة تغليف تغلِّف وتستدعي دوالًا أو خاصيات أخرى مما يوفر وظائفًا إضافيةً، إذ يشغّل المزخرِف @tracked مثلًا الشيفرة المُطبَّقة عليه، ويتتبّعه ويحدّث التطبيق تلقائيًا عند تغيير القيم. عُد إلى تبويب متصفحك مع تشغيل التطبيق، إذ يمكننا كتابة ما نريد وستظهر رسالة تنبيه تخبرنا بما كتبناه بالضبط عندما نضغط على مفتاح Enter. نحتاج الآن لمكان لتخزين المهام لتتمكن المكوّنات الأخرى من الوصول إليها. تخزين المهام باستخدام خدمة يملك إطار عمل Ember إدارة حالة مبنية مسبقًا على مستوى التطبيق يمكننا استخدامها لإدارة تخزين مهامنا، والسماح لكل مكوّن من مكوّناتنا الوصول إلى البيانات من تلك الحالة على مستوى التطبيق، إذ يستدعي إطار Ember خدمات البناء التي تبقى قيد التشغيل طوال عُمر الصفحة، إذ سيؤدي تحديث الصفحة إلى مسحها. شغّل الأمر التالي في الطرفية لإنشاء خدمة لتخزين بيانات قائمة المهام: ember generate service todo-data يعطي تشغيل الأمر السابق خرجًا يشبه الخرج التالي: installing service create app/services/todo-data.js installing service-test create tests/unit/services/todo-data-test.js مما يؤدي إلى إنشاء الملف todo-data.js ضمن المجلد todomvc/app/services لاحتواء خدمتنا، كما يحتوي هذا الملف في البداية على عبارة استيراد وصنف فارغ كما يلي: import Service from '@ember/service'; export default class TodoDataService extends Service { } يجب أولًا تتبّع كل من نص المهمة وما إذا كانت مكتملة أم لا، لذا أضف تعليمة الاستيراد التالية بعد تعليمة الاستيراد الموجودة مسبقًا: import { tracked } from '@glimmer/tracking'; أضف الصنف التالي بعد السطر السابق الذي أضفته: class Todo { @tracked text = ''; @tracked isCompleted = false; constructor(text) { this.text = text; } } يمثل الصنف السابق مهمَّة، لأنه يحتوي على الخاصية @tracked text التي تحتوي على نص المهمَّة، ويحتوي على الخاصية @tracked isCompleted التي تحدِّد ما إذا كانت المهام مكتملةً أم لا، فإذا أنشأنا نسخةً من هذا الصنف، فسيكون للكائن Todo قيمة text أولية تساوي النص المعطَى له عند إنشائه، وتُعطَى الخاصية isCompleted القيمة false. الجزء الوحيد الخاص بإطار عمل Ember من هذا الصنف هو المزخرِف @tracked الذي يرتبط بنظام التفاعل ويسمح لإطار Ember بتحديث ما تراه في تطبيقك تلقائيًا إذا تغيرت الخاصيات المُتتبَّعة tracked، وحان الوقت الآن لإضافة شيء ما إلى جسم الخدمة، لذا أضِف أولًا تعليمة استيراد import أخرى بعد التعليمة السابقة لإتاحة الإجراءات actions ضمن الخدمة: import { action } from '@ember/object'; عدّل الكتلة export default class TodoDataService extends Service { … } كما يلي: export default class TodoDataService extends Service { @tracked todos = []; @action add(text) { let newTodo = new Todo(text); this.todos = [...this.todos, newTodo]; } } ستحتفظ الخاصية todos في الخدمة بقائمة مهامنا الموجودة ضمن مصفوفة، وسنميّزها بالمزخرِف @tracked لأننا نريد تحديث واجهة المستخدِم أيضًا عند تحديث قيمة الخاصية todos. يُضاف المزخرِف @action إلى الدالة add() التي يستدعيها القالب لربطها بنسخة الصنف. يمكن أن يكون ذلك مألوفًا في لغة جافاسكربت، ولكن لاحظ استدعاء التابع pushObject() في المصفوفة todos، والسبب أن إطار عمل Ember يوسّع نموذج المصفوفات في لغة جافاسكربت افتراضيًا، مما يمنحنا توابعًا ملائمة لضمان معرفة نظام التعقّب في إطار Ember بهذه التغييرات. هناك العشرات من هذه التوابع مثل pushObject() أو insertAt() أو popObject() التي يمكن استخدامها مع أيّ نوع وليس مع الكائنات فقط. يمنحنا التابع ArrayProxy الخاص بإطار عمل Ember توابعًا سهلة الاستخدام مثل isAny() وfindBy() وfilterBy() لتسهيل الأمور. استخدام الخدمة من مكون الترويسة يمكننا الآن بعد أن تحديد طريقة لإضافة المهام التفاعل مع هذه الخدمة من مكوّن الإدخال في الملف header.js لبدء إضافتها فعليًا، إذ يجب أولًا حقن الخدمة في القالب باستخدام المزخرِف @inject الذي سنعيد تسميته إلى @service. أضف تعليمة الاستيراد التالية إلى الملف header.js بعد تعليمتَي الاستيراد الموجودتَين مسبقًا: import { inject as service } from '@ember/service'; يمكننا الآن توفير الخدمة todo-data ضمن الصنف HeaderComponent عبر الكائن todos باستخدام المزخرِف @service، لذلك أضف السطر التالي بعد سطر التصدير export الأول مباشرةً: @service('todo-data') todos; يمكن الآن استبدال سطر النص البديل alert(text); باستدعاء الدالة add() الجديدة كما يلي: this.todos.add(text); إذا جربنا ذلك في تطبيق المهام في متصفحنا من خلال كتابة الأمر npm start ثم الانتقال إلى المضيف المحلي localhost:4200، فلن يحدث أيّ شيء بعد الضغط على مفتاح Enter، إذ يُعَدّ بناء التطبيق بدون أيّ أخطاء علامةً جيدةً، ولكن يمكننا رؤية إضافة مهامنا باستخدام الفاحص Ember Inspector كما يلي: عرض المهام يجب أن تكون هناك طريقة لوضع المهام التي ننشئها فعليًا مكان مهامنا الساكنة "Buy Movie Tickets"، إذ يجب في المكوّن TodoList إخراج المهام من الخدمة وتصيير Render المكوّن Todo لكل مهمة، كما يمكن استعادة المهام من الخدمة، ولكن يحتاج المكوّن TodoList أولًا إلى صنف داعم للمكوّن لاحتواء هذه الوظيفة، لذلك اضغط على الاختصار Ctrl + C لإيقاف خادم التطوير وأدخِل الأمر التالي في الطرفية: ember generate component-class todo-list يؤدي ذلك إلى إنشاء صنف المكوّن الجديد في المجلد todomvc/app/components/todo-list.js. املأ هذا الملف بالشيفرة التالية بحيث يمكن لقالبنا الوصول إلى الخدمة todo-data باستخدام الخاصية todos، ويمكن الوصول إليها باستخدام this.todos ضمن كل من الصنف والقالب: import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; export default class TodoListComponent extends Component { @service('todo-data') todos; } تتمثّل إحدى المشاكل هنا في أن الخدمة تسمَّى todos وتُسمَّى قائمة المهام todos أيضًا، لذلك يمكننا حاليًا الوصول إلى البيانات باستخدام this.todos.todos. لا يُعَد هذا أمرًا سهلًا، لذلك سنضيف تابعًا جالبًا getter إلى الخدمة this.todos.todos بالاسم all سيمثِّل جميع المهام. ارجع إلى الملف todo-data.js وأضِف ما يلي بعد التعليمة @tracked todos = [];: get all() { return this.todos; } يمكننا الآن الوصول إلى البيانات باستخدام this.todos.all، إذ يُعَدّ ذلك أسهل. انتقل إلى المكوّن todo-list.hbs، وضع مكان استدعاءات المكوّن الساكنة التالية: <Todo /> <Todo /> كتلة #each الآلية، وهي صيغة مُبسطَّة من تابع جافاسكربت forEach()، إذ تنشئ كتلة #each المكوّنَ <Todo /> لكل مهمة متوفرة في قائمة المهام التي يعيدها التابع الجالب all() الخاص بالخدمة: {{#each this.todos.all as |todo|}} <Todo @todo={{todo}} /> {{/each}} لنتعرّف على محتويات الشيفرة السابقة: this: سياق التصيير أو نسخة المكوّن. todos: خاصية this التي عرّفناها في المكوّن todo-list.js باستخدام التعليمة @service('todo-data') todos;، وهي مرجع إلى الخدمة todo-data، مما يسمح بالتفاعل مع نسخة الخدمة مباشرةً. all: جالب الخدمة todo-data الذي يعيد جميع المهام. جرّب تشغيل الخادم مرةً أخرى وانتقل إلى التطبيق، وستجده يعمل، ولكن كلما أدخلت عنصر مهمة جديد، فسيظهر عنصر قائمة جديد تحت حقل إدخال النص، وستظهر العبارة "Buy Movie Tickets" دائمًا للأسف، لأن عنوان النص في كل عنصر قائمة مضمَّنٌ في ذلك النص كما هو موضَّح في الملف todo.hbs: <label>Buy Movie Tickets</label> عدِّل السطر السابق كما يلي لاستخدام الوسيط @todo الذي سيمثل المهمة التي مررناها إلى المكوّن عند استدعائه ضمن الملف todo-list.hbs في السطر <Todo @todo={{todo}} />: <label>{{@todo.text}}</label> جرّبه مرةً أخرى، ويجب أن تجد الآن أنّ النص المُرسَل من حقل الإدخال <input> يظهر بصورة صحيحة في واجهة المستخدِم كما يلي: الخلاصة يمكننا الآن إضافة عناصر المهمات إلى تطبيقنا، كما يمكن تتبّع حالة البيانات باستخدام الخدمة، وسننتقل في المقال التالي إلى تشغيل وظائف التذييل Footer بما في ذلك عدّاد المهام، كما سنتعرّف على التصيير الشرطي وتصميم المهام بصورة صحيحة عند تحديدها، وسنفعّل زر "مسح المهام المكتملة Clear completed". ترجمة -وبتصرّف- للمقال Ember interactivity: Events, classes and state. اقرأ أيضًا المقال السابق: بنية تطبيق إطار العمل Ember وتقسيمها إلى مكونات مقدمة إلى إطار العمل Ember
-
سنخطط في هذا المقال بنية تطبيق TodoMVC في إطار العمل Ember، وسنضيف توصيف HTML له، ثم سنقسّم بنية HTML إلى مكونات. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية. يُعَد فهم ميزات جافاسكربت الحديثة مثل الأصناف Classes والوحدات Modules وما إلى ذلك مفيدًا للغاية، لأن إطار العمل Ember يستخدِمها بكثرة. الهدف: تعلّم كيفية إنشاء بنية تطبيق Ember، ثم تقسيم هذه البنية إلى مكونات. التخطيط لتصميم تطبيق TodoMVC أنشأنا في المقال السابق مشروع Ember جديد ثم أضفنا وضبطنا أنماط CSS، وسنضيف الآن توصيف HTML وسنخطط بنية ودلالات تطبيق TodoMVC. يُعَدّ توصيف HTML لصفحة هبوط تطبيقنا مُعرَّفًا في المسار app/templates/application.hbs، وهو موجود مسبقًا وتبدو محتوياته حاليًا كما يلي: {{!-- يعرض المكون التالي رسالة ترحيب Ember الافتراضية. --}} <WelcomePage /> {{!-- لا تتردد في إزالتها --}} {{outlet}} المكوّن <WelcomePage /> هو مكون توفّره إضافة Ember التي تصيِّر Render صفحة الترحيب الافتراضية التي رأيناها في المقال السابق عندما انتقلنا لأول مرة إلى الخادم على المضيف المحلي localhost:4200، لكننا لا نريد ذلك، وإنما نريد أن يحتوي هذا المكون على بنية تطبيق TodoMVC، لذلك احذف محتويات الملف application.hbs واستبدلها بما يلي: <section class="todoapp"> <h1>todos</h1> <input class="new-todo" aria-label="What needs to be done?" placeholder="What needs to be done?" autofocus > </section> احفظ الملف application.hbs، إذ سيَعيد خادم التطوير الذي شغّلته سابقًا بناء التطبيق وتحديث المتصفح تلقائيًا، أي يجب أن يبدو الخرج المُصيَّر الآن كما يلي: لا يتطلب الأمر كثيرًا من الجهد لجعل توصيف HTML يبدو مثل تطبيق قائمة مهام كامل الميزات. عدِّل الملف application.hbs مرةً أخرى ليكون محتواه على النحو التالي: <section class="todoapp"> <h1>todos</h1> <input class="new-todo" aria-label="What needs to be done?" placeholder="What needs to be done?" autofocus > <section class="main"> <input id="mark-all-complete" class="toggle-all" type="checkbox"> <label for="mark-all-complete">Mark all as complete</label> <ul class="todo-list"> <li> <div class="view"> <input aria-label="Toggle the completion of this todo" class="toggle" type="checkbox" > <label>Buy Movie Tickets</label> <button type="button" class="destroy" title="Remove this todo" ></button> </div> <input autofocus class="edit" value="Todo Text"> </li> <li> <div class="view"> <input aria-label="Toggle the completion of this todo" class="toggle" type="checkbox" > <label>Go to Movie</label> <button type="button" class="destroy" title="Remove this todo" ></button> </div> <input autofocus class="edit" value="Todo Text"> </li> </ul> </section> <footer class="footer"> <span class="todo-count"> <strong>0</strong> todos left </span> <ul class="filters"> <li> <a href="#">All</a> <a href="#">Active</a> <a href="#">Completed</a> </li> </ul> <button type="button" class="clear-completed"> Clear Completed </button> </footer> </section> يجب أن يكون الخرج المُصيَّر الآن على النحو التالي: يبدو التطبيق مكتملًا، ولكن تذكّر أنه الآن مجرد نموذج أولي ساكن، لذلك يجب تقسيم توصيف HTML إلى مكونات آلية، إذ سنحوّله لاحقًا إلى تطبيق تفاعلي بالكامل. إذا نظرنا إلى الشيفرة الموجودة بجوار تطبيق المهام المُصيَّر في الشكل التالي، فهناك طرق متعددة يمكن من خلالها تحديد كيفية تقسيم واجهة المستخدِم UI، ولكن لنخطّط الآن لتقسيم توصيف HTML إلى المكونات التالية: مجموعات المكونات هي كما يلي: حقل الإدخال الرئيسي new-todo: باللون الأحمر في الشكل السابق. الجسم Body الذي يحتوي على قائمة المهام والزر الذي يضع علامة على جميع المهام المكتملة mark-all-complete: باللون الأرجواني في الشكل السابق. الزر mark-all-complete المميز بوضوح للأسباب التي سنوضّحها لاحقًا: باللون الأصفر في الشكل السابق. كل مهمة هي مكوّن لوحده: باللون الأخضر في الشكل السابق. التذييل Footer: باللون الأزرق في الشكل السابق. لاحظ أنّ مربع الاختيار mark-all-complete المميَّز باللون الأصفر يُصيَّر بجوار حقل الإدخال new-todo أثناء وجوده في القسم main لأن تنسيقات CSS الافتراضية تضع مربع الاختيار والعنصر label بقيم سالبة للإحداثيات العلوية واليسارية لتحريكهما بجوار حقل الإدخال، عوضًا عن وضعهما في القسم main. استخدام واجهة سطر الأوامر CLI لإنشاء المكونات نريد إنشاء أربعة مكونات لتمثيل تطبيقنا وهذه المكونات هي: الترويسة Header. القائمة List. عنصر المهمة Todo. التذييل Footer. استخدِم الأمر ember generate component متبوعًا باسم المكوّن لإنشائه، ولننشئ مكوّن الترويسة أولًا باتباع الخطوات التالية: أوقف تشغيل الخادم بالانتقال إلى الطرفية والضغط على الاختصار Ctrl + C. أدخِل الأمر التالي في الطرفية: ember generate component header سينشئ هذا الأمر بعض الملفات الجديدة، كما هو موضّح في ناتج الطرفية النهائي التالي: installing component create app/components/header.hbs skip app/components/header.js tip to add a class, run `ember generate component-class header` installing component-test create tests/integration/components/header-test.js يُعَدّ الملف header.hbs بأنه ملف القالب الذي سيتضمّن بنية HTML لمكوّن الترويسة فقط، وسنضيف لاحقًا الوظائف الآلية المطلوبة مثل روابط البيانات والاستجابة لتفاعل المستخدِم وما إلى ذلك. يُعَدّ الملف header-test.js ملفًا مخصَّصًا لكتابة الاختبارات الآلية للتأكد من أن تطبيقنا يستمر في العمل بمرور الوقت أثناء الترقية وإضافة الميزات وإعادة البناء وما إلى ذلك، ولن نتطرّق للاختبار في هذا المقال، ولكن يجب تطبيق الاختبار أثناء عملية التطوير وليس بعده، إذ يمكن أن تنساه لاحقًا. لننشئ شيفرة مساعِدة Scaffolding للمكوّنات الأخرى قبل إضافة أيّ شيفرة مكوّن، وبالتالي أدخِل الأوامر التالية في طرفيتك واحدًا تلو الآخر: ember generate component todo-list ember generate component todo ember generate component footer سترى الآن ما يلي في المجلد todomvc/app/components: بما أنه أصبح لدينا الآن جميع ملفات بنية المكونات، فيمكننا قص ولصق توصيف HTML لكل مكوّن من الملف application.hbs إلى كل مكوّن من هذه المكوّنات، ثم تعديل الملف application.hbs ليمثّل تجريداتنا الجديدة. أولًا، يجب تعديل الملف header.hbs ليحوي ما يلي: <input class="new-todo" aria-label="What needs to be done?" placeholder="What needs to be done?" autofocus > ثانيًا، كما يجب تعديل الملف todo-list.hbs ليحوي ما يلي: <section class="main"> <input id="mark-all-complete" class="toggle-all" type="checkbox"> <label for="mark-all-complete">Mark all as complete</label> <ul class="todo-list"> <Todo /> <Todo /> </ul> </section> ثالثًا، أضف ما يلي إلى الملف todo.hbs: <li> <div class="view"> <input aria-label="Toggle the completion of this todo" class="toggle" type="checkbox" > <label>Buy Movie Tickets</label> <button type="button" class="destroy" title="Remove this todo" ></button> </div> <input autofocus class="edit" value="Todo Text"> </li> رابعًا، يجب تعديل الملف footer.hbs بحيث يحوي ما يلي: <footer class="footer"> <span class="todo-count"> <strong>0</strong> todos left </span> <ul class="filters"> <li> <a href="#">All</a> <a href="#">Active</a> <a href="#">Completed</a> </li> </ul> <button type="button" class="clear-completed"> Clear Completed </button> </footer> خامسًا، أخيرًا، يجب تعديل محتويات الملف application.hbs بحيث تُستدَعى المكونات المناسبة كما يلي: <section class="todoapp"> <h1>todos</h1> <Header /> <TodoList /> <Footer /> </section> سادسًا، شغِّل الأمر npm start في الطرفية مرةً أخرى بعد إجراء هذه التعديلات، ثم توجَّه إلى المضيف المحلي http://localhost:4200 للتأكد من أنّ تطبيق المهام لا يزال يبدو كما كان قبل إعادة البناء. لاحظ كيف يعرض كلا عنصري المهام العبارة "Buy Movie Tickets" بسبب استدعاء المكوّن نفسه مرتين، ونَص المهمة ثابت فيه. الخلاصة يبدو كل شيء كما ينبغي، إذ نجحنا في إعادة بناء توصيف HTML إلى مكوّنات، وسنبدأ في المقال التالي بالتعرّف على إضافة التفاعل إلى تطبيق Ember. ترجمة -وبتصرّف- للمقال Ember app structure and componentization. اقرأ أيضًا المقال السابق: مقدمة إلى إطار العمل Ember
-
سنلقي نظرةً في هذا المقال على إطار العمل Ember وآلية عمله وفوائده، وكيفية تثبيت سلسلة أدواته محليًا وإنشاء تطبيق نموذجي ثم إجراء إعداد أولي لتجهيز التطبيق للتطوير. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، كما يُعَدّ فهم ميزات جافاسكربت الحديثة مثل الأصناف Classes والوحدات Modules وما إلى ذلك مفيدًا للغاية، لأن إطار العمل Ember يستخدِمها بكثرة. الهدف: معرفة كيفية تثبيت إطار عمل Ember وإنشاء تطبيق بسيط. يُعَدّ Ember إطار عمل من النوع خدمة-مكونات Component-service يركّز على عملية تطوير تطبيقات الويب وإضفاء تجربة مميزة على واجهاتها ويقلل من الاختلافات بين التطبيقات، ويُعَدّ طبقةً حديثةً وخفيفةً فوق طبقة جافاسكربت الأصيلة Native، كما يتمتع بتوافق كبير مع الإصدارات السابقة واللاحقة لمساعدة الشركات على مواكبة أحدث إصدارات Ember وأحدث الاتفاقيات التي يقودها المجتمع. تُعَدّ المكونات حزمًا من شيفرات السلوك والتنسيق Style والتوصيف Markup التي تشبه إلى حد كبير ما توفره أطر عمل الواجهة الأمامية الأخرى مثل React و Vue و Angular، كما يوفر جانب الخدمة حالةً مشتركةً طويلة الأمد وسلوكًا وواجهةً للتكامل مع المكتبات أو الأنظمة الأخرى، إذ يُعَدّ الموجِّه Router الذي سنشرحه لاحقًا خدمةً مثلًا، وتشكّل المكونات والخدمات القسم الأكبر من تطبيق EmberJS. هذا المقال جزء من سلسلة تقديمية حول إطار العمل Ember وإليك فهرس كامل السلسلة: مقدمة إلى إطار العمل Ember بنية تطبيق إطار العمل Ember وتقسيمها إلى مكونات تنفيذ التفاعل في تطبيق Ember: الأحداث والأصناف والحالة تنفيذ التفاعل في تطبيق Ember: وظيفة التذييل والعرض الشرطي التوجيه Routing في إطار العمل Ember حالات الاستخدام يناسب إطار عمل EmberJS بناء التطبيقات التي تستهدف إحدى السمتين التاليتين أو كلتيهما: تطبيقات الصفحة الواحدة، بما في ذلك تطبيقات الويب الشبيهة بالتطبيقات الأصيلة وتطبيقات الويب التقدمية Progressive Web Apps أو PWA اختصارًا. يعمل إطار عمل Ember بصورة أفضل عندما يشكِّل الواجهة الأمامية الكاملة لتطبيقك. زيادة التماسك بين العديد من تقنيات الفرق البرمجية. تتيح أفضل الممارسات التي يدعمها المجتمع سرعة تطوير أكبر على المدى الطويل. يملك إطار عمل Ember اصطلاحات أو اتفاقيات واضحة ومفيدة لفرض التناسق ومساعدة أعضاء الفريق على العمل بسرعة. إضافات إطار عمل Ember يمتلك إطار العمل EmberJS معمارية الإضافات Plugin، مما يعني أنه يمكن تثبيت الإضافات Add-ons وتوفير وظائف إضافية دون كثير من الإعداد إذا وُجِد، ومن هذه الإضافات: PREmber: تصيير Rendering موقع ويب ساكن للمدونات أو المحتوى التسويقي. FastBoot: تصيير من طرف الخادم، بما في ذلك تحسين محركات البحث SEO، أو تحسين أداء التصيير الأولي لصفحات الويب المعقدة عالية التفاعل. empress-blog: تأليف منشورات المدونات باستخدام لغة ماركداون Markdown مع تحسين محركات البحث SEO باستخدام الإضافة PREmber. ember-service-worker: إعداد تطبيق ويب تقدمي PWA بحيث يمكن تثبيت التطبيق على الأجهزة المحمولة مثل تثبيته من متجر تطبيقات الجهاز نفسه. تطبيقات الجوال الأصيلة يمكن استخدام إطار عمل Ember مع تطبيقات الأجهزة المحمولة الأصيلة مع جسر بين تطبيقات الهاتف المحمول الأصيلة ولغة جافاسكربت مثل الجسر الذي توفره واجهة Corber. الآراء والأعراف يُعَدّ إطار العمل EmberJS أحد أكثر أطر عمل الواجهة الأمامية تشبثًا برأيه، وتُعَدّ الآراء في Ember مجموعةً من الاتفاقيات أو الأعراف التي تساعد على زيادة كفاءة المطورين، ولكن يجب تعلّم تلك الاتفاقيات، إذ تساعد الآراء التي تدعم الاتفاقيات في تقليل الاختلافات بين التطبيقات مثل اللغات المُستخدَمة والنظام المجتمعي بفضل تعريف هذه الاتفاقيات ومشاركتها، وهو هدف مشترك بين جميع الأطر المتشبثة برأيها، ويصبح المطورون بعدها أكثر قدرةً على التبديل بين المشاريع والتطبيقات دون الحاجة إلى إعادة تعلم المعمارية والأنماط والمصطلحات وما إلى ذلك، وستلاحظ خلال هذا المقال آراء إطار العمل Ember مثل اصطلاحات تسمية ملفات المكونات. ارتباط إطار عمل Ember مع لغة جافاسكربت الصرفة Vanilla JavaScript بُني إطار عمل Ember على تقنيات جافاسكربت، ويُعَدّ طبقةً رقيقةً فوق البرمجة التقليدية كائنية التوجه، مع السماح للمطورين باستخدام تقنيات البرمجة الوظيفية. يستخدِم Ember صيغتين رئيسيتين هما: جافاسكربت أو لغة TypeScript اختياريًا. لغة قوالب Ember الخاصة التي تعتمد على لغة Handlebars. تُستخدَم لغة القوالب Templating Language لتحسين عملية البناء ووقت التشغيل، وهي مجموعة شاملة من لغة HTML، وبالتالي يمكن لأيّ شخص يعرف لغة HTML تقديم مساهمات مهمة لأيّ مشروع من مشاريع Ember، كما يمكن للمصممين وغيرهم من غير المطورين المساهمة في قوالب الصفحات دون أي معرفة بلغة جافاسكربت، ثم يمكن إضافة التفاعل لاحقًا، كما تتيح لغة القوالب هذه حمولات أصول أخف نظرًا لتصريف القوالب في شيفرة ثنائية Byte Code يمكن تحليلها بسرعة أكبر من تحليل شيفرة جافاسكربت. كل شيء آخر في Ember هو جافاسكربت وخاصةً أصناف جافاسكربت التي تُعَدّ المكان الذي تعمل فيه معظم أجزاء إطار العمل حيث توجد الأصناف الأبناء، ويكون لكل نوع من الأشياء غرض مختلف وموقع متوقع مختلف ضمن مشروعك. يوضِّح الشكل التالي تأثير Ember على جافاسكربت في المشاريع النموذجية، إذ يوضِّح كيف أن أقل من 20% من شيفرة JS المكتوبة خاصةً بإطار العمل Ember. بدء استخدام Ember سننشئ أولًا نسخةً من نموذج تطبيق TodoMVC التقليدي لتعلُّم كيفية استخدام أساسيات إطار عمل Ember، إذ يُعَدّ تطبيق TodoMVC تطبيقًا أساسيًا لتتبّع المهام، ويُستخدَم في العديد من التقنيات المختلفة، وإليك نسخةً مكتملةً منه ليكون مرجع لك. إنشاء تطبيق جديد في Ember يحتوي مشروع TodoMVC على بعض المشاكل من حيث الالتزام بممارسات الويب التي يمكن الوصول إليها افتراضيًا، وهناك نوعان من مشاكل جيت هاب GitHub المفتوحة حول هذا الموضوع في مجموعة مشاريع TodoMVC هي: إضافة وصول مستخدِمي لوحة المفاتيح إلى العروض التوضيحية. إعادة تفعيل المخطط حول العناصر القابلة للتركيز. يهتم إطار Ember بموضوع إمكانية الوصول accessibility كثيرًا وأن تكون التطبيقات المبنية فيه سهلة الوصول لكامل المستخدمين حتى بمن فيهم أي إعاقة وقد وفر دليلًا شاملًا عنه، ولكن بما أنّ هذا المقال يركِّز على جانب جافاسكربت في إنشاء تطبيق ويب صغير، فإنّ قيمة TodoMVC تأتي من توفير ملفات CSS المبنيّة مسبقًا وبنية HTML الموصَى بها، مما يلغي الاختلافات بين التطبيقات، كما يسمح بإجراء موازنة أسهل بينها، وسنركِّز لاحقًا على إضافة شيفرة إلى تطبيقنا لإصلاح بعض أكبر أخطاء تطبيق TodoMVC. تثبيت أدوات Ember يستخدِم Ember واجهة سطر الأوامر CLI لبناء أجزاء من تطبيقك وإنشاء شيفرتها المساعدة Scaffolding. أولًا، ستحتاج إلى تثبيت أداة node ومدير الحزم npm قبل تمكّنك من تثبيت أداة ember-cli، كما يمكنك الانتقال إلى مقال دليل استخدام سطر الأوامر في عملية تطوير الويب من طرف العميل لمعرفة كيفية تثبيت node و npm إذا لم تكن مثبّتةً لديك مسبقًا. ثانيًا، اكتب الأمر التالي في طرفيتك لتثبيت أداة ember-cli: npm install -g ember-cli توفِّر هذه الأداة برنامج ember في طرفيتك، وتُستخدَم لإنشاء وبناء وتطوير واختبار تطبيقك وإنشاء شيفرته المساعِدة، كما يمكنك تشغيل الأمر ember --help للحصول على قائمة كاملة بالأوامر وخياراتها. ثالثًا، يمكنك إنشاء تطبيق جديد تمامًا من خلال كتابة الأمر التالي في طرفيتك، مما يؤدي إلى إنشاء مجلد جديد له الاسم todomvc ضمن المجلد الذي أنت فيه حاليًا، بحيث يحتوي على أدوات لبناء تطبيق Ember جديد، وتأكد من الانتقال إلى مكان مناسب في الطرفية مثل مجلد "سطح المكتب" أو مجلد "المستندات" لتعثر على مجلد تطبيقك بسهولة قبل تشغيل الأمر التالي: ember new todomvc أو شغّل الأمر التالي على نظام ويندوز: npx ember-cli new todomvc يؤدي هذا الأمر إلى إنشاء بيئة تطوير تطبيقات جاهزة للإنتاج تتضمن افتراضيًا الميزات التالية: خادم التطوير مع إعادة التحميل المباشر. معمارية الإضافات التي تسمح لحزم الطرف الثالث بتحسين تطبيقك. أحدث إصدار من جافاسكربت متكامل مع Babel و Webpack. بيئة اختبار آلية تدير اختباراتك في المتصفح، مما يتيح لك الاختبار مثل المستخدِم العادي. عملية التحويل Transpilation والتصغير Minification لكل من شيفرات CSS وجافاسكربت الخاصة بعمليات البناء للإنتاج. اتباع العرف السائد في كتابة الشيفرة مما يقلل الاختلافات بين التطبيقات ويسمح بتبديل السياق بسهولة. الاستعداد لبناء مشروع Ember ستحتاج إلى محرِّر شيفرات قبل الاستمرار بمشروعك الجديد، فإذا لم يكن لديك محرّر مُعَدّ مسبقًا، فإنّ Ember Atlas لديه بعض الإرشادات حول كيفية إعداد المحرّرات المختلفة. تثبيت الأصول المشتركة لمشاريع TodoMVC لا يُعَدّ تثبيت الأصول أو ملفات المشروع المشتركة خطوةً مطلوبةً للمشاريع الجديدة، لكنه يسمح باستخدام ملفات CSS المشتركة الحالية حتى لا نخمّن ما هو ملف CSS المطلوب لإنشاء أنماط أو تنسيقات مشروع TodoMVC. أولًا، انتقل أولًا إلى المجلد todomvc في الطرفية باستخدام الأمر cd todomvc في نظامَي macOS أو لينكس Linux مثلًا. ثانيًا، شغّل الأمر التالي لوضع ملف CSS المشترك الخاص بمشروع todomvc ضمن تطبيقك: npm install --save-dev todomvc-app-css todomvc-common ثالثًا، ابحث بعد ذلك عن الملف ember-cli-build.js في المجلد todomvc الموجود في المجلد الجذر، وافتحه في محرر الشيفرة الذي اخترته، إذ يُعَدّ الملف ember-cli-build.js مسؤولًا عن إعداد التفاصيل حول كيفية بناء مشروعك بما في ذلك تجميع كل ملفاتك مع بعضها البعض وتصغير الأصول وإنشاء خرائط الشيفرة البرمجية، لذلك ليس هناك داع للقلق بشأن هذا الملف، كما سنضيف سطورًا إلى الملف ember-cli-build.js لاستيراد ملفات CSS المشتركة، بحيث تصبح جزءًا من عملية البناء دون الحاجة إلى استيرادها @import صراحةً في الملف app.css، إذ سيتطلب ذلك إعادة كتابة عنوان URL في وقت البناء وبالتالي سيكون أقل كفاءة وأكثر تعقيدًا في الإعداد. رابعًا، ابحث عن الشيفرة التالية في الملف ember-cli-build.js: let app = new EmberApp(defaults, { // أضف خيارات هنا }); خامسًا، أضف الأسطر التالية بعد ذلك قبل حفظ الملف: app.import('node_modules/todomvc-common/base.css'); app.import('node_modules/todomvc-app-css/index.css'); سادسًا، أخيرًا، ابحث عن الملف app.css الموجود في المسار app/styles/app.css، والصق ما يلي فيه: :focus, .view label:focus, .todo-list li .toggle:focus + label, .toggle-all:focus + label { outline: #d86f95 solid !important; } يعدِّل ملف CSS بعض الأنماط التي توفرها حزمة npm والتي هي todomvc-app-css، مما يسمح بظهور تركيز لوحة المفاتيح ويؤدي إلى حد ما إلى إصلاح أحد عيوب الشمولية الرئيسية لتطبيق TodoMVC الافتراضي. بدء تشغيل خادم التطوير يمكنك بدء تشغيل التطبيق في وضع التطوير عن طريق كتابة الأمر التالي في الطرفية أثناء وجودك ضمن المجلد todomvc: ember server ويجب أن يظهر لديك خرج مشابه لما يلي: Build successful (190ms) – Serving on http://localhost:4200/ Slowest Nodes (totalTime >= 5%) | Total (avg) -----------------------------------------+----------- BroccoliMergeTrees (17) | 35ms (2 ms) Package /assets/vendor.js (1) | 13ms Concat: Vendor Styles/assets/vend... (1) | 12ms يُشغَّل خادم التطوير على المضيف المحلي http://localhost:4200، والذي يمكنك زيارته في متصفحك للتحقق من عملك حتى الآن. إذا كان كل شيء على ما يرام، فسترى صفحةً تشبه الصفحة التالية: الخلاصة وصلنا إلى النقطة التي يمكننا فيها البدء في بناء نموذجنا لتطبيق TodoMVC في إطار عمل Ember، وسنتعرّف في المقال التالي على بناء بنية شيفرة توصيف تطبيقنا بوصفها مجموعة من المكونات المنطقية. ترجمة -وبتصرّف- للمقال Getting started with Ember. اقرأ أيضًا مقارنة بين أطر الواجهات الأمامية: Angular و React و Vue
-
سنزوّدك من خلال هذا المقال بقائمة من مصادر React التي يمكنك استخدامها للمضي قدمًا في مسار تعلّمك تطوير الواجهات الأمامية وبناء تطبيقات الويب. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية. الهدف: توفير مصادر إضافية لمعرفة المزيد عن مكتبة React. التنسيقات على مستوى المكون تُعرِّف العديد من تطبيقات React تنسيقاتها على أساس كل مكوِّن بدلًا من أن تكون في صفحة تنسيقات واحدة متجانسة، إذ تتيح الأداة create-react-app استيراد ملفات CSS إلى وحدات جافاسكربت، بحيث تُرسَل شيفرة CSS إلى المستخدِم فقط عند تصيير Render المكوِّن المقابل، وقد كان بإمكاننا في تطبيقنا مثلًا كتابة ملف Form.css مخصّص ليحتوي على تنسيقات تلك المكونات، ثم استيراد التنسيقات إلى وحداتها كما يلي: import Form from './Form'; import './Form.css' تسهِّل هذه الطريقة تحديد وإدارة شيفرة CSS المُخصَّصة لمكوِّن معيَّن، ولكنها تؤدي إلى تجزئة ملف التنسيقات عبر قاعدة شيفرتك البرمجية، كما يمكن ألّا تكون هذه التجزئة مفيدةً، إذ يُعَدّ الحدّ من مقدار الشيفرة التي ليست ذات فائدة والمُرسَلة إلى المستخدِم، أمرًا منطقيًا بالنسبة للتطبيقات الأكبر حجمًا، والتي تحتوي على مئات من العروض الفريدة والكثير من الأجزاء المتحركة، إذ يُحتمَل أن تكون لديك تنسيقات على مستوى التطبيق وتنسيقات مكونات محدَّدة مبنية فوقها. أدوات تطوير React استخدمنا التابع console.log() للتحقق من حالة تطبيقنا وخاصياته Props، إذ سترى بعض التحذيرات المفيدة ورسائل الخطأ التي تعطيها React في واجهة سطر الأوامر CLI وطرفية جافاسكربت في المتصفح. ولكن هناك المزيد الذي نستطيع إجراؤه هنا. تتيح الأداة المساعدة React DevTools فحص الأجزاء الداخلية لتطبيق React مباشرةً في المتصفح، إذ تضيف لوحةً جديدةً إلى أدوات مطور متصفحك، كما يمكنك بواسطتها فحص حالة المكوّنات المختلفة وخاصياتها، وتعديل الحالة والخاصيات لإجراء تغييرات فورية على تطبيقك، إذ تُظهر لقطة الشاشة التالية تطبيقنا النهائي كما يظهر في الأداة React DevTools: نرى على اليسار جميع المكونات التي يتألف منها تطبيقنا بما في ذلك بعض المفاتيح الفريدة للأشياء المُصيَّرة من المصفوفات، في حين نرى على اليمين الخاصيات والخطّافات Hooks التي يستخدِمها المكوِّن App، ولاحظ أنّ للمكوّنات Form وFilterButton وTodo مسافةً بادئةً إلى اليمين، وهذا يشير إلى أنّ المكوِّن App هو المكوِّن الأب لها، لذا يُعَدّ هذا العرض رائعًا في التطبيقات الأكثر تعقيدًا لفهم العلاقات بين الأبناء والآباء بسهولة، كما تتوفر الأداة React DevTools في عدد من الأشكال مثل: امتداد متصفح كروم Chrome. امتداد متصفح فايرفوكس Firefox. امتداد متصفح Chromium Edge (سيُتاح قريبًا). تطبيق مستقل يمكنك تثبيته باستخدام NPM أو Yarn. جرّب تثبيت إحدى هذه الأدوات، ثم استخدمها لفحص التطبيق الذي أنشأته للتو. واجهة برمجة تطبيقات السياق Context API استخدَم التطبيق الذي أنشأناه خاصيات المكوِّنات لتمرير البيانات من المكوِّن App إلى المكوِّنات الأبناء التي تحتاجها، إذ تُعَدّ الخاصيات طريقةً مناسبةً لمشاركة البيانات في معظم الأحيان، ولكنها ليست الأفضل دائمًا بالنسبة للتطبيقات المعقَّدة والمتداخلة كثيرًا. توفِّر React واجهة برمجة تطبيقات السياق Context API بوصفها طريقةً لتوفير البيانات للمكوّنات التي تحتاجها دون تمرير الخاصيات إلى أسفل شجرة المكونات، كما يوجد الخطّاف useContext الذي يسهِّل ذلك. أصناف المكونات يمكن بناء مكونات React باستخدام أصناف ES6 التي تُسمَّى بأصناف المكونات Class Components، إذ كانت أصناف ES6 قبل ظهور الخطّافات الطريقة الوحيدة لجلب الحالة إلى المكوّنات أو إدارة آثار التصيير الجانبية، ولا تزال الطريقة الوحيدة للتعامل مع بعض الميزات الأخرى في الحالات الطارئة، وهي شائعة جدًا في مشاريع React القديمة، كما يمكنك الاطلاع على المصادر التالية: حالة ودورة حياة المكونات في توثيق React. توثيق React على موسوعة حسوب. تعلّم لغة جافاسكربت من خلال توثيقها على موسوعة حسوب. الاختبار توفِّر create-react-app بعض الأدوات لاختبار تطبيقك، كما يغطي توثيق create-react-app بعض أساسيات الاختبار. التوجيه يُعالَج التوجيه تقليديًا باستخدام خادم وليس باستخدام تطبيق على حاسوب المستخدِم، ولكن يمكن ضبط تطبيق ويب لقراءة موقع المتصفح وتحديثه، وتصيير واجهات مستخدِم معيّنة، وهذا ما يسمى بالتوجيه من طرف العميل Client-side Routing، كما يمكن إنشاء العديد من المسارات الفريدة لتطبيقك مثل /home أو /dashboard أو login/. أنتج مجتمع React مكتبتين رئيسيتين للتوجيه من طرف العميل هما React Router وReach Router. تُعَدّ مكتبة React Router مناسبةً للتطبيقات ذات احتياجات التوجيه المعقَّدة، كما أنها تلبي بعض الحالات الطارئة بطريقة أفضل من مكتبة Reach Router، ولكن تُعَدّ React Router مكتبةً أكبر. تُعَدّ مكتبة Reach Router مناسبةً للتطبيقات الأبسط، وتدير التركيز تلقائيًا أثناء تنقل المستخدِم من صفحة إلى أخرى. تُعَدّ إدارة التركيز أمرًا ضروريًا في التوجيه من طرف العميل، إذ يمكن وقوع مستخدِمي لوحة المفاتيح في مأزق التركيز، ويمكن ألّا يكون لدى مستخدِمي قارئ الشاشة أيّ فكرة عن انتقالهم إلى صفحة جديدة، في حين تُعَدّ مكتبة Reach Router مكانًا جيدًا للبدء، لأنها أفضل من حيث إمكانية الوصول، لكن سيُدمَج هذان المشروعان في المستقبل القريب، وستكون حينها مكتبة React Router هي المشروع الباقي مع إضافة ميزات إدارة التركيز الخاصة بمكتبة Reach. أخيرًا، لا تنسى الرجوع إلى توثيق React على موسوعة حسوب فضع هذا المرجع في جعبتك (بالإضافة إلى المراجع الأخرى التي توفرها الموسوعة) والذي ستحتاج إلى الرجوع إليه بين الحين والآخر في رحلة سيرك مع مكتبة React لبناء واجهات المواقع والتطبيقات. ترجمة -وبتصرُّف- للمقال React resources. اقرأ أيضًا مكونات React الأساسية (React Components) المصطلحات المستخدمة في React تعلم البرمجة
-
سنركِّز في هذا المقال على الشمولية Accessibility (أو تترجم إلى سهولة وصول أيضًا) بما في ذلك إدارة التركيز Focus Management في React التي يمكنها تحسين قابلية الاستخدام وتقليل الارتباك لكل من مستخدمي لوحة المفاتيح فقط وقارئات الشاشة. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية. الهدف: تعلّم كيفية تنفيذ إمكانية وصول مستخدِمي لوحة المفاتيح في React. مستخدمو لوحة المفاتيح أنجزنا حتى الآن جميع الميزات التي أردنا تنفيذها، إذ أصبح بإمكان المستخدِم إضافة مهمة جديدة، وتحديد المهام، وإلغاء تحديدها، وحذف المهام، وتعديل أسماء المهام، وترشيح قائمة المهام جميعها أو المهام النشطة أو المهام المكتملة، إذ يمكن للمستخدِمين تنفيذ جميع هذه المهام باستخدام الفأرة، ولكن لا يمكن لمستخدِمي لوحة المفاتيح فقط الوصول إلى هذه الميزات بسهولة. استكشاف مشكلة قابلية استخدام لوحة المفاتيح انقر على حقل الإدخال الموجود أعلى التطبيق كما لو أنك تريد إضافة مهمة جديدة، إذ سترى خطًا سميكًا يمثل حدود العنصر outline حول حقل الإدخال، إذ تُعَدّ هذه الحدود المؤشر المرئي على تركيز المتصفح على هذا العنصر حاليًا، لذا اضغط على مفتاح Tab من لوحة المفاتيح، وسترى ظهور المخطط حول زر الإضافة Add تحت حقل الإدخال، إذ يدل ذلك على انتقال تركيز المتصفح. اضغط على مفتاح Tab عدة مرات، وسترى مؤشر التركيز المتقطع ينتقل بين أزرار الترشيح، واستمر في الضغط على مفتاح Tab إلى أن يصبح مؤشر التركيز حول زر التعديل Edit الأول، ثم اضغط على مفتاح Enter، إذ سيبدّل المكوِّن <Todo /> بين القوالب كما صمّمنا، وسترى نموذجًا يتيح تعديل اسم المهمة. ولكن قد تتساءل عن مكان وجود مؤشر التركيز حاليًا، إذ تُزال تمامًا العناصر التي كانت موجودةً سابقًا لاستبدال شيء آخر بها عندما نبدِّل بين القوالب في المكوِّن <Todo />، وهذا يعني اختفاء العنصر الذي ركّزنا عليه سابقًا، ولا يوجد شيء نركّز عليه حاليًا، فقد يؤدي ذلك إلى إرباك مجموعة كبيرة من المستخدِمين، وخاصةً المستخدِمين الذين يعتمدون على لوحة المفاتيح أو الذين يستخدِمون قارئ الشاشة، كما يمكن تحسين تجربة مستخدِمي لوحة المفاتيح وقارئ الشاشة من خلال إدارة تركيز المتصفح بأنفسنا. التركيز بين القوالب إذا بدَّل المستخدِم قالب <Todo/> من قالب العرض إلى قالب التعديل، فيجب علينا التركيز على العنصر <input> المستخدَم لإعادة تسميته، ويجب علينا إعادة التركيز مرةً أخرى إلى زر التعديل Edit عند التبديل مرةً أخرى من قالب التعديل إلى قالب العرض. استهداف العناصر يجب علينا إخبار React بالعنصر الذي نريد التركيز عليه في نموذج DOM وكيفية العثور عليه، ويساعدنا في ذلك الخطّاف useRef في React الذي ينشئ كائنًا له الخاصية current، إذ يمكن أن تكون هذه الخاصية مرجعًا لأيّ شيء نريده، ثم يمكننا البحث عن هذا المرجع لاحقًا، وهذا مفيد للإشارة إلى عناصر DOM، لذا عدِّل تعليمة الاستيراد import في الجزء العلوي من الملف Todo.js لتتضمن الخطّاف useRef كما يلي: import React, { useRef, useState } from "react"; أنشئ بعد ذلك ثابتَين جديدين بعد الخطّافات في الدالة Todo()، إذ يجب أن يكون أحد هذين الثابتين مرجعًا لزر التعديل Edit في قالب العرض والآخر لحقل التعديل في قالب التعديل. const editFieldRef = useRef(null); const editButtonRef = useRef(null); تملك هذه المراجع قيمةً افتراضيةً هي null لأنها ستكون بلا قيمة حين ربطها بالعناصر الخاصة بها من خلال إضافة الخاصية ref لكل عنصر، وضبط قيمها على كائنات ref المسماة بأسماء مناسبة، ويجب تعديل مربع النص <input> في قالب التعديل كما يلي: <input id={props.id} className="todo-text" type="text" value={newName} onChange={handleChange} ref={editFieldRef} /> يجب أن يكون زر التعديل Edit في قالب العرض كما يلي: <button type="button" className="btn" onClick={() => setEditing(true)} ref={editButtonRef} > Edit <span className="visually-hidden">{props.name}</span> </button> التركيز على عناصر ref باستخدام الخطاف useEffect يمكننا استخدام عناصر ref للغرض المقصود منها عن طريق استيراد خطّاف React آخر هو useEffect() الذي سُمِّي بهذا الاسم لأنه يعمل بعد أن تصيِّر React مكونًا معينًا، وسيشغّل هذا الخطّاف أيّ أمور إضافية نريد إضافتها إلى عملية التصيير، والتي لا يمكننا تشغيلها ضمن جسم الدالة الرئيسية، إذ يُعَدّ الخطّاف useEffect() مفيدًا في الوضع الحالي لأننا لا نستطيع التركيز على عنصر إلا بعد تصيير المكوِّن <Todo /> ومعرفة React مكان عناصر ref، لذا عدِّل تعليمة الاستيراد import في الملف Todo.js مرةً أخرى لإضافة الخطّاف useEffect كما يلي: import React, { useEffect, useRef, useState } from "react"; يأخذ الخطّاف useEffect() دالةً على أساس وسيط له، إذ تُنفَّذ هذه الدالة بعد تصيير المكوِّن، لذا ضع استدعاء الخطّاف useEffect() التالي قبل تعليمة return مباشرةً في جسم الدالة Todo()، ومرّر إليه دالةً تسجّل العبارة "side effect" في الطرفية: useEffect(() => { console.log("side effect"); }); أضف تابع console.log() آخر، لتوضيح الفرق بين عملية التصيير الرئيسية وتشغيل الشيفرة ضمن الخطّاف useEffect()، أي ضع التعليمة التالية بعد الإضافة السابقة: console.log("main render"); افتح الآن التطبيق في متصفحك، إذ يجب أن ترى كلتا الرسالتين في الطرفية مع تكرار كل منهما ثلاث مرات، ولاحظ ظهور عبارة التصيير الرئيسي main render أولًا، ثم عبارة "side effect" ثانيًا، على الرغم من وجود عبارة "side effect" أولًا في الشيفرة. main render (3) Todo.js:100 side effect (3) Todo.js:98 احذف تعليمة console.log("main render") الآن، ولننتقل إلى تطبيق إدارة التركيز. التركيز على حقل التعديل تذكّر أننا نريد التركيز على حقل التعديل عندما ننتقل إلى قالب التعديل، لذا عدِّل الخطّاف useEffect() ليصبح كما يلي: useEffect(() => { if (isEditing) { editFieldRef.current.focus(); } }, [isEditing]); إذا كانت قيمة isEditing هي true، فستقرأ React قيمة مرجع editFieldRef الحالية وتنقل تركيز المتصفح إليه، كما سنمرِّر مصفوفةً إلى الخطّاف useEffect() على أساس وسيط ثان، إذ تُعَدّ هذه المصفوفة قائمةً من القيم التي يجب أن يعتمد عليها الخطّاف useEffect()، فإذا ضمّنا هذه القيم، فلن يُشغَّل الخطاف useEffect() إلا عند تغيير إحدى هذه القيم، إذ نريد تغيير التركيز عندما تتغير قيمة isEditing فقط، لذا انتقل إلى متصفحك الآن، وسترى انتقال التركيز إلى العنصر <input> المقابل لزر التعديل Edit عند النقر عليه. نقل التركيز إلى زر التعديل مرة أخرى يمكن أن يبدو نقل React للتركيز مرةً أخرى إلى زر التعديل Edit عند حفظ التعديل أو إلغائه أمرًا سهلًا للوهلة الأولى، فلنجرّب إضافة شرط إلى الخطّاف useEffect للتركيز على زر التعديل إذا كانت قيمة الحالة isEditing هي false مثلًا، لذا عدِّل استدعاء الخطّاف useEffect() كما يلي: useEffect(() => { if (isEditing) { editFieldRef.current.focus(); } else { editButtonRef.current.focus(); } }, [isEditing]); عُد إلى متصفحك وسترى انتقال تركيزك بين تعديل العنصر <input> وزر التعديل Edit مع بدء التعديل وإنهائه، ولكن يركِّز الزر Edit في المكوِّن <Todo /> الأخير مباشرةً على تحميل الصفحة قبل تفاعلنا مع التطبيق، وهنا يتصرّف الخطّاف useEffect() كما صممناه تمامًا، فهو يعمل بمجرد تصيير المكوِّن، ويرى أن قيمة الحالة isEditing هي false، كما يركِّز على الزر Edit، وبما أنه يوجد ثلاث نسخ من المكوِّن <Todo />، فإننا نرى التركيز على الزر Edit الأخير، إذ يجب إعادة التفكير ليتغير التركيز عندما تتغير قيمة الحالة isEditing فقط. إدارة التركيز الصارمة يجب معرفة قيمة الحالة isEditing ومتى تتغير هذه القيمة، لذا يجب أن نكون قادرين على قراءة قيمة isEditing السابقة، ويجب أن يكون منطق العمل كما يلي باستخدام الشيفرة الوهمية التالية: if (wasNotEditingBefore && isEditingNow) { focusOnEditField() } if (wasEditingBefore && isNotEditingNow) { focusOnEditButton() } ناقش فريق React طرقًا للحصول على حالة المكوِّن السابقة، وقدّم مثالًا عن الخطّاف المُخصَّص الذي يمكننا استخدامه لذلك. الصق الشيفرة التالية بالقرب من أعلى الملف Todo.js قبل الدالة Todo(): function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; } سنعرِّف الآن الثابت wasEditing بعد الخطّافات في الجزء العلوي من الدالة Todo()، إذ نريد أن يتتبع هذا الثابت قيمة isEditing السابقة، لذلك سنستدعي الدالة usePrevious مع isEditing على أساس وسيط كما يلي: const wasEditing = usePrevious(isEditing); يمكننا باستخدام هذا الثابت تحديث الخطّاف useEffect() لتطبيق الشفرة الوهمية التي ناقشناها سابقًاـ لذا عدِّل هذه الشيفرة لتصبح كما يلي: useEffect(() => { if (!wasEditing && isEditing) { editFieldRef.current.focus(); } if (wasEditing && !isEditing) { editButtonRef.current.focus(); } }, [wasEditing, isEditing]); لاحظ اعتماد منطق useEffect() الآن على الثابت wasEditing، لذلك سنقدِّمه ضمن مصفوفة الاعتماديات، وحاول مرةً أخرى استخدام زرَي التعديل Edit والإلغاء Cancel للتبديل بين قوالب المكوِّن <Todo />، إذ سترى مؤشر تركيز المتصفح يتحرك بطريقة مناسبة دون ظهور المشكلة التي ناقشناها سابقًا. التركيز عندما يحذف المستخدم مهمة هناك مشكلة واحدة أخيرة في تجربة لوحة المفاتيح، وهي اختفاء التركيز عندما يحذف المستخدِم مهمةً من القائمة، لذا سنتبع نمطًا مشابهًا للتعديلات السابقة، إذ سننشئ مرجعًا جديدًا، وسنستخدِم الخطّاف usePrevious()، لنتمكّن من التركيز على عنوان القائمة عندما يحذف المستخدِم مهمةً. يكون المكان الذي نرغب في إرسال تركيزنا إليه واضحًا في بعض الأحيان، إذ كانت لدينا نقطة أصل للرجوع إليها وهي زر التعديل Edit عند التبديل بين قوالب <Todo />، لكن لا يوجد مكان للرجوع إليه في حالتنا، لأننا نزيل العناصر تمامًا من نموذج DOM، فعنوان القائمة هو أفضل خيار لأنه قريب من عنصر القائمة الذي سيحذفه المستخدِم، كما سيخبر التركيز عليه المستخدِم بعدد المهام المتبقية. إنشاء المرجع استورد الخطّافَين useRef() وuseEffect() إلى الملف App.js كما يلي: import React, { useState, useRef, useEffect } from "react"; صرّح بعد ذلك عن مرجع جديد ضمن الدالة App() قبل تعليمة return مباشرةً كما يلي: const listHeadingRef = useRef(null); تحضير العنوان لا تكون عناصر العنوان مثل <h2> قابلةً للتركيز عادةً، ولكن لا يُعَدّ ذلك مشكلة، إذ يمكننا جعل أيّ عنصر قابلًا للتركيز برمجيًا عن طريق إضافة السمة tabindex="-1" إليه، أي يمكن التركيز عليه باستخدام لغة جافاسكربت فقط، كما لا يمكنك الضغط على مفتاح Tab للتركيز على عنصر له السمة tabindex="-1" باستخدام الطريقة نفسها التي يمكنك اتباعها مع عناصر <button> أو <a>، إذ يمكن تطبيق ذلك باستخدام السمة tabindex="0"، ولكنه لا يُعَدّ مناسبًا في هذه الحالة. لنضِف السمة tabindex المكتوبة بالصورة tabIndex في صيغة JSX إلى العنوان الموجود أعلى قائمة المهام مع المرجع headingRef كما يلي: <h2 id="list-heading" tabIndex="-1" ref={listHeadingRef}> {headingText} </h2> ملاحظة: تُعَدّ السمة tabindex رائعةً لحالات إمكانية الوصول الطارئة، ولكن يجب عليك الحرص على عدم الإفراط في استخدامها، إذ يمكنك تطبيقها على عنصر فقط عندما تكون متأكدًا تمامًا من أنّ جعله قابلًا للتركيز سيفيد المستخدِم بطريقة ما، كما يجب عليك استخدام العناصر التي يمكن التركيز عليها طبيعيًا مثل الأزرار وعناصر الروابط وحقول الإدخال، في حين يمكن أن يكون لاستخدام السمة tabindex غير المسؤول تأثيرًا سلبيًا عميقًا على مستخدِمي لوحة المفاتيح وقارئات الشاشة. الحصول على الحالة السابقة نريد التركيز على العنصر المرتبط بالمرجع باستخدام السمة ref عندما يحذف المستخدِم مهمةً من القائمة فقط، إذ سيتطلب ذلك الخطّاف usePrevious() الذي استخدمناه سابقًا، لذا أضف هذا الخطّاف في أعلى الملف App.js بعد عبارات الاستيراد مباشرةً كما يلي: function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; } أضف الآن ما يلي قبل تعليمة return ضمن الدالة App(): const prevTaskLength = usePrevious(tasks.length); استدعينا الخطّاف usePrevious() لتتبع طول حالة المهام. ملاحظة: بما أننا نستخدِم الآن الخطّاف usePrevious() في ملفين، فستتمثّل إعادة البناء الفعالة في نقل الدالة usePrevious() إلى ملفها، وتصديرها من هذا الملف، واستيرادها حيث تريدها. استخدام الخطاف useEffect() للتحكم في التركيز الرئيسي يمكننا الآن إعداد الخطّاف useEffect() لتشغيله عندما يتغير عدد المهام، والذي سيركِّز على العنوان إذا كان عدد المهام أقل مما كان عليه سابقًا، أي أننا حذفنا مهمةً. أضف ما يلي إلى جسم الدالة App() بعد الإضافات السابقة مباشرةً: useEffect(() => { if (tasks.length - prevTaskLength === -1) { listHeadingRef.current.focus(); } }, [tasks.length, prevTaskLength]); نحاول التركيز فقط على عنوان القائمة إذا كانت لدينا مهام أقل مما كانت عليه سابقًا، إذ تضمن الاعتماديات Dependencies االمُمرَّرة في هذا الخطّاف محاولة إعادة التشغيل فقط عندما تتغير أيّ من هذه القيم، أي عدد المهام الحالية أو عدد المهام السابقة، وسترى الآن ظهور مخطط التركيز المنقط حول العنوان أعلى القائمة عندما تحذف مهمةً في متصفحك. الخلاصة انتهينا من إنشاء تطبيق React من الألف إلى الياء، وستكون المهارات التي تعلمتها أساسًا للبناء عليها لتواصل العمل مع React، كما يمكنك أن تكون مساهمًا فعالًا في مشروع React حتى إذا كان كل ما تفعله هو التفكير مليًا في المكونات وحالتها وخاصياتها، وتذكّر أن تكتب دائمًا أفضل شيفرة HTML ممكنة. يُعَدّ الخطّافان useRef() وuseEffect() ميزات متقدمةً إلى حد ما، ويجب أن تفخر بنفسك لاستخدامها، وابحث عن فرص لممارستها أكثر، مما سيسمح لك بإنشاء تجارب شاملة للمستخدِمين، إذ يمكن تعذُّر الوصول إلى تطبيقنا لمستخدِمي لوحة المفاتيح بدونهما. ملاحظة: إذا كنت بحاجة إلى التحقق من شيفرتك مقابل نسختنا من الشيفرة، فيمكنك العثور على نسخة نهائية من نموذج شيفرة تطبيق React في المستودع todo-reaction. كما يمكنك الحصول على إصدار مباشر قيد التشغيل من خلال مراجعة todo-react-build. سنقدِّم في المقال التالي قائمةً بموارد React التي يمكنك استخدامها للمضي قدمًا في مسار تعلّمك. ترجمة -وبتصرُّف- للمقال Accessibility in React. اقرأ أيضًا تنفيذ التفاعل في تطبيق React: التعديل والترشيح والتصيير الشرطي إنشاء تطبيق قائمة مهام باستخدام React تقسيم تطبيق React إلى مكونات أساسيات بناء تطبيقات الويب
-
سنضيف في هذا المقال اللمسات الأخيرة على وظائف تطبيق قائمة المهام Todo List الرئيسية -الذي بنيناه في المقالات السابقة- من خلال السماح بتعديل المهام الحالية، وترشيح Filtering قائمة المهام جميعها والمهام المكتملة وغير المكتملة فقط، كما سنتعرّف على التصيير الشرطي Conditional Rendering لواجهة المستخدِم UI. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية. الهدف: التعرف على التصيير الشرطي في React وتطبيق ترشيح القائمة وتعديل واجهة مستخدِم في تطبيقنا. تعديل اسم المهمة لا توجد لدينا واجهة مستخدِم لتعديل اسم المهمة حتى الآن، ولكن يمكننا على الأقل تنفيذ الدالة editTask() في الملف App.js حاليًا، والتي تشبه الدالة deleteTask() لأنها ستأخذ معرِّفًا id للعثور على الكائن الهدف، وستأخذ الخاصية newName التي تحتوي على الاسم الذي سنعدّل اسم المهمة إليه، في حين أننا سنستخدِم التابع Array.prototype.map() بدلًا من التابع Array.prototype.filter() لأننا نريد إعادة مصفوفة جديدة مع بعض التعديلات بدلًا من حذف شيء منها، لذا أضف الدالة editTask() ضمن المكوِّن App مع الدوال الأخرى كما يلي: function editTask(id, newName) { const editedTaskList = tasks.map(task => { // إذا كانت هذه المهمة لها معرّف المهمة المعدّلة نفسه if (id === task.id) { // return {...task, name: newName} } return task; }); setTasks(editedTaskList); } مرِّر editTask إلى مكونات <Todo /> بوصفها خاصيةً Prop بالطريقة نفسها التي مررنا بها الخاصية deleteTask كما يلي: const taskList = tasks.map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} toggleTaskCompleted={toggleTaskCompleted} deleteTask={deleteTask} editTask={editTask} /> )); افتح الآن الملف Todo.js، إذ سنُجري إعادة بناء. واجهة مستخدم التعديل يجب توفير واجهة مستخدِم للمستخدِمين للسماح لهم بتعديل مهمة، لذا استورد أولًا الخطّاف useState إلى المكوِّن Todo كما فعلنا سابقًا مع المكوِّن App عن طريق تعديل تعليمة الاستيراد الأولى إلى ما يلي: import React, { useState } from "react"; سنضبط الآن الحالة isEditing التي يجب أن تكون قيمتها الافتراضية false، لذا أضف السطر التالي في الجزء العلوي من تعريف المكون Todo(props) { … }: const [isEditing, setEditing] = useState(false); كما يجب إعادة التفكير في المكوِّن <Todo /> من الآن فصاعدًا، إذ نريد منه عرض أحد القالبَين التاليين بدلًا من القالب الوحيد المستخدَم حتى الآن: قالب العرض View عند عرض المهام فقط، وهو ما استخدمناه حتى الآن. قالب التعديل Editing عند تعديل المهام، وسننشئه بعد قليل. انسخ الشيفرة التالية في الدالة Todo() بعد الخطّاف useState() وقبل التعليمة return: const editingTemplate = ( <form className="stack-small"> <div className="form-group"> <label className="todo-label" htmlFor={props.id}> New name for {props.name} </label> <input id={props.id} className="todo-text" type="text" /> </div> <div className="btn-group"> <button type="button" className="btn todo-cancel"> Cancel <span className="visually-hidden">renaming {props.name}</span> </button> <button type="submit" className="btn btn__primary todo-edit"> Save <span className="visually-hidden">new name for {props.name}</span> </button> </div> </form> ); const viewTemplate = ( <div className="stack-small"> <div className="c-cb"> <input id={props.id} type="checkbox" defaultChecked={props.completed} onChange={() => props.toggleTaskCompleted(props.id)} /> <label className="todo-label" htmlFor={props.id}> {props.name} </label> </div> <div className="btn-group"> <button type="button" className="btn"> Edit <span className="visually-hidden">{props.name}</span> </button> <button type="button" className="btn btn__danger" onClick={() => props.deleteTask(props.id)} > Delete <span className="visually-hidden">{props.name}</span> </button> </div> </div> ); أصبح لدينا الآن بنيتا قوالب مختلفتين -"Edit" و"View"- معرّفتان ضمن ثابتين منفصلين، وهذا يعني أنّ التعليمة return الخاصة بالمكوِّن <Todo /> مكررة، فهي تحتوي على تعريف قالب العرض View أيضًا، ويمكن تنظيف هذا التكرار باستخدام التصيير الشرطي Conditional Rendering لتحديد القالب الذي يعيده المكوِّن، وبالتالي يُصيَّر في واجهة المستخدِم. التصيير الشرطي يمكننا في صيغة JSX استخدام شرط لتغيير ما يصيّره المتصفح، إذ يُكتَب الشرط في صيغة JSX باستخدام معامِل ثلاثي Ternary Operator، فالشرط في حالة المكوِّن <Todo /> هو "هل تُعدَّل هذه المهمة؟"، لذا عدِّل التعليمة return ضمن الدالة Todo()، بحيث تصبح كما يلي: return <li className="todo">{isEditing ? editingTemplate : viewTemplate}</li>; يجب أن يصيّر متصفحك جميع مهامك كما كانت سابقًا، ويمكن مشاهدة قالب التعديل من خلال تغيير القيمة الافتراضية للحالة isEditing من false إلى true في شيفرتك حاليًا، إذ سنجعل زر التعديل Edit يبدِّل هذه القيمة لاحقًا. التبديل بين قوالب سنجعل الآن ميزة التعديل تفاعليةً، إذ يجب أولًا استدعاء الدالة setEditing() مع القيمة true عندما يضغط المستخدِم على زر التعديل Edit في قالب العرض viewTemplate لنتمكّن من التبديل بين القوالب، لذا عدِّل زر التعديل Edit في قالب العرض viewTemplate كما يلي: <button type="button" className="btn" onClick={() => setEditing(true)}> Edit <span className="visually-hidden">{props.name}</span> </button> سنضيف الآن السمة onClick نفسها إلى زر الإلغاء Cancel في قالب التعديل editingTemplate، ولكن سنضبط الحالة isEditing هذه المرة على القيمة false لتعيدنا إلى قالب العرض، لذا عدِّل زر الإلغاء Cancel في قالب التعديل editingTemplate كما يلي: <button type="button" className="btn todo-cancel" onClick={() => setEditing(false)} > Cancel <span className="visually-hidden">renaming {props.name}</span> </button> يجب أن تكون قادرًا الآن على الضغط على زرَي التعديل Edit والإلغاء Cancel في عناصر المهام للتبديل بين القالبين. الخطوة التالية هي جعل وظيفة التعديل تعمل فعليًا. التعديل من واجهة المستخدم ما سنعمل عليه تاليًا مماثل لما عملنا عليه مع المكون Form (انظر المقال السابق تقسيم تطبيق React إلى مكونات). إذا كتب المستخدِم شيئًا في حقل الإدخال الجديد، فيجب تتبّع النص الذي يدخله، ويجب استخدام خاصية رد النداء Callback Prop بمجرد إرسال النموذج لتحديث الحالة باسم المهمة الجديد، إذ سنبدأ بإنشاء خطّاف Hook جديد لتخزين وضبط الاسم الجديد، لذا ضع ما يلي أسفل الخطّاف الموجود مسبقًا في الملف Todo.js: const [newName, setNewName] = useState(''); أنشئ بعد ذلك الدالة handleChange() التي تضبط الاسم الجديد، لذا ضع ما يلي بعد الخطافات وقبل القوالب: function handleChange(e) { setNewName(e.target.value); } سنعدِّل الآن الحقل <input /> الخاص بقالب التعديل editingTemplate بضبط السمة value على newName وربط الدالة handleChange() مع الحدث onChange كما يلي: <input id={props.id} className="todo-text" type="text" value={newName} onChange={handleChange} /> يجب أخيرًا إنشاء دالة تعالِج الحدث onSubmit الخاصة بنموذج التعديل، لذا أضف ما يلي مباشرةً بعد الدالة السابقة التي أضفتها: function handleSubmit(e) { e.preventDefault(); props.editTask(props.id, newName); setNewName(""); setEditing(false); } تذكّر أنّ خاصية رد النداء editTask() تحتاج إلى معرِّف المهمة التي نعدّلها بالإضافة إلى اسمها الجديد. اربط الدالة handleSubmit() مع حدث إرسال submit النموذج عبر إضافة معالج الحدث onSubmit التالي إلى عنصر النموذج <form> الخاص بقالب التعديل editingTemplate: <form className="stack-small" onSubmit={handleSubmit}> يجب الآن أن تكون قادرًا على تعديل مهمة في متصفحك. العودة إلى أزرار الترشيح اكتملت الآن ميزاتنا الرئيسية، وبالتالي يمكننا التفكير في أزرار الترشيح التي تكرّر العنوان "All" حاليًا بدون وظائف تطبّقها، إذ سنعيد تطبيق بعض المهارات التي استخدمناها في المكوِّن <Todo /> من أجل ما يلي: إنشاء خطّاف لتخزين المرشِّح Filter النشط. تصيير مصفوفة من عناصر <FilterButton /> التي تسمح للمستخدِمِين بتغيير المرشِّح النشط بين جميع المهام والمهام المكتملة والمهام غير المكتملة. إضافة خطاف ترشيح أضف خطّافًا جديدًا إلى الدالة App() التي تقرأ وتضبط المرشِّح، إذ نريد أن يكون المرشّح الافتراضي هو All لأنه يجب عرض جميع المهام في البداية. const [filter, setFilter] = useState('All'); تعريف المرشحات هدفنا الآن هو: يجب أن يكون لكل مرشِّح اسم فريد. يجب أن يكون لكل مرشِّح سلوك فريد. يُعَدّ كائن JavaScript طريقةً رائعةً لربط الأسماء بالسلوكيات، فالمفتاح هو اسم المرشِّح، والخاصية هي السلوك المرتبط بهذا الاسم. أضِف كائنًا بالاسم FILTER_MAP في الجزء العلوي من الملف App.js بعد تعليمات الاستيراد وقبل الدالة App() كما يلي: const FILTER_MAP = { All: () => true, Active: task => !task.completed, Completed: task => task.completed }; تُعَدّ قيم الكائن FILTER_MAP دوالًا سنستخدِمها لترشيح مصفوفة بيانات المهام tasks كما يلي: يعرِض المرشِّح All جميع المهام، لذلك يعيد القيمة true لجميع المهام. يعرِض المرشِّح Active المهام التي يكون فيها للخاصية completed القيمة false. يعرِض المرشِّح Completed المهام التي يكون فيها للخاصية completed القيمة true. أضف ما يلي بعد الذي أضفناه منذ قليل، إذ سنستخدِم التابع Object.keys() لتجميع مصفوفة FILTER_NAMES: const FILTER_NAMES = Object.keys(FILTER_MAP); ملاحظة: نعرِّف هذه الثوابت خارج الدالة App() لأنها إذا عُرِّفت ضمنها، فسيُعاد حسابها في كل مرة يعاد فيها تصيير المكوِّن <App />، ولا نريد حصول ذلك، فلن تتغير هذه المعلومات أبدًا بغض النظر عمّا يفعله تطبيقنا. تصيير المرشحات أصبح لدينا الآن مصفوفة FILTER_NAMES، وبالتالي يمكننا استخدامها لتصيير المرشِّحات الثلاثة، إذ يمكننا إنشاء ثابت يسمى filterList ضمن الدالة App()، إذ سنستخدِم هذا الثابت لربط مصفوفة الأسماء وإعادة المكوِّن <FilterButton />، وتذكّر أننا هنا بحاجة إلى مفاتيح أيضًا، لذا أضف ما يلي بعد التصريح عن الثابت taskList: const filterList = FILTER_NAMES.map(name => ( <FilterButton key={name} name={name}/> )); سنستبدل الآن الثابت filterList بالمكونات <FilterButton /> الثلاثة المكرَّرة في الملف App.js، أي بدلًا مما يلي: <FilterButton /> <FilterButton /> <FilterButton /> ضع التالي: {filterList} لن يعمل ذلك الآن، إذ لدينا المزيد لعمله أولًا. المرشحات التفاعلية يمكنك جعل أزرار المرشِّحات تفاعليةً من خلال تحديد الخاصيات التي تحتاج إلى استخدامها. نعلم أن المكوِّن <FilterButton /> يجب أن يبلّغ عمّا إذا كان مضغوطًا حاليًا، ويجب الضغط عليه إذا تطابق اسمه مع القيمة الحالية لحالة المرشِّح. نعلم أن المكوِّن <FilterButton /> يحتاج إلى دالة رد نداء لضبط المرشِّح النشط، كما يمكننا الاستفادة مباشرةً من الخطّاف setFilter. عدِّل الثابت filterList كما يلي: const filterList = FILTER_NAMES.map(name => ( <FilterButton key={name} name={name} isPressed={name === filter} setFilter={setFilter} /> )); كما يجب الآن تعديل الملف FilterButton.js لاستخدام الخاصيات التي قدمناها له بالطريقة نفسها التي طبّقناها سابقًا مع المكوِّن <Todo />، لذا طبّق ما يلي وتذكَّر استخدام الأقواس المعقوصة لقراءة هذه المتغيرات: ضع all مكان الخاصية {props.name}. اضبط قيمة السمة aria-pressed على الخاصية {props.isPressed}. أضف السمة onClick التي تستدعي الخطّاف props.setFilter() مع اسم المرشِّح. يجب أن تكون الآن الدالة FilterButton() كما يلي: function FilterButton(props) { return ( <button type="button" className="btn toggle-btn" aria-pressed={props.isPressed} onClick={() => props.setFilter(props.name)} > <span className="visually-hidden">Show </span> <span>{props.name}</span> <span className="visually-hidden"> tasks</span> </button> ); } انتقل إلى متصفحك مرةً أخرى، إذ يجب أن ترى تسمية الأزرار المختلفة بأسمائها الخاصة، فإذا ضغطتَ على زر الترشيح Filter، فيجب أن ترى نَص الزر يأخذ تخطيطًا جديدًا، إذ يخبرك هذا بأنه مُحدَّد، فإذا ألقيت نظرةً على فاحص صفحة أدوات التطوير DevTool’s Page Inspector أثناء النقر على الأزرار، فسترى أنّ قيم السمة aria-pressed تتغير وفقًا لذلك. لكن لا تزال الأزرار لا ترشِّح المهام في واجهة المستخدِم. ترشيح المهام في واجهة المستخدم يربط الثابت taskList في الدالة App() حاليًا حالة المهام ويعيد مكوِّن <Todo /> جديدًا لكل منها، ولكننا لا نريد ذلك، إذ يجب تصيير المهمة فقط إذا كانت مُضمَّنةً في نتائج تطبيق المرشِّح المحدَّد، وبالتالي يجب ترشيح حالة المهام باستخدام التابع Array.prototype.filter() قبل ربطها لإزالة الكائنات التي لا نريد تصييرها، لذا عدِّل الثابت taskList كما يلي: const taskList = tasks .filter(FILTER_MAP[filter]) .map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} toggleTaskCompleted={toggleTaskCompleted} deleteTask={deleteTask} editTask={editTask} /> )); يمكن الوصول إلى قيمة في المصفوفة FILTER_MAP التي تتوافق مع مفتاح حالة المرشِّح لتحديد دالة رد النداء التي يجب استخدامها في التابع Array.prototype.filter()، فإذا كان المرشِّح هو All مثلًا، فسيُقيَّم العنصر FILTER_MAP[filter] على () => true، كما يؤدي اختيار المرشِّح في متصفحك الآن إلى إزالة المهام التي لا تفي بمعاييره، في حين سيتغير العدد الموجود في العنوان أعلى القائمة ليمثِّل القائمة. الخلاصة اكتمل تطبيقنا الآن، ولكن يمكننا إجراء بعض التحسينات لضمان إمكانية استخدام مجموعة أكبر من المستخدِمين له، كما يتناول المقال التالي تضمين إدارة التركيز Focus Management في React التي يمكنها تحسين قابلية الاستخدام وتقليل الارتباك لكل من مستخدِمي لوحة المفاتيح فقط وقارئات الشاشة. ترجمة -وبتصرُّف- للمقال React interactivity: Editing, filtering, conditional rendering. اقرأ أيضًا تنفيذ التفاعل في تطبيق React: الأحداث والحالة إنشاء تطبيق قائمة مهام باستخدام React تقسيم تطبيق React إلى مكونات أساسيات بناء تطبيقات الويب
-
حان الوقت الآن لتعديل تطبيقنا من واجهة مستخدِم ثابتة تمامًا إلى واجهة مستخدِم تسمح لنا بالتفاعل معها وتعديلها بعد وضع خطة للمكوّنات من خلال البحث عن الأحداث Events والحالة State، إذ ينتج في النهاية تطبيق يمكننا من خلاله إضافة المهام وحذفها بنجاح، ووضع علامة على المهام المكتملة. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية. الهدف: التعرف على كيفية التعامل مع الأحداث والحالة في React، واستخدامها لإنشاء دراسة حالة تطبيق تفاعلي. معالجة الأحداث إذا استخدَمت لغة جافاسكربت الصرفة Vanilla JavaScript سابقًا، فلا بدّ أنك معتاد على وجود ملف جافاسكربت منفصل، إذ يمكنك الاستعلام عن بعض عُقد DOM وربط المستمعين بها كما يلي: const btn = document.querySelector('button'); btn.addEventListener('click', () => { alert("hi!"); }); يمكننا في React كتابة معالِجات الأحداث مباشرةً للعناصر الموجودة في JSX كما يلي: <button type="button" onClick={() => alert("hi!")} > Say hi! </button> يمكن أن يُعَدّ ذلك أمرًا غير مناسب لنصائح أفضل الممارسات التي تميل إلى عدم استخدام معالِجات الأحداث المُضمَّنة في HTML، ولكن تذكر أنّ صيغة JSX هي جزء من لغة جافاسكربت. أضفنا في المثال السابق السمة onClick إلى عنصر الزر <button>، وقيمة هذه السمة هي دالة تشغّل تنبيهًا بسيطًا، إذ تملك السمة onClick معنًى خاصًا هنا، فهي تخبر React بتشغيل دالة معيّنة عندما ينقر المستخدِم على الزر، كما يجب ملاحظة بعض الأشياء الأخرى، وهي: تُعَدّ طبيعة اسم السمة onClick ذات حالة الجَمل Camel-cased مهمةً، إذ لن تتعرف صيغة JSX على السمة onclick، لأن هذه الكلمة محجوزة في لغة جافاسكربت، وتُستخدَم لغرض مختلف يمثل خاصيات معالِج الحدث onclick المعيارية. تتبع جميع أحداث المتصفح هذا التنسيق في صيغة JSX باستخدام الجزء on متبوعًا باسم الحدث. لنطبّق هذه الملاحظات على تطبيقنا بدءًا من المكوِّن Form.js. معالجة إرسال النموذج أنشئ دالةً بالاسم handleSubmit() في الجزء العلوي من دالة المكوِّن Form()، إذ يجب على هذه الدالة منع سلوك الحدث submit الافتراضي، ثم يجب إطلاق تنبيه alert() بالذي تريده كما يلي: function handleSubmit(e) { e.preventDefault(); alert('Hello, world!'); } يمكنك استخدام هذه الدالة من خلال إضافة السمة onSubmit إلى العنصر <form>، وضبط قيمتها على الدالة handleSubmit كما يلي: <form onSubmit={handleSubmit}> إذا عدت إلى متصفحك ونقرت على زر "الإضافة Add"، فسيعرض المتصفح مربع حوار تنبيه يحتوي على الرسالة "Hello, world!" أو أيّ شيء آخر اخترته. خاصيات رد النداء يندر اقتصار التفاعل على مكوِّن واحد فقط في تطبيقات React، إذ ستؤثِّر الأحداث التي تحدث في أحد المكوِّنات على أجزاء أخرى من التطبيق، فإذا كان لدينا القدرة على إنشاء مهام جديدة مثلًا، فستؤثِّر الأشياء التي تحدث في المكوِّن <Form /> على القائمة المُصيَّرة في المكوِّن <App />. نريد أن تساعدنا الدالة handleSubmit() في إنشاء مهمة جديدة، لذلك سنحتاج إلى طريقة لتمرير المعلومات من المكوِّن <Form /> إلى المكوِّن <App />، فلا يمكننا تمرير البيانات من الابن إلى الأب بالطريقة نفسها التي نمرر بها البيانات من الأب إلى الابن باستخدام الخاصيات Props المعيارية، إذ يمكننا بدلًا من ذلك كتابة دالة في المكوِّن <App /> تتوقع بعض البيانات من نموذجنا بوصفها دخلًا لها، ثم تمرير هذه الدالة إلى المكوِّن <Form /> بوصفها خاصيةً، وتُسمَّى هذه الدالة التي تُعامَل على أنها خاصية بخاصية رد النداء Callback Prop، إذ يمكننا استدعاء خاصية رد النداء ضمن المكوِّن <Form /> لإرسال البيانات الصحيحة إلى المكوِّن <App />. معالجة إرسال النموذج باستخدام خاصيات رد النداء أنشئ دالةً بالاسم addTask() في الجزء العلوي من دالة المكوِّن App()، بحيث تحتوي هذه الدالة على معامِل واحد هو name: function addTask(name) { alert(name); } سنمرِّر بعد ذلك الدالة addTask() إلى المكوِّن <Form /> بوصفها خاصيةً، إذ يمكن أن تحمل الخاصية أيّ اسم تريده، ولكن اختر اسمًا تفهمه لاحقًا مثل الاسم addTask الذي يتطابق مع اسم الدالة ومع ما ستفعله، وهنا يجب تعديل استدعاء المكوِّن <Form /> كما يلي: <Form addTask={addTask} /> أخيرًا، يمكنك استخدام هذه الخاصية ضمن الدالة handleSubmit() في المكوِّن <Form /> كما يلي: function handleSubmit(e) { e.preventDefault(); props.addTask("Say hello!"); } سيؤدي النقر على زر "الإضافة Add" في متصفحك إلى إثبات عمل دالة رد النداء addTask()، لكن سيكون جيدًا أن نحصل على تنبيه لإظهار ما نكتبه في حقل الإدخال، وهذا ما سنفعله لاحقًا. ملاحظة: سمّينا خاصية رد النداء بالاسم addTask لتسهيل فهم ما ستفعله هذه الخاصية، ومن الاصطلاحات الشائعة الأخرى التي قد تصادفها في شيفرة React هي أن تُسبَق أسماء خاصيات رد النداء بالكلمة on متبوعةً باسم الحدث الذي سيؤدي إلى تشغيلها، إذ يمكننا مثلًا تسمية خاصية بالاسم onSubmit مع القيمة addTask. الحالة والخطاف useState استخدَمنا حتى الآن الخاصيات لتمرير البيانات عبر المكوّنات، ولكننا نحتاج إلى شيء آخر عند تعاملنا مع دخل المستخدِم وتحديثات البيانات. تأتي الخاصيات من أب المكوِّن، فلن يرث المكوِّن <Form /> مثلًا اسمًا جديدًا للمهمة، إذ يتواجد العنصر <input /> مباشرةً ضمن المكوِّن <Form />، لذا سيكون هذا المكوِّن مسؤولًا مباشرةً عن إنشاء هذا الاسم الجديد، كما لا يمكننا الطلب من المكوِّن <Form /> إنشاء خاصياته تلقائيًا، ولكن يمكننا أن نطلب منه تتبع بعض بياناته، إذ تسمى هذه البيانات التي يمتلكها المكوِّن نفسه بالحالة State، والتي تُعَدّ أداةً قويةً أخرى من React، لأن المكوّنات لا تمتلك الحالة فحسب، وإنما يمكنها تحديثها لاحقًا، في حين لا يمكن تحديث الخاصيات التي يتلقاها المكوِّن، فهي للقراءة فقط. توفِّر React مجموعةً متنوعةً من الدوال الخاصة التي تسمح لنا بتوفير إمكانات جديدة للمكوّنات مثل الحالة، إذ تُسمَّى هذه الدوال بالخطّافات Hooks، والخطّاف useState -كما يوحي اسمه- هو بالضبط الذي نحتاجه لإعطاء مكوِّننا حالةً، وهنا يجب استيراد خطّاف React من الوحدة react لاستخدامه، لذا عدِّل السطر الأول في الملف Form.js ليصبح كما يلي: import React, { useState } from "react"; يسمح لنا ذلك باستيراد الدالة useState()، واستخدامها في أيّ مكان من هذا الملف، كما تنشئ هذه الدالة حالةً لمكوِّن، ويحدِّد معامِلها الوحيد القيمة الأولية لتلك الحالة، كما تُعيد هذه الدالة الحالةَ ودالةً يمكن استخدامها لتحديث الحالة لاحقًا، ولنجرب ذلك أولًا من خلال إنشاء الحالة name ودالةً لتحديث هذه الحالة، لذا اكتب السطر التالي قبل الدالة handleSubmit() ضمن الدالة Form(): const [name, setName] = useState('Use hooks!'); يحدُث ما يلي في السطر السابق: ضبط قيمة الحالة name الأولية على القيمة "Use hooks!". تعريف دالة بالاسم setName() وظيفتها تعديل الحالة name. تعيد الدالة useState() الشيئين السابقين، لذا فإننا نستخدِم عملية هدم المصفوفات Array Destructuring لإسنادهما إلى متغيرَين منفصلين. حالة القراءة يمكنك رؤية الحالة name قيد التشغيل مباشرةً، لذا أضِف السمة value إلى العنصر input في النموذج، واضبط قيمتها لتكون name، إذ سيصيِّر متصفحك التعليمة "Use hooks!" في العنصر input. <input type="text" id="new-todo-input" className="input input__lg" name="text" autoComplete="off" value={name} /> عدِّل بعد ذلك التعليمة "Use hooks!" إلى سلسلة نصية فارغة، فهذا ما نريده لحالتنا الأولية كما يلي: const [name, setName] = useState(''); قراءة دخل المستخدم يجب التقاط دخل المستخدِم الذي يكتبه قبل تمكننا من تغيير قيمة الحالة nameمن خلال الاستماع إلى الحدث onChange، فلنكتب الدالة handleChange()، ونستمع إليها على الوسم <input />. // قُرب أعلى المكوِّن `Form` function handleChange(e) { console.log("Typing!"); } // بعد تعليمة `return` <input type="text" id="new-todo-input" className="input input__lg" name="text" autoComplete="off" value={name} onChange={handleChange} /> لن تتغير قيمة الدخل أثناء الكتابة حاليًا، ولكن سيطبع متصفحك الكلمة "Typing!" على طرفية جافاسكربت (نافذة console)، وبالتالي سنعلم أنّ مستمع الحدث متصل بالدخل، كما يمكنك تغيير قيمة الدخل من خلال استخدام الدالة handleChange() لتحديث الحالة name. يمكن قراءة محتويات حقل الإدخال عند تغييرها من خلال الوصول إلى الخاصية value الخاصة بالعنصر input عن طريق قراءة القيمة e.target.value ضمن الدالة handleChange()، إذ يمثِّل e.target العنصر الذي أَطلق الحدث change، وهو العنصر input، وبالتالي تكون الخاصية value هي النص الموجود ضمنها، كما يمكنك تنفيذ التابع console.log() على هذه القيمة لرؤيتها في طرفية متصفحك كما يلي: function handleChange(e) { console.log(e.target.value); } حالة التحديث يجب تخزين حالة name المحدَّثة مع تغير قيمة الدخل، لذا غيِّر التابع console.log() إلى setName() كما يلي: function handleChange(e) { setName(e.target.value); } يجب الآن تعديل الدالة handleSubmit() لتستدعي الخاصية props.addTask مع الحالة name على أساس وسيط، مما يؤدي إلى إرسال المهمة مرةً أخرى إلى المكوِّن App، لنتمكن من إضافتها إلى قائمة المهام لاحقًا، كما يجب مسح الدخل بعد إرسال النموذج، لذلك سنستدعي الدالة setName() مرةً أخرى مع سلسلة نصية فارغة. function handleSubmit(e) { e.preventDefault(); props.addTask(name); setName(""); } أخيرًا، يمكنك كتابة شيء ما في حقل الإدخال في متصفحك والنقر فوق زر "Add"، وبالتالي سيظهر كل ما كتبته في مربع حوار التنبيه، والآن يجب أن يكون الملف Form.js كما يلي: import React, { useState } from "react"; function Form(props) { const [name, setName] = useState(""); function handleChange(e) { setName(e.target.value); } function handleSubmit(e) { e.preventDefault(); props.addTask(name); setName(""); } return ( <form onSubmit={handleSubmit}> <h2 className="label-wrapper"> <label htmlFor="new-todo-input" className="label__lg"> What needs to be done? </label> </h2> <input type="text" id="new-todo-input" className="input input__lg" name="text" autoComplete="off" value={name} onChange={handleChange} /> <button type="submit" className="btn btn__primary btn__lg"> Add </button> </form> ); } export default Form; ملاحظة: هناك شيء واحد ستلاحظه، وهو أنه يمكنك إرسال مهام فارغة عند الضغط على زر الإضافة Add دون إدخال اسم المهمة، لذلك يجب منع إضافة المهام الفارغة من خلال إضافة تحقق ما إلى الدالة handleSubmit() على سبيل المثال. إضافة مهمة أصبحنا الآن جاهزين لكتابة دوال تسمح للمستخدِم بإضافة مهمة جديدة من متصفحه بعد أن تدربنا على الأحداث وخاصيات رد النداء والخطّافات. استخدام المهام بوصفها حالات استورِد الخطّاف useState إلى الملف App.js لتتمكن من تخزين المهام في حالة ما من خلال تعديل سطر استيراد React إلى ما يلي: import React, { useState } from "react"; نريد تمرير الخاصية props.tasks إلى الخطّاف useState()، إذ ستحتفظ هذه الخاصية بحالة الخطّاف الأولية، لذا أضف السطر التالي في الجزء العلوي من تعريف الدالة App(): const [tasks, setTasks] = useState(props.tasks); يمكننا الآن تغيير ربط قائمة المهام taskList لتكون نتيجةً لربط tasks بدلًا من props.tasks، إذ يجب أن يبدو تصريح الثابت taskList كما يلي: const taskList = tasks.map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} /> ) ); إضافة مهمة لدينا الآن الخطّاف setTasks الذي يمكننا استخدامه في الدالة addTask() لتحديث قائمة المهام، ولكن هناك مشكلة أنه لا يمكننا فقط تمرير الوسيط name الخاص بالدالة addTask() إلى الخطاف setTasks، لأنّ tasks هي مصفوفة من الكائنات؛ أما الوسيط name، فهو سلسلة نصية، وبالتالي ستكون السلسلة النصية مكان المصفوفة. يجب أولًا وضع الوسيط name في كائن له بنية مهامنا الحالية نفسها، إذ سننشئ ضمن الدالة addTask() الكائن newTask لإضافته إلى المصفوفة، ويجب بعد ذلك إنشاء مصفوفة جديدة مع إضافة هذه المهمة الجديدة إليها، ثم تحديث حالة بيانات المهام إلى هذه الحالة الجديدة من خلال استخدام صيغة الانتشار Spread Syntax لنسخ المصفوفة الحالية، وإضافة الكائن في النهاية، ثم تمرير هذه المصفوفة إلى الدالة setTasks() لتحديث الحالة، وبالتالي يجب أن تصبح الدالة addTask() كما يلي: function addTask(name) { const newTask = { id: "id", name: name, completed: false }; setTasks([...tasks, newTask]); } يمكنك الآن استخدام المتصفح لإضافة مهمة إلى بياناتنا، لذا اكتب أيّ شيء تريده في النموذج، وانقر زر الإضافة Add أو اضغط على مفتاح Enter من لوحة المفاتيح، إذ سترى عنصر المهام الجديد يظهَر في واجهة المستخدِم، لكن هناك مشكلة أخرى تتمثّل بإعطاء الدالة addTask() المعرّف id نفسه لكل مهمَّة، إذ يُعَدّ ذلك أمرًا سيئًا لإمكانية الوصول، كما يجعل التمييز بين المهام المستقبلية أمرًا مستحيلًا على React باستخدام الخاصية key، إذ ستعطيك React تحذيرًا في طرفية أدوات التطوير DevTools مثل رسالة التحذير التالية: "Warning: Encountered two children with the same key…". يجب إصلاح هذه المشكلة، كما يُعَدّ إنشاء معرّفات فريدة مشكلةً صعبةً، وهي مشكلة كتَب لها مجتمع جافاسكربت بعض المكتبات المفيدة من أجلها، إذ سنستخدِم حاليًا المكتبة nanoid لأنها صغيرة الحجم وتعمل جيدًا، لذا تأكّد من أنك في المجلد الجذر لتطبيقك وشغّل الأمر التالي في طرفيتك: npm install nanoid وفي ملاحظة مهمة، إذا أردتَ استخدام مدير الحزم yarn، فيجب كتابة الأمر: yarn add nanoid. يمكننا الآن استيراد المكتبة nanoid في الجزء العلوي من الملف App.js لنتمكّن من استخدامها لإنشاء معرِّفات فريدة لمهامنا الجديدة، لذا ضمّن أولًا سطر الاستيراد التالي في أعلى الملف App.js: import { nanoid } from "nanoid"; لنحدّث الآن الدالة addTask() بحيث يصبح كل معرِّف مهمة مؤلفًا من البادئة todo- بالإضافة إلى سلسلة نصية فريدة تنشئها المكتبة nanoid، لذا عدِّل تصريح الثابت newTask إلى ما يلي: const newTask = { id: "todo-" + nanoid(), name: name, completed: false }; احفظ كل شيء، وجرب تطبيقك مرةً أخرى، إذ يمكنك الآن إضافة المهام دون تلقِّي هذا التحذير بشأن المعرِّفات المُكرَّرة. عد المهام يمكننا الآن إضافة مهام جديدة، ولكن هناك مشكلة تتمثَّل بقراءة العنوان ثلاث مهام متبقية، بغض النظر عن عدد المهام، إذ يمكن إصلاح ذلك عن طريق حساب طول قائمة المهام taskList وتغيير نص العنوان وفقًا لذلك، لذا أضف ما يلي ضمن تعريف الدالة App() قبل تعليمة return: const headingText = `${taskList.length} tasks remaining`; هذا صحيح تقريبًا باستثناء أنه إذا احتوت قائمتنا على مهمة واحدة، فسيظل العنوان يستخدِم الكلمة "tasks"، ولذلك يجب أن نجعلها متغيرةً، لذا عدِّل الشيفرة التي أضفتها للتو كما يلي: const tasksNoun = taskList.length !== 1 ? 'tasks' : 'task'; const headingText = `${taskList.length} ${tasksNoun} remaining`; يمكنك الآن استبدال المتغير headingText بمحتوى نص عنوان القائمة، لذا عدِّل العنصر <h2> ليصبح كما يلي: <h2 id="list-heading">{headingText}</h2> إكمال مهمة لاحظ أنّ مربع الاختيار يُحدَّد ويُلغَى تحديده بطريقة مناسبة عند النقر عليه، كما تُعَدّ معرفة المتصفح لكيفية تذكّر مدخلات مربعات الاختيار المُحدَّدة أو المُلغَى تحديدها دون مساعدتنا ميزةً في لغة HTML، ولكن تخفي هذه الميزة مشكلةً، إذ لا يغيِّر تحديد مربع الاختيار أو عدم تحديده الحالة في تطبيق React، مما يعني عدم تزامن المتصفح مع التطبيق، لذلك يجب كتابة شيفرتنا لإعادة المتصفح متزامنًا مع التطبيق. إثبات الخطأ لنكتب الدالة toggleTaskCompleted() في المكوِّن App()، إذ سيكون لهذه الدالة المعامِل id، لكننا لن نستخدِمها حاليًا، إذ سنسجِّل الآن المهمة الأولى في المصفوفة في الطرفية، وسنفحص ما يحدث عندما نحدِّدها أو نلغي تحديدها في متصفحنا، لذا أضف ما يلي قبل التصريح عن الثابت taskList مباشرةً: function toggleTaskCompleted(id) { console.log(tasks[0]) } سنضيف بعد ذلك الخاصية toggleTaskCompleted إلى خاصيات كل مكوّن من مكوّنات <Todo /> المُصيَّرة ضمن taskList كما يلي: const taskList = tasks.map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} toggleTaskCompleted={toggleTaskCompleted} /> )); انتقل إلى المكوِّن Todo.js وأضف معالِج الحدث onChange إلى العنصر <input /> الذي يجب أن يستخدِم دالةً مجهولةً لاستدعاء الخاصية props.toggleTaskCompleted() مع المعامِل props.id، إذ يجب أن يكون العنصر <input /> الآن كما يلي: <input id={props.id} type="checkbox" defaultChecked={props.completed} onChange={() => props.toggleTaskCompleted(props.id)} /> احفظ كل شيء وعُد إلى متصفحك ولاحظ تحديد المهمة الأولى Eat، ثم افتح طرفية جافاسكربت، ثم انقر على مربع الاختيار الموجود بجوار الخيار Eat، وبالتالي فإنّ مربع الاختيار هذا غير محدَّد كما توقّعنا، ولكن ستعطي طرفية جافاسكربت الخاصة بك شيئًا كما يلي: Object { id: "task-0", name: "Eat", completed: true } يُلغَى تحديد مربع الاختيار في المتصفح، لكن تخبرنا الطرفية بأن المهمَّة Eat لا تزال مكتملةً، وسنصلح ذلك لاحقًا. مزامنة المتصفح مع بياناتنا لنَعُد إلى الدالة toggleTaskCompleted() في الملف App.js، إذ نريدها أن تغيِّر الخاصية completed للمهمَّة التي أُلغِي تحديدها فقط، وترك المهام الأخرى كما هي، لذلك سنطبّق التابع map() على قائمة المهام وسنغيّر القائمة التي أكملناها فقط، لذا عدِّل الدالة toggleTaskCompleted() إلى ما يلي: function toggleTaskCompleted(id) { const updatedTasks = tasks.map(task => { // إذا كان لهذه المهمة معرِّف المهمة المُعدَّلة نفسه if (id === task.id) { // استخدم انتشار الكائن لإنشاء كائن جديد // عُدِّلت الخاصية `completed` الخاصة به return {...task, completed: !task.completed} } return task; }); setTasks(updatedTasks); } عرّفنا الثابت updatedTasks الذي يمر على عناصر المصفوفة tasks الأصلية، فإذا طابقت خاصية معرّفُ id المهمة المعرّفَ id المقدَّم للدالة، فسنستخدِم صيغة انتشار الكائن Object Spread Syntax لإنشاء كائن جديد، وسنبدّل إلى الخاصية checked لهذا الكائن قبل إعادته؛ أما إذا لم يتطابقا، فسنعيد الكائن الأصلي. نستدعي بعد ذلك الدالة setTasks() مع هذه المصفوفة الجديدة لتحديث الحالة. حذف مهمة سيتَّبع حذف مهمة نمطًا مشابهًا لتبديل حالتها المكتملة، إذ يجب تعريف دالة لتحديث الحالة، ثم تمرير هذه الدالة إلى المكوِّن <Todo /> بوصفها خاصيةً، واستدعاؤها عند حدوث الحدث الصحيح. خاصية رد النداء deleteTask سنكتب الدالة deleteTask() في المكوِّن App، إذ ستأخذ هذه الدالة المعامِل id مثل الدالة toggleTaskCompleted()، وسنسجّل هذا المعرِّف في الطرفية، لذا أضف ما يلي بعد الدالة toggleTaskCompleted(): function deleteTask(id) { console.log(id) } أضف بعد ذلك خاصية رد نداء أخرى إلى مصفوفة مكوّنات <Todo /> كما يلي: const taskList = tasks.map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} toggleTaskCompleted={toggleTaskCompleted} deleteTask={deleteTask} /> )); يجب استدعاء الدالة props.deleteTask() في الملف Todo.js عند الضغط على زر الحذف Delete، كما تحتاج الدالة deleteTask() إلى معرفة معرِّف المهمَّة التي ستستدعيها لتتمكّن من حذف المهمَّة الصحيحة من الحالة، لذا عدِّل زر الحذف Delete ضمن الملف Todo.js كما يلي: <button type="button" className="btn btn__danger" onClick={() => props.deleteTask(props.id)} > Delete <span className="visually-hidden">{props.name}</span> </button> إذا نقرتَ الآن على أيّ من أزرار الحذف Delete في التطبيق، فيجب أن تسجِّل طرفية المتصفح معرّف المهمَّة المرتبطة به. حذف المهام من الحالة وواجهة المستخدم يمكننا الآن استدعاء الخطّاف setTasks() في الدالة deleteTask() استدعاءً صحيحًا لحذف هذه المهمة فعليًا من حالة التطبيق وحذفها مرئيًا من واجهة مستخدِم التطبيق، وبما أنّ الخطّاف setTasks() يتوقع مصفوفةً بوصفها وسيطًا له، فيجب تزويده بمصفوفة جديدة تنسخ المهام الحالية باستثناء المهمة التي يتطابق معرِّفها مع معرِّف المهمَّة المُمرَّرة إلى الدالة deleteTask(). يمكننا الآن استخدام التابع Array.prototype.filter()، إذ يمكننا اختبار كل مهمة، واستبعاد مهمة من المصفوفة الجديدة إذا تطابقت خاصيتها id مع المعامِل id المُمرَّر إلى الدالة deleteTask()، لذا عدِّل الدالة deleteTask() ضمن الملف App.js كما يلي: function deleteTask(id) { const remainingTasks = tasks.filter(task => id !== task.id); setTasks(remainingTasks); } جرِّب تطبيقك مرةً أخرى، إذ يجب أن تكون قادرًا على حذف مهمَّة من تطبيقك. الخلاصة قدّمنا في هذا المقال معلومات حول كيفية تعامل React مع الأحداث والحالة، وتنفيذ وظائف إضافة المهام وحذفها ووضع علامة على المهام المكتملة، كما سنطبّق في المقال التالي وظيفة تعديل المهام الحالية وترشيح قائمة المهام جميعها والمهام المكتملة وغير المكتملة فقط، كما سنطّلع على التصيير الشرطي لواجهة المستخدِم UI. ترجمة -وبتصرُّف- للمقال React interactivity: Events and state. اقرأ أيضًا إنشاء تطبيق قائمة مهام باستخدام React تقسيم تطبيق React إلى مكونات أساسيات بناء تطبيقات الويب
-
يُعَدّ تطبيقنا الذي عملنا عليه في المقال السابق وحدةً متراصةً، لذلك يجب تقسيمه إلى مكوّنات يمكن وصفها وإدارتها قبل تمكننا من جعل تطبيقنا يفعل شيئًا ما، إذ لا تحتوي مكتبة React على قواعد صارمة لتحديد ما يُعَدّ مكوّنًا Component، فالأمر متروك لك، وسنعرض في هذا المقال طريقةً معقولةً لتقسيم تطبيقنا إلى مكونات. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية. الهدف: إظهار طريقة لتقسيم تطبيق قائمة المهام إلى مكوّنات. تحديد المكون الأول قد يبدو تحديد أحد المكوّنات أمرًا صعبًا إلى حين حصولك على بعض الخبرة العملية، ولكن الأمور المهمة هي: إذا كان أحد الأشياء جزءًا واضحًا من تطبيقك، فيُحتمَل أن يكون مكوّنًا. إذا أُعيد استخدام أحد الأشياء كثيرًا، فيُحتمَل أن يكون مكوّنًا. تُعَدّ النقطة الثانية ذات قيمة خاصة، إذ يتيح إنشاء مكوّن من عناصر واجهة المستخدِم تعديلَ شيفرتك البرمجية في مكان واحد ورؤية تلك التعديلات في جميع الأماكن التي يُستخدَم فيها هذا المكوّن، إذ ليس عليك تقسيم كل شيء إلى مكوِنات مباشرةً، ولنستخدِم النقطة الثانية لإنشاء مكوّن من أكثر الأجزاء المُعاد استخدامها والأكثر أهميةً في واجهة المستخدِم وهو عنصر قائمة المهام. إنشاء المكون <Todo /> يجب علينا إنشاء ملف جديد للمكوّن قبل إنشائه، ويجب إنشاء مجلد لهذه المكونات، إذ تنشئ الأوامر التالية المجلد components وملفًا ضمنه يُسمَّى Todo.js، ولكن تأكّد من وجودك في جذر تطبيقك قبل تشغيل هذه الأوامر: mkdir src/components touch src/components/Todo.js إنّ ملف Todo.js الجديد فارغ حاليًا، لذا افتحه واكتب فيه السطر الأول التالي: import React from "react"; سننشئ مكوّنًا يسمّى Todo، لذلك يمكننا إضافة شيفرتنا إلى الملف Todo.js على النحو التالي، إذ سنعرِّف الدالة Todo() ونصدّرها على السطر نفسه كما يلي: export default function Todo() { return ( ); } كل شيء جيد حتى الآن، لكن يجب أن يعيد المكون شيئًا ما، لذا ارجع إلى الملف src/App.js، وانسخ أول عنصر <li> من القائمة غير المرتبة، والصقه في الملف Todo.js بحيث يصبح كما يلي: export default function Todo() { return ( <li className="todo stack-small"> <div className="c-cb"> <input id="todo-0" type="checkbox" defaultChecked={true} /> <label className="todo-label" htmlFor="todo-0"> Eat </label> </div> <div className="btn-group"> <button type="button" className="btn"> Edit <span className="visually-hidden">Eat</span> </button> <button type="button" className="btn btn__danger"> Delete <span className="visually-hidden">Eat</span> </button> </div> </li> ); } ملاحظة: يجب أن تعيد المكونات شيئًا ما دائمًا، فإذا حاولت لاحقًا تصيير Render مكون لا يعيد شيئًا، فستعرِض React خطأً في متصفحك. أصبح المكوّن Todo مكتملًا حاليًا، وبالتالي يمكننا استخدامه، والآن أضف السطر التالي في الملف App.js بالقرب من أعلى الملف لاستيراد المكوّن Todo: import Todo from "./components/Todo"; يمكنك مع استيراد هذا المكون وضع استدعاءات المكوّن <Todo /> مكان جميع عناصر <li> في الملف App.js، ويجب أن يصبح العنصر <ul> كما يلي: <ul role="list" className="todo-list stack-large stack-exception" aria-labelledby="list-heading" > <Todo /> <Todo /> <Todo /> </ul> إذا نظرت إلى متصفحك، فستلاحظ شيئًا مؤسفًا، إذ تُكرِّر قائمتك المهمة الأولى ثلاث مرات كما يلي: لا نريد الأكل فقط، إذ لدينا مهام أخرى يجب تنفيذها، وسنوضّح فيما يلي كيفية إجراء استدعاءات لمكوّنات مختلفة تصيّر محتوًى فريدًا. إنشاء مكون <Todo /> فريد تُعَدّ المكونات مهمةً لأنها تتيح إعادة استخدام أجزاء من واجهة المستخدِم، والإشارة إلى مكان ما ليكون مصدرًا لواجهة المستخدِم تلك، ولكن تكمن المشكلة في أننا لا نريد إعادة استخدام جميع المكونات، وإنما نريد إعادة استخدام معظم الأجزاء، وتعديل أجزاء صغيرة، لذا يجب استخدام الخاصيات Props. الخاصية name إذا أردنا تتبّع أسماء المهام التي نريد إكمالها، فيجب علينا التأكد من أنّ كل مكوّن <Todo /> يصيِّر اسمًا فريدًا، لذا امنح كل مكوّن <Todo /> في الملف App.js خاصية الاسم name، ولنستخدم أسماء المهام التي كانت لدينا سابقًا كما يلي: <Todo name="Eat" /> <Todo name="Sleep" /> <Todo name="Repeat" /> إذا حدّثتَ متصفحك، فسترى الشيء السابق نفسه بالضبط، إذ أعطينا المكوّن <Todo /> بعض الخاصيات، لكننا لم نستخدِمها بعد، فلنَعُد الآن إلى الملف Todo.js ونصلح كل شيء. عدّل أولًا تعريف الدالة Todo() بحيث تأخذ الخاصيات props على أساس معامِل، كما يمكنك تطبيق التابع console.log() على الخاصيات props كما فعلنا سابقًا إذا أردت التحقق من استلام المكوّن للخاصيات استلامًا صحيحًا، ثم يمكنك وضع خاصية الاسم name مكان تكرارات المهمة Eat، وتذكّر استخدام الأقواس المعقوصة لحقن قيمة متغير في تعابير JSX، إذ يجب أن تكون الدالة Todo() بعد ذلك كما يلي: export default function Todo(props) { return ( <li className="todo stack-small"> <div className="c-cb"> <input id="todo-0" type="checkbox" defaultChecked={true} /> <label className="todo-label" htmlFor="todo-0"> {props.name} </label> </div> <div className="btn-group"> <button type="button" className="btn"> Edit <span className="visually-hidden">{props.name}</span> </button> <button type="button" className="btn btn__danger"> Delete <span className="visually-hidden">{props.name}</span> </button> </div> </li> ); } يجب أن يعرض متصفحك الآن ثلاث مهام فريدة، ولكن لا تزال جميع هذه المهام مُحدَّدةً افتراضيًا. الخاصية completed حُدِّدت المهمة Eat فقط في القائمة الثابتة الأصلية، إذ نريد إعادة استخدام معظم واجهة المستخدِم التي تشكل المكوِّن <Todo /> مع تعديل شيء واحد من خلال منح كل استدعاء للمكوّن <Todo /> في الملف App.js الخاصية completed الجديدة، كما يجب أن يكون للخاصية completed التابعة للمكوّن الأول الذي اسمه Eat القيمة true، وللمكونات الأخرى القيمة false كما يلي: <Todo name="Eat" completed={true} /> <Todo name="Sleep" completed={false} /> <Todo name="Repeat" completed={false} /> يجب علينا العودة إلى الملف Todo.js لاستخدام هذه الخاصيات، لذا عدّل السمة defaultChecked للعنصر <input /> بحيث تساوي قيمتها الخاصية completed، ثم يكون عنصر <input /> الخاص بالمكون Todo على النحو التالي: <input id="todo-0" type="checkbox" defaultChecked={props.completed} /> حدّث متصفحك لإظهار تحديد المكوِّن Eat فقط كما يلي: إذا عدّلت كل خاصيات completed الخاصة بالمكوّن <Todo />، فسيحدِّد متصفحك أو يلغي تحديد مربعات الاختيار المكافئة والمُصيَّرة وفقًا لذلك. الخاصية id يعطي المكوّن <Todo /> لكل مهمة السمة id بالقيمة todo-0، وهذا خطأ في HTML لأن سمات id يجب أن تكون فريدةً، إذ تستخدِمها لغات جافاسكربت وCSS وغيرها بوصفها معرّفات فريدةً لأجزاء المستند، وبالتالي يجب أن نعطي المكون الخاصية id التي تأخذ قيمةً فريدةً لكل مكوّن Todo. إذًا لنمنح كل نسخة من المكوِّن <Todo /> معرّفًا باستخدام التنسيق todo-i، إذ تزيد قيمة i بمقدار واحد في كل مرة كما يلي: <Todo name="Eat" completed={true} id="todo-0" /> <Todo name="Sleep" completed={false} id="todo-1" /> <Todo name="Repeat" completed={false} id="todo-2" /> عُد الآن إلى الملف Todo.js واستفد من الخاصية id، إذ يجب تعديل قيمة السمة id للعنصر <input /> وقيمة السمة htmlFor الخاصة بالعنصر label كما يلي: <div className="c-cb"> <input id={props.id} type="checkbox" defaultChecked={props.completed} /> <label className="todo-label" htmlFor={props.id}> {props.name} </label> </div> كل شيء جيد حتى الآن، ولكن تُعَدّ شيفرتنا مكرَّرةً، فالأسطر الثلاثة التي تصيّر المكوّن <Todo /> متطابقة تقريبًا مع اختلاف واحد فقط هو قيمة كل خاصية، كما يمكننا تنظيف شيفرتنا باستخدام إحدى ميزات جافاسكربت الأساسية وهي التكرار Iteration، ولكن يجب أولًا إعادة التفكير في المهام لاستخدام هذه الميزة. بيانات المهام تحتوي كل مهمة من مهامنا حاليًا على ثلاثة أجزاء من المعلومات، وهي اسمها، وما إذا كانت مُحدَّدة، ومعرّفها الفريد، كما تُترجَم هذه البيانات إلى كائن Object، وبما أنه لدينا أكثر من مهمة، فسنستخدِم مصفوفةً من الكائنات لتمثيل هذه البيانات، لذا أنشئ ثابتًا const جديدًا بعد تعليمة الاستيراد الأخيرة في الملف src/index.js وقبل التابع ReactDOM.render() كما يلي: const DATA = [ { id: "todo-0", name: "Eat", completed: true }, { id: "todo-1", name: "Sleep", completed: false }, { id: "todo-2", name: "Repeat", completed: false } ]; سنمرِّر بعد ذلك الثابت DATA إلى المكوّن <App /> بوصفه خاصيةً تُسمَّى tasks، إذ يجب أن يكون السطر الأخير من الملف src/index.js كما يلي: ReactDOM.render(<App tasks={DATA} />, document.getElementById("root")); أصبحت هذه المصفوفة متاحةً الآن للمكون App بالصورة props.tasks، كما يمكنك استخدام التابع console.log() للتحقق من ذلك. التصيير مع التكرار يمكننا تصيير مصفوفة الكائنات من خلال تحويل كل منها إلى المكون <Todo />، إذ تمنحنا لغة جافاسكربت تابع مصفوفة لتحويل البيانات إلى شيء آخر، وهو Array.prototype.map()، لذا أنشئ ثابتًا const جديدًا يسمى taskList قبل تعليمة return الخاصة بالدالة App()، واستخدِم التابع map() لتحويله، ولنحوّل مجموعة مهامنا إلى شيء بسيط يتمثّل باسم name كل مهمة كما يلي: const taskList = props.tasks?.map(task => task.name); لنحاول وضع الثابت taskList مكان جميع أبناء العنصر <ul> كما يلي: <ul role="list" className="todo-list stack-large stack-exception" aria-labelledby="list-heading" > {taskList} </ul> يمنحنا ذلك طريقةً لإظهار جميع المكوّنات مرةً أخرى، إذ يصيّر المتصفح حاليًا اسم كل مهمة بوصفه نصًا دون بنية معينة، كما ينقصنا حاليًا بنية HTML مثل عنصر <li> ومربعات الاختيار والأزرار. يمكننا إصلاح ذلك من خلال إعادة المكوّن <Todo /> من التابع map()، وتذكَّر أنّ صيغة JSX تسمح لنا بخلط بنى جافاسكربت مع اللغات التوصيفية Markup، فلنجرب ما يلي بدلًا مما لدينا حاليًا: const taskList = props.tasks.map(task => <Todo />); انظر مرةً أخرى إلى تطبيقك، إذ تبدو مهامنا الآن كما كانت سابقًا، لكنها تفتقد إلى أسماء المهام نفسها، وتذكَّر أنّ كل مهمة نطبّق عليها التابع map() لها الخاصيات id وname وchecked، والتي نريد تمريرها إلى المكوّن <Todo />، وبالتالي سنحصل على الشيفرة التالية: const taskList = props.tasks.map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} /> )); يبدو التطبيق الآن كما كان سابقًا، ولكن أصبحت شيفرتنا أقل تكرارًا. خاصيات key الفريدة يجب أن تتعقّب React المهام لتصييرها بصورة صحيحة بعد أن صيّرت هذه المهام من مصفوفة، إذ تستخدِم React التخمين لتتبع الأشياء، ولكن يمكننا مساعدتها عن طريق تمرير الخاصية key لمكونات <Todo />، إذ تُعَدّ key خاصيةً خاصةً تديرها React، ولا يمكنك استخدام الكلمة key لأيّ غرض آخر، وبما أنّ الخاصية key يجب أن تكون فريدةً، فسنعيد استخدام خاصية id الخاصة بكل كائن مهمة على أساس مفتاح له، لذا عدِّل الثابت taskList كما يلي: const taskList = props.tasks.map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} /> ) ); يجب عليك دائمًا تمرير مفتاح فريد لأيّ شيء تُصيّره مع التكرار، ولن يتغير شيء واضح في متصفحك، ولكن إذا لم تستخدِم مفاتيح فريدةً، فستعطي React تحذيرات في الطرفية console ويمكن أن يتصرف تطبيقك بطريقة غريبة. تقسيم أجزاء التطبيق المتبقية إلى مكونات يمكننا الآن تحويل باقي التطبيق إلى مكونات بعد فرزِنا المكوّن الأكثر أهميةً، وتذكَّر أنّ المكونات هي إما أجزاء واضحة من واجهة المستخدِم، أو أجزاء معاد استخدامها من واجهة المستخدِم، أو كليهما، كما يمكننا إنشاء مكونين آخرين هما: <Form/> <FilterButton/> بما أننا نعلم بحاجتنا لهذين المكوِنين، فيمكننا تجميع أوامر إنشاء الملفات في أمر واحد في الطرفية، لذا شغّل الأمر التالي في طرفيتك، مع الانتباه إلى أنك في المجلد الجذر لتطبيقك: touch src/components/Form.js src/components/FilterButton.js المكون <Form/> افتح الملف components/Form.js ونفِّذ ما يلي: استورد مكتبة React في أعلى الملف كما فعلنا في الملف Todo.js. أنشئ المكوِّن Form() الجديد باستخدام بنية Todo() الأساسية نفسها، ثم صدِّر هذا المكوِّن. انسخ وسوم <form> وما يوجد بينها من الملف App.js، والصقها ضمن تعليمة return الخاصة بالمكوِّن Form(). صدّر المكوِّن Form في نهاية الملف. يجب أن يكون الملف Form.js الآن كما يلي: import React from "react"; function Form(props) { return ( <form> <h2 className="label-wrapper"> <label htmlFor="new-todo-input" className="label__lg"> What needs to be done? </label> </h2> <input type="text" id="new-todo-input" className="input input__lg" name="text" autoComplete="off" /> <button type="submit" className="btn btn__primary btn__lg"> Add </button> </form> ); } export default Form; المكون <FilterButton/> كرِّر الأمور نفسها التي نفّذتها لإنشاء الملف Form.js على الملف FilterButton.js، ولكن استدعِ المكوِّن FilterButton() وانسخ جزء HTML للزر الأول الموجود ضمن العنصر <div> ذو الصنف filters من الملف App.js في تعليمة return، إذ يجب أن يكون الملف الآن كما يلي: import React from "react"; function FilterButton(props) { return ( <button type="button" className="btn toggle-btn" aria-pressed="true"> <span className="visually-hidden">Show </span> <span>all </span> <span className="visually-hidden"> tasks</span> </button> ); } export default FilterButton; استيراد جميع المكونات أضف بعض تعليمات الاستيراد import في الجزء العلوي من الملف App.js لاستيراد هذه المكوّنات الجديدة، ثم عدّل تعليمة return الخاصة بالمكوِّن App() لتصيير المكونات، إذ يجب أن يكون الملف App.js كما يلي: import React from "react"; import Form from "./components/Form"; import FilterButton from "./components/FilterButton"; import Todo from "./components/Todo"; function App(props) { const taskList = props.tasks.map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} /> ) ); return ( <div className="todoapp stack-large"> <h1>TodoMatic</h1> <Form /> <div className="filters btn-group stack-exception"> <FilterButton /> <FilterButton /> <FilterButton /> </div> <h2 id="list-heading">3 tasks remaining</h2> <ul role="list" className="todo-list stack-large stack-exception" aria-labelledby="list-heading" > {taskList} </ul> </div> ); } export default App; نكون بذلك جاهزين تقريبًا للتعامل مع التفاعل في تطبيق React الخاص بنا. الخلاصة تعمّقنا في كيفية تقسيم التطبيق إلى مكوّنات وتصييرها بكفاءة، إذ سننتقل الآن إلى إلقاء نظرة على كيفية التعامل مع الأحداث في React وإضافة التفاعل. ترجمة -وبتصرُّف- للمقال Componentizing our React app. اقرأ أيضًا أساسيات بناء تطبيقات الويب مكونات React الأساسية (React Components)
-
لنفترض أننا نريد توضيح مفهوم React من خلال إنشاء تطبيق يسمح للمستخدِمين بإضافة المهام التي يريدون العمل عليها وتعديلها وحذفها، وكذلك وضع علامة على المهام المكتملة دون حذفها، إذ سنوجّهك من خلال هذا المقال لوضع بنية المكوّن App الأساسية وتصميمه في المكان الصحيح، وتعريف المكوّنات الفردية والتفاعلية التي سنضيفها لاحقًا. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية. الهدف: تقديم دراسة حالة تطبيق قائمة المهام، ووضع بنية المكوّن App الأساسية وتصميمه في المكان الصحيح. قصص مستخدم التطبيق تُعَدّ قصة المستخدِم في تطوير البرمجيات هدفًا قابلًا للتنفيذ من منظور المستخدِم، إذ سيساعدنا تحديد قصص المستخدِمين قبل البدء في تركيز عملنا، ويجب على تطبيقنا تحقيق القصص التالية، إذ يمكن للمستخِدم تنفيذ ما يلي: قراءة قائمة المهام. إضافة مهمة باستخدام الفأرة أو لوحة المفاتيح. وضْع علامة على المهام المكتملة باستخدام الفأرة أو لوحة المفاتيح. حذف أيّ مهمة باستخدام الفأرة أو لوحة المفاتيح. تعديل أيّ مهمة باستخدام الفأرة أو لوحة المفاتيح. عرض مجموعة فرعية محدَّدة من المهام: جميع المهام، أو المهمة النشطة فقط، أو المهام المكتملة فقط. تجهيز المشروع الأولي أنشأتْ الأداة create-react-app بعض الملفات التي لن نستخدِمها مطلقًا في مشروعنا، إذ لن نضيف ملف تنسيق سابق لعرض المكونات، لذا احذف أولًا استيراد App.css من أعلى الملف App.js، كما أننا لن نستخِدم الملف logo.svg، لذا أزِل استيراده أيضًا، ثم انسخ والصق بعد ذلك الأوامر التالية في طرفيتك لحذف بعض الملفات غير الضرورية، وتأكّد من أنك تبدأ من المجلد الجذر للتطبيق: # انتقل إلى المجلد src الخاص بمشروعك cd src # احذف بعض الملفات rm -- App.test.js App.css logo.svg serviceWorker.js setupTests.js # انتقل احتياطيًا إلى جذر المشروع cd .. ملاحظتان: هناك ملفان من الملفات التي حذفناها مخصَّصان لاختبار التطبيق، وبالتالي لن نغطّي الاختبار في مثالنا. إذا أوقفت خادمك لتنفيذ المهام السابقة في الطرفية، فيجب تشغيله مرةً أخرى باستخدام الأمر npm start. شيفرة المشروع الأساسية سنقدِّم فيما يلي شيفرة الدالة App() وشيفرة CSS لتنسيق تطبيقك لتستخدمها بدلًا من الشيفرة التي لديك الآن. صيغة JSX انسخ مقتطف الشيفرة التالي إلى مفكرتك، ثم الصقه في الملف App.js بحيث يحل محل دالة App() الحالية: function App(props) { return ( <div className="todoapp stack-large"> <h1>TodoMatic</h1> <form> <h2 className="label-wrapper"> <label htmlFor="new-todo-input" className="label__lg"> What needs to be done? </label> </h2> <input type="text" id="new-todo-input" className="input input__lg" name="text" autoComplete="off" /> <button type="submit" className="btn btn__primary btn__lg"> Add </button> </form> <div className="filters btn-group stack-exception"> <button type="button" className="btn toggle-btn" aria-pressed="true"> <span className="visually-hidden">Show </span> <span>all</span> <span className="visually-hidden"> tasks</span> </button> <button type="button" className="btn toggle-btn" aria-pressed="false"> <span className="visually-hidden">Show </span> <span>Active</span> <span className="visually-hidden"> tasks</span> </button> <button type="button" className="btn toggle-btn" aria-pressed="false"> <span className="visually-hidden">Show </span> <span>Completed</span> <span className="visually-hidden"> tasks</span> </button> </div> <h2 id="list-heading"> 3 tasks remaining </h2> <ul role="list" className="todo-list stack-large stack-exception" aria-labelledby="list-heading" > <li className="todo stack-small"> <div className="c-cb"> <input id="todo-0" type="checkbox" defaultChecked={true} /> <label className="todo-label" htmlFor="todo-0"> Eat </label> </div> <div className="btn-group"> <button type="button" className="btn"> Edit <span className="visually-hidden">Eat</span> </button> <button type="button" className="btn btn__danger"> Delete <span className="visually-hidden">Eat</span> </button> </div> </li> <li className="todo stack-small"> <div className="c-cb"> <input id="todo-1" type="checkbox" /> <label className="todo-label" htmlFor="todo-1"> Sleep </label> </div> <div className="btn-group"> <button type="button" className="btn"> Edit <span className="visually-hidden">Sleep</span> </button> <button type="button" className="btn btn__danger"> Delete <span className="visually-hidden">Sleep</span> </button> </div> </li> <li className="todo stack-small"> <div className="c-cb"> <input id="todo-2" type="checkbox" /> <label className="todo-label" htmlFor="todo-2"> Repeat </label> </div> <div className="btn-group"> <button type="button" className="btn"> Edit <span className="visually-hidden">Repeat</span> </button> <button type="button" className="btn btn__danger"> Delete <span className="visually-hidden">Repeat</span> </button> </div> </li> </ul> </div> ); } افتح الآن الملف public/index.html وعدّل نص العنصر <title> ليصبح TodoMatic، بحيث يطابق العنصر <h1> الموجود أعلى تطبيقنا. <title>TodoMatic</title> يجب أن ترى ما يلي عند تحديث متصفحك: لا يبدو هذا التطبيق جميلًا ولا يعمل بعد، لكنه جيد حاليًا، وضَع في بالك شيفرة JSX، وكيفية توافقها مع قصص المستخدِمين: لدينا عنصر <form> مع العنصر <input type="text"> لكتابة مهمة جديدة، وزر لإرسال النموذج. لدينا مجموعة من الأزرار التي سنستخدِمها لمهامنا. لدينا عنوان heading يخبرنا عن عدد المهام المتبقية. لدينا ثلاث مهام مرتبة ضمن قائمة غير مرتبة، إذ تُعَدّ كل مهمة أنها عنصر قائمة <li>، كما تحتوي على أزرار لتعديلها وحذفها، بالإضافة إلى مربع اختيار لإيقاف تشغيلها. سيسمح النموذج بإنشاء المهام، إذ تسمح الأزرار بترشيح المهام، ويُعَدّ العنوان والقائمة طريقةً لقراءتها، إذ ليست واجهة المستخدِم المُستخدَمة لتعديل مهمة موجودةً في الوقت الحالي، ولكن سنكتبها لاحقًا. ميزات الشمولية قد تلاحظ بعض السمات غير العادية هنا مثل: <button type="button" className="btn toggle-btn" aria-pressed="true"> <span className="visually-hidden">Show </span> <span>all</span> <span className="visually-hidden"> tasks</span> </button> تخبر السمة aria-pressed التقنيات المساعدة مثل قارئات الشاشة أنه قد يكون الزر في إحدى حالتين وهما مضغوط pressed أو غير مضغوط unpressed، إذ تمثِّلان حالة التشغيل on والإيقاف off، ويعني تعيين القيمة true أنّ الزر مضغوط افتراضيًا. ليس للصنف visually-hidden أيَّ تأثير حتى الآن، لأننا لم نضمِّن شيفرة CSS، فإذا وضعنا التنسيقات في مكانها الصحيح، فسيُخفَى أيّ عنصر في هذا الصنف عن المستخدِمين المبصرين، وسيظل متاحًا لمستخدِمي قارئ الشاشة، لأن هذه الكلمات لا يحتاجها المستخدِمون المبصرون، إذ تُستخدَم لتقديم المزيد من المعلومات حول ما يفعله الزر لمستخدِمي قارئ الشاشة الذين ليس لديهم القدرة البصرية الإضافية لمساعدتهم، كما يمكنك العثور على العنصر <ul> التالي: <ul role="list" className="todo-list stack-large stack-exception" aria-labelledby="list-heading" > تساعد السمة role التقنيات المساعِدة في توضيح نوع العنصر الذي يمثِّله الوسم، إذ يجري التعامل مع العنصر <ul> بوصفه قائمةً افتراضيًا، ولكن ستؤدي التنسيقات التي نضيفها إلى تعطيل هذه الوظيفة، في حين ستؤدي السمة role إلى استعادة القائمة التي تعني العنصر <ul>. تخبر السمة aria-labelledby التقنيات المساعِدة بتعاملنا مع عنوان قائمتنا بوصفه العنوان الذي يصف الغرض من القائمة الموجودة تحته، مما يساعد مستخدِمي قارئ الشاشة على فهم الغرض منها بصورة أفضل، وأخيرًا، فتملك عناصر label وinput في عناصر القائمة بعض السمات الفريدة الخاصة بصيغة JSX، وهي: <input id="todo-0" type="checkbox" defaultChecked={true} /> <label className="todo-label" htmlFor="todo-0"> Eat </label> السمة defaultChecked في الوسم <input /> تخبر React بتحديد مربع الاختيار مبدئيًا، فإذا أردنا استخدام السمة checked كما نفعل في HTML، فستعرِض React بعض التحذيرات المتعلقة بمعالجة أحداث مربع الاختيار في طرفية متصفحك (نافذة console)، والتي يجب تجنبها، ولا تقلق كثيرًا بشأن ذلك في الوقت الحالي، إذ سنغطي ذلك لاحقًا عندما نبدأ باستخدام الأحداث. تتوافق السمة htmlFor مع السمة for المُستخدَمة في لغة HTML، ولا يمكننا استخدام الكلمة for بوصفها سمةً في صيغة JSX لأنها كلمة محجوزة، لذلك تستخدِم React السمة htmlFor بدلًا من ذلك. ملاحظتان: يمكنك استخدام القيم المنطقية -أي true وfalse- في سمات JSX من خلال إحاطة هذه القيم بأقواس معقوصة، فإذا كتبت السمة defaultChecked="true" مثلًا، فستكون "true" هي قيمة السمة defaultChecked، والتي تُعَدّ سلسلةً حرفيةً String Literal، لأنها لغة جافاسكربت وليست لغة HTML. تملك السمة aria-pressed القيمة "true" في مثالنا لأنّ aria-pressed ليست سمةً منطقيةً حقيقيةً بالطريقة التي تستخدِمها السمة checked بها. تنفيذ التنسيقات الصق شيفرة CSS التالية في الملف src/index.css لتحُل محل ما هو موجود حاليًا: /* إعادة الضبط */ *, *::before, *::after { box-sizing: border-box; } *:focus { outline: 3px dashed #228bec; outline-offset: 0; } html { font: 62.5% / 1.15 sans-serif; } h1, h2 { margin-bottom: 0; } ul { list-style: none; padding: 0; } button { border: none; margin: 0; padding: 0; width: auto; overflow: visible; background: transparent; color: inherit; font: inherit; line-height: normal; -webkit-font-smoothing: inherit; -moz-osx-font-smoothing: inherit; -webkit-appearance: none; } button::-moz-focus-inner { border: 0; } button, input, optgroup, select, textarea { font-family: inherit; font-size: 100%; line-height: 1.15; margin: 0; } button, input { overflow: visible; } input[type="text"] { border-radius: 0; } body { width: 100%; max-width: 68rem; margin: 0 auto; font: 1.6rem/1.25 Arial, sans-serif; background-color: #f5f5f5; color: #4d4d4d; } @media screen and (min-width: 620px) { body { font-size: 1.9rem; line-height: 1.31579; } } /* نهاية إعادة الضبط */ /* التنسيقات العامة */ .form-group > input[type="text"] { display: inline-block; margin-top: 0.4rem; } .btn { padding: 0.8rem 1rem 0.7rem; border: 0.2rem solid #4d4d4d; cursor: pointer; text-transform: capitalize; } .btn.toggle-btn { border-width: 1px; border-color: #d3d3d3; } .btn.toggle-btn[aria-pressed="true"] { text-decoration: underline; border-color: #4d4d4d; } .btn__danger { color: #fff; background-color: #ca3c3c; border-color: #bd2130; } .btn__filter { border-color: lightgrey; } .btn__primary { color: #fff; background-color: #000; } .btn-group { display: flex; justify-content: space-between; } .btn-group > * { flex: 1 1 49%; } .btn-group > * + * { margin-left: 0.8rem; } .label-wrapper { margin: 0; flex: 0 0 100%; text-align: center; } .visually-hidden { position: absolute !important; height: 1px; width: 1px; overflow: hidden; clip: rect(1px 1px 1px 1px); clip: rect(1px, 1px, 1px, 1px); white-space: nowrap; } [class*="stack"] > * { margin-top: 0; margin-bottom: 0; } .stack-small > * + * { margin-top: 1.25rem; } .stack-large > * + * { margin-top: 2.5rem; } @media screen and (min-width: 550px) { .stack-small > * + * { margin-top: 1.4rem; } .stack-large > * + * { margin-top: 2.8rem; } } .stack-exception { margin-top: 1.2rem; } /* نهاية التنسيقات العامة */ .todoapp { background: #fff; margin: 2rem 0 4rem 0; padding: 1rem; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1); } @media screen and (min-width: 550px) { .todoapp { padding: 4rem; } } .todoapp > * { max-width: 50rem; margin-left: auto; margin-right: auto; } .todoapp > form { max-width: 100%; } .todoapp > h1 { display: block; max-width: 100%; text-align: center; margin: 0; margin-bottom: 1rem; } .label__lg { line-height: 1.01567; font-weight: 300; padding: 0.8rem; margin-bottom: 1rem; text-align: center; } .input__lg { padding: 2rem; border: 2px solid #000; } .input__lg:focus { border-color: #4d4d4d; box-shadow: inset 0 0 0 2px; } [class*="__lg"] { display: inline-block; width: 100%; font-size: 1.9rem; } [class*="__lg"]:not(:last-child) { margin-bottom: 1rem; } @media screen and (min-width: 620px) { [class*="__lg"] { font-size: 2.4rem; } } .filters { width: 100%; margin: unset auto; } /* تنسيقات عناصر Todo */ .todo { display: flex; flex-direction: row; flex-wrap: wrap; } .todo > * { flex: 0 0 100%; } .todo-text { width: 100%; min-height: 4.4rem; padding: 0.4rem 0.8rem; border: 2px solid #565656; } .todo-text:focus { box-shadow: inset 0 0 0 2px; } /* تنسيقات مربعات الاختيار */ .c-cb { box-sizing: border-box; font-family: Arial, sans-serif; -webkit-font-smoothing: antialiased; font-weight: 400; font-size: 1.6rem; line-height: 1.25; display: block; position: relative; min-height: 44px; padding-left: 40px; clear: left; } .c-cb > label::before, .c-cb > input[type="checkbox"] { box-sizing: border-box; top: -2px; left: -2px; width: 44px; height: 44px; } .c-cb > input[type="checkbox"] { -webkit-font-smoothing: antialiased; cursor: pointer; position: absolute; z-index: 1; margin: 0; opacity: 0; } .c-cb > label { font-size: inherit; font-family: inherit; line-height: inherit; display: inline-block; margin-bottom: 0; padding: 8px 15px 5px; cursor: pointer; touch-action: manipulation; } .c-cb > label::before { content: ""; position: absolute; border: 2px solid currentColor; background: transparent; } .c-cb > input[type="checkbox"]:focus + label::before { border-width: 4px; outline: 3px dashed #228bec; } .c-cb > label::after { box-sizing: content-box; content: ""; position: absolute; top: 11px; left: 9px; width: 18px; height: 7px; transform: rotate(-45deg); border: solid; border-width: 0 0 5px 5px; border-top-color: transparent; opacity: 0; background: transparent; } .c-cb > input[type="checkbox"]:checked + label::after { opacity: 1; } احفظ الملف وألقِ نظرةً على المتصفح، إذ يجب أن يتمتع تطبيقك الآن بتنسيق مقبول. الخلاصة يبدو تطبيق قائمة المهام الآن أشبه بتطبيق حقيقي، ولكنه لا يفعل أيّ شيء فعليًا، إذ سنبدأ في إصلاح ذلك لاحقًا في المقالات القادمة. ترجمة -وبتصرُّف- للمقال Beginning our React todo list. اقرأ أيضًا أساسيات بناء تطبيقات الويب مكونات React الأساسية (React Components) إنشاء تطبيق قائمة مهام بسيط باستخدام Laravel 5
-
سنلقي في هذا المقال نظرةً على مكتبة React، إذ سنطّلع على بعض التفاصيل حول خلفيتها وحالات استخدامها، وسننشئ سلسلة أدوات React الأساسية وتطبيقًا بسيطًا بحيث نتعلم كيفية عمل React. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية، إذ تستخدِم React صيغة لغة HTML ضمن جافاسكربت HTML-in-JavaScript، والتي تسمى JSX، أي JavaScript وXML، كما سيساعدك التعرف على كل من لغة HTML وجافاسكربت على تعلّم صيغة JSX، وتحديد ما إذا كانت الأخطاء في تطبيقك مرتبطةً بجافاسكربت أو بمجال أكثر تحديدًا من React. الهدف: إعداد بيئة تطوير React المحلية، وإنشاء تطبيق بسيط، وفهم أساسيات عمله. تُعَدّ React مكتبةً لبناء واجهات المستخدِم، ولا تُعَدّ إطار عمل، فهي ليست حصريةً للويب، كما تُستخدَم مكتبة React مع المكتبات الأخرى للتصيير Render إلى بيئات معينة، إذ يمكن استخدام إطار عمل React Native لبناء تطبيقات الهاتف المحمول، لكن يستخدِم المطورون مكتبة React جنبًا إلى جنب مع ReactDOM للبناء للويب، إذ تُستخدَم React و ReactDOM في المجالات نفسها ولحل المشكلات نفسها التي تستخدِمها أطر تطوير الويب الحقيقية الأخرى، لذلك نشير إلى React بوصفها إطار عمل Framework. تهدف React إلى تقليل الأخطاء التي تحدث عندما يبني المطورون واجهات المستخدِم من خلال استخدام المكوّنات Components، والتي تُعَدّ أجزاءً من الشيفرة البرمجية المنطقية والمستقلة ذاتيًا والتي تصف جزءًا من واجهة المستخدِم، إذ يمكن تكوين هذه المكونات مع بعضها البعض لإنشاء واجهة مستخدِم كاملة، كما تجرِّّد React كثيرًا من أعمال التصيير، وبالتالي تجعلك تركِّز على تصميم واجهة المستخدِم. حالات الاستخدام Use cases لا تفرض React قواعد صارمةً حول اصطلاحات الشيفرة أو تنظيم الملفات خلاف أطر العمل Frameworks الأخرى، مما يتيح لفرق العمل تحديد الاصطلاحات التي تناسبها بصورة أفضل، واستخدام مكتبة React بالطريقة التي ترغب بها، إذ يمكن لمكتبة React معالجة زر واحد أو أجزاء من الواجهة أو واجهة المستخدِم للتطبيق بأكمله، فإذا أردت استخدام React لأجزاء صغيرة من الواجهة، فلا يُعَدّ ذلك سهلًا مثل بناء تطبيق باستخدام مكتبة مثل jQuery أو إطار عمل مثل Vue، إذ يكون استخدام مكتبة React أسهل عند إنشاء تطبيقك بالكامل باستخدامها. كما تتطلب العديد من مزايا تجربة المطوِّر لتطبيق React مثل كتابة الواجهات باستخدام صيغة JSX، عملية تصريف Compilation، في حين تبطّئ إضافة مصرِّف مثل Babel إلى موقع ويب الشيفرة الموجودة عليه، لذلك يُعِدّ المطورون مثل هذه الأدوات باستخدام خطوة بناء، إذ يمكن القول أنّ React لها متطلبات أدوات كثيرة، ولكن يمكن تعلّمها، كما سيركِّز هذا المقال على حالة استخدام React لتصيير واجهة المستخدِم بالكامل لتطبيق ما باستخدام الأدوات التي توفرها أداة create-react-app الخاصة بفيسبوك. كيفية استخدام React للغة جافاسكربت تستخدِم React ميزات لغة جافاسكربت الحديثة للعديد من أنماطها، ولكن يأتي أكبر تحوّل لها عن جافاسكربت عند استخدام صيغة JSX التي توسِّع صيغة جافاسكربت، بحيث يمكن أن تكون الشيفرة البرمجية التي تشبه HTML جنبًا إلى جنب معها، وإليك المثال التالي: const heading = <h1>Mozilla Developer Network</h1>; يُعرَف الثابت heading السابق بتعبير JSX، ويمكن لمكتبة React استخدامه لتصيير الوسم <h1> في التطبيق، ولنفترض أننا أردنا تغليف العنوان heading بوسم <header> لأسباب دلالية، إذ تتيح صيغة JSX بتداخل العناصر ضمن بعضها بعضًا كما نفعل مع لغة HTML كما يلي: const header = ( <header> <h1>Mozilla Developer Network</h1> </header> ); ملاحظة: لا تُعَدّ الأقواس في المقتطف السابق خاصةً بصيغة JSX، وليس لها أيّ تأثير على تطبيقك، وإنما تُعَدّ إشارةً لك ولحاسوبك بأن الأسطر المتعددة من الشيفرة البرمجية الموجودة ضمنها هي جزء من التعبير نفسه، كما يمكنك كتابة تعبير header كما يلي: const header = <header> <h1>Mozilla Developer Network</h1> </header> لا يمكن لمتصفحك قراءة صيغة JSX بدون مساعدة، إذ سيبدو التعبير header كما يلي عند تصريفه باستخدام أداة Babel أو Parcel: const header = React.createElement("header", null, React.createElement("h1", null, "Mozilla Developer Network") ); يمكن تخطي خطوة التصريف واستخدام التابع React.createElement() لكتابة واجهة المستخدِم بنفسك، ولكنك تفقد بذلك ميزة JSX التصريحية، وتصبح قراءة شيفرتك أصعب، إذ يُعَدّ التصريف خطوةً إضافيةً في عملية التطوير، في حين يعتقد العديد من المطورين في مجتمع React أنّ قابلية قراءة JSX تستحق العناء، كما تجعل الأدوات الشائعة تصريف صيغة JSX إلى جافاسكربت جزءًا من عملية الإعداد، ولا يتعين عليك إعداد التصريف بنفسك إلّا إذا أردت ذلك. تُعَدّ صيغة JSX مزيجًا من لغتَي HTML وجافاسكربت، لذلك يجدها بعض المطورين سهلة التعلم، ويجدها آخرون مربكةً بسبب طبيعتها الممزوجة، ولكنها ستسمح لك ببناء واجهات مستخدِم بسرعة وبسهولة إذا أتقنتها، كما ستسمح للآخرين بفهم قاعدة شيفرتك البرمجية فهمًا أفضل وبسرعة، كما يمكنك الاطلاع على صفحة شرح JSX بالتفصيل من توثيق React في موسوعة حسوب لقراءة المزيد عن JSX. إعداد تطبيق React الأول هناك العديد من الطرق لاستخدام React، لكننا سنستخدِم create-react-app وهي أداة واجهة سطر الأوامر -أو CLI اختصارًا-، إذ تسرّع هذه الأداة عملية تطوير تطبيق React عن طريق تثبيت بعض الحزم وإنشاء بعض الملفات، والتعامل مع الأدوات الموضَّحة سابقًا، كما يمكن إضافة React إلى موقع ويب دون استخدام الأداة create-react-app عن طريق نسخ بعض عناصر <script> في ملف HTML، ولكن تُعَدّ الأداة create-react-app نقطة بداية شائعة لتطبيقات React، إذ سيسمح لك استخدامها بقضاء المزيد من الوقت في بناء تطبيقك ووقت أقل في التفكير في الإعداد. المتطلبات يجب تثبيت Node.js من أجل استخدام create-react-app، كما يوصى باستخدام إصدار الدعم طويل الأمد Long-term Support -أو LTS اختصارًا-، إذ يتضمن Node مدير الحزم npm ومشغّل الحزم npx، كما يمكنك استخدام مدير الحزم Yarn، لكننا سنفترض أنك تستخدِم npm في هذا المقال، وهنا يمكنك الاطلاع على مقال أساسيات إدارة الحزم لمزيد من المعلومات حول npm وYarn. إذا استخدمت نظام ويندوز Windows، فستحتاج إلى تثبيت بعض البرامج التي تمنحك التكافؤ مع طرفية نظام يونيكس Unix أو نظام ماك macOS لاستخدام أوامر الطرفية التي سنستخدِمها، إذ يُعَدّ كل من Gitbash الذي يكون جزءًا من مجموعة أدوات git لنظام ويندوز أو نظام ويندوز الفرعي للينكس Windows Subsystem for Linux -أو WSL اختصارًا- مناسبَين، كما يمكنك الاطلاع على دليل استخدام سطر الأوامر للحصول على مزيد من المعلومات حول هذه الأوامر وحول أوامر الطرفية بصفة عامة. ضع في بالك أنّ React و ReactDOM ينتجان تطبيقات تعمل فقط على مجموعة حديثة إلى حد ما من المتصفحات مثل IE9+ باستخدام تعويض نقص دعم المتصفحات Polyfill، كما يوصَى باستخدام متصفح حديث مثل فايرفوكس Firefox أو مايكروسوفت إيدج Microsoft Edge أو سفاري Safari أو كروم Chrome. تهيئة التطبيق تأخذ الأداة create-react-app وسيطًا واحدًا هو الاسم الذي ترغب في منحه لتطبيقك، وتستخدِمه لإنشاء مجلد جديد، ثم تنشئ الملفات الضرورية بداخله، وتأكد من تطبيق الأمر cd على المكان الذي تريد أن يكون فيه تطبيقك على القرص الصلب، ثم شغّل الأمر التالي في الطرفية: npx create-react-app moz-todo-react يؤدي تشغيل الأمر السابق إلى إنشاء المجلد moz-todo-response، مع تنفيذ الأمور التالية ضمنه: تثبيت بعض حزم npm الأساسية لعمل التطبيق. كتابة سكربتات لبدء التطبيق وتنفيذه. إنشاء بنية من الملفات والمجلدات التي تحدِّد معمارية التطبيق الأساسية. تهيئة المجلد بوصفه مستودع جيت git إذا كان جيت مثبتًا على حاسوبك. ملاحظة: إذا كان مدير الحزم yarn مثبتًا لديك، فستُستخدَم أداة create-react-app افتراضيًا لاستخدام yarn بدلًا من npm، وإذا كان كل من مديرَي الحزم مثبَّتَين لديك وتريد استخدام npm صراحةً، فيمكنك إضافة الراية --use-npm عند تشغيل create-react-app: npx create-react-app moz-todo-react --use-npm ستعرِض create-react-app عددًا من الرسائل في الطرفية أثناء عملها، وهذا أمر طبيعي، إذ يمكن أن يستغرق ذلك بضع دقائق، فالوقت مناسب الآن لتحضير كوب من الشاي. غيّر المسار الحالي إلى المجلد moz-todo-react باستخدام الأمر cd عند اكتمال العملية، ثم شغّل الأمر npm start، إذ سيبدأ تشغيل السكربتات المُثبَّتة باستخدام الأداة create-react-app على خادم محلي على المضيف المحلي localhost الذي هو 3000، وافتح التطبيق في تبويب جديد من المتصفح، إذ سيعرِض متصفحك ما يلي: بنية التطبيق تمنحنا أداة create-react-app كل ما نحتاجه لتطوير تطبيق React، إذ تبدو بنية الملفات الأولية الخاصة به كما يلي: moz-todo-react ├── README.md ├── node_modules ├── package.json ├── package-lock.json ├── .gitignore ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg ├── reportWebVitals.js └── setupTests.js يُعَدّ المجلد src بأنه المكان الذي سنقضي فيه معظم وقتنا، فهو مكان وجود شيفرة تطبيقنا البرمجية، كما يحتوي المجلد public على ملفات سيقرأها متصفحك أثناء تطوير التطبيق وأهمها index.html، إذ تحقن React شيفرتك البرمجية في هذا الملف ليتمكّن متصفحك من تشغيلها، وهناك بعض الوسوم الأخرى التي تساعد الأداة create-react-app في عملها، لذا احرص على عدم تعديلها إلا إذا كنت متأكدًا مما تفعله، ولكن يجب عليك تغيير النص الموجود داخل العنصر <title> في هذا الملف ليعكس عنوان تطبيقك، وعناوين الصفحات الدقيقة مهمة من أجل إمكانية الوصول. سيُنشَر أيضًا المجلد public عند إنشاء ونشر إصدار الإنتاج من تطبيقك، إذ لن نغطّي مرحلة النشر في هذا المقال، ولكن يجب أن تكون قادرًا على استخدام حل مشابه لذلك الموضَّح في مقال نشر التطبيق، في حين يحتوي الملف package.json على معلومات حول مشروعنا، والتي يستخدِمها كل من Node.js وnpm لإبقائه منظمًّا، كما لا يُعَدّ هذا الملف خاصًا بتطبيقات React، ولا تحتاج إلى فهم هذا الملف على الإطلاق لإكمال هذا المقال، ولكن إذا أردتَ معرفة المزيد عنه، فيمكنك قراءة مقال أساسيات إدارة الحزم. استكشاف مكون React الأول يُعَدّ المكوّن Component في React وحدةً قابلةً لإعادة الاستخدام والتي تصيّر جزءًا من التطبيق، كما يمكن أن تكون هذه الأجزاء كبيرةً أو صغيرةً، لكنها تكون عادةً محددةً بوضوح، فهي تخدم غرضًا واحدًا واضحًا، ولنفتح الملف src/App.js، لأنّ متصفحنا يطالبنا بتعديله، إذ يحتوي هذا الملف على المكوِّن الأول App وعدد قليل من سطور الشيفرة البرمجية الأخرى: import React from 'react'; import logo from './logo.svg'; import './App.css'; function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> </div> ); } export default App; يتكون ملف App.js من ثلاثة أجزاء رئيسية وهي كما يلي، إذ تتبع معظم مكونات React هذا النمط: بعض تعليمات الاستيراد import في الأعلى. المكوِّن App في المنتصف. تعليمة تصدير export في الأسفل. تعليمات الاستيراد Import تسمح تعليمات الاستيراد الموجودة في أعلى الملف App.js باستخدام الشيفرة المُعرَّفة في مكان آخر، وهذه التعليمات هي: import React from 'react'; import logo from './logo.svg'; import './App.css'; تستورِد التعليمة الأولى مكتبة React التي تحوِّل صيغة JSX التي نكتبها إلى التابع React.createElement()، ويجب على جميع مكونّات React استيراد وحدة React، فإذا تخطيت هذه الخطوة، فسيعطي تطبيقك خطأً، في حين تستورِد التعليمة الثانية صورة شعار Logo من الملف './logo.svg'، ولاحظ استخدام /. في بداية المسار، والامتداد .svg في نهايته، إذ يدل ذلك على أن الملف محلي وأنه ليس ملف جافاسكربت، ويوجد الملف logo.svg في مجلدنا المصدر، ولا نكتب مسارًا أو امتدادًا عند استيراد وحدة React، لأنه لا يُعَدّ ملفًا محليًا، وإنما يُدرَج بوصفه اعتماديةً Dependency في الملف package.json. تستورِد التعليمة الثالثة ملف CSS المتعلق بالمكوّن App، ولاحظ عدم وجود اسم متغير والموجِّه from، إذ لا تُعَدّ هذه الصيغة أصيلةً Native في صيغة وحدة جافاسكربت، وإنما تأتي من أداة Webpack وهي الأداة التي تستخدِمها create-react-app لتجميع جميع ملفات جافاسكربت مع بعضها بعضًا وتقديمها إلى المتصفح. المكون App توجد دالة تسمَّى App بعد تعليمات الاستيراد، إذ يفضِّل مجتمع جافاسكربت استخدام الأسماء بحالة الجَمل Camel-case مثل helloWorld، في حين تستخدِم مكونات React أسماء المتغيرات بحالة باسكال Pascal-case مثل HelloWorld لتوضيح أنّ عنصر JSX المحدَّد هو مكون React وليس وسم HTML عادي، فإذا أردت إعادة تسمية الدالة App لتصبح app، فسيعطي متصفحك خطأً. function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> </div> ); } تعيد الدالة App تعبير JSX الذي يحدِّد ما يصيّره متصفحك على DOM في النهاية، كما تحتوي بعض العناصر في هذا التعبير على سمات Attributes مكتوبةً كما تُكتَب في لغة HTML تمامًا باتباع النمط attribute="value"، في حين يحتوي وسم الفتح <div> على السمة className في السطر الثالث، وهي الخاصية class نفسها في لغة HTML، ولكن لا يمكننا استخدام الكلمة class، لأنّ صيغة JSX هي لغة جافاسكربت وهي كلمة محجوزة فيها، مما يعني أنّ لغة حافاسكربت تستخدِمها مسبقًا لغرض معيَّن، وقد يتسبّب استخدامها في شيفرتنا في حدوث مشاكل، كما تُكتَب بعض سمات HTML الأخرى بطريقة مختلفة في JSX عن تلك الموجودة في لغة HTML للسبب ذاته. عدِّل الوسم <p> في السطر السادس، بحيث يصبح "Hello, world!"، ثم احفظ ملفك، إذ ستلاحظ أن هذا التعديل سيُصيَّر مباشرةً في خادم التطوير الذي يعمل على المضيف المحلي http://localhost:3000 في متصفحك، ثم احذف بعد ذلك الوسم <a> واحفظ الملف، مما يؤدي إلى اختفاء رابط "Learn React"، إذ يجب أن يبدو المكوّن App الآن كما يلي: function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Hello, World! </p> </header> </div> ); } تعليمات التصدير تجعل تعليمة التصدير export default App في الجزء السفلي من الملف App.js المكوّنَ App متاحًا للوحدات الأخرى. الملف index.js لنفتح الملف src/index.js الذي يُعَدّ المكان الذي يُستخدَم فيه المكوّن App، وهو نقطة الدخول إلى تطبيقنا، إذ يبدو في البداية كما يلي: import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); // إذا أردت أن يعمل تطبيقك في وضع عدم الاتصال وأن يُحمَّل بسرعة، فيمكنك تغيير // unregister() إلى register() في الأسفل، ولاحظ أنّ هذا يأتي مع بعض المخاطر. serviceWorker.unregister(); يبدأ الملف index.js باستيراد جميع وحدات JS والملفات الأخرى التي يحتاجها للعمل كما هو الحال مع الملف App.js، ويحتفظ الملف src/index.css بالتنسيقات العامة المطبَّقة على تطبيقنا بالكامل، كما يمكننا رؤية المكوّن App الذي استوردناه، فهو متاح للاستيراد بفضل تعليمة التصدير export في أسفل الملف App.js، في يستدعي السطر السابع الدالة ReactDOM.render() مع وسيطين هما: المكوِّن الذي نريد تصييره، وهو <App /> في هذه الحالة. عنصر DOM الذي نريد تصيير المكوِّن ضمنه، وهو العنصر ذو المعرِّف root في هذه الحالة، فإذا نظرت ضمن الملف public/index.html، فستجد أن هذا العنصر هو <div> ضمن العنصر <body>. وهذا يعني أننا نريد تصيير تطبيق React الخاص بنا مع المكوِّن App بوصفه الجذر أو المكوِّن الأول. ملاحظة: يجب أن تحتوي مكونات React وعناصر HTML على شرطات إغلاق مائلة في صيغة JSX، إذ ستؤدي كتابة المكوّن <App> فقط أو الوسم <img> فقط إلى حدوث خطأ. تُعَدّ عمّال الخدمة Service workers أجزاءً مثيرةً من الشيفرة البرمجية التي تحسّن أداء التطبيق وتسمح لميزات تطبيقات الويب بالعمل في وضع عدم الاتصال، لكننا لن نتحدّث عنها في هذا المقال، إذ يمكنك حذف السطر الخامس ومعظم الشيفرة الموجودة أسفله، وهنا يجب أن يبدو ملف index.js النهائي كما يلي: import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; ReactDOM.render(<App />, document.getElementById('root')); المتغيرات والخاصيات سنستخدِم فيما يلي بعضًا من مهارات جافاسكربت لنتمكّن من تعديل المكوّنات والتعامل مع البيانات في React، إذ سنتحدث عن كيفية استخدام المتغيرات في JSX، وسنشرح الخاصيات Props التي تُعَدّ طريقةً لتمرير البيانات إلى المكوّن الذي يمكن الوصول إليه بعد ذلك باستخدام المتغيرات. المتغيرات في JSX لنركّز على السطر التاسع في الملف App.js: <img src={logo} className="App-logo" alt="logo" /> وُضِعت قيمة السمة src الخاصة بالوسم <img /> ضمن أقواس معقوصة، وهي الطريقة التي تتعرف بها صيغة JSX على المتغيرات، إذ تشير القيمة {logo} إلى استيراد الشعار logo في السطر الثاني من التطبيق، ثم استرداد ملف الشعار وتصييره، ولنحاول إنشاء متغير خاص بنا من خلال إضافة التعليمة const subject = 'React'; قبل تعليمة return في الدالة App، إذ يجب أن يبدو المكوّن App الآن كما يلي: function App() { const subject = "React"; return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Hello, World! </p> </header> </div> ); } غيّر السطر الثامن لاستخدام المتغير subject بدلًا من االكلمة "world" كما يلي: function App() { const subject = "React"; return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Hello, {subject}! </p> </header> </div> ); } يجب أن يعرض المتصفح التعليمة "Hello, React!" بدلًا من التعليمة "Hello, world!" عند الحفظ، ولا يستفيد المتغير الذي ضبطناه للتو استفادةً كبيرةً من ميزات React، لذلك نحتاج إلى استخدام الخاصيات Props. خاصيات المكون الخاصية هي البيانات الممرَّرة إلى مكوِّن React، كما تشبه الخاصيات إلى حد ما سمات HTML، ولكن تمتلك عناصر HTML سمات وتمتلك مكونات React خاصيات، إذ تُكتَب الخاصيات ضمن استدعاءات المكوِّن، وتستخدِم الصيغة نفسها التي تستخدمها سمات HTML وهي prop="value"، كما يكون تدفّق البيانات أحادي الاتجاه في React، إذ يمكن تمرير الخاصيات من المكوّنات الآباء إلى المكوّنات الأبناء فقط، وتكون الخاصيات للقراءة فقط، فلنفتح الملف index.js ونمنح المكوّن <App/> استدعاءه الأول، ثم أضف الخاصية subject إلى استدعاء المكوِّن <App/> مع القيمة Clarice، إذ يجب أن تبدو شيفرتك البرمجية كما يلي: ReactDOM.render(<App subject="Clarice" />, document.getElementById('root')); لنفتح الملف App.js ولننتقل إلى الدالة App() التي يجب أن تكون كما يلي مع اختصار تعليمة return للإيجاز: function App() { const subject = "React"; return ( // تعليمة return ); } عدّل الدالة App بحيث تقبل الخاصيات props على أساس معامِل لها، واحذف الثابت subject، كما يمكنك وضع الخاصيات props في التابع console.log() لطباعتها على طرفية المتصفح كما يلي: function App(props) { console.log(props); return ( // تعليمة return ); } احفظ ملفك وتحقق من طرفية جافاسكربت (نافذة console) في متصفحك، إذ يجب أن ترى شيئًا يشبه ما يلي: Object { subject: "Clarice" } تتوافق خاصية الكائن subject مع الخاصية subject التي أضفناها إلى استدعاء المكون <App />، كما تتوافق سلسلة Clarice النصية مع قيمتها، إذ تُجمَع خاصيات المكوِّن في React دائمًا ضمن كائنات بهذه الطريقة، ولنستخدِم الخاصية subject في الملف App.js، لذا غيّر الثابت subject لقراءة قيمة props.subject بدلًا من تعريفه على أنه سلسلة React، كما يمكنك حذف التابع console.log() إذا أردت ذلك. function App(props) { const subject = props.subject; return ( // تعليمة return ); } يجب أن يعرض التطبيق عبارة "Hello, Clarice!" عند الحفظ، فإذا عدت إلى الملف index.js وعدّلت قيمة subject ثم حفظته، فسيتغيّر النص. الخلاصة تعرّفنا في هذا المقال على مكتبة React، بما في ذلك كيفية تثبيتها محليًا، وإنشاء تطبيق بسيط، وكيفية عمل الأساسيات؛ أما في المقال التالي، فسنبدأ بإنشاء أول تطبيق مناسب وهو تطبيق قائمة المهام، لكن لنلخّص بعض الأشياء التي تعلمناها حتى الآن. في React: يمكن للمكونات استيراد الوحدات التي تحتاجها ويجب أن تصدِّر نفسها في الجزء السفلي من ملفاتها. تُسمَّى دوال المكوِّن باستخدام حالة باسكال PascalCase. يمكنك قراءة متغيرات JSX بوضعها بين أقواس معقوصة مثل {so}. تختلف بعض سمات JSX عن سمات HTML بحيث لا تتعارض مع كلمات جافاسكربت المحجوزة، إذ تُترجَم class في لغة HTML إلى className في JSX مثلًا، ولاحظ أنّ السمات متعددة الكلمات تُسمَّى باستخدام حالة الجَمل camel-cased. تُكتَب الخاصيات تمامًا مثل السمات ضمن استدعاءات المكوِّن وتُمرَّر إلى المكوّنات. ترجمة -وبتصرُّف- للمقال Getting started with React. اقرأ أيضًا مدخل إلى React.js - مكتبة تطوير الواجهات الرسومية من فيس بوك مدخل إلى استعمال المكتبة React-Router اختبار تطبيقات React باستعمال Jest ومكتبة React Testing Library المصطلحات المستخدمة في React
-
يملك كل إطار عمل جافاسكربت رئيسي نهجًا مختلف لتحديث نموذج كائن المستند DOM، ومعالجة أحداث المتصفح، وتوفير تجربة مطوِّر ممتعة، إذ سنستكشف في هذا المقال الميزات الرئيسية لأطر عمل "الأربعة الكبار"، وكيفية عمل هذه الأطر، والاختلافات بينها. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت. الهدف: فهم ميزات شيفرة أطر العمل Frameworks الرئيسية. لغات المجال المحدد Domain-specific Languages سنشغّل جميع أطر العمل التي سنناقشها في هذا المقال باستخدام لغة جافاسكربت، إذ ستتيح جميعها استخدام لغات المجال المحدد Domain-specific Languages -أو DSLs اختصارًا- لبناء التطبيقات، كما تستخدم مكتبة React صيغة JSX لكتابة مكوناتها، في حين يستخدِم إطار عمل Ember لغة Handlebars، كما تعرف هذه اللغات كيفية قراءة متغيرات البيانات على عكس لغة HTML، ويمكن استخدام هذه البيانات لتبسيط عملية كتابة واجهة المستخدِم؛ أما تطبيقات Angular، فتستخدِم لغة TypeScript التي لا تهتم بكتابة واجهات المستخدِم ولكنها لغة مجال محدد، وتختلف كثيرًا عن لغة جافاسكربت الصرفة Vanilla JavaScript. لا يستطيع المتصفح قراءة لغات DSL مباشرةً، لذلك يجب تحويلها إلى لغة جافاسكربت أو HTML أولًا، إذ يُعَدّ التحويل خطوةً إضافيةً في عملية التطوير، ولكن تتضمن أدوات إطار العمل الأدوات المطلوبة لمعالجة هذه الخطوة، أو يمكن تعديلها لتضمين هذه الخطوة، كما يمكن إنشاء تطبيقات إطار عمل دون استخدام هذه اللغات، لكن سيبسّط استخدامها عملية التطوير ويسهّل العثور على المساعدة من مجتمعات تلك الأطر. صيغة JSX يرمز الاختصار JSX إلى لغتي جافاسكربت و XML، ويُعَدّ امتدادًا للغة جافاسكربت، إذ يضيف صيغةً تشبه لغة HTML إلى بيئة جافاسكربت، وقد اخترع فريق React صيغة JSX لاستخدامها في تطبيقات React، ولكن يمكن استخدامها لتطوير تطبيقات أخرى مثل تطبيقات Vue، وإليك مثال بسيط لصيغة JSX: const subject = "World"; const header = ( <header> <h1>Hello, {subject}!</h1> </header> ); يمثِّل التعبير السابق عنصر <header> في لغة HTML وبداخله عنصر <h1>، إذ تخبر الأقواس المعقوصة حول subject في السطر الرابع التطبيق بقراءة قيمة الثابت subject وإدخاله في العنصر <h1>، في حين ستُصرَّف صيغة JSX من جزء الشيفرة السابق عند استخدامها مع إطار عمل React إلى ما يلي: var subject = "World"; var header = React.createElement("header", null, React.createElement("h1", null, "Hello, ", subject, "!") ); سينتج جزء الشيفرة السابق ما يلي في لغة HTML عندما يصيّره المتصفح في النهاية: <header> <h1>Hello, World!</h1> </header> لغة Handlebars لا تُعَدّ لغة القوالب Handlebars لغةً خاصةً بتطبيقات Ember، ولكنها تُستخدَم بكثرة معها، إذ تشبه شيفرة Handlebars لغة HTML، ولكن لديها خيار سحب البيانات من مكان آخر، إذ يمكن استخدام هذه البيانات للتأثير على ملفات HTML التي يبنيها التطبيق في النهاية، كما تستخدِم لغة Handlebars -مثل صيغة JSX- الأقواس المعقوصة لحقن قيمة متغير، ولكنها تستخدِم زوجًا مزدوجًا من الأقواس المعقوصة بدلًا من زوج واحد، وإليك قالب Handlebars التالي: <header> <h1>Hello, {{subject}}!</h1> </header> والبيانات التالية: { subject: "World" } ستبني لغة Handlebars جزء HTML التالي: <header> <h1>Hello, World!</h1> </header> لغة TypeScript تُعَدّ لغة TypeScript مجموعةً شاملةً من جافاسكربت، أي أنها توسّعها، إذ تُعَدّ كل شيفرات جافاسكربت صالحةً للغة TypeScript، ولكن العكس ليس صحيحًا، كما تُعَدّ لغة TypeScript مفيدةً للصرامة التي تسمح للمطورين بفرضها على شيفرتهم البرمجية مثل دالة add() التي تأخذ الأعداد الصحيحة a وb وتعيد ناتج جمعهما، ويمكن كتابة هذه الدالة في لغة جافاسكربت على النحو التالي: function add(a, b) { return a + b; } قد تكون هذه الشيفرة بسيطةً جدًا بالنسبة لشخص اعتاد على استخدام جافاسكربت، ولكنها يمكن أن تكون أوضح، إذ تتيح لنا لغة جافاسكربت استخدام المعامل + لربط السلاسل مع بعضها بعضًا، لذلك ستعمل هذه الدالة أيضًا إذا كان a وb عبارة عن سلاسل نصية Strings، وبالتالي قد لا تمنحك النتيجة التي تتوقعها، فإذا أردنا السماح فقط بتمرير الأعداد إلى هذه الدالة، فستجعل لغة TypeScript ذلك ممكنًا كما يلي: function add(a: number, b: number) { return a + b; } يخبر النوع : number المكتوب بعد كل معامِل في لغة TypeScript أنّ كلا المعامِلَين a وb يجب أن يكونا عددَين، فإذا أردنا استخدام هذه الدالة وتمرير القيمة '2' إليها بوصفها وسيطًا، فستعطي لغة TypeScript خطأً أثناء التصريف Compilation، وبالتالي سنضطر إلى إصلاح هذا الخطأ، كما يمكننا كتابة شيفرة جافاسكربت الخاصة بنا والتي تعطينا هذه الأخطاء، إلا أنها ستجعل شيفرتنا البرمجية أكثر تفصيلًا، إذ يمكن أن يكون السماح للغة TypeScript بمعالجة مثل هذه الفحوصات نيابةً عنا أمرًا منطقيًا. كتابة المكونات تحتوي معظم أطر العمل على نموذج مكونات، إذ يمكن كتابة مكونات React باستخدام صيغة JSX، ومكونات Ember باستخدام لغة Handlebars، ومكونات Angular وVue باستخدام صيغة القوالب التي توسّع لغة HTML قليلًا، كما توفِّر مكونات كل إطار عمل -بغض النظر عن كيفية كتابة المكونات- طريقةً لوصف الخاصيات الخارجية التي قد تحتاجها، والحالة الداخلية التي يجب أن يديرها المكوِّن، والأحداث التي يمكن للمستخدِم أن أن يتفاعل بها مع المكوِّن، كما سنعطي في هذا المقال أمثلةً من مقتطفات شيفرة React والتي ستُكتَب باستخدام صيغة JSX. الخاصيات Properties تُعَدّ الخاصيات Properties -أو props اختصارًا- بيانات خارجية يحتاجها المكوِّن من أجل تصييرها Render، ولنفترض أنك تنشئ موقعًا إلكترونيًا لمجلة على الإنترنت، وتحتاج إلى التأكُّد من أن كل كاتب مساهم يُنسَب له عمله، فيمكنك إنشاء مكوِّن AuthorCredit لكل مقال، إذ يحتاج هذا المكوِّن إلى عرض صورة شخصية للمؤلف وسطر قصير يحتوي على بعض المعلومات عنه، لذلك يحتاج المكوِّن AuthorCredit إلى قبول بعض الخاصيات من أجل معرفة الصورة المراد تصييرها والسطر القصير المطلوب طباعته، إذ يمكن أن يبدو تمثيل React للمكوِّن AuthorCredit كما يلي: function AuthorCredit(props) { return ( <figure> <img src={props.src} alt={props.alt} /> <figcaption>{props.byline}</figcaption> </figure> ); } تمثِّل {props.src} و{props.alt} و{props.byline} المكان الذي ستُدرَج فيه الخاصيات ضمن المكوِّن، إذ يمكن تصيير هذا المكوِّن من خلال كتابة الشيفرة التالية في المكان الذي نريده، والذي سيكون على الأرجح ضمن مكوِّن آخر: <AuthorCredit src="./assets/zelda.png" alt="Portrait of Zelda Schiff" byline="Zelda Schiff is editor-in-chief of the Library Times." /> مما يؤدي في النهاية إلى تصيير عنصر <figure> التالي في المتصفح مع بنيته المحدَّدة في المكوِّن AuthorCredit، ومحتواه المحدَّد في الخاصيات المدرجة في استدعاء المكوِّن AuthorCredit: <figure> <img src="assets/zelda.png" alt="Portrait of Zelda Schiff" > <figcaption> Zelda Schiff is editor-in-chief of the Library Times. </figcaption> </figure> الحالة State يُعَدّ وجود آلية قوية للتعامل مع الحالة مفتاحًا لإطار عمل فعّال، وقد يحتوي كل مكوِّن على بيانات يجب التحكم بحالتها، إذ ستستمر هذه الحالة بطريقة ما طالما أنّ المكوِّن قيد الاستخدام، ويمكن استخدام الحالة مثل الخاصيات للتأثير على كيفية تصيير المكوِّن، ولنفترض مثلًا وجود زر يحسب عدد مرات النقر فوقه، إذ يجب أن يكون هذا المكوِّن مسؤولًا عن تتبّع حالة العد count الخاصة به، ويمكن كتابته كما يلي: function CounterButton() { const [count] = useState(0); return ( <button>Clicked {count} times</button> ); } يُعَدّ useState() خطاف React الذي سيتتبع قيمة بيانات أولية أثناء تحديثها عند إعطائه تلك القيمة، وستُصيَّر الشيفرة بدايةً كما يلي في المتصفح: <button>Clicked 0 times</button> يتتبّع استدعاء الخطاف useState() الحالة count بطريقة قوية عبر التطبيق دون الحاجة إلى كتابة شيفرة لتنفيذ ذلك بنفسك. الأحداث Events تحتاج المكونات إلى طرق للاستجابة لأحداث المتصفح من أجل أن تكون تفاعلية، وبالتالي ستتمكن تطبيقاتنا من الاستجابة للمستخدِمين، إذ يوفّر كل إطار من أطر العمل صيغته الخاصة للاستماع إلى أحداث المتصفح، والتي تشير إلى أسماء أحداث المتصفح الأصيلة المكافِئة، إذ يتطلب الاستماع إلى حدث النقر click خاصيةً خاصةً هي onClick في React، ولنحدّث شيفرة CounterButton السابقة للسماح لها بحساب عدد النقرات كما يلي: function CounterButton() { const [count, setCount] = useState(0); return ( <button onClick={() => setCount(count + 1)}>Clicked {count} times</button> ); } استخدمنا دالة useState() إضافية لإنشاء دالة setCount() خاصة يمكن استدعاؤها لتحديث قيمة count، إذ نستدعي هذه الدالة في السطر الرابع، ونضبط قيمة count على قيمتها الحالية مع إضافة 1 إليها. مكونات التنسيق Styling components يوفِّر كل إطار من أطر العمل طريقةً لتحديد تنسيقات لمكوناتك أو للتطبيق كله، إذ توفِّر جميعها طرقًا متعددةً لتعريف تنسيقات المكوِّن على الرغم من اختلاف نهج كل إطار عن الآخر، ويمكنك تصميم تطبيقات إطار العمل باستخدام Sass أو Less، أو تحويل Transpile ملفات تنسيقات CSS باستخدام PostCSS مع إضافة بعض الوحدات المساعِدة. التعامل مع الاعتماديات توفِّر جميع الأطر الرئيسية آليات للتعامل مع الاعتماديات Dependencies باستخدام مكوِّنات ضمن مكوّنات أخرى وبمستويات هرمية متعددة في بعض الأحيان، وستختلف آليات هذه الإطارات عن بعضها بعضًا، ولكن النتيجة النهائية هي نفسها كما هو الحال مع الميزات الأخرى، كما تميل المكوّنات إلى استيراد مكوّنات في مكوّنات أخرى باستخدام صيغة وحدة جافاسكربت المعيارية أو شيء آخر مشابه. مكونات ضمن مكونات أخرى تتمثَّل إحدى الفوائد الرئيسية لبنية واجهة المستخدِم القائمة على المكوّنات في أنه يمكن تكوين المكوّنات مع بعضها بعضًا، إذ يمكنك استخدام مكونات ضمن مكونات أخرى لبناء تطبيق ويب مثل كتابة وسوم HTML ضمن بعضها بعضًا لإنشاء موقع ويب، كما يتيح لك كل إطار عمل بكتابة مكوّنات تستخدِم وتعتمد على مكوّنات أخرى، كما يمكن استخدام مكوِّن React الذي هو AuthorCredit ضمن المكوِّن Article مثلًا، وهذا يعني حاجة المكوِّن Article إلى استيراد المكوِّن AuthorCredit. import AuthorCredit from "./components/AuthorCredit"; يمكن بعد ذلك استخدام المكوِّن AuthorCredit ضمن المكوِّن Article كما يلي: ... <AuthorCredit /> … حقن الاعتماديات تشتمل التطبيقات الواقعية على بنى مكونات ذات مستويات متعددة من التداخل Nesting في أغلب الأحيان، وقد يحتاج مكوِّن AuthorCredit المتداخل بعمق في العديد من المستويات لسبب ما إلى بيانات من المستوى الجذر لتطبيقنا، ولنفترض تنظيم موقع المجلة الذي نبنيه على النحو التالي: <App> <Home> <Article> <AuthorCredit {/* props */} /> </Article> </Home> </App> يحتوي المكوِّن App على البيانات التي يحتاجها المكوِّن AuthorCredit، كما يمكننا إعادة كتابة المكوِّن Home وArticle لمعرفة كيفية تمرير الخاصيات، ولكن قد يكون ذلك مملًا إذا كان هناك العديد من المستويات بين أصل ووجهة البيانات، كما قد يؤثر ذلك على الأداء، فلا يستخدِم المكوِّنان Home وArticle صورة المؤلف أو السطر القصير الذي يعطي معلومات مختصَرةً عن المؤلف، ولكن إذا أردنا الحصول على هذه المعلومات في المكوِّن AuthorCredit، فينحتاج إلى تغيير المكوِّنَين Home وArticle لإضافتها. تُسمَّى مشكلة تمرير البيانات عبر العديد من طبقات المكوّنات بتمرير الخاصيات Prop Drilling التي لا تُعَدّ مثاليةً للتطبيقات الكبيرة، إذ يمكن التحايل على هذه المشكلة من خلال توفير أطر العمل وظيفة تُعرَف باسم حقن الاعتمادية Dependency Injection، وهي طريقة لإعطاء بيانات معينة مباشرةً إلى المكوّنات التي تحتاجها دون تمريرها عبر المستويات المتداخلة، إذ ينفّذ كل إطار عمل عملية حقن الاعتمادية تحت اسم مختلف وبطريقة مختلفة، ولكن التأثير هو نفسه في النهاية. يسمّي إطار العمل Angular هذه العملية حقن الاعتمادية، في حين يمتلك إطار العمل Vue توابع المكوّنات provide() وinject()؛ أما React، فيحتوي على واجهة برمجة تطبيقات السياق Context API، بينما يشارك إطار عمل Ember الحالة من خلال خدمات. دورة الحياة Life Cycle تُعَدّ دورة حياة المكوّن في سياق إطار العمل مجموعةً من المراحل التي يمر بها المكوِّن من وقت إلحاقه بنموذج DOM ثم تصييره بواسطة المتصفح -والذي يدعى بالتركيب Mounting في أغلب الأحيان- إلى وقت إزالته من نموذج DOM -والذي يطلَق عليه التفكيك Unmounting في أغلب الأحيان، كما يسمّي كل إطار عمل مراحل دورة الحياة هذه بطريقة مختلفة، ولا تمنح جميعها المطورين الوصول إلى المراحل نفسها، كما تتبع جميع الأطر النموذج العام نفسه، إذ تسمح للمطورين بتنفيذ إجراءات معينة عند تركيب Mount المكوِّن، وعند تصييره، وعند تفكيكه Unmount، وفي عدة مراحل بينها. تُعَدّ مرحلة التصيير Render المرحلة الأهم، لأنها تتكرر عندما يتفاعل المستخدِم مع تطبيقك، وتُشغَّل في كل مرة يحتاج فيها المتصفح إلى تصيير شيء جديد، سواءً كانت هذه المعلومات الجديدة إضافةً إلى ما هو موجود في المتصفح أو حذفه أو تعديله، كما يمكنك الاطلاع على هذا الرسم البياني لدورة حياة مكون React الذي يوضِّح هذا المفهوم. تصيير العناصر تتخِذ أطر العمل أساليبًا مختلفةً ولكنها متشابهة لتصيير التطبيقات، وتتعقّب جميعها الإصدار الحالي المُصيَّر من DOM في متصفحك، كما تتخذ كل منها قرارات مختلفةً قليلًا حول كيفية تغيير نموذج DOM مثل إعادة تصيير المكوّنات في تطبيقك، وبما أنّ أطر العمل تتخِذ هذه القرارات نيابةً عنك، فهذا يعني أنك لا تتفاعل مع نموذج DOM بنفسك، كما يُعَدّ هذا التجريد البعيد عن نموذج DOM أكثر تعقيدًا واستهلاكًا للذاكرة من تحديثه بنفسك، ولكن لا يمكن لأطر العمل بدونه السماح لك بالبرمجة بالطريقة التصريحية المعروفة بها. يُعَدّ نموذج DOM الافتراضي Virtual DOM نهجًا يمكن من خلاله تخزين معلومات حول نموذج DOM في متصفحك ضمن ذاكرة جافاسكربت، إذ يحدّث تطبيقك هذه النسخة من DOM، ثم يوازنها مع DOM الحقيقي المصيَّر لمستخدِميك فعليًا لتحديد ما سيُصيَّر، كما ينشئ التطبيق اختلافًا Diff لموازنة الاختلافات بين DOM الافتراضي المُحدَّث و DOM المُصيَّر حاليًا، إذ يُستخدَم هذا الاختلاف لتطبيق التحديثات على نموذج DOM الحقيقي، كما يستخدِم كل من React وVue نموذج DOM الافتراضي، لكنهما لا يطبِّقان المنطق نفسه بالضبط عند تطبيق الاختلاف Diffing أو التصيير Rendering، ويمكنك قراءة المزيد عن DOM الافتراضي في توثيق React على موسوعة حسوب. يشبه نموذج DOM التزايدي Incremental DOM نموذج DOM الافتراضي Virtual DOM في أنه ينشئ اختلافًا في نموذج DOM لتحديد ما سيُصيَّر، إلا أنه يختلف في عدم إنشائه نسخةً كاملةً من DOM في ذاكرة جافاسكربت، وهو يتجاهل أجزاء DOM التي لا تحتاج إلى تغيير، فإطار العمل Angular هو الإطار الوحيد الذي ناقشناه حتى الآن والذي يستخدِم نموذج DOM التزايدي، كما يمكنك قراءة المزيد حول نموذج DOM التزايدي على مدونة Auth0. آلة Glimmer الافتراضية خاصة بإطار عمل Ember، ولا تُعَدّ نموذج DOM افتراضي أو DOM تزايدي، وإنما هي عملية منفصلة يمكن من خلالها تحويل قوالب Ember إلى نوع من شيفرة ثنائية Byte Code تكون أسهل وأسرع في القراءة من جافاسكربت. التوجيه Routing يُعَدّ التوجيه جزءًا مهمًا من تجربة الويب، إذ يوفِّر كل إطار عمل مكتبةً أو أكثر بحيث تساعد المطورين على تنفيذ التوجيه من جانب العميل في تطبيقاتهم، لتجنّب تجربة معطَّلة في التطبيقات المعقدة ذات المشاهدات الكثيرة. الاختبار Testing تستفيد جميع التطبيقات من تغطية الاختبار التي تضمن استمرار برنامجك في التصرف بالطريقة التي تتوقعها، إذ يوفر النظام المجتمعي لكل إطار عمل الأدوات التي تسهّل كتابة الاختبارات، ولا تُضمَّن أدوات الاختبار في الأطر نفسها، ولكن تمنحك أدوات واجهة سطر الأوامر المُستخدَمة لإنشاء تطبيقات إطار العمل، الوصول إلى أدوات الاختبار المناسبة، إذ يحتوي كل إطار على أدوات واسعة النطاق في نظامه المجتمعي مع إمكانات اختبار الوحدة والتكامل على حد سواء. تُعَدّ مكتبة الاختبار Testing Library مجموعةً من أدوات الاختبار المساعِدة التي تحتوي على أدوات للعديد من بيئات جافاسكربت بما في ذلك React وVue وAngular، ويغطي توثيق Ember اختبار تطبيقاته، وإليك اختبار سريع للمكوِن CounterButton مكتوب بمساعَدة مكتبة اختبار React، إذ يختبر هذا الاختبار عددًا من الأشياء مثل وجود الزر وما إذا كان الزر يعرض النص الصحيح بعد النقر عليه 0 و1 و2 مرة: import React from "react"; import { render, fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import CounterButton from "./CounterButton"; it("renders a semantic with an initial state of 0", () => { const { getByRole } = render(<CounterButton />); const btn = getByRole("button"); expect(btn).toBeInTheDocument(); expect(btn).toHaveTextContent("Clicked 0 times"); }); it("Increments the count when clicked", () => { const { getByRole } = render(<CounterButton />); const btn = getByRole("button"); fireEvent.click(btn); expect(btn).toHaveTextContent("Clicked 1 times"); fireEvent.click(btn); expect(btn).toHaveTextContent("Clicked 2 times"); }); الخلاصة يجب أن يكون لديك الآن مزيدًا من الأفكار حول اللغات والميزات والأدوات الفعلية التي ستستخدِمها أثناء إنشاء التطبيقات باستخدام أطر العمل، ولا بدّ أنك متحمس للبدء بكتابة الشيفرة، وهذا ما ستفعله لاحقًا، ولكن يمكنك الآن اختيار إطار العمل الذي ترغب في بدء تعلمه أولًا مثل: React Ember Vue Svelte Angular ترجمة -وبتصرُّف- للمقال Framework main features. اقرأ أيضًا مقدمة إلى أطر عمل تطوير الويب من طرف العميل فهم أدوات تطوير الويب من طرف العميل بناء نموذج كامل لسلسلة أدوات تطوير الويب من طرف العميل
-
سنبدأ مقالنا بإلقاء نظرة على تاريخ لغة جافاسكربت JavaScript وأطر العمل Frameworks، وسبب وجود هذه الأطر وفوائدها، وكيفية اختيار إطار عمل، وما هي البدائل المتاحة لأطر العمل من طرف العميل. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML و CSS و جافاسكربت. الهدف: فهم أطر عمل جافاسكربت من طرف العميل والمشاكل التي تحلها وبدائلها وكيفية اختيارها. لمحة تاريخية ظهرت لغة جافاسكربت لأول مرة في عام 1996 والتي أضافت تفاعلًا إلى الويب الذي احتوى على مستندات ثابتة سابقًا، وبالتالي أصبح الويب مكانًا لفعل أشياء، وليس مجرد مكان لقراءة تلك الأشياء، كما زادت شعبية جافاسكربت، كما كتب المطورون الذين عملوا معها، أدوات لحل المشاكل التي واجهوها، وجمّعوها في حزم قابلة لإعادة الاستخدام سميّت بمكتبات Libraries ليتمكنوا من مشاركة حلولهم مع بعضهم البعض، إذ ساعد هذا النظام المجتمعي المشترك للمكتبات في تشكيل نمو الويب. تُعَدّ لغة جافاسكربت الآن جزءًا أساسيًا من الويب، وتُستخدَم في 95% من مواقع الويب، كما أصبح المستخدِمون يكتبون أوراقًا papers، ويديرون ميزانياتهم، ويستمعون إلى الموسيقى، ويشاهدون الأفلام، ويتواصلون مع بعضهم البعض عن بعد فوريًا من خلال الدردشة النصية أو الصوتية أو المرئية، وبالتالي أتاح الويب تنفيذ أمور كانت ممكنةً سابقًا فقط في التطبيقات الأصيلة المثبَّتة على حواسيبنا، إذ يُشار إلى هذه المواقع الحديثة والمعقَّدة والتفاعلية باسم تطبيقات الويب Web Applications. أدى ظهور أطر عمل جافاسكربت الحديثة إلى تسهيل إنشاء تطبيقات تفاعلية وديناميكية، فإطار العمل Framework هو عبارة عن مكتبة تقدّم آراءً لبناء البرمجيات، إذ تسمح هذه الآراء بإمكانية التنبؤ والتجانس في التطبيق، فالقدرة على التنبؤ تسمح للبرمجيات بالتوسع إلى حجم هائل مع بقائها قابلةً للصيانة، إذ تُعَدّ القدرة على التنبؤ وقابلية الصيانة أمران ضروريان لصحة البرمجيات وطول عمرها. تعمل أطر عمل جافاسكربت على تشغيل الكثير من البرمجيات الرائعة على الويب الحديث، بما في ذلك العديد من مواقع الويب التي يُحتمَل استخدامها كل يوم، إذ تستخدِم صفحة توثيق الويب في MDN مثلًا إطار عمل React/ReactDOM لتشغيل واجهتها الأمامية، وتوجد هناك أطر عمل متعددة، ولكن "الأربعة الكبار" هي: إمبر Ember أُصدِر إطار عمل Ember في ديسمبر كانون الأول عام 2011 على أساس استمرار للعمل الذي بدأ في مشروع SproutCore، ويُعَدّ Ember إطار عمل قديم به عدد مستخدِمين أقل من البدائل الحديثة مثل React وVue، لكنه لا يزال يتمتع بقدر لا بأس به من الشعبية نظرًا لاستقراره ودعم المجتمع وبعض مبادئ البرمجة الذكية. Angular هو إطار عمل لتطبيق ويب مفتوح المصدر بقيادة فريق أنجولار Angular في جوجل ومجتمع من الأفراد والشركات، إذ نتج Angular عن إعادة كتابة AngularJS بالكامل من الفريق نفسه الذي بناه وقد أُصدِر أنجولار رسميًا في 14 سبتمبر أيلول من عام 2016، وهي إطار عمل قائم على المكونات، وتستخدِم قوالب HTML التصريحية Declarative، كما يترجم مصرّف إطار العمل القوالب إلى تعليمات جافاسكربت محسَّنة في وقت البناء وبشفافية عن المطورين، تستخدِم أنجولار لغة TypeScript، وهي مجموعة شاملة من لغة جافاسكربت التي سنلقي نظرةً عليها بمزيد من التفصيل لاحقًا. Vue أصدَر إيفان يو Evan You لأول مرة إطار عمل Vue في عام 2014 بعد العمل والتعلم من مشروع AngularJS الأصلي، إذ يُعَدّ Vue الأصغر بين الأربعة الكبار، لكنه تمتَّع مؤخرًا بشعبية متزايدة، كما يوسّع إطار عمل Vue مثل AngularJS لغة HTML بشيفرته، وهو يعتمد بصورة أساسية على لغة جافاسكربت المعيارية الحديثة. React أصدَرت شركة فيسبوك مكتبة React في عام 2013، إذ استخدِمت React قبل ذلك في حل العديد من مشاكلها داخليًا، ولا تُعَدّ React نفسها إطار عمل، وإنما مكتبةً لتصيير Rendering مكونات واجهة المستخدِم، كما تُستخدَم React جنبًا إلى جنب مع المكتبات الأخرى لإنشاء التطبيقات، إذ تُمكِّن React وReact Native المطورين من إنشاء تطبيقات للهاتف المحمول، في حين تمكِّن React وReactDOM المطورين من إنشاء تطبيقات الويب، وتُعرَف React بوصفها إطار عمل جافاسكربت نظرًا لاستخدام React وReactDOM معًا في كثير من الأحيان، كما توسّع React لغة جافاسكربت بصيغة تشبه لغة HTML، إذ تُعرَف هذه الصيغة باسم JSX. سبب وجود أطر العمل ناقشنا البيئة التي ألهمت إنشاء أطر العمل، ولكننا لم نناقش السبب الحقيقي لحاجة المطورين إلى إنشائها، إذ يتطلب استكشاف الأسباب فحص تحديات تطوير البرمجيات أولًا. ضع في الحسبان نوعًا شائعًا من التطبيقات، وهو مُنشئ قائمة المهام To-do List Creator التي سننفِّذها باستخدام مجموعة متنوعة من أطر العمل لاحقًا، إذ يجب أن يسمح هذا التطبيق للمستخدِمين بتطبيق بعض الأمور مثل عرض قائمة المهام وإضافة مهمة جديدة وحذف مهمة، كما يجب أن ينفّذ ذلك أثناء تتبّع وتحديث بيانات التطبيق الأساسية بصورة موثوقة، إذ تُعرَف هذه البيانات الأساسية بالحالة State في تطوير البرمجيات. هذه الأهداف بسيطة من الناحية النظرية بمعزل عن أهداف الأخرى، إذ يمكننا تكرار البيانات لتصييرها، كما يمكننا إضافة كائن لعمل مهمة جديدة، واستخدام معرِّف Identifier للعثور على مهمة أو تعديلها أو حذفها، إذ يجب أن يسمح التطبيق للمستخدِم بتطبيق كل هذه الأشياء من خلال المتصفح، ولكن قد تبدأ بعض المشاكل في الظهور، إلا أنّ المشكلة الحقيقية هي الحاجة إلى تحديث واجهة المستخدِم المناسبة في كل مرة نغيّر فيها حالة تطبيقنا، إذ يمكننا فحص صعوبة هذه المشكلة من خلال النظر إلى ميزة واحدة فقط من تطبيق قائمة مهام وهي عرض قائمة المهام. تغيرات DOM المطولة يستغرق إنشاء عناصر HTML وتصييرها في المتصفح في الوقت المناسب قدرًا كبيرًا من الشيفرة، ولنفترض أنّ حالتنا هي مصفوفة من الكائنات كما يلي: const state = [ { id: 'todo-0', name: 'Learn some frameworks!' } ] يمكنك التساؤل عن كيفية عرض إحدى هذه المهام للمستخدِم، إذ نريد تمثيل كل مهمة بعنصر قائمة، أي العنصر <li> في لغة HTML ضمن عنصر القائمة غير المرتبة <ul> كما يلي: function buildTodoItemEl(id, name) { const item = document.createElement('li'); const span = document.createElement('span'); const textContent = document.createTextNode(name); span.appendChild(textContent); item.id = id; item.appendChild(span); item.appendChild(buildDeleteButtonEl(id)); return item; } استخدمنا التابع document.createElement() لإنشاء العنصر <li> والعديد من أسطر الشيفرة لإنشاء الخصائص والعناصر الأبناء التي يحتاجها، في حين يشير جزء الشيفرة التالي إلى دالة بناء أخرى هي buildDeleteButtonEl()، والتي تتبع نمطًا مشابهًا للنمط الذي استخدمناه لبناء عنصر القائمة: function buildDeleteButtonEl(id) { const button = document.createElement('button'); const textContent = document.createTextNode('Delete'); button.setAttribute('type', 'button'); button.appendChild(textContent); return button; } لا ينفِّذ هذا الزر أيّ شيء حتى الآن، ولكنه سينفِّذ شيئًا ما لاحقًا عندما نقرِّر تنفيذ ميزة الحذف، كما يمكن أن تقرأ الشيفرة التي ستصيّر العناصر على الصفحة شيئًا كما يلي: function renderTodoList() { const frag = document.createDocumentFragment(); state.tasks.forEach(task => { const item = buildTodoItemEl(task.id, task.name); frag.appendChild(item); }); while (todoListEl.firstChild) { todoListEl.removeChild(todoListEl.firstChild); } todoListEl.appendChild(frag); } لدينا الآن أكثر من ثلاثين سطرًا من الشيفرة المخصَّصة لواجهة المستخدِم فقط -أي إلى خطوة تصيير شيء ما في DOM- دون إضافة أصناف Classes التي يمكننا استخدامها لاحقًا لتصميم عناصر القائمة، كما يتطلب العمل مباشرةً مع نموذج DOM فهم أشياء كثيرة حول كيفية عمله مثل كيفية إنشاء العناصر، وتغيير خصائصها، وكيفية وضع العناصر ضمن بعضها البعض، والحصول عليها على الصفحة، فلا تعالج هذه الشيفرة تفاعلات المستخدِم أو إضافة مهمة أو حذفها، فإذا أضفنا هذه الميزات، فيجب علينا تذكّر تحديث واجهة المستخدِم في الوقت المناسب وبالطريقة الصحيحة. أُنشِئت أطر عمل جافاسكربت لتسهيل هذا النوع من العمل، إذ أُوجِدت لتوفير تجربة مطوِّر أفضل، فهي لا تضيف ميزات جديدة إلى جافاسكربت، وإنما تمنحك وصولًا أسهل لميزاتها لتتمكّن من بناء تطبيقات ويب بطريقة عصرية، فإذا أردت رؤية نماذج شيفرة هذا المقال عمليًا، فيمكنك التحقق من إصدار عامل من التطبيق على CodePen الذي يسمح للمستخدِمين بإضافة مهام جديدة وحذفها. طريقة أخرى لبناء واجهات المستخدم توفّر إطارات عمل جافاسكربت طريقةً لكتابة واجهات المستخدِم بطريقة تصريحية، أي أنها تسمح بكتابة الشيفرة التي توضِّح كيف يجب أن تبدو واجهة المستخدِم، كما يحقّق إطار العمل ذلك ضمن نموذج DOM في الخلفية، وكان فهم منهج جافاسكربت الصرفة Vanilla JavaScript لإنشاء عناصر DOM جديدة بطريقة تكرارية في لمح البصر أمرًا صعبًا، لكن يوضِّح الجزء التالي من الشيفرة الطريقة التي يمكنك من خلالها استخدام إطار عمل Vue لوصف قائمة من المهام: <ul> <li v-for="task in tasks" v-bind:key="task.id"> <span>{{task.name}}</span> <button type="button">Delete</button> </li> </ul> يختصر جزء الشيفرة السابق ما يقرب من اثنين وثلاثين سطرًا من الشيفرة في ستة أسطر، فإذا كانت الأقواس المعقوصة وسمات v- غير مألوفة لك، فلا بأس بذلك، إذ سنتعرّف على الصيغة التي يستخدِمها إطار عمل Vue لاحقًا، كما تشبه الشيفرة السابقة واجهة المستخدِم التي تمثِّلها، في حين لا تشبه شيفرة جافاسكربت الصرفة ذلك. يمكن عدم كتابة دوالنا لبناء واجهة المستخدم بفضل إطار عمل Vue الذي سيتعامل مع ذلك بطريقة مثلى وفعالة، فدورنا الوحيد هو شرحنا لإطار عمل Vue الشكل الذي يجب أن يبدو عليه كل عنصر، كما يمكن للمطورين الذين هم على دراية بإطار عمل Vue الانضمام إلى مشروعنا والعمل بسرعة على ما يجري، فاستعمال إطار العمل Vue -وأي إطار عمل آخر- يحسّن كفاءة الفريق وأعضائه. يمكن تطبيق أشياء مشابهة في لغة جافاسكربت الصرفة، إذ تسهّل سلاسل القالب الحرفية Template literal strings كتابة سلاسل HTML التي تمثِّل الصورة التي سيبدو عليها العنصر الأخير، وقد يكون ذلك فكرةً مفيدةً لشيء بسيط مثل تطبيق قائمة المهام، ولكنه ليس قابلًا للصيانة للتطبيقات الكبيرة التي تتعامل مع آلاف سجلات البيانات، كما يمكن تصيير العديد من العناصر الفريدة في واجهة المستخدِم. فوائد أطر العمل الأخرى لنلقِ نظرةً على بعض المزايا الأخرى التي تمنحنا إياها أطر العمل، كما يمكن تحقيق مزايا الأطر في لغة جافاسكربت الصرفة، ولكن يزيح استخدام إطار العمل عبء ضرورة حل هذه المشاكل بأنفسنا. الأدوات يمتلك كل إطار من أطر العمل مجتمع مطورين كبير ونشط، لذلك يوفِّر النظام المجتمعي لكل إطار الأدوات التي تعمل على تحسين تجربة المطوِّر، إذ تسهّل هذه الأدوات إضافة أشياء مثل الاختبار للتأكد من عمل تطبيقك كما ينبغي، أو تدقيق الصياغة Linting للتأكد من خلو شيفرتك البرمجية من الأخطاء متجانسها من حيث الأسلوب. التجزئة Compartmentalization تشجع معظم الأطر الرئيسية المطورين على تجريد الأجزاء المختلفة من واجهات المستخدِم إلى مكونات Components، إذ تُعَدّ هذه المكونات أجزاءً من شيفرة برمجية قابلة للصيانة وإعادة الاستخدام ويمكنها التواصل مع بعضها بعضًا، كما يمكن وضْع الشيفرة المتعلقة بمكوّن معيّن في ملف واحد أو ملفين محدَّدين، بحيث يعرف المطوِّر بالضبط إلى أين يذهب لإجراء تغييرات على هذا المكوّن، في حين سيتعيّن عليك في تطبيق مكتوب بلغة جافاسكربت الصرفة إنشاء مجموعة اصطلاحات لتحقيق ذلك بطريقة فعالة وقابلة للتوسيع، كما يمكن انتهاء المطاف بالعديد من مطوري جافاسكربت بنشر الشيفرة البرمجية المتعلقة بجزء واحد من واجهة المستخدِم في جميع أنحاء الملف أو في ملف آخر تمامًا. التوجيه Routing يتيح الويب للمستخدمين التنقل من صفحة إلى أخرى، إذ يُعَدّ شبكةً من الوثائق المترابطة، فإذا ضغطتَ على رابط في موقع الويب هذا، فسيتصل متصفحك بخادم ما ويجلب محتوًى جديدًا لعرضه لك، وبالتالي سيتغير عنوان URL في شريط العنوان، ويمكنك حفظ عنوان URL الجديد والعودة إلى الصفحة لاحقًا، أو مشاركته مع الآخرين ليتمكنوا من العثور على الصفحة نفسها بسهولة، كما يتذكر متصفحك سجل التنقل ويسمح لك بالتنقل ذهابًا وإيابًا، وهذا ما يسمى بالتوجيه من طرف الخادم Server Side Routing. لا تجلب تطبيقات الويب الحديثة عادةً ملفات HTML الجديدة لتصييرها، وإنما تحمّل صفحة HTML واحدةً وتحدِّث نموذج DOM ضمنها باستمرار -ويشار إليها باسم تطبيقات الصفحة الواحدة Single Page Apps أو SPAs اختصارًا- دون انتقال المستخدِمين إلى عناوين جديدة على الويب، كما يطلَق عادةً على كل صفحة ويب وهمية Pseudo-Webpage جديدة اسم عرض View دون إجراء أيّ توجيه. إذا كان تطبيق SPA معقدًا بدرجة كافية ويصيِّر عددًا كافيًا من العروض الفريدة، فيجب إدخال وظائف التوجيه في تطبيقك، وقد اعتاد الناس على القدرة على الارتباط بصفحات معينة في التطبيقات، والانتقال ذهابًا وإيابًا في سجل التنقّل وما إلى ذلك، ولكن تتأثر تجربتهم عند تعطل ميزات الويب المعيارية هذه، فإذا تعامل تطبيق عميل مع التوجيه بهذه الطريقة، فإنه يسمى بالتوجيه من طرف العميل Client Side Routing، كما يمكن إنشاء موجّه باستخدام إمكانيات جافاسكربت والمتصفح الأصيلة، لكن الأطر الشائعة والمطوّرة بطريقة نشطة لها مكتبات مرافقة تجعل التوجيه جزءًا أسهل في عملية التطوير. أمور يجب مراعاتها عند استخدام الأطر يفضِّل مطور الويب الفعال استخدام أنسب الأدوات لكل عمل، كما تسهّل أطر عمل جافاسكربت تطوير التطبيقات الأمامية، لكنها ليست حلًا سحريًا لحل جميع المشاكل، إذ سنوضِّح فيما يلي الأمور التي يجب مراعاتها عند استخدام الأطر، وضَع في الحسبان أنك قد لا تحتاج إلى إطار عمل إطلاقًا. معرفة كيفية استخدام الأداة تستغرق الأطر وقتًا للتعلم تمامًا مثل لغة جافاسكربت الصرفة، لذلك تأكد من امتلاكك الوقت لتعلّم ما يكفي من ميزات إطار العمل قبل أن تقرر استخدامه لمشروع ما ليكون مفيدًا لك بدلًا من أن يعيقك، وتأكد من راحة زملائك في الفريق في التعامل معه أيضًا. الهندسة الفائقة Overengineering إذا كان مشروع تطوير الويب ملفًا شخصيًا يتكون من بضع صفحات، وكانت هذه الصفحات ذات قدرة تفاعلية قليلة أو معدومة، فقد لا يكون إطار العمل وجافاسكربت ضروريين إطلاقًا، إذ لا تُعَدّ أطر العمل وحدةً مترابطةً، فبعضها أكثر ملاءمةً للمشاريع الصغيرة من غيرها، إذ كتبت سارة دراسنر Sarah Drasner في مقال لمجلة Smashing Magazine عن كيفية استبدال Vue بـ jQuery بوصفها أداةً لجعل أجزاء صغيرة من صفحة ويب تفاعلية. قاعدة شيفرة أكبر وتجريد أكبر تسمح لك أطر العمل بكتابة المزيد من الشيفرة البرمجية التصريحية -وأحيانًا مقدار أقل من الشيفرة البرمجية- من خلال التعامل مع تفاعلات DOM نيابةً عنك في الخلفية، ويُعَدّ هذا التجريد رائعًا لتجربتك بوصفك مطورًا، ولكنه ليس مجانيًا، إذ يجب على أطر العمل تشغيل شيفرتها البرمجية لترجمة ما تكتبه إلى تغييرات DOM، والتي بدورها تجعل الجزء الأخير من البرنامج أكبر وأكثر تكلفةً. تُعَدّ الشيفرة البرمجية الإضافية أمرًا لا مفر منه، وسيسمح لك إطار العمل الذي يدعم تقنية هز الشجرة Tree-Shaking -أي إزالة أي شيفرة غير مُستخدَمة فعليًا في التطبيق أثناء عملية البناء- بالحفاظ على تطبيقاتك صغيرة، ولكن ذلك لا يزال عاملًا يجب وضعه في الحسبان عند التفكير في أداء تطبيقك، خاصةً على الأجهزة المقيَّدة بالشبكة أو بالتخزين مثل الهواتف المحمولة. لا يؤثِّر تجريد الأطر على شيفرة جافاسكربت فحسب، وإنما يؤثِّر على علاقتك بطبيعة الويب ذاتها، فنتيجة تطبيقك النهائية أو الطبقة التي يتفاعل معها المستخدِمون في النهاية هي HTML بغض النظر عن كيفية بنائك لتطبيق الويب، إذ يمكن أن تجعلك كتابة تطبيقك بالكامل باستخدام لغة جافاسكربت غافلًا عن HTML والغرض من وسومها المختلفة، وتقودك إلى إنتاج مستند HTML غير دلالي Un-semantic ولا يمكن الوصول إليه، إذ يمكن كتابة تطبيق هش يعتمد كليًا على جافاسكربت ولن يعمل بدونه. ليست الأطر مصدر مشاكلنا، إذ يمكن أن يكون أيّ تطبيق هشًا ومتضخمًا ولا يمكن الوصول إليه مع وجود أولويات خاطئة، ولكن يُعَدّ تضخم أطر العمل من أولويات المطورين، فإذا كانت أولوياتك هي إنشاء تطبيق ويب معقَّد، فيمكنك تنفيذ ذلك بسهولة، في حين إذا كانت أولوياتك هي عدم حماية الأداء وإمكانية الوصول، فستزيد أطر العمل من هشاشة تطبيقك وتضخمه وعدم إمكانية الوصول إليه، وقد أدّت أولويات المطور الحديثة التي ضخَّمتها أطر العمل، إلى قلب بنية الويب في مواضع مختلفة، إذ يضع الويب الآن جافاسكربت أولًا وتجربة المستخدِم أخيرًا في أغلب الأحيان بدلًا من إنشاء شبكة مستندات قوية وتعتمد على المحتوى. إمكانية الوصول على شبكة ويب مقادة بأطر العمل تتطلب إمكانية الوصول إلى واجهات المستخدِم بعض التفكير والجهد دائمًا، ويمكن أن تؤدي الأطر إلى تعقيد هذه العملية، إذ يجب استخدام واجهات برمجة تطبيقات إطار عمل متقدمة في أغلب الأحيان للوصول إلى ميزات المتصفح الأصيلة مثل مناطق ARIA الحية أو إدارة التركيز. تخلق تطبيقات إطار العمل في بعض الحالات حواجز وصول غير موجودة في المواقع التقليدية مثل التوجيه من طرف العميل كما ذكرنا سابقًا، في حين تكون للتنقل عبر الويب باستخدام التوجيه التقليدي من طرف الخادم نتائج يمكن التنبؤ بها، إذ يعرف المتصفح كيفية ضبط التركيز على الجزء العلوي من الصفحة وستعلن التقنيات المساعدة عن عنوان الصفحة، إذ تحدث هذه الأمور في كل مرة تنتقل فيها إلى صفحة جديدة. لا يحمّل متصفحك صفحات ويب جديدة باستخدام التوجيه من طرف العميل، لذلك لا يعرف أنه يجب عليه ضبط التركيز تلقائيًا أو الإعلان عن عنوان صفحة جديد، وقد كرّس مؤلفو أطر العمل وقتًا وعملًا هائلَين لكتابة شيفرة جافاسكربت التي تعيد إنشاء هذه الميزات، ولكن لم يطبّق أيّ إطار عمل ذلك بطريقة مثالية، وبذلك يجب عليك التفكير في إمكانية الوصول منذ بداية كل مشروع ويب، وضع في الحسبان أنه من المرجَّح معاناة قواعد الشيفرة المجردة التي تستخدِم الأطر من مشاكل الوصول الرئيسية إذا لم تفعل ذلك. كيفية اختيار إطار العمل يتّخذ كل إطار من أطر العمل التي ناقشناها سابقًا مناهج مختلفةً لتطوير تطبيقات الويب، إذ يتحسّن كل منها أو يتغير بانتظام، ولكل منها إيجابياته وسلبياته، كما يُعَدّ اختيار إطار العمل الصحيح عمليةً تعتمد على الفريق والمشروع، إذ يجب عليك إجراء بحث للكشف عمّا يناسب احتياجاتك، ولكننا حدّدنا بعض الأسئلة التي يمكنك طرحها من أجل البحث في خياراتك بفعالية أكبر، وهي كما يلي: ما المتصفحات التي يدعمها إطار العمل؟ ما اللغات الخاصة بالنطاق التي يستخدمها إطار العمل؟ هل يحتوي الإطار على مجتمع قوي وتوثيق جيد ودعم متاح؟ يوفِّر الجدول الموجود في هذا المقال ملخصًا سريعًا لدعم المتصفح الحالي الذي يقدِّمه كل إطار عمل، بالإضافة إلى لغات المجال المحدَّد Domain-specific Languages -أو DSLs اختصارًا- التي يمكن استخدامها، إذ تُعَدّ لغات المجال المحدَّد لغات برمجة مرتبطةً بمجالات محدَّدة من تطوير البرمجيات، كما تُعَدّ في سياق أطر العمل أنها أنواع من لغات جافاسكربت أو HTML التي تسهّل التطوير باستخدام هذا الإطار. لا يتطلب أيّ إطار من أطر العمل مطوِّرًا لاستخدام لغة DSL معينة، ولكن صُمِّمت جميعها تقريبًا مع وضع لغة DSL محدَّدة في الحسبان، إذ يعني اختيار عدم استخدام لغة DSL المفضلة لإطار العمل أنك ستفقد الميزات التي من شأنها تحسين تجربة المطوِّر، كما يجب عليك التفكير بجدية في مصفوفة الدعم ولغات DSL الخاصة بإطار العمل عند اختيارك لأيّ مشروع جديد، إذ يمكن أن يكون دعم المتصفح غير المتطابق عائقًا أمام المستخدِمين، ويمكن أن يكون دعم لغة DSL غير المناسب عائقًا أمامك وأمام زملائك في الفريق. table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } إطار العمل دعم المتصفح لغة DSL المفضلة لغات DSL المدعومة إطار العمل Angular المتصفح IE9+ لغة TypeScript لغات HTML-based وTypeScript إطار العمل React متصفح IE9+ الحديث مع تعويض نقص دعم المتصفحات Polyfill صيغة JSX صيغة JSX ولغة TypeScript إطار العمل Vue المتصفح IE9+ لغة HTML-based لغات HTML-based وJSX وPug إطار العمل Ember متصفح IE9+ الحديث في إصدار Ember رقم 2.18 لغة Handlebars لغات Handlebars وTypeScript وفي ملاحظة مهمة، تجدر الإشارة إلى أن لغات DSL التي وصفناها بأنها " لغات تستند إلى HTML أو HTML-based" لا تمتلك أسماءً رسميةً، فهي ليست لغات DSL حقيقية، ولكنها لغة HTML غير معيارية، لذا وجب الإشارة إلى هذه النقطة. قد يكون مجتمع إطار العمل المقياس الأصعب في القياس، لأن حجم المجتمع لا يرتبط مباشرةً بالأعداد التي يسهل الوصول إليها، ويمكنك التحقق من عدد نجوم مشروع جيت هاب GitHub أو التنزيلات الأسبوعية من npm للحصول على فكرة عن شعبيته، ولكن أفضل ما يمكنك فعله في بعض الأحيان هو البحث في عدد قليل من المنتديات أو التحدث إلى مطوِّرين آخرين، إذ لا يتعلق الأمر بحجم المجتمع فحسب، وإنما يتعلق بشموليته ومدى جودة التوثيق المتاح. هناك مناقشات كثيرة في جميع أنحاء الويب حول إطار العمل الأفضل، فقد اختارت مؤسسة ويكيميديا Wikimedia مؤخرًا استخدام إطار العمل Vue لواجهتها الأمامية، ونشرت طلبًا للتعليقات Request For Comments -أو RFC اختصارًا- حول اعتماد هذا الإطار، وقد استغرق إريك جاردنر Eric Gardner مؤلف RFC وقتًا لتوضيح احتياجات مشروع ويكيميديا وسبب كون بعض أطر العمل اختيارات جيدة للفريق، إذ يُعَدّ طلب التعليقات هذا مثالًا رائعًا لنوع البحث الذي يجب عليك تطبيقه بنفسك عند التخطيط لاستخدام إطار عمل للواجهة الأمامية. يُعَدّ استبيان حالة جافاسكربت مجموعةً مفيدةً من ملاحظات مطوري جافاسكربت، كما يغطّي العديد من الموضوعات المتعلقة بجافاسكربت بما في ذلك البيانات حول استخدام أطر العمل ورأي المطورين بها، وهناك حاليًا مجموعة من البيانات المتاحة على مدى عدة سنوات، مما يسمح لك بالتعرف على شعبية إطار العمل، كما وازن فريق Vue بين Vue وأطر العمل الشائعة الأخرى، إذ قد يكون هناك بعض التحيز في هذه الموازنة، لكنها تُعَدّ موردًا قيّمًا. بدائل لأطر العمل من طرف العميل إذا أردت البحث عن أدوات لتسريع عملية تطوير الويب، وعلمتَ أنّ مشروعك لن يتطلب شيفرة جافاسكربت مكثفةً من طرف العميل، فيمكنك الوصول إلى أحد الحلول القليلة لبناء الويب مثل: نظام إدارة المحتوى Content Management System. التصيير من طرف الخادم Server-side Rendering. مولّد موقع ساكن Static Site Generator. أنظمة إدارة المحتوى تُعَدّ أنظمة إدارة المحتوى Content Management Systems -أو CMSes اختصارًا- أدوات تسمح للمستخدِم بإنشاء محتوى للويب دون كتابة الشيفرة البرمجية مباشرةً، كما تُعَدً حلًا جيدًا للمشاريع الكبيرة وخاصةً المشاريع التي تتطلب مدخلات من كتّاب المحتوى الذين لديهم قدرةً محدودةً على كتابة شيفرة برمجية، أو للمبرمجين الذين يرغبون في توفير الوقت، إلا أنها تتطلب قدرًا كبيرًا من الوقت لإعدادها. يعني استخدام نظام CMS أنك تتخلى على الأقل عن قدر من التحكم في ناتج موقعك النهائي على الويب، فإذا لم يؤلِّف نظام إدارة المحتوى الذي اخترته محتوًى يمكن الوصول إليه افتراضيًا على سبيل المثال، فسيكون تحسين ذلك أمرًا صعبًا في أغلب الأحيان، وتشمل الأمثلة المستخدَمة حاليًا ووردبريس Wordpress وجوملا Joomla ودروبال Drupal. التصيير من طرف الخادم يُعَدّ التصيير من طرف الخادم Server-side Rendering -أو SSR اختصارًا- بنية تطبيقات تكون مهمة الخادم فيها تصيير تطبيق مؤلف من صفحة واحدة، وهو عكس التصيير من طرف العميل Client-side Rendering، كما يُعَدّ الطريقة الأكثر شيوعًا والأكثر مباشرةً لبناء تطبيق جافاسكربت، إذ يكون التصيير من طرف الخادم أسهل على جهاز العميل، لأنك ترسل ملف HTML المُصيَّر إليه فقط، ولكن يمكن أن يكون إعداده صعبًا بالموازنة مع التطبيق المُصيَّر من طرف العميل. تدعم جميع أطر العمل التي ذكرهانا في هذا المقال التصيير من طرف الخادم والتصيير من طرف العميل، كما يمكنك الاطلاع على Next.js لإطار العمل React وNuxt.js لإطار العمل Vue وFastBoot لإطار العمل Ember وAngular Universal لإطار العمل Angular. مولدات الموقع الساكنة تُعَدّ مولِّدات المواقع الساكنة Static Site Generators برامج تنشئ ديناميكيًا جميع صفحات الويب الخاصة بموقع متعدِّد الصفحات -بما في ذلك شيفرة جافاسكربت أو CSS ذات الصلة، بحيث يمكن نشرها في أماكن متعددة، كما يمكن أن يكون مضيف النشر فرعًا من صفحات جيت هاب أو مثيل Netlify أو أيّ خادم خاص من اختيارك مثلًا، ولهذا النهج مزايا متعددة فيما يتعلق بالأداء، فلا يبني جهاز المستخدِم الخاص بك الصفحة باستخدام جافاسكربت، فهو مكتمل فعليًا، والأمان، إذ تمتلك الصفحات الساكنة عددًا أقل من متجهات الهجوم، كما لا يزال بإمكان هذه المواقع استخدام شيفرة جافاسكربت حيثما يحتاجون إليها، لكنها لا تعتمد عليها، وتستغرق مولّدات المواقع الساكنة وقتًا لتعلّمها مثل أيّ أداة أخرى، كما يمكن أن تكون عائقًا أمام عملية التطوير. يمكن أن تحتوي المواقع الساكنة على صفحات فريدة قليلة أو كثيرة حسبما تريد، كما تمكّنك أطر العمل من كتابة تطبيقات جافاسكربت من طرف العميل بسرعة، وتتيح لك مولّدات المواقع الساكنة طريقةً لإنشاء ملفات HTML بسرعة، كما تسمح مولّدات المواقع الساكنة للمطورين بكتابة المكوّنات التي تحدِّد الأجزاء المشتركة من صفحات الويب الخاصة بك، وتكوين هذه المكونات معًا لإنشاء صفحة نهائية، إذ تسمَّى هذه المكونات ضمن سياق مولّدات الموقع الساكنة قوالبًا Templates، ويمكن أن تكون صفحات الويب التي أنشأها مولِّد المواقع الساكنة موطنًا لتطبيقات إطار العمل إذا أردت صفحةً واحدةً محدَّدةً من موقع الويب المُولَّد بطريقة ساكنة لتشغيل تطبيق React عندما يزوره المستخدِم مثلًا. مولّدات المواقع الساكنة موجودة منذ فترة طويلة، لكنها شهدت بعض التجدّد في تاريخ الويب الحديث، وتتوفر الآن منها مجموعة من الخيارات القوية مثل Hugo وJekyll وEleventy وGatsby، فإذا أردت معرفة المزيد حول مولّدات المواقع الساكنة، فراجع دليل تاتيانا ماك Tatiana Mac للمبتدئين في Eleventy، إذ تشرح في المقال الأول من السلسلة ما هو مولِّد الموقع الساكن، وكيفية ارتباطه بالوسائل الأخرى لنشر محتوى الويب. الخلاصة لم نعلّمك كتابة أيّ شيفرة برمجية حتى الآن، ولكن نأمل أننا قدّمنا لك خلفيةً مفيدةً حول سبب استخدامك لأطر العمل في المقام الأول وكيفية البدء في الاختيار، كما يبحث مقالنا القادم في أنواع محدَّدة من ميزات أطر العمل، وسبب عملها بطريقة معينة. ترجمة -وبتصرُّف- للمقال Introduction to client-side frameworks. اقرأ أيضًا دليل استخدام سطر الأوامر في عملية تطوير الويب من طرف العميل بناء نموذج كامل لسلسلة أدوات تطوير الويب من طرف العميل أساسيات إدارة الحزم في تطوير الويب من طرف العميل
- 1 تعليق
-
- 1
-
سنوضح في هذا المقال مثالًا عمليًا عن خطوات تصميم قاعدة بيانات لجامعة، كما سنضع أمثلةً عمليةً تشرح كيفية إنشاء مخططات ERD، إذ يشرح المثال الأول مثالًا عن مخطط ERD لشركة تصنيع، ويشرح المثال الثاني مثالًا عن مخطط ERD لوكيل سيارات، كما يقدم خطوات لحل تمرين باستخدام لغة SQL وعباراتها. مثال عملي عن تصميم قاعدة بيانات لجامعة فيما يلي متطلبات البيانات لمنتج من أجل دعم تسجيل وتقديم المساعدة لطلاب جامعة تعليم إلكتروني وهمية. تحتاج جامعة تعليم إلكتروني إلى الاحتفاظ بتفاصيل طلابها وموظفيها، والمقررات التي تقدمها وأداء الطلاب الذين يدرسون فيها. تدار الجامعة في أربع مناطق جغرافية (إنجلترا واسكتلندا وويلز وأيرلندا الشمالية). يجب تسجيل معلومات كل طالب في البداية عند التسجيل، ويتضمن ذلك رقم تعريف الطالب الصادر في الوقت والاسم وسنة التسجيل والمنطقة الموجود فيها الطالب. ليس الطالب ملزمًا بالتسجيل في أي مقرر عند التسجيل، فيمكنه التسجيل في مقررٍ ما في وقتٍ لاحق. يجب أن تتضمن المعلومات المسجلة لكل عضو في القسم التعليمي وقسم الإرشاد رقمَ الموظف والاسم والمنطقة التي يوجد بها. قد يعمل كل موظف كمرشد counselor لطالبٍ أو أكثر، وقد يعمل كمدرس tutor لطالبٍ أو أكثر في مقررٍ أو أكثر. قد لا يُخصَّص لأحد الموظفين أي طالب كمدرس أو كمرشد في أي وقتٍ معين. يملك كل طالب مرشدًا واحدًا يخصَّص له عند التسجيل، ويقدّم الدعم للطالب طوال حياته الجامعية. يُخصَّص للطالب مدرسٌ منفصلٌ لكل مقرر سجّل فيه الطالب. يُسمَح للموظف فقط العمل كمرشد أو كمدرّس لطالبٍ مقيم في نفس منطقته. يجب أن يكون لكل مقرر متوفر للدراسة رمز مقرر وعنوان وقيمة من حيث نقاط الائتمان، حيث يكون للمقرر إما 15 نقطة أو 30 نقطة. قد يكون للمقرر حصة quota لعدد الطلاب المسجلين فيه في أي عرض. لا يحتاج المقرر إلى أي طالب مسجل فيه (مثل المقرر الذي كُتِب للتو ثم عُرِض للدراسة). يُقيَّد الطلاب في عدد المقررات التي يمكنهم التسجيل فيها في نفس الوقت، فقد لا يأخذون المقررات في نفس الوقت إذا تجاوز مجموع النقاط المدمَجة للمقررات المسجلين فيها 180 نقطة. قد يكون للمقرر ذي الـ 15 نقطة ما يصل إلى ثلاث وظائف لكل عرض، ويكون للمقرر ذي الـ 30 نقطة ما يصل إلى خمس وظائف لكل عرض. تُسجَّل درجة الوظيفة في أي مقرر كعلامةٍ من 100. قاعدة بيانات الجامعة التالية نموذج بيانات محتمل يصِف مجموعة المتطلبات المذكورة أعلاه. يحتوي النموذج على عدة أجزاء، بدءًا من مخطط ERD ويليه وصفٌ لأنواع الكيانات والقيود والافتراضات. عملية التصميم الخطوة الأولى هي تحديد النوى والتي هي عادة أسماء: الموظفين Staff والمقرر Course والطالب Student والوظيفة Assignment. الخطوة التالية هي توثيق جميع السمات attributes لكل كيان entity. هذا هو المكان الذي تحتاج فيه إلى التأكد من توحيد normalized جميع الجداول توحيدًا صحيحًا. أنشئ مخطط ERD الأولي وراجعه مع المستخدمين. أجرِ تغييرات إن لزم الأمر بعد مراجعة مخطط ERD. تحقق من نموذج ER مع المستخدمين لوضع اللمسات الأخيرة على التصميم. يوضّح الشكل التالي مخطط ERD للجامعة الذي يمثّل نموذج بيانات لنظام سجلات الطلاب والموظفين الكيان Entity Student (StudentID, Name, Registered, Region, StaffNo). Staff (StaffNo, Name, Region): يحتوي هذا الجدول على مدرّسين وغيرهم من الموظفين. Course (CourseCode, Title, Credit, Quota, StaffNo). Enrollment (StudentlD, CourseCode, DateEnrolled, FinalGrade). Assignment (StudentID, CourseCode, AssignmentNo, Grade). القيود Constraints يجوز لأحد الموظفين أن يدرّس أو يرشد الطلاب المتواجدين في نفس منطقتهم فقط. قد لا يسجّل الطلاب في مقررات لا تزيد قيمتها عن أكثر من 180 نقطة في نفس الوقت. للسمة Credit (ضمن المقرر Course) قيمة هي 15 أو 30 نقطة. قد يكون للمقرر الذي له 30 نقطة ما يصل إلى خمس وظائف، بينما يكون للمقرر الذي له 15 نقطة ما يصل إلى ثلاث وظائف. للسمة Grade (ضمن الوظيفة Assignment) قيمة هي علامة من 100. الافتراضات Assumptions يستطيع الطالب أن يسجّل مرة واحدة للمقرر حيث تُسجَّل عمليات التسجيل الحالية فقط. تُقدَّم الوظيفة مرة واحدة فقط. العلاقات Relationships (تشمل عددية العلاقة cardinality) لاحظ في الشكل الآتي أن سجل الطالب مرتبط مع مقررات مُسجَّلة بحد أدنى مقرر واحد إلى مقررات متعددة كحد أقصى. يجب أن يكون لكل تسجيل enrollment طالب صالح. يوضح الشكل الآتي ارتباط سجل الموظفين (المدرّس هنا) بحد أدنى 0 طالب وبطلاب متعددين كحد أقصى. قد يكون لسجل الطالب مدرسٌ tutor أو قد يكون بدون مدرس. يرتبط سجل الموظفين Staff (المدرّس هنا) بعدد لا يقل عن 0 مقرّر كحد أدنى وبمقررات متعددة كحد أقصى. قد يكون المقرر course مرتبطًا بمدرّس instructor أو غير مرتبط بمدرس. يجب توفير المقرر (في عملية التسجيل enrollment) مرة واحدة على الأقل ومرات متعددة كحد أقصى، كما يجب أن يحتوي جدول التسجيل Enrollment على مقرر واحد صالح على الأقل إلى مقررات متعددة كحد أقصى. يمكن أن تحتوي عملية التسجيل على 0 مهمة كحد أدنى أو مهام متعددة كحد أقصى. يجب أن ترتبط الوظيفة assignment بتسجيل واحد على الأقل وبتسجيلٍ واحد كحد أقصى. أمثلة عملية عن إنشاء مخططات ERD سنعرض في هذه الجزئية مثالين عن عملية إنشاء مخططات ERD. التمرين الأول: شركة تصنيع Manufacturer تنتج شركة تصنيع منتجات، وتخزّن معلومات المنتج التالية: اسم المنتج product name ومعرّف المنتج product name والكمية المتوفرة quantity. تتكون هذه المنتجات من مكونات متعددة، ويوفّرموِّردٌ أو أكثر كلَّ مكون. تُحفَظ معلومات المكوّن التالية: معرّف المكون component ID واسمه name ووصف عنه description الموّردون suppliers الذين يوفرونه والمنتجات products التي تستخدم هذا المكوّن (استخدم الشكل الآتي لحل هذا التمرين). أنشِئ مخطط ERD لإظهار كيفية تتبع هذه المعلومات. اعرض أسماء الكيانات entity names والمفاتيح الرئيسية primary keys وسمات attributes كل كيان والعلاقات بين الكيانات وعددية العلاقة cardinality. الافتراضات Assumptions يمكن وجود الموّرد دون أن يوفّر مكونات. ليس واجبًا أن يرتبط مكونٌ بموّرد. ليس واجبًا أن يرتبط مكوّنٌ مع منتج، فليست جميع المكونات مستخدمَةً في المنتجات. لا يمكن أن يوجد منتج بدون مكونات. جواب مخطط ERD Component(CompID, CompName, Description) PK=CompID. Product(ProdID, ProdName, QtyOnHand) PK=ProdID. Supplier(SuppID, SuppName) PK = SuppID. CompSupp(CompID, SuppID) PK = CompID, SuppID. Build(CompID, ProdID, QtyOfComp) PK= CompID, ProdID. التمرين الثاني: وكيل سيارات Car Dealership أنشئ مخطط ERD لوكيل سيارات، حيث يبيع هذا الوكيل كلًا من السيارات الجديدة والمستعملة، ويشغّل قسمًا للخدمات. ابنِ تصميمك على قواعد الأعمال التالية: قد يبيع مندوب المبيعات salesperson سيارات متعددة، ولكن تُباع كل سيارة بواسطة مندوب مبيعات واحد فقط. يمكن أن يشتري العميل customer سيارات متعددة، ولكن تُشترى كل سيارة بواسطة عميل واحد فقط. يكتب مندوب المبيعات فاتورةً invoice واحدة لكل سيارة يبيعها. يحصل العميل على فاتورة لكل سيارة يشتريها. قد يأتي العميل من أجل الحصول على خدماتٍ لسيارته فقط، وهذا يعني أن العميل لا يحتاج إلى شراء سيارة لكي يُصنَّف كعميل. إذا جلب العميل سيارةً أو أكثر لإصلاحها أو للحصول على خدمة، فستُكتَب تذكرة خدمة service ticket لكل سيارة. يحتفظ وكيل السيارات بتاريخ خدمة لكل من السيارات المُخدَّمة، ويُشار إلى سجلات الخدمة عن طريق رقم السيارة التسلسلي. يمكن أن يعمل على السيارة التي تُجلَب للحصول على خدمة ميكانيكيون متعددون، وقد يعمل كل ميكانيكي على سيارات متعددة. قد تحتاج السيارة التي تحصل على خدمة إلى قِطع أو قد لا تحتاج إلى قطع (مثل عملية ضبط المفحّم carburetor أو تنظيف فوهة حاقن الوقود التي لا تتطلب توفير قِطعٍ جديدة). جواب مخطط ERD حل تمرين باستخدام لغة SQL نزّل السكريبت التالي: OrdersAndData.sql. الجزء الأول: استخدم لغة DDL استخدم السكريبت orderData.sql الذي ينشئ جداولًا ويضيف بيانات مخطط ERD للطلبات والبيانات في الشكل السابق. أنشئ قاعدة بيانات تسمّى Orders، وعدّل السكريبت لدمج المفتاح الرئيسي PK والسلامة المرجعية referential integrity. استخدم عبارات CREATE TABLE مع التعديلات بما في ذلك القيود الموجودة في الخطوة 3. أضف القيود التالية: tblCustomers table: Country (Canada قيمته الافتراضية هي) tblOrderDetails: Quantity – > 0 tblShippers: CompanyName (يجب أن يكون فريدًا) tblOrders: ShippedDate (order date يجب أن يكون أكبر تاريخ الطلب) CREATE DATABASE Orders Go Use Orders Go Use Orders Go CREATE TABLE [dbo].[tblCustomers] [CustomerID] nvarchar(5) NOT NULL, [CompanyName] nvarchar(40) NOT NULL, [ContactName] nvarchar(30) NULL, [ContactTitle] nvarchar(30) NULL, [Address] nvarchar(60) NULL, [City] nvarchar(15) NULL, [Region] nvarchar(15) NULL, [PostalCode] nvarchar(10) NULL, [Country] nvarchar(15) NULL Constraint df_country DEFAULT ‘Canada’, [Phone] nvarchar(24) NULL, [Fax] nvarchar(24) NULL, Primary Key (CustomerID) ); CREATE TABLE [dbo].[tblSupplier] ( [SupplierID] int NOT NULL, [Name] nvarchar(50) NULL, [Address] nvarchar(50) NULL, [City] nvarchar(50) NULL, [Province] nvarchar(50) NULL, Primary Key (SupplierID) ); CREATE TABLE [dbo].[tblShippers] ( [ShipperID] int NOT NULL, [CompanyName] nvarchar(40) NOT NULL, Primary Key (ShipperID),< CONSTRAINT uc_CompanyName UNIQUE (CompanyName) ); CREATE TABLE [dbo].[tblProducts] ( [ProductID] int NOT NULL, [SupplierID] int NULL, [CategoryID] int NULL, [ProductName] nvarchar(40) NOT NULL, [EnglishName] nvarchar(40) NULL, [QuantityPerUnit] nvarchar(20) NULL, [UnitPrice] money NULL, [UnitsInStock] smallint NULL, [UnitsOnOrder] smallint NULL, [ReorderLevel] smallint NULL, [Discontinued] bit NOT NULL, Primary Key (ProductID), Foreign Key (SupplierID) References tblSupplier ); CREATE TABLE [dbo].[tblOrders] ( [OrderID] int NOT NULL, [CustomerID] nvarchar(5) NOT NULL, [EmployeeID] int NULL, [ShipName] nvarchar(40) NULL, [ShipAddress] nvarchar(60) NULL, [ShipCity] nvarchar(15) NULL, [ShipRegion] nvarchar(15) NULL, [ShipPostalCode] nvarchar(10) NULL, [ShipCountry] nvarchar(15) NULL, [ShipVia] int NULL, [OrderDate] smalldatetime NULL, [RequiredDate] smalldatetime NULL, [ShippedDate] smalldatetime NULL, [Freight] money NULL Primary Key (OrderID), Foreign Key (CustomerID) References tblCustomers, Foreign Key (ShipVia) References tblShippers, Constraint valid_ShipDate CHECK (ShippedDate > OrderDate) ); CREATE TABLE [dbo].[tblOrderDetails] ( [OrderID] int NOT NULL, [ProductID] int NOT NULL, [UnitPrice] money NOT NULL, [Quantity] smallint NOT NULL, [Discount] real NOT NULL, Primary Key (OrderID, ProductID), Foreign Key (OrderID) References tblOrders, Foreign Key (ProductID) References tblProducts, Constraint Valid_Qty Check (Quantity > 0) ); Go الجزء الثاني: إنشاء عبارات لغة SQL اعرض قائمة العملاء customers والطلبات orders المُنشَأة خلال عام 2014. أظهر الحقول customer ID و order ID و order date و date ordered. Use Orders Go SELECT CompanyName, OrderID, RequiredDate as ‘order date’, OrderDate as ‘date ordered’ FROM tblcustomers JOIN tblOrders on tblOrders.CustomerID = tblCustomers.CustomerID WHERE Year(OrderDate) = 2014 أضف حقلًا جديدًا (نشطًا) في جدول tblCustomer باستخدام عبارة ALTER TABLE، حيث تكون قيمته الافتراضية True. ALTER TABLE tblCustomers ADD Active bit DEFAULT (‘True’) اعرض جميع الطلبات التي جرى شراؤها قبل 1 سبتمبر 2012 (اعرض الحقول company name و date ordered وكلفة الطلب الإجمالية (بما في ذلك تكلفة الشحن freight). SELECT tblOrders.OrderID, OrderDate as ‘Date Ordered’, sum(unitprice*quantity*(1-discount))+ freight as ‘Total Cost’ FROM tblOrderDetails join tblOrders on tblOrders.orderID = tblOrderDetails.OrderID WHERE OrderDate < ‘September 1, 2012’ GROUP BY tblOrders.OrderID, freight, OrderDate اعرض جميع الطلبات المشحونة عبر شركة Federal Shipping (اعرض الحقول OrderID و ShipName و ShipAddress و CustomerID). SELECT OrderID, ShipName, ShipAddress, CustomerID FROM tblOrders join tblShippers on tblOrders.ShipVia = tblShippers.ShipperID WHERE CompanyName= ‘Federal Shipping’ اعرض جميع العملاء الذين لم يشتروا في عام 2011. SELECT CompanyName FROM tblCustomers WHERE CustomerID not in ( SELECT CustomerID FROM tblOrders WHERE Year(OrderDate) = 2011 ) اعرض جميع المنتجات التي لم تُطلَب أبدًا. SELECT ProductID from tblProducts Except SELECT ProductID from tblOrderDetails أو يمكن حل ذلك بالشكل التالي: SELECT Products.ProductID,Products.ProductName FROM Products LEFT JOIN [Order Details] ON Products.ProductID = [Order Details].ProductID WHERE [Order Details].OrderID IS NULL اعرض معرّفات الطلبات OrderID للزبائن الذين يقيمون في لندن باستخدام استعلام فرعي (اعرض الحقول CustomerID و CustomerName و OrderID). SELECT Customers.CompanyName,Customers.CustomerID,OrderID FROM Orders LEFT JOIN Customers ON Orders.CustomerID = Customers.CustomerID WHERE Customers.CompanyName IN (SELECT CompanyName FROM Customers WHERE City = ‘London’) اعرض المنتجات التي يوفّرها الموّرد A والموّرد B (اعرض الحقول product name و supplier name). SELECT ProductName, Name FROM tblProducts JOIN tblSupplier on tblProducts.SupplierID = tblSupplier.SupplierID WHERE Name Like ‘Supplier A’ or Name Like ‘Supplier B’ اعرض جميع المنتجات التي تأتي ضمن صناديق (اعرض الحقول product name و QuantityPerUnit). SELECT EnglishName, ProductName, QuantityPerUnit FROM tblProducts WHERE QuantityPerUnit like ‘%box%’ ORDER BY EnglishName الجزء الثالث: الإدخال Insert والتعديل Update والحذف Delete والفهارس Indexes أنشئ جدول الموظفين Employee. يجب أن يكون المفتاح الرئيسي هو معرّف الموظف EmployeeID وهو حقل ترقيم تلقائي autonumber. أضف الحقول التالية: LastName و FirstName و Address و City و Province و Postalcode و Phone و Salary. استخدم عبارة إنشاء جدول CREATE TABLE وعبارات إدخال INSERT خمسة موظفين. ضم جدول الموظفين employee إلى الجدول Tblorders. اعرض السكريبت لإنشاء الجدول وإعداد القيود وإضافة الموظفين. Use Orders CREATE TABLE [dbo].[tblEmployee]( EmployeeID Int IDENTITY NOT NULL , FirstName varchar (20) NOT NULL, LastName varchar (20) NOT NULL, Address varchar (50), City varchar(20), Province varchar (50), PostalCode char(6), Phone char (10), Salary Money NOT NULL, Primary Key (EmployeeID) Go INSERT into tblEmployees Values (‘Jim’, ‘Smith’, ‘123 Fake’, ‘Terrace’, ‘BC’, ‘V8G5J6’, ‘2506155989’, ‘20.12’), (‘Jimmy’, ‘Smithy’, ‘124 Fake’, ‘Terrace’, ‘BC’, ‘V8G5J7’, ‘2506155984’, ‘21.12’), (‘John’, ‘Smore’, ’13 Fake’, ‘Terrace’, ‘BC’, ‘V4G5J6’, ‘2506115989’, ‘19.12’), (‘Jay’, ‘Sith’, ’12 Fake’, ‘Terrace’, ‘BC’, ‘V8G4J6’, ‘2506155939’, ‘25.12’), (‘Jig’, ‘Mith’, ’23 Fake’, ‘Terrace’, ‘BC’, ‘V8G5J5’, ‘2506455989’, ‘18.12’); Go أضف حقلًا يسمّى Totalsales إلى جدول Tblorders. استخدم تعليمات لغة DDL وعبارة ALTER TABLE. ALTER TABLE tblOrders ADD Foreign Key (EmployeeID) references tblEmployees (EmployeeID) استخدم عبارة UPDATE لإضافة مجموع مبيعات كل طلب بناءً على جدول تفاصيل الطلب order details. UPDATE tblOrders Set TotalSales = (select sum(unitprice*quantity*(1-discount)) FROM tblOrderDetails WHERE tblOrderDetails.OrderID= tblOrders.OrderID GROUP BY OrderID ترجمة -وبتصرف- للمقالات: Appendix A University Registration Data Model Example Appendix B Sample ERD Exercises Appendix C SQL Lab with Solution لـ Adrienne Watt و Nelson Eng اقرأ أيضًا: المقال السابق: لغة معالجة البيانات DML الخاصة بلغة SQL لغة معالجة البيانات DML الخاصة بلغة SQL نمذجة الكيان العلاقي ER عند تصميم قواعد البيانات نظرة سريعة على لغة الاستعلامات الهيكلية SQL النسخة العربية الكاملة لكتاب تصميم قواعد البيانات
-
سنتعرّف من خلال هذا المقال على وحدات Node.js الأساسية مثل وحدة fs وpath وos وevents وتوابعها المتعددة، كما يمكنك بناء وحدة مخصَّصة بالاعتماد على الوحدات الأساسية، حيث سنتعرّف على كيفية استخدام واجهة module.exports البرمجية لتصدير بياناتك، وسنتعرّف على وحدة MySQL للتعامل مع قواعد البيانات. وحدة fs توفّر وحدة fs عمليات متعددة ومفيدة للوصول إلى نظام الملفات والتفاعل معه، وليست هناك حاجة لتثبيتها نظرًا لكونها جزءًا من نواة نود، إذ يمكن استخدامها ببساطة عن طريق طلبها كما يلي: const fs = require('fs') ثم يمكنك الوصول إلى جميع توابعها التي تشمل ما يلي: fs.access(): يتحقق من وجود الملف ويمكن لنود الوصول إلى الملف باستخدام أذوناته. fs.appendFile(): يُلحِق بيانات بملف، وينشئ الملف إذا كان غير موجود مسبقًا. fs.chmod(): يغيّر أذونات الملف المحدَّد بواسطة اسم الملف المُمرَّر، ويتعلق بالتابعَين fs.lchmod() وfs.fchmod(). fs.chown(): يغيّر مالك ومجموعة الملف المحدَّد بواسطة اسم الملف المُمرَّر، ويتعلق بالتابعَين fs.fchown() وfs.lchown(). fs.close(): يغلق واصف الملف file descriptor. fs.copyFile(): ينسخ ملفًا. fs.createReadStream(): ينشئ مجرى stream ملف قابل للقراءة. fs.createWriteStream(): ينشئ مجرى ملف قابل للكتابة. fs.link(): ينشئ رابطًا صلبًا hard link جديدًا إلى ملف. fs.mkdir(): ينشئ مجلدًا جديدًا. fs.mkdtemp(): ينشئ مجلدًا مؤقتًا. fs.open(): يضبط نمط الملف. fs.readdir(): يقرأ محتويات مجلد. fs.readFile(): يقرأ محتوى ملف، ويتعلق بالتابع fs.read(). fs.readlink(): يقرأ قيمة الوصلة الرمزية symbolic link. fs.realpath(): يُستخدَم لربط resolve مؤشرات مسار الملف النسبي (. و..) مع المسار الكامل. fs.rename(): يعيد تسمية ملف أو مجلد. fs.rmdir(): يزيل مجلدًا. fs.stat(): يعيد حالة الملف المحدَّد بواسطة اسم الملف المُمرَّر، ويتعلق بالتابعَين fs.fstat() وfs.lstat(). fs.symlink(): ينشئ وصلةً رمزيةً جديدًا إلى ملف. fs.truncate(): يقتطع الملف المحدَّد بواسطة اسم الملف المُمرَّر إلى طول معيَّن، ويتعلق بالتابع fs.ftruncate(). fs.unlink(): يزيل ملفًا أو وصلةً رمزيةً. fs.unwatchFile(): يوقِف مشاهدة التغييرات على ملف. fs.utimes(): يغيّر الطابع الزمني timestamp للملف المحدَّد باسم الملف المُمرَّر، ويتعلق بالتابع fs.futimes(). fs.watchFile(): يبدأ بمشاهدة التغييرات على ملف، ويتعلق بالتابع fs.watch(). fs.writeFile(): يكتب بيانات في ملف، ويتعلق بالتابع: fs.write(). جميع التوابع في وحدة fs غير متزامنة افتراضيًا، ولكن يمكنها العمل بطريقة متزامنة من خلال إلحاق الكلمة Sync باسم التابع كما يلي: fs.rename() fs.renameSync() fs.write() fs.writeSync() ويحدِث ذلك فرقًا كبيرًا في تدفق تطبيقك. لنختبر التابع fs.rename() مثلًا، حيث تُستخدَم واجهة برمجة التطبيقات API غير المتزامنة مع دالة رد نداء callback: const fs = require('fs') fs.rename('before.json', 'after.json', (err) => { if (err) { return console.error(err) } //done }) يمكن استخدام واجهة برمجة تطبيقات متزامنة مثل المثال التالي مع كتلة try/catch لمعالجة الأخطاء: const fs = require('fs') try { fs.renameSync('before.json', 'after.json') //done } catch (err) { console.error(err) } الاختلاف الرئيسي هو إيقاف تنفيذ السكربت الخاص بك في المثال الثاني إلى أن تنجح عملية الملف. للمزيد من المعلومات حول هذه الوحدة، يمكنك الرجوع إلى توثيق التعامل مع نظام الملفات في Node.js في موسوعة حسوب. وحدة المسار path توفِّر وحدة path عمليات متعددة ومفيدة للوصول إلى نظام الملفات والتفاعل معه، وليست هناك حاجة لتثبيتها نظرًا لكونها جزءًا من نواة نود، إذ يمكن استخدامها ببساطة عن طريق طلبها كما يلي: const path = require('path') توفِّر هذه الوحدة محرف path.sep الذي يوفّر فاصل مقاطع المسار path segment separator (\ على نظام ويندوز Windows و/ على نظامي لينكس Linux وmacOS)، بالإضافة إلى محرف path.delimiter الذي يوفّر محدّد المسار path delimiter (; على ويندوز Windows و: على نظامَي لينكس Linux وmacOS). توابع وحدة path هي: path.basename() path.dirname() path.extname() path.isAbsolute() path.join() path.normalize() path.parse() path.relative() path.resolve() للمزيد من المعلومات حول هذه الوحدة، يمكنك الرجوع إلى توثيق وحدة المسار (Path) في Node.js في موسوعة حسوب. التابع path.basename() يعيد هذا التابع الجزء الأخير من المسار، ويمكن للمعامِل الثاني تحديد امتداد الملف لإعطاء الملف دون امتداده كما يلي: require('path').basename('/test/something') //something require('path').basename('/test/something.txt') //something.txt require('path').basename('/test/something.txt', '.txt') //something التابع path.dirname() يعيد هذا التابع جزء المجلد أو الدليل من المسار كما يلي: require('path').dirname('/test/something') // /test require('path').dirname('/test/something/file.txt') // /test/something التابع path.extname() يعيد هذا التابع جزء الامتداد من المسار كما يلي: require('path').dirname('/test/something') // '' require('path').dirname('/test/something/file.txt') // '.txt' التابع path.isAbsolute() يعيد هذا التابع القيمة true إذا كان المسار مسارًا مطلقًا. require('path').isAbsolute('/test/something') // true require('path').isAbsolute('./test/something') // false التابع path.join() يربط هذا التابع جزأين أو أكثر من المسار مع بعضها البعض كما يلي: const name = 'flavio' require('path').join('/', 'users', name, 'notes.txt') //'/users/flavio/notes.txt' التابع path.normalize() يحاول هذا التابع حساب المسار الفعلي عندما يحتوي على محددات نسبية مثل . أو .. أو شرطات مائلة مزدوجة: require('path').normalize('/users/flavio/..//test.txt') ///users/test.txt التابع path.parse() يوزّع هذا التابع مسارًا على كائن يتكون من أجزاء متعددة هي: root: يمثّل الجذر. dir: هو مسار المجلد بداية من الجذر. base: يمثّل اسم الملف مع الامتداد. name: هو اسم الملف. ext: يمثّل امتداد الملف. إليك المثال التالي: require('path').parse('/users/test.txt') وتكون النتيجة كما يلي: { root: '/', dir: '/users', base: 'test.txt', ext: '.txt', name: 'test' } التابع path.relative() يقبل هذا التابع مسارين على أساس وسائط، ويعيد المسار النسبي من المسار الأول إلى المسار الثاني بناءً على مجلد العمل الحالي مثل المثال التالي: require('path').relative('/Users/flavio', '/Users/flavio/test.txt') //'test.txt' require('path').relative('/Users/flavio', '/Users/flavio/something/test.txt') //'something/test.txt' التابع path.resolve() يمكنك حساب المسار المطلق لمسار نسبي باستخدام التابع path.resolve() كما يلي: path.resolve('flavio.txt') //'/Users/flavio/flavio.txt' إذا شُغِّل من المجلد المحلي إذا حدّدت المعامل الثاني، فسيستخدم التابع resolve المعامِل الأول أساسًا للمعامِل الثاني كما يلي: path.resolve('tmp', 'flavio.txt') // '/Users/flavio/tmp/flavio.txt' إذا شُغِّل من المجلد المحلي إذا بدأ المعامِل الأول بشرطة مائلة، فهذا يعني أنه مسار مطلق كما يلي: path.resolve('/etc', 'flavio.txt')//'/etc/flavio.txt' وحدة os توفِّر هذه الوحدة عمليات متعددة يمكنك استخدامها لاسترداد معلومات من نظام التشغيل الأساسي والحاسوب الذي يعمل عليه البرنامج والتفاعل معه. const os = require('os') هناك بعض الخاصيات المفيدة التي تخبرنا ببعض الأمور الأساسية المتعلقة بمعالجة الملفات مثل: os.EOL التي تعطينا متسلسلة محدّد السطور، وهي \n على نظامَي لينكس Linux وmacOS؛ أما على نظام ويندوز Windows فهي \r\n. os.constants.signals التي تعطينا كل الثوابت المتعلقة بمعالجة إشارات العمليات مثل SIGHUP وSIGKILL وما إلى ذلك، كما يمكنك الاطلاع على جميع هذه الثوابت على /node_os. os.constants.errno التي تضبط الثوابت في تقارير الخطأ مثل EADDRINUSE وEOVERFLOW وغير ذلك. لنتعرّف الآن على التوابع الرئيسية التي توفرها وحدة os وهي: os.arch() os.cpus() os.endianness() os.freemem() os.homedir() os.hostname() os.hostname() os.loadavg() os.networkInterfaces() os.platform() os.release() os.tmpdir() os.totalmem() os.type() os.uptime() os.userInfo() للمزيد من المعلومات حول هذه الوحدة، يمكنك الرجوع إلى توثيق الوحدة os في Node.js في موسوعة حسوب. التابع os.arch() يعيد هذا التابع السلسلة النصية التي تحدد البنية الأساسية مثل arm وx64 وarm64. التابع os.cpus() يعيد معلومات وحدات المعالجة المركزية المتوفرة على نظامك، وإليك المثال التالي: [ { model: 'Intel(R) Core(TM)2 Duo CPU P8600 @ 2.40GHz', speed: 2400, times: { user: 281685380, nice: 0, sys: 187986530, idle: 685833750, irq: 0 } }, { model: 'Intel(R) Core(TM)2 Duo CPU P8600 @ 2.40GHz', speed: 2400, times: { user: 282348700, nice: 0, sys: 161800480, idle: 703509470, irq: 0 } } ] التابع os.endianness() يعيد هذا التابع القيمة BE أو القيمة LE بناءً على طريقة تصريف نود باستخدام تخزين البتات الأقل أهمية أولًا Big Endian أو تخزين البتات الأكثر أهمية أولًا Little Endian. التابع os.freemem() يعيد هذا التابع عدد البايتات التي تمثل الذاكرة المتاحة في النظام. التابع os.homedir() يعيد هذا التابع المسار إلى مجلد المستخدِم الحالي الرئيسي مثل المثال التالي: '/Users/flavio' التابع os.hostname() يعيد هذا التابع اسم المضيف hostname. التابع os.loadavg() يعيد هذا التابع الحساب الذي أجراه نظام التشغيل على متوسط التحميل، حيث يعيد فقط قيمة ذات معنى في نظامَي لينكس Linux وmacOS مثل المثال التالي: [ 3.68798828125, 4.00244140625, 11.1181640625 ] التابع os.networkInterfaces() يعيد هذا التابع تفاصيل واجهات الشبكة المتوفرة على نظامك، وإليك المثال التالي: { lo0: [ { address: '127.0.0.1', netmask: '255.0.0.0', family: 'IPv4', mac: 'fe:82:00:00:00:00', internal: true }, { address: '::1', netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', family: 'IPv6', mac: 'fe:82:00:00:00:00', scopeid: 0, internal: true }, { address: 'fe80::1', netmask: 'ffff:ffff:ffff:ffff::', family: 'IPv6', mac: 'fe:82:00:00:00:00', scopeid: 1, internal: true } ], en1: [ { address: 'fe82::9b:8282:d7e6:496e', netmask: 'ffff:ffff:ffff:ffff::', family: 'IPv6', mac: '06:00:00:02:0e:00', scopeid: 5, internal: false }, { address: '192.168.1.38', netmask: '255.255.255.0', family: 'IPv4', mac: '06:00:00:02:0e:00', internal: false } ], utun0: [ { address: 'fe80::2513:72bc:f405:61d0', netmask: 'ffff:ffff:ffff:ffff::', family: 'IPv6', mac: 'fe:80:00:20:00:00', scopeid: 8, internal: false } ] } التابع os.platform() يعيد هذا التابع المنصة الذي جرى تصريف نود من أجلها مثل: darwin0. freebsd. linux. openbsd. win32. وغيرها الكثير. التابع os.release() يعيد هذا التابع سلسلةً نصيةً تحدِّد رقم إصدار نظام التشغيل. التابع os.tmpdir() يعيد هذا التابع المسار إلى المجلد المؤقت المعيَّن. التابع os.totalmem() يعيد هذا التابع عدد البايتات الذي يمثِّل إجمالي الذاكرة المتوفرة في النظام. التابع os.type() يحدّد هذا التابع نظام التشغيل كما يلي: Linux. Darwin على نظام macOS. Windows_NT على نظام ويندوز. التابع os.uptime() يعيد هذا التابع عدد الثواني التي عمل فيها الحاسوب منذ آخر إعادة تشغيل. التابع os.userInfo() يعيد معلومات عن المستخدِم الفعّال حاليًا. وحدة الأحداث events توفِّر لنا وحدة الأحداث events الصنف EventEmitter، وتُعَدّ أساسًا للعمل مع الأحداث في نود. const EventEmitter = require('events') const door = new EventEmitter() يختبر مستمع الأحداث event listener أحداثه الخاصة ويستخدِم الحدثين التاليين: newListener عند إضافة المستمع. removeListener عند إزالة المستمع. سنشرح فيما يلي التوابع المفيدة التالية: emitter.addListener() emitter.emit() emitter.eventNames() emitter.getMaxListeners() emitter.listenerCount() emitter.listeners() emitter.off() emitter.on() emitter.once() emitter.prependListener() emitter.prependOnceListener() emitter.removeAllListeners() emitter.removeListener() emitter.setMaxListeners() للمزيد من المعلومات حول هذه الوحدة، يمكنك الرجوع إلى توثيق الأحداث في Node.js في موسوعة حسوب. التابع emitter.addListener() وهو الاسم البديل للتابع emitter.on(). التابع emitter.emit() يصدر هذا التابع حدثًا، حيث يستدعي بصورة متزامنة كل مستمع حدث بالترتيب الذي سُجِّلت به. التابع emitter.eventNames() يعيد هذا التابع مصفوفةً من السلاسل النصية التي تمثِّل الأحداث المُسجَّلة في كائن EventListener الحالي: door.eventNames() التابع emitter.getMaxListeners() يُستخدَم هذا التابع للحصول على الحد الأقصى من المستمعين الذي يمكن إضافته إلى كائن EventListener، حيث يُضبَط هذا العدد افتراضيًا على القيمة 10 ولكن يمكن زيادته أو إنقاصه باستخدام setMaxListeners(). door.getMaxListeners() التابع emitter.listenerCount() يُستخدَم هذا التابع للحصول على عدد مستمعي الحدث المُمرَّرين على أساس معامِلات كما يلي: door.listenerCount('open') التابع emitter.listeners() يُستخدَم هذا التابع للحصول على مصفوفة مستمعي الحدث المُمرَّرين على أساس معامِلات كما يلي: door.listeners('open') التابع emitter.off() يمثّل هذا التابع الاسم البديل للتابع emitter.removeListener() المُضاف في الإصدار 10 من نود. التابع emitter.on() يضيف هذا التابع دالة رد النداء التي تُستدعَى عند إصدار حدث، ويُستخدَم هذا التابع كما يلي: door.on('open', () => { console.log('Door was opened') }) التابع emitter.once() يضيف هذا التابع دالة رد النداء التي تُستدعَى عند إصدار حدث لأول مرة بعد تسجيله، حيث ستُستدعَى دالة رد النداء تلك مرةً واحدةً فقط، ولن تُستدعَى مرةً أخرى. const EventEmitter = require('events') const ee = new EventEmitter() ee.once('my-event', () => { //ًاستدعِ دالة رد النداء مرةً واحدة }) التابع emitter.prependListener() يُضاف المستمع الذي تضيفه باستخدام on أو addListener في آخر طابور المستمعين ويُستدعَى أخيرًا كذلك، ولكنه يُضاف ويُستدعَى قبل المستمعين الآخرين باستخدام prependListener. التابع emitter.prependOnceListener() يُضاف المستمع الذي تضيفه باستخدام once في آخر طابور المستمعين ويُستدعَى أخيرًا كذلك، ولكنه يُضاف ويُستدعَى قبل المستمعين الآخرين باستخدام prependOnceListener. التابع emitter.removeAllListeners() يزيل هذا التابع جميع مستمعي الكائن الذي يصدر الأحداث ويستمع إلى حدث محدَّد: door.removeAllListeners('open') التابع emitter.removeListener() يزيل مستمعًا محدَّدًا عن طريق حفظ دالة رد النداء في متغير عند إضافته، بحيث يمكنك الإشارة إليه لاحقًا: const doSomething = () => {} door.on('open', doSomething) door.removeListener('open', doSomething) التابع emitter.setMaxListeners() يضبط الحد الأقصى لعدد المستمعين الذي يمكن إضافته إلى كائن EventListener، حيث يُضبَط هذا العدد افتراضيًا على القيمة 10 ولكن يمكن زيادته أو إنقاصه. door.setMaxListeners(50) وحدة HTTP توفّر وحدة http في Node.js دوالًا وأصنافًا مفيدة لبناء خادم HTTP، وتُعَدّ الوحدة الأساسية لشبكات نود، كما يمكن تضمين وحدة http كما يلي: const http = require('http') توفّر وحدة http بعض الخاصيات properties والتوابع methods والأصناف classes. للمزيد من المعلومات حول هذه الوحدة، يمكنك الرجوع إلى توثيق الوحدة HTTP في Node.js في موسوعة حسوب. الخاصيات توفِّر وحدة HTTP الخاصيات التالية: http.METHODS http.STATUS_CODES http.globalAgent الخاصية http.METHODS تعطي هذه الخاصية قائمةً بجميع توابع HTTP المدعومة كما يلي: > require('http').METHODS [ 'ACL', 'BIND', 'CHECKOUT', 'CONNECT', 'COPY', 'DELETE', 'GET', 'HEAD', 'LINK', 'LOCK', 'M-SEARCH', 'MERGE', 'MKACTIVITY', 'MKCALENDAR', 'MKCOL', 'MOVE', 'NOTIFY', 'OPTIONS', 'PATCH', 'POST', 'PROPFIND', 'PROPPATCH', 'PURGE', 'PUT', 'REBIND', 'REPORT', 'SEARCH', 'SUBSCRIBE', 'TRACE', 'UNBIND', 'UNLINK', 'UNLOCK', 'UNSUBSCRIBE' ] الخاصية http.STATUS_CODES تعطي هذه الخاصية قائمةً بجميع رموز حالة HTTP ووصفها كما يلي: > require('http').STATUS_CODES { '100': 'Continue', '101': 'Switching Protocols', '102': 'Processing', '200': 'OK', '201': 'Created', '202': 'Accepted', '203': 'Non-Authoritative Information', '204': 'No Content', '205': 'Reset Content', '206': 'Partial Content', '207': 'Multi-Status', '208': 'Already Reported', '226': 'IM Used', '300': 'Multiple Choices', '301': 'Moved Permanently', '302': 'Found', '303': 'See Other', '304': 'Not Modified', '305': 'Use Proxy', '307': 'Temporary Redirect', '308': 'Permanent Redirect', '400': 'Bad Request', '401': 'Unauthorized', '402': 'Payment Required', '403': 'Forbidden', '404': 'Not Found', '405': 'Method Not Allowed', '406': 'Not Acceptable', '407': 'Proxy Authentication Required', '408': 'Request Timeout', '409': 'Conflict', '410': 'Gone', '411': 'Length Required', '412': 'Precondition Failed', '413': 'Payload Too Large', '414': 'URI Too Long', '415': 'Unsupported Media Type', '416': 'Range Not Satisfiable', '417': 'Expectation Failed', '418': 'I\'m a teapot', '421': 'Misdirected Request', '422': 'Unprocessable Entity', '423': 'Locked', '424': 'Failed Dependency', '425': 'Unordered Collection', '426': 'Upgrade Required', '428': 'Precondition Required', '429': 'Too Many Requests', '431': 'Request Header Fields Too Large', '451': 'Unavailable For Legal Reasons', '500': 'Internal Server Error', '501': 'Not Implemented', '502': 'Bad Gateway', '503': 'Service Unavailable', '504': 'Gateway Timeout', '505': 'HTTP Version Not Supported', '506': 'Variant Also Negotiates', '507': 'Insufficient Storage', '508': 'Loop Detected', '509': 'Bandwidth Limit Exceeded', '510': 'Not Extended', '511': 'Network Authentication Required' } الخاصية http.globalAgent تؤشّر هذه الخاصية إلى نسخة كائن الوكيل Agent العامة، والذي هو نسخة من الصنف http.Agent، حيث يُستخدَم هذا الصنف لإدارة الاتصالات المستمرة وإعادة استخدام عملاء HTTP، وهو المكوّن الأساسي من شبكات HTTP الخاصة بنود Node. التوابع توفِّر وحدة HTTP التوابع التالية: http.createServer() http.request() http.get() التابع http.createServer() يعيد هذا التابع نسخةً جديدةً من الصنف http.Server، حيث يُستخدَم كما يلي: const server = http.createServer((req, res) => { //معالجة كل طلب باستخدام دالة رد النداء هذه }) التابع http.request() ينشئ طلب HTTP إلى خادم، مما يؤدي إلى إنشاء نسخة من الصنف http.ClientRequest. التابع http.get() يشبه التابع http.request()، ولكنه يضبط تلقائيًا تابع HTTP على GET ويستدعي التابع req.end() تلقائيًا. الأصناف Classes توفِّر وحدة HTTP خمسة أصناف هي: http.Agent http.ClientRequest http.Server http.ServerResponse http.IncomingMessage الصنف http.Agent نُنشئ نود نسخةً عامةً من الصنف http.Agent لإدارة الاتصالات المستمرة وإعادة استخدام عملاء HTTP، وهو المكوّن الأساسي من شبكات HTTP الخاصة بنود Node، كما يتأكّد هذا الكائن من وضع كل طلب إلى الخادم في طابور ومن إعادة استخدام المقبس socket، كما أنه يحتفظ بمجمّع من المقابس بهدف تحسين الأداء. الصنف http.ClientRequest يُنشَأ الكائن http.ClientRequest عند استدعاء التابع http.request() أو التابع http.get()، حيث يُستدعَى حدث response مع الاستجابة عند تلقيها باستخدام نسخة من كائن http.IncomingMessage على أساس وسيط، ويمكن قراءة بيانات الاستجابة المُعادة بطريقتين هما: استدعاء التابع response.read(). يمكنك إعداد مستمعٍ للحدث data في معالج الحدث response بحيث يمكنك الاستماع للبيانات المتدفقة إليه. الصنف http.Server تُنشَأ وتُعاد عادةً نسخة من هذا الصنف عند إنشاء خادم جديد باستخدام التابع http.createServer()، يمكنك الوصول إلى توابع كائن خادم بعد إنشائه، وهذه التوابع هي: close() الذي يوقِف الخادم من قبول اتصالات جديدة. listen() الذي يشغّل خادم HTTP ويستمع للاتصالات. الصنف http.ServerResponse ينشئه الصنف http.Server ويمرّره على أساس معامل ثانٍ لحدث request الذي يشغّله، حيث يُعرَف هذا الصنف ويُستخدَم في الشيفرة على أنه كائن res كما يلي: const server = http.createServer((req, res) => { //res هو كائن http.ServerResponse }) التابع الذي ستستدعيه دائمًا في المعالج هو end() والذي يغلق الاستجابة بعد اكتمال الرسالة ثم يستطيع الخادم إرسالها إلى العميل، إذ يجب استدعاؤه في كل استجابة، وتُستخدَم التوابع التالية للتفاعل مع ترويسات HTTP: getHeaderNames(): للحصول على قائمة بأسماء ترويسات HTTP المضبوطة مسبقًا. getHeaders(): للحصول على نسخة من ترويسات HTTP المضبوطة مسبقًا. setHeader('headername', value): يحدّد قيمة ترويسة HTTP. getHeader('headername'): للحصول على ترويسة HTTP المضبوطة مسبقًا. removeHeader('headername'): يزيل ترويسة HTTP المضبوطة مسبقًا. hasHeader('headername'): يعيد القيمة true إذا احتوت الاستجابة على هذه الترويسة المضبوطة. headersSent(): يعيد القيمة true إذا أُرسِلت الترويسات إلى العميل. مسبقًا يمكنك إرسال الترويسات بعد معالجتها إلى العميل عن طريق استدعاء التابع response.writeHead() الذي يقبل رمز الحالة statusCode على أساس معامل أول، ورسالة الحالة الاختيارية، وكائن الترويسات، كما يمكنك إرسال البيانات إلى العميل في جسم الاستجابة عن طريق استخدام التابع write() الذي سيرسل البيانات المخزَّنة إلى مجرى استجابة HTTP، فإذا لم تُرسَل الترويسات بعد باستخدام التابع response.writeHead()، فستُرسَل الترويسات أولًا مع رمز الحالة والرسالة المحدَّدة في الطلب والتي يمكنك تعديلها عن طريق ضبط قيم الخاصيات statusCode وstatusMessage كما يلي: response.statusCode = 500 response.statusMessage = 'Internal Server Error' الصنف http.IncomingMessage يُنشَأ كائن http.IncomingMessage باستخدام: http.Server عند الاستماع إلى الحدث request. http.ClientRequest عند الاستماع إلى الحدث response. يمكن استخدام كائن http.IncomingMessage للوصول إلى خاصيات الاستجابة التالية: الحالة status باستخدام توابع statusCode وstatusMessage الخاصة به. الترويسات باستخدام توابع headers أو rawHeaders الخاصة به. تابع HTTP باستخدام تابع method الخاص به. إصدار HTTP باستخدام تابع httpVersion. عنوان URL باستخدام تابع url. المقبس الأساسي underlying socket باستخدام تابع socket. يمكن الوصول إلى البيانات باستخدام المجاري streams، حيث ينفّذ كائن http.IncomingMessage واجهة المجرى Stream القابلة للقراءة. وحدة MySQL تُعَدّ MySQL واحدةً من أكثر قواعد البيانات العلائقية شيوعًا في العالم، إذ يحتوي نظام نود Node المجتمعي على حزم مختلفة تتيح لك التعامل مع MySQL وتخزين البيانات واسترداد البيانات وما إلى ذلك، كما سنستخدِم حزمة mysqljs/mysql التي تحتوي على أكثر من 12000 نجمة على GitHub وهي موجودة منذ سنوات. تثبيت حزمة نود mysql يمكنك تثبيتها باستخدام الأمر التالي: npm install mysql تهيئة الاتصال بقاعدة البيانات يجب تضمين الحزمة أولًا كما يلي: const mysql = require('mysql') ثم تنشئ اتصالًا كما يلي: const options = { user: 'the_mysql_user_name', password: 'the_mysql_user_password', database: 'the_mysql_database_name' } const connection = mysql.createConnection(options) ثم تهيئ اتصالًا جديدًا عن طريق استدعاء ما يلي: connection.connect(err => { if (err) { console.error('An error occurred while connecting to the DB') throw err } }) خيارات الاتصال احتوى كائن options في المثال السابق على 3 خيارات هي: const options = { user: 'the_mysql_user_name', password: 'the_mysql_user_password', database: 'the_mysql_database_name' } هناك خيارات أخرى متعددة يمكنك استخدامها مثل: host: اسم مضيف قاعدة البيانات، وقيمته الافتراضية هي localhost. port رقم منفذ خادم MySQL، وقيمته الافتراضية هي 3306. socketPath: يُستخدَم لتحديد مقبس يونيكس unix بدلًا من host وport. debug: يمكن استخدامه لتنقيح الأخطاء debugging عند تعطيله افتراضيًا. trace: يطبع تعقبات المكدس stack traces عند حدوث الأخطاء عند تفعيله افتراضيًا. ssl: يُستخدَم لإعداد اتصال SSL إلى الخادم. إجراء استعلام SELECT أصبحتَ الآن جاهزًا لإجراء استعلام SQL في قاعدة البيانات، وسيستدعي الاستعلامُ بمجرد تنفيذه دالةَ رد النداء التي تحتوي على الخطأ المُحتمَل error والنتائج results والحقول fields كما يلي: connection.query('SELECT * FROM todos', (error, todos, fields) => { if (error) { console.error('An error occurred while executing the query') throw error } console.log(todos) }) يمكنك تمرير القيم التي ستُتجاوز تلقائيًا كما يلي: const id = 223 connection.query('SELECT * FROM todos WHERE id = ?', [id], (error, todos, fields) => { if (error) { console.error('An error occurred while executing the query') throw error } console.log(todos) }) يمكنك تمرير قيم متعددة من خلال وضع مزيد من العناصر في المصفوفة التي تمررها على أساس معامل ثانٍ كما يلي: const id = 223 const author = 'Flavio' connection.query('SELECT * FROM todos WHERE id = ? AND author = ?', [id, author], (error, todos, fields) => { if (error) { console.error('An error occurred while executing the query') throw error } console.log(todos) }) إجراء استعلام INSERT يمكنك تمرير كائن كما يلي: const todo = { thing: 'Buy the milk' author: 'Flavio' } connection.query('INSERT INTO todos SET ?', todo, (error, results, fields) => { if (error) { console.error('An error occurred while executing the query') throw error } }) إذا احتوى الجدول على مفتاح رئيسي primary key مع auto_increment، فستُعاد قيمته ضمن القيمة results.insertId كما يلي: const todo = { thing: 'Buy the milk' author: 'Flavio' } connection.query('INSERT INTO todos SET ?', todo, (error, results, fields) => { if (error) { console.error('An error occurred while executing the query') throw error }} const id = results.resultId console.log(id) ) إغلاق الاتصال يمكنك استدعاء التابع end() عند إنهاء الاتصال بقاعدة البيانات كما يلي: connection.end() يعمل ذلك على التأكد من إرسال أي استعلام مُعلَّق وإنهاء الاتصال بأمان. وحدات مخصصة إذا لم تعثر على الوحدات المناسبة لك ضمن الوحدات الأساسية، فيمكنك بناء وحدة مخصَّصة تخدم غرضك بالاعتماد على الوحدات الأساسية ويمكنك أن تصدِّرها وتستوردها حتى أنه يمكنك بناء مكتبة كاملة، حيث سنتعرّف فيما يلي على كيفية استخدام واجهة module.exports البرمجية لتصدير بياناتك إلى ملفات أخرى في تطبيقك أو إلى تطبيقات أخرى. يمتلك نود نظام وحدات مبنيّ مسبقًا، إذ يمكن لملف Node.js استيراد العمليات التي تصدّرها ملفات Node.js الأخرى، فإذا أردت استيراد شيءٍ ما، فاستخدم ما يلي لاستيراد العمليات الظاهرة في ملف library.js الموجود في مجلد الملف الحالي، إذ يجب إظهار العمليات في هذا الملف قبل أن تستوردها ملفات أخرى: const library = require('./library') يكون أيّ كائن أو متغير آخر مُعرَّف في الملف خاصًا private افتراضيًا ولا يظهر لأيّ شيء خارجي، وهذا ما تسمح لنا به واجهة برمجة تطبيقات module.exports التي يوفِّرها [نظام module](https://nodejs.org/api/modules.html)، وإذا أسندتَ كائنًا أو دالةً مثل خاصية exports جديدة، فهذا هو الشيء الذي يظهر، ويمكن استيراده على هذا النحو في أجزاء أخرى من تطبيقك أو في تطبيقات أخرى أيضًا، حيث يمكنك تطبيق ذلك بطريقتين، الأولى هي إسناد كائن لوحدة module.exports، وهو كائن خارجي يوفّره نظام module، وبالتالي سيصدّر ملفك هذا الكائن فقط: const car = { brand: 'Ford', model: 'Fiesta' } module.exports = car //في الملف الآخر.. const car = require('./car') أما الطريقة الثانية فهي إضافة الكائن المُصدَّر على أساس خاصية exports، حيث تتيح لك هذه الطريقة تصدير كائنات أو دوال أو بيانات متعددة: const car = { brand: 'Ford', model: 'Fiesta' } exports.car = car أو مباشرةً: const car = { brand: 'Ford', model: 'Fiesta' } كما ستستخدمه في الملف الآخر من خلال الإشارة إلى خاصية الاستيراد كما يلي: const items = require('./items') items.car أو كما يلي: const car = require('./items').car هناك فرق بين module.exports وexports، فالأول يُظهِر الكائن الذي يؤشّر إليه، بينما يُظهِر الثاني خاصيات الكائن الذي يؤشّر إليه. ترجمة -وبتصرّف- للفصل Some essential core modules من كتاب The Node.js handbook لصاحبه Flavio Copes. اقرأ أيضًا المقال السابق: التعامل مع الملفات في Node.js البرمجة باستخدام الوحدات مقدمة إلى الوحدات Modules في جافاسكربت
-
سنتعرّف من خلال هذا المقال على كيفية التعامل مع الملفات والمجلدات في Node.js من خلال شرح واصفات الملفات وإحصائياتها ومساراتها وقراءتها وكتابتها، كما سنتعرّف على مفهوم المجاري streams وميّزاتها وأنواعها. واصفات الملفات File descriptors يمكن التفاعل مع واصفات الملفات باستخدام نود Node، إذ يجب أن تحصل على واصف ملف قبل تمكنك من التفاعل مع ملف موجود في نظام الملفات الخاص بك، وواصف الملف هو ما يُعاد عند فتح الملف باستخدام التابع open() الذي توفره وحدة fs: const fs = require('fs') fs.open('/Users/flavio/test.txt', 'r', (err, fd) => { //fd هو واصف الملف }) استخدمنا الراية r على أساس معامِل ثانٍ لاستدعاء fs.open()، إذ تعني هذه الراية أننا نفتح الملف للقراءة؛ أما الرايات الأخرى المُستخدَمة فهي: r+ فتح الملف للقراءة والكتابة. w+ فتح الملف للقراءة والكتابة، مع وضع المجرى stream في بداية الملف وإنشاء الملف إذا لم يكن موجودًا مسبقًا. a فتح الملف للكتابة، مع وضع المجرى في نهاية الملف وإنشاء الملف إن لم يكن موجودًا مسبقًا. a+ فتح الملف للقراءة والكتابة، مع وضع المجرى في نهاية الملف وإنشاء الملف إن لم يكن موجودًا مسبقًا. يمكنك فتح الملف باستخدام التابع fs.openSync الذي يعيد كائن واصف الملف بدلًا من توفيره في دالة رد نداء: const fs = require('fs') try { const fd = fs.openSync('/Users/flavio/test.txt', 'r') } catch (err) { console.error(err) } يمكنك تنفيذ جميع العمليات المطلوبة مثل استدعاء التابع fs.open() والعديد من العمليات الأخرى التي تتفاعل مع نظام الملفات بمجرد حصولك على واصف الملف بأيّ طريقة تختارها. إحصائيات الملف يأتي كل ملف مع مجموعة من التفاصيل التي يمكننا فحصها باستخدام نود Node باستخدام التابع stat() الذي توفِّره وحدة fs، حيث يمكنك استدعاؤه مع تمرير مسار ملف إليه، حيث سيستدعي نود بعد حصوله على تفاصيل الملف دالة رد النداء التي تمررها مع معاملين هما رسالة خطأ وإحصائيات الملف: const fs = require('fs') fs.stat('/Users/flavio/test.txt', (err, stats) => { if (err) { console.error(err) return } //يمكننا الوصول إلى إحصائيات الملف في `stats` }) كما يوفِّر نود تابعًا متزامنًا يوقِف الخيط thread إلى أن تصبح إحصائيات الملف جاهزةً: const fs = require('fs') try { const stats = fs.stat('/Users/flavio/test.txt') } catch (err) { console.error(err) } تُضمَّن معلومات الملف في المتغير stats، ويمكننا استخراج أنواع معلومات متعددة باستخدام توابع stats كما يلي: استخدم التابع stats.isFile() والتابع stats.isDirectory() لمعرفة إذا كان الملف عبارة عن مجلد أو ملف. استخدم التابع stats.isSymbolicLink() لمعرفة إذا كان الملف وصلةً رمزيةً symbolic link. استخدم التابع stats.size لمعرفة حجم الملف مقدَّرًا بالبايت. هناك توابع متقدمة أخرى، ولكن الجزء الأكبر مما ستستخدمه هو التوابع السابقة. const fs = require('fs') fs.stat('/Users/flavio/test.txt', (err, stats) => { if (err) { console.error(err) return } stats.isFile() //true stats.isDirectory() //false stats.isSymbolicLink() //false stats.size //1024000 //= 1MB }) مسارات الملفات سنتعرّف على كيفية التفاعل مع مسارات الملفات والتعامل معها في نود Node، فلكل ملف في النظام مسار، وقد يبدو المسار في نظامَي لينكس Linux وmacOS كما يلي: /users/flavio/file.txt بينما الحواسيب التي تعمل بنظام ويندوز Windows مختلفة، إذ يكون للمسار بنية كما يلي: C:\users\flavio\file.txt يجب الانتباه عند استخدام المسارات في تطبيقاتك، إذ يجب مراعاة هذا الاختلاف، كما يمكنك تضمين وحدة المسار في ملفاتك كما ما يلي: const path = require('path') ثم يمكنك البدء في استخدام توابعها، كما يمكنك استخراج معلومات من مسار باستخدام التوابع التالية: dirname: للحصول على مجلد الملف الأب. basename: للحصول على جزء اسم الملف. extname: للحصول على امتداد الملف. إليك المثال التالي: const notes = '/users/flavio/notes.txt' path.dirname(notes) // /users/flavio path.basename(notes) // notes.txt path.extname(notes) // .txt يمكنك الحصول على اسم الملف بدون امتداده عن طريق تحديد وسيط ثانٍ للتابع basename كما يلي: path.basename(notes, path.extname(notes)) //notes كما يمكنك ربط جزأين أو أكثر من المسار مع بعضها البعض باستخدام التابع path.join() كما يلي: const name = 'flavio' path.join('/', 'users', name, 'notes.txt') //'/users/flavio/notes.txt' يمكنك حساب مسار الملف المطلق absolute path من مساره النسبي relative path باستخدام التابع path.resolve() كما يلي: path.resolve('flavio.txt') //'/Users/flavio/flavio.txt' if run from my home folder سيُلحِق في هذه الحالة نود Node ببساطة المسارَ النسبي /flavio.txt بدليل أو مجلد العمل الحالي، فإذا حددت مجلدًا على أساس معامل آخر، فسيستخدِم تابع resolve المعامل الأول أساسًا للمعامل الثاني كما يلي: path.resolve('tmp', 'flavio.txt')//'/Users/flavio/tmp/flavio.txt' إذا شُغِّل من المجلد المحلي إذا بدأ المعامل الأول بشرطة مائلة، فهذا يعني أنه مسار مطلق مثل المثال التالي: path.resolve('/etc', 'flavio.txt')//'/etc/flavio.txt' يُعَدّ path.normalize() تابعًا آخرًا مفيدًا يحسب المسار الفعلي عندما يحتوي على محددات نسبية مثل . أو .. أو شرطة مائلة مزدوجة كما يلي: path.normalize('/users/flavio/..//test.txt') ///users/test.txt لن يتحقق التابعان resolve وnormalize من وجود المسار، وإنما يحسبان المسار فقط بناءً على المعلومات المتاحة. قراءة الملفات أبسط طريقة لقراءة ملف في نود هي استخدام تابع fs.readFile()، حيث نمرِّر له مسار الملف ودالة رد النداء التي ستُستدعَى مع بيانات الملف ومع الخطأ كما يلي: const fs = require('fs') fs.readFile('/Users/flavio/test.txt', (err, data) => { if (err) { console.error(err) return } console.log(data) }) يمكنك بدلًا من ذلك استخدام الإصدار المتزامن من التابع السابق وهو التابع fs.readFileSync(): const fs = require('fs') try { const data = fs.readFileSync('/Users/flavio/test.txt', 'utf8') console.log(data) } catch (err) { console.error(err) } الترميز الافتراضي هو utf8، ولكن يمكنك تحديد ترميز مُخصَّص باستخدام معامل ثانٍ، كما يقرأ كل من التابعَين fs.readFile() وfs.readFileSync() محتوى الملف الكامل في الذاكرة قبل إعادة البيانات، وهذا يعني أن الملفات الكبيرة سيكون لها تأثير كبير على استهلاك الذاكرة وسرعة تنفيذ البرنامج، وبالتالي يكون الخيار الأفضل في هذه الحالة هو قراءة محتوى الملف باستخدام المجاري streams. كتابة الملفات أسهل طريقة للكتابة في الملفات في Node.js هي استخدام واجهة برمجة تطبيقات fs.writeFile()، وإليك المثال التالي: const fs = require('fs') const content = 'Some content!' fs.writeFile('/Users/flavio/test.txt', content, (err) => { if (err) { console.error(err) return } //كُتِب الملف بنجاح }) يمكنك بدلًا من ذلك استخدام الإصدار المتزامن وهو fs.writeFileSync(): const fs = require('fs') const content = 'Some content!' try { const data = fs.writeFileSync('/Users/flavio/test.txt', content) //كُتِب الملف بنجاح } catch (err) { console.error(err) } ستبدّل واجهة برمجة التطبيقات هذه افتراضيًا محتويات الملف إذا كان موجودًا مسبقًا، ولكن يمكنك تعديل الإعداد الافتراضي عن طريق تحديد راية كما يلي: fs.writeFile('/Users/flavio/test.txt', content, { flag: 'a+' }, (err) => {}) الرايات التي يمكنك استخدامها هي: r+ لفتح الملف للقراءة والكتابة. w+ لفتح الملف للقراءة والكتابة مع وضع المجرى في بداية الملف وإنشاء الملف إذا لم يكن موجودًا مسبقًا. a لفتح الملف للكتابة مع وضع المجرى في نهاية الملف وإنشاء الملف إذا لم يكن موجودًا مسبقًا. a+ لفتح الملف للقراءة والكتابة، مع وضع المجرى في نهاية الملف، وإنشاء الملف إن لم يكن موجودًا مسبقًا. يمكنك العثور على المزيد من الرايات على /nodejs. إلحاق محتوى بملف يمكنك إلحاق محتوى بنهاية الملف من خلال استخدام التابع fs.appendFile() ونسخته المتزامنة التابع fs.appendFileSync(): const content = 'Some content!' fs.appendFile('file.log', content, (err) => { if (err) { console.error(err) return } //done! }) استخدام المجاري streams تكتب كل التوابع السابقة المحتوى الكامل في الملف قبل إعادة التحكم إلى برنامجك مرةً أخرى، أي تنفيذ دالة رد النداء في النسخة غير المتزامنة، وبالتالي الخيار الأفضل هو كتابة محتوى الملف باستخدام المجاري streams. لنتعرّف على الغرض الأساسي من المجاري streams وسبب أهميتها وكيفية استخدامها، حيث سنقدِّم مدخلًا بسيطًا إلى المجاري، ولكن هناك جوانب أكثر تعقيدًا لتحليلها. مفهوم المجاري streams تُعَدّ المجاري أحد المفاهيم الأساسية التي تعمل على تشغيل تطبيقات Node.js، وهي طريقة للتعامل مع ملفات القراءة/الكتابة أو اتصالات الشبكة أو أيّ نوع من تبادل المعلومات من طرف إلى طرف بطريقة فعالة، كما ليست المجاري مفهومًا خاصًا بنود Node.js، إذ توفّرت في نظام التشغيل يونيكس Unix منذ عقود، ويمكن للبرامج أن تتفاعل مع بعضها البعض عبر تمرير المجاري من خلال معامِل الشريط العمودي أو الأنبوب pipe operator (|). يُقرأ الملف في الذاكرة من البداية إلى النهاية ثم تعالجه، عندما تطلب من البرنامج قراءة ملف بالطريقة التقليدية على سبيل المثال، لكن يمكنك قراءة الملف قطعةً تلو الأخرى باستخدام المجاري، ومعالجة محتواه دون الاحتفاظ به بالكامل في الذاكرة، إذ توفِّر وحدة نود stream الأساس الذي يُبنَى عليه جميع واجهات برمجة التطبيقات ذات المجرى، كما توفِّر المجاري ميزتَين رئيسيتَين باستخدام طرق معالجة البيانات الأخرى هما: فعالية الذاكرة Memory efficiency: لست بحاجة إلى تحميل كميات كبيرة من البيانات في الذاكرة قبل أن تكون قادرًا على معالجتها. فعالية الوقت Time efficiency: تستغرق وقتًا أقل لبدء معالجة البيانات بمجرد حصولك عليها، بدلًا من انتظار اكتمال حمولة البيانات للبدء. يوضِّح المثال التالي قراءة ملفات من القرص الصلب، حيث يمكنك باستخدام وحدة نود fs قراءة ملف وتقديمه عبر بروتوكول HTTP عند إنشاء اتصال جديد بخادم http: const http = require('http') const fs = require('fs') const server = http.createServer(function (req, res) { fs.readFile(__dirname + '/data.txt', (err, data) => { res.end(data) }) }) server.listen(3000) يقرأ التابع readFile() محتويات الملف الكاملة، ويستدعي دالة رد النداء callback function عند الانتهاء، بينما سيعيد التابع res.end(data) في دالة رد النداء محتويات الملف إلى عميل HTTP، فإذا كان الملف كبيرًا، فستستغرق العملية وقتًا طويلًا، ويمكن تطبيق الأمر نفسه باسخدام المجاري streams كما يلي: const http = require('http') const fs = require('fs') const server = http.createServer((req, res) => { const stream = fs.createReadStream(__dirname + '/data.txt') stream.pipe(res) }) server.listen(3000) يمكننا بث الملف عبر المجاري إلى عميل HTTP بمجرد أن يكون لدينا مجموعة كبيرة من البيانات جاهزة للإرسال بدلًا من انتظار قراءة الملف بالكامل؛ ويستخدم المثال السابق stream.pipe(res)، أي استدعاء تابع pipe() في مجرى الملف، حيث يأخذ هذا التابع المصدر، ويضخّه إلى وجهة معينة، كما يُستدعَى هذا التابع على مجرى المصدر، وبالتالي يُضَخ مجرى الملف إلى استجابة HTTP في هذه الحالة، وتكون القيمة المُعادة من التابع pipe() هي مجرى الوجهة، وهذا أمر ملائم للغاية لربط استدعاءات pipe() متعددة كما يلي: src.pipe(dest1).pipe(dest2) الذي يكافئ ما يلي: src.pipe(dest1) dest1.pipe(dest2) تتوفَّر واجهات برمجة تطبيقات API الخاصة بنود Node التي تعمل باستخدام المجاري Streams، إذ توفِّر العديد من وحدات Node.js الأساسية إمكانات معالجة المجرى الأصيلة، ومن أبرزها: process.stdin التي تعيد مجرًى متصلًا بمجرى stdin. process.stdout التي تعيد مجرًى متصلًا بمجرى stdout. process.stderr التي تعيد مجرًى متصلًا بمجرى stderr. fs.createReadStream() الذي ينشئ مجرًى قابلًا للقراءة إلى ملف. fs.createWriteStream() الذي ينشئ مجرًى قابلًا للكتابة إلى ملف. net.connect() الذي يبدأ اتصالًا قائمًا على مجرى. http.request() الذي يعيد نسخة من الصنف http.ClientRequest، وهو مجرى قابل للكتابة. zlib.createGzip() الذي يضغط البيانات باستخدام خوارزمية الضغط gzip في مجرى. zlib.createGunzip() الذي يفك ضغط مجرى gzip. zlib.createDeflate() الذي يضغط البيانات باستخدام خوارزمية الضغط deflate في مجرى. zlib.createInflate() الذي يفك ضغط مجرى deflate. أنواع المجاري المختلفة هناك أربع أصناف من المجاري هي: Readable: هو مجرى يمكن الضخ pipe منه ولكن لا يمكن الضخ إليه، أي يمكنك تلقي البيانات منه ولكن لا يمكنك إرسال البيانات إليه، فإذا دفعتَ بيانات إلى مجرى قابل للقراءة، فستُخزَّن مؤقتًا حتى يبدأ المستهلك في قراءة البيانات. Writable: هو مجرى يمكن الضخ إليه، ولكن لا يمكن الضخ منه، أي يمكنك إرسال البيانات إليه، ولكن لا يمكنك تلقي البيانات منه. Duplex: هو مجرى يمكن الضخ منه وإليه، أي هو مزيج من مجرى Readable ومجرى Writable. Transform: مجرى التحويل مشابه للمجرى Duplex، ولكن خرجه هو تحويل لدخله. كيفية إنشاء مجرى قابل للقراءة يمكن الحصول على مجرى قابل للقراءة من وحدة stream، كما يمكن تهيئته كما يلي: const Stream = require('stream') const readableStream = new Stream.Readable() ثم يمكننا إرسال البيانات إليه بعد تهيئته: readableStream.push('hi!') readableStream.push('ho!') كيفية إنشاء مجرى قابل للكتابة يمكنك إنشاء مجرى قابل للكتابة من خلال وراثة كائن Writable الأساسي وتطبيق تابعه _write(). أنشئ أولًا كائن Stream كما يلي: const Stream = require('stream') const writableStream = new Stream.Writable() ثم التابع _write كما يلي: writableStream._write = (chunk, encoding, next) => { console.log(chunk.toString()) next() } يمكنك الآن الضخ إلى مجرى قابل للقراءة كما يلي: process.stdin.pipe(writableStream) كيفية الحصول على بيانات من مجرى قابل للقراءة يمكنك قراءة البيانات من مجرًى قابل للقراءة باستخدام مجرى قابل للكتابة كما يلي: const Stream = require('stream') const readableStream = new Stream.Readable() const writableStream = new Stream.Writable() writableStream._write = (chunk, encoding, next) => { console.log(chunk.toString()) next() } readableStream.pipe(writableStream) readableStream.push('hi!') readableStream.push('ho!') كما يمكنك استهلاك مجرى قابل للقراءة مباشرةً باستخدام الحدث readable كما يلي: readableStream.on('readable', () => { console.log(readableStream.read()) }) كيفية إرسال بيانات إلى مجرى قابل للكتابة استخدم تابع المجرى write() كما يلي: writableStream.write('hey!\n') إعلام مجرى قابل للكتابة بانتهاء الكتابة استخدم التابع end() كما يلي: const Stream = require('stream') const readableStream = new Stream.Readable() const writableStream = new Stream.Writable() writableStream._write = (chunk, encoding, next) => { console.log(chunk.toString()) next() } readableStream.pipe(writableStream) readableStream.push('hi!') readableStream.push('ho!') writableStream.end() التعامل مع المجلدات توفِّر وحدة Node.js الأساسية fs توابعًا متعددةً مفيدةً يمكنك استخدامها للتعامل مع المجلدات. التحقق من وجود مجلد يُستخدَم التابع fs.access() للتحقق مما إذا كان المجلد موجودًا، ويمكن لنود الوصول إلى المجلد باستخدام أذوناته. إنشاء مجلد جديد يُستخدَم التابع fs.mkdir() أو التابع fs.mkdirSync() لإنشاء مجلد جديد. const fs = require('fs') const folderName = '/Users/flavio/test' try { if (!fs.existsSync(dir)){ fs.mkdirSync(dir) } } catch (err) { console.error(err) } قراءة محتوى مجلد يُستخدَم التابع fs.readdir() أو التابع fs.readdirSync لقراءة محتويات مجلد، ويقرأ جزء الشيفرة التالية محتوى مجلد من ملفات ومجلدات فرعية، ويعيد مساراتها النسبية: const fs = require('fs') const path = require('path') const folderPath = '/Users/flavio' fs.readdirSync(folderPath) يمكنك الحصول على المسار الكامل من خلال ما يلي: fs.readdirSync(folderPath).map(fileName => { return path.join(folderPath, fileName) } كما يمكنك تصفية النتائج لإعادة الملفات فقط واستبعاد المجلدات كما يلي: const isFile = fileName => { return fs.lstatSync(fileName).isFile() } fs.readdirSync(folderPath).map(fileName => { return path.join(folderPath, fileName)).filter(isFile) } إعادة تسمية مجلد يُستخدَم التابع fs.rename() أو التابع fs.renameSync() لإعادة تسمية مجلد، حيث يكون المعامِل الأول هو المسار الحالي، والمعامِل الثاني هو المسار الجديد: const fs = require('fs') fs.rename('/Users/flavio', '/Users/roger', (err) => { if (err) { console.error(err) return } //done }) كما يمكنك استخدام التابع fs.renameSync() الذي هو النسخة المتزامنة كما يلي: const fs = require('fs') try { fs.renameSync('/Users/flavio', '/Users/roger') } catch (err) { console.error(err) } إزالة مجلد يُستخدَم التابع fs.rmdir() أو التابع fs.rmdirSync() لإزالة مجلد، ويمكن أن تكون إزالة مجلد أكثر تعقيدًا إذا تضمّن محتوىً، لذلك نوصي في هذه الحالة بتثبيت وحدة fs-extra التي تحظى بشعبية ودعم كبيرَين، وهي بديل سريع لوحدة fs، وبالتالي تضيف مزيدًا من الميزات عليها، كما ستحتاج استخدام التابع remove(). ثبّت وحدة fs-extra باستخدام الأمر: npm install fs-extra، واستخدمها كما يلي: const fs = require('fs-extra') const folder = '/Users/flavio' fs.remove(folder, err => { console.error(err) }) كما يمكن استخدامها مع الوعود promises كما يلي: fs.remove(folder).then(() => { //done }).catch(err => { console.error(err) }) أو مع صيغة async/await كما يلي: async function removeFolder(folder) { try { await fs.remove(folder) //done } catch (err) { console.error(err) } } const folder = '/Users/flavio' removeFolder(folder) للمزيد، يمكنك الرجوع إلى توثيق التعامل مع نظام الملفات في Node.js في موسوعة حسوب. ترجمة -وبتصرّف- للفصل File System من كتاب The Node.js handbook لصاحبه Flavio Copes. اقرأ أيضًا المقال التالي: تعرف على وحدات Node.js الأساسية المقال السابق: التعامل مع الطلبيات الشبكية في Node.js التعامل مع الملفات في البرمجة مدخل إلى التعامل مع الملفات في جافا كيفية التعامل مع الملفات النصية في بايثون 3 التعامل مع الملفات النصية في لغة سي شارب #C
-
سنتعرّف من خلال هذا المقال على طريقة إرسال واستقبال الطلبيات بين الخادم والعميل عبر الشبكة في Node.js باستخدام مكتبة Axios، ولكن سنبدأ بشرح وسيلة التواصل الأساسية بين الخادم والمتصفح وهو بروتوكول HTTP، وسنتعرّف على بديل اتصال HTTP في تطبيقات الويب الذي هو مقابس الويب WebSockets. كيفية عمل بروتوكول HTTP يُعَدّ بروتوكول نقل النص الفائق Hyper Text Transfer Protocol -أو HTTP اختصارًا- أحد بروتوكولات تطبيق TCP/IP وهي مجموعة البروتوكولات التي تشغّل شبكة الإنترنت، إذ يُعَدّ البروتوكول الأنجح والأكثر شعبية على الإطلاق، كما يُشغِّل هذا البروتوكول شبكة الويب العالمية World Wide Web، مما يمنح المتصفحات لغة للتواصل مع الخوادم البعيدة التي تستضيف صفحات الويب. وُحِّد بروتوكول HTTP لأول مرة في عام 1991على أساس نتيجة لعمل تيم بيرنرز لي Tim Berners-Lee في المركز الأوروبي للأبحاث النووية European Center of Nuclear Research -أو CERN اختصارًا- منذ عام 1989، وكان الهدف هو السماح للباحثين بتبادل أبحاثهم بسهولة وربطهم ببعضهم بعضًا على أساس وسيلة تحسِّن عمل المجتمع العلمي، كما تكوّنت تطبيقات الإنترنت الرئيسية في ذلك الوقت من بروتوكول FTP أي بروتوكول نقل الملفات File Transfer Protocol والبريد الإلكتروني ونظام يوزنت Usenet أي مجموعات الأخبار newsgroups، ولكنها أصبحت غير مُستخدَمة حاليًا تقريبًا. صدر متصفح موزاييك Mosaic في عام 1993، وهو أول متصفح ويب رسومي، وتطورت الأمور عندها، إذ أصبح الويب التطبيق القاتل في شبكة الإنترنت، حيث سبّب ظهوره ضجةً كبيرةً، كما تطوّر الويب والنظام المجتمعي المحيط به تطوّرًا كبيرًا بمرور الوقت مع بقاء الأساسيات على حالها، وأحد الأمثلة على هذا التطور هو أنّ بروتوكول HTTP يشغّل حاليًا -بالإضافة إلى صفحات الويب- واجهات برمجة تطبيقات REST، وهي إحدى الطرق الشائعة للوصول إلى خدمة عبر الإنترنت برمجيًا. عُدِّل بروتوكول HTTP تعديلًا ثانويًا في عام 1997 في الإصدار HTTP/1.1، وخلفه الإصدار HTTP/2 الذي وُحِّد في عام 2015 ويُطبَّق الآن على خوادم الويب الرئيسية المُستخدَمة في جميع أنحاء العالم، كما يُعَدّ بروتوكول HTTP غير آمن مثل أيّ بروتوكول آخر غير مخدَّم عبر اتصال مشفَّر مثل بروتوكولات SMTP وFTP وغيرها، وهذا هو السبب في التوجّه الكبير حاليًا نحو استخدام بروتوكول HTTPS، وهو بروتوكول HTTP مخدَّم عبر بروتوكول TLS، ولكن بروتوكول HTTP هو حجر الأساس لبروتوكول HTTP/2 وHTTPS. مستندات HTML بروتوكول HTTP هو الطريقة التي تتواصل بها متصفحات الويب web browsers مثل Chrome وFirefox وEdge ومتصفحات أخرى سنسمّيها عملاء clients مع خوادم الويب web servers، كما اُشتق الاسم بروتوكول نقل النص الفائق Hyper Text Transfer Protocol من الحاجة إلى نقل الملفات كما هو الحال في بروتوكول FTP والذي يشير إلى بروتوكول نقل الملفات File Transfer Protocol، بالإضافة إلى النصوص الفائقة hypertexts التي ستُكتَب باستخدام لغة HTML، ثم تُمثَّل رسوميًا باستخدام المتصفح مع عرض جميل وروابط تفاعلية، وساهمت الروابط بقوة في اعتماد بروتوكول HTTP إلى جانب سهولة إنشاء صفحات ويب جديدة، حيث ينقل هذا البروتوكول ملفات النصوص الفائقة بالإضافة إلى الصور وأنواع الملفات الأخرى عبر الشبكة. الروابط والطلبيات يمكن أن يؤشّر مستند إلى مستند آخر باستخدام الروابط ضمن متصفح الويب، حيث يحدِّد جزء الرابط الأول كل من البروتوكول وعنوان الخادم من خلال إما اسم نطاق domain name أو عنوان IP، وليس هذا الجزء خاصًا ببروتوكول HTTP؛ أما الجزء الثاني فهو جزء المستند الذي يتبع جزء العنوان ويمثِّل مسار المستند مثل https://flaviocopes.com/http/ الذي يتكوّن مما يلي: https هو البروتوكول. flaviocopes.com هو اسم النطاق الذي يؤشر إلى الخادم. /http/ هو عنوان URL النسبي للمستند إلى مسار الخادم الجذر. يمكن أن يتداخل المسار مثل https://academy.hsoub.com/files/c5-programming/، حيث يكون عنوان URL للمستند هو /files/c5-programming؛ أما خادم الويب فيُعَدّ مسؤولًا عن تفسير الطلب وتقديم الاستجابة الصحيحة بعد تحليل الطلب، كما يمكن أن يكون الطلب عنوان URL الذي رأيناه سابقًا، فإذا أدخلنا عنوانًا وضغطنا Enter من لوحة المفاتيح في المتصفح، فسيرسل الخادم طلبًا في الخلفية إلى عنوان IP الصحيح مثل الطلب التالي: GET /a-page حيث /a-page هو عنوان URL الذي طلبته، كما يمكن أن يكون الطلب تابع HTTP ويُسمّى فعلًا verb أيضًا، حيث حدّد بروتوكول HTTP سابقًا ثلاثةً من هذه التوابع وهي: GET. POST. HEAD. وقدّم الإصدار HTTP/1.1 التوابع: PUT. DELETE. OPTIONS. TRACE. سنتحدّث عنها لاحقًا، وقد يكون الطلب مجموعة ترويسات HTTP، فالترويسات Headers هي مجموعة من أزواج المفتاح-القيمة key: value التي تُستخدَم للتواصل مع المعلومات الخاصة بالخادم المُحدَّدة مسبقًا ليتمكّن الخادم من فهم ما نعنيه، كما أنّ جميع الترويسات اختيارية باستثناء الترويسة Host. توابع HTTP أهم توابع HTTP هي: GET: هو التابع الأكثر استخدامًا، وهو الخيار الذي يُستخدَم عند كتابة عنوان URL في شريط عنوان المتصفح، أو عند النقر على رابط، كما يطلب هذا التابع من الخادم إرسال المورد المطلوب على أساس استجابة. HEAD: يتشابه هذا التابع مع التابع GET تمامًا، ولكن HEAD يخبر الخادم بعدم إرسال جسم الاستجابة response body، بل إرسال الترويسات فقط. POST: يستخدِم العميل هذا التابع لإرسال البيانات إلى الخادم، حيث يُستخدَم عادةً في النماذج forms مثلًا، وعند التفاعل مع واجهة برمجة تطبيقات REST. PUT: يهدف هذا التابع إلى إنشاء مورد في عنوان URL المحدَّد باستخدام المعاملات المُمرَّرة في جسم الطلب، كما يُستخدم استخدامًا رئيسيًا في واجهات برمجة تطبيقات REST. DELETE: يُستدعَى هذا التابع مع عنوان URL لطلب حذف المورد المقابل لهذا العنوان، كما يُستخدَم استخدامًا رئيسيًا في واجهات برمجة تطبيقات REST. OPTIONS: يجب أن يرسِل الخادم قائمة توابع HTTP المسموح بها إلى عنوان URL المحدَّد عندما يتلقى طلب OPTIONS. TRACE: يعيد هذا التابع إلى العميل الطلب المُستلَم، حيث يُستخدَم هذا التابع لتنقيح الأخطاء debugging أو لأغراض التشخيص. اتصال HTTP خادم/عميل بروتوكول HTTP هو بروتوكول عديم الحالة stateless مثل معظم البروتوكولات التي تنتمي إلى مجموعة بروتوكولات TCP/IP، حيث ليس لدى الخوادم أيّ فكرة عن حالة العميل الحالية، فكل ما يهم الخوادم هو أن تتلقى طلبات ثم تلبيتها، كما لا يكون لطلب مسبق أيّ معنى في هذا السياق، وبالتالي يمكن أن يكون خادم الويب سريعًا جدًا، مع وجود قليل من المعالجة وحيز نطاق تراسلي bandwidth مناسب لمعالجة كثير من الطلبات المتزامنة. يُعَدّ بروتوكول HTTP مرنًا واتصاله سريعًا جدًا اعتمادًا على حِمل الشبكة، وهذا يتناقض مع البروتوكولات الأكثر استخدامًا في وقت صدوره مثل TCP وPOP/SMTP وبروتوكولات البريد التي تتضمن كثيرًا من عمليات المصافحة handshaking والتأكيدات على النهايات المُستقبَلة، كما تجرِّد المتصفحات الرسومية هذا الاتصال، ولكن يمكن توضيحه كما يلي، إذ يبدأ سطر الرسالة الأول بتابع HTTP ثم مسار المَورد النسبي وإصدار البروتوكول كما يلي: GET /a-page HTTP/1.1 ثم يجب إضافة ترويسات طلبات HTTP، إذ توجد هناك ترويسات متعددة، ولكن الترويسة الإلزامية الوحيدة هي Host: GET /a-page HTTP/1.1 Host: flaviocopes.com يمكنك اختبار ذلك باستخدام أداة telnet، وهي أداة سطر أوامر تتيح لنا الاتصال بأي خادم وإرسال الأوامر إليه، والآن افتح طرفيتك terminal واكتب telnet flaviocopes.com 80 مثلًا، حيث سيؤدي ذلك إلى فتح طرفية تعرض ما يلي: Trying 178.128.202.129... Connected to flaviocopes.com. Escape character is '^]'. أنت الآن متصل بخادم الويب Netlify، ثم اكتب ما يلي: GET /axios/ HTTP/1.1 Host: flaviocopes.com اضغط بعد ذلك على زر Enter في سطر فارغ لتشغيل الطلب، وستكون الاستجابة كما يلي: HTTP/1.1 301 Moved Permanently Cache-Control: public, max-age=0, must-revalidate Content-Length: 46 Content-Type: text/plain Date: Sun, 29 Jul 2018 14:07:07 GMT Location: https://flaviocopes.com/axios/ Age: 0 Connection: keep-alive Server: Netlify Redirecting to https://flaviocopes.com/axios/ وهذه هي استجابة HTTP التي حصلنا عليها من الخادم، وهي طلب 301 Moved Permanently الذي يخبرنا بانتقال المَورد إلى موقع آخر انتقالًا دائمًا، وذلك لأننا اتصلنا بالمنفذ 80 وهو المنفذ الافتراضي لبروتوكول HTTP، ولكننا ضبطنا الخادم على إعادة التوجيه التلقائي إلى HTTPS، كما حُدِّد الموقع الجديد في ترويسة استجابة HTTP التي هي Location، وهناك ترويسات استجابة أخرى سنتحدث عنها لاحقًا، كما يفصل سطر فارغ ترويسة الطلب عن جسمه في كل من الطلب والاستجابة، حيث يحتوي جسم الطلب في مثالنا على السلسلة النصية التالية: Redirecting to https://flaviocopes.com/axios/ يبلغ طول هذه السلسلة النصية 46 بايتًا كما هو محدَّد في ترويسة Content-Length، إذ تظهر هذه السلسلة في المتصفح عند فتح الصفحة ريثما يُعاد توجيهك إلى الموقع الصحيح تلقائيًا، كما نستخدم أداة telnet في مثالنا، وهي أداة منخفضة المستوى يمكننا استخدامها للاتصال بأي خادم، لذلك لا يمكننا الحصول على أي نوع من إعادة التوجيه التلقائي، فلنتصل الآن بالمنفذ 443 وهو المنفذ الافتراضي لبروتوكول HTTPS، حيث لا يمكننا استخدام أداة telnet بسبب مصافحة SSL التي يجب أن تحدث،فولنستخدم الآن أداة curl وهي أداة سطر أوامر أخرى، إذ لا يمكننا كتابة طلب HTTP مباشرةً، لكننا سنرى الاستجابة: curl -i https://flaviocopes.com/axios/ سنحصل في المقابل على ما يلي: HTTP/1.1 200 OK Cache-Control: public, max-age=0, must-revalidate Content-Type: text/html; charset=UTF-8 Date: Sun, 29 Jul 2018 14:20:45 GMT Etag: "de3153d6eacef2299964de09db154b32-ssl" Strict-Transport-Security: max-age=31536000 Age: 152 Content-Length: 9797 Connection: keep-alive Server: Netlify <!DOCTYPE html> <html prefix="og: http://ogp.me/ns#" lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>HTTP requests using Axios</title> .... لن ينقل خادم HTTP ملفات HTML فقط، وإنما يمكنه نقل ملفات أخرى مثل ملفات CSS و JS و SVG و PNG و JPG وأنواع ملفات متعددة أخرى، إذ يعتمد ذلك على الإعداد، فبروتوكول HTTP قادر تمامًا على نقل هذه الملفات، وسيعرف العميل نوع الملف، وبالتالي سيفسرها بالطريقة الصحيحة، وهذه هي الطريقة التي يعمل بها الويب عند استرداد صفحة HTML بواسطة المتصفح، إذ تُفسَّر هذه الصفحة وأي مَورد آخر يحتاجه المتصفح لعرض خاصية (CSS و JavaScript والصور وغير ذلك) مُسترَدّة عبر طلبات HTTP إضافية إلى الخادم نفسه. بروتوكول HTTPS والاتصالات الآمنة يُعَدّ بروتوكول HTTPS امتدادًا لبروتوكول HTTP -أي بروتوكول نقل النص الفائق- والذي يوفِّر اتصالًا آمنًا، فبروتوكول HTTP غير آمن في تصميمه، فإذا فتحتَ متصفحك وطلبتَ من خادم الويب إرسال صفحة ويب لك، فستسير بياناتك ضمن رحلتين تكون الأولى من المتصفح إلى خادم الويب، والأخرى من خادم الويب إلى المتصفح، وقد تحتاج بعد ذلك إلى مزيد من الاتصالات -اعتمادًا على محتوى صفحة الويب- للحصول على ملفات CSS وملفات JavaScript والصور وما إلى ذلك، كما يمكن فحص بياناتك والتلاعب بها خلال مرورها في الشبكة أثناء أيّ من هذه الاتصالات. قد تكون العواقب وخيمةً، فقد يراقب ويسجّل طرف ثالث كل أنشطة شبكتك دون علمك، وقد تحقن بعض الشبكات إعلانات، وقد تكون عرضةً لهجوم الوسيط man-in-the-middle، وهو تهديد أمني يستطيع المهاجم من خلاله التلاعب ببياناتك وحتى انتحال شخصية حاسوبك عبر الشبكة، إذ يمكن لأي شخص الاستماع بسهولة إلى حزم HTTP المُرسَلة عبر شبكة واي فاي Wi-Fi عامة وغير مشفَّرة، حيث يهدف بروتوكول HTTPS إلى حل هذه المشكلة من خلال تشفير الاتصال الكامل بين متصفحك وخادم الويب. تُعَدّ كل الخصوصية والأمن مصدر قلق كبير في شبكة الإنترنت حاليًا، فقد كان الأمر مختلفًا قبل بضع سنوات، حيث كان بإمكانك توفير الأمن من خلال استخدام اتصال مشفر فقط في الصفحات المحمية بتسجيل الدخول أو أثناء عمليات الدفع في المتاجر الإلكترونية، كما أنّ معظم مواقع الويب قد استخدَمت بروتوكول HTTP بسبب أسعار شهادات SSL وتعقيداتها. يُعَدّ استخدام HTTPS إلزاميًا على جميع المواقع في الوقت الحالي، إذ يستخدِمه حاليًا أكثر من 50% من مواقع الويب، وقد بدأ Google Chrome مؤخرًا في تمييز مواقع HTTP بأنها غير آمنة، لمنحك سببًا وجيهًا في جعل بروتوكول HTTPS إلزاميًا على جميع مواقع الويب الخاصة بك. يكون منفذ الخادم الافتراضي هو 80 عند استخدام بروتوكول HTTP، في حين يكون 443 عند استخدام بروتوكول HTTPS، وليست إضافته بصورة صريحة أمرًا إلزاميًا إذا استخدَم الخادم المنفذ الافتراضي، كما يُطلق على بروتوكول HTTPS أحيانًا اسم HTTP عبر SSL أو HTTP عبر TLS، حيث يكون بروتوكول TLS خلَفًا لبروتوكول SSL؛ أما الشيء الوحيد غير المشفَّر عند استخدام بروتوكول HTTPS، فهو نطاق خادم الويب ومنفذ الخادم، بينما تُشفَّر كل المعلومات الأخرى بما في ذلك مسار المَورد والترويسات وملفات تعريف الارتباط cookies ومعامِلات الاستعلام. لن نشرح تفاصيل تحليل كيفية عمل بروتوكول TLS الداخلي، لكنك قد تعتقد أنه يضيف قدرًا كبيرًا من الحِمل على الشبكة، وربما هذا صحيح، إذ تتسبب أيّ عملية حسابية مُضافَة إلى معالجة موارد الشبكة في زيادة الحِمل على العميل والخادم وحجم الرُزم المرسَلة على حد سواء. يتيح HTTPS استخدام أحدث بروتوكول وهو HTTP/2 الذي يحتوي على ميزة إضافية يتفوق بها على الإصدار HTTP/1.1، وهذه الميزة هي أنه أسرع لعدة أسباب مثل ضغط الترويسة وتعدُّد الموارد، كما يمكن للخادم زجّ مزيد من الموارد عند طلب أحدها، فإذا طلب المتصفح صفحةً، فسيتلقى جميع الموارد اللازمة مثل الصور وملفات CSS وJS، كما يُعَدّ HTTP/2 تحسّنًا كبيرًا على HTTP/1.1 ويتطلب بروتوكول HTTPS، وهذا يعني أنّ HTTPS أسرع من HTTP بكثير إذا ضُبِط كل شيء ضبطًا صحيحًا باستخدام إعداد حديث على الرغم من وجود عبء التشفير الإضافي. كيفية عمل طلبات HTTP سنشرح ما يحدث عند كتابة عنوان URL في المتصفح من البداية إلى النهاية، حيث سنوضّح كيف تطبّق المتصفحات طلبات الصفحة باستخدام بروتوكول HTTP/1.1، إذ ذكرنا HTTP على وجه الخصوص لأنه يختلف عن اتصال HTTPS. إذا أجريت مقابلةً عمل من قبل، فقد تُسأَل ماذا يحدث عندما تكتب شيئًا ما في مربع بحث جوجل ثم تضغط مفتاح Enter؟ فهو أحد الأسئلة الأكثر شيوعًا التي ستُطرَح عليك، لمعرفة ما إذا كان بإمكانك شرح بعض المفاهيم الأساسية وما إذا كان لديك أيّ فكرة عن كيفية عمل الإنترنت، حيث سنحلّل ما يحدث عندما تكتب عنوان URL في شريط عنوان متصفحك ثم تضغط على Enter، وتُعَدّ هذه التقنية نادرة التغيّر وتشغّل أحد أكثر الأنظمة المجتمعية التي بناها الإنسان تعقيدًا واتساعًا. تحليل طلبات URL تملك المتصفحات الحديثة القدرة على معرفة ما إذا كان الشيء الذي كتبته في شريط العناوين هو عنوان URL فعلي أو مصطلح بحث، حيث سيستخدم المتصفح محرّك البحث الافتراضي إذا لم يكن عنوان URL صالحًا، فلنفترض أنك كتبتَ عنوان URL فعليًا، حيث ينشئ المتصفح أولًا عنوان URL الكامل عند إدخال العنوان ثم الضغط على مفتاح Enter، فإذا أدخلت نطاقًا مثل flaviocopes.com، فسيضيف المتصفح إلى بدايته HTTP:// افتراضيًا اعتمادًا على بروتوكول HTTP. مرحلة بحث DNS يبدأ المتصفح عملية بحث DNS للحصول على عنوان IP الخادم، ويُعَدّ اسم النطاق اختصارًا مفيدًا للبشر، ولكن الإنترنت منظَّم بطريقة تمكّن الحواسيب من البحث عن موقع الخادم الدقيق من خلال عنوان IP الخاص به، وهو عبارة عن مجموعة من الأعداد مثل 222.324.3.1 في الإصدار IPv4، حيث يتحقق المتصفح أولًا من ذاكرة DNS المخبئية المحلية، للتأكد من أن النطاق قد جرى تحليله resolved مؤخرًا، كما يحتوي كروم Chrome على عارض مفيد لذاكرة DNS المخبئية الذي يمكنك رؤيته من خلال chrome://net-internals/#dns، فإذا لم تعثر على أي شيء هناك، فهذا يعني استخدام المتصفح محلّل DNS عن طريق استدعاء نظام gethostbyname POSIX لاسترداد معلومات المضيف. gethostbyname يبحث استدعاء النظام gethostbyname أولًا في ملف المضيفِين hosts المحلي، والذي يوجد في نظامَي macOS أو لينكس Linux ضمن /etc/hosts، للتأكد من أن النظام يوفِّر المعلومات محليًا، فإذا لم يقدّم ملف المضيفِين المحلي أيّ معلومات عن النطاق، فسيقدّم النظام طلبًا إلى خادم DNS، حيث يُخزَّن عنوان خادم DNS في تفضيلات النظام، كما يُعَدّ الخادمان التاليان خادمي DNS شهيرين: 8.8.8.8: خادم DNS العام الخاص بجوجل. 1.1.1.1: خادم CloudFlare DNS. يستخدِم معظم الأشخاص خادم DNS الذي يوفِّره مزوّد خدمة الإنترنت الخاص بهم، كما يطبّق المتصفح طلب DNS باستخدام بروتوكول UDP، فالبروتوكولان TCP وUDP من بروتوكولات الشبكات الحاسوبية الأساسية ويتواجدان بالمستوى نفسه، لكن بروتوكول TCP موجَّه بالاتصال، بينما بروتوكول UDP عديم الاتصال وأخف، ويُستخدَم لإرسال الرسائل مع قليل من الحِمل على الشبكة. قد يحتوي خادم DNS على عنوان IP النطاق في الذاكرة المخبئية، فإذا لم يكن كذلك، فسيسأل خادم DNS الجذر، إذ يتكون هذا النظام من 13 خادم حقيقي موزع في أنحاء العالم، حيث يقود هذا النظام شبكة الإنترنت بأكملها، كما لا يعرف خادم DNS عنوان كل اسم نطاق على هذا الكوكب، ولكن يكفي معرفة مكان وجود محلّلي DNS من المستوى الأعلى، إذ يُعَدّ نطاق المستوى الأعلى top-level domain امتداد النطاق مثل .com و.it و.pizza وغير ذلك. يَعيد خادم DNS توجيه الطلب عند تلقّيه إلى خادم DNS الخاص بنطاق المستوى الأعلى TLD، ولنفترض أنك تبحث عن موقع flaviocopes.com، حيث يعيد خادم DNS الخاص بالنطاق الجذر عنوان IP الخاص بخادم نطاق المستوى الأعلى .com، ويخزّن بعدها محلّل DNS الخاص بنا عنوان IP لخادم نطاق المستوى الأعلى، بحيث لا يتعيّن عليه أن يسأل خادم DNS الجذر مرةً أخرى عنه. سيمتلك خادم DNS الخاص بنطاق المستوى الأعلى عناوين IP لخوادم الأسماء الرسمية الخاصة بالنطاق الذي نبحث عنه، إذ عند شرائك لنطاقٍ يرسل مسجل النطاق domain registrar نطاق المستوى الأعلى المناسب TDL إلى خوادم الأسماء.، فإذا حدّثتَ خوادم الأسماء عند تغيير مزود الاستضافة مثلًا، فسيُحدِّث مسجّل النطاق الخاص بك هذه المعلومات تلقائيًا، ونوضِّح فيما يلي أمثلةً عن خوادم DNS لمزود الاستضافة التي تكون أكثر من خادم عادةً لاستخدامها على أساس نسخة احتياطية: ns1.dreamhost.com ns2.dreamhost.com ns3.dreamhost.com يبدأ محلل DNS بالخادم الأول، ويحاول طلب عنوان IP الخاص بالنطاق مع النطاق الفرعي أيضًا الذي تبحث عنه، وهو المصدر النهائي لعنوان IP. إنشاء اتصال/مصافحة handshaking طلب TCP يمكن للمتصفح الآن بدء اتصال TCP عند توفر عنوان IP الخادم، حيث يتطلب اتصال TCP عملية مصافحة handshaking قبل تهيئته بالكامل والبدء بإرسال البيانات، إذ يمكننا إرسال الطلب بعد إنشاء الاتصال. إرسال الطلب يكون الطلب عبارةً عن مستند نصي منظَّم بطريقة دقيقة يحدّدها بروتوكول الاتصال، ويتكون من 3 أجزاء هي: سطر الطلب request line. ترويسة الطلب request header. جسم الطلب request body. يضبط سطر الطلب ما يلي في سطر واحد: تابع HTTP. موقع المَورد. إصدار البروتوكول. إليك المثال التالي: GET / HTTP/1.1 تتكون ترويسة الطلب من مجموعة من أزواج الحقل-القيمة field: value التي تحدِّد قيمًا معينةً، وهناك حقلان إلزاميان هما Host وConnection، بينما جميع الحقول الأخرى اختيارية: Host: flaviocopes.com Connection: close يشير الحقل Host إلى اسم النطاق الذي نريد الوصول إليه، بينما يُضبَط الحقل Connection على القيمة close دائمًا إلّا في حالة إبقاء الاتصال مفتوحًا، وبعض حقول الترويسة الأكثر استخدامًا هي: Origin Accept Accept-Encoding Cookie Cache-Control Dnt وهناك غيرها الكثير، ويُنهَى جزء الترويسة بسطر فارغ. أما جسم الطلب فهو اختياري ولا يُستخدَم في طلبات GET، ولكنه يُستخدَم بكثرة في طلبات POST وفي أفعال أخرى في بعض الأحيان، كمايمكن أن يحتوي على بيانات بتنسيق JSON، وبما أننا الآن نحلّل طلب GET، فإن الجسم فارغ. الاستجابة Response يعالِج الخادم الطلب بعد إرساله ويرسل استجابةً، حيث تبدأ الاستجابة برمز الحالة status code ورسالة الحالة status message، فإذا كان الطلب ناجحًا ويعيد القيمة 200، فستبدأ الاستجابة بما يلي: 200 OK قد يعيد الطلب رمز ورسالة حالة مختلفَين مثل الأمثلة التالية: 404 Not Found 403 Forbidden 301 Moved Permanently 500 Internal Server Error 304 Not Modified 401 Unauthorized تحتوي الاستجابة بعد ذلك على قائمة بترويسات HTTP وجسم الاستجابة الذي سيكون HTML لأننا ننفّذ الطلب في المتصفح. تحليل HTML تلقّى المتصفح الآن ملف HTML وبدأ في تحليله، وسيكرّر العملية نفسها بالضبط على جميع الموارد التي تطلبها الصفحة مثل: ملفات CSS. الصور. الأيقونة المفضلة أو رمز الموقع favicon. ملفات جافا سكريبت. وغير ذلك. الطريقة التي تصيّر render بها المتصفحاتُ الصفحةَ خارج نطاق مناقشتنا، ولكن يجب فهم أن العملية التي شرحناها غير مقتصرة على صفحات HTML فقط، بل يمكن تطبيقها على أيّ عنصر مُقدَّم عبر بروتوكول HTTP. بناء خادم HTTP باستخدام Node.js خادم ويب HTTP الذي سنستخدِمه هو الخادم نفسه الذي استخدمناه سابقًا مثل تطبيق Node Hello World. const http = require('http') const port = 3000 const server = http.createServer((req, res) => { res.statusCode = 200 res.setHeader('Content-Type', 'text/plain') res.end('Hello World\n') }) server.listen(port, () => { console.log(`Server running at http://${hostname}:${port}/`) }) لنحلّل المثال السابق بإيجاز: ضمّنا وحدة http التي نستخدمها لإنشاء خادم HTTP، وضُبِط الخادم للاستماع على المنفذ المحدَّد 3000، حيث تُستدعَى دالة رد النداء listen عندما يكون الخادم جاهزًا، فدالة رد النداء التي نمررها هي الدالة التي ستُنفَّذ عند وصول كل طلب، ويُستدَعى حدث request عند تلقّي طلب جديد، مما يوفّر كائنين هما: طلب (كائن http.IncomingMessage) واستجابة ( كائن http.ServerResponse). يوفّر الطلب request تفاصيل الطلب، حيث نصل من خلاله إلى ترويسات الطلبات وبياناتها؛ أما الاستجابة response فتُستخدَم لتوفير البيانات التي سنعيدها إلى العميل، كما ضبطنا خاصية statusCode على القيمة 200 في مثالنا، للإشارة إلى استجابة ناجحة. res.statusCode = 200 وضبطنا ترويسة Content-Type كما يلي: res.setHeader('Content-Type', 'text/plain') ثم أغلقنا الاستجابة في النهاية بإضافة المحتوى على أساس وسيط للتابع end(): res.end('Hello World\n') إجراء طلبات HTTP سنشرح كيفية إجراء طلبات HTTP في Node.js باستخدام GET و POST و PUT و DELETE. إجراء طلب GET const https = require('https') const options = { hostname: 'flaviocopes.com', port: 443, path: '/todos', method: 'GET' } const req = https.request(options, (res) => { console.log(`statusCode: ${res.statusCode}`) res.on('data', (d) => { process.stdout.write(d) }) }) req.on('error', (error) => { console.error(error) }) req.end() إجراء طلب POST const https = require('https') const data = JSON.stringify({ todo: 'Buy the milk' }) const options = { hostname: 'flaviocopes.com', port: 443, path: '/todos', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': data.length } } const req = https.request(options, (res) => { console.log(`statusCode: ${res.statusCode}`) res.on('data', (d) => { process.stdout.write(d) }) }) req.on('error', (error) => { console.error(error) }) req.write(data) req.end() PUT وDELETE تستخدِم طلبات PUT وDELETE تنسيق طلب POST نفسه مع تغيير قيمة options.method فقط. مكتبة Axios تُعَدّ Axios مكتبة جافاسكربت يمكنك استخدامها لإجراء طلبات HTTP، وتعمل في المنصَّتين المتصفح Browser ونود Node.js. تدعم هذه المكتبة جميع المتصفحات الحديثة بما في ذلك الإصدار IE8 والإصدارات الأحدث، كما تستند على الوعود، وهذا يتيح لنا كتابة شيفرة صيغة عدم التزامن/الانتظار async/await لإجراء طلبات XHR بسهولة، كما يتمتع استخدام مكتبة Axios ببعض المزايا بالموازنة مع واجهة Fetch API الأصيلة، وهذه المزايا هي: تدعم المتصفحات القديمة، حيث تحتاج Fetch إلى تعويض نقص دعم المتصفحات polyfill. لديها طريقة لإبطال طلب. لديها طريقة لضبط مهلة الاستجابة الزمنية. تحتوي على حماية CSRF مبنية مسبقًا. تدعم تقدّم التحميل. تجري تحويل بيانات JSON تلقائيًا. تعمل في Node.js. تثبيت Axios يمكن تثبيت Axios باستخدام npm: npm install axios أو باستخدام yarn: yarn add axios أو يمكنك تضمينها ببساطة في صفحتك باستخدام unpkg.com كما يلي: <script src="https://unpkg.com/axios/dist/axios.min.js"></script> واجهة برمجة تطبيقات Axios يمكنك بدء طلب HTTP من كائن axios: axios({ url: 'https://dog.ceo/api/breeds/list/all', method: 'get', data: { foo: 'bar' } }) لكنك ستستخدم التوابع التالية كما هو الحال في jQuery، حيث يمكنك استخدام $.get() و$.post() بدلًا من $.ajax(): axios.get() axios.post() توفِّر مكتبة Axios توابعًا لجميع أفعال HTTP، والتي تُعَدّ أقل شيوعًا ولكنها لا تزال مُستخدَمة: axios.delete() axios.put() axios.patch() axios.options() axios.head(): وهو تابع يُستخدَم للحصول على ترويسات HTTP لطلب ما مع تجاهل الجسم. إرسال واستقبال الطلبات إحدى الطرق الملائمة لاستخدام مكتبة Axios هي استخدام صيغة async/await الحديثة في الإصدار ES2017، حيث يستعلم مثال Node.js التالي عن واجهة Dog API لاسترداد قائمة بجميع سلالات الكلاب dogs breeds باستخدام التابع axios.get()، ويحصي هذه السلالات: const axios = require('axios') const getBreeds = async () => { try { return await axios.get('https://dog.ceo/api/breeds/list/all') } catch (error) { console.error(error) } } const countBreeds = async () => { const breeds = await getBreeds() if (breeds.data.message) { console.log(`Got ${Object.entries(breeds.data.message).length} breeds`) } } countBreeds() إذا لم ترغب في استخدام صيغة async/await، فيمكنك استخدام صيغة الوعود Promises: const axios = require('axios') const getBreeds = () => { try { return axios.get('https://dog.ceo/api/breeds/list/all') } catch (error) { console.error(error) } } const countBreeds = async () => { const breeds = getBreeds() .then(response => { if (response.data.message) { console.log( `Got ${Object.entries(response.data.message).length} breeds` ) } }) .catch(error => { console.log(error) }) } countBreeds() يمكن أن تحتوي استجابة GET على معامِلات في عنوان URL مثل https://site.com/?foo=bar، حيث يمكنك تطبيق ذلك في مكتبة Axios عن طريق استخدام عنوان URL كما يلي: axios.get('https://site.com/?foo=bar') أو يمكنك استخدام خاصية params في الخيارات كما يلي: axios.get('https://site.com/', { params: { foo: 'bar' } }) يشبه إجراء طلب POST تمامًا إجراء طلب GET مع استخدم axios.post بدلًا من axios.get: axios.post('https://site.com/') الكائن الذي يحتوي على معامِلات POST هو الوسيط الثاني: axios.post('https://site.com/', { foo: 'bar' }) مقابس الويب Websockets مقابس الويب WebSockets هي بديل لاتصال HTTP في تطبيقات الويب، إذ توفِّر قناة اتصال ثنائية الاتجاه طويلة الأمد بين العميل والخادم، كما تبقى القناة مفتوحة بمجرد إنشائها، مما يوفر اتصالًا سريعًا جدًا مع زمن انتقال وحِمل منخفضَين، كما تدعم جميع المتصفحات الحديثة مقابس WebSockets. قد تتساءل، ما وجه الاختلاف بين WebSockets وبين HTTP؟ حسنًا، يُعَدّ HTTP بروتوكولًا وطريقة تواصل مختلفة تمامًا، فهو بروتوكول طلب/استجابة request/response، إذ يعيد الخادم البيانات التي يطلبها العميل، بينما تفيد مقابس WebSockets فيما يلي: يمكن للخادم إرسال رسالة إلى العميل دون أن يطلب العميل صراحةً شيئًا ما. يمكن للعميل والخادم التحدث مع بعضهما البعض في الوقت نفسه. في حالة تبادل كمية قليلة من البيانات الإضافية لإرسال الرسائل، وهذا يعني اتصالًا ذو زمن انتقال منخفض. تُعَدّ مقابس WebSockets مناسبةً للاتصالات طويلة الأمد في الوقت الحقيقي، بينما يُعَدّ بروتوكول HTTP مفيدًا لتبادل البيانات والتفاعلات المؤقتة التي يبدأها العميل، كما يُعَدّ بروتوكول HTTP أبسط بكثير في التطبيق، بينما تتطلب مقابس WebSockets مزيدًا من العبء الإضافي. مقابس الويب الآمنة استخدم دائمًا البروتوكول الآمن والمشفّر لمقابس الويب أي wss://، ويشير ws:// إلى إصدار مقابس WebSockets غير الآمن -مثل http:// في مقابس WebSockets- الذي يجب تجنبه. إنشاء اتصال WebSockets جديد إليك المثال التالي: const url = 'wss://myserver.com/something' const connection = new WebSocket(url) يُعَدّ connection كائن WebSocket، كما يُشغَّل حدث open عند إنشاء الاتصال بنجاح، ويمكنك الاستماع إلى الاتصال عن طريق إسناد دالة رد نداء callback إلى خاصية onopen الخاصة بكائن connection كما يلي: connection.onopen = () => { //... } إذا كان هناك أي خطأ، فستُشغَّل دالة رد النداء onerror كما يلي: connection.onerror = error => { console.log(`WebSocket error: ${error}`) } إرسال البيانات إلى الخادم باستخدام WebSockets يمكنك إرسال البيانات إلى الخادم بمجرد فتح الاتصال، حيث يمكنك إرسال البيانات بسهولة ضمن دالة رد النداء onopen كما يلي: connection.onopen = () => { connection.send('hey') } استقبال البيانات من الخادم باستخدام WebSockets استمع إلى الاتصال باستخدام دالة رد النداء onmessage التي تُستدعَى عند تلقي حدث message كما يلي: connection.onmessage = e => { console.log(e.data) } تطبيق خادم WebSockets في Node.js تُعَدّ مكتبة ws مكتبة WebSockets شائعةً ومُستخدَمةً مع Node.js، كما سنستخدمها لبناء خادم WebSockets، ويمكن استخدامها أيضًا لتطبيق العميل مع استخدام مقابس WebSockets للتواصل بين خدمَتين من الخدمات الخلفية، كما يمكنك تثبيت هذه المكتبة بسهولة باستخدام الأمر التالي: yarn init yarn add ws ليست الشيفرة التي تحتاج إلى كتابتها كبيرةً كما يلي: const WebSocket = require('ws') const wss = new WebSocket.Server({ port: 8080 }) wss.on('connection', ws => { ws.on('message', message => { console.log(`Received message => ${message}`) }) ws.send('ho!') }) تُنشئ الشيفرة السابقة خادمًا جديدًا على المنفذ 8080 وهو المنفذ الافتراضي لمقابس الويب WebSockets-، وتضيف دالة رد نداء عند إنشاء اتصال، مما يؤدي إلى إرسال ho! إلى العميل، وتسجيل الرسائل التي يتلقاها. شاهد مثالًا حيًا لخادم مقابس الويب WebSockets ومثالًا حيًا لعميل WebSockets يتفاعل مع الخادم على Glitch. ترجمة -وبتصرّف- للفصل Networking من كتاب The Node.js handbook لصاحبه Flavio Copes. اقرأ أيضًا المقال التالي: التعامل مع الملفات في Node.js المقال السابق: البرمجة غير المتزامنة في Node.js تطبيقات الشبكات الحاسوبية: البريد الإلكتروني طبقة النقل في بروتوكول TCP/IP استكشاف عملية توصيل الرزم عند بناء الشبكات
-
تُعَدّ الحواسيب غير متزامنةً في تصميمها، ويعني المصطلح غير متزامن Asynchronous أنّ الأشياء يمكن حدوثها حدوثًا مستقلًا عن تدفق البرنامج الرئيسي، إذ يُشغَّل كل برنامج لفتحة زمنية محدَّدة في الحواسيب الاستهلاكية الحالية، ثم يتوقف تنفيذه للسماح لبرنامج آخر بمواصلة التنفيذ، حيث تجري هذه العملية ضمن دورة سريعة جدًا بحيث لا يمكن ملاحظتها، وبالتالي نعتقد أن الحواسيب تشغّل برامجًا متعددةً في الوقت نفسه، لكن ذلك وهم باستثناء الأجهزة متعددة المعالجات. تستخدم البرامج المقاطعات interrupts داخليًا، فالمقاطعة هي إشارة تنبعث من المعالج لجذب انتباه النظام، ولن نخوض في التفاصيل الداخلية، ولكن ضع في بالك أن عدم تزامن البرامج أمرٌ طبيعي، إذ توقف تنفيذها إلى أن تتنبّه مرةً أخرى، بحيث يمكن للحاسوب تنفيذ أشياء أخرى في هذه الأثناء، فإذا انتظر برنامج استجابةً من الشبكة، فلا يمكن إيقاف المعالج إلى أن ينتهي الطلب. تكون لغات البرمجة متزامنةً عادةً، وتوفِّر بعضها طريقةً لإدارة عدم التزامن في اللغة نفسها أو من خلال المكتبات، فاللغات C و Java و C# و PHP و Go و Ruby و Swift و Python متزامنة افتراضيًا، كما تعالجِ بعضها عدم التزامن باستخدام الخيوط threads، مما ينتج عنه عملية جديدة، فلغة جافاسكربت متزامنة افتراضيًا وتعمل على خيط وحيد، وهذا يعني أنّ الشيفرة لا يمكنها إنشاء خيوط جديدة وتشغيلها على التوازي، إذ تُنفَّذ سطور الشيفرة تسلسليًا سطرًا تلو الآخر كما في المثال التالي: const a = 1 const b = 2 const c = a * b console.log(c) doSomething() نشأت جافاسكربت داخل المتصفح، وكانت وظيفتها الرئيسية في البداية الاستجابة لإجراءات المستخدِم مثل onClick وonMouseOver وonChange وonSubmit وما إلى ذلك، ولكن بيئتها ساعدتها في التعامل مع نمط البرمجة المتزامن من خلال المتصفح الذي يوفّر مجموعةً من واجهات برمجة التطبيقات APIs التي يمكنها التعامل مع هذا النوع من العمليات، كما قدّم Node.js في الآونة الأخيرة بيئة إدخال/إخراج دون توقف لتوسيع هذا المفهوم ليشمل الوصول إلى الملفات واستدعاءات الشبكة وغير ذلك. دوال رد النداء Callbacks لا يمكنك معرفة الوقت الذي سينقر فيه المستخدِم على زر، لذلك تعرِّف معالج أحداث لحدث النقر الذي يقبل دالةً تُستدعَى عند بدء الحدث كما يلي: document.getElementById('button').addEventListener('click', () => { //نُقِر العنصر }) وهذا ما يسمى دالة رد النداء، وهي دالة بسيطة تُمرَّر على أساس قيمة إلى دالة أخرى وستُنفَّذ عند وقوع الحدث فقط، إذ يمكن ذلك لأن للغة جافاسكربت دوالًا من الصنف الأول، والتي يمكن إسنادها للمتغيرات وتمريرها إلى دوال أخرى تسّمى دوال الترتيب الأعلى higher-order functions، كما تُغلَّف شيفرة العميل في مستمع حدث load على الكائن window الذي يشغِّل دالة رد النداء عندما تكون الصفحة جاهزة فقط مثل المثال التالي: window.addEventListener('load', () => { //حُمِّلت الصفحة //افعل ما تريده }) تُستخدَم دوال رد النداء في كل مكان، ولا تقتصر على أحداث DOM فقط، فأحد الأمثلة الشائعة هو استخدام المؤقتات: setTimeout(() => { // تشغيل بعد 2 ثانية }, 2000) تقبَل طلبات XHR دالة رد نداء عن طريق إسناد دالة لخاصية في المثال التالي، إذ ستُستدعَى هذه الدالة عند وقوع حدث معيّن -أي حدث تغيّرات حالة الطلب في مثالنا-: const xhr = new XMLHttpRequest() xhr.onreadystatechange = () => { if (xhr.readyState === 4) { xhr.status === 200 ? console.log(xhr.responseText) : console.error('error') } } xhr.open('GET', 'https://yoursite.com') xhr.send() معالجة الأخطاء في دوال رد النداء تتمثَّل إحدى الإستراتيجيات الشائعة جدًا في استخدام ما يعتمده Node.js وهو المعامل الأول في أيّ دالة رد نداء هي كائن الخطأ، وبالتالي تُسمّى دوال رد النداء مع معامل الأخطاء الأول error-first callbacks، فإذا لم يكن هناك خطأً، فستكون قيمة الكائن null، وإذا كان هناك خطأ، فسيحتوي هذا الكائن وصفًا للخطأ ومعلومات أخرى. fs.readFile('/file.json', (err, data) => { if (err !== null) { //عالِج الخطأ console.log(err) return } //لا يوجد خطأ، إذَا عالِج البيانات console.log(data) }) مشكلة دوال رد النداء تُعَدّ دوال رد النداء رائعةً في الحالات البسيطة، ولكن تضيف كل دالة رد نداء مستوىً من التداخل nesting، وبالتالي تتعقَّد الشيفرة بسرعة كبيرة عند وجود كثير من دوال رد النداء كما يلي: window.addEventListener('load', () => { document.getElementById('button').addEventListener('click', () => { setTimeout(() => { items.forEach(item => { //أضِف شيفرتك هنا }) }, 2000) }) }) تُعَدّ الشيفرة السابقة بسيطةً، إذ تتألف من 4 مستويات فقط، لكنك قد تصادف مستويات أكثر بكثير من التداخل وبالتالي سيزداد تعقيد الشيفرة. بدائل دوال رد النداء قدّمت جافاسكربت بدءًا من الإصدار ES6 ميزات متعددةً تساعدنا في التعامل مع الشيفرة غير المتزامنة التي لا تتضمن استخدام دوال رد النداء مثل: الوعود Promises في الإصدار ES6. صيغة عدم التزامن/الانتظار Async/Await في الإصدار ES8. الوعود Promises الوعود هي إحدى طرق التعامل مع الشيفرات غير المتزامنة في جافاسكربت دون كتابة كثير من دوال رد النداء في الشيفرة. مدخل إلى الوعود يُعرَّف الوعد Promise عمومًا على أنه وكيل لقيمة ستتوفر في وقت لاحق، فالوعود موجودة منذ سنوات، لكن وُحِّدت وقُدِّمت في الإصدار ES2015، واُستبدِلت دوال عدم التزامن Async functions في الإصدار ES2017 بها والتي تستخدِم واجهة برمجة تطبيقات الوعود أساسًا لها، لذلك يُعَدّ فهم الوعود أمرًا أساسيًا حتى في حالة استخدام دوال عدم التزامن في الشيفرة الأحدث عوضًا عن الوعود، وإليك شرح مختصر عن كيفية عمل الوعود. يبدأ الوعد عند استدعائه في حالة انتظار pending state، أي أن الدالة المستدعِية تواصل التنفيذ في الوقت الذي تنتظر به الوعد لينفّذ معالجته الخاصة ويستجيب لها، حيث تنتظر الدالة المستدعِية إما إعادة الوعد في حالة التأكيد أو الحل resolved state أو في حالة الرفض rejected state، ولكن لغة جافاسكربت غير متزامنة، لذلك تتابع الدالة تنفيذها ريثما ينتهي الوعد من عمله، كما تستخدِم واجهات برمجة تطبيقات الويب المعيارية الحديثة الوعود بالإضافة إلى شيفرتك ومكتباتها، ومن هذه الواجهات البرمجية: Battery API. Fetch API. Service Workers. ستستخدَم الوعود بالتأكيد في جافاسكربت الحديثة، لذلك يجب فهمها جيدًا. إنشاء وعد تُظهِر واجهة برمجة الوعد Promise API باني وعد Promise constructor يمكن تهيئته باستخدام الدالة new Promise(): let done = true const isItDoneYet = new Promise( (resolve, reject) => { if (done) { const workDone = 'Here is the thing I built' resolve(workDone) } else { const why = 'Still working on something else' reject(why) } } ) يتحقّق الوعد من الثابت العام done، فإذا كانت قيمته صحيحة true، فإننا نعيد قيمة وعد مؤكَّد، وإلا فسنعيد وعدًا مرفوضًا، كما يمكننا إعادة قيمة باستخدام القيم resolve وreject، حيث أعدنا سلسلةً نصيةً فقط في المثال السابق، لكنها يمكن أن تكون كائنًا أيضًا. استهلاك وعد لنرى الآن كيفية استهلاك أو استخدام وعد. const isItDoneYet = new Promise( //... ) const checkIfItsDone = () => { isItDoneYet .then((ok) => { console.log(ok) }) .catch((err) => { console.error(err) }) } سيؤدي تشغيل الدالة checkIfItsDone() إلى تنفيذ الوعد isItDoneYet() وستنتظر إلى أن يُؤكَّد الوعد باستخدام دالة رد النداء then، وإذا كان هناك خطأ، فستعالجه في دالة رد النداء catch. إذا أردت مزامنة وعود مختلفة، فسيساعدك التابع Promise.all() على تحديد قائمة وعود، وتنفيذ شيء ما عند تأكيد هذه الوعود جميعها، وإليك المثال التالي: const f1 = fetch('/something.json') const f2 = fetch('/something2.json') Promise.all([f1, f2]).then((res) => { console.log('Array of results', res) }) .catch((err) => { console.error(err) }) تتيح لك صيغة إسناد الهدم destructuring assignment syntax الخاصة بالإصدار ES2015 تنفيذ ما يلي: Promise.all([f1, f2]).then(([res1, res2]) => { console.log('Results', res1, res2) }) ليس الأمر مقتصرًا على استخدام fetch بالطبع، إذ يمكنك استخدام أيّ وعد، كما يُشغَّل التابع Promise.race() عند تأكيد أول وعد من الوعود التي تمرّرها إليه، ويشغِّل دالةَ رد النداء المصاحبة للوعد مرةً واحدةً فقط مع نتيجة الوعد الأول المُؤكَّد resolved، وإليك المثال التالي: const first = new Promise((resolve, reject) => { setTimeout(resolve, 500, 'first') }) const second = new Promise((resolve, reject) => { setTimeout(resolve, 100, 'second') }) Promise.race([first, second]).then((result) => { console.log(result) // second }) سلسلة الوعود Chaining promises يمكن أن يُعاد وعدٌ إلى وعد آخر، وبالتالي ستنشأ سلسلة من الوعود، إذ تقدِّم واجهة Fetch API -وهي طبقة فوق واجهة برمجة تطبيقات XMLHttpRequest- مثالًا جيدًا عن سلسلة وعود، إذ يمكننا استخدام هذه الواجهة للحصول على مورد ووضع سلسلة من الوعود في طابور لتنفيذها عند جلب المورد، كما تُعَدّ واجهة Fetch API آليةً قائمةً على الوعود، حيث يكافئ استدعاءُ الدالة fetch() تعريف وعد باستخدام new Promise()، وإليك المثال التالي عن كيفية سلسلة الوعود: const status = (response) => { if (response.status >= 200 && response.status < 300) { return Promise.resolve(response) } return Promise.reject(new Error(response.statusText)) } const json = (response) => response.json() fetch('/todos.json') .then(status) .then(json) .then((data) => { console.log('Request succeeded with JSON response', data) }) .catch((error) => { console.log('Request failed', error) }) نستدعي في المثال السابق التابع fetch() للحصول على قائمة من عناصر TODO من ملف todos.json الموجود في نطاق الجذر، وننشئ سلسلة من الوعود، كما يعيد تشغيل التابع fetch() استجابةً لها خاصيات منها: status وهي قيمة عددية تمثِّل رمز حالة HTTP. statusText وهي رسالة حالة تكون قيمتها OK إذا نجح الطلب. تحتوي الاستجابة response أيضًا على تابع json() الذي يعيد وعدًا سيُؤكد ويُربَط مع محتوى الجسم المُعالَج والمُحوَّل إلى JSON. الوعد الأول في السلسلة هو الدالة التي حدّدناها وهي status() التي تتحقق من حالة الاستجابة، فإذا لم تكن استجابةً ناجحةً -أي قيمتها بين 200 و299، فسترفِض الوعد، إذ ستؤدي هذه العملية إلى تخطي جميع الوعود المتسلسلة المدرجَة في سلسلة الوعود وستنتقل مباشرةً إلى تعليمة catch() في الأسفل، مما يؤدي إلى تسجيل نص فشل الطلب Request failed مع رسالة الخطأ؛ أما إذا نجحت الاستجابة، فستُستدعَى دالة json() التي حدّدناها، وبما أنّ الوعد السابق يعيد كائن الاستجابة response عند النجاح، فسنحصل عليه على أساس دخل للوعد الثاني، وبالتالي نُعيد بيانات JSON المُعالَجة في هذه الحالة، لذا فإن الوعد الثالث يتلقى JSON مباشرةً مع تسجيله ببساطة في الطرفية كما يلي: .then((data) => { console.log('Request succeeded with JSON response', data) }) معالجة الأخطاء ألحقنا في المثال السابق تعليمة catch بسلسلة وعود، فإذا فشل أيّ شيء في سلسلة الوعود مسببًا خطأً أو رفض وعد، فسينتقل التحكم إلى أقرب تعلمية catch() أسفل السلسلة. new Promise((resolve, reject) => { throw new Error('Error') }) .catch((err) => { console.error(err) }) // أو new Promise((resolve, reject) => { reject('Error') }) .catch((err) => { console.error(err) }) إذا ظهر خطأ ضمن تعليمة catch()، فيمكنك إلحاق تعليمة catch() ثانية لمعالجة الخطأ وهلم جرًا وهذا ما يسمى بعملية معالجة توريث الأخطاء Cascading errors. new Promise((resolve, reject) => { throw new Error('Error') }) .catch((err) => { throw new Error('Error') }) .catch((err) => { console.error(err) }) إذا ظهر الخطأ Uncaught TypeError: undefined is not a promise في الطرفية، فتأكد من استخدام new Promise() بدلًا من استخدام Promise(). صيغة عدم التزامن/الانتظار async/await تطوّرت لغة جافاسكربت في وقت قصير جدًا من دوال رد النداء callbacks إلى الوعود promises في الإصدار ES2015، وأصبحت لغة جافاسكربت غير المتزامنة منذ الإصدار ES2017 أبسط مع صيغة عدم التزامن/الانتظار async/await، فالدوال غير المتزامنة هي مزيج من الوعود والمولِّدات generators، وهي في الأساس ذات مستوىً أعلى من الوعود من ناحية التجريد، فصيغة async/await مبنية على الوعود. سبب ظهور صيغة async/await هو أنها تقلل من الشيفرة التكرارية أو المتداولة boilerplate الموجودة في الوعود، وتقلّل من محدودية قيود عدم كسر السلسلة في تسلسل الوعود، فقد كان الهدف من تقديم الوعود في الإصدار ES2015 حلَّ مشكلة التعامل مع الشيفرة غير المتزامنة، وقد حلّت هذه المشكلة حقًا، ولكن كان واضحًا على مدار العامين اللذين فصلا بين الإصدارين ES2015 وES2017 أن الوعود ليست الحل النهائي. اُستخدِمت الوعود لحل مشكلة جحيم دوال رد النداء callback hell الشهيرة، لكنها أدخلت التعقيد فيها بالإضافة إلى تعقيد الصيغ، وقد كانت عناصرًا أوليةً جيدةً يمكن من خلالها إظهار صيغة أفضل للمطورين، لذلك حصلنا على دوال غير متزامنة في الوقت المناسب، إذ تظهر الشيفرة على أنها متزامنة، لكنها غير متزامنة وغير قابلة للتوقّف non-blocking في الحقيقة. كيفية عمل صيغة async/await تعيد الدالة غير المتزامنة وعدًا كما في المثال التالي: const doSomethingAsync = () => { return new Promise((resolve) => { setTimeout(() => resolve('I did something'), 3000) }) } إذا أردت استدعاء هذه الدالة، فستضيف الكلمة await في البداية، وستتوقف شيفرة الاستدعاء حتى تأكيد أو رفض الوعد. إليك المثال التالي: const doSomething = async () => { console.log(await doSomethingAsync()) } إليك المثال التالي أيضًا والذي يوضِّح استخدام صيغة async/await لتشغيل دالة تشغيلًا غير متزامن: const doSomethingAsync = () => { return new Promise((resolve) => { setTimeout(() => resolve('I did something'), 3000) }) } const doSomething = async () => { console.log(await doSomethingAsync()) } console.log('Before') doSomething() console.log('After') ستطبع الشيفرة السابقة ما يلي في طرفية المتصفح: Before After I did something //after 3s تطبيق الوعود على كل شيء تعني إضافة الكلمة المفتاحية async في بداية أيّ دالة أنّ هذه الدالة ستعيد وعدًا، وإذا لم تفعل ذلك صراحةً، فستعيد وعدًا داخليًا، وهذا سبب كون الشيفرة التالية صالحةً valid: const aFunction = async () => { return 'test' } aFunction().then(alert) // سيؤدي هذا إلى تنبيه 'test' وكذلك الشيفرة التالية: const aFunction = async () => { return Promise.resolve('test') } aFunction().then(alert) // سيؤدي هذا إلى تنبيه 'test' تبدو الشيفرة السابقة بسيطةً للغاية إذا وازنتها مع الشيفرة التي تستخدِم وعودًا صريحةً مع الدوال المتسلسلة ودوال رد النداء، كما يُعَدّ المثال السابق بسيطًا للغاية، لذلك ستظهر الفوائد جليةً عندما تكون الشيفرة أكثر تعقيدًا، وإليك المثال التالي الذي يوضّح كيفية الحصول على مورد JSON وتحليله parse باستخدام الوعود: const getFirstUserData = () => { return fetch('/users.json') // الحصول على قائمة المستخدِمين .then(response => response.json()) // تحليل JSON .then(users => users[0]) // التقاط المستخدِم الأول .then(user => fetch(`/users/${user.name}`)) // الحصول على بيانات المستخدِم .then(userResponse => response.json()) // تحليل JSON } getFirstUserData() وإليك المثال التالي الذي ينفّذ ما يفعله المثال السابق ولكن باستخدام صيغة await/async: const getFirstUserData = async () => { const response = await fetch('/users.json') // الحصول على قائمة المستخدِمين const users = await response.json() // تحليل JSON const user = users[0] // التقاط المستخدِم الأول const userResponse = await fetch(`/users/${user.name}`) // الحصول على بيانات المستخدِم const userData = await user.json() // تحليل JSON return userData } getFirstUserData() استخدام دوال متعددة غير متزامنة ضمن سلسلة يمكن وضع الدوال غير المتزامنة ضمن سلسلة بسهولة باستخدام صيغة أكثر قابلية للقراءة من الوعود الصرفة كما يلي: const promiseToDoSomething = () => { return new Promise(resolve => { setTimeout(() => resolve('I did something'), 10000) }) } const watchOverSomeoneDoingSomething = async () => { const something = await promiseToDoSomething() return something + ' and I watched' } const watchOverSomeoneWatchingSomeoneDoingSomething = async () => { const something = await watchOverSomeoneDoingSomething() return something + ' and I watched as well' } watchOverSomeoneWatchingSomeoneDoingSomething().then((res) => { console.log(res) }) ستطبع الشيفرة السابقة ما يلي: I did something and I watched and I watched as well سهولة تنقيح الأخطاء يُعَدّ تنقيح أخطاء Debugging الوعود أمرًا صعبًا لأن منقِّح الأخطاء لن يتخطى الشيفرة غير المتزامنة، بينما تجعل صيغة Async/await هذا الأمر سهلًا لأنها تُعَدّ مجرد شيفرة متزامنة بالنسبة للمصرِّف compiler. ترجمة -وبتصرّف- للفصل Asynchronous programming من كتاب The Node.js handbook لصاحبه Flavio Copes. اقرأ أيضًا المقال التالي: التعامل مع الطلبيات الشبكية في Node.js المقال السابق: كيفية تنفيذ الدوال داخليا ضمن Node.js البرمجة غير المتزامنة في جافاسكريبت مقدّمة إلى البرمجة غير المتزامنة في Xamarin المدخل الشامل لتعلم علوم الحاسوب
-
سنتعرّف في هذا المقال على مفهوم حلقة الأحداث وكيفية سير عملية تنفيذ الدوال تنفيذًا غير متزامن ضمن نود، كما سنوضِّح كيفية التعامل مع الأحداث المخصَّصة من خلال الصنف EventEmitter الذي يُستخدَم لمعالجة الأحداث. حلقة الأحداث event loop تُعَدّ حلقة الأحداث Event Loop أحد أهم جوانب جافاسكربت التي يجب فهمها. مدخل إلى حلقة الأحداث سنشرح التفاصيل الداخلية لكيفية عمل جافاسكربت باستخدام خيط thread واحد، وسنوضّح كيفية معالجة الدوال غير المتزامنة. تُشغَّل شيفرة جافاسكربت الخاصة بك ضمن خيط واحد، أي أن هناك شيئًا واحدًا فقط يحدث في الوقت نفسه، هذا القيد مفيد جدًا لأنه يبسّط كثيرًا من عملية البرمجة دون القلق بشأن مشاكل التزامن، فما عليك إلا التركيز على كيفية كتابة شيفرتك الخاصة وتجنب أي شيء يمكن أنه إيقاف الخيط مثل استدعاءات الشبكة المتزامنة أو الحلقات اللانهائية. توجد حلقة أحداث لكل تبويب في معظم المتصفحات لعزل العمليات عن بعضها البعض وتجنب صفحة الويب ذات الحلقات اللانهائية أو ذات المعالجة الكبيرة التي تؤدي إلى توقّف المتصفح بأكمله، كما تدير البيئة حلقات أحداث متزامنة متعددة لمعالجة استدعاءات واجهة API مثلًا، كما تُشغَّل عمَّال الويب Web Workers في حلقة الأحداث الخاصة بها أيضًا، إذ يجب عليك الاهتمام فقط بتشغيل شيفرتك ضمن حلقة أحداث واحدة، وكتابة شيفرتك مع وضع ذلك في الحسبان لتجنب توقفها. إيقاف حلقة الأحداث ستوقِف شيفرة جافاسكربت التي تستغرق وقتًا طويلًا لإعادة التحكم إلى حلقة الأحداث مرةً أخرى تنفيذَ أيّ شيفرة جافاسكربت في الصفحة، إذ يمكن أن توقِف خيط واجهة المستخدِم، وبالتالي لا يمكن للمستخدِم تمرير الصفحة أو النقر عليها وغير ذلك، كما تُعَدّ جميع عناصر الدخل/الخرج الأولية في جافاسكربت غير قابلة للإيقاف non-blocking تقريبًا مثل طلبات الشبكة وعمليات نظام ملفات Node.js وما إلى ذلك، ولكن الاستثناء هو توقّفها، وهذا هو سبب اعتماد جافاسكربت الكبير على دوال رد النداء callbacks واعتمادها مؤخرًا على الوعود promises وصيغة عدم التزامن/الانتظار async/await. مكدس الاستدعاءات call stack مكدس الاستدعاءات هو طابور LIFO أي القادم أخيرًا يخرج أولًا Last In First Out، حيث تتحقّق حلقة الأحداث باستمرار من مكدس الاستدعاءات للتأكد من وجود دالة يجب تشغيلها، حيث تضيف حلقة الأحداث عندها أي استدعاء دالة تجده إلى مكدس الاستدعاءات وتنفّذ كل استدعاء بالترتيب. قد تكون على دراية بتعقّب مكدس الأخطاء في منقِّح الأخطاء debugger أو في وحدة تحكم المتصفح، حيث يبحث المتصفح عن أسماء الدوال في مكدس الاستدعاءات لإعلامك بالدالة التي تنشئ الاستدعاء الحالي: شرح بسيط لحلقة الأحداث افترض المثال التالي: const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') bar() baz() } foo() الذي يطبع ما يلي: foo bar baz تُستدعَى الدالة foo() أولًا عند تشغيل الشيفرة السابقة، ثم نستدعي الدالة bar() أولًا ضمن الدالة foo()، ثم نستدعي الدالة baz()، ويبدو مكدس الاستدعاءات في هذه المرحلة كما يلي: تتأكد حلقة الأحداث في كل تكرار من وجود شيء ما في مكدس الاستدعاءات، وتنفِّذه كما يلي إلى أن يصبح مكدس الاستدعاءات فارغًا: تنفيذ طابور الدوال لا يوجد شيء مميز في المثال السابق، حيث تعثر شيفرة جافاسكربت على الدوال لتنفيذها وتشغيلها بالترتيب، ولنشاهد كيفية تأجيل تنفيذ دالة إلى أن يصبح المكدس فارغًا، حيث تُستخدَم حالة الاستخدام setTimeout(() => {}), 0) لاستدعاء دالة، ولكنها تُنفَّذ عند كل تنفيذ لدالة أخرى في الشيفرة، وإليك المثال التالي: const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) baz() } foo() تطبع الشيفرة السابقة ما يلي: foo baz bar تُستدعَى الدالة foo() أولًا عند تشغيل الشيفرة، ثم نستدعي setTimeout أولًا ضمن الدالة foo()، ونمرّر bar على أساس وسيط، ونطلب منه العمل على الفور بأسرع ما يمكنه، ونمرر القيمة 0 على أساس مؤقت timer، ثم نستدعي الدالة baz()، حيث يبدو مكدس الاستدعاءات في هذه المرحلة كما يلي: يوضِّح الشكل التالي ترتيب تنفيذ جميع الدوال في البرنامج: طابور الرسائل Message Queue يبدأ المتصفح أو Node.js المؤقت timer عند استدعاء الدالة setTimeout()، ثم توضَع دالة رد النداء callback function في طابور الرسائل Message Queue بمجرد انتهاء صلاحية المؤقت عالفور مثل حالة وضع القيمة 0 على أساس مهلة زمنية timeout. يُعَدّ طابور الرسائل المكان الذي توضَع الأحداث التي بدأها المستخدِم مثل أحداث النقر أو أحداث لوحة المفاتيح أو جلب الاستجابات الموجودة في طابور قبل أن تتاح لشيفرتك فرصة الرد عليها أو أحداث DOM مثل onLoad. لا يتعيّن علينا انتظار دوال مثل الدالة setTimeout أو انتظار جلب أو تنفيذ أشياء أخرى لهذه الدوال لأن المتصفح يوفّرها وتتقيّد بخيوطها الخاصة، فإذا ضبطتَ مهلة setTimeout الزمنية على 2 ثانية مثلًا، فلن تضطر إلى الانتظار لمدة 2 ثانية، بل يحدث الانتظار في مكان آخر. طابور العمل Job Queue الخاص بالإصدار ES6 قدّم المعيار ECMAScript 2015 مفهوم طابور العمل Job Queue الذي تستخدمه الوعود Promises التي قُدِّمت أيضًا ضمن الإصدار ES6/ES2015، ويُعَدّ هذا المفهوم طريقةً لتنفيذ نتيجة دالة غير متزامنة بأسرع ما يمكن بدلًا من وضعها في نهاية مكدس الاستدعاءات. ستُنفَّذ الوعود المؤكَّدة قبل انتهاء الدالة الحالية بعدها مباشرةً، حيث يشبه ذلك ركوب الأفعوانية في مدينة ملاهي، إذ يضعك طابور الرسائل بعد جميع الأشخاص الآخرين الموجودين في هذا الطابور، بينما طابور العمل هو مثل تذكرة Fastpass تتيح لك الركوب في رحلة أخرى في الأفعوانية بعد الانتهاء من الرحلة السابقة مباشرةً، وإليك المثال التالي: const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) new Promise((resolve, reject) => resolve('should be right after baz, before bar') ).then(resolve => console.log(resolve)) baz() } foo() تطبع الشيفرة السابقة ما يلي: foo baz should be right after baz, before bar bar يشكّل ذلك فرقًا كبيرًا بين الوعود Promises وصيغة Async/await المبنيّة على الوعود والدوال القديمة غير المتزامنة من خلال الدالة setTimeout() أو واجهات API للمنصات الأخرى. المؤقتات Timers: التنفيذ غير المتزامن في أقرب وقت ممكن تُعَدّ الدالة process.nextTick() جزءًا مهمًا من حلقة أحداث Node.js، حيث نسمّي كل دورة كاملة تدورها حلقة الأحداث بالاسم نبضة tick، كما يؤدي تمرير دالة إلى process.nextTick() إلى استدعاء هذه الدالة في نهاية العملية الحالية وقبل بدء نبضة حلقة الأحداث التالية. process.nextTick(() => { //افعل شيئًا ما }) حلقة الأحداث مشغولة بمعالجة شيفرة الدالة الحالية، كما يشغّل محرك JS عند انتهاء هذه العملية جميع الدوال المُمرَّرة إلى استدعاءات nextTick خلال تلك العملية، وهي الطريقة التي يمكننا من خلالها إخبار محرك JS بمعالجة دالة بطريقة غير متزامنة بعد الدالة الحالية في أقرب وقت ممكن دون وضعها في طابور، كما سيؤدي استدعاء setTimeout(() => {}, 0) إلى تنفيذ الدالة في النبضة التالية بعد وقت أطول من استخدام الدالة nextTick()، واستخدم الدالة nextTick() عندما تريد التأكد من تنفيذ الشيفرة في تكرار حلقة الأحداث التالي. الدالة setTimeout() قد ترغب في تأخير تنفيذ دالة عند كتابة شيفرة جافاسكربت، وهذه هي مهمة الدالة setTimeout، حيث تحدِّد دالة رد نداء لتنفيذها لاحقًا مع قيمة تعبِّر عن مقدار التأخير لتشغيلها لاحقًا مقدَّرةً بالميلي ثانية: setTimeout(() => { // تشغيل بعد 2 ثانية }, 2000) setTimeout(() => { // تشغيل بعد 50 ميلي ثانية }, 50) تحدِّد هذه الصيغة دالةً جديدةً، حيث يمكنك استدعاء أيّ دالة أخرى تريدها هناك، أو يمكنك تمرير اسم دالة موجودة مسبقًا مع مجموعة من المعاملات كما يلي: const myFunction = (firstParam, secondParam) => { // افعل شيئًا ما } // تشغيل بعد 2 ثانية setTimeout(myFunction, 2000, firstParam, secondParam) تعيد الدالة setTimeout معرِّف المؤقت timer id، وهذا المعرِّف غير مُستخدَم، ولكن يمكنك تخزينه ومسحه إذا أردت حذف تنفيذ الدوال المجدولة: const id = setTimeout(() => { // يجب تشغيله بعد 2 ثانية }, 2000) //غيّرنا رأينا clearTimeout(id) الدالة setImmediate إذا أردت تنفيذ جزء من الشيفرة بطريقة غير متزامنة ولكن في أقرب وقت ممكن، فإنّ أحد الخيارات هو استخدام الدالة setImmediate() التي يوفّرها Node.js: setImmediate(() => { //شغّل شيئًا ما }) تمثِّل الدالة المُرَّرة على أساس وسيط للدالة setImmediate() دالةَ رد نداء تُنفَّذ في تكرار حلقة الأحداث التالي، كما تختلف setImmediate() عن setTimeout(() => {}, 0) مع تمرير مهلة زمنية مقدارها 0 ميلي ثانية وعن process.nextTick()، إذ تُنفَّذ الدالة المُمرَّرة إلى process.nextTick() في تكرار حلقة الأحداث الحالي بعد انتهاء العملية الحالية، وهذا يعني أنها ستُنفَّذ دائمًا قبل setTimeout وsetImmediate، كما تشبه دالةُ رد النداء setTimeout() مع تأخير 0 ميلي ثانية الدالةَ setImmediate()، في حين يعتمد ترتيب التنفيذ على عوامل مختلفة، ولكنهما ستُشغَّلان في تكرار حلقة الأحداث التالي. التأخير الصفري Zero delay إذا حدّدت تأخير المهلة الزمنية بالقيمة 0، فستُنفَّذ دالة رد النداء في أقرب وقت ممكن ولكن بعد تنفيذ الدالة الحالية: setTimeout(() => { console.log('after ') }, 0) console.log(' before ') ستطبع الدالة السابقة before after. يُعَدّ هذا مفيدًا لتجنب إيقاف وحدة المعالجة المركزية CPU في المهام المكثفة والسماح بتنفيذ الدوال الأخرى أثناء إجراء عملية حسابية ثقيلة عن طريق وضع الدوال ضمن طابور في المجدوِل scheduler. الدالة setInterval() تُعَدّ setInterval دالةً مشابهةً للدالة setTimeout، مع اختلاف أنّ الدالة setInterval ستشغِّل دالةَ رد النداء إلى الأبد ضمن الفاصل الزمني الذي تحدِّده مقدَّرًا بالميلي ثانية بدلًا من تشغيلها مرةً واحدةً: setInterval(() => { // تشغيل كل 2 ثانية }, 2000) تُشغَّل الدالة السابقة كل 2 ثانية ما لم تخبرها بالتوقف باستخدام clearInterval من خلال تمرير معرِّف id الفاصل الزمني الذي تعيده الدالة setInterval: const id = setInterval(() => { // تشغيل كل 2 ثانية }, 2000) clearInterval(id) يشيع استدعاء clearInterval ضمن دالة رد نداء الدالة setInterval، للسماح لها بالتحديد التلقائي إذا وجب تشغيلها مرةً أخرى أو إيقافها، حيث تشغّل الشيفرة التالية شيئًا على سبيل المثال إذا لم تكن قيمة App.somethingIWait هي arrived: const interval = setInterval(() => { if (App.somethingIWait === 'arrived') { clearInterval(interval) return } // وإلّا افعل شيئًا ما }, 100) دالة setTimeout العودية تبدأ setInterval دالةً كل n ميلي ثانية، دون الأخذ في الحسبان موعد انتهاء تنفيذ هذه الدالة، فإذا استغرقت الدالة القدر نفسه من الوقت دائمًا، فلا بأس بذلك: قد تستغرق الدالة أوقات تنفيذ مختلفة اعتمادًا على ظروف الشبكة مثلًا: وقد يتداخل وقت تنفيذ دالة طويل مع وقت تنفيذ الدالة التالية: يمكن تجنب ذلك من خلال جدولة دالة setTimeout العودية لتُستدعَى عند انتهاء دالة رد النداء: const myFunction = () => { // افعل شيئًا ما setTimeout(myFunction, 1000) } setTimeout( myFunction() }, 1000) بهدف تحقيق السيناريو التالي: يتوفَّر كل من setTimeout وsetInterval في Node.js من خلال وحدة المؤقتات Timers module، كما يوفِّر Node.js أيضًا الدالة setImmediate() التي تعادل استخدام setTimeout(() => {}, 0) المستخدَمة للعمل مع حلقة أحداث Node.js في أغلب الأحيان. مطلق الأحداث Event Emitter الخاص بنود Node إذا استخدمت جافاسكربت في المتصفح سابقًا، فلا بد أنك تعرف مقدار تفاعلات المستخدِم المُعالَجة من خلال الأحداث مثل نقرات الفأرة وضغطات أزرار لوحة المفاتيح والتفاعل مع حركة الفأرة وغير ذلك، وهنالك الكثير من الأحداث الأساسية في المتصفح ولكن قد تحتاج في وقت ما إلى أحداث مخصَّصة غير تلك الأساسية لتطلقها وفقًا لوقوع حدث ما ثم تعالجها بما يناسبك. يوفِّر نود على جانب الواجهة الخلفية خيارًا لإنشاء نظام مماثل باستخدام وحدة الأحداث events module، إذ تقدّم هذه الوحدة الصنف EventEmitter الذي يُستخدَم لمعالجة الأحداث، كما يمكنك تهيئته كما يلي: const eventEmitter = require('events').EventEmitter() يُظهِر هذا الكائن التابعين on وemit من بين أشياء متعددة. emit الذي يُستخدَم لبدء حدث. on الذي يُستخدَم لإضافة دالة رد نداء والتي ستُنفَّذ عند بدء الحدث. لننشئ حدث start مثلًا ثم نتفاعل معه من خلال تسجيل الدخول إلى الطرفية: eventEmitter.on('start', () => { console.log('started') }) فإذا شغّلنا ما يلي: eventEmitter.emit('start') فستُشغَّل دالة معالج الأحداث، وسنحصل على سجل طرفية. يمكنك تمرير الوسائط إلى معالج الأحداث من خلال تمريرها على أساس وسائط إضافية إلى التابع emit() كما يلي: eventEmitter.on('start', (number) => { console.log(`started ${number}`) }) eventEmitter.emit('start', 23) أو من خلال تمرير وسائط متعددة كما يلي: eventEmitter.on('start', (start, end) => { console.log(`started from ${start} to ${end}`) }) eventEmitter.emit('start', 1, 100) يظهِر كائن EventEmitter توابعًا متعددةً أخرى للتفاعل مع الأحداث مثل: once(): يضيف مستمعًا لمرة واحدة. removeListener() أو off(): يزيل مستمع حدث من الحدث. removeAllListeners(): يزيل جميع المستمعين لحدث ما. يمكنك قراءة جميع التفاصيل الخاصة بهذه التوابع في صفحة وحدة الأحداث events module على Node.js. ترجمة -وبتصرّف- للفصل Working with the event loop من كتاب The Node.js handbook لصاحبه Flavio Copes. اقرأ أيضًا المقال التالي: البرمجة غير المتزامنة في Node.js المقال السابق: دليلك الشامل إلى مدير الحزم npm في Node.js إعداد تطبيق node.js لسير عمل يعتمد على الحاويات باستخدام Docker Compose تأمين تطبيق Node.js يعمل على الحاويات باستخدام Nginx و Let’s Encrypt و Compose Docker استخدام الوضع التفاعلي والتعامل مع سطر الأوامر في Node.js
-
سنأخذ مثال سلسلة الأدوات الذي أنشأناه في المقال السابق، ثم نضيفه لنتمكن من نشر تطبيقنا، إذ سنرفع الشيفرة على GitHub، وننشر التطبيق باستخدام Netlify، وسنوضح كيفية تطبيق اختبار بسيط عليه. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت. الهدف: إنهاء العمل من خلال دراسة الحالة الكاملة لسلسلة الأدوات مع التركيز على مرحلة نشر التطبيق. مرحلة ما بعد التطوير يُحتَمل أن تواجه العديد من المشاكل التي يجب حلها في هذه المرحلة من دورة حياة المشروع، لذلك يجب إنشاء سلسلة أدوات تعالج هذه المشاكل بطريقة تتطلب أقل قدر ممكن من التدخل اليدوي. إليك بعض الأشياء التي يجب مراعاتها في المشروع: إنشاء بنية إنتاج: ضمان تصغير الملفات وتقسيمها وتطبيق تقنية هز الشجرة وتعطيل ذاكرة الإصدارات المخبئية Cache Busted للمتصفح. تشغيل الاختبارات: يمكن أن تتراوح من "هل نُسِّقت هذه الشيفرة تنسيقًا صحيحًا؟" إلى "هل يطبّق هذا الشيء ما هو متوقع منه؟"، والتأكد من أن الاختبارات الفاشلة تمنع النشر. نشر الشيفرة المحدثة فعليًا إلى عنوان URL مباشر: أو عنوان URL مرحلي لمراجعتها أولًا. كما تنقسم المهام السابقة إلى مهام أخرى، لأن معظم فرق تطوير الويب لها شروطها وعملياتها الخاصة لجزء من مرحلة ما بعد التطوير على الأقل. سنستخدم في مشروعنا عرض الاستضافة الثابت من Netlify لاستضافة مشروعنا. تمنحنا Netlify استضافة أو عنوان URL لعرض المشروع عبر الإنترنت ومشاركته مع أصدقائك وعائلتك وزملائك. يكون النشر على الاستضافة في نهاية دورة حياة المشروع، ولكن تعمل خدمات مثل Netlify على خفض تكلفة النشر من الناحية المالية والوقت المطلوب للنشر الفعلي، إذ يمكن النشر أثناء التطوير إلى مشاركة العمل أو الحصول على إصدار تجريبي لأغراض الأخرى. تتيح خدمة Netlify تشغيل مهام ما قبل النشر، وهو ما يعني في حالتنا أنه يمكن تنفيذ جميع عمليات إنشاء شيفرة الإنتاج ضمن Netlify وإذا نجح الإصدار، فستُنشَر تغييرات موقع الويب. تقدّم Netlify خدمة النشر بالسحب والإفلات Drag and Drop Deployment Service، لكننا نعتزم بدء نشر جديد إلى Netlify في كل مرة نرفع فيها الشيفرة على مستودع GitHub. إنها بالضبط أنواع الخدمات المتصلة التي نشجعك على البحث عنها عند اتخاذ قرار بشأن سلسلة أدوات البناء الخاصة بك. يمكننا رفع شيفرتنا على GitHub، وستشغّل الشيفرة المحدثة تلقائيًا منهج البناء الكامل. إذا كان كل شيء على ما يرام، فسيُنشَر التغيير المباشر تلقائيًا، لكن الإجراء الوحيد الذي نحتاجه هو الرفع الأولي. عملية البناء بما أننا نستخدم Parcel للتطوير، فإن خيار البناء سهل الإضافة. يمكننا تشغيل الخادم باستخدام الأمر npx parcel build src/index.html بدلًا من الأمر npx parcel src/index.html، وستبني Parcel كل شيء جاهزًا للإنتاج بدلًا من تشغيله لأغراض التطوير والاختبار فقط، ويتضمن ذلك تصغير الشيفرة وتطبيق تقنية هز الشجرة عليها، وتعطيل الذاكرة المخبئية على أسماء الملفات. تُوضَع شيفرة الإنتاج التي أنشأناها في دليل جديد يسمى dist يحتوي على جميع الملفات المطلوبة لتشغيل موقع الويب، ويكون جاهزًا للتحميل على الخادم. ليس تطبيق هذه الخطوة يدويًا هدفنا النهائي، بل نريد أن يحدث البناء تلقائيًا وأن تُنشَر نتيجة الدليل dist مباشرة على موقعنا على الإنترنت. يجب إعداد شيفرتنا وGitHub وNetlify للتواصل مع بعضها بعضًا، ليكتشف Netlify التغييرات تلقائيًا ويشغّل مهام البناء ويصدر تحديثًا جديدًا في كل مرة نحدّث فيها مستودع شيفرة GitHub. سنضيف أمر البناء إلى الملف package.json بوصفه سكربت npm، ليشغّل الأمر npm run build عملية البناء. ليست هذه الخطوة ضرورية، لكنها أفضل ممارسة جيدة لعادة الإعداد في جميع المشاريع، ثم يمكننا الاعتماد على الأمر npm run build لتطبيق خطوة البناء الكاملة، دون الحاجة إلى تذكر وسطاء أمر البناء المحدَّدة لكل مشروع. افتح الملف package.json في الدليل الجذر لمشروعك، وابحث عن الخاصية scripts. سنضيف الأمر build الذي يمكننا تشغيله لبناء شيفرتنا. أضف السطر التالي إلى مشروعك: "scripts": { ... "build": "parcel build src/index.html" } ملاحظة: إذا احتوت الخاصية scripts على أمر ضمنها، فضع فاصلة في نهايتها حسب صيغة JSON. يجب أن تكون الآن قادرًا على تشغيل الأمر التالي في جذر دليل مشروعك لتشغيل خطوة بناء الإنتاج، ولكن أنهِ أولًا عملية التشغيل باستخدام الاختصار Ctrl + C: npm run build يكون خرج الأمر السابق كما يلي، ويوضح هذا الخرج ملفات الإنتاج المُنشَأة، وحجمها، والمدة التي استغرقتها للبناء: dist/src.99d8a31a.js.map 446.15 KB 63ms dist/src.99d8a31a.js 172.51 KB 5.55s dist/stars.7f1dd035.svg 6.31 KB 145ms dist/asteroid2.3ead4904.svg 3.51 KB 155ms dist/asteroid1.698d75e9.svg 2.9 KB 153ms dist/src.84f2edd1.css.map 2.57 KB 3ms dist/src.84f2edd1.css 1.25 KB 1.53s dist/bg.084d3fd3.svg 795 B 147ms dist/index.html 354 B 944ms يجب استضافة شيفرة المشروع في مستودع git الخاص بك لتتمكّن من إنشاء نسخة منه. خطوتنا التالية هي رفع المشروع على GitHub. تنفيذ التغييرات على GitHub سيساعدك هذا القسم على تجاوز حدود تخزين شيفرتك في مستودع git، ولكنك لن تتعلّم git بالتفصيل. هيّأنا دليل العمل بوصفه دليل عمل git سابقًا، وهناك طريقة سريعة للتحقق من ذلك وهي تشغيل الأمر التالي: git status يجب أن تحصل على تقرير بحالة الملفات المُتتبَّعة والملفات المُنظَّمة وما إلى ذلك، وتُعَد هذه المصطلحات جزءًا من قواعد git. إذا حصلتَ على الخطأ fatal: not a git repository، فهذا يدل على أن دليل العمل ليس دليل عمل git، وبالتالي يجب تهيئة git باستخدام الأمر git init. أمامنا الآن ثلاث مهام وهي: إضافة التغييرات التي أجريناها إلى مكان يدعَى stage، وهو اسم خاص بالمكان الذي يودع git الملفات فيه. تنفيذ التغييرات على المستودع. رفع التغييرات على GitHub. أولًا، يمكنك إضافة التغييرات من خلال تشغيل الأمر التالي: git add . لاحظ النقطة في النهاية التي تعني "كل شيء في هذا الدليل". يشبه الأمر git add . إلى حدٍ ما نهج المطرقة، إذ سيضيف جميع التغييرات المحلية التي عملت عليها دفعة واحدة. إن أردت تحكمًا أفضل فيما تضيفه، فاستخدم الأمر git add -p للعمليات التفاعلية، أو أضف ملفات باستخدام الأمر git add path/to/file. ثانيًا، أصبحت الآن الشيفرة منظمة، ويمكننا تثبيت التغيير من خلال تشغيل الأمر التالي: git commit -m ’committing initial code’ ثالثًا، أخيرًا، يجب رفع الشيفرة على مستودع GitHub المستضاف. يمكنك زيارة الصفحة new في موقع github لإنشاء مستودعك لاستضافة هذه الشيفرة. رابعًا، امنح مستودعك اسمًا قصيرًا يسهل تذكره بدون مسافات (استخدم الشرطات لفصل الكلمات)، واكتب وصفًا مناسبًا، ثم انقر على زر إنشاء مستودع Create Repository في أسفل الصفحة. يجب أن يكون لديك الآن عنوان URL بعيد يؤشّر إلى مستودع GitHub الجديد الخاص بك. خامسًا، يجب إضافة هذا الموقع البعيد إلى مستودع git المحلي قبل أن نتمكن من رفعه هناك، وإلا فلن يتمكن من العثور عليه. يجب تشغيل أمر له البنية التالية (استخدم خيار HTTPS المُقدَّم حاليًا وليس خيار SSH، خاصة إذا كنت جديدًا على GitHub): git remote add github https://github.com/yourname/repo-name.git لذلك إذا كان عنوان URL البعيد الخاص بك هو https://github.com/remy/super-website.git -كما في لقطة الشاشة أعلاه- فسيكون الأمر كما يلي: git remote add github https://github.com/remy/super-website.git غيّر عنوان URL إلى المستودع الخاص بك، وشغّل الأمر. سادسًا، أصبحنا الآن جاهزين لرفع شيفرتنا على GitHub، ويمكنك الآن تشغيل الأمر التالي: git push github main سيُطلب منك الآن إدخال اسم مستخدم وكلمة مرور قبل أن يسمح Git بإرسال الرفع، لأننا استخدمنا خيار HTTPS بدلًا من خيار SSH كما رأينا سابقًا. لذلك تحتاج إلى اسم مستخدم Github الخاص بك وكلمة مرور -إن لم تكن المصادقة الثنائية Two-Factor Authentication -أو 2FA اختصارًا- مفعّلة فإننا نشجعك دائمًا على تفعيلها، ولكن ضع في بالك أنك إذا فعلتها فستحتاج لاستخدام رمز وصول شخصي بجانب كلمة السر الخاصة بالحساب. تحتوي صفحات المساعدة على Github على إرشادات بسيطة وممتازة تغطي كيفية الحصول على هذا الرمز. ملاحظة: إذا كنت مهتمًا باستخدام خيار SSH، وبالتالي تجنب الحاجة إلى إدخال اسم المستخدم وكلمة المرور في كل مرة ترفع فيها شيفرة على GitHub، فيمكنك الاطلاع على فيديو الاتصال بخدمة GitHub دون كلمة سر. يوجّه الأمر السابق git لرفع الشيفرة -أو ما يسمى بالنشر- على الموقع البعيد الذي أطلقنا عليه اسم github -وهو المستودع المستضاف على github.com ويمكننا أن نطلق عليه أي اسم نريده- باستخدام الفرع main. لم ننشئ أي فروع إضافية على هذا المشروع الإطلاق، ولكن الفرع main هو الفرع الافتراضي لعملنا وهو ما يؤسسه git بصورة افتراضية، وهو أيضًا الفرع الافتراضي الذي سيبحث عنه Netlify. الخطوة التالية في سلسلة الأدوات هي توصيل GitHub مع Netlify لنشر مشروعنا مباشرة على الويب. استخدام Netlify للنشر يُعَد النشر من GitHub إلى Netlify أمرًا بسيطًا بمجرد معرفة الخطوات، خاصة مع مواقع الويب الثابتة Static Websites مثل مشروعنا. انتقل إلى صفحة البداية في Netlify. اضغط على زر Github أسفل عنوان النشر المستمر Continuous Deployment الذي يعني أنه كلما تغير مستودع الشيفرة، فسيحاول Netlify نشرها، وبالتالي فهي مستمرة. يمكن أن تحتاج إلى ترخيص Netlify مع GitHub اعتمادًا على ما إذا أعطيت Netlify ترخيصًا من قبل، واختيار الحساب الذي تريد إعطاءه ترخيصًا، إذا كان لديك عدة حسابات أو مؤسسات على GitHub. اختر الحساب الذي رفعت مشروعك عليه. سيطالبك Netlify بقائمة من مستودعات GitHub التي يمكنك العثور عليها. حدد مستودع مشروعك وانتقل إلى الخطوة التالية. بما أننا ربطنا Netlify بحساب Github وأعطيناه إذن الوصول لنشر مستودع المشروع، فسيسأل Netlify عن كيفية إعداد المشروع للنشر وما الذي يجب نشره. يجب إدخال الأمر npm run build وتحديد الدليل dist لدليل النشر الذي يحتوي على الشيفرة التي نريد جعلها عامة. انقر على نشر الموقع Deploy site في النهاية. يجب أن تحصل بعد انتظار قصير لحدوث النشر على عنوان URL يمكنك الانتقال إليه لرؤية موقعك المنشور. إذا أجريت تغييرًا ورفعت التغيير إلى مستودع git البعيد على GitHub، فسيؤدي ذلك إلى إرسال إشعار إلى Netlify الذي سيشغّل مهمة البناء ثم ينشر دليل dist الناتج على موقعنا المنشور. جرّب إجراء تغيير بسيط على تطبيقك، ثم ارفعه إلى GitHub باستخدام الأوامر التالية: git add . git commit -m ‘simple netlify test’ git push github main يجب أن ترى تحديث موقعك المنشور بالتغيير. يستغرق ذلك بضع دقائق للنشر، لذا تحلى بالصبر. يمكننا اختياريًا تغيير اسم مشروع Netlify أو تحديد استخدام اسم نطاقنا الذي يقدّم Netlify بعض الوثائق الممتازة عنه. الاختبار يُعَد الاختبار بحد ذاته موضوعًا واسعًا حتى في مجال تطوير الواجهة الأمامية. سنوضح كيفية إضافة اختبار أولي إلى مشروعك وكيفية استخدام الاختبار للسماح بنشر المشروع أو منعه في حال وجود مشاكل. هناك طرق متعددة للتعامل مع مشاكل الاختبارات هي: الاختبار الشامل End-to-end Testing: يتضمن نقر الزائر على شيء ما مع حدوث بعض الأمور الأخرى. اختبار التكامل Integration Testing: يتضمن هذا الاختبار السؤال: "هل ستستمر بالعمل إحدى كتل الشيفرة بطريقة صحيحة عند اتصالها بكتلة أخرى؟" اختبار الوحدة Unit Testing: تُختبَر أجزاء صغيرة ومحددة من الوظائف لمعرفة ما إذا كانت تفعل ما يفترض منها فعله. تذكّر أيضًا أن الاختبارات لا تقتصر على شيفرة جافاسكربت، إذ يمكن تشغيل الاختبارات على DOM المُصيَّر، وتفاعلات المستخدم، وCSS، وحتى على مظهر الصفحة. سننشئ اختبارًا صغيرًا في مشروعنا يتحقق من بيانات وكالة ناسا التابعة لجهة خارجية والتأكد من أنها في التنسيق الصحيح. إذا لم يكن الأمر كذلك، فسيفشل الاختبار وسيمنع المشروع من العمل. الاختبار موضوع ضخم يتطلب مقالات منفصلة خاصة به، ولكننا نأمل أن يجعلك هذا القسم على الأقل مدركًا لأهمية الاختبار. لا يتضمن اختبار هذا المشروع إطارًا اختباريًا، إلا أن هناك عددًا كبيرًا من خيارات إطار العمل. ليس الاختبار مهمًا بحد ذاته، فالمهم هو كيفية التعامل مع فشل أو نجاح الاختبار. ستتضمن بعض منصات النشر طريقة محددة للاختبار كجزء من سير العمل الخاصة بها. تدعم جميع المنتجات مثل GitHub وGitLab إجراء الاختبارات على التنفيذات. بما أننا ننشر مشروعنا على Netlify الذي لا يسأل إلا عن أمر البناء، فسيتعين علينا جعل الاختبارات جزءًا من عملية البناء. إذا فشل الاختبار، فسيفشل البناء، ولن ينشر Netlify. أولًا، انتقل إلى الملف package.json وافتحه. ثانيًا، ابحث عن الخاصية scripts وحدّثها لتحتوي على أوامر البناء والاختبار التالية: "scripts": { … "test": "node tests/*.js", "build": "npm run test && parcel build src/index.html" } ثالثًا، يجب الآن إضافة الاختبار إلى قاعدة شيفرتنا. أنشئ دليلًا جديدًا في الدليل الجذر الخاص بك وسمِّه tests: mkdir tests رابعًا، أنشئ ملف اختبار ضمن الدليل الجديد: cd tests touch nasa-feed.test.js خامسًا، افتح هذا الملف وأضف محتويات الملف nasa-feed.test.js إليه. سادسًا، يستخدم هذا الاختبار حزمة axios لجلب البيانات التي نريد اختبارها. شغّل الأمر التالي لتثبيت هذه الاعتمادية: npm install --save-dev axios يجب تثبيت axios يدويًا لأن Parcel لن تساعدنا فيها. تقع اختباراتنا خارج نطاق رؤية Parcel في نظامنا، نظرًا لأن Parcel لا ترى أو تدير أيًا من شيفرة الاختبار، لذلك يجب تثبيت الاعتمادية بأنفسنا. سابعًا، يمكننا تشغيل الأمر التالي في سطر الأوامر لإجراء الاختبار يدويًا: npm run test إذا نجحت عملية الاختبار، فالنتيجة هي لا شيء، وهذا يُعَد نجاحًا بحد ذاته. كما جرى الخروج من الاختبار بإشارة خاصة تخبر سطر الأوامر بأن الاختبار ناجح، وتكون قيمة إشارة الخروج 0، وإذا كان هناك فشل، فسيفشل الاختبار مع رمز الخروج 1، وهي قيمة على مستوى النظام تدل على حدوث فشل شيء ما. يستخدم الأمر npm run test لغة Node.Js لتشغيل جميع الملفات الموجودة في دليل الاختبارات التي تنتهي بالامتداد .js. يُستدعَى الأمر npm run test في سكربت البناء، ثم سترى السلسلة && التي تعني أنه "إذا نجح الشيء الموجود على اليسار (الخرج صفر)، فافعل الشيء الموجود على اليمين"، أي إذا نجحت الاختبارات، فطبّق بناء الشيفرة. ثامنًا، يجب رفع الشيفرة الجديدة إلى GitHub باستخدام أوامر التالية المماثلة لما استخدمته سابقًا: git add . git commit -m ‘adding test’ git push github main يمكن أن ترغب في بعض الأحيان في اختبار نتيجة شيفرة البناء، لأنها ليست الشيفرة الأصلية التي كتبناها، لذلك يجب تشغيل الاختبار بعد أمر البناء. أخيرًا، سينشر Netlify تحديث المشروع بعد دقيقة أو نحو ذلك من الرفع، إذا اجتاز الاختبار فقط. الخلاصة لا يزال هناك طريق طويل لقطعه قبل أن تتمكن من عَدّ نفسك ممتازًا في استخدام الأدوات من طرف العميل، لكن نأمل أن تكون هذه السلسلة من المقالات منحتك أول خطوة مهمة نحو فهم هذه الأدوات. لنلخص جميع أجزاء سلسلة الأدوات: تُطبَّق جودة الشيفرة وصيانتها بواسطة الأداتين Eslint وPrettier، وتُضاف هذه الأدوات بوصفها اعتماديات تطوير devDependencies إلى المشروع عبر الأمر npm install --dev eslint prettier eslint-plugin-react. كما يجب استخدام إضافة Eslint لأن المشروع يستخدم React. هناك نوعان من ملفات الإعداد التي تقرأها أدوات جودة الشيفرة هما: .eslintrc و.prettierrc. نستخدم أداة Parcel أثناء التطوير للتعامل مع الاعتماديات. يعمل parcel src/index.html في الخلفية لمراقبة التغييرات وبناء الشيفرة المصدرية تلقائيًا. يُعالَج النشر عن طريق رفع التغييرات إلى Github في الفرع main، مما يؤدي إلى البناء والنشر على Netlify لنشر المشروع. يكون عنوان URL في مثالنا هو near-misses.netlify.com، وسيكون لديك عنوان URL الفريد الخاص بك. كما يوجد اختبار بسيط يمنع بناء ونشر الموقع إذا لم تعطنا NASA API تنسيق البيانات الصحيح. هذا المقال جزء من سلسلة مقالات بعنوان تعلم تطوير الويب والتي تشرح كامل عملية تطوير الويب من واجهات أمامية وخلفية بالكامل. ترجمة -وبتصرُّف- للمقال Deploying our app. اقرأ أيضًا المقال السابق: بناء نموذج كامل لسلسلة أدوات تطوير الويب من طرف العميل أساسيات بناء تطبيقات الويب كيفية نشر تطبيق Rails باستخدام AZK
-
سنعمل في هذا المقال على ترسيخ معرفتك بأدوات تطوير الويب من طرف العميل من خلال إرشادك خلال عملية بناء نموذج لسلسلة أدوات toolchain، وسنقطع شوطًا طويلًا في إعداد بيئة تطوير ووضع أدوات التحويل في مكانها لنشر تطبيقك فعليًا على Netlify. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت. الهدف: ترسيخ ما تعلمناه حتى الآن من خلال العمل على دراسة حالة كاملة لسلسلة أدوات. هناك فعليًا مجموعات غير محدودة من الأدوات مع طرق استخدامها المختلفة، وما تراه في هذا المقال هو طريقة واحدة فقط يمكن من خلالها استخدام الأدوات المميزة لمشروع ما. وصف دراسة الحالة سنستخدم سلسلة الأدوات التي سننشئها في هذا المقال لبناء ونشر موقع صغير يعطي قائمة بيانات مأخوذة من إحدى واجهات برمجة التطبيقات المفتوحة التابعة لوكالة ناسا فيما يتعلق بالأجسام الفضائية التي يحُتمَل أن تكون خطرة وتهدد وجودنا على الأرض: كما يمكنك مشاهدة نسخة حية من الموقع على near-misses.netlify.com. الأدوات المستخدمة في سلسلة أدواتنا سنستخدم الأدوات والميزات التالية: JSX: مجموعة من امتدادات الصياغة ذات الصلة بإطار العمل React والتي تتيح تطبيق أشياء مثل تحديد بنى المكونات ضمن جافاسكربت. لن تحتاج إلى معرفة إطار عمل React لاتباع هذا المقال، لكننا ضمّنا هذه الأداة لإعطائك فكرة عن كيفية دمج لغة ويب غير أصيلة non-native ضمن سلسلة أدوات. أحدث ميزات جافاسكربت المبنية مسبقًا (في وقت كتابة النسخة الأجنبية من هذا المقال) مثل import. أدوات تطوير مفيدة مثل Prettier للتنسيق وEslint لكشف الأخطاء في الصياغة. PostCSS لتوفير إمكانات تداخل CSS. Parcel: لبناء وتقليل حجم شيفرتنا وكتابة محتوى ملف الإعداد تلقائيًا. GitHub: لإدارة التحكم في الشيفرة المصدرية. Netlify: لأتمتة عملية النشر. يمكن ألّا تكون على دراية بجميع الميزات والأدوات السابقة أو ما تفعله، ولكن لا داعي للقلق، لأننا سنشرحها لاحقًا. سلاسل الأدوات وتعقيدها المتوارث كلما زاد عدد الروابط الموجودة في سلسلة أدواتك، كانت أكثر تعقيدًا ويُحتمَل أن يكون ترابطها هشًا، وإعدادها أكثر تعقيدًا وسهلة الكسر. بينما كلما قل عدد الروابط، زادت مرونة سلسلة الأدوات. تختلف مشاريع الويب عن بعضها بعضًا، لذلك يجب التفكير في الأجزاء الضرورية من سلسلة أدواتك والنظر في كل جزء بعناية. أصغر سلسلة أدوات هي سلسلة لا تحتوي على روابط على الإطلاق. يمكنك كتابة شيفرة HTML يدويًا، واستخدام لغة جافاسكربت الصرفة Vanilla JavaScript دون وجود أطر عمل أو لغات وسيطة، وتحميلها يدويًا إلى خادم للاستضافة. لكن يُرجَّح أن تستفيد متطلبات البرامج الأكثر تعقيدًا من استخدام الأدوات للمساعدة في تبسيط عملية التطوير. كما يجب تضمين الاختبارات قبل النشر إلى خادم الإنتاج الخاص بك للتأكد من أن برنامجك يعمل على النحو المنشود، وهذا يبدو بالفعل وكأنه سلسلة أدوات ضرورية. سنستخدم في مشروعنا سلسلة أدوات مصممة خصيصًا للمساعدة في تطوير برامجنا ودعم الخيارات التقنية التي تُجرَى أثناء مرحلة تصميم البرنامج، ولكن سنتجنب أيّ أدوات غير ضرورية، بهدف تقليل التعقيد إلى الحد الأدنى. كان بإمكاننا تضمين أداة لتقليل أحجام ملفات SVG أثناء الإنشاء مثلًا، ولكن يحتوي هذا المشروع على 4 صور SVG فقط التي صغّرناها يدويًا باستخدام الأداة SVGO قبل إضافتها إلى المشروع. المتطلبات الأساسية ذكرنا خدمتين على الويب في قائمة الأدوات أعلاه إلى جانب الأدوات التي سنثبّتها وستساهم في سلسلة الأدوات الخاصة بنا. إذًا لننتهز الفرصة للتأكد من إعدادها قبل المواصلة، وستحتاج إلى إنشاء حسابات في GitHub وNetlify. GitHub هي خدمة مستودع لشيفرة مصدرية تضيف مجموعة ميزات خاصة بمجتمع المطورين مثل تتبع المشكلات وإصدارات المشروع التالية وغير ذلك. سنرفع لاحقًا الشيفرة إلى مستودع شيفرة GitHub الذي سيضيف تأثيرًا تسلسليًا لنشر البرامج على الصفحة الرئيسية في الويب. Netlify هي خدمة استضافة لمواقع الويب الثابتة، أي مواقع الويب التي تتكون بالكامل من ملفات لا تتغير في الوقت الحقيقي، والتي تتيح النشر عدة مرات في اليوم واستضافة المواقع الثابتة من جميع الأنواع بحريّة. توفر Netlify الصفحة الرئيسية على الويب، وبالتالي توفّر استضافة مجانية لنشر تطبيق الاختبار الخاص بنا عليه. يمكنك التسجيل في GitHub (من خلال النقر على رابط التسجيل Sign Up في الصفحة الرئيسية إن لم يكن لديك حساب سابقًا، واتبع تعليمات استخدام حساب GitHub في عملية الاستيثاق على Netlify (انقر على تسجيل Sign Up، ثم اختر GitHub من قائمة "التسجيل باستخدام إحدى القوائم التالية Sign up with one of the following")، لذلك ما عليك سوى إنشاء حساب جديد. ستحتاج لاحقًا إلى ربط حساب Netlify بمستودع GitHub لنشر هذا المشروع، وسنرى كيفية تطبيق ذلك في المقال التالي. مراحل الأدوات تُنظَّم سلسلة الأدوات ضمن المراحل التالية: شبكة الأمان Safety Net: تجعل تجربة تطوير البرمجيات مستقرة وأكثر كفاءة، ونشير إليها بوصفها بيئة التطوير. التحويل Transformation: هي الأدوات التي تتيح استخدام أحدث ميزات لغة مثل لغة جافاسكربت أو لغة أخرى مثل JSX أو TypeScript في عملية التطوير، ثم تحوّل شيفرتنا لكي يستمر إصدار الإنتاج يعمل على مجموعة متنوعة من المتصفحات الحديثة والقديمة. ما بعد التطوير Post Development: هي الأدوات التي تُشغَّل بعد الانتهاء من التطوير لضمان وصول برنامجك إلى الويب واستمراره في العمل. سننظر في إضافة اختبارات إلى شيفرتنا، ونشر التطبيق باستخدام Netlify لتكون متاحةً على الويب ويمكن للمستخدمين مشاهدته. لنبدأ العمل بدءًا من بيئة التطوير خاصتنا. إنشاء بيئة تطوير يُنظَر أحيانًا إلى هذا الجزء من سلسلة الأدوات على أنه يؤخر العمل الفعلي، إذ ستقضي كثيرًا من الوقت في محاولة جعل البيئة مناسبة تمامًا. لكن يمكنك النظر إلى هذا الجزء بالطريقة نفسها التي تُعِد بها بيئة عملك المادية، إذ يجب أن يكون الكرسي مريحًا ومجهزًا في وضع جيد، ويجب استخدام منافذ طاقة وواي فاي وUSB، ويمكن لبعض الزخارف أو الموسيقى أن تحسين مزاجك، إذ تُعَد هذه الأشياء مهمة لتنفّذ أفضل عمل ممكن، وتجهز لمرة واحدة فقط إن احسنت تجهيزها. وكذلك يجب إعداد بيئة التطوير الخاصة بك بالطريقة نفسها، ويمكن إعدادها مرة واحدة فقط وإعادة استخدامها في العديد من المشاريع المستقبلية، إذا أُعِدت بطريقة صحيحة. يمكن أن ترغب في مراجعة هذا الجزء من سلسلة الأدوات بطريقة شبه منتظمة والتفكير فيما إذا كان هناك أيّ ترقيات أو تغييرات يجب إدخالها، ولكن لا ينبغي أن يكون هذا مطلوبًا كثيرًا. ستعتمد سلسلة أدواتك على احتياجاتك الخاصة، ولكن بالنسبة لمثالنا الخاص بسلسلة أدوات كاملة، فإن الأدوات التي يجب تثبيتها مقدمًا هي: أدوات تثبيت المكتبة Library Installation Tools لإضافة الاعتماديات. التحكم في مراجعة الشيفرة Code Revision Control. أدوات ترتيب الشيفرة Code Tidying Tools لتنظيم شيفرات جافاسكربت وCSS وHTML. أدوات كشف أخطاء الشيفرة Code Linting Tools. أدوات تثبيت المكتبة سنستخدم مدير الحزم npm لتثبيت أدواتنا، ويجب أن يكون لديك Node.js وnpm مثبتين مسبقًا. سنستخدم npm لتثبيت الأجزاء اللاحقة من سلسلة أدواتنا، وسنثبّت git للمساعدة في التحكم في المراجعة. التحكم في مراجعة الشيفرة يُحتمَل أنك سمعت عن أداة Git من قبل، إذ تعد أداة Git حاليًا من أكثر أدوات التحكم في مراجعة الشيفرة المصدرية شيوعًا والمتاحة للمطورين، إذ توفر التحكم في مراجعة الشيفرة البرمجية بالإضافة للعديد من المزايا مثل طريقة نسخ عملك الاحتياطي في مكان بعيد وآلية العمل ضمن فريق في المشروع نفسه دون الخوف من الكتابة فوق شيفرة بعضنا بعضًا. Git وGitHub مختلفان، إذ تُعَد أداة Git أداةً التحكم في المراجعة، بينما يُعَد موقع GitHub مخزنًا على الإنترنت لمستودعات Git بالإضافة إلى عدد من الأدوات المفيدة للعمل معها. لاحظ أنه بالرغم من أننا نستخدم GitHub في هذا المقال، إلا أن هناك العديد من البدائل بما في ذلك GitLab وBitbucket، كما يمكنك استضافة مستودعات git الخاصة بك على جهازك الشخصي ببساطة. سيساعد استخدام التحكم في المراجعة في مشاريعك وإدراجه بوصفه جزءًا من سلسلة الأدوات في إدارة تطور شيفرتك، وتوفر طريقة للالتزام بكتل العمل أثناء تقدمك، جنبًا إلى جنب مع تعليقات مثل "تنفيذ ميزة جديدة معينة"، أو "إصلاح خطأ معين بسبب تغييرات محددة". كما يمكن أن يتيح لك التحكم في المراجعة تفريع شيفرة مشروعك، وإنشاء إصدار منفصل وتجربة وظائف جديدة، دون أن تؤثر تلك التغييرات على الشيفرة الأصلية. أخيرًا، يمكن أن يساعدك على التراجع عن تغييرات أو إعادة الشيفرة إلى الوقت الذي كانت تعمل فيه عند إدخال خطأ في مكان ما دون إصلاحه، وسيحتاجُ جميع المطورين ذلك من حين لآخر. يمكن تنزيل Git وتثبيته عبر موقع git-scm على الويب. نزّل برنامج التثبيت المناسب لنظامك، وشغّله، واتبع التعليمات التي تظهر على الشاشة. يمكنك التفاعل مع git بعدة طرق مختلفة، من استخدام سطر الأوامر إلى استخدام تطبيق git GUI لإصدار الأوامر من خلال الضغط على الأزرار، أو حتى من داخل محرر الشيفرة الخاص بك مباشرةً، كما هو موضح في المثال التالي في Visual Studio Code: لكن تثبيت git هو كل ما نحتاجه حاليًا. أدوات ترتيب الشيفرة سنستخدم الأداة Prettier لترتيب شيفرتنا في المشروع. ثبّت Prettier، أو يمكنك تثبيتها بوصفها أداة مساعدة عالمية باستخدام الطرفية Terminal. يمكنك التحقق مما إذا كانت Prettier مثبّتة عالميًا باستخدام الأمر التالي: prettier -v ستحصل في حالة التثبيت على رقم إصدار مُعاد مثل رقم الإصدار 2.0.2، وإن لم تكن مثبّتة، فسيعيد شيئًا مثل "الأمر غير موجود command not found". إن لم تكن مثبّتة، فثبتها باستخدام الأمر التالي: npm install prettier -g يمكن بعد ذلك تشغيل الشيفرة وترتيبها في سطر الأوامر على أساس ملف من أي مكان على حاسوبك كما يلي: prettier --write ./src/index.html استخدمنا في الأمر السابق الأداة Prettier مع الراية --write، إذ ستفهمُ Prettier أن هذا يعني أنه "إذا كانت هناك مشاكل في تنسيق شيفرتك، فتابع وأصلحها، ثم احفظ الملف"، وهذا يناسب عملية التطوير الخاصة بنا، ولكن يمكننا استخدام Prettier بدون الراية وستتحقق من الملف فقط. يُعد التحقق من الملف -دون حفظه- مفيدًا لأغراض مثل عمليات التحقق المُشغَّلة قبل الإصدار، أي "لا تصدر أيّ شيفرة ليس تنسيقها صحيحًا". ولكن تشغيل الأمر على كل ملف أمرًا صعبًا، وبالتالي سيكون استخدام أمر واحد لذلك مفيدًا جدًا، وينطبق الشيء نفسه على أدوات كشف الأخطاء Linting الخاصة بنا. هناك العديد من الطرق المختلفة لحل هذه المشكلة ومنها: استخدام سكربتات npm لتشغيل أوامر متعددة من سطر الأوامر دفعة واحدة مثل الأمر npm run tidy-code. استخدام خطّافات جيت git hooks الخاصة لاختبار ما إذا كانت الشيفرة منسَّقة قبل الالتزام. استخدام إضافات plugins محرر الشيفرة لتشغيل أوامر prettier في كل مرة يُحفَظ الملف فيها. أحد الإضافات المفيدة في VS Code هو Prettier Code Formatter من Esben Petersen التي تتيح تنسيق الشيفرة تلقائيًا عند الحفظ، وهذا يعني تنسيق أي ملف في المشروع الذي نعمل عليه، بما في ذلك ملفات HTML وCSS وجافاسكربت وJSON وmarkdown وغير ذلك. كل ما يحتاجه محرّر الشيفرة هو تفعيل "التنسيق عند الحفظ Format On Save". كما يمكن استخدام Prettier دون الحاجة إلى تهيئة أي شيء، إن كنت راضيًا عن الإعدادات الافتراضية. أدوات كشف أخطاء الشيفرة يساعد كشف الأخطاء Linting في تحسين جودة الشيفرة، وهو وسيلة لكشف الأخطاء المحتمَلة في وقت مبكر أثناء عملية التطوير. كما أنه مكون رئيسي في سلسلة الأدوات وتتضمنه العديد من مشاريع التطوير افتراضيًا. تكون أدوات كشف أخطاء تطوير الويب خاصة بلغة جافاسكربت في أغلب الأحيان، بالرغم من توفر القليل منها للغتي HTML وCSS. إذا اُستخدِم عنصر HTML غير معروف أو خاصية CSS غير صالحة، فيُحتمَل ألّا ينكسر أيّ شيء نظرًا للطبيعة المرنة لهاتين اللغتين. بينما تُعَد لغة جافاسكربت أكثر هشاشة، فمثلًا يؤدي استدعاء دالة غير موجودة عن طريق الخطأ إلى تعطّل الشيفرة، لذلك يُعَد كشف أخطاء شيفرة جافاسكربت مهمًا جدًا خاصةً في المشاريع الكبيرة. أداة الانتقال إلى كشف أخطاء شيفرة جافاسكربت هي Eslint التي تُعَد أداة قوية ومتعددة الاستخدامات، ولكن يمكن أن تكون صعبة نوعًا ما لإعدادها بطريقة صحيحة، إذ يستغرق الأمر غالبًا عدة ساعات في محاولة الحصول على الإعداد الصحيح تمامًا. ستظهر الأداة Eslint خطأ بأنه لا يمكنها العثور على ملف الإعداد إذا شغّلته. يدعم ملف الإعداد تنسيقات متعددة ولكننا سنستخدم في مشروعنا ملف .eslintrc.json، وتعني النقطة في بداية اسم الملف أن الملف مخفي افتراضيًا. تُثبَّت الأداة Eslint باستخدام npm، لذلك لديك خيار تثبيت هذه الأداة تثبيتًا محليًا أو عامًا، إذ يوصَى باستخدام كليهما: يجب دائمًا تضمين Eslint كاعتمادية محلية في المشاريع التي تنوي مشاركتها ليتمكن أي شخص من إنشاء نسخته الخاصة اتباع القواعد التي طبقتها على المشروع. يجب أن تفكر في تثبيت الأداة Eslint تثبيتًا عامًا لتمكنك من استخدامها بسرعة للتحقق من أي ملف تريده. لن نشرح جميع ميزات Eslint في هذا المقال، لكننا سنضع إعدادًا مناسبًا لمشروعنا الخاص ومتطلباته. ضع في بالك أنه إذا رغبتَ في تحسين وفرض قاعدة حول كيفية ظهور شيفرتك أو التحقق من صحتها، فيُحتمَل أن يُطبَّق ذلك باستخدام إعداد Eslint الصحيح. سنقدم لاحقًا ملف إعداد Eslint، إذ يمكن أن يؤدي تشغيل الأمر إلى إنشاء بعض المعلومات المفيدة. فيما يلي مثال لخرج Eslint: ./my-project/src/index.js 2:8 error 'React' is defined but never used no-unused-vars 22:20 error 'body' is defined but never used no-unused-vars 96:19 error 'b' is defined but never used no-unused-vars ✖ 3 problems (3 errors, 0 warnings) يُعَد دعم تكامل محرر الشيفرة مفيدًا لأداة Eslint، ويُحتمَل أن يكون أكثر فائدة لأنه يمكن أن يقدم لنا ملاحظات في الوقت الحقيقي عند ظهور المشاكل: إعداد المشروع الأولي يمكن إعداد مشروع جديد بأمان باستخدام هذه الأدوات مع العلم أن العديد من المشاكل الأساسية ستُكتشَف مبكرًا. يمكننا إنشاء المشروع وتثبيت الأدوات الأولية وإنشاء ملفات إعداد أولية باستخدام سطر الأوامر، ثم ستشعر بما يجب أن يكون عليه الإعداد الافتراضي بمجرد تكرار هذه العملية عدة مرات. حسنًا لنبدأ الإعداد المشروع الأولي. ابدأ بفتح الطرفية، وانتقل إلى مكان يمكنك العثور عليه والوصول إليه بسهولة مثل سطح المكتب Desktop أو المجلد الرئيسي أو مجلد المستندات. ثم شغّل الأوامر التالية لإنشاء مجلد لتحتفظ بمشروعك فيه، وانتقل إلى المجلد: mkdir will-it-miss cd will-it-miss سننشئ الآن دليلًا جديدًا لجميع شيفرات تطوير موقع الويب لنضعها فيه. شغّل الأمر التالي: mkdir src يختلف تنظيم الشيفرة من فريق لآخر. سنضع شيفرة مشروعنا المصدرية في الدليل src. تأكد من أنك داخل جذر الدليل will-it-miss، ثم أدخِل الأمر التالي لبدء وظيفة التحكم في شيفرة git المصدرية التي تعمل في الدليل: git init هذا يعني أنك ستتمكن من بدء تخزين مراجعات محتويات المجلد، وحفظها في مستودع بعيد وغير ذلك. أدخل الأمر التالي بعد ذلك لتحويل دليلك إلى حزمة npm مع المزايا التي ناقشناها في المقال السابق: npm init --force سيؤدي الأمر السابق إلى إنشاء ملف package.json افتراضي يمكننا تهيئته لاحقًا إذا رغبنا في ذلك. كما يؤدي استخدام الراية --force في أن ينشئ الأمر ملف package.json افتراضي على الفور دون طرح جميع الأسئلة المعتادة حول المحتويات التي تريدها كما رأينا سابقًا، وسنحتاج فقط الإعدادات الافتراضية حاليًا، وبالتالي نوفر قليلًا من الوقت. الحصول على ملفات شيفرة المشروع سنحصل في هذه المرحلة على ملفات شيفرة المشروع (HTML وCSS وجافاسكربت وغيرها)، وسنضعها في الدليل src. لن نعلّمك كيفية عملها، لأنه ليس هدف هذا المقال، بل هدفنا تشغيل الأدوات لتتعلّم كيفية عملها. يمكنك الحصول على ملفات الشيفرة من خلال زيارة هذه الصفحة وتنزيل وفك ضغط محتويات هذا المستودع على قرصك الصلب المحلي في مكانٍ ما. يمكنك تنزيل المشروع بأكمله كملف مضغوط عن طريق تحديد خيار نسخ Clone أو تنزيل Download ثم Download ZIP. انسخ محتويات الدليل src الخاص بالمشروع إلى دليل src فارغ حاليًا. أصبحت ملفات مشروعنا في مكانها الصحيح، هذا كل ما نحتاجه حاليًا. تثبيت أدواتنا حان الوقت الآن لتثبيت المجموعة الأولية من الأدوات التي سنستخدمها في بيئة التطوير الخاصة بنا. شغّل الأمر ضمن الدليل الجذر لمشروعك: npm install --save-dev eslint prettier babel-eslint هناك شيئان مهمان يجب ملاحظتهما حول الأمر الذي شغّلته للتو. الأول هو أننا نثبّت الاعتماديات محليًا على المشروع، إذ يُعَد تثبيت الأدوات محليًا أفضل لمشروع معين. يتيح التثبيت محليًا إعادة إنشاء الإعداد بسهولة على أجهزة أخرى، ولكن لا يشمل ذلك الخيار --global. الجزء الثاني المهم من أمر التثبيت هو الخيار --save-dev الذي يخبر npm أن هذه الاعتماديات المعينة مطلوبة فقط من أجل التطوير، لذلك يسردها npm في الملف package.json ضمن اعتماديات التطوير devDependencies وليس ضمن الاعتماديات devDependencies، وبالتالي إذا ثُبِّتَ هذا المشروع في وضع الإنتاج، فلن تُثبَّت هذه الاعتماديات. يمكن أن يحتوي المشروع النموذجي على العديد من اعتماديات التطوير التي لا حاجة إليها لتشغيل الشيفرة فعليًا في الإنتاج، ويؤدي إبقائها كاعتماديات منفصلة إلى تقليل العمل غير الضروري عند النشر في عملية الإنتاج، وهو ما سنلقي نظرة عليه في المقال التالي. يجب تطبيق إعداد قبل البدء في تطوير شيفرة التطبيق الفعلية لتعمل أدواتنا بطريقة صحيحة، وهو ليس شرطًا أساسيًا في تطوير الويب، ولكن يُعَد إعداد الأدوات بطريقة صحيحة أمرًا مفيدًا، إذا ساعد في اكتشاف الأخطاء أثناء عملية التطوير، وهو أمر مفيد خاصة لأداة Eslint. إعداد أدواتنا سنضيف ملفات الإعداد في جذر المشروع -وليس في الدليل src- لإعداد الأدوات Prettier وEslint. يمكنك العثور على ملفات الإعداد في جذر المشروع، والتي لا تحتوي غالبًا على خيارات الإعداد المعبَّر عنها ببنية JSON، بالرغم من أن أدواتنا والعديد من الأدوات الأخرى تدعم بنية YAML أيضًا، والتي يمكنك التبديل إليها إذا كانت المفضلة لديك. أنشئ أولًا ملفًا في جذر الدليل will-it-Miss وسمّه .prettierrc.json. يمكنك إعداد Prettier من خلال وضع المحتويات التالية في الملف .prettierrc.json: { "singleQuote": true, "trailingComma": "es5" } إذا استخدمت الإعدادات السابقة مع تنسيقات Prettier الخاصة بلغة جافاسكربت، فستستخدم علامات اقتباس فردية لجميع القيم المقتبسة، ولن تستخدم فواصل لاحقة، وهي ميزة أحدث في ECMAScript ستتسبب في حدوث أخطاء في المتصفحات القديمة. يجب إعداد Eslint بعد ذلك من خلال إنشاء ملف آخر في جذر الدليل will-it-miss بالاسم .eslintrc.json، وضع فيه المحتويات التالية: { "env": { "es6": true, "browser": true }, "extends": "eslint:recommended", "parserOptions": { "ecmaVersion": 6, "sourceType": "module" }, "rules": { "no-console": 0 } } يشير إعداد Eslint السابق إلى أننا نريد استخدام إعدادات Eslint الموصَى بها، وأننا سنسمح باستخدام ميزات ES6 مثل map() أو Set()، ويمكننا استخدام عبارات استيراد الوحدة import، وأن استخدام التابع console.log() مسموح. لكننا نستخدم صيغة JSX الخاصة بإطار عمل React في ملفات المشروع المصدرية، ويمكن أن تستخدم في مشاريعك الحقيقية إطار العمل React أو Vue أو أي إطار عمل آخر، ويمكن ألّا تستخدم إطار عمل على الإطلاق. سيؤدي وضع صيغة JSX في شيفرة جافاسكربت إلى أن تظهر أخطاء Eslint بسرعة كبيرة من الإعداد الحالي، لذلك سنحتاج إلى إضافة مزيد من إعدادات Eslint لقبول ميزات JSX. يجب أن يبدو ملف الإعداد config file النهائي على النحو التالي: { "env": { "es6": true, "browser": true }, "extends": ["eslint:recommended", "plugin:react/recommended"], "parserOptions": { "ecmaVersion": 6, "sourceType": "module", "ecmaFeatures": { "jsx": true } }, "plugins": ["react"], "rules": { "semi": "error", "no-console": 0, "react/jsx-uses-vars": "error" } } بما أن الإعداد يستخدم مكونًا إضافيًا يسمى React، فيجب تثبيت اعتمادية التطوير هذه حتى تكون الشيفرة موجودة لتشغيل هذا الجزء من عملية كشف الأخطاء. شغّل الأمر التالي في الطرفية ضمن جذر مجلد مشروعك: npm install --save-dev eslint-plugin-react هناك قائمة كاملة بقواعد Eslint التي يمكنك تعديلها وإعدادها وفقًا لمحتواك، بالإضافة إلى ذلك نشرت العديد من الشركات والفرق إعدادات Eslint الخاصة بها، والتي يمكن أن تكون مفيدة في بعض الأحيان إما للاستلهام منها أو لتحديد إحداها التي تناسب معاييرك الخاصة. وبذلك اكتمل إعداد بيئة التطوير، وأصبحنا جاهزين لكتابة الشيفرة تقريبًا. أدوات البناء والتحويل سنستخدم إطار العمل React في مشروعنا، مما يعني استخدام صيغة JSX في الشيفرة المصدرية. كما سنستخدم في مشروعنا أحدث ميزات لغة جافاسكربت. هناك مشكلة وهي أنه لا يوجد متصفح يدعم JSX بطريقة أصيلة، لإنها لغة وسيطة ويُفترَض أن تُصرَّف إلى لغات يفهمها المتصفح في شيفرة الإنتاج. إذا حاول المتصفح تشغيل شيفرة جافاسكربت المصدرية، فستظهر أخطاء على الفور، إذ يحتاج المشروع إلى أداة بناء لتحويل الشيفرة المصدرية إلى شيء يمكن أن يفهمه المتصفح دون مشاكل. هناك عدد من الخيارات لأدوات التحويل، وبالرغم من أن WebPack هو خيار شائع، لكننا سنستخدم Parcel في مشروعنا لأنها تتطلب إعدادًا أقل بكثير. تعمل Parcel على أساس أنها ستحاول إعداد متطلبات التطوير الخاصة بك على الفور، إذ ستراقب الشيفرة وتشغّل خادم ويب لإعادة التحميل مباشرةً أثناء التطوير، وبالتالي ستثبّت Parcel اعتماديات برامجنا تلقائيًا كما هو مشار إليها في الشيفرة المصدرية. كما ستهتم Parcel بتثبيت أي أدوات تحويل وإعداد المطلوب دون الحاجة إلى التدخل في معظم الحالات، ويمكن أن تُجمّع Parcel الشيفرة البرمجية وتجهّزها للنشر في وضع الإنتاج مع الاهتمام بالتصغير ومتطلبات توافق المتصفح. لذلك يجب تثبيت اعتمادية parcel في مشروعنا من خلال تشغيل الأمر التالي في طرفيتك: npm install --save-dev parcel-bundler استخدام الميزات المستقبلية تستخدم شيفرة مشروعنا بعض ميزات الويب الجديدة بما في ذلك الميزات الجديدة جدًا التي لم يكتمل توحيدها بعد مثل استخدام اقتراح W3C لتداخل CSS بدلًا من الوصول إلى أداة مثل الأداة Sass. يسمح تداخل nesting لغة CSS بأن تتداخل محدّدات وخصائص CSS ضمن بعضها بعضًا، وبالتالي ينشأ مجال محدِّد selector أكثر تحديدًا. كانت Sass من أوائل المعالجات المسبقة التي تدعم التداخل -إن لم تكن الأولى فعلًا- ولكن يبدو أن التداخل سيُوحَّد قريبًا، مما يعني أننا سنوفره في متصفحاتنا دون الحاجة إلى أدوات البناء. حتى ذلك الحين، ستحوّل Parcel بين CSS المتداخلة وCSS المدعومة بطريقة أصيلة بمساعدة PostCSS. بما أننا قررنا أن مشروعنا يجب أن يستخدم تداخل CSS بدلًا من Sass، فسيحتاج المشروع إلى تضمين إضافة PostCSS. لنستخدم postcss-preset-env التي تتيح "استخدام CSS المستقبلية اليوم use tomorrow's CSS today" من خلال اتباع الخطوات التالية: أضف ملفًا بالاسم .postcssrc إلى جذر دليل المشروع. أضف المحتويات التالية إلى الملف الجديد التي ستمنحنا تلقائيًا وصولًا كاملًا إلى أحدث ميزات CSS: { "plugins": { "postcss-preset-env": { "stage": 0 } } } وهذا كل ما نحتاجه. تذكر أن Parcel تثبّت الاعتماديات افتراضيًا. يمكن أن تكون هذه المرحلة من سلسلة أدواتنا صعبة، لأننا اخترنا أداة تحاول تقليل الإعداد والتعقيد، لكن لا يوجد شيء يجب تطبيقه أثناء مرحلة التطوير، إذ تُستورَد الوحدات استيرادًا صحيحًا، وتحوَّل CSS المتداخلة إلى CSS عادية تحويلًا صحيحًا، ولا تعوق عمليةُ البناء عمليةَ التطوير. تشغيل التحويل سنشغّل خادم Parcel في سطر الأوامر لبدء العمل في مشروعنا الذي سيراقب التغييرات في شيفرتنا ويثبّت الاعتماديات تلقائيًا، وبالتالي لن نضطر إلى الانتقال ذهابًا وإيابًا بين الشيفرة وسطر الأوامر. انتقل إلى الطرفية وشغّل الأمر التالي لبدء تشغيل Parcel في الخلفية: npx parcel src/index.html يجب أن ترى الخرج التالي بمجرد تثبيت الاعتماديات: Server running at http://localhost:1234 ✨ Built in 129ms. كما يثبّت Parcel الاعتماديات التي سنستخدمها في شيفرتنا، بما في ذلك react وreact-dom وreact-async-hook وdate-fns وformat-number، وبالتالي سيكون تشغيل Parcel الأول أطول من التشغيل المعتاد. يعمل الخادم الآن على عنوان URL المطبوع، وهو localhost: 1234 في حالتنا. انتقل إلى عنوان URL السابق في متصفحك وسترى التطبيق قيد التشغيل. يمتلك Parcel خدعة ذكية أخرى وهي أن أي تغييرات تطرأ على شيفرتك المصدرية ستؤدي إلى تحديث في المتصفح. يمكنك تجربة هذه الميزة من خلال ما يلي: حمّل الملف src/components/App.js في محرر النصوص المفضل لديك. ابحث عن النص "near misses"، واستبدله بشيء مثل "flying fish". احفظ الملف، ثم ارجع مباشرة إلى التطبيق المُشغَّل في متصفحك. ستلاحظ تحديث المتصفح تلقائيًا، وتغيير السطر " there will be near misses" في أعلى الصفحة. كما يمكنك تجربة استخدام Eslint وPrettier. حاول إزالة المسافة البيضاء من أحد ملفاتك وحاول وضع Prettier عليه لتنظيفه، أو أدخل خطأً صياغيًا في أحد ملفات جافاسكربت الخاصة بك وشاهد الأخطاء التي يعطيها Eslint عندما تحاول استخدام Parcel لبنائه مرة أخرى. الخلاصة أنشأنا في هذا المقال بيئة تطوير محلية بسيطة إلى حد ما لإنشاء تطبيق فيها، إذ يمكنك في هذه المرحلة أثناء تطوير برامج الويب صياغة شيفرة برنامجك الذي تنوي إنشائه. بما أننا نتكلم عن تعلم الأدوات المتعلقة بتطوير الويب، وليس شيفرة تطوير الويب نفسها، فلن نعلمك كتابة شيفرة فعلية. لذلك كتبنا مثالًا لمشروع لتستخدم فيه أدواتك. نقترح عليك العمل في المقال التالي باستخدام شيفرة مثالنا، ثم يمكنك محاولة تغيير محتويات دليل src في مشروعك ونشره على Netlify، وسنناقش مرحلة النشر على Netlify في المقال التالي. هذا المقال جزء من سلسلة مقالات بعنوان تعلم تطوير الويب والتي تشرح كامل عملية تطوير الويب من واجهات أمامية وخلفية بالكامل. ترجمة -وبتصرُّف- للمقال Introducing a complete toolchain. اقرأ أيضًا المقال السابق: أساسيات إدارة الحزم في تطوير الويب من طرف العميل دليل استخدام سطر الأوامر في عملية تطوير الويب من طرف العميل فهم أدوات تطوير الويب من طرف العميل
-
يمثِّل مدير حزم نود npm -اختصارًا إلى Node Package Manager- أساس نجاح Node.js، فقد صدر تقرير في شهر 1 من عام 2017 بوجود أكثر من 350000 حزمة مُدرجَة في سجل npm، مما يجعله أكبر مستودع لشيفرات لغة على الأرض، فكُن على ثقة أنك ستجد فيه حزمةً لكل شيء تقريبًا. يُعَدّ npm مدير حزم Node.js المعياري، فقد اُستخدِم في البداية على أساس طريقة لتنزيل وإدارة اعتماديات حزم Node.js، لكنه أصبح بعدها أداةً تُستخدَم في واجهة جافاسكربت الأمامية أيضًا. إدارة تنزيل الحزم والمكتبات والاعتماديات يدير npm تنزيلات جميع اعتماديات مشروعك. تثبيت جميع الاعتماديات Dependencies يمكنك استخدام الأمر التالي إذا احتوى المشروع على ملف packages.json: npm install سيثبِّت كل ما يحتاجه المشروع في المجلد node_modules مع إنشاء هذا المجلد إذا لم يكن موجودًا مسبقًا. تثبيت حزمة واحدة يمكنك أيضًا تثبيت حزمة معينة عن طريق تشغيل الأمر: npm install <package-name> سترى في أغلب الأحيان مزيدًا من الرايات flags المضافة إلى هذا الأمر مثل: --save التي تثبّت وتضيف مدخلة إلى اعتماديات ملف package.json وهي الافتراضية فلا داعي لإضافتها في كل مرة تثبت فيها حزمة في مشروعك. --save-dev التي تثبّت وتضيف مدخلة إلى اعتماديات تطوير devDependencies ملف package.json. يتمثل الاختلاف الأساسي بينهما في أن اعتماديات التطوير devDependencis هي أدوات تطوير مثل مكتبة الاختبار، بينما تُجمَّع الاعتماديات dependencies مع التطبيق الذي يكون قيد الإنتاج. يمكن تثبيت إصدار أقدم من حزمة npm أو تثبيت إصدار محدد بعينه، وهو شيء قد يكون مفيدًا في حل مشكلة التوافق، كما يمكنك تثبيت إصدار قديم من حزمة npm باستخدام صيغة @ كما يلي: npm install <package>@<version> يثبّت الأمر التالي الإصدار الإصدار الأخير الأحدث من حزمة cowsay: npm install cowsay يمكنك تثبيت الإصدار 1.2.0 من خلال الأمر التالي: npm install cowsay@1.2.0 يمكن تطبيق الشيء نفسه مع الحزم العامة كما يلي: npm install -g webpack@4.16.4 وقد تكون مهتمًا بسرد جميع إصدارات الحزمة السابقة من خلال استخدام الأمر npm view <package> versions كما يلي: npm view cowsay versions [ '1.0.0', '1.0.1', '1.0.2', '1.0.3', '1.1.0', '1.1.1', '1.1.2', '1.1.3', '1.1.4', '1.1.5', '1.1.6', '1.1.7', '1.1.8', '1.1.9', '1.2.0', '1.2.1', '1.3.0', '1.3.1' ] مكان تثبيت npm للحزم يمكنك إجراء نوعين من التثبيت، عند تثبيت حزمة باستخدام npm أو yarn: تثبيت محلي local install. تثبيت عام global install. إذا كتبتَ أمر تثبيت npm install مثل الأمر التالي، فستُثبَّت الحزمة في شجرة الملفات الحالية ضمن المجلد الفرعي node_modules افتراضيًا، ويضيف عندها npm أيضًا المدخلة lodash في خاصية الاعتماديات dependencies الخاصة بملف package.json الموجود في المجلد الحالي: npm install lodash يُطبَّق التثبيت العام باستخدام الراية -g: npm install -g lodash لن يثبِّت npm الحزمة ضمن المجلد المحلي وإنما سيستخدم موقعًا عامًا، إذ سيخبرك الأمر npm root -g بمكان هذا الموقع الدقيق على جهازك، حيث يمكن أن يكون هذا الموقع /usr/local/lib/node_modules في نظام macOS أو لينكس، ويمكن أن يكون C:\Users\YOU\AppData\Roaming\npm\node_modules على نظام ويندوز، لكن إذا استخدمت nvm لإدارة إصدارات Node.js، فقد يختلف هذا الموقع، حيث استخدمنا nvm على سبيل المثال وكان موقع الحزم هو /Users/flavio/.nvm/versions/node/v8.9.0/lib/node_modules. كيفية استخدام أو تنفيذ حزمة مثبتة باستخدام npm هل تساءلت عن كيفية تضمين واستخدام حزمة مثبَّتة في مجلد node_modules في شيفرتك الخاصة؟ حسنًا، لنفترض أنك ثبَّت مكتبة أدوات جافاسكربت الشائعة lodash باستخدام الأمر التالي: npm install lodash سيؤدي ذلك إلى تثبيت الحزمة في مجلد node_modules المحلي التي يمكنك استخدامها في شيفرتك الخاصة من خلال استيرادها في برنامجك باستخدام require: const _ = require('lodash) إذا كانت حزمتك الخاصة قابلة للتنفيذ، فسيوضَع الملف القابل للتنفيذ ضمن المجلد node_modules/.bin/، وإحدى طرق إثبات ذلك هي استخدام الحزمة cowsay، حيث توفِّر هذه الحزمة برنامج سطر أوامر يمكن تنفيذه لإنشاء بقرة تقول شيئًا -وحيوانات أخرى أيضًا-، حيث ستثبِّت هذه الحزمة نفسها وعددًا من الاعتماديات في المجلد node_modules عند تثبيتها باستخدام الأمر npm install cowsay: يوجد مجلد .bin مخفي يحتوي على روابط رمزية إلى ملفات cowsay الثنائية: يمكنك تنفيذ هذه الحزمة من خلال كتابة ./node_modules/.bin/cowsay لتشغيلها، لكن يُعَدّ npx المُضمَّن في الإصدارات الأخيرة من npm -منذ الإصدار 5.2- الخيار الأفضل، فما عليك إلا تشغيل الأمر التالي: npx cowsay وسيجد npx موقع الحزمة. تحديث الحزم أصبح التحديث سهلًا أيضًا عن طريق تشغيل الأمر: npm update سيتحقّق npm من جميع الحزم بحثًا عن إصدار أحدث يلبي قيود إدارة الأصدارات Versioning الخاصة بك، كما يمكنك تحديد حزمة واحدة لتحديثها أيضًا باستخدام الأمر: npm update <package-name> إذا أردت تحديث جميع اعتماديات npm المخزَّنة في الملف package.json -الذي سنشرحه بعد قليل- إلى أحدث إصدار متاح لها، فثبَّتَ حزمةً باستخدام الأمر npm install <packagename> الذي سينزِّل أحدث إصدار متاح من الحزمة ويوضَع هذا الإصدار في مجلد node_modules، وستُضاف مدخلة مقابلة إلى الملف package.json والملف package-lock.json الموجودَين في مجلدك الحالي، إذ يحسب npm الاعتماديات ويثبّت أحدث إصدار متاح منها أيضًا. لنفترض أنك ثبَّتَ الحزمة cowsay، وهي أداة سطر أوامر رائعة تتيح لك إنشاء بقرة تقول أشياء، فإذا ثبَّتَها باستخدام الأمر npm install cowsay، فستضاف المدخلة التالية إلى ملف package.json: { "dependencies": { "cowsay": "^1.3.1" } } يمثّل ما يلي جزءًا من ملف package-lock.json، حيث أزلنا الاعتماديات المتداخلة للتوضيح: { "requires": true, "lockfileVersion": 1, "dependencies": { "cowsay": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/cowsay/-/cowsay-1.3.1.tgz", "integrity": "sha512-3PVFe6FePVtPj1HTeLin9v8WyLl+VmM1l1H/5P+BTTDkMAjufp+0F9eLjzRnOHzVAYeIYFF5po5NjRrgefnRMQ==", "requires": { "get-stdin": "^5.0.1", "optimist": "~0.6.1", "string-width": "~2.1.1", "strip-eof": "^1.0.0" } } } } يوضّح هذان الملفان أننا ثبَّتنا الإصدار 1.3.1 من الحزمة cowsay باستخدام قاعدة التحديثات ^1.3.1، والتي تعني بالنسبة لقواعد إدارة إصدارات npm أنه يمكن تحديث npm إلى إصدار حزمة التصحيح patch والإصدار الثانوي minor، أي 0.13.1 و0.14.0 وما إلى ذلك، فإذا كان هناك إصدار ثانوي أو إصدار حزمة تصحيح جديد وكتبنا الأمر npm update، فسيُحدَّث الإصدار المثبَّت، وسيُملَأ ملف package-lock.json بالإصدار الجديد، بينما يبقى الملف package.json دون تغيير، كما يمكنك اكتشاف إصدارات الحزم الجديدة من خلال تشغيل الأمر npm outdated، وفيما يلي قائمة ببعض الحزم القديمة في مستودع واحد لم نحدِّثها لفترة طويلة: تُعَدّ بعض هذه التحديثات إصدارات رئيسية، إذ لن يؤدي تشغيل الأمر npm update إلى تحديثها، فالإصدارات الرئيسية لا تُحدَّث بهذه الطريقة أبدًا لأنها حسب التعريف تقدِّم تغييرات جذرية، ولأن npm يريد توفير المتاعب عليك، في حين يمكنك تحديث جميع الحزم إلى إصدار رئيسي جديد من خلال تثبيت الحزمة تثبيتًا عامًا باستخدام الأمر npm-check-updates كما يلي: npm install -g npm-check-updates ثم تشغيلها باستخدام الأمر التالي: ncu -u سيؤدي ذلك إلى ترقية جميع تلميحات الإصدار في ملف package.json إلى الاعتماديات dependencies وdevDependencies، لذلك يستطيع npm تثبيت الإصدار الرئيسي الجديد، ويمكنك الآن تشغيل أمر التحديث كما يلي: npm update إذا حمّلتَ المشروع بدون اعتماديات node_modules وأردت تثبيت الإصدارات الجديدة أولًا، فما عليك إلا تشغيل الأمر التالي: npm install إدارة الإصدارات وسرد إصدارات الحزم المثبتة يدير npm أيضًا -بالإضافة إلى التنزيلات العادية- عملية الأصدَرة versioning، بحيث يمكنك تحديد إصدار معيّن من الحزمة، أو طلب إصدار أحدث أو أقدم مما تحتاجه، وستجد في كثير من الأحيان أنّ المكتبة متوافقة فقط مع إصدار رئيسي لمكتبة أخرى، أو قد تجد خطأً غير مُصحَّح بعد في الإصدار الأخير من مكتبة، مما يسبِّب مشاكلًا، كما يساعد تحديد إصدار صريح من مكتبة أيضًا في إبقاء كل فريق العمل على إصدار الحزمة الدقيق نفسه، بحيث يشغّل الفريق بأكمله الإصدار نفسه حتى تحديث ملف package.json. تساعد عملية تحديد الإصدار كثيرًا في جميع الحالات السابقة، حيث يتبع npm معيار إدارة الإصدارات الدلالية semantic versioning - أو semver اختصارًا- والذي سنشرحه تاليًا في قسم منفصل، وقد تحتاج عمومًا إلى معرفة إصدار حزمة معينة ثبَّتها في تطبيقك، وهنا يمكنك استخدم الأمر التالي لمعرفة الإصدار الأحدث من جميع حزم npm المثبَّتة بالإضافة إلى اعتمادياتها: npm list إليك المثال التالي: npm list /Users/flavio/dev/node/cowsay └─┬ cowsay@1.3.1 ├── get-stdin@5.0.1 ├─┬ optimist@0.6.1 │ ├── minimist@0.0.10 │ └── wordwrap@0.0.3 ├─┬ string-width@2.1.1 │ ├── is-fullwidth-code-point@2.0.0 │ └─┬ strip-ansi@4.0.0 │ └── ansi-regex@3.0.0 └── strip-eof@1.0.0 يمكنك فتح ملف package-lock.json فقط، ولكنه يحتاج بعض الفحص البصري، حيث يطبق الأمر npm list -g الشيء نفسه ولكن للحزم المثبَّتة تثبيتًا عامًا، كما يمكنك الحصول على حزم المستوى الأعلى فقط، أي الحزم التي طلبتَ من npm تثبيتها وأدرجتَها في ملف package.json، من خلال تشغيل الأمر npm list --depth=0 كما يلي: npm list --depth=0 /Users/flavio/dev/node/cowsay └── cowsay@1.3.1 يمكنك الحصول على إصدار حزمة معينة عن طريق تحديد اسمها كما يلي: npm list cowsay /Users/flavio/dev/node/cowsay └── cowsay@1.3.1 وتعمل هذه الطريقة أيضًا مع اعتماديات الحزم التي ثبَّتها كما يلي: npm list minimist /Users/flavio/dev/node/cowsay └─┬ cowsay@1.3.1 └─┬ optimist@0.6.1 └── minimist@0.0.10 إذا أردت معرفة أحدث إصدار متوفر من الحزمة في مستودع npm، فشغّل الأمر npm view [package_name] version كما يلي: npm view cowsay version 1.3.1 إلغاء تثبيت حزم npm قد تسأل نفسك ماذا لو أردت إلغاء تثبيت حزمة npm المُثبَّتة تثبيتًا محليًا أو عامًا؟ يمكنك إلغاء تثبيت حزمة مثبَّتة مسبقًا محليًا locally باستخدام الأمر npm install <packagename> في مجلد node_modules من خلال تشغيل الأمر التالي في مجلد جذر المشروع، أي المجلد الذي يحتوي على مجلد node_modules: npm uninstall <package-name> تُستخدَم الراية -S أو --save لإزالة جميع المراجع في ملف package.json، فإذا كانت الحزمة عبارة عن اعتمادية تطوير مُدرَجة في اعتماديات devDependencies الخاصة بملف package.json، فيجب عليك استخدام الراية -D أو الراية --save-dev لإزالتها من الملف كما يلي: npm uninstall -S <package-name> npm uninstall -D <package-name> إذا ثُبِّتت الحزمة تثبيتًا عامًا globally، فيجب إضافة الراية -g أو الراية --global كما يلي: npm uninstall -g <package-name> إليك المثال التالي: npm uninstall -g webpack كما يمكنك تشغيل هذا الأمر من أي مكان تريده على نظامك لأن المجلد الذي تتواجد فيه حاليًا غير مهم. تشغيل مهام وتنفيذ سكربتات من سطر الأوامر يدعم ملف package.json تنسيقًا لتحديد مهام سطر الأوامر التي يمكن تشغيلها باستخدام الأمر التالي: npm run <task-name> فمثلًا: { "scripts": { "start-dev": "node lib/server-development", "start": "node lib/server-production" }, } يشيع استخدام هذه الميزة لتشغيل Webpack: { "scripts": { "watch": "webpack --watch --progress --colors --config webpack.conf.js", "dev": "webpack --progress --colors --config webpack.conf.js", "prod": "NODE_ENV=production webpack -p --config webpack.conf.js", }, } يمكنك تشغيل الأوامر التالية بدلًا من كتابة الأوامر الطويلة السابقة التي يسهل نسيانها أو كتابتها بصورة خاطئة: $ npm run watch $ npm run dev $ npm run prod الملف package.json نقطة ارتكاز المشروع يُعَدّ ملف package.json عنصرًا أساسيًا في كثير من قواعد شيفرات التطبيقات المستندة إلى نظام Node.js المجتمعي، فإذا استخدمت سابقًا لغة جافاسكريبت أو تعاملت مع مشروع JavaScript أو Node.js أو مشروع واجهة أمامية، فلا بد أنك صادفت ملف package.json، كما يُعَدّ ملف package.json بيانًا manifest لمشروعك، إذ يمكنه تطبيق أشياء غير مرتبطة متعددة، فهو مستودع مركزي لإعداد الأدوات مثلًا، كما أنه المكان الذي يخزّن فيه npm وyarn أسماء وإصدارات الحزم المُثبَّتة. معمارية الملف package.json فيما يلي مثال لملف package.json: { } هذا الملف فارغ، إذ لا توجد متطلبات ثابتة لما يجب تواجده في ملف package.json خاص بتطبيقٍ ما، فالشرط الوحيد هو أنه يجب أن يتبع تنسيق JSON، وإلّا فلا يمكن أن تقرأه البرامج التي تحاول الوصول إلى خصائصه برمجيًا، وإذا أردت بناء حزمة Node.js التي ترغب في توزيعها عبر npm، فسيتغيّر كل شيء جذريًا، إذ يجب أن يكون لديك مجموعة من الخصائص التي ستساعد الأشخاص الآخرين على استخدام هذا الملف، حيث سنتحدّث عن ذلك لاحقًا، وإليك مثال آخر عن ملف package.json: { "name": "test-project" } يعرِّف الملف السابق خاصية الاسم name والتي تعطي اسم التطبيق أو الحزمة الموجودة في المجلد نفسه الذي يوجد فيه هذا الملف، وإليك المثال التالي الأكثر تعقيدًا والمُستخرَج من عينة تطبيق Vue.js: { "name": "test-project", "version": "1.0.0", "description": "A Vue.js project", "main": "src/main.js", "private": true, "scripts": { "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "start": "npm run dev", "unit": "jest --config test/unit/jest.conf.js --coverage", "test": "npm run unit", "lint": "eslint --ext .js,.vue src test/unit", "build": "node build/build.js" }, "dependencies": { "vue": "^2.5.2" }, "devDependencies": { "autoprefixer": "^7.1.2", "babel-core": "^6.22.1", "babel-eslint": "^8.2.1", "babel-helper-vue-jsx-merge-props": "^2.0.3", "babel-jest": "^21.0.2", "babel-loader": "^7.1.1", "babel-plugin-dynamic-import-node": "^1.2.0", "babel-plugin-syntax-jsx": "^6.18.0", "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", "babel-plugin-transform-runtime": "^6.22.0", "babel-plugin-transform-vue-jsx": "^3.5.0", "babel-preset-env": "^1.3.2", "babel-preset-stage-2": "^6.22.0", "chalk": "^2.0.1", "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.28.0", "eslint": "^4.15.0", "eslint-config-airbnb-base": "^11.3.0", "eslint-friendly-formatter": "^3.0.0", "eslint-import-resolver-webpack": "^0.8.3", "eslint-loader": "^1.7.1", "eslint-plugin-import": "^2.7.0", "eslint-plugin-vue": "^4.0.0", "extract-text-webpack-plugin": "^3.0.0", "file-loader": "^1.1.4", "friendly-errors-webpack-plugin": "^1.6.1", "html-webpack-plugin": "^2.30.1", "jest": "^22.0.4", "jest-serializer-vue": "^0.3.0", "node-notifier": "^5.1.2", "optimize-css-assets-webpack-plugin": "^3.2.0", "ora": "^1.2.0", "portfinder": "^1.0.13", "postcss-import": "^11.0.0", "postcss-loader": "^2.0.8", "postcss-url": "^7.2.1", "rimraf": "^2.6.0", "semver": "^5.3.0", "shelljs": "^0.7.6", "uglifyjs-webpack-plugin": "^1.1.1", "url-loader": "^0.5.8", "vue-jest": "^1.0.2", "vue-loader": "^13.3.0", "vue-style-loader": "^3.0.1", "vue-template-compiler": "^2.5.2", "webpack": "^3.6.0", "webpack-bundle-analyzer": "^2.9.0", "webpack-dev-server": "^2.9.1", "webpack-merge": "^4.1.0" }, "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" }, "browserslist": [ "> 1%", "last 2 versions", "not ie <= 8" ] } هناك خاصيات متعددة يجب شرحها في المثال السابق: name التي تضبط اسم التطبيق أو الحزمة. version التي تشير إلى الإصدار الحالي. description وهي وصف مختصَر للتطبيق أو للحزمة. main التي تضبط نقطة الدخول للتطبيق. private التي تمنع نشر التطبيق أو الحزمة عن طريق الخطأ على npm إذا ضُبِطت على القيمة true. scripts التي تحدّد مجموعة من سكربتات نود التي يمكنك تشغيلها. dependencies التي تضبط قائمة بحزم npm المثبَّتة كاعتماديات. devDependencies التي تضبط قائمة بحزم npm المثبَّتة كاعتماديات تطوير. engines التي تحدّد إصدار نود الذي تعمل عليه هذه الحزمة أو التطبيق. browserslist التي تُستخدَم لمعرفة المتصفحات وإصداراتها التي تريد دعمها. تُستخدَم جميع هذه الخصائص إما باستخدام npm أو باستخدام أدوات أخرى. خاصيات الملف package.json يشرح هذا القسم الخاصيات التي يمكنك استخدامها ضمن الملف package.json بالتفصيل، حيث سنطبّق كل شيء على الحزمة، ولكن يمكن تطبيق الشيء نفسه على التطبيقات المحلية التي لا تستخدِمها على أساس حزم، كما تُستخدَم معظم هذه الخاصيات فقط على npm، ويُستخدَم البعض الآخر بواسطة السكربتات التي تتفاعل مع شيفرتك مثل npm أو غيره. name تضبط هذه الخاصية اسم الحزمة مثل المثال التالي: "name": "test-project" يجب أن يتضمّن الاسم أقل من 214 محرفًا وألا يحتوي على مسافات، كما لا يمكن أن يحتوي إلّا على أحرف صغيرة أو واصلات - أو شرطات سفلية _، وذلك لأن الحزمة تحصل على عنوان URL الخاص بها بناءً على هذه الخاصية عند نشرها على npm، إذا نشرتَ هذه الحزمة علنًا على GitHub، فستكون القيمة المناسبة لهذه الخاصية هي اسم مستودع GitHub. author تعطي هذه الخاصية اسم مؤلف الحزمة مثل المثال التالي: { "author": "Flavio Copes <flavio@flaviocopes.com> (https://flaviocopes.com)" } يمكن استخدامها أيضًا بالتنسيق التالي: { "author": { "name": "Flavio Copes", "email": "flavio@flaviocopes.com", "url": "https://flaviocopes.com" } } contributors يمكن أن يكون للمشروع مساهم أو أكثر بالإضافة إلى المؤلف، وهذه الخاصية هي مصفوفة تعطي قائمة المساهمين مثل المثال التالي: { "contributors": [ "Flavio Copes <flavio@flaviocopes.com> (https://flaviocopes.com)" ] } كما يمكن استخدام هذه الخاصية أيضًا بالتنسيق التالي: { "contributors": [ { "name": "Flavio Copes", "email": "flavio@flaviocopes.com", "url": "https://flaviocopes.com" } ] } bugs تُستخدَم هذه الخاصية للربط بمتتبّع مشاكل الحزمة، أي بصفحة مشاكل GitHub مثلًا كما يلي: { "bugs": "https://github.com/flaviocopes/package/issues" } homepage تضبط هذه الخاصية صفحة الحزمة الرئيسية مثل المثال التالي: { "homepage": "https://flaviocopes.com/package" } version تشير هذه الخاصية إلى إصدار الحزمة الحالي مثل المثال التالي: "version": "1.0.0" تتبع هذه الخاصية صيغة إدارة الإصدارات الدلالية semver، مما يعني أنّ الإصدار يُعبَّر عنه دائمًا بثلاثة أعداد: x.x.x، حيث يمثِّل العدد الأول الإصدار الرئيسي، ويمثِّل العدد الثاني الإصدار الثانوي؛ أما العدد الثالث فهو إصدار حزمة التصحيح patch version، فالإصدار الذي يصلح الأخطاء فقط هو إصدار حزمة التصحيح، والإصدار الذي يقدّم تغييرات متوافقة مع الإصدارات السابقة هو الإصدار الثانوي، كما يمكن أن يحتوي الإصدار الرئيسي على تغييرات جذرية. license تشير إلى رخصة الحزمة مثل المثال التالي: "license": "MIT" keywords تحتوي هذه الخاصية على مصفوفة من الكلمات المفتاحية المرتبطة بما تفعله حزمتك مثل المثال التالي: "keywords": [ "email", "machine learning", "ai" ] تساعد هذه الخاصية في العثور على حزمتك عند التنقل بين حزم مماثلة، أو عند تصفح موقع npm. description تحتوي هذه الخاصية على وصف مختصَر للحزمة مثل المثال التالي: "description": "A package to work with strings" هذه الخاصية مفيدة إذا قرّرت نشر حزمتك على npm لمعرفة معلومات الحزمة. repository تحدّد هذه الخاصية مكان وجود مستودع الحزمة مثل المثال التالي: "repository": "github:flaviocopes/testing", لاحظ البادئة github، وهناك خدمات شائعة أخرى مثل gitlab: "repository": "gitlab:flaviocopes/testing", وأيضًا bitbucket: "repository": "bitbucket:flaviocopes/testing", يمكنك ضبط نظام التحكم بالإصدارات بصورة صريحة كما يلي: "repository": { "type": "git", "url": "https://github.com/flaviocopes/testing.git" } كما يمكنك استخدام أنظمة مختلفة للتحكم بالإصدارات كما يلي: "repository": { "type": "svn", "url": "..." } main تضبط هذه الخاصية نقطة الدخول إلى الحزمة، وهي المكان الذي سيبحث فيه التطبيق عن عمليات تصدير الوحدة عند استيرادها في أحد التطبيقات مثل المثال التالي: "main": "src/main.js" private إذا ضُبِطت هذه الخاصية على القيمة true، فستمنع نشر التطبيق أو الحزمة عن طريق الخطأ على npm كما يلي: "private": true scripts تحدّد هذه الخاصية مجموعة سكربتات نود التي يمكنك تشغيلها مثل المثال التالي: "scripts": { "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "start": "npm run dev", "unit": "jest --config test/unit/jest.conf.js --coverage", "test": "npm run unit", "lint": "eslint --ext .js,.vue src test/unit", "build": "node build/build.js" } تُعَدّ هذه السكربتات تطبيقات سطر الأوامر، حيث يمكنك تشغيلها عن طريق استدعاء الأمر npm run XXXX أو yarn XXXX، حيث XXXX هو اسم الأمر مثل npm run dev، كما يمكنك إعطاء الأمر أيّ اسم تريده، ويمكن للسكربتات فعل أيّ شيء تريده. dependencies تضبط هذه الخاصية قائمة حزم npm المثبَّتة على أساس اعتماديات. إذا ثبَّتَ حزمةً باستخدام npm أو yarn كما يلي: npm install <PACKAGENAME> yarn add <PACKAGENAME> ستُدخَل تلك الحزمة في هذه القائمة تلقائيًا، وإليك المثال التالي: "dependencies": { "vue": "^2.5.2" } devDependencies تضبط هذه الخاصية قائمة حزم npm المثبَّتة على أساس اعتماديات تطوير، وهي تختلف عن الخاصية dependencies لأنها مُخصَّصة للتثبيت على آلة تطوير فقط، وليست ضرورية لتشغيل الشيفرة في عملية الإنتاج، فإذا ثبَّتَ حزمةً باستخدام npm أو yarn: npm install --dev <PACKAGENAME> yarn add --dev <PACKAGENAME> فستُدخَل تلك الحزمة في هذه القائمة تلقائيًا، وإليك المثال التالي: "devDependencies": { "autoprefixer": "^7.1.2", "babel-core": "^6.22.1" } engines تضبط هذه الخاصية إصدارات Node.js والأوامر الأخرى التي تعمل عليها هذه الحزمة أو التطبيق مثل المثال التالي: "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0", "yarn": "^0.13.0" } browserslist تُستخدَم هذه الخاصية لمعرفة المتصفحات وإصداراتها التي تريد دعمها، وقد أشارت إليها أدوات Babel وAutoprefixer وأدوات أخرى أنها تُستخدَم لإضافة تعويض نقص دعم المتصفحات polyfills والنسخ الاحتياطية fallbacks اللازمة للمتصفحات التي تستهدفها، وإليك المثال التالي: "browserslist": [ "> 1%", "last 2 versions", "not ie <= 8" ] يعني الإعداد السابق أنك تريد دعم آخر إصدارين رئيسيين من جميع المتصفحات باستخدام 1% على الأقل -من إحصائيات موقع caniuse- باستثناء الإصدار IE8 والإصدارات الأقدم (اطلع على المزيد من موقع حزمة). خصائص خاصة بالأوامر يمكن أن يستضيف ملف package.json أيضًا إعدادًا خاصًا بالأوامر مثل Babel وESLint وغير ذلك، فلكل منها خاصية معينة مثل eslintConfig وbabel وغيرها، وهذه هي الخصائص الخاصة بالأوامر، كما يمكنك العثور على كيفية استخدامها في توثيق الأمر أو المشروع المرتبط بها. إصدارات الحزم رأيت في الوصف أعلاه أرقام الإصدارات مثل: ~3.0.0 أو ^0.13.0، حيث يحدِّد الرمز الموجود على يسار رقم الإصدار التحديثات التي تقبلها الحزمة من تلك الاعتمادية، ولنفترض استخدام semver -الأصدَرة الدلالية semantic versioning-، حيث تتضمن جميع الإصدارات 3 خانات عددية، أولها هو الإصدار الرئيسي وثانيها هو الإصدار الثانوي وثالثها هو إصدار حزمة التصحيح patch release، وبالتالي لديك القواعد التالية: ~: إذا كتبت ~0.13.0، فهذا يعني أنك تريد فقط تحديث إصدارات حزمة التصحيح، أي أنّ الإصدار 0.13.1 مقبول، ولكن الإصدار 0.14.0 ليس كذلك. ^: إذا كتبت ^0.13.0، فهذا يعني أنك تريد تحديث إصدار حزمة التصحيح والإصدار الثانوي، أي الإصدارات 0.13.1 و0.14.0 وهكذا. *: إذا كتبت *، فهذا يعني أنك تقبل جميع التحديثات بما في ذلك ترقيات الإصدارات الرئيسية. <: أي أنك تقبل أي إصدار أعلى من الإصدار الذي تحدِّده. =<: أي أنك تقبل أي إصدار مساوي أو أعلى من الإصدار الذي تحدِّده. =>: أي أنك تقبل أي إصدار مساوي أو أدنى من الإصدار الذي تحدِّده. >: أي أنك تقبل أي إصدار أدنى من الإصدار الذي تحدِّده. وهناك قواعد أخرى هي: بدون رمز: أي أنك تقبل فقط الإصدار الذي تحدّده. latest: أي أنك تريد استخدام أحدث إصدار متاح. كما يمكنك دمج معظم ما سبق ضمن مجالات مثل 1.0.0 || >=1.1.0 <1.2.0 لاستخدم إما الإصدار 1.0.0 أو أحد الإصدارات الأعلى أو المساوية للإصدار 1.1.0 والأدنى من الإصدار 1.2.0. الملف package-lock.json ودوره في إدارة الإصدارات يُنشَأ الملف package-lock.json تلقائيًا عند تثبيت حزم نود، حيث قدَّم npm ملف package-lock.json في الإصدار رقم 5، والهدف من هذا الملف هو تتبّع الإصدار الدقيق لكل حزمة مثبَّتة، وبالتالي فإن المنتج قابل لإعادة الإنتاج بنسبة 100% بالطريقة نفسها حتى إذا حدّث القائمون على الصيانة الحزم، كما يحل ذلك مشكلةً تركَها ملف package.json دون حل، إذ يمكنك في ملف package.json ضبط الإصدارات التي تريد الترقية إليها -أي إصدار حزمة التصحيح أو الإصدار الثانوي- باستخدام صيغة semver كما يلي: إذا كتبت ~0.13.0، فهذا يعني أنك تريد فقط تحديث إصدار حزمة التصحيح، أي أن الإصدار 0.13.1 مقبول، ولكن الإصدار 0.14.0 ليس كذلك. إذا كتبت ^0.13.0، فهذا يعني أنك تريد تحديث إصدار حزمة التصحيح والإصدار الثانوي، أي الإصدارات 0.13.1 و0.14.0 وهكذا. إذا كتبت 0.13.0، فهذا يعني الإصدار الدقيق الذي سيُستخدَم. لستَ ملزَمًا بتوزيع مجلد node_modules الضخم باستخدام برنامج جت Git، وإذا حاولتَ نسخ المشروع على جهاز آخر باستخدام الأمر npm install -إذا حدّدتَ الصيغة ~ مع إصدار حزمة التصحيح الخاص بالحزمة- فسيثبَّت هذا الإصدار، كما يحدث الأمر ذاته مع الصيغة ^ والإصدارات الثانوية. قد تحاول أنت أو أي شخص آخر تهيئة المشروع على الجانب الآخر من العالم عن طريق تشغيل الأمر npm install، لذلك فإن مشروعك الأصلي والمشروع المُهيَّأ حديثًا مختلفان فعليًا، إذ يجب ألّا يدخِل إصدار حزمة التصحيح أو الإصدار الثانوي تغييرات معطِّلة، ولكننا نعلم أن الأخطاء ممكنة الحدوث وستحدث بالفعل. يضبط ملف package-lock.json الإصدار المثبَّت حاليًا من كل حزمة باستخدام رمز stone، وسيستخدِم npm هذه الإصدارات المحدَّدة عند تشغيل الأمر npm install، كما أنّ هذا المفهوم ليس بجديد، إذ يستخدِم مديرو حزم لغات البرمجة الأخرى -مثل مكتبات Composer في لغة PHP- نظامًا مشابهًا منذ سنوات. يجب أن يكون ملف package-lock.json ملتزمًا بمستودع Git الخاص بك حتى يجلبه أشخاص آخرون إذا كان المشروع عامًا أو لديك متعاونون أو إذا استخدمت Git على أساس مصدر لعمليات النشر، كما ستُحدَّث إصدارات الاعتماديات في ملف package-lock.json عند تشغيل الأمر npm update. يوضِّح المثال التالي معمارية ملف package-lock.json التي نحصل عليها عند تشغيل الأمر npm install cowsay في مجلد فارغ: { "requires": true, "lockfileVersion": 1, "dependencies": { "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, "cowsay": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/cowsay/-/cowsay-1.3.1.tgz", "integrity": "sha512-3PVFe6FePVtPj1HTeLin9v8WyLl+VmM1l1H/5P+BTTDkMAjufp+0F9eLjzRnOHzVAYeIYFF5po5NjRrgefnRMQ==", "requires": { "get-stdin": "^5.0.1", "optimist": "~0.6.1", "string-width": "~2.1.1", "strip-eof": "^1.0.0" } }, "get-stdin": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=" }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "minimist": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" }, "optimist": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", "requires": { "minimist": "~0.0.1", "wordwrap": "~0.0.2" } }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "requires": { "is-fullwidth-code-point": "^2.0.0", "strip-ansi": "^4.0.0" } }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "requires": { "ansi-regex": "^3.0.0" } }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" } } } ثبّتنا حزمة cowsay التي تعتمد على الحزم التالية: get-stdin. optimist. string-width. strip-eof. تتطلب هذه الحزم حزمًا أخرى مثل الحزم الموجودة في الخاصية requires كما يلي: ansi-regex. is-fullwidth-code-point. minimist. wordwrap. strip-eof. تُضاف هذه الحزم إلى الملف بالترتيب الأبجدي، ولكل منها حقل version، وحقل resolved يؤشّر إلى موقع الحزمة، وسلسلة نصية integrity يمكننا استخدامها للتحقق من الحزمة. قواعد الإدارة الدلالية لنسخ الاعتماديات تُعَدّ الإدارة الدلالية لنسخ الاعتماديات Semantic Versioning اصطلاحًا يُستخدَم لتوفير معنى للإصدارات، فإذا كان هناك شيء رائع في حزم Node.js، فهو اتفاق الجميع على استخدام هذا المفهوم لترقيم إصداراتهم، كما يُعَدّ مفهوم الإدارة الدلالية للنسخ بسيطًا للغاية، فلكل الإصدارات 3 خانات عددية x.y.z: العدد الأول هو الإصدار الرئيسي. العدد الثاني هو الإصدار الثانوي. العدد الثالث هو إصدار التصحيح. إذا أردت إنشاء إصدار جديد، فلن تزيد عددًا كما يحلو لك، بل لديك قواعد يجب الالتزام بها وهي: يُحدَّث الإصدار الرئيسي عند إجراء تغييرات غير متوافقة مع واجهة برمجة التطبيقات API. يُحدَّث الإصدار الثانوي عند إضافة عمليات بطريقة متوافقة مع الإصدارات السابقة. يُحدَّث إصدار تصحيح عند إجراء إصلاحات أخطاء متوافقة مع الإصدارات السابقة. اُعتمِد هذا المفهوم في جميع لغات البرمجة ومن المهم أن تلتزم بها كل حزمة npm لأن النظام بأكمله يعتمد على ذلك، إذ وضَع npm بعض القواعد التي يمكننا استخدامها في ملف package.json لاختيار الإصدارات التي يمكن تحديث حزمنا إليها عند تشغيل الأمر npm update، وتستخدِم هذه القواعد الرموز التالية: ^: إذا كتبت ^0.13.0 عند تشغيل الأمر npm update، فهذا يؤدي إلى تحديث إصدار حزمة التصحيح والإصدار الثانوي، أي الإصدارات 0.13.1 و0.14.0 وهكذا. ~: إذا كتبت ~0.13.0 عند تشغيل الأمر npm update، فهذا يؤدي إلى تحديث إصدارات حزمة التصحيح، أي أن الإصدار 0.13.1 مقبول، ولكن الإصدار 0.14.0 ليس كذلك. <: أي أنك تقبل أي إصدار أعلى من الإصدار الذي تحدده. =<: أي أنك تقبل أي إصدار مساوي أو أعلى من الإصدار الذي تحدّده. =>: أي أنك تقبل أي إصدار مساوي أو أدنى من الإصدار الذي تحدّده. >: أي أنك تقبل أي إصدار أدنى من الإصدار الذي تحدّده. =: أي أنك تقبل الإصدار المحدَّد. -: أي أنك تقبل مجالًا من الإصدارات مثل المجال مثل: 2.1.0 - 2.6.2. ||: يُستخدَم لدمج مجموعات من الإصدارات مثل: < 2.1 || > 2.6. يمكنك دمج بعض القواعد السابقة مثل: 1.0.0 || >=1.1.0 <1.2.0 لاستخدام إما الإصدار 1.0.0 أو أحد الإصدارات الأعلى أو المساوية للإصدار 1.1.0 والأدنى من الإصدار 1.2.0. هناك قواعد أخرى أيضًا هي: بدون رمز: أي أنك تقبل فقط الإصدار الذي تحدّده مثل: 1.2.1. latest: أي أنك تريد استخدام أحدث إصدار متاح. أنواع الحزم تُصنَّف الحزم وفقًا لمجال نطاق رؤيتها، أي المكان الذي تُرَى الحزمة منه ويمكن استخدامها فيه، وتنقسم إلى حزمة عامة وخاصة أو محلية، كما تصنَّف أيضًا وفقًا لبيئة استخدامها وتكون إما اعتماديات أساسية ضرورية للمشروع في بيئة الإنتاج والتطوير معًا، وإما اعتماديات خاصة ببيئة التطوير فقط، أي مطلوبة في وقت تطوير المشروع وغير مطلوبة في بيئة الإنتاج. الحزم العامة والحزم المحلية الفرق الرئيسي بين الحزم المحلية والعامة هو: تُثبَّت الحزم المحلية في المجلد أو المسار حيث تشغّل الأمر npm install <package-name>، وتوضَع في مجلد node_modules ضمن هذا المجلد أو المسار. توضَع جميع الحزم العامة في مكان واحد في نظامك بالاعتماد على إعدادك الخاص بغض النظر عن مكان تشغيل الأمر npm install -g <package-name>. وكلاهما مطلوب بالطريقة نفسها في شيفرتك الخاصة كما يلي: require('package-name') يجب تثبيت جميع الحزم محليًا، إذ يضمن ذلك أنه يمكنك الحصول على عشرات التطبيقات على حاسوبك، وتشغِّل جميعها إصدارًا مختلفًا من كل حزمة إذ لزم الأمر، بينما سيجعل تحديث حزمة عامة جميع مشاريعك تستخدِم الإصدار الجديد، مما قد يسبّب مشاكلًا ضخمةً في عملية الصيانة، حيث قد تخرِّب بعض الحزم التوافق بمزيد من الاعتماديات وما إلى ذلك. تحتوي جميع المشاريع على نسختها المحلية الخاصة من الحزمة، فقد يبدو ذلك ضياعًا للموارد، ولكنه ضياع ضئيل بالموازنة مع العواقب السلبية المحتمَلة، كما يجب تثبيت الحزمة العامة عندما توفِّر هذه الحزمة أمرًا قابلًا للتنفيذ بحيث تشغّله من الصدفة shell أي واجهة سطر الأوامر CLI، ويُعاد استخدام هذه الحزمة عبر المشاريع، كما يمكنك أيضًا تثبيت الأوامر القابلة للتنفيذ محليًا وتشغيلها باستخدام npx، ولكن تثبيت الحزم العامة أفضل بالنسبة لبعض الحزم. فيما يلي أمثلة رائعة عن الحزم العامة الشائعة التي قد تعرفها: npm. create-react-app. vue-cli. grunt-cli. mocha. react-native-cli. gatsby-cli. forever. nodemon. يُحتمَل أن تكون لديك بعض الحزم العامة المثبَّتة على نظامك التي يمكنك رؤيتها عن طريق تشغيل الأمر التالي في سطر الأوامر الخاص بك: npm list -g --depth 0 الاعتماديات الأساسية واعتماديات التطوير إذا ثبَّتَ حزمة npm باستخدام الأمر npm install <package-name>، فهذا يعني أنك ثبّتها على أساس اعتمادية dependency، حيث تُدرَج الحزمة تلقائيًا في ملف package.json ضمن قائمة dependencies بدءًا من الإصدار npm 5، إذ احتجنا سابقًا إلى تحديد الراية --save يدويًا، فإذا أضفتَ الراية -D أو الراية --save-dev، فهذا يعني أنك تثبّتها على أساس اعتمادية تطوير، وبالتالي ستُضاف إلى قائمة devDependencies. يُقصَد باعتماديات التطوير أنها حزم للتطوير فقط، وهي غير ضرورية في عملية الإنتاج مثل حزم الاختبار أو حزم webpack أو Babel، فإذا كتبت الأمر npm install واحتوى المجلد على ملف package.json في عملية الإنتاج، فستُثبَّت اعتماديات التطوير، حيث يفترض npm أنّ هذه عملية نشر تطوير، كما يجب ضبط الراية --production من خلال الأمر npm install --production لتجنب تثبيت اعتماديات التطوير. npx يُعَدّ npx طريقةً رائعةً جدًا لتشغيل شيفرة نود، كما يوفِّر ميزات مفيدةً متعددةً، إذ كان متاحًا في npm بدءًا من الإصدار 5.2، الذي صدر في شهر 7 من عام 2017. يتيح لك npx تشغيل الشيفرة المُنشَأة باستخدام نود والمنشورة من خلال سجل npm، كما يتميز npx بالميزات التالية: تشغيل الأوامر المحلية بسهولة اعتاد مطورو نود على نشر معظم الأوامر القابلة للتنفيذ على أساس حزم عامة لتكون هذه الأوامر في المسار الصحيح وقابلةً للتنفيذ مباشرةً، إذ كان هذا أمرًا صعبًا جدًا، لأن تثبيت إصدارات مختلفة من الأمر نفسه غير ممكن، كما يؤدي تشغيل الأمر npx commandname تلقائيًا إلى العثور على مرجع الأمر الصحيح ضمن مجلد node_modules الخاص بالمشروع دون الحاجة إلى معرفة المسار الدقيق، ودون الحاجة إلى تثبيت الحزمة على أنها حزمة عامة وفي مسار المستخدِم. تنفيذ الأوامر دون تثبيتها هناك ميزة أخرى رائعة في npm تسمح بتشغيل الأوامر دون تثبيتها أولًا، حيث يُعَدّ ذلك مفيدًا جدًا للأسباب التالية: لا تحتاج إلى تثبيت أي شيء. يمكنك تشغيل إصدارات مختلفة من الأمر نفسه باستخدام صيغة .@version يمكن توضيح استخدام npx من خلال الأمر cowsay الذي سيطبع بقرةً تقول ما تكتبه ضمن الأمر، حيث سيطبع الأمر cowsay "Hello" ما يلي على سبيل المثال: _______ < Hello > ------- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || || يحدث ذلك إذا كان الأمر cowsay مثبَّتًا تثبيتًا عامًا من npm سابقًا، وإلا فستحصل على خطأ عند محاولة تشغيل الأمر، كما يسمح لك npx بتشغيل الأمر npm السابق دون تثبيته محليًا كما يلي: npx cowsay "Hello" يُعَدّ الأمر السابق للتسلية فقط ودون فائدة، ولكن يمكنك استخدام npx في حالات مهمة أخرى مثل: تشغيل أداة واجهة سطر الأوامر vue لإنشاء تطبيقات جديدة وتشغيلها باستخدام الأمر npx vue create myvue-app إنشاء تطبيق React جديد باستخدام الأمر npx create-react-app my-react-app. و حالات أخرى أيضًا، كما ستُمسَح الشيفرة المُنزَّلة لهذه الأوامر بمجرد تنزيلها. تشغيل شيفرة باستخدام إصدار نود Node مختلف استخدم الرمز @ لتحديد الإصدار، وادمج ذلك مع حزمة npm التي هي node: npx node@6 -v #v6.14.3 npx node@8 -v #v8.11.3 يساعد ذلك في تجنب استخدام أدوات مثل أداة nvm أو أدوات إدارة إصدارات نود الأخرى. تشغيل أجزاء شيفرة عشوائية مباشرة من عنوان URL لا يقيّدك npx بالحزم المنشورة في سجل npm، إذ يمكنك تشغيل الشيفرة الموجودة في GitHub gist مثل المثال التالي: npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32 يجب أن تكون حذرًا عند تشغيل شيفرة لا تتحكم بها، فالقوة العظمى تستوجب مسؤولية عظمى أيضًا. ترجمة -وبتصرّف- للفصل Node modules and npm من كتاب The Node.js handbook لصاحبه Flavio Copes. اقرأ أيضًا المقال التالي: كيفية تنفيذ الدوال داخليا ضمن Node.js المقال السابق: استخدام الوضع التفاعلي والتعامل مع سطر الأوامر في Node.js مقدمة إلى Node.js تأمين تطبيق Node.js يعمل على الحاويات باستخدام Nginx و Let’s Encrypt و Compose Docker