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

Ola Abbas

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

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

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

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

  1. سنركِّز في هذه السلسلة تعلم تطوير الويب من المقالات على اختبار مشاريع الويب للتوافق مع المتصفحات Cross Browser Testing، حيث سنحدِّد الجمهور المستهدَف مثل معرفة المستخدِمين والمتصفحات والأجهزة التي يجب أخذها في الحسبان، وسنتعلّم كيفية إجراء الاختبار والمشاكل الرئيسية التي ستواجهها مع أنواع مختلفة من الشيفرات البرمجية وكيفية التخفيف من هذه المشاكل، كما سنتعرّف على الأدوات المفيدة لمساعدتك على اختبار المشاكل وإصلاحها وكيفية استخدام الاختبارات الآلية لتسريع عملية الاختبار. المتطلبات الأساسية: يجب أن تتعلم أساسيات لغات HTML وCSS وجافاسكربت JavaScript أولًا قبل محاولة استخدام الأدوات التي سنشرحها لاحقًا. تتألف هذه السلسلة من المقالات التالية: مدخل إلى اختبار مشاريع الويب للتوافق مع المتصفحات (المقال الحالي): سنقدم في هذا المقال نظرةً عامةً حول موضوع اختبار مشاريع الويب للتوافق مع المتصفحات، وسنجيب على أسئلة مثل "ما هو اختبار التوافق مع المتصفحات؟" و"ما هي أكثر أنواع المشاكل شيوعًا التي ستواجهها؟" و"ما هي الطرق الرئيسية لاختبار المشاكل وتحديدها وإصلاحها؟". استراتيجيات اختبارات مشاريع الويب للتوافق مع المتصفحات: سننتقل بعد ذلك إلى إجراء الاختبارات، وتحديد الجمهور المستهدَف مثل المتصفحات والأجهزة والمجالات الأخرى التي يجب التأكد من اختبارها، وتحديد استراتيجيات اختبار Lo-fi مثل الحصول على مجموعة من الأجهزة وبعض الآلات الافتراضية وإجراء اختبارات مخصصة Ad-hoc عند الحاجة، واستراتيجيات التقنيات المتقدمة مثل الاختبارات الآلية واستخدام تطبيقات الاختبار المخصَّصة، وإجراء الاختبارات مع مجموعات المستخدِمين. معالجة المشاكل الشائعة للتوافق مع المتصفحات في شيفرة HTML وCSS: سننظر بعد ذلك على وجه التحديد في المشاكل الشائعة للتوافق مع المتصفحات التي ستواجهها في شيفرة HTML وCSS، بالإضافة إلى الأدوات التي يمكن استخدامها لمنع حدوث هذه المشاكل أو إصلاحها،كما يتضمن ذلك فحص أخطاء الشيفرة Linting Code والتعامل مع بادئات CSS واستخدام أدوات تطوير المتصفح لتتبّع المشاكل واستخدام تعويض دعم المتصفحات Polyfill لإضافة الدعم إليها ومعالجة مشاكل التصميم المتجاوب مع الشاشات وغير ذلك الكثير. معالجة المشاكل الشائعة للتوافق مع المتصفحات في شيفرة جافاسكربت: سننظر بعد ذلك في مشاكل جافاسكربت الشائعة للتوافق مع المتصفحات وكيفية إصلاحها، ويتضمن ذلك معلومات حول استخدام أدوات تطوير المتصفح لتتبّع المشاكل وإصلاحها واستخدام تعويض دعم المتصفحات Polyfill والمكتبات لحل المشاكل والحصول على ميزات جافاسكربت الحديثة التي تعمل في المتصفحات القديمة وغير ذلك الكثير. معالجة مشاكل سهولة الوصول Accessibility الشائعة للتوافق مع المتصفحات: سننتقل بعد ذلك إلى سهولة الوصول Accessibility وتوفير معلومات حول مشاكلها الشائعة، وكيفية إجراء اختبار بسيط والاستفادة من أدوات التدقيق والاختبارات الآلية للعثور على مشاكل سهولة الوصول. كيفية اكتشاف دعم المتصفحات للميزات أثناء اختبار مشاريع الويب: يتضمن اكتشاف الميزات معرفة ما إذا كان المتصفح يدعم كتلةً معينةً من الشيفرة البرمجية، ويعتمد تشغيل شيفرة برمجية على كونها مدعومة أم لا، بحيث يمكن للمتصفح دائمًا توفير تجربة عمل ناجحة بدلًا من التعطل أو ظهور الأخطاء في بعض المتصفحات، إذ يوضح هذا المقال بالتفصيل كيفية كتابة اكتشاف المتصفحات للميزات البسيطة وكيفية استخدام مكتبة لتسريع التطبيق واستخدام الميزات الأصيلة Native لاكتشاف الميزات مثل الميزة ‎@supports. مدخل إلى اختبارات مشاريع الويب الآلية للتوافق مع المتصفحات: يمكن أن يصبح إجراء الاختبارات يدويًا على العديد من المتصفحات والأجهزة عدة مرات في اليوم أمرًا مملًا ويستغرق وقتًا طويلًا، لذا يمكن معالجة ذلك بكفاءة من خلال استخدام الأدوات الآلية، إذ سنلقي نظرةً في هذا المقال على الأدوات المتاحة وكيفية استخدام مشغّلي المهام وأساسيات كيفية استخدام تطبيقات الاختبارات الآلية للمتصفحات التجارية مثل Sauce Labs و Browser Stack. إعداد بيئتك للاختبارات الآلية في مشاريع الويب للتوافق مع المتصفحات: سنعلِّمك في هذا المقال كيفية تثبيت بيئة الاختبار الآلية وإجراء اختباراتك الخاصة باستخدام بيئة Selenium/WebDriver ومكتبة الاختبارات مثل المكتبة selenium-webdriver الخاصة ببيئة Node، كما سنتعرّف على كيفية دمج بيئة اختبارك المحلية مع التطبيقات التجارية. لنبدأ بمقالنا الأول ولنتعرف على مفهوم اختبار مشاريع الويب للتوافق مع المتصفحات Cross Browser Testing. ما هو اختبار التوافق مع المتصفحات؟ يُستخدَم اختبار التوافق مع المتصفحات للتأكد من أنّ مواقع وتطبيقات الويب التي تنشئها تعمل بنجاح على عدد مقبول من متصفحات الويب، إذ تقع على عاتقك -بصفتك مطور ويب- مسؤولية التأكد من أنّ مشاريعك لا تعمل فقط، ولكنها تعمل مع جميع المستخدِمين بغض النظر عن المتصفح أو الجهاز أو الأدوات المساعدة الإضافية التي يستخدمونها، إذ عليك التفكير فيما يلي: متصفحات مختلفة غير المتصفح أو المتصفحَين اللذين تستخدِمهما بانتظام على أجهزتك بما في ذلك المتصفحات الأقدم قليلًا التي يُحتمَل أنه لا يزال بعض الأشخاص يستخدِمونها والتي لا تدعم أحدث ميزات CSS وجافاسكربت. أجهزة مختلفة ذات قدرات مختلفة من أحدث الحواسيب إلى الأجهزة اللوحية والهواتف الذكية وأجهزة التلفاز الذكية وصولًا إلى الأجهزة اللوحية الرخيصة وحتى الهواتف ذات الميزات القديمة التي يمكن أن تشغّل متصفحات ذات إمكانات محدودة. الأشخاص ذوو الاحتياجات الخاصة الذين يستخدِمون الويب بمساعدة التقنيات المساعدة مثل قارئات الشاشة أو الأشخاص الذين لا يستخدِمون الفأرة، إذ يستخدِم بعض الأشخاص لوحة المفاتيح فقط. تذكَّر دائمًا أنك لست أحد المستخدِمين، إذ لا يعني عمل موقعك بنجاح على جهاز MacBook Pro أو جهاز Galaxy Nexus المتطور أنه سيعمل مع جميع المستخدِمين، فهناك الكثير من الاختبارات التي يجب إجراؤها. إذا تحدّثنا عن المواقع الإلكترونية التي تعمل على متصفحات مختلفة، فهذا يعني أنه يجب أن توفر تجربة مستخدِم مقبولة على متصفحات مختلفة، وقد يُحتمَل ألّا يقدم الموقع التجربة نفسها بالضبط على جميع المتصفحات طالما أن الوظيفة الأساسية يمكن الوصول إليها بطريقة ما، إذ يمكن أن تحصل في المتصفحات الحديثة على شيء متحرك وثلاثي الأبعاد وحديث، في حين يمكن أن تحصل في المتصفحات القديمة على رسم مسطح يمثل المعلومات نفسها، وطالما أنّ صاحب الموقع سعيد بذلك، فهذا يعني أنك نجحت في عملك. من ناحية أخرى، ليس مقبولًا أن يعمل الموقع بنجاح مع المستخدِمين المبصرين مع تعذر الوصول إليه بالنسبة للمستخدِمين ضعاف البصر لأن تطبيق قارئ الشاشة لا يمكنه قراءة أيٍّ من المعلومات المخزنة عليه. لا نعني بعبارة "عبر عدد مقبول من متصفحات الويب" نسبةَ 100% من المتصفحات في العالم، فهذا مستحيل تقريبًا، إذ يمكنك إجراء بحث بشأن المتصفحات والأجهزة التي سيستخدِمها مستخدمو موقعك كما سنناقش في المقال التالي من هذه السلسلة، ولكن لا يمكنك ضمان كل شيء، وتحتاج -بصفتك مطور ويب- الموافقة على مجموعة من المتصفحات والأجهزة التي تحتاجها الشيفرة البرمجية للعمل عليها مع مالك الموقع، ولكنك ستحتاج بعد ذلك لاستخدام البرمجة الدفاعية Defensive Coding لمنح المتصفحات الأخرى أفضل فرصة ممكنة لتكون قادرةً على استخدام محتواك، ويُعَدّ ذلك أحد أكبر التحديات التي تواجه تطوير الويب. سبب حدوث مشاكل التوافق مع المتصفحات هناك العديد من الأسباب المختلفة لحدوث مشاكل التوافق مع المتصفحات، ولاحظ أننا هنا نتحدث عن المشاكل التي تتصرف فيها الأشياء بطريقة مختلفة على مختلف المتصفحات أو الأجهزة أو تفضيلات التصفح، ولكن يجب عليك إصلاح الأخطاء الموجودة في شيفرتك البرمجية قبل أن تحل مشاكل التوافق مع المتصفحات. تحدث مشاكل التوافق مع المتصفحات للأسباب التالية: تحتوي المتصفحات في بعض الأحيان على أخطاء أو يمكن أن تطبّق ميزات بطريقة مختلفة، ولكن يُعَدّ هذا الوضع أقل سوءًا بكثير مما كان عليه في السابق، إذ عمدت شركات المتصفحات إلى تطبيق الأشياء بطريقة مختلفة عن بعضها البعض في محاولة لاكتساب ميزة تنافسية عندما تنافسَ كل من IE4 و Netscape 4 على موقع المتصفح المهمين في التسعينات، مما صعّب الأمور كثيرًا على المطورين، وقد أصبحت المتصفحات أفضل بكثير في اتباع المعايير حاليًا، ولكن لا يخلو الأمر من بعض الاختلافات والأخطاء. يمكن أن تحتوي بعض المتصفحات على مستويات مختلفة من الدعم للميزات التقنية عن غيرها من المتصفحات، وهذا أمر لا مفر منه عندما تتعامل مع ميزات متطورة بدأت المتصفحات في تطبيقها، أو إذا كان عليك دعم المتصفحات القديمة التي لم تَعُدّ قيد التطوير وجُمِّدت، أي لم يُطبَّق عليها أيّ عمل جديد قبل وقت طويل من اختراع ميزة جديدة، فإذا أردت مثلًا استخدام أحدث ميزات جافاسكربت في موقعك، فيمكن ألّا تعمل في المتصفحات القديمة، وإذا كنت بحاجة إلى دعم المتصفحات القديمة، فيمكن أن تضطر إلى عدم استخدام هذه الميزات أو تحويل شيفرتك البرمجية إلى صيغة قديمة الطراز باستخدام نوع من المُصرِّفات Compilers عند الحاجة. يمكن أن تحتوي بعض الأجهزة على قيود تؤدي إلى بطء تشغيل موقع الويب أو عرضه بطريقة سيئة، فإذا صُمِّم موقع ما ليبدو جميلًا على حاسوب مكتبي مثلًا، فيُحتمَل أن يبدو صغيرًا ويصعب قراءته على هاتف محمول، وإذا احتوى موقعك على عدد كبير من الرسوم المتحركة الكبيرة، فيمكن أن يكون الأمر جيدًا على جهاز لوحي عالي المواصفات، ولكنه سيكون بطيئًا على جهاز منخفض المواصفات. هناك المزيد من الأسباب الأخرى، وسنتعرّف في المقالات اللاحقة على مشاكل التوافق مع المتصفحات للوصول إلى حلول لها. سير عمل اختبار التوافق مع المتصفحات يمكن أن يبدو هذا الاختبار مستهلِكًا للوقت ومخيفًا، ولكن لا داعي لأن يكون كذلك، فكل ما عليك فعله هو التخطيط له بعناية والتأكد من إجراء اختبارات كافية في الأماكن الصحيحة للتأكد من عدم ظهور مشاكل غير متوقعة، فإذا أردت العمل في مشروع كبير، فيجب اختباره بانتظام للتأكد من أنّ الميزات الجديدة تعمل بنجاح مع جمهورك المستهدَف وأن الإضافات الجديدة إلى الشيفرة البرمجية لا تعطِّل الميزات القديمة التي كانت عاملةً في السابق، فإذا تركت جميع الاختبارات إلى نهاية المشروع، فستكون أيّ أخطاء أو زلات برمجية تكتشفها أكثر تكلفةً وستستغرق وقتًا طويلًا لإصلاحها مما لو اكتشفتها وأصلحتها أثناء العمل على المشروع. يمكن تقسيم سير عمل الاختبار وإصلاح الزلات البرمجية في المشروع إلى المراحل الأربع التالية تقريبًا، وهذا تقسيم تقريبي فقط، إذ يمكن أن يعمل أشخاص آخرون بطريقة مختلفة: التخطيط الأولي Initial Planning. التطوير Development. الاختبار Testing/الاكتشاف Discovery الإصلاحات Fixes/التكرار Iteration تُكرَّر الخطوات 2 و3 و4 عدة مرات حسب الضرورة لإنجاز التنفيذ بالكامل، كما سنتعرّف على الأجزاء المختلفة من عملية الاختبار بمزيد من التفصيل في المقالات اللاحقة، ولكن لنلخّص فقط ما يحدث في كل خطوة حاليًا. التخطيط الأولي يُحتمَل أن يكون لديك في مرحلة التخطيط الأولي العديد من اجتماعات التخطيط مع مالك الموقع أو العميل الذي يمكن أن يكون رئيسك في العمل أو شخصًا من شركة خارجية تبني موقع ويب لها، حيث تحدد بالضبط ما يجب أن يكون عليه موقع الويب، وما هو المحتوى والوظائف التي يجب أن يحتوي عليها وكيف يجب أن يبدو وما إلى ذلك، كما سترغب في هذه المرحلة في معرفة مقدار الوقت المتاح لك لتطوير الموقع أي الموعد النهائي Deadline، وكم سيدفعون لك مقابل عملك، ولن نخوض في الكثير من التفاصيل حول هذا الأمر، ولكن يمكن أن يكون لمشاكل التوافق مع المتصفحات تأثير خطير على هذا التخطيط. يجب أن تبدأ في استكشاف الجمهور المستهدَف بمجرد حصولك على فكرة عن مجموعة الميزات المطلوبة والتقنيات التي ستبني هذه الميزات بها، مثل استكشاف المتصفحات والأجهزة وغير ذلك والتي سيستخدِمها الجمهور المستهدَف لهذا الموقع، فيمكن أن يكون لدى العميل بيانات حول هذه الأمور من بحث سابق أجراه من مواقع الويب الأخرى التي يمتلكها أو من الإصدارات السابقة من موقع الويب الذي تعمل عليه الآن مثلًا، فإذا لم يكن الأمر كذلك، فستتمكن من الحصول على فكرة جيدة من خلال النظر إلى مصادر أخرى مثل إحصائيات الاستخدام للمنافسين أو البلدان التي سيخدّمها الموقع، كما يمكنك استخدام بعض التخمين. يمكن أن تنشئ مثلًا موقعًا للتجارة الإلكترونية يخدّم العملاء في أمريكا الشمالية، ويجب أن يعمل الموقع بالكامل في الإصدارات الأخيرة من متصفحات الحواسيب المكتبية والهاتف المحمول (iOS و Android و Windows phone) الأكثر شيوعًا، إذ يجب أن يشمل ذلك كروم Chrome وأوبرا Opera الذي يعتمد على محرّك التصيير Rendering نفسه الخاص بكروم، بالإضافة إلى فايرفوكس Firefox و IE/Edge وسفاري Safari، كما يجب أن توفر تجربة مقبولة في الإصدارين IE 8 و9 ويمكن الوصول إليها باتباع تعليمات WCAG AA. أصبحتَ الآن تعرف منصات الاختبار المستهدفة، لذا يجب العودة ومراجعة مجموعة الميزات المطلوبة والتقنيات التي ستستخدِمها. إذا أراد مالك موقع التجارة الإلكترونية مثلًا جولةً ثلاثية الأبعاد مدعومة من WebGL لكل منتج مُضمَّن في صفحات المنتجات، فيجب قبول أنّ ذلك لن يعمل في إصدارات المتصفح IE قبل الإصدار 11، كما يجب أن توافق على توفير نسخة من الموقع بدون هذه الميزة لمستخدِمي إصدارات IE الأقدم، كما يجب عليك تجميع قائمة بمواقع المشاكل المحتمَلة. ملاحظة: يمكنك الرجوع إلى موقع caniuse.com للحصول على مزيد من تفاصيل معلومات دعم المتصفحات للتقنيات. يمكنك المضي قدمًا والبدء في تطوير الموقع بمجرد الاتفاق على هذه التفاصيل. التطوير لننتقل الآن إلى تطوير الموقع، إذ يجب عليك تقسيم الأجزاء المختلفة من عملية التطوير إلى وحدات، حيث يمكنك تقسيم مناطق الموقع المختلفة إلى الصفحة الرئيسية وصفحة المنتجات وعربة التسوق وسير عملية الدفع مثلًا، ثم يمكنك تقسيم هذه الأجزاء إلى تطبيق جزأي الترويسة والتذييل المشتركَين في الموقع، وتطبيق عرض تفاصيل صفحة المنتجات، وعنصر عربة التسوق الدائم وغير ذلك. هناك العديد من الاستراتيجيات العامة لتطوير التوافق مع المتصفحات مثل: الحصول على جميع الوظائف التي تعمل بنجاح قدر الإمكان في جميع المتصفحات المستهدفة، ويمكن أن يتضمن ذلك كتابة مسارات شيفرات برمجية مختلفة تعيد إنتاج الوظائف بطرق مختلفة تستهدف متصفحات مختلفة، أو استخدام تعويض دعم المتصفحات Polyfill لتقليد أيّ دعم مفقود باستخدام جافاسكربت أو تقنيات أخرى، أو استخدام مكتبة تسمح لك بكتابة جزء من الشيفرة البرمجية ثم تطبيق أشياء مختلفة في الخلفية اعتمادًا على ما يدعمه المتصفح. القبول بأن بعض الأشياء لن تعمل بالطريقة نفسها على جميع المتصفحات، وتقديم حلول مختلفة مقبولة في المتصفحات التي لا تدعم الوظائف الكاملة، ويكون ذلك في بعض الأحيان أمرًا لا مفر منه بسبب قيود الجهاز، إذ لن تقدِّم شاشة السينما العريضة التجربة المرئية نفسها لشاشة الجوال التي مقاسها 4 بوصات بغض النظر عن كيفية برمجة موقعك. القبول بأن موقعك لن يعمل في بعض المتصفحات القديمة، ثم المضي قدمًا في عملية التطوير، ويُعَدّ ذلك جيدًا بشرط عدم وجود مشكلة لدى عميلك أو قاعدة المستخدِمين في ذلك. تتضمن عملية التطوير مزيجًا من الأساليب الثلاثة السابقة، ولكن أهم شيء هو أن تختبر كل جزء صغير قبل تنفيذه وألّا تترك جميع الاختبارات حتى النهاية. الاختبار/الاكتشاف يجب اختبار الوظيفة الجديدة بعد كل مرحلة تطبيق، إذ يجب عليك أولًا التأكد من عدم وجود مشاكل عامة في شيفرتك البرمجية تعطل عمل الميزة كما يلي: اختبرها في بعض المتصفحات المستقرة على نظامك مثل Firefox أو Safari أو Chrome أو IE/Edge. أجرِ اختبارات سهولة الوصول Lo-fi مثل محاولة استخدام موقعك باستخدام لوحة المفاتيح فقط أو استخدام موقعك عبر قارئ الشاشة لمعرفة ما إذا كان قابلًا للتنقل عبره أم لا. أجرِ اختبارات على منصة للهاتف المحمول مثل Android أو iOS. أصلِح أيّ مشاكل تجدها في شيفرتك البرمجية الجديدة في هذه المرحلة، ثم يجب أن تحاول توسيع قائمة المتصفحات التي تريد اختبارها إلى قائمة كاملة من متصفحات الجمهور المستهدف والبدء في التركيز على التخلص من مشاكل التوافق مع المتصفحات، فمثلًا: حاول اختبار أحدث تغيير على جميع متصفحات الحواسيب المكتبية الحديثة بما في ذلك Firefox و Chrome و Opera و IE و Edge وSafari على أنظمة التشغيل Mac و Windows و Linux. اختبرها في متصفحات الهاتف المحمول والأجهزة اللوحية الشائعة مثل iOS Safari على أجهزة iPhone/iPad و Chrome و Firefox على أجهزة iPhone/iPad/Android. أجرِ اختبارات أيضًا على أيّ متصفحات أخرى ضمّنتها في قائمة أهدافك. خيار اختبار Lo-fi الأكثر استخدامًا هو إجراء جميع الاختبارات التي يمكنك تطبيقها بنفسك مثل أن يجرّب زملاؤك في الفريق الموقع إذا كنت تعمل في فريق، إذ يجب أن تحاول اختباره على أجهزة حقيقية إذا أمكن ذلك، فإذا لم تكن لديك الوسائل اللازمة لاختبار جميع مجموعات المتصفحات وأنظمة التشغيل والأجهزة المختلفة على العتاد الفيزيائي، فيمكنك الاستفادة من المقلّدات أو المحاكيات Emulators -التي تقلد جهازًا باستخدام برنامج على حاسوبك المكتبي- والآلات الافتراضية التي هي برامج تسمح بمحاكاة مجموعات متعددة من أنظمة التشغيل والبرامج على حاسوبك. يُعَدّ ذلك خيارًا شائعًا جدًا خاصةً في ظروف معينة مثل ألّا يتيح نظام ويندوز تثبيت إصدارات متعددة منه في الوقت ذاته وعلى الجهاز نفسه، لذا يكون استخدام آلات افتراضية متعددة هو الخيار الوحيد في هذه الحالة. يوجد خيار آخر هو استخدام مجموعات المستخدِمين، أي استخدام مجموعة من الأشخاص من خارج فريقك لاختبار موقعك مثل مجموعة من الأصدقاء أو العائلة أو مجموعة من الموظفين الآخرين أو صف دراسي في جامعة محلية أو إعداد اختبار احترافي بحيث يُدفَع للأشخاص لاختبار موقعك وتقديم النتائج. أخيرًا، يمكنك استخدام الاختبار باستخدام أدوات التدقيق أو الاختبارات الآلية، حيث يُعَدّ هذا الاختيار معقولًا مع المشاريع الكبيرة، إذ يمكن أن يستغرق إجراء هذا الاختبار يدويًا وقتًا طويلًا جدًا، كما يمكنك إعداد نظام اختبار آلي مثل تطبيق Selenium الشهير الذي يمكنه تحميل موقعك في عدد من المتصفحات المختلفة مثلًا، ويمكنه: معرفة ما إذا كان النقر على الزر يؤدي إلى حدوث شيء ما بنجاح مثل عرض خريطة، وعرض النتائج بمجرد اكتمال الاختبارات. التقاط لقطة شاشة لكل من هذه الاختبارات، مما يسمح لك بمعرفة ما إذا كان تخطيط الموقع متناقسًا على المتصفحات المختلفة. هناك أدوات تجارية متاحة مثل Browserling و Sauce Labs و Browser Stack و Endtest و LambdaTest و TestingBot و CrossBrowserTesting التي تطبّق هذه الاختبارات دون الحاجة إلى القلق بشأن الإعداد إذا أردت استثمار بعض المال في الاختبارات، كما يمكنك إعداد بيئة تشغّل الاختبارات تلقائيًا نيابةً عنك ثم تتيح لك فقط التحقق من التغييرات التي أجريتها على مستودع الشيفرة المركزي إذا نجحت الاختبارات باستمرار. الاختبار على المتصفحات التجريبية يكون اختبار الإصدارات التجريبية من المتصفحات فكرة جيدة في أغلب الأحيان، لذا اطّلع على الروابط التالية: Firefox Developer Edition. Edge Insider Preview. Safari Technology Preview. Chrome Canary. Opera Developer. ينتشر هذا النوع من الاختبارات خاصةً إذا استخدمتَ تقنيات حديثة جدًا في موقعك ورغبت في اختبار أحدث التطبيقات؛ أو إذا صادفت زلةً برمجيةً في أحدث إصدار من المتصفح وأردت معرفة إذا أصلح مطورو المتصفح هذه الزلة البرمجية في إصدار أحدث. الإصلاحات/التكرار يجب محاولة إصلاح الزلة البرمجية بمجرد اكتشافها، فأول شيء يجب فعله هو تضييق نطاق مكان حدوث الزلة البرمجية قدر الإمكان، لذا احصل على أكبر قدر ممكن من المعلومات من الشخص الذي يبلّغ عن الزلة البرمجية مثل معرفة ما هي المنصة أو المنصات، والجهاز أو الأجهزة، وإصدار أو إصدارات المتصفح التي يستخدمها، وجرب هذا الاختبار على إعدادات مماثلة مثل إصدار المتصفح نفسه على منصات حواسيب مختلفة أو عدة إصدارات مختلفة من المتصفح نفسه على المنصة نفسها لمعرفة مدى انتشار الزلة البرمجبة. يمكن ألّا يكون هذا خطأك، لذا نأمل إصلاح الزلة البرمجية بسرعة في حالة وجودها في المتصفح، كما يمكن أن تكون الزلة البرمجية قد أُصلِحت مسبقًا مثل وجود خطأ في الإصدار 49 من Firefox، ولكنها لم تَعُد موجودةً في Firefox Nightly (الإصدار 52)، فعندئذٍ تكون قد أُصلِحت فعليًا، فإذا لم تكون قد أُصلِحت مسبقًا، فيمكنك إرسال تقرير بالزلة البرمجية. إذا كان ذلك خطأك، فيجب عليك إصلاحه، إذ يتضمن اكتشاف سبب الزلة البرمجية الإستراتيجية نفسها لأي زلة برمجية في عملية تطوير الويب، فإذا اكتشفتَ سبب الزلة البرمجية، فيجب أن تقرر كيفية التعامل معها في المتصفح التي تتسبب في حدوث مشاكل، ولا يمكنك تعديل شيفرة المشكلة البرمجية فقط، إذ يمكن أن يؤدي ذلك إلى تعطّل الشيفرة في المتصفحات الأخرى، والأسلوب العام في هذه الحالة هو تفرّع الشيفرة البرمجية بطريقةٍ ما مثل استخدام شيفرة اكتشاف المتصفحات لميزة جافاسكربت لاكتشاف المواقف التي لا تعمل فيها الميزة التي تؤدي إلى المشكلة، وتشغيل الشيفرة البرمجية الأخرى في الحالات التي تعمل فيها هذه الميزة. يجب تكرار عملية الاختبار بعد إصلاح المشكلة للتأكد من أنّ الإصلاح يعمل بنجاح ولا يتسبب في تعطل الموقع في أماكن أخرى أو في متصفحات أخرى. إصدار تقارير بالأخطاء إذا اكتشفت أخطاءً في المتصفحات، فيجب عليك التبليغ عنها باستخدام ما يلي: Firefox Bugzilla. متعقّب مشاكل EdgeHTML. Safari. Chrome. Opera. الخلاصة يُفترَض أن يمنحك هذا المقال فهمًا عالي المستوى لأهم المفاهيم التي تحتاج معرفتها حول اختبار التوافق مع المتصفحات، وبالتالي أصبحت الآن جاهزًا للمضي قدمًا والبدء في التعرف على استراتيجيات اختبار التوافق مع المتصفحات. ترجمة -وبتصرُّف- للمقالين Cross browser testing وIntroduction to cross browser testing. اقرأ أيضًا المقال السابق: إضافة خاصية ترشيح لتطبيق Angular وتجهيزه للنشر المدخل الشامل لتعلم تطوير الويب وبرمجة المواقع فهم أدوات تطوير الويب من طرف العميل
  2. تُعَد إدارة ضبط الخادم (يُشار إليها أيضًا باسم أتمتة تقانة المعلومات IT Automation) حلًا لتحويل إدارة بنيتك التحتية إلى الشيفرة البرمجية الأساسية، ولوصف جميع العمليات اللازمة لنشر خادم في مجموعة من سكربتات الإعداد المسبق Provisioning Scripts التي يمكن إصدارها وإعادة استخدامها بسهولة، ويمكنها تحسين التكامل لأيّ بنية خادم تحتية بصورة كبيرة بمرور الوقت. تحدثنا في مقال سابق عن الفوائد الرئيسية لتنفيذ إستراتيجية إدارة الضبط لبنية الخادم التحتية، وكيفية عمل أدوات إدارة الضبط والعناصر المشتركة بين هذه الأدوات. سنوضح في هذا المقال عملية أتمتة إعداد الخادم المسبق باستخدام الأداة Puppet، وهي أداة شائعة لإدارة الضبط قادرة على إدارة البنية التحتية المعقدة بطريقة شفافة باستخدام خادم رئيسي Master لتنسيق ضبط العقد، وسنركز على مصطلحات اللغة والصياغة والميزات اللازمة لإنشاء مثال مبسط للأتمتة الكاملة لنشر خادم ويب Ubuntu 18.04 باستخدام أباتشي Apache. تحتوي القائمة التالية على جميع الخطوات التي نحتاجها للأتمتة حتى الوصول إلى هدفنا: حدّث ذاكرة apt المخبئية. ثبّت خادم أباتشي Apache. أنشئ مجلد المستند الجذر المُخصَّص. ضع ملف index.html في المستند الجذر المخصص. طبّق قالبًا لإعداد المضيف الوهمي المخصص. أعِد تشغيل أباتشي. سنبدأ بإلقاء نظرة على المصطلحات التي تستخدمها أداة Puppet، ثم سنتعرّف على ميزات اللغة الرئيسية التي يمكن استخدامها لكتابة ملفات البيان Manifests، وسنشاركك في النهاية المثال الكامل لتتمكّن من تجربته بنفسك. ملاحظة: يهدف هذا المقال إلى تعريفك بلغة Puppet وكيفية كتابة ملفات البيان لأتمتة إعداد خادمك المسبق. اطّلع على تثبيت الأداة Puppet لإدارة بنية الخوادم التحتية لمعرفة الخطوات اللازمة لتثبيت أداة Puppet والبدء باستخدامها. هذا المقال جزء من سلسلة حول إدارة ضبط الخوادم، وإليك روابط فصول السلسلة: مدخل إلى إدارة ضبط الخوادم Configuration Management مبادئ إدارة ضبط الخوادم Configuration Management: كتابة دليل تشغيل الأداة Ansible مبادئ إدارة ضبط الخوادم Configuration Management: كتابة ملفات البيان Manifests للأداة Puppet مبادئ إدارة ضبط الخوادم Configuration Management: كتابة الوصفات Recipes في الأداة Chef البدء باستخدام الأداة Puppet يجب أن نتعرف أولًا على المصطلحات والمفاهيم المهمة التي قدمتها أداة Puppet قبل البدء بالعمل. مصطلحات Puppet تحتوي القائمة التالية على أهم المصطلحات التي تستخدمها أداة Puppet: جهاز Puppet الرئيسي Master: الخادم الرئيسي الذي يتحكم في الضبط على العقد. عقدة الوكيل الخاصة بالأداة Puppet: عقدة يتحكم فيها جهاز Puppet الرئيسي. ملف البيان Manifest: ملف يحتوي على مجموعة من التعليمات لتنفيذها. المورد Resource: جزء الشيفرة البرمجية الذي يصرّح عن عنصر في النظام وكيفية تغيير حالته، فمثلًا يجب لتثبيت حزمة تحديدُ المورد package والتأكد من ضبط حالته على أنه "مُثبَّت". الوحدة Module: مجموعة من ملفات البيان والملفات الأخرى ذات الصلة المنظمة بطريقة مُعرَّفة مسبقًا لتسهيل مشاركة وإعادة استخدام أجزاء من الإعداد المسبق. الصنف Class: تُستخدَم الأصناف في الأداة Puppet لتنظيم الإعداد المسبق بصورة أفضل وتسهيل إعادة استخدام أجزاء من الشيفرة البرمجية كما هو الحال مع لغات البرمجة العادية. الحقائق Facts: المتغيرات العامة التي تحتوي على معلومات حول النظام مثل واجهات الشبكة ونظام التشغيل. الخدمات Services: تُستخدَم لبدء تغييرات حالة الخدمة مثل إعادة تشغيل الخدمة أو إيقافها. تُكتَب عمليات الإعداد المسبق الخاصة بالأداة Puppet باستخدام لغة المجال المحدَّد Domain Specific Language -أو DSL اختصارًا- المخصَّصة والتي تعتمد على لغة روبي Ruby. الموارد Resources تُعرَّف المهام أو الخطوات باستخدام الأداة Puppet من خلال التصريح عن الموارد، ويمكن أن تمثل الموارد الحزم والملفات والخدمات والمستخدمين والأوامر، ويكون لها حالة تؤدي إلى بدء تغيير النظام في حالة اختلاف حالة المورد المُصرَّح عنه عن حالته الحالية في النظام، فمثلًا سيؤدي ضبط المورد package على القيمة installed في ملف البيان إلى بدء تثبيت الحزمة على النظام إن لم مُثبَّتةً مسبقًا. يكون مورد الحزمة package كما يلي: package { 'nginx': ensure => 'installed' } يمكنك تنفيذ أيّ أمر عشوائي من خلال التصريح عن المورد exec كما يلي: exec { 'apt-get update': command => '/usr/bin/apt-get update' } لاحظ أن الجزء apt-get update في السطر الأول ليس التصريح عن الأمر الفعلي، بل هو معرِّفٌ لهذا المورد الفريد، إذ يجب في أغلب الأحيان الإشارة إلى موارد أخرى من موردٍ ما، ونستخدم معرّفها لذلك، حيث يكون المعرّف في حالتنا هو apt-get update، ولكن يمكن أن يكون أيّ سلسلة نصية أخرى. اعتمادية الموارد Resource Dependency يجب أن تضع في بالك أن الأداة Puppet لا تقيّم الموارد بترتيب تعريفها نفسه عند كتابة ملفات البيان، ويُعَد ذلك مصدر ارتباك شائع للأشخاص المبتدئين في استخدام الأداة Puppet، إذ يجب أن تعرِّف الموارد اعتماديتها بين بعضها البعض صراحةً، وإلّا فلن يكون هناك ضمان بشأن المورد الذي يجب تقييمه وتنفيذه أولًا. لنفترض أنك تريد تنفيذ أمرٍ ما، ولكن يجب التأكد من تثبيت الاعتمادية أولًا كما يلي: package { 'python-software-properties': ensure => 'installed' } exec { 'add-repository': command => '/usr/bin/add-apt-repository ppa:ondrej/php5 -y' require => Package['python-software-properties'] } يأخذ الخيار require مرجعًا لمورد آخر بوصفه وسيطًا له، ونشير في هذه الحالة إلى مورد الحزمة Package المُعرَّف بوصفه python-software-properties. لاحظ أننا نستخدم exec و package وغير ذلك للتصريح عن الموارد (بأحرف صغيرة)، بينما نستخدم Exec و Package وغير ذلك (بأحرف كبيرة) عند الإشارة إلى الموارد المُعرَّفة مسبقًا. لنفترض الآن أنه يجب التأكد من تنفيذ مهمةٍ قبل مهمة أخرى، حيث يمكننا استخدام الخيار before في هذه الحالة كما يلي: package { 'curl': ensure => 'installed' before => Exec['install script'] } exec { 'install script': command => '/usr/bin/curl http://example.com/some-script.sh' تنسيق ملف البيان تُعَد ملفات البيان مجموعة من تصريحات عن الموارد باستخدام الامتداد ‎.pp. يمكنك العثور فيما يلي على مثال لدليل تشغيل Playbook بسيط يؤدي مهمتين هما: تحديث ذاكرة apt المخبئية ثم تثبيت vim: exec { 'apt-get update': command => '/usr/bin/apt-get update' } package { 'vim': ensure => 'installed' require => Exec['apt-get update'] } سنرى لاحقًا مثالًا واقعيًا أكثر عن ملف البيان بالتفصيل، ولكن سيعطيك القسم التالي نظرة عامة على أهم العناصر والميزات التي يمكن استخدامها لكتابة ملفات بيان Puppet. كتابة ملفات البيان أصبحت الآن على دراية بالمصطلحات الأساسية والتنسيق العام لملف البيان في Puppet، لذا سنتعرف على بعض ميزات ملف البيان. التعامل مع المتغيرات يمكن تعريف المتغيرات في أي وقت في ملف البيان، وأكثر أنواع المتغيرات شيوعًا هي السلاسل النصية ومصفوفات السلاسل النصية، والأنواع الأخرى مدعومة أيضًا مثل القيم المنطقية والقيم المُعمَّاة المختصرة Hashes. يعرّف المثال التالي متغير سلسلة نصية يُستخدَم لاحقًا ضمن موردٍ ما: $package = "vim" package { $package: ensure => "installed" } استخدام الحلقات Loops تُستخدم الحلقات عادةً لتكرار مهمة باستخدام قيم دخل مختلفة، فمثلًا يمكنك إنشاء مهمة واحدة واستخدام حلقة لتكرار المهمة مع جميع الحزم المختلفة التي تريد تثبيتها بدلًا من إنشاء 10 مهام لتثبيت 10 حزم مختلفة. أبسط طريقة لتكرار مهمة بقيم مختلفة في Puppet هي باستخدام المصفوفات كما في المثال التالي: $packages = ['vim', 'git', 'curl'] package { $packages: ensure => "installed" } تدعم الأداة Puppet من الإصدار 4 طرقًا إضافية لتكرار المهام، فمثلًا يطبّق المثال التالي الشيء نفسه الذي طبّقه المثال السابق، ولكن باستخدام المكرِّر each، حيث يمنحك هذا الخيار مزيدًا من المرونة لتكرار تعريفات الموارد: $packages.each |String $package| { package { $package: ensure => "installed" } } استخدام التعليمات الشرطية يمكن استخدام التعليمات الشرطية لتحديد ما إذا كان يجب تنفيذ كتلة من الشيفرة البرمجية أم لا ديناميكيًا بناءً على متغير أو خرج أمرٍ ما. تدعم الأداة Puppet معظم البنى الشرطية التي يمكنك العثور عليها باستخدام لغات البرمجة التقليدية مثل تعليمات if/else و case، وتدعم بعض الموارد مثل exec السمات التي تعمل بوصفها تعليمة شرطية، ولكنها تقبل فقط خرج الأمر بوصفه شرطًا. لنفترض أنك تريد تنفيذ أمر بناءً على حقيقة Fact، إذ يجب في هذه الحالة استخدام أحد البنى الشرطية المدعومة مثل if/else عندما تريد اختبار قيمة متغير كما يلي: if $osfamily != 'Debian' { warning('This manifest is not supported on this OS.') } else { notify { 'Good to go!': } } يوجد موقف شائع آخر عندما تريد وضع شرط لتنفيذ أمرٍ ما بناءً على خرج أمرٍ آخر، حيث يمكنك في هذه الحالة استخدام التعليمة onlyif أو unless كما في المثال الآتي، إذ لن يُنفَّذ هذا الأمر إلّا عندما يكون خرج ‎/bin/which php ناجحًا، أي أنّ الأمر ينتهي مع الحالة 0: exec { "Test": command => "/bin/echo PHP is installed here > /tmp/test.txt", onlyif => "/bin/which php" } وبالمثل، سينفَّذ الأمر التعليمة unless في جميع الأوقات، إلّا عندما ينتهي الأمر الموجود ضمنها بنجاح: exec { "Test": command => "/bin/echo PHP is NOT installed here > /tmp/test.txt", unless => "/bin/which php" } التعامل مع القوالب تُستخدَم القوالب لإعداد ملفات الضبط، مما يسمح باستخدام المتغيرات والميزات الأخرى التي تهدف إلى جعل هذه الملفات أكثر تنوعًا وقابلية لإعادة الاستخدام. تدعم الأداة Puppet تنسيقين مختلفين للقوالب هما: قوالب Puppet المُضمَّنة Embedded Puppet -أو EPP اختصارًا- وقوالب روبي المُضمَّنة Embedded Ruby -أو ERB اختصارًا، ولكن يعمل تنسيق EPP فقط مع الإصدارات الحديثة من الأداة Puppet (بدءًا من الإصدار 4.0). يوجد فيما يلي مثال على قالب ERB لإعداد مضيف أباتشي Apache الوهمي باستخدام متغير لإعداد المستند الجذر لهذا المضيف: <VirtualHost *:80> ServerAdmin webmaster@localhost DocumentRoot <%= @doc_root %> <Directory <%= @doc_root %>> AllowOverride All Require all granted </Directory> </VirtualHost> يجب لتطبيق القالب إنشاء المورد file الذي يعرض محتوى القالب باستخدام التابع template، وإليك طريقة تطبيق هذا القالب لاستبدال مضيف أباتشي الوهمي الافتراضي: file { "/etc/apache2/sites-available/000-default.conf": ensure => "present", content => template("apache/vhost.erb") } تضع الأداة Puppet بعض الافتراضات عند التعامل مع الملفات المحلية لفرض التنظيم والتقسيم إلى وحدات Modularity، حيث ستبحث الأداة Puppet عن ملف قالب vhost.erb ضمن المجلد apache/templates في مجلد وحداتك. تعريف وبدء الخدمات تُستخدَم موارد الخدمة service للتأكد من تهيئة الخدمات وتفعيلها، وتُستخدَم لبدء إعادة تشغيل الخدمة. لنأخذ مثالنا السابق لاستخدام القالب، حيث ضبطنا مضيف أباتشي الوهمي. إذا أردتَ التأكد من إعادة تشغيل أباتشي بعد تغيير المضيف الوهمي، فيجب أولًا إنشاء مورد خدمة service لخدمة أباتشي. إليك طريقة تعريف هذا المورد في Puppet: service { 'apache2': ensure => running, enable => true } يجب الآن تضمين الخيار notify لبدء إعادة التشغيل عند تعريف المورد كما يلي: file { "/etc/apache2/sites-available/000-default.conf": ensure => "present", content => template("vhost.erb"), notify => Service['apache2'] } مثال عن ملف البيان لنلقِ الآن نظرة على ملف البيان الذي سيؤدي إلى أتمتة عملية تثبيت خادم ويب أباتشي على نظام لينكس أوبنتو Ubuntu كما ناقشنا سابقًا. يمكنك العثور على المثال الكامل بما في ذلك ملف القالب لإعداد أباتشي وملف HTML ليخدّمه خادم الويب على Github، ويحتوي المجلد على الملف Vagrantfile الذي يتيح لك اختبار ملف البيان في إعداد مبسط باستخدام آلة افتراضية تديرها الأداة Vagrant. إليك ملف البيان الكامل: $doc_root = "/var/www/example" exec { 'apt-get update': command => '/usr/bin/apt-get update' } package { 'apache2': ensure => "installed", require => Exec['apt-get update'] } file { $doc_root: ensure => "directory", owner => "www-data", group => "www-data", mode => 644 } file { "$doc_root/index.html": ensure => "present", source => "puppet:///modules/main/index.html", require => File[$doc_root] } file { "/etc/apache2/sites-available/000-default.conf": ensure => "present", content => template("main/vhost.erb"), notify => Service['apache2'], require => Package['apache2'] } service { 'apache2': ensure => running, enable => true } لنتعرّف على كل جزء من ملف البيان السابق بمزيد من التفصيل: يبدأ ملف البيان في السطر الأول بتعريف المتغير ‎$doc_root الذي يُستخدَم لاحقًا في تعريف المورد. ينفّذ المورد exec في الأسطر 3-5 الأمر apt-get update. يثبّت مورد الحزمة package في الأسطر 7-10 الحزمة apache2 محدّدًا أن مورد apt-get update مطلوب، مما يعني أنه لن يُنفَّذ إلا بعد تقييم المورد المطلوب. نستخدم مورد الملف file في الأسطر 12-17 لإنشاء مجلد جديد سيكون بمثابة المستند الجذر. يمكن استخدام المورد file لإنشاء مجلدات وملفات، ويُستخدَم لتطبيق القوالب ونسخ الملفات المحلية إلى الخادم البعيد، حيث يمكن تنفيذ هذه المهمة في أيّ مرحلة من الإعداد المسبق، لذلك لم نكن بحاجة إلى ضبط أيّ خيار require هنا. نستخدم مورد ملف file آخر في الأسطر 19-23 لنسخ ملف index.html المحلي إلى المستند الجذر ضمن الخادم، حيث نستخدم المعامل source للسماح لأداة Puppet بمعرفة مكان العثور على الملف الأصلي. تعتمد هذه التسمية على الطريقة التي تتعامل بها Puppet مع الملفات المحلية، فإذا ألقيت نظرة على مستودع المثال على Github، فسترى كيفية إنشاء بنية المجلد للسماح لأداة Puppet بالعثور على هذا المورد، ويجب إنشاء مجلد المستند الجذر قبل تنفيذ هذا المورد، لذا ضمّنا الخيار require الذي يشير إلى المورد السابق. يُستخدَم مورد ملف جديد في الأسطر 25-30 لتطبيق قالب أباتشي وإعلام الخدمة بإعادة التشغيل. نُظِّمت عملية الإعداد المسبق في وحدة تسمى main، لذا يكون مورد القالب هو main/vhost.erb. نستخدم التعليمة require للتأكد من أن مورد القالب template لا يُنفَّذ إلّا بعد تثبيت الحزمة apache2، وإلّا فلن تكون بنية المجلد التي يستخدمها أباتشي موجودة بعد.ص يصرّح مورد الخدمة service في الأسطر 32-35 عن الخدمة apache2، والتي نعلِمها بإعادة التشغيل من المورد الذي يطبّق قالب المضيف الوهمي. الخلاصة تُعَد الأداة Puppet أداة قوية لإدارة الضبط وتستخدم لغة DSL مُخصَّصة ومعبّرة لإدارة موارد الخادم وأتمتة المهام، وتقدّم لغتها مواردًا متقدمة يمكن أن توفّر مرونة إضافية لضبط إعدادك المسبق، إذ يجب أن تتذكر أن الموارد لا تُقيَّم بترتيب تعريفها نفسه، لذا يجب توخي الحذر عند تحديد الاعتماديات بين الموارد لإنشاء سلسلة التنفيذ الصحيحة. سنلقي نظرة على الأداة Chef في المقال التالي، وهي أداة قوية أخرى لإدارة الضبط وتستفيد من لغة برمجة روبي Ruby لأتمتة إدارة البنية التحتية والإعداد المسبق. ترجمة -وبتصرُّف- للمقال Configuration Management 101: Writing Puppet Manifests لصاحبته Erika Heidi. اقرأ أيضًا تنصيب Puppet لإدارة البنية التَّحتِيَّة للخواديم الإدارة عن بعد باستخدام Puppet على خواديم أوبنتو تنصيب Puppet لإدارة البنية التَّحتِيَّة للخواديم
  3. يحتوي البرنامج الذي يعمل في الذاكرة على مكونين رئيسيين هما: الشيفرة البرمجية Code المعروفة أيضًا باسم النص Text والبيانات Data. لا يبقى الملف القابل للتنفيذ في الذاكرة، ولكنه يقضي معظم وقته بوصفه ملفًا على القرص الصلب ينتظر تحميله عند التشغيل. يُعَد الملف مجرد مصفوفة متجاورة من البتات، لذا تبتكر جميع الأنظمة طرقًا لتنظيم الشيفرة البرمجية والبيانات ضمن الملفات للتنفيذ عند الطلب، حيث يشار إلى هذه الصيغة من الملفات باسم ملف ثنائي Binary أو ملف قابل للتنفيذ Executable، وتكون البتات والبايتات الخاصة بالملف بصيغة جاهزة لوضعها في الذاكرة وتفسيرها مباشرةً بواسطة عتاد المعالج. تمثيل الملفات القابلة للتنفيذ يجب أن تحدّد أيّ صيغة لملفٍ قابل للتنفيذ executable file مكانَ وجود الشيفرة البرمجية والبيانات في الملف الثنائي، حيث تُعَد الشيفرة البرمجية والبيانات القسمين الأساسيين لملفٍ قابل للتنفيذ، وأحد المكونات الإضافية التي لم نذكرها حتى الآن هو مساحة تخزين المتغيرات العامة غير المُهيَّأة uninitialised global variables. إذا صرّحنا عن متغير وأعطيناه قيمة أولية، فيجب تخزين هذه القيمة في ملف قابل للتنفيذ بحيث يمكن تهيئته بالقيمة الصحيحة عند بدء البرنامج، ولكن هناك العديد من المتغيرات غير المهيأة أو التي قيمتها صفر عند تنفيذ البرنامج لأول مرة. يُعَد حجز مساحة لهذه المتغيرات في الملف القابل للتنفيذ ثم تخزين قيم صفرية أو فارغة NULL هدرًا للمساحة، مما يؤدي إلى تضخّم حجم الملف القابل للتنفيذ على القرص الصلب دون داع لذلك. تُعرِّف معظم الصيغ الثنائية مفهوم القسم BSS الإضافي بوصفه حجمًا بديلًا للبيانات الصفرية غير المُهيَّأة. يمكن تخصيص الذاكرة الإضافية التي يحدّدها القسم BSS وضبطها على القيمة صفر عند تحميل البرنامج. يرمز الاختصار BSS إلى العبارة Block Started by Symbol، وهو أمر بلغة تجميع حاسوب IBM القديم، ولكن يُرجَّح أن الاشتقاق الدقيق له ضاع مع الوقت. الصيغة الثنائية Binary Format يُنشَأ الملف القابل بالتنفيذ باستخدام سلسلة أدوات من الشيفرة المصدرية، حيث يجب أن يكون هذا الملف بصيغة محددة وواضحة بحيث يمكن للمصرِّف إنشاؤه ويمكن لنظام التشغيل تحديده وتحميله في الذاكرة وتحويله إلى عملية مُشغَّلة يمكن لنظام التشغيل إدارتها. يمكن أن تكون هذه الصيغة من الملفات القابلة للتنفيذ خاصة بنظام التشغيل، إذ لا نتوقع تنفيذ برنامج مُصرَّف لنظامٍ ما على نظام آخر مثل أن تعمل برامج ويندوز على نظام لينكس أو أن تعمل برامج لينكس على نظام macOS. لكن خيط المعالجة Thread المشترك بين جميع صيغ الملفات القابلة للتنفيذ هو أنها تتضمن ترويسة معيارية مُعرَّفة مسبقًا توضّح كيفية تخزين شيفرة وبيانات البرنامج في بقية الملف، حيث يمكن أن تشرح ذلك بالكلمات مثل أن نقول: "تبدأ شيفرة البرنامج من 20 بايت في هذا الملف، ويبلغ طولها 50 كيلوبايت، وتتبعها بيانات البرنامج ويبلغ طولها 20 كيلوبايت". هناك صيغة معينة أصبحت في الآونة الأخيرة معيارًا لتمثيل الملفات القابلة للتنفيذ في الأنظمة الحديثة القائمة على نظام يونكس، ويطلَق على هذا التنسيق بصيغة الرابط والملفات القابلة للتنفيذ Executable and Linker Format -أو ELF اختصارًا، حيث سنشرحها بمزيد من التفصيل لاحقًا. تاريخ الصيغة الثنائية سنوضح فيما يلي صيغتين للملفات الثنائية سبقت ظهور صيغة ملفات ELF هما a.out و COFF. a.out لم تكن صيغة ملفات ELF المعيار دائمًا، إذ استخدمت أنظمة يونكس الأصلية صيغة ملف بالاسم a.out. يمكننا أن نرى آثار ذلك عند تصريف برنامج بدون الخيار ‎-o لتحديد اسم ملف الخرج، حيث سينشأ الملف القابل للتنفيذ بالاسم الافتراضي a.out الذي يُعَد اسم ملف الخرج الافتراضي الناتج عن الرابط Linker. يستخدم المصرِّف Compiler أسماء الملفات المُنشَأة عشوائيًا بوصفها ملفات وسيطة لشيفرة التجميع والشيفرة المُصرَّفة. a.out هو صيغة ترويسة بسيطة تسمح فقط بقسم واحد للبيانات والشيفرة وBSS، وهذا غير كافٍ للأنظمة الحديثة ذات المكتبات الديناميكية. COFF كانت صيغة ملف التعليمات المُصرَّفة المشترك Common Object File Format -أو COFF اختصارًا- مقدمة لظهور صيغة ملفات ELF، حيث كانت صيغة ترويستها أكثر مرونة، مما يسمح بمزيد -ولكن محدود- من الأقسام في الملف. تواجه صيغة COFF صعوبات في دعم المكتبات المشتركة، لذا اختيرت صيغة ELF بوصفها تقديمًا Implementation بديلًا على نظام لينكس. لكن توجد صيغة COFF في مايكروسوفت ويندوز بوصفها صيغة ملفات قابلة للتنفيذ والنقل Portable Executable -أو PE اختصارًا- التي تُعَد بالنسبة إلى ويندوز مثل صيغة ملفات ELF في لينكس. صيغة ملفات ELF تُعَد صيغة ملفات ELF صيغةً مرنة لتمثيل الشيفرة الثنائية في النظام، حيث يمكنك باتباع معيار ELF تمثيل النواة Kernel ثنائيًا بسهولة مثل تمثيل ملف قابل للتنفيذ أو مكتبة نظام عادية. يمكن استخدام الأدوات نفسها لفحص وتشغيل جميع ملفات ELF ويمكن للمطورين الذين يفهمون صيغة ملفات ELF الاستفادة من مهاراتهم في معظم الأنظمة الحديثة المبنية على يونكس. توسّع الصيغة ELF صيغة الملفات COFF وتمنح الترويسة مرونة كافية لتحديد عدد عشوائي من الأقسام، بحيث يكون لكل منها خاصياته الخاصة، مما يسهّل الربط الديناميكي وتنقيح الأخطاء Debugging. ترويسة ملفات ELF يحتوي الملف على ترويسة ملف File Header تصِف الملف، ثم يحتوي على مؤشرات لكل قسم من الأقسام التي يتكون منها الملف. يوضح المثال التالي الوصف على النحو الوارد في توثيق واجهة برمجة تطبيقات ELF32 (نموذج 32 بت من صيغة ملفات ELF)، وهو تخطيط لبنية لغة C الذي يعرّف ترويسة ELF: typedef struct { unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; Elf32_Off e_phoff; Elf32_Off e_shoff; Elf32_Word e_flags; Elf32_Half e_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; Elf32_Half e_shnum; Elf32_Half e_shstrndx; } Elf32_Ehdr; إليك مثال عن ترويسة ELF كما هو موضح باستخدام الأداة readelf: $ readelf --header /bin/ls ELF Header: Magic: 7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, big endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: PowerPC Version: 0x1 Entry point address: 0x10002640 Start of program headers: 52 (bytes into file) Start of section headers: 87460 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 8 Size of section headers: 40 (bytes) Number of section headers: 29 Section header string table index: 28 [...] يوضّح المثال السابق نموذجًا سهل القراءة على الإنسان كما مولدًا باستخدام برنامج readelf، وهو جزء من أدوات Binutils في GNU. تُوجَد المصفوفة e_ident في بداية أيّ ملف ELF، وتبدأ دائمًا بمجموعة بايتات سحرية. البايت الأول هو 0x7F ثم الثلاثة بايتات التالية هي "ELF". يمكنك فحص ملف ELF الثنائي لترى ذلك بنفسك باستخدام الأمر hexdump. يفحص المثال التالي عدد ELF السحري: ianw@mingus:~$ hexdump -C /bin/ls | more 00000000 7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00 |.ELF............| ... (يتبع ذلك بقية البرنامج) … لاحظ وجود البت 0x7F في البداية ثم سلسلة آسكي ASCII المُشفَّرة "ELF". ألقِ نظرة على المعيار وشاهد ما تعرّفه بقية المصفوفة وما هي القيم الموجودة في الملف الثنائي. لدينا بعد ذلك بعض الرايات Flags لنوع الجهاز الذي اُنشِئ هذا الملف الثنائي من أجله. لاحظ أن صيغة ELF تعرّف إصدارات مختلفة من الأحجام مثل إصدارات 32 بت و64 بت، حيث سنشرح هنا الإصدار 32 بت. يكمن الاختلاف في أنه يجب الاحتفاظ بالعناوين على أجهزة 64 بت في متغيرات بحجم 64 بتًا. يمكننا أن نرى أن الملف الثنائي أُنشِئ للجهاز الذي يستخدم صيغة Big Endian (تخزين البتات الأقل أهمية أولًا)، حيث يستخدم هذا الجهاز المكمل الثنائي لتمثيل الأعداد السالبة. لاحظ بعد ذلك أن الخاصية Machine تخبرنا أنه جهاز PowerPC الثنائي. يبدو أن عنوان نقطة الدخول واضح وصريح بدرجة كافية، وهو العنوان الموجود في الذاكرة الذي تبدأ منه شيفرة البرنامج. يُقال لمبرمجي لغة C المبتدئين أن الدالة الرئيسية main()‎ هي أول برنامج يُستدعَى في برامجهم، ولكن يمكننا التحقق من أنه ليس كذلك باستخدام عنوان نقطة الدخول كما يلي: $ cat test.c #include <stdio.h> int main(void) { printf("main is : %p\n", &main); return 0; } $ gcc -Wall -o test test.c $ ./test main is : 0x10000430 $ readelf --headers ./test | grep 'Entry point' Entry point address: 0x100002b0 $ objdump --disassemble ./test | grep 100002b0 100002b0 <_start>: 100002b0: 7c 29 0b 78 mr r9,r1 لاحظ في المثال السابق أنه يمكننا أن نرى أن نقطة الدخول هي دالة تسمى ‎_start. لم يعرّف برنامجنا هذه الدالة على الإطلاق، ويشير الخط السفلي في بداية اسم الدالة إلى أنها موجودة في فضاء أسماء منفصل. سنشرح لاحقًا كيفية بدء البرنامج بالتفصيل. تحتوي الترويسة بعد ذلك على مؤشرات إلى المكان الموجود في الملف الذي تبدأ فيه الأجزاء المهمة الأخرى من ملف ELF مثل جدول المحتويات. الرموز Symbols 3 والمنقولات Relocation توفر مواصفات ملف ELF جداول رموز Symbol Tables تربط بين السلاسل النصية أو الرموز ومواقع في الملف. تُعَد الرموز مطلوبة للربط Linking، فمثلًا يمكن أن يتطلب إسنادُ قيمة للمتغير foo المُصرَّح عنه بالشكل extern int foo رابطًا للعثور على عنوان المتغير foo، والذي يمكن أن يتضمن البحث عن الكلمة "foo" في جدول الرموز وإيجاد العنوان. ترتبط المنقولات Relocations ارتباطًا وثيقًا بالرموز، حيث يُعَد الانتقال مساحةً فارغة تُترَك لإصلاحها لاحقًا، إذ لا يمكن استخدام المتغير foo في المثال السابق حتى معرفة عنوانه، ولكن نعلم في نظام 32 بت أن عنوان المتغير foo يجب أن يكون بقيمة 4 بايتات، لذلك يمكن للمصرِّف ببساطة ترك مساحة فارغة بمقدار 4 بايتات والاحتفاظ بانتقالٍ Relocation يخبر الرابط بأن يضع القيمة الحقيقية للمتغير foo في هذه المساحة التي مقدارها 4 بايتات في هذا العنوان في أيّ وقت يحتاج فيه المصرِّف استخدامَ هذا العنوان لإسناد قيمة مثلًا، ولكن يتطلب ذلك تحليل الرمز "foo". المقاطع Segments والأقسام Sections تحدد صيغة ELF عرضين لملف ELF، حيث يُستخدَم أحدهما للربط والآخر للتنفيذ، مما يوفر مرونة كبيرة لمصممي الأنظمة. سنتحدث عن الأقسام الموجودة في شيفرة الكائن التي تنتظر أن تُربَط بملف قابل للتنفيذ، ويُربَط قسم واحد أو أكثر مع مقطعٍ ما في الملف القابل للتنفيذ. المقاطع Segments من الأسهل في بعض الأحيان النظر إلى المستوى الأعلى من التجريد abstraction المتمثل بالمقاطع قبل فحص الطبقات السفلية. يحتوي ملف ELF على ترويسة تصف تخطيط الملف العام، حيث تشير ترويسة ELF إلى مجموعة أخرى من الترويسات تسمى ترويسات البرامج Program Headers، حيث تصِف هذه الترويسات لنظام التشغيل أيّ شيء يمكن أن يكون مطلوبًا لتحميل الملف الثنائي في الذاكرة وتنفيذه. كما تصف ترويسات البرامج المقاطع، ولكن هناك بعض الأشياء الأخرى المطلوبة لتشغيل الملف القابل للتنفيذ. إليك مثال عن ترويسة برنامج: typedef struct { Elf32_Word p_type; Elf32_Off p_offset; Elf32_Addr p_vaddr; Elf32_Addr p_paddr; Elf32_Word p_filesz; Elf32_Word p_memsz; Elf32_Word p_flags; Elf32_Word p_align; } يوضح المثال السابق تعريف ترويسة البرنامج. لا بد أنك لاحظت من تعريف ترويسة ELF سابقًا وجود الحقول e_phoff وe_phnum وe_phentsize التي تمثّل الإزاحة في الملف حيث تبدأ ترويسات البرامج وعدد ترويسات البرامج الموجودة وحجم كل ترويسة برنامج، وبالتالي يمكنك العثور على ترويسات البرامج وقراءتها بسهولة باستخدام هذه الأجزاء الثلاثة من المعلومات. تُعَد ترويسات البرامج أكثر من مجرد مقاطع، حيث يعرّف الحقل p_type ما تعرّفه ترويسة البرنامج، فمثلًا إذا كان هذا الحقل هو PT_INTERP، فستُعرَّف الترويسة بأنها مؤشر سلسلة نصية يؤشّر إلى مفسّر Interpreter الملف الثنائي. ناقشنا سابقًا الفرق بين اللغات المُصرَّفة Compiled واللغات المُفسَّرة Interpreted وميّزنا المُصرِّف بأنه ينشئ ملفًا ثنائيًا يمكن تشغيله بطريقة مستقلة. لكن لا بد أنك تتساءل عن سبب حاجتنا لمفسّر! حسنًا، ترغب الأنظمة الحديثة في المرونة عند تحميل الملفات القابلة للتنفيذ، لذا لا يمكن الحصول على بعض المعلومات بصورة كافية إلّا في الوقت الفعلي الذي يُعَد فيه البرنامج للتشغيل، وهذا ما يسمى بالربط الديناميكي Dynamic Linking الذي سنتحدث عنه لاحقًا، وبالتالي يجب إجراء بعض التغييرات الطفيفة على البرنامج الثنائي للسماح له بالعمل بصورة صحيحة في وقت التشغيل. لذا يُعَد مفسّر الملف الثنائي المعتاد هو المحمّل الديناميكي Dynamic Loader، لأنه يأخذ الخطوات النهائية لإكمال تحميل الملف القابل للتنفيذ وإعداد الصورة الثنائية للتشغيل. تصف القيمة PT_LOAD في الحقل p_type المقاطع، ثم تصف الحقول الأخرى في ترويسة البرنامج كلّ مقطع منها. يخبرك الحقل p_offset بمقدار بُعد بيانات المقطع عن الملف الموجود على القرص الصلب. بينما يخبرك الحقل p_vaddr بالعنوان الذي يجب أن توجد عنده البيانات في الذاكرة الوهمية Virtual Memory، حيث يصف الحقل p_addr العنوان الحقيقي Physical Address الذي يُعَد مفيدًا للأنظمة المدمَجة الصغيرة التي لا تطبّق الذاكرة الوهمية. تخبرك الرايتان p_filesz وp_memsz بحجم المقطع الموجود على القرص الصلب وكم يجب أن يكون حجمه في الذاكرة. إذا كان حجم الذاكرة أكبر من حجم القرص الصلب، فيجب ملء التداخل بينهما بالأصفار، وبالتالي يمكنك توفير مساحة كبيرة في ملفاتك الثنائية من خلال عدم الاضطرار إلى هدر مساحة للمتغيرات العامة الفارغة. أخيرًا، يشير الحقل p_flags إلى أذونات المقطع، حيث يمكن تحديد أذونات التنفيذ والقراءة والكتابة، فمثلًا يجب تمييز مقاطع الشيفرة البرمجية بأنها للقراءة والتنفيذ فقط، وتمييز أقسام البيانات للقراءة والكتابة فقط بدون تنفيذ. هناك عدد من أنواع المقاطع الأخرى المُعرَّفة في ترويسات البرامج الموصوفة كاملةً في مواصفات المعايير. الأقسام Sections تشكّل الأقسام مقاطعًا، حيث تُعَد الأقسام طريقة لتنظيم الملف الثنائي في مناطق منطقية لتوصيل المعلومات بين المصرِّف والرابط. تُستخدَم الأقسام في بعض الملفات الثنائية الخاصة مثل نواة لينكس Linux Kernel بطرق أكثر تحديدًا سنوضحها لاحقًا. رأينا كيف تصل المقاطع في النهاية إلى كتلة بيانات في ملف على القرص الصلب مع بعض المواصفات حول المكان الذي يجب تحميلها فيه والأذونات التي تمتلكها. تمتلك الأقسام ترويسةً مماثلة لترويسة المقاطع كما هو موضح في المثال التالي: typedef struct { Elf32_Word sh_name; Elf32_Word sh_type; Elf32_Word sh_flags; Elf32_Addr sh_addr; Elf32_Off sh_offset; Elf32_Word sh_size; Elf32_Word sh_link; Elf32_Word sh_info; Elf32_Word sh_addralign; Elf32_Word sh_entsize; } تحتوي الأقسام على عدد من الأنواع المُعرَّفة للحقل sh_type مثل تعريف قسم من النوع SH_PROGBITS بوصفه قسمًا يحتوي على بيانات ثنائية يستخدمها البرنامج. تشير الرايات الأخرى إلى ما إذا كان هذا القسم جدولَ رموز يستخدمه الرابط أو منقح الأخطاء مثلًا أو يمكن أن يكون شيئًا ما خاصًا بالمحمّل الديناميكي. كما توجد سمات إضافية مثل سمة التخصيص Allocate التي تشير إلى أن هذا القسم سيحتاج إلى ذاكرة مخصصة له. سنختبر الآن البرنامج الموضح في المثال التالي: #include <stdio.h> int big_big_array[10*1024*1024]; char *a_string = "Hello, World!"; int a_var_with_value = 0x100; int main(void) { big_big_array[0] = 100; printf("%s\n", a_string); a_var_with_value += 20; } يوضح المثال التالي خرج الأداة readelf مع بعض الأجزاء الأخرى، حيث يمكننا باستخدام هذا الخرج تحليل كل جزء من برنامجنا البسيط السابق ومعرفة ما سيحدث به في خرج الملف الثنائي النهائي: $ readelf --all ./sections ELF Header: ... Size of section headers: 40 (bytes) Number of section headers: 37 Section header string table index: 34 Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 10000114 000114 00000d 00 A 0 0 1 [ 2] .note.ABI-tag NOTE 10000124 000124 000020 00 A 0 0 4 [ 3] .hash HASH 10000144 000144 00002c 04 A 4 0 4 [ 4] .dynsym DYNSYM 10000170 000170 000060 10 A 5 1 4 [ 5] .dynstr STRTAB 100001d0 0001d0 00005e 00 A 0 0 1 [ 6] .gnu.version VERSYM 1000022e 00022e 00000c 02 A 4 0 2 [ 7] .gnu.version_r VERNEED 1000023c 00023c 000020 00 A 5 1 4 [ 8] .rela.dyn RELA 1000025c 00025c 00000c 0c A 4 0 4 [ 9] .rela.plt RELA 10000268 000268 000018 0c A 4 25 4 [10] .init PROGBITS 10000280 000280 000028 00 AX 0 0 4 [11] .text PROGBITS 100002b0 0002b0 000560 00 AX 0 0 16 [12] .fini PROGBITS 10000810 000810 000020 00 AX 0 0 4 [13] .rodata PROGBITS 10000830 000830 000024 00 A 0 0 4 [14] .sdata2 PROGBITS 10000854 000854 000000 00 A 0 0 4 [15] .eh_frame PROGBITS 10000854 000854 000004 00 A 0 0 4 [16] .ctors PROGBITS 10010858 000858 000008 00 WA 0 0 4 [17] .dtors PROGBITS 10010860 000860 000008 00 WA 0 0 4 [18] .jcr PROGBITS 10010868 000868 000004 00 WA 0 0 4 [19] .got2 PROGBITS 1001086c 00086c 000010 00 WA 0 0 1 [20] .dynamic DYNAMIC 1001087c 00087c 0000c8 08 WA 5 0 4 [21] .data PROGBITS 10010944 000944 000008 00 WA 0 0 4 [22] .got PROGBITS 1001094c 00094c 000014 04 WAX 0 0 4 [23] .sdata PROGBITS 10010960 000960 000008 00 WA 0 0 4 [24] .sbss NOBITS 10010968 000968 000000 00 WA 0 0 1 [25] .plt NOBITS 10010968 000968 000060 00 WAX 0 0 4 [26] .bss NOBITS 100109c8 000968 2800004 00 WA 0 0 4 [27] .comment PROGBITS 00000000 000968 00018f 00 0 0 1 [28] .debug_aranges PROGBITS 00000000 000af8 000078 00 0 0 8 [29] .debug_pubnames PROGBITS 00000000 000b70 000025 00 0 0 1 [30] .debug_info PROGBITS 00000000 000b95 0002e5 00 0 0 1 [31] .debug_abbrev PROGBITS 00000000 000e7a 000076 00 0 0 1 [32] .debug_line PROGBITS 00000000 000ef0 0001de 00 0 0 1 [33] .debug_str PROGBITS 00000000 0010ce 0000f0 01 MS 0 0 1 [34] .shstrtab STRTAB 00000000 0011be 00013b 00 0 0 1 [35] .symtab SYMTAB 00000000 0018c4 000c90 10 36 65 4 [36] .strtab STRTAB 00000000 002554 000909 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific) There are no section groups in this file. ... Symbol table '.symtab' contains 201 entries: Num: Value Size Type Bind Vis Ndx Name ... 99: 100109cc 0x2800000 OBJECT GLOBAL DEFAULT 26 big_big_array ... 110: 10010960 4 OBJECT GLOBAL DEFAULT 23 a_string ... 130: 10010964 4 OBJECT GLOBAL DEFAULT 23 a_var_with_value ... 144: 10000430 96 FUNC GLOBAL DEFAULT 11 main لنلقِ أولًا نظرة على المتغير big_big_array الذي -كما يوحي الاسم- هو مصفوفة عامة كبيرة إلى حد ما، وإذا انتقلنا إلى جدول الرموز، فيمكننا أن نرى أن هذا المتغير موجود في الموقع 0x100109cc الذي يمكننا ربطه بالقسم ‎.bss في قائمة الأقسام لأنه يبدأ تحته مباشرةً عند الموقع 0x100109c8، ولاحظ حجمه الكبير جدًا. ذكرنا أن القسم BSS هو جزء معياري من صورة ثنائية، لأنه ليس منطقيًا أن تطلب أن يكون لملفٍ ثنائي على القرص الصلب 10 ميجابايتات من المساحة المخصَّصة له عندما تكون كل هذه المساحة قيمًا صفرية. لاحظ أن هذا القسم يحتوي على النوع NOBITS، مما يعني أنه لا يحتوي على أيّ بايت على القرص الصلب. لذا يُعرَّف القسم ‎.bss للمتغيرات العامة التي يجب أن تكون قيمتها صفرًا عند بدء البرنامج. رأينا كيف يمكن أن يختلف حجم الذاكرة عن حجم القرص الصلب عند مناقشتنا للمقاطع، فوجود المتغيرات في القسم ‎.bss دليل على أنها ستُعطَى قيمة صفرية عند بدء البرنامج. يوجد المتغير a_string في القسم ‎.sdata الذي يمثّل البيانات الصغيرة Small Data، حيث يُعَد هذا القسم وقسم ‎.sbss المقابل له أقسامًا متوفرة في بعض المعماريات حيث يمكن الوصول إلى البيانات باستخدام الإزاحة عن بعض المؤشرات المعروفة، وهذا يعني أنه يمكن إضافة قيمة ثابتة إلى العنوان الأساسي، مما يجعل الوصول إلى البيانات في الأقسام أسرع نظرًا لعدم وجود عمليات بحث مطلوبة إضافية وتحميل للعناوين في الذاكرة. تقتصر معظم المعماريات على حجم القيم الفورية Immediate Value التي يمكنك إضافتها إلى المسجل مثل القيمة الفورية 70 عند تطبيق التعليمة r1 = add r2, 70;‎ على عكس جمع قيمتين مخزنتين في مسجلين r1 = add r2,r3، وبالتالي يمكن تطبيق إزاحة بمقدار مسافة صغيرة معينة عن العنوان. يمكننا أيضًا أن نرى أن المتغير a_var_with_value يوجد في المكان نفسه. بينما توجد الدالة الرئيسية main في القسم ‎.text. تذكر أن "النص Text" و"الشيفرة Code" يُستخدَمان للإشارة إلى برنامج في الذاكرة. الأقسام والمقاطع مع بعضها البعض إليك مثال يحتوي على الأقسام والمقاطع مع بعضها البعض: $ readelf --segments /bin/ls Elf file type is EXEC (Executable file) Entry point 0x100026c0 There are 8 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x10000034 0x10000034 0x00100 0x00100 R E 0x4 INTERP 0x000154 0x10000154 0x10000154 0x0000d 0x0000d R 0x1 [Requesting program interpreter: /lib/ld.so.1] LOAD 0x000000 0x10000000 0x10000000 0x14d5c 0x14d5c R E 0x10000 LOAD 0x014d60 0x10024d60 0x10024d60 0x002b0 0x00b7c RWE 0x10000 DYNAMIC 0x014f00 0x10024f00 0x10024f00 0x000d8 0x000d8 RW 0x4 NOTE 0x000164 0x10000164 0x10000164 0x00020 0x00020 R 0x4 GNU_EH_FRAME 0x014d30 0x10014d30 0x10014d30 0x0002c 0x0002c R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x4 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_ r .rela.dyn .rela.plt .init .text .fini .rodata .eh_frame_hdr 03 .data .eh_frame .got2 .dynamic .ctors .dtors .jcr .got .sdata .sbss .p lt .bss 04 .dynamic 05 .note.ABI-tag 06 .eh_frame_hdr 07 يوضح المثال السابق كيف تظهِر الأداة readelf ربط المقاطع والأقسام في ملف ELF مع الملف الثنائي ‎/bin/ls. انتقل إلى نهاية الخرج حيث يمكننا أن نرى الأقسام المنقولة إلى المقاطع، فمثلًا يُوضَع القسم ‎.interp في المقطع الذي له الراية INTERP. لاحظ أن الأداة readelf تخبرنا بطلب المفسّر ‎/lib/ld.so.1، وهو الرابط الديناميكي الذي يُشغَّل لإعداد الملف الثنائي للتنفيذ. يمكننا أن نرى الفرق بين النص والبيانات بالنظر إلى مقطعي LOAD. لاحظ أن المقطع الأول لديه أذونات القراءة والتنفيذ فقط، بينما يكون للمقطع الآخر أذونات القراءة والكتابة والتنفيذ، أي أن مقطع الشيفرة له أذونات القراءة والكتابة (r/w) ومقطع البيانات له أذونات القراءة والكتابة والتنفيذ (r/w/e)، ولكن لا يجب أن تكون البيانات قابلة للتنفيذ. لن يُميَّز قسم البيانات في معظم المعماريات مثل المعمارية x86 الأكثر شيوعًا على أنه يحتوي على قسم بيانات قابل للتنفيذ. لكن المثال السابق مأخوذ من معمارية PowerPC التي لها نموذج برمجة مختلف قليلًا وهو واجهة التطبيق الثنائية Application Binary Interface -أو ABI اختصارًا- التي تتطلب أن يكون قسم البيانات قابلًا للتنفيذ. هذه هي حياة مبرمج الأنظمة، إذ وُضِعت القواعد لكسرها. تستدعي واجهة ABI في معمارية PowerPC شيفرات اختبارية Stubs للدوال في المكتبات الديناميكية مباشرةً في جدول الإزاحة العام Global Offset Table -أو GOT اختصارًا- بدلًا من جعلها ترتد بين مدخلات منفصلة من جدول PLT، وبالتالي يحتاج المعالج إلى أذونات تنفيذ للقسم GOT الذي يمكنك رؤيته مضمَّنًا في مقطع البيانات. الشيء الآخر الذي يجب ملاحظته هو أن حجم الملف هو حجم الذاكرة نفسه لمقطع الشيفرة، ولكن حجم الذاكرة أكبر من حجم ملف مقطع البيانات، ويأتي ذلك من القسم BSS الذي يحتوي على متغيرات عامة صفرية. واجهات ABI تُعَد واجهة ABI مصطلحًا ستسمع عنه كثيرًا عند العمل مع برمجة الأنظمة، وهو مختلف عن مصطلح API الذي يُعَد واجهات يراها المبرمج في شيفرته البرمجية. تشير ABI إلى واجهات المستوى الأدنى التي يجب أن يتفق عليها المصرِّف ونظام التشغيل والمعالج إلى حد ما للتواصل مع بعضها البعض. سنقدم فيما يلي عددًا من المفاهيم المهمة لفهم واجهات ABI. ترتيب البايتات تُرتَّب البايتات باستخدام ترتيب Endianess الذي يحتوي على نوعين هما: Big-endian أي تخزين البتات الأقل أهمية أولًا، و Little-endian أي تخزين البتات الأكثر أهمية أولًا. العرف المتبع في الاستدعاءات يمكن تنفيذ الاستدعاءات بطريقتين هما: تمرير المعاملات باستخدام المسجلات registers أو المكدس stack وواصفات الدوال. بخصوص واصفات الدوال، لا تُستدعَى الدالة في العديد من المعماريات مباشرةً، بل تُستدعَى عبر واصف دالة Function Descriptor. يتكون واصف الدالة في المعمارية IA64 مثلًا من مكونين هما: عنوان الدالة (يُمثَّل بقيمة مقدارها 64 بتًا أو 8 بايتات) وعنوان المؤشر العام Global Pointer أو gp اختصارًا. تحدد واجهة ABI أن المسجل r1 يجب أن يحتوي دائمًا على قيمة المؤشر gp الخاص بالدالة، وهذا يعني أن مهمة المستدعي عند استدعاء دالة هي حفظ قيمة المؤشر gp الخاصة به وضبط المسجل r1 على القيمة الجديدة من واصف الدالة ثم استدعاء هذه الدالة. تُعَد واصفات الدوال مفيدة للغاية كما سترى لاحقًا. يمكن أن تأخذ تعليمة الجمع add في المعالج IA64 قيمة فورية ذات حجم بحد أقصى 22 بتًا بسبب الطريقة التي يحزُم بها المعالج IA64 التعليمات، حيث تُوضَع ثلاثة تعليمات في كل حزمة، ولا يوجد سوى مساحة كافية للاحتفاظ بقيمة 22 بتًا للحفاظ على الحزمة مع بعضها البعض. القيمة الفورية Immediate Value هي القيمة المحددة مباشرةً وليس القيمة الموجودة في المسجل، إذ تُعَد القيمة 100 في التعليمة add r1 + 100 هي القيمة الفورية. يمكن أن تتمكن 22 بتًا من تمثيل 4194304 بايت أو 4 ميجابايتات، وبالتالي يمكن إزاحة كل دالة مباشرة في حيّز ذاكرة كبير مقداره 4 ميجابايتات دون الحاجة إلى تحمل عناء تحميل أيّ قيمٍ في المسجل. إذا اتفق المصرِّف والرابط والمحمِّل على ما يشير إليه المؤشر العام كما هو محدد في واجهة ABI، فيمكن تحسين الأداء من خلال تقليل عمليات التحميل. ترجمة -وبتصرُّف- للأقسام Review of executable files و Representing executable files و ELF و ABIs من فصل Behind the process من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال السابق: تطبيق عملي لبناء برنامج تنفيذي من شيفرة مصدرية بلغة C العمليات وعناصرها في نظام تشغيل الحاسوب تمثيل الأنواع والأعداد في الأنظمة الحاسوبية
  4. تُعَد إدارة ضبط الخادم (يُشار إليها أيضًا باسم أتمتة تقانة المعلومات IT Automation) حلًا لتحويل إدارة بنيتك التحتية إلى الشيفرة البرمجية الأساسية، ولوصف جميع العمليات اللازمة لنشر خادم في مجموعة من سكربتات الإعداد المسبق Provisioning Scripts التي يمكن إصدارها وإعادة استخدامها بسهولة، ويمكنها تحسين التكامل لأيّ بنية خادم تحتية بصورة كبيرة بمرور الوقت. تحدثنا في المقال السابق مدخل إلى إدارة ضبط الخوادم Configuration Management عن الفوائد الرئيسية لتنفيذ إستراتيجية إدارة الضبط لبنية الخادم التحتية، وكيفية عمل أدوات إدارة الضبط والعناصر المشتركة بين هذه الأدوات، وسنوضح في هذا المقال عملية أتمتة إعداد الخادم المسبق باستخدام الأداة Ansible، وهي أداة لإدارة الضبط توفر إطار عمل أتمتة كامل وقدرات تنسيق مع الحفاظ على هدف السهولة والبساطة المطلقة، وسنركز على مصطلحات اللغة والصياغة والميزات اللازمة لإنشاء مثال مبسّط للأتمتة الكاملة لنشر خادم ويب أوبنتو 18.04 باستخدام أباتشي Apache. تحتوي القائمة التالية على جميع الخطوات التي نحتاجها للأتمتة حتى الوصول إلى هدفنا: قائمة الحزم. ثبّت أباتشي Apache. أنشئ مجلد المستند الجذر المُخصَّص. ضع ملف index.html في المستند الجذر المخصص. طبّق قالبًا لإعداد المضيف الوهمي المخصص. أعِد تشغيل أباتشي. سنبدأ بإلقاء نظرة على المصطلحات التي تستخدمها أداة Ansible، ثم سنتعرّف على ميزات اللغة الرئيسية التي يمكن استخدامها لكتابة أدلة التشغيل Playbooks، ثم ستجد محتويات مثال الإعداد المسبق الكامل لأتمتة الخطوات الموضحة لإعداد أباتشي على Ubuntu 18.04. ملاحظة: يهدف هذا المقال إلى تعريفك بلغة Ansible وكيفية كتابة أدلة التشغيل لأتمتة إعداد خادمك المسبق. اطّلع على كيفيّة تثبيت وضبط Ansible على Ubuntu 18.04 لمعرفة الخطوات اللازمة لتثبيت أداة Ansible والبدء باستخدامها، بالإضافة إلى كيفية تشغيل أوامر Ansible وأدلة تشغيلها. هذا المقال جزء من سلسلة حول إدارة ضبط الخوادم، وإليك روابط فصول السلسلة: مدخل إلى إدارة ضبط الخوادم Configuration Management مبادئ إدارة ضبط الخوادم Configuration Management: كتابة دليل تشغيل الأداة Ansible مبادئ إدارة ضبط الخوادم Configuration Management: كتابة ملفات البيان Manifests للأداة Puppet مبادئ إدارة ضبط الخوادم Configuration Management: كتابة الوصفات Recipes في الأداة Chef البدء باستخدام أداة Ansible يجب أن نتعرف أولًا على المصطلحات والمفاهيم المهمة التي قدمتها أداة Ansible قبل البدء بالعمل. المصطلحات تحتوي القائمة التالية على نظرة عامة سريعة على المصطلحات الأهم التي تستخدمها أداة Ansible: عقدة التحكم Control Node: الجهاز المُثبّت عليه أداة Ansible، وهو المسؤول عن تشغيل الإعداد المسبق على الخوادم التي تديرها. المخزن Inventory: ملف INI يحتوي على معلومات حول الخوادم التي تديرها. دليل التشغيل Playbook: ملف YAML يحتوي على سلسلة من الإجراءات التي يجب أن تكون مؤتمتة. المهمة Task: كتلة تحدد إجراءً واحدًا لتنفيذه مثل تثبيت حزمة. الوحدة Module: تجرّد الوحدة مهمة النظام مثل التعامل مع الحزم أو إنشاء وتغيير الملفات. تحتوي أداة Ansible على العديد من الوحدات المبنية مسبقًا، ولكن يمكنك إنشاء وحداتك المُخصَّصة. الدور Role: مجموعة من أدلة التشغيل والقوالب والملفات الأخرى ذات الصلة، وهي منظمة بطريقة مُعرَّفة مسبقًا لتسهيل إعادة الاستخدام والمشاركة. التشغيل Play: هو الإعداد المسبق المنفَّذ من البداية إلى النهاية. الحقائق Facts: المتغيرات العامة التي تحتوي على معلومات حول النظام مثل واجهات الشبكة أو نظام التشغيل. المعالجات Handlers: تُستخدَم لبدء تغييرات حالة الخدمة مثل إعادة تشغيل الخدمة أو إعادة تحميلها. تنسيق المهمة تحدّد المهمة خطوة مؤتمتة واحدة يجب أن تنفّذها الأداة Ansible، وتتضمن عادةً استخدام وحدة Module أو تنفيذ أمر خام Raw Command، حيث تبدو المهمة كما يلي: - name: This is a task apt: name=vim state=latest يُعَد الجزء name اختياريًا ولكن يُوصَى به، حيث يظهر في مخرجات الإعداد المسبق عند تنفيذ المهمة، والجزء apt هو وحدة Ansible مبنية مسبقًا تجرّد إدارة الحزم في التوزيعات القائمة على نظام دبيان Debian. تخبر المهمة في المثال السابق أداة Ansible أن الحزمة vim يجب أن تتغير حالتها إلى الأحدث latest، مما يؤدي إلى أن يثبّت مدير الحزم هذه الحزمة في حالة عدم تثبيتها بعد. تنسيق دليل التشغيل أدلة التشغيل هي ملفات YAML تحتوي على سلسلة من الموجّهات Directives لأتمتة إعداد الخادم المسبق. يوضّح المثال التالي دليل تشغيل بسيط يؤدي مهمتين هما: تحديث ذاكرة apt المخبئية ثم تثبيت حزمة vim: --- - hosts: all become: true tasks: - name: Update apt-cache apt: update_cache=yes - name: Install Vim apt: name=vim state=latest تعتمد لغة YAML على المسافة البادئة لسَلسلة بنى البيانات، لذا يجب أن تكون حريصًا للحفاظ على المسافة البادئة الصحيحة عند كتابة أدلة التشغيل وخاصة عند نسخ الأمثلة. سنرى في نهاية المقال مثالًا واقعيًا لأدلة التشغيل بالتفصيل، ولكن سيعطيك القسم التالي نظرة عامة على أهم العناصر والميزات التي يمكن استخدامها لكتابة دليل تشغيل Ansible. كتابة أدلة التشغيل أصبحت الآن على دراية بالمصطلحات الأساسية والتنسيق العام لأدلة التشغيل والمهام في Ansible، لذا سنتعرف على بعض ميزات دليل التشغيل التي يمكن أن تساعدنا في إنشاء عمليات أتمتة أكثر تنوعًا. التعامل مع المتغيرات هناك طرق مختلفة يمكنك من خلالها تعريف المتغيرات في أداة Ansible، ولكن أبسط طريقة هي استخدام القسم vars في دليل التشغيل. يعرّف المثال التالي المتغير package الذي سنستخدمه لاحقًا ضمن مهمة: --- - hosts: all become: true vars: package: vim tasks: - name: Install Package apt: name={{ package }} state=latest للمتغير package نطاق عام، مما يعني أنه يمكن الوصول إليه من أي نقطة في الإعداد المسبق حتى من الملفات والقوالب المُضمَّنة. استخدام الحلقات تُستخدَم الحلقات عادةً لتكرار مهمة باستخدام قيم دخل مختلفة، فمثلًا يمكنك إنشاء مهمة واحدة واستخدام حلقة لتكرار المهمة مع جميع الحزم المختلفة التي تريد تثبيتها بدلًا من إنشاء 10 مهام لتثبيت 10 حزم مختلفة. يمكنك إنشاء حلقة ضمن مهمة من خلال تضمين الخيار with_items مع مصفوفة من القيم، ويمكن الوصول إلى المحتوى باستخدام متغير الحلقة item كما هو موضح في المثال التالي: - name: Install Packages apt: name={{ item }} state=latest with_items: - vim - git - curl ويمكنك استخدام متغير مصفوفة لتعريف عناصرك: --- - hosts: all become: true vars: packages: [ 'vim', 'git', 'curl' ] tasks: - name: Install Package apt: name={{ item }} state=latest with_items: "{{ packages }}" استخدام التعليمات الشرطية يمكنك استخدام التعليمات الشرطية لتقرير ما إذا كان يجب تنفيذ مهمة ما أم لا ديناميكيًا بناءً على متغير أو خرج أمرٍ ما مثلًا، حيث سيوقف المثال التالي الأنظمة القائمة على دبيان فقط: - name: Shutdown Debian Based Systems command: /sbin/shutdown -t now when: ansible_os_family == "Debian" تأخذ التعليمة الشرطية when تعبيرًا يجب تقييمه بوصفه وسيطًا، إذ لا تُنفَّذ المهمة إلا في حالة تقييم التعبير على أنه true، حيث اختبرنا في مثالنا حقيقةً Fact للتحقق مما إذا كان نظام التشغيل من عائلة دبيان. حالة الاستخدام الشائعة للتعليمات الشرطية في أتمتة تقانة المعلومات هي عندما يعتمد تنفيذ مهمة على خرج أمرٍ ما، والطريقة التي نطبّق بها ذلك باستخدام أداة Ansible هي من خلال تسجيل متغير ليحتفظ بنتائج تنفيذ الأمر، ثم اختبار هذا المتغير في مهمة لاحقة. يمكننا اختبار حالة خروج الأمر (إذا فشل أو نجح)، ويمكننا التحقق من وجود محتويات معينة في الخرج بالرغم من أن ذلك يمكن أن يتطلب استخدام تعابير نمطية RegEx وأوامر تحليل السلاسل النصية. يوضح المثال الآتي مهمتين شرطيتين بناءً على خرج الأمر php -v، حيث سنختبر حالة الخروج من الأمر، لأننا نعلم أنه سيفشل في التنفيذ في حالة عدم تثبيت PHP على الخادم، ويُعَد الجزء ignore_errors من المهمة مهمًا للتأكد من استمرار الإعداد المسبق حتى عندما يفشل تنفيذ الأمر. - name: Check if PHP is installed register: php_installed command: php -v ignore_errors: true - name: This task is only executed if PHP is installed debug: var=php_install when: php_installed|success - name: This task is only executed if PHP is NOT installed debug: msg='PHP is NOT installed' when: php_installed|failed الوحدة debug المُستخدَمة في مثالنا هي وحدة مفيدة لعرض محتويات المتغيرات أو رسائل تنقيح الأخطاء، إذ يمكنها إما طباعة سلسلة نصية (عند استخدام الوسيط msg) أو طباعة محتويات متغير (عند استخدام الوسيط var). التعامل مع القوالب تُستخدَم القوالب عادةً لإعداد ملفات الضبط، مما يسمح باستخدام المتغيرات والميزات الأخرى التي تهدف إلى جعل هذه الملفات أكثر تنوعًا وقابلية لإعادة الاستخدام، حيث تستخدم أداة Ansible محرك القوالب Jinja2. يوضَح المثال التالي قالبًا لإعداد مضيف أباتشي الوهمي باستخدام متغيرٍ لإعداد المستند الجذر لهذا المضيف: <VirtualHost *:80> ServerAdmin webmaster@localhost DocumentRoot {{ doc_root }} <Directory {{ doc_root }}> AllowOverride All Require all granted </Directory> </VirtualHost> تُستخدَم الوحدة template المبنية مسبقًا لتطبيق القالب من مهمةٍ ما، فإذا سمّيتَ ملف القالب السابق بالاسم vhost.tpl، ووضعته في في مجلد دليل تشغيلك نفسه، فالطريقة التالية هي الطريقة التي ستطبِّق بها القالب لاستبدال مضيف أباتشي الوهمي الافتراضي: - name: Change default Apache virtual host template: src: vhost.tpl dest: /etc/apache2/sites-available/000-default.conf تعريف المعالجات Handlers وتشغيلها تُستخدَم المعالجات لبدء تغيير الحالة في خدمة ما مثل إعادة التشغيل أو الإيقاف، حيث لا تنفَّذ المعالجات إلّا عندما يبدؤها الموجّه notify مسبقًا في مهمةٍ ما بالرغم من أنها تبدو متشابهة إلى حدٍ ما مع المهام العادية. تُعرَّف المعالجات عادةً بوصفها مصفوفة في القسم handlers من دليل التشغيل، ولكن يمكن أن تكون موجودةً في ملفات منفصلة أيضًا. لنأخذ مثالنا السابق لاستخدام القالب حيث ضبطنا مضيف أباتشي الوهمي. إذا أردت التأكد من إعادة تشغيل أباتشي بعد تغيير المضيف الوهمي، فيجب أولًا إنشاء معالج لخدمة أباتشي، حيث تمثل الطريقة التالي طريقةَ تعريف المعالجات ضمن دليل التشغيل: handlers: - name: restart apache service: name=apache2 state=restarted - name: other handler service: name=other state=restarted يُعَد الموجّه name مهمًا لأنه سيكون المعرّف الفريد لهذا المعالج، ويمكنك بدء هذا المعالج من مهمة من خلال استخدام الخيار notify كما يلي: - name: Change default Apache virtual host template: src: vhost.tpl dest: /etc/apache2/sites-available/000-default.conf notify: restart apache رأينا بعضًا من أهم الميزات التي يمكنك استخدامها لبدء كتابة أدلة تشغيل Ansible، وسنتعمق في القسم التالي في مثال أكثر واقعية عن دليل التشغيل الذي سيؤتمِت تثبيت أباتشي وضبطه على أوبنتو 18.04. تطبيق عملي عن دليل التشغيل لنلقِ الآن نظرة على دليل التشغيل الذي سيؤتمِت تثبيت خادم ويب أباتشي على نظام أوبنتو 18.04. يمكن العثور على المثال الكامل بما في ذلك ملف القالب لإعداد أباتشي وملف HTML ليخدّمه خادم الويب على GitHub، ويحتوي المجلد أيضًا على الملف Vagrantfile الذي يتيح لك اختبار دليل التشغيل في إعداد مبسط باستخدام آلة افتراضية تديرها أداة Vagrant. محتويات دليل التشغيل إليك المحتويات الكاملة لدليل التشغيل: --- - hosts: all become: true vars: doc_root: /var/www/example tasks: - name: Update apt apt: update_cache=yes - name: Install Apache apt: name=apache2 state=latest - name: Create custom document root file: path={{ doc_root }} state=directory owner=www-data group=www-data - name: Set up HTML file copy: src=index.html dest={{ doc_root }}/index.html owner=www-data group=www-data mode=0644 - name: Set up Apache virtual host file template: src=vhost.tpl dest=/etc/apache2/sites-available/000-default.conf notify: restart apache handlers: - name: restart apache service: name=apache2 state=restarted لنتعرّف على كل جزء من دليل التشغيل بمزيد من التفصيل: hosts: all: يبدأ دليل التشغيل بالإشارة إلى وجوب تطبيقه على جميع all المضيفين في مخزونك (hosts: all). يمكن تقييد تنفيذ دليل التشغيل على مضيف معين أو مجموعة من المضيفين، ويمكن تعديل هذا الخيار في وقت التنفيذ. become: true: يخبر هذا الجزء الأداة Ansible باستخدام ارتقاء الصلاحيات sudo لتنفيذ جميع المهام في دليل التشغيل، ويمكن تعديل هذا الخيار لتنفيذ مهمة تلوَ الأخرى. vars: يعرّف هذا الجزء متغيرًا هو doc_root الذي يُستخدَم لاحقًا في مهمة، ويمكن أن يحتوي على متغيرات متعددة. tasks : تُعرَّف المهام الفعلية في هذا القسم، حيث تحدِّث المهمة الأولى ذاكرة apt المخبئية، وتثبّت المهمة الثانية الحزمة apache2. تستخدم المهمة الثالثة ملف الوحدة المبنية مسبقًا لإنشاء مجلد ليكون بمثابة المستند الجذر، ويمكن استخدام هذه الوحدة لإدارة الملفات والمجلدات، وتستخدم المهمة الرابعة نسخة الوحدة لنسخ ملف محلي إلى الخادم البعيد، حيث سننسخ ملف HTML بسيط لتقديمه بوصفه موقع ويب يستضيفه أباتشي. handlers: يُصرَّح عن الخدمات في القسم handlers. عرّفنا المعالج restart apache الذي يُعلَم من المهمة الرابعة في مكان تطبيق قالب أباتشي. بدء تنفيذ دليل التشغيل يمكنك استخدام دليل التشغيل ansible-playbook لتنفيذه على عقدة واحدة أو أكثر من مخزونك بعد تنزيل محتويات دليل التشغيل إلى عقدة تحكم Ansible، حيث ينفّذ الأمر التالي دليل التشغيل على جميع المضيفين من ملف مخزونك الافتراضي باستخدام استيثاق زوج مفاتيح SSH للاتصال كمستخدم النظام الحالي: ansible-playbook playbook.yml يمكنك استخدام ‎-l لقصر التنفيذ على مضيف واحد أو مجموعة مضيفين من مخزونك كما يلي: ansible-playbook -l host_or_group playbook.yml إذا أردتَ تحديد مستخدم SSH مختلف للاتصال بالخادم البعيد، فيمكنك تضمين الوسيط ‎-u user في هذا الأمر كما يلي: ansible-playbook -l host_or_group playbook.yml -u remote-user يرجى الاطلاع على كيفية تثبيت وضبط Ansible على Ubuntu 18.04 لمزيد من المعلومات حول كيفية تشغيل أوامر وأدلة تشغيل Ansible. الخلاصة تُعَد Ansible أداة أتمتة بسيطة لتقانة المعلومات لديها مسار تعليمي بسيط، وتستخدم لغة YAML لسكربتات الإعداد المسبق، وتحتوي على عدد كبير من الوحدات المبنية مسبقًا التي يمكن استخدامها لتجريد المهام مثل تثبيت الحزم والعمل مع القوالب. يمكن أن تكون متطلبات البنية التحتية المبسطة واللغة البسيطة مناسبة جدًا للأشخاص المبتدئين في مجال إدارة الضبط، ولكنها يمكن أن تفتقر إلى بعض الميزات المتقدمة التي يمكنك العثور عليها باستخدام أدوات أكثر تعقيدًا مثل Puppet و Chef. سنتعرّف في المقال التالي على الأداة Puppet، وهي أداة إدارة ضبط شائعة تستخدم لغة DSL مخصصة قوية وتعتمد على لغة روبي Ruby لكتابة سكربتات الإعداد المسبق. ترجمة -وبتصرُّف- للمقال Configuration Management 101: Writing Ansible Playbooks لصاحبته Erika Heidi. اقرأ أيضًا كيف تساعد Ansible على أتمتة مهام بسيطة لإدارة حواسيب المنزل مدخل إلى وحدات Ansible
  5. تعرّفنا في المقال السابق من سلسلة مدخل لعلم الحاسوب على الخطوات الثلاث لبناء ملف قابل للتنفيذ هي: التصريف Compiling والتجميع Assembling والربط Linking، وسنطبّق في هذا المقال هذه الخطوات عمليًا لبناء ملف قابل للتنفيذ. تابع فيما الخطوات التي جرى اتخاذها لبناء تطبيق بسيط خطوة بخطوة. لاحظ أن الأمر gcc يشغّل برنامجَ تشغيلٍ driver program يخفي معظم الخطوات عنك، وهذا هو ما تريده بالضبط في ظل الظروف العادية، لأن الأوامر والخيارات الدقيقة للحصول على ملف قابلٍ للتنفيذ على نظام حقيقي يمكن أن تكون معقدة للغاية وخاصةً بكل معمارية على حدة. سنشرح عملية التصريف في المثالين التاليين، حيث سنستخدم ملفين مصدريين مكتوبين بلغة C، إذ يعرّف أحدهما الدالة الرئيسية main()‎ التي تُعَد نقطة الدخول الأولية، ويصرّح الملف الآخر عن دالة مساعدة، وهناك متغير عام واحد. إليك مثال مرحبًا بالعالم Hello World: #include <stdio.h> ‫/* نحتاج نموذجًا أوليًا ليعرف المصرّف نوع الدالة‫ function()‎ */ int function(char *input); ‫/* ‫بما أن هذا المتغير ساكن static، فيمكننا تعريفه في كلٍّ من الملفين hello.c وfunction.c */ static int i = 100; /* هذا متغير عام */ int global = 10; int main(void) { ‫/* ‫يجب أن تعيد الدالة function()‎ قيمة المتغير العام global */ int ret = function("Hello, World!"); exit(ret); } إليك مثال على دالة: #include <stdio.h> static int i = 100; ‫/* مُصرّح عنه بأنه خارجي‫ extern لأنه مُعرَّف في الملف hello.c */ extern int global; int function(char *input) { printf("%s\n", input); return global; } التصريف Compiling لكل المصرِّفات خيارٌ لتنفيذ الخطوة الأولى من التصريف فقط مثل استخدام الراية ‎-S لوضع الخرج في ملف يحمل اسم ملف الدخل نفسه ولكن مع اللاحقة ‎.s، وبالتالي يمكننا عرض الخطوة الأولى باستخدام الأمر gcc -S كما هو موضح في المثال التالي: $ gcc -S hello.c $ gcc -S function.c $ cat function.s .file "function.c" .pred.safe_across_calls p1-p5,p16-p63 .section .sdata,"aw",@progbits .align 4 .type i#, @object .size i#, 4 i: data4 100 .section .rodata .align 8 .LC0: stringz "%s\n" .text .align 16 .global function# .proc function# function: .prologue 14, 33 .save ar.pfs, r34 alloc r34 = ar.pfs, 1, 4, 2, 0 .vframe r35 mov r35 = r12 adds r12 = -16, r12 mov r36 = r1 .save rp, r33 mov r33 = b0 .body ;; st8 [r35] = r32 addl r14 = @ltoffx(.LC0), r1 ;; ld8.mov r37 = [r14], .LC0 ld8 r38 = [r35] br.call.sptk.many b0 = printf# mov r1 = r36 ;; addl r15 = @ltoffx(global#), r1 ;; ld8.mov r14 = [r15], global# ;; ld4 r14 = [r14] ;; mov r8 = r14 mov ar.pfs = r34 mov b0 = r33 .restore sp mov r12 = r35 br.ret.sptk.many b0 ;; .endp function# .ident "GCC: (GNU) 3.3.5 (Debian 1:3.3.5-11)" تُعَد عملية التجميع Assembly معقدة قليلًا، ولكن يجب أن تكون قادرًا على معرفة مكان تعريف المتغير i بوصفه data4 أي 4 بايتات أو 32 بتًا بحجم النوع int، ومكان تعريف الدالة function (بالشكل function:‎) واستدعاء الدالة printf()‎. أصبح لدينا الآن ملفا تجميع جاهزين لتجميعهما في شيفرة الآلة البرمجية machine code. التجميع Assembly التجميع هو عملية مباشرة إلى حد ما، ويُطلَق على المجمّع as ويأخذ وسائطًا بطريقة مماثلة للأمر gcc. إليك مثال عن التجميع: $ as -o function.o function.s $ as -o hello.o hello.s $ ls function.c function.o function.s hello.c hello.o hello.s تنتج عن عملية التجميع التعليمات المُصرَّفة Object Code، حيث تكون هذه الشيفرة جاهزةً لربطها مع بعضها البعض في الملف النهائي القابل للتنفيذ. يمكنك تخطي الاضطرار إلى استخدام المُجمِّع يدويًا من خلال استدعاء المصرِّف مع الراية ‎-c التي تحوّل ملف الدخل مباشرةً إلى شيفرة كائن، وتضعها في ملف له البادئة نفسها ولكن مع اللاحقة ‎.o. لا يمكننا فحص شيفرة التعليمات المُصرَّفة مباشرةً لأنها في صيغة ثنائية، ولكن يمكننا استخدام بعض الأدوات لفحص ملفات التعليمات المُصرَّفة مثل الأداة readelf --symbols التي ستعرض الرموز الموجودة في ملف الكائن كما يلي: $ readelf --symbols ./hello.o Symbol table '.symtab' contains 15 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 5: 0000000000000000 0 SECTION LOCAL DEFAULT 5 6: 0000000000000000 4 OBJECT LOCAL DEFAULT 5 i 7: 0000000000000000 0 SECTION LOCAL DEFAULT 6 8: 0000000000000000 0 SECTION LOCAL DEFAULT 7 9: 0000000000000000 0 SECTION LOCAL DEFAULT 8 10: 0000000000000000 0 SECTION LOCAL DEFAULT 10 11: 0000000000000004 4 OBJECT GLOBAL DEFAULT 5 global 12: 0000000000000000 96 FUNC GLOBAL DEFAULT 1 main 13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND function 14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND exit $ readelf --symbols ./function.o Symbol table '.symtab' contains 14 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS function.c 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 5: 0000000000000000 0 SECTION LOCAL DEFAULT 5 6: 0000000000000000 4 OBJECT LOCAL DEFAULT 5 i 7: 0000000000000000 0 SECTION LOCAL DEFAULT 6 8: 0000000000000000 0 SECTION LOCAL DEFAULT 7 9: 0000000000000000 0 SECTION LOCAL DEFAULT 8 10: 0000000000000000 0 SECTION LOCAL DEFAULT 10 11: 0000000000000000 128 FUNC GLOBAL DEFAULT 1 function 12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf 13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND global يُعَد هذا الخرج معقدًا للغاية، ولكن يجب أن تكون قادرًا على فهم الكثير منه مثل: لاحظ الرمز الذي يحمل الاسم i في الخرج hello.o، حيث يُسبَق هذا الرمز بالكلمة LOCAL أي أنه محلي، لأننا صرّحنا عنه بأنه ساكن static، وبالتالي يُميَّز على أنه محلي لملف الكائن. لاحظ المتغير global في الخرج نفسه المُعرَّف على أنه متغير عام GLOBAL، مما يعني أنه مرئي خارج هذا الملف، وتكون الدالة الرئيسية main()‎ مرئية من خارج الملف. لاحظ أن الرمز function له النوع UND أو غير مُعرَّف Undefined من أجل استدعاء الدالة function()‎، أي أن الأمر متروك للرابط Linker للعثور على عنوان الدالة. لاحظ الرموز الموجودة في الملف function.c وكيفية ملاءمتها مع الخرج. الربط Linking يُعَد استدعاء الرابط المُسمَّى ld عمليةً معقدة للغاية على نظام حقيقي، لذلك نترك عملية الربط للأمر gcc، ولكن يمكننا التعرّف على ما يفعله داخليًا باستخدام الراية ‎-v التي ترمز إلى Verbose أي مُفصَّلة. إليك مثال عن عملية الربط: /usr/lib/gcc-lib/ia64-linux/3.3.5/collect2 -static /usr/lib/gcc-lib/ia64-linux/3.3.5/../../../crt1.o /usr/lib/gcc-lib/ia64-linux/3.3.5/../../../crti.o /usr/lib/gcc-lib/ia64-linux/3.3.5/crtbegin.o -L/usr/lib/gcc-lib/ia64-linux/3.3.5 -L/usr/lib/gcc-lib/ia64-linux/3.3.5/../../.. hello.o function.o --start-group -lgcc -lgcc_eh -lunwind -lc --end-group /usr/lib/gcc-lib/ia64-linux/3.3.5/crtend.o /usr/lib/gcc-lib/ia64-linux/3.3.5/../../../crtn.o أول شيء تلاحظه هو استدعاء برنامج بالاسم collect2 وهو عبارة عن مُغلِّف للرابط ld، ويستخدم الأمر gcc داخليًا. الشيء الآخر الذي ستلاحظه هو ملفات الكائنات التي تبدأ بالرمز crt أي أنها مُحدَّدة للرابط. يُوفّر الأمر gcc ومكتبات النظام هذه الدوال التي تحتوي على الشيفرة البرمجية المطلوبة لبدء البرنامج. لا تُعَد الدالة الرئيسية main()‎ أول دالة مُستدعاة عند تشغيل البرنامج، بل تُستدعَى أولًا الدالة ‎_start الموجودة في ملفات الكائنات crt، حيث تضبط هذه الدالة بعض الإعدادات العامة التي لا يجب أن يقلق مبرمجو التطبيقات بشأنها. يُعَد تسلسل المسار الهرمي معقدًا للغاية، ولكن يمكننا أن نرى أن الخطوة الأخيرة هي ربط بعض ملفات الكائنات الإضافية وهي: crt1.o: توفره مكتبات النظام libc، ويحتوي على الدالة ‎_start التي تُعَد أول شيء يُستدعَى في البرنامج. crti.o: توفّره مكتبات النظام. crtbegin.o crtsaveres.o crtend.o crtn.o يمكنك أن ترى بعد ذلك أننا نربط ملفي الكائنات hello.o وfunction.o، ثم نحدّد بعض المكتبات الإضافية باستخدام رايات ‎-l، حيث تُعَد هذه المكتبات خاصةً بالنظام ومطلوبة لكل برنامج. الراية الرئيسية هي الراية ‎-lc التي تجلب مكتبة C التي تحتوي على جميع الدوال المشتركة مثل الدالة printf()‎. نربط بعد ذلك مرة أخرى بعض ملفات كائنات النظام التي تطبّق بعض عمليات التنظيف بعد انتهاء البرامج. تُعَد هذه التفاصيل معقدة، إلا أن مفهومها واضح ومباشر. سنربط بعد ذلك جميع ملفات التعليمات المُصرَّفة مع بعضها البعض في ملف واحد قابل للتنفيذ وجاهز للتشغيل. الملف القابل للتنفيذ Executable سندخل في مزيد من التفاصيل حول الملف القابل للتنفيذ لاحقًا، ولكن يمكننا إجراء فحص بطريقة مماثلة لملفات الكائنات لمعرفة ما يحدث. إليك مثال عن ملف قابل للتنفيذ: ianw@lime:~/programs/csbu/wk7/code$ gcc -o program hello.c function.c ianw@lime:~/programs/csbu/wk7/code$ readelf --symbols ./program Symbol table '.dynsym' contains 11 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 6000000000000de0 0 OBJECT GLOBAL DEFAULT ABS _DYNAMIC 2: 0000000000000000 176 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2 (2) 3: 600000000000109c 0 NOTYPE GLOBAL DEFAULT ABS __bss_start 4: 0000000000000000 704 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2 (2) 5: 600000000000109c 0 NOTYPE GLOBAL DEFAULT ABS _edata 6: 6000000000000fe8 0 OBJECT GLOBAL DEFAULT ABS _GLOBAL_OFFSET_TABLE_ 7: 60000000000010b0 0 NOTYPE GLOBAL DEFAULT ABS _end 8: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses 9: 0000000000000000 544 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2 (2) 10: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ Symbol table '.symtab' contains 127 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 40000000000001c8 0 SECTION LOCAL DEFAULT 1 2: 40000000000001e0 0 SECTION LOCAL DEFAULT 2 3: 4000000000000200 0 SECTION LOCAL DEFAULT 3 4: 4000000000000240 0 SECTION LOCAL DEFAULT 4 5: 4000000000000348 0 SECTION LOCAL DEFAULT 5 6: 40000000000003d8 0 SECTION LOCAL DEFAULT 6 7: 40000000000003f0 0 SECTION LOCAL DEFAULT 7 8: 4000000000000410 0 SECTION LOCAL DEFAULT 8 9: 4000000000000440 0 SECTION LOCAL DEFAULT 9 10: 40000000000004a0 0 SECTION LOCAL DEFAULT 10 11: 40000000000004e0 0 SECTION LOCAL DEFAULT 11 12: 40000000000005e0 0 SECTION LOCAL DEFAULT 12 13: 4000000000000b00 0 SECTION LOCAL DEFAULT 13 14: 4000000000000b40 0 SECTION LOCAL DEFAULT 14 15: 4000000000000b60 0 SECTION LOCAL DEFAULT 15 16: 4000000000000bd0 0 SECTION LOCAL DEFAULT 16 17: 4000000000000ce0 0 SECTION LOCAL DEFAULT 17 18: 6000000000000db8 0 SECTION LOCAL DEFAULT 18 19: 6000000000000dd0 0 SECTION LOCAL DEFAULT 19 20: 6000000000000dd8 0 SECTION LOCAL DEFAULT 20 21: 6000000000000de0 0 SECTION LOCAL DEFAULT 21 22: 6000000000000fc0 0 SECTION LOCAL DEFAULT 22 23: 6000000000000fd0 0 SECTION LOCAL DEFAULT 23 24: 6000000000000fe0 0 SECTION LOCAL DEFAULT 24 25: 6000000000000fe8 0 SECTION LOCAL DEFAULT 25 26: 6000000000001040 0 SECTION LOCAL DEFAULT 26 27: 6000000000001080 0 SECTION LOCAL DEFAULT 27 28: 60000000000010a0 0 SECTION LOCAL DEFAULT 28 29: 60000000000010a8 0 SECTION LOCAL DEFAULT 29 30: 0000000000000000 0 SECTION LOCAL DEFAULT 30 31: 0000000000000000 0 SECTION LOCAL DEFAULT 31 32: 0000000000000000 0 SECTION LOCAL DEFAULT 32 33: 0000000000000000 0 SECTION LOCAL DEFAULT 33 34: 0000000000000000 0 SECTION LOCAL DEFAULT 34 35: 0000000000000000 0 SECTION LOCAL DEFAULT 35 36: 0000000000000000 0 SECTION LOCAL DEFAULT 36 37: 0000000000000000 0 SECTION LOCAL DEFAULT 37 38: 0000000000000000 0 SECTION LOCAL DEFAULT 38 39: 0000000000000000 0 SECTION LOCAL DEFAULT 39 40: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 41: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 42: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 43: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 44: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 45: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 46: 0000000000000000 0 FILE LOCAL DEFAULT ABS <command line> 47: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 48: 0000000000000000 0 FILE LOCAL DEFAULT ABS <command line> 49: 0000000000000000 0 FILE LOCAL DEFAULT ABS <built-in> 50: 0000000000000000 0 FILE LOCAL DEFAULT ABS abi-note.S 51: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 52: 0000000000000000 0 FILE LOCAL DEFAULT ABS abi-note.S 53: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 54: 0000000000000000 0 FILE LOCAL DEFAULT ABS abi-note.S 55: 0000000000000000 0 FILE LOCAL DEFAULT ABS <command line> 56: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 57: 0000000000000000 0 FILE LOCAL DEFAULT ABS <command line> 58: 0000000000000000 0 FILE LOCAL DEFAULT ABS <built-in> 59: 0000000000000000 0 FILE LOCAL DEFAULT ABS abi-note.S 60: 0000000000000000 0 FILE LOCAL DEFAULT ABS init.c 61: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 62: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 63: 0000000000000000 0 FILE LOCAL DEFAULT ABS initfini.c 64: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 65: 0000000000000000 0 FILE LOCAL DEFAULT ABS <command line> 66: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 67: 0000000000000000 0 FILE LOCAL DEFAULT ABS <command line> 68: 0000000000000000 0 FILE LOCAL DEFAULT ABS <built-in> 69: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 70: 4000000000000670 128 FUNC LOCAL DEFAULT 12 gmon_initializer 71: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 72: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 73: 0000000000000000 0 FILE LOCAL DEFAULT ABS initfini.c 74: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 75: 0000000000000000 0 FILE LOCAL DEFAULT ABS <command line> 76: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 77: 0000000000000000 0 FILE LOCAL DEFAULT ABS <command line> 78: 0000000000000000 0 FILE LOCAL DEFAULT ABS <built-in> 79: 0000000000000000 0 FILE LOCAL DEFAULT ABS /build/buildd/glibc-2.3.2 80: 0000000000000000 0 FILE LOCAL DEFAULT ABS auto-host.h 81: 0000000000000000 0 FILE LOCAL DEFAULT ABS <command line> 82: 0000000000000000 0 FILE LOCAL DEFAULT ABS <built-in> 83: 6000000000000fc0 0 NOTYPE LOCAL DEFAULT 22 __CTOR_LIST__ 84: 6000000000000fd0 0 NOTYPE LOCAL DEFAULT 23 __DTOR_LIST__ 85: 6000000000000fe0 0 NOTYPE LOCAL DEFAULT 24 __JCR_LIST__ 86: 6000000000001088 8 OBJECT LOCAL DEFAULT 27 dtor_ptr 87: 40000000000006f0 128 FUNC LOCAL DEFAULT 12 __do_global_dtors_aux 88: 4000000000000770 128 FUNC LOCAL DEFAULT 12 __do_jv_register_classes 89: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c 90: 6000000000001090 4 OBJECT LOCAL DEFAULT 27 i 91: 0000000000000000 0 FILE LOCAL DEFAULT ABS function.c 92: 6000000000001098 4 OBJECT LOCAL DEFAULT 27 i 93: 0000000000000000 0 FILE LOCAL DEFAULT ABS auto-host.h 94: 0000000000000000 0 FILE LOCAL DEFAULT ABS <command line> 95: 0000000000000000 0 FILE LOCAL DEFAULT ABS <built-in> 96: 6000000000000fc8 0 NOTYPE LOCAL DEFAULT 22 __CTOR_END__ 97: 6000000000000fd8 0 NOTYPE LOCAL DEFAULT 23 __DTOR_END__ 98: 6000000000000fe0 0 NOTYPE LOCAL DEFAULT 24 __JCR_END__ 99: 6000000000000de0 0 OBJECT GLOBAL DEFAULT ABS _DYNAMIC 100: 4000000000000a70 144 FUNC GLOBAL HIDDEN 12 __do_global_ctors_aux 101: 6000000000000dd8 0 NOTYPE GLOBAL DEFAULT ABS __fini_array_end 102: 60000000000010a8 8 OBJECT GLOBAL HIDDEN 29 __dso_handle 103: 40000000000009a0 208 FUNC GLOBAL DEFAULT 12 __libc_csu_fini 104: 0000000000000000 176 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.2 105: 40000000000004a0 32 FUNC GLOBAL DEFAULT 10 _init 106: 4000000000000850 128 FUNC GLOBAL DEFAULT 12 function 107: 40000000000005e0 144 FUNC GLOBAL DEFAULT 12 _start 108: 6000000000001094 4 OBJECT GLOBAL DEFAULT 27 global 109: 6000000000000dd0 0 NOTYPE GLOBAL DEFAULT ABS __fini_array_start 110: 40000000000008d0 208 FUNC GLOBAL DEFAULT 12 __libc_csu_init 111: 600000000000109c 0 NOTYPE GLOBAL DEFAULT ABS __bss_start 112: 40000000000007f0 96 FUNC GLOBAL DEFAULT 12 main 113: 6000000000000dd0 0 NOTYPE GLOBAL DEFAULT ABS __init_array_end 114: 6000000000000dd8 0 NOTYPE WEAK DEFAULT 20 data_start 115: 4000000000000b00 32 FUNC GLOBAL DEFAULT 13 _fini 116: 0000000000000000 704 FUNC GLOBAL DEFAULT UND exit@@GLIBC_2.2 117: 600000000000109c 0 NOTYPE GLOBAL DEFAULT ABS _edata 118: 6000000000000fe8 0 OBJECT GLOBAL DEFAULT ABS _GLOBAL_OFFSET_TABLE_ 119: 60000000000010b0 0 NOTYPE GLOBAL DEFAULT ABS _end 120: 6000000000000db8 0 NOTYPE GLOBAL DEFAULT ABS __init_array_start 121: 6000000000001080 4 OBJECT GLOBAL DEFAULT 27 _IO_stdin_used 122: 60000000000010a0 8 OBJECT GLOBAL DEFAULT 28 __libc_ia64_register_back 123: 6000000000000dd8 0 NOTYPE GLOBAL DEFAULT 20 __data_start 124: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses 125: 0000000000000000 544 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_ 126: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ إليك بعض الأشياء التي يجب ملاحظتها: لاحظ طريقة بناء الملف القابل للتنفيذ السهلة. لاحظ وجود نوعين من جداول الرموز هما: dynsym وsymtab. سنشرح كيفية عمل رموز dynsym لاحقًا، ولكن لاحظ أن بعضها يحمل الرمز @. لاحظ الرموز العديدة المُضمَّنة من ملفات الكائنات الإضافية، حيث يبدأ الكثير منها بالرمز __ لتجنب التعارض مع الأسماء التي يختارها المبرمج. اقرأ واختر الرموز التي ذكرناها سابقًا من ملفات الكائنات واكتشف إن تغيرت بأيّ شكل من الأشكال. ترجمة -وبتصرُّف- للقسم A practical example من فصل The Toolchain من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال السابق: كيفية إنشاء ملف قابل للتنفيذ Executable File من شيفرة برمجية مصدرية Source Code المقال التالي: مفهوم الربط الديناميكي Dynamic Linking في معمارية الحاسوب كيفية تصريف وبناء البرامج المكتوبة بلغة Cpp أنظمة التصريف المستخدمة لبناء البرامج المكتوبة بلغة Cpp وأهم أخطاء عملية البناء
  6. تشير إدارة الضبط Configuration Management -أو CM اختصارًا- إلى عملية المعالجة المنتظمة للتغييرات على النظام بطريقة تحافظ على التكامل بمرور الوقت، ويُستخدَم هذا المصطلح على نطاق واسع للإشارة إلى إدارة ضبط الخادم بالرغم من أن هذه العملية لم تنشأ في مجال تقانة المعلومات. تلعب عملية الأتمتة دورًا أساسيًا في إدارة ضبط الخادم، وهي الآلية المُستخدَمة لجعل الخادم يصل إلى الحالة المرغوبة، والتي حدَّدتها مسبقًا سكربتات الإعداد المسبق Provisioning Scripts باستخدام لغة وميزات معينة خاصة بالأداة. تُعَد الأتمتة جوهر إدارة ضبط الخوادم، لذا من الشائع الإشارة إلى أدوات إدارة الضبط بأنها أدوات الأتمتة Automation Tools أو أدوات أتمتة تقانة المعلومات IT Automation Tools. يوجد مصطلح شائع آخر يُستخدَم لوصف ميزات الأتمتة التي تقدّمها أدوات إدارة الضبط وهو تنسيق الخادم Server Orchestration أو تنسيق تقانة المعلومات IT Orchestration، لأن هذه الأدوات قادرة على إدارة خادم أو مئات الخوادم من جهاز تحكم مركزي، وقد فصلنا الفرق بينهما في مقال الفرق بين التنسيق والأتمتة فارجع إليه لمزيد من التفاصيل. هناك عدد من أدوات إدارة الضبط المتاحة في السوق مثل: Puppet Ansible Chef Salt حيث ستتميز كل أداة بخصائصها وستعمل بطرق مختلفة، إلا أن لها الغرض نفسه، وهو التأكد من أن حالة النظام تتطابق مع الحالة التي توضحها سكربتات الإعداد المسبق. هذا المقال جزء من سلسلة حول إدارة ضبط الخوادم، وإليك روابط فصول السلسلة: مدخل إلى إدارة ضبط الخوادم Configuration Management مبادئ إدارة ضبط الخوادم Configuration Management: كتابة دليل تشغيل الأداة Ansible مبادئ إدارة ضبط الخوادم Configuration Management: كتابة ملفات البيان Manifests للأداة Puppet مبادئ إدارة ضبط الخوادم Configuration Management: كتابة الوصفات Recipes في الأداة Chef الفوائد التي تجنيها الخوادم من إدارة الضبط يتطلب استخدام إدارة الضبط تخطيطًا أوليًا وجهدًا أكثر من إدارة النظام اليدوية، ولكن ستتحسّن بنى الخادم التحتية بأكملها باستثناء البسيطة منها من خلال الفوائد التي توفرها إدارة الضبط، حيث سنوضح فيما يلي بعضًا من هذه الفوائد. الإعداد المسبق السريع للخوادم الجديدة يمكن لأداة إدارة الضبط أتمتة معظم -إن لم يكن كل- عملية الإعداد المسبق نيابة عنك عند نشر خادم جديد، إذ تجعل الأتمتة عملية الإعداد المسبق أسرع وأكثر كفاءة لأنها تتيح أداء المهام الشاقة بصورة أسرع وأدق من أيّ إنسان يطبّقها، حيث يمكن أن يستغرق نشر خادم الويب يدويًا مثلًا ساعات بالموازنة مع بضع الدقائق التي تحتاجها إدارة أو أتمتة الضبط حتى مع وجود التوثيق الصحيح والشامل. التعافي السريع من الأحداث العصيبة تأتي ميزة أخرى مع الإعداد المسبق السريع، وهي التعافي السريع من الأحداث العصيبة، فإذا أصبح الخادم غير متصل بسبب ظروف غير معروفة، فقد يستغرق الأمر عدة ساعات لفحص النظام بصورة صحيحة ومعرفة ما حدث، حيث يكون في مواقف مشابهة نشرُ خادم بديل هو الطريقة الأكثر أمانًا لإعادة خدماتك للعمل أثناء إجراء فحص مفصّل على الخادم المتأثر، ويمكن تطبيق ذلك بطريقة سريعة وموثوقة من خلال إدارة الضبط والأتمتة. لا مزيد من الخوادم ذات الإدارة اليدوية Snowflake تبدو للوهلة الأولى إدارة النظام اليدوية طريقة سهلة لنشر الخوادم وإصلاحها بسرعة ولكن لها سلبياتها، إذ يصبح بمرور الوقت من الصعب معرفة ما هو مُثبَّتٌ على الخادم وما هي التغييرات التي أجريت عندما لا تكون العملية مؤتمتة. يمكن للإصلاحات اليدوية السريعة Hotfixes وتعديلات الضبط وتحديثات البرمجيات أن تحوّل الخوادم إلى خوادم Snowflake فريدة تصعب إدارتها وتكرارها. تُوثَّق جميع الإجراءات اللازمة لإحضار خادم جديد أو تحديث خادم موجود مسبقًا في سكربتات الإعداد المسبق باستخدام أداة إدارة الضبط. التحكم في الإصدارات لبيئة الخادم سيكون لديك القدرة على تطبيق العديد من الأدوات وطرق سير العمل التي تستخدمها في شيفرة البرمجيات على بيئة الخادم بمجرد ترجمة ضبط خادمك إلى مجموعة من سكربتات الإعداد المسبق. يمكن استخدام أدوات التحكم في الإصدارات مثل جيت Git لتتبّع التغييرات التي أُجريت على الإعداد المسبق ولإبقاء فروع الإصدارات القديمة من السكربتات منفصلةً، ويمكنك استخدام التحكم في الإصدارات لتنفيذ سياسة مراجعة الشيفرة البرمجية لسكربتات الإعداد المسبق، إذ يجب إرسال أي تغييرات بوصفها طلبَ سحب ويجب أن يوافق قائد المشروع عليها قبل قبولها. ستضيف هذه الممارسة تناسقًا إضافيًا إلى إعداد بنيتك التحتية. البيئات المكررة تسهّل إدارة الضبط تكرار البيئات مع البرمجيات والضبط نفسه، مما يمكّنك من بناء نظام بيئي متعدد المراحل بفعالية مع خوادم الإنتاج والتطوير والاختبار، ويمكنك استخدام الآلات الافتراضية المحلية للتطوير، والتي أُنشِئت باستخدام سكربتات الإعداد المسبق نفسها. ستعمل هذه الممارسة على تقليل المشاكل التي تسببها اختلافات البيئة التي تحدث بصورة متكررة عند نشر التطبيقات في بيئة الإنتاج أو عند مشاركتها بين زملاء العمل الذين يستخدمون إعدادات أجهزة مختلفة (مثل نظام تشغيل و/أو إصدارات برمجيات و/أو ضبط مختلف). نظرة عامة على أدوات إدارة الضبط لكل أداة إدارة ضبط CM مصطلحاتها الخاصة وفلسفتها ونظامها البيئي، ولكنها تشترك في العديد من الخصائص ولديها مفاهيم متشابهة، إذ تستخدم معظم أدوات إدارة الضبط نموذج متحكِّم أو رئيسي ونموذج عقدة أو وكيل، حيث يوجّه المتحكِّم ضبط العقد بناءً على سلسلة من التعليمات أو المهام المُحدَّدة في سكربتات الإعداد المسبق. سنوضّح فيما يلي الميزات الأكثر شيوعًا الموجودة في معظم أدوات إدارة ضبط الخوادم. إطار عمل مؤتمت توفر كل أداة إدارة ضبط CM صياغة محددة ومجموعة من الميزات التي يمكنك استخدامها لكتابة سكربتات الإعداد المسبق، إذ تحتوي معظم الأدوات على ميزات تجعل لغتها تشبه لغات البرمجة التقليدية ولكن بطريقة مبسطة، حيث تُعَد المتغيرات والحلقات والتعليمات الشرطية ميزات شائعة متوفرة لتسهيل إنشاء سكربتات إعداد مسبق أكثر تنوعًا. السلوك الراسخ Idempotent Behavior تتعقّب أدوات إدارة الضبط حالة الموارد لتجنب تكرار المهام التي جرى تنفيذها مسبقًا، فإذا ثُبِّتت الحزمة، فلن تحاول الأداة تثبيتها مرة أخرى. الهدف هو أن يصل النظام (أو يحتفظ) بالحالة المطلوبة بعد تشغيل كل إعداد مسبق حتى لو شغّلته عدة مرات. تتمتع هذه الأدوات بسلوك راسخ وهذا ما يميزها، ولكن لا يُفرَض هذا السلوك بالضرورة في جميع الحالات. معلومات النظام توفر أدوات إدارة الضبط معلومات مفصلة حول النظام المُعَد مسبقًا، وهذه البيانات متاحة من خلال المتغيرات العامة وتُعرَف باسم الحقائق Facts، والتي تشمل أشياءً مثل واجهات الشبكة وعناوين IP ونظام التشغيل والتوزيع. ستوفر كل أداة مجموعة مختلفة من الحقائق، ويمكن استخدامها لجعل سكربتات الإعداد المسبق والقوالب أكثر تكيفًا مع أنظمة متعددة. نظام القوالب توفر معظم أدوات إدارة الضبط CM نظام قوالب مبني مسبقًا يمكن استخدامه لتسهيل إعداد ملفات الضبط والخدمات، حيث تدعم القوالب عادةً المتغيرات والحلقات والتعليمات الشرطية التي يمكن استخدامها لتحقيق أقصى قدر من التنوع، فمثلًا يمكنك استخدام قالب لإعداد مضيف وهمي جديد بسهولة ضمن خادم أباتشي Apache مع إعادة استخدام القالب نفسه لعمليات تثبيت خوادم متعددة. يجب أن يحتوي القالب على عناصر بديلة للقيم التي يمكن أن تتغير من مضيف إلى آخر مثل NameServer و DocumentRoot بدلًا من وجود قيم ثابتة وساكنة فقط. التوسع Extensibility يمكن أن تكون سكربتات الإعداد المسبق متخصصة جدًا في تلبية احتياجات ومتطلبات خادم معين، ولكن هناك العديد من الحالات التي يكون لديك فيها إعدادات خادم مماثلة أو أجزاء من إعداد يمكن مشاركتها بين خوادم متعددة، إذ توفر معظم أدوات الإعداد المسبق طرقًا يمكنك من خلالها بسهولة إعادة استخدام ومشاركة أجزاء أصغر من الإعداد المسبق بوصفها وحدات أو إضافات. يمكن بسهولة العثور على الوحدات والإضافات التابعة لجهات خارجية على الإنترنت، وخاصة لإعدادات الخادم الشائعة مثل تثبيت خادم ويب PHP، إذ تميل أدوات إدارة الضبط CM إلى إنشاء مجتمع قوي حولها وتشجع المستخدمين على مشاركة توسّعاتها المخصصة. يمكن أن يوفر لك استخدام التوسّعات التي يوفرها المستخدمون الآخرون الكثير من الوقت، ويمثل طريقة ممتازة لتعلم كيفية حل المستخدمين الآخرين للمشاكل الشائعة باستخدام الأداة التي تختارها. اختيار أداة إدارة الضبط هناك العديد من أدوات إدارة الضبط CM المتاحة في السوق، ولكلٍ منها مجموعة مختلفة من الميزات ومستويات تعقيد مختلفة، ومن هذه الأدوات Chef و Ansible و Puppet. التحدي الأول هو اختيار أداة مناسبة لاحتياجاتك، فهناك بعض الأشياء التي يجب أن تأخذها في الحسبان قبل الاختيار وسنوضّحها فيما يلي. تعقيد البنية التحتية تتطلب معظم أدوات إدارة الضبط حدًا أدنى من التسلسل الهرمي يتكون من جهاز متحكِّم وعقدة سيديرها المتحكِّم، فمثلًا تتطلب أداة Puppet تثبيت تطبيق وكيل Agent على كل عقدة وتثبيت تطبيق رئيسي Master على جهاز المتحكِّم. بينما تتمتع أداة Ansible ببنية لامركزية لا تتطلب تثبيت برامج إضافية على العقد، ولكنها تعتمد على بروتوكول SSH لتنفيذ مهام الإعداد المسبق. يمكن أن تبدو البنية التحتية المبسطة مناسبة بصورة أفضل للمشاريع الأصغر، ولكن يجب مراعاة جوانب أخرى مثل قابلية التوسع والأمان، والتي يمكن ألّا تفرضها الأداة. يمكن أن تحتوي بعض الأدوات على مزيدٍ من المكونات والأجزاء المتحركة، مما يزيد من تعقيد بنيتك التحتية، ويؤثر على مسار التعلم ويمكن أن يزيد من تكلفة التنفيذ الإجمالية. مسار التعلم توفر أدوات إدارة الضبط CM صياغة مخصصة، حيث تستخدم في بعض الأحيان لغة مجال محدَّد Domain Specific Language -أو DSL اختصارًا، ومجموعة من الميزات التي تشكل إطار عملها للأتمتة. تتطلب بعض الأدوات إتقان مسار تعليمي أعلى كما هو الحال مع لغات البرمجة التقليدية، ويمكن أن تؤثر متطلبات البنية التحتية على مدى تعقيد الأداة ومدى سرعة قدرتك على رؤية عائد استثمارك. التكلفة تقدم معظم أدوات إدارة الضبط CM إصدارات مجانية أو مفتوحة المصدر مع اشتراكات مدفوعة للحصول على ميزات وخدمات متقدمة، حيث تحتوي بعض الأدوات على قيود أكثر من غيرها، لذلك يمكن أن تدفع مقابل هذه الخدمات اعتمادًا على احتياجاتك الخاصة وكيفية نمو بنيتك التحتية. يجب أن تفكر في التدريب بوصفه تكلفة إضافية محتملة، ليس فقط من الناحية المالية، ولكن أيضًا فيما يتعلق بالوقت الذي سيكون ضروريًا ليواكب فريقك سرعة تطور الأداة التي ستختارها في النهاية. الأدوات المتقدمة تقدم معظم الأدوات خدمات مدفوعة يمكن أن تشمل الدعم والتوسّعات والأدوات المتقدمة كما ذكرنا سابقًا، لذا يجب أن تحلل احتياجاتك المحددة وحجم بنيتك التحتية وما إذا كانت هناك حاجة لاستخدام هذه الخدمات أم لا. تُعَد لوحات الإدارة مثلًا خدمة شائعة تقدمها هذه الأدوات، ويمكن أن تسهّل بصورة كبيرة عملية إدارة ومراقبة جميع خوادمك من نقطة مركزية، إذ يجب أن تفكر في خيارات الضرورة المستقبلية المحتمَلة حتى إن لم تكن بحاجة إلى مثل هذه الخدمات حاليًا. المجتمع والدعم يمكن أن يكون المجتمع القوي والمرحِّب موردًا مهمًا للدعم والتوثيق، حيث يكون المستخدمون سعيدين عادةً بمشاركة معرفتهم والتوسعات التي يجرونها (الوحدات والإضافات وسكربتات الإعداد المسبق) مع مستخدمين آخرين، مما يكون مفيدًا في تسريع مسار تعلمك وتجنب التكاليف الإضافية للدعم أو التدريب المدفوع. نظرة عامة إلى أدوات إدارة الضبط الشائعة يمنحك الجدول التالي نظرة عامة سريعة على الاختلافات الرئيسية بين ثلاث من أكثر أدوات إدارة الضبط شيوعًا المتوفرة في السوق اليوم وهي: Ansible و Puppet و Chef. أداة Ansible أداة Puppet أداة Chef لغة كتابة السكربتات لغة YAML لغة DSL مستندة إلى لغة روبي Ruby لغة روبي البنية التحتية يطبّق الجهاز المتحكِّم Controller الضبط على العقد باستخدام بروتوكول SSH يزامن جهاز الأداة Puppet الرئيسي Master الضبط على عقد Puppet تدفع محطات عمل الأداة Chef الضبط إلى خادم Chef الذي سيحدِّث عقد Chef تتطلب برمجيات متخصصة للعقد لا نعم نعم يوفر نقطة تحكم مركزية لا، إذ يمكن لأيّ حاسوب أن يكون متحكمًا نعم، باستخدام جهاز Puppet الرئيسي Master نعم، باستخدام خادم Chef مصطلحات السكربت دليل التشغيل Playbook/الأدوار Roles ملفات البيان Manifests/الوحدات Modules الوصفات Recipes/الأدلة Cookbooks ترتيب تنفيذ المهام تسلسلي غير تسلسلي تسلسلي الخطوات التالية رأينا حتى الآن كيفية عمل إدارة ضبط الخوادم، وما يجب مراعاته عند اختيار أداة لبناء بنية إدارة الضبط التحتية، وسنتعرّف في المقالات اللاحقة عمليًا على ثلاث أدوات شائعة لإدارة الضبط هي: Ansible و Puppet و Chef، حيث سنستخدم مثالًا بسيطًا لإعداد الخادم الذي يجب أن يكون مؤتمتًا بالكامل بواسطة كل أداة من هذه الأدوات لمنحك فرصة للموازنة بين هذه الأدوات بنفسك، إذ يتكون هذا الإعداد من خادم أوبنتو 18.04 يشغّل أباتشي Apache لاستضافة صفحة ويب بسيطة. الخلاصة يمكن لإدارة الضبط تحسين تكامل الخوادم بصورة كبيرة بمرور الوقت من خلال توفير إطار عمل لأتمتة العمليات وتتبع التغييرات التي أُجريت على بيئة النظام. سنرى في المقال التالي كيفية تطبيق استراتيجية إدارة الضبط عمليًا باستخدام أداة Ansible. ترجمة -وبتصرُّف- للمقال An Introduction to Configuration Management لصاحبته Erika Heidi. اقرأ أيضًا دليل إعداد خادم ويب محلي خطوة بخطوة مراقبة وإدارة عدة خوادم لينكس عبر واجهة متصفح مرئية باستخدام Cockpit
  7. ناقشنا حتى الآن في سلسلة مدخل لعلم الحاسوب كيفية تحميل البرنامج في الذاكرة الوهمية، وسنبدأ في هذا المقال بالتعرف على عملية يتعقّبها نظام التشغيل ويتفاعل معها باستخدام استدعاءات النظام هي عملية التصريف Compiling. سنتعرّف في هذا المقال على الخطوات الثلاث لإنشاء ملف قابل للتنفيذ، ولكن سنبدأ أولًا بالتعرّف على الفرق بين البرامج المُصرَّفة Compiled Programs والبرامج المُفسَّرة Interpreted Programs. البرامج المُصرَّفة Compiled Programs والبرامج المُفسَّرة Interpreted programs يجب أن يكون البرنامج الذي يمكن تحميله مباشرةً في الذاكرة بصيغة ثنائية binary format، حيث تُسمَّى عملية تحويل الشيفرة البرمجية المكتوبة بلغةٍ مثل لغة C إلى ملف ثنائي جاهز للتنفيذ بعملية التصريف التي تُطبَّق باستخدام مصرِّف Compiler، والمثال الأكثر انتشارًا هو المصرِّف gcc. للبرامج المُصرَّفة بعض العيوب في تطوير البرمجيات الحديثة، إذ يجب استدعاء المصرِّف لإعادة إنشاء الملف القابل للتنفيذ في كل مرة يُجري فيها المطور تعديلًا -لو بسطيًا- وفي المقابل يمكن منطقيًا وبناءً على ذلك تصميم برنامجٍ مُصرَّف يمكنه قراءة برنامج آخر وتنفيذ شيفرته البرمجية سطرًا سطرًا، ونسمي هذا النوع من البرامج المُصرَّفة بالبرامج المُفسَّرة Interpreter لأنها تفسّر كل سطر من ملف الدخل وتنفّذه بوصفه شيفرة برمجية، بحيث لا تكون هناك حاجة لتصريف البرنامج وستظهر أي تعديلات جديدة مضافة في المرة التالية التي يشغّل فيها المفسِّر الشيفرة البرمجية. تعمل البرامج المفسَّرة عادةً بصورة أبطأ من نظيرتها المُصرَّفة، حيث يمكن مصادفة حِمل البرنامج في قراءة وتفسير الشيفرة البرمجية مرةً واحدة فقط في البرامج المُصرَّفة، بينما يصادف البرنامج المفسَّر هذا الحِمل في كل مرة يُشغَّل فيها. لكن تمتلك اللغات المفسَّرة العديد من الجوانب الإيجابية، حيث تعمل العديد من اللغات المفسرة فعليًا في آلة افتراضية virtual machine مُجرَّدة من العتاد الأساسي. تُعَد لغتا بايثون Python و Perl 6 من اللغات التي تستخدم آلةً افتراضية تفسّر الشيفرة البرمجية. الآلات الافتراضية Virtual Machines يعتمد البرنامج المُصرَّف كليًا على عتاد الآلة التي يُصرَّف من أجلها، إذ يجب أن يكون هذا العتاد قادرًا على نسخ البرنامج في الذاكرة وتنفيذه، حيث تُعَد الآلة الافتراضية Virtual Machine تجريدًا برمجيًا للعتاد. تستخدم لغة جافا Java مثلًا نهجًا هجينًا يجمع بين التصريف والتفسير، فهي لغة مُصرَّفة جزئيًا ومُفسَّرة جزئيًا. تُصرَّف شيفرة جافا في برنامج يعمل ضمن آلة جافا الافتراضية Java Virtual Machine أو يشار إليها JVM اختصارًا، وبالتالي يمكن تشغيل البرنامج المُصرَّف على أيّ عتاد يحتوي على آلة JVM خاصة به، أي يمكنك أن تكتب شيفرتك البرمجية مرة واحدة وتشغّلها في أيّ مكان. بناء ملف قابل للتنفيذ هناك ثلاث خطوات منفصلة تتضمنها عملية إنشاء ملف قابل للتنفيذ عندما نتحدث عن المُصرِّفات وهذه الخطوات هي: التصريف Compiling التجميع Assembling الربط Linking تسمَّى جميع المكونات المتضمنة في هذه العملية بسلسلة الأدوات Toolchain، إذ تكون هذه الأدوات على شكل سلسلة بحيث يكون خرج إحداها دخلًا للأخرى حتى الوصول إلى الخرج النهائي. يأخذ كل رابط في السلسلة الشيفرةَ البرمجية تدريجيًا بحيث تكون أقرب إلى كونها شيفرة برمجية ثنائية مناسبةً للتنفيذ. التصريف Compiling تتمثل الخطوة الأولى لتصريف ملف مصدري إلى ملف قابل للتنفيذ في تحويل الشيفرة البرمجية من لغة عالية المستوى يفهمها الإنسان إلى شيفرة تجميع Assembly Code تعمل مباشرةً مع التعليمات والمسجلات التي يوفرها المعالج. تُعَد عملية التصريف أكثر الخطوات تعقيدًا لعدة أسباب أولها أنه لا يمكن التنبؤ بتصرفات البشر، فلديهم شيفرتهم البرمجية الخاصة بأشكال مختلفة. يهتم المصرِّف بالشيفرة البرمجية الفعلية فقط، ولكن يحتاج البشر لأشياء إضافية مثل التعليقات والمسافات البيضاء (الفراغات ومسافات الجدولة Tab والمسافات البادئة وما إلى ذلك) لفهم هذه الشيفرة البرمجية. تسمى العملية التي يتخذها المصرِّف لتحويل الشيفرة البرمجية التي يكتبها الإنسان إلى تمثيلها الداخلي بعملية التحليل Parsing. هناك خطوة قبل تحليل الشيفرة البرمجية في الشيفرة المكتوبة بلغة C، حيث تسمَّى هذه الخطوة بالمعالجة المُسبَقة أو التمهيدية يقوم بها المعالج المسبق Pre-processor، وهو عبارة عن برنامج لاستبدال النصوص، حيث يُستبدَل مثلًا المتغير variable المُصرَّح عنه بالشكل ‎#define variable text بالنص text، ثم تُمرَّر هذه الشيفرة البرمجية المعالَجة مسبقًا إلى المصرِّف. الصياغة لكل لغة برمجة صياغة معينة تمثّل قواعد اللغة، بحيث يعرف المبرمج والمصرِّف قواعد الصياغة ليفهما بعضهما البعض ويسير كل شيء على ما يرام. ينسى البشر القواعد أو يكسرونها في أغلب الأحيان، مما يجعل المصرِّف غير قادر على فهم ما يقصده المبرمج، فإن لم تضع قوس الإغلاق لشرط if مثلًا، فلن يعرف المصرِّف مكان الشرط فعليًا. تُوصَف الصياغة في صيغة باكوس نور Backus-Naur Form -أو BNF اختصارًا- في أغلب الأحيان، وهي لغة يمكنك من خلالها وصف اللغات، والشكل الأكثر شيوعًا منها هو صيغة باكور نور الموسَّعة Extended Backus-Naur Form -أو EBNF اختصارًا- التي تسمح ببعض القواعد الإضافية الأكثر ملاءمة للغات الحديثة. توليد شيفرة التجميع Assembly Generation وظيفة المصرِّف هي ترجمة لغة عالية المستوى higher level language إلى شيفرة تجميع مناسبة للهدف من التصريف، فلكل معمارية مجموعة تعليمات مختلفة وأعداد مختلفة من المسجلات وقواعد مختلفة للتشغيل الصحيح. المحاذاة Alignment تُعَد محاذاة المتغيرات في الذاكرة أمرًا مهمًا للمصرِّفات، إذ يحتاج مبرمجو الأنظمة أن يكونوا على دراية بقيود المحاذاة لمساعدة المصرِّف على إنشاء أكثر شيفرة برمجية فعّالة ممكنة. لا تستطيع وحدات المعالجة المركزية CPU تحميل قيمة في المسجل من موقع ذاكرة عشوائي، إذ يتطلب ذلك أن تحاذي المتغيرات حدودًا معينة. يمكننا أن نرى في الشكل السابق كيفية تحميل قيمة 32 بتًا (4 بايتات) في مسجل على آلة تتطلب محاذاة بمقدار 4 بايتات للمتغيرات. يمكن تحميل المتغير الأول في المسجل مباشرةً، حيث يقع بين حدود 4 بايتات، ولكن يجتاز المتغير الثاني حدود 4 بايتات، مما يعني أنه ستكون هناك حاجة إلى عمليتي تحميل على الأقل للحصول على المتغير في مسجل واحد إحداهما للنصف السفلي أولًا ثم النصف العلوي. يمكن لبعض المعماريات مثل معمارية x86 التعاملَ مع عمليات التحميل التي تكون دون محاذاة في العتاد مع انخفاض في الأداء، حيث يطبّق العتاد العمل الإضافي للحصول على القيمة في المسجل، بينما لا يمكن أن يكون هناك انتهاك لقواعد المحاذاة في المعماريات الأخرى وسترفع استثناءً يكتشفه نظام التشغيل الذي يتعين عليه بعد ذلك تحميل المسجل يدويًا على أجزاء، مما يتسبب في مزيد من الحِمل. حاشية البنية Structure Padding يجب أن يأخذ المبرمجون المحاذاة في الحسبان خاصةً عند إنشاء البنى struct، حيث يمكن للمبرمجين في بعض الأحيان أن يتسببوا في سلوك دون المستوى الأمثل، بينما يعرف المصرِّف قواعد المحاذاة للمعماريات التي يبنيها. ينص معيار C99 على أن البنى ستُرتَّب في الذاكرة بالترتيب المُحدَّد في التصريح نفسه، وستكون جميع العناصر بالحجم نفسه في مصفوفة من البنى. إليك مثال عن حاشية بنية Struct Padding: $ cat struct.c #include <stdio.h> struct a_struct { char char_one; char char_two; int int_one; }; int main(void) { struct a_struct s; printf("%p : s.char_one\n" \ "%p : s.char_two\n" \ "%p : s.int_one\n", &s.char_one, &s.char_two, &s.int_one); return 0; } $ gcc -o struct struct.c $ gcc -fpack-struct -o struct-packed struct.c $ ./struct 0x7fdf6798 : s.char_one 0x7fdf6799 : s.char_two 0x7fdf679c : s.int_one $ ./struct-packed 0x7fcd2778 : s.char_one 0x7fcd2779 : s.char_two 0x7fcd277a : s.int_one أنشأنا في المثال السابق بنية تحتوي على بايتين من النوع char متبوعين بعدد صحيح بحجم 4 بايتات من النوع int. يضيف المصرِّف حاشية للبنية البنية كما يلي: نوجّه في المثال السابق المصرِّف إلى عدم حشو البنى، وبالتالي يمكننا أن نرى أن العدد الصحيح يبدأ مباشرةً بعد قيمتين من النوع char. محاذاة خط الذاكرة المخبئية Cache line alignment تحدثنا سابقًا عن استخدام الأسماء البديلة في الذاكرة المخبئية، وكيف يمكن ربط عدة عناوين مع سطر الذاكرة المخبئية نفسه. يجب أن يتأكد المبرمجون من أنهم لا يتسببون في ارتداد Bouncing في خطوط الذاكرة المخبئية عندما يكتبون برامجهم. يحدث هذا الموقف عندما يصل البرنامج باستمرار إلى منطقتين من الذاكرة ترتبطان مع خط الذاكرة المخبئية نفسه، مما يؤدي إلى هدر هذا الخط، حيث يُحمَّل ويُستخدَم لفترة قصيرة ثم يجب إزالته وتحميل خط الذاكرة المخبئية الآخر في المكان نفسه من الذاكرة المخبئية. يؤدي تكرار هذا الموقف إلى تقليل الأداء بصورة كبيرة، ولكن يمكن تخفيفه من خلال تنظيم البيانات المتعارضة بطرق مختلفة لتجنب تعارض خطوط الذاكرة المخبئية. إحدى الطرق الممكنة لاكتشاف هذا النوع من المواقف هي التشخيص Profiling الذي يمثّل مراقبة الشيفرة البرمجية لتحليل مساراتها التي يمكن استخدامها والمدة المُستغرقَة لتنفيذها. يمكن للمصرِّف باستخدام التحسين المُوجَّه بالتشخيص Profile Guided Optimization -أو PGO اختصارًا- وضعَ بتات إضافية خاصة من الشيفرة البرمجية في أول ثنائية binary يبنيها ويشغّلها ويسجّل الفروع المأخوذة منها وغير ذلك. يمكنك بعد ذلك إعادة تصريفها مع المعلومات الإضافية لإنشاء ثنائية binary مع أداء أفضل، وإلّا فيمكن للمبرمج أن ينظر إلى خرج عملية التشخيص ويكتشف مواقفًا أخرى مثل ارتداد خط الذاكرة المخبئية. المقايضة بين المساحة والسرعة يمكن المقايضة مع ما فعله المصرِّف سابقًا باستخدام ذاكرة إضافية لتحسين السرعة عند تشغيل شيفرتنا البرمجية. يعرف المصرِّف قواعد المعمارية ويمكنه اتخاذ قرارات بشأن أفضل طريقة لمحاذاة البيانات عن طريق مقايضة كميات صغيرة من الذاكرة المهدورة لزيادة الأداء أو للوصول إلى الأداء الصحيح فقط. لا يجب أبدًا -بصفتك مبرمجًا- وضع افتراضات حول طريقة ترتيب المصرِّف للمتغيرات والبيانات، لأنها لا تُعَد قابلةً للنقل، إذ يكون للمعماريات المختلفة قواعدٌ مختلفة ويمكن أن يتخذ المُصرِّف قرارات مختلفة بناءً على أوامر أو مستويات تحسين صريحة. وضع الافتراضات يجب أن تكون -بصفتك مبرمجًا بلغة C- على دراية بما يمكنك افتراضه بشأن ما سيفعله المصرِّف وما يمكن أن يكون متغيرًا. ذُكِر بالتفصيل ما يمكنك أن تفترضه بالضبط وما لا يمكنك افتراضه في معيار C99، حيث إذا كنت مبرمجًا بلغة C، فلا بد أن يكون التعرف على القواعد جديرًا بالعناء لتجنب كتابة شيفرة برمجية غير قابلة للنقل. إليك مثال عن محاذاة المكدس Stack Alignment: $ cat stack.c #include <stdio.h> struct a_struct { int a; int b; }; int main(void) { int i; struct a_struct s; printf("%p\n%p\ndiff %ld\n", &i, &s, (unsigned long)&s - (unsigned long)&i); return 0; } $ gcc-3.3 -Wall -o stack-3.3 ./stack.c $ gcc-4.0 -o stack-4.0 stack.c $ ./stack-3.3 0x60000fffffc2b510 0x60000fffffc2b520 diff 16 $ ./stack-4.0 0x60000fffff89b520 0x60000fffff89b524 diff 4 يمكننا أن نرى في المثال السابق المأخوذ من آلة إيتانيوم Itanium أن حاشية ومحاذاة المكدس تغيرت بصورة كبيرة بين إصدارات المصرِّف gcc، وهذا أمر متوقع ويجب على المبرمج مراعاته. كما يجب عليك التأكد من عدم وضع افتراضات حول حجم الأنواع أو قواعد المحاذاة. مفاهيم لغة C الخاصة بالمحاذاة هناك عدد من تسلسلات الشيفرة البرمجية الشائعة التي تتعامل مع المحاذاة، ويجب أن تضعها معظم البرامج في حساباتها. يمكن أن ترى "مفاهيم الشيفرة البرمجية" في العديد من الأماكن خارج النواة Kernel عند التعامل مع البرامج التي تعالج أجزاءً من البيانات بصيغة أو بأخرى، لذا فإن الأمر يستحق البحث. يمكننا أخذ بعض الأمثلة من نواة لينكس Linux kernel التي يتعين عليها في أغلب الأحيان التعامل مع محاذاة صفحات الذاكرة ضمن النظام. إليك مثال عن التعامل مع محاذاة الصفحات: [ include/asm-ia64/page.h ] /* * ‫يحدّد PAGE_SHIFT حجم صفحة النواة الفعلي */ #if defined(CONFIG_IA64_PAGE_SIZE_4KB) # define PAGE_SHIFT 12 #elif defined(CONFIG_IA64_PAGE_SIZE_8KB) # define PAGE_SHIFT 13 #elif defined(CONFIG_IA64_PAGE_SIZE_16KB) # define PAGE_SHIFT 14 #elif defined(CONFIG_IA64_PAGE_SIZE_64KB) # define PAGE_SHIFT 16 #else # error Unsupported page size! #endif #define PAGE_SIZE (__IA64_UL_CONST(1) << PAGE_SHIFT) #define PAGE_MASK (~(PAGE_SIZE - 1)) #define PAGE_ALIGN(addr) (((addr) + PAGE_SIZE - 1) & PAGE_MASK) يمكننا أن نرى في المثال السابق أن هناك عددًا من الخيارات المختلفة لأحجام الصفحات داخل النواة التي تتراوح من 4 كيلوبايتات إلى 64 كيلوبايت. يَُعد الماكرو PAGE_SIZE واضحًا إلى حد ما، فهو يعطي حجم الصفحة الحالي المحدّد ضمن النظام عن طريق انزياح قيمته 1 باستخدام رقم الانزياح المُعطَى، ويعادل ذلك 2n حيث n هو انزياح الصفحة PAGE_SHIFT. لدينا بعد ذلك تعريف قناع الصفحة PAGE_MASK الذي يسمح لنا بالعثور على تلك البتات الموجودة في الصفحة الحالية فقط، أي إزاحة offset العنوان في صفحته. التحسين Optimisation يريد المصرِّف بمجرد الحصول على تمثيل داخلي للشيفرة البرمجية إيجادَ أفضل خرج بلغة التجميع لدخل الشيفرة البرمجية المُحدَّد. هذه مشكلة كبيرة ومتنوعة وتتطلب معرفة كل شيء من الخوارزميات الفعالة المعتمَدة في علوم الحاسوب إلى المعرفة العميقة بالمعالج الذي ستعمل الشيفرة البرمجية عليه. هناك بعض التحسينات الشائعة التي يمكن أن ينظر إليها المصرِّف عند توليد الخرج، وهناك العديد والعديد من الاستراتيجيات لإنشاء الشيفرة البرمجية الأفضل، ويُعَد ذلك مجال بحث غني. يمكن للمصرِّف أن يرى في كثير من الأحيان أنه لا يمكن استخدام جزء معين من الشيفرة البرمجية، لذا يتركه لتحسين بنية لغة معينة وينتقل إلى شيء أصغر يوصل للنتيجة نفسها. فك الحلقات Unrolling Loops: إذا احتوت الشيفرة البرمجية على حلقة مثل حلقة for أو while وكان لدى المُصرِّف فكرة عن عدد المرات التي ستنفّذ فيها، فسيكون فك الحلقة أكثر فاعلية بحيث تُنفَّذ تسلسليًا، إذ تُكرَّر شيفرة الحلقة الداخلية لتنفيذها عدد المرات ذاك أخرى بدلًا من تنفيذ الجزء الداخلي من الحلقة ثم العودة إلى البداية لتكرار العملية. تزيد هذه العملية من حجم الشيفرة البرمجية، إذ يمكن أن تسمح للمعالج بتنفيذ التعليمات بفعالية، حيث يمكن أن تتسبب الفروع في تقليل كفاءة خط أنابيب التعليمات الواردة إلى المعالج. الدوال المضمنة Inlining Functions: يمكن وضع دوال مُضمَّنة لاستدعائها ضمن المستدعي callee، ويمكن للمبرمج تحديد ذلك للمصرِّف من خلال وضع الكلمة inline في تعريف الدالة، ويمكنك مقايضة حجم الشيفرة البرمجية بتسلسل تنفيذها من خلال ذلك. توقع الفرع Branch Prediction: إذا صادف الحاسوب تعليمة if، فهناك نتيجتان محتملتان إما صحيحة أو خاطئة. يريد المعالج الاحتفاظ بأنابيبه الواردة ممتلئة قدر الإمكان، لذا لا يمكنه انتظار نتيجة الاختبار قبل وضع الشيفرة البرمجية في خط الأنابيب، وبالتالي يمكن للمصرِّف أن يتنبأ بالطريقة التي يُحتمَل أن يسير بها الاختبار. هناك بعض القواعد البسيطة التي يمكن أن يستخدمها المصرِّف لتخمين هذه الأمور، فمثلًا لا يُحتمَل أن تكون التعليمة if (val == -1)‎ صحيحةً، لأن القيمة ‎-1 تشير عادةً إلى رمز خطأ ونأمل ألّا تُشغَّل هذه التعليمة كثيرًا. يمكن لبعض المصرِّفات تصريف البرنامج، وجعل المستخدم يشغّله ليلاحظ الطريق الذي تسير به الفروع في ظل ظروف واقعية، ويمكنه بعد ذلك إعادة تصريفه بناءً على ما شاهده. المجمع Assembler تبقى شيفرة التجميع التي أخرجها المصرِّف في صيغة يمكن أن يقرأها الإنسان إذا كنت على معرفة بتفاصيل شيفرة التجميع الخاصة بالمعالج. يُلقي المطورون في أغلب الأحيان نظرة خاطفة على خرج التجميع للتحقق يدويًا من أن الشيفرة البرمجية هي الأفضل أو لاكتشاف أخطاء المصرِّف، ويُعَد ذلك أكثر شيوعًا مما هو متوقع خاصةً عندما يكثِر المصرِّف من التحسينات. المجمِّع assembly هو عملية آلية لتحويل شيفرة التجميع إلى صيغة ثنائية. يحتفظ المجمّع بجدول كبير لكل تعليمة ممكنة ولنظيرها الثنائي الذي يسمى شيفرة العملية Op Code. يدمج المجمّع شيفرات العمليات مع المسجلات المحدَّدة في شيفرة التجميع لإنتاج ملف ثنائي بوصفه خرجًا. يُطلق على هذه الشيفرة بشيفرة التعليمات المُصرَّفة Object Code، وهي شيفرة غير قابلة للتنفيذ في هذه المرحلة، وتُعد مجرد تمثيل ثنائي للدخل الذي يمثل شيفرة برمجية مصدرية. يُفضَّل ألّا يضع المبرمج الشيفرة المصدرية بأكملها في ملفٍ واحد. الرابط Linker ستُقسَم في أغلب الأحيان الشيفرة البرمجية في برنامج كبير إلى ملفات متعددة لتكون الدوال ذات الصلة مع بعضها بعضًا. يمكن تصريف كل ملفٍ من هذه الملفات إلى شيفرة تعليمات مُصرَّفة ولكن هدفك النهائي هو إنشاء ملف قابل للتنفيذ. يجب أن يكون هناك طريقة ما لدمجها في ملف واحد قابل للتنفيذ، حيث نسمي هذه العملية بالربط Linking. لاحظ أنه لا يزال يجب ربط برنامجك بمكتبات نظام معينة للعمل بصورة صحيحة حتى إن كان برنامجك مناسبًا لملفٍ واحد، إذ يكون الاستدعاء printf مثلًا في مكتبة يجب دمجها مع ملفك القابل للتنفيذ ليعمل، لذا لا تزال هناك بالتأكيد عملية ربط تحدث لإنشاء ملفك القابل للتنفيذ بالرغم من أنه لا داعي للقلق صراحةً بشأن الربط في هذه الحالة. سنشرح فيما يلي بعض المصطلحات الأساسية لفهم عملية الربط. الرموز Symbols لجميع المتغيرات والدوال أسماء في الشيفرة المصدرية، إذ نشير إليها باستخدام هذه الأسماء. تتمثل إحدى طرق التفكير في تعليمة التصريح عن متغير int a في أنك تخبر المصرِّف بأن يحجز حيزًا من الذاكرة بحجم sizeof(int)‎، وبالتالي كلما استخدمت اسم المتغير a، فسيشير إلى هذه الذاكرة المخصَّصة، وكذلك الأمر بالنسبة للدالة التي تخبر المصرِّف بأن يحزّن هذه الشيفرة البرمجية في الذاكرة، ثم ينتقل إليها وينفّذها عند استدعاء الدالة function()‎. وبالتالي نستدعي الرمزين a و function لأنهما يُعَدان تمثيلًا رمزيًا لمنطقةٍ من الذاكرة. تساعد هذه الرموز البشر على فهم البرمجة. لكن يمكنك القول أن المهمة الأساسية لعملية التصريف هي إزالة هذه الرموز، إذ لا يعرف المعالج ما يمثله الرمز a، فكل ما يعرفه هو أن لديه بعض البيانات في عنوان ذاكرة معين. تحوِّل عملية التصريف التعليمة a += 2 إلى العبارة "زيادة القيمة الموجودة في العنوان 0xABCDE من الذاكرة بمقدار 2". لنفترض أنك قسمت برنامجك إلى ملفين، ولكن تريد بعضُ الدوال مشاركةَ متغيرٍ ما. نريد تعريفًا Definition أو موقعًا واحدًا فقط في الذاكرة للمتغير المشترك وإلا فلا يمكن مشاركته، ولكن يجب أن يشير كلا الملفين إليه. يمكن ذلك من خلال التصريح عن المتغير في ملف واحد، ثم نصرّح في الملف الآخر عن متغير بالاسم نفسه مع البادئة extern التي ترمز إلى أنه خارجي External وترمز للمبرمج بأن هذا المتغير مُصرَّحٌ عنه في مكان آخر. تخبر الكلمة extern المصرِّف أنه لا ينبغي تخصيص أي مساحة في الذاكرة لهذا المتغير، ويجب ترك هذا الرمز في التعليمات المُصرَّفة لإصلاحه لاحقًا. لا يمكن للمصرِّف أن يعرف مكان تعريف الرمز فعليًا ولكن الرابط Linker يمكنه ذلك، فوظيفته هي النظر في جميع ملفات التعليمات المُصرَّفة ودمجها في ملف واحد قابل للتنفيذ. لذا سيرى الرابط هذا الرمز في الملف الثاني، وسيقول: "رأيت هذا الرمز مسبقًا في الملف 1، وأعلم أنه يشير إلى موقع الذاكرة 0x12345"، وبالتالي يمكن تعديل قيمة الرمز لتكون قيمة الذاكرة للمتغير الموجود في الملف الأول. تُعَد الكلمة ساكن static عكس خارجي extern تقريبًا، لأنها تضع قيودًا على رؤية الرمز الذي نريد تعديله. إذا صرّحتَ عن متغير بأنه ساكن static، فهذا يعني للمصرّف بألا يترك أيّ رموز لهذا المتغير في شيفرة التعليمات المصرَّفة، وبالتالي لن يرى الرابط هذا الرمز أبدًا عندما يربط ملفات التعليمات المُصرَّفة مع بعضها البعض، أي لا يمكنه القول بأنه رأى هذا الرمز سابقًا. يُعَد استخدام الكلمة static مفيدًا للفصل بين الرموز وتقليل التعارضات بينها، إذ يمكنك إعادة استخدام اسم المتغير المُصرَّح عنه بأنه static في ملفات أخرى دون وجود تعارضات بين الرموز. يمكن القول بأننا نقيّد رؤية الرمز، لأننا لا نسمح للرابط برؤيته بعكس الرمز الذي لم يُصرَّح عنه بأنه static ويمكن للرابط رؤيته. عملية الربط تتكون عملية الربط من خطوتين هما: دمج جميع ملفات التعليمات المُصرَّفة في ملف واحد قابل للتنفيذ ثم الانتقال إلى كل ملف لتحليل الرموز. يتطلب ذلك تمريرين، أحدهما لقراءة جميع تعريفات الرموز وتدوين الرموز التي لم تُحلَّل والثاني لإصلاح تلك الرموز التي لم تُحلَّل في المكان الصحيح. يجب أن يكون الملف القابل للتنفيذ النهائي بدون رموز غير مُحلَّلة، إذ سيفشل الرابط مع وجود خطأ بسبب هذه الرموز. نسمي ذلك بالربط الساكن Static Linking، فالربط الديناميكي هو مفهوم مشابه يُطبَّق ضمن الملف القابل للتنفيذ في وقت التشغيل، حيث سنتطرق إليه لاحقًا. تعرّفنا في هذا المقال على الخطوات الثلاث لبناء ملف قابل للتنفيذ هي: التصريف Compiling والتجميع Assembling والربط Linking، وسنطبّق في المقال التالي هذه الخطوات عمليًا لبناء ملف قابل للتنفيذ. ترجمة -وبتصرُّف- للأقسام: Compiled v Interpreted Programs Building an executable Compiling Assembler Linker من فصل The Toolchain من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال السابق: دعم عتاد الحاسوب للذاكرة الوهمية Virtual Memory المقال التالي: تطبيق عملي لبناء برنامج تنفيذي من شيفرة مصدرية بلغة سي C مفهوم التصريف Compilation في لغات البرمجة تحسين الشيفرات المكتوبة بلغة Cpp وتشخيصها
  8. تلعب عملية أتمتة الخادم server automation الآن دورًا أساسيًا في إدارة الأنظمة نظرًا لطبيعة بيئات التطبيقات الحديثة التي تُستخدَم لمرة واحدة ثم يمكنك التخلص منها، إذ تُستخدَم أدوات إدارة الضبط Configuration management مثل أداة Ansible لتبسيط عملية إعداد الخادم من خلال إنشاء إجراءات معيارية للخوادم الجديدة، مما يقلل من الأخطاء البشرية المرتبطة بالإعدادات اليدوية. لذا تقدّم أداة Ansible معمارية مبسَّطة لا تتطلب تثبيت برامج خاصة على العُقد، وتوفر مجموعة من الميزات والوحدات المبنية مسبقًا والتي تسهل كتابة سكربتات الأتمتة. سيوضح هذا المقال كيفية إعداد ملف المخزون Inventory File وتنفيذ مجموعة من سكربتات الإعداد المسبَق Provisioning لأتمتة عملية إعداد خادم حزمة LEMP أو (Linux و ‎(E)nginx و MariaDB و PHP-FPM) على أوبنتو ونشر تطبيق لارافيل Laravel التجريبي على هذا النظام. ملاحظة: يهدف هذا المقال إلى توضيح كيفية استخدام أدلة التشغيل Playbooks لأتمتة إعداد الخادم باستخدام أداة Ansible، ونشجعك على تعديل وتكييف الإعداد المضمَّن ليناسب احتياجاتك الخاصة بالرغم من أننا سنستخدم تطبيق لارافيل الذي يعمل على خادم LEMP. المتطلبات الأساسية ستحتاج ما يلي لمتابعة هذا المقال: عقدة تحكم Ansible واحدة: وهي جهاز يعمل بنظام أوبنتو 22.04 مع تثبيت أداة Ansible عليه وضبطه للاتصال بمضيفات Ansible باستخدام مفاتيح SSH. تأكّد من أن عقدة التحكم لديها مستخدم عادي يمتلك أذونات مستخدم sudo (أذونات المستخدم الجذر) مع تفعيل جدار حماية، وتعلّم كيفية تثبيت وضبط Ansible على أوبنتو لإعداد الأداة Ansible. مضيف Ansible واحد أو أكثر: هو خادم أوبنتو واحد أو أكثر، إذ يجب أن يكون لكل مضيفٍ المفتاح العام لعقدة التحكم الذي يُضاف إلى ملف authorized_keys. إن أردتَ استخدام أجهزة DigitalOcean Droplets الافتراضية بوصفها عقدًا، فيمكنك استخدام لوحة التحكم لإضافة مفتاحك العام إلى مضيفات Ansible. الخطوة الأولى: نسخ المستودع التجريبي يجب أولًا نسخ المستودع الذي يحتوي على سكربتات Ansible للإعداد المسبَق وتطبيق لارافيل التجريبي الذي سننشره على الخوادم البعيدة، حيث يمكن العثور على جميع الملفات الضرورية في مستودع جيت هَب do-community/ansible-laravel-demo. انسخ المستودع بعد تسجيل الدخول إلى عقدة تحكم Ansible كمستخدم جذر sudo، وانتقل إلى المجلد الذي ينشئه الأمر git كما يلي: git clone https://github.com/do-community/ansible-laravel-demo.git cd ansible-laravel-demo يمكنك الآن تشغيل الأمر ls لفحص محتويات المستودع المنسوخ كما يلي: ls -l --group-directories-first وسترى خرجًا يشبه التالي: drwxrwxr-x 3 sammy sammy 4096 Mar 24 15:24 application drwxrwxr-x 2 sammy sammy 4096 Mar 24 15:24 group_vars drwxrwxr-x 7 sammy sammy 4096 Mar 24 15:24 roles -rw-rw-r-- 1 sammy sammy 102 Mar 24 15:24 inventory-example -rw-rw-r-- 1 sammy sammy 1987 Mar 24 15:24 laravel-deploy.yml -rw-rw-r-- 1 sammy sammy 794 Mar 24 15:24 laravel-env.j2 -rw-rw-r-- 1 sammy sammy 920 Mar 24 15:24 readme.md -rw-rw-r-- 1 sammy sammy 318 Mar 24 15:24 server-setup.yml إليك نظرة عامة على المجلدات والملفات السابقة: application/‎: يحتوي هذا المجلد على تطبيق لارافيل التجريبي الذي سننشره على الخادم البعيد في النهاية. group_vars/‎: يحتوي هذا المجلد على ملفات المتغيرات التي تحتوي على خيارات مُخصَّصة لإعداد التطبيق مثل ثبوتيات Credentials قاعدة البيانات ومكان تخزين ملفات التطبيق على الخادم البعيد. roles/‎: يحتوي هذا المجلد على أدوار Ansible المختلفة التي تعالج الإعداد المسبَق لخادم Ubuntu LEMP. inventory-example: يمكن استخدام هذا الملف بوصفه قاعدةً لإنشاء مخزون مُخصَّص لبنيتك التحتية. laravel-deploy.yml: دليل التشغيل الذي سينشر تطبيق لارافيل التجريبي على الخادم البعيد. laravel-env.j2: يستخدم دليلُ التشغيل laravel-deploy.yml هذا القالب لإعداد ملف بيئة التطبيق. readme.md: يحتوي هذا الملف على معلومات عامة حول الإعداد المسبَق المتضمن في هذا المستودع. server-setup.yml: سيُعِدّ دليل التشغيل هذا مسبقًا خادم LEMP باستخدام الأدوار المُحدَّدة في المجلد roles/‎. الخطوة الثانية: إعداد ملف المخزون واختبار الاتصال بالعقد سننشئ الآن ملف مخزون لسرد المضيفين الذين نريد إدارتهم باستخدام أداة Ansible. انسخ أولًا الملف inventory-example إلى ملف جديد يسمى hosts: cp inventory-example hosts استخدم محرر النصوص الذي تريده لفتح ملف المخزون الجديد وحدّثه باستخدام خوادمك الخاصة، إذ سنستخدم في مثالنا محرر النصوص nano: nano hosts يحتوي المخزون في مثالنا على مجموعتين هما: التطوير dev والإنتاج production اللتان توضحان كيفية استخدام متغيرات المجموعة لتخصيص النشر في بيئات متعددة. إذا أردتَ اختبار هذا الإعداد باستخدام عقدة واحدة، فيمكنك استخدام إما مجموعة التطوير dev أو مجموعة الإنتاج production وإزالة المجموعة الأخرى من ملف المخزون. [dev] 203.0.113.0.101 [prod] 203.0.113.0.102 [all:vars] ansible_python_interpreter=/usr/bin/python3 ملاحظة: يحدّد المتغير ansible_python_interpreter المسار إلى ملف بايثون التنفيذي على المضيف البعيد، ونطلب هناك من أداة Ansible أن تضبط هذا المتغير لجميع المضيفات في ملف المخزون. احفظ وأغلق الملف عند الانتهاء، فإذا أردتَ استخدام محرر النصوص nano، فيمكنك ذلك عن طريق الضغط على الاختصار "CTRL+X" ثم نضغط "Y" وزر الادخال "ENTER" للتأكيد. يمكنك بعد الانتهاء من ضبط ملف المخزون تنفيذ وحدة ping الخاصة بالأداة Ansible لاختبار ما إذا كانت عقدة التحكم قادرة على الاتصال بالمضيفين كما يلي: ansible all -i hosts -m ping -u root لنتعرّف على الأمر السابق بالتفصيل: all: يخبر هذا الخيار أداة Ansible بتشغيل الأمر الذي يليه على جميع المضيفات من ملف المخزون المحدَّد. ‎-i hosts: يحدد المخزون الذي يجب استخدامه، وإن لم يتوفّر هذا الخيار، فستحاول أداة Ansible استخدام المخزون الافتراضي الذي يوجد عادةً في المجلد ‎/etc/ansible/hosts. ‎-m ping: سيؤدي هذا الأمر إلى تنفيذ وحدة ping الخاصة بأداة Ansible، والتي ستختبر الاتصال بالعقد وما إذا كان يمكن العثور على ملف بايثون التنفيذي على الأنظمة البعيدة أم لا. ‎-u root: يحدّد هذا الخيار المستخدم البعيد الذي يجب استخدامه للاتصال بالعقد، إذ نستخدم حساب الجذر في مثالنا لأنه الحساب الوحيد المتاح على خوادم جديدة. يمكن أن تكون خيارات الاتصال الأخرى ضرورية بناءً على مزود البنية التحتية وضبط SSH. إذا ضُبِط اتصال SSH بالعقد بصورة صحيحة، فستحصل على الخرج التالي: 203.0.113.0.101 | SUCCESS => { "changed": false, "ping": "pong" } 203.0.113.0.102 | SUCCESS => { "changed": false, "ping": "pong" } تعني الاستجابة pong أن عقدة التحكم الخاصة بك قادرة على الاتصال بالعقد المُدارة، وأن أداة Ansible قادرة على تنفيذ أوامر بايثون على المضيفين البعيدين. الخطوة الثالثة: إعداد ملفات المتغيرات يجب أولًا قبل تشغيل أدلة التشغيل المُضمَّنة في مثالنا تعديلُ ملف المتغيرات الذي يحتوي على إعدادات مثل اسم المستخدم البعيد الذي سيُنشَأ وثبوتيات قاعدة البيانات لإعدادها باستخدام قاعدة بيانات MariaDB. افتح الملف group_vars/all باستخدام محرر النصوص الذي تريده كما يلي: nano group_vars/all.yml يحتوي هذا الملف على المحتويات التالية: --- # Initial Server Setup remote_user: sammy # MySQL Setup mysql_root_password: MYSQL_ROOT_PASSWORD mysql_app_db: travellist mysql_app_user: travellist_user mysql_app_pass: DB_PASSWORD # Web Server Setup http_host: "{{ ansible_facts.eth0.ipv4.address }}" remote_www_root: /var/www app_root_dir: travellist-demo document_root: "{{ remote_www_root }}/{{ app_root_dir }}/public" # Laravel Env Variables app_name: Travellist app_env: dev app_debug: true app_url: "http://{{ http_host }}" db_host: localhost db_port: 3306 db_database: "{{ mysql_app_db }}" db_user: "{{ mysql_app_user }}" db_pass: "{{ mysql_app_pass }}" المتغيرات التي يجب معرفتها هي: remote_user: سيُنشَأ المستخدم المُحدَّد على الخادم البعيد وسيُمنَح صلاحيات المستخدم الجذر sudo. mysql_root_password: يحدّد هذا المتغير كلمة مرور قاعدة البيانات الجذر لخادم MariaDB، ولاحظ أنه يجب أن تكون كلمة مرور آمنة من اختيارك. mysql_app_db: اسم قاعدة البيانات المُراد إنشاؤها لتطبيق لارافيل. لست بحاجة إلى تغيير هذه القيمة، ولكن لك الحرية في تغييرها إذا أردتَ ذلك، حيث ستُستخدَم هذه القيمة لإعداد ملف ضبط لارافيل ‎.env. mysql_app_user: اسم مستخدم قاعدة البيانات لتطبيق لارافيل. لست بحاجة إلى تغيير هذه القيمة، ولكن لك الحرية في تغييرها إذا أردتَ ذلك. mysql_app_pass: كلمة مرور قاعدة البيانات لتطبيق لارافيل، إذ يجب أن تكون كلمة مرور آمنة من اختيارك. http_host: اسم النطاق أو عنوان IP للمضيف البعيد. نستخدم في مثالنا حقيقة Fact خاصة بأداة Ansible التي تحتوي على عنوان IPv4 لواجهة الشبكة eth0. إذا كان هناك أسماء نطاقات تؤشّر إلى مضيفاتك البعيدة، فيمكن أن ترغب في إنشاء ملفات متغيرات منفصلة لكل منها، وتعديل هذه القيمة ليحتوي ضبطُ خادم Nginx على اسم المضيف الصحيح لكل خادم. احفظ الملف وأغلقه عندما تنتهي من تعديل هذه القيم. إنشاء ملفات متغيرات إضافية لبيئات متعددة إذا أعددتَ ملف المخزون مع عقد متعددة، فيمكن أن ترغب في إنشاء ملفات متغيرات إضافية لإعداد كل عقدة وفقًا لذلك، حيث أنشأنا في مثالنا مجموعتين متميزتين للمخزون هما: التطوير dev والإنتاج production، إذ يجب إنشاء ملف متغيرات منفصل للاحتفاظ بقيم الإنتاج لتجنب وجود ثبوتيات قاعدة البيانات والإعدادات الأخرى نفسها في كلتا البيئتين. يمكن أن ترغب في نسخ ملف المتغيرات الافتراضي واستخدامه بوصفه قاعدةً لقيم الإنتاج الخاصة بك كما يلي: cp group_vars/all.yml group_vars/production.yml nano group_vars/production.yml يحتوي ملف all.yml على القيم الافتراضية التي يجب أن تكون صالحة لجميع البيئات، لذا يمكنك إزالة جميع المتغيرات التي لن تحتاج إلى تغييرها من ملف production.yml الجديد. المتغيرات التي يجب تحديثها لكل بيئة هي: prod_user MYSQL_PROD_ROOT_PASSWORD MYSQL_PROD_APP_PASSWORD prod false وذلك في الشيفرة التالية: --- # Initial Server Setup remote_user: prod_user # MySQL Setup mysql_root_password: MYSQL_PROD_ROOT_PASSWORD mysql_app_pass: MYSQL_PROD_APP_PASSWORD # Laravel Env Variables app_env: prod app_debug: false لاحظ أننا غيّرنا قيمة app_env إلى prod وضبطنا قيمة app_debug على false، وهذه هي إعدادات لارافيل الموصَى بها لبيئات الإنتاج. احفظ الملف وأغلقه بعد الانتهاء من تخصيص متغيرات الإنتاج. تشفير ملفات المتغيرات باستخدام ميزة Vault الخاصة بأداة Ansible إذا أردت مشاركة إعداد أداة Ansible الخاص بك مع مستخدمين آخرين، فيجب الحفاظ على أمان ثبوتيات قاعدة البيانات والبيانات الحساسة الأخرى في ملفات المتغيرات، ويمكن تحقيق ذلك باستخدام ميزة Vault الخاصة بأداة Ansible، وهي ميزة مُضمَّنة فيها افتراضيًا، إذ تسمح هذه الميزة بتشفير ملفات المتغيرات بحيث يتمكن فقط المستخدمون الذين لديهم إذن الوصول إلى كلمة مرور Vault من عرض هذه الملفات أو تعديلها أو إلغاء تشفيرها. تُعَد كلمة مرور Vault ضرورية لتشغيل دليل التشغيل أو الأمر الذي يستخدم الملفات المُشفَّرة. شغّل الأمر التالي لتشفير ملف متغيرات الإنتاج: ansible-vault encrypt group_vars/production.yml سيُطلَب منك تقديم كلمة مرور Vault وتأكيدها، ثم سترى أن البيانات مُشفَّرة إذا فحصتَ محتويات هذا الملف. إذا أردتَ عرض ملف المتغيرات دون تغيير محتوياته، فيمكنك استخدام أمر view التالي: ansible-vault view group_vars/production.yml سيُطلَب منك تقديم كلمة المرور نفسها التي حددتها عند تشفير هذا الملف باستخدام ansible-vault، ثم ستظهر محتويات الملف في طرفيتك Terminal بعد تقديم كلمة المرور، ويمكنك الخروج من عرض الملف من خلال الضغط على q. استخدم الأمر edit التالي لتعديل ملف مُشفَّر مسبقًا باستخدام Ansible Vault: ansible-vault edit group_vars/production.yml سيطلب منك هذا الأمر تقديم كلمة مرور Vault لهذا الملف، ثم سيُستخدَم محرّر طرفيتك الافتراضي لفتح الملف لتعديله. احفظ الملف وأغلقه بعد إجراء التغييرات المطلوبة، وسيُشفَّر تلقائيًا مرة أخرى باستخدام Ansible Vault. انتهيت الآن من إعداد ملفات المتغيرات، وسنشغّل في الخطوة التالية دليل التشغيل لإعداد Nginx و PHP-FPM و MariaDB (والتي تشكل مع نظام التشغيل المستند إلى لينكس مثل أوبنتو حزمة LEMP) على الخادم أو الخوادم البعيدة. الخطوة الرابعة: تنفيذ دليل تشغيل حزمة LEMP يجب إعداد بيئة LEMP التي تخدّم التطبيق قبل نشر تطبيق لارافيل التجريبي على الخادم أو الخوادم البعيدة، إذ يتضمن دليل التشغيل server-setup.yml أدوار Ansible الضرورية لإعدادها. يمكنك فحص محتويات دليل التشغيل server-setup.yml من خلال تشغيل الأمر التالي: cat server-setup.yml وسيظهر الخرج التالي: --- - hosts: all become: true roles: - { role: setup, tags: ['setup'] } - { role: mariadb, tags: ['mysql', 'mariadb', 'db', 'lemp'] } - { role: php, tags: ['php', 'web', 'php-fpm', 'lemp'] } - { role: nginx, tags: ['nginx', 'web', 'http', 'lemp'] } - { role: composer, tags: ['composer'] } إليك نظرة عامة على جميع الأدوار المضمنة في دليل التشغيل السابق: setup: يحتوي على المهام اللازمة لإنشاء مستخدم جديد للنظام ومنحه صلاحيات المستخدم الجذر sudo بالإضافة إلى تفعيل جدار الحماية ufw. mariadb: يثبّت خادم قاعدة بيانات MariaDB وينشئ قاعدة بيانات ومستخدم التطبيق. php: يثبّت وحدات php-fpm و PHP الضرورية لتشغيل تطبيق لارافيل. nginx: يثبّت خادم الويب Nginx ويتيح الوصول إلى المنفذ 80. composer: تثبيت مدير الحزم Composer على المستوى العام. لاحظ أننا ضبطنا بعض الوسوم ضمن كل دور لتسهيل إعادة تشغيل أجزاء فقط من دليل التشغيل إن لزم الأمر. إذا أجريت تغييرات على ملف قالب Nginx مثلًا، فيمكن أن ترغب في تشغيل الدور Nginx فقط. سينفّذ الأمر التالي دليل التشغيل على جميع الخوادم من ملف مخزونك، حيث يُعَد ‎--ask-vault-pass ضروريًا فقط في حالة استخدام ansible-vault لتشفير ملفات المتغيرات في الخطوة السابقة: ansible-playbook -i hosts server-setup.yml -u root --ask-vault-pass وستحصل على خرج يشبه ما يلي: PLAY [all] ********************************************************************************************** TASK [Gathering Facts] ********************************************************************************** ok: [203.0.113.0.101] ok: [203.0.113.0.102] TASK [setup : Install Prerequisites] ******************************************************************** changed: [203.0.113.0.101] changed: [203.0.113.0.102] ... RUNNING HANDLER [nginx : Reload Nginx] ****************************************************************** changed: [203.0.113.0.101] changed: [203.0.113.0.102] PLAY RECAP ********************************************************************************************** 203.0.113.0.101 : ok=31 changed=27 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1 203.0.113.0.102 : ok=31 changed=27 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1 أصبحت عقدتك (أو مجموعة عقدك) جاهزةً الآن لتخديم تطبيقات PHP باستخدام Nginx و PHP-FPM مع خادم قاعدة بيانات MariaDB، وسننشر في الخطوة التالية تطبيق لارافيل التجريبي المُضمَّن باستخدام دليل تشغيل Ansible الذي هو laravel-deploy.yml. الخطوة الخامسة: نشر تطبيق لارافيل أصبح لديك الآن بيئة LEMP تعمل على الخادم أو الخوادم البعيدة، فيمكنك تنفيذ دليل التشغيل laravel-deploy.yml الذي سينفّذ المهام التالية: إنشاء المستند الجذر للتطبيق على الخادم البعيد، إن لم يُنشَأ فعليًا. مزامنة مجلد التطبيق المحلي مع الخادم البعيد باستخدام الوحدة sync. استخدام الوحدة acl لضبط أذونات مستخدم www-data في مجلد التخزين. إعداد ملف التطبيق ‎.env بناءً على القالب laravel-env.j2. تثبيت اعتماديات التطبيق باستخدام مدير الحزم Composer. توليد مفتاح أمان التطبيق. إعداد رابط عام للمجلد storage. تشغيل عمليات تهجير Migration قاعدة البيانات والبذر Seeder. يجب أن ينفِّذ مستخدمٌ غير جذر لديه أذونات sudo دليلَ التشغيل، ويجب أن يكون هذا المستخدم مُنشَأً عند تنفيذ دليل التشغيل server-setup.yml في الخطوة السابقة باستخدام الاسم الذي يحدّده المتغير remote_user. شغّل دليل التشغيل laravel-deploy.yml باستخدام الأمر التالي: ansible-playbook -i hosts laravel-deploy.yml -u sammy --ask-vault-pass يُعَد ‎--ask-vault-pass ضروريًا فقط في حالة استخدام ansible-vault لتشفير ملفات المتغيرات في الخطوة السابقة. وستحصل على خرج يشبه ما يلي: PLAY [all] ********************************************************************************************** TASK [Gathering Facts] ********************************************************************************** ok: [203.0.113.0.101] ok: [203.0.113.0.102] TASK [Make sure the remote app root exists and has the right permissions] ******************************* ok: [203.0.113.0.101] ok: [203.0.113.0.102] TASK [Rsync application files to the remote server] ***************************************************** ok: [203.0.113.0.101] ok: [203.0.113.0.102] ... TASK [Run Migrations + Seeders] ************************************************************************* ok: [203.0.113.0.101] ok: [203.0.113.0.102] PLAY RECAP ********************************************************************************************** 203.0.113.0.101 : ok=10 changed=9 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 203.0.113.0.102 : ok=10 changed=9 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 يمكنك عند انتهاء التنفيذ الوصول إلى التطبيق التجريبي من خلال توجيه متصفحك إلى اسم نطاق أو عنوان IP الخاص بعقدتك: http://node_domain_or_IP وسترى صفحة تشبه ما يلي: الخلاصة أوضح هذا المقال كيفية إعداد ملف مخزون الأداة Ansible والاتصال بالعقد البعيدة، وكيفية تشغيل أدلة تشغيل Ansible لإعداد خادم LEMP ونشر تطبيق لارافيل التجريبي عليه. اطّلع على مستودع جيت هَب التجريبي الذي يحتوي على جميع الملفات الضرورية لمتابعة العمل. ترجمة -وبتصرُّف- للمقال Automating Server Setup with Ansible: A DigitalOcean Workshop Kit لصاحبته Erika Heidi. اقرأ أيضًا ما الفرق بين التنسيق والأتمتة؟ أتمتة إعداد خادم أولي أوبونتو كيفية تثبيت وإعداد Ansible لأتمتة إدارة الإعدادات ونشر التطبيقات على خادوم Ubuntu
  9. ذكرنا من الفصل السابق الذاكرة الوهمية والذاكرة الحقيقية في معمارية الحاسوب من سلسلتنا مدخل لعلم الحاسوب حتى الآن أن العتاد يعمل مع نظام التشغيل لتقديم Implementation الذاكرة الوهمية، وألقينا نظرة على تفاصيل كيفية حدوث ذلك. تعتمد الذاكرة الوهمية بصورة كبيرة على معمارية العتاد، حيث يكون لكل معمارية خواصها الدقيقة، ولكن هناك عدد من العناصر الخاصة بالذاكرة الوهمية في العتاد التي سنستعرضها في هذا المقال. الوضع الحقيقي والوضع الوهمي تحتوي جميع المعالجات على مفهومٍ ما للعمل في الوضع الحقيقي Physical Mode أو الوضع الوهمي Virtual Mode، إذ يتوقع العتاد في الوضع الحقيقي أن يشير أيّ عنوان إلى عنوانٍ موجود في ذاكرة النظام الفعلية، بينما يعرف العتاد في الوضع الوهمي أنه يجب ترجمة العناوين للعثور على العنوان الحقيقي. يُشار إلى هذين الوضعين في العديد من المعالجات مثل المعالج إيتانيوم Itanium ببساطة على أنهما الوضع الحقيقي والوضع الوهمي. المعالج x86 الأكثر شيوعًا لديه الكثير من الخاصيات منذ الأيام الماضية التي تسبق الذاكرة الوهمية، حيث يُشار إلى هذين الوضعين على أنهما الوضع الفعلي Real Mode والوضع المحمي Protected Mode. كان أول معالج يطبق الوضع المحمي هو المعالج 386، ولا تزال أحدث المعالجات من عائلة x86 بإمكانها العمل في الوضع الفعلي بالرغم من عدم استخدامه. يطبّق المعالج في الوضع الفعلي شكلًا من أشكال تنظيم الذاكرة يسمى التقطيع Segmentation. مشاكل التقطيع كان التقطيع أمرًا مهمًا سابقًا، ولكن قلّلت الذاكرة الوهمية من أهميته، فللتقطيع عيوبه مثل كونه مربكًا للمبرمجين المبتدئين، لذا اُخترِعت أنظمة الذاكرة الوهمية لحل هذه المشاكل إلى حد كبير. يوجد في التقطيع عدد من المسجّلات التي تحتوي على عنوان يمثل بداية المقطع، والطريقة الوحيدة للوصول إلى عنوان في الذاكرة هي تحديده بوصفه إزاحة من أحد هذه المسجلات. يمكن تحديد حجم المقطع والحد الأقصى للإزاحة الذي يمكنك تحديده من خلال عدد البتات المتاحة للإزاحة من مسجل المقطع الأساسي. الحد الأقصى للإزاحة في المعالج x86 هو 16 بتًا أو 64 كيلوبايت فقط، مما يؤدي إلى ظهور جميع أشكال الفوضى، حيث إذا أراد شخصٌ ما استخدام عنوان يبعد أكثر من 64 كيلوبايت، فستتحول الأمور من مجرد إزعاج بسيط إلى فشل كامل عندما ينمو حجم الذاكرة إلى عدد من الميجابايتات أو الجيجابايتات. لنفترض أن أقصى إزاحة هي 32 بتًا، وبالتالي يمكن الوصول إلى فضاء العناوين بالكامل بوصفه إزاحة من مقطع عند العنوان 0x00000000 وسيكون لديك تخطيط مسطح، ولكن لا تضاهي هذه الحالة جودة الذاكرة الوهمية. السبب الوحيد لكون الإزاحة بمقدار 16 بت هو أن معالجات إنتل Intel الأصلية كانت محدودة بهذا المقدار مع محافظة الشرائح على التوافق مع الإصدارات السابقة. التقطيع Segmentation هناك ثلاثة مسجلات مقاطع في الشكل السابق تؤشّر جميعها إلى المقاطع، ويظهر حد الإزاحة الأقصى المُقيَّد بعدد البتات المتاحة في المنطقة المُظلَّلة. إذا أراد البرنامج عنوانًا خارج هذا النطاق، فيجب إعادة ضبط مسجّلات المقاطع الذي سرعان ما يصبح مصدر إزعاج كبير فيما بعد. بينما تسمح ذاكرة البرنامج الوهمية بتحديد العنوان الذي تريده ويطبّق نظام التشغيل والعتاد عملية الترجمة إلى عنوان حقيقي. مخزن الترجمة المؤقت TLB يُعَد مخزن الترجمة المؤقت Translation Lookaside Buffer -أو TLB اختصارًا- مكون المعالج الرئيسي المسؤول عن الذاكرة الوهمية، وهو ذاكرةٌ مخبئية لعمليات ترجمة الصفحة الوهمية إلى الإطار الحقيقي في المعالج. يعمل نظام التشغيل والعتاد مع بعضهما البعض لإدارة مخزن TLB أثناء تشغيل النظام. أخطاء الصفحات إذا طُلِب عنوان وهمي من العتاد باستخدام تعليمة تحميل load مثلًا للحصول على بعض البيانات، فسيبحث المعالج عن ترجمة العنوان الوهمي إلى العنوان الحقيقي في مخزن TLB الخاص به، فإن احتوى على ترجمة صالحة، فيمكن عندئذٍ دمجه مع جزء الإزاحة للانتقال مباشرةً إلى العنوان الحقيقي وإكمال التحميل. لكن إن لم يتمكّن المعالج من العثور على ترجمةٍ في مخزن TLB، فيجب على المعالج إصدار خطأ الصفحة Page Fault الذي يشبه المقاطعة، ويجب أن يعالجه نظام التشغيل، إذ يجب أن يستعرض نظام التشغيل جدول الصفحات للعثور على الترجمة الصحيحة وإدخالها في مخزن TLB عند حدوث خطأ صفحة. إن لم يتمكّن نظام التشغيل من العثور على ترجمةٍ في جدول الصفحات أو إن تحقّق نظام التشغيل من أذونات الصفحة المطلوبة ولم يُسمَح للعملية بالوصول إليها، فيجب على نظام التشغيل إنهاء العملية. إن رأيتَ مسبقًا خطأ تقطيع أو ما يُسمَّى Segfault، فهذا يعني أن نظام التشغيل ينهي العملية التي تجاوزت حدودها. بينما إن تمكّن نظام التشغيل من العثور على الترجمة وكان مخزن TLB ممتلئًا حاليًا، فيجب إزالة ترجمة قبل إدخال ترجمةٍ أخرى. ليست إزالة الترجمة التي يُحتمَل استخدامها لاحقًا أمرًا منطقيًا، إذ ستتحمل عناء العثور على المدخلة في جداول الصفحات مرةً أخرى. تستخدم مخازن TLB شيئًا يشبه خوارزمية الأقل استخدامًا مؤخرًا Least Recently Used -أو LRU اختصارًا، حيث تُخرَج أقدم ترجمة غير مُستخدمَة لإدخال ترجمة جديدة. يمكن بعد ذلك محاولة الوصول مرة أخرى وسيسير كل شيء على ما يرام، حيث يجب العثور على الترجمة في مخزن TLB وستحدث الترجمة بصورة صحيحة. العثور على جدول الصفحات لا بد أنك تساءلت عن كيفية إيجاد نظام التشغيل للذاكرة التي تحتوي على جدول الصفحات page table عندما قلنا أن نظام التشغيل يجد الترجمة في هذا الجدول. يُحتفَظ بقاعدة جدول الصفحات في مسجّل مرتبط بكل عملية، حيث يسمى هذا المسجل بالمسجل الأساسي لجدول الصفحات page-table base-register أو ما شابه ذلك. يمكن تحديد موقع المدخلة الصحيحة بوضع العنوان في هذا المسجل وإضافة رقم الصفحة إليه. أخطاء أخرى متعلقة بالصفحات هناك عيبان مهمان آخران يمكن أن يولّدهما مخزن TLB ويساعدان على إدارة الصفحات المتسخة dirty pages أي الصفحات التي جرى الوصول إليها مسبقًا، حيث تحتوي كل صفحة على سمة مُمثَّلة ببتٍ واحد تحدّد إذا جرى الوصول إلى الصفحة أي أنها أصبحت صفحة متسخة. يمكن تمييز الصفحة على أنها صفحة جرى الوصول إليها مسبقًا عند تحميل ترجمة الصفحة مبدئيًا في مخزن TLB. إذا حمّلتَ الصفحة دون وصول معلَّق، فيمكن تسمية ذلك بالتأمّل Speculation مثل تطبيق شيءٍ ما مع التوقّع بأنه سيؤتي ثماره، حيث إذا قرأت الشيفرة البرمجية من الذاكرة خطيًا مثلًا، فيمكن أن يؤدي وضع ترجمة الصفحة التالية في مخزن TLB إلى توفير الوقت وتحسين الأداء. يعمل نظام التشغيل على تصفح جميع الصفحات دوريًا ويصفّر بت الوصول ليميز الصفحات التي تكون قيد الاستخدام حاليًا من غيرها. تكون الصفحات التي لم يُعاد ضبط بت الوصول الخاص بها (بعد تصفيره) هي أفضل المرشحين للإزالة لأنها لم تُستخدَم لفترة أطول عندما تمتلئ ذاكرة النظام ويحين الوقت لنظام التشغيل لاختيار الصفحات التي ستُبدَّل إلى القرص الصلب. الصفحة المتسخة هي الصفحة التي تحتوي على بياناتٍ مكتوبةٍ عليها، وبالتالي لا تتطابق مع أيّ بيانات موجودة فعليًا على القرص الصلب. إذا بُدِّلت الصفحة من ثم كتبت فيها عملية مثلًا، فيجب تحديث نسختها الموجودة على القرص الصلب قبل تبديلها. لا تُجرَى أيّ تغييرات على الصفحة النظيفة، لذلك لا حاجة لنسخ الصفحة مرةً أخرى إلى القرص الصلب. يتشابه هذان النوعان من الصفحات من حيث أنهما يساعدان نظام التشغيل في إدارة الصفحات، حيث تحتوي الصفحة على بتين إضافيين هما: البت المتسخ Dirty Bit وبت الوصول Accessed Bit، إذ يُضبَط هذان البتان عند وضع الصفحة في مخزن TLB للإشارة إلى أن وحدة المعالجة المركزية يجب أن تصدر خطأ. يطبّق العتادُ عملية الترجمة المعتادة عندما تحاول إحدى العمليات الرجوع إلى الذاكرة، ولكنه يجري أيضًا فحصًا إضافيًا للتأكد من عدم ضبط راية الوصول. إذا كان الأمر كذلك، فسيؤدي ذلك إلى حدوث خطأ في نظام التشغيل الذي يجب أن يضبط البت ويسمح للعملية بالاستمرار. إذا اكتشف العتاد أنه يكتب في صفحةٍ يكون بتها المتسخ غير مضبوط، فسيؤدي ذلك إلى حدوث خطأ في نظام التشغيل لتمييز الصفحة بوصفها متسخة. إدارة مخزن TLB يمكننا القول أن مخزن TLB يستخدمه العتاد ولكن تديره البرمجيات، فالأمر متروك لنظام التشغيل لتحميل المدخلات الصحيحة إلى مخزن TLB ولإزالة المدخلات القديمة منه. تفريغ مخزن TLB تُسمَّى عملية إزالة المدخلات من مخزن TLB باسم التفريغ Flushing. يُعَد تحديث مخزن TLB جزءًا مهمًا من الحفاظ على فضاءات عناوين العمليات منفصلة، لأن كل عملية يمكن أن تستخدم العنوان الوهمي نفسه دون تحديث مخزن TLB، وهذا يعني أن العملية يمكن أن تكتب فوق ذاكرة العمليات الأخرى، بينما تريد في حالة الخيوط Threads مشاركة فضاء العناوين، وبالتالي لا يُفرَّغ مخزن TLB عند التبديل بين الخيوط في العملية نفسها. يُفرَّغ مخزن TLB بالكامل في بعض المعالجات في كل مرة يوجد فيها تبديل سياق، ويمكن أن يكون ذلك مرهقًا للغاية، لأنه يعني أن العملية الجديدة يجب أن تمر عبر المراحل كاملةً من أخذ خطأ الصفحة ثم العثور على الصفحة في جداول الصفحات وإدخال الترجمة. بينما تطبّق المعالجات الأخرى معرّف فضاء عناوين إضافي Address Space ID -أو ASID اختصارًا- يُضاف إلى كل ترجمة في مخزن TLB لجعلها فريدة، وبالتالي يحصل كل فضاء عناوين -أو كل عملية حيث تريد الخيوط مشاركة فضاء العناوين نفسه- على معرّفها الخاص الذي يُخزَّن مع الترجمات في مخزن TLB، أي ليس هناك داعٍ لتفريغ مخزن TLB عند تبديل السياق، لأن العملية التالية سيكون لها معرّف فضاء عناوين مختلف، وسيختلف معرّف فضاء العناوين وستختلف الترجمة إلى الصفحة الحقيقية حتى إن طلبت العملية العنوان الوهمي نفسه. يقلل هذا النظام من عملية التفريغ ويزيد من أداء النظام، ولكنه يتطلب مزيدًا من عتاد TLB ليحتفظ ببتات معرّف ASID. يمكن تنفيذ ذلك من خلال وجود مسجل إضافي بوصفه جزءًا من حالة العملية التي تتضمن معرّف ASID. ينظر مخزن TLB إلى هذا المسجّل عند ترجمة الصفحة الوهمية إلى الصفحة الحقيقية، وسيطابق فقط المدخلات التي لها معرّف ASID الخاص بالعملية التي تكون قيد التشغيل حاليًا. يحدّد حجم هذا المسجل رقم معرّفات ASID المتاحة وبالتالي له تأثير على الأداء. مخزن TLB المحمل برمجيا وعتاديا يُعَد التحكم في مخزن TLB من اختصاص نظام التشغيل، ولكنها ليست القصة كاملة، إذ تشرح العملية الموضَّحة في فقرة "أخطاء الصفحات" خطأ الصفحة الذي يُرفَع إلى نظام التشغيل، ويمر على جدول الصفحات للعثور على ترجمة الصفحة الوهمية إلى الحقيقية وتثبيتها في مخزن TLB. يمكن أن يسمَّى ذلك بمخزن TLB المُحمَّل برمجيًا Software-loaded TLB، وهناك بديل آخر هو مخزن TLB المُحمَّل عتاديًا Hardware-loaded TLB. تحدد معمارية المعالج تخطيطًا معينًا لمعلومات جدول الصفحات في مخزن TLB المُحمَّل عتاديًا، ويجب اتباعها حتى ترجمة العنوان الوهمي. سيمر المعالج تلقائيًا على جداول الصفحات لتحميل مدخلة الترجمة الصحيحة استجابةً للوصول إلى عنوان وهمي غير موجود في مخزن TLB، وسيرفع المعالج استثناءً ليعالج نظام التشغيل مدخلة الترجمة غير الموجودة في مخزن TLB. يوفّر التنفيذ المُقدَّم للمرور على جدول الصفحات في العتاد المتخصص مزايا السرعة عند البحث عن الترجمات، ولكنه يزيل المرونة عن مطبّقي أنظمة التشغيل الذين يرغبون في تنفيذ مخططات بديلة لجداول الصفحات. يمكن تصنيف جميع المعماريات على نطاق واسع ضمن هاتين المنهجيتين السابقتين، وسنطّلع تاليًا على بعض المعماريات الشائعة ودعم الذاكرة الوهمية. دعم العتاد للذاكرة الوهمية يوفّر عتاد المعالج جدول بحث يربط العناوين الوهمية بالعناوين الحقيقية، حيث تحدد معماريات المعالجات طرقًا مختلفة لإدارة مخزن TLB مع مزايا وعيوب مختلفة. يشار إلى جزء المعالج الذي يتعامل مع الذاكرة الوهمية باسم وحدة إدارة الذاكرة Memory Management Unit أو MMU اختصارًا. معالج إيتانيوم توفر وحدة MMU في معالج إيتانيوم ميزات متعددة لنظام التشغيل للتعامل مع الذاكرة الوهمية سنوضحها فيما يلي. فضاءات العناوين Address spaces شرحنا سابقًا مفهوم معرّف فضاء العناوين لتقليل تكلفة تفريغ مخزن TLB عند تبديل السياق، ولكن يستخدم المبرمجون في أغلب الأحيان الخيوط Threads للسماح لسياقات التنفيذ بمشاركة فضاء العناوين، حيث تحتوي جميع الخيوط على معرّف ASID نفسه، وبالتالي يتشاركون بمدخلات مخزن TLB، مما يؤدي إلى زيادة الأداء. لكن يمنع معرّف ASID واحد مخزن TLB من فرض الحماية، حيث تصبح المشاركة متمثلة بنهج "الكل أو لا شيء"، إذ يجب أن تتخلى الخيوط عن الحماية من بعضها البعض لمشاركة بعض البايتات. رسم توضيحي للمناطق ومفاتيح الحماية في معالج إيتانيوم تدرس وحدة MMU في معالج إيتانيوم المشاكل السابقة وتوفر القدرة على مشاركة فضاء العناوين ومدخلات الترجمة بدقة أقل بكثير مع الحفاظ على الحماية داخل العتاد. يقسم معالج إيتانيوم فضاء العناوين المؤلف من 64 بتًا إلى 8 مناطق كما هو موضَّح في الشكل السابق. تحتوي كل عملية على ثمانية مسجلات مناطق بحجم 24 بتًا بوصفها جزءًا من حالتها، ويحتوي كل منها على معرّف منطقة Region ID -أو RID اختصارًا- لكل منطقة من المناطق الثمانية الخاصة بفضاء عناوين العملية. تُوسَم ترجمات مخزن TLB بمعرّف RID، وبالتالي لن تتطابق إلا إذا احتوت العملية على معرّف RID نفسه كما هو موضَّح في الشكل التالي: رسم توضيحي لترجمة مخزن TLB في معالج إيتانيوم لا تُحتسَب البتات الثلاثة الأولى (بتات المنطقة) في ترجمة العنوان الوهمي، لذلك إذا كانت هناك عمليتان تشتركان في معرّف RID أي تحتفظان بالقيمة نفسها في أحد مسجلات المنطقة الخاصة بهما، فسيكون لديهما اسم بديل لتلك المنطقة. إذا احتوت العملية A على معرّف RID قيمته 0x100 في مسجل المنطقة 3 واحتوت العملية B معرّف RID نفسه الذي قيمته 0x100 في مسجّل المنطقة 5، فستُسمَّى المنطقة 3 من العملية A باسم بديل هو process-B, region 5. تعني هذه المشاركة المحدودة أن كلتا العمليتين تتلقيان فوائد مدخلات مخزن TLB المشتركة دون الحاجة إلى منح إذن الوصول إلى كامل فضاء العناوين. مفاتيح الحماية Protection Keys تُوسَم كل مدخلة من مخزن TLB في معالج إيتانيوم بمفتاح حماية للسماح بمشاركة أكثر دقة. تحتوي كل عملية على عدد إضافي من مسجلات مفاتيح الحماية يحدده نظام التشغيل. تُوسَم كل صفحة بمفتاح فريد ويمنح نظام التشغيل العمليات المسموح بها للوصول إلى الصفحات التي تستخدم هذا المفتاح عند مشاركة سلسلة من الصفحات مثل شيفرة برمجية لمكتبة نظام مشتركة. يفحص مخزن TLB المفتاح المرتبط بمدخلة الترجمة مقابل المفاتيح التي تحتفظ بها العملية في مسجلات مفتاح الحماية الخاصة بها عند الإشارة إلى صفحة ما، مما يسمح بالوصول إليها في حالة وجود المفتاح أو يؤدي إلى رفع خطأ حماية لنظام التشغيل. يمكن للمفتاح فرض الأذونات أيضًا، إذ يمكن أن تحتوي إحدى العمليات مثلًا على مفتاح يمنح أذونات الكتابة ويمكن أن تحتوي عملية أخرى على مفتاح للقراءة فقط، مما يسمح بمشاركة مدخلات الترجمة بدقة وفي نطاق أوسع بكثير وصولًا إلى مستوى الصفحة الواحدة، ويؤدي ذلك إلى تحسينات محتملة كبيرة في أداء مخزن TLB. أداة إيتانيوم العتادية للمرور على جدول الصفحات Itanium Hardware Page-Table Walker يؤدي تبديل السياق إلى نظام التشغيل عند حل خطأ في مخزن TLB إلى إضافة عبء كبير إلى مسار معالجة الخطأ، إذ يواجه معالج إيتانيوم ذلك العبء من خلال السماح بخيار استخدام العتاد المُدمَج لقراءة جدول الصفحات وتحميل ترجمات الصفحة الوهمية إلى الصفحة الحقيقية في مخزن TLB تلقائيًا. تتجنب أداة المرور على جدول الصفحات العتادية Hardware Page-table Walker -أو HPW اختصارًا- عمليات الانتقال المكلفة إلى نظام التشغيل، ولكنه يتطلب أن تكون الترجمات بصيغة ثابتة ومناسبة للعتاد لتفهمه. يُشار إلى أداة HPW الخاصة بمعالج إيتانيوم في توثيق إنتل على أنها أداة مُعمَّاة وهميًا للمرور على جدول الصفحات Virtually Hashed Page-table Walker أو VHPT walker اختصارًا. يمنح معالج إيتانيوم المطورين خيارين من تقديمات HPW الحصرية تبادليًا، إذ يعتمد أحدهما على جدول الصفحات الخطي الوهمي ويعتمد الآخر على جدول التعمية Hash Table. تجدر الإشارة إلى أنه يمكن العمل بدون أداة عتادية للمرور على جدول الصفحات، حيث يحل نظام التشغيل كل خطأ TLB ويصبح المعالج معمارية محمَّلة برمجيًا، ولكن يُعَد تأثير تعطيل HPW على الأداء كبيرًا جدًا دون الحصول على أيّ فائدة. جدول الصفحات الخطي الوهمي Virtual Linear Page-Table يشار إلى تقديم جدول الصفحات الخطي الوهمي في التوثيقات على أنه الصيغة القصيرة لجدول الصفحات المُعمَّاة وهميًا Short Format Virtually Hashed Page-table -أو SF-VHPT اختصارًا، وهو نموذج HPW الافتراضي الذي يستخدمه لينكس على معالج إيتانيوم. الحل المعتاد هو استخدام جدول صفحات متعدد المستويات أو هرمي، حيث تُستخدَم البتات التي تتكون من رقم الصفحة الوهمية بوصفها فهرسًا إلى مستويات وسيطة من جدول الصفحات. لا توجد مناطق فضاء عناوين وهمية فارغة في جدول الصفحات الهرمي. تُهدَر مساحة صغيرة نسبيًا في الحِمل الإضافي بالنسبة للحالة الواقعية لفضاء العناوين المُجمَّعة Clustered بإحكام والمملوءة بصورة ضئيلة بالمقارنة مع جدول الصفحات الخطي، ولكن العيب الرئيسي هو مراجع الذاكرة المتعددة المطلوبة للبحث. رسم توضيحي لجدول صفحات هرمي يأخذ الجدول الخطي الذي حجمه 512 جيبي بايت GiB مع فضاء عناوين 64 بتًا ما مقداره 0.003% فقط من 16 إكسابايت المتاحة، وبالتالي يمكن إنشاء جدول صفحات خطي وهمي Virtual Linear Page-table -أو VLPT اختصارًا- في منطقة متجاورة من فضاء العناوين الوهمية. يستخدم العتاد عند حدوث خطأ في مخزن TLB رقم الصفحة الوهمية للإزاحة عن قاعدة جدول الصفحات تمامًا كما هو الحال بالنسبة لجدول صفحات خطي حقيقي. إذا كانت هذه المدخلة صحيحة، فستُقرَأ الترجمة وتُدرَج مباشرةً في مخزن TLB، ولكن يكون عنوان مدخلة الترجمة في حد ذاتها عنوانًا وهميًا باستخدام جدول VLPT، وبالتالي هناك احتمال أن تكون الصفحة الوهمية التي توجد بها غير موجودة في مخزن TLB، وسيُرفَع خطأ متداخل Nested Fault إلى نظام التشغيل في هذه الحالة. يجب على البرمجيات بعد ذلك تصحيح هذا الخطأ عن طريق ربط الصفحة التي تحتوي على مدخلة الترجمة مع جدول VLPT. تقديم جدول VHPT بصيغة قصيرة في معالج إيتانيوم يمكن جعل هذه العملية مباشرةً إذا احتفظ نظام التشغيل بجدول صفحات هرمي، حيث تحتوي الصفحة التي تمثل ورقة من جدول صفحات هرمي على مدخلات ترجمة لمنطقة متجاورة وهميًا من العناوين، وبالتالي يمكن ربطها باستخدام مخزن TLB لإنشاء جدول VLPT كما هو موضح في الشكل السابق. صيغ مدخلة PTE في معالج إيتانيوم تحدث ميزة جدول VLPT الرئيسية عندما يطلب أحد التطبيقات وصولًا متكررًا أو متواصلًا إلى الذاكرة. ضع في حساباتك أن الخطأ الأول في عملية المرور على الذاكرة المتجاورة وهميًا سيؤدي إلى ربط صفحة مليئة بمدخلات الترجمة مع جدول الصفحات الخطي الوهمي. سيتطلب الوصول اللاحق إلى الصفحة الوهمية التالية تحميلَ مدخلة الترجمة التالية في مخزن TLB، والتي تتوفر الآن في جدول VLPT، وبالتالي تُحمَّل بسرعة كبيرة دون استدعاء نظام التشغيل. سيكون ذلك ميزةً عند الاستفادة من تكلفة الخطأ المتداخل الأولي على عمليات مرور HPW الناجحة اللاحقة. العيب الرئيسي هو أن جدول VLPT يتطلب الآن مدخلات مخزن TLB، مما يؤدي إلى زيادة الضغط عليه. يتطلب كل فضاء عناوين جدول صفحاتٍ خاص به، لذلك تصبح التكلفة أكبر كلما أصبح النظام أكثر نشاطًا، ولكن يجب أن تكون أيّ زيادة في أخطاء الوصول إلى مخزن TLB أكبر من الفائدة الحاصلة عند انخفاض تكاليف إعادة الملء من أداة المرور العتادية الفعالة. لاحظ أن الحالة السيئة يمكن أن تتخطى مدخلات بمقدارٍ يساوي نتيجة قسمة حجم الصفحة page_size على حجم الترجمة translation_size، مما يتسبب في حدوث أخطاء متداخلة ومتكررة، ولكنه يُعَد نمط وصول غير مُحتمَل. تتوقع أداة المرور العتادية hardware walker أن تكون مدخلات الترجمة بصيغة معينة كما هو موضح على يسار الشكل السابق، حيث يتطلب جدول VLPT ترجمات بصيغة قصيرة مؤلَّفة من 8 بايتات. إذا استخدم نظام التشغيل جدول الصفحات الخاص به بوصفه دعمًا لجدول VLPT كما في الشكل "تقديم جدول VHPT بصيغة قصيرة في معالج إيتانيوم"، فيجب أن تستخدم هذه صيغة الترجمة القصيرة. تتجاهل المعمارية عددًا محدودًا من البتات في هذه الصيغة وبالتالي تكون متاحة لتستخدمها البرمجيات مع عدم احتمال حدوث تعديلات كبيرة. يعتمد جدول الصفحات الخطي linear page-table على فكرة حجم الصفحة الثابت، ويُعَد دعم أحجام الصفحات المتعددة مشكلةً لأنه يعني أن ترجمة صفحة وهمية معينة لم تَعُد عند إزاحة ثابتة، ولكن يمكن حل هذه المشكلة من خلال احتواء كل منطقة من المناطق الثمانية في فضاء العناوين -كما هو موضّح في الشكل "رسم توضيحي للمناطق ومفاتيح الحماية في معالج إيتانيوم"- على جدول VLPT منفصل يربط عناوين تلك المنطقة فقط. يمكن إعطاء حجم الصفحة الوهمية لكل منطقة، حيث تُخصَّص منطقة واحدة لصفحات أكبر (باستخدام مخزن HugeTLB في لينكس) ولكن لا يمكن استخدام أحجام صفحات متعددة ضمن المنطقة الواحدة. جدول التعمية الوهمي Virtual Hash Table يمكن أن يكون استخدام مدخلات مخزن TLB لمحاولة تقليل تكاليف إعادة تعبئته -كما هو الحال مع جدول SF-VHPT- مقايضة فعّالة أو يمكن ألّا يكون كذلك. يطبّق معالج إيتانيوم جدول صفحات مُعمَّى hashed page-table مع إمكانية خفض تكاليف مخزن TLB، حيث يعمّي المعالج في هذا المخطط عنوانًا وهميًا للعثور على إزاحة في جدول مجاور. يُعَد جدول الصفحات الخطي الذي ناقشناه سابقًا جدول صفحات مُعمَّى باستخدام تعمية Hash مثالية لن ينتج عنها تضاربٌ أبدًا، ولكن يتطلب ذلك مقايضة غير عملية لمناطق ضخمة من الذاكرة الحقيقية المتجاورة. يزيد تقييد متطلبات الذاكرة لجدول الصفحات من احتمال حدوث تضاربات عند تعمية عنوانين وهميين إلى الإزاحة نفسها. تتطلب الترجمات المتضاربة مؤشر سلسلة Chain Pointer لإنشاء قائمة مترابطة من المدخلات البديلة الممكنة. يتطلب تمييز المدخلة الصحيحة في القائمة المترابطة وسمًا Tag مشتقًا من العنوان الوهمي الوارد. تؤدي المعلومات الإضافية المطلوبة لكل مدخلة ترجمة إلى ظهور اسم بديل بصيغة جدول VHPT الطويلة -أو LF-VHPT اختصارًا. تنمو مدخلات الترجمة إلى 32 بايتًا كما هو موضح على الجانب الأيمن من الشكل "صيغ مدخلة PTE في معالج إيتانيوم". الميزة الرئيسية لهذه الطريقة هي أن جدول التعمية العام يمكن تثبيته باستخدام مدخلة TLB واحدة. تشترك جميع العمليات في الجدول، لذا يجب أن ينمو حجمه بطريقة أفضل من صيغة SF-VHPT، إذ تتطلب كل عملية أعدادًا متزايدة من مدخلات صفحات جدول VLPT في مخزن TLB. لكن تكون المدخلات الأكبر أقل ملاءمة للذاكرة المخبئية، إذ يمكننا ملاءمة أربعة مدخلات ذات صيغة قصيرة بحجم 8 بايتات مع كل مدخلة ذات صيغة طويلة بحجم 32 بايت. يمكن أن تساعد الذواكر المخبئية الكبيرة جدًا الموجودة على معالج إيتانيوم في تخفيف هذا التأثير. تتمثل إحدى مزايا صيغة SF-VHPT في أن نظام التشغيل يمكنه الاحتفاظ بالترجمات في جدول صفحات هرمي، ويمكنه ربط الصفحات الورقية في هذه البنية الهرمية مباشرةً مع جدول VLPT مع الاحتفاظ بصيغة الترجمة العتادية. بينما يجب على نظام التشغيل باستخدام صيغة LF-VHPT إما استخدام جدول التعمية بوصفه مصدرًا أساسيًا لمدخلات الترجمة أو الاحتفاظ بجدول التعمية بوصفه ذاكرة مخبئية لمعلومات الترجمة الخاصة به. يُعَد الاحتفاظ بجدول التعمية بصيغة LF-VHPT بوصفه ذاكرة مخبئية دون المستوى الأمثل إلى حد ما بسبب زيادة الحِمل في مسارات الأخطاء الأساسية في الوقت المناسب، ولكن تُكتسَب الفوائد من الجدول الذي يتطلب مدخلة واحدة فقط في مخزن TLB. ترجمة -وبتصرُّف- للقسمين Hardware Support و Hardware support for virtual memory من فصل Virtual Memory من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال السابق: الذاكرة الوهمية والذاكرة الحقيقية في معمارية الحاسوب المقال التالي: كيف تنشئ ملفا قابلا للتنفيذ Executable File من شيفرة برمجية مصدرية نظرة عميقة على تسلسل الذواكر الهرمي والذاكرة المخبئية في معمارية الحاسوب تعرف على نظام العد الثنائي Binary أساس الحوسبة
  10. يُعَد نهج التكامل والتسليم والنشر المستمر -المعروف باسم CI/CD- جزءًا لا يتجزأ من التطوير الحديث الذي يهدف إلى تقليل الأخطاء أثناء التكامل والنشر مع زيادة سرعة إنجاز المشروع الرقمي، حيث يمثل هذا النهج فلسفة ومجموعة من الممارسات التي يعزّزها استخدام أدوات قوية تؤكد على الاختبار المؤتمت في كل مرحلة من مراحل خط إنتاج Pipeline البرمجيات، وبالتالي يمكنك من خلال دمج هذه الأفكار ضمن ممارساتك تقليلُ الوقت المطلوب لتكامل التغييرات لإصدارٍ ما واختبار كل تغيير بدقة قبل نقله إلى مرحلة الإنتاج. يتمتع نهج CI/CD بالعديد من الفوائد، ولكن يتطلب التقديم الناجح لهذا النهج في أغلب الأحيان قدرًا كبيرًا من الأمور التي يجب أن تضعها في الحسبان، إذ يمكن أن يكون تحديد كيفية استخدام الأدوات والتغييرات التي قد تحتاجها في عملياتك أمرًا صعبًا بدون اتباع طريقة التجربة والخطأ، ولكن ستكون جميع عمليات التقديم مختلفة، لذا يمكن أن يساعدك الالتزام بأفضل الممارسات على تجنب المشاكل الشائعة والتحسن بصورة أسرع. سنرشدك في هذا المقال لتعلم مبادئ كيفية تقديم وصيانة منظومة CI/CD لخدمة احتياجات مؤسستك بأفضل طريقة، حيث سنوضح عددًا من الممارسات التي ستساعدك على تحسين فعالية خدمة منظومة CI/CD. لا تتردد في القراءة بالترتيب الموجود أو يمكنك الانتقال إلى المجالات التي تهمك مباشرةً. حافظ على سرعة خطوط الإنتاج تساعد خطوط إنتاج CI/CD في الاهتمام بالتغييرات ابتداءً من دورات الاختبار المؤتمت ثم بيئات التحضير Staging Environments وأخيرًا مرحلة الإنتاج، فكلما كانت خطوط الإنتاج تشمل مرحلة الاختبار، زادت ثقتك في أن التغييرات لن تحدث آثارًا جانبية غير متوقعة في مرحلة النشر والتسليم، ولكن يجب أن يمر كل تغيير بهذه العملية، لذا يُعَد الحفاظ على خطوط إنتاجك سريعة وموثوقة أمرًا مهمًا للغاية. يمكن أن يكون من الصعب الموازنة بين هذين المطلبين، فهناك بعض الخطوات المباشرة التي يمكنك اتخاذها لتحسين السرعة مثل توسيع بنية CI/CD التحتية وتحسين مرحلة الاختبارات، ولكن قد تضطر مع مرور الوقت إلى اتخاذ قرارات حاسمة بشأن القيمة النسبية للاختبارات المختلفة ومرحلة أو ترتيب إجرائها. لذا يُعَد تقليص مجموعة اختباراتك -من خلال إزالة الاختبارات ذات القيمة المنخفضة أو التي لا تدل على أمور جوهرية- أذكى طريقة في بعض الأحيان للحفاظ على السرعة التي تتطلبها خطوط الإنتاج المُستخدَمة بكثرة. تأكد عند اتخاذ هذه القرارات المهمة من فهم وتوثيق المقايضات التي تطبّقها، وتشاور مع أعضاء الفريق وأصحاب المصلحة لمواءمة افتراضات الفريق حول مسؤولية مجموعة الاختبار وما يجب التركيز عليه. دورة إدارة تطوير المنتجات احترف إدارة تطوير المنتجات الرقمية بدءًا من التخطيط وتحليل السوق وحتى إطلاق منتج مميز وناجح اشترك الآن عزل وتأمين بيئة منظومة CI/CD تمثل منظومة CI/CD إحدى أكثر البنى التحتية أهمية التي يجب حمايتها من وجهة نظر الأمان التشغيلي، إذ يجب تأمينها بما أن لديها وصولًا كاملًا إلى شيفرتك البرمجية الأساسية وثبوتياتك لنشرها في بيئات مختلفة، ويجب عزلها وقفلها قدر الإمكان نظرًا لقيمتها العالية بوصفها هدفًا للمهاجمين. يجب نشر أنظمة CI/CD على الشبكات الداخلية المحمية دون أن تكون مكشوفة لأطراف خارجية، لذلك يوصَى بإعداد شبكات VPN أو أي تقنية أخرى للتحكم في الوصول إلى الشبكة للتأكد من أن المشغّلين الموثقين فقط هم من يمكنهم الوصول إلى نظامك. يمكن أن تحتاج منظومة CI/CD خاصتك إلى الوصول إلى عدة شبكات مختلفة لنشر الشيفرة البرمجية في بيئات مختلفة اعتمادًا على مدى تعقيد مخطط شبكتك، فإن لم تُؤمَّن أو تُعزَل بصورة صحيحة، فقد يتمكّن المهاجمون الذين يمكنهم الوصول إلى بيئة واحدة من استخدام تقنية قفز الجزيرة Island Hop -وهي تقنية تُستخدَم لتوسيع الوصول من خلال الاستفادة من قواعد الشبكات الداخلية الأكثر تساهلًا- للوصول إلى بيئات أخرى عبر نقاط ضعف خوادم CI/CD. تعتمد استراتيجيات العزل والأمان المطلوبة بصورة كبيرة على مخطط الشبكة وبنيتها التحتية ومتطلبات الإدارة والتطوير. يجب أن تضع في بالك أن أنظمة CI/CD هي أهداف ذات قيمة عالية وتتمتع في كثير من الحالات بدرجة كبيرة من الوصول إلى أنظمتك الأساسية الأخرى، وبالتالي ستساعد حماية الوصول الخارجي إلى الخوادم والتحكم في أنواع الوصول الداخلي المسموح به في تقليل مخاطر تعرض منظومة CI/CD للخطر. جعل خط إنتاج CI/CD الطريقة الوحيدة للنشر في بيئة الإنتاج تساعد الأدوات في أغلب الأحيان في فرض أفضل الممارسات للاختبار والنشر، مما يمكّن منظومة CI/CD من تحسين ممارسات التطوير وجودة الشيفرة البرمجية، إذ تتطلب ترقية الشيفرة البرمجية عبر خطوط إنتاج CI/CD أن يثبت كل تغيير التزامه بالمعايير والإجراءات المُوثّقة الخاصة بمؤسستك. تظهر حالات الفشل في خط إنتاج CI/CD مباشرةً ويتوقف تقدم الإصدار المتأثر إلى مراحل لاحقة من الدورة، إذ تحمي هذه الآلية البيئات الأكثر أهمية من الشيفرة البرمجية غير الموثوق بها. يمكن تحقيق هذه المزايا من خلال التأكد من أن كل تغيير في بيئة إنتاجك يمر عبر خط إنتاجك، إذ يجب أن يكون خط إنتاج CI/CD هو الآلية الوحيدة التي تدخل من خلالها الشيفرة البرمجية إلى بيئة الإنتاج. يمكن أن يحدث ذلك تلقائيًا في نهاية الاختبار الناجح مع ممارسات النشر المستمر، أو من خلال الترقية اليدوية للتغييرات المُختبَرة التي تعتمدها وتتيحها منظومة CI/CD. تبدأ الفِرق باستمرار في استخدام خطوط إنتاجهم للنشر، ولكنها تجري استثناءات عند حدوث مشاكل مع وجود ضغط لحلها بسرعة. يجب التخفيف من وقت التوقف والمشاكل الأخرى في أسرع وقت ممكن، ولكن يجب أن نفهم أن منظومة CI/CD هي أداة جيدة لضمان ألّا تدخل تغييراتك أخطاءً أخرى أو تسبّب تعطل النظام. سيؤدي وضع إصلاحٍ ما لشيفرتك عبر خط الإنتاج (أو استخدام منظومة CI/CD للتراجع) إلى منع نشر الإصدار التالي للتطبيق من إزالة الإصلاحات العاجلة التي طبقتها يدويًا على البيئة الإنتاجية، إذ يحمي خط الإنتاج صلاحية عمليات النشر بغض النظر عمّا إذا كان ذلك إصدارًا منتظمًا أو مخططًا له أو إصلاحًا سريعًا لحل مشكلة جارية. يُعَد هذا الاستخدام لمنظومة CI/CD سببًا آخر للعمل على إبقاء خط إنتاجك سريعًا. الحفاظ على التطابق مع البيئة الإنتاجية ما أمكن ترفع خطوط إنتاج CI/CD التغييرات من خلال سلسلة من مجموعات الاختبار وبيئات النشر، إذ تُنشَر التغييرات التي تجتاز متطلبات مرحلةٍ تلقائيًا أو توضَع في طابور للنشر اليدوي في بيئات أكثر تقييدًا، حيث تهدف المراحل المبكرة إلى إثبات أنه من المفيد مواصلة الاختبار ودفع التغييرات لتصبح مناسبة للبيئة الإنتاجية. يساعد أن تكون بيئة الاختبار مطابقة قدر الإمكان للبيئة الإنتاجية أن نضمن في المراحل اللاحقة أن تظهِر الاختبارات بدقة كيفية تصرّف التغيير في البيئة الإنتاجية. يمكن أن تسمح الاختلافات الكبيرة بين بيئة التحضير staging وبيئة الإنتاج production أن تظهر مشاكل لم يعثر عليها في مرحلة الاختبار، وكلما زادت الاختلافات بين بيئتك الإنتاجية وبيئة الاختبار، قلَّ قياس اختباراتك لكيفية أداء الشيفرة البرمجية عند إصدارها. يُتوقَّع وجود بعض الاختلافات بين بيئتي التحضير والإنتاج، ولكن يجب إبقاؤها قابلة للإدارة والتأكد من فهمها جيدًا، إذ تستخدم بعض المؤسسات عمليات النشر الأزرق والأخضر Blue-green Deployments لمبادلة حركة مرور الإنتاج بين بيئتين متطابقتين تقريبًا تتناوبان بين كونهما بيئتي تحضير وإنتاج، وتضمنت الاستراتيجيات الأقل حِدةً نشر الضبط والبنية التحتية نفسها من الإنتاج إلى بيئة التحضير، ولكن على نطاق أقل. قد تختلف عناصرٌ مثل نقاط نهاية الشبكة بين بيئاتك، ولكن يمكن أن يساعد تحديد المعاملات لهذا النوع من البيانات المتغيرة في التأكد من أن الشيفرة البرمجية متناسقة وأن الاختلافات البيئية مُحدَّدة جيدًا. البناء مرة واحدة فقط وترقية النتائج عبر خط الإنتاج الهدف الأساسي لخط إنتاج التكامل المستمر والتسليم المستمر CI/CD هو بناء الثقة في تغييراتك وتقليل فرصة حدوث تأثير غير متوقع، حيث ناقشنا أهمية الحفاظ على التطابق بين البيئات، ولكن يجب ضمان مزيد من الاهتمام في جميع المكونات، فإن تطلّب برنامجك خطوة بناء أو حزمٍ أو تجميع، فيجب تنفيذ هذه الخطوة مرة واحدة فقط وإعادة استخدام الخرج الناتج على طول خط الإنتاج بأكمله. يساعد هذا المقال في منع المشاكل التي تنشأ عند تصريف البرمجيات أو حزمها عدة مرات، مما يسمح بمنع تعارضات ضئيلة في المنتجات الثانوية الناتجة، إذ يمكن أن يعني بناء البرمجيات بصورة منفصلة في كل مرحلة جديدة أن الاختبارات في البيئات السابقة لم تستهدف البرمجيات نفسها الذي ستُنشَر لاحقًا، مما يؤدي إلى إبطال النتائج. يمكن تجنب هذه المشكلة من خلال أن تتضمن أنظمة CI عملية بناء بوصفها خطوة أولى في خط الإنتاج الذي ينشئ البرمجية ويحزمها في بيئة نظيفة، ويجب إصدار المنتج الثانوي الناتج وتحميله إلى نظام تخزين المنتجات الثانوية لتسحبها المراحل اللاحقة من خط الإنتاج، مما يضمن عدم تغيير البناء أثناء تقدمه عبر النظام. إجراء أسرع الاختبارات في وقت مبكر يُعَد إبقاء خط الإنتاج سريعًا بالكامل هدفًا عامًا ورائعًا، ولكن ستكون أجزاءٌ من مجموعة اختبارك حتمًا أسرع من غيرها. تعمل منظومة CI/CD بوصفها ممرًا لجميع التغييرات التي تدخل نظامك، لذا يُعَد اكتشاف حالات الفشل في أقرب وقت ممكن أمرًا مهمًا لتقليل الموارد المُخصَّصة لعمليات البناء التي تسبب مشاكلًا، ويمكن تحقيق ذلك من خلال تحديد أولويات الاختبارات الأسرع وإجرائها أولًا. احفظ الاختبارات المعقدة التي تستغرق وقتًا طويلًا حتى بعد التحقق من صحة البناء باستخدام اختبارات صغيرة سريعة التشغيل. تحتوي هذه الإستراتيجية على عدد من الفوائد التي يمكن أن تساعد في الحفاظ على صحة عملية CI/CD، وتشجعك على فهم تأثير أداء الاختبارات الفردية، وتسمح لك بإكمال معظم اختباراتك مبكرًا، وتزيد من احتمالية الفشل السريع، مما يعني أنه يمكن التراجع عن التغييرات التي تسبب مشاكلًا أو إصلاحها قبل وقف عمل أعضاء الفريق الآخرين. يعني تحديدُ أولويات الاختبار تشغيلَ اختبارات الوحدة لمشروعك أولًا، لأن هذه الاختبارات تميل إلى أن تكون سريعة ومعزولة وتركز على المكونات، وتمثّل اختبارات التكامل بعد ذلك المستوى التالي من التعقيد والسرعة، وتليها اختبارات على مستوى النظام، وأخيرًا اختبارات القبول التي تتطلب غالبًا مستوًى معينًا من التفاعل البشري. تقليل التفريع في نظام التحكم في الإصدارات يتمثل أحد المبادئ الرئيسية لمنظومة CI/CD في دمج التغييرات في المستودع المشترك الأساسي مبكرًا وفي مرات متعددة، مما يساعد في تجنب مشاكل التكامل المتعبة باستمرار عندما يحاول العديد من المطورين دمج تغييرات كبيرة ومتباينة ومتضاربة في الفرع الرئيسي للمستودع استعدادًا للنشر، إذ تُضبَط أنظمة CI/CD لمراقبة واختبار التغييرات الملتزمة بفرع واحد فقط أو عدة فروع. يمكن الاستفادة من المزايا التي يوفرها التكامل المستمر CI من خلال تحديد عدد الفروع ونطاقها في مستودعك، حيث تشير معظم التطبيقات إلى أن المطورين يلتزمون مباشرة بالفرع الرئيسي main أو master أو يدمجون التغييرات من الفروع المحلية مرةً واحدةً على الأقل يوميًا. تحتوي الفروع التي لا تتعقّبها منظومة CI/CD على شيفرة برمجية غير مُختبَرة بغض النظر عن أهميتها أو وظيفتها، حيث يساعد التقليل من التفريع لتشجيع التكامل المبكر بين شيفرات المطورين المختلفة على الاستفادة من نقاط القوة في النظام، ويمنع المطورين من إبطال المزايا التي يوفرها. إجراء الاختبارات محليًا قبل الالتزام بخط إنتاج CI/CD يجب تشجيع المطورين على إجراء بعض الاختبارات محليًا قبل رفعها على المستودع المشترك فيما يتعلق بنقطة اكتشاف حالات الفشل مبكرًا، مما يجعل من الممكن اكتشاف بعض التغييرات التي تسبب مشاكلًا قبل وقف أعضاء الفريق الآخرين. ليس محتملًا أن تكون بيئة المطور المحلية قادرة على تشغيل مجموعة الاختبارات بأكملها في بيئة شبيهة ببيئة الإنتاج، ولكن تمنح هذه الخطوة الإضافية الأفراد مزيدًا من الثقة بأن التغييرات التي يجرونها تجتاز الاختبارات الأساسية وتستحق محاولة الاندماج مع الشيفرة البرمجية الأساسية الأكبر. يمكن التأكد من أن المطورين يمكنهم الاختبار بفعالية من تلقاء أنفسهم من خلال أن تكون مجموعة اختباراتك قابلة للتشغيل باستخدام أمر واحد يمكن تشغيله من أيّ بيئة، إذ يجب أن تستخدم منظومة CI/CD الأمر نفسه الذي يستخدمه المطورون على أجهزتهم المحلية لبدء الاختبارات على الشيفرة البرمجية المضافة في المستودع، ويمكن تنسيق ذلك في أغلب الأحيان من خلال توفير سكربت bash أو أداة makefile لأتمتة تشغيل أدوات الاختبار بطريقة تكرارية ويمكن التنبؤ بها. إجراء الاختبارات في بيئات مؤقتة عندما يكون ذلك ممكنًا يكون من الجيد في أغلب الأحيان استخدام بيئات اختبار نظيفة ومؤقتة عندما يكون ذلك ممكنًا للمساعدة في ضمان إجراء الاختبارات بالطريقة نفسها في مراحل مختلفة، وهذا يعني تشغيل الاختبارات ضمن حاويات Containers لتجريد الاختلافات بين الأنظمة المضيفة ولتوفير واجهة برمجة تطبيقات معيارية لربط المكونات مع بعضها البعض بمقاييس مختلفة. تعمل الحاويات بأدنى قد من المعلومات المُحتفظة minimal state لذا لا تطّلع عمليات التشغيل اللاحقة لمجموعة الاختبارات على الآثارَ الجانبية أو حالات الاختبارات المحفوظة التي يمكن أن تؤدي إلى إفساد النتائج. هناك فائدة أخرى لبيئات الاختبار الموضوعة ضمن حاويات، وهي قابلية نقل بنية اختبارك التحتية، إذ يمتلك المطورون باستخدام الحاويات وقتًا أسهل لتكرار الضبط الذي سيُستخدَم لاحقًا في خط الإنتاج دون الحاجة إلى إعداد البنية التحتية يدويًا وصيانتها أو التضحية بدقة أداء البيئة. يمكن إنشاء الحاويات بسهولة عند الحاجة ثم تدميرها، لذا يمكن للمستخدمين تقديم تنازلات أقل فيما يتعلق بدقة بيئة اختبارهم عند إجراء الاختبارات المحلية. يقيد استخدام الحاويات في بعض الجوانب بيئة التنفيذ للمساعدة في تقليل الاختلافات بين مراحل خطوط الإنتاج. الخلاصة سيكون كل تقديم لمنظومة CI/CD مختلفًا عن الآخر، ولكن سيساعدك اتباع بعض المبادئ الأساسية التي وضّحناها في هذا المقال على تجنب بعض العقبات الشائعة وتعزيز ممارسات الاختبار والتطوير، وسيساعد مزيج من العملية والأدوات والعادات في جعل تغييرات التطوير أكثر نجاحًا وتأثيرًا كما هو الحال مع معظم جوانب منهج التكامل المستمر والتسليم المستمر. ترجمة -وبتصرُّف- للمقال An Introduction to CI/CD Best Practices لصاحبه Justin Ellingwood. اقرأ أيضًا استخدام GitHub Actions لتحقيق التكامل المستمر والنشر المستمر التوسع أكثر في نهج التكامل والتسليم المستمر إعداد التكامل المستمر والنشر المستمر باستخدام الخدمتين CircleCI وCoveralls
  11. يمكن القول بأن الذاكرة الوهمية Virtual Memory هي طريقة لتوسيع الذاكرة RAM من خلال استخدام القرص الصلب بوصفه ذاكرة نظام إضافية ولكنها أبطأ، أي ينتقل النظام إلى القرص الصلب الذي يُستخدَم بوصفه ذاكرةً وهمية بمجرد نفاد الذاكرة في نظامك. يُشار إلى الذاكرة الوهمية عادةً في أنظمة التشغيل الحديثة باسم ذاكرة سواب Swap Space، لأن الأجزاء غير المُستخدَمة من الذاكرة تُبعَد إلى القرص الصلب لتحرير الذاكرة الرئيسية، إذ لا يمكن تنفيذ البرامج إلا من الذاكرة الرئيسية. تُعَد القدرة على إبعاد الذاكرة إلى القرص الصلب أمرًا مهمًا، ولكنها ليست الغرض الأساسي للذاكرة الوهمية، بل لها تأثيرٌ آخر مفيد للغاية سنراه لاحقًا. ما هي الذاكرة الوهمية Virtual Memory؟ تدور الذاكرة الوهمية حول فكرة الاستفادة من فضاء العناوين Address Space، حيث يشير فضاء العناوين الخاص بالمعالج إلى مجال العناوين المُحتمَلة التي يمكن استخدامها عند التحميل والتخزين في الذاكرة. يُعَد فضاء العناوين محدودًا بعرض المسجّلات Registers، لأننا نحتاج لتحميل عنوانٍ إطلاقَ تعليمة تحميل load مع العنوان الذي سيُحمَّل منه العنوان المُخزَّن في المسجّل، إذ يمكن مثلًا أن تحتوي المسجلات التي يبلغ عرضها 32 بتًا على عناوين في مجال المسجل من 0x00000000 إلى 0xFFFFFFF. يساوي 2‎32‎ ما مقداره 4 جيجابايت، لذلك يمكن للمعالج ذي 32 بت تحميلُ أو تخزين ما يصل إلى 4 جيجابايتات من الذاكرة. المعالجات ذات 64 بتا جميع المعالجات الجديدة هي معالجات 64 بتًا التي -كما يوحي اسمها- تحتوي على مسجّلات بعرض 64 بتًا، حيث يكون فضاء العناوين المتاح لهذه المعالجات كبيرًا. تحتوي المعالجات ذات 64 بت على بعض المقايضات مقابل استخدام معالجات ذات عرض بتات أصغر، حيث يتطلب كل برنامجٍ مُصرَّفٍ Compiled في وضع 64 بتًا مؤشرات حجمها 8 بايتات، والتي يمكن أن تزيد من حجم الشيفرة البرمجية والبيانات، وبالتالي تؤثر على أداء كل من الذاكرة المخبئة الخاصة بالتعليمة والبيانات، ولكن تميل معالجات 64 بت إلى الحصول على عدد أكبر من المسجلات، مما يعني تقليل الحاجة إلى حفظ المتغيرات المؤقتة في الذاكرة عندما يكون المصرّف Compiler واقعًا تحت الضغط القادم من المسجّلات. العناوين المعيارية Canonical Addresses تحتوي معالجات 64 بت على مسجلات بعرض 64 بتًا، ولكن لا تطبّق الأنظمة جميع هذه 64 بتًا للعنونة، إذ لا يُعَد تحميل load أو تخزين store كل 16 إكسابايت من الذاكرة الحقيقية أمرًا ممكنًا. لذا تحدّد معظم المعماريات منطقةً غير قابلة للاستخدام Unimplemented من فضاء العناوين التي يَعُدّها المعالج غير صالحة للاستخدام. تعرِّف كلٌّ من المعماريتين x86-64 وإيتانيوم Itanium البت الصالح الأكثر أهمية في العنوان، ويجب بعد ذلك تمديد إشارته لإنشاء عنوان صالح، والنتيجة هي تقسيم إجمالي فضاء العناوين بفعالية إلى جزأين هما: جزء علوي وجزء سفلي مع وجود عناوين غير صالحة بينهما، وهذا موضح في الشكل الآتي. تُسمَّى العناوين الصالحة عنواين معيارية Canonical Addresses، بينما تُسمَّى العناوين غير الصالحة عناوين غير معيارية Non-canonical. يمكن العثور على قيمة البت الأكثر أهمية للمعالج من خلال الاستعلام عن المعالج نفسه باستخدام تعليمة الحصول على المعلومات. ستكون قيمة البت الأكثر أهمية 48 بالرغم من أن القيمة الدقيقة تعتمد على التقديم Implementation، مما يؤدي إلى توفير 2‎48‎ = 256 تيرابايت TiB من فضاء العناوين القابلة للاستخدام. يُعَد تقليل فضاء العناوين المُحتمَل أنه يمكن تحقيق توفيرٍ كبير مع جميع أجزاء منطق العنونة في المعالج والمكونات ذات الصلة، لأنها تعلم أنها لن تحتاج للتعامل مع عناوين 64 بت كاملة. يحدّد التقديمُ البتات العليا على أنه يجب تمديد إشارتها، مما يؤدي إلى منع أنظمة التشغيل القابلة للنقل التي تستخدم هذه البتات لتخزين أو تحديد المعلومات الإضافية وضمان التوافق عند الرغبة في تقديم مزيدٍ من فضاء العناوين مستقبلًا. دورة علوم الحاسوب دورة تدريبية متكاملة تضعك على بوابة الاحتراف في تعلم أساسيات البرمجة وعلوم الحاسوب اشترك الآن استخدام فضاء العناوين Address space تعمل الذاكرة الافتراضية -كما هو الحال مع معظم مكونات نظام التشغيل- بوصفها تجريدًا بين فضاء العناوين والذاكرة الحقيقية المتوفرة في النظام، أي إذا استخدم برنامجٌ ما عنوانًا، فلن يشير العنوان إلى البتات الموجودة في الموقع الفعلي الحقيقي في الذاكرة، لذا نقول أن جميع العناوين التي يستخدمها البرنامج هي عناوين وهمية. يتعقّب نظام التشغيل العناوين الوهمية وكيفية تخصيصها للعناوين الحقيقية، فإذا طبّق أحد البرامج عملية تحميل أو تخزين من عنوانٍ ما، فسيعمل المعالج ونظام التشغيل مع بعضهما البعض لتحويل هذا العنوان الوهمي إلى العنوان الحقيقي في شرائح ذاكرة النظام. الصفحات Pages يُقسَم إجمالي فضاء العناوين إلى صفحات Pages. يمكن أن تكون الصفحات بأحجام مختلفة، حيث يمكن أن يبلغ حجمها حوالي 4 كيلوبايت KiB، ولكنها ليست قاعدة صارمة ويمكن أن تكون أكبر بكثير ولكنها ليست أصغر من ذلك. تُعَد الصفحة أصغر وحدة ذاكرة يمكن لنظام التشغيل والعتاد التعامل معها. تحتوي كل صفحة على عدد من السمات التي يضبطها نظام التشغيل، وتشمل أذونات القراءة والكتابة والتنفيذ للصفحة الحالية، حيث يمكن لنظام التشغيل مثلًا تمييز صفحات الشيفرة البرمجية لعمليةٍ ما باستخدام راية قابلة للتنفيذ ويمكن للمعالج اختيار عدم تنفيذ أيّ شيفرة برمجية من الصفحات بدون ضبط هذه البتات. يمكن أن يفكر المبرمجون في هذه المرحلة في أنه يمكنهم بسهولة تخصيص كميات صغيرة من الذاكرة -أي أصغر بكثير من 4 كيلوبايتات- باستخدام الاستدعاء malloc أو استدعاءات مماثلة. تدعم عمليات تخصيص حجم الصفحة كومةَ Heap الذاكرة، حيث يقسمها تقديم الاستدعاء malloc ويديرها بطريقة فعّالة. الذاكرة الحقيقية Physical Memory يقسم نظام التشغيل فضاء العناوين المُحتمَلة إلى صفحات Pages، ويقسم الذاكرة الحقيقية المتاحة إلى إطارات Frames، حيث يُعَد الإطار الاسم التقليدي لقطعة كبيرة من الذاكرة الحقيقية لها حجم صفحة النظام نفسها. يحتفظ نظام التشغيل بجدول الإطارات Frame-table الذي يُعَد قائمةً بجميع الصفحات المُحتمَلة للذاكرة الحقيقية ويحدد ما إذا كانت حرةً أو متاحة للتخصيص أم لا. إذا خُصِّصت الذاكرة لعمليةٍ ما، فستُميَّز على أنها مُستخدَمة في جدول الإطارات، وبذلك يتعقّب نظام التشغيل جميع عمليات تخصيص الذاكرة. يعرف نظام التشغيل الذاكرة المتوفرة من خلال تمرير المعلومات الخاصة بمكان وجود الذاكرة ومقدارها وسماتها وغير ذلك إلى نظام التشغيل باستخدام نظام BIOS أثناء عملية التهيئة Initialisation. جداول الصفحات تتمثل مهمة نظام التشغيل في تعقّب نقاط الصفحة الوهمية المقابلة للإطار الحقيقي، حيث يجري الاحتفاظ بهذه المعلومات في جدول صفحات. يمكن أن يكون جدول الصفحات في أبسط أشكاله جدولًا يحتوي كل صف فيه على الإطار المرتبط به، وهذا ما يسمى بجدول الصفحات الخطي Linear Page-table. وإن استخدمتَ هذا النظام البسيط مع فضاء عناوين بحجم 32 بتًا وصفحات بحجم 4 كيلوبايت، فسيكون هناك 1048576 صفحة يمكن تعقّبها في جدول الصفحات (أي 2‎32 ÷ 4096)، وبالتالي سيكون طول الجدول 1048576 مدخلةً لضمان أنه يمكننا دائمًا ربط صفحة وهمية مع صفحة حقيقية. يمكن أن تحتوي جداول الصفحات على العديد من البنى المختلفة ويمكن تحسينها بدرجة كبيرة، إذ يمكن أن تستغرق عملية البحث عن صفحة في جدول الصفحات وقتًا طويلًا. سنتطرق إلى جدول الصفحات بمزيد من التفصيل لاحقًا. يخضع جدول صفحات العملية لتحكم نظام التشغيل الحصري، فإذا طلبت إحدى العمليات ذاكرةً، فسيجد نظام التشغيل صفحة خالية من الذاكرة الحقيقية ويسجّل ترجمة الصفحة الوهمية إلى الصفحة الحقيقية Virtual-to-physical في جدول صفحات العمليات. بينما إن تخلت العملية عن الذاكرة، فسيُزال سجل ترجمة الصفحة الوهمية إلى الصفحة الحقيقية ويصبح الإطار الأساسي حرًا لتخصيصه لعملية أخرى. العناوين الوهمية Virtual Address لا يعرف أو يهتم البرنامج عند وصوله إلى الذاكرة بمكان تخزين الذاكرة الحقيقية التي تدعم العنوان، ولكنه يعرف أن الأمر متروك لنظام التشغيل والعتاد، بحيث يتعاونان للربط مع العنوان الحقيقي الصحيح وبالتالي توفير الوصول إلى البيانات التي يريدها. لذا نطلق على العنوان الذي يستخدمه البرنامج للوصول إلى الذاكرة عنوانًا وهميًا Virtual Address الذي يتكون من جزأين هما: الصفحة Page والإزاحة Offset في هذه الصفحة. الصفحة يُقسَم فضاء العناوين المُحتمَل إلى صفحات ذات حجم ثابت، حيث يتواجد كل عنوان ضمن صفحة، ويعمل مكون الصفحة الخاص بالعنوان الوهمي بوصفه فهرسًا إلى جدول الصفحات. تُعَد الصفحة أصغر وحدة لتخصيص الذاكرة في النظام، لذلك هناك مقايضة بين جعل الصفحات صغيرة جدًا مع وجود عدد كبير جدًا منها ليديرها نظام التشغيل وبين جعل الصفحات أكبر مع وجود احتمال في هدر الذاكرة. الإزاحة Offset تُسمَّى البتات الأخيرة من العنوان الوهمي بالإزاحة Offset التي تعبّر عن الفرق في الموقع بين عنوان البايت الذي تريده وبداية الصفحة، إذ يجب وجود بتات كافية في الإزاحة لتتمكن من الوصول إلى أيّ بايت في الصفحة، حيث تحتاج بالنسبة لصفحة بحجم 4 كيلوبايتات إلى 12 بتًا للإزاحة حيث 4K = 4 * 1024 = 4096 = 2‎12‎. تذكر أن أقل قدر من الذاكرة يتعامل معه نظام التشغيل أو العتاد يساوي صفحة، لذا يوجد كل بتٍ من 4096 بايتًا ضمن صفحة واحدة ويجري التعامل معها على أنها كتلة واحدة. ترجمة العنوان الوهمي Virtual Address تشير ترجمة العناوين الوهمية إلى عملية اكتشاف الصفحة الحقيقية المربوطة مع الصفحة الوهمية. سنتعامل فقط مع رقم الصفحة عند ترجمة عنوان وهمي إلى عنوان حقيقي، حيث نأخذ رقم الصفحة من العنوان المُعطَى ونبحث عنه في جدول الصفحات للعثور على مؤشر إلى عنوان حقيقي مع إضافة الإزاحة من العنوان الوهمي إليه، مما يؤدي إلى إعطاء الموقع الفعلي في نظام الذاكرة. تخضع جداول الصفحات لسيطرة نظام التشغيل، فإن لم يكن العنوان الوهمي موجودًا في جدول الصفحات، فسيعرف نظام التشغيل أن العملية تحاول الوصول إلى الذاكرة التي ليست مخصَّصةً لها ولن يُسمَح لها بالوصول. ترجمة العنوان الوهمي يوضّح المثال السابق جدول صفحات خطي بسيط، حيث سيتطلب فضاء العناوين ذو 32 بتًا جدولًا مؤلفًا من 1048576 مدخلةً عند استخدام صفحات بحجم 4 كيلوبايتات، وبالتالي ستكون الخطوة الأولى لربط العنوان 0x80001234 هي إزالة بتات الإزاحة. نعلم في هذه الحالة أن لدينا 12 بتًا (2‎12‎ = 4096) من الإزاحة مع صفحات بحجم 4 كيلوبايتات. لذا سنزيح 12 بتًا من العنوان الوهمي إزاحةً يمنى، وبالتالي يبقى لدينا 0x80001، وستكون القيمة العشرية الموجودة في السطر رقم 524289 من جدول الصفحات الخطي هي الإطار الحقيقي المقابل لهذه الصفحة. يمكن أن ترى مشكلة في جدول الصفحات الخطي، حيث يجب حساب كل صفحة سواء كانت قيد الاستخدام أم لا، وبالتالي لا يُعَد جدول الصفحات الخطي الحقيقي عمليًا تمامًا مع فضاء عناوين 64 بت. ضع في حساباتك فضاء عناوين 64 بت المقسَّم إلى صفحات مؤلفة من 64 كيلوبايت (كبيرة جدًا)، حيث ينشئ هذا الفضاء 2‎64/2‎16=2‎52‎ صفحة لإدارتها. لنفترض أن كل صفحة تتطلب مؤشرًا بحجم 8 بايتات لموقع حقيقي، فسيتطلب ذلك 2‎52*2‎3=2‎55‎ أو 512 جيجابايت GiB من الذاكرة المتجاورة لجدول الصفحات فقط. مفاهيم متعلقة بالعناوين الوهمية والصفحات وجداول الصفحات تُعَد العناوين الوهمية والصفحات وجداول الصفحات أساس كل نظام تشغيل حديث، لأنها تشكّل أساس معظم الأشياء التي نستخدم أنظمتنا من أجلها. فضاءات العناوين المفردة يمكن لكل عملية التظاهر بأنها تستطيع الوصول إلى فضاء العناوين الكامل المتاح من المعالج من خلال إعطاء كل عملية جدول صفحات خاص بها، إذ يمكن أن تستخدم عمليتان العنوان نفسه، حيث ستربط جداولُ الصفحات المختلفة العمليةَ مع إطار مختلف من الذاكرة الحقيقية، إذ توفّر أنظمة التشغيل الحديثة لكل عمليةٍ فضاءَ عناوين خاص بها. تصبح الذاكرة الحقيقية مجزأة Fragmented بمرور الوقت، مما يعني أن هناك ثقوب في الفضاء الحر من الذاكرة الحقيقية. سيكون الاضطرار إلى حل مشكلة هذه الثقوب أمرًا مزعجًا في أحسن الأحوال ولكنه سيصبح أمرًا خطيرًا للمبرمجين، فإذا نفّذتَ الاستدعاء malloc لتخصيص 8 كيلوبايتات من الذاكرة مثلًا، فسيتطلب ذلك دعم إطارين بحجم 4 كيلوبايتات، وبالتالي لن تكون هذه الإطارات متجاورة، أي بجوار بعضها البعض فعليًا. لا يُعَد استخدام العناوين الوهمية أمرًا مهمًا بقدر ما يتعلق الأمر باحتواء العملية على 8 كيلوبايت من الذاكرة المتجاورة، حتى لو كانت هذه الصفحات مدعومة بإطارات متباعدة جدًا. يمكن للمبرمج ترك مهمة حل مشكلة التجزئة لنظام التشغيل من خلال إسناد فضاء عناوين وهمية لكل عملية. الحماية يُدعَى الوضع الوهمي للمعالج 386 بالوضع المحمي Protected Mode، وينشأ هذا الاسم من الحماية التي يمكن أن توفرها الذاكرة الوهمية للعمليات التي تعمل عليها. تتمتع كل عملية في نظام بدون ذاكرة وهمية بوصولٍ كامل إلى ذاكرة النظام بأكملها، وهذا يعني أنه لا يوجد شيء يمنع عمليةً ما من الكتابة فوق ذاكرة عمليات أخرى، مما يؤدي إلى تعطّلها أو إعادة قيم غير صحيحة في أسوأ الأحوال خاصة إذا كان هذا البرنامج يدير حسابك المصرفي مثلًا. لذا يجب توفير هذا المستوى من الحماية لأن نظام التشغيل يُعَد طبقة تجريد بين العملية والوصول إلى الذاكرة، فإذا أعطت العملية عنوانًا وهميًا لا يغطيه جدول الصفحات الخاص بها، فسيعلم نظام التشغيل أن هذه العملية تطبّق شيئًا خاطئًا ويمكنه إبلاغ العملية أنها تعدّت حدودها. تمتلك كل صفحة سمات إضافية، لذا يمكن ضبط الصفحة للقراءة فقط أو للكتابة فقط أو غيرها من الخاصيات الأخرى. إذا حاولت العملية الوصول إلى الصفحة، فيمكن لنظام التشغيل التحقق مما إذا كان لديها أذونات كافية وإيقافها إن لم تكن كذلك مثل محاولة الكتابة في صفحة للقراءة فقط. تُعَد الأنظمة التي تستخدم الذاكرة الوهمية أكثر استقرارًا لأنه يمكن للعملية في نظام تشغيل مثالي أن تعطّل نفسها فقط دون تعطيل النظام بأكمله، حيث تُبرمَج أنظمة تشغيل مع تجاهل الأخطاء التي يمكن أن تتسبّب في تعطل الأنظمة بأكملها. التبديل Swap يمكننا الآن أن نرى كيفية تقديم تبديل ذاكرة، حيث يمكن تغيير مؤشر الصفحة ليؤشّر إلى موقع على القرص الصلب بدلًا من التأشير إلى منطقة من ذاكرة النظام. يحتاج نظام التشغيل عند الرجوع إلى هذه الصفحة إلى نقلها من القرص الصلب إلى ذاكرة النظام، إذ لا يمكن تنفيذ شيفرة البرنامج إلا من ذاكرة النظام. إذا كانت ذاكرة النظام ممتلئة، فيجب إخراج صفحة أخرى من ذاكرة النظام وتبديلها بالقرص الصلب قبل وضع الصفحة المطلوبة في الذاكرة. إذا كانت هناك عملية أخرى تريد الصفحة التي أُخرِجت للتو، فستتكرر هذه العملية مرةً أخرى. يمكن أن يؤدي ذلك إلى مشكلةٍ كبيرة في تبديل الذاكرة، حيث يُعَد التحميل من القرص الصلب بطيئًا جدًا بالموازنة مع العمليات التي تُنجَز في الذاكرة، وسيكون معظم الناس متآلفين مع فكرة الجلوس أمام الحاسوب أثناء توقف القرص الصلب مرارًا وتكرارًا مع بقاء النظام غير مستجيب. mmap تُعَد عملية ربط الذاكرة Memory Map أو mmap (من اسم استدعاء النظام) عمليةً مختلفة ولكنها ذات صلة، حيث إن لم يؤشّر جدول الصفحات إلى الذاكرة الحقيقية أو لم يؤشّر تبديل جدول الصفحات إلى ملف على القرص الصلب، فسنقول أن الملف مربوط بالذاكرة mmap. تحتاج عادةً إلى فتح open ملف على القرص الصلب للحصول على واصف الملف ثم قراءته read وكتابته write في صيغة تسلسلية. إذا كان الملف مربوطًا بالذاكرة، فيمكن الوصول إليه مثل الذاكرة RAM الخاصة بالنظام. مشاركة الذاكرة تحصل كل عملية على جدول صفحات خاص بها، لذلك يُربَط أيّ عنوان تستخدمه مع إطار فريد في الذاكرة الحقيقية، ولكن إن أشّر نظام التشغيل إلى مدخلتَين من جدول الصفحات إلى الإطار نفسه، فهذا يعني التشارك في هذا الإطار، وستكون أيّ تغييرات تجريها إحدى العمليتين مرئية للعملية الأخرى. يمكنك أن ترى الآن كيفية تقديم الخيوط Threads. يمكن للدالة clone()‎ الخاصة بنظام لينكس مشاركة قدر كبير أو صغير من العملية الجديدة مع العملية القديمة وفق ما هو مطلوب. إن استدعت عمليةٌ الدالة clone()‎ لإنشاء عملية جديدة، ولكنها تطلب أن تشترك العمليتان في جدول الصفحات نفسه، فسيكون لديك خيط حيث ترى كلتا العمليتين الذاكرة الحقيقية الأساسية نفسها. كما يمكنك معرفة كيفية إجراء النسخ عند الكتابة، حيث إذا ضبطتَ أذونات إحدى الصفحات لتكون للقراءة فقط، فسيجري إعلام نظام التشغيل عندما تحاول إحدى العملياتُ الكتابةَ في الصفحة. إذا عَلِم نظام التشغيل أن هذه الصفحة هي صفحة نسخ عند الكتابة، فيجب إنشاء نسخة جديدة من الصفحة في ذاكرة النظام ويجب أن توشّر الصفحة في جدول الصفحات إلى هذه الصفحة الجديدة. يمكن بعد ذلك تحديث سمات الصفحة للحصول على أذونات الكتابة ويكون للعملية نسختها الفريدة من الصفحة. ذاكرة القرص الصلب المخبئة Cache توجد في الأنظمة الحديثة ذاكرة متوفرة أكثر مما يستخدمه النظام حاليًا بدلًا من وجود ذاكرة قليلة جدًا والاضطرار إلى تبديل الذاكرة. يخبرنا تسلسل الذواكر الهرمي بأن الوصول إلى القرص الصلب أبطأ بكثير من الوصول إلى الذاكرة، لذلك يُفضَّل نقل أكبر قدر ممكن من البيانات من القرص الصلب إلى ذاكرة النظام إن أمكن ذلك. ينسخ نظام لينكس والعديد من الأنظمة الأخرى البيانات من الملفات الموجودة على القرص الصلب إلى الذاكرة عند استخدامها. يُحتمَل أن يرغب البرنامج في الوصول إلى بقية الملف مع استمراره في المعالجة حتى إن طلب في البداية جزءًا صغيرًا فقط من الملف، ويتحقق نظام التشغيل عند قراءة ملف أو الكتابة فيه أولًا مما إذا كان الملف موجودًا في الذاكرة المخبئة Cache. يجب أن تكون هذه الصفحات هي أولى الصفحات التي ستُزال عند زيادة ضغط الذاكرة في النظام. ذاكرة الصفحة المخبئة Page Cache المصطلح الذي يمكن أن تسمعه عند مناقشة النواة Kernel هو ذاكرة الصفحة المخبئية Page Cache التي تشير إلى قائمة الصفحات التي تحتفظ بها النواة والتي تشير إلى الملفات الموجودة على القرص الصلب، حيث تندرج صفحة التبديل والصفحات المربوطة بالذاكرة وصفحات ذاكرة القرص الصلب المخبئية ضمن هذه الفئة. تحتفظ النواة بهذه القائمة لأنها تحتاج إلى أن تكون قادرة على البحث عنها بسرعة استجابةً لطلبات القراءة والكتابة. مواصفات الذاكرة الوهمية في لينكس تبقى مفاهيم الذاكرة الوهمية الأساسية ثابتة، إلّا أن تفاصيل التقديمات تعتمد بصورة كبيرة على نظام التشغيل والعتاد. مخطط فضاء العناوين يقسم لينكس فضاء العناوين المتاح إلى مكون نواة Kernel مشترك وفضاء عناوين خاص بالمستخدم، وهذا يعني أن العناوين الموجودة في منفذ النواة لفضاء العناوين ترتبط مع الذاكرة الحقيقية نفسها لكل عملية، بينما يكون فضاء عناوين المستخدم خاصًا بالعملية، ويوجد في نظام لينكس فضاء النواة المشترك في أعلى فضاء العناوين المتاح. يحدث هذا الانقسام على المعالج x86 الأكثر شيوعًا المكون من 32 بت عند حجم 3 جيجابايتات، وبما أن 32 بت يمكنها ربط 4 جيجابايتات كحد أقصى، مما يؤدي إلى ترك المنطقة العليا بمقدار 1 جيجابايت لتكون منطقة النواة المشتركة. مع ذلك، تريد العديد من الأجهزة دعم أكثر من 4 جيجابايتات لكل عملية، حيث يسمح دعم الذاكرة العالي للمعالجات بالوصول إلى 4 جيجابايتات كاملة باستخدام توسّعات خاصة. مخطط فضاء العناوين في لينكس جدول الصفحات المكون من المستويات الثلاثة هناك العديد من الطرق المختلفة لنظام التشغيل لتنظيم جداول الصفحات، ولكن يختار نظام لينكس استخدام النظام الهرمي. تستخدم جداول الصفحات تسلسلًا هرميًا بعمق ثلاثة مستويات، لذلك يُشار إلى نظام لينكس باسم جدول الصفحات المكوَّن من ثلاثة مستويات. أثبت جدول الصفحات المكون من ثلاثة مستويات أنه اختيار قوي بالرغم من أنه لا يخلو من بعض المساوئ. تختلف تفاصيل تقديم الذاكرة الوهمية بين المعالجات، مما يعني أن جدول الصفحات العام الذي يختاره نظام لينكس يجب أن يكون قابلًا للنقل وعامًا نسبيًا. لا يُعَد مفهوم مستويات جدول الصفحات الثلاثة أمرًا صعبًا، لأننا نعلم أن العنوان الوهمي يتكون من رقم صفحة وإزاحة في صفحة الذاكرة الحقيقية، إذ يُقسَم العنوان الوهمي إلى مستويات مُرقَّمة في جدول الصفحات المكون من ثلاثة مستويات. يُعَد كل مستوًى جدولَ صفحات بحد ذاته، أي أنه يرتبط مع رقم الصفحة الحقيقية. ترتبط مدخلة المستوى 1 مباشرةً مع الإطار الحقيقي في جدول صفحات مؤلفٍ من مستوًى واحد، بينما يعطي كل مستوًى من المستويات العليا عنوان إطار الذاكرة الحقيقية الذي يحتفظ بجدول صفحات المستويات الدنيا التالي في الإصدار متعدد المستويات من جدول الصفحات. جدول صفحات لينكس المكون من ثلاثة مستويات يتضمن المثال السابق الانتقال إلى جدول الصفحات ذي المستوى الأعلى، والعثور على الإطار الحقيقي الذي يحتوي على عنوان المستوى التالي، وقراءة مستويات ذلك الجدول وإيجاد الإطار الحقيقي الذي يوجد فيه جدول صفحات المستويات التالية من جدول الصفحات وما إلى ذلك. يبدو أن هذا النموذج معقدًا في البداية، ولكن السبب الرئيسي لتنفيذ هذا النموذج هو متطلبات الحجم. تخيل مثلًا عملية ما لها صفحة واحدة مرتبطة بالقرب من نهاية فضاء العناوين الوهمية، حيث قلنا سابقًا أنه يمكن العثور على مدخلة جدول الصفحات بوصفها إزاحةً من مسجل جدول الصفحات الأساسي، لذلك يجب أن يكون جدول الصفحات مصفوفةً متجاورةً في الذاكرة، وبالتالي تتطلب الصفحة القريبة من نهاية فضاء العناوين المصفوفةَ بأكملها والتي يمكن أن تشغَل مساحةً كبيرة، أي العديد والعديد من صفحات الذاكرة الحقيقية. يكون المستوى الأول في نظام مؤلفٍ من ثلاثة مستويات هو إطار ذاكرة حقيقي واحد فقط، ويرتبط مع المستوى الثاني الذي هو إطار ذاكرة واحد، والذي بدوره يرتبط مع المستوى الثالث، وبالتالي يقلّل نظام المستويات الثلاثة من عدد الصفحات المطلوبة إلى جزء صغير فقط من الصفحات المطلوبة لنظام المستوى الواحد. هناك عيوب واضحة في هذا النظام، إذ يتطلب البحث عن عنوان واحد مزيدًا من المراجع، ويمكن أن يكون ذلك مكلفًا. يتفهم لينكس أن هذا النظام يمكن ألّا يكون مناسبًا للعديد من أنواع المعالجات المختلفة، لذلك يمكن أن تقلل بعض المعماريات من مستويات جدول الصفحات بسهولة مثل المعمارية x86 الأكثر شيوعًا التي تستخدم نظامًا مؤلفًا من مستويين فقط في التقديم الخاص بها. ترجمة -وبتصرُّف- لأقسام من فصل Virtual Memory من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال السابق: أهم المفاهيم التي تنظم العمليات وتعالجها في معمارية الحاسوب الحديثة المقال التالي: دعم عتاد الحاسوب للذاكرة الوهمية Virtual Memory الذاكرة الوهمية (Virtual memory) في نظام التشغيل إعداد الذّاكرة الوهميّة (ملفّات التبادل) على خادوم خاص وهميّ (VPS) أنظمة المعالجات في معمارية الحاسوب تعرف على وحدة المعالجة المركزية وعملياتها في معمارية الحاسوب
  12. بدأنا في مقال إنشاء تطبيق قائمة مهام باستخدام Angular ببناء تطبيق مهام ثم أضفنا المكونات له في المقال السابق إنشاء مكونات Components في تطبيق Angular وسننتقل الآن إلى إضافة وظائف للسماح للمستخدِمين بترشيح عناصر مهامهم حتى يتمكنوا من عرض العناصر النشطة أو المكتملة أو جميع العناصر، كما سنوضّح في هذا المقال كيفية بناء تطبيق جاهز للإنتاج، وسنوفر موارد إضافية لمواصلة رحلة تعلمك لإطار عمل Angular. المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية. الهدف: إضافة وظيفة الترشيح Filtering لتطبيقنا، ومعرفة كيفية بناء تطبيق Angular. شيفرة الترشيح يعتمد ترشيح العناصر على الخاصية filter التي أضفناها مسبقًا إلى الملف app.component.ts: filter: 'all' | 'active' | 'done' = 'all'; القيمة الافتراضية للخاصية filter هي all، ولكن يمكن أن تكون active أو done. إضافة عناصر تحكم الترشيح أضِف شيفرة HTML التالية في الملف app.component.html قبل زر "الإضافة Add" وفوق القسم الذي يعطي قائمةً بالعناصر، وتكون فيما يلي الأقسام الموجودة في ملف HTML ضمن تعليقات لتتمكن من معرفة مكان وضع الأزرار بالضبط. <!-- <button class="btn-primary" (click)="addItem(newItem.value)">Add</button> --> <!-- الأزرار التي تعرض جميع العناصر أو العناصر النشطة أو العناصر المكتملة عند النقر عليها --> <div class="btn-wrapper"> <button class="btn btn-menu" [class.active]="filter == 'all'" (click)="filter = 'all'"> All </button> <button class="btn btn-menu" [class.active]="filter == 'active'" (click)="filter = 'active'"> To Do </button> <button class="btn btn-menu" [class.active]="filter == 'done'" (click)="filter = 'done'"> Done </button> </div> <!-- <h2>{{items.length}} item(s)</h2> <ul>... --> يؤدي النقر على الأزرار إلى تغيير قيم خاصية الترشيح filter التي تحدِّد العناصر items المعروضة والتنسيق الذي يطبّقه إطار عمل Angular على الزر النشط. إذا نقر المستخدِم على زر "جميع العناصر All"، فستظهر كافة العناصر. إذا نقر المستخدِم على زر " العناصر النشطة To do"، فستظهر العناصر التي تكون فيها قيمة done هي false. إذا نقر المستخدِم على زر "العناصر المكتملة Done"، فستظهر العناصر التي تكون فيها قيمة done هي true. يتحكم ربط سِمة الصنف باستخدام الأقواس المربعة [] في لون نص الأزرار، إذ يطبِّق ربط الأصناف [class.active] الصنفَ active عندما تتطابق قيمة الخاصية filter مع التعبير، فإذا نقر المستخدِم مثلًا على زر "العناصر المكتملة Done" الذي يضبط الخاصية filter على القيمة done، فسيُقيَّم تعبير ربط الأصناف filter == 'done'‎ على أنه true، كما يطبق إطار عمل Angular الصنف active على زر "العناصر المكتملة Done" عندما تكون قيمة الخاصية filter هي done لجعل لون النص أخضر، فإذا نقر المستخدِم على أحد الأزرار الأخرى، فلن تكون قيمة الخاصية filter هي done، وبالتالي لن يكون لون النص أخضرًا. بناء تطبيق Angular النهائي يمكنك الآن بعد أن انتهيت من تطوير تطبيقك تشغيل أمر البناء build في واجهة سطر الأوامر CLI في إطار عمل Angular، فإذا شغّلتَ الأمر build في المجلد todo، فسيُصرَّف تطبيقك في مجلد خرج بالاسم dist/‎. شغّل الأمر التالي في سطر الأوامر في المجلد todo: ng build -c production تصرِّف واجهة CLI التطبيق وتضع الخرج في مجلد dist جديد، كما تتخلص الراية --configuration production/-c production مع الأمر ng build من الأشياء التي لا تحتاجها في عملية الإنتاج. نشر تطبيقك يمكنك نسخ محتويات المجلد dist/my-project-name إلى خادم الويب لنشر تطبيقك، وبما أنّ هذه الملفات ساكنة static files، فيمكنك استضافتها على أيّ خادم ويب يمتلك القدرة على تشغيل الملفات مثل: Node.js Java ‎.NET يمكنك استخدام أيّ منصة واجهة خلفية مثل Firebase أو Google Cloud أو App Engine. الخلاصة بما أنه كان لديك شيفرة المرشح filter في الملف app.component.ts مسبقًا، فكل ما عليك فعله هو تعديل القالب لتوفير عناصر التحكم بترشيح العناصر والتعلم به، كما تناولنا في هذا المقال كيفية بناء تطبيق Angular جاهز للإنتاج. أنشأت حتى الآن تطبيقًا أساسيًا، لكن رحلتك في تعلم إطار عمل Angular بدأت للتو، ويمكنك معرفة المزيد من خلال الاطلاع على سلسلة مقالات Angular في أكاديمية حسوب التي تعزّز معرفتك في رحلة تعلّمك لإطار عمل Angular. ترجمة -وبتصرُّف- للمقالين Filtering our to-do items وBuilding Angular applications and further resources. اقرأ أيضًا تهيئة بيئة تطبيقات Angular ونشرها على الويب إضافة التنقل وإدارة البيانات في تطبيق Angular كيفية استعمال Angular في بناء تطبيقات الويب مقدمة في بناء تطبيقات الويب باستخدام إطار العمل Angular وقاعدة بيانات Firestore
  13. توفّر المكونات طريقةً لتنظيم تطبيقك، لذا سنرشدك في هذا المقال لإنشاء مكوِّن للتعامل مع العناصر الفردية في قائمة المهام الذي بدأنا العمل عليه في المقال السابق، وإضافة وظائف تحديد المهام وتعديلها وحذفها، حيث سنغطي نموذج أحداث Angular. المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية. الهدف: معرفة المزيد حول المكونات بما في ذلك كيفية عمل الأحداث للتعامل مع التحديثات، وإضافة وظائف تحديد المهام وتعديلها وحذفها. إنشاء مكون جديد أنشئ مكونًا بالاسم item باستخدام أمر واجهة سطر الأوامر CLI التالي: ng generate component item ينشئ الأمر ng generate component مكونًا ومجلدًا بالاسم الذي تحدده، حيث سيكون في مثالنا اسم المجلد والمكوِّن هو item، كما يمكنك العثور على مجلد العناصر item ضمن المجلد app. يتكون المكوِّن ItemComponent من الملفات التالية مثل المكون AppComponent: الملف item.component.html الخاص بالتوصيف باستخدام لغة HTML. الملف item.component.ts الخاص بالشيفرة البرمجية أو بالمنطق باستخدام لغة TypeScript +الملف item.component.css الخاص بالتنسيق باستخدام لغة CSS. يمكنك رؤية مرجع لملفات HTML وCSS في البيانات الوصفية لعنصر التصميم ‎@Component()‎ في الملف item.component.ts كما يلي: @Component({ selector: 'app-item', templateUrl: './item.component.html', styleUrls: ['./item.component.css'], }) إضافة توصيف HTML الخاص بالمكون ItemComponent يمكن أن يتولى المكوِّن ItemComponent مهمة منح المستخدِم طريقة لتحديد العناصر بوصفها مكتملةً أو لتعديلها أو حذفها، لذا أضِف شيفرة HTML لإدارة العناصر من خلال استبدال محتوى العنصر البديل Placeholder في الملف item.component.html بما يلي: <div class="item"> <input [id]="item.description" type="checkbox" (change)="item.done = !item.done" [checked]="item.done" /> <label [for]="item.description">{{item.description}}</label> <div class="btn-wrapper" *ngIf="!editable"> <button class="btn" (click)="editable = !editable">Edit</button> <button class="btn btn-warn" (click)="remove.emit()">Delete</button> </div> ‏<-- ‫يظهر هذا القسم فقط في حالة نقر المستخدم على زر "التعديل Edit" –!> <div *ngIf="editable"> <input class="sm-text-input" placeholder="edit item" [value]="item.description" #editedItem (keyup.enter)="saveItem(editedItem.value)"> <div class="btn-wrapper"> <button class="btn" (click)="editable = !editable">Cancel</button> <button class="btn btn-save" (click)="saveItem(editedItem.value)">Save</button> </div> </div> </div> حقل الإدخال Input الأول هو مربع اختيار بحيث يمكن للمستخدِمين تحديد العناصر عند اكتمال أحدها، كما تشير الأقواس المزدوجة المعقوصة {{}} في حقل الإدخال <input> والتسمية <label> الخاصين بمربع الاختيار إلى استخدام إطار عمل Angular الذي يستخدِم الصيغة {{item.description}} لاسترداد وصف العنصر item الحالي من مصفوفة العناصر items، وسنوضّح في القسم التالي كيفية مشاركة المكونات للبيانات بالتفصيل. كما يوجد زران لتعديل العنصر الحالي وحذفه ضمن الوسم <div> الذي يوجد فيه الموجّه ‎*ngIf، وهو موجّه Angular مبني مسبقًا يمكنك استخدامه لتغيير بنية نموذج DOM ديناميكيًا، إذ يشير الموجّه ‎*ngIf إلى أنه إذا كانت قيمة المتغير editable هي false، فإنّ العنصر <div> موجود في نموذج DOM؛ وإذا كانت قيمة المتغير editable هي true، فسيزيل إطار عمل Angular العنصر <div> من نموذج DOM. <div class="btn-wrapper" *ngIf="!editable"> <button class="btn" (click)="editable = !editable">Edit</button> <button class="btn btn-warn" (click)="remove.emit()">Delete</button> </div> إذا نقر المستخدِم على زر "التعديل Edit"، فستصبح قيمة المتغير editable هي true، مما يؤدي إلى إزالة العنصر <div> وأبنائه من نموذج DOM، فإذا نقر المستخدِم على زر "الحذف Delete"، فسيرفع المكوِّن ItemComponent حدثًا يعلِم فيه المكوِّن AppComponent بعملية الحذف. لاحظ أيضًا وجود الموجه ‎*ngIf في عنصر <div> التالي، ولكن ضُبِط المتغير editable على القيمة true، فإذا كانت قيمة المتغير editable هي true في هذه الحالة، فسيضع إطار عمل Angular العنصر <div> وعناصره الأبناء <input> و <button> في نموذج DOM. ‏<-- ‫يظهر هذا القسم فقط في حالة نقر المستخدم على زر "التعديل Edit" –!> <div *ngIf="editable"> <input class="sm-text-input" placeholder="edit item" [value]="item.description" #editedItem (keyup.enter)="saveItem(editedItem.value)"> <div class="btn-wrapper"> <button class="btn" (click)="editable = !editable">Cancel</button> <button class="btn btn-save" (click)="saveItem(editedItem.value)">Save</button> </div> </div> تكون قيمة العنصر <input> مرتبطةً بوصف description العنصر الحالي باستخدام الصيغة ‎[value]="item.description"‎، وبالتالي يكون وصف description العنصر هو قيمة العنصر <input>، لذا إذا كانت قيمة الوصف description هي eat، فسيكون الوصف description موجودًا مسبفًا في العنصر <input>، وبذلك إذا عدّل المستخدِم العنصر، فستكون قيمة العنصر <input> هي eat مسبقًا. يعني متغير القالب ‎#editedItem في العنصر <input> أنّ إطار عمل Angular يخزّن كل ما يكتبه المستخدِم في العنصر <input> في متغير اسمه editedItem، إذ يستدعي الحدث keyup التابع saveItem()‎ ويمرّر قيمة المتغير editedItem إذا اختار المستخدِم الضغط على مفتاح Enter بدلًا من النقر على زر "الحفظ Save". إذا نقر المستخدِم على زر "الإلغاء Cancel"، فستتبدّل قيمة المتغير editable إلى false، مما يزيل حقل الإدخال والأزرار الخاصة بالتعديل من نموذج DOM، فإذا كانت قيمة المتغير editable هي false، فسيعيد إطار عمل Angular وضع العنصر <div> مع زرَّي "التعديل Edit" و"الحذف Delete" في نموذج DOM. يؤدي النقر على زر "الحفظ Save" إلى استدعاء التابع saveItem()‎ الذي يأخذ القيمة من المتغير ‎#editedItem الخاص بالعنصر <input> ويغير وصف description العنصر إلى السلسلة النصية editedItem.value. إعداد المكون AppComponent سنضيف في القسم التالي شيفرةً برمجيةً تعتمد على الاتصال بالمكونين AppComponent و ItemComponent، إذ يجب إعداد المكون AppComponent أولًا عبر إضافة السطر التالي لاستيراد Item في بداية الملف app.component.ts: import { Item } from "./item"; بعد ذلك اضبط المكون AppComponent بإضافة ما يلي في نفس ملف الصنف: remove(item) { this.allItems.splice(this.allItems.indexOf(item), 1); } يستخدِم التابع remove()‎ تابع جافاسكربت Array.splice()‎ لإزالة عنصر واحد عند فهرس indexOf العنصر ذي الصلة، ويعني ذلك أن التابع splice()‎ يزيل العنصر من المصفوفة. إضافة الشيفرة البرمجية أو المنطق إلى المكون ItemComponent يمكن استخدام واجهة المستخدِم الخاصة بالمكوِّن ItemComponent من خلال إضافة شيفرة برمجية إلى المكون مثل إضافة دوال وطرق لدخول البيانات وخروجها. عدّل تعليمات استيراد جافاسكربت في الملف item.component.ts كما يلي: import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Item } from "../item"; تسمح إضافة Input و Output و EventEmitter للمكوِّن ItemComponent بمشاركة البيانات مع المكوِّن AppComponent، كما يمكن أن يفهم المكوِّن ItemComponent ما هو العنصر item من خلال استيراد Item. استبدل الصنف ItemComponent الناتج في الملف item.component.ts بما يلي: export class ItemComponent { editable = false; @Input() item: Item; @Input() newItem: string; @Output() remove = new EventEmitter<Item>(); saveItem(description) { if (!description) return; this.editable = false; this.item.description = description; } } تساعد الخاصية editable في تبديل قسم من القالب حيث يمكن للمستخدِم تعديل عنصر، إذ تُعَدّ الخاصية editable هي الخاصية نفسها في توصيف HTML ضمن تعليمة ‎*ngIf التي هي ‎*ngIf="editable"‎، فإذا أردتَ استخدام خاصية في القالب، فيجب التصريح عنها في الصنف. يسهّل كل من ‎@Input()‎ و ‎@Output()‎ و EventEmitter الاتصال بين مكونيك، إذ يعمل ‎@Input()‎ بوصفه وسيلةً لدخول البيانات إلى المكوِّن، ويعمل ‎@Output()‎ بوصفه وسيلةً لخروج البيانات من المكوِّن، كما يجب أن يكون ‎@Output()‎ من النوع EventEmitter بحيث يمكن للمكوِّن رفع حدث عندما تكون هناك بيانات جاهزة لمشاركتها مع مكوِّن آخر. استخدم ‎@Input()‎ لتحديد أن قيمة الخاصية يمكن أن تأتي من خارج المكوِّن، واستخدم ‎@Output()‎ مع EventEmitter لتحديد أن قيمة الخاصية يمكن أن تترك المكوِّن بحيث يمكن لمكوِّن آخر تلقي تلك البيانات. يأخذ التابع saveItem()‎ وسيطًا هو الوصف description الذي يُعَدّ النص الذي يدخله المستخدِم في حقل الإدخال <input> عند تعديل عنصر في القائمة، كما يُعَدّ الوصف description السلسلة النصية نفسها التي تأتي من العنصر <input> مع متغير القالب ‎#editedItem. إذا لم يدخِل المستخدِم أيّ قيمة ولكنه نقر على زر "الحفظ Save"، فلن يعيد التابع saveItem()‎ شيئًا ولن يُحدِّث الوصف description، فإذا لم تكن تعليمة if موجودةً، فيمكن للمستخدِم النقر على زر "الحفظ Save" بدون وجود أيّ شيء في حقل الإدخال <input> وسيصبح الوصف description سلسلةً نصيةً فارغةً. إذا أدخل المستخدِم نصًا ونقر على زر "الحفظ Save"، فسيضبط التابع saveItem()‎ الخاصية editable على القيمة false، مما يتسبب في أن يزيل الموجّه ‎*ngIf في القالب ميزة التعديل ويصيّر أزرار "التعديل Edit" و"الحذف Delete" مرةً أخرى. يجب أن يُصرَّف التطبيق في هذه المرحلة، ولكن يجب استخدام المكوِّن ItemComponent ضمن المكوِّن AppComponent لتتمكّن من رؤية الميزات الجديدة في المتصفح. استخدام المكون ItemComponent ضمن المكون AppComponent يمنحك تضمين أحد المكونات ضمن مكوِّن آخر في سياق علاقة المكوِّن الأب بالمكوِّن الابن المرونةَ في استخدام المكونات حيثما تريد ذلك، إذ يُعَدّ المكوِّن AppComponent صدَفةً shell للتطبيق بحيث يمكنك تضمين مكونات أخرى. يمكنك استخدام المكوِّن ItemComponent ضمن المكوِّن AppComponent من خلال وضع محدّد المكوِّن ItemComponent في قالب المكون AppComponent، كما يحدّد إطار عمل Angular محدّد المكوِّن في البيانات الوصفية لعنصر التصميم ‎@Component()‎، إذ يكون المحدّد Selector في مثالنا هو app-item: @Component({ selector: 'app-item', templateUrl: './item.component.html', styleUrls: ['./item.component.css'] }) يمكنك استخدام محدّد المكون ItemComponent ضمن المكوِّن AppComponent من خلال إضافة العنصر <app-item> الذي يتوافق مع المحدّد الذي عرّفته لصنف المكوِّن في الملف app.component.html، لذا استبدل القائمة غير المرتبة الحالية في الملف app.component.html بما يلي: <h2>{{items.length}} <span *ngIf="items.length === 1; else elseBlock">item</span> <ng-template #elseBlock>items</ng-template></h2> <ul> <li *ngFor="let item of items"> <app-item (remove)="remove(item)" [item]="item"></app-item> </li> </ul> تدخِل صيغة الأقواس المزدوجة المتعرجة {{}} في العنصر <h2> طول المصفوفة items وتعرضه. يستخدِم العنصر <span> في عنصر العنوان <h2> الموجّه ‎*ngIf وelse لتحديد ما إذا كان يجب أن يشير العنصر <h2> إلى "العنصر item" أو مجموعة "العناصر items"، فإذا كان هناك عنصر واحد فقط في القائمة، فسيُعرَض العنصر <span> الذي يحتوي على الكلمة "عنصر item"، في حين إذا كان طول المصفوفة items هو أيّ شيء آخر غير القيمة 1، فسيظهر العنصر <ng-template> الذي أطلقنا عليه الاسم elseBlock بالصورة ‎#elseBlock بدلًا من العنصر <span>، كما يمكنك استخدام العنصر <ng-template> في إطار عمل Angular عندما لا تريد عرض المحتوى افتراضيًا، وبالتالي إذا كان طول مصفوفة العناصر items يساوي 1، فسيعرض الموجّه ‎*ngIf العنصر elseBlock وليس العنصر <span>. يستخدِم العنصر <li> موجّه التكرار ‎*ngFor للمرور على جميع العناصر في مصفوفة items، إذ يُعَدّ الموجَّه ‎*ngFor الخاص بإطار عمل Angular موجّهًا آخرًا يساعدك على تغيير بنية نموذج DOM مع كتابة شيفرة برمجية أقل، كما يكرّر إطار عمل Angular العنصر <li> وكل شيء بداخله لكل عنصر item حيث يتضمن العنصر <li> العنصر <app-item>، وهذا يعني أنّ إطار عمل Angular ينشئ نسخةً أخرى من العنصر <app-item> لكل عنصر في المصفوفة، وبالتالي سينشئ إطار عمل Angular العديد من عناصر <li> لأيّ عدد من العناصر في المصفوفة، كما يمكنك استخدام الموجّه ‎*ngFor مع عناصر أخرى مثل <div> أو <span> أو <p>. يحتوي المكوِّن AppComponent على التابع remove()‎ لإزالة العنصر المرتبط بالخاصية remove من المكوِّن ItemComponent، كما تربط الخاصية item الموجودة بين قوسين مربعين [] قيمة العنصر item بين المكونين AppComponent و ItemComponent. يجب أن تكون الآن قادرًا على تعديل العناصر وحذفها من القائمة، ويجب أن يتغير عدد العناصر عند إضافة عناصر أو حذفها، كما يمكنك جعل القائمة أسهل استخدامًا من خلال إضافة تنسيق إلى المكوِّن ItemComponent. إضافة التنسيق إلى المكون ItemComponent يمكنك استخدام ملف تنسيق لمكوِّن لإضافة تنسيقات خاصة به، إذ يضيف تنسيق CSS التالي التنسيقات الأساسية ونموذج الصندوق المرن Flexbox للأزرار ومربعات الاختيار المخصَّصة. الصق التنسيق التالي في الملف item.component.css: .item { padding: .5rem 0 .75rem 0; text-align: left; font-size: 1.2rem; } .btn-wrapper { margin-top: 1rem; margin-bottom: .5rem; } .btn { /* menu buttons flexbox styles */ flex-basis: 49%; } .btn-save { background-color: #000; color: #fff; border-color: #000; } .btn-save:hover { background-color: #444242; } .btn-save:focus { background-color: #fff; color: #000; } .checkbox-wrapper { margin: .5rem 0; } .btn-warn { background-color: #b90000; color: #fff; border-color: #9a0000; } .btn-warn:hover { background-color: #9a0000; } .btn-warn:active { background-color: #e30000; border-color: #000; } .sm-text-input { width: 100%; padding: .5rem; border: 2px solid #555; display: block; box-sizing: border-box; font-size: 1rem; margin: 1rem 0; } /* Custom checkboxes Adapted from https://css-tricks.com/the-checkbox-hack/#custom-designed-radio-buttons-and-checkboxes */ /* Base for label styling */ [type="checkbox"]:not(:checked), [type="checkbox"]:checked { position: absolute; left: -9999px; } [type="checkbox"]:not(:checked) + label, [type="checkbox"]:checked + label { position: relative; padding-left: 1.95em; cursor: pointer; } /* checkbox aspect */ [type="checkbox"]:not(:checked) + label:before, [type="checkbox"]:checked + label:before { content: ''; position: absolute; left: 0; top: 0; width: 1.25em; height: 1.25em; border: 2px solid #ccc; background: #fff; } /* checked mark aspect */ [type="checkbox"]:not(:checked) + label:after, [type="checkbox"]:checked + label:after { content: '\2713\0020'; position: absolute; top: .15em; left: .22em; font-size: 1.3em; line-height: 0.8; color: #0d8dee; transition: all .2s; font-family: 'Lucida Sans Unicode', 'Arial Unicode MS', Arial; } /* checked mark aspect changes */ [type="checkbox"]:not(:checked) + label:after { opacity: 0; transform: scale(0); } [type="checkbox"]:checked + label:after { opacity: 1; transform: scale(1); } /* accessibility */ [type="checkbox"]:checked:focus + label:before, [type="checkbox"]:not(:checked):focus + label:before { border: 2px dotted blue; } الخلاصة يجب أن يكون لديك الآن تطبيق قائمة المهام باستخدام إطار عمل Angular الذي يمكنه إضافة العناصر وتعديلها وإزالتها، والخطوة التالية هي إضافة الترشيح Filtering بحيث يمكنك عرض العناصر التي تحقق معايير محددة. ترجمة -وبتصرُّف- للمقال Creating an item component. اقرأ أيضًا مقدمة في مفاهيم Angular تهيئة بيئة تطبيقات Angular ونشرها على الويب كيفية استعمال Angular في بناء تطبيقات الويب المرشحات (filters) في AngularJS الخدمات (Services) في AngularJS
  14. تعلّمنا في المقال السابق دعم إطار عمل Svelte للغة TypeScript وكيفية استخدام ذلك لجعل تطبيقك أقوى، كما سنتعرّف في هذا المقال على كيفية نشر تطبيقك عبر الإنترنت ومشاركة بعض موارد التعلم التي يجب الانتقال إليها لمواصلة رحلة التعلّم في إطار عمل Svelte. المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية، وستحتاج طرفية مثبَّت عليها node وnpm لتصريف وبناء تطبيقك. الهدف: التعرّف على كيفية تحضير تطبيق Svelte لعملية الإنتاج والموارد التعليمية التي يجب الاطلاع عليها بعد ذلك. يمكن متابعة كتابة شيفرتك معنا، لذلك انسخ أولًا مستودع github -إذا لم تفعل ذلك مسبقًا- باستخدام الأمر التالي: git clone https://github.com/opensas/mdn-svelte-tutorial.git ثم يمكنك الوصول إلى حالة التطبيق الحالية من خلال تشغيل الأمر التالي: cd mdn-svelte-tutorial/08-next-steps أو يمكنك تنزيل محتوى المجلد مباشرةً كما يلي: npx degit opensas/mdn-svelte-tutorial/08-next-steps تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير. تصريف التطبيق شغّلنا حتى الآن تطبيقنا في وضع التطوير باستخدام الأمر npm run dev الذي يخبر إطار عمل Svelte بتجميع المكونات وملفات جافاسكربت في الملف public/build/bundle.js وجميع أقسام CSS الخاصة بالمكونات في الملف public/build/bundle.css، كما يشغّل خادم التطوير ويراقب التغييرات ويعيد تصريف التطبيق وتحديث الصفحة عند حدوث تغيير. سيكون الملفان bundle.js و bundle.css كما يلي (لاحظ وجود حجم الملف على اليسار): 504 Jul 13 02:43 bundle.css 95981 Jul 13 02:43 bundle.js يمكن تصريف التطبيق للإنتاج من خلال تشغيل الأمر npm run build، إذ لن يشغّل إطار عمل Svelte خادم ويب أو يستمر في مراقبة التغييرات في هذه الحالة، ولكن سيصغّر ويضغط ملفات جافاسكربت باستخدام الأداة terser. سيكون الملفان bundle.js و bundle.css كما يلي بعد تشغيل الأمر npm run build: 504 Jul 13 02:43 bundle.css 21782 Jul 13 02:43 bundle.js جرّب تشغيل الأمر npm run build في المجلد الجذر لتطبيقك الآن، كما يمكن أن تتلقى تحذيرًا، لكن يمكنك تجاهله حاليًا. يبلغ حجم تطبيقنا بالكامل الآن 21 كيلوبايت و8.3 كيلوبايت عند ضغطه بتنسيق gzip، كما لا توجد أوقات تشغيل أو اعتماديات إضافية لتنزيلها وتحليلها وتنفيذها والاستمرار في عملها في الذاكرة، إذ حلّل إطار عمل Svelte المكونات وصرّف الشيفرة البرمجية إلى لغة جافاسكربت الصرفة Vanilla JavaScript. عملية التصريف في إطار Svelte سيستخدِم إطار عمل Svelte الأداة rollup بوصفها أداة تحزيم للوحدات افتراضيًا عندما تنشئ تطبيقًا جديدًا باستخدام الأمر الآتي: npx degit sveltejs/template my-svelte-project ملاحظة: يوجد قالب رسمي لاستخدام Webpack والعديد من الإضافات التي يديرها المجتمع لأدوات التحزيم الأخرى. يمكنك أن ترى في الملف package.json أنّ السكربتات dev وstart تستدعي أداة التحزيم Rollup: "scripts": { "build": "rollup -c", "dev": "rollup -c -w", "start": "sirv public" }, نمرّر في السكربت dev الوسيط ‎-w الذي يخبر أداة Rollup بمراقبة الملفات وإعادة البناء عند إجراء التغييرات، فإذا ألقينا نظرةً على الملف rollup.config.js، فيمكننا رؤية أنّ مصرِّف Svelte هو مجرد إضافة من Rollup: import svelte from 'rollup-plugin-svelte'; // … import { terser } from 'rollup-plugin-terser'; const production = !process.env.ROLLUP_WATCH; export default { input: 'src/main.js', output: { sourcemap: true, format: 'iife', name: 'app', file: 'public/build/bundle.js' }, plugins: [ svelte({ // تفعيل عمليات فحص وقت التشغيل عندما لا تكون في وضع الإنتاج dev: !production, // ‫سنستخرج أيّ مكون CSS في ملف منفصل، وهذا أفضل للأداء css: css => { css.write('public/build/bundle.css'); } }), سترى لاحقًا في الملف نفسه كيف تقلل أداة Rollup من حجم السكربتات في وضع الإنتاج وتطلق خادمًا محليًا في وضع التطوير: // ‫استدعِ الأمر "npm run start" في وضع التطوير بمجرد إنشاء الحزمة !production && serve(), // راقب المجلد‫ public وحدّث المتصفح عند حدوث تغييرات عندما لا تكون في وضع الإنتاج !production && livereload('public'), // ‫إذا أردت البناء للإنتاج (أي استخدمت npm run build بدلًا من npm run dev)، فطبّق عملية التصغير production && terser() ], هناك العديد من إضافات Rollup التي تتيح لك تخصيص سلوكها، ومنها الإضافة svelte-preprocess التي يهتم بها فريق Svelte، حيث تعالج هذه الإضافة مسبقًا العديد من اللغات المختلفة في ملفات Svelte مثل PostCSS و SCSS و Less و CoffeeScript و SASS و TypeScript. نشر تطبيق Svelte لا يُعَدّ تطبيق Svelte من وجهة نظر خادم الويب أكثر من مجموعة من ملفات HTML و CSS و JavaScript، فكل ما تحتاجه هو خادم ويب قادر على تقديم ملفات ثابتة أو ساكنة، مما يعني أنه لديك الكثير من الخيارات للاختيار من بينها، إذًا لنلقِ نظرةً على بعض منها. ملاحظة: يمكن تطبيق القسم التالي على أيّ موقع ويب ساكن من جانب العميل يتطلب خطوة بناء، وليس فقط على تطبيقات Svelte. النشر باستخدام Vercel يُعَدّ استخدام Vercel من أسهل الطرق لنشر تطبيق Svelte، ويُعَدّ Vercel منصةً سحابيةً مصمَّمةً خصيصًا للمواقع الساكنة، وتتمتع بدعم معظم أدوات الواجهة الأمامية الشائعة مثل إطار عمل Svelte. اتبع الخطوات التالية لنشر تطبيقك: سجّل للحصول على حساب Vercel. انتقل إلى جذر تطبيقك وشغّل الأمر npx vercel، إذسيُطلَب منك في المرة الأولى إدخال عنوان بريدك الإلكتروني واتباع الخطوات الواردة في البريد الإلكتروني المرسل إلى هذا العنوان لأغراض أمنية. شغّل الأمر npx vercel مرةً أخرى، وسيُطلَب منك الإجابة على بعض الأسئلة كما يلي: > npx vercel Vercel CLI 19.1.2 ? Set up and deploy "./mdn-svelte-tutorial"? [Y/n] y ? Which scope do you want to deploy to? opensas ? Link to existing project? [y/N] n ? What's your project's name? mdn-svelte-tutorial ? In which directory is your code located? ./ Auto-detected Project Settings (Svelte): - Build Command: `npm run build` or `rollup -c` - Output Directory: public - Development Command: sirv public --single --dev --port $PORT ? Want to override the settings? [y/N] n Linked to opensas/mdn-svelte-tutorial (created .vercel) Inspect: https://vercel.com/opensas/mdn-svelte-tutorial/[...] [1s] ✅ Production: https://mdn-svelte-tutorial.vercel.app [copied to clipboard] [19s] Deployed to production. Run `vercel --prod` to overwrite later (https://vercel.link/2F). To change the domain or build command, go to https://zeit.co/opensas/mdn-svelte-tutorial/settings اقبل جميع الإعدادات الافتراضية وسيكون كل شيء على ما يرام. انتقل إلى عنوان "Production" في متصفحك بمجرد الانتهاء من النشر، وسترى تطبيقك منشورًا. كما يمكنك استيراد مشروع Svelte git إلى Vercel من GitHub أو GitLab أو BitBucket. ملاحظة: يمكنك تثبيت Vercel بطريقة عامة باستخدام الأمر npm i -g vercel حتى لا تضطر إلى استخدام npx لتشغيله. النشر التلقائي على صفحات GitLab Pages يمكن استضافة الملفات الساكنة من خلال استخدام خدمات عبر الإنترنت تسمح لك بنشر موقعك تلقائيًا عندما ترفع التغييرات إلى مستودع git، وتتضمن معظم هذه الخدمات إنشاء خط أنابيب نشر يُشغَّل في كل عملية git push، كما تهتم ببناء موقع الويب ونشره. سننشر فيما يلي تطبيق قائمة المهام على GitLab Pages: يجب أولًا التسجيل في GitLab ثم إنشاء مشروع جديد، وسَمّ مشروعك الجديد باسم قصير وسهل مثل mdn-svelte-todo، إذ سيكون لديك عنوان url يشير إلى مستودع GitLab git الجديد مثل git@gitlab.com:[your-user]/[your-project].git ثانيًا، يُفضَّل إضافة ملف ‎.gitignore لإخبار جيت git بالملفات التي يجب استبعادها من عملية التحكم بالمصدر قبل البدءأ برفع المحتوى إلى مستودع git، إذ سنخبر git في حالتنا باستبعاد الملفات الموجودة في المجلد node_modules من خلال إنشاء ملف ‎.gitignore في المجلد الجذر لمشروعك المحلي الذي يحتوي على ما يلي: node_modules/ ثالثًا، لنعد الآن إلى GitLab، إذ سيرحب GitLab بك برسالة تشرح الخيارات المختلفة لرفع ملفاتك الموجودة مسبقًا بعد إنشاء مستودع جديد، لذا اتبع الخطوات المدرجة ضمن عنوان رفع مجلد موجود مسبقًا Push an existing folder: cd your_root_directory # انتقل إلى المجلد الجذر لمشروعك git init git remote add origin https://gitlab.com/[your-user]/mdn-svelte-todo.git git add . git commit -m "Initial commit" git push -u origin main ملاحظة: يمكنك استخدام بروتوكول git بدلًا من بروتوكول https الذي يُعَدّ أسرع ولا يجعلك تكتب اسم المستخدِم وكلمة المرور في كل مرة تدخل فيها إلى مستودعك الأصلي، لكن يجب إنشاء زوج مفاتيح SSH لاستخدامه، وسيكون عنوان URL الأصلي كما يلي: git@gitlab.com:[your-user]/mdn-svelte-todo.git هيّأنا مستودع git محلي عبر التعليمات السابقة، ثم ضبطنا الأصل البعيد حيث سنرفع شيفرتنا ليكون مستودعنا على GitLab، ثم يجب وضع جميع الملفات في مستودع git المحلي وبعد ذلك نرفعها إلى الأصل البعيد على GitLab. يستخدِم GitLab أداةً مبنيةً مسبقًا تسمى GitLab CI/CD لبناء موقعك ونشره على خادم GitLab Pages، إذ يُنشَأ تسلسل السكربتات الذي تشغّله الأداة GitLab CI/CD لإنجاز هذه المهمة من ملف يُدعى ‎.gitlab-ci.yml الذي يمكنك إنشاؤه وتعديله حسب الرغبة، كما ستجعل وظيفة معينة تسمى pages في ملف الإعداد GitLab على دراية بأنك تنشر موقع GitLab Pages. لنجرب ذلك: أولًا، أنشئ الملف ‎.gitlab-ci.yml ضمن جذر مشروعك وضَع فيه المحتوى التالي: image: node:latest pages: stage: deploy script: - npm install - npm run build artifacts: paths: - public only: - main طلبنا من GitLab استخدام صورة مع أحدث إصدار من Node لبناء تطبيقنا، ثم صرّحنا عن الوظيفة pages، لتفعيل GitLab Pages، وكلما كان هناك عملية رفع إلى المستودع، فسيشغّل GitLab الأمرين npm install و npm run build لبناء تطبيقنا، كما نطلب من GitLab نشر محتويات المجلد public، ونُعِد GitLab في السطر الأخير لإعادة نشر تطبيقنا فقط عندما يكون هناك عملية رفع إلى الفرع الرئيسي. ثانيًا، يجب إنشاء مراجع لملفات جافاسكربت وCSS في الملف ذي المسار النسبي public/index.html، بما أنّ تطبيقنا سيُنشَر في مجلد فرعي مثل الآتي: https://your-user.gitlab.io/mdn-svelte-todo ويمكن ذلك من خلال إزالة الشرطات المائلة / من العناوين: ‎/global.css ‎/build/bundle.css ‎/build/bundle.js كما يلي: <title>Svelte To-Do list</title> <link rel='icon' type='image/png' href='favicon.png'> <link rel='stylesheet' href='global.css'> <link rel='stylesheet' href='build/bundle.css'> <script defer src='build/bundle.js'></script> ثالثًا، يجب الآن فقط رفع التغييرات إلى GitLab من خلال تشغيل الأوامر التالية: > git add public/index.html > git add .gitlab-ci.yml > git commit -m "Added .gitlab-ci.yml file and fixed index.html absolute paths" > git push Counting objects: 5, done. Delta compression using up to 8 threads. Compressing objects: 100% (5/5), done. Writing objects: 100% (5/5), 541 bytes | 541.00 KiB/s, done. Total 5 (delta 3), reused 0 (delta 0) To gitlab.com:opensas/mdn-svelte-todo.git 7dac9f3..5725f46 main -> main سيعرض GitLab رمزًا يوضح تقدّم هذه الوظيفة كلما كانت هناك وظيفة تعمل، كما سيسمح لك النقر عليه بفحص الخرج. كما يمكنك التحقق من تقدم الوظائف الحالية والسابقة من قائمة "CI / CD" ثم الخيار "Jobs" الخاص بمشروع GitLab. سيكون تطبيقك متاحًا على https://your-user.gitlab.io/mdn-svelte-todo/‎ بمجرد انتهاء GitLab من بناء ونشر تطبيقك، ويكون في حالتنا https://opensas.gitlab.io/mdn-svelte-todo/‎، كما يمكنك التحقق من عنوان URL لصفحتك في واجهة مستخدِم GitLab من قائمة Settings ثم الخيار "Pages". سيُعاد بناء التطبيق تلقائيًا ونشره في GitLab Pages كلما رفعت تغييرات إلى مستودع GitLab باستخدام هذا الإعداد. موارد إضافية لتعلم إطار عمل Svelte سنمنحك الآن بعض الموارد والمشاريع للتحقق منها ومواصلة مشوار تعلّمك إطار عمل Svelte. توثيق Svelte يجب عليك بالتأكيد زيارة الصفحة الرئيسية لإطار Svelte حيث ستجد العديد من المقالات التي تشرحه، فإذا لم تفعل ذلك سابقًا، فاطّلع على برنامج Svelte التعليمي التفاعلي، وقد غطينا فعليًا معظم محتوياته في هذه السلسلة من المقالات، لذا لن يستغرق الأمر وقتًا طويلًا لإكماله، ويمكنك الرجوع إلى توثيق Svelte API والأمثلة المتاحة. مشاريع ذات صلة هناك مشاريع أخرى متعلقة بإطار عمل Svelte تستحق المراجعة ومنها: Sapper: إطار تطبيق مدعوم من Svelte يوفِّر التصيير من جانب الخادم SSR وتقسيم الشيفرة البرمجية والتوجيه المستند إلى الملفات والدعم دون اتصال والمزيد، كما يُعَدّ بمثابة Next.js لإطار عمل Svelte، فإذا أردت تطوير تطبيق ويب معقد إلى حد ما، فيجب عليك بالتأكيد إلقاء نظرة على هذا المشروع. Svelte Native: إطار عمل تطبيقات الهاتف المحمول مدعوم من Svelte، ويُعَدّ بمثابة React Native لإطار عمل Svelte. Svelte for VS Code: إضافة VS Code المدعوم رسميًا للعمل مع ملفات ‎.svelte. الخلاصة تهانينا، فقد أكملت سلسلة مقالات تعلم إطار عمل Svelte، حيث انتقلنا من مرحلة عدم المعرفة بإطار Svelte إلى إنشاء تطبيق كامل ونشره. تعلمنا مفهوم Svelte وما يميزه عن أطر عمل الواجهة الأمامية الأخرى. رأينا كيفية إضافة سلوك ديناميكي إلى موقع الويب، وكيفية تنظيم تطبيقنا في مكونات وطرق مختلفة لمشاركة المعلومات فيما بينها. استفدنا من نظام Svelte التفاعلي وتعلمنا كيفية تجنب الأخطاء الشائعة. رأينا بعض المفاهيم والتقنيات المتقدمة للتفاعل مع عناصر DOM ولتوسيع إمكانيات عناصر HTML برمجيًا باستخدام الموجّه use. رأينا بعد ذلك كيفية استخدام المخازن للعمل مع مستودع بيانات مركزي، وأنشأنا مخزننا المُخصَّص لاستمرار بيانات تطبيقنا في تخزين الويب. كما ألقينا نظرةً على دعم Svelte للغة TypeScript. تعلمنا في هذا المقال خيارين من الخيارات لنشر تطبيقنا في عملية الإنتاج ورأينا كيفية إعداد خط أنابيب أساسي لنشر تطبيقنا على GitLab عند كل تغيير ثم قدمنا قائمةً بموارد Svelte للمضي قدمًا في تعلم Svelte. يجب أن تكون لديك قاعدة قوية يمكنك من خلالها البدء في تطوير تطبيقات ويب احترافية باستخدام إطار عمل Svelte بعد الانتهاء من هذه السلسلة من المقالات. ترجمة -وبتصرُّف- للمقال Deployment and next steps. اقرأ أيضًا تقسيم تطبيق Svelte إلى مكونات إنشاء تطبيق قائمة مهام باستعمال إطار عمل Svelte بدء استخدام إطار العمل Svelte لبناء تطبيقات ويب التفاعلية ودورة الحياة وسهولة وصول المستخدمين في إطار عمل Svelte
  15. أصبحنا الآن جاهزين للبدء بإنشاء تطبيق قائمة المهام باستخدام إطار عمل Angular، إذ سيعرض التطبيق النهائي قائمةً بعناصر المهام وسيتضمن ميزات التعديل والحذف والإضافة، كما سنتعرّف في هذا المقال على بنية تطبيق Angular وسنعمل على عرض قائمة أساسية من عناصر المهام ومعرفة كيفية تنسيق التطبيق. المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية. الهدف: إنشاء بنية تطبيقك الأساسية، وعرض قائمة بعناصر المهام، وفهم مفاهيم Angular الأساسية مثل بنية المكونات ومشاركة البيانات بين المكونات وإنشاء حلقات المحتوى ومعرفة كيفية تنسيق تطبيق Angular. بنية تطبيق قائمة المهام يحتوي تطبيق Angular على الملف index.html مثل أيّ تطبيق أساسي لا يستخدِم إطار عمل، ويستخدِم عنصرًا خاصًا هو <app-root> ضمن الوسم <body>. في الملف index.html لإدراج مكوِّنك الرئيسي الذي يتضمن بدوره المكونات الأخرى التي تنشئها، كما لا تحتاج إلى تعديل الملف index.html بصورة عامة، وإنما يجب أن تركّز اهتمامك على مناطق متخصصة من تطبيقك هي المكونات Components. تنظيم تطبيقك ضمن المكونات تُعَدّ المكونات لبنة البناء المركزية لتطبيقات Angular، إذ يحتوي تطبيق قائمة المهام على مكونين هما مكوِّن يمثّل أساس تطبيقك ومكوِّن للتعامل مع عناصر المهام. يتكون كل مكوِّن من صنف TypeScript وشيفرة HTML وCSS، إذ تُترجَم أو تُحوَّل شيفرة Typescript إلى شيفرة جافاسكربت، مما يعني أنّ تطبيقك يتحوّل في النهاية إلى شيفرة جافاسكربت عادية مع القدرة على استخدام ميزات لغة Typescript الموسَّعة وصيغتها المبسَّطة. تعديل واجهة المستخدم ديناميكيا باستخدام الموجهين ‎ngIf* و‎*ngFor سنغطي في هذا المقال أيضًا موجّهَين Directives مهمين في Angular لتعديل بنية نموذج DOM ديناميكيًا، إذ يشبه الموجّه الأمر الذي يمكنك استخدامه في لغة HTML لإحداث تغيير في تطبيقك. الموجّه الأول هو المكرِّر ‎*ngFor الذي يمكنه إنشاء عناصر DOM ديناميكيًا بناءً على عناصر في مصفوفة؛ أما الموجّه الثاني فهو ‎*ngIf الذي يمكنك استخدامه لإضافة عناصر أو إزالتها من نموذج DOM بناءً على شرط معيّن، فإذا أراد المستخدِمون مثلًا تعديل عنصر في قائمة المهام، فيمكنك تزويدهم بالوسائل اللازمة لتعديل هذا العنصر؛ وإذا لم يرغبوا في تعديل عنصر، فيمكنك إزالة واجهة التعديل. مشاركة البيانات بين المكونات يجب إعداد مكوناتك لمشاركة البيانات في هذا التطبيق، إذ يجب على المكوِّن الرئيسي إرسال العنصر الجديد إلى المكوِّن الثاني لإضافة عناصر جديدة إلى قائمة المهام، ويدير هذا المكوِّن الثاني العناصر ويهتم بتعديل العناصر الفردية ووضع علامة عليها بوصفها مكتملةً وحذفها. يمكن تحقيق مشاركة البيانات بين مكونات Angular باستخدام عناصر تصميم خاصة هي ‎@Input()‎ و ‎@Output()‎ التي يمكنك استخدامها لتحديد أنّ بعض الخاصيات تسمح للبيانات بالدخول إلى مكون أو الخروج منه، كما يجب رفع حدث في أحد المكونات لاستخدام عنصر التصميم ‎@Output()‎ حتى يعرف المكوِّن الآخر أنّ هناك بيانات متاحة. تعريف عنصر أنشئ ملفًا جديدًا بالاسم item.ts بالمحتويات التالية في المجلد app: export interface Item { description: string; done: boolean; } لن تستخدِم هذا الملف حتى وقت لاحق، ولكن هذا هو الوقت المناسب لمعرفة ما هو العنصر item. تنشئ واجهة العنصر Item نموذج كائن عنصر بحيث يفهم تطبيقك ما هو العنصر، في حين يُعَدّ العنصر في تطبيق قائمة المهام كائنًا له وصف ويمكن إكماله. إضافة شيفرة إلى القالب AppComponent يمكنك الآن -بعد أن عرفت ما هو العنصر item- وضع بعض العناصر في تطبيقك عبر إضافتها إلى ملف TypeScript وهو app.component.ts واستبدال محتوياته بما يلي: import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'todo'; filter: 'all' | 'active' | 'done' = 'all'; allItems = [ { description: 'eat', done: true }, { description: 'sleep', done: false }, { description: 'play', done: false }, { description: 'laugh', done: false }, ]; get items() { if (this.filter === 'all') { return this.allItems; } return this.allItems.filter((item) => this.filter === 'done' ? item.done : !item.done); } } السطر الأول هو تعليمة استيراد بلغة جافاسكربت تستورد إطار عمل Angular، إذ يحدد عنصر التصميم ‎@Component()‎ بيانات AppComponent الوصفية، وتكون خاصيات البيانات الوصفية الافتراضية كما يلي: selector: يخبرك باسم محدّد CSS الذي تستخدِمه في قالب لإنشاء نسخة من هذا المكوِّن وهو هنا 'app-root'، وقد أضافت واجهة Angular CLI الوسم <app-root></app-root> في الملف index.html ضمن الوسم body عند إنشاء تطبيقك، كما يمكنك استخدام جميع محددات المكونات بالطريقة نفسها من خلال إضافتها إلى قوالب HTML الخاصة بالمكونات الأخرى. templateUrl: يحدّد ملف HTML لربطه بهذا المكوِّن، وهو هنا '‎./app.component.html'. styleUrls: يوفّر موقع واسم ملف التنسيق الذي ينطبق تحديدًا على هذا المكوِّن، وهو هنا '‎./app.component.css'. تكون خاصية الترشيح filter من النوع union، وبالتالي يمكن أن يكون للخاصية filter القيمة all أو active أو done، فإذا ارتكبت خطأً مطبعيًا أثناء كتابة القيمة التي تسندها إلى الخاصية filter مع النوع union، فستتيح لغة TypeScript معرفة ذلك لتتمكن من اكتشاف الخطأ مبكرًا، كما يوضح لك هذا المقال كيفية إضافة الترشيح في خطوة لاحقة، ولكن يمكنك استخدام خاصية الترشيح لإظهار القائمة الافتراضية لجميع العناصر. تحتوي المصفوفة allItems على عناصر المهام وما إذا كانت مكتملةً done أم لا، وتكون قيمة الخاصية done للعنصر الأولeat هي true. يعمل التابع الجالب get items()‎ على استرداد العناصر من المصفوفة allItems إذا كانت الخاصية filter مساويةً للقيمة all، وإلّا فسيعيد الجالب get items()‎ العناصر المكتملة done أو العناصر المعلَّقة اعتمادًا على كيفية ترشيح المستخدِم للعرض، إذ ينشئ الجالب Getter مصفوفةً بالاسم items التي سنستخدِمها لاحقًا. إضافة شيفرة HTML إلى القالب AppComponent استبدل محتويات الملف app.component.html بتوصيف HTML التالي للاطلاع على قائمة العناصر في المتصفح: <div class="main"> <h1>My To Do List</h1> <h2>What would you like to do today?</h2> <ul> <li *ngFor="let item of items">{{item.description}}</li> </ul> </div> يحتوي العنصر <li> على الموجّه ‎*ngFor، وهو موجّه Angular مضمَّن يُكرَّر على العناصر في المصفوفة items، إذ ينشئ الموجّه ‎*ngFor عنصر <li> جديد لكل عنصر، كما توجّه الأقواس المزدوجة المعقوصة التي تحتوي على item.description إطار عمل Angular لملء كل عنصر <li> بنص يصِف كل عنصر. سترى قائمة العناصر في المتصفح كما يلي: My To Do List What would you like to do today? * eat * sleep * play * laugh إضافة عناصر إلى القائمة تحتاج قائمة المهام لطريقة ما لإضافة عناصر، لذا أضِف التابع التالي إلى الصنف في الملف app.component.ts: addItem(description: string) { this.allItems.unshift({ description, done: false }); } يأخذ التابع addItem()‎ عنصرًا يوفّره المستخدِم ويضيفه إلى المصفوفة عندما ينقر المستخدِم على زر "الإضافة Add"، ويستخدِم هذا التابع تابع المصفوفة unshift()‎ لإضافة عنصر جديد إلى بداية المصفوفة وأعلى القائمة، أو يمكنك بدلًا من ذلك استخدام التابع push()‎ الذي سيضيف العنصر الجديد إلى نهاية المصفوفة وأسفل القائمة، كما يمكنك استخدام التابع addItem()‎ من خلال تعديل قسم HTML في القالب AppComponent. استبدل العنصر <h2> بما يلي في الملف app.component.html: <label for="addItemInput">What would you like to do today?</label> <input #newItem placeholder="add an item" (keyup.enter)="addItem(newItem.value); newItem.value = ''" class="lg-text-input" id="addItemInput" /> <button class="btn-primary" (click)="addItem(newItem.value)">Add</button> إذا كتب المستخدِم عنصرًا جديدًا في عنصر الإدخال <input> وضغط على مفتاح Enter، فسيضيف التابع addItem()‎ هذه القيمة إلى المصفوفة items، كما يؤدي الضغط على مفتاح Enter إلى إعادة ضبط قيمة حقل الإدخال <input> إلى سلسلة نصية فارغة، أو يمكن للمستخدِم النقر على زر "الإضافة Add" الذي يستدعي التابع addItem()‎ نفسه. تنسيق تطبيق Angular أصبحت بنية التطبيق الأساسية جاهزةً، إذًا لنطّلع على كيفية تعامل إطار عمل Angular مع تنسيق التطبيقات. إضافة التنسيق إلى تطبيق Angular تنشئ واجهة Angular CLI نوعين من ملفات التنسيق هما: تنسيق المكونات: توفِّر واجهة Angular CLI لكل مكوِّن ملفه الخاص بالتنسيق الذي يُطبَّق على هذا المكوِّن فقط. styles.css: يوجد هذا الملف في المجلد src، ويُطبَّق التنسيق الموجود في هذا الملف على التطبيق بأكمله ما لم تحدّد تنسيقًا على مستوى المكوِّن. يمكن أن يختلف الامتداد الموجود في ملفات CSS اعتمادًا على ما إذا أردت معالج CSS المُسبَق، كما يدعم Angular لغات CSS و SCSS وSass و Less و Stylus. الصق ما يلي في الملف src/styles.css: body { font-family: Helvetica, Arial, sans-serif; } .btn-wrapper { /* flexbox */ display: flex; flex-wrap: nowrap; justify-content: space-between; } .btn { color: #000; background-color: #fff; border: 2px solid #cecece; padding: 0.35rem 1rem 0.25rem 1rem; font-size: 1rem; } .btn:hover { background-color: #ecf2fd; } .btn:active { background-color: #d1e0fe; } .btn:focus { outline: none; border: black solid 2px; } .btn-primary { color: #fff; background-color: #000; width: 100%; padding: 0.75rem; font-size: 1.3rem; border: black solid 2px; margin: 1rem 0; } .btn-primary:hover { background-color: #444242; } .btn-primary:focus { color: #000; outline: none; border: #000 solid 2px; background-color: #d7ecff; } .btn-primary:active { background-color: #212020; } يُطبَّق قسم CSS في الملف src/styles.css على التطبيق بأكمله، ولكن لا يؤثر هذا التنسيق على كل شيء في الصفحة، فالخطوة التالية هي إضافة التنسيق الذي يُطبَّق على الملف AppComponent على وجه التحديد. أضِف التنسيق التالي في الملف app.component.css: .main { max-width: 500px; width: 85%; margin: 2rem auto; padding: 1rem; text-align: center; 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: 600px) { .main { width: 70%; } } label { font-size: 1.5rem; font-weight: bold; display: block; padding-bottom: 1rem; } .lg-text-input { width: 100%; padding: 1rem; border: 2px solid #000; display: block; box-sizing: border-box; font-size: 1rem; } .btn-wrapper { margin-bottom: 2rem; } .btn-menu { flex-basis: 32%; } .active { color: green; } ul { padding-inline-start: 0; } ul li { list-style: none; } الخطوة الأخيرة هي زيارة المتصفح مرةً أخرى وإلقاء نظرة على كيفية تحديث تنسيق التطبيق الخلاصة يجب الآن أن يكون لديك قائمة المهام الأساسية معروضةً في متصفحك مع تنسيق جيد لتطبيق Angular، وسننشئ في المقال التالي مكونًا مناسبًا لعناصر المهام بحيث يمكنك تحديد عناصر المهام وتعديلها وحذفها. ترجمة -وبتصرُّف- للمقالين Beginning our Angular todo list app وStyling our Angular app. اقرأ أيضًا البدء باستخدام إطار عمل Angular مقدمة في بناء تطبيقات الويب باستخدام إطار العمل Angular وقاعدة بيانات Firestore بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore - إضافة التدوينات وعرضها
  16. حان الوقت الآن للتعرّف على إطار العمل Angular من جوجل، وهو خيار شائع آخر ستصادفه كثيرًا، إذ سنلقي في هذا المقال نظرةً على ما يقدمه إطار Angular وكيفية تثبيت المتطلبات الأساسية وإعداد تطبيق نموذجي، كما سنتعرّف على معمارية إطار Angular الأساسية. المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية. الهدف: إعداد بيئة تطوير Angular محلية وإنشاء تطبيق البداية وفهم أساسيات كيفية عمله. ما هو إطار Angular؟ يُعَدّ إطار عمل Angular منصة تطوير مبنية على لغة TypeScript، ويشمل Angular بوصفه منصةً ما يلي: إطار عمل يعتمد على المكونات لبناء تطبيقات ويب قابلة للتوسع. مجموعة من المكتبات المتكاملة جيدًا التي تغطي مجموعة متنوعة من الميزات بما في ذلك التوجيه وإدارة النماذج والاتصال بين الخادم والعميل وغير ذلك. مجموعة من أدوات المطورين لمساعدتك على تطوير وبناء واختبار وتحديث شيفرتك البرمجية. إذا أنشأت تطبيقات باستخدام إطار عمل Angular، فستستفيد من المنصة التي يمكنها التوسع من مشاريع مطور واحد إلى تطبيقات على مستوى المؤسسة، وقد صُمِّم إطار عمل Angular لتسهيل عملية التحديث قدر الإمكان، بحيث يمكنك الاستفادة من أحدث التطورات بأقل جهد ممكن، وأفضل ما في الأمر هو أنّ نظام Angular البيئي يتكون من مجموعة متنوعة تضم أكثر من 1.7 مليون مطور ومؤلف مكتبات ومنشئ محتوى. يجب أن تكون على دراية بواجهة CLI الخاصة بإطار عمل Angular قبل أن تبدأ في استكشاف منصة Angular، إذ تُعَدّ Angular CLI الطريقة الأسرع والأسهل والموصَى بها لتطوير تطبيقات Angular، وتسهّل عددًا من المهام، وإليك بعض الأمثلة عن هذه الأوامر: ‎ng build: يصرّف تطبيق Angular في مجلد الخرج. ‎ng serve: يبني تطبيقك ويشغّله ويعيد بناءه عند حدوث تغييرات في الملف. ‎ng generate: يولّد أو يعدّل الملفات على أساس تخطيطي. ‎ng test: يشغّل اختبارات الوحدة على مشروع معيّن. ‎ng e2e: يبني تطبيق Angular ويشغّله ثم يجري اختبارات شاملة. ستجد أنّ واجهة سطر الأوامر CLI في Angular أداة قيّمة لبناء تطبيقاتك. سترشدك هذه السلسلة من المقالات خلال عملية إنشاء تطبيق قائمة المهام، إذ ستتعلم كيفية استخدام إطار عمل Angular لإدارة العناصر وتعديلها وإضافتها وحذفها وترشيحها. المتطلبات المسبقة تحتاج إلى ما يلي لتثبيت Angular على نظامك المحلي: بيئة Node.js الخاصة بإطار عمل Angular التي تتطلب الإصدار الحالي أو الإصدار النشط للدعم طويل الأمد LTS أو إصدار الصيانة للدعم طويل الأمد من Node.js، ويمكنك الحصول على معلومات حول متطلبات الإصدار المحددة من خلال الاطلاع مفتاح engines في الملف package.json، كما يمكنك الحصول على مزيد من المعلومات حول تثبيت Node.js من خلال زيارة موقع nodejs.org، فإذا لم تكن متأكدًا من إصدار Node.js الذي يعمل على نظامك، فشغّل الأمر node -v في نافذة الطرفية. يعتمد مدير الحزم npm الخاص بإطار Angular وواجهة Angular CLI وتطبيقات Angular على حزم npm في العديد من الميزات والدوال، ويمكنك تنزيل حزم npm وتثبيتها باستخدام مدير حزم npm، كما سنستخدِم في هذا المقال واجهة سطر أوامر العميل في npm التي تُثبَّت افتراضيًا مع Node.js، وشغّل الأمر node -v في نافذة الطرفية للتحقق من تثبيت عميل npm. إعداد التطبيق يمكنك استخدام واجهة Angular CLI لتشغيل الأوامر في طرفيتك لإنشاء تطبيقات Angular وبنائها واختبارها ونشرها، لذا شغّل الأمر التالي في طرفيتك لتثبيت Angular CLI: npm install -g @angular/cli تبدأ أوامر Angular CLI جميعها بالاختصار ng متبوعة بما تريد أن تفعله واجهة سطر الأوامر CLI، لذا استخدم أمر ng new التالي في مجلد سطح المكتب لإنشاء تطبيق جديد بالاسم todo: ng new todo --routing=false --style=css ينشئ الأمر ng new تطبيق Angular صغير على سطح المكتب، إذ تحدد الرايات الإضافية ‎--routing و ‎--style كيفية التعامل مع التنقل والتنسيقات في التطبيق، وسنشرح هذه الميزات بمزيد من التفصيل لاحقًا، فإذا ظهرت رسالة تطالبك بفرض فحص أنواع أكثر صرامة، فيمكنك الرد بنعم. انتقل إلى مشروعك الجديد باستخدام أمر cd التالي: cd todo استخدم الأمر التالي لتشغيل التطبيق todo: ng serve إذا أظهرت واجهة CLI رسالةً تطالبك بالتحليلات، فأجب بلا. انتقل إلى المضيف المحلي http://localhost:4200/‎ في متصفحك لرؤية تطبيق البدء الجديد، فإذا عدّلتَ أيًا من ملفات المصدر، فسيُعاد تحميل التطبيق تلقائيًا. يمكن أن ترغب في فتح تبويب أو نافذة طرفية ثانية لتشغيل الأوامر أثناء تشغيل الأمر ng serve، فإذا أردت في أيّ وقت التوقف عن تشغيل تطبيقك، فاضغط على الاختصار Ctrl+c في الطرفية. تعرف على تطبيق Angular توجد ملفات التطبيق التي سنركّز عليها في هذا المقال في المجلد src/app، إذ تتضمن الملفات الأساسية التي تنشئها واجهة CLI تلقائيًا ما يلي: app.module.ts: يحدّد الملفات التي يستخدمها التطبيق، ويعمل هذا الملف على أساس موزّع مركزي للملفات الأخرى في تطبيقك. app.component.ts: المعروف بوصفه صنفًا، ويحتوي على شيفرة صفحة التطبيق الرئيسية. app.component.html: يحتوي على شيفرة HTML الخاصة بالمكوِّن AppComponent، وتُعرَف محتويات هذا الملف بوصفها قالبًا، إذ يحدد القالب العرض أو ما تراه في المتصفح. app.component.css: يحتوي على تنسيق المكوِّن AppComponent، إذ يمكنك استخدام هذا الملف عندما تريد تعريف التنسيق الذي ينطبق على مكوِّن معيّن فقط وليس على تطبيقك الكامل. يتكون المكوِّن في إطار عمل Angular من ثلاثة أجزاء رئيسية هي القالب والتنسيق والصنف، إذ تشكّل جميع الملفات app.component.ts و app.component.html و app.component.css مثلًا المكوِّن AppComponent، كما تفصل هذه البنية بين الشيفرة البرمجية والعرض والتنسيق بحيث يكون التطبيق أكثر قابلية للصيانة والتوسع، وبالتالي ستستخدِم أفضل الممارسات منذ البداية باستخدام هذه الطريقة. كما تنشئ واجهة Angular CLI ملفًا لاختبار المكوِّن بالاسم app.component.spec.ts، ولكننا لن نتطرق للاختبار، لذا يمكنك تجاهل هذا الملف. تنشئ واجهة CLI هذه الملفات الأربعة في مجلد بالاسم الذي تحدده عندما تنشئ مكوِّنًا. بنية تطبيق Angular إطار عمل Angular مبني باستخدام لغة TypeScript والتي تُعَدّ مشتقة من لغة جافاسكربت ومُوسِّعة لها، مما يعني أنّ أيّ شيفرة جافاسكربت صالحة هي شيفرة TypeScript صالحة، كما توفر لغة TypeScript أنواعًا وصيغةً أكثر إيجازًا من التي توفرها لغة جافاسكربت، مما يمنحك أداةً لإنشاء شيفرة برمجية أكثر قابليةً للصيانة مع أخطاء أقل. تُعَدّ المكونات اللبنات الأساسية لتطبيق Angular، إذ يشتمل المكوِّن على صنف TypeScript الذي يحتوي على عنصر التصميم ‎@Component()‎ الذي يحدد البيانات الوصفية له مثل قالب HTML وملف التنسيق. الصنف يُعَدّ الصنف المكان الذي تضع فيه أيّ شيفرة برمجية أو منطقًا يحتاجه المكوِّن، ويمكن أن تتضمن هذه الشيفرة دوالًا ومستمعي أحداث وخاصيات ومراجع للخدمات على سبيل المثال. يوجد الصنف في ملف باسم مثل الاسم feature.component.ts، حيث feature هو اسم مكونك، لذا يمكن أن يكون لديك ملفات بأسماء مثل header.component.ts أو signup.component.ts أو feed.component.ts، ويمكنك إنشاء مكوِّن باستخدام عنصر التصميم ‎@Component()‎ الذي يحتوي على بيانات وصفية تخبر إطار Angular بمكان العثور على ملفات HTML وCSS. إليك المكوِّن التالي: import { Component } from '@angular/core'; @Component({ selector: 'app-item', // تحدّد البيانات الوصفية التالية موقع الأجزاء الأخرى من المكون templateUrl: './item.component.html', styleUrls: ['./item.component.css'] }) export class ItemComponent { // ضع شيفرتك البرمجية هنا } يسمى هذا المكوِّن بالاسم ItemComponent ومحدّده selector بالاسم app-item، إذ يمكنك استخدام المحددات مثل وسوم HTML العادية من خلال وضعها ضمن قوالب أخرى، ويصيّر Render المتصفحُ قالب ذلك المكوِّن عندما يكون المحدِّد ضمن قالب، كما سنرشدك في المقالات اللاحقة لإنشاء مكوِّنين واستخدام أحدهما ضمن الآخر. ملاحظة: لاحظ أن اسم المكون في الأعلى ItemComponent هو نفسه اسم الصنف فما السبب؟ ببساطة الاسمان متماثلان لأن المكون ما هو إلا صنف يصفه المزخرف ‎@Component. يوفر نموذج مكونات Angular تغليفًا قويًا وبنية تطبيق سهلة الاستخدام، وتعمل المكونات على تسهيل اختبار الوحدة الخاصة بتطبيقك ويمكنها تحسين قابلية قراءة شيفرتك البرمجية الكلية. قالب HTML يحتوي كل مكوِّن على قالب HTML يوضح كيفية تصيير هذا المكوِّن، ويمكنك تعريف هذا القالب إما بطريقة مضمَّنة أو باستخدام مسار ملف. استخدم الخاصية templateUrl للإشارة إلى ملف HTML خارجي كما يلي: @Component({ selector: 'app-root', templateUrl: './app.component.html' }) export class AppComponent { } استخدم الخاصية template واكتب شيفرة HTML بين فواصل عليا مائلة Backticks لكتابة شيفرة HTML مضمّنة كما يلي: @Component({ selector: 'app-root', template: `<h1>Hi!</h1>`, }) export class AppComponent { } يوسّع إطار عمل Angular لغة HTML باستخدام صيغة إضافية تتيح إدراج قيم ديناميكية من مكونك، كما يحدّث تلقائيًا نموذج DOM المصيَّر عندما تتغير حالة المكون، وأحد استخدامات هذه الميزة هو إدراج نص ديناميكي كما هو موضح في المثال التالي: <h1>{{ title }}</h1> توجّه الأقواس المزدوجة المعقوصة إطار عمل Angular لإدخال المحتويات ضمنها، وتأتي قيمة title من صنف المكوِّن: import { Component } from '@angular/core'; @Component ({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'To do application'; } يرى المتصفح ما يلي عندما يحمّل التطبيقُ المكوِّنَ وقالبه: <h1>To do application</h1> التنسيق يمكن للمكوِّن أن يرث التنسيق العام من الملف styles.css الخاص بالتطبيق ثم يعدّل أو يعيد كتابته باستخدام تنسيقه الخاص، ويمكنك كتابة تنسيق خاص بالمكوِّن مباشرةً في عنصر التصميم ‎@Component()‎ أو تحديد المسار إلى ملف CSS. استخدِم الخاصية styles لتضمين التنسيق مباشرةً في عنصر التصميم كما يلي: @Component({ selector: 'app-root', templateUrl: './app.component.html', styles: ['h1 { color: red; }'] }) يستخدِم المكوِّن عادةً تنسيقًا موجودًا ضمن ملف منفصل باستخدام الخاصية styleUrls كما يلي: @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) يمكنك تنظيم ملف CSS بحيث يسهل صيانته ونقله باستخدام التنسيق الخاص بالمكونات. الخلاصة يجب أن تكون الآن جاهزًا لإنشاء تطبيق Angular وأن يكون لديك فهم أساسي لكيفية عمله، وسنطور في المقال التالي هذه المعرفة ونبدأ ببناء بنية تطبيق قائمة المهام. ترجمة -وبتصرُّف- للمقال Getting started with Angular. اقرأ أيضًا مبادئ AngularJS مقدمة في بناء تطبيقات الويب باستخدام إطار العمل Angular وقاعدة بيانات Firestore بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore - إضافة التدوينات وعرضها
  17. تعرّفنا في المقال السابق على مخازن Svelte وطبّقنا مخزننا المُخصَّص لاستمرار معلومات التطبيق على تخزين الويب، وألقينا نظرةً على استخدام موجّه الانتقال لتطبيق الحركة على عناصر DOM في إطار عمل Svelte. سنتعلم الآن كيفية استخدام لغة TypeScript في تطبيقات Svelte، إذ سنتعلم أولًا ماهي لغة TypeScript وفوائدها وسنرى كيفية إعداد مشروعنا للعمل مع ملفات TypeScript، ثم سننتقل إلى تطبيقنا ونرى التعديلات التي يجب إجراؤها للاستفادة الكاملة من ميزات TypeScript. المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية، كما ستحتاج طرفية مثبَّت عليها node وnpm لتصريف وبناء تطبيقك. الهدف: تعلّم كيفية إعداد واستخدام لغة TypeScript عند تطوير تطبيقات Svelte. لاحظ أنّ تطبيقنا يعمل جيدًا، ويُعَدّ نقله إلى لغة TypeScript أمرًا اختياريًا تمامًا، فهناك آراء مختلفة حول هذا الموضوع، وسنتحدث في هذا المقال بإيجاز عن إيجابيات وسلبيات استخدام هذه اللغة، إذ سيكون هذا المقال مفيدًا من خلال معرفة ما تقدمه لغة TypeScript ومساعدتك في اتخاذ قرارك حتى إذا لم تخطط لاستخدامها، فإذا لم تكن مهتمًا على الإطلاق باستخدام لغة TypeScript، فيمكنك تخطي هذا المقال والانتقال إلى المقال التالي حيث سنتعرّف على الخيارات المختلفة لنشر تطبيقات Svelte وموارد التعلم الإضافية وغير ذلك. يمكنك متابعة كتابة شيفرتك معنا، لذلك انسخ أولًا مستودع github -إذا لم تفعل ذلك مسبقًا- باستخدام الأمر التالي: git clone https://github.com/opensas/mdn-svelte-tutorial.git ثم يمكنك الوصول إلى حالة التطبيق الحالية من خلال تشغيل الأمر التالي: cd mdn-svelte-tutorial/07-typescript-support أو يمكنك تنزيل محتوى المجلد مباشرةً كما يلي: npx degit opensas/mdn-svelte-tutorial/07-typescript-support تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير. ملاحظة: لا يتوفر دعم لغة TypeScript في أداة REPL حتى الآن لسوء الحظ. لغة TypeScript: استخدام الأنواع الثابتة الاختيارية للغة جافاسكربت تُعَدّ لغة TypeScript مشتقة من لغة جافاسكربت ومُوسِّعة لها إذ توفر ميزات مثل استخدام الأنواع الثابتة الاختيارية والأصناف والواجهات والأنواع المُعمَّمة Generics، فالهدف من TypeScript هو المساعدة في اكتشاف الأخطاء مبكرًا من خلال نظام الأنواع وجعل التطوير باستخدام لغة جافاسكربت أكثر كفاءةً، كما تتمثل إحدى الفوائد الكبيرة في تفعيل بيئات التطوير المتكاملة IDE لتوفير بيئة لاكتشاف الأخطاء الشائعة أثناء كتابة الشيفرة البرمجية. أفضل ما في الأمر هو أنّ شيفرة جافاسكربت هي شيفرة TypeScript صالحة، إذ تُعَدّ لغة TypeScript مجموعةً من لغة جافاسكربت، ويمكنك إعادة تسمية معظم ملفات ‎.js إلى ملفات ‎.ts وستعمل مباشرةً، كما ستكون شيفرة TypeScript قادرةً على العمل في أيّ مكان يمكن أن تعمل فيه شيفرة جافاسكربت، إذ تترجِم لغة TypeScript شيفرتنا البرمجية إلى لغة جافاسكربت الصرفة Vanilla JavaScript، وهذا يعني أنها تحلّل شيفرة TypeScript وتنتج شيفرةً مكافئةً بلغة جافاسكربت الصرفة لتشغيلها على المتصفحات. ملاحظة: إذا كنت مهتمًا بمعرفة كيف تترجم لغة TypeScript الشيفرة البرمجية إلى لغة جافاسكربت، فيمكنك إلقاء نظرةً على TypeScript Playground. كان دعم TypeScript الميزة الأكثر طلبًا من إطار عمل Svelte لبعض الوقت، لذلك سنوضح لك في القسم التالي كيفية إعداد مشروع Svelte مع دعم TypeScript لتجربته. فوائد لغة TypeScript الفوائد الرئيسية للغة TypeScript هي: الرصد المبكر للزلات البرمجية والأخطاء: يتحقق المصرَّف Compiler من الأنواع في وقت التصريف ويقدم تقاريرًا بالأخطاء. قابلية القراءة: تمنح الأنواع الثابتة الشيفرة البرمجية بنيةً، مما يجعلها توثيقًا ذاتيًا وأكثر قابليةً للقراءة. الدعم الكبير لبيئة IDE: تسمح معلومات الأنواع لمحررات الشيفرة البرمجية وبيئات IDE بتقديم ميزات مثل التنقل في الشيفرة البرمجية والإكمال التلقائي والتلميحات الذكية. إعادة البناء الآمنة: تسمح الأنواع لبيئات IDE بمعرفة المزيد عن شيفرتك البرمجية، ومساعدتك أثناء إعادة بناء أجزاء كبيرة من قاعدة شيفرتك. استنتاج الأنواع: يمكنك الاستفادة من العديد من ميزات لغة TypeScript حتى بدون التصريح عن أنواع المتغيرات. توفير ميزات جافاسكربت الجديدة والمستقبلية: تنقل لغة TypeScript العديد من ميزات الإصدار ES6 الحديثة إلى لغة جافاسكربت القديمة، مما يسمح لك باستخدامها حتى مع وكلاء المستخدِمين الذين لا يدعمونها أصلًا حتى الآن. تحتوي لغة TypeScript على بعض العيوب هي: الأنواع الثابتة غير الصحيحة: تُختبَر الأنواع في وقت التصريف فقط، وتُزال من الشيفرة البرمجية المُنشَأة. المنحنى التعليمي الصعب: تُعَدّ لغة TypeScript صعبة التعلم بالرغم من أنها تُعَدّ مجموعةً من لغة جافاسكربت وليست لغة جديدةً تمامًا، خاصةً إذا لم يكن لديك خبرة على الإطلاق في اللغات الثابتة أو الساكنة مثل جافا أو سي شارب C#‎. مزيد من الشيفرة البرمجية: يجب كتابة مزيد من الشيفرة البرمجية والاحتفاظ بها. عدم وجود بديل للاختبارات الآلية: لا تُعَدّ لغة TypeScript بديلًا حقيقيًا لمجموعة شاملة من الاختبارات الآلية، بالرغم من أنّ الأنواع يمكنها مساعدك في اكتشاف العديد من الأخطاء. الشيفرة المتداولة Boilerplate: يمكن أن يؤدي العمل مع الأنواع والأصناف والواجهات والأنواع المُعمَّمة إلى قواعد شيفرة برمجية متداولة أو مكرَّرة كثيرًا. يبدو أنّ هناك إجماعًا واسعًا على أن لغة TypeScript مناسبة للمشاريع واسعة النطاق حيث يعمل العديد من المطورين على قاعدة الشيفرة البرمجية نفسها، إذ تستخدِمها حاليًا العديد من المشاريع الكبيرة مثل Angular 2 و Vue 3 و Ionic و Visual Studio Code و Jest ومصرِّف إطار Svelte، لكن يفضِّل بعض المطورين استخدامها في المشاريع الصغيرة أيضًا مثل المشروع الذي نطوّره حاليًا. إنشاء مشروع Svelte باستخدام لغة TypeScript من الصفر يمكنك بدء مشروع Svelte جديد باستخدام لغة TypeScript عبر القالب المعياري، إذ كل ما عليك فعله هو تشغيل أوامر الطرفية التالية التي يجب تشغيلها في المكان الذي تخزّن فيه مشاريع اختبار Svelte حيث سيُنشَأ مجلد جديد: npx degit sveltejs/template svelte-typescript-app cd svelte-typescript-app node scripts/setupTypeScript.js تؤدي هذه الأوامر إلى إنشاء مشروع أولي يتضمن دعم TypeScript والذي يمكنك تعديله لاحقًا كما يحلو لك، كما يجب بعد ذلك إخبار مدير الحزم npm بتنزيل الاعتماديات Dependencies وبدء المشروع في وضع التطوير كما يلي: npm install npm run dev إضافة دعم TypeScript إلى مشروع Svelte قائم مسبقا يمكنك إضافة دعم TypeScript إلى مشروع Svelte قائم من خلال اتباع هذه التعليمات، أو يمكنك بدلًا من ذلك تنزيل ملف setupTypeScript.js إلى المجلد scripts ضمن المجلد الجذر لمشروعك، ثم شغّل الأمر الآتي: node scripts/setupTypeScript.js كما يمكنك استخدام الأداة degit لتنزيل السكربت، وهذا ما سنفعله لبدء نقل تطبيقنا إلى لغة TypeScript. ملاحظة: تذكَّر أنه يمكنك تشغيل الأمر الآتي: npx degit opensas/mdn-svelte-tutorial/07-typescript-support svelte-todo-typescript للحصول على تطبيق قائمة المهام الكامل بلغة جافاسكربت قبل البدء في نقله إلى لغة TypeScript. انتقل إلى المجلد الجذر للمشروع وأدخِل الأوامر التالية: npx degit sveltejs/template/scripts scripts # نزِّل ملف السكربت في مجلد السكربتات node scripts/setupTypeScript.js # شغّله Converted to TypeScript. كما يجب إعادة تشغيل مدير الاعتماديات للبدء. npm install # نزّل الاعتماديات الجديدة npm run dev # شغّل التطبيق في وضع التطوير تنطبق هذه التعليمات على أيّ مشروع Svelte ترغب في تحويله إلى لغة TypeScript، وضَع في الحسبان أنّ مجتمع Svelte يعمل باستمرار على تحسين دعم TypeScript في إطار عمل Svelte، لذلك يجب تشغيل الأمر npm update بانتظام للاستفادة من أحدث التغييرات. ملاحظة: إذا وجدت أيّ مشكلة في العمل مع TypeScript ضمن تطبيق Svelte، فألقِ نظرةً على قسم استكشاف الأخطاء وإصلاحها أو الأسئلة الشائعة حول دعم TypeScript. تُعَدّ لغة TypeScript مشتقة من لغة جافاسكربت كما قلنا سابقًا، لذلك سيعمل تطبيقك بدون تعديلات، وستشغّل حاليًا تطبيق جافاسكربت عادي مع تفعيل دعم TypeScript دون الاستفادة من الميزات التي توفرها لغة TypeScript، ويمكنك الآن البدء في إضافة الأنواع تدريجيًا. يمكنك البدء في استخدام لغة TypeScript من مكونات Svelte بعد إعدادها من خلال إضافة <script lang='ts'‎> في بداية القسم script، وما عليك سوى تغيير امتداد الملف من ‎.js إلى ‎.ts لاستخدامه في ملفات جافاسكربت العادية. كما يجب تحديث أيّ تعليمات import استيراد مقابلة، إذ لا يجب أن تضمّن الامتداد ‎.ts في تعليمات الاستيراد لأن لغة TypeScript تحذف الامتدادات. ملاحظة: استخدام TypeScript في أقسام توصيف Markup المكونات غير متاح حتى الآن، لذا يجب استخدام لغة جافاسكربت في التوصيف واستخدام لغة TypeScript في القسم <script lang='ts'‎>. تحسين تجربة المطور باستخدام لغة TypeScript توفِّر لغة TypeScript محرّرات شيفرات برمجية وبيئات IDE مع الكثير من المعلومات للسماح لها بتقديم تجربة تطوير أفضل، وسنستخدِم المحرر Visual Studio Code لإجراء اختبار سريع ونرى كيف يمكننا الحصول على تلميحات الإكمال التلقائي وفحص الأنواع أثناء كتابة المكونات. ملاحظة: إذا لم ترغب في استخدام VS Code، فسنقدِّم إرشادات لاستخدام التحقق من أخطاء TypeScript من الطرفية بدلًا من ذلك لاحقًا. هناك عمل قيد التقدم لدعم TypeScript في مشاريع Svelte في العديد من محرّرات الشيفرات البرمجية، ولكن يتوفر الدعم الأكبر حتى الآن في الإضافة Svelte for VS Code extension التي يطورها ويعمل على صيانتها فريق Svelte، إذ توفر هذه الإضافة التحقق من الأنواع والفحص وإعادة البناء والتحسس الذكي للمساعدة على كتابة الشيفرة ومعلومات التمرير والإكمال التلقائي وميزات أخرى، حيث يُعَدّ هذا النوع من مساعدة المطورين سببًا وجيهًا آخر لبدء استخدام لغة TypeScript في مشاريعك. ملاحظة: تأكد من أنك تستخدِم الإضافة Svelte for VS Code وليس Svelte القديمة لصاحبها James Birtles التي جرى إيقافها، فإذا ثبّتها، فيجب عليك إلغاء تثبيتها وتثبيت إضافة Svelte الرسمية بدلًا من ذلك. لنفترض أنك ضمن تطبيق VS Code، اكتب code .‎ من جذر مجلد مشروعك، حيث تخبر هذه النقطة اللاحقة شيفرة VS بفتح المجلد الحالي لفتح محرر الشيفرة البرمجية، كما سيخبرك المحرّر VS Code بوجود إضافات موصَى بها للتثبيت. سيؤدي النقر على "تثبيت الكل Install all" إلى تثبيت الإضافة Svelte for VS Code. كما يمكننا أن نرى أنّ الملف setupTypeScript.js أجرى بعض التغييرات على مشروعنا، إذ أُعيدت تسمية الملف main.js إلى main.ts، مما يعني أنّ شيفرة VS يمكن أن توفر معلومات التمرير حول مكونات Svelte: سنحصل على ميزة التحقق من الأنواع مجانًا، فإذا مررنا خاصيةً غير معروفة في معامِل الخيارات لدالة البناء App مثل كتابة الخطأ المطبعي traget بدلًا من target، فستعطي TypeScript خطأً كما يلي: أضاف السكربت setupTypeScript.js في المكوِّن App.svelte السمة lang="ts"‎ إلى الوسم <script>. كما لن نحتاج في كثير من الحالات لتحديد الأنواع للحصول على مساعدة الشيفرة البرمجية بفضل ميزة استنتاج الأنواع، فإذا أضفتَ الخاصية ms إلى استدعاء المكوِّن Alert مثلًا، فستستنتج لغة TypeScript من القيمة الافتراضية أنّ الخاصية ms يجب أن تكون عددًا. وإذا مرّرت شيئًا ليس عددًا، فسيظهر خطأ كما يلي: يحتوي قالب التطبيق على سكربت تحقق check مُعَدّ لتشغيل أداة svelte-check للتحقق من شيفرتك البرمجية، إذ تسمح هذه الحزمة باكتشاف الأخطاء والتحذيرات التي يعرضها محرر الشيفرات البرمجية من سطر الأوامر، مما يجعلها مفيدة جدًا لتشغيلها في خط أنابيب تكامل مستمر continuous integration أو CI اختصارًا، لذا شغّل الأمر npm run check لفحص أجزاء شيفرة CSS غير المُستخدَمة، وإعادة تلميحات الشمولية A11y وأخطاء تصريف شيفرة TypeScript. إذا شغّلتَ الأمر npm run check في هذه الحالة في طرفية VS Code أو طرفية جهازك، فسيظهر الخطأ التالي: وإذا شغّلته من الطرفية المدمجة في VS Code التي يمكنك فتحها باستخدام الاختصار "Ctrl + `‎"، فسينقلك الضغط مع المفتاح Cmd أو Ctrl على اسم الملف إلى السطر الذي يحتوي على الخطأ. كما يمكنك تشغيل سكربت التحقق check في وضع المراقبة باستخدام الأمر npm run check -- --watch، حيث سيُنفَّذ السكربت في هذه الحالة كلما عدّلتَ ملفًا، فإذا شغّلتَ هذا الأمر في الطرفية العادية، فاجعلها تعمل في الخلفية في نافذة طرفية منفصلة بحيث يمكنها الاستمرار في الإبلاغ عن الأخطاء ولكنها لن تتداخل مع استخدام طرفية أخرى. إنشاء نوع مخصص تدعم لغة TypeScript الأنواع البنيوية، والتي هي طريقة لربط الأنواع بناءً على عناصرها فقط، حتى إذا لم تحدد النوع صراحةً، وسنعرِّف النوع TodoType لنرى كيف تفرض لغة TypeScript أنّ أيّ شيء يُمرَّر إلى مكوِّن يتوقع أن يكون من النوع TodoType يجب أن يكون متوافقًا بنيويًا معه. أنشئ المجلد types ضمن المجلد src، ثم أضف الملف todo.type.ts ضمنه وضع المحتوى التالي فيه: export type TodoType = { id: number name: string completed: boolean } ملاحظة: يستخدِم قالبُ Svelte المعالجَ المُسبَق svelte-preprocess 4.0.0 لدعم لغة TypeScript، كما يجب استخدام صيغة الأنواع export/import لاستيراد الأنواع والواجهات من هذا الإصدار فصاعدًا. سنستخدِم الآن النوع TodoType في المكوِّن Todo.svelte، لذا أضِف أولًا السمة lang="ts"‎ إلى الوسم <script>، لنستورد النوع ولنستخدِمه للتصريح عن الخاصية todo، لذا استبدل السطر export let todo بما يلي: import type { TodoType } from "../types/todo.type"; export let todo: TodoType; ملاحظة: تذكّر أنه يجب حذف الامتداد عند استيراد ملف ‎.ts. سننشئ الآن نسخةً من المكوِّن Todo من الملف Todos.svelte مع كائن حرفي بوصفه معامِلًا قبل استدعاء المكوِّن MoreActions كما يلي: <hr /> <Todo todo={ { name: 'a new task with no id!', completed: false } } /> <!-- MoreActions --> <MoreActions {todos} أضف السمة lang='ts'‎ إلى الوسم <script> الخاص بالمكوِّن Todos.svelte لمعرفة كيفية استخدام التحقق من الأنواع الذي حددناه، ولكن سنحصل على الخطأ التالي: يجب الآن أن تحصل على فكرة حول نوع المساعدة التي يمكننا الحصول عليها من لغة TypeScript عند إنشاء مشاريع Svelte، وسنتراجع الآن عن هذه التغييرات من أجل البدء في نقل تطبيقنا إلى لغة TypeScript، لذلك لن نشعر بالانزعاج من جميع تحذيرات التحقق. أزِل المهمة التي تسبب الأخطاء والسمة lang='ts'‎ من الملف Todos.svelte. أزِل استيراد النوع TodoType والسمة lang='ts'‎ من الملف Todos.svelte. نقل تطبيق قائمة المهام إلى لغة TypeScript أصبحنا الآن جاهزين لبدء نقل تطبيق قائمة المهام للاستفادة من جميع الميزات التي توفرها لغة TypeScript. لنبدأ بتشغيل سكربت التحقق في وضع المراقبة ضمن جذر المشروع: npm run check -- --watch يجب أن ينتج ما يلي: svelte-check "--watch" Loading svelte-check in workspace: ./svelte-todo-typescript Getting Svelte diagnostics... ==================================== svelte-check found no errors and no warnings لاحظ أنه إذا استخدَمتَ محرر شيفرات داعم مثل المحرّر VS Code، فستكون الطريقة البسيطة لبدء نقل مكوِّن Svelte هي بإضافة <script lang='ts'‎> فقط في أعلى المكوِّن والبحث عن تلميحات ثلاثية النقاط كما يلي: المكون Alert.svelte لنبدأ بالمكوِّن Alert.svelte. أضِف السمة lang="ts"‎ إلى الوسم <script> الخاص بالمكوِّن Alert.svelte، إذ سترى بعض التحذيرات في خرج السكربت check كما يلي: $ npm run check -- --watch > svelte-check "--watch" ./svelte-todo-typescript Getting Svelte diagnostics... ==================================== ./svelte-todo-typescript/src/components/Alert.svelte:8:7 Warn: Variable 'visible' implicitly has an 'any' type, but a better type may be inferred from usage. (ts) let visible ./svelte-todo-typescript/src/components/Alert.svelte:9:7 Warn: Variable 'timeout' implicitly has an 'any' type, but a better type may be inferred from usage. (ts) let timeout ./svelte-todo-typescript/src/components/Alert.svelte:11:28 Warn: Parameter 'message' implicitly has an 'any' type, but a better type may be inferred from usage. (ts) Change = (message, ms) => { ./svelte-todo-typescript/src/components/Alert.svelte:11:37 Warn: Parameter 'ms' implicitly has an 'any' type, but a better type may be inferred from usage. (ts) (message, ms) => { يمكنك إصلاح هذه التحذيرات من خلال تحديد الأنواع المقابلة كما يلي: export let ms = 3000 let visible: boolean let timeout: number const onMessageChange = (message: string, ms: number) => { clearTimeout(timeout) if (!message) { // إخفاء التنبيه إذا كانت الرسالة فارغة ملاحظة: ليست هناك حاجة لتحديد نوع ms في التعليمة export let ms:number = 3000، لأن لغة TypeScript تستنتجه مباشرةً من قيمته الافتراضية. المكون MoreActions.svelte سنطبّق الآن الشيء نفسه على المكوِّن MoreActions.svelte. أضِف السمة lang='ts'‎، وستحذِّرنا لغة TypeScript من الخاصية todos والمتغير t في الاستدعاء todos.filter((t) =>...)‎. Warn: Variable 'todos' implicitly has an 'any' type, but a better type may be inferred from usage. (ts) export let todos Warn: Parameter 't' implicitly has an 'any' type, but a better type may be inferred from usage. (ts) $: completedTodos = todos.filter((t) => t.completed).length سنستخدِم النوع TodoType الذي عرّفناه سابقًا لإعلام لغة TypeScript أنّ الخاصية todos هي مصفوفة TodoType، لذا استبدل export let todos بما يلي: import type { TodoType } from "../types/todo.type"; export let todos: TodoType[]; لاحظ أنّ لغة TypeScript يمكنها الآن أن تستنتج أنّ المتغير t في الاستدعاء todos.filter(t => t.completed)‎ من النوع TodoType، ولكن يمكننا تحديده كما يلي إذا اعتقدنا أنّ ذلك يسهل قراءة شيفرتنا: $: completedTodos = todos.filter((t: TodoType) => t.completed).length; ستكون لغة TypeScript قادرةً على استنتاج نوع المتغير التفاعلي بصورة صحيحة، ولكن يمكن أن يظهر الخطأ "implicitly has an 'any' type" عند العمل مع الإسنادات التفاعلية، لذا يمكنك في هذه الحالات التصريح عن نوع المتغير في تعليمة مختلفة كما يلي: let completeTodos: number; $: completedTodos = todos.filter((t: TodoType) => t.completed).length; لا يمكنك تحديد النوع في الإسناد التفاعلي نفسه، فالتعليمة التالي غير صالحة: ‎$: completedTodos: number = todos.filter[...]‎ المكون FilterButton.svelte أضف السمة lang='ts'‎ إلى الوسم <script> مثل العادة وستلاحظ عدم وجود تحذيرات، إذ تستنتج لغة TypeScript نوع متغير الترشيح filter من قيمته الافتراضية، ولكننا نعلم أنه يملك ثلاث قيم صالحة فقط هي: جميع المهام all والمهام النشط active والمهام المكتملة completed، حيث يمكننا السماح للغة TypeScript بالتعرف على هذه القيم من خلال إنشاء مُرشح filter من النوع الثوابت المتعددة enum. أنشئ بعدها ملفًا بالاسم filter.enum.ts في المجلد types، وضع فيه المحتويات التالية: export enum Filter { ALL = 'all', ACTIVE = 'active', COMPLETED = 'completed', } سنستخدِم ذلك الآن في المكوِّن FilterButton، لذا استبدل محتوى الملف FilterButton.svelte بما يلي: <!-- components/FilterButton.svelte --> <script lang='ts'> import { Filter } from '../types/filter.enum' export let filter: Filter = Filter.ALL </script> <div class="filters btn-group stack-exception"> <button class="btn toggle-btn" class:btn__primary={filter === Filter.ALL} aria-pressed={filter === Filter.ALL} on:click={()=> filter = Filter.ALL} > <span class="visually-hidden">Show</span> <span>All</span> <span class="visually-hidden">tasks</span> </button> <button class="btn toggle-btn" class:btn__primary={filter === Filter.ACTIVE} aria-pressed={filter === Filter.ACTIVE} on:click={()=> filter = Filter.ACTIVE} > <span class="visually-hidden">Show</span> <span>Active</span> <span class="visually-hidden">tasks</span> </button> <button class="btn toggle-btn" class:btn__primary={filter === Filter.COMPLETED} aria-pressed={filter === Filter.COMPLETED} on:click={()=> filter = Filter.COMPLETED} > <span class="visually-hidden">Show</span> <span>Completed</span> <span class="visually-hidden">tasks</span> </button> </div> استوردنا فقط المرشح Filter من النوع enum واستخدمناه بدلًا من قيم السلاسل النصية التي استخدمناها سابقًا. المكون Todos.svelte سنستخدِم المرشح Filter من النوع enum في المكوِّن Todos.svelte. أضِف أولًا السمة lang='ts'‎ إليه، ثم استورد المرشح Filter من النوع enum، لذا أضِف تعليمة الاستيراد التالية بعد تعليمات الاستيراد الموجودة مسبقًا: import { Filter } from "../types/filter.enum"; سنستخدِمه الآن في أيّ مكان نشير فيه إلى المرشّح Filter الحالي، لذا استبدل الكتلتين المتعلقتين به بما يلي: let filter: Filter = Filter.ALL; const filterTodos = (filter: Filter, todos) => filter === Filter.ACTIVE ? todos.filter((t) => !t.completed) : filter === Filter.COMPLETED ? todos.filter((t) => t.completed) : todos; $: { if (filter === Filter.ALL) $alert = "Browsing all todos"; else if (filter === Filter.ACTIVE) $alert = "Browsing active todos"; else if (filter === Filter.COMPLETED) $alert = "Browsing completed todos"; } سيظل السكربت check يعطينا بعض التحذيرات من المكوِّن Todos.svelte، فلنصلح ذلك من خلال استيراد النوع TodoType وإعلام لغة TypeScript أنّ المتغير todos هو مصفوفة من النوع TodoType، لذا استبدل السطر export let todos = []‎ بالسطرين التاليين: import type { TodoType } from "../types/todo.type"; export let todos: TodoType[] = []; سنحدّد بعد ذلك جميع الأنواع المفقودة، إذ يُعَدّ المتغير todosStatus -الذي استخدمناه للوصول برمجيًا إلى التوابع التي يمكن الوصول إليها من المكوِّن TodosStatus- من النوع TodosStatus، كما ستكون كل مهمة todo من النوع TodoType، لذا عدّل القسم <script> ليبدو كما يلي: <script lang='ts'> import FilterButton from './FilterButton.svelte' import Todo from './Todo.svelte' import MoreActions from './MoreActions.svelte' import NewTodo from './NewTodo.svelte' import TodosStatus from './TodosStatus.svelte' import { alert } from '../stores' import { Filter } from '../types/filter.enum' import type { TodoType } from '../types/todo.type' export let todos: TodoType[] = [] let todosStatus: TodosStatus // ‫مرجع إلى نسخة TodosStatus $: newTodoId = todos.length > 0 ? Math.max(...todos.map(t => t.id)) + 1 : 1 function addTodo(name: string) { todos = [...todos, { id: newTodoId, name, completed: false }] $alert = `Todo '${name}' has been added` } function removeTodo(todo: TodoType) { todos = todos.filter(t => t.id !== todo.id) todosStatus.focus() // نقل التركيز إلى عنوان الحالة $alert = `Todo '${todo.name}' has been deleted` } function updateTodo(todo: TodoType) { const i = todos.findIndex(t => t.id === todo.id) if (todos[i].name !== todo.name) $alert = `todo '${todos[i].name}' has been renamed to '${todo.name}'` if (todos[i].completed !== todo.completed) $alert = `todo '${todos[i].name}' marked as ${todo.completed ? 'completed' : 'active'}` todos[i] = { ...todos[i], ...todo } } let filter: Filter = Filter.ALL const filterTodos = (filter: Filter, todos: TodoType[]) => filter === Filter.ACTIVE ? todos.filter(t => !t.completed) : filter === Filter.COMPLETED ? todos.filter(t => t.completed) : todos $: { if (filter === Filter.ALL) $alert = 'Browsing all todos' else if (filter === Filter.ACTIVE) $alert = 'Browsing active todos' else if (filter === Filter.COMPLETED) $alert = 'Browsing completed todos' } const checkAllTodos = (completed: boolean) => { todos = todos.map(t => ({...t, completed})) $alert = `${completed ? 'Checked' : 'Unchecked'} ${todos.length} todos` } const removeCompletedTodos = () => { $alert = `Removed ${todos.filter(t => t.completed).length} todos` todos = todos.filter(t => !t.completed) } </script> المكون TodosStatus.svelte نواجه الأخطاء التالية المتعلقة بتمرير todos إلى المكوِّنين TodosStatus.svelte و Todo.svelte: ./src/components/Todos.svelte:70:39 Error: Type 'TodoType[]' is not assignable to type 'undefined'. (ts) <TodosStatus bind:this={todosStatus} {todos} /> ./src/components/Todos.svelte:76:12 Error: Type 'TodoType' is not assignable to type 'undefined'. (ts) <Todo {todo} تظهر هذه الأخطاء لأن الخاصية todos في المكوِّن TodosStatus ليس لها قيمة افتراضية، لذلك استنتجت لغة TypeScript أنها من النوع غير المعرَّف undefined، وهو غير متوافق مع مصفوفة من النوع TodoType، كما يحدث الشيء نفسه مع المكوِّن Todo، إذًا لنصلح هذه الأخطاء. افتح الملف TodosStatus.svelte وأضِف السمة lang='ts'‎ ثم استورد النوع TodoType وصرّح عن الخاصية todos على أنها مصفوفة من النوع TodoType، لذا استبدل السطر الأول من القسم <script> بما يلي: import type { TodoType } from "../types/todo.type"; export let todos: TodoType[]; كما سنحدّد المتغير headingEl -الذي استخدمناه لربط وسم العنوان- بوصفه من النوع HTMLElement، لذا عدّل السطر let headingEl كما يلي: let headingEl: HTMLElement; أخيرًا، ستلاحظ الخطأ التالي المتعلق بالمكان الذي ضبطنا فيه السمة tabindex، لأن لغة TypeScript تتحقق من نوع العنصر <h2> وتتوقع أن تكون السمة tabindex من النوع العددي number. يمكن إصلاح هذا الخطأ من خلال استبدال tabindex="-1"‎ بالشكل tabindex={-1}‎ كما يلي: <h2 id="list-heading" bind:this="{headingEl}" tabindex="{-1}"> {completedTodos} out of {totalTodos} items completed </h2> يمكن أن تمنعنا لغة TypeScript باستخدام هذه الطريقة من إسناد هذه السمة إلى متغير من نوع السلسلة النصية بصورة غير صحيحة. المكون NewTodo.svelte أضِف السمة lang='ts'‎ مثل العادة، حيث سيشير التحذير إلى أنه يجب تحديد نوع للمتغير nameEl، لذا اضبط نوعه على النوع HTMLElement كما يلي: let nameEl: HTMLElement; // reference to the name input DOM node كما يجب تحديد النوع الصحيح للمتغير autofocus، لذا عدّل تعريفه إلى ما يلي: export let autofocus: boolean = false; المكون Todo.svelte تظهر التحذيرات الوحيدة التي يصدرها الأمر npm run check من خلال استدعاء المكوِّن Todo.svelte، إذًا لنصلحها. افتح الملف Todo.svelte وأضِف السمة lang='ts'‎ ثم استورد النوع TodoType واضبط نوع الخاصية todo، أي استبدل السطر export let todo بما يلي: import type { TodoType } from "../types/todo.type"; export let todo: TodoType; التحذير الأول الذي حصلنا عليه هو أنّ لغة TypeScript تُعلِمنا بأنه يجب تحديد نوع المتغير updatedTodo الخاص بالدالة update()‎، كما يمكن أن يكون هذا صعبًا بعض الشيء لأن المتغير updatedTodo يحتوي فقط على سمات المهمة todo التي جرى تحديثها، وهذا يعني أنها ليست مهمة todo كاملة، فهي تحتوي فقط على مجموعة فرعية من خاصيات المهام، لذلك توفر لغة TypeScript العديد من الأدوات المساعدة الخاصة بالأنواع لتسهيل تطبيق هذه التحويلات الشائعة، حيث سنستخدِم الآن الأداة المساعدة Partial<T>‎ التي تتيح تمثيل جميع المجموعات الفرعية من نوع معيّن، وتعيد نوعًا جديدًا بناءً على النوع T، إذ تكون كل خاصية من خصائص T اختياريةً، كما سنستخدِم هذه الأداة في الدالة update()‎، لذلك عدّلها كما يلي: function update(updatedTodo: Partial<TodoType>) { todo = { ...todo, ...updatedTodo }; // تطبيق التعديلات على المهمة dispatch("update", todo); // إصدار حدث التحديث } بذلك نخبر لغة TypeScript أنّ المتغير updatedTodo سيحتوي على مجموعة فرعية من الخاصيات ذات النوع TodoType. تخبرنا الآن الأداة svelte-check أنه يجب تعريف نوع معامِلات دالة الإجراء: ./07-next-steps/src/components/Todo.svelte:45:24 Warn: Parameter 'node' implicitly has an 'any' type, but a better type may be inferred from usage. (ts) const focusOnInit = (node) => node && typeof node.focus === 'function' && node.focus() ./07-next-steps/src/components/Todo.svelte:47:28 Warn: Parameter 'node' implicitly has an 'any' type, but a better type may be inferred from usage. (ts) const focusEditButton = (node) => editButtonPressed && node.focus() يجب فقط تعريف المعامل node ليكون من النوع HTMLElement، لذا استبدل النسخة الأولى من المعامِل node بالشكل node: HTMLElement في السطرين السابقين المشار إليهما. الملف actions.js أعد تسمية الملف actions.js إلى actions.ts وأضِف نوع المعامِل node كما يلي: // actions.ts export function selectOnFocus(node: HTMLInputElement) { if (node && typeof node.select === "function") { // ‫تأكد من أن المعامل node مُعرَّف ولديه التابع select()‎ const onFocus = () => node.select(); // معالج الحدث node.addEventListener("focus", onFocus); // ‫استدعِ التابع onFocus()‎ عند انتقال التركيز إلى العقدة node return { destroy: () => node.removeEventListener("focus", onFocus), // ‫سيُنفَّذ هذا عند حذف العقدة node في نموذج DOM }; } } عدّل الآن الملفين Todo.svelte و NewTodo.svelte حيث نستورد ملف الإجراءات actions، وتذكّر أن تعليمات الاستيراد في لغة TypeScript لا تتضمن امتداد الملف. import { selectOnFocus } from "../actions"; ترحيل المخازن Stores إلى لغة TypeScript يجب الآن ترحيل الملفين stores.js و localStore.js إلى لغة TypeScript. سيتحقق السكربت npm run check الذي يستخدِم الأداة svelte-check من ملفات ‎.svelte الخاصة بتطبيقنا، فإذا أردتَ التحقق من ملفات ‎.ts، فيمكنك تشغيل الأمر npm run check && npx tsc --noemit الذي يخبر مصرِّف TypeScript بالتحقق من الأخطاء دون إنشاء ملفات الخرج ‎.js، كما يمكنك إضافة سكربت إلى الملف package.json الذي يشغّل ذلك الأمر. سنبدأ بترحيل الملف stores.js: أعِد تسمية الملف إلى stores.ts ثم اضبط نوع المصفوفة initialTodos إلى النوع TodoType[]‎ كما يلي: // stores.ts import { writable } from "svelte/store"; import { localStore } from "./localStore.js"; import type { TodoType } from "./types/todo.type"; export const alert = writable("Welcome to the To-Do list app!"); const initialTodos: TodoType[] = [ { id: 1, name: "Visit MDN web docs", completed: true }, { id: 2, name: "Complete the Svelte Tutorial", completed: false }, ]; export const todos = localStore("mdn-svelte-todo", initialTodos); تذكّر تحديث تعليمات الاستيراد في الملفات App.svelte و Alert.svelte وTodos.svelte، وأزِل فقط الامتداد ‎.js كما يلي: import { todos } from "../stores"; لننتقل الآن إلى الملف localStore.js، وعدّل تعليمة الاستيراد في الملف stores.ts كما يلي: import { localStore } from "./localStore"; أعِد تسمية الملف إلى localStore.ts، حيث تخبرنا لغة TypeScript بأنه يجب تحديد نوع المتغيرات key و initial و value، إذ يجب أن يكون المتغير الأول -وهو مفتاح تخزين الويب المحلي- سلسلةً نصيةً، لكن يجب أن يكون المتغيران initial و value أيّ كائن يمكن تحويله إلى سلسلة JSON صالحة باستخدام التابع JSON.stringify، مما يعني أن أيّ كائن جافاسكربت له قيود معينة مثل undefined والدوال والرموز التي ليست قيم JSON صالحة، لذا سننشئ النوع JsonValue لتحديد هذه الشروط. أنشئ الملف json.type.ts في المجلد types وضع فيه المحتوى التالي: export type JsonValue = | string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; يتيح المعامِل | التصريح عن المتغيرات التي يمكنها تخزين قيم من نوعين أو أكثر، كما يمكن أن يكون النوع JsonValue سلسلةً نصيةً أو عددًا أو قيمةً منطقيةً وما إلى ذلك، كما نستخدِم في هذه الحالة الأنواع العودية لتحديد أنّ النوع JsonValue يمكن أن يحتوي على مصفوفة من القيم ذات النوع JsonValue وكائن له خاصيات من النوع JsonValue. سنستورد النوع JsonValue وسنستخدِمه وفقًا لذلك، لذا عدّل الملف localStore.ts كما يلي: // localStore.ts import { writable } from "svelte/store"; import type { JsonValue } from "./types/json.type"; export const localStore = (key: string, initial: JsonValue) => { // يتلقى مفتاح التخزين المحلي وقيمة أولية const toString = (value: JsonValue) => JSON.stringify(value, null, 2); // دالة مساعدة const toObj = JSON.parse; // دالة مساعدة if (localStorage.getItem(key) === null) { // العنصر غير موجود في التخزين المحلي localStorage.setItem(key, toString(initial)); // تهيئة التخزين المحلي بالقيمة الأولية } const saved = toObj(localStorage.getItem(key)); // تحويل إلى كائن const { subscribe, set, update } = writable(saved); // إنشاء المتجر الأساسي القابل للكتابة return { subscribe, set: (value: JsonValue) => { localStorage.setItem(key, toString(value)); // حفظ القيمة في التخزين المحلي كسلسلة نصية return set(value); }, update, }; }; إذا حاولنا الآن إنشاء مخزن localStore مع شيء لا يمكن تحويله إلى صيغة JSON باستخدام التابع JSON.stringify()‎ مثل كائن مع دالة بوصفها خاصيةً له، فسيعطي المحرر VS Code أو validate خطأً كما يلي: سيعمل الملف localStore.ts مع صيغة الاشتراك التلقائي ‎$store، فإذا حاولنا حفظ قيمة غير صالحة في مخزن todos باستخدام صيغة ‎$store كما يلي: <!-- App.svelte --> <script lang="ts"> import Todos from "./components/Todos.svelte"; import Alert from "./components/Alert.svelte"; import { todos } from "./stores"; // ‫هذه قيمة غير صالحة، فالمحتوى لا يمكن تحويله إلى صيغة JSON باستخدام JSON.stringify $todos = { handler: () => {} }; </script> فسيعطي سكربت التحقق الخطأ التالي: > npm run check Getting Svelte diagnostics... ==================================== ./svelte-todo-typescript/src/App.svelte:8:12 Error: Argument of type '{ handler: () => void; }' is not assignable to parameter of type 'JsonValue'. Types of property 'handler' are incompatible. Type '() => void' is not assignable to type 'JsonValue'. Type '() => void' is not assignable to type '{ [key: string]: JsonValue; }'. Index signature is missing in type '() => void'. (ts) $todos = { handler: () => {} } يُعَدّ ذلك مثالًا آخر على كيفية تحديد الأنواع الذي يمكن أن يجعل شيفرتنا أقوى ويساعدنا في اكتشاف مزيد من الأخطاء قبل دخولها في عملية الإنتاج. حوّلنا بذلك تطبيقنا بالكامل ليستخدِم لغة TypeScript. حماية المخازن من الأخطاء باستخدام الأنواع المعممة نقلنا المخازن إلى لغة TypeScript، ولكن يمكننا تنفيذ عمل أفضل، فلا يجب علينا تخزين أيّ نوع من القيم، لأننا نعلم أنّ مخزن التنبيه alert يجب أن يحتوي على رسائل نصية، كما يجب أن يحتوي متجر المهام todos على مصفوفة من النوع TodoType وما إلى ذلك، ويمكننا السماح للغة TypeScript بفرض ذلك باستخدام الأنواع المُعمَّمة Generics في لغة TypeScript. الأنواع المعممة Generics في لغة TypeScript تسمح الأنواع المُعمَّمة بإنشاء مكونات برمجية قابلة لإعادة الاستخدام تعمل مع مجموعة متنوعة من الأنواع بدلًا من نوع واحد، ويمكن تطبيقها على الواجهات والأصناف والدوال. تُمرَّر الأنواع المُعمَّمة بوصفها معامِلات باستخدام صيغة خاصة، إذ تُحدَّد بين قوسَي زاوية، ويُشار إليها حسب الاصطلاح بحرف واحد كبير، كما تسمح الأنواع المُعمَّمة بالتقاط الأنواع التي يوفرها المستخدِم لاستخدامها لاحقًا، ولنرى مثالًا سريعًا يمثل الصنف Stack البسيط الذي يتيح دفع push العناصر وسحبها pop كما يلي: export class Stack { private elements = [] push = (element) => this.elements.push(element) pop() { if (this.elements.length === 0) throw new Error('The stack is empty!') return this.elements.pop() } } تكون elements في هذه الحالة مصفوفةً من النوع any، وبالتالي يستقبل ويعيد التابعان push()‎ و pop()‎ متغيرًا من النوع any، لذا يمكنك تطبيق ما يلي: const anyStack = new Stack(); anyStack.push(1); anyStack.push("hello"); لكن إذا أردنا الحصول على الصنف Stack الذي يعمل فقط مع نوع السلسلة النصية string، فيمكننا تطبيق ما يلي: export class StringStack { private elements: string[] = [] push = (element: string) => this.elements.push(element) pop(): string { if (this.elements.length === 0) throw new Error('The stack is empty!') return this.elements.pop() } } أما إذا أردنا التعامل مع الأعداد، فسنضطر بعد ذلك إلى تكرار الشيفرة وإنشاء الصنف NumberStack، وإذا أردنا التعامل مع مجموعة من الأنواع التي لا نعرفها بعد ويجب أن يعرّفها المستخدِم، فيمكننا استخدام الأنواع المُعمَّمة Generics. إليك الصنف Stack الذي أعيد تطبيقه باستخدام الأنواع المُعمَّمة Generics: export class Stack<T> { private elements: T[] = [] push = (element: T): number => this.elements.push(element) pop(): T { if (this.elements.length === 0) throw new Error('The stack is empty!') return this.elements.pop() } } نعرِّف النوع T المُعمَّم ثم نستخدِمه كما نستخدِم أيّ نوع آخر، إذ تُعَدّ elements الآن مصفوفةً من النوع T، كما يستقبل ويعيد كل من التابعين push()‎ و pop()‎ متغيرًا من النوع T. إليك الطريقة التي نستخدِم بها النوع المُعمَّم Stack: const numberStack = new Stack<number>() numberStack.push(1) تعرف لغة TypeScript الآن أنّ Stack يمكنه قبول الأعداد فقط وسيعطي خطأً إذا حاولنا دفع أيّ شيء آخر: كما يمكن للغة TypeScript استنتاج الأنواع المُعمَّمة من خلال استخدامها، إذ تدعم القيم الافتراضية والقيود، كما تُعَدّ الأنواع المُعمَّمة ميزةً قويةً تسمح للشيفرة البرمجية بتجريد الأنواع المُحدَّدة المُستخدَمة، مما يجعلها أكثر قابليةً لإعادة الاستخدام ومُعمَّمة دون التخلي عن أمان الأنواع. استخدام مخازن Svelte مع الأنواع المعممة تدعم مخازن Svelte الأنواع المُعمَّمة، إذ يمكننا الاستفادة من ميزة استنتاج الأنواع المُعمَّمة دون لمس شيفرتنا البرمجية، فإذا فتحت الملف Todos.svelte وأسندتَ النوع number إلى المخزن ‎$alert، فستحصل على الخطأ التالي: لأنه عندما عرّفنا المخزن alert في الملف stores.ts باستخدام ما يلي: export const alert = writable("Welcome to the To-Do list app!"); استنتجت لغة TypeScript أنّ النوع المُعمَّم هو سلسلة نصية string، فإذا أردنا أن نكون صريحين بشأن ذلك، فيمكننا كتابة ما يلي: export const alert = writable < string > "Welcome to the To-Do list app!"; سنجعل الآن المخزن localStore يدعم الأنواع المُعمَّمة، وتذكّر أننا عرّفنا النوع JsonValue لمنع استخدام المخزن localStore مع قيم لا يمكن استمرارها باستخدام التابع JSON.stringify()‎. نريد الآن أن يتمكن مستخدِمو المخزن localStore من تحديد نوع البيانات المستمرة، ولكن يجب استخدام النوع JsonValue بدلًا من العمل مع أيّ نوع، لذا سنحدد ذلك بقيد مُعمَّم كما يلي: export const localStore = <T extends JsonValue>(key: string, initial: T) سنعرّف النوع المُعمَّم T وسنحدِّد أنه يجب أن يكون متوافقًا مع النوع JsonValue، ثم سنستخدِم النوع T بطريقة مناسبة، وسيكون الملف localStore.ts كما يلي: // localStore.ts import { writable } from 'svelte/store' import type { JsonValue } from './types/json.type' export const localStore = <T extends JsonValue>(key: string, initial: T) => { // يتلقى مفتاح التخزين المحلي وقيمة أولية const toString = (value: T) => JSON.stringify(value, null, 2) // دالة مساعدة const toObj = JSON.parse // دالة مساعدة if (localStorage.getItem(key) === null) { // العنصر غير موجود في التخزين المحلي localStorage.setItem(key, toString(initial)) // تهيئة التخزين المحلي بالقيمة الأولية } const saved = toObj(localStorage.getItem(key)) // تحويل إلى كائن const { subscribe, set, update } = writable<T>(saved) // إنشاء المتجر الأساسي القابل للكتابة return { subscribe, set: (value: T) => { localStorage.setItem(key, toString(value)) // حفظ القيمة في التخزين المحلي كسلسلة نصية return set(value) }, update } } تعرف لغة TypeScript أن المخزن ‎$todos يجب أن يحتوي على مصفوفة من النوع TodoType بفضل ميزة استنتاج الأنواع المُعمَّمة: كما ذكرنا سابقًا إذا أردنا أن نكون صريحين، فيمكننا ذلك في الملف stores.ts كما يلي: const initialTodos: TodoType[] = [ { id: 1, name: 'Visit MDN web docs', completed: true }, { id: 2, name: 'Complete the Svelte Tutorial', completed: false }, ] export const todos = localStore<TodoType[]>('mdn-svelte-todo', initialTodos) يمكنك الوصول إلى نسختك من مستودعنا على النحو التالي لمعرفة حالة الشيفرة كما يجب أن تكون في نهاية هذا المقال: cd mdn-svelte-tutorial/08-next-steps أو يمكنك تنزيل محتوى المجلد مباشرةً باستخدام الأمر التالي: npx degit opensas/mdn-svelte-tutorial/08-next-steps تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير. ملاحظة: كما قلنا سابقًا لا يتوفر دعم لغة TypeScript في أداة REPL حتى الآن لسوء الحظ. الخلاصة نقلنا تطبيق قائمة المهام إلى لغة TypeScript في هذا المقال، إذ تعرّفنا أولًا على لغة TypeScript ومزاياها، ثم رأينا كيفية إنشاء مشروع Svelte جديد مع دعم TypeScript، وكيفية تحويل مشروع Svelte قائم هو تطبيق قائمة المهام لاستخدام لغة TypeScript. رأينا كيفية العمل مع المحرّر Visual Studio Code والإضافة Svelte للحصول على ميزات مثل التحقق من الأنواع والإكمال التلقائي، واستخدمنا الأداة svelte-check لفحص مشاكل TypeScript من سطر الأوامر، كما سنتعلّم في المقال التالي كيفية تصريف ونشر تطبيقنا للإنتاج، وسنرى موارد التعلم المتاحة عبر الإنترنت للمضي قدمًا في تعلم إطار عمل Svelte. ترجمة -وبتصرُّف- للمقال TypeScript support in Svelte. اقرأ أيضًا بدء استخدام إطار العمل Svelte لبناء تطبيقات ويب مدخل إلى TypeScript الخطوات الأولى في بناء تطبيقات الويب باستعمال TypeScript إنشاء تطبيق قائمة مهام باستعمال إطار عمل Svelte
  18. أكملنا في مقال التفاعلية ودورة الحياة وسهولة وصول المستخدمين تطوير تطبيقنا وانتهينا من تنظيمه في مكونات، كما ناقشنا بعض التقنيات المتقدمة للتعامل مع التفاعلية والعمل مع عُقد DOM والوصول إلى وظائف المكونات، وسنعرض في هذا المقال طريقةً أخرى للتعامل مع إدارة الحالة في إطار عمل Svelte وهي المخازن Stores والتي تُعَدّ مستودعات بيانات عامة تحتفظ بالقيم، ويمكن للمكونات الاشتراك بالمخازن وتلقّي إشعارات عندما تتغير قيمها. المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية، كما ستحتاج طرفية مثبَّت عليها node و npm لتصريف وبناء تطبيقك. الهدف: تعلّم كيفية استخدام المخازن Stores في إطار عمل Svelte. سننشئ المكوِّن Alert الذي يعرض إشعارات على الشاشة باستخدام المخازن، ويمكنه استقبال الرسائل من جميع المكونات الأخرى، إذ يكون المكوِّن Alert في هذه الحالة مستقلًا عن المكونات الأخرى، فهو ليس أبًا أو ابنًا لأيّ مكوِّن آخر، لذلك لا تتناسب الرسائل مع تسلسل المكونات الهرمي، كما سنرى كيفية تطوير المخزن المخصَّص للاحتفاظ بمعلومات المهام في تخزين الويب، مما يسمح لمهامنا بالاستمرار خلال في صفحات التحميل. يمكن متابعة كتابة شيفرتك معنا، لذلك انسخ أولًا مستودع github -إذا لم تفعل ذلك مسبقًا- باستخدام الأمر التالي: git clone https://github.com/opensas/mdn-svelte-tutorial.git ثم يمكنك الوصول إلى حالة التطبيق الحالية من خلال تشغيل الأمر التالي: cd mdn-svelte-tutorial/06-stores أو يمكنك تنزيل محتوى المجلد مباشرةً كما يلي: npx degit opensas/mdn-svelte-tutorial/06-stores تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، فإذا أردت متابعتنا، فابدأ بكتابة الشيفرة باستخدام الأداة REPL من svelte.dev. التعامل مع حالة تطبيقنا رأينا مسبقًا كيف يمكن لمكوناتنا التواصل مع بعضها البعض باستخدام الخاصيات وربط البيانات ثنائي الاتجاه والأحداث، إذ تعاملنا في كل هذه الحالات مع التواصل بين المكونات الآباء والمكونات الأبناء، لكن لا تنتمي جميع حالات التطبيق إلى تسلسل مكونات التطبيق الهرمي مثل معلومات المستخدِم الذي سجّل الدخول أو ما إذا كان القالب الداكن محددًا أم لا، إذ ستحتاج حالة التطبيق في بعض الأحيان إلى الوصول إليها من خلال عدة مكونات غير مرتبطة بالتسلسل الهرمي أو عن طريق وحدة JavaScript عادية. كما يمكن أن يصبح ترحيل البيانات بين المكونات صعبًا جدًا عندما يكون تطبيقك وتسلسل المكونات الهرمي معقّدَين، لذا يمكن أن يكون الانتقال إلى مخزن بيانات عام خيارًا جيدًا في هذه الحالة، فإذا استخدمت مسبقًا مكتبتَي Redux أو Vuex، فستكون على دراية بكيفية عمل هذا النوع من المخازن، إذ تقدّم مخازن Svelte ميزات مماثلة لإدارة الحالة. يُعَدّ المخزن Store كائنًا يمتلك التابع subscribe()‎ الذي يسمح بإعلام الأطراف المهتمة كلما تغيرت قيمة هذا المخزن، والتابع الاختياري set()‎ الذي يسمح بضبط قيم جديدة للمخزن، إذ يُعرَف الحد الأدنى من واجهة برمجة التطبيقات باسم عَقد المخزن Store Contract. يوفر إطار عمل Svelte دوالًا لإنشاء مخازن قابلة للقراءة والكتابة ومشتقة في وحدة svelte/store، كما يوفر طريقةً بسيطةً لدمج المخازن في نظام التفاعل باستخدام الصيغة التفاعلية ‎$store، فإذا أنشأتَ مخازنك الخاصة مع التقيّد بعَقد المخزن، فستحصل على هذه الصيغة المُختصَرة التفاعلية مجانًا. إنشاء المكون Alert سننشئ المكوِّن Alert لتوضيح كيفية العمل مع المخازن، ويمكن أن تُعرَف هذه الأنواع من عناصر واجهة المستخدِم باسم الإشعارات المنبثقة أو الرسائل المؤقتة toast أو الإشعارات الفقاعية Notification Bubbles. سيعرض المكوِّنُ App المكوِّنَ Alert، ولكن يمكن لأيّ مكوِّن إرسال إشعارات إليه، حيث سيكون المكوِّن Alert مسؤولًا عن عرض الإشعار على الشاشة عند وصوله. إنشاء مخزن لنبدأ بإنشاء مخزن قابل للكتابة، إذ سيتمكن أيّ مكوِّن من الكتابة في هذا المخزن وسيشترك المكوِّن Alert فيه ويعرض رسالةً كلما جرى تعديل المخزن. أولًا، أنشئ ملفًا جديدًا بالاسم stores.js ضمن المجلد src. ثانيًا، أضِف إليه المحتوى التالي: import { writable } from 'svelte/store' export const alert = writable('Welcome to the to-do list app!') ملاحظة: يمكن تعريف المخازن واستخدامها خارج مكونات Svelte بحيث يمكنك تنظيمها بأيّ طريقة تريدها. استوردنا في الشيفرة السابقة الدالة writable()‎ من الوحدة svelte/store واستخدمناها لإنشاء مخزن جديد يسمى alert مع قيمة أولية هي Welcome to the to-do list app!‎ ثم صدّرنا هذا المخزن. إنشاء المكون الفعلي لننشئ المكوِّن Alert ونرى كيف يمكننا قراءة القيم من المخزن. أولًا، أنشئ ملفًا جديدًا آخر بالاسم src/components/Alert.svelte. ثانيًا، ضع فيه المحتوى التالي: <script> import { alert } from '../stores.js' import { onDestroy } from 'svelte' let alertContent = '' const unsubscribe = alert.subscribe(value => alertContent = value) onDestroy(unsubscribe) </script> {#if alertContent} <div on:click={() => alertContent = ''}> <p>{ alertContent }</p> </div> {/if} <style> div { position: fixed; cursor: pointer; margin-right: 1.5rem; margin-left: 1.5rem; margin-top: 1rem; right: 0; display: flex; align-items: center; border-radius: 0.2rem; background-color: #565656; color: #fff; font-size: 0.875rem; font-weight: 700; padding: 0.5rem 1.4rem; font-size: 1.5rem; z-index: 100; opacity: 95%; } div p { color: #fff; } div svg { height: 1.6rem; fill: currentColor; width: 1.4rem; margin-right: 0.5rem; } </style> لنتعرّف على تفاصيل الشيفرة البرمجية السابقة: نستورد أولًا المخزن alert. ثم نستورد دالة دورة الحياة onDestroy()‎ التي تتيح تنفيذ دالة رد نداء بعد إلغاء تثبيت المكوِّن. ثم ننشئ متغيرًا محليًا بالاسم alertContent، وتذكّر أنه يمكننا الوصول إلى متغيرات المستوى الأعلى من شيفرة HTML، وكلما عُدِّلت، سيُحدَّث نموذج DOM وفقًا لذلك. ثم نستدعي التابع alert.subscribe()‎ ونمرِّر له دالة رد نداء بوصفها معاملًا، وكلما تغيرت قيمة المخزن، ستُستدعَى دالة رد النداء مع القيمة الجديدة بوصفها معاملًا لها، كما نسند القيمة التي نتلقاها إلى متغير محلي في دالة رد النداء، مما يؤدي إلى تحديث نموذج DOM الخاص بالمكوِّن. يعيد التابع subscribe()‎ دالة التنظيف التي تتولى تحرير الاشتراك، وبذلك نشترك عند تهيئة المكوِّن ونستخدِم الدالة onDestroy لإلغاء الاشتراك عندما يكون المكوِّن غير مثبَّت. نستخدِم أخيرًا المتغير alertContent في شيفرة HTML، فإذا نقر المستخدِم على التنبيه، فسننظفه. نضمّن في النهاية عددًا من سطور CSS لتنسيق المكوِّن Alert. يتيح هذا الإعداد العمل مع المخازن بطريقة تفاعلية، فإذا تغيرت قيمة المخزن، فستُنفَّذ دالة رد النداء، ونسند قيمةً جديدةً لمتغير محلي، مما يؤدي إلى تحديث شيفرة HTML وجميع الاعتماديات التفاعلية وفقًا لذلك بفضل خاصية التفاعل في إطار عمل Svelte. استخدام المكون لنستخدِم الآن مكوننا الخاص. أولًا، سنستورد المكوِّن في الملف App.svelte، لذا أضِف تعليمة الاستيراد التالية بعد تعليمات الاستيراد الأخرى الموجودة مسبقًا: import Alert from './components/Alert.svelte' ثم استدعِ المكوِّن Alert قبل استدعاء المكوِّن Todos مباشرةً كما يلي: <Alert /> <Todos {todos} /> حمّل تطبيقك التجريبي الآن، وسترى الآن رسالة تنبيه Alert على الشاشة، إذ يمكنك النقر عليها لإبعادها. جعل المخازن تفاعلية باستخدام الصيغة التفاعلية ‎$store يمكن أن ينجح ذلك، ولكن يجب عليك نسخ الشيفرة التالية ولصقها في كل مرة تريد فيها الاشتراك في المخزن: <script> import myStore from "./stores.js"; import { onDestroy } from "svelte"; let myStoreContent = ""; const unsubscribe = myStore.subscribe((value) => (myStoreContent = value)); onDestroy(unsubscribe); </script> {myStoreContent} تُعَدّ الشيفرة البرمجية السابقة مكررةً بكثرة، وهذا كثير على إطار عمل Svelte، لأنه يمتلك مزيدًا من الموارد لتسهيل الأمور لكونه مصرِّفًا Compiler، كما يوفر إطار عمل Svelte الصيغة التفاعلية ‎$store المعروفة باسم الاشتراك التلقائي، إذ ما عليك سوى إضافة العلامة $ إلى المخزن، وسينشئ إطار Svelte الشيفرة البرمجية اللازمة لجعله تفاعليًا تلقائيًا، لذلك يمكن استبدال كتلة الشيفرة البرمجية السابقة بما يلي: <script> import myStore from "./stores.js"; </script> {$myStore} سيكون المخزن ‎$myStore تفاعليًا بصورة كاملة، وينطبق ذلك على مخازنك المخصَّصة، فإذا طبَّقتَ التابعَين subscribe()‎ و set()‎ كما سنفعل لاحقًا، فستُطبَّق الصيغة التفاعلية ‎$store على مخازك أيضًا. لنطبّق ذلك على المكوِّن Alert، لذا عدّل القسمين <script> وشيفرة HTML في الملف Alert.svelte كما يلي: <script> import { alert } from '../stores.js' </script> {#if $alert} <div on:click={() => $alert = ''}> <p>{ $alert }</p> </div> {/if} تحقق من تطبيقك مرةً أخرى، وسترى أنه يعمل بصورة أفضل. أنشأ إطار عمل Svelte في الخلفية شيفرةً برمجيةً للتصريح عن المتغير المحلي ‎$alert وللاشتراك في المخزن alert ولتحديث ‎$alert كلما جرى تعديل محتوى المخزن ولإلغاء الاشتراك عند إلغاء تثبيت المكوِّن، كما سينشِئ التابع alert.set()‎ كلما أسندنا قيمة إلى ‎$alert. النتيجة النهائية لذلك هي أنه يمكنك الوصول إلى المخازن العامة بالسهولة نفسها لاستخدام المتغيرات المحلية التفاعلية، ويُعَدّ ذلك مثالًا ممتازًا لكيفية وضع إطار Svelte المصرِّف مسؤولًا عن بيئة عمل أفضل للمطورين، ليس فقط من خلال إنقاذنا من كتابة شيفرة برمجية مكررة، وإنما من خلال إنشاء شيفرة برمجية أقل عرضةً للأخطاء أيضًا. الكتابة في المخزن الكتابة في المخزن هي مجرد مسألة استيراد هذا المخزن وتنفيذ ‎$store = 'new value'‎، لذا لنستخدِم ذلك في المكوِّن Todos. أولًا، أضِف تعليمة الاستيراد التالية بعد تعليمات الاستيراد الأخرى الموجودة مسبقًا: import { alert } from '../stores.js' عدّل الدالة addTodo()‎ كما يلي: function addTodo(name) { todos = [...todos, { id: newTodoId, name, completed: false }] $alert = `Todo '${name}' has been added` } عدّل الدالة removeTodo()‎ كما يلي: function removeTodo(todo) { todos = todos.filter((t) => t.id !== todo.id) todosStatus.focus() // انقل التركيز إلى عنوان الحالة $alert = `Todo '${todo.name}' has been deleted` } عدّل الدالة updateTodo()‎ كما يلي: function updateTodo(todo) { const i = todos.findIndex((t) => t.id === todo.id) if (todos[i].name !== todo.name) $alert = `todo '${todos[i].name}' has been renamed to '${todo.name}'` if (todos[i].completed !== todo.completed) $alert = `todo '${todos[i].name}' marked as ${todo.completed ? 'completed' : 'active'}` todos[i] = { ...todos[i], ...todo } } أضِف الكتلة التفاعلية التالية بعد الكتلة التي تبدأ بالتعليمة let filter = 'all'‎: $: { if (filter === 'all') { $alert = 'Browsing all to-dos'; } else if (filter === 'active') { $alert = 'Browsing active to-dos'; } else if (filter === 'completed') { $alert = 'Browsing completed to-dos'; } } أخيرًا، عدّل الكتلتين const checkAllTodos و const removeCompletedTodos كما يلي: const checkAllTodos = (completed) => { todos = todos.map(t => ({...t, completed})) $alert = `${completed ? 'Checked' : 'Unchecked'} ${todos.length} to-dos` } const removeCompletedTodos = () => { $alert = `Removed ${todos.filter((t) => t.completed).length} to-dos` todos = todos.filter((t) => !t.completed) } لذلك استوردنا المخزن وحدّثناه في كل حدث، مما يتسبب في ظهور تنبيه جديد في كل مرة، وألقِ نظرةً على تطبيقك مرةً أخرى، وحاول إضافة أو حذف أو تحديث بعض المهام. سيشغّل إطار عمل Svelte التابع alert.set()‎ كلما نفّذنا ‎$alert = ...‎، إذ سيُرسَل إشعار إلى المكوِّن Alert -مثل أيّ مشترك آخر في مخزن التنبيهات alert- عندما يتلقى المخزن قيمةً جديدةً، وسيُحدَّث توصيفه بفضل خاصية التفاعل إطار عمل Svelte، كما يمكننا تطبيق الشيء نفسه ضمن أيّ مكوِّن أو ملف ‎.js. ملاحظة: لا يمكنك استخدام الصيغة ‎$store خارج مكونات Svelte لأن مصرِّف Svelte لن يستطيع الوصول إلى أيّ شيء خارج مكونات Svelte، لذا يجب عليك الاعتماد على التابعَين store.subscribe()‎ و store.set()‎. تحسين المكون Alert يُعَدّ اضطرارك للنقر على التنبيه للتخلص منه أمرًا مزعجًا بعض الشيء، لذا يُفضَّل أن يختفي الإشعار بعد بضع ثوان، إذ سنحدد خاصيةً مقدَّرةً بالميلي ثانية للانتظار قبل مسح الإشعار، وسنحدّد مهلةً لإزالة التنبيه، كما سنهتم بمسح المهلة عند إلغاء تثبيت المكوِّن Alert لمنع تسرّب الذاكرة. أولًا، عدّل القسم <script> الخاص بالمكوِّن Alert.svelte كما يلي: <script> import { onDestroy } from 'svelte' import { alert } from '../stores.js' export let ms = 3000 let visible let timeout const onMessageChange = (message, ms) => { clearTimeout(timeout) if (!message) { // إخفاء التنبيه إذا كانت الرسالة فارغة visible = false } else { visible = true // إظهار التنبيه if (ms > 0) timeout = setTimeout(() => visible = false, ms) // ‫وإخفائه بعد ms ميلي ثانية } } $: onMessageChange($alert, ms) // شغّل الدالة‫ onMessageChange كلما تغير مخزن alert أو خاصيات ms onDestroy(()=> clearTimeout(timeout)) // تأكد من تنظيف المهلة الزمنية </script> وعدّل قسم شيفرة HTML الخاصة بالمكوِّن Alert.svelte كما يلي: {#if visible} <div on:click={() => visible = false}> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M12.432 0c1.34 0 2.01.912 2.01 1.957 0 1.305-1.164 2.512-2.679 2.512-1.269 0-2.009-.75-1.974-1.99C9.789 1.436 10.67 0 12.432 0zM8.309 20c-1.058 0-1.833-.652-1.093-3.524l1.214-5.092c.211-.814.246-1.141 0-1.141-.317 0-1.689.562-2.502 1.117l-.528-.88c2.572-2.186 5.531-3.467 6.801-3.467 1.057 0 1.233 1.273.705 3.23l-1.391 5.352c-.246.945-.141 1.271.106 1.271.317 0 1.357-.392 2.379-1.207l.6.814C12.098 19.02 9.365 20 8.309 20z"/></svg> <p>{ $alert }</p> </div> {/if} ننشئ هنا أولًا الخاصية ms بقيمة افتراضية 3000 (ميلي ثانية)، ثم ننشئ الدالة onMessageChange()‎ التي ستهتم بالتحكم في ما إذا كان التنبيه مرئيًا أم لا، إذ نطلب من إطار عمل Svelte تشغيل هذه الدالة باستخدام ‎$: onMessageChange($alert, ms)‎ عند تغيير المخزن ‎$alert أو الخاصية ms. كما سننظّف أيّ مهلة زمنية مُعلَّقة عندما يتغير المخزن ‎$alert، فإذا كان المخزن ‎$alert فارغًا، فسنضبط الخاصية visible على القيمة false وسيُزال التنبيه Alert من نموذج DOM؛ أما إذا لم يكن فارغًا، فسنضبط الخاصية visible على القيمة true وسنستخدِم الدالة setTimeout()‎ لمسح التنبيه بعد مدة مقدارها ms ميلي ثانية. أخيرًا، نتأكد من استدعاء الدالة clearTimeout()‎ باستخدام دالة دورة الحياة onDestroy()‎، كما أضفنا رمز SVG أعلى قسم التنبيه ليبدو أجمل. جرب التطبيق مرةً أخرى لترى كافة التغييرات. جعل المكون Alert قابلا للوصول إليه يعمل المكوِّن Alert بصورة جيدة، ولكنه ليس مناسبًا جدًا للتقنيات المساعدة، إذ تكمن المشكلة في العناصر التي تُضاف وتُزال من الصفحة ديناميكيًا، فيمكن ألّا تكون هذه العناصر واضحة جدًا لمستخدِمي التقنيات المساعدة مثل قارئات الشاشة، بالرغم من كونها واضحة من الناحية المرئية للمستخدِمين الذين يمكنهم رؤية الصفحة، كما يمكننا الاستفادة من خاصيات مناطق ARIA الحية التي توفر طريقةً لعرض تغييرات المحتوى الديناميكي برمجيًا لمعالجة هذه المواقف، بحيث يمكن أن تصل إليها التقنيات المساعِدة وتعلن عنها. يمكننا التصريح عن منطقة تحتوي على محتوى ديناميكي ويُعلَن عنها من خلال التقنيات المساعدة باستخدام السمة aria-live متبوعة بالإعداد المؤدّب Politeness الذي يُستخدَم لضبط الأولوية التي يجب أن تتعامل بها قارئات الشاشة مع تحديثات تلك المناطق، وتكون الإعدادات المُحتمَلة إما off أو polite أو assertive، كما لديك أيضًا العديد من قيم السمة role المتخصصة والمحدَّدة مسبقًا التي يمكن استخدامها مثل log و status و alert. ستؤدي إضافة السمة role="alert"‎ إلى الحاوية <div> في حالتنا إلى تنفيذ ما يلي: <div role="alert" on:click={() => visible = false}> يُعَدّ اختبار تطبيقاتك باستخدام قارئات الشاشة فكرةً جيدةً لاكتشاف مشاكل الشمولية وسهولة الوصول وللتعود على كيفية استخدام الأشخاص ذوي المشاكل البصرية للويب، كما لديك العديد من الخيارات مثل استخدام قارئ الشاشة NVDA لنظام التشغيل ويندوز وChromeVox للمتصفح كروم، وOrca على نظام لينكس، وVoiceOver لنظام التشغيل Mac OS X وiOS من بين خيارات أخرى. استخدام مخزن لحفظ المهام يتيح تطبيقنا الصغير إدارة مهامنا بسهولة تامة، ولكنه عديم الفائدة إذا حصلنا دائمًا على قائمة مهام ثابتة hardcoded نفسها عند إعادة تحميلها، لذا يجب معرفة كيفية استمرار مهامنا لجعلها مفيدة. يجب أولًا إيجاد طريقة ما لكي يعيد المكوِّن Todos المهام المُحدَّثة إلى المكوِّن الأب، إذ يمكننا إصدار حدث محدَّث مع قائمة المهام، ولكن يُعَدّ ربط المتغير todos أسهل، لذا لنفتح الملف App.svelte ونجرب ذلك. أضف أولًا السطر التالي بعد مصفوفة todos: $: console.log('todos', todos) عدّل بعد ذلك استدعاء المكون Todos كما يلي: <Todos bind:todos /> ملاحظة: <Todos bind:todos /‎> هو مجرد اختصار للتعليمة <Todos bind:todos={todos} /‎>. ارجع إلى تطبيقك وحاول إضافة بعض المهام، ثم انتقل إلى طرفية الويب الخاصة بأدوات المطور، حيث ستلاحظ أنّ كل تعديل نجريه على مهامنا ينعكس على المصفوفة todos المُعرَّفة في الملف App.svelte بفضل الموجّه bind. يجب الآن إيجاد طريقة لحفظ هذه المهام، إذ يمكننا تطبيق شيفرة برمجية في المكوِّن App.svelte لقراءة مهامنا وحفظها في تخزين الويب أو خدمة الويب، لكن يمكن أن يكون من الأفضل أن نطور مخزنًا عامًا يسمح بحفظ محتواه، إذ يمكن استخدامه مثل أيّ مخزن آخر تمامًا وتجريد آلية استمرار، كما يمكننا إنشاء مخزن يزامن محتواه مع تخزين الويب، ثم تطوير مخزن آخر لاحقًا يتزامن مع خدمة الويب، وسيكون التبديل بينهما أمرًا بسيطًا ولن نضطر إلى تعديل المكوِّن App.svelte على الإطلاق. حفظ المهام لنبدأ باستخدام مخزن عادي قابل للكتابة لحفظ مهامنا. افتح الملف stores.js وأضِف المخزن التالي بعد المخزن الموجود مسبقًا: export const todos = writable([]) يجب الآن استيراد المخزن واستخدامه في الملف Alert.svelte، وتذكّر أنه يجب استخدام صيغة المخزن ‎$todos التفاعلية للوصول إلى المهام الآن، لذا عدّل الملف Alert.svelte كما يلي: <script> import Todos from "./components/Todos.svelte"; import Alert from "./components/Alert.svelte"; import { todos } from "./stores.js"; $todos = [ { id: 1, name: "Create a Svelte starter app", completed: true }, { id: 2, name: "Create your first component", completed: true }, { id: 3, name: "Complete the rest of the tutorial", completed: false } ]; </script> <Alert /> <Todos bind:todos={$todos} /> جرب تطبيقك الآن، إذ يجب أن يعمل كل شيء بصورة جيدة، وسنرى بعد ذلك كيفية تعريف مخازننا المُخصَّصة. كيفية إنشاء مخزن يمكنك إنشاء مخازنك الخاصة دون الاعتماد على الوحدة svelte/store من خلال تنفيذ مخزن تعمل ميزاته على النحو التالي: يجب أن يحتوي المخزن على التابع subscribe()‎ الذي يجب أن يقبل دالة اشتراك بوصفها وسيطًا له، ويجب استدعاء جميع دوال الاشتراك النشطة في المخزن عندما تتغير قيمة المخزن. يجب أن تعيد الدالةُ subscribe()‎ الدالةَ unsubscribe()‎ التي يجب أن توقف الاشتراك عند استدعائها. يمكن أن يحتوي المخزن اختياريًا على التابع set()‎ الذي يجب أن يقبل قيمة المخزن الجديدة على أساس وسيط له، والذي يستدعي بطريقة متزامنة جميع دوال الاشتراك النشطة في المخزن، كما يُطلَق على المخزن الذي يحتوي على التابع set()‎ اسم مخزن قابل للكتابة. أولًا، أضِف تعليمات console.log()‎ التالية إلى المكوِّن App.svelte لرؤية مخزن todos ومحتواه أثناء العمل، لذا أضِف الأسطر التالية بعد المصفوفة todos: console.log('todos store - todos:', todos) console.log('todos store content - $todos:', $todos) سترى شيئًا يشبه ما يلي في طرفية الويب عند تشغيل التطبيق: يُعَدّ مخزننا مجرد كائن يحتوي على التوابع subscribe()‎ و set()‎ و update()‎، وتُعَدّ ‎$todos مصفوفة المهام. إليك مخزن أساسي مُطبَّق من الصفر: export const writable = (initial_value = 0) => { let value = initial_value // محتوى المخزن let subs = [] // معالجات المشتركين const subscribe = (handler) => { subs = [...subs, handler] // أضِف معالجًا إلى مصفوفة المشتركين handler(value) // استدعِ المعالج باستخدام القيمة الحالية return () => subs = subs.filter(sub => sub !== handler) // إعادة دالة إلغاء الاشتراك } const set = (new_value) => { if (value === new_value) return // إذا كانت القيمة نفسها، فاخرج value = new_value // حدّث القيمة subs.forEach(sub => sub(value)) // حدّث المشتركين } const update = (update_fn) => set(update_fn(value)) // حدّث الدالة return { subscribe, set, update } // عَقد المخزن } نصرّح في الشيفرة السابقة عن subs والتي هي مصفوفة من المشتركين، كما نضيف في التابع subscribe()‎ المعالج إلى المصفوفة subs ونعيد دالةً ستزيل المعالج من المصفوفة عند تنفيذها، كما نحدّث قيمة المخزن ونستدعي كل معالج عند استدعاء التابع set()‎ من خلال تمرير القيمة الجديدة بوصفها معاملًا. لا نطبّق المخازن عادةً من الصفر، وإنما يمكنك استخدام المخزن القابل للكتابة لإنشاء متاجر مخصَّصة باستخدام شيفرة برمجية محدَّدة النطاق، وسننشئ في المثال التالي مخزنًا لعدّاد يسمح بإضافة واحد إلى العدّاد أو إعادة ضبط قيمته فقط: import { writable } from 'svelte/store'; function myStore() { const { subscribe, set, update } = writable(0); return { subscribe, addOne: () => update(n => n + 1), reset: () => set(0) }; } إذا أصبح تطبيق قائمة المهام معقدًا للغاية، فيمكننا السماح لمخزن المهام بمعالجة كل تعديل للحالة، إذ يمكننا نقل جميع التوابع التي تعدل مصفوفة todos مثل التابعَين addTodo()‎ و removeTodo()‎ وغير ذلك من المكوِّن Todo إلى المخزن، فإذا كان لديك مكان مركزي لتطبيق جميع تعديلات الحالة، فيمكن للمكونات استدعاء هذه التوابع فقط لتعديل حالة التطبيق وعرض المعلومات التي يسمح المخزن بالوصول إليها بصورة تفاعلية، إذ يسهّل وجود مكان فريد لمعالجة تعديلات الحالة التفكيرَ بشأن مشكلات تدفق الحالة وتحديدها. لن يجبرك إطار عمل Svelte على تنظيم إدارة حالتك بطريقة معينة، وإنما يوفِّر الأدوات لاختيار كيفية معالجتها. تنفيذ مخزننا المخصص للمهام لا يُعَدّ تطبيق قائمة المهام معقدًا، لذلك لن ننقل جميع توابع التعديل إلى مكان مركزي، وإنما سنتركها كما هي، وسنركز على استمرار مهامنا بدلًا من ذلك. ملاحظة: إذا أردت تتبّع هذا المقال باستخدام الأداة Svelte REPL، فلن تتمكن من إكمال هذه الخطوة، إذ تعمل Svelte REPL في بيئة وضع الحماية التي لن تسمح لك بالوصول إلى تخزين الويب، وستحصل على خطأ "العملية غير آمنة The operation is insecure"، وعلى هذا الأساس لا بد من استنساخ المستودع والانتقال إلى المجلد الآتي: mdn-svelte-tutorial/06-stores أو يمكنك تنزيل محتوى المجلد مباشرةً باستخدام الأمر الآتي: npx degit opensas/mdn-svelte-tutorial/06-stores يمكنك تطبيق مخزن مخصص يحفظ محتواه في تخزين الويب من خلال استخدام مخزن قابل للكتابة يطبّق ما يلي: يقرأ القيمة من تخزين الويب في البداية ويهيئها بقيمة افتراضية إذا لم تكن موجودةً. يحدّث المخزن نفسه والبيانات الموجودة في التخزين المحلي عند تعديل القيمة. يدعم تخزين الويب حفظ قيم السلاسل النصية فقط، لذا يجب تحويلها من كائن إلى سلسلة نصية عند الحفظ، والعكس صحيح عند تحميل القيمة من التخزين المحلي. أولًا، أنشئ ملفًا جديدًا بالاسم localStore.js في المجلد src. ثانيًا، ضع فيه المحتوى التالي: import { writable } from 'svelte/store'; export const localStore = (key, initial) => { // يتلقى مفتاح التخزين المحلي وقيمة أولية const toString = (value) => JSON.stringify(value, null, 2) // دالة مساعدة const toObj = JSON.parse // دالة مساعدة if (localStorage.getItem(key) === null) { // العنصر غير موجود في التخزين المحلي localStorage.setItem(key, toString(initial)) // تهيئة التخزين المحلي بالقيمة الأولية } const saved = toObj(localStorage.getItem(key)) // تحويل إلى كائن const { subscribe, set, update } = writable(saved) // إنشاء المتجر الأساسي القابل للكتابة return { subscribe, set: (value) => { localStorage.setItem(key, toString(value)) // حفظ القيمة في التخزين المحلي كسلسلة نصية return set(value) }, update } } لنشرح الشيفرة البرمجية السابقة: ستكونlocalStore دالةً تقرأ محتواها من تخزين الويب أولًا وتعيد كائنًا مع ثلاث توابع هي subscribe()‎ و set()‎ و update()‎ عند تنفيذها. يجب تحديد مفتاح تخزين الويب وقيمة أولية عند إنشاء دالة localStore جديدة، ثم نتحقق مما إذا كانت القيمة موجودة في تخزين الويب، وننشئها إذا لم يكن الأمر كذلك. نستخدِم التابعَين localStorage.getItem(key)‎ و localStorage.setItem(key, value)‎ لقراءة المعلومات وكتابتها في تخزين الويب، كما نستخدِم الدالتين المساعدتين toString()‎ و toObj()‎ (التي تستخدم التابع JSON.parse()‎) لتحويل القيم. نحوّل بعد ذلك المحتوى المُستلمَ من تخزين الويب من سلسلة نصية إلى كائن، ونحفظ هذا الكائن في مخزننا. أخيرًا، نحدّث تخزين الويب مع تحويل القيمة إلى سلسلة نصية في كل مرة نحدّث فيها محتويات المخزن. لاحظ أنه كان علينا فقط إعادة تعريف التابع set()‎ من خلال إضافة العملية لحفظ القيمة في تخزين الويب، وما تبقى من الشيفرة البرمجية في أغلبه هو عبارة عن تهيئة وتحويل. سنستخدِم الآن المخزن المحلي في stores.js لإنشاء مخزن المهام المستمر محليًا، لذا عدّل الملف stores.js كما يلي: import { writable } from 'svelte/store' import { localStore } from './localStore.js' export const alert = writable('Welcome to the to-do list app!') const initialTodos = [ { id: 1, name: 'Visit MDN web docs', completed: true }, { id: 2, name: 'Complete the Svelte Tutorial', completed: false }, ] export const todos = localStore('mdn-svelte-todo', initialTodos) هيّأنا المخزن لحفظ البيانات في تخزين الويب ليكون تابعًا للمفتاح mdn-svelte-todo باستخدام الدالة الآتية: localStore('mdn-svelte-todo', initialTodos)‎ كما ضبطنا بعض المهام لتكون قيمًا أوليةً. لنتخلص الآن من المهام الثابتة في المكوِّن App.svelte، لذا حدّث محتوياته كما يلي، حيث سنحذف فقط المصفوفة ‎$todos وتعليمات console.log()‎: <script> import Todos from './components/Todos.svelte' import Alert from './components/Alert.svelte' import { todos } from './stores.js' </script> <Alert /> <Todos bind:todos={$todos} /> ملاحظة: يُعَدّ ذلك التغيير الوحيد الذي يجب إجراؤه لاستخدام مخزننا المُخصَّص، فالمكون App.svelte واضح تمامًا من حيث نوع المخزن الذي نستخدِمه. جرّب تطبيقك مرةً أخرى، وأنشئ بعض المهام ثم أغلق المتصفح، كما يمكنك إيقاف خادم Svelte وإعادة تشغيله، حيث ستظل مهامك موجودة عند إعادة زيارة عنوان URL. كما يمكنك فحص تطبيقك في طرفية أدوات التطوير DevTools من خلال إدخال الأمر localStorage.getItem('mdn-svelte-todo')‎، لذا طبّق بعض التغييرات على تطبيقك مثل الضغط على زر "إلغاء تحديد الكل Uncheck All" وتحقق من محتوى تخزين الويب مرةً أخرى، وستحصل على شيء يشبه ما يلي: توفِّر مخازن Svelte طريقةً بسيطةً جدًا وخفيفة الوزن ولكنها قوية للغاية للتعامل مع حالة التطبيق المعقدة من مخزن بيانات عام بطريقة تفاعلية، ويمكن أن يوفِّر إطار عمل Svelte صيغة الاشتراك التلقائي ‎$store التي تسمح لنا بالعمل مع المخازن باستخدام الطريقة نفسها للتعامل مع المتغيرات المحلية لأن إطار عمل Svelte يصرِّف الشيفرة، كما تمتلك المخازن الحد الأدنى من واجهة برمجة التطبيقات، مما يؤدي إلى سهولة إنشاء مخازننا المُخصَّصة لتجريد عمل المخزن الداخلي. الانتقالات لنضِف الآن حركةً إلى التنبيهات، إذ يوفر إطار عمل Svelte وحدةً كاملةً لتعريف الانتقالات transitions والحركات animations لنتمكن من جعل واجهات المستخدِم أكثر جاذبيةً. يمكن تطبيق الانتقالات باستخدام الموجّه transition:fn الذي يعمل عند دخول عنصر إلى نموذج DOM أو مغادرته بوصفه نتيجةً لتغيير الحالة، إذ تصدّر الوحدة svelte/transition سبع دوال هي fade و blur و fly و slide و scale و draw و crossfade. لنعطِ المكوِّن Alert انتقالًا transition من النوع fly، لذا افتح الملف Alert.svelte واستورد الدالة fly من الوحدة svelte/transition. أولًا، ضع تعليمة الاستيراد التالية بعد تعليمات الاستيراد الموجودة مسبقًا: import { fly } from 'svelte/transition' ثانيًا، عدّل وسم الفتح <div> كما يلي لاستخدام هذا الانتقال: <div role="alert" on:click={() => visible = false} transition:fly > يمكن أن تأخذ الانتقالات معامِلات كما يلي: <div role="alert" on:click={() => visible = false} transition:fly="{{delay: 250, duration: 300, x: 0, y: -100, opacity: 0.5}}" > ملاحظة: لا تُعَدّ الأقواس المزدوجة المعقوصة صيغةً خاصةً بإطار عمل Svelte، وإنما هي مجرد كائن جافاسكربت حرفي يُمرَّر بوصفه معامِلًا للانتقال fly. جرب تطبيقك مرةً أخرى، وسترى أنّ الإشعارات الآن أكثر جاذبيةً. ملاحظة: يسمح إطار عمل Svelte بتحسين حجم الحزمة من خلال استبعاد الميزات غير المستخدَمة لكونه مصرِّفًا، فإذا صرّفنا تطبيقنا للإنتاج باستخدام الأمر npm run build، فسيكون وزن الملف public/build/bundle.js أقل بقليل من 22 كيلوبايت، وإذا أزلنا الموجِّه transitions:fly، فإنّ إطار عمل Svelte ذكي بما يكفي لإدراك عدم استخدام الدالة fly، وسينخفض حجم الملف bundle.js إلى 18 كيلوبايت فقط. ما هذا سوى غيض من فيض، إذ يمتلك إطار عمل Svelte الكثير من الخيارات للتعامل مع الحركات والانتقالات، كما يدعم تحديد انتقالات مختلفة لتطبيقها عند إضافة العنصر أو إزالته من نموذج DOM باستخدام الموجِّه in:fn أو out:fn، ويتيح تعريف انتقالات CSS وجافاسكربت المُخصَّصة، كما لديه العديد من دوال تحسين الحركة Easing لتحديد معدل التغيير بمرور الوقت، ويمكنك إلقاء نظرة على أداة تحسين الحركة البصرية ease visualizer لاستكشاف دوال تحسين الحركة المتاحة المختلفة. يمكنك الوصول إلى نسختك من مستودعنا على النحو التالي لمعرفة حالة الشيفرة كما يجب أن تكون في نهاية هذا المقال: cd mdn-svelte-tutorial/07-next-steps أو يمكنك تنزيل محتوى المجلد مباشرةً باستخدام الأمر التالي: npx degit opensas/mdn-svelte-tutorial/07-next-steps تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، فإذا أردت متابعتنا، فابدأ بكتابة الشيفرة باستخدام الأداة REPL من هنا. الخلاصة أضفنا في هذا المقال ميزتين جديدتين هما المكوِّن Alert واستمرار المهام todos في تخزين الويب. سمح لنا ذلك بعرض بعض تقنيات إطار عمل Svelte المتقدمة، كما طوّرنا المكوِّن Alert لإظهار كيفية تطبيق إدارة الحالة عبر المكونات باستخدام المخازن Stores، ورأينا كيفية الاشتراك التلقائي في المخازن لدمجها بسلاسة مع نظام Svelte التفاعلي. رأينا بعد ذلك كيفية تطبيق مخزننا الخاص من الصفر وكيفية توسيع مخزن Svelte القابل للكتابة لاستمرار البيانات في تخزين الويب. ألقينا في النهاية نظرةً على استخدام الموجِّه transition في إطار عمل Svelte لتطبيق الحركات على عناصر DOM. سنتعرّف في مقال قادم على كيفية إضافة دعم لغة TypeScript إلى تطبيق Svelte، كما سننقل تطبيقنا بالكامل إلى TypeScript للاستفادة من جميع ميزاتها. ترجمة -وبتصرُّف- للمقال Working with Svelte stores. اقرأ أيضًا التفاعلية ودورة الحياة وسهولة وصول المستخدمين في إطار عمل Svelte بدء استخدام إطار العمل Svelte لبناء تطبيقات ويب إنشاء تطبيق قائمة مهام باستعمال إطار عمل Svelte التعامل مع المتغيرات والخاصيات في إطار عمل Svelte تقسيم تطبيق Svelte إلى مكونات
  19. أضفنا في مقال تقسيم تطبيق Svelte إلى مكونات من هذه السلسلة مزيدًا من الميزات إلى قائمة المهام وبدأنا بتنظيم تطبيقنا ضمن مكونات، وسنضيف في هذا المقال الميزات النهائية لتطبيقنا مع استكمال تقسيمه إلى مكونات، وسنتعلم كيفية التعامل مع مشاكل التفاعل المتعلقة بتحديث الكائنات والمصفوفات، كما سنتعرّف على حل بعض مشاكل تركيز سهولة الوصول أو الشمولية أي سهولة وصول كل المستخدِمين خصوصًا من يملك بعض الإعاقات وغير ذلك. المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية، وستحتاج طرفية مثبَّت عليها node و npm لتصريف وبناء تطبيقك. الهدف: تعلّم بعض تقنيات Svelte المتقدمة التي تتضمن حل مشاكل التفاعل ومشاكل سهولة الوصول لمستخدِمي لوحة المفاتيح المتعلقة بدورة حياة المكونات وغير ذلك. سنركز على بعض مشاكل سهولة الوصول التي تتضمن إدارة التركيز، إذ سنستخدِم بعض التقنيات للوصول إلى عُقد نموذج DOM وتنفيذ توابع مثل التابعَين focus()‎ و select()‎، كما سنرى كيفية التصريح عن تنظيف مستمعي الأحداث على عناصر DOM، كما سنتعلّم بعض الأمور عن دورة حياة المكونات لفهم متى تُثبَّت عُقد DOM ومتى تُفصَل من نموذج DOM وكيف يمكننا الوصول إليها، كما سنتعرف على الموجه action الذي سيسمح بتوسيع وظائف عناصر HTML بطريقة قابلة لإعادة الاستخدام والتصريح. أخيرًا، سنتعلم المزيد عن المكونات، فقد رأينا سابقًا كيف يمكن للمكونات مشاركة البيانات باستخدام الخاصيات Props والتواصل مع المكونات الآباء باستخدام الأحداث وربط البيانات ثنائي الاتجاه، وسنرى الآن كيف يمكن للمكونات الوصول إلى التوابع والمتغيرات. سنطوّر المكونات الجديدة التالية خلال هذا المقال: MoreActions: يعرض الزرين "تحديد الكل Check All" و"حذف المهام المكتملة Remove Completed" ويصدر الأحداث المقابلة المطلوبة للتعامل مع وظائفهما. NewTodo: يعرض حقل الإدخال <input> وزر "الإضافة Add" لإضافة مهمة جديدة. TodosStatus: عرض عنوان الحالة "x out of y items completed" التي تمثِّل المهام المكتملة. يمكن متابعة كتابة شيفرتك معنا، لذلك انسخ أولًا مستودع github -إذا لم تفعل ذلك مسبقًا- باستخدام الأمر التالي: git clone https://github.com/opensas/mdn-svelte-tutorial.git ثم يمكنك الوصول إلى حالة التطبيق الحالية من خلال تشغيل الأمر التالي: cd mdn-svelte-tutorial/05-advanced-concepts أو يمكنك تنزيل محتوى المجلد مباشرةً كما يلي: npx degit opensas/mdn-svelte-tutorial/05-advanced-concepts تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، فإذا أردت متابعتنا، فابدأ بكتابة الشيفرة باستخدام الأداة REPL من svelte.dev. المكون MoreActions سنعالج الآن الزرين "تحديد الكل Check All" و"حذف المهام المكتملة Remove Completed"، لذا لننشئ مكونًا يكون مسؤولًا عن عرض الأزرار وإصدار الأحداث المقابلة. أولًا، أنشئ ملفًا جديدًا بالاسم components/MoreActions.svelte. ثانيًا، سنرسل الحدث checkAll عند النقر على الزر الأول للإشارة إلى أنه يجب تحديد أو إلغاء تحديد جميع المهام، كما سنرسل الحدث removeCompleted عند النقر على الزر الثاني للإشارة إلى أنه يجب حذف جميع المهام المكتملة، لذا ضَع المحتوى التالي في الملف MoreActions.svelte: <script> import { createEventDispatcher } from 'svelte' const dispatch = createEventDispatcher() let completed = true const checkAll = () => { dispatch('checkAll', completed) completed = !completed } const removeCompleted = () => dispatch('removeCompleted') </script> <div class="btn-group"> <button type="button" class="btn btn__primary" on:click={checkAll}>{completed ? 'Check' : 'Uncheck'} all</button> <button type="button" class="btn btn__primary" on:click={removeCompleted}>Remove completed</button> </div> ضمّنا المتغير completed للتبديل بين تحديد جميع المهام وإلغاء تحديدها. ثالثًا، سنستورد المكوِّن MoreActions مرةً أخرى في Todos.svelte وسننشئ دالتين للتعامل مع الأحداث الصادرة من المكوِّن MoreActions، لذا أضف تعليمة الاستيراد التالية بعد تعليمات الاستيراد الموجودة مسبقًا: import MoreActions from './MoreActions.svelte' رابعًا، أضف بعد ذلك الدوال الموضَّحة في نهاية القسم <script>: const checkAllTodos = (completed) => todos.forEach((t) => t.completed = completed) const removeCompletedTodos = () => todos = todos.filter((t) => !t.completed) خامسًا، انتقل الآن إلى الجزء السفلي من شيفرة HTML الخاصة بـ Todos.svelte واستبدل العنصر <div> الذي له الصنف btn-group والذي نسخناه إلى MoreActions.svelte باستدعاء المكوِّن MoreActions كما يلي: <!-- MoreActions --> <MoreActions on:checkAll={e => checkAllTodos(e.detail)} on:removeCompleted={removeCompletedTodos} /> سادسًا، لنعد إلى التطبيق ونجربه، إذ ستجد أنّ زر "حذف المهام المكتملة Remove Completed" يعمل بصورة جيدة، ولكن يفشل الزر "تحديد الكل Check All" أو "إلغاء تحديد الكل Uncheck All". اكتشاف التفاعل: تحديث الكائنات والمصفوفات يمكننا تسجيل المصفوفة todos من الدالة checkAllTodos()‎ إلى الطرفية لمعرفة ما يحدث. أولًا، عدّل الدالة checkAllTodos()‎ إلى ما يلي: const checkAllTodos = (completed) => { todos.forEach((t) => t.completed = completed); console.log('todos', todos); } ثانيًا، ارجع إلى متصفحك وافتح طرفية أدوات التطوير DevTools وانقر على زر تحديد الكل أو إلغاء تحديد الكل عدة مرات. ستلاحظ أنّ المصفوفة تُحدَّث بنجاح في كل مرة تضغط فيها على الزر، إذ تُبدَّل الخاصيات completed الخاصة بالكائنات todo بين القيمتين true و false، ولكن إطار Svelte ليس على علم بذلك، وهذا يعني أنه لن تكون تعليمة التفاعل مثل التعليمة ‎$: console.log('todos', todos)‎ مفيدةً جدًا في هذه الحالة، لذلك يجب فهم كيفية عمل التفاعل في إطار Svelte عند تحديث المصفوفات والكائنات. تستخدِم العديد من أطر عمل الويب تقنية نموذج DOM الافتراضي لتحديث الصفحة، إذ يُعَدّ DOM الافتراضي نسخةً في الذاكرة لمحتويات صفحة الويب، كما يحدّث إطار العمل هذا التمثيل الافتراضي الذي تجري مزامنته بعد ذلك مع نموذج DOM الحقيقي، وهذا أسرع بكثير من التحديث المباشر لنموذج DOM ويسمح لإطار العمل بتطبيق العديد من تقنيات التحسين، إذ تعيد هذه الأطر تشغيل كل شيفرة جافاسكربت افتراضيًا في كل تغيير لنموذج DOM الافتراضي، وتطبّق توابعًا مختلفةً لتخزين العمليات الحسابية باهظة الثمن مؤقتًا ولتحسين التنفيذ. لا يستخدِم إطار Svelte تمثيل نموذج DOM الافتراضي، وإنما يحلّل الشيفرة وينشئ شجرةً اعتماديةً، ثم ينشئ شيفرة جافاسكربت المطلوبة لتحديث أجزاء نموذج DOM التي تحتاج إلى تحديث فقط، إذ تنشئ هذه التقنية شيفرة جافاسكربت مثالية بأقل قدر من عمليات المعالجة إلى حد ما ولكن لذلك لا يخلو من بعض القيود. يتعذر على إطار Svelte في بعض الأحيان اكتشاف التغييرات التي تطرأ على المتغيرات المُراقَبة، وتذكَّر أنه يمكنك إخبار إطار Svelte بتغيّر متغير ما من خلال إسناد قيمة جديدة إليه، كما يجب أن يظهر اسم المتغير المُحدَّث على الجانب الأيسر من هذا الإسناد كما يلي على سبيل المثال: const foo = obj.foo foo.bar = 'baz' لن يحدِّث إطار عمل Svelte مراجع الكائن obj.foo.bar إلّا إذا تتبّعتها باستخدام الإسناد obj = obj، إذ لا يمكن لإطار عمل Svelte تتبّع مراجع الكائنات، لذلك يجب إخباره صراحةً أنّ الكائن obj تغير باستخدام الإسناد. ملاحظة: إذا كان foo متغيرًا من المستوى الأعلى، فيمكنك بسهولة إخبار إطار Svelte بتحديث الكائن obj عندما يتغير المتغير foo باستخدام تعليمة التفاعل التالية: ‎$: foo, obj = obj، وبالتالي يُعرَّف foo على أنه اعتمادية، وكلما تغير، سيعمل إطار عمل Svelte على تشغيل عملية الإسناد obj = obj. إذا شغلت ما يلي في الدالة checkAllTodos()‎: todos.forEach((t) => t.completed = completed); لن يلحظ إطار Svelte تغيّر المصفوفة todos لأنه لا يعرف أننا نعدّلها عند تحديث المتغير t ضمن التابع forEach()‎، ويُعَدّ ذلك منطقيًا، إذ سيعرف إطار Svelte عمل التابع forEach()‎ الداخلي إذا حدث عكس ذلك، لذا سيُطبَّق الأمر نفسه بالنسبة لأيّ تابع مرتبط بكائن أو مصفوفة، لكن هناك تقنيات مختلفة يمكننا تطبيقها لحل هذه المشكلة، وتتضمن جميعها إسناد قيمة جديدة للمتغير المُراقَب. يمكننا إخبار إطار عمل Svelte بتحديث المتغير باستخدام إسناد ذاتي كما يلي: const checkAllTodos = (completed) => { todos.forEach((t) => t.completed = completed); todos = todos; } تحل هذه الطريقة المشكلة، إذ سيرفع إطار Svelte رايةً تعبِّر عن تغيير المصفوفة todos ويزيل الإسناد الذاتي الذي يراه زائدًا، كما يمكن أن تبدو هذه الطريقة غريبةً، ولكنها تُعَدّ جيدةً ومختصَرةً. يمكننا الوصول أيضًا إلى المصفوفة todos باستخدام الفهرس كما يلي: const checkAllTodos = (completed) => { todos.forEach((t, i) => todos[i].completed = completed); } تعمل الإسنادات إلى خاصيات المصفوفات والكائنات مثل obj.foo += 1 أو array[i] = x بالطريقة نفسها للإسنادات إلى القيم نفسها، فإذا حلّل إطار عمل Svelte هذه الشيفرة، فيمكنه اكتشاف أنّ المصفوفة todos تُعدَّل. يوجد حل آخر هو إسناد مصفوفة جديدة إلى المصفوفة todos، إذ تحتوي هذه المصفوفة الجديدة على نسخة من جميع المهام مع تحديث الخاصية completed وفقًا لذلك كما يلي: const checkAllTodos = (completed) => { todos = todos.map((t) => ({ ...t, completed })); } نستخدِم في هذه الحالة التابع map()‎ الذي يعيد مصفوفةً جديدةً مع نتائج تنفيذ الدالة المتوفرة لكل عنصر، إذ تعيد الدالة نسخةً من كل مهمة باستخدام صيغة الانتشار Spread Syntax وتعيد كتابة خاصية القيمة completed وفقًا لذلك، وتتمثل فائدة هذا الحل في إعادة مصفوفة جديدة مع كائنات جديدة وتجنب تغيير المصفوفة todos الأصلية. ملاحظة: يتيح إطار Svelte تحديد خيارات مختلفة تؤثر على كيفية عمل المصرِّف Compiler، إذ يخبر الخيار <svelte:options immutable={true}/‎> المصرِّف بأنك تتعهد بعدم تغيير أيّ كائنات، مما يتيح له بأن يكون أقل تحفظًا بشأن التحقق من تغيير القيم وإنشاء شيفرة أبسط وأكثر فعالية. تتضمن كل هذه الحلول إسنادًا يكون فيه المتغير المحدَّث في الجانب الأيسر من المساواة، وستسمح جميعها لإطار Svelte بملاحظة تعديل المصفوفة todos، لذا اختر أحد هذه الحلول وحدّث الدالة checkAllTodos()‎ كما هو مطلوب، ويجب الآن أن تكون قادرًا على تحديد جميع مهامك وإلغاء تحديدها دفعةً واحدةً. الانتهاء من المكون MoreActions سنضيف أحد تفاصيل إمكانية الاستخدام إلى مكوننا، إذ سنعطّل الأزرار في حالة عدم وجود مهام لمعالجتها من خلال استخدام المصفوفة todos بوصفها خاصيةً وضبط الخاصية disabled لكل زر وفقًا لذلك. أولًا، عدّل المكوِّن MoreActions.svelte كما يلي: <script> import { createEventDispatcher } from 'svelte' const dispatch = createEventDispatcher() export let todos let completed = true const checkAll = () => { dispatch('checkAll', completed) completed = !completed } const removeCompleted = () => dispatch('removeCompleted') $: completedTodos = todos.filter(t => t.completed).length </script> <div class="btn-group"> <button type="button" class="btn btn__primary" disabled={todos.length === 0} on:click={checkAll}>{completed ? 'Check' : 'Uncheck'} all</button> <button type="button" class="btn btn__primary" disabled={completedTodos === 0} on:click={removeCompleted}>Remove completed</button> </div> صرّحنا عن متغير التفاعل completedTodos لتفعيل أو تعطيل زر "إزالة المهام المكتملة Remove Completed". لا تنسى تمرير الخاصية إلى المكوِّن MoreActions من المكوِّن Todos.svelte حيث يُستدعَى المكوِّن كما يلي: <MoreActions {todos} on:checkAll={(e) => checkAllTodos(e.detail)} on:removeCompleted={removeCompletedTodos} /> التعامل مع نموذج DOM: التركيز على التفاصيل أكملنا جميع الوظائف المطلوبة للتطبيق، وسنركِّز على بعض ميزات سهولة الوصول Accessibility التي ستحسّن إمكانية استخدام تطبيقنا لكل من مستخدمِي لوحة المفاتيح فقط وقارئات الشاشة، كما يواجه تطبيقنا حاليًا مشكلتين متعلقتين بسهولة وصول استخدام لوحة المفاتيح وتتضمن إدارة التركيز، لذا لنلقِ نظرةً على هذه المشاكل. استكشاف مشاكل سهولة الوصول لمستخدمي لوحة المفاتيح في تطبيقنا سيكتشف مستخدِمو لوحة المفاتيح حاليًا أنّ تدفق التركيز في تطبيقنا لا يمكن التنبؤ به أو غير مترابط، فإذا نقرت على حقل الإدخال في الجزء العلوي من تطبيقنا، فسترى حدًّا سميكًا ومتقطعًا حول هذا الحقل، إذ يُعَد هذا الحدّ المؤشر المرئي على أنّ المتصفح يركِّز حاليًا على هذا العنصر. إذا كنت من مستخدمِي الفأرة، فيمكن أن تتخطى هذه الإشارة المرئية، ولكن إذا أردت العمل باستخدام لوحة المفاتيح فقط، فمعرفة عنصر التحكم المُركَّز عليه أمرٌ بالغ الأهمية، إذ يخبرنا هذا التركيز أيّ عنصر تحكم سيتلقى ضغطات المفاتيح، فإذا ضغطت على مفتاح Tab بصورة متكررة، فسترى مؤشر التركيز المتقطع يتنقل بين جميع العناصر القابلة للتركيز على الصفحة، وإذا نقلت التركيز إلى زر "التعديل Edit" وضغطتَ على مفتاح Enter، فسيختفي التركيز فجأة دون إمكانية تحديد عنصر التحكم الذي سيتلقى ضغطات المفاتيح. إذا ضغطت على مفتاح Escape أو Enter، فلن يحدث شيء؛ أما إذا نقرت على زر "الإلغاء Cancel" أو "الحفظ Save"، فسيختفي التركيز مرةً أخرى، كما سيكون هذا السلوك محيرًا بالنسبة لمستخدِم يعمل باستخدام لوحة المفاتيح. كما نود إضافة بعض ميزات إمكانية الاستخدام مثل تعطيل زر "الحفظ Save" عندما تكون الحقول المطلوبة فارغةً، أو التركيز على بعض عناصر HTML أو التحديد التلقائي للمحتويات عند التركيز على حقل إدخال النص، كما يجب الوصول برمجيًا إلى عقد نموذج DOM لتشغيل دوال مثل الدالتين focus()‎ و select()‎ بهدف تطبيق جميع هذه الميزات، ويجب استخدام التابعين addEventListener()‎ و removeEventListener()‎ لتشغيل مهام محددة عندما يتلقى عنصر التحكم التركيز. تكمن المشكلة في أنّ جميع عقد نموذج DOM ينشئها إطار عمل Svelte ديناميكيًا في وقت التشغيل، لذا علينا الانتظار حتى إنشائها وإضافتها إلى نموذج DOM لاستخدامها، إذ يجب التعرف على دورة حياة المكونات لفهم متى يمكننا الوصول إليها. إنشاء المكون NewTodo أنشئ ملف مكوِّن جديد وعدّل الشيفرة لإصدار الحدث addTodo من خلال تمرير اسم المهمة الجديدة مع التفاصيل الإضافية كما يلي: أولًا، أنشئ ملفًا جديدًا بالاسم components/NewTodo.svelte. ضع بعد ذلك المحتويات التالية ضمن هذا الملف: <script> import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); let name = ''; const addTodo = () => { dispatch('addTodo', name); name = ''; } const onCancel = () => name = ''; </script> <form on:submit|preventDefault={addTodo} on:keydown={(e) => e.key === 'Escape' && onCancel()}> <h2 class="label-wrapper"> <label for="todo-0" class="label__lg">What needs to be done?</label> </h2> <input bind:value={name} type="text" id="todo-0" autoComplete="off" class="input input__lg" /> <button type="submit" disabled={!name} class="btn btn__primary btn__lg">Add</button> </form> ربطنا هنا العنصر <input> بالمتغير name باستخدام bind:value={name}‎ وعطّلنا زر "الإضافة Add" عندما يكون حقل الإدخال فارغًا -أي لا يحتوي على محتوى نصي- باستخدام disabled={!name}‎، كما عالجنا استخدام مفتاح Escape باستخدام on:keydown={(e) => e.key === 'Escape' && onCancel()}‎، إذ نشغّل التابع onCancel()‎ الذي يمسح المتغير name في كل مرة نضغط فيها على مفتاح Escape. يجب الآن استيراد import المكوِّن NewTodo واستخدامه ضمن المكوِّن Todos وتحديث الدالة addTodo()‎ للحصول على اسم المهمة الجديد، لذا أضف تعليمة الاستيراد التالية بعد تعليمات الاستيراد الأخرى الموجودة ضمن Todos.svelte: import NewTodo from './NewTodo.svelte' عدّل الدالة addTodo()‎ بعد ذلك كما يلي: function addTodo(name) { todos = [...todos, { id: newTodoId, name, completed: false }] } تتلقى الدالة addTodo()‎ الآن اسم المهمة الجديدة مباشرةً، لذلك لم نَعُد بحاجة المتغير newTodoName لإعطائه قيمة، إذ سيهتم المكوِّن NewTodo بذلك. ملاحظة: تُعَدّ الصيغة { name } اختصارًا للصيغة { name: name }، إذ يأتي هذا الاختصار من لغة جافاسكربت وليس له علاقة بإطار Svelte مع توفير بعض الإلهام للاختصارات الخاصة بإطار Svelte. أخيرًا، استبدل شيفرة HTML الخاصة بنموذج NewTodo باستدعاء المكوِّن NewTodo كما يلي: <!-- NewTodo --> <NewTodo on:addTodo={(e) => addTodo(e.detail)} /> التعامل مع عقد نموذج DOM باستخدام الموجه bind:this={dom_node}‎ نريد الآن أن يعود التركيز إلى العنصر <input> الخاص بالمكون NewTodo في كل مرة يُضغَط فيها على زر "الإضافة Add"، لذا سنحتاج مرجعًا إلى عقدة نموذج DOM الخاصة بحقل الإدخال، إذ يوفر إطار عمل Svelte طريقةً لذلك باستخدام الموجِّه bind:this={dom_node}‎، كما يسند إطار Svelte مرجع عقدة DOM إلى متغير محدد بمجرد تثبيت المكوِّن وإنشاء عقدة DOM. لننشئ المتغير nameEl ونربطه بحقل الإدخال باستخدام bind:this={nameEl}‎، ثم سنستدعي التابع nameEl.focus()‎ ضمن الدالة addTodo()‎ لإعادة التركيز إلى العنصر <input> مرةً أخرى بعد إضافة المهام الجديدة، وسنطبّق الشيء نفسه عندما يضغط المستخدِم على مفتاح Escape باستخدام الدالة onCancel()‎. عدّل محتويات المكوِّن NewTodo.svelte كما يلي: <script> import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); let name = ''; let nameEl; // ‫مرجع إلى عقدة حقل الإدخال name في نموذج DOM const addTodo = () => { dispatch('addTodo', name); name = ''; nameEl.focus(); // ‫ركّز على حقل الإدخال name } const onCancel = () => { name = ''; nameEl.focus(); // ‫ركّز على حقل الإدخال name } </script> <form on:submit|preventDefault={addTodo} on:keydown={(e) => e.key === 'Escape' && onCancel()}> <h2 class="label-wrapper"> <label for="todo-0" class="label__lg">What needs to be done?</label> </h2> <input bind:value={name} bind:this={nameEl} type="text" id="todo-0" autoComplete="off" class="input input__lg" /> <button type="submit" disabled={!name} class="btn btn__primary btn__lg">Add</button> </form> جرِّب التطبيق واكتب اسم مهمة جديدة في حقل الإدخال <input> واضغط على المفتاح tab للتركيز على زر "الإضافة Add"، ثم اضغط على مفتاح Enter أو Escape لترى كيف يستعيد حقل الإدخال التركيز. التركيز التلقائي على حقل الإدخال الميزة التالية التي سنضيفها إلى المكوِّن NewTodo هي الخاصية autofocus التي ستسمح بتحديد أننا نريد التركيز على حقل الإدخال <input> في صفحة التحميل. محاولتنا الأولى هي كما يلي: لنحاول إضافة الخاصية autofocus واستدعاء التابع nameEl.focus()‎ في كتلة القسم <script>، لذا عدِّل الجزء الأول من القسم <script> الخاص بالمكوِّن NewTodo.svelte (الأسطر الأربعة الأولى) لتبدو كما يلي: <script> import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); export let autofocus = false; let name = ''; let nameEl; // مرجع إلى عقدة حقل الإدخال‫ name في نموذج DOM if (autofocus) nameEl.focus(); عُد الآن إلى المكوِّن Todos ومرّر الخاصية autofocus إلى استدعاء المكوِّن <NewTodo> كما يلي: <!-- NewTodo --> <NewTodo autofocus on:addTodo={(e) => addTodo(e.detail)} /> إذا جربت تطبيقك، فسترى أنّ الصفحة فارغة حاليًا، وسترى في طرفية أدوات تطوير الويب خطأً بالشكل: TypeError: nameEl is undefined. دورة حياة المكون والدالة onMount()‎ يشغّل إطار Svelte شيفرة التهيئة -أي قسم <script> الخاص بالمكون- عند إنشاء نسخة من هذا المكون، ولكن تكون جميع العقد التي يتألف منها المكوِّن غير مرتبطة بنموذج DOM في تلك اللحظة، وهي في الحقيقة غير موجودة أصلًا، كما يمكن أن تتساءل عن كيفية معرفة وقت إنشاء المكوِّن فعليًا وتثبيته على نموذج DOM، والإجابة هي أنه لكل مكوِّن دورة حياة تبدأ عند إنشائه وتنتهي عند تدميره، وهناك عدد من الدوال التي تسمح بتشغيل الشيفرة في اللحظات المهمة خلال دورة الحياة هذه. الدالة التي ستستخدِمها بكثرة هي الدالة onMount()‎ والتي تتيح تشغيل دالة رد نداء Callback بمجرد تثبيت المكوِّن على نموذج DOM، لذا لنجربها ونرى ما سيحدث للمتغير nameEl. أضِف أولًا السطر التالي في بداية القسم <script> الخاص بالمكوِّن NewTodo.svelte: import { onMount } from 'svelte'; وأضِف الأسطر التالية في نهايته: console.log('initializing:', nameEl); onMount( () => { console.log('mounted:', nameEl); }) احذف الآن السطر if (autofocus) nameEl.focus()‎ لتجنب الخطأ الذي رأيناه سابقًا. سيعمل التطبيق الآن مرةً أخرى، وسترى ما يلي في الطرفية: initializing: undefined mounted: <input id="todo-0" class="input input__lg" type="text" autocomplete="off"> يكون المتغير nameEl غير مُعرَّف أثناء تهيئة المكوِّن، وهو أمر منطقي لأن عقدة حقل الإدخال <input> غير موجودة حتى الآن، لذا أسند إطار عمل Svelte مرجع العقدة <input> في نموذج DOM إلى المتغير nameEl بفضل الموجّه bind:this={nameEl}‎ بعد تثبيت المكوِّن. يمكنك تشغيل وظيفة التركيز التلقائي من خلال استبدال كتلة onMount()‎ ضمن console.log()‎ السابقة بما يلي: onMount(() => autofocus && nameEl.focus()); // ‫سنشغّل التابع nameEl.focus()‎ إذا كانت قيمة autofocus هي true انتقل إلى تطبيقك مرةً أخرى وسترى الآن تركيز حقل الإدخال <input> على صفحة التحميل. انتظار تحديث نموذج DOM باستخدام الدالة tick()‎ سنهتم الآن بتفاصيل إدارة تركيز المكون Todo، إذ نريد أولًا أن يستلم تعديل حقل الإدخال <input> الخاص بالمكوِّن Todo التركيز عند الدخول في وضع التعديل من خلال الضغط على زر "التعديل Edit"، كما سننشئ المتغير nameEl ضمن المكوِّن Todo.svelte وسنستدعي التابع nameEl.focus()‎ بعد ضبط المتغير editing على القيمة true. أولًا، افتح الملف components/Todo.svelte وأضِف التصريح عن المتغير nameEl التالي بعد التصريح عن المتغيرين editing و name مباشرةً: let nameEl; // مرجع إلى عقدة حقل الإدخال‫ name في نموذج DOM ثانيًا، عدّل الدالة onEdit()‎ كما يلي: function onEdit() { editing = true; // الدخول في وضع التعديل nameEl.focus(); // ‫ضبط التركيز على حقل الإدخال name } أخيرًا، اربط المتغير nameEl بحقل الإدخال <input> من خلال تعديله كما يلي: <input bind:value={name} bind:this={nameEl} type="text" id="todo-{todo.id}" autocomplete="off" class="todo-text" /> ولكن ستحصل على خطأ بالشكل: "TypeError: nameEl is undefined" في الطرفية عند الضغط على زر تعديل المهمة. لا يحدّث إطار عمل Svelte نموذج DOM مباشرةً عند تحديث حالة المكوِّن، وإنما ينتظر حتى المهمة السريعة microtask التالية لمعرفة ما إذا كانت هناك أيّ تغييرات أخرى يجب تطبيقها بما في ذلك التغييرات في المكونات الأخرى، مما يؤدي إلى تجنب العمل غير الضروري ويسمح للمتصفح بتجميع الأشياء بطريقة أكثر فعالية. لا يكون تعديل حقل الإدخال <input> مرئيًا في هذه الحالة لأنه غير موجود في نموذج DOM عندما يكون للمتغير editing القيمة false، لذا اضبط editing = true في الدالة onEdit()‎ وحاول بعد ذلك مباشرةً الوصول إلى المتغير nameEl ونفّذ التابع nameEl.focus()‎، ولكن المشكلة هنا هي أنّ إطار عمل Svelte لم يحدّث نموذج DOM بعد. تتمثل إحدى طرق حل هذه المشكلة في استخدام التابع setTimeout()‎ لتأخير استدعاء التابع nameEl.focus()‎ حتى دورة الأحداث التالية وإعطاء إطار عمل Svelte الفرصة لتحديث نموذج DOM كما يلي: function onEdit() { editing = true; // الدخول في وضع التعديل setTimeout(() => nameEl.focus(), 0); // استدعاء غير متزامن لضبط التركيز على حقل الإدخال‫ name } الحل السابق جيد، ولكنه غير مرتب إلى حد ما، إذ يوفر إطار Svelte طريقةً أفضل للتعامل مع هذه الحالات، حيث تعيد الدالة tick()‎ وعدًا Promise يُحَل بمجرد تطبيق أيّ تغييرات على حالة مُعلَّقة في نموذج DOM، أو مباشرةً إذا لم تكن هناك تغييرات على حالة مُعلَّقة. استورد أولًا tick في بداية القسم <script> مع تعليمات الاستيراد الموجودة مسبقًا كما يلي: import { tick } from 'svelte' استدعِ بعد ذلك الدالة tick()‎ مع المعامِل await من دالة غير متزامنة، وعدّل الدالة onEdit()‎ كما يلي: async function onEdit() { editing = true; // الدخول في وضع التعديل await tick(); nameEl.focus(); } إذا جربت التطبيق الآن، فسترى أنّ كل شيء يعمل كما هو متوقع. إضافة وظائف إلى عناصر HTML باستخدام الموجه use:action نريد بعد ذلك أن يحدّد حقل الإدخال <input> كل النص عند التركيز عليه، كما نريد تطوير ذلك بطريقة يمكن إعادة استخدامه بسهولة على أيّ عنصر <input> في HTML وتطبيقه بطريقة تصريحية، وسنستخدِم هذا المتطلب بوصفه سببًا لإظهار ميزة قوية جدًا يوفرها إطار Svelte لإضافة وظائف لعناصر HTML العادية، وهذه الميزة هي الإجراءات actions. يمكنك تحديد نص عقدة حقل إدخال في نموذج DOM من خلال استدعاء التابع select()‎، إذ يجب استخدام مستمع أحداث لاستدعاء هذه الدالة كلما انتقل التركيز إلى هذه العقدة كما يلي: node.addEventListener('focus', event => node.select()) كما يجب استدعاء الدالة removeEventListener()‎ عند تدمير العقدة لتجنب تسرّب الذاكرة Memory Leak. ملاحظة: كل ما سبق هو مجرد وظيفة قياسية من واجهة WebAPI دون وجود شيء خاص بإطار عمل Svelte. يمكن تحقيق كل ذلك في المكوِّن Todo كلما أضفنا أو أزلنا عنصر <input> من نموذج DOM، ولكن يجب أن نكون حريصين جدًا على إضافة مستمع الأحداث بعد إضافة العقدة إلى نموذج DOM وإزالة المستمع قبل إزالة العقدة من نموذج DOM. كما أنّ هذا الحل لن يكون قابلًا لإعادة الاستخدام بصورة كبيرة، وهنا يأتي دور إجراءات إطار Svelte التي تسمح بتشغيل دالة كلما أُضيف عنصر إلى نموذج DOM وبعد إزالته من نموذج DOM. سنعرّف دالة بالاسم selectOnFocus()‎ تأخذ عقدة على أساس معامل لها، وستضيف هذه الدالة مستمع أحداث إلى تلك العقدة بحيث يُحدَّد النص كلما انتقل التركيز إليها، ثم ستعيد كائنًا مع الخاصية destroy التي سينفذها إطار Svelte بعد إزالة العقدة من نموذج DOM، وسنزيل هنا المستمع للتأكّد من أننا لا نترك أيّ تسرّب للذاكرة خلفنا. أولًا، لننشئ الدالة selectOnFocus()‎، لذا أضف ما يلي إلى أسفل القسم <script> الخاص بالمكوِّن Todo.svelte: function selectOnFocus(node) { if (node && typeof node.select === 'function' ) { // ‫تأكّد من أن العقدة مُعرَّفة ولديها التابع select()‎ const onFocus = event => node.select(); // معالج الحدث node.addEventListener('focus', onFocus); // ‫استدعِ التابع onFocus()‎ عندما ينتقل التركيز إلى العقدة return { destroy: () => node.removeEventListener('focus', onFocus) // ‫سيُنفَّذ هذا السطر عند إزالة العقدة من نموذج DOM } } } يجب الآن إعلام حقل الإدخال <input> بأن يستخدِم هذه الدالة من خلال الموجّه use:action كما يلي: <input use:selectOnFocus /> نطلب باستخدام هذا الموجّه من إطار Svelte تشغيل هذه الدالة وتمرير عقدة نموذج DOM الخاصة بحقل الإدخال <input> بوصفها معاملًا لها بمجرد تثبيت المكوِّن على نموذج DOM، وسيكون مسؤولًا عن تنفيذ الدالة destroy عند إزالة المكوِّن من نموذج DOM، وبالتالي يهتم Svelte بدورة حياة المكوِّن باستخدام الموجّه use، وسيكون العنصر <input> في حالتنا كما يلي: عدّل أول زوج تسمية أو عنوان/حقل إدخال label/input للمكوِّن ضمن قالب التعديل على النحو التالي: <label for="todo-{todo.id}" class="todo-label">New name for '{todo.name}'</label> <input bind:value={name} bind:this={nameEl} use:selectOnFocus type="text" id="todo-{todo.id}" autocomplete="off" class="todo-text" /> انتقل إلى تطبيقك واضغط على زر تعديل المهام ثم اضغط على المفتاح Tab لإبعاد التركيز عن العنصر <input>، ثم انقر عليه وسترى تحديد نص حقل الإدخال بالكامل. جعل الإجراء قابلا لإعادة الاستخدام لنجعل الآن هذه الدالة قابلة لإعادة الاستخدام بين المكونات، إذ تُعَدّ الدالة selectOnFocus()‎ مجرد دالة لا تعتمد على المكوِّن Todo.svelte، لذا يمكننا وضعها في ملف واستخدامها من هناك. أولًا، أنشئ ملفًا جديدًا بالاسم actions.js ضمن المجلد src. ثانيًا، ضع فيه المحتوى التالي: export function selectOnFocus(node) { if (node && typeof node.select === 'function' ) { // ‫تأكّد من أن العقدة مُعرَّفة ولديها التابع select()‎ const onFocus = event => node.select(); // معالج الحدث node.addEventListener('focus', onFocus); // يُستدعى عند التركيز على القعدة return { destroy: () => node.removeEventListener('focus', onFocus) // ‫سيُنفَّذ هذا السطر عند إزالة العقدة من نموذج DOM } } } استورده من داخل المكوِّن Todo.svelte من خلال إضافة تعليمة الاستيراد التالية: import { selectOnFocus } from '../actions.js' احذف تعريف الدالة selectOnFocus()‎ من المكوِّن Todo.svelte، لأننا لم نعُد بحاجة إليها هناك. إعادة استخدام الإجراء لنستخدم الإجراء في المكوِّن NewTodo.svelte لإثبات إمكانية إعادة استخدامه. أولًا، استورد الدالة selectOnFocus()‎ من الملف actions.js في الملف NewTodo.svelte كما يلي: import { selectOnFocus } from '../actions.js'; ثانيًا، أضف الموجّه use:selectOnFocus إلى العنصر <input> كما يلي: <input bind:value={name} bind:this={nameEl} use:selectOnFocus type="text" id="todo-0" autocomplete="off" class="input input__lg" /> يمكننا إضافة وظائف لعناصر HTML العادية بطريقة قابلة لإعادة الاستخدام وتصريحية باستخدام بضعة أسطر من الشيفرة البرمجية، إذ يتطلب الأمر استيرادًا import وموجّهًا قصيرًا مثل الموجّه use:selectOnFocus الذي يوضِّح الغرض منه، ويمكننا تحقيق ذلك دون الحاجة إلى إنشاء عنصر مُغلِّف مخصَّص مثل TextInput أو MyInput أو ما شابه ذلك، كما يمكنك إضافة العديد من موجّهات use:action إلى عنصر ما. كما أنه ليس علينا أن نعاني باستخدام الدوال onMount()‎ أو onDestroy()‎ أو tick()‎، إذ يهتم الموجّه use بدورة حياة المكوِّن. تحسينات الإجراءات الأخرى كان علينا في القسم السابق أثناء العمل مع مكونات Todo التعاملَ مع الدوال bind:this و tick()‎ و async للتركيز على حقل الإدخال <input> بمجرد إضافته إلى نموذج DOM. يمكننا تطبيق ذلك باستخدام الإجراءات كما يلي: const focusOnInit = (node) => node && typeof node.focus === 'function' && node.focus(); يجب بعد ذلك إضافة موجّه use:‎ آخر في شيفرة HTML كما يلي: <input bind:value={name} use:selectOnFocus use:focusOnInit /> يمكن الآن أن تكون الدالة onEdit()‎ أبسط كما يلي: function onEdit() { editing = true; // الدخول في وضع التعديل } لنعُد إلى المكوِّن Todo.svelte ونركّز على زر "التعديل Edit" بعد أن يضغط المستخدِم على زر "الحفظ Save" أو "الإلغاء Cancel". يمكننا محاولة إعادة استخدام الإجراء focusOnInit مرة أخرى من خلال إضافة الموجّه use:focusOnInit إلى زر "التعديل Edit"، لكننا سندخِل بذلك زلة برمجية، إذ سينتقل التركيز عند إضافة مهمة جديدة إلى زر "التعديل Edit" الخاص بالمهمة التي أُضيفت مؤخرًا بسبب تشغيل الإجراء focusOnInit عند إنشاء المكوِّن. لا نريد ذلك، وإنما نريد أن يستلم زر "التعديل Edit" التركيز فقط عندما يضغط المستخدِم على زر "الحفظ Save" أو "الإلغاء Cancel". لذا ارجع إلى الملف Todo.svelte، إذ سننشئ أولًا رايةً بالاسم editButtonPressed ونهيّئها بالقيمة false، لذا أضف ما يلي بعد تعريفات المتغيرات الأخرى: let editButtonPressed = false; // تتبّع إذا ضُغِط على زر التعديل للتركيز عليه بعد الإلغاء أو الحفظ سنعدّل بعد ذلك وظيفة زر "التعديل Edit" لحفظ هذه الراية وإنشاء إجرائها الخاص، لذا عدّل الدالة onEdit()‎ كما يلي: function onEdit() { editButtonPressed = true; // سيؤدي ضغط المستخدم على زر التعديل إلى عودة التركيز إليه editing = true; // الدخول في وضع التعديل } أضِف بعد ذلك تعريف الدالة focusEditButton()‎ التالي: const focusEditButton = (node) => editButtonPressed && node.focus(); أخيرًا، استخدم الموجّه use:focusEditButton مع زر "التعديل Edit" كما يلي: <button type="button" class="btn" on:click={onEdit} use:focusEditButton> Edit<span class="visually-hidden"> {todo.name}</span> </button> جرّب تطبيقك مرةً أخرى، إذ يُنفَّذ الإجراء focusEditButton في هذه المرحلة في كل مرة يُضاف فيها زر "التعديل Edit" إلى نموذج DOM، ولكنه سيعطي التركيز فقط للزر إذا كانت قيمة الراية editButtonPressed هي true. ملاحظة: لم نتعمق كثيرًا في الإجراءات هنا، إذ يمكن أن تحتوي الإجراءات على معامِلات تفاعلية، ويتيح إطار Svelte اكتشاف متى يتغير أيّ من هذه المعامِلات لنتمكن من إضافة وظائف تتكامل جيدًا مع نظام التفاعل في إطار Svelte، كما تُعَدّ الإجراءات مفيدةً للتكامل بسلاسة مع المكتبات الخارجية. ربط المكونات: الوصول إلى توابع ومتغيرات المكون باستخدام الموجه bind:this={component}‎ توجد مشكلة أخرى وهي أنه يتلاشى التركيز عندما يضغط المستخدِم على زر "الحذف Delete"، إذ تتضمن الميزة الأخيرة التي سنشرحها في هذا المقال ضبط التركيز على عنوان الحالة بعد حذف مهمة. اخترنا التركيز على عنوان الحالة بسبب حذف العنصر الذي جرى التركيز عليه، لذلك لا يوجد عنصر آخر واضح لتلقي التركيز، إذ يُعَدّ عنوان الحالة قريبًا من قائمة المهام، وهو طريقة مرئية لمعرفة إزالة المهمة بالإضافة إلى توضيح ما حدث لمستخدِمي قارئ الشاشة. أولًا، أنشئ ملفًا جديدًا بالاسم components/TodosStatus.svelte. ثانيًا، أضِف إليه المحتويات التالية: <script> export let todos; $: totalTodos = todos.length; $: completedTodos = todos.filter((todo) => todo.completed).length; </script> <h2 id="list-heading"> {completedTodos} out of {totalTodos} items completed </h2> ثالثًا، استورد هذا الملف في بداية المكوِّن Todos.svelte من خلال إضافة تعليمة الاستيراد import التالية بعد تعليمات الاستيراد الأخرى: import TodosStatus from './TodosStatus.svelte'; رابعًا، استبدل عنوان الحالة <h2> ضمن الملف Todos.svelte باستدعاء المكوِّن TodosStatus من خلال تمرير todos إليه بوصفها خاصيةً كما يلي: <TodosStatus {todos} /> خامسًا، أزِل المتغيرين totalTodos و completedTodos من المكوِّن Todos.svelte، إذ ما عليك سوى إزالة السطرين ‎$: totalTodos = ...‎ و‎$: completedTodos = ...‎ وإزالة المرجع إلى المتغير totalTodos عندما نحسب newTodoId واستخدم todos.length بدلًا من ذلك، أي استبدل الكتلة التي تبدأ بالسطر let newTodoId بما يلي: $: newTodoId = todos.length ? Math.max(...todos.map(t => t.id)) + 1 : 1; يعمل كل شيء كما هو متوقع، واستخرجنا للتو آخر جزء من شيفرة HTML إلى مكوِّنه الخاص. يجب الآن إيجاد طريقة للتركيز على تسمية الحالة <h2> بعد إزالة المهمة، إذ رأينا حتى الآن كيفية إرسال المعلومات إلى مكوِّن باستخدام الخاصيات Props، وكيف يمكن للمكوِّن التواصل مع المكوِّن الأب عن طريق إصدار أحداث أو استخدام ربط البيانات ثنائي الاتجاه، إذ يمكن للمكوِّن الابن الحصول على مرجع إلى العقدة <h2> باستخدام الموجّه bind:this={dom_node}‎ ويمكن للمكونات الخارجية الوصول إليه باستخدام ربط البيانات ثنائي الاتجاه، لكن سيؤدي ذلك إلى كسر تغليف المكوِّن، لذلك نحن بحاجة إلى المكوِّن TodosStatus للوصول إلى تابع يمكن للمكوِّن الابن استدعاؤه للتركيز علي، إذ تُعَدّ حاجة المكوِّن لإمكانية وصول المستخدِم لبعض السلوك أو المعلومات أمرًا شائعًا جدًا، لذا لنرى كيفية تحقيق ذلك في إطار عمل Svelte. رأينا سابقًا أن إطار عمل Svelte يستخدِم التعليمة export let varname = ...‎ للتصريح عن الخاصيات، ولكن إذا صدّرتَ ثابتًا const أو صنفًا class أودالةً function بدلًا من استخدام let لوحدها، فستكون للقراءة فقط خارج المكوِّن، وتُعَدّ تعابير الدوال خاصيات صالحةً. تُعَدّ التصريحات الثلاثة الأولى في المثال التالي خاصيات، والتصريحات الأخرى هي عبارة عن قيم مُصدَّرة: <script> export let bar = "optional default initial value"; // خاصية export let baz = undefined; // خاصية export let format = n => n.toFixed(2); // خاصية // these are readonly export const thisIs = "readonly"; // تصدير للقراءة فقط export function greet(name) { // تصدير للقراءة فقط alert(`hello ${name}!`); } export const greet = (name) => alert(`hello ${name}!`); // تصدير للقراءة فقط </script> لننشئ تابعًا بالاسم focus()‎ يركّز على العنوان <h2>، لذا سنحتاج إلى المتغير headingEl للاحتفاظ بالمرجع إلى عقدة DOM، ويجب ربطه بالعنصر <h2> باستخدام الموجّه ‎‎bind:this={headingEl}‎‎‎، إذ سيشغّل تابع التركيز فقط headingEl.focus()‎. أولًا، عدّل محتويات المكوِّن TodosStatus.svelte كما يلي: <script> export let todos; $: totalTodos = todos.length; $: completedTodos = todos.filter((todo) => todo.completed).length; let headingEl; export function focus() { // shorter version: export const focus = () => headingEl.focus() headingEl.focus(); } </script> <h2 id="list-heading" bind:this={headingEl} tabindex="-1"> {completedTodos} out of {totalTodos} items completed </h2> لاحظ أننا أضفنا السمة tabindex إلى العنوان <h2> للسماح للعنصر بتلقي التركيز برمجيًا، إذ يعطينا استخدام الموجِّه bind:this={headingEl}‎ مرجعًا إلى عقدة DOM في المتغير headingEl كما رأينا سابقًا، كما نستخدم بعد ذلك التعليمة export function focus()‎ لإمكانية الوصول إلى دالة تركّز على العنوان <h2>، كما يمكنك ربط نسخ المكوِّن باستخدام الموجِّه bind:this={component}‎ مثل ربط عناصر DOM باستخدام الموجّه bind:this={dom_node}‎، لذا تحصل على مرجع لعقدة DOM عند استخدام الموجِّه bind:this مع عنصر HTML، وتحصل على مرجع إلى نسخة من هذا المكوِّن عندما تفعل ذلك مع مكوِّن Svelte. ثانيًا، سننشئ أولًا المتغير todosStatus في Todos.svelte للربط بنسخة من المكوِّن Todos.svelte، لذا أضف السطر التالي بعد تعليمات الاستيراد الموجودة مسبقًا: let todosStatus; // ‫مرجع إلى نسخة من المكون TodosStatus ثالثًا، أضِف بعد ذلك الموجّه bind:this={todosStatus}‎ إلى الاستدعاء كما يلي: <!-- TodosStatus --> <TodosStatus bind:this={todosStatus} {todos} /> رابعًا، يمكننا الآن استدعاء التابع focus()‎ المُصدَّر من التابع removeTodo()‎ كما يلي: function removeTodo(todo) { todos = todos.filter((t) => t.id !== todo.id); todosStatus.focus(); // ركّز على عنوان الحالة } خامسًا، ارجع إلى تطبيقك، فإذا حذفت أيّ مهمة الآن، فسينتقل التركيز إلى عنوان الحالة، وهذا مفيد لتسليط الضوء على التغيير في عدد المهام لكل من المستخدِمين المبصرين ومستخدِمي قارئات الشاشة. ملاحظة: يمكن أن تتساءل عن سبب حاجتنا للتصريح عن متغير جديد لربط المكون بالرغم من أنه يمكننا فقط استدعاء التابع TodosStatus.focus()‎، إذ يمكن أن يكون لديك العديد من نسخ المكوِّن TodosStatus النشطة، لذلك تحتاج لطريقة للرجوع إلى كل نسخة معينة، وبالتالي يجب تحديد متغير لربط كل نسخة محددة به. يمكنك الوصول إلى نسختك من مستودعنا على النحو التالي لمعرفة حالة الشيفرة كما يجب أن تكون في نهاية هذا المقال: cd mdn-svelte-tutorial/06-stores أو يمكنك تنزيل محتوى المجلد مباشرةً باستخدام الأمر التالي: npx degit opensas/mdn-svelte-tutorial/06-stores تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، فإذا أردت متابعتنا، فابدأ بكتابة الشيفرة باستخدام الأداة REPL من موقع svelte.dev. الخلاصة انتهينا في هذا المقال من إضافة جميع الوظائف المطلوبة إلى تطبيقنا، بالإضافة إلى اهتمامنا بعدد من مشاكل سهولة الوصول وسهولة الاستخدام، وانتهينا من تقسيم تطبيقنا إلى مكونات يمكن إدارتها مع إعطاء كل منها مسؤولية فريدة، كما رأينا بعض تقنيات إطار عمل Svelte المتقدمة مثل: التعامل مع اكتشاف التفاعل عند تحديث العناصر والمصفوفات. العمل مع عقد DOM باستخدام الموجّه bind:this={dom_node}‎ (ربط عناصر DOM). استخدام الدالة onMount()‎ الخاصة بدورة حياة المكوِّن. إجبار إطار عمل Svelte على حل تغييرات الحالة المُعلَّقة باستخدام الدالة tick()‎. إضافة وظائف لعناصر HTML بطريقة تصريحية وقابلة لإعادة الاستخدام باستخدام الموجّه use:action. الوصول إلى توابع المكونات باستخدام الموجّه bind:this={component}‎ (ربط المكونات). سنرى في المقال التالي كيفية استخدام المخازن Stores للتواصل بين المكونات وإضافة الحركة إلى المكونات. ترجمة -وبتصرُّف- للمقال Advanced Svelte: Reactivity, lifecycle, accessibility. اقرأ أيضًا بدء استخدام إطار العمل Svelte لبناء تطبيقات ويب إنشاء تطبيق قائمة مهام باستعمال إطار عمل Svelte التعامل مع المتغيرات والخاصيات في إطار عمل Svelte تقسيم تطبيق Svelte إلى مكونات
  20. بدأنا في مقال التعامل مع المتغيرات والخاصيات بتطوير تطبيق قائمة المهام، والهدف الأساسي من هذا المقال هو تعلّم كيفية تقسيم تطبيقنا إلى مكونات يمكن إدارتها ومشاركة المعلومات فيما بينها. سنقسّم تطبيقنا إلى مكونات، ثم سنضيف مزيدًا من الوظائف للسماح للمستخدمين بتحديث المكونات الحالية. المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية، وستحتاج طرفية مثبَّت عليها node و npm لتصريف وبناء تطبيقك. الهدف: تعلم كيفية تقسيم تطبيقنا إلى مكونات ومشاركة المعلومات فيما بينها. يمكنك متابعة كتابة شيفرتك معنا، لذلك انسخ أولًا مستودع github -إذا لم تفعل ذلك مسبقًا- باستخدام الأمر التالي: git clone https://github.com/opensas/mdn-svelte-tutorial.git ثم يمكنك الوصول إلى حالة التطبيق الحالية من خلال تشغيل الأمر التالي: cd mdn-svelte-tutorial/04-componentizing-our-app أو يمكنك تنزيل محتوى المجلد مباشرةً كما يلي: npx degit opensas/mdn-svelte-tutorial/04-componentizing-our-app تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، وإذا أردت متابعتنا فابدأ بكتابة الشيفرة باستخدام الأداة REPL. تقسيم التطبيق إلى مكونات يتكون التطبيق في إطار عمل Svelte من مكوِّن واحد أو من مكونات متعددة، ويُعَدّ المكوِّن كتلةً من الشيفرة البرمجية القابلة لإعادة الاستخدام والمستقلة ذاتيًا والتي تغلّف شيفرة HTML و CSS وجافاسكربت المرتبطة مع بعضها البعض والمكتوبة في ملف ‎.svelte، كما يمكن أن تكون المكونات كبيرةً أو صغيرةً، لكنها تكون عادةً محدَّدةً بوضوح، فالمكونات الأكثر فاعليةً هي المكونات التي تخدم غرضًا واحدًا واضحًا. من فوائد تحديد المكونات هو قابلية هذه المكونات للموازنة مع أفضل الممارسات العامة بهدف تنظيم شيفرتك البرمجية ضمن أجزاء يمكن إدارتها، مما يساعدك على فهم كيفية ارتباطها ببعضها البعض ويعزِّز إعادة الاستخدام ويجعل شيفرتك البرمجية أسهل للتفكير بها وصيانتها وتوسيعها. لا توجد قواعد صارمة لتقسيم المكونات، لذا يفضِّل بعض الأشخاص اتباع نهج بسيط يتمثل بالنظر إلى شيفرة HTML ثم رسم مربعات حول كل مكوِّن ومكوِّن فرعي يبدو أنّ له شيفرته الخاصة، في حين يطبق أشخاص آخرون الأساليب نفسها المُستخدَمة لتحديد ما إذا كان يجب إنشاء دالة أو كائن جديد، وأحد هذه الأساليب هو مبدأ المسؤولية الفردية، أي يجب أن يطبّق المكون شيئًا واحدًا فقط بصورة مثالية، ثم يمكننا تقسيمه إلى مكونات فرعية أصغر إذا لزم الأمر، كما يجب أن يكمل هذا النهجان بعضهما البعض لمساعدتك على تحديد كيفية تنظيم مكوناتك بطريقة أفضل. سنقسم تطبيقنا إلى المكونات التالية: Alert.svelte: مربع إشعارات عام لإرسال الإجراءات التي حدثت. NewTodo.svelte: حقل إدخال النص والزر الذي يسمح بإدخال عنصر مهام جديد. FilterButton.svelte: أزرار "كل المهام All" و"المهام النشطة Active" و"المهام المكتملة Completed" التي تسمح بتطبيق المرشّحات Filters على عناصر المهام المعروضة. TodosStatus.svelte: العنوان الذي يعرض العبارة "x out of y items completed" التي تمثّل عدد المهام المكتملة. Todo.svelte: عنصر مهام مفرد، إذ سيُعرَض كل عنصر مهمة مرئي في نسخة منفصلة من هذا المكوِّن. MoreActions.svelte: الزرّان "تحديد الكل Check All" و"احذف المهام المكتملة Remove Completed" الموجودان أسفل واجهة المستخدِم، ويسمحان بتنفيذ مجموعة إجراءات على عناصر المهام. سنركز في هذا المقال على إنشاء المكونين FilterButton و Todo وسنشرح المكونات الأخرى في المقالات القادمة. ملاحظة: سنتعلم أيضًا في عملية إنشاء أول مكونين تقنيات مختلفة لتواصل المكونات مع بعضها بعضًا، وإيجابيات وسلبيات كل من هذه التقنيات. استخراج مكون الترشيح سننشئ أولًا المكون FilterButton.svelte باتباع الخطوات التالية: أولًا، أنشئ ملفًا جديدًا components/FilterButton.svelte. ثانيًا، سنصرّح عن الخاصية filter في هذا الملف ثم سننسخ شيفرة HTML المتعلقة به من الملف Todos.svelte، لذا أضِف المحتوى التالي إلى هذا الملف: <script> export let filter = 'all' </script> <div class="filters btn-group stack-exception"> <button class="btn toggle-btn" class:btn__primary={filter === 'all'} aria-pressed={filter === 'all'} on:click={()=> filter = 'all'} > <span class="visually-hidden">Show</span> <span>All</span> <span class="visually-hidden">tasks</span> </button> <button class="btn toggle-btn" class:btn__primary={filter === 'active'} aria-pressed={filter === 'active'} on:click={()=> filter = 'active'} > <span class="visually-hidden">Show</span> <span>Active</span> <span class="visually-hidden">tasks</span> </button> <button class="btn toggle-btn" class:btn__primary={filter === 'completed'} aria-pressed={filter === 'completed'} on:click={()=> filter = 'completed'} > <span class="visually-hidden">Show</span> <span>Completed</span> <span class="visually-hidden">tasks</span> </button> </div> ثالثًا، ارجع إلى المكوِّن Todos.svelte، حيث نريد الاستفادة من المكوِّن FilterButton، إذ يجب استيراده أولًا، لذا أضِف السطر التالي قبل القسم <script> في المكوِّن Todos.svelte: import FilterButton from './FilterButton.svelte' رابعًا، استبدل الآن العنصر <div> الذي يملك اسم الصنف filters باستدعاء المكون FilterButton الذي يأخذ المرشح الحالي بوصفه خاصيةً كما يلي: <FilterButton {filter} /> ملاحظة: تذكَّر أنه إذا تطابق اسم سمة في لغة HTML مع اسم المتغير، فيمكن استبدالهما بالشكل {variable}، لذا يمكننا استبدال <FilterButton filter={filter} /‎> بالشكل <FilterButton {filter} /‎>. لنجرب التطبيق الآن، حيث ستلاحظ أنه إذا نقرت على أزرار الترشيح، فستُحدَّد هذه الأزرار وسيُحدَّث التنسيق بطريقة مناسبة، ولكن لدينا مشكلة وهي عدم ترشيح المهام، وسبب هذه المشكلة هو انتقال المتغير filter من المكوِّن Todos إلى المكوِّن FilterButton عبر الخاصية، ولكن لا تنتقل التغييرات التي تحدث في المكوِّن FilterButton مرةً أخرى إلى المكوِّن الأب، إذ يكون ارتباط البيانات أحادي الاتجاه افتراضيًا. مشاركة البيانات بين المكونات: تمرير المعالج بوصفه خاصية تتمثل إحدى طرق السماح للمكونات الأبناء بإعلام المكونات الآباء بأيّ تغييرات في تمرير المعالج بوصفه خاصيةً Prop، حيث سينفّذ المكوِّن الابن المعالج، ويمرّر المعلومات المطلوبة بوصفها معاملًا وسيعدّل المعالج حالة المكوِّن الأب، كما سيتلقّى المكوِّن FilterButton في حالتنا المعالج onclick من المكوِّن الأب، فإذا نقر المستخدِم على أيّ زر ترشيح، فسيستدعي المكونُ الابن المعالجَ onclick ويمرّر المرشّح المحدد على أساس معامل إلى المكوِّن الأب. سنصرّح فقط عن الخاصية onclick التي تُسنَد إلى معالِج وهمي لمنع الأخطاء كما يلي: export let onclick = (clicked) => {} وسنصرّح عن التعليمة التفاعلية ‎$: onclick(filter)‎ لاستدعاء المعالِج onclick كلما جرى تحديث المتغير filter. أولًا، يجب أن يبدو القسم <script> الخاص بالمكوِّن FilterButton كما يلي: <script> export let filter = 'all' export let onclick = (clicked) => {} $: onclick(filter) </script> إذا استدعينا المكوِّن FilterButton ضمن المكوِّن Todos.svelte الآن، فيجب تحديد المعالج، لذا عدّله إلى ما يلي: <FilterButton {filter} onclick={ (clicked) => filter = clicked }/> إذا نقرت على أيّ زر ترشيح، فسنعدّل المتغير filter باستخدام المرشّح الجديد وسيعمل المكوِّن FilterButton مرةً أخرى. طريقة أسهل لربط البيانات ثنائي الاتجاه باستخدام الموجه bind أدركنا في المثال السابق أنّ المكوِّن FilterButton لم يعمل، لأنّ حالة التطبيق تنتقل من المكوِّن الأب إلى المكوِّن الابن من خلال الخاصية filter، ولكنها لا ترجع مرةً أخرى من المكوِّن الابن إلى المكوِّن الأب، لذلك أضفنا الخاصية onclick للسماح للمكوِّن الابن بإرسال قيمة الخاصية filter الجديدة إلى المكوِّن الأب. يعمل التطبيق جيدًا، ولكن يوفر إطار عمل Svelte طريقةً سهلةً ومباشرةً لتحقيق ربط البيانات ثنائي الاتجاه، إذ تتدفق البيانات عادةً من المكوِّن الأب إلى المكوِّن الابن باستخدام الخاصيات، وإذا أردنا أن تتدفق في الاتجاه الآخر من المكوِّن الابن إلى المكوِّن الأب، فيمكننا استخدام الموجّه bind:‎. سنخبر إطار عمل Svelte باستخدام الموجّه bind أنّ أيّ تغييرات تجرَى على الخاصية filter في المكوِّن FilterButton يجب أن تنتشر إلى المكوِّن الأب Todos، أي أننا سنربط قيمة المتغير filter في المكوِّن الأب بقيمته في المكوِّن الابن. أولًا، عدّل استدعاء المكوِّن FilterButton في Todos.svelte كما يلي: <FilterButton bind:filter={filter} /> يوفِّر إطار عمل Svelte اختصارًا، إذ تعادل التعليمةُ bind:value={value}‎ التعليمةَ bind:value، لذلك يمكنك في المثال السابق كتابة <FilterButton bind:filter /‎> فقط. ثانيًا، يمكن للمكوِّن الابن الآن تعديل قيمة المتغير filter الخاص بالمكون الأب، لذلك لم نعد بحاجة إلى الخاصية onclick، لذا عدّل القسم <script> الخاص بالمكوِّن FilterButton كما يلي: <script> export let filter = 'all' </script> ثالثًا، جرب تطبيقك مرةً أخرى، وستظل ترى أن المرشّحات تعمل بصورة صحيحة. إنشاء المكون Todo سننشئ الآن المكون Todo لتغليف كل مهمة بما في ذلك مربع الاختيار وشيفرة التعديل لتتمكّن من تعديل مهمة موجودة مسبقًا، وسيتلقى المكوِّن Todo الكائن todo بوصفه خاصيةً، لذا لنصرّح عن الخاصية todo ولننقل الشيفرة البرمجية من المكوِّن Todos، كما سنستبدل حاليًا استدعاء removeTodo باستدعاء alert وسنضيف هذه الوظيفة مرةً أخرى في وقت لاحق. أنشئ ملف مكوِّن جديد components/Todo.svelte، وضَع بعد ذلك المحتويات التالية ضمن هذا الملف: <script> export let todo </script> <div class="stack-small"> <div class="c-cb"> <input type="checkbox" id="todo-{todo.id}" on:click={() => todo.completed = !todo.completed} checked={todo.completed} /> <label for="todo-{todo.id}" class="todo-label">{todo.name}</label> </div> <div class="btn-group"> <button type="button" class="btn"> Edit <span class="visually-hidden">{todo.name}</span> </button> <button type="button" class="btn btn__danger" on:click={() => alert('not implemented')}> Delete <span class="visually-hidden">{todo.name}</span> </button> </div> </div> يجب الآن استيراد المكوِّن Todo إلى Todos.svelte، لذا انتقل إلى هذا الملف الآن وأضف تعليمة الاستيراد import التالية بعد تعليمة الاستيراد الموجودة مسبقًا: import Todo from './Todo.svelte' يجب بعد ذلك تحديث كتلة {‎#each} لتضمين المكوِّن <Todo> لكل مهمة بدلًا من الشيفرة المنقولة إلى Todo.svelte، ويجب تمرير كائن todo الحالي إلى المكوِّن بوصفه خاصيةً، لذا عدّل كتلة {‎#each} ضمن المكوِّن Todos.svelte كما يلي: <ul role="list" class="todo-list stack-large" aria-labelledby="list-heading"> {#each filterTodos(filter, todos) as todo (todo.id)} <li class="todo"> <Todo {todo} /> </li> {:else} <li>Nothing to do here!</li> {/each} </ul> تُعرَض قائمة المهام على الصفحة، ويجب أن تعمل مربعات الاختيار (حاول تحديد أو إلغاء تحديد مربعات الاختيار، ثم لاحظ أنّ المرشحات لا تزال تعمل كما هو متوقع)، ولكن لن يُحدَّث عنوان الحالة "x out of y items completed" وفقًا لذلك لأن المكوِّن Todo يتلقى المهام باستخدام الخاصية، لكنه لا يرسل أيّ معلومات إلى المكوِّن الأب، وسنصلح ذلك لاحقًا. مشاركة البيانات بين المكونات: نمط الخاصيات للأسفل Props-down والأحداث للأعلى Events-up يُعَد الموجّه bind واضحًا جدًا ويسمح بمشاركة البيانات بين المكوِّن الأب والمكوِّن الابن، ولكن يمكن أن يكون تتبّع جميع القيم المرتبطة ببعضها بعضًا أمرًا صعبًا عندما ينمو تطبيقك بصورة أكبر وأكثر تعقيدًا، لذا يمكنك استخدام نهج مختلف هو نمط الاتصال "props-down, events-up". يعتمد هذا النمط على المكونات الأبناء التي تتلقى البيانات من آبائها عبر الخاصيات والمكونات الآباء لتحديث حالتها من خلال معالجة الأحداث التي تطلقها المكونات الأبناء، لذا تتدفق الخاصيات للأسفل Flow Down من المكوِّن الأب إلى المكوِّن الابن وتنتشر Bubble Up الأحداث للأعلى من المكوِّن الابن إلى المكوِّن الأب، إذ ينشئ هذا النمط تدفقًا أسهل ثنائي الاتجاه للمعلومات. لنلقِ نظرةً على كيفية إصدار أحداثنا لإعادة تطبيق وظيفة زر "الحذف Delete" المفقودة، إذ يمكن إنشاء أحداث مخصصة من خلال استخدام الأداة createEventDispatcher التي تعيد الدالة dispatch()‎ التي تسمح بإصدار أحداث مخصصة، فإذا أرسلتَ حدثًا، فيجب تمرير اسم الحدث وكائن اختياري به معلومات إضافية تريد تمريرها إلى كل مستمع، كما ستكون هذه البيانات الإضافية متاحةً في الخاصية detail لكائن الحدث. ملاحظة: تشترك الأحداث المخصصة في إطار عمل Svelte بواجهة برمجة التطبيقات نفسها التي تستخدِمها أحداث DOM العادية، كما يمكنك نشر حدث إلى المكوِّن الأب عن طريق تحديد on:event بدونّ أي معالج. سنعدّل المكون Todo لإصدار الحدث remove عبر تمرير المهمة المحذوفة بوصفها معلومات إضافية. أضِف أولًا الأسطر التالية إلى الجزء العلوي من القسم <script> للمكوِّن Todo: import { createEventDispatcher } from 'svelte' const dispatch = createEventDispatcher() عدّل الآن زر "الحذف Delete" في قسم شيفرة HTML بالملف نفسه ليبدو كما يلي: <button type="button" class="btn btn__danger" on:click={() => dispatch('remove', todo)}> Delete <span class="visually-hidden">{todo.name}</span> </button> نصدر الحدث remove من خلال استخدام dispatch('remove', todo)‎ ونمرِّر المهام todo المحذوفة بوصفها بيانات إضافية، إذ سيُستدعى المعالج باستخدام كائن الحدث المتوفر مع البيانات الإضافية المتوفرة في الخاصية event.detail. يجب الآن الاستماع إلى هذا الحدث من داخل الملف Todos.svelte والتصرف وفقًا لذلك، لذا ارجع إلى هذا الملف وعدّل استدعاء المكوِّن <Todo> كما يلي: <Todo {todo} on:remove={e => removeTodo(e.detail)} /> يتلقى معالجنا المعامِل e (كائن الحدث) الذي يحتفظ بالمهام المحذوفة في الخاصية detail. إذا حاولت تجربة تطبيقك مرةً أخرى الآن، فسترى أنّ وظيفة الحذف تعود للعمل، وبذلك نجح حدثنا المخصّص كما توقعنا، كما يرسل مستمع الحدث remove تغيّر البيانات إلى المكوِّن الأب، لذلك سيُحدَّث عنوان الحالة "x out of y items completed" بصورة مناسبة عند حذف المهام. سنهتم الآن بالحدث update بحيث يمكن إعلام المكوِّن الأب بأيّ مهام مُعدَّلة. تحديث المهام لا يزال يتعين علينا تنفيذ الوظيفة للسماح بتعديل المهام الحالية، إذ يجب تضمين وضع التعديل في المكوِّن Todo، كما سنعرض حقل الإدخال <input> عند الدخول في وضع التعديل للسماح بتعديل اسم المهمة الحالي مع زرين لتأكيد التغييرات أو إلغائها. معالجة الأحداث أولًا، سنحتاج متغيرًا واحدًا لتتبّع ما إذا كنا في وضع التعديل أم في وضع آخر لتخزين اسم المهمة المُعدَّلة، لذا أضِف تعريفات المتغيرات التالية في الجزء السفلي من القسم <script> للمكوِّن Todo: let editing = false // تتبّع نمط التعديل let name = todo.name // تخزين اسم المهمة المُعدَّلة يجب أن نقرِّر ما هي الأحداث التي سيصدرها المكوِّن Todo كما يلي: يمكننا إصدار أحداث مختلفة لتبديل الحالة وتعديل الاسم مثل updateTodoStatus و updateTodoName. أو يمكننا اتباع نهج أعم وإصدار حدث update واحد لكلتا العمليتين. سنتخذ النهج الثاني لنتمكن من إظهار طريقة مختلفة، إذ تتمثل ميزة هذا النهج في أنه يمكننا لاحقًا إضافة المزيد من الحقول إلى المهام مع إمكانية معالجة جميع التحديثات باستخدام الحدث نفسه، فلننشئ الدالة update()‎ التي ستتلقى التغييرات وتصدر حدث تحديث مع المهام المُعدَّلة، لذا أضف ما يلي مرةً أخرى إلى الجزء السفلي من القسم <script>: function update(updatedTodo) { todo = { ...todo, ...updatedTodo } // تطبيق تعديلات على المهمة dispatch('update', todo) // إصدار حدث التحديث } استخدمنا صيغة الانتشار Spread Syntax لإعادة المهمة الأصلية مع التعديلات المُطبَّقة عليها. سننشئ بعد ذلك دوالًا مختلفةً للتعامل مع كل إجراء للمستخدِم، إذ يمكن للمستخدِم حفظ التغييرات أو إلغائها عندما تكون المهمة في وضع التعديل، ويمكن للمستخدِم حذف المهمة أو تعديلها أو تبديل حالتها بين الحالة المكتملة والنشطة عندما لا تكون في وضع التعديل، لذا أضف مجموعة الدوال التالية بعد آخر دالة للتعامل مع هذه الإجراءات: function onCancel() { name = todo.name // ‫إعادة المتغير name إلى قيمته الأولية editing = false // والخروج من وضع التعديل } function onSave() { update({ name: name }) // تحديث اسم المهمة editing = false // والخروج من وضع التعديل } function onRemove() { dispatch('remove', todo) // إصدار حدث الحذف } function onEdit() { editing = true // الدخول في وضع التعديل } function onToggle() { update({ completed: !todo.completed}) // تحديث حالة المهمة } تحديث ملف شيفرة HTML يجب الآن تحديث شيفرة HTML الخاصة بالمكون Todo لاستدعاء الدوال السابقة عند اتخاذ الإجراءات المناسبة، إذ يمكنك التعامل مع وضع التعديل من خلال استخدام المتغير editing الذي له قيمة منطقية، فإذا كانت قيمة هذا المتغير true، فيجب أن يُعرَض حقل الإدخال <input> لتعديل اسم المهمة وزرَّي "الإلغاء Cancel" و"الحفظ Save"؛ أما إذا لم تكن في وضع التعديل، فسيُعرَض مربع الاختيار واسم المهمة وأزرار تعديل المهام وحذفها. يمكن تحقيق ذلك من خلال استخدام كتلة if التي تصيّر شيفرة HTML شرطيًا، ولكن ضع في الحسبان أنها لن تظهِر أو تخفي شيفرة HTML بناءً على شرط معيّن، وإنما ستضيف وتزيل عناصر نموذج DOM ديناميكيًا اعتمادًا على هذا الشرط. إذا كانت قيمة المتغير editing هي true مثلًا، فسيعرض إطار عمل Svelte نموذج التحديث؛ أما إذا كانت قيمته false، فسيزيله من نموذج DOM وسيضيف مربع الاختيار، لذا سيكون تعيين قيمة المتغير editing كافيًا لعرض عناصر HTML الصحيحة بفضل خاصية التفاعل في إطار عمل Svelte. ستكون كتلة if كما يلي: <div class="stack-small"> {#if editing} <!-- markup for editing to-do: label, input text, Cancel and Save Button --> {:else} <!-- markup for displaying to-do: checkbox, label, Edit and Delete Button --> {/if} </div> يمثِّل الجزء {‎:else} أو النصف السفلي من كتلة if قسم عدم التعديل، كما سيكون مشابهًا جدًا للقسم الموجود في المكوِّن Todos، ولكن الاختلاف الوحيد بينهما هو أننا نستدعي الدوال onToggle()‎ و onEdit()‎ و onRemove()‎ اعتمادًا على إجراء المستخدِم. {:else} <div class="c-cb"> <input type="checkbox" id="todo-{todo.id}" on:click={onToggle} checked={todo.completed} > <label for="todo-{todo.id}" class="todo-label">{todo.name}</label> </div> <div class="btn-group"> <button type="button" class="btn" on:click={onEdit}> Edit<span class="visually-hidden"> {todo.name}</span> </button> <button type="button" class="btn btn__danger" on:click={onRemove}> Delete<span class="visually-hidden"> {todo.name}</span> </button> </div> {/if} </div> تجدر الإشارة إلى ما يلي: ننفّذ الدالة onEdit()‎ التي تضبط المتغير editing على القيمة true عندما يضغط المستخدِم على زر "التعديل Edit". نستدعي الدالة onToggle()‎ التي تنفذ الدالة update()‎ من خلال تمرير كائن مع قيمة completed الجديدة بوصفه معامِلًا عندما ينقر المستخدِم على مربع الاختيار. تصدِر الدالة update()‎ الحدث update من خلال تمرير نسخة من المهمة الأصلية مع التغييرات المطبَّقة بوصفها معلومات إضافية. أخيرًا، تصدِر الدالة onRemove()‎ الحدث remove من خلال تمرير المهمة todo المراد حذفها بوصفها بيانات إضافية. ستحتوي واجهة المستخدِم الخاصة بالتعديل -أي النصف العلوي- على حقل الإدخال <input> وزرين لإلغاء التغييرات أو حفظها كما يلي: <div class="stack-small"> {#if editing} <form on:submit|preventDefault={onSave} class="stack-small" on:keydown={e => e.key === 'Escape' && onCancel()}> <div class="form-group"> <label for="todo-{todo.id}" class="todo-label">New name for '{todo.name}'</label> <input bind:value={name} type="text" id="todo-{todo.id}" autoComplete="off" class="todo-text" /> </div> <div class="btn-group"> <button class="btn todo-cancel" on:click={onCancel} type="button"> Cancel<span class="visually-hidden">renaming {todo.name}</span> </button> <button class="btn btn__primary todo-edit" type="submit" disabled={!name}> Save<span class="visually-hidden">new name for {todo.name}</span> </button> </div> </form> {:else} [...] إذا ضغط المستخدِم على زر "التعديل Edit"، فسيُضبَط المتغير editing على القيمة true وسيزيل إطار عمل Svelte شيفرة HTML في الجزء {‎:else} من نموذج DOM وسيستبدله بشيفرة HTML الموجودة في القسم {‎#if‎}. ستكون الخاصية value الخاصة بالعنصر <input> مرتبطةً بالمتغير name، وستستدعي أزرار إلغاء التغييرات وحفظها الدالتين onCancel()‎ و onSave()‎ على التوالي كما يلي، وقد أضفنا هاتين الدالتين سابقًا: إذا استُدعيت الدالة onCancel()‎، فستُعاد الخاصية name إلى قيمتها الأصلية عند تمريرها بوصفها خاصيةً Prop وسنخرج من وضع التعديل عن طريق ضبط المتغير editing على القيمة false. إذا استُدعيت الدالة onSave()‎، فسنشغّل الدالة update()‎ من خلال تمرير الخاصية name المُعدَّلة، وسنخرج من وضع التعديل. كما نعطّل زر "الحفظ Save" عندما يكون حقل الإدخال <input> فارغًا باستخدام السمة disabled={!name}‎، كما نسمح للمستخدِم بإلغاء التعديل باستخدام المفتاح Escape كما يلي: on:keydown={e => e.key === 'Escape' && onCancel()}. كما نستخدِم الخاصية todo.id لإنشاء معرّفات فريدة لعناصر التحكم بحقل الإدخال والتسميات Labels الجديدة. تبدو شيفرة HTML المعدَّلة الكاملة للمكون Todo كما يلي: <div class="stack-small"> {#if editing} <!-- markup for editing todo: label, input text, Cancel and Save Button --> <form on:submit|preventDefault={onSave} class="stack-small" on:keydown={e => e.key === 'Escape' && onCancel()}> <div class="form-group"> <label for="todo-{todo.id}" class="todo-label">New name for '{todo.name}'</label> <input bind:value={name} type="text" id="todo-{todo.id}" autoComplete="off" class="todo-text" /> </div> <div class="btn-group"> <button class="btn todo-cancel" on:click={onCancel} type="button"> Cancel<span class="visually-hidden">renaming {todo.name}</span> </button> <button class="btn btn__primary todo-edit" type="submit" disabled={!name}> Save<span class="visually-hidden">new name for {todo.name}</span> </button> </div> </form> {:else} <!-- markup for displaying todo: checkbox, label, Edit and Delete Button --> <div class="c-cb"> <input type="checkbox" id="todo-{todo.id}" on:click={onToggle} checked={todo.completed} > <label for="todo-{todo.id}" class="todo-label">{todo.name}</label> </div> <div class="btn-group"> <button type="button" class="btn" on:click={onEdit}> Edit<span class="visually-hidden"> {todo.name}</span> </button> <button type="button" class="btn btn__danger" on:click={onRemove}> Delete<span class="visually-hidden"> {todo.name}</span> </button> </div> {/if} </div> ملاحظة: يمكننا أيضًا تقسيم هذا المكوِّن إلى مكوِّنين مختلفين أحدهما لتعديل المهام والآخر لعرضها، إذ يتلخص الأمر في مدى شعورك بالراحة في التعامل مع هذا المستوى من التعقيد باستخدام مكوِّن واحد، لذا يجب التفكير فيما إذا كان تقسيمه سيمكّنك أكثر من إعادة استخدام هذا المكوِّن في سياق مختلف. يجب معالجة الحدث update من المكوِّن Todos لتشغيل وظيفة التحديث، لذا أضف معالِج الأحداث التالي في القسم <script>: function updateTodo(todo) { const i = todos.findIndex(t => t.id === todo.id) todos[i] = { ...todos[i], ...todo } } نجد المهمة todo باستخدام معرِّفها id في مصفوفة المهام todos ونحدّث محتواها باستخدام صيغة الانتشار، وقد كان بإمكاننا أيضًا استخدام todos[i‎] = todo في هذه الحالة، ولكن هذا التطبيق أفضل، مما يسمح للمكوِّن Todo بإعادة الأجزاء المُعدَّلة فقط من المهام. يجب بعد ذلك الاستماع إلى الحدث update في استدعاء المكون <Todo>، وتشغيل الدالة updateTodo()‎ عند حدوث ذلك لتغيير المتغير name والحالة completed، لذا عدّل استدعاء المكوِّن <Todo> كما يلي: {#each filterTodos(filter, todos) as todo (todo.id)} <li class="todo"> <Todo {todo} on:update={e => updateTodo(e.detail)} on:remove={e => removeTodo(e.detail)} /> </li> جرب تطبيقك مرةً أخرى وسترى أنه يمكنك حذف وإضافة وتعديل وإلغاء تعديل وتبديل حالة اكتمال المهام، وسيُعدَّل عنوان الحالة "x out of y items completed" بطريقة مناسبة عند اكتمال المهام. يُعَدّ تطبيق نمط "props-down, events-up" في إطار عمل Svelte سهلًا، ولكن يمكن أن يكون الموجّه bind اختيارًا جيدًا للمكونات البسيطة، وسيتيح لك إطار Svelte الاختيار. ملاحظة: يوفِّر إطار Svelte آليات أكثر تقدمًا لمشاركة المعلومات بين المكونات، وهي واجهة Context API والمخازن Stores، إذ توفِّر Context API آليةً للمكونات وأحفادها للتواصل مع بعضها البعض دون تمرير البيانات والدوال بوصفها خاصيات، أو إرسال الكثير من الأحداث، في حين تتيح المخازن Stores مشاركة البيانات التفاعلية بين المكونات غير المرتبطة بطريقة هرمية. يمكنك الوصول إلى نسختك من مستودعنا على النحو التالي لمعرفة حالة الشيفرة كما يجب أن تكون في نهاية هذا المقال: cd mdn-svelte-tutorial/05-advanced-concepts أو يمكنك تنزيل محتوى المجلد مباشرةً باستخدام الأمر التالي: npx degit opensas/mdn-svelte-tutorial/05-advanced-concepts تذكر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، فإذا أردت متابعتنا فابدأ بكتابة الشيفرة باستخدام الأداة REPL. الخلاصة أضفنا جميع الوظائف المطلوبة لتطبيقنا، إذ يمكننا عرض المهام وإضافتها وتعديلها وحذفها وتمييزها على أنها مكتملة وترشيحها حسب الحالة، كما غطينا في هذا المقال المواضيع التالية: استخراج وظائف مكوِّن جديد. تمرير المعلومات من المكوِّن الابن إلى المكوِّن الأب باستخدام معالج يُستقبَل بوصفه خاصيةً. تمرير المعلومات من المكوِّن الابن إلى المكوِّن الأب باستخدام الموجّه bind. عرض كتل شيفرة HTML المشروطة باستخدام كتلة if. تطبيق نمط الاتصال "props-down, events-up". إنشاء الأحداث المخصصة والاستماع إليها. سنواصل في المقال التالي من جزئية svelte من هذه السلسلة تقسيم تطبيقنا إلى مكونات ونتعرف على بعض التقنيات المتقدمة للعمل مع نموذج DOM. ترجمة -وبتصرُّف- للمقال Componentizing our Svelte app. اقرأ أيضًا التعامل مع المتغيرات والخاصيات في إطار عمل Svelte إنشاء تطبيق قائمة مهام باستعمال إطار عمل Svelte بدء استخدام إطار العمل Svelte لبناء تطبيقات ويب
  21. يتحدّث الكاتب في هذا المقال عن تجربته الشخصية في الانتقال من الأعمال الخدمية إلى العمل مع المنتجات وإطلاق منتج جديد. لماذا أردت العمل مع المنتجات؟ تلقيتُ -يقول الكاتب- عرضًا للانضمام إلى فريق سوق Tradalaxy في الشهر العاشر من سنة 2018، وعرفتُ مباشرةً أنه المكان المناسب لي، إذ لدي خبرة كبيرة في إطلاق أنواع مختلفة من الأسواق وهو شيء وجدته محفزًا وجذابًا دائمًا. كانت فرصةً أخرى للعودة إلى العمل مع إدارة المنتجات التي اشتقتُ إليها نوعًا ما، حيث كان آخر أعمالي فيها في سنة 2014 عندما عملت على تطبيق منصة إنترنت الأشياء للسيارات بما في ذلك خدمة الويب وتطبيقات الهاتف المحمول والعتاد، وكانت أول تجربة لي في العمل مع شركة ناشئة. شغلت قبل الانضمام إلى شركة Tradalaxy مناصبًا على مستوى المديرين التنفيذيين في شركات خدمات وعملت مع فرق من مختلف المتخصصين من التنفيذيين إلى مديري المبيعات والمصممين والمطورين. كان عملائي من جميع أنحاء العالم وساعدتهم على جعل أعمالهم مربحة مثل المشاريع التقنية المالية وإنترنت العملاء والأسواق وما إلى ذلك، وكان كل شيء على ما يرام لولا فكرة واحدة، وهي أنني لا أنتمي إلى هذا العالم. حصلت بعدها على فرصة للمساعدة في إنشاء المنتجات، حيث كانت لدي خبرة في الاستشارات وتطوير مشاريع العملاء، وأدركت أنني أفكر بصوت عالٍ، فقد أردت حقًا العودة لكوني مبدعًا أو مبتكرًا مرةً أخرى بدلًا من أكون منفّذًا فقط، وهنا بدت العودة إلى إدارة المنتجات خيارًا جيدًا، إذ يجب أن تستغل فرصتك في حال توفرها. كذلك، أتاح لي الانتقال إلى العمل مع أحد المنتجات الابتعاد عن الحاجة المستمرة للمساومة الموجودة في الشركات الخدمية، إذ لن يسمح النقص في الوقت بالانغماس في التفاصيل بصورة كاملة. كل مشروع للعميل لديه نقطة بداية ونهاية، فهو مثلث متساوي الأضلاع كما تنص معايير PMI، حيث تمثل الأضلاع الثلاثة لهذا المثلث الميزانية والوقت ونطاق العمل. تعزّز المواعيد النهائية تلك الفكرة، حيث تُعَد المواعيد النهائية عنصرًا ضروريًا في أي نوع من أنواع العمل، فالمشاريع التي بدون مواعيد نهائية عديمة الجدوى، إذ لن تُنجَز بدون هذه القيود الضرورية، لكن المواعيد النهائية تخلق مواقفًا تكون فيها الحلول المُقدَّمة (سواءً كانت متعلقةً بالتصميم أو البناء أو المشاكل التقنية) عبارةً عن تسويات. لا تُعَد هذه الحلول شاملةً كما تحب أن تكون، إذ تُعَد القدرة على تتبّع ما إذا كانت هذه الحلول تساعد الأعمال أم لا الاستثناء وليست القاعدة، لأن سير العمل الطبيعي يتعلق أكثر بالوفاء بالتزامك تجاه العميل لإنشاء منتج X مثلًا في الإطار الزمني Y ضمن نطاق Z. يوجد نموذج فريق مُخصَّص في الأعمال الخدمية، والذي (وإن كان جزئيًا وليس دائمًا) يسمح للفريق بالعمل من منظور طويل الأمد ومراقبة فعالية الحلول المُقدَّمة، فقد أكسبني العمل مع المنتجات القدرة الدائمة على مراقبة فعالية الحلول، وهو ما أقدره حقًا. من جهة أخرى، جذبتني احتمالية العمل مع مؤشرات الوحدات الاقتصادية التي لا يكشف عنها العملاء في الأعمال الخدمية، حيث كانت لدي رغبة في التعمق في احتياجات المستخدمين النهائيين والعمل مع مهامهم وتحدياتهم وتوقعاتهم والتحدث الفعلي معهم والتعاطف قدر الإمكان لفهم ما يزعجهم والسبب في ذلك؛ فقد رغبتُ في تجربة طرق إطلاق منتج لحساب النقطة التي يصل عندها إلى نقطة التعادل بلا ربح أو خسارة مثل نماذج أرباح التصميم وخيارات خارطة الطريق الافتراضية. سمحت لي هذه الوظيفة بالعمل عن قرب مع الفريق بكل أدواره المختلفة، وصقلتُ هذه المهارة في مناصبي الإدارية السابقة وأردتُ تطويرها وتطبيقها بصورة أكبر، فنصيب الأسد من العمل على المنتجات يأخذه التواصل الذي له الأولوية الأكبر. ويتطلب التواصل بناء وتعديل العمليات والإجراءات المثلى للأدوار المختلفة في الفريق، وقد أفادتني تجربتي السابقة في عملي كوكيل. يتضمن دوري في شركة Tradalaxy تطوير المنتجات وإدارتها. يُعَد تعريفي لمدير المنتجات مشابهًا لتعريف شركة Intercom الذي ينص على أن دور مدير المنتجات هو مزيج من تجربة المستخدم (تجربة التفاعل بالمعنى الواسع) والحلول التقنية والأعمال. يعرف مدير المنتجات ما المشكلة ولمَن ولماذا صُمِّم المنتج لحلها، ويجب أن يفهم كيفية قياس الأداء وأين سيكون المنتج بعد فترة من الوقت، ولا أتفق مع فكرة أن مدير المنتجات يُعَد مديرًا تنفيذيًا صغيرًا. لقد جرى إضفاء الطابع العاطفي على مديري المنتجات بطريقة مفرطة في وسائل الإعلام، وهناك نقص معين في فهم المسؤولية الواقعة على عاتق الرئيس التنفيذي بما في ذلك الجانب المالي. يُعَد مدير المنتجات -بالنسبة لي- وسيلة اتصال رئيسية بين المنطقتين الداخلية (مع الفريق) والخارجية (السوق وأصحاب المصلحة). ويكون مدير المنتجات مدافعًا عمّا يريده المستخدم، حيث يربط هذه المتطلبات مع الفريق ويقدّم حلولًا فعالة وأنيقة. مدير المنتجات هو اختصاص رائع يجمع بين العديد من الكفاءات في وقت واحد، حيث تتّحد مهاراته من خلال التواصل والقدرة على توصيل رسالة المنتج. العلامة التجارية ونموذجها يُعَد المنتج الجيد منتجًا له علامة تجارية رائعة، ويُرجَّح أن يشتري الناس المنتجات التي يحبونها ويفهمونها أو يرتبطون بها. لكن هناك رأي مثير للجدل إلى حدٍ ما مفاده أن المنتج ليس بأهمية العلامة التجارية، لذلك كانت إحدى مهامي الأولى في شركة Tradalaxy هي تطوير نموذج للعلامة التجارية. يجب أولًا فهم مَن نحن ومَن هو جمهورنا وكيفية التواصل، وعندها نطور هويتنا البصرية ونطبّقها من خلال قنوات محددة. لقد تضمّن تطوير نموذج العلامة التجارية التواصلَ مع الشركات المحلية لفهم احتياجاتهم وتحليل منافسينا، وإن كان ذلك بطريقة غير مباشرة. وقد اكتشفنا من خلال هذه العملية أن أفضل وصف للعلامة التجارية لشركة Tradalaxy هو أنها دليل في بحر التجارة الدولية والصادرات والقوانين، لذا أصبح النجم الذي يرشد البحارة هو الرمز والعلامة البصرية الرئيسية للشركة. أثبت هذا النموذج للعلامة التجارية أنه مثالي نظرًا للاهتمامات التجارية التي حددناها لجمهورنا المستهدف. تتمثل إحدى الاحتياجات المهيمنة لشركات التصدير في تلقي معلومات معقدة وغير واضحة حول التجارة والاقتصاد والظواهر المرتبطة بها بعبارات بسيطة وسهلة الفهم. يعجبني مفهوم البساطة الذي يحدده جيفري كلوغر Jeffrey Kluger: "شرح الأشياء المعقدة بلغة بسيطة"، وتتمثل مهمة منصة Tradalaxy في جعل التجارة الدولية أسهل. قيمة تجربة المستخدم UX لإدارة المنتجات تسمح أدوات تجربة المستخدم UX بالإدارة الفعالة لعرض قيمة المنتج أو جوهره، حيث يمكنك استخدام تجربة المستخدم لمساعدة المستخدم النهائي على رؤية قيمة المنتج وفهمه وتوظيف المنتج لإنجاز واجباته والاستمرار في استخدامه ومشاركته مع الآخرين، ويرتبط ذلك بالبحث عن جمهورك من العملاء. يُعَد تطوير العملاء والمقابلات المتعمقة ومجموعات التركيز بعض الأساليب التي سنستخدمها، ويمكن أن تنجح مكالمة زووم Zoom قصيرة واستبيان موجز واجتماع ودي على فنجان من القهوة، بالرغم من أن هذه الأساليب يمكن ألّا تبدو أنيقةً (أو لا تحتوي على مصطلح رائع يثني عليه الآخرون على الإنترنت). لقد استمتعتُ بتطبيق هذه الأساليب لأنها تعطينا فهمًا أعمق لاحتياجات عملائنا، إذ يأتي سؤال لمَن هذا المنتج في المرتبة الأولى، ثم تليه كيفية إنشائه. وتتعلق مسألة الكيفية بملاءمة المنتج ووضوحه، إذ لا يمكن أن يكون المنتج المربك مناسبًا للاستخدام المنتظم. أعتقد أن الشركات التي تبخل على تجربة المستخدم ستخسر أمام الشركات التي تعطي الأولوية لتجربة المستخدم. تتعلق تجربة المستخدم بإعطاء نقطة تركيز عاطفية في 0.02 ثانية أثناء تفكير المستخدم في منتجك، فالفوز بمعركة جذب الانتباه وجعل المنتج بديهيًا وجذابًا هو طموح أداة تجربة المستخدم، كما تتمحور تجربة المستخدم حول راحة المستخدم وجلب عمليات الشراء المتكررة والاحتفاظ بالعملاء أو معايير AARRR الأخرى، وتتعلق بتجميع مجموعة من المكونات من منظور الأعمال، مثل القوائم المنسدلة وخطوات الإعداد ومعلومات صفحة المنتج ضمن تنسيق واضح وموجز. يجب أن تبسّط تجربة المستخدم الأشياء، فسبب وجود البساطة هو إحساس المستخدم بالراحة عند التفاعل مع منتجك من خلال واجهته وقائمته البريدية والمحتوى النصي، حيث يتعلق العمل على واجهة المنتج بتوفير تجربة مستخدم مريحة تؤمن الأمان والراحة ويمكن فهمها والتنبؤ بها، وهذه هي إحدى القواعد الأولى للخدمات والمبيعات التي علّمتُها للفرق التي عملت معها. من جهة أخرى، استثمرنا بصورة كبيرة في تطوير استراتيجية اتصال لتشكيل صوتنا الذي يمثلنا بالإضافة إلى نموذج علامتنا التجارية وهويتنا المتناسقة والمميزة وتطوير عملائنا مع أبحاث السوق، فقد أنشأنا صورًا لعملائنا والتي بدورها شكلت الأساس لتصميم الواجهة وقوائم الاختبار، ثم حددنا الأشخاص الذين هم عملاؤنا فعليًا، ولكننا لا نعرفهم بعد. كذلك، جمّعنا الكلمات الرئيسية الدلالية لبناء دليل للمحتوى والعلاقات العامة PR والتسويق عبر شبكات التواصل الاجتماعي SMM، ولولا هذا النهج الشمولي، لكانت النتائج مجردة وغير واقعية. كلما زاد البحث، زادت أهمية نتائجك، ولكن الأبحاث المختصة تتطلب جهودًا متفانيةً عوضًا عن الميزانيات الضخمة، إذ لا تُعَد متابعة الاتجاهات الرائجة أو إنفاق كميات هائلة من الموارد أمرًا ضروريًا. لا تقتصر مسؤولياتي بصفتي مدير منتجات على المنتج والعلامة التجارية وتجربة المستخدم، فلدي مسؤوليات أيضًا في تصميم وتطبيق سير العمل الأمثل. أعتقد أن الخبرة الإدارية تمثل رصيدًا ضخمًا في تطوير المنتج وعمليات البناء في فرق هذا المنتج، ولدي هذه الميزة لحسن حظي، فقد كانت خبرتي السابقة في الأعمال الخدمية مفيدة جدًا. كذلك، يتعلق المنتج بالخدمة وبالقيمة المضافة التي يوفرها للمستخدم. وتعني القيمة الصفرية أن المنتج يفتقد شيئًا ما بصورة واضحة، لذا أشعر بالراحة في توجيه المنتج نحو حالة موجهة نحو تحقيق القيمة، ولدي حرية اتخاذ القرارات بناءً على رؤيتي التي تعكس ما يحتاجه المستخدمون، وأنا أتحمّل مسؤوليتها. أخيرًا، أود أن أكرر أهمية الاستثمار في البحث، فبدونه لا يمكنك بناء منتج يحتاجه العملاء فعليًا، ويمثل ذلك العلامة الرئيسية للنجاح. اعتمِد على المحترفين في حل المشاكل التي ليس لديك خبرة فيها، ولا بأس في ألّا تعرف شيئًا ما، ولكن يجب أن يعرف كل محترف أين ينتهي مجال اختصاصه. لا تتجاوز نطاق خبرتك بالتوصيات والقرارات، ولكن اعمل جيدًا ضمنها. ترجمة -وبتصرُّف- للمقال Maker vs Doer: How I Returned To Product Management) لصاحبه Taras Zherebetskyy. اقرأ أيضًا كيف تدخل إلى مجال إدارة المنتجات وتنجح فيه؟ كيفية الدخول في مجال إدارة المنتجات وإتقانه دليل إرشادي للدخول في مجال إدارة المنتجات مجموعة مصادر مهمة تساعد على دخول مجال إدارة المنتج
  22. يمكننا الآن البدء في تطوير الميزات المطلوبة لتطبيق قائمة المهام في إطار عمل Svelte بعد أن أصبح التوصيف والتنسيق جاهزًا، إذ سنستخدِم في هذا المقال المتغيرات والخاصيات Props لجعل تطبيقنا ديناميكيًا، مما يسمح بإضافة وحذف المهام ووضع علامة على المهام المكتملة وترشيحها حسب الحالة. المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية، وستحتاج طرفية مثبَّت عليها node وnpm لتصريف وبناء تطبيقك. الهدف: تعلّم وتطبيق بعض مفاهيم Svelte الأساسية مثل إنشاء المكونات وتمرير البيانات باستخدام الخاصيات وتصيير Render تعابير جافاسكربت في شيفرة HTML وتعديل حالة المكونات وتكرارها عبر القوائم. يمكن متابعة كتابة شيفرتك معنا، لذلك انسخ أولًا مستودع github -إذا لم تفعل ذلك مسبقًا- باستخدام الأمر التالي: git clone https://github.com/opensas/mdn-svelte-tutorial.git ثم يمكنك الوصول إلى حالة التطبيق الحالية من خلال تشغيل الأمر التالي: cd mdn-svelte-tutorial/03-adding-dynamic-behavior أو يمكنك تنزيل محتوى المجلد مباشرةً كما يلي: npx degit opensas/mdn-svelte-tutorial/03-adding-dynamic-behavior تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، وإذا أردت متابعتنا فابدأ بكتابة الشيفرة باستخدام الأداة REPL من هنا. التعامل مع المهام يعرِض المكوِّن Todos.svelte حاليًا شيفرة HTML ثابتةً لا تتغير، إذًا لنبدأ في جعله أكثر ديناميكيةً، إذ سنأخذ معلومات المهام من شيفرة HTML ونخزنها في المصفوفة todos، كما سننشئ متغيرين لتتبّع العدد الإجمالي للمهام والمهام المكتملة، إذ ستُمثَّل حالة المكوِّن من خلال هذه المتغيرات الثلاثة ذات المستوى الأعلى. أولًا، أنشئ قسم <script> قبل المكوِّن src/components/Todos.svelte وضَع فيه المحتوى التالي: <script> let todos = [ { id: 1, name: "Create a Svelte starter app", completed: true }, { id: 2, name: "Create your first component", completed: true }, { id: 3, name: "Complete the rest of the tutorial", completed: false } ]; let totalTodos = todos.length; let completedTodos = todos.filter((todo) => todo.completed).length; </script> لنبدأ بعد ذلك بإظهار رسالة الحالة، لذا ابحث عن العنوان <h2> الذي له المعرِّف id بالقيمة list-heading واستبدل العدد الثابت للمهام النشطة والمكتملة بتعابير ديناميكية كما يلي: <h2 id="list-heading">{completedTodos} out of {totalTodos} items completed</h2> انتقل إلى التطبيق وسترى الرسالة "‎2 out of 3 items completed" كما كانت سابقًا، ولكن تأتي المعلومات هذه المرة من المصفوفة todos. أخيرًا، يمكن إثبات ذلك من خلال الانتقال إلى تلك المصفوفة ثم محاولة تغيير بعض قيم الخاصية المكتملة completed لكائن المهمة، ويمكنك إضافة كائن مهمة جديد أيضًا، ولاحظ كيف تُحدَّث الأعداد في الرسالة بطريقة مناسبة. إنشاء مهام من بيانات يدخلها المستخدم تُعَدّ عناصر المهام المعروضة ثابتةً حاليًا، ونريد تكرار كل عنصر في المصفوفة todos وتصيير Render شيفرة HTML لكل مهمة. ليس لدى لغة HTML طريقة للتعبير عن المنطق مثل التعابير الشرطية والحلقات، ولكن إطار عمل Svelte يمكنه ذلك من خلال استخدام الموجّه {‎#each...‎} للتكرار عبر المصفوفة todos. يتضمن المعامِل الثاني -إذا كان موجودًا- فهرس العنصر الحالي، كما يمكن توفير تعبير مفتاحي يحدد كل عنصر بطريقة فريدة، وسيستخدِمه إطار Svelte لمعرفة الاختلاف في القائمة عند تغيير البيانات بدلًا من إضافة العناصر أو إزالتها في النهاية، ويُعَدّ تحديد عنصر مفتاحي دائمًا من الممارسات الجيدة، كما يمكن توفير كتلة ‎:else التي ستُصيَّر عندما تكون القائمة فارغةً. أولًا، استبدل العنصر <ul> الحالي بالإصدار المبسط التالي لتفهم كيفية العمل: <ul> {#each todos as todo, index (todo.id)} <li> <input type="checkbox" checked={todo.completed}/> {index}. {todo.name} (id: {todo.id}) </li> {:else} Nothing to do here! {/each} </ul> ثانيًا، ارجع إلى التطبيق وسترى شيئًا يشبه ما يلي: رأينا الآن أنّّ كل شيء يعمل جيدًا، فلننشئ عنصر مهمة مكتملة مع كل حلقة للموجّه {‎#each}، ونضمّن فيها المعلومات من المصفوفة todos مثل id وname وcompleted واستبدل كتلة <ul> الحالية بما يلي: <!-- Todos --> <ul role="list" class="todo-list stack-large" aria-labelledby="list-heading"> {#each todos as todo (todo.id)} <li class="todo"> <div class="stack-small"> <div class="c-cb"> <input type="checkbox" id="todo-{todo.id}" checked={todo.completed}/> <label for="todo-{todo.id}" class="todo-label"> {todo.name} </label> </div> <div class="btn-group"> <button type="button" class="btn"> Edit <span class="visually-hidden">{todo.name}</span> </button> <button type="button" class="btn btn__danger"> Delete <span class="visually-hidden">{todo.name}</span> </button> </div> </div> </li> {:else} <li>Nothing to do here!</li> {/each} </ul> لاحظ كيفية استخدام الأقواس المعقوصة لتضمين تعابير جافاسكربت ضمن سمات HTML كما فعلنا مع السمتين checked و id لمربع الاختيار. حوّلنا بذلك شيفرة HTML الثابتة إلى قالب ديناميكي جاهز لعرض المهام من حالة المكوِّن. التعامل مع الخاصيات لا يُعَدّ المكوِّن Todos مفيدًا جدًا بوجود قائمة مهام ثابتة، إذ يمكن تحويل المكون إلى محرّر مهام للأغراض العامة من خلال السماح لأب هذا المكون بالمرور على قائمة المهام لتعديلها، مما يسمح بحفظها في خدمة ويب أو في التخزين المحلي واستعادتها لاحقًا لتحديثها، لذلك لنحوّل المصفوفة إلى خاصية prop. أولًا، استبدل كتلة let todos = ...‎ الموجودة مسبقًا في Todos.svelte بالتعليمة التالية: export let todos = [] يمكن أن يبدو هذا غريبًا بعض الشيء في البداية، فهذه ليست الطريقة التي تعمل بها تعليمة export في وحدات جافاسكربت، وإنما هي الطريقة التي يوسّع بها إطار عمل Svelte شيفرة جافاسكربت من خلال استخدام صيغة صالحة لهدف جديد. يستخدِم إطار عمل Svelte في حالتنا الكلمة export لتمييز التصريح عن متغير بوصفه خاصية Property أو Prop، مما يعني أنه يصبح في متناول مستخدِمي المكوِّن، كما يمكنك تحديد قيمة أولية افتراضية للخاصية التي تُستخدَم إذا لم يحدد مستخدِم المكون الخاصية الخاصة بالمكوِّن أو إذا كانت قيمتها الأولية غير محدَّدة عند إنشاء نسخة من المكوِّن، لذا نخبر إطار Svelte من خلال التعليمة export let todos = []‎ أنّ المكوِّن Todos.svelte سيقبل السمة todos والتي ستُهيَّأ إلى مصفوفة فارغة عند حذفها. ثانيًا، ألقِ نظرةً على التطبيق وسترى الرسالة "Nothing to do here!‎" لأننا لا نمرّر حاليًا أيّ قيمة إليه من المكوِّن App.svelte، لذلك سيستخدِم القيمة الافتراضية. ثالثًا، لننقل مصفوفة مهامنا todos إلى المكوِّن App.svelte ولنمرّرها إلى المكوِّن Todos.svelte بوصفها خاصيةً، لذا عدِّل المكوِّن src/App.svelte كما يلي: <script> import Todos from "./components/Todos.svelte"; let todos = [ { id: 1, name: "Create a Svelte starter app", completed: true }, { id: 2, name: "Create your first component", completed: true }, { id: 3, name: "Complete the rest of the tutorial", completed: false } ]; </script> <Todos todos={todos} /> أخيرًا، يسمح إطار عمل Svelte بتحديد المتغير بوصفه اختصارًا عندما يكون للسمة والمتغير الاسم نفسه، ويمكننا بذلك إعادة كتابة السطر الأخير كما يلي: <Todos {todos} /> يجب أن تُصيَّر المهام كما كانت سابقًا باستثناء أننا نمرّرها الآن من المكوِّن App.svelte. تبديل وإزالة المهام سنضيف الآن بعض الوظائف لتبديل حالة المهمة، إذ يحتوي إطار Svelte على الموجّه on:eventname للاستماع إلى أحداث DOM، لذا أضِف معالجًا إلى الحدث on:click الخاص بمربع الاختيار لتبديل القيمة المكتملة. أولًا، عدّل العنصر <input type="checkbox"‎> في المكوِّن src/components/Todos.svelte كما يلي: <input type="checkbox" id="todo-{todo.id}" on:click={() => todo.completed = !todo.completed} checked={todo.completed} /> ثانيًا، سنضيف دالة لإزالة مهمة من المصفوفة todos، لذا أضف الدالة removeTodo()‎ في الجزء السفلي من القسم <script> في المكوِّن Todos.svelte كما يلي: function removeTodo(todo) { todos = todos.filter((t) => t.id !== todo.id) } سنستدعي بعد ذلك هذه الدالة باستخدام زر "الحذف Delete"، لذا عدّلها باستخدام الحدث click كما يلي: <button type="button" class="btn btn__danger" on:click={() => removeTodo(todo)} > Delete <span class="visually-hidden">{todo.name}</span> </button> يُعَدّ تمرير نتيجة تنفيذ دالة بوصفها معالجًا بدلًا من تمرير الدالة من الأخطاء الشائعة جدًا في المعالجات في إطار Svelte، فإذا حدّدت on:click={removeTodo(todo)}‎ مثلًا، فستُنفَّذ الدالة removeTodo(todo)‎ وستُمرَّر النتيجة بوصفها معالِجًا، لذا يجب تحديد on:click={() => removeTodo(todo)}‎ على أساس معالج، فإذا لم تأخذ الدالة removeTodo()‎ أيّ معامِل، فيمكنك استخدام on:event={removeTodo}‎، ولكن لا يمكنك استخدام on:event={removeTodo()}‎، كما أنّ ليس هذا الشكل صيغةً خاصةً من Svelte، وإنما استخدمنا دوال جافاسكربت السهمية Arrow Functions العادية. يمكننا الآن حذف المهام، إذ تُزال المهام ذات الصلة من المصفوفة todos عند الضغط على زر حذف عنصر المهمة، وتُحدَّث واجهة المستخدِم لعدم إظهاره لاحقًا، كما يمكننا الآن تحديد مربعات الاختيار، وستُحدَّث الحالة المكتملة للمهام ذات الصلة في المصفوفة todos، لكن لا يُحدَّث العنوان "x out of y items completed". المهام التفاعلية يعرِف إطار Svelte كيفية تحديث واجهة المستخدِم في كل مرة تُعدَّل فيها قيمة متغير المستوى الأعلى للمكوِّن، حيث تُعدَّل في تطبيقنا قيمة المصفوفة todos مباشرةً في كل مرة تُبدَّل أو تُحذَف فيها المهام المطلوبة، وبالتالي سيحدّث إطار Svelte نموذج DOM تلقائيًا. لا ينطبق الأمر نفسه على المتغيرين totalTodos وcompletedTodos، إذ يُسنَد إليهما قيمة عند إنشاء نسخة من المكوِّن وينفَّذ السكربت في الشيفرة التالية، ولكن لا تُعدَّل قيمتهما بعد ذلك: let totalTodos = todos.length let completedTodos = todos.filter((todo) => todo.completed).length يمكننا إعادة حسابهما بعد تبديل المهام وإزالتها، ولكن هناك طريقة أسهل لذلك، حيث نخبر إطار Svelte بأننا نريد أن يكون المتغيران totalTodos و completedTodos تفاعليين من خلال جعلهما مسبوقين بالرمز :$‎، إذ سينشئ إطار Svelte الشيفرة لتحديثهما تلقائيًا كلما تغيرت البيانات التي يعتمدان عليها. ملاحظة: يستخدِم إطار Svelte صيغة تعليمة تسمية جافاسكربت :$‎ لتمييز التعليمات التفاعلية مثل الكلمة export المستخدَمة للتصريح عن الخاصيات، إذ يُعَد هذا المثال مثالًا آخرًا يستفيد فيه إطار Svelte من صيغة جافاسكربت صالحة مع إعطائها هدفًا جديدًا، وهو في هذه الحالة "إعادة تشغيل هذه الشيفرة كلما تغيرت أيّ من القيم المشار إليها". عدِّل تعريف المتغيرين totalTodos وcompletedTodos ضمن الملف src/components/Todos.svelte لتبدو كما يلي: $: totalTodos = todos.length $: completedTodos = todos.filter((todo) => todo.completed).length إذا فحصت تطبيقك الآن، فسترى تحديث أرقام العناوين عند اكتمال المهام أو حذفها. يحلّل مصرِّف Svelte الشيفرة لإنشاء شجرة اعتماديات، ثم ينشئ شيفرة جافاسكربت لإعادة تقييم كل تعليمة تفاعلية كلما حُدِّثت إحدى اعتمادياتها، كما تُطبَّق التفاعلية في Svelte بطريقة خفيفة الوزن وفعالة دون استخدام المستمعِين Listeners أو التوابع الجالبة Getters أو الضابطة Setters أو أيّ آلية معقدة أخرى. إضافة مهام جديدة يجب الآن إضافة بعض الوظائف لإضافة مهام جديدة. أولًا، سننشئ متغيرًا للاحتفاظ بنص المهام الجديدة، لذا أضف التصريح التالي إلى القسم <script> في الملف Todos.svelte: let newTodoName = '' سنستخدِم الآن هذه القيمة في العنصر <input> لإضافة مهام جديدة، وسنحتاج ربط المتغير newTodoName بدخل todo-0، بحيث تبقى قيمة المتغير newTodoName متزامنةً مع الخاصية value الخاصة بالعنصر <input> كما يلي: <input value={newTodoName} on:keydown={(e) => newTodoName = e.target.value} /> كلما تغيرت قيمة المتغير newTodoName، فسينتقل هذا التغيير إلى السمة value الخاصة بحقل الإدخال، وكلما ضُغِط على مفتاح في حقل الإدخال، فسنحدّث محتويات المتغير newTodoName، إذ يُعَدّ ذلك تطبيقًا يدويًا لربط البيانات ثنائي الاتجاه لحقل الإدخال، لكننا لسنا بحاجة لهذه الآلية، إذ يوفِّر إطار Svelte طريقةً أسهل لربط أيّ خاصية بمتغير باستخدام الموجّه bind:property كما يلي: <input bind:value={newTodoName} /> إذًا لنعدّل حقل الإدخال todo-0 كما يلي: <input bind:value={newTodoName} type="text" id="todo-0" autocomplete="off" class="input input__lg" /> يمكن اختبار نجاح هذه الطريقة من خلال إضافة تعليمة تفاعلية لتسجيل محتويات المتغير newTodoName، لذا أضف مقتطف الشيفرة التالي في نهاية القسم <script>: $: console.log('newTodoName: ', newTodoName) ملاحظة: لا تقتصر التعليمات التفاعلية على التصريح عن المتغيرات، إذ يمكنك وضع أيّ تعليمة جافاسكربت بعد الرمز :$‎. ارجع الآن إلى المضيف المحلي localhost:5042 واضغط على الاختصار Ctrl + Shift + K لفتح طرفية المتصفح واكتب شيئًا ما في حقل الإدخال، ويجب أن ترى إدخالاتك مسجلةً، كما يمكنك الآن حذف التابع console.log()‎ التفاعلي إذا رغبت في ذلك. سننشئ بعد ذلك دالةً لإضافة مهمة جديدة وهي الدالة addTodo()‎ التي ستدفع كائن todo جديد إلى المصفوفة todos، لذا أضف ما يلي إلى الجزء السفلي من كتلة <script> ضمن الملف src/components/Todos.svelte: function addTodo() { todos.push({ id: 999, name: newTodoName, completed: false }) newTodoName = '' } ملاحظة: سنسنِد حاليًا المعرِّف id نفسه لكل مهمة، ولكن لا تقلق إذ سنصلح ذلك لاحقًا. نريد الآن تحديث ملف HTML لاستدعاء الدالة addTodo()‎ كلما أُرسِل النموذج، لذا عدّل وسم فتح النموذج NewTodo كما يلي: <form on:submit|preventDefault={addTodo}> يدعم الموجّه on:eventname إضافة مُعدِّلات إلى حدث DOM باستخدام المحرف |، حيث يخبر المُعدِّل preventDefault إطار Svelte بإنشاء شيفرة لاستدعاء التابع event.preventDefault()‎ قبل تشغيل المعالج. إذا حاولت إضافة مهام جديدة، فستُضاف هذه المهام الجديدة إلى المصفوفة todos، ولكن لن تُحدَّث واجهة المستخدِم، وتذكَّر أنه في إطار Svelte يبدأ التفاعل باستخدام الإسنادات، وهذا يعني تنفيذ الدالة addTodo()‎ وإضافة عنصر إلى المصفوفة todos، ولكن لن يكتشف إطار Svelte أن تابع الدفع قد عدّل المصفوفة، وبذلك لن يحدّث مهام العنصر <ul>، كما ستؤدي إضافة todos = todos إلى نهاية الدالة addTodo()‎ إلى حل هذه المشكلة، ولكن يبدو تضمين ذلك في نهاية الدالة أمرًا غريبًا، لذلك سنأخذ التابع push()‎ مع استخدام صيغة الانتشار Spread Syntax لتحقيق النتيجة نفسها، إذ سنسند قيمة إلى المصفوفة todos تساوي المصفوفة todos بالإضافة إلى الكائن الجديد. ملاحظة: المصفوفة Array لديها العديد من العمليات المتغيرة مثل push()‎ و pop()‎ و splice()‎ و shift()‎ و unshift()‎ و reverse()‎ و sort()‎ التي يمكن أن يتسبب استخدامها في حدوث آثار جانبية وأخطاء يصعب تتبعها، لذا نتجنب تغيّر المصفوفة باستخدام صيغة الانتشار بدلًا من التابع push()‎، ويُعَدّ ذلك من الممارسات جيدة. عدّل الدالة addTodo()‎ كما يلي: function addTodo() { todos = [...todos, { id: 999, name: newTodoName, completed: false }] newTodoName = '' } إعطاء كل مهمة معرفا فريدا إذا حاولت إضافة مهام جديدة في تطبيقك الآن، فستتمكن من إضافة مهام جديدة وستظهر في واجهة المستخدِم أيضًا، ولكن إذا جربته مرةً ثانية، فلن يعمل، وستتلقى رسالة تقول "Error: Cannot have duplicate keys in a keyed each"، أي نحتاج إلى معرِّفات فريدة لمهامنا. لنصرّح أولًا عن المتغير newTodoId يُحسَب من عدد المهام زائد 1، ولنجعله تفاعليًا، لذا أضِف مقتطف الشيفرة التالي إلى القسم <script>: let newTodoId $: { if (totalTodos === 0) { newTodoId = 1; } else { newTodoId = Math.max(...todos.map((t) => t.id)) + 1; } } ملاحظة: لا تقتصر التعليمات التفاعلية على سطر واحد One-liners، كما يمكن استخدام التعليمة التفاعلية الآتية: ‎$: newTodoId = totalTodos ? Math.max(...todos.map(t => t.id)) + 1 : 1 والتي تُعَدّ مفيدةً أيضًا، ولكنه أقل قابليةً للقراءة. يحلّل المصرِّف التعليمة التفاعلية بأكملها، ويكتشف أنها تعتمد على المتغير totalTodos والمصفوفة todos. لذا كلما عُدِّل أيّ منهما، فسيُعاد تقييم هذه الشيفرة وتحديث newTodoId وفقًا لذلك، ولنستخدِم ذلك في الدالة addTodo()‎، ولنعدّلها كما يلي: function addTodo() { todos = [...todos, { id: newTodoId, name: newTodoName, completed: false }] newTodoName = '' } ترشيح المهام حسب الحالة لنطبّق الآن القدرة على ترشيح مهامنا حسب الحالة، لذا سننشئ متغيرًا للاحتفاظ بالمرشِّح الحالي، ودالة مساعدة ستعيد المهام المُرشَّحة. أولًا، أضف ما يلي في الجزء السفلي من القسم <script>: let filter = 'all' const filterTodos = (filter, todos) => filter === 'active' ? todos.filter((t) => !t.completed) : filter === 'completed' ? todos.filter((t) => t.completed) : todos نستخدِم المتغير filter للتحكم في مرشّح جميع المهام all أو المهام النشطة active أو المكتملة completed، إذ سيؤدي إسناد إحدى هذه القيم إلى المتغير filter إلى تفعيل المرشح وتحديث قائمة المهام، إذ ستتلقى الدالة filterTodos()‎ المرشِّح الحالي وقائمة المهام وستعيد مصفوفةً جديدةً من المهام المُرشَّحة وفقًا لذلك. لنحدّث بعد ذلك شيفرة HTML الخاصة بزر الترشيح لجعله ديناميكيًا ولنحدّث المرشِّح الحالي عندما يضغط المستخدِّم على أحد أزرار الترشيح كما يلي: <div class="filters btn-group stack-exception"> <button class="btn toggle-btn" class:btn__primary={filter === 'all'} aria-pressed={filter === 'all'} on:click={()=> filter = 'all'} > <span class="visually-hidden">Show</span> <span>All</span> <span class="visually-hidden">tasks</span> </button> <button class="btn toggle-btn" class:btn__primary={filter === 'active'} aria-pressed={filter === 'active'} on:click={()=> filter = 'active'} > <span class="visually-hidden">Show</span> <span>Active</span> <span class="visually-hidden">tasks</span> </button> <button class="btn toggle-btn" class:btn__primary={filter === 'completed'} aria-pressed={filter === 'completed'} on:click={()=> filter = 'completed'} > <span class="visually-hidden">Show</span> <span>Completed</span> <span class="visually-hidden">tasks</span> </button> </div> سنعرِض المرشِّح الحالي من خلال تطبيق الصنف btn__primary على زر ترشيح المهام النشطة، ويمكنك تطبيق أصناف تنسيق CSS شرطيًا على عنصر من خلال استخدام الموجِّه class:name={value}‎، فإذا قُيِّمت عبارة القيمة على أنها صحيحة، فسيُطبَّق اسم الصنف، كما يمكنك إضافة العديد من هذه الموجّهات بشروط مختلفة إلى العنصر نفسه، لذلك إذا كانت التعليمة class:btn__primary={filter === 'all'}‎، فسيطبّق إطار Svelte الصنف btn__primary إذا كان المرشح يساوي جميع المهام all. ملاحظة: يوفِّر إطار العمل Svelte اختصارًا يتيح لنا إمكانية اختصار <div class:active={active}‎> إلى <div class:active> عندما يتطابق الصنف Class مع اسم المتغير. يحدث شيء مشابه مع aria-pressed={filter === 'all'}‎ عند تقييم تعبير جافاسكربت الممرَّر بين الأقواس المعقوصة إلى قيمة صحيحة، حيث ستُضاف السمة aria-pressed إلى الزر، وبالتالي سنحدِّث متغير filter باستخدام class:btn__primary={filter === 'all'}‎ كلما نقرنا على الزر. يجب الآن استخدام الدالة المساعدة في حلقة {‎#each} كما يلي: ... <ul role="list" class="todo-list stack-large" aria-labelledby="list-heading"> {#each filterTodos(filter, todos) as todo (todo.id)} ... يكتشف إطار Svelte بعد تحليل شيفرتنا أنّ الدالة filterTodos()‎ تعتمد على المتغيرين filter و todos، وكلما تغيرت أيّ من هذه الاعتماديات، فسيُحدَّث نموذج DOM وفقًا لذلك، لذا كلما تغيَّر المتغيران filter وtodos، فسيُعاد تقييم الدالة filterTodos()‎ وستُحدَّث العناصر الموجودة ضمن الحلقة. ملاحظة: يمكن أن تكون التفاعلية خادعةً في بعض الأحيان، إذ يتعرّف إطار Svelte على المتغير filter بوصفه اعتماديةً لأننا نشير إليه في التعبيرfilterTodos(filter, todo)‎، إذ يُعَدّ المتغير filter متغيرًا من المستوى الأعلى، لذلك يمكن إزالته من معامِلات الدالة المساعدة واستدعائه بالشكل: filterTodos(todo)‎، كما يمكن أن ينجح هذا الأمر، ولكن ليس لدى إطار Svelte الآن طريقةً لمعرفة أنّ ‎{‎#each filterTodos(todos)... }‎ يعتمد على المتغير filter، ولن تُحدَّث قائمة المهام المُرشَّحة عندما يتغير المرشّح، وتذكَّر دائمًا أنّ إطار Svelte يحلِّل الشيفرة لاكتشاف الاعتماديات، لذلك يُفضَّل أن تكون صريحًا بشأنه وألّا تعتمد على رؤية متغيرات المستوى الأعلى، كما يُعَدّ جعل الشيفرة واضحةً وصريحةً بشأن المعلومات التي تستخدِمها من الممارسات الجيدة. يمكنك الوصول إلى نسختك من مستودعنا على النحو التالي لمعرفة حالة الشيفرة كما يجب أن تكون في نهاية هذا المقال: cd mdn-svelte-tutorial/04-componentizing-our-app أو يمكنك تنزيل محتوى المجلد مباشرةً باستخدام الأمر التالي: npx degit opensas/mdn-svelte-tutorial/04-componentizing-our-app تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، فإذا أردت متابعتنا، فابدأ بكتابة الشيفرة باستخدام الأداة REPL. الخلاصة طبّقنا في هذا المقال معظم الوظائف المطلوبة، إذ يمكن لتطبيقنا عرض وإضافة وحذف المهام وتبديل حالتها المكتملة وإظهار عدد المهام المكتملة وتطبيق المرشحات، حيث غطينا المواضيع التالية: إنشاء واستخدام المكونات. تحويل شيفرة HTML الثابتة إلى قالب حي. تضمين تعابير جافاسكربت في شيفرة HTML. التكرار على القوائم باستخدام الموجّه {‎#each}. تمرير المعلومات بين المكونات باستخدام الخاصيات. الاستماع إلى أحداث DOM. التصريح عن التعليمات التفاعلية. تنقيح الأخطاء الأساسي باستخدام التابع console.log()‎ والتعليمات التفاعلية. ربط خاصيات HTML بالموجّه bind:property. بدء التفاعل باستخدام الإسنادات. استخدام العبارات التفاعلية لترشيح البيانات. التعريف الصريح عن الاعتماديات التفاعلية. سنضيف مزيدًا من الوظائف التي ستسمح للمستخدِمين بتعديل المهام في المقال التالي. ترجمة -وبتصرُّف- للمقال Dynamic behavior in Svelte: working with variables and props. اقرأ أيضًا بدء استخدام إطار العمل Svelte لبناء تطبيقات ويب إنشاء تطبيق قائمة مهام باستعمال إطار عمل Svelte
  23. يمكننا الآن البدء في إنشاء تطبيقنا مثل تطبيق قائمة المهام بعد أن فهمنا الأمور الأساسية في إطار عمل Svelte في المقال السابق، إذ سنلقي في هذا المقال نظرةً على الوظائف المطلوبة لتطبيقنا أولًا ثم سننشئ المكوِّن Todos.svelte وسنضع شيفرة HTML وشيفرة التنسيق الثابتة في مكانها، وبالتالي سيصبح كل شيء جاهزًا لبدء تطوير ميزات تطبيق قائمة المهام التي سننتقل إليها في المقالات اللاحقة. نريد أن يتمكن المستخدِمون من تصفح المهام وإضافتها وحذفها ووضع علامة عليها بوصفها مكتملةً، كما سنلقي نظرةً على بعض المفاهيم الأكثر تقدمًا. المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML و CSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية، وستحتاج طرفية مثبَّت عليها node وnpm لتصريف وبناء تطبيقك. الهدف: معرفة كيفية إنشاء مكوِّن Svelte وتصييره في مكوِّن آخر وتمرير البيانات إليه باستخدام الخاصيات Props وحفظ حالته. يمكن متابعة كتابة شيفرتك معنا، لذلك انسخ أولًا مستودع github -إذا لم تفعل ذلك مسبقًا- باستخدام الأمر التالي: git clone https://github.com/opensas/mdn-svelte-tutorial.git ثم يمكنك الوصول إلى حالة التطبيق الحالية من تشغيل الأمر التالي: cd mdn-svelte-tutorial/02-starting-our-todo-app أو يمكنك تنزيل محتوى المجلد مباشرةً كما يلي: npx degit opensas/mdn-svelte-tutorial/02-starting-our-todo-app تذكَّر تشغيل الأمر التالي لبدء تشغيل تطبيقك في وضع التطوير: npm install && npm run dev فإذا أردت متابعتنا فابدأ بكتابة الشيفرة باستخدام أداة REPL. ميزات تطبيق قائمة المهام سيبدو تطبيق قائمة المهام كما يلي بمجرد أن يصبح جاهزًا: سيتمكّن المستخدِم من تطبيق الأمور التالية باستخدام واجهة المستخدِم: تصفح المهام. وضع علامة على المهام المكتملة أو تعليقها دون حذفها. إزالة المهام. إضافة مهام جديدة. ترشيح المهام حسب الحالة: جميع المهام أو المهام النشطة أو المهام المكتملة. تعديل المهام. وضع علامة على جميع المهام بوصفها نشطةً أو مكتملةً. إزالة جميع المهام المكتملة. إنشاء المكون الأول لننشئ المكوِّن Todos.svelte الذي سيحتوي على قائمة المهام. أولًا، أنشئ مجلدًا جديدًا بالاسم src/components. ملاحظة: يمكنك وضع مكوناتك في أيّ مكان ضمن المجلد src، ولكن المجلد components هو اصطلاح معروف يجب اتباعه، مما يسمح لك بالعثور على مكوناتك بسهولة. ثانيًا، أنشئ ملفًا بالاسم src/components/Todos.svelte بحيث يحوي ما يلي: <h1>Svelte To-Do list</h1> ثالثًا، عدّل العنصر title في الملف public/index.html ليحتوي على النص "Svelte To-do list" كما يلي: <title>Svelte To-Do list</title> رابعًا، افتح الملف src/App.svelte واستبدل محتوياته بما يلي: <script> import Todos from './components/Todos.svelte' </script> <Todos /> سيصدر إطار عمل Svelte في وضع التطوير تحذيرًا في طرفية المتصفح عند تحديد خاصية غير موجودة في المكوِّن مثل تحديد الخاصية name عند إنشاء نسخة من المكون App ضمن الملف src/main.js، إذ لا تُستخدَم هذه الخاصية ضمن المكوِّن App، كما يجب أن تعطيك الطرفية حاليًا رسالة مثل الرسالة "‎ was created with unknown prop 'name'‎"، لكن يمكنك حل هذه المشكلة من خلال إزالة الخاصية name من src/main.js ويجب أن يبدو الآن كما يلي: import App from './App.svelte' const app = new App({ target: document.body }) export default app إذا تحققت من عنوان URL لخادم الاختبار، فسترى تصيير المكوِّن Todos.svelte كما يلي: إضافة شيفرة HTML الثابتة سنبدأ أولًا بتمثيل شيفرة HTML لتطبيقنا لتتمكّن من رؤية الشكل الذي سيبدو عليه، لذا انسخ والصق ما يلي في ملف المكوِّن Todos.svelte ليحل محل المحتوى الموجود مسبقًا: <!-- Todos.svelte --> <div class="todoapp stack-large"> <!-- NewTodo --> <form> <h2 class="label-wrapper"> <label for="todo-0" class="label__lg"> What needs to be done? </label> </h2> <input type="text" id="todo-0" autocomplete="off" class="input input__lg" /> <button type="submit" disabled="" class="btn btn__primary btn__lg"> Add </button> </form> <!-- Filter --> <div class="filters btn-group stack-exception"> <button class="btn toggle-btn" aria-pressed="true"> <span class="visually-hidden">Show</span> <span>All</span> <span class="visually-hidden">tasks</span> </button> <button class="btn toggle-btn" aria-pressed="false"> <span class="visually-hidden">Show</span> <span>Active</span> <span class="visually-hidden">tasks</span> </button> <button class="btn toggle-btn" aria-pressed="false"> <span class="visually-hidden">Show</span> <span>Completed</span> <span class="visually-hidden">tasks</span> </button> </div> <!-- TodosStatus --> <h2 id="list-heading">2 out of 3 items completed</h2> <!-- Todos --> <ul role="list" class="todo-list stack-large" aria-labelledby="list-heading"> <!-- todo-1 (editing mode) --> <li class="todo"> <div class="stack-small"> <form class="stack-small"> <div class="form-group"> <label for="todo-1" class="todo-label"> New name for 'Create a Svelte starter app' </label> <input type="text" id="todo-1" autocomplete="off" class="todo-text" /> </div> <div class="btn-group"> <button class="btn todo-cancel" type="button"> Cancel <span class="visually-hidden">renaming Create a Svelte starter app</span> </button> <button class="btn btn__primary todo-edit" type="submit"> Save <span class="visually-hidden">new name for Create a Svelte starter app</span> </button> </div> </form> </div> </li> <!-- todo-2 --> <li class="todo"> <div class="stack-small"> <div class="c-cb"> <input type="checkbox" id="todo-2" checked/> <label for="todo-2" class="todo-label"> Create your first component </label> </div> <div class="btn-group"> <button type="button" class="btn"> Edit <span class="visually-hidden">Create your first component</span> </button> <button type="button" class="btn btn__danger"> Delete <span class="visually-hidden">Create your first component</span> </button> </div> </div> </li> <!-- todo-3 --> <li class="todo"> <div class="stack-small"> <div class="c-cb"> <input type="checkbox" id="todo-3" /> <label for="todo-3" class="todo-label"> Complete the rest of the tutorial </label> </div> <div class="btn-group"> <button type="button" class="btn"> Edit <span class="visually-hidden">Complete the rest of the tutorial</span> </button> <button type="button" class="btn btn__danger"> Delete <span class="visually-hidden">Complete the rest of the tutorial</span> </button> </div> </div> </li> </ul> <hr /> <!-- MoreActions --> <div class="btn-group"> <button type="button" class="btn btn__primary">Check all</button> <button type="button" class="btn btn__primary">Remove completed</button> </div> </div> تحقّق من الخرج المُصيَّر مرةً أخرى، وسترى شيئًا يشبه ما يلي: يُعَدّ تنسيق شيفرة HTML السابق ليس جيدًا كما أنه غير مفيد وظيفيًا، ولكن لنلقِ نظرةً على الشيفرة ونرى مدى ارتباطها بالميزات التي نرغب بها: تسمية أو عنوان Label ومربع نص لإدخال مهام جديدة. ثلاثة أزرار لترشيح المهام حسب حالتها. تسمية label توضّح العدد الإجمالي للمهام والمهام المكتملة. قائمة غير مرتبة تحتوي على عنصر قائمة لكل مهمة. يحتوي عنصر القائمة عند تعديل المهمة على حقل إدخال وزرَين لإلغاء التعديلات أو حفظها. إذا لم تكن المهمة قيد التعديل، فهناك مربع اختيار لضبط حالة المهمة المكتملة وزرَين لتعديل المهمة أو حذفها. يوجد زران لتحديد أو إلغاء تحديد جميع المهام وإزالة المهام المكتملة. سنعمل في المقالات اللاحقة على تشغيل جميع هذه الميزات. ميزات سهولة الوصول Accessibility لقائمة المهام لاحظ وجود بعض السمات غير المعتادة مثل: <button class="btn toggle-btn" aria-pressed="true"> <span class="visually-hidden">Show</span> <span>All</span> <span class="visually-hidden">tasks</span> </button> تخبر السمة aria-pressed التقنيات المساعدة مثل قارئات الشاشة أنّ الزر يمكن أن يكون في إحدى الحالتين: pressed أو unpressed مثل القول بأن الزر في وضع التشغيل أو الإيقاف، ويعني ضبط القيمة true أنّ الزر مضغوط افتراضيًا. ليس للصنف visually-hidden أيّ تأثير حتى الآن، لأننا لم نضمّن أيّ ملف CSS، وسيُخفَى أيّ عنصر موجود في هذا الصنف عن المستخدِمين المبصرين وسيظل متاحًا لمستخدِمي قارئات الشاشة بمجرد أن نضع التنسيق في مكانه، لأن هذه الكلمات لا يحتاجها المستخدِمون المبصرون، وإنما تُستخدَم لتقديم مزيد من المعلومات حول ما يفعله الزر لمستخدِمي قارئات الشاشة الذين ليس لديهم القدرة البصرية لمساعدتهم. كما يمكنك العثور على عنصر <ul> التالي: <ul role="list" className="todo-list stack-large" aria-labelledby="list-heading"> تساعد السمة role التقنيات المساعدة في توضيح نوع القيمة الدلالية للعنصر أو ما هو الغرض منه، إذ يُعامَل العنصر <ul> بوصفه قائمةً افتراضيًا، ولكن ستؤدي التنسيقات التي نريد إضافتها إلى تعطيل هذه الوظيفة، ولكن سيعيد هذا الدور معنى القائمة إلى العنصر <ul>. تخبر السمة aria-labelledby التقنيات المساعدة بأننا نتعامل مع العنصر <h2> مع معرِّف id عنوان القائمة list-heading بوصفه التسمية التي تشرح الغرض من القائمة الموجودة تحتها، إذ يعطي هذا الارتباط القائمةَ سياقًا مفيدًا، مما يساعد مستخدِمي قارئات الشاشة على فهم الغرض منها بصورة أفضل. دعم إطار عمل Svelte لسهولة الوصول يركّز إطار Svelte على سهولة الوصول أو الشمولية Accessibility، والهدف هو تشجيع المطورين على كتابة المزيد من الشيفرة البرمجية الشاملة افتراضيًا، وبما أنّ إطار Svelte يُعَدّ مصرِّفًا، فيمكنه تحليل قوالب HTML بطريقة ساكنة لتوفير تحذيرات متعلقة بسهولة الوصول عند تصريف المكونات. لا يُعَدّ عملية تطبيق مبادئ سهولة الوصول -التي تُختصَر إلى a11y- أمرًا سهلًا دائمًا، ولكن سيساعدك إطار Svelte من خلال تحذيرك إذا كتبت شيفرة HTML لا تراعي تلك المبادئ، فإذا أضفنا العنصر <img> مثلًا إلى المكوِّن todos.svelte بدون الخاصية alt المقابلة له كما يلي: <h1>Svelte To-Do list</h1> <img height="32" width="88" src="https://www.w3.org/WAI/wcag2A" /> فسيعطي المصرِّف التحذير التالي: (!) Plugin svelte: A11y: <img> element should have an alt attribute src/components/Todos.svelte 1: <h1>Svelte To-Do list</h1> 2: 3: <img height="32" width="88" src="https://www.w3.org/WAI/wcag2A"> ^ created public/build/bundle.js in 220ms [2020-07-15 04:07:43] waiting for changes… كما يمكن لمحرر الشيفرة عرض هذا التحذير حتى قبل استدعاء المصرِّف كما يلي: يمكنك إخبار إطار عمل Svelte بتجاهل هذا التحذير للكتلة التالية من شيفرة HTML بتعليق يبدأ بعبارة svelte-ignore كما يلي: <!-- svelte-ignore a11y-missing-attribute --> <img height="32" width="88" src="https://www.w3.org/WAI/wcag2A"> ملاحظة: يمكنك باستخدام المحرّر VSCode إضافة تعليق التجاهل هذا تلقائيًا بالنقر على الرابط "Quick fix…‎" أو بالضغط على الاختصار Ctrl + .‎. إذا أردت تعطيل هذا التحذير، فيمكنك إضافة المعالج onwarn إلى الملف rollup.config.js ضمن إعداد الإضافة Svelte كما يلي: plugins: [ svelte({ dev: !production, css: css => { css.write('public/build/bundle.css'); }, // Warnings are normally passed straight to Rollup. You can // optionally handle them here, for example to squelch // warnings with a particular code onwarn: (warning, handler) => { // e.g. I don't care about screen readers -> please DON'T DO THIS!!! if (warning.code === 'a11y-missing-attribute') return; // let Rollup handle all other warnings normally handler(warning); } }), ... ] تُنفَّذ هذه التحذيرات في المصرِّف نفسه حسب التصميم وليس على أساس إضافة يمكن أن تختار إضافتها إلى مشروعك، كما تكمن الفكرة في التحقق من وجود مشاكل سهولة الوصول a11y في الشيفرة افتراضيًا والسماح بإلغاء تحذيرات معينة. ملاحظة: لا يجب تعطيل هذه التحذيرات إلا إذا كانت لديك أسباب وجيهة لذلك مثل تعطيلها أثناء إنشاء نموذج أولي prototype سريع، إذ يجب أن تجعل صفحاتك قابلةً للوصول إلى أوسع قاعدة ممكنة من المستخدِمين. قواعد الشمولية التي تحقق منها إطار عمل Svelte مأخوذة من الإضافة eslint-plugin-jsx-a11y، وهي إضافة من ESLint توفِّر فحوصات ساكنة للعديد من قواعد سهولة الوصول على عناصر JSX، كما يهدف إطار Svelte إلى تنفيذ كل من هذه القواعد في مصرِّفه، وقد نُقِل معظمها إلى Svelte فعليًا، بالإضافة إلى أنه يمكنك على GitHub معرفة فحوصات الشمولية التي لا تزال مفقودة، ويمكنك التحقق من معنى كل قاعدة من خلال النقر على رابطها الخاص. تنسيق التطبيق لنجعل قائمة المهام تبدو أفضل قليلًا، لذا استبدل محتويات الملف public/global.css بما يلي: /* RESETS */ *, *::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; } } /*END RESETS*/ /* GLOBAL STYLES */ .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__primary:disabled { color: darkgrey; background-color:#565656; } .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; } /* END GLOBAL STYLES */ .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 item styles */ .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; } /* CHECKBOX STYLES */ .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; } يبدو كل شيء الآن أفضل كما يلي: يمكنك الوصول إلى نسختك من مستودعنا على النحو التالي لمعرفة حالة الشيفرة، كما يجب أن تكون في نهاية هذا المقال: cd mdn-svelte-tutorial/03-adding-dynamic-behavior أو يمكنك تنزيل محتوى المجلد مباشرةً باستخدام الأمر التالي: npx degit opensas/mdn-svelte-tutorial/03-adding-dynamic-behavior تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، فإذا أردت متابعتنا، فابدأ بكتابة الشيفرة باستخدام الأداة REPL. الخلاصة بدأ تطبيق قائمة المهام في التبلور مع تطبيق شيفرة HTML وشيفرة التنسيق CSS، وأصبح كل شيء جاهزًا لنتمكّن من التركيز على الميزات التي يجب تطبيقها. ترجمة -وبتصرُّف- للمقال Starting our Svelte to-do list app. اقرأ أيضًا المقال السابق: بدء استخدام إطار العمل Svelte لبناء تطبيقات ويب إنشاء تطبيق قائمة مهام باستخدام React إنشاء تطبيق Todo List بسيط باستخدام Laravel 5 - الجزء الأول
  24. لا توجد وصفة مثالية لتطوير منتج ناجح، ولكن يُعَد امتلاك فكرةٍ مجرد بداية تساعدك على اكتشاف ملاءمة السوق وخصائص المنتج. يكون تطبيق هذه الفكرة عمليةً صعبة يمكن أن تخفق بسهولة، حيث يمتد التطوير على مدى فترة زمنية طويلة مع ألوف العوامل لتحويل الفكرة الأولية. يتواجد مدير المنتجات في مركز كل ذلك، فمهمة مدير المنتجات هي إبقاء الجميع في حالة تزامن أثناء تطور فكرة المنتج، بحيث تفهم جميع الفرق المنتجَ بالطريقة نفسها. يمكن تحقيق ذلك بمساعدة وثيقة استراتيجية تسمى خارطة طريق المنتج Product Roadmap. سنحدد في هذا المقال دور مدير المنتجات في تطوير المنتج وخريطة طريق المنتج وميزاتها الرئيسية، وسنتعرّف على أنواع خرائط الطريق الشائعة وأمثلة عنها وبعض النصائح حول إنشائها. من هو مدير المنتجات؟ سنقدّم لك أولًا مخططًا سريعًا لمهام مدير المنتجات الذي يبني خرائط الطريق لفهم خصائصها، حيث تكون المهام الرئيسية لمدير المنتجات هي: تحليل السوق والمنافسين. التواصل مع العملاء. تطوير رؤية المنتج وتخطيطه واستراتيجيته. تقدير العمل وتحديد أولوياته. إنشاء خارطة طريق المنتج. مشاركة خرائط الطريق عبر المؤسسة. الالتزام بخارطة طريق المنتج. يتمثل الجزء الأساسي من وظيفة مدير المنتجات في اكتشاف ما سيكون عليه المنتج وإخبار الجميع عنه من خلال خارطة طريق المنتج. الفرق بين مدير المنتجات ومالك المنتج لا بد أنك تتساءل عن الفرق بين مدير المنتجات Product Manager -أو PM اختصارًا- ومالك المنتج Product Owner -أو PO اختصارًا، إذ يتعامل كلاهما مع تفاصيل المنتج. يكون مالك المنتج PO مسؤولًا عن قيمة المنتج وأعماله المتراكمة وقصص المستخدمين، والشيء الرئيسي الذي يجب تذكره هو أنه لا يوجد دور لمالك المنتج PO خارج مشروع سكروم Scrum. وفي المقابل، يُعَد مدير المنتجات مهنةً، ويمكن أن يكون مدير المنتجات مالكًا للمنتج ضمن فريق سكروم. يقدّم مدير المنتجات PM خارطة طريق للمنتج مع الرؤية والاستراتيجية، بينما يعمل مالك المنتج PO على أعمال المنتج المتراكمة ويحدد المتطلبات التجارية والتقنية. ما هي خارطة طريق المنتج؟ خارطة طريق المنتج هي وثيقة استراتيجية عالية المستوى ترسم المراحل العامة لتطوير المنتج، والغرض الرئيسي منها هو ربط رؤية المنتج بأهداف أعمال الشركة. مثال عن خارطة طريق المنتج. المصدر: Roadmunk تنشأ خارطة طريق المنتج بوصفها نتيجةً للتخطيط الاستراتيجي، وتوثّق كلًا من الاستراتيجية التنفيذية وأهداف المنتج العامة. تتضمن خارطة طريق المنتج الاستراتيجية النقاط الرئيسية التالية: رؤية المنتج: تمثل ما تريد أن يصبح منتجك عليه في المستقبل. الاستراتيجية: خطة تنفيذ توضح بالتفصيل ما ستفعله شركتك لتحقيق الرؤية. الهدف: هدف محدد زمنيًا يمكن قياسه بمقياس معين. المبادرة: موضوعات واسعة توحّد الميزات التي يجب تطبيقها لتحقيق الهدف. الميزة: جزء فعلي من منتج يكون إما جزءًا من وظيفة أو تطبيقًا تابعًا لجهة خارجية. الأطر الزمنية: المواعيد أو الفترات الزمنية لهدف أو ميزة معينة يجب الانتهاء منها، حيث تقترح خارطة طريق المنتج مواعيدًا زمنية تقريبية كقاعدة عامة. علامات الحالة: تُستخدَم لتتبّع تقدّم العمل. المقاييس: المساعدة في قياس الأهداف الموجَّهة بالبيانات مثل معدل النفور Churn Rate أو حركة الزوار الطبيعية Organic Traffic. مخطط عناصر تخطيط المنتج يمكن أن يختلف عدد الفرق المشاركة في خرائط الطريق اعتمادًا على المنتج الذي تطوره والمنهجيات التي تمارسها، حيث سيكون لديك في أغلب الأحيان فريق الهندسة وتجربة المستخدم UX والمبيعات والتسويق وفريق الدعم والفريق التشغيلي والمصممين وفريق الاختبار، وتمثل هذه الفرق الأشخاص الذين سيعملون على المنتج الفعلي. يجب أن تكون خارطة طريق أي منتج واضحةً وسهلة الفهم، مما يساعد مدير المنتجات في توجيه جميع الفرق خلال عملية التطوير بما يتماشى مع احتياجات العملاء وأهداف العمل. لذا تكون خارطة طريق المنتج مفيدة وقابلة للتطبيق عندما تلبي بالمتطلبات التالية: تنقل استراتيجية تطوير المنتج. تُظهِر رؤية المنتج. تتطور وتتغير حسب المنتج ومتطلبات السوق. تعطي الأولوية لوحدات التطوير عالية المستوى. تعمل بوصفها أداة اتصال بين جميع الأشخاص المعنيين. تحدد الأطر الزمنية طويلة الأمد. تحدد الأهداف الدقيقة وتربطها بأهداف العمل. يبني مدير المنتجات في بعض الأحيان خرائط طريق متعددة من أنواع مختلفة لتقديم المعلومات إلى أصحاب المصلحة الداخليين والخارجيين. كيف تنشئ خارطة طريق المنتج؟ يُعَد التمسك بالممارسات العامة والمُجرَّبة جيدًا أمرًا منطقيًا عند التفكير في عملية متعددة الخطوات مثل إعداد خارطة طريق. إذًا إليك بعض النصائح العملية لإنشائها. صغ استراتيجيتك ورؤيتك للمنتج تحدَّث إلى أصحاب المصلحة الداخليين والخارجيين وإلى عملائك، وانظر إلى السوق ومنافسيك. حدد شخصيات عملائك، وأنصِت إلى ما يقوله مندوبو المبيعات لك، وتحدث إلى العملاء أنفسهم، ووفّر معلومات تمثل صوت العميل لفريق الإنتاج والإدارة. ستكون لديك بيانات الإدخال المطلوبة لبدء العمل على خارطة طريقك بمجرد ملاءمة الرؤية مع جميع أصحاب المصلحة والمشاركين. حدد جمهورك لا تُعَد خارطة طريق المنتج خطةً ذات حجم واحد يناسب الجميع، إذ سيكون الجمهور الذي يجب عليك تقديم خارطة الطريق إليه عاملًا مُحدَّدًا مسبقًا لشكل خارطة الطريق ونوعها والمحتويات التي يجب تضمينها فيها. يُعَد تحديد نوع خارطة الطريق أمرًا معقدًا، وسنفصّل ذلك في القسم التالي. اختر تنسيقا مناسبا يؤثر التنسيق على اختيارك للمحتويات، حيث يمكن أن يكون التنسيق الذي تختاره أكثر ملاءمةً لجمهور معين، فمثلًا، لا يكون التنسيق المستند إلى الميزات مناسبًا لقسم التسويق أو الإدارة، ولكن يُوصَى به لفريقك الهندسي. سيقترح التنسيق المختار عناصر المعلومات الضرورية التي يجب تمييزها والمواضيع أو الأهداف التي يجب تحديدها حسب الأولوية في الجدول الزمني. اختر المقاييس ولائمها مع الميزات الفعلية ستساعدك المقاييس على رؤية صورة أوسع وقياس تقدمك، ويمكنك اختيار المقاييس الموجهة لاحتياجات العملاء أو احتياجات العمل اعتمادًا على الغرض من خارطة الطريق. يمكنك تحليل السوق ومنافسيك أو اللجوء إلى محلل المجال كمصدر للمقاييس ذات الصلة. استخدم أدوات محددة لخارطة الطريق يمكن أن يكون استخدام أداة مثل إكسل Excel لبناء خارطة طريق عمليةً صعبة، إذ ستحصل على عرض تقديمي ثابت يصعب تحديثه إلى حدٍ ما. تتيح لك أدوات خرائط الطريق المستندة إلى السحابة تسريع العملية والحفاظ على خارطة الطريق مُحدَّثةً عندما تتغير الأولويات. إليك بعض الأدوات التي يمكنك استخدامها: OpenProject: هي أداة خارطة طريق مجانية تتيح لك إنشاء مشاريع غير محدودة ضمن ملف تعريف مستخدم واحد، وهي برنامج إدارة منتجات مفتوح المصدر مصمَّم لتلبية احتياجات فرق سكروم/أجايل Agile/Scrum. Roadmap Planner: هي أداة أخرى مفتوحة المصدر لإدارة المنتجات لنظام لينكس Linux. تُعَد الأداة ProductPlan الأكثر شيوعًا من بين هذه الأدوات، حيث تستخدمه كبرى الشركات الرقمية مثل ويندوز Windows وأدوبي Adobe. تشارك الأداة ProductPlan عددًا كبيرًا من قوالب خرائط الطريق لأغراض مختلفة، ويمكنك استيراد عناصر من نظام جيرا Jira أو جداول Spreadsheets أو VSTS، مما يجعل عملية التخطيط أسهل بكثير. Aha!‎: هي شركة عملاقة أخرى تستخدمها شركات شاترستوك Shutterstock ولينكد إن LinkedIn وديل Dell، حيث تمثل قائمة متكاملة مع التطبيقات الأخرى. Roadmunk: من أفضل تطبيقات إدارة المنتجات التي تلبي جميع المعايير اللازمة مع أسعار مناسبة. أداة أنا من حسوب: وهي أداة عربية يمكن استخدامها لإدارة مشاريعك وفريق عملك عن بعد، حيث يمكنك باستخدامها بناء خرائط ذاتية حسب ميولات كل شخص ومتطلبات المشروع. حافظ على معلومات محدثة وعالية المستوى يجب التركيز على توفير الرؤية العامة والاستراتيجية دون التركيز على الأساليب للحفاظ على وظائف خارطة الطريق الاستراتيجية. تُعَد عناصر معلوماتك الثانوية قيّمةً، ولكن خارطة طريقك هي وثيقة استراتيجية يجب أن تكون واضحة وسهلة الفهم. لذا يجب تجنّب الإفراط في التفاصيل أو تضمين الكثير من المعلومات غير الضرورية. يجب التفاعل بديناميكية مع تغييرات خارطة طريق المنتج، حيث سيجلب تقدّم منتجك ميزات وأهدافًا جديدة، لذا يجب تحديث خارطة طريق المنتج باستمرار لتتبعها ونقل المعلومات إلى بقية أصحاب المصلحة، مما يعني تطورًا تدريجيًا مع المنتج. أنواع جمهور خارطة طريق المنتج لخارطة الطريق -كما المنتج- جمهورها المُستهدَف. ترتبط مجموعات مختلفة من الأشخاص بالمنتج، لذلك يجب التواصل مع تلك المجموعات باستخدام معلومات مختلفة ودقيقة. سيخبرك عامل الجمهور بنوع المحتوى المراد تضمينه وشكله ومدى تفصيله، لذا يمكنك -بصفتك مدير منتجات- إما إنشاء خرائط طريق متعددة لكل مجموعة من الأشخاص أو إنشاء مستند استراتيجي واحد للجميع (وهي حالة نادرة). لنلقِ نظرةً على أنواع الجمهور الذي يمكن إنشاء خارطة طريقك من أجله. أنواع الجمهور في خرائط الطريق يمكن أن يكون جمهور خارطة طريق المنتج داخليًا مثل فريقك والمديرين التنفيذيين، أو خارجيًا مثل العملاء والمستثمرين. تنقل خرائط الطريق الداخلية المعلومات التي يطلبها كل قسم، لذلك يجب اقتراح البيانات المناسبة للأشخاص المناسبين. وتكون خرائط الطريق الداخلية مُخصَّصةً للمديرين التنفيذيين وفريق الإنتاج والمبيعات. تتطلّب المجموعة التنفيذية نظرةً أكثر استراتيجيةً على البيانات، لذلك يجب أن تركّز خارطة الطريق على الرؤية والأهداف الاستراتيجية والجداول الزمنية وأرقام السوق وغير ذلك، بينما يركز فريق الإنتاج على الجوانب التخطيطية والمواعيد النهائية وتفاصيل التطبيق التقنية، حيث يجب أن تنقل خارطة الطريق معلومات منخفضة المستوى بناءً على أجزاء المنتج أو مواضيعه أو ميزاته الفعلية لتحقيق قيمة حقيقية لفريق الإنتاج. يهتم فريق المبيعات بمجموعة ميزات المنتج وفوائده للعملاء، لذا يجب أن يركز هذا النوع من خرائط الطريق على قيمة المنتج، إذ يُعَد التنسيق المستند إلى موضوعٍ ما هو الأنسب، حيث يمكن للمواضيع إظهار الهدف الذي تحققه كل ميزة بيانيًا. تحتوي خرائط الطريق الخارجية على تنسيق يشبه العرض التقديمي لأنها لا تشارك أي معلومات محددة حول العمليات الداخلية. يجب أن تكون خرائط الطريق الخارجية سهلة الفهم وواضحة بصريًا وتشارك أكبر قدر من المعلومات حول الفوائد التي تعود على العملاء. لا تحتوي خرائط الطريق التي تُشارَك مع الجمهور على أي مواعيد نهائية في أغلب الأحيان، بل تقدّم أطرًا زمنية تقريبية وتتالٍ من إصدارات الميزات. أنواع خارطة طريق المنتج وأمثلة عنها تختلف خرائط طريق المنتج من مشروع لآخر، وذلك لأنها يمكن أن تكون مصممةً لنقل أنواع مختلفة من البيانات أو تتبع منطق مختلف، وبذلك سيختلف شكلها وبنيتها. صنّف بريان لاولي Brian Lawley في كتابه "Expert Product Development" خرائط الطريق للأغراض العامة إلى الأنواع التالية: خارطة طريق الاستراتيجية والسوق: تتعامل مع التفاصيل عالية المستوى وحالة السوق. خارطة الطريق ذات الرؤية: تحدد رؤية المنتج. خارطة الطريق التقنية: مختلفة تمامًا عن النوعين السابقين، وهي خارطة طريق تقنية منخفضة المستوى لفريق الإنتاج. خارطة الطريق التقنية الخاصة بالمنتج: مزيج من التقنيات أو الميزات الفعلية المُخطَّطة للمنتج أو لمجموعة المنتجات. خارطة طريق المنصة: موجَّهة إلى المنتجات الرقمية متعددة المنصات. خارطة طريق المنتج الداخلية والخارجية: مرتبطة بأنواع مختلفة من الجمهور. لكن تنوع يكون خرائط الطريق في العالم الحقيقي يكون أوسع بكثير بين مستخدمي أجايل Agile والشركات التقنية الرقمية. لنلقِ نظرة الآن على بعض الأنواع الشائعة من خرائط الطريق مع وضع الجمهور بوصفه عاملًا أساسيًا في الحسبان. تصف خارطة الطريق الآن-التالي-لاحقًا Now-Next-Later المهام/الفترات الزمنية السريعة Sprints/الميزات بطريقة مرتبة حسب الأولوية، وهي نسخة مبسطة من أعمال المنتج المتراكمة التي تصنّف عناصر المعلومات أفقيًا وشاقوليًا. تعرض هذه الخارطة ما سيصدر الآن وما سيُعَد تاليًا وما سيصدر لاحقًا، والغرض منها هو إظهار الأولويات بأبسط طريقة ممكنة. خارطة طريق الآن-التالي-لاحقًا. المصدر: Scrum تساعد خارطة طريق المنتج swimlane في توضيح تفاصيل المشروع الأساسية، مع تبيان تقسيمات الأعمال أقسام المشروع، موضحةً المسؤول عن كل عملية مع المرحلة التي وصل إليها بالتنفيذ والنسبة المئوية لمعدل اكتمال العمل لديه. مثال عن خارطة طريق المنتج swimlane. المصدر: Roadmunk تساعد خارطة Business Development Roadmap في إبقاء جميع المعلومات مُجمَّعة ومُفسَّرة بوضوح، إذ تحدد الأهداف سبب وجود كل ميزة، ويمكن تحديد الهدف بكلمات بسيطة مثل "زيادة مشاركة المستخدمين" أو "تسريع عملية التسجيل". يمكنك الحفاظ على خارطة طريق عالية المستوى وعلى استراتيجيتك ورؤيتك سهلة الفهم من خلال تنظيم المعلومات حول الأهداف. مثال عن خارطة الطريق المستندة إلى الموضوع من موقع Roadmunk تستخدم خارطة الطريق المستندة إلى الميزات Feature Roadmap الميزات بوصفها نقطة مركزية لخارطة الطريق، مما يجعلها مُفصَّلة جدًا، ولكن لها بعض العيوب هي: لا تُعَد الميزة وحدًة مستقرةً بالنظر إلى السوق المتغير، إذ تتسبب الابتكارات التقنية واحتياجات العملاء في تغيير مجموعة ميزاتك في كثير من الأحيان. لا يوفر التنسيق المستند إلى الميزات تفاصيلًا عالية المستوى، مما يؤدي إلى تشويش الرؤية العامة للمنتج ويجعل فهم خارطة الطريق والحفاظ عليها أمرًا صعبًا بصورة أكبر. مثال عن خارطة الطريق المستندة إلى الميزات. المصدر: Roadmunk خارطة طريق الإستراتيجية Strategy Roadmap هي خارطة طريق للأغراض العامة، ويمكن أن تتضمن أي نوع من المعلومات وتكون مناسبةً لكل من الجمهور الداخلي والخارجي؛ كما تُعَد مخططًا عالي المستوى لمعلومات المنتج العامة المرتبطة بجانب معين اعتمادًا على الغرض منه. مثال عن خارطة طريق الاستراتيجية. المصدر: blog.aha.io خرائط الطريق التقنية Technology Roadmap أو خرائط طريق تقانة المعلومات IT Roadmaps هي وثائق ذات مستوى منخفض تُنشَأ عادةً لدعم خارطة طريق الاستراتيجية الرئيسية، وتُستخدَم للفرق الداخلية لصياغة المتطلبات التقنية. تحدد خرائط الطريق التقنية استخدامَ تقنية معينة، وتساعد في تخصيص الموارد التي تعتمد عليها. مثال عن خارطة طريق تقنية من موقع Roadmunk تُعَد خارطة طريق الإصدار Release Roadmap مثالًا عن خارطة طريق خارجية مُقدَّمة للعملاء. يمثل هذا النوع الإصدارات الرئيسية لوظائف التطبيق للاستخدام العام، لذلك لا تحتاج إلى كثير من التفاصيل التقنية أو العملية. خارطة طريق جدول الإصدار الزمني لأصحاب المصلحة الخارجيين من موقع Roadmunk خارطة طريق السوق Market Roadmap هي مستند يمكن استخدامه عند التخطيط لإطلاق المنتج عبر أسواق متعددة، وهي مُطوَّرة لتمكين قسم التسويق وأصحاب المصلحة الداخليين من تخطيط استراتيجية التسويق لمنتج واحد أو منتجات متعددة. يمكن أن تكون خرائط طريق السوق خرائط الطريق الأكثر ديناميكيةً، حيث يجب عليها التقاط التغيّرات السريعة في السوق، ويمكن أن يتسبب المنافس أو التقدم التقني في تحولات كبيرة تتطلب تعديل الاستراتيجية. تتضمن خارطة طريق السوق ثلاثة أو أربعة عناصر، فنادرًا ما توزع الشركات منتجاتها على عدد كبير من الأسواق. قوالب خارطة طريق المنتج قد لا يكون الفهم الأساسي لنوع خارطة طريق المنتج كافيًا إذا أردت إنشاء خارطة طريقك، ولكن يمكن استخدام جميع الأمثلة السابقة كمرجع لك. سنوفر الآن بعض القوالب التي يمكن استخدامها أو مشاركتها مع جمهورك، وأبسط مثال هو قالب جدول البيانات التالي الذي يمثل خارطة طريق مستندة إلى الموضوع: 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; } Q1 2018 المحتوى التسويق الرقمي العلامة التجارية الفعاليات والعلاقات العامة المدير البحث في المجتمعات المستهدفة كلمات البحث المفتاحية تحديد عرض القيمة المُقترَح مشاركات العروض التقديمية نشر الوظائف إعداد القنوات الاجتماعية إعداد لوحات التحكم بناء صفحات الهبوط استراتيجية العلاقات الإعلامية تقييم المكدس بناء تقويم المحتوى الزمني نشرة أخبار الإطلاق Q2 2018 إطلاق المدونة تحسين إعلانات AdWords إنشاء دليل التنسيق مؤتمر الخطة في تموز/يوليو توظيف مدير تسويق رقمي زيادة حركة الزوار الطبيعية تحسين الكلمات المفتاحية إنشاء شعار العلامة التجارية حجز قاعة لسلسلة المناسبات الصيفية إعداد الميزانية تكامل الشبكات الاجتماعية إرسال دعوات للمتحدثين تطبيق النشر المشترك على منصات متعددة Q3 2018 جمع منشورات الزوار التكامل مع خدمة MailChimp إعادة تصميم الموقع تنظيم سوق رقمي من الدرجة الأولى توظيف مدير العلامة التجارية تحسين الروابط الخلفية اختبار أ/ب للخصم البريدي إنشاء دليل العلامة التجارية توظيف مدير المحتوى تقليل معدل التخلي عن عربة التسوق توسيع قسم التوظيف إعداد الميزانية يقدّم موفرو برامج إنشاء خرائط الطريق قوالبًا مجانية يمكن استخدامها أثناء الفترات التجريبية أو تنزيلها مثل: قوالب Roadmunk. قوالب Aha!‎: متاحة بعد تسجيل الدخول فقط. قوالب ProductPlan. قوالب Miro: متوفرة بعد تسجيل الدخول فقط. قوالب Venngage. بينما إذا أردت قالبًا يمكن تنزيله، فيمكنك استخدام أحد القوالب الآتية، حيث يُعَد تحديث ملفات خرائط الطريق المُضمَّنة أمرًا صعبًا وتتطلب تسجيلًا، ولكن يمكن مشاركتها مجانًا: قوالب Office Timeline القابلة للتنزيل. قوالب TemplateLAB القابلة للتنزيل. قوالب UseFyi القابلة للتنزيل. المستندات الداعمة لخارطة الطريق لا يزال يتعين على خارطة طريق المنتج بأي شكل من أشكالها أن تحافظ على نقاط المعلومات الأساسية العامة. يمكن تجنب إضافة معلومات منخفضة المستوى إلى المستند من خلال استخدام أداتين أخريين تدعمان خارطة الطريق الاستراتيجية، فعلى سبيل المثال، تُعَد أعمال المنتج المتراكمة Product Backlog أداةً من نموذج سكروم Scrum مع قائمة بالمتطلبات والميزات عالية المستوى، حيث ينشئها مالكو المنتجات وتتكون من قصص المستخدمين. وتُعَد أعمال المنتج المتراكمة هي قائمة مهام تحدد تطوير المنتج على المستوى التخطيطي. خطة الإصدار Release Plan هي وثيقة تحدد مواعيد إصدار صارمة، حيث يحدد مديرو المنتجات الأطر الزمنية بين الإصدارات. تظهِر خارطة طريق المنتج تعاقب إصدارات المنتج، بينما تقدم خطة الإصدار مواعيدًا أدق لميزة معينة يجب إصدارها، كما يمكن مطابقة الفترات الزمنية السريعة في عملية التطوير مع ميزات معينة أو إصلاحات الأخطاء. هل خارطة طريق المنتج ضرورية؟ لا بد أنك الآن تتساءل عمّا إذا كان بناء خارطة طريق للمنتج أمرًا ضروريًا. حسنًا، يتطلب إنشاء أي مستند بمفرده كثيرًا من الجهد، وسيقضي مدير المنتجات وقتًا في جمع بيانات الإدخال من أصحاب المصلحة وفريق المنتجات، لكن خرائط الطريق تمنح -التي يُحافَظ عليها وعلى تنسيقها بصورة صحيحة- فرقَك وصولًا سهلًا إلى المعلومات الاستراتيجية، وبالتالي تُعَد أداةً مفيدة. إن ساعدتك خارطة الطريق في تحقيق أهدافك الإنتاجية، فاستخدمها؛ لكن إذا استغرق الأمر وقتًا أطول في البناء والتوزيع أو تطلّب تعديل خارطة الطريق تحديث مستندات متعددة في كل مرة، فيمكنك الاستغناء عنها. إذا كان تحديد أولويات المهام وتحديد مواعيد نهائية صارمة أمرًا صعبًا، فهناك تقنيات أكثر ملاءمة في إدارة المنتجات مثل تخطيط قصة المستخدم، وضع في بالك تقنيات تحديد أولويات التراكم العامة المستخدمة في أجايل، لأن المنتجات هي عبارة عن أعمال متراكمة بطريقة أو بأخرى. ترجمة -وبتصرُّف- للمقال Product Roadmap: Key Features, Types, Building Tips, and Roadmap Examples. اقرأ أيضًا مدخل مبسط إلى عالم إدارة المنتجات كيفية الدخول في مجال إدارة المنتجات وإتقانه مجموعة مصادر مهمة تساعد على دخول مجال إدارة المنتج مقارنة بين مدير المنتج ومدير المشروع
  25. سنقدّم في هذا المقال مقدمةً سريعةً عن إطار عمل Svelte، إذ سنرى كيفية عمله وما يميزه عن باقي أطر العمل وأدواته، ثم سنتعلم كيفية إعداد بيئة التطوير وإنشاء تطبيق ويب بسيط وفهم بنية المشروع ومعرفة كيفية تشغيله محليًا وإنشائه للإنتاج. المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية، إذ يُعَد إطار العمل Svelte مصرِّفًا Compiler ينشئ شيفرة جافاسكربت مُصغَّرةً ومُحسَّنةً من شيفرتنا البرمجية، لذا ستحتاج إلى طرفية مثبَّت عليها node و npm لتصريف وبناء تطبيقك. الهدف: إعداد بيئة تطوير Svelte محلية وإنشاء وبناء تطبيق بسيط وفهم أساسيات كيفية عمله. إطار عمل Svelte: طريقة جديدة لبناء واجهات المستخدم يوفِّر إطار العمل Svelte نهجًا مختلفًا لبناء تطبيقات الويب عن بعض أطر العمل الأخرى التي تحدثنا عنها في هذه السلسلة تعلم تطوير الويب مثل Ember أو Vue.js، إذ تطبّق أطر العمل مثل React أو Vue.js الجزء الأكبر من عملها في متصفح المستخدِم أثناء تشغيل التطبيق، بينما ينقل إطار Svelte العمل إلى خطوة التصريف التي لا تحدث إلا عند بناء تطبيقك، مما ينتج عنه شيفرة مُحسَّنة باستخدام لغة جافاسكربت الصرفة Vanilla JavaScript، كما ينتج عن هذا النهج حزم تطبيقات أصغر وأداء أفضل، بالإضافة إلى تجربة مطور أسهل للأشخاص الذين لديهم خبرة محدودة في النظام البيئي المجتمعي للأدوات الحديثة. يلتزم إطار عمل Svelte بنموذج تطوير الويب الكلاسيكي باستخدام اللغات HTML و CSS و JS مع إضافة بعض الامتدادات إلى HTML وجافاسكربت، إذ يمكن القول أنه لديه مفاهيم وأدوات أقل للتعلم من خيارات أطر العمل الأخرى، لكن تتمثل عيوب Svelte الرئيسية الحالية في أنه إطار عمل جديد، وبالتالي فإن نظامه البيئي محدود أكثر من الأطر الأقدم من حيث الأدوات والدعم والإضافات وأنماط الاستخدام الواضحة وما إلى ذلك، كما أنّ هناك فرص عمل أقل متعلقة به، لذلك يجب أن تكون مزاياه كافيةً للاهتمام باستخدامه. ملاحظة: أضاف إطار Svelte مؤخرًا دعم لغة TypeScript الرسمي، وهو أحد أكثر الميزات المطلوبة. حالات الاستخدام يمكن استخدام إطار عمل Svelte لتطوير أجزاء صغيرة من واجهة أو تطبيقات كاملة، إذ يمكنك إما البدء من نقطة الصفر والسماح لإطار عمل Svelte بتشغيل واجهة المستخدِم أو يمكنك دمجه مع تطبيق موجود مسبقًا، كما يُعَدّ إطار Svelte مناسبًا لمعالجة المواقف التالية: تطبيقات الويب المخصصة للأجهزة ذات الإمكانات المنخفضة: تتميز التطبيقات المُنشَأة باستخدام إطار عمل Svelte بأحجام حزم أصغر، وهي مثالية للأجهزة ذات اتصالات الشبكة البطيئة وقوة المعالجة المحدودة، إذ يؤدي استخدام شيفرة برمجية أقل إلى استخدام كيلوبايتات أقل لتنزيلها وتحليلها وتنفيذها والاستمرار في التنقل ضمن الذاكرة بسلاسة. الصفحات التفاعلية جدًا أو ذات المؤثرات البصرية المعقدة: إذا أردت بناء مؤثرات البيانات البصرية التي تحتاج لعرض عدد كبير من عناصر نموذج DOM، فستضمن مكاسب الأداء التي تأتي من إطار عمل بدون تكاليف تشغيل إضافية أن تكون تفاعلات المستخدِم ذات استجابة سريعة. تأهيل الأشخاص ذوي المعرفة الأساسية بتطوير الويب: يتمتع إطار Svelte بمنحنى تعليمي سطحي، إذ يمكن لمطوري الويب الذين لديهم معرفة أساسية بلغات HTML و CSS وجافاسكربت استيعاب تفاصيل إطار Svelte بسهولة في وقت قصير والبدء في إنشاء تطبيقات الويب. يساعد إطار عمل Sapper الذي يعتمد على إطار عمل Svelte في تطوير تطبيقات ذات ميزات متقدمة مثل التصيير من طرف الخادم Server-side Rendering وتقسيم الشيفرة والتوجيه المستند إلى الملفات والدعم دون الاتصال بالإنترنت، وهناك إطار عمل Svelte Native الذي يتيح بناء تطبيقات هاتف محمول أصيلة Native. كيفية عمل إطار عمل Svelte يمكن لإطار عمل Svelte توسيع لغات HTML و CSS وجافاسكربت نظرًا لكونه مصرِّفًا، مما يؤدي إلى إنشاء شيفرة جافاسكربت مثالية دون أيّ تكاليف تشغيل إضافية، إذ يوسّع إطار عمل Svelte تقنيات الويب الصرفة بالطرق التالية: يوسّع لغة HTML عن طريق السماح بتعابير جافاسكربت في شيفرة التوصيف وتوفير الموجّهات لاستخدام الشروط والحلقات بطريقة تشبه لغة Handlebars. يوسّع لغة CSS عن طريق إضافة آلية تحديد نطاق، مما يسمح لكل مكوِّن بتحديد تنسيقه الخاص دون التعرض لخطر التعارض مع تنسيق المكونات الأخرى. يوسّع لغة جافاسكربت من خلال إعادة تفسير موجّهات محددة للغة لتحقيق تفاعل حقيقي وتسهيل إدارة حالة المكوِّن. يتدخل المصرِّف فقط في مواقف محددة للغاية وفي سياق مكونات Svelte، كما تُعَدّ الامتدادات في لغة جافاسكربت قليلةً وتُنتَقى بعناية بهدف عدم تغيير صيغة جافاسكربت أو إبعاد المطورين، إذ ستعمل باستخدام لغة جافاسكربت الصرفة Vanilla JavaScript في أغلب الأحيان. الخطوات الأولى لاستخدام إطار Svelte لا يمكنك إضافة الوسم <script src="svelte.js"‎> إلى صفحتك واستيرادها إلى تطبيقك فقط، إذ سيتعين عليك إعداد بيئة التطوير للسماح للمصرِّف بتطبيق عمله. المتطلبات يجب تثبيت Node.js للعمل مع إطار عمل Svelte، إذ يوصَى باستخدام إصدار الدعم طويل الأمد LTS، كما يتضمن Node مدير الحزم npm ومشغّل الحزم npx. لاحظ أنه يمكنك استخدام مدير الحزم Yarn بدلًا من npm، لكننا سنفترض أنك تستخدِم npm في هذا المقال، ويمكنك مراجعة مقال أساسيات إدارة الحزم لمزيد من المعلومات حول npm وyarn. إذا استخدَمت نظام ويندوز، فيجب عليك تثبيت بعض البرامج لمنحك التكافؤ مع طرفية نظامَي يونكس Unix أو ماك macOS من أجل استخدام أوامر الطرفية المذكورة في هذا المقال، إذ يُعَدّ كل من Gitbash الذي يأتي على أساس جزء من مجموعة أدوات git لنظام ويندوز أو نظام ويندوز الفرعي لنظام لينكس -WSL اختصارًا- مناسبين، كما يُعَدّ برنامج Cmder بديلًا آخر جيدًا وكاملًا، ويمكنك مراجعة مقال سطر الأوامر للحصول على مزيد من المعلومات حول هذه الأوامر وأوامر الطرفية. إنشاء تطبيق Svelte الأول أسهل طريقة لإنشاء قالب تطبيق بسيط هي مجرد تنزيل قالب تطبيق البدء من خلال زيارة صفحة sveltejs/template على GitHub أو يمكنك تجنب الاضطرار إلى تنزيله وفك ضغطه واستخدام أداة degit فقط. أنشئ قالب تطبيق البدء وشغّل أوامر الطرفية التالية: npx degit sveltejs/template moz-todo-svelte cd moz-todo-svelte npm install npm run dev ملاحظة: تتيح degit تنزيل أحدث إصدار من محتويات مستودع Git وفك ضغطه، وهذا أسرع بكثير من استخدام git clone لأنه لن ينزّل كل محفوظات المستودع أو ينشئ نسخةً محليةً كاملةً. سيصرّف إطار عمل Svelte التطبيق ويبنيه بعد تشغيل الأمر npm run dev، كما سيشغّل خادمًا محليًا على المضيف المحلي localhost:8080، إذ يراقب Svelte تحديثات الملفات، ويعيد تلقائيًا تصريف وتحديث التطبيق نيابةً عنك عند إجراء تغييرات على الملفات المصدرية، ثم سيعرض متصفحك شيئًا يشبه ما يلي: بنية التطبيق يأتي قالب البدء بالبنية التالية: moz-todo-svelte ├── README.md ├── package.json ├── package-lock.json ├── rollup.config.js ├── .gitignore ├── node_modules ├── public │ ├── favicon.png │ ├── index.html │ ├── global.css │ └── build │ ├── bundle.css │ ├── bundle.js │ └── bundle.js.map ├── scripts │ └── setupTypeScript.js └── src ├── App.svelte └── main.js يتكون من المحتويات التالية: الملفان package.json و package-lock.json: يحتويان على معلومات حول المشروع التي يستخدمها Node.js ومدير الحزم npm لإبقاء المشروع منظمًا، ولا تحتاج إلى فهم هذين الملفين على الإطلاق، لكن إذا أردت معرفة المزيد عنهما، فاطلع على مقال أساسيات إدارة الحزم. node_modules: هو المكان الذي تحفظ فيه Node اعتماديات المشروع، ولن تُرسَل هذه الاعتماديات إلى مرحلة الإنتاج، وإنما ستُستخدَم فقط لأغراض التطوير. ‎.gitignore: يحدِّد git الملفات أو المجلدات التي يجب تجاهلها من المشروع، وهذا مفيد إذا قررت تضمين تطبيقك في مستودع git. rollup.config.js: يستخدِم إطار عمل Svelte مجمّع الوحدات rollup.js، كما يوضّح ملف الإعداد كيفية تجميع وبناء تطبيقك، وإذا فضلت استخدام أداة Webpack، فيمكنك إنشاء مشروعك باستخدام الأمر npx degit sveltejs/template-webpack svelte-app بدلًا من ذلك. scripts: يحتوي على سكربتات الإعداد المطلوبة، ويجب أن يحتوي حاليًا على الملف setupTypeScript.js فقط. setupTypeScript.js: يضبط هذا السكربت دعم لغة TypeScript في إطارعمل Svelte. src: هذا المجلد هو المكان الذي توجد فيه شيفرة تطبيقك البرمجية، أي حيث ستنشئ شيفرة تطبيقك. App.svelte: هو مكوّن المستوى الأعلى لتطبيقك، إذ يصيّر حتى الآن الرسالة "Hello World!‎". main.js: نقطة الدخول إلى التطبيق. ينشئ نسخةً من المكون App ويربطها بجسم صفحة html. public: يحتوي هذا المجلد على جميع الملفات التي ستُنشَر في مرحلة الإنتاج. favicon.png: الرمز المفضل لتطبيقك، وهو شعار Svelte حاليًا. index.html: الصفحة الرئيسية لتطبيقك، وهي في البداية مجرد صفحة HTML5 فارغة تحمّل ملفات CSS وحزم JS التي ينشئها إطار عمل Svelte. global.css: يحتوي هذا الملف على تنسيقات غير محددة النطاق، وهو ملف CSS الذي سيُطبَّق على التطبيق بأكمله. build: يحتوي هذا المجلد على شيفرة CSS وجافاسكربت الناتجة. bundle.css: ملف CSS الذي أنشأه إطار عمل Svelte من التنسيقات المُعرَّفة لكل مكوّن. bundle.js: ملف جافاسكربت المُصرَّف من كل شيفرة جافسكربت المصدرية. مكون Svelte الأول المكونات هي اللبنات الأساسية لتطبيقات Svelte وتُكتَب في الملفات ذات اللاحقة ‎.svelte باستخدام مجموعة شاملة من شيفرة HTML، كما تُعَدّ جميع الأقسام الثلاثة <script> و <style> والتوصيف Markup أقسامًا اختياريةً ويمكن أن تظهر بأيّ ترتيب تريده. <script> // ضع شيفرتك البرمجية هنا </script> <style> /* ضع تنسيقاتك هنا */ </style> ‏<-- ضع التوصيف (أي عناصر‫ HTML) هنا --!>‏ لنلقِ نظرةً على الملف src/App.svelte المرفق مع قالب البداية، حيث يجب أن ترى شيئًا يشبه ما يلي: <script> export let name; </script> <main> <h1>Hello {name}!</h1> <p>Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p> </main> <style> main { text-align: center; padding: 1em; max-width: 240px; margin: 0 auto; } h1 { color: #ff3e00; text-transform: uppercase; font-size: 4em; font-weight: 100; } @media (min-width: 640px) { main { max-width: none; } } </style> القسم <script> تحتوي كتلة <script> على شيفرة جافاسكربت التي تُشغَّل عند إنشاء نسخة من المكوِّن، إذ تكون المتغيرات المصرَّح عنها أو المستوردة في المستوى الأعلى مرئيةً من شيفرة توصيف المكوِّن، وتُعَدّ متغيرات المستوى الأعلى الطريقة التي يتعامل بها إطار عمل Svelte مع حالة المكوِّن وتكون تفاعليةً افتراضيًا، إذ سنشرح بالتفصيل ما يعنيه ذلك لاحقًا. <script> export let name; </script> يستخدِم إطار العمل Svelte الكلمة export للتصريح عن المتغير بوصفه خاصيةً Prop، مما يعني أنه يصبح بإمكان مستخدِمي المكوِّن -مثل المكونات الأخرى- الوصول إليه، ويمثل هذا المثال توسعة إطار عمل Svelte لصيغة لغة جافاسكربت لجعلها أكثر فائدةً مع بقائها مألوفة. قسم التوصيف يمكنك إدراج أيّ شيفرة HTML تريدها في قسم التوصيف، كما يمكنك إدراج تعبير جافاسكربت صالح ضمن أقواس معقوصة مفردة {}، وسنضمّن في حالتنا قيمة الخاصية name بعد النص Hello مباشرةً. <main> <h1>Hello {name}!</h1> <p>Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p> </main> كما يدعم إطار عمل Svelte وسومًا مثل {‎#if...‎} و {‎#each...‎} و {‎#await...‎} التي تتيح لك تصييرًا مشروطًا لجزء من شيفرة التوصيف والتكرار على قائمة من العناصر والعمل بقيم غير متزامنة. القسم <style> إذا كانت لديك خبرة في العمل مع لغة CSS، فيجب أن يكون جزء الشيفرة التالية مفهومًا: <style> main { text-align: center; padding: 1em; max-width: 240px; margin: 0 auto; } h1 { color: #ff3e00; text-transform: uppercase; font-size: 4em; font-weight: 100; } @media (min-width: 640px) { main { max-width: none; } } </style> سنطبِّق تنسيقًا على العنصر <h1>، لذلك لا بدّ أنك تتساءل عمّا سيحدث للمكونات الأخرى التي تحتوي على عناصر <h1> ضمنها. يُحدَّد في إطار عمل Svelte نطاق شيفرة CSS ضمن كتلة <style> الخاصة بمكوِّن ما لهذا المكوِّن فقط من خلال إضافة صنف Class إلى العناصر المحدَّدة، ولا يضاف هذا الصنف عشوائيًا، وإنما يعتمد على قيمة مُعمَّاة Hash خاصة بتنسيق هذا المكوِّن. يمكنك رؤية جميع هذه الأمور عمليًا من خلال فتح المضيف المحلي localhost:5042 في تبويب متصفح جديد، ثم الضغط بزر الفأرة الأيمن أو الضغط على مفتاح Ctrl على العنوان HELLO WORLD!‎ وتحديد الخيار فحص Inspect: يغيِّر إطار العمل Svelte عند تصريف التطبيق تعريف تنسيق العنصر h1 إلى h1.svelte-1tky8bj، ثم يعدِّل كل عنصر <h1> في المكوِّن إلى الشكل <h1 class="svelte-1tky8bj"‎> بحيث يُطبَّق التنسيق على العنصر الخاص بالمكوِّن المحدَّد فقط. ملاحظة: يمكنك تغيير هذا السلوك وتطبيق التنسيق على محدد Selector بطريقة عامة باستخدام المعدِّل ‎:global(...)‎. إجراء بعض التغييرات يمكنك تعديل المكوِّن App.svelte مثل تعديل العنصر <h1> في السطر رقم 6 من المكوِّن App.svelte بحيث يكون كما يلي: <h1>Hello {name} from MDN!</h1> يؤدي حفظ التعديلات إلى حفظ التطبيق المُشغَّل على المضيف المحلي localhost:5042 تلقائيًا. التفاعل في إطار العمل Svelte يعني التفاعل Reactivity في سياق إطار عمل واجهة المستخدِم أنّ إطار العمل يمكنه تلقائيًا تحديث نموذج DOM عند تعديل حالة أيّ مكون، إذ يُشغَّل التفاعل في إطار عمل Svelte عن طريق إسناد قيمة جديدة لأيّ متغير في المستوى الأعلى ضمن أحد المكونات، فيمكننا مثلًا تضمين دالة toggleName()‎ في المكوِّن App وزر لتشغيلها. عدّل القسمين <script> والتوصيف كما يلي: <script> export let name; function toggleName() { if (name === 'world') { name = 'svelte' } else { name = 'world' } } </script> <main> <h1>Hello {name}!</h1> <button on:click={toggleName}>Toggle name</button> <p>Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p> </main> ينفّذ إطار العمل Svelte الدالة toggleName()‎ عند النقر على الزر، مما يؤدي إلى تحديث قيمة المتغير name. يُحدَّث عنوان Label العنصر <h1> تلقائيًا، إذ ينشئ إطار Svelte شيفرة جافاسكربت لتحديث نموذج DOM كلما تغيرت قيمة المتغير name دون استخدام نموذج DOM الافتراضي أو أيّ آلية توافق معقدة أخرى، ولاحظ استخدام : في on:click التي تُعَدّ صيغة Svelte للاستماع إلى أحداث DOM. فحص main.js‎: نقطة الدخول إلى التطبيق افتح الملف src/main.js حيث يُستورَد ويُستخدَم المكوِّن App، إذ يُعَدّ هذا الملف نقطة الدخول لتطبيقنا، ويبدو في البداية كما يلي: import App from './App.svelte'; const app = new App({ target: document.body, props: { name: 'world' } }); export default app; يبدأ الملف main.js باستيراد مكوِّن Svelte الذي سنستخدِمه، ثم ينشئ نسخةً منه في السطر رقم 3، ويمرّر كائنًا له الخاصيات التالية: target: عنصر DOM الذي نريد تصيير المكوِّن ضمنه، وهو العنصر <body> في هذه الحالة. props: القيم المراد إسنادها لكل خاصية للمكوِّن App. نظرة إلى خلفية إطار Svelte لا بدّ أنك تتساءل عن كيفية تمكّن إطار Svelte من جعل كل هذه الملفات تعمل مع بعضها البعض بطريقة صحيحة، إذ يعالِج مصرّف Svelte القسم <style> لكل مكوِّن ويصرّفه في الملف public/build/bundle.css، ويصرّف قسمَي التوصيف و<script> لكل مكوِّن ويخزن النتيجة في الملف public/build/bundle.js، كما يضيف الشيفرة البرمجية في الملف src/main.js للإشارة إلى ميزات كل مكوِّن يتضمن الملف public/index.html الملفين bundle.css و bundle.js: <!DOCTYPE html> <html lang="en"> <head> <meta charset='utf-8'> <meta name='viewport' content='width=device-width,initial-scale=1'> <title>Svelte app</title> <link rel='icon' type='image/png' href='/favicon.png'> <link rel='stylesheet' href='/global.css'> <link rel='stylesheet' href='/build/bundle.css'> <script defer src='/build/bundle.js'></script> </head> <body> </body> </html> حجم الإصدار المصغر من الملف bundle.js أكثر بقليل من 3 كيلوبايت، والذي يتضمن "وقت تشغيل Svelte" -أي 300 سطر فقط من شيفرة جافاسكربت- والمكوِّن المُصرَّف App.svelte، كما يُعَدّ الملف bundle.js ملف جافاسكربت الوحيد الذي يشير إليه الملف index.html، ولا توجد مكتبات أخرى مُحمَّلة في صفحة الويب. تُعَدّ هذه المساحة أصغر بكثير من الحزم المُصرَّفة في أطر عمل أخرى، وضَع في الحسبان أنه لا يقتصر الأمر على حجم الملفات التي يجب تنزيلها في حالة تجميع الشيفرة البرمجية، والتي هي شيفرة قابلة للتنفيذ يجب تحليلها وتنفيذها والاحتفاظ بها في الذاكرة، لذلك يحدث ذلك فرقًا حقًا خاصةً في الأجهزة ذات الإمكانات المنخفضة أو التطبيقات ذات الاستخدام الكبير لوحدة المعالجة المركزية. متابعة هذه السلسلة من المقالات سنبني في هذه السلسلة من المقالات تطبيق ويب كامل، لذلك وفّرنا مستودع GitHub مع مجلد يحتوي على شيفرة التطبيق البرمجية الكاملة، كما يمكنك قراءة المحتوى فقط للحصول على فهم جيد لميزات Svelte، ولكن ستحصل على أقصى استفادة من هذا السلسلة من المقالات إذا اتبعت شيفرة التطبيق معنا، كما سنوفر مستودع GitHub مع مجلد يحتوي على شيفرة التطبيق البرمجية كما هي في بداية كل مقال لتسهيل متابعته. يوفِّر إطار Svelte أداة REPL على الإنترنت، وهي أداة لتطبيقات Svelte ذات الشيفرة الحية المباشرة على الويب دون الحاجة إلى تثبيت أيّ شيء على جهازك، ولنتحدث الآن عن كيفية استخدام هذه الأدوات. استخدام Git يُعَدّ Git أكثر أنظمة التحكم في الإصدارات شيوعًا مع GitHub، وهو موقع يوفِّر استضافةً لمستودعاتك والعديد من الأدوات للعمل بها، وسنستخدِم GitHub لتتمكّن من تنزيل الشيفرة البرمجية بسهولة. يجب تنفيذ الأمرالتالي بعد تثبيت Git لنسخ المستودع: git clone https://github.com/opensas/mdn-svelte-tutorial.git يمكنك إدخال الأمر cd في المجلد المقابل وبدء تشغيل التطبيق في وضع التطوير dev لترى ما يجب أن تكون عليه حالة التطبيق الحالية كما يلي: cd 02-starting-our-todo-app npm install npm run dev ملاحظة: إذا أردت تنزيل الملفات فقط دون نسخ مستودع Git، فيمكنك استخدام الأداة degit في الأمر npx degit opensas/mdn-svelte-tutorial، كما يمكنك تنزيل مجلد محدد باستخدام الأمر npx degit opensas/mdn-svelte-tutorial/01-getting-started، ولن تنشئ الأداة degit مستودع git محلي، وإنما ستنزّل ملفات المجلد المحدَّد فقط. استخدام أداة REPL في إطار عمل Svelte تُعَدّ أداة REPL (أي حلقة قراءة-تقييم-طباعة read–eval–print Loop) بيئةً تفاعليةً تسمح بإدخال الأوامر والاطلاع على النتائج مباشرةً، وتوفِّر العديد من لغات البرمجة أداة REPL، كما تُعَدّ حلقة REPL في إطار Svelte أداةً عبر الإنترنت تتيح إنشاء تطبيقات كاملة وحفظها عبر الإنترنت ومشاركتها مع الآخرين، إذ تُعَدّ أسهل طريقة لبدء العمل باستخدام Svelte من أيّ جهاز دون الحاجة إلى تثبيت أيّ شيء، كما يستخدمها مجتمع Svelte على نطاق واسع، لذا إذا أردت مشاركة فكرة أو طلب المساعدة أو الإبلاغ عن مشكلة، فيمكنك إنشاء نسخة REPL توضِّح المشكلة. لنلقِ نظرةً سريعةً على أداة REPL في إطار عمل Svelte وكيفية استخدامها، حيث تبدو كما يلي: افتح المتصفح وانتقل إلى أداة REPL. لنتعرف على محتوياتها: سترى شيفرة مكوناتك على الجانب الأيسر من الشاشة، وسترى على اليمين خرج تنفيذ تطبيقك. يتيح لك الشريط الموجود أعلى الشيفرة إنشاء ملفات ‎.svelte و ‎.js وإعادة تنظيمها، كما يمكنك إنشاء ملف ضمن مجلد من خلال تحديد اسم المسار الكامل components/MyComponent.svelte ثم سيُنشَأ المجلد تلقائيًا. يوجد عنوان أداة REPL فوق هذا الشريط. يمكنك الضغط عليه لتعديله. يوجد على الجانب الأيمن ثلاث تبويبات هي: يعرض تبويب "النتيجة Result" خرج التطبيق، ويوفِّر طرفيةً Console في الأسفل. يتيح تبويب "JS output" فحص شيفرة جافاسكربت التي أنشأها إطار عمل Svelte ويضبط خيارات المصرّف Compiler. يعرض تبويب '"CSS Output" شيفرة CSS التي أنشأها إطار عمل Svelte. ستجد شريط أدوات فوق التبويبات، حيث يتيح شريط الأدوات الدخول إلى وضع ملء الشاشة وتنزيل تطبيقك، فإذا سجّلتَ الدخول باستخدام حساب GitHub، فستتمكّن من نسخ التطبيق وحفظه، وستتمكن من رؤية جميع أدوات REPL المحفوظة من خلال النقر على اسم مستخدِم حسابك على GitHub وتحديد التطبيقات المحفوظة. كلما عدّلت أيّ ملف على REPL، فسيعيد إطار عمل Svelte تصريف التطبيق وتحديث تبويب النتيجة، كما يمكنك مشاركة تطبيقك من خلال مشاركة عنوان URL مثل رابط REPL الخاص بتشغيل تطبيقنا الكامل. ملاحظة: لاحظ كيف يمكنك تحديد إصدار Svelte في عنوان URL، إذ يكون ذلك مفيدًا عند الإبلاغ عن المشاكل المتعلقة بإصدار معيّن من إطار Svelte. ملاحظة: لا يمكن لأداة REPL حاليًا التعامل مع أسماء المجلدات بطريقة صحيحة، فإذا أردت متابعة الخطوات التي نطبّقها، فأنشئ جميع مكوناتك ضمن المجلد الجذر، فإذا رأيت مسارًا في الشيفرة import Todos from './components/Todos.svelte'‎، فضَع مكانه عنوان URL مسطح Flat مثل import Todos from './Todos.svelte'‎. يمكنك نسخ مستودع github -إذا لم تفعل ذلك مسبقًا- باستخدام الأمر التالي: git clone https://github.com/opensas/mdn-svelte-tutorial.git ثم يمكنك الوصول إلى حالة التطبيق الحالية من خلال تشغيل الأمر التالي: cd mdn-svelte-tutorial/01-getting-started أو يمكنك تنزيل محتوى المجلد مباشرةً كما يلي: npx degit opensas/mdn-svelte-tutorial/01-getting-started تذكَّر تشغيل الأمر npm install && npm run dev لبدء تطبيقك في وضع التطوير، فإذا أردت متابعتنا، فابدأ بكتابة الشيفرة باستخدام الأداة REPL من هنا. الخلاصة ألقينا في هذا المقال النظرة الأولية إلى إطار عمل Svelte بما في ذلك كيفية تثبيته محليًا وإنشاء تطبيق بدء بسيط وكيفية عمل الأساسيات، كما سنبدأ في المقال التالي ببناء أول تطبيق وهو تطبيق قائمة المهام، ولنلخص بعض الأشياء التي تعلمناها في Svelte وهي: تعريف سكربت وتنسيق وتوصيف كل مكون في ملف ‎.svelte واحد. يُصرَّح عن خاصيات المكونات باستخدام الكلمة export. يمكن استخدام مكونات Svelte فقط عن طريق استيراد ملف ‎.svelte المقابل. يجب تحديد نطاق تنسيق المكونات، مما يمنعها من التضارب مع بعضها البعض. يمكنك تضمين أيّ تعبير جافاسكربت في قسم التوصيف Markup بوضع هذا التعبير بين قوسين معقوصين. تشكّل متغيرات المستوى الأعلى للمكوِّن حالته. يمكن إطلاق التفاعل عن طريق إسناد قيمة جديدة لمتغير المستوى الأعلى. ترجمة -وبتصرُّف- للمقال Getting started with Svelte. اقرأ أيضًا استخدام أطر العمل في برمجة تطبيقات الويب: فلاسك نموذجا مقدمة في بناء تطبيقات الويب باستخدام إطار العمل Angular وقاعدة بيانات Firestore أساسيات بناء التطبيقات في إطار العمل Laravel 5 البدء مع إطار العمل جانغو لإنشاء تطبيق ويب.
×
×
  • أضف...