يوم جيد ، عزيزي Habrazhiteli!
اليوم DevOps على موجة النجاح. في أي مؤتمر تقريبًا حول الأتمتة ، يمكنك سماع من المتحدث يقول "لقد طبقنا DevOps هنا وهناك ، وطبقنا هذا ، وأصبح من الأسهل بكثير إجراء المشاريع ، إلخ ،". وهو أمر يستحق الثناء. ولكن ، كقاعدة عامة ، ينتهي تنفيذ DevOps في العديد من الشركات إلى مرحلة أتمتة عمليات تكنولوجيا المعلومات ، وقلة قليلة من الناس يتحدثون عن تنفيذ DevOps مباشرة في عملية التطوير نفسها.
أود تصحيح سوء التفاهم الصغير هذا. يمكن تطوير DevOps من خلال إضفاء الطابع الرسمي على قاعدة الكود ، على سبيل المثال ، عند كتابة واجهة المستخدم الرسومية لـ REST API.
في هذه المقالة ، أود أن أشاطركم حلاً للحالة غير القياسية التي واجهتها شركتنا - تمكنا من أتمتة تشكيل واجهة تطبيق الويب. سوف أخبركم كيف توصلنا إلى هذه المهمة وما اعتدنا على حلها. لا نعتقد أن مقاربتنا هي الطريقة الحقيقية الوحيدة ، لكننا نحبها حقًا.
آمل أن تكون هذه المواد مثيرة للاهتمام ومفيدة لك.
حسنًا ، لنبدأ!
قبل التاريخ
بدأت هذه القصة قبل عام تقريبًا: لقد كان يومًا صيفيًا جميلًا وكان قسم التطوير لدينا ينشئ تطبيق الويب التالي. على جدول الأعمال كانت مهمة إدخال ميزة جديدة في التطبيق - كان من الضروري إضافة القدرة على إنشاء روابط مخصصة.

في ذلك الوقت ، تم تصميم بنية تطبيق الويب الخاص بنا بطريقة من أجل تنفيذ ميزة جديدة ، نحتاج إلى القيام بما يلي:
- في النهاية الخلفية: قم بإنشاء نموذج لكيان جديد (خطافات) ، ووصف حقول هذا النموذج ، ووصف كامل منطق الإجراءات التي يمكن للنموذج القيام بها ، إلخ.
- في الواجهة الأمامية: قم بإنشاء فئة عرض تتوافق مع النموذج الجديد في واجهة برمجة التطبيقات (API) ، ووصف جميع الحقول التي يحتوي عليها هذا النموذج يدويًا ، وأضف جميع أنواع الإجراءات التي يمكن لهذا العرض تشغيلها ، إلخ.
اتضح أننا في وقت واحد في مكانين ، كان من الضروري إجراء تغييرات مماثلة للغاية في الكود ، بطريقة أو بأخرى ، "تكرار" لبعضنا البعض. وهذا ، كما تعلم ، ليس جيدًا ، لأنه مع مزيد من التغييرات ، سيحتاج المطورون إلى إجراء تغييرات في نفس المكان في مكانين في نفس الوقت.
لنفترض أننا بحاجة إلى تغيير نوع حقل "الاسم" من "السلسلة" إلى "textarea". للقيام بذلك ، سنحتاج إلى إجراء هذا التعديل في الكود النموذجي على الخادم ، ثم إجراء تغييرات مماثلة على كود العرض التقديمي على العميل.
هل هو معقد جدا؟
في السابق ، توصلنا إلى هذه الحقيقة ، لأن العديد من التطبيقات لم تكن كبيرة جدًا وكان هناك مكان "لتكرار" الكود على الخادم وعلى العميل. ولكن في ذلك اليوم الصيفي ذاته ، قبل إدخال الميزة الجديدة ، تم النقر فوق شيء ما بداخلنا ، وأدركنا أنه لم يعد بإمكاننا العمل بهذه الطريقة. كان النهج الحالي غير معقول للغاية ويتطلب الكثير من الوقت والعمل. بالإضافة إلى ذلك ، قد يؤدي "ازدواجية" الشفرة في النهاية الخلفية والواجهة الأمامية إلى حدوث أخطاء غير متوقعة في المستقبل: يمكن للمطورين إجراء تغييرات على الخادم ونسيان إجراء تغييرات مماثلة على العميل ، ومن ثم لن يسير كل شيء على ما يرام حسب الخطة
كيفية تجنب الازدواجية رمز؟ ابحث عن حل
بدأنا نتساءل كيف يمكننا تحسين عملية تقديم ميزات جديدة.
سألنا أنفسنا السؤال: "هل يمكننا الآن تجنب تكرار التغييرات في تمثيل النموذج في الواجهة الأمامية ، بعد أي تغيير في هيكله في النهاية الخلفية؟"
فكرنا وأجابنا: "لا ، لا يمكننا ذلك".
ثم سألنا أنفسنا سؤالًا آخر: "حسنًا ، ما هو إذن سبب تكرار هذا الرمز؟"
وبعد ذلك برزت علينا: المشكلة ، في الواقع ، هي أن الواجهة الأمامية لدينا لا تتلقى بيانات حول بنية API الحالية. لا تعرف الواجهة الأمامية أي شيء عن الطرز الموجودة في واجهة برمجة التطبيقات (API) حتى نعلمها نحن عنها.
ثم حصلنا على الفكرة: ماذا لو قمنا ببناء بنية التطبيق بطريقة:
- الواجهة الأمامية المستلمة من واجهة برمجة التطبيقات (API) ليس فقط لنماذج البيانات ، ولكن أيضًا من هيكل هذه النماذج ؛
- تمثيل أمامي تم تشكيله ديناميكيًا استنادًا إلى هيكل النماذج ؛
- يتم عرض أي تغيير في بنية واجهة برمجة التطبيقات تلقائيًا على الواجهة الأمامية.
سيستغرق تنفيذ ميزة جديدة وقتًا أقل بكثير ، لأنه سيتطلب تغييرات فقط على الجانب الخلفي ، وستقوم الواجهة الأمامية تلقائيًا بالتقاط كل شيء وتقديمه للمستخدم بشكل صحيح.
براعة العمارة الجديدة
وبعد ذلك ، قررنا التفكير بشكل أوسع قليلاً: هل الهيكل الجديد مناسب فقط لتطبيقنا الحالي ، أم يمكننا استخدامه في مكان آخر؟

