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

Ola Abbas

الأعضاء
  • المساهمات

    189
  • تاريخ الانضمام

  • تاريخ آخر زيارة

كل منشورات العضو Ola Abbas

  1. سنزوّدك من خلال هذا المقال بقائمة من مصادر 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 تعلم البرمجة
  2. سنركِّز في هذا المقال على الشمولية 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 إلى مكونات أساسيات بناء تطبيقات الويب
  3. سنضيف في هذا المقال اللمسات الأخيرة على وظائف تطبيق قائمة المهام 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 إلى مكونات أساسيات بناء تطبيقات الويب
  4. حان الوقت الآن لتعديل تطبيقنا من واجهة مستخدِم ثابتة تمامًا إلى واجهة مستخدِم تسمح لنا بالتفاعل معها وتعديلها بعد وضع خطة للمكوّنات من خلال البحث عن الأحداث 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 إلى مكونات أساسيات بناء تطبيقات الويب
  5. يُعَدّ تطبيقنا الذي عملنا عليه في المقال السابق وحدةً متراصةً، لذلك يجب تقسيمه إلى مكوّنات يمكن وصفها وإدارتها قبل تمكننا من جعل تطبيقنا يفعل شيئًا ما، إذ لا تحتوي مكتبة 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)
  6. لنفترض أننا نريد توضيح مفهوم 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
  7. سنلقي في هذا المقال نظرةً على مكتبة 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
  8. يملك كل إطار عمل جافاسكربت رئيسي نهجًا مختلف لتحديث نموذج كائن المستند 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. اقرأ أيضًا مقدمة إلى أطر عمل تطوير الويب من طرف العميل فهم أدوات تطوير الويب من طرف العميل بناء نموذج كامل لسلسلة أدوات تطوير الويب من طرف العميل
  9. سنبدأ مقالنا بإلقاء نظرة على تاريخ لغة جافاسكربت 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. اقرأ أيضًا دليل استخدام سطر الأوامر في عملية تطوير الويب من طرف العميل بناء نموذج كامل لسلسلة أدوات تطوير الويب من طرف العميل أساسيات إدارة الحزم في تطوير الويب من طرف العميل
  10. سنوضح في هذا المقال مثالًا عمليًا عن خطوات تصميم قاعدة بيانات لجامعة، كما سنضع أمثلةً عمليةً تشرح كيفية إنشاء مخططات 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 النسخة العربية الكاملة لكتاب تصميم قواعد البيانات
  11. سنتعرّف من خلال هذا المقال على وحدات 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 في جافاسكربت
  12. سنتعرّف من خلال هذا المقال على كيفية التعامل مع الملفات والمجلدات في 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
  13. سنتعرّف من خلال هذا المقال على طريقة إرسال واستقبال الطلبيات بين الخادم والعميل عبر الشبكة في 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 استكشاف عملية توصيل الرزم عند بناء الشبكات
  14. تُعَدّ الحواسيب غير متزامنةً في تصميمها، ويعني المصطلح غير متزامن 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 المدخل الشامل لتعلم علوم الحاسوب
  15. سنتعرّف في هذا المقال على مفهوم حلقة الأحداث وكيفية سير عملية تنفيذ الدوال تنفيذًا غير متزامن ضمن نود، كما سنوضِّح كيفية التعامل مع الأحداث المخصَّصة من خلال الصنف 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
  16. سنأخذ مثال سلسلة الأدوات الذي أنشأناه في المقال السابق، ثم نضيفه لنتمكن من نشر تطبيقنا، إذ سنرفع الشيفرة على 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
  17. سنعمل في هذا المقال على ترسيخ معرفتك بأدوات تطوير الويب من طرف العميل من خلال إرشادك خلال عملية بناء نموذج لسلسلة أدوات 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. اقرأ أيضًا المقال السابق: أساسيات إدارة الحزم في تطوير الويب من طرف العميل دليل استخدام سطر الأوامر في عملية تطوير الويب من طرف العميل فهم أدوات تطوير الويب من طرف العميل
  18. يمثِّل مدير حزم نود 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
  19. سنلقي في هذا المقال نظرة على مدير الحزم Package Manager بشيء من التفصيل لفهم كيفية استخدامه في مشاريعنا لتثبيت اعتماديات Dependencies أدوات المشروع وتحديثها وغير ذلك. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت. الهدف: فهم مدراء ومستودعات الحزم، وسبب حاجتنا لها، وأساسيات استخدامها. اعتماديات المشروع تعدّ الاعتمادية Dependency جزءًا من برنامج تابع لجهة خارجية كتبه شخص ما غيرك لحل مشكلة نيابة عنك. يمكن أن يحتوي مشروع الويب على أي عدد من الاعتماديات، تتراوح من لا شيء إلى اعتماديات متعددة، ويمكن أن تتضمن اعتمادياتك اعتماديات فرعية لم تثبّتها صراحة، إذ يمكن لاعتمادياتك أن يكون لها اعتمادياتها الخاصة. من الأمثلة البسيطة على الاعتماديات المفيدة التي يمكن أن يحتاجها مشروعك الشيفرة البرمجية لحساب التواريخ النسبية بوصفها نصًا قابلًا للقراءة. يمكنك بالتأكيد كتابة هذه الشيفرة بنفسك، ولكن يمكن أن يحل شخص آخر هذه المشكلة بدلًا عنك، فلا داعي لنضيع الوقت في إعادة كتابة هذه الشيفرة (ليس علينا إعادة اختراع العجلة). يُحتمَل اختبار اعتمادية موثوقة لطرف ثالث في مواقف متعددة مختلفة، مما يجعلها أقوى وأكثر توافقًا من حلك مع المتصفحات. يمكن أن تكون اعتمادية المشروع عبارة عن مكتبة أو إطار عمل جافاسكربت كامل مثل React وVue أو أداة مساعدة صغيرة جدًا مثل مكتبة التاريخ القابلة للقراءة، أو يمكن أن تكون أداة سطر أوامر مثل Prettier أو Eslint التي تحدثنا عنها في المقال السابق. إن لم نستخدم أدوات البناء الحديثة، فيمكن تضمَّين مثل هذه الاعتماديات في مشروعك باستخدام عنصر <script> بسيط، ولكن يمكن ألا تعمل هذه الطريقة مباشرة، وبالتالي ستحتاج بعض الأدوات الحديثة لتجميع الشيفرة والاعتماديات مع بعضها بعضًا عند إصدارها على الويب. الحزمة Bundle هي المصطلح المُستخدَم للإشارة إلى ملف على خادم الويب الخاص بك الذي يحتوي على شيفرة جافاسكربت الخاصة ببرنامجك، وتُضغَطُ الحزمة قدر الإمكان للمساعدة في تقليل الوقت الذي يستغرقه تنزيل البرنامج وعرضه في متصفح زوار موقعك. لا يُعَد تتبّع بعض الاعتماديات أمرًا صعبًا إذا وجدت أداة أفضل تريد استخدامها بدلًا من الأداة الحالية، أو أُصدِر إصدار جديد من الاعتمادية تريد التحديث إليه، ولكن يمكن أن يصبح هذا النوع من الأشياء صعبًا في المشاريع الكبيرة التي تحتوي على اعتماديات متعددة، لذلك يمكنك استخدام مدير حزم Package Manager مثل npm، لأنه سيضمن إضافة الشيفرة وإزالتها بطريقة نظيفة، بالإضافة إلى مجموعة من المزايا الأخرى. مدير الحزم مدير الحزم هو نظام يدير اعتماديات مشروعك، ويوفّر طريقة لتثبيت اعتماديات جديدة يُشار إليها باسم "الحزم Packages"، وإدارة مكان تخزين الحزم على نظام ملفاتك، وتقديم إمكانيات لنشرها. يمكنك تنزيل اعتماديات مشروعك وتخزينها يدويًا دون استخدام مدير حزم، ولكن مدير الحزم سيتعامل بسلاسة مع تثبيت الحزم وإلغاء تثبيتها. إن لم تستخدم مدير حزم، فسيتعين عليك التعامل مع ما يلي يدويًا: العثور على كافة ملفات جافاسكربت الصحيحة الخاصة بالحزمة. التحقق منها للتأكد من عدم وجود أي ثغرات أمنية. تنزيلها ووضعها في الأماكن الصحيحة في مشروعك. كتابة الشيفرة لتضمين الحزمة أو الحزم في تطبيقك، إذ يمكن تطبيق ذلك باستخدام وحدات جافاسكربت. تطبيق الخطوات نفسها مع جميع اعتماديات الحزم الفرعية التي يُحتمَل وجود عشرات أو مئات منها. إزالة جميع الملفات مرة أخرى إذا أردت إزالة الحزم. كما يتعامل مدير الحزم مع الاعتماديات المكرَّرة، وهو أمر مهم وشائع في تطوير الواجهة الأمامية. لديك خياران لمكان تثبيت اعتمادياتك في حالة npm ومدير الحزم المستند إلى جافاسكربت وNode، ويمكن تثبيت الاعتماديات بطريقة عامة أي لكل المشاريع أو محليًا في مشروع معين فقط. بالرغم من وجود كثير من إيجابيات التثبيت العام، إلّا أن إيجابيات التثبيت المحلي لكل مشروع مهمة مثل قابلية نقل الشيفرة وقفل الإصدار. إذا اعتمد مشروعك مثلًا على Webpack مع إعداد معين، فيجب التأكد من عمل هذا الإعداد عند تثبيت المشروع على جهاز آخر أو العودة إليه لاحقًا. إذا ثُبِّت إصدار مختلف من Webpack، فيمكن ألّا يكون متوافقًا، لذلك يجب تثبيت الاعتماديات محليًا في المشروع. حاول تنزيل وتشغيل مشروع قائم، فإذا نجح وعملت جميع الاعتماديات مباشرة، فعندئذٍ لديك اعتماديات محلية يجب أن تشكرها على حقيقة أن الشيفرة قابلة للنقل. سجلات الحزم Package Registries يحتاج مدير الحزم ليعمل إلى معرفة المكان الذي نثبّت الحزم منه، وهو ما يدعَى بسجل الحزمة Package Registry. السجل هو مكان مركزي تُنشَر الحزمة عليه وبالتالي يمكن تثبيتها منه. يُعدُّ npm اسم سجل الحزم الأكثر استخدامًا لحزم جافاسكربت. ليس npm الخيار الوحيد لإدارة سجل حزمتك، إذ تتيح لك منتجات مثل Microsoft Azure إنشاء وكلاء لسجل npm إذ يمكنك تجاوز أو قفل حزم معينة، كما يقدم GitHub خدمة تسجيل حزمة، ويُحتمَل ظهور مزيد من الخيارات مع مرور الوقت. المهم هو التأكد من اختيارك لأفضل سجل مناسب لك، ولكن ستستخدم العديد من المشاريع مدير الحزم npm، وسنستخدمها في أمثلتنا. استخدام نظام الحزم المجتمعي لنعرض مثالًا باستخدام مدير وسجل الحزم لتثبيت أداة مساعدة لسطر الأوامر. Parcel هي أداة ذكية يستخدمها المطورون لمراقبة محتويات الشيفرة لاستدعاء الاعتماديات، وتثبّتها تلقائيًا أيّ اعتماديات ترى أن شيفرتنا بحاجة إليها، ويمكنها إنشاء الشيفرة تلقائيًا. ثبّتنا الأداة Prettier تثبيتًا عامًا لكل المشاريع في المقال السابق، ولكن سنستخدم الآن npm لتثبيت Parcel محليًا باستخدام أفضل الممارسات، وسنثبّتها بوصفها جزءًا من تطبيق تجريبي. إعداد التطبيق بوصفه حزمة npm أنشئ أولًا مجلدًا جديدًا لتخزين تطبيقنا التجريبي ضمنه في مكان يمكنك إيجاده بسهولة مرة أخرى، وسنسميه parcel-experiment، ولكن بالطبع يمكنك تسميته ما تشاء: mkdir parcel-experiment cd parcel-experiment لنهيّئ بعد ذلك تطبيقنا كحزمة npm التي تُنشئ ملف الإعداد package.json الذي يسمح بحفظ تفاصيل الإعداد الخاصة بنا في حال أردنا إعادة إنشاء هذه البيئة لاحقًا، أو نشر الحزمة على سجل npm بالرغم من أن ذلك إلى حد ما خارج نطاق هذا المقال. اكتب الأمر التالي، وتأكد من أنك داخل الدليل parcel-experiment: npm init ستُطرَح بعض الأسئلة عليك الآن، ثم سينشئ npm ملف package.json افتراضي بناءً على إجاباتك: name: الاسم الذي يعرّف التطبيق. اضغط على زر Enter لقبول الاسم parcel-experiment الافتراضي. version: رقم إصدار البداية الخاص بالتطبيق. اضغط على زر Enter لقبول الإصدار 1.0.0 الافتراضي. description: وصف سريع للغرض من التطبيق. اكتب شيئًا بسيطًا مثل "حزمة npm بسيطة للتعرف على كيفية استخدام npm"، ثم اضغط على زر Enter. entry point: يمثل ملف جافاسكربت ذي المستوى الأعلى في التطبيق. الملف index.js الافتراضي مناسب حاليًا، ثم اضغط على زر Enter. test command وgit repository وkeywords: اضغط على زر Enter لترك هذه الخيارات فارغة حاليًا. author: مؤلف المشروع. اكتب اسمك، ثم اضغط على زر Enter. license: ترخيص نشر الحزمة. اضغط على زر Enter لقبول الإعداد الافتراضي حاليًا. اضغط على زر Enter مرة أخرى لقبول هذه الإعدادات. انتقل إلى المجلد parcel-experiment وستجد أن لديك الملف package.json. افتحه ويجب أن يبدو كما يلي: { "name": "parcel-experiment", "version": "1.0.0", "description": "A simple npm package to learn about using npm", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Chris Mills", "license": "ISC" } إذ يمثل ملف الإعداد الذي يحدد حزمتك. تثبيت Parcel شغّل الأمر التالي لتثبيت Parcel محليًا: npm install parcel-bundler سنكون بعد ذلك جاهزين للتطوير الحديث من طرف العميل الذي يعني استخدام أدوات البناء لتسهيل العمل على المطور. ألقِ نظرة أخرى على ملف package.json أولًا، ستلاحظ أن npm أضاف حقلًا جديدًا وهو الاعتماديات dependencies: "dependencies": { "parcel-bundler": "^1.12.4" } إذا نقلتَ في المستقبل قاعدة شيفرتك إلى موقع آخر على جهاز آخر، فيمكنك إعادة إنشاء الإعداد نفسه عن طريق تشغيل الأمر npm install، وسينظر npm في الاعتماديات ثم يثبّتها لك. لكن أحد مساوئ npm هو أن Parcel متاح فقط ضمن التطبيق parcel-experiment، إذ لن تتمكن من تشغيله في مجلد مختلف. إعداد التطبيق يتوقّع Parcel أن يعمل ملف index.html وملف index.js، ولكن بخلاف ذلك ليست بنية مشروعك مُعلَنة. يمكن أن تكون الأدوات الأخرى مختلفة، ولكن تسهّل الأداة Parcel على الأقل من تجربتنا الأولية. يجب إضافة ملف index.html إلى دليل عملنا. أنشئ الملف index.html في دليل الاختبار الخاص بك، وضع فيه المحتويات التالية: <!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8"> <title>My test page</title> </head> <body> <script src="./index.js"></script> </body> </html> يجب بعد ذلك إضافة ملف index.js في دليل الملف index.html نفسه. يمكن أن يكون الملف index.js فارغًا حاليًا، ولكن يجب أن يكون موجودًا لذلك أنشئه الآن. تطبيق Parcel سنشغّل الآن أداة Parcel المثبَّتة حديثًا. شغّل الأمر التالي في طرفيتك: parcel index.html يجب أن ترى شيئًا كالتالي مطبوعًا على طرفيتك: Server running at http://localhost:1234 ✨ Built in 193ms. إذا واجهتَ مشكلةً في الطرفية التي تعرض خطأ من النوع "الأمر غير موجود command not found"، فحاول تشغيل الأمر السابق باستخدام الأداة npx مثل الأمر npx parcel index.html. يمكننا الآن الاستفادة من نظام حزمة جافاسكربت المجتمعي الكامل. يوجد حاليًا خادم ويب محلي يعمل على العنوان http://localhost:1234، فإذا انتقلت إليه، فلن ترى أي شيء حاليًا، ولكن ستعيد Parcel بنائه وستعمل على تحديث الخادم تلقائيًا عند إجراء تعديلات على تطبيقك لتتمكن من رؤية تأثير التحديث مباشرة. لنفترض أننا نريد عرض التواريخ النسبية التي يمكن قراءتها، مثل "منذ ساعتين 2‎ hours ago" و"قبل 4 أيام 4‎ days ago" وما إلى ذلك. يُعَد التابع formatDistanceToNow()‎ الخاص بالحزمة date-fns مفيدًا لذلك، وهناك حزم أخرى تطبّق الشي نفسه. أضف الشيفرة التالية في ملف index.js واحفظها: import { formatDistanceToNow } from 'date-fns' const date = '1996-09-13 10:00:00'; document.body.textContent = formatDistanceToNow(new Date(date)) + ' ago'; ارجع إلى http://localhost:1234 وسترى كم مضى منذ بلوغ المؤلف 18 عامًا. ما يميز الشيفرة السابقة هو أنها تستخدم الدالة formatDistanceToNow()‎ من الحزمة date-fns التي لم نثبّتها. اكتشفت الأداة Parcel أنك بحاجة الوحدة، وبحثت عنها في سجل الحزمة npmjs.com، وثبّتتها محليًا تلقائيًا. يمكنك إثبات ذلك من خلال البحث في ملف package.json مرة أخرى، وسترى تحديث حقل الاعتماديات dependencies كما يلي: "dependencies": { "date-fns": "^2.12.0", "parcel-bundler": "^1.12.4" } كما أضافت Parcel الملفات المطلوبة لشخص آخر لأخذ هذا المشروع وتثبيت أي اعتماديات استخدمناها. إذا ألقيت نظرة على الدليل الذي شغّلتَ أمر parcel فيه، فستجد عددًا من الملفات الجديدة مثل: node_modules: ملفات اعتماديات Parcel وdate-fns. dist: دليل التوزيع distribution directory، وهو الملفات المُجمَّعة والمُصغَّرة تلقائيًا التي أنشتأها Parcel، والملفات المُخدَّمة على العنوان localhost:1234، وهي الملفات التي ستحمّلها إلى خادم الويب الخاص بك عند إطلاق الموقع عبر الإنترنت ليراه المستخدمين. طالما أننا نعرف اسم الحزمة، فيمكننا استخدامها في شيفرتنا وستوقِف Parcel تشغيل الحزمة -أو نسخة منها- وتجلبها وتثبّتها في دليلنا المحلي ضمن node_modules. بناء الشيفرة للإنتاج لا تعدّ الشيفرة السابقة جاهزة للإنتاج، إذ يكون لمعظم أنظمة أدوات البناء وضع تطوير Development Mode ووضع إنتاج Production Mode. الاختلاف المهم هو أن الكثير من الميزات المفيدة التي ستستخدمها في التطوير ليست ضرورية في الموقع النهائي، لذلك ستُستبعَد للإنتاج مثل استبدال الوحدة وإعادة التحميل المباشر والشيفرة المصدرية غير المضغوطة والمُعلَّقة. هذه بعض ميزات تطوير الويب الشائعة المفيدة جدًا في مرحلة التطوير، ولكنها ليست مفيدة في مرحلة الإنتاج. أوقِف أمر Parcel السابق باستخدام الاختصار Ctrl + C. يمكننا الآن تجهيز أساسات موقع لنشره وهميًا، إذ توفّر Parcel أمرًا إضافيًا لإنشاء ملفات مناسبة للنشر، وإنشاء حزم Bundles مع خيار البناء. شغّل الأمر التالي: parcel build index.html ويجب أن ترى الناتج التالي: ✨ Built in 9.35s. dist/my-project.fb76efcf.js.map 648.58 KB 64ms dist/my-project.fb76efcf.js 195.74 KB 8.43s dist/index.html 288 B 806ms وجهة ملفات الإنتاج هي الدليل dist. تقليل حجم ملف التطبيق حجم حزمة جافاسكربت my-project.fb76efcf.js هو 195K وهو حجم كبير جدًا، بالرغم من أن كل ما تفعله هو طباعة سطر نصي. هناك بالتأكيد بعض الحسابات، ولكننا لسنا بحاجة إلى 195K من شيفرة جافاسكربت لذلك. يجدر بك التساؤل عما إذا كانت أدوات التطوير تطبّق الشيء الصحيح عن استخدامها، إذ يبلغ حجم الحزمة 200K تقريبًا في مثالنا لأنها تضمّنت مكتبة date-fns الكاملة، وليس الدالة التي نستخدمها فقط. إذا تجنّبنا استخدام أي أدوات تطوير واستخدمنا العنصر <script src=""‎> مع إصدار مستضاف من date-fns، فسيحدث الشيء نفسه تقريبًا، وستُنزَّل المكتبة عند تحميل صفحة مثالنا في المتصفح. يمكننا أن نطلب من البرنامج أثناء وجود الأدوات على أجهزتنا فحص استخدامنا للشيفرة وتضمين الدوال التي نستخدمها في مرحلة الإنتاج فعليً فقط، وهي عملية تُعرف باسم هز الشجرة Tree Shaking. يُعَد ذلك منطقيًا لأننا نريد تقليل حجم الملف وبالتالي تحميل تطبيقنا في أسرع وقت ممكن، وتتيح الأدوات المختلفة تطبيق تقنية هز الشجرة بطرق مختلفة. هناك ثلاثة عروض رئيسية للأدوات التي تنشئ حزمًا من الشيفرة المصدرية هي: Webpack وRollup وParcel. سيكون هناك مزيد من الخيارات المتاحة، لكن تُعَد الأنواع التالية شائعة الاستخدام: تقدم أداة RollUp تقنية هز الشجرة وتقسيم الشيفرة. تتطلب أداة Webpack إعدادًا، بالرغم من أن البعض يقلل من تعقيد إعداد Webpack الخاص بالمطورين. هناك في حالة Parcel -قبل الإصدار Parcel 2- راية خاصة مطلوبة هي ‎--experimental-scope-hoisting التي ستهز الشجرة أثناء البناء. لنستخدم Parcel حاليًا، بما أننا ثبّتناها سابقًا. لنشغّل الأمر التالي: parcel build index.html --experimental-scope-hoisting سترى أن هذا الأمر يحدث فرقًا كبيرًا: ✨ Built in 7.87s. dist/my-project.86f8a5fc.js 10.34 KB 7.17s dist/index.html 288 B 753ms أصبح حجم الحزمة حوالي 10K، وهذا أفضل بكثير. إذا أردنا إصدار هذا المشروع إلى خادم، فسنعدّل الملفات الموجودة في مجلد dist، إذ تتعامل Parcel تلقائيًا مع جميع تغييرات اسم الملف. نوصي بإلقاء نظرة على الشيفرة المصدرية في dist/index.html لتتمكن من رؤية التغييرات التي أجرتها Parcel تلقائيًا. توجد الكثير من الأدوات المتاحة، وينمو نظام حزمة جافاسكربت المجتمعي بمعدل غير مسبوق، وهو أمر له إيجابيات وسلبيات. كما تُجرَى تحسينات طوال الوقت، لكن يجب معرفة قدرة الأداة قبل اختيارها. دليل عملاء مدير الحزم ثبّتنا في مثالنا حزمة Parcel باستخدام npm، ولكن هناك بعض البدائل كما ذكرنا سابقًا، لذلك لنلقِ نظرة عليها. سنذكر بعض مدراء الحزم وهي: npm في npmjs.org. pnpm في pnpm.js.org. yarn في yarnpkg.com. يتشابه npm وpnpm من وجهة نظر سطر الأوامر، ويهدف pnpm إلى الحصول على تكافؤ كامل مع خيارات الوسطاء التي يقدمها npm، ولكنهما يختلفان إذ يستخدم pnpm طريقة مختلفة لتنزيل الحزم وتخزينها على حاسوبك بهدف تقليل المساحة الإجمالية المطلوبة على القرص الصلب. سنستخدم npm في الأمثلة التالية، ويمكنك استخدام pnpm وسيعمل الأمر بطريقة صحيحة. يُعتقَد أن yarn أسرع من npm من ناحية عملية التثبيت في أغلب الأحيان، وهذا مهم للمطورين بسبب وجود قدر كبير من الوقت الضائع في انتظار تثبيت الاعتماديات والنسخ إلى الحاسوب. لنراجع الآن الإجراءات الشائعة التي يجب تنفيذها مع مدير الحزم. بدء مشروع جديد npm init yarn init ستوجهك الأوامر السابقة وترشدك عبر سلسلة من الأسئلة لوصف مشروعك مثل الاسم والترخيص والوصف وما إلى ذلك، ثم إنشاء ملف package.json الذي يحتوي على معلومات وصفية حول مشروعك واعتمادياته. تثبيت الاعتماديات npm install date-fns yarn add date-fns رأينا أمر التثبيت install قيد التنفيذ أعلاه الذي سيؤدي إلى إضافة حزمة date-fns مباشرة إلى دليل العمل ضمن دليل فرعي يسمى node_modules مع اعتماديات date-fns. سيثبّت الأمر السابق افتراضيًا أحدث إصدار من date-fns، ولكن يمكنك التحكم في ذلك، إذ يمكنك استخدام الأمر date-fns@1 الذي يمنحك أحدث إصدار من 1‎.x وهو 1.30.1، أو يمكنك تجربة الأمر date-fns@^2.3.0 الذي يعطي أحدث إصدار يتضمن الإصدار 2.3.0 أو بعده (2.8.1 في وقت كتابة النسخة الأجنبية من هذا المقال). تحديث الاعتماديات npm update yarn upgrade سينظر الأمر السابق في الاعتماديات المثبَّتة حاليًا ويحدّثها عند وجود تحديث متاح ضمن المجال المحدّد في الحزمة. حُدِّد المجال في إصدار الاعتمادية في ملف package.json الخاص بك مثل date-fns^2.0.1، إذ يعني المحرف ^ جميع الإصدارات الثانوية وإصدارات تصحيح الأخطاء التي تتضمن الإصدار 2.0.1 والإصدارات التي بعدها باستثناء الإصدار 3.0.0. يُحدَّد الإصدار باستخدام نظام يسمى semver الذي يبدو معقدًا بعض الشيء في توثيقه، ولكن يمكن تبسيطه من خلال النظر فقط في المعلومات الموجزة، ويُمثّل الإصدار بالشكل MAJOR.MINOR.PATCH مثل 2.0.1، إذ يمثل الرقم 2 الإصدار الرئيسي Major Version ويمثل الرقم 1 إصدار تصحيح الأخطاء Patch Version. يمكنك تجربة قيم Semver باستخدام حاسبة semver. يجب أن نتذكر أن الأمر npm update لن يرقّي الاعتماديات خارج المجال المحدَّد في ملف package.json، وإنما يثبت الإصدار الموجود على وجه التحديد. تدقيق الثغرات npm audit yarn audit سيؤدي الأمر السابق إلى التحقق من كامل شجرة اعتمادية مشروعك وتشغيل الإصدارات المحدَّدة التي تستخدمها لتلافي ثغرات قاعدة البيانات وإعلامك إذا كانت هناك حزم ذات ثغرات محتمَلة في مشروعك. يُعَد مشروع Snyk نقطة انطلاق جيدة للتعرف على الثغرات، والذي يغطي كلًا من حزم جافاسكربت ولغات البرمجة الأخرى. التحقق من الاعتمادية npm ls date-fns yarn why date-fns يظهِر الأمر السابق إصدار الاعتمادية المُثبَّتة وكيفية تضمينها في مشروعك، ويُحتمَل سحب حزمة أخرى ذات مستوى أعلى في date-fns. كما يمكن أن يكون لديك إصدارات متعددة من حزمة معينة في مشروعك، ولوحظ هذا الأمر عدة مرات مع حزمة lodash أنها مفيدة جدًا لضمان عمل كافة الشيفرة البرمجية. يمكن أن ترغب في التحقق بالضبط من الإصدار المثبت، بالرغم من أن مدير الحزم سيبذل قصارى جهده لإزالة تكرار الحزم. إنشاء أوامرك الخاصة يدعم مدير الحزم إنشاء أوامرك الخاصة وتنفيذها من سطر الأوامر، إذ يمكننا استخدام الأمر التالي مثلًا: npm run dev # ‫ أو yarn run dev سيؤدي الأمر السابق إلى تشغيل سكربت مخصص لبدء مشروعنا في وضع التطوير، ويمكننا تضمينه بانتظام في جميع المشاريع إذ يميل إعداد التطوير المحلي إلى العمل بطريقة مختلفة عن كيفية تشغيله في عملية الإنتاج. إذا حاولت تشغيل هذا السكربت في مشروع اختبار Parcel الخاص بك، فيُحتمَل أن يدّعي أن السكربت dev مفقود، لأن npm وyarn يبحثان عن خاصية تسمى dev ضمن الخاصية scripts لملف package.json الخاص بك. يمكن أن تشغّل Parcel خادم تطوير باستخدام الأمر parcel serve filename.html، لأننا سنرغب في استخدامه كثيرًا أثناء عملية التطوير، لذلك لننشئ أمر مخصَّصًا مختصرًا هو "dev" في ملف package.json. يجب أن يكون لديك ملف package.json ضمن الدليل parcel-experiment. افتحه ويجب أن تبدو الخاصية scripts كما يلي: "scripts": { "test": "echo \"Error: no test specified\" && exit 1", }, حدّثها لتبدو كما يلي، ثم احفظ الملف: "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "dev": "parcel serve index.html" }, أضفنا الأمر dev المخصص بوصفه سكربت npm. حاول الآن تشغيل الأمر التالي في طرفيتك، وتأكد من أنك ضمن الدليل parcel-experiment: npm run dev يجب أن يبدأ الأمر السابق Parcel ويقدم ملف index.html الخاص بك على خادم التطوير المحلي كما رأينا سابقًا: Server running at http://localhost:1234 ✨ Built in 5.48s. كما تُعَد أوامر npm وyarn ذكية من لأنها ستبحث عن أدوات سطر الأوامر المثبَّتة محليًا للمشروع قبل محاولة العثور عليها من خلال الطرق التقليدية، ويخزّن حاسوبك عادةً البرامج ويسمح بإيجادها. يمكنك معرفة المزيد حول تعقيدات الأمر run، بالرغم من أن سكربتاتك ستعمل بصورة جيدة في معظم الحالات. يمكنك إضافة جميع الأنواع إلى الخاصية scripts التي تساعدك على أداء عملك. إلى هنا تنتهي جولتنا في التعرف على مدير الحزم، وخطوتنا التالية هي بناء نموذج لسلسلة أدوات، ووضع كل ما تعلمناه حتى الآن موضع التنفيذ. هذا المقال جزء من سلسلة مقالات بعنوان تعلم تطوير الويب والتي تشرح كامل عملية تطوير الويب من واجهات أمامية وخلفية بالكامل. ترجمة -وبتصرُّف- للمقال Package management basics. اقرأ أيضًا المقال التالي: بناء نموذج كامل لسلسلة أدوات تطوير الويب من طرف العميل المقال السابق: أدوات مطوري الويب المدمجة في المتصفحات فهم أدوات تطوير الويب من طرف العميل مدخل إلى Helm: مدير حزم Kubernetes نظام إدارة الحزم NuGet في dot NET كيف تدير الحزم على نظام FreeBSD 10.1 بواسطة Pkg
  20. سنتعرّف في هذا المقال على الوضع التفاعلي REPL في Node.js الذي نستخدمه في الطرفية لتقييم تعبير برمجي مكتوب بلغة javascript، كما سنتعلّم كيفية التعامل مع سطر الأوامر في Node.js من تمرير وسائط منه وإرسال مخرجات إليه وقبول مدخلات منه. استخدام الوضع التفاعلي REPL يسمى الوضع التفاعلي في Node.js باسم REPL اختصارًا إلى اقرأ-قيّم-اطبع-كرر Read-Evaluate-Print-Loop، أي اقرأ وقدِّر قيمة التعبير البرمجي ثم اطبع الناتج وكرر العملية، وهو طريقة رائعة لاستكشاف ميزات Node.js بطريقة سريعة. استعملنا الأمر node سابقًا لتشغيل أحد السكربتات: node script.js أما إذا حذفنا اسم الملف، فسندخل في الوضع التفاعلي REPL: node وعندما تجرب الأمر السابق في الطرفية عندك، فسيحدث ما يلي وسيبقى الأمر في وضع السكون وينتظر منك إدخال تعبير ما: $ node Welcome to Node.js v14.15.0. Type ".help" for more information. > إذا أردنا أن نكون أكثر دقةً، فإن الوضع التفاعلي ينتظر منّا إدخال شيفرة JavaScript، لذا لنبدأ بسطر بسيط: > console.log('test') test undefined > القيمة الأولى المطبوعة test هي ناتج الأمر الذي طلبنا منه طباعة سلسلة نصية إلى الطرفية، ثم حصلنا على القيمة undefined وهي القيمة المعادة من تنفيذ console.log()‎، ويمكننا الآن إدخال سطر JavaScript جديد. استخدام الزر tab للإكمال التلقائي من أجمل مزايا الوضع التفاعلي REPL هو أنه تفاعلي، ففي أثناء كتابك للشيفرة إذا ضغطت على زر tab، فسيحاول الوضع التفاعلي الإكمال التلقائي لما كتبته ليوافق اسم متغير عرفته مسبقًا. استكشاف كائنات JavaScript جرِّب إدخال اسم كائن من كائنات JavaScript مثل Number وأضف إليه نقطةً ثم اضغط على tab، إذ سيعرض لك الوضع التفاعلي جميع الخاصيات والتوابع التي يمكنك الوصول إليها في ذاك الكائن. استكشاف الكائنات العامة يمكنك معاينة الكائنات العامة بكتابة global.‎ ثم الضغط على الزر tab: المتغير الخاص _ إذا كتبت _ بعد شيفرة ما، فسيؤدي ذلك إلى طباعة ناتج آخر عملية. الأوامر ذوات النقط يملك الوضع التفاعلي REPL بعض الأوامر الخاصة وكلها تبدأ بنقطة . وهي: ‎.help: يظهر المساعدة للأوامر ذوات النقط. ‎.editor: تفعيل وضع المحرر، وذلك لكتابة شيفرة JavaScript متعددة الأسطر بسهولة، وبعد دخولك في هذا الوضع وكتابتك للشيفرة المطلوبة، فيمكنك إدخال Ctrl+D لتنفيذ الأمر الذي كتبت. ‎.break: إذا كتبت الأمر ‎.break عند كتابتك لتعبير متعدد الأسطر، فستلغي أي مدخلات قادمة، ومثله مثل الضغط على Ctrl+C. ‎.clear: إعادة ضبط سياق الوضع التفاعلي إلى كائن فارغ وإزالة أي تعابير متعددة الأسطر جرت كتابتها. ‎.load: تحميل ملف JavaScript بمسار نسبي إلى مجلد العمل الحالي. ‎.save: حفظ ما أدخلته في الجلسة التفاعلية إلى ملف مع تمرير مسار الملف بعد كتابة ‎.save. ‎‎.exit: الخروج من الوضع التفاعلي وهو يماثل الضغط على Ctrl+C مرتين. يعرِف الوضع التفاعلي REPL متى تكتب تعبيرًا متعدد الأسطر دون الحاجة إلى تشغيل الأمر ‎.editor، فإذا بدأت مثلًا بكتابة حلقة تكرار مثل هذه: [1, 2, 3].forEach(num => { ثم ضغطت على زر enter، فسيعرف الوضع التفاعلي أنك تكتب تعبيرًا متعدد الأسطر ويبدأ سطرًا جديدًا بثلاث نقط …، مما يشير إلى أنك ما زلت تعمل على القسم أو المقطع نفسه: ... console.log(num) ... }) إذا كتبت ‎.break في آخر السطر، فسيتوقف وضع تعدد الأسطر ولن يُنفَّذ التعبير الذي كتبته. تمرير الوسائط من سطر الأوامر إلى Node.js سنشرح في هذا القسم كيفية استقبال وسائط arguments في برنامج Node.js مُمرَّرة من سطر الأوامر، إذ يمكنك تمرير أي عدد من الوسائط أثناء تشغيل برنامج Node.js باستخدام الأمر: node app.js يمكن أن تكون الوسائط بمفردها، أو على شكل زوج من المفاتيح والقيم مثل: node app.js ahmed أو node app.js name=ahmed يجعل هذا طريقة الحصول على القيمة مختلفةً في شيفرة Node.js. إذ أنّ طريقة الحصول على الوسائط هي استخدام الكائن process المبني في Node.js، ففيه الخاصية argv التي هي مصفوفة تحتوي على جميع الوسائط الممررة عبر سطر الأوامر؛ ويكون الوسيط الأول هو المسار الكامل للأمر node، بينما الوسيط الثاني هو مسار الملف المُنفَّذ كاملًا، وجميع الوسائط الإضافية ستكون موجودةً من الموضع الثالث إلى آخره، كما يمكنك المرور على جميع المعاملات بما في ذلك مسار node ومسار الملف المُنفَّذ باستخدام حلقة تكرار: process.argv.forEach((val, index) => { console.log(`${index}: ${val}`) }) يمكنك الحصول على الوسائط الإضافية من خلال إنشاء مصفوفة جديدة تستثني أول قيمتين: const args = process.argv.slice(2) إذا كان لديك معامل بمفرده دون مفتاح مثل: node app.js ahmed فيمكنك الوصول إليه كما يلي: const args = process.argv.slice(2) args[0] أما في حالة كان الوسيط هو مفتاح وقيمة كما في: node app.js name=ahmed فإن قيمة args[0]‎ هي name=ahmed، وستحتاج إلى تفسيرها، وأفضل طريقة هي استخدام المكتبة minimist التي تساعدنا بالتعامل مع الوسائط: const args = require('minimist')(process.argv.slice(2)) args['name'] // ahmed إرسال تطبيق Node.js المخرجات إلى سطر الأوامر سنتعلم كيفية الطباعة إلى سطر الأوامر باستخدام Node.js بدءًا من الاستخدام الأساسي للتابع console.log حتى وصولنا إلى الأمور المعقدة. طباعة المخرجات باستخدام الوحدة console توفِّر Node.js الوحدة console التي توفر عددًا كبيرًا من الطرائق المفيدة في التعامل مع سطر الأوامر، وهي تشبه إلى حد ما الكائن console الموجود في المتصفحات، والتابع الأساسي الأكثر استخدامًا في هذه الوحدة هو التابع console.log()‎، الذي يطبع السلسلة النصية التي تمررها إليه إلى الطرفية، وإذا مررت كائنًا فسيعرضه على أساس سلسلة نصية، كما يمكنك تمرير قيمًا متعددةً إلى التابع console.log()‎ كما يلي، إذ ستطبع Node.js القيمتين x وy معًا: const x = 'x' const y = 'y' console.log(x, y) يمكنك أيضًا تنسيق السلاسل النصية بتمرير القيم ومُحدِّد التنسيق كما في المثال التالي: console.log('My %s has %d years', 'cat', 2) إذ أنّ محددات التنسيق format specifiers هي: ‎%s: تنسيق المتغير على أساس سلسلة نصية. ‎%d أو ‎%i: تنسيق المتغير على أساس عدد صحيح. ‎%f: تنسيق المتغير على أساس عدد ذي فاصلة عشرية. ‎%O: تنسيق المتغير على أساس كائن كما في المثال البسيط الآتي: console.log('%O', Number) مسح محتوى الطرفية يمسح التابع console.clear()‎ محتوى الطرفية، لكن يختلف سلوكه اعتمادًا على الطرفية المستخدَمة. عد العناصر يُعَدّ التابع console.count()‎ مفيدًا في بعض الحالات التي تريد فيها عدّ المرات التي طُبِعَت فيها السلسلة النصية وإظهار الرقم بجوارها، خذ هذا المثال: const x = 1 const y = 2 const z = 3 console.count( 'The value of x is ' + x + ' and has been checked .. how many times?' ) console.count( 'The value of x is ' + x + ' and has been checked .. how many times?' ) console.count( 'The value of y is ' + z + ' and has been checked .. how many times?' ) يمكننا إحصاء عدد البرتقالات والتفاحات التي لدينا: const oranges = ['orange', 'orange'] const apples = ['just one apple'] oranges.forEach(fruit => { console.count(fruit) }) apples.forEach(fruit => { console.count(fruit) }) طباعة تتبع مكدس الاستدعاء قد تكون هنالك حالات من المفيد فيها طباعة تتبع مكدس الاستدعاء call stack trace لإحدى الدوال، وذلك للإجابة مثلًا على السؤال التالي: كيف وصلنا إلى هذا الجزء من الشيفرة، إذ يمكنك استخدام التابع console.trace()‎: const function2 = () => console.trace() const function1 = () => function2() function1() سيطبع مكدس الاستدعاء ما يلي، وهذا هو الناتج إذا جربنا الشيفرة السابقة في النمط التفاعلي REPL في Node.js: Trace at function2 (repl:1:33) at function1 (repl:1:25) at repl:1:1 at ContextifyScript.Script.runInThisContext (vm.js:44:33) at REPLServer.defaultEval (repl.js:239:29) at bound (domain.js:301:14) at REPLServer.runBound [as eval] (domain.js:314:12) at REPLServer.onLine (repl.js:440:10) at emitOne (events.js:120:20) at REPLServer.emit (events.js:210:7) طباعة الزمن المستغرق يمكنك ببساطة حساب مقدار الوقت الذي أخذته الدالة للتنفيذ باستخدام التابعين time()‎ وtimeEnd()‎ كما في المثال التالي: const doSomething = () => console.log('test') const measureDoingSomething = () => { console.time('doSomething()') // افعل شيئًا وقس الوقت المستغرق لتنفيذه doSomething() console.timeEnd('doSomething()') } measureDoingSomething() مجرى الخرج القياسي stdout والخطأ القياسي stderr رأينا أنّ التابع console.log()‎ رائع لطباعة الرسائل في الطرفية، وهذا ما يسمى مجرى الخرج القياسي standard output stream أو stdout؛ أما التابع console.error()‎ فسيطبع الرسائل المرسَلة إلى مجرى الخطأ القياسي stderr والتي لن تظهر في الطرفية وإنما ستظهر في سجل الخطأ. طباعة المخرجات بألوان مختلفة يمكنك أن تغيير لون المخرجات النصية في الطرفية باستخدام تسلسلات التهريب escape sequences، وتسلسل التهريب هو مجموعة من المحارف التي تُعرِّف لونًا معينًا في الطرفية مثل: console.log('\x1b[33m%s\x1b[0m', 'hi!') يمكنك تجربة ذلك باستخدام النمط التفاعلي في Node.js وستُعرَض السلسلة النصية hi!‎ باللون الأصفر، لكن هذه الطريقة ذات مستوى منخفض، فأبسط طريقة لتلوين المخرجات في الطرفية هي باستخدام مكتبة مثل Chalk، حيث يمكنها أيضًا إجراء عمليات التنسيق الأخرى إضافةً إلى تلوينها للنص مثل جعل النص غامقًا أو مائلًا أو مسطرًا تحته، كما يمكنك تثبيتها باستخدام npm install chalk ثم استخدامها: const chalk = require('chalk') console.log(chalk.yellow('hi!')) من المؤكد أن استخدام chalk.yellow أكثر راحةً من محاولة تذكُّر شيفرات التهريب وستكون قابلية قراءة الشيفرة أفضل. إنشاء شريط تقدم في الطرفية تُعَدّ حزمة Progress حزمةً رائعةً لإنشاء شريط تقدُّم في الطرفية، حيث يمكنك تثبيتها باستخدام npm install progress ببساطة. تُنشِئ الشيفرة التالية شريط تقدُّم بعشر خطوات، حيث ستكتمل خطوة كل 100 ميللي ثانية، وحين اكتمال الشريط سنصفِّر العداد: const ProgressBar = require('progress') const bar = new ProgressBar(':bar', { total: 10 }) const timer = setInterval(() => { bar.tick() if (bar.complete) { clearInterval(timer) } }, 100) قبول المدخلات من سطر الأوامر يمكننا جعل تطبيق Node.js الذي يعمل من سطر الأوامر تفاعليًا باستخدام وحدة readline المبنية في أساس Node.js. أصبحت Node.js بدءًا من الإصدار السابع توفِّر الوحدة readline التي تحصل على المدخلات من مجرى قابل للقراءة مثل process.stdin (مجرى الدخل القياسي stdin) سطرًا بسطر والذي يكون أثناء تنفيذ برنامج Node.js من سطر الأوامر هو الدخل المكتوب في الطرفية. const readline = require('readline').createInterface({ input: process.stdin, output: process.stdout }) readline.question(`What's your name?`, (name) => { console.log(`Hi ${name}!`) readline.close() }) تسأل الشيفرة السابقة المستخدِم عن اسمه، وبعد كتابة المستخدِم الاسم والضغط على enter سنرسل تحيةً له. يُظهِر التابع question()‎ المعامل parameter الأول -أي السؤال- وينتظر مدخلات المستخدِم، ثم يستدعي دالة رد نداء عندما يضغط المستخدِم على زر enter. لاحظا أننا أغلقنا واجهة readline في دالة رد النداء، كما توفِّر الواجهة readline توابعًا كثيرةً متعددةً وسنتركك لاستكشافها من توثيقها؛ أما إذا احتجت إلى إدخال كلمة مرور، فمن الأفضل إظهار رمز * بدلًا منها، وأسهل طريقة لذلك هو استخدام الحزمة readline-sync التي تشبه readline في الواجهة البرمجية وتستطيع التعامل مع هذه الحالة دون عناء، والخيار الممتاز والمتكامل هو الحزمة inquirer.js، حيث يمكنك تثبيتها باستخدام npm install inquirer، كما يمكنك إعادة كتابة الشيفرة السابقة كما يلي: const inquirer = require('inquirer') var questions = [{ type: 'input', name: 'name', message: "What's your name?", }] inquirer.prompt(questions).then(answers => { console.log(`Hi ${answers['name']}!`) }) تسمح لك مكتبة inquirer.js بفعل أمور كثيرة مثل الأسئلة متعددة الخيارات وأزرار الانتقاء والتأكيدات وغير ذلك، كما من المفيد معرفة جميع الخيارات المتاحة أمامنا خصوصًا التي توفِّرها Node.js، لكن إذا أردت التعامل مع مدخلات سطر الأوامر كثيرًا، فالخيار الأمثل هو مكتبة inquirer.js. ترجمة -وبتصرّف- للفصل Command Line من كتاب The Node.js handbook لصاحبه Flavio Copes. اقرأ أيضًا المقال التالي: دليلك الشامل إلى مدير الحزم npm في Node.js المقال السابق: مقدمة إلى Node.js تأمين تطبيق Node.js يعمل على الحاويات باستخدام Nginx و Let’s Encrypt و Compose Docker نشر تطبيق Node.js على الويب: خدمة هيروكو (Heroku) مثالًا
  21. سيُطلَب منك بلا شك خلال عملية التطوير تشغيل بعض الأوامر في الطرفية Terminal أو على سطر الأوامر Command Line، لذلك سنقدّم من خلال هذا المقال مقدمة عن الطرفية، والأوامر الأساسية التي ستحتاج إلى إدخالها، وكيفية ربط الأوامر مع بعضها بعضًا، وإضافة أدوات واجهة سطر الأوامر Command Line Interface -أو CLI اختصارًا- الخاصة بك. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت. الهدف: فهم سطر الأوامر أو الطرفية، والأوامر الأساسية التي يجب تعلمها، وكيفية تثبيت أدوات سطر أوامر جديدة. الطرفية Terminal الطرفية هي واجهة نصية تُستخدَم لتنفيذ البرامج النصية. إذا شغّلتَ أدوات تطوير الويب، فهناك فرصة كبيرة لفتح سطر الأوامر وتشغيل بعض الأوامر لاستخدام الأدوات التي اخترتها، إذ سيُشار إلى هذه الأدوات باسم أو أدوات واجهة سطر الأوامر Command Line Interface أو أدوات CLI اختصارًا. يمكن استخدام عدد كبير من الأدوات عن طريق كتابة الأوامر في سطر الأوامر، ويكون العديد منها مثبتًا مسبقًا على نظامك، ويمكن تثبيت أدوات أخرى من سجلات الحزم التي تشبه متاجر التطبيقات، ولكن تُستخدَم غالبًا للأدوات والبرامج المستندة إلى سطر الأوامر. يفتقر سطر الأوامر إلى تجربة المستخدم، بل إن التجربة الأولى مع سطر الأوامر غالبًا ما تكون متعبة وصعبة الاستخدام نسبيًا، إذ يحتوي على شاشة فارغة ومؤشر وامض مع قليل من المساعدة الواضحة المتاحة بشأن ما يجب تطبيقه، لكننا نعدك أن استخدامه سيصبح أسهل مع قليل من التوجيه والممارسة، وسنُساعدك في هذا المقال للبدء في هذه البيئة. نشأت الطرفية في الخمسينات والستينات من القرن الماضي، ولكن شكلها الأصلي لا يشبه ما نستخدمه اليوم. بقيت الطرفية منذ ذلك الحين ميزة ثابتة في جميع أنظمة التشغيل من أجهزة سطح المكتب إلى خوادم السحابة إلى الحواسيب الصغيرة مثل Raspberry PI Zero وحتى الهواتف المحمولة. توفر الطرفية وصولًا مباشرًا إلى نظام ملفات الحاسوب الأساسي والميزات منخفضة المستوى، وبالتالي تُعَد مفيدة لأداء المهام المعقدة بسرعة. كما أنها مفيدة في عملية الأتمتة مثل كتابة أمر لتحديث أسماء مئات الملفات فوريًا كتعديل اسم الملف "ch01-xxxx.png" إلى "ch02-xxxx.png"، فإذا حدّثتَ أسماء الملفات باستخدام تطبيق Finder أو Explorer GUI، فسيستغرق الأمر وقتًا طويلًا. يمكنك من خلال الشكل الآتي رؤية بعض الأشكال المختلفة للبرامج المتوفرة التي يمكن أن تنقلك إلى الطرفية، وتظهَر موجِّهات الأوامر المتوفرة في ويندوز مثل برامج Cmd و Powershell التي يمكن تشغيلها من قائمة ابدأ عن طريق كتابة اسم البرنامج. كما يوضّح الشكل التالي تطبيق طرفية نظام ماك macOS: كيفية الوصول إلى الطرفية يستخدم العديد من المطورين حاليًا أدوات تستند إلى يونيكس مثل الطرفية والأدوات التي يمكنك الوصول إليها من خلالها، إذ تدعم العديد من الأدوات الموجودة على الويب الأنظمة التي تستند إلى يونيكس، ولكن لا داعي للقلق، فهي متوفرة في معظم الأنظمة. سنوضّح فيما يلي كيفية الوصول إلى الطرفية على النظام الذي تريده. لينكس أو يونيكس: تحتوي أنظمة لينكس أو يونيكس طرفية متاحة افتراضيًا مدرجَة ضمن تطبيقاتك. نظام ماك macOS: يحتوي نظامًا يسمى داروين Darwin يقع أسفل واجهة المستخدم الرسومية ويشبه نظام يونيكس، ويوفر الطرفية والوصول إلى الأدوات ذات المستوى المنخفض. الطرفية متاحة على نظام ماك من خلال المسار Applications/Utilities/Terminal. ويندوز: لم يكن استخدام الطرفية أو سطر الأوامر على ويندوز بسيطًا أو سهلًا كما هو الحال في أنظمة التشغيل الأخرى، ولكن الأمور تتحسن مع مرور الوقت. لطالما امتلك ويندوز برنامجًا شبيهًا بالطرفية يسمى Cmd أو موجه الأوامر، ولكنه ليس متكافئًا مع أوامر يونيكس، بل يكافئ موجه ويندوز دوز Windows DOS ذي النمط القديم. توجد برامج أفضل لتوفير طرفية على ويندوز مثل PowerShell وGitbash الذي يكون جزءًا من مجموعة أدوات git for Windows. لكن أفضل خيار لنظام ويندوز حاليًا هو نظام ويندوز الفرعي لنظام لينكس Windows Subsystem for Linux -أو WSL اختصارًا، وهو طبقة توافق لتشغيل أنظمة تشغيل لينكس مباشرة من داخل الإصدار رقم 10 من ويندوز، مما يتيح لك تشغيل طرفية حقيقية مباشرة على ويندوز دون الحاجة إلى آلة افتراضية، ويمكن تثبيت نظام WSL مباشرة من متجر ويندوز مجانًا. نوصي بشدة بتثبيت نظام WSL، ويمكنك التمسك باستخدام موجّه الأوامر الافتراضي cmd إذ ستعمل أدوات متعددة بطريقة صحيحة، ولكنك ستجد الأمور أسهل إذا كان لديك تكافؤ أفضل مع أدوات يونيكس. هناك فرق بين الطرفية وسطر الأوامر، فالطرفية تقنيًا هي برنامج يبدأ ويتصل بالصدفة Shell التي تُعَد بيئة جلستك إذ يمكنك تخصيص أشياء مثل الموجه والاختصارات، بينما سطر الأوامر هو السطر الحرفي الذي تكتب فيه الأوامر ويومض المؤشر. إذا استخدمت أدواتٍ مثل Visual Studio Code، فهناك مجموعة كبيرة من الامتدادات التي يمكن استخدامها كوكيل Proxy لاستخدام أوامر الطرفية دون الحاجة إلى استخدامها مباشرة، وبالرغم من وجود الكثير من الأدوات المتاحة في سطر الأوامر. لكنك لن تجد امتدادًا لمحرر الشيفرات لكل ما تريده، لذلك سيتعين عليك اكتساب خبرة في استخدام الطرفية في النهاية. أوامر المدمجة الأساسية في الطرفية إليك بعض الأشياء التي يمكن لسطر الأوامر تطبيقها، بالإضافة إلى أسماء الأدوات ذات الصلة بكل حالة: انقل نظام ملفات حاسوبك إلى جانب مهام المستوى الأساسي مثل الإنشاء والنسخ وإعادة التسمية والحذف: التنقل بين المجلدات باستخدام الأمر cd. إنشاء مجلد باستخدام الأمر mkdir. إنشاء ملفات وتعدّيل بياناتها الوصفية باستخدام الأمر touch. نسخ الملفات باستخدام الأمر cp. نقل الملفات باستخدام الأمرmv. حذف الملفات أو المجلدات باستخدام الأمر rm. تنزيل الملفات الموجودة في عناوين URL محدَّدة باستخدام الأمر curl. البحث عن أجزاء نصية ضمن مجموعات نصية أكبر باستخدام الأمر grep. عرض محتويات ملف بمقدار صفحة تلو الأخرى باستخدام الأمر less وcat. معالجة وتحويل مجاري النصوص مثل تغيير جميع وسوم <div> في ملف HTML إلى <article> باستخدام الأوامر awk وtr وsed. التنقل في سطر الأوامر ستحتاج حتمًا إلى الانتقال إلى مسار معين عندما تزور سطر الأوامر. تشغّل جميع أنظمة التشغيل برنامجها الطرفي في المسار الموجود فيه سطر الأوامر، وغالبًا سترغبُ بالانتقال إلى لمسار مختلف. يتيح لك الأمر cd تغيير الدليل Change Directory، وليس هذا الأمر برنامجًا وإنما مبني مسبقًا، إذ لا يمكنك حذفه عن طريق الخطأ. لا داعي للقلق كثيرًا بشأن ما إذا كان الأمر مبنيًا مسبقًا أم لا، ولكن ضع في بالك أن الأوامر المبنية مسبقًا تظهر في جميع الأنظمة القائمة على نظام يونيكس. يمكنك تغيير المسار من خلال كتابة الأمر cd في الطرفية متبوعًا بالمجلد الذي تريد الانتقال إليه. يمكنك استخدام الأمر cd Desktop بافتراض وجود المجلد ضمن المسار الرئيسي كما يلي: اكتب الأمر التالي في طرفية نظامك: cd Desktop إذا أردتَ الرجوع إلى المسار السابق، فيمكنك استخدام نقطتين كما يلي: cd.. هناك اختصار في الطرفية مفيد جدًا وهو استخدام مفتاح tab لإكمال الأسماء تلقائيًا التي تعرف أنها موجودة بدلًا من كتابة اسم المجلد بالكامل فمثلًا كتابة الأمر cd D ثم الضغط على مفتاح tab، سيكمل سطر الأوامر كتابة اسم المجلد Desktop نيابةً عنك، بشرط وجوده في المجلد الحالي. إذا كان المجلد الذي تريد الانتقال إليه متداخلًا، فيجب أن تعرف المسار للوصول إليه، إذ يصبح ذلك أسهل عندما تصبح أكثر دراية بمعمارية نظام ملفاتك، ولكن إن لم تكن متأكدًا من المسار، فيمكنك اكتشافه باستخدام الأمر ls، ومن خلال النقر في نافذة المستكشف Explorer أو Finder لمعرفة مكان المجلد بناءً على مكان وجودك حاليًا. إذا أردت الانتقال مثلًا إلى مجلد يسمى src يقع ضمن مجلد اسمه project موجود على سطح المكتب Desktop، فيمكنك كتابة هذه الأوامر الثلاثة للوصول إلى هناك من مجلدك الرئيسي كما يلي: cd Desktop cd project cd src ولكن يمكنك كتابة أمر واحد مع العناصر المختلفة في المسار مفصول بينها بشرطة مائلة للأمام، تمامًا كما تفعل عند تحديد مسارات للصور أو الأصول الأخرى في شيفرة CSS أو HTML أو جافاسكربت: cd Desktop/project/src لاحظ أن تضمين شرطة مائلة في المسار يجعله مسارًا مطلقًا مثل ‎/Users/your-user-name/Desktop، إذ يؤدي حذف الشرطة المائلة في المقدمة كما فعلنا سابقًا إلى جعل المسار مسارًا نسبيًا متعلقًا بمجلد العمل الحالي، وهذا هو بالضبط ما تراه مع عناوين URL في متصفح الويب، إذ تعني الشرطة المائلة في البداية جذر موقع الويب، بينما يعني حذف الشرطة المائلة أن عنوان URL متعلق بصفحتي الحالية. يستخدم نظام ويندوز الشرطة المائلة للخلف بدلًا من الشرطة المائلة للأمام مثل الأمر التالي: cd Desktop\project\src سرد محتويات مجلد الأمر ls مبني مسبقًا في نظام التشغيل يونيكس وهو اختصار للكلمة الأجنبية List، ووظيفته سرد محتويات المجلد الذي تتواجد فيه حاليًا. لاحظ أن هذا الأمر لن ينجح إذا استخدمتَ موجّه أوامر ويندوز الافتراضي cmd، فالأمر المكافئ له هو dir. شغّل الأمر التالي في الطرفية: ls يمنحك الأمر ls قائمة بالملفات والمجلدات الموجودة في مجلد العمل الحالي، ولكنها معلومات أساسية، وستحصل فقط على اسم كل عنصر موجود دون معرفة نوعه. يمكن أن يمنحك تغيير بسيط في استخدام هذا الأمر كثيرًا من المعلومات. خيارات الأوامر تحتوي معظم أوامر الطرفية على خيارات، هي المعدّلات Modifiers التي تضيفها إلى نهاية الأمر، مما يجعله يتصرف بطريقة مختلفة. تتكون هذه الخيارات عادةً من مسافة بعد اسم الأمر، متبوعة بشرطة، متبوعة بحرف واحد أو أكثر. جرب الأمر التالي على سبيل المثال، وشاهد النتيجة: ls -l يمنحك الخيار ‎-l في الأمر ls قائمة بملف أو مجلد واحد في كل سطر ومعلومات أخرى. يمكن التعرف على المجلدات من خلال البحث عن الحرف "d" على الجانب الأيسر من السطور، إذ يمكننا استخدام الأمر cd مع هذه المجلدات. يوضّح الشكل الآتي طرفية نظام ماك الصرفة في الجزء العلوي، وطرفية مخصّصة مع بعض الرموز والألوان الإضافية في الجزء السفلي، وكلاهما يعرض نتائج تشغيل الأمر ls -l: الإنشاء والنسخ والنقل والإزالة هناك عدد من الأوامر الأساسية الأخرى التي يُحتمَل أن تستخدمها كثيرًا أثناء عملك مع الطرفية. هذه الأوامر بسيطة جدًا، لذا لن نشرحها بقدر كبير من التفاصيل مثل الأمرين السابقين. استخدم أمثلة الأوامر التالية لفهمها: mkdir: يؤدي هذا الأمر إلى إنشاء مجلد جديد ضمن المجلد الحالي الذي تتواجد فيه، مع الاسم الذي تقدمه بعد الأمر، فمثلًا سينشئ الأمر mkdir my-awesome-website مجلدًا جديدًا اسمه my-awesome-website. rmdir: يزيل هذا الأمر المجلد المحدد إذا كان فارغًا فقط، فمثلًا سيزيل الأمر rmdir my-awesome-website المجلد الذي أنشأناه سابقًا. إذا أردتَ إزالة مجلد غير فارغ وإزالة كل ما يحتويه، فيمكنك استخدام الخيار ‎-r وهو اختصار recursive تكراري، ولكن يجب الانتباه عند استخدامه، إذ يجب أن تتأكد من عدم وجود أيّ شيء يمكن أن تحتاجه داخل المجلد، إذ سيختفي إلى الأبد. touch: ينشئ هذا الأمر ملفًا فارغًا جديدًا ضمن المجلد الحالي، فمثلًا ينشئ الأمر touch mdn-example.md ملفًا فارغًا جديدًا اسمه mdn-example.md. mv: ينقل هذا الأمر ملفًا من موقع الملف الأول المحدَّد إلى موقع الملف المحدد الثاني، وينقل الأمر mv mdn-example.md mdn-example.txt الملف mdn-example.md في المجلد الحالي إلى الملف mdn-example.txt في المجلد الحالي (تُكتَب المواقع كمسارات ملفات)، إذ يُعَد الملف منقولًا، ولكن هذا الأمر يعيد تسمية الملف فعليًا. cp: يشبه الأمر mv في الاستخدام، إذ ينشئ الأمر cp نسخة من الملف الموجود ضمن الموقع الأول المحدد في الموقع الثاني المحدَّد، إذ يُنشئ الأمر cp mdn-example.txt mdn-example.txt.bak نسخة من الملف mdn-example.txt بالاسم mdn-example.txt.bak، ويمكنك بالطبع تسمية هذه النسخة باسم آخر إذا أدرتَ ذلك. rm: يزيل هذا الأمر الملف المحدَّد، فمثلًا يحذف الأمر rm mdn-example.txt ملفًا اسمه mdn-example.txt. لاحظ أن هذا الحذف نهائي ولا يمكن التراجع عنه عبر سلة المحذوفات الموجودة غالبًا على واجهة مستخدم سطح المكتب. تسمح لك العديد من أوامر الطرفية باستخدام العلامات النجمية بوصفها محارف بدل Wild Card مع أيّ تسلسل من المحارف، مما يسمح بتشغيل عملية على عدد كبير من الملفات في الوقت نفسه، وتتطابق جميعها مع النمط المحدد، إذ يحذف الأمر rm mdn-*‎ جميع الملفات التي تبدأ بسلسلة المحارف mdn-‎، بينما يحذف الأمر rm mdn-*.bak جميع الملفات التي تبدأ بسلسلة المحارف mdn-‎ وتنتهي بسلسلة المحارف ‎.bak. هل تعد الطرفية خطيرة؟ عليك توخي الحذر عند استخدام الطرفية. لا تحمل الأوامر البسيطة الكثير من المخاطر، ولكن إذا جمّعتَ أوامرًا أكثر تعقيدًا، فعليك التفكير مليًا فيما ستفعله هذه الأوامر، ومحاولة اختبارها أولًا قبل تشغيلها في المجلد المحدَّد. لنفترض أن لديك 1000 ملف نصي في مجلد، وأردت الاطلاع عليها جميعًا وحذف الملفات التي يحتوي اسمها على سلسلة فرعية معينة. إن لم تكن حذرًا، يمكن أن ينتهي بك الأمر بحذف شيء مهم، وبالتالي تفقد عملك خلال هذه العملية. إحدى العادات الجيدة التي يجب عليك اتباعها هي كتابة الأمر ضمن محرر نصوص، ثم إنشاء نسخة احتياطية من مجلدك وتشغيل الأمر على هذه النسخة أولًا لاختباره. إن لم تكن مرتاحًا لتجربة أوامر الطرفية على جهازك الخاص، فيمكنك تجربتها في مكان آمن هو Glitch.com الذي يُعَد مكانًا رائعًا لتجربة شيفرة تطوير الويب، بالإضافة إلى إمكانية الوصول إلى طرفية، إذ يمكنك تشغيل كل هذه الأوامر مباشرة فيها. يُعَد tldr.sh من الموارد الرائعة للحصول على نظرة عامة وسريعة على أوامر طرفية معينة، وهي خدمة توثيق مقادَة من المجتمع على غرار MDN، ولكنها خاصة بالأوامر الطرفية. لنتعلّم الآن كيفية توصيل الأدوات مع بعضها في سطر الأوامر. ربط الأوامر باستخدام الأشرطة العمودية تعرّفنا سابقًا على الأمر ls الذي يعرض محتويات المجلد الحالي: ls ولكن إذا أردنا حساب عدد الملفات والمجلدات بسرعة ضمن المجلد الحالي، فلن يستطيع الأمر ls ذلك من تلقاء نفسه. هناك أداة أخرى متاحة من يونيكس تسمى wc تحسب عدد الكلمات أو الأسطر أو المحارف أو البايتات لكل ما يُوضَع فيها، إذ يمكن أن يكون ذلك ملفًا نصيًا، إذ ينتج عن تنفيذ الأمر التالي عدد الأسطر في الملف myfile.txt: wc -l myfile.txt كما يمكنها حساب عدد الأسطر لخرج أي أمر يُربَط معها من خلال رمز الشريط العمودي |، وسيحسبُ الأمر التالي عدد الأسطر الناتجة عن الأمر ls -أي ما سيطبعه إلى الطرفية إذا شُغِّل بمفرده- ويخرج عدد الأسطر في الطرفية: ls | wc -l بما أن الأمر ls يطبع كل ملف أو مجلد على سطر خاص به، فهذا يعطينا عدد الأدلة والملفات بفعالية. تطبع أدوات سطر أوامر يونيكس النص إلى الطرفية، ويشار إليها بالطباعة إلى الخرج المعياري Printing To Standard Output أو STDOUT اختصارًا، إذ يمكن لعدد كبير من الأوامر قراءة المحتوى من مجرى الدخل المعروف باسم الدخل المعياري Standard Input أو STDIN اختصارًا. يربط المعامل | هذه المدخلات والمخرجات مع بعضها، مما يسمح ببناء عمليات أكثر تعقيدًا لتناسب احتياجاتنا، إذ يمكن أن يصبح خرج أحد الأوامر دخلًا للأمر الذي بعده. يطبع الأمر ls عادةً خرجه إلى STDOUT، ولكن يُربَط خرجه مع الأمر wc من خلال الرمز | سيأخذ الأمر wc هذا الخرج بوصفه دخلًا له، ويحسب عدد الأسطر، ويطبعها إلى STDOUT. مثال أكثر تعقيدا سنحاول أولًا جلب محتويات صفحة "fetch" من MDN باستخدام الأمر curl الذي يمكن استخدامه لطلب المحتوى من عناوين URL مثل الرابط الآتي: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch كما يلي: curl https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch لن تحصل على خرج بسبب إعادة توجيه الصفحة إلى ‎/Web/API/fetch، إذ يجب إعلام الأمر curl صراحةً باتباع عمليات إعادة التوجيه باستخدام الراية ‎-L. لنلقِ نظرةً على الترويسات التي يعيدها developer.mozilla.org باستخدام الراية ‎-I الخاصة بالأمر curl، ونطبع جميع عمليات إعادة التوجيه التي يرسلها الموقع إلى الطرفية عن طريق ربط خرج الأمر curl مع الأمر grep باستخدام الرمز |، وسنطلبُ من الأمر grep إعادة جميع الأسطر التي تحتوي على الكلمة "location". حاول تشغيل الأمر التالي (لاحظ أن هناك إعادة توجيه واحدة فقط قبل أن نصل إلى الصفحة الأخيرة): curl https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch -L -I | grep location يجب أن يبدو الخرج كما يلي، إذ سينتج الأمر curl أولًا بعض عدّادات التنزيل أو ما شابه ذلك: location: /en-US/docs/Web/API/fetch كما يمكننا تحويل محتويات سطور الموقع location:‎، وإضافة الأصل الأساسي إلى بداية كل منها لنحصل على عناوين URL كاملة مطبوعة، لذلك سنضيف الأمر awk، وهي لغة برمجة تشبه جافاسكربت أو روبي Ruby أو بايثون Python، ولكنها أقدم منها. شغّل الأمر التالي: curl https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch -L -I | grep location | awk '{ print "https://developer.mozilla.org" $2 }' ويجب أن يكون الخرج النهائي كما يلي: https://developer.mozilla.org/en-US/docs/Web/API/fetch خصّصنا الخرج من خلال دمج هذه الأوامر لإظهار عناوين URL الكاملة التي يعيد خادم Mozilla التوجيه من خلالها عندما نطلب العنوان ‎/docs/Web/API/WindowOrWorkerGlobalScope/fetch. إضافة مزايا ألقينا نظرة على بعض الأوامر المبنية مسبقًا التي يأتي نظامك مزودًا بها، ولنلقِ نظرة الآن على كيفية تثبيت أداة CLI لجهة خارجية والاستفادة منها. يتوفر حاليًا النظام المجتمعي الواسع للأدوات القابلة للتثبيت لتطوير واجهة الويب الأمامية ضمن npm، وهي خدمة استضافة حزم يملكها القطاع الخاص، وتعمل بصورة وثيقة مع Node.js، ولكن يمكنك أن تتوقع رؤية مزيد من مزودي الحزم مع مرور الوقت. يؤدي تثبيت Node.js إلى تثبيت أداة سطر أوامر npm وأداة إضافية تعتمد على npm تسمى npx، وتوفر أداة npm بوابة لتثبيت أدوات سطر أوامر إضافية. يعمل Node.js وnpm بالطريقة نفسها في جميع الأنظمة: ماك macOS وويندوز Windows ولينكس Linux. ثبّت أداة npm على نظامك الآن مع تنزيل وتشغيل مثبّت Node.js المناسب لنظام تشغيلك. إذا طُلب منك ذلك، فتأكد من تضمين npm كجزء من عملية التثبيت. كما سنتعرّف على أداة Prettier، وهي أداة لتنسيق الشيفرات البرمجية وتحتوي على عدة خيارات بسيطة. مكان تثبيت أدوات CLI يمكننا تثبيت الأدوات بطريقة عامة باستخدام npm لنتمكن من الوصول إليها في أي مكان، أو محليًا في مجلد المشروع الحالي. هناك إيجابيات وسلبيات في كل طريقة، وهذه القائمة توضح إيجابيات وسلبيات التثبيت العام للأدوات: 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; } إيجابيات التثبيت العام سلبيات التثبيت العام يمكن الوصول إليها من أي مكان في الطرفية يمكن ألّا تكون متوافقة مع شيفرة مشروعك تُثبَّت مرة واحدة فقط لن يتمكن المطورون الآخرون في فريقك من الوصول إلى هذه الأدوات، مثل مشاركة قاعدة شيفرتك عبر أداة مثل git. يستهلك مساحة أقل على القرص الصلب تتعلق هذه النقطة بالنقطة السابقة، إذ يصعب نسخ شيفرة المشروع (إذا ثبّت أدواتك محليًا، فيمكن إعدادها كاعتماديات وتثبيتها باستخدام الأمر npm install). الإصدار نفسه دائمًا يبدو مثل أي أمر يونيكس آخر يُحتمَل أن يكون التأثير السلبي للتثبيت العام أكبر بكثير من الفوائد بالرغم من أن قائمة السلبيات أقصر، ولكننا سنثبّت الأدوات بطريقة عامة لإبقاء الأمور بسيطة. سنلقي نظرة أكثر على عمليات التثبيت المحلية وفوائدها في المقال التالي. تثبيت Prettier سنثبّت أداة Prettier بوصفها أداة مساعدة عامة لسطر الأوامر، وهي أداة تنسيق شيفرة لمطوري الواجهة الأمامية، وتركز على اللغات المستندة إلى لغة جافاسكربت بالإضافة إلى دعمها لغات HTML و CSS و SCSS و JSON وغير ذلك. يمكن أن تطبّق أداة Prettier ما يلي: حفظ العبء المعرفي المتمثل في جعل النمط متسقًا يدويًا عبر جميع ملفات شيفرتك، إذ يمكن أن تطبّق Prettier ذلك تلقائيًا. مساعدة المتعلمين الجدد في تطوير الويب على تنسيق شيفرتهم من خلال تطبيق أفضل الممارسات. يمكن تثبيتها على أي نظام تشغيل وكجزء مباشر من أدوات المشروع، مما يضمن لزملائك وأصدقائك الذين يعملون على شيفرتك أن يستخدموا نمط الشيفرة الذي تستخدمه. يمكن تهيئتها للتشغيل عند الحفظ، أو أثناء الكتابة، أو حتى قبل نشر الشيفرة. افتح الطرفية بعد تثبيت نود وشغّل الأمر التالي لتثبيت Prettier: npm install --global prettier تصبح بعد ذلك أداة Prettier متاحة في الطرفية وفي أيّ مكان في نظام ملفاتك. سيؤدي تشغيل الأمر بدون أي وسطاء -كما هو الحال مع العديد من الأوامر الأخرى- إلى تقديم معلومات حول كيفية الاستخدام والمساعدة. جرب الأمر التالي وشاهد النتيجة: prettier يجب أن يبدو الخرج كما يلي: Usage: prettier [options] [file/glob ...] By default, output is written to stdout. Stdin is read if it is piped to Prettier and no files are given. … يجب دائمًا الاطلاع على معلومات الاستخدام على الأقل، حتى لو كانت طويلة، لأنها ستساعدك في فهم كيفية استخدام الأداة بطريقة أفضل. لنجرب استخدام الأداة Prettier لنتمكن من معرفة كيفية عملها. أنشئ أولًا مجلدًا جديدًا في مكان ما على نظام ملفاتك يسهل العثور عليه مثل مجلد اسمه prettier-test على سطح المكتب Desktop، ثم احفظ الشيفرة التالية في ملف جديد يسمى index.js ضمن مجلد الاختبار: const myObj = { a:1,b:{c:2}} function printMe(obj){console.log(obj.b.c)} printMe(myObj) يمكننا أن نشغّل prettier في قاعدة الشيفرة للتحقق من حاجة شيفرتنا لتعديلٍ ما. شغّل الأمر cd للانتقال إلى مجلدك، وشغّل الأمر التالي: prettier --check index.js ويجب أن تحصل على الخرج التالي: Checking formatting... index.js Code style issues found in the above file(s). Forgot to run Prettier? لذلك هناك بعض أنماط الشيفرات التي يمكن إصلاحها، سيؤدي إضافة الخيار ‎--write إلى الأمر prettier إلى إصلاح هذه المشاكل، مما يجعلنا نركز على كتابة شيفرة مفيدة فقط. شغّل إصدار الأمر التالي: prettier --write index.js وستحصل على الخرج التالي: Checking formatting... index.js Code style issues fixed in the above file(s). ولكن إذا نظرت إلى ملف جافاسكربت الخاص بك، فستجد أنه منسَّق مثل الشيفرة التالية: const myObj = { a: 1, b: { c: 2 }, }; function printMe(obj) { console.log(obj.b.c); } printMe(myObj); يمكنك جعل هذا التنسيق جزءًا تلقائيًا من عمليتك بناءً على طريقة سير عملك. الأتمتة هي المكان الذي تتفوق فيه الأدوات، وأفضل أتمتة هي الأتمتة الذي "يحدث على الفور" دون الحاجة إلى إعداد أي شيء. هناك عدد من الطرق التي يمكن من خلالها تحقيق الأتمتة مع أداة Prettier. هناك بعض الموارد الممتازة عبر الإنترنت للمساعدة، على الرغم من أن ذلك خارج نطاق هذا المقال. يمكنك استدعاء أداة Prettier: قبل تأكيد شيفرتك في مستودع git باستخدام Husky. عندما تضغط على "حفظ" في محرر الشيفرة، سواء أكان محرر VS Code أو Atom أو Sublime Text. بوصفها جزءًا من فحوصات التكامل المستمرة باستخدام أدوات مثل Github Actions. تفضيلنا الشخصي هو الخيار الثاني -أثناء استخدام محرر VS Code، وتعمل Prettier على تشغيل وتنظيف أي تنسيق تحتاجه في كل عملية حفظ. أدوات أخرى إليك قائمة مختصرة ممتعة من الأدوات لتجربتها: bat: النسخة الأفضل من الأداة cat (تُستخدم cat لطباعة محتويات الملفات). prettyping: نفّذ الأمر ping على سطر الأوامر. تُعَد أداة ping مفيدة للتحقق مما إذا كان الخادم يستجيب. htop: عارض للعمليات، مفيد عندما يكون هناك شيء ما يجعل مروحة وحدة المعالجة المركزية الخاصة بك تتصرف مثل محرك نفاث وتريد تحديد البرنامج المخالف. tldr: متاح بوصفه أداة لسطر الأوامر. لاحظ أن بعض الاقتراحات السابقة يمكن أن تحتاج إلى التثبيت باستخدام npm، كما فعلنا مع Prettier. هذا المقال جزء من سلسلة مقالات بعنوان تعلم تطوير الويب والتي تشرح كامل عملية تطوير الويب من واجهات أمامية وخلفية بالكامل. ترجمة -وبتصرُّف- للمقال Command line crash course. اقرأ أيضًا المقال التالي: أدوات مطوري الويب المدمجة في المتصفحات المقال السابق: فهم أدوات تطوير الويب من طرف العميل كيف تستخدم أدوات المطوِّر في المتصفحات الحديثة الفرق بين صفحة الويب وموقع الويب وخادم الويب ومحرك البحث
  22. يمكن أن تكون أدوات تطوير الويب من طرف العميل غير مفهومة بالنسبة لكثير من المطورين، لذلك سنوضّح من خلال سلسلة من المقالات الغرض من بعض أدوات تطوير الويب الأكثر شيوعًا من طرف العميل، وسنشرح أيضًا الأدوات التي يمكنك ربطها مع بعضها بعضًا، وكيفية تثبيتها باستخدام مدير الحزم، والتحكم فيها باستخدام سطر الأوامر، وسنقّدّم مثالًا كاملًا عن سلسلة أدوات يوضح كيفية زيادة الإنتاجية، ولكن في البداية وقبل محاولة استخدام هذه الأدوات يجب أن تتعلم أساسيات لغات HTML وCSS وجافاسكربت JavaScript. وفّرنا من خلال موسوعة حسوب مرجعًا لكل من لغات HTML وCSS وJavaScript يمكنك البدء منه. سنوضح في هذه السلسلة من المقالات المواضيع التالية لفهم أدوات تطوير الويب من طرف العميل: نظرة عامة على أدوات تطوير الويب من طرف العميل: نقدّم في هذا المقال نظرة عامة على أدوات الويب الحديثة، وأنواع الأدوات المتاحة وأين ستصادفها في دورة حياة تطوير تطبيقات الويب، وكيفية العثور على المساعدة باستخدام هذه الأدوات. دورة مكثفة لفهم سطر الأوامر: سيُطلب منك بلا شك تشغيل بعض الأوامر في الطرفية Terminal أو في سطر الأوامر خلال عملية التطوير، حيث يقدم هذا المقال مقدمة إلى الطرفية والأوامر الأساسية التي ستحتاج إلى إدخالها، وكيفية ربط الأوامر مع بعضها بعضًا، وكيفية إضافة أدوات واجهة سطر الأوامر Command Line Interface -أو CLI اختصارًا. أساسيات إدارة الحزم: سنلقي نظرة على مدراء الحزم بشيء من التفصيل لفهم كيفية استخدامها في مشاريعك لتثبيت اعتماديات Dependencies أدوات المشروع وتحديثها وغير ذلك. سلسلة أدوات كاملة: سنعمل في المقالين الأخيرين من السلسلة على ترسيخ معرفتك بالأدوات من خلال إرشادك خلال عملية بناء نموذج لسلسلة أدوات عن طريق إعداد بيئة تطوير ووضع أدوات التحويل في مكانها لنشر تطبيقك فعليًا على Netlify. كما سنقدم دراسة حالة مع إعداد بيئة التطوير الخاصة بنا وإعداد أدوات تحويل شيفرتنا. نشر التطبيق: سنأخذ في المقال الأخير من هذه السلسلة مثالًا عن سلسلة الأدوات التي أنشأناها في المقال السابق ونضيفها لنتمكن من نشر التطبيق، إذ سنرفع الشيفرة البرمجية للمشروع على موقع مشاركة الشيفرات جيت هب GitHub، وننشرها باستخدام نيتليفاي Netlify، وسنوضّح كيفية إضافة اختبار بسيط لهذه العملية. لنبدأ بمقالنا الأول من هذه السلسلة من خلال إلقاء نظرة سريعة على أدوات تطوير الويب من طرف العميل. المتطلبات الأساسية: الإلمام بمفاهيم لغات HTML و CSS وجافاسكربت الأساسية. الهدف: فهم أنواع الأدوات من طرف العميل وكيفية العثور عليها والحصول على المساعدة بشأنها. استخدام الأدوات الحديثة أصبحت كتابة برمجيات الويب أكثر تعقيدًا بمرور الوقت، وبالرغم من ذلك لا يزال كتابة برمجيات الويب يدويًا بلغة HTML و CSS وجافاسكربت أمرًا ممكنًا إلا أنه يوجد الآن مجموعة كبيرة من الأدوات التي يمكن للمطورين استخدامها لتسريع عملية إنشاء موقع ويب أو تطبيق. توجد بعض الأدوات المستقرة التي أصبحت أسماء مألوفة وشائعة في مجتمع تطوير الويب، بالإضافة إلى الأدوات الجديدة التي نراها تصدر كل يوم لحل مشكلات معينة، فيمكن أن تكتب برنامجًا للمساعدة في عملية التطوير الخاصة بك ولحل مشكلة معينة لا يمكن أن تحلها الأدوات الحالية. يمكنك استخدام عدد هائل من الأدوات وتضمينها في مشروع واحد، كما يمكنك استخدام أداة واحدة مثل Webpack وإعداد ملف تكوينها من مئات الأسطر، وغالبًا ما ينظر المبرمجين المبتدئين لهذه الشيفرة وكأنها تعويذات تؤدي المهمة بطريقة سحرية ولن يفهمها إلا المهندسون الخبراء. إلا أنه في الواقع غالبية الخبراء يواجهون مشاكلًا في استخدام الأدوات من وقت لآخر، إذ يمكن إضاعة ساعات في محاولة تشغيل مسار أدوات قبل لمس سطر واحد من شيفرة التطبيق. لذلك لا داعي للقلق، فأنت لست وحدك. سنزودك من خلال هذا المقال بنقطة بداية مفيدة لفهم أساسيات استخدام أدوات الويب، إذ يُفضَّل أن تبدأ على نطاق صغير، ثم تشق طريقك تدريجيًا إلى الاستخدامات أكثر تقدمًا. نظام أدوات المطورين الحديث يعدّ النظام البيئي Ecosystem الحديث لأدوات المطورين في يومنا الحالي ضخم نسبيًا، لذا يجب من الأفضل تكوين فكرة عامة عن المشاكل الرئيسية التي تحلها هذه الأدوات. إذا انتقلت إلى محرك البحث المفضل لديك وبحثت عن "أدوات مطور الواجهة الأمامية"، فستصل لمجموعة كبيرة من النتائج تتراوح من محرّرات النصوص والمتصفحات وحتى نوع الأقلام التي يمكنك استخدامها لتدوين الملاحظات. وبالرغم من أن اختيارك لمحرّر الشيفرة هو بالتأكيد جزء من خيارات الأدوات، ولكن في هذه السلسلة من المقالات سنتجاوز ذلك من خلال التركيز على أدوات المطور التي تساعدك على إنتاج شيفرة ويب ذات كفاءة عالية. يمكنك تصنيف الأدوات من طرف العميل ضمن الفئات الثلاث التالية: شبكة الأمان Safety Net: أدوات مفيدة أثناء تطوير شيفرتك. التحويل Transformation: الأدوات التي تحول الشيفرة بطريقة ما مثل تحويل لغة وسيطة إلى لغة جافاسكربت التي يمكن أن يفهمها المتصفح. أدوات ما بعد التطوير Post-development: الأدوات المفيدة بعد كتابة شيفرتك مثل أدوات الاختبار والنشر. لنلقِ نظرة على كل واحدة من هذه الفئات بمزيد من التفصيل. شبكة الأمان هي الأدوات التي تحسّن شيفرتك البرمجية التي تكتبها، ويجب أن تكون هذه الأدوات خاصةً ببيئة التطوير الخاصة بك، بالرغم من ذلك من غير المألوف أن يكون لدى الشركات سياسة أو إعدادات جاهزة للتثبيت لكي تضمن أن يستخدم جميع مطوري هذه الشركات العمليات نفسها. تتضمن هذه الأدوات أيّ شيء يسهّل عملية التطوير فيما يتعلق بإنشاء شيفرة مستقرة وموثوقة. كما يجب أن تساعدك أدوات شبكة الأمان في منع حدوث الأخطاء أو تصحيحها تلقائيًا دون الحاجة إلى إنشاء شيفرة من نقطة الصفر في كل مرة. سنوضّح فيما يلي بعض أنواع أدوات شبكة الأمان الشائعة التي يستخدمها المطورون. منقحات الصياغة Linters منقحات الصياغة هي أدوات تتحقق من شيفرتك وتخبرك بوجود أخطاء وبأنواعها وسطور الشيفرة التي توجد فيها. يمكن ضبط منقحات الصياغة للإبلاغ عن الأخطاء، وللإبلاغ عن أيّ انتهاكات لدليل النمط المحدَّد الذي يستخدمه فريقك مثل الشيفرة التي تستخدم عددًا خاطئًا من المسافات البادئة، أو استخدام صياغة قالب Template Literals بدلًا من صياغة سلسلة نصية عادية String Literals. يعدّ Eslint منقّح أخطاء معياري خاص بلغة جافاسكربت ويمكن ضبط هذه الأداة لاكتشاف أخطاء الصياغة المحتملة وتشجيعك لاستخدام أفضل الممارسات ضمن شيفرتك البرمجية. شاركت بعض الشركات والمشاريع إعدادات Eslint الخاصة بها، ويمكنك العثور على أدوات اكتشاف أخطاء للغات أخرى مثل Csslint. كما يمكنك استخدام Webhint وهو منقح أخطاء مفتوح المصدر يمكن ضبطه للويب، ويعرض أفضل الممارسات لاستخدامها بما في ذلك أساليب الوصول والأداء والتوافق مع المتصفحات باستخدام بيانات توافق متصفح MDN والأمان واختبار تطبيقات الويب ذات الصفحة الواحدة PWA وغير ذلك. كما يتوفر كأداة سطر أوامر Node.js وكامتداد VS Code. التحكم بالشيفرة البرمجية يُعرَف أيضًا باسم أنظمة التحكم بالإصدارات Version Control Systems أو VCS اختصارًا، ويُعَد التحكم بالشيفرة البرمجية ضروريًا لدعم العمل الفردي أو ضمن فريق. يتضمن نظام VCS إصدارًا محليًا من الشيفرة التي تجري تغييرات عليها، ثم ترفع التغييرات إلى إصدار رئيسي من الشيفرة داخل مستودع بعيد مُخزَّن على خادم في مكان ما. هناك عادةً طريقة للتحكم في التعديلات التي تُطبَّق على النسخة الرئيسية من الشيفرة وتنسيقها ومتى تطبَّق، وبالتالي لا يكتب فريق المطورين فوق عمل بعضهم بعضًا. يعد نظام جيت هاب Git من أشهر نظم التحكم بالشيفرة البرمجية الذي يستخدمه معظم المطورين حاليًا، إذ يمكن الوصول إليه عبر سطر الأوامر أو عبر واجهات سهلة الاستخدام. كما يمكنك رفع نسخة من المشروع إلى الخادم الخاص بك باستخدام شيفرتك في مستودع جيت، أو يمكنك استخدام موقع ويب مستضاف للتحكم بالشيفرة المصدرية مثل جيت هب GitHub أو جيت لاب GitLab أو بيت باكيت BitBucket، ولكن سنستخدم GitHub في مثالنا. مُنسِّقات الشيفرة Code Formatters ترتبط مُنسِّقات الشيفرة إلى حد ما بمنقّحات الصياغة، باستثناء أنه بدلًا من الإشارة إلى الأخطاء في شيفرتك، فإنها عادةً ما تميل إلى التأكد من تنسيق شيفرتك الصحيح وفقًا لقواعد النمط الخاصة بك، وتعمل تلقائيًا على إصلاح الأخطاء التي تعثر عليها. أحد الأمثلة الشائعة لمنسقات الشيفرة البرمجية هو Prettier، والذي سنستخدمه لاحقًا في مثالنا. الحزم Bundlers أو Packagers هي الأدوات التي تجعل شيفرتك جاهزةً لعملية الإنتاج عن طريق تقنية هز الشجرة Tree-Shaking مثلًا للتأكّد من إدراج أجزاء مكتبات الشيفرة التي تستخدمها فعليًا فقط في شيفرة الإنتاج النهائي، أو التصغير Minifying لإزالة كل مسافة فارغة في شيفرة الإنتاج، مما يصغّرها قدر الإمكان قبل رفعها إلى الخادم. تعد أداة Parcel من الأدوات الذكية التي يمكنها تطبيق المهام المذكورة السابقة، كما أنها تساعد أيضًا في حزم الملفات أو الملحقات assets مثل HTML وCSS وملفات الصور ضمن حزم ملائمة يمكنك نشرها لاحقًا، وتضيف اعتماديات تلقائيًا كلما حاولت استخدامها. كما يمكن لهذه الأداة التعامل مع بعض مهام تحويل الشيفرة نيابة عنك. يذكر أن أداة ويب باك Webpack هي أشهر أداة حزم تطبّق مهامًا مماثلة. التحويل Transformation تتيح هذه المرحلة من دورة حياة تطبيق الويب كتابة شيفرة برمجية إما في شيفرة مستقبلية مثل تحويل الشيفرة البرمجية لأحدث ميزات لغة CSS أو جافاسكربت التي يمكن ألّا تدعمها بعض المتصفحات حتى الآن، أو إنشاء شيفرة برمجية مكافئة لشيفرة برمجية مكتوبة بلغة معينة مثل لغة TypeScript، لتتوافق الشيفرة التي أنشأتها الأداة المطلوبة مع المتصفح لاستخدامها في عملية الإنتاج. يُنظَر إلى تطوير الويب على أنه مؤلَّف من ثلاث لغات هي HTML وCSS وجافاسكربت، وهناك أدوات تحويل لجميع هذه اللغات. يقدّم التحويل فائدتين رئيسيتين هما: القدرة على كتابة شيفرة برمجية باستخدام أحدث ميزات اللغة وتحويلها إلى شيفرة تعمل على جميع الأجهزة، فيمكن أن ترغب مثلًا في كتابة شيفرة بلغة جافاسكربت باستخدام ميزات لغة جديدة متطورة، ولكن لا يزال لديك شيفرة الإنتاج النهائي التي تعمل على المتصفحات القديمة التي لا تدعم هذه الميزات. تشمل الأمثلة التالية: Babel: مصرّف جافاسكربت JavaScript الذي يسمح للمطورين بكتابة شيفرة باستخدام أحدث إصدارات جافاسكربت، والتي يأخذها Babel ويحوّلها إلى إصدار جافاسكربت قديم يمكن لمزيد من المتصفحات فهمه. كما يمكن للمطورين كتابة ونشر إضافات Babel. PostCSS: تطبّق هذه الأداة الشيء نفسه الذي يطبّقه Babel، ولكن مع ميزات CSS المتطورة. إذا لم تكن هناك طريقة مكافئة لتطبيق شيء ما باستخدام ميزات CSS القديمة، فسيثبّت PostCSS تعويض نقص دعم المتصفحات Polyfill بلغة جافاسكربت لمحاكاة تأثير CSS الذي تريده. خيار كتابة الشيفرة بلغة مختلفة تمامًا وتحويلها إلى لغة متوافقة مع الويب مثل: Sass / SCSS: يتيح لك هذا الامتداد من لغة CSS استخدام المتغيرات والقواعد المتداخلة والمزج والدوال والعديد من الميزات الأخرى، إذ يُعَد بعضها متاحًا في لغة CSS الأصلية مثل المتغيرات، وبعضها ليس كذلك. TypeScript: هي مجموعة شاملة من لغة جافاسكربت التي تقدم مجموعة من الميزات الإضافية. يحوّل مصرّف TypeScript شيفرة TypeScript إلى جافاسكربت عند البناء بهدف الإنتاج. توفّر أطر العمل مثل React وEmber وVue الكثير من الوظائف مجانًا وتسمح لك باستخدامها عبر صيغة مخصَّصة مبنية على لغة جافاسكربت الصرفة Vanilla JavaScript. تعمل شيفرة جافاسكربت الخاصة بإطار العمل في الخلفية لتفسير هذه البنية المخصَّصة وتقديمها بوصفها تطبيق ويب نهائي. أدوات ما بعد التطوير Post Development تضمن أدوات ما بعد التطوير أن يصل برنامجك إلى الويب ويستمر في عمله، إذ تتضمن مرحلة ما بعد التطوير عمليات النشر وأطر عمل الاختبار وأدوات التدقيق وغير ذلك. تُعَد هذه المرحلة بأنها المرحلة التي تحتاج أقل قدر من التفاعل النشط بحيث تُشغَّل تلقائيًا بمجرد تهيئتها، وتخبرك بحدوث خطأ ما. أدوات الاختبار Testing Tools تأخذ هذه الأدوات شكل أداة تختبر تلقائيًا شيفرت البرمجية للتأكد من صحتها قبل المضي قدمًا مثل رفع تعديلات إلى مستودع جيب هب GitHub Repo. يمكن أن يشمل ذلك الكشف عن الأخطاء Linting، ويشمل إجراءات أكثر تعقيدًا مثل اختبارات الوحدة إذ تشغّل جزءًا من شيفرتك، مع التأكد من أنها تتصرف كما ينبغي. تشمل أطر عمل اختبارات الكتابة Jest وMocha وJasmine. تتضمن أنظمة التشغيل والاختبارات الآلية Travis CI وJenkins وCircle CI وغيرها. أدوات النشر Deployment Tools تسمح أنظمة النشر بنشر موقع الويب الخاص بك، وهي متاحة لكل من المواقع الثابتة والديناميكية، وتميل للعمل جنبًا إلى جنب مع أنظمة الاختبار، إذ ستنتظرك سلسلة الأدوات إلى أن ترفع التغييرات إلى المستودع البعيد، وتجري بعض الاختبارات لمعرفة ما إذا كانت التغييرات مناسبة، وإذا نجحت الاختبارات، فستنشر تطبيقك تلقائيًا على موقع إنتاج. تعد Netlify واحدة من أكثر أدوات النشر شيوعًا في الوقت الحالي، ولكن هناك أدوات أخرى مثل Vercel وGithub Pages. أدوات ما بعد التطوير الأخرى هناك عدد من أنواع الأدوات الأخرى المتاحة للاستخدام في مرحلة ما بعد التطوير، بما في ذلك Code Climate لجمع مقاييس جودة الشيفرة، وامتداد متصفح Webhint لإجراء تحليل وقت التشغيل للتوافق مع المتصفحات وعمليات التحقق الأخرى، وGithub bots لتوفير المزيد من ميزات GitHub القوية، وUpdown لتوفير مراقبة وقت تشغيل التطبيق وغير ذلك الكثير. أنواع الأدوات تُطبَّق أنواع الأدوات المختلفة في دورة حياة التطوير وفق ترتيب معين، ولكن كن مطمئنًا أنك لست مضطرًا إلى أن يكون لديك كل هذه الأدوات لإصدار موقع ويب، فلن لا تحتاج لأيٍّ منها. لكن سيؤدي تضمين بعض هذه الأدوات في عملياتك إلى تحسين تجربة التطوير، ويُحتمَل أن يؤدي إلى تحسين جودة شيفرتك الإجمالية. يستغرق استقرار أدوات المطور الجديدة بعض الوقت حسب تعقيدها. تشتهر إحدى أشهر الأدوات وهي Webpack بكونها معقدة للغاية للتعامل معها، ولكن كان هناك ضغط كبير لتبسيط الاستخدام في أحدث إصدار رئيسي، لذا قُلٍّل الإعداد المطلوب إلى الحد الأدنى. ليس هناك حل سحري يضمن النجاح باستخدام الأدوات، ولكن ستجد تدفقات عمل تناسبك أو تناسب فريقك ومشاريعك مع زيادة خبرتك، ويجب أن تكون سلسلة الأدوات شيئًا يمكنك نسيانه وأن تركّز على العمل فقط، بمجرد تسوية جميع مكامن الخلل في العملية. كيفية اختيار أداة معينة والحصول عليها تميل معظم الأدوات إلى كتابتها وإصدارها بصورة منفصلة، لذلك لا تتوفر أبدًا في المكان أو التنسيق نفسه، على الرغم من وجود مساعدة شبه مؤكدة، فيمكن أن يكون العثور على مساعدة في استخدام أداة أو حتى اختيار الأداة التي تريد استخدامها أمرًا صعبًا. المعرفة حول أفضل الأدوات لاستخدامها هي معرفة مجتمعية إلى حد ما، مما يعني أنه إن لم تكن بالفعل في مجتمع الويب، فستكون معرفة الأدوات التي تريدها بالضبط أمرًا صعبًا، وهذا هو أحد الأسباب التي دفعتنا إلى كتابة هذه السلسلة من المقالات، ونأمل أن نقدم تلك الخطوة الأولى التي يصعب إيجادها بطريقة أخرى. ستحتاج على الأرجح إلى مجموعة الأشياء التالية: نجح المدرسون أو الموجهون أو الزملاء الطلاب ذوو الخبرة أو الزملاء الذين لديهم بعض الخبرة في حل هذه المشكلات من قبل، ويمكنهم تقديم المشورة. مكان محدد مفيد للبحث، إذ تكون عمليات البحث العامة على الويب عن أدوات مطور الواجهة الأمامية عديمة الفائدة إلا إن عرفتَ اسم الأداة التي تبحث عنها. إذا استخدمتَ مدير الحزم NPM لإدارة اعتمادياتك على سبيل المثال، فيُفضَّل الانتقال إلى صفحة npm الرئيسية والبحث عن نوع الأداة التي تبحث عنها. حاول مثلًا البحث عن "التاريخ date" إن أردتَ أداة تنسيق التاريخ، أو "المُنسِّق formatter" إذا كنت تبحث عن مُنسق شيفرة عام. انتبه إلى درجات الشعبية والجودة والصيانة، وآخر تحديث للحزمة. انقر على صفحات الأداة لمعرفة عدد تنزيلات الحزمة الشهرية، واحتوائها على توثيق جيد يمكنك استخدامه للتأكد من أنها تطبّق ما تريده، وبالتالي تُعَد مكتبة date-fns أداة تنسيق تاريخ جيدة لاستخدامها. إذا أردت البحث عن إضافة Plugin لدمج وظائف الأدوات في محرّر الشيفرة، فألقِ نظرة على صفحة الإضافات / الامتدادات لمحرر الشيفرة، وراجع حزم Atom وامتدادات VSCode على سبيل المثال. ألقِ نظرة على الامتدادات المميزة في الصفحة الأولى، وحاول مرة أخرى البحث عن نوع الامتداد الذي تريده (أو اسم الأداة مثل البحث عن "eslint" في صفحة امتدادات VSCode). إذا حصلتَ على نتائج، فألقِ نظرة على معلومات عدد النجوم أو التنزيلات التي يحتويها الامتداد، إذ يُعَد ذلك مؤشرًا على جودته. المنتديات المتعلقة بالتنمية لطرح أسئلة حول الأدوات التي يجب استخدامها مثل قسم الأسئلة والأجوبة البرمجية في أكاديمية حسوب. إذا اخترت أداة لاستخدامها، فيجب أن يكون طريقة فهم الأداة هو الصفحة الرئيسية لمشروع الأداة والذي يكون غالبًا موقع ويب كامل أو يكون مستندًا تمهيديًا واحدًا في مستودع الشيفرة. يمكن أن ترغب في العثور على بعض البرامج التعليمية المخصصة لبدء استخدام أنواع معينة من الأدوات، لذا تعد المقالات البرمجية ومقالات DevOps في أكاديمية حسوب أماكن الانطلاق الرائعة للبحث. يُحتمَل أن تمر عبر العديد من الأدوات المختلفة أثناء البحث عن الأدوات المناسبة لك، وأن تجربها لمعرفة ما إذا كانت منطقية ومدعومة جيدًا وتطبّق ما تريده منها. يُعَد كل ذلك مناسبًا للتعلم، وسيصبح الطريق أسلس كلما اكتسبت مزيدًا من الخبرة. الخلاصة قدّمنا من خلال هذا المقال مقدمة بسيطة عن أدوات الويب من طرف العميل، وسنقدّم في المقال القادم دورة مكثفة عن سطر الأوامر، إذ تُستدعَى كثير من الأدوات منه، وسنلقي نظرة على ما يمكن أن يفعله سطر الأوامر ثم نحاول تثبيت الأداة الأولى ونستخدمها. هذا المقال جزء من سلسلة مقالات بعنوان تعلم تطوير الويب والتي تشرح كامل عملية تطوير الويب من واجهات أمامية وخلفية بالكامل. ترجمة -وبتصرُّف- للمقالين Understanding client-side web development tools وClient-side tooling overview. اقرأ أيضًا مدخل إلى أدوات التطوير في متصفح الويب DevTools كيف تستخدم أدوات المطوِّر في المتصفحات الحديثة الفرق بين مصمم الويب ومطور الويب وكيفية معرفة الأنسب بينهما الأدوات المستخدمة في بناء مواقع ويب
  23. بدأنا بالحديث عن شبكات التراكب ثم تطرقنا إلى شبكات الند للند ثم بروتوكول البت تورنت وسنكمل في هذا القسم الأخير في الحديث عن شبكات توزيع المحتوى. رأينا بالفعل كيف يسمح تشغيل بروتوكول HTTP عبر بروتوكول TCP لمتصفحات الويب باسترداد الصفحات من خوادم الويب، ولكن يعرف أي شخصٍ ينتظر إلى الأبد لاسترداد صفحة ويب أن النظام بعيدٌ عن الكمال. تُنشَأ شبكة الإنترنت الرئيسية backbone الآن من روابطٍ ذات 40 جيجابت في الثانية، لذلك ليس واضحًا سبب حدوث ذلك. هناك أربع نقاط اختناقٍ محتملة في النظام عندما يتعلق الأمر بتنزيل صفحات الويب هي: الميل الأول The first mile، حيث قد يحتوي الإنترنت على روابطٍ ذات سعةٍ عالية، ولكن هذا لا يساعدك على تنزيل صفحة ويب بصورةٍ أسرع عندما تكون متصلًا بخط DSL بسرعة 1.5 ميجابت في الثانية أو رابطٍ لاسلكي سيء الأداء. الميل الأخير The last mile، حيث يمكن أن يُحمَّل الرابط الذي يربط الخادم بالإنترنت بصورةٍ كبيرة عن طريق طلباتٍ كثيرة جدًا، حتى إذا كان حيز النطاق التراسلي bandwidth الإجمالي لذلك الرابط مرتفعًا جدًا. الخادم نفسه The server itself، حيث يحتوي الخادم على كميةٍ محدودةٍ من الموارد مثل وحدة المعالجة المركزية، والذاكرة، وحيز نطاق القرص الصلب وغير ذلك، ويمكن تحميله بصورةٍ كبيرة overload بسبب العديد من الطلبات المتزامنة. نقاط التناظر Peering points، حيث قد يكون لدى عددٍ من مزودي خدمة الإنترنت الذين يطبّقون جميعًا شبكة الإنترنت الرئيسية backbone أنابيبًا ذات حيز نطاقٍ تراسلي عالٍ، لكن لديهم القليل من الحافز لتوفير اتصالٍ عالي السعة لأندادهم؛ فإذا كنت متصلًا بمزود خدمة الإنترنت A وكان الخادم متصلًا بمزود خدمة الإنترنت B، فقد تُسقَط الصفحة التي تطلبها عند النقطة التي يتقابل فيها المزوّدان A وB مع بعضهما بعضًا. ليس هناك الكثير من الأشياء الممكن فعلها بشأن المشكلة الأولى، ولكن يمكن استخدام النسخ أو التضاعف لمعالجة المشكلات المتبقية، وتُسمّى الأنظمة المسؤولة عن ذلك بشبكات توزيع المحتوى Content Distribution Networks -أو اختصارًا CDNs-، حيث تدير أكاماي Akamai أشهر شبكة CDN. تتمثل فكرة شبكة CDN في توزيع مجموعةٍ من بدائل الخادم server surrogates جغرافيًا، والتي تضع الصفحات ضمن الذاكرة المخبئية في مجموعةٍ معينةٍ من خوادم الواجهة الخلفية backend servers. وبالتالي يمكن نشر الحِمل على عدة خوادم، بدلًا من أن ينتظر الملايين من المستخدمين إلى الأبد للاتصال عند ظهور قصةٍ إخباريةٍ كبيرة، حيث يُعرَف مثل هذا الموقف بالتجمّع المفاجئ flash crowd. وبدلًا من الاضطرار إلى عبور عددٍ من مزودي خدمة الإنترنت للوصول إلى الموقع www.cnn.com، فيجب أن يكون ممكنًا الوصول إلى خادمٍ بديلٍ دون الحاجة إلى عبور نقطة تناظر، إذا كانت هذه الخوادم البديلة منتشرةً عبر جميع مزودي خدمة الإنترنت الأساسيين. صيانةُ آلاف الخوادم البديلة في أنحاء شبكة الإنترنت مكلفٌ للغاية لأي موقعٍ يريد توفير وصولٍ أفضل إلى صفحات الويب الخاصة به. توفّر شبكات CDN التجارية هذه الخدمة للعديد من المواقع، وهذا يؤدي إلى تسديد التكلفة عبر العديد من العملاء. نسمّي هذه الخوادم خوادمًا بديلة، لكن يمكن عدّها ذواكرًا مخبئية caches. فإذا لم يكن لدى هذه الخوادم البديلة الصفحة التي طلبها العميل، فإنها تطلبها من خادم الواجهة الخلفية، ولكن عمليًا تنسخ خوادم الواجهة الخلفية بياناتها مسبقًا إلى الخوادم البديلة عوضًا عن انتظار الخوادم البديلة لطلبها عند الضرورة، وهذه نفس حالة توزيع الصفحات الثابتة فقط، التي لا تحوي محتوىً ديناميكيًا عبر الخوادم البديلة. يجب على العملاء الانتقال إلى خادم الواجهة الخلفية لأي محتوىً يتغير بصورةٍ متكررة، مثل نتائج الألعاب الرياضية وأسعار الأسهم، أو لأي محتوىً ينتج عن بعض العمليات الحسابية، مثل استعلام قاعدة البيانات. لا يحلّ وجودُ مجموعةٍ كبيرة من الخوادم الموزّعة جغرافيًا المشكلة تمامًا، حيث تحتاج شبكات CDN أيضًا إلى توفير مجموعةٍ من مُعيدات التوجيه redirectors التي تمرر طلبات العميل إلى الخادم الأنسب، كما هو موضحٌ في الشكل السابق. يتمثل الهدف الأساسي من معيدات التوجيه بتحديد الخادم لكل طلبٍ ينتج عنه أفضل وقت استجابةٍ response time للعميل؛ أما الهدف الثانوي فهو معالجة النظام عددًا من الطلبات في الثانية والتي يستطيع العتاد الأساسي مثل روابط الشبكة وخوادم الويب دعمها. يُعَد متوسط عدد الطلبات الممكن تلبيتها في فترةٍ زمنيةٍ معينة، والمعروفة باسم إنتاجية النظام system throughput، مشكلةً أساسيةً عندما يكون النظام تحت عبءٍ ثقيل، مثل وصول تجمّعٍ مفاجئ إلى مجموعةٍ صغيرةٍ من الصفحات أو هجماتٍ موزَّعة لحجب الخدمة Distributed Denial of Service -أو اختصارًا DDoS- على موقعٍ ما، كما حدث لمواقع CNN وYahoo والعديد من المواقع البارزة الأخرى في فبراير (شباط) عام 2000. تستخدم شبكات CDN عدة عواملٍ لتحديد كيفية توزيع طلبات العملاء، كأن يختار معيد التوجيه خادمًا بناءً على قرب شبكته لتقليل وقت الاستجابة، ولكن يُستحسَن موازنة الحمل بالتساوي عبر مجموعة من الخوادم لتحسين إنتاجية النظام الإجمالية، حيث يمكن تحسين كلٍّ من الإنتاجية ووقت الاستجابة إذا أخذت آلية التوزيع المكان ضمن حساباتها؛ أي تختار خادمًا يُحتمَل أن يكون لديه بالفعل الصفحة المطلوبة في ذاكرته المخبئية. سنشرح فيما يلي تركيبة العوامل التي يجب أن تستخدمها شبكة CDN. الآليات Mechanisms معيد التوجيه هو وظيفةٌ مجردة، على الرغم من أنه يظهر مثل شيءٍ قد يُطلَب من الموجّه فعله لأنه يمرر منطقيًا رسالة طلبٍ كما يمرر الموجّه الرزم. هناك العديد من الآليات الممكن استخدامها لتطبيق إعادة التوجيه redirection التي سنشرحها الآن؛ والتي سنفترض فيها لغرض هذه المناقشة معرفة كل معيد توجيه عنوان كل خادمٍ متاح، كما سنتخلى عن مصطلح بديل surrogate ونتحدث ببساطةٍ من حيث مجموعة من الخوادم. الآلية الأولى هي تطبيق إعادة التوجيه عن طريق تعزيز نظام DNS لإرجاع عناوين خوادم مختلفة للعملاء، حيث يمكن لخادم DNS إرجاع عنوان IP للخادم الذي يستضيف صفحات الويب الخاصة بموقع CNN والمعروف عنه أنه يحتوي على أخف حِمل، عندما يطلب أحد العملاء تحليل resolve الاسم www.cnn.com على سبيل المثال؛ بينما قد ترجع مجموعةٌ معينةٌ من الخوادم العناوين فقط بطريقة جولة روبن round robin الدورية. لاحظ أن دقة إعادة التوجيه المستندة إلى نظام DNS تكون عادةً على مستوى الموقع، مثل cnn.com عوضًا عن عنوان URL محدد، مثل: https://www.cnn.com/2020/11/12/politics Biden-wins-arizona index.html لكن يمكن للخادم إعادة كتابة عنوان URL عند إرجاع رابطٍ مضمَّن، وبالتالي توجيه العميل بفعالية إلى الخادم الأنسب لهذا الكائن المحدد. تستخدم شبكات CDN التجارية تركيبةً من إعادة كتابة عناوين URL مع إعادة التوجيه المستندة إلى نظام DNS، حيث يشير خادم DNS عالي المستوى أولًا ولأسبابٍ تتعلق بقابلية التوسع، إلى خادم DNS على المستوى الإقليمي، الذي يرد بعنوان الخادم الفعلي. تعدّل خوادم DNS فترات TTL لسجلات الموارد التي تعود إلى فترةٍ قصيرة جدًا مثل 20 ثانية، بهدف الاستجابة للتغيرات بسرعة، ويُعَد هذا ضروريًا حتى لا يضع العملاء النتائج في ذاكرةٍ مخبئية وبالتالي يفشلون في الرجوع إلى خادم DNS للحصول على أحدث ربطٍ بين عنوان URL والخادم. الآلية الثانية هي استخدام ميزة إعادة توجيه بروتوكول HTTP، حيث يرسل العميل رسالة طلبٍ إلى الخادم، الذي يستجيب بخادمٍ جديدٍ أفضل يتوجب على العميل الاتصال به للحصول على الصفحة. تتطلب إعادة التوجيه المستندة إلى الخادم وقتًا إضافيًا ذهابًا وإيابًا عبر الإنترنت، ويمكن أيضًا أن تكون الخوادم عرضةً للحِمل الزائد بسبب مهمة إعادة التوجيه redirection نفسها. بدلًا من ذلك، إذا كانت هناك عقدةٌ قريبةٌ من العميل، مثل وكيل ويب محلي local Web proxy على درايةٍ بالخوادم المتاحة، فيمكنها اعتراض رسالة الطلب وإرشاد العميل لطلب الصفحة من خادمٍ آخر مناسب. إما أن يكون معيد التوجيه في هذه الحالة ضمن نقطة اختناقٍ بحيث تمر جميع الطلبات التي تغادر الموقع من خلالها، أو سيتعيّن على العميل التعاون من خلال معالجة الوكيل صراحةً، كما هو الحال مع الوكيل الكلاسيكي وليس الوكيل الشفّاف transparent proxy؛ الذي هو خادمٌ يقع بين حاسوبك والإنترنت ويعيد توجيه طلباتك واستجاباتك دون تعديلها. قد تتساءل عن علاقة شبكات CDN بشبكات التراكب، حيث يتخذ معيد التوجيه المستند إلى الوكيل قرار توجيهٍ على مستوى التطبيق مثل عقدة التراكب، ويمرر طلبات HTTP بناءً على عنوان URL وعلى معرفته بموقع ومدى حِمل مجموعةٍ من الخوادم بدلًا من تمرير رزمة استنادًا إلى عنوانٍ ما address وبناءً على معرفته بمخطط الشبكة. لا تدعم معمارية الإنترنت الحالية إعادة التوجيه مباشرةً، حيث نعني بكلمة "مباشرةً" أن العميل يرسل طلب HTTP إلى معيد التوجيه، الذي يمرره إلى الوِجهة، بدلًا من تطبيق إعادة التوجيه بصورةٍ غير مباشرة عن طريق معيد التوجيه الذي يرجع عنوان الوجهة المناسب والعميل الذي يتصل بالخادم ذاته. السياسات Policies نستعرض الآن بعض الأمثلة عن السياسات التي قد يستخدمها مُعيدو التوجيه لتمرير الطلبات، حيث اقترحنا بالفعل سياسةً واحدةً بسيطةً وهي جولة روبن round-robin، وسيكون المثال الآخر هو اختيار أحد الخوادم المتاحة عشوائيًا. يوزع كلا الأسلوبين الحِمل بالتساوي عبر شبكة توصيل المحتوى، لكنهما لا يقلّلان من وقت الاستجابة المتوقَّع للعميل. من الواضح عدم أخذ هذين الأسلوبين قُرب الشبكة في حساباتهما، وتجاهلهما أيضًا للمنطقة المحلية؛ أي يُعاد توجيه الطلبات الخاصة بعنوان URL نفسه إلى خوادم مختلفة، مما يقلل من احتمالية تخديم الصفحة من الذاكرة المخبئية الموجودة ضمن الخادم المحدد. هذا يفرض على الخادم استرداد الصفحة من قرصه الصلب، أو ربما من خادم الواجهة الخلفية. كيف يمكن لمجموعةٍ موزعةٍ من معيدات التوجيه أن تتسبب في انتقال الطلبات لنفس الصفحة إلى نفس الخادم أو إلى مجموعةٍ صغيرةٍ من الخوادم دون تنسيقٍ عالمي؟ الإجابة بسيطةٌ وهي: تستخدم جميع مُعيدات التوجيه شكلًا من أشكال تطبيق التعمية hashing لربط عناوين URL بمجالٍ صغير من القيم. الفائدة الأساسية من هذا النهج هي أنه لا حاجة لوجود اتصالٍ بين معيدي التوجيه لتحقيق عمليةٍ منسقة؛ فإن عملية التعمية تنتج نفس الخرج بغض النظر عن معيد التوجيه الذي يتلقى عنوان URL. إذًا ما الذي يجعل مخطط التعمية جيدًا؟ مخطط تعمية النموذج الكلاسيكي، الذي يجري تعميةً على كل وحدة URL مع عددٍ من الخوادم، غير مناسبٍ لهذه البيئة، لأن حساب النموذج في حالة تغيير عدد الخوادم سيؤدي إلى تناقص الصفحات التي تحتفظ بتخصيصات الخادوم نفسها. لا نتوقع حدوث تغييراتٍ متكررةٍ في مجموعة الخوادم، إلا أن حقيقة إضافة خوادمٍ جديدة إلى المجموعة ستؤدي إلى إعادة تخصيص ضخمة أمرٌ غير مرغوبِ فيه. البديل هو استخدام نفس خوارزمية التعمية المستقرة التي ناقشناها سابقًا، حيث يجري كل معيد توجيه أولًا تعميةً على كل خادمٍ ضمن الدائرة، ثم يجري أيضًا تعميةً إلى قيمةٍ على الدائرة لكل عنوان URL واصل، ويُعيَّن عنوان URL للخادم الأقرب في الدائرة مع قيمة التعمية الخاصة به. فإذا فشلت العقدة في هذا المخطط، فسينتقل حِملُها إلى جيرانها على الدائرة، وبالتالي فإن إضافة أو إزالة خادمٍ يؤدي فقط إلى تغييراتٍ محلية في تخصيصات الطلب. يعرف كل معيد توجيه كيفية ربط مجموعة الخوادم مع الدائرة، لذلك يمكن لكل معيد توجيه اختيار الخادم الأقرب بصورةٍ مستقلة، على عكس حالة ند لند؛ حيث توجَّه الرسالة من عقدةٍ إلى أخرى من أجل العثور على الخادم الذي يكون معرّفه هو الأقرب إلى الكائنات. يمكن توسيع هذه الاستراتيجية بسهولة لأخذ حِمل الخادم ضمن حساباتنا، حيث نفترض معرفة معيد التوجيه الحِمل الحالي لكلٍ من الخوادم المتاحة. قد لا تكون هذه المعلومات محدَّثة تمامًا، ولكن يمكننا تخيل معيد التوجيه يحسب ببساطة عدد المرات التي مرر فيها طلبًا إلى كل خادمٍ في الثواني القليلة الماضية ويستخدم هذا العدد مثل تقديرٍ لحِمل هذا الخادم الحالي. يجري معيد التوجيه تعميةً على عنوان URL المُستقبَل مع كلٍ من الخوادم المتاحة ويرتّب القيم الناتجة، حيث تحدّد هذه القائمة المرتَّبة بفعالية الترتيب الذي سينظر فيه معيد التوجيه في الخوادم المتاحة، ثم يتحرّك معيد التوجيه في هذه القائمة حتى يعثر على خادمٍ يكون الحِمل عليه أقل من حدٍ معين. تتمثل فائدة هذا الأسلوب في أن ترتيب الخادم يختلف باختلاف عنوان URL، لذلك إذا فشل خادمٌ ما، فسيُوزَّع حِمله بالتساوي بين الأجهزة الأخرى. يُعَد هذا النهج أساس بروتوكول توجيه ذاكرة المصفوفة المخبئية Cache Array Routing Protocol -أو اختصارًا CARP- الموضّح في الشيفرة التالية: SelectServer(URL, S) for each server s in server set S weight[s] = hash(URL, address[s]) sort weight for each server s in decreasing order of weight if Load(s) < threshold then return s return server with highest weight يتغير هذا المخطط مع زيادة الحِمل من استخدام الخادم الأول فقط في القائمة المُرتَّبة إلى نشر الطلبات على عدة خوادم. ستعالج خوادمٌ أقل انشغالًا أيضًا بعضَ الصفحات التي تتعامل معها الخوادم المشغولة. بما أن هذه العملية تستند إلى حِمل الخادم الكلي عوضًا عن الطلب على كل صفحةٍ مفردة، فقد تجد الخوادمُ التي تستضيف بعض الصفحات المطلوبة المزيدَ من الخوادم التي تتشارك معها حِملها، أكثر من الخوادم التي تستضيف بصورةٍ جماعية صفحاتٍ غير مطلوبة. ستُنسَخ بعض الصفحات غير المطلوبة ضمن النظام لمجرد أنها مُستضافة على خوادمٍ مشغولة، وإذا أصبحت بعض الصفحات مطلوبةً للغاية، فيمكن أن تكون جميع الخوادم في النظام مسؤولةً عن خدمة هذه الصفحات. أخيرًا، يمكن إدخال تقارب الشبكة network proximity في المعادلة بطريقتين مختلفتين على الأقل. الطريقة الأولى هي إخفاء التمييز بين حِمل الخادم وتقارب الشبكة من خلال مراقبة المدة التي يستغرقها الخادم للاستجابة للطلبات واستخدام القياس الناتج مثل معاملٍ لحِمل الخادم في الخوارزمية السابقة. تميل هذه الإستراتيجية إلى تفضيل الخوادم القريبة / غير المحمَّلة كثيرًا على الخوادم البعيدة / المحمَّلة بصورةٍ كبيرة. تتمثل الطريقة الثانية في إدخال عامل التقارب في القرار في مرحلةٍ مبكرة عن طريق قَصر مجموعة الخوادم المرشَّحة التي رأيناها ضمن الخوارزميات المذكورة أعلاه على تلك الخوادم القريبة فقط. المشكلة الأصعب هي اختيار الخوادم التي يُحتمَل أن تكون قريبةً بصورةٍ مناسبة، حيث تتمثل إحدى الطرق في اختيار تلك الخوادم المتوفّرة فقط على نفس مزود خدمة الإنترنت مثل عملاء. تتمثل الطريقة الأعقد في النظر إلى خارطة الأنظمة المستقلة التي ينتجها بروتوكول BGP واختيار تلك الخوادم فقط التي تبعد عددًا معينًا من القفزات عن العميل على أنها خوادم مرشَّحة. البحث عن التوازن الصحيح بين تقارب الشبكة ومحلية ذاكرة الخادم المخبئية هو موضوع بحثٍ مستمر. منظور الفصل التاسع: السحابة هي شبكة الإنترنت الجديدة كما رأينا سابقًا، كان هناك انتقالٌ لتطبيقات الإنترنت التقليدية، مثل البريد الإلكتروني وخوادم الويب، من الأجهزة التي تعمل محليًا إلى الآلات الافتراضية التي تعمل ضمن سحاباتٍ سلعية. يتوافق هذا مع تحوُّلٍ في المصطلحات من خدمات الويب إلى الخدمات السحابية وتحولٍ أيضًا في العديد من التقنيات الأساسية المستخدمة من الآلات الافتراضية إلى الخدمات الصغيرة السحابية الأصلية. لكن تأثير السحابة على كيفية تطبيق تطبيقات الشبكة اليوم أكبر مما يوحي به هذا الانتقال، فهو مزيجٌ من السحابات السلعية وشبكات التراكب المشابهة لتلك الموضحة أعلاه، التي قد يكون لها التأثير الأكبر في النهاية. يحتاج التطبيق المستند إلى التراكب وجود بصمةٍ واسعةٍ ليكون فعالًا، أي وجود عدة نقاط تواجد حول العالم. تُنشَر موجهات IP على نطاقٍ واسع، لذلك إذا كان لديك إذنٌ لاستخدام مجموعةٍ منها مثل عقدٍ أساسية في شبكة التراكب الخاصة بك، فأنت جاهزٌ للعمل. لكن هذا لن يحدث، حيث لا يوجد مشغلو شبكات أو مسؤولو مؤسسةٍ على استعدادٍ للسماح للأشخاص العشوائيين بتحميل برمجيات التراكب على الموجهات الخاصة بهم. قد يكون خيارك التالي هو حشد موارد مواقع الاستضافة لبرمجيات التراكب الخاصة بك، حيث سينجح ذلك إذا اشتركت مع أشخاصٍ آخرين على هدفٍ واحد، مثل تنزيل الموسيقى المجانية. لكن انتشار تطبيق التراكب الجديد أمرٌ صعب، وحتى إذا حدث ذلك، فقد يكون التأكدُ من وجود سعةٍ كافيةٍ في أي وقتٍ لحمل كل حركة المرور التي يولدها تطبيقك مشكلةً، حيث ينجح ذلك أحيانًا مع الخدمات المجانية، ولكنه لن ينجح مع تطبيقٍ تأمل في تحقيق الربح منه. توجد طريقةٌ للدفع لشخصٍ ما مقابل حقوق تحميل وتشغيل برنامجك على خوادمٍ منتشرةٍ في جميع أنحاء العالم؛ وهذا هو بالضبط ما توفره السحابات السلعية مثل Amazon AWS و Microsoft Azure و Google Cloud Platform، حيث توفّر السحابة عددًا غير محدودٍ من الخوادم ظاهريًا، ولكنها لا تقل أهميةً، إن لم تكن الأهم، عن مكان وجود هذه الخوادم، حيث تُوزَّع على نطاقٍ واسع عبر أكثر من 150 موقعًا متصلًا بصورةٍ جيدة. لنفترض مثلًا أنك تريد بث مجموعةٍ من قنوات الفيديو أو الصوت الحية لملايين المستخدمين، أو أنك تريد دعم الآلاف من جلسات مؤتمرات الفيديو التي يربط كل منها عشرات المشاركين المُوزعين على نطاقٍ واسع. ستنشئ في كلتا الحالتين شجرة تراكب متعدد البث (شجرةٌ لكل قناة فيديو في المثال الأول، وشجرةٌ لكل جلسة مؤتمر في المثال الثاني)، مع وجود عقد التراكب في الشجرة في مجموعةٍ من تلك المواقع السحابية البالغ عددها 150 موقعًا. ثم تسمح للمستخدمين النهائيين، من متصفحات الويب ذات الأغراض العامة أو تطبيقات الهواتف الذكية المصممة لهذا الغرض، بالاتصال بشجرةٍ أو مجموعة شجرات البث المتعدد التي يختارونها. إذا كنت بحاجةٍ إلى تخزين قدرٍ من محتوى الفيديو أو الصوت لتشغيله في وقتٍ لاحق، لدعم تحويل الوقت time shifting؛ فيمكنك أيضًا شراء سعة تخزينٍ في بعض أو كل هذه المواقع السحابية، وبناء شبكة توزيع المحتوى الخاصة بك بفعالية. عُدَّ الإنترنت في الأصل مثل خدمة اتصالٍ بحتة، مع السماح لتطبيقات الحوسبة والتخزين بالتطور على أطراف الشبكة، لكن برمجيات التطبيقات اليوم هي لجميع الأغراض العملية المضمَّنة داخل الشبكة أو الموزَّعة عبرها، حيث تُعَد معرفة مكان نهاية الإنترنت وبداية السحابة أمرًا صعبًا، وسيستمر هذا المزج في التعمق حتى اقتراب السحابة من طرف الشبكة، مثل الاقتراب من آلاف المواقع التي ترتبط بها شبكات الوصول، وستقود تكاليفُ التوسّع الأجهزةَ العتادية المستخدَمة في بناء مواقع الإنترنت أو السحابة إلى أن تصبح عمومية. ترجمة -وبتصرّف- للقسم Overlay Networks من فصل Applications من كتاب Computer Networks: A Systems Approach. اقرأ أيضًا المتطلبات اللازمة لبناء شبكة حاسوبية البرمجيات المستخدمة في بناء الشبكات الحاسوبية معمارية الشبكة الحاسوبية وشبكة الإنترنت (Network Architecture) الشبكات الحاسوبية متعددة الوصول (Multi-Access Networks)
  24. سنتعلّم من خلال هذا المقال كيفية إنشاء مذياع في برنامج الإليستريتور Adobe Illustrator باستخدام الأشكال الأساسية مثل المستطيلات والمستطيلات ذات الزوايا المستديرة والدوائر، بالإضافة إلى تقنيات رسم الأشكال المتجهة vector، بعد ذلك سنضيف الظلال والإضاءات إلى المذياع لجعله أكثر لمعانًا وثلاثي الأبعاد. إنشاء مستند جديد شغّل برنامج الإليستريتور Illustrator ثم اضغط على الاختصار Ctrl + N لإنشاء مستند جديد. حدّد خيار البكسلات Pixels من قائمة الوحدات Units، وأدخِل القيمة 834 في خانة العرض width والقيمة 700 في خانة الارتفاع height، ثم انقر على خيارات متقدمة Advanced. حدّد نمط الألوان RGB والخيار Screen (72ppi)‎، وتأكّد من إلغاء تحديد مربع اختيار محاذاة الكائنات الجديدة إلى شبكة البكسلات Align New Objects to Pixel Grid قبل النقر على موافق OK. إنشاء جسم المذياع استخدم أداة المستطيل Rectangle Tool (باستخدام الاختصار M) لإنشاء مستطيل أبعاده 518‎×333 بكسل، ثم أنشئ نسخةً منه باستخدام الاختصار Ctrl + C ثم Ctrl + F، واستبدل لون حدّ stroke هذه النسخة الحالي باللون الأحمر. حدّد المستطيل الأحمر وانتقل إلى قائمة تأثير Effect ثم Stylize ثم زوايا مستديرة Round Corners، ثم أدخِل نصف قطر Radius مقداره 105 بكسلات وانقر على موافق، ثم انتقل إلى قائمة كائن Object ثم توسيع المظهر Expand Appearance. استخدم أداة التحديد المباشر Direct Selection Tool باستخدام الاختصار A، لتحديد نقطة ارتكاز الكائن الأحمر المميزة باللون الأزرق ثم أزِلها. حدّد بعد ذلك نقطة الارتكاز العلوية اليمنى وحرّكها بمقدار 7 بكسلات إلى اليسار، ثم حدّد نقطة الارتكاز السفلية اليمنى وحرّكها بمقدار 50 بكسلًا إلى اليسار. حدّد نقطة الارتكاز المميزة باللون الأسود وانقر على أيقونة "تحويل نقاط الارتكاز المحدَّدة إلى سلِسة Convert selected anchor points to smooth" من شريط خصائص Properties، ثم انقر على مقبض هذه النقطة العلوي، واضغط على الفأرة واسحبه للأعلى. حدّد بعد ذلك نقطتي الارتكاز المميزتين باللون الأزرق وأزِلهما، ثم حدّد نقطتي الارتكاز المميزتين باللون الأرجواني وحرّكهما بمقدار 100 بكسل إلى اليمين. حدّد الكائن الأحمر وانتقل إلى قائمة كائن Object ثم تحويل Transform ثم انعكاس Reflect، واضبط المحور Axis على الخيار رأسي Vertical، ثم انقر على نسخ Copy. حدّد هذه النسخة واستمر في الضغط على مفتاح Shift، ثم انقر على المستطيل الأسود الذي أنشأناه في الخطوة رقم 2، ثم حرّر مفتاح Shift وانقر على المستطيل الأسود مرةً أخرى (لتثبيت موضعه). افتح لوحة المحاذاة Align (من قائمة Window ثم Align) وانقر على زر المحاذاة اليسرى أفقيًا Horizontal Align Left. أعِد تحديد الكائنين الملوّنين باللون الأحمر، ثم افتح لوحة مستكشف المسار Pathfinder (من قائمة Window ثم Pathfinder) وانقر على زر دمج Unite، ثم حدّد المستطيل الأسود وأزِله. حدّد الكائن الأحمر وانتقل إلى قائمة كائن Object ثم Transform ثم Scale، وحدّد الخيار غير موحّد Non-Uniform، وأدخِل القيمة 95 في خانة أفقي Horizontal، ثم انقر على نسخ Copy. استبدل لون حدّ هذه النسخة الحالي باللون الأسود ثم حرّكه بمقدار 57 بكسلًا للأعلى. استخدم أداة التحديد المباشر A لتحديد نقطة ارتكاز الكائن الأسود العلوية اليسرى وحرّكها بمقدار 5 بكسلات إلى اليمين، ثم حدّد نقطة الارتكاز العلوية اليمنى وحرّكها بمقدار 5 بكسلات إلى اليسار. حدّد الكائن الأحمر، وأنشئ نسخةً منه بالضغط على Ctrl + C ثم Ctrl + F، حدّد هذه النسخة واستمر في الضغط على مفتاح Shift ثم انقر على الكائن الأسود، ثم افتح لوحة مستكشف المسار Pathfinder (من قائمة Window ثم Pathfinder) وانقر على زر Minus Back. حدد الكائن الأحمر وأنشئ نسخةً منه بالضغط على Ctrl + C ثم Ctrl + F، ثم استبدل لون حدّ هذه النسخة الحالي باللون الأزرق واجلبه إلى الأمام باستخدام الاختصار Ctrl + Shift + Right Square Bracket. اختر أداة إضافة نقطة ارتكاز Add Anchor Point Tool (+)، وانقر على النقطتين المميزتين باللون الأحمر في الكائن الأزرق، مما يؤدي إلى إضافة نقطتي ارتكاز جديدتين إليه. استخدم أداة التحديد المباشر (A) مع مفتاح Shift لتحديد نقاط الارتكاز العلوية الأربع من الكائن الأزرق وحرّكها بمقدار 112 بكسلًا للأسفل. اختر أداة تحويل نقاط الارتكاز Convert Anchor Point Tool باستخدام الاختصار Shift + C، ثم انقر على المقبض العلوي لنقطة الارتكاز العلوية اليسرى من الكائن الأزرق واضغط على الفأرة، ثم اسحب هذا المقبض إلى الجهة السفلية اليمنى. انقر بعد ذلك على المقبض العلوي لنقطة الارتكاز العلوية اليمنى، واضغط على الفأرة، ثم اسحب هذا المقبض إلى الجهة السفلية اليسرى. حدّد الكائن الأحمر الذي أنشأناه في الخطوة رقم 4 ثم أزِل حدّه واملأه بالتدرج اللوني الخطي linear gradient الموضّح أدناه. أنشئ نسخةً من الشكل الناتج بالضغط على Ctrl + C ثم Ctrl + F، ثم استبدل لون تعبئة هذه النسخة الحالي باللون الرمادي الفاتح (‎# e7dfd0). حدّد الشكل الناتج وانتقل إلى قائمة تأثير Effect ثم Stylize ثم Feather، وأدخِل نصف قطر مقداره 20 بكسلًا وانقر على موافق. حدّد الكائن الأسود الذي أنشأناه في الخطوة رقم 5، ثم أزِل حدّه واملأه بالتدرج اللوني الخطي الموضّح أدناه. حدّد الكائن الأزرق الذي أنشأناه في الخطوة رقم 6 وأنشئ نسخةً منه باستخدام الاختصار Ctrl + C ثم Ctrl + F، ثم اضغط على الاختصار Ctrl +3 لإخفاء هذه النسخة. أخيرًا، أعِد تحديد الكائن الأزرق الأصلي، وأزِل حدّه واملأه بالتدرج اللوني الخطي الموضّح أدناه. اضغط على الاختصار Ctrl + Alt +3 لإظهار الكائن الذي أخفيناه في الخطوة رقم 7. حدّد هذا الكائن واستبدل لون حدّه الحالي باللون الأزرق الفاتح (‎# 0074bd) وغيّر ثُخن هذا الحد إلى 1.5 بكسل. استخدم أداة التحديد المباشر (A) ومفتاح Shift لتحديد نقطتي الارتكاز المميزتين باللون الأحمر، ثم انقر على أيقونة "قص المسارات عند نقاط الارتكاز المحدَّدة Cut paths at selected anchor points" من شريط خصائص Properties، مما يجعل الكائن مؤلفًا من مسارين. أعِد تحديد المسار العلوي وأنشئ نسخة منه بالضغط على Ctrl + C ثم Ctrl + F، بعد ذلك حرّك هذه النسخة بمقدار 2 بكسل للأسفل. حدّد هذا المسار، واستبدل لون حدّه الحالي باللون السماوي (‎# 41b8bd). حدد المسار السفلي الذي أنشأناه في الخطوة رقم 8، وغيّر ثُخن حدّه إلى 3 بكسلات واستبدل لون الحدّ الحالي باللون الأزرق الزاهي (‎# 239ef1)، ثم طبّق الخيار Width Profile 1 على المسار الناتج. حدّد المسار الناتج، واستخدم أداة إضافة نقطة ارتكاز Add Anchor Point (+) لإضافة نقطتي ارتكاز عند النقاط المميّزة باللون الأحمر. أعِد تحديد نقطتي الارتكاز اللتين أنشأناهما للتو وانقر على أيقونة "قص المسارات عند نقاط الارتكاز المحدَّدة Cut paths at selected anchor points" من شريط خصائص Properties، مما يجعل المسار مؤلفًا من ثلاثة مسارات، ثم حدّد المسارين الأقصر وأزِلهما. أعِد تحديد المسار المتبقي وحرّكه بمقدار 1 بكسل للأسفل، ثم طبّق عليه تأثير Gaussian Blur بمقدار 3 بكسلات. حدّد الآن الشكل الأزرق الذي أنشأناه في الخطوة رقم 7 وأنشئ نسخةً منه بالضغط على Ctrl + C ثم Ctrl + F، ثم أحضِر هذه النسخة إلى الأمام Ctrl + Shift + Right Square Bracket. أبقِ هذه النسخة محدَّدةً واستمر في الضغط على مفتاح Shift، ثم انقر على المسار الذي طبّقنا عليه تأثير الضبابية blur في الخطوة الحالية. انقر بزر الفأرة الأيمن على لوحة الرسم، ثم حدّد خيار إنشاء قناع قطع Make Clipping Mask من القائمة. حدّد الشكل الرمادي الفاتح الذي طبّقنا عليه تأثير Feather في الخطوة رقم 7 وأنشئ نسخةً منه، ثم أحضِر هذه النسخة إلى الأمام (Ctrl + Shift + Right Square Bracket). حدّد هذه النسخة، وافتح لوحة Appearance من قائمة Window ثم Appearance، وأزِل تأثير feather. أزِل لون تعبئة الشكل الناتج، ثم أضِف له حدًا باللون الأحمر مقداره 1 بكسل. حدّد الكائن الأحمر وانتقل إلى قائمة كائن Object ثم Transform ثم Scale، وحدّد الخيار Non-Uniform، ثم أدخِل القيمة 98 في خانة أفقي Horizontal والقيمة 94 في خانة رأسي Vertical، ثم انقر على موافق. حدّد الكائن الناتج وافتح نافذة Scale مرةً أخرى، ثم حدّد الخيار موحّد Uniform، وأدخِل القيمة 95 في قسم Scale وانقر على نسخ Copy، ثم حرّك هذه النسخة بمقدار 1 بكسل للأعلى و5 بكسلات إلى اليسار. أعِد تحديد الكائنين الملونين باللون الأحمر اللذين أنشأناهما في الخطوة الحالية، ثم افتح لوحة مستكشف المسار Pathfinder (من قائمة Window ثم Pathfinder) وانقر على زر Minus Front. حدّد الكائن الناتج، ثم أزِل حدّه واملأه باللون الرمادي الداكن (‎# 7a7b80). طبّق تأثير Gaussian Blur بمقدار 3 بكسلات على الشكل الناتج، ثم اضبط نمط المزج Blending Mode على الخيار Multiply وقلّل التعتيم Opacity إلى 40%. حدّد الشكل الرمادي الفاتح الذي طبّقنا عليه تأثير Feather في الخطوة رقم 7 مرةً أخرى وأنشئ نسخةً منه، ثم أحضِر هذه النسخة إلى الأمام عن طريق الاختصار Ctrl + Shift + Right Square Bracket. حدّد هذه النسخة، واستمر في الضغط على مفتاح Shift وانقر على الشكل الذي طبّقنا عليه تأثير blur في الخطوة الحالية، ثم انقر بزر الفأرة الأيمن على لوحة الرسم، وحدّد الخيار إنشاء قناع قطع Make Clipping Mask من القائمة. استخدم أداة القلم Pen Tool (باستخدام الاختصار P) لإنشاء شكل أبيض (‎# ffffff) كما هو موضّح أدناه، ثم طبّق تأثير Gaussian Blur بمقدار 6 بكسلات، مع ضبط نمط المزج على الخيار Lighten. حدّد الشكل الناتج واضغط على الاختصار Ctrl + X لقصه، ثم اختر أداة التحديد Selection Tool (باستخدام الاختصار V)، وانقر نقرًا مزدوجًا على مجموعة القطع التي أنشأناها في الخطوة رقم 10 واضغط على الاختصار Ctrl + F للصق الشكل الذي قصصناه في الخطوة الحالية، ثم انقر نقرًا مزدوجًا في أيّ مكان خارج مجموعة القطع. حدّد الشكل العلوي الذي أنشأناه في الخطوة رقم 7 وأنشئ نسخةً منه بالضغط على Ctrl + C ثم Ctrl + F، ثم أحضِر هذه النسخة إلى الأمام (Ctrl + Shift + Right Square Bracket). أبقِ هذه النسخة محدَّدةً، ثم أزِل لون التعبئة، وأضِف حدًا بمقدار 2 بكسل باللون الأسود (‎# 160000)، ثم طبّق الخيار Width Profile 1 على الكائن الناتج. استخدم أداة التحديد المباشر (A) ومفتاح Shift لتحديد نقطتي ارتكاز الكائن الناتج المميزتين باللون الأحمر، ثم انقر على أيقونة "قص المسارات عند نقاط الارتكاز المحدَّدة Cut paths at selected anchor points" من شريط خصائص Properties، مما يجعل هذا الكائن مؤلفًا من مسارين، ثم حدد المسار العلوي وأزِله. حدّد المسار الآخر وأنشئ نسخةً منه، ثم استبدل لون الحدّ الحالي باللون الرمادي الداكن (‎# 8e8871). أبقِ هذا المسار محدَّدًا، واستخدم أداة إضافة نقاط ارتكاز Add Anchor Point Tool (+) لإضافة نقطتي ارتكاز عند النقاط المميزة باللون الأزرق. أعِد تحديد هاتين النقطتين وانقر على أيقونة "قص المسارات عند نقاط الارتكاز المحدَّدة Cut paths at selected anchor points" من شريط خصائص Properties، مما يجعل هذا المسار مؤلفًا من ثلاثة مسارات. حدّد المسارين الأقصر وأزِلهما، ثم أعِد تحديد المسار المتبقي وحرّكه بمقدار 2 بكسل للأعلى. استخدم أداة القلم (P) لإنشاء شكلين كما هو موضّح أدناه. حدّد هذين الشكلين وطبّق عليهما تأثير Gaussian Blur بمقدار 5 بكسلات، ثم انتقل إلى قائمة كائن Object ثم Transform ثم انعكاس Reflect. اضبط المحور Axis على الخيار رأسي Vertical ثم انقر على نسخ Copy. اسحب هذه النُسخ إلى اليمين، ولا تنسَ الاستمرار في الضغط على مفتاح Shift من لوحة المفاتيح للسحب السويّ. حدد الآن الشكل العلوي الذي أنشأناه في الخطوة رقم 7 وأنشئ نسخةً منه، ثم أحضِر هذه النسخة إلى الأمام (Ctrl + Shift + Right Square Bracket). حدّد هذه النسخة ثم استمر في الضغط على مفتاح Shift وانقر على الأشكال الأربعة التي طبّقنا عليها تأثير الضبابية blur في الخطوة الحالية، ثم انقر بزر الفأرة الأيمن على لوحة الرسم وحدّد خيار إنشاء قناع قطع Make Clipping Mask من القائمة. يجب أن يبدو مذياعك الآن كما في الشكل التالي: إنشاء مكبر الصوت استخدم أداة الدائرة Ellipse Tool (باستخدام الاختصار L) لإنشاء دائرة أبعادها 192‎×192 بكسل، ثم ضع هذا الكائن في الموضع الذي تراه في الشكل الأول أدناه. حدّد هذه الدائرة وأنشئ نسخةً منها بالضغط على Ctrl + C ثم Ctrl + F، ثم اضغط على الاختصار Ctrl +3 لإخفاء هذه النسخة. أعِد تحديد الدائرة الحمراء الأصلية وانتقل إلى قائمة كائن Object ثم Transform ثم Scale، وحدّد الخيار Uniform، ثم أدخِل القيمة 87 في نافذة Scale وانقر على نسخ Copy. حرّك هذه النسخة بمقدار 4 بكسلات إلى اليمين، ثم استبدل لون الحدّ الحالي باللون الأسود. استخدم أداة القلم (P) لإنشاء مسارين باللون الأزرق كما هو موضّح في الشكل الأول أدناه. حدّد هذين المسارين بعد رسمهما، ثم انقر بزر الفأرة الأيمن على لوحة الرسم وحدد خيار ضم Join من القائمة، ثم انقر بزر الفأرة الأيمن على لوحة الرسم وحدّد خيار ضم Join مرةً أخرى. حدّد الآن الشكل الأزرق الذي أنشأناه في الخطوة رقم 7 وأنشئ نسخةً منه بالضغط على Ctrl + C ثم Ctrl + F. حدّد هذه النسخة واستمر في الضغط على مفتاح Shift، ثم انقر على الكائن الأزرق الذي أنشأناه في الخطوة الحالية، ثم افتح لوحة مستكشف المسار Pathfinder من قائمة Window ثم Pathfinder، وانقر على زر التقاطع Intersect. حدّد الكائن الناتج واستمر في الضغط على مفتاح Shift، ثم انقر على الدائرة الحمراء التي أنشأناها في الخطوة رقم 15، ثم انقر على زر الدمج Unite من لوحة مستكشف المسار Pathfinder. استخدم أداة القلم (P) لإنشاء مسارين باللون الأحمر كما هو موضّح في الشكل الأول أدناه. حدّد هذين المسارين بعد رسمهما، ثم انقر بزر الفأرة الأيمن على لوحة الرسم وحدد خيار الضم Join من القائمة، ثم انقر بزر الفأرة الأيمن على لوحة الرسم وحدّد خيار الضم Join مرةً أخرى. حدّد الآن الشكل الأزرق الذي أنشأناه في الخطوة رقم 7 وأنشئ نسخةً منه بالضغط على Ctrl + C ثم Ctrl + F. حدّد هذه النسخة واستمر في الضغط على مفتاح Shift، ثم انقر على الكائن الأحمر الذي أنشأناه في الخطوة الحالية، ثم افتح لوحة مستكشف المسار Pathfinder (من قائمة Window ثم Pathfinder) وانقر على زر التقاطع Intersect. حدّد الكائن الناتج واستمر في الضغط على مفتاح Shift، ثم انقر على الدائرة السوداء التي أنشأناها في الخطوة رقم 15، ثم انقر على زر الدمج Unite من لوحة مستكشف المسار Pathfinder. حدّد الكائنين اللذين أنشأناهما من بداية الخطوة رقم 16 حتى الآن، ثم أزِل حدّهما واملأهما بالتدرجات اللونية الخطية الموضّحة أدناه. أعِد تحديد الشكل الأصغر وانتقل إلى قائمة تأثير Effect ثم Stylize ثم ظل ساقط Drop Shadow، واتبع البيانات الموضّحة أدناه وانقر على موافق. حدّد الشكل الأكبر وأنشئ نسخةً منه بالضغط على Ctrl + C ثم Ctrl + F، ثم أحضِر هذه النسخة إلى الأمام باعتماد الاختثار Ctrl + Shift + Right Square Bracket. حدّد هذه النسخة، واستمر في الضغط على مفتاح Shift ثم انقر على الشكل الذي طبّقنا عليه تأثير الظل في الخطوة الحالية. انقر بزر الفأرة الأيمن على لوحة الرسم، ثم حدّد خيار إنشاء قناع قطع Make Clipping Mask من القائمة. اضغط على الاختصار Ctrl + Alt +3 لإظهار الدائرة الحمراء التي أخفيناها في الخطوة رقم 15، ثم أحضِرها إلى الأمام (Ctrl + Shift + Right Square Bracket). حدّد هذه الدائرة وانتقل إلى قائمة كائن Object ثم Transform ثم Scale، وحدّد الخيار Uniform، ثم أدخِل القيمة 73 في نافذة Scale وانقر على موافق. أبقِ الدائرة الناتجة محدّدةً وافتح نافذة Scale مرةً أخرى، وحدّد الخيار Uniform، ثم أدخِل القيمة 92 في قسم Scale وانقر على نسخ Copy. حدّد الآن الشكل الدائري الأكبر، ثم أزِل حدّه واملأه بالتدرج اللوني الخطي الموضّح أدناه، ثم انتقل إلى قائمة تأثير Effect ثم Stylize ثم إضاءة خارجية Outer Glow، واتبع البيانات الموضّحة أدناه وانقر على موافق. أخيرًا، حدّد الدائرة الحمراء المتبقية، ثم أزِل حدّها واملأها باللون الأحمر الداكن (‎# 1d0500). اختر أداة المضلع Polygon Tool من شريط الأدوات وانقر على لوحة الرسم، ثم أدخِل القيمة 5 بكسلات في خانة نصف القطر Radius والقيمة 6 في خانة الأضلاع Sides، ثم انقر على موافق. اختر أداة التحديد (V) واستمر في الضغط على مفتاح Alt، وانقر على الشكل السداسي الذي أنشأناه للتو، واضغط على الفأرة واسحبه للأسفل إلى الموضع الموضّح أدناه، ولا تنسَ الاستمرار في الضغط على مفتاح Shift من لوحة المفاتيح للسحب السويّ، مما يؤدي إلى إنشاء نسخة من الشكل السداسي. استخدم أداة التحديد (V) لتحديد هذين الشكلين السداسيين، ثم استمر في الضغط على مفتاح Alt وانقر على الشكلين السداسيين المحددّين واسحبهما للأسفل أثناء الضغط على مفتاح Shift. اضغط بعد ذلك على الاختصار Ctrl + D سبع مرات للحصول على النتائج الموضّحة أدناه. أخيرًا، حدّد وجمّع (بالضغط على الاختصار Ctrl + G) جميع الأشكال السداسية التي أنشأناها في الخطوة الحالية. اختر أداة التحديد (V) واستمر في الضغط على مفتاح Alt، ثم انقر على المجموعة التي أنشأناها في الخطوة رقم 20، واضغط على الفأرة مع سحبها إلى الجهة اليمنى السفلية. استخدم أداة التحديد (V) لتحديد المجموعتين اللتين أنشأناهما أخيرًا، ثم استمر في الضغط على مفتاح Alt، وانقر على المجموعتين المحدَّدتين واسحبهما إلى اليمين إلى الموضع الموضّح أدناه أثناء الضغط على مفتاح Shift. اضغط بعد ذلك على الاختصار Ctrl + D ثماني مرات للحصول على النتائج الموضحة أدناه. أخيرًا، حدد وجمّع (Ctrl + G) جميع المجموعات التي أنشأناها من بداية الخطوة رقم 20 إلى الخطوة الحالية. ضع المجموعة التي أنشأناها في الخطوة رقم 21 في الموضع الذي تراه في الشكل الأول أدناه. حدّد الآن الدائرة ذات اللون الأحمر الداكن التي أنشأناها في الخطوة رقم 19 وأنشئ نسخةً منها بالضغط على Ctrl + C ثم Ctrl + F، ثم أحضِر هذه النسخة إلى الأمام (Ctrl + Shift + Right Square Bracket). أبقِ هذه النسخة محدَّدةً، واستمر في الضغط على مفتاح Shift، ثم انقر على المجموعة التي أنشأناها في الخطوة رقم 21. انقر بزر الفأرة الأيمن على لوحة الرسم، ثم حدّد خيار إنشاء قناع قطع Make Clipping Mask من القائمة. استخدم أداة القلم (P) لإنشاء الكائن الموضّح في الشكل الأول أدناه، ثم أنشئ نسخةً منه (بالضغط على Ctrl + C ثم Ctrl + F)، بعد ذلك استبدل لون حدّ هذه النسخة الحالي باللون الأحمر. اختر أداة إضافة نقطة ارتكاز Add Anchor Point (+)، ثم انقر على النقطتين المميزتين باللون الأصفر من الكائن الأحمر، مما يؤدي إلى إضافة نقطتي ارتكاز جديدتين إلى هذا الكائن الأحمر. حدّد نقطتي الارتكاز المميزتين باللون الأزرق وحرّكهما بمقدار 2 بكسل للأسفل، ثم حدّد نقطتي الارتكاز المميزتين باللون الأصفر وحرّكهما بمقدار 3 بكسلات للأعلى. أخيرًا، أعِد تحديد الكائن الأخضر الذي أنشأناه في الخطوة الحالية، وأزِل حدّه ثم املأه بالتدرج اللوني الخطي الموضّح أدناه. حدّد الكائن الأحمر الذي أنشأناه في الخطوة رقم 23 وأنشئ نسخةً منه بالضغط على Ctrl + C ثم Ctrl + F، ثم استبدل لون الحدّ الحالي باللون الأصفر، ثم كرّر العملية نفسها التي طبّقناها في الخطوة رقم 23 لضبط شكل الكائن الأصفر، إذ يجب أن يبدو هذا الكائن تقريبًا مثل الشكل الأول أدناه. أعِد تحديد الكائن الأحمر، وأزِل حدّه واملأه بالتدرج اللوني الخطي الموضّح في الشكل الثاني أدناه. حدّد الكائن الأصفر الذي أنشأناه في الخطوة الحالية، وأزِل حدّه واملأه باللون الأبيض (‎# f6fdff). بعد ذلك حدّد الشكل الناتج وطبّق عليه تأثير Feather بمقدار 2 بكسل. استخدم أداة القلم (P) لإنشاء الكائنات الستة الموضّحة في الشكل الأول أدناه. حدّد هذه الكائنات بعد إنشائها، ثم أزِل حدّها واملأها بالتدرج اللوني الخطي الموضّح في الشكل الثاني أدناه. استخدم أداة القلم (P) لإنشاء ستة أشكال رمادية داكنة (‎# 607278) كما هو موضّح في الشكل الثالث أدناه، ثم أنشئ ستة أشكال سوداء (‎# 020000). استخدم أداة القلم (P) لإنشاء ستة أشكال بيضاء (‎# f6fdff) كما هو موضّح في الشكلين الأول والثاني أدناه. حدّد هذه الأشكال الستة البيضاء وطبّق عليها تأثير Feather بمقدار 3 بكسلات، واستخدم أداة القلم (P) لإنشاء سبعة أشكال باللون الأزرق الفاتح (‎# 7599e2)، ثم طبّق عليها تأثير Gaussian Blur بمقدار 2 بكسل. يجب أن يبدو مذياعك الآن كما يلي: إنشاء مقبض التحكم بالصوت استخدم أداة الدائرة (L) لإنشاء دائرة أبعادها 156‎×156 بكسل، ثم انتقل إلى قائمة كائن Object ثم Transform ثم Scale، ثم حدّد الخيار Uniform، وأدخِل القيمة 90 في قسم Scale وانقر على نسخ Copy، ثم استبدل لون حدّ هذه النسخة الحالي باللون الأحمر. أعِد تحديد الدائرة السوداء التي أنشأناها في الخطوة الحالية، ثم أزِل حدّها واملأها بالتدرج اللوني الخطي الموضّح أدناه، ثم طبّق تأثير Feather بمقدار 5 بكسلات على الشكل الناتج. حدّد الدائرة الحمراء التي أنشأناها في الخطوة رقم 28 واملأها بالتدرج اللوني الشعاعي radial gradient الموضّح أدناه. حدّد الدائرة الناتجة، ثم افتح لوحة Stroke من قائمة Window ثم Stroke، وغيّر ثُخن هذا الحدّ إلى 2.5 بكسل وانقر على أيقونة محاذاة الحدّ إلى الخارج Align Stroke to Outside، ثم استبدل لون الحد الحالي بالتدرج اللوني الخطي المطبّق على الحدّ والموضّح أدناه. أبقِ هذه الدائرة محدّدةً، ثم افتح لوحة Appearance من قائمة Window ثم Appearance، وانقر على زر إضافة حدٍّ جديد Add New Stroke (في الزاوية السفلية اليسرى من لوحة Appearance)، مما يؤدي إلى إضافة حدّ ثانٍ. املأ هذا الحدّ بالتدرج اللوني الخطي الموضّح أدناه وغيّر ثُخنه إلى 2 بكسل. حدّد الدائرة التي أنشأناها في الخطوة رقم 29 وانتقل إلى قائمة كائن Object ثم Transform ثم Scale، ثم حدّد الخيار Uniform، وأدخِل القيمة 71 في قسم Scale وانقر على نسخ Copy. حدّد هذه النسخة، وافتح لوحة Appearance (من قائمة Window ثم Appearance)، ثم طبّق الخطوات الموضّحة أدناه. استخدم أداة القلم (P) لإنشاء الشكلين الموضّحين أدناه باللون الرمادي الداكن (‎# 4e5661)، ثم طبّق عليهما تأثير Gaussian Blur بمقدار 2 بكسل. حدّد الآن الدائرة التي أنشأناها في الخطوة رقم 29 وأنشئ نسخةً منها (بالضغط على Ctrl + C ثم Ctrl + F)، ثم أحضِر هذه النسخة إلى الأمام (Ctrl + Shift + Right Square Bracket). أبقِ هذه النسخة محدّدةً، واستمر في الضغط على مفتاح Shift، ثم انقر على الشكلين اللذين طبّقنا عليهما تأثير الضبابية blur في الخطوة الحالية. انقر بزر الفأرة الأيمن على لوحة الرسم، ثم حدّد خيار إنشاء قناع قطع Make Clipping Mask من القائمة، ثم اخفِ مجموعة القطع خلف الدائرة التي أنشأناها في الخطوة رقم 30. حدّد الدائرة التي أنشأناها في الخطوة رقم 30 وأنشئ نسخةً منها بالضغط على Ctrl + C ثم Ctrl + F. حدّد هذه النسخة، وافتح لوحة Appearance (من قائمة Window ثم Appearance) وأزِل حدّها، ثم انتقل إلى قائمة كائن كائن Object ثم Transform ثم Scale، وحدّد الخيار Uniform، بعد ذلك أدخِل القيمة 97 في قسم Scale وانقر على موافق، ثم حرّك الدائرة الناتجة بمقدار 11 بكسلًا للأسفل. أنشئ نسخةً من الدائرة التي حرّكناها للتو، ثم أزِل لون تعبئة هذه النسخة وأضِف إليها حدًّا مقداره 1 بكسل باللون الأحمر. استخدم أداة القلم (P) لإنشاء الأشكال الأربعة الموضّحة أدناه، ثم حدّدها وطبّق عليها تأثير Gaussian Blur بمقدار 5 بكسلات. حدّد الآن الدائرة الحمراء التي أنشأناها في الخطوة الحالية وأنشئ نسخةً منها بالضغط على Ctrl + C ثم Ctrl + F، ثم أحضِر هذه النسخة إلى الأمام (Ctrl + Shift + Right Square Bracket). حدّد هذه النسخة، ثم استمر في الضغط على مفتاح Shift وانقر على الأشكال الأربعة التي طبّقنا عليها تأثير الضبابية blur في الخطوة الحالية. انقر بزر الفأرة الأيمن على لوحة الرسم، وحدّد خيار إنشاء قناع قطع Make Clipping Mask من القائمة، ثم اخفِ مجموعة القطع خلف الدائرة الحمراء. حدّد جميع الكائنات التي أنشأناها في الخطوة رقم 32، وانتقل إلى قائمة كائن Object ثم Transform ثم Scale، بعد ذلك حدّد الخيار Uniform، وأدخِل القيمة 72 في قسم Scale ثم انقر على نسخ Copy. استخدم أداة القلم (P) لإنشاء الأشكال الثلاثة الموضّحة أدناه باللون الرمادي الداكن (‎# 5f6775)، ثم طبّق عليها تأثير Gaussian Blur بمقدار 2 بكسل. حدّد الأشكال الناتجة، ثم اضغط على الاختصار Ctrl + X لقصها، ثم اختر أداة التحديد Selection Tool (باستخدام الاختصار V) وانقر نقرًا مزدوجًا على مجموعة القطع التي أنشأناها في الخطوة الحالية، وبعد ذلك اضغط على الاختصار Ctrl + F للصق الأشكال التي قصصناها للتو، ثم انقر نقرًا مزدوجًا في أيّ مكان خارج مجموعة القطع. حدّد مجموعة القطع والدائرة الحمراء الصغيرة، ثم اضغط على الاختصار Ctrl +2 لقفلهما، ثم حدّد الدائرة الأخرى التي أنشأناها في الخطوة الحالية واستبدل لون التعبئة الحالي بتدرج لوني شعاعي جديد كما هو موضّح أدناه. أخيرًا، اضغط على الاختصار Ctrl + Alt +2 لفك قفل جميع الكائنات التي قفلناها في الخطوة الحالية. حدّد الدائرة الحمراء الكبيرة، ثم غيّر ثُخن حدّها إلى 1.5 بكسل، واستبدل لون الحد الحالي بالتدرج اللوني الخطي الموضّح أدناه ضمن الحد. حدّد الدائرة الحمراء الأخرى، وغيّر ثُخن الحدّ إلى 5 بكسلات، ثم استبدل لون الحدّ الحالي باللون الرمادي الداكن (‎# 9e9ea3). أنشئ نسخةً من الدائرة الناتجة (بالضغط على Ctrl + C ثم Ctrl + F)، ثم غيّر ثُخن حدّ هذه النسخة إلى 4 بكسلات واستبدل لون الحدّ الحالي باللون الأزرق الفاتح (‎# b7d6ff). حدّد الآن الدائرة الأولى التي أنشأناها في الخطوة رقم 32 وانتقل إلى قائمة تأثير Effect ثم Stylize ثم ظل ساقط Drop Shadow، واتبع البيانات الموضّحة أدناه، ثم انقر على موافق. استخدم أداة القلم (P) لإنشاء الشكلين الموضحين أدناه باللون الرمادي الداكن (‎# 656b77)، ثم طبّق عليهما تأثير Gaussian Blur بمقدار 2 بكسل. حدّد الآن الدائرة التي أنشأناها في الخطوة رقم 30 وأنشئ نسخةً منها بالضغط على Ctrl + C ثم Ctrl + F، ثم أحضِر هذه النسخة إلى الأمام (Ctrl + Shift + Right Square Bracket). أبقِ هذه النسخة محدَّدةً واستمر في الضغط على مفتاح Shift، ثم انقر على الشكلين اللذين طبّقنا عليهما تأثير الضبابية blur في الخطوة الحالية. انقر بزر الفأرة الأيمن على لوحة الرسم، ثم حدّد خيار إنشاء قناع قطع Make Clipping Mask من القائمة، ثم اخفِ مجموعة القطع هذه خلف الدائرة الأولى التي أنشأناها في الخطوة رقم 32. حدّد جميع الكائنات التي أنشأناها من بداية الخطوة رقم 28 وحتى الآن وجمّعها (Ctrl + G)، ثم ضع هذه المجموعة في الموضع الموضّح في الشكل التالي: إنشاء ضابط المذياع استخدم أداة المستطيل (M) لإنشاء مستطيل أحمر أبعاده 296‎×54 بكسل، ثم انتقل إلى قائمة كائن Object ثم Transform ثم Scale، وحدّد الخيار Non-Uniform، ثم أدخِل القيمة 97 في خانة أفقي Horizontal والقيمة 86 في خانة رأسي Vertical، ثم انقر على نسخ Copy. أعِد تحديد هذين المستطيلين وأنشئ نسخةً منهما بالضغط على Ctrl + C ثم Ctrl + F. حدّد هذه النُسخ، ثم افتح لوحة مستكشف المسار Pathfinder (من قائمة Window ثم Pathfinder) وانقر على زر Minus Front. حدّد الكائن الناتج، ثم أزِل حدّه واملأه بالتدرج اللوني الخطي الموضّح أدناه، ثم انتقل إلى قائمة تأثير Effect ثم Stylize ثم ظل ساقط Drop Shadow، واتبع البيانات الموضحة أدناه ثم انقر على موافق. حدّد المستطيل الأحمر الأكبر الذي أنشأناه في الخطوة الحالية وأنشئ نسخةً منه، ثم أحضِر هذه النسخة إلى الأمام (Ctrl + Shift + Right Square Bracket). تأكّد من أن هذه النسخة لا تزال محدَّدةً واستمر في الضغط على مفتاح Shift، ثم انقر على الشكل الذي طبّقنا عليه تأثير الظل Shadow في الخطوة الحالية. انقر بزر الفأرة الأيمن على لوحة الرسم، ثم حدّد خيار إنشاء قناع قطع Make Clipping Mask من القائمة. حدّد المستطيل الأحمر الأكبر الذي أنشأناه في الخطوة رقم 37، واجلبه إلى الأمام عن طريق الاختصار Ctrl + Shift + Right Square Bracket. استخدم أداة التحديد المباشر (A) مع مفتاح Shift لتحديد نقطتي ارتكاز هذا المستطيل الأحمر السفليتين، ثم انقر على أيقونة "قص المسارات عند نقاط الارتكاز المحدَّدة Cut paths at selected anchor points" من شريط خصائص Properties، مما يجعل هذا المستطيل الأحمر مؤلفًا من مسارين. حدّد المسار العلوي وأزِله، ثم أعِد تحديد المسار الآخر واستبدل لون حدّه الحالي باللون السماوي الفاتح (‎# d7f0f5). أخيرًا، حدّد المستطيل الأحمر الآخر، وأزِل حدّه واملأه باللون البني الداكن (‎# 231f18). استخدم أداة الخط Line Segment Tool () لإنشاء خط رأسي بطول 46 بكسلًا، وبحدٍّ مقداره 2 بكسل باللون الأسود (‎# 2b2820) وبلا تعبئة. حدّد هذا الخط وانتقل إلى قائمة كائن Object ثم Transform ثم Move، ثم أدخِل القيمة 3 بكسلات في خانة أفقي Horizontal وانقر على نسخ Copy، ثم اضغط على الاختصار Ctrl + D إلى أن تحصل على النتائج الموضّحة أدناه. أخيرًا، حدّد جميع الخطوط التي أنشأناها في الخطوة الحالية وجمّعها (Ctrl + G)، ثم ضع هذه المجموعة في الموضع الموضّح أدناه. استخدم أداة الخط Line Segment Tool () لإنشاء خط أفقي بطول 260 بكسلًا، وبحدّ مقداره 1.5 بكسل باللون الأزرق المائل إلى الرمادي (‎# d1d3d4) وبلا تعبئة. أنشئ خطًا رأسيًا بطول 9 بكسلات، وبحدٍّ مقداره 1.5 بكسل باللون الأزرق المائل إلى الرمادي وبلا تعبئة، ثم ضع هذين الخطين في المواضع الموضّحة أدناه. أعِد تحديد الخط الرأسي وانتقل إلى قائمة كائن Object ثم Transform ثم Move، وأدخِل القيمة ‎-3 بكسل في خانة أفقي Horizontal وانقر على نسخ Copy، ثم اضغط على الاختصار Ctrl + D إلى أن تحصل على النتائج الموضّحة أدناه. استخدم أداة التحديد (V) مع مفتاح Shift لتحديد الخطوط الرأسية التسعة الموضّحة أدناه، ثم افتح لوحة Transform من قائمة Window ثم Transform، وغيّر قيمة الخيار H إلى 17 بكسلًا. أخيرًا، حدّد جميع الخطوط التي أنشأناها في الخطوة الحالية وجمّعها (Ctrl + G)، ثم ضع هذه المجموعة في الموضع الموضّح أدناه. اختر أداة الكتابة Type Tool باستخدام الاختصار T، ثم افتح لوحة Character من قائمة Window ثم Type ثم Character، واتبع البيانات الموضّحة أدناه. انقر بعد ذلك على لوحة الرسم واكتب النص "AM" و "FM"، ثم ضع هذه النصوص في الموضع الموضّح أدناه. استخدم أداة الخط Line Segment Tool () لإنشاء خط رأسي بطول 46 بكسلًا، وبحدٍّ مقداره 2 بكسل باللون الأحمر (‎# ed1c24) وبلا تعبئة. استخدم الآن أداة المستطيل (M) وأداة التدرج (G) لإنشاء مستطيل أبعاده 276‎×40 بكسل كما هو موضّح أدناه، ثم طبّق عليه تأثير Gaussian Blur بمقدار 2 بكسل. يجب أن يبدو مذياعك الآن كما يلي: إنشاء الأزرار انتقل إلى القسم العلوي من المذياع، ثم اختر أداة المستطيل ذي الزوايا المستديرة Rounded Rectangle Tool من شريط الأدوات، وانقر على لوحة الرسم. بعد ذلك أدخِل البيانات الموضّحة في الشكل الأول أدناه، ثم ضع هذا المستطيل في الموضع الموضّح أدناه. استخدم أداة التحديد المباشر (A) مع مفتاح Shift لتحديد نقطتي ارتكاز المستطيل الأصفر المميزتين باللون الأخضر وحرّكهما بمقدار 2 بكسل للأسفل. حدّد الآن نقطة الارتكاز العلوية اليسرى وحرّكها بمقدار 2 بكسل إلى اليمين، ثم حدّد نقطة الارتكاز العلوية اليمنى وحرّكها بمقدار 2 بكسل إلى اليسار. أنشئ نسخةً (بالضغط على Ctrl + C ثم Ctrl + F) من المستطيل الأصفر الذي أنشأناه في الخطوة رقم 43، ثم استبدل لون حدّ هذه النسخة باللون الأخضر. حدّد نقطتي ارتكاز هذا المستطيل الأخضر المميزتين باللون الأزرق وأزِلهما، ثم حدّد نقطة ارتكاز الكائن الناتج العلوية اليسرى، وحرّكها بمقدار 8 بكسلات للأسفل و 2 بكسل إلى اليسار، ثم حدّد نقطة الارتكاز العلوية اليمنى وحرّكها بمقدار 8 بكسلات للأسفل، ثم 2 بكسل إلى اليمين. أخيرًا، استخدم أداة التحديد المباشر (A) مع مفتاح Shift لتمديد مقابض نقطتي الارتكاز العلويتين كما هو موضّح أدناه. حدّد الكائن الأصفر الذي أنشأناه في الخطوة رقم 43، ثم أزِل حدّه واملأه بالتدرج اللوني الخطي الموضّح أدناه، ثم حدّد الكائن الأخضر، وأزِل حده واملأه باللون الأسود (‎# 1a1a1a). حدّد الشكل الناتج وأنشئ نسخةً منه بالضغط على Ctrl + C ثم Ctrl + F، بعد ذلك استبدل لون تعبئة هذه النسخة الحالي بالتدرج اللوني الخطي الموضّح أدناه. حدّد الآن نقطة ارتكاز هذه النسخة العلوية اليسرى، وحرّكها بمقدار 3 بكسلات للأسفل و 1 بكسل إلى اليمين، ثم حدّد نقطة الارتكاز العلوية اليمنى، وحرّكها بمقدار 3 بكسلات للأسفل و1 بكسل إلى اليسار، ثم حدّد الشكل الأول الذي أنشأناه في الخطوة الحالية وأنشئ نسخةً منه. بعد ذلك أحضِر هذه النسخة إلى الأمام (Ctrl + Shift + Right Square Bracket). أخيرًا، أزِل لون تعبئة هذه النسخة، ثم أضِف حدًا مقداره 1 بكسل باللون الأزرق المائل إلى الرمادي (‎# bcbec0). استخدم أداة المستطيل ذي الزوايا المستديرة Rounded Rectangle Tool لإنشاء مستطيل أبيض (‎# f2f2f2) كما هو موضّح أدناه، ثم أنشئ نسخةً منه بالضغط على Ctrl + C ثم Ctrl + F. حدّد هذه النسخة، ثم أزِل لون تعبئتها وأضف حدًا مقداره 1 بكسل باللون الأبيض (‎# fcfcfc). أعِد تحديد المستطيل الأول الذي أنشأناه في الخطوة الحالية وأنشئ نسخةً منه، ثم حرّك هذه النسخة بمقدار 10 بكسلات للأسفل، ثم استبدل لون تعبئة هذه النسخة الحالي بالتدرج اللوني الخطي الموضّح أدناه، ثم أرسِل المستطيل الناتج إلى الخلف (Ctrl + Shift + Left Square Bracket). حدّد المستطيل الأخير الذي أنشأناه في الخطوة رقم 46 وانتقل إلى قائمة كائن Object ثم Transform ثم Scale، وحدّد الخيار Non-Uniform، وأدخِل القيمة 103 في خانة أفقي Horizontal ثم انقر على نسخ Copy. استبدل لون تعبئة هذه النسخة الحالي باللون الأسود (‎# 000000)، وحرّك المستطيل الناتج بمقدار 2 بكسل للأسفل، ثم أرسله للخلف (Ctrl + Left Square Bracket). حدّد الآن جميع الكائنات التي أنشأناها من بداية الخطوة رقم 46 إلى الخطوة الحالية، ثم انتقل إلى قائمة كائن Object ثم Transform ثم Move، وأدخِل القيمة 71 في خانة أفقي Horizontal وانقر على نسخ Copy، ثم اضغط على الاختصار Ctrl + D مرةً واحدة للحصول على النتائج الموضّحة أدناه. حدد وجمّع (Ctrl + G) جميع الكائنات التي أنشأناها من بداية الخطوة رقم 46 حتى الآن، ثم ضع هذه المجموعة في الموضع الموضّح أدناه. إنشاء الهوائي استخدم أداة المستطيل (M) وأداة الدائرة (L) لإنشاء مستطيل أبعاده 16‎×55 بكسل ودائرة أبعادها ‎16×5 بكسل، ثم ضع هذين الكائنين في الموضع الموضّح أدناه. اختر أداة إضافة نقطة ارتكاز Add Anchor Point (+) وانقر على النقطة المميزة باللون الأرجواني من المستطيل الذي أنشأناه في الخطوة الحالية، مما يؤدي إلى إضافة نقطة ارتكاز جديدة لهذا الكائن. حدّد نقطة الارتكاز المضافَة حديثًا، ثم حرّكها بمقدار 4 بكسلات للأسفل. اختر أداة تحويل نقاط الارتكاز Convert Anchor Point Tool (باستخدام الاختصار Shift + C)، ثم انقر على نقطة الارتكاز التي حرّكتها للتو، واضغط على الفأرة واسحبها إلى اليسار أثناء الضغط على مفتاح Shift. أعِد تحديد الكائنين اللذين أنشأناهما في الخطوة الحالية وانتقل إلى قائمة كائن Object ثم Transform ثم Scale، ثم حدّد الخيار Uniform، وأدخِل القيمة 87 في قسم Scale وانقر على نسخ Copy، ثم حرّك هذه النُسخ بمقدار 54 بكسلًا للأعلى. حدد آخر كائنين أنشأناهما في الخطوة رقم 49 وانتقل إلى قائمة كائن Object ثم Transform ثم Scale، وحدّد الخيار Uniform، ثم أدخِل القيمة 87 في قسم Scale وانقر على نسخ Copy، ثم حرّك هذه النُسخ بمقدار 46 بكسلًا للأعلى. حدّد الكائنين الناتجين وافتح نافذة Scale مرةً أخرى، ثم حدّد الخيار Uniform وأدخِل القيمة 138 في قسم Scale، ثم انقر على نسخ Copy. حدّد نقاط ارتكاز الكائن الأرزق الثلاثة السفلية وحرّكها بمقدار 49 بكسلًا للأعلى. حدّد الكائن الأزرق العلوي الذي أنشأناه في الخطوة رقم 50، ثم أزِل حدّه واملأه بالتدرج اللوني الخطي الموضّح أدناه. حدّد الكائنين الزرقاويين المتبقيين، ثم اختر أداة القطارة Eyedropper Tool (باستخدام الاختصار I) وانقر على الشكل الأول الذي أنشأناه في الخطوة الحالية. حدّد الدائرة العلوية التي أنشأناها في الخطوة رقم 49، ثم أزِل حدّه واملأه بالتدرج اللوني الخطي الموضّح أدناه. أخيرًا، أعِد تحديد الدائرتين الحمراوتين المتبقيتين، ثم اختر أداة القطارة Eyedropper Tool (باستخدام الاختصار I) وانقر على الدائرة العلوية التي ملأتها بالتدرج اللوني الخطي في الخطوة الحالية. حدّد الشكل الأول الذي ملأته بالتدرج اللوني الخطي في الخطوة رقم 51، وأنشئ نسخةً منه بالضغط على Ctrl + C ثم Ctrl + F. حدّد هذه النسخة وانتقل إلى قائمة تأثير Effect ثم Stylize ثم ظل ساقط Drop Shadow، واتبع البيانات الموضّحة في الشكل الأول أدناه وانقر على موافق. حدّد الآن الشكل الموضّح في الشكل الثاني أدناه وأنشئ نسخة منه، ثم أحضِر هذه النسخة إلى الأمام (Ctrl + Shift + Right Square Bracket). حدّد هذه النسخة واستمر في الضغط على مفتاح Shift، ثم انقر على الشكل الذي طبّقنا عليه تأثير الظل Shadow المطبق في الخطوة الحالية. انقر بزر الفأرة الأيمن على لوحة الرسم، ثم حدّد خيار إنشاء قناع قطع Make Clipping Mask من القائمة، ثم اخفِ مجموعة القطع خلف الشكل الأول الذي ملأناه بالتدرج اللوني الخطي في الخطوة رقم 51. حدّد وجمّع (Ctrl + G) جميع الكائنات التي أنشأناها من بداية الخطوة رقم 49 حتى الآن، ثم ضع هذه المجموعة في الموضع الموضّح أدناه. استخدم أداة الدائرة (L) لإنشاء دائرة أبعادها 21‎×12 بكسل باللون الأسود (‎# 130000) كما هو موضّح أدناه، ثم اخفِ هذه الدائرة خلف المجموعة التي أنشأناها في الخطوة الحالية. حدّد جميع الكائنات التي أنشأناها من بداية الخطوة رقم 2 حتى الآن وجمّعها (Ctrl + G). أصبح المذياع جاهزًا ويجب أن يبدو مثل الشكل التالي: إنشاء الخلفية استخدم أداة المستطيل (M) لإنشاء مستطيلين أبعادهما 834‎×700 بكسل و 882‎×284 بكسل، ثم ضع هذين المستطيلين في الموضع الموضّح أدناه. أعِد تحديد هذين المستطيلين، ثم أزِل حدّهما واملأهما بالتدرجات اللونية الشعاعية الموضّحة أدناه. أخيرًا، أعِد تحديد المستطيل الأصغر وطبّق عليه تأثير Gaussian Blur بمقدار 11 بكسلًا. ضع المذياع في هذه الخلفية، ثم أضف ظلًا له لإعطائه مظهرًا أكثر واقعية، وذلك من خلال استخدام أداة القلم (P) وأداة التدرج (G) لإنشاء الشكل الموضّح أدناه، ثم طبّق عليه تأثير Gaussian Blur بمقدار 5 بكسلات، ثم اخفِ الشكل الناتج خلف المذياع. وبذلك أصبح كل شيء جاهزًا، حيث يُفترض أن ينتج معك الشكل النهائي الآتي: ترجمة -وبتصرّف- للمقال Learn to create a Vector Radio in Adobe Illustrator لصاحبه Bao Nguyen. اقرأ أيضًا كيفية إنشاء تلفاز قديم باستخدام إليستريتور كيفية إنشاء الدواليب الهوائية الملونة باستخدام الإليستريتور كيفية إنشاء مصباح كهربائي في برنامج إليستريتور إنشاء ساعة كاسيو Casio في الإليستريتور
  25. تحدثنا في مقال سابق عن شبكات التراكب ثم تطرقنا إلى شبكات الند للند وسنكمل في هذا المقال الحديث عن بروتوكول البت تورنت، وهو بروتوكول مشاركة ملفات ند لند ابتكره برام كوهين Bram Cohen، ويعتمد على نسخ الملف أو نسخ أجزاءٍ منه والتي تُسمى قِطعًا pieces، حيث يمكن تحميل أية قطعةٍ معينة من عدة أنداد، حتى لو كان هناك ندٌ فقط لديه الملف بأكمله. الفائدة الأساسية لعملية النسخ التي يفعلها بروتوكول بت تورنت هي تجنب الاختناق المتمثل في وجود مصدرٍ واحد فقط للملف، حيث يكون هذا مفيدًا عندما تفكر في أن أي حاسوبٍ لديه سرعةٌ محدودةٌ يمكنه من خلالها خدمة الملفات عبر رابطٍ صاعدٍ خاصٍ به إلى الإنترنت، ويكون حد هذه السرعة منخفضًا غالبًا بسبب الطبيعة غير المتماثلة لمعظم شبكات النطاق العريض. تكمن أهمية بروتوكول بت تورنت في أن النَّسخ replication هو أحد الآثار الجانبية الطبيعية لعملية التنزيل downloading، فبمجرد تنزيل أحد الأنداد لقطعةٍ معينة، يصبح هذا الند مصدرًا آخر لتلك القطعة، وكلما زاد عدد الأنداد الذين ينزّلون قطعًا من الملف كلما حدث المزيد من النَّسخ للقطعة، مع توزيع التحميل بصورةٍ متناسبة، وزيادة حيز النطاق التراسلي الإجمالي المتاح لمشاركة الملف مع الآخرين. تُنزَّل القطع بترتيبٍ عشوائي لتجنب الموقف الذي يجد فيه الأنداد أنفسهم مفتقرين لنفس مجموعة القطع. تجري مشاركة كل ملفٍ عبر شبكة بت تورنت المستقلة الخاصة به، والتي تُسمى سربًا swarm، ويمكن أن يشارك السرب مجموعةً من الملفات، لكننا نصف حالة ملفٍ فقط للبساطة. تكون دورة حياة السرب النموذجية كما يلي: يبدأ السرب ندًا يملك نسخةً كاملةً من الملف، ثم تنضم العقدة التي تريد تنزيل الملف إلى السرب لتصبح العضو الثاني، وتبدأ بتنزيل أجزاءٍ من الملف من الند الأصلي، وبذلك تصبح مصدرًا آخر للِقطع التي نزّلتها، حتى لو لم تنزّل الملف بالكامل بعد. من الشائع أن يترك الأنداد السرب بمجرد الانتهاء من تنزيلاتهم، على الرغم من تشجيعهم للبقاء لفترة أطول. تنضم العقد الأخرى إلى السرب وتبدأ بتنزيل القطع من أندادٍ متعددين، وليس فقط من الند الأصلي، كما هو موضّحٌ في الشكل السابق. إذا بقي الطلب مرتفعًا على الملف، مع وجود مجرىً من الأنداد الجدد الذين يحلّون محل أولئك الذين غادروا السرب، فيمكن أن يظل السرب نشطًا إلى أجلٍ غير مسمى؛ وإذا لم يكن الأمر كذلك، فقد يتقلص السرب ليشمل الند الأصلي فقط حتى ينضم أندادٌ جدد إلى السرب. يمكننا، بعد أن أصبح لدينا نظرةً عامةً لبروتوكول بت تورنت، أن نسأل كيف توجَّه الطلبات إلى الأنداد الذين لديهم قطعةً معينة. يجب أن ينضم المنزِّل downloader المحتمل أولًا إلى السرب لتقديم الطلبات، فيبدأ بتنزيل ملفٍ يحتوي على معلوماتٍ تعريفيةٍ حول الملف والسرب، حيث يُنزَّل عادةً الملف الذي يمكن نسخه بسهولة من خادم ويب ويمكن اكتشافه باتباع الروابط من صفحات الويب. يحتوي هذا الملف على ما يلي: حجم الملف الهدف. حجم القطعة. القيم المعمَّاة SHA-1 المحسوبة مسبقًا من كل قطعة؟ عنوان URL لمتتبّع tracker السرب. المتتبّع هو خادمٌ يتتبع عضوية السرب الحالية. سنرى لاحقًا أنه يمكن توسيع بروتوكول بت تورنت للتخلص من هذه النقطة المركزية التي قد تؤدي إلى حدوث اختناقٍ أو فشل. ينضم المنزِّل المحتمل بعد ذلك إلى السرب، ليصبح ندًا، عن طريق إرسال رسالةٍ إلى المتتبّع تحتوي على عنوان شبكته ومعرّف الند الذي أنشأه عشوائيًا لنفسه. تحمل الرسالة أيضًا القيمة المُعمَّاة SHA-1 للجزء الرئيسي من الملف، والتي تُستخدَم مثل معرّف سرب. افترض أن الند الجديد يُدعَى P. يرد المتتبّع على الند P بقائمةٍ جزئيةٍ من الأنداد الذين يقدمون معرفاتهم وعناوين الشبكة الخاصة بهم، وينشئ الند P اتصالاتٍ عبر بروتوكول TCP مع بعض هؤلاء الأنداد. لاحظ أن الند P متصلٌ مباشرةً بمجموعةٍ فرعيةٍ فقط من السرب، على الرغم من أنه قد يقرر الاتصال بأندادٍ إضافيين أو طلب المزيد من الأنداد من المتتبّع. يرسل الند P معرّف السرب الخاص به ومعرّفَ الند لإنشاء اتصال بت تورنت بندٍ معين بعد إنشاء اتصال TCP الخاص به، ويرد الند بمعرّفه ومعرّف السرب؛ فإذا لم تتطابق معرّفات السرب، أو لم يكن معرّف الند الذي أجرى الرد هو ما يتوقعه الند P، فسيُلغى الاتصال. اتصال بت تورنت الناتج هو اتصالٌ متماثل نظرًا لإمكانية كل طرفٍ التنزيل من الطرف الآخر، حيث يبدأ كل طرفٍ بإرسال خارطةٍ نقطية bitmap تعلن فيها للطرف الآخر عن الأجزاء الموجودة به، بحيث يعرف كل ندٍ الحالة الأولية للند الآخر. يرسل المنزِّل D عندما ينتهي من تنزيل قطعةٍ أخرى رسالةً تحدد تلك القطعة إلى كل من أنداده المتصلين مباشرةً، حتى يتمكن هؤلاء الأنداد من تحديث تمثيلهم الداخلي لحالة المنزِّل D، ويكون هذا بمثابة إجابةٍ للسؤال حول كيفية توجيه طلب تنزيل لقطعةٍ ما إلى ندٍ لديه القطعة، لأنه يعني أن كل ندٍ يعرف الند المتصل مباشرةً به والذي يملك القطعة. إذا احتاج المنزِّل D إلى قطعةٍ لا تمتلكها أيٌّ من اتصالاته، فيمكنه الاتصال بأندادٍ أكثر أو أندادٍ مختلفين؛ حيث يمكنه الحصول على المزيد من الأنداد من المتتبّع، أو يشغُل نفسه بقطعٍ أخرى على أمل أن تحصل بعض اتصالاته على القطعة من اتصالاتها. كيف تُربَط الكائنات، أو القطع في هذه الحالة مع العقد الأنداد؟ سيحصل كل ندٍ في النهاية على جميع القطع، لذا فإن السؤال يتعلق بالقطع التي يمتلكها الند في وقتٍ معين قبل أن يحتوي على كل القطع أو حول الترتيب الذي ينزِّل به الندُ القطعَ. الإجابة هي أنهم ينزّلون القطع بترتيبٍ عشوائي، وذلك لمنعهم من الحصول على مجموعةٍ فرعيةٍ صارمةٍ أو مجموعةٍ شاملةٍ من القطع الخاصة بأيٍ من أندادهم. يستخدم بروتوكول بت تورنت الموصوف حتى الآن متتبّعًا مركزيًا يشكل نقطة فشل واحدة للسرب ويمكن أن يكون نقطة اختناق للأداء. كما يمكن أن يكون أيضًا توفير متتبّعٍ مصدر إزعاجٍ لمن يرغب في إتاحة ملفٍ عبر بروتوكول بت تورنت. تدعم الإصدارات الأحدث من بروتوكول بت تورنت أيضًا الأسراب التي لا تحتوي متتبّعًا trackerless، والتي تستخدِم تطبيقًا يعتمد على جدول التعمية الموزَّع DHT؛ حيث لا يطبّق برنامج عميل بت تورنت الذي لا يحتوي متتبّعًا ند بت تورنت فحسب، بل يطبّق أيضًا ما نُطلق عليه أداة البحث عن الأنداد peer finder، التي تُسمَّى عقدة node باستخدام مصطلحات بت تورنت، والتي يستخدمها الند للعثور على أنداده. تشكّل أدوات البحث عن الأنداد شبكةَ التراكب الخاصة بها باستخدام بروتوكولها الخاص عبر بروتوكول UDP لتطبيق جدول DHT، حيث تتضمّن شبكة أداة البحث أدوات بحثٍ عن الأنداد الذين ينتمي أندادهم المرتبطين بهم إلى أسرابٍ مختلفة؛ أي يشكّل كل سربٍ شبكةً مميزةً من أنداد بت تورنت، لكن تمتد شبكة أدوات البحث عن الأنداد عبر الأسراب بدلًا من ذلك. تنشئ أدوات البحث عن الأنداد معرّفات أدوات البحث الخاصة بها عشوائيًا، والتي تكون بنفس حجم معرّفات السرب؛ أي 160 بتًا. وتحتفظ كل أداة بحث بجدولٍ متواضع يحتوي بصورةٍ أساسية على أدوات البحث عن الأنداد والأنداد المرتبطين بها، التي تكون معرّفاتها قريبةً من معرّف هذه الأداة، بالإضافة إلى بعض أدوات البحث التي تكون معرّفاتها أبعد. تضَمن الخوارزمية التالية إمكانية معرفة أدوات البحث، التي تكون معرّفاتها قريبةً من معرّف سربٍ معين، أندادها من هذا السرب؛ حيث توفر الخوارزمية في الوقت نفسه طريقةً للبحث عنهم. عندما تحتاج أداة البحث F إلى العثور على أندادٍ من سرب معين، فإنها ترسل طلبًا إلى أدوات البحث في جدولها التي تكون معرّفاتها قريبةً من معرّف ذلك السرب، فإذا عرفت أداة البحث المتصلة أندادًا من هذا السرب، فإنها ترد بمعلومات الاتصال الخاصة بها؛ وإلا فإنها ترد بمعلومات الاتصال الخاصة بأدوات البحث في جدولها القريبة من السرب، بحيث يمكن لأداة البحث F الاستعلام بصورةٍ متكررةٍ عن أدوات البحث تلك. إذا وصل البحث إلى طريقٍ مسدود، نظرًا لعدم وجود أدوات بحثٍ أقرب إلى السرب، تُدخِل أداة البحث F معلومات الاتصال الخاصة بها والند المرتبط بها ضمن أدوات البحث الأقرب إلى السرب، وبالنتيجة يحدث إدخال أنداد سربٍ معين في جداول أدوات البحث القريبة من هذا السرب. تفترض المناقشة أعلاه أن أداة البحث F هي بالفعل جزءٌ من شبكة أداة البحث، وتفترض أن هذه الأداة تعرف بالفعل كيفية الاتصال ببعض أدوات البحث الأخرى. هذا الافتراض صحيحٌ بالنسبة لعمليات تثبيت أداة البحث المُشغَّلة سابقًا، لأنها تحفظ المعلومات حول أدوات البحث الأخرى، حتى عبر عمليات التنفيذ. إذا كان السرب يستخدم متتبّعًا، فسيكون أنداد هذا السرب قادرين على إخبار أدوات البحث الخاصة بهم عن أدوات البحث الأخرى (على عكس الدور الذي يلعبه الأنداد وأدوات البحث) بسبب توسّع بروتوكول أنداد بت تورنت لتبادل معلومات اتصال أداة البحث. ولكن كيف يمكن لأداة بحث مثبَّتة مؤخرًا اكتشاف أدواة بحثٍ أخرى؟ تتضمن ملفات الأسراب التي لا تحوي متتبّعًا معلومات الاتصال الخاصة بأداةٍ أو عددٍ قليلٍ من أدوات البحث، بدلًا من عنوان URL الخاص بالمتتبّع، لهذه الحالة فقط. أحد الجوانب غير الاعتيادية في بروتوكول بت تورنت هو أنه يتعامل مباشرةً مع مسألة العدالة أو المواطنة الجيدة على الشبكة، حيث تعتمد البروتوكولات غالبًا على السلوك الجيد للأنداد دون القدرة على فرض هذا السلوك، فيمكن مثلًا أن يحصل الند صاحب السلوك السيء في شبكة إيثرنت على أداءٍ أفضل باستخدام خوارزمية التراجع التي تكون أكثر قوةً من التراجع الأسي، أو يمكن لند TCP صاحب السلوك السيء الحصول على أداءٍ أفضل من خلال عدم التعاون في التحكم في الازدحام. السلوك الجيد الذي يعتمد عليه بروتوكول بت تورنت هو رفع uploading الأنداد القطعَ إلى أندادٍ آخرين. نظرًا لحاجة مستخدم بت تورنت العادي لتنزيل الملف فقط بأسرع ما يمكن، فهناك إغراءٌ لتطبيق ندٍ يحاول تنزيل جميع القطع مع أقل قدرٍ ممكنٍ من الرفع،ويُعد هذا ندًا سيئَا. يشتمل بروتوكول بت تورنت على آلياتٍ تسمح للأنداد بمكافأة أو معاقبة بعضهم بعضًا بهدف التقليل من هذا السلوك السيء؛ فإذا أساء أحد الأنداد التصرفَ بعدم الرفع بصورةٍ جيدة إلى ندٍ آخر، فيمكن للند الثاني أن يخنق choke الند السيء، حيث يمكنه أن يقرر إيقاف الرفع إلى الند السيء، مؤقتًا على الأقل، ويرسل إليه رسالةً تفيد بذلك، وهناك أيضًا نوع من الرسائل لإخبار الند بإلغاء الاختناق. يستخدم الند آليةَ الاختناق أيضًا للحد من عدد اتصالات بت تورنت النشطة، للحفاظ على أداءٍ جيد لبروتوكول TCP. هناك العديد من خوارزميات الاختناق المحتملة، لكن ابتكار خوارزميةٍ جيدةٍ هو فنٌ بحد ذاته. ترجمة -وبتصرّف- للقسم Overlay Networks من فصل Applications من كتاب Computer Networks: A Systems Approach. اقرأ أيضًا المتطلبات اللازمة لبناء شبكة حاسوبية البرمجيات المستخدمة في بناء الشبكات الحاسوبية بروتوكولات طرف إلى طرف End-to-End Protocols في الشبكات الحاسوبية
×
×
  • أضف...