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

ابراهيم الخضور

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

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

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

كل منشورات العضو ابراهيم الخضور

  1. تعرفنا في مقال سابق على أساسيات العمل مع استمارات الويب وسنلقي نظرة أقرب في هذا المقال على العناصر التي تُستخدم لهيكلة استمارة الويب وإعطاء كل عنصر دلالته الخاصة. فمرونة استمارات ويب تجعلها إحدى الهيكليات اﻷكثر تعقيدًا في HTML، حيث تستطيع بناء أي نوع من استمارات الويب اﻷساسية باستخدام وسم الاستمارات المخصص <form> وسماته. وبالتالي سيكون اختيار الهيكلية الصحيحة لاستمارة الويب أساسيًا لضمان قابلية استخدام هذه الاستمارة وسهولة الوصول إليها accessible form. كما ننصحك قبل المتابعة في قراءة هذا المقال أن تطلع على سلسلة المقالات "مدخل إلى HTML" للتعرف على هيكلية صفحات ويب باستخدام لغة HTML وتتعرف على أهم عناصرها. العنصر <form> يُعرّف العنصر <form> أنه استمارة أو نموذج مع سمات تحدد سلوكه. وكلما أردت بناء استمارة ويب عليك أن تبدأ باستخدام العنصر <form> وتضع ضمنه المحتوى المطلوب. وتستطيع العديد من تقنيات ويب الحديثة وإضافات المتصفحات browser plugins اكتشاف عناصر الاستمارات وتنفيذ خطافات مخصصة لها كي يسهل استخدامها. تحذير: يمنع صراحة وضع عنصر <form> داخل عنصر <form> آخر، لأن تداخل هذه العناصر قد يؤدي إلى سلوك غير متوقع. ومن الممكن دائمًا استخدام عناصر تحكم بالاستمارة تقع خارج العنصر <form>. وفي هذه الحالة، لن يكون هناك أي ترابط بين عناصر التحكم والاستمارة ما لم تربط بنفسك هذه العناصر بالاستمارة باستخدام السمة for لعناصر التحكم. وقد أدخلت هذه السمة كي تربط العناصر الموجودة خارج الاستمارة ضمنيًا باستمارة ويب محددة. وسنتعرف تاليًا على العناصر الهيكلية التي تجدها داخل استمارة الويب. العنصران <fieldset> و <legend> يُعد العنصر <fieldsset> مناسبًا ﻹنشاء مجموعات من عناصر التحكم لها نفس الغاية سواء للتنسيق أو إعطاء دلالة خاصة. وبإمكانك وضع عنوان للعنصر <fieldset> بتضمين العنصر <legend> تحت وسم البداية الخاص به، وهكذا يصف محتوى العنصر <legend> الغاية من استخدام العنصر <fieldset>. تستخدم الكثير من التقنيات المساعدة العنصر <legend> على أنه جزء من عنوان كل عنصر تحكم يقع داخل <fieldset> وتقرأ بعض قارئات الشاشات مثل Jaws و NVDA محتوى العنصر <legend> قبل قراءة عنوان عنصر التحكم. إليك مثالًا: <form> <fieldset> <legend>Fruit juice size</legend> <p> <input type="radio" name="size" id="size_1" value="small" /> <label for="size_1">Small</label> </p> <p> <input type="radio" name="size" id="size_2" value="medium" /> <label for="size_2">Medium</label> </p> <p> <input type="radio" name="size" id="size_3" value="large" /> <label for="size_3">Large</label> </p> </fieldset> </form> ملاحظة: ستجد هذا المثال على جت-هب وبإمكانك تجربته مباشرة. عند قراءة الشيفرة السابقة، ستنطق قارئات الشاشة العبارة "Fruit juice size small" المتعلقة بعنصر التحكم اﻷول، والعبارة "Fruit juice size medium" للعنصر الثاني، والعبارة "Fruit juice size large" للثالث. إن حالة الاستخدام التي يعرضها هذا المثال مهمة جدًا؛ فإن كان لديك مجموعة من أزرار الاختيار radio buttons، عليك في هذه الحالة وضعها ضمن عنصر <fieldset> واحد. ستجد العديد من حالات الاستخدام لهذه العناصر، لكنها تُستخدم عمومًا في تجزئة الاستمارة. إذ توزّع استمارات الويب الطويلة على عدة صفحات، لكن إن كان لا بد من وضعها في صفحة واحدة، من الأفضل حينها تجزئة الاستمارة من خلال وضع العناصر التي تؤدي غاية محددة ضمن عناصر <fieldset> مخصصة لتحسين قابلية استخدام الاستمارة. ونظرًا لتأثير العنصر <fieldset> على التقنيات المساعدة، فهو عنصر أساسي في بناء استمارات تدعم شمولية الوصول أو سهولة الوصول accessibility، وتبقى عليك مسؤولية استخدامها بالشكل الصحيح. حاول إن أمكن أن تسمع ما تنطقه قارئات الشاشة عندما تبني استمارتك، فإن بدا الأمر غريبًا، جرّب أن تحسن هيكلية الاستمارة الخاصة بك. العنصر <label> رأينا في مقال سابق أن العنصر <label> يعرّف لنا عنوانًا لوصف عنصر التحكم الموجود في استمارة الويب. وهذا العنصر شديد اﻷهمية لاسيما إن أردت بناء استمارة يدعم سهولة الوصول، فعندما تستخدمها بالشكل الصحيح، ستنطقها قارئات الشاشة مع أية توجيهات أخرى، كما أنها مفيدة للمستخدمين الأصحاء كذلك لتوضح لهم دلالة مكونات الاستمارة. إليك المثال الذي خبرناه في المقال السابق: <label for="name">Name:</label> <input type="text" id="name" name="user_name" /> عندما يُربط العنوان بالعنصر <input> من خلال السمة for التي تحمل قيمة معرّف عنصر اﻹدخال، ستنطق قارئات الشاشة العبارة "Name, edit text". وهناك طريقة أخرى لربط عنصر تحكم الاستمارة بالعناوين عن طريق وضع عنصر التحكم داخل العنوان <label> صراحة كما يلي: <label for="name"> Name: <input type="text" id="name" name="user_name" /> </label> وحتى في هذه الحالات، يُعد استخدام السمة for ممارسة مفضلة كي نضمن فهم التقنيات المساعدة للعلاقة الموجودة بين العنوان وعنصر التحكم، فإن لم يكن هناك عنوان أو لم يرتبط عنصر التحكم في استمارة ويب صراحة أو ضمنيًا بعنوان، ستنطق قارئات الشاشة عبارة مثل "Edit text blank" وهذا لن يساعد أبدًا. العناوين قابلة للنقر أيضًا من الإيجابيات اﻷخرى لإعداد عناوين لعناصر تحكم استمارة الويب هو إمكانية النقر على العنوان لتفعيل عنصر التحكم المرتبط به. ولهذا اﻷمر فائدته عند استخدام عناصر اﻹدخال النصية، إذ سيتلقى العنصر تركيز الدخل بالنقر عليه أو على العنوان المرافق له. وتظهر أهميته الفعلية عند استخدام أزرار الاختيار radio buttons وصناديق التحقق check boxes، فقد تكون المنطقة التي يشغلها الزر ضيقة يصعب نقرها في بعض اﻷجهزة لذلك من السهل حينها النقر على العنوان الموافق لتفعيل الخيار. فالنقر على العنوان "I like cherry" في مثالنا التالي سيغير حالة عنصر اﻹدخال taste_cherry: <form> <p> <input type="checkbox" id="taste_1" name="taste_cherry" value="cherry" /> <label for="taste_1">I like cherry</label> </p> <p> <input type="checkbox" id="taste_2" name="taste_banana" value="banana" /> <label for="taste_2">I like banana</label> </p> </form> ملاحظة: ستجد هذا المثال على جت-هب وبإمكانك أيضًا تجربته مباشرة. عناوين متعددة لا يوجد في الواقع ما يمنعك من وضع عدة عناوين لعنصر التحكم نفسه، لكن الفكرة سيئة من منظور اﻷدوات المساعدة، فقد تُخطئ تلك اﻷدوات في التعامل معها. وإن أردت استخدام عناوين متعددة، عليك وضع عنصر التحكم مع عناوينه المرتبطة به ضمن عنصر <label> وحيد: <p>Required fields are followed by <span aria-label="required">*</span>.</p> <!-- ينجح اﻷمر كالتالي --> <!--div> <label for="username">Name:</label> <input id="username" type="text" name="username" required> <label for="username"><span aria-label="required">*</label> </div--> <!-- لكن من اﻷفضل أن ينجز كالتالي --> <!--div> <label for="username"> <span>Name:</span> <input id="username" type="text" name="username" required> <span aria-label="required">*</span> </label> </div--> <!-- وهذا هو التنفيذ اﻷفضل --> <div> <label for="username">Name: <span aria-label="required">*</span></label> <input id="username" type="text" name="username" required /> </div> See the Pen web- form-structure1 by Hsoub Academy (@HsoubAcademy) on CodePen. توضح الفقرة النصية في بداية الشيفرة أن الحقول المعلمة * ضرورية. ولا بد من تضمين هذه الفقرة في البداية وقبل أن يطبق محتواها، لكي يتمكن المستخدمون ضعاف البصر أو حتى اﻷصحاء -الذين يعتمدون على قارئات الشاشة- من فهم معناها قبل أن يصلوا إلى الحقول الضرورية في الاستمارة. وعلى الرغم من أنها علامة * مفيدة للأصحاء وواضحة، لكن لا يمكن الاعتماد عليها كليًا. إذ ستلفظ قارئات الشاشة هذا المحرف على الشكل "star"، ولن تكون مفيدة هنا، ولا بد أن تظهر الكلمة "مطلوب required" عندما يمرر المستخدم السليم مؤشر الفأرة فوق العنوان، ويُنجز هذا اﻷمر باستخدام السمة title، أما نطق قارئات الشاشة لقيمة هذه السمة، فيعتمد على إعداداتها. لهذا من اﻷسلم أن نضيف أيضًا السمة aria-label التي تنطقها قارئات الشاشة دائمًا. تتفاوت فعالية الطرق في المثال السابق في تنفيذ اﻷمر: لا يُقرأ العنوان في الطريقة اﻷولى إطلاقًا مع عنصر الدخل، فما تحصل عليه هو لفظ العبارة "edit text blank" إضافة إلى العناوين الفعلية التي تُقرأ بشكل منفصل، وستربك العناوين المتعددة قارئ الشاشة. تبدو اﻷشياء أوضح بقليل في الطريقة الثانية، إذ يُقرأ العنوان مع عنصر الدخل "name star name edit text required"، لكن العناوين ستُقرأ أيضًا بشكل منفصل، وهذا مربك قليلًا. تحسن الوضع هنا لوجود عنوان مرتبط بالعنصر <input>. الطريقة الثالثة هي اﻷفضل، إذ يُقرأ العنوان الفعلي في نفس الوقت ويُقرأ عنصر العنوان بالشكل "name required edit text". ملاحظة: قد تختلف النتائج قليلًا وفقًا لقارئ الشاشة الذي تستخدمه. ملاحظة: ستجد هذا المثال على جت-هب وبإمكانك تجربته مباشرة. لا تحاول اختيار المثال بشكله الحالي، ابق على طريقة واحدة وحوّل الطريقتين الباقيتين إلى تعليقات، كي لا تُربك قارئات الشاشة بوجود عناوين متعددة وعناصر إدخال متعدد لها نفس الاسم. هيكليات شائعة لتنظيم استمارات الويب تذكر دائمًا أن هيكلة استمارة الويب تكون باستخدام HTML، وتستطيع استخدام كامل إمكانيات لغة التوصيف هذه في هيكلة الاستمارة. وقد تلاحظ من اﻷمثلة السابقة أن تغليف العنوان وعنصر التحكم ضمن العنصر <li> ممارسة شائعة وتوضع هذه العناصر بدورها ضمن عنصر قائمة <ul> أو <ol>، كما يُستخدم أيضًا العنصر <div>. وينصح باستخدام القوائم في حال كانت عناصر اﻹدخال على شكل صناديق تحقق أو أزرار خيارات متعددة. وإضافة إلى العنصر <fieldset>، يشيع استخدام عناصر العناوين الرئيسية في HTML مثل <h1> و <h2> والعنصر <section> لبناء هيكليات أكثر تعقيدًا لاستمارات الويب. ويعود اﻷمر إليك في انتقاء اﻷسلوب المناسب لكتابة الشيفرة على أن تكون النتيجة استمارة قابلةً للاستخدام وتراعي شمولية الوصول. وينبغي أن يوضع كل قسم يضم وظيفة محددة داخل عنصر <section> منفصل واستخدام العنصر <fieldset> لاحتواء أزرار الاختيارات المتعددة. تطبيق عملي: بناء هيكل لاستمارة ويب لنضع اﻵن ما تعلمناه من أفكار موضع التنفيذ ونبني استمارة أكثر عمقًا للاستمارات بإضافة وسيلة دفع. يتضمن الاستمارة الجديدة عددًا من أنواع عناصر التحكم التي قد لا تفهمها، لكن لا بأس حاليًا، إذ سنعرض تفاصيل أكثر عنها في مقال لاحق. وكل ما عليك اﻵن قراءة الوصف بتمعن أثناء متابعتك للتعليمات وفهمك للطريقة التي نغلف فيها العناصر في الاستمارة كي نبني هيكليتها، ولماذا فعلنا ذلك. أنشئ نسخة محلية من ملف التطبيق في مجلد جديد على حاسوبك. أنشئ تاليًا استمارةك باستخدام العنصر <form>. <form> أضف ترويسة وعنوان رئيسي ضمن العنصر <form>، لتعرض للمستخدم كيفية تمييز الحقول المطلوبة: <h1>Payment form</h1> <p> Required fields are followed by <strong><span aria-label="required">*</span></strong>. </p> نضيف تاليًا قسمًا أكبر من الشيفرة تحت الشيفرة السابقة، وسترى كيف نغلّف حقول معلومات الاتصال ضمن عنصر <section> مستقل. لدينا أيضًا مجموعة من ثلاث أزرار اختيار من متعدد، نضع كل منها ضمن عنصر قائمة <li> مستقل. ولدينا عنصرا إدخال نصيان <input> مع العنوان المرافق لكل منهما يوضع كل منهما ضمن فقرة نصية <p>، ولدينا أيضًا عنصر إدخال نصي لإدخال كلمة المرور: <section> <h2>Contact information</h2> <fieldset> <legend>Title</legend> <ul> <li> <label for="title_1"> <input type="radio" id="title_1" name="title" value="A" /> Ace </label> </li> <li> <label for="title_2"> <input type="radio" id="title_2" name="title" value="K" /> King </label> </li> <li> <label for="title_3"> <input type="radio" id="title_3" name="title" value="Q" /> Queen </label> </li> </ul> </fieldset> <p> <label for="name"> <span>Name: </span> <strong><span aria-label="required">*</span></strong> </label> <input type="text" id="name" name="username" required /> </p> <p> <label for="mail"> <span>Email: </span> <strong><span aria-label="required">*</span></strong> </label> <input type="email" id="mail" name="usermail" required /> </p> <p> <label for="pwd"> <span>Password: </span> <strong><span aria-label="required">*</span></strong> </label> <input type="password" id="pwd" name="password" required /> </p> </section> يضم القسم الثاني <section> من الاستمارة معلومات الدفع حيث نضيف ثلاثة عناصر تحكم مع عناوينها ويغلف كل منها ضمن فقرة نصية <p>. العنصر اﻷول هو قائمة منسدلة <select> لاختيار نوع بطاقة االائتمان، والثاني عنصر إدخال نصي <input> من النوع tel ﻹدخال رقم بطاقة اﻹئتمان (يمكن اختيار النوع number لكننا لم نجد شكله مناسبًا لواجهة المستخدم)، والثالث عنصر إدخال من النوع text ﻹدخال تاريخ إنتهاء صلاحية البطاقة، ويتضمن العنصر اﻵخير السمة placeholder التي تشير إلى التنسيق الصحيح، والسمة pattern التي تختبر صحة التنسيق الذي يدخله المستخدم. وقد أدخلت هذه العناصر اﻷخيرة في HTML5: <section> <h2>Payment information</h2> <p> <label for="card"> <span>Card type:</span> </label> <select id="card" name="usercard"> <option value="visa">Visa</option> <option value="mc">Mastercard</option> <option value="amex">American Express</option> </select> </p> <p> <label for="number"> <span>Card number:</span> <strong><span aria-label="required">*</span></strong> </label> <input type="tel" id="number" name="cardnumber" required /> </p> <p> <label for="expiration"> <span>Expiration date:</span> <strong><span aria-label="required">*</span></strong> </label> <input type="text" id="expiration" name="expiration" required placeholder="MM/YY" pattern="^(0[1-9]|1[0-2])\/([0-9]{2})$" /> </p> </section> نضيف القسم اﻷخير البسيط الذي يضم زرًا <button> من النوع submit ﻹرسال البيانات، لهذا ضع الشيفرة التالية أسفل شيفرة الاستمارة: <section> <p> <button type="submit">Validate the payment</button> </p> </section> أكمل الاستمارة بإضافة وسم اﻹغلاق للعنصر <form>: </form> طبقنا كذلك تنسيقات CSS إضافية على الاستمارة، فإن أردت إجراء تغييرات على مظهره، انسخ التنسيقات التي تحتاجها وعدّل عليه. See the Pen Untitled by Hsoub Academy (@HsoubAcademy) on CodePen. الخلاصة بهذا نكون قد استعرضنا أهم العناصر المستخدمة لهيكلة استمارة الويب وإعطاء كل عنصر دلالته الخاصة، وشرحنا الطريقة المثلى لاستخدام هذه العناصر في هيكلة استمارة الويب لتكون قابلةً للاستخدام وتدعم شمولية الوصول بالطريقة الصحيحة، وسنتوسع في شرح الميزات التي عرضناها هنا في مقالات قادمة. ترجمة -وبتصرف- للمقال: How to structure a web form اقرأ أيضًا المقال السابق: إضافة تنسيق بسيط لاستمارة الويب وإرسال بياناتها للخادم متحكمات واجهة المستخدم وكيفية عرضها: متحكمات الدخل أدوات سهولة الوصول Accessibility اللازمة في عملية تطوير الويب معالجة مشاكل سهولة الوصول Accessibility الشائعة للتوافق مع المتصفحات
  2. تعرفنا في المقال السابق على أساسيات العمل مع استمارات الويب Web forms وأهميتها كوسيلة للتفاعل بين المستخدم وموقع الويب، ووضحنا بمثال عملي كيفية بناء استمارة تواصل بسيطة باستخدام عناصر HTML الأساسية، وسنشرح في مقال اليوم كيفية تنسيق هذه الاستمارة لمنحها منظرًا أكثر احترافية، وطريقة التعامل مع إرسال البيانات التي يكتبها المستخدم في هذه الاستمارة إلى خادم الويب. التنسيق اﻷساسي لاستمارة الويب باستخدام CSS بعد أن انتهيت في المقال السابق من كتابة شيفرة HTML التي تحدد مكونات أو عناصر الاستمارة الأساسية وعرضت الصفحة في المتصفح، لن يعجبك مظهر هذه الاستمارة بالتأكيد، حيث سيبدو منظرها بدائيًا جدًا كما في الصورة التالية: ملاحظة: إن كنت تعتقد أن شيفرتك ليس صحيحة، قارن الكود الذي كتبته مع هذه النسخة المكتملة من التطبيق على جت-هاب للاطلاع على النسخة بعد تنسيقها (وبإمكانك تجربتها مباشرة) أيضًا. لتحسين مظهر الاستمارة نحتاج لتنسيقها باستخدام لغة CSS وسنركز في مقال اليوم فقط على إضافة بعض تنسيقات CSS البسيطة كي نحسّن مظهر الاستمارة العام، لكننا لن نتطرق إلى تنسيق عناصر الاستمارات بشكل مكثف فهذا اﻷمر واسع ومتشعب، فاستمارات الويب تتكون عادة من عدة عناصر مختلفة مثل الحقول النصية والأزرار ومربعات الاختيار والقوائم المنسدلة وغيرها وكل نوع من هذه العناصر قد يتطلب تنسيقًا خاصًا به، كما أن هناك تنسيقات تتعلق بسهولة الوصول accessibility، وتنسيقات أخرى تتعلق بعرض رسائل خطأ عند إدخال بيانات غير صحيحة وغيرها من التفاصيل التي تقع خارج نطاق مقالنا حاليًا. لتنسيق الاستمارة أضف بداية العنصر <style> إلى كود صفحتك ضمن الترويسة <head>، وسيبدو اﻷمر كالتالي: <style> … </style> بعدها أضف الشيفرة التالية ضمن وسمي البداية والنهاية <style> <style/> للعنصر السابق: body { /* ضبط موقع الاستمارة منتصف الصفحة */ text-align: center; } form { display: inline-block; /* اﻹطار الخارجي للاستمارة */ padding: 1em; border: 1px solid #ccc; border-radius: 1em; } ul { list-style: none; padding: 0; margin: 0; } form li + li { margin-top: 1em; } label { /* تنسيق الحجم والمحاذاة بشكل منتظم */ display: inline-block; min-width: 90px; text-align: right; } input, textarea { /*للتأكد من أن كل الحقول النصية لها نفس إعدادات الخط monospace خط أحادي الفراغ textarea افتراضيًا، يكون للعناصر */ font: 1em sans-serif; /* حجم متساوي لجميع الحقول النصية */ width: 300px; box-sizing: border-box; /* ضبط حواف حقول الاستمارة */ border: 1px solid #999; } input:focus, textarea:focus { /* تظليل إضافي للعناصر التي تتلقى تركيز الدخل */ border-color: #000; } textarea { /* محاذاة الحقول النصية إلى جوار عناوينها*/ vertical-align: top; /* تأمين مساحة للكتابة ضمنها */ height: 5em; } .button { /* محاذاة الزر مع الحقول النصية */ padding-left: 90px; /* نفس قياس العناوين */ } button { /*هوامش إضافية تماثل المسافة بين العناوين والحقول النصية المقابلة */ margin-left: 0.5em; } احفظ التغيّرات وأعد تحميل الصفحة، وسيبدو الاستمارة أفضل حالًا كما يلي: See the Pen webform-1 by Hsoub Academy (@HsoubAcademy) on CodePen. ملاحظة: يمكنك مقارنة الكود الذي كتبته مع هذه النسخة من التطبيق على جت-هاب للاطلاع على النسخة بعد تنسيقها ، وبإمكانك تجربتها مباشرة أيضًا. إرسال بيانات الاستمارة إلى الخادم نأتي إلى القسم الأخير من التطبيق وقد يكون الأصعب، وهو التعامل مع تسليم بيانات هذه الاستمارة إلى الخادم أو بمعنى آخر التخاطب بين العميل والخادم، حيث يحدد العنصر <form> كما ذكرنا في المقال السابق أين ستذهب البيانات وكيف ستنقل للخادم من خلال السمتين action و method. إذ تحدد action الوجهة النهائية للبيانات المُدخلة في الاستمارة، بينما تحدد method الطريقة التي سيتم بها إرسال البيانات إلى الخادم وهناك طريقتان رئيسيتان يمكن استخدامهما: الأولى هي GET حيث تلحق بيانات الاستمارة بعنوان URL وترسل كجزء من رابط الصفحة ولا تناسب هذه الطريقة إرسال بيانات حساسة لأن البيانات المرسلة ستكون مرئية في شريط العنوان، والثانية هي POST حيث ترسل البيانات كجزء من جسم طلب HTTP وهي تناسب إرسال البيانات الحساسة أو إرسال كميات كبيرة من البيانات من العميل للخادم. وكنا قد زودنا سابقًا كل عنصر من عناصر الاستمارة بالسمة name وهي ضرورية عند العمل مع طرفي العميل والخادم. إذ تخبر هذه السمة المتصفح على الاسم الذي سيطلقه على كل جزء من البيانات، أما الخادم فسيتعامل مع هذه البيانات وفق هذا الاسم، ثم ترسل البيانات إلى الخادم على شكل أزواج أو ثنائيات مكونة من اسم/قيمة. فلتسمية البيانات في الاستمارة ستحتاج إلى السمة name لكل عنصر تحكم يجمع جزءًا من البيانات المطلوبة. لنلق نظرة على شيفرة الاستمارة ونتذكر عناصرها: <form action="/my-handling-form-page" method="post"> <ul> <li> <label for="name">Name:</label> <input type="text" id="name" name="user_name" /> </li> <li> <label for="mail">Email:</label> <input type="email" id="mail" name="user_email" /> </li> <li> <label for="msg">Message:</label> <textarea id="msg" name="user_message"></textarea> </li> <li class="button"> <button type="submit">Send your message</button> </li> </ul> </form> على جانب العميل، سيرسل النموذج ثلاثة أجزاء من البيانات أسماؤها هي "user_name" و "user_email" و "user_message" إلى عنوان URL التالي "/my-handling-form-page/" باستخدام الطلب HTTP POST. على جانب الخادم، سيكون لدينا سكريبت بلغة برمجة معينة موجود على العنوان "/my-handling-form-page/" يستقبل البيانات المرسلة من النموذج وفق ثلاثة قوائم من الشكل مفتاح/قيمة ضمن طلب HTTP. أما طريقة تعامل السكريبت مع هذه البيانات فهو أمر عائد للمبرمج، فلكل لغة برمجة تعمل على الخادم آلية مخصصة للتعامل مع البيانات. هذا الموضوع خارج إطار مقالنا الحالي لكننا سنعرض بعض الأمثلة المختلفة مع شرحها بالتفصيل في مقال لاحق. الخلاصة تهانينا! لقد بنيت أول استمارة ويب خاصة بك ونسقتها بطريقة أنيقة وتعرفت على أساسيات حول التعامل مع بياناتها، لا زالنا بالطبع في بداية التعامل مع الاستمارات، وسنتابع الغوص في بناء استمارات ويب وهيكلتها وتنسيقها وإرسال بياناتها للخادم في مقالات قادمة، فإمكانات استمارات الويب أكبر بكثير مما عرضناه في هذا المقال. ترجمة -وبتصرف- للجزء الثاني من المقال: Your first form اقرأ أيضًا المقال السابق: أساسيات التعامل مع استمارات الويب Web Forms كيفية تنسيق الموقع الإلكتروني باستخدام تعليمات HTML تعرف على أساسيات لغة CSS التحقق من سهولة الوصول لصفحات الويب معالجة بيانات طلبيات HTTP والتعامل مع أخطاء رفع الملفات في PHP
  3. سنبدأ في هذا المقال والمقالات التالية شرح أساسيات التعامل مع استمارات الويب Web forms. فاستمارات الويب هي أداة قوية للتفاعل مع المستخدمين وخاصة في جمع بيانات عنهم أو السماح لهم في التحكم بواجهة المستخدم، سنحرص على تغطية كامل ميزات هذه الأداة القوية ونشرح كل ما يتعلق بها بما في ذلك كتابة شيفرة HTML وتنسيق عناصر التحكم وتسليم البيانات إلى الخادم. متطلبات العمل على هذه السلسلة الاطلاع على سلسلة المقالات مدخل إلى HTML، وعندها لن تجد صعوبة في فهم القسم التمهيدي من مقالات السلسلة، وستكون قادرًا على الاستفادة من معلومات مقال أدوات التحكم اﻷصلية لاستمارات الويب. تعلم أساسيات التنسيق باستخدام CSS وأساسيات البرمجة بلغة جافا سكريبت. إذ عليك تعلم بعض التقنيات الخاصة في تنسيق عناصر التحكم التي تضيفها للاستمارة، وكتابة سكريبتات التعامل مع تقييم بيانات الاستمارة وإنشاء استمارات ويب مخصصة. ولهذا كان لا بد من عرض استمارات الويب في سلسلة مقالات خاصة بها ولم نخلطها مع سلاسل المقالات التي تحدثنا فيها عن HTML و CSS وجافا سكريبت. فعناصر الاستمارات أكثر تعقيدًا من عناصر HTML الأخرى، وتتطلب استخدامًا محددًا لتقنيات CSS وجافا سكريبت إن أردت الحصول على كامل إمكانيات الاستمارات. ملاحظة: إن كنت تستخدم حاسوبًا أو جهازًا لوحيًا أو غيره من الأجهزة التي لاتمكّنك من إنشاء ملفاتك الخاصة وتنفيذ الشيفرات البرمجية محليًا، فجرِّب الشيفرة التي ستجدها في الأمثلة من خلال برامج كتابة شيفرة على الإنترنت مثل Glitch أو JSBin فهذه البيئات تساعدك على كتابة شيفرتك وتنفيذها دون الحاجة إلى تثبيت برمجيات خاصة على جهازك. سنبدأ في هذا المقال التمهيدي بالتعرف بشكل مفصل على استمارات الويب وأهميتها وننشئ أول استمارة ويب خاصة بنا من خلال تصميم استمارة تواصل بسيطة contact form وتنفيذها برمجيًا باستخدام عناصر HTML، ونستكمل في المقال التالي تنسيقها. ما هي استمارات الويب Web Forms تُعد استمارات ويب إحدى النقاط اﻷساسية التي تعزز التواصل بين المستخدم و موقع الويب أو التطبيق. إذ تتيح للمستخدم إدخال البيانات التي تُرسل عادة إلى الخادم كي يعالجها ويخزنها في قواعد البيانات، أو تستخدم لتحديث تصميم واجهة المستخدم مباشرة بطريقة ما (مثل إضافة عنصر جديد إلى قائمة أو إظهار وإخفاء بعض معالم الواجهة). تتكون استمارات الويب المبنية على أساس HTML من عنصر تحكم بالاستمارة أو أكثر، إضافة إلى عناصر أخرى تساعد في هيكلة الاستمارة عمومًا، لهذا يُشار إليها أحيانًا باسم استمارات HTML. وقد تكون عناصر التحكم حقولًا نصية مفردة أو متعددة اﻷسطر أو قوائم منسدلة drop-down box أو أزرار أو صناديق تحقق check boxes أو أزرار اختيار من متعدد radio buttons، وتنشأ هذه العناصر عادة باستخدام العنصر <input> مع وجود بعض العناصر اﻷخرى التي سنطلع عليها تباعًا. يمكن برمجة عناصر التحكم أيضًا كي نجبر المستخدم إدخال صيغ أو قيم مخصصة (من أجل تقييم الاستمارة) وأن نقرن هذه العناصر بنصوص توضيحية تصف الغاية من هذه العناصر لضعيفي البصر والمستخدم السليم لتعزيز الشمولية وسهولة الوصول Accessibility. تصميم استمارة الويب اﻷولى من الجيد التراجع إلى الخلف قليلًا قبل الشروع في كتابة الشيفرة والتفكير مليًا بالاستمارة الذي نريد تصميمه. إذ يساعدك تصور مخطط أولي لاستمارةك في تحديد مجموعة البيانات التي تريد أن تطلبها من المستخدم. وتذكّر أن الاستمارات الكبيرة تزيد عدد المستخدمين الذي يرفضون إكمالها وذلك من منظور تجربة المستخدم UX. لهذا أبق الاستمارة بسيطًا وركّز على البيانات التي تحتاجها بالفعل. إن خطوة تصميم الاستمارة مهمة جدًا في بناء الموقع أو التطبيق. لن نغطي في هذا المقال تصميم استمارات الويب من منظور تجربة المستخدم لكن ستجد الكثير من اﻷفكار في كتاب مدخل إلى تجربة المستخدم من إنتاج أكاديمية حسوب. سنبني استمارة ويب تتيح للمستخدمين التواصل معنا، ولتبدأ بالرسم التوضيحي لهذا الاستمارة كما في الشكل التالي: تتضمن الاستمارة ثلاث حقول نصية وزر واحد، وتطلب من المستخدم إدخال اسمه وبريده اﻹلكتروني والرسالة التي يريد إرسالها كي تُخزّن على الخادم عند النقر على زر الإرسال أسفل الاستمارة. تطبيق عملي: إنجاز شيفرة HTML الخاصة بالاستمارة نستخدم في استمارةنا عناصر HTML التالية: عنصر استمارة ويب <form>. عناصر <input>. زر <button>. وقبل أن تتابع معنا، أنشئ نسخة خاصة بك من قالب HTML الخاص بالتطبيق الذي ستضع استمارتك ضمنه. العنصر <form> تبدأ الاستمارات جميعها بالعنصر <form> كالتالي: <form action="/my-handling-form-page" method="post">…</form> يحدد العنصر السابق رسميًا استمارة HTML، ويمثل حاوية تشابه العنصر <select> أو <footer> لكنه مخصص للاستمارات، ويتضمن عدة سمات تحدد سلوك الاستمارة. وتُعد جميع سمات العنصر اختيارية، لكن، ومن الناحية التطبيقية، تُستخدم سميتين على اﻷقل هما action و method: تحدد السمة action موقع عنوان URL الذي تُرسل إليه بيانات الاستمارة عند تسليمها. تحدد السمة method طلب HTTP المستخدم في إرسال البيانات ( get أو post عادة). ملاحظة: سنرى طريقة استخدام السمتين السابقتين في مقالات قادمة. العنصران <textarea> و <input> إن الاستمارة التي نبنيها هي استمارة اتصال contact form بسيطة تتضمن قسم إدخال البيانات فيه ثلاثة حقول نصية مع عناوين <label> تستخدم للدلالة على ما يجب إدخاله في كل حقل: يتكون عنصر إدخال اسم المستخدم من عنصر إدخال نصي وحيد السطر. يتكون عنصر إدخال البريد اﻹلكتروني عنصر إدخال من النوع email وهو حقل إدخال نصي يقبل عناوين بريد إلكتروني صحيحة فقط. يتكون عنصر إدخال الرسالة على عنصر نصي متعدد اﻷسطر هو <textarea>. إذًا سنحتاج إلى شيفرة HTML التالية لتنفيذ الاستمارة: <form action="/my-handling-form-page" method="post"> <ul> <li> <label for="name">Name:</label> <input type="text" id="name" name="user_name" /> </li> <li> <label for="mail">Email:</label> <input type="email" id="mail" name="user_email" /> </li> <li> <label for="msg">Message:</label> <textarea id="msg" name="user_message"></textarea> </li> </ul> </form> حدّث شيفرتك لتكون مماثلة للشيفرة السابقة. نستخدم عناصر القائمة <li> لهيكلة الشيفرة وتسهيل تنسيق الاستمارة (ما سنراه لاحقًا في هذا المقال). كما وضعنا عنوانًا صريحًا لكل عصر تحكم في الاستمارة لسهولة الاستخدام وسهولة الوصول Accessibility. لاحظ أيضًا استخدام السمة for في جميع العناوين، وتأخذ هذه السمة قيمة المعرّف id لعنصر التحكم المرتبطة به، وهي الطريقة التي تربط فيها عنوانًا بعنصر تحكم. إن الفائدة من استخدام هذا اﻷسلوب كبيرة، إذ يسمح ذلك للمستخدم بالنقر على العنوان لتفعيل عنصر التحكم المرتبط به، كما تعطي اسمًا مقروءًا بالنسبة لقارئات الشاشة عند مرور مؤشر الفأرة فوقه، وستجد تفاصيل أكثر في مقالات قادمة. وبالنسبة لعناصر اﻹدخال <input>، ستجد أن السمة type هي الأهم، لأنها تُعرّف الشكل الذي يظهر فيه عنصر اﻹدخال وسلوكه. استخدمنا في مقالنا السمة type بالشكل type = text لعنصر اﻹدخال اﻷول وهي القيمة الافتراضية لهذه السمة، وتمثل مربعًا نصيًا بسيطًا من سطر واحد يقبل أية مدخلات نصية. واستخدمنا السمة بالشكل type = email لعنصر اﻹدخال الثاني ليكون عنصر إدخال نصي وحيد السطر يقبل عناوين بريد إلكتروني صحيحة كمدخل له. إذ تحوّل هذه القيمة عنصر اﻹدخال النصي إلى نوع من العناصر الذكية القادرة على إجراء تقييم للبيانات المدخلة إليه من قبل المستخدم، كما يسبب عرض لوحة مفاتيح مناسبة أكثر ﻹدخال بريد إلكتروني (في أجهزة الهاتف الذكية) كأن يعرض الرمز @افتراضيًا. وسنتحدث في مقال لاحق عن مفهوم التحقق من صحة البيانات المدخلة. وأخيرًا وليس أخرًا، لاحظ الاختلاف في الصياغة القواعدية لعنصر اﻹدخال <input> (دون وسم إغلاق) مقارنة بالعنصر <textarea></textarea> (وجود وسم إغلاق)، وهو أحد اﻷمور الغريبة في HTML. وسبب ذلك أن العنصر <input> عنصر فارغ void لا يحتاج إلى وسم إغلاق، بينما العنصر <textarea> ليس فارغًا لهذا يجب إغلاقه. ولهذا اﻷمر تأثيره على نواحي خاصة في هيكلة الاستمارة، إذ علينا في حالة عنصر اﻹدخال استخدام السمة value لتقديم نص افتراضي داخل العنصر: <input type="text" value="by default this element is filled with this text" /> بينما إن أردت وضع نص افتراضي داخل العنصر <textarea> سيكون ذلك بين وسمي البداية والنهاية: <textarea> by default this element is filled with this text </textarea> العنصر <button> نحتاج إلى الزر <button> في الاستمارة ﻹرسال بيانات المستخدم (أو تسليمها submit) عندما يملأ جميع الحقول المطلوبة. لهذا سنضيف الشيفرة التالية إلى ملف HTML فوق وسم اﻹغلاق <ul/> مباشرة: <li class="button"> <button type="submit">Send your message</button> </li> يقبل العنصر <button> السمة type أيضًا ولها ثلاثة قيم هي submit, reset, button: يؤدي النقر على زر من النوع submit، وهي القيمة الافتراضية للسمة، إلى إرسال بيانات الاستمارة إلى صفحة الويب التي تحددها السمة action للعنصر <form>. ويؤدي النقر على زر من النوع reset إلى ضبط قيم عناصر التحكم في الاستمارة إلى القيم الافتراضية مباشرة. ويُعد هذا الأمر ممارسة سيئة من منظور تجربة المستخدم، لهذا عليك تفادي استخدام هذا النوع من الأزرار ما لم يكن لديك سبب وجيه جدًا في استخدامه. لا يؤدي النقر على زر من النوع button إلى أي شيء، وقد يبدو هذا اﻷمر بلا معنى لكنه عظيم الفائدة في بناء عناصر تحكم مخصصة، إذ تستطيع تحديد وظيفة هذه الأزرار باستخدام جافا سكريبت. ملاحظة: بإمكانك استخدام عنصر اﻹدخال <input> مع السمة type ﻹنشاء زر مثل <'input type = 'submit>، لكن لا يمكن إضافة سوى محتوى نصي إلى عنوان الزر، بينما يتيح لك العنصر <button> إضافة محتوى معقد إلى الزر كالصور مثلًا. الخلاصة قدمنا في مقال اليوم لأحد أهم عناصر صفحات الويب وهي الاستمارات forms، وألقينا نظرة عامة على مختلف عناصر التحكم في استمارات الويب مثل الحقول النصية والقوائم المنسدلة والأزرار، وسنتابع في المقال التالي تنسيق هذه الاستمارة ونوضح طريقة تسليم بياناتها للخادم، كما سنشرح في مقالات تالية المزيد من الأمور المتعلقة باستمارات الويب مثل التحقق من صحة مدخلاتها والاعتبارات اﻷمنية المرتبطة بإرسال هذه البيانات للخادم. ترجمة -وبتصرف- لمقال: Web form building blocks ومقال: Your first form اقرأ أيضًا النماذج (Forms) في HTML5 استخدام نماذج الويب والتحقق منها في فلاسك باستخدام الإضافة Flask-WTF العمل مع الاستمارات Forms في تطبيقات جانغو الأزرار والأيقونات والنماذج في إطار العمل Bootstrap
  4. من اﻷمور التي ستحتاجها غالبًا عند كتابة شيفرة برمجية لصفحات وتطبيقات الويب هي التعامل مع مستندات الويب بطريقة أو بأخرى. وعادة ما يجري ذلك من خلال شجرة DOM وهي واجهة برمجية للتحكم بملف HTML وتنسيق المعلومات التي تستخدم الكائن Document بكثرة. سنفرد هذا المقال للحديث عن استخدام DOM بالتفصيل، إضافة إلى بعض الواجهات التي يمكنها تغيير بيئة العمل بطرائق مفيدة. ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي HTML و CSS. تكون ملمًا بلغة جافا سكريبت. تطلع على سلاسل المقالات السابقة التي ناقشت أساسيات جافا سكريبت والكائنات في جافا سكريبت. اﻷجزاء المهمة في متصفح الويب تُعد المتصفحات برمجيات معقدة تضم الكثير من اﻷجزاء التي لا يمكن لمطور الويب التعامل معها أو التحكم بها باستخدام جافا سكريبت. وقد تعتقد أن هذه المحدودية أمر سيء، لكن الأسباب الكامنة وراء إقفال بعض أجزاء المتصفحات وجيهة بالفعل ويتعلق معظمها باﻷمان. تخيّل مثلًا أن تتمكن صفحة ويب ما من الوصول إلى كلمات السر التي تخزنها في المتصفح أو غيرها من المعلومات الحساسة ومن ثم تستخدم هذه البيانات في الدخول إلى صفحات أخرى! وعلى الرغم من تلك المحدوديات، تمنح واجهات الويب البرمجية إمكانية الوصول إلى الكثير من الوظائف التي تمكنك من تنفيذ أشياء مفيدة جدًا في صفحات الويب، وهنالك بالفعل بعض النقاط الواضحة التي تراها باستمرار وتستخدمها في الشيفرة. تأمل مثلًا المخطط التالي الذي يمثل الأجزاء الرئيسية من المتصفح التي تشارك مباشرة في عرض صفحة الويب: النافذة window: وهي الجزء الذي تُحمّل ضمنه صفحة الويب ويُمثّل في جافا سكريبت بالكائن Window. ونتمكن باستخدام التوابع التي يقدمها هذا الكائن من تنفيذ أشياء عديدة مثل الحصول على حجم النافذة (باستخدام خاصيات مثل Window.innerWidth و Window.innerHeight) أو التعامل مع المستند الذي يُحمّل ضمنها وتخزين بيانات متعلقة به في طرف العميل (مثل استخدام قاعدة بيانات محلية أو غيرها من آليات التخزين) أو ربط معالج أحداث بالنافذة الحالية وغيرها الكثير. المستكشف navigator: ويمثّل حالة وهوية المتصفح (العميل الذي يستخدم المتصفح) عندما يتصفح الويب. يُمثَّل المستكشف في جافا سكريبت عن طريق الكائن Navigator الذي يُستخدم في الحصول على معلومات متعلقة بالمستخدم مثل اللغة المفضلة والتقاط بث كاميرا الويب الخاصة به وغيرها. المستند document: تمثله شجرة DOM في المتصفح وهو في الواقع صفحة الويب التي تُحمّل ضمن النافذة. ويُمثَّل في جافا سكريبت من خلال الكائن Document. ويُستخدم هذا الكائن في التعامل مع عناصر HTML وأصناف CSS في المستند واستخلاص المعلومات منها أو تعديلها مثل الحصول على مرجع لأحد عناصر شجرة DOM وتغيير محتواه النصي وتنسيقه، كما يمكّنك من إنشاء عناصر جديدة وإضافتها إلى العنصر الحالي كعناصر أبناء، وكذلك حذفهم جميعًا. ونركز في مقالنا بشكل أساسي على التعامل مع المستند، مع بعض اﻹضافات اﻷخرى. شجرة DOM (نموذج كائن المستند) يُمثّل المستند الذي يُحمَّل ضمن كل نافذة فرعية من المتصفح على شكل شجرة من العناصر تُعرف بنموذج كائن المستند Document Object Model أو شجرة DOM اختصارًا. وتسهّل هذه الشجرة التي يُنشئها المتصفح الوصول إلى عناصر HTML برمجيًا. كما يستخدم المتصفح هذه الشجرة لتطبيق التنسيقات وغيرها من المعلومات على العنصر الصحيح عند تصيير الصفحة. وستتمكن أنت كمطوّر من التعامل مع شجرة DOM من خلال جافا سكريبت بعد أن تُصيّر الصفحة. نعرض تاليًا مثالًا بسيطًا لتوضيح اﻷمر (يمكنك تجربته مباشرة أيضًا). لهذا جرّب أن تفتح الملف في متصفحك وسترى صفحة بسيطة جدًا تضم العنصر <section> وضمنه صورة وفقرة نصية تضم رابطًا تشعبيًا. <!doctype html> <html lang="en-US"> <head> <meta charset="utf-8" /> <title>Simple DOM example</title> </head> <body> <section> <img src="dinosaur.png" alt="A red Tyrannosaurus Rex: A two legged dinosaur standing upright like a human, with small arms, and a large head with lots of sharp teeth." /> <p> Here we will add a link to the <a href="https://www.mozilla.org/">Mozilla homepage</a> </p> </section> </body> </html> تبدو شجرة DOM كالتالي: يُدعى كل مدخل في الشجرة عقدة node وبإمكانك ملاحظة أن بعض العقد في المخطط السابق تمثل عناصر مثل HTML و HEAD و META، كما يمثل غيرها نصوصًا (معرّفة بالوسم text#). وهنالك أيضًا أنواع أخرى من العقد لكن ما ذكرناه هي العقد الرئيسية التي تواجهها. يُشار إلى العقد أيضًا بموقعها ضمن الشجرة نسبةً إلى عقد أخرى: عقدة جذرية root node: وهي أعلى عقدة في الشجرة وهي دائمًا العقدة HTML في ملفات HTML (وتختلف من لغة تأشير إلى أخرى). عقدة ابن child node: وهي عقدة تقع مباشرة ضمن عقدة أخرى، مثل العنصر IMG داخل العنصر SECTION في المثال السابق. عقدة سليلة Descendant node: وهي عقد تقع في أي مكان داخل عقدة أخرى، مثل العنصر IMG داخل العنصر SECTION في المثال السابق فهي أيضًا عقدة سليلة. وهي ليست عقدة ابن للعنصر BODY لأنها تبعد مستويين عنه لكنها عقدة سليلة له. عقدة أم Parent node: وهي عقدة تحتوي على عقد أخرى ضمنها، مثل العقدة BODY التي تمثل عقدة أم للعنصر SECTION. عقد شقيقة Sibling nodes: وهي عقد تقع في نفس المستوى من شجرة DOM مثل العنصرين IMG و P. ومن اﻷفضل طبعًا أن تعتاد على هذه المصطلحات قبل العمل مع DOM، إذ تُستخدم في العديد من مصطلحات الشيفرة التي تصادفها، ومن المحتمل أن تكون صادفتها فعلًا إن درست صفحات التنسيق CSS (مثل المحدد السليل والمحدد الابن). تطبيق عملي: أساسيات العمل مع شجرة DOM لنبدأ العمل مع شجرة DOM من خلال التطبيق العملي التالي: أنشئ نسخة محلية من صفحة التطبيق والصورة التي ترافقها. أضف العنصر <script></script> إلى ملف الشيفرة بعد الوسم <body/> مباشرة. ولكي تتعامل مع عنصر ضمن شجرة DOM عليك تخزين مرجع إليه ضمن متغير، لهذا أضف الشيفرة التالية ضمن العنصر <script>: const link = document.querySelector("a"); وهكذا سيكون بإمكانك اﻵن التعامل مع العنصر الذي تخزن مرجعًا إليه باستخدام الخاصيات والتوابع المتاحة لهذا العنصر (تُعرَّف هذه الخاصيات والتوابع ضمن واجهات خاصة نذكر منها HTMLAnchorElement التي تتعامل مع الرابط التشعبي <a> وكذلك الواجهة اﻷم HTMLElement والواجهة Node التي تمثل جميع عقد الشجرة). سنبدأ العمل بتغيير النص ضمن الرابط التشعبي بتحديث قيم الخاصية Node.textContent، لهذا أضف هذا السطر تحت السطر السابق: link.textContent = "Mozilla Developer Network"; ولا بد أيضًا من تغيير عنوان URL الذي يشير إليه الرابط التشعبي كي لا ينقل المستخدم إلى وجهة خاطئة عند النقر عليه، لهذا نضيف السطر التالي تحت اﻷسطر السابقة: link.href = "https://developer.mozilla.org"; وتجدر اﻹشارة إلى وجود عدة طرق لاختيار العنصر الذي نريد التعامل معه في جافا سكريبت، ويُعد التابع ()Document.querySelector مقاربة حديثة ننصح بها لأنها تسمح لك باختيار العنصر باستخدام محددات تنسيق CSS. إذ يبحث التابع السابق عند استدعائه في شيفرتنا عن أول عنصر <a> في المستند، وإن أردت البحث عن المزيد العناصر كي تتعامل معها دفعة واحدة، تستطيع استخدام التابع ()Document.querySelectorAll الذي يبحث عن كل عنصر في الصفحة له نفس مُحدد CSS الذي نستهدفه ومن ثم يخزّن مراجعًا إلى هذه العناصر ضمن كائن شبيه بالمصفوفة يُدعى قائمة عقد NodeList. وتصادف أيضًا طرقًا أخرى للحصول على مراجع للعناصر ضمن الشجرة منها: ()Document.getElementByID: يبحث هذا التابع عن عنصر ذو قيمة محددة للسمة id. فلو أردنا الوصول إلى العنصر <p id="myId">My paragraph</p>، نمرر قيمة هذه السمة إلى التابع كالتالي: const elementRef = document.getElementById('myId'); ()Document.getElementsByTagName: تعيد كائنًا يشبه المصفوفة يضم كل العناصر من النوع نفسه مثل عناصر الفقرات النصية <p> أو الروابط التشعبية <a> وغيرها. يُمرر نوع العنصر إلى التابع كما في المثال التالي: const elementRefArray = document.getElementsByTagName('p'); يعمل هذان التابعان بشكل أفضل مع المتصفحات القديمة، لكنهما أقل ملائمة مقارنة بالتابع ()Document.querySelector. إنشاء عقد جديدة ووضعها ضمن شجرة DOM رأيت مما سبق ما يمكن إنجازه، لكننا سنتقدم الآن خطوة للأمام لإلقاء نظرة على كيفية إنشاء عناصر جديدة. بالعودة إلى الشيفرة السابقة سنحاول إنشاء مرجع إلى العنصر <section>، لهذا ضع الشيفرة التالية في نهاية السكريبت (وهذا ما سنفعله مع اﻷسطر التي نضيفها تاليًا): const sect = document.querySelector("section"); لننشئ الآن فقرة نصية جديدة باستخدام ()DocumentCreateElement ونضع فيها بعض العبارات وفق نفس اﻷسلوب الذي اتبعناه سابقًا: const para = document.createElement("p"); para.textContent = "We hope you enjoyed the ride."; نُلحق الفقرة النصية الجديدة بنهاية العنصر <section> من خلال التابع ()Node.appendChild: sect.appendChild(para); لنُضف أخيرًا عقدة نصية text node إلى الفقرة النصية التي تضم الرابط التشعبي، لهذا ننشئ أولًا العقدة النصية باستخدام التابع ()Document.createTextNode: const text = document.createTextNode( " — the premier source for web development knowledge.", ); ثم ننشئ مرجعًا إلى الفقرة النصية ونلحق بها العقدة النصية: const linkPara = document.querySelector("p"); linkPara.appendChild(text); هذا كل ما تحتاجه غالبًا ﻹضافة عقد إلى شجرة DOM وستستخدم التوابع السابقة كثيرًا عند بناء واجهة ديناميكية. نقل وإزالة عناصر تحتاج أحيانًا إلى نقل عقدة أو حذفها وهذا أمر ممكن، فإن أردت نقل الفقرة النصية التي تضم الرابط التشعبي إلى نهاية العنصر <section>: sect.appendChild(linkPara); ولن تحدث بالطبع عملية نسخ للفقرة النصية إلى المكان الجديد لأن المتغير linkPara هو مرجع إلى النسخة الوحيدة فقط من هذه الفقرة. لكن إن أردت إنشاء نسخة عنها وإضافتها إلى المكان الذي تريد، فهذا أمر ممكن أيضًا من خلال التابع ()Node.cloneNode. أما إزالة العقدة فهو أمر سهل وخاصة عندما يكون لديك مرجع إلى العقدة التي تريد إزالتها ومرجع إلى العقدة اﻷم لها، وعندها نستدعي التابع Node.removeChild: sect.removeChild(linkPara); باﻹمكان أيضًا إزالة عقدة بناء على مرجعها فقط، وهذا أمر شائع باستخدام التابع ()Element.remove: linkPara.remove(); لا تدعم المتصفحات الأقدم هذا التابع، إذ لا تمتلك أي طريقة لتطلب من عقدة حذف نفسها، لهذا عليك اﻹلتفاف على الموضوع كالتالي: linkPara.parentNode.removeChild(linkPara); أضف السطر السابق إلى شيفرتك. العمل مع تنسيقات العناصر بإمكاننا تعديل تنسيق CSS لعناصر الصفحة من خلال أكواد جافا سكريبت بطرق كثيرة. وكبداية بإمكانك الحصول على قائمة بكل ملفات التنسيق المرتبطة بالصفحة من خلال التابع Document.stylesheets الذي يعيد كائنًا مشابهًا للمصفوفات يضم عناصر من النوع CSSStyleSheet. وعندها ستتمكن من إضافة أو إزالة التنسيقات كما تشاء. ولن نتوسع في هذا اﻷسلوب لأنه قديم نوعًا ما ويصعب التعامل مع التنسيقات باستخدامه، وهنالك بالطبع طرق أسهل. تقتضي الطريقة اﻷولى إضافة تنسيقات سطرية inline style مباشرة ضمن العناصر التي تريد تعديل تنسيقها ديناميكيًا. يُنفَّذ الأمر من خلال الخاصية HTMLElemet.style التي تضم معلومات عن التنسيق السطري لكل عنصر في الصفحة. وعندما تغيير قيمة هذه الخاصية سيتغير تنسيق العنصر مباشرة. جرّب تغيير تنسيقات الفقرة النصية في شيفرتنا: para.style.color = "white"; para.style.backgroundColor = "black"; para.style.padding = "10px"; para.style.width = "250px"; para.style.textAlign = "center"; ثم أعد تحميل الصفحة لتشاهد التنسيقات الجديدة وقد طُبّقت على الفقرة النصية. ولو حاولت تفحّص الفقرة النصية من خلال المتصفح ستجد أن تنسيقًا سطريًا قد أضيف إلى شيفرتها: <p style="color: white; background-color: black; padding: 10px; width: 250px; text-align: center;"> We hope you enjoyed the ride. </p> ملاحظة: لاحظ كيف كُتبت نسخ جافا سكريبت من تنسيقات CSS بأسلوب سنام الجمل camelCase (مثلًا backgroundColor) مقارنة مع أسلوب CSS اﻷصلي وهو أسلوب الكباب lower-kebab-case (مثلًا background-color). احرص ألا تختلط عليك اﻷمور. وهنالك أيضًا طريقة شائعة أخرى للتعامل مع تنسيق العناصر دينياميكيًا، سنلقي عليها نظرة اﻵن: احذف اﻷسطر الخمسة اﻷخيرة التي أضفتها. أضف ما يلي ضمن ترويسة الصفحة <head>: <style> .highlight { color: white; background-color: black; padding: 10px; width: 250px; text-align: center; } </style> نستخدم التابع ()Element.setAttribute للتعامل مع عناصر HTML عمومًا، ويأخذ هذا التابع وسيطان أولهما الخاصية التي تريد ضبطها والثاني قيمة هذه الخاصية. وفي حالتنا ستكون الخاصية هي صنف الفقرة النصية class وقيمتها هي محدد CSS الذي نريد إسناده إلى الفقرة النصية لتنسيقها: para.setAttribute("class", "highlight"); حدّث صفحتك ولن ترى أية تغيير في شيفرة HTML للفقرة النصية لأن التنسيق قد طّبق عليها بمنحها صنف تنسيق وليس بإضافة تنسيق سطري إليها. إن اعتماد أسلوب معين هي حرية شخصية، ولكل اﻷساليب إيجابياتها وسلبياتها. فاﻷسلوب الأول بسيط ولا يتطلب إعدادات خاصة وهو جيد في بعض الحالات، بينما يعزز اﻷسلوب الثاني مبدأ فصل شيفرة التنسيق CSS عن شيفرة الصفحة HTML والذي يعُدّ ممارسة جيدة). وقد تميل مع تقدمك في مسيرة بناء تطبيقات أضخم وأعقد إلى استخدام اﻷسلوب الثاني أكثر، لكن في النهاية اﻷمر كما ذكرنا يعود إليك. ربما لم تلمس حتى اللحظة فائدة استخدام جافا سكريبت من إنشاء عناصر ساكنة، ومن الممكن كتابتها في شيفرة HTML مباشرة دون الحاجة إلى جافا سكريبت. فاستخدام جافا سكريبت أعقد وإنشاء المحتوى باستخدامها ينطوي على مشاكل أخرى تتعلق مثلًا بقدرة محركات البحث على قراءتها. لهذا سننقل في الفقرة القادمة إلى تطبيق آخر يُظهر استخدامًا عمليًا أكثر. ملاحظة: يمكنك إلقاء نظرة على التمرين التطبيقي المنتهي) (أو تجربته مباشرة أيضًا). تطبيق عملي: قائمة تسوّق ديناميكية نريد في هذا التطبيق إنشاء قائمة تسوّق بسيطة تسمح بإضافة عناصر بشكل ديناميكي إلى القائمة باستخدام نموذج إدخال مكوّن من مربع نصي وزر. وينبغي عند إضافة مُدخل إلى المربع النصي والنقر على الزر: أن يُعرض العنصر الجديد ضمن القائمة. أن يجاور كل عنصر زر لتتمكن من حذف هذا العنصر عند النقر على الزر. أن يُفرّغ مربع اﻹدخال ويكتسب تركيز الدخل بعد إضافة عنصر إلى القائمة استعدادًا ﻹدخال عنصر آخر. قد يبدو الشكل النهائي للتطبيق كالتالي: وﻹكمال التمرين، اتبع الخطوات التالية وتأكد أن سلوك القائمة سيكون مطابقًا لما أشرنا إليه: نزّل نسخة عن ملف HTML الخاص بالتطبيق، وضعها في مكان مناسب. وسترى أن الملف يضم تنسيقًا بسيطًا وعنصر <div> وعنوان وعنصر إدخال <input> وزر وقائمة فارغة وعنصر <script> تضع ضمنه كل الشيفرة التي تحتاجها. أنشئ ثلاثة متغيرات لتحمل مراجع إلى القائمة <ul> والزرين <button>. أنشئ دالة تعمل عند النقر على الزر. خزّن ضمن الدالة القيمة الحالية لعنصر اﻹدخال ضمن متغير. فرّغ عنصر اﻹدخال من المحتوى بإسناد القيمة '' له. أنشئ ثلاثة عناصر جديدة هي عنصر قائمة <li> وعنصر <span> وعنصر <button>. ألحق الزر والعنصر <span> كابنين لعنصر القائمة. اجعل المحتوى النصي للعنصر <span> مطابقًا لقيمة عنصر اﻹدخال واجعل العبارة 'Delete' المحتوى النصي للزر. ألحق عنصر القائمة بالقائمة كعنصر ابن. اربط معالج حدث بزر الحذف كي يُحذف عنصر القائمة بأكمله (<li>...</li>) عند النقر على الزر. استخدم أخيرًا التابع ()focus لنقل تركيز الدخل إلى عنصر اﻹدخال كي يكون جاهزًا ﻹدخال العنصر التالي في القائمة. ملاحظة: يمكنك إلقاء نظرة على التمرين التطبيقي المنتهي (أو تجربته مباشرة أيضًا). الخلاصة وصلنا اﻵن إلى نهاية مقالنا عن استخدام DOM وتعديلها من خلال جافا سكريبت، ومن المفترض في هذه المرحلة أن تكون قد أدركت اﻷجزاء المهمة من متصفح ويب فيما يتعلق بالتحكم بالمستندات وغيرها من النواحي التي تؤثر على تجربة مستخدم الويب. ومن المهم اﻷكثر أن تستوعب ماهية شجرة DOM وكيفية التعامل معها لتقديم ميزات مفيدة لتطبيقك. ترجمة -وبتصرف- للمقال: Manipulating documents المقال السابق: مدخل إلى واجهات الويب البرمجية Web APIs تعرف على البنية الشجرية لـ DOM مدخل إلى DOM مكونات الويب: التعامل مع شجرة DOM الخفية مكونات الويب: عناصر HTML المخصصة وقوالبها
  5. نلقي نظرة في هذا المقال على الواجهات البرمجية في جافا سكريبت، ونشرح ماهيتها وكيفية عملها، وآلية استخدامها في الشيفرة و أساليب هيكلتها. كما نلقي نظرة على اﻷصناف اﻷساسية للواجهات البرمجية، وإمكانيات استخدامها. ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي HTML و CSS. تكون ملمًا بلغة جافا سكريبت. تطلع على سلاسل المقالات السابقة التي ناقشت أساسيات جافا سكريبت والكائنات في جافا سكريبت. ما هي الواجهات البرمجية؟ واجهات التطبيقات البرمجية أو الواجهات البرمجية API هي بُنىً برمجية تقدمها لغات البرمجة لتتيح للمطورين إنشاء مهام معقدة بسهولة أكبر. إذ تساهم في تحييدهم عن الشيفرات المعقدة وتقدم صياغة قواعدية أوضح وأسهل. فكّر كمثال عن الواجهة البرمجية بالتغذية الكهربائية في منزلك، فإن أردت تشغيل جهاز كهربائي ستضع قابس الجهاز في مأخذ الكهرباء المخصص وسيعمل الجهاز، ولن تضطر بالطبع إلى إجراء التوصيلات الكهربائية إلى مصدر التغذية مباشرة، على الرغم من إمكانية ذلك، لكن إن لم تكن كهربائيًا مختصًا فالأمر خطر وصعب. وبالمثل، لو أردت برمجة رسوميات ثلاثية اﻷبعاد، من اﻷسهل حينها استخدام واجهة برمجية مكتوبة بلغة عالية المستوى مثل جافا سكريبت أو بايثون بدلًا من كتبتها باستخدام لغات أكثر تعقيدًا تتحكم مباشرة بوحدة المعالجة الرسومية GPU أو غيرها من الوظائف الرسومية مثل C و ++C. واجهات جافا سكريبت البرمجية في طرف العميل تقدم لغة جافا سكريبت في طرف العميل تحديدًا العديد من الواجهات البرمجية المهمة. ولا تُعد هذه الواجهات جزءًا من اللغة بحد ذاتها وإنما بُنيت على أساس جافا سكريبت لزيادة إمكانات شيفرتها، وتصنف هذه الواجهات عمومًا ضمن فئتين رئيسيتين: واجهات المتصفح البرمجية Browser API: وهي برمجيات مضمّنة في المتصفح وقادرة على عرض البيانات الموجودة في المتصفح و البيئة الحاسوبية التي تستضيفه، وتنفيذ عمليات مركّبة على تلك البيانات. وكمثال على هذه الواجهات نذكر Web Audio API التي تقدم هيكلية لجافا سكريبت تساعد في التعامل مع الصوتيات في المتصفح، علمًا أن المتصفح يتعامل مع الموارد الصوتية عمليًا من خلال شيفرة معقدة مكتوبة بلغات أخرى مثل (++C و Rust)، مع ذلك ما تقدمه تلك الواجهة هو تفادي تعقيد تلك العمليات. واجهات يقدمها طرف آخر Third-party APIs: لا تُضمّن هذه الواجهات ضمن المتصفح افتراضيًا، وعليك الحصول على شيفرتها والمعلومات المتعلقة بها من الويب. فالواجهة Google Maps API على سبيل المثال تمنحك القدرة على عرض خرائط تفاعلية على موقعك، وتقدم هيكلية مخصصة تساعدك في استعلام خدمة خرائط جوجل Google Maps للحصول على معلومات محددة. العلاقة بين جافا سكريبت و الواجهات البرمجية و أدوات جافا سكريبت تحدثنا في الفقرة السابقة عن طبيعة واجهات جافا سكريبت البرمجية في طرف العميل وعلاقتها بلغة جافا سكريبت، لهذا دعونا نسترجع ما شرحناها لتوضيح الأمر أكثر واﻹشارة إلى أدوات جافا سكريبت الملائمة: لغة جافا سكريبت: وهي لغة سكريبت عالية المستوى مضمنة ضمن المتصفحات، وتسمح لك بتنفيذ مهام مختلفة في صفحات وتطبيقات الويب. وتجدر اﻹشارة أن جافا سكريبت متاحة أيضًا في بيئات أخرى غير بيئة المتصفحات مثل بيئة Node.js. واجهات المتصفح البرمجية: وهي هيكليات مضمنة في المتصفح أساسها جافا سكريبت وتسمح بتنفيذ وظائف مختلفة بسهولة أكبر. واجهات برمجية يقدمها طرف آخر: وهي هيكليات مبنية على منصات مختلفة (مثل فيسبوك) تتيح لك استخدام بعض وظائف هذه المنصة في موقعك الخاص. مكتبات جافا سكريبت: وهي عادة ملف جافا سكريبت أو أكثر تتضمن دوال مخصصة يمكنك ربطها بصفحات الويب الخاصة بك لتسريع تنفيذ أو استخدام وظائف معينة. من اﻷمثلة عليها المكتبات jQuery و Mootools و React. إطارات عمل جافا سكريبت: وهي تقنية أكثر تقدمًا من المكتبات مثل Angular و Ember وتتكون عادة من حزم HTML و CSS وجافا سكريبت وتقنيات أخرى تُثبّت لتساعدك في كتابة تطبيقات ويب كاملة من الصفر. أما الاختلاف الجوهري بين المكتبات وإطارات العمل فهو "التحكم المعكوس Inversion of Control". إذ يتحكم المطور باستدعاء دوال من مكتبة، لكن إطار العمل هو من يستدعي شيفرة المطور. ما الذي يمكن أن تقدّمه الواجهات البرمجية؟ ستجد كمًا هائلًا من الواجهات البرمجية المتاحة التي تسمح لك بتنفيذ مهام ووظائف متنوعة في شيفرتك، لهذا نلقي نظرة في الفقرات التالية على أهم هذه الواجهات ووظائفها. واجهات المتصفح البرمجية اﻷكثر شيوعًا من أكثر فئات واجهات المتصفح البرمجية شيوعًا والتي سنشرحها في هذه المقالات نجد: الواجهات البرمجية التي تتعامل مع المستندات تُحمّل هذه الواجهات ضمن المتصفح ومن أكثرها وضوحًا الواجهة DOM التي تتيح لك العمل مع HTML و CSS مثل إنشاء العناصر وإزالتها وتنسيقها ديناميكيًا وتطبيق التغييرات على صفحة الويب وغيرها. فكلما رأيت نافذ منبثقة من الصفحة أو لاحظت تغيرًا في المحتوى المعروض فهو من فعل واجهة DOM. الواجهات التي تحضر البيانات من الخادم وتستخدم هذه الواجهات مثلًا في تحديث محتوى جزء من الصفحة فقط، ولهذا اﻷمر الذي يبدو بسيطًا أثرًا هائلًا على أداء وسلوك المواقع. فلو أردت أن تحدث فقط قائمة البضائع في متجر أو عرض قائمة بالقصص الجديدة المتوفرة وتمكنت من تنفيذ اﻷمر مباشرة دون الحاجة إلى إعادة تحميل الصفحة، سيبدو حينها الموقع أو التطبيق أكثر تجاوبًا. ومن الواجهات الرئيسية المستخدمة في تحقيق ذلك نجد Fetch وكذلك XMLHttpRequest التي تستخدمها الشيفرات اﻷقدم. وقد تمر بالمصطلح AJAX الذي يصف تمامًا هذه التقنية القديمة. الواجهات البرمجية الخاصة بالرسوميات وهي واجهات مدعومة جيدًا في معظم المتصفحات الحديثة وأكثرها شهرة Canvas و WebGL. إذ تسمح هذه الواجهات بتغيير بيانات كل بكسل من بكسلات عنصر HTML المخصص <canvas> لتكوين مشاهد ثنائية وثلاثية اﻷبعاد. وتتمكن أيضًا من رسم أشكال ضمن هذا العنصر مثل المربعات والدوائر أو عرض صورة وتطبيق مرشحات عليها كأن تحولها إلى اللون الرمادي وذلك باستخدام الواجهة البرمجية Canvas. وتتيح لك الواجهة WebGL إنشاء مشاهد ثلاثية أبعاد مركّبة مع إضاءة وخامة texture وغيرها. وتُستخدم هذه الواجهات عادة مع واجهات أخرى لتنفيذ حلقات رسومية للتحريك مثل ()window.requestAnimationFrame وغيرها كي تحدّث باستمرار المشهد كما في الرسوم المتحركة واﻷلعاب. الواجهات البرمجية الخاصة بالصوت والفيديو تسمح لك هذه الواجهات بتنفيذ الكثير من المهام المتعلقة بتشغيل الوسائط المتعدد وإنشاء واجهات تحكم مخصصة للتعامل مع ملفات الصوت والفيديو وعرض معلومات نصية عن المقاطع والمسارات إضافة إلى عناوينها وكلماتها. كما تساعدك في التقاط مقاطع فيديو باستخدام كاميرا الويب والتعديل عليها بمساعدة الواجهة البرمجية Canvas أو عرضها على حاسوب شخص آخر أو إضافة تأثيرات إلى المسارات الصوتية. نذكر من هذه الواجهات HTMLMediaElement و Web Audio API و WebRTC الواجهات البرمجية التي تتعامل مع الأجهزة تمكنك هذه الواجهات من التفاعل مع العتاد الصلب لجهازك مثل الوصول إلى شريحة GPS لتحديد موقع المستخدم من خلال الواجهة Geolocation API. الواجهات البرمجية التي تتعامل مع تخزين البيانات في طرف العميل تمكنك هذه الواجهات من تخزين البيانات على جهاز العميل، وبالتالي ستكون قادرًا على إنشاء تطبيق يحتفظ بحالته أثناء تحميل الصفحات المختلفة وحتى العمل دون الإتصال باﻹنترنت. وستجد العديد من الخيارات المتاحة مثل التخزين البسيط المبني على مبدأ اسم/قيمة من خلال الواجهة Web Storage API أو تخزين البيانات ضمن قواعد بيانات أعقد مثل الواجهة IndexedDB API. الواجهات البرمجية التي تطورها أطراف أخرى تتنوع هذه الواجهات وتغطي استخدامات متنوعة، لكننا سنذكر منها الأكثر شعبية، فقد تضطر إلى استخدامها عاجلًا أم آجلًا. واجهات الخرائط: مثل Mapquest و Google Maps API، وتسمح لك بتنفيذ الكثير من اﻷشياء التي تتعلق بالخرائط ضمن موقعك. مجموعة واجهات فيسبوك Facebook suite of APIs: تمكنك هذه الواجهات من استخدام اﻷجزاء المختلفة لمنظومة فيسبوك لدعم تطبيقك مثل تقديم آلية لتسجيل الدخول اعتمادًا على حساب فيسبوك واستخدام بوابات الدفع اﻹلكتروني في تطبيقك وإدارة حملات دعائية وغيرها. واجهات تلغرام Telegram APIs: وتتيح لك إدراج محتوى أقنية تلغرام في موقعك، وتقديم دعم للبرامج اﻵلية bots. واجهة يوتيوب YouTube API: وتتيح لك إدراج مقاطع فيديو من يوتيوب في موقعك أو البحث ضمن يوتيوب أو إنشاء قوائم تشغيل وغيرها. الواجهة Twilio API: وتقدم إطار عمل لبناء وظائف تتعلق بمكالمات الصوت والفيديو في تطبيقك، وإرسال رسائل SMS و MMS من التطبيق وغيرها. الواجهة Disqus API: تزودك بمنصةّ للتعليقات يمكن إضافتها إلى موقعك اﻹلكتروني. الواجهة Mastodon API: تساعدك في التعامل مع ميزات شبكة Mastodon للتواصل الاجتماعي برمجيًا. الواجهة IFTTT API: وتساعدك على دمج عدة واجهات برمجية ضمن منصة واحدة. كيف تعمل الواجهات البرمجية؟ توجد بعض الاختلافات في طريقة عمل واجهات جافا سكريبت البرمجية، إلا أنها تشترك بالعديد من الميزات وسمات العمل. وسنشرح أوجه التشابه في الفقرات التالية. الاعتماد على الكائنات تتواصل شيفرتك مع الواجهات البرمجية باستخدام كائن أو أكثر من كائنات جافا سكريبت. وتعمل هذه الكائنات كحاويات لتخزين البيانات التي تستخدمها الواجهة (من خلال خاصيات الكائن properties) وكوسيلة لإيصال الوظائف التي تقدمها الواجهة البرمجية (من خلال توابع الكائن methods). ملاحظة: إن لم تكن على دراية بطريقة عمل الكائنات، ننصحك بالعودة إلى مقال "أساسيات العمل مع الكائنات في جافا سكريبت". لو عدنا اﻵن إلى واجهة الويب الخاصة بالصوتيات Web Audio API وهي واجهات معقدة نوعًا ما، ستجد أنها تضم عددًا من الكائنات أكثرها وضوحًا: AudioContext: ويقدم وسيلة للتعامل مع تشغيل الصوتيات ضمن المتصفح ويتضمن عدة توابع وخاصيات تساعدك في ذلك. MediaElementAudioSourceNode: ويمثّل العنصر <audio> الذي يضم المقطع الصوتي المطلوب تشغيله ضمن الكائن AudioContext. AudioDestinationNode: ويمثّل الطرفية التي تصدر الصوت فعليًا في جهازك، وعادة ما تكون مكبّر الصوت أو سماعات الرأس. لكن كيف تتفاعل هذه الكائنات مع بعضها؟ ألق نظرة على مثال تشغيل الصوتيات في المتصفح (بإمكانك تجربة التطبيق مباشرة أيضًا) وستجد شيفرة HTML التالية: <audio src="outfoxing.mp3"></audio> <button class="paused">Play</button> <br /> <input type="range" min="0" max="1" step="0.01" value="1" class="volume" /> أول ما فعلناه هو استخدام العنصر <audio> الذي يرتبط بمقطع صوتي MP3، ولم نضف أية أدوات تحكم افتراضية بالصوت مضمّنة في المتصفح. كما أضفنا بعد ذلك العنصر <button> الذي نستخدمه لتشغيل وإيقاف المقطع الصوتي، وعنصر إدخال <input> من النوع "مجال "type="range" الذي نستخدمه للتحكم بمستوى الصوت أثناء التشغيل. ونبدأ شيفرة جافا سكريبت بإنشاء نسخة عن الكائن AudioContext للتعامل مع الملف الصوتي: const AudioContext = window.AudioContext || window.webkitAudioContext; const audioCtx = new AudioContext(); ننشئ بعد ذلك ثوابت لتخزين مراجع إلى العناصر <audio> و <button> و <input> ومن ثم نستخدم التابع ()AudioContext.createMediaElementSource ﻹنشاء الكائن MediaElementAudioSourceNode الذي يمثّل بدوره المصدر الذي ينبع منه الصوت وهو العنصر <audio>. const audioElement = document.querySelector("audio"); const playBtn = document.querySelector("button"); const volumeSlider = document.querySelector(".volume"); const audioSource = audioCtx.createMediaElementSource(audioElement); نستخدم تاليًا معالجي أحداث أحدهما للتنقل بين وضعي التشغيل واﻹيقاف عند النقر على الزر واﻵخر ﻹعادة الملف الصوتي إلى بدايته عند انتهاء تشغيله. // تشغيل وايقاف الملف الصوتي playBtn.addEventListener("click", () => { // check if context is in suspended state (autoplay policy) if (audioCtx.state === "suspended") { audioCtx.resume(); } // إن توقف تشغيل الملف أعد تشغيله if (playBtn.getAttribute("class") === "paused") { audioElement.play(); playBtn.setAttribute("class", "playing"); playBtn.textContent = "Pause"; // إن كان الملف في وضع التشغيل أوقفه } else if (playBtn.getAttribute("class") === "playing") { audioElement.pause(); playBtn.setAttribute("class", "paused"); playBtn.textContent = "Play"; } }); // عند إنتهاء اﻷغنية audioElement.addEventListener("ended", () => { playBtn.setAttribute("class", "paused"); playBtn.textContent = "Play"; }); ملاحظة: قد يلاحظ البعض أن التابعين ()play و ()pause المستخدمان في تشغيل وايقاف الملف الصوتي ليسا جزءًا من الواجهة Web Audio API، بل جزءًا من الواجهة البرمجية HTMLMediaElement التي تختلف قليلًا عنها. ننشئ بعد ذلك الكائن GainNode باستخدام التابع ()AudioContext.createGain وذلك لضبط مستوى الصوت، ثم ننشئ معالج حدث آخر يغيّر قيمة مستوى الصوت كلما تغير موقع الزالقة: // مستوى الصوت const gainNode = audioCtx.createGain(); volumeSlider.addEventListener("input", () => { gainNode.gain.value = volumeSlider.value; }); آخر ما نفعله ﻹنجاز مثالنا هو ربط العقد المختلفة معًا عن طريق التابع ()AudioNode.connect الموجود في كل عقدة audioSource.connect(gainNode).connect(audioCtx.destination); نبدأ بمصدر الصوت ومن ثم نربط به عقدة التحكم بمستوى الصوت والتي تتصل بدورها إلى العقدة التي تشغّل الصوت فعليًا في جهازك (تمثّل الخاصية AudioContext.destination الوجهة الافتراضية AudioDestinationNode المتاحة على حاسوبك لتشغيل الصوت مثل المكبرات). لجميع الواجهات مداخل مميزة عليك أن تعرف تمامًا مداخل entry points الواجهات البرمجية قبل أن تتعامل معها، وهذا اﻷمر بسيط نوعًا ما في الواجهة Web Audio API، فمدخلها هو الكائن AudioContext الذي تحتاجه للتعامل مع أي ملف صوتي. وللواجهة البرمجية DOM مدخل بسيط أيضًا، إذ تتواجد معظم ميزاتها ضمن الكائن Document، أو ضمن نسخة عن كان HTML الذي نريد التعامل معه: const em = document.createElement("em"); //إنشاء عنصر جديد const para = document.querySelector("p"); //موجود <p> مرجع إلى عنصر em.textContent = "Hello there!"; //em إسناد محتوى إلى المتغير para.appendChild(em); //para ضمن المتغير em وضع وتعتمد الواجهة أيضًا على كائن سياق context object للتعامل مع المقادير المختلفة. وعلى الرغم من أن السياق في هذه الحالة رسومي وليس صوتي، إلا أن كائن السياق الخاص سيُنشأ من خلال مرجع إلى العنصر <canvas> الذي تريد الرسم ضمنه ومن ثم تستدعي التابع ()HTMLCanvasElement.getContext: const canvas = document.querySelector("canvas"); const ctx = canvas.getContext("2d"); وكل ما تحتاجه بعد ذلك للعمل مع canvas هو استدعاء الخاصيات والتوابع لكائن السياق الرسومي (وهو في هذه الحالة نسخة عن CanvasRenderingContext2D? Ball.prototype.draw = function () { ctx.beginPath(); ctx.fillStyle = this.color; ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI); ctx.fill(); }; ملاحظة: بإمكانك رؤية هذه الشيفرة وهي تعمل ضمن تطبيق الكرات القافزة التجريبي (وبإمكانك تجربته مباشرة أيضًا) تستخدم اﻷحداث للتعامل مع تغيرات الحالة ناقشنا في مقالمدخل إلى اﻷحداث في جافا سكريبت موضوع اﻷحداث والتعامل معها وكيفية استخدامها في الشيفرة. فإن لم تكن على دراية بها، ننصحك بالعودة إلى هذا المقال والعمل عليه قبل المتابعة. لا تتضمن بعض واجهات الويب البرمجية أحداثًا ويضم بعضها اﻵخر القليل منها، ولقد رأينا عمل عدد من معالجات اﻷحداث في مثالنا عن الواجهة Web Audio APi: // play/pause audio playBtn.addEventListener("click", () => { // check if context is in suspended state (autoplay policy) if (audioCtx.state === "suspended") { audioCtx.resume(); } // if track is stopped, play it if (playBtn.getAttribute("class") === "paused") { audioElement.play(); playBtn.setAttribute("class", "playing"); playBtn.textContent = "Pause"; // if track is playing, stop it } else if (playBtn.getAttribute("class") === "playing") { audioElement.pause(); playBtn.setAttribute("class", "paused"); playBtn.textContent = "Play"; } }); // if track ends audioElement.addEventListener("ended", () => { playBtn.setAttribute("class", "paused"); playBtn.textContent = "Play"; }); تمتلك آليات إضافية لمسائل اﻷمان عند الحاجة لواجهات الويب البرمجية الاعتبارات اﻷمنية نفسها التي نواجهها في جافا سكريبت وفي غيرها من تقنيات الويب (مثل سياسة اﻷصل المشترك same-origin policy) لكنها تتمتع في بعض اﻷحيان بميزات أمنية إضافة. فلن تعمل على سبيل المثال بعض واجهات الويب البرمجية الحديثة سوى في الصفحات التي تُخدَّم وفق بروتوكول HTTPS وذلك لإمكانية نقلها بيانات حساسة (من اﻷمثلة عليها عمال الخدمة service workers والواجهة Push). وإضافة إلى ذلك، تطلب بعض الواجهات الحصول على بعض اﻷدونات من المستخدم عندما يستدعيها من خلال الشيفرة مثل الواجهة Notifications API التي تطلب من المستخدم إذنًا لعرض النوافذ المنبثقة pop-ups: وتخضع الواجهتين Web Audio و HTMLMediaElement إلى آلية أمنية تُدعى سياسة التشغيل التلقائي autoplay policy، وتعني مبدئيًا أنك لن تستطيع تشغيل الملف الصوتي تلقائيًا عند تحميل الصفحة، وعليك أن تدفع المستخدم إلى تشغيل المقطع بالنقر على زر مثلًا. والسبب في ذلك أن تشغيل المقطع الصوتي تلقائيًا أمر مزعج ولا ينبغي فرض اﻷمر على المستخدم. ملاحظة: تبعًا لصرامة المتصفح الذي تستخدمه، فقد لا تسمح سياسات أمنية كالتي ذكرناها تشغيل المثال السابق محليًا (على حاسوبك) إن حمّلت ملف المثال وحاولت تشغيله بدلًا من تشغيله مباشرة على الخادم. الخلاصة من المفترض في نهاية هذا المقال أن تمتلك فكرة لا بأس بها عن الواجهات البرمجية وفوائدها عندما تستخدمها في جافا سكريبت. وأن تكون قد أصبحت جاهزًا نوعًا ما لاستخدام الواجهات التي شرحناها لتنفيذ مهام مختلفة، وهذا ما سنراه في مقالات لاحقة. ترجمة -وبتصرف- للمقال Introduction to web APIs اقرأ أيضًا المقال السابق: تحريك سلسلة رسوم متحركة باستخدام الوعود في جافا سكريبت ما هي الواجهة البرمجية للتطبيقات API؟ دليل استخدام ChatGPT API لتحسين خدماتك عبر الإنترنت دليلك لربط واجهة OpenAI API مع Node.js إنشاء واجهة برمجية API في Laravel
  6. سنتعلم في هذا المقال كيفية استخدام الوعود (Promises) في جافا سكريبت لتحريك مجموعة من الصورأو الرسومات بترتيب محدد باستخدام الواجهة البرمجية Web Animations API. حيث سنطبق بشكل عملي كل كود برمجي يعرض صورًا ثابتة ثم نعدله شيفرة معينة لتحقيق حركة تسلسلية لهذه الصور وفق تتابع محدد بحيث تتحرك الصورة الأولى، وعند انتهائها تتحرك الصورة الثانية، وعند انتهائها تتحرك الصورة الثالثة. ملفات التطبيق لدينا في مجلد التطبيق الملفات الأساسية التالية: index.html style.css main.js style.css وملف الصورة التي سنحركها alice.svg لنوضح دلالة كل ملف من هذه الملفات الملف index.html بداية نكتب الكود التالي في مستند HTML لإنشاء صفحة ويب تحتوي على ثلاث صور متماثلة مرتبة قطريًا، والتي سيتم تحريكها باستخدام جافا سكريبت <!DOCTYPE html> <html lang="en-US"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Sequencing animations</title> <script type="text/javascript" src="main.js" defer></script> <link href="style.css"rel="stylesheet"> </head> <body> <div id="alice-container"> <img id="alice1" src="alice.svg" role="img" alt="silhouette of crouching long haired character in dress and short boots"> <img id="alice2" src="alice.svg" role="img" alt="silhouette of crouching long haired character in dress and short boots"> <img id="alice3" src="alice.svg" role="img" alt="silhouette of crouching long haired character in dress and short boots"> </div> </body> </html> الملف style.css سنكتب في هذا الملف التنسيقات اللازمة لتنسيق الصور الثلاثة لعرضها في حاوية في وسط الصفحة باستخدام تخطيط الشبكة Grid Layout. ونعين مناطق محددة لكل صورة ضمن الشبكة لضمان ترتيبها بشكل قطري كما يلي: body { background: #6c373f; display: flex; justify-content: center; } #alice-container { width: 90vh; display: grid; place-items: center; grid-template-areas: "a1 . ." ". a2 ." ". . a3"; } #alice1 { grid-area: a1; } #alice2 { grid-area: a2; } #alice3 { grid-area: a3; } الملف main.js سنكتب في هذا الملف كود لاختيار الصور الثلاثة من الصفحة باستخدام معرفاتها الفريدة وتخزينها في متغيرات لتسهيل استخدامها لاحقًا في تحريكها. ونعرف ثابت aliceTumbling يحدد كيفية الحركة من خلال التحولات transforms التي ستقع للصورة وثابت aliceTiming يحدد تفاصيل توقيت الحركة. const aliceTumbling = [ { transform: 'rotate(0) scale(1)' }, { transform: 'rotate(360deg) scale(0)' } ]; const aliceTiming = { duration: 2000, iterations: 1, fill: 'forwards' } const alice1 = document.querySelector("#alice1"); const alice2 = document.querySelector("#alice2"); const alice3 = document.querySelector("#alice3"); لو فتحت اﻵن الملف "index.html" ضمن متصفحك سترى ثلاث صور مرتبة قطريًا على النحو التالي: انتبه إلى أن الملف الوحيد الذي ستعدله في الخطوات التالية من أجل تحريك الصور هو الملف "main.js". كتابة كود التحريك في جافا سكريبت نريد تحديث صفحة الويب السابقة لتحريك الصور الثلاث واحدة تلو اﻷخرى باستخدام فكرة الوعود Promises في جافا سكريبت، فعندما تنتهي حركة الصورة اﻷولى، نحرّك الصورة الثانية بعدها مباشرة، ومن ثم نحرك الصورة الثالثة. لتحقيق ذلك سنعدل شيفرة التحريك في الملف "main.js" لتدوير الصور وتقليص حجمها حتى تختفي، وانتبه إلى أن عملية التحريك تجري مرة واحدة لذا عليك إعادة تحميل الصفحة لمشاهدة الحركة مجددًا اتبع الخطوات المشروحة في اﻷقسام التالية. تحريك الصورة الأولى نستخدم الواجهة البرمجية Web Animations API لتحريك الصورة، وبالتحديد التابع ()element.animate، لهذا، حدّثنا الملف "main.js" باستدعاء التابع ()alice1.animate كالتالي: const aliceTumbling = [ { transform: "rotate(0) scale(1)" }, { transform: "rotate(360deg) scale(0)" }, ]; const aliceTiming = { duration: 2000, iterations: 1, fill: "forwards", }; const alice1 = document.querySelector("#alice1"); const alice2 = document.querySelector("#alice2"); const alice3 = document.querySelector("#alice3"); alice1.animate(aliceTumbling, aliceTiming); أعد تحميل الصفحة، وسترى كيف تدور الصورة اﻷولى وتتقلص. تحريك جميع الصور نريد الآن تحريك الصورة alice2 عند اكتمال حركة الصورة alice1 ثم تحريك alice3 عند اكتمال alice2. ويعيد التابع ()animate الكائن Animation الذي يمتلك الخاصية finished وهذه الاخيرة هي وعد Promise يُنجز عند انتهاء تحريك الصورة. لهذا بإمكانك استخدام هذا الوعد لتحديد متى تحرّك الصورة التالية. const aliceTumbling = [ { transform: 'rotate(0) scale(1)' }, { transform: 'rotate(360deg) scale(0)' } ]; const aliceTiming = { duration: 2000, iterations: 1, fill: 'forwards' } const alice1 = document.querySelector("#alice1"); const alice2 = document.querySelector("#alice2"); const alice3 = document.querySelector("#alice3"); alice1.animate(aliceTumbling, aliceTiming).finished .then(() => alice2.animate(aliceTumbling, aliceTiming).finished) .then(() => alice3.animate(aliceTumbling, aliceTiming).finished) .catch(error => console.error(`Error animating Alices: ${error}`)); عند تنفيذ الكود أعلاه ستحصل على الخرج التالي(اضغط على زر Rerun في الأسفل لمشاهدة الحركة من جديد): See the Pen sequencing-animations by Hsoub Academy (@HsoubAcademy) on CodePen. تنفيذ التسلسل باستخدام الوعود بطرق مختلفة نطلب منك أن تجرّب عدة طرق مختلفة في تنفيذ اﻷمر باستخدام الوعود: نفّذ الأمر باستخدام نسخة "جحيم الاستدعاءات Callback Hell" التي تحدثنا عنها في مقال سابق. نفّذ اﻷمر من جديد باستخدام سلسلة الوعود، وانتبه إلى وجود عدة طرق لكتابة الشيفرة نظرًا للأشكال المختلفة التي يمكنك من خلالها كتابة دالة سهمية. لهذا جرّب بعض اﻷشكال واستنتج الشكل اﻷسهل واﻷوضح قراءة. نفّذ اﻷمر باستخدام التعليمتين await و async. وتذكّر أن ()element.animate لا تعيد وعدًا Promise، بل كائن Animation له الخاصية finished وهي بحد ذاتها وعد. الخلاصة استعرضنا في هذا المقال طريقة تحريك مجموعة من الصور في جافا سكريبت من خلال تطبيق عملي بسيط لكود برمجي يعرض صورًا ثابتة ثم عدلناه لتحقيق حركة تسلسلية لهذه الصور بحيث تتحرك الصورة الأولى، وعند انتهائها تتحرك الصورة الثانية، وعند انتهائها تتحرك الصورة الثالثة باستخدام باستخدام الوعود لتحقيق التسلسل والواجهة البرمجية Web Animations API لتحقيق الحركة، وبإمكانك إلقاء نظرة على التمرين بشكله النهائي وبهذا الأسلوب، يمكنك تطبيق الوعود في مشاريعك لتحريك أي رسومات بترتيب محدد وإضافة ديناميكية وتفاعلية أكبر لصفحات الويب الخاصة بك. ترجمة -وبتصرف- للمقال: Sequencing animations اقرأ أيضًا المقال السابق: مدخل إلى عمّال Workers جافا سكريبت مقدمة إلى ردود النداء callbacks في جافاسكربت الواجهة البرمجية Promise في JavaScript إنشاء الحركات عبر جافاسكربت فهم الأحداث في جافاسكربت
  7. نتحدث في مقالنا عن عمّال جافا سكريبت workers وهي تقنية تساعدك في تنفيذ المهام ضمن خيوط معالجة threads منفصلة. ولقد أشرنا في مقالات سابقة إلى ما يحدث عندما تنفّذ عملية متزامنة طويلة في برنامجك، فقد يصبح البرنامج غير متجاوب بالكامل. ويعود السبب أساسًا إلى أن البرنامج يعمل على خيط معالجة واحد single-threaded. يُعرّف خيط المعالجة بأنه سلسلة من التعليمات المتلاحقة التي يتبعها البرنامج. فإن كان البرنامج وحيد الخيط سيُنفَّذ تعليمة تلو اﻷخرى، وسينتظر البرنامج انتهاء أي عملية متزامنة طويلة التنفيذ حتى يتابع ولا يمكنه أثناء ذلك تنفيذ أي شيء. لهذا السبب ظهرت تقنية العمال workers لتمنحك القدرة على إنجاز بعض المهام ضمن خيوط معالجة مختلفة، إذ تبدأ حينها مهمة معينة ثم تنتقل (وهي لا تزال قيد المعالجة) لتعمل على أخرى (كأن تعالج مدخلات المستخدم). لكن ما يجب الانتباه إليه هو قدرة العمال على الوصول إلى البيانات المشتركة وتغييرها بشكل مستقل وغير متوقع أحيانًا وهذا ما يسبب بعض الثغرات التي يصعب إيجادها. لهذا ولكي نتفادى هذا النوع من المشاكل وخاصة في تطبيقات الويب، لا ينبغي لشيفرة العمال وللشيفرة اﻷساسية الوصول إلى متغيرات اﻷخرى، وقد يشتركا ببعض البيانات في حالات خاصة جدًا. تُنفذ الشيفرة اﻷساسية وشيفرة العمال في عالمين منفصلين تمامًا، وتتواصلان مع بعضهما من خلال تبادل رسائل. ويعني ذلك تحديدًا عدم قدرة العمال على الوصول إلى شجرة DOM (مثل النوافذ أو المستند أو عناصر صفحة الويب). وهناك ثلاث أنواع مختلفة من العمال: عمال مختصون Dedicated workers. عمال مشتركون Shared workers. عمال خدمة Service workers. سنتعرف على النوع الأول من خلال مثال تطبيقي، ثم نناقش النوعين اﻵخرين باختصار. استخدام عمال ويب هل تتذكر تلك الصفحة التي توّلد أعدادًا أولية في مقال سابق؟ سنعود إليها ونستخدم عاملًا لتنفيذ الحسابات حتى تبقى الصفحة متجاوبة مع ما يفعله المستخدم. مولّد اﻷعداد اﻷولية المتزامن لنلق نظرة في البداية إلى شيفرة جافا سكريبت الموافقة: function generatePrimes(quota) { function isPrime(n) { for (let c = 2; c <= Math.sqrt(n); ++c) { if (n % c === 0) { return false; } } return true; } const primes = []; const maximum = 1000000; while (primes.length < quota) { const candidate = Math.floor(Math.random() * (maximum + 1)); if (isPrime(candidate)) { primes.push(candidate); } } return primes; } document.querySelector("#generate").addEventListener("click", () => { const quota = document.querySelector("#quota").value; const primes = generatePrimes(quota); document.querySelector("#output").textContent = `Finished generating ${quota} primes!`; }); document.querySelector("#reload").addEventListener("click", () => { document.querySelector("#user-input").value = 'Try typing in here immediately after pressing "Generate primes"'; document.location.reload(); }); ستتوقف استجابة البرنامج يعد أن نستدعي الدالة ()generatePrimes. مولّد أرقام أولية باستخدام عامل ويب Web worker عليك أولًا قبل أن تشرع العمل معنا تنزيل نسخة من ملفات البرنامج، وهي أربعة ملفات: index.html style.css main.js generate.js ستجد أن الملفين "index.html" و "style.css" مكتملان. إليك أولًا ملف HTML: <!doctype html> <html lang="en-US"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>Prime numbers</title> <script src="main.js" defer></script> <link href="style.css" rel="stylesheet" /> </head> <body> <label for="quota">Number of primes:</label> <input type="text" id="quota" name="quota" value="1000000" /> <button id="generate">Generate primes</button> <button id="reload">Reload</button> <textarea id="user-input" rows="5" cols="62"> Try typing in here immediately after pressing "Generate primes" </textarea> <div id="output"></div> </body> </html> وهذا محتوى ملف CSS: textarea { display: block; margin: 1rem 0; } كما ستجد الملفين "main.js" و "generate.js" فارغين وسنضع الشيفرة الرئيسية في الملف "main.js " وشيفرة العامل ضمن الملف "generate.js". إذا ما نلاحظه أولًا كيفية فصل شيفرة العامل عن الشيفرة الرئيسية، كما سترى أننا أضفنا فقط الشيفرة اﻷساسية إلى صفحة الويب "index.html" ضمن العنصر <script>. انسخ اﻵن الشيفرة التالية إلى الملف "main.js": //"generate.js" أنشئ عاملًا جديدًا، ونسند إليه الشيفرة الموجودة في الملف const worker = new Worker("./generate.js"); //أرسل رسالة إلى العامل "Generate primes" عندما ينقر المستخدم //"quota" وتضم أيضًا القيمة "generate" اﻷمر الموجود في الرسالة هو //وهي عدد اﻷرقام اﻷولية التي نولّدها document.querySelector("#generate").addEventListener("click", () => { const quota = document.querySelector("#quota").value; worker.postMessage({ command: "generate", quota, }); }); //عندما يعيد العامل ارسال إلى خيط المعالجة الرئيسي //حدّث صندوق الخرج برسال إلى المستخدم تتضمن عدد اﻷعداد الأولية التي ولدناها // والمأخوذة من بيانات الرسالة اﻷصلية worker.addEventListener("message", (message) => { document.querySelector("#output").textContent = `Finished generating ${message.data} primes!`; }); document.querySelector("#reload").addEventListener("click", () => { document.querySelector("#user-input").value = 'Try typing in here immediately after pressing "Generate primes"'; document.location.reload(); }); ننشئ بداية عاملًا باستخدام البانية ()Worker، ونمرر إليها عنوان URL يشير إلى سكريبت العامل.تُنفَّذ شيفرة العامل بمجرد إنشاءه. وكما هو الحال في النسخة المتزامنة من التطبيق نضيف معالجًا للحدث click خاصًا بالزر "Generate primes". لكن وبدلًا من استدعاء الدالة ()generatePrimes، نرسل رسالة إلى العامل باستخدام التابع ()worker.postMessage الذي يأخذ وسيطًا واحدًا. لهذا نمرر له كائن JSON يضم خاصيتين: command: وتضم قيمة نصية تخبر العامل ما عليه فعله (في حال كان باستطاعته تنفيذ أكثر من شيء). quota: عدد اﻷعداد اﻷولية التي يولّدها. نضيف تاليًا معالج الحدث message إلى العامل، لكي يبلغنا العامل من انتهاء عمله ويعيد أية نتائج نريدها. يأخذ معالج الحدث بياناته من الخاصية data العائدة للرسالة ويطبعها ضمن عنصر الخرج (البيانات هنا هي نفسها قيمة الخاصية quota، لذا لا حاجة لها عمليًا ووضعناها لعرض مبدأ العمل فقط). أضفنا اخيرة شيفرة معالج حدث النقر click للزر "Reload"، وهي مشابهة تمامًا لشيفرة النسخة المتزامنة. انقل اﻵن الشيفرة التالية إلى الملف "generate.js": // Listen for messages from the main thread. // If the message command is "generate", call `generatePrimes()` addEventListener("message", (message) => { if (message.data.command === "generate") { generatePrimes(message.data.quota); } }); // Generate primes (very inefficiently) function generatePrimes(quota) { function isPrime(n) { for (let c = 2; c <= Math.sqrt(n); ++c) { if (n % c === 0) { return false; } } return true; } const primes = []; const maximum = 1000000; while (primes.length < quota) { const candidate = Math.floor(Math.random() * (maximum + 1)); if (isPrime(candidate)) { primes.push(candidate); } } // When we have finished, send a message to the main thread, // including the number of primes we generated. postMessage(primes.length); } وتذكّر أن هذه الشيفرة ستُنفَّذ بمجرد إنشاء عامل جديد. يترصّد العامل بداية الرسائل التي ترسلها الشيفرة الرئيسية من خلال الدالة ()addEventListener وهي دالة عامة في العامل. وتضم الخاصية data الموجودة ضمن معالج حدث الرسالة message نسخة من الوسيط الذي تمرره الشيفرة الرئيسية. فإذا مررت الشيفرة الرئيسية اﻷمر generate، نستدعي حينها الدالة ()generatePrimes ونمرر لها القيمة quota من الحدث message. تشبه الدالة ()generatePrimes مقابلتها في النسخة المتزامنة من التمرين ما عدا أننا نرسل رسالة إلى السكريبت الرئيسي عند الانتهاء بدلًا من إعادة قيمة. ونستخدم في هذه الحالة التابع ()postMessage والذي يشبه الدالة ()addEventListener بأنه عام في شيفرة العامل أيضًا. وكما رأينا، يستمع السكريبت الرئيسي إلى الرسالة ويُحدّث شجرة DOM عند استقبال الرسالة. ملاحظة: لتشغيل هذا الموقع، عليك تشغيل خادم محلي على حاسوبك،إذ لا يُسمح بتحميل شيفرة العامل من الوجهة. وإن صادفتك أي مشاكل في إنشاء نسختك من التمرين` بإمكانك الاطلاع على النسخة المكتملة) منه على جت-هب أو تجربته مباشرة. أنواع أخرى من العمال workers يُدعى العامل الذي أنشأناه في المثال السابق بالعامل المخصص dedicated worker. ويعني ذلك أنه استخدم من قبل سكريبت واحد. وهنالك نوعين آخرين هما: العمال المشتركون shared workers: ويمكن مشاركتهم بين أكثر من سكريبت أثناء تنفيذها في نوافذ مختلفة للمتصفح. عمّال الخدمة service workers: ويعملون كخوادم وكيلة proxy servers أو لتخزين الموارد مؤقتًا كي تعمل صفحة ويب عندما لا يكون المتصفح متصلا بالشبكة، فهي مكوّن أساسي من مكوّنات تطبيقات الويب المتقدمة Progressive Web Apps الخلاصة تعرّفنا في هذا المقال على عمال ويب web workers، وهي تقنية تمكّن تطبيق ويب من نقل المهام إلى خيط معالجة آخر. ورأينا أن خيط المعالجة اﻷساسي وخيط العمال لا يتشاركان المتغيرات، بل يتواصلان من خلال إرسال الرسائل التي يتلقاها الطرف اﻵخر على شكل أحداث للكائن message. يمكن أن تقدم هذه التقنية طريقة فعالة ﻹبقاء التطبيق اﻷساسي متجاوبًا، على الرغم من عدم قدرة العمال على الوصول إلى كل الواجهات البرمجية التي يصلها التطبيق اﻷساسي وخصوصًا عناصر شجرة DOM. ترجمة -وبتصرف- للمقال: Introducing workers اقرأ أيضًا: المقال السابق: إنجاز واجهة برمجية في جافا سكريبت تعتمد على الوعود معالجة الأحداث في جافا سكريبت مدخل إلى جافا سكريبت غير المتزامنة المزخرفات decorators والتمرير forwarding في جافاسكربت
  8. شرحنا في المقال السابق طريقة استخدام واجهات برمجة التطبيقات التي تعيد الوعود promises، وسنتحدث في هذا المقال عن تعريف الواجهات البرمجية التي تعيد وعودًا في لغة البرمجة جافا سكريبت، ونلقي نظرة على كيفية إنجاز هذه الواجهات البرمجية بأنفسنا، وعلى الرغم من أن مهمة كهذه ليست شائعة الاستخدام كثيرًا لكن من المفيد لك معرفتها. ملاحظة: عند تنفيذ واجهة برمجة تطبيقات معتمدة على الوعود promise-based API ستحتاج لأن نغلّف عملية غير متزامنة (مثل اﻷحداث events، أو دوال رد النداء callbacks، أو نموذج يعمل على تمرير الرسائل message-passing model) وسيكون عليك هيكلة كائن Promise للتعامل مع حالات نجاح تنفيذ هذه العملية resolve أو إخفاقها reject. تنفيذ الواجهة البرمجية ()alarm ما سنفعله في هذا المثال هو إنجاز واجهة برمجية لمنبّه تُدعى ()alarm. تقبل هذه الواجهة وسيطًا هو اسم الشخص الذي ستوقظه كما تأخذ وسيطًا آخر هي الفترة الزمنية التي ينتظرها المنبه بالميلي ثانية قبل إيقاظ الشخص. بعد ذلك تُرسل الدالة الرسالة "Wake up" (أي استيقظ) يليها اسم الشخص. تغليف الدالة ()setTimeout نستخدم الواجهة ()setTimeout في إنجاز واجهتنا ()alarm، وتأخذ هذه الواجهة وسيطين: دالة تستدعيها عند بلوغ زمن التنبيه، واﻵخر زمن التنبيه بالميلي ثانية. وعندما تُستدعى الواجهة ()setTimeout، تبدأ بالعد وصولًا إلى زمن التنبيه وتستدعي عندها الدالة التي مررناها إليها. نستدعي في مثالنا التالي الدالة ()setTimeout مع وسيطيها دالة رد النداء وزمن التنبيه الذي سيكون 1000 ميلي ثانية. إليك شيفرة HTML: <button id="set-alarm">Set alarm</button> <div id="output"></div> ثم شيفرة جافا سكريبت: const output = document.querySelector("#output"); const button = document.querySelector("#set-alarm"); function setAlarm() { setTimeout(() => { output.textContent = "Wake up!"; }, 1000); } button.addEventListener("click", setAlarm); إليك نتيجة التنفيذ: See the Pen promise-based API1 by Hsoub Academy (@HsoubAcademy) on CodePen. الدالة البانية ()Promise تُعيد الدالة وعدًا Promise يُنجز عندما ينقضي الوقت المخصص للمنبّه، ويمرر الرسالة إلى معالج ()then بينما يُرفض الوعد إن كان زمن التنبيه سالبًا. إن المفتاح الرئيسي للعمل هنا هي الدالة البانية ()Promise التي تأخذ دالة استدعاء كوسيط لها وندعوها دالة منفّذة executer وعليك كتابة شيفرة هذه الدالة عند إنشاء وعد جديد. وتأخذ الدالة المنفذة دالتين كوسيط لها تُدعيان تقليديًا الدالة resolve والدالة reject. تستدعي الدالة المنفذة الدالة غير المتزامنة فإن أنجز الوعد ستُستدعى الدالة resolve وإن أخفق تُستدعى الدالة reject. إما إذا رمت الدالة المنفذة خطأً، ستُستدعى reject تلقائيًا. باﻹمكان تمرير أية قيم إلى الدالتين resolve و reject. لذا بإمكانك كتابة شيفرة الواجهة ()alarm كالتالي: function alarm(person, delay) { return new Promise((resolve, reject) => { if (delay < 0) { throw new Error("Alarm delay must not be negative"); } setTimeout(() => { resolve(`Wake up, ${person}!`); }, delay); }); } تُنشئ الدالة السابقة وعدًا Promise وتعيده، وضمن الدالة المنفذة: نتحقق من قيمة زمن التنبيه delay حتى لا تكون سالبة. نستدعي الدالة ()setTimeout ونمرر لها القيمة delay، ومن ثم تُستدعى دالة رد النداء الموجودة ضمن الدالة السابقة عند انقضاء الوقت المحدد ومن ثم تٌستدعى الدالة resolve بعد أن نمرر لها الرسالة "!Wake up". استخدام الواجهة البرمجية ()alarm من المفترض أن تكون الشيفرة التالية مألوفة من المقال السابق. إذ نستطيع استدعاء الواجهة ()alarm ومن ثم نستدعي ()then و ()catch لكائن الوعد الذي تعيده للتعامل مع حالتي إنجاز الوعد أو رفضه: const name = document.querySelector("#name"); const delay = document.querySelector("#delay"); const button = document.querySelector("#set-alarm"); const output = document.querySelector("#output"); function alarm(person, delay) { return new Promise((resolve, reject) => { if (delay < 0) { throw new Error("Alarm delay must not be negative"); } setTimeout(() => { resolve(`Wake up, ${person}!`); }, delay); }); } button.addEventListener("click", () => { alarm(name.value, delay.value) .then((message) => (output.textContent = message)) .catch((error) => (output.textContent = `Couldn't set alarm: ${error}`)); }); إليك نتيجة التنفيذ: See the Pen promise-based API2 by Hsoub Academy (@HsoubAcademy) on CodePen. جرب تحديد قيم مختلفة لكل من person و delay وحاول تمرير قيم delay سالبة وراقب الخرج الناتج. استخدام التعليمين async و await مع الواجهة البرمجية alarm()‎ طالما أن الواجهة ()alarmتعيد وعدًا، نستطيع أن نطبق عليه ما يُطبق على أي وعد آخر، مثل ربطه ضمن سلسلة وعود promise-chain أو استخدام ()all. وكذلك wait/async. const name = document.querySelector("#name"); const delay = document.querySelector("#delay"); const button = document.querySelector("#set-alarm"); const output = document.querySelector("#output"); function alarm(person, delay) { return new Promise((resolve, reject) => { if (delay < 0) { throw new Error("Alarm delay must not be negative"); } setTimeout(() => { resolve(`Wake up, ${person}!`); }, delay); }); } button.addEventListener("click", async () => { try { const message = await alarm(name.value, delay.value); output.textContent = message; } catch (error) { output.textContent = `Couldn't set alarm: ${error}`; } }); إليك نتيجة التنفيذ: See the Pen promise-based API3 by Hsoub Academy (@HsoubAcademy) on CodePen. الخلاصة تعرفنا في هذا المقال على كيفية استخدام الواجهات البرمجية التي تعيد وعودًا promises، وألقينا نظرة على كيفية إنجاز الواجهات البرمجية التي تعيد الوعود من خلال مثال تطبيقي ينجز واجهة منبه للمستخدم بعد انقضاء وقت معين. ترجمة -وبتصرف- للمقال: How to implement a promise-based API اقرأ أيضًا: المقال السابق: استخدام الوعود Promises في جافا سكريبت اللاتزامن والانتظار async/await في جافاسكربت مدخل إلى جافا سكريبت غير المتزامنة البرمجة غير المتزامنة في جافاسكريبت الوعود Promise في جافاسكربت
  9. تُعد فكرة الوعود Promises أساسًا للغة جافا سكريبت غير المتزامنة. والوعد هو كائن يُعاد من الدالة غير المتزامنة ويمثّل الوضع الراهن للعملية. ولا تكون العملية قد انتهت بعد في الوقت الذي تعيد في الدالة الوعد إلى مستدعيها، لكن كائن الوعد المُعَاد يمتلك توابع لمعالجة التنفيذ الناجح أو المخفق للعملية. تحدثنا في المقال السابق عن استخدام الاستدعاءات لإنجاز الدوال غير المتزامنة. إذ نستدعي وفق هذا اﻷسلوب الدالة غير المتزامنة ممررين إليها دالة استدعاء أخرى تسمى دالة رد النداء callback، عندها تُعيد هذه الدالة قيمتها مباشرة، ثم تستدعي بعد ذلك دالة رد النداء التي مررناها عندما تنتهي العملية. وما يحدث في الواجهات البرمجية المبنية على الوعود، أن الدالة غير المتزامنة تبدأ عملية ما وتعيد كائن وعد Promise، وبإمكانك حينها ربط هذا الكائن بمعالجات أحداث ستُنفَّذ عند نجاح أو إخفاق هذه العملية. استخدام الواجهة البرمجية fetch ملاحظة: سنتعلم مفهوم الوعود في هذا المقال بنسخ عينات من الكود البرمجي من الصفحة إلى طرفية جافا سكريبت في المتصفح. وﻹعداد هذا اﻷمر: انتقل إلى الموقع: https://example.org افتح طرفية جافا سكريبت الموجودة ضمن أدوات مطوري الويب في نفس النافذة الفرعية. عندما نعرض مثالًا ما، انسخه إلى الطرفية، وعليك حينها إعادة تحميل الصفحة في كل مرة تُلصق فيها مثالًا جديدًا، وإلا تعترض الطرفية لأنك أعدت تصريح المتغير fetchPromise. سننزل في هذا المثال ملف JSON ونسجّل بعض المعلومات المتعلقة به، ولتنفيذ اﻷمر نرسل طلب HTTP إلى الخادم يتضمن رسالة مرسلة إليه وننتظر الاستجابة. ففي مثالنا سنطلب ملف JSON من الخادم. وكما أشرنا في المقال السابق، نستخدم الواجهة البرمجية XMLHttpRequest لتنفيذ طلبات HTTP، لكننا سنستخدم في هذا المقال الواجهة البرمجية ()fetch وهي بديل أحدث عن كائن XMLHttpRequest ومبنية على الوعود. انسخ اﻵن الشيفرة التالية إلى طرفية جافا سكريبت في متصفحك: const fetchPromise = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); console.log(fetchPromise); fetchPromise.then((response) => { console.log(`Received response: ${response.status}`); }); console.log("Started request…"); ما فعلناه في هذه الشيفرة هو التالي: استدعينا الواجهة()fetchوأسندنا القيمة التي تعيدها إلى المتغير fetchPromise. سجلنا بعد ذلك مباشرة قيمة المتغير، ومن المفترض أن تشبه النتيجة ما يلي: Promise { <state>: "pending" }، لتخبرنا أنه لدينا كائن وعد Promise له حالة قيمتها "pending" ويعني ذلك أن عملية إحضار الملف لا تزال قيد التنفيذ. مررنا دالة معالجة إلى التابع ()then العائد لكائن الوعد، فإن نجحت العملية وعندما تنتهي، يستدعي الوعد دالة المعالجة التي نمرر إليها كائن الاستجابة Response الذي يضم استجابة الخادم. طبعنا الرسالة "…Started request" لتدل على أننا بدأنا تنفيذ الطلب. من المفترض أن يكون خرج الشيفرة السابقة كالتالي: Promise { <state>: "pending" } Started request… Received response: 200 لاحظ كيف ظهرت العبارة "Started request" على الشاشة قبل تلقي الاستجابة. فعلى خلاف الدوال المتزامنة، تعيد()fetch قيمتها قبل أن يكتمل الطلب، مما يسمح للبرنامج بمتابعة التنفيذ. ثم يعيد البرنامج بعد ذلك رمز الحالة 200 ويعني أن الطلب قد نُفِّذ بنجاح. قد يبدو هذا المثال مشابهًا للمثال في المقال السابق الذي استخدمنا فيه معالجات أحداث على الكائن XMLHttpRequest، لكننا مررنا هذه المرة دالة معالجة إلى التابع ()then العائد لكائن الوعد الذي تعيده الدالة()fetch. سلسلة من الوعود بمجرد حصولك على كائن استجابة Response باستخدام الواجهة()fetch، عليك استدعاء دالة أخرى للحصول على بيانات الاستجابة، ونريدها في هذه الحالة على هيئة بيانات JSON لهذا نستدعي التابع ()json العائد للكائن Respond. وكذلك الأمر، التابع ()json غير متزامن، هنا سنكون أمام حالة نستدعي فيها دالتين غير متزامنتين على التوالي. جرّب اﻵن ما يلي: const fetchPromise = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); fetchPromise.then((response) => { const jsonPromise = response.json(); jsonPromise.then((data) => { console.log(data[0].name); }); }); أضفنا في هذا المثال أيضًا التابع ()then إلى الوعد الذي تعيده ()fetch. لكن المعالج سيستدعي هذه المرة()response.json ومن ثم يمرر معالج ()then جديد إلى الوعد الذي يعيده التابع ()response.json. يُفترض في هذه الحالة طباعة العبارة في الطرفية (وهو اسم أول منتج في القائمة التي يضمها الملف "products.json"). لكن مقارنة مع الاستدعاءات التي شرحناها في مقال سابق، سنجد أننا نستدعي التابع ()then ضمن تابع ()then آخر وهذا مشابهة لفكرة استدعاء دالة استدعاء ضمن دالة استدعاء بشكل متعاقب، وكنا قد قلنا بأن هذا اﻷمر سيزيد من صعوبة قراءة الشيفرة وفهمها، وأطلقنا عليه اسم "جحيم الاستدعاء callback hell"، وما يحدث هنا أمر مشابه لكن مع الدالة ()then! نعم، اﻷمر نفسه تمامًا، لكن الوعد يقدم ميزة خاصة وهي أن التابع ()then يعيد هو أيضًا وعدًا يكتمل بنتيجة الدالة التي مُرر إليها. لهذا من اﻷفضل إعادة كتابة المثال السابق كالتالي: const fetchPromise = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); fetchPromise .then((response) => response.json()) .then((data) => { console.log(data[0].name); }); فبدلًا من استدعاء تابع ()then آخر ضمن معالج التابع ()then الأول، يمكننا إعادة الوعد الذي يعيده التابع ()json ثم نستدعي التابع ()then الثاني على القيمة المُعادة. يُدعى هذا اﻷمر بسلسلة الوعود Promise chaining، ونستطيع من خلال هذه الميزة تفادي زيادة مستوى التداخل عندما نضطر إلى استدعاء الدوال غير المتزامنة بشكل متتالٍ. قبل الانتقال إلى الخطوة التالية، علينا إضافة شيء آخر، وهو التأكد من قبول الخادم للطلب وقدرته على معالجته قبل أن نحاول قراءة الاستجابة، ولتنفيذ اﻷمر نتحقق من رمز حالة الطلب ونعرض خطأ إن لم يكن رمز الحالة 200 (أو OK) const fetchPromise = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); fetchPromise .then((response) => { if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return response.json(); }) .then((data) => { console.log(data[0].name); }); التقاط اﻷخطاء وصلنا حاليًا إلى الجزء اﻷخير المتعلق بمعالجة الأخطاء. إذ يمكن للواجهة البرمجية ()fetch رمي أخطاء لأسباب عديدة مثل عدم وجود اتصال بالشبكة أو تمرير عنوان URL غير صالح وغيرها من الأسباب، وبإمكاننا أيضًا رمي أخطاء بأنفسنا إن أعاد الخادم حالة خطأ. ورأينا في المقال السابق أن معالجة اﻷخطاء قد يغدو صعبًا عند تداخل الاستدعاءات ويدفعنا لمعالجة اﻷخطاء في كل مستوى على حدة. ولمعالجة اﻷخطاء، يقدّم الكائن Promise التابع ()catch الذي يشبه كثيرًا التابع ()then من حيث استدعاؤه وتمريره إلى دالة المعالجة. لكن ما يحدث أن استدعاء التابع ()catch يكون عند إخفاق العملية غير المتزامنة وليس نجاحها. فلو أضفت ()catch إلى نهاية سلسلة الوعود، سيُستدعى هذا التابع إن أخفقت أية دالة غير متزامنة في السلسلة. أي بإمكانك إنجاز أي عملية غير متزامنة على شكل سلسلة من الدوال غير المتزامنة المتتابعة التي تنتهي بتابع يعالج جميع اﻷخطاء. جرّب هذه النسخة التي تستخدم ()catch وتُعدّل عنوان URL حتى تُخفق العملية: const fetchPromise = fetch( "bad-scheme://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); fetchPromise .then((response) => { if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } return response.json(); }) .then((data) => { console.log(data[0].name); }) .catch((error) => { console.error(`Could not get products: ${error}`); }); نفّذ الشيفرة السابقة، ومن المفترض أن ترى الخطأ الذي يعرضه التابع ()catch. المصطلحات الخاصة بالوعود تستخدم الوعود مجموعة مخصصة من المصطلحات التي لا بد من توضيحها. بداية للوعد حالة من ثلاث حالات وهي كالتالي: قيد التنفيذ pending: يُنشأ كائن الوعد في هذه الحالة، لكن الدالة غير المتزامنة المرتبطة به لم تنجح أو تخفق بعد. وتوافق هذه الحالة إعادة الوعد إلى الدالة بعد استدعاء()fetch بينما لا يزال الطلب قيد التنفيذ. منجز fulfilled: وهي حالة نجاح العملية غير المتزامنة، ويُستدعى حينها التابع ()then. مرفوض rejected: وهي حالة إخفاق العملية غير المتزامنة، ويُستدعى عندها التابع ()catch. أما معنى "النجاح" أو "اﻹخفاق" فيعود للواجهة البرمجية المستخدمة. فالواجهة()fetch مثلًا ترفض الوعد المعاد لأسباب منها خطأ في الشبكة يمنع إرسال الطلب، وتنجزه عندما يعيد الخادم الاستجابة حتى لو كانت اﻹستجابة حالة خطأ مثل 404 Not Found. كما نستخدم أيضًا مصطلح "مسوّىً settled" ليشير إلى حالتي الرفض أو اﻹنجاز. ونقول عن الوعد أنه "مقضي resolved" إن جرت "تسويته settled" أو كان "مقفلًا locked in" بانتظار حالة وعد آخر. الجمع بين عدة وعود إن تكوّنت العملية غير المتزامنة من عدة دوال نستخدم حينها سلسلة من الوعود، ولا بد حينها من تسوية كل وعد قبل الانتقال إلى اﻵخر. لكنك قد تحتاج أحيانًا إلى الجمع بين عدة استدعاءات لدوال غير المتزامنة، لهذا تزوّدك الواجهة البرمجية ()Promis ببعض الدوال المساعدة. قد يتطلب الأمر في بعض التطبيقات إنجاز عدة وعود لا تتعلق ببعضها البعض. والطريقة اﻷكثر فعالية لتنفيذ هذا اﻷمر هي استدعاء جميع الدوال في نفس الوقت، ثم الحصول على تنبيه عندما تنجز جميعها، وهذا ما يقدمه التابع ()Promise.all فهو يُعيد مصفوفة من الوعود كما يعيد وعدًا واحدًا، ويكون هذا الوعد: منجزَا: عندما تُنجز كل الوعود في المصفوفة. ويُستدعى عند ذلك معالج التابع ()then وتُمرّر له مصفوفة من الاستجابات وبنفس ترتيب الوعود التي مُررت إلى التابع ()all. مرفوضًا: إن رٌفض أي من وعود المصفوفة، ويُستدعى عند ذلك معالج التابع ()catch ويُمرر له الخطأ الناتج عن الوعد الذي رُفض. إليك مثالًا: const fetchPromise1 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); const fetchPromise2 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found", ); const fetchPromise3 = fetch( "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json", ); Promise.all([fetchPromise1, fetchPromise2, fetchPromise3]) .then((responses) => { for (const response of responses) { console.log(`${response.url}: ${response.status}`); } }) .catch((error) => { console.error(`Failed to fetch: ${error}`); }); نفّذنا في الشيفرة السابقة ثلاثة طلبات()fetchإلى ثلاثة عناوين URL مختلفة، فإن نجحت هذه الطلبات، نعرض حالة الاستجابة لكل طلب، وإن أخفقت إحداها، نطبع الخطأ. ينبغي أن تُنجز الطلبات جميعها لأن العناوين صحيحة، مع ملاحظة أن رقم الحالة للطلب الثاني سيكون 404 بدلًا من 200 لأن الملف الذي نطلبه غير موجود حقيقة. لهذا سيكون خرج الكود السابق على النحو التالي: https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json: 200 https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found: 404 https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json: 200 جرّب أن تنفّذ الشيفرة السابقة بعد كتابة العناوين بشكل خاطئ: const fetchPromise1 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); const fetchPromise2 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found", ); const fetchPromise3 = fetch( "bad-scheme://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json", ); Promise.all([fetchPromise1, fetchPromise2, fetchPromise3]) .then((responses) => { for (const response of responses) { console.log(`${response.url}: ${response.status}`); } }) .catch((error) => { console.error(`Failed to fetch: ${error}`); }); نتوقع اﻵن أن تُنفَّذ دالة المعالجة الخاصة بالتابع ()catch، ومن المفترض حينها أن يكون الخرج مشابهًا للتالي: Failed to fetch: TypeError: Failed to fetch وقد تريد في بعض الحالات أن يُنجز أحد الوعود ولا يهم أيها، عندها يمكنك الاستفادة من التابع ()Promise.any الذي يشابه ()Promise.all لكنه يُنجز بمجرد إنجاز أي وعد في مصفوفة الوعود، ويُرفض إن رُفضت جميع الوعود: const fetchPromise1 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); const fetchPromise2 = fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found", ); const fetchPromise3 = fetch( "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json", ); Promise.any([fetchPromise1, fetchPromise2, fetchPromise3]) .then((response) => { console.log(`${response.url}: ${response.status}`); }) .catch((error) => { console.error(`Failed to fetch: ${error}`); }); ملاحظة: لا يُمكن في هذه الحالة توقّع أي وعد سيُنجز أولًا. للتعرف على بقية التوابع التي يمكن استخدامها للجمع بين الوعود راجع توثيق ()Promis. استخدام التعليمتين async و await تُسهّل التعليمة async عمل الشيفرة غير المتزامنة التي تعتمد على الوعود، فإضافة هذه التعليمة إلى بداية الدالة يجعلها دالة غير متزامنة: async function myFunction() { // This is an async function } وباﻹمكان استخدام التعليمة await قبل استدعاء الدالة التي تعيد وعدًا. وهذا ما يجعل الشيفرة تنتظر عند هذه النقطة حتى يسوّى الوعد وعندها تُعد قيمة الوعد المنجز هي القيمة المعادة من قبل الدالة أو ترمي الدالة قيمة الوعد المرفوض كخطأ. وهكذا ستتمكن من كتابة شيفرة غير متزامنة مع أنها تبدو كذلك. لهذا سنحاول كتابة مثال()fetch كالتالي: async function fetchProducts() { try { //`()fetch` تنتظر الدالة بعد هذا السطر حتى يسوّى الاستدعاء // الذي سيُعسد استجابة أو يرمي خطأ const response = await fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } //`response.json()` تنتظر الدالة بعد هذا السطر حتى يسوّى الاستدعاء //أو يرمي خطأ JSON الذي يعيد كائن const data = await response.json(); console.log(data[0].name); } catch (error) { console.error(`Could not get products: ${error}`); } } fetchProducts(); نستدعي هنا الدالة ()await fetch وسنحصل على كائن استجابة Response مكتمل بدلًا من الوعد ()Promisوكأن()fetchدالة متزامنة. ونستطيع أيضًا استخدام الكتلة try...catch لمعالجة اﻷخطاء كما لو كنا نكتب شيفرة متزامنة. وتذكر أن الدوال غير المتزامنة تُعيد وعدًا دائمًا، لهذا لا يمكن أن نكتب شيفرة كهذه: async function fetchProducts() { try { const response = await fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error(`Could not get products: ${error}`); } } const promise = fetchProducts(); console.log(promise[0].name); //هو كائن وعد فلن تعمل هذه الشيفرة "promise" ويجب عليك تصحيح الكود السابق على النحو التالي: async function fetchProducts() { try { const response = await fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const data = await response.json(); return data; } catch (error) { console.error(`Could not get products: ${error}`); } } const promise = fetchProducts(); promise.then((data) => console.log(data[0].name)); وتذكر أن استخدام await يكون ضمن دالة، إلا في الحالة التي تكون فيها الشيفرة ضمن وحدة JavaScript module وليس ضمن سكريبت نمطي: try { // using await outside an async function is only allowed in a module const response = await fetch( "https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json", ); if (!response.ok) { throw new Error(`HTTP error: ${response.status}`); } const data = await response.json(); console.log(data[0].name); } catch (error) { console.error(`Could not get products: ${error}`); } قد تستخدم دوال async كثيرًا مقارنة باستخدام سلسلة الوعود لكونها تجعل العمل مع الوعود أكثر وضوحًا. وتذكر أن await -كما هو حال سلسلة الوعود- تجبر العمليات المتزامنة على التنفيذ المتسلسل، وهذا أمر ضروري إن اعتمدت نتيجة العملية الثانية على سابقتها، أما إن لم يكن الوضع كذلك، ففكرّ في هذه الحالة باستخدام الدالة ()Promise.all. الخلاصة تُعرفنا في مقال اليوم على مفهوم الوعود التي تعد أساسًا للبرمجة غير المتزامنة في جافا سكريبت الحديثة. إذ تجعل تسلسل العمليات غير المتزامنة أكثر وضوحًا وإدراكًا بدلًا من التداخل المفرط للاستدعاءات، كما تدعم نمطًا من معالجة الأخطاء مشابهًا ﻵلية try...catch. وتسّهل التعليمتان async و await بناء عمليات باستدعاء سلسلة من الدوال غير المتزامنة المتتابعة دون الحاجة إلى سلاسل صريحة من الوعود وكتابة شيفرة شبيهة بالشيفرة المتزامنة. تعمل الوعود في النسخ اﻷخيرة من معظم المتصفحات الحديثة، وستكون المشكلة فقط مع متصفحي أوبرا ميني Opera mini وإنترنت إكسبلورر 11 والنسخ اﻷقدم. لم نناقش بالتأكيد كل ميزات الوعود في مقالنا الحالي، بل سلطنا الضوء على الميزات اﻷكثر أهمية واستخدامًا، وستتعلم خلال مسيرتك في تعلم جافا سكريبت الكثير من الميزات والتقنيات المفيدة الأخرى، ويجدر بالذكر أن الكثير من واجهات الويب البرمجية مبنية أساسًا على الوعود مثل WebRTC و Web Audio API Media Capture and Streams API وغيرها الكثير. ترجمة -وبتصرف- للمقال: How to use promises اقرأ أيضًا: المقال السابق: مدخل إلى جافا سكريبت غير المتزامنة اللاتزامن والانتظار async/await في جافاسكربت الوعود Promise في جافاسكربت البرمجة غير المتزامنة في جافاسكريبت
  10. نتحدث في هذا المقال عن مفهومي البرمجة المتزامنة synchronous وغير المتزامنة asynchronous، وفوائد استخدام التقنيات غير المتزامنة، كما نتحدث بإيجاز عن المشاكل التي تعلّقت تاريخيًا بتنفيذ الدوال غير المتزامنة في جافا سكريبت. تُعرّف البرمجة غير المتزامنة أنها تقنية تسمح لبرامجك بتنفيذ مهام مطوّلة تأخذ وقتًا طويلًا لتنفيذها، لكنها تبقى مع ذلك قادرة على الاستجابة للأحداث التي تقع أثناء تنفيذ هذه المهمة، بدلًا من الانتظار حتى تنتهي بالكامل. وستظهر نتيجة الاستجابة للحدث بمجرد انتهاء المهمة اﻷساسية وعرض نتيجة التنفيذ. على سبيل المثال معظم الدوّال المضمّنة في المتصفحات قد تستغرق وقتًا لذلك فهي تنفذ بطريقة غير متزامنة، نذكر منها على سبيل المثال: ()fetch: تستخدم لإحضار الموارد باستخدام طلبات HTTP. ()getUserMedia: تستخدم للوصول إلى كاميرا أو ميكروفون المستخدم. ()showOpenFilePicke: تستخدم لتطلب من المستخدم اختيار ملف. وقد لا تضطر كثيرًا لاستخدام دوال غير متزامنة خاصة بك، لكن إن احتجت إلى ذلك، لا بد من معرفة الطريقة الصحيحة لتنفيذها. سنبدأ هذا المقال بالنظر إلى ما تسببه الدوال المتزامنة التي تستغرق وقتًا طويلًا لتنفيذها، وضرورة استخدام البرمجة غير المتزامنة في هذه الحالات. البرمجة المتزامنة لنتأمل الشيفرة التالية: const name = "Muhammd"; const greeting = `Hello, my name is ${name}!`; console.log(greeting); // "Hello, my name is Muhammd!" في هذه الشيفرة: name: يصرّح عن نص. greeting: يصرّح عن نص آخر يستخدم المتغير name. تطبع الشيفرة بعد ذلك رسالة الترحيب على الطرفية. ينتقل المتصفح سطرًا سطرًا في الشيفرة السابقة وينفذها بهذا الترتيب. وينتظر المتصفح نهاية تنفيذ السطر اﻷول حتى ينتقل إلى السطر التالي، فكل سطر يعتمد على السطر الذي يسبقه. ندعو هذا البرنامج برنامجًا متزامنًا synchronous program، وسيبقى متزامنًا حتى لو استخدمنا دالة منفصلة كما في المثال التالي: function makeGreeting(name) { return `Hello, my name is ${name}!`; } const name = "Muhammd"; const greeting = makeGreeting(name); console.log(greeting); // "Hello, my name is Muhammd!" فالدالة ()makeGreating هي دالة متزامنة، لأن مُستدعيها سينتظر حتى تنهي الدالة عملها وتعيد القيمة المطلوبة ثم يستأنف تنفيذ الشيفرة البرمجية. دالة متزامنة تستغرق وقتًا في التنفيذ ماذا لو استغرقت الدالة المتزامنة وقتًا طويلًا في التنفيذ؟ سنعرض تاليًا برنامجًا يعتمد على خوارزمية ضعيفة وغير مجدية لتوليد أعداد أولية عندما ينقر المستخدم على زر "توليد أعداد أولية Generating Primes". وكلما كبر عدد اﻷعداد الأولية الذي يحدده المستخدم ستستغرق الدالة وقتًا أكبر ﻹيجاد النتيجة. إليك شيفرة HTML: <label for="quota">Number of primes:</label> <input type="text" id="quota" name="quota" value="1000000" /> <button id="generate">Generate primes</button> <button id="reload">Reload</button> <div id="output"></div> وشيفرة جافا سكريبت: const MAX_PRIME = 1000000; function isPrime(n) { for (let i = 2; i <= Math.sqrt(n); i++) { if (n % i === 0) { return false; } } return n > 1; } const random = (max) => Math.floor(Math.random() * max); function generatePrimes(quota) { const primes = []; while (primes.length < quota) { const candidate = random(MAX_PRIME); if (isPrime(candidate)) { primes.push(candidate); } } return primes; } const quota = document.querySelector("#quota"); const output = document.querySelector("#output"); document.querySelector("#generate").addEventListener("click", () => { const primes = generatePrimes(quota.value); output.textContent = `Finished generating ${quota.value} primes!`; }); document.querySelector("#reload").addEventListener("click", () => { document.location.reload(); }); وستكون نتيجة التنفيذ كالتالي: See the Pen frame_a_long-running_synchronous_function by Hsoub Academy (@HsoubAcademy) on CodePen. جرّب أن تنقر على الزر "Generate primes"، ستلاحظ أن الحاسوب سيتأخر عدة ثوان (وفقًا لسرعة حاسوبك) قبل أن يعرض رسالة الانتهاء "!Finished". مشاكل الدوال المتزامنة تشبه شيفرة المثال التالي ما تنفذه شيفرة المثال السابق باستثناء وجود مربع نص لتكتب فيه. جرّب أن تنقر اﻵن على الزر "Generate primes" ثم حاول أن تكتب مباشرةً شيئًا ما في مربع النص. ما ستلاحظه أن البرنامج سيفقد تجاوبه طالما أن الدالة ()generatePrimes قيد التنفيذ ولن تتمكن من الكتابة في مربع النص أو أن تنقر في أي مكان أو تفعل أي شيء. See the Pen Untitled by Hsoub Academy (@HsoubAcademy) on CodePen. يعود السبب في ذلك إلى أن برنامج جافا سكريبت ذو خيط معالجة وحيد single-threaded. وخيط المعالجة هو سلسلة من التعليمات يتبعها البرنامج. ولأن البرنامج وحيد الخيط، سينفذ التعليمات واحدة تلو اﻷخرى. لهذا استغرقت الدالة المتزامنة السابقة وقتًا حتى تعيد قيمتها وكان علينا الانتظار حتى تنتهي. وما نحتاجه عادة في برنامجنا طريقة لتنفيذ اﻵتي: البدء بتنفيذ مهمة طويلة باستدعاء دالتها. أن تكون الدالة قادرة على تنفيذ مهمتها والانتهاء مباشرة، حتى يبقى البرنامج متجاوبًا مع بقية اﻷحداث. أن تنفّذ الدالة مهامها دون أن تعيق خيط المعالجة الرئيسي، كأن تبدأ مثلًا خيط معالجة جديد. إبلاغنا بنتيجة عملياتها عندما تنتهي. هذه النقاط ما تقدمه بالفعل الدوال غير المتزامنة، وسنتابع في هذا المقال شرح كيفية إنجاز هذه الدوال في جافا سكريبت. معالجات اﻷحداث Event Handlers قد يذكرك شرحنا للداول غير المتزامنة بمعالجات اﻷحداث وستكون محقًا في ذلك. فمعالجات الأحداث هي نوع من البرمجة غير المتزامنة، إذ تزوّدنا بدالة (معالج الحدث) لن تُستدعى مباشرة بل بمجرّد وقوع الحدث الذي نترصّده. فإن كان الحدث هو إنهاء دالة غير المتزامنة لمهمتها، فقد يُستخدم حدث اﻹنهاء لتنبيه المستدعي بنتيجة استدعاء هذه الدالة. وقد استخدمت بعض الواجهات البرمجية غير المتزامنة في البدايات هذا اﻷسلوب، نذكر منها الواجهة البرمجية XMLHttpRequest التي تمكنك من إرسال طلبات HTTP إلى خادم واستقبالها باستخدام جافا سكريبت. فهذه العمليات قد تستغرق وقتًا، لهذا فهي غير المتزامنة. وتتلقى من خلال هذه الواجهة تنبيهًا عن تقدم الطلب وانتهائه عن طريق ربط مترصد أحداث Event Listener إلى الكائن XMLHttpRequest. نعرض في المثال التالي طريقة تنفيذ اﻷمر، فعندما تنقر على الزر "انقر ﻹرسال طلب Click to start request"، سننشئ كائن XMLHttpRequest جديد، ثم نترصد وقوع الحدث loadend (انتهاء التحميل). يطبع بعد ذلك معالج الحدث الرسالة "!Finished" إضافة إلى رمز الحالة Status Code. بعد إضاف معالج الحدث، نرسل الطلب. ولاحظ أنه بعد إرسال الطلب يمكننا طباعة العبارة "Started XHR request" أي يتابع برنامجنا تنفيذ تعليماته في الفترة التي يُعالج فيها الطلب، ثم يُستدعى معالج الحدث عند اكتمال الطلب. إليك شيفرة HTML: <button id="xhr">Click to start request</button> <button id="reload">Reload</button> <pre readonly class="event-log"></pre> وهذه شيفرة جافا سكريبت: const log = document.querySelector(".event-log"); document.querySelector("#xhr").addEventListener("click", () => { log.textContent = ""; const xhr = new XMLHttpRequest(); xhr.addEventListener("loadend", () => { log.textContent = `${log.textContent}Finished with status: ${xhr.status}`; }); xhr.open( "GET", "https://raw.githubusercontent.com/mdn/content/main/files/en-us/_wikihistory.json", ); xhr.send(); log.textContent = `${log.textContent}Started XHR request\n`; }); document.querySelector("#reload").addEventListener("click", () => { log.textContent = ""; document.location.reload(); }); وستكون نتيجة التنفيذ كالتالي: See the Pen frame_event_handlers by Hsoub Academy (@HsoubAcademy) on CodePen. يشبه اﻷمر ما شرحناه في مقال مدخل إلى اﻷحداث في جافا سكريبت، لكن بدل أن يكون الحدث الذي نترصده هو فعل ينفّذه المستخدم مثل النقر على زر، سيكون الحدث تغيّر حالة كائن ما. دوال رد النداء Callbacks تُعد معالجات الأحداث نوعًا خاصًا من دوال رد النداء، ودالة رد النداء هي دالة تُمرر إلى دالة أخرى، لكنها تُستدعى في الوقت المناسب. فدوال رد النداء هي الطريقة الرئيسية التي نُفّذت فيها الدوال غير المتزامنة في جافا سكريبت. لكن قد تصبح الشيفرة التي تعتمد على دوال رد النداء صعبة الفهم وخاصة عندما تستدعي تلك دوال رد النداء دوال رد النداء أخرى. ستواجه هذه الحالة كثيرًا إن احتجت لتنفيذ بعض العمليات المكونة من سلسلة من الدوال غير المتزامنة، إليك مثالًا: function doStep1(init) { return init + 1; } function doStep2(init) { return init + 2; } function doStep3(init) { return init + 3; } function doOperation() { let result = 0; result = doStep1(result); result = doStep2(result); result = doStep3(result); console.log(`result: ${result}`); } doOperation(); لدينا هنا عملية واحدة مقسّمة إلى ثلاث خطوات تتعلق كل واحدة بسابقتها. إذ تضيف الخطوة الأولى 1 إلى الدخل وتضيف الثانية العدد 2 والثالثة تُضيف العدد 3. فلو كان الدخل هو 0 ستكون النتيجة 6 (0+1+2+3). إن هذا البرنامج سهل ومباشر وفق أسلوب البرمجة المتزامنة، لكن إن نفذناه وفق أسلوب دوال رد النداء، سنجد: function doStep1(init, callback) { const result = init + 1; callback(result); } function doStep2(init, callback) { const result = init + 2; callback(result); } function doStep3(init, callback) { const result = init + 3; callback(result); } function doOperation() { doStep1(0, (result1) => { doStep2(result1, (result2) => { doStep3(result2, (result3) => { console.log(`result: ${result3}`); }); }); }); } doOperation(); ولأنه علينا استدعاء دوال رد النداء، ستكون النتيجة دالة ()doOperation متداخلة بشدة، ومن الصعب حينها قراءتها وتنقيحها. يُعرف هذا الأمر باسم "جحيم دوال رد النداء callback hell" أو "هرم الهلاك pyramid of doom" إذ تتخذ الدوال المتداخلة شكلًا هرميًا. وقد يصعب عليك جدًا التعامل مع الأخطاء إن تداخلت دوال رد النداء، إذ يتوجب عليك عندها معالجة الأخطاء في كل مستوىً من مستويات الهرم بدلًا من معالجتها مرة واحدة في قمة الهرم. لهذه اﻷسباب، لا تستخدم معظم الواجهات البرمجية غير المتزامنة دوال رد النداء حاليًا. وبدلًا من ذلك تبنت جافا سكريبت أساسًا جديدًا في البرمجة غير المتزامنة من خلال الوعد promise. وهذا ما سنتعرف عليه بالتفصيل في المقال التالي من السلسلة. ترجمة -وبتصرف- للمقال: Introducing asynchronous JavaScript اقرأ أيضًا المقال السابق: بناء تطبيق كرات مرتدة ملونة باستخدام الكائنات في جافا سكريبت -الجزء الثاني إرسال البيانات واستلامها عبر الشبكة في جافاسكربت البرمجة غير المتزامنة في جافاسكريبت طرق كتابة شيفرات غير متزامنة التنفيذ في Node.js
  11. ننهي في هذا المقال اﻷخير من سلسلتنا ما بدأناه في إنشاء واجهة برمجية REST باستخدام Express.js، ونناقش فيه موضوع السماحيات permissions والاختبارات المؤتمتة لعمل التطبيق. سماحيات المستخدم بعد التحقق من هوية مستثمر الواجهة البرمجية، علينا أن نعرف إن كان مسموحًا لهذا المستثمر الوصول إلى الموارد التي تتيحها الواجهة أم لا. ويشيع استخدام مجموعة من السماحيات لكل مستخدم، إذ يقدّم هذا اﻷسلوب طريقة أكثر مرونة من استراتيجية مستوى الوصول access level وأقل تعقيدًا. وبصرف النظر عن منطق عمل كل سماحية (تصريح)، من اﻷوضح إتباع طريقة عامة للتعامل معها. عامل مقارنة البتات AND (&) وقوة العدد 2 نستخدم العامل & المضمّن في جافا سكريبت ﻹدارة السماحيات. إذ نخزّن كل التصريحات ضمن كيان واحد ورقم واحد يخص كل مستخدم. ثم باستخدام التمثيل الثنائي (0100 للرقم 4 مثلًا) للرقم الخاص بالمستخدم والعامل & يمكننا معرفة التصريحات المعطاة له. لا تعر اهتمامًا كبيرًا بالرياضيات طالما أن الفكرة سهلة الاستخدام. نعرّف كل نوع من التصريحات (رايات سماحية permission flags) على شكل قوّة للرقم 2 أي: 2, 4, 8, 16… ، وهكذا حتى نحصل على حد أعظمي مكوّن من 31 راية. لنأخذ مثالًا عمليًا عن مدوّنة تقدم محتوى صوتي إضافة إلى النصي تُعطى فيها السماحيات التالية: 1: مؤلف ويمكنه التعديل على النص. 2: مصوّر ويمكنه التعديل على الصور. 4: معلّق ويمكنه تغيير الملف الصوتي الخاص بكل فقرة نصية. 8: مترجم يمكنه التعديل على الترجمات. وباستخدام النهج السابق يمكن إعطاء كل أنواع السماحيات للمستخدمين، وإليك أمثلة عن ذلك: المستخدم له صلاحية تعديل النص:سيُمنح الرقم 1 فقط. المستخدم له صلاحية تعديل النص والتعليق عليه: سيُمنح الرقم 5=1+4. المستخدم له صلاحية تعديل النص والصور: سيُمنح الرقم 3=2+1 المستخدم له صلاحية تعديل النص والصور والترجمة: سيُمنح الرقم 11=1+2+8 المستخدم مدير له جميع الصلاحيات الحالية والمستقبلية: سيُمنح الرقم 2,147,483,647 وهو أعلى عدد صحيح مكوّن من 32 بت. لكن كيف سيجري اﻷمر؟ لنفرض الحالة التي يحمل فيها المستخدم الرقم 12 الذي يُمثّل ثنائيًا كالتالي 00001100، ويحاول التعليق الصوتي ذو التصريح رقم 4 الذي يُمثل ثنائيًا كالتالي 00000100. نستخدم اﻵن العملية & على الرقمين السابقين كالتالي: 00001100 & 00000100 ستكون النتيجة 00000100 وذلك بمقارنة قيمة كل خانة من الأول مه مقابلتها في الثاني ووضع النتيجة 1 إذا حملت الخانتين القيمة 1 وصفر فيما عدا ذلك. إن النتيجة هي الرقم 4 وبالتالي يُسمح له بالتعليق الصوتي. نطبق نفس الخوارزمية على رقم المستخدم والصلاحية التي يريد استخدامها فإن كانت نتيجة العملية & هي 0 يُمنع من استخدام الصلاحية وإلا سيُسمح له باستخدامها (جرّب أرقامًا أخرى!). كتابة شيفرة رايات السماحيات نخزن رايات السماحيات ضمن المجلد common لأن منطق العملية قد يتكرر في وحدات أخرى مستقبلًا، ثم ننشئ الملف common/middleware/common.permissionflag.enum.ts ليحمل قيم بعض الرايات: export enum PermissionFlag { FREE_PERMISSION = 1, PAID_PERMISSION = 2, ANOTHER_PAID_PERMISSION = 4, ADMIN_PERMISSION = 8, ALL_PERMISSIONS = 2147483647, } ملاحظة: طالما أن التطبيق هو مجرد مثال، اخترنا أن تكون أسماء الصلاحيات معممة. لنعد اﻵن إلى الدالة ()addUser ضمن كائن DAO لاستبدال الرقم 1 بالقيمة PermissionFlag.FREE_PERMISSION بعد إدراج الملف السابق في مكان مناسب. كما يمكن إدراج هذا الملف ضمن ملف جديد لوحدة وسيطة تضم صنف متفرّد يُدعى PermissionFlag.FREE_PERMISSION: import express from 'express'; import { PermissionFlag } from './common.permissionflag.enum'; import debug from 'debug'; const log: debug.IDebugger = debug('app:common-permission-middleware'); وبدلًا من إنشاء عدة دوال وسيطة مشابهة، نستخدم نمط المصنع factory pattern ﻹنشاء مصنع للتوابع أو الدوال (أو ببساطة مصنع). تسمح لنا دالة المصنع توليد دوال وسيطة -عند تهيئة المسار- تتحقق من راية أية سماحية مطلوبة. وهكذا نتفادى تكرار الدوال الوسيطة عندما نضيف راية جديدة. إليك شيفرة المصنع الذي يوّلد الدوال الوسيطة التي تتحقق من رايات السماحيات: permissionFlagRequired(requiredPermissionFlag: PermissionFlag) { return ( req: express.Request, res: express.Response, next: express.NextFunction ) => { try { const userPermissionFlags = parseInt( res.locals.jwt.permissionFlags ); if (userPermissionFlags & requiredPermissionFlag) { next(); } else { res.status(403).send(); } } catch (e) { log(e); } }; } ويسمح لمستخدم الدخول إلى حساب معين إذا كان فقط حسابه الشخصي أو كان مديرًا: async onlySameUserOrAdminCanDoThisAction( req: express.Request, res: express.Response, next: express.NextFunction ) { const userPermissionFlags = parseInt(res.locals.jwt.permissionFlags); if ( req.params && req.params.userId && req.params.userId === res.locals.jwt.userId ) { return next(); } else { if (userPermissionFlags & PermissionFlag.ADMIN_PERMISSION) { return next(); } else { return res.status(403).send(); } } } نضيف اﻵن دالة وسيطة جديدة إلى الملف users.middleware.ts: async userCantChangePermission( req: express.Request, res: express.Response, next: express.NextFunction ) { if ( 'permissionFlags' in req.body && req.body.permissionFlags !== res.locals.user.permissionFlags ) { res.status(400).send({ errors: ['User cannot change permission flags'], }); } else { next(); } } وطالما أن الدالة السابقة تعتمد على القيمة res.locals.user، باﻹمكان نشرها ضمن الدالة ()validateUserExists قبل استدعاء ()next: // ... if (user) { res.locals.user = user; next(); } else { // ... إن ما فعلناه ضمن الدالة ()validateUserExists ينفي ضرورة تنفيذه ضمن الدالة ()validateSameEmailBelongToSameUser، لهذا يمكن حذف استدعاء قاعدة البيانات فيه واستبداله بالقيمة التي يمكن نضمّن أنها مخزّنة في res.locals: - const user = await userService.getUserByEmail(req.body.email); - if (user && user.id === req.params.userId) { + if (res.locals.user._id === req.params.userId) { نستطيع اﻵن ربط منطق تحديد السماحيات بالملف users.routes.config.ts. طلب التصريحات ندرج بداية اﻷداة الوسيطة الجديدة والملف enum: import jwtMiddleware from '../auth/middleware/jwt.middleware'; import permissionMiddleware from '../common/middleware/common.permission.middleware'; import { PermissionFlag } from '../common/middleware/common.permissionflag.enum'; ما نريده ألا تُعرض قائمة المستخدمين إلا لمستخدم ذو صلاحيات مدير، لكننا نريد أيضًا أن يكون إنشاء مستخدم جديد متاحًا للعموم، كما تجري اﻷمور اعتياديًا. دعونا بداية نحدد سماحيات الوصول إلى قائمة المستخدمين عن طريق دالة المصنع قبل المتحكم: this.app .route(`/users`) .get( jwtMiddleware.validJWTNeeded, permissionMiddleware.permissionFlagRequired( PermissionFlag.ADMIN_PERMISSION ), UsersController.listUsers ) // ... وتذكّر أن استدعاء دالة المصنع يُعيد دالة وسيطة، وبالتالي تُعيد مرجعًا إلى الدوال الوسيطة اﻷخرى التي لا تنتج عنها دون أن تُنفّذ. من القيود اﻷخرى هو منع الوصول إلى المسارات التي تضم معرّف المستخدم userId، ما لم يكن صاحب التصريح هو صاحب المعرّف أو المدير: .route(`/users/:userId`) - .all(UsersMiddleware.validateUserExists) + .all( + UsersMiddleware.validateUserExists, + jwtMiddleware.validJWTNeeded, + permissionMiddleware.onlySameUserOrAdminCanDoThisAction + ) .get(UsersController.getUserById) وعلينا أيضًا منع المستخدمين من رفع مستوى سماحياتهم عن طريق إضافة القيمة UsersMiddleware.userCantChangePermission قبل المرجع إلى الدالة في نهاية الوجهات التي تقود إلى العمليتين PUT و PATCH. وسندعم أيضًا في الواجهة البرمجية منطق السماح للمستخدمين الذين يمتلكون التصريح PAID_PERMISSION فقط في تحديث بياناتهم، وقد لا يلائم هذه اﻷمر مشاريع أخرى، لكننا سننفذه هنا كي نميز بين السماحيات المدفوعة والمجانية. باﻹمكان تنفيذ اﻷمر بإضافة دالة مولّدة أخرى بعد كل مرجع أضفناه إلى التصريح userCantChangePermission: permissionMiddleware.permissionFlagRequired( PermissionFlag.PAID_PERMISSION ), وهكذا نكون مستعدين ﻹعادة تشغيل Node.js والتجريب. الاختبار اليدوي للتصريحات لنحاول بداية أن نحصل على قائمة المستخدمين دون مفتاح: curl --include --request GET 'localhost:3000/users' \ --header 'Content-Type: application/json' ستكون الاستجابة HTTP 401 لاننا بحاجة إلى مفتاح JWT صالح، لهذا سنعيد المحاولة باستخدام مفتاح الوصول الذي حصلنا عليه في مقالنا السابق: curl --include --request GET 'localhost:3000/users' \ --header 'Content-Type: application/json' \ --header "Authorization: Bearer $REST_API_EXAMPLE_ACCESS" وسنحصل هذه المرة أيضًا على الاستجابة HTTP 403 لأن المفتاح صالح لكن الوصول إلى نقطة الوصول هذه مرفوض لعدم امتلاك الطلب التصريح ADMIN_PERMISSION. ,وبالطبع لن نحتاج إلى تصريح كهذا للحصول على سجل المستخدم نفسه: curl --request GET "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --header "Authorization: Bearer $REST_API_EXAMPLE_ACCESS" وستكون الاستجابة كالتالي: { "_id": "UdgsQ0X1w", "email": "marcos.henrique@toptal.com", "permissionFlags": 1, "__v": 0 } وبالمقابل، يُفترض أن يفشل طلب تحديث سجلات المستخدم نفسه لأن قيمة السماحية التي يمتلكها هي 1 (مجاني FREE_PERMISSION? curl --include --request PATCH "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --header "Authorization: Bearer $REST_API_EXAMPLE_ACCESS" \ --data-raw '{ "firstName": "Marcos" }' والاستجابة كما نتوقع هي : HTTP 403. إن متطلبات استعلام الوجهة auth/ من خلال الطلب POST يُظهر سيناريو أمني لا بد من الانتباه إليه. فعندما يغير مالك الموقع سماحيات مستخدم، كأن يحاول إلغاء مستخدم يسئ السلوك، فلن يشعر المستخدم بذلك حتى يحصل تحديث على مفتاح JWT الذي يمتلكه، لأن التحقق من السماحيات يجري على بيانات مفتاح JWT لتقليل الوصول إلى قاعدة البيانات. تساعدك خدمات مثل Auth0 بتقديم تدوير تلقائي للمفاتيح، لكن مع ذلك، سيلاحظ المستخدمين سلوكًا غريبًا في الفترة التي تفصل بين عمليات التدوير، ويدوم لفترة قصيرة عادة. ولتخفيف هذه الظاهرة، على المطور أن ينتبه إلى ضرورة تنفيذ آلية تحديث المفاتيح كاستجابة لتغيّر السماحيات. ويجدر بمطوري الواجهات البرمجية REST الانتباه إلى الثغرات بتنفيذ الكثير من عمليات CRUD وبشكل دوري، لكن سرعان ما يصبح هذا الأمر مرهقًا ومنبعًا لمشاكل جديدة. الاختبارات المؤتمتة مع توسع وزيادة حجم وإمكانيات الواجهة البرمجية، سيصعب صيانتها والمحافظة على جودتها وخصوصًا مع التغيّر المستمر لمنطق العمل. ولتقليل الثغرات في الواجهة البرمجية بقدر المستطاع ونشر التغييرات الجديدة بثقة، يشيع استخدام أطقم برمجية لاختبار الواجهتين الأمامية والخلفية للتطبيقات. وبدلًا من الغوص في تفاصيل كتابة اختبارات وشيفرة اختبارات، سنعرض آليات أساسية تقدّم مجموعة اختبارت يمكن لنا البناء عليها. التعامل مع البيانات الناتجة عن الاختبارات قبل أن نؤتمت الاختبارات، من الجيد التفكير بما سيحدث للبيانات الناتجة عن الاختبارات. إذ نستخدم في تطبيقنا Docker Compose لتشغيل قاعدة البيانات المحلية، وتؤثر الاختبارات على قاعدة البيانات بترك مجموعة جديدة من السجلات بعد كل اختبار. لا يمثل هذا اﻷمر مشكلة في أغلب اﻷوقات، لكن إن رأيت أنه كذلك، لا بأس بتعديل الملف docker-compose.yml ﻹنشاء قاعدة بيانات جديدة لأغراض الاختبار. عادة ما يُجري المطورين اختباراتهم المؤتمتة في الواقع كجزء من خط التسليم المستمر. ولتنفيذ اﻷمر، من المنطقي إعداد طريقة ﻹنشاء قاعدة بيانات مؤقتة لتنفيذ كل اختبار. لهذا نستخدم Mocha و Chai و SuperTest لإنشاء اختباراتنا: npm i --save-dev chai mocha supertest @types/chai @types/express @types/mocha @types/supertest ts-node يدير Mocha تطبيقنا ويًجري الاختبارات، بينما يسهّل Chai قراءة تعابير الاختبارات، ويسهّل SuperTest اختبارات التكامل بين الواجهتين أو اختبارات طرف إلى طرف end-to-end بين الواجهة البرمجية RESTوعميل ريست. علينا بداية تعديل السكريبت package.json: "scripts": { // ... "test": "mocha -r ts-node/register 'test/**/*.test.ts' --unhandled-rejections=strict", "test-debug": "export DEBUG=* && npm test" }, تسمح لنا الشيفرة السابقة بتنفيذ الاختبارات في مجلد جديد ندعوه test. اختبار وصفي لتجربة هيكلية الاختبار، سننشئ الملف test/app.test.ts: import { expect } from 'chai'; describe('Index Test', function () { it('should always pass', function () { expect(true).to.equal(true); }); }); قد تبدو الصياغة غير مألوفة لكنها صحيحة. إذ نعرف الاختبار بتوقع سلوك معين ()expect ضمن الكتلة ()it التي تُستدعى بدورها ضمن الكتلة ()describe. ننفذ التعليمة التالية في الطرفية: npm run test ومن المفترض أن نرى النتيجة التالية: > mocha -r ts-node/register 'test/**/*.test.ts' --unhandled-rejections=strict Index Test ✓ should always pass 1 passing (6ms) وهكذا تكون مكتبات الاختبار جاهزة للعمل. تحضير الاختبارات للعمل ليبقى خرج الاختبارات واضحًا، علينا إيقاف عمل المكتبة winston التي تسجّل الطلبات كليًا خلال تنفيذ الاختبارات، لهذا غيّر الفرع else (ليس في وضع التنقيح طبعًا) في الملف app.ts للتحقق من وجود الدالة ()it من Mocha: if (!process.env.DEBUG) { loggerOptions.meta = false; // عندما لا نكون في وضع التنقيح + if (typeof global.it === 'function') { + loggerOptions.level = 'http'; // for non-debug test runs, squelch entirely + } } علينا أخيرًا تصدير الملف app.ts ليكون عرضة للاختبارات، لهذا سنضيف السطر export default في نهاية الملف app.ts قبل السطر ()server.listen مباشرة، لأن ()listen يعيد الكائن http.Server. تحقق من أن كل شيئ على ما يرام بتنفيذ اﻷمر: npm run test. الاختبار المؤتمت اﻷول على واجهتنا البرمجية حتى نبدأ إعداد الاختبارات على المستخدمين، سننشئ الملف test/users/users.test.ts ونبدؤه بإدراج المكتبات واﻹعتماديات اللازمة: import app from '../../app'; import supertest from 'supertest'; import { expect } from 'chai'; import shortid from 'shortid'; import mongoose from 'mongoose'; let firstUserIdTest = ''; // سيضم لاحقًا قيمة تعيدها الواجهة البرمجية const firstUserBody = { email: `marcos.henrique+${shortid.generate()}@toptal.com`, password: 'Sup3rSecret!23', };`()server.listen``()server.listen` let accessToken = ''; let refreshToken = ''; const newFirstName = 'Jose'; const newFirstName2 = 'Paulo'; const newLastName2 = 'Faraco'; ننشئ تاليًا الكتلة الخارجية ()describe مع بعض اﻹعدادات والتعريفات: describe('users and auth endpoints', function () { let request: supertest.SuperAgentTest; before(function () { request = supertest.agent(app); }); after(function (done) { //وأغلق الاتصال Express.js أغلق خادم shut down the Express.js //بأننا انتهينا Mocha ثم أخبر MongoDB مع app.close(() => { mongoose.connection.close(done); }); });will later hold a value returned by our API }); تُستدعى الدوال التي نمررها إلى ()before و ()after قبل وبعد كل الاختبارات التي نعرّفها عند استدعاء ()it ضمن نفس الكتلة ()describe. وللدالة التي نمررها إلى ()after دالة أخرى done نستدعيها بمجرّد انتهينا من إعادة التطبيق واتصاله مع قاعدة البيانات إلى الوضع الأساسي. ملاحظة: لو لم نستخدم الدالة ()after، ستتوقف Mocha عن الاستجابة بعد إكمال كل اختبار بنجاح. وننصح هنا باستدعاء Mocha دومًا مع الوسيط exit-- لتفادي هذا السلوك مع أنه ينطوي على ثغرة. فلو توقف الاختبار عن الاستجابة لأسباب أخرى، نتيجة وعد لم يُكتب بطريقة صحيحة في الاختبار أو التطبيق مثلًا، عندها لن تنتظر Mocha وتعطي رسالة بنجاح الاختبار حتى لو حدث خطأ في التطبيق مما يزيد تعقيد عملية تنقيح اﻷخطاء أصبحنا اﻵن جاهزين ﻹضافة اختبارات فردية طرف-إلى-طرف ضمن الكتلة ()describe: it('should allow a POST to /users', async function () { const res = await request.post('/users').send(firstUserBody); expect(res.status).to.equal(201); expect(res.body).not.to.be.empty; expect(res.body).to.be.an('object'); expect(res.body.id).to.be.a('string'); firstUserIdTest = res.body.id; }); تُنشئ هذه الدالة مستخدمًا جديدًا وفريدًا لأن البريد اﻹلكتروني للمستخدم قد ولد سابقًا باستخدام shortid. بضم المتغير request عميل SuperTest الذي يسمح لنا تنفيذ طلبات HTTP إلى الواجهة البرمجية. وعلى جميع الطلبات أن تستخدم await لهذا تكون جميع الدوال التي نمررها إلى ()it غير متزامنة async. ثم نستخدم بعد ذلك ()expect من Chai لاختبار مختلف نواحي النتيجة. جرب تنفذ اﻷمر اﻵن لترى كيف يعمل الاختبار. سلسلة من الاختبارات سنضيف جميع كتل ()it التالية ضمن الكتلة ()describe، ولا بد من إضافتها وفق الترتيب المعروض تاليًا كي تعمل جميعها مع المتغيّرات التي نغيّرها مثل firstUserIdTest: it('should allow a POST to /auth', async function () { const res = await request.post('/auth').send(firstUserBody); expect(res.status).to.equal(201); expect(res.body).not.to.be.empty; expect(res.body).to.be.an('object'); expect(res.body.accessToken).to.be.a('string'); accessToken = res.body.accessToken; refreshToken = res.body.refreshToken; }); نحضر هنا مفتاحي الوصول والتحديث الجديدين من المستخدم الذي أضيف مؤخرًا: it('should allow a GET from /users/:userId with an access token', async function () { const res = await request .get(`/users/${firstUserIdTest}`) .set({ Authorization: `Bearer ${accessToken}` }) .send(); expect(res.status).to.equal(200); expect(res.body).not.to.be.empty; expect(res.body).to.be.an('object'); expect(res.body._id).to.be.a('string'); expect(res.body._id).to.equal(firstUserIdTest); expect(res.body.email).to.equal(firstUserBody.email); }); يدفع ذلك الطلب GET من المسار userID: (والذي يحمل مفتاح التشفير) إلى التحقق من أن بيانات المستخدم في الاستجابة تتطابق مع ما أرسلناه أساسًا. الاختبارات المتداخلة وتجاوز الاختبارات وعزلها يمكن للكتل ()itفي Mocha أن تضم كتل ()describe، أي يمكن أن تتداخل الاختبارات، وهذا ما سنفعله في اختبارنا التالي. سيجعل ذلك الاعتماديات المتعاقبة أوضح في نواتج الاختبار كما سنراها في النهاية: describe('with a valid access token', function () { it('should allow a GET from /users', async function () { const res = await request .get(`/users`) .set({ Authorization: `Bearer ${accessToken}` }) .send(); expect(res.status).to.equal(403); }); }); لا تغطي الاختبارات الفعالة ما نتوقعه نجاحه فقط، بل ما نتوقعه أن يفشل أيضًا. إذ حاولنا هنا الحصول على قائمة بأسماء المستخدمين وتوقعنا أن تكون النتيجة الخطأ 403 لأن المستخدم الجديد سيحمل التصريح الافتراضي ولا يسمح له بمثل هذا الطلب إلى نقطة الوصول. وهكذا يمكننا الاستمرار في كتابة اختبارات ضمن الكتلة ()describe الجديدة. وطالما أننا ناقشنا الميزات التي استخدمناها في بقية شيفرة الاختبار، يمكنك الاطلاع عليها في المستودع الخاص بالتطبيق. مر مع تزوّدنا المكتبة Mocha بميزات عدة قد تجدها مفيدة أثناء تطوير وتنقيح الاختبارات منها: التابع ()skip:ويستخدم لتفادي اختبار وحيد أو كتلة كاملة من الاختبارات. فعندما نستبدل ()it بالدالة ()it.skip (وكذلك الأمر مع ()describe)، لن يعمل الاختبار أو الاختبارات محط الاهتمام بل ستؤجل ويشار إليها بالعبارة "بانتظار التنفيذ pending" في الخرج الأخير للمكتبة Mocha. التابع ()only: يُستخدم لتجاهل جميع الاختبارات غير الموسومة بالوسم only.، ولن يظهر أي شيء في الخرج على أنه "بانتظار التنفيذ". المعامل bail--: باﻹمكان استخدام هذا المعامل ضمن سطر تعريف Mocha في الملف package.json ﻹيقاف الاختبارات بمجرد فشل إحداها. ولهذا اﻷمر فائدة خاصة في تطبيق الواجهة البرمجية الذي نبنيه، لأننا أعددنا الاختبارات لتكون متعاقبة، وبالتالي عند فشل أول اختبار ستتوقف Mocha مباشرة بدلًا من المتابعة واﻹشارة إلى أخطاء في بقية الاختبارات المتعلقة بالاختبار الفاشل علمًا بأنها قد تكون ناجحة لكنها فشلت نتيجة لفشل أول اختبار. عند تنفيذ مجموعة اختباراتنا اﻵن باستخدام سطر اﻷوامر npm run test، سنلاحظ إخفاق ثلاث اختبارات (لو أردنا ترك الدوال الثلاث التي تعتمد على شيفرة غير منجزة حتى اللحظة، فمن المناسب جدًا لتطبيق التابع ()skip عليها). أخفقت الاختبارات الثلاث السابقة نظرًا لوجود قطعتين مفقودتين من شيفرة التطبيق حتى اللحظة، اﻷولى موجودة ضمن الملف npm run test: this.app.put(`/users/:userId/permissionFlags/:permissionFlags`, [ jwtMiddleware.validJWTNeeded, permissionMiddleware.onlySameUserOrAdminCanDoThisAction, //من الضروري وجود كتلتي الشيفرة السابقتين كجزء من الأداة الوسيطة //لأنها تغطي فقط .all() على الرغم من وجود مرجع إليهما ضمن الدوال // /users/:userId, وليس كل شيء تختها في الشجرة permissionMiddleware.permissionFlagRequired( PermissionFlag.FREE_PERMISSION ), UsersController.updatePermissionFlags, ]); وعلينا أن نعدّل ثانيًا الملف users.controller.ts لأننا وضعنا مرجعًا إلى دالة غير موجودة فيه. لهذا علينا إضافة السطر ب;import { PatchUserDto } from '../dto/patch.user.dto' بالقرب من أعلى الصفحة وإضافة الدالة المفقودة إلى الصنف: async updatePermissionFlags(req: express.Request, res: express.Response) { const patchUserDto: PatchUserDto = { permissionFlags: parseInt(req.params.permissionFlags), }; log(await usersService.patchById(req.body.id, patchUserDto)); res.status(204).send(); } إن إضافة هذه اﻹمكانيات مفيد في حالات الاختبار لكنه لن يناسب معظم الاحتياجات في التطبيقات الحقيقية، لهذا نترك لك التمرينين التاليين للعمل عليهما: فكر بطرق تمنع فيها الشيفرة مجددًا المستخدم من تغيير رايات السماحية permissionFlags الخاصة به وتسمح مع ذلك باختبار نقاط الوصول المقيّدة بالتصريحات. أنشئ ونفّذ منطق عمل (بما في ذلك الاختبارات المتعلقة به) يعالج تغيير الماحيات permissionFlags من خلال الواجهة البرمجية (انتبه لمعضلة الدجاجة والبيضة هنا: فكيف يمكن لشخص معين الحصول على تصريح لتغيير التصريحات؟) نفّذ اﻵن اﻷمر npm run test ومن المفترض أن تجري الاختبارات بنجاح ويكون الخرج منسقًا بالشكل التالي: Index Test ✓ should always pass users and auth endpoints ✓ should allow a POST to /users (76ms) ✓ should allow a POST to /auth ✓ should allow a GET from /users/:userId with an access token with a valid access token ✓ should allow a GET from /users ✓ should disallow a PATCH to /users/:userId ✓ should disallow a PUT to /users/:userId with an nonexistent ID ✓ should disallow a PUT to /users/:userId trying to change the permission flags ✓ should allow a PUT to /users/:userId/permissionFlags/2 for testing with a new permission level ✓ should allow a POST to /auth/refresh-token ✓ should allow a PUT to /users/:userId to change first and last names ✓ should allow a GET from /users/:userId and should have a new full name ✓ should allow a DELETE from /users/:userId 13 passing (231ms) وهكذا نكون قد أنجزنا أسلوبًا سريعًا في التحقق من عمل الواجهة البرمجية بالطريقة التي نتوقعها. تنقيح التطبيق من خلال الاختبارات يمكن للمطورين الذين يعانون من إخفاقات غير متوقعة لشيفرتهم تعزيز قدرة شيفرات التنقيح لكل من winstonو Node.js عند تنفيذ سلسلة الاختبارات. فمن السهل مثلًا التركيز على استعلامات Mongoose التي تُنفّذ باستخدام اﻷمر DEBUG=mquery npm run test (لاحظ عدم وجود البادئة export والعامل && في الوسط مما يجعل بيئة العمل تستمر لتنفيذ أوامر أخرى). من الممكن أيضًا عرض خرج جميع عمليات التنقيح باستخدام npm run test-debug ويساعدنا في ذلك طريقة اﻹعداد التي اتبعناها في الملف package.json وبهذا نكون قد بنينا واجهة برمجية REST قابلة للتوسع ومدعومة بقاعدة بيانات MongoDB وسلسلة من الاختبارات المناسبة، لكن هناك بعض النقص. اﻷمان في تطبيقات Express.js ينبغي دائمًا الاطلاع على توثيق ي عند العمل مع التطبيقات المبنية عليها وكحد أدنى ما يتعلق بأفضل ممارسات اﻷمان: دعم إعدادات TLS. إضافة أداة وسيطة للتحكم بمعدّل الطلبات إلى الواجهة البرمجية مثل express-limit-rate التأكد من أمان اعتمديات npm (من الممكن تنفيذ البرنامج باستخدام npm audit، أو استخدام snyk). استخدام المكتبة Helmet للمساعدة في حماية التطبيق من نقاط الضعف الشائعة وهذا ما سنضيفه مباشرة إلى تطبيقنا: npm i --save helmet ومن ثم ندرجه ضمن الملف app.ts ونضيف الاستدعاء ()app.use: import helmet from 'helmet'; // ... app.use(helmet()); ولا يعني استخدام Helmet أنك بأمان، فكل خطوة ولو كانت بسيطة لتأمين التطبيق لها فائدتها. احتواء الواجهة البرمجية ضمن Docker لم نخض في تفاصيل Docker كثيرًا في سلسلة مقالاتنا على الرغم من استخدامه لاحتواء قاعدة البيانات MongoDB. فإن كنت تريد التقدم خطوة أبعد في العمل مع Docker، أنشئ ملفًا بالاسم Dockerfile ودون لاحقة في جذر المشروع وضمّنه الشيفرة التالية: FROM node:14-slim RUN mkdir -p /usr/src/app WORKDIR /usr/src/app COPY . . RUN npm install EXPOSE 3000 CMD ["node", "./dist/app.js"] تبدأ هذه اﻹعدادات بالسطر node:14-slim وهي النسخة الرسمية من Docker، ثم تشغّل الواجهة البرمجية ضمن الحاوية. يمكن أن تتغير هذه اﻹعدادات من حالة إلى أخرى، لكن هذه اﻹعدادات التي تبدو عامة مناسبة لمشروعنا. ولبناء هذه النسخة نفّذ الأوامر التالية في جذر المشروع مستبدلًا tag_your_image_here بما يناسب): docker build . -t tag_your_image_here ثم نفّذ ما يلي لتشغيل الواجهة ضمن الحاوية: docker run -p 3000:3000 tag_your_image_here يمكن اﻵن لقاعدة البيانات MongoDB و Node.js استخدام Docker، لكن علينا تشغيلهما بطريقتين مختلفتين. ونترك لك البحث عن كيفية إضافة تطبيق Node.js اﻷساسي إلى الملف docker-compose.yml كي يعمل التطبيق ككل من خلال أمر docker-compose واحد. خلاصة: استكشاف مهارات أخرى في بناء واجهات برمجية REST أجرينا في هذا المقال تحسينات واسعة على الواجهة البرمجية REST التي بدأنا العمل عليها في سلسلة مقالاتنا، إذ أضفنا طريقة للاستيثاق باستخدام مفاتيح JWT وبنينا نظام سماحيات مرن، وكتبنا مجموعة من الاختبارات المؤتمتة. يُعد ما فعلناه نقطة انطلاق قوية لمطوري الواجهة الخلفية الجدد والمتقدمين. ومع ذلك، قد لا يكون مشروعنا مثاليًا في بعض جوانبه وخاصة عند إنجاز نسخة إنتاج أو محاولة توسيعه. وإضافة إلى التمارين التي أو صينا بحلها خلال سلسلة المقالات السابقة، ما الذي يمكن تعلمه أيضًا؟ نوصي على مستوى بناء الواجهات البرمجية بالاطلاع على مواصفات بناء واجهات متوافقة مع OpenAPI. وننصح أيضًا المهتمين بتطوير المشاريع، تجربة إطار العمل NestJS المبني على أساس Express.js، فهو أكثر قوة وتجريدًا. لهذا، من الجيد العمل على مشروعنا كي تعتاد العمل مع أساسيات Express.js بداية. ولا ننسى بالطبع إطار العمل GraphQL الذي لا يقل أهمية عنه، إذ يلقى رواجًا وانتشارًا كبديل عن الواجهات البرمجية REST. أما فيما يخص موضوع الاستيثاق والسماحيات،فقد أنشأنا أسلوبًا يعتمد على رايات تُختبر بالصيغة الثنائية ومولد دوال وسيطة لتحديد تلك الرايات يدويًا. ولكي تشعر بثقة أكبر إن قررت زيادة حجم تطبيقاتك، يجدر بك النظر في مكتبة CASL التي تتكامل مع Mongoose. إذ تزيد هذه المكتبة مرونة النهج الذي اتبعناه ويسمح بتعريفات موجزة للإمكانات التي يجب أن يسمح بها تصريح معين مثل: can(['update', 'delete'], '(model name here)', { creator: 'me' }); بدلاً من الدوال الوسيطة المخصصة لهذا اﻷمر. كما قدمنا نقطة انطلاق في عملية الاختبار المؤتمت، ولكن بعض الموضوعات المهمة كانت خارج نطاقنا سلسلة مقالاتنا، لهذا نوصيك أن: تستكشف اختبار الوحدة Unit test كي تختبر كل وحدة برمجية على حدى. يمكن استخدام Mocha و Chai لهذا الغرض أيضًا. تلق نظرة على أدوات تغطية التعليمات البرمجية، والتي تساعد في تحديد الثغرات في مجموعة الاختبارات من خلال عرض أسطر الأوامر التي لا تُنفَّذ أثناء الاختبار. تُمكّنك هذه اﻷدوات من استكمال الاختبارات في تطبيقنا، بما يناسبك، لكنها قد لا تكشف عن كل الحالات التي تسبب المشاكل، مثل الحالة التي تدرس إمكانية تعديل المستخدم لسماحياته من خلال طلب PATCH إلى الوجهة users /:userId/. تجرب أساليب أخرى للاختبارات المؤتمتة. لقد استخدمنا مبدأ التطوير القائم على السلوك (BDD) من خلال الدالة ()expect من CHAI، لكن المكتبة تدعم أيضًا الدوال ()should و ()assert. ومن الجيد أيضًا تعلم مكتبات اختبار أخرى، مثل JEST. بصرف النظر عن هذه الموضوعات، فإن الواجهة البرمجية REST التي بنيناها اعتمادًا على Node.js/TypeScript قابلة للتوسع. فقد ترغب في في تنفيذ المزيد من البرامج الوسيطة لفرض منطق عمل مشترك لاستغلال مورد المستخدمين القياسي. لن نتعمق في ذلك هنا، لكنني سنكون سعداء بتقديم التوجيه والنصائح من خلال نقاش الصفحة. للاطلاع على المشروع بصيغته النهائية راجع المستودع المخصص على جيتهب أو حمله من هنا مباشرة.rest-series.rar ترجمة -وبتصرف للقسم الثاني من مقال Building a Node.js TypeScript REST API, Part 3 MongoDB, Authentication, and Automated Tests اقرأ أيضًا المقال السابق: بناء واجهة برمجية متوافقة مع REST في بيئة Node.js القسم الثالث: قاعدة البيانات MongoDB والاستيثاق مدخل إلى إطار عمل الويب Express وبيئة Node الاستيثاق عبر مفتاح المستخدم المشفر (token authentication) في تطبيق Node.js و React اعتبارات نشر مشاريع Node.js وExpress على الويب
  12. نكمل في هذا المقال تطوير تطبيق الواجهة البرمجية REST الذي بدأناه في مقال سابق، حيث نضيف للتطبيق المميزات التالية: Mongoose التي تسمح لنا بالعمل مع قاعدة البيانات MongoDB بدلًا من كائن DAO المقيم في الذاكرة. استيثاق Authentication ﻹضافة إمكانيات منح اﻷذونات كي يتمكن مستثمرو الواجهة البرمجية من استخدام مفتاح تشفير ويب JSON Web Token للوصول بأمان إلى نقاط وصول الواجهة البرمجية. اختبارات مؤتمتة: باستخدام إطار عمل الاختبارات Mocha، والمكتبة Chai والوحدة البرمجية SuperTest، للمساعدة في التحقق من المحدودية عند زيادة حجم الشيفرة وتغيّرها. كما سنضيف تباعًا مكتبات للتحقق من صحة المدخلات ومكتبات خاصة بآمان التطبيق، ,وسنكتسب بعض الخبرة في التعامل مع مدير الحاويات Docker. تثبيت MongoDB على هيئة حاوية لنبدأ باستبدال قاعدة البيانات المقيمة في الذاكرة التي استخدمناها مسبقا بقاعدة بيانات حقيقة هي MongoDB. باﻹمكان طبعًا تثبيت القاعدة محليًا على جهازك، لكن ونظرًا للاختلافات بين بيئات التشغيل (إصدارات مختلفة لنظام التشغيل مثلًا) والتي قد تسبب بعض المشكلات، سنستخدم مدير الحاويات Docker وهو أداة معيارية حاليًا في صناعة البرمجيات. وكل ما عليك فعله هو تثبيت Docker ثم Docker Compose، وتأكد بعدها من التثبيت بتنفيذ اﻷمر docker -v على الطرفية كي ترى نسخة Docker Composer التي ثُبتت على جهازك. لتشغيل MongoDBضمن جذر المشروع، سننشئ أولًا ملف YAML يُدعى docker.compose.yml يضم الشيفرة التالية: version: '3' services: mongo: image: mongo volumes: - ./data:/data/db ports: - "27017:27017" يتيح Docker Composer تشغيل عدة حاويات في نفس الوقت باستخدام ملف تهيئة واحد. لهذا سنستخدمه اﻵن لتشغيل MongoDB دون أن نثبتها على الجهاز: sudo docker-compose up -d يشغّل اﻷمر up الحاوية المحددة، مصغيًا إلى منفذ MongoDB27017، بينما تفصل d- اﻷمر عن الطرفية ليعمل التطبيق مستقلًا، وستكون النتيجة مشابهة لما يلي إذا جرى كل شيء دون مشاكل: Creating network "toptal-rest-series_default" with the default driver Creating toptal-rest-series_mongo_1 ... done تنشئ التعليمة السابقة أيضًا الملجد في جذر المشروع، لهذا عليك إضافة سطر بالمجلد data في الملف الذي ينتهي باللاحقة gitignore. وعندما نريد إيقاف الحاوية التي تحتوي MongoDB، علينا فقط تنفيذ الأمر sudo docker -compose down: Stopping toptal-rest-series_mongo_1 ... done Removing toptal-rest-series_mongo_1 ... done Removing network toptal-rest-series_default هذا كل ما تحتاج معرفته لتشغيل MongoDB مع الواجهة البرمجية REST. تأكد اﻵن من تنفيذ اﻷمر sudo docker-compose up -d حتى تكون MongoDB جاهزة للاستخدام مع التطبيق. استخدام Mongoose للوصول إلى MongoDB نستخدم مكتبة نمذجة البيانات Mongoose في الاتصال مع قاعدة البيانات MongoDB. وعلى الرغم من سهول استخدام Mongoose، سيفيدك الاطلاع على توثيقها لتعلّم كل اﻹمكانيات التي تقدمها للتطبيقات الفعلية. استخدم سطر اﻷوامر التالي لتثبيت Mongoose: npm i mongoose لنبدأ بإعداد خدمة Mongoose ﻹدارة الاتصال مع قاعدة البيانات MongoDB، وطالما أن عدة موارد تتشارك هذه الخدمة، سنضيفها إلى الملجد common مباشرة. نستخدم أيضًا الكائن mongooseOptions لتخصيص خيارات Mongoose التالية (على الرغم من أن استخدامه ليس إلزاميًا): useNeewUrlPrser: إن لم يُضبط هذا الخيار على القيمة true ستعرض Mongoose تحذير بأن هذه الخاصية عرضة للإهمال في النسخ المستقبلية depreciation warning. useUnified Topology: يوصي توثيق Mongoose بضبط قيمة هذا الخيار على true، وذلك لاستخدام محرك إدارة اتصال أحدث. serverSelectionTimeoutMS: لغايات تصميمية تتعلق بتطبيقنا، وفي حال نسيت تشغيل MongoDB قبل Node.js، سيكون اختيارك مدة أقل من المدة الافتراضية 30 ثانية سبيلًا لعرض معلومات عن MongoDB مباشرةً بانتظار استجابة الواجهة الخلفية. useFindAndModify: تلغي القيمة false لهذا الخيار عرض التحذير deprecation warning، وقد أشار توثيق Mongoose إلى التحذير الناتج عن هذا الخيار من بين جميع الخيارات اﻷخرى. إذ يدفع هذا الخيار Mongoose لاستخدام ميزات أصلية أحدث لقاعدة البيانات MongoDB بدلًا من كائنات Mongoose أقدم. إليك الشيفرة النهائية للملف common/services/mongoose.service.ts بعد جمع الخيارات السابقة مع بعض اﻹعدادات المتعلقة بإعادة محاولة الاتصال: import mongoose from 'mongoose'; import debug from 'debug'; const log: debug.IDebugger = debug('app:mongoose-service'); class MongooseService { private count = 0; private mongooseOptions = { useNewUrlParser: true, useUnifiedTopology: true, serverSelectionTimeoutMS: 5000, useFindAndModify: false, }; constructor() { this.connectWithRetry(); } getMongoose() { return mongoose; } connectWithRetry = () => { log('Attempting MongoDB connection (will retry if needed)'); mongoose .connect('mongodb://localhost:27017/api-db', this.mongooseOptions) .then(() => { log('MongoDB is connected'); }) .catch((err) => { const retrySeconds = 5; log( `MongoDB connection unsuccessful (will retry #${++this .count} after ${retrySeconds} seconds):`, err ); setTimeout(this.connectWithRetry, retrySeconds * 1000); }); }; } export default new MongooseService(); تأكد أنك تميّز بين الدالة ()connect التي توفرها مكتبة Mongoose وبين دالة الخدمة الخاصة بنا connectWithRetry: تحاول mongoose.connect الاتصال بخدمة MongoDB الخاصة بنا (التي نشغلها باستخدام docker-compose) والذي ينتهي بعد زمن يُحدده الخيار serverSelectionTimeoutMS بالميلي ثانية. تعيد الدالة connectWithRetry محاولة الاتصال السابقة في حال بدأ التطبيق العمل قبل عمل خدمة MongoDB. وطالما أنها دالة بانية لصنف متفرّد singleton، وبالتالي ستعمل هذه الدالة مرة واحدة، لكنها ستحاول استدعاء ()connect باستمرار يتخلل ذلك فترة توقف مدتها retrySeconds ثانية عند ينتهي الوقت المخصص لمحاولة الاتصال. ستكون الخطوة التالية استبدال البيانات المقيمة في الذاكرة بقاعدة البيانات MongoDB! إزالة قاعدة البيانات المقيمة في الذاكرة وإضافة MongoDB استخدمنا سابقًا قاعدة البيانات المقيمة في الذاكرة كي نركّز في عملنا على بقية الوحدات البرمجية التي نبنيها. ولكي نستخدم MongoDB اﻵن، علينا أن نعيد بناء الملف users.dao.ts بالكامل، ويتطلب اﻷمر بداية إدراج اعتمادية أخرى: import mongooseService from '../../common/services/mongoose.service'; نزيل اﻵن كل ما يتعلق بتعريف الصنف UserDao ما عدا الدالة البانية، ثم نعيد بناءه بإنشاء تخطيط Schema لبيانات المستخدم يعمل مع Mongoose قبل الدالة البانية: Schema = mongooseService.getMongoose().Schema; userSchema = new this.Schema({ _id: String, email: String, password: { type: String, select: false }, firstName: String, lastName: String, permissionFlags: Number, }, { id: false }); User = mongooseService.getMongoose().model('Users', this.userSchema); تعرّف الشيفرة السابقة مجموعة Mongoose الخاصة بنا، وتضيف ميزة خاصة لا تمتلكها قاعدة البيانات المقيمة في الذاكرة وهي الخيار select:false للحقل password لإخفاء هذا الحقل كلما طلبنا قائمة بأسماء المستخدمين. قد يبدو تخطيط المستخدم مألوفًا فهو يشبه تخطيط كائن نقل البيانات DTO الذي تعاملنا معه، لكن الفرق الرئيسي هو أننا نعرّف الحقول التي يجب أن تتواجد ضمن مجموعة MongoDB التي تُدعى Users، بينما يُعرّف الكائن DTO الحقول المقبولة في طلبات HTTP. مع ذلك لن يتغير هذا الجزء المتعلق بكائن DTO، إذ سندرج كائنات DTO الثلاث التي عرّفناها سابقًا في أعلى الملف users.dao.ts. لكن وقبل كتابة شيفرة العمليات اﻷساسية CRUD على قاعدة البيانات، سنجري تغييرين على كائنات DTO. التغير اﻷول على كائن DTO: الحقل id_ مقابل الحقل id نزيل الحقل id من كائنات DT لأن Mongoose تقدم الحقل id_ تلقائيًا، إذ يأتي ضمن معاملات طلب الوجهة (المسار route). وانتبه إلى أن نماذج Mongoose تزوّدنا بمستخلص افتراضي للمعرّف id، لهذا عطلّنا هذا الخيار كالتالي:{ id: false } لتفادي أي التباس. ولأن هذا اﻷمر يعطّل مرجعنا إلى user.id ضمن الدالة الوسيطة ()validateSameEmailBelongToSameUser، سنتحتاج إلى user._id بدلًا عنه. تستخدم بعض قواعد البيانات id وتستخدم أخرى id_ للإشارة إلى حقل المعرّف. لكن في تطبيقنا الذي يستخدم Mongoose، انتبهنا ببساطة إلى أي ترميز نستخدمه في كل مرحلة من مراحل كتابة الشيفرة، لكن مع ذلك سيظهر الاختلاف لمستثمري الواجهة البرمجية كما يوضح الشكل التالي: (تظهر الصورة أعلاه استخدام وظهور معرفات المستخدم في مشروع الواجهة البرمجية REST بصورته النهائية. ولاحظ كيف يختلف ترميز حقل المعرّف باختلاف مصدر المعرّف سواء كان معامل لطلب مباشر، أو بيانات مشفرة بطريقة JWT أو سجل قاعدة بيانات مُحضر حديثًا). التغيير الثاني: إعداد السماحيات المبنية على تفعيل رايات محددة سنغيّر أيضًا الراية permissionLevel إلى permissionFlags في كائنات DTO لنعكس عملنا على منظومة سماحيات أكثر تعقيدًا، إضافة إلى تعريف مخطط Mongoose السابق userSchema. كائنات DTO ومبدأ عدم التكرار DRY تذكّر أن ما يضمه كائن DTO هي فقط الحقول التي نريد تمريرها بين الواجهة البرمجية وقاعدة البيانات، وقد يبدو اﻷمر بأنه تداخل أو تكرار في البيانات الموجودة في نموذج التعامل مع البيانات وتلك الموجودة في كائنات DTO وانتهاكًا لمبدأ "عدم التكرار"، لكن لا تبالغ في تطبيق هذا المبدأ على حساب اﻷمان الذي يكون في أفضل حالاته مع اﻹعدادات الافتراضية. فعندما يتطلب اﻷمر مثلًا إضافة حقل في مكان واحد، من المحتمل عندها أن يعرضه المطوّرون في الواجهة البرمجية دون قصد وغير منتبهين إلى أن تخزين البيانات ونقلها سياقان منفصلان وقد يتطلب كلًا منهما مجموعات مختلفة من المتطلبات. يمكننا بعد الانتهاء من التغييرات على كائنات DTO تنفيذ توابع العمليات اﻷساسية CRUD (بعد الدالة البانية UsersDao)، وسنبدأ بعملية إنشاء مستخدم جديد: async addUser(userFields: CreateUserDto) { const userId = shortid.generate(); const user = new this.User({ _id: userId, ...userFields, permissionFlags: 1, }); await user.save(); return userId; } لاحظ أننا نلغي قيمة permissionFlags التي قد يرسلها مستثمر الواجهة البرمجية عبر userFields, ونستبدلها بالقيمة 1. ننتقل تاليًا إلى عملية القراءة التي لها وظائف الحصول على مستخدم من خلال معرّفه أو من خلال البريد اﻹلكتروني أو الحصول على قائمة المستخدمين ضمن صفحات: async getUserByEmail(email: string) { return this.User.findOne({ email: email }).exec(); } async getUserById(userId: string) { return this.User.findOne({ _id: userId }).populate('User').exec(); } async getUsers(limit = 25, page = 0) { return this.User.find() .limit(limit) .skip(limit * page) .exec(); } ستكفي دالة DAO واحدة لتحديث مستخدم لأن دالة Mongoose في الخلفية findOneAndUpdate قادرة على تحديث المستند بأكمله أو تحديث جزء منه. وانتبه إلى أن دالتنا الخاصة تأخذ أحد القيمتين PatchUserDto أو PutUserDto للمعامل userFields من خلال استخدام نوع الاجتماع (|) في TypeScript: async updateUserById( userId: string, userFields: PatchUserDto | PutUserDto ) { const existingUser = await this.User.findOneAndUpdate( { _id: userId }, { $set: userFields }, { new: true } ).exec(); return existingUser; } يبلغ الخيار new: true مكتبة Mongoose بأن تعيد الكائن كما هو بعد التحديث بدلًا عما كانه أصلًا. أما عملية الحذف فهي موجزة في Mongoose: async removeUserById(userId: string) { return this.User.deleteOne({ _id: userId }).exec(); } قد يلاحظ القارئ أن كل استدعاء للدوال اﻷعضاء للصنف User مقترن باستدعاء للدالة ()exec، وهذا اﻷمر اختياري لكنه مفضّل بين مطوري Mongoose لأنه يقدم طريقة أفضل في تتبع حالة المكدس عند التنقيح. علينا اﻻن بعد الانتهاء من كتابة شيفرة كائن DAO تحديث الملف users.service.ts قليلًا حتى يتماشى مع الدوال الجديدة. ولا حاجة هنا ﻹعادة بناء الملف، بل فقط ثلاث لمسات بسيطة: @@ -16,3 +16,3 @@ class UsersService implements CRUD { async list(limit: number, page: number) { - return UsersDao.getUsers(); + return UsersDao.getUsers(limit, page); } @@ -20,3 +20,3 @@ class UsersService implements CRUD { async patchById(id: string, resource: PatchUserDto): Promise<any> { - return UsersDao.patchUserById(id, resource); + return UsersDao.updateUserById(id, resource); } @@ -24,3 +24,3 @@ class UsersService implements CRUD { async putById(id: string, resource: PutUserDto): Promise<any> { - return UsersDao.putUserById(id, resource); + return UsersDao.updateUserById(id, resource); } يبقى استدعاء بقية الدوال كما هو تمامًا، لأننا أبقينا الهيكلية التي أنشأناها سابقًا على الرغم من إعادة بناء الصنف UsersDao مع بعض الاستثناءات: نستخدم اﻵن الدالة ()updateUserById لتنفيذ العمليتين PUT و PATCH (والسبب كما ذكرنا سابقًا أننا ندعم النهج النمطي لتنفيذ واجهات برمجية REST، بعيدًا عن بعض التوصيات التي لا تدعم استخدام PUT في إنشاء موارد جديدة غير موجودة أصلًا كي لا تسمح الواجهة الخلفية لمستثمريها التحكم بتولد المعرّفات ID). طالما أن كائن DAO الجديد يستخدم المعاملين limit و page، سنمررهما إلى الدالة ()getUsers. تقدّم الهيكلية السابقة نموذجًا متماسكًا، إذ يرفض أية محاولة مثلًا لاستبدال MongoDB Mongoose بمكتبات أخرى مثل TypefORM و PostgreSL. ولتنفيذ مثل هذه التغييرات لا بد من إعادة بناء كل دالة من دوال DAO مع الحفاظ على بصمة هذه الدوال signature لتتفق مع بقية الشيفرة. اختبار الواجهة البرمجية REST المبنية على Mongoose لنشغّل الواجهة البرمجية باستخدام سطر اﻷوامر npm start، ونحاول إنشاء مستخدم: curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json' \ --data-raw '{ "password":"secr3tPass!23", "email":"marcos.henrique@toptal.com" }' يتضمن كائن الاستجابة معرّف المستخدم الجديد: { "id": "7WYQoVZ3E" } وسيكون إجراء بقية الاختبارات يديويًا أسهل باستخدام متغيرات البيئة: REST_API_EXAMPLE_ID="put_your_id_here" يبدو تحديث مستخدم كالتالي: curl --include --request PATCH "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "firstName": "Marcos", "lastName": "Silva" }' من المفترض ان تبدأ الاستجابة بالترويسة HTTP/1.1 204 No Content. (لاحظ أنه بدون include-- لن تُطبع أية استجابة). ولكي تتحقق من نجاح الطلبين السابقين نفّذ ما يلي: curl --request GET "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "firstName": "Marcos", "lastName": "Silva" }' تعرض الاستجابة الحقول المتوقعة ومن بينها id_ الذي ناقشناه سابقًا: { "_id": "7WYQoVZ3E", "email": "marcos.henrique@toptal.com", "permissionFlags": 1, "__v": 0, "firstName": "Marcos", "lastName": "Silva" } لاحظ وجود حقل خاص هو v__ يُستخدم من قبل Mongoose لتحديد الإصدار وسيتغير في كل مرة يُحدَّث فيها نفس السجّل. دعونا اﻵن نطلب قائمة بأسماء المستخدمين: curl --request GET 'localhost:3000/users' \ --header 'Content-Type: application/json' وستكون الاستجابة نفسها، لكنها مغلفة بالقوسين []. بعد أن تأكدنا أن كلمة المرور قد حُفظت بأمان، سنتحقق من إمكانية حذف المستخدم: curl --include --request DELETE "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' نتوقع أن نحصل هنا على الاستجابة 204 مجددًا. قد تتسائل إن كان حقل كلمة المرور يعمل كما نتوقع، طالما أننا حددنا الخيار select: false في تعريف تخطيط Mongoose وبالتالي لن يظهر الحقل عند الاستجابة للطلب GET. لهذا سنكرر إنشاء مستخدم جديد ونتحرى اﻷمر (لا تنسى تخزين المعرّف الجديد لاستخدامات لاحقة). كلمات المرور المخفية والتنقيح المباشر للبيانات في حاوية MongoDB للتحقق من التخزين اﻵمن لكلمات المرور (مشفرّة بدلًا من أن تظهر كما هي)، يمكن للمطورين تفحّص MongoDB مباشرة. وأحد الطرق الممكنة استخدام واجهة سطر اﻷوامر المعيارية mongo من داخل حاوية Docker قيد التشغيل: sudo docker exec -it toptal-rest-series_mongo_1 mongo من هناك نفّذ اﻷمر use api-db يتبعه ()db.users.find().pretty لتحصل على بيانات جميع المستخدمين بما فيها كلمات المرور. أما لمن يفضل العمل على واجهات رسومية، بإمكانهم تثبيت عميل MongoDB مثل Robo 3T: تُعد البادئة $...argon2 جزءًا من التنسيق النصي PHC وقد خّزنت دون تعديل عن قصد. فإظهار عبارة Argon2 ومعاملاتها العامة لن يساعد المخترقين على تحديد كلمة النص السرياﻷصلية إن استطاعوا سرقة قاعدة البيانات. ويمكن تعزيز كلمة المرور أكثر باستخدام التغفيل salting، وهي تقنية سنستخدمها لاحقًا مع مفتاح JWT. تأكدنا اﻵن أن Mongoose ترسل البيانات بنجاح إلى MongoDB، لكن كيف نتأكد أن مستثمري الواجهة البرمجية سيرسلون بيانات مناسبة ضمن طلباتهم إلى الوجهات الخاصة المستخدم؟ إضافة المكتبة express-validator هنالك طرق عدة للتحقق من الطلبات، سنستخدم منها في تطبيقنا المكتبة express-validator وهي مكتبة مستقرة وسهلة الاستخدام وتتمتع بتوثيق جيد. وعلى الرغم من إمكانية استخدام وظيفة التحقق المضمنة مع Mongoose، لكن الميزات التي Express.js يقدمها express-validator أكثر. إذ تقدم لك المكتبة متحققًا جاهزًا من صحة البريد اﻹلكتروني، بينما عليك كتابة شيفرته بنفسك في Mongoose. سنثبت اﻵن المكتبة express-validator: npm i express-validator ولكي نحدد الحقول التي نتحقق من صحتها، نستخدم التابع ()body الذي سندرجه في الملف users.routes.config.ts. يتحقق بعدها التابع ()bodyمن الحقول ويولّد قائمة باﻷخطاء مخزّنة في الكائن express.Request في حال أخفق الطلب، ونحتاج عندها إلى أداة وسيطة كي تستفيد من تلك القائمة وتتحقق من اﻷخطاء. وطالما أننا نستخدم نفس المنطق على بقية المسارات، سننشئ الملف common/middleware/body.validation.middleware.ts الذي يضم الشيفرة التالية: import express from 'express'; import { validationResult } from 'express-validator'; class BodyValidationMiddleware { verifyBodyFieldsErrors( req: express.Request, res: express.Response, next: express.NextFunction ) { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).send({ errors: errors.array() }); } next(); } } export default new BodyValidationMiddleware(); وهكذا سنكون قادرين على التعامل مع أية أخطاء تولّدها الدالة ()body. لنضف اﻵن ما يلي إلى الملف users.routes.config.ts: import BodyValidationMiddleware from '../common/middleware/body.validation.middleware'; import { body } from 'express-validator'; نستطيع اﻵن تحديث وجهاتنا كالتالي: @@ -15,3 +17,6 @@ export class UsersRoutes extends CommonRoutesConfig { .post( - UsersMiddleware.validateRequiredUserBodyFields, + body('email').isEmail(), + body('password') + .isLength({ min: 5 }) + .withMessage('Must include password (5+ characters)'), + BodyValidationMiddleware.verifyBodyFieldsErrors, UsersMiddleware.validateSameEmailDoesntExist, @@ -28,3 +33,10 @@ export class UsersRoutes extends CommonRoutesConfig { this.app.put(`/users/:userId`, [ - UsersMiddleware.validateRequiredUserBodyFields, + body('email').isEmail(), + body('password') + .isLength({ min: 5 }) + .withMessage('Must include password (5+ characters)'), + body('firstName').isString(), + body('lastName').isString(), + body('permissionFlags').isInt(), + BodyValidationMiddleware.verifyBodyFieldsErrors, UsersMiddleware.validateSameEmailBelongToSameUser, @@ -34,2 +46,11 @@ export class UsersRoutes extends CommonRoutesConfig { this.app.patch(`/users/:userId`, [ + body('email').isEmail().optional(), + body('password') + .isLength({ min: 5 }) + .withMessage('Password must be 5+ characters') + .optional(), + body('firstName').isString().optional(), + body('lastName').isString().optional(), + body('permissionFlags').isInt().optional(), + BodyValidationMiddleware.verifyBodyFieldsErrors, UsersMiddleware.validatePatchEmail, لا تنس إضافة BodyValidationMiddleware.verifyBodyFieldsErrors في كل وجهة بعد أي سطر يضم الدالة ()body وإلا لن يكون لها فائدة. لاحظ أيضًا كيف حدّثنا الوجهتين PUT و POST لاستخدام express-validater بدلًا من الدالة validateRequiredUserBodyFields التي بنيناها بأنفسنا. لأنهما الوجهتان الوحيدتان اللتان تستخدمان تلك الدالة. ويمكن حذف شيفرتها من الملف users.middleware.ts. هذا كل ما في اﻷمر، شغّل Node.js وجرّب باستخدام أي عميل REST تفضلّه كيف تتعامل الواجهة البرمجية مع المدخلات المختلفة. ولا تنس اﻹطلاع على توثيق express-validator لترى اﻹمكانيات اﻷخرى التي تقدّمها. فما فعلنا هو مجرد نقطة انطلاق. الاستيثاق والسماحيات (منح التصريحات) تتيح واجهتنا البرمجية مجموعة من نقاط الوصول endpoints لمستثمري الواجهة كي ينشئوا ويعدّلوا المستخدمين ويطلبو قوائم بهم، وتمنح كل نقطة منها وصولًا عامًا غير محدود. وتمنع أنماط الاستخدام أي مستخدم من تغيير بيانات آخر، كما تمنع أي دخلاء من الوصول إلى أية نقطة لا نريد وصولًا عامًا إليها. تتضمن القيود السابقة ناحيتين أساسيتين نختصرهما بالعبارة "auth" وتعني الإذن: اﻷولى هي الاستيثاق authentication وتعني معرفة صاحب الطلب، والثانية هي الترخيص أو التصريح authorization أي السماح لصاحب الطلب بتنفيذ طلبه أو لا. لهذا انتبه جيدًا إلى الناحية التي تُناقش، وخاصة في طلبات HTTP المعيارية، فالحالة 401 Unauthorized تتعلق بالاستيثاق، أما 403 Forbidden فتتعلق بالتصريح ولهذا السبب سنستخدم الاصطلاح "auth" ليعني الاستيثاق، ونستخدم المصطلح سماحيات permissions لمواضيع التصاريح. هنالك العديد من نُهج الاستيثاق التي يمكنك الاطلاع عليها، بما في ذلك الخدمات التي يقدمها طرف ثالث مثل Ath0، لكننا سنستخدم في تطبيقنا نهجًا أبسط مبني على مفاتيح JWT لكنه قابل للتوسع. يتكون مفتاح JWT من نص JSON مشفّر مع بعض البيانات الوصفية التي لا تتعلق بالاستيثاق، وتضم في حالتنا البريد اﻹلكتروني للمستخدم ورايات السماحيات permissions flags. كما يضم نص JSON نصًا سرًيا Secret للتحقق من سلامة البيانات الوصفية. إن الفكرة التي نتبعها هي الطلب من العميل إرسال JWT ضمن كل طلب غير عام، مما يسمح لنا بالتحقق من امتلاك العميل الثبوتيات الصحيحة لاستخدام نقطة الوصول المطلوبة دون الحاجة إلى إرسال الثبوتيات بأنفسهم عبر قناة الاتصال عند كل طلب. لكن في أي مكان سنكتب الشيفرة المناسبة؟ يمكن من خلال الدوال الوسيطة استخدامها عندما تكون ضمن إعدادات الوجهات (المسارات). إضافة وحدة الاستيثاق لنحضّر أولًا ما يضمه مفتاح JWT، إذ سنبدأ استعمال الحقل permissionFlags العائد إلى مورد المستخدم لأنه فقط يقدم بيانات وصفية ملائمة للتشفير ضمن مفاتيح JWT، وليس لوجود أي علاقة بين مفاتيح JWT ومنطق تحديد السماحيات. سنضيف قبل إنشاء أداة وسيطة middleware لتوليد مفاتيح JWT دالة خاصة إلى الملف users.dao.ts تستخلص حقل كلمة المرور، لأننا أعددنا Mongoose لتمنع عرضه: async getUserByEmailWithPassword(email: string) { return this.User.findOne({ email: email }) .select('_id email permissionFlags +password') .exec(); } أضف ما يلي في الملف users.dao.ts: async getUserByEmailWithPassword(email: string) { return UsersDao.getUserByEmailWithPassword(email); } ننشئ اﻵن المجلد auth في جذر المشروع، ونضيف نقطة وصول لنسمح لمستثمري الواجهة البرمجية بتوليد مفاتيح JWT. لهذا ننشئ أولًا الوحدة الوسيطة auth/middleware/auth.middleware.ts على شكل صنف متفرّد singleton يُدعى AuthMiddleware، كما سنحتاج إلى استيراد بعض المكتبات: import express from 'express'; import usersService from '../../users/services/users.service'; import * as argon2 from 'argon2'; ننشئ ضمن الصنف AuthMiddleware دالة تتحقق من وجود ثبوتيات صالحة ضمن طلبات مستخدمي الواجهة البرمجية: async verifyUserPassword( req: express.Request, res: express.Response, next: express.NextFunction ) { const user: any = await usersService.getUserByEmailWithPassword( req.body.email ); if (user) { const passwordHash = user.password; if (await argon2.verify(passwordHash, req.body.password)) { req.body = { userId: user._id, email: user.email, permissionFlags: user.permissionFlags, }; return next(); } } // إعطاء نفس الرسالة في كلتا الحالتين // يساعد في الحماية من محاولات الاختراق res.status(400).send({ errors: ['Invalid email and/or password'] }); } ولكي تتأكد الدالة الوسيطة من وجود الحقلين email و password في جسم الطلب req.body، نستخدم express-validator، عندما نهيئ لاحقًا المسار لاستخدام الدالة ()verifyUserPassword. تخزين النص السري ضمن مفتاح JWT نحتاج إلى سر كي نوّلد مفتاح JWT، الذي نستخدمه في تعليم المفتاح وللتحقق من صحة المفاتيح القادمة مع طلبات العملاء. سنخزّن هذا النص السريضمن ملف متغيّر بيئة env. منفصل بدلًا من كتابته ضمن ملف TypeScript، ولن يُدفع هذا الملف إلى مستودع الشيفرة. وكما جرت العادة، أضفنا الملف env.example. إلى المستودع لمساعدة المطورين على فهم أي المتغيرات مطلوبة عند إنشاء الملف env. الحقيقي. ونحتاج في حالتنا إلى متغير يُدعى JWT_SECRET يُخزّن سر مفتاح JWT على هيئة نص. وتذكّر أن تغيّر هذا النص السريفي نسختك إن أردت العمل على التطبيق انطلاقًا من المستودع الذي يضم المشروع المكتمل في نهاية هذه السلسلة من المقالات. كما تجدر اﻹشارة هنا إلى ضرورة اتباع الممارسات اﻷفضل عند استخدام مفاتيح JWT بالتمييز بين المفاتيح وفق البيئة (تطوير، إنتاج،…). ينبغي على الملف env. الموجود في جذر المشروع اتباع التنسيق التالي، لكن ليس بالضرورة استخدام نفس السر: JWT_SECRET=My!@!Se3cr8tH4sh3 ومن الطرق السهلة لتحميل هذه المتغيرات ضمن التطبيق استخدام مكتبة تُدعى dotenv: npm i dotenv وكل ما تحتاجه لتهيئتها هو استدعاء الدالة ()dotenv.config حالما تشغّل التطبيق. لهذا أضف الشيفرة التالية أعلى الملف app.ts: import dotenv from 'dotenv'; const dotenvResult = dotenv.config(); if (dotenvResult.error) { throw dotenvResult.error; } متحكم الاستيثاق آخر المتطلبات التي تلزمنا قبل توليد مفتاح JWT هو تثبيت المكتبة jsonwebtoken وأنواع TypeScript الخاصة بها: npm i jsonwebtoken npm i --save-dev @types/jsonwebtoken لننشئ اﻵن المتحكم auth/controllers/auth.controller.ts، ولا حاجة ﻹدراج المكتبة dotenv لأن إدراجها في ملف التطبيق app.ts جعل محتوى ملف متغيرات البيئة متاحًا ضمن كامل التطبيق من خلال كائن Node.js العام process: import express from 'express'; import debug from 'debug'; import jwt from 'jsonwebtoken'; import crypto from 'crypto'; const log: debug.IDebugger = debug('app:auth-controller'); /** * تُنشر هذه القيمة تلقائيًا من ملف متغيرات البيئة الذي عليك إنشاءه بنفسك في جذر المشروع *في المستودع لمعرفة التنسيق المطلوب .env.example اطلع على الملف */ // @ts-expect-error const jwtSecret: string = process.env.JWT_SECRET; const tokenExpirationInSeconds = 36000; class AuthController { async createJWT(req: express.Request, res: express.Response) { try { const refreshId = req.body.userId + jwtSecret; const salt = crypto.createSecretKey(crypto.randomBytes(16)); const hash = crypto .createHmac('sha512', salt) .update(refreshId) .digest('base64'); req.body.refreshKey = salt.export(); const token = jwt.sign(req.body, jwtSecret, { expiresIn: tokenExpirationInSeconds, }); return res .status(201) .send({ accessToken: token, refreshToken: hash }); } catch (err) { log('createJWT error: %O', err); return res.status(500).send(); } } } export default new AuthController(); تربط المكتبة المفتاح الجديد بالنص السري الذي حددناه jwtSecret. كما سنوّلد مُوهِم (أو مُغفِّل salt) ومعمّي hash باستخدام الوحدة البرمجية اﻷصلية crypto في Node.js، وبعدها ننشئ المفتاح الذي يتمكن مستثمرو الواجهة من تحديث مفتاح JWT الحالي، وهذا اﻷمر مفيد بشكل خاص عند توسيع التطبيق. لكن ما الفرق بين refreshKey و refreshToken و accessToken؟ تُرسل هذه المفاتيح إلى مستثمر الواجهة البرمجية والغاية من ذلك هو: استخدام accessToken للطلبات التي تقع خارج إطار الوصول العام، و refreshToken لطلب استبدال مفتاح accessToken منتهي الصلاحية. ويستخدم المفتاح refreshKey لتمرير المتغير salt مشفّرًا ضمن المفتاح refreshToken إلى دالة التحديث الوسيطة التي سنشرحها لاحقًا. تجدر الملاحظة إلى أننا اعتمدنا على المكتبة jsonwebtoken في تحديد مدة صلاحية المفتاح، فإن انتهت صلاحية مفتاح لا بد من إعادة الاستيثاق من جديد: مسار التحقق اﻷساسية في الواجهة البرمجية REST المبنية على Node.js لنهيئ نقطة الوصول ضمن الملف auth/auth.routes.config.ts: import { CommonRoutesConfig } from '../common/common.routes.config'; import authController from './controllers/auth.controller'; import authMiddleware from './middleware/auth.middleware'; import express from 'express'; import BodyValidationMiddleware from '../common/middleware/body.validation.middleware'; import { body } from 'express-validator'; export class AuthRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'AuthRoutes'); } configureRoutes(): express.Application { this.app.post(`/auth`, [ body('email').isEmail(), body('password').isString(), BodyValidationMiddleware.verifyBodyFieldsErrors, authMiddleware.verifyUserPassword, authController.createJWT, ]); return this.app; } } لا تنس إضافة التالي إلى الملف app.ts: // ... import { AuthRoutes } from './auth/auth.routes.config'; // ... routes.push(new AuthRoutes(app)); //Userroutes قد يكون قبل أو بعد // ... أصبحنا اﻵن مستعدين ﻹعادة تشغيل Node.js واختبار الشيفرة، ولا بد من استخدام نفس الثبوتيات التي استخدمناها عند إنشاء المستخدم سابقًا: curl --request POST 'localhost:3000/auth' \ --header 'Content-Type: application/json' \ --data-raw '{ "password":"secr3tPass!23", "email":"marcos.henrique@toptal.com" }' ستكون الاستجابة شبيهة بالتالي: { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJVZGdzUTBYMXciLCJlbWFpbCI6Im1hcmNvcy5oZW5yaXF1ZUB0b3B0YWwuY29tIiwicHJvdmlkZXIiOiJlbWFpbCIsInBlcm1pc3Npb25MZXZlbCI6MSwicmVmcmVzaEtleSI6ImtDN3JFdDFHUmNsWTVXM0N4dE9nSFE9PSIsImlhdCI6MTYxMTM0NTYzNiwiZXhwIjoxNjExMzgxNjM2fQ.cfI_Ey4RHKbOKFdVGsowePZlMeX3fku6WHFu0EMjFP8", "refreshToken": "cXBHZ2tJdUhucERaTVpMWVNTckhNenQwcy9Bd0VIQ2RXRnA4bVBJbTBuQVorcS9Qb2xOUDVFS2xEM1RyNm1vTGdoWWJqb2xtQ0NHcXhlWERUcG81d0E9PQ==" } وكما فعلنا سابقًا، سنستخدم متغيرات البيئة لاستخدام القيم السابقة بشكل ملائم: REST_API_EXAMPLE_ACCESS="put_your_access_token_here" REST_API_EXAMPLE_REFRESH="put_your_refresh_token_here" وهكذا نكون قد حصلنا على مفتاح الوصول ومفتاح التحديث لكننا نحتاج إلى أداة وسيطة للاستفادة منهما. وحدة وسيطة للتعامل مع JWT نحتاج بداية إلى نوع TypeScript جديد للتعامل مع بنية المفتاح JWT بشكلها غير المرمّز، لهذا أنشئ الملف الذي يضم الشيفرة التالية common/types/jwt.ts: export type Jwt = { refreshKey: string; userId: string; permissionFlags: string; }; لنكتب اﻵن شيفرة الدوال الوسيطة التي تتحقق من وجود مفتاح تحديث، والتحقق منه، والتحقق من مفتاح JWT. وسنضع الدوال الثلاث في الملف الجديد common/types/jwt.ts: import express from 'express'; import jwt from 'jsonwebtoken'; import crypto from 'crypto'; import { Jwt } from '../../common/types/jwt'; import usersService from '../../users/services/users.service'; // @ts-expect-error const jwtSecret: string = process.env.JWT_SECRET; class JwtMiddleware { verifyRefreshBodyField( req: express.Request, res: express.Response, next: express.NextFunction ) { if (req.body && req.body.refreshToken) { return next(); } else { return res .status(400) .send({ errors: ['Missing required field: refreshToken'] }); } } async validRefreshNeeded( req: express.Request, res: express.Response, next: express.NextFunction ) { const user: any = await usersService.getUserByEmailWithPassword( res.locals.jwt.email ); const salt = crypto.createSecretKey( Buffer.from(res.locals.jwt.refreshKey.data) ); const hash = crypto .createHmac('sha512', salt) .update(res.locals.jwt.userId + jwtSecret) .digest('base64'); if (hash === req.body.refreshToken) { req.body = { userId: user._id, email: user.email, permissionFlags: user.permissionFlags, }; return next(); } else { return res.status(400).send({ errors: ['Invalid refresh token'] }); } } validJWTNeeded( req: express.Request, res: express.Response, next: express.NextFunction ) { if (req.headers['authorization']) { try { const authorization = req.headers['authorization'].split(' '); if (authorization[0] !== 'Bearer') { return res.status(401).send(); } else { res.locals.jwt = jwt.verify( authorization[1], jwtSecret ) as Jwt; next(); } } catch (err) { return res.status(403).send(); } } else { return res.status(401).send(); } } } export default new JwtMiddleware(); تتحقق الدالة ()validRefreshNeeded أيضًا إذا ما كان مفتاح التحديث صحيحًا من أجل معرّف مستخدم محدد. فإن كان كذلك، سنستخدم الدالة authController.createJWT لتوليد مفتاح JWT جديد للمستخدم. وتتحقق الدالة ()validJWTNeeded, إن أرسل مستثمر الواجهة البرمجية مفتاح JWT صالح ضمن ترويسة طلب HTTP وفق الصيغة Authorization: Bearer <token> (وهذا أيضًا للأسف تضارب آخر بين الاستيثاق والتصريح). علينا اﻵن تهيئة مسار جديد لتحديث المفتاح، وقد شُفِّرت ضمنه رايات السماحية. مسار لتحديث مفاتيح JWT سندرج اﻷداة الوسطية الجديدة ضمن الملف auth.routes.config.ts: import jwtMiddleware from './middleware/jwt.middleware'; ثم سنضيف المسارات التالية: this.app.post(`/auth/refresh-token`, [ jwtMiddleware.validJWTNeeded, jwtMiddleware.verifyRefreshBodyField, jwtMiddleware.validRefreshNeeded, authController.createJWT, ]); سنختبر الآن إن كانت تعمل كما هو مطلوب باستخدام المفتاحين accessToken و refreshToken اللذان حصلنا عليهما سابقًا: curl --request POST 'localhost:3000/auth/refresh-token' \ --header 'Content-Type: application/json' \ --header "Authorization: Bearer $REST_API_EXAMPLE_ACCESS" \ --data-raw "{ \"refreshToken\": \"$REST_API_EXAMPLE_REFRESH\" }" علينا أن نتوقع الحصول على مفتاحين جديدين accessToken و refreshToken كي نستخدمهما لاحقًا. بإمكانك اﻵن تجريب كيف تتحقق الواجهة الخلفية من المفتاحين السابقين، وكيف تحدد عدد المرات التي يمكنك فيها الحصول على مفاتيح جديدة. واﻵن سيتمكن مستثمرو الواجهة الخلفية من إنشاء مفاتيح JWT والتحقق منها وتحديثها. الخلاصة أكملنا في هذا المقال العمل على تطبيقنا الذي يُنشئ واجهة برمجة REST باستخدام TypeScript و Express.js في بيئة Node.js وقد انتقلنا في هذا القسم من قاعدة بيانات مؤقتة مقيمة في الذاكرة إلى استخدام قاعدة بيانات MongoDB بالاستعانة بالمكتبة Mongoose كما عملنا على تنفيذ آلية للتحقق من صحة المدخلات باستخدام مفاتيح ويب JWT والمكتبة jsonwebtoken. أما عن مفاهيم السماحيات وكيفية تنفيذها لتتكامل مع مفاتيح JWT فهذا ما سنراه في المقال التالي. ترجمة -وبتصرف- للقسم اﻷول من المقال Building a Node.js TypeScript REST API, Part 3 MongoDB, Authentication, and Automated Tests اقرأ أيضًا المقال السابق: بناء واجهة برمجية متوافقة مع REST باستخدام Express.js القسم الثاني: نماذج البيانات والبرمجيات الوسيطة والخدمات دمج قاعدة البيانات MongoDB في تطبيقك Node تطبيق عملي لتعلم Express - الجزء الأول: إنشاء موقع ويب هيكلي لمكتبة محلية مقارنة بين MySQL و MongoDB
  13. يُطلب منك في هذا التمرين التطبيقي أن تستخدم تطبيق الكرات المرتدة الذي بنيناه في المقال السابق كنقطة انطلاق ومن ثم إضافة بعض الميزات المهمة اﻷخرى. كما ننصحك قبل محاول العمل على هذا التمرين الاطلاع على سلسلة المقالات السابقة التي تشرح مبادئ البرمجة بالكائنات في جافا سكريبت. نقطة الانطلاق انسخ بداية الملفات index-finished.html و style.css و main-finished.js إلى حاسوبك -وهي ملفات تخص التمرين التطبيقي في المقال السابق- واحفظها في مجلد جديد. كما يمكنك استخدام محررات على اﻹنترنت مثل CodePen و JSFiddle و Glitch. إذ تسمح هذه المحررات بلصق شيفرة HTML و CSS ضمن المحرر، لكن إن لم يكن للمحرر الذي تستخدمه نافذة مخصصة شفرة جافا سكريبت، بإمكانك حينها وضع الشيفرة داخل عنصر <script> في صفحة HTML. نصائح وتلميحات إليك بعض المؤشرات التي تخص التطبيق قبل أن تبدأ: التمرين يحمل تحديًا واضحًا، لهذا ننصحك بقراءة المطلوب بالكامل قبل أن تبدأ كتابة شيفرة كل مهمة، وانتقل من خطوة إلى أخرى ببطء وحذر. قد يكون من الجيد أن تنشئ نسخة مستقلة عن كل التمرين بعد أن تنجز كل مرحلة بنجاح كي تتمكن من العودة إليها إن واجهتك المشاكل لاحقًا. موجز المشروعتأكل لقد كان بناء تطبيق الكرات المرتدة ممتعًا، لكن عليك جعله اﻵن أكثر تفاعلًا بإضافة كرة شريرة يتحكم بها المستخدم الكرات إن التقطتها. كما نريد اختبار مهارتك في بناء إنشاء الكائنات بإنشاء كائن عام Shape ترثه الكرات الملونة والكرة الشريرة. ونريد أخيرًًا عدادًا للنتيجة لتحديد عدد الكرات الباقية التي يجب التقاطها. تعرض الصورة التالية مثالًا عما سيكونه التطبيق عند إنجازه: ولتكوّن فكرة أوضح، ألقِ نظرة على التطبيق المكتمل لكن لا تختلس النظر إلى شيفرته! الخطوات التي عليك إكمالها تصف اﻷقسام التالية ما عليك إنجازه. إنشاء الصنف shape أنشئ قبل كل شيء الصنف Shape. ويمتلك هذا الصنف دالة بانية فقط تُعرّف الخاصيات x و y و velX و velY كما تعرّفها الدالة البانية للصنف ()Ball، لكن دون الخاصيتين color و size. يجب أن يرث الصنف Ball الكائن Shape باستخدام التعليمة extends، وينبغي للدالة البانية الخاصة بالصنف Ball أن: تأخذ نفس الوسطاء كما هو حالها سابقًا: x و y و velX و velY و size و color. استدعاء الدالة البانية للصنف باستخدام اﻷمر ()super، وتمرير الوسطاء x و y و velX و velY إليها. تهيئة الخاصيتين color و size من القيم التي تُمرر إليها. وعلى الصنف Ball تعريف خاصية جديدة تُدعى exists التي تُستخدم لتتبع وضع الكرة إن كانت لا تزال في البرنامج (لم تؤكل بعد). وينبغي أن تأخذ الخاصية قيم منطقية (true / false) وتُهيأ بالقيمة true. كما يحتاج التابع ()collisionDetect في الصنف Ball إلى تحديث طفيف، وهو ألا تؤخذ الكرة في الحسبان عند حساب التصادمات إن لم تكن موجودة (أي قيمة الخاصية exists لها هي true). لهذا عليك استبدال شيفرة الدالة ()collisionDetect collisionDetect() { for (const ball of balls) { if (!(this === ball) && ball.exists) { const dx = this.x - ball.x; const dy = this.y - ball.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < this.size + ball.size) { ball.color = this.color = randomRGB(); } } } } وكما ناقشنا سابقًا، ستكون اﻹضافة الوحيدة للتابع هو التحقق من كون الكرة موجودة وذلك باستخدام العبارة ball.exists ضمن الشرط if. وينبغي ألا تغيّر في تعريف التابعين أي شيء ويجب أن يبقيا تمامًا كما عرّفناهما سابقًا. حاول اﻵن أن تعيد تحميل الشيفرة وستعمل بنفس طريقة عملها السابقة، على الرغم من إعادة تصميم الكائنات الموجودة. تعريف الكرة الشريرة حان الوقت للعمل مع الكرة الشريرة والصنف EvilCircle، وسيضم تطبيقنا كرة واحدة من هذا الصنف. سنعرّف الصنف باستخدام دالة بانية ترث من الصنف Shapeوذلك ﻹكسابك بعض المهارة. فقد ترغب لاحقًا بإضافة كرة شريرة ثانية يتحكم بها لاعب آخر، أو مجموعة من الكرات الشريرة التي يتحكم بها الحاسوب. وتذكر أننا نحتاج كرة شريرة واحدة فقط في تطبيقنا. أنشئ إذًا تعريفًا للصنف EvilCircle واجعله يرث من الصنف Shape باستخدام extends. الدالة البانية للصنف EvilCircle ينبغي أن تتميز الدالة البانية بما يلي: يُمرر لها الوسيطان xو y. تُمرر الوسيطين إلى الدالة البانية للصنف اﻷب ()Shape مع القيمتين velx و velY واللتان تُمررا كقيم ثابتة 20، أي عليك كتابة الشيفرة كالتالي super(x,v,20,20). اضبط اللون color على الأبيض white وقياس الكرة size على القيمة 10. وأخيرًا لا بد للدالة البانية من إعداد الشيفرة ليتمكن المستخدم من تحريك الكرة على الشاشة: window.addEventListener("keydown", (e) => { switch (e.key) { case "a": this.x -= this.velX; break; case "d": this.x += this.velX; break; case "w": this.y -= this.velY; break; case "s": this.y += this.velY; break; } }); تضيف الشيفرة السابقة مترصّد لحدث الضغط على مفتاح keydown إلى الكائن window، وبالتالي، عندما يُضغط مفتاح يتفقد المتصفح قيمة الخاصية key لكائن الحدث لمعرفة الزر الذي ضُغط.فإن كان أحد المفاتيح اﻷربعة المخصصة، تتحرك الكرة إلى اليمين أو اليسار أو اﻷعلى أو الأسفل. تعريف توابع الكائن EvilCircle للصنف ثلاث توابع نشرحها تاليًا. التابع ()draw وله نفس غاية التابع ()draw في الصنف Ball الذي يرسم نسخة من الكائن على اللوحة. يعمل التابع في الصنف EvilCircle بنفس الطريقة، لهذا بإمكانك نسخ شيفرة التابع ()draw للصنف Ball ومن ثم إجراء التعديلات التالية: لا نريد أن تملئ الكرة بأي لون بل إطار خارجي فقط. وتستطيع أن تفعل ذلك بتغيير التابعين fillstyle و fill إلى التابعين strokeStyle و stroke على الترتيب. اجعل الإطار الخارجي أكثر ثخانة كي تُرى الكرة الشريرة بشكل أوضح. وتستطيع إنجاز الأمر بضبط قيمة lineWidth (على القيمة 3 مثلًا) وذلك بعد وضع هذه التعليمة في مكان مناسب بعد الدالة ()beginPath. التابع ()checkBounds ينفّذ هذا التابع ما ينفّذه الجزء الأول من التابع ()update في الصنف Ball وذلك بالتحقق من وصول حافة الكرة إلى حواف الشاشة ومنعها من ذلك. لهذا انسخ شيفرة هذه الدالة وأجر عليها التعديلات التالية: احذف السطرين اﻷخيرين، فلا نريد تغيير موقع الكرة الشريرة تلقائيًا في كل إطار لأننا نريد تحريكها بطريقة أخرى كما سنرى لاحقًا. إن أعاد الشرط داخل العبارة ()if القيمة true، فلن نريد في هذه الحالة تحديث قيمتي الخاصيتين velx و velY بل نريد تغيير قيمتي x و y كي ترتد الكرة الشريرة قليلًا عن الشاشة. قد يفي بالغرض إضافة أو طرح (بما يناسب) قياس الكرة الشريرة. التابع ()collisionDetect يعمل هذا التابع بنفس الطريقة التي يعمل فيها التابع المقابل في الصنف Ball لهذا بإمكانك نسخ شيفرته واستخدامها كأساس للتابع الجديد. لكن بالطبع مع إجراء التغييرات التالية: لا حاجة في الشرط if الخارجي للتحقق من أن الكرة المختبرة هي نفسها الكرة التي وصل إليها التكرار لأنها لم تعد كرة عادية بل الكرة الشريرة. وما عليك فعله هو التأكد من أن الكرة التي نختبرها موجودة (ما هي الخاصية التي نستخدمها؟). فإن لم تكن موجودة فإن الكرة قد أُكلت مسبقًا من قبل الكرة الشريرة، فلا حاجة لاختبارها مجددًا. لا حاجة في الشرط if الداخلي إلى تغيير لون الكرة عند التقاط حالة تصادم، بل عليك فقط أن تجعل الكرة التي تصطدم بالكرة الشريرة غير موجودة (كيف ستفعل ذلك؟) إدخال الكرة الشريرة إلى البرنامج بعد أن عرّفنا الكرة الشريرة، لا بد من إظهارها فعليًا في المشهد. وﻹنجاز اﻷمر، عليك إجراء بعض التعديلات على الدالة ()loop. أنشئ بداية نسخة جديدة عن كائن الكرة الشريرة (اضبط المعاملات اللازمة). عليك فعل ذلك مرة واحدة، وليس في كل تكرار للدالة ()loop. في النقطة التي نتنقل فيها بين الكرات ونستدعي التوابع ()draw و ()update و ()collisionDetect لكل كرة، استدع التوابع السابقة فقط إن كانت الكرة التي وصلنا إليها في التكرار موجودة. استدع التوابع ()draw و ()checkBounds و ()collisionDetect لنسخة الكرة الشريرة في كل تكرار للحلقة. إنجاز شيفرة عداد النتيجة اتبع الخطوات التالية: أضف <p> تحت العنصر <h1> في ملف HTML وفيها النص Ball count. أضف القاعدة التالية في آخر ملف CSS: p { position: absolute; margin: 0; top: 35px; right: 5px; color: #aaa; } قم بإجراء التعديلات التالية في ملف جافا سكريبت: أنشئ متغيرًا لتخزين مرجع إلى الفقرة النصية. أظهر عدد الكرات على الشاشة بطريقة تجدها مناسبة. زد العداد وحدّث عدد الكرات الموجود على الشاشة في كل مرة تُضاف فيها كرة إلى المشهد. انقص العداد وحدّث عدد الكرات على الشاشة في كل مرة تأكل فيها الكرة الشريرة كرة (أي تجعلها غير موجودة). الخلاصة تعلمنا في هذا المقال كيفية تحسين تطبيق الكرات المرتدة الذي بنيناه في المقال السابق بإضافة ميزات جديدة مثل كرة شريرة يتحكم بها المستخدم حيث تتحرك هذه الكرة عبر الشاشة ويستطيع المستخدم التحكم بها باستخدام مفاتيح الأسهم كما أضفنا عدّاد للنتيجة ليُظهر عدد الكرات المتبقية على الشاشة وشرحنا طريقة إخفاء الكرات بحيث تُصبح الكرة غير موجودة عند اصطدامها بالكرة الشريرة. ترجمة -وبتصرف لمقال Adding features to our bouncing ball demo اقرأ أيضًا المقال السابق: بناء تطبيق الكرات المرتدة الملونة باستخدام الكائنات في جافا سكريبت -الجزء الأول تجربتك اﻷولى مع جافا سكريبت التعامل مع التصاميم، الألوان والخطوط باستخدام Canvas في جافاسكربت الرسم على لوحة في جافاسكربت معالجة الأحداث في جافا سكريبت
  14. نكمل في هذا المقال ما بدأناه في مقالنا السابق الذي يتحدث عن بناء واجهة برمجية REST، وكنا قد خصصناه لشرح النقاط التالية: استخدام npm في إنشاء واجهة خلفية من الصفر. تحضير الاعتماديات اللازمة مثل TypeScript. استخدام الوحدة البرمجية debug المضمنة في بيئة Node.js. بناء هيكلية مشروع Express.js وتسجيل اﻷحداث التي تقع أثناء تشغيل التطبيق باستخدام Winston. إن كنت تشعر أن المفاهيم التي تحدثنا عنها واضحة بالنسبة إليك، انسخ رابط المشروع، وانتقل إلى الفرع toptal-article-01 ثم تابع القراءة خدمات الواجهة البرمجية ريست والبرمجيات الوسيطة والمتحكمات ونماذج البيانات سنفصّل في مقالنا هذا النقاط التالية: الخدمات Services: التي تجعل الشيفرة أكثر وضوحًا بترتيب العمليات المنطقية ضمن دوال يمكن للبرمجيات الوسيطة والمتحكمات استدعاءها. البرمجيات الوسيطة Middleware: التي تقيِّم حالة المتطلبات prerequisites قبل أن تستدعي Express.js دالة المتحكم المناسب. المتحكمات Controllers: تستخدم الخدمات لمعالجة الطلبات قبل إرسال نتيجة الطلب إلى العميل. نماذج البيانات Models: تصف بياناتنا وتساعد في عمليات التحقق التي نجريها أثناء تصريف التطبيق. سنضيف أيضًا قاعدة بيانات بسيطة ولا تصلح بالطبع لنسخة اﻹنتاج، فغايتها الوحيدة تسهيل فهم تطبيقنا ومتابعته، وتمهّد للمقال التالي الذي يتحدث عن الاتصال بقاعدة البيانات، والتكامل مع القاعدة MongoDB والمكتبة Mongoose. الخطوات اﻷولى للعمل مع DAOs و DTOs وقاعدة بياناتنا المؤقتة لن تستخدم قاعدة البيانات في هذه المرحلة ملفات لتخزين البيانات، بل ستحتفظ ببيانات المستخدمين ضمن مصفوفة، أي أن البيانات ستزول بمجرد إغلاق Node.js. ستدعم قاعدة البيانات العمليات اﻷساسية CRUD ( إنشاء Create، قراءة Read، تحديث Update، حذف Delete). ونشير إلى مفهومين نستخدمهما هنا وهما: كائنات الوصول إلى البيانات Data access Objects واختصارًا DAOs. كائنات نقل البيانات Data transfer Objects واختصارًا DTOs. يُستخدم DAO للاتصال بقاعدة بيانات محددة وتنفيذ عمليات CRUD عليها، بينما يحتوي DTO البيانات الخام التي يرسلها DAO أو التي يستقبلها من قاعدة البيانات. وبعبارة أخرى، تتوافق كائنات DTO مع أنواع نماذج البيانات، بينما تُعد كائنات DAO خدمات تستخدم كائنات DTO. وهكذا قد تكون كائنات DTO أكثر تعقيدًا وفقًا لمستوى تداخل البيانات في بنية قاعدة البيانات، بينما تكون نسخة واحدة من كائن DAO مسؤولة عن فعل محدد على سطر واحد فقط من قاعدة البيانات. لماذا تُستخدم كائنات DTO يساعد استخدام كائنات DTO كي تتوافق كائنات TypeScript مع نماذج البيانات الخاصة بنا في الحفاظ على تناسق قاعدة البيانات كما سنرى لاحقًا. لكن هنالك نقص واضح، فلا يمكن أن تقدم كائنات DTO ولا حتى TypeScript نفسها أي نوع من التحقق التلقائي مما يُدخله المستخدم، لأن ذلك يحدث أثناء تنفيذ التطبيق. فعندما يصل أحد المدخلات إلى نقطة وصول الواجهة البرمجية، فقد يكون لهذا المُدخل: حقول زائدة. حقول مطلوبة مفقودة (كتلك التي لا تبدأ بالمحرف ?). نوع بيانات الحقل لا تطابق نوع البيانات المحدد في نموذج البيانات الذي يعتمد على TypeScript. فلن تتحقق TypeScript (أو جافا سكريبت التي ستُنقل الشيفرة إليها) من تلك المدخلات، لهذا من المهم ألا ننسى عمليات التحقق، وخاصة عندما تكون الواجهة البرمجية متاحة للعموم. قد تساعدك في ذلك حزم مثل ajv، لكنها تعمل عادة بتعريف كائنات لها تخطيط مخصص لمكتبة محددة بدلًا من كائنات TypeScript الأصلية (ستلعب مكتبة Mongoose هذا الدور كما سنلاحظ في مقال تالٍ). وقد يخطر في بالك السؤال التالي: "هل علي استخدام كلًا من كائنات DAO و DTO إن توفّر ما هو أبسط؟"من اﻷفضل تفادي استخدام كائنات DTO في مشاريع Express.js/TypeScript حقيقة صغيرة إلا في الحالة التي تخطط فيها توسيع هذه المشاريع لتصبح متوسطة الحجم. لكن حتى لو لم تكن بصدد استخدامها في نسخ اﻹنتاج، يبقى هذا التطبيق فرصة مفيدة على طريق احتراف إنشاء واجهات برمجية باستخدام TypeScript. فمن الجيد التمرّن على توسيع استخدام أنواع TypeScript لتشمل أساليب أخرى، والعمل مع كائنات DTO لتقارنها مع أساليب أكثر بساطة عند إضافة مكوّنات ونماذج بيانات. نموذج المستخدم في الواجهة البرمجية REST على مستوى TypeScript نعرّف بداية ثلاث كائنات DTO للمستخدم، لهذا ننشئ مجلدًا يُدعى dto ضمن المجلد user، ثم ننشئ ملفًا يُدعى create.user.dto.ts يضم الشيفرة التالية: export interface CreateUserDto { id: string; email: string; password: string; firstName?: string; lastName?: string; permissionLevel?: number; } يعني ذلك أنه كلما انشأنا مستخدمًا جديدًا، وبصرف النظر عن قاعدة البيانات، لا بد أن يمتلك معرّفًا id وكلمة مرور password وبريد إلكتروني email وحقلين اختياريين هما الاسم اﻷول والثاني. يمكن لهذه المتطلبات أن تتغير وفقًا لمتطلبات العمل على مشروع محدد. ولا بد من تحديث الكائن بأكمله عند استخدام الاستعلام PUT، وسيكون الحقلان الاختياريين اﻵن ضروريان. لهذا أنشئ الملف put.user.dto.ts في نفس المجلد السابق ليضم الشيفرة التالية: export interface PutUserDto { id: string; email: string; password: string; firstName: string; lastName: string; permissionLevel: number; } وبالنسبة إلى طلبات PATCH، باﻹمكان استخدام الميزة partial من TypeScript والتي تنشئ نوعًا جديدًا بنسخ نوع آخر وجعل كل حقوله اختيارية. وهكذا ستكون شيفرة الملف patch.user.dto.ts هي فقط الشيفرة التالية: import { PutUserDto } from './put.user.dto'; export interface PatchUserDto extends Partial<PutUserDto> {} لننشئ اﻵن قاعدة البيانات المؤقتة في الذاكرة، لهذا ننشئ أولًا المجلد daos داخل المجلد user ومن ثم نضيف الملف users.dao.ts. ندرج أولًا كائنات DTO التي أنشأناها: import { CreateUserDto } from '../dto/create.user.dto'; import { PatchUserDto } from '../dto/patch.user.dto'; import { PutUserDto } from '../dto/put.user.dto'; وللتعامل مع معرّفات المستخدمين IDs، سنضيف المكتبة shortid باستخدام الطرفية: npm i shortid npm i --save-dev @types/shortid بالعودة إلى الملف users.dao.ts، سندرج المكتبة shortid: import shortid from 'shortid'; import debug from 'debug'; const log: debug.IDebugger = debug('app:in-memory-dao'); بإمكاننا اﻵن إنشاء صنف يُدعى UserDao يبدو كالتالي: class UsersDao { users: Array<CreateUserDto> = []; constructor() { log('Created new instance of UsersDao'); } } export default new UsersDao(); سنستخدم في هذا الصنف نمط التصميم المتفرد singleton وبالتالي سيقدم هذا الصنف نفس النسخة، ونفس مصفوفة المستخدمين users عندما ندرجه ضمن ملفات أخرى. والسبب أن Node.js تخزّن هذا الملف مؤقتًا كلما أُدرج، وتجري كل عمليات الإدراج عند إقلاع التطبيق. أي سيُسلم كل ملف يشير إلى الملف users.dao.ts مرجعًا إلى النسخة ()new UsersDao التي صُدِّرت في أول مرة يعالج فيها Node.js هذا الملف. سنرى طريقة العمل هذه عندما نستخدم الصنف لاحقًا في المقال، ونستخدم هذا النمط من اﻷصناف الشائعة في TypeScript/Express.js مع تقدمنا في تطوير التطبيق. ملاحظة: من سلبيات استخدام نمط التصميم singleton صعوبة كتابة اختبارات وحدة لها، لكننا لن نلاحظ هذه السلبية في الكثير من حالات استخدامنا لهذه الأصناف، لأنها لا تضم متغيرات أعضاء تحتاج إلى إعادة ضبط قيمها. أما بالنسبة للحالات التي يجب فيها إعادة ضبط المتغيرات اﻷعضاء في singleton سنترك اﻷمر كتمرين للقارئ كي يفكّر في انتهاج طريقة للحل تعتمد على فكرة حقن الاعتمادية dependency injection. أما اﻵن، سنضيف العمليات اﻷساسية للتعامل مع قواعد البيانات CRUD إلى الصنف كدوال، وستكون بداية دالة إنشاء مستخدم كالتالي: async addUser(user: CreateUserDto) { user.id = shortid.generate(); this.users.push(user); return user.id; } وستأتي دالة استرجاع أسماء المستخدمين بأسلوبين: اﻷول هو "قراءة كل الموارد (جميع المستخدمين المسجلين)" واﻵخر "استرداد مستخدم من خلال المعرّف ID فقط": async getUsers() { return this.users; } async getUserById(userId: string) { return this.users.find((user: { id: string }) => user.id === userId); } أما الدالة التي تحدّث سجلات المستخدمين فقد تعيد كتابة الكائن بالكامل (الاستعلام PUT) أو جزء منه (PATCH? async putUserById(userId: string, user: PutUserDto) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); this.users.splice(objIndex, 1, user); return `${user.id} updated via put`; } async patchUserById(userId: string, user: PatchUserDto) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); let currentUser = this.users[objIndex]; const allowedPatchFields = [ 'password', 'firstName', 'lastName', 'permissionLevel', ]; for (let field of allowedPatchFields) { if (field in user) { // @ts-ignore currentUser[field] = user[field]; } } this.users.splice(objIndex, 1, currentUser); return `${user.id} patched`; } وكما ذكرنا سابقًا، فعلى الرغم من التصريح عن UserDto في طريقة تعريف الدوال السابقة، لا يقدم TypeScript أي طريقة للتحقق من اﻷنواع في زمن التنفيذ، ويعني ذلك أن: الدالة ()putUserById ستحتوي ثغرة تسمح لمستخدمي الواجهة البرمجية بتخزين قيم لحقول ليست جزءًا من النموذج الذي يعرّفه كائن DTO. ()patchUserById تعتمد هذه الدالة على على قائمة مكررة من أسماء الحقول والتي يجب أن تبقى متزامنة مع النموذج. وبدون وجود تزامن بين هذه القائمة والنموذج قد يستخدم النموذج القائمة التي يمثلها الكائن الذي حُدِّث، وبالتالي سيتجاهل قيم الحقول التي هي في الواقع جزء من النموذج الذي عرّفه كائن DTO لكنها لم تخزّن ضمنه سابقًا. سنعالج هاتين الحالتين بالشكل الصحيح لاحقًا عندما نتعامل مع تطبيقنا على مستوى قاعدة البيانات. أما العملية اﻷخيرة فهي عملية الحذف، وستكون دالتها كالتالي: async removeUserById(userId: string) { const objIndex = this.users.findIndex( (obj: { id: string }) => obj.id === userId ); this.users.splice(objIndex, 1); return `${userId} removed`; } وكنقطة إضافية، نعلم أن من شروط التسجيل الصحيح لمستخدم جديد هو عدم تكرار البريد اﻹلكتروني، لهذا سنضيف الدالة getUserByEmail: async getUserByEmail(email: string) { const objIndex = this.users.findIndex( (obj: { email: string }) => obj.email === email ); let currentUser = this.users[objIndex]; if (currentUser) { return currentUser; } else { return null; } } ملاحظة: في الحالات الحقيقة، قد تتصل بقاعدة البيانات من خلال مكتبات موجودة مسبقًا مثل Mongoose و Sequelize والتي تقدّم آلية لتنفيذ كل العمليات اﻷساسية التي تحتاجها. لهذا لن نتوسّع في شرح طريقة إنجاز الدوال السابقة. طبقة الخدمات في الواجهة البرمجية REST لتطبيقنا بعد أن أنشأنا كائن DAO أساسي مقيم في الذاكرة، بإمكاننا إنشاء خدمة تستدعي دوال CRUD. وطالما أن هذه الدوال مطلوبة لكل خدمة تتصل بقاعدة بيانات، سننشئ الواجهة CRUD التي تضم التوابع التي نحتاجها في كل مرة ننفذ فيها خدمة جديدة. تتمتع معظم بيئات التطوير المتكاملة التي نعمل عليها حاليًا ميزة توليد الشيفرة التي تمكننا من إضافة الدوال التي ننجزها في كل مرة نحتاجها مما يقلل كمية الشيفرة المكررة التي علينا كتابتها. أنشئ ضمن المجلد common مجلدّا بالاسم interfaces ثم أنشئ الملف crud.interface.ts وأضف إليه الشيفرة التالية: export interface CRUD { list: (limit: number, page: number) => Promise<any>; create: (resource: any) => Promise<any>; putById: (id: string, resource: any) => Promise<string>; readById: (id: string) => Promise<any>; deleteById: (id: string) => Promise<string>; patchById: (id: string, resource: any) => Promise<string>; } لننشئ اﻵن المجلد services ضمن المجلد users وضمنه الملف users.service.ts ونزوّده بالشيفرة التالية: import UsersDao from '../daos/users.dao'; import { CRUD } from '../../common/interfaces/crud.interface'; import { CreateUserDto } from '../dto/create.user.dto'; import { PutUserDto } from '../dto/put.user.dto'; import { PatchUserDto } from '../dto/patch.user.dto'; class UsersService implements CRUD { async create(resource: CreateUserDto) { return UsersDao.addUser(resource); } async deleteById(id: string) { return UsersDao.removeUserById(id); } async list(limit: number, page: number) { return UsersDao.getUsers(); } async patchById(id: string, resource: PatchUserDto) { return UsersDao.patchUserById(id, resource); } async readById(id: string) { return UsersDao.getUserById(id); } async putById(id: string, resource: PutUserDto) { return UsersDao.putUserById(id, resource); } async getUserByEmail(email: string) { return UsersDao.getUserByEmail(email); } } export default new UsersService(); كانت خطوتنا الأولى إدراج الكائن DAO ثم اعتماديات الواجهة ثم نوع TypeScript الخاص بكل كائن DTO. سنعمل اﻵن على إنجاز الخدمة UserService كصنف متفرّد كما فعلنا مع الكائن DAO. تستدعي جميع دوال الواجهة CRUD الدوال التي تقبلها من UsersDao، وبالتالي عندما يحين الوقت لاستبدال الكائن DAO، لن نغيّر أي شيء في المشروع، ما عدا بعض التعديلات في هذا الملف حيث تُستدعى دوال DAO. أي لن نضطر إلى تتبع كل استدعاء للدالة ()list مثلًا والتجقق من محتواها قبل استبدالها، وهذه هي فائدة هذه الطبقة مقابل بعض الشيفرة اﻷساسية البسيطة التي رأيتها سابقًا. التعليمتان Async/Await في Node.js قد ترى أن استخدام async مع دوال الخدمة بلا معنى، وهذا صحيح حاليًا. فجميع هذه الدوال تعيد قيمها مباشرة، دون استخدام الوعود promise أو await داخليًا. لكننا فقط أردنا تحضير الشيفرة اﻷساسية للخدمات التي ستستخدم async. وبالمثل ستجد أن جميع الاستدعاءات لهذه الدوال تستخدم await. ستجد في نهاية المقال تطبيقًا قابلًا للتنفيذ والتجريب. وسيكون من الجيد توليد أخطاء مختلفة في أماكن مختلفة من الشيفرة، ومراقبة ما يحدث عند التصريف والاختبار، مع الانتباه إلى أن اﻷخطاء في استخدام async بالتحديد لن تظهر كما قد تتوقع! و اﻵن وقد انتهينا من إنجاز الكائن DAO والخدمات، سنعود إلى المتحكم بالمستخدم. بناء متحكم خاص بالواجهة البرمجية REST إن الفكرة من استخدام المتحكمات كما أشرنا سابقًا هي فصل إعدادات الوجهات عن الشيفرة التي تعالج في النهاية الطلب الذي يصل إلى الوجهة المطلوبة. وبالتالي لا بد أن تجري جميع عمليات التقييم قبل أن يصل الطلب إلى المتحكم. وكل ما يحتاجه المتحكم هو معرفة ما الذي سيفعله مع الطلب الفعلي، لأن الطلب الذي وصل إلى هذه المرحلة لابد وأن يكون صالحًا. يستدعي المتحكم بعد ذلك الخدمة التي تتوافق مع كل طلب تتعامل معه. علينا قبل أن نبدأ تثبيت مكتبة لتأمين تشفير كلمة المرور: npm i argon2 لنبدأ بإنشاء مجلد يُدعى controllers ضمن المجلد users وننشئ ضمنه الملف users.controller.ts: //ﻹضافة الأنواع إلى كائنات الطلب والاستجابة express ندرج //العائدة إلى دوال المتحكم import express from 'express'; //ندرج خدمة المستخدم التي أنشأناها مؤخرًا import usersService from '../services/users.service'; //لتشفير كلمة المرور argon2 ندرج المكتبة import argon2 from 'argon2'; //وفق سياق مخصص كما شرحنا في المقال السابق debug نستخدم المكتبة import debug from 'debug'; const log: debug.IDebugger = debug('app:users-controller'); class UsersController { async listUsers(req: express.Request, res: express.Response) { const users = await usersService.list(100, 0); res.status(200).send(users); } async getUserById(req: express.Request, res: express.Response) { const user = await usersService.readById(req.body.id); res.status(200).send(user); } async createUser(req: express.Request, res: express.Response) { req.body.password = await argon2.hash(req.body.password); const userId = await usersService.create(req.body); res.status(201).send({ id: userId }); } async patch(req: express.Request, res: express.Response) { if (req.body.password) { req.body.password = await argon2.hash(req.body.password); } log(await usersService.patchById(req.body.id, req.body)); res.status(204).send(); } async put(req: express.Request, res: express.Response) { req.body.password = await argon2.hash(req.body.password); log(await usersService.putById(req.body.id, req.body)); res.status(204).send(); } async removeUser(req: express.Request, res: express.Response) { log(await usersService.deleteById(req.body.id)); res.status(204).send(); } } export default new UsersController(); ملاحظة: تعيد اﻷسطر السابقة الاستجابة HTTP 204 No Content وتعني أن الطلب قد أنجز، لكن لا يوجد محتوى إضافي لإعادته مع جسم الاستجابة. بعد اﻹنتهاء من كتابة شيفرة المتحكم على شكل متفرّد، أصبحنا جاهزين لكتابة شيفرة الوحدة الوسيطة، وهي الوحدة البرمجية اﻷخرى التي تعتمد على نموذج كائن الواجهة البرمجية REST التجريبي وخدمته وهي الأداة الوسيطة. أداة وسيطة REST باستخدام Node.js و Express.js ما الذي يمكن أن تقدمه أداة وسيطة مبنية باستخدام Express.js ؟ بداية عمليات التحقق من صحة البيانات وهذا أمر شديد اﻷهمية، لنبدأ إذًا بإضافة آليات تحقق بسيطة من الطلبات قبل وصولها إلى متحكم المستخدم: التأكد من وجود حقول معينة لبيانات المستخدم مثل email و password وهي ضرورية ﻹنشاء مستخدم أو تحديث بياناته. التأكد من عدم استخدام البريد اﻹلكتروني المدخل من قبل. التحقق من عدم تغيير حقل البريد اﻹلكتروني بعد إنشاء المستخدم (لأننا سنستخدمه للسهولة كمعرّف أساسي للمستخدم). التحقق من وجود مستخدم محدد مسبقًا. ولتعمل آليات التحقق السابقة مع Express.js، لابد من كتابتها على شكل دوال تتوافق مع نمط Express.js وذلك ﻹدارة نقل التحكم باستخدام الدالة ()next كما شرحنا في المقال السابق. لهذا سنتحتاج إلى ملف جديد users/middleware/users.middleware.ts نضع فيه الشيفرة التالية: import express from 'express'; import userService from '../services/users.service'; import debug from 'debug'; const log: debug.IDebugger = debug('app:users-controller'); class UsersMiddleware { } export default new UsersMiddleware(); نضيف اﻵن بعض دوال اﻷداة الوسيطة إلى جسم الصنف: async validateRequiredUserBodyFields( req: express.Request, res: express.Response, next: express.NextFunction ) { if (req.body && req.body.email && req.body.password) { next(); } else { res.status(400).send({ error: Missing required fields email and password, }); } } async validateSameEmailDoesntExist( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.getUserByEmail(req.body.email); if (user) { res.status(400).send({ error: User email already exists }); } else { next(); } } async validateSameEmailBelongToSameUser( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.getUserByEmail(req.body.email); if (user && user.id === req.params.userId) { next(); } else { res.status(400).send({ error: Invalid email }); } } //بالشكل الصحيح this نستخدم هنا الدالة السهمية كي نربط التعليمة validatePatchEmail = async ( req: express.Request, res: express.Response, next: express.NextFunction ) => { if (req.body.email) { log('Validating email', req.body.email); this.validateSameEmailBelongToSameUser(req, res, next); [إضغط و إسحب للتحريك] } else { next(); } }; async validateUserExists( req: express.Request, res: express.Response, next: express.NextFunction ) { const user = await userService.readById(req.params.userId); if (user) { next(); } else { res.status(404).send({ error: User ${req.params.userId} not found, }); } } ولكي نسهل على مستثمري الواجهة البرمجية تنفيذ طلبات إضافية على المستخدم الذي أنشئ حديثًا، سننشئ دالة مساعدة تستخلص الحقل userId من معاملات الطلب التي تصل من عنوان URL للطلب نفسه، ومن ثم نضيف الحقل إلى جسم الطلب، حيث تتواجد بقية بيانات المستخدم. والغاية من ذلك هي استخدام جسم الطلب كاملًا عندما نريد تحديث معلومات المستخدم، دون القلق من ضرورة الحصول على معرّف المستخدم في كل مرة، بل ستهتم اﻷداة الوسطية بهذا الموضوع. ستبدو الدالة بالشكل التالي: async extractUserId( req: express.Request, res: express.Response, next: express.NextFunction ) { req.body.id = req.params.userId; next(); } إضافة إلى منطق التنفيذ، ستجد أن الاختلاف الرئيسي بين اﻷداة الوسيطة المتحكم هو استخدام اﻷداة الوسيطة الدالة ()next لتمرير التحكم عبر سلسلة من الدوال المهيّأة مسبقًا حتى يصل إلى وجهته النهائية وهي المتحكم في حالتنا. تجميع كل الوحدات: إعادة تشكيل الوجهات بعد أن انتهينا من إنجاز مختلف نواحي معمارية التطبيق، سنعود إلى الملف users.routes.config.ts الذي عرّفناه في المقال السابق، والذي يستدعي اﻷداة الوسيطة والمتحكمات وكلاهما يعتمد على خدمة المستخدم والتي تتطلب بدورها نموذج المستخدم. سيكون الملف بشكله النهائي كالتالي: import { CommonRoutesConfig } from '../common/common.routes.config'; import UsersController from './controllers/users.controller'; import UsersMiddleware from './middleware/users.middleware'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } configureRoutes(): express.Application { this.app .route(`/users`) .get(UsersController.listUsers) .post( UsersMiddleware.validateRequiredUserBodyFields, UsersMiddleware.validateSameEmailDoesntExist, UsersController.createUser ); this.app.param(`userId`, UsersMiddleware.extractUserId); this.app .route(`/users/:userId`) .all(UsersMiddleware.validateUserExists) .get(UsersController.getUserById) .delete(UsersController.removeUser); this.app.put(`/users/:userId`, [ UsersMiddleware.validateRequiredUserBodyFields, UsersMiddleware.validateSameEmailBelongToSameUser, UsersController.put, ]); this.app.patch(`/users/:userId`, [ UsersMiddleware.validatePatchEmail, UsersController.patch, ]); return this.app; } } أعدنا هنا تعريف الوجهات بإضافة أداة وسيطة لتقييم منطق العمل واختيار دوال المتحكم المناسبة لمعالجة الطلب إن كان كل شيء صحيحًا. كما استخدمنا الدالة ()param التي تقدمها Express.js لاستخلاص قيمة الحقل userId. كما مررنا الدالة validateUserExists العائدة للأداة الوسيطة UserMiddleware في جميع الدوال ()all. كي تُستدعى قبل وصول أي طلب GET أو PUT أو PATCH أو DELETE إلى نقطة الوصول user/:usersId/. أي لا حاجة أن تكون validateUserExists ضمن مصفوفة الدوال اﻹضافية التي نمررها إلى ()put. أو ()patch.، إذ تُستدعى قبل هذه الدوال. كما عززنا قابلية الاستخدام المتكرر للأداة الوسيطة بطريقة أخرى أيضًا، وذلك بتمرير الدالة UsersMiddleware.validateRequiredUserBodyFields كي تُستخدم ضمن سياق استخدام POST و PUT، إذ نعيد دمجها في دوال وسيطة أخرى. تنبيه ﻹخلاء المسؤولية: ما فعلناه اﻵن هو آلية بسيطة للتحقق من صحة المدخلات، لكن عليك التفكير بكل القيود التي يجب وضعها في الشيفرة عندما تتعامل مع مشاريع حقيقية. ولكي نتوخى البساطة، افترضنا أن المستخدم ليس قادرا على تغيير بريده اﻹلكتروني. اختبار الواجهة البرمجية REST المبنية باستخدام Express/TypeScript نستطيع اﻵن تصريف وتشغيل تطبيق Node.js، وبمجرد أن يعمل سنكون قادرين على اختبار وجهات الواجهة البرمجية باستخدام عميل REST مثل Postman أو cURL. سنجرّب أولاً الحصول على قائمة المستخدمين: curl --request GET 'localhost:3000/users' \ --header 'Content-Type: application/json' إن الاستجابة التي سنحصل عليها حاليًا هي مصفوفة فارغة، وهذا صحيح، لذك علينا إنشاء المستخدم الأول كالتالي: curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json' لاحظ كيف ستكون النتيجة هي خطأ يرسله التطبيق من خلال اﻷداة الوسيطة: { "error": "Missing required fields email and password" } وﻹصلاح اﻷمر، سنرسل طلبًا صحيحًا إلى المورد users/: curl --request POST 'localhost:3000/users' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "marcos.henrique@toptal.com", "password": "sup3rS3cr3tPassw0rd!23" }' سنرى هذه المرة استجابة شبيهة بالتالي: { "id": "ksVnfnPVW" } إن هذا المعرف id هو المعرّف الخاص بالمستخدم الجديد وقد يكون مختلفًا على جهازك. ولتسهيل بقية الاختبارات، يمكنك تنفيذ بقية اﻷوامر باستخدام المعرّف الذي حصلت عليه (على افتراض أنك تستخدم بيئة تشغيل شبيه بنظام لينكس): REST_API_EXAMPLE_ID="put_your_id_here" بإمكانك أن ترى اﻵن الاستجابة التي تحصل عليها عند تنفيذ الطلب GET باستخدام المتغّير السابق: curl --request GET "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' وتستطيع أيضًا تعديل المورد (المستخدم) بأكمله من خلال تنفيذ الطلب PUT: curl --request PUT "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "marcos.henrique@toptal.com", "password": "sup3rS3cr3tPassw0rd!23", "firstName": "Marcos", "lastName": "Silva", "permissionLevel": 8 }' كما تستطيع اختبار آلية التحقق بتغيير عنوان البريد اﻹلكتروني، ومن المفترض عندها ظهور رسالة خطأ. لاحظ أيضًا أن استخدام الطلب PUT لتحديث مورد ذو معرّف محدد، لا بد لنا -كمستخدمين للواجهة البرمجية- أن نرسل كائن الطلب بأكمله كي يتوافق مع معايير نموذج REST. فلو أردنا مثلًا تعديل الاسم اﻷخير فقط lastName، باستخدام PUT، لا بد من إرسال الكائن بأكمله لتنجح عملية التحديث. لكن من السهل في حالة كهذه استخدام الطلب PATCH لأنه يعمل ضمن قيود REST، وبإمكانك عندها إرسال قيمة lastName فقط: curl --request PATCH "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' \ --data-raw '{ "lastName": "Faraco" }' وتذكر أن التمييز بين PUT و PATCH في شيفرتنا اﻷساسية عائد إلى أسلوب إعداد الوجهات عن طريق استخدام دوال الأداة الوسيطة التي أضفناها. هل نستخدم PUT أو PATCH أو كلاهما؟ قد ترى أنه لا ضرورة ملحة لاستخدام PUT نظرًا لمرونة PATCH، وبالفعل تتبنى بعض الواجهات البرمجية الفكرة. وقد يصّر البعض على دعم PUT كي تتوافق الواجهة البرمجية تمامًا مع REST. مع ذلك، قد يكون إنشاء وجهات تدعم PUT أمرًا مناسبًا لبعض حالات الاستخدام الشائعة. وفي واقع اﻷمر هذه النقاط هي موضع نقاشات أعمق، لهذا دعمنا في تطبيقنا استخدام PUT وكذلك PATCH، لكننا نشجعك على الاطلاع والبحث أكثر عندما تكون مستعدًا. إن حاولت الحصول على قائمة المستخدمين مجددًا، سترى المستخدم الجديد وقد حّدثت بياناته: [ { "id": "ksVnfnPVW", "email": "marcos.henrique@toptal.com", "password": "$argon2i$v=19$m=4096,t=3,p=1$ZWXdiTgb922OvkNAdh9acA$XUXsOHaRN4uVg5ltIwwO+SPLxvb9uhOKcxoLER1e/mM", "firstName": "Marcos", "lastName": "Faraco", "permissionLevel": 8 } ] بإمكاننا أخيرًا اختبار حذف مستخدم كالتالي: curl --request DELETE "localhost:3000/users/$REST_API_EXAMPLE_ID" \ --header 'Content-Type: application/json' إن حاولت اﻵن الحصول على قائمة المستخدمين مجددًا، فلن ترى المستخدم الذي أنشأته سابقًا. وهكذا نكون قد أنجزنا جميع العمليات اﻷساسية CRUD. الخلاصة استكشفنا في هذا المقال المكمّل للمقال السابق مفاهيم أساسية في بناء واجهة برمجية REST باستخدام Express.js. إذ جزءنا شيفرتنا إلى خدمات وأداة وسيطة ومتحكمات ونماذج بيانات، ولكل منها دوال تنفذ مهامًا محددة كالتحقق من صحة المدخلات وتنفيذ عمليات منطقية أو معالجة الطلبات الصحيحة والاستجابة لها. كما أنشأنا بنية شديدة البساطة لتخزين البيانات هدفها الوحيد تنفيذ بعض الاختبارات في هذه المرحلة، ومن ثم ستُستبدل بشيء عملي أكثر في مقالات قادمة. وما سنتعرف عليه في المقال القادم، بعد توخي البساطة الشديدة في إنشاء واجهتنا البرمجية، هو خطوات إضافية لجعل التطبيق أسهل صيانة وأكثر قابلية للتوسع وكذلك أكثر أمانًا مثل: استبدال قاعدة البيانات المؤقتة بقاعدة بيانات MongoDB، واستخدام المكتبة Mongoose لتسهيل كتابة الشيفرة. إضافة طبقة أمان والتحكم بالوصول من خلال مقاربة لا تعتمد على حالة التطبيق باستخدام JWT. إعداد اختبارات مؤتمتة تسمح لنا بتوسيع تطبيقنا. بإمكانك اﻵن الاطلاع على الشيفرة النهائية حتى هذه المرحلة من هنا. ترجمة -وبتصرف- للمقال Building a Node.js TypeScript REST API,Part2: Models, Middleware and Services اقرأ أيضًا المقال السابق: بناء واجهة برمجية متوافقة مع REST باستخدام Express.js -الجزء الأول دليلك لربط واجهة OpenAI API مع Node.js مدخل إلى إطار عمل الويب Express وبيئة Node
  15. ألقينا نظرة في المقالات السابقة على النظرية اﻷساسية لكائنات جافا سكريبت وصياغتها بشيء من التفصيل كي تمتلك قاعدة صلبة تبنى عليها معارفك. وما سنفعله في هذا المقال هو التعمق أكثر من خلال تمرين تطبيقي للتمرن على بناء كائنات جافا سكريبت خاصة بك، وفيها سننشئ بعض الكرات الملونة المرتدة. ننصحك قبل أن تبدأ العمل معنا في هذه السلسلة أن تطلع على: أساسيات HTML. أساسيات عمل CSS أساسيات جافا سكريبت كما شرحناها في سلسلة المقالات السابقة. أساسيات البرمجة كائنية التوجه في جافا سكريبت OOJS كما شرحناها في مقال أساسيات البرمجة كائنية التوجه في جافا سكريبت، ومقال كائنات Prototype في جافا سكريبت. تطبيق الكرات المرتدة في جافا سكريبت سنكتب في هذا التطبيق شيفرة برنامج تجريبي بعنوان "الكرات المرتدة Bouncing Balls" لاستعراض مدى فائدة الكائنات في كتابة برامج جافا سكريبت. سترتد الكرات في تطبيقنا عن حواف الشاشة، وتغير ألوانها عندما تتلامس أو تصدم مع بعضها البعض، وسيبدو شكل التطبيق عندما ينتهي مشابهًا للصورة التالية: يستخدم التطبيق الواجهة البرمجية Canvas لرسم الكرات على الشاشة، والواجهة لتحريك المشهد بأكمله. لا حاجة لأي معرفة مسبقة بالتعامل مع هذه الواجهات البرمجية، على أمل أن يدفعك العمل خلال فترة إنجاز التطبيق إلى استكشاف هاتين الواجهتين أكثر. ستستخدم أيضًا بعض الكائنات اﻷنيقة، ونستعرض تقنيات جميلة مثل جعل كرة ترتد عن جدار، والتحقق من تصادم كرتين (وتعرف باكتشاف التصادم collision detection). نقطة الانطلاق لتبدأ العمل، انسخ الملفات index.html و style.css و main.js إلى حاسوبك، وتتضمن هذه الملفات: ملف HTML بسيط يضم العنصر <h1> والعنصر <canvas> لرسم الكرات وعناصر لتطبيق تنسيقات CSS وشيفرة جافا سكريبت على صفحة HTML. بعض التنسيقات البسيطة جدًا لتنسيق وضبط موقع العنصر، والتخلص من حواشي الصفحة وأشرطة التمرير لتظهر بمظهر أنيق. ملف يضم بعض شيفرة جافا سكريبت ﻹعداد العنصر <canvas> وتحضير دالة عامة سنستخدمها في التطبيق. يبدو القسم اﻷول من الشيفرة كالتالي: const canvas = document.querySelector("canvas"); const ctx = canvas.getContext("2d"); const width = (canvas.width = window.innerWidth); const height = (canvas.height = window.innerHeight); تعيد الشيفرة السابقة مرجعًا إلى العنصر <canvas>، ثم تستدعي التابع العائد لهذا العنصر كي يمهد مشهدًا نرسم ضمنه، وتخزّن نتيجة استدعاء التابع ضمن الثابت ctx الذي يمثل منطقة الرسم مباشرة، والتي تسمح لنا برسم أشكال ثنائية البعد ضمنها. نُعد تاليًا الثابتين width و height اللذين يمثلان اتساع وارتفاع لوحة الرسم (أي قيمتي الخاصيتين canvas.width وcanvas.height) ليكونا مساويين لارتفاع واتساع نافذة عرض المتصفح (المنطقة التي تُعرض عليها صفحة الويب) ونحصل عليهما من التعليمتين Window.innerWidth و Window.innerHeight. لاحظ كيف ربطنا عدة عمليات إسناد معًا لتسريع عملية ضبط قيم المتغيرات، وهذا أمر صحيح تمامًا. لدينا بعد ذلك دالتين مساعدتين: function random(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } function randomRGB() { return `rgb(${random(0, 255)},${random(0, 255)},${random(0, 255)})`; } الأولى هي الدالة ()random التي تأخذ وسيطين وتعيد عددًا عشوائيًا بين قيمتي الوسيطين، والثانية هي ()randomRGB وتولّد لونًا عشوائيًا على شكل نص ناتج عن تنفيذ الدالة ()rgb. نمذجة كرة في تطبيقنا يعرض تطبيقنا الكثير من الكرات التي ترتد عن حواف الشاشة، وطالما أن هذه الكرات ستسلك نفس السلوك، فمن المنطقي أن نمثلها على شكل كائن. لنبدأ بإضافة تعريف الصنف التالي في نهاية الشيفرة الموجودة: class Ball { constructor(x, y, velX, velY, color, size) { this.x = x; this.y = y; this.velX = velX; this.velY = velY; this.color = color; this.size = size; } } يضم الصنف حتى اللحظة دالة بانية فقط، مهمتها تهيئة الخاصيات التي تحتاجها كل كرة حتى تعمل كما هو مطلوب في التطبيق: x و y هما الإحداثيين اﻷفقي والعمودي للنقطة التي تنطلق منها الكرة على الشاشة. يمكن أن تبدأ النقاط من النقطة (0,0) وهي أعلى ويسار نافذة العرض، حتى النقطة التي تمثل أقصى اتساع وأقصى ارتفاع لنافذة عرض المتصفح وهي أدنى ويمين نافذة العرض. السرعتين اﻷفقية velX والعمودية velY: إذ تُعطى لكل كرة سرعتين أفقية وعمودية. وما نفعله في الواقع هو إضافة هاتين القيمتين بانتظام إلى اﻹحداثيين x و y عند تحريك الكرة، وذلك لدفعها بهذا المقدار في كل إطار للحركة. اللون color: وهو اللون الذي تأخذه الكرة. القياس size: وهو قياس الكرة، ويُمثَّل بنصف قطرها. تمثل القيم السابقة خاصيات الكائن، لكن ماذا عن التوابع؟ فنحن نريد للكرات أن تسلك سلوكًا ما في التطبيق! رسم الكرة أضف بداية التابع ()draw إلى الصنف Ball: draw() { ctx.beginPath(); ctx.fillStyle = this.color; ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI); ctx.fill(); } سنجعل الكرة ترسم نفسها على الشاشة باستخدام هذا التابع، وذلك باستدعاء سلسلة من أعضاء مشهد الرسم ثنائي البعد الذي عرفناه سابقًا ctx. ومشهد الرسم شبيه بصفحة، ونطلب من القلم الذي يمثله التابع السابق رسم شيء ما. نستخدم أولًا التابع ()beginPath لنوضح أننا نريد رسم شكل ما على اللوحة. نستخدم تاليًا التابع fillStyle لنحدد لون الشكل، وضبطناه على قيمة الخاصية color للكرة. نستخدم بعدها التابع ()arc لرسم قوس على الصفحة، وللتابع المعاملات التالية: x و y: وهما إحداثيا مركز القوس، ونضبطهما من قبل الخاصيتين x و y للكرة. نصف قطر القوس: ونضبطه على قيم الخاصية size للكرة. يحدد آخر معاملين زاوية بداية ونهاية القوس على الدائرة (مقدرين بالراديان). وفي حالتنا كانت قيمة البداية 0 والنهاية 2*PI التي تعادل 360 درجة. وهكذا سنتمكن من رسم كامل الدائرة، ولو كانت النهاية PI سنحصل على نصف دائرة (180 درجة). نستخدم أخيرًا التابع ()fill والذي يقتضي مبدئيًا إنهاء رسم المسار الذي بدأه التابع ()beginPath ومن ثم ملئ المساحة الناتجة باللون الذي خصصناه باستخدام التابع fillStyle. بإمكانك اختبار الكائن اﻵن: احفظ التغييرات التي أجريتها على الشيفرة وأعد تحميل ملف HTMl. افتح طرفية جافا سكريبت في المتصفح وأعد تحميل الصفحة كي تأخذ لوحة الرسم أبعاد نافذة عرض المتصفح الجديدة والتي تكون أصغر من اﻷصلية لأنها تُعرض مع الطرفية. اكتب الشيفرة التالية ﻹنشاء نسخة عن الكائن Ball: const testBall = new Ball(50, 100, 4, 4, "blue", 10); جرب استدعاء عناصر الكائن: testBall.x; testBall.size; testBall.color; testBall.draw(); عندما تكتب التعليمات السابقة، ستشاهد الكرة وقد رسمت نفسها على اللوحة. تحديث بيانات الكرة بإمكاننا رسم كرة انطلاقًا من نقطة محددة، لكن لتحريك الكرة فعليًا، سنحتاج إلى تابع لتحديث موضع الكرة. لهذا أضف الشيفرة التالية ضمن الصنف Ball: update() { if ((this.x + this.size) >= width) { this.velX = -(this.velX); } if ((this.x - this.size) <= 0) { this.velX = -(this.velX); } if ((this.y + this.size) >= height) { this.velY = -(this.velY); } if ((this.y - this.size) <= 0) { this.velY = -(this.velY); } this.x += this.velX; this.y += this.velY; } تتحقق اﻷجزاء اﻷربعة الأولى من التابع إن وصلت الكرة إلى أحد أطراف لوحة الرسم، فإن وصلت بالفعل نعكس جهة الحركة بتغيير إشارة السرعة. فلو تحركت الكرة مثلًا إلى اﻷعلى (بسرعة عمودية velY سالبة) عندها تتغير إشارة السرعة لتصبح موجبة كي تتحرك الكرة إلى اﻷسفل. نتحقق في أربع حالات إذا ما كان: اﻹحداثي x أكبر من اتساع لوحة الرسم، ويعني ذلك تجاوز الكرة إلى أقصى يمين اللوحة. اﻹحداثي x أصغر من 0، ويعني ذلك أن تجاوز الكرة إلى أقصى يسار اللوحة. اﻹحداثي y أكبر من ارتفاع لوحة الرسم، ويعني ذلك تجاوز الكرة إلى أسفل لوحة الرسم. اﻹحداثي y أصغر من 0، ويعني ذلك تجاوز الكرة إلى أعلى اللوحة. نضيف في كل مرة قياس الكرة إلى الحسابات لأن x و y يمثلان إحداثيي مركز الكرة والمطلوب هو وصول محيط الكرة إلى حافة الشاشة حتى ترتد عنها، ولا نريد أن يخرج نصف الكرة خارج الشاشة حتى ترتد. يضيف السطرين اﻷخيرن قيمتي velX و velY إلى قيمتي x و yعلى الترتيب كي تتحرك الكرة كل مرة نستدعي فيها هذا التابع. ما فعلناه كافٍ حتى اللحظة، لننتقل الآن إلى مرحلة تحريك الرسوم. تحريك الكرة سنبدأ اﻵن إضافة كرات إلى لوحة الرسم وتحريكها. ونحتاج بداية إلى إنشاء مكان نُخزن فيه كل الكرات ثم نطلقها، والشيفرة التالية ستؤدي المطلوب: const balls = []; while (balls.length < 25) { const size = random(10, 20); const ball = new Ball( // تُرسم الكرات دائمًا في موقع يبعد على اﻷقل مقدار اتساع كرة // عن حافة لوحة الرسم لتفادي أية أخطاء random(0 + size, width - size), random(0 + size, height - size), random(-7, 7), random(-7, 7), randomRGB(), size, ); balls.push(ball); } تُنشئ الحلقة while نسخة جديدة من الكائن باستخدام قيم عشوائية تولّدها الدالتان المساعدتان ()random و ()randomRGB ثم نستخدم التابع ()push لدفع الكرة الناتجة إلى نهاية مصفوفة الكرات، طالما أن عدد الكرات في المصفوفة أقل من 25 كرة. بإمكانك تغيير عدد الكرات اﻷعظمي في المصفوفة من التعليمة balls.length < 25 وذلك وفقًا لقدرة المعالجة التي يتمتع بها حاسوبك ومتصفحك، فاختيار عدة آلاف من الكرات قد تبطئ حركة الرسوم. أضف تاليًا الشيفرة التالية إلى نهاية الشيفرة الموجودة: function loop() { ctx.fillStyle = "rgb(0 0 0 / 25%)"; ctx.fillRect(0, 0, width, height); for (const ball of balls) { ball.draw(); ball.update(); } requestAnimationFrame(loop); } تتضمن معظم البرامج التي تحرّك الرسوم نوعًا من الحلقات، وذلك لتحديث المعلومات في البرنامج وتصيير نتيجة الحسابات وعرضها في كل إطار من إطارات المشهد. وهذا هو أساس معظم اﻷلعاب والبرامج المشابهة. وفي تطبيقنا تنفّذ الدالة ()loop ما يلي: يضبط لون خلفية لوحة الرسم على لون أسود وتتكون نصف شفافة، ومن ثم ترسم مربعًا من نفس اللون ليغطي أبعاد اللوحة باستخدام التابع ()fillRectangle (معاملاته هي إحداثيات نقطة بداية المربع واتساعه وارتفاعه). أما وظيفية هذا المربع فهو تغطية الإطار السابق قبل رسم الإطار التالي. فإن لم نفعل ذلك سنرى أفاعي طويلة تلتف ضمن اللوحة بدلًا من كرات تتحرك. أما سبب اللون نصف الشفاف للخلفية ( 25% / 0 0 0)rgb، فهو السماح للإطار السابق أن يظهر بشكل طفيف خلف اﻹطار الحالي لتعطي أثرًا خلف الكرة المتحركة. وإن جعلت قيمة الشفافية 1 بدلًا من 0.25، فلن ترى هذا اﻷثر. حاول أن تغير هذه القيمة وراقب ما يحدث. تتنقل بين جميع الكرات في مصفوفة الكرات balls، وينفّذ الدالتين ()draw و ()update لرسم كل كرة على الشاشة، ثم تنفّذ التحديثات اللازمة للموقع والسرعة في الوقت المناسب للإطار التالي. تشغّل نفسها مجددًا باستخدام التابع ()requestAnimationFrame الذي يُنفَّذ تكرارًا، وتُمرر له الدالة نفسها. ينفّذ التابع الدالة عدة مرات بالثانية لتكوين رسوميات متحركة سلسلة. وهذه العملة تُنفَّذ عادة بطريقة تعاودية recursively، أي تستدعي الدالة نفسها في كل مرة تُنفَّذ فيها، وهكذا تُنفَّذ مرارًا وتكرارًا. أضف أخيرًا السطر التالي إلى أسفل الشيفرة، إذ عليك استدعاء الدالة مرة حتى تبدأ عملية تحريك الرسوم: loop(); هذا هو المطلوب بدايةً، جرّب حفظ التغييرات التي أجريتها واختبر الكرات المرتدة. إضافة آلية لالتقاط التصادم بين الكرات سنضيف اﻵن آلية لاكتشاف التصادم بين الكرات في تطبيقنا، كي تعرف كراتنا أنها تصطدم ببعضها. أضف بداية التابع التالي لى الصنف Ball: collisionDetect() { for (const ball of balls) { if (this !== ball) { const dx = this.x - ball.x; const dy = this.y - ball.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < this.size + ball.size) { ball.color = this.color = randomRGB(); } } } } في التابع السابق بعض التعقيد، لذا لا تقلق إن لم تفهم تمامًا طريقة عمله، وإليك شرحه: نتحقق من أجل كل كرة إذا ما اصطدمت بالكرة الحالية. ولتنفيذ اﻷمر نبدأ حلقة for...of جديدة للتنقل بين جميع كرات المصفوفة []balls. نستخدم مباشرة في الحلقة for العبارة if للتحقق من أن الكرة التي وصلنا إليها هي نفسها الكرة الحالية التي نتفحصها. ولأننا لا نريد أن نتحقق إن اصطدمت كرة بنفسها، نتحقق أن الكرة الحالية (وهي الكرة التي استدعت التابع ()collisonDetect) هي نفسها الكرة التي وصلنا إليها في هذا التكرار من تكرارات الحلقة. نستخدم بعد ذلك عامل النفي ! في عبارة التحقق this !== ball كي لا تُنفَّذ الشيفرة داخل if إلا إن لم تكن الكرة هي نفسها. نستخدم بعد ذلك خوارزمية شائعة للتحقق من التصادم بين كرتين، وذلك عن طريق التحقق من تداخل مساحتي الكرتين. إن اكتُشف التصادم، تُنفَّذ الشيفرة ضمن عبارة if الثانية. وفي هذه الحالة نغيّر فقط لوني الكرتين المتصادمتين عشوائيًا. بإمكاننا أيضًا تعقيد التطبيق بجعل الكرات ترتد عن بعضها كما يجري اﻷمر في الواقع، لكن ذلك صعب التنفيذ. ولمحاكاة هذه العمليات الفيزيائية، يميل المطورّون إلى استخدام مكتبات جاهزة مثل Physics و mattre و Phaser وغيرها. لا بد أيضًا من استدعاء هذه الدالة من أجل كل إطار من إطارات المشهد المتحرك. عدّل الدالة لتستدعي التابع ()ball.collisionDetect بعد التابع ()ball.update: 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); } احفظ التغييرات وأعد تحميل التطبيق التجريبي، وسترى كيف تغيّر الكرات لونها عندما تتصادم. ملاحظة: إن واجهت صعوبة في تنفيذ هذا التطبيق، تستطيع مقارنة ما فعلته مع النسخة الجاهزة من الشيفرة، ويمكنك أيضًا تجريب النسخة العاملة. الخلاصة نتمنى أن تكون قد استفدت من التقنيات واﻷفكار التي قدمناه في هذا التمرين التطبيقي الذي بنيناه باستخدام كائنات مختلفة وفق أسلوب البرمجة كائنية التوجه. سيمنحك العمل في هذا المقال معرفة تطبيقية باستعمال الكائنات على أمثلة من الواقع. ترجمة -وبتصرف- للمقال: Object building practice اقرأ أيضًا المقال السابق: تعريف الأصناف في جافا سكريبت مشروع لعبة منصة باستخدام جافاسكربت معالجة الأحداث في جافا سكريبت إنشاء رسوم متحركة باستخدام CSS
  16. نتحدث في هذه السلسلة من المقالات عن خطوات بناء تطبيق بسيط يمثل واجهة خلفية على هيئة واجهة برمجية API باستخدام إطار عمل Express.js ولغة البرمجة TypeScript. كيف أكتب واجهة برمجية REST في بيئة Node.js غالبًا ما تكون المكتبة Express.js هي الخيار اﻷول من بين إطارات عمل Node.js عند كتابة واجهة خلفية لتكون واجهة برمجية REST. وعلى الرغم من أنها تدعم أيضًا بناء صفحات وقوالب HTML، لكننا سنركز في هذه السلسلة من المقالات على بناء واجهة خلفية باستخدام لغة TypeScript كي نسمح لأي واجهة أمامية أو واجهة خلفية خارجية (خادم آخر) من الاستعلام منها، لهذا عليك أن: تمتلك معرفة أساسية بلغة جافا سكريبت، وكذلك معرفة ببيئة عمل Node.js ومطلعًا على معمارية REST. تمتلك نسخة مثبّتة وجاهزة من بيئة Node.js (يفضل النسخة 14 وما بعد). سنبدأ من الطرفية أو (محرر سطر اﻷوامر) لإنشاء مجلّد خاص بالمشروع، ثم ننفّذ اﻷمر run npm init يُنشئ هذا الأمر بعض الملفات الأساسية التي نحتاجها لمشروع Node.js. ثم نضيف بعد ذلك إطار العمل Express.js وبعض المكتبات المفيدة اﻷخرى لمشروعنا من خلال الأمر التالي: npm i express debug winston express-winston cors وبالطبع هناك أسباب وجيهة كي يفضّل مطوّرو Node.js المكتبات السابقة: debug: هي وحدة برمجية تُستخدم لتفادي استخدام اﻷمر ()console.log أثناء تطوير التطبيقات. إذ تستخدم لترشيح العبارات التي نريد تنقيحها عند محاولة حل المشاكل التي تواجهنا. وباﻹمكان إيقافها كليًا في نسخة اﻹنتاج بدلًا من إزالتها يدويًا. winston: الوحدة المسؤولة عن تسجيل الطلبات القادمة إلى الواجهة البرمجية والاستجابات (واﻷخطاء) التي تعيدها الواجهة. وتتكامل express-winston مباشرة مع Express.js لهذا ستكون شيفرة واجهة برمجية المتعلقة بعملية إدارة السجلات التي تؤديها winston جاهزة. cors: هي جزء من أداة Express.js الوسيطة والتي تسمح لنا بمشاركة الموارد ذات اﻷصول المختلطة cross-origin resource sharing. وبدون هذه المكتبة لن تتمكن الواجهة البرمجية من تخديم سوى الواجهة الأمامية الموجودة في نفس النطاق الفرعي الذي يحوي الواجهة الخلفية. تستخدم الواجهة الخلفية في تطبيقنا تلك الحزم عند يعمل التطبيق، لهذا علينا تثبيت بعض اعتماديات مرحلة التطوير لتهيئة TypeScript. لهذا ننفذ الأمر التالي: npm i --save-dev @types/cors @types/express @types/debug source-map-support tslint typescript نحتاج الاعتماديات السابقة لتفعيل TypeScript في شيفرة تطبيقنا، واﻷنواع التي تستخدمها Express.js والاعتماديات اﻷخرى. وسيوّفر هذا اﻷمر وقتك عند استخدام بيئات تطوير متكاملة مثل WebStorm أو VSCode التي تتيح لك ميزات اﻹكمال التلقائي لبعض التوابع أثناء كتابة الشيفرة. ينبغي أن تكون الاعتمادات بشكلها النهائي ضمن الملف package.json كالتالي: "dependencies": { "debug": "^4.2.0", "express": "^4.17.1", "express-winston": "^4.0.5", "winston": "^3.3.3", "cors": "^2.8.5" }, "devDependencies": { "@types/cors": "^2.8.7", "@types/debug": "^4.1.5", "@types/express": "^4.17.2", "source-map-support": "^0.5.16", "tslint": "^6.0.0", "typescript": "^3.7.5" } وهكذا تكون جميع الاعتماديات اللازمة لعمل تطبيقنا جاهزة! هيكيلية مشروع واجهة برمجية REST باستخدام TypeScript سنستخدم في مشروعنا ثلاث ملفات وهي: app.ts/. common/common.routes.config.ts/. users/users.routes.config.ts/. إن الفكرة من استخدام مجلدين (common و users) في مشروعنا هو تكوين وحدتين لكل منهما مسؤولياتها الخاصة. وبالتالي قد نعطي الوحدتين بعض أو كل الميزات التالية: تهيئة الوجهة Rourte: لتعريف الطلبات التي يمكن أن تتعامل معها الواجهة البرمجية. خدمات Services: لتنفيذ مهام مثل الاتصال بقاعدة البيانات وتنفيذ استعلامات أو الاتصال بخدمات خارجية ضرورية للاستعلام. وسيط أو برمجية وسيطة middleware: للتحقق من صلاحية طلب معيّن قبل أن يتعامل المتحكم النهائي بالمسار مع تفاصيل الاستعلام. وحدات Models: لتعريف وحدات البيانات التي تطابق تخطيط قاعدة بيانات محددة، لتسهيل تخزين البيانات واستعادتها. متحكّمات controllers: لفصل معلومات تهيئة المسار أو الوجهة التي سننتقل إليها route configuration عن الشيفرة التي تعالج في النهاية (بعد المرور على أية برامج وسيطة) طلب هذا المسار أو تستدعي دوال خدمة من مستوى أعلى عند الحاجة، وتعيد الاستجابة على هذا الطلب إلى العميل. للمجلد هيكلية ذات تصميم بسيط متوافق مع الواجهة البرمجية. ملفات مسارات Routes شائعة الاستخدام في TypeScript سنعمل على تنظيم ملفات Routes في تطبيق Express.js ،لذا سننشئ الملف common.routes.config.ts في المجلّد common ونضع فيه الشيفرة التالية: import express from 'express'; export class CommonRoutesConfig { app: express.Application; name: string; constructor(app: express.Application, name: string) { this.app = app; this.name = name; } getName() { return this.name; } } إن الطريقة التي ننشئ فيها المسارات routes هنا اختيارية، لكن، وطالما أننا نعمل مع TypeScript، فمن الجيد أن نتدرب على بناء المسارات باستخدام الوراثة من خلال التعليمة extends كما سنرى بعد قليل. إذ ننشئ صنفًا رئيسيًا لتحديد سلوك وملامح مشتركة بين جميع مسارات التطبيق وسيكون لجميع الملفات في هذا المشروع السلوك ذاته، كما سيكون لها اسم وإمكانية الوصول إلى كائن Express.js اﻷساسي في تطبيقنا Application ثم ننشئ صنفًا فرعيًا منه لتحديد سلوك مسارات معينة. الآن، يمكننا أن نبدأ في إنشاء ملف مسارات المستخدمين في مجلد المستخدمين users، دعونا ننشئ ملف باسم users.routes.config.ts ونكتب بداخله الشيفرة البرمجية التالية import {CommonRoutesConfig} from '../common/common.routes.config'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } } هنا، نقوم باستيراد الصنف CommonRoutesConfig ونوسعه إلى صنف جديد UsersRoutes. ونرسل من خلال الدالة البنائية constructor التطبيق (أي كائن express.Application الرئيسي) واسم UsersRoutes إلى دالة البناء الخاصة بـ CommonRoutesConfig. قد يبدو المثال بسيطًا لكن عند توسيع الأمر ليشمل عدة ملفات routes سيساعدنا ذلك في تفادي تكرار الشيفرة. لنفترض أننا سنحتاج إلى إضافة ميزات جديدة في هذا الملف، مثل إدارة السجلات وتسجيل الأحداث، عندها بإمكاننا إضافة الحقول الضرورية إلى الصنف CommonRoutesConfig وعندها ستتمكن جميع اﻷصناف المشتقة منه من الوصول إلى هذه الميزة. استخدام دوال TypeScript المجرّدة لتقديم وظائف متشابه بين اﻷصناف ماذا لو أردنا الحصول على وظائف متشابهة ضمن اﻷصناف المختلفة (مثل تهيئة نقاط الاتصال بالواجهات البرمجية)، على الرغم من اختلاف طرق تنفيذ هذه الوظائف من صنف ﻵخر؟ أحد الخيارات المتاحة هو استخدام ميزة تُدعى التجريد abstraction في TypeScript. لنحاول إنشاء دالة مجرّدة بسيطة جدًا يرثها الصنف UsersRoutes (وبقية أصناف التوجيه التي قد ننشئها لاحقًا) من الصنف CommonRoutesConfig. ولنفترض أننا سنجبر كل الوجهات على امتلاك دالة (حتى نتمكن من استدعائها من الدالة البانية المشتركة) تُدعى()configureRoutes، وفيها نصرّح عن نقاط الوصول الخاصة بكل مورد من موارد الصنف. ولتنفيذ اﻷمر، سنضيف هذه اﻷشياء إلى الملف common.routes.config.ts: الكلمة المحجوزة abstract إلى السطر الذي يضم الكلمة class كي نفعّل خاصية التجريد لهذا الصنف. تصريح عن دالة جديدة في نهاية الصنف abstract configureRoutes(): express.Application، تجبر أي صنف مشتق من الصنف CommonRoutesConfig على تقديم آلية تطابق توقيع الدالة function signature، وسيرمي مصرّف TypeScript خطأ إن لم يجد آلية كهذه. استدعاء الدالة ()this.configureRoutes في نهاية الدالة البانية طالما أننا متأكدين من وجود هذه الدالة. ستبدو الشيفرة اﻵن كالتالي: import express from 'express'; export abstract class CommonRoutesConfig { app: express.Application; name: string; constructor(app: express.Application, name: string) { this.app = app; this.name = name; this.configureRoutes(); } getName() { return this.name; } abstract configureRoutes(): express.Application; } وهكذا ينبغي على كل صنف مشتق من الصنف CommonRoutesConfig أن يمتلك دالة ()configureRoutes تُدعى تُعيد كائنًا من النوع express.Application، وبالتالي لا بد من تحديث الملف users.routes.config.ts: import {CommonRoutesConfig} from '../common/common.routes.config'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } configureRoutes() { // (we'll add the actual route configuration here next) return this.app; } } دعنا نراجع ما فعلناه حتى الآن: أدرجنا بداية الملف common.routes.config ومن ثم الوحدة البرمجية express، وعرفنا بعد ذلك الصنف UserRoutes الذي أردناه أن يرث الصنف اﻷساسي CommonRoutesConfig وبالتالي سيضمّ الدالة ()configureRoutes ويقدّم آلية لتنفيذها. وﻹرسال المعلومات عبر الصنف CommonRoutesConfig، نستخدم الدالة البانية للصنف التي تتوقع تمرير كائن express.Application إليها، وهذا ما سنشرحه بتفاصيل أكثر لاحقًا. نمرر من خلال الدالة ()super التطبيق إلى الدالة البانية للصنف CommonRoutesConfig واسم الوجهة (وهي في هذه الحالة UsersRoutes). وتستدعي الدالة ()super بدورها الدالة ()configureRoutes. تهيئة وجهات Express.js الخاصة بنقاط الوصول إلى المستخدمين ستكون الدالة المكان الذي ننشئ فيه نقاط الوصول بين المستخدم والواجهة البرمجية REST. وفيها نستخدم التطبيق مع وظائف التوجيه من خلال Express.js. تكمن الفكرة في استخدام الدالة ()app.route لتفادي تكرار الشيفرة، وهذا اﻷمر سهل نسبيًا طالما أننا ننشئ واجهة برمجية REST ذات موارد محددة تمامًا. إن المورد اﻷساسي في تطبيقنا هو users، ولدينا حالتان: عندما يريد مستدعي الواجهة البرمجية إنشاء مستخدم جديد أو الحصول على قائمة بالمستخدمين الموجودين، لا بد أن يكون اسم المورد فقط users في نهاية المسار إلى المورد (لا نريد الخوض في هذه الحالة في فلترة أو تنظيم نتائج الاستعلام أو غيرها من العمليات في هذا التطبيق). عندما يريد المستدعي أن ينفّذ عملية ما على سجل مستخدم، وعندها لابد أن يكون نمط المسار إلى المورد كالتالي: users/:userId. تتيح آلية عمل الدالة ()route. في Express.js التعامل مع طلبات HTTP بأسلوب متسلسل أنيق، لأن جميع التوابع ()get. و ()post. وغيرها، ستعيد نفس النسخة من الكائن التي يعيدها التابع ()route.. لهذا سننهي عملية التهيئة كالتالي: configureRoutes() { this.app.route(`/users`) .get((req: express.Request, res: express.Response) => { res.status(200).send(`List of users`); }) .post((req: express.Request, res: express.Response) => { res.status(200).send(`Post to users`); }); this.app.route(`/users/:userId`) .all((req: express.Request, res: express.Response, next: express.NextFunction) => { // /users/:userId يُنفّذ البرنامج الوسيط هذه الدالة قبل أي استعلام // لكنه لا ينفذ شيئًا اﻵن // next() بل يمرر ببساطة التحكم إلى الدالة التالية في التطبيق تحت next(); }) .get((req: express.Request, res: express.Response) => { res.status(200).send(`GET requested for id ${req.params.userId}`); }) .put((req: express.Request, res: express.Response) => { res.status(200).send(`PUT requested for id ${req.params.userId}`); }) .patch((req: express.Request, res: express.Response) => { res.status(200).send(`PATCH requested for id ${req.params.userId}`); }) .delete((req: express.Request, res: express.Response) => { res.status(200).send(`DELETE requested for id ${req.params.userId}`); }); return this.app; } تتيح الشيفرة السابقة لعميل الواجهة البرمجية المتوافقة مع REST استدعاء نقطة الوصول users باستخدام أحد الاستعلامين POST أو GET، وتتيح له بنفس اﻷسلوب استدعاء نقطة الوصول users/:userId من خلال استعلامات GET أو PUT أو PATCH أو DELETE. كما أضفنا إلى نقطة الوصول users/:userId برنامج وسيط يستخدم الدالة ()all التي تُنفَّذ قبل أي استدعاء للدوال ()get أو ()put أو ()patch أو ()delete. وسيكون لهذه الدالة أهميتها عندما ننشئ لاحقًا مسارات يصل إليها فقط المستخدمين المستوثقين. وقد تلاحظ أن جميع الدوال ()all -وأية أجزاء من البرنامج الوسيط- تمتلك ثلاثة أنواع من الحقول Request و Response و NextFunction: النوع Request هو طريقة Express.js لتقديم طلبات HTTP التي يعالجها. ويُحدّث هذا النوع ويوسّع نوع Node.js اﻷصلي الذي يتعامل مع الطلبات. النوع Response هو طريقة Express.js لتقديم استجابات HTTP التي يعالجها. ويُحدّث هذا النوع ويوسّع نوع Node.js اﻷصلي الذي يتعامل مع الطلبات. كما يستخدم الحقل NextFunction الذي لا يقل أهمية عن الاثنين السابقين كدالة استدعاء تسمح بتمرير التحكم إلى أية دوال أخرى يضمها الوسيط. وتتشارك جميع البرامج الوسيطة نفس كائنات الطلب والاستجابة قبل أن يُرسل المتحكم الاستجابة إلى صاحب الطلب في النهاية. الملف app.ts المدخل إلى Node.js بعد أن وضعنا هيكلية بسيطة للتوجّه في التطبيق، ننتقل إلى تهيئة مدخل entry point إليه، لهذا سننشئ الملف app.ts في المجلد الجذري للمشروع ونبدؤه بالشيفرة التالية: import express from 'express'; import * as http from 'http'; import * as winston from 'winston'; import * as expressWinston from 'express-winston'; import cors from 'cors'; import {CommonRoutesConfig} from './common/common.routes.config'; import {UsersRoutes} from './users/users.routes.config'; import debug from 'debug'; هناك إدراجان فقط جديدان في هذا الملف هما: http: وهو وحدة برمجية أصلية في Node.js، نحتاجها في تشغيل تطبيق Express.js. body-parser: وهو وسيط يأتي مع Express.js، ويفسّر الطلب (صيغة JSON في حالتنا) قبل وصول التحكم إلى معالج الطلب الذي حددناه. ننتقل بعد إدراج الملفات إلى التصريح عن المتغيرات التي نريد استخدامها: const app: express.Application = express(); const server: http.Server = http.createServer(app); const port = 3000; const routes: Array<CommonRoutesConfig> = []; const debugLog: debug.IDebugger = debug('app'); تعيد الدالة ()express كائن تطبيق Express.js اﻷساسي الذي نمرره عبر تطبيقنا، من خلال إضافته بدايةً إلى الكائن http.Server (نحتاج إلى تشغيله بعد تهيئة الكائن express.Application الخاص بتطبيقنا) نترقب اﻵن الطلبات إلى المنفذ 3000 الذي تفهمه TypeScript على أنه من النوع Number بدلًا من المنافذ المعيارية مثل 80 لطلبات HTTP و 443 لطلبات HTTPS التي تُستخدم نمطيًا للاتصال مع الواجهة الأمامية للتطبيق. لماذا المنفذ 3000؟ لا توجد قاعدة تنص على أن المنفذ يجب أن يكون 3000، وسيُختار رقم المنفذ اعتباطيًا إن لم نخصص واحدًا. لكن الرقم 3000 يستخدم بكثرة في أمثلة توثيق Node.js و Express.js لهذا أكملنا على هذا النحو! هل يمكن أن تتشارك Node.js المنفذ مع الواجهه اﻷمامية؟ يمكننا أن نشغّل التطبيق محليًا على منفذ مخصص حتى لو أردنا من الواجهة الخلفية أن تستجيب للطلبات على المنافذ المعيارية. يتطلب اﻷمر خادم وكيل عكسي reverse proxy له نطاق رئيسي أو فرعي يستقبل الطلبات على أحد المنفذين 80 أو 443 ثم يعيد توجيهها إلى المنفذ الداخلي 3000. تتبع المصفوفة routes ملفات التوجيه الخاصة بنا لأغراض التنقيح كما سنرى، ونرى أخيرا كيف ينتهي debugLog بدالة مشابهة للدالة console.log، لكنها أفضل من ناحية إمكانية الضبط الدقيق، إذ تغطي تلقائيًا ما نريد أن ندعو به ملفاتنا أو وحداتنا البرمجية (دعوناه في حالتنا "app" عندما مررناه كنص إلى الدالة البانية ()debug). أصبحنا اﻵن جاهزين لتهيئة جميع وحدات Express.js الوسيطة والوجهات إلى الواجهة البرمجية: // JSON نضيف هنا وسيط لتفسير كل الطلبات القادمة بصيغة app.use(express.json()); // CORS نضيف هنا وسيطًا للسماح بالطلبات مختلطة الأصول app.use(cors()); //expressWinston نحضّر هنا إعدادات الوحدة الوسيطة المخصصة ﻹدارة التسجيل //Exprress.js التي تعالجها HTTP والتي تسجّل جميع طلبات const loggerOptions: expressWinston.LoggerOptions = { transports: [new winston.transports.Console()], format: winston.format.combine( winston.format.json(), winston.format.prettyPrint(), winston.format.colorize({ all: true }) ), }; if (!process.env.DEBUG) { loggerOptions.meta = false; // سجل الطلب على سطر واحد إن لم يكن التنقيح مفعّلًا } // هيئ المسجل بالإعدادات السابقة app.use(expressWinston.logger(loggerOptions)); //Express.js إلى مصفوفتنا بعد إرسال كائن UserRoutes نضيف //كي تضاف الوجهات إلى التطبيق routes.push(new UsersRoutes(app)); // هذه وجهة بسيطة للتأكد أن كل شيء يعمل كما هو مطلوب const runningMessage = `Server running at http://localhost:${port}`; app.get('/', (req: express.Request, res: express.Response) => { res.status(200).send(runningMessage) }); يرتبط expressWinston.logger تلقائيًا بالمكتبة Express.js ويسجل التفاصيل من خلال نفس البنية التحتية التي يستخدمها debug، وذلك لكل طلب مكتمل. وستنسق الخيارات التي مررناها إليه وتلوّن خرج الطرفية التي تعرض السجلات، إضافة إلى عرض سجلات أكثر تفصيلًا (وهو الأمر الافتراضي) عندما نفعّل نمط التنقيح. وتجدر الملاحظة إلى ضرورة تعريف وجهاتنا بعد إعداد expressWinston.logger. وأخيرًا نأتي إلى اﻷمر اﻷكثر أهمية: server.listen(port, () => { routes.forEach((route: CommonRoutesConfig) => { debugLog(`Routes configured for ${route.getName()}`); }); //console.log الحالة الوحيدة التي لن تحاشى فيها استخدام // هو معرّفة متى ينتهي الخادم من عملية اﻹقلاع console.log(runningMessage); }); تشغّل الشيفرة السابقة الخادم فعليًا، وعندما يبدأ تنفيذها يشغّل Node.js توابع الاستدعاء الخاصة بنا والتي تسجّل في وضع التنقيح أسماء كل الوجهات routes التي أعددناها وهي UsersRoutes حتى اللحظة. تنبهنا دوال الاستدعاء بعد ذلك إلى أن الواجه الخلفية جاهزة لاستقبال الطلبات، حتى لو كانت تعمل في وضع الإنتاج. تحديث الملف package.json لنقل شيفرة TypeScript إلى جافا سكريبت وتشغيل التطبيق بعد إنجاز البنية اﻷساسية للتطبيق وتحضيره للتشغيل، نحتاج أولًا إلى بعض اﻹعدادات لتمكين نقل transpilation شيفرة TypeScript: { "compilerOptions": { "target": "es2016", "module": "commonjs", "outDir": "./dist", "strict": true, "esModuleInterop": true, "inlineSourceMap": true } } نضيف أخيرًا بعض اللمسات النهائية على الملف package.json على هيئة سكربتات: "scripts": { "start": "tsc && node --unhandled-rejections=strict ./dist/app.js", "debug": "export DEBUG=* && npm run start", "test": "echo \"Error: no test specified\" && exit 1" }, يعمل السكربت test كملف مؤقت سنستبدله لاحقًا. تنتمي الوحدة tsc في السكربت start إلى TypeScript، وهو المسؤول عن نقل شيفرة TypeScript إلى جافا سكريبت التي ستظهر في المجلد dist. ثم نشغّل النسخة المبنية من التطبيق باستخدام التعليمة node ./dist/app.js. نمرر الوسيط unhandled-rejections=strict-- إلى Node.js (حتى في النسخ 16 وأعلى) لإيقاف التنفيذ عند ظهور خطأ غير محسوب في الشيفرة، ويسهّل ذلك معرفة سبب الخطأ وتصحيحه وهذا اﻷسلوب أوضح من الخيار اﻵخر وهو الاعتماد على كائن السجلات expressWinston.errorLogger الذي يزوّدك بقائمة اﻷخطاء بعد توقف المصرّف. ومعنى ذلك أننا سنترك Node.js يعمل على الرغم من وجود خطأ غير محسوب في الشيفرة وقد يسبب ذلك سلوكًا غير متوقع للخادم وظهور أخطاء أخرى قد تكون أكثر تعقيدًا. يستدعي السكربت debug السكربت start لكنه يعرّف أولًا متغير البيئة DEBUG. ولهذا المتغير تأثير في تمكين جميع عبارات ()debugLog (إضافة إلى تلك التي تقدمها Express.js، والتي تستخدم نفس وحدة التنقيح debug التي نستخدمها) لعرض تفاصيل مفيدة على الطرفية، وإلا ستختفي هذه التفاصيل عند تشغيل الخادم في وضع اﻹنتاج باستخدام التعليمة npm start. جرّب تنفيذ اﻷمر npm run debug. بنفسك، وقارن نتائج الخرج على الطرفية مع تلك التي تنتج عن تنفيذ npm start. تلميح: بإمكانك تحديد خرج التنقيح ليعطي فقط عبارات ()debugLog الموجودة في الملف app.ts، وذلك باستخدام DEBUG=app بدلًا من *\=DEBUG. فالوحدة debug مرنة عمومًا، وهذه الميزة ليست استثناءً. قد يحتاج مستخدمي ويندوز استبدال export بالتعليمة SET لأن export هي الطريقة التي تعمل على لينكس و ماك أو إس. أما إن أردت دعم عدة بيئات تطوير في تطبيقك، جرّب الحزمةcross-env package التي تزوّدك بحلول واضحة لهذه المسألة. اختبار الواجهة الخلفية مع تنفيذ أحد اﻷمرين npm run debug أو npm start ستكون الواجهة الخلفية جاهزة لتلقي الطلبات على المنفذ 3000. يمكننا عندها استخدام أحد المكتبات cURL أو Postman أو Insomnia لاختبار الواجهة الخلفية. وطالما أننا لم ننشئ سوى هيكلية للمورد users، بإمكاننا ببساطة إرسال طلبات دون جسم للطلب لنتأكد أن كل شيئ يجري كما هو متوقع، فمثلًا: curl --request GET 'localhost:3000/users/12345' ستعيد عندها الواجهة الخلفية الاستجابة: GET requested for id 12345. وعند استخدام POST: curl --request POST 'localhost:3000/users' \ --data-raw '' وغيرها من أنواع الطلبات، ستعيد الواجهة الخلفية نفس الاستجابة. الخلاصة بدأنا في هذا المقال في إنشاء واجهة برمجية REST بتهيئة المشروع من الصفر ومن ثم دخلنا في أساسيات إطار العمل Express.js. خطونا بعد ذلك أولى خطواتنا في احتراف TypeScript عن طريق بناء نموذج UsersRoutesConfig يرث CommonRoutesConfig وسنعيد استخدام هذا النموذج في الجزء الثاني من هذه السلسلة. أنهينا العمل بعد ذلك بتهيئة ملف المدخل app.ts لاستخدام الوجهات، ومن ثم تهيئة ملف package.json بالسكربتات اللازمة لبناء وتشغيل التطبيق. وعلى الرغم من استخدام أساسيات الواجهة البرمجية REST مع Express.js و TypeScript في مقالنا، فسوف نركز في المقال التالي على بناء متحكمات مناسبة للموارد والتعرف على نماذج أخرى مثل الخدمات والوحدات الوسيطة والمتحكمات وغيرها من الوحدات البرمجية. ترجمة -وبتصرف- للمقال Building a Node.js TypeScript REST API Part1 Express.js اقرأ أيضًا مدخل إلى Node.js وExpress دليلك لربط واجهة OpenAI API مع Node.js شرح فلسفة RESTful - تعلم كيف تبني واجهات REST البرمجية إنشاء مدوّنة باستخدام Node.js و Express (الجزء الأول)
  17. تُعد صيغة جافاسكريبت لترميز الكائنات JavaScript Object Notation واختصارًا JSON صيغة نصية لتمثيل البيانات المهيكلة وفقًا للصياغة الكائنية في جافاسكريبت، وهي شائعة الاستخدام لتمثيل وتبادل المعطيات عبر الويب (مثل إرسال البيانات من الخادم إلى العميل لتُعرض على صفحة الويب). ستصادف هذه الصيغة كثيرًا لهذا سنشرح لك في هذا المقال ما تحتاج معرفته عن عمل JSON في جافاسكربت بما في ذلك تفسير صياغتها للوصول إلى البيانات التي تضمها وكتابة بياناتك الخاصة بهذه الصياغة. ننصحك قبل أن تبدأ العمل معنا في هذه السلسلة أن تطلع على: أساسيات HTML. أساسيات عمل CSS أساسيات جافاسكربت كما شرحناها في سلسلة المقالات السابقة. أساسيات البرمجة كائنية التوجه كما شرحناها في مقال أساسيات العمل مع الكائنات في جافا سكريبت. ماهي صيغة JSON؟ JSON هو تنسيق نصي لتمثيل البيانات يتبع الصياغة القواعدية للغة جافا سكريبت، نشره وزاد شعبيته دوغلاس كروكفورد. وعلى الرغم من الشبه الكبير بين هذا التنسيق والصياغة القواعدية لكائنات جافا سكريبت الحرفية، يمكن استخدامه بشكل مستقل عن جافا سكريبت، وتتمتع الكثير من بيئات البرمجة بالقدرة على قراءة (تفسير) البيانات بتنسيق JSON وتوليدها. لتنسيق JSON طبيعة نصية، وهو أمر مفيد لنقل البيانات عبر الشبكات. وتحتاج إلى تحويل تنسيق JSON إلى كائن جافا سكريبت أصلي إن أردت الوصول إلى محتوياته. وبالطبع، لن يكون اﻷمر صعبًا لوجود كائن JSON عام في جافا سكريبت يقدّم توابع للتحويل بين الصيغتين (JSON - كائن جافا سكريبت). ملاحظة: تُدعى عملية تحويل كائن إلى نص بغية إرساله عبر شبكة بالسَلسَلة serialization، وعملية تحويل سلسلة نصية إلى كائن أصلي إلغاء السَلسَلة deserialization. يمكن تخزين نص JSON في ملف خاص، وسيكون على شكل ملف نصي نمطي يحمل اللاحقة json.، وله نمط MIME التالي: application/json. هيكلية JSON قلنا سابقًا أن نص JSON هو سلسلة نصية منسقة بطريقة تحاكي كائن جافا سكريبت حرفي. وباﻹمكان إضافة نفس اﻷنواع اﻷساسية للبيانات ضمن تنسيق JSON كما تفعل تمامًا عند التعامل مع كائن جافا سكريبت، مثل النصوص واﻷرقام والمصفوفات والقيم المنطقية وكائنات حرفية أخرى. يتيح لك هذا التنسيق بناء البيانات بشكل هرمي كالتالي: { "squadName": "Super hero squad", "homeTown": "Metro City", "formed": 2016, "secretBase": "Super tower", "active": true, "members": [ { "name": "Molecule Man", "age": 29, "secretIdentity": "Dan Jukes", "powers": ["Radiation resistance", "Turning tiny", "Radiation blast"] }, { "name": "Madame Uppercut", "age": 39, "secretIdentity": "Jane Wilson", "powers": [ "Million tonne punch", "Damage resistance", "Superhuman reflexes" ] }, { "name": "Eternal Flame", "age": 1000000, "secretIdentity": "Unknown", "powers": [ "Immortality", "Heat Immunity", "Inferno", "Teleportation", "Interdimensional travel" ] } ] } ولو حملّنا النص السابق في برنامج جافا سكريبت وفسرناه إلى كائن يدعى superHeroes مثلًا، سنتمكن من الوصول إلى البيانات التي يضمها وفق أسلوب الوصول إلى أعضاء الكائنات (نقطة أو قوس مربع). إليك مثالًا: superHeroes.homeTown; superHeroes["active"]; وللوصول إلى بيانات أعمق في التسلسل الهرمي، عليك كتابة سلسلة بأسماء الخاصيات وأدلة المصفوفات وصولًا إلى الخاصية المطلوبة. فلو أردت الوصول إلى القوة الخارقة الثالثة للبطل الخارق الثاني الموجود في قائمة البيانات، عليك تنفيذ ما يلي: superHeroes["members"][1]["powers"][2]; نستخدم اولًا اسم المتغير superHeroes.. ندخل بعدها إلى الخاصية members ضمنه، لهذا نستخدم التعليمة ["members"]. تضم الخاصية members مصفوفة كائنات، وعلينا الوصول إلى الكائن الثاني منها لهذا نستخدم الأمر [1]. وضمن الكائن الثاني من المصفوفة، نريد الوصول إلى الخاصية powers، لهذا نستخدم التعليمة ["powers"]. تضم الخاصية powers مصفوفة تحتوي على القوى الخارقة للأبطال، ونريد من بينها الثالثة [2]. ملاحظة: يمكنك الوصول إلى البيانات السابقة بتنسيق JSON ضمن الملف JSONTest.html (ألق نظرة على الشيفرة المصدرية). جرّب أن تحمّل الملف ثم الوصول إلى البيانات ضمن المتغيّر عبر طرفية جافا سكريبت في متصفحك. المصفوفات وتنسيق JSON أشرنا سابقًا أن نص JSON يبدو مشابهًا لكائن جافا سكريبت ضمن سلسلة نصية. ويمكننا أيضًا تحويل المصفوفات من وإلى تنسيق JSON. إليك مثالًا عن استخدام صحيح لتنسيق JSON: [ { "name": "Molecule Man", "age": 29, "secretIdentity": "Dan Jukes", "powers": ["Radiation resistance", "Turning tiny", "Radiation blast"] }, { "name": "Madame Uppercut", "age": 39, "secretIdentity": "Jane Wilson", "powers": [ "Million tonne punch", "Damage resistance", "Superhuman reflexes" ] } ] هذه الشيفرة نص JSON صحيح تمامًا، وعليك فقط الوصول إلى أعضاء المصفوفة (في النسخة المفسّرة منه) بأن تبدأ بدليل العنصر الذي تريده من المصفوفة مثل [0]["powers"][0]. ملاحظات أخرى إن JSON هو نص نمطي تنسق فيه البيانات بشكل محدد، ويتضمن خاصيات فقط ولا يضم توابع. لا بد من استخدام علامات اقتباس مزدوجة حول القيم النصية وأسماء الخاصيات، ولا يمكن استخدام علامة الاقتباس المفردة إلا لإحاطة نص JSON بأكمله. ستسبب أية أخطاء في وضع الفواصل أو النقطتين المتعامدتين في ملف JSON بوقوع مشاكل، ولن يعمل عندها الملف. انتبه إلى ضرورة التحقق من أية بيانات تحاول استخدامها (على الرغم من ضآلة احتمال وجود أخطاء في بيانات JSON التي يولدها الحاسوب، إن كان البرنامج الذي يولدها صحيحًا). بإمكانك التحقق من صحة بيانات JSON باستخدام تطبيقات مخصصة لذلك يمكنك البحث عنها في اﻹنترنت. يمكن لملف JSON شكل أي نوع من أنواع البيانات الصالحة للاستخدام في JSON وليست فقط الكائنات والمصفوفات، فسلسلة نصية مفردة أو رقم مفرد هي بيانات JSON صحيحة. على عكس شيفرة جافا سكريبت التي لا تتطلب وضع علامات اقتباس حول الخاصيات، لا بد من إحاطة أسماء الخاصيات في JSON بعلامتي اقتباس مزدوجتين. تطبيق عملي: العمل مع JSON سنعمل في هذا التمرين على استعراض طريقة الاستفادة من بيانات منسقة وفق تنسيق JSON في صفحة ويب. نقطة الانطلاق أنشئ بداية نسخة عن الملفين heroes.html و style.css على حاسوبك. ويتضمن الملف الثاني بعض تنسيقات CSS البسيطة لتنسيق الصفحة، بينما يضم اﻷول شيفرة HTML بسيطة لجسم الصفحة، إضافة إلى العنصر <script> الذي يضم شيفرة جافا سكريبت التي سنكتبها خلال عملنا على هذا التمرين. <header> ... </header> <section> ... </section> <script> ... </script> تتوفر بيانات JSON للتمرين ضمن ملف مخصص على جيت-هب، وسنحمّل هذا الملف باستخدام الشيفرة ونستخدم بعض دوال شجرة DOM لعرض البيانات: دوال المستوى اﻷعلى وهي كالتالي: async function populate() { const requestURL = "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json"; const request = new Request(requestURL); const response = await fetch(request); const superHeroes = await response.json(); populateHeader(superHeroes); populateHeroes(superHeroes); } نستخدم للحصول على بيانات JSON واجهة برمجية تُدعى Fetch تسمح لنا بإرسال طلبات عبر اﻹنترنت للحصول على موارد من الخادم من خلال جافا سكريبت (مثل الصور والنصوص وملفات JSON وحتى مقتطفات من شيفرة HTML). أي بإمكاننا تحديث جزء صغير من محتوى الصفحة بدلًا من اعادة تحميل الصفحة بالكامل. تستخدم اﻷسطر اﻷربعة اﻷولى من الدالة الواجهة البرمجية Fetch ﻹحضار ملف JSON من الخادم: صرحنا عن المتغير requestURL لتخزين عنوان الملف على جيت-هب. استخدمنا عنوان URL لتهيئة كائن الطلب Request الجديد. أرسلنا طلب عبر اﻹنترنت باستخدام الدالة ()fetch وسيعيد ذلك كائن استجابة Response. نحصل على الاستجابة بتنسيق JSON عن طريق استخدام الدالة ()json العائدة للكائن Response. ملاحظة: الواجهة البرمجية غير متزامنة asynchronous، لهذا لابد من إضافة الكلمة المحجوزة async قبل اسم الدالة التي تستخدم الواجهة والكلمة المحجوزة await قبل استدعاء أية دوال غير متزامنة. بعد كل ذلك، سيضم المتغير superHeroes كان جافا سكريبت المبني وفق بيانات JSON. ثم نمرر بعد ذلك الكائن إلى استدعاء دالتين الأولى لتملأ العنصر <header> بالبيانات الصحيحة، والثانية لتنشئ بطاقة معلومات لكل بطل في الفريق ووضعها ضمن العنصر <section>. ترتيب المعلومات في الترويسة بعد أن حصلنا على البيانات من الخادم وفق تنسيق JSON وحولناها إلى كائن جافا سكريبت، سنستخدمها عن طريق الدالتين اللتين أشرنا إليهما سابقًا. لهذا أضف الشيفرة التالية التي تمثّل تصريحًا عن دالة تحت الشيفرة الموجودة: function populateHeader(obj) { const header = document.querySelector("header"); const myH1 = document.createElement("h1"); myH1.textContent = obj.squadName; header.appendChild(myH1); const myPara = document.createElement("p"); myPara.textContent = `Hometown: ${obj.homeTown} // Formed: ${obj.formed}`; header.appendChild(myPara); } تنشئ الشيفرة السابقة بداية عنصر <h1> باستخدام التابع ()createElement، وأسندنا إلى محتواه قيمة الخاصية squadName التي تعود إلى كائن جافا سكريبت، ثم ألحقنا العنصر ومحتواه بالترويسة مستخدمين التابع ()appendChild. كررنا بعد ذلك نفس الخطوات مع الفقرة النصية، إذ أنشأناها بداية ثم وضعنا فيها المحتوى المناسب وألحقناها بالترويسة. والفرق الوحيد بين الحالتين هو أننا ضبطنا المحتوى النصي للفقرة ليكون مساويًا لقالب مفسّر (أو حرفي) template literal يضم قيمتي الخاصيتين homeTown و formed. إنشاء بطاقة معلومات البطل الخارق أضف اﻵن شيفرة الدالة التي تُنشئ وتعرض بطاقة البطل إلى نهاية الشيفرة السابقة كالتالي: function populateHeroes(obj) { const section = document.querySelector("section"); const heroes = obj.members; for (const hero of heroes) { const myArticle = document.createElement("article"); const myH2 = document.createElement("h2"); const myPara1 = document.createElement("p"); const myPara2 = document.createElement("p"); const myPara3 = document.createElement("p"); const myList = document.createElement("ul"); myH2.textContent = hero.name; myPara1.textContent = `Secret identity: ${hero.secretIdentity}`; myPara2.textContent = `Age: ${hero.age}`; myPara3.textContent = "Superpowers:"; const superPowers = hero.powers; for (const power of superPowers) { const listItem = document.createElement("li"); listItem.textContent = power; myList.appendChild(listItem); } myArticle.appendChild(myH2); myArticle.appendChild(myPara1); myArticle.appendChild(myPara2); myArticle.appendChild(myPara3); myArticle.appendChild(myList); section.appendChild(myArticle); } } بدأنا بتخزين قيمة الخاصية members لكائن جافا سكريبت في متغير جديد. وهذه الخاصية هي مصفوفة تضم عدة كائنات تضم معلومات عن كل بطل. ثم تنقلنا بين كائنات الخاصية باستخدام حلقة for...of لـِ: إنشاء عدة عناصر HTML جديدة وهي: <article> و <h2> وثلاث فقرات نصية <p> و <ul>. ضبط محتوى <h2> ليضم اسم البطل name. ملئ الفقرات النصية الثلاث بقيم الخاصيتين secretIdentity و age وبعبارة "Superpowers" لتقديم المعلومات الموجودة في القائمة. تخزين قيمة الخاصية powers في متغير آخر جديد يُدعى superPowers وتضم الخاصية مصفوفة تضم القوى الخارقة التي يتمتع بها البطل. استخدام حلقة for...of من جديد للتنقل بين القوى التي يمتلكها البطل وإنشاء عنصر <li> لوضع القوة ضمنه ثم نلحق عنصر القائمة listItem بالقائمة غير المرتبة <ul> (التس تُسمى <myList). إلحاق العنصر <h2> والفقرات النصية الثلاث <p> والقائمة <ul> بالعنصر <article> (الذي يُسمى myArticle) وإلحاقه بالعنصر <section>. وانتبه إلى إلحاق العناصر وفق الترتيب السابق تمامًا، فسيعكس ذلك ترتيب الملعومات التي ستُعرض في صفحة HTML. ملاحظة: إن واجهت صعوبة في تنفيذ التمرين، عُد إلى الشيفرة المصدرية له أو اطلع على النسخة الجاهزة منه. ملاحظة: إن واجهت صعوبة في استيعاب استخدام ترميز (النقطة والأقواس المربعة) في الوصول إلى كائنات جافا سكريبت، سيساعدك فتح الملف superheroes.json في نافذة أخرى لمحررك النصي والرجوع إليه عند متابعة شيفرة جافا سكريبت. كما يمكنك العودة إلى مقال أساسيات العمل مع الكائنات في جافا سكريبت لمعلومات أوضح عن طريقة الترميز هذه. استدعاء توابع المستوى اﻷعلى علينا أخيرًا استدعاء الدالة الأعلى مستوى ()populate populate(); التحويل بين الكائنات والنصوص يبدو التمرين السابق بسيطًا من ناحية الوصول إلى كائنات جافا سكريبت، لأننا حولنا الاستجابة التي أرسلها الخادم مباشرة إلى كائن جافا سكريبت باستخدام response.json. لكن في أحيان أخرى، قد تكون الاستجابة سلسلة JSON خام (غير منسّقة)، وعلينا حينها تحويلها إلى كائن جافا سكريبت بأنفسنا. وعندما نريد إرسال كائن جافا سكريبت إلى الخادم عبر اﻹنترنت، لا بد من تحويله إلى تنسيق JSON قبل اﻹرسال. ونظرًا لشيوع الحالتين السابقتين في تطوير الويب، تضم المتصفحات كائن JSON مضمن يمتلك التابعين التاليين: ()parse: يقبل نص JSON كمعامل ويعيد كائن جافا سكريبت الموافق. ()stringify: يقبل كائنًا كمعامل له، ويعيد نص JSON الموافق. بإمكانك رؤية عمل التابع الأول في النسخة الجاهزة من التمرين السابق (انظر أيضًا إلى شيفرته المصدرية) وهو يفعل تمامًا ما تفعله النسخة التي بنيناها باستثناء أننا: حصلنا على الاستجابة على شكل سلسلة نصية بدلًا من تنسيق JSON لاستخدامنا التابع ()text للحصول على الاستجابة. استخدمنا التابع ()parse لتحويل النص إلى كائن جافا سكريبت. إليك مقطع الشيفرة الموافق لما ذكرنا: async function populate() { const requestURL = "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json"; const request = new Request(requestURL); const response = await fetch(request); const superHeroesText = await response.text(); const superHeroes = JSON.parse(superHeroesText); populateHeader(superHeroes); populateHeroes(superHeroes); } وكما ترى، يعمل التابع ()stringify بطريقة معكوسة. جرّب اﻵن إدخال اﻷسطر التالية إلى طرفية جافا سكريبت في متصفحك سطرًا تلو اﻵخر: let myObj = { name: "Chris", age: 38 }; myObj; let myString = JSON.stringify(myObj); myString; ننشئ هنا كائن جافا سكريبت ثم نتحقق من محتواه ونحوّله بعدها إلى نص JSON باستخدام التابع ()stringify. نخزّن القيمة المعادة في متغيّر جيد ثم نتحقق منه مجددًا. الخلاصة قدمنا في هذا المقال دليلًا بسيطًا لاستخدام تنسيق JSON في برامجك بما في ذلك إنشاء وتفسير نصوص JSON وكيفية الوصول إلى البيانات التي يضمها. ترجمة -وبتصرف- لمقال Working with JSON اقرأ أيضًا تعلم JSON صيغة JSON وتوابعها في جافاسكربت ما عليك معرفته عن حقول JSON في قواعد بيانات MySQL كيفية استخدام صيغة JSON في لغة Go
  18. قدمنا في مقال سابق مدخلًا إلى مفاهيم البرمجة كائنية التوجه في جافا سكريبت، وناقشنا مثالًا عن استخدام مبادئها لنمذجة مدرسين وطلاب في مدرسة. كما تحدثنا أيضًا عن إمكانية استخدام الكائنات المجردة prototype والدوال البانية constructor لتنفيذ نماذج مشابهة، والميزات المرتبطة بمفاهيم البرمجة غرضية التوجه التقليدية التي تقدمها جافا سكريبت. سنتوسع في شرح هذه الميزات في مقالنا، وعليك الانتباه إلى أن الميزات التي نشرحها ليست طريقة جديدة لدمج الكائنات classes، وأن الكائنات ستستخدم دائمًا كائنات prototype خلف الكواليس. وكل ما هنالك أنها وسيلة لتسهيل بناء سلسلة الكائنات prototype. ننصحك قبل أن تبدأ العمل معنا في هذه السلسلة أن تطلع على: أساسيات HTML. أساسيات عمل CSS أساسيات جافاسكريبت كما شرحناها في سلسلة المقالات السابقة. أساسيات البرمجة كائنية التوجه في جافا سكربت كما شرحناها في مقال أساسيات العمل مع الكائنات في جافاسكربت، ومقال الوراثة باستخدام الكائنات في جافاسكربت إضافة إلى مفاهيم أساسية في البرمجة كائنية التوجه. اﻷصناف والدوال البانية يمكن التصريح عن صنف باستخدام الكلمة المحجوزة new، إليك كيفية تعريف الصنف Personالذي تعاملنا معه في مقال سابق: class Person { name; constructor(name) { this.name = name; } introduceSelf() { console.log(`Hi! I'm ${this.name}`); } } تُصرّح الشيفرة السابقة صنفًا يُدعى person له: الخاصية name. دالة بانية لها معامل وحيد name ويُستخدم لتهيئة قيمة الخاصية name للكائن الجديد. تابع ()introduceSelf يمكنه اﻹشارة إلى خاصيات الكائن باستخدام التعليمة this. يُعد التصريح ;name في البداية اختياريًا يمكن الاستغناء عنه، إذ يُنشئ السطر ;this.name = name في الدالة البانية الخاصية name قبل تهيئتها. لكن التصريح عمومًا عن الخاصياتن يجعل الشيفرة أسهل قراءة، ويوّضح تمامًا الخاصيات التي يمتلكها الصنف. كما تستطيع أيضًا تهيئة الخاصية بقيمة افتراضية عند التصريح عنها على الشكل ; =name. عّرفت الدالة البانية باستخدام الكلمة المحجوزة constructor، وكما هو الحال عند تعريف الدالة البانية خارج الصنف،سيكون لها المهام التالية: إنشاء كائن جديد. ربط التعليمة this بالكائن الجديد كي تتمكن من استخدام هذه التعليمة في اﻹشارة إلى الكائن ضمن شيفرتها. تنفيذ شيفرة الدالة البانية. إعادة الكائن الجديد. وبالعودة إلى شيفرة التصريح عن الكائن السابق، تستطيع إنشاء واستخدام نسخة جديدة من الكائن Person كالتالي: const giles = new Person("Giles"); giles.introduceSelf(); // Hi! I'm Giles لاحظ كيف نستدعي الدالة البانية باستخدام اسم الصنف، وهو Personفي مثالنا. حذف الدالة البانية إن لم تكن هناك حاجة لتهيئة قيم الخاصيات، تستطيع إهمال الدالة البانية، وعندها ستُولَّد دالة بانية افتراضية: class Animal { sleep() { console.log("zzzzzzz"); } } const spot = new Animal(); spot.sleep(); // 'zzzzzzz' الوراثة Inheritance تسمح الوراثة بإنشاء علاقة تسلسلية بين الكائنات، حيث يمكن للكائنات الفرعية أو الأبناء sub classes وراثة الخاصيات والتوابع من الكائنات الأساسية أو الآباء base classes، وفي نفس الوقت يمكنها تعديلها أو إضافة خصائص جديدة. لنتعرف على كيفية تحقيق مفهوم الوراثة في جافا سكريبت، لذا دعونا نستخدم الصنفPerson السابق في تعريف كائن فرعي أو كائن ابن له باسمprofessor: class Professor extends Person { teaches; constructor(name, teaches) { super(name); this.teaches = teaches; } introduceSelf() { console.log( `My name is ${this.name}, and I will be your ${this.teaches} professor.`, ); } grade(paper) { const grade = Math.floor(Math.random() * (5 - 1) + 1); console.log(grade); } } نستخدم الكلمة المحجوزة extends للدالة إلى أن الصنف الجديد يرث صنفًا آخر. ويضيف الصنف Professor خاصية جديدة هي teaches لهذا نُصرّح عنها. وطالما أننا نريد تهيئة قيمة تلك الخاصية عندما ننشئ كائنًا جديدًا من الصنف Professor، لا بد من تعريف دالة بانية تأخذ معاملين هما name و teaches. وما تفعله الدالة البانية هنا، هو استدعاء الدالة البانية للصنف اﻷب باستخدام التابع ()super ممررة له المعامل name وستتكفل الدالة البانية للصنف اﻷب بضبط قيمة الخاصية name. وبعدها تهيئ الدالة البانية للصنف Professor قيمة الخاصية teaches. ملاحظة: إن كان على الدالة البانية للصنف الابن تهيئة أية قيم خاصة به، عليها أولًا التأكد من تهيئة قيم الصنف الأب لهذا الصنف من خلال استدعاء الدالة ()superوتمرير قيم أية معاملات تحتاجها. كما يتجاوز الكائن الابن التابع ()introduceSelf الخاص بالصنف اﻷب ويقدّم نسخته الخاصة، ويضيف التابع ()grade لتصحيح اﻷوراق (طبعًا في مثالنا يوزّع المدرّس علامات عشوائية على الأوراق). وهكذا سنتمكن اﻵن من إنشاء مدرسين جديد: const walsh = new Professor("Walsh", "Psychology"); walsh.introduceSelf(); // 'My name is Walsh, and I will be your Psychology professor' walsh.grade("my paper"); // some random grade التغليف Encapsulation لنرى أخيرًا كيف ننجز مفهوم التغليف في جافاسكريبت. فقد ناقشنا في مقال سابق كيف أردنا أن تكون الخاصية year للكائن Studentخاصّة، كي نتمكن من تغيير شروط التسجيل في دروس الرماية دون اﻹخلال بالشيفرة التي تستخدم الكائن Student. class Student extends Person { #year; constructor(name, year) { super(name); this.#year = year; } introduceSelf() { console.log(`Hi! I'm ${this.name}, and I'm in year ${this.#year}.`); } canStudyArchery() { return this.#year > 1; } } وما فعلناه في التصريح السابق عن الصنف، أننا جعلنا الخاصية year# خاصّة بالصنف، وهكذا سنتمكن من بناء كائن Student يستخدم الخاصية year# داخليًا، وسيعطي المتصفح رسالة خطأ إن حاولت شيفرة خارج الكائن الوصول إليها. const summers = new Student("Summers", 2); summers.introduceSelf(); // Hi! I'm Summers, and I'm in year 2. summers.canStudyArchery(); // true summers.#year; // SyntaxError ملاحظة: يمكن للشيفرة المكتوبة ضمن طرفية جافا سكريبت في المتصفح الوصول إلى الخاصيات الخاصة حتى لو كانت خارج الصنف. وهذا أمر خاص لتحرير أدوات مطوري ويب فقط من قيود الصياغة اللغوية لجافا سكريبت. إذًا ينبغي أن يبدأ اسم الخاصيات الخاصة بالمحرف # ويجب أن يُصرّح عنها داخل الصنف. التوابع الخاصة Private methods بإمكانك تحديد توابع أيضًا لتكون توابع خاصة بالصنف كما في الخاصيات، بحيث تكون تابعة للكائن نفسه ولا يمكن الوصول إليها من خارجه الدالة خاصة بالكائن نفسه، كما في الخاصيات ولا بد في هذه الحالة أن يبدأ اسم التابع بالمحرف # أيضًا، وعندها ستُستدعى فقط من قبل توابع هذا الصنف كما في المثال التالي: class Example { somePublicMethod() { this.#somePrivateMethod(); } #somePrivateMethod() { console.log("You called me?"); } } const myExample = new Example(); myExample.somePublicMethod(); // 'You called me?' myExample.#somePrivateMethod(); // SyntaxError الخلاصة ناقشنا في هذا المقال الأدوات التي تقدمها جافا سكريبت لكتابة الكائنات والتعامل معها في البرامج كائنية التوجه، وتجدر الإشارة إلى أننا لم نغطي كل النقاط المتعلقة بالموضوع في هذا المقال، لكن ما قدمناه سيكون كافيًا في البداية. ترجمة -وبتصرف- لمقال Classes in JavaScript اقرأ أيضًا المقال السابق: مفاهيم أساسية في البرمجة كائنية التوجه وتحقيقها في جافاسكريبت مدخل إلى جافاسكريبت كائنية التوجه (Object-Oriented JavaScript) لغة البرمجة بالكائنات Object-Oriented Programming برمجة الكائنات Objects في جافاسكريبت مختصر البرمجة كائنية التوجه OOP وتطبيقها في بايثون
  19. البرمجة كائنية التوجه Object-Oriented programming واختصارًا OOP هي مصطلح برمجي أساسي في الكثير من لغات البرمجة مثل جافا و ++C. ونحاول في هذا المقال تزويدك بإحاطة شاملة عن أساسيات مفهوم البرمجة كائنية التوجه، ونشرح مفاهيمها اﻷساسية، ونوضح مفهوم اﻷصناف classes والنسخ instances والوراثة inheritance والتغليف encapsulation. ولن نخص في شرحنا لهذه المفاهيم لغة جافا سكريبت حاليًا، وستكون اﻷمثلة جميعها مكتوبة بلغة معممة (أو بالشيفرة الوهمية pseudo-code). ملاحظة: لتوخي الدقة، عليك معرفة أن ما سنشرحه هو نمط مخصص من البرمجة كائنية التوجه هو البرمجة كائنية التوجه التقليدية. وهو المصطلح المقصود عندما يجري الحديث عمومًا عن البرمجة كائنية التوجه. بعد ذلك، سنوضح العلاقة بين الدوال البانية constructor وكائنات prototype في جافا سكريبت وبين مفاهيم البرمجة كائنية التوجه التي أشرنا إليها سابقًا، وأبرز الاختلافات بينها. وسنركز في مقالات لاحقة على بعض الميزات اﻹضافية في جافا سكريبت التي تساعد على تنفيذ برامج كائنية التوجه. ننصحك قبل أن تبدأ العمل معنا في هذه السلسلة أن تطلع على: أساسيات جافا سكريبت كما شرحناها في سلسلة المقالات السابقة. أساسيات البرمجة كائنية التوجه في جافا سكريبت، كما شرحناها في مقال أساسيات العمل مع الكائنات في جافا سكريبت، ومقال استخدام كائنات Prototype في جافا سكريبت. ما هي البرمجة كائنية التوجه تشير البرمجة كائنية التوجه إلى طريقة لنمذجة نظام على شكل مجموعة من الكائنات، يمثّل كل كائن بعض ميزات النظام. وتضم الكائنات دوال functions أو (توابع methods) وبيانات data، كما يقدّم الكائن واجهة عمومية تستخدمها كائنات أو شيفرات أخرى للتعامل معه وواجهة خاصة تحتفظ بمعلومات وبيانات عن الحالة الداخلية للكائن ولا يمكن التعامل معها من خارج الكائن أي أنها تكون غير مرئية لما هو خارج الكائن، وبالتالي لن تُضطر بقية أجزاء المنظومة إلى معرفة ما يجري داخليًا ضمن أجزاء أو كائنات أخرى. اﻷصناف Classes والنُّسَخ Instances عندما نريد نمذجة مسألة وفقًا لمبدأ البرمجة كائنية التوجه OOP، ننشئ تعريفات عامة تمثّل أنواع الكائنات التي نريدها في المنظومة. فلو أدرنا مثلًا نمذجة مدرسة، قد نرغب بإنشاء كائنات تمثل المدرّسين، ويكون لهؤلاء المدرسين ميزات أو سمات مشتركة كأن يكون لهم أسماء ومواد يدرّسونها. وإضافة إلى ذلك، يمكن لأي مدرس تنفيذ أعمال محددة، مثل تصحيح اﻷوراق أو تقديم أنفسهم إلى الطلاب في بداية العام الدراسي مثلًا. لهذا يمكن أن يكون المدرّس ضنفًا في المنظومة باسم Professor، ويٌعرّف الصنف بداخله مجموعة من البيانات والتوابع التي يمتلكها كل مدرّس في المدرسة. على سبيل المثال يمكن أن يُعرّف الصنف Professor بالشيفرة الوهمية التالية: class Professor properties name teaches methods grade(paper) introduceSelf() تُعرّف الشيفرة السابقة الصنف Professor كالتالي: خاصيتين أو سمتين تحملان بيانات المدرس وهما name التي تمثل اسم المدرس و teaches التي تمثل المواد التي يقوم بتدريسها. تابعين هما ()grade لتصحيح ورقة، و ()introduceSelf للتعريف عن أنفسهم. كما تلاحظ ليس للصنف وظيفة قائمة بحد ذاتها، بل هو أقرب إلى قالب ﻹنشاء كائنات objects من هذا النوع أو الصنف. فكل مدرّس ننشئه وفق القالب Professor يُدعى نسخة instance عن هذا القالب. تُنشأ نسخة عن صنف باستخدام نوع خاص من الدوال تُدعى بالدوال البانية constructors. إذ نمرر قيمًا إلى الدوال البانية لتهيئة النسخة بقيم تضبط حالتها الداخلية اﻷساسية. تُكتب الدوال البانية عمومًا كجزء من تعريف الصنف، ولها عادة نفس اسم الصنف، لاحظ الشيفرة التالية: class Professor properties name teaches constructor Professor(name, teaches) methods grade(paper) introduceSelf() تأخذ الدالة البانية في الشيفرة السابقة معاملين أو وسيطين، لهذا بإمكاننا تهيئة الخاصيتين name و teaches عند إنشاء نسخة جديدة أو كائن عن صنف المدرّس. وطالما أن لدينا دالة بانية اﻵن، سنتمكن من إنشاء بعض المدرسين، وتستخدم بعض لغات البرمجة عادة الكلمة المحجوزة new للإشارة إلى استدعاء الدالة البانية: walsh = new Professor("Ahmad", "Psychology"); lillian = new Professor("Lyla", "Poetry"); walsh.teaches; // 'Psychology' walsh.introduceSelf(); // 'My name is Professor Ahmad and I will be your Psychology professor.' lillian.teaches; // 'Poetry' lillian.introduceSelf(); // 'My name is Professor Lyla and I will be your Poetry professor.' تنشئ شيفرة جافا سكريبت السابقة كائنين، وكلاهما نسخة عن الصنف Professor. الوراثة Inheritance لنفترض أننا نريد إنشاء طلاب في منظومة المدرسة السابقة، لكن لا يمكن للطلاب تصحيح اﻷوراق ولا يمكنهم تدريس مواد، وينتمون إلى سنوات دراسية محددة. لكن سيحمل الطلاب أسماءً، وقد يرغبون بتقديم أنفسهم، لهذا يمكن صياغة صنف خاص الطلاب من خلال الشيفرة الوهمية التالية: class Student properties name year constructor Student(name, year) methods introduceSelf() ومن المفيد أن نشير إلى اشتراك الطلاب والمدرسين ببعض الخاصيات، أو بشكل أدق اﻹشارة إلى أنهما ينتميان إلى نوع واحد عند مستوى ما، وهذا ما تسمح به الوراثة inheritance في البرمجة كائنية التوجه. فقد ننظر إلى المدرسين والطلاب بداية على أنهم أشخاص، وللأشخاص أسماء ويقدموّن أنفسهم عند الحاجة. ولنمذجة هذه الفكرة، ننشئ صنفًا جديدًا هو Person، نعرّف فيه جميع الخصائص المشتركة للأشخاص، ثم باﻹمكان اشتقاق الصنفين Professor و Student من الصنف Person ومن ثم إضافة الخاصيات المميزة لكل صنف: class Person properties name constructor Person(name) methods introduceSelf() class Professor : extends Person properties teaches constructor Professor(name, teaches) methods grade(paper) introduceSelf() class Student : extends Person properties year constructor Student(name, year) methods introduceSelf() وهكذا يمكن القول أن الصنف Person هو صنف أعلى super class أو صنف أب parent class لكل من الصنف Professor والصنف Student اللذان يُدعيان في هذه الحالة بأصناف فرعية sub classes أو أصناف أبناء child class. ولاحظ كيف عُرِّف التابع ()introduceSelf في جميع الأصناف الثلاث، والسبب هو اختلاف الطريقة التي يُقدّم فيها كل صنف نفسه: ahmad = new Professor("Ahmad", "Psychology"); ahmad.introduceSelf(); // 'My name is Professor Ahmad and I will be your Psychology professor.' summers = new Student("Summers", 1); summers.introduceSelf(); // 'My name is Summers and I'm in the first year.' وباﻹمكان أيضًا كتابة تابع افتراضي ()introduceSelf للتعريف عن أشخاص ليسوا طلابًا ولا مدرسين: passam = new Person("Passam"); passam.introduceSelf(); // 'My name is Passam.' تُدعى فكرة وجود تابع بنفس الاسم في عدة أصناف، لكنه ينفّذ وظيفة خاصة في كل صنف بتعدد الأشكال polymorphism، حيث يمكن تعريف تابع بنفس الاسم في الصنف الأب والابن وفي هذه الحالة يحل التابع المعرف في الصنف الابن محل التابع المعرف في الصنف الابن، ونقول في هذه الحالة أننا تجاوزنا overrides نسخة التابع الموجودة في الصنف اﻷب. التغليف Encapsulation تقدّم الكائنات واجهة عامة لبقية الشيفرة كي تتخاطب معها، لكنها تحتفظ بحالتها الداخلية (قيم مخصصة تساعدها على تنفيذ وظائفها). ونقول أن الحالة الداخلية للصنف تبقى خاصة private، أي يمكن الوصول إليها من قبل التوابع الخاصة بالصنف فقط، وليس من قبل أية كائنات أخرى. تُدعى عملية إبقاء الحالة الداخلية للصنف خاصة أو الفصل الواضح بين الواجهة العامة للصنف وأعضاءه الداخليين عمومًا بالتغليف encapsulation. تأتي أهمية هذه الميزة بأنها تسمح للمبرمج بتغيير الواجهة الداخلية للكائن دون الحاجة إلى البحث عن الشيفرة التي تستخدمه وتعديلها. فهي تقدم شكلًا من أشكال جدران الحماية بين الكائن وبقية مكونات المنظومة. فلو سمُح لطلاب السنة الثانية وما فوق دراسة الرماية، باﻹمكان تنفيذ اﻷمر بالاستفادة من الخاصية year، وستتمكن بقية الشيفرة من تحديد إمكانية تسجيل الطالب في صف الرماية أو لا: if (student.year > 1) { // allow the student into the class } لكن المشكلة ستقع إذا غيرنا معيار السماح للطلاب بالتسجيل في درس الرماية، كأن يحتاج إلى موافقة ولي أمره، عندها علينا تغيير الشيفرة التي تتحقق من إمكانية تسجيل الطالب في كل مكان. لهذا من اﻷفضل إنشاء تابع ()canStudyArchery في الكائنات Student لتنفيذ منطق العملية. class Student : extends Person properties year constructor Student(name, year) methods introduceSelf() canStudyArchery() { return this.year > 1 } if (student.canStudyArchery()) { // allow the student into the class } وهكذا، سيكون علينا تغيير الصنف Student فقط إذا أردنا تغيير شروط دراسة الرماية، وستعمل بقية الشيفرة في كل مكان كما يجب. وبامكاننا في الكثير من لغات البرمجة كائنية التوجه منع بقية الشيفرة من الوصول إلى الحالة الداخلية للكائن بجعل خاصياتها private، وسينتج خطأ إن حاولت الشيفرة خارج الصنف الوصول إلى الخاصية: class Student : extends Person properties private year constructor Student(name, year) methods introduceSelf() canStudyArchery() { return this.year > 1 } student = new Student('Wael', 1) student.year // error: 'year' is a private property of Student أما في اللغات التي لا تفرض قيودًا كهذه على الوصول، يستخدم المبرمجون أسلوبًا في التسمية، يميزّون فيه الخاصيات ذات الوصول الخاص، كأن تبدأ التسمية بشرطة سفلية _. تحقيق البرمجة كائنية التوجه في لغة جافا سكريبت ناقشنا حتى اللحظة في مقالنا الميزات اﻷساسية للبرمجة كائنية التوجه بالعموم والتي تعتمدها لغات برمجة عديدة مثل ++C وجافا، وكنا قد ألقينا النظرة في مقالي أساسيات الكائنات في جافا سكريبت وكائنات prototype عن مفهومي الدوال البانية والكائنات prototype. وترتبط هاتان الميزتان بالتأكيد مع ميزات البرمجة كائنية التوجه إلى حد ما. حيث تزوّدنا الدوال البانية في جافا سكريبت بما يشبه تعريف الصنف، مما يساعد على تحديد شكل الكائن، بما في ذلك التوابع التي قد يتضمنها في مكان واحد من الشيفرة. كما يمكن استخدام كائنات prototype أيضًا، فلو عُرِّف تابع مثلًا ضمن الخاصية prototype لدالة بانية، فإن جميع الكائنات التي ننشأها باستخدام الدالة البانية ستمتلك هذا التابع الذي مرر إليها من خلال الكائن prototype، ولا حاجة لتعريفه ضمن الدالة البانية نفسها. كما تبدي سلسلة prototype chain سلوكًا يشبه سلوك الوراثة، فلو كان لدينا كائن من الصنف Student يمتلك الكائن Person ككائن prototype، فسيرث الخاصية name والتابع ()introduceSelf. لكن من المهم أيضًا فهم الاختلاف بين تلك الميزات ومفاهيم البرمجة كائنية التوجه التقليدية. وهذا ما سنناقشه بشيء من التفصيل. بداية، هناك اختلاف واضح في البرمجة كائنية التوجه بين الكائنات واﻷصناف، فالكائنات هي دائمًا نسخ عن اﻷصناف، وهنالك اختلاف واضح بين طريقة تعريف الصنف (الصياغة القواعدية بحد ذاتها) وطريقة إنشاء نسخ عن هذا الكائن (الدالة البانية). بينما نتمكن في جافا سكريبت من إنشاء الكائنات دون الحاجة إلى وجود تعريف مستقل للصنف، سواء عند استخدام الدالة البانية أو بإنشاء الكائن حرفيًا. وهذا ما يجعل العمل مع الكائنات في جافا سكريبت أسرع مقارنة مع البرمجة كائنية التوجه. ثانيًا، على الرغم من أن سلسلة كائنات prototype قريبة من مفهوم الوراثة، وتسلك السلوك نفسه بشكل أو بآخر، لكنهما مفهومان مختلفان. فعند إنشاء كائنات من صنف ابن subclass سنحصل على كائن واحد يجمع بين الخاصيات المعرفة في الصنف الابن والخاصيات المعرّفة في الصنف اﻷب. بينما يتميز نموذج prototyping بأن كل مستوى من مستويات الوراثة الهرمية في سلسلة يمثل بكائن مستقل، وسترتبط هذه الكائنات ببعضها عبر الخاصية _proto_. فالكائنات في سلسلسة prototype chain هي أقرب إلى مفهوم التفويض delegation من مفهوم الوراثة. والتفويض هو نمط برمجي يعطي الكائن القدرة على تنفيذ مهمة ما توكل إليه بنفسه أو تفويض كائن آخر لتنفيذ هذه المهمة. وفي الكثير من اﻷحيان، نرى أن التفويض أكثر مرونة في ربط الكائنات مع بعضها مقارنة بالوراثة (لسبب مهم وهو إمكانية تغيير أو استبدال الكائن المفوَّض كليًا أثناء تنفيذ البرنامج). وهكذا نرى أن الدوال البانية وكائنات prototype هي ميزات تمكننا من تنفيذ أو مقاربة بعض مفاهيم الوراثة كائنية التوجه في لغة جافا سكريبت، لكن استخدامها المباشر في تنفيذ ميزات مثل الوراثة أمر على قدر من الصعوبة. لهذا تقدم جافا سكريبت ميزات مبنية على نموذج كائنات prototype ترتبط بشكل أوضح بمفاهيم الوراثة كائنية التوجه، وهذا ما سنراه بتفصيل أكبر في مقالات لاحقة. الخاتمة تحدثنا في هذا المقال عن الميزات اﻷساسية للبرمجة كائنية التوجه OOP وطريقة تحقيقها في جافا سكريبت، كما ألقينا نظرة سريعة على مواطن الشبه بين الدوال البانية وكائنات prototype في جافا سكريبت وبين مميزات البرمجة بالكائنات. ترجمة-وبتصرف- للمقال Object-Oriented programming اقرأ أيضًا المقال السابق: استخدام كائنات Prototype في جافا سكريبت مدخل إلى جافاسكريبت كائنية التوجه (Object-Oriented JavaScript) لغة البرمجة بالكائنات Object-Oriented Programming برمجة الكائنات Objects في جافاسكريبت
  20. من الضروري لأي مصمم مواقع ويب أن يتم بتصميم موقعه بشكل يتوافق مع احتياجات وتفضيلات الجمهور المستهدف. وفي مقال اليوم نناقش كيفية تحقيق ذلك وإظهار الموقع بشكل مقبول على مجموعة متنوعة من المتصفحات، بما في ذلك الإصدارات القديمة، مما يضمن تجربة مرضية لجميع الزوار. عليك قبل البدء في قراءة سلسلة المقالات هذه أن: تطلع على أساسيات HTML كما شرحناها في سلسلة المقالات مدخل إلى HTML. تتفهم أساسيات CSS كما شرحناها في سلسلة المقالات خطواتك الأولى في CSS. فقد يزور صفحتك بعض الزوار الذين يستخدمون متصفحات أقدم او لا تدعم أساليب تخطيط صفحات الويب الحديثة. وهذا ما يحدث في عالم الويب، فلا تدعم كل المتصفحات جميع الميزات الجديدة مباشرة بل يعتمد مطورها أولويات مختلفة في دعم ما يستجد. وسنشرح في هذا المقال كيف نستخدم تقنيات الويب المعاصرة دون أن نستثني من يستخدم التقنيات الأقدم. ما المتصفحات التي ستعرض عليها موقعك؟ تختلف مواقع الويب عن بعضها وفقًا للجمهور المستهدف، لهذا لا بد من معرفة عدد متابعي موقعك الذين يستخدمون متصفحات قديمة بل أن تقرر النهج الذي تتبعه في تصميم الموقع. وهذه العملية واضحة ومباشرة إن كان لديك موقع ويب ترغب في تعديله أو استبداله بآخر، فمن المحتمل أن تمتلك مجموعة من الأدوات التحليلية القادرة على تحديد التقنية التي يستخدمها الزائرون.أما إن لم تكن تمتلك هذه الأدوات، أو كان موقعك جديد كليًا، فستجد مواقع مثل Statcounter تستطيع تزويدك بإحصائيات وفقًا لموقعك الجغرافي. لا بد أن تأخذ أيضًا بعين الاعتبار نوع الجهاز الذي يستخدمه الزائر لتصفح موقعك. فقد تجد أن نسبة مستخدمي الهواتف المحمولة تزيد عن المتوسط. كما أن مراعاة شمولية الوصول أمر ضروري، فمعرفة عدد الزائرين الذين يستخدمون تقنيات حساسة أمر ضروري وقد يغدو حيويًا في بعض المواقع. إذ يلاحظ أن مطوري الويب مهتمون جدًا بتعزيز تجربة مستخدمي متصفحات إنترنت إكسبلورر القديمة الذين لا تتجاوز نسبتهم 1%، ولا يهتمون إطلاقًا بالزائرين ذوي الاحتياجات الخاصة الذين يشكلون نسبة أعلى بكثير. ما هو الدعم المقدم للميزات التي تريد استخدامها؟ بعد أن تطلع على أنواع المتصفحات التي قد تستعرض موقعك، يمكنك تقييم التقنيات التي عليك استخدامها بناء على دعم المتصفحات لها وسهولة تقديم بديل للزائرين الذين لا تدعم متصفحاتهم هذه التقنيات. سنحاول في هذا المقال تسهيل الأمر عليك ستجد في موسوعة حسوب شرحًا لكل خاصية وأدنى إصدار للمتصفحات التي تدعمها. كما ستجد معلومات مماثلة ضمن موقع شبكة مطوري موزيلا. ومن الطرق الشعبية الأخرى لمعرفة دعم المتصفحات لخاصية ما نجد مواقع مساعدة مثل Can I Use. إذ يعرض هذا الموقع قوائم لتقنيات الويب و منصاتها الرئيسية ومعلومات عن دعم المتصفحات المختلفة لها. كما ستتمكن من معرفة إحصائيات عن استخدام ميزة ما في نطاق موقعك الجغرافي أو أي منطقة تريد من العالم. وبإمكانك أيضًا ربط حساب جوجل أنيلتيكس Google Analytics كي تحصل على تحليل للمعلومات وفقًا لمعلومات مستخدميك. ستزيد معرفة التقنيات التي يستخدمها زائرو موقعك ودعم المتصفحات لها من قدرتك على اتخاذ القرار المناسب والطريقة الأنسب لدعمهم جميعًا. لا يعني الدعم رؤية ما تتوقع أن تراه دائمًا! لا يمكن أن ترى موقعك بنفس الشكل دائمًا على جميع المتصفحات، فقد يستعرض بعض الزائرين موقعك على هاتف محمول وآخرين على شاشة حاسوب مكتبي عريضة. وكذلك ستجد أن بعضهم يستخدم متصفحات أقدم وآخرين يستخدمون أحدث إصدارات متصفح معين. كما يقد يستمع بعض الزائرين لموقعك عن طريق قارئات الشاشة أو قد يكبرون أو يصغرون الشاشة للحصول على تجربة قراءة أوضح. ويعني تمامًا دعم جميع هؤلاء المستخدمين تقديم نسخة دفاعية من المحتوى، إذ تبدو هذه النسخة رائعة في المتصفحات الحديثة وتبقى صالحة من ناحية تقديم الوظيفة الأساسية للموقع ضمن المتصفحات الأقدم. يأتي المستوى الأول من دعم المتصفحات عن طريق هيكلة محتوى الصفحة جيدًا كي تتوافق مع تخطيط الانسياب الأساسي الاعتيادي normal flow. فقد لا يستفيد زائر يستخدم هاتفًا محدود الميزات من ميزات التنسيق، لكن انسياب المحتوى بشكل صحيح سيسهّل عليه التتبع والقراءة. وبالتالي نجعل الهيكلة الجيدة لصفحة HTML نقطة الانطلاق دائمًا، وينبغي ألا يتأثر محتوى الصفحة من ناحية البنية والتتابع إن حذفت تنسيقات CSS. ويرى البعض أن تُبقي على هذه الطريقة الأساسية في عرض الصفحة كي يتراجع إليها مستخدمو المتصفحات القديمة أو المحدودة الإمكانيات. فإن كان عدد المستخدمين هؤلاء ممن يزورون صفحتك قليلًا جدًا، قد لا يكون مفيدًا من ناحية تجارية أن تهدر وقتك في منحهم تجربة تماثل تجربة مستخدمي المتصفحات الحديثة. ومن الأجدى أن تستغل هذا الوقت في جعل موقعك أفضل من ناحية شمولية الوصول (الوصول السهل Accessibility) فقد يخدم ذلك عددًا أكبر من الزائرين. إذًا هنالك دائمًا خيار وسط بين صفحة HTML الأساسية وكل الزخارف التي تقدمها تنسيقات CSS، وقد ساهمت CSS بالفعل في جعل التراجع إلى النسخة الأساسية مباشرًا وسهلًا. إنشاء خطة تراجع في CSS تتضمن مواصفات CSS معلومات تشرح ما يفعله المتصفح عندما يُطبّق تخطيطين مختلفين في الوقت ذاته. أي يوجد تعريف محدد لما سيحصل إن كان العنصر معومًا و عنصر شبكة في نفس الوقت على سبيل المثال. وبالاستفادة من فكرة أن المتصفح يتجاهل قواعد التنسيق التي لا يفهمها ومعرفتك بطريقة إنشاء تخطيطات وفق التقنيات القديمة التي غطيناها في مقال سابق والتي تهمل مقابل تخطيط الشبكة سيُعرض الموقع بالشكل الأمثل ضمن المتصفحات الحديثة التي تفهمها وبشكل مقبول وظيفيًا في المتصفحات الأقدم. التراجع من تخطيط الشبكة إلى تخطيط التعويم لدينا في مثالنا التالي ثلاث حاويات <div> معروضة في نفس السطر تراها المتصفحات التي لا تدعم تخطيط الشبكة كثلاث صناديق معوّمة. وبما أن العنصر المعوّم الذي يصبح عنصر شبكة سيفقد سلوك التعويم، ستصبح هذه العناصر عناصر شبكة. وبالتالي ستُعرض على شكل عناصر شبكة في المتصفحات الحديثة وستتتجاهل المتصفحات الأقدم الخاصية display: grid وما يتعلق بها وتستخدم تخطيط التعويم. * { box-sizing: border-box; } .wrapper { background-color: palegoldenrod; padding: 10px; max-width: 400px; display: grid; grid-template-columns: repeat(3, 1fr); } .item { border-radius: 5px; background-color: rgb(207 232 220); } @supports (grid-template-rows: subgrid) { .wrapper { grid-template-rows: subgrid; gap: 10px; background-color: lightblue; text-align: center; } } <div class="wrapper"> <div class="item">Item One</div> <div class="item">Item Two</div> <div class="item">Item Three</div> <div class="item">Item Four</div> <div class="item">Item Five</div> <div class="item">Item Six</div> </div> See the Pen Untitled by Hsoub Academy (@HsoubAcademy) on CodePen. ملاحظة: لا تأثير للخاصية clear أيضًا عندما تصبح العناصر عناصر شبكة، لهذا تستطيع إنشاء تخطيط له تذييل footer معزول باستخدام clear والذي يتحول عندها إلى تخطيط شبكة. أساليب التراجع هنالك العديد من التخطيطات التي يمكن استخدامها بطريقة تشابه مثالنا السابق، وبإمكانك اختيار الطريقة التي تراها أنسب لما يحتاجه موقعك: أسلوب التعويم والعزل: كما رأينا في المثال السابق، تؤثر الخاصيتين float و clear على التخطيط إن تحوّلت العناصر المعوّمة أو المعزولة إلى عناصر مرنة flex أو عناصر شبكة grid. استخدام القاعدة display: inline-block: تُستخدم هذه الطريقة لإنشاء تخطيط أعمدة. فإن تحوّل عنصر يمتلك الخاصية display: inline-block إلى عنصر مرن أو عنصر شبكة سيتجاهل المتصفح القاعدة display: inline-block. القاعدة display:table: يُستخدم هذا الأسلوب في إنشاء تخطيط جدول. فإن امتلك العنصر خواص مثل display: table و display: table-cell ثم تحوّل إلى عنصر شبكة أو عنصر مرن، يتجاهل المتصفح سلوك الخاصية display. التخطيط متعدد الأعمدة: يمكنك في حالات محددة استخدام التخطيط متعدد الأعمدة كتخطيط تراجع إذا امتلكت الحاوية إحدى الخاصيات -column ومن ثم تحوّلت إلى حاوية شبكة، سيتجاهل حينها المتصفح سلوك تعدد الأعمدة. التراجع من تخطيط الشبكة إلى تخطيط الصندوق المرن: يتمتع تخطيط الصندوق المرن بدعم أوسع من قبل المتصفحات موازنة بتخطيط الشبكة كونه مدعوم من متصفح إنترنت إكسبلورر بنسختيه 10 و 11. فإن حوّلت تخطيط الصندوق المرن إلى تخطيط شبكة سيتجاهل المتصفح أي خاصية من خواص flex التي تُطبق على العناصر الأبناء. باستخدام حيل CSS كالتي استعرضناها ستكون قادرًا على منح مستخدمي المتصفحات الأقدم تجربة لائقة. إذ يمكننا إضافة تخطيط أبسط مبني على تقنيات قديمة مدعومة جيدًا ثم نستخدم خاصيات تنسيق أحدث لإنشاء تخطيط عصري يراه أكثر من 90% من المستخدمين. لكن لا بد في بعض الحالات كتابة شيفرة تراجع تتضمن تقنيات تفهمها المتصفحات الحديثة أيضًا، ومن الأمثلة عليها استخدام نسبة مئوية لتقدير اتساع العناصر المعوّمة كي تبدو الأعمدة أقرب إلى شكل الشبكة وتتمدد لتملأ الحاوية. يُحسب اتساع العنصر المعوّم ليكون 33.333% من اتساع الحاوية، أي ثلث الاتساع، بينما تحسب نسبة 33.333% في الشبكة من المساحة التي يقع ضمنها في الشبكة وستصبح ثلث القياس الذي نريد بمجرد تحوّل التخطيط إلى تخطيط شبكة. * { box-sizing: border-box; } .wrapper { background-color: rgb(79, 185, 227); padding: 10px; max-width: 400px; display: grid; grid-template-columns: 1fr 1fr 1fr; } .item { float: left; border-radius: 5px; background-color: rgb(207, 232, 220); padding: 1em; width: 33.333%; } <div class="wrapper"> <div class="item">Item One</div> <div class="item">Item Two</div> <div class="item">Item Three</div> </div> See the Pen Untitled by Hsoub Academy (@HsoubAcademy) on CodePen. للتعامل مع هذه الحالة لابد من طريقة لمعرفة إن كان تخطيط الشبكة مدعومًا وبالتالي ستتجاوز اتساع التخطيط الأقدم ،وهنا تقدم لنا CSS حلًا. الاستعلام عن الميزات يساعدك الاستعلام عن الميزات في اختبار دعم المتصفح لأي ميزة من ميزات CSS، أي بإمكانك كتابة تنسيقات خاصة بالمتصفحات التي لا تدعم ميزات معينة، ثم التحقق لترى إن كان المتصفح يدعم ما تختبره من ميزات فإن كان كذلك سيعرض التخطيط العصري. إن أضفنا استعلام عن ميزة إلى مثالنا السابق، سنتمكن من إعادة ضبط الاتساعات العناصر على القيمة auto إن علمنا أن المتصفح يدعم تخطيط الشبكة: * { box-sizing: border-box; } .wrapper { background-color: rgb(79, 185, 227); padding: 10px; max-width: 400px; display: grid; grid-template-columns: 1fr 1fr 1fr; } .item { float: left; border-radius: 5px; background-color: rgb(207, 232, 220); padding: 1em; width: 33.333%; } @supports (display: grid) { .item { width: auto; } } <div class="wrapper"> <div class="item">Item One</div> <div class="item">Item Two</div> <div class="item">Item Three</div> </div> See the Pen Untitled by Hsoub Academy (@HsoubAcademy) on CodePen. تدعم المتصفحات الحديثة استعلامات الميزات جيدًا، لكن عليك الانتباه إلى أن المتصفحات التي لا تدعم تخطيط الشبكة قد لا تدعم استعلامات الميزات. ويعني ذلك أن المقاربة التي فصلناها في المثال السابق ستعمل أيضًا جيدًا في تلك المتصفحات. فما نفعله هو كتابة تنسيقات CSS القديمة أولًا خارج إطار الاستعلام. فالمتصفحات التي لا تدعم الشبكة ولا تدعم استعلام الميزات ستستعمل معلومات التنسيق القديم التي تفهمها وتتجاهل كليًا أي شيء آخر. أما المتصفحات التي تدعم استعلام الميزات وتدعم تخطيط الشبكة ستنفذ تنسيقات الشبكة الموجودة ضمن استعلام الميزات وتتجاهل كل شيء آخر. تتضمن توصيفات استعلام الميزات القدرة على اختبار عدم قدرة المتصفح على دعم ميزة وهذا مفيد فقط إن دعم المتصفح استعلام الوسائط. سنرى مستقبلًا مقاربة مبنية على التحقق من عدم دعم المتصفح للميزات، إذ ستختفي المتصفحات التي لا تدعم استعلامات الميزات. أما الآن، فعليك استخدام مقاربة التنسيقات القديمة أولًا ثم تجاوزها إذا دعم المتصفح الميزات الأحدث. نسخ من خاصيات الشبكة الخاصة بمتصفح إنترنت إكسبلورر 11 و10 ضمّت توصيفات شبكة CSS دعمًا أوليًا للمتصفح إنرتنت إكسبلور 10. وطالما أن إنترنت إكسبلورر بنسختيه 10 و 11 لا يقدم دعمًا للشبكات العصرية، فهو لا يمتلك نسخة جيدة من تخطيط الشبكة يمكن استخدامها. ولدعم تخطيط الشبكة في هذين المتصفحين توضع البادئة -ms- قبل اسم الخاصية، ويعني ذلك إمكانية استخدام هذه الخاصية لدعم إنترنت إكسبلورر 10 و11 وستتجاهل بقية المتصفحات هذه الخاصيات. مع ذلك لا يزال مايكروسوفت إيدج قادرًا على فهم الصياغة القديمة، لهذا لا بد من الانتباه جيدًا والتأكد من تجاوز الخاصيات القديمة بأمان إن كنت تعمل على تخطيط شبكة عصري. وعمومًا إن لم يكن عدد مستخدمي إنترنت إكسبلورر من زائري موقعك كبيرًا، قد يكون من الأفضل التركيز على إنجاز نسخة تراجع تعمل جيدًا مع جميع المتصفحات التي لا تدعم تخطيط الشبكة العصري. اختبار المتصفحات الأقدم تدعم معظم المتصفحات الحديثة تخطيطي الصندوق المرن وتخطيط الشبكة، وسيكون من الصعب أن تختبر المتصفحات القديمة. من الطرق التي قد تنفع في هذا الحالة استخدام أدوات اختبار مثل Sauce Labs. بإمكانك أيضًا تنزيل وتثبيت محاكيات افتراضية ومن ثم تشغيل نسخ أقدم من المتصفحات ضمن بيئة معزولة. إن كنت ترى أن دعم إنترنت إكسبلورر ضروري، ستجد مجموعة من المحاكيات التي تقدمها مايكروسوفت مجانًا، وهي متاحة لأنظمة تشغيل ويندوز وماك ولينكس وهي بالفعل طريقة ممتازة لاختبار متصفحات ويندوز القديمة والحديثة حتى لو لم تكن تستخدم حاسوبًا يعمل على هذا النظام. الخلاصة لديك الآن كل ما تحتاجه من المعلومات لاستخدام تقنيات مثل تخطيط الشبكة وإنشاء نسخ تراجع خاصة بالمتصفحات الأقدم، ولاستخدام أية تقنيات جديدة قد تظهر في المستقبل. ترجمة -وبتصرف لمقال Supporting older browsers اقرأ أيضًا المقال السابق: الأساليب القديمة في تخطيط صفحات الويب كيف تتحقّق من الخصائص المدعومة في المتصفحات سهولة الوصول كيفية تصميم جسم صفحة موقع إلكتروني باستخدام CSS أساسيّات التَمَوْضُع على صفحات الويب (CSS Positioning 101)
  21. يُعد استخدام الشبكة Grid ميزة من ميزات تخطيط الصفحات اعتمادًا على CSS. لكن ما جرى قبل ظهور هذه الخاصية هو اعتماد أسلوب تعويم العناصر float أو استخدام ميزات أخرى. إذًا كان عليك أن تتخيل مثلًا صفحتك بعدة أعمدة ( 4 أو 6 أو 12 وهكذا) ومن ثم العمل على ترتيب المحتوى ضمن هذه الأعمدة التخيلية. ما ستكتشفه في هذا المقال هي الأساليب القديمة في تخطيط الصفحات كي تفهم طريقة عملها إن اضطررت للعمل على مشروع قديم. عليك قبل البدء في قراءة هذا المقال أن: تطلع على أساسيات HTML. تفهم أساسيات عمل CSS. تخطيط الصفحات والشبكات قبل ظهور تخطيط شبكات CSS قد يتفاجأ القادمون الجدد الذين لديهم خلفية في التصميم أن شبكات CSS لم تظهر إلا مؤخرًا، وقد استخدمت قبلها أساليب متنوعة ليست مثالية في إنشاء تصميمات شبيهة بالشبكات، ندعوها الآن بالأساليب القديمة أو الموروثة legacy. تعتمد المشاريع الحديثة على تخطيط شبكات CSS برفقة واحدة أو أكثر من أساليب التخطيط الحديثة لإنشاء أساس لأي تخطيط. لكن، قد تصادف تخطيط شبكة يعتمد على الأساليب القديمة بين الفينة والأخرى، لهذا من المفيد أن تتعرف على طريقة عملها ولماذا تختلف هذه الشبكات عن شبكات CSS. سنشرح في هذا المقال كيف تعمل منظومات الشبكات القديمة وإطارات عمل الشبكات grid frameworks اعتمادًا على التعويم ومبدأ الصندوق المرن. وقد تتفاجأ إن درست شبكات CSS بتعقيد هذه المنظومات، لكن معرفتك ستساعدك على كتابة شيفرة تراجع أو شيفرة آمنة من أجل المتصفحات التي لا تدعم الأساليب الحديثة في التخطيط. إضافة إلى ذلك، ستكون قادرًا على العمل على مشاريع سابقة تعتمد هذه الطرق القديمة في التخطيط. ومن المهم أن تتذكر دائمًا أن منظومات الشبكات القديمة لا تعمل إطلاقًا بالطريقة التي يعمل بها تخطيط شبكات CSS، بل تعتمد فكرة إعطاء العناصر أبعادًا محددةً ثم دفعها بطريقة تجعلها تبدو أنها شبكة. تخطيط من عمودين لنبدأ بأكثر الأمثلة بساطة وهو تخطيط مكون من عمودين. بإمكانك متابعة العمل معنا بإنشاء ملف باسم index.html على حاسوبك ثم نقل قالب HTML الخاص بالمثال إليه ومن ثم وضع الشيفرة التالية في أماكنها المناسبة ضمن القالب. سترى في نهاية المثال كيف ستبدو نتيجة العمل مباشرة. أولًا لابد من توفير محتوىً ما لوضعه ضمن الأعمدة، لهذا، استبدل كل ما هو موجود ضمن جسم الصفحة بالشيفرة التالية: <h1>2 column layout example</h1> <div> <h2>First column</h2> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla luctus aliquam dolor, eu lacinia lorem placerat vulputate. Duis felis orci, pulvinar id metus ut, rutrum luctus orci. Cras porttitor imperdiet nunc, at ultricies tellus laoreet sit amet. Sed auctor cursus massa at porta. Integer ligula ipsum, tristique sit amet orci vel, viverra egestas ligula. Curabitur vehicula tellus neque, ac ornare ex malesuada et. In vitae convallis lacus. Aliquam erat volutpat. Suspendisse ac imperdiet turpis. Aenean finibus sollicitudin eros pharetra congue. Duis ornare egestas augue ut luctus. Proin blandit quam nec lacus varius commodo et a urna. Ut id ornare felis, eget fermentum sapien. </p> </div> <div> <h2>Second column</h2> <p> Nam vulputate diam nec tempor bibendum. Donec luctus augue eget malesuada ultrices. Phasellus turpis est, posuere sit amet dapibus ut, facilisis sed est. Nam id risus quis ante semper consectetur eget aliquam lorem. Vivamus tristique elit dolor, sed pretium metus suscipit vel. Mauris ultricies lectus sed lobortis finibus. Vivamus eu urna eget velit cursus viverra quis vestibulum sem. Aliquam tincidunt eget purus in interdum. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. </p> </div> يحتاج كل عمود إلى عنصر خارجي كي يضم محتواه لهذا سنحاول فعل ذلك. اخترنا في مثالنا العنصر الحاوي <div>، وبإمكانك أيضًا اختيار عنصر آخر أكثر ملائمة من ناحية الدلالة مثل <article> و <section> و <aside>. أما بالنسبة لتنسيق CSS، فعليك أولًا تطبيق ما يلي على صفحتك لضبطها مبدئيًا: body { width: 90%; max-width: 900px; margin: 0 auto; } سيشكل جسم الصفحة 90% من نافذة العرض لكن دون أن يزيد عن 900 بكسل وعندها سيحافظ على هذا الاتساع ويتمركز في وسط الصفحة. ستمتد العناصر الأبناء له (العنصر <h1> والعنصران <div>) حتى يشغلا كامل اتساع الجسم. ولو أردنا أن يعوم العنصران <div> إلى جوار بعضهما، لا بد عندها من ضبط اتساعهما ليشغلا 100% من اتساع العنصر الأب أو أقل. لهذا أضف الشيفرة التالية إلى نهاية تنسيقات CSS: div:nth-of-type(1) { width: 48%; } div:nth-of-type(2) { width: 48%; } ضبطنا اتساع كلا العنصرين ليشغل 48% من مساحة العنصر الأب وبالتالي سيشغلان معًا 96% من المساحة الكلية له، وتركنا 4% لتمثل قناة تفصل بينهما وتعطي المحتوى مجالًا للتنفس. علينا الآن تعويم الأعمدة كالتالي: div:nth-of-type(1) { width: 48%; float: left; } div:nth-of-type(2) { width: 48%; float: right; } تكون النتيجة النهائية للمثال كالتالي: See the Pen Legacy layout by Hsoub Academy (@HsoubAcademy) on CodePen. ستلاحظ أننا استخدمنا نسب مئوية لتقدير الاتساعات، وهي استراتيجية جيدة كونها تنشئ تخطيطًا مرنًا أو "سائلًا" يمكن ضبطه ليلائم مختلف أبعاد الشاشات ويحافظ على نفس نسبة الاتساع الذي يأخذه كل عمود ضمن الشاشات الأصغر. حاول أن تغيّر أبعاد نافذة المتصفح وراقب ما يحدث! ملاحظة: يمكنك أن تتابع طريقة عمل هذا المثال على جيت-هاب (كما يمكنك الاطلاع على شيفرته المصدرية) إنشاء إطار عمل لشبكة وفق الأسلوب القديم تستخدم معظم أطر العمل القديمة سلوك التعويم float كي تعوّم أحد الأعمدة إلى جانب الآخر والحصول على تخطيط يشبه الشبكة. ويساعدك العمل على إنشاء شبكة من هذا النوع في معرفة طريقة عملها ويقدم لك بعض المفاهيم المتقدمة كي تبني على الأساسيات التي تعلمناها في مقال تعويم العناصر في CSS. يُعد إطار العمل الأسهل لهذا النوع من الشبكات هو الإطار ذو الاتساع الثابت. وكل ما نحتاجه هو معرفة مقدار الاتساع المطلوب لتصميمنا، وكم عدد الأعمدة وما هو اتساعها واتساع الأقنية الفاصلة بينها. بينما إن أردنا أن نبني شبكتنا على شكل أعمدة تتسع وتتقلص وفقًا لاتساع نافذة المتصفح، لا بد حينها من حساب اتساع الأعمدة والأقنية كنسب مئوية. سنلقي نظرة في القسم التالي على كيفية إنشاء التخطيطين السابقين، وسننشئ شبكة من 12 عمودًا وهو خيار شائع جدًا وواسع الاستخدام لحالات كثيرة كونه عدد قابل للقسمة على 2 و 4 و 6. شبكة بسيطة ثابتة الاتساع لننشئ بداية شبكة ذات أعمدة ثابتة الاتساع، وعليك أن تبدأ بإنشاء نسخة عن ملف المثال على حاسوبك والتي تضم الشيفرة التالية لجسم الصفحة: <div class="wrapper"> <div class="row"> <div class="col">1</div> <div class="col">2</div> <div class="col">3</div> <div class="col">4</div> <div class="col">5</div> <div class="col">6</div> <div class="col">7</div> <div class="col">8</div> <div class="col">9</div> <div class="col">10</div> <div class="col">11</div> <div class="col">12</div> </div> <div class="row"> <div class="col span1">13</div> <div class="col span6">14</div> <div class="col span3">15</div> <div class="col span2">16</div> </div> </div> إن الغاية من ذلك الحصول على شبكة من سطرين و 12 عمودًا، إذ يظهر السطر الأول قياس كل عمود بينما يعرض السطر الثاني مناطق مختلفة القياسات من الشبكة. أضف ضمن العنصر <style> الشيفرة التالية والتي تعطي لحاوية التغليف اتساعًا مقداره 980 بكسل مع حشوة إلى جهة اليمين مقدارها 20 بكسل. يترك لنا ذلك اتساعًا مقداره 960 بكسل للأعمدة والأقنية الفاصلة بينها، والسبب في أن الحشوة قد استهلكت مساحة من الاتساع الكلي هو أننا ضبطنا قيمة الخاصية box-sizing على القيمة border-box: * { box-sizing: border-box; } body { width: 980px; margin: 0 auto; } .wrapper { padding-right: 20px; } استخدم الآن حاوية السطر التي تغلف كل سطر من أسطر الشبكة لتمييز كل سطر عن الآخر وذلك من خلال إضافة شيفرة التنسيق التالية بعد شيفرة التنسيق السابقة: .row { clear: both; } ويعني تطبيق هذا التباعد أنه لا ضرورة لملئ السطر بالعناصر حتى نحصل على 12 عمودًا، بل ستبقى الأسطر منفصلة وغير متداخلة مع بعضها. أما بالنسبة للأقنية الفاصلة فقط جعلنا اتساعها 20 بكسل وأنشأناها على شكل هامش إلى يسار العمود بما في ذلك العمود الأول وذلك لموازنة الحشوة الموجودة إلى يمين الحاوية والتي كان اتساعها 20 بكسل. وبالتالي لدينا الآن 12 قناة تفصل بين الأعمدة اتساعها 12x20=240. لا بد من طرح المساحة السابقة من 960 بكسل وتكون النتيجة 720 بكسل للأعمدة جميعها وبتقسيم هذه القيمة على 12 وهو عدد الأعمدة سيكون اتساع العمود 60 بكسل. ننشئ كخطوة ثانية الصنف col. الذي يضبط العمود ويعوّمه إلى اليسار، إذ ضبطنا فيه الخاصية margin-left على القيمة 20px لإنشاء قناة فصل، ومنحناه اتساعًا width مقداره 60 بكسل، إليك قواعد التنسيق اللازمة: .col { float: left; margin-left: 20px; width: 60px; background: rgb(255, 150, 150); } سيظهر السطر الأول بأعمدة مفردة وكأنه شبكة. ملاحظة: منحنا كل عمود لونًا أحمر فاتح لكي ترى تمامًا الاتساع الذي يشغله. أما تخطيط الحاوية التي نريدها أن تمتد إلى عدة أعمدة فيتطلب أصنافًا خاصة لضبط قيم الاتساع للأعمدة المطلوبة إضافة إلى اتساع الأقنية بينها. نحتاج في مثالنا إلى صنف جديد ليسمح للحاويات أن تمتد بين العمودين 2 و 12. وينتج كل اتساع عن إضافة اتساع كل عمود من تلك الأعمدة إلى اتساع أقنية الفصل والتي عددها أقل دائمًا بواحد من عدد الأعمدة. أضف الشيفرة التالية إلى آخر شيفرة CSS: /* Two column widths (120px) plus one gutter width (20px) */ .col.span2 { width: 140px; } /* Three column widths (180px) plus two gutter widths (40px) */ .col.span3 { width: 220px; } /* And so on… */ .col.span4 { width: 300px; } .col.span5 { width: 380px; } .col.span6 { width: 460px; } .col.span7 { width: 540px; } .col.span8 { width: 620px; } .col.span9 { width: 700px; } .col.span10 { width: 780px; } .col.span11 { width: 860px; } .col.span12 { width: 940px; } بهذه الأصناف التي أنشأناها سنتمكن من وضع أعمدة مختلفة الاتساع ضمن الشبكة. حاول أن تحفظ التغييرات وتعيد تحميل الصفحة لترى تأثيرها. ملاحظة: إن وجدت صعوبة في تطبيق المثال السابق، الق نظرة عليه بشكله النهائي على جت-هاب (تستطيع أيضًا تنفيذه مباشرة هناك). حاول أن تعدّل الأصناف التي تطبقها على عناصرك أو حتى إضافة أو إزالة بعض الحاويات، لترى كيف يمكن أن يتغير التخطيط. إذ يمكنك مثلًا أن تجعل السطر الثاني يبدو كالتالي: <div class="row"> <div class="col span8">13</div> <div class="col span4">14</div> </div> لقد حصلنا الآن على شبكة يمكنك فيها تعريف عدد الأسطر والأعمدة في كل منها، ومن ثم وضع المحتوى الذي تريد في كل حاوية. إنشاء شبكة مرنة أو "سائلة" تعمل كما رأينا الشبكة السابقة جيدًا، لكن مشكلتها هو اتساعها الثابت. لكن ما نحتاجه حقًا هو شبكة مرنة تنمو وتتقلص وفقًا لاتساع نافذة عرض المتصفح. ولإنجاز الأمر، يمكن تحويل وحدات الاتساع من البكسل إلى نسب مئوية. تُعطى المعادلة التي تحوّل الاتساع الثابت إلى نسبة مئوية مرنة التالي: target / context = result بالنسبة إلى مثالنا سيكون الاتساع المستهدف للعمود target هو 60 بكسل والسياق أو الاتساع الكلي context هو 960 بكسل. سنستخدم هذه المعادلة لحساب النسب المئوية: 60 / 960 = 0.0625 بإزاحة الفاصلة العشرية مرتبتين إلى اليمين نحصل على النسبة المئوية 6.25%، ونستطيع الآن استبدال اتساع العمود المقدّر 60 بكسل بالنسبة المئوية 6.25%. سنفعل الشيء نفسه لضبط اتساع الأقنية الفاصلة: 20 / 960 = 0.02083333333 لهذا نستبدل قيمة الخاصية margin-left في الصنف col. وقيمة الخاصية padding-right في الصنف wrapper. لتصبحان 2.083%. تحديث الشبكة في مثالنا لتبدأ العمل في هذا القسم أنشئ نسخة جديدة عن المثال السابق على جهازك أو نسخة جديدة عن ملف المثال لتستخدمه كنقطة انطلاق. غيّر قاعدة التنسيق الثانية في المحدد wrapper. كالتالي: body { width: 90%; max-width: 980px; margin: 0 auto; } .wrapper { padding-right: 2.08333333%; } لم نغير قيمة الاتساع width لتصبح كنسبة مئوية فقط، بل أضفنا الخاصية max-width كي لايصبح التخطيط واسعًا أكثر مما هو مطلوب. عدّل بعد ذلك القاعدة الرابعة في المحدد col. كالتالي: .col { float: left; margin-left: 2.08333333%; width: 6.25%; background: rgb(255, 150, 150); } سنبدأ الآن الجزء الذي يتطلب مزيدًا من العمل، ونحتاج إلى تحديث كل قواعد المحددات col.span. كي تستخدم النسب المئوية بدلًا من البكسل. يستغرق الأمر وقتًا لإنجاز الحسابات، لكن لتوفير الوقت أجريت هذه الحسابات مسبقًا. حدّث الكتلة السفلية من قواعد CSS كالتالي: /* Two column widths (12.5%) plus one gutter width (2.08333333%) */ .col.span2 { width: 14.58333333%; } /* Three column widths (18.75%) plus two gutter widths (4.1666666) */ .col.span3 { width: 22.91666666%; } /* And so on… */ .col.span4 { width: 31.24999999%; } .col.span5 { width: 39.58333332%; } .col.span6 { width: 47.91666665%; } .col.span7 { width: 56.24999998%; } .col.span8 { width: 64.58333331%; } .col.span9 { width: 72.91666664%; } .col.span10 { width: 81.24999997%; } .col.span11 { width: 89.5833333%; } .col.span12 { width: 97.91666663%; } احفظ التغييرات التي أجريتها على الشيفرة وأعد تحميل الصفحة ثم حاول تغيير اتساع نافذة المتصفح. من المفترض أن ترى كيف يتغير اتساع الأعمدة بما يلائم اتساع نافذة المتصفح. ملاحظة: إن وجدت صعوبة في تطبيق المثال السابق، الق نظرة عليه بشكله النهائي على جت-هاب (تستطيع أيضًا تنفيذه مباشرة). إجراءات حسابات أسهل باستخدام الدالة ()calc بإمكاننا استخدام الدالة ()calc لتنفيذ العمليات الحسابية ضمن شيفرة CSS، إذ تسمح لك بإدخال معادلات رياضية بسيطة لحساب قيمة CSS. هذه الدالة مفيدة خصوصًا عندما تضطر لتنفيذ علاقات رياضية معقدة، كما تساعد في تنفيذ حسابات باستخدام وحدات مختلفة كان تريد مثلًا أن يكون ارتفاع العنصر 100% من ارتفاع العنصر الأب دائمًا بينما يكون ارتفاعه 50 بكسل. بالعودة إلى شبكتنا، نجد أن أي عمود يمتد ليغطي أكثر من عمود في شبكتنا له الاتساع 6.25% مضروبًا بعدد الأعمدة التي يمتد عليها ومضافًا إليه الاتساع 2.083% مضروبًا بعدد الأقنية الفاصلة (وهو عدد الأعمدة ناقصًا واحد). تسمح لنا الدالة ()calc بإجراء الحسابات السابقة ضمن قيمة الخاصية width، فمن أجل أي عمود يمتد على أربعة أعمدة مثلًا يمكن تنفيذ الشيفرة التالية: .col.span4 { width: calc((6.25% * 4) + (2.08333333% * 3)); } جرّب أن تستبدل الشيفرة في كتلة CSS الأخيرة بالقواعد التالية، ثم أعد تحميل الصفحة لترى النتيجة: .col.span2 { width: calc((6.25% * 2) + 2.08333333%); } .col.span3 { width: calc((6.25% * 3) + (2.08333333% * 2)); } .col.span4 { width: calc((6.25% * 4) + (2.08333333% * 3)); } .col.span5 { width: calc((6.25% * 5) + (2.08333333% * 4)); } .col.span6 { width: calc((6.25% * 6) + (2.08333333% * 5)); } .col.span7 { width: calc((6.25% * 7) + (2.08333333% * 6)); } .col.span8 { width: calc((6.25% * 8) + (2.08333333% * 7)); } .col.span9 { width: calc((6.25% * 9) + (2.08333333% * 8)); } .col.span10 { width: calc((6.25% * 10) + (2.08333333% * 9)); } .col.span11 { width: calc((6.25% * 11) + (2.08333333% * 10)); } .col.span12 { width: calc((6.25% * 12) + (2.08333333% * 11)); } ملاحظة1: إن وجدت صعوبة في تطبيق المثال السابق، الق نظرة عليه بشكله النهائي على جت-هاب (تستطيع أيضًا تنفيذه مباشرة). ملاحظة2: قد لا تتمكن من تطبيق المثال لأن المتصفح لا يدعم الدالة ()calc مع أنها مدعومة جيدًا في أغلب المتصفحات وصولًا إلى مايكروسوفت إنترنت إكسبلورر IE9. منظومات الشبكات الدلالية وغير الدلالية إن إضافة أصناف التنسيق إلى شيفرة HTML لتعريف التخطيطات يعني أن شيفرتك ومحتوى صفحتك مرهونان تمامًا بمنظورك الشخصي، لهذا قد تسمع أن هذه الطريقة في استخدام أصناف CSS هي طريقة غير دلالية أي لا تصف تمامًا كيف ينظَّم المحتوى بدلًا من استخدام الأصناف لتوصيف المحتوى، وهذا هو الأمر في حالتنا مع الأصناف span. لا يُعد النهج الذي استخدمناه هو المنهج الوحيد، بل يمكنك أن تقرر ما هي أبعاد شبكتك ثم تضيف معلومات عن هذه الأبعاد إلى قواعد الصفوف الدلالية الموجودة. فلو كان لديك العنصر <div> الذي يمتلك الصنف content وأردته أن يمتد على 8 أعمدة، يمكن إضافة القاعدة التالية إلى المحدد content.: .content { width: calc((6.25% * 8) + (2.08333333% * 7)); } ملاحظة: إن كنت تستخدم معالج أولي للتنسيق مثل Sass تكون قادرًا على إنشاء توجيه mixin@ لإدراج تلك القيم في أي مكان تريده من الصفحة. تمكين حاويات الإزاحة في الشبكة تعمل شبكتنا جيدًا طالما أننا نريد أن تتدفق الحاويات ابتداءً من الطرف اليميني للشبكة. لكن إن أردنا ترك فراغ بحجم عمود قبل أول حاوية أو بين حاويتين، لا بد عندها من إيجاد نوع من الإزاحة باستخدام صنف تنسيق يضيف هامشًا إلى اليسار كي يدفع الموقع عبر الشبكة. لنحاول أن نفعل ذلك، ولنبدأ بالشيفرة السابقة أو استخدم ملف المثال كنقطة انطلاق. شننشئ أيضًا صنفًا يعمل على إزاحة الحاوية بمقدار اتساع عمود واحد. أضف الشيفرة التالية في شيفرة CSS: .offset-by-one { margin-left: calc(6.25% + (2.08333333% * 2)); } أو إن أردت حساب النسب المئوية لاتساع الأعمدة بنفسك، استخدم الشيفرة التالية: .offset-by-one { margin-left: 10.41666666%; } بإمكانك الآن إضافة هذا الصنف إلى أية حاوية تريد كي تترك مساحة فارغة باتساع عمود من ناحية اليسار. فلو كان لدينا مثلًا شيفرة HTML التالية: <div class="col span6">14</div> استبدلها بهذه الشيفرة: <div class="col span5 offset-by-one">14</div> ملاحظة: انتبه إلى ضرورة تقليل عدد الأعمدة التي تريدها أن تمتد كي تترك مساحة للإزاحة. جرّب تحميل وتحديث الصفحة لترى الفرق، وبإمكانك أن ترى المثال على جيت-هاب وأن تجربته هنالك مباشرة. سيظهر المثال بشكله النهائي كالتالي: تمرين: هل بإمكانك كتابة صنف offset-by-two لإزاحة عمود بمقدار عمودين؟ محدودية الشبكة المعوّمة عند استخدام شبكة كهذه لا بد من حساب مجموع الاتساعات الإجمالي بشكل صحيح، وأن لا تضع عناصر في صف تجعله يمتد إلى أعمدة أكثر مما يمكنه أن يضم. ونظرًا للطريقة التي يعمل وفقها مبدأ التعويم، إن أصبح عدد أعمدة الشبكة أكثر مما تستوعبه الشبكة ستنتقل العناصر في نهاية السطر إلى سطر جديد وتخرّب الشبكة. تذكر أيضًا، إن ازداد اتساع محتوى عنصر أكثر من قدرة السطر على استيعابه سيطفح المحتوى خارج العمود ويبدو الأمر كارثيًا. وتبقى المحدودية الأهم لهذا الشبكة هي أنها وحيد البعد، فما نفعله هو التعامل مع أعمدة وجعل العناصر تتمدد على عدد منها ولا نتعامل مع أسطر فعليًا. من الصعب إذًا التعامل مع هذه التخطيطات القديمة للتحكم بارتفاع العناصر دون أن تحدد هذه الإرتفاع صراحة وهذه مقاربة لا تتمتع بالمرونة إطلاقًا، فلا يمكن أن تضمن النتائج ما لم تضمن أنّ المحتوى سيكون دائمًا بارتفاع محدد. شبكات الصندوق المرن إن اطلعت على مقالنا السابق حول [تخطيط الصندوق المرن]() فقد ترى أن هذا التخطيط هو الحل المثالي لإنشاء شبكة عناصر. إذ توجد بالفعل الكثير من تخطيطات الشبكات المعتمدة على الصندوق المرن وتخطيطات صندوق مرن التي تحل الكثير من المشاكل التي شرحناها في هذا المقال. لكن لم يُصمم الصندوق المرن لإنشاء منظومة شبكة لهذا ستظهر مشاكل عدة عند استخدامه. وكمثال بسيط يشرح ما نقصده، سنعود إلى نفس شيفرة المثال السابق ومن ثم نستخدم شيفرة CSS التالية لتنسيق الأصناف wrapper و raw و col: body { width: 90%; max-width: 980px; margin: 0 auto; } .wrapper { padding-right: 2.08333333%; } .row { display: flex; } .col { margin-left: 2.08333333%; margin-bottom: 1em; width: 6.25%; flex: 1 1 auto; background: rgb(255, 150, 150); } حاول أن تجري التعديلات السابقة على نسختك من المثال أو الق نظرة على نسخته على جت-هاب ( كما يمكنك الاطلاع على طريقة عمله أيضًا) ما فعلناه هنا هو تحويل كل سطر إلى حاوية مرنة فلا زلنا نحتاج إلى الأسطر في شبكات الصندوق المرن كي نستطيع استخدام العناصر التي يصل مجموع اتساعاتها إلى أقل من 100%. لهذا ضبطنا الخاصية display للحاوية على القيمة flex. ضبطنا أيضًا قيمة الخاصية flex في الصنف col. على القيمة 1 لتجعل العنصر يتمدد، وكذلك الخاصية flex-shrink على القيمة 1 كي يتقلص العنصر. أما قيمة الخاصية flex-basis فكانت auto لأن العنصر له اتساع محدد، وتستخدم تلك القيمة هذا الاتساع كقيمة للخاصية flex-basis. لدينا 12 صندوق في السطر الأول ضمن شبكة تنمو وتتقلص بشكل متماثل مع تغيّر اتساع نافذة العرض. وفي السطر الثاني لدينا فقط 4 أعمدة تتمدد وتتقلص أيضًا على أساس الاتساع المحدد 60 بكسل. وهكذا ستتمكن الأعمدة الأربعة في السطر الثاني من التمدد أكثر من الأعمدة في السطر الأول وستشغل هذه الأعمدة مساحة السطر الثاني بأكملها: ولإصلاح الأمر لابد من استخدام الأصناف span لتزويدنا باتساع يستبدل تلك القيمة التي تستخدمها الخاصية flex-basis للعنصر المحدد. وهذه الأصناف لا تحترم أيضًا تخطيط الشبكة الذي تنتظم وفقه العناصر لأنها لا تعرف شيئًا عن الشبكة. فالصندوق المرن تخطيط وحيد البعد ويتعامل مع أسطر أو أعمدة وليس الاثنان معًا. لهذا السبب لا يمكن إنشاء شبكة صريحة من أعمدة وأسطر، وهذا يعني أننا لا زلنا بحاجة إلى حساب نسب مئوية للأبعاد كما في عملية تعويم العناصر floating. مع ذلك قد تقرر استخدام الشبكة المرنة بدلًا من التعويم لأنها تقدم ميزات أفضل من ناحية المحاذاة وتوزيع المساحات الفارغة. لكن لا بد من الانتباه أنك تستخدم أداة لم تخصص لهذا الغرض، وقد تشعر أنك تدور في حلقات إضافية حتى تصل إلى النتيجة المطلوبة. شبكات تؤمنها أطراف خارجية أخرى بعد أن تعرفنا على طريقة حساب الأبعاد في شبكتنا، بإمكاننا الآن استيعاب بعض المنظومات التي صممتها أطراف أخرى للاستخدامات العامة. فإن بحثت في الويب عن " أطر عمل شبكات CSS" أو "CSS Grid framework" ستجد عددًا كبيرًا من الخيارات منها Bootstrap و Foundation والتي تضم تخطيط شبكة خاصة بها. كما ستجد منظومات شبكات مستقلة جرى تطويرها باستخدام CSS أو باستخدام معالجات أولية. لنلق نظرة على إحدى هذه المنظومات المستقلة لنعرض التقنيات الشائعة في إطارات عمل الشبكات. سنستخدم شبكة تمثلًا جزءًا من إطار العمل Skeleton وهو إطار عمل CSS بسيط. لهذا انتقل إلى موقع الويب الخاص بإطار العمل ثم نزّل الملف المضغوط الخاص بإطار العمل واستخرج ملفاته إلى حاسوبك ثم انسخ الملفين skeleton.css و normalize.css إلى مجلد جديد. انسخ أيضًا الملف html-skeleton.html إلى نفس المجلد السابق. اربط الملفين Normalize و skeleton بصفحة الويب بإضافة ما يلي إلى ترويسة الصفحة: <link href="normalize.css" rel="stylesheet" /> <link href="skeleton.css" rel="stylesheet" /> يتضمن الملف أكثر من نظام شبكة كما يضم شيفرة لتنسيق خط الكتابة وتنسيق عناصر أخرى يمكنك الاستفادة منها كنقطة انطلاق. سنترك كل شيء كما هو افتراضيًا، فالشبكة الافتراضية هي التي نحتاجها حاليًا. ملاحظة: تُعد المكتبة Normalize مكتبة تنسيقات CSS مفيدة حقًا كتبها نيكولاس غالفر، وتجري بعض الإصلاحات الأساسية على التخطيط ليظهر التنسيق الافتراضي للعناصر أكثر اتساقًا ضمن المتصفح. نستخدم تاليًا نفس شيفرة HTML للمثال السابق، لهذا أضف ما يلي إلى جسم ملف HTML: <div class="container"> <div class="row"> <div class="col">1</div> <div class="col">2</div> <div class="col">3</div> <div class="col">4</div> <div class="col">5</div> <div class="col">6</div> <div class="col">7</div> <div class="col">8</div> <div class="col">9</div> <div class="col">10</div> <div class="col">11</div> <div class="col">12</div> </div> <div class="row"> <div class="col">13</div> <div class="col">14</div> <div class="col">15</div> <div class="col">16</div> </div> </div> كي نبدأ باستخدام Skeleton لا بد أن يمتلك عنصر التغليف <div> الصنف container وهو موجود أصلًا في ملف HTML. ويجمّع هذا الصنف المحتوى في مركز الحاوية التي تأخذ اتساع أعظمي مقداره 960 بكسل. بإمكانك أن ترى كيف لن يتجاوز اتساع الصناديق 960 بكسل. إن ألقيت نظرة على الملف skeleton.css سترى التنسيقات التي تُطبّق عند استخدام ذلك الصنف. إذ سترى كيف يتوضع المحتوى في مركز الحاوية باستخدام هوامش يمينية ويسارية بقيمة auto، كما تُطبّق حشوات يسارية ويمينية مقدارها 20 بكسل.وتُضبط أيضًا قيمة الخاصية box-sizing على border-box كما فعلنا سابقًا كي يجري تضمين اتساع الحشوات والإطار ضمن اتساع الحاوية الكلي. .container { position: relative; width: 100%; max-width: 960px; margin: 0 auto; padding: 0 20px; box-sizing: border-box; } يكون العنصر جزءًا من الشبكة إن كان ضمن سطر row، لهذا سنحتاج -كما هو الحال في مثالنا السابق- إلى عنصر <div> آخر يمتلك الصنف ويأتي بين العنصر <div> الخاص بالمحتوى والعنصر <div> الخاص بالحاوية. لنرتب الآن الصناديق ضمن الحاوية، فالإطار Skeleton مكوّن من شبكة ذات 12 عمودًا. وتحتاج صناديق السطر الأول إلى أصناف one column كي تجعلها تمتد على عمود واحد. أضف الشيفرة التالية: <div class="container"> <div class="row"> <div class="one column">1</div> <div class="one column">2</div> <div class="one column">3</div> /* and so on */ </div> </div> اعط الآن حاويات السطر الثاني أصنافًا تشرح عدد الأعمدة التي ستمتد عليها هذه الحاويات كالتالي: <div class="row"> <div class="one column">13</div> <div class="six columns">14</div> <div class="three columns">15</div> <div class="two columns">16</div> </div> احفظ التغييرات على ملف HTML وحمّل الصفحة من جديد في المتصفح لترى ما يحدث! ملاحظة: إن واجهت مشكلة في تشغيل هذا المثال، حاول أن توسّع نافذة العرض التي تستخدمها فلن تُعرض الشبكة كما وصفنا إن كانت نافذة العرض ضيّقة. لم ينفع الأمر، حاول أن توازن ما فعلت بالملف html-skeleton-finished.html (أو اطّلع على طريقة تنفيذه مباشرة). إن ألقيت نظرة على الملف skeleton.css بإمكانك معرفة كيف يعمل المثال. فالملف يتضمن الأصناف "three columns" التالية لتنسيق العناصر بثلاثة أعمدة: .three.columns { width: 22%; } إن إطار العمل Skeleton وغيره من إطارات عمل الشبكات يُعرّف مسبقًا أصناقًا يمكنك استخدامها في صفحاتك. وتعمل كما لو أجريت الحسابات بالنسبة المئوية بنفسك. وكما ترى، لن تحتاج إلى كتابة الكثير من التنسيقات باستخدام Skeleton، فهي تتعامل مع العناصر المعوّمة نيابة عنا بمجرد إضافة أصناف التنسيق الملائمة إلى شيفرة HTML. إن هذه القدرة على التحكم بالتخطيط هو ما يجعل إطارات العمل لبناء شبكات خيارًا جذّابًا. لكن، ومع ظهور تخطيط شبكة CSS، يبتعد المطوّرون عن استخدام أطر العمل لاستخدام تلك التقنية المدمجة التي تقدمها CSS بشكل أصيل. الخلاصة تعرّفنا في هذا المقال كيف تعمل مختلف منظومات الشبكات، وهذا أمر مهم عند العمل مع مواقع قديمة، ولفهم الفرق بين شبكة CSS الأصيلة وهذه الأنظمة الأقدم للشبكات. ترجمة -وبتصرف- للمقال: Legacy layout methods اقرأ أيضًا المقال السابق: دليلك إلى استعلامات الوسائط Media Queries في CSS التحكم في تخطيط الصفحة وضبط محاذاة العناصر في CSS مدخل إلى تخطيط صفحات الويب باستخدام CSS التخطيط متعدد الأعمدة باستخدام CSS
  22. يشير مصطلح prototype إلى اﻵلية التي ترث فيها الكائنات ميزات من بعضها في جافا سكريبت، ويختلف عملها عن الوراثة في غيرها من اللغات كائنية التوجه، وهذا ما سنشرحه في هذا المقال. ننصحك قبل أن تبدأ العمل معنا في هذه السلسلة أن تطلع على: أساسيات HTML. أساسيات عمل CSS أساسيات جافا سكريبت أساسيات البرمجة كائنية التوجه في جافا سكربت كما شرحناه في مقال أساسيات العمل مع الكائنات في جافا سكريبت. سلسلة من اﻷنماط المجرّدة حاول أن تنشئ في طرفية جافا سكريبت في متصفحك الكائن التالي: const myObject = { city: "Madrid", greet() { console.log(`Greetings from ${this.city}`); }, }; myObject.greet(); // Greetings from Madrid يمتلك الكائن خاصية واحدة لتخزين البيانات هي city وتابعًا واحدًا هو ()greet. فإن كتبت اسم الكائن تليه نقطة في الطرفية مثل .myobject، ستعرض الطرفية قائمة بجميع الخاصيات التي يمتلكها العنصر. وسترى إضافة إلى الخاصية city والتابع ()greet، الكثير من الخاصيات اﻷخرى! __defineGetter__ __defineSetter__ __lookupGetter__ __lookupSetter__ __proto__ city constructor greet hasOwnProperty isPrototypeOf propertyIsEnumerable toLocaleString toString valueOf جرّب الوصول إلى إحداها: myObject.toString(); // "[object Object]" لقد نجح اﻷمر (حتى لو لم يكن واضحًا لك بالضبط ما الذي يفعله التابع ()toString هنا). فما قصة هذه الخاصيات اﻹضافية، ومن أين أتت؟ في الواقع يملك كل كائن في جافا سكريبت خاصية مضمنة تُدعى prototype وهي بحد ذاتها كائن أيضًا ويضم بدوره خاصية أو كائن مجرّد إن صح التعبير، مما يوّلد ما يُدعى سلسلة prototype chain. تنتهي السلسلة عند الوصول إلى كائن قيمة الخاصية prototype له تساوي null. ملاحظة:لا تُدعى الخاصية التي تشير إلى prototype بالاسم prototype، إذ ليس لها اسم معياري لكنها تُكتب بالممارسة العملية بالشكل _proto_. وتُعد الطريقة المعيارية للوصول إلى الخاصية prototype هي استخدام التابع ()Object.getPrototypeOf. عندما تحاول الوصول إلى إحدى خاصيات كائن، ولم يُعثر على الخاصية في الكائن نفسه، يجري البحث عنها ضمن الكائن prototype، وإن لم يُعثر عليها يجري البحث مجددًا ضمن الكائن prototype للكائن prototype حتى نهاية السلسلة، فإن لم يجدها، سيُعيد القيمة undefined. فعندما تُنفّذ التعليمة ()myObject.toString: يبحث المتصفح عن التابع toString ضمن الكائن myObject. إن لم يجده، سيبحث عنه في الكائن prototype للكائن myObject. يجده هناك ويستدعيه. لكن ما هو prototype للكائن myObject؟ لمعرفة ذلك، يمكننا استخدام الأمر: Object.getPrototypeOf(myObject); // Object { } سنجد أن prototype هو كائن يُدعى Object.prototype، وهو أبسط الكائنات prototype، وتمتلكه جميع الكائنات افتراضيًا، والكائن prototype الخاص به هو null. لذا يقع هذا الكائن في نهاية سلسلة كائنات prototype. لكن لا يمثل Object.prototype دائمًا prototype لكل كائن، جرّب ما يلي لترى: const myDate = new Date(); let object = myDate; do { object = Object.getPrototypeOf(object); console.log(object); } while (object); // Date.prototype // Object { } // null تنشئ الشيفرة السابقة كائن من النوع Date، ثم تنتقل ضمن سلسلة كائنات prototype الخاصة به وتسجل أسماء هذه الكائنات. وتظهر أن النوع المجرد للكائن myDate هو Date.prototype وprototype الخاص بهذا اﻷخير هو Object.prototype. وعندما تستدعي توابع مثل ()mydate2.getMonth، فأنت تستدعي في واقع اﻷمر توابع معرّفة ضمن النوع Date.prototype. إخفاء الخاصيات ما الذي يحدث إن عرّفت خاصية في كائن وكانت هناك خاصية معرّفة بنفس الاسم ضمن الكائن prototype له؟ ألق نظرة على الشيفرة التالية: const myDate = new Date(1995, 11, 17); console.log(myDate.getYear()); // 95 myDate.getYear = function () { console.log("something else!"); }; myDate.getYear(); // 'something else!' لا بد أن تكون النتيجة التي حصلت عليها متوقعة. ووفقًا لوصف سلسلة الكائنات المجردة، سيبحث المتصفح عن الخاصية ()getYear ضمن خاصيات myDate التي تحمل هذا الاسم، ولا يتحقق من خاصيات الكائن المجرد إلا في الحالة التي لم نعرّف فيها هذه الخاصية. لهذا عندما أضفنا التابع ()getYear حرفيًا إلى الكائن myDate تُستدعى هذه النسخة مباشرة ويُعرف هذا اﻷمر بإخفاء الخاصية shadowing. إعداد كائنات prototype هناك طرق مختلفة ﻹعداد وضبط هذه الكائنات في جافا سكريبت، و سنناقش هنا طريقتان: الأولى باستخدام ()Object.create والثانية استخدام الدوال البانية constructors. استخدام التابع ()Object.create يُنشئ التابع ()Object.create كائنًا جديدًا ويسمح لك بتخصيص كائن ليصبح prototype الجديد الخاص به، إليك مثالًا: const personPrototype = { greet() { console.log("hello!"); }, }; const carl = Object.create(personPrototype); carl.greet(); // hello! أنشأنا في الشيفرة السابقة كائنًا باسم personPrototype، يمتلك التابع ()greet، ثم أنشأنا كائنًا جديدًا باستخدام التابع ()Object.create وجعلنا personPrototype كائن prototype له. وبالتالي نستطيع اﻵن استدعاء التابع ()greet من خلال الكائن الجديد، لأن كائن prototype قد زوّده به. استخدام الدالة البانية تمتلك جميع الدوال في جافا سكريبت خاصية تًدعى prototype. وعندما تستدعي الدالة على شكل دالة بانية، تُضبط تلك الخاصية لتكون prototype للكائن المبني حديثًا (داخل الخاصية التي تُدعى _proto_ تقليديًا). لهذا، وعندما نضبط القيمة prototype للدالة البانية، نضمن أن الكائنات التي تُنشئها هذه الدالة تمتلك كائن prototype: const personPrototype = { greet() { console.log(`hello, my name is ${this.name}!`); }, }; function Person(name) { this.name = name; } Object.assign(Person.prototype, personPrototype); // or // Person.prototype.greet = personPrototype.greet; لقد أنشأنا هنا: كائنًا بالاسم personPrototype يمتلك التابع ()greet. دالة بانية ()Person تهيئ اسم الشخص الذي نحييه. وضعنا بعد ذلك التوابع المعرّفة ضمن الكائن personPrototype ضمن الكائن prototype للدالة البانية باستخدام التابع Object.assign. وهكذا ستمتلك الكائنات المبنية باستخدام الدالة ()Person prototype Person.prototype الذي يضم تلقائيًا التابع greet. const reuben = new Person("Reuben"); reuben.greet(); // hello, my name is Reuben! يشرح هذا أيضًا ما قلناه سابقًا بأن الكائن prototype للكائن myDate هو Date.prototype، إذ يمثّل الخاصية للبانية Date. الخاصيات المملوكة Own properties يمتلك الكائن الذي أنشأناه باستخدام البانية ()Person خاصيتين: الخاصية name التي ضبطنا قيمتها باستخدام الدالة البانية، لهذا تظهر مباشرة ضم الكائن Person. التابع ()greetالذي ضبُط من خلال الكائن prototype. من الشائع أن تشاهد هذا الأسلوب الذي تُعرّف فيه التوابع ضمن كائنات prototype، وتُعرّف فيه خاصيات البيانات ضمن الدوال البانية. ذلك أن التوابع تبقى نفسها عادة لجميع الكائنات التي ننشئها، لكننا غالبا ما نريد أن يأخذ كل كائن قيم مخصصة لخاصيات البيانات (كأن يكون لكل شخص اسم خاص). تُدعى الخاصيات التي تُعرّف مباشرة ضمن الكائن مثل الخاصية name بالخاصيات المملوكة Own Property، وبإمكانك التحقق من كون الخاصية مملوكة باستخدام التابع الساكن ()Object.hasOwn: const irma = new Person("Irma"); console.log(Object.hasOwn(irma, "name")); // true console.log(Object.hasOwn(irma, "greet")); // false ملاحظة: كما تستطيع استخدام التابع غير الساكن ()Object.hasOwnProperty في هذه الحالة لكننا ننصح باستخدام التابع الساكن ما أمكن. الكائنات prototype والوراثة تُعد كائنات prototype ميزة قوية ومرنة في جافا سكريبت، تسمح لك بإعادة استخدام الشيفرة ودمج الكائنات. وهي بالتحديد تدعم نوعًا من الوراثة inheritance، والتي هي ميزة من ميزات البرمجة كائنية التوجه OOP. إذ تسمح الوراثة للمبرمج التعبير عن فكرة مفادها أن بعض الكائنات هي نسخ أكثر تخصيصًا من كائنات أخرى. فلو كنا نبني نموذجًا عن مدرسة، فقد ننشئ كائنات مثل مدرّس أو طالب وكلاهما أشخاص ويمتلكان بعض الميزات المتشابهة كاﻷسماء مثلًا، لكن قد يكون لكل منهما ميزات إضافية تميّزه عن اﻵخر (كأن يكون للمدرس موّاد يدرّسها)، وقد تنجز نفس الميزات بطريقة مختلفة لكل منهما. لهذا نقول في البرمجة كائنية التوجه OOP بأن الطالب والمدرس كائنان يرثان من كائن آخر يُدعى شخص. أما في جافا سكريبت فيمكن للكائنين Professor و Student أن يمتلكا نفس الكائن المجرد Person، ويرثا الخاصيات التي يمتلكها كائن prototype كما يمكن أن نعرّف خاصيات وتوابع جديدة تناسب كل منهما. سنناقش في مقالات لاحقة مفهوم الوراثة إضافة إلى الميزات الأخرى للبرمجة كائنية التوجه وطريقة دعم جافا سكريبت لها. الخلاصة غطينا في مقالنا كائنات prototype في جافا سكريبت، وآلية تكوين سلاسل كائنات prototype التي تسمح للكائنات أن ترث ميزات من بعضها، كما ناقشنا الخاصية prototype وكيفية استخدامها في إضافة توابع إلى الدوال البانية وغيرها من النقاط التي تتعلق بكائنات prototype. ترجمة -وبتصرف- للمقال Object prototpes اقرأ أيضًا المقال السابق: أساسيات العمل مع الكائنات في جافا سكريبت وراثة الأصناف (Class inheritance) في جافاسكربت كيف أتعلم لغة جافا سكريبت من الصفر حتى الاحتراف لغة البرمجة بالكائنات Object-Oriented Programming
  23. نلقي نظرة في هذا المقال على المصفوفات وهي إحدى الطرق الأنيقة لتخزين قوائم من العناصر تحت اسم متغير واحد. سنتعلم فائدة المصفوفات ونكتشف بعدها كيف نكوّن المصفوفة ونضيف العناصر إليها أو نزيلها أو نستعيدها، إضافة إلى بعض النقاط المفيدة الأخرى. ننصحك قبل المتابعة في قراءة هذا المقال بالاطلاع على بعض المقالات السابقة مثل: أساسيات علوم الحاسب. أساسيات HTML. أساسيات عمل CSS ما هي المصفوفات؟ توصف المصفوفات Array عمومًا أنها كائنات تشبه القوائم، فهي في مبدأها كائنات مفردة تتضمن قيمًا مخزّمة ضمنها على شكل قائمة. يمكن تخزين المصفوفات ضمن المتغيرات ويجري التعامل معها كغيرها من القيم، لكن الفرق الوحيد هو إمكانية الوصول إلى كل قيمة ضمن المصفوفة بشكل منفصل عن غيرها مما يتيح إمكانيات كبيرة وفعّالة في التعامل مع القوائم والتنقل بين عناصرها ضمن حلقات لتنفيذ نفس التغييرات على كل قيمة أو على قيم منتقاة. فقد تتضمن المصفوفة مثلًا قائمة من أسعار منتجات وتريد طباعة هذه الأسعار ضمن فاتورة ثم جمع السعر الكلي وطباعته أسفل القائمة. ودون وجود مصفوفات كان علينا تخزين كل عنصر في متغير منفصل ومن ثم استدعاء الشيفرة التي تنفذ عملية الطباعة والجمع لكل عنصر وبشكل منفصل. ستكون الشيفرة عندها طويلة وغير فعّالة وموّلدة لأخطاء أكثر. فتخيل إن كان عدد العناصر 10 مثلًا، سيكون إضافتها إلى الفاتورة مزعجًا، فكيف هو الحال إن كان هناك 1000 عنصر؟ وكما فعلنا في مقالات أخرى سنتدرب على أساسيات التعامل مع المصفوفات باستخدام طرفية جافا سكريبت في المتصفح والتي يمكنك الوصول لها من خلال النقر على مفاتيح (Ctrl + Shift+ K في فايرفكس). إنشاء المصفوفات تتكون المصفوفة من قوسين مربعين وعناصر تفصل بينها فاصلة ,. افترض أنك تريد تخزين لائحة التسوق التالية في مصفوفة، أنشأ هذه المصفوفة بنسخ ولصق الشيفرة التالية في الطرفية: const shopping = ["bread", "milk", "cheese", "hummus", "noodles"]; console.log(shopping); إن كل عناصر المصفوفة السابقة هي عناصر نصية، لكن بإمكانك تخزين أنواع مختلفة من البيانات مثل الأعداد والسلاسل النصية والكائنات وحتى مصفوفات أخرى. كما يمكن استخدام أنواع مختلفة في المصفوفة نفسها، فلا ضرورة لإلزام أنفسنا بإنشاء مصفوفة لتخزين الأعداد وأخرى للنصوص. إليك مثالًا: const sequence = [1, 1, 2, 3, 5, 8, 13]; const random = ["tree", 795, [0, 1, 2]]; جرب إنشاء بعض المصفوفات قبل المتابعة. إيجاد طول مصفوفة بإمكانك إيجاد طول مصفوفة (عدد العناصر التي تضمها) بنفس طريقة إيجاد عدد المحارف في سلسلة نصية باستخدام الخاصية ()length: const shopping = ["bread", "milk", "cheese", "hummus", "noodles"]; console.log(shopping.length); // 5 الوصول إلى عناصر مصفوفة وتعديلها تُرقم عناصر المصفوفة ابتداءً من الصفر ويُدعى هذا الرقم دليل العنصر item index. وهكذا سيكون دليل العنصر الأول هو 0 والثاني 1 وهكذا. وللوصول إلى عنصر معين في مصفوفة ضع اسم المصفوفة يليها قوسين مربعين ضمنهما دليل العنصر أي بنفس الطريقة التي تصل فيها إلى محرف في سلسلة نصية: const shopping = ["bread", "milk", "cheese", "hummus", "noodles"]; console.log(shopping[0]); // returns "bread" بالإمكان أيضًا تعديل عنصر في المصفوفة بإسناد قيمة جديدة إلى العنصر المطلوب: const shopping = ["bread", "milk", "cheese", "hummus", "noodles"]; shopping[0] = "tahini"; console.log(shopping); // shopping will now return [ "tahini", "milk", "cheese", "hummus", "noodles" ] ملاحظة: تذكر أن العد أو الفهرسة في جافا سكريبت تبدأ من 0 وليس من 1. تُدعى المصفوفة ضمن مصفوفة بالمصفوفة متعددة الأبعاد، ويمكن الوصول إلى عنصر في مصفوفة موجودة ضمن مصفوفة أخرى بكتاب اسم المصفوفة الخارجية يليها زوجين من الأقواس المربعة يضم الأول دليل المصفوفة الداخلية ضمن المصفوفة الخارجية وفي الثاني دليل العنصر المطلوب في المصفوفة الداخلية. فلو أردت الوصول إلى أحد عناصر المصفوفة التي دليلها 2 (العنصر الثالث) ضمن المصفوفة random يمكنك إنجاز الأمر كالتالي: const random = ["tree", 795, [0, 1, 2]]; random[2][2]; جرّب أن تعدّل على عناصر المصفوفات التي أنشأتها قبل المتابعة. إيجاد دليل العناصر في مصفوفة إن لم تكن تعرف دليل العنصر، استخدم التابع ()indexOf الذي يأخذ العنصر وسيطًا له ويعيد دليله إن كان موجودًا أو 1- إن لم يجده. const birds = ["Parrot", "Falcon", "Owl"]; console.log(birds.indexOf("Owl")); // 2 console.log(birds.indexOf("Rabbit")); // -1 إضافة عنصر إلى مصفوفة ﻹضافة عنصر أو أكثر إلى نهاية المصفوفة، نستخدم التابع ()push وعليك حينها التأكد من إضافة عنصر أو آخر إلى نهاية المصفوفة. const cities = ["Manchester", "Liverpool"]; cities.push("Cardiff"); console.log(cities); // [ "Manchester", "Liverpool", "Cardiff" ] cities.push("Bradford", "Brighton"); console.log(cities); // [ "Manchester", "Liverpool", "Cardiff", "Bradford", "Brighton" ] يعيد التابع طول المصفوفة الجديد عند نجاح العملية، وبإمكانك أيضًا تخزين طول المصفوفة الجديد بإسناد التابع إلى متغير كالتالي: const cities = ["Manchester", "Liverpool"]; const newLength = cities.push("Bristol"); console.log(cities); // [ "Manchester", "Liverpool", "Bristol" ] console.log(newLength); // 3 وﻹضافة عناصر إلى بداية مصفوفة، استخدم التايع ()unshift: const cities = ["Manchester", "Liverpool"]; cities.unshift("Edinburgh"); console.log(cities); // [ "Edinburgh", "Manchester", "Liverpool" ] إزالة عناصر من مصفوفة ﻹزالة العنصر اﻷخير من مصفوفة، استخدم التابع ()pop: const cities = ["Manchester", "Liverpool"]; cities.pop(); console.log(cities); // [ "Manchester" ] يعيد هذا التابع العنصر الذي أزيل من المصفوفة، ولكي تخزن هذا العنصر في متغيّر، يمكنك اتباع الطريقة التالية: const cities = ["Manchester", "Liverpool"]; const removedCity = cities.pop(); console.log(removedCity); // "Liverpool" وﻹزالة العنصر اﻷول من مصفوفة استخدم التابع ()shift: const cities = ["Manchester", "Liverpool"]; cities.shift(); console.log(cities); // [ "Liverpool" ] وإن كنت تعلم دليل العنصر بإمكانك إزالته من المصفوفة باستخدام التابع ()splice: const cities = ["Manchester", "Liverpool", "Edinburgh", "Carlisle"]; const index = cities.indexOf("Liverpool"); if (index !== -1) { cities.splice(index, 1); } console.log(cities); // [ "Manchester", "Edinburgh", "Carlisle" ] يحدد الوسيط الأول للتابع ()splice دليل العنصر الذي تبدأ عنده إزالة العناصر، ويحدد الوسيط الثاني عدد العناصر التي يجب إزالتها، وبالتالي بإمكانك استخدامه ﻹزالة عدة عناصر: const cities = ["Manchester", "Liverpool", "Edinburgh", "Carlisle"]; const index = cities.indexOf("Liverpool"); if (index !== -1) { cities.splice(index, 2); } console.log(cities); // [ "Manchester", "Carlisle" ] الوصول إلى كل العناصر قد تحتاج أحيانًا الوصول إلى كل عنصر من عناصر مصفوفة، عندها يمكنك استخدام الحلقة for...of: const birds = ["Parrot", "Falcon", "Owl"]; for (const bird of birds) { console.log(bird); } وقد تضطر أحيانًا إلى تنفيذ عملية ما على كل عنصر من عناصر مصفوفة للحصول على مصفوفة جديدة مختلفة عن اﻷصل. استخدم لهذه الغاية التابع ()map. يوضح المثال التالي كيفية مضاعفة جميع أعداد مصفوفة عددية: function double(number) { return number * 2; } const numbers = [5, 2, 7, 6]; const doubled = numbers.map(double); console.log(doubled); // [ 10, 4, 14, 12 ] مررنا إلى التابع ()map وسيًا هو الدالة double التي يستدعيها لمضاعفة كل عنصر ثم يضيف ناتج كل استدعاء إلى المصفوفة الجديدة ويعيد هذه المصفوفة في النهاية. وقد تحتاج في بعض اﻷحيان إلى تشكل مصفوفة جديدة تضم عناصر من مصفوفة قديمة إذا حققت هذه العناصر شرطًا ما، استخدم لهذه الغاية التابع ()filter. لاحظ كيف نستخدم هذا التابع في المثال التالي الذي يأخذ مصفوفة ويعيد مصفوفة تضم فقط العناصر التي طولها أكبر من 8: function isLong(city) { return city.length > 8; } const cities = ["London", "Liverpool", "Totnes", "Edinburgh"]; const longer = cities.filter(isLong); console.log(longer); // [ "Liverpool", "Edinburgh" ] وكذلك يستدعي التابع ()filterدالة لاختبار كل عنصر من عناصر المصفوفة فإن أعادت القيمة true يُضاف العنصر إلى المصفوفة الجديدة ثم يعيد هذه المصفوفة في النهاية. التحويل بين المصفوفات والسلاسل النصية قد يُعرض عليك كم كبير من البيانات الخام التي تتضكن سلاسل نصية طويلة، وتجد ضرورة لتقسيم هذه البيانات إلى عناصر أكثر فائدة يمكن معالجتها لاحقًا كأن تعرضها في جدول. يمكنك في هذه الحالة استخدام التابع ()split، وهو تابع يأخذ في أبسط أشكاله معاملًا واحدًا هو المحرف الذي تحدث عنده عملية تقسيم السلسلة. ملاحظة: هذا التابع هو تابع لمعالجة السلاسل النصية لكنه يعيد مصفوفة لذلك أشرنا إليه في هذا المقال. ولمعرفة طريقة عمل ()split اتبع الخطوات التالية: أنشئ السلسلة النصية التالية في طرفية جافا سكربت ;"const data = "Manchester,London,Liverpool,Birmingham,Leeds,Carlisle افصل السلسلة عند المحرف ,: const cities = data.split(","); cities; حاول حساب طول المصفوفة الجديدة واستخلص بعض عناصرها: cities.length; cities[0]; // the first item in the array cities[1]; // the second item in the array cities[cities.length - 1]; // the last item in the array بإمكانك عكس العملية باستخدام التابع ()join: const commaSeparated = cities.join(","); commaSeparated; كما يمكن تحويل المصفوفة إلى سلسلة نصية باستخدام التابع ()toString الذي يعده البعض أبسط من ()join لأنه يأخذ معاملًا وحيدًا، لكنه أكثر محدودية فلا يمكنه الفصل سوى عند المحرف , على عكس ()join الذي يمكن أن تحدد فيه أكثر من محرف لفصل السلاسل. const dogNames = ["Rocket", "Flash", "Bella", "Slugger"]; dogNames.toString(); // Rocket,Flash,Bella,Slugger تطبيق عملي: طباعة قائمة منتجات بالعودة إلى مثالنا السابق عن قائمة المنتجات وأسعارها التي نريد إدراجها في فاتورة ثم نحسب إجمالي الفاتورة ونطبعها في اﻷسفل. ستجد في المحرر التفاعلي التالي مجموعة من التعليقات المرقمة، ويحدد كل تعليق مكانًا لكتابة شيفرة معينة: تحت التعليق number 1// ستجد عددًا من السلاسل النصية التي تضم كلا منها اسم المنتج وسعره ويفصل بينهما فاصلة. والمطلوب منك تحويلها إلى مصفوفة وتخزينها ضمن المتغيّر products أنشئ حلقة for...of تحت التعليق number 2// كي تمر على جميع عناصر المصفوفة السابقة. اكتب تحت التعليق number 3// شيفرة لفصل عناصر المصفوفة السابقة () إلى عنصرين يضم اﻷول الاسم والثاني السعر. إن لم تكن متأكدًا من طريقة تنفيذ اﻷمر راجع مقال [توابع جافا سكريبت اﻷصلية للتعامل مع النصوص]()، أو عد إلى فقرة التحويل بين المصفوفات والسلاسل النصية التي عرضناها قبل قليل. ويجب عليك أيضًا أن تحوّل السعر في السطر السابق من نص إلى عدد. يمكنك العودة إلى مقال التعامل مع النصوص في جافا سكريبت كي تتذكر آلية تنفيذ اﻷمر. ستجد متغرًا باسم totalوقد أسندت إليه القيمة 0. نطلب إليك أن تضيف سطرًا ضمن الحلقلة الموجودة أسفل التعليق number 4// ﻹضاف سعر العنصر الحالي إلى قيمة المتغير total عند كل تكرار للحلقة حتى نحصل في النهاية على إجمالي السعر أسفل الفاتورة، وقد تحتاج إلى استخدام عامل إسناد مناسب. غيّر السطر أسفل التعليق number 5// لتصبح قيمة المتغيّر مطابقة للسلسلة "العنصر الحالي — سعر العنصر الحالي$" مثل "Shoes — $23.99" عند كل تكرار كي تُطبع المعلومات الصحيحة لكل منتج ضمن الفاتورة. وتُنفّضذ العملية بضم بسيط لسلسلتين نصيتين. أضف القوس { أسفل التعليق number 6// ﻹنهاء حلقة for...of. See the Pen js-array-1 by Hsoub Academy (@HsoubAcademy) on CodePen. تطبيق عملي: نتائج البحث الخمسة الأولى يظهر استخدام مهم للتابعين ()push و ()pop عندما تريد أن تحدّث سجلًا لعناصر نشطة في تطبيق ويب، كأن يكون لديك تطبيق يعرض رسومًا متحركة ويضم عددًا كبيرًا من الكائنات مثل الخلفية وعناصر اخرى وتريد لسبب أو ﻵخر عرض 50 كائنًا فقط معًا. فعند إضافة عناصر جديدة لمصفوفة الكائنات تّحذف عناصر أقدم ليبقى عدد الكائنات المعروضة 50. في تطبيقنا هذا سنبسط اﻷمر أكثر، إذ سنفترض وجود محرك بحث وهمي يضم صندوق بحث، ومن المفترض عرض قائمة بآخر خمس عمليات بحث عند إدخال أي شيء في صندوق البحث. وعند تجاوز عمليات البحث 5 عمليات تُحذف العملية اﻷقدم وتضاف العملية الجديدة كي يبقى عدد عناصر القائمة 5. ملاحظة: قد تكون قادرًا في تطبيقات البحث الفعلية على النقر على زر ما لاستعادة جمع عمليات البحث التي جرت وطريقة لعرض كل النتائج. ﻹنجاز اﻷمر: أضف سطرًا تحت التعليق number 1// الذي يضيف عملية البحث المدخلة مؤخرًا إلى بداية المصفوفة، ويمكن الحصول على هذه القيمة من خلال اﻷمر searchInput.vlue. أضف سطرًا تحت التعليق number 2// كي يزيل العنصر الموجود في آخر المصفوفة حاليًا. See the Pen js-array-2 by Hsoub Academy (@HsoubAcademy) on CodePen. الخلاصة لا بد وأنك استنتجت أهمية المصفوفات بعد إكمال قراءة هذا المقال، وستجدها في كل مكان في شيفرة جافا سكريبت وعادة بالتشارك مع حلقات كي تكرر نفس العمليات على كل عنصر من عناصرها. أما اﻵن وقد انتهيت من هذه السلسلة من التعليمات التي شرحنا فيها أساسيات جافا سكريبت، خذ قسطًا من الراحة واستعد للسلسلة القادمة التي سنوضح فيها مواضيع أكثر تقدمًا في لغة جافا سكريبت مثل العبارات الشرطية واتخاذ القرار في جافا سكريبت والتعامل مع الدوال البرمجية وغيرها من المفاهيم الضرورية لأي مطور. ترجمة -وبتصرف- للمقال Arrays اقرأ أيضًا المقال السابق: توابع التعامل مع النصوص في جافا سكريبت فهم المصفوفات في الجافاسكربت البحث والترتيب في المصفوفات Array في جافا كيفية استخدام وظائف المصفوفات في الجافا سكريبت – توابع التعديل تعلم جافا سكريبت
  24. تحدثنا في المقالات السابقة عن اﻷسس والمفاهيم التي يجب استيعابها لتكوين الدوائر اﻹلكترونية والتعامل معها وإجراء بعض القياسات الكهربائية فيها. كما تعرّفنا على عناصر إلكترونية سميناها فعّالة لأنها تزوّد الدوائر بالطاقة أو تستهلك الطاقة لأداء عملها مثل البطاريات والترانزيستورات وأخرى سميناها ساكنة لأنها تبدد الطاقة أو تمررها فقط. ورأينا أن تنفيذ أية دائرة إلكترونية لها وظيفة محددة يمر بمرحلتين أساسيتين: اﻷولى: فهم الوظيفة المطلوبة وتصوّر طريقة تنفيذها. الثانية: اختيار العناصر اﻹلكترونية المناسبة وربطها بالطريقة الصحيحة ﻹنجاز الوظيفة. وقد يخطر لك السؤال التالي: "ماذا لو أردت أن أكرر هذه الوظيفة في عدة أماكن مختلفة من الدائرة؟" كأن احتاج عدة مؤقتات زمنية لمراقبة أشياء مختلفة، هل سأكرر الدائرة الكهربائية نفسها مرات عدة ثم أضيفها إلى الدائرة اﻷساسية أم ماذا؟ الجواب على هذا السؤال هو نعم وليس بالضرورة! نعم أي لا بد في الدائرة اﻹلكترونية من تكرار العناصر التي تؤدي وظيفة ما إن أردت استنساخ هذه الوظيفة أكثر من مرة، وليس بالضرورة لوجود شيء أبسط يحوّل هذه العناصر جميعها إلى عنصر واحد ضمن إطار فيزيائي واحد ندعوه دائرة متكاملة Integrated circuit. ما تحتاجه لإكمال التمارين العملية في هذا المقال إليك قائمة بالعناصر الإلكترونية والتجهيزات اللازمة لإكمال التطبيقات العملية: بطارية جهدها 5 فولط. مقاومات قيمها 1.2، 2.2، 10، 22 كيلو أوم. الدائرة المتكاملة NE555 (واحدة تكفي). الدائرة المتكاملة المنظمة للجهد 7805 (واحدة تكفي). المتحكم الصغري PIC16F84A (اختياري لمجرّد التعرف على شكله وتوزع أرجله وقراءة أرقامها). مؤشرات ضوئية (ليد) تعمل عند جهد 5 فولط أو أقل (ثلاثة تكفي). مكثفات سعتها 47 ميكرو فاراد وأخرى 1 ميكرو فاراد جهدها 16 فولط. مكثفات عدسية 20 نانو فاراد (اختيارية إن أردت توصيل دائرة المتحكم الصغري). هزاز كريستالي تردده 2 ميجا هرتز (اختياري لتوصيل دائرة المعالج الصغري). ديودات من طراز 1N4007 (يكفي اثنان). ترانزستورات قطبية من الطراز 2N2222. لوحة اختبار مثقبة (إن أردت فاﻷمر اختياري). مقياس كهربائي متعدد الوظائف AV multi-meter. الدائرة المتكاملة الدائرة المتكاملة Integrated circuits هي دائرة إلكترونية مخصصة ﻷداء وظيفة واحدة أو عدة وظائف، وتتكون ضمنًا من مجموعة من العناصر اﻷساسية مثل الترانزستورات والمكثفات والمقاومات التي تقوم بالعمل المطلوب. توضع هذه العناصر ضمن غلاف مغلق لا يظهر منه إلى العالم الخارج سوى أرجل معدنية تربط الدائرة المتكاملة بالعالم الخارجي. وقد يخطر في بالنا السؤال التالي: إن كانت الدائرة المتكاملة مكوّنة من نفس العناصر الأساسية التي نستخدمها فما فائدتها إذًا؟ إليك الجواب: حجم أصغر بكثير: تخيل أن الدائرة ستتكون من 12 ترانزيستور و10 مقاومات و3 مكثفات، ما الحجم الذي تشغله هذه العناصر مهما كان تصنيعها جيدًا وطريقة توصيلها احترافية؟ بالتأكيد ستشغل حجمًا لا بأس به. لكن إن صُنّعت ضمن دائرة متكاملة فقد لا تتجاوز أبعادها عدة ملليمترات في كل اتجاه. موثوقية أكبر بكثير: لربما قد لا حظت عند محاولتك تجريب التمارين التطبيقية التي طرحناها في المقالين السابقين صعوبة التوصيلات في بعض اﻷحيان أو هفوات صغيرة تُفقدك أعصابك قبل معرفة المشكلة، فما بالك بالدوائر التي يجب أن تؤدي وظيفتها بكل دقة؟ تضمن طريقة التصنيع المتبعة في الدوائر المتكاملة توصيلات غاية في الدقة وترتيبًا مثاليًا للعناصر إضافة إلى اﻷحجام الصغيرة جدًا لهذه العناصر. التعامل مع أرجل الدائرة دون الحاجة لأية تفاصيل تصميمية: لا حاجة لأن تعرف العناصر التي تتكون منها الدائرة اﻹلكترونية وكيف رُتبت، فما تتعامل معه فقط هي اﻷرجل التي يحدد صانعو الدائرة وظيفة كل منها وكيفية وصلها. ولا تخلو فكرة الدوائر المتكاملة من عيوب تتعلق بعدم القدرة على تصنيع دوائر متكاملة تتعامل مع استطاعات كهربائية كبيرة، وقدرتها المحدودة على التخلص من الحرارة الناتجة عن تشغيلها لصغر حجمها، و حساسيتها العالية للضجيج -وهي إشارات غير مرغوبة تصل إليها عن طريق التغذية أو اﻷجهزة المحيطة-. أنواع الدوائر اﻹلكترونية المتكاملة للدوائر اﻹلكترونية أنواع مختلف وتصنف عادة ضمن ثلاثة فئات واسعة: دوائر متكاملة تماثلية: وتتعامل مع قيم تماثلية تطبيق على أرجل الدخل وتعطي قيمًا تماثلية على أرجل الخرج نذكر منها دوائر منظمات الجهد الكهربائي ودارات قياس درجات الحرارة. دوائر متكاملة رقمية: وتتعامل مع قيم رقمية في دخلها وخرجها، نذكر منها المعالجات المصغّرة Microprocessor في الحواسب. تُبنى هذه الدوائر باستخدام ترانزستورات قطبية فتعرف عندها بعائلة TTL وباستخدام ترانزستورات حقلية MOSFET فتعرف باسم CMOS. دوائر رقمية تشابهية: تتعامل مع قيم رقمية في مداخلها وتعطي قيمًا تماثلية على المخارج أو العكس أو كلاهما (تتعامل مع الرقمي والتماثلي في الدخل والخرج)، نذكر منها الدوائر التي تحول درجات الحرارة مثلًا إلى أرقام وكذلك دوائر المتحكمات الصغرية micro-controller التي سنتحدث عنها في الفقرات القادمة. تغليف الدوائر المتكاملة عندما تُصنع الدوائر المتكاملة تظهر أرجلها إلى العالم الخارجي من حواف غلاف بلاستيكي أسود اللون عادة، وتُكتب عليه معلومات عن الشركة الصانعة وتاريخ الصنع ورقم الدائرة المميز ولواحق تتعلق بطريقة التغليف ومجالات العمل وغيرها وتختلف طريقة الترميز من شركة لأخرى لكن رقم الدائرة يبقى كما هو. أشكال أغلفة الدوائر المتكاملة تكون الأغلفة مستطيلة أو مربعة، وتخرج منها اﻷرجل وفق ترتيب محدد وتباعد محدد، وتصنف إلى: أغلفة من النوع DIP: وتكون مستطيلة تخرج اﻷرجل الطرفين بشكل متناظر. أغلفة من النوع SOP: وتكون مستطيلة أو مربعة، تخرج اﻷرجل من طرفيها وتكون صغيرة على شكل حرف L ومتقاربة من بعضها. أغلفة من النوع QFT: وتكون مربعة وصغيرة الحجم ومسطحة، تخرج اﻷرجل على شكل حرف L من جميع أطرافها. أغلفة من النوع BGA: وتكون مربعة الشكل وليس لها أرجل، بل يكون الخرج والدخل على شكل نقاط نافرة أسفل الدائرة وموزعة على صفوف في كل اﻷطراف. ترقيم أرجل الدوائر المتكاملة لكل رجل من أرجل الدائرة المتكاملة عمل محدد كأرجل تغذية وأرجل تأريض وأرجل دخل وخرج. ولكل رجل أيضًا رقم محدد، ويبدأ الترقيم بالرقم 1 وهي الرجل التي يقع إلى يسار حفرة الدائرية موجودة على سطح الغلاف يليه للأسفل الرقم 2 ثم تتزايد أرقام اﻷرجل بعكس جهة دوران عقارب الساعة. تطبيق عملي: تنظيم الجهد باستخدام الدائرة المتكاملة 7805 نحتاج في كثير من التطبيقات إلى مصدر جهد ثابت لا يتغيّر ودون ضجيج كي يستقر عمل الدائرة اﻹلكترونية. لهذا نستخدم نوع خاص من الدوائر التكاملية التي تُدعى بمنظمات الجهد ومنها الدائرة 7805. تعطي هذه الدائرة في خرجها جهدًا موجبًا مستقرًا قدره 5 فولط وتقدم تيارًا أعظميًا شدته 1.5 أمبير على أن يكون جهد مصدر التغذية الذي نريد تنظيمه أعلى من جهد الخرج بحدود 2 إلى 3 فولط. للدائرة 7805 ثلاثة أرجل مع خلفية معدنية لربطها مع جسم معدني أو مبدد حراري إذا كان التيار المستجر عبرها عاليًا لتخفيض الحرارة الناتجة عن عملها. فإذا أمسكتها بحيث تكون في مواجهتك تكون الرجل اليسارية هي رجل جهد الدخل الذي تريد تنظيمه والوسطى رجل التأريض واليمينة رجل الخرج التي تعطينا 5 فولط. شكّل الدائرة البسيطة التالية: صل رجل الدخل إلى المسرى الموجب للوحة المثقبة ثم صل معه القطب الموجب لمكثفة 1 ميكروفاراد ومهبط الديود. صل الرجل اﻷخرى للمكثفة مع المسرى السالب ومصعد الديود مع رجل الخرج. صل الرجل الوسطى مع المسرى السالب. صل رجل الخرج مع الرجل الطويلة للمؤشر الضوئي وصل رجله اﻷخرى مع رجل مقاومة 2.2 كيلو أوم والرجل اﻷخرى للمقاومة مع المسرى السالب. صل قطبي البطارية 8 فولط إلى المسريين الموجب والسالب للوحة المثقبة. استخدم مقياس اﻵفو لتحديد الجهد بين المسرى السالب و رجل الخرج للدائرة 7805 ماذا تجد؟ الدوائر المتكاملة القابلة للبرمجة تُعرّف عملية البرمجة في اﻹلكترونيات عمومًا بأنها طريقة تحديد وظيفة العنصر اﻹلكتروني والطريقة التي يتواصل فيها مع الدائرة المحيطة به. وكما ذكرنا قبل قليل أن الدوائر اﻹلكترونية المتكاملة قد تشتمل على عدة وظائف وعندها تكون برمجة هذه الدائرة هو تحديد الوظيفة التي نريدها أن تؤديها من بين وظائف عدة. وللبرمجة في عالم اﻹلكترونيات تصنيفان أساسيان: برمجة فيزيائية: نحدد فيها وظيفة الدائرة اﻹلكترونية بتغيير طريقة توصيلها مع الدائرة المحيطة، أي تفرض على الدائرة المتكاملة وظيفة معينة وفقًا لطريقة توصيل أرجلها مع عناصر إلكترونية أخرى محددة. وأغلب الدارات المتكاملة التماثلية متعددة الوظائف تبرمج بهذا الشكل. برمجة بالشيفرة: وفيها تُكتب برمجيات رقمية خاصة خارج هذه الدائرة ثم تنقل إليها، وقد تكون هذه البرمجيات دائمة أي تحمََّل مرة واحدة ولا يمكن تعديلها لاحقًا مثل الدائرة التكاملية التي تُحمّل برمجيات إقلاع الحاسب BIOS، ومن الممكن أيضًا تغيير هذه البرامج وتعديلها في أي وقت كما في الدوائر المتكاملة التي تُدعى المتحكمات الصغرية. تُدعى البرمجيات التي تُحمّل إلى الدوائر المتكاملة "برمجيات قيادة Firmeware" وتكتب باستخدام لغات برمجة منخفضة المستوى عادة، ويمكن استخدام لغات برمجة عالية المستوى مثل C++,C, بايثون. تطبيق عملي: البرمجة الفيزيائية للدائرة المتكاملة 555 تُصنف الدائرة 555 ضمن فئة المؤقتات أو الهزازات التي يتأرجح دخلها بين قيمة عليا (قيمة وصل) هي قيمة جهد تغذية الدائرة وقيم دنيا (قيمة فصل) هي 1.2 فولط وفقًا لطريقتي برمجة فيزيائية: اﻷولى تُدعى الوضع الوحيد الاستقرار وفيه تعمل الدائرة كمؤقت إذ تُبرمج كي يكون الخرج موصولًا لفترة زمنية محددة. الثانية: تُدعى الوضع غير المستقر وفيه يُبرمج الخرج كي يتبدل بين الفصل والوصل خلال أزمة محددة لكل حالة. تتكون الدائرة 555 من ثمانية أرجل، لا حاجة حاليًا لشرح وظيفة كل رجل، وما سنستعرضه هي طريقة برمجة الدائرة كي تعطي الوظيفتين السابقتين. لهذا استخدم العناصر التي أشرنا إليها سابقًا لتشكيل الدائرة التالية: برمجة الوظيفة الأولى: ضع الدائرة 555 لتكون أرجلها ضمن نصفين مختلفين للوحة المثقبة ثم صل الرجل رقم 1 (اﻷرضي) بالمسرى السالب والرجلين 8 (تغذية) والرجل 4 بالمسرى الموجب. صل الرجل رقم 7 بمقاومة R1 قيمتها 22 كيلو أوم ورجلها الثانية بالمسرى الموجب. صل الرجل رقم 2 بالرجل 6 بسلك ثم صلهما بالرجل الموجبة لمكثفة C1 قيمتها 47 ميكروفاراد وصل رجلها السالبة بالمسرى السالب. صل بمقاومة R2 قيمتها 22 كيلو أوم نقطة التقاء المقاومة 22 كيلو أوم والرجل 7 مع نقطة التقاء الرجل 6 والمكثفة. صل الرجل 5 إلى مكثفة 1 ميكروفاراد وصل رجلها السالبة باﻷرضي (بإمكانك تجاهل هذه الحركة حاليًا). صل أخيرًا رجل الخرج رقم 3 من خلال مقاومة 10 كيلو أوم بقاعدة الترانزستور وانتبه إلى أن يكون بعيدًا عن ثقوب الدائرة 555 ثم صل باعثه إلى المسرى السالب من خلال مقاومة 2.2 كيلو أوم وصل مؤشر ضوئي بين مجمّعه والمسرى الموجب. صل بطارية 8 فولط إلى المسريين الموجب والسالب للوحة المثقبة، وراقب ما يحدث. ستلاحظ كيف يضيء وينطفئ المصباح بشكل منتظم وباستمرار طالما أن الدائرة موصولة بالتغذية الكهربائية وتُبرمج فترتي اﻹضاءة والتوقف من خلال تحديد قيم المقاومتين R1 و R2 وسعة المكثفة C1 وفق المعادلتين البسيطتين التاليتين: t(off) = 0.0069xC1xR2 ..............(زمن الفصل بالثانية) t(on)= 0.0069xC1x(R1+R2)..........(زمن التوصيل بالثانية) برمجة الوظيفة الثانية: اتبع نفس الخطوات السابقة لكن لا تصل الرجل 2 بالرجل 6 بل اجعل السلك موصولًا بالرجل 2 وحرًا من الطرف اﻵخر. ضع السلك الموصول بالرجل 2 (رجل القدح) بالمسرى السالب للحظة ثم اخرجه وسترى أن الضوء يضيئ مدة زمنية محددة ثم ينطفئ، وتبرمج هذه المدة من خلال تحديد قيمة المقاومة R1 والمكثفة C1 وفق المعادلة التالية: t= 0.001xR1xC1.......(زمن الوصل بالثانية) المتحكمات الصغرية والدوائر المتكاملة المبرمجة بالشيفرة المتحكمات الصغرية هي دائرة عالية التكامل متعددة الوظائف من النوع الرقمي أو الرقمي-التشابهي المختلط. وهي دوائر قابلة للبرمجة بالشيفرة وبالتالي لابد أن تكون المتحكمات قادرةً على قراءة الشيفرة وتنفيذها. تُعد هذه الدائرة بمثابة حاسوب حقيقي لكنه مصغّر ومحدود اﻹمكانية وينقصه فقط لوحة مفاتيح وشاشة عرض (وفي الواقع يمكن وصل شاشات ولوحات مفاتيح خاصة إليه وبرمجته ليتواصل معهما). يمكن للمتحكمات الصغرية التعامل مع القيم التماثلية التي تأتيه من الوسط الخارجي، مثل قراءة درجات الحرارة عبر وصله بحساسات مناسبة، كما يمكنه التحكم بأجهزة تماثلية مثل التحكم بمحركات التيار المستمر. ولهذا نجد أن المتحكمات الصغرية هي غالبًا الدماغ الذي يقود الروبوتات. وإضافة إلى القيم التماثلية فهو قادر على فهم اﻹشارات الرقمية والمنطقية وقادر على التخاطب الرقمي مع الوسط الخارجي. البنية العامة للمتحكم الصغري يتكون المتحكم الصغري أيًا يكن نوعه أو الشركة المصنعة له من مكوّنات بنيوية أساسية هي: وحدة معالجة مركزية CPU وهي المسؤولة عن تنفيذ العمليات الرياضية والحسابية. وحدة إدارة الذاكرة (الكتابة والقراءة منها) بشقيها ذاكرة الوصول العشوائي RAM التي تُستخدم أثناء تنفيذ الشيفرة وذاكرة القراءة فقط ROM التي تخزن شيفرة البرنامج. وحدة إدارة الدخل والخرج. منافذ دخل وخرج. دوائر توقيت. ساعة داخلية. مبدلات رقمية تماثلية والعكس لتحويل كل منهما إلى اﻷخرى حسب الحاجة. وحدة وحدات قادر على الاتصال مع التجهيزات الخارجية وفق معايير مشتركة بين هذه العناصر (بروتوكولات نقل). لن نهتم في الواقع إلى هذه التفاصيل كثيرًا في بداية مشوارنا لكن لا بد من الانتباه لها عند شراء معالج صغري، إذ نهتم عادة بالنقاط التالية وبما يلائم مشروعنا: حجم ذاكرته. السرعة التي يعمل عندها. عدد بوابات الدخل والخرج وعدد اﻷرجل في كل منها. عدد اﻷرجل التي تقبل دخلًا تماثليًا. عدد المؤقتات ودقتها. عدد العدادات فيه ودقتها. عدد وحدات الاتصال مع تجهيزات الوسط الخارجي وأنواعها (نقل تسلسلي، تفرعي،…). دعم الاتصال مع مع تجهيزات أخرى باستخدام واجهات مثل USB وغيرها. يمكن للمتحكم الصغري أن يستخدم ساعته الداخلية لمزامنة قراءة الشيفرة وتنفيذها أو ساعة خارجية يؤمنها موّلد نبض خارجي وله أنواع كثيرة أشهرها الهزازات الكريستالية Crystal Oscillators التي تعمل عند ترددات مختلفة بما يلائم السرعة القصوى المطلوبة من المتحكم المستخدم. الهيكلية الخارجية للمتحكم يغلّف المتحكم الصغري بأحد طرق تغليف الدوائر التكاملية التي ذكرناها سابقًا ولا يخرج منه إلى الوسط الخارجي سوى اﻷرجل. تُرقم أرجل المتحكم الصغري كما تُرقم أرجل أي دائرة متكاملة أي من الرقم 1 للرجل التي تقع على يسار الحفرة المرجعية على السطح وتتزايد اﻷرقام بعكس دوران عقارب الساعة. البوابات ووظائف اﻷرجل تقسم اﻷرجل كما ذكرنا إلى: أرجل مخصصة لها وظيفة واحدة كأرجل التغذية والتصفير وأرجل الاتصال مع الساعة الخارجية. أرجل عامة للدخل والخرج أرجل عامة للدخل والخرج مع وظائف خاصة. تشكل كل مجموعة من اﻷرجل العامة مايُسمى بوابة Port وتضم كل بوابة ما بين 3 إلى 16 رجل وعادة ما تُصنع أرجل البوابة الواحدة وفق آلية محددة تجعلها متوافقة مع بعضها. ويمكن برمجة كل رجل لتكون دخل أو خرج بغض النظر عن بقية أرجل البوابة، لكن عندما تريد استخدام البوابة ككل فلابد أن تكون كل أرجل هذه البوابة دخل أو كلها خرج. يمكن لبعض اﻷرجل وليس جميعها أن تستقبل قيمًا تماثلية لأنها مبنية ومصممة لهذا الغرض لكن كل اﻷرجل قادرة على فهم القيم الرقمية أو المنطقية. أما الأرجل ذات الوظائف الخاصة، فإنها تؤدي هذه الوظائف عندما نحدد ذلك عن طريق البرنامج. وعندها تؤدي فقط هذه الوظيفة الخاصة ولا يمكن أن تستخدم للدخل أو الخرج العام. ومن هذه الوظائف نجد العدادات وتبادل البيانات مع تجهيزات أخرى. ومن الوظائف الخاصة أيضًا اﻷرجل التي تتصل بجهاز البرمجة وتنقل البرنامج الذي كتبناه من الحاسوب إلى المتحكم. تطبيق عملي: تعرّف على المتحكم PIC16f84A وهو متحكم صغري من إنتاج Microchip يعمل عند جهد 2 إلى 5.5 فولط وسرعة بين 32 كيلو هرتز و20 ميغا هرتز يمكن تحديدها من خلال الساعة الخارجية المتصلة به (هزاز النبضات). لهذا المتحكم بعض الميزات منها: بوابتين A و B تضم البوابة اﻷولى خمسة أرجل مرمّزة من A0 وحتى A4، بينما تضم الثانية ثمان أرجل مرمّزة من B0 وحتى B7. دارة مؤقت/ عداد (حسب برمجتها) مرمّزة بالاسم TMR0 متصلة بالرجل RA4، إذا لهذه الرجل وظيفة عامة (دخل أو خرج) ووظيفة خاصة (عد النبضات الواردة إلى هذه الرجل) ويمكن ضبط الوظيفة المطلوبة برمجيًا. لا يمكنه التعامل مع اﻹشارات التماثلية مباشرة أي لا يحتوي على أرجل مهيأة للتعامل مع القيم التماثلية ويحتاج إلى عنصر خارجي يُدعى محوّل تماثلي رقمي. يدعم الاتصال التسلسلي مع الحاسب وبعض التجهيزات اﻷخرى. ذاكرة برنامج مقدارها 2048 بايت وذاكرة عشوائية للعمل مقدارها 64 بايت. يبرمج باستخدام لغة خاصة به كما يبرمج باستخدام لغة C. لتوصيل المتحكم إلى دائرة إلكترونية اتبع الخطوات التالية: صل الرجل رقم 14 إلى منبع تغذية موجب بين 3 إلى 5 فولط، يمكنك بالطبع استخدام منظم الجهد 7805 الذي تحدثنا عنه في تطبيق عملي سابق، لأنه من الضروري تنظيم الجهد الواصل إلى المتحكم وإزالة أية آثار للضجيج. صل الرجل رقم 5 باﻷرضي (المسرى السالب). صل مهتز كريستالي تردده 2 ميغا هرتز مثلًا بين الرجلين 15 و 16 ثم صل كل رجل للمهتز بالمسرى السالب من خلال مكثف عدسي 20 نانو فاراد. هذا المهتز هو من سيحدد سرعة عمل المتحكم ويضبط توقيت العمل. صل الرجل رقم 4 بالمسرى الموجب. في هذه اللحظة سيدخل المتحكم في مرحلة اﻹقلاع وصولًا إلى مرحلة تنفيذ البرنامج الذي حمّلناه مسبقًا إلى ذاكرته. كتابة برنامج للمتحكم وتحميله لا بد قبل كل شيء من تعلّم لغة برمجة، وخاصة C أو ++C أو بايثون حتى تستطيع التفكير بطريقة برمجية إضافة إلى فهم صياغة الشيفرة. وعليك أن تعرف أن تعاملك في المتحكمات سيكون مع اﻷرجل أو مع وحدات الاتصال. عندما تحدد أحد اﻷرجل على أنه رجل خرج، سيمر تيار له نفس جهد التغذية وشدة أقصاها 25 ميلي أمبير إلى العنصر الذي تريده وذلك في حال أعطيته القيمة المنطقية 1. إن حددت أحد الأرجل على أنها رجل دخل، سيترقب المعالج بشكل مستمر وصول تيار جهده يماثل جهد التغذية إلى هذه الرجل ثم ينفّذ عملًا معينًا إذا حدث ذلك. إن أردت من رجل ذات وظيفة خاصة أن تنفّذ هذه الوظيفة، عليك أن تحدد ذلك في البرنامج. توضع الشيفرة كلها ضمن كتلة يعيد المتحكم تنفيذها باستمرار وينتقل من تعليمة إلى التي تليها بشكل متسلسل. لنحاول أن نكتب برنامجًا بسيطًا للمتحكم PIC16F84A ينتظر ورود إشارة على الرجل RA0 ثم يجعل الأضواء الموصولة مع اﻷرجل RA1, RA2, RA3 تعمل بالتناوب لمرة واحد على أن يكون هناك فاصل زمني بين كل منها مقداره 2 ثانية ثم تنطفئ. إليك البرنامج: #include <16F877A.h> // لاستخدام الميزات الخاصة بالمتحكم المطلوب #use delay(clock=2000000) // اختيار سرعة المعالج بالهرتز ويماثل تردد الهزاز الكريستالي void main() { /* نضع في هذه الكتلة تعليمات برنامج المتحكم الذي يكررهابشكل مستمر طالما أنه في حالة عمل */ if(input_state(pin_a0)){ /*ستُنفَّذ التعليمات التالية RA0 إذا وصل تيار جهد 5 فولط إلى الرجل */ output_high(pin_a1);//ٌ لتمرر تيارًا إلى المؤشر الضوئي RA1 تفعيل الرجل delay_ms(2000);// الانتظار مدة 2000 ميلي ثانية أي ثانيتين output_high(pin_a2);// لتمرر تيارًا إلى المؤشر الضوئي RA2 تفعيل الرجل delay_ms(2000);// الانتظار مدة 2000 ميلي ثانية أي ثانيتين output_high(pin_a3);// لتمرر تيارًا إلى المؤشر الضوئي RA3 تفعيل الرجل delay_ms(2000);// الانتظار مدة 2000 ميلي ثانية أي ثانيتين output_low(pin_a1);//وإطفاء المؤشر الضوئي RA1 قطع التيار عن الرجل output_low(pin_a2);//وإطفاء المؤشر الضوئي RA2 قطع التيار عن الرجل output_low(pin_a3);//وإطفاء المؤشر الضوئي RA3 قطع التيار عن الرجل }; } يُكتب هذا البرنامج ضمن أي محرر نصي ثم يُستخدم برنامج حاسوبي لتحويله إلى شكل يفهمه المتحكم وهو الترميز المنطقي بالأصفار 0 والواحدات 1 وتكون النتيجة ملف له الامتداد hex.. نحتاج بعد ذلك إلى تجهيزة خاصة تُدعى مبرمجة مهمتها نقل الملف السابق من الحاسوب إلى المتحكم، وستجد أنواعًا مختلفة من المبرمجات يخصص كل منها لعائلة أو أكثر من المتحكمات ويأتي مع كل مبرمجة التوصيلات الخاصة مع الحاسوب وبرنامج نقل ملف الشيفرة ودليل الاستعمال. وهذا الموضوع بالطبع خارج نطاق هذا المقال ويتطلب مزيدًا من الشرح والتوضيح. الخلاصة هكذا نكون قد انتهينا من سلسلة هذه المقالات التي تحدّثت عن علم اﻹلكترونيات والدارات الإلكترونية انطلاقًا من المفاهيم اﻷساسية وصولًا إلى المتحكمات القابلة للبرمجة والتي تُعد نواةً للتحكم بالروبوتات والحواسب المصغرة وأجهزة التحكم الصناعي وغيرها الكثير. فإن رأيت أنك مهتم بما قرأت شاركنا رأيك في نقاش الصفحة ودعنا نساعدك في توضيح ما يُشكل عليك فهمه وتوجيهك نحو خطوات قادمة. اقرأ أيضًا المقال السابق: أساسيات في عالم الإلكترونيات: تشكيل الدوائر اﻹلكترونية والعناصر الفعالة برمجة الروبوت: الدليل الشامل تجميع راسبيري باي والتحضير لاستخدامه تصميم وتنفيذ لعبة حسية تفاعلية باستخدام لوحة راسبيري باي بيكو تصميم وتنفيذ آلة موسيقية باستخدام لوحة راسبيري باي بيكو
  25. سنوضح في مقال اليوم طريقة التعامل مع استعلام الوسائط Media Query في CSS والتي توفر طريقة لتطبيق تنسيقات معينة على عناصر HTML عندما تحقق بيئة العرض في جهاز أو متصفح معايير أوشروط محددة، كأن يكون اتساع نافذة العرض أكبر من 480 بكسل. إن هذا النمط من الاستعلام هو المفتاح لتصميم الويب المتجاوب Responsive web design، إذ يساعد فى بناء تخطيطات مختلفة للصفحات وفقًا لاتساع نافذة العرض. كما يمكن استخدام هذه الاستعلامات في معرفة بعض ميزات البيئة التي يعمل ضمنها موقعك كأن تعرف إن كان المستخدم يستعمل شاشة لمس بدلًا من الفأرة. لهذا سنتعلم أولًا طريقة صياغة استعلامات الوسائط، ثم سنتعلم استخدامها عمليًا من خلال مثال تفاعلي يشرح كيفية تحويل تخطيط بسيط إلى تخطيط متجاوب. عليك قبل البدء في قراءة هذا المقال أن: تطلع على أساسيات HTML كما شرحناها في سلسلة المقالات مدخل إلى HTML. تفهم أساسيات عمل CSS. أساسيات استعلامات الوسائط تبدو شيفرة استعلام الوسائط بشكلها الأبسط كالتالي: @media media-type and (media-feature-rule) { /* CSS rules go here */ } وهي تتكون من الأجزاء التالية: نوع واسطة العرض media type والذي يخبر المتصفح بطبيعة واسطة العرض التي كُتبت هذه الشيفرة من أجلها (طابعة، شاشة، ...إلخ.). شرط تطبيق الاستعلام media expression وهي قاعدة أو اختبار لا بد من تحققه حتى تُطبق شيفرة CSS المطلوبة. مجموعة قواعد تنسيق CSS التي تُطبق عند تحقق شرط تطبيق الاستعلام. أنواع وسائط العرض هناك ثلاث قيم لواسطة العرض: all print screen يضبط الاستعلام التالي مثلًا حجم الخط في جسم الصفحة على 12pt عند طباعة الصفحة، لكن هذه القاعدة لن تطبق عند عرض هذه الصفحة ضمن المتصفح: @media print { body { font-size: 12pt; } } ملاحظة1: إن نوع الوسائط في الاستعلامات مفهوم مختلف عن ما يُدعى نوع الوسائط المتعددة أو نوع المحتوى MIME-type وهو سلسلة نصية تُرسل مع الملف المرسل عبر الانترنت لتحديد نوعه أو وصف تنسيقه، على سبيل المثال، يمكن تسمية ملف صوتي audio/ogg، أو ملف صورة image/png). ملاحظة2: توجد أنواع أخرى من وسائط العرض أصّيفت في مواصفات المستوى الثالث من استعلامات الوسائط، لكنها أهملت ويجب تحاشيها. ملاحظة3: نوع الوسائط media type قيمة اختيارية، فإن لم ترغب بتحديد نوع واسطة العرض فلا تفعل وستكون القيمة الافتراضية all أي جميع الوسائط. قواعد تطبيق ميزات استعلام الوسائط بعد تخصيص نوع واسطة العرض يمكنك استهداف إحدى ميزات هذه الواسطة كي تحقق شرطًا أو اختبارًا ما: الاتساع والارتفاع أكثر الميزات استهدافًا للحصول على تصميم متجاوب وأكثرها دعمًا من قبل مختلف المتصفحات هي اتساع نافذة العرض viewport width. وهكذا يمكننا تطبيق مجموعة من قواعد التنسيق إن كان اتساع نافذة العرض أعلى أو أدنى أو يعادل قيمة محددة باستخدام ميزات استعلام الوسائط التالية: min-width و max-width و width. تُستخدم الميزات السابقة في إنشاء تخطيطات تتجاوب مع مختلف أبعاد الشاشات. فلو أردنا مثلًا تغيير لون خط الكتابة في جسم المستند إلى اللون الأحمر عندما يكون اتساع نافذة العرض 600 بكسل تمامًا، سنستخدم الاستعلام التالي: @media screen and (width: 600px) { body { color: red; } } ألق نظرة على هذا المثال على جيت-هاب أو اطلع على الشيفرة المصدرية. يمكن استخدام ميزتي الاتساع والارتفاع كمجالات وعندها تسبقان بالبادئة -min أو -max للإشارة إلى أن القيمة المعطاة هي أدنى أو أعلى قيمة. فإن أردنا في مثالنا السابق أن يكون لون الخط أحمر إن كان اتساع نافذة العرض 600 بكسل أو أضيق فنستخدم الميزة max-width: @media screen and (max-width: 600px) { body { color: blue; } } يمكنك إلقاء نظرة على هذا المثال على جيت-هاب أو الاطلاع على الشيفرة المصدرية. إن استخدام القيم العظمى والصغرى أكثر فائدة عمليًا في التصميم المتجاوب، لهذا قلما تُستخدم الميزتان width أو height وحدهما. ستجد العديد من ميزات وسائط الاستعلام التي يمكن استهدافها على الرغم من محدودية دعم المتصفحات للميزات الأحدث الموضوعة في مواصفات المستويين 4 و 5 من استعلامات الوسائط. ويمكنك الاطلاع على كل ميزة ومدى دعم المتصفحات لها من خلال شبكة مطوري موزيللا. جهة انسياب المحتوى من الميزات المدعومة جيدًا لاستعلامات الوساط نجد الميزة orientation التي تسمح باختيار نمط عرض الصورة إما كصورة عمودية portrait أو أفقية landscape. ولكي نغير لون خط كتابة جسم الصفحة إن كان نمط عرض الجهاز أفقيًا، نستخدم الاستعلام التالي: @media (orientation: landscape) { body { color: rebeccapurple; } } يمكنك إلقاء نظرة على هذا المثال على جيت-هاب أو الاطلاع على الشيفرة المصدرية. تعتمد شاشات حواسيب سطح المكتب نمط العرض الأفقي، وما يعمل جيدًا وفق نمط العرض هذا قد لا يعمل جيدًا على الهاتف المحمول أو الجهاز اللوحي الذي يعمل على النمط العمودي. وبالتالي سيساعدك الاستعلام عن نمط العرض في تبني تخطيط محسّن يخدم نمط العرض في الجهاز المستهدف. استخدام جهاز تأشير pointing device قدّمت مواصفات المستوى 4 لاستعلامات الوسائط الميزة hover التي تساعدك على اختبار قدرة المستخدم على إحداث أثر عند المرور فوق عنصر مما يدل على استخدامه نمطًا من أجهزة التأشير كالفأرة، فلا يمكن إحداث أثر عند المرور فوق عنصر في شاشات اللمس أو عند استخدام لوحة المفاتيح في التنقل بين العناصر. @media (hover: hover) { body { color: rebeccapurple; } } ألق نظرة على هذا المثال على جت-هاب. فإن عرفت أن المستخدم لا يعتمد على جهاز تأشير، بإمكانك عندها تقديم بعض الميزات التفاعلية للصفحة افتراضيًا، بينما يمكن تقديم هذه الميزات لمستخدمي أجهزة التأشير عند مرور المؤشر فوق العنصر. كما تضم مواصفات المستوى الرابع الميزة pointer التي تأخذ واحدة من ثلاث قيم هي none و fine و coarse. تُستخدم القيمة fine لأجهزة تأشير مثل الفأرة أو لوحة التتبع، وتساعد المستخدم على استهداف مساحة ضيقة من الصفحة. أما القيمة coarse فتدل على أن المستخدم يستعمل أصابعه أو يستعمل شاشة لمس. وأخيرًا تشير القيمة none إلى عدم استخدام أجهزة تأشير كحالة استخدام لوحة مفاتيح أو استخدام الأوامر الصوتية. يساعدك استخدام الميزة السابقة في تصميم واجهات مستخدم متجاوبة مع طريقة تفاعل المستخدم مع الشاشة. فبإمكانك مثلًا إنشاء مساحة لمس أوسع لمستخدم يستعمل شاشة لمس. استعلامات وسائط أكثر تعقيدًا قد ترغب أحيانًا بضم أكثر من استعلام أو إنشاء قائمة استعلامات قد يتحقق أيًا منها استخدام عامل الربط المنطقي and يُستخدم العامل and بنفس الطريقة التي استخدمناها سابقًا لربط نوع واسطة العرض مع الميزة. فقد نرغب مثلًا أن نختبر الميزتين min-width و orientation معًا. إذ نريد مثلًا أن يكون لون خط الكتابة أزرق إن كان اتساع نافذة العرض اكبر من 600 بكسل وكان الجهاز يعتمد طريقة العرض الأفقية: @media screen and (min-width: 600px) and (orientation: landscape) { body { color: blue; } } ألق نظرة على هذا المثال على جيت-هاب أو اطلع على الشيفرة المصدرية. استخدام عامل الربط المنطقي "or" ويُستخدم لتطبيق تنسيق معين عند تحقق واحدة من عدة استعلامات على الأقل وعندها نستخدم الفاصلة , للفصل بين هذه الاستعلامات. إذ يعرض المثال التالي خط الكتابة باللون الأزرق إن كانت اتساع نافذة العرض 600 بكسل على الأقل واعتمد الجهاز المستهدف طريقة العرض الأفقية. @media screen and (min-width: 600px), screen and (orientation: landscape) { body { color: blue; } } ألق نظرة على هذا المثال على جيت-هاب أو اطلع على الشيفرة المصدرية. استخدام عامل النفي المنطقي not بإمكانك نفي الاستعلام بالكامل باستخدام العامل not، إذ يعكس هذا العامل معنى الاستعلام تمامًا. لاحظ كيف يكون النص في مثالنا التالي أزرق اللون إن كان نمط العرض عموديًا: @media not all and (orientation: landscape) { body { color: blue; } } ألق نظرة على هذا المثال على جيت-هاب أو اطلع على الشيفرة المصدرية. كيفية اختيار النقاط الحدِّية breakpoints حاول المصممون في بدايات التصميم المتجاوب استهداف شاشات بقياسات محددة، بالاستفادة من قوائم تضم أبعاد شاشات أكثر الهواتف المحمولة والأجهزة اللوحية شعبية، وبالتالي سيكون التصميم ملائمًا تمامًا لنافذة العرض المستهدفة. أما الآن، وبوجود كم هائل من الأجهزة مختلفة الأبعاد، فلا جدوى من هذا النهج. وبدلًا من استهداف قياسات بعينها، ظهرت مقاربة تعتمد على تغيير التصميم أو التخطيط عندما لا يعود هذا التصميم ملائمًا لأبعاد الشاشة التي تعرضه. فقد يغدو السطر في نص ما طويلًا جدًا أو أن يضغط وتظهر أشرطة تمرير تصعب معها القراءة. في هذه الحالات، تساعدك استعلامات الوسائط في تغيير التصميم إلى آخر أفضل يلائم المساحة المتاحة للعرض. وهذا يعني أنك لن تحتاج إلى معرفة القياسات الدقيقة لأبعاد الشاشة المستخدمة، بل يتغير التصميم ضمن مجالات محددة لأبعاد نافذة العرض. تُدعى النقاط التي تُعرّف عندها استعلام الوسائط بنقاط الانتقال أو النقاط الحدية Breakpoints التي تسمح بالانتقال من تخطيط لآخر أو من تنسيق لآخر). يُساعدك نمط التصميم المتجاوب في أدوات مطوري ويب لمتصفح فايرفوكس في تفقد عمل نقاط الانتقال. إذ يمكنك بسهولة تصغير نافشة العرض أو تكبيرها لتتفحص كيفية تحسين التصميم إن أضفت استعلامات وسائط. تطبيق عملي: التصميم المتجاوب وقاعدة "الهاتف المحمول أولًا" يمكنك عمومًا اختيار أحد نهجين في التصميم المتجاوب. فإما أن تبدأ التصميم للحواسيب المكتبية أو الشاشات العريضة ثم تضيف نقاط انتقال يتغير عندها التصميم عند الانتقال إلى شاشات أضيق. أو أن تبدأ تصميمك لأصغر نوافذ العرض ثم تغير التخطيط مع ازدياد اتساع نافذة العرض. يُدعى النهج الأخير بنهج الهاتف المحمول أولًا وهو غالبًا ما يكون النهج الأفضل عمليًا. يُعرض المحتوى في الشاشات الصغيرة عادة ضمن تخطيط عمود واحد بسيط، كما هو الحال في تخطيط الانسياب الاعتيادي normal flow. أي أنك لن تحتاج غالبًا إلى تخطيطات معقدة للأجهزة الصغيرة، وكل ما عليك فعله هو ترتيب الشيفرة المصدرية جيدًا لتحصل على تخطيط واضح مقروء افتراضيًا. سنعمل في التطبيق التالي على توضيح هذا النهج من خلال تخطيط بسيط جدًا، وتذكّر أنه في المواقع الفعلية قد تواجه أشياء أكثر تعقيدًا تحتاج إلى ضبطها من خلال استعلامات الوسائط، لكن النهج سيبقى ذاته. تخطيط بسيط على نهج "الهاتف المحمول أولًا" سننطلق من مستند HTML مع بعض تنسيقات CSS التي تضيف ألونًا لخلفيات الأقسام المختلفة للتخطيط كما يلي. * { box-sizing: border-box; } body { width: 90%; margin: 2em auto; font: 1em/1.3 Arial, Helvetica, sans-serif; } a:link, a:visited { color: #333; } nav ul, aside ul { list-style: none; padding: 0; } nav a:link, nav a:visited { background-color: rgba(207, 232, 220, 0.2); border: 2px solid rgb(79, 185, 227); text-decoration: none; display: block; padding: 10px; color: #333; font-weight: bold; } nav a:hover { background-color: rgba(207, 232, 220, 0.7); } .related { background-color: rgba(79, 185, 227, 0.3); border: 1px solid rgb(79, 185, 227); padding: 10px; } .sidebar { background-color: rgba(207, 232, 220, 0.5); padding: 10px; } article { margin-bottom: 1em; } لم ندخل أية تغييرات على التخطيط من خلال شيفرة التنسيق السابقة، لكننا رتبنا الشيفرة المصدرية بطريقة تجعل المحتوى واضحًا. هذه الخطوة أساسية ومهمة من جهة، وتضمن سهولة قراءة المحتوى من قبل قارئات الشاشة من ناحية أخرى. <body> <div class="wrapper"> <header> <nav> <ul> <li><a href="">About</a></li> <li><a href="">Contact</a></li> <li><a href="">Meet the team</a></li> <li><a href="">Blog</a></li> </ul> </nav> </header> <main> <article> <div class="content"> <h1>Veggies!</h1> <p>…</p> </div> <aside class="related"> <p>…</p> </aside> </article> <aside class="sidebar"> <h2>External vegetable-based links</h2> <ul> <li>…</li> </ul> </aside> </main> <footer><p>&copy;2019</p></footer> </div> </body> يعمل هذا التخطيط البسيط جيدًا على الهاتف المحمول. وبإمكانك استخدام نمط التصميم المتجاوب في أدوات مطوري الويب لترى كيف يعمل بشكل واضح ومرضٍ على شاشة الهاتف المحمول. اطلع على الخطوة الأولى ضمن متصفحك أو ألق نظرة على الشيفرة المصدرية. وإن أردت أن تتابع العمل معنا، نزّل نسخة من الملف step1.html على حاسوبك. ابتداءً من هذه الخطوة، اسحب نافذة العرض في وضع التصميم المتجاوب لتصبح أوسع حتى اللحظة التي ترى فيها أن طول سطر الكتابة أصبح طويلًا، ولدينا متسع من المساحة لعرض المحتوى أفقيًا، هنا سنضع أول استعلام وسائط. سنستخدم واحدة em وتعني أنه إذا زاد المستخدم حجم الخط فإن نقطة الانتقال ستحدث عند طول السطر ذاته لكن ضمن نافذة عرض أوسع. أضف الشيفرة التالية إلى آخر الملف step1.html: @media screen and (min-width: 40em) { article { display: grid; grid-template-columns: 3fr 1fr; column-gap: 20px; } nav ul { display: flex; } nav li { flex: 1; } } يعطينا تنسيق CSS تخطيطًا من عمودين ضمن العنصر <article> الأول يضم محتوى المقال الأساسي والآخر لمعلومات متعلقة بالمحتوى إلى الجانب. كما استخدمنا الصندوق المرن لوضع قائمة التنقل ضمن صف واحد. اطلع على الخطوة الثانية ضمن متصفحك أو الق نظرة على الشيفرة المصدرية. نتابع الآن العمل ونزيد الاتساع بالمقدار الذي نرى أنه مناسب كي يشكل الشريط الجانبي عمودًا جديدًا. وسنضع ضمن استعلام الوسائط شيفرة تحوّل العنصر الأساسي إلى شبكة من عمودين، وعلينا عندها إزالة margin-bottom من العنصر كي يتحاذى العمودان، كما سنضيف حدًا border أعلى التذييل. إن ما فعلناه عمليًا هو الشيء الذي نحتاجه ليبدو التصميم جيدًا عند كل نقطة انتقال. @media screen and (min-width: 70em) { main { display: grid; grid-template-columns: 3fr 1fr; column-gap: 20px; } article { margin-bottom: 0; } footer { border-top: 1px solid #ccc; margin-top: 2em; } } اطلع على الخطوة الثالثة ضمن متصفحك أو الق نظرة على الشيفرة المصدرية. لو نظرت إلى المثال الأخير سترى كيف يتجاوب التصميم مع الاتساعات المختلفة للشاشة ابتداءًا من عمود واحد ثم عمودين وثلاثة أعمدة وفقًا للاتساع المتاح. وهذا بالطبع مثال بسيط عن التصميم وفق مبدأ "الهاتف المحمول أولًا". الوسم <meta> الخاص بنافذة العرض إن ألقيت نظرة على الشيفرة المصدرية لصفحة متجاوبة سترى عادة الوسم <meta> ضمن الترويسة كالتالي: <meta name="viewport" content="width=device-width,initial-scale=1" /> وهي طريقة للتحكم بكيفية تصيير متصفحات الهاتف المحمول للمحتوى، لأن متصفحات الهواتف المحمولة لا تكون صادقة تمامًا فيما يخص اتساع نافذة العرض.ولا تُعرض معظم المواقع غير المتجاوية بالشكل الأفضل ضمن نوافذ العرض الضيقة. لهذا تصيير الهواتف الذكية المحتوى وفق نافذة العرض أوسع من نافذة العرض الفعلية للجهاز (عادة 980 بكسل) ومن ثم تقلّص الصفحة بعد تصييرها لتلائم شاشة الجهاز. ويعني هذا أن المواقع المتجاوبة لن تعمل كما هو متوقع إن كان اتساع نافذة العرض التي يتعامل معها الجهاز هي 980 بكسل. فالتخطيط الذي تريده عند النقطة الحدِّية{}media screen and (max-width: 600px)@ مثلًا لن يُصيّر كما هو متوقع. يأتي الحل لهذه المشكلة باستخدام الوسم <meta> الذي يعرف نافذة العرض كما في الشيفرة السابقة والذي يمنع متصفح الهاتف من تصيير المحتوى على أساس اتساع 980 بكسل، بل وفقًا لنافذة العرض الفعلية للجهاز، ويضبط المقياس افتراضيًا ليكون كمقياس الصفحة الأصلي. عندها ستعمل استعلامات الوسائط كما هو متوقع. هل نحتاج فعلًا استعلامات الوسائط؟ تقدم لك تقنيات مثل الصندوق المرنflexbox وتخطيط الشبكة grid والتخطيط متعدد الأعمدة multicol وسيلة لإنشاء صفحات ويب مرنة ومتجاوبة دون الحاجة إلى استعلامات الوسائط. ومن الأفضل التفكير في تصميمك إن كان يحتاج فعلًا إلى هذه الاستعلامات أو لا، فقد ترغب مثلًا بعرض مجموعة من البطاقات اتساعها على الأقل 200 بكسل بقدر ما تتسع له الحاوية، هذا الأمر سهل الإنجاز باستخدام تخطيط الشبكة دون استعلامات وسائط كما يلي: <ul class="grid"> <li> <h2>Card 1</h2> <p>…</p> </li> <li> <h2>Card 2</h2> <p>…</p> </li> <li> <h2>Card 3</h2> <p>…</p> </li> <li> <h2>Card 4</h2> <p>…</p> </li> <li> <h2>Card 5</h2> <p>…</p> </li> </ul> .grid { list-style: none; margin: 0; padding: 0; display: grid; gap: 20px; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); } .grid li { border: 1px solid #666; padding: 10px; } افتح هذا المثال باستخدام المتصفح أو اطلع على الشيفرة المصدرية. إن فتحت المثال في متصفحك، حاول أن تغيير اتساع نافذة المتصفح لترى كيف يتغير عدد الأعمدة في الصفحة. والمثير في هذه الطريقة عدم اعتماد الشبكة على اتساع نافذة العرض بل على مقدار المساحة المتاحة للعنصر أو الحاوية. قد تجد كتابة مقال عن استعلامات الوسائط ثم التوصية باستخدام تقنيات أخرى أمرًا غريبًا، لكن ما ستراه في الواقع التطبيقي هو تخطيطات ويب حديثة مدعومة باستعلامات وسائط للحصول على أفضل النتائج. الخلاصة تعلمنا في هذا المقال مبادئ استعلامات الوسائط وكيفية استخدامها عمليًا في إنشاء تصميمات تعتمد على قاعدة "الهاتف المحمول أولًا". بإمكانك استخدام الأمثلة والشيفرات المصدرية التي عرضناها كنقطة انطلاق وتتمرن بعدها على تطبيق استعلامات الوسائط المختلفة كأن تغير مثلًا حجم قائمة التنقل إن اكتشفت أن الزائر يستخدم جهاز تأشير خشن (غير دقيق) بالاستفادة من الميزة pointer. يمكنك اختبار الاستعلامات أيضًا بإضافة مكونات مختلفة وتحري إن كان إضافة استعلامات وسائط أو استخدام أساليب التخطيط المختلفة كالصندوق المرن أو الشبكات هو الأفضل في جعل تلك المكونات متجاوبة. إذًا لا توجدغالبًا طريقة صحيحة وأخرى خاطئة، وما عليك فعله هو التجريب لتعرف ما هو الأنسب لتصميمك. ترجمة -وبتصرف- للمقال: Beginners guide to media queries اقرأ أيضًا المقال السابق: التصميم المتجاوب لصفحات الويب Responsive Web Design استعلامات الوسائط (Media Queries) في CSS مدخل إلى التصميم المتجاوب والتصميم المتكيف عرض محتوى صفحات الويب بتجاوب على الأجهزة المتعددة
×
×
  • أضف...