في الواقع ، بطريقة أو بأخرى ، تحتوي جميع التطبيقات تقريبًا على جزء من وظيفة مماثلة:
- تحتوي جميع التطبيقات تقريبًا على مستخدمين ، وفي هذا الصدد ، من الضروري وجود وظيفة مرتبطة بتسجيل المستخدم والترخيص ؛
- تحتوي جميع التطبيقات تقريبًا على عدة أنواع من طرق العرض: هناك طريقة عرض لعرض قائمة كائنات نموذج ، وهناك طريقة عرض لعرض سجل مفصل لكائن مفرد أو فردي ؛
- تحتوي جميع الطُرز تقريبًا على سمات مماثلة في الكتابة: بيانات السلسلة ، والأرقام ، وما إلى ذلك ، وفي هذا الصدد ، يجب أن تكون قادرًا على العمل معهم في النهاية الخلفية وفي الواجهة الأمامية.
ونظرًا لأن شركتنا تقوم غالبًا بتطوير تطبيقات الويب المخصصة ، فكرنا في: لماذا نحتاج إلى إعادة اختراع العجلة في كل مرة وتطوير وظائف مماثلة في كل مرة من نقطة الصفر ، إذا كان بإمكانك كتابة إطار عمل مرة واحدة يصف كل ما هو أساسي ومشترك للكثيرين التطبيقات والأشياء ، ثم ، إنشاء مشروع جديد ، واستخدام التطورات الجاهزة كما التبعيات ، وإذا لزم الأمر ، وتغييرها بشكل تعريفي في مشروع جديد.
وهكذا ، خلال نقاش طويل ، كانت لدينا فكرة إنشاء VSTUtils - إطار من شأنه أن:
- كانت تحتوي على الوظائف الأساسية ، الأكثر تشابهًا مع معظم التطبيقات ؛
- يُسمح بإنشاء الواجهة الأمامية على الطاير ، بناءً على هيكل واجهة برمجة التطبيقات.
كيفية تكوين صداقات النهاية الخلفية والأمامية؟
حسنًا ، حينئذٍ ، علينا أن نفعل ، كما اعتقدنا. لقد كان لدينا بالفعل بعض الخلفية ، وبعض الواجهة الأمامية أيضًا ، ولكن لا يوجد لدى الخادم أو العميل أداة يمكنها الإبلاغ عن بيانات واجهة برمجة التطبيقات أو استلامها.
في البحث عن حل لهذه المشكلة ،
انتبهنا إلى مواصفات
OpenAPI ، والتي تقوم ، بناءً على وصف النماذج والعلاقات بينهما ، بإنشاء JSON ضخم يحتوي على كل هذه المعلومات.
وقد اعتقدنا أنه من الناحية النظرية ، عند تهيئة التطبيق على العميل ، يمكن للجهة الأمامية أن تتلقى JSON من واجهة برمجة التطبيقات (API) وبناء جميع المشاهدات اللازمة على أساسها. يبقى فقط لتعليم الواجهة الأمامية لدينا للقيام بكل هذا.
وبعد بعض الوقت علمناه.
الإصدار 1.0 - ما خرج منه
تتألف بنية إطار VSTUtils للإصدارات الأولى من 3 أجزاء مشروطة وبدا ما يشبه هذا:
- النهاية الخلفية:
- Django و Python كلها منطق نموذج ذات الصلة. بناءً على قاعدة Django Model ، قمنا بإنشاء عدة فئات من طرازات VSTUtils الأساسية. جميع الإجراءات التي يمكن أن تقوم بها هذه النماذج نفذناها باستخدام Python ؛
- Django REST Framework - جيل REST API. استنادًا إلى وصف النماذج ، يتم تشكيل واجهة برمجة تطبيقات REST ، بفضل اتصال الخادم والعميل ؛
- البينية بين النهاية الخلفية والواجهة الأمامية:
- إنشاء OpenAPI - JSON مع وصف بنية API. بعد أن تم وصف جميع الطرز في النهاية الخلفية ، يتم إنشاء طرق عرض لها. تؤدي إضافة كل طريقة عرض إلى إدخال المعلومات اللازمة في JSON الناتجة:
مثال JSON - مخطط OpenAPI{ // , (, ), // - , // - . definitions: { // Hook. Hook: { // , (, ), // - , // - (, ..). properties: { id: { title: "Id", type: "integer", readOnly: true, }, name: { title: "Name", type: "string", minLength:1, maxLength: 512, }, type: { title: "Type", type: "string", enum: ["HTTP","SCRIPT"], }, when: { title: "When", type: "string", enum: ["on_object_add","on_object_upd","on_object_del"], }, enable: { title:"Enable", type:"boolean", }, recipients: { title: "Recipients", type: "string", minLength: 1, } }, // , , . required: ["type","recipients"], } }, // , (, ), // - ( URL), // - . paths: { // '/hook/'. '/hook/': { // get /hook/. // , Hook. get: { operationId: "hook_list", description: "Return all hooks.", // , , . parameters: [ { name: "id", in: "query", description: "A unique integer value (or comma separated list) identifying this instance.", required: false, type: "string", }, { name: "name", in: "query", description: "A name string value (or comma separated list) of instance.", required: false, type: "string", }, { name: "type", in: "query", description: "Instance type.", required: false, type: "string", }, ], // , (, ), // - ; // - . responses: { 200: { description: "Action accepted.", schema: { properties: { results: { type: "array", items: { // , . $ref: "#/definitions/Hook", }, }, }, }, }, 400: { description: "Validation error or some data error.", schema: { $ref: "#/definitions/Error", }, }, 401: { // ... }, 403: { // ... }, 404: { // ... }, }, tags: ["hook"], }, // post /hook/. // , Hook. post: { operationId: "hook_add", description: "Create a new hook.", parameters: [ { name: "data", in: "body", required: true, schema: { $ref: "#/definitions/Hook", }, }, ], responses: { 201: { description: "Action accepted.", schema: { $ref: "#/definitions/Hook", }, }, 400: { description: "Validation error or some data error.", schema: { $ref: "#/definitions/Error", }, }, 401: { // ... }, 403: { // ... }, 404: { // ... }, }, tags: ["hook"], }, } } }
- الواجهة الأمامية:
- JavaScript عبارة عن آلية تقوم بتوزيع مخطط OpenAPI وإنشاء طرق عرض. يتم تشغيل هذه الآلية مرة واحدة ، عند تهيئة التطبيق على العميل. عن طريق إرسال طلب إلى واجهة برمجة التطبيقات (API) ، يستقبل JSON المطلوب استجابةً لوصف بنية واجهة برمجة التطبيقات (API) ويقوم بتحليله بإنشاء جميع كائنات JS اللازمة التي تحتوي على معلمات تمثيلات النموذج. طلب API هذا ثقيل للغاية ، لذلك نقوم بتخزينه مؤقتًا ونطلبه مرة أخرى فقط عند تحديث إصدار التطبيق ؛
- JavaScript SPA libs - تقديم طرق العرض والتوجيه بينهما. تمت كتابة هذه المكتبات بواسطة أحد مطوري الواجهة الأمامية لدينا. عندما يصل المستخدم إلى صفحة معينة ، يرسم محرك العرض الصفحة بناءً على المعلمات المخزنة في كائنات تمثيل JS.
وبالتالي ، ما لدينا: لدينا خلفية تصف كل المنطق المرتبط بالنماذج. ثم يدخل OpenAPI اللعبة ، والتي تقوم ، بناءً على وصف النماذج ، بإنشاء JSON مع وصف بنية API. بعد ذلك ، يتم نقل عصا القيادة إلى العميل ، الذي يقوم بتحليل OpenAPI JSON الذي تم إنشاؤه تلقائيًا بإنشاء واجهة ويب.
تضمين الميزات في التطبيق على الهيكل الجديد - كيف يعمل
تذكر مهمة إضافة السنانير المخصصة؟ إليك كيفية تطبيقه في تطبيق يستند إلى VSTUtils:

الآن بفضل VSTUtils نحن لسنا بحاجة لكتابة أي شيء من الصفر. إليك ما نقوم به لإضافة القدرة على إنشاء روابط مخصصة:
- في النهاية الخلفية: نأخذ ونرث من أنسب فئة في VSTUtils ، إضافة وظائف جديدة خاصة بالنموذج الجديد ؛
- في النهاية الأمامية:
- إذا كان طريقة عرض هذا النموذج لا تختلف عن طريقة العرض الأساسية لـ VSTUtils ، فإننا لا نفعل شيئًا ، فسيتم عرض كل شيء تلقائيًا بشكل صحيح ؛
- إذا كنت بحاجة إلى تغيير سلوك العرض بطريقة أو بأخرى ، باستخدام آلية الإشارة ، فنحن نوسع السلوك الأساسي للعرض أو نغيره تمامًا.
ونتيجة لذلك ، حصلنا على حل جيد للغاية ، وحققنا هدفنا ، وأصبحت الواجهة الأمامية لدينا يتم إنشاؤها تلقائيًا. تسارعت عملية إدخال ميزات جديدة في المشاريع الحالية بشكل ملحوظ: بدأت الإصدارات تصدر كل أسبوعين ، بينما سبق أن أصدرنا الإصدارات كل 2-3 أشهر مع عدد أقل بكثير من الميزات الجديدة. أود أن أشير إلى أن فريق التطوير قد بقي على حاله ، فقد كان هيكل التطبيق الجديد هو الذي أعطانا الثمار.
الإصدار 1.0 - قلوبنا تغيير الطلب
ولكن ، كما تعلمون ، لا يوجد حد للكمال ، ولم يكن VSTUtils استثناءً.
على الرغم من حقيقة أننا كنا قادرين على أتمتة تشكيل الواجهة الأمامية ، فإن النتيجة لم تكن الحل المباشر الذي أردناه أصلاً.
لم يتم التفكير في بنية التطبيق من جانب العميل تمامًا ، واتضح أنها غير مرنة قدر الإمكان:
- لم تكن عملية إدخال الحمولة الزائدة الوظيفية مريحة دائمًا ؛
- لم تكن آلية تحليل OpenAPI هي الأمثل ؛
- تم تنفيذ التمثيل والتمثيل بينهما باستخدام مكتبات مكتوبة ذاتياً ، والتي لم تناسبنا أيضًا لعدة أسباب:
- هذه المكتبات لم تكن مغطاة بالاختبارات.
- لم يكن هناك وثائق لهذه المكتبات.
- لم يكن لديهم أي مجتمع - في حالة اكتشاف الأخطاء فيهم أو رحيل الموظف الذي كتبهم ، سيكون دعم هذا الرمز صعباً للغاية.
وبما أننا في شركتنا نلتزم بنهج DevOps ونحاول توحيد رمزنا وإضفاء الطابع الرسمي عليه قدر الإمكان ، في فبراير من هذا العام ، قررنا إجراء إعادة هيكلة عالمية لإطار عمل VSTUtils الأمامي. كان لدينا العديد من المهام:
- لتكوين فصول العرض التقديمي ليس فقط في الواجهة الأمامية ، ولكن أيضًا فصول النموذج - أدركنا أنه سيكون من الأصح فصل البيانات (وهيكلها) عن العرض التقديمي. بالإضافة إلى ذلك ، فإن وجود العديد من التجريدات في شكل تمثيل ونموذج من شأنه أن يسهل إلى حد كبير إضافة الأحمال الزائدة من الوظائف الأساسية في المشاريع القائمة على VSTUtils ؛
- استخدم إطارًا تم اختباره مع مجتمع كبير (Angular ، React ، Vue) للتقديم والتوجيه - سيسمح لنا ذلك بالتخلص من كل الصداع مع دعم التعليمات البرمجية المتعلقة بالتقديم والتوجيه داخل تطبيقنا.
إعادة بيع - اختيار إطار JS
من بين أطر JS الأكثر شعبية: الزاوي ، رد الفعل ، فو ، وقع اختيارنا على فو بسبب:
- تزن قاعدة كود Vue أقل من React و Angular ؛
Gzipped إطار مقارنة حجم الرسم البياني
- تستغرق عملية عرض صفحة Vue وقتًا أقل من React and Angular ؛

- عتبة الدخول في Vue أقل بكثير من React و Angular ؛
- بناء جملة مفهومة أصلاً من القوالب ؛
- وثائق أنيقة ومفصلة متوفرة بعدة لغات ، بما في ذلك الروسية ؛
- نظام بيئي متطور يوفر ، بالإضافة إلى مكتبة Vue الأساسية ، مكتبات للتوجيه وإنشاء مستودع بيانات تفاعلي.
الإصدار 2.0 - نتيجة إعادة بيع الواجهات الأمامية
استغرقت عملية إعادة التعمير العالمي للواجهة الأمامية لـ VSTUtils حوالي 4 أشهر ، وهذا ما انتهى بنا إلى:

لا يزال إطار الواجهة الأمامية لـ VSTUtils يتكون من كتلتين كبيرتين: الأولى تعمل على تحليل مخطط OpenAPI ، والثاني هو تقديم وجهات النظر والتوجيه بينهما ، ولكن خضعت كلتا الكتلتين لعدد من التغييرات المهمة.
تمت إعادة كتابة الآلية التي تغسل نظام OpenAPI بالكامل. لقد تغير نهج تحليل هذا المخطط. لقد حاولنا أن نجعل بنية الواجهة الأمامية متشابهة قدر الإمكان للهندسة الخلفية. الآن من جانب العميل ، ليس لدينا مجرد تجريد واحد في شكل تمثيلات ، والآن لدينا أيضًا تجريدات في شكل نماذج و QuerySets:
- كائنات الفئة النموذجية وأحفادها عبارة عن كائنات متوافقة مع تجريدات جانب الخادم من نماذج Django. تحتوي الكائنات من هذا النوع على بيانات حول بنية النموذج (اسم النموذج ، وحقول النموذج ، وما إلى ذلك) ؛
- كائنات فئة QuerySet وأحفادها عبارة عن كائنات متوافقة مع تجريد Django QuerySets من جانب الخادم. تحتوي الكائنات من هذا النوع على طرق تتيح لك تنفيذ طلبات واجهة برمجة التطبيقات (إضافة بيانات الكائنات النموذجية وتعديلها وتلقيها وحذفها) ؛
- كائنات فئة العرض - الكائنات التي تخزن البيانات حول كيفية تمثيل النموذج في صفحة معينة ، وأي قالب يمكن استخدامه "لتقديم" الصفحة ، وأي تمثيلات أخرى للنماذج التي يمكن لهذه الصفحة الارتباط بها ، إلخ.
الوحدة المسؤولة عن التقديم والتوجيه قد تغيرت أيضًا بشكل كبير. لقد تخلينا عن مكتبات JS SPA المكتوبة ذاتيا لصالح Vue.js. لقد قمنا بتطوير مكونات Vue الخاصة بنا والتي تشكل جميع صفحات تطبيق الويب الخاص بنا. يتم التوجيه بين طرق العرض باستخدام مكتبة الموجه vue ، ونحن نستخدم vuex كتخزين تفاعلي لحالة التطبيق.
أود أيضًا أن أشير إلى أن تطبيق الفئات و QuerySet و View في الجهة الأمامية لا يعتمد على وسائل التقديم والتوجيه ، أي إذا أردنا التبديل فجأة من Vue إلى إطار عمل آخر ، على سبيل المثال ، React أو شيء جديد ، إذن كل ما نحتاج إليه هو إعادة كتابة مكونات Vue إلى مكونات الإطار الجديد ، وإعادة كتابة جهاز التوجيه ، والمستودع ، وهذا كل شيء - سيعمل إطار عمل VSTUtils مرة أخرى. سيبقى تطبيق فئات النموذج و QuerySet و View كما هو ، نظرًا لأنه لا يعتمد على Vue.js. نعتقد أن هذه مساعدة جيدة جدًا للتغييرات المستقبلية المحتملة.
لتلخيص
وبالتالي ، أدى الإحجام عن كتابة الكود "المكرر" إلى مهمة أتمتة تشكيل الواجهة الأمامية لتطبيق ويب ، والتي تم حلها عن طريق إنشاء إطار عمل VSTUtils. لقد نجحنا في بناء بنية تطبيق الويب بحيث تكمل كل من الواجهة الخلفية والواجهة الأمامية بعضها البعض بشكل متناغم ويتم التقاط أي تغيير في بنية واجهة برمجة التطبيقات تلقائيًا وعرضه بشكل صحيح على العميل.
الفوائد التي تلقيناها من إضفاء الطابع الرسمي على بنية تطبيق الويب:
- بدأت إصدارات التطبيقات التي تعمل على أساس VSTUtils في الظهور مرتين أكثر. هذا يرجع إلى حقيقة أنه الآن لإدخال ميزة جديدة ، في كثير من الأحيان ، نحتاج إلى إضافة رمز فقط في النهاية الخلفية ، سيتم إنشاء الواجهة الأمامية تلقائيًا - وهذا يوفر الوقت ؛
- تحديث مبسط للوظائف الأساسية. منذ الآن يتم تجميع جميع الوظائف الأساسية في إطار واحد ، من أجل تحديث بعض التبعيات المهمة أو إجراء تحسين في الوظيفة الأساسية ، نحتاج إلى إجراء تغييرات في مكان واحد فقط - في قاعدة كود VSTUtils. عند تحديث إصدار VSTUtils في المشاريع الفرعية ، سيتم تلقائيًا التقاط جميع الابتكارات ؛
- أصبح العثور على موظفين جدد أسهل. موافق ، من الأسهل كثيرًا العثور على مطور لمجموعة تقنية رسمية (Django، Vue) بدلاً من البحث عن شخص يوافق على العمل مع مسجل غير معروف. نتائج البحث للمطورين الذين ذكروا Django أو Vue على HeadHunter في سيرهم الذاتية (عبر جميع المناطق):
- Django - تم العثور على 3،454 سيرة ذاتية لـ 3،136 من المتقدمين ؛
- Vue - تم العثور على 4،092 سيرة ذاتية لـ 3747 من الباحثين عن عمل.
تشمل عيوب هذا الإضفاء طابع رسمي على بنية تطبيق الويب ما يلي:
- نظرًا لتحليل نظام OpenAPI ، فإن تهيئة التطبيق على العميل تستغرق وقتًا أطول قليلاً من السابق (حوالي 20-30 مللي ثانية) ؛
- فهرسة البحث غير مهم. الحقيقة هي أننا في الوقت الحالي لا نستخدم تقديم الخادم في إطار VSTUtils ، ويتم تشكيل جميع محتويات التطبيق في النموذج النهائي بالفعل على العميل. لكن بالنسبة لمشاريعنا ، فغالبًا ما لا تكون هناك حاجة إلى نتائج بحث عالية وليست بالنسبة لنا مهمة بالغة الأهمية.
في هذه قصتي وصلت إلى نهايتها ، شكرا لاهتمامكم!
روابط مفيدة