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

رشا سعد

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

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

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

3 متابعين

المعلومات الشخصية

  • النبذة الشخصية
    مهندس اتصالات من سوريا

آخر الزوار

1189 زيارة للملف الشخصي

إنجازات رشا سعد

عضو مساهم

عضو مساهم (2/3)

2

السمعة بالموقع

  1. الخادم السحابي Cloud Server هو بنية تحتية متاحة للمستخدمين عبر الإنترنت، تؤمن لهم ما يحتاجونه من الموارد الحاسوبية عن بعد، مثل مساحات التخزين والذواكر والمعالجات وغيرها، يمكنك النظر إليه على أنه حاسوب خاص تستطيع إعداده والتحكم به بالطريقة نفسها التي تتعامل بها مع أجهزتك المحلية، سواء كانت خوادم أو حواسيب شخصية أو محمولة. يعرض هذا المقال بنية الخادم السحابي ومكوناته، والفرق بينه وبين عروض الاستضافة السحابية الأخرى، مع بعض النصائح لاختيار الاستضافة الأنسب لموقعك الإلكتروني أو تطبيقك على الويب. ستصادف في هذا المجال مصطلحات "خادم" و"خادم سحابي" و"خادم ويب" بالتبادل، ويشير الخادم السحابي عادةً إلى بيئة لينكس كاملة أو فعليًّا إلى حاسوب كامل. تعمل الخوادم السحابية بصفتها أجهزة افتراضية (VMs) أو برمجيات تحاكي الحواسيب التقليدية، وتجدها دائمًا ضمن مجموعات كبيرة أو عناقيد Clusters في تقنية تسمى الحوسبة الافتراضية Virtualization. يمكنك معرفة المزيد عن آلية عملها بمراجعة مقال [مقدمة إلى الاستضافة السحابية](). البرمجيات المستخدمة على السحابة التعرُّف عليها ضروري لفهم الخوادم السحابية، سنعرضها باختصار: نظم التشغيل: تثبيت نظام التشغيل هو خطوتك الأولى في إعداد الخادم السحابي، يعتمد معظم مستخدمي السحابة نظام تشغيل لينكس بتوزيعاته المختلفة، مثل: أوبنتو Ubuntu أو روكي لينكس Rocky Linux لبناء خوادمهم، نظرًا لانتشاره الواسع في بيئات الخوادم، والدعم الكبير الذي يحظى به من المجتمع التقني، والذي يسهل عليك إيجاد مصادر للتعلُّم، بالإضافة لكونه مجاني في الأساس، وحتى إن احتجت لتراخيص فتراخيصه مرنة موازنةً بغيره. يمكنك الاطلاع على مقال [كيف تختار توزيعة لينكس المناسبة لك؟]() لتتعرف أكثر على توزيعات لينكس، وتختار ما يناسبك منها. برمجيات جانب الخادم: يقصد بها مجموعة واسعة من البرمجيات والأدوات المستخدمة لإعداد الخوادم، وهي مصممة للعمل في البيئات السحابية، ومناسبة تمامًا لها، حيث لا شاشات عرض ولا بيئة سطح مكتب، فغالبًا ما تُثَبّت برمجيات جانب الخادم Server-Side وتُضبَط إعداداتها باستخدام واجهة سطر الأوامر، ويصل إليها المستخدم النهائي عبر المتصفح أو التطبيقات الأخرى. تتنوع هذه الأدوات وسيساعدك فهم أساسياتها على تجهيز خادمك السحابي. خوادم الويب: تسمح خوادم الويب لخادمك السحابي بالتواصل مع المستخدمين عبر الإنترنت بواسطة بروتوكول HTTP، وهي من أدوات جانب الخادم، تُبَرمَج للاستجابة بطريقة محددة مع كل طلب يطلبه العميل عبر برمجيات جانب العميل Client-Side، لنفترض على سبيل المثال: أن المستخدم طلب URL معين من المتصفح (الذي يلعب هنا دور العميل)، سيستجيب له الخادم، ويجلب مستند HTML المناسب، ثم يرسله للمتصفح فيعرض صفحة الويب المطلوبة. ستحتاج خادم ويب حتمًا، إذا كنت تبني خادمك السحابي من الصفر، وتنوي تجهيزه لاستضافة موقع إلكتروني أو تطبيق ويب، وأشهر خوادم الويب المستخدمة هي Nginx و Apache. يمكنك الاطلاع على مدخل إلى خادم الويب لمزيد من المعلومات. خوادم API: واجهات برمجة التطبيقات أو APIs هي برمجيات وسيطة تسمح للتطبيقات المختلفة بالتخاطب مع بعضها. خادم الويب نفسه هو خادم API يطبق HTTP API، ويوجد أنواع كثيرة غيره، مثل APIs الخاصة بمعلومات الطقس أو رحلات الطيران أو غيرها، يستخدمها الخادم السحابي لتبادل البيانات مع التطبيقات الخارجية ومصادر البيانات الخارجية، وقد يتصل الخادم بأكثر من واجهة API حسب طبيعة تطبيقك، وتدعى نقاط اتصال API بالتطبيق، أي النقاط التي تنفذ عمليات API، بنقاط الوصول أو endpoints، وتكون مثلًا عناوين URL تدل على الموارد المطلوب الاستعلام عنها. خوادم قواعد البيانات: يمكنك تسميها بقواعد البيانات فقط، وهي نوع آخر من خوادم API، لكنها بخلاف خوادم الويب لا تعرض البيانات المطلوبة منها بهيئة صفحات HTML ضمن المتصفح، إنما يجري التواصل معها عبر واجهات برمجة تطبيقات خاصة تسمى واجهات استعلام قاعدة البيانات. بعض قواعد البيانات متاح للاستعلام الخارجي عبر الويب، وبعضها الآخر مخصص للاستخدام الداخلي من قبل برمجياتك السحابية فقط. الخوادم السحابية وبدائلها الخادم السحابي هو حاسوب افتراضي كامل، لذا فجميع المنتجات السحابية الأخرى تتعلق به، ومزودو الخدمة السحابية يقدمون لك في عروضهم ميزة أو وظيفة محددة فقط من وظائف هذا الخادم، على سبيل المثال استضافة ويب مخصصة أو استضافة قاعدة بيانات مخصصة، وكل حل سحابي يوفر خادم ويب أو قاعدة بيانات ينبثق فعليًّا من خادم سحابي، غالبًا ما تبنى الخوادم السحابية باستخدام عناقيد من الخوادم المادية أو الفيزيائية التي تعمل معًا بتقنية الحوسبة الافتراضية. فالفرق الجوهري إذًا بين الخادم السحابي (الذي يسمى VPS أو الخادم الافتراضي الخاص Virtual Private Server) والعروض السحابية الأخرى هو أنك تستطيع تثبيت التطبيقات التي تريدها على الخادم السحابي، وتشغيلها بحرية حسب احتياجك، بينما تفرض عليك العروض الأخرى مجموعة ميزات ووظائف بعينها لا يسعك الخروج عنها. أحدثت الحوسبة الافتراضية تغييرًا كبيرًا في سوق الخدمات السحابية خلال العقود الماضية، فقبل ظهورها كان مزود الخدمة يوفر خادمًا مثل Nginx أو Apache لتقديم الاستضافة لعشرات المواقع مع أذونات مخصصة لكل مشترك، ولم يكن المشترك مسؤولًا عن أي مهام إدارية على الخادم، لكن بالمقابل اقتصرت المواقع المستضافة وقتها على المواقع ذات الطبيعة الثابتة أي التي تتضمن عناصر HTML و CSS وجافا سكريبت ثابتة بدون محرك خلفي، وأيضًا مواقع drop-in PHP التي لا تمتلك أي اعتماديات سوى خادم الويب. ولاحقًا بعد انتشار الحوسبة الافتراضية، ازداد الطلب على عروض VPS أو الخوادم السحابية الكاملة، بسبب مرونتها العالية، لكن تشغيلها وتحديدًا في بيئة الإنتاج، يتطلب منك مهارات خاصة، لا يهتم المبرمجون بتحصيلها عادةً، لأنها لا ترتبط مباشرةً بكتابة الشيفرات البرمجية، أبرزها مثلًا: معرفة جيدة بأساسيات لينكس، وكيفية التعامل مع سطر الأوامر، ومهام إدارة النظام، بالإضافة إلى هندسة DevOps لإدارة عمليات النشر وتلبية المعايير الأمنية للتشغيل. هل أستخدم خادمًا سحابيًا لمشروعي؟ تحتوي الخوادم السحابية ميزات حماية افتراضية مدمجة فيها، ولست مضطرًا لخوض عملية نشر على نطاق تجاري بحذافيرها لتُشغل تطبيقًا مفتوح المصدر بطريقة آمنة وموثوقة على خادم سحابي، أضف إلى ذلك أن إعدادات الأمان الافتراضية مضبوطة بإحكام على معظم الحزم المستخدمة في الخوادم، وتخضع للتحديث الدوري لسد ثغراتها الأمنية، لذا يكفيك في معظم الأحيان تثبيت جدار حماية بسيط مثل ufw أو الجدار الناري غير المعقد ليتحكم ببوابات الشبكة على خادمك السحابي، فيسمح أو يمنع اتصالات معينة حسب متطلبات عملك، أو على الأقل يمررها إلى خادم ويب مخضرم مثل Nginx تعتمده ملايين الخوادم حول العالم. إضافةً إلى الخوادم السحابية، يوفر بعض مزودي الخدمات السحابية عروضًا أخرى يمكن تشبيهها بمضيفي الويب، تلائم مواقع الويب ذات المكونات الثابتة المعتمدة على ميزات جافا سكريبت الحديثة، التي تخفف الحاجة إلى الخادم الخلفي Backend، يسمى هذا النوع من الاستضافة نظام إدارة محتوى بدون رأس Headless CMS، ويقدم المزود معه عادةً أدوات تأليف ونماذج ويب تعد جزءًا من عرض أوسع يسمى البرنامج بصيغة خدمة أو Software As A Service. ويقدم بعض المزودين أيضًا استضافة الخوادم الخفية Serverless أو ما يسمى الوظائف بدون خادم، ويقصد بها كتابتك التعليمات البرمجية لتطبيقك مرةً واحدة بمعزل عن البنية التحتية للخوادم، ثم نشرها في البيئة السحابية ليجري تشغيلها فقط عند الطلب، يطلق على هذا الأسلوب عند استخدامه مع مواقع الويب الثابتة اسم جامستاك Jamstack. تتميز خدمات الحوسبة خفية الخوادم وخدمات نشر مواقع الويب الثابتة بأنها تقنيات قابلة للنقل، وتشبه الاستضافات القديمة إذ تعفي صاحب التطبيق تقريبًا من جميع مسؤوليات الأمن والصيانة التي ترافق عمليات النشر الكاملة على الخوادم، لكنها مع ذلك تظل محدودة النطاق، وقد لا تفي بالغرض في بعض الحالات، فإذا تطلب مشروعك على سبيل المثال نشر حاوية دوكر خلف خادم Nginx في مكدس واحد، لن تكفيك هذه الاستضافات وستحتاج خادمًا سحابيًا كاملًا. عمومًا، أي تطبيق تنشره على الخادم السحابي يمكنك نشره على حاسوبك المحلي أيضًا، مع بعض الاختلافات لصالح الخادم السحابي، فنادرًا ما يثبت الأفراد نظام تشغيل لينكس على حواسيبهم الشخصية، كما هو الحال في الخوادم السحابية، بل يعتمدون ويندوز أو ماك، ومعظم أدوات جانب الخادم لا تعمل مباشرةً مع هذه النظم على عكس لينكس. قد لا تكون هذه النقطة مؤثرة كثيرًا بالنسبة لك، لكن ما نريد إيصاله أن الخادم السحابي يناسب جميع الاحتياجات، ويمكنك إعداده لأي غرض تريده تمامًا كما تفعل مع حاسوبك المحلي. كيفية تحديد مواصفات الخادم يعتمد أداء الخوادم السحابية وسعرها على مواصفات العتاد الصلب، ويخصص لكل خادم سحابي ضمن العنقود Cluster مقدار معين من الموارد الحاسوبية، يمكن زيادته أو إنقاصه بمرونة تامة وبسرعة، على عكس الحواسيب الفيزيائية، لذا قبل حجز خادمك السحابي حدد أولًا مواصفاته الفنية، وادرس تأثيرها على جودة عمل التطبيق، ثم وازن ذلك مع ميزانيتك، وبعدها اختر الخادم المناسب. تُحَدَّد مواصفات الخوادم السحابية عادةً بعدد نوى المعالجات المركزية CPU المتوفرة لديها، وحجم ذاكرة الوصول العشوائي RAM المتاحة، وما يخصص لها من مساحات تخزينية على الأقراص الصلبة. ونظرًا لاختلاف مقاييس سرعة الأقراص والمعالجات في الخوادم السحابية عنها في ظروف التشغيل في العالم الحقيقي، فقد اعتمد معظم مزودي الخدمات السحابية مواصفاتٍ موحدة لها، فمتوسط سرعة الأقراص يعادل تقريبًا سرعة أقراص SSD، وسرعة وحدة المعالجة المركزية تماثل سرعة نواة Intel Xeon. وبالإضافة إلى المواصفات السابقة يفرض بعض المزودين قيودًا على الخوادم السحابية في الطبقة الدنيا، مثل: تحديد عدد عمليات الإدخال والإخراج المسموحة على القرص (IOPS)، وإجمالي حركة المرور المتبادلة مع الخودام عبر الشبكة وغير ذلك، قد تسبب هذه القيود اختناقات لبعض البرامج المستضافة. أما بخصوص الحجوم التخزينية، فيوفر لك معظم المزودين إمكانية حجز مساحات تخزين إضافية حسب احتياجاتك، بتقنيات مثل: التخزين الكتلي والتخزين الكائني، وربطها بخادمك الخاص VPS، وهذا أفضل من احتفاظك بجميع البيانات في مكان واحد على القرص التخزين الأساسي داخل VPS وتوسيع مساحته باستمرار. يُعطى لكل خادم سحابي عنوان IP عام مخصص له، للاتصال به عبر شبكة الإنترنت، وقد يكون العنوان من الإصدار الرابع IP4 وفق الصيغة 123.123.123.123 أو من الإصدار السادس IP6 من نمط 2001:0db8:0000:0000:0000:ff00:0042:8329، والحالة المعتادة أن يقابل عنوان IP اسم دومين مثل: https://my_domain.com. تتضمن بعض عروض الاستضافة السحابية تزويدك بعنوان IP لكل خادم VPS تحجزه، بينما يطلب منك بعض المزودين شراء عناوين IP بنفسك وتسجيلها للخوادم، تسمى هذه العناوين بعناوين IP المحجوزة Reserved IPs، وهي مناسبة لعمليات النشر واسعة النطاق. يمكنك شراء اسم الدومين من مزود الاستضافة نفسه فبعضهم مثل ديجيتال أوشن يقدمون لك المنتجين معًا، وتستطيع أيضًا الحصول عليه من مصدر آخر ثم إنجاز الربط بينهما بواسطة سجلات DNS. الوصول للخادم السحابي يتطلب الوصول إلى الخوادم السحابية إتقانك العمل في بيئة سطر الأوامر أو الطرفية Terminal محليًا أو عن بعد، وأكثر البروتوكولات استخدامًا في هذا المجال بروتوكول SSH الذي يستخدم البوابة 22 ويعتمد عليه مسؤولو الخادم، وبروتوكول HTTP على البوابة 80 و HTTPS على البوابة 443 المخصصين أكثر للمستخدم النهائي، تتمحور إدارة النظام السحابي حول هذه الخوادم والخدمات والبروتوكولات. الخاتمة تناول هذا المقال التعليمي نظرة عامة على الخوادم والعروض السحابية، وكيفية تطورها مع الزمن، ليضعك على بداية الطريق في العمل مع هذه الخدمات، ويعينك على اختيار أنسبها لمشروعك. ترجمة -وبتصرف- للمقال Cloud Servers: An Introduction لصاحبه Alex Garnett. اقرأ أيضًا مفهوم السحابة Cloud تعلم الحوسبة السحابيّة: المتطلبات الأساسيّة، وكيف تصبح مهندس حوسبة سحابيّة الحوسبة الافتراضية virtualization وأوجه اختلافها عن الحاويات containers كل ما تود معرفته عن السحابة الهجينة Hybrid Cloud
  2. يزداد الاعتماد على التطبيقات الذكية في جوانب حياتنا يومًا بعد يوم، ومع هذا الانتشار يزداد تعقيدها، وتتشعب وظائفها، فتصبح الحاجة ملحةً أكثر لابتكار تقنيات جديدة تحسِّن التفاعل بين خوادم التطبيقات والعملاء، ولعل أبرزها في السنوات الأخيرة GraphQL، وهي لغة استعلام مفتوحة المصدر لواجهات برمجة التطبيقات APIs وبيئة تشغيل لتنفيذ الاستعلامات، طورتها شركة فيسبوك Facebook في عام 2012 بهدف التغلب على نقاط الضعف في بنية REST التقليدية، وطرحتها للاستخدام العام في 2015، وأكثر ما يميز GraphQL جودة أدائها وكونها لغة تصريحية declarative وموجهة كليًا لتلبية طلبات العميل وما يحتاجه حقًا من معلومات. فما هي مفاهيم GraphQL الأساسية؟ وما أوجه التشابه والاختلاف بينها وبين REST؟ ما هي GraphQL؟ GraphQL هي اختصار للعبارة Graph Query Language وتعني لغة استعلام بيانية، وهي مختلفة قليلًا عن لغات الاستعلام الأخرى مثل SQL وغيرها، فهي لا تتخاطب مع قاعدة بياناتك مباشرةً إنما تصف نموذج التواصل بين العميل وخادم واجهة برمجة التطبيقات API، ولديها مجموعة مواصفات قياسية بمثابة معيار موحد يحدد خصائصها وقواعد استخدامها، وبما أنك تتبع مواصفات GraphQL، فيمكنك استخدامها مع أي لغة برمجة، ومع أي قاعدة بيانات، ومع جميع أنواع العملاء إذا كانوا قادمين من تطبيق ويب أو تطبيق هاتف محمول، فهي كما ذكرنا مفتوحة المصدر لا تقتصر على أنواع معينة. يُعد Apollo GraphQL من أشهر تطبيقات خادم وعميل GraphQL التجارية وأكثرها انتشارًا بين المطورين، وستجد في هذا المقال على أكاديمية حسوب مثالًا عمليًّا عن طريقة بنائه. خصائص GraphQL سنعرض بعضًا من خصائص GraphQL الأساسية، مثل: استعلاماتها التصريحية declarative والهرمية hierarchical، وكونها ذات قواعد صارمة في التعامل مع أنواع البيانات strongly-typed، وأيضًا استقرائية introspective تسمح بالكشف عن مواصفات مخططاتها الداخلية Schema ليستفيد منها طالب الاستعلام. تصريحية Declarative تعني التصريحية أن العميل سيحدد أو يصرح عن الحقول التي يريد الاستعلام عنها فقط ويطلبها من الخادم، والخادم بدوره سيرجعها هي بالذات دون أي معلومات إضافية. ألقِ نظرةً على المثال التالي لإيضاح الأمر. لنفترض أنك تطلب واجهة برمجية API للعبة ما وتستعلم عن حقول محددة لأحد شخصياتها، على سبيل المثال الاسم name والتصنيف race لشخصية المحارب warrior صاحب المعرف رقم "1"، فسيكون الطلب وفق الآتي: { warrior(id: "1") { name race } } ستعيد الاستجابة المُعادة من تنسيق JSON كائنًا لنسميه data يتضمن الحقلين المطلوبين فقط من بيانات المحارب رقم "1": { "data": { "warrior": { "name": "Merlin", "race": "HUMAN" } } } تتضمن هذه الاستجابة ما يطلبه العميل فقط دون زيادة أو نقصان وتمنح تطبيقك كفاءةً أعلى وأداءً أفضل على الشبكة موازنةً ببدائل GraphQL الأخرى مثل REST التي تُعيد للعميل كامل بيانات العنصر المُستَعلم عنه فتسبب ضغطًا على الشبكة. هرمية Hierarchical يمكنك طلب استعلامات هرمية من GraphQL أي الاستعلام عن أصل وفروعه، وستصلك البيانات المعادة من الخادم بنفس الهرمية التي طلبتها؛ فطلب الاستعلام يحدد شكل الاستجابة. لو عُدنا للمثال السابق واستبدلنا الاستعلام عن اسم المحارب وتصنيفه بالاستعلام عن اسمه وأسلحتهweapons وبالتحديد عن اسم كل سلاح name ودرجة قوته الهجومية attack: { warrior(id: "1") { name weapons { name attack } } } ستتضمن الاستجابة الآن اسم المحارب ومصفوفة كائنات الأسلحة weapons مرتبةً كما طلبناها في الاستعلام warrior، وقد استطاعت GraphQL إحضارها بطلب استعلام واحد فقط، رغم أن بيانات الأسلحة ومقاتلي اللعبة تكون مخزنة غالبًا في جداول منفصلة ضمن قاعدة البيانات وهذه نقطة قوتها. نذكرك هنا أن GraphQL ليست معنية مطلقًا بطريقة تخزين البيانات في قاعدة البيانات إنما بتحديد نموذج الاستعلام فقط. ألقِ نظرةً على الاستجابة الهرمية لاستعلامنا: { "data": { "warrior": { "name": "Merlin", "weapons": [ { "name": "Sword", "attack": 4 }, { "name": "Bow", "attack": 3 }, { "name": "Axe", "attack": 2 } ] } } } صارمة في تحديد الأنواع Strongly-typed توصف GraphQL بأنها صارمة في التعامل مع أنواع البيانات، ولديها نظام خاص لتحديد الأنواع يسمى نظام النوع، يصف إمكانات الخادم أي أنواع البيانات التي يقبلها، وتتدرج من البيانات المفردة Scalars وهي بيانات أولية، مثل: الأعداد الصحيحة والسلاسل النصية والقيم المنطقية، وصولًا إلى أنواع البيانات المعقدة مثل الكائنات التي تتكون من مجموعة حقول من البيانات الأولية. يبين المثال التالي إنشاء نوع في مخطط GraphQL اسمه Weapon، وهو كائن يمتلك حقولًا أولية من نوع نص String وعدد صحيح Int: type Weapon{ name: String! attack: Int range: Int } إذًا، نظام النوع هو المسؤول عن صحة تعريف مخطط GraphQL، ويُقيّم الخادم بواسطته إذا كان طلب الاستعلام الذي كتبته مقبولًا أم لا قبل تنفيذه، ثم يُخضِعه للتحقق للتأكد من سلامته قواعديًا وخلوه من الأخطاء. ذاتية التوثيق Self-documenting يدعم خادم GraphQL خاصية الاستقراء Introspection، وهذا يعطي عملاءه والبرامج المتصلة معه القدرة على استقراء مخططاته الداخلية والاستعلام عن بنيتها، ويُسهل أيضًا تطوير أدوات مساعدة للتعامل معه نحو GraphiQL التي توفر بيئة تطوير متكاملة IDE وبيئة تجريبية Playground تعمل ضمن المتصفح، وغيرها من أدوات التوثيق الآلية. هذا مثال بسيط عن استخدام الاستقراء لاستكشاف معلومات إضافية عن النوع Weapon باستعمال الكلمة المفتاحية schema__: { __schema { types { name kind description } } } سيجيبك خادم GraphQL بصيغة JSON المعتادة وفق الآتي: { "data": { "__schema": { "types": [ { "name": "Weapon", "kind": "OBJECT", "description": "A powerful weapon that a warrior can use to defeat enemies." } ] } } } موجهة بطلبات العميل Client-driven يتركز جُلّ عمل المطور عند بناء GraphQL API في الواجهة الخلفية، فيُعرّف المخطط schema وينفّذه، ويهيئ نقطة الوصول الوحيدة endpoint مع الواجهة البرمجية التي تًميّز GraphQL عن غيره وتُعدّ نقطة قوته، أما من طرف العميل فيمكنه طلب البيانات التي يريدها بدقة عبر الاستعلامات التصريحية، ومهما تغيرت مواصفات الاستعلامات، يستطيع مطور الواجهة الأمامية مواكبتها وإنجاز تصاميم تكرارية سريعة لتطبيقه دون أي تعديل إضافي على الواجهة الخلفية. بنية GraphQL يعمل GraphQL في طبقة التطبيقات Application layer وسيطًا بين العميل والبيانات، إذ يصف خادم GraphQL إمكانات الاستعلام التي تتيحها الواجهة البرمجية API، ويحدد العميل متطلبات طلب الاستعلام حسب احتياجه. الخادم Server يعمل GraphQL في طبقة التطبيق، وهو غير مرتبط ببروتوكول نقل محدد، لكنه يستخدم غالبًا بروتوكول HTTP، ولديه نقطة اتصال وحيدة تسمى عادةً graphql/ وتتيح الوصول لكل موارد الخادم. يمكنك برمجة خادم GraphQL بأي لغة برمجة، فعلى سبيل المثال تساعدك البرمجية الوسيطة express-graphql على إنشاء GraphQL API على خادم Express HTTP في بيئة Node؛ وفيما يخص قواعد البيانات فلا يقتصر خادم GraphQL على نوعٍ محدد منها، إذ يمكنه التعامل مع البيانات المخزنة في MySQL أو PostgreSQL أو MongoDB أو حتى القادمة من مصادر أخرى عبر نقاط اتصال لواجهات REST التقليدية، فالمهم في الأمر أن تُعرّف البيانات في مخطط GraphQL صحيح يبين الاستعلامات التي يستطيع العميل طلبها من الواجهة البرمجية API. العميل Client تدعى الطلبات المرسلة من عميل GraphQL إلى الخادم بالمستندات documents، وقد تكون طلبات قراءة فقط أي استعلامات، أو طلبات كتابة تسبب تعديلًا على البيانات وهذه تسمى طفرات mutations. يمكنك توجيهها بطلب XMLHttpRequest بسيط، أو بعملية fetch لجلب البيانات من متصفح الويب، أو بالاعتماد على أدوات عميل متقدمة نحو عميل Apollo أو ريلي فيسبوك التي تقدم لك مميزات مختلفة مثل التخزين المؤقت. إليك مثال لاستخدام الدالة fetch في جلب البيانات من نقطة الوصول graphql/، وقد مُرِّر مستند GraphQL بهيئة سلسلة نصية ضمن متن الطلب POST: async function fetchwarriors() { const response = await fetch('/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: `{ warriors { id name }, }`, }), }) const warriors = await response.json() return warriors } fetchwarriors() وهذه هي الاستجابة: { "data": { "warriors": [ { "id": "1", "name": "Merlin" }, { "id": "2", "name": "Gandalf" } ] } } مقارنة بين GraphQL و REST كلاهما يعملان للهدف نفسه، وهو تبادل البيانات بين تطبيقات مختلفة؛ إذ عرّفنا GraphQL في بداية المقال على أنها لغة استعلام بيانية وبيئة تشغيل لتنفيذ الاستعلامات؛ أما REST فهي اختصارٌ للعبارة Representational State Transfer وهي معمارية شهيرة لمشاركة البيانات عبر الويب، و RESTful API هي واجهة برمجة تطبيقات تتبع معايير REST، مثل: انعدام الحالة stateless، وقابلية التخزين في ذاكرة التخزين المخبئية Cache، واستقلال التقنيات المستخدمة في جانبي العميل والخادم عن بعضها، إضافةً إلى الواجهة المعيارية الموحدة التي تستخدم معرفات فريدة مثل عناوين URIs وغيرها. تُعد GraphQL أحدث من REST وقد بُنيت في الأساس لمعالجة نقاط ضعفها بإنشاء واجهة API عالية الكفاءة وموجهة للعميل. لنبدأ الآن الموازنة بينهما لنعرف مزايا وعيوب كل تقنية: البنية: تحتوي بنية REST نقاط وصول متعددة للتخاطب مع الخادم، أما GraphQL فتستخدم نقطة وصول وحيدة، وستعطيك مخططًا للبيانات التي طلبتها باستعلام واحد مهما كانت معقدة ومتشعبة، أما في REST فستحتاج عددًا من الاستعلامات للحصول على البيانات المتشعبة نفسها. إذًا، تتفوق GraphQL على REST في تخفيف الضغط على الشبكة. جلب البيانات: عندما تستعلم عن تفصيل ما من واجهة REST تجيبك بأكثر مما طلبت، وترسل لك مجموعة البيانات المرتبطة بهذا التفصيل كاملةً كما هي معرفة على الخادم، حتى لو كنت تحتاج رقمًا واحدًا منها فقط، وفي حالاتٍ أخرى لا تكون استجابتها كافية، فعلى سبيل المثال قد لا تعطيك نقطة الوصول الخاصة بقائمة ما على الخادم جميع الخصائص التي تريد معرفتها عن القائمة فستحتاج نقاط أخرى معها أيضًا، بينما تخلصك GraphQL باستعلاماتها التصريحية من هذا الإفراط أو التقصير في جلب البيانات، وتحضرها لك بالهيكلية التي يحددها العميل دون زيادة أو نقصان. التعامل مع الأخطاء: لا ينحصر استخدام GraphQL مع بروتوكول HTTP فقط، بل يمكنها الاعتماد على غيره أيضًا، لذا فهي لا تعتمد على رموز استجابة HTTP لعرض أخطاء طلباتها للعميل، فمعظم الطلبات العائدة من نقاط وصول GraphQL ستحمل الرمز 200 سواء كانت صحيحة أو خاطئة، وستجد ضمن استجابة الطلبات الخاطئة رسائل واضحة عن أخطائك جنبًا إلى جنب مع البيانات data وهذا بفضل خاصية الأخطاء errors، أما واجهة RESTful API فتعتمد كليًا على رموز استجابة HTTP، فيشير الرمز 200 دومًا للطلبات الصحيحة، وتعبّر رموز 400 عن الطلبات الخاطئة دون أي تفاصيل عن طبيعة الخطأ. الإصدارات: تسعى GraphQL في جميع تعديلاتها لتجنب التغيرات الجذرية breaking changes، التي من المحتمل أن تسبب أخطاء في جانب العميل، ويحاول مطوروها الحفاظ على التوافقية مع الإصدارات السابقة، فهي توفر إمكانية زيادة ميزات جديدة على الواجهة بإضافة أنواع بيانات جديدة وحقول جديدة بدون الحاجة لإنشاء إصدار جديد، على عكس REST التي تتعامل مع أي تعديل على أنه إصدار جديد لنقاط الوصول، ويُشار للإصدارات صراحةً في عناوين URL، فتجد فيها رموزًا مثل: V1/ أو V2/، ولهذه الآلية أيضًا مشكلاتها. يمكنك عمومًا التعامل مع تغيُّر الإصدارات في التقنيتين وإن كانت طريقة REST أكثر تقليدية. التخبئة Caching: تُعد التخبئة مبدأ من مبادئ REST، ولكونها تعتمد على توابع HTTP لجلب البيانات من نقاط وصولها المتعددة، فبوسعها الاستفادة من ميزة التخبئة -إحدى أساسيات HTTP- وعدم إعادة جلب الموارد نفسها من الخادم في كل مرة يحتاجها العميل. بالمقابل لا تستفيد GraphQL من ميزة التخبئة المُضمّنة في HTTP، لأنها تستخدم نقطة وصول وحيدة لجميع الطلبات، وكل طلب من طلباتها مخصص وفريد عن غيره، ومع ذلك يستطيع عملاء GraphQL استخدام التخبئة بصورة مبسطة باستعمال المُعرّف العمومي للكائن. لنختتم الفقرة الآن بجدول يلخص أوجه الشبه والاختلاف بين GraphQL و REST، واضعًا في حسبانك إمكانية استخدامهما معًا في مشروع واحد، فتكون GraphQL مثل بوابة أو نقطة تجميع تتلقى البيانات الواردة من خدمات REST. الميزة GraphQL REST الوصف GraphQL هي لغة استعلام لواجهات برمجة التطبيقات، ووقت تشغيل من جانب الخادم لتنفيذ الاستعلامات REST هي نمط معماري لتصميم خدمات الويب جلب البيانات عبر نقطة وصول وحيدة تستقبل طلبات الاستعلام المحددة بدقة من طرف العميل والمنقولة ببروتوكول HTTP عبر عدة نقاط وصول HTTP تعيد للعميل مجموعة بيانات محددة مسبقًا من قبل الخادم الإصدارات غير شائعة شائعة رموز استجابة HTTP كافة الاستجابات حتى الخاطئة منها تعود بالرمز 200 تطبق رموز استجابة HTTP بدلالتها المعروفة التحقق التحقق من البيانات الوصفية مضمن في GraphQL المطور يجري عمليات التحقق بنفسه يدويًا التوثيق التوثيق ذاتي بفضل وجود نظام النوع وميزة الاستقراء لا يوجد توثيق ذاتي لكن يمكنك الاستفادة من أدوات خاصة مثل OpenAPI التخبئة غير متاح متاح أساليب الطلب جميع الطلبات سواء كانت استعلامات أو طفرات أو اشتراكات تُرسل بأسلوب POST عبر بروتوكول HTTP تستخدم كل أساليب HTTP مثل GET و POST و PUT و PATCH و DELETE وغيرها صيغة الاستجابة JSON بأي صيغة مثل JSON و XML و HTML وغير ذلك الخاتمة كان هذا المقال الأول من سلسلة مقالات تتناول إدارة البيانات باستخدام GraphQL، وقد تعرفنا فيه على GraphQL لغة الاستعلام مفتوحة المصدر الخاصة بواجهات برمجة التطبيقات، هذه اللغة التي طورتها فيسبوك موجهةً إياها كليًا لتلبية طلبات العميل، عبر كتابة استعلامات تصريحية يوضح فيها ما يحتاجه فعلًا، لتتجاوز بذلك مشكلات REST التقليدية، مثل الإفراط أو التقصير في جلب البيانات، إضافةً إلى انخفاض الكفاءة على الشبكة. لا يعتبر المقال GraphQL بديًلا لواجهات REST، فلكلٍ منهما تطبيقات خاصة تتميز فيها عن الأخرى، وأيضًا طريقة خاصة في إدارة البيانات المتبادلة بين العميل والخادم، وقد عرضنا هنا طريقة GraphQL في عرض البيانات، تابع معنا بقية مقالات السلسلة لتطلع على المزيد. ترجمة -وبتصرف- للمقال An Introduction to GraphQL لصاحبته Tania Rascia. اقرأ المزيد مدخل إلى المكتبة GraphQL واستعمالاتها في بناء تطبيقات الويب الحديثة. شرح فلسفة RESTful - تعلم كيف تبني واجهات REST البرمجية. دليلك الشامل إلى برمجة التطبيقات.
  3. تُسهّل GraphQL الاتصال بين الواجهة الأمامية لتطبيقك وقاعدة البيانات عبر استعلاماتها التصريحية والموجهة بطلبات العميل، والبنية التي توضح إمكانات هذه الاستعلامات وتفاصيلها هي مخططات GraphQL المحكومة بنظام النوع، فما هو نظام النوع؟ وكيف يساعدك فهمه على بناء مخططات فعالة تحقق لك الفائدة المرجوة من GraphQL؟ سنعرض ذلك مع بعض الأمثلة التطبيقية على كل نوع بدءًا من الأنواع الخمسة المفردة المبنية مسبقًا built-in scalar، والتعدادات Enums، والقوائم والواجهات Interfaces والأنواع غير الفارغة الغالفة non-null wrapping، ونوع الكائن Object، وأنواع التجريد Abstract والاتحاد Union أيضًا. متطلبات العمل ستحتاج هذه الأساسيات لتطبيق الأمثلة الموجودة هنا: الاطلاع على المقال الأول مقدمة إلى GraphQL لفهم المبادئ الأساسية للتقنية. بيئة عمل تحوي خادم GraphQL، يمكنك تجهيزها بتنفيذ الخطوات الموجودة في مقالنا الثاني إعداد خادم GraphQL في بيئة Node.js. الأنواع المفردة Scalar إذا شبهت استجابة GraphQL بالشجرة متفرعة الأغصان فستكون البيانات المفردة هي الأوراق في نهاية كل غصن، ففي نهاية الأمر تتحلل جميع البيانات في مخطط GraphQL مهما بلغ تعقيدها وتداخلها إلى بيانات مفردة، وللبيانات المفردة خمسة أنواع: نوع العدد الصحيح Int Int هو نوع عدد صحيح لا يحتوي فاصلة عشرية، ومُؤشّر بإشارة موجب أو سالب، ويتكون من 32 بت، لذا تتراوح قيمته بين "2,147,483,647-" و "2,147,483,647"، والعدد الصحيح هو واحد من نوعين فقط لتمثيل الأعداد في GraphQL. نوع العدد العشري Float Float هو نوع عدد عشري ثنائي الدقة، مثل 1.2، ومُؤشّر بإشارة موجب أو سالب، وهو النوع المفرد الثاني لتمثيل الأعداد في GraphQL. نوع السلسلة النصية String String هو نوع سلسلة محارف بترميز UTF-8 تُستخدم لكتابة النصوص والأعداد الكبيرة جدًا، وتُعد السلاسل النصية أكثر الأنواع المفردة استخدامًا. النوع البولياني Boolean يحمل النوع البولياني Boolean إما قيمة صحيحة true أو خاطئة false. نوع الرقم التعريفي ID ID هو رقم تعريفي فريد، يُمثّل دائمًا بصيغة سلسلة نصية حتى لو تضمن أرقامًا، ويُولّد غالبًا بواسطة خوارزميات المُعرّف العالمي الفريد Universally Unique Identifier أو اختصارًا UUID. أنواع مفردة مخصصة تغطي الأنواع المفردة الخمسة السابقة معظم حالات الاستخدام، لكنك قد تحتاج أنواعًا أخرى تناسب طبيعة تطبيقك، مثل الوقت Time، والتاريخ Date، وعناوين url، والتي يمكنك تعريفها باستعمال الكلمة المفتاحية scalar، وسيعتمد الخادم عندها هذه الأنواع معيارًا للتحقق من صحة بياناتك المدخلة ومطابقتها لمواصفات النوع المخصص الذي تنتمي إليه. إليك مثالًا عن تعريف نوع التاريخ Date: scalar Date يستخدم خادم GraphQL الوحدة البرمجية GraphQLScalarType للتعامل مع الأنواع الجديدة المُضافة. نوع التعداد Enum يصف التعداد مجموعةً من القيم المحتملة للعنصر. لو طبقت الأمر على اللعبة التي استخدمناها مثالًا في مقالات السلسلة، ستجد أنك تحتاج نوع التعداد عند تعريف وظائف شخصيات اللعبة Job، وأنواعهم Species، وأسلحتهم وما إلى ذلك؛ فلكل شخصية قيم محددة لا ينبغي أن يقبل النظام غيرها. يُعرّف نوع التعداد باستعمال الكلمة المفتاحية enum، وتكتب التعدادات ضمنه بالحروف الإنجليزية الكبيرة كما يلي: "The job class of the character." enum Job { FIGHTER WIZARD } "The species or ancestry of the character." enum Species { HUMAN ELF DWARF } تضمن بهذه الطريقة أن وظيفة الشخصية Job ستكون إما مقاتل FIGHTER أو ساحر WIZARD فقط، ولن يقبل الخادم أي كلمة أخرى مهما كانت؛ أما لو عرّفت وظيفة الشخصية على أنها سلسلة نصية عادية String، فستكون جميع الكلمات سيان عند الخادم وسيقبل أي مجموعة محارف تُعطى له. تفيدك التعدادات أيضًا في تمرير القيم الوسيطة، إذ يمكنك مثلًا تعريف تعداد enum لتحديد إذا كان سلاح الشخصية يُحمَل بيد واحدة مثل السيف أم أنه ثقيل مثل الفأس ويحتاج لكلتا اليدين، سيساعدك هذا التعداد على التحكم بتجهيز الشخصية بسلاح واحد أو بسلاحين، لنسمي التعداد اليد Hand، ونعرّفه بالتعليمات التالية: enum Hand { SINGLE DOUBLE } "A valiant weapon wielded by a fighter." type Weapon { name: String! attack: Int range: Int hand: Hand } type Query { weapons(hand: Hand = SINGLE): [Weapon] } صُرّح عن التعداد Hand بحالتين لاستعمال اليد، مفردة SINGLE، ومزدوجة DOUBLE، أما الوسيط المسند إلى الحقل weapons في الاستعلام فيحمل القيمة الافتراضية SINGLE، وهي القيمة التي يأخذها في حال لم تمنحه قيمة مغايرة. النوع غير الفارغ Non-Null قد تستغرب عدم وجود النوع null أو undefined بين أنواع GraphQL المفردة رغم أنه يُعدّ نوعًا شائعًا في معظم لغات البرمجة، ويرجع السبب في ذلك إلى أن جميع أنواع GraphQL تقبل القيمة Null افتراضيًا وفي حال أردت تغيير الأمر عليك استخدام إشارة التعجب مع اسم النوع. يُعرّف النوع غير الفارغ Non-Null في GraphQL بأنه مُعدِّل للأنواع الأخرى، فمثلًا الحقل String هو حقل اختياري قد يحمل قيمة أو لا، أما الحقل من النوع !String فهو إجباري أي لا يجب أن يكون فارغًا. نوع القائمة List نوع القائمة هو أيضًا من الأنواع المُعدِّلة للأنواع الأخرى، فأي نوع يوضع بين قوسين مربعين [] يتحول لقائمة. على سبيل المثال يُعرّف النوع [Int] قائمةً من نوع الأعداد الصحيحة Int والنوع [String] قائمةً من السلاسل النصية String وقِس على ذلك، ويمكنك استخدام القائمة مع إشارة التعجب لتحصل على نوع غير فارغ ومُعرّف على أنه قائمة مثل ![String]. نوع الكائن Object شبهنا الأنواع المفردة بأوراق الشجرة في بداية المقال، وتبعًا لهذا التشبيه تكون الكائنات هي الأغصان، وهي معظم ما سنكتبه في مخطط GraphQL. يبدأ تعريف الكائن بالكلمة المفتاحية type، ويحتوي كل كائن على حقل واحد على الأقل. تُسمى الحقول بالمفاتيح وبجانب كل حقل تجد نوع القيمة التي يقبلها. لا يجوز أن تبدأ أسماء الحقول برمز الشرطتين السفليتين __، فهو مخصص لنظام الاستقراء. لو أعطينا مثالًا في سياق أمثلتنا السابقة عن اللعبة، فيمكنك إنشاء كائن يمثل المقاتلين Fighter: "A hero with direct combat ability and strength." type Fighter { id: ID! name: String! level: Int active: Boolean! } يمتلك هذا الكائن أربعة حقول: id رقم تعريفي من نوع ID غير فارغ. name سلسلة نصية String غير فارغة. level عدد صحيح Int. active قيمة منطقية Boolean غير فارغة. ونود الإشارة هنا إلى قدرتك على كتابة تعليق قبل تعريف الكائن مباشرةً بين علامتي اقتباس، مثل: "A hero with direct combat ability and strength." وسيظهر تعليقك بمثابة الوصف للكائن. قد تكون حقول الكائن بيانات مفردة كما في مثالنا السابق أو كائنات أخرى. ألقِ نظرةً على المثال أدناه، إذ عرّفنا في البداية كائنًا من نوع السلاح Weapon يتضمن أسماء الأسلحة وطبيعة هجومها ومدى تأثيرها، ثم أضفنا السلاح weapon على أنه أحد حقول كائن المقاتل Fighter وهو weapon. يمكن لمخطط GraphQL أن يُضبط بحيث يعالج الحقل weapon الكائن Weapon: "A valiant weapon wielded by a fighter." type Weapon { name: String! attack: Int range: Int } "A hero with direct combat ability and strength." type Fighter { id: ID! name: String! level: Int active: Boolean! weapon: Weapon } يمكن أن تتداخل الكائنات ببعضها ويشترك أكثر من كائن بالحقل نفسه. أنواع عمليات الجذر عمليات الجذر هي أنواع خاصة من الكائنات تنطبق عليها جميع قواعد الكائنات، تمثل جذور مخطط GraphQL وتسمى نقاط دخول entrypoints، ولها ثلاثة أنواع هي الاستعلامات والطفرات والاشتراكات. تُعرّف هذه العمليات ضمن كائن الجذر schema في مخطط GraphQL، والمميز بالكلمة المفتاحية schema. ألقِ نظرةً على التعليمات التالية: schema { query: Query mutation: Mutation subscription: Subscription } لنبدأ بنوع الاستعلام Query type، وهو عملية أساسية لا غنى عنها في أي مخطط GraphQL، ويمثّل عملية قراءة، ويقابل الطلب GET في واجهة REST API. ألقِ نظرةً على تعريف استعلام الجذر Query للعبة نفسها، إذ يُرجِع هذا الاستعلام قائمة بالمقاتلين: type Query { fighters: [Fighter] } أما الطفرات فتمثل طلبات الكتابة لأنها تعدل على المخطط، فهي تشبه طلبات PUT و DELETE و POST في REST API. يبين المثال أدناه الطفرة Mutation التي تستخدم الوسيط input لإضافة مقاتل addFighter: type Mutation { addFighter(input: FighterInput): Fighter } عملية الجذر الأخيرة هي الاشتراكات Subscription، التي تفيدك في حالات تدفق الأحداث بين الخادم والعميل، وتعمل بتوافق تام مع تقنية WebSocket. بالعودة للعبة، يمكنك استخدام الاشتراكات لتمثيل الاصطدامات العشوائية في المعارك مثلًا على النحو التالي: type Subscription { randomBattle(enemy: Enemy): BattleResult } وسطاء الحقول Field Arguments حقول الكائن في GraphQL هي دوال تقليدية ترجِِع قيمًا وتقبل متغيراتٍ وسيطة مثلها مثل الدوال الأخرى، وتنتمي وسطاء الحقل إلى أي نوع غير فارغ من أنواع GraphQL باستثناء الكائنات، وتُعرّف بكتابة اسم المتغير يليه نوعه. ألقِ نظرةً على المثال التالي لترشيح الكائن Fighter باستخدام الحقل id، ولاحظ إشارة التعجب بعد النوع للدلالة على عدم قبول قيم ID الفارغة. type Query { fighter(id: ID!): Fighter } تلعب الوسطاء في هذا المثال دورًا في ترشيح كائن المقاتل Fighter بحسب الرقم التعريفي، فتمكّنك من جلب عنصر محدد من مخزن البيانات، وتستطيع استخدامها لأي استعلامات أخرى تريدها. نوع الواجهة Interface تشبه بنية نوع الواجهة بنية الكائنات، فهي تتكون من عدة حقول لكل منها اسم ونوع، والغاية منها وضع نموذج مشترك يمكن تطبيقه على مجموعة كائنات تمتلك حقولًا مشتركة. أنشأنا في الفقرات السابقة كائن المقاتل Fighter وهو يشترك في بعض حقوله مع كائنات أخرى تلعب أدوارًا في اللعبة، مثل المعالجين Healer والسحرة Wizard وغيرهم. يمكنك في هذه الحالة إنشاء واجهة تتضمن الحقول المشتركة بين جميع هذه الشخصيات، ثم إنشاء كائنات منفصلة لكل شخصية تكون تطبيقًا للواجهة. ألقِ نظرةً على المثال التالي لتوضيح الأمر. أنشئ في البداية واجهة تسمى BaseCharacter تبدأ بالكلمة المفتاحية interface وتتضمن كل الحقول المشتركة بين الشخصيات، كما يلي: "A hero on a quest." interface BaseCharacter { id: ID! name: String! level: Int! species: Species job: Job } لكل شخصية في اللعبة رقم تعريفي خاص id واسم name ومستوى level ونوع species وعمل job. خذ مثلًا نوع المقاتل Fighter ونوع الساحر Wizard لدى كل منهم جميع الحقول الموجودة في الواجهة BaseCharacter مع بعض الاختلاف؛ فالمقاتل يستعمل الأسلحة Weapon؛ والساحر يستعمل التعويذات Spell. يمكنك إذًا تطبيق الواجهة لتسهيل تعريف النوعين، وذلك باستخدام الكلمة المفتاحية implements، وفق التالي: "A hero with direct combat ability and strength." type Fighter implements BaseCharacter { id: ID! name: String! level: Int! species: Species job: Job! weapon: Weapon } "A hero with a variety of magical powers." type Wizard implements BaseCharacter { id: ID! name: String! level: Int! species: Species job: Job! spells: [Spell] } تذكر أن شرط تطبيق الواجهة على كائن معين هو امتلاك الكائن لجميع حقول الواجهة دون استثناء. نوع الاتحاد Union يمثل نوع الاتحاد مجموعة كائنات تصلح لأن تكون استجابة لاستعلام واحد. لننشئ مثلًا اتحادًا لشخصيات اللعبة Character تتضمن كافة كائنات الشخصيات، وهي في حالتنا كائنات المقاتل Fighter والساحر Wizard. union Character = Wizard | Fighter يبدأ تعريف الاتحاد بالكلمة المفتاحية Union، والتي تكون مدخلاتها إما كائنات أو واجهات فهي لا تقبل الأنواع المفردة. ترمز إشارة اليساوي = ضمن التعريف إلى إسناد القيمة، أما الخط الرأسي | فيكافئ عبارة OR المنطقية. الآن، إذا رغبت بالاستعلام عن شخصيات اللعبة فيمكنك استخدام الاتحاد Character ليستجيب الخادم بقائمة تضم جميع الكائنات من نوع مقاتل Fighter أو ساحر Wizard. الخاتمة تعرّفنا في هذا المقال على أنواع GraphQL المختلفة، شبهنا المخطط بالشجرة فكانت الأنواع المفردة بمثابة الأوراق التي ينتهي إليها كل شيء، فهي أنواعٌ تساهم بتشكيل الأنواع الأخرى، ولها خمسة أنواع أساسية هي Int و Float و String و Boolean و ID ويمكنك إنشاء أنواعك الخاصة، ثم عرضنا نوع التعداد الذي يمنحك تحكمًا أكبر في استجابة الخادم فتزوده بمجموعة محددة من القيم الصالحة، وبعدها اطلعنا على مُعدِّلات الأنواع أي القائمة والقيم غير الفارغة التي تُغيّر طبيعة الأنواع الأخرى، وتوسعنا في الكائنات لأنها عماد مخططات GraphQL فهي أشبه بأغصان الشجرة بما فيها من كائنات جوهرية تتمثل في عمليات الجذر أي الاستعلامات والطفرات والاشتراكات، ولا ننسَ الواجهات والروابط التي تُسهّل علينا التعامل مع الكائنات ذات الخصائص المشتركة. لديك الآن أساسٌ نظري جيد ننصحك ببناء خادمك الخاص بمساعدة مقال إعداد خادم GraphQL في بيئة Node.js والتدرب عمليًّا على تطبيق الأنواع لحين إتقانها. ترجمة -وبتصرف- للمقال Understanding the GraphQL Type System لصاحبته Tania Rascia. اقرأ أيضًا المقال السابق إعداد خادم GraphQL في بيئة Node.js دليلك الشامل إلى أنواع البيانات مقدمة إلى Node.js كتابة أول برنامج في بيئة Node.js وتنفيذه مدخل إلى المكتبة GraphQL واستعمالاتها في بناء تطبيقات الويب الحديثة النسخة الكاملة لكتاب البرمجة باستخدام Node.js
  4. GraphQL لغة استعلام مفتوحة المصدر لواجهات برمجة التطبيقات API وبيئة تشغيل لتنفيذ الاستعلامات، وأُنشِئَت في الأساس لمعالجة مشكلات REST التقليدية وقد عرضنا بعضًا منها، وسنطّلع في هذا المقال على آلية تفاعل مكونات GraphQL مع بعضها، عبر بناء خادم GraphQL API في بيئة Node.js، وسيُستخدم خادم Express API بدلًا من Apollo GraphQL رغم أنه الأشهر بين تطبيقات GraphQL. ستتعلم خلال متابعتك لخطوات العمل إنشاء مخطط GraphQL متوافق مع نظام الأنواع بما فيه من عمليات، مثل الاستعلامات والطفرات ومحللات الاستجابة، وستتآلف مع بيئة التطوير المتكاملة GraphiQL، إذ سنستعملها لاستعراض المخطط وتصحيح أخطائه، ولطلب استعلامات العميل من واجهة GraphQL. متطلبات العمل جهّز المتطلبات التالية لتتابع معنا سير العمل: بيئة Node.js جاهزة على خادمك المحلي، ويمكنك الاسترشاد بأحد المقالين تثبيت Node.js على نظام أبونتو 18.04 أو كيفية تثبيت Node.js على Debian 8 لإعدادها. فهم أساسيات GraphQL. الإلمام ببروتوكول HTTP، ننصحك بقراءة مدخل إلى HTTP. معرفة أساسية بكل من HTML و JavaScript، تفيدك هذه المقالات أساسيات إنشاء موقع ويب باستخدام تعليمات HTML، وأساسيات لغة جافاسكربت. إعداد خادم Express HTTP يبدأ إعداد خادم Express بتثبيت الحزمتين express و cors، باستخدام الأمر npm install ضمن مشروع جديد وفق التالي: npm install express cors Express هو إطار لتطبيقات الويب المبنية على Node.js، وهو مصمم خصيصًا لبناء واجهات برمجة التطبيقات، وسيكون إطار العمل الخاص بخادمنا، أما الحزمة CORS وهي اختصار لمصطلح البرمجيات الوسيطة لمشاركة الموارد ذات الأصل المختلط Cross-Origin Resource Sharing، إذ إن الغاية منها هي السماح لمتصفحك بالوصول إلى موارد الخادم. ثبّت أيضًا الاعتمادية Nodemon مع الراية D بصفتها اعتمادية تطوير، لتستخدمها في مرحلة التطوير فقط: npm install -D nodemon Nodemon هي أداة مساعدة في تطوير تطبيقات Node، تعيد تشغيل التطبيق في كل مرة يطرأ فيها تغيير على ملفات المشروع الموجودة في مجلد الجذر لتأخذ التعديلات مفعولها. سينشأ بتثبيت هذه الحزم المجلد node_modules والملف package.json مع اعتماديتي إنتاج واعتمادية تطوير. إذا فتحت الملف package.json بمحرر نصوص مثل نانو nano ستجده يحتوي المعلومات التالية: { "dependencies": { "cors": "^2.8.5", "express": "^4.17.3" }, "devDependencies": { "nodemon": "^2.0.15" } } عدّل عليه ليصبح وفق التالي: { "main": "server.js", "scripts": { "dev": "nodemon server.js" }, "dependencies": { "cors": "^2.8.5", "express": "^4.17.3" }, "devDependencies": { "nodemon": "^2.0.15" }, "type": "module" } لنشرح التعديلات السابقة، إذ وضعنا ملف الخادم "server.js" -الذي سننشئه بعد قليل- في الحقل main، وبهذا تتأكد من تشغيل npm start للخادم؛ أما الملف النصي المسمى "dev"، فيُسهل التطوير إذ يتضمن أمر تشغيل nodemon server.js. أضفنا في السطر الأخير القيمة module لحقل النوع type لنُفعّل إمكانية استخدام تعليمة الاستيراد import عوضًا عن require -تعليمة CommonJS الافتراضية- وفي هذا أيضًا تسهيلٌ للعمل. احفظ التغييرات، وأغلق الملف، لننتقل لإنشاء ملف الخادم. سنبني الآن خادم Express بسيط عبر إنشاء ملف نصي باسم "server.js"، يتضمن التعليمات المبينة أدناه، يعمل هذا الخادم على المنفذ 4000، وتظهر في صفحته الأولى عبارة الترحيب !Hello, GraphQL: import express from 'express' import cors from 'cors' const app = express() const port = 4000 app.use(cors()) app.use(express.json()) app.use(express.urlencoded({ extended: true })) app.get('/', (request, response) => { response.send('Hello, GraphQL!') }) app.listen(port, () => { console.log(`Running a server at http://localhost:${port}`) }) لاحظ أن الملف يبدأ بتعليمة استيراد الدالة express، ثم بعض الإعدادات الخاصة بكل من CORS و JSON، وبعدها استعملنا ('/')app.get لتحديد ما ينبغي إرساله إلى الجذر / ضمن طلبات GET، وفي نهاية الملف استخدمنا ()app.listen لتحديد منفذ خادم API. احفظ الآن التغيرات، وأغلق الملف، ثم نفذ الأمر التالي لتشغيل خادم Node: npm run dev تأكد -قبل الانتقال للخطوات التالية- من سلامة تشغيل الخادم بظهور صفحة الترحيب !Hello, GraphQL عند فتح الرابط "http://localhost:4000" من المتصفح أو تشغيل الأمر: curl http://localhost:4000 إعداد البرمجية الوسيطة لخادم GraphQL HTTP يوجد ثلاث خطوات أساسية لتحقيق التكامل بين مخطط GraphQL مع خادم إكسبريس، وتتلخص في إعداد الاتصال مع مخزن البيانات، وتعريف المخطط، وتعريف محللات الاستجابة. ستحتاج في البداية لتنفيذ الأمر التالي: npm install graphql@14 express-graphql @graphql-tools/schema الذي يثبت الحزم الضرورية للعمل مع GraphQL، وهي: graphql: الحزمة المرجعية التي تستخدمها لغة جافا سكربت لجعل التعامل مع GraphQL متاحًا في اللغة. express-graphql: البرمجية الوسيطة لخادم HTTP للتعامل مع GraphQL. graphql-tools/schema@: مجموعة أدوات تُسرّع تطوير GraphQL. ضِف تعليمات استيراد هذه الحزم إلى الملف "server.js"، لتصبح بدايته على النحو التالي: import express from 'express' import cors from 'cors' import { graphqlHTTP } from 'express-graphql' import { makeExecutableSchema } from '@graphql-tools/schema' ... لننجز الآن إعدادات الاتصال مع مخزن البيانات قبل إنشاء مخطط GraphQL القابل للتنفيذ. أنشئ الكائن data بإضافة التعليمات الخاصة به إلى الملف "server.js" مباشرةً بعد تعليمات الاستيراد كما هو مبين أدناه، إذ يمثل هذا الكائن مخزنًا للبيانات في الذاكرة in-memory store، ويحتوي مجموعة بيانات بسيطة لنستعلم عنها من خادم GraphQL. نخفف بهذه الطريقة عبء إنشاء قاعدة بيانات حقيقية والاتصال معها كوننا نجهز بيئة تعليمة بسيطة. import express from 'express' import cors from 'cors' import { graphqlHTTP } from 'express-graphql' import { makeExecutableSchema } from '@graphql-tools/schema' const data = { warriors: [ { id: '001', name: 'Jaime' }, { id: '002', name: 'Jorah' }, ], } … تمثّل هيكلة البيانات جدولًا بسيطًا يدعى warriors، الذي يحتوي سطرين 'Jaime' و 'Jorah' لبيانات اثنين من محاربي لعبة افتراضية سنستخدمها في مثالنا. ملاحظة: لا يغطي هذا المقال حالة اتصال خادم GraphQL بقاعدة بيانات حقيقية، وعادةً ما يستخدم خادم GraphQL دوال الاختزال reducers للوصول إلى قواعد البيانات والتعديل عليها، ويعتمد تقنياتٍ مثل Prisma، وهي أداة ربط العلاقات بالكائنات ORM، أما إعدادات الاتصال مع قاعدة البيانات فيحملها المعامل context أحد معاملات محللات الاستجابة غير المتزامنة. سيمثل المتغير data إعدادات اتصالنا مع مخزن البيانات حتى نهاية هذا المقال. الآن، بعد تثبيت الحزم المطلوبة، وتجهيز بعض البيانات للاستعلام، يمكننا إنشاء المخطط الذي يُعرّف واجهة API، ويصف ماهية البيانات الممكن الاستعلام عنها. مخطط GraphQL سننشئ مخططًا أوليًّا لواجهة API يتضمن الحد الأدنى من التعليمات البرمجية التي تسمح لنا بالاتصال مع نقطة الوصول GraphQL Endpoint. سنفترض في هذا المثال أننا نستعلم عن بيانات لعبة مغامرات فيها أدوار لشخصيات مختلفة، مثل المحاربين والسحرة والمعالجين وغيرهم، لذا سنحرص عند إنشاء المخطط أن يكون مرنًا وقابلًا لإعادة الاستخدام، لتستطيع تكراره مع أي نوع من شخصيات اللعبة وتوسعته ليشمل متغيرات أخرى تخصهم مثل الأسلحة والتعويذات. تستند مخططات GraphQL إلى نظام الأنواع، ويمكنك دائمًا إنشاء أنواعك الخاصة إذا لم تلبِ الأنواع الافتراضية المُضمّنة في النظام حاجتك، وهذا ما سنفعله هنا إذ سننشئ نوعًا type يدعى المحارب warrior له حقلين id و name. type Warrior { id: ID! name: String! } الحقل id نوعه ID رقم تعريفي، والحقل name نوعه سلسلة نصية String، وكلاهما من الأنواع الأولية المُضمّنة في نظام الأنواع، والأنواع الأولية تستخدم لإنشاء أنواع أعقد مثل الكائنات، أما إشارة التعجب ! الواردة في التعليمات أعلاه فتشير لكون الحقل بجانبها لا يقبل قيمة فارغة "Null"، فلا يجب أن يكون فارغًا بأية حال. الآن بعد إنشاء نوع المحارب، سننشئ نوع الاستعلام Query، وهو نقطة الدخول entry point إلى استعلام GraphQL. ألقِ نظرةً على التعليمات المبينة أدناه، إذ يُعرّف هذا النوع المحاربين warriors على أنهم مصفوفة من البيانات التابعة لنوع المحارب Warrior. type Query { warriors: [Warrior] } أصبح مخططنا جاهزًا للاستخدام مع البرمجية الوسيطة GraphQL HTTP، لذا سنمرره للدالة makeExecutableSchema، إحدى أدوات graphql-tools، بصفته typeDefs ليصبح قابلًا للتنفيذ. للدالة makeExecutableSchema معاملان أحدهما للمخطط والآخر لمحللات الاستجابة، وينبغي تعريفها قبل تنفيذ الدالة: typeDefs: لغة مخطط GraphQL، وهي سلسلة نصية. resolvers: محللات الاستجابة، وهي الدوال التي تُستدعى لتنفيذ حقل معين وإرجاع قيمة. اكتب تعليمات تعريف typeDefs ضمن الملف "server.js" تمامًا بعد تعليمات استيراد الاعتماديات، وأسند له قيمة نصية هي مخطط GraphQL، وفق التالي: ... const data = { warriors: [ { id: '001', name: 'Jaime' }, { id: '002', name: 'Jorah' }, ], } const typeDefs = ` type Warrior { id: ID! name: String! } type Query { warriors: [Warrior] } ` ... إذًا، فقد جهزّنا إلى الآن البيانات التي سنستعلم عنها data، وعرّفنا المخطط ضمن typeDefs، وسنعرّف في الخطوة التالية محللات الاستجابة، التي ستحدد طريقة استجابة خادمنا للطلبات الواردة. محللات الاستجابة محللات الاستجابة Resolvers هي مجموعة من الدوال البرمجية التي توّلد استجابة خادم GraphQL، ولكل محلل استجابة أربعة معاملات: obj: الكائن الأب، غير ضروري في حالتنا لأن الكائن المستخدم لدينا وحيد وهو الأب أو الجذر. args: تمرر هنا أي متغيرات وسيطة يحتاجها GraphQL. context: حقل مشترك بين جميع محللات الاستجابة، ويُخصص غالبًا لمعلومات الاتصال مع قاعدة البيانات. info: معلومات إضافية. سننشئ الآن محلل استجابة لاستعلام الجذر Query، يُرجع لطالب الاستعلام معلوماتٍ عن المحاربين warriors، وسنمرّر له مخزن البيانات في الذاكرة الذي عرّفناه سابقًا. ضِف ما يلي إلى الملف "server.js": ... const typeDefs = ` type Warrior { id: ID! name: String! } type Query { warriors: [Warrior] } ` const resolvers = { Query: { warriors: (obj, args, context, info) => context.warriors, }, } ... أنشأنا بهذه التعليمات دالة محلل استجابة تدعى warriors، ترتبط بالاستعلام Query وهو نقطة دخولنا إلى خادم GraphQL، ومهمة هذه الدالة هي إرجاع بيانات المحاربين من جدول المحاربين warriors الموجود في قاعدة البيانات التي يشير إليها المعامل context، وهي في حالتنا مخزن البيانات البسيط data. إذًا، فلكل دالة محلل استجابة أربعة معاملات: obj و args و info، إضافةً إلى context، الذي كان أكثرها ارتباطًا بمخططنا، وهذا المعامل مشترك بين جميع محللات الاستجابة، ويشير غالبًا إلى إعدادات الاتصال بين خادم GraphQL وقاعدة البيانات. الآن، بعد أن عرّفنا typeDefs و resolvers معاملات الدالة makeExecutableSchema، يمكننا تحويل المخطط إلى مخطط قابل للتنفيذ بكتابة الأوامر التالية في الملف "server.js": ... const resolvers = { Query: { warriors: (obj, args, context, info) => context.warriors, }, } const executableSchema = makeExecutableSchema({ typeDefs, resolvers, }) ... تنشئ الدالة makeExecutableSchema مخططًا كاملًا يمكنك تمريره إلى نقطة وصل GraphQL. لنستبدل نقطة الوصل الحالية أي عبارة الترحيب !Hello, GraphQL بنقطة الوصول graphql/ عبر كتابة التعليمات التالية: ... const executableSchema = makeExecutableSchema({ typeDefs, resolvers, }) app.use( '/graphql', graphqlHTTP({ schema: executableSchema, context: data, graphiql: true, }) ) ... يتطلب استخدام البرمجية الوسيطة graphqlHTTP مخططًا أنشأناه وسياقًا يمثّل معلومات الاتصال مع مخزن البيانات، وينص الاتفاق على استخدام خادم GraphQL نقطة الوصول graphql/. إليك المحتوى النهائي للملف "server.js" بعد جميع التعديلات: import express from 'express' import cors from 'cors' import { graphqlHTTP } from 'express-graphql' import { makeExecutableSchema } from '@graphql-tools/schema' const app = express() const port = 4000 // مخزن البيانات في الذاكرة const data = { warriors: [ { id: '001', name: 'Jaime' }, { id: '002', name: 'Jorah' }, ], } // المخطط const typeDefs = ` type Warrior { id: ID! name: String! } type Query { warriors: [Warrior] } ` // محلل الاستجابة للمحاربين const resolvers = { Query: { warriors: (obj, args, context) => context.warriors, }, } const executableSchema = makeExecutableSchema({ typeDefs, resolvers, }) app.use(cors()) app.use(express.json()) app.use(express.urlencoded({ extended: true })) // نقطة الدخول app.use( '/graphql', graphqlHTTP({ schema: executableSchema, context: data, graphiql: true, }) ) app.listen(port, () => { console.log(`Running a server at http://localhost:${port}`) }) احفظ التغييرات على الملف وأغلقه. تكتمل بذلك واجهة برمجة التطبيقات، يمكنك الآن فتح الرابط "http://localhost:4000/graphql"، واستعراض مخطط GraphQL وتصحيح أخطائه باستخدام بيئة التطوير المتكاملة GraphiQL IDE، وهو موضوع القسم التالي من المقال. استخدام GraphiQL IDE الخطوة الأولى لاستخدام بيئة التطوير GraphiQL IDE هي تفعيلها بإسناد القيمة true للخيار graphiql في كتلة التعليمات الخاصة بالبرمجية الوسيطة ضمن ملف الخادم، ولو رجعت للتعليمات السابقة ستجدها مُفعلة. تعمل GraphiQL ضمن المتصفح، وتفيدك في كتابة استعلامات GraphQL، والتحقق من صحتها، واختبارها لضمان خرج صحيح من الخادم. تنقسم شاشة GraphiQL إلى قسمين، القسم الأيسر لكتابة التعليمات والأيمن لعرض النتائج. سنجري الآن استعلامًا عن المحاربين warriors، لذلك اطلب المُعرّف id والاسم name لكل محارب بكتابة التعليمات التالية في القسم الأيسر من الشاشة: { warriors { id name } } اضغط بعدها على الزر التشغيل أعلى يسار الشاشة، وستظهر نتيجة استعلامك في القسم الأيمن بصيغة JSON، وفق التالي: Output{ "data": { "warriors": [ { "id": "001", "name": "Jaime" }, { "id": "002", "name": "Jorah" } ] } } حاول تعديل الاستعلام، استعلم مثلًا عن الاسم name فقط: { warriors { name } } ولاحظ تغير النتيجة تبعًا لاستعلامك، فهي تشمل الآن أسماء المحاربين دون أرقامهم: Output{ "data": { "warriors": [{ "name": "Jaime" }, { "name": "Jorah" }] } } تعكس هذه التجربة واحدةً من أقوى خصائص GraphQL، وهي أنها موجهة بطلبات العميل [Client-driven]()، إذ تسمح له بالاستعلام عن الحقول التي يحتاجها فقط، وتعرضها هي بالذات دون زيادة أو نقصان. تفيدك GraphiQL أيضًا باستعراض تفاصيل المخطط، فكل ما تحتاجه هو الضغط على زر التوثيق Docs في يمين الشاشة، وسيظهر لك شريط جانبي خاص بالتوثيقات. لننتقل إلى القسم الأخير من مقالنا، وفيه محاكاة لتقديم طلبات استعلام فعلية من العميل إلى واجهة GraphQL API. إرسال طلب استعلام لواجهة GraphQL API من طرف العميل يمكنك إرسال طلبات الاستعلام إلى واجهة GraphQL عبر بروتوكول HTTP بالطريقة نفسها المتبعة مع واجهات REST، وتستطيع أيضًا استعمال واجهات APIs المُضمّنة في المتصفحات مع GraphQL، ومن أمثلتها fetch الخاصة بجلب البيانات. افتح الملف "index.html"، وأنشئ بداخله هيكل HTML مع الوسم <pre> كما يلي: <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>GraphQL Client</title> </head> <pre><!-- البيانات ستعرض هنا --></pre> <body> <script> // Add query here </script> </body> </html> ثم أنشئ دالة غير متزامنة تحت الوسم script، ترسل طلب POST لواجهة GraphQL API: ... <body> <script> async function queryGraphQLServer() { const response = await fetch('http://localhost:4000/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: '{ warriors { name } }', }), }) const data = await response.json() // إضافة البيانات إلى الوسم const pre = document.querySelector('pre') pre.textContent = JSON.stringify(data, null, 2) // JSON بصيغة } queryGraphQLServer() </script> </body> ... أسند القيمة application/json للحقل Content-Type في ترويسة المقطع script، واكتب الاستعلام الذي تطلبه بصيغة سلسلة نصية ضمن المتن، فيكون تسلسل الإجراءات كما يلي: يستدعي script الدالة غير المتزامنة، فترسل طلبًا للخادم، وعند ورود الاستجابة توضع تحت الوسم pre. إليك المحتوى النهائي للملف "index.html": <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>GraphQL</title> </head> <pre></pre> <body> <script> async function queryGraphQLServer() { const response = await fetch('http://localhost:4000/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: '{ warriors { name } }', }), }) const data = await response.json() const pre = document.querySelector('pre') pre.textContent = JSON.stringify(data, null, 2) // Pretty-print the JSON } queryGraphQLServer() </script> </body> </html> احفظ التغييرات، وأغلق الملف "index.html"، ثم استعرضه بواسطة المتصفح، وافتح نافذة الشبكة Network ضمن أدوات المطور لترى الطلب المرسل عبر الشبكة من المتصفح إلى نقطة وصول GraphQL ذات العنوان "http://localhost:4000/graphql"، وافحص بعدها استجابة الخادم؛ فإذا تضمنت البيانات التي استعلمت عنها مع رمز الاستجابة 200، فاستعلامك صحيح وقد نجحت بإنشاء خادم GraphQL API. الخاتمة تناول هذا المقال إعداد خادم GraphQL API في بيئة Node.js باستخدام إطار العمل Express بطريقة مبسطة تعينك على فهم أساسيات GraphQL، وأبرزها أن الخادم يتكون من نقطة وصول وحيدة graphql/ تستقبل جميع طلبات الاستعلام، وواجهة برمجة التطبيقات تحتوي دومًا مخطط ومحلل استجابة. يتضمن المخطط نوعين، نوعًا أساسيًا للاستعلام Query، ونوعًا مخصصًا تُعرّفه حسب حالتك، وفي مثالنا كان نوع المحارب Warrior، ومهمة محلل الاستجابة جلب البيانات المناسبة لهذه الأنواع من مخزن البيانات. نأمل أننا ساعدناك في خطوتك الأولى مع GraphQL، وننصحك بتوسيع بحثك عنها لتتعرف على خياراتها المتقدمة ومكملاتها، مثل أدوات المصادقة والأمان والتخبئة caching، لتزيد فعاليتها في تطبيقاتك. ترجمة -وبتصرف- للمقال How To Set Up a GraphQL API Server in Node.js لصاحبته Tania Rascia. اقرأ أيضًا المقال السابق: مقدمة إلى GraphQL مقدمة إلى Node.js كتابة أول برنامج في بيئة Node.js وتنفيذه مدخل إلى المكتبة GraphQL واستعمالاتها في بناء تطبيقات الويب الحديثة النسخة الكاملة لكتاب البرمجة باستخدام Node.js
  5. جانغو هو أشهر أطر العمل البرمجية المكتوبة بلغة بايثون، إذ يساعدك هذا الإطار المميز على تطوير تطبيقات الويب ووضعها في الخدمة بسرعة ويُسر، ويحتوي جانغو خادم تطوير مُضمّن يتيح لك اختبار الكود محليًا والتأكد من عمله إلّا أنه مبسط يلبي أغراض التجربة فقط، فأي تطبيق يُطلق في بيئة العمل الفعلية مهما بلغت بساطته يتطلب خادم ويب بمواصفات أعلى وحماية أكبر. سنُحاكي في هذا المقال بيئة عمل كاملة لتطبيق جانغو مبنية على خادم أوبونتو إصدار 20.04، ونعرض كيفية تثبيت وإعداد المكونات اللازمة لعمله على الخادم، بدءًا من بناء قاعدة بيانات PostgreSQL بدلًا من SQLite (قاعدة بيانات جانغو الافتراضية)، ومن ثم إعداد خادم التطبيق من نوع Gunicorn، وصولًا إلى تهيئة خادم وكيل عكسي من نوع Nginx يعمل أمام Gunicorn ويقدم لتطبيقنا كل ما يملكه من مميزات تدعم أمانه وأدائه. متطلبات بيئة العمل والأهداف ستحتاج إلى خادم بنظام تشغيل أوبونتو وقد استعملنا في هذا المقال الإصدار 20.04، مع مستخدم –ليس مسؤول- ولكنه يتمتع بصلاحيات عالية sudo، بالإضافة إلى تفعيل جدار الحماية الافتراضي على الخادم، ويمكنك الاستعانة بمقال التهيئة الأولية لخادم أوبونتو. سنثبت جانغو في بيئةٍ افتراضية لما تقدمه من مرونة تتيح لنا التعامل مع التطبيق ومتطلباته الخارجية بطريقة منفصلة أي كل على حدة، وسنبدأ بإعداد قاعدة البيانات وبمجرد الانتهاء منها وتشغيل التطبيق، سنثبت Gunicorn ليتعامل مع طلبات العملاء الواردة عبر HTTP ويحولها إلى أوامر بايثون يعالجها التطبيق، ومن ثم سنشغل خادم وسيط عكسي هو Nginx أمام Gunicorn للاستفادة من قدراته العالية في التعامل الفعال والآمن مع حركة البيانات الواردة إلى التطبيق. لنبدأ التطبيق العملي! تثبيت الحزم اللازمة من مستودعات أوبونتو سنُنثبت معظم المكونات اللازمة للعمل من مستودعات أوبونتو، ومن ثم سنستخدم pip مدير حزم بايثون لتثبيت مكونات أخرى خاصة بالتطبيق. البداية دومًا مع تحديث apt دليل الحزم المحلي لأوبونتو، ومن بعدها سنبدأ بتحميل الحزم اللازمة وتثبيتها بما يتوافق مع إصدار بايثون الذي تستخدمه فالأوامر تختلف قليلًا بين بايثون 2 و 3، علمًا أن جانغو 1.11 هو أحدث إصدار من الإصدارات التي تدعم بايثون 2، لذا ننصحك بترقية إطار العمل الذي تعتمده واستخدام بايثون 3 إن كنت تنشئ مشروعًا جديدًا. إن كنت تستخدم بايثون 3 اكتب الأوامر: sudo apt update sudo apt install python3-pip python3-dev libpq-dev postgresql postgresql-contrib nginx curl أما أوامر بايثون 2 فهي: sudo apt update sudo apt install python-pip python-dev libpq-dev postgresql postgresql-contrib nginx curl ثبتنا بموجب الأوامر السابقة pip وملفات تطوير جانغو اللازمة لبناء Gunicorn لاحقًا و نظام قواعد البيانات Postgres مع المكاتب اللازمة للتفاعل معه وأخيرًا خادم الويب Nginx. إنشاء قاعدة البيانات PostgreSQL ومستخدم التطبيق يمتلك نظام قواعد بيانات Postgres ميزةً افتراضية تسمى توثيق النِّدّ Peer Authentication وهي خاصة بمصادقة الاتصالات المحلية، وتعني أن وجود مستخدم في نظام التشغيل يطابق اسمه اسم مستخدم Postgres فعال، يمنح هذا المستخدم -أي مستخدم نظام التشغيل- القدرة على الدخول إلى Postgres دون الحاجة إلى توثيق. وقد أُنشئ مستخدم لنظام التشغيل اسمه postgres ليتوافق مع مستخدم postgres المدير لنظام PostgreSQL، وسنستفيد من هذا المستخدم للمهام الإدارية، مع إمكانية استخدام sudo وتمرير اسم المستخدم بعد الراية u-. سجل الدخول إلى Postgres عبر الأمر التالي: sudo -u postgres psql ستظهر لك نافذة أوامر PostgreSQL، اكتب فيها تعليمة إنشاء قاعدة بيانات المشروع: CREATE DATABASE myproject; تنويه: تذكر أن تنهي كافة أوامر Postgres بفاصلة منقوطة لتضمن التنفيذ السليم. أنشئ بعدها مستخدم قاعدة البيانات واختر له كلمة سر قوية، وفق الأمر التالي: CREATE USER myprojectuser WITH PASSWORD 'password'; اضبط الآن القيم الافتراضية لمحددات اتصال المستخدم المنشأ بالتوافق مع توصيات جانغو بحيث لا يتطلب الأمر الاستعلام عنها وإعادة تعيينها مع كل تأسيس جديد للاتصال ما سينعكس إيجابًا على تسريع عمليات قاعدة البيانات، والمحددات المطلوب ضبطها هي: الترميز encoding الذي يأخذ القيمة الافتراضية UTF-8، وأسلوب عزل العملية transaction isolation scheme الذي يوضع "read committed" ومهمته منع قراءة العمليات غير المكتملة uncommitted transactions، وأخيرًا المنطقة الزمنية نثبتها على UTC، وفق التعليمات التالية: ALTER ROLE myprojectuser SET client_encoding TO 'utf8'; ALTER ROLE myprojectuser SET default_transaction_isolation TO 'read committed'; ALTER ROLE myprojectuser SET timezone TO 'UTC'; وأخيرًا امنح مستخدم قاعدة البيانات المنشأ صلاحيات الإدارة الكاملة لقاعدة البيانات: GRANT ALL PRIVILEGES ON DATABASE myproject TO myprojectuser; يمكنك الآن الخروج من موجه أوامر PostgreSQL بكتابة الأمر: \q إنشاء بيئة بايثون الافتراضية بعد أن جهزنا قاعدة بيانات التطبيق سننتقل الآن لتثبيت كافة متطلبات بايثون اللازمة له ضمن بيئة افتراضية سهلة الإدارة، ولكن أولًا علينا تثبيت virtualenv بعد تحديث pip، ستختلف أوامر التثبيت اختلافًا بسيطًا بين بايثون 2 وبايثون 3. إن كنت تستخدم بايثون 3 اكتب الأوامر التالية: sudo -H pip3 install --upgrade pip sudo -H pip3 install virtualenv أما لبايثون 2 فاكتب: sudo -H pip install --upgrade pip sudo -H pip install virtualenv أنشئ مجلدًا باسم مشروعك، ثم انتقل إلى داخله: mkdir ~/myprojectdir cd ~/myprojectdir وأنشئ البيئة الافتراضية ضمنه بكتابة الأمر التالي: virtualenv myprojectenv بموجب الأوامر السابقة، سيُنشَأ ضمن مجلد مشروعك myprojectdir مجلدًا فرعيًا للبيئة الافتراضية اسمه myprojectenv، وسيُثبت بداخله نسخة محلية من بايثون ومدير الحزم pip محلي. تؤمن هذه البيئة الافتراضية بيئة بايثون معزولة لمشروعنا ولكنها تحتاج لتفعيل قبل البدء باستخدامها وتنزيل ما يلزم ضمنها، والتفعيل وفق الأمر التالي: source myprojectenv/bin/activate سيتغير بعدها محث سطر الأوامر ليشير إلى أنك تعمل في البيئة الافتراضية وسيبدو مثل: (myprojectenv)user@host:~/myprojectdir$ يمكنك الآن باستخدام pip تثبيت جانغو و Gunicorn و psycopg2 الخاص بالتخاطب مع قاعدة البيانات التي بنيناها: تنويه: يُستخدم pip دومًا داخل البيئة الافتراضية وليس pip3 بغض النظر عن إصدار بايثون المعتمد. pip install django gunicorn psycopg2-binary يفترض الآن تثبيت كل المتطلبات اللازمة لبدء مشروع جانغو Django جديد. إنشاء وإعداد مشروع جانغو جديد يمكننا الآن إنشاء ملفات المشروع بعد أن أعددنا متطلبات بايثون اللازمة له ضمن البيئة الافتراضية. إنشاء مشروع جانغو Django لدينا بالفعل مجلدًا لمشروعنا هو myprojectdir ولذا سنكتبه في أمر إنشاء المشروع حتى يثبت جانغو الملفات ضمنه بدلًا من اختياره مجلدًا آخر تلقائيًا، وعند تنفيذ الأمر سيُنشئ مجلدًا فرعيًا ضمن مجلد المشروع يحمل نفس الاسم ليحتوي على الشيفرة البرمجية أو كود التطبيق ويضاف فيه أيضًا السكربت الخاص بالإدارة: django-admin startproject myproject ~/myprojectdir وبناءً على ذلك سيتضمن مجلد المشروع (وهو في حالتنا myprojectdir/~) العناصر التالية: myprojectdir/manage.py/~: سكربت الإدارة الخاص بجانغو. /myprojectdir/myproject/~: حزمة مشروع جانغو، ويجب أن تتضمن الملفات التالية: ‎init__.py__ settings.py urls.py asgi.py wsgi.py /myprojectdir/myprojectenv/~: مجلد البيئة الافتراضية المنشأة في الخطوة السابقة. ضبط إعدادات مشروع جانغو سنعمل في هذه الفقرة على ثلاث إعدادات هي قائمة العناوين وأسماء النطاقات المسموح باستخدامها للاتصال مع جانغو، وقاعدة البيانات المعتمدة، ومسار ملفات التطبيق الساكنة. افتح ملف الإعدادات باستعمال محرر النصوص لنبدأ بهذه التعديلات بالترتيب: nano ~/myprojectdir/myproject/settings.py ابحث عن المحدد ALLOWED_HOSTS إذ سنبدأ تعديلاتنا به، ويحميك هذا المحدد من الاتصالات غير المشروعة، ووظيفته تعريف الخوادم المسموح باستخدامها للاتصال بالتطبيق، وتُعرف باستخدام عنوان IP الخادم أو اسم النطاق أو اسم النطاق الفرعي، وأي طلب يرد للتطبيق يحمل في ترويسته اسم مضيف غير معرف ضمن هذا المحدد فسيُمنع. تُكتب قائمة عناوين IP أو أسماء النطاقات المسموح لها بالاتصال ضمن الأقواس المربعة، وكل عنوان أو اسم منها ضمن اشارتي اقتباس مفردتين، ويفصل بين كل قيمة وأخرى فاصلة، ولو رغبت بالإشارة إلى النطاق الرئيس وكل ما يحوي من نطاقات فرعية فاستخدم النقطة قبل اسم النطاق، انظر الأمثلة التالية لمزيد من الإيضاح: . . . # The simplest case: just add the domain name(s) and IP addresses of your Django server # ALLOWED_HOSTS = [ 'example.com', '203.0.113.5'] # To respond to 'example.com' and any subdomains, start the domain with a dot # ALLOWED_HOSTS = ['.example.com', '203.0.113.5'] ALLOWED_HOSTS = ['your_server_domain_or_IP', 'second_domain_or_IP', . . ., 'localhost'] تنويه: تأكد من إضافة الخادم المحلي localhost على قائمة الأجهزة المسموح لها بالاتصال إذ إننا سننشئ عليه وكيل محلي Nginx. المحدد التالي للضبط هو DATABASES الخاص بقواعد البيانات، ابحث عنه ضمن ملف الإعدادات وسنُعدّله ليناسب قاعدة بيانات PostgreSQL التي أعددناها بدلًا من SQLite قاعدة البيانات الافتراضية لجانغو. تتمثل تعديلاتنا عليه في جعل جانغو يستخدم المحول psycopg2 (سبق أن ثبتناه باستخدام pip) ليناسب التخاطب مع قاعدة البيانات الجديدة PostgreSQL، ومن ثم كتابة اسم قاعدة البيانات الصحيح واسم مستخدم قاعدة البيانات وكلمة المرور الخاصة به، وأيضًا تحديد مضيف قاعدة البيانات وهو في حالتنا الخادم المحلي، أما بالنسبة لبوابة الاتصال مع قاعدة البيانات port فيمكنك تركها فارغة، انظر التعليمات أدناه: . . . DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'myproject', 'USER': 'myprojectuser', 'PASSWORD': 'password', 'HOST': 'localhost', 'PORT': '', } } . . . أما التعديل الأخير على الملف settings.py فهو إضافة مسار ملفات التطبيق الساكنة، وهذا التعديل ضروري ليتمكن Nginx من معرفة مكان هذه الملفات والتعامل مع الطلبات الواردة بخصوصها، انتقل الآن إلى نهاية الملف واكتب الأسطر التالية التي ستحدد مكان وجود هذه الملفات ويُشار إليه بـ static: . . . STATIC_URL = '/static/' import os STATIC_ROOT = os.path.join(BASE_DIR, 'static/') والآن احفظ الملف وأغلقه. إكمال الإعداد الأولي للمشروع بعد أن ضبطنا إعدادات قاعدة البيانات يمكننا الآن تهجير مخطط قاعدة البيانات الأولي إلى قاعدة بيانات PostgreSQL المعتمدة في مشروعنا وذلك باستخدام سكريبت الإدارة: ~/myprojectdir/manage.py makemigrations ~/myprojectdir/manage.py migrate ومن بعدها سننشئ حساب المستخدم المسؤول عن إدارة التطبيق بكتابة الأمر التالي: ~/myprojectdir/manage.py createsuperuser وسيُطلب منك النظام إدخال اسم مستخدم لهذا الحساب وكلمة مرور وعنوان بريد إلكتروني. أما الآن سننفذ عملية جمع المكونات الساكنة للتطبيق static assets في المسار الذي حددناه في الفقرة السابقة وذلك وفق الأمر التالي: ~/myprojectdir/manage.py collectstatic سيُطلب منك تأكيد العملية، وبعدها ستُجمع الملفات الساكنة ضمن مجلد مشروعك في المكان الذي أطلقنا عليه اسم static. كما ورد في متطلبات بيئة العمل في بداية المقال فإن الخادم أوبونتو يجب أن يتضمن جدار ناري فعال هو الجدار الناري الأساسي للينكس UFW (يسمى الجدار الناري غير المعقد)، ولذا يتحتم علينا فتح البوابة التي سنستخدمها في هذا الجدار (وهي البوابة 8000) حتى نتمكن من الاتصال مع خادم تطوير جانغو واختباره. يتم فتح البوابة بالأمر التالي: sudo ufw allow 8000 والآن يمكنك اختبار تطبيقك بتشغيل خادم التطوير عبر الأمر التالي: ~/myprojectdir/manage.py runserver 0.0.0.0:8000 استعرض التطبيق بكتابة عنوان IP أو اسم النطاق مع البوابة 8000: ضمن متصفحك: http://server_domain_or_IP:8000 وستحصل على صفحة الدليل index الافتراضية لجانغو: ولو أضفت admin/ في نهاية الرابط، سترى شاشة تسجيل الدخول إلى لوحة التحكم: أدخل اسم المستخدم وكلمة المرور الذي أنشأته عبر استخدام الأمر createsuperuser فتدخل إلى لوحة التحكم: أوقف خادم التطوير بالضغط على ctrl-c داخل شاشة الطرفية حالما تنتهي من اختباراتك. التحقق من قدرة Gunicorn على تخديم المشروع التحقق من قدرة Gunicorn على تخديم المشروع هو الأمر الأخير الذي سنقوم به ضمن البيئة الافتراضية، وذلك عبر الدخول إلى مجلد المشروع ومن ثم استخدام الأمر gunicorn لتشغيل وحدة WSGI الخاصة بتطبيقنا: cd ~/myprojectdir gunicorn --bind 0.0.0.0:8000 myproject.wsgi سيشغل الأمر السابق Gunicorn على نفس الواجهة التي استخدمها خادم تطوير جانغو ويمكنك اختبار ذلك بإعادة طلب صفحات التطبيق عبر المتصفح. تنويه: ستلاحظ أن لوحة التحكم غير مُنسقة لأن Gunicorn لا يملك حتى الآن مسار صفحات CSS المسؤولة عن ذلك. لاحظ أننا استدعينا Gunicorn ومررنا له الوحدة التي تتضمن ملف wsgi.py -الذي يعدّ نقطة الدخول إلى تطبيقنا- مع تحديد موقع هذا الملف (حددناه عبر تنفيذ الأمر في مكان وجود الملف أي في مجلد المشروع)، ويحتوي الملف wsgi.py على دالة تسمى التطبيق application هي المسؤولة عن التواصل مع التطبيق. يمكنك معرفة المزيد حول مواصفات WSGI. اضغط على الاختصار ctrl-c داخل شاشة الطرفية عندما تنتهي من الاختبار لإيقاف خادم Gunicorn. يمكنك الآن إيقاف البيئة الافتراضية بالأمر التالي بعد أن انتهت إعدادات جانغو: deactivate وستلاحظ اختفاء محث البيئة الافتراضية من نافذة سطر الأوامر. إنشاء ملفات تمهيد Gunicorn اختبرنا في الفقرة السابقة تفاعل Gunicorn مع جانغو وتأكدنا من عمله، والآن سننشئ ملفات التمهيد systemd الخاصة به (المِقبس socket والخدمة)، لاستخدامها في إيقاف وتشغيل خادم التطبيق بطريقة أفضل. سيُنشَأ عند الإقلاع مِقبس Gunicorn ويبقى جاهزًا يستمع بانتظار الاتصالات، وعند ورود أي اتصال، فإن systemd سيشغل تلقائيًا عملية Gunicorn لتتعامل مع هذا الاتصال. لنبدأ بإنشاء ملف التمهيد لمِقبس Gunicorn بكتابة الأمر التالي مع امتيازات sudo: sudo nano /etc/systemd/system/gunicorn.socket بنية هذا الملف تتضمن ثلاثة مقاطع سنكتبها ضمنه، وهي: المقطع [Unit] مخصص لوصف المِقبس، والمقطع [Socket] يبين موقع المِقبس، أما المقطع [Install] فيحرص على إنشاء المقبس في الوقت الصحيح، وبذلك يصبح الملف بالشكل التالي: [Unit] Description=gunicorn socket [Socket] ListenStream=/run/gunicorn.sock [Install] WantedBy=sockets.target يمكنك الآن إغلاق الملف بعد حفظ التغييرات. ومن ثم أنشئ ملف التمهيد لخدمة Gunicorn وفق الأمر التالي وأيضًا مع امتيازات sudo، لاحظ أن الاسم والمسار متطابقان بين ملف المقبس وملف الخدمة باستثناء اللاحقة فهي التي تحدد نوع الملف: sudo nano /etc/systemd/system/gunicorn.service تتضمن بنية ملف الخدمة ثلاثة مقاطع أيضًا هي: [Unit] و [Service] و [Install]. يُخصص المقطع [Unit] لبيانات التعريف والاعتماديات، ونكتب ضمنه وصف الخدمة ونخبر نظام التمهيد أن تشغيل هذه الخدمة مرتبط بالوصول إلى هدف الشبكة الذي يعتمد على المقبس (عند ورود اتصال على المقبس كما ذكرنا سابقًا) وهذه العلاقة بين المقبس والخدمة سنذكرها ضمن المحدد Requires كما يلي: [Unit] Description=gunicorn daemon Requires=gunicorn.socket After=network.target نحدد ضمن المقطع [Service] من يتمتع بصلاحية تشغيل الخدمة من مستخدمين ومجموعات عمل، وهم في حالتنا مستخدم التطبيق sammy وهو مالك كافة الملفات المرتبطة، ومجموعة العمل www-data التي ستتيح لخادم Nginx الاتصال بسهولة مع Gunicorn. ونوضح بعده مجلد العمل والأمر الذي سيُستخدم لتشغيل الخدمة: بكتابة المسار الكامل لملف Gunicorn التنفيذي (الذي تم تثبيته في بيئتنا الافتراضية)، وربط العملية بمقبس يونيكس الذي أنشأناه ضمن المسار المحدد في run/ (انظر المقطع [Socket] في ملف المقبس) حتى تتمكن العملية من التواصل مع Nginx. وأخيرًا سنكتب ضمن المقطع التعليمات اللازمة لتُسجل كافة الأحداث في ملفات خرج قياسية حتى تتمكن journald من التقاط تسجيلات الأحداث الصادرة عن Gunicorn، وننوه إلى إمكانية ضبط تعديلات أخرى خاصة بـ Gunicorn هنا مثل تحديد عدد العمال workers بثلاثة كما في حالتنا. انظر الآن إلى ملف الخدمة بعد إضافة المقطع [Service]: [Unit] Description=gunicorn daemon Requires=gunicorn.socket After=network.target [Service] User=sammy Group=www-data WorkingDirectory=/home/sammy/myprojectdir ExecStart=/home/sammy/myprojectdir/myprojectenv/bin/gunicorn \ --access-logfile - \ --workers 3 \ --bind unix:/run/gunicorn.sock \ myproject.wsgi:application أما المقطع [Install] وهو المقطع الأخير، نستخدمه لإخبار نظام التمهيد بمرحلة بدء التشغيل التي سيرتبط بها تشغيل الخدمة في حال فعلّناها لتعمل تلقائيًا عند الإقلاع، وفي حالتنا ضبطنا الموضوع لتعمل الخدمة عندما يصل نظام التشغيل إلى مرحلة تعدد المستخدمين multi-user. انظر الآن الشكل النهائي لملف الخدمة، ولاتنسَ حفظ التغييرات عليه قبل الإغلاق: [Unit] Description=gunicorn daemon Requires=gunicorn.socket After=network.target [Service] User=sammy Group=www-data WorkingDirectory=/home/sammy/myprojectdir ExecStart=/home/sammy/myprojectdir/myprojectenv/bin/gunicorn \ --access-logfile - \ --workers 3 \ --bind unix:/run/gunicorn.sock \ myproject.wsgi:application [Install] WantedBy=multi-user.target يمكننا الآن تشغيل وتفعيل مقبس Gunicorn وفق الأمرين أدناه، وسيؤدي هذا إلى إنشاء ملف المقبس في المسار run/gunicorn.sock/ الآن عند تشغيلنا اليدوي للمقبس وتلقائيًا عند إقلاع نظام التشغيل، وفور ورود اتصال إلى المقبس سيشغل systemd تلقائيًا الخدمة gunicorn.service لتتعامل مع الاتصال: sudo systemctl start gunicorn.socket sudo systemctl enable gunicorn.socket يمكنك التأكد من نجاح العملية عبر التحقق من وجود ملف المقبس. اطلع على معلومات إضافية عن systemd من خلال المقالين: أساسيات Systemd: العمل مع الخدمات، الوحدات Units، واليوميات Journal إدارة الخدمات على لينكس بأدوات نظام التمهيد systemd التحقق من ملف المقبس تأكد من حالة العملية لترى إن كانت فعالة وقابلة للتشغيل عن طريق الأمر التالي: sudo systemctl status gunicorn.socket وستحصل على ما يشبه الخرج التالي في حال كانت فعالة دون أخطاء: ● gunicorn.socket - gunicorn socket Loaded: loaded (/etc/systemd/system/gunicorn.socket; enabled; vendor prese> Active: active (listening) since Fri 2020-06-26 17:53:10 UTC; 14s ago Triggers: ● gunicorn.service Listen: /run/gunicorn.sock (Stream) Tasks: 0 (limit: 1137) Memory: 0B CGroup: /system.slice/gunicorn.socket تحقق الآن من وجود ملف المقبس gunicorn.sock ضمن المسار run/ بكتابة الأمر التالي: file /run/gunicorn.sock ستحصل على الخرج التالي الذي يبين وجود الملف: /run/gunicorn.sock: socket أما في حال عدم وجود الملف gunicorn.sock، أو ظهور أي خطأ في خرج التعليمة systemctl status السابقة، فهذا يعني حدوث خطأ ما منع اكتمال العملية وإنشاء ملف المقبس، يمكنك تبيانه عبر استعراض سجلات الأحداث الخاصة بمقبس Gunicorn من خلال الأمر: sudo journalctl -u gunicorn.socket ومن ثم فتح الملف gunicorn.socket ضمن المسار etc/systemd/system/ ثانيةً لتصحيح واستدراك أي مشاكل موجودة بينتها السجلات قبل استكمال العمل. التحقق من تفعيل المقبس صحيح أننا شغلنا المقبس gunicorn.socket وتحققنا منه في الخطوات السابقة، ولكن الخدمة gunicorn.service لم تُفعل بعد وهذا طبيعي فهي مرتبطة بالاتصالات الواردة ولن تعمل قبل ورود أول اتصال. ويمكنك التحقق من حالتها بالأمر: sudo systemctl status gunicorn وستحصل على الخرج التالي الذي يبين أنها غير فعالة: ● gunicorn.service - gunicorn daemon Loaded: loaded (/etc/systemd/system/gunicorn.service; disabled; vendor preset: enabled) Active: inactive (dead) أرسل الآن اتصال تجريبي عبر curl لتختبر آلية العمل، وفق التالي: curl --unix-socket /run/gunicorn.sock localhost وستحصل بموجبه على صفحة HTML من تطبيقك داخل شاشة الطرفية، ما يعني أن Gunicorn بدأ بالعمل وأنه قادرٌ على تخديم التطبيق، نفذ مجددًا أمر التحقق من حالة الخدمة: sudo systemctl status gunicorn ولاحظ اختلاف الخرج التنفيذ السابق قبل وجود اتصالات: ● gunicorn.service - gunicorn daemon Loaded: loaded (/etc/systemd/system/gunicorn.service; disabled; vendor preset: enabled) Active: active (running) since Fri 2020-06-26 18:52:21 UTC; 2s ago TriggeredBy: ● gunicorn.socket Main PID: 22914 (gunicorn) Tasks: 4 (limit: 1137) Memory: 89.1M CGroup: /system.slice/gunicorn.service ├─22914 /home/sammy/myprojectdir/myprojectenv/bin/python /home/sammy/myprojectdir/myprojectenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunico> ├─22927 /home/sammy/myprojectdir/myprojectenv/bin/python /home/sammy/myprojectdir/myprojectenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunico> ├─22928 /home/sammy/myprojectdir/myprojectenv/bin/python /home/sammy/myprojectdir/myprojectenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunico> └─22929 /home/sammy/myprojectdir/myprojectenv/bin/python /home/sammy/myprojectdir/myprojectenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunico> Jun 26 18:52:21 django-tutorial systemd[1]: Started gunicorn daemon. Jun 26 18:52:21 django-tutorial gunicorn[22914]: [2020-06-26 18:52:21 +0000] [22914] [INFO] Starting gunicorn 20.0.4 Jun 26 18:52:21 django-tutorial gunicorn[22914]: [2020-06-26 18:52:21 +0000] [22914] [INFO] Listening at: unix:/run/gunicorn.sock (22914) Jun 26 18:52:21 django-tutorial gunicorn[22914]: [2020-06-26 18:52:21 +0000] [22914] [INFO] Using worker: sync Jun 26 18:52:21 django-tutorial gunicorn[22927]: [2020-06-26 18:52:21 +0000] [22927] [INFO] Booting worker with pid: 22927 Jun 26 18:52:21 django-tutorial gunicorn[22928]: [2020-06-26 18:52:21 +0000] [22928] [INFO] Booting worker with pid: 22928 Jun 26 18:52:21 django-tutorial gunicorn[22929]: [2020-06-26 18:52:21 +0000] [22929] [INFO] Booting worker with pid: 22929 وفي حال حصلت على أي أخطاء في خرج systemctl status أو خرج curl، تفقد عندها سجلات الأحداث عبر الأمر: sudo journalctl -u gunicorn وافتح الملف gunicorn.service ضمن المسار etc/systemd/system/ وصحح المشاكل التي بينتها السجلات قبل إكمال العمل، وانتبه إلى أن أي تعديلات تطبقها عليه لن تصبح سارية المفعول إلّا بعد إعادة تحميل الخدمة ومن ثم إعادة تشغيل Gunicorn وفق الأوامر التالية: sudo systemctl daemon-reload sudo systemctl restart gunicorn إعداد Nginx ليؤدي دور وكيل Gunicorn لنبدأ بإنشاء كتلة خادم جديدة لخادم Nginx ضمن المجلد sites-available. اكتب أولًا الأمر التالي الذي سيفتح ملفًا نصيًا: sudo nano /etc/nginx/sites-available/myproject أنشئ فيه كتلة خادم جديدة وحدد مواصفاتها بحيث تتلقى الطلبات الطبيعية التي ترد إلى البوابة 80 وتطلب اسم نطاقك أو عنوان IP الخادم، وذلك بكتابة التعليمات التالية: server { listen 80; server_name server_domain_or_IP; } وبعدها أضف ضمن الكتلة نفسها التعليمات الخاصة بـ location المبينة أدناه، ووظيفتها جعل Nginx يتجاهل الأخطاء التي تنجم عن عدم العثور على رمز الموقع favicon، وإرشاده إلى موقع ملفات التطبيق الساكنة أي المسارmyprojectdir/static/~ الذي اختصرناه إلى "static/"، إذًا أصبحت كتلة الخادم على الشكل التالي: server { listen 80; server_name server_domain_or_IP; location = /favicon.ico { access_log off; log_not_found off; } location /static/ { root /home/sammy/myprojectdir; } } والخطوة الأخيرة ستكون إنشاء كتلة {} / location التي ستتعامل مع بقية الطلبات، والإشارة ضمنها للملف proxy_params القياسي الذي أنُشئ أثناء تثبيت Nginx، ومن ثم تمرير كامل حركة البيانات مباشرةً إلى مقبس Gunicorn: server { listen 80; server_name server_domain_or_IP; location = /favicon.ico { access_log off; log_not_found off; } location /static/ { root /home/sammy/myprojectdir; } location / { include proxy_params; proxy_pass http://unix:/run/gunicorn.sock; } } احفظ التغييرات على الملف وأغلقه. ومن ثم فعل الملف عبر ربطه بالمجلد sites-enabled باستخدام الأمر: sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled اختبر صحة تعليمات Nginx من ناحية قواعد الكتابة من خلال الأمر: sudo nginx -t أعد تشغيل Nginx إن لم تظهر لك أي أخطاء: sudo systemctl restart nginx لنضبط الآن إعدادات الجدار الناري UFW عبر السماح بحركة مرور البيانات عبر البوابة 80، وإغلاق البوابة 8000 التي سمحنا بها سابقًا للتخاطب مع خادم التطوير، فهي لم تعد لازمة. اكتب الأوامر التالية لضبط الجدار الناري: sudo ufw delete allow 8000 sudo ufw allow 'Nginx Full' يمكنك الآن الوصول لتطبيقك باستخدام اسم النطاق أو عنوان IP. تنويه: الخطوة الأهم بعد إعداد الخادم الوكيل هي تأمين شهادات TLS لتشفير حركة البيانات المتبادلة مع التطبيق وحماية البيانات الحساسة مثل كلمات المرور، لذا احرص على تنفيذ هذه الخطوة، ويمكنك الحصول على شهادات مجانية من خدمة Let’s Encrypt في كان لديك اسم نطاق محجوز، واتبع الخطوات الواردة في مقال كيف تؤمّن خادم ويب NGINX على أوبنتو 16.04 لتثبيت الشهادة فالطريقة نفسها. استكشاف الأخطاء وإصلاحها في هذه البيئة إذا لم تُظهر هذه الخطوة الأخيرة تطبيقك، فستحتاج إلى استكشاف أخطاء التثبيت وإصلاحها. يظهر Nginx الصفحة الافتراضية بدلاً من تطبيق جانغو في حال أظهر Nginx صفحته الافتراضية بدلًا من تطبيقك، فهذا يعني عادةً حاجتك لضبط server_name في الملف التالي: /etc/nginx/sites-available/myproject ليشير إلى اسم نطاقك أو عنوان IP للخادم، إذ يستخدم Nginx القيمة الموجودة في server_name ليُحدد أي كتلة خادم ستلبي الطلبات الواردة إليه، وعندما يرجع لك صفحته الافتراضية فهذا يعني أنه لم يتمكن من مطابقة طلبك بدقة مع أي من الكتل الموجودة ضمن ملفاته فيُرجع تلقائيًا الصفحة الموجودة في كتلته الافتراضية في المسار التالي: /etc/nginx/sites-available/default لذا احرص على تحديد server_name في الكتلة الخاصة بمشروعك بدقة وعلى نطاق أضيق من المذكور في الكتلة الافتراضية بحيث لا تسمح لخادم Nginx بتجاهلها والعودة إلى كتلته الافتراضية. ظهور Nginx الخطأ 502 بدلا من تطبيق جانغو يوجد مجموعة واسعة من الاحتمالات التي قد تسبب الخطأ 502 Bad Gateway Error، لذا فهو يحتاج إلى معلومات وتقصي أكثر لبيان سببه الفعلي. سجلات أحداث Nginx هي أهم مصدر معلومات عن الأحداث والأخطاء التي تحصل خلال عمل الوكيل أو الوسيط، ويمكنك تتبعها باستخدام الأمر: sudo tail -F /var/log/nginx/error.log والآن حدث الصفحة في متصفح الإنترنت لتولد خطأ جديدًا في السجل واقرأه بدقة من شأنه تضييق نطاق البحث عن سبب الخطأ، وسنعطيك بعض الاحتمالات عن ما يمكن أن يظهر لك من رسائل خطأ. احتمال الخطأ الأول: connect() to unix:/run/gunicorn.sock failed (2: No such file or directory) وهو يعني أن Nginx لم يتمكن من العثور على ملف المقبس gunicorn.sock في الموقع المحدد، وعندها عليك مقارنة موقع proxy_pass المذكور في الملف التالي: /etc/nginx/sites-available/myproject مع الموقع الفعلي المذكور في ملف المقبس gunicorn.sock المنشئ بواسطة وحدة تمهيد النظام gunicorn.socket. أما في حال لم تجد ملف المقبس gunicorn.sock في المسار run/ فارجع إلى فقرة التحقق من ملف المقبس واتبع إرشاداتها لمعالجة هذه الحالة. احتمال الخطأ الثاني: connect() to unix:/run/gunicorn.sock failed (13: Permission denied) يدل هذا الخطأ على مشاكل في صلاحيات وصول Nginx إلى مقبس Gunicorn، وغالبًا ما ينجم هذا الخطأ عن تنفيذ إجرائية العمل بمستخدم الجذر بدلًا من مستخدم يملك امتيازات sudo وعندها فإن systemd سيُنشئ مقبس Gunicorn ولكنّ Nginx لن يتمكن من الوصول إليه. ومن الممكن أن تظهر نفس المشكلة في حال وجود أي تقييد للصلاحيات على طول المسار الواصل إلى ملف المقبس بدءًا من الجذر / وصولًا إلى الملف gunicorn.sock، ويمكنك استعراض صلاحيات الوصول واسم المالك على كافة المجلدات الموجودة على مسار ملف المقبس باستخدام التعليمة namei وفق التالي: namei -l /run/gunicorn.sock وستحصل على المعلومات التفصيلية عن صلاحيات كافة عناصر المسار ضمن الخرج بالشكل التالي: f: /run/gunicorn.sock drwxr-xr-x root root / drwxr-xr-x root root run srw-rw-rw- root root gunicorn.sock يبين لك العمود الأول الصلاحيات الممنوحة على العنصر، والعمود الثاني يشير إلى اسم المستخدم المالك، بينما العمود الثالث فهو خاص باسم مجموعة العمل التي ينتمي إليها المالك، وبذلك يمكنك معرفة نوع الوصول المسموح إلى ملف المقبس. فمثلًا في الخرج السابق، كافة عناصر المسار تحمل صلاحية القراءة والتنفيذ فهي تنتهي بالرمز r-x بدلًا من --- وهذا يعني إمكانية الوصول إليها من قبل Nginx فلو كان أي منها لا يحمل هذه الصلاحيات لحصلت مشكلة في الوصول إلى المقبس، وتُحل المشكلة آنذاك بإعطاء صلاحية العمل الناقصة أو بضم مستخدم Nginx إلى مجموعة العمل التي تتضمن مالك العنصر. ظهور جانغو (لا يمكن الاتصال بالخادم: تم رفض الاتصال) هي إحدى الرسائل التي قد تتلقاها من جانغو عند محاولتك استعراض بعض أجزاء التطبيق في متصفح الإنترنت: OperationalError at /admin/login/ could not connect to server: Connection refused Is the server running on host "localhost" (127.0.0.1) and accepting TCP/IP connections on port 5432? تعني هذه الرسالة أن جانغو لم يتمكن من الوصول إلى قاعدة البيانات، لذا فالخطوة الأولى هي التأكد من إقلاع نسخة قاعدة البيانات Postgres instance باستخدام الأمر التالي: sudo systemctl status postgresql وفي حال تبين لك أنها لا تعمل فيمكنك تشغيلها ومن ثم تفعيلها لتعمل تلقائيًا عند الإقلاع (ما لم تكن مفعلة للتشغيل التلقائي) وفق الأمرين: sudo systemctl start postgresql sudo systemctl enable postgresql وإن استمرت المشكلة تأكد من إعدادات قاعدة البيانات الموجودة في الملف التالي وصححها: ~/myprojectdir/myproject/settings.py المزيد من استكشاف الأخطاء وإصلاحها استعن دومًا بسجلات الأحداث فهي مصدر مهم للمعلومات وحدد الأخطاء بدقة وابحث عنها، إليك أهم السجلات. للتحقق من سجلات عملية Nginx اكتب: sudo journalctl -u nginx للتحقق من سجلات وصول Nginx اكتب: sudo less /var/log/nginx/access.log للتحقق من سجلات خطأ Nginx اكتب: sudo less /var/log/nginx/error.log للتحقق من سجلات أحداث Gunicorn اكتب: sudo journalctl -u gunicorn للتحقق من سجلات أحداث مقبس Gunicorn اكتب: sudo journalctl -u gunicorn.socket عند تعديل أو تحديث الإعدادات أو التطبيق يجب إعادة تشغيل العمليات لتصبح التعديلات سارية المفعول. ففي حال عدلت تطبيق جانغو يمكنك إعادة تشغيل عملية Gunicorn عبر الأمر: sudo systemctl restart gunicorn وإن عدلت ملفات مقبس أو خدمة Gunicorn فعليك إعادة تحميل العفريت وإعادة تشغيل الخدمة: sudo systemctl daemon-reload sudo systemctl restart gunicorn.socket gunicorn.service أما لو عدلت إعدادات كتلة الخادم لـ Nginx فعليك التحقق من صحة القواعد أولًا ومن ثم إعادة تشغيل Nginx وفق التالي: sudo nginx -t && sudo systemctl restart nginx ستفيدك هذه الأوامر في عكس تعديلاتك على عمل البيئة بصورة سليمة. خاتمة أعددنا في هذا المقال مشروع جانغو ضمن بيئة افتراضية معزولة ومجهز بخادم Gunicorn يوصل طلبات العملاء إلى جانغو حتى يعالجها، وبخادم Nginx الذي يلعب دور الوكيل العكسي لتحسين أداء التطبيق وأمانه، وقدمنا مجموعة من الأدوات المساعدة التي من شأنها خدمة تطبيقاتك العاملة على خادم واحد. ترجمة -وبتصرف- للمقال How To Set Up Django with Postgres, Nginx, and Gunicorn on Ubuntu 20.04 لصاحبه Erin Glass. اقرأ أيضًا المرجع الشامل إلى تعلم PostgreSQL البدء مع إطار العمل جانغو لإنشاء تطبيق ويب كيف تُرقِّي خادم Nginx موجود بدون قطع اتصالات العميل
  6. هذا المقال هو الثالث من سلسلة تعليمية تتضمن ثلاث مقالات عن الانتقال من الحاويات إلى كوبيرنتس Kubernetes باستخدام جانغو Django إطار العمل الخاص بتسريع تطوير تطبيقات الويب المبنية بلغة بايثون، في المقال الأول تعلمنا تعديل عينة تجريبية من تطبيق جانغو (تسمى التطبيق Polls) وفق منهجية Twelve-Factor الخاصة بتطوير تطبيقات ويب سحابية قابلة للتوسع والعمل ضمن الحاويات، وفي المقال الثاني تعرفنا على طرق توسيع هذا التطبيق (المعدّل المغلف في حاوية دوكر) توسيعًا أفقيًا وتأمينه بتشغيل وكيل عكسي أمامه هو خادم Nginx مع استخدام شهادات TLS مصدقة من Let's Encrypt، أما اليوم نشارككم كيفية نشر التطبيق المعدّل نفسه باستعمال عنقود كوبيرنتس Kubernetes Cluster. كوبيرنتس kubernetes هو حل مفتوح المصدر لإدارة الحاويات، ميزته الأساسية أتمتة عمليات نشر وتوسيع وإدارة تطبيقات الويب المغلفة ضمن حاويات، وذلك من خلال مجموعة من الكائنات الخاصة مثل خرائط الإعدادات ConfigMaps والأسرار Secrets التي تسمح بتهيئة مركزية للبيئة من خارج الحاويات، ووحدات التحكم مثل النشر Deployments المسؤولة عن إعادة تشغيل الحاويات تلقائيًا في حال حدوث أي خلل وعن التوسعة التلقائية بتشغيل حاويات إضافية مماثلة لحاوية التطبيق عند ازدياد الطلب عليه، وأيضًا كائن الإدخال ingress أو المُدخل (تترجم أحيانًا إلى كائن الولوج واعتمدنا هنا كائن الإدخال والمُدخل حسب سياق الجملة) المسؤول عن توجيه حركة البيانات HTTP أو HTTPS المشفرة بشهادات TLS من المصادر الخارجية إلى خدمات كوبيرنتس الداخلية عبر قواعد وصول محددة مع وحدة التحكم مفتوحة المصدر ingress-nginx، أما التجديد الدوري للشهادات وتصديقها من Let's Encrypt تنجزه الوظيفة الإضافية مدير الشهادات cert-manager. هذا المقال جزء من سلسلة قصيرة حول احتواء تطبيقات جانغو بحاويات وإدارتها وإليك فهرس السلسلة: بناء تطبيق جانغو بخادم Gunicorn ووضعه ضمن حاوية دوكر توسيع تطبيق جانغو وتأمينه عبر حاوية دوكر وخادم Nginx وخدمة Let's Encrypt نشر تطبيق جانغو آمن وقابل للتوسيع باستخدام كوبيرنتس متطلبات بيئة العمل ستحتاج المتطلبات التالية لتتمكن من التطبيق العملي لهذا المقال: عنقود كوبيرنتس بإصدار 1.15 أو أعلى، مع تفعيل خاصية التحكم بالوصول المستند على الدور Role-based access control (RBAC)، استخدمنا في المقال عنقود خدمة DigitalOcean، لكنك لست ملزمًا بذلك يمكنك إعداده بنفسك بالطريقة التي تناسبك. تثبيت أداة سطر الأوامر kubectl على جهازك المحلي وإعدادها للاتصال بالعنقود، يمكنك معرفة المزيد عن تثبيت kubectl بالاطلاع على توثيقات كوبيرنتس الرسمية. أما إن اعتمدت DigitalOcean فاستعن بالمقال إعداد الاتصال مع عنقود كوبيرنتس في DigitalOcean. حجز اسم نطاق لتطبيقك، سنعتمد في مقالنا على الاسم your_domain.com، مع العلم بإمكانية حصولك على اسم نطاق مجاني من Freenom. تثبيت وحدة التحكم ingress-nginx ومدير الشهادات cert-manager وضبط الإعدادات اللازمة للتحقق من الشهادات، يمكنك الاطلاع على كيفية إعداد Nginx Ingress ومدير الشهادات على كوبيرنتس. ربط اسم النطاق بعنوان IP العام لمُدخل موازن الحمل عبر إضافة سجل DNS من النوع A، يمكنك الحصول على تفاصيل إضافية حول DNS بالاطلاع على القسم الخاص بخدمة اسم النطاق DNS على أكاديمية حسوب. خدمة تخزين كائني متوافقة مع S3 ومع إضافة django-storages مثل DigitalOcean Space وظيفتها تأمين المساحة التخزينية اللازمة لملفات تطبيق جانغو الساكنة، مع مجموعة مفاتيح الوصول الخاصة بإدارة هذه المساحة، استرشد بكيفية إعداد مساحة على DigitalOcean Space وعدّل بما يلائم خدمة التخزين التي اخترتها. نظام إدارة قواعد بيانات متوافق مع جانغو مع إنشاء قاعدة بيانات ومستخدم خاص بالتطبيق، نحن اخترنا خادم PostgreSQL، اهتم بالتفاصيل التالية أثناء إنجازك لهذه النقطة لتتمكن من متابعة خطوات المقال: استخدم الاسم polls لقاعدة البيانات والاسم sammy لمستخدم قاعدة البيانات (هذه الأسماء ليست ملزمة ولكننا استخدمناها في المقال واستخدامك لها سيسهل عليك المتابعة)، لمزيد من التفاصيل حول الإنشاء استرشد بالخطوة /1/ من مقالنا السابق [بناء تطبيق جانغو بخادم Gunicorn ووضعه ضمن حاوية دوكر]()، مع العلم أن هذه المهمة يمكن إنجازها من أي خادم من الخوادم الثلاثة لبيئة العمل. اعتمدنا في المقال على نظام إدارة قواعد البيانات من نوع Managed PostgreSQL cluster الذي توفره DigitalOcean. لكن يمكنك تثبيت PostgreSQL وتشغيله وإعداده بنفسك مع الاستعانة بالفيديو التعليمي تثبيت وإعداد قاعدة بيانات PostgreSQL. حساب على Docker Hub، استرشد بتوثيقات دوكر الخاصة بالمستودعات لإنشائه. تثبيت دوكر على جهازك المحلي، استعن لذلك بمقال كيفية تثبيت دوكر واستخدامه على دبيان. الخطوة 1: استنساخ التطبيق وضبط إعداداته تتلخص هذه الخطوة باستنساخ كود التطبيق من مستودع GitHub بالإضافة إلى ضبط محددات الاتصال مع خادم قاعدة البيانات والمفاتيح الخاصة بخدمة التخزين الكائني المستخدمة. سنحصل على كود التطبيق والملف Dockerfile من القسم polls-docker من مستودع GitHub الذي يتضمن نسخًا تجريبية من التطبيق المسمى Polls وهذه النسخ تتناسب مع أغراض جانغو التعليمية، بالإضافة لكونها مغلفة ومعدّلة بكل ما يلزم لتعمل بفعالية داخل الحاوية وتتكيف مع متغيرات بيئة التشغيل وفق ما تعلمنا في المقال الأول من هذه السلسلة. اكتب الأمر التالي للاستنساخ: git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git توجه للمجلد django-polls عبر الأمر: cd django-polls يتضمن هذا المجلد كود التطبيق والملف Dockerfile الذي سيستخدمه دوكر لبناء صورة الحاوية، بالإضافة إلى الملف env الذي يتضمن متغيرات البيئة التي ستمرر للحاوية من بيئة التشغيل المحيطة. استعرض الملف Dockerfile عبر تعليمة لينكس cat: cat Dockerfile وستحصل على الخرج التالي: FROM python:3.7.4-alpine3.10 ADD django-polls/requirements.txt /app/requirements.txt RUN set -ex \ && apk add --no-cache --virtual .build-deps postgresql-dev build-base \ && python -m venv /env \ && /env/bin/pip install --upgrade pip \ && /env/bin/pip install --no-cache-dir -r /app/requirements.txt \ && runDeps="$(scanelf --needed --nobanner --recursive /env \ | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ | sort -u \ | xargs -r apk info --installed \ | sort -u)" \ && apk add --virtual rundeps $runDeps \ && apk del .build-deps ADD django-polls /app WORKDIR /app ENV VIRTUAL_ENV /env ENV PATH /env/bin:$PATH EXPOSE 8000 CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "mysite.wsgi"] وفقًا للخرج السابق، فإن Dockerfile سيستخدم صورة دوكر بإصدار بايثون 3.7.4، ويثبت اعتماديات وحزم بايثون اللازمة لكل من جانغو وخادم gunicorn وفق ملف متطلبات التطبيق django-polls/requirements.txt ومن ثم يحذف الملفات غير اللازمة بعد انتهاء التثبيت، وينسخ بعد ذلك كود التطبيق إلى الصورة ويسند قيمة المتغير PATH، وأخيرًا سيحدد البوابة أو المنفذ 8000 لاستقبال حركة البيانات الواردة إلى الحاوية، ويشغل gunicorn مع عمال workers عدد /3/ عبر المنفذ 8000. لتعرف المزيد عن كل مرحلة ضمن Dockerfile راجع مقالنا الأول السابق من السلسلة. لنبني صورة الحاوية باستخدام docker build: docker build -t polls . سميّنا الصورة باسم polls باستخدام الراية t- ومررنا المسار الحالي . كمسار context للتعليمة build فهو يتضمن كافة الملفات اللازمة لعملية البناء. بعد انتهاء عملية البناء استخدم الأمر docker images لاستعراض الصور المتاحة: docker images وستحصل على الخرج التالي: REPOSITORY TAG IMAGE ID CREATED SIZE polls latest 80ec4f33aae1 2 weeks ago 197MB python 3.7.4-alpine3.10 f309434dea3a 8 months ago 98.7MB لنضبط متغيرات البيئة تمهيدًا لعملية التشغيل docker run، وذلك بتعديل الملف env باستعمال محرر النصوص: nano env سيظهر أمامك الملف env بالشكل التالي: DJANGO_SECRET_KEY= DEBUG=True DJANGO_ALLOWED_HOSTS= DATABASE_ENGINE=postgresql_psycopg2 DATABASE_NAME=polls DATABASE_USERNAME= DATABASE_PASSWORD= DATABASE_HOST= DATABASE_PORT= STATIC_ACCESS_KEY_ID= STATIC_SECRET_KEY= STATIC_BUCKET_NAME= STATIC_ENDPOINT_URL= DJANGO_LOGLEVEL=info كما ترى فبعض محددات الملف قيمها موجودة والبعض الآخر يحتاج إلى ضبط بما يناسب بيئتك، سنذكر المحددات التي تحتاج إلى ضبط بالترتيب: المفتاح السري DJANGO_SECRET_KEY اختر له قيمة فريدة صعبة التخمين كما توصي توثيقات Docker نقدم لك في الخطوة /5/ من مقال كيفية إعداد تطبيق جانغو عبر DigitalOcean Managed Databases and Spaces إحدى طرق جانغو لتوليد المفاتيح. أما DJANGO_ALLOWED_HOSTS فأسند له اسم النطاق الخاص بك your_domain.com، أو يمكنك وضع الرمز * لأغراض الاختبار فقط وليس ضمن بيئة العمل الفعلية فهذا الرمز يسمح لأي عنوان IP بالوصول، يحميك هذا المحدد من الهجمات المعتمدة على حقن ترويسة HTTP وفي حال أردت المزيد من المعلومات اطلع على توثيقات جانغو الخاصة بهذا المحدد ضمن Core Settings DATABASE_USERNAME ضع اسم مستخدم قاعدة البيانات PostgreSQL الذي أنشأته أثناء إعدادك لمتطلبات العمل. DATABASE_NAME ضع القيمة polls أو الاسم الذي اخترته لقاعدة البيانات أثناء إعدادك لمتطلبات العمل. كذلك الأمر لكلمة المرور DATABASE_PASSWORD. ضع اسم مضيف قاعدة البيانات DATABASE_HOST ورقم بوابة الاتصال معها DATABASE_PORT. أما المحددات STATIC_ACCESS_KEY_ID و STATIC_SECRET_KEY و STATIC_BUCKET_NAME و STATIC_ENDPOINT_URL فهي تتعلق بخدمة التخزين الكائني الخارجية، اضبطها بما يتناسب مع الخدمة التي تستخدمها. احفظ التغيرات على الملف env بعد الانتهاء وأغلقه، ولننتقل للخطوة التالية. الخطوة 2: إنشاء مخطط قاعدة البيانات ورفع ملفات التطبيق إلى وحدة التخزين الكائني شغل صورة الحاوية polls:latest عبر الأمر التالي: docker run --env-file env polls sh -c "python manage.py makemigrations && python manage.py migrate" وبموجبه الأمر سيجري تجاوز الأمر الافتراضي عند بدء التشغيل المحدد بتعليمة CMD ضمن الملف Dockerfile وتنشئ أثناء التشغيل مخطط قاعدة بيانات التطبيق وهو الأمر الفرعي: sh -c "python manage.py makemigrations && python manage.py migrate" إن كنت تنفذ الأمر docker run للمرة الأولى فستحصل على الخرج التالي الذي يبين نجاح إنشاء مخطط قاعدة البيانات: No changes detected Operations to perform: Apply all migrations: admin, auth, contenttypes, polls, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying polls.0001_initial... OK Applying sessions.0001_initial... OK مع العلم أن جانغو لن يجري أي عملية عندما تنفذ migrate في المرات القادمة طالما لم يطرأ أي تغيير على مخطط قاعدة بيانات التطبيق. نفذ بعدها أمر تشغيل جديد للحاوية مع استدعاء سطر الأوامر الخاص بالصدفة Shell كما يلي: docker run -i -t --env-file env polls sh تظهر بنتيجة هذا الأمر نافذة الصدفة shell ضمن الحاوية اكتب فيها تعليمة إنشاء المستخدم الخاص بتطبيق جانغو: python manage.py createsuperuser استكمل بياناته وهي اسم المستخدم وكلمة المرور والبريد الإلكتروني، واضغط بعدها على الاختصار Ctrl+D للخروج من الحاوية وإنهاء عملها. أما العملية الأخيرة فهي جمع الملفات الساكنة الخاصة بالتطبيق collectstatic ورفعها على خدمة التخزين الكائني وتتم وفق الأمر التالي علمًا أنها قد تستغرق بعض الوقت: docker run --env-file env polls sh -c "python manage.py collectstatic --noinput" يظهر بعدها الخرج التالي: 121 static files copied. يمكنك الآن تشغيل التطبيق: docker run --env-file env -p 80:8000 polls ستحصل على الخرج: [2019-10-17 21:23:36 +0000] [1] [INFO] Starting gunicorn 19.9.0 [2019-10-17 21:23:36 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) [2019-10-17 21:23:36 +0000] [1] [INFO] Using worker: sync [2019-10-17 21:23:36 +0000] [7] [INFO] Booting worker with pid: 7 [2019-10-17 21:23:36 +0000] [8] [INFO] Booting worker with pid: 8 [2019-10-17 21:23:36 +0000] [9] [INFO] Booting worker with pid: 9 مع العلم أن التشغيل هذه المرة وفق الإعدادات الافتراضية المنصوص عليها في ملف Dockerfile وهي: gunicorn --bind :8000 --workers 3 mysite.wsgi:application وأن البوابة 8000 للحاوية فُتحت لتستقبل حركة البيانات القادمة من البوابة 80 للخادم أوبونتو. يمكنك الآن كتابة عنوان URL الخاص بالتطبيق polls بمتصفح الإنترنت واستعراضه، لاحظ أنك ستحصل على الخطأ 404 (لم يتم العثور على الصفحة) في حال اكتفيت بالعنوان: http://APP_SERVER_1_IP/ بسبب عدم وجود بيانات للتطبيق تحت الجذر / لذا اكتب العنوان التالي لتحصل على واجهة polls: http://APP_SERVER_1_IP/polls اكتب الرابط التالي: http://APP_SERVER_1_ip/admin وستتمكن من الوصول إلى واجهة الدخول للوحة التحكم الخاصة بالتطبيق: أدخل بيانات المستخدم مدير التطبيق المنشأ سابقًا باستخدام الأمر createsuperuser وستظهر أمامك واجهة الإدارة والتحكم الخاصة بالتطبيق. تذكر أن تسليم المكونات الساكنة للتطبيق admin و polls يتم من خدمة التخزين الكائني ويمكنك اتباع هذه الخطوات لاختبار جودته وأيضًا التأكد من صحة إحضار الملفات. بعد أن تنتهي من اختبار عمل التطبيق اضغط على الاختصار Ctrl+c في نافذة كتابة الأوامر السطرية التي تشغل حاوية دوكر لتنهي عمل الحاوية. والآن بعد إتمام بناء صورة دوكر للتطبيق، وبناء مخطط قاعدة البيانات وتحميل مكونات التطبيق الساكنة إلى وحدة التخزين الكائني، وأخيرًا اختبار عمل الحاوية والتطبيق، يمكننا رفع الصورة إلى سجل صور مثل Docker Hub وهو موضوع خطوتنا التالية. الخطوة 3: رفع صورة جانغو إلى Docker Hub لتستخدم تطبيقك ضمن عنقود كوبيرنتس، ستحتاج أولًا رفع صورة التطبيق إلى سجل صور عام أو خاص مثل Docker Hub حتى تتمكن خدمة كوبيرنتس من سحب الصورة ونشر التطبيق. علمًا أن لسجلات الصور نوعان عام وخاص، السجل العام مجاني إلاّ أنه يجعل صورك في متناول جميع المستخدمين ويمكّنهم من سحبها، أما الخاص فيعطي خصوصية أكبر إذا يسمح لك بمشاركة الصور مع مجموعة محددة من المستخدمين مثل زملاء عملك، وتقدم بعض الشركات -مثل سجل حاويات DigitalOcean على سبيل المثال لا الحصر- عروضًا مجانية لإنشاء سجلات خاصة محدودة، أما Docker Hub فيدعم النوعين العام والخاص. سنقدم في مثالنا طريقة سحب الصور من سجل Docker Hub عام ويمكنك الاطلاع على توثيقات كوبيرنتس لمعرفة طريقة العمل مع السجلات الخاصة. لنبدأ بتسجيل الدخول إلى Docker Hub وفق الحساب الذي ذكرناه في متطلبات العمل: docker login سيُطلب منك إدخال اسم المستخدم وكلمة المرور لحسابك وفق التالي: Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one. Username: تحمل صورة التطبيق في بيئتنا الوسم polls:latest أعطها وسمًا جديدًا يتضمن اسم حسابك على Docker Hub واسم مستودعك باستخدام tag وفق التالي: docker tag polls:latest your_dockerhub_username/your_dockerhub_repo_name:latest استعملنا في مثالنا الاسم sammy للحساب و sammy-django للمستودع وبذلك يصبح الأمر على الشكل التالي: docker push sammy/sammy-django:latest ستحصل على خرج يتضمن تفاصيل عملية الرفع، وبعد إتمام العملية ستصبح الصورة متاحة أمام كوبيرنتس على Docker Hub. الخطوة 4: إعداد خريطة الإعدادات ConfigMap خريطة الإعدادات ConfigMap هي كائن كوبيرنتس المسؤول عن تمرير متغيرات بيئة التشغيل العامة إلى العنقود مثل إعدادات التطبيق، ويشاركه المهمة كائن آخر يدعى السرّ Secret إلاّ أنه يُعنى بالمتغيرات الحساسة مثل البيانات الخاصة بواجهات التخاطب API أو بقاعدة البيانات ولذا فهو يخضع لقواعد خاصة لحمايته والتحكم بوصول المستخدمين إليه مثل تفعيل خاصية encryption at rest، وبناءً على هذا الاختلاف في طبيعة بيانات الكائنين فإن بيانات ConfigMap تخزن بشكل نصي صريح ومقروء للمستخدم، بينما تخزن بيانات Secret معماة بأسلوب base64. تتشابه هذه الآلية في الواقع مع تمرير متغيرات البيئة لحاوية دوكر بالملف env الذي يلقم هذه المعلومات لأمر تشغيل الحاوية docker-run. لنبدأ بإعداد ConfigMap، والخطوة الأولى هي إنشاء مجلد يدعى yaml سنخزن ضمنه وثائق كوبرنيتس الأساسية: mkdir yaml cd ومن ثم أنشئ ضمن المجلد ملف نصي باسم polls-configmap.yaml يتضمن خريطة الإعدادات وفق التالي: nano polls-configmap.yaml وهذا يعني أننا بصدد إنشاء كائن كوبرنتس من نوع ConfigMap يحمل الاسم polls-config. الآن لننتقي من الملف env (الذي عملنا عليه في الخطوة /1/) البيانات العامة للتطبيق التي لا تتمتع بحساسية خاصة ونكتبها في الملف polls-configmap.yaml وذلك وفق الصيغة التالية: apiVersion: v1 kind: ConfigMap metadata: name: polls-config data: DJANGO_ALLOWED_HOSTS: "*" STATIC_ENDPOINT_URL: "https://your_space_name.space_region.digitaloceanspaces.com" STATIC_BUCKET_NAME: "your_space_name" DJANGO_LOGLEVEL: "info" DEBUG: "True" DATABASE_ENGINE: "postgresql_psycopg2" بالطبع مع وضع نفس القيم الموجودة في env وبخصوص المحدد DJANGO_ALLOWED_HOSTS نعطيه القيمة * كوننا نعمل في بيئة تجريبية. نحفظ بعدها التغييرات ونغلق الملف تمهيدًا لإنشاء الكائن ConfigMap بالأمر الأمر التالي: kubectl apply -f polls-configmap.yaml وسنحصل على الخرج التالي الذي يبين نجاح العملية: configmap/polls-config created الخطوة 5: إعداد السر Secret بيانات الكائن من نوع سرّ secret هي بيانات حساسة ومهمة كما ذكرنا سابقًا لذا فهي تُعمَّى بالأساس64، ولتحقيق ذلك لديك طريقتين إما ترميز البيانات خارجيًا من ثم كتابتها معماة ضمن الملف الذي سنعدّه للكائن، أو استخدام الأمر kubectl create مع الراية from-env-file-- وهي الطريقة التي استعملناها هنا. نعود للملف env استخدمنا متغيراته العامة في إنشاء الكائن Configmap والآن سنستخدم بقية المتغيرات التي تعد بيانات حساسة لإعداد الكائن secret. لننتقل للتطبيق العملي، انسخ الملف env إلى المجلد yaml وسمي النسخة polls-secrets وفق التالي: cp ../env ./polls-secrets افتح الملف polls-secrets باستعمال محرر النصوص: nano polls-secrets ستحصل على بيانات الملف env المنسوخ كاملةً: DJANGO_SECRET_KEY= DEBUG=True DJANGO_ALLOWED_HOSTS= DATABASE_ENGINE=postgresql_psycopg2 DATABASE_NAME=polls DATABASE_USERNAME= DATABASE_PASSWORD= DATABASE_HOST= DATABASE_PORT= STATIC_ACCESS_KEY_ID= STATIC_SECRET_KEY= STATIC_BUCKET_NAME= STATIC_ENDPOINT_URL= DJANGO_LOGLEVEL=info الآن احذف منه المتغيرات التي كتبناها ضمن ConfigMap في الخطوة السابقة، وستبقى المتغيرات التالية ضمنه: DJANGO_SECRET_KEY=your_secret_key DATABASE_NAME=polls DATABASE_USERNAME=your_django_db_user DATABASE_PASSWORD=your_django_db_user_password DATABASE_HOST=your_db_host DATABASE_PORT=your_db_port STATIC_ACCESS_KEY_ID=your_space_access_key STATIC_SECRET_KEY=your_space_access_key_secret ضع القيم الخاصة بتطبيقك نفسها التي ضبطناها في الخطوة /1/ من المقال (مثل محددات قاعدة البيانات والمفتاح الخاص وغيرها)، واحفظ التغييرات على الملف polls-secrets وأغلقه. أنشئ الآن الكائن من نوع secret والذي يحمل الاسم polls-secret من خلال الأمر التالي مع تمرير المتغيرات اللازمة للعنقود عبر الملف polls-secrets الذي أغلقته توًا: kubectl create secret generic polls-secret --from-env-file=poll-secrets وستحصل على الخرج الذي يبين نجاح الإنشاء: secret/polls-secret created استعرض بعدها وصف الكائن باستخدام kubectl describe: kubectl describe secret polls-secret لاحظ القيم المرمزة في الخرج: Name: polls-secret Namespace: default Labels: <none> Annotations: <none> Type: Opaque Data ==== DATABASE_PASSWORD: 8 bytes DATABASE_PORT: 5 bytes DATABASE_USERNAME: 5 bytes DJANGO_SECRET_KEY: 14 bytes STATIC_ACCESS_KEY_ID: 20 bytes STATIC_SECRET_KEY: 43 bytes DATABASE_HOST: 47 bytes DATABASE_NAME: 5 bytes مررنا بذلك إعدادات التطبيق جانغو إلى داخل عنقود كوبيرنتس وتم تخزينها باستخدام كائنات من نوع Secret و ConfigMap، وأصبحت بيئتنا جاهزة لنشر التطبيق ضمن العنقود. الخطوة 6: نشر التطبيق باستخدام وحدة تحكم النشر وحدة النشر Deployment هي إحدى وحدات تحكم عنقود كوبيرنتس، تتولى إدارة التطبيقات عديمة الحالة stateless applications التي تعمل ضمن العنقود. إذ أنها تدير أعباء العمل صعودًا ونزولًا فتُدخل العناصر بالخدمة أو تخرجها تبعًا لضغط العمل، بالإضافة إلى إعطائها أوامر إعادة التشغيل للحاويات التي تفشل أثناء الإقلاع. تتحكم وحدة النشر Deployment بواحد أو أكثر من كائنات Pod، أما Pod فهي أصغر عناصر عنقود كوبيرنتس وكل واحدة منها يمكن أن تتضمن حاوية أو أكثر، لمعرفة المزيد يمكنك الاطلاع على تعلم أساسيات Kubernetes على أكاديمية حسوب. افتح ملفًا جديدًا يحمل الاسم polls-deployment.yaml عبر أي محرر تفضله وليكن عبر nano مثلًا: nano polls-deployment.yaml والصق ضمنه التعليمات التالية: apiVersion: apps/v1 kind: Deployment metadata: name: polls-app labels: app: polls spec: replicas: 2 selector: matchLabels: app: polls template: metadata: labels: app: polls spec: containers: - image: your_dockerhub_username/app_repo_name:latest name: polls envFrom: - secretRef: name: polls-secret - configMapRef: name: polls-config ports: - containerPort: 8000 name: gunicorn أكتب اسم الصورة التي بنيناها في الخطوة /2/ ولا تنسَ الإشارة للاسم الذي رفعنا به الصورة على Docker Hub. لنشرح التعليمات السابقة بالترتيب. أطلقنا على وحدة التحكم الاسم polls-app وميّزناها بعنوان label ذا القيمة المزدوجة app: polls، وضبطنا عدد نسخ Pods المطلوب تشغيلها بنسختين وفق replicas: 2 أما إعدادات هذه Pods فتُحددها المواصفات المكتوبة تحت template. أما configMapRef و secretRef المستخدمين بعد envFrom فيحددان على الترتيب قيم المتغيرات التي عرفناها من خلال كائن خريطة الإعدادات واسمه polls-config والكائن سرّ واسمه polls-secret، وفي نهاية الملف عرفنا بوابة الحاوية containerPort وهي تحمل الرقم 8000 والاسم gunicorn. يمكنك تعلم المزيد عن وحدة تحكم النشر من خلال توثيقات كوبيرنتس. أغلق الملف بعد حفظ التغييرات، لنباشر بإنشاء وحدة تحكم النشر عبر الأمر التالي: kubectl apply -f polls-deployment.yaml تحصل على الخرج الذي يؤكد نجاح العملية: deployment.apps/polls-app created يمكنك أيضًا التأكد من الإنشاء عبر الأمر: kubectl get deploy polls-app الذي يعطي النتيجة: NAME READY UP-TO-DATE AVAILABLE AGE polls-app 2/2 2 2 6m38s في حال واجهت أي خطأ في عمل وحدة التحكم وأردت التأكد من حالتها استخدم الأمر: kubectl describe deploy أما لفحص الـ Pods وحالتها فاستخدم: kubectl get pod وستحصل على الخرج التالي: NAME READY STATUS RESTARTS AGE polls-app-847f8ccbf4-2stf7 1/1 Running 0 6m42s polls-app-847f8ccbf4-tqpwm 1/1 Running 0 6m57s الخطوة 7: السماح للمصادر الخارجية بالوصول إلى الخدمة سننشئ في هذه الخطوة خدمة كوبيرنتس خاصة بتطبيقنا، والخدمات في كوبيرنتس عبارة عن مفهوم مجرد يعرّف مجموعة من كائنات Pod لتظهر أمام المصادر الخارجية كخدمة شبكة واحدة أي كطرف واحد endpoint بغض النظر عن توقف أي Pod أو تشغيلها، والمصادر الخارجية في مثالنا هي حركة البيانات باتجاه التطبيق، وبالتالي من يطلب التطبيق جانغو سيطلب خدمة كوبيرنتس واحدة مستقرة ولن يتأثر بتوقف أو إضافة أي Pod داخلية. تتعدد أنواع خدمات كوبيرنتس، مثل خدمة ClusterIP التي تعتمد على عنوان IP الداخلي للعنقود، وخدمة NodePort التي تعرض الخدمة على بوابة محددة من كل عقدة، وخدمة LoadBalancer التي توفر موازنة حمل سحابية لحركة مرور البيانات القادمة من المصادر الخارجية إلى Pods داخل العنقود وذلك عبر خدمات NodePorts الخاصة بكل عقدة، والتي يتم إنشاؤها تلقائيًا في هذه الحالة. يمكنك تعلم المزيد عن هذه الخدمات من خلال توثيقات كوبيرنتس. لنبدأ بإنشاء الخدمة الخاصة بتطبيق جانغو وسنعتمد النوع NodePort. أنشئ ملفًا باسم polls-svc.yaml باستعمال محرر النصوص الذي تفضله وليكن باستعمال نانو nano: nano polls-svc.yaml والصق ضمنه التعليمات التالية: apiVersion: v1 kind: Service metadata: name: polls labels: app: polls spec: type: NodePort selector: app: polls ports: - port: 8000 targetPort: 8000 ``` لاحظ التعليمات السابقة، نوع الخدمة هو NodePort، اسمها polls وهي مميزة بالعنوان app: polls، وحددنا من خلال مُحدِّد selector الربط مع Pods الواجهات الخلفية بالعنوان app: polls و البوابة 8000. أغلق الملف بعد حفظ التغييرات، ومن ثم أنشئ الخدمة بالأمر kubectl apply: kubectl apply -f polls-svc.yaml ونحصل على ما يؤكد الإنشاء وفق الخرج التالي: service/polls created استعرض الخدمة وتأكد من إنشائها عبر kubectl get svc: kubectl get svc polls وسيظهر لك الخرج التالي، الذي يتضمن عنوان IP الداخلي للعنقود وبوابة العقدة NodePort وهي في حالتنا 32654: NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE polls NodePort 10.245.197.189 <none> 8000:32654/TCP 59s حصلنا على رقم البوابة، ونحتاج إلى عنوان IP الخارجي للخدمة للاتصال مع التطبيق، وسنحصل عليه بالأمر التالي: kubectl get node -o wide الذي يعطينا الخرج: NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME pool-7no0qd9e0-364fd Ready <none> 27h v1.18.8 10.118.0.5 203.0.113.1 Debian GNU/Linux 10 (buster) 4.19.0-10-cloud-amd64 docker://18.9.9 pool-7no0qd9e0-364fi Ready <none> 27h v1.18.8 10.118.0.4 203.0.113.2 Debian GNU/Linux 10 (buster) 4.19.0-10-cloud-amd64 docker://18.9.9 pool-7no0qd9e0-364fv Ready <none> 27h v1.18.8 10.118.0.3 203.0.113.3 Debian GNU/Linux 10 (buster) 4.19.0-10-cloud-amd64 docker://18.9.9 أصبحت معلوماتنا كاملة لنستعرض التطبيق بكتابة العنوان التالي في المتصفح: http://203.0.113.1:32654/polls من المفترض حصولك على واجهة التطبيق نفسها التي استعرضتها محليًا في الخطوة /1/: تصفح أيضًا واجهة إدارة التطبيق admin/ للتأكد أكثر: http://203.0.113.1:32654/admin إذًا تطبيق جانغو الآن يستخدم حاويتين متماثلتين ويمكن الوصول له من خارج العنقود عبر خدمة كوبيرنتس مستقرة من نوع NodePort، والخطوة الأخيرة المتبقية لنا هي تأمين هذا الوصول ليتم عبر HTTPS وذلك بالاستفادة من وحدة التحكم ingress-nginx (وهي من متطلبات بيئة العمل المفترض تحضيرها) وبإنشاء كائن إدخال يوجه الحركة الخارجية إلى الخدمة polls. الخطوة 8: إعداد HTTPS باستخدام Nginx Ingress ومدير الشهادات توجه موارد كوبيرنتس من نوع المدخلات Ingresses حركة البيانات HTTP و HTTPS بمرونة إلى داخل العنقود، وذلك من خلال كائنات الإدخال التي تحدد قواعد التوجيه من المصادر الخارجية إلى خدمات كوبيرنتس الموجودة داخل عنقودك، ووحدات تحكم الإدخال التي تنفذ هذه القواعد عبر موازنة الحمل وتوجيه الطلبات إلى خدمات الواجهات الخلفية الأنسب. لابد أنك جهزت المتطلبات الواردة في بداية المقال وأنت تحضر بيئة العمل، ومنها تثبيت وحدة التحكم Ingress-nginx والوظيفة الإضافية cert-manager الخاصة بأتمتة شهادات TLS، مع ما يلزم لتصديق النطاق من Let’s Encrypt، وفي أثناء العملية أنشأت كائن إدخال لاختبار صلاحية الشهادة والتشفير TLS مع اثنين من خدمات الواجهات الخلفية الوهمية المنشأة لهذا الغرض، لذا سنبدأ بحذف echo-ingress الذي أنشأته خلال التحضير وفق التالي: kubectl delete ingress echo-ingress يمكنك أيضًا حذف الخدمات الوهمية المنشأة أثناء التحضير عبر التعليمتين kubectl delete svc kubectl delete deploy لكنه ليس أمرًا ملزمًا في حالتنا. لديك أيضًا (من متطلبات بيئة العمل) سجل DNS من النوع A يربط اسم لنطاقك بعنوان الـ IP العام لمُدخل موازن الحمل، فيمكنك إذًا إنشاء مُدخل لنطاقك your_domain.com والخدمة polls. أنشئ ملف polls-ingress.yaml باستخدام محرر النصوص: nano polls-ingress.yaml والصق ضمنه التعليمات التالية: [polls-ingress.yaml] apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: polls-ingress annotations: kubernetes.io/ingress.class: "nginx" cert-manager.io/cluster-issuer: "letsencrypt-staging" spec: tls: - hosts: - your_domain.com secretName: polls-tls rules: - host: your_domain.com http: paths: - backend: serviceName: polls servicePort: 8000 بموجب التعليمات السابقة، وصّفنا كائن إدخال يدعى polls-ingress ليستخدم وحدة التحكم ingress-nginx ووظيفة مدير الشهادات cert-manager مع ضبط ClusterIssuer الخاص بها على خيار staging، كما فعّلنا الشهادات TLS لترتبط باسم النطاق الخاص بنا your_domain.com أما ملف الشهادة والمفتاح الخاص تم تخزينهم في كائن من نوع سرّ يدعى polls-tls، وأخيرًا وجهّنا حركة البيانات التي تطلب النطاق your_domain.com لتمر عبر البوابة 8000 من الخدمة polls. احفظ الملف الآن وأغلقه، لننشئ الكائن بالاعتماد عليه، باستعمال الأمر التالي: kubectl apply -f polls-ingress.yaml سنحصل بعدها على الخرج: ingress.networking.k8s.io/polls-ingress created يمكنك استعراض وصف الكائن وتتبع حالته باستخدام kubectl describe: kubectl describe ingress polls-ingress التي تعطي الخرج: Name: polls-ingress Namespace: default Address: workaround.your_domain.com Default backend: default-http-backend:80 (<error: endpoints "default-http-backend" not found>) TLS: polls-tls terminates your_domain.com Rules: Host Path Backends ---- ---- -------- your_domain.com polls:8000 (10.244.0.207:8000,10.244.0.53:8000) Annotations: cert-manager.io/cluster-issuer: letsencrypt-staging kubernetes.io/ingress.class: nginx Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal CREATE 51s nginx-ingress-controller Ingress default/polls-ingress Normal CreateCertificate 51s cert-manager Successfully created Certificate "polls-tls" Normal UPDATE 25s nginx-ingress-controller Ingress default/polls-ingress وبنفس الطريقة تستطيع استعراض وصف polls-tls للتأكد من صحته: kubectl describe certificate polls-tls وستظهر لك حالة الشهادة في الخرج: . . . Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Issuing 3m33s cert-manager Issuing certificate as Secret does not exist Normal Generated 3m32s cert-manager Stored new private key in temporary Secret resource "polls-tls-v9lv9" Normal Requested 3m32s cert-manager Created new CertificateRequest resource "polls-tls-drx9c" Normal Issuing 2m58s cert-manager The certificate has been successfully issued بذلك نكون قد وجّهنا الطلبات الواردة إلى التطبيق لتصبح HTTPS مشفرة، ولكن Clusterissuer في وضع staging ما يعني أن شهادتنا التجريبية المزيفة من Let's Encrypt لن تحظى بثقة معظم متصفحات الويب، أكتب الطلب التالي لنطاقك في نافذة سطر الأوامر: wget -O - http://your_domain.com/polls ولاحظ التحذير الظاهر في الخرج بسبب هذه الشهادة: . . . ERROR: cannot verify your_domain.com's certificate, issued by ‘CN=Fake LE Intermediate X1’: Unable to locally verify the issuer's authority. To connect to your_domain.com insecurely, use `--no-check-certificate'. سنتبع المقترح الوارد في الخرج أعلاه ونستخدم الراية no-check-certificate-- مع الطلب wget لتجاوز التحذير الخاص بالشهادة وفق التالي: wget --no-check-certificate -q -O - http://your_domain.com/polls ستحصل عندها على الخرج المبين أدناه والذي يظهر صفحة HTML المقابلة للواجهة polls/ وأيضًا مسار التخزين الكائني الذي أُحضرت منه الصفحة: <link rel="stylesheet" type="text/css" href="https://your_space.nyc3.digitaloceanspaces.com/django-polls/static/polls/style.css"> <p>No polls are available.</p> وهذا دليل على نجاح طلب النطاق، ما يعني أن بإمكاننا الآن تبديل Clusterissuer إلى وضع المنتج production عوضًا عن staging عبر فتح الملف polls-ingress.yaml: nano polls-ingress.yaml وتعديل قيمة cluster-issuer كما ذكرنا: [polls-ingress.yaml] apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: polls-ingress annotations: kubernetes.io/ingress.class: "nginx" cert-manager.io/cluster-issuer: "letsencrypt-prod" spec: tls: - hosts: - your_domain.com secretName: polls-tls rules: - host: your_domain.com http: paths: - backend: serviceName: polls servicePort: 8000 احفظ التغييرات على الملف وأغلقه، ولكن انتبه فهي لن تصبح نافذة حتى تُحدّث الكائن polls-ingress باستخدام التعليمة kubectl apply: kubectl apply -f polls-ingress.yaml سيشير الخرج إلى نجاح العملية: ingress.networking.k8s.io/polls-ingress configured شاهد التغير الذي طرأ على حالة الشهادات باستخدام: kubectl describe certificate polls-tls أو kubectl describe ingress polls-ingress وستحصل على الخرج التالي الذي يبين نجاح التحقق من الشهادات الجديدة وكونها مخزنة في السرّ polls-tls: . . . Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal CREATE 23m nginx-ingress-controller Ingress default/polls-ingress Normal CreateCertificate 23m cert-manager Successfully created Certificate "polls-tls" Normal UPDATE 76s (x2 over 22m) nginx-ingress-controller Ingress default/polls-ingress Normal UpdateCertificate 76s cert-manager Successfully updated Certificate "polls-tls" استعرض الآن تطبيقك your_domain.com/polls ولاحظ تفعيل HTTPS وعدم وجود أي تحذير بخصوص صلاحية الشهادة ويمكنك استعراض تفاصيل الشهادة وسلطة التصديق بالضغط على رمز القفل قرب الشريط المخصص لكتابة عنوان URL في متصفحك: ننصحك بإجراء اختياري أخير هو تعديل نوع الخدمة polls من NodePort إلى ClusterIP إذ إن هذا النوع يقبل فقط الحركة القادمة من عنوان IP الداخلي للعنقود ما يمنح تطبيقك مزيدًا من الأمان: افتح الملف polls-svc.yaml باستخدام المحرر: nano polls-svc.yaml بدل قيمة المحدد type من NodePort إلى ClusterIP: apiVersion: v1 kind: Service metadata: name: polls labels: app: polls spec: type: ClusterIP selector: app: polls ports: - port: 8000 targetPort: 8000 احفظ التغييرات على الملف وأغلقه، وحدّث بعدها الخدمة عبر الأمر: kubectl apply -f polls-svc.yaml --force سيؤكد لك الخرج التالي نجاح تعديل الخدمة: service/polls configured تحقق من حالة الخدمة المحدثة باستخدام: kubectl get svc polls ولاحظ معلومات الخرج: NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE polls ClusterIP 10.245.203.186 <none> 8000/TCP 22s وبذلك قيدّت الوصول لتطبيقك ليكون حصرًا من خلال اسم النطاق والمُدخل المنشأ في هذه الخطوة. خاتمة تعلمنا في هذا المقال طريقة نشر تطبيق جانغو قابل للتوسيع ومؤمن ببروتوكول HTTPS باستخدام عنقود كوبيرنتس، مع تسليم ملفات التطبيق الساكنة من وحدة تخزين كائني خارجية، وتوفير المرونة التامة لزيادة أو إنقاص عدد Pods التي تُخدم التطبيق بسرعة وبسهولة عبر المحدد replicas من ملف الإعدادات الخاص بوحدة تحكم النشر Deployment والتي أسميناها polls-app، ونذكرك بإمكانية تسريع التسليم باستخدام شبكة توصيل المحتوى CDN كإجراء اختياري. ترجمة -وبتصرف- للمقال How To Deploy a Scalable and Secure Django Application with Kubernetes لصاحبه Hanif Jetha. اقرأ أيضًا ما الفرق بين دوكر Docker وكوبيرنيتيس Kubernetes؟ مدخل إلى Helm: مدير حزم Kubernetes البدء مع إطار العمل جانغو لإنشاء تطبيق ويب
  7. توفر البيئة السحابية حلولًا مرنة لتوسيع التطبيقات العاملة ضمنها مثل جانغو Django وتأمينها، ومن أبرز هذه الحلول التوسع الأفقي بزيادة عدد خوادم التطبيق وعملها معًا لتحقيق إتاحة عالية للخدمة تخفف من فترات التوقف الناجمة عن الأعطال أو الضغط أو غيرها، وزيادة الإنتاجية وسعة المعالجة، واستخدام وكيل عكسي يوجه الطلبات الخارجية إلى خوادم التطبيق. لجهة التوسع الأفقي يمكنك استخدام أكثر من خادم تطبيق لأداء مهمة تشغيل جانغو مع خادم WSGI HTTP الخاص به (وهو في الغالب Gunicorn أو uWSGI)، وحول توجيه الحركة يمكنك إعداد خادم خاص يوزع الطلبات بين خوادم التطبيق مثل Nginx فهو قادر على القيام بدور الوكيل العكسي reverse proxying و موازن الحمل load balancing، بالإضافة إلى تأمين التخزين المؤقت Cache لمكونات وأصول التطبيق الساكنة static assets، علاوةً على أنه يدعم بروتوكول أمان طبقة النقل TLS لتكون كافة اتصالات التطبيق آمنة عبر HTTPS. أما حاويات دوكر المستخدمة في حالتنا فهي تتميز بسهولة تثبيت الحزم وإجراء الإعدادات للمكونات الموجودة داخلها (مثل جانغو وخادم Nginx) كما أنها تضمن للمطوّر أن هذه المكونات التي أعدّها ستعمل دائمًا بنفس الأسلوب بصرف النظر عن البيئة التي تُبنى فيها. ستتعلم في هذا المقال كيفية توسيع تطبيق جانغو Polls -هو اسم التطبيق- المغلف في حاوية توسيعًا أفقيًا بإعداد خادمين للتطبيق يُشغل كل منهما حاوية تتضمن جانغو وخادم Gunicorn، وكيفية تأمينه بتفعيل HTTPS وإعداد خادم ثالث يؤدي وظيفة الوكيل العكسي ويُشغل حاويتين واحدة تتضمن Nginx والأخرى تتضمن عميل Certbot الذي سيوفر لخادم Nginx شهادات TLS مصدقة من Let’s Encrypt وقادرة على منح تطبيقك تقييم عالي بنتائج اختبار SSL Labs، سيتلقى هذا الوكيل العكسي كافة الطلبات الخارجية الواردة ويحولها إلى خادمي تطبيق جانغو للإجابة عليها، وزيادة في الأمان سنقيّد الاتصال الخارجي بحيث يتم عبر الوكيل العكسي حصرًا. هذا المقال جزء من سلسلة قصيرة حول احتواء تطبيقات جانغو بحاويات وإدارتها وإليك فهرس السلسلة: بناء تطبيق جانغو بخادم Gunicorn ووضعه ضمن حاوية دوكر توسيع تطبيق جانغو وتأمينه عبر حاوية دوكر وخادم Nginx وخدمة Let's Encrypt نشر تطبيق جانغو آمن وقابل للتوسيع باستخدام كوبيرنتس متطلبات بيئة العمل ستحتاج المتطلبات التالية لتتمكن من التطبيق العملي للمقال: ثلاثة خوادم عليها نظام تشغيل أوبنتو (استعملنا في المقال إصدار 18.04 واستعن بمقال التهيئة الأولية لخادم أوبونتو 18.04): اثنان منها خوادم تطبيق لزوم تشغيل تطبيقك (جانغو/Gunicorn). خادم واحد يؤدي وظيفة الوكيل لزوم تشغيل Nginx و Certbot. مستخدم عادي -غير مسؤول- يتمتع بصلاحية sudo وجدار ناري فعال على الخوادم الثلاثة. تثبيت دوكر على الخوادم الثلاثة، اطلع على كيفية تثبيت دوكر واستخدامه على دبيان لإنجاز المهمة إذ طريقة التثبيت نفسها في النظامين. حجز اسم نطاق لتطبيقك، سنعتمد في مقالنا على الاسم your_domain.com، مع العلم بإمكانية حصولك على اسم نطاق مجاني من Freenom. ربط اسم النطاق بعنوان الـ IP العام للخادم الوكيل عبر إضافة سجل DNS من النوع A، يمكنك الحصول على تفاصيل إضافية حول الموضوع بالاطلاع على القسم الخاص بخدمة اسم النطاق DNS على أكاديمية حسوب. خدمة تخزين كائني متوافقة مع S3 ومع إضافة django-storages مثل DigitalOcean Space وظيفتها تأمين المساحة التخزينية اللازمة لملفات تطبيق جانغو الساكنة، مع مجموعة مفاتيح الوصول الخاصة بإدارة هذه المساحة، استرشد بكيفية إعداد مساحة على DigitalOcean Space وعدّل بما يلائم خدمة التخزين التي اخترتها. نظام إدارة قواعد بيانات متوافق مع جانغو مع إنشاء قاعدة بيانات ومستخدم خاص بالتطبيق، نحن اخترنا خادم PostgreSQL، اهتم بالتفاصيل التالية أثناء إنجازك لهذه النقطة لتتمكن من متابعة خطوات المقال: استخدم الاسم polls لقاعدة البيانات والاسم sammy لمستخدم قاعدة البيانات (هذه الأسماء ليست ملزمة ولكننا استخدمناها في المقال واستخدامك لها سيسهل عليك المتابعة)، لمزيد من التفاصيل حول الإنشاء استرشد بالخطوة /1/ من مقالنا السابق [بناء تطبيق جانغو بخادم Gunicorn ووضعه ضمن حاوية دوكر]()، مع العلم أن هذه المهمة يمكن إنجازها من أي خادم من الخوادم الثلاثة لبيئة العمل. اعتمدنا في المقال على نظام إدارة قواعد البيانات من نوع Managed PostgreSQL cluster الذي توفره DigitalOcean. لكن يمكنك تثبيت برنامج PostgreSQL وتشغيله وإعداده بنفسك مع الاستعانة بالفيديو التعليمي تثبيت وإعداد قاعدة بيانات PostgreSQL. الخطوة 1: إعداد خادم التطبيق الأول تتلخص العملية باستنساخ مستودع جانغو على الخادم الأول، ومن ثم بناء صورة التطبيق مع كل الإعدادات اللازمة لذلك وأخيرًا اختبارها وتشغيلها باستخدام حاوية دوكر. تنويه: في حال كنت مستمرًا معنا من المقال السابق [بناء تطبيق جانغو بخادم Gunicorn ووضعه ضمن حاوية دوكر]() فهذه الخطوة ستكون جاهزة لديك ويمكنك الانتقال للخطوة /2/ وهي تجهيز الخادم الثاني. سنبدأ بتسجيل الدخول إلى خادم التطبيق الأول واستنساخ polls-docker من قسم تطبيقات جانغو Polls في مستودع GitHub فهذا القسم يتضمن عينات تجريبية من التطبيق Polls مجهزة خصيصًا لتوثيقات جانغو التعليمية. أما القسم polls-docker من المستودع فيتضمن نسخًا معدّلة مسبقًا من تطبيق Polls ومجهزة للعمل بفعالية في بيئة الحاويات، راجع مقالنا السابق بناء تطبيق جانغو بخادم Gunicorn ووضعه ضمن حاوية دوكر لمزيد من المعلومات. إليك تعليمة الاستنساخ اللازم تنفيذها: git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git توجه بعدها إلى المجلد django-polls عبر الأمر التالي: cd django-polls يتضمن هذا المجلد كود جانغو المكتوب بلغة بايثون، ويحتوي أيضًا الملف Dockerfile الذي سيستخدمه دوكر لبناء صورة الحاوية، بالإضافة إلى الملف env الذي يتضمن متغيرات البيئة التي ستمرر للحاوية من بيئة التشغيل المحيطة. استعرض الملف Dockerfile عبر تعليمة لينكس cat: cat Dockerfile وستحصل على الخرج التالي وهو عبارة عن البيانات المكتوبة ضمن الملف Dockerfile: FROM python:3.7.4-alpine3.10 ADD django-polls/requirements.txt /app/requirements.txt RUN set -ex \ && apk add --no-cache --virtual .build-deps postgresql-dev build-base \ && python -m venv /env \ && /env/bin/pip install --upgrade pip \ && /env/bin/pip install --no-cache-dir -r /app/requirements.txt \ && runDeps="$(scanelf --needed --nobanner --recursive /env \ | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ | sort -u \ | xargs -r apk info --installed \ | sort -u)" \ && apk add --virtual rundeps $runDeps \ && apk del .build-deps ADD django-polls /app WORKDIR /app ENV VIRTUAL_ENV /env ENV PATH /env/bin:$PATH EXPOSE 8000 CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "mysite.wsgi"] انظر تعليمات الملف السابق بالترتيب وتابع الشرح الآتي. سيستخدم الملف صورة دوكر بإصدار بايثون 3.7.4، ويثبت اعتماديات وحزم بايثون اللازمة لكل من جانغو وخادم gunicorn وفق ملف متطلبات التطبيق django-polls/requirements.txt ومن ثم يحذف الملفات غير اللازمة بعد انتهاء التثبيت، وينسخ بعد ذلك كود التطبيق إلى الصورة ويسند قيمة المتغير PATH، وأخيرًا سيحدد البوابة 8000 لاستقبال حركة البيانات الواردة إلى الحاوية، ويشغل خادم gunicorn مع عمال workers عدد /3/ عبر البوابة 8000. لننتقل إلى بناء صورة الحاوية باستخدام docker build: docker build -t polls . سميّنا الصورة polls باستخدام الراية t- ومررنا المسار الحالي . كمسار context للتعليمة build فهو يتضمن كافة الملفات اللازمة لعملية البناء. بعد انتهاء عملية البناء استخدم الأمر docker images لاستعراض الصور المتاحة: docker images وستحصل على الخرج التالي: REPOSITORY TAG IMAGE ID CREATED SIZE polls latest 80ec4f33aae1 2 weeks ago 197MB python 3.7.4-alpine3.10 f309434dea3a 8 months ago 98.7MB تشغيل الصورة هي الخطوة التالية لعملية البناء، إلا أنها تتطلب ضبط متغيرات البيئة الموجودة ضمن الملف env وهو أحد لوازم تعليمة التشغيل docker run، لنعدّل عليه: افتح الملف env الموجود ضمن المجلد django-polls باستخدام محرر النصوص nano: nano env سيظهر أمامك الملف بالشكل التالي: DJANGO_SECRET_KEY= DEBUG=True DJANGO_ALLOWED_HOSTS= DATABASE_ENGINE=postgresql_psycopg2 DATABASE_NAME=polls DATABASE_USERNAME= DATABASE_PASSWORD= DATABASE_HOST= DATABASE_PORT= STATIC_ACCESS_KEY_ID= STATIC_SECRET_KEY= STATIC_BUCKET_NAME= STATIC_ENDPOINT_URL= DJANGO_LOGLEVEL=info كما ترى فبعض محددات الملف قيمها موجودة والبعض الآخر يحتاج إلى ضبط بما يناسب بيئتك، سنذكر المحددات التي تحتاج إلى ضبط بالترتيب: المفتاح السري DJANGO_SECRET_KEY: اختر له قيمة فريدة صعبة التخمين كما توصي توثيقات Docker نقدم لك في الخطوة /5/ من مقال كيفية إعداد تطبيق جانغو عبر DigitalOcean Managed Databases and Spaces إحدى طرق جانغو لتوليد المفاتيح. DJANGO_ALLOWED_HOSTS: أسند له اسم النطاق الخاص بك your_domain.com، أو يمكنك وضع الرمز * لأغراض الاختبار فقط وليس ضمن بيئة العمل الفعلية فهذا الرمز يسمح لأي عنوان IP بالوصول، يحميك هذا المحدد من الهجمات المعتمدة على حقن ترويسة HTTP وفي حال أردت المزيد من المعلومات اطلع على توثيقات جانغو الخاصة بهذا المحدد ضمن Core Settings DATABASE_USERNAME: ضع اسم مستخدم قاعدة البيانات PostgreSQL الذي أنشأته أثناء إعدادك لمتطلبات العمل. DATABASE_NAME: ضع القيمة polls أو الاسم الذي اخترته لقاعدة البيانات أثناء إعدادك لمتطلبات العمل. كذلك الأمر لكلمة المرور DATABASE_PASSWORD. ضع اسم مضيف قاعدة البيانات DATABASE_HOST ورقم بوابة الاتصال معها DATABASE_PORT أما المحددات STATIC_ACCESS_KEY_ID و STATIC_SECRET_KEY و STATIC_BUCKET_NAME و STATIC_ENDPOINT_URL فهي تتعلق بخدمة التخزين الكائني الخارجية، اضبطها بما يتناسب مع الخدمة التي تستخدمها. احفظ التغيرات على الملف env بعد الانتهاء وأغلقه استعدادًا لتنفيذ أمر تشغيل الحاوية عبر التعليمة docker run التي سنكتبها بطريقة تسمح بتجاوز CMD (التي تحدد افتراضيات بدء التشغيل) وتنشئ أثناء التشغيل مخطط قاعدة البيانات باستخدام كل من manage.py makemigrations و manage.py migrate وفق ما يلي: docker run --env-file env polls sh -c "python manage.py makemigrations && python manage.py migrate" لنشرح الأمر السابق، شغلنا صورة الحاوية polls:latest ومررنا لها متغيرات البيئة التي ضبطناها للتو ضمن الملف env، وتجاوزنا الأمر الافتراضي في ملف Dockerfile لننفذ الأمر التالي بدلًا منه الذي أنشأ مخطط قاعدة البيانات بالمواصفات المحددة في كود التطبيق: sh -c "python manage.py makemigrations && python manage.py migrate" إن كنت تنفذ docker run للمرة الأولى فستحصل على الخرج التالي: No changes detected Operations to perform: Apply all migrations: admin, auth, contenttypes, polls, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying polls.0001_initial... OK Applying sessions.0001_initial... OK مع العلم أن جانغو لن يجري أي عملية عندما تنفذ migrate في المرات القادمة طالما لم يطرأ أي تغيير على مخطط قاعدة بيانات التطبيق. نفذ بعدها أمر تشغيل جديد للحاوية مع استدعاء سطر الأوامر الخاص بالصدفة Shell كما يلي: docker run -i -t --env-file env polls sh تظهر بنتيجة هذا الأمر نافذة الصدفة shell ضمن الحاوية اكتب فيها تعليمة إنشاء المستخدم الخاص بتطبيق جانغو: python manage.py createsuperuser استكمل بياناته وهي اسم المستخدم وكلمة المرور والبريد الإلكتروني، واضغط بعدها على الاختصار Ctrl+D للخروج من الحاوية وإنهاء عملها. أما العملية الأخيرة فهي جمع الملفات الساكنة الخاصة بالتطبيق collectstatic ورفعها على خدمة التخزين الكائني وتتم وفق الأمر التالي علمًا أنها قد تستغرق بعض الوقت: docker run --env-file env django-polls:v0 sh -c "python manage.py collectstatic --noinput" يظهر بعدها الخرج التالي: 121 static files copied. يمكنك الآن تشغيل التطبيق: docker run --env-file env -p 80:8000 polls ستحصل على الخرج: [2019-10-17 21:23:36 +0000] [1] [INFO] Starting gunicorn 19.9.0 [2019-10-17 21:23:36 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) [2019-10-17 21:23:36 +0000] [1] [INFO] Using worker: sync [2019-10-17 21:23:36 +0000] [7] [INFO] Booting worker with pid: 7 [2019-10-17 21:23:36 +0000] [8] [INFO] Booting worker with pid: 8 [2019-10-17 21:23:36 +0000] [9] [INFO] Booting worker with pid: 9 مع العلم أن التشغيل هذه المرة وفق الإعدادات الافتراضية المنصوص عليها في ملف Dockerfile وهي: gunicorn --bind :8000 --workers 3 mysite.wsgi:application وأن البوابة أو المنفذ 8000 للحاوية فُتحت لتستقبل حركة البيانات القادمة من المنفذ 80 للخادم أوبونتو. يمكنك الآن كتابة عنوان URL الخاص بالتطبيق polls بمتصفح الإنترنت واستعراضه، لاحظ أنك ستحصل على الخطأ 404 (لم يتم العثور على الصفحة) في حال اكتفيت بالعنوان: http://APP_SERVER_1_IP/ بسبب عدم وجود بيانات للتطبيق تحت الجذر / لذا اكتب العنوان التالي لتحصل على واجهة polls: http://APP_SERVER_1_IP/polls ** تنويه **: إذا كنت تستخدم جدارًا ناريًا من النوع UFW فإنك ستلاحظ وجود ثغرة أمنية في عمل حاوية دوكر وهي أن دوكر يتجاهل كافة السياسات الأمنية الموجودة على هذا النوع من الجدران النارية ويسمح بالاتصالات بغض النظر عنها، فقد مكّن الاتصال مع البوابة 80 على الخادم دون أن نضبط مسبقًا أي إعداد على الجدار الناري، هذه الثغرة في الواقع موثقة على GitHub Issue وتعالجها الخطوة /5/ من مقالنا عبر تصحيح إعدادات الجدار الناري UFW، أما في حال كنت تستخدم جدارًا ناريًا أقوى وأكثر تطورًا كالذي توفره DigitalOcean’s Cloud Firewalls على سبيل المثال يمكنك تخطي هذا التحذير. اكتب الرابط التالي http://APP_SERVER_1_ip/admin للوصول إلى واجهة الدخول للوحة التحكم الخاصة بالتطبيق: أدخل بيانات المستخدم مدير التطبيق المنشأ سابقًا باستخدام الأمر createsuperuser وستظهر أمامك واجهة الإدارة والتحكم الخاصة بالتطبيق. تذكر أن تسليم المكونات الساكنة للتطبيق admin و polls يتم من خدمة التخزين الكائني ويمكنك اتباع هذه الخطوات لاختبار جودته وأيضًا التأكد من صحة إحضار الملفات. بعد أن تنتهي من التصفح اضغط Ctrl+c في نافذة كتابة الأوامر السطرية التي تشغل حاوية دوكر لتنهي عمل الحاوية ثم شغلها مجددًا بالنمط المنفصل detached الذي يسمح لها أن تعمل بالخلفية ويمكنك من تسجيل الخروج من جلسة ssh. اكتب الأمر التالي لتشغيل النمط المنفصل: docker run -d --rm --name polls --env-file env -p 80:8000 polls تشير الراية d- إلى النمط المنفصل، بينما تشير الراية rm-- إلى تنظيف نظام ملفات الحاوية بعد إغلاقها، أما polls فهو اسم الحاوية. ننهي بذلك إعداد خادم التطبيق الأول سجل الخروج منه، وافتح رابط التطبيق من المتصفح للمرة الأخيرة للتأكد من عمله بشكل سليم قبل انتقالنا للخطوة /2/ الخاصة بتجهيز الخادم الثاني. الخطوة 2: إعداد خادم التطبيق الثاني سنعرض الأوامر باختصار في هذه الخطوة نظرًا للتشابه الكبير بين إعداد هذا الخادم وخادم التطبيق الأول الذي شرحناه في الخطوة /1/، لنباشر التطبيق العملي. سجل الدخول إلى خادم التطبيقات الثاني وابدأ باستنساخ القسم polls-docker من مستودع django-polls على GitHub: git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git توجه إلى المسار التالي: cd django-polls أنشئ صورة التطبيق باستخدام docker build: docker build -t polls . افتح الملف env بمحرر النصوص الذي تفضله: nano env لتحصل على الخرج التالي: DJANGO_SECRET_KEY= DEBUG=True DJANGO_ALLOWED_HOSTS= DATABASE_ENGINE=postgresql_psycopg2 DATABASE_NAME=polls DATABASE_USERNAME= DATABASE_PASSWORD= DATABASE_HOST= DATABASE_PORT= STATIC_ACCESS_KEY_ID= STATIC_SECRET_KEY= STATIC_BUCKET_NAME= STATIC_ENDPOINT_URL= DJANGO_LOGLEVEL=info اضبط قيم الملف بما يناسب بيئتك ثم احفظ التغييرات على الملف وأغلقه. وأخيرًا شغل حاوية التطبيق بالنمط المنفصل كما يلي: docker run -d --rm --name polls --env-file env -p 80:8000 polls وافتح التطبيق باستخدام المتصفح للتأكد من سلامة عمله: http://APP_SERVER_2_IP/polls لاحظ الفرق الذي أحدثه النمط المنفصل فيمكنك الآن تسجيل الخروج من الخادم الثاني دون إنهاء عمل الحاوية فهي تعمل في الخلفية. أنهينا تجهيز خادمي تطبيق جانغو لننتقل للخطوة التالية ونجهز حاوية الوكيل العكسي. الخطوة 3: إعداد حاوية دوكر للخادم Nginx يتمتع خادم الويب Nginx بالعديد من المزايا أهمها قدرته على أداء وظائف متعددة مثل الوكيل العكسي وموازنة الحمل والتخزين لملفات الموقع، سيؤدي منها في هذه البيئة وظيفة الوكيل العكسي لحماية الواجهات الخلفية لخادمي جانغو وموازن الحمل لتوزيع الطلبات بينهما، كما سيؤمن تشفير الاتصالات مع خوادم التطبيق باستخدام شهادة TLS التي يوفرها Certbot ما يعني أنه سيجبر العميل على استخدام HTTPS عبر إعادة توجيه كافة الطلبات من نوع HTTP إلى المنفذ 443، ومن ثم إيصال طلبات HTTPS إلى خوادم جانغو بعد فك تشفيرها، أما وظيفة التخزين فلن نحتاجها إذ الآلية التي استخدمناها لإفراغ ملفات جانغو على وحدة التخزين الكائني تفي بالغرض. ونوّد أن نبين لك أن التصميم الذي اخترناه في هذا المقال والمؤلف من خادمي تطبيق كل منهما في حاوية مع حاوية ثالثة تتضمن Nginx هو واحد من عدة خيارات متاحة لكل منها مزايا مختلفة من حيث الأداء والحماية، فعلى سبيل المثال كان الممكن بناء Nginx ضمن إحدى حاويتي التطبيق ليقوم بدور الوكيل محليًا لخادم التطبيق الموجود معه في الحاوية نفسها وللخادم في الحاوية الثانية، وفي احتمالٍ آخر يمكنك بناء حاويتي Nginx فتصبح البنية حاوية Nginx مقابل كل حاوية تطبيق ومن ثم استخدام موازن حمل سحابي من أي مزود خدمة مثل DigitalOcean، أما المفاضلة بين هذه التصاميم المختلفة أنجزها بناءً على تحليل متطلباتك ونتائج اختبار الحمل Load Test الذي يبين لك نقاط الاختناق في كل بنية لتوسعها، فبنية مقالنا مثلًا تتمتع بالمرونة لجهة قابليتها للتوسع بإضافة حاوية Nginx ثانية أو أكثر في أي مرحلة تظهر فيها اختناقات الشبكة بسبب خادم Nginx الوحيد، كذلك يمكننا الاعتماد على موزان حمل سحابي بسرعة أعلى أو موازن حمل من رتبة الطبقة الرابعة L4 مثل HAProxy. والآن بعد أن أعطينا فكرة عن الاحتمالات المتعددة لتصميم البنية لنرجع إلى تطبيقنا العملي. سجل الدخول إلى الوكيل العكسي Nginx وأنشئ مسارًا يدعى config كما يلي: mkdir conf ومن ثم أنشئ ملفًا نصيًا باسم nginx.conf باستعمال أحد محررات النصوص مثل nano: nano conf/nginx.conf والصق ضمن هذا الملف الإعدادات التالية: upstream django { server APP_SERVER_1_IP; server APP_SERVER_2_IP; } server { listen 80 default_server; return 444; } server { listen 80; listen [::]:80; server_name your_domain.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name your_domain.com; # SSL ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem; ssl_session_cache shared:le_nginx_SSL:10m; ssl_session_timeout 1440m; ssl_session_tickets off; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; client_max_body_size 4G; keepalive_timeout 5; location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://django; } location ^~ /.well-known/acme-challenge/ { root /var/www/html; } } جهزنا الإعدادات السابقة بالاعتماد على عدة مصادر تتحدث عن إعداد Nginx مثل GitHub\Certbot و Gunicorn و Docker\Nginx ودراستها وتعديلها لتناسب البيئة، هذه العملية في الواقع خارج نطاق مقالنا لذا ننصحك بالاطلاع على المقال فهم بنية ملف إعدادات nginx وسياقات الإعدادات من أكاديمية حسوب والمقال الذي يتحدث عن خوارزميات اختيار كتل Server و Location ضمن الملف من DigitalOcean لتفهم المزيد عن الإعدادات، أو الاستعانة ببعض الأدوات الخاصة بتجهيز ملف Nginx مثل NGINXConfig على سبيل المثال لا الحصر. لنشرح الآن دلالات هذه الإعدادات وكيفية عملها حيث أنها تتضمن السياقات الرئيسية upstream و server و location وهي تُعنى بإعادة توجيه طلبات HTTP إلى HTTPS ومن ثم إيصالها إلى خادمي التطبيق مع موازنة الحمل بينهما. سياقات الملف conf/nginx.conf بالترتيب هي: سياق الخوادم العليا upstream وهو وحيد، يليه ثلاثة سياقات متتالية تسمى سياق الخادم server، وأخيرًا سياق الموقع location وهو مكرر مرتين متتاليتين في نهاية الملف. يحدد سياق الخوادم العليا upstream الخوادم التي ستتلقى طلبات الوكيل عبر عملية التوجيه proxy_pass: upstream django { server APP_SERVER_1_IP; server APP_SERVER_2_IP; } . . . أطلقنا الاسم django على هذا السياق وعرفنا ضمنه خادمي التطبيق عبر كتابة عنوان الـ IP الخاص بكل خادم، أما في حال كنت تستخدم شبكة سحابية افتراضية خاصة VPC للربط بين الخادمين فاستبدل هذه العناوين بعناوين الخوادم الخاصة Private IP وفق إعدادات الشبكة VPC، ويمكنك الاستعانة بالمثال لمزيد من التفاصيل حول تفعيل هذه الشبكة. بعد توجيه الطلبات من سياق الخوادم العليا، يلتقط أول سياق خادم الطلبات التي لا تخص نطاقك وينهيها، وتعدّ طلبات HTTP المباشرة لخوادم التطبيق مثالًا على الحالات التي يتعامل معها هذا السياق: . . . server { listen 80 default_server; return 444; } . . . أما ثاني سياق من نوع خادم فهو يتلقى الطلبات الواردة إلى نطاقك عبر HTTP ويعيد توجيهها إلى HTTPS عبر توجيه HTTP 301، ليعالجها بعد ذلك سياق الخادم الأخير: . . . server { listen 80; listen [::]:80; server_name your_domain.com; return 301 https://$server_name$request_uri; } . . . تحدد أسطر التوجيه التالية مسارات شهادة TLS والمفتاح الخاص Secret Key، علمًا أن Certbot سيؤمن هذه الملفات، ومن ثم ستُوصل إلى داخل الحاوية عبر التعليمة mount كما سنرى في المراحل التالية: . . . ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem; . . . أما محددات اتصال SSL فتُضبط وفقًا للقيم الافتراضية التي توصي بها Certbot كما يلي: . . . ssl_session_cache shared:le_nginx_SSL:10m; ssl_session_timeout 1440m; ssl_session_tickets off; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; . . . إن رغبت بمعلوماتٍ إضافية عن هذه المحددات وطريقة ضبطها فإننا نرشّح لك مقالين حول الموضوع من Nginx و Mozilla. استرشدنا بتوثيق Gunicorn الخاص بإعدادات Nginx لضبط عدد الاتصالات الأعظمي المسموح به client_max_body_size وزمن جلسة الاتصال keepalive_timeout وهي المدة الزمنية العظمى التي يتم بعدها قطع الاتصال مع العميل وتقدر بالثانية: . . . client_max_body_size 4G; keepalive_timeout 5; . . . لننتقل الآن للسياقات الأخيرة ضمن الملف وهي سياقات الموقع. يوجه أول سياق موقع Nginx أثناء تلقيمه وعكسه الطلبات إلى خوادم التطبيق المعرّفة ضمن سياق الخوادم العليا upstream وذلك عبر بروتوكول HTTP، مع المحافظة على معلومات ترويسة HTTP لاتصال العميل مثل عنوان الـ IP الأصلي وبروتوكول الاتصال المستخدم وعنوان الهدف: . . . location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://django; } . . . اطلع على توثيق Gunicorn وتوثيق النموذج ngx_http_proxy_module لمزيدٍ من المعلومات. يُسجل سياق الموقع الثاني الطلبات ضمن المسار /well-known/acme-challenge/ لاستخدامها من قبل Certbot للتحقق من اسم النطاق وفق تحدي HTTP-01 وهو أحد أنواع تحديات Let’s Encrypt التي تستخدمها للاختبار والتحقق. . . . location ^~ /.well-known/acme-challenge/ { root /var/www/html; } أغلق الآن ملف الإعدادات بعد حفظ التغييرات، حيث أنه سيُستخدم لتشغيل الحاوية. لننفذ الآن أمر تشغيل الحاوية وفق الصيغة التالية، بالاعتماد على الصورة nginx:1.19.0 وهي إحدى صور دوكر الرسمية المخصصة لخادم Nginx: docker run --rm --name nginx -p 80:80 -p 443:443 \ -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \ -v /var/www/html:/var/www/html \ nginx:1.19.0 ستلاحظ أثناء التشغيل ظهور خطأ مفاده عدم وجود الشهادات TLS المذكورة ضمن ملف الإعدادات nginx.conf، ومع ذلك سيكمل دوكر بناء الحاوية وتشغيلها، أما الخطأ سيُعالج في الخطوة التالية بتوفير الشهادات المطلوبة باستخدام Certbot و Let’s Encrypt. بالرجوع لأمر التشغيل سترى أننا أطلقنا الاسم nginx على الحاوية، وربطنا المنفذ 80 والمنفذ 443 للمضيف بالمنافذ التي تحمل الأرقام نفسها في الحاوية، واستخدمنا الراية v- لوصل ملف الإعدادات nginx.conf الذي ضبطناه مسبقًا إلى المسار etc/nginx/conf.d/nginx.conf/ داخل الحاوية بحيث تعتمده الصورة مع الانتباه إلى التعليمة تضمنت الراية ro التي تشير إلى "read only" أي أن الملف سيكون للقراءة فقط بالنسبة الحاوية فلا يمكن تعديله من داخلها، بالإضافة لذلك وصلنا مسار الجذر لتطبيق الويب إلى المسار var/www/html/ المقابل ضمن الحاوية، أما السطر الأخير في أمر التشغيل وهو nginx:1.19.0 فسيرشد دوكر إلى الصورة المطلوبة على Dockerhub لاستخدامها. الخطوة 4: إعداد Certbot وتجديد الشهادة من Let’s Encrypt Certbot هو عميل Let’s Encrypt الأشهر طورته شركة Electronic Frontier Foundation، وظيفته تزويد خوادم الويب بشهادات TLS مجانية مصدقة من Let’s Encrypt تعطي للمتصفحين الثقة بهوية الموقع. لنبدأ بالتطبيق العملي انطلاقًا من صورة Certbot المتوفرة على Dockerhub، ولكن تأكد أولًا من وجود سجل DNS من النوع A يرتبط بعنوان Public IP للخادم الوكيل، ثم نفذ أمر التشغيل التالي ليوفر عميل certbot الشهادات اللازمة للاختبار staging مع خوادم Let’s Encrypt: docker run -it --rm -p 80:80 --name certbot \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ certbot/certbot certonly --standalone --staging -d your_domain.com يشغل الأمر السابق الصورة certbot في الوضع التفاعلي، ويربط البوابة 80 على الخادم المضيف مع البوابة 80 ضمن الحاوية، كما أنه ينشئ المسارين /etc/letsencrypt/ و /var/lib/letsencrypt/ ويوصلهما بنفس المسارات على المضيف. سيعمل Certbot بموجب الأمر السابق بالنمط المستقل أي بدون خادم Nginx ويتصل بخوادم Let’s Encrypt لإنجاز اختبار التشفير والتحقق من النطاق، وأثناء ذلك سيُطلب منك عنوان بريد إلكتروني وتأكيد الموافقة على شروط الخدمة، أدخل المطلوب وانتظر اكتمال العملية وفي حال نجح التحقق من نطاقك ستحصل على الخرج التالي الذي يخبرك بمكان تخزين الشهادة: Obtaining a new certificate Performing the following challenges: http-01 challenge for stubb.dev Waiting for verification... Cleaning up challenges IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/your_domain.com/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/your_domain.com/privkey.pem Your cert will expire on 2020-09-15. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run "certbot renew" - Your account credentials have been saved in your Certbot configuration directory at /etc/letsencrypt. You should make a secure backup of this folder now. This configuration directory will also contain certificates and private keys obtained by Certbot so making regular backups of this folder is ideal. يمكنك استعراض ملف الشهادة باستخدام cat: sudo cat /etc/letsencrypt/live/your_domain.com/fullchain.pem سنعيد الآن تشغيل الحاوية nginx بنفس أمر التشغيل المستخدم في الخطوة /3/ ولكن مع إضافة مسارات Let’s Encrypt التي بنيناها توًا أثناء تشغيل حاوية certbot: docker run --rm --name nginx -p 80:80 -p 443:443 \ -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \ -v /etc/letsencrypt:/etc/letsencrypt \ -v /var/lib/letsencrypt:/var/lib/letsencrypt \ -v /var/www/html:/var/www/html \ nginx:1.19.0 فور إقلاع الحاوية nginx يمكنك أن تجرب فتح التطبيق من خلال المتصفح بوضع الرابط http://your_domain.com وستلاحظ وجود تحذير أمني مفاده انتهاء صلاحية الشهادة وهذا متوقع لأننا ما زلنا نعمل على الشهادات المرحلية الاختبارية staging، ولكن مع ذلك ستجد أن طلبك قد تم إعادة توجيهه من HTTP إلى HTTPS. اضغط الآن على الاختصار Ctrl+c للخروج من حاوية Nginx وشغل عميل Cerbot مجددًا ولكن مع حذف الراية الخاص بالاختبار staging--: docker run -it --rm -p 80:80 --name certbot \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ certbot/certbot certonly --standalone -d your_domain.com ستظهر لك رسالة لاختيار الإجراء الذي ترغب بتنفيذه تجديد الشهادة أو استبدالها، اضغط على الرقم 2 لخيار التجديد ومن ثم زر الإدخال Enter وستحصل بذلك على شهادة TLS فعالة production، ثم شغل الآن حاوية nginx بالاعتماد على هذه الشهادة وفق التالي: docker run --rm --name nginx -p 80:80 -p 443:443 \ -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \ -v /etc/letsencrypt:/etc/letsencrypt \ -v /var/lib/letsencrypt:/var/lib/letsencrypt \ -v /var/www/html:/var/www/html \ nginx:1.19.0 استعرض التطبيق مجددًا بكتابة الرابط في المتصفح: http://your_domain.com لاحظ إعادة توجيه طلبك إلى HTTPS وظهور رسالة "لم يتم العثور على الصفحة" بسبب عدم إعداد توجيه افتراضي للتطبيق Polls، استخدم إذًا الرابط التالي: https://your_domain.com/polls وذلك لاستعراض واجهة التطبيق التي تشبه الصورة التالية: زودنا تطبيقنا لغاية الآن بشهادة TLS عبر حاوية Certbot ووكيل عكسي وموازن حمل لتوجيه وموازنة الطلبات الخارجية الواردة إلى خوادم التطبيق، و قبل الانتقال للخطوة /5/ الختامية يتبقى لدينا موضوع أخير وهو تجهيز آلية مناسبة لتجديد صلاحية TLS حيث أن الشهادات التي تعطيها Let’s Encrypt تحتاج إلى تجديد دوري كل 90 يومًا. سنبدل أولًا نمط عمل Certbot من النمط المستقل standalone إلى النمط webroot ذلك أن بيئتنا تضمن خادم ويب Nginx، وهذا يعني أن عميل Certbot سينشئ الملف الخاص بعملية التحقق ضمن مسار الجذر لخادم الويب التالي: /var/www/html/.well-known/acme-challenge/ كذلك ستصل طلبات التحقق من Let’s Encrypt لهذا المسار وفق قواعد التوجيه الموجودة في سياقات الموقع ضمن ملف إعدادات Nginx، وعند إتمام عملية التجديد سنحتاج إلى إعادة تحميل Nginx ليستخدم الشهادات الجديدة الصالحة. يلجأ المستخدمون عادةً إلى أتمتة هذه العملية حتى ينجزها Certbot تلقائيًا وذلك بعدة أساليب أشهرها إضافة العملية على قائمة المهام المجدولة لنظام التشغيل لينكس عبر تعليمة cron، يمكنك الاطلاع على هذه العملية ضمن مقالٍ آخر حول تأمين Node.js من DigitalOcean. لننهي عمل حاوية Nginx بالضغط على الاختصار Ctrl+c ونعيد تشغيلها في الوضع المنفصل مع الراية d- وفق التالي: docker run --rm --name nginx -d -p 80:80 -p 443:443 \ -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \ -v /etc/letsencrypt:/etc/letsencrypt \ -v /var/lib/letsencrypt:/var/lib/letsencrypt \ -v /var/www/html:/var/www/html \ nginx:1.19.0 بينما يعمل خادم Nginx في الخلفية، سنشغل الحاوية certbot مع كل من إضافة webroot-- التي ستحدد مسار الجذر لخادم الويب، والراية dry-run-- لمحاكاة عملية التجديد والتأكد أن كافة الخيارات صحيحة دون القيام بالتجديد فعلًا: docker run -it --rm --name certbot \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ -v "/var/www/html:/var/www/html" \ certbot/certbot renew --webroot -w /var/www/html --dry-run وفي حال كانت المحاكاة سليمة ستحصل على الخرج التالي: Cert not due for renewal, but simulating renewal for dry run Plugins selected: Authenticator webroot, Installer None Renewing an existing certificate Performing the following challenges: http-01 challenge for your_domain.com Using the webroot path /var/www/html for all unmatched domains. Waiting for verification... Cleaning up challenges - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - new certificate deployed without reload, fullchain is /etc/letsencrypt/live/your_domain.com/fullchain.pem - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates below have not been saved.) Congratulations, all renewals succeeded. The following certs have been renewed: /etc/letsencrypt/live/your_domain.com/fullchain.pem (success) ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates above have not been saved.) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - يتوجب علينا بعد ذلك إعادة تحميل Nginx ليستخدم الشهادة المجددة وذلك وفق الأمر التالي: docker kill -s HUP nginx حيث ترسل التعليمة HUP إشارة إلى خادم Nginx داخل الحاوية ليعيد تحميل الإعدادات ويستخدم الإعدادات الجديدة بما فيها الشهادات المجددة. لننتقل الآن للخطوة الأخيرة وهي حماية خوادم الواجهات الخلفية للتطبيق عبر تقيّد الاتصالات الخارجية معها بالخادم الوكيل فقط. الخطوة 5: منع الوصول الخارجي إلى خادمي تطبيق جانغو في البنية الموصوفة في هذا المقال يتولى خادم Nginx فك تشفير اتصالات SSL ويوصل البيانات بدون تشفير إلى خوادم جانغو، تعدّ هذه الآلية مناسبة في العديد من الحالات إلا أن بعض التطبيقات (المالية مثلًا) قد تتطلب مستويات أمان أعلى فيعتمد مشغلوّها تقنية التشفير طرف إلى طرف end-to-end، وهو ما يمكنك تنفيذه بإيصال حزم البيانات مشفرة إلى خوادم التطبيق ليُفك تشفيرها هناك، أو بإعادة تشفيرها ثانيةً عند الخادم الوكيل وفك التشفير مجددًا على خوادم تطبيق جانغو، لن نتناول هذه التقنية من التشفير في مقالنا ولكن يمكنك الحصول على المزيد من المعلومات حولها بمراجعة التشفير طرف-إلى-طرف. إذًا الخادم Nginx في بنيتنا هو بوابة العبور بين الاتصالات الخارجية وخادمي جانغو ومن المفترض نظريًا أن لا يتصل أي عميل مع خوادم التطبيق إلا عبر هذه البوابة، ولكن تذكر الثغرة الأمنية المفتوحة التي واجهتنا في الخطوة /1/ حيث تمكن دوكر من تجاوز الجدار الناري UFW وفتح بوابات خارجية دون إعداد أي سياسات صريحة لفتح هذه البوابات على الجدار الناري. سنعالج هذه الثغرة عبر تعديل إعدادات الجدار الناري غير المعقد UFW مع العلم أن بإمكانك معالجتها بتعديل iptables الخاصة بنظام التشغيل مباشرةً بحيث تمنع الوصول غير المصرح به للحاوية دوكر، ويمكنك تعلم المزيد من مقال التّشبيك والتّواصُل على Docker ومقال أساسيات IPTables - قواعد وأوامر شائعة للجدار الناري، لكننا ننصحك باستخدام جدران نارية أكثر تطورًا في البيئة الحقيقية كالجدران السحابية مثلًا. لنبدأ الآن بتعديل إعدادات الجدار الناري UFW المشروحة في مستودع ufw-docker في GitHub لمنع الاتصالات الخارجية مع بوابات المضيف التي فتحها دوكر، فقد مررنا الراية p 80:8000- لحاويات خوادم التطبيق أثناء تشغيلها، ما يعني أن البوابة 80 على المضيف فُتحت ووُجّهت إلى البوابة 8000 على الحاوية وهي بذلك أصبحت متاحة أمام الاتصالات الخارجية أيضًا حيث أنها تمكننا من فتح الرابط: http://your_app_server_1_IP سجل الدخول إلى خادم التطبيق الأول وافتح الملف etc/ufw/after.rules/ بامتيازات sudo: sudo nano /etc/ufw/after.rules أدخل كلمة المرور واضغط زر الإدخال Enter وستظهر لك محتويات الملف after.rules وهي الإعدادات الخاصة بالجدار الناري UFW: # # rules.input-after # # Rules that should be run after the ufw command line added rules. Custom # rules should be added to one of these chains: # ufw-after-input # ufw-after-output # ufw-after-forward # # Don't delete these required lines, otherwise there will be errors *filter :ufw-after-input - [0:0] :ufw-after-output - [0:0] :ufw-after-forward - [0:0] # End required lines # don't log noisy services by default -A ufw-after-input -p udp --dport 137 -j ufw-skip-to-policy-input -A ufw-after-input -p udp --dport 138 -j ufw-skip-to-policy-input -A ufw-after-input -p tcp --dport 139 -j ufw-skip-to-policy-input -A ufw-after-input -p tcp --dport 445 -j ufw-skip-to-policy-input -A ufw-after-input -p udp --dport 67 -j ufw-skip-to-policy-input -A ufw-after-input -p udp --dport 68 -j ufw-skip-to-policy-input # don't log noisy broadcast -A ufw-after-input -m addrtype --dst-type BROADCAST -j ufw-skip-to-policy-input # don't delete the 'COMMIT' line or these rules won't be processed COMMIT الصق التعليمات التالية في نهاية الملف السابق (ومصدرها مستودع ufw-docker كما ذكرنا): . . . # BEGIN UFW AND DOCKER *filter :ufw-user-forward - [0:0] :DOCKER-USER - [0:0] -A DOCKER-USER -j RETURN -s 10.0.0.0/8 -A DOCKER-USER -j RETURN -s 172.16.0.0/12 -A DOCKER-USER -j RETURN -s 192.168.0.0/16 -A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN -A DOCKER-USER -j ufw-user-forward -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16 -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8 -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12 -A DOCKER-USER -j RETURN COMMIT # END UFW AND DOCKER تُقيّد التعليمات السابقة الوصول العام إلى البوابات التي فتحها دوكر وتسمح بالوصول لعناوين الشبكة الخاصة فقط وهي هنا 10.0.0.0/8 و 172.16.0.0/12 و 192.168.0.0/16 في حال كنت تستخدم الشبكات الخاصة مثل VPC وبذلك فإن Droplets البوابة الخاصة بهذه الشبكة ستتمكن من الوصول إلى البوابات المفتوحة بينما تُمنع المصادر الخارجية من ذلك، لتعرف المزيد عن إعدادات UFW في هذه الحالة انظر ufw-docker how-it-works. وفي حال أنك وضعت العناوين العامة Public IP لكل من خادمي التطبيق ضمن سياق الخوادم العليا في ملف إعدادات Nginx ولم تعتمد VPC، فعليك إذًا تعديل إعدادات UFW والسماح صراحةً بحركة البيانات القادمة من الخادم الوكيل Nginx إلى خوادم التطبيق عبر البوابة 80 وذلك عبر إنشاء سياسة خاصة بذلك باستخدام التعليمة allow، استعن لكتابتها بالمقال أساسيات IPTables - قواعد وأوامر شائعة للجدار الناري، واحفظ بعدها التغيرات وأغلق الملف. أعد تشغيل الجدار الناري ufw ليأخذ الإعدادات الجديدة عبر الأمر: sudo systemctl restart ufw تصفح بعدها موقع تطبيقك: http://APP_SERVER_1_IP ولاحظ أنه لم يعد متاحًا فقد منعت الاتصالات المباشرة مع خادم التطبيق عبر البوابة 80. سجل الخروج من الخادم الأول، وكرر الخطوات نفسها على خادم التطبيق الثاني وذلك عبر فتح الملف etc/ufw/after.rules/ بصلاحيات sudo: sudo nano /etc/ufw/after.rules ولصق التعليمات التالية في نهايته: . . . # BEGIN UFW AND DOCKER *filter :ufw-user-forward - [0:0] :DOCKER-USER - [0:0] -A DOCKER-USER -j RETURN -s 10.0.0.0/8 -A DOCKER-USER -j RETURN -s 172.16.0.0/12 -A DOCKER-USER -j RETURN -s 192.168.0.0/16 -A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN -A DOCKER-USER -j ufw-user-forward -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16 -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8 -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12 -A DOCKER-USER -j RETURN COMMIT # END UFW AND DOCKER أغلق الملف بعد حفظ التغييرات، ومن ثم أعد تشغيل الجدار الناري: sudo systemctl restart ufw وتصفح موقع الخادم ولاحظ أنه لم يعد متاحًا فقد منعت الاتصالات المباشرة مع هذا الخادم أيضًا.: http://APP_SERVER_2_IP افتح الآن الرابط التالي للتأكد من استمرار قدرة الخادم الوكيل على الوصول إلى التطبيق: https://your_domain_here/polls ولاحظ أن الوصول ممكن وواجهة التطبيق ستظهر أمامك في المتصفح. الخاتمة قدمنا لك هذا المقال نموذجًا لإعداد تطبيق جانغو في بيئة حاويات موزعة ومرنة قابلة للتوسيع بعدة طرق -في حال دعت الحاجة وازداد الطلب على التطبيق- منها توسيع خوادم تطبيق جانغو أو الخادم الوكيل أو قاعدة البيانات. ونقترح عليك بعض الأفكار لتطوير إدارة هذه البيئة منها استخدام أداة التمهيد systemd لإدارة نظامك بشكلٍ أفضل والاطلاع على توثيق دوكر الخاص بالتشغيل التلقائي للحاويات لخيارات متخصصة في التشغيل التلقائي للحاويات، والاستعانة بإحدى أدوات تكوين الحاويات مثل Ansible أو Chef لتسهيل العمل في بعض البيئات التي يستخدم فيها أكثر من مضيف الحاوية نفسها. دون أن ننسى إمكانية استخدام سجل الصور من Docker Hub وكذلك خط أنابيب pipeline لتسهيل نشر واختبار الصور على خوادم متعددة عوضًا عن إعادة بنائها وضبط إعداداتها مجددًا على كل خادم. ترجمة -وبتصرف- للمقال How To Scale and Secure a Django Application with Docker, Nginx, and Let's Encrypt لصاحبه Hanif Jetha. اقرأ أيضًا كيف تضبط جدار IPTables لحماية البيانات المنقولة بين خواديمك الهجمات الأمنية Security Attacks في الشبكات الحاسوبية رموز الإجابة في HTTP كيف تؤمّن خادم ويب NGINX
  8. يُعّد جانغو Django واحدًا من أقوى أطر العمل البرمجية التي تسهل عملية تطوير تطبيقات الويب المبنية باستعمال بايثون Python، وذلك لما يتمتع به من مميزاتٍ مفيدة مثل استخدامه تقنية ORM أي ربط الكائنات العلاقية object-relational mapper وتوفيره آلية للتحقق من هوية المستخدمين وواجهة مرنة قابلة للتخصيص لإدارة تطبيقك، بالإضافة إلى امتلاكه خاصية التخزين المؤقت وتشجيعه على تصميم الكود النظيف من خلال مصمم عناوين URL Dispatcher ونظام القوالب Template system. سنعرض في هذا المقال التعليمي كيفية بناء تطبيق جانغو اسمه Polls قابل للتوسع والنقل بحاويات دوكر Docker، وما يلزم من تعديلات ليعمل بفعالية داخل الحاوية ويتكيف مع متغيرات بيئة التشغيل، كما سنتطرق إلى ضبط تسجيل أحداث التطبيق، وتخزين أو تفريغ أصوله الساكنة static assets وهي صفحات جافاسكريبت وملفات التنسيق CSS في مساحة التخزين الكائني object storage لما له من أثر إيجابي على إدارتها إدارة مركزية سلسة ومنظمة بالأخص في بيئة متعددة الحاويات. وسننشئ بعد تنفيذ التعديلات المطلوبة -المستوحاة من منهجية Twelve-Factor الخاصة بتطوير تطبيقات ويب سحابية قابلة للتوسع والعمل ضمن الحاويات- على عينة من جانغو Polls صورة التطبيق image ونشغلّها ضمن حاوية دوكر. بالنتيجة ستغدو في نهاية هذا المقال قادرًا على وضع تطبيق جانغو في حاوية قابلة للنقل، وفي مقالات لاحقة ستتعلم استخدام Docker Compose مع جانغو وخادم Nginx كوكيل عكسي، ومن ثم تطبيق هذه البنى التي تعرفت عليها في حاويات عنقودية مثل كوبيرنتس Kubernetes. أخيرًا ننصحك بمتابعة خطوات العمل الواردة هنا بالتسلسل لضمان فهم كافة التغييرات التي سنُجريها على التطبيق، ولكن في حال رغبت بتخطي ذلك فيمكنك الحصول على كود التطبيق المعدل من القسم الخاص بتطبيقات polls-docker من مستودع GitHub. هذا المقال جزء من سلسلة قصيرة حول احتواء تطبيقات جانغو بحاويات وإدارتها وإليك فهرس السلسلة: بناء تطبيق جانغو بخادم Gunicorn ووضعه ضمن حاوية دوكر توسيع تطبيق جانغو وتأمينه عبر حاوية دوكر وخادم Nginx وخدمة Let's Encrypt نشر تطبيق جانغو آمن وقابل للتوسيع باستخدام كوبيرنتس متطلبات بيئة العمل ستحتاج المتطلبات التالية لتتمكن من التطبيق العملي لهذا المقال: تهيئة خادم بنظام تشغيل أوبونتو (استعملنا في المقال إصدار 18.04)، يمكنك الاستعانة بالمقال التهيئة الأولية لخادم أوبونتو 18.04 لتهيئته. تثبيت دوكر على الخادم، استعن بمقال كيفية تثبيت دوكر واستخدامه على دبيان فآلية العمل متشابهة في النظامين، دون أن تنسى إضافة مستخدم أوبونتو الخاص بك إلى مجموعة العمل دوكر ليأخذ صلاحيات العمل اللازمة. توفير خدمة تخزين كائني متوافقة مع S3 تؤمن المساحة التخزينية اللازمة لملفات وأصول تطبيق جانغو الساكنة بالإضافة إلى مجموعة من مفاتيح الوصول لإدارة هذه المساحة، وقد استُخدمت خدمة DigitalOcean Space من ديجيتال أوشن DigitalOcean في هذا المقال ويمكنك اختيار خدمة التخزين من المزود الأنسب لك مع إجراء تغييرات بسيطة تلائمها على خطوات الإعداد. نظام إدارة قواعد بيانات متوافق مع جانغو لإنشاء قاعدة بيانات التطبيق، وقد اخترنا DigitalOcean Managed PostgreSQL cluster استرشد بخطواتنا مع تغيير ما يلزم لقاعدة البيانات الخاصة بك. الخطوة 1: إنشاء قاعدة البيانات PostgreSQL مع مستخدم قاعدة البيانات يتلخص ما سنقوم به في هذه الخطوة بثلاث نقاط هي الاتصال بخادم قواعد بيانات PostgreSQL من خادم أوبونتو، وإنشاء قاعدة بيانات ومستخدم ضمنها خاص بتطبيق جانغو، ومن ثم ضبط إعداداتها لتعمل بكفاءة مع التطبيق، ولتحقيق ذلك سنبدأ بتثبيت عميل قاعدة بيانات PostgreSQL من مستودع برامج أوبونتو ولكن بعد تحديث دليل مدير الحزم apt، وفق الأمرين: sudo apt update sudo apt install postgresql-client ثم نؤكد العملية بالضغط على الحرف Y وزر الإدخال Enter، وبعد إتمام التحميل والتثبيت ننتقل للمرحلة التالية وهي إنشاء قاعدة البيانات والمستخدم الخاص بتطبيق جانغو. تتطلب هذه العملية معرفة محددات الاتصال مع PostgreSQL cluster مثل عنوان المضيف واسم قاعدة البيانات ورقم البوابة كذلك اسم المستخدم وكلمة المرور، ويمكنك الحصول على كافة هذه البيانات من لوحة التحكم الخاصة بخدمة PostgreSQL cluster ضمن مربع خاص بمحددات الاتصال إن كنت تستخدم نظام إدارة قواعد بيانات سحابي (مثل DigitalOcean أو غيره). والآن باستخدام هذه المحددات ونسخة العميل من PostgreSQL التي سبق تثبيتها، يمكنك تسجيل الدخول إلى PostgreSQL cluster وفق الأمر التالي: psql -U username -h host -p port -d database --set=sslmode=require ستُطلب منك كلمة المرور، أدخلها، واضغط زر الإدخال Enter، لتنتقل إلى موجه أوامر PostgreSQL الذي يخولك التحكم بقاعدة البيانات. أنشئ قاعدة بيانات خاصة بمشروعك باسم polls وفق التعليمة التالية (مع التنويه إلى أن كافة تعليمات psql يجب أن تنتهي بفاصلة منقوطة): CREATE DATABASE polls; ثم توجه إلى قاعدة البيانات polls الخاصة بك كما يلي: \c polls; وأنشئ ضمنها مستخدم قاعدة بيانات لمشروعك باسم sammy مثلًا -أو أي اسم- كما يلي واحرص على حمايته بكلمة مرور قوية: CREATE USER sammy WITH PASSWORD 'password'; اضبط بعد ذلك القيم الافتراضية لمحددات اتصال المستخدم المنشأ بالتوافق مع توصيات جانغو بحيث لا يتطلب الأمر الاستعلام عنها وإعادة تعيينها مع كل تأسيس جديد للاتصال ما سينعكس إيجابًا على تسريع عمليات قاعدة البيانات، والمحددات المطلوب ضبطها هي: الترميز encoding الذي يأخذ القيمة الافتراضية UTF-8، وآلية عزل العمليات transaction isolation scheme الذي يضبط إلى القيمة "read committed" ومهمته منع قراءة العمليات غير المكتملة uncommitted transactions، وأخيرًا المنطقة الزمنية نثبتها على UTC، وفق التعليمات التالية: ALTER ROLE sammy SET client_encoding TO 'utf8'; ALTER ROLE sammy SET default_transaction_isolation TO 'read committed'; ALTER ROLE sammy SET timezone TO 'UTC'; يتبقى الإعداد الأخير وهو منح مستخدم قاعدة البيانات المنشأ صلاحيات الإدارة الكاملة لقاعدة البيانات: GRANT ALL PRIVILEGES ON DATABASE polls TO sammy; وبعدها اخرج من موجه أوامر PostgreSQL باستخدام: \q جهزنا بذلك قاعدة البيانات ليُديرها تطبيق جانغو، لننتقل للخطوة الثانية المتمثلة بنسخ كود التطبيق Polls من GitHub وتحديد حزم اعتماديات بايثون اللازمة له. الخطوة 2: استنساخ مستودع التطبيق والتصريح عن اعتماديات عمله سنستخدم لعملية الاستنساخ مستودع التطبيق django-polls الذي يتضمن الكود الكامل لتطبيق Polls المجهز لأغراض البرنامج التعليمي في مشروع جانغو. بدايةً سجل الدخول إلى خادم أوبونتو وأنشئ مجلدًا يدعى polls-project ومن ثم استخدم الأمر git لاستنساخ مستودع django-polls من GitHub: mkdir polls-project cd polls-project git clone https://github.com/do-community/django-polls.git انتقل إلى django-polls واستعرض محتوياته بتعليمة ls: cd django-polls ls وسينتج الخرج التالي: LICENSE README.md manage.py mysite polls templates الذي يبين لك محتويات تطبيق django-polls، وهي العناصر المبينة أدناه، نعددها لك بإيجاز ويمكنك تعلم المزيد عنها بالاطلاع على التوثيق الخاص بإنشاء المشاريع من توثيقات جانغو الرسمية: manage.py: أداة سطر الأوامر الرئيسية لتنفيذ أوامر معالجة التطبيق. polls: يتضمن أكواد التطبيق polls. mysite: يتضمن أكواد وإعدادات جانغو project-scope. templates: يتضمن قوالب خاصة بواجهة الإدارة. أنشئ بعدها ملفًا نصيًا باسم requirements.txt ضمن نفس المسار polls-project/django-polls الذي تكلمنا عنه، وافتحه باستخدام محرر النصوص والصق ضمنه اعتماديات بايثون التالية، ثم احفظ التغييرات على الملف وأغلقه: boto3==1.9.252 botocore==1.12.252 Django==2.2.6 django-storages==1.7.2 docutils==0.15.2 gunicorn==19.9.0 jmespath==0.9.4 psycopg2==2.8.3 python-dateutil==2.8.0 pytz==2019.3 s3transfer==0.2.1 six==1.12.0 sqlparse==0.3.0 urllib3==1.25.6 كما تلاحظ فقد أوردنا بدقة كافة اعتماديات بايثون التي يحتاج إليها المشروع ومن بينها إضافة django-storages الخاصة بتخزين ملفات وأصول الموقع الساكنة على مساحة التخزين الكائني، وإضافة gunicorn الخاصة بخادم WSGI، كذلك مُحول psycopg2 لمواءمة التخاطب مع قاعدة بيانات PostgreSQL. والآن بعد أن أنهينا استنساخ التطبيق وحددنا ما يلزمه من اعتماديات لنبدأ بتعديله ليصبح قابلًا للنقل. الخطوة 3: تكييف جانغو مع متغيرات البيئة توصي منهجية Twelve-Factor باستخراج بيانات الإعدادات المدمجة مع كود التطبيق التي لا يمكن تعديلها إلاّ بتعديل الكود، وفصلها عن الكود ما يسمح للتطبيق بالتكيّف وتغيير سلوكه تبعًا لمتغيرات بيئة التشغيل، يوافقها في ذلك توصيات إعداد الحاويات لدى كلٍ من دوكر وكوبيرنتس Kubernetes، لذا سنتبع هذا النمط ونعدّل ملف الإعدادات django-polls/mysite/settings.py الخاص بتطبيق جانغو -وهو وحدة بايثون- بحيث يحصل على البيانات من بيئة التشغيل الفعلية المحلية عوضًا عن قراءتها من الكود، وذلك باستخدام الدالة getenv مع الوحدة os. لنبدأ بالتطبيق عمليًا! سنجعل المحددات المدمجة مع الكود ضمن الملف settings.py تأخذ قيمها من متغيرات البيئة عبر استدعاء الدالة os.getenv وتحديد اسم المتغير المرتبط ضمن قوسين، مع العلم بإمكانية تمرير قيمة ثانية للدالة تسمى بالقيمة الافتراضية أو الاحتياطية تُرجعها الدالة في حال كان المتغير غير معرف، لاحظ التالي: … SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') DEBUG = os.getenv('DJANGO_DEBUG', False) ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '127.0.0.1').split(',') … سيبحث جانغو عن متغير بيئة يدعى DJANGO_SECRET_KEY ويسند قيمته إلى المتغير SECRET_KEY، مع العلم أننا لم نمرر له قيمة احتياطية يعتمدها في حال أن المتغير لم يكن معرفًا، ذلك أننا نريده موحدًا على مستوى جميع خوادم التطبيق، وبنفس الوقت لا يجب أن يكون مدمجًا مع الكود، فكان تعريفه خارجيًا دون قيمة احتياطية هو الحل الأنسب، حيث أن فشل تشغيل التطبيق بسبب عدم توفر المتغير يعدّ بالتأكيد خيارًا أفضل من تشغيله مع المشاكل التي يمكن أن تنجم عن استخدام قيم متباينة من المفتاح السري SECRET_KEY في نسخ التطبيق المختلفة. بنفس الطريقة للمتغير أو المحدد DEBUG سيبحث عن متغير بيئة يدعى DJANGO_DEBUG ويأخذ قيمته، وفي حال لم يكن معرفًا فإنه سيأخذ القيمة الاحتياطية false التي ستحمينا من فقدان أي بيانات قد تكون حساسة ما لم نضبط المتغير إلى قيمة true عمدًا. أما بالنسبة للمتغير أو المحدد ALLOWED_HOSTS فسيُحضر قيمة متغير البيئة المرتبط به DJANGO_ALLOWED_HOSTS ويأخذ قيمته بعد فصله إلى قائمة بايثون باستخدام الدالة ()split مع اعتماد , كفاصل، وفي حال أن المتغير لم يكن معرفًا يأخذ المحدد القيمة 127.0.0.1 الاحتياطية. فور انتهائك من تعديل المحددات الثلاثة السابقة انتقل لتعديل محدد قاعدة البيانات DATABASES ضمن نفس الملف بحيث يأخذ أيضًا قيمته الافتراضية default من متغيرات البيئة، وذلك وفق التالي: . . . # Database # https://docs.djangoproject.com/en/2.1/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.{}'.format( os.getenv('DATABASE_ENGINE', 'sqlite3') ), 'NAME': os.getenv('DATABASE_NAME', 'polls'), 'USER': os.getenv('DATABASE_USERNAME', 'myprojectuser'), 'PASSWORD': os.getenv('DATABASE_PASSWORD', 'password'), 'HOST': os.getenv('DATABASE_HOST', '127.0.0.1'), 'PORT': os.getenv('DATABASE_PORT', 5432), 'OPTIONS': json.loads( os.getenv('DATABASE_OPTIONS', '{}') ), } } . . . لاحظ أننا استخدمنا json.loads لتعيين قيمة محدد قاعدة البيانات وفق الصيغة: DATABASES['default']['OPTIONS'] نستنتج من هذه الحالة أن متغيرات البيئة لن تكون دومًا سلاسل بسيطة سهلة القراءة فيمكن أن تأخذ لأنواعًا مختلفة من البيانات، ومن هنا تبرز أهمية المرونة التي يوفرها تمرير المتغيرات بملف JSON حتى ولو كانت على حساب الوضوح في قراءة الإعدادات. دون أن تنسى تفعيل مكتبة json ضمن ملف الإعدادات settings.py وفق التالي حتى تتمكن من استخدام هذا النوع من المتغيرات: """ Django settings for mysite project. Generated by 'django-admin startproject' using Django 2.1. For more information on this file, see https://docs.djangoproject.com/en/2.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/2.1/ref/settings/ """ import os import json . . . نشير أيضًا إلى محدد آخر يستدعي الاهتمام هو اسم قاعدة البيانات: DATABASES['default']['NAME'] فهو يدل على اسم قاعدة البيانات في معظم نظم إدارة قواعد البيانات العلاقية، أما في حال استخدامك قاعدة بيانات SQLite فإنه سيشير إلى ملف قاعدة البيانات لذا انتبه لهذه النقطة. وفي ختام التعديل لا بد لنا من الإشارة إلى أن الملف setting.py هو في النهاية ملف بايثون وبالتالي يمكنك قراءته والتعديل عليه بالطريقة التي تفضلها وما اتبعناه لهذا الغرض كان إحدى سبل التعديل المتاحة. ضبطنا حتى الآن إعدادات جانغو الرئيسية ومتغيراته بما فيها محددات قاعدة البيانات لتناسب النمط العام القابل للنقل، وسنتابع العملية في الخطوة التالية ولكن هذه المرة على مكونات وأصول التطبيق الساكنة مثل جافاسكريبت وصفحات CSS لتكون متاحة مركزيًا على خدمة التخزين الكائني المتوافقة مع S3. الخطوة 4: تفريغ المكونات الساكنة Offloading Static Assets تحتوي بعض بيئات التطوير على عدة حاويات جانغو وقد تكون كثيرة العدد ما يصعب على المطوّر مهمة الاحتفاظ بإصدارات محددة من الملفات والمكونات الساكنة الخاصة بكل حاوية، فيلجأ إلى جعل المشترك منها متوفرًا عبر وحدة تخزين خارجية يتم الوصول إليها من خلال الشبكة عوضًا عن توفيرها محليًا ومحاولة مزامنتها دائمًا باستخدام النسخ الاحتياطي الدوري أو التكرارية أو التحميل الروتيني، تسمى هذه العملية بالتفريغ Offloading. أما عن ضبط المحددات الخاصة بهذه الوحدة سنترك بعضها مدمجًا مع الكود hard-coded ونمرر بعضها الآخر لحاويات تطبيق جانغو عبر متغيرات البيئة بنفس الطريقة التي اتبعناها في الخطوة /3/ السابقة للتعامل مع محددات قاعدة البيانات. يتطلب هذا النوع من التخزين تثبيت حزمة django-storages التي تدعم وجود واجهات خلفية بعيدة للتطبيق تُخزن عليها المكونات والأصول الساكنة بما فيها وحدات التخزين الكائني المتوافقة مع S3 مثل DigitalOcean Space المستخدمة في مقالنا أو غيرها، ويمكنك الاستعانة بمقالة أجنبية أخرى تشرح عملية التفريغ في الخطوة رقم 7 منها. سنبدأ بتعديل الملف django-polls/mysite/settings.py وهو نفس الملف الذي تعاملنا معه في الفقرة السابقة بإضافة التطبيق storages على قائمة التطبيقات التي يستخدمها جانغو وذلك من خلال المحدد INSTALLED_APPS كما يلي: . . . INSTALLED_APPS = [ . . . 'django.contrib.staticfiles', 'storages', ] . . . تذكر أن التطبيق storages هو أحد الاعتماديات التي حددناها في الملف requirements.txt الذي ورد في الخطوة /1/ من الإعداد (وذلك من خلال django-storages). وبعد INSTALLED_APPS سنعدّل المحدد STATIC_URL كما يلي: # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.1/howto/static-files/ # Moving static assets to DigitalOcean Spaces as per: # https://www.digitalocean.com/community/tutorials/how-to-set-up-object-storage-with-django AWS_ACCESS_KEY_ID = os.getenv('STATIC_ACCESS_KEY_ID') AWS_SECRET_ACCESS_KEY = os.getenv('STATIC_SECRET_KEY') AWS_STORAGE_BUCKET_NAME= os.getenv('STATIC_BUCKET_NAME') AWS_S3_ENDPOINT_URL = os.getenv('STATIC_ENDPOINT_URL') AWS_S3_OBJECT_PARAMETERS = { 'CacheControl': 'max-age=86400', } AWS_LOCATION = 'static' AWS_DEFAULT_ACL = 'public-read' STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' STATIC_URL='{}/{}/'.format(AWS_S3_ENDPOINT_URL, AWS_LOCATION) STATIC_ROOT = 'static/' كما أسلفنا في مقدمة الفقرة، فإن بعض المحددات ستبقى مدمجة مع الكود hard-coded وهي: STATICFILES_STORAGE: يحدد الواجهة الخلفية التي يستخدمها جانغو لتفريغ الملفات الساكنة والقيمة S3Boto3Storage لهذا المحدد ستعمل مع كافة خدمات التخزين المتوافقة مع S3 بما فيها DigitalOcean Spaces. AWS_S3_OBJECT_PARAMETERS: يحدد معايير التخزين المؤقت cache control الخاصة بمكونات التطبيق الساكنة. AWS_LOCATION: يحدد مسار تخزين كامل الملفات الساكنة على خدمة التخزين الكائني وهذا المسار يسمى static. AWS_DEFAULT_ACL: يحدد قائمة التحكم بالوصول ACL لملفات التطبيق الساكنة، واختيار القيمة public-read لهذا المحدد يعني أن الملفات قابلة للقراءة من قبل العامة. STATIC_URL: يحدد العنوان الرئيس للملفات الساكنة Base URL ويتشكل بجمع مكونين الأول هو endpoint URL ويشير إلى خدمة التخزين الكائني والثاني هو مسار وجود الملفات الساكنة للتطبيق. STATIC_ROOT: يحدد المسار المحلي لتجميع الساكنة قبل نسخها إلى خدمة التخزين الكائني. أما المحددات التي ستعطي المرونة للاتصال مع وحدة التخزين وتأخذ قيمها من متغيرات البيئة فهي: AWS_ACCESS_KEY_ID: يأخذ قيمته من متغير البيئة STATIC_ACCESS_KEY_ID الذي يشير إلى معرف مفتاح الوصول الخاص بخدمة التخزين (وهي DigitalOcean Spaces في حالتنا). AWS_SECRET_ACCESS_KEY: يأخذ قيمته من المتغير STATIC_SECRET_KEY ويشير إلى المفتاح السري لخدمة التخزين. AWS_STORAGE_BUCKET_NAME: يأخذ قيمته من المتغير STATIC_BUCKET_NAME ويدل على الموقع الذي ستُرفع ملفات جانغو الساكنة إليه على خدمة التخزين. AWS_S3_ENDPOINT_URL: يأخذ قيمته من المحدد STATIC_ENDPOINT_URL ويستخدم للوصول إلى خدمة التخزين الكائني، فسيكون شكله من قبيل هذا العنوان https://nyc3.digitaloceanspaces.com في خدمة DigitalOcean Spaces على سبيل المثال بالطبع مع بعض الاختلافات المتعلقة بالمنطقة التي تحوي ملفاتك. لا تنسَ حفظ التغيرات على الملف settings.py قبل إغلاقه. ننهي بذلك الضبط المتعلق بالمكونات الساكنة، وبموجبه سيرفع جانغو ملفاته إلى وحدة التخزين الكائني البعيدة في كل مرة تشغل فيها manager.py collectionstatic لتُجمَّع الملفات الساكنة لمشروعك. ويتبقى لنا الإشارة لنقطة اختيارية وليست ملزمة لتسريع تسليم موقعك وهي الاشتراك بخدمة شبكة توصيل المحتوى CDN التي تحتفظ بنسخ من ملفات الموقع على خوادم الشبكة وتقوم بتسليم المستخدم الصفحات المطلوبة من أقرب نقطة جغرافية لمكانه، ويمكنك الحصول عليها من مزود خدمة التخزين الكائني نفسه إن كان يوفرها (على غرار ما يفعل DigitalOcean Spaces) أو من مصدر آخر، يمكنك كذلك إعداد نطاق فرعي للمساحة التخزينية المخصصة لك وهو إجراء اختياري أيضًا، للاستزادة استعن بالمقال الخاص بخدمة CDN. لننتقل الآن للخطوة التالية وهي ضبط تسجيل أحداث جانغو ضمن مجرى قياسي للخرج والخطأ STDOUT و STDERR تتعامل معه حاوية دوكر من خلال docker logs، وهذا سيكون تعديلنا الأخير على settings.py. الخطوة 5: ضبط تسجيل الأحداث إذا كنت تعتمد خادم التطوير المدمج في إطار العمل جانغو ليؤدي دور خادم HTTP لتطبيقك، وفعلت خيار التنقيح بإعطاء DEBUG القيمة true فإن جانغو سيُسجل الأحداث تلقائيًا في مجرى خرج قياسي STDOUT ومجرى خطأ قياسي STDERR، أما الإخلال بأي من الشرطين سواءً باستخدام خادم HTTP مغاير لخادم التطوير أو بإعطاء القيمة false للمحدد DEBUG، وكلاهما شائع في بيئة العمل الفعلية، فستتغير آلية التسجيل للتطبيق فبدلًا من تسجيل كافة الأحداث التي تحمل مستوى الخطورة INFO وما فوق ضمن المجرى القياسي، فإنه سيبعث الأحداث التي تحمل المستوى ERROR أو CRITICAL إلى البريد الإلكتروني لمدير النظام. هذا السلوك من جانغو لا يعدّ ملائمًا لبيئة الحاويات بالأخص المتعددة مثل Kubernetes (التي تعتمد النظام العنقودي) ففي هذه البيئات تُجمع تسجيلات الأحداث ضمن ملفات قياسية لكلٍ من الخرج والخطأ وتخزن في مسار مركزي على نظام ملفات العقدة بحيث تكون متاحة لأوامر الحاوية مثل kubectl و docker، وسهلة المراقبة والتتبع على فرق التشغيل. من هذه النقطة تبرز الحاجة لتعديل تسجيل أحداث جانغو ليتماشى مع هذه البنية ويعطي ملفات الخرج المرغوبة، والتطبيق مرن من هذه الناحية، فهو يستخدم وحدة التسجيل logging من مكاتب بايثون القياسية التي تتيح تعريف قاموس مخصص للتسجيل بالتفاصيل والصيغ التي نريدها وذلك باستخدام الدالة logging.config.dictConfig. لنبدأ التطبيق العملي! افتح الملف django-polls/mysite/settings.py عبر محرر النصوص، واكتب ضمنه تعليمة import إضافية خاصة بـ logging.config ستسمح لك بتجاوز سلوك التسجيل الإفتراضي لتطبيق جانغو عبر تمرير قاموس تسجيل الأحداث التي ترغب بها للدالة dictConfig، كما يلي: import json import os import logging.config . . . ثم انتقل إلى نهاية الملف والصق الكود التالي الخاص بإعدادات تسجيل الأحداث: . . . # Logging Configuration # Clear prev config LOGGING_CONFIG = None # Get loglevel from env LOGLEVEL = os.getenv('DJANGO_LOGLEVEL', 'info').upper() logging.config.dictConfig({ 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'console': { 'format': '%(asctime)s %(levelname)s [%(name)s:%(lineno)s] %(module)s %(process)d %(thread)d %(message)s', }, }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'formatter': 'console', }, }, 'loggers': { '': { 'level': LOGLEVEL, 'handlers': ['console',], }, }, }) لنناقش التعليمات السابقة: أعطينا المحدد LOGGING_CONFIG القيمة none وذلك لتعطيل النمط الإفتراضي لتسجيل جانغو، ومنحنا LOGLEVEL قيمة افتراضية هي info مع ترك المجال مفتوح لتجاوزها عند الحاجة بتمرير أي قيمة أخرى نريدها عن طريق متغير البيئة DJANGO_LOGLEVEL. وأخيرًا استخدمنا الدالة dictConfig لتعيين قاموس تسجيل الأحداث المرغوب الذي ستعتمده الوحدة logging.config، حددنا في القاموس طريقة تنسيق النص عبر formatters بحيث يتضمن مستوى خطورة الحدث ونوع الخط وغيرها من التفضيلات، كذلك حددنا الخرج عن طريق إعداد handlers، ومن ثم وضحنا كل سوية حدث بأي handler ترتبط باستخدام المسجلين loggers. الضبط الذي أجريناه هو الحد الأدنى المقبول من ضبط سجلات الأحداث فهو يمكننا من تحديد سوية خطورة معينة للأحداث عبر المتغير DJANGO_LOGLEVEL ما يجعل جانغو يسجل الأحداث بنماذج قياسية يمكن للحاويات التعامل معها وعرضها عبر docker logs أو kubectl logs تبعًا لنوع الحاوية، وإن رغبت بتفاصيل أعمق استرشد بتوثيقات جانغو حول سجلات الأحداث. ننهي بذلك التعديلات البرمجية على تطبيق جانغو الذي يدعى Polls وسنبدأ بتهيئة الحاوية عبر ملف Dockerfile في الخطوة التالية. الخطوة 6: كتابة الملف Dockerfile لحاوية التطبيق نقدم لك في هذه الفقرة إحدى أبسط الطرق لبناء صورة الحاوية container image اللازمة لتشغيل تطبيق جانغو مع خادم Gunicorn WSGI الخاص به، وكل ما يلزم العملية من إعدادات، مع العلم بوجود طرق أخرى يمكنك اتباعها. اختيار صورة الحاوية المناسبة للبناء عليها يوجد طريقتين لبناء صورة الحاوية إما أن تستخدم scratch الذي يوفر صورًا فارغة لا تحتوي أي نظام ملفات وتبني حاويتك من الصفر أو أن تعتمد صورة حاوية جاهزة، وهنا أيضًا لدينا العديد من الخيارات ولكلٍ منها متطلباته الخاصة من حيث الحزم المطلوبة ونسخة نظام التشغيل كذلك لغة البرمجة وغيرها، لذا يعد اختيار الصورة قرارًا مهمًا ويحتاج لبعض الدراسة. اختيار إحدى صور الحاويات الموجودة في مستودعات دوكر يعدّ خيارًا مناسبًا فهي مختبرة وفق أفضل الممارسات ويتم تحديثها بانتظام لتطويرها ومعالجة الثغرات الأمنية فيها، ويمكنك استعراض صور الحاويات المعتمدة على لغة بايثون من ذلك المستودع فهي متوافقة بالتأكيد مع جانغو المطور بنفس اللغة، ولاحظ أن لكل صورة خصائص معينة من حيث إصدار بايثون وأدواته كذلك نسخة نظام التشغيل، ويمكنك اختيار الصورة الأنسب لبيئتك من جهتنا فضلنا صور Alpine Linux فهي توفر بيئة تشغيل قوية على الرغم من بساطتها وصغر حجم نظام الملفات الخاص بها إلا أنها تتضمن نظامًا كاملًا لإدارة الحزم ومستودعات برمجية متنوعة ما يسهل عليك إضافة الوظائف التي يمكن أن تحتاجها. تنويه: ستلاحظ أثناء استعراضك صور الحاويات المعتمدة على لغة بايثون أن كل صورة تحمل أكثر من وسم tag وأن الوسوم نفسها توضع على صورٍ مختلفة، فمثلًا الوسم 3alpine يشير إلى تثبيت أحدث إصدار من بايثون 3 على أحدث إصدار من Alpine وفي حال نزل إصدار جديد من أحدهما فسيُعاد تعيين الوسم على الصورة الجديدة أيضًا، لذا اختر الصورة التي تحمل الوسوم الأكثر تحديدًا لتحصل على أقرب صورة للبيئة التي ترغب بتحقيقها، وفي مثالنا الحالي سنستخدم الصورة ذات الوسم 3.7.4-alpine3.10. سنحدد المستودع ووسم الصورة في ملف Dockerfile الخاص بتطبيقنا وذلك باستخدام التعليمة from، لنخرج أولًا من مجلد التطبيق django-polls حيث كنا نطبق التعليمات في الخطوات السابقة وذلك وفق الأمر التالي: cd .. أصبحنا الآن في المجلد الأب polls-project لننشئ ضمنه ملف نصي باستخدام المحرر ونسميه Dockerfile ونكتب فيه التعليمة التالية التي تعدّ نقطة البداية في إنشاء صورة الحاوية التي نجهزها للتطبيق: FROM python:3.7.4-alpine3.10 كتابة التعليمات التنفيذية الخاصة بإعداد التطبيق للحاوية تتضمن هذه الفقرة إضافة التعليمات التنفيذية وبعض الاعتماديات اللازمة وتهيئة بيئة التشغيل، قد تجد بعض أوجه التشابه التي تجمعها مع خطوات إعداد خادم التطبيق باستثناء بعض المفاهيم الخاصة بالحاويات. نضيف الكود التالي إلى الملف Dockerfile بعد تعليمة from السابقة: . . . ADD django-polls/requirements.txt /app/requirements.txt RUN set -ex \ && apk add --no-cache --virtual .build-deps postgresql-dev build-base \ && python -m venv /env \ && /env/bin/pip install --upgrade pip \ && /env/bin/pip install --no-cache-dir -r /app/requirements.txt \ && runDeps="$(scanelf --needed --nobanner --recursive /env \ | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ | sort -u \ | xargs -r apk info --installed \ | sort -u)" \ && apk add --virtual rundeps $runDeps \ && apk del .build-deps ADD django-polls /app WORKDIR /app ENV VIRTUAL_ENV /env ENV PATH /env/bin:$PATH EXPOSE 8000 سنقدم فيما يلي شرحًا لبعض ما ورد ضمن الكود السابق ولمزيد من المعلومات يمكنك الاطلاع على ملف Dockerfile جاهز لتطبيق جانغو. تتضمن التعليمة الأولى نسخ الملف requirements.txt إلى المسار app/requirements.txt/ بحيث تصبح اعتماديات التطبيق اللازمة موجودة ضمن نظام ملفات الصورة ليتم استخدامها في تنزيل كافة حزم بايثون الضرورية لعمل التطبيق، لاحظ أننا وضعنا هذه التعليمة في خطوة منفصلة وذلك ليقوم دوكر بتخزين الصورة التي تتضمن requirements.txt في طبقة منفصلة ما يتيح له إعادة استخدامها عند الحاجة عوضًا عن بنائها من جديد (بالطبع على فرض أن الاعتماديات لم تتغير)، ذلك أن دوكر ينشئ طبقة للصورة على نظام الملفات بعد تنفيذ كل تعليمة ADD أو RUN أو COPY. أما التعليمة الثانية RUN فهي تشتمل على مجموعة من أوامر لينكس يجمع بينها الإشارة && لتنفيذها على التوالي نوردها بإيجاز: تثبيت PostgreSQL وبناء اعتماديتها باستخدام apk مدير حزم Alpine. إنشاء البيئة الافتراضية. تثبيت اعتماديات بايثون المذكورة في الملف requirements.txt وذلك باستخدام pip. إعداد قائمة بالحزم المطلوبة للتشغيل من خلال تحليل متطلبات حزم بايثون المثبتة. إزالة تثبيت أي اعتماديات مبنية ولم تعد لازمة. جمعنا الأوامر السابقة بتعليمة RUN واحدة حتى يجمعها دوكر في طبقة واحدة ما يخفف عدد الطبقات المنشأة أثناء العمل، وحيث أن حذف العناصر المنشأة ضمن طبقة صورة معينة لن يكون متاحًا خارجها في طبقات لاحقة، فقد وضعنا الأمر del ضمن التعليمة Run نفسها التي تتضمن بناء الاعتماديات واستخدامها لتثبيت حزم التطبيق، ومن ثم حذفها عبر apk del، وذلك بهدف توفير المساحات التخزينية. أما التعليمة الثالثة بعد Run كانت ADD وهي تهدف إلى نسخ كود التطبيق ومسار العمل WORKDIR وذلك لضبط مسار العمل في الصورة على مسار التطبيق. تعرّف التعليمة الرابعة ENV اثنين من متغيرات البيئة التي ستكون متاحة ضمن الحاوية، المتغير الأول هو VIRTUAL_ENV ويأخذ القيمة env/ أما المتغير الثاني PATH يضبط على المسار env/bin/، وتحاكي هذه العملية الطريقة التقليدية المتبعة لتفعيل البيئة الافتراضية للتطبيق عبر سكريبت env/bin/activate/. أما العملية الأخيرة في هذه المرحلة فهي تحديد البوابة 8000 للاتصال مع الحاوية في وقت التشغيل عبر تعليمة EXPOSE. شارف الملف Dockerfile الاكتمال بما نفذناه حتى الآن والخطوة المتبقية له هي تحديد الأمر الذي سيُنفذ افتراضيًا عندما تبدأ الحاوية باستخدام الصورة أي عند بدء تشغيل الحاوية. تحديد الأمر الافتراضي يُكتب الأمر الافتراضي لحاوية دوكر في الملف Dockerfile باستخدام تعليمة ENTRYPOINT أو تعليمة CMD أو كلاهما معًا، ويحدد هذا الأمر ما سيُنفذ افتراضيًا عند بدء تشغيل الحاوية. في حال استخدمنا ENTRYPOINT و CMD معًا فإن الأولى ستحدد الأمر التنفيذي (الإجراء) الذي سينفذ عند تشغيل الحاوية والثانية ستحدد قائمة الوسطاء arguments اللازمة له. علمًا أن المستخدم يستطيع بسهولة تجاوز الوسطاء المعرّفين في CMD وذلك بتمرير وسطاء غيرهم لأمر تشغيل الحاوية كما يلي: docker run <image> <arguments> بينما لا يمكنه بنفس السهولة تجاوز الأمر التنفيذي الوارد في ENTRYPOINT لذا يلجأ الأغلبية لجعل الأمر التنفيذي المعطى لـ ENTRYPOINT على هيئة سكريبت يهيئ البيئة وينفذ الإجراءات التي تتناسب مع قائمة الوسطاء التي يتلقاها. ذكرنا فيما سبق طريقة استخدام ENTRYPOINT و CMD معًا، أما لو أردنا استخدام إحداهما فقط فسيكون الوضع كالتالي: استخدام ENTRYPOINT لوحدها يحدد الأمر التنفيذي للحاوية فقط دون تحديد قائمة الوسطاء. أما استخدام CMD لوحدها يتيح تحديد الأمر command والوسطاء arguments مع إمكانية تجاوز الوسطاء لاحقًا عبر docker run كما أسلفنا. لنطبق عمليًا على الصورة التي نبنيها ونناقش المتطلبات التي ستقودنا لاختيار التعليمة المناسبة لبدء التشغيل. نريد للتطبيق أن يعمل افتراضيًا باستخدام الخادم gunicorn أما تقديم قائمة الوسطاء لعمل gunicorn فهو غير ملزم في مرحلة بدء التشغيل، كما أننا نفضل بقاء الفرصة متاحة لتشغيل أوامر أخرى بسهولة عند بدء تشغيل الحاوية مثل عملية تنقيح الأخطاء debugging أو بعض المهام الإدارية (جمع المكونات الساكنة للتطبيق أو تهيئة قاعدة البيانات أو غيرها)، كل ذلك يدفعنا لاستخدام CMD بمفردها دون الحاجة لتعليمة ENTRYPOINT، لنتعرف إذًا على صيغ كتابتها. تكتب CMD عمومًا بثلاث صيغ: تمرير الوسطاء لتعليمة ENTRYPOINT: CMD ["argument 1", "argument 2", . . . ,"argument n"] بصيغة exec: CMD ["command", "argument 1", "argument 2", . . . ,"argument n"] بصيغة shell: CMD command "argument 1" "argument 2" . . . "argument n" تستخدم CMD في الصيغة الأولى مع ENTRYPOINT لتمرير قائمة الوسطاء فقط كما ذكرنا، أما في الصيغتين الثانية والثالثة تكون مسؤولة عن تحديد الأمر الذي سينفذ وكذلك الوسطاء، والفرق بينهما أن صيغة exec (وهي الصيغة الموصى بها) تنفذ الأمر المحدد مباشرةً وتمرر قائمة الوسطاء دون القيام بأي عمليات أو معالجة في الصدفة shell، أما في صيغة الصدفة shell فإن قائمة الوسطاء تُمرر إلى sh -c، وقد تكون ضرورية لك في الحالات التي تحتاج فيها لتبديل أحد متغيرات البيئة التي يستخدمها الأمر. بالنتيجة اعتمدنا الشكل النهائي التالي لتحديد افتراضات تشغيل الصورة، وكُتب ضمن Dockerfile كما يلي: . . . CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "mysite.wsgi:application"] وبموجبه تشغل الحاوية gunicorn على الجهاز المحلي localhost افتراضيًا وذلك عبر البوابة 8000 باستخدام عمال workers عدد 3، كما أنها تشغل الدالة application الموجودة ضمن الملف wsgi.py في المسار mysite. وبذلك أصبحنا جاهزين للمرحلة التالية وهي استخدام الأمر docker build لبناء صورة التطبيق والأمر docker run لتشغيل الحاوية. بناء صورة دوكر يبني docker build الصورة باستخدام كل من ملف dockerfile الذي يتضمن إعدادات البناء، وسياق "context" الذي يشير إلى مسار مجموعة الملفات التي يجب أن تتوفر لعفريت دوكر Docker daemon لينجز عملية البناء. المتغير "context" يطابق في معظم الأحيان المسار الحالي الذي نكون فيه عند تنفيذ الأمر docker build ويُشار إلى المسار الحالي في لينكس برمز النقطة . . للتنفيذ العملي، ابحث عن مسار الملف Dockerfile ونفذ ضمنه التالي مع تمرير اسم الصورة بعد الراية t- بالإضافة إلى تمرير المسار الحالي: docker build -t django-polls:v0 . لاحظ أننا أعطينا الاسم django-polls للصورة أما v0 فهي تشير للإصدار رقم 0 من الصورة، حيث أن عفريت دوكر الذي يعمل في الخلفية يبني الصورة عبر سلسلة من الطبقات بناءً على بنية تعليمات ملف Dockerfile. سنحصل على الخرج التالي فور إكتمال تنفيذ الأمر docker build: Successfully built 8260b58f5713 Successfully tagged django-polls:v0 نظريًا يمكنك تشغيل الحاوية عبر الأمر docker run بعد انتهاء عملية البناء بنجاح، ولكن في الواقع سيفشل الأمر run ما لم تعرف داخل الحاوية كافة متغيرات البيئة التي حددناها خارجيًا في الخطوات السابقة مثل SECRET_KEY ومحددات قاعدة البيانات ضمن الملف setting.py وغيرها، وهو ما سنقوم به في الخطوة التالية. الخطوة 7: ضبط متغيرات بيئة التشغيل واختبار التطبيق يوفر دوكر عدة طرق لتعريف المتغيرات داخل الحاوية، ولكون حالتنا تتطلب إعادة تعريف كامل المتغيرات التي عملنا عليها في الخطوة /1/، سنتبع الطريقة env-file التي تتيح تمرير ملف يتضمن كامل متغيرات البيئة وقيمة كل منها. لتنفيذ ذلك سننشئ ملفًا نصيًا يدعى env ضمن المجلد polls-project مع لصق القائمة التالية ضمنه وهي تحتوي كامل المتغيرات وقيمها: DJANGO_SECRET_KEY=your_secret_key DEBUG=True DJANGO_ALLOWED_HOSTS=your_server_IP_address DATABASE_ENGINE=postgresql_psycopg2 DATABASE_NAME=polls DATABASE_USERNAME=sammy DATABASE_PASSWORD=your_database_password DATABASE_HOST=your_database_host DATABASE_PORT=your_database_port STATIC_ACCESS_KEY_ID=your_space_access_key_id STATIC_SECRET_KEY=your_space_secret_key STATIC_BUCKET_NAME=your_space_name STATIC_ENDPOINT_URL=https://nyc3.digitaloceanspaces.com DJANGO_LOGLEVEL=info لا تنسَ تغيير القيم المطلوبة بما يناسب إعداداتك وفق التالي: بالنسبة للمفتاح السري DJANGO_SECRET_KEY اختر له قيمة فريدة صعبة التخمين كما توصي توثيقات دوكر تقدم لك الخطوة /5/ من مقال كيفية إعداد تطبيق جانغو عبر DigitalOcean Managed Databases and Spaces الأجنبي إحدى طرق جانغو لتوليد المفاتيح. ضع عنوان IP الخاص بالخادم أوبونتو مقابل DJANGO_ALLOWED_HOSTS ويمكنك وضع الرمز * لأغراض الاختبار فقط وليس ضمن بيئة العمل الفعلية فهو يسمح باتصال أي عنوان IP. اكتب اسم مستخدم قاعدة البيانات وكلمة المرور كما حددتهما في الخطوات السابقة مقابل DATABASE_USERNAME و DATABASE_PASSWORD. كذلك الأمر بالنسبة لاعدادات الاتصال مع قاعدة البيانات من حيث اسم مضيف قاعدة البيانات وبوابة اتصال قاعدة البيانات ضع هذه البيانات مقال كل من DATABASE_HOST و DATABASE_PORT. أما المحددات STATIC_ACCESS_KEY_ID و STATIC_SECRET_KEY و STATIC_BUCKET_NAME و STATIC_ENDPOINT_URL فهي تتعلق بخدمة التخزين الكائني الخارجية، اضبطها بما يتناسب مع الخدمة التي تستخدمها. قبل تشغيل الأمر docker run احفظ التغيرات على الملف السابق env وتأكد أن المحدد DEBUG يحمل القيمة false وأن سجلات الأحداث مضبوطة بحيث تعطي التفاصيل المرغوبة. وبعدها نفذ أمر التشغيل للإصدار v0 من الصورة django-polls بطريقة تتجاوز الإعدادات الافتراضية التي حددناها سابقًا ضمن الملف Dockerfile (باستخدام CMD) وتنشئ أثناء التشغيل مخطط قاعدة البيانات الخاص بالتطبيق عبر manage.py makemigrations و manage.py migrate: docker run --env-file env django-polls:v0 sh -c "python manage.py makemigrations && python manage.py migrate" سيظهر الخرج التالي بعد التنفيذ: No changes detected Operations to perform: Apply all migrations: admin, auth, contenttypes, polls, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying polls.0001_initial... OK Applying sessions.0001_initial... OK ننفذ بعدها أمر تشغيل جديد لحاوية التطبيق نستدعي خلاله سطر الأوامر الخاص بالصدفة Shell كما يلي: docker run -i -t --env-file env django-polls:v0 sh تظهر عندها نافذة صدفة shell ضمن الحاوية وننشئ من خلالها المستخدم الذي يحمل صلاحية مدير تطبيق جانغو: python manage.py createsuperuser يلي ذلك إدخال بيانات المستخدم وهي اسم المستخدم وكلمة المرور والبريد الإلكتروني، ومن ثم الخروج من الحاوية وإنهاء عملها باستخدام الأمر Ctrl+D. أما العملية الأخيرة هي جمع الملفات الساكنة الخاصة بالتطبيق collectstatic ورفعها على خدمة التخزين الكائني: docker run --env-file env django-polls:v0 sh -c "python manage.py collectstatic --noinput" لنحصل على الخرج التالي: 121 static files copied. يمكننا الآن تشغيل التطبيق بالإعدادات الافتراضية المحددة ضمن Dockerfile: docker run --env-file env -p 80:8000 django-polls:v0 ونحصل على الخرج التالي: [2019-10-17 21:23:36 +0000] [1] [INFO] Starting gunicorn 19.9.0 [2019-10-17 21:23:36 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) [2019-10-17 21:23:36 +0000] [1] [INFO] Using worker: sync [2019-10-17 21:23:36 +0000] [7] [INFO] Booting worker with pid: 7 [2019-10-17 21:23:36 +0000] [8] [INFO] Booting worker with pid: 8 [2019-10-17 21:23:36 +0000] [9] [INFO] Booting worker with pid: 9 كما ذكرنا فإن التشغيل هذه المرة تم وفق الإعدادات الافتراضية ضمن ملف Dockerfile وهي كما حددناها سابقًا: gunicorn --bind :8000 --workers 3 mysite.wsgi:application وبذلك فإن البوابة 80 الخاصة بالخادم أوبونتو ستوجه الحركة إلى البوابة 8000 في الحاوية django-polls:v0. يمكنك الآن فتح التطبيق polls عبر متصفح الإنترنت بكتابة عنوان URL الخاص به، وانتبه أنك ستحصل على الخطأ 404 (لم يتم العثور على الصفحة) في حال اكتفيت بالعنوان التالي مثلًا: http://your_server_ip/ بسبب عدم وجود بيانات للتطبيق تحت الجذر / ، لذا اكتب العنوان التالي: http://your_server_ip/polls اكتب الرابط http://your_server_ip/admin للوصول إلى واجهة الدخول للوحة التحكم الخاصة بالتطبيق: أدخل بيانات المستخدم مدير التطبيق المنشأ سابقًا باستخدام الأمر createsuperuser وستظهر أمامك واجهة الإدارة والتحكم الخاصة بالتطبيق. لاحظ أن تسليم المكونات الساكنة للتطبيق admin و polls يتم من خدمة التخزين الكائني ويمكنك اتباع اختبار جودة والتأكد من صحة إحضار الملفات. اضغط Ctrl+c في نافذة كتابة الأوامر السطرية التي تشغل حاوية دوكر لإنهاء عمل الحاوية. خاتمة استعرضنا في هذا المقال طريقة تكييف تطبيق جانغو ليعمل بفعالية ضمن بيئة تطوير سحابية أصلية cloud-native معتمدة على الحاويات container-based، وكتابة ملف Dockerfile محلي ومبسط لصورة الحاوية ومن ثم تشغيله عبر محرك دوكر، ويمكنك رؤية التغييرات التي طبقناها وتأثيراتها باستعراض قسم polls-docker من مستودع GitHub فهو يتضمن كافة تعديلات التطبيق polls التي ناقشناها في هذا المقال. إن ما تعلمته هنا يمهد لك الطريق لتجهيز بيئة متعددة الحاويات فيمكنك جمع حاوية Django/Gunicorn التي عملنا عليها مع حاوية تتضمن خادم وكيل عكسي Nginx لمعالجة طلبات HTTP بكفاءة، والحصول على شهادات TLS لتشفير هذه الطلبات عبر حاوية ثالثة تتضمن Certbot، وفي النهاية إدارة هذه الحاويات جميعًا من خلال Docker Compose. وقبل الختام لابد لنا من التنويه إلى أن متطلبات العمل في بيئة حقيقية تفرض عليك تشغيل Gunicorn خلف خادم HTTP وإلا فإن تطبيقك سيكون عرضة لهجمات الحرمان من الخدمة، كذلك عليك زيادة عدد العمال عن الذي اعتمدنا في المقال فالعدد 3 بالتأكيد لن يكون مناسبًا، عدّل العدد ليلائم حجم الطلب على الموقع، كذلك الأمر بالنسبة لتفريغ مكونات التطبيق الساكنة على وحدة تخزين كائني خارجية فقد اخترنا ذلك للتخفيف من عبء إدارتها في بيئة متعددة الحاويات أما في بيئتك الفعلية فاختر التصميم الذي تجده مناسبًا لمتطلباتك. أخيرًا يمكنك تسجيل الحاوية التي أعددتها على Dockerhub وجعلها متوفرة لأي نظام يحتوي دوكر سواء أكان خوادم أوبونتو أو بيئات افتراضية أو نظم حاويات عنقودية مثل Kubernetes. ترجمة -وبتصرف- للمقال How to Build a Django and Gunicorn Application with Docker لصاحبيه Hanif Jetha و Justin Ellingwood. اقرأ أيضًا ما هي تقنية Docker؟ التعامل مع قواعد البيانات نظام كوبيرنتس Kubernetes وكيفية عمله مدخل إلى صدفة باش Bash
  9. تجنب الأعداء والركض بعيدًا عنهم شيء والقتال ضدهم شيء آخر تمامًا، فهو يزيد التفاعل بين المستخدم واللعبة، وهو موضوع المقال الختامي من سلسلة بناء لعبة من الصفر باستخدام بايثون، إليك مقالات السلسلة بالترتيب قبل أن نبدأ: بناء لعبة نرد بسيطة بلغة بايثون. بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame. إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame. تحريك شخصية اللعبة باستخدام PyGame. إضافة شخصية العدو للعبة. إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame. محاكاة أثر الجاذبية في لعبة بايثون. إضافة خاصية القفز والركض إلى لعبة بايثون. إضافة الجوائز إلى اللعبة المطورة بلغة بايثون. تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة. إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون. لديك تفصيلين تهتم بهما في هذه الحالة، الأول هو إنتاج العنصر الذي سيُرمى والثاني هو المعيار أو الشرط الذي سيبقيه فعالًا فالسهام والكرات النارية لابد ستختفي في النهاية قد تختفي عند حافة الشاشة مثلًا أو تظل صالحة لفترة زمنية محدودة ينتهي مفعولها بعدها وهكذا. بالإضافة إلى تحديد قدرة اللاعب على القذف فهو يستطيع أن يقذف عنصرًا واحدًا فقط في كل مرة، يرفع هذا الخيار من مستوى التحدي بإعطاء اللاعب فرصةً وحيدة في كل مرة لإصابة العدو، ويساهم من جهةٍ ثانية بتبسيط تعليماتك البرمجية. يمكنك بعد نهاية القراءة وتعلم الموجود هنا أن تسعى لتطوير البرنامج وتعطي لاعبك القدرة على قذف أكثر من عنصر في وقتٍ واحد. إنشاء صنف الكائن المقذوف ستجد الدالة __init__ ضمن تعليمات الصنف المبينة أدناه لإنتاج كائن بايثون للعنصر المقذوف، وهي الدالة نفسها المستخدمة سابقًا لإنتاج البطل و الأعداء. class Throwable(pygame.sprite.Sprite): """ إنتاج الكائن المقذوف """ def __init__(self, x, y, img, throw): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images',img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.firing = throw الاختلاف الجوهري بين الدالة __init__ الموجودة في هذا الصنف والدالة نفسها الموجودة في صنف اللاعب Player وصنف العدو Enemy هو المتغير self.firing الذي يتتبع حالة الكائن المقذوف فيما إذا كان لا يزال فعالًا وظاهرًا على الشاشة أم انتهت صلاحيته، وتتغير قيمته تبعًا لذلك فهو يحمل القيمة 1 عندما ينشئ الكائن. قياس زمن فعالية الكائن المقذوف تمامًا مثل البطل والعدو يحتاج هذا الكائن إلى الدالة update لتحديث موقع ظهوره على الشاشة حتى يتحرك في الهواء منطلقًا باتجاه العدو بعد أن رماه البطل. أسهل طريقة لقياس زمن فعالية الكائن المقذوف هي وصوله إلى حافة الشاشة وخروجه عن مدى الرؤية، لكن عليك أن تحدد أي حافة ستراقب تبعًا لحركة هذا الكائن إن كانت أفقية أو عمودية. إذا كان البطل يقذفه أفقيًا مثل السهم أو الطاقة السحرية أو ماشابه فعليك مراقبة حافة الشاشة على المحور الأفقي وتحددها القيمة worldx. أما إذا كان يقذف الأشياء عموديًا أو بكلا الاتجاهين يتعين عليك مراقبة الحافة العمودية للشاشة باستخدام القيمة worldy. يفترض هذا المثال أن الجسم المقذوف يتحرك قليلًا للأمام ثم يسقط على الأرضية دون ارتداد فيتابع سقوطه إلى أسفل الشاشة، اطلع على التعليمات التالية وغيّر القيم الواردة فيها قليلًا لترى ما يناسب لعبتك أكثر: def update(self,worldy): ''' فيزياء المقذوفات ''' if self.rect.y < worldy: #المحور العمودي self.rect.x += 15 #سرعة حركته إلى الأمام self.rect.y += 5 #سرعة سقوطه إلى أسفل else: self.kill() #انتهاء فاعلية العنصر المقذوف self.firing = 0 #تحرير المتغير أو تصفيره ارفع قيم self.rect لتسريع حركة الكائن المقذوف. عندما يصل المقذوف إلى حافة الشاشة سيختفي أو بالأحرى يُدَمّر، وتتحرر الذاكرة المؤقتة التي أُشغلت بوجوده وتُضبط قيمة المتغير self.firing إلى الصفر ويتحرر تمهيدًا للرمية التالية. ضبط إعدادات الكائن المقذوف أنشئ في مقطع الإعدادات ضمن برنامج مجموعةً خاصة للكائنات التي يمكن أن يقذفها البطل، بصورةٍ مشابهة لمجموعات البطل والأعداء، وجهز كائنًا قابلًا للقذف غير فعال حتى يبدأ به البطل اللعبة، وإلّا فإن قذفته الأولى ستفشل. يفترض هذا المثال أن سلاح البطل هو الكرة النارية وتحدد مواصفاتها باستعمال المتغير fire ويمكنك في مستوياتٍ لاحقة تطوير السلاح وتغيير صورته ومواصفاته بالاعتماد على الصنف Throwable نفسه. اكتب التعليمات الآتية علمًا أن تعليمات أول سطرين لمعرفة السياق ولا حاجة لكتابتها مجددًا: player_list = pygame.sprite.Group() #للسياق player_list.add(player) #للسياق fire = Throwable(player.rect.x,player.rect.y,'fire.png',0) firepower = pygame.sprite.Group() لاحظ أن مكان إنتاج الكائن المقذوف مطابق لمكان إنتاج كائن البطل وهذا يعطي انطباعًا بأنه ينطلق من جعبة البطل، بعد إنتاج كرة النار الأولى يأخذ المتغير self.firing القيمة صفر ليتيح إمكانية القذف. إضافة القذف إلى الحلقة الرئيسية المبدأ نفسه على طول السلسلة اذكر ما تريد تنفيذه في الحلقة الرئيسية، أضف في البداية تعليمات التحكم التي ستُطلق الكرة النارية عندما يستخدم اللاعب الزر المحدد على لوحة المفاتيح. عند تحريك البطل استخدمنا الضغط على الزر لتبدأ حركة البطل وتحرير الزر أو رفع الضغط عنه ليتوقف البطل عن الحركة، لا يوجد توقف في عملية القذف فلن تحتاج أكثر من إشارة اختر واحدة منهما الضغط أو التحرير. اكتب التعليمات التالية وأول سطرين لتبيان السياق: if event.key == pygame.K_UP or event.key == ord('w'): player.jump(platform_list) if event.key == pygame.K_SPACE: if not fire.firing: fire = Throwable(player.rect.x,player.rect.y,'fire.png',1) firepower.add(fire) تُسند القيمة 1 للمتغير self.firing على عكس الكرة النارية الأولى غير الفعالة التي كُتبت سابقًا في مقطع الإعدادات. اكتب التعليمات الخاصة بتحديث الكائن المقذوف ورسمه على الشاشة، واحرص على كتابتها في الموقع الصحيح تمامًا كما هو مبين أدناه: enemy.move() # للسياق if fire.firing: fire.update(worldy) firepower.draw(world) player_list.draw(screen) # للسياق enemy_list.draw(screen) # للسياق لاحظ أن تعليمات التحديث تعمل فقط عندما يحمل المتغير self.firing القيمة 1، أما إذا كانت قيمته 0 فإن الشرط لن يتحقق ويتجاوز البرنامج هذه التعليمات دون أن ينفذها، أما لو كتبت هذه التعليمات دون شروط وحاولت تطبيقها بغض النظر عن قيمة المتغير سيفشل عمل اللعبة لأنها لن تجد كرةً نارية لترسمها أو تحدّث ظهورها على الشاشة. شغل اللعبة الآن وحاول استخدام السلاح. اكتشاف التصادم فور تجريب اللعبة ستلاحظ أن البطل يقذف سلاحه باتجاه العدو وقد يصيبه لكن دون أي تأثير، فالبرنامج لا يملك حتى الآن أي آلية تكتشف تصادم الجسم المقذوف بالعدو. الآلية المطلوبة مشابهة جدًا للمذكورة في صنف اللاعب Player، لذا أضف الدالة update التالية في صنف العدو Enemy: def update(self,firepower, enemy_list): """ اكتشاف التصادم مع الكرة النارية """ fire_hit_list = pygame.sprite.spritecollide(self,firepower,False) for fire in fire_hit_list: enemy_list.remove(self) التعليمات بسيطة فهي تتحقق من حدوث التصادم بين كائن العدو وكل كرة نارية firepower تنتمي لمجموعة كائنات الكرات النارية التي يقذفها البطل، وفي حال تبين حدوث التصادم يُزال العدو من مجموعة الأعداء ويختفي عن الشاشة. أضف السطر الأخير من التعليمات الآتية إلى كتلة تعليمات firing في الحلقة الرئيسية: if fire.firing: # للسياق fire.update(worldy) # للسياق firepower.draw(screen) # للسياق enemy_list.update(firepower,enemy_list) # تحديث العدو يتبقى لدينا أمر أخير للتعامل معه وهو اتجاه القذف. تغيير اتجاه القذف يقذف بطل اللعبة حاليًا كراته باتجاه اليمين فقط، فعندما ما عرفنا دالة التحديث في صنف القذف جعلناها تزيد عددًا من البكسلات لموقع الكرة وهي تتحرك على المحور X والزيادة على هذا المحور تعني التحرك يمينًا، فماذا لو استدار البطل وأراد القذف نحو اليسار؟ يتمثل الحل المتبع هنا في تعريف متغير جديد يحدد توجه البطل ومن ثم تحديد اتجاه القذف الأنسب وفقًا لقيمته. عرّف المتغير facing_right في صنف اللاعب وأعطه القيمة الابتدائية True إذ إن صورة اللاعب المستخدمة في هذا المثال تتجه افتراضيًا نحو اليمين: self.score = 0 self.facing_right = True # أضف هذا السطر self.is_jumping = True وبذلك تكون القيمة True مؤشرًا على اتجاه البطل إلى اليمين والقيمة False تشير لاتجاهه يسارًا، وينبغي التبديل بينهما في كل مرة يغير البطل اتجاه حركته، وأنسب مكان ذلك هو التعليمات الخاصة بالتحكم بحركة البطل عبر أزرار لوحة المفاتيح وفق ما يلي: if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) player.facing_right = False # أضف هذا السطر if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) player.facing_right = True # أضف هذا السطر عدّل أخيرًا الدالة update في صنف القذف Throwable لتجمع أو تطرح عددًا من وحدات البكسل إلى موقع الكرة النارية بناءً على اتجاه حركة البطل: if self.rect.y < worldy: if player.facing_right: self.rect.x += 15 else: self.rect.x -= 15 self.rect.y += 5 جرب اللعبة الآن وتفقد النتائج. حاول تطوير اللعبة وامنح البطل بعض النقاط مع كل إصابة موفقة للعدو. برنامج اللعبة كاملًا #!/usr/bin/env python3 # by Seth Kenlon # GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import pygame import pygame.freetype import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 ani = 4 world = pygame.display.set_mode([worldx, worldy]) forwardx = 600 backwardx = 120 BLUE = (80, 80, 155) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ALPHA = (0, 255, 0) tx = 64 ty = 64 font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fonts", "amazdoom.ttf") font_size = tx pygame.freetype.init() myfont = pygame.freetype.Font(font_path, font_size) ''' Objects ''' def stats(score, health): myfont.render_to(world, (4, 4), "Score:"+str(score), BLUE, None, size=64) myfont.render_to(world, (4, 72), "Health:"+str(health), BLUE, None, size=64) class Throwable(pygame.sprite.Sprite): """ Spawn a throwable object """ def __init__(self, x, y, img, throw): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.firing = throw def update(self, worldy): ''' throw physics ''' if self.rect.y < worldy: if player.facing_right: self.rect.x += 15 else: self.rect.x -= 15 self.rect.y += 5 else: self.kill() self.firing = 0 # x location, y location, img width, img height, img file class Platform(pygame.sprite.Sprite): def __init__(self, xloc, yloc, imgw, imgh, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)).convert() self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.y = yloc self.rect.x = xloc class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.movex = 0 self.movey = 0 self.frame = 0 self.health = 10 self.damage = 0 self.score = 0 self.facing_right = True self.is_jumping = True self.is_falling = True self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'walk' + str(i) + '.png')).convert() img.convert_alpha() img.set_colorkey(ALPHA) self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() def gravity(self): if self.is_jumping: self.movey += 3.2 def control(self, x, y): """ control player movement """ self.movex += x def jump(self): if self.is_jumping is False: self.is_falling = False self.is_jumping = True def update(self): """ Update sprite position """ # moving left if self.movex < 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) # moving right if self.movex > 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = self.images[self.frame // ani] # collisions enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False) if self.damage == 0: for enemy in enemy_hit_list: if not self.rect.contains(enemy): self.damage = self.rect.colliderect(enemy) if self.damage == 1: idx = self.rect.collidelist(enemy_hit_list) if idx == -1: self.damage = 0 # set damage back to 0 self.health -= 1 # subtract 1 hp ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False) for g in ground_hit_list: self.movey = 0 self.rect.bottom = g.rect.top self.is_jumping = False # stop jumping # fall off the world if self.rect.y > worldy: self.health -=1 print(self.health) self.rect.x = tx self.rect.y = ty plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) for p in plat_hit_list: self.is_jumping = False # stop jumping self.movey = 0 if self.rect.bottom <= p.rect.bottom: self.rect.bottom = p.rect.top else: self.movey += 3.2 if self.is_jumping and self.is_falling is False: self.is_falling = True self.movey -= 33 # how high to jump loot_hit_list = pygame.sprite.spritecollide(self, loot_list, False) for loot in loot_hit_list: loot_list.remove(loot) self.score += 1 print(self.score) plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) self.rect.x += self.movex self.rect.y += self.movey class Enemy(pygame.sprite.Sprite): """ Spawn an enemy """ def __init__(self, x, y, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.counter = 0 def move(self): """ enemy movement """ distance = 80 speed = 8 if self.counter >= 0 and self.counter <= distance: self.rect.x += speed elif self.counter >= distance and self.counter <= distance * 2: self.rect.x -= speed else: self.counter = 0 self.counter += 1 def update(self, firepower, enemy_list): """ detect firepower collision """ fire_hit_list = pygame.sprite.spritecollide(self, firepower, False) for fire in fire_hit_list: enemy_list.remove(self) class Level: def ground(lvl, gloc, tx, ty): ground_list = pygame.sprite.Group() i = 0 if lvl == 1: while i < len(gloc): ground = Platform(gloc[i], worldy - ty, tx, ty, 'tile-ground.png') ground_list.add(ground) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return ground_list def bad(lvl, eloc): if lvl == 1: enemy = Enemy(eloc[0], eloc[1], 'enemy.png') enemy_list = pygame.sprite.Group() enemy_list.add(enemy) if lvl == 2: print("Level " + str(lvl)) return enemy_list # x location, y location, img width, img height, img file def platform(lvl, tx, ty): plat_list = pygame.sprite.Group() ploc = [] i = 0 if lvl == 1: ploc.append((200, worldy - ty - 128, 3)) ploc.append((300, worldy - ty - 256, 3)) ploc.append((550, worldy - ty - 128, 4)) while i < len(ploc): j = 0 while j <= ploc[i][2]: plat = Platform((ploc[i][0] + (j * tx)), ploc[i][1], tx, ty, 'tile.png') plat_list.add(plat) j = j + 1 print('run' + str(i) + str(ploc[i])) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return plat_list def loot(lvl): if lvl == 1: loot_list = pygame.sprite.Group() loot = Platform(tx*5, ty*5, tx, ty, 'loot_1.png') loot_list.add(loot) if lvl == 2: print(lvl) return loot_list ''' Setup ''' backdrop = pygame.image.load(os.path.join('images', 'stage.png')) clock = pygame.time.Clock() pygame.init() backdropbox = world.get_rect() main = True player = Player() # spawn player player.rect.x = 0 # go to x player.rect.y = 30 # go to y player_list = pygame.sprite.Group() player_list.add(player) steps = 10 fire = Throwable(player.rect.x, player.rect.y, 'fire.png', 0) firepower = pygame.sprite.Group() eloc = [] eloc = [300, worldy-ty-80] enemy_list = Level.bad(1, eloc) gloc = [] i = 0 while i <= (worldx / tx) + tx: gloc.append(i * tx) i = i + 1 ground_list = Level.ground(1, gloc, tx, ty) plat_list = Level.platform(1, tx, ty) enemy_list = Level.bad( 1, eloc ) loot_list = Level.loot(1) ''' Main Loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(-steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(steps, 0) if event.key == pygame.K_UP or event.key == ord('w'): player.jump() if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) player.facing_right = False if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) player.facing_right = True if event.key == pygame.K_SPACE: if not fire.firing: fire = Throwable(player.rect.x, player.rect.y, 'fire.png', 1) firepower.add(fire) # scroll the world forward if player.rect.x >= forwardx: scroll = player.rect.x - forwardx player.rect.x = forwardx for p in plat_list: p.rect.x -= scroll for e in enemy_list: e.rect.x -= scroll for l in loot_list: l.rect.x -= scroll # scroll the world backward if player.rect.x <= backwardx: scroll = backwardx - player.rect.x player.rect.x = backwardx for p in plat_list: p.rect.x += scroll for e in enemy_list: e.rect.x += scroll for l in loot_list: l.rect.x += scroll world.blit(backdrop, backdropbox) player.update() player.gravity() player_list.draw(world) if fire.firing: fire.update(worldy) firepower.draw(world) enemy_list.draw(world) enemy_list.update(firepower, enemy_list) loot_list.draw(world) ground_list.draw(world) plat_list.draw(world) for e in enemy_list: e.move() stats(player.score, player.health) pygame.display.flip() clock.tick(fps) ترجمة -وبتصرف- للمقال Add throwing mechanics to your Python game لصاحبيه Seth Kenlon و Jess Weichler. اقرأ أيضًا المقال السابق: تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة النسخة العربية الكاملة من كتاب البرمجة بلغة بايثون
  10. عرض النتائج على الشاشة جزء مهم وحيوي من أي لعبة وهو موضوع المقال ما قبل الأخير من سلسلة بناء لعبة من الصفر باستخدام بايثون، سنعرض فيه نقاط اللاعب وصحته بناءً على ما اغتنمه من جوائز وما أصابه من ضرر جراء التصادم مع الأعداء. يمكنك مطالعة المقالات ضمن السلسلة: بناء لعبة نرد بسيطة بلغة بايثون. بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame. إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame. تحريك شخصية اللعبة باستخدام PyGame. إضافة شخصية العدو للعبة. إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame. محاكاة أثر الجاذبية في لعبة بايثون. إضافة خاصية القفز والركض إلى لعبة بايثون. إضافة الجوائز إلى اللعبة المطورة بلغة بايثون. تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة. إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون. اكتسبت على مدار السلسلة كل الأساسيات اللازمة لبناء لعبة فيديو بلغة بايثون وستضيف لها اليوم مهارة جديدة جوهرية لكل مبرمج أيًّا كان اختصاصه، فإضافةً لعرض النتائج على الشاشة بتنسيق وخط مناسبين ستتعلم قراءة توثيقات المكتبة أو اللغة البرمجية ومعرفة إمكاناتها وكيفية استخدام ميزاتها التي لم تتعامل معها قبلًا وهو فعليًا الهدف الحقيقي المقال. عرض النتائج في Pygame يتضمن برنامج اللعبة كل المتغيرات اللازمة لتتبع نقاط اللاعب التي كسبها من الجوائز وأيضًا صحته أو أدواره المتأثرة بالتصادم مع الأعداء، إلّا أن هذه المتغيرات تعمل في الخلفية ولا تُظهر أي نتائج على شاشة اللعبة لذا ينصب اهتمامنا هنا على طريقة العرض واختيار الخط المناسب لها. قراءة التوثيقات معظم وحدات بايثون ومن بينها Pygame لها توثيقات رسمية تشرحها وحتى الجزء البسيط من الوحدات الذي لا تشمله التوثيقات تشرحه Help دالة المساعدة من بايثون، والتوثيقات عمومًا هي توصيف لكافة دوال الوحدة وأصنافها وتكتب بلغة فنية دقيقة تحدد مثلًا أنواع المدخلات المطلوبة وما شابه، وقد تكون مربكة للبعض غير المعتاد عليها فهي لا تشبه السرد البسيط المتبع في المقالات التعليمية. بخصوص Pygame الوحدة التي نستخدمها تستطيع الوصول لتوثيقاتها الغنية عبر رابط في صفحتها الرسمية، ولكن قبل البحث في أي توثيق فكر أولًا بما تريد الحصول عليه، فهنا مثلًا نسعى لإظهار نقاط اللاعب وصحته على شاشة اللعبة، ومن بعدها حاول تخمين العناصر البرمجية المطلوبة لتحقيقه مثل المتغيرات والدوال وغيرها، وإن لم تتمكن من وصف احتياجك بهذه الدقة، صِفهُ بالعموم وببساطة، فهنا مثلًا تحتاج لنصوص ترسمها Pygame على الشاشة لتظهر النتائج، وربما شعرت بقدرتك على إظهارها بطريقة مشابهة لعرض المنصات والكائنات على شاشة اللعبة. تقنيًا لا يوجد ما يمنعك من فعل ذلك، واستخدام صور الأرقام بكل بساطة وعرضها على الشاشة لتمثل نتائج اللعبة عبر Pygame، قد لا تكون هذه الطريقة المثلى لتصل لهدفك لكنها صحيحة في نهاية المطاف وقد أوصلك إليها التفكير العام بأساليب عرض النتائج، بكل الأحوال لو تصفحت توثيقات Pygame ستلفت انتباهك على الفور وحدة الخط وتدعى Font وهي مختصة بتسهيل كتابة النصوص على الشاشة وتنسيق خطوطها. فهم رموز التوثيق الفني يبدأ توثيق وحدة الخط بالدالة ‎pygame.font.init‎(‎)‎ ويصفها التوثيق بأنها دالة التهيئة للوحدة، تُستدعى تلقائيًا بواسطة ()pygame.init المستدعاة أساسًا في برنامج اللعبة، واستخدامها شبيه بما تعلمته خلال السلسلة، إذ ستتمكن من استخدام دوالها وكتابة النتيجة على الشاشة بقراءة بسيطة للتوثيق ومراجعة لما تعلمته. ولو استمريت باستعراض الوحدات في توثيقات Pygame ستصادف الوحدة Pygame.freetype ومن الاسم يتضح ارتباطها بحالتنا وهي أفضل من وحدة الخط إذ يصفها التوثيق بأنها: وفي صفحة التوثيق نفسها المخصصة للوحدة ستجد بعض الأمثلة على طريقة استخدامها ومنها: import pygame import pygame.freetype السطر موجود لدينا في برنامج اللعبة وما نحتاجه هو السطر الثاني، أضفه إذًا لمجموعة تعليمات import لتصبح على الشكل التالي: import pygame import sys import os import pygame.freetype استخدام الخطوط في Pygame استنادًا للتوثيق تعرض الوحدتان النصوص على الشاشة باستعمال الخطوط الإفتراضية في Pygame أو أي خطوط أخرى تزودها بها، وستجد ضمن توثيق الوحدة pygame.freetype التوصيف الفني للدالة التالية: pygame.freetype.Font # إنشاء خط جديد انطلاقًا من أحد ملفات الخطوط المدعومة Font(file, size=0, font_index=0, resolution=0, ucs4=False) -> Font pygame.freetype.Font.name # اسم ملف الخط pygame.freetype.Font.path # مسار ملف الخط pygame.freetype.Font.size # حجم الخط الافتراضي المستخدم في العرض وهي مسؤولة عن إنشاء كائن Object الخط في Pygame ومواصفاته ولو دققت فيها ستجدها مشابهة لتعليمات إنشاء كائنات اللعبة مثل الأبطال والأعداء فبدلًا من صورة الشخصية ستحدد هنا ملف الخط وبمجرد توفره يمكنك إنشاء كائن الخط باستخدام الدالة السابقة pygame.freetype.Font ومن ثم استدعائه للعرض على الشاشة. إدارة الخط بصفته أحد أصول اللعبة لتضمن عمل لعبتك بصورةٍ جيدة عليك تضمن الخط الذي تعتمده ضمن ملفات اللعبة فمجموعات الخطوط تختلف من حاسبٍ لآخر، لذا أنشئ مجلدًا خاصًا بالخطوط في المجلد الأب للعبتك تمامًا مثل مجلد الصور وضع فيه ملفات الخطوط التي تستخدمها. واحرص على عدم انتهاك حقوق الملكية الفكرية، فاستعمل خطوطًا مفتوحة المصدر أو تتمتع برخصة المشاع الإبداعي، فحتى استعمال أحد ملفات الخطوط المتوفرة على حاسبك الشخصي وجعله خطًا للعبتك لا يعد تصرفًا قانونيًا. وفيما يلي قائمة ببعض المواقع التي توفر خطوطًا مجانية واستخدامها قانوني يمكنك الاستفادة منها: Font Library. Font Squirrel. League of Moveable Type. حمّل الخط الذي أعجبك من هذه المواقع على شكل ملف مضغوط zip أو tar وفك ضغطه لاستخراج ملف الخط، ويكون عادةً بصيغة ttf. أو otf. ومن ثم ضعه في مجلد الخطوط الخاصة باللعبة. لا يوجد أي حاجة لتثبيت الخط في نظام التشغيل لديك بل يكفي وجوده في مجلد اللعبة لتستخدمه Pygame، وفي حال تضمن اسم الملف رموزًا أو فراغات أزلها وعدّل الاسم، ويفضل لو تختر له اسمًا بسيطًا ومختصرًا لتكتبه بسهولة في التعليمات البرمجية. تعليمات الخط في Pygame بناءً على توثيق الدالة pygame.freetype.Font يمكنك إنشاء كائن الخط عبر تحديد مسار ملف الخط المرغوب على الأقل المدخلات الأخرى فهي اختيارية وليست ملزمة. Font(file, size=0, font_index=0, resolution=0, ucs4=False) -> Font أنشئ متغيرًا جديدًا اسمه myfont وأسند له خرج الدالة Font المذكورة وفق التعليمات الآتية وملف الخط الذي اعتمدنا عليه هنا هو amazdoom.ttf على سبيل المثال. font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"fonts","amazdoom.ttf") font_size = tx pygame.freetype.init() myfont = pygame.freetype.Font(font_path, font_size) عرض النص على الشاشة في Pygame بعد إنشاء الخط عرف الدالة stats التالية في مقطع الكائنات ضمن برنامج اللعبة، مهمتها رسم النصوص المطلوبة على الشاشة وهي تتطلب ذكر النصوص التي تريد عرضها وتحديد لون معين لها من باستخدام النظام اللوني RGB بالطريقة نفسها التي اتبعناها عند إظهار خلفية عالم اللعبة ومنصاتها، مع ضرورة التنويه إلى أن هذه الدالة عامة ومستقلة ولا تتبع لصنف معين من أصناف برنامج اللعبة. def stats(score,health): myfont.render_to(world, (4, 4), "Score:"+str(score), BLACK, None, size=64) myfont.render_to(world, (4, 72), "Health:"+str(health), BLACK, None, size=64) والآن استدعيها في حلقة التكرار الرئيسية: stats(player.score,player.health) # draw text إذا شغلت اللعبة الآن ستحصل على خطأ فهي ما زالت بحاجة لبعض التعديلات. تفسير الأخطاء تفسير الأخطاء ومحاولة فهمها عملية مهمة جدًا للمبرمجين وتفيدهم في اصلاح مشاكل البرامج، والأخطاء في لغة بايثون دلالية وسهلة الفهم نوعًا ما لكنها مع ذلك تحتاج لتفسير لتُحلل ما يحدث. عند تشغيل اللعبة في هذه المرحلة ستحصل على هذا الخطأ: Traceback (most recent call last): File "/home/tux/PycharmProjects/game_001/main.py", line 41, in <module> font_size = tx NameError: name 'tx' is not defined يدّل الخطأ صراحةً على عدم تعريف المتغير tx رغم أن المتغير معرف وقد استخدمناه مررًا ضمن البرنامج، ويشير أيضًا إلى السطر رقم 41 لا تعني هذه الإشارة بالضرورة أن الخطأ موجود في السطر 41 بل تعني أن التنفيذ توقف عنده. لو رجعت إلى برنامج اللعبة وتفقدت تعريف المتغير tx ستجده معرفًا مع المغير ty في مقطع الإعدادات ضمن البرنامج وهو يأتي بعد السطر 41 في تسلسل الأسطر. اتضحت الصورة الآن علينا نقل تعريف المتغير إلى موضعٍ آخر ضمن البرنامج ترتيبه يسبق السطر 41 وبذلك يمرّ عليه بايثون قبل الوصول لهذا السطر ويُحل الخطأ. جميع البرامج معرضة للأخطاء وستواجه هذه الحالات دائمًا، قد تكون صعبة أو سهلة دوّن المعلومات التي يظهرها الخطأ وراجع البرنامج في ضوئها بعنايةً وستصل للحل حتى أمهر المبرمجين يواجهون هذه الأخطاء وكلما زاد إتقانك للغة بايثون زادت قدرتك على تفسير الأخطاء وحلها. تشغيل اللعبة شغل اللعبة وتفقد نجاح العملية. يزداد رصيد اللاعب من النقاط مع كل جائزة يغتنمها، وتنخفض مؤشرات سلامته أو صحته كلما اصطدم بعدو. لكنك ستكتشف مع تقدم اللعبة مشكلةً بسيطة أخرى، فنقاط صحة اللاعب تنخفض كثيرًا بطريقةٍ غير عادلة مع كل تصادم بالعدو، وسنعالجها في الفقرة القادمة. لا يعطل هذا النوع من الأخطاء عمل البرنامج فهو ليس من الأخطاء "القاتلة" إنما يعد من المشكلات البسيطة التي تعطي نتائج غير منطقية أو تزعج المستخدم. إصلاح عداد صحة البطل المشكلة في عداد صحة اللاعب أن نقاطه تتناقص مع كل نبضة ساعة يكون فيها العدو والبطل في حالة تصادم، وهذا غير عادلة طبعًا بالعدو بطيء الحركة سينقص صحة اللاعب بمقدار 200- نقطة أو ربما أكثر، يمكنك بكل بساطة زيادة رصيد الصحة الابتدائي للاعبك إلى رقم كبير نسبيًا مثل 10000 سيخفف هذا من أثر المشكلة لكنه لا يعد حلًا مثاليًا. فالحل الأفضل يكتشف حادثتين عوضًا عن واحدة، فلن يكتشف التصادم فقط إنما أيضًا انتهاء التصادم. عرّف المتغير الموجود في السطر الأخير ضمن صنف اللاعب ليمثل حالة التصادم بين البطل والعدو: self.frame = 0 self.health = 10 self.damage = 0 والآن استبدل كتلة التعليمات التالية في دالة التحديث update: for enemy in enemy_hit_list: self.health -= 1 #print(self.health) بهذه الكتلة: if self.damage == 0: for enemy in enemy_hit_list: if not self.rect.contains(enemy): self.damage = self.rect.colliderect(enemy) يوجد بعض التشابه بين كتلة االتعليمات المحذوفة والكتلة البديلة فكلاهما يؤديان نفس الغرض إلّا أن الجديدة أعقد فهي تعمل لمرة واحدة فقط عند انتهاء التصادم ولا تعمل باستمرار وتنقص النقاط باستمرار مثل التعليمات القديمة، وتسنخدم دالتين من Pygame نتعرف عليهما للمرة الأولى في هذه السلسلة. الدالة الأولى هي self.rect.contains مهمتها معرفة فيما إذا كان العدو متواجدًا داخل المربع المحيط باللاعب في لحظة التنفيذ. والدالة الثانية self.rect.colliderect تعطي المتغير self.damage القيمة 1 إذا كانت نتيجة الدالة الأولى محققة أي أن العدو داخل المربع المحيط بالبطل بغض النظر عن عدد مرات تحققها. وبدءًا من الآن سيُعدّ التصادم بين العدو والبطل لمدة ثلاث ثواني حادث تصادم واحد فقط من وجهة نظر Pygame ويخصم عنه مرة واحدة فقط. يمكننا دومًا اكتشاف هذه الدوال بقراءة توثيقات Pygame وقد وفرت الكثير من العناء والبرمجة، ننصحك دائمًا بقراءة التوثيقات ولا نعني بذلك قراءة كل توثيقات المكتبة أو اللغة وقراءة كل كلمة فيها لكن اهتم بقراءة توثيقات الوحدات الجديدة التي تستخدمها، اطلع على مزاياها سيوفر ذلك عليك إعادة اختراع العجلة كما يقال ويغنيك عن المحاولات المضنية لاكتشاف الحلول لمشاكل محلولة أساسًا. اكتب أخيرًا التعليمات التالية لتكتشف انتهاء حالة التصادم أو التلامس بين اللاعب والعدو وعندها فقط يطرح البرنامج نقطة واحدة من رصيد صحة اللاعب: if self.damage == 1: idx = self.rect.collidelist(enemy_hit_list) if idx == -1: self.damage = 0 # تصفير قيمة المتغير self.health -= 1 # إنقاص نقطة من صحة اللاعب لا تعمل التعليمات السابقة ما لم يحدث التصادم مع العدو فهي لا تتأثر بركض اللاعب في عالم اللعبة أو جمعه للجوائز والكنوز شرطها الوحيد هو تنشيط المتغير self.damage. وعندما تعمل فإنها تستخدم Self.rect.collidelist لتبيان إن كان تصادم البطل مع عدو ينتمي لمجموعة الأعداء ما زال مستمرًا أم أنه انتهى (ويعطي عنها collidelist القيمة 1-) وبمجرد انتهاء التصادم تعاد قيمة المتغير self.damage وتخصم نقطة من صحة اللاعب. شغل اللعبة الآن. يفتح لك إظهار النتائج خياراتٍ جديدة في لعبتك، يمكنك مثلًا إضافة نوع معين من الجوائز يعوض اللاعب بعضًا من نقاط صحته أو ربما تريد للاعبك أن يعود إلى بداية المستوى عندما يصل رصيد صحته إلى الصفر. كل ما عليك هو تجربة المبادئ التي تعلمتها لتكتشف هذه الأحداث المتنوعة وتطور عالم اللعبة وفق ما تريد. طور مهاراتك طوّر مهارتك في بايثون واستفد من التوثيقات، تعرف على مكاتب ووحداتٍ جديدة وابتكر ألعابًا أو تطبيقات جديدة فهذه اللغة متعددة الأغراض والبرمجة تحتاج للمثابرة، واجعل برامجك مفتوحة المصدر ليساهم معك مطورو بايثون من مختلف أنحاء العالم. سنضيف في المقال التالي بعض الآليات القتالية للعبة، وننهي هذا المقال بتذكيرك ببرنامج اللعبة كامًلا حتى الآن: #!/usr/bin/env python3 # by Seth Kenlon # GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import pygame import pygame.freetype import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 ani = 4 world = pygame.display.set_mode([worldx, worldy]) forwardx = 600 backwardx = 120 BLUE = (80, 80, 155) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ALPHA = (0, 255, 0) tx = 64 ty = 64 font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fonts", "amazdoom.ttf") font_size = tx pygame.freetype.init() myfont = pygame.freetype.Font(font_path, font_size) ''' Objects ''' def stats(score,health): myfont.render_to(world, (4, 4), "Score:"+str(score), BLUE, None, size=64) myfont.render_to(world, (4, 72), "Health:"+str(health), BLUE, None, size=64) # x location, y location, img width, img height, img file class Platform(pygame.sprite.Sprite): def __init__(self, xloc, yloc, imgw, imgh, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)).convert() self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.y = yloc self.rect.x = xloc class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.movex = 0 self.movey = 0 self.frame = 0 self.health = 10 self.damage = 0 self.score = 0 self.is_jumping = True self.is_falling = True self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert() img.convert_alpha() img.set_colorkey(ALPHA) self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() def gravity(self): if self.is_jumping: self.movey += 3.2 def control(self, x, y): """ control player movement """ self.movex += x def jump(self): if self.is_jumping is False: self.is_falling = False self.is_jumping = True def update(self): """ Update sprite position """ # moving left if self.movex < 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) # moving right if self.movex > 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = self.images[self.frame // ani] # collisions enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False) if self.damage == 0: for enemy in enemy_hit_list: if not self.rect.contains(enemy): self.damage = self.rect.colliderect(enemy) if self.damage == 1: idx = self.rect.collidelist(enemy_hit_list) if idx == -1: self.damage = 0 # set damage back to 0 self.health -= 1 # subtract 1 hp ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False) for g in ground_hit_list: self.movey = 0 self.rect.bottom = g.rect.top self.is_jumping = False # stop jumping # fall off the world if self.rect.y > worldy: self.health -=1 print(self.health) self.rect.x = tx self.rect.y = ty plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) for p in plat_hit_list: self.is_jumping = False # stop jumping self.movey = 0 if self.rect.bottom <= p.rect.bottom: self.rect.bottom = p.rect.top else: self.movey += 3.2 if self.is_jumping and self.is_falling is False: self.is_falling = True self.movey -= 33 # how high to jump loot_hit_list = pygame.sprite.spritecollide(self, loot_list, False) for loot in loot_hit_list: loot_list.remove(loot) self.score += 1 print(self.score) plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) self.rect.x += self.movex self.rect.y += self.movey class Enemy(pygame.sprite.Sprite): """ Spawn an enemy """ def __init__(self, x, y, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.counter = 0 def move(self): """ enemy movement """ distance = 80 speed = 8 if self.counter >= 0 and self.counter <= distance: self.rect.x += speed elif self.counter >= distance and self.counter <= distance * 2: self.rect.x -= speed else: self.counter = 0 self.counter += 1 class Level: def ground(lvl, gloc, tx, ty): ground_list = pygame.sprite.Group() i = 0 if lvl == 1: while i < len(gloc): ground = Platform(gloc[i], worldy - ty, tx, ty, 'tile-ground.png') ground_list.add(ground) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return ground_list def bad(lvl, eloc): if lvl == 1: enemy = Enemy(eloc[0], eloc[1], 'enemy.png') enemy_list = pygame.sprite.Group() enemy_list.add(enemy) if lvl == 2: print("Level " + str(lvl)) return enemy_list # x location, y location, img width, img height, img file def platform(lvl, tx, ty): plat_list = pygame.sprite.Group() ploc = [] i = 0 if lvl == 1: ploc.append((200, worldy - ty - 128, 3)) ploc.append((300, worldy - ty - 256, 3)) ploc.append((550, worldy - ty - 128, 4)) while i < len(ploc): j = 0 while j <= ploc[i][2]: plat = Platform((ploc[i][0] + (j * tx)), ploc[i][1], tx, ty, 'tile.png') plat_list.add(plat) j = j + 1 print('run' + str(i) + str(ploc[i])) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return plat_list def loot(lvl): if lvl == 1: loot_list = pygame.sprite.Group() loot = Platform(tx*5, ty*5, tx, ty, 'loot_1.png') loot_list.add(loot) if lvl == 2: print(lvl) return loot_list ''' Setup ''' backdrop = pygame.image.load(os.path.join('images', 'stage.png')) clock = pygame.time.Clock() pygame.init() backdropbox = world.get_rect() main = True player = Player() # spawn player player.rect.x = 0 # go to x player.rect.y = 30 # go to y player_list = pygame.sprite.Group() player_list.add(player) steps = 10 eloc = [] eloc = [300, worldy-ty-80] enemy_list = Level.bad(1, eloc) gloc = [] i = 0 while i <= (worldx / tx) + tx: gloc.append(i * tx) i = i + 1 ground_list = Level.ground(1, gloc, tx, ty) plat_list = Level.platform(1, tx, ty) enemy_list = Level.bad( 1, eloc ) loot_list = Level.loot(1) ''' Main Loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(-steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(steps, 0) if event.key == pygame.K_UP or event.key == ord('w'): player.jump() if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) # scroll the world forward if player.rect.x >= forwardx: scroll = player.rect.x - forwardx player.rect.x = forwardx for p in plat_list: p.rect.x -= scroll for e in enemy_list: e.rect.x -= scroll for l in loot_list: l.rect.x -= scroll # scroll the world backward if player.rect.x <= backwardx: scroll = backwardx - player.rect.x player.rect.x = backwardx for p in plat_list: p.rect.x += scroll for e in enemy_list: e.rect.x += scroll for l in loot_list: l.rect.x += scroll world.blit(backdrop, backdropbox) player.update() player.gravity() player_list.draw(world) enemy_list.draw(world) loot_list.draw(world) ground_list.draw(world) plat_list.draw(world) for e in enemy_list: e.move() stats(player.score, player.health) pygame.display.flip() clock.tick(fps) ترجمة -وبتصرف- للمقال Add scorekeeping to your Python game لصاحبيه Seth Kenlon و Jess Weichler. اقرأ أيضًا المقال السابق: إضافة الجوائز إلى اللعبة المطورة بلغة بايثون النسخة العربية الكاملة من كتاب البرمجة بلغة بايثون
  11. سيُمنح اللاعب بعضًا من الجوائز ليجمعها ويعزز نقاطه في هذا الإصدار من اللعبة المطورة باستخدام بايثون 3 والوحدة Pygame المتخصصة بألعاب الفيديو، والبداية مع روابط مقالات هذه السلسلة. بناء لعبة نرد بسيطة بلغة بايثون. بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame. إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame. تحريك شخصية اللعبة باستخدام PyGame. إضافة شخصية العدو للعبة. إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame. محاكاة أثر الجاذبية في لعبة بايثون. إضافة خاصية القفز والركض إلى لعبة بايثون. إضافة الجوائز إلى اللعبة المطورة بلغة بايثون. تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة. إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون. عرضت هذه السلسلة المبادئ الأساسية لتطوير ألعاب الفيديو باستخدام بايثون، ولابدّ أنها منحتك أساسًا جيدًا تنطلق منه لبناء لعبتك الخاصة، لا تتقيد حرفيًا بأسلوب السلسلة، بل استخدم المبادئ نفسها بطرقٍ جديدة، عدّلها لتبرمج عناصرك الخاصة وشروط اللعب التي تريدها. مقال اليوم هو خيرُ مثالٍ على ذلك، إذ ستلاحظ التشابه الكبير في المبادئ بين إضافة المنصات التي تعلمتها سابقًا وإضافة الجوائز، فكلاهما من كائنات اللعبة التي لا تخضع لتحكم خارجي من قبل المستخدم، إنما تُمرر مع عالم اللعبة خلال مستوياتها المتتابعة، وفي الحالتين تحتاج إلى طريقةٍ برمجية لاكتشاف التصادم بين كائن البطل وهذه الكائنات واحتساب تأثيره على نقاط اللاعب وصحته سواءً سلبًا أو إيجابًا. وقبل أن تبدأ جهّز صورًا تمثل الكنوز أو الجوائز التي يجمعها اللاعب، اختر صورًا لعملات معدنية أو جواهر أو ما شابه، يمكنك تحميل حزمة الصور نفسها من موقع kenney.nl التي أخذنا منها صور البلاطات المربعة لبناء منصات اللعبة فهي تتضمن صور جواهر ومفاتيح وكلاهما يفي بالغرض. بناء دالة الجوائز إضافة الجوائز والكنوز إلى لعبة مشابه جدًا لإضافة المنصات كما ذكرنا في المقدمة، في الواقع هو مشابه لدرجة تجعلنا نستدعي صنف المنصة نفسه Platform لإنتاجها. ابدأ بإنشاء دالة جديدة تحمل الاسم loot في صنف المستوى level إذ من المحتمل جدًا أن يتبدل نوع الجوائز بين مستوى وآخر، وتمامًا مثلما فعلنا مع المنصات العائمة والأرضية والأعداء أنشئ في هذه الدالة مجموعة خاصة بكائنات الجوائز سميها loot_list وأضف إليها الجوائز، ستُفيدك هذه المجموعة في عملية اكتشاف التصادمات. def loot(lvl): if lvl == 1: loot_list = pygame.sprite.Group() loot = Platform(tx*9, ty*5, tx, ty, 'loot_1.png') loot_list.add(loot) if lvl == 2: print(lvl) return loot_list استخدم هذا المثال أبعاد قطع البلاط ty و tx لتحديد إحداثيات ظهور الجوائز من خلال مضاعفاتها للسهولة فقط، ستفيدك هذه الطريقة إن خططت للمستوى على أوراق الرسم البياني مع تمثيل كل بلاطة بمربع أو كان المستوى الذي صممته طويلًا، لكن في النهاية يعود الخيار لك إذ يمكنك عوضًا ذلك كتابة إحداثيات كل جائزة صراحةً برقم مقدر بالبكسل فتصبح هذه الأرقام مدمجة مع برنامج اللعبة hard-coded. لا تنسَ أن باستطاعتك إظهار القدر الذي تريده من الجوائز المتنوعة لكن بشرط أن تضيفها جميعًا لمجموعة الجوائز. أما بالنسبة لوسطاء صنف المنصة Platform في هذه الحالة فهم إحداثيات موقع ظهور الجائزة X و Y بالإضافة إلى أبعاد صورة الجائزة، وننصحك بالتخطيط لأماكن توضع الجوائز منذ البداية وتضمينها في مخطط تصميم المستوى واختيار صور مربعة لتمثيلها تعادل أبعادها أبعاد بلاطات الأرضية والمنصات فهذا سيوفر عليك الكثير. استدعِ الآن دالة الجوائز في مقطع الإعدادات ضمن البرنامج وفق التالي: loot_list = Level.loot(1) وأخيرًا اذكرها ضمن الحلقة الرئيسية لتظهر على الشاشة: loot_list.draw(world) شغل اللعبة ولاحظ النتائج. ظهرت صورة الجائزة بعد تشغيل اللعبة، إلّا أنها لم تُحدث أي تأثير على اللاعب عندما اصطدم بها ولم تُمرر مع تحرك اللاعب وتمرير عالم اللعبة، وهذا هو موضوع فقراتنا اللاحقة. تمرير الجوائز مع عالم اللعبة تمامًا مثل المنصات ينبغي أن تُمرر الجوائز مع عالم اللعبة إلى الأمام والخلف تبعًا لحركة البطل. اكتب السطرين الأخيرين من التعليمات التالية لتُمرر الجوائز إلى الأمام: for e in enemy_list: e.rect.x -= scroll for l in loot_list: # تمرير الجوائز l.rect.x -= scroll # تمرير الجوائز واكتب السطرين الأخيرين من التالي لتُمررها للخلف: for e in enemy_list: e.rect.x += scroll for l in loot_list: # تمرير الجوائز l.rect.x += scroll # تمرير الجوائز تفقد اللعبة الآن واختبر التمرير بالاتجاهين، ولاحظ أن الجوائز أصبحت تتصرف وكأنها عنصر أصيل من عناصر عالم اللعبة وليست مجرد صورة. اكتشاف التصادم استخدم ما تعلمته سابقًا عند اكتشاف تصادم البطل مع الأرضية والمنصات العائمة والأعداء لتكتشف لتصادمه مع الجوائز، الطريقة نفسها تمامًا إلّا أن النتائج المترتبة على التصادم ستكون مختلفة فهي لن تتعلق بالجاذبية ولن تُنقص من صحة اللاعب وأدواره بل ستؤدي إلى زيادة نقاطه واختفاء الجائزة التي اصطدم بها من عالم اللعبة، وذلك عبر إزالتها من مجموعة كائنات الجوائز loot_list ما يعني أنها لن تظهر مجددًا في المرة القادمة التي تُعيد فيها الحلقة الرئيسية رسم عالم اللعبة، وهذا منطقي فالبطل اغتنمها وحاز على بعض النقاط بسببها. اكتب التعليمات التالية في الدالة update ضمن صنف اللاعب Player تمامًا فوق التعليمات المسؤولة عن اكتشاف التصادم مع المنصات، وانتبه للسطر الأخير فهو لتبيان السياق فقط وما من داعٍ لتعيد كتابته: loot_hit_list = pygame.sprite.spritecollide(self, loot_list, False) for loot in loot_hit_list: loot_list.remove(loot) self.score += 1 print(self.score) plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) عرّف الآن المتغير الذي استخدمه لزيادة نقاط اللاعب بعد الاصطدام بالجائزة في الدالة __init__ الموجودة في صنف اللاعب وهي المسؤولة عن تحديد خصائصه المختلفة، وذلك بكتابة السطر الثالث من التعليمات أدناه: self.frame = 0 self.health = 10 self.score = 0 طبق المهارات التي تعلمتها في السابق طبق المهارات التي تعلمتها خلال هذه السلسلة وأضف الأعداء في أماكن مناسبة، خطط لذلك ومن ثم عدّل برنامج اللعبة، حاول أيضًا الاستفادة من المبادئ الأساسية، ووظفها بطرقٍ أخرى، وصمم ألعابًا متنوعة وإن كانت وحيدة المستوى في المرحلة كبداية، ولا تنسَ التخطيط الجيد للعبة قبل البرمجة، فكر بأوسع نطاق تريده وبرمج على هذا الأساس، يساعدك ذلك في الحصول على إصداراتٍ جيدة لا تتطلب كثيرًا من التعديلات. وفي ختام المقال نذكرك ببرنامج اللعبة كاملًا من بداية السلسلة. #!/usr/bin/env python3 # by Seth Kenlon # GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import pygame import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 ani = 4 world = pygame.display.set_mode([worldx, worldy]) forwardx = 600 backwardx = 120 BLUE = (25, 25, 200) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ALPHA = (0, 255, 0) ''' Objects ''' # x location, y location, img width, img height, img file class Platform(pygame.sprite.Sprite): def __init__(self, xloc, yloc, imgw, imgh, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)).convert() self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.y = yloc self.rect.x = xloc class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.movex = 0 self.movey = 0 self.frame = 0 self.health = 10 self.score = 0 self.is_jumping = True self.is_falling = True self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert() img.convert_alpha() img.set_colorkey(ALPHA) self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() def gravity(self): if self.is_jumping: self.movey += 3.2 def control(self, x, y): """ control player movement """ self.movex += x def jump(self): if self.is_jumping is False: self.is_falling = False self.is_jumping = True def update(self): """ Update sprite position """ # moving left if self.movex < 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) # moving right if self.movex > 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = self.images[self.frame // ani] # collisions enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False) for enemy in enemy_hit_list: self.health -= 1 # print(self.health) ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False) for g in ground_hit_list: self.movey = 0 self.rect.bottom = g.rect.top self.is_jumping = False # stop jumping # fall off the world if self.rect.y > worldy: self.health -=1 print(self.health) self.rect.x = tx self.rect.y = ty plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) for p in plat_hit_list: self.is_jumping = False # stop jumping self.movey = 0 if self.rect.bottom <= p.rect.bottom: self.rect.bottom = p.rect.top else: self.movey += 3.2 if self.is_jumping and self.is_falling is False: self.is_falling = True self.movey -= 33 # how high to jump loot_hit_list = pygame.sprite.spritecollide(self, loot_list, False) for loot in loot_hit_list: loot_list.remove(loot) self.score += 1 print(self.score) plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) self.rect.x += self.movex self.rect.y += self.movey class Enemy(pygame.sprite.Sprite): """ Spawn an enemy """ def __init__(self, x, y, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.counter = 0 def move(self): """ enemy movement """ distance = 80 speed = 8 if self.counter >= 0 and self.counter <= distance: self.rect.x += speed elif self.counter >= distance and self.counter <= distance * 2: self.rect.x -= speed else: self.counter = 0 self.counter += 1 class Level: def ground(lvl, gloc, tx, ty): ground_list = pygame.sprite.Group() i = 0 if lvl == 1: while i < len(gloc): ground = Platform(gloc[i], worldy - ty, tx, ty, 'tile-ground.png') ground_list.add(ground) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return ground_list def bad(lvl, eloc): if lvl == 1: enemy = Enemy(eloc[0], eloc[1], 'enemy.png') enemy_list = pygame.sprite.Group() enemy_list.add(enemy) if lvl == 2: print("Level " + str(lvl)) return enemy_list # x location, y location, img width, img height, img file def platform(lvl, tx, ty): plat_list = pygame.sprite.Group() ploc = [] i = 0 if lvl == 1: ploc.append((200, worldy - ty - 128, 3)) ploc.append((300, worldy - ty - 256, 3)) ploc.append((550, worldy - ty - 128, 4)) while i < len(ploc): j = 0 while j <= ploc[i][2]: plat = Platform((ploc[i][0] + (j * tx)), ploc[i][1], tx, ty, 'tile.png') plat_list.add(plat) j = j + 1 print('run' + str(i) + str(ploc[i])) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return plat_list def loot(lvl): if lvl == 1: loot_list = pygame.sprite.Group() loot = Platform(tx*5, ty*5, tx, ty, 'loot_1.png') loot_list.add(loot) if lvl == 2: print(lvl) return loot_list ''' Setup ''' backdrop = pygame.image.load(os.path.join('images', 'stage.png')) clock = pygame.time.Clock() pygame.init() backdropbox = world.get_rect() main = True player = Player() # spawn player player.rect.x = 0 # go to x player.rect.y = 30 # go to y player_list = pygame.sprite.Group() player_list.add(player) steps = 10 eloc = [] eloc = [300, 0] enemy_list = Level.bad(1, eloc) gloc = [] tx = 64 ty = 64 i = 0 while i <= (worldx / tx) + tx: gloc.append(i * tx) i = i + 1 ground_list = Level.ground(1, gloc, tx, ty) plat_list = Level.platform(1, tx, ty) enemy_list = Level.bad( 1, eloc ) loot_list = Level.loot(1) ''' Main Loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(-steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(steps, 0) if event.key == pygame.K_UP or event.key == ord('w'): player.jump() if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) # scroll the world forward if player.rect.x >= forwardx: scroll = player.rect.x - forwardx player.rect.x = forwardx for p in plat_list: p.rect.x -= scroll for e in enemy_list: e.rect.x -= scroll for l in loot_list: l.rect.x -= scroll # scroll the world backward if player.rect.x <= backwardx: scroll = backwardx - player.rect.x player.rect.x = backwardx for p in plat_list: p.rect.x += scroll for e in enemy_list: e.rect.x += scroll for l in loot_list: l.rect.x += scroll world.blit(backdrop, backdropbox) player.update() player.gravity() player_list.draw(world) enemy_list.draw(world) loot_list.draw(world) ground_list.draw(world) plat_list.draw(world) for e in enemy_list: e.move() pygame.display.flip() clock.tick(fps) ترجمة -وبتصرف- للمقال Put some loot in your Python platformer game لصاحبيه Seth Kenlon و Jess Weichler. اقرأ أيضًا المقال السابق: إضافة خاصية القفز والركض إلى لعبة في بايثون النسخة العربية الكاملة من كتاب البرمجة بلغة بايثون
  12. تناول المقال السابق من هذه السلسلة محاكاةً برمجية لقوى الجاذبية وتأثيرها على تحركات بطل اللعبة، وفي هذا المقال سيتوفر للبطل ما يعينه على مواجهة أثرها ولو للحظات معدودة عبر منحه خاصية القفز. سيَقفز اللاعب وعندما يصل لذروة قفزته ستُطبق عليه الجاذبية من جديد فيعود للسقوط، فما هي الآلية التي تحكم ذلك وكيف يميز البرنامج بين القفز والسقوط؟ الآلية المستخدمة هنا تعتمد على المتغيرات، إذ سنعرف بدايةً متغير خاص يراقب حالة اللاعب ويخبر بايثون بأن اللاعب يقفز الآن ليطبق عليه الجاذبية فيسقط بتأثيرها على أقرب كائن لموقعه من كائنات اللعبة. قبل أن نبدأ نذكرك بمقالات السلسلة: بناء لعبة نرد بسيطة بلغة بايثون. بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame. إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame. تحريك شخصية اللعبة باستخدام PyGame. إضافة شخصية العدو للعبة. إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame. محاكاة أثر الجاذبية في لعبة بايثون. إضافة خاصية القفز والركض إلى لعبة بايثون. إضافة الجوائز إلى اللعبة المطورة بلغة بايثون. تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة. إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون. ضبط متغيرات القفز سنضيف متغيرين جديدين إلى صنف اللاعب. يتتبع المتغير الأول حالة القفز عند اللاعب، ويحدد في كل لحظة إن كان الكائن يقفز أم يقف على أرضٍ صلبة. أما المتغير الثاني فيسحب كائن اللاعب إلى أرضية اللعبة. بناءً على ما سبق عرّف المتغيرات المنطقية التالية ضمن صنف اللاعب علمًا أن أول سطرين لتبيان السياق فقط ولا حاجة لإعادة كتابتها: self.frame = 0 self.health = 10 # أدناه المتغيرات الخاصة بالقفز self.is_jumping = True self.is_falling = False إنه الاستخدام الأول للمتغيرات المنطقية في هذه السلسلة والتي تسمى أيضًا بالمتغيرات البوليانية نسبةً إلى عالم الرياضيات جورج بول، تأخذ هذه المتغيرات إحدى قيمتين فقط لا ثالث لهما إما صحيح True أو خاطئ False، أي أن اللاعب يقفز أو لا يقفز وفقًا للمتغير الأول، ويسقط أو لا يسقط وفقًا للمتغير الثاني. لاحظ أننا أعطينا المتغير الأول is_jumping الذي يشير إلى القفز قيمةً ابتدائية هي True، ذلك أن كائن البطل في مثالنا يظهر بدايةً في سماء عالم اللعبة وينبغي سحبه إلى الأسفل مباشرةً ليبدأ اللعب، وكانت هذه أسهل الطرق لإنجاز ذلك رغم أنها لا توظف المتغير المنطقي توظيفًا مثاليًا لكنها تغنينا عن كتابة تعليمات خاصة تحدد موقع إنتاج البطل في كل مستوى، زد على أنها تُذكرنا بالألعاب الكلاسيكية التي يقفز فيها اللاعب إلى عالم اللعبة في لحظة البدء، فاعتمد هذه الخدعة البسيطة وسهّل برنامجك لا بأس في ذلك. أما المتغير الثاني is_falling فقد أُسندت له القيمة الابتدائية False ليهبط اللاعب إلى الأرضية فور بدء اللعبة. شروط تفعيل أثر الجاذبية القفز في العالم الحقيقي هو فعل مقاوم للجاذبية، وفي لعبتنا ستعمل الجاذبية في حالة واحدة فقط هي قفز اللاعب -أي عدم وقوفه على أرضٍ صلبة- وتتوقف في بقية الحالات، أما إن عملت باستمرار كما في المقال السابق فإن اللاعب سيعاني ارتدادًا دائمًا بسبب سحبه للاصطدام بالأرض. الجاذبية في ألعاب الفيديو هي دالة تُبنى خصيصًا في بعض الألعاب لتحاكي الجاذبية الأرضية الحقيقية، وليست ميزة مضمنة في محركات الألعاب مثل Pygame وهي بطبيعة الحال لا تُدار من قبلها، فبعض الألعاب المطورة باستعمال Pygame لا تحتاج للجاذبية مطلقًا وفقًا لقصتها وتصميمها، والسقوط ليس سقوطًا بالمعنى الفعلي للكلمة بل مجرد تحريك للكائن تفرضه الدالة، وحتى تمنح البطل قدرة القفز ومقاومة الجاذبية أو الاصطدام بالمنصات العائمة عليك بالتأكيد تعديل هذه الدالة وجعلها تنشط فقط عندما يقفز. عدّل إذًا دالة الجاذبية التي استخدمناها في المقال السابق لتصبح على الشكل التالي: def gravity(self): if self.is_jumping: self.movey += 3.2 تتسبب هذه الدالة في سقوط البطل إلى أسفل الشاشة مباشرةً متجاوزًا الأرضية والفقرة التالية ستُعالج ذلك من خلال اكتشاف تصادمه مع أرضية اللعبة. برمجة الأرضية الصلبة طبقنا في المقال السابق حيلةً بسيطة لمنع كائن البطل من السقوط بعيدًا إلى أسفل الشاشة وجعلناه يرتد إلى مسافة معينة تبقيه ضمن الشاشة، يمكننا تشبيه هذه الحيلة بإنشاء جدار غير مرئي بارتفاعٍ معين يمتد على عرض الشاشة من الأسفل، لا يسعنا فعليًا التمسك بهذه الطريقة فهي لا تحقق معيار الشيفرة النظيفة في البرمجة، ففي الشيفرة النظيفة تُمثل الكائنات بكائنات برمجية فعلية (ونعني بذلك الجدار الوهمي الذي ذكرناه)، أضف إلى ذلك أننا لابد سنحتاج في بعض الحالات إلى إسقاط بطلنا حتى أسفل عالم اللعبة ربما عقبًا له على قفزة خاطئة أو عند خسارة كل نقاطه أو ما شابه فهذه الحركة شائعة في تصميم الألعاب. اكتب التعليمات التالية في الدالة update ضمن صنف اللاعب: ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False) for g in ground_hit_list: self.movey = 0 self.rect.bottom = g.rect.top self.is_jumping = False # إيقاف القفز # السقوط عبر عالم اللعبة if self.rect.y > worldy: self.health -=1 print(self.health) self.rect.x = tx self.rect.y = ty تتحقق هذه التعليمات من حدوث التصادم بين كائن الأرضية وكائن البطل وفق الأسلوب نفسه الذي استخدمناه للتحقق من التصادم بين البطل والعدو. وفي حال حدوث التصادم ستعدّل هذه التعليمات موضع البطل عبر تعديل قيمة self.rect.bottom التي تعني أخفض نقطة في صورة كائن البطل لتصبح مساوية لقيمة g.rect.top التي تعني قمة كائن الأرضية أو أعلى نقطة فيه، وبذلك يبدو البطل كأنه يقف تمامًا فوق أرضية اللعبة ويمنعه ذلك من السقوط إلى الأرضية فهو يقف عليها. علمًا أن هذه التفاصيل (مثل أخفض نقطة وأعلى نقطة في صورة الكائن) هي معلومات توفرها Pygame عن الكائنات عمومًا ويمكنك دائمًا الاستفادة منها. بعد تعديل موضع كائن البطل ستُغَيّر هذه التعليمات قيمة self.is_jumping إلى False ليعرف برنامج اللعبة أن البطل لم يعد في حالة قفز بعد الآن، وقيمة self.movey أيضًا إلى 0 حتى لا يخضع البطل لتأثير الجاذبية (في الواقع هي طرافة في فيزياء اللعبة أن تتوقف عن جذب الكائن إلى الأرض طالما أنه يقف عليها). أما مهمة if الشرطية في الجزء الأخير من التعليمات فهي اكتشاف القفزات الخاطئة لبطل اللعبة التي ينزل بموجبها تحت مستوى الأرضية، لتَخصم بناءً عليها نقطًا من صحته ومن ثم تعيد إنتاجه في أعلى يسار الشاشة باستخدام القيم ty و tx التي تمثل أبعاد بلاط الأرضية وقد استخدمناها للسهولة فقط في حين تستطيع تحديد الموضع الذي تريد، ويمكنك طبعًا عدم خصم نقاط من صحة اللاعب لقاء السقوط دون مستوى الأرضية فأنت من يحدد شروط اللعبة في نهاية المطاف. القفز في Pygame أنشئ أولًا دالة القفز jump لتقلب قيم المتغيرات is_falling و is_jumping. def jump(self): if self.is_jumping is False: self.is_falling = False self.is_jumping = True عمليًا الانطلاق الفعلي للقفز يحدث في الدالة update ضمن صنف اللاعب. if self.is_jumping and self.is_falling is False: self.is_falling = True self.movey -= 33 # مقدار علو القفزة تعمل هذه التعليمات بشرط تحقق أمرين أن تكون قيمة المتغير is_jumping هي True وقيمة المتغير is_falling هي False، وستُعَدّل عندها إحداثيات كائن البطل على المحور العمودي Y لتصبح 33- بكسل في فضاء عالم اللعبة وقيمتها سالبة بالطبع لأن الصفر في الأعلى والقيم الأصغر هي الأقرب إلى أعلى الشاشة، يعطيك هذا الرقم قفزةً جيدة لكن يمكنك تعديله لتحظى بقفزاتٍ أعلى أو أخفض. بعد تعديل الإحداثي العمودي تُغَيّر هذه التعليمات قيمة is_falling لتصبح True حتى لا تتراكم القفزات فيُطلق الكائن عاليًا في فضاء اللعبة. استدعاء دالة القفز أشرنا للقفز سابقًا في مقال تحريك شخصية اللعبة عن طريق PyGame وربطناه بالضغط على السهم العلوي من أسهم الاتجاهات على لوحة المفاتيح قبل إنشاء دالة القفز ودون استدعائها وقد اكتفينا حينها بطباعة عبارة تشير للقفز في نافذة الطرفية باستخدام print، والآن يمكننا استدعاء دالة القفز في الحلقة الرئيسية بدلًا من الطباعة. عدّل القيمة المقابلة للضغط على السهم العلوي في الحلقة الرئيسية لتصبح على الشكل التالي: if event.key == pygame.K_UP or event.key == ord('w'): player.jump() يمكنك استخدام زر آخر بدل السهم العلوي مثل زر المسافة إن رغبت بذلك أو الجمع بين الخيارين باستخدام تعليمة if بسيطة، علمًا أن الرمز الذي يعبر عن المسافة في Pygame هو: pygame.K_SPACE الهبوط على المنصات العائمة تعاملنا في فقرةٍ سابقة مع اصطدام اللاعب بأرضية اللعبة وما نتج عنه من إيقاف لعمل الجاذبية، والآن سنعيد الخطوات نفسها مع بقية المنصات العائمة، إذ إن أرضية اللعبة والمنصات في مثالنا لا تنتميان لمجموعة الكائنات نفسها بل لمجموعتين مختلفتين، مع العلم أنك تستطيع دمجها معًا في مجموعةٍ واحدة، والتعامل مع الأرضية بصفتها منصة عادية، لكن المقال اعتمد الخيار الأوسع الذي يسمح بتفضيلات إضافية، في النتيجة السماح لكائن البطل بالوقوف على إحدى المنصات يعني أن يكتشف البرنامج تصادمه مع كائن المنصة ويمنع الجاذبية من سحبه إلى الأسفل. اكتب التعليمات التالية ضمن الدالة update: plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) for p in plat_hit_list: self.is_jumping = False # إيقاف القفز self.movey = 0 # اكتشاف محاولة الوصول للمنصة من الأسفل if self.rect.bottom <= p.rect.bottom: self.rect.bottom = p.rect.top else: self.movey += 3.2 تفحص هذه التعليمات كافة المنصات العائمة في اللعبة وتتحقق إن حدث أي تصادم بينها وبين كائن البطل، وفي حال ثبت حدوث التصادم تُعدّل قيمة متغير القفز إلى False وتُعطل حركة البطل على المحور العمودي. منصات اللعبة عائمة في الهواء ما يعطي الأريحية للاعب بالوصول إليها من الأعلى أو من الأسفل، عادةً ما تمنع الألعاب وصول البطل للمنصات من الأسفل وتسمح به من الأعلى فقط أي عندما يقفز البطل أعلى من مستوى المنصة، يمكنك تجربة الحالتين لكن هذا المثال تعامل مع المنصات على أنها سقوف والجزء الثاني من التعليمات يسمح للاعب بالقفز على المنصة طالما أن قفزته بلغت مسافةً أعلى من قمة المنصة ويمنعه عندما يحاول القفز عليها من الأسفل. يتحقق السطر الأول من التعليمة if الشرطية فيما إذا كانت قيمة أخفض نقطة من كائن البطل أقل من موقع المنصة ما يعني أن البطل أعلى منها على الشاشة لأن القيم سالبة، وإن تحقق ذلك يُسمح له بالهبوط على المنصة وتتعدل قيمة أخفض نقطة فيه لتصبح مساوية لقيمة قمة كائن المنصة، وإن لم يتحقق الشرط فيزداد الإحداثي Y لموقع لبطل مؤديًا إلى سقوطه نحو الأسفل بعيدًا عن المنصة العائمة التي كان يحاول الوصول إليها. السقوط إذا شغلت اللعبة الآن ستجد أن دالة القفز تعمل وبطلك يتنقل بين المنصات لكنه لا يسقط أرضًا بعد أن يهبط على أي منصة بل يبقى معلقًا في الهواء، والسبب في ذلك أننا عطلنا الجاذبية عند الهبوط على المنصة ولم نكتب أي تعليمة تعيدها للعمل ثانيةً عندما يصل البطل إلى حافة المنصة. عدّل دالة التحديث update في صنف اللاعب لتصبح مسؤولة عن تشغيل وإيقاف تشغيل الجاذبية أثناء حركة الكائن وفق التالي انتبه أنك ستحتاج لإضافة الأسطر التي تتضمن تعليقات فقط. if self.movex < 0: self.is_jumping = True # إيقاف تشغيل الجاذبية self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) if self.movex > 0: self.is_jumping = True # تشغيل الجاذبية self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = self.images[self.frame // ani] إذا فشل التحقق من حدوث تصادم مع إحدى المنصات خلال فترة تشغيل الجاذبية فإن البطل سيسقط أرضًا. شغل اللعبة الآن، ولاحظ أن الوظائف كافة تعمل جيدًا، يمكنك تغيير قيم بعض المتغيرات لتجرب تأثيرها. إليك شيفرة اللعبة كاملًا حتى هذه النقطة، أما في القسم التالي سنمنح البطل القدرة على الركض بحرية عبر تمرير عالم اللعبة جانبيًا. #!/usr/bin/env python3 # by Seth Kenlon # GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import pygame import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 ani = 4 world = pygame.display.set_mode([worldx, worldy]) BLUE = (25, 25, 200) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ALPHA = (0, 255, 0) ''' Objects ''' # x location, y location, img width, img height, img file class Platform(pygame.sprite.Sprite): def __init__(self, xloc, yloc, imgw, imgh, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)).convert() self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.y = yloc self.rect.x = xloc class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.movex = 0 self.movey = 0 self.frame = 0 self.health = 10 self.is_jumping = True self.is_falling = True self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert() img.convert_alpha() img.set_colorkey(ALPHA) self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() def gravity(self): if self.is_jumping: self.movey += 3.2 def control(self, x, y): """ control player movement """ self.movex += x def jump(self): if self.is_jumping is False: self.is_falling = False self.is_jumping = True def update(self): """ Update sprite position """ # moving left if self.movex < 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) # moving right if self.movex > 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = self.images[self.frame // ani] # collisions enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False) for enemy in enemy_hit_list: self.health -= 1 # print(self.health) ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False) for g in ground_hit_list: self.movey = 0 self.rect.bottom = g.rect.top self.is_jumping = False # stop jumping # fall off the world if self.rect.y > worldy: self.health -=1 print(self.health) self.rect.x = tx self.rect.y = ty plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) for p in plat_hit_list: self.is_jumping = False # stop jumping self.movey = 0 if self.rect.bottom <= p.rect.bottom: self.rect.bottom = p.rect.top else: self.movey += 3.2 if self.is_jumping and self.is_falling is False: self.is_falling = True self.movey -= 33 # how high to jump self.rect.x += self.movex self.rect.y += self.movey class Enemy(pygame.sprite.Sprite): """ Spawn an enemy """ def __init__(self, x, y, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.counter = 0 def move(self): """ enemy movement """ distance = 80 speed = 8 if self.counter >= 0 and self.counter <= distance: self.rect.x += speed elif self.counter >= distance and self.counter <= distance * 2: self.rect.x -= speed else: self.counter = 0 self.counter += 1 class Level: def ground(lvl, gloc, tx, ty): ground_list = pygame.sprite.Group() i = 0 if lvl == 1: while i < len(gloc): ground = Platform(gloc[i], worldy - ty, tx, ty, 'tile-ground.png') ground_list.add(ground) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return ground_list def bad(lvl, eloc): if lvl == 1: enemy = Enemy(eloc[0], eloc[1], 'enemy.png') enemy_list = pygame.sprite.Group() enemy_list.add(enemy) if lvl == 2: print("Level " + str(lvl)) return enemy_list # x location, y location, img width, img height, img file def platform(lvl, tx, ty): plat_list = pygame.sprite.Group() ploc = [] i = 0 if lvl == 1: ploc.append((200, worldy - ty - 128, 3)) ploc.append((300, worldy - ty - 256, 3)) ploc.append((550, worldy - ty - 128, 4)) while i < len(ploc): j = 0 while j <= ploc[i][2]: plat = Platform((ploc[i][0] + (j * tx)), ploc[i][1], tx, ty, 'tile.png') plat_list.add(plat) j = j + 1 print('run' + str(i) + str(ploc[i])) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return plat_list ''' Setup ''' backdrop = pygame.image.load(os.path.join('images', 'stage.png')) clock = pygame.time.Clock() pygame.init() backdropbox = world.get_rect() main = True player = Player() # spawn player player.rect.x = 0 # go to x player.rect.y = 30 # go to y player_list = pygame.sprite.Group() player_list.add(player) steps = 10 eloc = [] eloc = [300, 0] enemy_list = Level.bad(1, eloc) gloc = [] tx = 64 ty = 64 i = 0 while i <= (worldx / tx) + tx: gloc.append(i * tx) i = i + 1 ground_list = Level.ground(1, gloc, tx, ty) plat_list = Level.platform(1, tx, ty) ''' Main Loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(-steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(steps, 0) if event.key == pygame.K_UP or event.key == ord('w'): player.jump() if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) world.blit(backdrop, backdropbox) player.update() player.gravity() player_list.draw(world) enemy_list.draw(world) ground_list.draw(world) plat_list.draw(world) for e in enemy_list: e.move() pygame.display.flip() clock.tick(fps) تفعيل الركض ذهابًا وإيابا في عالم اللعبة سنعطي البطل في هذه المرحلة القدرة على الركض ذهابًا وإيابًا عبر تمرير عالم اللعبة جانبيًا. خططت في مقالٍ سابق مستوى للعبة يتضمن عدة منصات وعندما نفذته لم تظهر كافة المنصات على شاشة العرض فقد تجاوزت أبعادها وأعدادها مساحة الشاشة الواحدة، والتمرير الجانبي لعالم اللعبة أو ما يسمى side-scroller هو الحل الأمثل لهذه الحالة ولإظهار المستوى المخطط له كاملًا. تمامًا كما يوحي الاسم، ستتحرك المنصات جانبيًا كلما اقترب البطل من حافة الشاشة لتعطي المستخدم شعورًا يشبه مشاهدة الفيديو وكأن كاميرا متحركة تتنقل في عالم اللعبة وتصوره، يتطلب التمرير حجز منطقة ميتة على جانبي الشاشة الأيمن والأيسر وعندما يصل البطل إلى هذه المناطق سيبقى ثابتًا بينما يُمرر العالم جانبيًا من حوله وتتحرك المنصات. تثبيت نقاط التمرير الجانبي على الشاشة إن رغبت بالسماح للاعب بالتحرك إلى الأمام والخلف فستحتاج إلى نقطتي تمرير على جانبي الشاشة، أما لو سمحت له بالتقدم للأمام فقط تكفيك عندها نقطةٌ واحدة. سنحدد نقاط التمرير باستخدام المتغيرات بمسافة 100 أو 200 بيكسل عن كل جانب من جوانب الشاشة. عرّف إذًا متغيرات التمرير في مقطع المتغيرات ضمن برنامجك على الشكل التالي: forwardx = 600 backwardx = 230 والآن تحقق من اللحظة التي يصل فيها بطلك إلى نقاط التمرير ضمن الحلقة الرئيسية للبرنامج وذلك عبر تعليمة if الشرطية، وإذا وصل اللاعب إلى إحدى هذه النقاط حرك عندها عالم اللعبة يمينًا أو يسارًا. اكتب التعليمات التالية في حلقة التكرار الرئيسية للبرنامج وانتبه أن لا تتداخل مع الحلقة for الخاصة باكتشاف الضغط على أزرار لوحة المفاتيح، الأسطر الثلاثة الأخير هي للسياق فقط لن تحتاج لإعادة كتابتها: # تمرير عالم اللعبة إلى الأمام if player.rect.x >= forwardx: scroll = player.rect.x - forwardx player.rect.x = forwardx for p in plat_list: p.rect.x -= scroll # تمرير عالم اللعبة إلى الخلف if player.rect.x <= backwardx: scroll = backwardx - player.rect.x player.rect.x = backwardx for p in plat_list: p.rect.x += scroll # تعليمات التمرير أعلى هذا السطر world.blit(backdrop, backdropbox) player.gravity() # فحص الجاذبية player.update() شغل اللعبة الآن وجرب التمرير. ستلاحظ بعد التشغيل أن التمرير الجانبي يعمل جيدًا والمنصات تتحرك مع البطل إلى الأمام والخلف أما العنصر الوحيد الذي لا يستجيب للتمرير فهو كائن شخصية العدو. اكتب التعليمات اللازمة لتمرير إن أردت أن يُمرر مثلهم، وتستطيع طبعًا تمريره للأمام فقط أو للخلف فقط أو بكلا الاتجاهين ليلاحق بطل اللعبة. تمرير العدو مع عالم اللعبة طبق التعليمات نفسها الخاصة بتمرير المنصات على شخصية العدو، والأفضل لك أن تطبقها على المجموعة التي تضم جميع كائنات العدو إذ إن اللعبة تتضمن أكثر من عدو، هذه ميزة جمع العناصر المتشابهة في مجموعات فهي تسهل على المبرمج تطبيق التعليمات عليها جميعًا دفعةً واحدة. أضف السطرين الأخيرين من التعليمات التالية ضمن الحلقة الرئيسية في كتلة التمرير إلى الأمام لتمرير العدو إلى الأمام: # تمرير عالم اللعبة إلى الأمام if player.rect.x >= forwardx: scroll = player.rect.x - forwardx player.rect.x = forwardx for p in plat_list: p.rect.x -= scroll for e in enemy_list: # تمرير العدو e.rect.x -= scroll # تمرير العدو ومن ثم السطرين الأخيرين من هذه التعليمات إلى كتلة التمرير إلى الخلف ضمن الحلقة الرئيسية لتمرير العدو بالاتجاه الآخر: # تمرير عالم اللعبة إلى الخلف if player.rect.x <= backwardx: scroll = backwardx - player.rect.x player.rect.x = backwardx for p in plat_list: p.rect.x += scroll for e in enemy_list: # تمرير العدو e.rect.x += scroll # تمرير العدو والآن شغل اللعبة وتفقد النتائج. إليك الشكل النهائي لبرنامج اللعبة حتى هذه المرحلة ليبقى مرجعًا لديك: #!/usr/bin/env python3 # by Seth Kenlon # GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import pygame import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 ani = 4 world = pygame.display.set_mode([worldx, worldy]) forwardx = 600 backwardx = 230 BLUE = (25, 25, 200) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ALPHA = (0, 255, 0) ''' Objects ''' # x location, y location, img width, img height, img file class Platform(pygame.sprite.Sprite): def __init__(self, xloc, yloc, imgw, imgh, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)).convert() self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.y = yloc self.rect.x = xloc class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.movex = 0 self.movey = 0 self.frame = 0 self.health = 10 self.is_jumping = True self.is_falling = True self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert() img.convert_alpha() img.set_colorkey(ALPHA) self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() def gravity(self): if self.is_jumping: self.movey += 3.2 def control(self, x, y): """ control player movement """ self.movex += x def jump(self): if self.is_jumping is False: self.is_falling = False self.is_jumping = True def update(self): """ Update sprite position """ # moving left if self.movex < 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) # moving right if self.movex > 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = self.images[self.frame // ani] # collisions enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False) for enemy in enemy_hit_list: self.health -= 1 # print(self.health) ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False) for g in ground_hit_list: self.movey = 0 self.rect.bottom = g.rect.top self.is_jumping = False # stop jumping # fall off the world if self.rect.y > worldy: self.health -=1 print(self.health) self.rect.x = tx self.rect.y = ty plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) for p in plat_hit_list: self.is_jumping = False # stop jumping self.movey = 0 if self.rect.bottom <= p.rect.bottom: self.rect.bottom = p.rect.top else: self.movey += 3.2 if self.is_jumping and self.is_falling is False: self.is_falling = True self.movey -= 33 # how high to jump self.rect.x += self.movex self.rect.y += self.movey class Enemy(pygame.sprite.Sprite): """ Spawn an enemy """ def __init__(self, x, y, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.counter = 0 def move(self): """ enemy movement """ distance = 80 speed = 8 if self.counter >= 0 and self.counter <= distance: self.rect.x += speed elif self.counter >= distance and self.counter <= distance * 2: self.rect.x -= speed else: self.counter = 0 self.counter += 1 class Level: def ground(lvl, gloc, tx, ty): ground_list = pygame.sprite.Group() i = 0 if lvl == 1: while i < len(gloc): ground = Platform(gloc[i], worldy - ty, tx, ty, 'tile-ground.png') ground_list.add(ground) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return ground_list def bad(lvl, eloc): if lvl == 1: enemy = Enemy(eloc[0], eloc[1], 'enemy.png') enemy_list = pygame.sprite.Group() enemy_list.add(enemy) if lvl == 2: print("Level " + str(lvl)) return enemy_list # x location, y location, img width, img height, img file def platform(lvl, tx, ty): plat_list = pygame.sprite.Group() ploc = [] i = 0 if lvl == 1: ploc.append((200, worldy - ty - 128, 3)) ploc.append((300, worldy - ty - 256, 3)) ploc.append((550, worldy - ty - 128, 4)) while i < len(ploc): j = 0 while j <= ploc[i][2]: plat = Platform((ploc[i][0] + (j * tx)), ploc[i][1], tx, ty, 'tile.png') plat_list.add(plat) j = j + 1 print('run' + str(i) + str(ploc[i])) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return plat_list ''' Setup ''' backdrop = pygame.image.load(os.path.join('images', 'stage.png')) clock = pygame.time.Clock() pygame.init() backdropbox = world.get_rect() main = True player = Player() # spawn player player.rect.x = 0 # go to x player.rect.y = 30 # go to y player_list = pygame.sprite.Group() player_list.add(player) steps = 10 eloc = [] eloc = [300, 0] enemy_list = Level.bad(1, eloc) gloc = [] tx = 64 ty = 64 i = 0 while i <= (worldx / tx) + tx: gloc.append(i * tx) i = i + 1 ground_list = Level.ground(1, gloc, tx, ty) plat_list = Level.platform(1, tx, ty) ''' Main Loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(-steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(steps, 0) if event.key == pygame.K_UP or event.key == ord('w'): player.jump() if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) # scroll the world forward if player.rect.x >= forwardx: scroll = player.rect.x - forwardx player.rect.x = forwardx for p in plat_list: p.rect.x -= scroll for e in enemy_list: # enemy scroll e.rect.x -= scroll # enemy scroll # scroll the world backward if player.rect.x <= backwardx: scroll = backwardx - player.rect.x player.rect.x = backwardx for p in plat_list: p.rect.x += scroll for e in enemy_list: # enemy scroll e.rect.x += scroll # enemy scroll world.blit(backdrop, backdropbox) player.update() player.gravity() player_list.draw(world) enemy_list.draw(world) ground_list.draw(world) plat_list.draw(world) for e in enemy_list: e.move() pygame.display.flip() clock.tick(fps) ترجمة -وبتصرف- للمقال Add jumping to your Python platformer game والمقال Enable your Python game player to run forward and backward لصاحبيه Seth Kenlon و Jess Weichler. اقرأ أيضًا المقال السابق: محاكاة أثر الجاذبية في لعبة بايثون تصميم وبرمجة لعبة قفز باستخدام سكراتش إنشاء السلالم وختام اللعبة
  13. يدعم تطبيق الشفافية ازدهار المجتمعات مفتوحة المصدر، لذا ينبغي على مديري هذه المجتمعات إدراك ذلك والسعي لتوفير الشفافية بمستوياتها الخمسة على أوسع نطاق ضمن مجتمعاتهم، وفي هذا المقال سنُعرّف كل مستوى من هذه المستويات، ونعرض أهميته وأهدافه. ما الذي يجعل الشفافية مهمة في المجتمعات مفتوحة المصدر؟ خلصنا إلى الأسباب التالية التي تجعل الشفافية مهمة في المجتمعات مفتوحة المصدر وهي: ترفع من مستوى الثقة بين المجتمعات ما يزيد التعاون المثمر بينها. تعمل بفضلها المجتمعات معًا وتتبادل الخبرات بانفتاح. تمنع الاحتكاك بين الأطراف. تتطلب من المدراء عرض النتائج والتقارير بشفافية أمام المساهمين. تشجع على عرض المعلومات الكاملة عن المجتمعات، وهو أمر صحي للغاية ويحفز بناء الثقة المتبادلة. ما هي المستويات الخمسة للشفافية؟ هنالك خمسة مستويات من الشفافية التي تساهم مساهمة كبيرة في ازدهار المجتمعات مفتوحة المصدر وهي: نشر الكود المصدري نشر الإرشادات للمجتمع الاحتفاء بالنجاح الاستماع إلى نبض المجتمع وتحليله الحفاظ على المجتمع سنشرحها تباعًا. المستوى الأول: نشر الكود المصدري يتمحور هذا المستوى حول إصدار الكود المصدري للبرمجيات بموجب ترخيص معتمد من مبادرة المصدر المفتوح OSI وبإصدار عام متاح للجميع عبر نظام خاص بإدارة الإصدارات مثل Git. يهدف المستوى الأول إذًا إلى تأسيس المشروع ليكون مفتوح المصدر. النقطة الأولى بديهية، المشروع مفتوح المصدر يعني إتاحة الكود المصدري للمشروع. الكود المصدري المرخص من مبادرة المصدر المفتوح OSI الذي يتشارك الجميع العمل عليه هو جوهر المشروع مفتوح المصدر. يتيح نظام إدارة الإصدارات التعاون ويسمح لكل من يرغب بتحليل المشروع والمساهمة فيه. المستوى الثاني: نشر الإرشادات للمجتمع يهتم المستوى الثاني من الشفافية بتوفير التوثيقات والمصادر وكل ما يلزم لإرشاد المساهمين، وتنظيم فعاليات خاصة لنشر المعرفة ضمن المجتمع. يتجلى هدف هذا المستوى بإنشاء وتنمية مجتمع المشروع مفتوح المصدر. بناء مجتمع فاعل هو مفهوم أوسع بكثير من مجرد إتاحة الكود المصدري. بقدر ما تتحلى بالشفافية حول كيفية عمل مشروعك، وكيف يمكن للآخرين المساهمة فيه، تزيد فرص انضمامهم إلى المشروع وتطويره. تتضمن تنمية المجتمع تنظيم الفعاليات والأنشطة للمساهمين. المستوى الثالث: الاحتفاء بالنجاح عندما تصل لهذا المستوى ستغدو مشاركة الأفكار والرؤى وعرض تطورات المشروع من أهم أولوياتك. ينصب هدف المستوى الثالث على الاحتفاء بالنجاح والإعلان عنه وتأمين المزيد من الدعم للمراحل اللاحقة. يزداد تتبع الأخبار صعوبةً مع توسع المجتمعات مفتوحة المصدر، ويتعذر على الجميع معرفة المستجدات في كل مكان. أن تعرض النتائج والنشاطات بشفافية في أرجاء المجتمع، يعني أن تمنح المساهمين شعورًا بالتقدير وبأن مشاركاتهم مهمة ومرئية. تتميز التقارير والتحليلات في هذا المستوى بكونها متفرقة ولا تعتمد أدواتٍ خاصة. المستوى الرابع: الاستماع إلى نبض المجتمع وتحليله الاستماع إلى نبض المجتمع ومراقبة تطوره هو جوهر المستوى الرابع من الشفافية، بالإضافة إلى تطوير برامج خاصة تساعد على اتخاذ الإجراءات التصحيحية. أما هدف هذا المستوى فيتمثل في نقل المجتمع إلى المستوى التالي عبر فهم مسار تطوره بطرق علمية ودقيقة. تساعد التقارير وأدوات التحليل في مراقبة ما يحدث. يمكنك مقارنة الأحداث في المجتمع، وتتبع تفاعل المساهمين معها. الاستماع الدائم والتحليل يقود إلى استنباط رؤى أعمق وأكثر إفادة عن المجتمع. المستوى الخامس: الحفاظ على المجتمع يركز المستوى الخامس والأخير على مقاييس المجتمع وتحسين المشاركة المجتمعية من أجل الحفاظ عليه ورعايته على المدى الطويل مهما حصلت من متغيرات. يسعى هذا المستوى إلى اتخاذ قرارات هادفة ومؤثرة تدعم المشاركة المجتمعية. تطبيق نظم مرنة للتفاعل مع التغيرات في مقاييس المجتمع. تحليل وتتبع التغيرات التي يمكن أن تطرأ على المجتمع. وضع قواعد للمشرفين والمطورين مثل اتفاقيات مستوى الخدمة SLA وتحمل المسؤولية ليجعلوا المشاركة المجتمعية في صدارة أهدافهم، مع الحرص على وجود نظم تضمن تحقيق ذلك. الخلاصة يحتاج مديرو المجتمعات مفتوحة المصدر إلى تطبيق هذه المستويات الخمسة من الشفافية حتى يدعموا تطور وازدهار هذه المجتمعات. ترجمة -وبتصرف- للمقال ‎5 levels of transparency for open source communities لأصحابه Georg Link و Anirudha Jadhav و Emilio Galeano Gryciuk. اقرأ أيضًا تراخيص البرامج مفتوحة المصدر ما الذي يحفز المساهمين لتطوير المشاريع مفتوحة المصدر؟ الفرق بين البرمجيات الحرة ومفتوحة المصدر
  14. عالمنا الحقيقي مليء بالحركة، والفيزياء هي ما يحكم ويفسر كافة ظواهره المتعلقة بحركة المادة، أما عالم ألعاب الفيديو فهو خالٍ من المادة ما يعني أنه منطقيًا خالٍ من الفيزياء أيضًا، لذا ينبغي على مطوري الألعاب محاكاة الفيزياء في ألعابهم لمنحها لمسة واقعية، وأكثر المبادئ الفيزيائية أهمية في عالم الألعاب هي الجاذبية والتصادم. تعلمنا قليلًا عن التصادم عندما أضفنا شخصية العدو إلى اللعبة، لكننا سنتناوله بمزيدٍ من التعمق في هذا المقال والذي يليه فالجاذبية تفرض علينا ذلك، فما هي العلاقة بين الجاذبية والتصادم؟ ستجيبك عن ذلك تعليماتنا البرمجية اللاحقة. الجاذبية في العالم الحقيقي كما نعرفها جميعًا من دروس الفيزياء هي ميل الأجسام ذات الكتلة إلى التجاذب المتبادل فيما بينها، وكلما زادت كتلة الجسم زاد تأثير الجاذبية التي يمارسها على غيره من الأجسام، فكيف سينعكس ذلك في عالم الألعاب؟ في الواقع الأثر الأوضح للجاذبية الذي ينبغي علينا محاكاته في الألعاب هو ميل الأجسام للسقوط تجاه الكائن الذي يملك افتراضيًا أكبر كتلة في عالم اللعبة، وهو عالم اللعبة نفسه. قبل أن نبدأ نذكرك بمقالات السلسلة: بناء لعبة نرد بسيطة بلغة بايثون. بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame. إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame. تحريك شخصية اللعبة باستخدام PyGame. إضافة شخصية العدو للعبة. إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame. محاكاة أثر الجاذبية في لعبة بايثون. إضافة خاصية القفز والركض إلى لعبة بايثون. إضافة الجوائز إلى اللعبة المطورة بلغة بايثون. تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة. إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون. إضافة دالة الجاذبية سنستفيد من خاصية الحركة التي يملكها كائن اللاعب حتى نسحبه أو نحركه باتجاه أسفل شاشة العرض، بما يحاكي أثر الجاذبية الأرضية، وتذكر أن صفر المحور العمودي في Pygame يقع في الأعلى ما يعني أن الإحداثيات الأكبر هي أكثر قربًا لأسفل الشاشة. الجاذبية انتقائية في ألعاب المنصات فالمبرمج يختار أثرها ولن تؤثر على كل شيء كما في العالم الحقيقي، فلو فعلت ذلك لرأيت منصات اللعبة بأكملها تسقط إلى الأرضية، لذا سنطبق أثر الجاذبية على شخصيتي اللاعب والعدو فقط. أضف أولاً دالة الجاذبية في صنف اللاعب: def gravity(self): self.movey += 3.2 # معدل سرعة سقوط اللاعب الدالة بسيطة فقد ضبطنا حركة اللاعب العمودية لتكون باتجاه أرضية اللعبة دائمًا بغض النظر إن كان أساسًا بحالة حركة أو سكون، أي أنه سيسقط دائمًا وهذا بالفعل معنى الجاذبية الأرضية، واستخدمنا لذلك خاصية الحركة العمودية التي يمتلكها اللاعب. اكتب دالة الجاذبية في الحلقة الرئيسية ليصبح أثرها دائمًا كما في السطر الأول من التعليمات أدناه وبذلك سيُطبقها بايثون على اللاعب مع كل نبضة ساعة. player.gravity() # تحقق من الجاذبية player.update() شغل لعبتك الآن وشاهد النتائج، سيسقط اللاعب سقوطًا حادًا وسريعًا من سماء عالم اللعبة إلى أسفل الشاشة، ربما ترغب بتغيير مقدار سرعة السقوط لتحظى بنتيجة أفضل، لكن أثر الجاذبية يعمل وهذا يكفي في هذه المرحلة. تعيين أرضية الجاذبية ليقف عندها اللاعب بعد السقوط دائمًا ما يلي السقوط إجراءٌ معين في ألعاب الفيديو، قد يكون مسح اللاعب عن شاشة اللعبة وإعادة إنتاجه في مكانٍ مختلف، أو خصم بعض النقاط من رصيده أو ما شابه، بالنتيجة أيًا كان الإجراء الذي تريده فينبغي أولًا إيجاد طريقة تجعل البرنامج يكتشف حادثة السقوط ويقرر إن سقط اللاعب واختفى عن الشاشة، أم أن ذلك لم يحدث، ويُتخذ بعدها الإجراء المطلوب، أما أنسب تعليمة لهذا النوع من الكشف فهي تعليمة if الشرطية. ستتحقق إذًا فيما إذا سقط لاعبك أم لا، وماهو مدى سقوطه، فلو سقط بعيدًا جدًا متجاوزًا الأرضية إلى أخفض نقطة في الشاشة فالوضع غير مقبول ويحتاج لبعض التعديلات فمنطيقًا كان عليه الاستقرار على أرضية اللعبة. لنبسط الموضوع اضبط موقع اللاعب عند 20 بكسل، وعدّل دالة الجاذبية لتصبح: def gravity(self): self.movey += 3.2 # معدل سرعة السقوط if self.rect.y > worldy and self.movey >= 0: self.movey = 0 self.rect.y = worldy-ty ومن ثم شغل اللعبة ولاحظ أن اللاعب يسقط من الأعلى ويتوقف عند أسفل الشاشة، حتى أنك قد لا تتمكن من رؤيته خلف كائن الأرضية، سنعالج الموضوع بإضافة ty- ثانية إلى موقعه الجديد تجعله يرتد أعلى بعد اصطدامه بأسفل الشاشة فيظهر على مسافة معينة ضمن الشاشة، وذلك وفق الأوامر التالية: def gravity(self): self.movey += 3.2 # how fast player falls if self.rect.y > worldy and self.movey >= 0: self.movey = 0 self.rect.y = worldy-ty-ty يحتاج بطل اللعبة إلى طريقة أو قوة دفع تساعده على مقاومة أثر الجاذبية ليجتاز المستويات ويتابع اللعب هذه القوة هي القفز سيناقشها المقال التالي مع توسع إضافي في تصادمه مع الأرضية والمنصات المختلفة. حاول بنفسك تطبيق أثر الجاذبية على كائن شخصية العدو، وانظر الشكل النهائي لكود اللعبة متضمنًا كل فعلناه في السلسلة حتى الآن: #!/usr/bin/env python3 # by Seth Kenlon # GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import pygame import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 ani = 4 world = pygame.display.set_mode([worldx, worldy]) BLUE = (25, 25, 200) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ALPHA = (0, 255, 0) ''' Objects ''' # x location, y location, img width, img height, img file class Platform(pygame.sprite.Sprite): def __init__(self, xloc, yloc, imgw, imgh, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)).convert() self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.y = yloc self.rect.x = xloc class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.movex = 0 self.movey = 0 self.frame = 0 self.health = 10 self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert() img.convert_alpha() img.set_colorkey(ALPHA) self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() def gravity(self): self.movey += 3.2 if self.rect.y > worldy and self.movey >= 0: self.movey = 0 self.rect.y = worldy-ty-ty def control(self, x, y): """ control player movement """ self.movex += x self.movey += y def update(self): """ Update sprite position """ self.rect.x = self.rect.x + self.movex self.rect.y = self.rect.y + self.movey # moving left if self.movex < 0: self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) # moving right if self.movex > 0: self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = self.images[self.frame // ani] hit_list = pygame.sprite.spritecollide(self, enemy_list, False) for enemy in hit_list: self.health -= 1 print(self.health) class Enemy(pygame.sprite.Sprite): """ Spawn an enemy """ def __init__(self, x, y, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.counter = 0 def move(self): """ enemy movement """ distance = 80 speed = 8 if self.counter >= 0 and self.counter <= distance: self.rect.x += speed elif self.counter >= distance and self.counter <= distance * 2: self.rect.x -= speed else: self.counter = 0 self.counter += 1 class Level: def ground(lvl, gloc, tx, ty): ground_list = pygame.sprite.Group() i = 0 if lvl == 1: while i < len(gloc): ground = Platform(gloc[i], worldy - ty, tx, ty, 'tile-ground.png') ground_list.add(ground) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return ground_list def bad(lvl, eloc): if lvl == 1: enemy = Enemy(eloc[0], eloc[1], 'enemy.png') enemy_list = pygame.sprite.Group() enemy_list.add(enemy) if lvl == 2: print("Level " + str(lvl)) return enemy_list # x location, y location, img width, img height, img file def platform(lvl, tx, ty): plat_list = pygame.sprite.Group() ploc = [] i = 0 if lvl == 1: ploc.append((200, worldy - ty - 128, 3)) ploc.append((300, worldy - ty - 256, 3)) ploc.append((500, worldy - ty - 128, 4)) while i < len(ploc): j = 0 while j <= ploc[i][2]: plat = Platform((ploc[i][0] + (j * tx)), ploc[i][1], tx, ty, 'tile.png') plat_list.add(plat) j = j + 1 print('run' + str(i) + str(ploc[i])) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return plat_list ''' Setup ''' backdrop = pygame.image.load(os.path.join('images', 'stage.png')) clock = pygame.time.Clock() pygame.init() backdropbox = world.get_rect() main = True player = Player() # spawn player player.rect.x = 0 # go to x player.rect.y = 30 # go to y player_list = pygame.sprite.Group() player_list.add(player) steps = 10 eloc = [] eloc = [300, 0] enemy_list = Level.bad(1, eloc) gloc = [] tx = 64 ty = 64 i = 0 while i <= (worldx / tx) + tx: gloc.append(i * tx) i = i + 1 ground_list = Level.ground(1, gloc, tx, ty) plat_list = Level.platform(1, tx, ty) ''' Main Loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(-steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(steps, 0) if event.key == pygame.K_UP or event.key == ord('w'): print('jump') if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) world.blit(backdrop, backdropbox) player.gravity() player.update() player_list.draw(world) enemy_list.draw(world) ground_list.draw(world) plat_list.draw(world) for e in enemy_list: e.move() pygame.display.flip() clock.tick(fps) كان هذا المقال السابع من سلسلة بناء لعبة من الصفر باستخدام بايثون 3 و Pygame. ترجمة -وبتصرف- للمقال Simulate gravity in your Python game لصاحبيه Seth Kenlon و Jess Weichler. اقرأ أيضًا المقال السابق: إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame إنشاء الجاذبية في اللعبة النسخة العربية الكاملة من كتاب البرمجة بلغة بايثون
  15. تتحدث كاتبة المقال عن تجربتها الشخصية مع البرمجيات مفتوحة المصدر، وبالتحديد عن الخطأ الأول الذي ارتكبته في بداية مشوارها، وكاد يثنيها عن الشروع بالعمل، وقد عنونت المقال بمتلازمة المحتال Impostor Syndrome في إشارة منها إلى ذلك الشعور الداخلي الذي يتغلب علينا في بعض الأحيان ويزعزع ثقتنا بأنفسنا عبر التشكيك بقدراتنا وما أنجزناه، علّ المقال يكون مفيدًا لكل راغب بدخول هذا المجال. تعلّمتُ -تقول الكاتبة- أساسيات البرمجة من خلال الإنترنت، اتبعت عددًا من البرامج التعليمية وأنجزت المشاريع الملحقة بها، وأحرزت تقدمًا، لكني تساءلت دومًا ماذا بعد؟ ما هي خطوتي التالية للارتقاء إلى المستوى الأعلى؟ وفي هذه المرحلة صادفني منشور على إحدى المدونات يخبر القارئ أن المساهمة في المشاريع مفتوحة المصدر هي الطريقة المثلى لكل مبتدئ يرغب بتطوير مهارته البرمجية والانتقال إلى مستوى أعلى. إذًا بعد أن قرأت ذلك المنشور انتابني شعورٌ قوي أني وصلت للطريق الصحيح، وأن المساهمة في المشاريع مفتوحة المصدر هي السبيل الوحيد لأغدو مطورًا حقيقيًا، لذا بدأت باتباع الإرشادات التي يذكرها المنشور وأولها إنشاء حساب على GitHub، وخلال أقل من عشر دقائق شعرت بالارتباك الشديد لدرجة جعلتني أتخلى عن الفكرة تمامًا. لم أكن شخصًا لا يحب التعلم بل على العكس، لكن المصادر التي اعتمدتها تتوقع من القارئ أن يمتلك معرفةً جيدة ومسبقة بمنصة Git و GitHub وكيف أنها أتاحت للعديد من المطورين حول العالم أن يتعاونوا في مشروعٍ واحد. لذا عدت إلى دروسي ولكن الرغبة في دخول المجال لم تفارقني أبدًا، وعشت صراعًا داخليًا لسنوات بين أن المساهمة في المصدر المفتوح تجربة سهلة وقيمة وينبغي أن أخوضها، وذلك الانطباع الداخلي بأني لست أهلًا بعد لكتابة الأكواد في مشاريع مفتوحة المصدر. لازمتني مشاعر عدم الثقة حتى بعد أن تآلفت مع Git، استمر جدل المحتال الداخلي بأني لم أكتب كودًا لمشاريع حقيقية بعد بل مجرد أكواد لمشاريع تعليمية وتدريبية بل حتى تجارب، وأني لست مؤهلةً للمساهمة في مشاريع حقيقية يستخدمها الآخرون ويعتمدون عليها، في الواقع حتى عندما عملت بصفتي مهندسة برمجيات وكتبت أكوادًا حقيقية، انتابتني المشاعر نفسها، وكانت الحجة أنني أعمل مع لغة برمجة واحدة، فما الذي يضمن نجاحي في مكانٍ آخر ومع أطر عمل وربما لغاتٍ برمجية أخرى. استغرقت فعليًا حوالي السنة ونصف من العمل الجاد بدوامٍ كامل، لأشعر أخيرًا بالثقة الكافية لإغلاق حجج المحتال الداخلي، وتقديم طلب السحب الأول PR على GitHub وبدء رحلتي مع البرمجيات مفتوحة المصدر. أساطير ضارة سنكشف أمامكم زيف اثنتين من أكثر الأساطير الضارة انتشارًا حول البرمجيات مفتوحة المصدر. الأسطورة الأولى: المساهمة في البرمجيات مفتوحة المصدر سهلة تكررت أمامي مرارًا خلال هذه الرحلة رسالةٌ مفادها أن المساهمة في البرمجيات مفتوحة المصدر أمرًا سهلًا، وعندما لم أتمكن من البدء بسهولة بدأت أتساءل عن مهاراتي الخاصة وأشك فيها. أما الآن أفهم معنى هذه الرسالة بطريقةٍ مختلفة، فعندما يصف الناس هذه المساهمة بالسهلة، فهم يقصدون أنها هدف قابل للتحقيق ويمكن للمبتدئين الوصول إليه إن اجتهدوا، أو أنك تستطع المساهمة في البرمجيات المفتوحة المصدر دون كتابة الكثير من التعليمات البرمجية المعقدة، هذا كله صحيح ولكن الأهم أن تعرف أن هذه المساهمة في الواقع صعبة وتتطلب منك وقتًا كافيًا تفهم خلاله قاعدة شيفرة جديدة وأدوات خاصة يستخدمها المطورون. بالتأكيد لا نريد إثناء المبتدئين عن المحاولة، لكننا نرغب في إخبارهم أن مواجهة التحديات أمرًا طبيعيًا ومتوقعًا للغاية خلال العملية. الأسطورة الثانية: لابد أن يساهم كل مطور حقيقي بالبرمجيات مفتوحة المصدر لطالما ذكرني المحتال الداخلي بأن عدم مساهمتي في البرمجيات مفتوحة المصدر يعدّ نقصًا واضحًا ووصمة عار في مسيرتي المهنية، بصراحة حتى وأنا أكتب هذا المقال أشعر بالذنب لأنني لم أساهم أكثر في المجتمع مفتوح المصدر. العمل على هذه البرمجيات طريقة رائعة للتعلم والمشاركة في مجتمع المطورين الأوسع هذا كله لا ريب فيه، إلّا أنها بالتأكيد ليست الطريقة الوحيدة، إذ يمكنك التدوين أو حضور اللقاءات أو العمل في مشاريع أخرى أو القراءة أو الإرشاد أو حتى العودة إلى المنزل في نهاية يوم عمل طويل للاستمتاع بلقاء ودي مع الأهل أو الأصدقاء. ما نريد قوله في النتيجة أن المساهمة في البرمجيات مفتوحة المصدر تحدي ممتع ومفيد حقًا، لكن بشرط أن يكون هو التحدي الذي اخترته. خطئي الأول في المساهمة بالمشاريع مفتوحة المصدر خطئي الأول كان السماح للمحتال الداخلي بالتحكم بقراري، فقد جعلني أتردد لسنوات بحجة أني لست مؤهلةً بعد، حتى أني لم أفهم في حينها حجم العمل الذي أحتاجه للوصول إلى مستوى أشعر معه بالثقة التامة لكتابة كود في مشروع غير مألوف (وهي بالمناسبة نقطة ما زلت أسعى تحقيقها حتى اليوم)، زد على ذلك أني استسلمت لفكرةٍ مفادها أن المساهمة في المجتمع مفتوح المصدر هي الطريقة الوحيدة لإثبات جدارتي بصفتي مطور جيد. في النهاية بدأت، وقدمت طلب السحب الأول على أحد المشاريع واسعة النطاق، ولكن انعدام الثقة بالنفس أفسد علي التجربة بأكملها وجعلها أقل إمتاعًا. لا تدع Git يحبط عزيمتك إذا كنت تحاول تعلم المزيد عن Git ووجدت صعوبة بذلك، أو كنت مبتدئًا ويمنعك Git من تقديم مساهمتك الأولى في هذا المجتمع، فلا تقلق لست الوحيد الذي يشعر بذلك لأن Git معقد بالفعل، ولا يُتوقع منك أن تتعلمه على الفور وتفهمه بمجرد إلقاء نظرة عليه فهو يحتاج وقتًا، ولكن ما إن تتعلمه حتى تكتشف فائدته الجمّة وما يقدم من تسهيلات للمطورين ليعملوا معًا على مشروعٍ واحد وفي وقت واحد ومن ثم قيامه بدمج تعديلاتهم الفردية في الكود النهائي. يوجد العديد من المصادر الجيدة لتتعرف على Git و GitHub (وهو الموقع الذي يستضيف الشيفرات البرمجية ويتيح للأشخاص أن يتعاون عليها باستخدام Git)، ويمكنك أن تبدأ من قسم Git على أكاديمية حسوب سيشكل نقطة انطلاق جيدة إضافة إلى أنه متاح باللغة العربية إذ فيه عدة سلاسل ومقالات مفيدة. أنصحك أن تنتقل إلى سلسلة "المساهمة في المشاريع مفتوحة المصدر" وأن تبدأ بقراءة المقال الأول فيها كيفية المساهمة في المشاريع مفتوحة المصدر: ابدأ بتعلم نظام Git فهي سلسلة مفيدة مؤلفة من ستة مقالات، كما تجد بالقسم أيضًا سلسلة عن تعلم Git مؤلفة من سبعة مقالات. ترجمة -وبتصرف- للمقال My first contribution to open source: Impostor Syndrome لصاحبته Galen Corey. اقرأ أيضًا ما هي البرمجيات مفتوحة المصدر؟ تراخيص البرامج مفتوحة المصدر الإشراف على مشاريع البرمجيات مفتوحة المصدر عبر غيت هب GitHub الفرق بين البرمجيات الحرة ومفتوحة المصدر
×
×
  • أضف...