-
المساهمات
164 -
تاريخ الانضمام
-
تاريخ آخر زيارة
آخر الزوار
6330 زيارة للملف الشخصي
إنجازات ابراهيم الخضور

عضو نشيط (3/3)
84
السمعة بالموقع
-
سنتعرف في هذا المقال على طريقة بناء أزرار عد تنازلي countdown ضمن ألعاب جودو لتساعدنا في تحقيق ميزة الانتظار في اللعبة، مثلًا يمكن أن لا تتفعّل ميزة أو قدرة معينة للاعب ما إلا بعد مضي فترة زمنية معينة، كما سنشرح طريقة بناء قائمة دائرية الشكل Radial menu تعرض للاعب عدة خيارات موزعة على شكل حلقة لتسهيل الوصول لكل خيار وإضافة طابع فريد لواجهة اللعبة. بناء أزرار العد التنازلي قد نرغب في إضافة عدة أزرار تمنح اللاعب مهارات أو قدرات خاصة ability buttons مع توفير ميزة الانتظار لفترة معينة قبل تمكين اللاعب من النقر على كل زر منها واكتساب القدرة المطلوبة، يمكننا تحقيق ذلك من خلال ميزة العد التنازلي countdown، وفي حال احتجنا لأيقونات ورسومات مناسبة لاستخدامها مع هذه الأزرار فستجد كمًا كبيرًا من التصاميم المناسبة في موقع Game-icons.net وسنستخدم بعضها في مقالنا. إعداد مشهد اللعبة يضم المشهد الذي سنعمل عليه العقد nodes التالية: لنوضح بإيجاز دور كل عقدة منها: العقدة النوع الوظيفة AbilityButton TextureButton زر ينشط قدرة خاصة للاعب عند الضغط عليه Sweep TextureProgress شريط تقدم يظهر عد تنازلي بعد الضغط على الزر Timer Timer مؤقت يتحكم في فترة التهدئة Cooldown قبل التمكن من إعادة استخدام القدرة الخاصة Counter MarginContainer حاوية خاصة تتيح إضافة هوامش Margins بين عناصرها Value Label مكون تسمية توضيحية يعرض التوقيت سنحدد الأيقونة الخاصة بكل زر من خلال الخاصية Textures ثم Normal لزر القدرة AbilityButton حيث يمكننا من هنا تحديد الأيقونة الافتراضية للزر عندما لا يكون مضغوطًا، ثم نختار القيمة Full Rect في العقدة Sweep من القائمة Presets لتحديد تأثيرات التعبئة أو المسح التدريجي ليكون على كامل الزر. بعد ذلك، نضبط الخاصية FillMode للعقدة Counter بالقيمة clockwise. نريد تغيير إضاءة زر الانتظار بشكل تدريجي وفق زاوية قطرية. لتحقيق ذلك، نختار الخاصية Visibilty للزر ثم Modulate ونختار قيمتها لتكون بلون رمادي قاتم مع إضافة بعض الشفافية لجعل الزر يبدو باهتًا في وضع الانتظار. نضبط عقدة المؤقت Timer على القيمة One Shot لجعلها تعمل مرة واحدة فقط، وفيما يخص العقدة Counter وهي حاوية تحتوي النص وتحاذيه، ينبغي ضبط تخطيطها على Bottom Wide وضبط الخاصيتين Margin Right و Margin Left للمسافات الجانبية على القيمة 5 وذلك ضمن القسم Theme Overrides ثم Constants. بالنسبة للعقدة value سنضبط خاصية المحاذاة الأفقية Horizontal Alignment على القيمة Right، وخاصية اقتصاص النص Clip Text على القيمة on لتجنب تجاوز النص لحدود الحاوية. ونختار الخط المناسب من القسم Theme Overrides ثم Font ونضع قيمة 0.0 في الحقل النصي. وطالما أن اﻷيقونة التي نستخدمها سوداء، فمن الجيد ضبط قيمة خاصية حجم الحدود Theme Outline Size من القسم Overrides ثم Constants بالقيمة 1 لجعل الأيقونة أكثر وضوحًا. إضافة كود برمجي لزر العد التنازلي نضيف سكريبت إلى عقدة زر القدرة AbilityButton. ثم نربط إشارة timeout الخاصة بالمؤقت Timer وإشارة pressed الخاصة بزر القدرة. وبالتالي عند النقر على الزر، سيبدأ العد التنازلي وعندما ينتهي العد يمكننا تنفيذ إجراء معين. extends TextureButton class_name AbilityButton @onready var time_label = $Counter/Value @export var cooldown = 1.0 func _ready(): time_label.hide() $Sweep.value = 0 $Sweep.texture_progress = texture_normal $Timer.wait_time = cooldown set_process(false) يبدأ السكريبت بتصدير المتغير cooldown الذي يحدد طول فترة الانتظار قبل تفعيل الزر، ومن ثم نضبط المؤقت Timer داخل التابع ()ready_ لاستخدام هذه القيمة. سنحتاج بعد ذلك لخامة texture لنسندها إلى TextureProgress، سنستخدم نفس خامة الزر، ويمكن استخدام أي خامة أخرى نفضلها. أخيرًا، لنتأكد من أن العمليات الخاصة بالمتغير Sweep قد انتهت بشكل صحيح، سنتأكد إن كانت قيمة Sweep هي 0 ونضبط قيمة معالجة العقدة processing على false. وبما أننا ننفذ التحريك ضمن التابع ()process_ لذا لا نحتاج لتنفيذ هذا التابع إن لم نكن في فترة التهدئة CoolDown. func _process(delta): time_label.text = "%3.1f" % $Timer.time_left $Sweep.value = int(($Timer.time_left / cooldown) * 100) نلاحظ في الكود السابق أننا استخدمنا الخاصية time_left للمؤقت Timer لضبط الخاصية text للعقدة labe والخاصية value للعقدة Sweep. func _on_AbilityButton_pressed(): disabled = true set_process(true) $Timer.start() time_label.show() عندما يُنقر الزر سيبدأ كل شيء: func _on_Timer_timeout(): print("ability ready") $Sweep.value = 0 disabled = false time_label.hide() set_process(false) كما يعود كل شيء إلى وضعه عندما ينتهي المؤقت من العد. بإمكاننا وضع عدة أزرار ضمن عقدة حاوية من النوع HBoxContainer وسنحصل على شريط أفقي من أزرار القدرة كما يلي: بناء قائمة دائرية منبثقة تُستخدم القوائم في العديد من اﻷلعاب للوصول إلى ميزات أو وظائف معينة، كأن نحدد من خلالها المهمة المطلوب تنفيذها في اللعبة حاليًا مثل التحدث أو التفتيش أو الهجوم وهكذا. ينبغي أن يكون مظهر وسلوك القائمة متلائمًا مع لعبتنا، لكننا سنركز في هذا المثال على آلية بناء قوائم دائرية Radial Menu ونترك لك حرية تنسيقها. توضح الصورة التالية قائمة العقد المطلوبة لتنفيذ القائمة: نحتاج لاستخدام عقدة TextureButton من النوع RadialMenuButton لتكون عقدة جذر وهي تمثل الزر الرئيسي الذي سننقره لفتح أو إغلاق القائمة الدائرية، وعقدة Buttons من النوع control كحاوية تتضمن كافة الأزرار التي نريد عرضها في القائمة الدائرية، ونتأكد من ضبط قيمة الخاصية Mouse ثم Filter على القيمة Ignore كي لا تعترض أفعال النقر على الفأرة. كما سنستخدم تسعة أزرار لعرض القدرات الخاصة من نوع العداد التنازلي Cooldown. الخطوة التالية هي إضافة السكريبت التالي للعقدة الجذر: extends TextureButton class_name RadialMenuButton export var radius = 120 export var speed = 0.25 var num var active = false يمثل المتغير radius حجم القائمة وهو قطر الدائرة التي سنوزع عليها اﻷزرار، بينما يُستخدم المتغير speed في تحديد سرعة تحريك أزرار القائمة فالقيم اﻷصغر هي اﻷسرع. ويحدد المتغير num عدد اﻷزرار في القائمة، بينما يمثل المتغير active راية flag تدل على إغلاق أو فتح القائمة. func _ready(): $Buttons.hide() num = $Buttons.get_child_count() for b in $Buttons.get_children(): b.position = position نبدأ بإعداد منطق القائمة في التابع ()ready_ وذلك بإخفاء جميع أزرار القائمة افتراضيًا وضبط المسافة بينها وبين الزر الرئيسي للقائمة. ثم نربط اﻹشارة pressed للزر الرئيسي: func _on_pressed(): disabled = true if active: hide_menu() else: show_menu() سيخفي النقر على الزر القائمة أو يظهرها، ونحتاج أيضًا إلى تعطيل الزر أثناء عملية تحريك الرسومات، وإلا سيعيد النقر عليه توليد اﻹطارات البينية tween وإعادة التحريك من جديد: func _on_tween_finished(): disabled = false if not active: $Buttons.hide() عندما ينتهي تحريك اﻹطارات البينية، ننقل حالة الزر إلى تمكين مجددًا. لنلقِ نظرة على الدالة ()show_menu: func show_menu(): $Buttons.show() var spacing = TAU / num active = true var tw = create_tween().set_parallel() tw.finished.connect(_on_tween_finished) for b in $Buttons.get_children(): #لوضع الزر اﻷول في اﻷعلى PI/2 اطرح var a = spacing * b.get_position_in_parent() - PI / 2 var dest = Vector2(radius, 0).rotated(a) tw.tween_property(b, "position", dest, speed).from(Vector2.ZERO).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) tw.tween_property(b, "scale", Vector2.ONE, speed).from(Vector2(0.5, 0.5)).set_trans(Tween.TRANS_LINEAR) نحسب في هذه الدالة المسافة spacing أو بالأصح الزاوية التي نريدها بين كل عنصرين على القائمة، ومن ثم نتنقل بين اﻷزرار ونحدد وجهة كل زر dest وفقًا للزاوية المحسوبة وقيمة نصف القطر radius. ونولد لكل زر خاصيتين هما position و scale لإعطاء اﻷثر المرغوب عند توليد إطارات التحريك tween أثناء تحرك الزر. وتنفذ الدالة ()hide_menu العكس تمامًا: func hide_menu(): active = false var tw = create_tween().set_parallel() tw.finished.connect(_on_tween_finished) for b in $Buttons.get_children(): tw.tween_property(b, "position", Vector2.ZERO, speed).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_IN) tw.tween_property(b, "scale", Vector2(0.5, 0.5), speed).set_trans(Tween.TRANS_LINEAR) ستبدو القائمة كالتالي: الخاتمة شرحنا في هذا المقال كيفية إنشاء أزرار عد تنازلي في محرك الألعاب جودو، والتي تتيح لنا ضبط فترات انتظار قبل إعادة استخدام الأزرار، وهي ميزة ضرورية للألعاب التي تعتمد على منح قدرات أو ميزات خاصة بعد فترة انتظار وتحديد فترة انتظار بعد كل استخدام. كما تناولنا آلية بناء قوائم دائرية توفر تجربة تفاعلية سلسة لعرض الأزار من خلال توزيع الأزرار بشكل منظم حول نقطة مركزية لتسهيل الوصول للخيارات المختلفة داخل اللعبة. ترجمة -وبتصرف- للمقالين: CoolDown Button و Radial Popup Menu اقرأ أيضًا المقال السابق: التعامل مع إجراءات دخل الفأرة في جودو تحريك اللاعب برمجيًا في لعبة ثلاثية الأبعاد باستخدام محرك جودو تعرف على أشهر محركات الألعاب Game Engines الطريقة الصحيحة للتواصل بين العقد في جودو
-
تبرز منصتا ووردبريس ولارافيل كخيارات مثالية في عالم الويب الذي يتطور باستمرار، هذا قد يدفعنا للتساؤل ما الخيار الأفضل من بينهما لتطوير الويب، والجواب على هذا السؤال يعتمد على طبيعة احتياجاتنا، ويحتاج منا لفهم نقاط القوة التي تميز كل منصة ونقاط الضعف المحتملة. نقارن في هذا المقال بين لارافيل و ووردبريس، ونحاول تقديم رؤية واضحة من خلال البحث في جوانب حساسة تتعلق بالمرونة وإمكانية زيادة حجم الأعمال والأمان وسهولة الاستخدام وقابلية توسيع المنصة extensibility كي نتخذ قرارًا مدروسًا. نظرة عامة على لارافيل و ووردبريس قبل أن ننتقل إلى المقارنة بين إطاري العمل، لنلقِ نظرة سريعة عامة عليهما. نظرة عامة على لارافيل لارافيل هو إطار عمل framework لتطوير الويب يعتمد على لغة PHP ويكتسب شعبية متزايدة بين المطورين، وقد اكتسب شعبيته نظرًا لصياغته المحسنّة وتوثيقه الشامل ووجود مجتمع داعم كبير ونشط. تظهر قوة الإطار في إنتاج تطبيقات ويب مخصصة وقابلة للتوسع. يقدم لارافيل مجموعة ملفتة من الميزات ويضم طبقة ORM فعّالة للتفاعل مع قاعدة البيانات حيث توفر هذه الطبقة واجهة لتمثيل الجداول في قاعدة البيانات ككائنات objects. ومنظومة توجيه Routing قوية، وصياغة قواعدية Syntax سهلة الفهم. تجعل هذه الميزات من لارافيل خيارًا ممتازًا للمطورين الذي يرغبون في بناء تطبيقات أكثر تخصصًا وقدرة على التكيف. نظرة عامة على ووردبريس تخطى ووردبريس الغاية الأصلية له كمنصة لإنشاء المدونات وتعداها ليصبح نظام إدارة محتوى Content Management System متعدد المهام يدعم أكثر من 40% من مواقع الويب عالميًا، ولا تزال سمعته كمنصة لإنشاء وإدارة محتوى ويب سهل الاستخدام لا جدال فيها. تمكن ووردبريس من جذب الكثير من الاهتمام بفضل واجهته الواضحة سهلة الاستخدام والعدد الكبير من القوالب والإضافات التي يوفرها وسهولة تشغيله. ويعد إطاراً مناسبًا للأفراد أو الأعمال الصغيرة التي تطمح إلى تأسيس موقع ويب دون الحاجة إلى خبرة تقنية كبيرة. مقارنة سريعة بين لارافيل و ووردبريس الميزة لارافيل ووردبريس نوع إطار العمل إطار عمل PHP مع أسلوب MVC نظام إدارة محتوى CMS نموذج التطوير برمجة كائنية التوجه OOP برمجة إجرائية مع بعض الكائنية تنظيم الشيفرة تتبع أسلوب MVC أي نموذج وعرض ومتحكم مرن نسبيًا، يتبع هيكلية القوالب والإضافات إمكانية التخصيص عالية جدًا محدودة بالقوالب والإضافات منحني التعلم منحني تعلم متدرج أسهل تعلمًا وخاصة للمبتدئين الأداء محسّن جدًا وسريع قد يتأثر الأداء بالإضافات دعم قواعد البيانات يدعم عدة قواعد بيانات من خلال تقنية ORM مثل Eloquent دعم مضمّن لقاعدة البيانات MySQL مع دعم قواعد بيانات أخرى الأمان يوفر معايير أمان مرتفعة عرضة للهجمات إن لم نعمل على صيانته باستمرار إمكانية التوسع مناسب لبناء تطبيقات قابلة للتوسع يعتمد على الإضافات وعلى الخادم مجتمع الداعمين مجتمع كبير ونشط مجتمع ضخم وداعم حالات الاستخدام تطبيقات ويب معقدة وواجهات برمجية مدونات ومواقع ويب صغيرة إلى متوسطة سرعة التطوير يتطلب وقتًا في تنفيذ الإعدادات الأولية وضبطها إعدادات سريعة وميزات وقوالب جاهزة للاستخدام الصيانة يحتاج إلى تحديث منتظم وصيانة يحتاج إلى تحديث متكرر وصيانة التكلفة مجاني ومفتوح المصدر مجاني ومفتوح المصدر المقارنة الشاملة بين لارافيل و ووردبريس بعد أن تعرفنا سريعًا على كلتا المنصتين، بإمكاننا الآن المقارنة بينهما من عدة نواحٍ: 1- الأداء وإمكانية التوسع تعد ميزات الأداء والتوسع ذات أهمية كبيرة في تطوير الويب حاليًا لأنها تؤثر بشكل كبير على تجربة المستخدمين، وزمن تحميل الصفحات، والاستجابة الكلية للتطبيق أو الموقع. لنوضح كيف تتعامل كلتا المنصتين مع هذه النواحي الحيوية: لارافيل لارفيل هو إطار عمل للغة PHP كما ذكرنا سابقًا، لهذا يستخدم مجموعة من التقنيات لتحسين الأداء وتنفيذ العمليات بسلاسة. ونظرًا لكون لارافيل مبني على الوحدات البرمجية وتوسيع الشيفرة فهو يتيح للمطورين بناء تطبيقات قادرة على النمو. ومن خلال دعم لارافيل لإمكانيات التوسع الأفقي، فإنه يستطيع إدارة التطبيقات الموزعة على عدة خوادم باستخدام أدوات مثل موازنات الحمل. كما يسهل لارافيل التعامل مع المهام المتزامنة كونه يدعم أنظمة الأرتال queueing systems مثل Redis و RabbiMO. إضافة إلى ذلك، تستفيد منصة لارافيل من فعالية محرّك PHP الذي يُحدّث ويُحسّن باستمرار. ووردبريس بُني نظام ووردبريس بشكل رئيسي باستخدام PHP، وقد خضع لترقيات مهمة في السنوات الأخيرة لتحسين الأداء قدر الإمكان. يوفر ووردبريس مكتبة واسعة من الإضافات والقوالب التي تسمح للمستخدمين يتطوير وتخصيص مواقعهم بسرعة وسهولة. وعلى الرغم من أن نظام ووردبريس قد صمم أساسًا لبناء مواقع ويب صغيرة الحجم ومحدودة الموارد، إلا أنه يسمح لنا بتعزيز موارد الموقع عند الحاجة باستخدام إضافات التخزين المؤقت، وشبكات توزيع المحتوى CDN، واستخدام بيئة استضافة محسنة لإدارة حركة البيانات الكثيفة فهذه التقنيات تعزز أداء مواقع ووردبريس بشكل كبير. 2- سهولة التطوير يضع المطورون الأولوية لسهولة تطوير التطبيقات عند اختيارهم لأي منصة أو إطار العمل. لهذا، نناقش في هذه الفقرة الفرق بين لارافيل و ووردبريس من ناحية منحنيات التعلم والموارد والتوثيق ودعم المجتمع الخاص بكل إطار. لارافيل يتميز لارافيل بصياغته القواعدية الأنيقة وميزاته المصممة خصيصًا لتسهيل عمل المطورين، وأولوياته في دعم سهولة تطوير المنتجات. إذ يقدم لارافيل أساس برمجي واضح البناء ومباشر وشيفرة سهلة الصيانة. وتبسط بيئة عمل لارافيل التي تضم حزم مضمّنة جاهزة المهام البرمجية الشائعة مثل التوجيه routing والاستيثاق authentication والتفاعل مع قواعد البيانات والتخزين المؤقت للبيانات. تتمحور فلسفة لارافيل الجوهرية حول فكرة إنتاجية المطور وسهولة الاستخدام، وتعزز إمكانية إعادة استخدام الشيفرة لتسهيل صيانة وتحديث المنتج. ووردبريس تدور فكرة التطوير في ووردبريس حول البساطة وسهولة الوصول، فهو يزود المستخدم بلوحة إدارة سهلة الاستخدام لتسريع إدارة المحتوى وتخصيصه. وتُعد قوالب وإضافات ووردبريس عناصر بناء التطبيقات التي تسمح للمطور بتوسيع التصميم ووظائف التطبيق دون العودة إلى نقطة الصفر. وتمكّن خطافات ووردبريس hooks ومرشحاته filters وقوالبه ذات البنية الهرمية وميزة التحديث التلقائي من بناء أقسام مميزة في مواقع الويب، وتعديل الوظائف البنيوية وضمان بقاء الموقع محدّثًا بأدنى جهد ممكن. 3- سهولة الاستخدام تؤثر سهولة استخدام المنصات على تجربة المستخدم ورضاه، لنلق نظرة الآن على ما يقدمه كل من لارافيل و ووردبريس في هذا الشأن لارافيل يركز لارافيل على سهولة الاستخدام من قبل المطورين أساسًا وبعدها سهولة الاستخدام من قبل المستخدم النهائي، حيث يوفر للمطور قاعدة واضحة وسهلة القراءة لكتابة الشيفرة. كما يبسّط التوثيق الشامل والهيكلية الجيدة للواجهات البرمجية من عملية التطوير. كما صممت ميزات لارافيل مثل التوجيه routing وإدارة قواعد البيانات والتوثيق لتكون سهلة الاستخدام من قبل المطورين، مما يعطي فعالية أكبر في التطوير دون المساس بالمرونة. ووردبريس يتمير ووردبريس بسهولة الاستخدام، سواء للمطورين أو المستخدمين غير المختصين. إذ فهو يوفر للمستخدمين واجهة سهلة الاستخدام، إضافة إلى لوحة تحكم واضحة ومكتبات كثيرة للقوالب والإضافات ويوفر أدوات لبناء الصفحات عبر تقنية السحب والإفلات، مما يجعل تخصيص مواقع الويب أمرًا سهلًا دون الحاجة إلى خبرات برمجية كبيرة. 4- الأمان والصيانة يعد موضوع الأمان والصيانة من أكثر الأمور أهمية عند اختيار منصة العمل، لنلق نظرة على ما تقدمه لارافيل و ووردبريس في هاتين الناحيتين. لارافيل يقدم لارافيل حماية مضمّنة من ثغرات الويب الشائعة مثل السكربتات العابرة للمواقع cross-site scripting وسكربتات انتحال الشخصية request forgery وهجوم حقن SQL. إذ يتجاوز باني الاستعلامات وطبقة ORM في لارافيل أي محارف مشبوهة في مدخلات المستخدم للحماية من حقن SQL، وتدعم كلمات السر المرمّزة بما في ذلك ميزات أمنية مثل مفاتيح التحقق CSFR والتعامل الآمن مع الجلسات. ووردبريس يلتزم ووردبريس بتقديم منصة آمنة وتنفيذ معايير أمنية متعددة. وتستهدف تحديثاتها المنتظمة الثغرات التي تظهر ويراقب فريق متخصص المشاكل الأمنية بنشاط ويستجيب لأي مشكلات قد تظهر لاحقًا. إضافة إلى ذلك، يحسن ووردبريس من القدرات الأمنية للتطبيقات من خلال إضافات مخصصة تقدم ميزات مثل فحص البرمجيات الضارة والحماية خلف جدار ناري وتسجيل الدخول الآمن. 5- الشعبية تلعب شعبية المنصة دورًا مهمًا في اختيارها، فهي تعكس نسبة تبني هذه المنصة والدعم المقدم من مجتمعها وطبيعة بيئة العمل فيها. لارافيل يعود سبب الزيادة الكبيرة في شعبية لارافيل إلى الدعم القوي الذي يقدمه مجتمع دعم المنصة وبيئة العمل التي تدعم التوسع. المصدر trends.builtwith يضمن التطوير الفعال للارافيل والتحديثات المستمرة التي تنفذ عليها وصول المطورين إلى أحدث الميزات والتحسينات والتحديثات الأمنية. وتشير شعبية لارافيل المتزايدة إلى الطلب الكبير عليه في سوق العمل، وهذا ما يقدم فرص عمل أوسع لمطوري لارافيل المحترفين. ووردبريس ترجع شعبية ووردبريس إلى القاعدة المعرفية الواسعة لهذه المنصة والدعم الكافي من قبل مجتمعها. المصدر trends.builtwith تساعد القوالب والإضافات الكثيرة والمتنوعة في تقديم تصاميم ووظائف مختلفة، كما تثري التطويرات المستمرة التي يقدمها مجتمع ووردبريس من بيئة العمل وتعززها. 6- الاستضافة إن اختيار الاستضافة المناسبة لموقعنا أو تطبيقنا أمر حيوي جدًا لأدائه وأمانه وتوفّره المستمر. لنرى كيف يكون نهج الاستضافة في كلتا المنصتين. لارافيل لا تتطلب منصة لارافيل أية متطلبات خاصة بالاستضافة نظرًا لمرونتها وكونها إطار عمل للغة PHP. ويمكن للمطورين الاستفادة من مزودات الاستضافة التي تقدم عروضًا مخصصة لمنصة لارافيل أو التي تقدم بيئة عمل محسنة للتطبيقات المبنية على هذه المنصة. ووردبريس صممت منصة ووردبريس لتكون متوافقة مع عدد كبير من بيئات الاستضافة، لكن علينا التفكير بعدة عوامل مثل أداء الخادم وسعة التخزين وحزمة تخديم البيانات ودعم العملاء عند البحث عن استضافة مناسبة لووردبريس. كما تقدم مزودات الاستضافة المخصصة لإدارة تطبيقات ووردبريس إعدادات محسنة للخادم وتحديثات تلقائية وميزات أمان إضافية. المقارنة بين لارافيل و ووردبريس من ناحية التكلفة من الأمور المهمة جدًا في المقارنة بين لارافيل ووردبريس مسألة التكلفة المادية. فمعرفة هيكلية التسعير وخيارات الاستضافة ستساعدنا في اختيار الحل الأكثر فعالية. تكلفة لارافيل لارافيل بحد ذاته مجاني الاستخدام، لكن أي مساعدة في تطوير تطبيقات لارافيل وصيانتها لن تكون رخيصة. وقد يكون مجال تلك التكلفة في الولايات المتحدة الأمريكية مثلًا بين 20-100 دولار في الساعة، مما يجعل كلفة موقع الويب بأكمله بين 3000 و 250 ألف دولار، وفقًا لمتطلبات المشروع وتعقيدها، وبالطبع تختلف هذه الأرقام من منطقة جغرافية لأخرى. كما نضيف إلى ذلك كلفة الاستضافة وقيمتها بالحد الأدنى 10 دولارات شهريًا. تكلفة ووردبريس تختلف تكلفة تطبيقات ووردبريس بحسب إن كنا سنصمم التطبيق بأنفسنا أو نستعين بشركة أو وكالة تطوير. فقد يكلف التطبيق إن قررنا تصميمه بنفسنا بين 20 إلى 300 دولار كما يصل إلى مجال 500-5000 دولار في حال الاستعانة بمطور مستقل وترتفع تكلفة التطوير إلى 3000-100000 دولار إن أردنا الاعتماد على شركة تطوير، ومن جديد ننوه لأن هذه الأرقام تختلف من منطقة جغرافية لأخرى. وتتراوح نفقات الصيانة بين 25 دولار في الشهر للصيانة الذاتية وما بين 50 إلى 100 دولار إن استعنا بمطور مستقل، كما قد تصل إلى 450 دولار إن اعتمدنا على شركة مختصة. كما نضيف إلى ذلك كلفة الاستضافة بحد ذاتها والتي تختلف في حدها الأدنى وفق مزود الاستضافة، وقد لا تقل عن 10 دولار شهريًا. وتتعلق تكلفة الاستضافة عمومًا بحجم الموقع وحجم تبادل البيانات المسموح شهريًا إضافة إلى بعض الميزات الأخرى التي تقع خارج إطار هذا المقال. بإمكاننا بالطبع الاستفادة من المستقلين العرب المتواجدين في منصة مستقل منصة العمل العربي الأكثر شهرة لبناء تطبيقات ويب مميزة باستخدام لارافيل أو ووردبريس بحرفية عالية وأسعار مدروسة. خلاصة: ما الخيار الأفضل لارافيل أم ووردبريس؟ يعتمد الخيار الأمثل على احتياجاتنا الخاصة وأهدافنا من التطبيق أو موقع الويب. إذ تكون لارافيل خيارًا مفضلًا عند إنشاء تطبيقات ويب مخصصة لأنها تقدم مرونة كبيرة وقابلية لتوسيع التطبيق وبيئة عمل قوية مفصلة خصيصًا للمطورين. بينما تشتهر ووردبريس في المقابل بسهولة استخدامها وفعاليتها في إدارة المحتوى وفهارسها التي تمتلئ بالقوالب والإضافات، مما يجعلها خيارًا مفضلًا للأعمال الفردية الصغيرة التي تهدف إلى تأسيس مكان على الإنترنت بسرعة. ومن الضروري جدًا أخذ متطلبات مشروعنا بعين الاعتبار، وخبراتنا في تطوير التطبيقات، ورؤيتنا المستقبلية للمنتج قبل أن نقرر ما هي المنصة الملائمة. أسئلة شائعة 1- من أفضل لارافيل أم ووردبريس الجواب: للمنصتين غايتان مختلفتان فلارافيل هو إطار عمل قوي للغة PHP يستخدم في تطوير تطبيقات الويب، بينما يشتهر ووردبريس بكونه نظام إدارة محتوى متألق في بناء المدونات ومواقع الويب. لهذا السبب، لا يمكن المقارنة بين المنصتين وفق هذا السياق. 3- هل ووردبريس سهل الاستخدام أكثر من لارافيل الجواب: يصنف ووردبريس بأنه أسهل استخدامًا وأبسط تعلمًا من لارافيل. فواجهته الواضحة وسهولة استخدامه عند إنشاء مواقع الويب وإدارة محتوياتها تجعله مفضلًا للمبتدئين الذين يفتقرون إلى الخبرة التقنية في مجال تطوير الويب، بينما يحتاج لارافيل في المقابل إلى معرفة برمجية جيدة لتطوير تطبيقات ويب فعالة. 4- متى نفضل لارافيل على ووردبريس الجواب: لارافيل هو الحل المناسب لبناء تطبيقات ويب معقدة بوظائف خاصة وقدرة على التوسع والتخصيص. بينما ووردبريس هو خيار مثالي عند بناء مواقع ويب هدفها الأساسي تقديم محتوى مثل المدونات والمواقع التعريفية، لأنه يقدم طريقة سريعة في إعداد الموقع من خلال عمليات واضحة وسهلة الاستخدام. ترجمة -وبتصرف- لمقال: Laravel vs WordPress: choosing the Ideal platform for your peb development needs لكاتبه Inshal Ali اقرأ أيضًا تعرف على إطار عمل تطوير الويب الشهير لارافيل Laravel إنشاء واجهة أمامية لمدونة باستخدام لارافيل أفضل الحزم البرمجية لتحسين تطبيقات لارافيل تعلم PHP تعلم ووردبريس
-
نتحدث في هذا المقال عن طرق التقاط مدخلات الفأرة في جودو، وذلك من خلال العمل مع الصنف الأساسي InputEventMouse الذي يتضمن الخاصيتين position و global_position ويرث من هذا الصنف كل من الصنفين InputEventMouseButton و InputEventMouseMotion. ملاحظة: بإمكاننا تعيين أحداث النقر على أزرار الفأرة من خلال الصنف InputMap وهو صنف متفرد singleton وبالتالي سنتمكن من استخدامها مع الدالة ()is_action_pressed. استخدام الصنف InputEventMouseButton يضم الصنف GlobalScope.ButtonList@ قائمة بكل ثوابت اﻷزرار الممكنة *_BUTTON التي قد نحددها في الخاصية button_index. ولنتذكر أن عجلة التمرير في الفأرة scrollwheel تُعد زرًا -أو زرين إن أردنا توخي الدقة- لأن الحدثين BUTTON_WHEEL_UP و BUTTON_WHEEL_DOWN منفصلان. تلميح: يولد النقر على عجلة تمرير الفأرة الحدث pressed فقط، ولا يوجد حدث لتحرير الزر كما في اﻷزرار الأخرى. لاحظ الكود التالي الذي يعرف دالة لمعالجة إدخالات الفأرة، حيث يتحقق فيما إذا كان الحدث يخص زر الفأرة، ثم يحدد ما إذا كان الزر الأيسر قد جرى الضغط عليه أو تحريره، مع طباعة موقع النقر عند الضغط، كما يتعامل الكود أيضًا بالتعامل مع تمرير عجلة الفأرة للأسفل وطباعة رسالة مناسبة عند حدوث ذلك. func _unhandled_input(event): if event is InputEventMouseButton: if event.button_index == BUTTON_LEFT: if event.pressed: print("Left button was clicked at ", event.position) else: print("Left button was released") if event.button_index == BUTTON_WHEEL_DOWN: print("Wheel down") استخدام الصنف InputEventMouseMotion تقع أحداث هذا الصنف عندما يتحرك مؤشر الفأرة، وبإمكاننا إيجاد المسافة المقطوعة وفقًا ﻹحداثيات الشاشة باستخدام الخاصية relative. فيما يلي كود يوضح استخدام حركة الفأرة في تدوير شخصية ثلاثية الأبعاد حول المحور الأفقي، حيث تعتمد سرعة التدوير على حساسية الفأرة: # حساسية الفأرة التي تتحكم في سرعة التدوير عند تحريك الفأرة var mouse_sensitivity = 0.002 func _unhandled_input(event): if event is InputEventMouseMotion: rotate_y(-event.relative.x * mouse_sensitivity) الاحتفاظ بمؤشر الفأرة ضمن نافذة اللعبة بإمكاننا إخفاء مؤشر الفأرة ومنعها من مغادرة نافذة اللعبة، وهذا سلوك شائع في اﻷلعاب ثلاثية الأبعاد وحتى بعض اﻷلعاب ثنائية البعد. وللفأرة أربعة أنماط يمكنك اختيار أي منها باستخدام Input.mouse_mode: MOUSE_MODE_VISIBLE: المؤشر مرئي ويمكن تحريكه بحرية داخل وخارج نافذة اللعبة MOUSE_MODE_HIDDEN:المؤشر مخفي ويمكن له مغادرة نافذة اللعبة MOUSE_MODE_CAPTURED:المؤشر مخفي ولا يمكن له مغادرة نافذة اللعبة MOUSE_MODE_CONFINED:مؤشر الفأرة مرئي ولا يمكن له مغادرة نافذة اللعبة الخيار الثالث Captured هو الخيار اﻷكثر شيوعًا، ويمكننا أيضًا ضبط حالة مؤشر الفأرة أثناء التنفيذ: func _ready(): Input.mouse_mode = Input.MOUSE_MODE_CAPTURED وتمرر أحداث الفأرة بالشكل الطبيعي عند الاحتفاظ بها، لكننا سنواجه بعض المشكلات، فلن نستطيع إغلاق اللعبة أو الانتقال لنافذة أخرى. لهذا من اﻷفضل وجود آلية لتحرير مؤشر الفأرة كأن نحرره عندما يضغط اللاعب على الزر Escape: func _input(event): if event.is_action_pressed("ui_cancel"): Input.mouse_mode = Input.MOUSE_MODE_VISIBLE وهكذا لن تستجيب العبة لحركة الفأرة عندما نكون في نافذة أخرى، ونستطيع التحقق من حالة الاحتفاظ بمؤشر الفأرة في عنصر التحكم بالشخصية من خلال العبارة: if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: وبمجرد أن يتحرر مؤشر الفأرة، سنضطر إلى إعادة الاحتفاظ بها لمتابعة اللعبة. ولنفترض أن لدينا حدثًا ضمن خريطة اﻹدخال يتطلب النقر على الفأرة، عندها، يمكن حل المشكلة كالتالي: if event.is_action_pressed("click"): if Input.mouse_mode == Input.MOUSE_MODE_VISIBLE: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED وطالما أننا قد نستخدم حدث النقر على الفأرة لإطلاق النار أو لتنفيذ إجراء ما، من الجيد إذًا الامتناع عن متابعة تنفيذ الحدث عند الانتهاء من اﻹجراء، وذلك بإضافة ما يلي بعد ضبط حالة مؤشر الفأرة: get_tree().set_input_as_handled() سحب وتحديد عدة عناصر باستخدام الفأرة قد نضطر في اﻷلعاب الاستراتيجية المباشرة لاختيار عدة عناصر أو وحدات لعب دفعة واحدة وإعطائها أوامر معينة، وستكون العملية عادة من خلال رسم صندوق مربع حول هذه العناصر مما يسبب اختيارها، وعندها يمكننا النقر على مكان ما على الخريطة مثلًا لنأمر هذه العناصر بالتحرك إلى هذا المكان. كما في المثال الظاهر في الصورة التالية: إعداد عناصر اللعب لاختبار اﻷمر، نحتاج إلى عدة عناصر تتحرك باتجاه محدد في اللعبة دون أن تتحرك نحو بعضها. إن أردنا أساسًا لبناء هذه الوحدات، يمكن العودة إلى المثال المكتمل ثم إزالة التعليقات عن أسطره لأننا لن نخوض في تفاصيل إنشاء مثل هذه العناصر في المقال. إعداد عالم اللعبة سنعالج عملية اختيار العناصر في عالم اللعبة، لهذا سننشئ هذا العالم من خلال اختيار عقدة Node2D وتسميتها World ثم إضافة نسخ من العناصر ضمنها. نضيف بعد ذلك سكريبتًا إلى العقدة World ثم نضيف المتغيرات التالية: extends Node2D var dragging = false # هل نسحب الفأرة حاليًا؟ var selected = [] # مصفوفة العناصر المسحوبة var drag_start = Vector2.ZERO # موقع بداية السحب var select_rect = RectangleShape2D.new() # شكل التصادم لصندوق السحب نلاحظ أننا سنحتاج إلى طريقة لتحديد العناصر داخل صندوق السحب بمجرد رسمه. لهذا نستعمل العقدة Rectangle Shape2D التي تستعلم من محرك الفيزياء وتعرف العناصر التي اصطدم بها الصندوق. رسم الصندوق نستخدم زر الفأرة اﻷيسر في هذه الطريقة، إذ تبدأ عملية النقر برسم مربع السحب وتنهي عملية تحرير زر الفأرة الرسم، وبهذا يُرسم الصندوق أثناء سحب الفأرة: func _unhandled_input(event): if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: if event.pressed: # إن نُقر زر الفأرة ولم نختر شيء نبدأ عملية السحب if selected.size() == 0: dragging = true drag_start = event.position # إن حُرر زر الفأرة ونحن في حالة سحب نوقف السحب elif dragging: dragging = false queue_redraw() if event is InputEventMouseMotion and dragging: queue_redraw() func _draw(): if dragging: draw_rect(Rect2(drag_start, get_global_mouse_position() - drag_start), Color.YELLOW, false, 2.0) اختيار العناصر بعد أن رسمنا صندوق الاختيار علينا إيجاد العناصر التي تقع ضمنه. فعندما نحرر زر الفأرة وتنتهي عملية السحب، لا بد من الاستعلام من الفضاء الفيزيائي المحيط عن عناصره التي اصطدمت بالصندوق. ولنتذكر أن العناصر هي عقد من النوع CharacterBody2D لكن ستتأثر أيضًا العقد من النوع Area2D وغيرها من اﻷجسام. نستخدم التابع ()PhysicsDirectSpaceState2D.intersect_shape ﻹيجاد العناصر، ويتطلب التابع رسم شكل مستطيل في حالتنا وإجراء تحويل transform للموقع: elif dragging: dragging = false queue_redraw() var drag_end = event.position select_rect.extents = abs(drag_end - drag_start) / 2 نبدأ بتسجيل الموقع الذي حررنا فيه زر الفأرة، إذ نستخدمه في تحديد خاصية الامتداد extents للكائن RectangleShape2D (تقاس extents من مركز المستطيل فهي تمثل نصف الارتفاع أو الاتساع) نبدأ بتسجيل الموقع الذي حررنا فيه زر الفأرة، وهو موقع نهاية السحب، حيث نستخدم هذا الموقع لحساب خاصية الامتداد extents للكائن RectangleShape2D، حيث تشير الخاصية extents تشير إلى نصف أبعاد الشكل، وتُقاس من مركز الشكل وهو في حالتنا شكل مستطيل. ننشئ في الكود التالي مربعًا يمثل المساحة التي جرى تحديدها عن طريق سحب الفأرة، ثم نبحث عن العناصر التي تتداخل مع هذا المستطيل في الفضاء الفيزيائي، ونحدد مكان المستطيل باستخدام موقع نقطة السحب بداية ونهاية الفأرة. # مساحة عالم اللعبة var space = get_world_2d().direct_space_state # استعلام بحث عن التصادم var query = PhysicsShapeQueryParameters2D.new() # تحديد الشكل الذي سنبحث عنه query.shape = select_rect # 2 تحديد العناصر في طبقة التصادم query.collision_mask = 2 # ضبط موقع الشكل query.transform = Transform2D(0, (drag_end + drag_start) / 2) # البحث عن التصادمات selected = space.intersect_shape(query) سنستخدم الآن محرك الفيزياء في جودو للاستعلام عن التصادمات بين الشكل المستطيل الذي حددناه وبين العناصر الفيزيائية الأخرى في اللعبة. لذا نعيد مرجعًا إلى محرك الفيزياء، ونهيئ استعلام الشكل باستخدام العقدة PhysicsShapeQueryParameters2D بعد إسناد شكلنا إليها، ونستخدم مركز المساحة التي تكونت نتيجة السحب كمبدأ للتحويل. ستكون النتيجة عند استدعاء التابع ()intersect_shape مصفوفة تتضمن معلومات عن الأجسام المتصادمة مع الشكل المستطيل، وتبدو كالتالي: [{ "rid": RID(4093103833089), "collider_id": 32145147326, "collider": Unit2:<CharacterBody2D#32145147326>, "shape": 0 }, { "rid": RID(4123168604162), "collider_id": 32229033411, "collider": Unit3:<CharacterBody2D#32229033411>, "shape": 0 }] يدل كل متصادم collider في هذه المصفوفة إلى عنصر، لهذا يمكن استخدامه لتمييز العناصر التي اختيرت وتفعيل طار ملون يحيط بهذه العناصر: for item in selected: item.collider.selected = true إرسال اﻷوامر إلى العناصر بإمكاننا اﻵن إرسال أمر التحرك للعناصر لتتجه نحو مكان ما على الشاشة: func _unhandled_input(event): if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: if event.pressed: # إن نُقر زر الفأرة ولم نختر شيء نبدأ عملية السحب if selected.size() == 0: dragging = true drag_start = event.position # وإلا ستخبر النقرة جميع العناصر المختارة بالتحرك else: for item in selected: item.collider.target = event.position item.collider.selected = false selected = [] تُنفذ شيفرة العبارة else عندما ننقر الفأرة ونختار عنصر أو أكثر. كما يضبط هدف target كل عنصر، وعلينا التأكد من إلغاء اختيار العنصر عندما يصل إلى وجهته لنتمكن من إعادة عملية اختياره عند الحاجة. الخاتمة تعلمنا في هذا المقال طريقة التعامل مع مدخلات المستخدم عن طريق الفأرة من خلال تحديد موقع مؤشر الفارة والاحتفاظ به ضمن نافذة اللعبة وكيفية استخدام الفأرة لتحديد عدة عناصر. ترجمة -وبتصرف- للمقالات: Mouse Input و Capturing the mouse و Mouse:Drag-Select multiple units اقرأ أيضًا المقال السابق: العمل مع إجراءات الدخل Inputs Actions في جودو حفظ واسترجاع البيانات المحلية بين جلسات اللعب تحريك اللاعب برمجيًا في لعبة ثلاثية الأبعاد باستخدام محرك جودو تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو
-
نشرح في هذا المقال كيفية استخدام إجراءات الدخل Input Actions في محرك الألعاب جودو، والتي توفر لنا طريقة فعالة للتحكم في الشخصيات والعناصر داخل اللعبة. فبدلاً من تحديد كل مفتاح كتابيًا في الشيفرة البرمجية، يمكننا استخدام إجراءات الدخل لتهيئة المدخلات وتخصيص مفاتيح التحكم باللاعب بطريقة منظمة وسهلة التعديل. لنفترض أن لدينا شخصية تتحرك في لعبتنا من اﻷعلى إلى اﻷسفل وأردنا كتابة شيفرة باستخدام العقدة InputActionKey التي تتيح لنا بالتحكم بالشخصية عبر مفاتيح اﻷسهم في لوحة المفاتيح، لكن سنجد أن الكثير من اللاعبين يفضلون استخادم المفاتيح W و A و S و D للتحكم بالشخصية، قد نحاول العودة إلى اللعبة وإضافة مفاتيح إضافية لكن النتيجة ستكون شيفرة مضاعفة وزائدة. هنا تساعدنا إجراءات الدخل Input Actionsعلى تهيئة الشيفرة البرمجية للشخصية بشكل أفضل بدلًا من تحديد كل مفتاح كتابيًا في الشيفرة، وسنكون قادرين على تعديل وتخصيص المفاتيح المطلوبة دون تغيير كامل الشيفرة. إنشاء المدخلات بإمكاننا تعريف إجراءات دخل وتعيينها من داخل محرر جودو من خلال الانتقال إلى المشروع Project ثم إعدادات المشروع Project Settings ثم الانتقال لتبويب خريطة اﻹدخال Input Map. سنجد عند النقر على هذا التبويب بعض إجراءات الدخل المهيأة افتراضيًا تُعرف جميعها بالاسم *_ui كي نعلم بوجود إجراءات إدخال افتراضية. ملاحظة: نحتاج لتفعيل مفتاح التبديل أظهر الإجراءات المدمجة Show default Actions من أجل عرض إجراءات الدخل. لا بد عمومًا من إنشاء أحداثنا الخاصة بدلًا من الاعتماد على اﻷحداث الموجودة، لهذا، سنفترض أننا نريد السماح للاعب بالتحرك والدوران ضمن اللعبة عبر لوحة المفاتيح والفأرة، ونحتاج لتمكين اللاعب من التصويب من خلال الضغط على زر الفأرة اﻷيسر أو من خلال الضغط على مفتاح المسافة Spacebar. سننشئ إجراءً جديدًا يُدعى shoot بكتابة اسم اﻹجراء ضمن الحقل إضافة إجراء جديد Add New Action ثم ننقر على زر أضف Add أو الضغط على مفتاح Enter، وسنرى أن الإجراء أضيف إلى القائمة الموجودة. سنتمكن اﻵن من تعيين مدخلات لهذا اﻹجراء بالنقر على الزر + إلى اليمين. قد تكون المدخلات من لوحة المفاتيح أو أزرار الفأرة أو عصا التحكم. وقد اخترنا في حالتنا لوحة المفاتيح Key ثم نقرنا ضمن حقل الاستماع إلى المدخلات listen to inputs ونقرنا بعدها على مفتاح المسافة Spacebar لتعيينه كمفتاح لحدث الدخل، ثم نقرنا على زر حسنًا OK للموافقة على الإضافة كما في الصورة التالية: بنفس الطريقة سندخل إجراء آخر ونختار أزرار الفأرة Mouse Button ونتأكد من وجودنا ضمن حقل الاستماع إلى المدخلات listen to inputs ثم ننقر زر الفأرة اﻷيسر لتعيينه. استخدام إجراءات الدخل بإمكاننا التحقق من اﻹجراء باستدعاء الصنف Input في كل إطار: func _process(delta): if Input.is_action_pressed("shoot"): #ستُنقذ هذه الشيفرة في كل إطار طالما أن زر اﻹدخال مضغوط ولهذا اﻷمر أهميته للإجراءات المستمرة التي نريد التحقق منها باستمرار مثل حركة اللاعب. لكن إن أردنا التقاط اﻹجراء لحظة وقوعه، نستطيع استخدام دالة رد النداء ()input_ أو ()unhandled_input_: func _unhandled_input(event): if event.is_action_pressed("shoot"): # ستعمل الشيفرة في اﻹطار بمجرد الضغط على عنصر اﻹدخال بإمكاننا استخدام دوال متعددة للتحقق من حالة اﻹدخال: is_action_pressed: تعيد القيمة true إن كانت حالة اﻹجراء حاليًا pressed is_action_released: تعيد القيمة true إن لم تكن حالة اﻹجراء حاليًا pressed. is_action_just_pressed و is_action_just_released: تعيدان true فقط ضمن إطار واحد عند وقوع الحدث. وهي مفيدة في اﻹجراءات غير المستمرة التي لابد فيها من تحرير الزر ثم ضغطه لتكرار اﻹجراء إضافة إجراءات دخل إلى الشيفرة مباشرة قد نرغب بإضافة إجراءات إدخال لخريطة اﻹدخال أثناء تنفيذ اللعبة أي نريد إضافة إجراء دخل أو أكثر مباشرة إلى السكريبت. سنجد الحل في الصنف InputMap الذي يقدم مجموعة من التوابع لتساعدنا في ذلك. فيما يلي مثال يضيف إجراءً جديدًا باسم attack عند الضغط على مفتاح المسافة Spacebar: func _ready(): InputMap.add_action("attack") var ev = InputEventKey.new() ev.keycode = KEY_SPACE InputMap.action_add_event("attack", ev) وإن أردنا أيضًا إضافة النقر على الزر اﻷيسر للفأرة إلى اﻹجراءات: ev = InputEventMouseButton.new() ev.button_index = MOUSE_BUTTON_LEFT InputMap.action_add_event("attack", ev) ملاحظة: سيرمي التابع ()InputMap.add_action خطأ إن كان اﻹجراء موجودًا مسبقًا، لهذا علينا التحقق من وجوده من خلال التابع ()InputMap.has_action قبل محاولة إضافته. مثال تطبيقي لنفترض أننا أنجزنا شخصية رئيسية في لعبة ونريد إعادة استخدامها في مشروع آخر. في حال قمنا بتخزين المشهد والسكريبت واﻷصول في مجلد واحد فلن نحتاج سوى إلى نسخ هذا المجلد إلى مشروعنا الجديد، مع تعديل خريطة اﻹدخال كي تعمل اﻹجراءات وفق المطلوب. بدلًا من ذلك، نستطيع إضافة الشيفرة التالية إلى سكريبت اللاعب ونتأكد أن المدخلات اللازمة ستُضاف تلقائيًا: var controls = {"walk_right": [KEY_RIGHT, KEY_D], "walk_left": [KEY_LEFT, KEY_A], "jump": [KEY_UP, KEY_W, KEY_SPACE]} func _ready(): add_inputs() func add_inputs(): var ev for action in controls: if not InputMap.has_action(action): InputMap.add_action(action) for key in controls[action]: ev = InputEventKey.new() ev.keycode = key InputMap.action_add_event(action, ev) الخاتمة نأمل أن يكون هذا المقال وضح لكم كيفية استخدام إجراءات الدخل في تطوير الألعاب بجودو وإنشاء إجراءات مخصصة وإضافتها إلى الشيفرة بطريقة ديناميكية، فسواء كنت تطور لعبة بسيطة أو مشروعًا معقدًا، فستحتاج لنظام إجراءات الدخل للتحكم بشخصيات لعبتك بكفاءة وسلاسة. ترجمة -وبتصرف- للمقالين: Input Actions و Adding Input Action in Code اقرأ أيضًا المقال السابق: دليل جودو تنظيم الحركات في جودو باستخدام SpriteSheet و AnimationTree StateMachine تعرف على مفهوم Delta في تطوير الألعاب تعرف على واجهة محرك الألعاب جودو تعرف على أشهر محركات الألعاب Game Engines
-
هل سبق وشعرتم أنكم تكررون كتابة نفس الشيفرة لتنفيذ مهام روتينية شائعة في مشاريع لارافيل؟ وأن بناء ميزات التطبيق من الصفر تستهلك الكثيرمن الوقت وتبطئ العمل! يمكن حل معظم هذه المشكلات باستخدام حزم لارافيل، وهي وحدات برمجية جاهزة وقابلة للاستخدام المتكرر لتنفيذ المهام والوظائف الشائعة، حيث تسهل تلك الحزم مسار عملنا وتعزز أمان تطبيقاتنا وتحسن وظائفها. نتحدث في هذا المقال عن حزم لارافيل ونستكشف أنواعها ونبحث في الفرق بين الحزم packages والتجميعات bundles، ونعرض قائمة بأفضل الحزم لبناء تطبيقات أسرع. فهرس المقال مفهوم الحزمة package في لارافيل الفرق بين الحزم packages والتجميعات bundles أنواع حزم لارافيل قائمة بأفضل حزم لارافيل حزم تطوير لارافيل حزم أمان لارافيل حزم لوحة تحكم الإدارة في لارافيل حزم لارافيل المخصصة للتجارة الإلكترونية حزم لارافيل لتحسين محركات البحث حزم لارافيل المخصصة لتنقيح الشيفرة حزم اختبار لارافيل مفهوم الحزمة package في لارافيل تُعد الحزمة package بمثابة صندوق أدوات لكل مهام التطوير، وتكون الحزمة على شكل وحدات برمجية مبنية مسبقًا تقدم وظائف مخصصة لتطبيقات لارافيل. توفر الحزم وقت المطور لانها تلغي الحاجة إلى بناء التطبيق من الصفر وتسمح للمطور بالتركيز على الميزات الجوهرية للتطبيق. الفرق بين حزم وتجميعات لارفيل يستخدم كلا المصطلحين حزمة Package وتجميعة Bundle للإشارة إلى نفس التقنية غالبًا على الرغم من وجود اختلاف صغير بينهما: الميزة الحزمة التجميعة المصدر يطورها مجتمع لارافيل أو طرف خارجي يطورها فريق لارافيل الأساسي الحصرية غير موجودة افتراضيًا موجودة عند تثبيت لارافيل أمثلة Debugbar و Socialite Authentication و Caching أنواع حزم لارافيل تأتي حزم لارافيل ضمن فئتين أساسيتين: حزنم مستقلة عن إطار العمل، وحزم مخصصة لإطار عمل الحزم المخصصة لإطار العمل صُمِّمت هذه الحزم خصيصًا لإطار العمل لارفيل. وتعزز هذه الحزم ميزات لارافيل وتقاليد الاستخدام وهندستها لتقديم وظائف مخصصة لتطبيقاتها. من الأمثلة عنها نجد حزم الاستيثاق وموسعّات العمل مع التخزين المؤقت. الحزم المستقلة عن إطار العمل لا ترتبط هذه الحزم بإطار عمل لارافيل ويمكن استخدامها في أي مشروع PHP بصرف النظر عن إطار العمل. تقدم هذه الحزم ميزات لا تتعلق بهندسة لارافيل مما يجعلها متعددة الاستخدامات في مشارع PHP المتنوعة. من الأمثلة عنها نجد مكتبات العمل مع قواعد البيانات وأدوات التحقق من استمارات الويب. قائمة بأفضل حزم لارفيل إليك قائمة بأفضل حزم لارافيل وفقًا لوظائفها المقدمة: الفئة اسم الحزمة الوصف حزم تطوير Laravel Debugbar تضيف أشريط أدوات لأغراض التنقيح. حزم تطوير Laravel User Verification تتولى مهام التحقق من المستخدم وتقييم صلاحية البريد الإلكتروني. حزم تطوير Socialite تمكن من تسجيل الدخول إلى مواقع التواصل الاجتماعي (فيسبوك، غوغل،…). حزم تطوير Laravel Mix أداة تصريف أصول مبنية اعتمادًا على Webpack. حزم تطوير Eloquent-Sluggable توّلد ملاحق عناوين URL محسنة من أجل محركات البحث. حزم تطوير Migrations Generator تولد آليًا ملفات التهجير migration files وفقًا لمخطط قاعدة البيانات. حزم تطوير Laravel Backup تولد نسخًا اختياطية لملفات التطبيق وقاعدة البيانات. حزم تطوير Laravel IDE Helper تعزز تجربة المطوّر من خلال تقديم بيئة تطوير متكاملة IDE. حزم أمان Entrust تقدم أذونات مبنية على أدوار المستخدمين في تطبيقك. حزم أمان No Captcha تقدم خدمة reCaptcha لمنع حالات الوصول غير المرغوبة لوحة تحكم المستخدم Voyager تقدم أداة بناء مرئية للوحة تحكم المدير. لوحة تحكم المستخدم LaraAdmin مولد مفتوح المصدر للوحة تحكم المدير وعمليات قواعد البيانات الأساسية CRUD. لوحة تحكم المستخدم Orchid صندوق أدوات مفتوح المصدر لبناء واجهات الإدارة ولوحة التحكم. تجارة إلكترونية Bagisto حزمة تجارة إلكترونية مفتوحة المصدر لتطبيقات لارافيل. تجارة إلكترونية AvoRed سلة تسوق لارفيل مفتوحة المصدر مع واجهة سهلة الاستخدام للأجهزة المحمولة تحسين محركات بحث Laravel Meta Manager تدير البيانات الوصفية لموقع الويب بغرض تحسين محركات البحث. تحسين محركات بحث SEOTools تحسن ترتيب ظهور الموقع في محركات البحث باعتماد أفضل الخبرات المتوفرة. تحسين محركات بحث Laravel-SEO تضيف وتحذف وتدير البيانات الوصفية لموقع الويب. حزم تنقيح Laravel Telescope تقدم إضاءات على الطلبات والاستثناءات والسجل وغيرها. حزم تطوير لارافيل تبسِّط حزم التطوير مسار العمل بقديمها أدوات تنقيح وتصريف أصول وتوليد شيفرة برمجية. 1. الحزمة Laravel Debugbar تضيف هذه الحزمة الأساسية صندوق أدوات تطوير وتعطينا تلميحات مباشرة عن أداء التطبيق. إذ تعرض الحزمة استعلامات قواعد البيانات والقوالب المصيّرة والمعاملات المُمرَّرة، كما تساعدنا على إضافة رسائل مخصصة لتسهيل التنقيح، وبالتالي زمنًا أكبر في تطوير التطبيق بدلًا من تخمين المشكلات. Debugbar::**info**($object); Debugbar::**error**('Error!'); Debugbar::**warning**('Watch out…'); Debugbar::**addMessage**('Another message', 'mylabel') 2. الحزمة Laravel User Verification تبسّط الحزمة Laravel User Verification تقديم المستخدم إلى الموقع حيث تتحقق من البريد الإلكتروني وصلاحيته. كما تزيد الأداة من مرونة تخصيص قوالب البريد الإلكتروني ومنطق التحقق وتجربة المستخدم كي تتلائم تمامًا مع احتياجات التطبيق، وهي تتكامل بسهولة مع نظام الاستيثاق والتنبيهات في لارافيل مما يساعد على ادخار وقت وجهد المطورين. public function **register**(Request $request) { $this->**validator**($request->**all**())->**validate**(); $user = $this->**create**($request->**all**()); **event**(new **Registered**($user)); $this->**guard**()->**login**($user); UserVerification::**generate**($user); UserVerification::**send**($user, 'My Custom E-mail Subject'); return $this->**registered**($request, $user) ?: **redirect**($this->**redirectPath**()); } 3. الحزمة Socialite طوِّرت حزمة Laravel Socialite من قبل فريق لارافيل لتسهيل عمليات تسجيل الدخول على منصات التواصل الاجتماعي مثل فيسبوك وجوجل وإكس، وهي تتكامل مع نظام الاستيثاق والتنبيهات في لارافيل وتتولى تعقيدات تطبيق بروتوكول OAuth وراء الكواليس دون تدخل منا كي تساعدنا على التركيز على تطوير ميزات التطبيق الأساسية. $user = Socialite::**driver**('github')->**user**(); // OAuth Two Providers $token = $user->token; $refreshToken = $user->refreshToken; // not always provided $expiresIn = $user->expiresIn; // All Providers $user->**getId**(); $user->**getName**(); $user->**getEmail**(); $user->**getAvatar**(); 4. الحزمة Laravel Mix تقدم الحزمة Laravel Mix أداة بسيطة وسهلة الاستخدام لتصريف أصول تطبيق لارافيل. حلّت هذه الحزمة مكان Laravel Elixir وهي تقدم واجهة برمجية واضحة وسهلة الاستخدام لتحديد خطوات بناء التطبيق بطريقة فعالة. تتكامل الأداة بسهول مع أداة التحزيم Webpack مما يحسن ميزة استبدال الوحدات البرمجية أثناء التنفيذ HMR والمزامنة مع المتصفح، مما يساعد في رؤية التغييرات مباشرة دون إعادة تحميل يدوي ويوفر وقت المطورين. mix.**js**('resources/assets/js/app.js', 'public/js') .**sass**('resources/assets/sass/app.scss', 'public/css'); 5. الحزمة Eloquent-Sluggable تقدم Eloquent-Sluggable أداة مفيدة لتوليد عناوين لطيفة slug بعناوين URL وفقًا لخاصيات نموذجنا وبطريقة مؤتمتة، منشئة عناوين تساعد في تحسين محركات البحث في تطبيقك. كما تقدم الحزمة خيارت لتخصيص حقول العناوين الملحقة والفواصل separators وسلوك التحديث. كما توفر أيضًا خطافات لمعالجة الحالات الخاصة وتتكامل مع منطق التطبيق للتأكد من تحسين عناوين URL. class Post extends Eloquent { use Sluggable; protected $fillable = ['title']; public function **sluggable**() { return [ 'slug' => [ 'source' => ['title'] ] ]; } } $post = new **Post**([ 'title' => 'My Awesome Blog Post', ]); // $post->slug is "my-awesome-blog-post 6. الحزمة Migrations Generator تحلل الحزمة Laravel Migrations Generator تخطيط قاعدة البيانات وتولد ملفات التهجير migration files تلقائيًا، مما يوفر علينا الوقت ويساعدنا على التركيز على منطق تطوير التطبيق وليس كتابة تلك الملفات. ويمكننا بكل بساطة تنفيذ أمر التهجير لكل جداول قاعدة البيانات معًا وترك الأمر لهذه الحزمة. php artisan migrate:generate //بالإمكان اختيار جداول معينة فقط php artisan migrate:generate table1,table2 7. الحزمة Laravel Backup تساعدنا الحزمة Laravel Backup في إنشاء نسخ احتياطية عن ملفاتنا عن طريق ضغط المجلدات وقاعدة البيانات في ملف snapshot واحد لضمان حماية وتأمين المشروع، وذلك من خلال تنفيذ أمر واحد. php artisan backup:run 8. الحزمة Laravel IDE Helper تحسن هذه الأداة الأساسية IDE Helper تجربتنا مع محررات الأكواد وبيئات التطوير مثل PhpStorm و VS Code من خلال ميزات عديدة منها الإكمال التلقائي للشيفرة، والتلميح بالنوع خصيصًا لمكونات لارافيل، فلن نضطر مع هذه الأداة إلى البحث عن التعليمات بل سنعرض تلميحات وتوجيهات مباشرة عن التوابع والمعاملات المتاحة. كما تولد الأداة تعليقات توثيقية لتسهّل علينا التنقل ضمن واجهة لارافيل البرمجية ضمن بيئة التطوير التي تفضلها. حزم أمان لارافيل تساعد حزم الأمان في دعم أمان التطبيق من خلال ميزات مثل الاستيثاق والأذونات المبنية على أدوار محددة والتكامل مع خدمة reCAPTCHA. 9. الحزمة Entrust تساعدنا Entrust في تحديد دور كل مستخدم وتسند إليه أذونات أو سماحيات محددة، وبالتالي سيتمكن المستخدم من الوصول إلى وظائف محددة مسبقًا فقط مما يحسن أمان التطبيق. تنشئ هذه الحزمة الجداول الأربعة التالية لأدوار المستخدمين: جدول أدوار لتخزين سجلات الأدوار جدول الأذونات لتخزين سجلت السماحيات جدول أدوار-مستخدم role-user لتخزين علاقات واحد-إلى-أكثر بين الأدوار والمستخدمين جدول أذونات-دور permission_role لتخزين علاقات واحد-إلى-أكثر بين الأدوار والأذونات بإمكاننا إنشاء دور بتنفيذ أسطر الشيفرة التالية: admin = new **Role**(); $admin->name = 'admin'; $admin->display_name = 'User Administrator'; // optional $admin->description = 'User is allowed to manage and edit other users'; // optional $admin->**save**() نسند الدور إلى المستخدم كالتالي: user = User::**where**('username', '=', 'michele')->**first**(); $user->**attachRole**($admin); ثم علينا تحديد الأذونات الخاصة بالدور: $createPost = new **Permission**(); $createPost->name = 'create-post'; $createPost->display_name = 'Create Posts'; $createPost->description = 'create new blog posts'; $createPost->**save**(); $admin->**attachPermission**($createPost); 10. الحزمة No Captcha تعمل الحزمة No Captcha على تحقيق تكامل لتطبيق لارافيل مع خدمة reCAPTCHA من جوجل لحمايته من الروبوتات الآلية bots وتضيف طبقة أمان إضافية إليه. لهذا، ننصح بالحصول على مفتاح الواجهة البرمجية API المجاني لهذه الحزمة وحماية التطبيقات. تقدم الحزمة أيضًا خيارات مخصصة لعنصر التحكم CAPTCHA كي نضمن اندماجه مع تصميم الاستمارات Forms بكل بساطة ودون التأثير على تجربة المستخدم. NoCaptcha::**shouldReceive**('verifyResponse') ->**once**() ->**andReturn**(true); $response = $this->**json**('POST', '/register', [ 'g-recaptcha-response' => '1', 'name' => 'Pardeep', 'email' => 'pardeep@example.com', 'password' => '123456', 'password_confirmation' => '123456', ]); حزم لوحة تحكم إدارة لارافيل تساعد حزم إدارة لوحة التحكم على بناء واجهات سهلة الاستخدام للوحات التحكم لمدير الواجهة الخلفية للتطبيقات 11. Voyager قد يكون بناء لوحة تحكم مدير واضحة وسهلة أمرًا يستهلك الكثير من وقت المطور، لكن الحزمة Voyager ستبسط العملية من خلال واجهة سهلة الاستخدام. تتميز الحزمة بتوثيقها الجيد وتتضمن واجهة واضحة وسهلة الاستخدام، وتوفر بيانات مخصصة للاختبارات ومدير وسائط متعددة متقدم. وتساعدنا في التركيز على بناء وظائف تطبيقات لارافيل لتتكفل هي بإنشاء لوحات التحكم. 12. الحزمة LaraAdmin تُعد LaraAdmin حزمة مجانية قوية ومفتوحة المصدرية لتطوير لارافيل، حيث تسهِّل إنشاء لوحة تحكم إدارة التطبيق بتقديمها ميزات مثل إدارة المستخدمين، والوصول المشروط بالدور الممنوح للمستخدم، والقوائم الديناميكية. تقدم الأداة أيضًا جداول بيانات سهلة التخصيص تدعم الفرز والترشيح وتعدد الصفحات مما يسمح بإدارة البيانات المعقدة بكل سهولة. لهذا ستساعدنا هذه الأداة في التركيز على البناء الوظيفي لتطبيقات لارافيل وتتكفل هي بإنشاء واجهات الإدارة. 13. الحزمة Orchid تُقدم الحزمة Orchid صندوق أدوات يتمتع بتصميم مرن وقابل للتوسع، مما يسمح ببناء واجهات سهلة الاستخدام مفصلة خصيصًا لتلائم تطبيقاتنا. وتتجاوز الأداة إنشاء لوحات تحكم بسيطة وتتصرف كمنظومة لإدارة تطبيقات الويب. إذ يمكن التفكير بها على أنها منظومة إدارة محتوى تسهل إدارة المحتوى والمستخدمين في تطبيقات لارافيل. حزم لارافيل المخصصة للتجارة الإلكترونية تساعدنا هذه الحزم في تسريع تطوير المتاجر الإلكترونية من خلال ميزاتها المتعددة مثل إدارة المنتجات وعربات التسوق وبوابات الدفع الإلكتروني. 14. الحزمة Bagisto تُعد Bagisto حزمة مفتوحة المصدر مخصصة لمتاجر لارافيل الإلكترونية التي تلفت انتباه المطورين بسرعة. إذ تقدم نظام إدارة مستخدمين سهل مع خيارات متعددة لإدارة المخازن وغيرها من الميزات. كما تُجمّع الحزمة Laravel CMS مع أدوات تسهل التنقل بين الأقسام المختلفة ضمن لوحة التحكم، وتقدم وظائف كثيرة للمتجر مثل تعدد العملات، وتخصيص الموقع الجغرافي للمتجر، وتحديد مستويات للوصول إلى البيانات، والتكامل مع بوابات الدفع الإلكتروني وغيرها من الميزات. 15. الحزمة AvoRed تسمح الحزمة AvoRed بتخصيص حزمة تسوق لارافيل مفتوحة المصدر بسهولة وحسب الحاجة، وتقدم افتراضيًا واجهة سهلة الاستخدام في الأجهزة المحمولة وتضم أفضل حزم تحسين محركات البحث في لارافيل. تسمح لك الأداة ببناء كيانات مفيدة مثل الفئات categories والسمات attributes وغيرها، وتقدم إمكانيات فعالة في إدارة طلبات الشراء وتتبّعها وإدارة معلومات العملاء والمتاجر وغيرها. حزم لارافيل لتحسين محركات البحث تساعد هذه الحزم في تحسين تطبيقات لارافيل كي يسهل إيجادها من قبل محركات البحث وذلك من خلال إدارة بيانات الوسوم الوصفية للموقع أو التطبيق وإدارة خارطة الموقع والبيانات المهيكلة. 16. الحزمة Laravel Meta Manager تساعد الأداة Laravel Meta Manager في تحسين ظهور صفحاتنا في محركات البحث ورفع ترتيبها ضمن صفحات نتائج البحث. حيث توضح بيانات الوسوم Meta tag وتسمح بتصحيحها باتباع أفضل الممارسات المتبعة في تحسين محركات البحث. تأتي الحزمة مع مجموعة وسوم ميتا جاهزة وموصى بها تتضمن Standard SEO و Dublin Core و Google Plus و Facebook Open Graph وغيرها. فبعد إتمام الإعداد، كل ما علينا هو إضافة هذه الوسوم المتولّدة إلى ترويسة الصفحة التي تريد كالتالي: @**include**('meta::manager') يستخدم الأمر السابق الإعدادات المعرفة سابقًا لإعادة تعيين الوسوم الوصفية، لكن إن أدرنا تعريف خيارات محددة بسرعة دون تدقيق، نستطيع استخدام الشيفرة التالية: @**include**('meta::manager', [ 'title' => 'My Example Title', 'description' => 'This is my example description', 'image' => 'Url to the image', ]) وإليكم مثالًا: <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> @**include**('meta::manager', [ 'title' => 'My Example Title', 'description' => 'This is my example description', 'image' => '', ]) </head> <body> </body> </html> 17. الحزمة SEOTools تسمح SEOTools بتحسين محركات البحث في تطبيقات لارافيل وفقًا لأفضل الممارسات، وتقدم ميزات تحسين ممتازة لمواقع الويب. ومن السهل دمج الحزمة مع المشاريع ولها واجهة سهلة الاستخدام بالنسبة للمبتدئين، كما تسمح بضبط العناوين والوسوم الوصفية لكل من منصة إكس و Open Graph. 18. الحزمة Laravel-SEO تساعدنا الحزمة Laravel-SEO في التحكم الكامل بقدرة تطبيقنا على تحسين محركات البحث وتبسّط إدارة الوسوم الوصفية من ناحية الإضافة والتحرير والحذف. وتسمح أيضًا بإضافة بيانات مهيكلة لتحسين نتائج محركات البحث وإدارة الوسوم الوصفية للمنصات الشهيرة مثل Open Graph و Dublin Core دون أي جهد، مما يزيد ظهور تطبيق لارافيل ويعزز ترتيبه في نتائج البحث. حزم تنقيح لارافيل تركز هذه الحزم على سلوك التطبيق من خلال أدوات التنقيح وتحليل الأداء. 19. الحزمة Laravel Telescope تُعد الحزمة Laravel Telescope أداة فعالة في يد مطوري لارافيل، إذ تكشف لنا كل ما يحدث خلف الستار، وتتعقب الطلبات القادمة وتنفيذها والسجلات واستعلامات قواعد البيانات وغيرها. وتقدم الأداة ملاحظات مباشرة على سلوك التطبيق أثناء التنفيذ، مما يسمح بتحديد وإصلاح الثغرات بفعالية. تجعل هذه الميزات من Laravel Telescope أداة أساسية لأي مطور لارافيل يعمل في بيئة تطور محلية. حزم اختبار لارافيل تساعدك هذه الحزم على تحسين نوعية الشيفرة وإمكانية صيانتها من خلال إعداد اختبارات وحدات واختبارات تكامل فعالة. 20. الحزمة Orchestral Testbench لن نتمكن من الوصول إلى جميع مساعدي اختبار لارافيل test helpers عندما نكتب الحزمة بنفسنا. فإن أردنا كتابة اختبارات لحزمتنا كما لو كانت ضمن تطبيق لارافيل نمطي ننصح باستخدام Orchestral Testbench package كالتالي: ضبط ملف Composer.json للحزمة الجديدة إضافة مزود خدمة ضبط الأسماء المستعارة Alias إنشاء صنف واجهة facade class هذه الخطوات الأربعة مهمة جدًا في كتابة حزمة لارافيل لتسريع الإنتاجية. الخلاصة قدمنا في هذا المقال قائمة بأفضل حزم لارافيل التي تساعدنا على تحسين إنتاجية تطبيقاتنا. ويعتمد اختيار أفضل حزم لارافيل على متطلبات المشروع بالدرجة الأولى. وطالما أن لارافيل تسهل على المطور تنفيذ عمليات مخصصة يحددونها بأنفسهم، سيكون استخدام تلك الحزم مساعدًا في تنفيذ تلك المهام الوظيفية بفعالية أكبر. أسئلة شائعة 1. ما هي حزم لارافيل؟ الحزم في PHP هي مجموعة من الوجهات routes والمتحكمات controllers والعروض views المهيأة لتوسيع وظائف تطبيق لارافيل. 2. ماهي التجميعة في لارافيل؟ هي تقنية قّدمت أول مرة في لارافيل 3.0، وهي طريقة لتجميع الشيفرة ضمن مكونات يمكن إضافتها إلى تطبيقات لارافيل. ولن يضطر المطور من إنشاء تلك المكونات من الصفر دائمًا عند استخدام التجميعات. ترجمة -وبتصرف- للمقال: Best laravel packages to optimize performance, security and SEO in 2024 اقرأ أيضًا استخدام عمليات CRUD لإنشاء مدونة بسيطة في لارافيل تجريد إعداد قواعد البيانات في لارافيل باستعمال عملية التهجير Migration والبذر Seeder تعرف على مفهوم إطار العمل Framework وأهميته في البرمجة تعرف على لغة PHP
-
نشرح في هذا المقال كيفية استخدام جدول SpriteSheet لتنظيم حركة الشخصية في الألعاب الثنائية الأبعاد ضمن محرك الألعاب جودو، كما نوضح دور المتحكم AnimationTreeState Machine في تنظيم حركة الشخصية والتحكم في عمليات التنقل بين حالات الحركة المتختلفة. الحركات داخل جدول الشخصيات SpriteSheet تُعد جداول الشخصيات Spritesheets من الطرق الشائعة لتوزيع الحركات للرسوم المتحركة ثنائية البعد، إذ توضع جميع إطارات الشخصية ضمن صورة واحدة. سنستخدم في مقالنا شخصية المغامر، ويمكن الحصول عليها وعلى غيرها من الشخصيات من متجر Elthen's Pixel Art Shop الذي يوفر ملحقات مفيدة يمكن استخدامها في تطوير الألعاب. تحذير: علينا التأكد من أن الصور في جدول الشخصيات مرتبة ضمن شبكة ذات حجم ثابت، مما يسمح لمحرك جودو باقتطاعها تلقائيًا، بينما إن وضعنا الشخصيات ضمن الجدول بشكل غير منتظم، فلن نتمكن من استخدام التقنيات التي نشرحها تاليًا. إعداد عقد التحريك تستخدم تقنية التحريك العقدة Sprite2D لعرض الخامة، بعدها نحرّك الإطارات المتغيرة باستخدام العقدة AnimationPlayer. يمكن لهذا الترتيب أن يعمل على أي عقدة ثنائية البعد، لكننا سنستخدم في مثالنا العقدة CharacterBody2D. لنضف العقد التالية إلى المشهد: CharacterBody2D: Player Sprite2D CollisionShape2D AnimationPlayer نسند جدول الشخصيات إلى الخاصية Texture للعقدة Sprite2D وسنلاحظ أن الجدول بكامله قد ظهر في نافذة العرض. ولكي نقطعه إلى إطارات فردية، نوسّع قسم التحريك Animation في نافذة الفاحص وناضبط قيم الخاصيتين Hframes و Vframes على 13 و 8 على الترتيب، وهما يمثلان عدد الإطارات الأفقية والعمودية في جدول الشخصيات: لنجرّب تغيير الخاصية Frame لمراقبة تغيّر الصورة، فهي الخاصية التي سنعمل على تحريكها. تحريك الشخصية سنختار العقدة AnimationPlayer ثم ننقر الزر تحريك Animation يتبعه الزر جديد New ونسمي الحركة الجديدة idle. نضبط بعد ذلك مدة الحركة على 2 ثانية وننقر الزر Loop كي تتكرر الحركة باستمرار. نجعل شريط التقدم Scrubber عند الزمن 0 ثم نختار العقدة Sprite2D. نضبط الخاصية Animation>Frame على 0 ثم ننقر على أيقونة المفتاح إلى جوار القيمة. إن حاولنا الآن تشغيل الحركة فلن نرى شيئًا، لأن الإطار الأخير رقم 12 يبدو مشابهًا للإطار الأول رقم 0. مع ذلك لم نتمكن من رؤية الإطارات بينهما. لإصلاح الأمر نغير الخاصية Update Mode للمسار من القيمة الافتراضية Discrete إلى Continuous وسنجد هذا الزر في نهاية المسار من الجانب الأيمن. نلاحظ أن هذا الحل سيعمل فقط مع جداول الشخصيات، حيث تكون الشخصيات مرتبة مسبقًا، فإن لم يكن الأمر كذلك، علينا ترتيب كل إطار على حدى ضمن المسار. يمكن تجربة وضع حركات أخرى مثل حركة القفز التي نجد صورها في الإطارات من 65 إلى 70. استخدام المتحكم AnimationTreeStateMachine لنتخيل أن لدينا كم كبير من الحركات، وأصبح من الصعب علينا التحكم عملية التنقل فيما بينها، وامتلأ السكريبت بعبارات if وكلما أردنا تصحيح شيء أخفق ما تبقى. لحل الأمر نستخدم العقدة AnimationTree لإنشاء مُتحكّم يسمح لنا بترتيب الحركات المختلفة للشخصية وإدارة عملية التنقل فيما بينها. سنستخدم في مثالنا نفس شخصية المغامر التي استخدمناها في المثال السابق، ونفترض أننا هيأنا مسبقًا حركات الشخصية باستخدام العقدة AnimationPlayer. وعندما نستخدم جدول الشخصيات السابق سنجد صورًا توافق الحركات التالية: سكون idle ركض run هجوم attack1 هجوم attack2 إصابة hurt موت die استخدام شجرة الرسوميات AnimationTree نضيف العقدة AnimationTree إلى المشهد ثم نختار New AnimationNodeStateMachine من الخاصية TreeRoot. تتحكم العقدة AnimationTree بالرسوميات التي تنشأ ضمن العقدة AnimationPlayer، ولكي نسمح لها بالوصول إلى الرسوميات الموجودة، ننقر على الخاصية Assign ضمن الخاصية Anim Player ثم نختار عقدة الحركة. يمكننا الآن إعداد متحكم التنقل ضمن نافذة AnimationTree: ننتبه إلى التحذير الظاهر، ونضبط الخاصية Active في نافذة الفاحص على القيمة On ثم ننقر بعد ذلك بالزر اليميني للفأرة ونختار Add Animation. نختار بعد ذلك الحركة idle وسنرى صندوقًا صغيرًا يمثل هذه الحركة. نكرر نفس العملية لإضافة مثل هذه الصناديق إلى بقية الحركات. سنتمكن الآن من إضافة الاتصالات، لهذا ننقر على زر Connect nodes ثم نتنقل بالسحب بن العقد لوصلها مع بعضها. وكمثال على الاتصال سنستخدم الرسوم المتحركة لحالتي الهجوم: عندما تختار حركة، ستتبع الشجرة المسار الذي يصل العقدة الحالية إلى الوجهة. لكن في طريقة إعداد المثال السابق، لن نرى الهجوم الأول attack1 إن شغلنا الهجوم الثاني attack2. يعود السبب في ذلك إلى أن نمط التبديل switch mode للاتصال نوعه مباشر immediate. لهذا، ننقر على زر Move/select ثم ننقر على الاتصال بين attack1 و attack2 ثم نغير من نافذة الفاحص الخاصية Switch Mode إلى At End ونكرر ذلك على الاتصال بين attack2 و idle. ما يحدث الآن أنه عند تشغيل في AnimationTree، أنه عند الانتقال من idle إلى attack2، يجري تشغيل الحركتين attack1 و attack2 على التتابع، ولكن بعد ذلك تتوقف الرسوم المتحركة عند attack2 بدلاً من العودة تلقائيًا إلى idle. لحل هذه المشكلة، نضبط الخاصية Advance>Mode على Auto مما يسمح للشجرة بالعودة إلى الحركة idle بشكل تلقائي بعد تنفيذ حركتي الهجوم attack، ونلاحظ أن أيقونة الاتصال تتحول إلى اللون الأخضر لإظهار ذلك. وهكذا ستُتنفذ الحركات على التتابع بمجرد تفعيلها. استدعاء الحالات في الشيفرة فيما يلي شجرة الحركات بأكملها: لنهيئ الآن الشخصية كي تستخدم هذه الحركات: extends CharacterBody2D var state_machine var run_speed = 80.0 var attacks = ["attack1", "attack2"] @onready var state_machine = $AnimationTree["parameters/playback"] تضم الخاصية state_machine مرجعًا إلى المتحكم بالحالة وهو AnimationNodeStateMachinePlayback، ولاستدعاء حركة محددة، نستخدم التابع travel الذي سيتّبع الاتصالات إلى الرسم المتحرك المحدد: func hurt(): state_machine.travel("hurt") func die(): state_machine.travel("die") set_physics_process(false) لدينا هنا مثال عن الدوال التي قد نستدعيها، إن أصيب اللاعب أو قتل. وبالنسبة إلى بقية الحالات كالركض والهجوم وغيرها فلا بد من جمعها مع شيفرة الحركة وشيفرة معالجة المدخلات. وستحدد الخاصية velocity إن كنا سنرى حالة حركة الركض run أو حركة السكون idle: func get_input(): var current = state_machine.get_current_node() velocity = Input.get_vector("move_left", "move_right", "move_up", "move_down") * run_speed if Input.is_action_just_pressed("attack"): state_machine.travel(attacks.pick_random()) return # اقلب الشخصية من اليمين إلى اليسار if velocity.x != 0: $Sprite2D.scale.x = sign(velocity.x) # اختر رسمًا متحركًا if velocity.length() > 0: state_machine.travel("run") else: state_machine.travel("idle") move_and_slide() نلاحظ استخدام return بعد الانتقال إلى حركة الهجوم كي نتمكن من الانتقال إلى حالات الحركة أو السكون لاحقًا في الدالة. الخاتمة تعرفنا في هذا المقال على طريقة استخدام SpriteSheet في جودو لتوليد حركات مختلفة للشخصية، كما تعرفنا على استخدام AnimationTree Animation Tree State Machine في إدارة التنقل بين الرسوميات المختلفة للشخصية. وبإمكانك من الاطلاع على المشروع بصيغته المكتملة لتنفيذه وفهمه بصورة أفضل. ترجمة -وبتصرف- للمقالين: SpriteSheet ANimation و Using AnimationTreeStateMachine اقرأ أيضًا الطريقة الصحيحة للتواصل بين العقد في جودو Godot إنشاء وبرمجة مشاهد لعبة ثنائية الألعاب في محرك جودو تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو كتابة سكربتات GDScript وإرفاقها بالعقد في جودو
-
يأتي مصطلح SOLID من الأحرف الأولى للمبادئ الخمسة للتصميم الكائني التوجه Object Oriented Design -أو OOD اختصارًا- والتي وضعها روبرت سي مارتن حيث تؤسس هذه المبادئ لممارسات تطبيقية أثناء تطوير البرمجيات مع الأخذ بعين الاعتبار سهولة الصيانة وقابلية التوسع مع نمو المشروع. ويساعد تبني هذه الممارسات على تفادي مشكلات كتابة الشيفرة وتحسين إعادة إنتاجها وتطوير برمجيات تعتمد نهج Agile أو Adaptive. مبادئ SOLID يرتكز مفهوم على المبادئ الخمس التالية: مبدأ المسؤولية المفردة Single-responsibility Principle الحرف S. مبدأ المفتوح والمغلق Open-closed Principle الحرف O. مبدأ استبدال ليسكوف Liskov Substitution Principle الحرف L. مبدأ عزل الواجهة Interface Segregation Principle الحرف I. مبدأ الاعتماديات المتبادلة Dependency Inversion Principle الحرف D. سنوضح في الفقرات التالية كل مبدأ من هذه المبادئ الخمسة على حدة، ونوضح فائدة SOLID في تطوير منتجات برمجية أفضل. ملاحظة: يمكن تطبيق هذه المبادئ على مختلف لغات البرمجة، لكن شيفرة الأمثلة في هذا المقال مكتوبة بلغة PHP. مبدأ المسؤولية المفردة ينص مبدأ المسؤولية المفردة SRP على مايلي: لنتأمل على سبيل المثال تطبيقًا يتلقى مجموعة من الأشكال هي عبارة عن دوائر ومربعات ويحسب مجموع مساحات الأشكال في هذه المجموعة. لتنفيذ المطلوب، ننشئ أصنافًا للأشكال ونضبط المعاملات المطلوبة لحساب المساحة عبر الدوال البانية constructor funcations. سنحتاج هنا إلى المتغير length لتخزين طول ضلع المربع class Square { public $length; public function construct($length) { $this->length = $length; } } كما سنحتاج إلى المتغير radius لتحديد نصف قطر الدائرة: class Circle { public $radius; public function construct($radius) { $this->radius = $radius; } } ننشئ تاليًا الصنف AreaCalculator ونكتب منطق جمع مساحات جميع الأشكال الموجودة، فمساحة المربع هي مربع طول الضلع ومساحة الدائرة هي مربع نصف القطر مضروبًا بالثابت Pi: class AreaCalculator { protected $shapes; public function __construct($shapes = []) { $this->shapes = $shapes; } public function sum() { foreach ($this->shapes as $shape) { if (is_a($shape, 'Square')) { $area[] = pow($shape->length, 2); } elseif (is_a($shape, 'Circle')) { $area[] = pi() * pow($shape->radius, 2); } } return array_sum($area); } public function output() { return implode('', [ '', 'Sum of the areas of provided shapes: ', $this->sum(), '', ]); } } ولاستخدام الصنف AreaCalculator لا بد من إنشاء نسخة عنه نمرر لها مصفوفة من الأشكال ومن ثم نعرض نتيجة حساب مجموع المساحات. إليك مثالًا عن مجموعة من ثلاث أشكال: دائرة نصف قطرها 2 مربع طول ضلعه 5 مربع طول ضلعه 6 $shapes = [ new Circle(2), new Square(5), new Square(6), ]; $areas = new AreaCalculator($shapes); echo $areas->output(); إن المشكلة في تابع الخرج هي أن الصنف AreaCalculator يعالج منطق إخراج البيانات. فلو تأملنا حالة يطلب فيها تحويل الخرج إلى صيغة أخرى مثل صيغة JSON، سيعالج الصنف AreaCalculator في هذه الحالة منطق تحويل الخرج إلى الصيغة المطلوبة وهذا خرق لمبدأ المسؤولية المفردة. لهذا ينبغي أن يكون الصنف AreaCalculator مسؤولًا فقط عن حساب مجموع مساحات الأشكال ولا يهتم بطريقة إخراج النتائج أيًا كانت. ولحل المشكلة، بإمكاننا إنشاء صنف آخر SumCalculatorOutputter واستخدامه لمعالجة منطق الخرج: class SumCalculatorOutputter { protected $calculator; public function __constructor(AreaCalculator $calculator) { $this->calculator = $calculator; } public function JSON() { $data = [ 'sum' => $this->calculator->sum(), ]; return json_encode($data); } public function HTML() { return implode('', [ '', 'Sum of the areas of provided shapes: ', $this->calculator->sum(), '', ]); } } نستخدم الصنف SumCalculatorOutputter كالتالي: $shapes = [ new Circle(2), new Square(5), new Square(6), ]; $areas = new AreaCalculator($shapes); $output = new SumCalculatorOutputter($areas); echo $output->JSON(); echo $output->HTML(); في هذه الحالة يتولى الصنف SumCalculatorOutputter عملية إخراج البيانات إلى المستخدم ويحقق مبدأ المسؤولية المفردة. مبدأ المفتوح والمغلق ينص المبدأ على ما يلي: ويعني ذلك أن الصنف ينبغي أن يكون قادرًا على التوسع دون الحاجة لتعديل أي شيء موجود فيه. لنعد مجددًا إلى الصنف AreaCalculator ونركز هذه المرة على التابع sum: class AreaCalculator { protected $shapes; public function __construct($shapes = []) { $this->shapes = $shapes; } public function sum() { foreach ($this->shapes as $shape) { if (is_a($shape, 'Square')) { $area[] = pow($shape->length, 2); } elseif (is_a($shape, 'Circle')) { $area[] = pi() * pow($shape->radius, 2); } } return array_sum($area); } } لنتأمل الآن حالة يرغب فيها المستخدم بجمع مساحات أشكال أخرى مثل المثلثات والمخمسات والمسدسات. سيكون علينا في هذا الحالة إضافة شروط جديدة في كتلة if/else، وهذا سيخرق مبدأ المفتوح والمغلق. سيكون أحد الحلول التي تحسن التابع sum هو إزالة منطق حساب مساحة كل شكل وتنفيذ العملية في كل صنف على حدة. نضيف هنا التابع area للصنف square: class Square { public $length; public function __construct($length) { $this->length = $length; } public function area() { return pow($this->length, 2); } } نضيف هنا التابع area للصنف Circle: class Circle { public $radius; public function construct($radius) { $this->radius = $radius; } public function area() { return pi() * pow($shape->radius, 2); } } ويمكن الآن إعادة كتابة التابع sum للصنف AreaCalculator كالتالي: class AreaCalculator { // ... public function sum() { foreach ($this->shapes as $shape) { $area[] = $shape->area(); } return array_sum($area); } } في هذه الحالة بإمكاننا إنشاء أصناف جديدة لأشكال جديدة ثم تمريرها إلى صنف حساب مجموع المساحات دون أن نغير الشيفرة. مع ذلك، ستظهر مشكلة جديدة تتمثل في عدم القدرة على تمييز إن كان الكائن المرر إلى الصنف AreaCalculator هو شكل أو يمتلك تابعًا اسمه area. ولأن بناء واجهة Interface هو جزء أساسي من SOLID، لننشئ واجهة ShapeInterface تملك التابع area: interface ShapeInterface { public function area(); } نعدّل أصناف الأشكال لتتخذ من ShapeInterface واجهة لها، ونبدأ بالصنف Square: class Square implements ShapeInterface { // ... } ثم الدائرة Circle: class Circle implements ShapeInterface { // ... } نتحقق بعد ذلك أثناء تنفيذ التابع sum إن كان للكائن الممرر واجهة شكل، أي أنه نسخة عن ShapeInterface ويرمي استثناء Exceptionخلاف ذلك: class AreaCalculator { // ... public function sum() { foreach ($this->shapes as $shape) { if (is_a($shape, 'ShapeInterface')) { $area[] = $shape->area(); continue; } throw new AreaCalculatorInvalidShapeException(); } return array_sum($area); } } وهكذا نكون قد حققنا مبدأ المفتوح والمغلق. مبدأ استبدال ليسكوف ينص المبدأ على ما يلي: ويعني ذلك أن أي صنف فرعي subclass أو مشتق قابل للاستبدال بواسطة الصنف الأب. لنعد إلى مثالنا السابق ولنفرض وجود صنف جديد VolumeCalculator يوسّع الصنف AreaCalculator: class VolumeCalculator extends AreaCalculator { public function construct($shapes = []) { parent::construct($shapes); } public function sum() { // logic to calculate the volumes and then return an array of output return [$summedData]; } } نتذكر أن الصنف SumCalculatorOutputter هو كالتالي: class SumCalculatorOutputter { protected $calculator; public function __constructor(AreaCalculator $calculator) { $this->calculator = $calculator; } public function JSON() { $data = array( 'sum' => $this->calculator->sum(); ); return json_encode($data); } public function HTML() { return implode('', array( '', 'Sum of the areas of provided shapes: ', $this->calculator->sum(), '' )); } } فلو حاولنا تنفيذ المثال كالتالي: $areas = new AreaCalculator($shapes); $volumes = new VolumeCalculator($solidShapes); $outputArea = new SumCalculatorOutputter($areas); $outputVolume = new SumCalculatorOutputter($volumes); عندما نستدعي التابع ()HTML للكائن outputVoulme$ سنحصل على خطأ E_NOTICE يخبرنا بوجود عملية تحويل من مصفوفة إلى نص. ولحل المشكلة نعيد المتغير summedData$ في الصنف VolumeCalculator بدلًا من إعادة مصفوفة: class VolumeCalculator extends AreaCalculator { public function construct($shapes = []) { parent::construct($shapes); } public function sum() { // logic to calculate the volumes and then return a value of output return $summedData; } } قد يكون المتغير summedData$ من النوع float أو double أو integer وهذا سيحقق مبدأ استبدال ليسكوف. مبدأ فصل الواجهة ينص المبدأ على ما يلي: نوضح هذا المبدأ بمتابعة العمل على مثالنا السابق وننطلق من الواجهة ShapeInterface. سنحتاج الآن إلى دعم ثلاث أشكال ثلاثية الأبعاد هي Cuboid و Spheroid، ونريد حساب حجومها volume. لنتأمل ما قد يحدث إن عدّلنا الواجهة ShapeInterface لإضافة تابع جديد: interface ShapeInterface { public function area(); public function volume(); } في هذه الحالة سيكون كل شكل مجبرًا على تبني تابع الحجم volume، لكن كما نعلم لا أحجام للأشكال ثنائية البعد مثل الدائرة وهذه الواجهة ستجبر الصنف Circle على تنفيذ هذا التابع الذي لا يحتاجه. يُعد هذا الأمر خرقًا لمبدأ عزل الواجهة، وبدلًا من ذلك، بإمكانك إنشاء واجهة جديدة تُدعى ThreeDimensionalShapeInterface تقدم التابع volume وتتبناها الأشكال ثلاثية الأبعاد: interface ShapeInterface { public function area(); } interface ThreeDimensionalShapeInterface { public function volume(); } class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface { public function area() { // calculate the surface area of the cuboid } public function volume() { // calculate the volume of the cuboid } } هذه النهج أفضل بكثير، لكن الأمر سيبدو مربكًا عند توضيح نوع استخدام الواجهة بالتعليقات مثلًا. لهذا، بدلًا من استخدام الواجهتين السابقتين بإمكاننا إنشاء واجهة أخرى مثل ManageShapeInterface وتطبيقها على الأشكال ثنائية وثلاثية البعد. وهكذا سيكون لدينا واجهة برمجية واحدة تدير الأشكال: interface ManageShapeInterface { public function calculate(); } class Square implements ShapeInterface, ManageShapeInterface { public function area() { // calculate the area of the square } public function calculate() { return $this->area(); } } class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface, ManageShapeInterface { public function area() { // calculate the surface area of the cuboid } public function volume() { // calculate the volume of the cuboid } public function calculate() { return $this->area(); } } في هذا النهج، بإمكاننا استبدال التابع area في الصنف AreaCalculatot بالتابع calculate، ونتحقق أيضًا أن الكائن هو نسخة عن الصنف ManageShapeInterface وليس ShapeInterface وهكذا يكون مبدأ فصل الواجهة قد تحقق لدينا. مبدأ الاعتماديات المتبادلة ينص هذا المبدأ على ما يلي: يسمح هذا المبدأ بفصل الشيفرة. ولإيضاح المبدأ لنأخذ مثالًا عن صنف PasswordReminder يربط الشيفرة بقاعدة البيانات MySQL: class MySQLConnection { public function connect() { // handle the database connection return 'Database connection'; } } class PasswordReminder { private $dbConnection; public function __construct(MySQLConnection $dbConnection) { $this->dbConnection = $dbConnection; } } يمثل الصنف MySQLConnection الوحدة البرمجية ذات المستوى الأدنى بينما يمثل الصنف PasswordReminder الوحدة ذات المستوى الأعلى لاعتماد الثاني على الأول. لكن ووفقًا لتعريف المبدأ الأخير الذي ينص على ضرورة الاعتماد على التجريد وليس طريقة التنفيذ، يخرق النهج السابق هذه القاعدة لأن الصنف PasswordReminder مجبر على الاعتماد على الصنف MySQLConnection. وإن حاولت مسبقًا تغيير قاعدة البيانات عليك تعديل الصنف PasswordReminder وفي هذا خرق للمبدأ O أي مبدأ المفتوح والمغلق. لا ينبغي أن يهتم الصنف PasswordReminder بنوع قاعدة البيانات المستخدمة، ولتلافي ذلك، بإمكانك بناء واجهة طالما أن المبدأ الأخير ينص على ضرورة اعتماد وحدات المستوى الأعلى والأدنى على التجريد: interface DBConnectionInterface { public function connect(); } تقدم الواجهة السابقة طريقة للاتصال بقاعدة البيانات ويتبناها الصنف MySQLConnection. وبدلًا من الإشارة إلى الصنف MySQLConnection ضمن بانية PasswordReminder بإمكانك الإشارة إلى DBConnectionInterface. وأيًا كان نوع قاعدة البيانات التي تستخدمها، سيتمكن الصنف PasswordReminder من الاتصال بها دون مشاكل، ولن تخرق القاعدة الثانية (المفتوح والمغلق). class MySQLConnection implements DBConnectionInterface { public function connect() { // handle the database connection return 'Database connection'; } } class PasswordReminder { private $dbConnection; public function __construct(DBConnectionInterface $dbConnection) { $this->dbConnection = $dbConnection; } } وهكذا ستعتمد وحدات المستوى الأعلى والأدنى على التجريد. الخلاصة قدمنا في هذا المقال المبادئ الأساسية الخمسة SOLID في كتابة الشيفرة البرمجية، ومن المفيد اتباعها في المشاريع البرمجية لجعلها تتمتع بإمكانيات المشاركة بين أطراف العمل بسهولة، والتعديل والاختبار وإعادة الإنتاج بأدنى مقدار من التعقيدات. ترجمة-وبتصرف- للمقال: SOLID: the first 5 principles of object oriented design لمؤلفيه Samuel Oloruntoba و Anish Singh Walia اقرأ أيضًا كيف تكتب كود برمجي مثل مهندسي البرمجيات قواعد البرمجة ببساطة للمبتدئين البرمجة بالكائنات Object-Oriented Programming تعلم كتابة أكواد بايثون من خلال الأمثلة العملية ما هي أكواد البرمجة
-
أشرنا في المقال السابق إلى إمكانية استخدام قاعدة البيانات المدمجة في المتصفح IndexedDB في تخزين ما هو أعقد من النصوص واﻷرقام، بل يتعداها إلى إمكانية تخزين أي شيئ تريده بما في ذلك الكائنات ذات البنى المعقدة مثل بيانات الفيديوهات والصور الخام. مع ذلك، ليس من الصعب تخزين واسترجاع هذه البيانات مقارنة بغيرها من البيانات التي تعاملنا معها. ولتوضيح هذا اﻷمر، سنطور مثالًا تطبيقيًا باسم IndexedDB video store بإمكانك الاطلاع على عمله مباشرة. ينزّل هذا التطبيق عند تشغيله جميع مقاطع الفيديو من الشبكة ويخزنها في قاعدة البيانات IndexedDB، ويعرض بعدها هذه المقاطع في واجهة المستخدم ضمن العنصر <video>، وعندما تشغّل التطبيق في المرات القادمة، سيجد التطبيق المقاطع ضمن قاعدة البيانات ويعرضها بدلًا من تنزيلها مجددًا مما يجعل العملية أسرع، ويوفّر استهلاك حزمة البيانات المتاحة للاتصال باﻹنترنت. تطبيق عملي: تخزين فيديو في قاعدة البيانات IndexedDB سنعرض تاليًا اﻷجزاء اﻷكثر أهمية في تطبيقنا، ولن نستعرض كل التفاصيل طبعًا، فالكثير منها مشابه تمامًا لما غطيناه في المقال السابق، إضافة إلى وجود تعليقات كافية ضمن الشيفرة توضح خطوات العمل. نخزن بداية أسماء مقاطع الفيديو التي نريد إحضارها ضمن مصفوفة كائنات كما يلي: const videos = [ { name: "crystal" }, { name: "elf" }, { name: "frog" }, { name: "monster" }, { name: "pig" }, { name: "rabbit" }, ]; ننفذ الدالة ()init عند نجاح الاتصال بقاعدة البيانات. ووظيفة هذه الدالة التنقل بين أسماء مقاطع الفيديو السابقة ومحاولة إيجاد سجل يوافق اسم المقطع ضمن قاعدة بيانات الفيديو. فإن وجد مقطع الفيديو ستكون نتيجة request.result هي true وإلا ستكون undefined. ستمرر الدالة بعد ذلك اسم المقطع إن وجد إلى الدالة ()displayVideo لتضعه ضمن واجهة المستخدم وإلا تستدعي الدالة ()fetchVideoFromNetwork ﻹحضار المقطع من اﻹنترنت. function init() { //تنقل بين أسماء مقاطع الفيديو واحدًا تلو اﻵخر for (const video of videos) { // افتح الاتصال مع قاعدة البيانات واحصل على مخزن الكائنات وكل فيديو فيه const objectStore = db.transaction("videos_os").objectStore("videos_os"); const request = objectStore.get(video.name); request.addEventListener("success", () => { // إن وجد المقطع ضمن قاعدة البيانات if (request.result) { //displayVideo احضر المقطع واعرضه على الواجهة باستخدام الدالة console.log("taking videos from IDB"); displayVideo( request.result.mp4, request.result.webm, request.result.name, ); } else { // احضر مقطع الفيديو من الشبكة fetchVideoFromNetwork(video); } }); } } تحضر الدالة ()fetchVideoFromNetwork مقاطع فيديو من النوعين MP4 و WebM باستخدام الطلب ()fetch، بعدها سنستخدم التابع ()response.blob لاستخلاص جسم كل طلب على شكل كائن بيانات ثنائية blob والذي يعطي كائنًا يمثل مقطع الفيديو، ويمكن تخزينه وعرضه لاحقًا. أما المشكلة التي تواجهنا هنا، أن هذين الطلبين غير متزامنين، لكن ما نريده فعلًا هو عرض أو تخزين المقطع فقط عندما يكتمل الوعد promise. لهذا نستخدم التابع ()promise.all الذي يقبل معاملًا واحدًا وهو مصفوفة مراجع إلى كل الوعود التي تريد التحقق من إكتمالها، ويعيد وعدًا يتحقق عندما تتحقق كل الوعود في المصفوفة. نستدعي ضمن التابع ()then المتعلق بهذا الوعد الدالة ()displayVideo كما فعلنا سابقًا لعرض مقطع الفيديو، ثم نستدعي أيضًا الدالة ()storeVideo لتخزين المقطع في قاعدة البيانات: // fetch() إحضار مقاطع الفيديو باستخدام الدالة // blob تحويل أجسام الاستجابات إلى كائن const mp4Blob = fetch(`videos/${video.name}.mp4`).then((response) => response.blob(), ); const webmBlob = fetch(`videos/${video.name}.webm`).then((response) => response.blob(), ); // نفّذ الشيفرة التالية إن تحقق كلا الوعدان Promise.all([mp4Blob, webmBlob]).then((values) => { //displayVideo() اعرض الفيديو الذي أحضرته من الإنترنت باستخدام الدالة displayVideo(values[0], values[1], video.name); //storeVideo() خزن مقطع الفيديو في قاعدة البيانات باستخدام storeVideo(values[0], values[1], video.name); }); يشبه عمل الدالة ()storeVideo ما رأيناه في المقال السابق عندما أضفنا بيانات إلى قاعدة البيانات، إذ نفتح قناة العمليات readwrite مع القاعدة ونتخذ مرجعًا إلى مخزن الكائن video_os ثم ننشئ كائنًا يمثل السجل الذي نريد إضافته إلى القاعدة ونستخدم بعدها التابع ()IDBObjectStore.add: // storeVideo() تعريف الدالة function storeVideo(mp4, webm, name) { // فتح قناة اتصال قراءة وكتابة مع قاعدة البيانات const objectStore = db .transaction(["videos_os"], "readwrite") .objectStore("videos_os"); //Add() إضافة السجل إلى قاعدة البيانات باستخدام const request = objectStore.add({ mp4, webm, name }); request.addEventListener("success", () => console.log("Record addition attempt finished"), ); request.addEventListener("error", () => console.error(request.error)); } تُنشئ الدالة ()displayVideo عناصر شجرة DOM اللازمة لإدراج مقطع الفيديو في واجهة المستخدم ومن ثم تلحق هذه العناصر بالصفحة. أما النقاط اﻷكثر أهمية، فهي التي نستعرضها تاليًا. لعرض كائن البيانات الثنائية الذي يضم الفيديو داخل العنصر <video>، لا بد من إنشاء كائن عنوان URL أي عناوين داخلية تشير إلى كائن البيانات الثنائية المخزن في الذاكرة باستخدام التابع ()URL.creatObjectURL. بعدها يمكننا أن نجعل تلك العناوين قيمًا للسمات src العائدة للعناصر <source> ويعمل عندها كل شيء كما هو متوقع: //displayVideo() تعريف الدالة function displayVideo(mp4Blob, webmBlob, title) { //blob يشير إلى الكائن URL إنشاء كائن const mp4URL = URL.createObjectURL(mp4Blob); const webmURL = URL.createObjectURL(webmBlob); //لإدراج الفيديو في الصفحة DOM إنشاء عنصر في شجرة const article = document.createElement("article"); const h2 = document.createElement("h2"); h2.textContent = title; const video = document.createElement("video"); video.controls = true; const source1 = document.createElement("source"); source1.src = mp4URL; source1.type = "video/mp4"; const source2 = document.createElement("source"); source2.src = webmURL; source2.type = "video/webm"; //في الشجرة DOM إدراج عنصر section.appendChild(article); article.appendChild(h2); article.appendChild(video); video.appendChild(source1); video.appendChild(source2); } تخزين اﻷصول للعمل دون اتصال بالشبكة عرضنا في المثال السابق طريقة إنشاء تطبيق يُخزّن أصولًا assets في قاعدة البيانات IndexedDB حتى لا نضطر إلى تحميلها مجددًا. ويحسن هذا اﻷمر تجربة المستخدم بشكل ملحوظ. لكن لا تزال بعض الأصول المهمة مفقودة كي يعمل التطبيق وهي ملف HTML الرئيسي وملفات CSS وجافا سكريبت، ولا بد من تنزيلها في كل مرة ندخل فيها إلى الموقع، وبالتالي لن يعمل التطبيق بدون الاتصال باﻹنترنت. وهنا يأتي دور عمّال الخدمة service workers والواجهة البرمجية cache API يُعرف عامل الخدمة service worker في جافا سكريبت على أنه ملف يُسجّل تحت مصدر محدد مثل موقع ويب أو جزء من موقع ويب في نطاق معين عندما يلج إليه متصفح ويب. ويتمكن هذا الملف لكونه مسجلًا على نطاق ما أن يتحكم بالصفحات التي تنتمي إلى نفس اﻷصل أو النطاق. ويتوضع هذا الملف في مكان وسط بين الصفحة التي اكتمل تحميلها وشبكة اﻹنترنت ويعترض طلبات الشبكة التي تحوّل من وإلى ذلك المصدر أو اﻷصل. وعندما يعترض العامل الطلب سيكون بمقدوره تنفيذ أي شيئ تريده على هذا الطلب، لكن استخدامه النمطي هو تخزين الاستجابات على طلبات الشبكة والاستجابة لهذا الطلبات في المرات القادمة بدلًا من الاتصال بالشبكة والحصول على الاستجابة. وكنتيجة ستتمكن من بناء صفحة ويب تعمل كليًا دون اتصال بشبكة اﻹنترنت. أما الواجهة البرمجية Cache API فهي آلية أخرى لتخزين البيانات في طرف العميل، مع اختلاف بسيط هو أنها مخصصة لتخزين الاستجابات على طلبات HTTP، لهذا ستعمل جيدًا مع عمال الخدمة service workers. مثال عن عمال الخدمة لنطرح مثالًا يوضح قليلًا الفكرة السابقة. إذ أنشانا نسخة أخرى من مثال تخزين ملفات الفيديو الذي فصلناه في الفقرة السابقة. وتعمل هذه النسخة بنفس اﻷسلوب ما عدا أنها تخزّن أيضًا ملفات HTML و CSS وجافا سكريبت ضمن Cache API من خلال عامل خدمة وبالتالي سيعمل المثال دون اتصال باﻹنترنت. بإمكانك تجريب هذه النسخة مباشرة على جيت-هاب والاطلاع على الشيفرة المصدرية أيضًا. تسجيل عامل الخدمة أول ما تلاحظه هو وجود شيفرة إضافية في ملف جافا سكريبت. تختبر هذه الشيفرة بداية وجود العضو serviceWorker ضمن الكائن Navigator. فإن كان موجودًا (أعادت الشيفرة القيمة true)، نعلم حينها وجود دعم أساسي لعمال الخدمة في المتصفح. نستخدم التابع ()ServiceWorkerContainer.register لتسجيل عامل الخدمة الموجود في الملف sw.js على المصدر الذي يتواجد فيه، وبالتالي سيكون قادرًا على التحكم بالصفحات الموجودة في نفس المجلد أو المجلدات الفرعية. وعندما يتحقق الوعد سيكون عامل الخدمة قد سُجِّل: // تسجيل عامل الخدمة لتتمكن من تشغيل المثال دون اتصال بالشبكة if ("serviceWorker" in navigator) { navigator.serviceWorker .register( "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js", ) .then(() => console.log("Service Worker Registered")); } ملاحظة: يُعطى مسار الملف sw.js بالنسبة إلى أصل الموقع، وليس بالنسبة إلى ملف جافا سكريبت الذي يضم الشيفرة. فالعامل موجود على العنوان: https://mdn.github.io/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js بينما عنوان اﻷصل هو https://mdn.github.io لذا لابد أن يكون العنوان المعطى عند التسجيل هو: /learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js وإذا أردت استضافة هذا المثال على حاسوبك محليًا، لا بد من تبديل هذه القيم بما يناسب. وعلى الرغم من أنه أمر مربك، لكنه ضروري لأسباب أمنية. تثبيت عامل الخدمة في كل مرة ندخل فيها إلى أحد الصفحات التي تقع تحت سيطرة عامل الخدمة، يُثبَّت عامل الخدمة على هذه الصفحة أي يبدأ العامل بالتحكم فيها. وعندما يحدث ذلك، يقع الحدث install على عامل الخدمة، وستتمكن من كتابة الشيفرة ضمن عامل الخدمة نفسه لتستجيب إلى حدث التثبيت. وإذا ألقينا نظرة على الملف sw.js سنجد أن مترصد حدث التثبيت مسجّل وفق القيمة self أي على العامل نفسه. والتعليمة self طريقة لتشير إلى الطبيعة العامة global scope لعامل الخدمة من داخل ملف عامل الخدمة. نستخدم ضمن دالة المعالج install التابع ()ExtendableEvent.waitUntil العائد إلى كائن الحدث لكي يبلغ المتصفح بعدم تثبيت العامل قبل أن يُنجز الوعد بنجاح. وهنا نجد طريقة عمل الواجهة Cache API الخاصة بعملية التخزين، إذ نستخدم التابع ()CacheStorage.open لفتح كائن تخزين مؤقت جديد cache object لنخزّن ضمنه الاستجابات. وعندما يتحقق الوعد، سيعيد كائن cache يمثل المخزن المؤقت للفيديو video-store. نستخدم بعد ذلك التابع ()Cache.addAll ﻹحضار سلسلة اﻷصول واستجاباتها إلى المخزن المؤقت: self.addEventListener("install", (e) => { e.waitUntil( caches .open("video-store") .then((cache) => cache.addAll([ "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/", "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.html", "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.js", "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/style.css", ]), ), ); }); وهكذا تنتهي عملية التثبيت. الاستجابة إلى طلبات أخرى مع تسجيل عامل الخدمة ليتحكم بصفحة HTML وإضافة كل اﻷصول إلى المخزن المؤقت، سيكون التطبيق جاهزًا للعمل. لكن بقي هناك شيء واحد وهو كتابة شيفرة تستجيب إلى طلبات HTML اﻷخرى التي ترد، وهذا ما يفعله القسم الثاني من الشيفرة في الملف sw.js. نضيف مترصدًا آخر عامًا إلى عامل الخدمة، يعمل على تنفيذ معالج الحدث عندما يقع حدث اﻹحضار fetch. ويقع هذا الحدث في كل مرة يحاول فيها المتصفح إجراء طلب لأحد اﻷصول الموجودة في نفس المجلد الذي يضم عامل الخدمة. نسجّل بداية ضمن دالة المعالج عنوان URL للأصل المطلوب، ثم نهيئ استجابة مخصصة لهذا الطلب باستخدام التابع ()FetchEvent.respondWith. وضمن كتلة التابع السابق نستخدم التابع ()CacheStorage.match للتحقق من وجود طلب موافق لهذا اﻷصل ضمن المخزن المؤقت. ويتحقق الوعد الموافق لهذا الطلب في حال وجود عنوان في المخزن يطابق عنوان URL للطلب وإلا سيعيد الوعد القيمة undefined. نعيد بعد ذلك الطلب على شكل استجابة مخصصة في حال كان العنوان موجودًا وإلا نستخدم الواجهة ()fetch ﻹحضاره من الشبكة كونه غير موجود في المخزن المؤقت: self.addEventListener("fetch", (e) => { console.log(e.request.url); e.respondWith( caches.match(e.request).then((response) => response || fetch(e.request)), ); }); وهكذا نستخدم عامل الخدمة، علمًا أن استخداماته أوسع من ذلك ولا يسعنا تغطيها في هذا المقال. اختبار المثال دون اتصال بالشبكة لاختبار عمل تطبيقنا، لا بد من تحميله عدة مرات للتأكد من تثبيت عامل الخدمة، وبعدها يمكنك: قطع الاتصال باﻹنترنت. اختيار العمل دون اتصال (اختر ملف ثم العمل دون اتصال إن كنت تستخدم فايرفوكس). الانتقال إلى أدوات مطوري ويب واختر تطبيقات ثم عمال الخدمة Service workers، ولا بد من تفعيل الخيار offline إن كنت تستخدم متصفح كروم. لو حاولت اﻵن تحديث الصفحة سترى أنها ستعيد التحميل دون أية مشكلات لأن كل أصول الصفحة قد خُزّنت في المخزن المؤقت cache، كما أن كل مقاطع الفيديو مخزنة ضمن قاعدة البيانات IndexedDB. الخلاصة تعرفنا في هذا المقال على طريقة لتشغيل صفحة ويب دون اتصال باﻹنترنت عن طريقة استخدام عامل خدمة service worker مع الواجهة البرمجية Cache التي تخزّن الأصول ضمن مخازن مؤقتة في حاسوبك. ترجمة -وبتصرف- للجزء الثالث من مقال: Client-side storage اقرأ أيضًا المقال السابق: تخزين البيانات في طرف العميل باستخدام قاعدة البيانات المفهرسة IndexedDB تخزين البيانات في طرف العميل: مخازن ويب Web Storage تخزين البيانات محليا في متصفح الويب عبر جافاسكربت التخزين المحلي (Local Storage) في HTML5 تعرّف على IndexedDB
-
Mohamed Magdy42 بدأ بمتابعة ابراهيم الخضور
-
تُعد الواجه البرمجية IndexedDB أو IDB اختصارًا منظومة قواعد بيانات كاملة مضمنة في المتصفح تساعدك على تخزين بيانات مترابطة معقدة لا تقتصر فيها أنواع البيانات على قيم بسيطة مثل النصوص واﻷعداد. إذ تستطيع تخزين مقاطع الفيديو والصور وتقريبًا أي شيء في نسخ منفصلة من القاعدة IndexedDB. وتتيح لك هذه الواجهة البرمجية إنشاء قاعدة بيانات ومن ثم إنشاء مخازن كائنات object stores ضمن القاعدة. وتُعد مخازن الكائنات بمثابة بنى شبيهة بالجداول الموجودة في قواعد البيانات العلاقية relational databases، ويمكن لأي مخزن أن يضم مخازن كائنات أخرى. وبالطبع تأتي هذه الميزات مع آثار لا بد منها، فالواجهة IndexedDB أكثر تعقيدًا من واجهة مخازن ويب Web Stores من ناحية الاستخدام. لهذا سنحاول في هذا المقال تقديم مثال يعرض جزءًا ضئيًلا جدًا من إمكانات هذه الواجهة، لكنه سيزوّدك باﻷساسيات التي تساعدك على الانطلاق. تطبيق عملي: تخزين ملاحظات سنبني تطبيق بسيط يسمح لك بتخزين ملاحظات في المتصفح ومراجعة هذه الملاحظات وحذفها متى شئت. وسيكون البناء خطوة خطوة نشرح فيها اﻷجزاء اﻷكثر أهمية من قاعدة البيانات IndexedDB. سيبدو شكل التطبيق عند انتهائه كالتالي: تتكون كل ملاحظة من عنوان ومحتوى نصي يمكن تحرير أي منهما بشكل مستقل عن اﻵخر. وستجد ضمن شيفرة جافا سكريبت الخاصة بالتطبيق تعليقات كافية لشرح كل خطوة بالتفصيل. نقطة الانطلاق انسخ بداية الملفات index.html و style.css و index-start.js إلى مجلد جديد تُنشئه على حاسوبك. الق نظرة في البداية على تلك الملفات، وسترى أن ملف HTML يُعرّف موقع ويب له ترويسة وتذييل ومنطقة محتوى رئيسي تُعرض فيه الملاحظات، إضافة إلى نموذج ﻹدخالها في قاعدة البيانات. يقدّم ملف CSS مجموعة من قواعد التنسيق لتوضيح ما يجري، بينما يضم ملف جافا سكريبت تصريحًا عن خمسة ثوابت تضم مراجع إلى العنصر <ul> وستُعرض الملاحظات على شكل عنوان ونص ضمن عنصري إدخال <input> كما ننشئ مرجعًا إلى النموذج <form> بحد ذاته، ومرجعًا إلى زر <button>. غير اسم ملف جافا سكريبت إلى index.js. تهيئة قاعدة البيانات لنلق نظرة اﻵن على ما يتوجب علبنا فعله بداية لإعداد قاعدة البيانات: أضف السطر التالي تحت التصريحات عن الثوابت: // إنشاء نسخة عن كائن قاعدة البيانات let db; نصرّح في هذا السطر عن متغير يُدعى db لنستخدمه لاحقًا في تخزين الكائن الذي يمثل قاعدة البيانات. وطالما أننا نستخدمه في عدة أماكن لذلك صرحنا عنه كمتغير عام global لتسهيل اﻷمر. أضف تاليًا الشيفرة التالية: // فتح قاعدة البيانات مما يؤدي إلى إنشائها إن لم تكن موجودة const openRequest = window.indexedDB.open("notes_db", 1); يُنشئ هذا السطر طلبًا لفتح النسخة 1 من قاعدة بيانات تُدعى notes_db، فإن لم تكن موجودة سوف ينشئها السطر الذي يليه. سترى هذا الشكل من الطلبات كثيرًا عند استخدام IndexedDB. وطالما أن العمليات على قواعد البيانات تحتاج وقتًا، فلا ينبغي ايقاف المتصفح ريثما ننتظر نتيجة الطلب، لهذا عمليات قواعد البيانات هي عمليات غير متزامنة لن تُنفّذ مباشرة، بل في فترة ما مستقبلًا وستُبلَّغ بتنفيذها. ولمعالجة اﻷمر في IndexedDB، ننشئ كائن طلب request object ندعوه مثلًا openRequest، ويمكنك حينها استخدام معالج حدث لتنفيذ شيفرة مخصصة عند اكتمال هذا الطلب أو فشله، وهذا ما ستراه بعد قليل. ملاحظة: إن رقم النسخة أمر مهم. فلو أردت تحديث قاعدة بياناتك (مثل تغيير هيكلية الجدول)، عليك تنفيذ شيفرتك مجددًا بعد زيادة رقم النسخة، وتحديد تخطيط مختلف ضمن معالج الحدث upgradeneeded. لكننا لن نغطي تحديث قاعدة البيانات في هذا المقال. أضف اﻵن معالج الحدث التالي تحت الشيفرة السابقة: // معالج خطأ يحدد حالة فشل الاتصال بقاعدة البيانات openRequest.addEventListener("error", () => console.error("Database failed to open"), ); // معالج نجاح يحدد نجاح فتح قاعدة البيانات openRequest.addEventListener("success", () => { console.log("Database opened successfully"); //db خزن قاعدة البيانات المفتوحة ضمن المتغير db = openRequest.result; //التي تعرض الملاحظات الموجودة في قاعدة البيانات displayData() نفّذ الدالة displayData(); }); يُنفَّذ معالج الحدث error عندما يُبلغك النظام أن طلبك قد أخفق، مما يتيح لك التعامل مع المشكلة. وما فعلناه في مثالنا هو عرض رسالة خطأ في طرفية جافا سكريبت. ويُنفَّذ معالج الحدث success عندما ينجح الطلب، بمعنى أن الاتصال إلى قاعدة البيانات تحقق، وأصبح الكائن الذي يمثّل قاعدة البيانات متاحًا ضمن الخاصية openRequest.result وبالتالي إمكانية التعامل من خلاله مع قاعدة البيانات. نخزّن النتيجة ضمن المتغيّر db وننفذ دالة تُدعى ()displayData وظيفتها عرض البيانات الموجودة في قاعدة البيانات داخل العنصر <ul> بمجرد انتهاء تحميل الصفحة، وسترى تعريف هذه الدالة لاحقًا. في نهاية هذا القسم سنضيف معالج الحدث upgradeneeded الذي يُنفَّذ إن لم تكن قاعدة البيانات قد هُيئت مسبقًا أو عندما تكون قاعدة البيانات مفتوحة. لهذا أضف اﻷسطر التالية في نهاية شيفرتك منتبهًا إلى ضرورة استخدام رقم نسخة أعلى من رقم النسخة المخزنة في قاعدة البيانات عندما تريد تحديث القاعدة: // هيئ جداول قاعدة البيانات إن لم تكن مهيأة مسبقًا openRequest.addEventListener("upgradeneeded", (e) => { // احصل على مرجع إلى قاعدة البيانات المفتوحة db = e.target.result; // أنشئ مخزن كائنات في قاعدة البيانات لتخزين الملاحظة مع مفتاح يزداد تلقائيًا ومخزن الكائنات مشابه للجدول في قاعدة البيانات العلاقية const objectStore = db.createObjectStore("notes_os", { keyPath: "id", autoIncrement: true, }); // حدد ما ستضمه عناصر البيانات ومخازن الكائنات objectStore.createIndex("title", "title", { unique: false }); objectStore.createIndex("body", "body", { unique: false }); console.log("Database setup complete"); }); نعرّف هنا التخطيط الذي نعتمده لقاعدة البيانات، أي نحدد مجموعة الأعمدة أو الحقول التي تضمها. وما فعلناه أننا حددنا مرجعًا في البداية إلى قاعدة البيانات الموجودة عبر الخاصية result لمعالج الحدث e.target.result الذي يمثّل كائن الطلب request. وهذا الأمر مكافئ للسطر: db = openRequest.result; داخل معالج الحدث succes، لكن لا بد هنا من تنفيذه بشكل مستقل لأن معالج الحدث upgradeneeded سيُنفذ إن احتجنا إليه قبل المعالج success، ولن يكون المتغير db متاحًا ما لم نفعل ذلك. نستخدم بعد ذلك التابع IDBDatabase.createObjectStore ﻹنشاء مخزن كائن جديد ضمن قاعدة البيانات المفتوحة يُدعى notes_os ويكافئ هذا المخزن جدولًا مفردًا في قواعد البيانات النمطية. كما خصصنا أيضًا حقلًا مفتاحيًا autoIncrement في هذا الكائن وسميناه id تزداد قيمة هذا الحقل كلما أضفنا ملاحظة جديدة ولا حاجة أن يفعل المطوّر هذا صراحة بل هي عملية آلية. يعمل هذا الحقل كمعرّف فريد لكل سجل وذلك عندما نريد تعديل أو حذف هذا السجل. وأنشأنا أيضًا حقلين مفهرسين آخرين باستخدام التابع ()IDBObjectStore.createIndex وهما title و body كي يضما عنوان الملاحظة ونصها. وسيمثَّل كل منهما على شكل كائن له التخطيط التالي، وذلك عندما نبدأ بإضافة الملاحظات إلى قاعدة البيانات وفق التخطيط الذي وضعناه: { "title": "Buy milk", "body": "Need both cows milk and soy.", "id": 8 } إضافة بيانات إلى قاعدة البيانات لنرى اﻵن كيف يمكننا إضافة سجلات إلى قاعدة البيانات باستخدام النموذج الذي صممناه في صفحتنا. لهذا أضف الأسطر التالية تحت معالج الحدث السابق. تضبط هذه اﻷسطر معالج الحدث submit الذي يستدعي الدالة ()addData عند تسليم النموذج (النقر على زر اﻹرسال): //addData() إنشاء معالج حدث لعملية تسليم النموذج يعمل عند تنفيذ الدالة form.addEventListener("submit", addData); لنعرّف اﻵن الدالة ()addData كالتالي: // التصريح عن الدالة function addData(e) { //نمنع السلوك الافتراضي، فلا نريد تسليم النموذج بالطريقة النمطية e.preventDefault(); // الحصول على القيم التي نريد تخزينها ووضعها ضمن عنصر تخزين const newItem = { title: titleInput.value, body: bodyInput.value }; // فتح قناة كتابة وقراءة إلى قاعدة البيانات const transaction = db.transaction(["notes_os"], "readwrite"); // استدعاء مخزن كائنات موجود بالفعل في قاعدة البيانات const objectStore = transaction.objectStore("notes_os"); //إلى مخزن الكائنات newItem إنشاء طلب ﻹضافة الكائن const addRequest = objectStore.add(newItem); addRequest.addEventListener("success", () => { // مسح النموذج استعدادًا ﻹضافة سجل آخر titleInput.value = ""; bodyInput.value = ""; }); // اﻹبلاغ عن نجاح العملية على قاعدة البيانات عند اكتمالها transaction.addEventListener("complete", () => { console.log("Transaction completed: database modification finished."); //مجددًا displayData تحديث عرض البيانات بعد إضافة السجل الجديد باستدعاء الدالة displayData(); }); transaction.addEventListener("error", () => console.log("Transaction not opened due to error"), ); } لنحاول تفسير هذه الشيفرة كونها معقدة نوعًا ما: تنفيذ الدالة ()Event.preventDefault على كائن الحدث لإيقاف إرسال بيانات النموذج بالطريقة النمطية (لأنها تدفع إلى إعادة تحديث الصفحة وإفساد التأثير المطلوب). إنشاء كائن يمثل السجل الذي نريد إدخاله إلى قاعدة البيانات ونشره بالقيم التي نحصل عليها من عناصر اﻹدخال. ولاحظ أنه لا حاجة لتزويد السجل بقيمة للمعرّف id كما ذكرنا سابقًا بل سيضاف تلقائيًا. فتح عملية القراءة والكتابة readwrite إلى مخزن الكائنات notes_os باستخدام التابع ()IDBDatabase.transaction. يساعد كائن عمليات قاعدة البيانات في الوصول إلى مخزن الكائن لتنفيذ العملية المطلوبة عليه مثل إضافة سجل جديد. الوصول إلى مخزن الكائن باستخدام التابع ()IDBTransaction.objectStore وتخزين النتيجة في المتغير objectStore. إضافة السجل الجديد إلى قاعدة البيانات باستخدام التابع ()IDBObjectStore.add الذي يُنشئ كائن طلب بنفس اﻹسلوب الذي رأيناه سابقًا. إضافة مجموعة من معالجات اﻷحداث إلى الكائن request والكائن transaction لتنفيذ الشيفرة المطلوبة عند النقاط المطلوبة خلال دورة حياة التطبيق. وبمجرد نجاح الطلب، نمسح حقول اﻹدخال في النموذج لتحضيرها لعملية إدخال أخرى. وعند اكتمال العملية، ننفذ الدالة ()displayData مجددًا لتحديث ما يُعرض من ملاحظات في الصفحة. عرض البيانات أشرنا إلى الدالة ()displayData مرتين في تطبيقنا، لهذا من اﻷفضل اﻵن تعريف هذه الدالة. أضف اﻷسطر البرمجية التالية بعد تعريف الدالة السابقة: // displayData() تعريف الدالة function displayData() { // نمحي محتوى عناصر القائمة في كل مرة نحدّث فيها ما يُعرض // وإلا ستتكرر العناصر في كل مرة نجري فيها تحديثًا while (list.firstChild) { list.removeChild(list.firstChild); } // افتح مخزن الكائنات واحصل على مؤشر يتنقل بين عناصره المختلفة const objectStore = db.transaction("notes_os").objectStore("notes_os"); objectStore.openCursor().addEventListener("success", (e) => { // احصل على مرجع إلى هذا المؤشر const cursor = e.target.result; // استمر في تنفيذ الشيفرة طالما هناك عناصر يمكن التنقل بينها if (cursor) { //p وفقرة نصية h3 وعنوان itemlist أنشئ عنصر قائمة //كي تضع ضمنها كل عنصر بيانات سيُعرض // ألحق الفقرة والعنوان بعنصر القائمة ثم ألحقه بالقائمة const listItem = document.createElement("li"); const h3 = document.createElement("h3"); const para = document.createElement("p"); listItem.appendChild(h3); listItem.appendChild(para); list.appendChild(listItem); // ضع البيانات الموجودة في المؤشر ضمن الفقرة النصية والعنوان h3.textContent = cursor.value.title; para.textContent = cursor.value.body; // خزن معرّف عنصر البيانات داخل سمة ضمن عنصر القائمة //كي نعرف إلى أي عنصر تنتمي. وسيفيدنا ذلك عند حذف العناصر listItem.setAttribute("data-note-id", cursor.value.id); // أنشئ زرًا وضعه ضمن كل عنصر قائمة const deleteBtn = document.createElement("button"); listItem.appendChild(deleteBtn); deleteBtn.textContent = "Delete"; // اضبط معالج حدث يحذف عنصر القائمة عند النقر على هذا الزر deleteBtn.addEventListener("click", deleteItem); // انقل المؤشر إلى العنصر التالي cursor.continue(); } else { //إن لم تكن هنالك أية عناصر قائمة `No notes stored` اعرض رسالة if (!list.firstChild) { const listItem = document.createElement("li"); listItem.textContent = "No notes stored."; list.appendChild(listItem); } // إن لم تكن هنالك أية عناصر اخرى للتنقل بينها، ابلغ عن ذلك برسالة console.log("Notes all displayed"); } }); } لنشرح الآن الشيفرة السابقة بشيء من التفصيل: نمحي بداية محتوى العنصر <ul> قبل أن نملأه مجددًا بالمحتوى المُحدَّث، وإلا سينتهي بك اﻷمر إلى قائمة مليئة بالعناصر المكررة التي تُضاف عند كل تحديث. نتخذ مرجعًا إلى مخزن الكائن notes_os باستخدام التابعين ()IDBDatabase.transaction و ()IDBTransaction.objectStore كما فعلنا مع الدالة ()addData، ماعدا أننا نربطهما معًا في سطر واحد هنا. نستخدم تاليًا التابع ()IDBObjectStore.openCursor لطلب للحصول على مؤشر Cursor، وهي بنية تُستخدم للتنقل بين السجلات في مخزن الكائنات. كما نربط معالج حدث النجاح success في نهاية السطر لنجعل الشيفرة أكثر ترابطًا. وعندما يُعاد المؤشر بنجاح يُنفَّذ المعالج. نتخذ مرجعًا إلى المؤشر ذاته (على شكل كائن IDBCursor) باستخدام السطر const cursor= e.target.result نتحقق تاليًا من وجود سجل من مخزن الكائنات ضمن المؤشر ({}if (cursor))، فإن كان اﻷمر كذلك، ننشئ فرعًا في شجرة DOM وننشر ضمنه بيانات السجل ومن ثم نعرضها على الصفحة ضمن العنصر <ul>. كما نضمّن الفرع زرًا يحذف عنصر القائمة الذي يضم بيانات السجل عند النقر على هذا الزر، وذلك بتنفيذ الدالة ()deleteItem التي نتعرف عليها في الفقرة القادمة. في نهاية الكتلة if نستخدم التابع ()IDBCursor.continue لنقل المؤشر إلى السجل التالي في المخزن وتنفيذ محتوى الكتلة if مجددًا إن كان هنالك سجل آخر سيعرض في الصفحة، ومن ثم يتابع المؤشر تفقد وجود سجلات أخرى. عند انتهاء السجلات، يعيد المؤشر القيمة undefined وبالتالي ستعمل الكتلة else هذه المرة. وتتحقق هذه الكتلة من إضافة أية ملاحظات إلى القائمة <ul>، فإن لم تُضاف أية ملاحظات، تعرض رسالة مفادها عدم وجود أية ملاحظات محفوظة. حذف ملاحظة أشرنا سابقا إلى أن النقر على زر الحذف الموجود إلى جوار الملاحظة المعروضة يسبب حذفها. وننفذ ذلك باستخدام الدالة ()deleteitem: // deleteItem() تعريف الدالة function deleteItem(e) { // الحصول على الملاحظة التي ينبغي حذفها على شكل عدد قبل //IDB: IDB key محاولة استخدامها عن طريق الزوج // والانتباه إلى أن القيم حساسة لحالة الأحرف const noteId = Number(e.target.parentNode.getAttribute("data-note-id")); // فتح قناة العمليات مع قاعدة البيانات وحذف الملاحظة التي حصلنا //على رقمها سابقًا عن طريق السمة التي خزنا فيها معرف الملاحظة const transaction = db.transaction(["notes_os"], "readwrite"); const objectStore = transaction.objectStore("notes_os"); const deleteRequest = objectStore.delete(noteId); // اﻹبلاغ عن حذف عنصر القائمة transaction.addEventListener("complete", () => { // حذف العنصر اﻷب للزر وهو في حالتنا عنصر القائمة ولن يُعرض بعدها e.target.parentNode.parentNode.removeChild(e.target.parentNode); console.log(`Note ${noteId} deleted.`); //إن لم تكن هنالك عناصر قائمة `No Notes Stored` عرض رسالة if (!list.firstChild) { const listItem = document.createElement("li"); listItem.textContent = "No notes stored."; list.appendChild(listItem); } }); } نحصل على معّرف السجل الذي نحذفه باستخدام التابع Number(e.target.parentNode.getAttribute('data-note-id'))، وتذكر أن معرّف السجل قد خُزِّن سابقًا ضمن السمة data-note-id لعنصر القائمة <li> عندما عُرض أول مرة. لكن لا بد من تمرير الدالة التي تعطينا قيمة السمة إلى التابع العام ()Number لأنها من النوع النصي وما نريده هو المكافئ العددي للقيمة وإلا لن تميزها قاعدة البيانات التي تتوقع عددًا. نتخذ تاليًا مرجعًا إلى مخزن الكائن مستخدمين اﻷسلوب الذي خبرناه سابقًا ومن ثم التابع ()IDBObjectStore.delete لحذف السجل من قاعدة البيانات بعد أن نمرر له معرف الملاحظة. عند اكتمال العملية على قاعدة البيانات، نحذف العنصر <li> من شجرة DOM ونتحقق مجددًا من خلو القائمة <ul> من العناصر. إن واجهت صعوبة في تطبيق المثال، قارن بين نسختك والنسخة المكتملة كما يمكنك أيضًا الاطلاع على الشيفرة المصدرية. الخلاصة تعرفنا في هذا المقال على قاعدة البيانات المدمجة في المتصفح IndexedDB وكيفية التعامل معها من خلال مثال تطبيقي يعرض أساسيات إضافة وحذف البيانات واسترجاعها. ترجمة -وبتصرف- للجزء الثاني من مقال: Client-side storage اقرأ أيضًا المقال السابق: تخزين البيانات في طرف العميل: مخازن ويب Web Storage التحقق من صحة بيانات استمارة ويب في طرف العميل نظرة على تفاعلات الخادم مع العميل في موقع ويب ديناميكي الواجهات البرمجية للتعامل مع الصوت والفيديو في جافا سكريبت
-
تقدم المتصفحات الحديثة مجموعة من التقنيات المختلفة التي تسمح بتخزين بيانات تتعلق بمواقع الويب ثم استرجاعها عند الضرورة، مما يسمح لنا بالحفاظ على البيانات لفترة أطول أو تخزينها للعمل دون اتصال باﻹنترنت وغير ذلك. لهذا سنناقش في مقالنا أبسط اﻷساسيات المتعلقة بهذا اﻷمر وكيفية عملها. ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي HTML و CSS. تكون ملمًا بلغة جافا سكريبت. تطلع على سلاسل المقالات السابقة التي ناقشت أساسيات جافا سكريبت والكائنات في جافا سكريبت. تطلع على أساسيات الواجهات البرمجية في طرف العميل. ماذا يعني تخزين البيانات في طرف العميل؟ تحدثنا في مقالات مختلفة عن الفرق بين مواقع الويب الساكنة والدينياميكية، لكن لا بد من اﻹشارة إلى أن معظم مواقع الويب الحديثة ديناميكية، فهي تخزن البيانات في الخادم مستخدمة نوعًا من قواعد البيانات (تخزين في طرف الخادم)، ومن ثم تنفّذ شيفرة في طرف الخادم لاستعادتها ووضعها ضمن قوالب صفحات ساكنة، لتقدّم النتيجة بعدها إلى العميل على شكل صفحاتHTML يعرضها المتصفح. ويعمل التخزين في طرف العميل وفق اﻷسلوب ذاته، لكن له استخدامات خاصة. ولتنفيذ هذه العمليات نحتاج إلى واجهات جافا سكريبت التي تسمح لنا بتخزين البيانات على جهاز العميل واستعادتها عند الحاجة. وتُخّزن البيانات في طرف العميل لاستخدامات محددة مثل: إضفاء خصوصية للمستخدم في الموقع (مثل عرض خيارات يفضلها المستخدم أو اختيار اللون أو حجم الخط). تخزين النشاطات السابقة للمستخدم مثل تخزين محتوى قائمة مشتريات من جلسة سابقة أو تذكر تسجيل الدخول السابق). تخزين البيانات واﻷصول محليًا لتسريع تحميل الموقع (مع احتمال انخفاض كلفة التصفح) وإمكانية التصفح دون الاتصال بالانترنت. تخزين صفحات الويب التي تولدها تطبيقات الويب ديناميكيًا محليًا لاستخدامها دون اتصال بالانترنت. تُستخدم الطريقتان السابقتان في التخزين معًا عادةً، فقد تحمّل مثلًا ملف موسيقى (يُستخدم مع لعبة ويب أو مشغّل موسيقى) ومن ثم تخزّنه في قاعدة بيانات طرف العميل ومن ثم تشغيله عند الحاجة. وهكذا يمكن للمستخدم تحميل الملف لمرة واحدة، وعند الزيارات اللاحقة للموقع يستخرج الملف من قاعدة بياناته المحلية مما يسرع العملية ويقلل تكلفة التصفح. ملاحظة: هناك حد لكمية البيانات التي يمكن تخزينها في طرف العميل عبر الواجهات البرمجية (منفصلة أو مجتمعة) ويختلف هذا الحد وفقًا للمتصفح، وقد يعتمد على اﻹعدادات التي يضبطها المستخدم. استخدام الطريقة التقليدية: ملفات تعريف الارتباط Cookies تُعد تقنية تخزين البيانات في طرف العميل تقنية قديمة، فقد استخدمت المواقع ملفات تعريف الارتباط cookies منذ البدايات الأولى للويب، وذلك لتخزين البيانات وإعطاء طابع شخصي للموقع. وقد كانت أولى أشكال تخزين البيانات في طرف العميل. أما حاليًا، فقد ظهرت تقنيات أفضل وأحدث لتخزين البيانات في طرف العميل، لذلك لن نتحدث عن استخدام ملفات تعريف اﻹرتباط في مقالنا الحالي. ولا يعني ذلك بالطبع أن ملفات تعريف الارتباط عديمة الفائدة في عالم الويب المعاصر، إذ لا تزال شائعة الاستخدام في تخزين البيانات المتعلقة بمعلومات المستخدم الشخصية وحالته مثل معرفات الجلسة session IDs ومفاتيح الوصول المشفرة access token. التقنية الجديدة: مخازن ويب وقاعدة البيانات IndexedDB من الميزات السهلة للتقنيتين اللتين يشير إليهما العنوان نجد: اﻵلية التي تقدمها واجهة مخازن ويب البرمجية Web Storage API في تخزين واسترجاع عناصر البيانات صغيرة الحجم والمكونة من اسم وقيمة موافقة. ولهذا اﻷمر أهميته عندما تحتاج إلى تخزين بيانات بسيطة مثل اسم المستخدم وتاريخ تسجيل الدخول إلى موقع الويب واللون الذي يفضله للخلفية وهكذا. قاعدة البيانات المتكاملة التي تقدمها الواجهة البرمجية IndexedDB API للمتصفح لتخزين البيانات الأكثر تعقيدًا. يمكن استخدام هذه القاعدة مثلًا في تخزين بيانات مجموعة كاملة من سجلات المستخدمين وحتى أنواع معقدة من البيانات مثل ملفات الصوت والفيديو. الواجهة البرمجية Cache صُممت هذه الواجهة لتحزين الاستجابات الناتجة عن طلبات HTTP محددة، وهي مفيدة خصوصًا في أمور مثل تخزين أصول موقع ويب محليًا ليتمكن الموقع من استخدامها باستمرار دون اتصال مع شبكة اﻹنترنت. تُستخدم واجهة التخزين المؤقت cache عادةً بمرافقة واجهة عمال الخدمة Service Worker API على الرغم من عدم الحاجة إلى ذلك فعليًا. ويُعد استخدام واجهة التخزين المؤقت مع واجهة عمال الخدمة موضوعًا متقدمًا لن نغطيه في سلسلة مقالاتنا بالتفصيل، مع ذلك، سنعرض في آخر مقال من هذه السلسلة مثالًا عنها. الواجهة Web Storage وتخزين بيانات بسيطة من السهل جدًا استخدام هذه الواجهة، إذ تخزن البيانات البسيطة على شكل أزواج مكونة من اسم name وقيمة value (محدودة بأنواع مخصصة مثل النصوص واﻷعداد وغيرها)، ومن ثم استرجاع تلك القيم عند الحاجة. الصياغة القواعدية الأساسية لنلق نظرة على ذلك: انتقل بداية إلى القالب الموجود على جيت-هاب وافتحه في نافذة جديدة. افتح طرفية جافا سكريبت في المتصفح. توضع مخازن ويب ضمن بُنى تشبه الكائنات في المتصفح هي sessionStorge و localStorage. تبقى البيانات المخزنة في البنية اﻷولى طالما أن المتصفح يعمل (تُحذف هذه البيانات عند إغلاق المتصفح)، بينما تبقى البيانات في البنية الثانية مقيمة في الذاكرة حتى بعد إغلاق المتصفح. سنستخدم في مقالنا البنية الثانية لأنها أكثر فائدة عمومًا. إذ يسمح التابع ()Strorage.setItem بتخزين البيانات في البنية Storge، وله معاملان: اﻷول هو اسم العنصر، والثاني هو القيمة. جرّب كتابة ما يلي في طرفية جافا سكريبت: localStorage.setItem("name", "Chris"); يأخذ التابع ()Storage.getItem معاملًا واحدًا يمثل عنصر البيانات الذي تريد استرجاع قيمته. جرّب اﻵن الشيفرة التالية: let myName = localStorage.getItem("name"); myName; سترى عند كتابتك الشيفرة السابقة كيف سيضم المتغير myName قيمة عنصر البيانات name. يأخذ التابع ()removeItem معاملًا واحدًا هو اسم عنصر البيانات التي تريد إزالته ومن ثم يزيله من مخزن ويب. جرّب الشيفرة التالية في طرفية جافا سكريبت: localStorage.removeItem("name"); myName = localStorage.getItem("name"); myName; من المفترض أن يعيد تنفيذ السطر الثالث القيمة null للعنصر name لأنه لم يعد موجودًا في مخزن ويب. البيانات المقيمة في الذاكرة من الميزات المهمة لمخازن ويب أن البيانات تبقى موجودة في الفترة التي تُحمّل فيها الصفحات وحتى بعد إغلاق المتصفح عند استخدام local Storage، لنلق نظرة على هذا اﻷمر: افتح مجددًا قالب مخازن ويب السابق لكن في متصفح يختلف عن المتصفح الذي تقرأ فيه هذا المقال. اكتب الشيفرة التالية في طرفية جافا سكريبت لهذا المتصفح: localStorage.setItem("name", "Chris"); let myName = localStorage.getItem("name"); myName; من المفترض أن ترى قيمة عنصر البيانات name. اغلق اﻵن المتصفح ثم افتحه مجددًا. اكتب الشيفرة التالية في طرفية جافا سكريبت: let myName = localStorage.getItem("name"); myName; سترى أن قيمة عنصر البيانات لا تزال متوفرة على الرغم من إغلاق المتصفح وفتحه مجددًا. مخزن منفصل لكل نطاق يُوجد مخزن بيانات منفصل لكل نطاق (لكل عنوان ويب حمّله المتصفح)، وسترى ذلك إن حمّلت موقعين وحاولت تخزين عنصر بيانات في أحدهما، فلن يكون هذا العنصر متاحًا للموقع اﻵخر. وهذا اﻷمر منطقي، فرؤية بيانات موقع من موقع آخر مصدر للكثير من الثغرات اﻷمنية. مثال على مخزن ويب بتفاصيل أكثر سنبني في هذه الفقرة مثالًا نطبق فيه ما تعلمناه ويعطيك فكرة عن كيفية استخدام مخزن ويب. ندخل في هذا المثال اسمًا ثم نُحدّث الصفحة بعد ذلك لترحب بصاحب الاسم شخصيًا. وستبقى هذه الحالة خلال إعادة تحميل الصفحات أو المتصفح لأننا سنخزن الاسم في مخزن ويب. بإمكانك إيجاد نسخة عن ملف HTML المستخدم على جيت-هاب، ويتضمن موقع ويب يتكون من ترويسة ومحتوى وتذييل ونموذج ﻹدخال الاسم. سنبني المثال اﻵن حتى نفهم آلية عمله: انسخ ملف المثال إلى مجلد على حاسوبك. لاحظ كيف يشير ملف HTML إلى ملف جافا سكريبت يُدعى index.js من خلال سطر يشبه السطر <script src="index.js" defer></script>. علينا إذًا إنشاء الملف index.js ضمن نفس المجلد الذي يضم ملف HTML وكتابة شيفرة جافا سكريبت ضمنه. نبدأ الشيفرة ببناء مراجع إلى جميع عناصر HTML التي نريد التعامل معها في مثالنا، وستكون هذه المراجع على شكل ثوابت لأننا لن نغيرها خلال دورة حياة التطبيق. أضف اﻵن الشيفرة التالية: // إنشاء الثوابت المطلوبة const rememberDiv = document.querySelector(".remember"); const forgetDiv = document.querySelector(".forget"); const form = document.querySelector("form"); const nameInput = document.querySelector("#entername"); const submitBtn = document.querySelector("#submitname"); const forgetBtn = document.querySelector("#forgetname"); const h1 = document.querySelector("h1"); const personalGreeting = document.querySelector(".personal-greeting"); علينا اﻵن كتابة مترصد أحداث بسيط لمنع النموذج من تسليم محتوياته عند النقر على زر اﻹرسال، فهذا ليس السلوك الذي نريده. أضف الشيفرة التالية تحت الشيفرة السابقة: // Stop the form from submitting when a button is pressed form.addEventListener("submit", (e) => e.preventDefault()); يجب إضافة الدالة التي تتعامل مع حدث النقر على الزر "Say hello"، وستجد شرحًا وافيًا ضمن تعليقات الشيفرة لكل خطوة، لكن ما تفعله الشيفرة عمومًا هو الحصول على الاسم الذي ندخله ضمن صندوق اﻹدخال النصي وتخزينه في مخزن ويب باستخدام الدالة ()setItem ثم تنفيذ الدالة ()nameDisplayCheck التي تعالج عملية تحديث النص المطلوب من الصفحة. أضف الشيفرة التالية أسفل الشيفرة السابقة: //`Say hello` نفذ الدالة عند النقر على الزر submitBtn.addEventListener("click", () => { // احفظ الاسم في مخزن ويب localStorage.setItem("name", nameInput.value); //لعرض التحية المخصصة nameDisplayCheck استخدم الدالة nameDisplayCheck(); }); نحتاج إلى معالج حدث للتعامل مع النقر على الزر "Forget" الذي يظهر فقط بعد النقر على الزر "Say hello". كما نزيل في دالة معالج الحدث العنصر name من مخزن ويب باستخدام التابع ()removeItem ثم ننفذ مجددًا الدالة ()nameDisplayCheck لتحديث ما يُعرض. أضف الآن الشيفرة التالية: //`Forget` نفّذ الدالة عند النقر على الزر forgetBtn.addEventListener("click", () => { //إزالة الاسم المخزن في مخزن ويب localStorage.removeItem("name"); //لعرض التحية الأصلية وتحديث ما يُعرض nameDisplayCheck استخدم الدالة nameDisplayCheck(); }); نعرّف اﻵن الدالة ()nameDisplayCheck التي نتحقق فيها فيما لو خُزِّن العنصر name في مخزن ويب باستخدام التابع ('name('localStorage.getItem من خلال عبارة شرطية. فإن وُجد في المخزن، ستكون نتيجة الشرط true وإلا ستكون false. نعرض في الحالة اﻷولى رسالة الترحيب الخاصة ونعرض الجزء "forget" من النموذج ونخفي الجزء "Say hello"، أما في الحالة الثانية، سنعرض الرسالة الأصلية ونجري عكس ما فعلناه في الحالة الأولى: // nameDisplayCheck() نعرّف الدالة function nameDisplayCheck() { // نتحقق من تخزين عنصر الاسم في مخزن ويب if (localStorage.getItem("name")) { // نعرض رسالة الترحيب المخصصة إن كان الأمر كذلك const name = localStorage.getItem("name"); h1.textContent = `Welcome, ${name}`; personalGreeting.textContent = `Welcome to our website, ${name}! We hope you have fun while you are here.`; //`forget` من الاستمارة ونعرض الجزء `remember` نخفي الجزء forgetDiv.style.display = "block"; rememberDiv.style.display = "none"; } else { // إن لم يكن الايم مخزنًانعرض الرسالة اﻷصلية h1.textContent = "Welcome to our website "; personalGreeting.textContent = "Welcome to our website. We hope you have fun while you are here."; //`remember` من الاستمارة ونعرض الجزء `forget` نخفي الجزء forgetDiv.style.display = "none"; rememberDiv.style.display = "block"; } } ننفذ الدالة ()nameDisplayCheck عند اكتمال تحميل الصفحة. لأن الرسالة المخصصة لن تظهر إن لم نفعل ذلك خلال تحميل الصفحة بشكل متكرر. أضف ما يلي إلى آخر الشيفرة: nameDisplayCheck(); وهكذا يكون مثالنا قد انتهي، وبإمكان الاطلاع في أي وقت على النسخة المكتملة منه على جيت-هاب. ملاحظة: تمنع السمة defer في السطر التالي من تنفيذ شيفرة جافا سكريبت حتى اكتمال تحميل الصفحة <script src="index.js" defer></script>. الخلاصة تعرفنا في هذا المقال على أساسيات تخزين البيانات في طرف العميل من خلال واجهات برمجية مخصصة مثل Web Storage API و IndexedDB. كما شرحنا مخازن ويب Web Storage وطريقة العمل معها من خلال مثال تطبيقي بسيط يعرض أساسيات العمل مع هذه الواجهة البرمجية. ترجمة -وبتصرف- للجزء الأول من مقال: Client-side storage اقرأ أيضًا المقال السابق: الواجهات البرمجية للتعامل مع الصوت والفيديو في جافا سكريبت معالجة المشاكل الشائعة للتوافق مع المتصفحات في شيفرة جافاسكربت الواجهة البرمجية fetch في JavaScript تخزين البيانات محليًا في المتصفح عبر قاعدة البيانات IndexedDB
-
تضم لغة HTML عناصر مخصصة لتضمين الوسائط المتعددة إلى صفحاتك مثل <audio> و <video> والتي تأتي مزوّدة بواجهة برمجية مخصصة للتحكم بتشغيلها وتقديمها وتأخيرها. لهذا سنتعرف في هذا المقال على طرق تنفيذ بعض المهام الشائعة كإنشاء أدوات تحكم متخصصة باستخدام الواجهة البرمجية HTMLMediaElement التي توفر عدة ميزات للتحكم في تشغيل الصوت و الفيديو برمجيًا . ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي HTML و CSS. تكون ملمًا بلغة جافا سكريبت. تطلع على سلاسل المقالات السابقة التي ناقشت أساسيات جافا سكريبت والكائنات Prototype في جافا سكريبت. تطلع على أساسيات الواجهات البرمجية في طرف العميل. عناصر HTML الخاصة بالصوت والفيديو يسمح العنصران <video> و <audio> بإدراج مقاطع الصوت و الفيديو في صفحات الويب، وستبدو الطريقة النمطية ﻹنجاز اﻷمر كالتالي: <video controls> <source src="rabbit320.mp4" type="video/mp4" /> <source src="rabbit320.webm" type="video/webm" /> <p> Your browser doesn't support HTML video. Here is a <a href="rabbit320.mp4">link to the video</a> instead. </p> </video> تعرض الشيفرة السابقة مشغل فيديو في صفحتك كالتالي: وأكثر ما يثير الاهتمام في الشيفرة السابقة هي السمة controls التي تعرض أدوات التحكم الافتراضية مع مشغل الفيديو، وإن لم تستخدم هذه السمة فلن ترى عناصر التحكم على المشغل: لهذه اﻷدوات إيجابيات، لكن من أبرز مشكلاتها هي اختلافها من متصفح إلى آخر وهذا أمر مربك إن حاولت دعم عدة متصفحات في شيفرتك. ومن المشكلات الكبيرة أيضًا أن أدوات التحكم اﻷصلية في معظم المتصفحات لا تدعم التحكم من خلال لوحة المفاتيح. ويمكن حل كلتا المشكلتين السابقتين بإخفاء أدوات التحكم الأصلية (عن طريق إزالة السمة controls) وبرمجة أدوات تحكم خاصة بك باستخدام HTML و CSS وجافا سكريبت. وسنلقي نظرة في اﻷقسام التالية على اﻷدوات البسيطة المتاحة لهذا الغرض. الواجهة البرمجية HTMLMediaElement تزّودك بعض مواصفات الواجهة البرمجية HTMLMediaElement بميزات تسمح لك بالتحكم في مشغلات الصوت و الفيديو برمجيًا مثل التوابع ()HTMLMediaElement.play و ()HTMLMediaElement.pause وغيرها. وهاتان الواجهتان متاحتان للاستخدام مع العنصرين <audio> و<video> فهما متطابقان من ناحية العمل. لهذا سنوضح طريقة استخدام هذه الواجهات من خلال المثال التالي: يبدو مثالنا عندما يكتمل مشابهًا للتالي: نقطة الانطلاق حتى نبدأ العمل، عليك تنزيل الملف المضغوط الخاص بالمثال ثم تستخرج محتوياته في مجلد جديد على حاسوبك. أما إن حمّلت مستودع اﻷمثلة بأكمله، ستجد المثال في المسار javascript/apis/video-audio/start/. إن حمّلت المثال على متصفحك سترى مشغل فيديو HTML نمطي مع أدوات التحكم الافتراضية. التعرف على ملف HTML عندما تفتح الملف index.html سترى عددًا من العناصر، وستلاحظ أن معظم الشيفرة تدور حول مشغل الفيديو وأدوات التحكم الخاصة به: <div class="player"> <video controls> <source src="video/sintel-short.mp4" type="video/mp4" /> <source src="video/sintel-short.webm" type="video/webm" /> <!-- fallback content here --> </video> <div class="controls"> <button class="play" data-icon="P" aria-label="play pause toggle"></button> <button class="stop" data-icon="S" aria-label="stop"></button> <div class="timer"> <div></div> <span aria-label="timer">00:00</span> </div> <button class="rwd" data-icon="B" aria-label="rewind"></button> <button class="fwd" data-icon="F" aria-label="fast forward"></button> </div> </div> وضعنا المشغل بأكمله داخل العنصر <div> حتى يُنسّق بالكامل عند الحاجة. يحتوي العنصر <video> على عنصرين من النوع <source> كي نتمكن من تحميل تنسيقات مختلفة لمقطع الفيديو وفقًا للمتصفح الذي نستخدمه. وربما تكون أدوات تحكم HTML هي اﻷكثر أهمية هنا: لدينا أربعة أزرار <button> لتشغيل وإيقاف العرض مؤقتًا واﻹطفاء والتقديم للأمام والعودة للخلف. خصصنا لكل زر سمات هي اسم صنف التنسيق class و data-icon لتحديد اﻷيقونة التي تُعرض على الزر (سنرى كيف ننفذ ذلك لاحقًا) و aria-label لتقديم وصف مفهوم عن عمل كل زر، وذلك لأننا لا نقدم عنوانًا مقروءًا ضمن وسم العنصر. ويُقرأ محتوى السمة aria-label من قبل قارئات الشاشة عندما ينتقل التركيز إلى العناصر التي تمتلك هذه السمة. تضم الصفحة أيضًا مؤقتًا ضمن عنصر <div> يعرض الوقت المنقضي من مقطع الفيديو، ولتحسين تجربة المستخدم، زودنا المثال بآليتين لتحديد الوقت المنقضي: الأولى ضمن عنصر <span> يعطي الوقت المنقضي بالدقائق والثواني، والثانية ضمن عنصر <div> يضم شريط أفقي يزداد طوله عندما يتقدم عرض الفيديو. ولكي تأخذ فكرة عما ستكونه الصفحة بشكلها الكامل ألق نظرة هنا.. التعرف على ملف CSS افتح اﻵن ملف CSS وألقِ نظرة عليه. لا يبدو هذا الملف معقدًا، لكننا سنشير إلى النقاط المهمة فيه. لاحظ بداية القاعدة controls.: .controls { visibility: hidden; opacity: 0.5; width: 400px; border-radius: 10px; position: absolute; bottom: 20px; left: 50%; margin-left: -200px; background-color: black; box-shadow: 3px 3px 5px black; transition: 1s all; display: flex; } .player:hover .controls, .player:focus-within .controls { opacity: 1; } بدأنا بالخاصية visibility لمجموعة أدوات التحكم المخصصة وضبطناها على hidden، لكننا سنضبطها لاحقًا عبر جافا سكريبت لتكون visible ونزيل السمة controls من العنصر <video>. وذلك كي يبقى المستخدم قادرًا على تشغيل الفيديو باستخدام أدوات التحكم الافتراضية في حال فشل تحميل شيفرة جافا سكريبت لسبب ما. منحنا أدوات التحكم قتامة افتراضية opacity قيمتها 0.5، كي لا تشتت الانتباه عند عرض الفيديو، لكن عندما تمرر الفأرة فوق المشغّل أو تمنحه تركيز الدخل ستكون اﻷدوات كاملة القتامة. نرتب اﻷزرار ضمن شريط التحكم باستخدام تخطيط الصندوق المرن display: flex لتسهيل ضبط مواقعها. لنلق نظرة تاليًا على أيقونات اﻷزرار: @font-face { font-family: "HeydingsControlsRegular"; src: url("fonts/heydings_controls-webfont.eot"); src: url("fonts/heydings_controls-webfont.eot?#iefix") format("embedded-opentype"), url("fonts/heydings_controls-webfont.woff") format("woff"), url("fonts/heydings_controls-webfont.ttf") format("truetype"); font-weight: normal; font-style: normal; } button:before { font-family: HeydingsControlsRegular; font-size: 20px; position: relative; content: attr(data-icon); color: #aaa; text-shadow: 1px 1px 0px black; } استخدمنا بداية في أعلى ملف CSS الكتلة font-face@ لاستيراد خط ويب مخصص، وهذا الخط هو عبارة عن أيقونات بدلًا من الحرف اﻷبجدية وتستخدم لعرض أيقونات مختلفة يشيع استخدامها في التطبيقات. نولد بعد ذلك محتوىً خاصًا لعرض اﻷيقونات على كل زر: نستخدم المحدد before:: لعرض المحتوى قبل كل زر <button>. نستخدم الخاصية content لضبط المحتوى الذي يعرض في كل حالة ليكون نفسه محتوى السمة data-icon. ففي حالة زر التشغيل مثلًا، سيكون محتوى السمة data-icon هو المحرف P (بشكله الكبير). نطبق خط ويب السابق على اﻷزرار باستخدام الخاصية font-family، وسيكون الحرف P في هذا الخط عمليًا أيقونة التشغيل، وهكذا ستظهر على زر التشغيل أيقونة التشغيل. إن الخطوط التي تعرض أيقونات جميلة ومفيدة ﻷسباب عديدة منها تقليل عدد طلبات HTTP لانك لن تحتاج إلى تحميل تلك اﻷيقونات على شكل ملفات صور، إضافة إلى إمكانية تكبير وتصغير اﻷيقونات بدقة وكذلك إمكانية استخدام خاصيات نصية لتنسيق تلك اﻷيقونات مثل color و text-shadow. لنلق نظرة أيضًا على تنسيق المؤقت الزمني: .timer { line-height: 38px; font-size: 10px; font-family: monospace; text-shadow: 1px 1px 0px black; color: white; flex: 5; position: relative; } .timer div { position: absolute; background-color: rgb(255 255 255 / 20%); left: 0; top: 0; width: 0; height: 38px; z-index: 2; } .timer span { position: absolute; z-index: 3; left: 19px; } ضبطنا قيمة الخاصية flex للعنصر timer. الخارجي على القيمة 5 لتشغل أكبر مساحة من شريط التحكم. كما ضبطنا خاصية الموقع بالشكل position:relative كي نتمكن من ضبط العناصر ضمن العنصر الخارجي كما نشاء وبالنسبة إلى حدوده وليس حدود العنصر <body>. ضبطنا موقع العنصر <div> الداخلي ليكون مطلقًا position:absolute لكي يظهر مباشرة في أعلى العنصر <div> الخارجي. كما ضبطنا قيمة اتساع العنصر على الشكل width:0 كي لا يُرى العنصر إطلاقًا. وعندما يبدأ العرض نستخدم جافا سكريبت لزيادة اتساع العنصر. ضطنا موقع العنصر <span> ليكون مطلقًا وبالتالي سيكون بالقرب من الطرف اﻷيسر لشريط المؤقت. ضبطنا خاصية العلو z-index للكائن <div> الداخلي والكائن <span> كي يُعرض الشريط الزمني في الأعلى وتحته العنصر <div> الداخلي، ونضمن بذلك أنك سترى كل المعلومات ولن يَحجِب صندوق آخر. إنجاز شيفرة جافا سكريبت بعد أن حضرنا واجهتي HTML و CSS، لا بد من كتاب شيفرة اﻷزرار المخصصة للتحكم بمشغل الفيديو. أنشئ ملف جافا سكريبت جديد في نفس المجلد الذي يضم الملف index.html وسمِّه custom-player.js. ضع الشيفرة التالية أعلى الملف: const media = document.querySelector("video"); const controls = document.querySelector(".controls"); const play = document.querySelector(".play"); const stop = document.querySelector(".stop"); const rwd = document.querySelector(".rwd"); const fwd = document.querySelector(".fwd"); const timerWrapper = document.querySelector(".timer"); const timer = document.querySelector(".timer span"); const timerBar = document.querySelector(".timer div"); أنشأنا في الشيفرة السابقة ثوابت لتكون مراجع إلى الكائنات التي نريد التعامل معها، ولدينا ثلاثة مجموعات: العنصر <video> وشريط التحكم. أزرار التحكم "تشغيل/إيقاف مؤقت play/pause" و "للأمام rewind" و "للخلف fast forward". غلاف المؤقت الخارجي <div> والعنصر <span> الذي يعرض المؤقت والعنصر <div> الخارجي الذي يزداد اتساعه عندما يتقدم الفيديو. ضع اﻵن الشيفرة التالية تحت سابقتها: media.removeAttribute("controls"); controls.style.visibility = "visible"; تزيل الشيفرة السابقة مشغل الفيديو الافتراضي الخاص بالمتصفح ويُظهر أدوات التحكم المخصصة: تشغيل وإيقاف الفيديو مؤقتًا سننجز اﻵن شيفرة التحكم بزر التشغيل و اﻹيقاف المؤقت: أضف بداية الشيفرة التالية في أسفل الشيفرة كي تُستدعى الدالة ()playPauseMedia عند النقر على زر التشغيل: play.addEventListener("click", playPauseMedia); ولتعريف الدالة ()playPauseMedia، أضف الشيفرة التالية إلى أسفل الشيفرة السابقة: function playPauseMedia() { if (media.paused) { play.setAttribute("data-icon", "u"); media.play(); } else { play.setAttribute("data-icon", "P"); media.pause(); } } نستخدم هنا عبارة if للتحقق من توقف تشغيل الفيديو، وتعيد الخاصية HTMLMediaElement.paused القيمة true عند توقف التشغيل مؤقتًا بما في ذلك عند ضبطته على القيمة 0 بعد تحميله أول مرة. عند ذلك نضبط قيمة السمة data-icon لزر التشغيل على u التي تعرض بدورها أيقونة التشغيل المؤقت عليه، وتستدعي التابع ()HTMLMediaElement.play لتشغيل الفيديو. وعند النقر على الزر مرة ثانية سيعود الزر كما كان، إذ تظهر أيقونة التشغيل وسيتوقف الفيديو بتنفيذ التابع ()HTMLMediaElement.paused. إيقاف عرض الفيديو نضيف بداية الشيفرة التي تتعامل مع إيقاف تشغيل الفيديو تحت الشيفرة السابقة: stop.addEventListener("click", stopMedia); media.addEventListener("ended", stopMedia); يضيف سطري الشيفرة مترصدي أحداث addEventListener للتعامل مع الحدث click الذي يوقف تشغيل الفيديو بتنفيذ الدالة ()stopMedia عند النقر على زر اﻹيقاف. ولا بد من إيقاف التشغيل أيضًا عند إنتهاء المقطع، لهذا نترصد أيضًا الحدث ended من خلال مترصد الحدث الثاني والذي ينفذ أيضًا الدالة ()stopMedia عند انتهاء مقطع الفيديو. نعرّف تاليًا الدالة ()stopMedia، بإضاف اﻷسطر التالية بعد الدالة ()playpauseMedia: function stopMedia() { media.pause(); media.currentTime = 0; play.setAttribute("data-icon", "P"); } وبما أن الواجهة البرمجية HTMLMediaElement لا تقدم تابعًا مخصصًا ﻹيقاف عرض الفيديو، سنستخدم التابع ()pause ﻹيقاف التشغيل مؤقتًا ثم نضبط قيمة الخاصية currentTime على القيمة 0 ليعود الفيديو إلى البداية. فضبط قيمة هذه الخاصية (بالثواني) سينقل الموقع الحالي للفيديو إلى النقطة الزمنية المحددة. يبقى علينا فقط إظهار أيقونة التشغيل على زر التشغيل. وبصرف النظر عن وضع الفيديو سواءً كان قيد التشغيل أو أوقف مؤقتًا عند النقر على زر إيقاف التشغيل "Stop"، لابد أن تُظهر أن المشغل جاهز للعمل مجددًا. التنقل بالفيديو إلى اﻷمام والخلف ستجد العديد من الطرق لتقديم أو إعادة المشغل إلى نقطة زمنية محددة، وما سنعرضه حاليًا طريقة معقدة نوعًا ما في تنفيذ الأمر لتفادي اﻷخطاء التي قد تحدث عند النقر على أزرار مختلفة بترتيب غير متوقع. أضف مترصدي الحدث التاليين تحت تعريف المترصدين السابقين: rwd.addEventListener("click", mediaBackward); fwd.addEventListener("click", mediaForward); أضف الدالتين ()mediaBackWard و ()mediaForWard التاليتين تحت الدوال السابقة، وستصبح الشيفرة كالتالي: let intervalFwd; let intervalRwd; function mediaBackward() { clearInterval(intervalFwd); fwd.classList.remove("active"); if (rwd.classList.contains("active")) { rwd.classList.remove("active"); clearInterval(intervalRwd); media.play(); } else { rwd.classList.add("active"); media.pause(); intervalRwd = setInterval(windBackward, 200); } } function mediaForward() { clearInterval(intervalRwd); rwd.classList.remove("active"); if (fwd.classList.contains("active")) { fwd.classList.remove("active"); clearInterval(intervalFwd); media.play(); } else { fwd.classList.add("active"); media.pause(); intervalFwd = setInterval(windForward, 200); } } هيأنا أولًا متغيرين intervalFwd و intervlRwd وسترى عملهما لاحقًا، كما ستلاحظ أن عمل الدالتين ()mediaBackWard و ()mediaForWard متطابق لكن بترتيب معكوس: يجب تصفير اﻷصناف والمجالات التي ضبطناها عند تنفيذ وظيفة التقديم السريع للأمام، لأننا لو نقرنا على زر rwd بعد النقر على الرز fwd من المفترض أن نلغي أي إعدادات خاصة بالتقديم السريع للمشغل fwd واستبدالها بإعدادت التراجع rwd، لأن المشغل سيخفق لو حاولنا النقر على كلا الزرين في نفس الوقت. استخدمنا عبارة if للتحقق من ضبط صنف الزر rwd ليكون active في إشارة إلى أن الزر قد نُقر للتو. ويتمتع كل عنصر بالخاصية classlist، وهي خاصية مفيدة تضم كل الأصناف التي يمتلكها العنصر وتقدم توابع ﻹزالة وإضافة اﻷصناف. وقد استخدمنا التابع ()classList.contains للتحقق من جود الصنف active ضمن أصناف الزر، وتعيد قيمة منطقية true/false. في حال كان active أحد أصناف العنصر rwd نزيله باستخدام التابع ()classList.remove ثم نلغي قيمة الفاصل الزمني الذي ضُبط مسبقًا عندما نقرنا على الزر ومن ثم نستخدم التابع ()HTMLMediaElement.play ﻹلغاء العودة للخلف وتشغيل الفيديو بشكل طبيعي. إن لم يمتلك الزر تلك الخاصية نضيفها إليه باستخدام التابع ()clasList.add ومن ثم نوقف الفيديو مؤقتًا باستخدام التابع ()HTMLMediaElement.pause. نضبط بعدها قيمة المتغير intervalRwd ليعادل القيمة المعادة من استدعاء الدالة ()setInterval. تُحدد هذه الدالة فترة زمنية معينة تنفذ بعد انقضائها الدالة التي تُمرر إليها كمعامل أول أما الفترة الزمنية فيحددها المعامل الثاني بالميلي ثانية. وهنا ننفذ الدالة كل 200 ميلي ثانية كي نعيد مشغل الفيديو إلى الخلف بوتيرة ثابتة. ولكي نوقف تنفيذ الدالة ()setInterval نستدعي الدالة ()clearIterval ممرين لها المتغير intervalRwd (الذي أسندت إليه الدالة ()setInterval). عرفنا أخيرًا الدالة ()windBackwrd والدالة ()windForward اللتان تمررا إلى ()setInterval، لهذا أضف الشيفرة التالية تحت الدوال السابقة: function windBackward() { if (media.currentTime <= 3) { rwd.classList.remove("active"); clearInterval(intervalRwd); stopMedia(); } else { media.currentTime -= 3; } } function windForward() { if (media.currentTime >= media.duration - 3) { fwd.classList.remove("active"); clearInterval(intervalFwd); stopMedia(); } else { media.currentTime += 3; } } سنشرح تاليًا الدالة الأولى فقط لكون الدالتين متطابقتان من ناحية الشيفرة ومتعاكستان عملًا. وما فعلناه في الدالة ()windBackward هو التالي (تذكر أنه بمجرد تفعيل الفاصل الزمني الذي سيتراجع فيه المشغل إلى الخلف ستُستدعى هذه الدالة كل 200 ميلي ثانية): نبدأ الشيفرة بالعبارة if التي تتحقق أن المدة المنقضية من المقطع أقل من 3 ثانية، أي سيعود المشغل عند تراجعه إلى ما قبل نقطة البداية، وهذا ما يسبب سلوكًا غريبًا للمشغل. فلو كانت الحالة كذلك، نوقف تشغيل المقطع باستدعاء الدالة ()stopMedia ومن ثم نزيل الصنف active من قائمة أصناف الزر rwd ونمحي قيمة المتغير intervalRwd ﻹيقاف عملية التراجع. وفي حال أهملنا هذه الخطوة اﻷخيرة سيستمر المشغل بالتراجع إلى ما لا نهاية. إن كان الوقت المنقضي أكبر من 3 ثانية، نزيل ثلاث ثوانٍ من الوقت الحالي باستخدام التعليمة media.currentTime -=3، أي نعيد مشغل الفيديو إلى ما قبل ثلاث ثوان وذلك كل 200 ميلي ثانية. تحديث الوقت المنقضي آخر ما سننفذه ﻹنجاز أدوات التحكم المخصصة بمشغل الفيديو هو تحديد الوقت المنقضي من زمن المقطع. لذا نشغّل دالة تحدّث الوقت الذي نعرضه في كل مرة يقع فيها الحدث timeupdate المرتبط بالعنصر <video>. أما تواتر عملية وقوع هذا الحدث، فتعتمد على المتصفح وقوة معالج جهازك. أضف اﻵن السطر التالي الذي يعرّف مترصد تحديث زمن التشغيل: media.addEventListener("timeupdate", setTime); ولتعريف الدالة ()setTime، أضف مايلي في أسفل ملف جافا سكريبت: function setTime() { const minutes = Math.floor(media.currentTime / 60); const seconds = Math.floor(media.currentTime - minutes * 60); const minuteValue = minutes.toString().padStart(2, "0"); const secondValue = seconds.toString().padStart(2, "0"); const mediaTime = `${minuteValue}:${secondValue}`; timer.textContent = mediaTime; const barLength = timerWrapper.clientWidth * (media.currentTime / media.duration); timerBar.style.width = `${barLength}px`; } هذه الدالة طويلة، لهذا سنناقشها خطوة خطوة: نعمل بداية على تحديد الدقائق والثواني المنقضية من خلال قيمة HTMLMediaElement.currentTime. نهيئ بعد ذلك متغيرين إضافيين هما minuteValue و secondValue، ثم نستخدم التابع ()padStart لكي نمثّل قيمة الدقائق والثواني على شكل محرفين فقط حتى لو كانت القيمة رقمًا وحيدًا. أما الوقت الفعلي الذي سيُعرض فهو قيمة المتغير minuteValue تليه نقطتان متعامدتان ثم قيمة المتغير secondValue. نضبط قيمة المؤقت Node.textContent لتعادل قيمة الوقت الحالي وبالتالي ستُعرض هذه القيمة على واجهة المشغّل. نحدد طول عنصر <div> الداخلي (الذي سيعرض شريط تقدم مقطع الفيديو) من خلال تحديد اتساع عنصر <div> الخارجي (نأخذها من الخاصية clientWidth) ومن ثم ضرب هذه القيمة بالوقت الحالي HTMLMediaElement.currentTime ونقسم على المدة الكلية لمقطع الفيديو HTMLMediaElement.duration. نضبط قيمة اتساع العنصر <div> الداخلي ليعادل طول شريط تتبع تقدم الفيديو بعد إضافة القيمة "px" كي تشير إلى الاتساع مقدرًا بالبكسل. إصلاح مشكلات التشغيل واﻹيقاف المؤقت للفيديو هنالك مشكلة واحدة ولا بد من حلها. فعند النقر على زر التشغيل أو إيقاف الفيديو وزر التقديم أو التراجع، فلن يعمل هذا الزر! وما علينا إصلاحه هنا هو إلغاء وظائف التقدم أو التراجع عند النقر على زر التشغيل لمتابع العمل كما هو متوقع، وهذا أمر سهل. أضف بداية الشيفرة التالية ضمن الدالة ()stopMedia: rwd.classList.remove("active"); fwd.classList.remove("active"); clearInterval(intervalRwd); clearInterval(intervalFwd); أضف اﻵن نفس اﻷسطر في بداية الدالة ()playpauseMedia (وقبل عبارة if). يمكنك اﻵن ازالة نفس الأسطر من الدالتين ()windBackwrd و ()windForward لأننا وضعنا هذه الوظيفة المشتركة بينهما في الدالة ()stopMedia. ملاحظة: يمكنك تحسين فعالية الشيفرة أكثر من خلال إنشاء دالة منفصلة تضم اﻷسطر السابقة ومن ثم استدعاء هذه الدالة عند الحاجة بدلًا من تكرار اﻷسطر عدة مرات في الشيفرة. الخلاصة تعلمنا في هذا المقال ما يكفي عن الواجهة HTMLMediaElement التي تقدم كما كبيرًا من الوظائف ﻹنشاء مشغل وسائط متعددة، وما رأيناه هو مجرد جزء ضئيل من إمكانياتها. إليك أخيرًا بعض الاقتراحات التي تساعد في تحسين مثالنا: يختل عمل المؤقت إن كانت مدة المقطع أكثر من ساعة (فلن يعرض الساعات بل الدقائق والثواني فقط). هل يمكنك تعديل الشيفرة لتعرض الساعات أيضًا؟ يمتلك العنصر <audio> نفس وظائف HTMLMediaElement وبالتالي يمكنك تشغيل المقاطع الصوتية بسهولة، جرب لك. هل يمكنك إيجاد طريقة الانتقال إلى مكان ما من المقطع بالنقر على شريط تقدم الفيديو (العنصر <div> الداخلي). وكتلميح يمكنك إيجاد x و y لزوايا الشريط من خلال التابع ()getBoundingClientRect وإيجاد إحداثيي موقع مؤشر الفأرة من خلال كائن الحدث الذي ينتج عن حدث النقر على المستند. إليك مثالًا: document.onclick = function (e) { console.log(e.x, e.y); }; ترجمة -وبتصرف- لمقال: Video and audio APIs اقرأ أيضًا المقال السابق: العمل مع واجهات الرسوميات البرمجية في جافا سكريبت: الحلقات والرسوم المتحركة إضافة مقاطع الفيديو عبر العنصر <video> في HTML5 إضافة محتوى سمعي ومرئي في صفحة HTML تأثيرات التمرير في صفحات الويب باستخدام Javascript وCSS
-
غطينا في مقالنا السابق بعض أساسيات الرسوم ثنائية البعد ضمن العنصر <canvas>، لكنك لن تلمس عمليًا فعالية هذا العنصر ما لم ترى قدرته على تحريك الرسوم. إذ يقدم هذا العنصر إمكانية إنشاء صور ورسومات باستخدام سكربتات مخصصة، لكن إن لم يكن هدفك تحريك أي شيء، عليك استخدام صور ثابتة لتوفر على نفسك عناء العمل. إنشاء الحلقات في Canvas لن يكون صعبًا التعامل مع الحلقات في <canvas>، وما عليك سوى استخدام تعليمات هذا العنصر (التوابع والخاصيات) داخل حلقة for أو غيرها من الحلقات كغيرها من شيفرات جافا سكريبت. لهذا سنعطي مثالًا تطبيقيًا عن الموضوع: أنشئ نسخة جديدة عن القالب الرسومي الذي أنشأناه في المقال السابق وافتحه ضمن محرر الشيفرة الذي تستخدمه. أضف السطر التالي إلى أسفل ملف جافا سكريبت، ويتضمن هذا السطر تابعًا جديدًا هو ()translate الذي يحرّك نقطة المبدأ في لوحة الرسم: ctx.translate(width / 2, height / 2); يسبب ذلك تحريك نقطة المبدأ (0,0) إلى مركز اللوحة بدلًا من كونها في الزاوية العليا اليسارية. ولهذا اﻷمر فائدته في الكثير من الحالات كما في مثالنا، إذ نريد أن يكون التصميم منسوبًا إلى مركز اللوحة. أضف اﻵن الشيفرة التالية: function degToRad(degrees) { return (degrees * Math.PI) / 180; } function rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } let length = 250; let moveOffset = 20; for (let i = 0; i < length; i++) {} سننجز هنا نفس الدالة ()degToRad التي رأيناها في مثال المثلث في مقالنا السابق، ونستخدم الدالة التي تعيد رقمًا عشوائيًا بين حدين علوي وسفلي معينين. إضافة إلى ذلك ننشئ المتغيرين length و moveOffset (سنرحهما لاحقًا)، كما نستخدم حلقة for فارغة. ما سنفعله هنا هو رسم شيء ما ضمن اللوحة لكن ضمن الحلقة for ثم نكرر ما نفعله عدة مرات. أضف اﻵن الشيفرة التالية داخل الحلقة for: ctx.fillStyle = `rgb(${255 - length} 0 ${255 - length} / 90%)`; ctx.beginPath(); ctx.moveTo(moveOffset, moveOffset); ctx.lineTo(moveOffset + length, moveOffset); const triHeight = (length / 2) * Math.tan(degToRad(60)); ctx.lineTo(moveOffset + length / 2, moveOffset + triHeight); ctx.lineTo(moveOffset, moveOffset); ctx.fill(); length--; moveOffset += 0.7; ctx.rotate(degToRad(5)); في كل تكرار: نضبط fillStyle ليكون ظلًا بنفسجيًا شفافًا قليلًا ويتغير كل مرة وفقًا لقيمة المتغير length. وكما سترى سيقل الطول في كل تكرار وبالتالي سيكون أثر ذلك على اللون الذي يصبح أكثر لمعانًا مع كل مثلث يُرسم على التتابع. نبدأ مسار الرسم. ننقل قلم الرسم إلى اﻹحداثي (moveOffset, moveOffset) ويُعرّف هذا المتغير المسافة التي يجب أن نحركها في كل مرة نرسم فيها مثلًا جديدًا. نرسم خطًا إلى اﻹحداثي (moveOffset+length, moveOffset) وهذا الخط طوله قيمة المتغير length ويوازي المحور x. نحسب ارتفاع المثلث كما فعلنا في المقال السابق. نرسم خطًا نحو رأس المثلث المتجه نحو الأسفل ومن ثم خطا إلى نقطة بداية المثلث. نستدعى التابع ()fill لملء المثلث. نحدّث قيمة المتغيرات التي تصف سلسلة المثلثات التي نرسمها كي نتمكن من رسم المثلث التالي. نخفض قيمة المتغير length بمقدار 1 وبالتالي سيصغر المثلث كل مرة. كما نزيد قيمة moveOffset بمقدار صغير كي يكون كل مثلث أبعد قليلًا عن سابقه. ونستخدم التابع ()rotate الذي يسمح لنا تدوير اللوحة بأكملها، حيث ندورها بمقدار خمس درجات قبل أن نرسم المثلث التالي. هذا كل ما في اﻷمر، وستبدو نتيجة مثالنا كالتالي: نشجعك اﻵن على إجراء تغييرات في هذا المثال وتجرّب ما تعلمته. إذ يمكنك مثلًا: أن ترسم مربعًا أو قوسًا بدلًا من المثلث. أن تغير قيمة المتغير length أو moveOffset. تغيير اﻷرقام العشوائية التي نولدها باستخدام الدالة ()rand التي وضعناها في الشيفرة السابقة ولم نستخدمها. ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. الرسوم المتحركة ما فعلناه في مثالنا السابق أمر جميل، لكن ما تحتاجه حقيقة حلقة ثابتة تستمر وتستمر في أي تطبيق فعلي يعتمد على عنصر <canvs> (مثل اﻷلعاب الإلكترونية، والعرض البصري المباشر). فلو فكرت بلوحة الرسم على أنها فيلم سينمائي، ستعرف أنه عليك تحديث اﻹطارات المعروضة بشكل مستمر وبمعدل 60 إطار في الثانية (القيمة المثالية) كي يبدو المشهد المتحرك ناعمًا ومريحًا للعين البشرية. تقدم لك جافا سكريبت مجموعة من الدوال التي تسمح لك بتنفيذ دوال أخرى بشكل متكرر عدة مرات في الثانية الواحدة. ونجد أنسب هذه الدوال لمثالنا الدالة ()window.requestAnimationFrame التي تأخذ معاملًا واحدًا وهو اسم الدالة التي نريد استدعاءها. فإن رسمت هذه الدالة تحديثًا جديدًا من سلسلة الرسوم المتحركة التي سنعرضها، عليك حينها استدعاء الدالة ()window.requestAnimationFrame مجددًا قبل نهاية الدالة المنفذة للرسم كي تستمر حلقة الرسم. تنتهي الحلقة عندما تتوقف عن استدعاء ()window.requestAnimationFrame أو عند استدعاء الدالة ()window.caancelAnimationFrame بعد استدعاء ()window.requestAnimationFrame وقبل البدء برسم اﻹطار (الذي سيكون اﻷخير حينها). ملاحظة:من الممارسات الجيدة استدعاء الدالة ()window.caancelAnimationFrame من شيفرتك الرئيسية عند الانتهاء من الرسم، كي تضمن عدم وجود أية تحديثات أخرى يمكن أن تُعرض على اللوحة. يُنفّذ المتصفح التفاصيل المعقدة للعملية مثل تحريك الرسوم بمعدل ثابت، والتأكد من عدم تنفيذ رسوميات لا تُرى. ولكي تتعرف على عمل المتصفح، سنلقي نظرة على مثالنا السابق "الكرات القافزة المرتدة" (يمكنك تجربتها مباشرة أو الاطلاع على الشيفرة المصدرية😞 function loop() { ctx.fillStyle = "rgb(0 0 0 / 25%)"; ctx.fillRect(0, 0, width, height); for (const ball of balls) { ball.draw(); ball.update(); ball.collisionDetect(); } requestAnimationFrame(loop); } loop(); نشغل الدالة()loop مرة واحدة في آخر الشيفرة لنبدأ دورة الرسوميات برسم اﻹطار المتحرك الأول ومن ثم تتولى الدالة ()loop مسؤولية استدعاء الدالة (loop(requestAnimationframe التي ستحضر وترسم اﻹطار الثاني من الرسوم المتحركة وتكرر اﻷمر حتى النهاية. وتجدر ملاحظة أننا نمسح اللوحة تمامًا عند رسم كل إطار ومن ثم نعيد رسم كل شيء. إذ نرسم كل كرة ونحدّث موقعها ونتحقق فيما لو اصطدمت بكرة أخرى. وبمجرد أن ترسم شيئًا الى اللوحة، لن تتمكن من تعديله بشكل مستقل كما هو الحال مع عناصر شجرة DOM. ولن تستطيع أيضًا تحريك كل كرة بمفردها ضمن اللوحة، لأنك بمجرد رسم الكرة ستصبح جزءًا من اللوحة وليست كائنًا مستقلًا يمكنك التعامل معه. لهذا عليك مسح وإعادة رسم العناصر، إما بمسح اﻹطار بأكمله وإعادة رسم كل شيء أو كتابة شيفرة تحد تمامًا الجزء الذي يجب مسحه وبالتالي إعادة الرسم في المنطقة المحددة من اللوحة. لهذا يُعد تحسين الرسوم المتحركة اختصاصًا برمجيًا بحد ذاته، ويتطلب استعمال العديد من التقنيات الذكية المتاحة. لكن هذا اﻷمر خارج إطار مقالنا والمثال الذي نعمل عليه. وعمومًا، تتطلب عملية تنفيذ رسوم متحركة ضمن اللوحة الخطوات التالية: مسح محتوى اللوحة باستخدام ()fillRect أو ()clearRect. تخزين الحالة عند الضرورة باستخدام ()save، وذلك عندما تحتاج إلى حفظ اﻹعدادات التي حدّثتها في اللوحة قبل الاستمرار، وللأمر فائدته في التطبيقات المتقدمة. رسم الأشياء التي تريد تحريكها. استعادة الإعدادات التي خزنتها في الخطوة الثانية باستخدام ()restore. استدعاء الدالة ()requestAnimationFrame لجدولة رسم اﻹطار التالي من الرسم المتحرك. ملاحظة: لن نغطي الدالتين ()save و ()restore في مقالنا. تحريك شخصية بسيطة سننشئ اﻵن رسمًا متحركًا خاصًا بنا، تمشي الشخصية المقتبسة من أحد ألعاب الحاسوب القديمة خلال عرض الرسم المتحرك عبر الشاشة. أنشئ نسخة جديدة من القالب الذي نستخدمه في أمثلتنا وافتحه في محرر اﻷلعاب. حدّث شيفرة HTML حتى تعكس الصورة: <canvas class="myCanvas"> <p>A man walking.</p> </canvas> أضف اﻷسطر التالية إلى نهاية ملف جافا سكريبت كي تكون نقطة المبدأ منتصف لوحة الرسم.: ctx.translate(width / 2, height / 2); ننشئ تاليًا كائن HTMLImgeElement ونضبط قيمة الخاصية src له كي تكون عنوان الصورة التي نريد تحميلها ثم نضيف معالجًا للحدث onload الذي يستدعي الدالة ()draw عند اكتمال تحميل الصورة: const image = new Image(); image.src = "walk-right.png"; image.onload = draw; سنضيف اﻵن بعض المتغيرات التي تتعقب موقع الشخصية في اللوحة وعدد الشخصيات التي نرسمها على اللوحة: let sprite = 0; let posX = 0; سنشرح تاليًا صورة الشخصية المأخوذة من التطبيق Walking cycle using CSS animation تتضمن الصورة ست شخصيات تمثل تسلسل حركة الشخصية. عرض صورة كل شخصية 102 بكسل وارتفاعها 148 بكسل. ولرسم كل شخصية على حدة، علينا استخدام التابع ()drawImage لاقتصاص صورة واحدة للشخصية وعرض هذا الجزء فقط، كما فعلنا مع شعار فايرفوكس في مثال سابق. وينبغي ضرب اﻹحداثي X للشريحة بالعدد 102 ويبقى اﻹحداثي Y مساويًا للصفر، وستبقى أبعاد الشريحة دائمًا 102x148 بكسل. سنضع اﻵن شيفرة الدالة ()draw في اﻷسفل لكي نزودها بالشيفرة اللازمة: function draw() {} أما بقية الشيفرة في هذا القسم فستكون ضمن الدالة ()draw. لهذا أضف اﻷسطر التالية التي تمسح اللوحة وتعدها لرسم كل إطار. وانتبه إلى ضرورة تخصيص الزاوية العليا اليسارية من المربع لتكون (width/2, height/2) لأننا اتخذنا مركز اللوحة نقطة البداية. ctx.fillRect(-(width / 2), -(height / 2), width, height); نرسم تاليًا الصورة باستخدام الدالة drawImage التي تقبل تسع معاملات: ctx.drawImage(image, sprite * 102, 0, 102, 148, 0 + posX, -74, 102, 148); وكما ترى: خصصنا image لتكون الصورة التي نرسمها. يحدد المعاملان 2 و 3 إحداثيا الزاوية العليا اليسارية من الشريحة التي نريد اقتصاصها من الصورة المصدرية، ويكون X هو قيمة المتغير sprite مضروبًا بالعدد 102 (حيث يمثل المتغير عدد الشخصيات الموجودة في الصورة من 0 إلى 5) بينما تبقى قيمة Y هي 0. يحدد المعاملان 4 و 5 أبعاد الشريحة التي نقتصها وهي 102x148 بكسل. يحدد المعاملان 6 و 7 الزاوية العليا اليسارية من الصندوق الذي نرسم ضمنه الشخصية، وتكون قيمة اﻹحداثي X هي 0 + posX وبالتالي نستطيع تغيير مكان رسم الخصية بتغيير قيمة posX. يحدد المعاملان 8 و 9 أبعاد الصورة على اللوحة، وعلينا هنا المحافظة على اﻷبعاد اﻷصلية لهذا كانت قمة المعاملين 102 و 148 على التتالي: علينا تعديل قيمة المتغير sprite عند كل رسم if (posX % 13 === 0) { if (sprite === 5) { sprite = 0; } else { sprite++; } } لاحظ كيف وضعنا الشيفرة السابقة ضمن الكتلة ({}if (posX % 13 === 0 واستخدمنا العامل % (عامل باقي القسمة) للتحقق من إمكانية قابلية قسمة قيمة المتغير posX على 13. فإن كان الوضع كذلك ننتقل إلى الشخصية التالية بزيادة قيمة المتغير sprite بمقدار 1 (ثم نعود إلى 0 عندما تصبح قيمته 5). ويعني ذلك فعليًا أننا نغير الشخصية عند اﻹطار 13 وتقريبًا حوالي 5 إطارات في الثانية (تكرر الدالة ()requestAnimationFrame العملية بمعدل 60 إطار في الثانية إن أمكن). وعندما نعرض أخر شخصية نعود بعدها إلى الشخصية 0 وإلا سنزيد المتغير sprite بمقدار 1. سنعمل اﻵن على آلية تغيير قيمة posX مع كل إطار، لهذا عليك إضافة الشيفرة التالية تحت الشيفرة السابقة: if (posX > width / 2) { let newStartPos = -(width / 2 + 102); posX = Math.ceil(newStartPos); console.log(posX); } else { posX += 2; } نستخدم عبارة if...else للتحقق من تجاوز قيمة المتغير posX القيمة width/2 والذي يعني خروج الشخصية من يمين لوحة الرسم، وعندها نحسب موقعًا جديدًا للشخصية يضعها على يسار الحافة اليسرى للوحة. بينما إن لم تتجاوز قيمة المتغير posX تلك القيمة نزيد قيمته بمقدار 2. وبالتالي ستتحرك الشخصية إلى اليمين قليلًا في الإطار التالي. ولا بد أخيرًا من تنفيذ الحركة السابقة باستمرار عن طريق استدعاء ()requestAnimationFrame في نهاية الدالة ()draw: window.requestAnimationFrame(draw); ستبدو نتيجة الشيفرة اﻵن كالتالي: ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. تطبيق رسومي بسيط كمثال أخير عن الرسوميات، سنعرض تطبيقًا بسيطًا جدًا للرسم نجمع فيه بين الاستجابة لمدخلات المستخدم (حركة الفأرة في هذا المثال) والحلقة التي تبني الرسم المتحرك. لن نشرح بالتفصيل خطوات بناء التطبيق بل سنلقي نظرة على الشيفرة اﻷكثر أهمية. بإمكانك الاطلاع على شيفرة التطبيق من خلال المستودع المخص له على جت-هب. لنلق نظرة على بعض اﻷجزاء المهمة: أولًا: نتتبع موقع الفأرة من خلال إحداثيات x و y، كما نترصد حدث نقر الفأرة وذلك من خلال المتغيرات curX و curY و pressed. وعندما تتحرك الفأرة يقع الحدث onmousemove وننفذ معالجه الذي يلتقط الإحداثيات الحالية لموقع الفأرة. كما نستخدم أيضًا معالجي الحدثين onmousedown و onmouseup لتغيير قيمة المتغير pressed إلى true عندما نضغط زر الفأرة وإلى false عندما نحرر الزر. let curX; let curY; let pressed = false; // حدّث إحداثيات موقع الفأرة document.addEventListener("mousemove", (e) => { curX = e.pageX; curY = e.pageY; }); canvas.addEventListener("mousedown", () => (pressed = true)); canvas.addEventListener("mouseup", () => (pressed = false)); عند النقر على الزر "مسح اللوحة Clean canvas" ننفذ دالة بسيطة تمحي اللوحة بأكملها وتعيدها إلى اللون اﻷسود: clearBtn.addEventListener("click", () => { ctx.fillStyle = "rgb(0 0 0)"; ctx.fillRect(0, 0, width, height); }); وحلقة الرسم بسيطة هنا، فعندما تكون قيمة المتغير pressed هي true، نرسم دائرة لها اللون الذي يحدده منتقي اﻷلوان color picker ونصف قطر يحدده عنصر تحديد المجال range input. وعلينا رسم الدائرة فوق النقطة المحددة بمقدار 85 بكسل، ذلك أن القياس مأخوذ بالنسبة إلى أعلى شاشة العرض (أعلى نافذة المتصفح) لكننا نرسم الدائرة بالنسبة ﻷعلى اللوحة التي تبدأ تحت شريط التحكم (الذي يضم منتقي اﻷلوان ومحدد نصف القطر) ذو الارتفاع 85 بكسل. ولو رسمنا الدائرة اعتمادًا على قيمة curY ستبدو الدائرة تحت النقطة المحددة للرسم بحدود 85 بكسل. function draw() { if (pressed) { ctx.fillStyle = colorPicker.value; ctx.beginPath(); ctx.arc( curX, curY - 85, sizePicker.value, degToRad(0), degToRad(360), false, ); ctx.fill(); } requestAnimationFrame(draw); } draw(); جميع أنواع عنصر الدخل <input> مدعومة جيدًا من قبل المتصفحات، وإن لم يدعمها متصفح سيعرض حقل نصي نمطي بدلًا عنه. الواجهة WebGL لنترك اﻵن البيئة الرسومية ثنائية البعد ونلقي نظرة سريعة على لوحات الرسم ثلاثية اﻷبعاد. تُستخدم الواجهة البرمجية WebGL API للعمل مع الرسومات ثلاثية البعد، وهي واجهة منفصلة تمامًا عن واجهة البيئة الرسومية ثنائية البعد مع أن شيفرتهما تُصيّر ضمن العنصر نفسه <canvas>. بنيت WebGL على أساس OpenGL (مكتبة الرسوميات المفتوحة Open Graphics Library) وتسمح لك بالتواصل مباشرة مع المعالج الرسومي للحاسوب GPU. لهذا فكتابة شيفرة WebGL خام أشبه بكتابة شيفرات لغات منخفضة المستوى مثل ++C مقارنة بشيفرة جافا سكريبت، فهي معقدة لكنها قوية جدًا. استخدام مكتبة جافا سكريبت خارجبة يستخدم معظم المطورون مكتبات يقدمها طرف آخر عند العمل مع الرسوميات ثلاثية البعد نظرًا لتعقيد WebGL مثل Three.js أو PlayCanvas أو Babylon.js. تعمل هذه المكتبات عومًا على نحو متشابه، فهي تقدم دوال أولية وأشكال مخصصة وكاميرات لعرض الموقع وطرق لتطبيق اﻹضاءة والظل ولتغطية السطوح بخامات مختلفة وغيرها. فهذه المكتبات تتعامل مباشرة مع WebGL بدلًا منك متيحة لك المجال للعمل وفق سوية برمجية أعلى. ويعني هذا بالطبع تعلم واجهات برمجية أخرى (واجهات يقدمها طرف آخر في حالتنا) لكنها أبسط بكثير من التعامل مع شيفرة WebGL الخام. إعادة إنشاء مكعب لنلق نظرة على مثال بسيط يشرح استخدام المكتبة WebGL، وسنختار فيه المكتبة Three.js كونها من أكثر المكتبات استخدامًا. وسنبني في مثالنا مكعب ثلاثي اﻷبعاد يدور حول نفسه. أنشئ نسخة عن ملف المثال) ضمن مجلد جديد ثم احفظ نسخة من الملف metal003.png في المجلد نفسه. ويمثل الملف اﻷخير الصورة التي نستخدمها لتغطية سطح المكعب لاحقًا. أنشئ ملفًا جديدًا باسم script.js في نفس المجلد السابق. نزّل المكتبة Three.min.js وخزنها في نفس المجلد السابق. لدينا اﻵن الملف three.js الذي يرتبط بصفحتنا، ويمكننا كتابة الشيفرة الذي تستخدمه ضمن الملف script.js. لنبدأ بإنشاء مشهد جديد عن طريق إضافة الشيفرة التالية: const scene = new THREE.Scene(); تُنشئ الدالة البانية()scene مشهدًا جديدًا يمثل بيئة عمل ثلاثية اﻷبعاد التي نريد عرضها. ونضيف بعد ذلك كاميرا لرؤية المشهد. ووفق مصطلحات التصميم ثلاثي اﻷبعاد، تمثل الكاميرا موقع المراقب، وﻹنشائها أضف اﻷسطر التالية: const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000, ); camera.position.z = 5; تأخذ الدالة البانية PrespectiveCamera أربع وسطاء: حقل الرؤية: ويدل على اتساع المساحة أمام الكاميرا التي يجب عرضها على الشاشة مقدرة بالدرجات. نسبة العرض aspect ratio: وهي عادة نسبة اتساع الشاشة مقسومًا على ارتفاعها، واستخدام نسب أخرى ستشوه المشهد. مستوي البعد: وتمثل البعد عن الكاميرا الذي لن تصير بعده اﻷشياء. نضبط أيضًا موقع الكاميرا ليكون على بعد خمس وحدات قياس بعيدًا عن المحور z، وهذا مشابه للخاصية z-index في CSS التي تمثل موقع العنصر بعيدًا عن الشاشة باتجاهك. أما المكون الحيوي الثالث فهو المصيّر renderer، وهو كائن يصير المشهد كما يُرى من الكاميرا. وسننشئ اﻵن مصيّرًا باستخدام الدالة البانية ()WebGLRenderer لكننا لن نستخدمه حاليًا: const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); يُنشئ السطر الأول مصيرًا جديدًا والثاني يضبط الأبعاد التي سيرسم المصير ضمنها ما تعرضه الكاميرا، بينما يربط السطر الثالث العنصر <canvas> الذي يُنشئه المصيّر بجسم مستند HTML وسيُعرض كل ما يرسمه المصيّر في نافذة المتصفح. وﻹنشاء المكعب الذي نريد رسمه في اللوحة، عليك إضافة اﻷسطر التالية إلى نهاية ملف جافا سكريبت: let cube; const loader = new THREE.TextureLoader(); loader.load("metal003.png", (texture) => { texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; texture.repeat.set(2, 2); const geometry = new THREE.BoxGeometry(2.4, 2.4, 2.4); const material = new THREE.MeshLambertMaterial({ map: texture }); cube = new THREE.Mesh(geometry, material); scene.add(cube); draw(); }); هناك نقاط عديدة يجدر شرحها في الشيفرة السابقة: ننشئ أولًا المتغير العام cube لكي نصل إلى المكعب في أي مكان من الشيفرة. ننشئ تاليًا كائن TextureLoader جديد ونستدعي التابع ()load العائد له. ويأخذ هذا التابع معاملين في حالتنا (علمًا أنه يأخذ أكثر): الخامة التي نريد أن نحمّلها (صورة PNG) ودالة ننفذها عند اكتمال تحميل الخامة. نستخدم داخل الدالة السابقة خاصيات الكائن لتكرار الصورة التي تغلف جميع أوجه المكعب بمقدار 2x2. ومن ثم ننشئ كائن BoxGeometry وكائن MeshLambertMaterial جديدان ونربطهما معًا ضمن شبكة Mesh ﻹنشاء المكعب. ويحتاج أي كائن نمطيًا إلى بنية هندسية (الشكل الذي سيكون عليه) ومظهر مادي (كيف سيبدو السطح الخارجي). وفي النهاية، نضيف المكعب إلى المشهد ومن ثم نستدعي الدالة ()draw لتبدأ عملية تحريك الرسم. وقبل أن نعرّف الدالة ()draw، نضيف زوجًا من الأضواء إلى المشهد، ليبدو المشهد أكثر حيوية: const light = new THREE.AmbientLight("rgb(255 255 255)"); // soft white light scene.add(light); const spotLight = new THREE.SpotLight("rgb(255 255 255)"); spotLight.position.set(100, 1000, 1000); spotLight.castShadow = true; scene.add(spotLight); يُعد الكائن AmbientLight نوعًا من اﻷضواء البرمجية التي تضيئ المشهد بأكمله بما يشبه الشمس التي تضيء عليك وأنت في الخارج. بينما يمثل الكائن spotLight شعاع ضوئي وفق اتجاه محدد مثل مشعل أو بقعة ضوء. لنضف اﻵن الدالة ()draw إلى أسفل شيفرة جافا سكريبت: function draw() { cube.rotation.x += 0.01; cube.rotation.y += 0.01; renderer.render(scene, camera); requestAnimationFrame(draw); } الشيفرة السابقة واضحة عمومًا، إذ ندوّر المكعّب قليلًا في كل إطار حول محوريه اﻷفقي والشاقولي ونصيّر المشهد كما يُرى من الكاميرا ونستدعي أخيرًا الدالة ()requestAnimationFrame لتحضير رسم اﻹطار التالي: إليك المشهد بشكله النهائي: ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. الخلاصة لا بد وأن تكون في نهاية هذا المقال قد امتلكت فكرة لا بأس بها عن أساسيات برمجة الرسوميات باستخدام الواجهة البرمجية Canvas و المكتبة WebGL وما يمكن فعله باستخدام هاتين الواجهتين، وامتلكت فكرة جيدًا عن الأماكن التي تقصدها لتحصل على معلومات أكثر. ترجمة -وبتصرف- للقسم الثاني من مقال Drawing graphics اقرأ أيضًا المقال السابق: العمل مع الرسوميات في جافا سكريبت: الرسومات ثنائية البعد ضمن العنصر Canvas مقدمة إلى WebGL - إضافة التفاصيل إلى سطح مجسَّم مدخل إلى صناعة ألعاب المتصفح الرسم على لوحة في جافاسكربت
-
يتضمن المتصفح مجموعة أدوات برمجية فعّالة للتعامل الرسوميات ابتداءً من لغة إنشاء الرسوميات الشعاعية SVG، إلى الواجهات التي تسمح لك بالرسم ضمن العنصر <canvas>. لهذا سنقدم في هذا المقال مدخلًا إلى الوجهة البرمجية Canvas، إضافة إلى بعض الموارد اﻷخرى لتزيد من معارفك في هذا المجال. ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي HTML و CSS. تكون ملمًا بلغة جافا سكريبت. تطلع على سلاسل المقالات السابقة التي ناقشت أساسيات جافا سكريبت والكائنات في جافا سكريبت. تطلع على أساسيات الواجهات البرمجية في طرف العميل. الرسوميات في الويب ذكرنا في مقالات سابقة أن الويب كان بداية نصيًا -أي يعرض محتوى نصي فقط- بشكل كامل مما جعله ضعيف الجاذبية، لذلك ظهرت الصور بداية من خلال العنصر <img> ولاحقًا من خلال خاصيات لغة التنسيق CSS مثل الخاصية background-image و من ثم بدأ استخدام الصور الشعاعية أو المتجهة SVG. مع ذلك لم يكن كل هذا كافيًا. وعلى الرغم من إمكانية استخدام CSS وجافا سكريبت لتحريك الصور الشعاعية SVG vector images والتعامل معها كونها تُكتب باستخدام تعليمات ترميز markup ولم تكن هناك طريقة لعمل المثل على الصور النقطية bitmap images وكانت الأدوات المتوفرة محدودة. ولم توجد طريقة أصيلة في الويب ﻹنشاء الرسوميات المتحركة أو اﻷلعاب أو المشاهد ثلاثية الأبعاد والتي تحتاج متطلبات خاصة تتعامل معها لغات برمجة منخفضة المستوى مثل ++C وجافا. بدأ الوضع بالتحسن عندما دعمت المتصفحات العنصر والواجهة البرمجية المتعلقة به في عام 2004. وكما سنرى تاليًا، تقدم عناصر canvas بعض اﻷدوات المفيدة التي تساعد في إنشاء رسوميات متحركة ثنائية البعد وألعاب، وعرض البيانات وغيرها من اﻹمكانات وخاصة عندما تتكامل مع واجهات برمجية أخرى تقدمها منصة الويب. لكن كان من الصعب إعدادها للوصول السهل accessibility. سترى في المثال التالي الكرات القافزة المرتدة التي عملنا عليها في مقال سابق وهي مشهد ثنائي البعد مبني على أساس العنصر canvas. وفي الفترة الممتدة بين 2006 إلى 2007 عملت موزيللا على إنجاز عناصر لوحات رسومية canvas ثلاثية اﻷبعاد، وتحولت فيما بعد إلى WebGL التي حظيت باهتمام مطوري المتصفحات وقد جرى توصيفها لتكون معيارًا بين عامي 2009-2010. وتتيح لك الواجهة WebGL إنشاء رسوميات ثلاثية اﻷبعاد ضمن المتصفح. بقدم المثال التالي مكعبًا يدور باستخدام هذه الواجهة: نركز في مقالنا على لوحات الرسم ثنائية البعد، وبما أن شيفرة WebGL الخام شديدة التعقيد، سنعرض طريقة استخدام المكتبة WebGL ﻹنشاء مشهد ثلاثي اﻷيعاد بسهولة أكبر. تطبيق عملي: ابدأ العمل مع لوحة الرسم canvas إن أردت إنشاء رسوميات ثنائية وثلاثية البعد على صفحة ويب عليك أن تنطلق من عنصر HTML الذي يُمثّل لوحة الرسم <canvas>. ويُستخدم هذا العنصر في تحديد منطقة من الصفحة للرسم فيها. واﻷمر بسيط ويتم بإضافة عنصر <canvas> إلى الصفحة كما يلي: <canvas width="320" height="240"></canvas> تنشئ الشيفرة السابقة لوحة رسم أبعادها 230 و 240 بكسل. ولا بد أن تضع شيئًا ما ضمن وسمي البداية والنهاية للعنصر كي يصف محتوى اللوحة لمستخدمي المتصفحات التي لا تدعم العنصر أو لمستخدمي قارئات الشاشة: <canvas width="320" height="240"> <p>اكتب هنا وصف اللوحة للمستخدمين الذين لا يمكنهم رؤيتها </p> </canvas> و لابد أن يعتبر ما تضعه ضمن وسمي العنصر بديلًا مفيدًا عن محتوى اللوحة، فإن كنت تصيّر أو تعرض رسمًا يتغير بشكل مستمر ليعبر عن أسعار البورصة مثلًا ، ينبغي أن يكون المحتوى البديل صورة تتضمن آخر تحديث للرسم مع نص بديل عنها alt يتحدث عن اﻷسعار أو قائمة من الروابط المستقلة لكل صفحة من صفحات هذه البورصة. ملاحظة: لا يمكن الوصول إلى محتوى لوحة الرسم من خلال قارئات الشاشات، لهذا عليك وضع نص يصف محتواها على شكل قيمة للسمة arial-label ضمن العنصر <canvas> نفسه أو استخدام محتوى مستقل ضمن وسمي البداية والنهاية للعنصر. وتذكر أن محتوى <canvas> ليس جزءًا من شجرة DOM لكن العنصر الذي تضعه ضمنه كذلك. إنشاء لوحة رسم وتحديد أبعادها لنبدأ بإنشاء لوحة رسم خاصة بتطبيقنا، لهذا اتبع الخطوات التالية: انسخ مجلد المشروع الذي يتضمن الملفات التالية: index.html script.js style.css افتح الملف index.html ثم أضف الشيفرة التالية ضمنه تحت الوسم <body>: <canvas class="myCanvas"> <p>Add suitable fallback here.</p> </canvas> أضفنا في الكود أعلاه صنفًا إلى العنصر <canvas> حتى يسهل الوصول إليه عن طريق جافا سكريبت في حال كان هناك أكثر من لوحة نريد العمل معها، لكننا أزلنا السمتين width و height حاليًا (بإمكانك إعادتهما إن أردت، لكننا سنضبطهما لاحقًا باستخدام جافا سكريبت). وستأخذ اللوحات افتراضيًا ارتفاعًا مقداره 150 بكسل واتساعًا مقداره 300 بكسل. افتح الملف scripts.js ثم أضف شيفرة جافا سكريبت التالية: const canvas = document.querySelector(".myCanvas"); const width = (canvas.width = window.innerWidth); const height = (canvas.height = window.innerHeight); خزّنا هنا مرجعًا إلى لوحة الرسم ضمن الثابت canvas ومن ثم أنشأنا ثابتًا آخر width وضبطنا قيمته وقيمة اتساع اللوحة لتكون مساوية لاتساع نافذة المتصفح Window.innerWidth، وكررنا ما فعلناه في السطر الثالث لكن مع ارتفاع اللوحة. وهكذا ستملأ لوحة الرسم نافذة المتصفح (تذكر أن الارتفاع هنا هو ارتفاع نافذة العرض viewport). لاحظ أيضًا كيف نفذنا سلسلة من اﻹسنادات باستخدام عدة عوامل مساواة =، وهذا أمر مسموح في جافا سكريبت ويُعد مفيدًا إن أردت إسناد القيمة ذاتها إلى عدة متغيرات. كما حرصنا على تأمين طريقة للوصول إلى أبعاد اللوحة بإسنادها إلى متغيرات، وتأتي فائدة هذه الفكرة إن احتجنا لاحقًا على سبيل المثال إلى رسم شيء ما في وسط اللوحة تمامًا. ملاحظة: علينا غالبًا ضبط أبعاد الصور باستخدام سمات HTML أو خاصيات شجرة DOM كما شرحنا في اﻷعلى. كما يمكنك استخدام CSS لكن تطبيق اﻷبعاد الجديدة سيكون بعد تصيير لوحة الرسم وهكذا قد تتعرض لوحة الرسم كغيرها من الصورة إلى التشوه. ضبط مسار العمل على اللوحة وإنهاء اﻹعداد نحتاج إلى مرجع خاص إلى منطقة العمل حتى نستطيع الرسم على اللوحة يُعرف بمسار العمل context. وننفذ هذا اﻷمر باستخدام التابع ()HTMLCanvasElement.getContext الذي يأخذ معاملًا واحدًا بأبسط حالات استخدامه تمثل نوع مسار العمل الذي نريده. وما نحتاجه في تطبيقنا لوحة ثنائية البعد، لهذا سنضيف شيفرة جافا سكريبت التالية في آخر الشيفرة الموجودة في الملف script.js: const ctx = canvas.getContext("2d"); ملاحظة: بإمكانك اختيار مسارات عمل أخرى مثل webgl من أجل WebGL و webgl2 من أجل 2 WebGL لكننا لن تحتاج هذه المسارات في مقالنا. وهكذا تصبح لوحة الرسم جاهزة في تطبيقنا، وسيحمل المتغير ctx الكائن CanvasRenderingContext2D وسيكون الرسم على اللوحة من خلال التعامل مع هذا الكائن. دعونا قبل إكمال العمل ننفذ شيئًا أخيرًا وهو تلوين خلفية الصفحة لتأخذ فكرة بسيطة عن الواجهة البرمجية Canvas. أضف الشيفرة التالية إلى شيفرة الملف script.js: ctx.fillStyle = "rgb(0 0 0)"; ctx.fillRect(0, 0, width, height); نضبط في هذه الشيفرة لون الخلفية باستخدام الخاصية fillStyle التي تأخذ قيمًا لونية كما هو حال خاصيات CSS المشابهة، ثم نرسم مربعًا يغطي كامل لوحة الرسم باستخدام التابع fillRect، ويمثل أول معاملين له الزاوية العليا اليسارية والمعاملين الباقين اتساع وارتفاع المربع الذي نريد رسمه (أخبرناك أن للمتغيرين width و height فوائد لاحقة). أساسيات الرسوميات ثنائية البعد ضمن العنصر <canvas> ذكرنا سابقًا أن جميع عمليات الرسم تجري من خلال التعامل مع الكائن CanvasRenderingContext2D (وهو ctx في تطبيقنا). وتحتاج الكثير من العمليات إلى إحداثيات لتحديد المكان الذي نرسم فيه بدقة، وتكون الزاوية العليا اليسارية بمثابة مبدأ الجملة اﻹحداثية وتمثل النقطة (0,0)، بينما يتجه المحور الأفقي (x) من اليسار نحو اليمين والعمودي (y) من اﻷعلى إلى اﻷسفل. تميل معظم الرسوميات إلى استخدام المربع البدائي primitive rectangle (الذي يمثل شكل أساسي يستخدم لبناء رسوميات أكثر تعقيدًا) أو تتبع خط عبر مسار محدد ومن ثم ملء الشكل الناتج. وسنشرح تاليًا كيف يجري اﻷمر. مربعات بسيطة سنبدأ برسم بعض المربعات البسيطة، لهذا: انسخ شيفرة قالب لوحة الرسم الذي حضرناه سابقًا (كما يمكنك إنشاء نسخة عن مجلد التطبيق إن لم تتابع معنا الخطوات السابقة). أضف الأسطر التالية من الشيفرة في أسفل شيفرة جافا سكريبت الموجودة: ctx.fillStyle = "rgb(255 0 0)"; ctx.fillRect(50, 50, 100, 150); إن حفظت التغيرات وأعدت تحميل الصفحة سترى مربعًا أحمر اللون ضمن اللوحة، تبعد زاويته العليا اليسارية مقدار 50 بكسل عن الحافتين العليا واليسارية للوحة (كما حددهما أول معاملين) وله اتساع مقداره 100 بكسل وارتفاع 150 بكسل (كما حددهما المعاملان اﻷخيران). لنضف اﻵن مربعًا آخر أخضر هذه المرة: ctx.fillStyle = "rgb(0 255 0)"; ctx.fillRect(75, 75, 100, 100); احفظ التغييرات وأعد تحميل الصفحة لترى النتيجة. تطرح الشيفرة السابقة نقطة هامة وهي أن جميع عمليات الرسم مثل رسم مربع أو خط وغيرها تُنفّذ وفق تسلسل ورودها في الشيفرة. فكّر بالأمر وكأنك ترسم على جدار، حيث تغطي كل طبقة ما تحتها. وبالطبع لا يمكن أن نغيّر هذا اﻷمر، لهذا عليك أن تفكّر مليًا بترتيب ما ترسمه على اللوحة. كما يمكنك إنشاء رسومات شبه شفافة عند اختيارك لونًا شبه شفاف باستخدام التابع ()rgb مثلًا. إذ تُعرّف القناة ألفا alpha channel مقدار الشفافية في اللون، وكلما كانت قيمتها أكبر كلما زادت قتامة اللون وغطّى ما تحته. أضف السطرين التاليين إلى الشيفرة: ctx.fillStyle = "rgb(255 0 255 / 75%)"; ctx.fillRect(25, 100, 175, 50); جرب أن ترسم مربعات لتختبر قدرتك! الإطارات وسماكة الخطوط رسمنا حتى اللحظة مربعات ممتلئة، لكنك تستطيع أيضًا رسم إطارات مربعة strokes. ولضبط لون اﻹطار نستخدم الخاصية strokeStyle ونرسمه باستخدام التابع strokeRect. أضف السطرين التاليين إلى الشيفرة: ctx.strokeStyle = "rgb(255 255 255)"; ctx.strokeRect(25, 25, 175, 200); للإطارات سماكة افتراضية قيمتها 1 بكسل، لكنك تستطيع تعديل السماكة باستخدام الخاصية lineWidth التي تأخذ قيمة تمثل سماكة اﻹطار مقدرة بالبكسل. أضف اﻵن السطر التالي إلى الشيفرة: ctx.lineWidth = 5; لاحظ كيف سيبدو اﻹطار أكثر سماكة. وسيبدو مثالنا حتى اللحظة كالتالي: ملاحظة: ستجد الشيفرة الكاملة لهذا المثال على جت-هب. رسم المسارات لو أردت رسم ما هو أعقد من مربع، لا بد حينها من رسم مسار. ويقتضي اﻷمر بأبسط أشكاله كتابة شيفرة تحدد تمامًا المسار الذي تريد أن يتحرك قلم الرسم عليه ضمن اللوحة حتى يرسم الشكل المطلوب. وتضم الواجهة Canvas دوال لرسم خطوط مستقيمة ودوائر ومنحنيات بيزيه وغيرها الكثير. لنبدأ اﻵن هذا القسم بنسخة جديدة من قالب المثال الذي أعددناه سابقًا، وسنستخدم بعض التوابع والخاصيات الشائعة خلال الأقسام التالية: ()beginPath: يبدأ رسم مسار من النقطة التي يكون عندها القلم حاليًا في اللوحة وستكون هذه النقطة مبدأ اﻹحداثيات إن كانت اللوحة جديدة. ()moveTo: ينقل القم إلى نقطة أخرى من اللوحة دون رسم أو تسجيل المسار بل يقفز القلم إلى النقطة المختارة. ()fill: يرسم شكلًا يملأ المسار الذي رسمه القلم. ()stroke: يرسم إطارًا مبنيًا على المسار الذي يرسمه القلم. باﻹمكان استخدام الخاصيات lineWidth و fillStyle أو strokeStyle مع المسارات أيضًا. تبدو شيفرة رسم مسار نمطي قريبة من التالي: ctx.fillStyle = "rgb(255 0 0)"; ctx.beginPath(); ctx.moveTo(50, 50); // ارسم مسارك ctx.fill(); رسم الخطوط لنرسم اﻵن مثلث متساوي الأضلاع ضمن اللوحة: أضف بداية الدالة المساعدة التالية في أسفل الشيفرة، مهمة هذه الدالة تحويل قيم الزوايا من درجات إلى راديان. تكمن فائدة هذه الدالة في أن جافا سكريبت تفهم قيم الزوايا بالراديان لكننا كبشر نفكر طبيعيًا بالدرجات. function degToRad(degrees) { return (degrees * Math.PI) / 180; } ابدأ المسار بإضافة الشيفرة التالية تحت الشيفرة السابقة، وفيها نضبط لون المثلث ونبدأ رسم المسار ثم ننتقل مباشرة إلى النقطة (0,0) دون رسم أي شيء ومن هذه النقطة نبدأ رسم المثلث: ctx.fillStyle = "rgb(255 0 0)"; ctx.beginPath(); ctx.moveTo(50, 50); أضف اﻷسطر التالية في نهاية الشيفرة السابقة: ctx.lineTo(150, 50); const triHeight = 50 * Math.tan(degToRad(60)); ctx.lineTo(100, 50 + triHeight); ctx.lineTo(50, 50); ctx.fill(); نرسم بداية خطًا من نقطة البداية إلى النقطة (150,50) وسيتجه مسارنا 100 بكسل إلى اليمين وفق المحور x. نحسب بعد ذلك ارتفاع المثلث متساوي اﻷضلاع باستخدام قواعد مثلثية بسيطة إذ نعلم أن زوايا المثلث هي 60 درجة. لهذا نستطيع تقسيم المثلث المتساوي اﻷضلاع الذي نوجهه للأسفل إلى مثلثين قائمين لكل منهما زاويتين حادتين قياسهما 30 و60 درجة. ونعرّف في المثلث القائم: الوتر hypotenuse: وهو أطول أضلاع المثلث القائم. المجاور adjacent: وهو هنا الضلع المجاور للزاوية 60 وطوله 50 بكسل لأنه يمثل نصف طول المسار الذي رسمناه سابقًا. المقابل opposite: وهو هنا الضلع المقابل للزاوية 60 ويمثل ارتفاع المثلث المتساوي اﻷضلاع الذي ننوي رسمه. يُعطى طول المجاور رياضيًا من خلال جداء المقابل بظل الزاوية tan: 50 * Math.tan(degToRad(60)) نستخدم هنا الدالة ()degToRad التي بنيناها سابقًا لتحويل الزاوية 60 درجة إلى راديان وهي القيمة التي يتوقعها التابع ()Math.tan الذي يحسب ظل الزاوية. بعد حساب اﻹرتفاع، نرسم خطًا آخر إلى النقطة (100, 50+triHeight) إلى نقطة أخرى لها إحداثي X يعادل نصف طول المسار المستقيمة السابق وإحداثي Y قيمته تعادل 50 زائدًا طول اﻹرتفاع، ذلك أن قاعدة المثلث تنزاح إلى داخل اللوحة مقدار 50 بكسل عن الحافة العليا لها. أما الخطوة التالية فهي رسم خط من آخر نقطة إلى نقطة البداية ليتكون المثلث. نستدعي في النهاية التابع ()ctx.fill ﻹنهاء المسار وملئ الشكل الناتج. رسم الدوائر لنلق نظرة على طريقة رسم الدوائر في اللوحة. تُنفّذ هذه العملية من خلال التابع ()arc الذي يرسم جزءًا من قوس الدائرة أو قوس الدائرة بأكمله ابتداءًا من نقطة محددة: ﻹضافة دائرة إلى لوحتنا ضع الشيفرة التالية في نهاية الشيفرة السابقة: ctx.fillStyle = "rgb(0 0 255)"; ctx.beginPath(); ctx.arc(150, 106, 50, degToRad(0), degToRad(360), false); ctx.fill(); يأخذ التابع ()arc ست معاملات، يحدد اﻷول والثاني اﻹحداثيين x و y لمركز الدائرة والثالث هو نصف قطر الدائرة، بينما يحدد المعاملين الخامس والسادس زاويتي البداية والنهاية لقوس الدائرة (0 و 360 يرسمان دائرة كاملة) ويحدد المعامل اﻷخير إذا ما كانت الدائرة سترسم باتجاه عقارب الساعة أو عكسها (تعني القيمة false أن الرسم باتجاه عقارب الساعة) ملاحظة: الزاوية 0 هي الزاوية الأفقية إلى اليمين. لنجرب إضافة قوس آخر: ctx.fillStyle = "yellow"; ctx.beginPath(); ctx.arc(200, 106, 50, degToRad(-45), degToRad(45), true); ctx.lineTo(200, 106); ctx.fill(); هناك اختلافان بسيطان عن النمط السابق: ضبطنا قيمة المعامل الأخير للتابع ()arc على القيمة true أي سيرسم القوس بعكس اتجاه عقارب الساعة، فحتى لو كانت زاوية البداية هي 45- وزاوية النهاية 45 درجة فإن القوس يغطي زاوية 270 درجة وليس 90 درجة والتي يمكن أن تحصل عليها إن كانت قيمة المعامل false. رسمنا خطًا إلى مركز الدائرة قبل استدعاء ()fill كي نحصل على دائرة اقتطع منها مثلث. وإن لم نرسم هذا الخط سيصل المتصفح نقطة البداية ونقطة النهاية ويملأ الشكل الناتج وهو دائرة اقتطع منها طرف. ستبدو نتيجة المثال السابق قريبة من التالي: ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. رسم النصوص يتيح لك العنصر <canvas> رسم عبارات نصية، وهذا ما سنتعلمه بإيجاز تاليًا. لنبدأ بإنشاء نسخة جديدة عن قالب التطبيق كي نرسم المثال الجديد. ونستخدم في هذا المثال التابعين: ()fillText: الذي يملأ النص. ()strokeText: الذي يرسم الحواف الخارجية للنص. يأخذ كل تابع منهما ثلاثة خاصيات بشكله البسيط: النص الذي سيُرسم واﻹحداثيين x و y للنقطة التي يبدأ الرسم عندها. هذه النقطة هي عمليًا الزاوية السفلى اليسارية لصندوق النص الذي نرسمه (الصندوق الذي يحيط بالنص). قد يسبب اﻷمر إرباكًا أحيانًا بالنظر إلى أن عمليات الرسم اﻷخرى تميل إلى البدء من الزاويا العليا اليسارية، تذكر ذلك جيدًا. وهنالك أيضًا عدد من الخاصيات التي تساعد في إدارة تصيير النص مثل font التي تسمح بتخصيص عائلة الخط وحجمه وغيرها، وتأخذ قيمها وفق الصيغة نفسها التي نستخدمها مع خاصية CSS التي تحمل نفس الاسم. ولا يمكن لقارئات الشاشة الوصول إلى محتوى العنصر <canvas> لأن النص الذي يُرسم في اللوحة لا يُعد جزءًا من شجرة DOM، لهذا لا بد من جعله متاحًا لذوي الاحتياجات الخاصة. وفي مثالنا جعلنا النص المكتوب ضمن اللوحة قيمة للسمة aria-label. أضف اﻵن الشيفرة التالية إلى نهاية شيفرة جافا سكريبت: ctx.strokeStyle = "white"; ctx.lineWidth = 1; ctx.font = "36px arial"; ctx.strokeText("Canvas text", 50, 50); ctx.fillStyle = "red"; ctx.font = "48px georgia"; ctx.fillText("Canvas text", 50, 150); canvas.setAttribute("aria-label", "Canvas text"); رسمنا باستخدام الشيفرة السابقة سطرين أولهما مفرّغ واﻵخر ممتلئ، ويبدو الشكل النهائي للوحة شبيهًا بالتالي: ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. رسم صور ضمن اللوحة بإمكانك أيضًا تصيّر صور خارجية كي تُرسم ضمن العنصر <canvas>، ويمكن أن تكون الصور بسيطةً أو إطارات من فيديو أو غير ذلك. وسنلقي نظرة على رسم صور بسيطة ضمن اللوحة. أنشئ نسخة جديدة من قالب التطبيق الذي نستخدمه لتنفيذ الرسوميات. إذ ترسم الصور ضمن اللوحة باستخدام التابع ()drawImage. ويأخذ التابع بأبسط أشكاله ثلاث معاملات هي مرجع إلى الصورة واﻹحداثيين x و y للزاوية العليا اليسارية من الصورة. لنبدأ بتحديد مصدر للصورة التي نريد رسمها، لهذا أضف الشيفرة التالية إلى ملف جافا سكريبت: const image = new Image(); image.src = "firefox.png"; أنشأنا في الشيفرة السابقة كائن HTMLImageElement جديد باستخدام الدالة البانية ()Image. وللكائن المعاد النوع ذاته الذي يُعاد عندما ننشئ مرجعًا إلى العنصر <img>، لهذا يمكن ضبط السمة src له كي تكون عنوان URL لصورة شعار فايرفوكس، وفي هذه المرحلة يبدأ المتصفح تحميل الصورة. يمكن اﻵن رسم الصورة ضمن اللوحة باستخدام ()drawImage، لكن علينا أولًا التأكد من اكتمال تحميل الصورة وإلا ستخفق العملية. نتحقق من ذلك عن طريق الحدث load الذي يقع فقط عند إنتهاء تحميل الصورة، لهذا أضف الشيفرة التالية: image.addEventListener("load", () => ctx.drawImage(image, 20, 20)); سترى إن أعدت تحميل اللوحة كيف رُسمت الصورة ضمن اللوحة. لكن بالطبع هناك المزيد. فماذا لو أردت رسم جزء من الصورة فقط أو أردت تغيير أبعادها؟ يمكننا بالطبع تنفيذ كلا اﻷمرين باستخدام صيغة أعقد للتابع ()drawImage. لهذا عدّل استدعاء التابع ()ctx.drawImage ليصبح كالتالي: ctx.drawImage(image, 20, 20, 185, 175, 50, 50, 185, 175); المعامل اﻷول هو مرجع إلى الصورة. يحدد المعاملان 2 و 3 إحداثيات الزاوية العليا اليسارية من المنطقة التي تريد اقتطاعها من الصورة المحمّلة، ولن يُرسم أي شئ أعلى أو إلى يسار قيمتي المعاملين السابقين. يحدد المعاملان 4 و 5 اتساع وارتفاع المنطقة التي تريد اقتطاعها من الصورة التي حملتها. يحدد المعاملان 6 و 7 إحداثيا النقطة التي نريد أن نبدأ فيها رسم الصورة المقتطعة انطلاقًا من الزاوية العليا اليسارية لها نسبة إلى الزاوية العليا اليسارية للوحة. يحدد المعاملان 8 و 9 اتساع وارتفاع المنطقة التي نريد أن نرسم فيها الصورة المقتطعة. وقد حددنا في مثالنا نفس أبعاد الصورة المقتطعة، لكن باﻹمكان إعادة تحجيم الصورة باستخدام قيم مختلفة للمعاملين. في حال غيّرت في الصورة تغييرًا واضحًا لابد من تحديث توصيف الصورة الخاص بسهولة الوصول accessibility. canvas.setAttribute("aria-label", "Firefox Logo"); ستبدو نتيجة المثال قريبة من التالي: ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. الخلاصة تعرفنا في هذا المقال على أساسيات الرسم ضمن العنصر <canvas> من حيث إعداد العنصر وضبط معاملاته. ثم تدربنا على رسم الخطوط والمسارات والدوائر والنصوص والصور في بيئة ثنائية البعد. وسنتابع في الجزء الثاني من هذا المقال العمل مع الرسومات المتحركة ثنائية وثلاثية البعد. ترجمة -وبتصرف- للقسم اﻷول من مقال: Drawing graphics اقرأ أيضًا المقال السابق: واجهات برمجية خارجية في جافا سكريبت Third Party APIs الرسم عبر عنصر canvas في HTML5 التعامل مع عنصر Canvas باستخدام جافاسكربت (رسم الأشكال) التعامل مع العنصر Canvas باستخدام جافاسكربت (رسم الصور ) التعامل مع التصاميم، الألوان والخطوط باستخدام Canvas في جافاسكربت
-
تُعد الواجهات البرمجية التي ذكرناها في مقالات سابقة واجهات مُضمَّنة في المتصفح، لكن ليست كل الواجهات البرمجية كذلك. إذ تقدم الكثير من الشركات مثل فيسبوك وجوجل و PayPal وغيرها، واجهات برمجية مخصصة تسمح للمطورين باستخدام بياناتها أو خدماتها (مثل عرض خريطة جوجل محددة في موقعك أو استخدام حساب فيسبوك لتسجيل المستخدمين في موقعك). لهذا نلقي نظرة في هذا المقال على الاختلافات بين الواجهات المضمنة في المتصفح والواجهات التي تقدمها أطراف أخرى والتي تعرف باسم third party APIs ونستعرض بعض الحالات النمطية لاستخدامها. ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي HTML و CSS. تكون ملمًا بلغة جافا سكريبت. تطّلع على سلاسل المقالات السابقة التي ناقشت أساسيات جافا سكريبت والكائنات في جافا سكريبت. تطّلع على أساسيات الواجهات البرمجية في طرف العميل. الواجهات البرمجية التي يقدمها طرف خارجي وهي واجهات يقدمها طرف خارجي (ليس أنت وليس المتصفح)، تسمح لك شركات مثل فيسبوك وتويتر وجوجل وغيرها بالوصول إلى بعض وظائف منتجاتها عبر جافا سكريبت واستخدامها في موقعك. ومن أكثر اﻷمثلة وضوحًا نجد الواجهات البرمجية للخرائط Maps API التي تعرض خرائط أماكن مخصصة على موقع الويب الخاص بك. دعنا نلقي نظرة على مثال بسيط يتعلق باستخدام الواجهة البرمجية Mapquest API يشرح الاختلاف بين الواجهات البرمجية التي يقدمها طرف آخر وتلك المضمنة في المتصفح. ملاحظة: بإمكانك تنزيل جميع ملفات اﻷمثلة دفعة واحدة ومن ثم البحث عن ملف المثال المطلوب الذي تحتاجه في كل قسم من المقال. الواجهات موجودة على خوادم الطرف الذي يقدمها تُضمن واجهات المتصفح البرمجية ضمن المتصفح وستتعامل معها من خلال جافا سكريبت مباشرة. ولقد رأيت في مقالنا التمهيدي كيف تعاملنا مع الواجهة البرمجية Web Audio API من خلال كائن جافا سكريبت اﻷصلي AudioContext: const audioCtx = new AudioContext(); // … const audioElement = document.querySelector("audio"); // … const audioSource = audioCtx.createMediaElementSource(audioElement); // etc. تتواجد الواجهات البرمجية التي يقدمها طرف خارجي على خوادم هذا الطرف، لهذا عليك أولًا الاتصال بتلك الواجهات حتى تتمكن من استخدامها في صفحاتك. وتقتضي هذه العملية بداية ربط صفحتك بمكتبة جافا سكريبت على ذلك الخادم عبر العنصر <script> كما في مثالنا عن الواجهة mapquest API. إليك شيفرة HTML: <script src="https://api.mqcdn.com/sdk/mapquest-js/v1.3.2/mapquest.js" defer></script> <link rel="stylesheet" href="https://api.mqcdn.com/sdk/mapquest-js/v1.3.2/mapquest.css" /> يمكنك اﻵن استخدام الكائنات التي تقدمها المكتبة: const map = L.mapquest.map("map", { center: [53.480759, -2.242631], layers: L.mapquest.tileLayer("map"), zoom: 12, }); تُنشئ الشيفرة السابقة متغيرًا لتخزين معلومات الخريطة، ثم تنشئ خريطة جديدة باستخدام التابع ()mapquest.map الذي يأخذ المعاملات التالية: معرّف العنصر <div> الذي تعرض الخريطة ضمنه وهو في مثالنا "map". كائن خيارات يضم تفاصيل الخريطة التي نريد عرضها، نذكر فيه إحداثيات الموقع وطبقة خريطة map layer نبنيها باستخدام التابع ()mapquest.titleLayer ومستوى تكبير الخريطة Zoom. هذه هي المعلومات التي تحتاجها الواجهة البرمجية mapquest API لعرض خريطة بسيطة، بينما يتكفل الخادم بمعالجة كل العمليات المعقدة مثل اختيار الخريطة الصحيحة للمنطقة المطلوبة وعرضها. ملاحظة: يختلف أسلوب الوصول إلى وظائف بعض الواجهات البرمجية عما عرضنا، إذ يطلب بعضها المطور الاتصال عن طريق طلب HTTP إلى عنوان URL معين للوصول إلى البيانات وتُدعى عندها RESTful APIs. تحتاج الواجهات البرمجية إلى مفاتيح وصول Access keys عادة تتعامل واجهات المتصفح البرمجية مع الأمور اﻷمنية من خلال عرض رسائل لتحديد السماحيات، والغاية من ذلك هو إعلام المستخدم بما يحصل على موقع الويب الذي يزوره وبالتالي لن يكون ضحية لاستخدامات مشبوهة للواجهة البرمجية. ويجري اﻷمر في الواجهات البرمجية التي يقدمها طرف خارجي على نحو مختلف قليلا، إذ تميل هذه الواجهات إلى استخدام مفاتيح لوصول المطورين إلى وظائف الواجهة، وهي مصممة غالبًا لحماية مقدّم الواجهة البرمجية بدلًا من المستخدم. إذ ستجد سطرًا برمجيًا مشابهًا للسطر التالي في مثالنا عن استخدام Mapquest API: L.mapquest.key = "YOUR-API-KEY-HERE"; يخصص هذا السطر مفتاح وصول إلى الواجهة لتستخدمه في تطبيقك. وينبغي أن يقدّم المطور طلبًا للحصول على مفتاح، ويمكنه بعدها تضمينه في الشيفرة كي يتمكن من الوصول إلى وظائف الواجهة البرمجية المطلوبة. وما عرضناه في السطر السابق هو بمثابة مفتاح افتراضي غير صحيح لتوضيح طريقة كتابته. ملاحظة: ستستخدم مفتاحك الخاص عندما تكتب تطبيقاتك الخاصة مكان المفتاح الافتراضي. وقد تطلب بعض الواجهات طريقة أخرى لتقديم مفتاحك، لكن الطرق متشابهة تقريبًا. إن الغاية من طلب مفتاح وصول وهو السماح لمقدمي الواجهة البرمجية معرفة مستخدمي الواجهة البرمجية وكيف يستخدمونها. فعندما يُمنح المطوّر مفتاحًا سيتعرف عليه مزوّد الواجهة، وسيتمكن المزود من اتخاذ الإجراءات المناسبة إن استخدم الواجهة بطريقة مشبوهة (مثل تعقب موقع شخص، أو محاولة إغراق الواجهة بالطلبات ﻹيقافها عن العمل). وستكون الطريق اﻷسهل للتعامل مع المطور هي إلغاء الامتيازات الخاصة به. توسيع مثال Mapquest لنضف اﻵن بعض الوظائف الجديدة إلى مثالنا الذي يستخدم الواجهة البرمجية mapquest وذلك لاستعراض بعض مزايا الواجهة: حتى نبدأ العمل في هذا القسم انسخ ملف المثال إلى مجلد جديد. وإن حضرت مسبقًا نسخة عن المستودع الذي يضم أمثلة المقال فستجد نسخة عن الملف المطلوب هنا في المجلد javascript/apis/mapquest/start. عليك تاليًا زيارة الموقع Mapquest developer site ثم إنشاء حساب والحصول على مفتاح مطوّر (يُدعى هذا المفتاح حتى لحظة كتابة هذه اﻷسطر "مفتاح المستهلك consumer key"، كما يُطلب إليك أثناء اﻹجراء تقديم عنوان رد النداء callback URL، لكن لا حاجة هنا لتقديم أي شيء لذا اتركه فارغًا). افتح ملف المثال واستبدل المفتاح الافتراضي بالمفتاح الذي حصلت عليه. تغيير نوع الخريطة هنالك أنواع مختلفة من الخرائط التي يمكن عرضها باستخدام الواجهة البرمجية mapquest API، ولاكتشاف الأمر، انتقل إلى السطر التالي: layers: L.mapquest.tileLayer("map"); وجرّب تغيير 'map' إلى 'hybrid' لعرض خريطة هجينة، وجرّب قيمًا أخرى أيضًا، من خلال الاطلاع على توثيق الخاصية titleLayer والخيارات المتاحة وغيرها من المعلومات. إضافة أدوات تحكم مختلفة يمكن استخدام عدة أدوات للتحكم بالخريطة، لكنها تعرض افتراضيًا فقط أدوات التكبير والتصغير. ولتوسيع أدوات التحكم المتاحة نستخدم التابع ()map.addControl، لهذا أضف السطر التالي إلى الشيفرة: map.addControl(L.mapquest.control()); يُنشئ التابع لوحة تحكم بسيطة كاملة الوظائف ويعرضها في الزاوية العليا اليمنى بشكل افتراضي. لكن بإمكانك تعديل موقع اللوحة بتخصيص كائن خيارات وتمريره كمعامل إلي التابع ()map.addControl بعد تحديد الموقع المطلوب. جرّب ما يلي: map.addControl(L.mapquest.control({ position: "bottomright" })); وهنالك أنواع أخرى من أدوات التحكم مثل اﻷدوات التي يقدمها التابعان ()mapquest.searchControl و ()mapquest.satelliteControl وبعضها معقدة وفعّالة. جرّب هذه اﻷدوات واكتشف إمكانياتها. إضافة علامة خاصة من السهل أيضًا إضافة علامة خاصة بك أو أيقونة إلى الخريطة، وذلك باستخدام التابع ()L.marker. أضف اﻵن الشيفرة التالية ضمن window.onload: L.marker([53.480759, -2.242631], { icon: L.mapquest.icons.marker({ primaryColor: "#22407F", secondaryColor: "#3B5998", shadow: true, size: "md", symbol: "A", }), }) .bindPopup("This is Manchester!") .addTo(map); وكما ترى يأخذ التابع بأبسط أشكاله معاملين: مصفوفة تضم الإحداثيات التي تريد عرض العلامة عندها، وكائن خيارات يضم الخاصية icon التي تحدد اﻷيقونة التي تُعرض في هذا المكان. تُعرّف الأيقونة باستخدام التابع ()mapquest.icons.marker الذي يتضمن معلومات مثل لون وحجم العلامة. وفي نهاية التابع نربط تابعًا آخر ('bindPopup('This is Manchester. يعرّف المحتوى الذي يُعرض عند النقر على العلامة. ثم نربط أخيرًا التابع (addTo(map. إلى نهاية السلسلة ﻹضافة العلامة فعليًا إلى الخريطة. اقرأ توثيق هذه الواجهة البرمجية وجرّب خيارات أخرى وراقب ما ستحصل عليه. إذ تقدم mapquest وظائف متقدمة مثل الاتجاهات والبحث وغيرها. ملاحظة: إن واجهتك أية مشاكل في تجربة المثال، قارن أمثلتك بالنسخة المكتملة منه. الواجهة البرمجية اﻹخبارية NYTimes (واجهة وفق معيار RESTful) لنلقِ نظرة اﻵن إلى مثال جديد مبني على الواجهة البرمجية لمجلة New York Times التي تسمح لك استخلاص اﻷخبار من المجلة وعرضها على صفحتك. يُعرف هذا النوع من الواجهات بواجهات RESTful والتي تحصل فيها على البيانات من خلال إرسال طلبات HTTP إلى عنوان URL محدد، وتُنفَّّذ عمليات البحث وغيرها من الخاصيات عن طريق تشفيرها ضمن عنوان URL (على شكل معاملات غالبًا). وهذا النوع شائع كثيرًا في الواجهات البرمجية إضافة إلى الواجهات التي تعتمد على ميزات مكتبات جافا سكريبت مثل mapquest. نهج لاستخدام الواجهات البرمجية التي يقدمها طرف خارجي سننتقل في المثال التالي خطوة خطوة لعرض طريقة استخدام الواجهة NYTimes والذي يقدم لك أيضًا خطوات عامة لتتبعها في العمل مع الواجهات البرمجية الجديدة. البحث عن التوثيق عندما تقرر العمل مع واجهة برمجية من طرف خارجي، عليك بداية إيجاد توثيق الواجهة والاطلاع على الميزات التي تقدمها وكيفية استخدامها. لهذا عليك الاطلاع على توثيق الواجهة البرمجية NYTimes قبل العمل على مثالنا. الحصول على مفتاح مطوّر تحتاج معظم الواجهات إلى استخدام مفتاح من نوع معين لأسباب إحصائية وآمنة. وللحصول على مفتاح للعمل على واجهة NYTimes اطلع على الخطوات اللازمة الواردة في صفحة المطورين. لنطلب مفتاحًا لاستخدام الواجهة في البحث عن مقال، لهذا أنشئ تطبيقًا جديدًا، واختر هذا الاستخدام ليكون الواجهة المطلوبة للتطبيق (املأ في النموذج اسم التطبيق ووصفًا له ثم اختر "Article search API" ثم انقر "Create"). انسخ المفتاح من الصفحة الناتجة عن التسجيل. حتى نبدأ المثال، انسخ جميع الملفات الموجودة في مجلد المثال إلى حاسوبك. وإن حضرت مسبقًا نسخة عن المستودع الذي يضم أمثلة المقال فستجد نسخة عن الملف المطلوب هنا في المجلد javascript/apis/nytimes/start. ستجد في الملف بعض المتغيرات التي تحتاجها ﻹعداد المثال، وسنملأ الملف بالشيفرة اللازمة لتزويده بالوظائف المطلوبة. سينتهي بك المطاف إلى تطبيق تكتب فيه معايير البحث مع إمكانية اختيار تاريخ بداية ونهاية فترة البحث، ومن ثم تستخدم هذه البيانات لاستعلام الواجهة NYTimes والبحث عن المطلوب. ربط الواجهة البرمجية مع التطبيق عليك أولًا ربط الواجهة البرمجية مع تطبيقك، وفي حالتنا، عليك أن تضيف المفتاح كمعامل للطلب get في كل مرة تطلب فيها بيانات من الخدمة على عنوان URL الصحيح. ابحث أولًا عن السطر التالي: const key = "INSERT-YOUR-API-KEY-HERE"; وأضف السطر التالي في ملف جافا سكريبت تحت التعليق: "// Event listeners to control the functionality". ومهمة هذا السطر تنفيذ الدالة ()submitSearch عند النقر على زر إرسال النموذج: searchForm.addEventListener("submit", submitSearch); أضف اﻵن تعريفي الدالتين ()submitSearch و ()fetchResults تحت السطر السابق: function submitSearch(e) { pageNumber = 0; fetchResults(e); } function fetchResults(e) { // Use preventDefault() to stop the form submitting e.preventDefault(); // Assemble the full URL let url = `${baseURL}?api-key=${key}&page=${pageNumber}&q=${searchTerm.value}&fq=document_type:("article")`; if (startDate.value !== "") { url = `${url}&begin_date=${startDate.value}`; } if (endDate.value !== "") { url = `${url}&end_date=${endDate.value}`; } } تضبط الدالة submitSearch رقم الصفحة على القيمة 0 ثم تستدعي الدالة ()fetchResults. نستخدم الدالة ()preventDefault العائدة لكائن الحدث كي لا تجري عملية إرسال مباشر للطلب قبل أن ننهي برمجة المثال. بعد ذلك، نشكل عنوان URL مكتمل بالعمل على بعض القيم النصية كي نستخدمه عند إرسال الطلب، وسنبدأ بالأجزاء الضرورية: قاعدة عنوان URL (تُؤخذ من المتغير baseURL). مفتاح الوصول إلى الواجهة البرمجية الذي يجب أن يُسند إلى المعامل api-key لعنوان URL (تؤخذ القيمة من المتغير key). رقم الصفحة الذي ينبغي إسناده إلى المعامل page لعنوان URL (تؤخذ القيمة من المتغير pageNumber). العبارة التي نبحث عنها، وتُسند إلى المعامل q لعنوان URL (تؤخذ القيمة من قيمة عنصر اﻹدخال <input> الذي يُدعى searchTerm). نوع المستند الذي نريد الحصول عليه، ويحدد من خلال التعبير الذي يُمرر إلى المعامل fq لعنوان URL. وفي حالتنا نريد أن يعيد البحث مقالًا. نستخدم تاليًا عبارتي ()if للتحقق من وجود قيم المتغيرين startDate و endDate. فإن كان اﻷمر كذلك وضعنا القيمتين في عنوان URL ضمن المعاملين الاختياريين begin_date و end_date. وسيبدو الشكل الكامل لعنوان URL شبيهًا بالعنوان التالي: https://api.nytimes.com/svc/search/v2/articlesearch.json?api-key=YOUR-API-KEY-HERE&page=0&q=cats&fq=document_type:("article")&begin_date=20170301&end_date=20170312 ملاحظة: بإمكانك الاطلاع على المعاملات اﻷخرى التي يمكن تضمينها ضمن عنوان URL في توثيق الواجهة البرمجية NYTimes. ملاحظة: يتحقق المثال بشكل مبسط من القيم المُدخلة. فيجب بداية إدخال نص البحث قبل إرسال الاستعلام باستخدام السمة required (مطلوب). كما يجب أن يضم حقل التاريخ 8 أرقام حتى يُرسل الطلب وذلك من خلال استخدام السمة pattern لتكون قيمتها {pattern=[0-9]{8. طلب البيانات من الواجهة البرمجية بعد أن شكلنا عنوان URL الخاص بالطلب لننفذ الطلب باستعمال الواجهة Fetch API. لهذا أضف الشيفرة التالية ضمن كتلة الدالة ()fetchResults: // لاستعلام الواجهة البرمجية fetch() استخدم fetch(url) .then((response) => response.json()) .then((json) => displayResults(json)) .catch((error) => console.error(`Error fetching data: ${error.message}`)); ننفذ الاستعلام بتمرير قيمة المتغير إلى الدالة ثم نحوّل الاستجابة إلى صيغة JSON عبر الدالة ()json ونمرر النتيجة إلى الدالة ()displayResults كي تُعرض البيانات على واجهة المستخدم. وبعدها يُعالج أي خطأ قد يقع باستخدام التابع ()catch.. عرض البيانات لننظر إلى الطريقة التي نعرض فيها البيانات على شاشة المستخدم. لهذا، أضف الدالة التالية تحت الدالة ()fetchResults مباشرة: function displayResults(json) { while (section.firstChild) { section.removeChild(section.firstChild); } const articles = json.response.docs; nav.style.display = articles.length === 10 ? "block" : "none"; if (articles.length === 0) { const para = document.createElement("p"); para.textContent = "No results returned."; section.appendChild(para); } else { for (const current of articles) { const article = document.createElement("article"); const heading = document.createElement("h2"); const link = document.createElement("a"); const img = document.createElement("img"); const para1 = document.createElement("p"); const keywordPara = document.createElement("p"); keywordPara.classList.add("keywords"); console.log(current); link.href = current.web_url; link.textContent = current.headline.main; para1.textContent = current.snippet; keywordPara.textContent = "Keywords: "; for (const keyword of current.keywords) { const span = document.createElement("span"); span.textContent = `${keyword.value} `; keywordPara.appendChild(span); } if (current.multimedia.length > 0) { img.src = `http://www.nytimes.com/${current.multimedia[0].url}`; img.alt = current.headline.main; } article.appendChild(heading); heading.appendChild(link); article.appendChild(img); article.appendChild(para1); article.appendChild(keywordPara); section.appendChild(article); } } } سنشرح فيما يلي النقاط التي عالجتها الشيفرة السابقة: تُستخدم الحلقة while عادة لحذف محتوى أي عنصر من عناصر شجرة DOM، وفي حالتنا لمسح محتوى العنصر <section>. إذ نتحقق في هذه الحلقة من وجد ابن أول first child للعنصر باستمرار ونحذفه إن وجد، ثم تنتهي الحلقة عندما لا يتبقى أبناء لهذا العنصر. نضبط قيمة المتغير articles لتكون قيمة json.reponse.docs وهي المصفوفة التي تضم كل الكائنات التي تمثل المقالات التي يعيدها البحث، وذلك لجعل لتبسيط الشيفرة التي تأتي لاحقًا. تتحقق الكتلة ()if اﻷولى من وجود 10 نتائج (لأن الواجهة تعيد حتى 10 نتائج في كل مرة)، فإن كان اﻷمر كذلك، تعرض الشيفرة العنصر <nav> الذي يضم زري التنقل بين الصفحات Previous 10 و Next 10. إما إن كان عدد النتائج أقل من عشرة فلن يُعرض الزران السابقان لأن الصفحة ستتسع للنتائج. ونناقش شيفرة زري التنقل في فقرة قادمة. تتحقق الكتلة ()if الثانية من عدم وجود مقالات يعيدها البحث، فإن كان اﻷمر كذلك، لن نعرض أي شيء، بل ننشئ عنصر فقرة <p> يضم النص "لا توجد نتائج No results return"، ونلحقها بالعنصر <section>. في حال وجود نتائج، ننشئ بداية العناصر اللازمة لعرض نتائج كل مقال إخباري ومن ثم ترتيب هذه العناصر ضمن بعضها بالشكل الصحيح وإلحاقها بشجرة DOM في المكان المناسب. ولمعرفة أية خاصيات لكائنات المقالات تحتوي على المعلومات التي نريد عرضها، عُد إلى مراجع البحث عن مقالات باستخدام الواجهة NYTimes. إن معظم العمليات السابقة واضحة، لكن بعضها يستحق التوقف والشرح: استخدمنا ()for ...of للتنقل بين جميع المفاتيح المرتبطة بكل مقال ومن ثم وضع كل مفتاح ضمن عنصر <span> مخصص داخل فقرة نصية <p> ليسهل تنسيق البيانات. استخدمنا الكتلة { }if (current.multimedia.length > 0) للتحقق إن احتوى أي مقال على صور، لأن بعضها لا يمتلك أيًا منها، ونعرض الصورة اﻷولى إن وجدت، وإلا سيُرمى خطأ. كتابة شيفرة أزرار التنقل بين الصفحات حتى يعمل زرا التنقل بين الصفحات لابد من زيادة قيمة المتغير pageNumber أو إنقاصها ومن ثم إعادة تنفيذ طلب إحضار البيانات بعد تحديث قيمة المتغير في عنوان URL. ويعمل هذا لأن الواجهة البرمجية تعيد 10 نتائج فقط في كل مرة، فإن توفّر أكثر من ذلك تعيد العشرة الأولى (من 0 إلى 9) إن كانت قيمة المعامل page هي 0 (أو لم يستخدم هذا المعامل في العنوان أصلًا) وستعيد المجموعة الثانية من النتائج (من 10-19) عندما تكون قيمة المعامل page هي 1. يتيح لنا ذلك كتابة دالة بسيطة للتنقل بين الصفحات. أضف الشيفرة التالية بعد الدالة ()addEventListener لكي تستدعي الدالتين ()nextPage و ()previousPage عند النقر على الزر الموافق: nextBtn.addEventListener("click", nextPage); previousBtn.addEventListener("click", previousPage); لنعرّف اﻵن بعد إضافة الشيفرة السابقة الدالتين السابقتين: function nextPage(e) { pageNumber++; fetchResults(e); } function previousPage(e) { if (pageNumber > 0) { pageNumber--; } else { return; } fetchResults(e); } تزيد الدالة اﻷولى قيمة المتغير pageNumber ومن ثم تنفّذ الدالة مجددًا لعرض نتائج الصفحة التالية. وتعمل الدالة الثانية بنفس الطريقة تمامًا لكن بالعكس، وعلينا اتخاذ خطوة إضافية للتحقق أن قيمة المتغير pageNumber ليست صفرًا قبل إنقاص القيمة، فقد يسبب تنفيذ طلب إحضار البيانات بقيمة سالبة لهذا المتغير خطأً. فإن كانت قيمته بالفعل 0 ننهي الدالة مباشرة بتنفيذ return، ولا حاجة ﻹعادة طلب نفس النتائج الموجودة مرة أخرى. ملاحظة: بإمكانك الاطلاع على النسخة المكتملة من التطبيق على جت-هب (وبإمكانك تجربته مباشرة أيضًا). مثال عن استخدام واجهة يوتيوب البرمجية نقدم لك مثالًا أيضًا عن واجهة يوتيوب البرمجية لتدرسه وتتعلم منه. لهذا الق نظرة على مثال YouTube video search example الذي يتضمن واجهتين مرتبطتين ببعضهما: الواجهة YouTube Data API لليحث عن فيديوهات على يوتيوب وإعادة النتائج. الواجهة YouTube IFrame Player API لعرض الفيديوهات التي يعيدها البحث ضمن إطار IFrame لتشغيل فيديو. تأتي أهمية هذا المثال من كونه يعرض طريقة للربط بين واجهتين برمجيتين يقدمهما طرف خارجي وتعملان معًا. اﻷولى تتوافق مع واجهات RESTful وتعمل اﻷخرى بطريقة مشابهة للواجهة mapquest (تستخدم توابع ودوال خاصة). وتجدر اﻹشارة إلى أن كلتا الواجهتين تتطلب استخدام مكتبة جافا سكريبت في الصفحة، وأن للواجهة RESTfull دوال للتعامل مع تحضير طلبات HTTP وإعادة النتائج. لن نعرض الكثير عن هذا المثال في مقالنا، لكنك ستجد الكثير من التفاصيل في التعليقات التي تشرح طريقة العمل. ولكي تشغّل المثال تحتاج إلى: قراءة توثيق الواجهة البرمجية. زيارة الصفحة Enabled APIs page والتأكد من حالة الواجهة YouTube Data API v3 (هل هي ON) ضمن قائمة الواجهات المعروضة. الحصول على مفتاح لاستخدام الواجهة من خلال Google Cloud. استبدال القيمة ENTER-API-KEY-HERE في الشيفرة المصدرية بالمفتاح الذي حصلت عليه. تنفيذ المثال من خلال خادم ويب، فلن يعمل إن شغّلته في متصفحك مباشرة. (من خلال العنوان //:file). الخلاصة يقدم هذا المقال مدخلًا مفيدة عن استخدام الواجهات البرمجية التي يقدمها طرف خارجي ﻹضافة وظائف جديدة إلى موقعك. ترجمة -وبتصرف- لمقال: Third-party APIs. اقرأ أيضًا المقال السابق: إحضار البيانات من الخادم باستخدام جافا سكريبت البرمجة غير المتزامنة في جافاسكريبت إرسال البيانات واستلامها عبر الشبكة في جافاسكربت استخدام Fetch مع الطلبات ذات الأصل المختلط Cross-Origin في جافاسكربت
-
تطرقنا سابقًا للحديث عن واجهة برمجة التطبيقات واستخدامها في مهام مختلفة مثل معالجة مستندات الويب وشجرة DOM، ومن المهام اﻷخرى الشائعة لتطبيقات ومواقع الويب إحضار بيانات محددة من الخادم لتحديث بعض أجزاء صفحة الويب دون الحاجة إلى تحميل صفحة جديدة. وكان لهذا اﻷمر البسيط أثر هائل على أداء وسلوك صفحات الويب. سنشرح في هذا المقال التقنيات التي تسمح بتنفيذ هذه اﻷمور مثل وعلى وجه الخصوص الواجهة Fetch. ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي HTML و CSS. تكون ملمًا بلغة جافا سكريبت. تطلع على سلاسل المقالات السابقة التي ناقشت أساسيات جافا سكريبت والكائنات في جافا سكريبت. تطلع على أساسيات الواجهات البرمجية في طرف العميل. ما الذي يحدث عند طلب مورد من الخادم؟ تتكون صفحة الويب من صفحة HTML إضافة إلى عدة ملفات أخرى كملفات التنسيق CSS والسكريبتات والصور. وما يحدث وفق النموذج النمطي لتحميل الصفحة، أن متصفحك سيرسل عدة طلبات إلى الخادم لإحضار تلك الملفات اللازمة لعرض الصفحة بالشكل المطلوب، ومن المفترض أن يستجيب الخادم مرسلًا تلك الملفات. ويتكرر اﻷمر في كل مرة تزور فيها صفحة جديدة. يعمل هذا النموذج جيدًا في العديد من المواقع، لكن ماذا لو كان الموقع مخصصًا ﻹحضار بيانات من الخادم مثل موقع مكتبة؟ ومن الممكن أن تفكّر في هذا النوع من المواقع على أنه واجهة تربط المستخدم بقاعدة بيانات. فقد يسمح لك بالبحث عن نوع معين من الكتب أو قد ينصحك بكتب تستهويك وفقًا لقائمة الكتب التي استعرتها سابقًا. وعندما تفعل ذلك، لا بد من تحديث محتوى الصفحة بمجموعة الكتب الجديدة التي بحثت عنها أو نصحك بها الموقع. مع هذا، سيبقى جُل محتوى الصفحة كما هو دون تغيير مثل الترويسة والتذييل والأشرطة الجانبية. إن مشكلة النموذج النمطي في هذه الحالة هو أنه سيحضر البيانات ويعيد تحميل الصفحة بأكملها حتى لو أردنا تحديث جزء صغير منها، وهذا أمر عديم الجدوى كما يقدم تجربة سيئة لمستخدمي الموقع. ولتجنب هذا اﻷمر، تستخدم العديد من مواقع الويب واجهات جافا سكريبت البرمجية APIs لطلب البيانات من الخادم وتحديث محتوى الصفحة دون إعادة تحميلها. فعندما يبحث المستخدم عن منتج جديد مثلًا، سيطلب المتصفح فقط البيانات التي يحتاجها لتحديث الصفحة كأن يطلب قائمة بالكتب التي يجب عرضها. إن الواجهة البرمجية الرئيسية في هذه الحالة هي الواجهة Fetch التي تسمح لشيفرة جافا سكريبت في صفحة الويب بإرسال طلبات إلى الخادم لإحضار مورد محدد. وعندما يقدم الخادم البيانات المطلوبة، يمكن للشيفرة أن تستخدمها لتحديث محتوى الصفحة من خلال واجهة برمجية أخرى هي شجرة DOM عادة. وغالبًا ما تكون البيانات المطلوبة محضّرة وفق تنسيق JSON وهي صيغة مناسبة لنقل البيانات المهيكلة، لكن البيانات قد تكون أيضًا شيفرة HTML أو مجرد نص نمطي. وستجد هذا النموذج في الكثير من العديد من المواقع المصممة لتبادل البيانات مثل أمازون ويوتيوب وإي باي وغيرها. ومن خلال هذا النموذج: سيكون تحديث الصفحة أسرع بكثير ولن تُضطر إلى الانتظار حتى تُحمَّل الصفحة ككل، وهذا ما يعطي شعورًا بان الموقع أسرع وأكثر تجاوبًا. يُنزَّل كم قليل من البيانات عند كل تحديث للمحتوى وبالتالي هدرًا أقل لحزمة البيانات المخصصة للمستخدم. وعلى الرغم أن هذا الأمر لن يكون مشكلة في اﻷجهزة المكتبية المتصلة بحزمة اتصال عريضة broadband لكنها مهمة جدًا في اﻷجهزة المحمولة أو في الدول التي لا تمتلك خدمة انترنت سريعة. ملاحظة: عُرفت هذه التقنية في بداياتها باسم "جافا سكريبت غير المتزامنة و XML" واختصارًا أجاكس Ajax لأنها تميل إلى إحضار البيانات على شكل بيانات XML. وعلى الرغم أن البيانات المطلوبة حاليًا هي بيانات JSON، لكن الطريقة تبقى ذاتها ولازال المصطلح Ajax يشير إلى هذه التقنية ولتسريع اﻷمر أكثر، تُخزّن بعض المواقع أيضًا بيانات وأصولًا ضمن حاسوب المستخدم عند طلبها للمرة اﻷولى، وبالتالي ستُستخدم هذه النسخة المخزنة عند الزيارات اللاحقة بدلًا من تنزيل نسخ جديدة في كل مرة تُحمّل فيها الصفحة. ولن يُعاد تحميل المحتوى من الخادم إلا عندما يُحدّث هذا المحتوى على الخادم. الواجهة البرمجية Fetch سنتعلم أكثر عن هذه الواجهة من خلال المثاليين اﻵتيين. إحضار محتوى نصي سنطلب في هذا المثال بيانات نصية من عدة ملفات ونستخدمها لتحديث الجزء الذي يضم محتوى الصفحة. ستعمل سلسلة الملفات النصية السابقة كقاعدة بيانات مفترضة، لكن تجدر اﻹشارة إلا أننا نستخدم لطلب البيانات عادة لغة برمجة من طرف الخادم في حالات كهذه مثل PHP أو بايثون أو Node.js من قاعدة بيانات حقيقية. مع ذلك، نتوخى في مثالنا البساطة ونحاول التركيز على تقنية إحضار البيانات من طرف العميل. وحتى تبدأ العمل معنا حمّل نسختك من الملفات fetch-start.html و verse1.txt و verse2.txt و verse3.txt verse4.txt، ثم ضعها في مجلد جديد على حاسوبك. وما سنفعله لاحقًا هو إحضار أبيات محددة من قصيدة عندما نختار هذه اﻷبيات من قائمة منسدلة. أضف الشيفر التالية ضمن العنصر <script>. إذ تُخزّن هذه الشيفرة مرجعين إلى العنصرين <select> و <pre>، وعندما يختار المستخدم قيمةً، تمرر هذه القيمة كمعامل إلى الدالة ()updateDisplay: const verseChoose = document.querySelector("select"); const poemDisplay = document.querySelector("pre"); verseChoose.addEventListener("change", () => { const verse = verseChoose.value; updateDisplay(verse); }); لنعرّف بداية الدالة ()updateDisplay بوضع الشيفرة التالية تحت الشيفرة السابقة: function updateDisplay(verse) { } نبدأ كتابة شيفرة الدالة بإنشاء عنوان URL نسبي يشير إلى الملف النصي الذي نريد تحميله، إذ نحتاجه لاحقًا. وستكون قيمة العنصر <select> مطابقة دائمًا لمحتوى هذا العنصر ما لم نسند إليه قيمة أخرى من خلال السمة value مثل "Verse 1". في هذه الحالة سيكون الملف النصي الموافق هو الملف "verse1.txt" الموجود في نفس المجلد الذي يضم ملف HTML، لهذا يكفي استخدام اسم الملف كعنوان URL نسبي. وانتبه إلى أن الخوادم تتحس حالة اﻷحرف غالبًا لهذا السبب علينا إزالة الفراغ من القيمة "Verse 1" وكذلك تحويل الحرف "V" إلى الشكل الصغير "v" ومن ثم إضافة اللاحقة "txt.". ولتنفيذ ذلك، استخدم التابعين النصيين ()replace و ()toLowerCase إضافة إلى قالب حرفي template literal {...}$ كالتالي: verse = verse.replace(" ", "").toLowerCase(); const url = `${verse}.txt`; وأخيرًا أصبحنا مستعدين لاستخدام الواجهة البرمجية Fetch: //URL ومرر إليها عنوان `fetch()`استدع fetch(url) //وعدًا، وعندما يستجيب الخادم يًستدعى التابع fetch() تعيد الدالة //`then()` .then((response) => { // يرمي معالج الحدث خطأً إن أخفق الوعد if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } // وإن نحج الوعد، يعيد معالج الحدث الاستجابة على شكل نص باستدعاء التابع //الذي يعيد بدوره وعدًا، وعند إنجاز الوعد اﻷخير response.text() }) //`poemDisplay` الذي يعيد النص فننسخه إلى مربع النص `then()` يُستدعى .then((text) => { poemDisplay.textContent = text; }) //`poemDisplay` التقاط أية أخطاء أخرى وعرضها برسالة ضمن الصندوق .catch((error) => { poemDisplay.textContent = `Could not fetch verse: ${error}`; }); وإليك توضيحًا للشيفرة السابقة: إن مدخل الواجهة البرمجية Fetch هو الدالة العامة ()fetch التي تأخذ عنوان URL معاملًا لها (ولها أيضًا معامل آخر اختياري لأغراض خاصة، لكننا لم نستخدمه). الدالة ()fetch هي دالة غير متزامنة تعيد وعدًا Promise، بإمكانك مراجعة مقال استخدام الوعود في جافا سكريبت غير المتزامنة إن لم تكن على دراية بمفهوم الوعود، ثم العودة والمتابعة معنا، وستلاحظ أن هذا المقال يتحدث أيضًا عن الواجهة Fetch. تعيد الدالة ()fetch وعدًا، لهذا نمرر دالة إلى التابع ()then المرتبط بهذا الوعد. يُستدعى هذا التابع حالما يتلقى طلب HTTP ردًا من الخادم. ومن ثم نتحقق من نجاح الوعد (إنجازه) ضمن دالة معالج الحدث ونرمي رسالة خطأ إن لم ينجح. وعند النجاح، نستدعي الدالة ()response.text للحصول على جسم الاستجابة على شكل نص. إن الدالة ()response.text هي أيضًا دالة غير متزامنة، لهذا نعيد الوعد الذي تعيده ونمرره إلى التابع ()then المرتبطة بالوعد الجديد. يُستدعى هذا التابع عندما يجهز نص الاستجابة، ونضع ضمنه شيفرة تحديث محتوى العنصر <pre>. نربط أخيرًا دالة المعالجة ()catch في النهاية لالتقاط أية أخطاء ترميها أيًا من الدوال غير المتزامنة التي استدعيناها أو معالجات اﻷحداث المتعلقة بها. أحد مشكلات هذا المثال أنه لن يعرض أية قصيدة عندما يُحمّل للمرة الأولى. وﻹصلاح اﻷمر، أضف السطرين التاليين في نهاية شيفرتك (قبل وسم النهاية <script/>) لتحميل verse 1 افتراضيًا ولكي يأخذ العنصر <select> القيمة الصحيحة. updateDisplay("Verse 1"); verseChoose.value = "Verse 1"; تشغيل المثال على الخادم لن تسمح المتصفحات الحديثة بتنفيذ طلبات HTTP إن كنت تشغّل المثال على حاسوبك الشخصي بسبب قيود أمنية. وللالتفاف على الموضوع، عليك اختبار المثال على خادم ويب محلي. للمزيد من المعلومات اطلع على مقال دليل إعداد خادم ويب محلي خطوة بخطوة الذي نشرته أكاديمية حسوب. متجر معلبات أنشأنا في هذا المثال موقعًا بسيطًا يُدعى متجر المعلبات The Can Store، وهو سوبر ماركت يبيع المعلبات فقط. بإمكانك تجربة المثال مباشرة على جت-هب والاطلاع على شيفرته المصدرية. يعرض هذا الموقع افتراضيًا جميع المنتجات، لكنك تستطيع ترشيح أو فلترة هذه المنتجات والبحث عنها باستخدام أدوات التحكم الموجودة ضمن العمود اليساري للصفحة. قد تجد بعض التعقيد في شيفرة ترشيح المنتجات وفقًا للتصنيف مثل معايير البحث ومعالجة النصوص لعرض البيانات بشكل صحيح على واجهة المستخدم وغيرها. لن نشرح بالطبع كل التفاصيل في هذا المقال لكنك ستجد كما كبيرًا من التعليقات التي تشرح الشيفرة ضمن الملف can-script.js، مع ذلك سنشرح شيفرة الواجهة fetch. ستجد أولى الكتل البرمجية التي تستخدم fetch في مقدمة الملف: fetch("products.json") .then((response) => { if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return response.json(); }) .then((json) => initialize(json)) .catch((err) => console.error(`Fetch problem: ${err.message}`)); تعيد الدالة ()fetch وعدًا، فإن أنجز هذا الوعد بنجاح، ستعاد الدالة الموجودة ضمن أول كتلة ()then تضم الاستجابة response من الشبكة، وما نفعله ضمن هذه الدالة هو: التحقق من عدم إرسال الخادم خطأً (مثل Not Found 404) وإن حدث ذلك، نرمي الخطأ. استدعاء التابع ()json للعمل على الاستجابة واستخلاص البيانات منها على شكل كائن JSON، ثم نعيد الوعد الذي يعيده response.json. نمرر دالة إلى التابع ()then المرتبط بالوعد المعاد، كما نمرر إلى هذه الدالة كائنًا يتضمن بيانات الاستجابة وفق تنسيق JSON بعد تمريرها إلى الدالة ()initialize التي تبدأ عملية عرض جميع المنتجات على واجهة المستخدم. ولمعالجة اﻷخطاء، نربط كتلة ()catch. في نهاية السلسلة، وستعمل شيفرة هذه الكتلة إذا وقع خطأ لسبب ما. نضع ضمن هذه الكتلة دالة يُمرر إليها الكائن err كمعامل ويُستخدم لتسجيل طبيعة الخطأ الذي حصل ونعرضه من خلال الدالة ()console.error. تتعامل المواقع المكتملة مع اﻷخطاء بطريقة شمولية أفضل، وذلك بعرض رسالة على شاشة المستخدم. كما قد تعرض أيضًا خيارات لحل المشكلة، لكننا لن نحتاج هنا إلا للتابع ()console.error. بإمكانك أيضًا اختبار حالات الفشل بنفسك: انسخ ملفات التمرين على حاسوبك. شغل الشيفرة باستخدام خادم ويب محلي. عدّل مسار الملف الذي نحضره مثل "produc.json" بدلًا من "product.json" (تأكد من ارتكاب خطأ كتابي). حمّل اﻵن الملف index.html في المتصفح (localhost:8000/index.html) ثم ألق نظرة على طرفية جافا سكريبت، وستجد رسالة خطأ مشابهة للرسالة "Fetch problem: HTTP error: 404". ستجد كتلة fetch الثانية ضمن الدالة ()fetchBlob: fetch(url) .then((response) => { if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return response.blob(); }) .then((blob) => showProduct(blob, product)) .catch((err) => console.error(`Fetch problem: ${err.message}`)); تعمل هذه الشيفرة تمامًا كسابقتها ما عدا أننا استخدمنا التابع ()blob بدلًا من ()json لأننا نريد في هذه الحالة الحصول على ملف صورة في الاستجابة وسيكون حينها تنسيق البيانات على شكل كائن بيانات ثنائية Blob (اختصارًا للعبارة "كائن ضخم ثنائي Binry Large Object"). ويُستخدم هذا الكائن لتمثيل كائنات ضخمة مشابهة للملفات مثل الصور والفيديو. وبمجرد أن نحصل على الكائن blob، نمرره إلى الدالة ()showProduct التي تعرضه. الواجهة البرمجية XMLHttpRequest سترى في بعض اﻷحيان وخاصة في الشيفرة اﻷقدم واجهة برمجية أخرى تُدعى XMLHttpRequest (وتختصر أحيانًا إلى "XHR") تُستخدم في إجراء طلبات HTTP. وقد سبقت هذه الواجهة الواجهة البرمجية Fetch وكانت أولى الواجهات التي استخدمت على نطاق واسع لتنفيذ تقنية AJAX. لكن ننصحك باستخدام Fetch إن أمكن فهي واجهة أبسط وتضم ميزات أكثر من الواجهة XMLHttpRequest. لن نقدم مثالًا عن استخدام الواجهة XMLHttpRequest، لكننا سنعرض نسخة XMLHttpRequest من مثال متجر المعلبات. سيبدو الطلب كالتالي: const request = new XMLHttpRequest(); try { request.open("GET", "products.json"); request.responseType = "json"; request.addEventListener("load", () => initialize(request.response)); request.addEventListener("error", () => console.error("XHR error")); request.send(); } catch (error) { console.error(`XHR error ${request.status}`); } هناك خمس مراحل: إنشاء كائن XMLHttpRequest. جديد. استدعاء التابع ()open لتهيئة الكائن الجديد. إضافة مترصد للحدث load يُنفَّذ عند إكتمال الطلب بنجاح. ونستدعي الدالة ()initialize بعد تزويدها بالبيانات ضمن دالة مترصد الحدث. إضافة مترصد حدث إلى الحدث error الذي يقع عندما يواجهة الطلب خطأً. إرسال الطلب ولا بد من تغليف الشيفرة السابقة ضمن كتلة try...catch للتعامل مع الأخطاء التي قد تحدث عند استخدام التابعين ()open أو ()send. ومن الجيد أن تدرك أن الواجهة Fetch هي تطوير للواجهة XMLHttpRequest، وأن تفهم الطريقة المتبعة في التعامل مع اﻷخطاء في كلتا الواجهتين. الخلاصة شرحنا في هذا المقال طريقة استخدام الواجهة البرمجية Fetch في إحضار البيانات من الخادم، وسنتناول بعض المواضيع التي وردت في المقال بمزيد من التفصيل في مقالاتنا التالية. ترجمة-وبتصرف- للمقال: Fetching data from the server. اقرأ أيضًا المقال السابق: الواجهات البرمجية والتعامل مع شجرة DOM في جافا سكريبت إرسال البيانات واستلامها عبر الشبكة في جافاسكربت أساسيات بناء تطبيقات الويب التعامل مع طلبات HTTP في Node.js التخزين المؤقت Cache ومقابس الويب Webscockets في PHP