-
المساهمات
164 -
تاريخ الانضمام
-
تاريخ آخر زيارة
نوع المحتوى
ريادة الأعمال
البرمجة
التصميم
DevOps
التسويق والمبيعات
العمل الحر
البرامج والتطبيقات
آخر التحديثات
قصص نجاح
أسئلة وأجوبة
كتب
دورات
كل منشورات العضو ابراهيم الخضور
-
تعرفنا في المقال السابق مكونات الويب: عناصر HTML المخصصة وقوالبها على عناصر HTML المخصصة وكيفية إنشائها وسنكمل التعرف على موضوع مهم في هذا الصدد وهو شجرة DOM الخفية Shadow DOM -والتي تترجم أيضًا إلى ظل شجرة DOM- تستخدَم للتغليف encapsulation، والتي تسمح لكل مكوِّن ويب مخصص أن يمتلك شجرةً خفيةً لا يمكن للمستند الرئيسي الوصول إليها صدفةً، وقد تمتلك قواعد تنسيق خاصة وغير ذلك من الخواص. شجرة DOM الخفية هل لديك فكرة عن مدى تعقيد إنشاء وتنسيق أدوات تحكم المتصفح؟ لنفترض وجود العنصر <"input type="range> مثلًا: input[type="range" i] { appearance: auto; cursor: default; color: rgb(157, 150, 142); padding: initial; border: initial; margin: 2px; } يستخدم المتصفح ضمنيًا DOM/CSS لرسم أدوات التحكم، ولا تظهر بنية شجرة DOM لنا عادةً، لكن يمكننا رؤيتها باستخدام أدوات التطوير بتفعيل الخيار Show user agent shadow DOM من إعدادات أدوات المطور Dev Tools تحديدًا قسم Elements في المتصفح Chrome مثلًا. ستبدو الشجرة DOM للعنصر السابق كالتالي: ما نراه تحت العنوان shadow-root# يسمَّى شجرة DOM المخفية، ولا يمكن الحصول على عناصر شجرة DOM المخفية المضمنة باستخدام استدعاءات JavaScript النظامية أو المحددات selectors، فهذه العناصر ليست أبناءً نظاميةً، بل هي تقنية تغليف متقدمة. سنرى في المثال السابق السمة المفيدة pseudo، وهي سمة غير قياسية وموجودة لأسباب تاريخية، حيث يمكننا استخدامها لتنسيق العناصر الفرعية باستخدام CSS كالتالي: <style> /* make the slider track red */ input::-webkit-slider-runnable-track { background: red; } </style> <input type="range"> See the Pen JS-p3-06-Shadow-DOM -ex1 by Hsoub (@Hsoub) on CodePen. بدأت المتصفحات باختبار الشيفرة بالاستفادة من بنى DOM الداخلية لتجهيز أدوات التحكم، وبعد فترة من الزمن طوّرت شجرة DOM الخفية وأصبحت معياريةً لتسمح لنا وللمطورين بالقيام بأشياء مماثلة. سنستخدم لاحقًا المعيار الحديث لشجرة DOM الخفية الذي تغطيه مواصفات DOM وغيرها من المواصفات ذات الصلة. الشجرة الخفية Shadow tree يمكن لكائن DOM أن يمتلك نوعين من الأشجار الفرعية: شجرة ظاهرة Light tree: وتمثل شجرةً فرعيةً نظاميةً مكوّنةً من عناصر HTML أبناء، وجميع الأشجار التي رأيناها في المقال السابق من هذا النوع. شجرة خفية Shadow tree: شجرة DOM فرعية لا تظهر على صفحة HTML، بل تكون مخفيةً من الظهور والرؤية. إذا امتلك العنصر الشجرتين السابقتين فسيصيّر المتصفح الشجرة الخفية، لكن يمكن إعداد توليفة بين الشجرتين، وسنرى تفاصيل أكثر في مقالات لاحقة. يمكن استخدام الشجرة الخفية في العناصر المخصصة لإخفاء المكوّنات الداخلية وتطبيق تنسيق خاص بالمكوِّن. يخفي العنصر <show-hello> على سبيل المثال شجرة DOM الداخلية ضمن شجرة خفية: <script> customElements.define('show-hello', class extends HTMLElement { connectedCallback() { const shadow = this.attachShadow({mode: 'open'}); shadow.innerHTML = `<p> Hello, ${this.getAttribute('name')} </p>`; } }); </script> <show-hello name="John"></show-hello> See the Pen JS-p3-06-Shadow-DOM -ex2 by Hsoub (@Hsoub) on CodePen. ستظهر النتيجة في أدوات تطوير متصفح بالشكل التالي، حيث تكون جميع المحتويات تحت "shadow-root#": يُنشئ الاستدعاء ({...:elem.attachShadow({mode شجرةً مخفيةً أولًا، لكن توجد عقبتان: يمكن إنشاء جذر مخفي واحد لكل عنصر. يجب أن يكون العنصر عنصرًا مخصصًا، أو أحد العناصر التالية: "article". "blockquote". "body". "div". "footer". "h1…h6". "header". "main". "nav". "p". "section". "span". ولا يمكن لغير هذه العناصر استضافة شجرة خفية. يضبط الخيار mode مستوى التغليف، ويمكن أن يحمل إحدى القيمتين: "open": يمكن أن نحصل على الجذر الخفي من خلال elem.shadowRoot، ويمكن لأي شيفرة الوصول إلى الشجرة الخفية لــ elem. "closed": ويعيد الأمر elem.shadowRoot القيمة null دائمًا. يمكن الوصول إلى شجرة DOM الخفية عن طريق المرجع الذي يعيده attachShadow فقط، ومن الممكن أن يكون مخفيًا ضمن صنف، وتكون الأشجار الخفية الأصلية المتعلقة بالمتصفح -مثل <input type="range">- من النوع المغلق، ولا يمكن الوصول إليها، ويشابه الجذر الخفي shadow root الذي يعيده attachShadow العناصر، ويمكن إظهاره باستخدام innerHTML أو توابع DOM مثل append. يُدعى العنصر الذي يمتلك جذرًا خفيًا بمضيف الشجرة الخفية shadow tree host، ويتاح للاستخدام ضمن الخاصية host للجذر الخفي: // لن يعيد شيئًا elem.shadowRoot وإلا {mode: "open"} بافتراض أن alert(elem.shadowRoot.host === elem); // محقق التغليف Encapsulation تفصَل شجرة DOM الخفية عن المستند الرئيسي: لا تكون عناصر شجرة DOM الخفية مرئيةً للتابع querySelector من شجرة DOM الظاهرة، وقد تمتلك عناصر شجرة DOM الخفية معرفات id تتعارض مع تلك الموجودة في شجرة DOM الظاهرة، فهي فريدة ضمن الشجرة الخفية فقط. لشجرة DOM الخفية أوراق تنسيق خاصة بها، إذ لا تُطبق قواعد التنسيق الخارجية عليها. إليك مثالًا: <style> /*(1) #elem لن يطبق التنسيق على الشجرة الخفية للعنصر*/ p { color: red; } </style> <div id="elem"></div> <script> elem.attachShadow({mode: 'open'}); // (للشجرة الخفية تنسيقها الخاص (2 elem.shadowRoot.innerHTML = ` <style> p { font-weight: bold; } </style> <p>Hello, John!</p> `; // مرئية فقط للاستعلامات التي تجري داخل الشجرة <p> (3) alert(document.querySelectorAll('p').length); // 0 alert(elem.shadowRoot.querySelectorAll('p').length); // 1 </script> تشير (1) و(2) و(3) أعلاه إلى الآتي: (1) لا يؤثر تنسيق الصفحة على تنسيق الشجرة الخفية. (2) يؤدي التنسيق داخل الشجرة عمله بطريقة صحيحة. (3) للحصول على العناصر من الشجرة الخفية، لا بدّ من الاستعلام عنها من داخل الشجرة. تقليم شجرة DOM الخفية وعملية التركيب تحتاج العديد من المكوّنات إلى محتوىً لتصييره، مثل النوافذ ومعارض الصور والقوائم وغيرها، ويتوقع العنصر المخصص <custom-tabs> محتوى نافذة ليمرَّر إليه، على غرار العناصر الأصلية المضمنة في المتصفح مثل <select> -عدا عناصر <option>-، ويحتاج العنصر <custom-menu> كذلك إلى محتوى على شكل عناصر قائمة. قد تبدو الشيفرة التي تستعمل العنصر <custom-menu> كالتالي: <custom-menu> <title>Candy menu</title> <item>Lollipop</item> <item>Fruit Toast</item> <item>Cup Cake</item> </custom-menu> سيتمكن بعدها مكوّننا من تصيير المحتوى بصورة صحيحة، على شكل قائمة جميلة لها عنوان وعناصر ومعالجات لأحداث القائمة وغيرها، ولكن كيف سننجز ذلك؟ يمكننا أن نحاول تحليل محتوى العناصر وأن ننسخ أو نعيد ترتيب عقد DOM ديناميكيًا، لكن إذا كنا سننقل عناصر إلى شجرة DOM الخفية shadow DOM، فلن يُطبَّق تنسيق CSS الموجود ضمن المستند عليها، وبالتالي سنفقد التنسيق المرئي، وسيحتاج ذلك إلى كتابة بعض الشيفرة، ولحسن الحظ ليس علينا فعل ذلك، إذ تدعم شجرة DOM الخفية عناصر <slot> التي تُملأ تلقائيًا بالمحتوى الموجود في شجرة DOM الظاهرة وشبهنا هذه العملية بعملية "التقليم" التي تجرى على الأشجار من قص وإنشاء فتحة في الشجرة ووضع فرع في الفتحة من شجرة أو نوع آخر لذلك أطلقنا عليها هذا الاسم وسنستعمل اسم "فتحة" المقابل للكلمة الأجنبية slot (اسم العنصر) للسهولة. الفتحات المسماة Named slots لنلق نظرةً على طريقة عمل التقليمات في هذا المثال البسيط، حيث توفر شجرة DOM الخفية للعنصر المخصص <user-card> فتحتين تُملآن من شجرة DOM الظاهرة: <script> customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = ` <div>Name: <slot name="username"></slot> </div> <div>Birthday: <slot name="birthday"></slot> </div> `; } }); </script> <user-card> <span slot="username">John Smith</span> <span slot="birthday">01.01.2001</span> </user-card> See the Pen JS-p3-06-Shadow-DOM -ex3 by Hsoub (@Hsoub) on CodePen. يعرّف العنصر <"slot name="X> في شجرة DOM الخفية نقطة إدراج، وهو المكان الذي تُصيَّر فيه العناصر التي تعرف فيها الخاصية "slot="X، ثم ينفِّذ المتصفح عملية تركيب composition، إذ يأخذ عناصر من شجرة DOM الظاهرة، ويصيّرها ضمن الفتحات الملائمة ضمن شجرة DOM الخفية، وهكذا سنحصل في نهاية الأمر على مكوِّن يمكن ملؤه بالبيانات، وهذا ما نريده. إليك بنية الشجرة DOM بعد تنفيذ السكربت مهملين عملية التركيب: <user-card> #shadow-root <div>Name: <slot name="username"></slot> </div> <div>Birthday: <slot name="birthday"></slot> </div> <span slot="username">John Smith</span> <span slot="birthday">01.01.2001</span> </user-card> لقد أنشأنا شجرة DOM خفيةً، وسنجدها تحت #shadow-root. سيمتلك العنصر الآن شجرتي DOM الظاهرة والخفية. سيبحث المتصفح -لأغراض التصيير- عن السمة "..."=slot لكل <"..."=slot name> التي لها نفس الاسم في شجرة DOM الظاهرة، وستُصيَّر هذه العناصر ضمن الفتحات. وعندها ستسمى النتيجة شجرة DOM المسطّحة flattened DOM. <user-card> #shadow-root <div>Name: <slot name="username"> <!-- slotted element is inserted into the slot --> <span slot="username">John Smith</span> </slot> </div> <div>Birthday: <slot name="birthday"> <span slot="birthday">01.01.2001</span> </slot> </div> </user-card> لقد أوُجِدت شجرة DOM المسطحة لأغراض التصيير ومعالجة الأحداث، قد تبدو الطريقة التي تُعرض بها الأمور افتراضيةً نوعًا ما، لكن لن تُنقَل العقد في المستند فعليًا، فمن السهل التحقق من أنّ العقد لا تزال في مكانها بتنفيذ الأمر querySelectorAll. alert( document.querySelectorAll('user-card span').length ); // 2 تُشتقّ شجرة DOM المسطحة من شجرة DOM الخفية بإدراج الفتحات، حيث يصيّرها المتصفح ويستخدمها في وراثة التنسيق وانتقال الأحداث -وهو ما سنوضّحه لاحقًا-، لكن نظرتها لن تغير JavaScript إلى المستند قبل الوصول إلى الشجرة المسطحة. ملاحظة: يمكن للأبناء من المستوى الأعلى فقط امتلاك السمة "…"=slot، إذ يمتلك الأبناء المباشرون للمضيف الخفي -وهو العنصر <user-card> في مثالنا- السمة "..."=slot فقط، بينما ستُهمل هذه السمة للعناصر الداخلية. وسيتجاهل المتصفح في المثال التالي العنصر <span> الثاني، فهو ليس ابنًا مباشرًا للعنصر <user-card>: <user-card> <span slot="username">John Smith</span> <div> <!-- invalid slot, must be direct child of user-card --> <span slot="birthday">01.01.2001</span> </div> </user-card> إذا وجدت عناصر لها فتحات بنفس الاسم في شجرة DOM الظاهرة، فستوضع في الفتحة واحدةً تلو الأخرى، إليك المثال التالي: <user-card> <span slot="username">John</span> <span slot="username">Smith</span> </user-card> إليك مثالًا عن شجرة DOM مسطحة تمتلك عنصرين ضمن الفتحة <"slot name="username>: <user-card> #shadow-root <div>Name: <slot name="username"> <span slot="username">John</span> <span slot="username">Smith</span> </slot> </div> <div>Birthday: <slot name="birthday"></slot> </div> </user-card> المحتوى الابتدائي للعنصر slot إذا وُضع محتوًى ما ضمن الفتحة <slot>، فسيصبح هو المحتوى الافتراضي حيث سيعرضه المتصفح إذا لم يوجد محتوىً مناسب ضمن شجرة DOM الظاهرة لملء الفتحة. عندها ستكون نتيجة تصيير هذا الجزء مثلًا من شجرة DOM الخفية هي Anonymous إذا لم توجد فتحة باسم "slot="username في شجرة DOM الظاهرة. <div>Name: <slot name="username">Anonymous</slot> </div> الفتحة الافتراضية هي أول فتحة بلا اسم ستكون أول فتحة <slot> في شجرة DOM الخفية غير مالكة لاسم هي الفتحة الافتراضية، وستحصل على كل العقد في شجرة DOM الظاهرة التي لم تُدرج في أي فتحة أخرى. لنضف في المثال التالي فتحةً افتراضيةً للعنصر المخصص <user-card> الذي سيخفي كل معلومات المستخدم غير المدرجة في فتحات: <script> customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = ` <div>Name: <slot name="username"></slot> </div> <div>Birthday: <slot name="birthday"></slot> </div> <fieldset> <legend>Other information</legend> <slot></slot> </fieldset> `; } }); </script> <user-card> <div>I like to swim.</div> <span slot="username">John Smith</span> <span slot="birthday">01.01.2001</span> <div>...And play volleyball too!</div> </user-card> See the Pen JS-p3-06-Shadow-DOM -ex4 by Hsoub (@Hsoub) on CodePen. سيوضع محتوى شجرة DOM الظاهرة غير المُدرج في المثال السابق ضمن العنصر <fieldset> الذي يحمل تسمية Other information، حيث ستُدرج العناصر ضمن الفتحة واحدًا تلو الآخر، ولذا ستُدرج قطعتا المعلومات معًا ضمن الفتحة الافتراضية. وستبدو شجرة DOM المسطحة كالتالي: <user-card> #shadow-root <div>Name: <slot name="username"> <span slot="username">John Smith</span> </slot> </div> <div>Birthday: <slot name="birthday"> <span slot="birthday">01.01.2001</span> </slot> </div> <fieldset> <legend>Other information</legend> <slot> <div>I like to swim.</div> <div>...And play volleyball too!</div> </slot> </fieldset> </user-card> مثال القائمة لنعد الآن إلى العنصر المخصص الذي أشرنا إليه سابقًا في هذا المقال، حيث يمكن استخدام الفتحات لتوزيع العناصر، إليك ترميز HTML للعنصر المخصص <custom-menu>: <custom-menu> <span slot="title">Candy menu</span> <li slot="item">Lollipop</li> <li slot="item">Fruit Toast</li> <li slot="item">Cup Cake</li> </custom-menu> تمثل الشيفرة التالية قالب شجرة DOM الخفية مزوّدةً بالفتحات الملائمة: <template id="tmpl"> <style> /* menu styles */ </style> <div class="menu"> <slot name="title"></slot> <ul><slot name="item"></slot></ul> </div> </template> يُدرج <"span slot="title> ضمن <"slot name="title>. هناك العديد من عناصر القائمة <li slot="item"> في القالب، لكن توجد فتحة مناسبة <slot name="item"> واحدة ضمنه، لذلك ستُدرج كل عناصر القائمة -مثل <li slot="item">- ضمن الفتحة الواحد تلو الآخر، وهذا ما يُشكِّل القائمة. ستصبح شجرة DOM المسطّحة كالتالي: <custom-menu> #shadow-root <style> /* menu styles */ </style> <div class="menu"> <slot name="title"> <span slot="title">Candy menu</span> </slot> <ul> <slot name="item"> <li slot="item">Lollipop</li> <li slot="item">Fruit Toast</li> <li slot="item">Cup Cake</li> </slot> </ul> </div> </custom-menu> قد نُلاحظ تواجد العنصر <li> ضمن القائمة <ul> في شجرة DOM صالحة، لكن الشجرة في الشيفرة السابقة مسطحة تصف آلية تصيير المكوِّن، وهذا الأمر طبيعي، ويبقى علينا إضافة معالج للحدث click لفتح وإغلاق القائمة <custom-menu> لتصبح جاهزةً: customElements.define('custom-menu', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); //هو قالب الشجرة الخفية في الأعلى this.shadowRoot.append( tmpl.content.cloneNode(true) ); // لا يمكن اختيار عقد من الشجرة الظاهرة، لذلك تُعالج النقرات ضمن الفتحة this.shadowRoot.querySelector('slot[name="title"]').onclick = () => { // فتح وإغلاق القائمة this.shadowRoot.querySelector('.menu').classList.toggle('closed'); }; } }); وستكون النتيجة: See the Pen JS-p3-06-Shadow-DOM -ex5 by Hsoub (@Hsoub) on CodePen. يمكن بالطبع تعزيز وظائف القائمة من خلال الأحداث والتوابع وغيرها. تحديث الفتحات ماذا لو أرادت الشيفرة الخارجية إضافة أو إزالة العناصر ديناميكيًا؟ يراقب المتصفح الفتحات، ثم تُحدَّث العناصر المصيّرة إذا تغيرت أو أزيلت العناصر المدرجة، وطالما ستُصيَّر العناصر الظاهرة فقط في الفتحات -ولن تُنسَخ- فستظهر أي تغييرات عليها مباشرةً، فلا حاجة لفعل أي شيء لتحديث نتيجة التصيير، لكن يمكن استخدام الحدث slotchange إذا أرادت شيفرة المكوّن مراقبة التغيرات في الفتحات. يُدرَج في المثال التالي عنصر قائمة ديناميكيًا بعد ثانية، ويتغير العنوان بعد ثانيتين: <custom-menu id="menu"> <span slot="title">Candy menu</span> </custom-menu> <script> customElements.define('custom-menu', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = `<div class="menu"> <slot name="title"></slot> <ul><slot name="item"></slot></ul> </div>`; //لا يمتلك الجذر الخفي معالجات أحداث لذا سنستخدم الابن الأول this.shadowRoot.firstElementChild.addEventListener('slotchange', e => alert("slotchange: " + e.target.name) ); } }); setTimeout(() => { menu.insertAdjacentHTML('beforeEnd', '<li slot="item">Lollipop</li>') }, 1000); setTimeout(() => { menu.querySelector('[slot="title"]').innerHTML = "New menu"; }, 2000); </script> See the Pen JS-p3-06-Shadow-DOM -ex6 by Hsoub (@Hsoub) on CodePen. يُحدَّث تصيير القائمة كل مرة دون تدخلنا، وسنلاحظ في الشيفرة السابقة وجود حدثي slotchange: عند التهيئة: حيث يقع الحدث slotchange: title مباشرةً عندما يُدرج "slot="title من شجرة DOM الظاهرة ضمن الفتحة الموافقة له. بعد ثانية: يقع الحدث slotchange: item عندما يُضاف عنصر قائمة جديد <"li slot="item>. لاحظ أنه لا وجود للحدث slotchange بعد ثانيتين عندما يُعدَّل محتوى "slot="title، والسبب هو عدم وجود أي تغييرات في الفتحة، حيث عدّلنا محتوى عنصر مُدرج في الفتحة، وهذا أمر مختلف. يمكن تتبع التغيرات الداخلية ضمن شجرة DOM الظاهرة باستخدام شيفرة JavaScript من خلال الآلية المعمّمة المعروفة بـ MutationObserver. الواجهة البرمجية للفتحات لنلق نظرةً على توابع JavaScript المتعلقة بالفتحات، إذ تنظر JavaScript كما رأينا سابقًا إلى شجرة DOM الحقيقية دون تسطيح flattening، لكن إذا كانت قيمة الخيار mode في الشجرة الخفية هي "open"، فسنتمكن من معرفة أي العناصر أدرِجت في فتحات، كما يمكننا معرفة الفتحة من العنصر الذي أدرج ضمنه: node.assignedSlot: تعيد الفتحة <slot> التي أسندت إليها عقدة من الشجرة node. ({slot.assignedNodes({flatten: true/false: يعيد عقد DOM التي أسندت إلى فتحة، وستكون قيمة الخيار flatten هي "false" افتراضيًا، لكن إذا ضبطت على القيمة "true" صراحةً، فسيُنظر إلى DOM المسطحة بعمق، كما سيعيد التابع الفتحات المتداخلة nested في حال وجود مكوّنات متداخلة، ويعيد "محتوى التراجع" إذا لم يجد أي عقد أسندت إلى فتحات. ({slot.assignedElements({flatten: true/false: يعيد عناصر DOM التي أسندت إلى فتحة، وهو يشابه التابع السابق إلا أنه يعيد عقد العنصر. تُستخدَم هذه التوابع عندما نريد إظهار محتوى الفتحات وتتبعها باستخدام JavaScript. وإذا أراد المكوّن <custom-menu> مثلًا معرفة ما الذي يعرضه، فيمكنه تعقّب الحدث slotchange والحصول على العناصر من خلال slot.assignedElements: <custom-menu id="menu"> <span slot="title">Candy menu</span> <li slot="item">Lollipop</li> <li slot="item">Fruit Toast</li> </custom-menu> <script> customElements.define('custom-menu', class extends HTMLElement { items = [] connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = `<div class="menu"> <slot name="title"></slot> <ul><slot name="item"></slot></ul> </div>`; // يقع عندما يتغير محتوى الفتحة this.shadowRoot.firstElementChild.addEventListener('slotchange', e => { let slot = e.target; if (slot.name == 'item') { this.items = slot.assignedElements().map(elem => elem.textContent); alert("Items: " + this.items); } }); } }); // items update after 1 second setTimeout(() => { menu.insertAdjacentHTML('beforeEnd', '<li slot="item">Cup Cake</li>')}, 1000); </script> See the Pen Untitled by Hsoub (@Hsoub) on CodePen. تنسيق شجرة DOM الخفية يمكن أن تحتوي شجرة DOM الخفية على الوسمين <style> و<link rel="stylesheet" href="…">، وتُخزّن أوراق التنسيق في الوسم الأخير في ذاكرة HTTP المؤقتة، لذلك لن يُعاد تحميلها للمكونات المختلفة التي تستخدم نفس القالب، ويمكن أن نعد ما لي قاعدةً عامةً: يعمل التنسيق المحلي داخل الشجرة الخفية، بينما تعمل تنسيقات المستند ككل خارجها، مع وجود بعض الاستثناءات. المحدد host: يسمح هذا المحدد باختيار المضيف الخفي، أي العنصر الذي يحتوي الشجرة الخفية، لنفترض مثلًا أننا سننشئ عنصرًا مخصصًا <custom-dialog> يجب أن يتوسط الصفحة، لذلك لا بدّ من تنسيق المكوّن <custom-dialog> ذاته، وهذا ما يفعله host: تمامًا: <template id="tmpl"> <style> /* the style will be applied from inside to the custom-dialog element */ :host { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); display: inline-block; border: 1px solid red; padding: 10px; } </style> <slot></slot> </template> <script> customElements.define('custom-dialog', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true)); } }); </script> <custom-dialog> Hello! </custom-dialog> See the Pen JS-p3-06-Shadow-DOM-styling -ex1 by Hsoub (@Hsoub) on CodePen. توريث التنسيقات Cascading يبقى المضيف الخفي -<custom-dialog> ذاته- ضمن شجرة DOM الظاهرة، لذلك سيتأثر بقواعد CSS التي يخضع لها المستند كاملاً، فإذا وجدت خاصية جرى تنسيقها محليًا ضمن host: وفي المستند بنفس الوقت، فسيُطبق حينها تنسيق المستند. فإن كان المستند مثلًا: <style> custom-dialog { padding: 0; } </style> فسيُعرض العنصر <custom-dialog> دون حاشية padding. من الملائم أن نعّرف تنسيقًا افتراضيًا للمكوّن ضمن القاعدة host:، ثم نعيد التنسيق -إذا أردنا- باستخدام تنسيق المستند، ويبقى الاستثناء في الخاصية التي نعنونها important!،عندها يُطبق التنسيق المحلي. المحدد (host(selector: هو نفسه host: لكنه يُطبّق فقط إذا تطابق اسم المضيف الخفي مع الوسيط selector، فعلى سبيل المثال: نريد توسيط المكوّن <custom-dialog> في الصفحة فقط إذا امتلك السمة centered: <template id="tmpl"> <style> :host([centered]) { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); border-color: blue; } :host { display: inline-block; border: 1px solid red; padding: 10px; } </style> <slot></slot> </template> <script> customElements.define('custom-dialog', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}).append(tmpl.content.cloneNode(true)); } }); </script> <custom-dialog centered> Centered! </custom-dialog> <custom-dialog> Not centered. </custom-dialog> See the Pen JS-p3-06-Shadow-DOM-styling -ex2 by Hsoub (@Hsoub) on CodePen. والآن سيُطبّق التوسيط فقط على المكوّن الأول <custom-dialog centered>. باختصار، يمكن استخدام عائلة المحددات host: لتنسيق العنصر الرئيسي للمكوِّن بناءً على السياق، ويمكن تغيير هذه التنسيقات -ما لم تستخدم السمة important!- من قبل تنسيق المستند. تنسيق محتوى الفتحات لننظر إلى حالة وجود فتحات slots. تأتي المكوّنات المقلمة slotted components من شجرة DOM الظاهرة، لذا ستستخدم تنسيقات المستند، ولن يؤثر التنسيق المحلي على المحتوى المركب. سنرى في المثال التالي أنّ العنصر المركب <span> سيظهر بخط سميك وفق تنسيق المستند، لكنه لن يتأثر بقيمة الخاصية background العائدة للتنسيق المحلي للعنصر: <style> span { font-weight: bold } </style> <user-card> <div slot="username"><span>John Smith</span></div> </user-card> <script> customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = ` <style> span { background: red; } </style> Name: <slot name="username"></slot> `; } }); </script> See the Pen JS-p3-06-Shadow-DOM-styling -ex3 by Hsoub (@Hsoub) on CodePen. سيكون الخط سميكًا لكنه ليس باللون الأحمر. وإذا أردنا تنسيق العناصر المركبة في مكوننا فأمامنا خياران هما: الأول هو تنسيق الفتحة <slot> نفسه اعتمادًا على وراثة تنسيق CSS: <user-card> <div slot="username"><span>John Smith</span></div> </user-card> <script> customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = ` <style> slot[name="username"] { font-weight: bold; } </style> Name: <slot name="username"></slot> `; } }); </script> See the Pen JS-p3-06-Shadow-DOM-styling -ex4 by Hsoub (@Hsoub) on CodePen. سيكون الخط سميكًا في <p>John Smith</p>، وذلك لأن وراثة CSS ستنقل التنسيق من الفتحة <slot> إلى المحتويات، لكن ليست كل خصائص CSS قابلة للوراثة. الخيار الثاني هو استخدام محدد الصنف الوهمي (slotted(selector::، والتي تطابق العناصر بناءً على شرطين: أن يكون العنصر مركبًا وآتيًا من الشجرة الظاهرة، ولا يهم اسم الفتحة هنا، ويطبق على العنصر المركب ذاته وليس على أبنائه. أن يطابق العنصر قيم الوسيط selector. يختار المحدد (slotted(div:: في مثالنا التالي العنصر <"div slot="username> تمامًا وليس أبناءه: <user-card> <div slot="username"> <div>John Smith</div> </div> </user-card> <script> customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = ` <style> ::slotted(div) { border: 1px solid red; } </style> Name: <slot name="username"></slot> `; } }); </script> See the Pen JS-p3-06-Shadow-DOM-styling -ex5 by Hsoub (@Hsoub) on CodePen. لاحظ أنه يمكن للمحدد slotted:: التقدم داخل عناصر الفتحة، لذا تكون المحددات التالية غير صحيحة: ::slotted(div span) { /*مع هذا المحدد div لايتطابق العنصر */ } ::slotted(div) p { /* DOM لا يمكنه التقدم داخل */ } يمكن استخدام slotted:: أيضًا في querySelector. خطافات CSS مع خاصيات مخصصة كيف ننسق العناصر الداخلية للمكوِّن من المستند الرئيسي؟ تطبق المحددات -مثل host:- القواعد على عناصر مخصصة، مثل <custom-dialog> أو <user-card>، لكن كيف ننسق عناصر شجرة DOM الخفية ضمنها؟ لا يوجد محدد قادر على التأثير المباشر على تنسيق شجرة DOM الخفية من المستند الرئيسي، لكن -وكما عرضنا توابع للتفاعل مع مكوننا- يمكن عرض متغيرات CSS -خصائص CSS مخصصة- إلى التنسيق. تتواجد الخصائص المخصصة على كل المستويات وفي كلتا الشجرتين الخفية والظاهرة، حيث يمكن مثلًا استخدام المتغير --user-card-field-color في شجرة DOM الخفية لتنسيق الحقول، ويمكن أن يضبط المستند الخارجي قيمتها: <style> .field { color: var(--user-card-field-color, black); /* معرفًا استخدم اللون الأسود --user-card-field-color */ } </style> <div class="field">Name: <slot name="username"></slot></div> <div class="field">Birthday: <slot name="birthday"></slot></div> عندها يمكن التصريح عن هذه الخاصية في المستند الخارجي للمكوّن <user-card>: user-card { --user-card-field-color: green; } تخترق خاصيات CSS المخصصة شجرة DOM الخفية، وهي مرئية في كل مكان، لذلك يمكن للقاعدة الداخلية field. استخدامها، وإليك مثالًا كاملًا: <style> user-card { --user-card-field-color: green; } </style> <template id="tmpl"> <style> .field { color: var(--user-card-field-color, black); } </style> <div class="field">Name: <slot name="username"></slot></div> <div class="field">Birthday: <slot name="birthday"></slot></div> </template> <script> customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.append(document.getElementById('tmpl').content.cloneNode(true)); } }); </script> <user-card> <span slot="username">John Smith</span> <span slot="birthday">01.01.2001</span> </user-card> See the Pen JS-p3-06-Shadow-DOM-styling -ex6 by Hsoub (@Hsoub) on CodePen. شجرة DOM الخفية والأحداث إنّ الفكرة الأساسية من الشجرة الخفية هي تغليف تفاصيل التنفيذ الداخلي للمكوّن. لنفترض وقوع حدث نقرٍ داخل شجرة DOM الخفية للمكوّن <user-card>، لكن لا تملك السكربتات في المستند الرئيسي أي فكرة عن ما يجري داخل الشجرة، وخاصةً إذا أتى المكون من طرف ثالث، لذا يعيد المتصفح استهداف الحدث للمحافظة على تغليف التفاصيل. تستهدف الأحداث التي تحدث في شجرة DOM الخفية العنصر المضيف عندما تُلتقط خارج إطار المكوِّن. وإليك مثالًا بسيطًا: <user-card></user-card> <script> customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = `<p> <button>Click me</button> </p>`; this.shadowRoot.firstElementChild.onclick = e => alert("Inner target: " + e.target.tagName); } }); document.onclick = e => alert("Outer target: " + e.target.tagName); </script> See the Pen JS-p3-06-Shadow-DOM-and-events -ex1 by Hsoub (@Hsoub) on CodePen. إذا نقرت على الزر فستظهر الرسائل التالية: الهدف الداخلي BUTTON: معالج حدث داخلي يحصل على الهدف الصحيح، وهو عنصر داخل الشجرة الخفية. الهدف الخارجي USER-CARD: معالج حدث المستند، والذي يحصل على المضيف الخفي مثل هدف. إنّ إعادة استهداف retargeting الأحداث أمر مفيد جدًا، إذ يُفترض أن لا يعرف المستند الخارجي ما يوجد داخل المكوّن، فالحدث قد وقع من وجهة نظره على المكوّن <user-card>. لا تحدث إعادة الاستهداف إذا وقع الحدث في عنصر مُركّب، وذلك لأنه موجود فيزيائيًا في شجرة DOM الظاهرة، فلو نقر مستخدم مثلًا على <span slot="username"> في المثال التالي، فسيستهدف الحدث العنصر span هذا بالتحديد، وفي كلا معالجي الحدث الخاصين بالشجرتين الظاهرة والخفية: <user-card id="userCard"> <span slot="username">John Smith</span> </user-card> <script> customElements.define('user-card', class extends HTMLElement { connectedCallback() { this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = `<div> <b>Name:</b> <slot name="username"></slot> </div>`; this.shadowRoot.firstElementChild.onclick = e => alert("Inner target: " + e.target.tagName); } }); userCard.onclick = e => alert(`Outer target: ${e.target.tagName}`); </script> See the Pen JS-p3-06-Shadow-DOM-and-events -ex2 by Hsoub (@Hsoub) on CodePen. فإذا نُقر الاسم "John Smith"، فسيكون الهدف بالنسبة لمعالجي الحدث الداخلي والخارجي هو <"span slot="username>، وهنا لن يُعاد الاستهداف لأنه عنصر من DOM الظاهرة. من ناحية أخرى، إذا نُقر عنصر يعود إلى شجرة DOM الخفية مثل العنصر <b>Name</b>، فسُيعاد ضبط هدف الحدث event.target أثناء انسياب الحدث لخارج شجرة DOM الخفية ليصبح <user-card>. التابعان Bubbling و()event.composedPath تُستخدم DOM المسطحة لدفع الأحداث إلى الخارج bubbling، لذا إذا وُجد عنصر مركب slotted في فتحة ووقع حدث في مكان ما بداخله، فسيندفع الحدث إلى خارج العنصر باتجاه الفتحة <slot> وإلى الأعلى، ويمكن الحصول على المسار الكامل للهدف الأصلي للحدث مع كل عناصر شجرته الخفية باستخدام التابع ()event.composedPath، ويؤخذ هذا المسار -كما يدل اسم التابع- بعد عملية التركيب، وإليك شجرة DOM المسطحة للمثال السابق: <user-card id="userCard"> #shadow-root <div> <b>Name:</b> <slot name="username"> <span slot="username">John Smith</span> </slot> </div> </user-card> عند النقر على <span slot="username">، سيعيد استدعاء التابع ()event.composedPath المصفوفة التالية: [span, slot, div, shadow-root, user-card, body, html, document, window]، وهي شجرة العناصر كاملةً ابتداءًا من العنصر المُستهدف في شجرة DOM المسطحة بعد التركيب. التابع event.composed تنساب معظم الأحداث إلى خارج حدود شجرة DOM الخفية، لكن هذا لا ينطبق على بعضها، وذلك بحسب قيمة كائن الحدث composed، فإذا كانت "true" فستعبر الأحداث حدود الشجرة، وإلا فستلتقَط داخل حدود الشجرة فقط. إذا ألقينا نظرةً على توصيفات أحداث واجهة المستخدم UI Events specification، فسنجد أن قيمة composed هي "true" للأحداث: blur وfocus وfocusin وfocusout. click وdblclick. mousedown وmouseup وmousemove وmouseout وmouseover. wheel. beforeinput وinput وkeydown وkeyup. جميع أحداث اللمس والتأشير. أما الأحداث التي يكون فيها composed: false فهي: mouseenter وmouseleave: التي لا تنساب للخارج إطلاقًا. load وunload وabort وerror. select. slotchange. يمكن التقاط هذه الأحداث من قبل العناصر داخل شجرة DOM نفسها، حيث يقيم العنصر المستهدف. الأحداث المخصصة لا بدّ من ضبط الخاصيتين bubbles وcomposed على القيمة "true" إذا أردنا إيفاد أحداث مخصصة تنساب إلى مستوى أعلى وإلى خارج المكوّن، وسننشئ في المثال التالي div#inner في شجرة DOM الخفية ضمن div#outer ونطلق حدثين فيها، وسيصل الحدث الذي يمتلك الخاصية composed: true فقط إلى الخارج، وصولًا إلى المستند: <div id="outer"></div> <script> outer.attachShadow({mode: 'open'}); let inner = document.createElement('div'); outer.shadowRoot.append(inner); /* div(id=outer) #shadow-dom div(id=inner) */ document.addEventListener('test', event => alert(event.detail)); inner.dispatchEvent(new CustomEvent('test', { bubbles: true, composed: true, detail: "composed" })); inner.dispatchEvent(new CustomEvent('test', { bubbles: true, composed: false, detail: "not composed" })); </script> خلاصة شجرة DOM الخفية هي طريقة لإنشاء شجرة محلية خاصة بالمكوِّن. ({shadowRoot = elem.attachShadow({mode: open|closed: ينشئ شجرة DOM خفيةً للعنصر elem، ويمكن الوصول إليها باستخدام الخاصية elem.shadowRoot إذا كانت الخاصية "mode="open. يمكن تعميم الكائن shadowRoot باستخدام innerHTML أو توابع DOM أخرى. عناصر شجرة DOM الخفية: لها فضاء خاص لقيم المعرّفات id. غير مرئية لمحددات JavaScript الموجودة في المستند الرئيسي، مثل querySelector. تستخدم التنسيق الموجود ضمن الشجرة الخفية فقط، وليس تنسيق المستند الرئيسي. تُصيّر شجرة DOM الخفية -إن وجدت- ضمن ما يُسمى شجرة DOM الظاهرة light DOM، مثل عناصر أبناء نظاميين. إذا امتلك عنصر شجرة DOM خفيةً، فلن تُعرض شجرة DOM الظاهرة عادةً، لذلك تسمح الفتحات slots -الناتجة عن عملية تقليم الشجرة- بعرض عناصر من شجرة DOM الظاهرة في أماكن محددة من شجرة DOM الخفية، ويوجد نوعان من الفتحات: الفتحات المسماة <slot name="X">...</slot>: الحصول على أبناء شجرة DOM الظاهرة فتحتها slot="X". الفتحة الافتراضية: وهي أول فتحة <slot> لا تحمل اسمًا، وتهمَل الفتحات غير المسماة التي تأتي لاحقًا، كما يحصل على أبناء شجرة DOM الظاهرة الذين لم يُدرجوا في فتحات. إذا وجدَت عدة عناصر في الفتحة نفسها، فستُدرج واحدًا بعد الآخر. يُستخدَم محتوى الفتحة <slot> محتوىً للتراجع، لذا يظهر هذا المحتوى عند عدم وجود أبناء لشجرة DOM الظاهرة في الفتحة. تُدعى عملية تصيير العناصر المدرجة في فتحات ضمن فتحاتها بالتركيب composition، وتسمى النتيجة شجرة DOM المسطّحة. لا تنقل عملية التركيب العقد في الواقع، فمن وجهة نظر JavaScript ستبقى شجرة DOM نفسها، ويمكن الوصول إلى الفتحات من خلال توابع JavaScript التالية: ()slot.assignedNodes/Elements: يعيد العقد أو العناصر داخل فتحة. node.assignedSlot: وهي الخاصية المعاكسة، وتعيد الفتحة من العقدة التي تشغلها. إذا أردنا معرفة ما نُظهره، فيمكننا تعقب محتوى الفتحة باستخدام: الحدث slotchange: ويقع في المرة الأولى التي تُملأ بها الفتحة، أو أثناء عمليات الإضافة أو الإزالة أو الاستبدال لعنصر مُدرج في الفتحة وليس لأبنائه، وستكون الفتحة event.target. MutationObserver للغوص عميقًا في محتوى الفتحة ومعرفة التغيرات التي حدثت ضمنها. بعد أن تعرفنا على كيفية إظهار عناصر من شجرة DOM الظاهرة، سنتعرف على كيفية تنسيقها بالشكل الصحيح، وتبقى القاعدة الأساسية هي أن عناصر شجرة DOM الخفية تُنسّق من الداخل، والظاهرة من الخارج -مع بعض الاستثناءات-، وهذا ما سنتعرف عليه في المقال القادم. يمكن أن تحوي شجرة DOM الخفية تنسيقات مثل <style> أو <"link rel="stylesheet>، وتؤثر التنسيقات المحلية في: الشجرة الخفية. المضيف الخفي عندما يستعمل عائلة المحددات host:. العناصر المركبة القادمة من شجرة DOM الظاهرة، حيث يسمح المحدد (slotted(selector:: باختيار العناصر المركبة ذاتها وليس أبنائها. بينما تؤثر تنسيقات المستند الرئيسي في: المضيف الخفي، إذا كان حيًّا في المستند الخارجي. العناصر المركبة ومحتوياتها، إذا كانت أيضًا في المستند الخارجي. سيُطبق تنسيق المستند الخارجي عندما يحدث تضارب في تنسيق العنصر، إلا عند استخدام الخاصية مع عنوان important!، حيث يُطبق التنسيق المحلي. تخترق خصائص CSS المخصصة شجرة DOM الخفية، وتستخدم مثل خطافات hooks لتنسيق المكوّن: يستخدم المكوِّن خصائص CSS المخصصة لتنسيق العناصر المفتاحية، مثل (<var(--component-name-title, <default value. ينشر مؤلف المكون هذه الخصائص للمطورين، وهي بأهمية توابع المكوّنات العامة. عندما لا يرغب المطور في قضاء وقت طويل على التنسيق، فسيسند الخاصية --component-name-title إلى المضيف الخفي أو الخاصية التي أشرنا إليها في الأعلى. الربح طبعًا! تتجاوز الأحداث حدود شجرة DOM الخفية إذا كانت قيمة الراية composedهي true، والتي تكون كذلك لمعظم الأحداث المضمنة Built-in، كما هو محدد في التوصيفات: أحداث واجهة المستخدم. أحداث اللمس. أحداث التأشير. من الأحداث المضمنة التي يكون فيها composed: false: mouseenter وmouseleave: لا تنساب للخارج إطلاقًا. load وunload وabort وerror. select. slotchange. يمكن التقاط هذه الأحداث من قِبل العناصر داخل شجرة DOM نفسها. إذا أردنا إيفاد أحداث مخصصة CustomEvent فلا بد من ضبط الخاصية composed على القيمة "true" بشكل صريح. المراجع مراجع DOM. مراجع التوافقية Compatibility ذكرت الشجرة الخفية في الكثير من المواصفات مثل DOM Parsing. ترجمة -وبتصرف- للفصول Shadow DOM and events و Shadow DOM slots, Composition وShadow DOM وShadow DOM Style من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا إنشاء الحركات عبر جافاسكربت التنقل في شجرة DOM عبر جافاسكربت تعديل الخاصيات والأصناف والأنماط في شجرة DOM إجراء تعديلات على DOM طريقة الوصول للعناصر في DOM
- 1 تعليق
-
- dom
- shadow dom
-
(و 1 أكثر)
موسوم في:
-
من لا يحب البشرة النضرة؟ صحيح أن قلةً من الناس فقط من يتمتع ببشرة طبيعية متألقة وصافية، لكن ما يريده الجميع هو بشرة خالية من العيوب، فالكثير منا ليس محظوظًا كفايةً للحصول على بشرة كهذه. يحاول البعض استخدام مساحيق التجميل إلا أن آخرين يفضلون استخدام الفوتوشوب لتنقية صورهم والحصول على بشرة خالية من العيوب. يُعَد برنامج الفوتوشوب من أدوبي أو Adobe Photoshop، أداةً قويةً لمساعدتك في تصفية بشرتك وإضافة لمسات ختامية لتبدو طبيعية المظهر، إلى درجة أنك قد لا تدرك أنّ الصور قد عُولجت باستخدام الفوتوشوب. تركز هذه المقالة على مجموعة من الحيل والنصائح لتنعيم البشرة وتنقيتها من خلال برنامج أدوبي فوتوشوب. ولنلق نظرةً إذًا على طريقة إنجاز الأمر. 1. أنشئ نسخة عن الصورة الأصلية سترى الصورة عندما تدرجها في البرنامج على طبقة الخلفية الموجودة ضمن لوحة الطبقات Layers panel. أزل بدايةً العيوب التي تراها قبل أن تنتقل إلى مرحلة تنعيم البشرة. انسخ طبقة الخلفية، وذلك بالضغط على المفتاحين ALT+WIN في لوحة المفاتيح، ثم النقر على طبقة الخلفية وسحبها إلى رمز الطبقة الجديدة New Layer الموجود في الناحية السفلية من لوحة الطبقات. سمِّ النسخة الجديدة Spot Healing ضمن مربع النص الذي يُدعى "مضاعفة الطبقة Duplicate Layer". ستظهر نسخة جديدة من الصورة باسم Spot Healing أعلى الصورة الأصلية، وسننجز عملية التنظيف الضرورية للبشرة ضمن طبقة منفصلة. 2. استخدم فرشاة معالجة البقع انتقل إلى شريط الأدوات وانقر على فرشاة معالجة البقع Spot Healing brush لنبدأ العمل. تُستخدم هذه الأداة لإزالة اللطخات والعيوب الموجودة على البشرة، وتشبه في عملها فرشاة المعالجة healing brush، حيث تلوّن المنطقة المختارة من الوجه بعينة منتقاة من البكسلات Pixels، بحيث تتلاءم مع البشرة المحيطة، وتمنح الأداة إمكانية تفتيح البشرة وجعلها شفافة، إلى جانب إمكانية إضافة الظلال التي تتمتع بها بكسلات العينة إلى بكسلات البقعة التي تحتاج إلى معالجة، لكن هناك فرق بين فرشاة المعالجة وفرشاة معالجة البقع، إذ لا تحتاج الأخيرة إلى نقطة لأخذ عينة من البكسلات، فهي تنتقي البكسلات تلقائيًا من المنطقة النقية على بشرة الوجه. 3. استخدم فرشاة معالجة البقع مع خيار الانتباه للمحتوى Content-Aware تتوفر ثلاثة خيارات مع فرشاة معالجة البقع: التطابق التقريبي Proximity match: تأخذ الأداة وفق هذا الخيار البكسلات حول محيط المنطقة المحددة لانتقاء البكسلات التي ستستخدم للترميم. إنشاء نسيج Create texture: حيث تأخذ الأداة بكسلات من المنطقة المحددة لتنشئ نسيجًا ترميميًا، فإن لم تنجح المحاولة، جرِّب أن تختار نفس المنطقة مجددًا. الانتباه إلى المحتوى Content-Aware: يوازن الخيار محتوى المنطقة المجاورة ليملأ المنطقة المحددة بما يطابق للمحتوى المدروس، ويبقي على التفاصيل الرئيسية مثل الظلال وحواف الكائنات. عليك هنا اختيار Content-Aware من صندوق الخيارات، وهو الخيار الافتراضي، لكن عليك التأكد من اختياره. 4. أزل اللطخات عن البشرة بالنقر عليها بعد ضبط جميع معاملات parameters فرشاة معالجة البقع، ابدأ بالنقر على اللطخات غير المرغوبة لإزالتها عن البشرة. يستبدل فوتوشوب المنطق المعيبة من البشرة بنسيج مناسب مستفيدًا من نسيج البشرة في المنطقة المحيطة. لتسهيل الأمر عليك، تأكد من زيادة حجم الفرشاة قليلًا عما هو عليه عادة، لكي تزداد تغطية الفرشاة للبقعة، وبالتالي ستزداد فعاليتها. استخدم مفتاح القوس المربع اليميني "[" لتكبير الفرشاة، والقوس اليساري"]" لتصغير حجمها. إن لاحظت بقاء البقعة جزئيًا، تراجع عما فعلته (تراجع عن النقر)، وذلك بالضغط على المفتاحين Ctrl+Z في لوحة المفاتيح، ثم أعد ضبط حجم الفرشاة لاستهداف ما تبقى من البقعة. الصورة المعروضة هي صورة لامرأة تعاني عيوبًا محددةً في بشرتها. لاحظ البثور الكثيرة في بشرة الجبين، هنا يمكنك وضع فرشاة معالجة البقع فوق الاندفاع الجلدي، ثم تكبيرها وإزالة العيوب بالنقر عليها. عند النقر، سيميز فوتوشوب المشكلة وسيجد النسيج الأفضل لترميم الجلد في هذه البقعة اعتمادًا على المناطق المجاورة لها، حيث يخلط بكسلات البقعة ببكسلات النسيج الذي شكّله وستختفي البقعة تمامًا. سنتابع العمل بنفس الطريقة لإزالة بقية العيوب (الاندفاعات الجلدية) على الجبين محافظين على حجم فرشاة معالجة البقع لاستهداف العيوب بدقة. ستختفي كل البقع والعيوب بالنقر هنا وهناك، وستكون النتيجة بشرةً أكثر نعومةً ولمعانًا دون أية عيوب. ألقي نظرةً على النتيجة في هذه الصورة: انتبه عندما تعمل على تنقية البشرة ألا تقترب من الشامات أو أية ندبات دائمة. لاتحاول إزالة الشامات أو الندبات، فهي تمثل المعالم الأساسية لوجه الإنسان وبالطبع من غير المنطقي أن تجعل شخصًا يبدو كشخص آخر عندما تعالج مشاكل البشرة في صورته. تابع العمل وفق التوجيهات السابقة على بقية مناطق الوجه التي تعاني من بقع ومشاكل في البشرة حتى تزيلها كلها، وألقي نظرةً على صورة الوجه قبل وبعد إزالة البقع: تُظهر الصورة اليمينة بشرة الوجه الأصلية مع جميع عيوبها، بينما تظهر اليسارية البشرة بعد إزالة العيوب. لا تستغرق غالبًا تنقية البشرة في الصور الكثير من الوقت. 5. أنشئ نسخة جديدة عن الطبقة التي عنونتها بـ Spot Healing سنحاول الآن تنعيم البشرة بعد إزالة جميع البقع من الصورة، لذلك علينا إنشاء طبقة جديدة مختلفة. سننسخ الطبقة Spot Healing داخل لوحة الطبقات بالضغط على المفتاح ALT في لوحة المفاتيح، ثم النقر على الطبقة Spot Healing وسحبها إلى أيقونة "طبقة جديدة New Layer" أسفل لوحة الطبقات. امنح الطبقة الاسم "Smoothen skin"، ثم انقر على الزر "OK". لدينا الآن الصورة الأصلية في الخلفية تعلوها طبقة إزالة العيوب spot healing، ومن ثم تأتي طبقة Smoothen skin في الأعلى. 6. استخدم مرشح التمرير العالي على الصورة يمكنك استخدام مرشح التمرير العالي High Pass filter لتنعيم مناطق البشرة التي أزلنا عيوبها، وذلك لكي تبدو صحيةً وأكثر إشراقًا. انقر على قائمة "مرشح Filter"، ثم انتقل إلى الخيار "High Pass": يُستخدم مرشح التمرير العالي عادةً في فوتوشوب لتوضيح الصور sharpening إلى حد معين، لكننا سنستخدم المرشح هنا لتنعيم الصورة وليس لتوضيحها أكثر، وستبقى خطوات العمل نفسها في الحالتين. يميّز المرشح الحواف بين المناطق التي تعاني تبدلًا فجائيًا في الإضاءة، أو في مستوى ألوان البكسلات المتجاورة. وفي حالة صور البورتريه (صور الوجه)، سيكون الوجه قريبًا، كما ستكون الحواف عادةً حول الشعر والعينين والفم. لا يحتوي نسيج البشرة عمومًا على الكثير من التفاصيل، ويعطي تدرجًا لونيًا ناعمًا بين مناطقه المختلفة التي لا تُعَد في هذه الحالة حوافًا. لن يميّز المرشح هذه المناطق بل سيملؤها فقط ببكسلات تحمل اللون الرمادي الحيادي neutral grey. لو كنا قد استخدمنا المرشح لتوضيح الصورة، لكان مرشح التمرير العالي قادرًا على توضيح الحواف دون التأثير على نسيج البشرة، لذلك سنستخدم المرشح بطريقة عكسية لتنعيم الصورة بتطبيقه على كامل المناطق اللونية ما عدا الحواف. لاحظ كيف ننجز ذلك: قيمة نصف القطر يُعدّ مرشح التمرير العالي الأداة هو الأفضل لتمييز حواف الكائن وتصحيحها. ستجد تحت مربع الحوار "تمرير عال High Pass" الخيار "نصف القطر Radius"، الذي يتحكم بعرض تمييز الحواف. فعندما يحدد فوتوشوب الحافة، ستعطينا قيمة نصف القطر عدد البكسلات الموزعة على طرفي منطقة الحافة. وإن كانت قيمة نصف القطر صغيرة، فسيميز المرشح تفاصيلًا أدق حول الحافة. وطالما أننا لن نحتاج إلى تنعيم هذه الحواف التي تمثل الأجزاء الأساسية في الصورة بل المناطق المجاورة لها، فلا بدّ من زيادة قيمة نصف القطر. وبالنسبة لصور الوجه، ستجد أن قيمة 24 بيكسل هي الأفضل. إن كان الجزء المعالج من الصورة بعيدًا للخلف أو كانت دقة الصورة منخفضة، فينبغي تقليل نصف القطر إلى حوالي 15 بيكسل. من المفترض أن تقبل قيمة نصف القطر القسمة على 3 مثل 24 أو 15 وسنرى فائدة ذلك لاحقًا. انقر الآن على الزر "Ok" لإغلاق مربع حوار "تمرير عال High Pass"، وسترى أنّ الصورة قد تحولت إلى اللون الرمادي. تبدو المناطق الواسعة التي لا تمتلك حوافًا باللون الرمادي، وهي مناطق قليلة التفاصيل، لكن المناطق التي تمتلك هالات لونية عالية التباين هي من تساهم في تمييز الحواف. 7. استفد من مرشح التغشية منتظم التوزع لا بدّ الآن من تغشية أثر فلتر الترشيح العالي، فعلى غير المتوقع، ستعطي تغشية المناطق التي نًعّمت باستخدام فلتر الترشيح العالي مظهرًا رائعًا للبشرة، لذلك افتح القائمة "ترشيح Filter"، وانتقل إلى "تغشية Blur"، ثم اخترGaussian Blur. اضبط قيمة نصف القطر Radius value في مربع الحوار الذي سيظهر لتكون ثلث القيمة السابقة في مرشح التمرير العالي. وطالما أننا ضبطنا تلك القيمة على 24 بيكسل، فستكون الأخيرة 8 بيكسل. انقر بعد ذلك على "Ok". سيبدو أثر مرشح التمرير العالي على النحو التالي بعد التغشية: 8. استخدم نمط المزج الخطي على طبقة التنعيم غيِّر قيمة الخيار "نمط المزج Blend Mode" للطبقة Smooth Skin في لوحة الطبقات من عادي normal إلى خطي Linear. سيدمج هذا الأمر نتيجة الترشيح العالي مع الصورة مكوّنًا بصورة عالية التباين مع تأثيرات أعلى وضوحًا. 9. لابد الآن من تغيير ألوان الصورة من قائمة "صورة Image"، انتقل إلى ضبط Adjustments، ثم اختر "تغيير Invert". ستبدو الصورة في حالة فوضى عارمة بعد تطبيق هذا الأمر وأكثر غشاوةً عما كانت عليه سابقًا. 10. انتقل إلى خيارات المزج Blending Options لتخفيف أثر الهالة، انتقل إلى خيار تنسيق الطبقات Layer styles الموجود أسفل لوحة الطبقات، ثم اختر "خيارات المزج Blending Options". 11. عدل إعدادات خيار المزج Blend If افتح مربع الحوار "تنسيق الطبقات Layer styles"، وابحث عن خيار المزج Blend If في النصف السفلي من الصندوق. ستجد مجموعتين من الخيارات هي: "هذه الطبقة This Layer"، و"الطبقة الأدنى Underlying Layer". استعمل الخيار العلوي. انظر إلى خيار المزج الواقع تحت نهايات كل أشرطة التدرج gradient bars. سنستخدم هذا الخيار في التحكم في مقدار التمازج بين الطبقة Smooth Skin والصورة التي تحتها. تجري العملية اعتمادًا على مستويات سطوع الطبقة brightness. إذ يمزج الخيار اليساري المناطق الأكثر قتامةً، بينما يمزج الخيار اليميني المناطق الفاتحة. العمل على الهالات الفاتحة ابدأ العمل على الهالات الفاتحة أكثر. اضغط الزر Alt في لوحة المفاتيح، ثم انقر على خيار المزلقة اليمينية واسحبها نحو اليسار. لاحظ أن المزلقة مقسومة إلى نصفين، وسيتحرك فقط الجزء اليساري ولن يؤثر اطلاقًا على الجزء اليميني من المزلقة. ستلاحظ اختفاء الهالات عن الصورة. اسحب المزلقة إلى أقصى يسار النصف الذي تتحرك عليه لتخفيف الهالات قدر الإمكان. تخفيف الهالات الأكثر قتامة اضغط الزر Alt في لوحة المفاتيح، ثم انقر المزلقة اليسارية واسحبها نحو اليمين قدر الإمكان لإزالة الهالات الأكثر قتامةً. انقر على الزر "OK" عندما تنتهي ولاحظ نتيجة عملك. 12. إضافة قناع على الطبقة للحد من التأثيرات التي تظهر في الصورة السابقة، لا بدّ من إضافة طبقة قناع layer mask جديدة. افتح لوحة الطبقات واضغط باستمرار على الزر Alt، ثم انقر على الأيقونة "إضافة طبقة قناع Add Layer Mask". ستظهر عند ذلك صورة مصغّرة عن طبقة القناع مملوءة بالأسود فوق الطبقة Smooth Skin. 13. اختر أداة الفرشاة اختر الفرشاة من شريط الأدوات. 14. اختر اللون الأبيض للفرشاة تأكد من أن لون الفرشاة أبيض. يمكنك البحث في عينات الألوان الخاصة بك للتحقق من اللون الأمامي ولون الخلفية. 15. لون فوق البشرة تحقق من خيارات الفرشاة المتاحة ضمن شريط الخيارات Options bar قبل أن تبدأ التلوين، ثم تحقق من أنّ النمط قد ضبط على الخيار عادي Normal، وأنّ العتامة Opacity قد ضبطت على القيمة 100%؛ أما الخيار انسياب Flow، فعلى 100%. بعد ذلك استخدم الفرشاة للتلوين على البشرة، وذلك لكي يظهر أثر التنعيم. انتق الفرشاة ناعمة الحواف soft-edged لتبدو تأثيرات التدرج أكثر نعومةً. انظر إلى نتيجة عملك بالكامل عند انتهاء العمل. خلاصة تقود هذه المقالة المبتدئين لتنعيم وتنقية البشرة باستخدام برنامج أدوبي فوتوشوب، ويمكنك بالطبع الاستفادة من جميع الملاحظات والحيل التي نفذناها في المقال لتنقية صورة بنجاح. ترجمة -وبتصرف- للمقالة 15 Tips to Get Skin Smoothing and Retouching Right in Photoshop لكاتبها Harsh Raval. اقرأ أيضًا كيفية تصميم تأثير الجلد على النصوص في الفوتوشوب كيفية قص الشعر من الخلفيات المعقدة في الفوتوشوب كيفية تطبيق تأثير الإضاءات المختلفة على الصور الشخصية في الفوتوشوب
-
ستتعلم في هذا المقل كيفية استثمار نظام التشغيل راسبيان Raspbian، حيث يمتلك راسبيري باي القدرة على تشغيل مجالٍ واسعٍ من البرمجيات متضمنةً عددًا من أنظمة التشغيل المختلفة، التي يعتمد عليه الجهاز أساسًا ليعمل. يُعدّ راسبيان نظام التشغيل الرسمي لمؤسسة راسبيري باي، وأكثر أنظمة التشغيل المستخدمة مع باي شعبية، حيث بُني راسبيان اعتمادًا على نظام التشغيل ديبيان لينكس Debian Linux، وقد صُمم خصيصًا ليناسب جهاز راسبيري باي، ويأتي مع حزمةٍ من البرمجيات والإضافات المثبتة مسبقًا والجاهزة للعمل. إذا استخدمت سابقًا أنظمة تشغيل مثل مايكروسوفت ويندوز، أو آبل ماك، أو إس؛ فالأمر مشابهٌ على راسبيان، حيث يعتمد على مبادئ النافذة والأيقونة والقائمة ومؤشر الفأرة نفسها، وستألف بيئة العمل بسرعة. سيحضِّرك هذا المقال للانطلاق في العمل ويعرّفك على بعض البرمجيات المجمّعة خصيصًا. معالج الإعداد الترحيبي Welcome Wizard سترى هذا المعالج عند ظهور سطح مكتب راسبيان للمرة الأولى، وهو أداةٌ مفيدةٌ تساعدك على تغيير بعض إعدادات النظام من أجل تهيئته بما يناسب طبيعة ومكان استخدامك الراسبيري باي. شكل 1-3 معالج الإعداد الترحيبي انقر على زر التالي Next، ثم اختر بلدك ولغتك ونطاقك الزمني Time zone بالنقر على القائمة المنسدلة الموافقة لكل خيار، كما هو موضحٌ في الشكل التالي. إذا كنت تستخدم لوحة مفاتيح عربية، فانقر على صندوق التحقق المجاور حتى تتأكد من أن راسبيان سيستخدم مخطط لوحة المفاتيح العربية الصحيح؛ وإذا أردت استخدام اللغة الإنكليزية أيًا كانت لغتك الأم، فانقر على صندوق التحقق Use English language لتفعيله، ثم انقر زر التالي Next عندما تنتهي. شكل 2-3 اختيار اللغة ستطلب منك الشاشة الثانية للمعالج تغيير كلمة السر الخاصة بالمستخدم الافتراضي pi، حيث تكون الكلمة الافتراضية raspberry، ومن المهم جدًا أن تختار كلمة سر جديدة لأسبابٍ أمنية. أدخل الكلمة في كلا الصندوقين (كلمة السر وتأكيدها) كما هو موضحٌ في الشكل التالي، وبالنقر على مربع التحقق بجوار إخفاء المحارف Hide characters ستكون قادرًا على رؤية الكلمة التي تكتبها، كما يجب أن تكون كلمة المرور متطابقةً في الصندوقين. شكل 3-3 إعداد كلمة سر جديدة تسمح لك الشاشة التالية المُوضحة بالشكل التالي باختيار الشبكة اللاسلكية التي ستتصل بها من خلال قائمةٍ من الشبكات المتوفرة، حيث يمكنك التَنقُّل عبر عناصر القائمة باستخدام الفأرة أو لوحة المفاتيح حتى تجد اسم شبكتك، بعد ذلك انقر عليها وانقر على زر التالي Next. سيُطلب منك كتابة كلمة مرور الشبكة اللاسلكية إذا كانت محميّة Secure، حيث من الأفضل أن تكون محمية، وتُعرف هذه الكلمة أيضًا بـ pre-shared key، وستجد هذا المفتاح عادةً على علبة الموجّه أو أسفل الموجِّه نفسه. انقر على زر التالي Next للاتصال بالشبكة، وإذا كنت لا تريد الاتصال بشبكة لاسلكية، فانقر زر تخطي Skip. شكل 4-3 اختيار الشبكة اللاسلكية تتحقق الشاشة التالية المُوضحة بالشكل التالي من وجود تحديثات updates لراسبيان وبقية البرمجيات على باي، وتتيح لك تثبيتها، حيث يُحدّث راسبيان بانتظامٍ لإصلاح الثغرات وإضافة ميزاتٍ جديدة وتحسين الأداء. انقر زر التالي Next لتثبيت التحديثات المتوفرة أو تخطي Skip لتجاوز العملية. قد يستغرق تنزيل التحديثات بعض الوقت، وستظهر نافذةٌ تخبرك أن النظام مُحدّث عند انتهاء عملية التثبيت، انقر بعدها على الزر OK. شكل 5-3 التحقق من وجود تحديثات ستسألك الشاشة الأخيرة من المعالج والموضحة في الشكل التالي عما إذا كنت تريد إعادة إقلاع النظام rebooting، حيث لن تُفعّل بعض التغييرات دون إعادة تشغيل راسبيري باي. وإذا طُلب منك ذلك، فانقر على زر إعادة الإقلاع Reboot، وسيعيد باي تشغيل نفسه، لكن معالج الإعداد الترحيبي سيختفي هذه المرة ويصبح باي جاهزًا للعمل. شكل 6-3 إعادة إقلاع راسبيان التعرف على سطح المكتب تُعرف نسخة راسبيان المُثبَّتة على معظم أجهزة راسبيري باي على أنها راسبيان مع سطح مكتب راسبيري باي، وذلك للدلالة على واجهة المستخدم الرسومية الرئيسية. تحتل مساحة سطح المكتب صورةً تُدعى خلفية سطح المكتب Wallpaper؛ حيث تدل العلامة A في الشكل التالي عليها، وستظهر فوقها البرامج التي تشغلها. ستشاهد في أعلى سطح المكتب شريط المهام Taskbar عند العلامة B، والذي يمكِّنك من تحميل أي برنامجٍ من البرامج التي وضعتها ضمنه، ويُشار إلى البرامج التي جرى تشغيلها بالمهمَّات tasks من مكان العلامة C في هذا الشريط. سطح مكتب راسبيان سنوضّح الآن أقسام سطح المكتب التي تشير إليها العلامات في الشكل السابق، وهي: A: خلفية سطح المكتب. B: شريط المهام. C: مهمّة. D: لوحة النظام system tray. E: إزالة الوسائط. F: أيقونة بلوتوث. G: أيقونة شبكة الاتصال. H: أيقونة حجم الصوت. I: الساعة. J: شريط تشغيل البرامج Launcher. K: أيقونة القائمة أو أيقونة راسبيري. L: أيقونة سلة المحذوفات. M: أيقونة السوّاقات Drivers القابلة للإزالة. N: شريط عنوان النافذة. O: تصغير النافذة. P: تكبير النافذة. Q: إغلاق النافذة. تقع لوحة النظام D على يسار شريط القائمة menu bar ويظهر فيها رمزٌ لإزالة أي وسيط تخزينٍ قابلٍ للإزالة، مثل سواقة تخزين USB إذا وصلتها بالجهاز. سيسمح لك النقر على هذا الرمز بإزالة وإخراج الوسيط بأمان، وستجد في أقصى يسار الشريط الساعة I، وستشاهد بالنقر عليها تقويمًا رقميًا موضحًا في الشكل التالي. شكل 8-3 تقويم رقمي تجد إلى جانب الساعة أيقونةً على شكل مكبر صوت H، يمكنك النقر عليها بزر الفأرة الأيسر لضبط حجم الصوت، وبالزر الأيمن لاختيار المخرج الصوتي الذي يستخدمه باي. كما ستجد أيقونة شبكة الاتصال G إلى جانب مكبر الصوت، حيث ستظهر قوة الإشارة على شكل أشرطةٍ متتابعةٍ إذا كنت متصلًا بشبكةٍ لاسلكية، وسترى فقط سهمين عموديين متعاكسين إذا كنت متصلًا بشبكةٍ سلكيةٍ محلية. انقر على أيقونة شبكة الاتصال لعرض قائمة الشبكات اللاسلكية المتاحة في الجوار، وعلى أيقونة البلوتوث F للاتصال بأجهزة البلوتوث المجاورة لراسبيري، كما هو موضح في الشكل التالي. شكل 9-3 قائمة الشبكات اللاسلكية المتاحة يتوضع شريط تشغيل البرامج J على يمين شريط القائمة، ويضم أيقونات البرامج التي ثبتَّها عند تثبيت راسبيان، وقد لا ترى جميع أيقونات البرامج على شريط التشغيل، حيث يتوضع بعضها ضمن قائمة راسبيري الموجودة أقصى يسار شريط القائمة، ويمكنك فتح القائمة بمجرد النقر على أيقونتها. شكل 10-3 قائمة راسبيان تُقسم البرامج الموجودة في قائمة راسبيري إلى فئاتٍ تدل أسماؤها على طبيعة البرامج ضمنها، حيث تضم فئة البرمجة Programming مثلًا البرامج المصممة لمساعدتك في كتابة برامجك الخاصة كما سترى في مقال لاحق حول البرمجة باستخدام سكراتش Scratch وما يليه؛ بينما ستسلّيك الألعاب Games لبعض الوقت. لن نستعرض بالطبع جميع البرامج في هذا الدليل، لذلك جرّب أن تتعلم بنفسك ما تريده منها. متصفح الويب كروميوم Chromium سنتمرن الآن على استخدام باي بتشغيل متصفح كروميوم على النحو التالي: انقر على أيقونة راسبيري في الزاوية اليسارية العليا لسطح المكتب لتظهر لك قائمة البرامج، ثم انتقل بعدها إلى فئة الإنترنت وانقر على أيقونة متصفح كروميوم، كما هو مبين في الشكل التالي. شكل 11-3 متصفح كروميوم سيبدو لك كروميوم مألوفًا إذا استخدمت سابقًا متصفح الويب جوجل كروم Google Chrome على حاسوبٍ آخر، حيث يتيح لك كروميوم مثل أي متصفح ويب زيارة المواقع المختلفة ومشاهدة مقاطع الفيديو والتمتع بالألعاب والتواصل مع أشخاص آخرين حول العالم من خلال المنتديات المختلفة أو مواقع المحادثة المباشرة. ابدأ باستخدام كروميوم من خلال تكبير نافذته إلى الحد الأقصى لتملأ نافذة سطح المكتب؛ حيث ستجد ثلاث أيقوناتٍ متجاورةٍ في أقصى يسار شريط عنوان نافذة كروميوم N. انقر على الأيقونة الموجودة في الوسط والتي تحمل رمز سهمٍ متّجِهٍ للأعلى P، وهي أيقونة تكبير النافذة لتشغل مساحة سطح المكتب؛ أما الأيقونة التي تأتي على يسارها فهي تصغير النافذة O، والتي تُخفي النافذة حتى تنقر عليها في شريط المهام أعلى سطح المكتب. وتحمل بالطبع أيقونة الإغلاق Q رمز الضرب إلى يمين أيقونة التكبير. إذا أردت تصفح الإنترنت فما عليك سوى النقر على شريط العناوين Address bar أعلى المتصفح؛ وهو الشريط الأبيض الطويل الذي ينتهي برمز عدسة البحث، ومن ثم كتابة العنوان الذي تريد التوجه إليه، مثل www.raspberrypi.org، ثم اضغط على زر Enter وسيحمّل كروميوم الموقع المطلوب كما هو موضح في الشكل التالي. تستطيع البحث عما تريده أيضًا بدلًا من كتابة عنوان محدد، كأن تكتب راسبيري باي، أو راسبيان، أو أية عباراتٍ أخرى، وسيبحث كروميوم عن محتوىً يضم هذه العبارات. شكل 12-3 تحميل موقع ويب راسبيري باي عبر كروميوم قد تظهر لك عدة نوافذٍ فرعية tabs ضمن نافذة كروميوم عند تشغيله للمرة الأولى، حيث يمكنك التنقل بينها بمجرد النقر على النافذة، كما يمكنك إغلاق نافذة فرعية محددة بالنقر على رمز الضرب الموجود على الطرف الأيمن للنافذة. لفتح نافذة فرعية جديدة، انقر على زر النافذة الفرعية الموجود إلى يمين آخر نافذة فرعية مفتوحة، أو اضغط معًا زري لوحة المفاتيح CTRL وT. تُعَد النوافذ الفرعية وسيلةً جيدةً لتصفح عدة مواقع ويب معًا دون ازدحام نوافذ كروميوم على الشاشة. انقر على زر الإغلاق عند الإنتهاء من العمل على كروميوم. مدير الملفات File Manager تُخزّن جميع الملفات في المجلد الرئيسي Home، سواءٌ كانت قصيدةً كتبتها، أو برنامجًا أو مقطع فيديو أعددته، أو صورةً نزّلتها من موقع ويب. وللوصول إلى هذا المجلد، انقر على زر القائمة، ثم انتقل إلى الملحقات Accessories، وبعدها انقر على مدير الملفات File Manager. شكل 13-3 برنامج مدير الملفات يتيح لك مدير الملفات إمكانية تصفّح الملفات والمجلدات الموجودة على بطاقة الذاكرة الأساسية microSD، أو أية وسائط تخزينٍ قابلةٍ للإزالة، مثل سواقات التخزين المحمولة بواجهة USB، حيث ينقلك البرنامج عند تشغيله إلى المجلد الرئيسي مباشرةً، ويضم هذا المجلد بدوره مجلداتٍ فرعيةٍ subdirectories أخرى مرتبةٍ في فئات كما هو حال برامج قائمة راسبيري، وأهمها: سطح المكتب Desktop الذي يضم ما تراه من ملفات على سطح المكتب، فإذا خزّنت ملفًا ضمن هذا المجلد سيظهر على سطح المكتب، وبالتالي يسهّل عليك إيجاده وفتحه. المستندات Documents: وهو المجلد الرئيسي لمعظم الملفات التي تُنشئها. التنزيلات Downloads: تُخزَّن ضمنه الملفات التي نزّلتها من الإنترنت عبر المتصفح كروميوم تلقائيًا. MagPi: يحتوي على نسخةٍ إلكترونية من مجلة MagPi؛ وهي المجلة الرسمية لمؤسسة راسبيري باي. الموسيقى Music: يضم ملفات موسيقى كنت قد أنشأتها أو خزنتها على راسبيري باي. الصور Pictures: وهو مجلدٌ فرعيٌ مخصصٌ لتخزين الصور. عام Public: طالما أن معظم الملفات والمجلدات خاصة، فما تضعه في هذا المجلد يصبح عامًا؛ أي يمكن لأي مستخدمٍ الوصول إليه حتى لو كان له معلومات استيثاق خاصة، مثل اسم المستخدم وكلمة المرور. الفيديوهات Videos: وهو مجلدٌ فرعيٌ مخصصٌ لتخزين مقاطع الفيديو، ويمثّل وجهة البحث الأولى لبرامج تشغيل الفيديو. تُقسم نافذة مدير الملفات إلى قسمين: القسم الأيسر الذي يعرض المجلدات الموجودة على راسبيري باي، والقسم الأيمن الذي يعرض الملفات والمجلدات الفرعية الموجودة ضمن المجلد الذي اخترته من مجلدات القسم الأيسر. إذا وصلت أحد وسائط التخزين القابلة للإزالة بأحد منافذ USB، فستظهر لك نافذةً تسألك عما إذا كنت تريد تصفح محتويات وسيط التخزين باستخدام مدير الملفات File Manager، كما هو مبين في الشكل التالي، انقر زر OK لتتمكن من ذلك. شكل 14-3 توصيل وسيط تخزين قابل للإزالة بإمكانك نسخ الملفات من الأجهزة القابلة للإزالة إلى بطاقة الذاكرة microSD أو العكس، وذلك بفتح مجلد بطاقة الذاكرة الخاصة براسبيري باي ومجلد الجهاز القابل للإزالة في نافذتين مختلفتين لمدير الملفات، ثم الضغط المستمر على الملف الذي تريد نسخه بزر الفأرة اليساري وسحبه إلى النافذة الأخرى، ثم إفلاته ضمنها، وتُعرف هذه العملية باسم السحب والإفلات Drag and Drop. هناك طريقةٌ أخرى لنسخ الملفات من خلال النقر مرةً واحدةً على الملف، ثم النقر على قائمة التحرير Edit واختيار نسخ Copy، والانتقال بعد ذلك إلى النافذة الأخرى والنقر على قائمة التحرير Edit، ثم اختيار لصق Paste. يمكنك أيضًا اختيار أمر النقل Move من قائمة التحرير Edit بدلًا من النسخ Copy، لكن الفرق بينهما أن عملية النقل ستحذف الملف من المجلد الأصلي بعد نقله إلى المجلد الجديد. يتيح راسبيان إمكانية تنفيذ العمليتين السابقتين عبر اختصارات لوحة المفاتيح، حيث يمكنك تنفيذ عملية النسخ بالضغط على زري CTRL وC معًا ؛ وتنفيذ النقل أو القص بالضغط على زري CTRL وX معًا؛ أما النسخ، فمن خلال الضغط على زري CTRL وV معًا. الشكل 15-3 سحب وإفلات ملف أغلق نافذة مدير الملفات حين تنتهي من تجربته، وأغلق أية نوافذٍ أخرى قد فتحتها بالنقر على زر الإغلاق في أعلى يمين النافذة، بعد ذلك أخرج وسائط التخزين القابلة للإزالة إذا وصلت أحدها براسبيري باي من خلال النقر على زر الإخراج Eject أعلى يمين الشاشة، ثم سحب الجهاز من منفذ USB. مجموعة البرامج المكتبية ليبر أوفيس LibreOffice وهي وسيلةٌ أخرى لاختبار إمكانيات راسبيري باي. انقر على أيقونة راسبيري، ثم انقل الفأرة إلى أدوات المكتب Office وانقر على محرر النصوص ليبر أوفيس رايتر LibreOffice Writer؛ وهو البرنامج الذي يعالج النصوص ضمن مجموعة ليبر أوفيس المكتبية ذات الشعبية الواسعة والمماثلة لغيرها من المجموعات المكتبية، مثل مايكروسوفت أوفيس Microsoft Office، وجوجل دوكس Google Docs. من الممكن ألا تجد ليبر أوفيس مُثبّتًا افتراضيًا مع بعض نسخ راسبيان، لذلك استخدم أداة برمجيات مفيدة Recommended Software لتثبيته، والتي سنتحدث عنها لاحقًا في هذا المقال. شكل 16-3 محرر نصوص ليبر أوفيس يتيح لك محرر نصوص ليبر أوفيس رايتر إمكانية كتابة المستندات وتنسيقها بطرقٍ مُلفتة، حيث يمكنك تغيير نمط الخط ولونه وقياسه، وإضافة التأثيرات والصور والمخططات البيانية والرسومية والجداول وغيرها من المحتويات، كما يساعدك المحرر على تتبع الأخطاء، حيث يّظلل مباشرةً أخطاء الكتابة باللون الأحمر وأخطاء القواعد بالأخضر أثناء الكتابة. ابدأ تجربتك بكتابة مقطعٍ عما تعلّمته حتى الآن عن راسبيري باي وبرمجياته، واختبر مختلف الأيقونات الموجودة أعلى نافذة المحرر لتكتشف عملها، وحاول أن تزيد حجم الكلمات وأن تغيّر لون النص. إذا لم تكن واثقًا من عمل أية أيقونة، فمرّر مؤشر الفأرة فوقها وسيظهر لك مربع ملاحظات tooltip يخبرك عن عمل الأيقونة. احفظ عملك إذا كنت مقتنعًا به بالنقر على قائمة ملف File، ثم خيار الحفظ Save، واختر اسمًا مناسبًا لملفك، ثم انقر زر حفظ Save. شكل 17-3 حفظ ملف يُعدُّ محرر النصوص ليبر أوفيس رايتر جزءًا من مجموعة ليبر أوفيس المكتبية المتكاملة، وستجد بقية برامج المجموعة في فئة أدوات المكتب Office ضمن قائمة راسبيري إلى جانب ليبر أوفيس رايتر. هذه البرامج هي: ليبر أوفيس بيس LibreOffice Base: قاعدة بياناتٍ لتخزين المعلومات والبحث عنها بسرعة وتحليلها. ليبر أوفيس كالك LibreOffice Calc: جداول Spreadsheet لمعالجة الأرقام وإنشاء المخططات والرسومات. ليبر أوفيس درو LibreOffice Draw: برنامج رسوميات لإنشاء الصور والمخططات. ليبر أوفيس إمبرس LibreOffice Impress: برنامج عروض تقديمية لتصميم شرائح عرض وتقديمها. ليبر أوفيس ماث LibreOffice Math: محرّر صيغٍ رياضية لكتابة معادلات وعبارات رياضية بالشكل المناسب، ثم استخدامها في برامجٍ أخرى. إذا أعجبك العمل على ليبر أوفيس، فيمكنك استخدامه على أجهزة حاسوبٍ أخرى. نزّل البرنامج من موقع ليبر أوفيس libreoffice.org بما يتناسب مع نظام التشغيل لديك سواءٌ كان مايكروسوفت ويندوز، أو توزيعات لينوكس، أو آبل ماك، أو إس وثبته بعد ذلك؛ وإذا أردت التعرف أكثر على ما يقدمه لك ليبر أوفيس، فانظر قسم ليبر أوفيس في أكاديمية حسوب. أداة البرمجيات المفيدة Recommended Software على الرغم من تنوع حزمة البرامج التي تأتي مع راسبيان، فهو قادرٌ على تشغيل الكثير غيرها، حيث ستجد مجموعةً من أفضل البرامج ضمن الأداة Recommended Software، ولا بدّ أن يتصل جهازك بالإنترنت لاستخدامها. انقر على أيقونة راسبيري وانتقل بمؤشر الفأرة إلى تفضيلات Preferences، ومن ثم إلى أداة البرمجيات مفيدة Recommended Software، حيث ستبدأ الأداة بمجرد تشغيلها بالبحث عبر الإنترنت عن البرمجيات المتوفرة، وستعرض بعد ثوانٍ معدودة قائمةً بحزم البرمجيات المتوافقة مع راسبيان كما هو موضحٌ في الشكل التالي، وتكون مرتبةً ضمن فئات تمامًا مثل البرامج في قائمة راسبيري. انقر على أي فئةٍ في القسم اليساري من النافذة لاستعراض البرامج التي تنتمي إليها، أو انقر على كل البرامج All Programs لعرض كل شيء. شكل 18-3 أداة Recommended Software إذا وجدت مربع التحقق بجانب البرنامج مُعلّمًا، فالبرنامج مثبتٌ بالفعل على جهازك؛ وإذا لم يكن كذلك، فيمكنك تعليمه بالنقر عليه حتى يُثبَّت، حيث يمكنك اختيار القدر الذي تشاء من البرمجيات لتثبيتها سويةً، لكن قد لا تتسع بطاقة الذاكرة لها إذا كان حجم البطاقة أقل من المطلوب. وبنفس الطريقة، يمكنك إلغاء تثبيت البرامج من خلال البحث عن البرنامج المثبت مسبقًا، حيث سترى مربع التحقق إلى جواره معلّمًا. انقر عليه لإزالة العلامة، وبالطبع يمكنك عكس العملية إن أخطأت لسبب ما. عندما ترى أنك اخترت ما تريد تثبيته أو إزالته، فانقر على زر OK لتبدأ العملية، كما هو موضحٌ في الشكل التالي، وبعد انتهاء تنزيل البرامج المطلوبة وتثبيتها سيظهر لك صندوق حوار، انقر الزر OK لإغلاق الأداة Recommended Software. شكل 19-3 إلغاء تثبيت برنامج يمكنك استخدام أداة إضافة وإزالة البرامج Add/Remove Software لتثبيت أو إزالة برنامج، وستجدها أيضًا ضمن نفس فئة التفضيلات Preferences في قائمة راسبيان؛ حيث تعرض هذه الأداة مجالًا أوسع من البرامج، لكن معظمها لم يُختبر من قبل مؤسسة راسبيري باي. أداة ضبط راسبيري باي Configuration Tool آخر ما سنتعرف عليه في هذا المقال هي أداة الضبط Raspberry Pi configuration tool التي تشابه معالج الإعداد الترحيبي، حيث تساعدك الأداة في تغيير مجموعةٍ متنوعةٍ من إعدادات راسبيان. انقر على أيقونة راسبيري، وانتقل بمؤشر الفأرة إلى تفضيلات Preferences، ومن ثم إلى أداة ضبط راسبيري باي Raspberry Pi Configuration المُبينة في الشكل الآتي. تنقسم نافذة الأداة إلى أربع نوافذٍ فرعية تتحكم كلٌ منها بمزايا محددة لراسبيان؛ أولى هذه النوافذ هي النظام System، وتظهر عند فتح البرنامج مباشرةً، حيث تمكّنك هذه النافذة من تغيير كلمة سر حسابك على راسبيان وتعيين اسمٍ لجهازك، وهو الاسم الذي يميِّز جهاز راسبيري باي عند الاتصال بشبكاتٍ محلية سلكيةٍ أو لاسلكية، وغيرها من الإعدادات. مع ذلك، لا تحتاج معظم هذه الإعدادات لأي تغيير. تضم النافذة الفرعية الثانية المُسماة واجهات Interfaces فئةً أخرى من الإعدادات التي يظهر معظمها غير مفعّلٍ disabled، حيث لا ينبغي تغيير هذه الإعدادات إلا عند إضافة طرفيةٍ جديدة لراسبيري، مثل تجهيزة الكاميرا وبالطريقة التي يطلبها صانعوا الطرفية فقط، ويُستنثنى من هذه القاعدة الخيارات التالية: الخيار SSH: الذي يفعّل ميزة Secure Shell، حيث تسمح هذه الميزة بالولوج إلى راسبيري باي من حاسوبٍ آخر على الشبكة باستخدام عميل SSH. الخيار VCN: الذي يفعّل ميزة Virtual Network Computer، حيث تسمح هذه الميزة بالتحكم في جهاز راسبيري باي عبر جهاز حاسوبٍ آخر باستخدام عميل VCN. الخيار Remote GPIO: الذي يسمح لك باستخدام أرجل المنصة GPIO لجهاز راسبيري باي من خلال حاسوبٍ آخر على الشبكة، وسنتعرف على ذلك لاحقًا عند تطرقنا للحوسبة الفيزيائية باستخدام بايثون وسكراتش في مقال لاحق. شكل 20-3 أداة ضبط راسبيري باي تساعدك النافذة الفرعية الثالثة "الأداء Performance" على ضبط حجم الذاكرة المخصصة لوحدة معالجة الرسوميات GPU، بالإضافة إلى زيادة أداء بعض نماذج راسبيري باي من خلال العملية المعروفة باسم رفع تردد العمل Overclocking. وكما أشرنا سابقًا، يُفضَّل عدم تغيير شيءٍ من إعدادات هذه الفئة. أما النافذة الأخيرة "إعدادت محلية Localization"؛ فهي مخصصةٌ لإعداداتٍ مثل تغيير الموقع، والذي يتحكم بدوره باللغة التي يستخدمها راسبيان وكيفية عرض الأرقام، بالإضافة إلى تغيير المنطقة الزمنية وتنسيق لوحة المفاتيح وتغيير اسم الدولة لأغراضٍ تتعلق بالشبكات اللاسلكية WiFi. يفضل حاليًا نقر زر Cancel لإغلاق الأداة دون تغيير أي شيء. إيقاف التشغيل بعد أن استُكشف سطح مكتب راسبيان، فقد حان الوقت لتعلّم إيقاف تشغيل راسبيري باي بأمان، حيث يُبقي راسبيري باي مثل غيره من الحواسيب جميع الملفات التي تعمل عليها في الذاكرة المتطايرة من النوع RAM، التي تفقد محتواها عند انقطاع التغذية الكهربائية. وبطبيعة الحال، تنتقل المستندات التي أنشأتها عند حفظها من الذاكرة المتطايرة إلى الذاكرة الدائمة المتمثلة ببطاقة الذاكرة microSD، وهذا كافٍ لتطمئن أنّ كل شيء على ما يرام. لكن ما يحدث أن راسبيان يفتح مجموعةً من الملفات التي يستخدمها أثناء العمل، وإذا فُصلت التغذية الكهربائية لسببٍ أو لآخر، فستبقى هذه الملفات مفتوحةً وقد ينتج عن ذلك خللٌ في نظام التشغيل، كما قد تضطر أيضًا إلى إعادة تثبيته. لهذا لابدّ من وسيلةٍ لإبلاغ راسبيان بإغلاق كل هذه الملفات استعدادًا لفصل التغذية الكهربائية، وهذا ما يُعرف بإيقاف التشغيل. انقر على أيقونة راسبيري في الزاوية اليسارية العليا لسطح المكتب، وانقر على إيقاف التشغيل Shut Down. ستظهر لك شاشةٌ تضم ثلاثة خيارات، هي إيقاف تشغيل Shut Down وإعادة إقلاع Reboot وتسجيل خروج Logout، كما هو موضحٌ بالشكل التالي. لدى اختيارك أمر إيقاف التشغيل Shut Down (وهو الأكثر استخدامًا)، فإنك تخبر راسبيان أن يغلق كافة البرمجيات والملفات المفتوحة ومن ثم يطفئ راسبيري باي. بعد أن تصبح شاشة العرض سوداء، انتظر عدة ثوانٍ حتى يختفي الضوء الأخضر على باي ومن ثم يمكنك فصل التغذية الكهربائية؛ ولإعادة تشغيل باي صل كابل الطاقة الكهربائية مجددًا. شكل 21-3 إيقاف تشغيل راسبيري باي تمر عملية إعادة الإقلاع Reboot بمراحل إيقاف التشغيل نفسها وذلك بإغلاق كل شيء، لكنها ستعيد تشغيل باي بدلًا من قطع التغذية الكهربائية عنه، أي كأنك أوقفت تشغيل الجهاز ثم فصلت كابل الطاقة الكهربائية وأعدت وصله. ستضطر إلى إعادة إقلاع باي عندما تجري تعديلات معينة على نظام التشغيل مثل تثبيت تحديثاتٍ محددة لبرمجياته البنيوية، أو أن يعمل أحد البرامج بصورةٍ خاطئة وتُعرف هذه الحالة بالانهيار crashing، فيؤثر على أداء راسبيان. أخيرًا، ستلمس فائدة تسجيل الخروج Logout إذا كان هناك أكثر من مستخدمٍ لجهاز راسبيري باي، حيث تغلق هذه العملية كل البرامج المفتوحة، وتنتقل بك إلى شاشة تسجيل الدخول Login التي تطلب منك إدخال اسم المستخدم وكلمة المرور؛ وإذا نقرت عن طريق الخطأ على تسجيل الخروج، فيمكنك العودة بسهولةٍ إلى العمل بكتابة pi اسمًا للمستخدم وأية كلمة سر كنت قد اخترتها في معالج الإعداد الترحيبي الذي شرحناه بداية هذا المقال. ترجمة -وبتصرف- للفصل الثالث Using your Raspberry Pi من كتاب The official Raspberry Pi Beginners Guide اقرأ أيضًا المقال السابق: تجميع راسبيري باي والتحضير لاستعماله تعرف على جهاز راسبيري باي Raspberry Pi المدخل الشامل لتعلم علوم الحاسوب أنظمة التشغيل للمبرمجين
-
من غير الممكن لسوء الحظ، إقناع كل زوار موقعك أن يصبحوا عملاءً. لهذا لا بدّ في عالم الأعمال أن تحلل في مرحلة ما سلوك الزوار واكتشاف الأسباب التي تمنعهم من التحوّل إلى عملاء. تحتاج الأعمال بأسلوبها التقليدي أو من خلال الإنترنت، إلى تجربة مستخدم مثالية كي تضمن جذب زوار أكثر ليصبحوا عملاءً، سواءً من خلال شاشات العرض داخل المتاجر، أو من خلال نماذج ويب لكسب العملاء، أو استخدام أسلوب الجذب الذي يُعرف بمخروط المبيعات متعدد المراحل multi-stage sales funnel. وبما أنه من غير غير الممكن إقناع كل الزوار بأن يصبحوا عملاءً، فلا بدّ لأصحاب الأعمال أو خبراء تصميم تجارب العملاء UX، أن يتابعوا البيانات التحليلية في مرحلة ما، وأن يتساءلوا عن الأسباب التي تمنع الزوار من التحوّل إلى عملاء. يُعَد جوجل أنالايتكس Google Analytics أكثر إطارات العمل شعبيةً في مجال تحليل البيانات، إلا أنه قد يُغرق المصممين ببيانات كمية عن سلوك المستخدم وعدد مرات استعرض الصفحات، ومعدل مغادرة الصفحات، ومصادر الحركة نحو الصفحات وغيرها. وعلى الرغم من كم البيانات التي سنحصل عليها من أداة تحليل البيانات هذه، إلا أنك لن تجد تفسيرًا لسلوك الزوار أثناء تصفحهم المواقع. فتسليط الضوء على مشكلة مثل معدل مغادرة الزوار المرتفع لموقع، هو أمر؛ أما اكتشاف الوسيلة لحل هذه المشكلة، فهو أمر آخر. ووفقًا لهذه الرؤية، نستطيع القول أن جوجل أنالايتكس يفتقر إلى أداة جوهرية يحتاجها مصممو تجربة المستخدم، وهي جمع الآراء من مستخدمين حقيقيين. الصفحة الرئيسية لـ Hotjar استخدام أداة Hotjar لفهم سلوك المستخدمين وجمع آرائهم يتيح هوتجار Hotjar للشركات إمكانية فهم ما يراه ويفعله الناس على مواقعهم الإلكترونية، ويساعدهم على فهم سبب استخدام الناس لمواقعهم تحديدًا وكيف يستخدمونها. وبفضل Hotjar ليس من الضروري وضع توقعات حول سلوك الزوار، وذلك لامتلاكه عددًا كبيرًا من الميزات التي ترشد المسوقين ومصممي تجربة المستخدم إلى تفاصيل التجربة التي يخوضها زوار مواقعهم الإلكترونية. الخرائط الحرارية للمواقع الخريطة الحرارية هي لقطة شاشة لصفحة ويب عليها مخطط من طبقات، تتدرج ألوانه من الدافئة إلى الباردة، لتظهر النقاط التي نقرها الزوار أكثر وإلى أي حد وصلوا في تصفحهم، والأماكن التي نقلوا إليها مؤشر الفأرة. وتمامًا مثل خريطة في نشرة جوية، سيكون اللون الأحمر هو الأكثر حرارةً أو الأكثر جذبًا للانتباه، أما الأزرق فيدل على المناطق الباردة، أو التي لا تسترعي انتباه الزوار. تُعَد الخرائط الحرارية أكثر أدوات Hotjar شعبيةً، ومن الممكن استخدامها على أية صفحة وأي جهاز. وتُلخّص سلوكيات المستخدمين كالتالي: خرائط النقر click maps: تُظهر الأماكن التي ينقر عليها الزوار (نقر أو لمس). خرائط التمرير scroll maps: وتُظهر كم المسافة التي تصفحها الزوار نزولًا في صفحة الويب. خرائط التنقل move maps: وتظهر مسارات حركة الفأرة. الخرائط الحرارية التي تولدها Hotjar. المنطقة التي تميل إلى الأحمر هي الأكثر نقرًا وبعكسها تلك التي تميل إلى الأزرق. تساعد الخرائط الحرارية المصممين بما تقدمه من إرشادات مرئية مختصة على إنشاء صفحات ذات تخطيط أفضل تلفت انتباه المستخدم إلى أهم أجزاء الصفحة رافعة نسبة تحول الزوار إلى عملاء. وأكثر التحسينات على التصميم شيوعًا، هو تحسين أو توجيه معدل اتخاذ القرار نحو المنطقة الدافئة، مما يرفع نسبة تحول الزوار إلى عملاء. سجلات جلسات العمل تلخص الخرائط الحرارية في ملف واحد السلوك الإجمالي للزوار ضمن صفحة محددة، بينما تسمح سجلات جلسات العمل لأصحاب الأعمال بالاطلاع على التجارب الفردية، وفهم طريقة تفاعل الأشخاص مع الموقع. أين يتجهون؟ متى يرتبك الزوار في الموقع ومتى يشعرون بالإحباط؟ لماذا لم يجذبهم الموقع ليصبحوا عملاء؟ تُعَد سجلات Hotjar بديلًا ممتازًا لاختبارات فعالية الاستخدام usability tests، مثل أبحاث تجارب المستخدم UX researchers، وتساعد المصممين على التجاوب مع عشرات التجارب التي يختبرها مستخدمون فعليون، وتمنحهم رؤيةً دقيقةً لأسلوب تصفح الزوار لمحتويات صفحات الويب، كما يزوّدهم كل سجل بمسار زمني تفاعلي يعرض نقاط التفاعل الأساسية مع الصفحات. لن تحتاج أكثر من عدة سجلات عادةً للتعرف على أنماط السلوك واكتشاف ما ينفع وما لا ينفع. فلو أخذنا على سبيل المثال حالة النقر المتكرر على عنصر، فسيوحي ذلك بأن الزائر قد انتقل إلى مرحلة اتخاذ قرار الشراء (سواءً فعل ذلك أم لا)، أو أن الرابط لا يعمل كما هو مطلوب. وفي كلتا الحالتين يمكن تسوية الحالة لجعل تجربة المستخدم ككل أكثر فعاليةً ومتعة. من الأفضل دائمًا في حالة السجلات أن تستفيد من خبرات مصممي تجارب المستخدم. وبما أنّ متابعة تجاوب الزوار أمر يستهلك الوقت، فلا بدّ من تحديد أهداف واضحة قبل متابعة السجلات لزيادة فعالية العملية وفائدتها. فقد ترغب مثلًا شركات SaaS في تحسين نموذج الاشتراك المجاني على مواقعها، بينما يرغب مالكو eCommerce في التركيز على عمليات التحقق. إذًا فتحديد هذه الأهداف مسبقًا سيسمح للمصممين بالتركيز على الأجزاء الأكثر إلحاحًا في السجلات دون هدر المزيد من الوقت. مخروط التحول يسعى أصحاب الأعمال إلى تحويل المزيد من الزوار إلى عملاء، فالهدف بالنسبة لهم إذًا تحويل الزوار إلى عملاء من خلال وسيلة الاصطياد المخروطية funnel سواءً للحصول على مشتركين جدد ضمن قوائم الرسائل الإخبارية، أو لشراء المزيد من المنتجات. يتألف المخروط النمطي في منصة eCommerce مثلًا، من خمس مراحل مختلفة على الأقل هي: الصفحة الرئيسية Homepage، ثم صفحة المنتجات Product page، ثم سلة التسوق Cart، ثم التحقق Checkout، بعدها صفحة تأكيد الطلب Confirmation page. وكما هو حال المخروط الفعلي، يضيق مخروط الاصطياد عبر الإنترنت عند النهاية، بمعنى أن عدد العملاء في النهاية أقل من عددهم في البداية، لكن من الصعب التكهن بما يجري في المراحل الضمنية، أو لماذا يُغادر الزوار. مراحل مخروط انتقال الزائر إلى عميل كما يظهره مخروط Hotjar. لاحظ نسبة من يبقى في كل مرحلة. يُعَد مخروط هوتجار ميزةً متقدمةً تسمح لأصحاب الأعمال بمعرفة عدد الزوار الذين اجتازوا مرحلةً معينةً مقابل الذين غادروها. وبفهم متى وكيف يغادر العملاء المحتملين، سيتمكن المصممون من إيجاد وضع مثالي للمخروط وتحسين المسار الكلي للمستخدم ضمن الصفحة. إنّ غياب عملية مطلوبة لاتخاذ قرار (مثل اختيار الحجم قبل إضافة منتج إلى سلة المشتريات) أو وضعها في مكان خاطئ أو نماذج طويلة يجب ملؤها، هي أمثلة عن عوائق لتحول الزائر إلى عميل، وستُحدث ثغرات في مخروط الجذب. ستجد هذه الثغرات في الصفحات التي تعاني من معدل مرتفع لمغادرة الزوار، لذلك سيحسن مصممو تجارب المستخدم من تصاميمهم لسد تلك الثغرات بمجرد تحديدها، مما سيدفع المزيد من الزوار نحو نهاية المخروط وبالتالي زيادة عدد التحولات المحتملة. النماذج ينتهي مخروط التحوّل Conversion funnel غالبًا بنموذج يُملئ ويُرسل، سواءً كان نموذج تسجيل، أو نموذج تحقق، أو نموذج إرسال بيانات. يُعَد تجاهل النماذج أمرًا شائعًا ولكنه لا يُعد أمرًا مهمًا دائمًا في تجربة المستخدم، غير أنه في الوقت ذاته مهم لفهم السبب الذي يدفع مستخدم يبدو وكأنه جاهز للتحول إلى تجاهل النموذج فجأة، فما يغير نيته عادة هي التجارب السلبية. تقرير Hotjar عن سبب مغادرة الزائر قبل إكمال نموذج الاشتراك أو الشراء. لاحظ عدد المغادرين عند كل مرحلة (كتابة الاسم أو البريد الإلكتروني أو اسم الشركة وهكذا..). يغطي تقرير النماذج في Hotjar مجالًا واسعًا من التحليلات، مثل عدد جلسات العمل، وزمن التفاعل الوسطي، ومعدل التحول، وحقول النموذج التي تُركت فارغة. تكون هذه الإضاءات مصحوبةً بممارسات أفضل، مثل إبقاء عدد الحقول في حده الأدنى، وتزويد النموذج بعناوين ومدخلات واضحة، ستساعد أولًا على التخلص من العوامل التي تدفع العملاء إلى المغادرة، وتقود إلى نماذجًا ذات معدلات تحوّل مرتفعة. استطلاعات الرأي من السهل الحصول على تصوّرات خاطئة عما يهتم با الناس أو لماذا يتصرفون على نحوٍ معين. فوضع افتراضات عن سلوك الزوّار هو العدو الأكبر لمالكي الأعمال ومصممي تجربة المستخدم، لأنها تقود إلى أخطاء قاتلة في التصميم والتسويق. ولا تمثّل الآراء الصادقة للعملاء الطريق الأفضل لتلافي تلك الأخطاء وحسب بل هي الطريق الأقصر لاكتشاف كيف وصلوا إلى مكان ما وما الذي يبحثون عنه، ماذا يحتاجون وما الذي يخشونه. تُعدُّ استطلاعات رأي هوتجار Hotjar’s poll أداةً غير مزعجة، تظهر أسفل الصفحة وتعرض أسئلةً للحصول على معلومات متعلقة بالموضوع المطروح. يمكن طرح هذه الأسئلة ضمن صفحة خاصة لتعرف مثلًا نوع المحتوى الذي يبحث عنه الزوار، أو وفقًا لسلوك الزائر، مثل الحالة التي يغادر فيها الصفحة. تظهر فائدة أداة استطلاع الرأي للأعمال المرتبطة بمنصة eCommerce في فهم سبب مغادرة العملاء قبل النقر على تأكيد الطلب Confirm Order في نهاية مخروط المبيعات، فأسئلة مثل "ما هي أكبر مخاوفك من شراء هذا المنتج من موقعنا؟"، أو "ما المعلومات التي تحتاجها لتسهل عليك اتخاذ قرار الشراء؟"، قد تساعدك على استخلاص آراءٍ تتعلق بالحالة المدروسة تمامًا، بما في ذلك نقص المعلومات التي توضح عوامل الأمان عند تنفيذ عملية الدفع وتكاليف الشحن، ووقت التسليم، وتفاصيل الكفالة أو سياسة إعادة المنتج الذي اشتريته. يتيح هوتجار أيضًا أداةً قابلةً للتحكم لرصد أراء العملاء السريعة، تُدعى الآراء المستقبلة Incoming feedback. تسمح هذه الأداة القابلة للتوسع للأشخاص بتقييم صفحة أو عنصر محدد والتعليق عليه باستخدام الوجوه التعبيرية emojis، والتي قد تمثل بديلًا أكثر لطفًا عن استطلاعات الرأي، وذلك اعتمادًا على الجمهور المستهدف ومدى جدية العلامة التجارية المطروحة. الاستبيانات لا أحد يعرف كيف يفكر العملاء سوى العملاء أنفسهم، لذلك تُعَد الاستبيانات طريقةً ممتازةً لجمع آراء نوعية من العملاء أنفسهم، وذلك أثناء زيارتهم لصفحة أو بعد زيارتها، إذ تستهدف صلب المواضيع المطروحة، فقد تهتم مواقع eCommerce على سبيل المثال باستبيانات ما قبل وما بعد الشراء لتحديد السبب الرئيسي الذي يدفع المستخدم إلى إتمام عملية الشراء أو تركها. أحد النماذج المثالية لتصميم استبيان يتيح هوتجار لمالكي المواقع الإلكترونية إمكانية إنشاء استبيانات مخصصة يمكن مشاركتها عبر البريد الإلكتروني أو الروابط، أو من خلال النوافذ المنبثقة في صفحات الويب، وقد تكون الاستبيانات على شكل أجوبة مفتوحة، أو خيارات متعددة أو أسئلة متدرجة. توفر لك الأجوبة المفتوحة في معظم الأوقات أكثر الإرشادات فائدةً، وذلك لأنها تعبير عن تجربة الأشخاص بكلماتهم الخاصة وما الذي يريدون تحسينه بالفعل. وبغض النظر عن طبيعة الأسئلة المطروحة، فلا بدّ أن تركّز على ما يحتاجه العملاء وما يسعون إليه، كما يمكن أن تركز على مسائل متنوعة مثل المحتوى (هل هذه المقالة مفيدة؟)، أو أن تحول الزوّار إلى عملاء (ما الذي تعتقد بأنه منعك من الشراء اليوم؟). يميل معظم الأشخاص إلى المشاركة في الاستبيانات إن كان هنالك شيء بالمقابل، لهذا فكر جديًا بالمحفّزات. استعمال Hotjar تثبيت Hotjar تتيح Hotjar اشتراكًا مجانيًا بميزات أساسية وحدود لتبادل البيانات. يُعَد هذا الخيار مثاليًا لمواقع الويب الصغيرة التي تريد أن تلقي نظرةً واسعةً على سلوك العملاء واحتياجاتهم، بينما قد يهتم أصحاب الأعمال الأكبر بالاشتراك المميز Premium الذي يمكّنهم من جمع بيانات أكثر والحصول على استطلاعات رأي مخصصة وواجهات لإجراء الاستبيانات. من السهل إعداد Hotjar، فما عليك سوى تثبيت مجموعة من شيفرات التتبع إما يدويًا، أو من خلال إضافة برمجية Plugin. يتوافق Hotjar مع مئات المنصات بما في ذلك ووردبرس من خلال الإضافة الرسمية الخاصة بالمنصة، ومنصات التجارة الإلكترونية eCommerce، مثل شوبفاي وماغينتو وبريستا شوب. بمجرد اشتراك الصفحة في Hotjar وتثبيت الشيفرة اللازمة، سيسهُل إنشاء الخرائط الحرارية ويبدأ في تسجيل جلسات العمل، أو إنشاء استطلاعات الرأي والاستبيانات. تُعَد لوحة التحكم في Hotjar واضحةً وتعرض النتائج بأسلوب مرئي، كما تُلخص البيانات في جداول بسيطة يسهل البحث فيها واستخلاص النتائج. الموازنة بين Hotjar وجوجل أنالايتكس يُعَد أنالايتكس أداةً معقدةً كما أشرنا سابقًا، وتركز فقط على كم البيانات التي يمكن جمعها، بينما يقدم Hotjar إضاءات كمية وكيفية بطريقة مفهومة. لا يُعَد Hotjar قويًا أو مفصّلًا مثل جوجل أنالايتكس من ناحية البيانات الكميةـ مثل التقارير الديموغرافية أو تقارير مخاريط التحوّل. وصف مارك زوكربيرغ في أحد المرات وصفة فيسبوك للنجاح، فقال:"لقد أنصتنا فعليًا لما يريده المستخدمون، إنصاتًا كيفيًا للكلمات التي يقولونها، وكميًا بتمعّن السلوك الذي يمارسونه". وبدلًا من النظر إلى أنالايتكس وHotjar على أنهما ضدان (وهما بالفعل ليسا كذلك)، من الأفضل عدّهما متكاملان. فاستخدام الحلول التي يتيحانها ستزوّد مصممي تجارب المستخدم والمسوقين بإضاءات لتحسين عملهم. التعامل مع مصممي تجارب المستخدم وأبحاث تجارب المستخدم يمكن للمسوقين ومحللي البيانات المحترفين تقديم العون للشركات في فهم البيانات الكمية، لكن جمع الآراء وتحليلها يتطلب معرفةً بعلم النفس والتعاطف. وعلى الرغم من أن واجهة Hotjar أكثر وضوحًا من جوجل أنالايتكس، فقد تكون مفيدةً لمالكي الأعمال كي يعملوا يدًا بيد مع الباحثين في مجال تجارب المستخدم ومصمميها لتفسير النتائج. يمكن لخبراء تجارب المستخدمين فعل الآتي: الإضاءة على جزئيات محددة. اكتشاف تحسينات محتملة. إعداد تقارير عن سجلات جلسات العمل. التفكير بأسئلة في صلب الموضوع عند تصميم استطلاعات الرأي والاستبيانات. التوصية بمحتوى معين أو تغييرات محددة في التصميم. تتبع اختبارات "الاختلاف الوحيد A/B test". سيساعد ذلك الأعمال بالمشاركة مع ميزات ومنافع Hotjar في تحسين تجربة عملائها وبالتالي نمو هذه الأعمال، فهذا استثمار يرفع قيمة عائدات الاستثمار ROI على المدى الطويل. ترجمة -وبتصرف- للمقال Hotjar UX and Conversion Tips لصاحبته Laura Geley. اقرأ أيضًا كيفية إجراء اختبار سريع لتجربة المستخدم كيفية استخدام اختبارات تجربة المستخدم لتحسين قيمة استمرار التعامل مع العميل CLV طرق مثبتة لتخصيص تجربة المستخدم كيفية تحسين إعدادات تطبيق بناء على تجربة المستخدم كيفية إجراء بحث في تجربة المستخدم لتحسين مشاريعك
-
يصف هذا القسم مجموعةً من المعايير الحديثة لمكونات الويب، وهذه المعايير بصدد التطوير حاليًا. تُدعم بعض الميزات وتتكامل ضمن معايير HTML/DOM، بينما يبقى البعض الآخر في مرحلة الإعداد. ويمكنك تجربة الأمثلة في أي متصفح، لكن يبقى المتصفح كروم Chrome الأكثر حداثةً فيما يتعلق بدعم هذه الميزات، والسبب طبعًا هو أنّ أشخاصًا من Google هم من وضعوا العديد من هذه المواصفات. ما هو الشيء المشترك بين محطة الفضاء ومكونات الويب إن فكرة المكونات ليست جديدة، فقد استُعملت في العديد من إطارات العمل وفي منصات مختلفة، وذلك قبل الانتقال إلى تفاصيل إنجاز تلك الميزات. لنلقي نظرةً على الصورة التالية، والتي تمثل إنجازًا عظيمًا للإنسانية: إنها محطة الفضاء الدولية، وتمثل الصورة التالية المكوِّنات التي تتألف منها -تقريبيًا طبعًا-: نلاحظ الآتي: تتألف المحطة من مكوِّنات عديدة. يمتلك كل مكوِّن بدوره أجزاءً أصغر في داخله. المكوّنات معقدّة جدًا، وهي بالطبع أعقد بكثير من معظم مواقع الويب. طوّرت مكونات المحطة على صعيد عالمي، ومن قِبل فرق من دول مختلفة تتحدث بلغات مختلفة. تحلق المحطة في مدارها حول الأرض وتبقي من في داخلها على قيد الحياة، فكيف بني هذا الجهاز المعقد؟ وما هي المبادئ التي يمكن أن نستعيرها لنطوّر مكوّنات ويب لها نفس مستوى الوثوقية وقابلة للنمو، أو قريبة من ذلك على الأقل؟ معمارية المكون تنص القاعدة الشهيرة في تطوير البرمجيات المعقدة على أن لا تطوّر برمجيات معقّدة، فإذا وصل بك الأمر إلى أمر معقد، فافصله إلى أجزاء أبسط واربطها ببعضها بأكثر الطرق وضوحًا، فالمعماري الأمهر هو من يستطيع تحويل المعقد إلى بسيط. يمكن تقسيم واجهة المستخدم إلى مكوّنات مرئية لكلٍّ منها موقعه في الصفحة، وتُنفِّذ مهمةً محددةً بعناية وتكون مفصولةً عن بعضها البعض، لنلق نظرةً على موقع ويب، وليكن تويتر مثلًا، يُقسم الموقع إلى مكوّنات: مكوّن للتنقل. معلومات المستخدم. اقتراحات المتابعة. نموذج إرسال. رسائل. غيرها من المكوِّنات. قد يكون للمكوّنات مكوّنات فرعيةً أيضًا، فقد تكون الرسائل مكوّنات فرعيةً من مكوّن أعلى مستوىً مثل قائمة الرسائل، كما قد تكون صورة المستخدم التي تقبل النقر مكوّنًا.. وهكذا. كيف نحدد ما هو المكوِّن إذًا؟ يأتي ذلك من حسن البديهة والخبرة والمنطق، حيث تشكًل المكوّنات عادةً كيانات مرئيةً نميزها وفقًا لوظيفتها وكيفية تتفاعلها مع الصفحة، ففي حالة الصفحة السابقة، سنجد أنها تحوي وحدات يلعب كل منها دوره الخاص، ومن المنطق إذًا أن نجعلها مكوّنات. للمكوِّن: صنف JavaScript خاص به. بنية DOM تُدار منفردةً من قبل الصنف الخاص بها، ولا يمكن الوصول إليها من قبل شيفرة خارجية (مبدأ التغليف). تنسيق CSS يُطبَّق على المكوّن. واجهة برمجية API، أحداث وتوابع يستخدمها الصنف وغيره من الكائنات التي تتفاعل مع المكوّنات الأخرى. تذكر أنّ فكرة الكائن ككل لا تفرض وجود أي شيء خاص. تتواجد الكثير من المنصات وإطارات العمل التي تقدم وسائل لإنشاء المكوّنات، وتستخدم أصناف CSS بالإضافة إلى ترتيبات أخرى لتعزيز المكوّنات وتحديد مجال تطبيق CSS وتغليف DOM. تزوّدنا مكوِّنات الويب بإمكانيات تصفّح مدمجةً معها ولا حاجة لمحاولة تقليدها بعد الآن. عناصر مخصصة Custom elements: لتعريف عناصر HTML مخصصة. شجرة DOM مخفية Shadow DOM: لإنشاء شجرة داخلية للمكوّن مخفية عن بقية المكوّنات. مجال لتطبيق أوراق التنسيق CSS Scoping: لتعريف قواعد تنسيق يمكن تطبيقها على الشجرة الداخلية لمكوّن. إعادة توجيه الحدث Event retargeting وغيره من الأمور الثانوية: لتحسين قدرة المكوّنات على التلاؤم مع عملية التطوير. كل الميزات المدعومة جيدًا لمكوّنات الويب جيدة في الأمور التي تقدر على تنفيذها. عناصر HTML المخصصة يمكننا إنشاء عناصر HTML مخصصة يصفها الصنف الذي ننشئه لهذا الغرض مزوّدًا بتوابعه وخصائصه وأحداثه الخاصة، وسنتمكن من استخدام العنصر ضمن عناصر HTML الأصلية بعد أن نعرّفه، وهذا أمر جيد، فعلى الرغم من غنى HTML بالعناصر، إلا أنها لا تحوي عناصرًا مثل علامة جداول سهلة التعامل <easy-tabs> أو سير دائري منزلق <sliding-carousel> أو الرفع بطريقة جذابة <beautiful-upload> أو أي عناصر أخرى قد نحتاجها. يمكن تعريف العناصر المخصصة ضمن أصناف خاصة، واستخدامها كما لو أنها جزء من HTML، ويوجد نوعان من هذه العناصر المخصصة، وهما: عناصر مخصصة ذاتية التصرف Autonomous: وهي عناصر جديدة توّسع عمل الصنف المجرّد HTMLElement. عناصر أصلية مخصصة customized: توسّع العناصر الأصلية، لإنجاز أزرار مخصصة مبنية على أساس الصنف HTMLButtonElement مثلًا. سنغطي أولًا العناصر ذاتية التصرف، ثم سننتقل إلى العناصر المخصصة، ولإنشاء عنصر مخصص لا بدّ من إخبار المتصفح عن جملة من التفاصيل حوله، مثل كيف سيُظهره؟ وماذا سيفعل عند إضافته أو إزالته من الصفحة؟ يجري ذلك بإنشاء صنف له توابعه الخاصة، وسيكون الأمر بسيطًا، فهي عدة توابع فقط وجميعها اختيارية. إليك تصورًا مع قائمة كاملة من التوابع: class MyElement extends HTMLElement { constructor() { super(); // إنشاء العنصر } connectedCallback() { //يستدعيه المتصفح عند إضافة عنصر ويمكن استدعاؤه عدة مرات إن أضيف أو حذف العنصر } disconnectedCallback() { //يستدعيه المتصفح عند حذف عنصر ويمكن استدعاؤه عدة مرات إن أضيف أو حذف العنصر } static get observedAttributes() { return [/* مصفوفة من أسماء السمات التي تراقب التغيرات */]; } attributeChangedCallback(name, oldValue, newValue) { // يُستدعى عندما تتغير صفة من السمات المذكوره أعلاه } adoptedCallback() { // يُستدعى عندما يُنقل العنصر إلى صفحة أخرى } // إن كان هناك عناصر أو سمات أخرى } نحتاج بعد ذلك إلى تسجيل العنصر: // سيُخدّم من قبل الصنف الجديد <my-element> أبلغ المتصفح أن customElements.define("my-element", MyElement); ستتولد الآن نسخة عن الصنف MyElement لكل عنصر HTML له الوسم <my-element>، وستستدعى التوابع المذكورة. كما يمكن تنفيذ ذلك باستخدام شيفرة JavaScript كالتالي: document.createElement('my-element') مثال عنصر لتنسيق الوقت يُستخدم العنصر الأصلي <time> لإظهار الوقت والتاريخ، لكنه لا يقدم أي تنسيق للبيانات التي يعطيها، لننشئ إذًا العنصر <time-formatted> الذي يعرض الوقت بتنسيق جميل يأخذ اللغة بالحسبان: <script> class TimeFormatted extends HTMLElement { // (1) connectedCallback() { let date = new Date(this.getAttribute('datetime') || Date.now()); this.innerHTML = new Intl.DateTimeFormat("default", { year: this.getAttribute('year') || undefined, month: this.getAttribute('month') || undefined, day: this.getAttribute('day') || undefined, hour: this.getAttribute('hour') || undefined, minute: this.getAttribute('minute') || undefined, second: this.getAttribute('second') || undefined, timeZoneName: this.getAttribute('time-zone-name') || undefined, }).format(date); } } customElements.define("time-formatted", TimeFormatted); // (2) </script> <!-- (3) --> <time-formatted datetime="2019-12-01" year="numeric" month="long" day="numeric" hour="numeric" minute="numeric" second="numeric" time-zone-name="short" ></time-formatted> See the Pen JS-p3-06-Custom-elements -ex1 by Hsoub (@Hsoub) on CodePen. تشير (1) إلى الصنف تابع واحد فقط هو ()connectedCallback، حيث يستدعيه المتصفح عندما يُضاف العنصر إلى الصفحة، أو عندما يكتشف مفسّر HTML وجوده، ويستخدم العنصر تابع تنسيق البيانات المدمج Intl.DateTimeFormat الذي يلقى دعمًا جيدًا من معظم المتصفحات لعرض الوقت بتنسيق جميل. تعني (2) أنه لا بدّ من تسجيل العنصر الجديد باستخدام الأمر (customElements.define(tag, class). تعني (3) أنه يمكن الآن استخدام العنصر في أي مكان. تحديث العنصر المخصص إذا صادف المتصفح العنصر <time-formatted> قبل عبارة التعريف customElements.define فلن يعدّ ذلك خطأً، بل سيعدّه مجرد عنصر غير معروف مثل أي وسم غير معياري، ويمكن تنسيق هذه العناصر غير المعروفة باستخدام المحدد (not(:defined:، وهو أحد محددات CSS. ستُحدّث هذه العناصر عندما يُستدعى التابع customElement.define، وستتولد نسخة من الصنف TimeFormatted لكلٍّ منها، ثم يُستدعى التابع connectedCallback وتتحول إلى عناصر معرّفة defined:. توجد توابع عدة للحصول على معلومات عن عناصر مخصصة: (customElements.get(name: يعيد الصنف الخاص بالعنصر الذي يحمل الاسم المحدد name. (customElements.whenDefined(name: يعيد وعدًا promise يحَلَّ -دون قيمة- عندما يُعرَّف العنصر المخصص الذي له الاسم name المحدد. التصيير داخل connectedCallback لا في constructor رأينا في المثال السابق كيف جرى تصيير محتوى العنصر ضمن التابع connectedCallback، لكن لماذا لم يُصيّر ضمن الدالة البانية constructor؟ إنّ السبب بسيط، لأنه من المبكر جدًا تصيير المكوّن عند استدعاء constructor، فعندما يُنشأ العنصر في هذه المرحلة، فلن يُعالجه المتصفح أو يسند إليه السمات الخاصة به، فاستدعاء getAttribute سيعيد القيمة "لاشيء" null، وبالتالي لن نتمكن من تصيير شيء، ولو فكرنا بالأمر قليلًا فسنجد أنّ هذا الأداء أفضل لتأخير العمل حتى يكون كل شيء جاهزًا. يقع الحدث connectedCallback عندما يُضاف العنصر إلى الصفحة، ولن يُضاف إلى عنصر آخر مثل ابن فقط، بل سيغدو عمليًا جزءًا من الصفحة، وهكذا سنتمكن من بناء شجرة DOM منفصلة، وإنشاء عناصر وتحضيرها للعمل لاحقًا، وستصيّر هذه العناصر فعليًا عندما تُضاف إلى الصفحة. مراقبة السمات ليس لتغير السمات attribute في الطريقة الحالية لتنفيذ العنصر <time-formatted> أي تأثير بعد تصييره، وهذا غريب بالنسبة إلى عنصر HTML، فعندما نغيّر في صفة مثل a.href، فسنتوقع عادةً أن نرى التغير مباشرةً. نصلح هذا الأمر إذًا. يمكن مراقبة السمات بتزويد دالة الإحضار الساكنة ()observedAttributes بقائمة تضم أسماءهم، وهكذا ستُستدعى الدالة attributeChangedCallback عندما نجري أي تعديلات على تلك السمات. لن يقع هذا الحدث بالطبع عندما تتغير سمات غير مدرجة في القائمة، لأسباب تتعلق بالأداء. إليك شيفرة العنصر <time-formatted> الذي يُحدَّث تلقائيًا عندما تتغير سماته: <script> class TimeFormatted extends HTMLElement { render() { // (1) let date = new Date(this.getAttribute('datetime') || Date.now()); this.innerHTML = new Intl.DateTimeFormat("default", { year: this.getAttribute('year') || undefined, month: this.getAttribute('month') || undefined, day: this.getAttribute('day') || undefined, hour: this.getAttribute('hour') || undefined, minute: this.getAttribute('minute') || undefined, second: this.getAttribute('second') || undefined, timeZoneName: this.getAttribute('time-zone-name') || undefined, }).format(date); } connectedCallback() { // (2) if (!this.rendered) { this.render(); this.rendered = true; } } static get observedAttributes() { // (3) return ['datetime', 'year', 'month', 'day', 'hour', 'minute', 'second', 'time-zone-name']; } attributeChangedCallback(name, oldValue, newValue) { // (4) this.render(); } } customElements.define("time-formatted", TimeFormatted); </script> <time-formatted id="elem" hour="numeric" minute="numeric" second="numeric"></time-formatted> <script> setInterval(() => elem.setAttribute('datetime', new Date()), 1000); // (5) </script> See the Pen JS-p3-06-Custom-elements -ex2 by Hsoub (@Hsoub) on CodePen. يُقصَد بـ (1) أنه قد نُقل منطق التصيير إلى التابع المساعد <time-formatted>. تعني (2) أن هذا التابع يُستدعى مباشرةً في اللحظة التي يضاف فيها العنصر إلى الصفحة. تعني (3) أنه يقع الحدث attributeChangedCallback عند حدوث أي تغير في السمات التي مررت على شكل قائمة إلى ()observedAttributes تعني (4) أنه يُعاد تصيير العنصر بعد ذلك. تعني (5) أنه في النهاية يمكننا إنشاء مؤقت مباشر بكل سهولة. تسلسل التصيير عندما يبني مفسِّر HTML الشجرة DOM، تعالَج العناصر الواحد تلو الآخر والأب قبل الابن، فلو كان لدينا العنصر: <outer><inner></inner></outer> فسيتولد العنصر <outer> أولًا ويضاف إلى DOM، ثم العنصر <inner>، وسيقود ذلك إلى تبعات بالنسبة للعناصر المخصصة، فإذا أراد العنصر المخصص الوصول إلى innerHTML ضمن connectedCallback مثلًا، فلن يحصل على شيء. <script> customElements.define('user-info', class extends HTMLElement { connectedCallback() { alert(this.innerHTML); // empty (*) } }); </script> <user-info>محمد</user-info> إذا نفّذت الشيفرة السابقة فستظهر الرسالة alert فارغة، والسبب في ذلك هو عدم وجود عناصر أبناء في هذه المرحلة، وستكون الشجرة DOM غير مكتملة، يربط مفسِّر HTML العنصر المخصص <user-info> وسيستأنف العمل بعدها لربط أبنائه لكنه لم يفعلها بعد. إذا أردنا تمرير معلومات إلى عنصر مخصص، فيمكننا استخدام السمات فهي متاحة مباشرةً، وإذا كنا بحاجة فعلًا إلى الأبناء، فيمكن تأجيل الوصول إليهم باستخدام الحدث setTimeoutدون تأخير زمني Zero-delay. <script> customElements.define('user-info', class extends HTMLElement { connectedCallback() { setTimeout(() => alert(this.innerHTML)); // محمد (*) } }); </script> <user-info>John</user-info> وهكذا ستُظهر الرسالة alert في السطر (*) الكلمة "محمد" طالما أننا ننفذ الأمر دون تزامن، وذلك بعد اكتمال تفسير شيفرة HTML، ويمكن معالجة الأبناء إذا اقتضى الأمر ثم إنهاء مرحلة التهيئة، لكن هذا الحل ليس مثاليًا. فإذا استخدمت العناصر المخصصة الحدث لتهيئة نفسها، فإنها ستصطف بحيث يقع الحدث setTimeout للعنصر الخارجي ثم الداخلي، وبالتالي ستنهي العناصر الخارجية تهيئة نفسها قبل الداخلية، لنشرح ذلك من خلال المثال التالي: <script> customElements.define('user-info', class extends HTMLElement { connectedCallback() { alert(`${this.id} connected.`); setTimeout(() => alert(`${this.id} initialized.`)); } }); </script> <user-info id="outer"> <user-info id="inner"></user-info> </user-info> تسلسل الخرج: يُربط العنصر الخارجي بالشجرة. يُربط العنصر الداخلي. يُهيأ العنصر الخارجي. يُهيأ العنصر الداخلي. يمكننا أن نرى بوضوح أن العنصر الخارجي سينهي تهيئة نفسه (الخطوة 3) قبل الداخلي (الخطوة 4)، ولا يوجد استدعاء مدمج يمكن أن يقع بعد أن يصبح العنصر ضمن الشجرة، كما يمكن تنفيذ استدعاء مثل هذا عند الحاجة بأنفسنا، حيث يمكن للعناصر الداخلية إرسال أحداث مثل initialized تستمع إليها العناصر الخارجية وتتفاعل معها. العناصر الأصلية المخصصة ليس للعناصر المخصصة -مثل <time-formatted>- أي دلالات تتعلق بها، فهي غير معروفة لمحركات البحث مثلًا، ولا تستطيع الأجهزة التي تدعم مبدأ الوصول السهل accessibility التعامل معها. قد تكون هذه الأشياء مهمةً، فقد يرغب محرّك البحث بمعرفة أن هذا العنصر سيظهر الوقت، فإذا أنشأنا نوعًا خاصًا من الأزرار، فلماذا لا يمكننا إعادة استخدام وظيفة الزر الأصلي <button>؟ يمكننا توسيع وتخصيص العناصر الأصلية بالوراثة من أصنافها، فالأزرار مثلًا هي نسخ عن الصنف HTMLButtonElement، ولإنشاء عنصر يرث منه ويوسّعه عليك باتباع الآتي: وسِّع الصنف HTMLButtonElement عن طريق الصنف الذي سننشئه: class HelloButton extends HTMLButtonElement { /* custom element methods */ } أعط قيمةً للوسيط الثالث للدالة customElements.define والذي يمثّل الوسم tag المستهدف: customElements.define('hello-button', HelloButton, {extends: 'button'}); يمكن أن تشترك وسوم مختلفة بصنف DOM، لذلك سنحتاج إلى استخدام التعليمة extends. لكي نستخدم أخيرًا العنصر المخصص، استخدم الوسم الأصلي النظامي <button>، لكن أضف إليه الصفة "is="hello-button. <button is="hello-button">...</button> إليك المثال كاملًا: <script> // عند ضغطه "hello" الزر الذي يظهر class HelloButton extends HTMLButtonElement { constructor() { super(); this.addEventListener('click', () => alert("Hello!")); } } customElements.define('hello-button', HelloButton, {extends: 'button'}); </script> <button is="hello-button">Click me</button> <button is="hello-button" disabled>Disabled</button> See the Pen JS-p3-06-Custom-elements -ex3 by Hsoub (@Hsoub) on CodePen. يوسِّع الزر الجديد الزر الأصلي، وبالتالي سيحتفظ بتنسيق وميزات الزر الأصلي مثل الصفة disabled. قوالب HTML يقدم العنصر الأصلي <template> قوالبًا لتخزين شيفرة HTML ويتجاهل محتوياتها، ويتحقق من صحة التركيب اللغوي للشيفرة ضمنها حصرًا، لكن يمكن الوصول إليها واستخدامها في إنشاء عناصر أخرى. يمكن نظريًا إنشاء أي عناصر مخفية ضمن ملف HTML لتخزين شيفرة HTML، فما الغاية من <template>؟ أولًا: قد يحتوي على أي شيفرة HTM صالحة، حتى تلك التي تتطلب عادةً وسم إغلاق Closing tag، فيمكن أن نضع ضمنها أسطر جدول <tr>: <template> <tr> <td>Contents</td> </tr> </template> عندما نحاول عادةً وضع <tr> ضمن <div> مثلًا، فسيكتشف المتصفح الخطأ في بنية DOM ويصلحها بإضافة الوسم <table> وجعل السطر ضمنه تلقائيًا. من الناحية الأخرى سيُبقي <template> ما وضعناه في المكان الذي وضعناه فيه دون تعديل. يمكننا وضع تنسيقات وسكربتات ضمن <template> أيضًا: <template> <style> p { font-weight: bold; } </style> <script> alert("Hello"); </script> </template> يَعُد المتصفح محتويات <template> "خارج المستند"، أي لن تُطبق قواعد التنسيق الموجودة ضمنه ولن يعمل <video autoplay>، كما سيُصبح المحتوى حيًا -أي يُطبَّق التنسيق وتنفَّذ السكربتات- عندما ندرجه ضمن المستند. إدراج القالب يُتاح محتوى قالب ضمن الخاصية content العائدة له على شكل عقدة من النوع DocumentFragment، وهي نوع خاص من عقد DOM، ويمكن التعامل معها مثلما نتعامل مع أي عقدة DOM عدا خاصية واحدة مميزة، وهي إدراج الأبناء بدلًا من العقدة عندما نحاول إدراجها، فعلى سبيل المثال: <template id="tmpl"> <script> alert("Hello"); </script> <div class="message">Hello, world!</div> </template> <script> let elem = document.createElement('div'); // انسخ محتوى القالب لاستخدامه مرات عدة elem.append(tmpl.content.cloneNode(true)); document.body.append(elem); // سيعمل الآن السكربت الموجود ضمن القالب </script> دعونا نعيد كتابة مثال شجرة DOM الخفية في الفصل السابق باستخدام القالب <template>: <template id="tmpl"> <style> p { font-weight: bold; } </style> <p id="message"></p> </template> <div id="elem">Click me</div> <script> elem.onclick = function() { elem.attachShadow({mode: 'open'}); elem.shadowRoot.append(tmpl.content.cloneNode(true)); // (*) elem.shadowRoot.getElementById('message').innerHTML = "Hello from the shadows!"; }; </script> See the Pen JS-p3-06-Template-element -ex1 by Hsoub (@Hsoub) on CodePen. عندما ننسخ وندرج tmpl.content في السطر (*) مثل عقدة من النوع DocumentFragment، فسيُدرج أبناؤها (<style> و<p>) بدلًا منها، وسيشكلان شجرة DOM الخفية: <div id="elem"> #shadow-root <style> p { font-weight: bold; } </style> <p id="message"></p> </div> خلاصة للعناصر المخصصة نوعان: الأول عناصر مخصصة ذاتية التصرف Autonomous، لها وسم خاص جديد وتوسع الصنف HTMLElement، وإليك تخطيط التعريف لهذه العناصر: class MyElement extends HTMLElement { constructor() { super(); /* ... */ } connectedCallback() { /* ... */ } disconnectedCallback() { /* ... */ } static get observedAttributes() { return [/* ... */]; } attributeChangedCallback(name, oldValue, newValue) { /* ... */ } adoptedCallback() { /* ... */ } } customElements.define('my-element', MyElement); /* <my-element> */ أما النوع الثاني، فهو عناصر أصلية معدَّلة customized: توّسع عناصرًا أصليةً موجودة، وتتطلب وسيطًا ثالثًا للدالة define.، وإلى الصفة "..."=is ضمن وسمها. class MyButton extends HTMLButtonElement { /*...*/ } customElements.define('my-button', MyElement, {extends: 'button'}); /* <button is="my-button"> */ تدعم معظم المتصفحات العناصر المخصصة جيدًا، كما يوجد موائم polyfill أي تعويض نقص الدعم للمتصفحات غير المدعومة. قد يكون محتوى <template> أي عناصر HTML صالحة قواعديًا. يعُد المتصفح محتويات <template> خارج المستند، إذ لن يكون لها أي تأثير. يمكن الوصول إلى محتوى القالب template.content من JavaScript، ونسخها لإعادة استخدامها في مكوَّن جديد. الوسم فريد لأن: محتويات القالب <template> ستتعرض إلى تدقيق قواعدي من قِبل المتصفح، وهذا مخالف لاستخدام قالب نصي ضمن سكربت. يُسمح باستخدام وسوم HTML عالية المستوى حتى تلك التي لا يجب استخدامها دون عنصر يغلّفها، مثل <tr> التي ينبغي تغليفها بالوسم <table>. تصبح المحتويات حيّةً -أي تُطبَّق وتُنفَّذ- عندما تُدرج ضمن المستند. لا يدعم العنصر <template> المُكررات iterator، ولا آليات الربط بقواعد البيانات أو تبديل المتغيرات، لكن يمكن إدراجها في أعلى القالب. مهام لإنجازها عنصر توقيت مباشر أنشئ العنصر <live-timer> الذي يعرض الوقت الحالي: ينبغي أن يستخدم عنصر التوقيت العنصر <time-formatted> الذي أنشأناه سابقًا، بصورة ضمنية وليس بنسخ وظيفته. يُحدَّث المؤقت كل ثانية. مع كل تحديث للمؤقت سيتولد حدث خاص يُدعى tick يحمل التاريخ الحالي ضمن الخاصية event.detail، راجع فصل "إنشاء أحداث مخصصة في المتصفح عبر جافاسكربت". طريقة الاستخدام: <live-timer id="elem"></live-timer> <script> elem.addEventListener('tick', event => console.log(event.detail)); </script> إليك المثال النموذجي: يمكنك فتح المثال في بيئة تجريبية لاحظ أن: صفرنا المؤقت setInterval عند إزالة العنصر من الصفحة وهذا أمر مهم إذ بدونه سيستمر الحدث بالعمل كل ثانية في الوقت الذي لا يُستعمَل فيه، ولا يمكن للمتصفح أيضًا أن يمسح الذاكرة من تلقاء نفسه. يمكن الوصول إلى القيمة الحالية عبر الخاصية elem.date، إذ كل توابع الصنف وخاصياته هي توابع للعنصر وخاصياته. ويمكنك فتح الحل في بيئة تجريبية. المراجع HTML Living Standard Compatiblity ترجمة -وبتصرف- للفصول Custom elements وFrom Orbital Height وTemplate element من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: إنشاء الحركات عبر جافاسكربت المقال التالي: مكونات الويب: التعامل مع شجرة DOM الخفية iframe { border: 1px solid #e7e5e3 !important; width: 100%; } iframe div#path { margin: auto; }
-
يتضمن هذا المقال تعريفًا بالتجهيزات الأساسية التي تحتاجها للعمل على راسبيري باي وكيفية توصيلها وإعدادها للعمل، حيث صُمم جهاز راسبيري باي ليكون سهل الإعداد وسريعًا قدر الإمكان، لكنه سيتأثر بالمكونّات الخارجية المُتصلة به، والتي تُدعى طرفيات peripherals مثل بقية الحواسيب. قد يبدو لك الأمر مربكًا للوهلة الأولى عندما تتأمل اللوحة المجردة لجهاز راسبيري باي، والتي تبدو مختلفةً تمامًا عن الحواسب الموضوعة في حواضن مغلقة، وستظن أنّ الوضع سيصبح أكثر تعقيدًا. في الواقع لن يحدث ذلك، حيث ستتعامل جيدًا مع راسبيري باي خلال عشر دقائق إن اتبعت ببساطةٍ خطوات هذا المقال. إذا كنت تمتلك عدة مبتدئي راسبيري باي Starter Kit والتي تباع عادةً مثل مجموعة متكاملة، فسيكون لديك كل ما يلزم لتبدأ العمل، وما عليك سوى تأمين شاشة حاسوب أو تلفازٍ قادرٍ على الاتصال بمنفذ HDMI لترى ما ينفّذه جهاز راسبيري باي؛ أما إذا لم تقتنِ بعد عدة مبتدئي راسبيري باي، فستحتاج إلى الأتي: جهاز راسبيري باي 4 نموذج B. مصدر تغذية كهربائي بمخرج USB-C: جهده 5 فولت وتياره 3 أمبير. ويُفضّل شراء مصدر التغذية الرسمي الخاص براسبيري باي كونه يتلاءم مع التبدلات السريعة لكمية الطاقة الكهربائية التي يتطلبها الجهاز. بطاقة ذاكرة من نوع microSD ثُبِّت عليها برنامج NOOBS، حيث تعمل بطاقة الذاكرة مثل مخزنٍ دائمٍ لراسبيري باي، وتحتوي على كل الملفات التي تخزنها والبرامج التي تثبتها بما في ذلك نظام التشغيل. قد تكفي ذاكرة بحجم 8GB لتبدأ العمل، لكن ستساعدك ذاكرة بحجم 16GB على توسيع عملك لاحقًا، ومن الأفضل والأوفر من ناحية الوقت أن يأتي برنامج NOOBS مثبتًا مسبقًا على بطاقة الذاكرة. برنامج NOOBS هو اختصارٌ لبرنامج لجهاز جديد New Out-Of Box Software، حيث تُوضع نسخةٌ منه على بطاقة ذاكرة مهيأة حديثًا ليتولى شؤون تنصيب نظام التشغيل المناسب بمجرد وضعها في راسبيري باي وتشغيله. راجع الملحق A للحصول على إرشادات حول تنصيب NOOBS على بطاقة ذاكرة فارغة إن لم يكن مثبتًا مسبقًا. فأرة ولوحة مفاتيح بمنفذ USB يتيحان لك التحكم بجهاز راسبيري باي، حيث ستعمل أية تجهيزاتٍ تأتي بمنفذ USB مع راسبيري باي بصورةٍ جيدة، سواءً السلكية منها أو اللاسلكية، لكن قد تستجر بعض لوحات المفاتيح المخصصة للألعاب، والتي تأتي مع أضواءٍ ملونة كميةً كبيرةً من الطاقة الكهربائية، وبالتالي لن يكون العمل معها موثوقًا. كابل من نوع Micro-HDMI لنقل الصوت والصورة من راسبيري باي إلى شاشة الحاسوب أو التلفاز، حيث يحمل أحد طرفيه وصلةً صغيرة الحجم من نوع micro-HDMI، بينما يحمل الطرف الآخر وصلة HDMI ذات حجمٍ قياسي لتوصيل الكابل مع جهاز العرض الذي تستخدمه. يمكنك كذلك استخدام كابل HDMI ذو طرفين قياسيين ومحوّل موائمة من HDMI إلى micro-HDMI، من أجل وصل الكابل مع راسبيري باي؛ وإذا كانت الشاشة التي تستخدمها لا تدعم HDMI، فاستخدم محوّل موائمة من micro-HDMI إلى منفذ العرض الرقمي DVI-D أو منفذ العرض الكلاسيكي VGA؛ ومن أجل توصيل راسبيري باي إلى جهاز تلفاز قديم يستخدم إشارة الفيديو المركبة composite video أو يمتلك مقبس SCART، استخدم كابل TRRS صوت وصورة، بقطر 3.5 ميليمتر مشابه لمقبس سماعات الرأس التي تتصل مع الهواتف المحمولة. تستطيع استخدام راسبيري باي بأمان دون حاضنة، لكن تجنب وضعه على سطحٍ معدني ناقلٍ للتيار الكهربائي، لما قد يسبب قصرًا في إحدى داراته، حيث يُجنّبك استخدام حاضنةٍ هذه المشكلة، لكنه يبقى أمرًا اختياريًا. تحتوي مجموعة أدوات المبتدئين على أية حال حاضنة راسبيري باي الرسمية، كما ستجد العديد من الحاضنات التي تنتجها أطرافٌ أخرى في نقاط البيع المختصة. ستحتاج أيضًا إلى كبل شبكة network cable إن كنت تنوي وصل راسبيري باي إلى شبكة سلكية محلية بدلًا من الاتصال اللاسلكي WiFi، وبالطبع لا بدّ من وصل الطرف الآخر للكابل بمحوّل شبكة network switch أو موجِّه router. لن تحتاج إلى الكابل إن قررت استخدام الاتصال اللاسلكي WiFi، لكنك ستحتاج إلى اسم الشبكة اللاسلكية التي ستنضم إليها مع مفتاحها أو عبارة المرور الصحيحة. إعداد العتاد الصلب Hardware أخرج راسبيري باي من صندوقه، وحاول إمساكه من حوافه بدلًا من المساحات الفارغة التي قد تجدها ضمن اللوحة؛ والمقصود هنا المساحات التي لا تحتوي على عناصر إلكترونية أو رقاقات، فمن الأفضل اتباع هذا الأسلوب من الآن وصاعدًا. انتبه جيدًا عندما تقترب من أرجل المنصات Header pins؛ فأي التواءٍ في أحدها قد يوقعك بمشاكلٍ أبسطها صعوبة التعامل مع أية طرفياتٍ قد تضيفها إلى راسبيري باي عبر هذه المنصات، وأسوؤها هو حدوث قِصَر في أحد الدارات وتضرر الجهاز بالكامل. فعلى الرغم من متانة راسبيري باي، لا يعني ذلك إطلاقًا أنه لا يتضرر. تجميع الحاضنة إذا قررت وضع جهازك في حاضنة لمزيدٍ من الحماية، فستكون خطوتك الأولى هي تثبيت لوحة راسبيري باي ضمنها؛ وفي حال استخدمت حاضنة راسبيري باي الرسمية، فابدأ بفصلها إلى قطعتين، القاعدة الحمراء والغطاء الأبيض. ومن أجل تجميع الحاضنة، اتبّع الخطوات التالية: 1- احمل القاعدة بحيث تصبح النهاية المرتفعة على يسارك والمنخفضة إلى يمينك. 2- أمسك اللوحة (دون وجود بطاقة الذاكرة) من غلافي منفذي USB ومحوّل الشبكة المحلية، ثم أَمِل اللوحة قليلًا، وادفع بمنافذ USB-C وmicro-HDMI وAV إلى الفتحات المقابلة ضمن القاعدة الحمراء. أنزل الناحية الأخرى التي تمسكها رويدًا رويدًا لتأخذ مكانها. 3- ثبّت مشابك الغطاء الأبيض في فتحتهما على يسار القاعدة فوق مدخل بطاقة الذاكرة، ثم اضغط الطرف الآخر للغطاء بلطفٍ نحو الأسفل حتى تسمع صوت الإطباق. إدخال وتوصيل بطاقة الذاكرة microSD اقلب حاضنة راسبيري باي إذا كنت تستخدم واحدة، ثم ازلق بطاقة الذاكرة ضمن حاضنتها، بحيث تكون لصاقة عنوان البطاقة بالاتجاه المعاكس للوحة راسبيري باي، ولن تدخل البطاقة على أية حال إلا باتجاهٍ واحدٍ وينبغي أن تدخل بسلاسةٍ دون ضغط زائد. ستنزلق بطاقة الذاكرة داخل الحاضنة وتتوقف دون صوت إقفال. إذا أردت إزالة البطاقة لاحقًا، فماعليك سوى إمساك نهايتها الظاهرة وسحبها بلطفٍ نحو الخارج، كما يجب الانتباه إلى ضرورة دفع البطاقة للداخل بلطفٍ أولًا لتحريرها إذا كنت تستخدم نماذجًا قديمة من راسبيري باي، لكن هذا غير ضروري أبدًا في النموذجين باي 3 و4. وصل لوحة المفاتيح والفأرة صِل كابل لوحة المفاتيح من نوع USB إلى أي منفذٍ من منافذ USB الأربعة، سواءً من النوع 2.0، أو 3.0 الموجودة على لوحة باي. إذا استخدمت الحاضنة الرسمية لباي، فستجد إشارةً في مؤخرتها إلى منفذ USB المخصص للفأرة، وإذا لم تستخدم الحاضنة فصِل الفأرة بأيٍ من منافذ USB الشاغرة. سيدخل مقبس USB للفأرة أو لوحة المفاتيح في المنفذ الموجود على باي بكل سهولة ودون ضغطٍ زائد، لكن إذا واجهتك صعوبةٌ في إدخاله، فلا بد من وجود مشكلةٍ ما، لذلك تأكد قبل كل شيء من جهة مقبس USB إذا كان اتجاه دخوله صحيحًا، وإلا فغيّر الاتجاه. وصل شاشة العرض صِل نهاية كابل micro-HDMI صغيرة الحجم بمنفذ micro-HDMI على راسبيري باي بجوار مدخل التغذية USB-C، وصِل الطرف الآخر ذو القياس المعياري بشاشة العرض. إذا اشتملت شاشتك على أكثر من منفذ HDMI، فتذكر رقم المنفذ الذي وصلت به راسبيري باي، لأنك ستُضطر إلى تغيير دخل الشاشة إلى هذا المنفذ وإلا لن ترى شيئًا. إذا لم ترى رقمًا بجانب المنفذ، فبدِّل منافذ دخل الشاشة عن طريق جهاز التحكم الخاص بها واحدًا تلو الآخر حتى ترى راسبيري باي. التوصيل الاختياري لراسبيري باي بكابل الشبكة المحلية Ethernet استخدم كابل شبكة محلية (كابل إيثرنت Ethernet) مزوّد بمقبس RJ45 في كلا الطرفين، ثم ادفع أحد النهايتين ضمن منفذ الشبكة السلكية في راسبيري باي، بحيث يكون الظفر البلاستيكي للمقبس نحو الأسفل حتى تسمع صوت القفل. ولنزع الكابل، اضغط الظفر البلاستيكي نحو الأعلى، ثم اسحب الكابل نحو الخارج بلطف. صِل الطرف الآخر للكابل إلى أي منفذٍ حر في موزع الشبكة السلكية network hub أو في الموجّه router بنفس الطريقة. وصل التغذية الكهربائية إلى راسبيري باي هي الخطوة الأخيرة في عملية التركيب. لا تُوصل التغذية الكهربائية حتى تكون مستعدًا لإعداد برمجيات الجهاز، حيث لا يملك راسبيري باي مفتاحًا لفصل ووصل الطاقة، وسيعمل بمجرد تغذيته بالكهرباء. صِل بداية مقبس USB-C لعلبة التغذية بالمنفذ المقابل له على راسبيري باي، حيث ينبغي أن يدخل المقبس بسهولة ضمن المنفذ بكلا الوضعين. إذا جاءت علبة التغذية مع كابلٍ قابل للنزع، فتأكد أنّ طرفه الآخر متصلٌ بجسم مصدر التغذية. صِل علبة التغذية إلى مأخذ الطاقة الكهربائية ثم شغّله. عندها سيبدأ راسبيري باي العمل مباشرةً. إعداد نظام تشغيل راسبيري باي عليك إعداد برمجيات الجهاز وعلى رأسها نظام التشغيل الذي يتحكم بوظائف باي حتى يعمل جهازك جديًا، لهذا صُمم برنامج NOOBS، وهو اختصارٌ إلى برمجيات الجهاز الجديد New Out-Of-Box Software، ويُستخدم لجعل العملية سهلةً قدر الإمكان؛ حيث يعطيك البرنامج حرية الاختيار بين عدة أنظمة تشغيل لتثبيتها تلقائيًا بالنيابة عنك. وأفضل ما في الأمر أنك ستنجز ذلك بنقر الفأرة عدة مرات فقط. عند تشغيل الجهاز للمرة الأولى، أو عند إقلاعه تمهيديًا Boot بوجود نسخة حديثة التثبيت من برنامج NOOBS على بطاقة الذاكرة microSD، سترى شاشةً تحمل شعار راسبيري باي ونافذةً صغيرةً في الزاوية اليسارية العليا تشير إلى تقدم العملية، وبعد فترةٍ وجيزة قد تستمر دقيقة سترى للمرة الأولى بطاقة الذاكرة التي تحمل برنامج NOOBS كما تبدو في الصورة التالية: برنامج NOOBS دون تثبيت أي نظام تشغيل ستعرض عليك تلك القائمة أنظمة التشغيل الممكن تثبيتها على جهازك، حيث تضم هذه القائمة نظامي تشغيل معياريين، هما: راسبيان Raspbian: وهو إصدارٌ من نظام تشغيل ديبيان لينوكس Debian Linux مصُممٌ خصيصًا لراسبيري باي. ليبريليك LibreELEC: وهو نسخةٌ من برنامج Kodi Entertainment Centre. تستطيع كذلك تحميل أنظمة تشغيل أخرى إن كنت متصلًا بالشبكة السلكية أو عن طريق تفعيل الاتصال اللاسلكي WiFi إذا كان متوفرًا من خلال النقر على الأيقونة (Wifi networks (w ضمن شريط الأيقونات العلوي لقائمة NOOBS. و لتثبيت نظام التشغيل، انقر بزر الفأرة اليساري فوق الخيار Raspbian Full، سترى بعد ذلك أنّ لون الأيقونة (Install (i لم يعد رماديًا وهذا دليلٌ على أن نظام التشغيل الذي اخترته أصبح جاهزًا للتثبيت. انقر على الأيقونة (Install (i بزر الفأرة اليساري وسترى عندها رسالة تحذير تخبرك بأن تثبيت نظام التشغيل سيمحو أية بياناتٍ قد خزنتها سابقًا على بطاقة microSD، وتستثني بالطبع الرسالة برنامج NOOBS الذي يبقى على حاله. انقر "Yes" لتبدأ عملية التثبيت. قد يستغرق تثبيت النظام من 10 إلى 30 دقيقة بناءً على سرعة بطاقة الذاكرة microSD، حيث يسمح لك شريط التقدم أسفل النافذة بمتابعة حالة تثبيت النظام. سترى أيضًا شريط عرض يُظلِّل بعض ميزاته الأساسية (سنتعرف أكثر على هذا الشريط وعلى نظام التشغيل في المقال التالي المتمحور حول استثمار نظام تشغيل راسبيري باي)، وحالما ينتهي تثبيت النظام ستظهر لك نافذة تحمل الزر "OK"، عندها انقر على هذا الزر لتعيد إقلاع باي على نظام التشغيل الذي ثبتّه توًّا. قد تلاحظ أثناء الإقلاع الكثير من الأسطر النصية المعروفة باسم رسائل الإقلاع تمر على الشاشة. قد يستغرق الأمر دقيقةً أو دقيقتان عند إقلاع نظام التشغيل راسبيان للمرة الأولى، حيث يكيّف النظام نفسه ليستغل المساحة الفارغة من بطاقة الذاكرة microSD أفضل استغلال، وستلاحظ أنّ العملية ستكون أسرع بكثير خلال الإقلاعات اللاحقة. أخيرًا، سترى لبرهة نافذةً تحمل شعار راسبيري باي قبل أن يظهر لك سطح مكتب راسبيان Raspbian ومعالج إعداد النظام setup wizard. وهكذا يكون تثبيت نظام التشغيل على راسبيري باي قد اكتمل تمامًا وأصبح جاهزًا للإعداد؛ وهذا ما ستتعلمه في المقال التالي. ترجمة -وبتصرف- للفصل الثاني Getting started with your Raspberry Pi من كتاب The official Raspberry Pi Beginners Guide. اقرأ أيضًا المقال السابق: تعرف على جهاز راسبيري باي Raspberry Pi المدخل الشامل لتعلم علوم الحاسوب أنظمة التشغيل للمبرمجين
-
يمكن لرسوم JavaScript التعامل مع حالات لا يمكن أن تتعامل معها CSS، مثل التحرك على مسار معقد مختلف عن منحنيات بيزيه Bezier curves باستخدام دالة توقيت، أو رسوميات متحركة على لوحة رسم. استخدام الدالة setInterval يمكن إنجاز الرسوم المتحركة في صورة سلسلة من الإطارات، والتي تكون عادةً تغيرات صغيرةً في خصائص HTML/CSS، فعلى سبيل المثال: يؤدي تغيير قيمة الخاصية style.left من 0px إلى 100px إلى تحريك العنصر، وإذا زدنا هذه القيمة ضمن الدالة setInterval، فسيؤدي تغير مقداره 2px مع تأخير ضئيل، لتكرار العملية بمقدار 50 مرةً في الثانية، مما يجعل الحركة سلسةً وناعمةً، ويُتَّبع هذا الأسلوب في السينما، فعرض 24 إطارًا في الثانية يجعل الصورة سلسةً. إليك الشيفرة المجردة pseudo-code للفكرة: let timer = setInterval(function() { if (animation complete) clearInterval(timer); else increase style.left by 2px }, 20); // تغير بمقدار 2 بكسل بتأخير 20 ميلي ثانية يعطي 50 إطار في الثانية وهذا مثال أكثر تعقيدًا: let start = Date.now(); // تذكر وقت البدء let timer = setInterval(function() { // كم مضى من الوقت منذ البداية let timePassed = Date.now() - start; if (timePassed >= 2000) { clearInterval(timer); // أنهي الحركة بعد ثانيتين return; } // ارسم إطار الحركة في اللحظة الحالية draw(timePassed); }, 20); // عندما يتغير الوقت بين 0 و2000 ميلي ثانية // تتغير قيمة الخاصية من 0 بكسل إلى 400 بكسل function draw(timePassed) { train.style.left = timePassed / 5 + 'px'; } إليك المثال النموذجي التالي: شيفرة الملف index.html: <!DOCTYPE HTML> <html> <head> <style> #train { position: relative; cursor: pointer; } </style> </head> <body> <img id="train" src="https://js.cx/clipart/train.gif"> <script> train.onclick = function() { let start = Date.now(); let timer = setInterval(function() { let timePassed = Date.now() - start; train.style.left = timePassed / 5 + 'px'; if (timePassed > 2000) clearInterval(timer); }, 20); } </script> </body> </html> وستكون النتيجة: See the Pen JS-p3-05-JavaScript-animations -ex1 by Hsoub (@Hsoub) on CodePen. استخدام الدالة requestAnimationFrame لنفترض تنفيذ عدة حركات انتقالية معًا. إذا شغّلنا هذه الرسوم منفصلةً فسيعيد المتصفح -وعلى الرغم من أنّ لكل حركة دالة (setInterval(..., 20 خاصةً- رسم الصور بمعدل أكبر بكثير من 20 ميلي ثانية، ويحدث هذا لوجود أوقات بداية مختلفة لكل حركة، وبالتالي سيختلف تكرار التغيّر الذي ضبطناه عند 20 ميلي ثانية بالنسبة لكل حركة، وهكذا سنكوِّن حالات تشغيل مستقلةً لكل حركة انتقالية تتكرر كل 20 ميلي ثانية، وبكلمات أخرى انظر الشيفرة التالية: setInterval(function() { animate1(); animate2(); animate3(); }, 20) والتي ستكون أخفّ من ناحية التنفيذ على المتصفح من الشيفرة: setInterval(animate1, 20); // حركة انتقالية مستقلة setInterval(animate2, 20); // في أماكن مختلفة من السكربت setInterval(animate3, 20); ينبغي تجميع عمليات الرسم المستقلة لتسهيل الأمر على المتصفح، ولتخفيف الحِمل على وحدة المعالجة إلى جانب إظهار حركة أكثر نعومةً. تذكر دائمًا أنه يجب ألا نشغل عملية الرسم كل 20 ميلي ثانية، لأنها قد تزيد حمولة وحدة المعالجة، أو لوجود أسباب لتقليل عملية إعادة الرسم، مثل الحالة التي تكون ضمن نافذة مخفية للمتصفح. ولتمييز ذلك في JavaScript سنستخدم ميزةً تُدعى توقيت الحركة Animation timing، والتي تزوّدنا بالدالة requestAnimationFrame التي تتعامل مع هذه الأمور وأكثر، وإليك صيغة استخدامها: let requestId = requestAnimationFrame(callback) تجدول الدالة requestAnimationFrame دالة الاستدعاء callback للعمل في أقرب وقت يريد فيه المتصفح تنفيذ الحركة، فإذا نفَّذنا أي تغييرات على العناصر ضمن الدالة callback، فستُجمّع مع غيرها من دوال الاستدعاء التي تجدولها الدالة requestAnimationFrame ومع رسوميات CSS، وهكذا سيعيد المتصفح الحسابات الهندسية، ثم يعيد الرسم مرةً واحدةً بدلًا من مرات متعددة. يمكن استخدام القيمة requestId التي تعيدها الدالة requestAnimationFrame في إلغاء الاستدعاء: // ألغ تنفيذ الاستدعاءات المجدولة cancelAnimationFrame(requestId); لدالة الاستدعاء callback وسيط واحد، وهو الوقت الذي انقضى منذ بداية تحميل الصفحة مقدرًا بالميكروثانية، والذي يمكن الحصول عليه باستدعاء التابع performance.now. يُنفَّذ الاستدعاء callback مبكرًا إلا في حالة التحميل الزائد للمعالج، أو عندما تقارب بطارية الحاسوب المحمول على النفاد أو لأسباب مشابهة، وتظهر الشيفرة التالية الوقت المستغرق خلال مرات التنفيذ العشرة الأولى للدالة requestAnimationFrame، وهو عادةً بين 10-20 ميلي ثانية: <script> let prev = performance.now(); let times = 0; requestAnimationFrame(function measure(time) { document.body.insertAdjacentHTML("beforeEnd", Math.floor(time - prev) + " "); prev = time; if (times++ < 10) requestAnimationFrame(measure); }) </script> See the Pen JS-p3-05-JavaScript-animations -ex2 by Hsoub (@Hsoub) on CodePen. الرسومات المتحركة المهيكلة Structured animation سننشئ الآن دالةً أكثر عموميةً مبنيةً على الدالة requestAnimationFrame: function animate({timing, draw, duration}) { let start = performance.now(); requestAnimationFrame(function animate(time) { // يتحرك الزمنين 0 و1 let timeFraction = (time - start) / duration; if (timeFraction > 1) timeFraction = 1; // حساب الحالة الراهنة للرسوم المتحركة let progress = timing(timeFraction) draw(progress); // تنفيذ الرسم if (timeFraction < 1) { requestAnimationFrame(animate); } }); } تقبل الدالة animate ثلاثة معاملات تصف الحركة، وهي: duration: الزمن الكلي لتنفيذ الحركة مقدرًا بالميلي ثانية. (timing(timeFraction: دالة توقيت تشابه الخاصية transition-timing-function في CSS، وتعطي نسبة الوقت الذي انقضى (0 عند البداية و1 عند النهاية)، وتعيد ما يدل على اكتمال الحركة، مثل الإحداثي y عند رسم منحني بيزيه، ولنتذكر أن الدالة الخطية تعني أن الحركة ستتقدم بانتظام وبالسرعة ذاتها: function linear(timeFraction) { return timeFraction; } وسيبدو الرسم البياني للدالة الخطية كالتالي: وهي مشابهة تمامًا للخاصية transition-timing-function، وسنرى لاحقًا بعض أشكال الاستخدام الأخرى. (draw(progress: وهي الدالة التي تأخذ معاملًا هو مقدار اكتمال الحركة وترسمه، وتشير القيمة progress=0 إلى حالة بداية الحركة، بينما تشير القيمة progress=1 إلى حالة النهاية، فهي الدالة التي ترسم الحركة فعليًا، إذا يمكنها نقل عنصر مثلًا: function draw(progress) { train.style.left = progress + 'px'; } أو تنفيذ أي شيء آخر، وبالتالي يمكننا تحريك أي شيء بالطريقة التي نريد، لنحرّك العنصر width من 0 حتى 100% باستخدام هذه الدالة: شيفرة الملف animate.js: unction animate({duration, draw, timing}) { let start = performance.now(); requestAnimationFrame(function animate(time) { let timeFraction = (time - start) / duration; if (timeFraction > 1) timeFraction = 1; let progress = timing(timeFraction) draw(progress); if (timeFraction < 1) { requestAnimationFrame(animate); } }); } شيفرة الملف index.html: <!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <style> progress { width: 5%; } </style> <script src="animate.js"></script> </head> <body> <progress id="elem"></progress> <script> elem.onclick = function() { animate({ duration: 1000, timing: function(timeFraction) { return timeFraction; }, draw: function(progress) { elem.style.width = progress * 100 + '%'; } }); }; </script> </body> </html> ستكون النتيجة كالتالي: وإليك الشيفرة المستخدمة: animate({ duration: 1000, timing(timeFraction) { return timeFraction; }, draw(progress) { elem.style.width = progress * 100 + '%'; } }); يمكننا بهذا الأسلوب تنفيذ أي دوال توقيت ورسم على خلاف CSS، ولا ترتبط دوال التوقيت بمنحني بيزيه فقط، كما يمكن للدالة draw أن تتخطى الخصائص إلى إنشاء عناصر جديدة لرسوميات نحتاجها، مثل الألعاب النارية. دوال التوقيت اطلعنا في الفقرات السابقة على أبسط الدوال وهي الدالة الخطية، لنرى الآن بعض الدوال الأخرى، حيث سنجرب بعض الحركات الانتقالية باستخدام دوال توقيت مختلفة. دالة القوة من الدرجة n يمكن استعمال progress بدلالة القوة من الدرجة n لتسريع الحركة، مثل الدالة التربيعية (أي من الدرجة 2): function quad(timeFraction) { return Math.pow(timeFraction, 2) } إليك الرسم البياني: لترى النتيجة انقر على الشكل التالي: كما يمكنك استعمال الدالة التكعيبية (من الدرجة 3)، وسترى أن سرعة الحركة ستزداد بزيادة درجة القوة، إليك نموذجًا تكون فيه progress من الدرجة 5: الدالة المثلثية القطعية arc صيغة الدالة: function circ(timeFraction) { return 1 - Math.sin(Math.acos(timeFraction)); } الرسم البياني: المثال النموذج: دالة إطلاق السهم back عند اطلاق السهم bow shooting فسنسحب وتر القوس ثم نحرره، وخلافًا للدالتين السابقتين، ستعتمد الدالة على معامل إضافي x هو ثابت المرونة elasticity coefficient، والذي يُعرِّف المسافة التي نسحب بها وتر القوس: function back(x, timeFraction) { return Math.pow(timeFraction, 2) * ((x + 1) * timeFraction - x) } الخط البياني للدالة عندما x = 1.5: المثال النموذجي عند نفس القيمة للمعامل x: دالة الارتداد bounce عندما نرمي كرةً فستسقط للأسفل وترتد عدة مرات ثم تتوقف. تسلك الدالة bounce هذا السلوك تمامًا لكن بترتيب معكوس، حيث يبدأ الارتداد مباشرةً، وتستخدم هذه الدالة بعض الثوابت الخاصة: function bounce(timeFraction) { for (let a = 0, b = 1, result; 1; a += b, b /= 2) { if (timeFraction >= (7 - 4 * a) / 11) { return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2) } } } إليك نموذجًا يستخدم دالة الإرتداد: دالة الحركة المرنة elastic إليك دالةً أخرى "مرنةً" تقبل معاملًا إضافيًا x يضبط المجال الابتدائي للحركة: function elastic(x, timeFraction) { return Math.pow(2, 10 * (timeFraction - 1)) * Math.cos(20 * Math.PI * x / 3 * timeFraction) } الخط البياني للدالة عندما x=1.5: إليك نموذجًا عن استخدام الدالة: الدوال بترتيب معكوس تعرفنا حتى اللحظة على دوال التوقيت التي تُعرف بتحويلات الدخول السهل easeIn، لكننا قد نحتاج أحيانًا إلى عرض الحركة بترتيب معكوس. تُنفَّذ هذه الحركات بالتحويل الذي يُعرف باسم الخروج السهل easeOut. التحويل easeOut تُوضع الدالة timing في هذا التحويل ضمن المُغلِّف timingEaseOut: timingEaseOut(timeFraction) = 1 - timing(1 - timeFraction) بعبارة أخرى لدينا دالة التحويل makeEaseOut التي تقبل دالة توقيت نظاميةً وتعيد المُغلّف الذي يحيط بها: // accepts a timing function, returns the transformed variant function makeEaseOut(timing) { return function(timeFraction) { return 1 - timing(1 - timeFraction); } } يمكن على سبيل المثال اختيار الدالة bounce التي شرحناها سابقًا وتطبيقها: let bounceEaseOut = makeEaseOut(bounce); وهكذا لن تكون الحركة الارتدادية في بداية الحركة بل في نهايتها، وستبدو الحركة أفضل: شيفرة الملف style.css: #brick { width: 40px; height: 20px; background: #EE6B47; position: relative; cursor: pointer; } #path { outline: 1px solid #E8C48E; width: 540px; height: 20px; } شيفرة الملف index.html: <!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="style.css"> <script src="https://js.cx/libs/animate.js"></script> </head> <body> <div id="path"> <div id="brick"></div> </div> <script> function makeEaseOut(timing) { return function(timeFraction) { return 1 - timing(1 - timeFraction); } } function bounce(timeFraction) { for (let a = 0, b = 1, result; 1; a += b, b /= 2) { if (timeFraction >= (7 - 4 * a) / 11) { return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2) } } } let bounceEaseOut = makeEaseOut(bounce); brick.onclick = function() { animate({ duration: 3000, timing: bounceEaseOut, draw: function(progress) { brick.style.left = progress * 500 + 'px'; } }); }; </script> </body> </html> وستكون النتيجة: سنرى هنا كيف سيغيّر التحويل سلوك الدالة، إذ ستعرَض التأثيرات الموجودة في بداية الحركة -مثل الارتداد- في النهاية، ففي الشكل التالي ستجد الارتداد الاعتيادي باللون الأحمر، والارتداد وفق تحويل الخروج السهل باللون الأزرق. الارتداد الاعتيادي: يرتد الكائن عند القاع، ثم يقفز بحدّة إلى القمة في النهاية. باستخدام easeOut الخروج السهل: يقفز أولًا إلى القمة ثم يرتد هناك. التحويل easeInOut يمكن أن نُظهر التأثير المطلوب في بداية ونهاية الحركة معًا، ويُدعى هذا التحويل بالدخول والخروج السهل easeInOut. سنحسب حالة الحركة باعتماد تابع توقيت ما كالتالي: if (timeFraction <= 0.5) { // نصف الحركة الأول return timing(2 * timeFraction) / 2; } else { // النصف الثاني للحركة return (2 - timing(2 * (1 - timeFraction))) / 2; } شيفرة المُغلِّف: function makeEaseInOut(timing) { return function(timeFraction) { if (timeFraction < .5) return timing(2 * timeFraction) / 2; else return (2 - timing(2 * (1 - timeFraction))) / 2; } } bounceEaseInOut = makeEaseInOut(bounce); إليك مثالًا نموذجيًا: شيفرة الملف style.css: #brick { width: 40px; height: 20px; background: #EE6B47; position: relative; cursor: pointer; } #path { outline: 1px solid #E8C48E; width: 540px; height: 20px; } شيفرة الملف index.html: <!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="style.css"> <script src="https://js.cx/libs/animate.js"></script> </head> <body> <div id="path"> <div id="brick"></div> </div> <script> function makeEaseInOut(timing) { return function(timeFraction) { if (timeFraction < .5) return timing(2 * timeFraction) / 2; else return (2 - timing(2 * (1 - timeFraction))) / 2; } } function bounce(timeFraction) { for (let a = 0, b = 1, result; 1; a += b, b /= 2) { if (timeFraction >= (7 - 4 * a) / 11) { return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2) } } } let bounceEaseInOut = makeEaseInOut(bounce); brick.onclick = function() { animate({ duration: 3000, timing: bounceEaseInOut, draw: function(progress) { brick.style.left = progress * 500 + 'px'; } }); }; </script> </body> </html> وستكون النتيجة: يدمج التحويل كائنين رسوميين معًا، وهما التحويل easeIn (الاعتيادي) في النصف الأول للحركة، والتحويل easeOut (المعكوس) للنصف الثاني. سنرى التأثير بوضوح عند الموازنة بين التحويلات الثلاث easeIn وeaseOut وeaseInOut عند تطبيقها على دالة التوقيت circ: easeIn: باللون الأحمر. easeOut: باللون الأخضر. easeInOut: باللون الأزرق. كما نرى طبقنا على النصف الأول من الحركة easeIn، وعلى النصف الآخر easeOut، لذا ستبدأ الحركة وتنتهي بنفس التأثير. دالة draw أكثر تميزًا يمكننا القيام بأكثر من مجرد تحريك العنصر، وكل ما علينا فعله هو كتابة شيفرة مناسبة للدالة draw، إليك طريقةً لإظهار ارتداد أثناء كتابة نص مثلًا: شيفرة الملف style.css: textarea { display: block; border: 1px solid #BBB; color: #444; font-size: 110%; } button { margin-top: 10px; } شيفرة الملف index.html: <!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="style.css"> <script src="https://js.cx/libs/animate.js"></script> </head> <body> <textarea id="textExample" rows="5" cols="60">He took his vorpal sword in hand: Long time the manxome foe he sought— So rested he by the Tumtum tree, And stood awhile in thought. </textarea> <button onclick="animateText(textExample)">Run the animated typing!</button> <script> function animateText(textArea) { let text = textArea.value; let to = text.length, from = 0; animate({ duration: 5000, timing: bounce, draw: function(progress) { let result = (to - from) * progress + from; textArea.value = text.substr(0, Math.ceil(result)) } }); } function bounce(timeFraction) { for (let a = 0, b = 1, result; 1; a += b, b /= 2) { if (timeFraction >= (7 - 4 * a) / 11) { return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2) } } } </script> </body> </html> وستكون النتيجة كالتالي: خلاصة يمكن أن تساعدك JavaScript في تنفيذ الرسوميات التي لا تستطيع CSS التعامل معها، أو تلك التي تتطلب تحكمًا دقيقًا. تّنفّذ حركات JavaScript باستخدام التابع المدمج requestAnimationFrame الذي يسمح بإعداد دالة استدعاء تُشغَّل عندما يحضّر المتصفح نفسه لعملية إعادة الرسم Repaint. لن تُنفَّذ عملية إعادة الرسم إطلاقًا عندما تكون الصفحة في الخلفية، وبالتالي لن تعمل دالة الاستدعاء وسيُعلّق تنفيذ الحركة، ولن يكون هناك استهلاك للموارد. إليك الدالة المساعدة animate التي تحضّر معظم الرسوميات المتحركة التي تحتاجها: function animate({timing, draw, duration}) { let start = performance.now(); requestAnimationFrame(function animate(time) { // timeFraction goes from 0 to 1 let timeFraction = (time - start) / duration; if (timeFraction > 1) timeFraction = 1; // calculate the current animation state let progress = timing(timeFraction); draw(progress); // draw it if (timeFraction < 1) { requestAnimationFrame(animate); } }); } الخيارات هي: duration: الزمن الكلي للحركة مقدرًا بالميلي ثانية. timing: الدالة التي تحسب مقدار تقدم الحركة، حيث تقبل الدالة قيمًا زمنيةً هي نسبة بين 0 و1، وتعيد مقدار تقدم العملية. draw: الدالة التي ترسم الحركة. وبالطبع يمكن تحسين هذه الدوال وإضافة العديد من الأمور الأخرى، لكن لا يتكرر استخدام رسوميات JavaScript كثيرًا، فهي تستخدَم لإظهار شيء مهم لا لأمور تقليدية، لذا يمكنك إضافة الميزة التي تريدها عند الحاجة. يمكن أن تستخدم JavaScript أي دوال توقيت، وقد غطينا الكثير منها في هذا الفصل وطبقنا عليها تحويلات عدةً، إذًا لسنا مقيدين بمنحنيات بيزيه كما هي الحال في CSS. يمكننا استخدام draw لتحريك أي شيء وليس خصائص CSS فقط. مهام لإنجازها 1. تحريك كرة مرتدة أنشئ كرةً ترتد كما في المثال التالي: افتح المثال في بيئة تجريبية. الحل لجعل الكرة ترتد، سنسنتعمل الخاصية top والخاصية position:absolute مع الكرة والخاصية position:relative مع الملعب، ويكن إحداثيات أرضية الملعب هي field.clientHeight. تشير الخاصية top إلى بداية أعلى الملعب لذا يجب أن تتغير من 0 إلى field.clientHeight - ball.clientHeight وهو أدنى موضع يمكن أن تنخفض إليه حافة الكرة العلوية. يمكن تطبيق تأثير الارتداد باستعمال دالة التوقيت bounce في الوضع easeOut. إليك الشيفرة النهاية الناتجة: let to = field.clientHeight - ball.clientHeight; animate({ duration: 2000, timing: makeEaseOut(bounce), draw(progress) { ball.style.top = to * progress + 'px' } }); افتح الحل في بيئة تجريبية. 2. حرك الكرة المرتدة إلى اليمين اجعل الكرة ترتد إلى اليمين كالتالي: اكتب الشيفرة بحيث تكون مسافة الانتقال إلى اليمين هي 100px. الحل احتجنا في التمرين السابق إلى تحريك خاصية واحدة فقط، بينما سنحتاج في هذا التمرين إلى تحريك خاصية إضافية هي elem.style.left. نريد تغيير الاحداثيات الأفقية للكرة بزيادتها تدريجيًا نحو اليمين أثناء السقوط، لذا سنضيف حركة إضافة anumate يمكن أن نستعمل معها دالة التوقيت linear ولكن تبدو makeEaseOut(quad) أفضل بكثير. إليك الشيفرة النهاية الناتجة: let height = field.clientHeight - ball.clientHeight; let width = 100; // animate top (bouncing) animate({ duration: 2000, timing: makeEaseOut(bounce), draw: function(progress) { ball.style.top = height * progress + 'px' } }); // animate left (moving to the right) animate({ duration: 2000, timing: makeEaseOut(quad), draw: function(progress) { ball.style.left = width * progress + "px" } }); افتح الحل في بيئة تجريبية. ترجمة -وبتصرف- للفصل JavaScript Animation من سلسلة The Modern JavaScript Tutorial اقرأ أيضًا المقال السابق: إنشاء رسوم متحركة باستخدام CSS النسخة العربية الكاملة لكتاب: التحريك عبر CSS iframe { border: 1px solid #e7e5e3 !important; width: 100%; } iframe div#path { margin: auto; }
-
لربما تكون قد سمعت عبارات مثل هذه: "ليس لدينا متسع من الوقت لاختبار (النسخة أو مسار المستخدم أو التصميم أو الميزات…) الآن، سنصدر المنتج حاليًا ونتابع لاحقًا ردود أفعال العملاء" إن عَملت سابقًا على تجربة مستخدم UX. يبني المطورون منتجاتهم بسرعة أكبر مما مضى، ويُفترض من المصممين متابعتهم خطوةً بخطوة. وعلى الرغم من فهم المصممين لضرورة اختبار تصاميمهم في قرارة أنفسهم، تتجاهل معظم المنظمات المعنية هذا الأمر، إذ ينبغي على المصممين التنقل المضني في دوائر المعطيات والأفكار حول مواضيع تصاميمهم للوصول إلى رصيد فعال من الأبحاث، ومع ذلك يباشرون أعمالهم بكمٍ غير مثالي من البيانات المطلوبة. عَمِلتُ -قبل أن أكون مديرًا تنفيذيًا وأحد مؤسسي شركة Maze- رئيسًا لمصممي تجربة مستخدم لعملاء مثل McKinsey وRocket Internet وPSG، وأدركت منذ ذلك الوقت حاجة المصممين إلى إطار عمل يُعتمد عليه، بحيث يساعد في تحديد المدخلات بسرعة في مرحلة مبكرة، وغالبًا أثناء عملية التصميم، وهكذا جمّعنا كل هذه الأفكار معًا في Maze وسنطلعكم على تفاصيل ذلك في هذه المقالة. إطار عمل الاختبار السريع: تعريفه وفوائده طورنا إطار عمل الاختبار السريع Rapid Testing Framework لنمنح فرق التصميم والإنتاج والتسويق أفكارًا قابلة للتطبيق في مختلف المراحل. فكل قرار يُتخذ أثناء عملية تطوير المنتج من اختيار وتطوير الميزات الصحيحة وتصميم مسار المستخدم User flow أو كتابة نسخة لتجربة المستخدم هي قرارات يمكن اختبارها ولا بدّ من تقييمها من قبل المستخدمين. سنتحدث في الفقرات القادمة عمّا تقدمه الاختبارات لفريق التصميم. الحصول على بعض الإضاءات خلال عملية التصميم تبدأ عملية التصميم قبل أن تبدأ حتى بتنفيذ أية رسومات أو مخططات، وغالبًا ما تبدأ عندما يناقش الفريق الافتراضات المطروحة حول ما يتوقعه المستخدم وما يحتاجه. يتيح لك إشراك المستخدمين والحصول على آرائهم باكرًا خلال عملية التصميم، إمكانية الغوص في تفاصيل المشاكل وتفادي الخوض في الكثير من الحلول المتوقعة، فكلما سعيت في إشراك مستخدمي منتجك مبكرًا، ستحصل على آرائهم وتجني الفوائد المرجوة منها في تحسين منتجك بسرعة أكبر. وكما تشير Helen Tsvirinka -مصممة المنتجات في شركة Shopify-، فإن الاختبارات سترفع عائدات استثمارك كثيرًا موازنةً باعتمادك على الافتراضات فقط. تقييم التجارب بمساعدة المستخدمين قبل إنهاء المنتج إن لم تعرض منتجك أثناء بنائه على المستخدمين، فسينتهي الأمر بفريقك إلى هدر الوقت والموارد على بناء ميزات عديمة الفائدة أو ميزات قد لا تعمل، فإن كنت ستطلع على آراء مستخدميك بعد أن تطرح منتجك، فستخسر نتيجة سباق بأكمله. إنّ وضع ما تبنيه في أيدي مستخدميك باكرًا وفي مرحلة التصميم عادةً، يعني أنك قادر على تقييم تجربة العملاء قبل إنهاء منتجك. تسمح الاختبارات لك ولفريقك أن تنتقل من مرحلة الفكرة إلى التصميم النهائي وأنت واثق أنك عالجت مسألةً واقعيةً ووجدت لها حلًا فعالًا. بناء قاعدة معرفية حول ما ينفع وما لا ينفع كلما اختبرت منتجك أكثر أثناء عملية التصميم، حزت على معلومات أكثر عن مستخدميك ومتطلباتهم وما ينفع منتجك وما لا ينفعه. وعلى الرغم من كم البيانات التي ستقدمه الاختبارات لك ولفريقك، فقد تكون هذه البيانات موزعةً داخل منظومة العمل مثل غيرها من الإضاءات والأفكار التي تحملها الفرق المختلفة أو أفراد الفريق الواحد. لن تمنحك الاختبارات السريعة فوائدًا على المدى القصير فقط عندما تسمح لك ببناء تجربة مستخدم مميزة، لكنها قيِّمة على المدى الطويل أيضًا، لأنها تزودك بطريقة معيارية لتوثيق ما تجده نافعًا وما تجده خلاف ذلك. كيف تستخدم إطار عمل الاختبار السريع في اختبار أي تجربة مستخدم لا تقتصر فائدة إطار عمل الاختبار السريع Rapid Testing على فريق التصميم، بل على عمل المنتج الأساسي للمؤسسة، إذ يتيح لكل طواقم العمل من المسوقين إلى المديرين تقييم عملياتهم، وبالتالي التقليل من الوقت المهدور على بناء ما هو خاطئ. استخدم الحلقة التكرارية Input-Objective-Test-Analysis واختصارًا IOTA في اتخاذ قراراتك، وإليك طريقة عملها: المدخلات Input المدخل هو الحالة التي تحتاج فيها إلى اتخاذ قرار، مثل الميزة التي نصممها أو النسخة التي سنستخدمها أو النموذج الأولي الذي سنبني عليه. وإليك بعض التساؤلات التي تدل على المدخلات القابلة للاختبار: ما هي الوظيفة التي تقدمها الميزة لمستخدميك؟ هل من السهل التنقل ضمن مسار المستخدم؟ هل التصميم سهل الاستخدام؟ هل النسخة المطروحة جاهزة لهذا العمل؟ الهدف Objective سيكون تحديد الهدف هو الخطوة التالية لتحديد المدخلات، وهو النقطة التي ستكون عند بلوغها قادرًا على تقييم القرار الذي اتخذته، فقد يكون هدفك مثلًا هو إكمال 80% على الأقل من المشاركين في الاختبار كل المهام، عند اختبار قابلية استخدام تصميم. إن لم تحقق الهدف من أول اختبار، فكرر العملية مجددًا حتى ينجح 80% من المشاركين في إتمام جميع المهام وفق التصميم الذي تبنيه. فإما أن تنتقل في نهاية الأمر إلى المرحلة التالية من العملية، أو أن تعود إلى لوحة الرسم لتتعمق أكثر في تفاصيل التجربة التي تقدمها لمستخدميك. الاختبار Test يأتي تاليًا تحديد طريقة العمل المثلى وفقًا للهدف الذي اعتمدته، فإن كنت ستختبر مثلًا الانطباع الأول عن علامتك التجارية الجديدة وملاحظة العملاء لوجودها، فسيكون اختبار الثواني الخمسة أو مقابلة تسأل أسئلةً فضفاضةً مناسبين جدًا. التحليل Analysis ستقيّم أخيرًا نتائج الاختبار وتوازنها مع الهدف المنشود، فإن حققت نتائجك الهدف، فستتمكن من الانتقال إلى الخطوة التالية في عملية التصميم. بينما عليك العودة إلى الحل الذي اعتمدته لتصميمك إن لم تكن النتائج مقنعة، ثم تلافي العيوب حتى تصل إلى هدفك أو أن تلغي القرار باعتماد هذا التصميم. مثال عن استخدام إطار العمل IOTA لنلق نظرةً على تجربتنا في Maze عندما حاولنا إنشاء تطبيق يستخدم صفحةً لعرض حالات تصميمية قد تلهم المصممين لإنشاء المزيد من المشاريع عند اشتراكهم في التطبيق. سنعرض لكم تاليًا كيف سخّرنا إطار العمل IOTA لاتخاذ القرار بالمضي قًدمًا في تصميمنا: المدخلات: كان مُدخلنا هو: هل ستلهم هذه الصفحة المستخدمين بعد رؤيتها في تطبيقنا لتصميم مشاريع أكثر؟ الهدف: كان هدفنا هو مشاركة 60% على الأقل من المستخدمين الذين زاروا الصفحة، وذلك بإنشائهم مشروعًا جديدًا عليها. الاختبار: صممنا نموذجًا أوليًا واختبرناه على المستخدمين الموجودين لنعرض حالة مسار المستخدم الذي بنيناه في الصفحة. التحليل: وجدنا عند تحليل النتائج أنّ غالبية المستخدمين لم يفهموا الغرض من بناء هذه الصفحة، وقررنا عندها أنّ الصفحة لا تهم مستخدمينا ولا يحتاجونها، وبالتالي أوقفنا تطويرها. أفضل الممارسات لاعتماد معايير خاصة بالاختبارات بعد أن أضفنا الاختبارات إلى مراحل التصميم بالاعتماد على عملاء Maze، وصلتُ إلى خلاصة لأفضل ما يمكن تطبيقه من ممارسات لتسريع الاختبارات ضمن مؤسستنا. وإليك بعض الممارسات المفتاحية لاعتماد معايير الاختبارات لمؤسستك: 1. إيصال فائدة الاختبار من الأمور الأساسية إيصال الفائدة من اختبارك باكرًا إلى أعضاء فريقك وإلى المديرين التنفيذيين والمهتمين بمنتجك. ابدأ بالاطلاع على خبرة أعضاء فريقك السابقة في الاختبارات، فقد تجد بعض النقص الذي عليك ترميمه. إليك بعض الأفكار لتبدأ: نظّم مناسبات أو مناقشات عبر الإنترنت لتشارك أهمية هذا الاختبار. اعرض أمثلةً مفتاحيةً تدل على الفائدة التي أتاحها الاختبار للمنتج ولفريق التصميم لتحقيق أهدافهم. اطلب المساعدة من المهتمين وأعضاء الفريق من خلال الانخراط في مقابلات أو استطلاعات رأي أو اختبارات استخدام لمنتجك. اعرض بانتظام الأمور المهمة التي استخلصتها على قنوات واسعة الانتشار. 2. تفهم إطارات عمل اتخاذ القرار إنّ معرفة نقاط القوة في صنع القرار داخل مؤسستك وكيفية استغلالها هي طريقة أخرى لاتخاذ القرارات. وكما يقول بيزود سيرجاني مؤسس Yet Another Studio، والمدير السابق لقسم الأبحاث وعمليات التحليل في Stack: "ينبغي أن نوجه البحث نحو قرار، فمعرفة ما الذي ستقرره سيساعدك على فهم ما تبحث عنه وكيف ستُجري هذا البحث". قد لا تتمكن من التأثير على كل القرارات في البداية، لكن مع بعض الفرص ستكون قادرًا على إيصال الفائدة من الاختبارات التي يساهم فيها المستخدمون. 3. ابن مجتمعا من المتابعين أو استفد من تلك الموجودة أصلا يعزز بناء مجتمعات المتابعين الذين ينخرطون في الاختبارات السريعة الفرص التي قد تزيد من إمكاناتك. لقد أنشأنا في Maze لجنةً استشاريةً للعملاء Customer Advisory Board نتواصل من خلالها مع الأعضاء عند اتخاذ قرارات استراتيجية، أو اختبار ميزة جديدة، أو عند تقييم أفكارنا. يمكنك بناء مجتمع للاختبارات من خلال إعداد مجتمع على Slack أو أنا، أو أية قناة أخرى تفضلها. ألق نظرةً إن شئت على Sketch Labs مثلًا، أو على Freetrade’s Community. الاختبار والتعلم أساس نجاح الفريق لمسنا منذ أن تبنينا إطار عمل الاختبار السريع توسّع آفاق فرق الإنتاج من الأفكار المحدودة والمنعزلة إلى منظومة تعلّم تضم كافة الفرق. لقد ساعد ذلك الجميع من مصممين وباحثين ومديري إنتاج ومسوقين في الحصول على الأفكار والإرشادات والتعاون في التعلّم والبحث، وأجد ذلك طريقةً حقيقيةً لنشر ديمقراطية الوصول إلى البيانات وتكوين مؤسسة قادرة على دفع أعضائها نحو مستقبل أكثر إشراقًا في بحثهم عن أفضل السبل لتلبية حاجات المستخدمين. ترجمة -وبتصرف- للمقال How to rapidly test any experience لكاتبه JONATHAN WIDAWSKI. اقرأ أيضًا كيفية استخدام اختبارات تجربة المستخدم لتحسين قيمة استمرار التعامل مع العميل CLV طرق مثبتة لتخصيص تجربة المستخدم كيفية تحسين إعدادات تطبيق بناء على تجربة المستخدم الفرق بين إمكانية الوصول والشمولية في تصميم تجربة المستخدم كيفية إجراء بحث في تجربة المستخدم لتحسين مشاريعك تصميم تجربة تهيئة المستخدم User Onboarding
-
يمكن استخدام لغة الأنماط الانسيابية CSS في إنشاء رسوم متحركة بسيطة، دون الحاجة إلى شيفرة JavaScript إطلاقًا، لكن يمكننا بالطبع استخدام JavaScript للتحكم برسوم CSS المتحركة وجعلها أفضل بكتابة القليل من الشيفرة. الحركة الانتقالية في CSS إنّ فكرة الحركة الانتقالية transition في CSS بسيطة جدًا، حيث نصف خاصيةً محددةً وآليةً لإظهار التغيرات فيها على شكل رسوم متحركة، فعندما تتغير قيمة الخاصية سيُظهر المتصفح هذا التغيير في حركة، وبالتالي كل ما علينا فعله هو تغيير الخاصية، وسينفذ المتصفح دفقًا من التغيرات المتتالية فيها. تُحرّك شيفرة CSS التغييرات في الخاصية background-color لمدة ثلاث ثوان: .animated { transition-property: background-color; transition-duration: 3s; } فلو كان لأي عنصر الصنف CSS الذي سميناه animated، فستظهر أي تغيرات على الخاصية background-color في هذا العنصر في حركة لمدة 3 ثوان. انقر على الزر الذي ستُظهره الشيفرة التالية لتحريك الخلفية: <button id="color">Click me</button> <style> #color { transition-property: background-color; transition-duration: 3s; } </style> <script> color.onclick = function() { this.style.backgroundColor = 'red'; }; </script> See the Pen JS-P3-03-Fetch-Download-progress-ex8 by Hsoub (@Hsoub) on CodePen. توصف الحركة الانتقالية في CSS بأربعة خصائص، هي: transition-property. transition-duration. transition-timing-function. transition-delay. سنشرح هذه الخصائص بعد قليل، لكن دعونا نلاحظ أنّ الخاصية transition ستسمح بالتصريح عن الخصائص الأربعة السابقة معًا وفق الترتيب التالي:property duration timing-function delay، بالإضافة إلى قدرتها على إظهار الحركة الانتقالية على عدة خصائص معًا. يُظهر النقر على الزر في الشيفرة التالية الحركة الانتقالية للخاصيتين color وfont-size: <button id="growing">Click me</button> <style> #growing { transition: font-size 3s, color 2s; } </style> <script> growing.onclick = function() { this.style.fontSize = '36px'; this.style.color = 'red'; }; </script> See the Pen JS-P3-05-CSS-animations-ex2 by Hsoub (@Hsoub) on CodePen. سنشرح الآن خصائص الحركة الانتقالية. الخاصية transition-property نكتب ضمن هذه الخاصية قائمةً بخصائص CSS التي نريد إظهار تغييراتها على شكل حركة انتقالية، مثل left وmargin-left وheight وcolor وغيرها، أو يمكن أن نختار all التي تعني تحريك كل الخصائص . توجد خصائص لا يمكن تحريكها، لكن معظم الخصائص شائعة الاستعمال وقابلة للتحريك. الخاصية transition-duration يمكن أن نحدد في هذه الخاصية المدة التي ستجري فيها الحركة الانتقالية، وينبغي أن يكون التوقيت بتنسيق CSS ومقدرًا بالثانية s أو بالميلي ثانية ms. الخاصية transition-delay نحدد في هذه الخاصية فترة الانتظار قبل عرض الحركة الانتقالية، فلو كانت قيمتها ثانيةً واحدةً وقيمة الخاصية transition-duration ثانيتن، فستُعرض الحركة الانتقالية بعد ثانية من تغيّر الخاصيّة، وستستمر لمدة ثانيتين، كما يمكن استخدام قيم سالبة أيضًا، وعندها ستبدأ الحركة مباشرةً، إلا أنّ نقطة البداية ستنسحب قليلًا، فلو كانت قيمة الخاصية transition-delay هي "-1" ثانية وقيمة الخاصية transition-duration هي "2" ثانية، فستبدأ الحركة من المنتصف، ولمدة ثانية واحدة. لاحظ تغير الأعداد من 0 إلى 9 في الرسم المتحرك التالي باستخدام الخاصية translate في CSS: شيفرة الملف script.js: stripe.onclick = function() { stripe.classList.add('animate'); }; شيفرة الملف style.css: #digit { width: .5em; overflow: hidden; font: 32px monospace; cursor: pointer; } #stripe { display: inline-block } #stripe.animate { transform: translate(-90%); transition-property: transform; transition-duration: 9s; transition-timing-function: linear; } شيفرة الملف index.html: <!doctype html> <html> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="style.css"> </head> <body> Click below to animate: <div id="digit"><div id="stripe">0123456789</div></div> <script src="script.js"></script> </body> </html> وستكون النتيجة: تجري الحركة الانتقالية للخاصية transform بالشكل التالي: #stripe.animate { transform: translate(-90%); transition-property: transform; transition-duration: 9s; } تضيف شيفرة JavaScript السابقة الصنف animate إلى العنصر، وتبدأ الحركة عند تنفيذ الأمر: stripe.classList.add('animate'); يمكن أن نبدأ التحريك انطلاقًا من نقطة ما من الحركة الانتقالية، أي من رقم محدد -وقد يكون متعلقًا بالثانية الحالية مثلًا- وباستخدام قيمة سالبة للخاصية transition-delay. لو نقرت على الرقم في المثال التالي، فستبدأ الحركة من الثانية الحالية: شيفرة الملف script.js: stripe.onclick = function() { let sec = new Date().getSeconds() % 10; stripe.style.transitionDelay = '-' + sec + 's'; stripe.classList.add('animate'); }; شيفرة الملف style.css: #digit { width: .5em; overflow: hidden; font: 32px monospace; cursor: pointer; } #stripe { display: inline-block } #stripe.animate { transform: translate(-90%); transition-property: transform; transition-duration: 9s; transition-timing-function: linear; } شيفرة الملف index.html: <!doctype html> <html> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="style.css"> </head> <body> Click below to animate: <div id="digit"><div id="stripe">0123456789</div></div> <script src="script.js"></script> </body> </html> وستكون النتيجة: يمكن تنفيذ ذلك باستخدام JavaScript لكن بإضافة سطر آخر من الشيفرة: stripe.onclick = function() { let sec = new Date().getSeconds() % 10; // مثلًا تعني القيمة -3 هنا أن الحركة ستبدأ من الثانية الثالثة stripe.style.transitionDelay = '-' + sec + 's'; stripe.classList.add('animate'); }; الخاصية transition-timing-function تصف دالة التوقيت هذه كيفية توزيع الحركة الانتقالية أثناء فترة العرض، كأن تبدأ ببطء ثم تسرع، أو العكس. ستبدو هذه الخاصية في البداية معقدة، لكنها ستغدو بسيطةً إذا فهمناها، كما ستقبل نوعين من القيم هما منحني بيزيه Bezier curve، أو الدالة steps، لنبدأ مع المنحني كونه الأكثر استخدامًا. منحني بيزيه يمكن إعداد منحنى بيزيه بأربعة نقاط حاكمة تحقق الشروط التالية: إحداثيات النقطة الأولى (0,0). إحداثيات النقطة الأخيرة (1,1). ينبغي أن يكون الإحداثي x لنقطتي المنتصف بين 0 و1، بينما يمكن أن نختار أي قيمة للإحداثي y. إن صيغة منحني بيزيه هي: (cubic-bezier(x2, y2, x3, y3، إذًا علينا فقط أن نحدد النقطتين الحاكمتين الثانية والثالثة، لأنّ الأولى والأخيرة ثابتتان. تحدد دالة التوقيت سرعة تحريك عملية تشكيل المنحني: يمثل المحور x محور الزمن الذي يبدأ بالنقطة "0"، وينتهي بالنقطة "1" لقيمة transition-duration. يمثل المحور y مقدار اكتمال العملية، ويبدأ بالنقطة "0" وينتهي بالنقطة "1" لقيمة الخاصية التي نريد إظهار الحركة الانتقالية لها. المثال الأبسط هو التحريك المنتظم، أي الخطي، وذلك باستخدام المنحني (cubic-bezier(0, 0, 1, 1، الذي سيبدو بالشكل التالي: عندما يمر الوقت (x) ستكتمل عملية التحريك (y) بثبات وتناغم من القيمة 0 إلى 1. وفي مثالنا التالي ستتحرك صورة القطار من اليسار إلى اليمين بسرعة ثابتة، إذا نقرت عليه: شيفرة الملف style.css: .train { position: relative; cursor: pointer; width: 177px; height: 160px; left: 0; transition: left 5s cubic-bezier(0, 0, 1, 1); } شيفرة الملف index.html: <!doctype html> <html> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="style.css"> </head> <body> <img class="train" src="https://js.cx/clipart/train.gif" onclick="this.style.left='450px'"> </body> </html> وستكون النتيجة: إليك شيفرة استخدام الخاصية transition بناءً على منحني بيزيه: .train { left: 0; transition: left 5s cubic-bezier(0, 0, 1, 1); /* JavaScript sets left to 450px */ } لكن كيف سنرى القطار وهو يُبطئ؟ يمكن استعمال منحني بيزيه آخر من الشكل (cubic-bezier(0.0, 0.5, 0.5 ,1.0، وفي المقال التالي سنرى كيفية بدء العملية بسرعة، حيث يتحرك القطار سريعًا ثم يتباطأ. شيفرة الملف style.css: .train { position: relative; cursor: pointer; width: 177px; height: 160px; left: 0px; transition: left 5s cubic-bezier(0.0, 0.5, 0.5, 1.0); } شيفرة الملف index.html: <!doctype html> <html> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="style.css"> </head> <body> <img class="train" src="https://js.cx/clipart/train.gif" onclick="this.style.left='450px'"> </body> </html> وستكون النتيجة: شيفرة CSS: .train { left: 0; transition: left 5s cubic-bezier(0, .5, .5, 1); /* JavaScript sets left to 450px */ } يمكنك استخدام العديد من المنحنيات الجاهزة، مثل linear أو ease أو ease-in أو ease-out أو ease-in-out، وإليك جدولًا بالمنحنيات الموافقة لها: وفي ملاحظة هامة، تجدر الإشارة إلى أن * تستخدَم ease قيمةً افتراضيةً إذا لم نحدد دالة توقيت timing-function. وكما هو واضح يمكن استعمال ease-out في حالة القطار الذي يُبطئ حركته: .train { left: 0; transition: left 5s ease-out; /* transition: left 5s cubic-bezier(0, .5, .5, 1); */ } لكنه يبدو مختلفًا نوعًا ما. قد تتجاوز الحركة الانتقالية المدى عند استخدام منحني بيزيه. ويمكن أن تحمل النقاط الحاكمة في منحني بيزيه أي قيمة للإحداثي y حتى القيم السالبة أو الضخمة، وبهذا قد يمتد المنحني على مساحة ضيقة جدًا أو واسعة جدًا، وقد تتجاوز الرسوم المتحركة مداها الطبيعي. لاحظ الشيفرة التالية: .train { left: 100px; transition: left 5s cubic-bezier(.5, -1, .5, 2); /* JavaScript sets left to 400px */ } ينبغي إظهار حركة انتقالية للخاصية left بين القيمتين 100px و400px، لكنك بالنقر على صورة القطار سترى ما يلي: أولًا سيتراجع القطار إلى الخلف، وستغدو قيمة left أقل من 100px. يتقدم بعدها القطار إلى الأمام لتتجاوز left القيمة 400px بقليل. ثم يعود مجددًا إلى القيمة 400px للخاصية left. شيفرة الملف style.css: .train { position: relative; cursor: pointer; width: 177px; height: 160px; left: 100px; transition: left 5s cubic-bezier(.5, -1, .5, 2); } شيفرة الملف index.html: <!doctype html> <html> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="style.css"> </head> <body> <img class="train" src="https://js.cx/clipart/train.gif" onclick="this.style.left='400px'"> </body> </html> وستكون النتيجة على النحو الآتي: ما جرى واضح جدًا إذا نظرنا إلى رسم منحني بيزيه لهذه الحركة، فقد نقلنا قيمة الإحداثي y للنقطة الثانية تحت الصفر، وجعلنا قيمته بالنسبة للنقطة الثالثة تتجاوز 1، لذا سيخرج شكل المنحني عن الشكل النظامي للمنحني من الدرجة الرابعة، فقيمة y خارج المجال المعياري بين 0 و1. وكما نعرف، تقيس y مدى اكتمال الحركة الانتقالية، حيث تتطابق القيمة y = 0 مع بداية الحركة، والقيمة y = 1 مع نهايتها. إذًا ستنقل القيمة y<0 نقطة البدء إلى ما قبل البداية، والقيمة y>1 إلى ما بعدها. يُعَد هذا التغيير بسيطًا طبعًا، فلو ضبطنا y على القيمة 99 أو -99، فسيقفز القطار بسرعة أكبر بكثير خارج المجال، لكن كيف سننشئ منحني بيزيه من أجل مهمة معينة؟ هنالك أدوات كثيرة، كما يمكن تنفيذه ضمن مواقع عديدة على الإنترنت. دالة التوقيت steps تسمح دالة التوقيت ([steps(number of steps[, start/end بتقسيم الحركة الانتقالية إلى خطوات، لنرى ذلك من خلال مثال قائمة من الأرقام التي لا تبدي أي حركة: شيفرة الملف style.css: #digit { border: 1px solid red; width: 1.2em; } #stripe { display: inline-block; font: 32px monospace; } شيفرة الملف index.html: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="style.css"> </head> <body> <div id="digit"><div id="stripe">0123456789</div></div> </body> </html> وستكون النتيجة: سنُظهر الأرقام منفصلة، وذلك بجعلها غير مرئية خارج النافذة الحمراء، بعدها سنحرك القائمة نحو اليسار في كل خطوة، وبالتالي سنحتاج إلى 9 خطوات لتنفيذ الأمر: #stripe.animate { transform: translate(-90%); transition: transform 9s steps(9, start); } شيفرة الملف style.css: #digit { width: .5em; overflow: hidden; font: 32px monospace; cursor: pointer; } #stripe { display: inline-block } #stripe.animate { transform: translate(-90%); transition-property: transform; transition-duration: 9s; transition-timing-function: steps(9, start); } شيفرة الملف index.html: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="style.css"> </head> <body> Click below to animate: <div id="digit"><div id="stripe">0123456789</div></div> <script> digit.onclick = function() { stripe.classList.add('animate'); } </script> </body> </html> وستكون النتيجة: يُمثّل الوسيط الأول للدالة (steps(9, start عدد الخطوات، أي ستقسم الحركة الانتقالية إلى 9 أجزاء -10% لكل خطوة-، كما سيُقسم الفاصل الزمني تلقائيًا إلى 9 أجزاء أيضًا، فلو كانت قيمة الخاصية transition تعادل 9 ثوان، فسيظهر كل رقم لمدة ثانية؛ أما الوسيط الثاني فيأخذ إحدى القيمتين start أو end، حيث تعني القيمة start أننا نريد تنفيذ الخطوة الأولى مباشرةً عند بداية الحركة، ويمكنك ملاحظة ذلك خلال الحركة، فعندما ننقر على الرقم سيتغير إلى "1" -أي الخطوة الأولى- مباشرةً، ثم يكمل ثانيةً وهكذا. تتقدم العملية كالتالي: -10% - 0s: يحدث التغيير الأول في بداية الثانية الأولى مباشرةً. 1s – -20%. … 8s – -80%. تُظهر الثانية الأخيرة القيمة الأخيرة. بينما تعني القيمة end وجوب تطبيق التغيير في نهاية كل ثانية، ويكتمل في نهاية الثانية المعدودة. وتتقدم العملية كالتالي: 0s – 0: لا يتغير شيء في الثانية الأولى. 1s – -10%: يحدث التغير الأول عند نهاية الثانية الأولى مباشرةً. 2s – -20%. … 9s – -90%. إليك مثالًا نموذجيًا عن استخدام الدالة (steps(9, start. شيفرة الملف style.css: #digit { width: .5em; overflow: hidden; font: 32px monospace; cursor: pointer; } #stripe { display: inline-block } #stripe.animate { transform: translate(-90%); transition-property: transform; transition-duration: 9s; transition-timing-function: steps(9, end); } شيفرة الملف index.html: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="style.css"> </head> <body> Click below to animate: <div id="digit"><div id="stripe">0123456789</div></div> <script> digit.onclick = function() { stripe.classList.add('animate'); } </script> </body> </html> وستكون النتيجة: إليك بعض القيم المختصرة: step-start: تماثل (steps(1, start. step-end: تماثل (steps(1, end. لا تستخدم هذه القيم إلا نادرًا لأنها لا تُصنَّف حركات فعلًا، بل تغيرات تحدث بالخطوة. الحدث transitionend يقع الحدث transitionend عندما تنتهي الحركة المبنية على خصائص CSS، ويستخدم لتنفيذ عمل ما بعد انتهاء الحركة، حيث يمكننا مثلًا ضم حركات مختلفة، فالسفينة في المثال التالي ستبدأ رحلتها وتستمر بالعودة عند النقر، وفي كل مرة تبتعد نحو اليمين أكثر: تبدأ الحركة نتيجة تنفيذ الدالة go، التي يُعاد تنفيذها في كل مرة تنتهي فيها الحركة، وتغير اتجاه الحركة: boat.onclick = function() { //... let times = 1; function go() { if (times % 2) { // sail to the right boat.classList.remove('back'); boat.style.marginLeft = 100 * times + 200 + 'px'; } else { // sail to the left boat.classList.add('back'); boat.style.marginLeft = 100 * times - 200 + 'px'; } } go(); boat.addEventListener('transitionend', function() { times++; go(); }); }; لكائن الحدث transitionend عدة خصائص مميزة، هي: event.propertyName: يعيد اسم الخاصية التي أنهت حركتها الانتقالية، وتظهر فائدتها عند تحريك عدة خصائص معًا. event.elapsedTime: الوقت الذي استغرقته الحركة بالثواني، دون احتساب transition-delay. القاعدة keyframes يمكن ضم عدة حركات بسيطة معًا باستخدام قاعدةkeyframes@ من CSS، والتي تحدد اسم الحركة الانتقالية، وقواعد مكانها وزمانها وكيفيتها، بعدها سنتمكن باستخدام الخاصية animation من ربط الحركة بالعنصر وتخصيص معاملات إضافية قد نحتاجها، إليك مثالًا مع الشرح: <div class="progress"></div> <style> @keyframes go-left-right { /* "go-left-right" أعط القاعدة اسمًا*/ from { left: 0px; } /* left: 0px حرك ابتداءً من قيمة */ to { left: calc(100% - 50px); } /* 100%-50px: حرك نحو اليسار */ } .progress { animation: go-left-right 3s infinite alternate; /* طبق الحركة التي عرفناها في الأعلى على العنصر المدة 3 ثوان عدد المرات: لانهائي بدّل اتجاه الحركة في كل مرة */ position: relative; border: 2px solid green; width: 50px; height: 20px; background: lime; } </style> See the Pen JS-P3-05-CSS-animations-ex3 by Hsoub (@Hsoub) on CodePen. ستجد العديد من المقالات التي تشرح القاعدة @keyframes من CSS مع ميزاتها المفصلة، ولن تحتاجها غالبًا إلا عندما يكون كل شيء في موقعك متحركًا باستمرار. الأداء يمكن تحريك أغلب خاصيات CSS بما أنها قيم رقمية مثل width و color و font-size، فيعمل المتصفح عندما تحرك هذه الخاصيات على تغيير هذه القيم تدريجيًا في كل إطار من الإطارات مما يعطي إيحاء بتأثير حركي سلس، ولكن انتظر، لا تبدو كل الحركات سلسلة كما تريدها لأن تغيير بعض خاصيات CSS عملية مجهدة. لنغوص أكثر في التفاصيل، عندما يكتشف المتصفح حدوث تغير في التنسيق، فإنه يمر بثلاثة خطوات حتى يخرج الصفحة بالتنسيق الجديد وهي: التخطيط Layout: إعادة حساب إحداثيات ومواضع كل عنصر من عناصر الصفحة، ثم الشكل Paint: إعادة حساب كيف يجب أن يظهر كل عنصر بعد تموضعه في مكانه وكل شيء حرفيًا بما فيها الخلفيات والألوان، ثم التركيب Composite: تصيير النتائج النهائية وإخراجها وعرضها على بكسلات الشاشة وتطبيق تحويلات CSS إن وجدت. تكرر هذه الخطوات خلال إجراء تحريك عبر CSS لكل إطار من الإطارات، رغم أن خاصيات CSS التي لا تؤثر على الإحداثيات أو التموضع مثل تغيير خاصية اللون color فقد تتخطى الخطوة الأولى وينتقل المتصفح إلى الخطوة الثانية مباشرةً ثم الثالثة وقد تتخطى بعض الخاصيات حتى الخطوة الثانية وتنتقل إلى الخطوة الثالثة مباشرةً، ويمكنك أن تطلع على قائمة خاصيات CSS والمراحل التي تستهدفها من موقع CSS Triggers. معلومٌ أن تلك الخطوات تأخذٌ وقتًا في الحساب خصوصًا إن حوت الصفحة على عناصر كثيرة وكانت تخطيطها معقدًا، فقد يُلاحظ تأخر في الحركة على أغلب الأجهزة مسببًا حركة مضطربة مرتعشة، لذا تتصف الحركات المطبقة على الخاصيات التي تتخطى حساب التخطيط (ويفضل أيضًا الشكل أي الخطوة الثانية) بالسرعة والسلاسة. تعد الخاصية transform خيارًا جيدًا لسببين هما: تستهدف التحويلات في CSS صندوق العنصر ككل (تدوير، أو قلب أو تمديد أو إزاحة …إلخ.) لا تؤثر التحويلات على العناصر المجاورة للعنصر المستهدف. وفقًا ذلك، تطبق التحويلات على عناصر يكون فيها التخطيط والشكل محسوبين مسبقًا وتنتقل مباشرةً إلى الخطوة الثالثة من الخطوات السابقة، أي أن المتصفح يحسب التخطيط (الحجم والمواضع) ثم يضيف لها تسنيق الشكل من ألوان وواجهات وغيرهما في المرحلة الثانية ثم يطبق التحويل الممثل بالخاصية transform على صندوق العنصر إن وجدت. أي تغييرات (تحريكات) تطبق على الخاصية transform لا تستدعي المرور ضمن الخطوة الأولى والثانية مطلقًا، علاوة على أن المتصفح يستغل قدرات الحاسوب الرسومية (بطاقة العرض التي تكون مدمجة في المعالج أو منفصلة) لمعالجة التحويلات مما يزيد من كفاءة العملية. مما سبق نجد أن الخاصية transform قوية للغاية ويمكنك عبرها تدوير عنصرٍ أو قلبه أو تطويله أو تقصيره أو حتى تحريكه وغيرها (انظر صفحة الخاصية على موسوعة حسوب)، فيمن استعمال transform: translateX(…) بدلًا من استعمال الخاصية left أو margin-left أو استعمال transform: scale لزيادة حجم عنصر وهكذا. لاحظ أن الخاصية opacity لا تمر على الخطوة الأولى من الخطوات السابقة (أيضًا تتخطى الخطوة الثانية في محرك Mozilla Gecko)، ويمكن استعمالها لتطبيق تأثيرات مثل الإظهار أو الإخفاء أو التلاشي، ويمكن استعمال transform معها لإنشاء حركات سلسلة جميلة. انظر مثلًا المثال التالي الذي يتحرك فيه العنصر بمجرد الضغط عليه إلى اليمين مقدار 300 بكسل ثم يختفي: <img src="https://js.cx/clipart/boat.png" id="boat"> <style> #boat { cursor: pointer; transition: transform 2s ease-in-out, opacity 2s ease-in-out; } .move { transform: translateX(300px); opacity: 0; } </style> <script> boat.onclick = () => boat.classList.add('move'); </script> See the Pen JS-P3-05-CSS-animations-ex4 by Hsoub (@Hsoub) on CodePen. وإليك أيضًا مثال آخر أكثر تعقيدًا يُطبَّق فيه إطارات الحركة @keyframes: <h2 onclick="this.classList.toggle('animated')">click me to start / stop</h2> <style> .animated { animation: hello-goodbye 1.8s infinite; width: fit-content; } @keyframes hello-goodbye { 0% { transform: translateY(-60px) rotateX(0.7turn); opacity: 0; } 50% { transform: none; opacity: 1; } 100% { transform: translateX(230px) rotateZ(90deg) scale(0.5); opacity: 0; } } </style> See the Pen JS-P3-05-CSS-animations-ex5 by Hsoub (@Hsoub) on CodePen. خلاصة تسمح الحركات الانتقالية التي تدعمها CSS بإجراء تغيرات ناعمة على خاصية أو أكثر، وهي تقنية جيدة للكثير من مهام الرسوم المتحركة، كما يمكننا استخدام رسوم JavaScript المتحركة وهذا ما سنراه في جزئية لاحقة من هذه السلسلة. النقاط الأساسية التي تجعل الرسومات المتحركة باستخدام CSS محدودةً موازنةً برسوم JavaScript هي: الإيجابيات. تنفَّذ الأشياء البسيطة ببساطة. سريعة وخفيفة على المعالج. السلبيات. رسوم JavaScript أكثر مرونةً، إذ يمكنها تنفيذ أي منطق رسومي، مثل "انفجار" عنصر مثلًا. لا تعتمد رسوميات JavaScript على تغيرات الخاصية، بل يمكن إنشاء عناصر جديدة في JavaScript مثل جزء من الرسوميات. في أمثلتنا الأولى حركنا font-size وleft وwidth وheight، لكن إذا رغبنا في أداء أفضل في مشاريعنا الحقيقية، فيجب أن نستخدم ()transform: scale و()transform: translate. يمكن تنفيذ الرسوميات المتحركة باستخدام CSS كما شرحناها في هذا المقال، وسيسمح لنا الحدث transitionend باستخدام JavaScript بعد انتهاء الحركة الانتقالية، وبالتالي ستتكامل جيدًا مع الشيفرة. سنتعرف في المقال القادم على رسوم JavaScript المتحركة التي تساعدنا في تنفيذ حالات أكثر تعقيدًا. مهام لإنجازها تحريك طائرة في CSS أظهر رسمًا متحركًا يشابه صورة الطائرة في الأسفل: يزداد حجم الصورة من 40x24px إلى 400x240px. تستغرق العملية 3 ثوان. اطبع كلمة "!Done" عند الانتهاء. لا يجب أن تتوقف العملية بالنقر على الصورة أثناء التنفيذ. افتح المثال في بيئة تجريبية الحل إليك الشيفرة: /* original class */ #flyjet { transition: all 3s; } /* JS adds .growing */ #flyjet.growing { width: 400px; height: 240px; } انتبه إلى أن الحدث transitionend يقع مرتين، مرة لكل خاصية، وبذلك ستظهر الرسالة مرتين إن لم نضع أي شرط تحقق. تحرك الطائرة 2 أعد التمرين السابق لكن اجعل الحجم يتجاوز الحجم الأقصى للصورة 400x240px، ثم يعود إلى هذا الحجم. الحل نريد اختيار أنسب منحني بيزيه لتلك الحركة، إذ يجب أن تحقيق y>1 في موضع ما للطائرة لتحقيق تأثير القفزة مثل cubic-bezier(0.25, 1.5, 0.75, 1.5)، انظر المخطط: افتح الحل في بيئة تجريبية. تحريك دائرة أنشئ الدالة showCircle(cx, cy, radius) التي تظهر دائرةً تنمو. cx,cy: يمثلان إحداثيات مركز الدائرة بالنسبة إلى إحداثيات النافذة. radius: نصف قطر الدائرة. انقر الزر لترى كيف سيظهر الحل: تجد المثال في هذه البيئة التجريبية وتجد حله في هذه البيئة التجريبية. تحريك دائرة مع دالة استدعاء انطلاقًا من المهمة السابقة، نحتاج إلى دائرة تُظهر رسالةً داخلها. ينبغي أن تظهر الرسالة مباشرةً بعد اكتمال الحركة، أي ظهور الدائرة بحجمها الكامل، وإلا سيبدو المنظر سيئًا. ترسم الدالة (showCircle(cx, cy, radius الدائرة، لكن لا يمكنها أن تدلّك على اكتمال الرسم، لذا أضف دالة استدعاء callback مثل معامل إلى الدالة السابقة (showCircle(cx, cy, radius, callback، حيث تُستدعى عندما ينتهي الرسم المتحرك. ينبغي أن تتلقى دالة الاستدعاء callback العنصر <div> للدائرة مثل وسيط. إليك مثالًا: showCircle(150, 150, 100, div => { div.classList.add('message-ball'); div.append("Hello, world!"); }); النموذج: تجد حل المثال في هذه البيئة التجريبية. ترجمة -وبتصرف- للفصل css-animations من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: منحنى بيزيه وأهميته في الرسوميات وصناعة الحركات في جافاسكربت تأثيرات الانتقال والحركة في CSS منحنى بيزيه وأهميته في الرسوميات وصناعة الحركات في جافاسكربت table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } p iframe { border: 1px solid #e7e5e3 !important; }
-
- 1
-
-
- تحريك
- animations
-
(و 1 أكثر)
موسوم في:
-
تستخدم منحنيات بيزيه Bezier curves في رسوميات الحاسوب لإنشاء الأشكال والرسوم المتحركة المعتمدة على أوراق التنسيق المتتالية CSS وغيرها. هذه المنحنيات بسيطة وتستحق الدراسة، وبعدها ستكون مرتاحًا عند التعامل مع الرسوميات الشعاعية vector graphics والرسوم المتحركة المتقدمة. نقاط التحكم يحدد منحني بيزيه بواسطة مجموعة من النقاط الحاكمة، وقد يكون عددها 2 أو 3 أو 4 أو أكثر، لاحظ مثلًا منحنيًا من نقطتين: والمنحني التالي محدد بثلاث نقاط: وهذا بأربع نقاط: لو تمعنت في هذه المنحنيات، فيمكنك أن تلاحظ التالي: لا تنتمي النقاط بالضرورة إلى المنحني وهذا أمر طبيعي، وستفهم ذلك عندما سنشرح طريقة بنائه لاحقًا. درجة المنحني curve order تساوي عدد النقاط ناقصًا واحدًا، فالمنحني المؤلف من نقطتين هو خط مستقيم من الدرجة الأولى، والمنحني المؤلف من ثلاث نقاط هو منحن تربيعي، أي قطع مكافئ، وهكذا. ينحصر المنحني ضمن المضلع المحدّب convex hull الذي تشكله النقاط الحاكمة: سنتمكن من أَمثَلة اختبارات التداخل في رسوميات الحاسوب اعتمادًا على الخاصية الأخيرة، فلن تتداخل المنحنيات إن لم تتداخل المضلعات المحدبة، وبالتالي ستعطي دراسة تداخل المضلعات المحدبة جوابًا سريعًا لتداخل المنحنيات، بالإضافة إلى أنّ دراسة تداخل المضلعات أسهل لأنّ أشكالها مثل المثلث والمربع، تُعَد مفهومة موازنةً بالمنحنيات. تكمن الفائدة الرئيسية لاستخدام منحني بيزيه في تغيير شكل المنحني بمجرد تحريك النقاط الحاكمة. لاحظ المنحني الذي تولده الشيفرة التالية، واستخدم الفأرة لتحريك النقاط الحاكمة وراقب تغير المنحني: ستتمكن بعد التمرن قليلًا من رسم المنحني المطلوب بمجرد تحريك النقاط الحاكمة، ويمكنك عمليًا الحصول على أي شكل تريده بوصل عدة منحنيات، إليك بعض الأمثلة: خوارزمية دي كاستلجو De Casteljau يمكننا استخدام صيغة رياضية لرسم منحنيات بيزيه، وهذا ما سنشرحه لاحقًا، لأنّ خوارزمية دي كاستلجو De Casteljau’s algorithm التي نسبت لمخترعها، تتطابق مع التعريف الرياضي، وتساعدنا على تمييز طريقة إنشاء المنحنيات بصريًا. سنرى أولًا مثالًا لمنحن من ثلاث نقاط حاكمة، لاحظ أنه يمكنك تحريك النقاط 1 و 2 و 3 بالفأرة، ثم انقر زر التشغيل الأخضر. خوازمية دي كاستلجو لبناء منحني بيزيه من ثلاث نقاط ارسم النقاط الثلاث، والتي تمثلها النقاط 1 و2 و3 في المثال السابق. صل بين النقاط السابقة باتجاه واحد 3 → 2 → 1، وهي الخطوط البنية في المثال السابق. يأخذ المعامل t قيمه بين "0" و"1"، وقد استخدمنا في مثالنا النموذجي السابق خطوةً مقدارها "0.05"، أي تتحرك حلقة التنفيذ وفق الآتي: 0 ثم 0.05 ثم 0.1 ثم 0.15 وهكذا. لكل قيمة من قيم t: نأخذ على كل خط بني بين نقطتين متتاليتين نقطةً تبعد عن بدايته مسافةً تتناسب مع t، وطالما أن هناك خطان، فسنحصل على نقطتين، وستكون كلتا النقطتين في بداية الخط عندما يكون t=0 مثلًا، كما ستبعد النقطة الأولى عن بداية الخط مسافةً تعادل 25% من طوله عندما تكون t=0.25، وهكذا. نصل بين النقطتين المتشكلتين، والخط الواصل في المثال التالي باللون الأزرق. table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } p iframe.code-result__iframe { border: 1px solid #e7e5e3 !important; } t=0.25 t=0.5 خذ على الخط الأزرق السابق نقطةً تبعد عن طرفه مقدارًا يتناسب مع t، ستكون النقطة في نهاية الربع الأخير من الخط من أجل t=0.25، وستكون في منتصف الخط من أجل t=0.5، وهي النقطة الحمراء في الشكل السابق. عندما تتحول t بين 0 و1 ستضيف كل قيمة لهذا المعامل نقطةً من نقاط المنحني، وتمثل هذه المجموعة من النقاط منحني بيزيه. نتبع الأسلوب ذاته بالنسبة لمنحنى مكون من أربع نقاط حاكمة. الخوارزمية المتبعة لأربع نقاط هي: صل بين النقاط الحاكمة 1 إلى 2، و2 إلى 3، و3 إلى 4، وستحصل على ثلاثة خطوط بنية. لكل قيمة للمعامل t بين 0 و1: نأخذ نقاطًا على الخطوط الثلاثة تتناسب مع t كما فعلنا سابقًا، وبوصل هذه النقاط الثلاثة المتشكلة سنحصل على خطين جديدين، باللون الأخضر مثلًا. نأخذ على الخطين الأخضرين نقاطًا متناسبةً مع t، وعندها سنصل بين النقطتين المتشكلتين فنحصل على خط واحد، وليكن باللون الأزرق. نأخذ على الخط الأزرق نقطةً تبعد عن أحد طرفي الخط مسافةً متناسبةً مع t، فنحصل على نقطة حمراء واحدة تمثل أحد نقاط منحني بيزيه. تمثل النقاط الحمراء كلها المنحني الكامل. إن الخوارزمية السابقية تعاودية، أي قادرة على إعادة نفسها، لذلك يمكن تعميمها إلى N نقطة حاكمة: نصل بين النقاط فنحصل على N-1 خطًا. لكل قيمة للمعامل t بين 0 و1 نأخذ على كل خط نقطةً تبعد عن طرف الخط مسافةً متناسبةً مع t، ونصل بين هذه النقاط فنحصل على N-2 خطًا. نكرر الخطوة 2 حتى نحصل على نقطة واحدة تمثل إحدى نقاط منحني بيزيه. تشكل جميع النقاط الناتجة في الخطوة 3 منحني بيزيه. شغّل الرسوميات التالية ولاحظ شكل المنحني: منحن يبدو مثل الخط البياني للدالة y=1/t. نقاط حاكمة بشكل Zig-zag تبدو جيدةً أيضًا: يمكن رسم حلقة: يمكن رسم منحني بيزيه غير أملس أيضًا: تابع الأمثلة الحية السابقة لترى كيف يُبنى المنحني، إذا وجدت شيئًا مبهمًا في وصف الخوارزمية. يمكن استخدام العدد الذي نريده من النقاط الحاكمة لرسم منحني بيزيه من أي درجة، ما دامت الخوارزمية تعاوديةً، لكن يفضل عمليًا استخدام عدد قليل من النقاط -عادةً 2 أو 3 نقاط-، ثم نربط بين عدة منحنيات بسيطة إذا أردنا شكلًا أكثر تعقيدًا. الصيغ الرياضية يمكن توصيف منحني بيزيه باستخدام صيغة رياضية، وكما رأينا فلا حاجة لمعرفة هذه الصيغة. يرسم المستخدمون المنحني بمجرد تحريك النقاط بالفأرة، لكن إذا أردت استخدام الرياضيات فلك هذا. لنفترض أنّ إحداثيي نقطة حاكمة هو Pi. ستكون النقطة الأولى (P1 = (x1, y1، والثانية (P2 = (x2, y2، وهكذا حتى آخر نقطة (Pi = (xi, yi، ويمكن أن نصف منحني بيزيه بمعادلة متغيرها t يأخذ قيمه بين 0 و1. صيغة منحن يمر بنقطتين هي: صيغة منحن يمر من 3 نقاط هي: صيغة منحن يمر من 4 نقاط هي: إنّ المعادلات السابقة هي معادلات شعاعية، أي يمكننا وضع الإحداثيين x وy بدلًا من P، فمن أجل المنحني المار من ثلاث نقاط مثلًا ستصبح الصيغة بدلالة x وy هي: ينبغي تعويض القيم x1, y1, x2, y2, x3, y3 في المعادلات وكتابة الصيغة بدلالة المتغير t، فلو فرضنا أنّ إحداثيات النقاط الحاكمة الثلاثة هي: (0,0) و(0.5, 1) و(1, 0)،فستصبح قيمة إحداثيي النقطة المقابلة من منحني بيزيه: سنحصل على إحداثيي نقطة جديدة (x,y) من نقاط منحني بيزيه كلما تغيرت قيمة t بين 0 و1. خلاصة تُحدَّد منحنيات بيزيه بثلاث نقاط حاكمة، ويمكن رسمها بأسلوبين كما رأينا: باستخدام عملية رسومية: أي خوارزمية دي كاستلجو. باستخدام صيغة رياضية. من الصفات الجيدة لمنحنيات بيزيه: إمكانية رسم منحنيات ملساء عبر تحريك النقاط الحاكمة باستخدام الفأرة. إمكانية رسم أشكال أكثر تعقيدًا بوصل عدة منحنيات بيزيه معًا. استخدامات منحنيات بيزيه: في رسوميات الحاسوب والنمذجة ومحررات الرسوميات الشعاعية، كما توصف بها أنواع خطوط الكتابة. في تطوير تطبيقات الويب، من خلال الرسم ضمن لوحات وفي الملفات بتنسيق SVG، وقد كتبت الأمثلة النموذجية السابقة بتنسيق SVG، وهي عمليًا مستند SVG مفرد أُعطي نقاطًا مختلفةً مثل معاملات، ويمكن فتح هذه النماذج في نوافذ منفصلة والاطلاع على الشيفرة المصدرية: demo.svg. في الرسوميات المتحركة لوصف مسارها وسرعتها. ترجمة -وبتصرف- للفصل Bezier curve من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: كائنا localStorage وsessionStorage لتخزين بيانات الويب في جافاسكربت أساسيات منحنى بيزيه Bezier Curve في سكريبوس تأثيرات الانتقال والحركة في CSS
-
يسمح هذان الكائنان بتخزين الأزواج "مفتاح/قيمة" في المتصفح، لكن الميزة الهامة لهما هي بقاء البيانات المخزنة في الكائن sessionStorage بعد تحديث الصفحة وبقاء المعلومات المخزنة في localStorage بعد إعادة تشغيل المتصفح، لكن السؤال الذي يلفت النظر هو: ما دام لدينا ملفات تعريف الارتباط cookies، فلماذا سنستخدم كائنات إضافيةً؟ والجواب: لا يُرسَل كائنا تخزين بيانات الويب هذان إلى الخادم مع كل طلب، وذلك خلافًا لملفات تعريف الارتباط، وبالتالي سنتمكن من تخزين بيانات أكثر، حيث تتيح أغلب المتصفحات حوالي 2 ميغابايت من البيانات -وأكثر-، ولها إعدادات لتهيئة حجم التخزين. لا يمكن للخادم التحكم بهذين الكائنين عبر ترويسات HTTP، وسيُنجز كل شيء باستخدام JavaScript، خلافًا لملفات تعريف الارتباط. ترتبط ذاكرة التخزين بالأصل الذي ولّدها (نطاق/بروتوكول/منفذ)، مما يعني أن البروتوكولات أو النطاقات الفرعية المختلفة ستدل على كائنات تخزين مختلفة، ولا يمكن أن تصل إلى بيانات بعضها البعض. لكائني التخزين التوابع والخصائص نفسها، وهي: (setItem(key, value: يخزّن الأزواج "مفتاح/قيمة". (getItem(key: يحصل على القيمة عن طريق المفتاح. (removeItem(key: يزيل المفتاح مع قيمته. ()clear: يحذف كل شيء. (key(index: يعيد مفتاحًا ذا موقع محدد. length: يعطي عدد العناصر المخزّنة. تشبه هذه التوابع ما يقوم به الترابط Map، لكنه يسمح أيضًا بالوصول إلى المفاتيح من خلال مواقعها (key(index. مثال نموذجي عن الكائن localStorage ميزات هذا الكائن الرئيسية هي: مشترك بين كل النوافذ التي تشترك بالأصل ذاته. ليس للبيانات فترة صلاحية، إذ تبقى بعد إعادة تشغيل المتصفح أو نظام التشغيل. فلو شغّلنا الشيفرة التالية مثلًا: localStorage.setItem('test', 1); إذا أغلقنا المتصفح ثم أعدنا تشغيله، أو فتحنا الصفحة نفسها في أكثر من نافذة، فسنحصل على القيمة التي خزناها بالشكل التالي: alert( localStorage.getItem('test') ); // 1 علينا فقط أن نكون ضمن صفحات تنتمي إلى الأصل ذاته (نطاق/منفذ/بروتوكول)، على الرغم من إمكانية اختلاف المسار، لأن الكائن localStorage مشترك بين كل النوافذ التي تنتمي إلى الأصل ذاته. الوصول بأسلوب الكائنات يمكن استخدام أسلوب الكائن البسيط للحصول على المفاتيح أو تغيير قيمها بالشكل التالي: // ضبط المفتاح localStorage.test = 2; // الحصول على قيمة المفتاح alert( localStorage.test ); // 2 // إزالة المفتاح delete localStorage.test; يُسمح بهذه الطريقة -التي ستعمل غالبًا- لأسباب تاريخية، لكن لا يُفضّل استعمالها للأسباب التالية: لو ولّد المستخدم المفتاح، فقد يكون أي شيء مثل length أو toString، أي قد يتشابه مع توابع محجوزة تستخدَم مع localStorage، في هذه الحالة ستعمل التوابع getItem/setItem، لكن سيخفق الوصول بأسلوب الكائنات . let key = 'length'; localStorage[key] = 5; // Error, can't assign length الحدث storage الذي يقع عند تعديل البيانات، وليس عند تطبيق أسلوب الكائنات، وسنرى ذلك لاحقًا في هذا المقال. التنقل بين المفاتيح ضمن حلقات تؤمّن التوابع السابقة وظائف الحصول على قيم المفاتيح وضبطها وحذفها، لكن كيف سنخزّن جميع القيم أو المفاتيح؟ لسوء الحظ لا تقبل كائنات تخزين البيانات التكرار، لكن إحدى الطرق المتبعة هي التنقل في حلقة كما لو أننا نتعامل مع مصفوفة: for(let i=0; i<localStorage.length; i++) { let key = localStorage.key(i); alert(`${key}: ${localStorage.getItem(key)}`); } يمكن استخدام الحلقة for key in localStorage كما نفعل مع الكائنات النظامية، حيث تتكرر تعليمات الحلقة وفقًا للمفاتيح المخزّنة، لكنها ستعطي حقولًا مدمجةً غير مطلوبة: // محاولة فاشلة for(let key in localStorage) { alert(key); // وغيرها من الحقول المدمجة getItem, setItem ستظهر } فإمّا أن نرشح الحقول التي ستُعرض بالتحقق من الخاصية hasOwnProperty: for(let key in localStorage) { if (!localStorage.hasOwnProperty(key)) { continue; // "setItem", "getItem" تتجاهل مفاتيح مثل } alert(`${key}: ${localStorage.getItem(key)}`); } أو نحصل على المفاتيح الخاصة بالكائن باستخدام الأمر Object.keys، ثم نطبق الحلقة عليها: let keys = Object.keys(localStorage); for(let key of keys) { alert(`${key}: ${localStorage.getItem(key)}`); } ستنجح الطريقة الأخيرة لأنّ التابع Object.keys سيعيد المفاتيح التي ترتبط بالكائن فقط، ويتجاهل النموذج الأولي prototype. قيم نصية فقط يجب الانتباه إلى أنّ المفاتيح وقيمها كائنات نصية، وستحوّل أي أنواع أخرى - مثل الأرقام- إلى قيم نصية تلقائيًا: sessionStorage.user = {name: "John"}; alert(sessionStorage.user); // [object Object] يمكن استخدام JSON لتخزين الكائنات أيضًا: sessionStorage.user = JSON.stringify({name: "John"}); // لاحقًا let user = JSON.parse( sessionStorage.user ); alert( user.name ); // John كما يمكن تحويل كائن التخزين بالكامل إلى نص، لأغراض التنقيح مثلًا: // لتبدو النتيجة أفضل JSON.stringify يمكن إضافة خيارات تنسيق إلى alert( JSON.stringify(localStorage, null, 2) ); الكائن sessionStorage يُستخدم في حالات أقل من الكائن localStorage، وله نفس التوابع والخصائص، لكنها أكثر محدوديةً. يتواجد الكائن فقط ضمن النافذة الحالية المفتوحة ضمن المتصفح. سيكون لنافذة أخرى مفتوحة ضمن المتصفح كائن آخر خاص بها. تتشارك النوافذ الضمنية الموجودة في نافذة نفس الكائن، بفرض أنها مشتركة بالأصل. تبقى البيانات المخزنة بعد تحديث الصفحة، لكنها تُحذف عند إغلاق النافذة وإعادة فتحها. شغّل هذه الشيفرة لترى آلية عمل الكائن: sessionStorage.setItem('test', 1); ثم حدّث الصفحة. عندها ستلاحظ أن البيانات مازالت موجودة: alert( sessionStorage.getItem('test') ); // after refresh: 1 لكن لو فتحت الصفحة نفسها في نافذة أخرى، وحاولت تنفيذ الشيفرة السابقة مجددًا، فستعيد القيمة "null" أي أنها لم تجد شيئًا، لأنّ الكائن sessionStorage لا يتعلق فقط بالأصل المشترك بل بالنافذة المفتوحة في المتصفح، لذا يندر استخدامه. أحداث التخزين عندما تُحدّث البيانات ضمن كائني التخزين فستقع أحداث التخزين التالية: key: المفتاح الذي تغيّر، وسيعيد null إذا استدعي التابع ()clear. oldValue: القيمة القديمة، وستكون null إذا أضيف المفتاح حديثًا. newValue: القيمة الجديدة، وستكون null إذا حُذف المفتاح. url: عنوان الصفحة التي حدث فيها التغيير. storageArea أو أحد الكائنين localStorage أو sessionStorage حيث حدث التغيير. أمّا الأمر الهام فهو أنّ هذه الأحداث ستقع في كل الكائنات window التي يمكن فيها الوصول إلى كائن تخزين البيانات، عدا تلك التي سببت وقوع الحدث. تخيل وجود نافذتين في المتصفح تعرضان الصفحة نفسها، عندئذ ستتشارك النافذتان الكائن localStorage نفسه، وقد يكون عرض الصفحة في نافذتين مختلفتين مناسبًا لاختبار الشيفرة التي سنعرضها تاليًا، إذا استمعت كلتا النافذتين إلى الحدث window.onstorage، فستتفاعلان مع التحديثات التي تجري في كلٍّ منهما. // يقع عند تحديث كائن التخزين من قبل صفحة أخرى window.onstorage = event => { // same as window.addEventListener('storage', event => { if (event.key != 'now') return; alert(event.key + ':' + event.newValue + " at " + event.url); }; localStorage.setItem('now', Date.now()); لاحظ أنّ الحدث سيتضمن أيضًا event.url، وهو عنوان الصفحة التي حصل فيها التغيير، كما يتضمن الحدث كائن التخزين (سيبقى الحدث نفسه للكائنين sessionStorage وlocalStorage)، لذا سيشير الحدث event.storageArea إلى الكائن الذي جرى تعديله منهما، وبما أننا نريد أن نعيد ضبط قيمة ما استجابةً للتغيير، فسيسمح ذلك للنوافذ ذات الأصل المشترك بتبادل الرسائل. تدعم المتصفحات الحديثة الواجهة البرمجية لقناة البث Broadcast channel API، وهي واجهة خاصة بتبادل الرسائل بين النوافذ التي لها أصل مشترك. لهذه الواجهة ميزات متكاملة أكثر لكنها أقل دعمًا، ومع ذلك فستجد الكثير من المكتبات التي توائم هذه الواجهة مع المتصفحات بالاستفادة من الكائن localStorage مما يجعلها متاحةً في أي مكان. خلاصة يسمح الكائنان localStorage وsessionStorage بتخزين الأزواج (مفتاح/قيمة) في المتصفح. المفتاح key والقيمة value من النوع النصي. الحد الأقصى للتخزين بحدود 5 ميغابايت وذلك تبعًا للمتصفح. ليس لها فترة صلاحية. ترتبط البيانات بأصل الصفحة (نطاق/ منفذ/بروتوكول). table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } localStorage sessionStorage مشترك بين النوافذ التي لها نفس الأصل تُرى ضمن نافذة واحدة في المتصفح بما فيها النوافذ الضمنية التي لها نفس الأصل تبقى بعد إعادة تشغيل المتصفح تبقى بعد تحديث الصفحة لكنها تحذف عند إغلاق النافذة الواجهة البرمجية: (setItem(key, value: يخزّن أزواج (مفتاح/قيمة). (getItem(key: يحصل على القيمة عن طريق المفتاح. (removeItem(key: يزيل المفتاح مع قيمته. ()clear: يحذف كل شيء. (key(index: يعيد مفتاحًا ذا موقع محدد. length: يعطي عدد العناصر المخزّنة. Object.keys: للحصول على جميع المفاتيح. يمكن الوصول إلى المفاتيح عبر خصائص الكائن، لكن لن يقع الحدث storage في هذه الحالة. أحداث التخزين: تقع نتيجةً للاستدعاءات التالية setItem أو removeItem أو clear. تحتوي على كل البيانات المتعلقة بالعملية key/oldValue/newValue، وبالصفحة url، وبكائن التخزين storageArea. تقع في جميع النوافذ التي يمكنها الوصول إلى كائن التخزين، عدا تلك التي ولّدته، ضمن نافذة واحدة بالنسبة للكائن sessionStorage، ولكل النوافذ بالنسبة للكائن localStorage. مهمات لإنجازها الحفظ التلقائي لحقل من حقول نموذج أنشئ حقلًا نصيًا textarea يحفظ تلقائيًا القيمة التي يحتويها بعد كل تغيير فيها، وبالتالي عندما يُغلق المستخدم الصفحة عن طريق الخطأ، ثم يفتحها مجددًا، فسيجد النص الذي لم يكمله بعد في مكانه. افتح المثال في بيئة تجريبية. وإن أردت الحل، فهو في هذه البيئة التجريبية. ترجمة -وبتصرف- للفصل localStorage, sessionStorage من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: العمل مع قاعدة البيانات IndexedDB في جافاسكربت ملفات تعريف الارتباط وضبطها في JavaScript المدخل الشامل لتعلم علوم الحاسوب
-
- كائنات التخزين
- localstorage
-
(و 1 أكثر)
موسوم في:
-
IndexedDB هي قاعدة بيانات مدمجة مع المتصفح، ولها ميزات أقوى بكثير من الكائن localStorage، أهمها: تخزن أي نوع من القيم تقريبًا من خلال المفاتيح ذات الأنواع المختلفة. تدعم الإجرائيات المترابطة transactions لوثوقية أعلى. تدعم الاستعلامات عن المفاتيح ضمن مجالات، كما تدعم الوصول إلى المفتاح بالفهرس index. يمكن أن تخزّن بيانات أحجام أكبر بكثير مما يخزنه الكائن localStorage. إنّ القدرة التي تؤمّنها قاعدة البيانات هذه تفوق المطلوب في تطبيقات (خادم-عميل) التقليدية، فهي مصممة للتطبيقات التي تعمل دون اتصال offline، وذلك لتشترك مع تقنية عمال الخدمات ServiceWorkers وغيرها من التقنيات. تشرح توصيفات قاعدة البيانات IndexedDB الواجهة الأصلية للتعامل مع القاعدة، وتتميز بأنها مقادة بالأحداث، كما يمكن أيضًا استخدام آلية async/await بمساعدة مُغلّف wrapper يعتمد على الوعود promise مثل المُغلّف idb، وعلى الرغم من أن هذه آلية مريحة، إلا أنها ليست مثاليةً، إذ لن يتمكن المغلف من استبدال الأحداث في كل الحالات، لذلك سنبدأ أولًا بالتعرف على الأحداث، ثم نتفهم قاعدة البيانات IndexedDb، ثم سنعود لاستخدام المُغلِّف. الاتصال مع قاعدة البيانات نحتاج أولًا إلى تأسيس اتصال مع قاعدة البيانات IndexedDB باستعمال التابع open قبل البدء بالعمل معها، ولهذا التابع الصيغة التالية: let openRequest = indexedDB.open(name, version); name: قيمة نصية تشير إلى اسم قاعدة البيانات. version: قيمة صحيحة موجبة لنسخة قاعدة البيانات، وهي 1 افتراضيًا. قد توجد قواعد بيانات عديدة بأسماء مختلفة، لكنها تعود جميعها إلى نفس الأصل (نطاق/بروتوكول/منفذ)، ولا يمكن لمواقع الويب المختلفة الوصول إلى قواعد بيانات المواقع الأخرى. يعيد الاستدعاء الكائن openRequest، وينبغي علينا الاستماع إلى الأحداث المتعلقة به، وهي: success: يقع عندما تكون قاعدة البيانات جاهزة، أو لما يتواجد كائن قاعدة بيانات ضمن openRequest.result، والذي علينا استخدامه في الاستدعاءات اللاحقة. error: يقع عند الإخفاق في إنشاء الاتصال مع قاعدة البيانات upgradeneeded: قاعدة البيانات جاهزة، لكن نسختها قديمة. تتميز IndexedDB بوجود آلية مدمجة فيها لتحديد نسخة تخطيطها schema versioning، والتي لا نراها في قواعد البيانات الموجودة في جهة الخادم، فهي قاعدة بيانات تعمل من جهة العميل وتخزّن بياناتها ضمن المتصفح، كما لا يستطيع المطورون الوصول إليها في أي وقت، لذا عندما يزور المستخدم موقع الويب بعد إطلاق نسخة جديدة من التطبيق، فلا بدّ من تحديث قاعدة البيانات، فإذا كانت نسخة قاعدة البيانات أقل من تلك التي يحملها الأمر open، فسيقع الحدث upgradeneeded الذي يوازن بين النسختين ويُحدِّث هياكل البيانات بما يناسب، كما يقع هذا الحدث أيضًا عندما تكون قاعدة البيانات غير موجودة (أي تكون نسختها -تقنيًا- "0")، وهذا ما يجعلنا قادرين على إجراء عملية التهيئة. لنفترض أننا قد أصدرنا النسخة الأولى من تطبيقنا، حيث يمكننا عندها تأسيس الاتصال مع قاعدة بيانات نسختها "1"، وتهيئتها بالاستفادة من معالج الحدث upgradeneeded بالشكل التالي: let openRequest = indexedDB.open("store", 1); openRequest.onupgradeneeded = function() { // يقع عندما لا يمتلك العميل قاعدة بيانات // ...إنجاز التهيئة... }; openRequest.onerror = function() { console.error("Error", openRequest.error); }; openRequest.onsuccess = function() { let db = openRequest.result; // متابعة العمل مع قاعدة البيانات }; ثم أصدرنا لاحقًا النسخة الثانية، عندها سنتمكن من تأسيس الاتصال مع النسخة "2" والتحديث بالشكل التالي: let openRequest = indexedDB.open("store", 2); openRequest.onupgradeneeded = function(event) { // قاعدة البيانات أقل من النسخة 2 أو غير موجودة let db = openRequest.result; switch(event.oldVersion) { // النسخة الموجودة من القاعدة case 0: // لا قاعدة بيانات // تهيئة case 1: // يمتلك المستخدم النسخة 1 من القاعدة // تحديث } }; لاحظ أنه عندما تكون نسختنا الحالية هي "2"، فسيتضمن معالج الحدث onupgradeneeded شيفرةً تعالج حالة النسخة "0"، وهذا الأمر ملائم للمستخدمين الذين يزورون الصفحة للمرة الأولى ولا يمتلكون قاعدة بيانات، كما يتضمن شيفرةً تعالج وجود النسخة "1" لتحديثها. سيقع الحدث openRequest.onsuccess عندما ينتهي معالج الحدث onupgradeneeded بنجاح، وينجح تأسيس الاتصال مع قاعدة البيانات. لحذف قاعدة البيانات: let deleteRequest = indexedDB.deleteDatabase(name) //العملية deleteRequest.onsuccess/onerror يتتبع الحدثان لا يمكن تأسيس اتصال مع قاعدة بيانات بنسخة أقدم إذا كانت النسخة الحالية لقاعدة بيانات المستخدم أعلى من النسخة التي نمررها للاستدعاء، أي نسخة القاعدة "3" ونحاول تأسيس اتصال مع النسخة "2" مثلًا، فسيتولد خطأ وسيقع الحدث openRequest.onerror. وعلى الرغم من ندرة حدوث هذا الأمر، إلا أنه قد يحصل عندما يحاول الزائر تحميل شيفرة JavaScript قديمة، مثل أن تكون من الذاكرة المؤقتة لخادم وكيل مثلًا، حيث ستكون الشيفرة قديمةً والقاعدة حديثة. ولا بدّ من التحقق من نسخة قاعدة البيانات db.version، واقتراح إعادة تحميل الصفحة إذا أردنا الحماية من الأخطاء، كما نستخدم ترويسة HTTP ملائمةً للتعامل مع الذاكرة المؤقتة لتفادي تحميل شيفرة قديمة، وبالتالي لن نواجه مشاكل. مشكلة التحديث المتوازي Parallel update ما دمنا نتكلم عن تحديد نسخة التطبيق، فسنتطرق إلى مشكلة صغيرة مرتبطة بذلك، لنتأمل الحالة التالية: فتح مستخدم موقعنا في نافذة متصفح، وكانت نسخة قاعدة البيانات هي "1". ثم حدّثنا الصفحة وأصبحت الشيفرة أحدث. ثم فتح المستخدم نفسه موقعنا في نافذة أخرى. أي ستكون هناك نافذة متصلة بقاعدة بيانات نسختها "1"، بينما تحاول النافذة الأخرى تحديثها إلى النسخة "2" عبر معالج الحدث upgradeneeded. تتلخص المشكلة بأن قاعدة البيانات مشتركة بين نافذتين، لأنهما تعودان لنفس الموقع ولهما الأصل ذاته، ولا يمكن أن تكونا من النسختين "1" و"2" في نفس الوقت، ولتنفيذ عملية الانتقال إلى النسخة "2"، ينبغي إغلاق كل قنوات الاتصال مع النسخة "1" بما فيها قناة اتصال النافذة الأولى، ولتنظيم ذلك سيقع الحدث versionchange ضمن كائن قاعدة البيانات "المنتهية الصلاحية"، لذا يفترض الاستماع لهذا الحدث، وإغلاق اتصال قاعدة البيانات القديمة. يمكن اقتراح إعادة تحميل الصفحة للحصول على الشيفرة الأحدث، فإذا لم نستمع إلى الحدث versionchange ولم نغلق قناة الاتصال، فلن يُنفَّذ الاتصال الثاني، وسيعطي الكائن openRequest الحدث blocked بدلًا من success ولن تعمل النافذة الثانية. إليك الشيفرة التي تتعامل بشكل صحيح مع التحديث المتوازي، إذ تثبّت معالج الحدث onversionchange الذي يقع عندما يصبح الاتصال الحالي مع قاعدة البيانات منتهي الصلاحية، أي عندما تُحدَّث نسخة قاعدة البيانات في مكان آخر، ويغلق الاتصال. let openRequest = indexedDB.open("store", 2); openRequest.onupgradeneeded = ...; openRequest.onerror = ...; openRequest.onsuccess = function() { let db = openRequest.result; db.onversionchange = function() { db.close(); alert("Database is outdated, please reload the page.") }; // ...قاعدة البيانات جاهزة! استخدمها... }; openRequest.onblocked = function() { }; إن ما نفعله هنا بعبارة أخرى هو: يُعلمنا المستمِع إلى الحدث db.onversionchange عن محاولة التحديث المتوازي، عندما تصبح النسخة الحالية لقاعدة البيانات منتهية الصلاحية. يُعلمنا المستمِع إلى الحدث openRequest.onblocked عن الحالة المعاكسة، وهي وجود اتصال لم يُغلَق بعد مع نسخة منتهية الصلاحية في نافذة ما، وبالتالي لن يعمل الاتصال الجديد. يمكن أن نتعامل مع الموضوع بطريقة ألطف عند استخدام الحدث db.onversionchange الذي يبلغ المستخدم بوجوب حفظ بياناته قبل قطع الاتصال، يمكن أيضًا اعتماد مقاربة أخرى لا تتعلق بإغلاق الاتصال مع قاعدة البيانات في معالج الحدث db.onversionchange، بل باستخدام معالج الحدث onblocked -في نافذة متصفح أخرى- لتنبيه المستخدم بأن النسخة الجديدة لن تُحمَّل قبل إغلاق النافذة الأخرى. لا يحدث التعارض في التحديث إلا نادرًا، ومع ذلك لا بدّ من توقعه والتعامل معه، على الأقل باستخدام معالج الحدث onblocked لمنع انهيار السكربت. مخزن الكائن نحتاج إلى مخزن الكائنات Object Store لتخزين أي شيء في قاعدة البيانات IndexedDB، وهو مفهوم جوهري فيها، ويقابل مفهوم "الجداول tables" أو "المجموعات collections" في قواعد البيانات الأخرى، كما تُخزّن فيه البيانات. وقد تتكون القاعدة من عدة مخازن، يكون الأول فيها للمستخدمين، والثاني للبضائع مثلًا وغيرها. على الرغم من اسم "مخزن الكائن" إلا أنه يمكن تخزين القيم الأولية فيه بالإضافة إلى الكائنات، إذ يمكن تخزين أي قيمة بما فيها الكائنات المعقدة، وتستخدم قاعدة البيانات خوارزمية التفكيك المعيارية standard serialization algorithm لنسخ وتخزين الكائن، ويشابه ذلك استخدام الأمر JSON.stringify لكن مع إمكانيات أكثر، وقدرة على تخزين أنواع أكثر من البيانات. ومن الكائنات التي لا يمكن تخزينها في هذه المخازن، نجد الكائن ذو المراجع الحلقية التي تشكل حلقةً يدل آخرها على أولها، والتي لا يمكن أن تُفكك، وسيفشل الأمر JSON.stringify معها. يجب أن يكون لكل قيمة ستُخزّن في قاعدة البيانات مفتاح فريد key، كما يجب أن تكون هذه المفاتيح من أحد الأنواع التالية: رقم أو تاريخ أو نص أو قيمة ثنائية أو مصفوفة، وسنتمكن من البحث عن القيم أو إزالتها أو تحديثها بواسطة هذه المفاتيح الفريدة. سنرى قريبًا كيف سنحصل على مفتاح عند إضافة قيمة جديدة إلى المخزن، كما يحدث عند استخدام الكائن localStorage، لكن ستسمح لنا قاعدة البيانات IndexedDB عند تخزين كائن؛ بضبط وإعداد خصائصه مثل مفتاح، وهذه مقاربة أفضل بكثير، كما يمكننا توليد المفاتيح تلقائيًا، لكننا سنحتاج إلى إنشاء مخزن للكائن أولًا. إليك الصيغة المستخدمة في إنشاء مخزن لكائن: db.createObjectStore(name[, keyOptions]); name: وتمثل اسم المخزن. keyOptions: وتمثل كائنًا اختياريًا له خاصيتان، هما: keyPath: المسار إلى خاصية الكائن التي سنستخدمها مفتاحًا، مثل id. autoIncrement: إذا كانت قيمته true فسيتولد تلقائيًا مفتاح للكائن الجديد، مثل رقم يتزايد باستمرار. إذا لم نستخدم keyOptions، فلا بدّ حينها من التصريح عن المفتاح لاحقًا عند تخزين الكائن. يستخدم مخزن الكائن التالي الخاصية id مفتاحًا، وبشكل صريح: db.createObjectStore('books', {keyPath: 'id'}); لا يمكن إنشاء أو تعديل مخزن كائن عند تحديث نسخة قاعدة البيانات إلا ضمن معالج الحدث upgradneeded، ويشكل هذا الأمر محدوديةً تقنيةً، إذ يمكن إضافة أو إزالة أو تحديث البيانات خارج المعالج، بينما لا يمكننا إنشاء أو حذف أو تغيير مخزن الكائن إلا خلال تحديث نسخة قاعدة البيانات. وتوجد مقاربتان لتنفيذ عملية تحديث نسخة قاعدة البيانات: يمكن كتابة دوال خاصة لتحديث كل نسخة ممكنة إلى النسخة الجديدة، من "1" إلى "2"، ومن "2" إلى "3" وهكذا، ثم يمكن موازنة النسخ ضمن جسم دالة معالج الحدث upgradeneeded من القديمة 2 إلى الحديثة 4 مثلًا، وتنفيذ الدالة المناسبة لتنفيذ التحديث خطوةً بخطوة، أي من 2 إلى 3، ثم من 3 إلى 4. فحص قاعدة البيانات والحصول على قائمة بمخازن الكائنات الموجودة باستخدام التابع db.objectStoreNames، ويمثل الكائن المُعاد قائمةً من النوع DOMStringList، والذي يزودنا بالتابع (contains(name الذي يتحقق من وجود مخزن باسم محدد، ثم سنتمكن من إجراء التحديث اعتمادًا على ما هو موجود وما هو غير موجود، وهذه المقاربة أبسط لقواعد البيانات الصغيرة، وإليك مثالًا عن استخدامها: let openRequest = indexedDB.open("db", 2); // تحديث وإضافة قواعد البيانات دون تحقق openRequest.onupgradeneeded = function() { let db = openRequest.result; if (!db.objectStoreNames.contains('books')) { // "books" إن لم يكن هناك مخزن باسم db.createObjectStore('books', {keyPath: 'id'}); // أنشئه } }; ولحذف مخزن كائن: db.deleteObjectStore('books') الإجرائيات المترابطة Transactions يُعَد مصطلح "إجرائيات مترابطة" مصطلحًا عامًا، ويُستخدم في قواعد بيانات عديدة، كما يشير إلى مجموعة من العمليات التي ينبغي أن تُنفَّذ مترابطةً، بحيث تنجح معًا أو تخفق معًا، فعندما يشتري شخص شيئًا ما مثلًا، فعلينا: سحب المبلغ من حساب المشتري. إضافة المشتريات إلى سلة مشترياته. سيبدو الأمر سيئًا بالطبع لو أُنجزت الخطوة الأولى بنجاح وفشلت الخطوة الثانية لسبب أو لآخر، فإما أن تنجح الخطوتان -أي يكتمل الشراء- أو تخفق الخطوتان، فعندها لن يخسر المشتري ماله ويستطيع إعادة المحاولة مجددًا. ينبغي أن تُنفَّذ جميع العمليات على البيانات ضمن إجرائيات مترابطة في القاعدة IndexedDB، وللبدء بإجرائية مترابطة يجب تنفيذ الأمر: db.transaction(store[, type]); store: اسم المخزن الذي ستُنفَّّذ عليه إجرائية مترابطة، المخزن "books" مثلًا، ويمكن أن يكون مصفوفةً من أسماء المخازن إذا كنا نريد الوصول إلى عدة مخازن. type: نوع الإجرائية المترابطة، وقد يكون: readonly: يُنفِّذ عمليات قراءة فقط، وهذا هو الخيار الافتراضي. readwrite: يُنفِّذ عمليات قراءة وكتابة فقط، ولا يستطيع إنشاء أو حذف أو تبديل الكائن. كما يمكن تنفيذ إجرائيات مترابطة من النوع versionchange، ويستطيع هذا النوع تنفيذ أي شيء، لكن لا يمكن إنشاؤه يدويًا، إذ تنشئ قاعدة البيانات الإجرائيات المترابطة من النوع versionchange عند تأسيس الاتصال مع قاعدة البيانات لتنفيذ معالج الحدث updateneeded، لذا فهو المكان الوحيد الذي نستطيع فيه تحديث هيكلية قاعدة البيانات أو إنشاء وحذف كائن المخزن. يمكن إضافة العناصر إلى المخزن بعد إنشاء الإجرائية المترابطة بالشكل التالي: let transaction = db.transaction("books", "readwrite"); // (1) // الحصول على كائن المخزن للتعامل معه let books = transaction.objectStore("books"); // (2) let book = { id: 'js', price: 10, created: new Date() }; let request = books.add(book); // (3) request.onsuccess = function() { // (4) console.log("Book added to the store", request.result); }; request.onerror = function() { console.log("Error", request.error); }; تُنفَّذ العملية مبدئيًا وفق خطوات أربع هي: إنشاء إجرائية مترابطة تشير إلى كل المخازن الذي سنصل إليها. الحصول على كائن المخزن باستخدام الأمر (transaction.objectStore(name. تنفيذ العمليات على كائن المخزن (books.add(book. التعامل مع حالة نجاح أو فشل الإجرائية المترابطة على كائن المخزن (books.add(book. تدعم كائنات المخازن تابعين لتخزين القيم، هما: ([put(value,[key: حيث يضيف القيمة value إلى المخزن، ويزوَّد التابع بالمعامل key فقط في الحالة التي لا يمتلك فيها المخزن أحد الخيارين keyPath أو autoIncrement، فإن وجدت قيمة لها نفس المفتاح فستُستبدَل. ([add(value,[key: يشابه التابع السابق، لكن إن وجدت قيمة لها نفس المفتاح فسيخفق الطلب، وسيولِّد خطأً باسم "ConstraintError". يمكن إرسال الطلب (books.add(book مثلًا بما يتناسب مع تأسيس اتصال مع قاعدة بيانات، ومن ثم ننتظر وقوع أحد الحدثين success/error. سيكون مفتاح الكائن الجديد هو نتيجة الطلب request.result للتابع add. فإن وقع خطأ ما فسنجده في الكائن request.error. اكتمال الإجراءات المترابطة بدأنا الإجرائية المترابطة في المثال السابق بتنفيذ الطلب add، لكن -وكما أشرنا سابقًا- قد تتألف الإجرائية المترابطة من عدة طلبات ينبغي أن تنجح معًا أو تخفق معًا، فكيف سنميِّز إذًا انتهاء الإجرائية ولا توجد طلبات أخرى قيد التنفيذ؟ الجواب باختصار هو أننا لا نستطيع، ولا بدّ من وجود طريقة يدوية لإنهاء الإجرائيات المترابطة في النسخة التالية 3.0 من التوصيفات، لكن حاليًا في النسخة 2.0 لا يوجد شيء مشابه لهذا. وستكتمل الإجرائية تلقائيًا عندما تنتهي جميع الطلبات المتعلقة بإجرائية مترابطة، وسيصبح صف المهام المتناهية الصغر microtasks queue فارغًا. توصف الإجرائية عادةً بأنها مكتملة عندما تكتمل كل طلباتها، وينتهي تنفيذ الشيفرة المتعلقة بها، لذا فلا حاجة في الشيفرة السابقة مثلًا إلى استدعاء خاص لإنهاء الإجرائية المترابطة. ويترافق مبدأ الاكتمال التلقائي للإجرائيات المترابطة بتأثير جانبي مهم، فلا يمكن تنفيذ عملية غير متزامنة مثل fetch أو setTimeout أثناء تنفيذ الإجرائية، كما لن تبقي قاعدة البيانات IndexedDB الإجرائية المترابطة في حالة انتظار حتى تُنجز هذه العملية. سيُخفق الطلب request2 في السطر(*) من الشيفرة التالية لأن الإجرائية قد اكتملت بالفعل، ولن نتمكن من تنفيذ طلبات أخرى: let request1 = books.add(book); request1.onsuccess = function() { fetch('/').then(response => { let request2 = books.add(anotherBook); // (*) request2.onerror = function() { console.log(request2.error.name); // TransactionInactiveError }; }); }; لأنّ fetch عملية غير متزامنة وتمثل مهمةً مستقلةً macrotask، وستُغلَق الإجرائية المترابطة قبل أن يبدأ المتصفح بتنفيذ مهمات مستقلة، كما سيرى محررو توصيفات قاعدة البيانات IndexedDB أنّ مدة إنجاز الإجرائيات المترابطة لا بدّ أن تكون قصيرة، وذلك لأسباب تتعلق بالأداء في الغالب. تقفِل الإجرائيات المترابطة من النوع readwrite المخزن عند الكتابة فيه، لذا إذا حاول جزء آخر من التطبيق تنفيذ عملية مشابهة على نفس المخزن، فعليه الانتظار. ستعلَّق الإجرائية المترابطة الجديدة حتى تنتهي الأولى، وهذا ما يسبب تأخيرًا غريبًا إذا استغرقت إجرائية مترابطة ما وقتًا طويلًا، فما العمل إذًا؟ بإمكاننا في المثال السابق تنفيذ إجرائية مترابطة جديدة db.transaction تمامًا قبل الطلب الجديد في السطر "(*)"، لكن يفضَّل -إن أردنا إبقاء العمليات معًا في إجرائية مترابطة واحدة- أن نفصل الإجرائية المترابطة في قاعدة البيانات IndexedDB عن الأمور الأخرى غير المتزامنة. نفِّذ العملية fetch أولًا، ثم حضر البيانات إن تطلب الأمر ذلك، ثم أنشئ إجرائيةً مترابطةً، ونفّذ كل الطلبات وسينجح الأمر. استمع إلى الحدث transaction.oncomplete لمعرفة اللحظة التي تكتمل فيها الإجرائية المترابطة بنجاح. let transaction = db.transaction("books", "readwrite"); // ...perform operations... transaction.oncomplete = function() { console.log("Transaction is complete"); }; تضمن الخاصية complete فقط اكتمال وحفظ الإجرائية بالكامل، وقد تنجح طلبات بمفردها، لكن قد تفشل بالمقابل عملية الكتابة النهائية، بسبب خطأ في منظومة الدخل/خرج مثلًا. استدعي التابع التالي لإيقاف الإجرائية المترابطة يدويًا: transaction.abort(); سيلغي هذا الاستدعاء كل التغييرات التي نفذتها الطلبات، ويتسبب بوقوع الحدث transaction.onabort. معالجة الأخطاء قد تُخفق طلبات الكتابة، ولا بدّ من توقع هذا الأمر -لا نتيجةً للأخطاء المحتملة التي قد نرتكبها فقط- بل لأسباب تتعلق بالإجرائيات المترابطة بحد ذاتها، فقد يحدث تجاوز حجم المخزن المحدد على سبيل المثال، لذا لا بدّ أن نكون مستعدين للتعامل مع حالات كهذه. يوقف إخفاق الطلب الإجرائية المترابطة تلقائيًا ويلغي كل التغييرات التي حدثت، لكننا قد نحتاج إلى التعامل مع حالة إخفاق الطلب، لتجريب طلب آخر مثلًا، دون إلغاء التغييرات التي حدثت، ومن ثم متابعة الإجرائية المترابطة، وهذا أمر ممكن، إذ يمكن لمعالج الحدث request.onerror أن يمنع إلغاء الإجرائية المترابطة عن طريق استدعاء التابع ()event.preventDefault. سنرى في المثال التالي كيف يُضاف كتاب جديد بمفتاح id موجود مسبقًا، وعندها سيولِّد التابع store.add الخطأ "ConstraintError"، الذي نتعامل معه دون إلغاء الإجرائية المترابطة: let transaction = db.transaction("books", "readwrite"); let book = { id: 'js', price: 10 }; let request = transaction.objectStore("books").add(book); request.onerror = function(event) { //عند إضافة قيمة بمفتاح موجود مسبقًا ConstraintError يقع الخطأ if (request.error.name == "ConstraintError") { console.log("Book with such id already exists"); // التعامل مع الخطأ event.preventDefault(); // لا تلغ الإجرائية المترابطة // استخدم مفتاح جديد للكتاب؟ } else { // خطأ غير متوقع لايمكن التعامل معه // ستلغى الإجرائية المترابطة } }; transaction.onabort = function() { console.log("Error", transaction.error); }; تفويض الأحداث هل نحتاج إلى الحدثين onsuccess/onerror عند كل طلب؟ والجواب هو لا، ليس في كل مرة، إذ يمكننا أن نستعمل تفويضًا للحدث event delegation بدلًا من ذلك. تجري عملية انسياب Bubbling الحدث في قاعدة بيانات بالشكل التالي: request → transaction → database والتي تقتضي التقاط العنصر الداخلي ضمن شجرة DOM للحدث، ثم تستمع إليه الأحداث الخارجية بالتتالي. قد تنساب الأحداث في الشجرة DOM للخارج bubbling أو للداخل capturing، لكن يُستخدم عادةً الانسياب نحو الخارج، ويمكن حينها التقاط الأخطاء عن طريق معالج الحدث db.onerror لإظهارها أو لأي أسباب أخرى. db.onerror = function(event) { let request = event.target; // الطلب الذي ولّد الخطأ console.log("Error", request.error); }; لكن ماذا لو تمكنا من التعامل مع الخطأ كاملًا؟ عندها لن نحتاج لإظهار أي شيء، يمكننا إيقاف الانسياب الخارجي للأحداث، وبالتالي إيقاف الحدث db.onerror، باستخدام الأمر ()event.stopPropagation ضمن دالة معالجة الحدث request.onerror. request.onerror = function(event) { if (request.error.name == "ConstraintError") { console.log("Book with such id already exists"); // معالجة الخطأ event.preventDefault(); // لا توقف الإجرائية المترابطة event.stopPropagation(); // لا تجعل الأحداث تنساب للخارج } else { // لا تفعل شيئًا // إيقاف الإجرائية المترابطة // transaction.onabort يمكن التعامل مع الخطأ ضمن } }; عمليات البحث يوجد نوعان أساسيان للبحث في مخزن الكائن: بقيمة المفتاح أو مجال المفتاح، ففي مثالنا عن المخزن "books"، سنتمكن من البحث عن قيمة أو مجال من القيم للمفتاح book.id. باستخدام حقل آخر من حقول الكائن، مثل البحث في مثالنا السابق اعتمادًا على الحقل book.price، ويتطلب هذا البحث هيكليةً إضافيةً للبيانات تُدعى الفهرس index. البحث بالمفتاح لنتعرف أولًا على النوع الأول، وهو البحث بالمفتاح، وتدعم طرق البحث حالة القيمة الدقيقة للمفتاح أو ما يسمى "مجالًا من القيم"، ويمثلها الكائن IDBKeyRange، وهي كائنات تحدد مجالًا مقبولًا من قيم المفاتيح، وتتولد الكائنات IDBKeyRange نتيجةً لاستخدام الاستدعاءات التالية: ([IDBKeyRange.lowerBound(lower, [open: وتعني أن تكون القيم أكبر أو تساوي الحد الأدنى lower، أو أكبر تمامًا إذا كانت قيمة open هي "true". ([IDBKeyRange.upperBound(upper, [open: تعني أن تكون القيم أصغر أو تساوي الحد الأعلى upper، أو أصغر تمامًا إذا كانت قيمة open هي "true". ([IDBKeyRange.bound(lower, upper, [lowerOpen], [upperOpen: تعني أن تكون القيمة محصورةً بين الحد الأدنى lower والأعلى upper، ولن يتضمن المجال قيمتي الحدين الأعلى والأدنى إذا ضبطنا open على القيمة "true". (IDBKeyRange.only(key: وتمثل مجالًا يتكون من مفتاح واحد، وهو نادر الاستخدام. سنرى التطبيق العملي لهذه الاستدعاءات السابقة قريبًا. ولتنفيذ بحث حقيقي ستجد التوابع التالية التي تقبل الوسيط query، وقد يكون هذا الوسيط قيمةً دقيقةً للمفتاح أو مجالًا: (store.get(query: يبحث عن أول قيمة من خلال مفتاح أو مجال. ([store.getAll([query], [count: يبحث عن جميع القيم، ويكون عدد القيم محدودًا إذا أعطينا قيمةً للوسيط count. (store.getKey(query: يبحث عن أول مفتاح يحقق الاستعلام، وعادةً يكون مجالًا. ([store.getAllKeys([query], [count: يبحث عن كل المفاتيح التي تحقق الاستعلام، ويكون عدد النتائج محدودًا إذا أعطينا count قيمةً. [(store.count([query: يعيد العدد الكلي للمفاتيح التي تحقق الاستعلام، وعادةً يكون مجالًا. قد يكون لدينا على سبيل المثال الكثير من الكتب في مخزننا، وما دام الحقل id هو المفتاح، فستتمكن تلك التوابع من البحث باستعماله، مثل: // الحصول على كتاب واحد books.get('js') // 'css' <= id <= 'html' الحصول على كتاب يكون books.getAll(IDBKeyRange.bound('css', 'html')) // id < 'html' الحصول على كتاب بحيث books.getAll(IDBKeyRange.upperBound('html', true)) // الحصول على كل الكتب books.getAll() // id > 'js' الحصول على كل الكتب التي تحقق books.getAllKeys(IDBKeyRange.lowerBound('js', true)) البحث بالحقول نحتاج إلى إنشاء هيكلية إضافية للبيانات تُدعى الفهرس index، لتنفيذ استعلام باستخدام حقل آخر من حقول الكائن، ويمثِّل الفهرس إضافةً add-on إلى المخزن لتتبع حقل محدد من كائن، ويُخزّن الفهرس -ومن أجل كل قيمة للحقل المحدد- قائمةً من مفاتيح الكائنات التي تمتلك تلك القيمة، وسنوضح ذلك لاحقًا بالتفصيل. إليك صيغة إنشاء الفهرس: objectStore.createIndex(name, keyPath, [options]); name: اسم الفهرس. keyPath: المسار إلى حقل الكائن الذي سيتتبعه الفهرس، وسنبحث اعتمادًا على ذلك الحقل. option: كائن اختياري له الخصائص التالية: unique: إذا كانت قيمته "true"، فيجب أن يوجد كائن واحد في المخزن له القيمة المعطاة في المسار المحدد، وسيجبر الفهرس تنفيذ هذا الأمر بتوليد خطأ إذا حاولنا إضافة نسخة مكررة. multiEntry: ويستخدَم فقط إذا كانت القيمة في المسار المحدد keyPath مصفوفةً، وسيعامل الفهرس في هذه الحالة المصفوفة بأكملها مثل مفتاح افتراضيًا، لكن إذا كانت قيمة multiEntry هي "true"، فسيحتفظ الفهرس بقائمة من كائنات المخزن لكل قيمة في تلك المصفوفة، وهكذا ستصبح عناصر المصفوفة مفاتيح فهرسة. نخزّن في المثال التالي الكتب باستعمال المفتاح id، ولنقل أننا نريد البحث باستعمال الحقل price، سنحتاج أولًا إلى إنشاء فهرس، ويجب أن ننجز ذلك ضمن معالج الحدث upgradeneeded تمامًا مثل مخزن الكائن: openRequest.onupgradeneeded = function() { //لابد من إنشاء الفهرس هنا، ضمن إجرائية تغيير النسخة let books = db.createObjectStore('books', {keyPath: 'id'}); let index = books.createIndex('price_idx', 'price'); }; سيتعقب الفهرس الحقل price. لن يكون السعر حقلًا بقيم فريدة، إذ يمكن وجود عدة كتب لها السعر نفسه، وبالتالي لن نضبط الخيار unique. السعر قيمة مفردة وليس مصفوفةً، لذا لن نطبّق الخيار multiEntry. لنتخيل الآن وجود 4 كتب في مخزننا inventory، إليك الصورة التي تظهر طبيعة الفهرس index: كما قلنا سابقًا، سيحفَظ الفهرس العائد لكل قيمة من قيم price -الوسيط الثاني- قائمةً بالمفاتيح التي ترتبط بهذا السعر، ويُحدَّث الفهرس تلقائيًا، فلا حاجة للاهتمام بهذا الأمر. سنطبِّق ببساطة التوابع السابقة نفسها على الفهرس عندما نريد أن نبحث عن سعر محدد: let transaction = db.transaction("books"); // قراءة فقط let books = transaction.objectStore("books"); let priceIndex = books.index("price_idx"); let request = priceIndex.getAll(10); request.onsuccess = function() { if (request.result !== undefined) { console.log("Books", request.result); // مصفوفة من الكتبالتي سعرها=10 } else { console.log("No such books"); } }; يمكن أن نستخدم IDBKeyRange أيضًا لإنشاء مجال محدد، والبحث عن الكتب الرخيصة أو باهظة الثمن: // جد الكتب التي سعرها يساوي أو أقل من 5 let request = priceIndex.getAll(IDBKeyRange.upperBound(5)); تُصنَّف الفهارس داخليًا وفق الحقل الذي تتعقبه، وهو السعر price في حالتنا، لذا سترتَّب النتائج حسب السعر عندما ننفذ البحث وفق حقل السعر. الحذف من مخزن يبحث التابع delete عن القيم التي يُطلب حذفها عبر استعلام، وله صيغة شبيهة بالتابع getAll. (delete(query: يحذف القيم المطابقة للاستعلام، إليك مثالًا: // id='js' احذف الكتاب الذي يحقق books.delete('js'); إذا أردنا حذف الكتب بناءً على سعرها أو بناءً على أي حقل آخر، فعلينا أولًا إيجاد المفتاح ضمن الفهرس، ثم استدعاء التابع delete: // ايجاد المفتاح في حالة السعر=5 let request = priceIndex.getKey(5); request.onsuccess = function() { let id = request.result; let deleteRequest = books.delete(id); }; ولحذف كل شيء: books.clear(); // افرغ المخزن المؤشرات Cursors تعيد التوابع -مثل getAll/getAllKeys- مصفوفةً من المفاتيح والقيم، وقد يكون كائن التخزين ضخمًا وأكبر من أن تتسع له الذاكرة المتاحة، وبالتالي سيخفق التابع getAll في الحصول على كل سجلات المصفوفة، فما العمل في حالة مثل هذه؟ ستساعدنا المؤشرات على الالتفاف على هذه المشكلة. المؤشرات هي كائنات خاصة تتجاوز كائن التخزين عند تنفيذ استعلام، وتعيد زوجًا واحدًا (مفتاح/قيمة) في كل مرة، وبالتالي ستساعدنا في توفير الذاكرة. ما دام مخزن الكائن سيُصنَّف وفقًا للمفتاح، فسينتقل المؤشر عبر المخزن وفق ترتيب المفتاح تصاعديًا (افتراضيًا). إليك صيغة استخدام المؤشر: // لكن مع مؤشر getAll مثل let request = store.openCursor(query, [direction]); // store.openKeyCursor للحصول على المفاتيح لا القيم query: مفتاح أو مجال لمفتاح، ويشابه في عمله getAll. direction: وهو وسيط اختياري، ويفيد في ترتيب تنقل المؤشر بين السجلات: next: وهي القيمة الافتراضية، حيث يتحرك المؤشر من المفتاح ذي القيمة الأدنى إلى الأعلى. prev: يتحرك المؤشر من القيمة العليا للمفتاح إلى الدنيا. nextunique وprevunique: تشابهان الخيارين السابقين، لكنهما تتجاوزان المفتاح المكرر، وتعملان فقط مع المؤشرات المبنية على فهارس، فعند وجود عدة قيم لن تُعاد إلا قيمة أول سعر يحقق معيار البحث. إن الاختلاف الرئيسي في عمل المؤشرات هو أنها تسبب وقوع الحدث request.onsuccess عدة مرات، مرةً عند كل نتيجة. إليك مثالًا عن استخدام المؤشر: let transaction = db.transaction("books"); let books = transaction.objectStore("books"); let request = books.openCursor(); // يُستدعى من أجل كل كتاب وجده المؤشر request.onsuccess = function() { let cursor = request.result; if (cursor) { let key = cursor.key; // (id) مفتاح الكتاب let value = cursor.value; // كائن الكتاب console.log(key, value); cursor.continue(); } else { console.log("No more books"); } }; للمؤشر التوابع التالية: (advance(count: يدفع المؤشر إلى الأمام بمقدار الوسيط count ويتجاوز القيم. ([continue([key]): يدفع المؤشر إلى القيمة التالية في المجال المطابق للبحث، أو إلى ما بعد مفتاح معين مباشرةً إذا حددنا القيمة key. يُستدعى معالج الحدث onsuccess، سواءً تعددت القيم التي تطابق معيار البحث أو لا، ثم سنتمكن من الحصول على المؤشر الذي يدل على السجل التالي ضمن النتيجة result التي حصلنا عليها، أو أن لا يؤشر إلى شيء unidefined. تدريب أنشئ المؤشر في المثال السابق لمخزن الكائن. يمكن أيضًا أن ننشئ المؤشر اعتمادًا على الفهارس، إذ تسمح الفهارس كما نتذكر بالبحث وفق أي حقل من حقول الكائن، وتتشابه المؤشرات المبنية على الفهارس مع تلك المبنية على أساس مخازن الكائنات، فهي توفر الذاكرة بإعادة قيمة واحدة في كل مرة. سيكون cursor.key مفتاح الفهرسة بالنسبة للمؤشرات المبنية على الفهارس، وينبغي أن نستخدم الخاصية cursor.primaryKey إذا أردنا الحصول على مفتاح الكائن. let request = priceIdx.openCursor(IDBKeyRange.upperBound(5)); // يُستدعى لكل سجل request.onsuccess = function() { let cursor = request.result; if (cursor) { let primaryKey = cursor.primaryKey; // مفتاح مخزن الكائن التالي (id field) let value = cursor.value; // مفتاح مخزن الكائن التالي (book object) let key = cursor.key; //مفتاح الفهرسة التالي (price) console.log(key, value); cursor.continue(); } else { console.log("No more books"); } }; مغلف الوعود Promise wrapper إنّ إضافة الحدثين onsuccess/onerror إلى كل طلب أمر مرهق، ويمكن أحيانًا تحسين الوضع باستخدام التفويض delegation، مثل إعداد معالجات أحداث للإجرائية المترابطة بأكملها، لكن الصيغة async/await أكثر ملائمةً. لنستخدم مُغلَّف الحدث البسيط idb في هذا المقال، حيث ننشئ كائن idb عامًا مزوّدًا بتوابع مبنية على الوعود promisified لقاعدة البيانات IndexedDB، وسنكتب الشيفرة التالية بدلًا من استخدام onsuccess/onerror: let db = await idb.openDB('store', 1, db => { if (db.oldVersion == 0) { // نفذ عملية التهيئة db.createObjectStore('books', {keyPath: 'id'}); } }); let transaction = db.transaction('books', 'readwrite'); let books = transaction.objectStore('books'); try { await books.add(...); await books.add(...); await transaction.complete; console.log('jsbook saved'); } catch(err) { console.log('error', err.message); } وهكذا سنرى الشيفرة المحببة للجميع، "شيفرة غير متزامنة " وكتلة "try…catch". معالجة الأخطاء إذا لم نلتقط الأخطاء، فستقع إلى أن تعترضها أول كتلة try..catch. ستتحول الأخطاء التي لا نعترضها إلى حدث "عملية رفض وعد غير معالجة" ضمن الكائن window، لكن يمكن التعامل مع هذا الخطأ بالشكل التالي: window.addEventListener('unhandledrejection', event => { let request = event.target; // IndexedDB كائن طلب أصلي للقاعدة let error = event.reason; //request.error خطأ غير مُعتَرض للكائن ...report about the error... }); إجرائية مترابطة غير فعالة تكتمل الإجرائية المترابطة -كما رأينا سابقًا- عندما ينتهي المتصفح من تنفيذ شيفرتها ومهامها المتناهية الصغر، فلو وضعنا مهمةً مستقلةً مثل fetch وسط إجرائية مترابطة، فلن تنتظر عندها الإجرائية انتهاء هذه المهمة المستقلة، بل ستكتمل الإجرائية ببساطة وسيخفق الطلب التالي. يبقى الأمر نفسه بالنسبة إلى مُغلَّف الوعود والصيغة async/await، إليك مثالًا عن العملية fetch داخل إجرائية مترابطة: let transaction = db.transaction("inventory", "readwrite"); let inventory = transaction.objectStore("inventory"); await inventory.add({ id: 'js', price: 10, created: new Date() }); await fetch(...); // (*) await inventory.add({ id: 'js', price: 10, created: new Date() }); // Error سيخفق الأمر inventory.add بعد العملية fetch في السطر "(*)" وسيقع الخطأ “inactive transaction” أي "إجرائية مترابطة غير فعالة"، لأنّ الإجرائية المترابطة قد اكتملت بالفعل وأغلقت، وسنلتف على هذه المشكلة كما فعلنا سابقًا، فإما أن ننشئ إجرائيةً مرتبطةً جديدةً، أو أن نفصل الأمور عن بعضها. حضّر البيانات ثم أحضر كل ما يلزم عبر fetch أولًا. احفظ البيانات داخل قاعدة البيانات. الحصول على كائنات أصيلة يُنفِّذ المُغلَّف طلب IndexedDB أصلي داخليًا، ويضيف معالجي الحدثين onerror/onsuccess إليه، ثم يعيد وعدًا يُرفَض أو يُنفَّذ مع نتيجة الطلب. يعمل هذا الأمر جيّدًا في معظم الأوقات، وستجد مثالًا في مستودع GitHub الخاص بمُغلًّف الحدث idb. وفي حالات قليلة نادرة، وعندما نحتاج إلى الكائن request الأصلي، يمكن الوصول إليه باستخدام الخاصية promise.request العائدة للوعد. let promise = books.add(book); // الحصول على الوعد دون انتظار النتيجة let request = promise.request; // كائن الطلب الأصلي let transaction = request.transaction; // كائن الإجرائية المترابطة الأصلي // ...do some native IndexedDB voodoo... let result = await promise; // إن كنا بحاجة لذلك الخلاصة يمكن تشبيه قاعدة البيانات بالكائن "localStorage" لكنها مدعَّمة، وهي قاعدة بيانات (مفتاح-قيمة) لها إمكانيات كبيرة كافية للتطبيقات التي تعمل دون اتصال، كما أنها سهلة الاستخدام. تُمثِّل التوصيفات الخاصة بقاعدة البيانات أفضل دليل لاستخدامها، ونسختها الحالية هي 2.0، كما ستجد بعض التوابع من النسخة 3.0 التي لن تختلف كثيرًا، وهي مدعومة جزئيًا. يمكن تلخيص طريقة استخدامها الأساسية بالشكل التالي: أولًا، الحصول على مُغلِّف وعود مثل idb. ثانيًا، فتح قاعدة البيانات. idb.openDb(name, version, onupgradeneeded) إنشاء مخزن للكائن وفهارس ضمن معالج الحدث onupgradeneeded، أو تنفيذ تحديث للنسخة عند الحاجة. ثالثًا، لتنفيذ الطلبات عليك باتباع الآتي: أنشئ إجرائيةً مترابطةً ('db.transaction('books، مع إمكانية القراءة والكتابة عند الحاجة. احصل على مخزن كائن بالشكل التالي ('transaction.objectStore('books. رابعًا، يمكنك البحث بالمفتاح أو استدعاء التوابع المتعلقة بكائن المخزن مباشرةً. أنشئ فهرسًا للبحث باستخدام حقل آخر من حقول الكائن. خامسًا، إذا لم تتسع الذاكرة للبيانات، فاستخدم مؤشرًا cursor. إليك هذا المثال النموذجي: <!doctype html> <script src="https://cdn.jsdelivr.net/npm/idb@3.0.2/build/idb.min.js"></script> <button onclick="addBook()">Add a book</button> <button onclick="clearBooks()">Clear books</button> <p>Books list:</p> <ul id="listElem"></ul> <script> let db; init(); async function init() { db = await idb.openDb('booksDb', 1, db => { db.createObjectStore('books', {keyPath: 'name'}); }); list(); } async function list() { let tx = db.transaction('books'); let bookStore = tx.objectStore('books'); let books = await bookStore.getAll(); if (books.length) { listElem.innerHTML = books.map(book => `<li> name: ${book.name}, price: ${book.price} </li>`).join(''); } else { listElem.innerHTML = '<li>No books yet. Please add books.</li>' } } async function clearBooks() { let tx = db.transaction('books', 'readwrite'); await tx.objectStore('books').clear(); await list(); } async function addBook() { let name = prompt("Book name?"); let price = +prompt("Book price?"); let tx = db.transaction('books', 'readwrite'); try { await tx.objectStore('books').add({name, price}); await list(); } catch(err) { if (err.name == 'ConstraintError') { alert("Such book exists already"); await addBook(); } else { throw err; } } } window.addEventListener('unhandledrejection', event => { alert("Error: " + event.reason.message); }); </script> وستكون النتيجة كالتالي: ترجمة -وبتصرف- للفصل indexeddb من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: ملفات تعريف الارتباط وضبطها في JavaScript تعرّف على IndexedDB مفهوم Service Worker وتأثيره في أداء وبنية مواقع وتطبيقات الويب
-
ستتعرف من خلال سلسلة المقالات هذه على تفاصيل جهاز راسبيري باي؛ الحاسوب الجديد الذي لا يتعدى حجمه حجم بطاقتك الإئتمانية، وستكتشف الكمّ الكبير من المكونات التي يدعمها، وما الذي ستقدمه لك هذه المكونات. يُعَد راسبيري باي Raspberry Pi جهازًا مميزًا، فهو حاسوبٌ متكامل وظيفيًا ضمن علبةٍ صغيرة رخيصة الثمن، فلو كانت غايتك هي جهازٌ تستخدمه لتصفح الإنترنت أو للألعاب، أو كنت مهتمًا بتطوير برامجك الخاصة أو تصميم دوائرك الإلكترونية أو تجهيزاتٍ خاصة بك، فجهاز راسبيري باي ومجتمع هواته ومحترفيه الرائعين سيشكلان السند الحقيقي لك في كل خطوةٍ تخطوها. تُعرف أجهزة راسبيري باي بأنها حواسيبٌ مجمّعةٌ على لوحة إلكترونية مفردة single board؛ فهي عبارة عن حاسوب مثل الحواسيب المكتبية أو المحمولة أو الهواتف الذكية، لكنه جُمِّع على لوحةٍ واحدةٍ طُبعت عليها جميع الدوائر الإكترونية المُكوِّنة له. يتمتع راسبيري باي مثل غيره من الحواسيب وحيدة اللوحة بميزةٍ مهمة، وهي أنه صغير الحجم، حيث لا يتعدى حجمه حجم البطاقة الائتمانية، ولا يعني ذلك إطلاقًا أنّ قدرته أضعف بل يستطيع تنفيذ ما يستطيع تنفيذه أي حاسوبٍ أكبر وأكثر استهلاكًا للطاقة، لكن ليس بالسرعة نفسها بالضرورة. ظهرت عائلات راسبيري باي نتيجةً للرغبة في تشجيع طرقٍ أكثر تقدّمًا في تعليم الحوسبة حول العالم، وقد انطلقت من فكرةٍ بسيطةٍ تبناها مخترعي راسبيري، الذين أنشؤوا لاحقًا مؤسسة راسبيري باي غير الربحية والتي أثبتت شعبيتها الكبيرة، فقد بيعت بضعة آلافٍ من القطع التي جُمِّعت عام 2012 لاختبار أنواع المياه، وتبعها مباشرةً شحنٌ لملايين الأجهزة إلى كل أصقاع الأرض في السنوات التالية. وجدت هذ التجهيزات طريقها إلى المنازل وصفوف التعليم والمكاتب ومراكز البيانات والمعامل، وحتى إلى الزوارق المقادة ذاتيًا والبالونات التي تُطلق إلى الفضاء. أُصدرت نماذجٌ متعددة من راسبيري باي تباعًا بعد إصدار النموذج الأصلي B، وحمل كلٌ منها مواصفاتٍ محسنة أو ميزاتٍ مخصصة لاستخداماتٍ محددة، حيث تتميز العائلة راسبيري باي زيرو Raspberry Pi Zero مثلًا، بأنها نسخةٌ مصغرةٌ عن أجهزة راسبيري باي ذات الأبعاد الكاملة، لكنها تفتقر إلى ميزاتٍ عدة وخصوصًا منافذ USB المتعددة ومنفذ الاتصال مع الشبكات السلكية حتى يكون مخططها أصغر وأقل استهلاكًا للطاقة الكهربائية. تشترك جميع نماذج راسبيري باي بميزةٍ هامةٍ، وهي التوافق Compatibility؛ فالبرنامج الذي تكتبه لنموذجٍ ما، سيعمل على كل النماذج الأخرى، كما أنك قادرٌ على نقل أحدث نسخةٍ من نظام تشغيل راسبيري باي وتشغيله على النموذج الأولي الأصلي (نموذج B)، حيث سيعمل ببطء بالتأكيد، لكنه سيعمل. سنتعرف خلال سلسلة المقالات هذه على راسبيري باي 4 نموذج B، وهو النموذج الأحدث والأقوى من أجهزة راسبيري باي، ومع ذلك من السهل تطبيق ما تعلمته على أية نماذجٍ أخرى من عائلة راسبيري باي، حيث لا داعي للقلق مما إذا كان نموذجك مختلفًا. تُظهر أجهزة راسبيري باي جميع مكوّناتها ومنافذ اتصالها وجميع ميزاتها، على خلاف الحواسيب التقليدية التي تخفي تفاصيلها في حاويتها cases، ويمكنك طبعًا شراء حاويةٍ مستقلةٍ لراسبيري باي لحماية الجهاز إن أردت. سيتيح لك ذلك فرصةً رائعةً لتعلم كيفية عمل الأجزاء المختلفة للحاسب، وسيسهل عليك معرفة ماذا يحدث وأين، وذلك عندما نصل إلى المرحلة التي نتعلم فيها طريقة توصيل أجهزة إضافيةٍ إلى لوحتك، أو ما يُعرف بالطرفيات peripherals. شكل 1-1 راسبيري باي 4 نموذج B يُظهر الشكل السابق جهاز راسبيري باي 4 نموذج B كما يبدو من الأعلى. حاول أن تُبقي لوحة الجهاز في الوضعية التي نعرضها في الصورة عندما تستخدم هذا الكتاب لتعلم راسبير باي، فقد تختلط عليك الأمور في بعض مراحل العمل، مثل ترتيب الأرجل في واجهة الدخل والخرج العمومية GPIO، والذي سنتحدث عنه بالتفصيل في الفصل السادس. وعلى الرغم من وجود الكثير من الأشياء المُدمجة في لوحة راسبيري باي الصغيرة، لكن فهم هذا الجهاز يُعَد أمرًا يسيرًا. وسنبدأ بمكوّناته، وهي الدوائر التي تجعل كل شيءٍ يعمل. مكونات راسبيري باي يتألف الجهاز مثل بقية الحواسيب التقليدية من مكوّناتٍ مختلفة لكلٍ دورها في المنظومة، حيث ستجد المكوّن الأكثر أهميةً قريبًا من مركز اللوحة على الوجه العلوي مغطّىً بغطاءٍ معدني، وهو رقاقة النظام المدمجة System-On-Chip -أو اختصارًا SoC-، كما هو موضحٌ في الشكل التالي. شكل 2-1 رقاقة النظام المدمجة يوحي اسم هذا المكوّن بما ستجده إذا نزعت الغطاء المعدني، وهو رقاقةٌ سيليكونيةٌ معروفةٌ باسم الدارة المتكاملة integrated circuit، وتضم كامل منظومة راسبيري باي بما فيها وحدة المعالج المركزية Central Processing Unit -أو اختصارًا CPU-، التي تمثل من الناحية الوظيفية دماغ الحاسب، ووحدة معالجة الرسوميات Graphics Processing Unit -أو اختصارًا GPU-، التي تتعامل مع نواحي الإظهار البصري. وطالما أن الدماغ سيغدو بلا فائدةٍ دون ذاكرة، فستجد إلى جانب رقاقة النظام رقاقةً أخرى تبدو مثل مربع بلاستيكي أسود كما هو موضحٌ في الشكل الآتي، وهي ذاكرة الوصول العشوائي Random Access Memory -أواختصارًا RAM-. ستكون ذواكر الوصول العشوائي مسرحًا لتنفيذ كل أعمالك التي لن تنتقل إلى الذاكرة الدائمة المتمثّلة ببطاقة ذاكرة من النوع microSD إلا عندما تخزِّن عملك. يُعرف نوعا الذاكرة السابقين بالذواكر المتطايرة والذواكر الدائمة غير المتطايرة؛ حيث تفقد ذواكر الوصول العشوائي المتطايرة RAM محتواها عند انقطاع التغذية الكهربائية، بينما تحتفظ الذاكرة غير المتطايرة الموجودة على بطاقة microSD بمحتواها. شكل 3-1 ذاكرة الوصول العشوائي لجهاز راسبيري باي ستجد على الزاوية اليمينية العليا من اللوحة غطاءً معدنيًا آخر يغطي منظومة الراديو التي تمنح جهاز راسبيري باي القدرة على الاتصال اللاسلكي كما هو موضحٌ في الشكل التالي، وتضم المنظومة وظيفيًا مكوّنين، أحدهما مكوّن الاتصال اللاسلكي بتقنية WiFi للاتصال مع شبكات الحاسب، والآخر مكوّن الاتصال اللاسلكي بتقنية بلوتوث Bluetooth للاتصال مع الطرفيات مثل الفأرة، أو نقل البيانات من وإلى الأجهزة الذكية المجاورة مثل الهواتف والحساسات. شكل 4-1 وحدة الاتصال الراديوي تُشاهد على الحافة السفلية للوحة رقاقةً سوداء أخرى مغلفةً بالبلاستيك خلف مجموعة منافذ الناقل التسلسلي العالمي Universal serial Bus -أو اختصارًا USB-؛ وهي رقاقةٌ التحكم بمنافذ USB والمسؤولة عن تشغيلها، كما ستجد إلى جانبها رقاقةً التحكم بالشبكة السلكية؛ وهي رقاقةٌ سوداء أصغر حجمًا تدير منفذ الاتصال السلكي المحلي Ethernet. ستجد في الناحية اليسارية العليا وإلى الأعلى قليلًا من مأخذ التغذية الكهربائية للّوحة عبر منفذ USB-C، رقاقةً سوداء أصغر حجمًا من بقية الرقاقات تُعرف باسم الدارة المتكاملة لإدارة الطاقة Power Management integrated circuit -أو اختصارًا PMIC-، كما هو موضحٌ في الشكل التالي؛ حيث تدير هذه الدارة الطاقة الكهربائية التي تصل عبر منفذ USB-C لتغطي حاجة جهاز راسبيري باي. شكل 5-1 الدارة المتكاملة لإدارة الطاقة PMIC لا تقلق إن بدا لك الأمر صعبًا، فلست مضطرًا إلى معرفة موضع كل مكوّن أو دوره لتستخدم راسبيري باي. منافذ الاتصال في راسبيري باي يضم جهاز راسبيري باي مجموعةً من المنافذ التي سنستعرضها خلال السطور القليلة التالية. منافذ الناقل التسلسلي العالمي USB ستجد هذه المنافذ في وسط الحافة السفلية للوحة وإلى يمينها، وتمنحك القدرة على الاتصال مع الطرفيات المتوافقة مع الناقل USB، مثل الفأرة ولوحة المفاتيح وآلات التصوير الرقمية والذواكر المتنقلة flash drives. يوجد نوعين لمنافذ USB من الناحية التقنية هما على النحو الأتي: USB 2.0: صُمم وفقًا للنسخة الثانية من معيار الناقل التسلسلي العالمي، وبالإمكان تمييزه من خلال القطعة البلاستيكية السوداء داخله. USB 3.0: وهو المنفذ الأسرع بينهما، وصُمم وفقًا للنسخة الثالثة الأحدث من المعيار السابق، وتستطيع تمييزه من خلال القطعة البلاستيكية الزرقاء داخله. الشكل 6-1 منافذ USB لجهاز راسبيري باي منفذ الاتصال السلكي المحلي Ethernet يتواجد على يمين منافذ USB -كما هو موضحٌ في الشكل التالي-، ويُعرف أيضًا بمنفذ الشبكة network port، ويُستخدم للاتصال مع شبكات الحاسوب سلكيًا عبر كابلٍ ينتهي بمقبسٍ من النوع RJ45 شبيهٍ بمقبس سلك الهاتف لكنه أوسع. ستجد مؤشرين ضوئيين LEDs أسفل المنفذ يمثلان مؤشري الحالة؛ يعطيانك فكرةً عن نجاح الاتصال من عدمه. شكل 7-1 منفذ الاتصال السلكي لجهاز راسبيري باي مخرج صوتي-بصري AV يتواجد على الحافة اليسارية للوحة راسبيري باي فوق منفذ الاتصال السلكي، ويُعرف أيضًا باسم مخرج سماعات الرأس، وهو بقطر 3.5 ميليمتر، كما هو موضحٌ بالشكل التالي. يُستخدم هذا المخرج لوصل الجهاز مع سماعات الرأس، كما يمكن وصله مع مكبر صوت Speaker للحصول على جودةٍ صوتيةٍ أعلى. لهذا المخرج ميزةٌ خفية، إذ بإمكانه نقل إشارة الفيديو، وبالتالي وصله إلى الشاشات أو أجهزة الإسقاط وغيرها من أجهزة العرض التي تدعم إشارة الفيديو المركبة التي تنتقل عبر كابلٍ ينتهي بطرفية الغلاف مدبب الرأس ثنائي الحلقة tip-ring-ring-sleeve -أواختصارًا TRRS-، وذلك تمامًا مثل المقبس التقليدي لسماعات الرأس التي تتصل بالهواتف المحمولة. شكل 8-1 مخرج صوتي-بصري بقطر 3.5 ميليمتر مدخل كاميرا التصوير يتوضع فوق مخرج AV مباشرةً، ويأتي على شكل وصلةٍ غريبة المظهر مزودةٍ بغطاء بلاستيكي قابلٍ للسحب، ويُعرف أيضًا باسم واجهة الكاميرا التسلسلية Camera Serial Interface -أواختصارًا CSI-. يوضح الشكل التالي هذا المنفذ الذي يسمح بالاتصال مع الكاميرا المُصممة خصيصًا لجهاز راسبيري باي، والتي سنتعلم التعامل معها في الفصل الثامن. شكل 9-1 وصلة كاميرا راسبيري باي منفذ واجهة الوسائط المتعددة عالية الدقة micro-HDMI يتوضع على الحافة اليسارية للوحة الجهاز وفوق وصلة الكاميرا مباشرةً، وهو منفذٌ من النموذج micro الذي يأتي أصغر حجمًا من منافذ HDMI المعيارية التي تراها في وحدات الألعاب أو أجهزة التلفاز، كما هو موضحٌ في الشكل التالي. تنقل هذه المنافذ إشارات الصوت والفيديو بجودةٍ عالية، ويمكنك استخدامها لوصل راسبيري باي بجهازٍ أو جهازي عرض، مثل شاشات الحاسوب أو أجهزة التلفاز أو أجهزة الإسقاط. شكل 10-1 منفذ HDMI منفذ USB-C للتغذية بالطاقة الكهربائية يأتي مباشرةً فوق منفذ HDMI، ويُستخدم لإيصال التغذية الكهربائية إلى لوحة راسبيري باي كما هو موضحٌ في الشكل التالي. من المؤكد أنك رأيت منفذ USB-C في الهواتف الذكية والأجهزة اللوحية وغيرها من الأجهزة المحمولة. ولا يُنصح بالطبع استخدام شواحن الهواتف المحمولة في تغذية راسبيري باي على الرغم من إمكانية ذلك، كما يفضّل استخدام مصدر التغذية المعتمد من قبل راسبيري باي. شكل 11-1 منفذ USB-C كمدخل تغذية بالطاقة الكهربائية واجهة شاشة الإظهار وهي وصلةٌ شبيهةٌ بوصلة الكاميرا، حيث يوضح الشكل التالي موصل الشاشة display connector، أو واجهة شاشة الإظهار Display Serial Interface -أو اختصارًا DSI-، والتي تقع على الحافة العليا للوحة الجهاز، حيث تُستخدم لتوصيل شاشة اللمس الخاصة براسبيري باي الموضحة بالشكل الثاني. شكل 12-1 وصلة DSI الخاصة براسبيري باي شكل 13-1 شاشة لمس راسبيري باي منصة GPIO تكون منصة أرجل الدخل والخرج للأغراض العامة general-purpose input/output أو اختصارًا GPIO، متواجدةً على الحافة اليمينية للوحة راسبيري باي، وتتكون من 40 رِجلًا معدنيةً pins منتظمةً في صفين يضم كلٌ منهما 20 رجلًا كما هو موضحٌ في الشكل التالي. تستخدم راسبيري باي هذه الأرجل للتخاطب مع عناصر الوسط الخارجي، مثل المؤشرات الضوئية LEDs والأزرار وغيرها، بما في ذلك الحساسات الإلكترونية، مثل حسّاسات درجة الحرارة ومقابض الألعاب joysticks، وشاشات مراقبة معدلات النبض. سنتعرف على عمل هذه الأرجل في الفصل السادس المتمحور حول الحوسبة الفيزيائية وبرمجة المكوّنات باستخدام بايثون Python وسكراتش Scratch. شكل 14-1 منصة GPIO منصة أرجل التغذية عبر شبكة الاتصال المحلية PoE منصة PoE هي اختصارٌ للمصطلح Power Over Ethernet، وهي تقنيةٌ لنقل الطاقة الكهربائية عبر سلكين ضمن كابل الاتصال، وتُستخدم هذه المنصة التي تقع أسفل ويسار منصة GPIO، والمؤلفة من أربعة أرجل أصغر حجمًا، مثل إضافةٍ لتغذية جهاز راسبيري باي عبر شبكة الاتصال السلكية المحلية بدلًا من منفذ USB-C. موصل بطاقة الذاكرة من نوع microSD وهو المنفذ الأخير الذي نستعرضه، ويقع على الوجه السفلي للّوحة مقابل مأخذ شاشة الإظهار DSI، كما هو موضحٌ في الشكل التالي. تمثل بطاقة الذاكرة microSD أداة التخزين الدائمة لنظام راسبيري باي، حيث توضع هذه البطاقة داخل الحاضنة المخصصة لها لتخزِّن كل الملفات التي تحتاجها، وكل البرامج التي تثبتها بالإضافة إلى نظام التشغيل الذي يقود الجهاز. شكل 15-1 حاضنة بطاقة الذاكرة من نوع microSD طرفيات راسبيري باي لن يقدم لك جهاز راسبيري باي الكثير بمفرده، فحاله حال حاضنة الحاسوب المكتبي بمفردها، ومن أجل استغلال إمكانيات الجهاز لا بدّ من تأمين الطرفيات المناسبة، حيث ستحتاج بالحد الأدنى إلى بطاقة ذاكرة من نوع microSD لتخزين البيانات، وشاشة أو تلفاز لترى ما تفعل، إلى جانب لوحة مفاتيح وفأرة لإيصال تعليماتك، كما ستحتاج إلى مصدر تغذية بالطاقة الكهربائية يعطي الجهاز جهدًا ثابتًا مقداره 5 فولت وتيار شدته 3 أمبير أو أفضل. وهكذا ستحصل على حاسوبٍ قادرٍ على العمل تمامًا، وسنتعلم كيفية ربط هذه الطرفيات مع جهاز راسبيري باي في الفصل الثاني. لا تمثل الطرفيات التي أشرنا إليها كل ما يمكنك استخدامه مع راسبيري باي، حيث تضم قائمة الطرفيات الرسمية التي تنتجها راسبيري باي مايلي: حاضنة للوحة الجهاز Raspberry case، والتي تؤمن حمايةً للّوحة دون أن تعيق وصولك إلى المنافذ المختلفة للجهاز. تجهيزة الكاميرا Camera Module: ستجد تفاصيلها في الفصل الثامن. شاشة لمس راسبيري باي متصلة مع منفذ شاشة الإظهار وتؤمن عرضًا بصريًا وواجهة لمس على هيئة جدول. الطرفية Sense HAT متعددة الوظائف والموضحة في الشكل التالي، وهي طرفيةٌ ذكيةٌ ترتبط بالجهاز Hardware Attached Top، وتنفذ العديد من الوظائف التي سنتعرف عليها في الفصل السابع المتمحور حول الحوسبة الفيزيائية باستخدام Sense HAT. تجهيزات متنوعة من أطراف أخرى، حيث ستجد في الأسواق الكثير من الأدوات والتجهيزات تبدأ بتلك التي تحول راسبيري باي إلى حاسوبٍ محمول أو إضافاتٍ تمنح جهازك القدرة على فهم الكلام، وحتى الرد عليك. شكل 16-1 الطرفية Sense HAT تذكر أنك بحاجةٍ إلى تعلم الكثير عن راسبيري باي قبل شراء تلك الطرفيات. ترجمة -وبتصرف- للفصل الأول Get to Know your Raspberry Pi من كتاب The Official Raspberry Pi] Beginner's Guide. اقرأ أيضًا المدخل الشامل لتعلم علوم الحاسوب أنظمة التشغيل للمبرمجين
-
ملفات تعربف الارتباط هي بيانات نصية تُخزَّن مباشرةً في ذاكرة المتصفح، وهي في الواقع جزء من بروتوكول HTTP تُعرِّفها المواصفات RFC 6265، ويضبط الخادم عادةً هذه الملفات مستخدمًا ترويسة الاستجابة Set-Cookie، ثم يضيفها المتصفح إلى كل طلب -تقريبًا- يُرسل إلى النطاق ذاته مستخدمًا الترويسة Cookie. يُعَد الاستيثاق authentication أكثر الحالات شيوعًا لاستعمال ملفات تعريف الارتبابط، وذلك بسبب ما يلي: يستخدم الخادم الترويسة Set-Cookie في الاستجابة أثناء تسجيل الدخول لإعداد ملف تعريف ارتباط له مُعرِّف جلسة عمل session فريد. عند إرسال الطلب في المرة القادمة إلى نفس النطاق، سيُرسل المتصفح ملف تعريف الارتباط عبر الشبكة باستخدام الترويسة Cookie. وبالتالي سيعلم الخادم الجهة التي أرسلت الطلب. يمكن الوصول إلى ملفات تعريف الارتباط من المتصفح باستخدام الخاصية document.cookie، وستواجهك الكثير من النقاط المربكة عند استخدام ملفات تعريف الارتباط والخيارات المتعلقة بها، وسنغطي هذه النقاط بالتفصيل في هذا المقال. القراءة من الخاصية document.cookie لتعرف إذا ما كان متصفحك يُخزِّن أي ملفات تعريف ارتباط عائدة لموقع ما، استخدم الأمر: alert( document.cookie ); // cookie1=value1; cookie2=value2;... تتكون قيمة الخاصية document.cookie من أزواج name=value تفصل بينها فاصلة منقوطة ";"، حيث يمثل كل منها ملف تعريف ارتباط منفصل. لإيجاد ملف تعريف ارتباط معين يمكن فصل قيمة الخاصية document.cookie بالاستفادة من الفاصلة المنقوطة ";"، ثم إيجاد الاسم المطلوب، وننفذ ذلك باستخدام تعابير برمجية نظامية أو مصفوفة دوال، وسنترك ذلك تمرينًا للقارئ، كما ستجد في نهاية المقال دوالًا مساعدةً للتعامل مع ملفات تعريف الارتباط. الكتابة إلى الخاصية document.cookie يمكن أن نكتب قيمًا ضمن الخاصية document.cookie، وهي ليست خاصية بيانات، وإنما هي أشبه بدالة وصول accessor (ضابط setter/ جالب getter)، لذا لا بدّ من التعامل معها بخصوصية. يضبط الاستدعاء التالي مثلًا ملف تعريف ارتباط بالاسم user على القيمة John: document.cookie = "user=John"; alert(document.cookie); ستشاهد عند تنفيذ الشيفرة السابقة ملفات ارتباط عدةً غالبًا، لأن الأمر = document.cookie يغيِّر قيمة ملف الارتباط الذي يحمل الاسم user فقط. يمكن أن يتألف الاسم أو القيمة تقنيًا من أي محارف، لكن للإبقاء على تنسيق قابل للقراءة، ينبغي ترميز المحارف الخاصة باستخدام الدالة المدمجة encodeURIComponent: // ينبغي ترميز المحارف الخاصة let name = "my name"; let value = "John Smith" // encodes the cookie as my%20name=John%20Smith document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value); alert(document.cookie); // ...; my%20name=John%20Smith لملفات الارتباط خيارات عدة، وينبغي ضبط بعضها لأهميته، حيث توضع هذه الخيارات بعد الأزواج key=value، وتفصل بينها فواصل منقوطة ";": document.cookie = "user=John; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT" الخيار path path=/mypath: ينبغي أن يكون مسار العنوان مُطلقًا absolute، بحيث يمكن الوصول إلى ملفات الارتباط ضمن جميع الصفحات التي تقع تحت هذا المسار، أما القيمة الافتراضية فهي "المسار الحالي"، فلو كان المسار path=/admin، فستكون الملفات مرئيةً في الصفحة الحالية admin وفي أي صفحة تحتها admin/something/، لكن ليس في صفحات مثل home/ أو adminpage/، ويُضبط هذا الخيار عادةً على القيمة /=path لتتمكن كل صفحات الموقع من الوصول إلى ملفات الارتباط. الخيار domain domain=site.com: يحدد هذا الخيار المكان الذي يمكن الوصول منه إلى ملفات الارتباط، لكن لن نتمكن عمليًا من تحديد أي نطاقات نريد، إذ لا يمكن الوصول إلى ملفات الارتباط افتراضيًا سوى من النطاق الذي أعدّها، فلو أعد الموقع hsoub.com ملفات الارتباط، فلن يتمكن الموقع site.com من الوصول إليها، لكن الأمر المربك فعلًا، هو أننا لن نصل إلى ملفات الارتباط ضمن النطاقات الفرعية مثل academy.hsoub.com!. // hsoub.com في الموقع document.cookie = "user=John" // academy.hsoub.com في الموقع alert(document.cookie); // no user لا توجد طريقة تُمكِّن نطاقًا من المستوى الثاني من الوصول إلى ملفات الارتباط التي يُعدّها نطاق آخر، إذ تمنعنا أمور أمنية من تخزين بيانات حساسة لا يجب أن تظهر سوى في الموقع داخل ملفات الارتباط، لكن يمكن -إن أردنا- السماح للنطاق الفرعي academy.hsoub.com بالوصول إلى ملفات الارتباط، وذلك بضبط الخيار domain عند إعداد ملف ارتباط في النطاق hsoub.com، على قيمة النطاق الجذر domain=hsoub.com: // at hsoub.com // الوصول لملف الارتباط من النطاقات الفرعية // make the cookie accessible on any subdomain *.hsoub.com: document.cookie = "user=John; domain=hsoub.com" // لاحقًا // academy.hsoub.com في alert(document.cookie); // has cookie user=John يعمل الأمر domain=.hsoub.com -لاحظ النقطة قبل اسم النطاق- بالطريقة ذاتها لأسباب تاريخية، حيث سيسمح بالوصول إلى ملفات الارتباط من النطاقات الفرعية، ولا بدّ من استخدام هذا الأسلوب القديم إذا أردنا دعم المتصفحات القديمة. إذًا سيسمح لنا الخيار domain بجعل النطاقات الفرعية قادرةً على الوصول إلى ملفات الارتباط. الخياران expire و max-age إذا لم يتضمن ملف الارتباط أيًا من هذين الخيارين، فسيختفي الملف افتراضيًا مع إغلاق المتصفح، ويُدعى هذا النوع من ملفات الارتباط "ملفات ارتباط جلسة العمل"، ولنحافظ على ملف الارتباط بعد إغلاق المتصفح فلابدّ من ضبط أحد الخيارين expires أو max-age، وإليك مثالًا عن ذلك: expires=Tue, 19 Jan 2038 03:14:07 GMT يحدد تاريخ انتهاء الصلاحية الوقت الذي سيحذف فيه المتصفح ملف الارتباط، وينبغي أن يكون التاريخ بالتنسيق السابق تمامًا ووفق توقيت GMT، ويمكن أن نستخدم الدالة date.toUTCString للحصول على التاريخ بهذا التنسيق، حيث يمكن على سبيل المثال ضبط فترة انتهاء الصلاحية بعد يوم كامل: //بعد يوم من الآن let date = new Date(Date.now() + 86400e3); date = date.toUTCString(); document.cookie = "user=John; expires=" + date; سيُحذف الملف إذا ضبطنا قيمة الخيار expires على قيمة في الماضي. max-age=3600: يمثل الخيار max-age بديلًا عن expires، ويعبّر عن زمن انتهاء صلاحية ملف الارتباط بالثواني ابتداءً من اللحظة الحالية، وسيُحذف ملف الارتباط إذا ضُبط على القيمة صفر أو أي قيمة سالبة. // سيحذف الملف بعد ساعة document.cookie = "user=John; max-age=3600"; // سيحذف الملف مباشرة document.cookie = "user=John; max-age=0"; الخيار secure secure: يجب نقل ملفات الارتباط من خلال بروتوكول HTTPS فقط. لو أعددنا ملف ارتباط ضمن الموقع http://hsoub.com، فسيظهر افتراضيًا أيضًا ضمن الموقع https://hsoub.com، والعكس بالعكس، فملفات الارتباط متعلقة بالنطاقات ولا تميّز بين البروتوكولات، لكن يمكن تفادي ذلك باستخدام الخيار secure، إذ لن تظهر ملفات الارتباط التي يُعدها النطاق https://hsoub.com ضمن النطاق http://hsoub.com، لأن استخدام هذا الخيار مهم في منع نقل البيانات الحساسة عبر بروتوكول HTTP غير المشفر. // https:// بافتراض أننا حاليًا في // ضبط ملف الارتباط ليكون آمنًا document.cookie = "user=John; secure"; الخيار samesite وهو خيار من خيارات الأمان، وقد صُمم للحماية ضد الهجوم المعروف باسم "تزوير الطلبات ما بين الصفحات cross-site request forgery" أو اختصارًا XSFR، ولفهم الآلية التي يعمل بها هذا الخيار، ومتى يكون مفيدًا لنلق نظرةً على الهجوم XSFR. الهجوم XSFR لنفترض أنك سجلت دخولك إلى الموقع bank.com، وبالتالي ستحصل على ملف ارتباط استيثاق من هذا الموقع، وسيرسل متصفحك هذا الملف إلى الموقع مع كل طلب حتى يميّزك وينفذ لك عمليات ماليةً حساسةً مثلًا، ولنتخيل الآن أنك تتصفح الشابكة في نافذة أخرى ووصل بك الأمر إلى الموقع المشبوه evil.com، الذي يمتلك شيفرة JavaScript. ترسل إلى الموقع bank.com النموذج <form action="https://bank.com/pay"> الذي يحتوي حقولًا تهيئ لعملية تحويل مالي إلى حساب المخترق، وسيرسل المتصفح ملف الارتباط في كل مرة تزور فيها الموقع bank.com، حتى لو أُرسل النموذج من الموقع evil.com، وهكذا سيميُّزك البنك وينفذ عملية الدفع؛ هذا هو الهجوم XSFR. طبعًا تحمي البنوك الحقيقية نفسها من هذا الهجوم، فجميع النماذج التي يولّدها الموقع bank.com تحتوي على حقل مميز يدعى مفتاح الحماية المشفّر protection token من هجوم XSRF، والتي لا يمكن للصفحات المشبوهة توليدها أو استخلاصها من صفحة أخرى، وبالتالي ستستطيع هذه الصفحات إرسال النماذج، لكن لا يمكن أن تحصل على بيانات من المواقع المستهدفة، وسيتحقق الموقع bank.com من وجود مفتاح الحماية المشفر في كل نموذج يتلقاه. تستغرق طريقة الحماية هذه وقتًا لإنجازها، إذ يجب التحقق من وجود المفتاح في كل نموذج، كما ينبغي التحقق من كل طلب. استخدام الخيار samsite يؤمّن هذا الخيار طريقةً للحماية من الهجوم السابق، ولا يتطلب -نظريًا- استخدام مفتاح الحماية المشفر، ولهذا الخيار قيمتان، هما: samesite=strict: وهو مشابه لاستخدام الخيار بلا قيمة، حيث لن يُرسل ملف الارتباط -عند استخدام هذه القيمة- إذا لم يكن المستخدم ضمن النطاق نفسه. وبعبارة أخرى، لن تُرسل ملفات الارتباط سواءً تبع المستخدم رابطًا ضمن بريده الإلكتروني أم أرسل نموذجًا من موقع مشبوه أو نفّذ عمليةً خارج النطاق. عندما يكون لملف ارتباط الاستيثاق الخيار samesite، فلن تكون هناك فرصة لتحقيق هجوم XSRF، لأن النموذج الذي يرسله موقع مشبوه evil.com سيأتي دون ملفات ارتباط، وبالتالي لن يميز الموقع bank.com المستخدم ولن يتابع عملية الدفع، كما لن ترسل ملفات الارتباط فعليًا في هذه الحالة. يمكن الاعتماد على أسلوب الحماية هذا، لكن مع ذلك يمكن أن نقلق، فعندما يتتبع المستخدم رابطًا مشروعًا إلى الموقع bank.com -مثل أن يكون ضمن الملاحظات التي يوّلدها الموقع-؛ سيتفاجأ المستخدم أن الموقع لن يميّزه، ويمكن أن نتخطى هذا الأمر باستخدام نوعين من ملفات الارتباط، حيث يُستخدم أحدهما لتمييز المستخدم لأغراض عامة مثل أن تلقي التحية عليه "مرحبًا أحمد"، أما النوع الآخر فيُستخدم لعمليات تغيير البيانات وسيُزود بالخيار samesite=strict، بهذا سيرى المستخدم الذي يصل إلى الموقع bank.com من موقع آخر رسالة ترحيب، لكن العمليات المالية لن تُنفَّذ إلا من الموقع نفسه، بعد إرسال ملف الارتباط الثاني. samesite=lax: وهو خيار أقل تشددًا، يحمي من هجوم XSRF ولا يهدد تجربة المستخدم، حيث يمنع المتصفح من إرسال ملفات الارتباط التي تأتي من خارج الموقع، لكنه يضيف بعض الاستثناءات. تُرسل ملفات الارتباط ذات الخيار samesite=lax إذا تحقق الشرطان التاليان: نوع طلب HTTP آمن (GET مثلًا وليس POST)، وستجد قائمةً كاملةً بطلبات HTTP الآمنة في التوصيفات RFC7231 specification، لكن مبدئيًا، ستستخدم هذه الطلبات في قراءة البيانات وليس في كتابتها، فلا ينبغي لها تنفيذ عمليات لتغيير البيانات، ويُتبع الرابط دائمًا بطلب آمن من النوع GET. أن تُنفِّذ العملية انتقالًا إلى عنوان عالي المستوى (تُغيِّر العنوان في شريط عناوين المتصفح)، وهو أمر محقق عادةً، لكن إذا جرى الانتقال إلى عنوان من خلال نافذة ضمنية <iframe> فلن يكون انتقالًا عالي المستوى، ولا تنفذ طلبات JavaScript عبر الشبكة أي تنقلات كونها لن تتناسب مع هذه العملية. لذلك نرى أن كل ما يفعله الخيار samesite=lax هو السماح بامتلاك عمليات التنقل الأساسية ملفات ارتباط، مثل عمليات الانتقال من ملاحظة يولدها موقع إلى الموقع ذاته، فهي تحقق الشرطين السابقين، لكن ستفقد العمليات الأكثر تعقيدًا -مثل طلبات الشبكة بين موقعين مختلفين- ملفات الارتباط، لذا استخدم هذا الخيار إن كان الأمر مناسبًا لك، إذ لن يؤثر ذلك غالبًا على تجربة المستخدم وسيؤمن لك الحماية. وعلى الرغم من أن استخدام الخيار samesite أمر جيد، لكنك ستواجه العائق المهم الذي يتمحور حول تجاهل المتصفحات القديمة -2017 وما قبلها- لهذا الخيار، بحيث ستخترَق المتصفحات القديمة إذا اعتمدت على هذه الطريقة في الحماية فقط. لكن يمكننا استخدام samesite مع معايير حماية أخرى، مثل مفاتيح الحماية المشفرة من هجمات xsrf، وبذلك سنضيف طبقة حماية جديدة، وبالتالي سنتمكن غالبًا من تفادي هجمات xsrf حين يتوقف دعم المتصفحات القديمة مستقبلًا. الخيار httpOnly ليس لهذا الخيار علاقة بلغة JavaScript، لكن لا بدّ من ذكره حرصًا على اكتمال الصورة. يُستخدم خادم الويب الترويسة Set-Cookie لإعداد ملف الارتباط، كما يمكن للخادم استخدام الخيار httpOnly الذي يمنع أي شيفرة JavaScript من الوصول إلى ملف الارتباط، إذ لا يمكن عندها رؤية ملف الارتباط أو التعامل معه باستعمال الأمر document.cookie، ويستعمل هذا الخيار عاملًا احترازيًا لحماية المستخدم من هجمات معينة، يدفع فيها المخترق شيفرة JavaScript إلى صفحة ويب وينتظر الضحية ليزورها، وبالطبع لا ينبغي أن يتمكن المخترقون من دفع شيفراتهم إلى موقعنا، لكن قد نترك وراءنا ثغرات تمكنهم من ذلك. وإذا حدث شيء كهذا وزار المستخدم صفحة ويب تحتوي على شيفرة لمخترقين، فستُنفَّذ هذه الشيفرة وسيصل المخترق إلى document.cookie، وبالتالي إلى ملف الارتباط التي يحتوي على معلومات الاستيثاق. لكن إذا استُخدم الخيار httpOnly فلن تتمكن الخاصية document.cookie من رؤية ملف الارتباط، وسيبقى المستخدم محميًا. الدوال التي تتعامل مع ملفات الارتباط سنبين مجموعةً صغيرةً من الدوال التي تتعامل مع ملفات الارتباط، وهي ملاءمة أكثر من التعديلات اليدوية على الخاصية document.cookie، وستجد العديد من المكتبات التي ستساعدك في إنجاز ذلك، لكننا سنستعرض هذه الدوال على سبيل التجربة. الدالة (getCookie(name إنّ الطريقة الأقصر للوصول إلى ملف الارتباط ستكون باستخدام التعابير النظامية. حيث تعيد الدالة (getCookie(name ملف ارتباط باسم محدد name. // تعيد الدالة ملف ارتباط باسم محدد // أو تعيد كائنًا غير محدد إن لم يوجد الملف function getCookie(name) { let matches = document.cookie.match(new RegExp( "(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)" )); return matches ? decodeURIComponent(matches[1]) : undefined; } يوّلد الكائن new RegExp ديناميكيًا ليُطابق اسم ملف الارتباط <name=<value. لاحظ أن قيمة ملف الارتباط مرمّزة، لذلك تستخدم الدالة getCookie الدالة decodeURIComponent لفك الترميز. الدالة (setCookie(name, value, options تجعل هذه الدالة القيمة value اسمًا لملف الارتباط name، وتضبط خيار المسار افتراضيًا على القيمة =/path، ويمكن تعديلها لإضافة قيم افتراضية لخيارات أخرى: function setCookie(name, value, options = {}) { options = { path: '/', // إضافة قيم افتراضية أخرى ...options }; if (options.expires instanceof Date) { options.expires = options.expires.toUTCString(); } let updatedCookie = encodeURIComponent(name) + "=" + encodeURIComponent(value); for (let optionKey in options) { updatedCookie += "; " + optionKey; let optionValue = options[optionKey]; if (optionValue !== true) { updatedCookie += "=" + optionValue; } } document.cookie = updatedCookie; } // :مثال الاستخدام setCookie('user', 'John', {secure: true, 'max-age': 3600}); الدالة (deleteCookie(name تُستدعى هذه الدالة بقيم سالبة لخيار فترة الصلاحية max-age: function deleteCookie(name) { setCookie(name, "", { 'max-age': -1 }) } ملفات ارتباط الطرف الثالث Third-party cookie يُعَد ملف الارتباط من "طرف ثالث" إذا أعدّه نطاق مختلف عن الصفحة التي تزورها، فمثلًا: تُحمِّل صفحة من الموقع hsoub.com شريطًا دعائيًا من موقع آخر <img src="https://ads.com/banner.png">. يمكن أن يهيئ الخادم الذي يستضيف الموقع ads.com الترويسة Set-Cookie متضمنةً ملف الارتباط id=1234 مثلًا، وسيكون هذا الملف -الذي يعود أصلًا إلى النطاق ads.com- مرئيًا فقط لصفحات هذا النطاق. عندما ننتقل إلى ads.com في المرة القادمة، سيحصل الخادم على مُعرِّف id ملف الارتباط وسيميِّز المستخدم. سيظهر الأمر الأكثر أهميةً عند انتقال المستخدم من الموقع hsoub.com إلى موقع آخر يحوي شريطًا دعائيًا، إذ سيحصل النطاق ads.com على ملف الارتباط لأنه ينتمي إليه، وبالتالي سيميّز المستخدم وسيتعقبه عند التنقل بين المواقع. استُخدمت ملفات ارتباط الطرف الثالث تقليديًا لخدمات الدعاية وتعقب المستخدمين نظرًا لطبيعتها، إذ ترتبط هذه الملفات بالنطاقات التي أعدَّتها، حيث يمكن للنطاق ads.com أن يتعقب المستخدم ذاته عبر مواقع مختلفة إذا أمكن لهذه المواقع الوصول إلى هذا النطاق. تسمح المتصفحات بتعطيل عمل هذا النوع من ملفات الارتباط لأن المستخدم لا يحب أن يُتتبع عادةً، كما تتبع بعض المتصفحات الحديثة سياسات خاصةً بملفات الارتباط هذه، وهي: لا يسمح المتصفح Safari بملفات ارتباط الطرف الثالث. يأتي المتصفح Firefox بقائمة سوداء تضم مجموعة نطاقات تحجب ملفات الارتباط التي تعود لها. التشريع الأوروبي GDPR لا يتعلق الموضوع باللغة إطلاقًا، لكن انتبه إليه عندما تعمل على إعداد ملفات الارتباط، حيث يجبر تشريع أوروبي يُدعى "GDPR" صفحات الويب على احترام مجموعة من القواعد التي تضمن خصوصية المستخدم، وتتطلب إحدى هذه القواعد إذنًا صريحًا لتتبع ملفات الارتباط العائدة لمستخدم، وتذكر أنه يخص ملفات ارتباط التتبع أو التعريف أو الاستيثاق، فإذا أردنا إعداد ملف ارتباط لحفظ بعض المعلومات -لا لتتبع أو تحديد مستخدم- فلنا كامل الحرية، ولابدّ من إذن صريح من المستخدم في حال تتبعه أو التعرف عليه. تطبق مواقع الويب هذا التشريع بأسلوبين، ولا بدّ أنك لاحظتهما أثناء تصفحك للإنترنت: عندما تحاول صفحة الويب تتبع مستخدمين مستَوثَقين، فلا بدّ عندها أن يحتوي نموذج التسجيل على صندوق تحقق checkbox يحمل عنوانًا مثل "قبول سياسة الخصوصية"، والتي تصف كيفية استخدام ملفات الارتباط، ولا بدّ أن يختاره المستخدم حتى يتابع، عندها ستستخدم الصفحة ملفات ارتباط الاستيثاق بحرية. عندما تحاول صفحة الويب تتبع كل مستخدم فستعرض الصفحة -إن أرادت تنفيذ ذلك بصورة قانونية- شاشة بداية splash screen للقادمين الجدد، وتطلب منهم الموافقة على استخدام ملفات الارتباط. عندها ستتمكن الصفحة من إنشاء ملفات ارتباط، ثم ستسمح للمستخدم بمتابعة محتوى الصفحة. قد يكون الأمر مربكًا للمستخدم، فلا أحد يحب النقر الإجباري على شاشة بداية قبل متابعة المحتوى، لكن التشريع GDPR يتطلب موافقةً صريحةً. لا يتعلق التشريع GDPR بملفات الارتباط، بل بأمور أخرى تتعلق بالخصوصية أيضًا، لكنها خارج إطار موضوعنا. خلاصة توفِّر الخاصية document.cookie وصولًا إلى ملفات الارتباط cookies، مع ملاحظة ما يلي: لا تعدل عمليات الكتابة سوى ملفات الارتباط التي تذكرها الخاصية. لا بدّ من ترميز الزوج "اسم/قيمة" name/value. لا يتجاوز حجم ملف الارتباط عتبة 4 كيلوبايت، ولا يتجاوز عدد ملفات الارتباط 20 ملفًا في الموقع، وفقًا للمتصفح المستخدم. خيارات ملف الارتباط: /=path: قيمته الافتراضية هي المسار الحالي، ويجعل ملف الارتباط مرئيًا فقط تحت المسار المحدد. domain=site.com: يكون ملف الارتباط مرئيًا ضمن النطاق المحدد فقط افتراضيًا، لكن ستكون ملفات الارتباط مرئيةً ضمن النطاقات الفرعية أيضًا إذا صُرّح عن النطاق. expires أو max-age: تحدد زمن انتهاء صلاحية ملف الارتباط، وبدونها يُحذف ملف الارتباط عند إغلاق المتصفح. secure: لا بدّ من استخدام بروتوكول HTTPS حتى تُرى ملفات الارتباط. samesite: يمنع المتصفح من إرسال ملفات الارتباط مع الطلبات الواردة من خارج نطاق الموقع، وسيساعد ذلك في إيقاف هجمات XSRF. إضافةً إلى ذلك: يمكن أن يمنع المتصفح استخدام ملفات الارتباط القادمة من طرف ثالث افتراضيًا مثل المتصفح Safari. ينبغي التقيد بالتشريع GDPR عند إعداد ملفات ارتباط لتتبع مستخدمين من الاتحاد الأوروبي. ترجمة -وبتصرف- للفصل cookies, document.cookies من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: آليات الاتصال المستمر مع الخادم في جافاسكربت الجلسات وملفات تعريف الارتباط ومكتبة cURL في PHP الواجهة البرمجية Fetch API في جافاسكريبت احم موقعك من الاختراق
-
سنشرح في هذا المقال آليات الاتصال الدائم المستمر مع الخادم وهو الاتصال الذي يُنشَأ فيه اتصال مستمر بين الخادم والعميل (غالبًا متصفح الويب) يجري فيه تبادل الطلبيات والردود آنيًا، وتنحصر آلية تنفيذ الاتصال الدائم تلك في ثلاثة أنواع: آلية الاستطلاع المفتوح Long polling وآلية مقابس الويب عبر البروتوكول WebSocket وآلية الأحداث المرسلة EventSource، سنناقش كل واحدة بالتفصيل على حدة. آلية الاستطلاع آلية الجس والاستطلاع polling هي الطريقة الأبسط للمحافظة على اتصال ثابت مع الخادم، ولا تتطلب أي بروتوكولات خاصة مثل WebSocket، أو أي أحداث من طرف الخادم، كما أنها سهلة الإنجاز وكافية في الكثير من الحالات. الاستطلاع الدوري Regular polling الطريقة الأبسط للحصول على معلومات من الخادم هي الاستطلاع الدوري، بإرسال طلبات إلى الخادم لسؤاله عن أي معلومات جديدة على فترات زمنية منتظمة (مثلًا: كل 10 ثوان)، وعند الاستجابة ينتبه الخادم إلى اتصال العميل به، ثم يرسل حزمةً من الرسائل التي تلقاها حتى اللحظة، سيعمل الأمر كما هو متوقع، لكن مع بعض الجوانب السلبية، منها: تمرَّر الرسائل بتأخير زمني قد يصل إلى 10 ثوان (الزمن الفاصل بين الطلبات). سيُغرَق الخادم بالطلبات كل 10 ثوان حتى لو غادر المستخدم الموقع أو استغرق في النوم، وهذا حمل كبير عليه من منظور الأداء. لهذا قد تكون هذه المقاربة جيدةً عندما تكون الخدمة بسيطةً، لكنها وبشكل عام تحتاج إلى التحسين. الاستطلاع المفتوح Long polling وهي طريقة أفضل للاستطلاع أو الجس من حالة الخادم، وتتميز بسهولة تنفيذها، وقدرتها على تسليم الرسائل دون تأخير. حيث: يُرسَل الطلب إلى الخادم. لا يغلق الخادم الاتصال حتى تكون لديه رسالة لتسليمها. عندما تظهر الرسالة، يستجيب الخادم للطلب بهذه الرسالة. يرسل المتصفح طلبًا جديدًا مباشرة. إن أساس هذه الطريقة هو إرسال المتصفح للطلب وإبقاء الاتصال مع الخادم قيد الانتظار، ولا يهيّئ المتصفح الاتصال من جديد حتى يتسلم الاستجابة، فإن فُقد الاتصال نتيجة خطأ ما، فسيرسل المتصفح طلبًا آخر مباشرةً. تمثل الشيفرة التالية الدالة subscribe التي ترسل الطلبات المفتوحة من جهة العميل: async function subscribe() { let response = await fetch("/subscribe"); if (response.status == 502) { // 502 خطأ تجاوز الوقت المسموح للاتصال // يحدث عند بقاء الاتصال مفتوحًا لفترة طويلة جدًا // ويغلقه الخادم لذا يجب إعادة الاتصال await subscribe(); } else if (response.status != 200) { // إظهار الخطأ showMessage(response.statusText); // إعادة الاتصال خلال ثانية await new Promise(resolve => setTimeout(resolve, 1000)); await subscribe(); } else { // الحصول على الرسالة وإظهارها let message = await response.text(); showMessage(message); // استدعاء التابع من جديد await subscribe(); } } subscribe(); يرسل التابع subscribe طلب إحضار، وينتظر الاستجابة ليعالجها عندما تصل، ثم يستدعي نفسه مجددًا. مثال اختباري لنظام محادثة بسيط إليك تطبيق محادثة اختباريًا يمكنك تنزيله وتشغيله محليًا إذا كنت على دراية باستخدام Node.JS وتثبيت الوحدات البرمجية اللازمة: ملف شيفرة العميل "browser.js": // POSTإرسال رسالة، طلب function PublishForm(form, url) { function sendMessage(message) { fetch(url, { method: 'POST', body: message }); } form.onsubmit = function() { let message = form.message.value; if (message) { form.message.value = ''; sendMessage(message); } return false; }; } // تلقي الرسائل باستخدام الاستطلاع المفتوح function SubscribePane(elem, url) { function showMessage(message) { let messageElem = document.createElement('div'); messageElem.append(message); elem.append(messageElem); } async function subscribe() { let response = await fetch(url); if (response.status == 502) { // تجاوز الوقت المسموح // يحدث عندما يبقى الاتصال مفتوحًا لفترة طويلة // لا بد من إعادة الاتصال await subscribe(); } else if (response.status != 200) { // Show Error showMessage(response.statusText); // إعادة الاتصال خلال ثانية await new Promise(resolve => setTimeout(resolve, 1000)); await subscribe(); } else { // الحصول على الرسالة let message = await response.text(); showMessage(message); await subscribe(); } } subscribe(); } ملف شيفرة الواجهة الخلفية "server.js": let http = require('http'); let url = require('url'); let querystring = require('querystring'); let static = require('node-static'); let fileServer = new static.Server('.'); let subscribers = Object.create(null); function onSubscribe(req, res) { let id = Math.random(); res.setHeader('Content-Type', 'text/plain;charset=utf-8'); res.setHeader("Cache-Control", "no-cache, must-revalidate"); subscribers[id] = res; req.on('close', function() { delete subscribers[id]; }); } function publish(message) { for (let id in subscribers) { let res = subscribers[id]; res.end(message); } subscribers = Object.create(null); } function accept(req, res) { let urlParsed = url.parse(req.url, true); // مستخدم جديد يريد النتائج if (urlParsed.pathname == '/subscribe') { onSubscribe(req, res); return; } // إرسال رسالة if (urlParsed.pathname == '/publish' && req.method == 'POST') { // POSTقبول req.setEncoding('utf8'); let message = ''; req.on('data', function(chunk) { message += chunk; }).on('end', function() { publish(message); // النشر للجميع res.end("ok"); }); return; } // البقية ثابتة fileServer.serve(req, res); } function close() { for (let id in subscribers) { let res = subscribers[id]; res.end(); } } // ----------------------------------- if (!module.parent) { http.createServer(accept).listen(8080); console.log('Server running on port 8080'); } else { exports.accept = accept; if (process.send) { process.on('message', (msg) => { if (msg === 'shutdown') { close(); } }); } process.on('SIGINT', close); } الملف الأساسي "index.html": <!DOCTYPE html> <script src="browser.js"></script> All visitors of this page will see messages of each other. <form name="publish"> <input type="text" name="message" /> <input type="submit" value="Send" /> </form> <div id="subscribe"> </div> <script> new PublishForm(document.forms.publish, 'publish'); // random url parameter to avoid any caching issues new SubscribePane(document.getElementById('subscribe'), 'subscribe?random=' + Math.random()); </script> وستظهر النتيجة بالشكل التالي: مجال الاستخدام تعمل طريقة الاستطلاع المفتوح بصورة ممتازة عندما تكون الرسائل نادرةً، لكن عندما تتكرر الرسائل بكثرة، فسيكون منحني (طلب-استجابة) المرسوم في الأعلى على هيئة أسنان المنشار، ولأن لكل رسالة طلبًا مستقلًا له ترويسات وآلية استيثاق خاصة، فمن الأفضل استخدام طرق أخرى مثل: Websocket أوServer Sent Events، التي سنشرحها في الفقرات التالية. استخدام البروتوكول WebSocket يزوّدنا هذا البروتوكول الموصوف في المعيار RFC 6455 بطريقة لتبادل البيانات بين الخادم والمتصفح عبر اتصال ثابت، حيث تمرَّر البيانات إلى كلا الطرفين على شكل حزم دون قطع الاتصال أو استخدام طلبات HTTP إضافية، وتظهر الفائدة الكبيرة لاستخدامه عند وجود خدمات تتطلب التبادل المستمر للبيانات، مثل ألعاب الشبكة وأنظمة التجارة العاملة في الزمن الحقيقي وغيرها. مثال بسيط سنحتاج إلى كائن مقبس اتصال جديد new WebSocket لفتح اتصال websocket باستخدام بروتوكول نقل خاص هو ws في العنوان: let socket = new WebSocket("ws://javascript.info"); كما يمكن استخدام البروتوكول المشفّر //:wss، فهو مشابه للبروتوكول HTTPS لكن مع websockets. ينبغي الاستماع إلى الأحداث التي تطرأ على مقبس الاتصال Socket بعد تأسيسه مباشرةً، وهي أربعة أحداث: open: عند تأسيس الاتصال. message: عند استقبال البيانات. error: عند وقوع خطأ يتعلق بالبروتوكول websocket. close: عند إغلاق الاتصال. ولإرسال أي بيانات يكفي استخدام الصيغة (socket.send(data، وإليك مثالًا: let socket = new WebSocket("wss://javascript.info/article/websocket/demo/hello"); socket.onopen = function(e) { alert("[open] Connection established"); alert("Sending to server"); socket.send("My name is John"); }; socket.onmessage = function(event) { alert(`[message] Data received from server: ${event.data}`); }; socket.onclose = function(event) { if (event.wasClean) { alert(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`); } else { // كأن تلغى العملية أو فشل في الشبكة // 1006 قيمته في هذه الحالة عادة event.code alert('[close] Connection died'); } }; socket.onerror = function(error) { alert(`[error] ${error.message}`); }; لقد كتبنا شيفرة خادم تجريبي باستخدام Node.JS في الملف server.js لتشغيل الشيفرة في المثال السابق، وسيرد الخادم بالرسالة "Hello from server, John"، ثم ينتظر 5 ثوان ويقطع الاتصال. سترى إذًا تسلسل الأحداث open → message → close، والأمر بهذه البساطة فعليًا، لأنك تستخدم الآن البروتوكول WebSocket، لنتحدث الآن في أمور أكثر عمقًا. فتح اتصال مقبس ويب WebSocket يُنشئ الأمر (new WebSocket(url كائنًا جديدًا ويبدأ عملية الاتصال مباشرةً، سيسأل المتصفح الخادم خلال تأسيس الاتصال -عبر الترويسات- إذا كان يدعم البروتوكول Websocket، فإن كان الجواب نعم فسيستمر العمل به، وهو يختلف عن HTTP كليًا. وإليك مثالًا عن ترويسات المتصفح للطلب الذي يرسله الأمر (new WebSocket("wss://javascript.info/chat. GET /chat Host: javascript.info Origin: https://javascript.info Connection: Upgrade Upgrade: websocket Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q== Sec-WebSocket-Version: 13 Origin: أصل الصفحة العميلة (https://javascript.info مثلًا)، فالكائنات WebSocket هي كائنات من أصول مختلطة بطبيعتها، حيث لا توجد أي قيود لاستخدامها، وطالما أنّ الخوادم القديمة لا تدعم هذا البروتوكول، فلن نواجه مشكلة توافق. تظهر أهمية الترويسة Origin في أنها تسمح للخادم بالاختيار إن كان سيتواصل مع الصفحة العميلة وفق هذا البروتوكول أم لا. Connection: Upgrade: يشير إلى رغبة العميل في تغيير البروتوكول. Upgrade: websocket: تدل أنّ البروتوكول المطلوب هو websocket. Sec-WebSocket-Key: مفتاح أمان عشوائي يولده المتصفح. Sec-WebSocket-Version: نسخة بروتوكول websocket، وهي حاليًا النسخة 13. ينبغي على الخادم أن يعيد الرمز 101 عندما يوافق على التحوّل إلى البروتوكول WebSocket: 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g= ستكون الترويسة Sec-WebSocket-Accept هي نفسها Sec-WebSocket-Key مسجلةً باستخدام خوارزمية خاصة، وسيستخدمها المتصفح للتأكد من توافق الاستجابة مع الطلب. بعد ذلك ستُنقل البيانات باستخدام بروتوكول WebSocket، وسنطلع على بنيتها (إطاراتها) قريبًا. الموسعات والبروتوكولات الفرعية هنالك أيضًا الترويستان Sec-WebSocket-Extensions التي تصف الموسّعات، وSec-WebSocket-Protocol التي تصف البروتوكولات الفرعية. فمثلًا: Sec-WebSocket-Extensions: deflate-frame: يعني أن المتصفح سيدعم ضغط البيانات، فالموسّع هو شيء يتعلق بنقل البيانات ووظيفة توسّع عمل البروتوكول، يرسل المتصفح الترويسة Sec-WebSocket-Extensions تلقائيًا مزودةً بقائمة الموسّعات المدعومة كلها. Sec-WebSocket-Protocol: soap, wamp: يعني أننا سننقل البيانات باستخدام أحد البروتوكولين الفرعيين SOAP أو WAMP (وهو اختصار للعبارة " WebSocket Application Messaging Protocol" أي "بروتوكول نقل الرسائل لتطبيق websocket"). تُسجل البروتوكولات الفرعية في IANA catalogue، أي أن هذه الترويسة الاختيارية تحدد صيغ البيانات التي سنستخدمها، وتُضبَط باستخدام المعامل الثاني للدالة البانية new WebSocket، فلو أردنا استخدام SOAP أو WAMP مثلًا، فسنضعهما في مصفوفة بالشكل التالي: let socket = new WebSocket("wss://javascript.info/chat", ["soap", "wamp"]); سيستجيب الخادم بقائمة من البروتوكولات والموسّعات التي يوافق على استخدامها، وإليك مثالًا: GET /chat Host: javascript.info Upgrade: websocket Connection: Upgrade Origin: https://javascript.info Sec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q== Sec-WebSocket-Version: 13 Sec-WebSocket-Extensions: deflate-frame Sec-WebSocket-Protocol: soap, wamp وستكون الاستجابة: 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g= Sec-WebSocket-Extensions: deflate-frame Sec-WebSocket-Protocol: soap أي أن الخادم سيدعم الموسّع "deflate-frame" والبروتوكول الفرعي SOAP فقط. نقل البيانات يتكون الاتصال من إطارات "frames"، وهي أجزاء من البيانات يمكن إرسالها من كلا الطرفين وقد تكون من الأنواع التالية: "text": يحتوي بيانات نصيةً ترسلها الأطراف إلى بعضها. "binary data": يحتوي بيانات ثنائيةً تُرسلها الأطراف إلى بعضها. "ping/pong": تستخدَم للتحقق من الاتصال، ويرسلها الخادم إلى المتصفح الذي يرد عليها تلقائيًا. ستجد أيضًا "connection close" وبعض الإطارات الأخرى. ونعمل في بيئة المتصفح مع الإطارات النصية والثنائية. يمكن للتابع ()send. إرسال بيانات نصية أو ثنائية.، ويسمح استدعاء التابع (socket.send(body بأن تكون قيمة جسم الطلبbody بالتنسيق النصي أو الثنائي، بما في ذلك الكائن Blob وArrayBuffer وغيرهما، ولا حاجة لإجراء ضبط خاص بل تُرسَل البيانات كما هي. تأتي البيانات النصية على شكل "string" عندما نستقبل البيانات، ومع ذلك يمكننا الاختيار بين BlobوArrayBuffer للبيانات الثنائية.** وكذا ضبط ذلك باستخدام الخاصية socket.binaryType، التي تأخذ القيمة "blob" افتراضيًا، لذلك تأتي البيانات الثنائية ضمن الكائن Blob، الذي يمثل كائن بيانات ثنائية عالي المستوى ويتكامل مباشرةً مع المعرّفين <a>و<img> وغيرهما، ويمكن الانتقال إلى استخدام الكائن ArrayBuffer عند معالجة البيانات الثنائية والعمل مع البايتات بشكل منفصل. socket.binaryType = "arraybuffer"; socket.onmessage = (event) => { //أما أن يكون نصًا أو بيانات ثنائيةً event.data }; محدودية معدل النقل Rate limiting لنتخيل أن تطبيقنا قادر على توليد كمية كبيرة من البيانات، لكن المستخدم يعاني من اتصال بطيء بالانترنت. يمكن استدعاء التابع (socket.send(data مرات عدةً، لكن البيانات ستُخزّن مؤقتًا في الذاكرة وستُرسَل بالسرعة التي تسمح بها الشبكة فقط، عندها يمكن استخدام الخاصية socket.bufferedAmount التي تُخزّن حجم البيانات الموجودة في المخزن المؤقت في هذه اللحظة وتنتظر الظروف المناسبة لنقلها، ويمكن فحص الحجم المتبقي من البيانات للتأكد من أن المقبس Socket لا يزال متاحًا لمتابعة نقل البيانات. // افحص المقبس كل 100 ثانية وحاول إرسال البيانات // عندما يكون حجم البيانات المخزّنة التي لم تصل صفرًا setInterval(() => { if (socket.bufferedAmount == 0) { socket.send(moreData()); } }, 100); إغلاق الاتصال عندما يحاول أحد الأطراف إغلاق الاتصال (للمخدم والمتصفح الحق نفسه في ذلك)، يرسَل عادةً إطار إغلاق الاتصال "connection close" الذي يحتوي قيمةً عدديةً وسببًا نصيًا للإغلاق. إليك الصيغة اللازمة: socket.close([code], [reason]); code: رمز خاص لإغلاق اتصال WebSocket، وهو اختياري. reason: نص يصف سبب الإغلاق، وهو اختياري. يستقبل معالج الحدث close في الطرف الآخر الرمز والسبب: // الطرف الذي سيغلق الاتصال: socket.close(1000, "Work complete"); // الطرف الآخر socket.onclose = event => { // event.code === 1000 // event.reason === "Work complete" // event.wasClean === true (clean close) }; إليك القيم الأكثر شيوعًا للرموز: 1000: وهي القيمة الافتراضية، وتستخدم للإغلاق العادي (تستخدم إن لم يكن هناك رمز code). 1006: لا يمكن ضبط هذا الرمز يدويًا ويشير إلى فقد الاتصال. 1001: عند مغادرة أحد الأطراف، مثل: إغلاق الخادم أو مغادرة المتصفح للصفحة. 1009: الرسائل أضخم من أن تُعالج. 1011: خطأ في الخادم. يمكن الاطلاع على القائمة الكاملة ضمن المعيار RFC6455, §7.4.1. إنّ رموز WebSocket مشابهة نوعًا ما لرموز HTTP وتختلف في بعض النواحي، فأي رمز أقل من 1000 سيكون محجوزًا، وسيتولد خطأ عند استخدامه. // في حال فشل الاتصال socket.onclose = event => { // event.code === 1006 // event.reason === "" // event.wasClean === false (no closing frame) }; حالة الاتصال تتيح الخاصية socket.readyState إمكانية معرفة الوضع الحالي للاتصال وفقًا للقيم التي تأخذها: 0 متصل "CONNECTING": أي أن الاتصال يجري، ولم يُؤسَّس بعد. 1 مفتوح "OPEN": التواصل قائم. 2 يُغلق "CLOSING": يجري إغلاق الاتصال. 3 مُغلق "CLOSED": الاتصال مغلق. مثال تطبيق محادثة آنية لنراجع مثال تطبيق المحادثة باستخدام الواجهة البرمجية WebSocket API والوحدة البرمجية Node.js WebSocket module، سنركز على طرف العميل، وسيكون الخادم بسيطًا أيضًا. نحتاج إلى نموذج <form> لإرسال الرسائل ومعرِّف <div> لاستقبالها. <!-- message form --> <form name="publish"> <input type="text" name="message"> <input type="submit" value="Send"> </form> <!-- div with messages --> <div id="messages"></div> كما سنستخدم في جافاسكربت: فتح الاتصال. إرسال رسالة عن طريق النموذج (socket.send(message. وضع الرسالة المستقبلة ضمن div#messages. وإليك الشيفرة اللازمة: let socket = new WebSocket("wss://javascript.info/article/websocket/chat/ws"); // إرسال رسالة إلى النموذج document.forms.publish.onsubmit = function() { let outgoingMessage = this.message.value; socket.send(outgoingMessage); return false; }; // div#messagesإظهار الرسالة بعد وصولها في socket.onmessage = function(event) { let message = event.data; let messageElem = document.createElement('div'); messageElem.textContent = message; document.getElementById('messages').prepend(messageElem); } لن نتعمق بشيفرة الواجهة الخلفية، فهي خارج نطاق مقالتنا، لكننا سنستخدم هنا Node.js، ومع ذلك فالأمر ليس إلزاميًا، إذ أن للعديد من المنصات طرقها للتعامل مع WebSocket. تمثل الخطوات التالية خوارزمية عمل الواجهة الخلفية: إنشاء مجموعة من مقابس الاتصال ()clients = new Set. إضافة كل مقبس مقبول إلى المجموعة clients.add(socket)، وتهيئة مستمع message للحدث للحصول على الرسائل المتعلقة بالمقبس. عندما استقبال رسالة كرر عملية إرسالها إلى كل عميل. احذف مقابس العملاء عند قطع الاتصال (clients.delete(socket. const ws = new require('ws'); const wss = new ws.Server({noServer: true}); const clients = new Set(); http.createServer((req, res) => { // websocket سنعالج فقط اتصالات // websocket في مشروعنا الحقيقي ستوجد هنا شيفرة تعالج الطلبات بطرق أخرى غير wss.handleUpgrade(req, req.socket, Buffer.alloc(0), onSocketConnect); }); function onSocketConnect(ws) { clients.add(ws); ws.on('message', function(message) { message = message.slice(0, 50); // 50سيكون الحجم الأكبر للرسالة هو for(let client of clients) { client.send(message); } }); ws.on('close', function() { clients.delete(ws); }); } إليك النتيجة: يمكن تنزيل التطبيق وتجربته محليًا على جهازك، ولا تنس تثبيت Node.js، ثم تثبيت WebSocket باستخدام الأمر npm install ws قبل تشغيل التطبيق. الأحداث المرسلة من قبل الخادم تشرح التوصيفات Server-Sent Events صنفًا مدمجًا هو EventSource، حيث يحافظ على الاتصال مع الخادم ويسمح باستقبال أحداث منه، وهو اتصال ثابت مثل WebSocket تمامًا لكن مع بعض الاختلافات بينهما: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } WebSocket EventSource ثنائي الاتجاه: إذ يمكن أن يتبادل الخادم والعميل الرسائل وحيد الاتجاه: فالخادم هو من يرسل الرسائل إلى العميل نصوص وبيانات ثنائية نصوص فقط بروتوكول websocket بروتوكول HTTP النظامي. يمثل EventSource طريقةً أقل قدرةً للاتصال مع الخادم بالموازنة مع WebSocket، إذًا لماذا نستخدمها؟ يعود السبب الرئيسي لاستخدامه في كونه أبسط، إذ لا نحتاج إلى القدرة الكبيرة لبروتوكول WebSocket في بعض التطبيقات، فتلقي بيانات من الخادم مثل رسائل محادثة أو لوائح أسعار أو أي شيء مشابه هو مجال EventSource، كما أنه يدعم إعادة الاتصال، وهو أمر لا بد من إنجازه يدويًا في WebSocket، ويستخدم كذلك بروتوكول HTTP وليس أي بروتوكول جديد. الحصول على الرسائل لا بد من إنشاء كائن جديد من الصنف EventSource للبدء باستقبال الرسائل، وذلك بتنفيذ الأمر (new EventSource(url، حيث سيتصل المتصفح بالعنوان url ويبقي الاتصال مفتوحًا ومنتظرًا الأحداث، وهنا ينبغي أن يستجيب الخادم برمز الحالة 200 وبالترويسة Content-Type: text/event-stream، وسيبقي الخادم بعدها قناة الاتصال مفتوحةً، وسيرسل الرسائل ضمنها بتنسيق خاص بالشكل التالي: data: Message 1 data: Message 2 data: Message 3 data: of two lines تأتي الرسالة بعد الكلمة :data بينما يعَد الفراغ الموجود بعد النقطتين اختياريًا. يفصَل بين الرسائل برمز السطر الجديد مضاعفًا n\n\ يمكننا عند الحاجة إلى إرسال رمز سطر جديد n\ أن نرسل مباشرةً الكلمة :data (كما في الرسالة الثالثة في المثال السابق)، لكن عمليًاP تًرسل الرسائل المركّبة مرمزةً بتنسيق JSON، وترمّز محارف الانتقال إلى سطر جديد بالشكل n\ ضمنها، فلا حاجة عندها للرسائل :data متعددة السطر. فمثلًا: data: {"user":"John","message":"First line\n Second line"} وهكذا يمكن أن نعد أن العبارة data: ستليها رسالة واحدة تمامًا. يولَّد الحدث message لكل من تلك الرسائل بالشكل التالي: let eventSource = new EventSource("/events/subscribe"); eventSource.onmessage = function(event) { console.log("New message", event.data); // سيطبع ثلاث رسائل في المثال السابق }; // or eventSource.addEventListener('message', ...) الطلبات ذات الأصل المختلط يدعم الصنف EventSource الطلبات ذات الأصل المختلط cross-origin requests كما يفعل fetch، إذ يمكننا استخدام أي عنوان URL: let source = new EventSource("https://another-site.com/events"); سيستقبل الخادم البعيد الترويسة Origin وينبغي عليه الإجابة بالترويسة Access-Control-Allow-Origin للمتابعة، ولتمرير الثبوتيات credentials لا بد من ضبط خيار إضافي هو withCredentials بالشكل التالي: let source = new EventSource("https://another-site.com/events", { withCredentials: true }); راجع فصل استخدام Fetch مع الطلبات ذات الأصول المختلطة للاطلاع على تفاصيل أكثر حول ترويسات الأصول المختلطة. إعادة الاتصال بعد إنشاء الكائن EventSource فسيتصل بالخادم مباشرةً ويعيد الاتصال في حال انقطاعه، وبالتالي لا حاجة لإجراء تدابير خاصة بهذا الأمر، وبطبيعة الحال يوجد تأخير زمني صغير لثوان معدودة افتراضيًا بين كل محاولتين لإعادة الاتصال، وهنا يمكن للخادم أن يضبط التأخير اللازم باستخدام الخاصية :retry في الاستجابة (مقدرًا بالميلي ثانية). retry: 15000 data: Hello, I set the reconnection delay to 15 seconds يمكن أن تأتي :retry مصحوبةً ببيانات أخرى أو برسالة وحيدة، وهكذا سيكون على الخادم انتظار الفترة المحددة قبل إعادة الاتصال، وقد ينتظر فترة أطول، فإذا علم المتصفح -عن طريق نظام التشغيل- أن الشبكة غير متصلة حاليًا مثلًا، فسينتظر حتى يعود الاتصال ثم يحاول ثانيةً. إذا أراد الخادم من المتصفح التوقف عن إعادة الاتصال، فعليه أن يرد برمز الحالة 204 (رمز HTTP). إذا أراد المتصفح إغلاق الاتصال، فعليه استدعاء التابع eventSource.close. let eventSource = new EventSource(...); eventSource.close(); إذا احتوت الاستجابة على ترويسة Content-Type خاطئةً فلن يعاد الاتصال، وكذلك إذا اختلف رمز حالة HTTP عن القيم 301 أو 307 أو 200 أو 204، فعندها سيُولد الحدث "error" ولن يعيد المتصفح الاتصال. معرف الرسالة الفريد Message id عندما ينقطع الاتصال جراء خلل في الشبكة، tلن يتأكد كلا الطرفين من كون الرسائل استلمَت أم لا، وحتى يُستأنَف الاتصال بشكل صحيح، فينبغي أن تحمل كل رسالة مُعرِّفًا مميزًا id مثل التالي: data: Message 1 id: 1 data: Message 2 id: 2 data: Message 3 data: of two lines id: 3 عندما يتلقى المتصفح الرسالة مع معرّفها :id فإنه: يضبط قيمة الخاصية eventSource.lastEventId على قيمة المعرِّف. يعيد الترويسة Last-Event-ID أثناء إعادة الاتصال مع قيمة المعرّف id، ليتمكن الخادم من إرسال الرسائل التالية إن وجدت. حالة الاتصال: الخاصية ReadyState يمتلك الكائن EventSource الخاصية readyState التي تحمل إحدى القيم الثلاث التالية: EventSource.CONNECTING = 0; // يتصل أو يعيد الاتصال EventSource.OPEN = 1; // متصل EventSource.CLOSED = 2; // الاتصال مغلق تكون قيمة هذه الخاصية عند إنشاء كائن جديد أو انقطاع الاتصال هي EventSource.CONNECTING دومًا (وهي تعادل 0)، ويمكن الاستعلام عن هذه الخاصية لمعرفة حالة الكائن EventSource. أنواع الأحداث يوّلِّد الكائن EventSource افتراضيًا ثلاثة أحداث: message: عند استلام رسالة، وتكون متاحةً باستخدام event.data. open: عندما يكون الاتصال مفتوحًا. error: عند عدم إمكانية تأسيس الاتصال، كأن يعيد الخادم رمز الحالة 500. فمثلًا: event: join data: Bob data: Hello event: leave data: Bob لمعالجة الأحداث السابقة لابد من استخدام مستمع الحدث addEventListener وليس onmessage: eventSource.addEventListener('join', event => { alert(`Joined ${event.data}`); }); eventSource.addEventListener('message', event => { alert(`Said: ${event.data}`); }); eventSource.addEventListener('leave', event => { alert(`Left ${event.data}`); }); مثال نموذجي كامل يرسل الخادم في هذا المثال الرسائل 1 و2 و3 ثم bye ويقطع الاتصال، بعدها يعيد المتصفح الاتصال تلقائيًا. شيفرة الخادم "server.js": let http = require('http'); let url = require('url'); let querystring = require('querystring'); let static = require('node-static'); let fileServer = new static.Server('.'); function onDigits(req, res) { res.writeHead(200, { 'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-cache' }); let i = 0; let timer = setInterval(write, 1000); write(); function write() { i++; if (i == 4) { res.write('event: bye\ndata: bye-bye\n\n'); clearInterval(timer); res.end(); return; } res.write('data: ' + i + '\n\n'); } } function accept(req, res) { if (req.url == '/digits') { onDigits(req, res); return; } fileServer.serve(req, res); } if (!module.parent) { http.createServer(accept).listen(8080); } else { exports.accept = accept; } شيفرة الملف "index.html": <!DOCTYPE html> <script> let eventSource; function start() { // "Start" عند ضغط الزر if (!window.EventSource) { // متصفح إنترنت إكسبلورر أو أي متصفح قديم alert("The browser doesn't support EventSource."); return; } eventSource = new EventSource('digits'); eventSource.onopen = function(e) { log("Event: open"); }; eventSource.onerror = function(e) { log("Event: error"); if (this.readyState == EventSource.CONNECTING) { log(`Reconnecting (readyState=${this.readyState})...`); } else { log("Error has occurred."); } }; eventSource.addEventListener('bye', function(e) { log("Event: bye, data: " + e.data); }); eventSource.onmessage = function(e) { log("Event: message, data: " + e.data); }; } function stop() { // when "Stop" button pressed eventSource.close(); log("eventSource.close()"); } function log(msg) { logElem.innerHTML += msg + "<br>"; document.documentElement.scrollTop = 99999999; } </script> <button onclick="start()">Start</button> Press the "Start" to begin. <div id="logElem" style="margin: 6px 0"></div> <button onclick="stop()">Stop</button> "Stop" to finish. وستظهر النتيجة بالشكل التالي: خلاصة تعرفنا في هذا الفصل على آليات إنشاء إتصال مستمر مع الخادم، فبدأ أولًا بعرض آلية الاستطلاع والجس وهي الطريقة الأبسط للمحافظة على اتصال ثابت مع الخادم، ولا تتطلب أي بروتوكولات أو أي أحداث خاصة من طرف الخادم، وهي طريقة سهلة الإنجاز وكافية في الكثير من الحالات. انتقلنا بعدها إلى شرح بروتوكول WebSocket والذي يُقدم طريقةً عصريةً لتأسيس اتصال ثابت بين المتصفح والخادم، له ميزات منها: لا توجد تقييدات للأصول المختلطة. مدعوم جيدًا من قبل المتصفحات. يمكنه إرسال نصوص وبيانات ثنائية واستقبالها. واجهته البرمجية بسيطة. التوابع المستعملة في هذا البروتوكول: socket.send(data) socket.close([ code ], [reason]) أما الأحداث: open message error close لا تقدّم WebSocket بذاتها تقنيات لإعادة الاتصال أو الاستيثاق وغيرها من الآليات عالية المستوى، لذلك ستجد مكتبات للعميل والخادم تهتم بهذه النقاط، ويمكنك أيضًا تنفيذ شيفرتها يدويًا بنفسك. ولكي تتكامل WebSocket مع مشروع جاهز، جرت العادة أن يشغل المستخدم خادم WebSocket بالتوازي مع خادم HTTP الرئيسي ليتشاركا بقاعدة بيانات واحدة، حيث تُرسل طلبات WebSocket إلى النطاق الفرعي wss://ws.site.com الذي يقود إلى خادم WebSocket، بينما يقود العنوان https://site.com إلى خادم HTTP الرئيسي، كما توجد طرق أخرى للتكامل. عرضنا أخيرًا الآلية الثالثة للاتصال وهي عبر الكائن EventSource الذي يؤسس اتصالًا ثابتًا مع الخادم ويسمح له بإرسال الرسائل عبر قناة الاتصال، ويقدم: إعادة الاتصال التلقائي مع قيمة زمنية retry لإعادة الاتصال بعدها. معرّفات فريدة للرسائل "ids" لاستئناف الأحداث من آخر معرّف أرسِل مع الترويسة Last-Event-ID أثناء إعادة الاتصال. حالة الاتصال من خلال الخاصية readyState. تجعل هذه المزايا الكائن EventSource بديًلا قيمًا للكائن WebSocket، الذي يعمل في مستوىً أدنى ويفتقر إلى الميزات السابقة، والتي يمكن إنجازها يدويًا بالطبع. يعَد الكائن EventSource في الكثير من التطبيقات الحقيقية قادرًا كفاية على إنجاز المطلوب، وتدعمه جميع المتصفحات الحديثة، بينما لا يدعمه IE، وإليك صيغة استخدامه: let source = new EventSource(url, [credentials]); للوسيط الثاني خيار وحيد ممكن هو: { withCredentials: true }، ويسمح بإرسال ثبوتيات طلبات الأصول المختلطة. إن عوامل الأمان المتعلقة باستخدام طلبات الأصول المختلطة للكائن EventSourceمشابهة للاتصال fetch وغيره من طرق الاتصال عبر الشبكات. خاصيات الكائن EventSource: readyState: حالة الاتصال القائم EventSource.CONNECTING (=0) EventSource.OPEN (=1) EventSource.CLOSED (=2) lastEventId: آخر معرّف استقبله المتصفح، وسيرسله أثناء إعادة الاتصال مع الترويسة Last-Event-ID. وتوابع EventSource فتتمثل بالتابع: ()close: يغلق الاتصال. أما أحداث EventSource فهي: message: عند استلام رسالة، وتكون متاحةً باستخدام event.data. open: عندما يكون الاتصال مفتوحًا. error: في حال وقع خطأ، بما في ذلك فقدان الاتصال أو الأخطاء القاتلة، يمكننا التحقق من الخاصية readyState للتأكد من وجود محاولات لإعادة الاتصال. يمكن للخادم اختيار اسم مخصص لحدث ما من خلال :event، وينبغي معالجة هذه الأحداث من خلال مستمع حدث addEventListener وليس باستخدام <on<event. رأينا من أجل تنسيق استجابة الخادم أن الخادم يرسل الرسائل مفصولةً عن بعضها باستخدامn\n\، ويمكن أن تحتوي الرسالة الحقول التالية: :data ويمثل جسم الرسالة، وتُفسَّر سلسلة من الحقول data كرسالة واحدة يفصل بين أجزائها الكتلة n\. :id تُجدِّد قيمة lastEventId وتُرسَل ضمن الترويسة Last-Event-ID عند إعادة الاتصال. :retry تحدد فترة التأخير الزمني بين كل محاولتين لإعادة الاتصال بالميلي ثانية، ولا يمكن ضبطها باستخدام JavaScript. :event اسم الحدث وينبغي أن تسبق الحقل data. يمكن أن تحتوي الرسالة على أحد الحقول السابقة أو أكثر وبأي ترتيب، لكن الحقل :id يأتي في النهاية عادةً. ترجمة -وبتصرف- للفصول Long polling وWebSocket وServer sent events من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: استئناف رفع الملفات في جافاسكريبت تطويع البيانات في جافاسكربت
-
من السهل رفع ملف باستخدام fetch، لكن كيف سنستأنف عملية الرفع بعد فقدان الاتصال؟ لا توجد خيارات مدمجة لتنفيذ هذا الأمر، لكن لدينا كل الأجزاء التي تمكننا من تنفيذ ذلك. ينبغي أن تأتي عمليات الرفع القابلة للاستئناف مع مؤشرات على تقدم العملية، خاصةً عندما يكون الملف ضخمًا -إن أردنا استئناف الرفع-، وطالما لن تسمح fetch بتعقب عملية الرفع؛ فسنحتاج إلى الكائن XMLHttpRequest. حدث تتبع غير مفيد كفاية لا بدّ من معرفة الكمية التي جرى رفعها قبل انقطاع الاتصال لاستئناف رفع الملف، حيث يمتلك الكائن xhr الحدث xhr.upload.onprogress لتتبع تقدم عملية الرفع، لكنه لن يفيدنا في استئناف الرفع -لسوء الحظ-، لأنه سيقع عند انتهاء إرسال البيانات، لكن لا يمكن للمتصفح تحديد هل استقبلها الخادم فعلًا أم لا، فمن الممكن أن تكون هذه البيانات قد خُزّنت مؤقتًا من قبل شبكة محلية وكيلة، وربما تكون العملية انتهت على الخادم البعيد قبل أن يتمكن من معالجة البيانات، أو أنها ببساطة قد فُقدت في مرحلة ما ولم تصل إلى المستقبل النهائي، لذلك لن تتعدى فائدة هذا الحدث إظهار شريط تقدم جميل على الصفحة. إذًا لا بدّ من معرفة عدد البايتات التي استقبلها الخادم بدقة حتى نتمكن من استئناف الرفع، والخادم فقط هو من يمتلك هذه المعلومة، لذا فلا بدّ من إرسال طلب إضافي. الخوارزمية ننشئ في البداية معرّفًا مميزًا id للملف الذي سنرفعه، بالشكل التالي: let fileId = file.name + '-' + file.size + '-' + file.lastModified; سنحتاج إليه لاستئناف الرفع، إذ لا بدّ من إبلاغ الخادم عن الملف الذي سنستأنف رفعه، حيث إذا تغير اسم أو حجم أو تاريخ آخر تعديل للبيانات، فسيتغيّر معرّف الملف fileId. ثم نرسل إلى الخادم طلبًا نسأله عن حجم البيانات التي وصلت إليه (مقدرًا بالبايت)، بالشكل التالي: let response = await fetch('status', { headers: { 'X-File-Id': fileId } }); // تلقى الخادم هذا العدد من البايتات let startByte = +await response.text(); هذا يفترض قدرة الخادم على تتبع تقدم عملية رفع الملف باستخدام الترويسة X-File-Id، وينبغي إنجاز ذلك في الواجهة الخلفية (من جانب الخادم)، فإذا لم يكن الملف موجودًا بعد على الخادم، فيستجيب الخادم بالرمز 0. يمكننا استخدام التابع slice العائد لكائن البيانات الثنائية Blob لإرسال الملف ابتداءً من البايت الذي سنتابع بعده startByte، بالشكل التالي: xhr.open("POST", "upload", true); // معرّف الملف ليعلم الخادم اسم الملف الذي يُرفع xhr.setRequestHeader('X-File-Id', fileId); // البايت الذي سنستأنف منه الرفع xhr.setRequestHeader('X-Start-Byte', startByte); xhr.upload.onprogress = (e) => { console.log(`Uploaded ${startByte + e.loaded} of ${startByte + e.total}`); }; //أو أي مصدر input.files[0] قد يكون مصدر الملف من xhr.send(file.slice(startByte)); وهكذا سنرسل إلى الخادم معرّف الملف عبر الترويسة X-File-Id ليعرف الملف الذي نرفعه، كما سنرسل بايت البداية عبر الترويسة X-Start-Byte ليعرف الخادم بأننا لا نرفع الملف من البداية بل نستأنف عملية رفع سابقة، ولا بدّ أن يتحقق الخادم من سجلاته. فإذا جرت عملية رفع سابقة لهذا الملف وكان حجمه الحالي مساويًا تمامًا لقيمة X-Start-Byte؛ فسيُلحِق البيانات التي يستقبلها تاليًا به. إليك تطبيقًا نموذجيًا يضم شيفرتي الخادم والعميل وقد كتبت باستخدام Node.js، ويمكنك تنزيله وتشغيله محليًا لترى آلية عمله. شيفرة الخادم "server.js": let http = require('http'); let static = require('node-static'); let fileServer = new static.Server('.'); let path = require('path'); let fs = require('fs'); let debug = require('debug')('example:resume-upload'); let uploads = Object.create(null); function onUpload(req, res) { let fileId = req.headers['x-file-id']; let startByte = +req.headers['x-start-byte']; if (!fileId) { res.writeHead(400, "No file id"); res.end(); } // we'll files "nowhere" let filePath = '/dev/null'; // يمكن استخدام مسار حقيقي عوضًا عنه، مثل: // let filePath = path.join('/tmp', fileId); debug("onUpload fileId: ", fileId); // تهيئة عملية رفع جديدة if (!uploads[fileId]) uploads[fileId] = {}; let upload = uploads[fileId]; debug("bytesReceived:" + upload.bytesReceived + " startByte:" + startByte) let fileStream; // صفرًا أو غير مهيئ فسننشئ ملفًا جديدًا، وإلا فسيختبر الحجم ويضمه إلى الملف الموجود startByte إذا كان if (!startByte) { upload.bytesReceived = 0; fileStream = fs.createWriteStream(filePath, { flags: 'w' }); debug("New file created: " + filePath); } else { // we can check on-disk file size as well to be sure if (upload.bytesReceived != startByte) { res.writeHead(400, "Wrong start byte"); res.end(upload.bytesReceived); return; } // ضمه إلى الملف الموجود fileStream = fs.createWriteStream(filePath, { flags: 'a' }); debug("File reopened: " + filePath); } req.on('data', function(data) { debug("bytes received", upload.bytesReceived); upload.bytesReceived += data.length; }); // إرسال جسم الطلب إلى الملف req.pipe(fileStream); // عندما ينتهي الطلب وتكتَب المعلومات كلها fileStream.on('close', function() { if (upload.bytesReceived == req.headers['x-file-size']) { debug("Upload finished"); delete uploads[fileId]; // يمكننا فعل شيء آخر بالملف المرفوع res.end("Success " + upload.bytesReceived); } else { // فقد الاتصال، نترك الملف غير المكتمل debug("File unfinished, stopped at " + upload.bytesReceived); res.end(); } }); // I/O أنهِ الطلب في حال خطأ fileStream.on('error', function(err) { debug("fileStream error"); res.writeHead(500, "File error"); res.end(); }); } function onStatus(req, res) { let fileId = req.headers['x-file-id']; let upload = uploads[fileId]; debug("onStatus fileId:", fileId, " upload:", upload); if (!upload) { res.end("0") } else { res.end(String(upload.bytesReceived)); } } function accept(req, res) { if (req.url == '/status') { onStatus(req, res); } else if (req.url == '/upload' && req.method == 'POST') { onUpload(req, res); } else { fileServer.serve(req, res); } } // ----------------------------------- if (!module.parent) { http.createServer(accept).listen(8080); console.log('Server listening at port 8080'); } else { exports.accept = accept; } شيفرة العميل "uploader.js": class Uploader { constructor({file, onProgress}) { this.file = file; this.onProgress = onProgress; // أنشئ معرّف الملف // يمكننا إضافة معرّف لجلسة المستخدم (عند وجوده ) لجعله فريدًا أكثر this.fileId = file.name + '-' + file.size + '-' + file.lastModified; } async getUploadedBytes() { let response = await fetch('status', { headers: { 'X-File-Id': this.fileId } }); if (response.status != 200) { throw new Error("Can't get uploaded bytes: " + response.statusText); } let text = await response.text(); return +text; } async upload() { this.startByte = await this.getUploadedBytes(); let xhr = this.xhr = new XMLHttpRequest(); xhr.open("POST", "upload", true); // send file id, so that the server knows which file to resume xhr.setRequestHeader('X-File-Id', this.fileId); // send the byte we're resuming from, so the server knows we're resuming xhr.setRequestHeader('X-Start-Byte', this.startByte); xhr.upload.onprogress = (e) => { this.onProgress(this.startByte + e.loaded, this.startByte + e.total); }; console.log("send the file, starting from", this.startByte); xhr.send(this.file.slice(this.startByte)); // return // true if upload was successful, // false if aborted // throw in case of an error return await new Promise((resolve, reject) => { xhr.onload = xhr.onerror = () => { console.log("upload end status:" + xhr.status + " text:" + xhr.statusText); if (xhr.status == 200) { resolve(true); } else { reject(new Error("Upload failed: " + xhr.statusText)); } }; // xhr.abort() فقط عندما يُستدعى onabort يقع xhr.onabort = () => resolve(false); }); } stop() { if (this.xhr) { this.xhr.abort(); } } } الملف "index.js": <!DOCTYPE HTML> <script src="uploader.js"></script> <form name="upload" method="POST" enctype="multipart/form-data" action="/upload"> <input type="file" name="myfile"> <input type="submit" name="submit" value="Upload (Resumes automatically)"> </form> <button onclick="uploader.stop()">Stop upload</button> <div id="log">Progress indication</div> <script> function log(html) { document.getElementById('log').innerHTML = html; console.log(html); } function onProgress(loaded, total) { log("progress " + loaded + ' / ' + total); } let uploader; document.forms.upload.onsubmit = async function(e) { e.preventDefault(); let file = this.elements.myfile.files[0]; if (!file) return; uploader = new Uploader({file, onProgress}); try { let uploaded = await uploader.upload(); if (uploaded) { log('success'); } else { log('stopped'); } } catch(err) { console.error(err); log('error'); } }; </script> وستكون النتيجة بالشكل التالي: تُعَد الأساليب الجديدة لإرسال الطلبات عبر الشبكة أقرب في إمكانياتها إلى إدارة الملفات كما رأينا، مثل التحكم بالترويسات ومؤشرات تقدم العمليات وإرسال الملفات وغير ذلك، وهذا ما مكّننا من تنفيذ شيفرات لاستئناف رفع الملفات وغيرها الكثير. ترجمة -وبتصرف- للفصل Resumable file upload من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا هياكل البيانات: الكائنات والمصفوفات في جافاسكريبت الحياة السرية للكائنات في جافاسكريبت ترميز النصوص والتعامل مع كائنات الملفات في جافاسكريبت
-
هو كائن مدمَج في المتصفح، يسمح بإرسال طلبات HTTP في جافاسكريبت JavaScript، ويمكنه العمل مع أي نوع من البيانات، وليس فقط بيانات "XML" على الرغم من وجود الكلمة "XML" في تسميته. يساعدنا هذا الكائن في عمليات رفع وتنزيل الملفات وتتبع تقدمها وغير ذلك الكثير، ولا بدّ من الإشارة إلى وجود أسلوب أكثر حداثةً منه وهو استخدام fetch التي ألغت استخدامه بشكل أو بآخر. يُستخدم XMLHttpRequest في تقنيات تطوير الويب العصرية لثلاثة أسباب، هي الآتية: أسباب تاريخية: عند الحاجة إلى دعم سكربت موجود يستخدم XMLHttpRequest. الحاجة إلى دعم المتصفحات القديمة دون استخدام شيفرة "polyfill". إنجاز شيء لا يمكن تنفيذه باستخدام fetch مثل تتبع تقدم رفع ملف. إن كنت تحتاج ما ذكرناه تابع قراءة المقال وإلا فتوجه إلى فصل استخدام Fetch. الأساسيات يعمل الكائن XMLHttpRequest وفق النمطين المتزامن وغير المتزامن، سنتحدث بدايةً عن الوضع غير المتزامن، لأنه يستخدم في أغلب الحالات. يُنجز طلب HTTP وفق 4 خطوات: الخطوة الأولى، إنشاء الكائن XMLHttpRequest: let xhr = new XMLHttpRequest(); ليس للدالة البانية أي وسطاء. الخطوة الثانية، تهيئة الكائن، وتكون بعد إنشائه عادةً: xhr.open(method, URL, [async, user, password]) يؤمن التابع open المعاملات الرئيسية للطلب، وهي: method: تحدد نوع طلب HTTP، عادةً يكون GET أو POST. URL: عنوان الموقع الذي نرسل الطلب إليه، ويمكن استخدام الكائن URL. async: تكون العملية متزامنةً إذا أسندت لها القيمة false صراحةً، وسنغطي ذلك لاحقًا. user وpassword: اسم المستخدم وكلمة المرور للاستيثاق، عند الحاجة. لا يفتح التابع open الاتصال على الرغم من اسمه، بل يهيئ الطلب فقط، ولا يبدأ الاتصال عبر الشبكة إلا باستدعاء send. الخطوة الثالثة، إرسال الطلب: xhr.send([body]) يفتح هذا التابع الاتصال ويرسل الطلب إلى الخادم، حيث يحتوي المعامل body على جسم الطلب.ولا تمتلك الطلبات مثل GET جسماً، وتستخدم طلبات أخرى مثل POST الجسم لإرسال البيانات إلى الخادم، وسنرى أمثلةً عن ذلك لاحقًا. الخطوة الرابعة، الاستماع إلى أحداث الكائن xhr لتلقي الاستجابة، وإليك أكثر الأحداث استخدامًا: load: عندما يكتمل الطلب ونحصل على الاستجابة كاملةً، حتى لو أعاد الخادم رمز الحالة 500 أو 400 مثلًا. error: عندما يتعذر تنفيذ الطلب، كأن تكون الشبكة معطلةً أو العنوان خاطئًا. progress: ويقع دوريًا أثناء تنزيل الاستجابة، ويوضح الكمية التي نُزِّلت. xhr.onload = function() { alert(`Loaded: ${xhr.status} ${xhr.response}`); }; xhr.onerror = function() { // عندما لا تحدث الاستجابة إطلاقًا alert(`Network Error`); }; xhr.onprogress = function(event) { // يقع دوريًا // event.loaded - الحجم الذي نزل بالبايت // event.lengthComputable = true إن أرسل الخادم ترويسة طول المحتوى // event.total - العدد الكلي للبايتات alert(`Received ${event.loaded} of ${event.total}`); }; إليك مثالًا كاملًا، حيث تحمّل الشيفرة الموالية العنوان الآتي article/xmlhttprequest/example/load/ من الخادم، وتطبع تقدم العملية: // 1. إنشاء الكائن let xhr = new XMLHttpRequest(); // 2. URL /article/.../load تهيئته مع العنوان xhr.open('GET', '/article/xmlhttprequest/example/load'); // 3. إرسال الطلب عبر الشبكة xhr.send(); // 4. سيستدعى هذا الجزء عند تلقي الاستجابة xhr.onload = function() { if (xhr.status != 200) { // analyze HTTP status of the response alert(`Error ${xhr.status}: ${xhr.statusText}`); // e.g. 404: Not Found } else { // show the result alert(`Done, got ${xhr.response.length} bytes`); // response is the server response } }; xhr.onprogress = function(event) { if (event.lengthComputable) { alert(`Received ${event.loaded} of ${event.total} bytes`); } else { alert(`Received ${event.loaded} bytes`); // no Content-Length } }; xhr.onerror = function() { alert("Request failed"); }; نستقبل نتيجة الطلب باستخدام خصائص الكائن xhr التالية حالما يستجيب الخادم: status: رمز حالة HTTP، عدد، مثل 200 و404 و403 وهكذا، كما يمكن أن يكون 0 عند حدوث خطأ لا علاقة له بالنقل وفق بروتوكول HTTP. statusText: رسالة حالة HTTP، نص، مثل OK لرمز الحالة 200 وNot Found لرمز الحالة 404، وForbidden لرمز الحالة 403. response: قد تستخدم السكربتات القديمة responseText، وتمثل جسم الاستجابة التي يرسلها الخادم. يمكن أن نحدد أيضًا زمن انتهاء timeout مستخدمين الخاصية الموافقة: xhr.timeout = 10000; // زمن الانتهاء بالميلي ثانية إن لم ينجح الطلب خلال الفترة الزمنية المحددة فسيُلغى وسيقع الحدث timeout. إضافة معاملات بحث إلى العنوان URL let url = new URL('https://google.com/search'); url.searchParams.set('q', 'test me!'); // 'q' ترميز المعامل xhr.open('GET', url); // https://google.com/search?q=test+me%21 نوع الاستجابة من الممكن استخدام الخاصية ResponseType لضبط تنسيق الاستجابة: "": القيمة الافتراضية، الحصول على الاستجابة كنص. "text": الحصول على الاستجابة كنص. "arraybuffer": الحصول على الاستجابة على شكل كائن ArrayBuffer، للحصول على بيانات ثنائية. "blob": الحصول على الاستجابة على شكل كائن Blob، للحصول على بيانات ثنائية. "document": الحصول على مستند XML، باستخدام اللغة XPath أو غيرها. "json": الحصول على الاستجابة بصيغة JSON، تفسر الاستجابة تلقائيًا. لنستقبل الاستجابة بصيغة JSON مثلًا: let xhr = new XMLHttpRequest(); xhr.open('GET', '/article/xmlhttprequest/example/json'); xhr.responseType = 'json'; xhr.send(); // {"message": "Hello, world!"} الاستجابة هي xhr.onload = function() { let responseObj = xhr.response; alert(responseObj.message); // Hello, world! }; حالات الجاهزية Ready States يمر الكائن بعدة حالات عند تقدم تنفيذ الطلب، ويمكن الوصول إلى الحالة من خلال xhr.readyState، وإليك الحالات جميعها كما وردت في التوصيفات: UNSENT = 0; // الحالة الأساسية OPENED = 1; //open استدعاء التابع HEADERS_RECEIVED = 2; // استقبال ترويسة الاستجابة LOADING = 3; // تلقي جسم الاستجابة DONE = 4; // اكتمال الطلب ينتقل الكائن XMLHttpRequest بين الحالات السابقة بالترتيب 0 ثم 1 ثم 2 ثم 3 ثم 4، وتتكرر الحالة 3 في كل مرة تُستقبل فيها حزمة بيانات عبر الشبكة، ويمكننا تتبع هذه الحالات باستخدام الحدث readystatechange. xhr.onreadystatechange = function() { if (xhr.readyState == 3) { // تحميل } if (xhr.readyState == 4) { // انتهاء الطلب } }; قد تجد مستمعات للأحداث readystatechange حتى في الشيفرات القديمة، وذلك لأنّ أحداثًا مثل load وغيرها، لم تكن موجودةً في فترة ما، لكن معالجات الأحداث load/error/progress قد ألغت استخدامها. طلب الإلغاء Aborting request يمكن إلغاء الطلب في أي لحظة، وذلك باستدعاء التابع ()xhr.abort : xhr.abort(); // إلغاء الطلب يحرّض استدعاء التابع الحدث abort وتأخذ الخاصية xhr.status القيمة 0. الطلبات المتزامنة عند إسناد القيمة false إلى المعامل الثالث async للتابع open، فسيُنفَّذ الطلب بتزامن Synchronously. وبعبارة أخرى، ستتوقف JavaScript مؤقتًا عن التنفيذ عند إرسال الطلب ()send ثم تتابع عند تلقي الاستجابة، وهذا يشابه تعليمتي alert وprompt. إليك المثال السابق وقد أعيدت كتابته مع وجود المعامل الثالث للتابع open: let xhr = new XMLHttpRequest(); xhr.open('GET', '/article/xmlhttprequest/hello.txt', false); try { xhr.send(); if (xhr.status != 200) { alert(`Error ${xhr.status}: ${xhr.statusText}`); } else { alert(xhr.response); } } catch(err) { // onerror بدلًا من alert("Request failed"); } قد يبدو الأمر جيدًا لكنها طريقة يندر استخدامها، لأنها تعيق تنفيذ شيفرة JavaScript حتى يكتمل التحميل، وقد يصبح من المستحيل تمرير الصفحة لعرض بقية المحتويات أثناء تنفيذها في بعض المتصفحات، وقد يقترح المتصفح أيضًا إغلاق الصفحة العالقة إذا تأخر تنفيذ الطلب المتزامن كثيرًا. ولا تتاح العديد من الإمكانيات المتقدمة للكائن XMLHttpRequest، مثل الطلب من نطاق آخر أو تحديد زمن الانتهاء من الطلب، عند إرسال الطلبات بتزامن، ولا مؤشرات أيضًا على تقدم العملية، لذلك لا تُنفّذ الطلبات المتزامنة إلا نادرًا. ترويسات HTTP يتيح الكائن XMLHttpRequest إرسال ترويسات مخصصة وقراءة ترويسات الاستجابة، وهنالك ثلاثة توابع للتعامل مع الترويسات: (setRequestHeader(name, value: يعطي اسمًا name وقيمةً value لترويسة الطلب، وإليك مثالًا عن ذلك: xhr.setRequestHeader('Content-Type', 'application/json') فيما يلي مثال لتوضيح ذلك: xhr.setRequestHeader('X-Auth', '123'); xhr.setRequestHeader('X-Auth', '456'); // ستكون الترويسة: // X-Auth: 123, 456 (getResponseHeader(name: تعيد ترويسة الاستجابة ذات الاسم المحدد name، عدا Set-Cookie وSet-Cookie2، إليك مثالًا: xhr.getResponseHeader('Content-Type') ()getAllResponseHeaders: تعيد كل ترويسات الاستجابة، عدا Set-Cookie وSet-Cookie2، وتُعاد القيم ضمن سطر مفرد، حيث سيكون الفاصل هو "\r\n" (لا يتعلق بنظام تشغيل محدد) بين كل ترويستين، وبالتالي يمكن فصلها إلى ترويسات مفردة، وإليك مثالًا: Cache-Control: max-age=31536000 Content-Length: 4260 Content-Type: image/png Date: Sat, 08 Sep 2012 16:53:16 GMT تفصل بين اسم الترويسة وقيمتها النقطتان المتعامدتان، يليها فراغ " :"، وهذا أمر ثابت في التوصيفات، فلو أردنا مثلًا الحصول على كائن له اسم وقيمةP فلا بد من تنفيذ الأمر بالشكل التالي، مفترضين أنه عند وجود ترويستين بنفس الاسم، وعندها ستحل الأخيرة منهما مكان الأولى: let headers = xhr .getAllResponseHeaders() .split('\r\n') .reduce((result, current) => { let [name, value] = current.split(': '); result[name] = value; return result; }, {}); // headers['Content-Type'] = 'image/png' الطلب POST والكائن FormData يمكن استخدام الكائن FormData لإرسال طلب HTTP-POST: إليك الشيفرة اللازمة: let formData = new FormData([form]); //<form> إنشاء كائن يُملأ اختيارًا من formData.append(name, value); // ربط الحقل تنشئ الشيفرة السابقة الكائن FormData وتملؤه بقيم من نموذج form اختياريًا، وتضيف append حقولًا أخرى إن اقتضى الأمر، ومن ثم: نستخدم الأمر (...,'xhr.open('POST: لنؤسس الطلب POST. نستخدم الأمر (xhr.send(formData: لإرسال النموذج إلى الخادم. إليك المثال التالي: <form name="person"> <input name="name" value="John"> <input name="surname" value="Smith"> </form> <script> // form ملؤها من let formData = new FormData(document.forms.person); // إضافة حقل جديد formData.append("middle", "Lee"); // إرساله let xhr = new XMLHttpRequest(); xhr.open("POST", "/article/xmlhttprequest/post/user"); xhr.send(formData); xhr.onload = () => alert(xhr.response); </script> يُرسَل النموذج بترميز multipart/form-data، فإذا أردنا استخدام JSON فلا بدّ من تنفيذ الأمر JSON.stringify ثم إرساله في هيئة نص، إلى جانب ضبط الترويسة Content-Type على القيمة application/json، وتفكِّك العديد من إطارات العمل مع الخادم محتوى JSON تلقائيًا بهذه الطريقة. let xhr = new XMLHttpRequest(); let json = JSON.stringify({ name: "John", surname: "Smith" }); xhr.open("POST", '/submit') xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8'); xhr.send(json); ويمكن للتابع أن يُرسل أي جسم للطلب بما في ذلك كائنات Blob وBufferSource. تقدم عمليات رفع البيانات يقع الحدث progress في مرحلة التنزيل فقط، فلو نشرنا شيئًا ما باستخدام الطلب POST، فسيرفع الكائن XMLHttpRequest البيانات -جسم الطلب- أولًا ومن ثم ينزل الاستجابة. وسيكون تتبع تقدم عملية رفع البيانات خاصةً إن كانت ضخمة؛ أمرًا هامًا، لكن لن نستفيد من الحدث xhr.onprogress في حالتنا، يوجد كائن آخر لا يمتلك توابعًا، وهو مخصص حصرًا لتتبع أحداث رفع البيانات وهو xhr.upload، الذي يولِّد أحداثًا كما يفعلxhr، لكنها تقع فقط عند رفع البيانات: loadstart: يقع عندما يبدأ رفع البيانات. progress: يقع دوريًا مع تقدم الرفع. abort: يقع عند إلغاء الرفع. error: يقع عند وقوع خطأ لا يتعلق بالبروتوكول HTTP. load: يقع عند نجاح عملية الرفع. timeout: يقع عند انتهاء الوقت المخصص لرفع البيانات، إذا ضُبطت الخاصية timeout. loadend: يقع عند انتهاء الرفع بنجاح أو بإخفاق. إليك أمثلةً عن معالجات هذه الأحداث: xhr.upload.onprogress = function(event) { alert(`Uploaded ${event.loaded} of ${event.total} bytes`); }; xhr.upload.onload = function() { alert(`Upload finished successfully.`); }; xhr.upload.onerror = function() { alert(`Error during the upload: ${xhr.status}`); }; إليك أيضًا مثالًا واقعيًا عن رفع ملف مع مؤشرات على تقدم العملية: <input type="file" onchange="upload(this.files[0])"> <script> function upload(file) { let xhr = new XMLHttpRequest(); // تعقب تقدم عملية الرفع xhr.upload.onprogress = function(event) { console.log(`Uploaded ${event.loaded} of ${event.total}`); }; // تعقب الانتهاء، بنجاح أو إخفاق xhr.onloadend = function() { if (xhr.status == 200) { console.log("success"); } else { console.log("error " + this.status); } }; xhr.open("POST", "/article/xmlhttprequest/post/upload"); xhr.send(file); } </script> الطلبات ذات الأصول المختلطة يمكن للكائن XMLHttpRequest تنفيذ طلبات الأصل المختلط مستخدمًا سياسة CORS، تمامًا كما يفعل fetch، ولن يُرسل ملفات تعريف الارتباط cookies أو معلومات استيثاق إلى مواقع ذات أصل مختلف افتراضيًا. ولتمكين ذلك لا بدّ من ضبط الخاصية xhr.withCredentials على القيمة true. let xhr = new XMLHttpRequest(); xhr.withCredentials = true; xhr.open('POST', 'http://anywhere.com/request'); ... اطلع على فصل "استخدام Fetch مع الطلبات ذات الأصل المختلط" لمعلومات أكثر. خلاصة تمثل الشيفرة التالية، الشيفرة النموذجية لطلب GET باستخدام XMLHttpRequest let xhr = new XMLHttpRequest(); xhr.open('GET', '/my/url'); xhr.send(); xhr.onload = function() { if (xhr.status != 200) { // HTTP error? // معالجة الخطأ alert( 'Error: ' + xhr.status); return; } // xhr.response الحصول على الاستجابة من }; xhr.onprogress = function(event) { // إعطاء تقرير عن التقدم alert(`Loaded ${event.loaded} of ${event.total}`); }; xhr.onerror = function() { // handle non-HTTP error (e.g. network down) }; هنالك أحداث أكثر في التوصيفات الحديثة، والتي وُضِعت في قائمة مرتبة وفق دورة حياة كل حدث: loadstart: عندما يبدأ الطلب. progress: عند وصول حزمة بيانات، وتكون الاستجابة الكاملة في هذه اللحظة ضمن الاستجابة response. ()abort: عند إلغاء الطلب باستدعاء التابع. error: عند وقوع خطأ في الاتصال، مثل اسم نطاق خاطئ ولم يحدث نتيجة خطأ HTTP مثل 404. load: عند انتهاء الطلب بنجاح. timeout: عند إلغاء الطلب نتيجة تجاوز الوقت المخصص، ويحدث عندما تُضبَط هذه الخاصية. loadend: ويقع بعد الأحداث load أو error أو timeout أو abort، وهذه الأحداث متنافية فيما بينها، أي لا يمكن وقوع سوى حدث واحد منها. إن أكثر الأحداث استخدامًا هما حدث إكمال التحميل load وحدث إخفاق التحميل error، كما يمكن استعمال معالج الحدث loadend والتحقق من خصائص الكائن xhr للتأكد من طبيعة الحدث الذي وقع. لقد تعرفنا في هذا المقال أيضًا على الحدث readystatechange، والذي ظهر منذ زمن بعيد قبل استقرار التوصيفات، ولا حاجة حاليًا لاستخدامه، حيث يُستبدل بالأحداث الأكثر عصريةً. لكن مع ذلك قد نصادفه في بعض السكربتات القديمة. إذا أردنا تعقب تقدم رفع البيانات، فلا بدّ من الاستماع إلى نفس أحداث التنزيل لكن باستخدام الكائن xhr.upload. ترجمة -وبتصرف- للفصل XMLHttpRequest من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: كائنات URL في جافاسكريبت ترميز النصوص والتعامل مع كائنات الملفات في جافاسكريبت فحص الأصناف عبر instanceof في جافاسكربت هياكل البيانات: الكائنات والمصفوفات في جافاسكريبت الحياة السرية للكائنات في جافاسكريبت
-
يقدم الصنف URL المدمج واجهةً ملائمةً لإنشاء عناوين الموارد وروابط URL وتفسيرها، لا تحتاج الطلبات عبر الشبكة إلى هذا الكائن بالتحديد، فالقيم النصية التي يمكن أن تعبّر عن العناوين كافية، وبالتالي لن نحتاج إليه تقنيًا، لكننا سنجد أن استخدامه مفيد في مناسبات عدة. إنشاء رابط URL إليك الصيغة البرمجية التي تُنشئ كائن URL جديدًا: new URL(url, [base]) url: عنوان URL الكامل، أو جزء منه عند إسناد قيمة إلى base. base: أساس اختياري للعنوان، فإذا أسندت قيمة لهذا الوسيط وكانت قيمة الوسيط الآخر url هي مسار فقط، فسيُولّد الكائن URL منسوبًا إلى القاعدة base. إليك مثالًا: let url = new URL('https://javascript.info/profile/admin'); لاحظ أن كائني URL التاليين متطابقين تمامًا: let url1 = new URL('https://javascript.info/profile/admin'); let url2 = new URL('/profile/admin', 'https://javascript.info'); alert(url1); // https://javascript.info/profile/admin alert(url2); // https://javascript.info/profile/admin يمكن بالطبع إنشاء كائن URL جديد مبني على مسار نسبي أساسه كائن URL موجود مسبقًا: let url = new URL('https://javascript.info/profile/admin'); let newUrl = new URL('tester', url); alert(newUrl); // https://javascript.info/profile/tester نستطيع الوصول إلى مكوّنات الكائن URL مباشرةً، وبالتالي سيقدم طريقةً أنيقةً لتفسير العناوين: let url = new URL('https://javascript.info/url'); alert(url.protocol); // https: alert(url.host); // javascript.info alert(url.pathname); // /url إليك لائحةً بمكوّنات URL: href: ويعيد العنوان كاملًا، تمامًا كما يفعل التابع ()url.toString. protocol: جزء من العنوان ينتهي بالنقطتين ":". search: سلسلة نصية من المعاملات يبدأ بإشارة الاستفهام "؟". hash: ويبدأ بالعلامة "#". كما يمكنك أن تجد الخاصيتين user وpassword عند استخدام استيثاق HTTP، مثل http://login:password@site.com، لكنه نادر الاستخدام. معامل البحث "?" لنفترض أننا سننشئ عنوان url له معاملات بحث محددة، مثل https://google.com/search?query=JavaScript، كما يمكننا وضع المعاملات عند إنشاء كائن URL: new URL('https://google.com/search?query=JavaScript') ويجب ترميز المعاملات إذا احتوت على فراغات أو أحرف ليست لاتينيةً وما شابه (ستجد المزيد عن ذلك في الفقرات التالية)، لذلك توجد خاصية تتولى ذلك هي url.searchParams، وهي كائن من النوع URLSearchParams، وتؤمن مجموعةً من التوابع التي تتعامل مع معاملات البحث: (append(name, value: يضيف المعامل المحدد بالاسم name. (delete(name: يحذف المعامل المحدد بالاسم name. (get(name: يحضر المعامل المحدد بالاسم name. (getAll(name: يحضر كل المعاملات التي لها نفس الاسم name، وهو أمر ممكن، مثل ?user=John&user=Pete. (has(name: التحقق من وجود معامل بالاسم name. (set(name, value: لضبط معامل أو تغييره. ()sort: فرز المعاملات بالاسم، وتُستخدّم نادرًا، وهي قابلة للمرور عليها Iterable بصورة مشابهة للترابط Map. let url = new URL('https://google.com/search'); url.searchParams.set('q', 'test me!'); //! يضيف معاملًا ضمنه فراغ وإشارة alert(url); // https://google.com/search?q=test+me%21 url.searchParams.set('tbs', 'qdr:y'); // ":" يضيف معاملًا يحوي العلامة // تُرمز المعاملات تلقائيًا alert(url); // https://google.com/search?q=test+me%21&tbs=qdr%3Ay //(المرور على المعاملات (فك ترميز for(let [name, value] of url.searchParams) { alert(`${name}=${value}`); // q=test me!, then tbs=qdr:y } الترميز Encoding يحدد المعيار RFC3986 المحارف المسموحة في العناوين، والمحارف التي لا يُسمح باستخدامها. وينبغي ترميز المحارف التي لا يسمح بها، مثل الأحرف غير اللاتينية والفراغات، باستبدالها بمقابلاتها في ترميز UTF-8 مسبوقًا بالمحرف "%"، كأن نكتب 20%. ويمكن ترميز الفراغ بالمحرف "+" لأسباب تاريخية -وهذه حالة استثنائية-، لكن الجيد بالأمر هو أنّ الكائن URL ينجز كل ذلك تلقائيًا، وكل ما علينا فعله هو تزويده بالمعاملات دون ترميز، وسيحول العنوان إلى نص: // using some cyrillic characters for this example let url = new URL('https://ru.wikipedia.org/wiki/Тест'); url.searchParams.set('key', 'ъ'); alert(url); //https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D1%81%D1%82?key=%D1%8A لاحظ أن القيمتين Тест في مسار العنوان وъ في المعامل قد رُمِّزا، سيغدو العنوان أطول لأن كل محرف سيُمثّل ببايتين في UTF-8، وبالتالي ستكون هناك كتلتان من الشكل ..% لكل محرف. ترميز القيم النصية استخدم المبرمجون -في السابق وقبل ظهور الكائن URL- القيم النصية لتمثيل العناوين، لكن استخدام الكائن URL حاليًا أكثر ملاءمةً، ومع ذلك لا يزال استخدام القيم النصية شائعًا، فهو يجعل العنوان أقصر في الكثير من الأحيان، ولا بدّ عند استخدام القيم النصية من ترميز أو فك ترميز المحارف الخاصة يدويًا، باستخدام دوال مدمجة هي: encodeURI: يُرمِّز العنوان بالكامل. decodeURI: يفك ترميز النص المرمَّز. encodeURIComponent: يرمّز مكوّنًا من مكونات العنوان، مثل معاملات البحث أو المسار. decodeURIComponent: يفك ترميز الجزء المُرمَز. لكن السؤال الطبيعي سيكون: "ما هو الفرق بين encodeURIComponent وencodeURI؟ ومتى سنستخدم كلًا منهما؟" من السهل استيعاب الفكرة عند النظر إلى الصورة السابقة التي تفصل العنوان التالي إلى مكوناته: https://site.com:8080/path/page?p1=v1&p2=v2#hash إذ يُسمح باستخدام المحارف التالية : و? و= و& و# في عنوان URL، ومن جهة أخرى إذا نظرنا إلى أي مكوّن من مكوّنات العنوان بمفرده، مثل: المعاملات، فلا بدّ من ترميز هذه المحارف حتى لا تخِلّ بتنسيق العنوان. وهنا سنرى استخدام الدالتين السابقتين: encodeURI: تُرمز المحارف المرفوضة كليًا في العناوين فقط. encodeURIComponent: ترمز نفس المحارف التي ترمزها الدالة السابقة بالإضافة إلى المحارف التالية: # و$ و& و+ و, و/ و: و; و= و? و@. لذلك يمكن استخدام الدالة encodeURI لترميز العنوان كاملًا: // using cyrillic characters in url path let url = encodeURI('http://site.com/привет'); alert(url); // http://site.com/%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82 بينما ستستخدم الدالة encodeURIComponent في ترميز معاملات العنوان: let music = encodeURIComponent('Rock&Roll'); let url = `https://google.com/search?q=${music}`; alert(url); // https://google.com/search?q=Rock%26Roll لاحظ الفرق عند استخدام encodeURI: let music = encodeURI('Rock&Roll'); let url = `https://google.com/search?q=${music}`; alert(url); // https://google.com/search?q=Rock&Roll حيث لا تُرمّز الدالة encodeURI المحرف & لأنه محرف مقبول في عنوان URL الكامل، لكن لا بدّ من ترميزه عندما يكون ضمن معامل البحث، وإلا ستكون نتيجة q=Rock&Roll هي q=Rock، بالإضافة إلى معامل غامض يمثله Roll، وهي ليست كما قصدنا. لذا لا بدّ من استخدام encodeURIComponent فقط عند تشفير كل معامل من معاملات البحث، لكي نضعه بشكله الصحيح في نص العنوان، وتبقى طريقة ترميز الاسم والعنوان معًا هي الأكثر أمانًا؛ إلا عندما نثق تمامًا بعدم احتواء أي منهما على محارف ممنوعة الاستخدام. // valid url with IPv6 address let url = 'http://[2607:f8b0:4005:802::1007]/'; alert(encodeURI(url)); // http://%5B2607:f8b0:4005:802::1007%5D/ alert(new URL(url)); // http://[2607:f8b0:4005:802::1007]/ ترجمة -وبتصرف- للفصل URL Objects من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: الواجهة البرمجية Fetch API ترميز النصوص والتعامل مع كائنات الملفات في جافاسكريبت
-
لقد أخذنا فكرةً لا بأس بها عن fetch في المقالات السابقة من هذه السلسلة (بدءًا من مقال إرسال البيانات واستلامها عبر الشبكة وحتى مقال استخدام Fetch مع الطلبات ذات الأصل المختلط Cross-Origin)، والآن لنلق نظرةً على بقية مكوّنات الواجهة البرمجية لنغطي كل إمكاناتها. إليك قائمةً كاملةً بكل خيارت fetch الممكنة مع قيمها الافتراضية (وضعنا البدائل في تعليقات): let promise = fetch(url, { method: "GET", // POST, PUT, DELETE, etc. headers: { // the content type header value is usually auto-set // depending on the request body "Content-Type": "text/plain;charset=UTF-8" }, body: undefined // string, FormData, Blob, BufferSource, or URLSearchParams referrer: "about:client", // or "" to send no Referer header, // or an url from the current origin referrerPolicy: "no-referrer-when-downgrade", // no-referrer, origin, same-origin... mode: "cors", // same-origin, no-cors credentials: "same-origin", // omit, include cache: "default", // no-store, reload, no-cache, force-cache, or only-if-cached redirect: "follow", // manual, error integrity: "", // a hash, like "sha256-abcdef1234567890" keepalive: false, // true signal: undefined, // AbortController to abort request window: window // null }); لقد غطينا المفاهيم method وheaders وbody في مقال استخدام Fetch، كما غطينا signal في مقال إيقاف تنفيذ Fetch، وسنتعرف الآن على بقية الإمكانات. خيارا المحيل referrer وسياسة المحيل referrerPolicy يتحكم هذان الخياران بكيفية ضبط fetch للترويسة Referrer، وهي إحدى ترويسات HTTP، وتُضبط تلقائيًا لتحتوي على عنوان الصفحة التي ولّدت الطلب، ولا تعَد هامةً في معظم الأحيان، وقد يكون من المنطقي أحيانًا إزالتها أو تقصيرها لأسباب تتعلق بالأمان. يسمح الخيار referrer بتسمية أي مُحيل، وهو الصفحة أو الرابط الذي أحالك إلى الصفحة الحالية التي تعمل عليها، أو إزالته على أن يشترك بالأصل مع الصفحة الحالية، وإذا لم ترغب بإرسال أي محيل فأسند إليه نصًا فارغًا: fetch('/page', { referrer: "" // لا توجد توريسة محيل }); ولوضع عنوان مورد آخر من الأصل ذاته: fetch('/page', { // https://javascript.info بفرض أننا في // نستطيع ضبط أي ترويسة محيل، لكن ضمن الأصل الحالي referrer: "https://javascript.info/anotherpage" }); يضبط الخيار referrerPolicy بعض القواعد العامة للمُحيل Referer. تنقسم الطلبات إلى ثلاث مجموعات، هي الآتية: الطلب إلى مورد من الأصل ذاته. الطلب إلى مورد من أصل مختلف. الطلب من بروتوكول HTTPS إلى بروتوكول HTTP: أي من بروتوكول النقل الآمن إلى غير الآمن. يدل الخيار referrerPolicy المتصفح على القواعد الخاصة باستخدام المُحيل في كل مجموعة من الطلبات، ولا يسمح بضبط القيمة الدقيقة للمحيل. ستجد جميع القيم الممكنة في توصيف سياسة المحيل: no-referrer-when-downgrade: قيمتها الافتراضية "full"، حيث تُرسل قيمة المحُيل دومًا عدا الحالة التي يُرسَل فيها الطلب من HTTPS إلى HTTP (إلى بروتوكول أقل أمانًا). no-referrer: لا يُرسَل المُحيل. origin: يُرسل الأصل فقط ضمن المُحيل وليس عنوان الصفحة المُحيلة الكامل، أي يُرسل العنوان على الشكل http://site.com وليس على الشكل http://site.com/path. origin-when-cross-origin: يُرسل العنوان الكامل للمحيل إلى المواقع ذات الأصل المشترك، بينما يُرسَل الأصل فقط إلى المواقع ذات الأصل المختلط. same-origin: يُرسل المُحيل كاملًا إلى المواقع التي تنتمي إلى نفس الأصل، ولايرُسل أبدًا إلى المواقع ذات الأصول المختلطة. strict-origin: يُرسل الأصل فقط وليس المُحيل كاملًا في الطلبات من HTTPS إلى HTTP. strict-origin-when-cross-origin: يُرسل المُحيل كاملًا إلى المواقع التي تنتمي إلى نفس الأصل، ويُرسل الأصل فقط إلى المواقع ذات الأصول المختلطة، عدا الحالة التي يُرسَل فيها الطلب من HTTPS إلى HTTP، فلا يُرسل شيء. unsafe-url: يُرسل عنوان المُحيل كاملًا، حتى في الحالة التي يُرسَل فيها الطلب من HTTPS إلى HTTP. يوضح الجدول التالي جميع الخيارات: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } القيمة إلى نفس الأصل إلى أصل مختلف HTTPS→HTTP "no-referrer" - - - no-referrer-when-downgrade أو "" وهي القيمة الافتراضية كاملًا كاملًا - "origin" الأصل الأصل الأصل "origin-when-cross-origin" كاملًا الأصل الأصل "same-origin" كاملًا - - "strict-origin" الأصل الأصل - "strict-origin-when-cross-origin" كاملًا الأصل - "unsafe-url" كاملًا كاملًا كاملًا لنفترض وجود صفحة بصلاحيات مدير، ولا ينبغي كشف عنوانها خارج نطاق الموقع، لذا فعند إرسال fetch، فسترسَل الترويسة Referer افتراضيًا مع عنوان صفحتنا كاملًا، عدا الحالة التي يُرسَل فيها الطلب من HTTPS إلى HTTP. حيث لا توجد أي ترويسة Referer، فإذا كان العنوان هو Referer: https://javascript.info/admin/secret/paths مثلًا، وأردنا إرسال الأصل فقط وليس العنوان الكامل، فيمكن أن نرسل الخيار التالي: fetch('https://another.com/page', { // ... referrerPolicy: "origin-when-cross-origin" // Referer: https://javascript.info }); يمكن وضع الخيار السابق لكل استدعاءات fetch، كما يمكن أيضًا دمجه في مكتبة JavaScript التي نستخدمها في مشروعنا، والتي تنفّذ كل الطلبات التي تستخدم fetch، ويقتصر الفرق الوحيد بينه وبين الخيار الافتراضي في أنه يرسِل الجزء الأصلي من عنوان الموقع المُحيل، مثلًا: https://javascript.info ولا يُرسل المسار الكلي، وسنحصل على العنوان الكامل في الطلبات المُرسَلة إلى مواقع من نفس الأصل، فربما تكون مفيدةً لأغراض التنقيح. الخيار mode ويمثل هذا الخيار الحارس الذي يمنع الطلبات ذات الأصل المختلط التي تحدث فجأةً. cors: وهي القيمة الافتراضية، وتسمح بالطلبات ذات الأصل المختلط كما ورد في فصل استخدام Fetch في الطلبات ذات الأصل المختلط. same-origin: يمنع استخدام الطلبات ذات الأصل المختلط. no-cors: يسمح فقط لطلبات الأصل المختلط الآمنة. قد تظهر أهمية هذا الخيار عندما يأتي العنوان القادم مع fetch من طرف ثالث، ونريد آليةً للحد من الإمكانات المسموحة للأصول المختلطة. الخيار credentials ويحدد ما إذا كان على fetch إرسال ملفات تعريف الارتباط cookies، وترويسات استيثاق HTTP مع الطلب. same-origin: وهي القيمة الافتراضية، لا تُرسل الثبوتيات مع الطلبات ذات الأصول المختلطة. include: تُرسل الثبوتيات دومًا، ونحتاج إلى الترويسة Access-Control-Allow-Credentials من الخادم ذي الأصل المختلط لتتمكن جافا سكربت من الوصول إلى الاستجابة، وقد شرحنا ذلك في فصل استخدام Fetch في الطلبات ذات الأصل المختلط. omit: لا تُرسل الثبوتيات أبدًا، حتى للطلبات من الأصل نفسه. الخيار cache تستخدم طلبات fetch افتراضيًا ذاكرة HTTP المؤقتة المعيارية HTTP-cache، فهي تحترم الترويستين Expires وCache-Control، وترسل الترويسة If-Modified-Since تمامًا كما تفعله طلبات HTTP النظامية. يسمح الخيار cache بتجاهل "HTTP-cache" أو يضبط استخدامه: default: تستخدم fetch ترويسات وقواعد "HTTP-cache" المعيارية. no-store: يتجاهل الطلب قواعد "HTTP-cache" كليًا، وتصبح هذه القيمة افتراضيةً عند إرسال إحدى الترويسات التالية: If-Modified-Since أو If-None-Match أو If-Unmodified-Since أو If-Match أو If-Range. reload: لا يأخذ النتيجة من "HTTP-cache" -إن وجدت-، بل ينشر محتويات الذاكرة المؤقتة مع الاستجابة، إذا سمحت ترويسات الاستجابة بذلك. no-cache: يُنشئ طلبًا شرطيًا عند وجود استجابة مخزنة في الذاكرة المؤقتة، وطلبًا عاديًا في غير تلك الحالة، وينشر "HTTP-cache" مع الاستجابة. force-cache: يستخدم الاستجابة الموجودة في "HTTP-cache" حتى لو كانت قديمة، وسينشئ طلب HTTP نظاميًا إذا لم تحتوي على استجابة، كما سيسلك الطلب السلوك الطبيعي. only-if-cached: يستخدم الاستجابة الموجودة في "HTTP-cache" حتى لو كانت قديمةً، وسيرمي خطأً إذا لم تحتوي على استجابة، وتعمل فقط مع القيمة same-origin للخيار mode. الخيار redirect تخضع fetch بكل شفافية لإعادة التوجيه "HTTP-redirect" مثل الحالتين 301 (النقل النهائي لمحتوى) و302 (موجود ولكن يفضل الانتقال إلى العنوان الجديد). follow: وهي القيمة الافتراضية، ويخضع الطلب عندها لحالات إعادة التوجيه. error: يرمي خطأً عند محاولة إعادة توجيه الطلب. manual: يسمح بالتعامل مع إعادة توجيه الطلب يدويًا، وسنحصل عندها على كائن استجابة خاص من النوع "response.type="opaqueredirect، وتكون قيمة خاصية الحالة response.status صفرًا، وكذلك قيم أغلب خصائصه. الخيار integrity يسمح هذا الخيار بالتحقق من مطابقة الاستجابة للقيم الاختبارية Checksum المحددة مسبقًا، كما هو محدد في التوصيفات. وتُدعم دوال "hash" التالية: SHA-256 وSHA-384 وSHA-512، كما قد تتطلب بعض الاعتماديات وفقًا للمتصفح، فإذا كنا بصدد تنزيل ملف مثلًا، ونعلم أنّ القيمة الاختبارية له وفق SHA-256 هي "abcdef"، والتي ستكون أطول في الواقع، فيمكننا وضعها قيمةً للخيار integrity بالشكل التالي: fetch('http://site.com/file', { integrity: 'sha256-abcdef' }); ستحسب fetch قيمة SHA-256 بنفسها وتوازنها مع القيمة التي وضعناها، وسترمي خطأً عند عدم تطابق القيمتين. الخيار keepalive يسمح هذا الخيار ببقاء الطلب فعالًا خارج الصفحة التي أنشأتها. لنفترض مثلًا أننا نجمع إحصائيات عن سلوك المستخدم الحالي لصفحتنا (عدد نقرات الفأرة وأجزاء الصفحة التي زارها) لتحليل تجربة المستخدم وتطويرها، ونرغب بحفظ البيانات على الخادم عندما يغادر هذا المستخدم الصفحة، حيث يمكن أن ننفذ ذلك باستخدام الحدث window.onunload بالشكل التالي: window.onunload = function() { fetch('/analytics', { method: 'POST', body: "statistics", keepalive: true }); }; لكن ستُغلق كل طلبات الشبكة المتعلقة بالمستند عند إزالته، وهنا تظهر أهمية الخيار keepalive الذي يخبر المتصفح بإبقاء الطلبات حيةً في الخلفية حتى بعد أن يغادر الزائر الصفحة، لأن هذا الخيار أساسي لاستمرار الطلب ونجاحه. لكن بالطبع هناك بعض التقييدات في استخدامه، والمتمثلة في الآتي: لا يمكن إرسال أحجام بالميجابايت: لأن الحد الأعلى لحجم جسم الطلب مع خيار keepalive هو 64 كيلوبابت. إذا أردنا جمع إحصائيات كثيرةً عن الزائر، فلا بدّ من إرسالها بانتظام ضمن حزم متتالية، لكي لا تبقى الكثير من المعلومات التي لم ترسل بعد عند تنفيذ الطلب الأخير مع الحدث onunload. تطبق هذه التقييدات على كل الطلبات التي تحمل الخيار keepalive معًا، أي يمكن تنفيذ عدة طلبات من هذا النوع في الوقت نفسه، لكن يجب ألا يتجاوز مجموع أحجام أجسام هذه الطلبات حد 64 كيلوبايت. لا يمكن التعامل مع استجابة الخادم عند إزالة المستند، لذا سينجح استخدام fetch في مثالنا بوجود keepalive، لكن بالطبع لن تُنفَّذ الدوال اللاحقة. لن تظهر المشاكل في أغلب الأحيان عند إرسال بيانات مثل الإحصائيات، لأنّ الخادم سيقبل هذه البيانات وسيعيد غالبًا استجابةً فارغةً لطلبات مثل هذه. ترجمة -وبتصرف- للفصل Fetch: API من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: استخدام Fetch مع الطلبات ذات الأصل المختلط في جافاسكريبت
-
من المحتمل أن يخفق الطلب fetch المرسَل إلى موقع ويب آخر، مثلًا: ستخفق محاولة الحصول على http://example.com: try { await fetch('http://example.com'); } catch(err) { alert(err); // Failed to fetch } لا بدّ من الإشارة أولًا إلى المفاهيم البنيوية لموضوعنا: الأصل origin: وهو الثلاثية (نطاق ومنفذ وبروتوكول). الطلبات ذات الأصل المختلط cross-origin requests: وهي الطلبات المرسَلة إلى نطاق (أو نطاق فرعي) آخر أو عبر منفذ آخر أو باستخدام بروتوكول آخر، وتتطلب ترويسات خاصةً من الجانب البعيد. تدعى هذه السياسة "CROS" وهو اختصار للعبارة "Cross-Origin Resource Sharing"، وتعني مشاركة الموارد ذات الأصول المختلطة. لماذا نحتاج إلى CROS؟ لمحة تاريخية موجزة وُجِدت هذه السياسة لحماية الإنترنت من المخترقين، فلسنوات عديدة لم يكن مسموحًا لسكربت من موقع ما أن يصل إلى محتوى موقع آخر، حيث لا يمكن لسكربت مشبوه من الموقع hacker.com مثلًا الوصول إلى صندوق البريد الإلكتروني لمستخدم على الموقع gmail.com، مما أشعَر مستخدمي الإنترنت في ذلك الوقت بالأمان لعدم امتلاك جافاسكريبت JavaScript، أي توابع خاصة لتنفيذ طلبات عبر الشبكة، فقد كانت عبارةً عن لغة للتسلية وتزيين صفحات الويب، إلا أن مطوري الويب احتاجوا سريعًا إلى قوة أكبر للتحكم بالصفحات، فاخترعوا أساليب متنوعةً للالتفاف على محدودية اللغة وإرسال الطلبات إلى مواقع أخرى. استخدام النماذج لقد كانت إحدى طرق التواصل مع خادم آخر هي إرسال نموذج <form> إليه، وذلك باستخدام الإطارات <iframe> لإبقاء الزوار ضمن الصفحة نفسها: <!-- form target --> <iframe name="iframe"></iframe> <!-- a form could be dynamically generated and submited by JavaScript --> <form target="iframe" method="POST" action="http://another.com/…"> ... </form> وهكذا تمكن الناس من إرسال طلبات GET/POST إلى مواقع أخرى دون وجود توابع لتنفيذ ذلك، لأنّ النماذج قادرة على إرسال البيانات إلى أي مكان، لكن لم يكن بالإمكان الحصول على الاستجابة لأنّ الوصول إلى محتويات الإطار <iframe> غير مسموح، ولنكون دقيقين؛ وُجِدت بعض الحيل للالتفاف على ذلك أيضًا، لكنها تطلبت سكربتًا خاصًا يوضع ضمن الإطار والصفحة، أي صار التواصل بينهما ممكنًا من الناحية التقنية. استخدام السكربتات اعتمدت إحدى الحيل المستخدَمة أيضًا على المعرّف <script>، إذ يمكن أن تكون قيمة الخاصية src لسكربت هي اسم أي نطاق أو موقع مثل: <script src="http://another.com/…"> وبالتالي يمكن تنفيذ سكربت أيًا كان مصدره، فإذا أراد موقع ما مثل another.com إتاحة أمكانية الوصول لبياناته، فسيستخدم البروتوكول "JSON with padding" واختصاره JSNOP، وإليك آلية عمله: لنفترض أننا نريد الوصول إلى بيانات الطقس على الموقع http://another.com إنطلاقًا من موقعنا: نعرّف في البداية دالةً عامةً لاستقبال البيانات ولتكن gotWeather. // صرح عن الدالة التي ستعالج البيانات المطلوبة function gotWeather({ temperature, humidity }) { alert(`temperature: ${temperature}, humidity: ${humidity}`); } ننشئ <script>، وتكون قيمة الخاصية src فيه هي src="http://another.com/weather.json?callback=gotWeather" وللمستخدمين اسم الدالة العامة كقيمة للمعامل callback الخاص بالعنوان URL. let script = document.createElement('script'); script.src = `http://another.com/weather.json?callback=gotWeather`; document.body.append(script); يوّلد الخادم البعيد another.com ديناميكيًا سكربتًا يستدعي الدالة ()gotWeatherبالبيانات التي يريدنا أن نحصل عليها. // ستبدو الاستجابة التي نتوقعها من الخادم كالتالي gotWeather({ temperature: 25, humidity: 78 }); عندما يُحمَّل السكربت الذي يولده الخادم ويُنفَّذ، ستُنفَّذ الدالة gotWeather ونحصل على البيانات. سيعمل الأسلوب السابق ولن يشكل خرقًا لأمن الموقع البعيد بسبب اتفاق كلا الطرفين على تبادل المعلومات بهذا الشكل، ولهذا لن تُعَدَّ العملية عندها اختراقًا، ولا زالت بعض الخدمات تتبع نفس الأسلوب في الوصول إلى البيانات البعيدة وتعمل حتى على المتصفحات القديمة جدًا. ظهرت بعد فترة من الزمن ضمن لغة JavaScript توابع الطلبات عبر الشبكة والتي ينفذها المتصفح، وقد رفضت الطلبات ذات الأصول المختلطة في البداية، إلا أنه سُمح باستخدامها نتيجة نقاشات طويلة، بشرط الحصول على سماحيات صريحة من الخادم لتنفيذ أي متطلبات، ويُعبَّر عنها من خلال ترويسات خاصة. الطلبات الآمنة هنالك نوعان من الطلبات ذات الأصل المختلط: الطلبات الآمنة safe requests. بقية الأنواع. من السهل إنشاء الطلبات الآمنة لذلك سنبدأ بها، إذ يُعَد الطلب آمنًا إذا حقق الشرطين التاليين: يستخدم نوعًا آمنًا مثل GET أو POST أو HEAD. يستخدم ترويسات آمنةً. ويسمح بالترويسات المخصصة التالية فقط: Accept. Accept-Language. Content-Language. Content-Type: بحيث تحمل إحدى القيم التالية application/x-www-form-urlencoded أو multipart/form-data أو text/plain. وتُعَد بقية الطلبات "غير آمنة"، حيث لا تطابق الطلبات باستخدام PUT أو باستخدام الترويسة API-Key معايير الأمان السابقة مثلًا. ويكمن الفرق الجوهري في إمكانية تنفيذ الطلبات الآمنة باستخدام معرِّف النموذج <form> أو معرّف السكربت <script> دون الحاجة لأي توابع خاصة، وبالتالي ستكون أقدم الخوادم قادرةً على استقبالها. لا يمكن في المقابل استخدام الطلبات التي لا تمتلك ترويسات معياريةً مثل DELETE بهذه الطريقة، ولفترة طويلة لم تكن JavaScript قادرةً على استخدام هذا النوع من الطلبات، وهكذا سيفترض الخادم القديم أن هذه الطلبات قادمة من مصدر مخوّل بذلك، لأنه يتوقع أنّ صفحة الويب غير قادرة على إرسال هذه الطلبات. عندما نرسل طلبًا غير آمن، فسيرسل المتصفح طلبًا تمهيديَا preflight، سيسأل الخادم فيه إن كان سيوافق على طلبات ذات أصول مختلطةً أم لا، فإن لم يؤكد الخادم ذلك صراحةً من خلال الترويسات، فلن يُرسَل الطلب غير الآمن. سياسة CROS للطلبات غير الآمنة سيضيف المتصفح دائمًا الترويسة Origin إلى الطلب من الأصول المختلطة، فإذا طلبنا المورد https://anywhere.com/request من الموقع https://javascript.info/page مثلًا؛ فستبدو الترويسات بالشكل التالي: GET /request Host: anywhere.com Origin: https://javascript.info ... تحتوي الترويسة origin كما نرى الأصل (نطاق وبروتوكول ومنفذ) كما هو لكن دون مسار، ويمكن للخادم أن يتحقق من الترويسة Origin، فإن وافق على قبول هذا الطلب، فسيضيف ترويسةً خاصةً هي Access-Control-Allow-Origin إلى الاستجابة، وينبغي أن تحتوي الترويسة على الأصل المقبول (https://javascript.info في حالتنا) أو رمز النجمة (*)، عندها سيكون الطلب ناجحًا وإلا فسيُعَد خاطئًا. يلعب المتصفح دور الوسيط الموثوق حيث: يضمن إرسال الأصل الصحيح في الطلب ذي الأصول المختلطة. يتحقق من وجود السماحية Access-Control-Allow-Origin في الاستجابة، فإذا وُجدَت فسيسمح لشيفرة JavaScript بالوصول إلى الاستجابة وإلا ستخفق العملية وسيحدث خطأ. 200 OK Content-Type:text/html; charset=UTF-8 Access-Control-Allow-Origin: https://javascript.info ترويسات الاستجابة افتراضيًا قد لا تتمكن JavaScript من الوصول إلا إلى الترويسات الآمنة للاستجابة عند إرسال طلبات ذات أصل مختلط، وهذه الترويسات هي: Cache-Control. Content-Language. Content-Type. Expires. Last-Modified. Pragma. ويسبب الدخول إلى أي ترويسات أخرى خطأً. لمنح إمكانية الوصول إلى ترويسة الاستجابة، ينبغي أن يُرسل الخادم الترويسة Access-Control-Expose-Headers، والتي تتضمن قائمةً بأسماء الترويسات غير الآمنة التي يُفترض جعلها قابلةً للوصول، وتفصل بينها فاصلة، كالمثال التالي: 200 OK Content-Type:text/html; charset=UTF-8 Content-Length: 12345 API-Key: 2c9de507f2c54aa1 Access-Control-Allow-Origin: https://javascript.info Access-Control-Expose-Headers: Content-Length,API-Key وبوجود ترويسة مثل Access-Control-Expose-Headers سيُسمح للسكربت بقراءة ترويستي الاستجابة Content-Length وAPI-Key. الطلبات غير الآمنة يمكن استخدام جميع طلبات HTTP مثل PATCH وDELETE وغيرها، وليس فقط GET/POST، ولم يتخيل أحد في السابق إمكانية تنفيذ صفحات الويب لهذه الطلبات، لذلك قد تجد بعض خدمات الويب التي تعامل الطلبات غير المعيارية مثل إشارة "بأنها طلبات مصدرها ليس المتصفح"، ويمكنها أن تأخذ هذا الأمر في الحسبان عندما تتحقق من حقوق الوصول، ولتفادي سوء الفهم، لن ينفِّذ المتصفح أي طلبات غير آمنة كانت قابلة للتنفيذ مباشرةً فيما مضى، وسيرسل طلبًا تمهيديًا preflight إلى الخادم لطلب الإذن، ويستخدم الطلب التمهيدي التابع OPTIONS دون جسم للطلب، وبوجود ترويستين: الترويسة Access-Control-Request-Method: وتؤمن تابعًا للطلب غير الآمن. الترويسة Access-Control-Request-Headers: وتؤمن قائمةً بترويسات غير آمنة تفصل بينها فاصلة. إذا وافق الخادم على تنفيذ الطلبات، فسيرسل استجابةً بجسم فارغ ورمز الحالة 200، بالإضافة إلى الترويسات التالية: Access-Control-Allow-Origin: ويجب أن تحمل القيمة (*) أو أصل الموقع الذي أرسل الطلب، مثل "https://javascript.info"، ليُسمح له بالوصول. Access-Control-Allow-Methods: ويجب أن تحتوي التوابع المسموحة. Access-Control-Allow-Headers: ويحب أن تضم قائمةً بالترويسات المسموحة. Access-Control-Max-Age: وهي ترويسة إضافية يمكنها تحديد الفترة الزمنية (ثوانٍ) للاحتفاظ بالإذن، لذا لن يكون على المتصفح إرسال طلبات تمهيدية للطلبات اللاحقة التي تحقق السماحيات الممنوحة سابقًا. لنلق نظرةً على آلية العمل خطوةً خطوةً، بمثال عن طلب PATCH ذي أصول مختلطة (والذي يُستخدَم غالبًا لتحديث البيانات): let response = await fetch('https://site.com/service.json', { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'API-Key': 'secret' } }); لدينا ثلاثة أسباب لعدّ هذا الطلب غير آمن (ويكفي أحدها بالطبع): الطلب هو PATCH. لاتحمل الترويسة Content-Type إحدى القيم: application/x-www-form-urlencoded أو multipart/form-data أو text/plain. وجود الترويسة API-Key غير الآمنة. الخطوة 1: الطلب التمهيدي preflight يرسل المتصفح بنفسه -قبل إرسال طلب غير آمنٍ كهذا- طلبًا تمهيديًا له الشكل التالي: OPTIONS /service.json Host: site.com Origin: https://javascript.info Access-Control-Request-Method: PATCH Access-Control-Request-Headers: Content-Type,API-Key OPTIONS: تابع الطلب التمهيدي. /service.json: المسار، ويطابق مسار الطلب الرئيسي تمامًا. الترويسات الخاصة بالطلب ذي الأصل المختلط: Origin: أصل مُرسل الطلب. Access-Control-Request-Method: نوع الطلب Access-Control-Request-Headers:قائمة بترويسات غير آمنة تفصل بينها فاصلة. الخطوة 2: الاستجابة للطلب التمهيدي ينبغي أن يستجيب الخادم برمز الحالة 200 والترويسات التالية: Access-Control-Allow-Origin: https://javascript.info Access-Control-Allow-Methods: PATCH Access-Control-Allow-Headers: Content-Type,API-Key. تسمح هذه الاستجابة بالتواصل المستقبلي مع الخادم وإلا فسيقع خطأ. إذا كنت سترسل طلبات أو ترويسات من أنواع أخرى مستقبلًا، فمن المنطقي أن تطلب الإذن مسبقًا، وذلك بإضافتهم إلى القائمة أثناء الطلب التمهيدي، يوضح المثال التالي استجابةً يُسمح فيها باستخدام PUT وDELETE بالإضافة إلى ترويسات أخرى: 200 OK Access-Control-Allow-Origin: https://javascript.info Access-Control-Allow-Methods: PUT,PATCH,DELETE Access-Control-Allow-Headers: API-Key,Content-Type,If-Modified-Since,Cache-Control Access-Control-Max-Age: 86400 سيرى المتصفح الآن الطلب PATCH في قائمة الطلبات المسموحة Access-Control-Allow-Methods، كما سيرى الترويسة Content-Type,API-Key في قائمة الترويسات المسموحة، لذا لن يتردد بإرسال الطلب الرئيسي المبني عليهما. إذا رأى المتصفح الترويسة Access-Control-Max-Age وقد أسندت إليها قيمة بالثواني، فسيحتفظ بالسماحيات التي مُنحت للطلب التمهيدي خلال هذه المدة الزمنية، أي سيُحتفظ بالسماحيات في مثالنا السابق فترة 86400 ثانية (أي يوم كامل)، ولن تحتاج الطلبات اللاحقة إلى نفس الخادم طلبات تمهيديةً أخرى، بفرض أنها تتلاءم مع السماحيات الممنوحة، وستُرسل مباشرةً. الخطوة 3: الطلب الفعلي يرسِل المتصفح الطلب الفعلي عندما ينجح الطلب التمهيدي، وتُنفَّذ العملية يطريقة مماثلة لإرسال طلب آمن. سيتضمن الطلب الرئيسي الترويسة Origin (لأنه طلب ذو أصل مختلط): PATCH /service.json Host: site.com Content-Type: application/json API-Key: secret Origin: https://javascript.info الخطوة 4: الاستجابة الفعلية لا بدّ للخادم من إضافة الترويسة Access-Control-Allow-Origin إلى الاستجابة الرئيسية، ولن يُعفيه الطلب التمهيدي الناجح من هذه المهمة: Access-Control-Allow-Origin: https://javascript.info تستطيع بعد ذلك قراءة استجابة الخادم الفعلية. يمكن لشيفرة JavaScript الآن قراءة الاستجابة على الطلب الفعلي. الثبوتيات Credentials لا تحضر الطلبات ذات الأصل المختلط التي تنتج عن شيفرة JavaScript أية ثبوتيات (ملفات تعريف الارتباط cookies أو استيثاق HTTP)، وهذا ليس أمرًا شائعًا في طلبات HTTP، فعند إرسال طلب HTTP إلى الموقع http://site.com مثلَا، فسيحمل الطلب جميع ملفات تعريف الارتباط الموجودة في نطاق المُرسِل، لكن الطلبات ذات الأصل المختلط الناتجة عن JavaScript تُمثّل استثناءً، حيث لن يُرسل الأمر (fetch(http://another.com أي ملفات تعريف ارتباط حتى تلك التي تنتمي إلى النطاق another.com. لكن لماذا؟ لأنّ الطلبات التي تُزوَّد بثبوتيات أقوى بكثير، إذ يمكن لشيفرة JavaScript -إن سُمح لها- أن تعمل بكامل إمكانياتها بالنيابة عن المستخدم، وأن تصل إلى معلومات حساسة بالاستفادة من هذه الثبوتيات. لكن هل يثق الخادم بسكربت ما إلى هذا الحد؟ إن كان الأمر كذلك، فلا بدّ من السماح صراحةً بالطلبات التي تحمل ثبوتيات من خلال ترويسة إضافية، حيث سنحتاج إلى إضافة الخيار credentials: "include" عند إرسال الثبوتيات مع fetchبالشكل التالي: fetch('http://another.com', { credentials: "include" }); يمكن الآن إرسال ملفات تعريف الارتباط التي تنتمي إلى another.com عبر الطلب fetch إلى الموقع الهدف، وينبغي على الخادم إذا وافق على قبول الثبوتيات، إضافة الترويسة Access-Control-Allow-Credentials: true إلى استجابته بالإضافة إلى الترويسة Access-Control-Allow-Origin، مثلًا: 200 OK Access-Control-Allow-Origin: https://javascript.info Access-Control-Allow-Credentials: true لاحظ أنه يُمنع استخدام النجمة (*) كقيمة للترويسة Access-Control-Allow-Origin في الطلبات التي تحمل ثبوتيات، إذ لا بدّ -كما نرى في المثال السابق- من تحديد الأصل بدقة، وهذا معيار إضافي للتأكد من أنّ الخادم يعرف تمامًا الجهة التي يثق بها لتنفيذ طلبات مثل هذه. خلاصة هنالك نوعان من الطلبات ذات الأصول المختلطة من وجهة نظر المتصفح: آمنة وغير آمنة. لا بدّ للطلبات الآمنة من تحقيق الشرطين التاليين: أن تستخدم نوعًا آمنًا مثل: GET أو POST أو HEAD. أن تستخدم الترويسات الآمنة التالية: Accept Accept-Language Content-Language Content-Type: وتحمل إحدى القيم application/x-www-form-urlencoded أو multipart/form-data أو text/plain. الفرق الجوهري هو أن الطلبات الآمنة ومنذ وقت طويل، تُنفَّذ باستخدام معرِّف النموذج <form> أو معرِّف السكربت <script>، بينما مُنعت المتصفحات من تنفيذ الطلبات غير الآمنة لفترة طويلة. يظهر هذا الفرق عمليًا في إمكانية إرسال الطلبات الآمنة مع الترويسة Origin مباشرةً، بينما يحتاج المتصفح إلى إرسال طلب تمهيدي preflight عند إرسال الطلبات غير الآمنة، يطلب فيها إذنًا من الخادم. للطلبات الآمنة: يرسل المتصفح الترويسة مع الأصل Origin. بالنسبة للطلبات التي لا تحمل ثبوتيات (لا ترسَل الثبوتيات بشكل افتراضي) لا بدّ أن يضبط الخادم ما يلي: Access-Control-Allow-Origin: على القيمة (*) أو نفس قيمة الأصل Origin. بالنسبة للطلبات التي تحمل ثبوتيات لا بدّ أن يضبط الخادم: Access-Control-Allow-Origin: على نفس قيمة الأصل Origin. Access-Control-Allow-Credentials: على القيمة "true". ولمنح JavaScript الوصول إلى ترويسات الاستجابة عدا Cache-Control و Content-Language وContent-Type وExpires وLast-Modified وPragma، فلا بدّ أن يضع الخادم الترويسة التي يُسمح بالوصول إليها ضمن الترويسة Access-Control-Expose-Headers. يُرسل المتصفح طلبًا تمهيديًا قبل الطلب الفعلي عند إرسال طلبات غير آمنة: يرسل المتصفح الطلب OPTIONS إلى نفس العنوان الذي سيرسِل إليه الطلب الفعلي مزوّدًا بالترويسات التالية: Access-Control-Request-Method: ويحمل نوع الطلب. Access-Control-Request-Headers: ويحمل قائمةً بترويسات غير آمنة يُطلب الإذن باستخدامها. يستجيب الخادم برمز الحالة 200، وبالترويسات التالية: Access-Control-Allow-Methods: تضم قائمةً بأنواع الطلبات المسموحة. Access-Control-Allow-Headers: تضم قائمةً بالترويسات المسموحة. Access-Control-Max-Age: وتحتوي على قيمة تمثل الفترة الزمنية (مقدرةً بالثواني) التي يُحتفظ فيها بالسماحيات. يُرسَل الطلب الفعلي بعد ذلك، وتُطبق خطوات إرسال الطلب الآمن. ترجمة -وبتصرف- للفصل Fetch: Cross-origin Requests من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: تتبع تقدم عملية التنزيل باستخدام Fetch وإلغاء العملية Fetch
-
تتيح الدالة fetch تتبع عملية التنزيل download. لاحظ أنه لا توجد حاليًا طريقة تسمح للدالة fetch بتتبع عملية الرفع upload، نستخدم لهذه الغاية الكائن XMLHttpRequest الذي سنغطيه لاحقًا. تُستخدم الخاصية response.body لتتبع تقدم التنزيل، وتمثل هذه الخاصية كائن ReadableStream، وهو كائن خاص يزودنا عند وصوله بجسم الطلب كتلةً بكتلة chunk-by-chunk، ستجد وصفًا لمجاري التدفق القابلة للقراءة Readable streams في توصيف الواجهة Streams API، وتمنح الخاصية response.body تحكمًا كاملًا بعملية القراءة على خلاف التابعين ()response.text و()response.json وغيرهما. كما تمنح إمكانية تقدير الوقت المستغرق في أية لحظة. إليك مثالُا عن شيفرة تقرأ الاستجابة من response.body: // والطرق الأخرى response.json() بدلا من const reader = response.body.getReader(); // حلقة لا نهائية حتي يكتمل التنزيل while(true) { // عند آخر جزء true القيمة done ستحمل //لبايتات كل جزء Unit8Array هو value const {done, value} = await reader.read(); if (done) { break; } console.log(`Received ${value.length} bytes`) } ستكون نتيجة الاستدعاء ()await reader.read كائنًا له الخاصيتان التاليتان: done : تأخذ القيمة true عندم اكتمال عملية القراءة، وإلا فستكون قيمتها false. value : مصفوفة من النوع Uint8Array. //واحصل على قارئ للبيانات Fetch الخطوة1: إبدأ تنفيذ الدالة let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits?per_page=100'); const reader = response.body.getReader(); // الخطوة2: احصل على الحجم الكلي const contentLength = +response.headers.get('Content-Length'); // الخطوة3 : إقرأ البيانات let receivedLength = 0; // حجم البايتات المستقبلة حتى اللحظة let chunks = []; // مصفوفة الأجزاء المستلمة التي تمثل جسم الاستجابة while(true) { const {done, value} = await reader.read(); if (done) { break; } chunks.push(value); receivedLength += value.length; console.log(`Received ${receivedLength} of ${contentLength}`) } // الخطوة 4: ضم الأجزاء في مصفوفة واحدة let chunksAll = new Uint8Array(receivedLength); // (4.1) let position = 0; for(let chunk of chunks) { chunksAll.set(chunk, position); // (4.2) position += chunk.length; } // الخطوة5: الترميز في سلسلة نصية let result = new TextDecoder("utf-8").decode(chunksAll); // النهاية let commits = JSON.parse(result); alert(commits[0].author.login); المخرجات: "Received 258566 of 0" "Received 444982 of 0" لنشرح الشيفرة السابقة: لقد نفّذنا الدالة ftech، لكننا استخلصنا مجرى التدفق ()reader response.body.getReader بدلًا من استدعاء التابع ()response.json، ولايمكن استخدام الطريقتين معًا لقراءة الاستجابة، استخدم إحداهما للحصول على النتيجة. يمكننا قبل الشروع في قراءة الاستجابة تحديد الحجم الكلي لها عن طريق الترويسة Content-Length، وقد لا تكون الترويسة موجودةً في الطلبات ذات الأصل المختلط Cross-origin، لكن لن يُعدَّها الخادم عمليًا، وستبقى في مكانها. نستدعي التابع ()await reader.read حتى ينهي عمله، ونُجمِّع أجزاء الاستجابة في المصفوفة chunks، وهذا الأمر ضروري لأن الاستجابة ستختفي ولن نتمكن من إعادة قراءتها باستخدام ()response.json ولا بأي طريقة أخرى، وستحصل على خطأ إذا حاولت ذلك. سنحصل في النهاية على chunks وهي مصفوفة من الأجزاء لها النوع Uint8Array، وعلينا تجميعها ضمن نتيجة واحدة، ولسوء الحظ لا يوجد تابع لضمها، لهذا علينا كتابة الشيفرة التي ستنجز العملية: إنشاء المصفوفة (chunksAll = new Uint8Array(receivedLength، وهي مصفوفة من النوع Uint8Array لها حجم جميع الأجزاء. استخدام التابع (set(chunk, position. لنسخ كل جزء بدوره إليها. سنحصل على النتيجة ضمن المصفوفة chunksAll، وهي مصفوفة من البايتات وليست نصًا، ولتحويلها إلى نص لا بد من تفسير هذه البيانات عن طريق الكائن TextDecoder، ثم استخدام JSON.parse إن استدعت الحاجة. لكن ماذا لو احتجنا إلى محتوىً ثنائي بدل النص؟ سيكون الأمر أبسط، علينا فقط استبدال الخطوتين 4 و5 بسطر وحيد يُنشئ كائن بيانات ثنائية Blob يضم كل الأجزاء. let blob = new Blob(chunks); وهكذا سنحصل على النتيجة بالصيغة التي نريد مع إمكانية تتبع تقدم العملية، مرةً أخرى يجب الانتباه إلى أن العملية غير صالحة لتتبع تقدم عملية الرفع، أي لا يمكن استخدام Fetch، بل فقط للتنزيل. ولا بد من التحقق من حجم البيانات الواصلة receivedLength في كل لحظة ضمن الحلقة وإنهائها بمجرد وصولها إلى حد معين، إذا لم يكن حجم البيانات التي سنستقبلها معروفًا، وبالتالي لن تستهلك المصفوفة chunks الذاكرة. الكائن AbortController: مقاطعة العمليات غير المتزامنة تعيد fetch كما نعرف وعدًا promise، ولكننا نعلم أنّ JavaScript لا تقبل إلغاء الوعود عمومًا، فكيف سنلغي عملية fetch أثناء تنفيذها؟ هنالك كائن خاص مدمج لهذا الغرض هو AbortController، يمكن استخدامه لإلغاء fetch وغيرها من المهام غير المتزامنة، ويطبّق مباشرةً. لننشئ متحكمًا بالشكل التالي: let controller = new AbortController(); والمتحكم هو كائن شديد البساطة، له: تابع وحيد هو ()abort. وخاصية واحدة هي signal تسمح بإعداد مستمع حدث event listener له. أما عند استدعاء التابع ()abort فسيحدث الآتي: تحرّض الخاصية controller.signal وقوع الحدث abort. تأخذ الخاصية controller.signal.aborted القيمة true. تكون للعملية في العادة مرحلتان: المرحلة التي تُنفِّذ عمليةً قابلة للإلغاء، وتهيئ مستمع حدث للخاصية controller.signal. المرحلة التي تلغي: وتُنفَّذ باستدعاء التابع ()controller.abort عندما يتطلب الأمر. إليك مثالًا كاملًا دون Fetch: let controller = new AbortController(); let signal = controller.signal; // القيام بعملية قابلة للإلغاء // "signal" الحصول على الكائن // controller.abort() ضبط إطلاق المستمع عند استدعاء signal.addEventListener('abort', () => alert("abort!")); // القيام بالإلغاء controller.abort(); // abort! // true القيمة signal.aborted إطلاق الحدث ويصبح لــ alert(signal.aborted); // true لاحظ أنّ الكائن AbortController هو مجرد أداة لتمرير الحدث abort عند استدعاء التابع ()abort، ومن الواضح أنه بالإمكان تنفيذ مستمع حدث كهذا باستخدام شيفرتنا الخاصة دون الحاجة إلى AbortController، لكن أهميته ستظهر عندما نعلم أن fetch تعرف تمامًا كيفية التعامل معه، فهو متكامل معها. مقاطعة العملية Fetch لإلغاء العملية fetch علينا تمرير قيمة الخاصية signal العائدة للكائن AbortController مثل خيار لها: let controller = new AbortController(); fetch(url, { signal: controller.signal }); تعلم fetch تمامًا كيفية التعامل مع AbortController، وستستمع إلى الحدث abort الذي تحرّض الخاصية signal وقوعه. لإلغاء العملية سنستدعي التابع: controller.abort(); وهكذا تلغى العملية، حيث تحصل fetch على الحدث abort من الخاصية signal وتلغي الطلب، عند إلغاء fetch سيُرفض الوعد الذي تعيده وسيُرمى الخطأ AbortError، وينبغي التعامل معه من خلال حلقة try..catch مثلًا. إليك مثالًا كاملًا مع استخدام fetch، حيث تلغى العملية بعد ثانية واحدة: // الإلغاء خلال ثانية واحدة let controller = new AbortController(); setTimeout(() => controller.abort(), 1000); try { let response = await fetch('/article/fetch-abort/demo/hang', { signal: controller.signal }); } catch(err) { if (err.name == 'AbortError') { // handle abort() alert("Aborted!"); } else { throw err; } } كائن قابل للتوسع يسمح الكائنAbortController بإلغاء عدة عمليات معًا، إليك الشيفرة التمثيلية التالية التي تحضر عدة موارد على التوازي، ثم تستخدم كائن متحكم وحيدًا لإلغائها جميعًا: let urls = [...]; //قائمة بالموارد التي ينبغي إحضارها let controller = new AbortController(); // fetch مصفوفة من الوعود التي ستعيدها عمليات let fetchJobs = urls.map(url => fetch(url, { signal: controller.signal })); let results = await Promise.all(fetchJobs); // بمجرد استدعاء حدث الإلغاء ستلغى جميع عمليات الإحضار وسنتمكن أيضًا من إلغاء أي عمليات أخرى غير متزامنة مع عمليات fetch باستخدام كائن AbortController وحيد، بمجرد الاستماع إلى الحدث abort: let urls = [...]; let controller = new AbortController(); let ourJob = new Promise((resolve, reject) => { // المهمة المطلوب إلغاءها ... controller.signal.addEventListener('abort', reject); }); let fetchJobs = urls.map(url => fetch(url, { // عمليات الإحضار signal: controller.signal })); // انتظار إنجاز جميع العمليات let results = await Promise.all([...fetchJobs, ourJob]); // بمجرد استدعاء حدث الإلغاء ستلغى جميع عمليات الإحضار // بالإضافة إلى بقية المهام خلاصة بهذا نكون قد تعرفنا على كيفية تتبع عملية التنزيل باستخدام Fetch، وذلك بالاعتماد على عدة خاصيات، كما تعرفنا على كيفية مقاطعة العملية Fetch، وذلك بالاعتماد على الكائنات الآتية: AbortController: هو كائن بسيط يولّد الحدث abort على الخاصية signal عند استدعاء التابع ()abort، الذي يعطي الخاصية signal.aborted القيمة "true" أيضًا. تتكامل fetch مع هذا الكائن، حيث تُمرر الخاصية signal كخيار لتستمع إليه، وبالتالي يصبح إلغاؤها ممكنًا. يمكن استخدام AbortController في شيفرتنا، حيث يستمع التابع ()abort إلى الحدث abort بعملية بسيطة تطبق في أي مكان، كما يمكن استخدامها دون استخدام fetch. ترجمة -وبتصرف- للفصلين Fetch: Download Progress وFetch: Abort من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا ترميز النصوص والتعامل مع كائنات الملفات في جافاسكريبت
-
يمكن للغة جافاسكربت JavaScript إرسال طلبات شبكة إلى الخادم وتحميل معلومات جديدة عندما يتطلب الأمر ذلك، إذ يمكننا على سبيل المثال استخدام طلبات الشبكة في الحالات التالية: إرسال طلب. بتحميل معلومات مستخدم. الحصول على آخر التحديثات من الخادم. ويجري كل ذلك دون إعادة تحميل الصفحة. تنضوي طلبات الشبكة التي تنفذها لغة JavaScript تحت المظلة AJAX، وهي اختصار للعبارة Asynchronous JavaScript And XML، ورغم ذلك لا نحتاج إلى استخدام XML، فقد وضعت العبارة السابقة منذ فترة طويلة لذلك وجدت هذه الكلمة ضمنها، وقد تكون سمعت بهذه العبارة الآن أيضًا. هنالك طرق عديدة لإرسال طلبات عبر الشبكة والحصول على معلومات من الخادم، وسنبدأ بالطريقة الأحدث ()fetch، علمًا أنه لا تدعم المتصفحات القديمة هذه الدالة (ويمكن الاستعاضة عنها بشيفرة بديلة)، لكنها مدعومة جيدًا في المتصفحات الحديثة، وإليك صيغتها: let promise = fetch(url, [options]) حيث: url: عنوان المورد الذي ستصل إليه الدالة. options: المعاملات الاختيارية من توابع وترويسات وغيرها. تتحول الدالة إلى طلب GET بسيط لتنزيل محتوى العنوان url إن لم تكن هناك معاملات اختيارية options، ويبدأ المتصفح الطلب مباشرةً ويعيد وعدًا promise ستستخدمه الشيفرة التي تستدعي الطلب للحصول على النتيجة، وتكون الاستجابة عادةً عمليةً بمرحلتين: الأولى: يُحلَّل الوعد الذي تعيده fetch عبر كائن من الصنف Respo-nse حالما يستجيب الخادم بالترويسات المناسبة، ويمكن التحقق من نجاح الطلب أو عدم نجاحه، والتحقق أيضًا من الترويسات، لكن لن يصل جسم الطلب في هذه المرحلة، ويُرفَض الوعد إن لم تكن fetch قادرةً على إنجاز طلب HTTP لمشاكل في الشبكة مثلًا، أو لعدم وجود موقع على العنوان المُعطى، ولن تسبب حالات HTTP غير العادية مثل 404 أو 500 أخطاءً. يمكن معرفة حالة طلب من خصائص الاستجابة: status: رمز الحالة status code لطلب HTTP مثل الرمز 200. ok: قيمة منطقية "true" عندما يكون رمز الحالة بين 200 و299. إليك المثال التالي: let response = await fetch(url); if (response.ok) { // إن كان رمز الحالة بين 200-299 // الحصول على جسم الطلب let json = await response.json(); } else { alert("HTTP-Error: " + response.status); } الثانية: استخدام استدعاء إضافي للحصول على جسم الطلب، ويؤمن الكائن Response عدة توابع مبنية على الوعد للوصول إلى جسم الطلب وبتنسيقات مختلفة: ()response.text: لقراءة الاستجابة وإعادة نص. ()response.json: يفسِّر النص وفق تنسيق JSON. ()response.formData: يعيد الاستجابة على شكل كائن FormData سنشرحه في الفقرة التالية. ()response.blob: يعيد الاستجابة على شكل كائن البيانات الثنائية Blob. ()response.arrayBuffer: يعيد الاستجابة على شكل كائن ArrayBuffer وهو تمثيل منخفض المستوى للبيانات الثنائية. الكائن response.body وهو كائن من الصنف ReadableStream يسمح بقراءة جسم الطلب كتلةً كتلةً، وسنعرض مثالًا عن ذلك لاحقًا. لنحاول على سبيل المثال الحصول على كائن JSON من آخر نسخة معتمدة لموقع الدورة التعليمية هذه على GitHub: let url = 'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits'; let response = await fetch(url); let commits = await response.json(); // Json قراءة الاستجابة على شكل شيفرة alert(commits[0].author.login); See the Pen JS-P3-Fetch-ex02 by Hsoub (@Hsoub) on CodePen. كما يمكن فعل ذلك من خلال الوعود الصرفة دون استخدام await: fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits') .then(response => response.json()) .then(commits => alert(commits[0].author.login)); See the Pen JS-P3-Fetch-ex03 by Hsoub (@Hsoub) on CodePen. استخدم ()await response.text للحصول على نص الطلب بدلًا من ()json.: let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits'); let text = await response.text(); // قراءة جسم الاستجابة على شكل نص alert(text.slice(0, 80) + '...'); See the Pen JS-P3-Fetch-ex04 by Hsoub (@Hsoub) on CodePen. لنستعرض مثالًا عن قراءة بيانات بالصيغة الثنائية، ونحضر صورةً ما ونظهرها: let response = await fetch('https://javascript.info/article/fetch/logo-fetch.svg'); let blob = await response.blob(); // Blob تنزيل على شكل // <img> إنشاء عنصر let img = document.createElement('img'); img.style = 'position:fixed;top:10px;left:10px;width:100px'; document.body.append(img); // إظهاره img.src = URL.createObjectURL(blob); setTimeout(() => { // إخفاءه بعد ثلاث ثوان img.remove(); URL.revokeObjectURL(img.src); }, 3000); See the Pen JS-P3-Fetch-ex05 by Hsoub (@Hsoub) on CodePen. let text = await response.text(); // انتهاء معالجة جسم الطلب let parsed = await response.json(); // سيخفق، فقد جرت المعالجة وانتهت ترويسات الاستجابة يمكن الحصول على ترويسات الاستجابة على شكل كائن ترويسات شبيه بالترابط Map من خلال الأمر response.headers، ولا يُعَد الكائن ترابطًا تمامًا، لكنه يمتلك توابع مماثلةً للحصول على ترويسات من خلال اسمها أو بالمرور عليها: let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits'); // الحصول على ترويسة واحدة alert(response.headers.get('Content-Type')); // application/json; charset=utf-8 // المرور على الترويسات كلها for (let [key, value] of response.headers) { alert(`${key} = ${value}`); } See the Pen JS-P3-Fetch-ex06 by Hsoub (@Hsoub) on CodePen. ترويسات الطلب يمكن استخدام خيار الترويسات headers لإعداد ترويسة الطلب في الدالة featch، إذ تمتلك كائنًا يضم الترويسات المُرسَلة كالتالي: let response = fetch(protectedUrl, { headers: { Authentication: 'secret' } }); لكن هناك قائمة من ترويسات HTTP المنوعة التي لا يمكن ضبطها: Accept-Charset وAccept-Encoding Access-Control-Request-Headers Access-Control-Request-Method Connection Content-Length Cookie وCookie2 Date DNT Expect Host Keep-Alive Origin Referer TE Trailer Transfer-Encoding Upgrade Via Proxy-* Sec-* تضمن هذه الترويسات ملاءمة طلبات HTTP وأمانها، لذلك يتحكم فيها المتصفح حصرًا. طلبات الكتابة POST لإرسال طلب POST أو طلب من أي نوع لا بدّ من استخدام خيارات fetch: method: نوع طلب HTTP مثل HTTP-POST. body: ويمثل جسم الطلب وقد يكون: نصًا: بتنسيق JSON مثلًا. كائن FormData لإرسال بيانات على شكل form/multipart. كائن Blob أو BufferSourceلإرسال بيانات ثنائية. URLSearchParams لإرسال البيانات بتشفير x-www-form-urlencoded، وهو نادر الاستخدام. يُستخدم تنسيق JSON غالبًا، حيث تُرسل الشيفرة التالية الكائن user وفق تنسيق JSON مثلًا: let user = { name: 'John', surname: 'Smith' }; let response = await fetch('https://javascript.info/article/fetch/post/user', { method: 'POST', headers: { 'Content-Type': 'application/json;charset=utf-8' }, body: JSON.stringify(user) }); let result = await response.json(); alert(result.message); لاحظ ضبط الترويسة Content-Type افتراضيًا على القيمتين text/plain;charset=UTF-8 إذا كان جسم الطلب على شكل نص، لكن طالما أننا سنرسل البيانات بصيغة JSON، فسنستخدم الخيار headers لإرسال الترويسة application/json بدلًا عن text/plain كونها تمثل المحتوى الصحيح للبيانات. إرسال صورة يمكن إرسال بيانات ثنائية عبر الدالة fetch باستخدام الكائنات Blob أو BufferSource، سنجد في المثال التالي معرّف لوحة رسم <canvas> التي يمكننا الرسم ضمنها بتحريك الفأرة، ومن ثم إرسال الصورة الناتجة إلى الخادم عند النقر على الزر "submit": <body style="margin:0"> <canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas> <input type="button" value="Submit" onclick="submit()"> <script> canvasElem.onmousemove = function(e) { let ctx = canvasElem.getContext('2d'); ctx.lineTo(e.clientX, e.clientY); ctx.stroke(); }; async function submit() { let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png')); let response = await fetch('/article/fetch/post/image', { method: 'POST', body: blob }); // يستجيب الخادم بتأكيد وصول البيانات وبحجم الصورة let result = await response.json(); alert(result.message); } </script> </body> ستظهر النتيجة كالتالي: See the Pen JS-P3-Fetch-ex07 by Hsoub (@Hsoub) on CodePen. لاحظ أننا لم نضبط هنا قيمة الترويسة Content-Type يدويًا، لأنّ الكائن Blob له نوع مضمَّن (هو image/png في حالتنا، كما ولّده التابع toBlob)، وسيمثّل هذا النوع قيمة الترويسة Content-Type في كائنات Blob. يمكن كتابة الدالة ()submit دون استخدام الصيغة async/await كالتالي: function submit() { canvasElem.toBlob(function(blob) { fetch('https://javascript.info/article/fetch/post/image', { method: 'POST', body: blob }) .then(response => response.json()) .then(result => alert(JSON.stringify(result, null, 2))) }, 'image/png'); } استخدام الكائن FormData لإرسال النماذج يمكننا الاستفادة من الكائن FormData لإرسال نماذج HTML مع ملفات أو بدونها، بالإضافة إلى حقول إضافية. وكما قد تخمِّن؛ يمثل هذا الكائن بيانات نماذج HTML، وإليك صيغة الدالة البانية له: let formData = new FormData([form]); سيتحكم الكائن FormData تلقائيًا بحقول العنصر form إذا استُخدم في مستند HTML، وما يميز الكائن FormData هو أنّ توابع إرسال الطلبات واستقبالها عبر الشبكة مثل Fetch ستقبله مثل جسم للطلب، إذ يُشفَّر ويُرسَل بترويسة قيمتها Content-Type: multipart/form-data، وتبدو العملية بالنسبة إلى الخادم مثل إرسال عادي لنموذج. إرسال نموذج بسيط لنرسل أولًا نموذجًا بسيطًا، وسيظهر في مثالنا هذا في نموذج من سطر واحد: <form id="formElem"> <input type="text" name="name" value="John"> <input type="text" name="surname" value="Smith"> <input type="submit"> </form> <script> formElem.onsubmit = async (e) => { e.preventDefault(); let response = await fetch('https://javascript.info/article/formdata/post/user', { method: 'POST', body: new FormData(formElem) }); let result = await response.json(); alert(result.message); }; </script> وستكون النتيجة كالتالي: See the Pen JS-P3-FormData-ex01 by Hsoub (@Hsoub) on CodePen. لا توجد شيفرة خاصة بالخادم في هذا المثال، لأنها خارج نطاق هذه الدورة التعليمية، حيث سيقبل الخادم الطلب HTTP-POST ويستجيب بالرسالة "User saved" أي "خُزّن المستخدم". توابع الكائن نستخدم عددًا من التوابع لتعديل الحقول في الكائن FormData: (formData.append(name, value: يُضيف حقلًا بالاسم name قيمته هي value. (formData.append(name, blob, fileName: يضيف حقلًا كما لو أنه العنصر <"input type="file>، حيث يحدد الوسيط الثالث للتابع fileName اسم الملف -وليس اسم الحقل- كما لو أنه اسم لملف في منظومة ملفات الجهاز. (formData.delete(name: يزيل حقلًا محددًا بالاسم name. (formData.get(name: يعطي قيمة الحقل المحدد بالاسمname. (formData.has(name: إذا وجد حقل بالاسم name فسيعيد القيمة true وإلا false. يمكن أن يحوي النموذج العديد من الحقول التي لها نفس الاسم، لذلك سينتج عن الاستدعاءات المختلفة لضم append الحقول حقولًا لها نفس الاسم. وسنجد التابع set الذي له صيغة append نفسها، لكنه يزيل جميع الحقول التي لها اسم محدد name، ثم يضيف الحقل الجديد، وبالتالي سنضمن وجود حقل وحيد بالاسم name، تشبه باقي التفاصيل التابع append. (formData.set(name, value (formData.set(name, blob, fileName. يمكن أيضًا إجراء تعداد على عناصر الكائن FormData باستخدام الحلقة for..of: let formData = new FormData(); formData.append('key1', 'value1'); formData.append('key2', 'value2'); // قائمة من الأزواج مفتاح/قيمة for(let [name, value] of formData) { alert(`${name} = ${value}`); // key1 = value1, then key2 = value2 } See the Pen JS-P3-FormData-ex02 by Hsoub (@Hsoub) on CodePen. إرسال نموذج مع ملف يُرسَل النموذج دائمًا بحيث تكون ترويسة المحتوى مثل التالي Content-Type: multipart/form-data، وتسمح هذه الطريقة في الترميز بإرسال الملفات، أي ستُرسَل الملفات التي يحددها العنصر <"input type="file> أيضًا بشكل مشابه للإرسال الاعتيادي للنماذج، إليك مثالًا عن ذلك: <form id="formElem"> <input type="text" name="firstName" value="John"> Picture: <input type="file" name="picture" accept="image/*"> <input type="submit"> </form> <script> formElem.onsubmit = async (e) => { e.preventDefault(); let response = await fetch('https://javascript.info/article/formdata/post/user-avatar', { method: 'POST', body: new FormData(formElem) }); let result = await response.json(); alert(result.message); }; </script> ستظهر النتيجة كالتالي: See the Pen JS-P3-FormData-ex03 by Hsoub (@Hsoub) on CodePen. إرسال ملف يحتوي على كائن بيانات ثنائية يمكن أن نرسل بيانات ثنائيةً مولّدةً تلقائيًا، مثل الصور على شكل كائن بيانات Blob، وبالتالي يمكن تمريره مباشرةً مثل المعامل body للدالة Fetch كما رأينا في الفقرة السابقة، ومن الأنسب عمليًا إرسال صورة لتكون جزءًا من نموذج له حقول وبيانات وصفية Metadata وليس بشكل منفصل، إذ يُلائم الخوادم عادةً استقبال نماذج مشفرة مكونة من أجزاء متعددة أكثر من بيانات ثنائية خام. يُرسل المثال التالي صورةً مرسومةً ضمن العنصر <canvas>، بالإضافة إلى بعض الحقول على شكل نموذج باستخدام الكائن FormData: <body style="margin:0"> <canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas> <input type="button" value="Submit" onclick="submit()"> <script> canvasElem.onmousemove = function(e) { let ctx = canvasElem.getContext('2d'); ctx.lineTo(e.clientX, e.clientY); ctx.stroke(); }; async function submit() { let imageBlob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png')); let formData = new FormData(); formData.append("firstName", "John"); formData.append("image", imageBlob, "image.png"); let response = await fetch('https://javascript.info/article/formdata/post/image-form', { method: 'POST', body: formData }); let result = await response.json(); alert(result.message); } </script> </body> وتظهر النتيجة كالتالي: See the Pen JS-P3-FormData-ex04 by Hsoub (@Hsoub) on CodePen. لاحظ كيف يُضاف الكائن Blob الذي يمثل الصورة: formData.append("image", imageBlob, "image.png"); وهذا الأسلوب مشابه لاستخدام العنصر ضمن نموذج، حيث يرسل الزائر الملف الذي يحمل الاسم "image.png" (الوسيط الثالث) والذي يحمل البيانات التي يحددها imageBlob (الوسيط الثاني) انطلاقًا من منظومة الملفات، ويقرأ الخادم بيانات النموذج وكذلك الملف كما لو أنها عملية إرسال نموذج اعتيادية. خلاصة يتكون طلب إحضار بيانات تقليدي من استدعاءين باستخدام الصيغة await: let response = await fetch(url, options); // يُنفَّذ مع ترويسة الاستجابة headers let result = await response.json(); // JSON قراءة جسم الطلب بتنسيق أو دون الصيغة await: fetch(url, options) .then(response => response.json()) .then(result => /* process result */) وتتمثل خصائص الاستجابة في الآتي: response.status: رمز حالة HTTP للاستجابة. response.ok: يأخذ القيمة "true" إذا كانت قيمة رمز الحالة بين 200-299. response.headers: تعيد كائنًا شبيهًا بالترابط Map يضم ترويسات HTTP. توابع الحصول على جسم الاستجابة: ()response.text: لقراءة الاستجابة وإعادة نص. ()response.json: يفسر النص وفق تنسيق JSON. ()response.formData: يعيد الاستجابة على شكل كائن FormData. ()response.blob: تعيد الاستجابة على شكل كائن بيانات ثنائية Blob. ()response.arrayBuffer: يعيد الاستجابة على شكل كائن ArrayBuffer وهو تمثيل منخفض المستوى للبيانات الثنائية. خيارات Fetch التي تعرفنا عليها حتى الآن: method: نوع طلب HTTP. headers: كائن يضم ترويسات الطلب، ويجب الانتباه إلى الترويسات التي يُمنع استخدامها. body: البيانات التي ستُرسل (جسم الطلب) على شكل string أو FormData أو BufferSource أو Blob أو UrlSearchParams. وسنتعرف على خيارات أخرى في الفصل التالي. تُستخدم الكائنات FormData للتحكم بنماذج وإرسالها باستخدام fetch أو أي دوال لإرسال الطلبات عبر الشبكة، ويمكن إنشاؤها بالأمر (new FormData(form انطلاقًا من نموذج HTML موجود، أو إنشاؤها دون نموذج ثم نضيف إليه الحقول باستخدام التوابع: (formData.append(name, value (formData.append(name, blob, fileName (formData.set(name, value (formData.set(name, blob, fileName لاحظ هاتين الميزتين: يزيل التابع set الحقول التي لها نفس الاسم، بينما لا يفعل ذلك التابع append، وهذا هو الاختلاف الوحيد بينهما. لا بدّ من استخدام صيغة تضم ثلاثة وسطاء لإرسال الملف، آخرها اسم الملف والذي يؤخذ عادةً من منظومة ملفات المستخدم من خلال العنصر <"input type="file>. من التوابع الأخرى: (formData.delete(name (formData.get(name (formData.has(name تمارين إحضار بيانات مستخدمين من GitHub أنشئ دالةً غير متزامنة "async" باسم (getUsers(names للحصول على مصفوفة من سجلات الدخول Logins على GitHub، وتحضر المستخدمين أيضًا، ثم تعيد مصفوفةً بأسماء المستخدمين على GitHub. سيكون العنوان الذي يحوي معلومات مستخدم معين له اسم مستخدم محدد USERNAME هو: api.github.com/users/USERNAME. ستجد مثالًا تجريبيًا وضعناه في نمط الحماية sandbox يوضح ذلك. تفاصيل مهمة: ينبغي أن يكون هناك طلب إحضار واحد لكل مستخدم. لا ينبغي أن ينتظر أي طلب انتهاء طلب آخر، لكي تصل البيانات بالسرعة الممكنة. في حال إخفاق أي طلب، أو عدم وجود مستخدم بالاسم المُعطى، فينبغي أن تعيد الدالة القيمة "null" في المصفوفة الناتجة. افتح التمرين في بيئة تجريبية الحل إليك حل التمرين: نفِّذ التعليمة التالية لإحضار مستخدم: fetch('https://api.github.com/users/USERNAME') استدع التابع ()json. لقراءة الكائن JS، إن كان رمز الحالة المرافق لاستجابة الخادم هو 200. في الحالة التي تخفق فيها تعليمة الإحضار fetch أو لم يكن رمز الحالة 200، أعد القيمة null في المصفوفة الناتجة. إليك الشيفرة: async function getUsers(names) { let jobs = []; for(let name of names) { let job = fetch(`https://api.github.com/users/${name}`).then( successResponse => { if (successResponse.status != 200) { return null; } else { return successResponse.json(); } }, failResponse => { return null; } ); jobs.push(job); } let results = await Promise.all(jobs); return results; } ملاحظة: يرتبط استدعاء التابع then. بمباشرة بالدالة fetch، وبالتالي لا تنتظر عمليات إحضار أخرى لتنتهي عندما تصلك الاستجابة على أحدها بل إبدأ بقراءة الاستجابة مستخدمًا ()json.. إن استخدمت الشيفرة التالية : await Promise.all(names.map(name => fetch(...))) ثم استدعيت ()json. لقراءة النتائج، فقد يكون عليك الانتظار لتنتهي جميع عمليات الإحضار. ستضمن قراءة نتيجة كل عملية إحضار بمفردها إن استخدمت مباشرة ()json. مع fetch. فما عرضناه كان مثالًا عن فائدة واجهة الوعود البرمجية منخفضة المستوى low-level Promise API حتى لو استخدمنا async/await. إليك الحل في بيئة تجريبية مع الاختبارات ترجمة -وبتصرف- للفصلين popups and window methods و FormData من سلسلة The Modern JavaScript Tutorial
-
يمثل ArrayBuffer جزءًا من معيار "ECMA"، وهو جزء من جافاسكريبت JavaScript، لكن توجد كائنات عالية المستوى ضمن المتصفح وُصِفت في الواجهة البرمجية الخاصة بالملفات File API وبالتحديد الكائن Blob، والذي يتألف من نص افتراضي هو type (من النوع متعدد الوسائط MIME عادةً)، بالإضافة إلى الوسيط blobParts وهو سلسلة من كائنات Blob أخرى ونصوص ومصدر للبيانات الثنائية BufferSource. تأخذ الدالة البانية الصيغة التالية: new Blob(blobParts, options); حيث: blobParts: هو مصفوفة قيمها كائنات Blob وBufferSource وString. options: ويتضمن كائنات اختياريةً هي: type: يمثل نوع الكائن Blob، وهو عادةً من النوع متعدد الوسائط MIME مثل: "image/png". endings: ويحدد إن كنا سنحوّل محرف نهاية السطر للكائن Blob بما يناسب نظام التشغيل الحالي (n\ أو \r\n\)، وسيأخذ افتراضيًا القيمة "transparent" أي لا تفعل شيئًا، وقد يأخذ القيمة "native" أي أَجرِ التحويل. إليك المثال التالي: // (blob) إنشاء كائن بيانات ثنائية من نص let blob = new Blob(["<html>…</html>"], {type: 'text/html'}); // لاحظ أن الوسيط الأول هو مصفوفة // إنشاء كائن بيانات ثنائية من نص ومصفوفة let hello = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" بالصيغة الثنائية let blob = new Blob([hello, ' ', 'world'], {type: 'text/plain'}); يمكن استخراج الشرائح المكونة للكائن Blob كالتالي: blob.slice([byteStart], [byteEnd], [contentType]); حيث: byteStart: بايت البداية وافتراضيًا هو البايت 0. byteEnd: البايت الأخير (ضمنًا، وافتراضيًا حتى آخر المخزن). contentType: نوع كائن blob الجديد، وسيكون افتراضيًا نفس نوع مصدر البيانات. حيث تشابه هذه الوسائط مقابلاتها في التابع array.slice، ويُسمح باستخدام القيم السالبة. الكائن Blob كعنوان لمورد URL يمكن استخدام الكائن Blob مثل عناوين للرابط التشعبي <a> ولمعرّف الصورة <img> لإظهار محتوياتهما بفضل الوسيط type، كما يمكن رفع أو تنزيل الكائنات Blob، وسيتحول النوع type بالطبع إلى Content-Type في طلبات الشبكة network requests. لنبدأ بالمثال البسيط التالي، فبالنقر على الرابط سينزل كائن Blob مولَّد آليًا يحتوي على النص "Hello world" ضمن ملف: <!-- download attribute forces the browser to download instead of navigating --> <a download="hello.txt" href='#' id="link">Download</a> <script> let blob = new Blob(["Hello, world!"], {type: 'text/plain'}); link.href = URL.createObjectURL(blob); </script> See the Pen JS-P3-01-Blob-ex1 by Hsoub (@Hsoub) on CodePen. بالإمكان أيضًا إنشاء رابط آليًا في JavaScript، ومن ثم محاكاة عملية النقر بزر الفأرة ()link.click وسيبدأ بعدها التنزيل آليًا، إليك الشيفرة التالية التي تسمح للمستخدم بتنزيل كائن Blob المولَّد آليًا دون أي شيفرة HTML: let link = document.createElement('a'); link.download = 'hello.txt'; let blob = new Blob(['Hello, world!'], {type: 'text/plain'}); link.href = URL.createObjectURL(blob); link.click(); URL.revokeObjectURL(link.href); See the Pen JS-P3-01-Blob-ex2 by Hsoub (@Hsoub) on CodePen. يأخذ التابع URL.createObjectURL الكائن Blob مثل وسيط وينشئ عنوانًا له على الشكل <blob:<origin>/<uuid. ستبدو قيمة الخاصية link.href كالتالي: blob:https://javascript.info/1e67e00e-860d-40a5-89ae-6ab0cbee6273 يُخزِّن المتصفح كل عنوان يولّده التابع URL.createObjectURL على شكل ارتباط map داخلي من الشكل "URL>Blob"، لذا ستكون هذه العناوين قصيرةً، لكنها تسمح بالوصول إلى الكائن Blob، وسيكون العنوان المولَّد -وبالتالي الرابط المتعلق به- صالحًا ضمن المستند الحالي طالما كان مفتوحًا، كما سيسمح بتحديد مرجع للكائن في كل من <img> و<a> وغيرهما من الكائنات التي تحتاج إلى عنوان URL، ومع ذلك هنالك أثر جانبي، فعلى الرغم من ارتباط Blob بالعنوان فهذا الكائن مقيم في الذاكرة، ولا يمكن للمتصفح تحرير الذاكرة المتعلقة به، وسيُزال الارتباط كليًا عند إنهاء المستند، وبالتالي ستتحرر الذاكرة المرتبطة بالكائنات Blob، لكن إن استمر التطبيق لفترة طويلة فلن تحدث هذه العملية خلال فترة وجيزة. إذا أنشأنا عنوانًا فسيبقى الكائن Blob مقيمًا في الذاكرة حتى لو لم تَعُد هناك حاجة له. يزيل التابع (URL.revokeObjectURL(url المرجع من علاقة الارتباط الداخلي بين الكائن Blob والعنوان متيحًا المجال لإزالته (إن لم يرتبط بمراجع أخرى) وتحرير الذاكرة، وقد حرصنا في المثال الأخير على استخدام Blob مرةً واحدةً للتنزيل الفوري، لذلك سنستدعي مباشرةً الآتي: URL.revokeObjectURL(link.href) لم نستدع التابع (URL.revokeObjectURL(link.href في مثالنا السابق الذي تضمن رابط HTML قابلًا للنقر، مما سيجعل الكائن Blob غير صالح، ولن يعمل العنوان بعد إلغاء الارتباط المرجعي. التحويل بين Blob ونص مشفر بطريقة base64 يمكن أن نحوّل الكائن Blob إلى سلسلة نصية بتشفير base64 لتكون يمثابة وسيلة بديلة لاستخدام التابع URL.createObjectURL، يمثل التشفير السابق البيانات الثنائية مثل نص يتكون من محارف ASCII من 0 حتى 64 قابلة للقراءة، ويتمتع بحيز أمان كبير جدًا، والأهم من ذلك إمكانية استخدام هذا التشفير مع عناوين موارد البيانات data urls، والتي لها الشكل: <data:[<mediatype>][;base64],<data يمكن استخدام هذه العناوين في أي مكان مثل العناوين النظامية. إليك طريقة تمثيل بيانات تعطي وجهًا تعبيريًا Smiley: <img src="data:image/png;base64,R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7"> حيث سيفكك المتصفح شيفرة البيانات ويعطي كنتيجة الوجه التعبيري التالي: نستخدم الكائن FileReader المدمج لتحويل Blob إلى base64، حيث يستطيع قراءة البيانات من Blob بصيغ مختلفة، وسنتعمق في هذا الموضوع أكثر في الفصل القادم. إليك نموذجًا عن تنزيل Blob من خلال التشفير base64 هذه المرة: let link = document.createElement('a'); link.download = 'hello.txt'; let blob = new Blob(['Hello, world!'], {type: 'text/plain'}); let reader = new FileReader(); reader.readAsDataURL(blob); // استدعاء التحويل reader.onload = function() { link.href = reader.result; // عنوان بيانات link.click(); }; See the Pen JS-P3-01-Blob-ex3 by Hsoub (@Hsoub) on CodePen. يمكن استخدام إحدى الطريقتين السابقتين لتحويل كائن Blob إلى عنوان، لكن تكون عادةً الطريقة (URL.createObjectURL(blob أبسط وأسرع. table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } Blob to data url (URL.createObjectURL(blob لا حاجة لإزالة أي شيء لابد من إزالتها إذا كنا نهتم بمقدار الذاكرة المحجوزة خسارة في الأداء والذاكرة لكائنات Blob الضخمة عند الترميز وصول مباشر إلى الكائن Blob، لا حاجة للترميز وفك الترميز تحويل الصورة إلى كائن Blob يمكن تحويل صورة أو جزء من صورة أو لقطة شاشة إلى كائن بيانات ثنائية Blob، وهذا مفيد عند تحميل هذه الصور إلى مكان ما، وتُنفّذ العمليات على الصور باستخدام العنصر <canvas>: ارسم صورةً أو جزءًا منها ضمن لوحة الرسم Canvas باستخدام التابع canvas.drawImage. استدع التابع (toBlob(callback, format, quality الذي يُنشئ وينفّذ استدعاءً محددًا عندما يكتمل. ستجد في المثال التالي صورةً نُسخت للتو، لكن يمكن اقتطاع جزء منها أو نقلها إلى لوحة رسم قبل إنشاء Blob: // خذ أية صورة let img = document.querySelector('img'); // شكّل لوحة رسم بنفس الحجم let canvas = document.createElement('canvas'); canvas.width = img.clientWidth; canvas.height = img.clientHeight; let context = canvas.getContext('2d'); // انسخ الصورة إلى اللوحة context.drawImage(img, 0, 0); // يمكن أن نعدل اللوحة كما نشاء // عملية التحويل إلى كائن ثنائي غير متزامنة canvas.toBlob(function(blob) { //الكائن جاهز سننزله let link = document.createElement('a'); link.download = 'example.png'; link.href = URL.createObjectURL(blob); link.click(); // امسح المرجع الداخلي للكائن حتى يتمكن Canvas المتصفح من إزالته URL.revokeObjectURL(link.href); }, 'image/png'); See the Pen JS-P3-01-The-clickjacking-attack-ex7 by Hsoub (@Hsoub) on CodePen. يمكن استخدام الصيغة async/await بدلًا من دوال الاستدعاء: let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png')); يمكن استخدام مكتبات خاصة لالتقاط صورة للشاشة، وما عليك إلا الانتقال ضمن الصفحة ورسمها ضمن لوحة <canvas>، ثم يمكن نقلها بعد ذلك إلى Blob بنفس الأسلوب السابق. التحويل من الكائن Blob إلى الكائن arrayBuffer تسمح الدالة البانية للكائن Blob بإنشاء هذا الكائن من أي شيء تقريبًا، بما في ذلك أية كائنات BufferSource، لكن لو أردنا إنجاز عملية معالجة منخفضة المستوى، فيمكننا الحصول على كائن ArrayBuffer ذو مستوى أدنى مستخدمين FileReader: // get arrayBuffer from blob let fileReader = new FileReader(); fileReader.readAsArrayBuffer(blob); fileReader.onload = function(event) { let arrayBuffer = fileReader.result; }; خلاصة تمثل الكائنات ArrayBuffer وUint8Array وغيرها من الكائنات التي تنضوي تحت المصطلح BufferSource بيانات ثنائيةً، بينما يمثل Blob بيانات ثنائيةً لها نوع، مما يجعل الكائن Blob مناسبًا لعمليات الرفع والتنزيل التي تستخدم بكثرة من المتصفح. يمكن للتوابع التي تُنفِّذ طلبات الويب web-requests مثل: XMLHttpRequest وfetch وغيرها؛ أن تعمل مع Blob كما تعمل مع غيره من أنواع البيانات الثنائية. يمكن التحويل بسهولة بين Blob وكائنات البيانات الثنائية منخفضة المستوى: يمكن التحويل بين Blob ومصفوفة النوع باستخدام الدالة البانية (...)new Blob يمكن الحصول على الكائن ArrayBuffer من الكائن Blob باستخدام التعليمة FileReader، ثم إنشاء كائن استعراض بناءً على هذا الأخير، وذلك لمعالجة البيانات الثنائية في مستويات عمل منخفضة. ترجمة -وبتصرف- للفصل Blob من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: ترميز النصوص والتعامل مع كائنات الملفات في جافاسكريبت
-
ماذا لو كانت البيانات الثنائية معلومات نصيةً؟ أي ماذا لو تلقينا ملفًا يحتوي على نص؟ سيسمح لنا الكائن TextDecoder المدمج ضمن JavaScript بقراءة البيانات وتحويلها إلى نص فعلي، بعد تزويده بالمخزن المؤقت buffer الذي يحتوي البيانات الثنائية وطريقة فك الترميز، حيث علينا أوّلًا إنشاء مفكك الترميز Decoder: let decoder = new TextDecoder([label], [options]); حيث: label: يمثل طريقة الترميز، والتي ستكون utf-8 افتراضيًا، كما تدعم كلًا من الطريقتين big5 وwindows-1251 وغيرها. options: وتضم كائنات اختياريةً هي: fatal: قيمة منطقية boolean، حيث عندما تأخذ القيمة "true"، فسترمي استثناءً عند وجود محارف غير صالحة أي لا يمكن فك ترميزها، وإلا -وهي الحالة الافتراضية- فستستبدلها بالمحرف "uFFFD\". ignoreBOM: وهي قيمة منطقية ستتجاهل BOM (وهي علامة Unicode خاصة بترتيب البايتات) التي تُستخدم نادرًا، ويحدث ذلك عندما تأخذ القيمة "true". ومن ثم عملية فك الترميز Decoding: let str = decoder.decode([input], [options]); input: ويمثل كائن BufferSource الذي يحتوي البيانات الثنائية. options: وتضم كائنًا اختياريًا هو: stream: وتأخذ القيمة "true" عندما نريد فك ترميز مجرى تدفق دخل، وذلك عند استدعاء مفكك الترميز باستمرار عن طريق مجموعات البيانات القادمة، وفي هذه الحالة قد يوضع محرف من عدة بايتات multi-byte charecter ليفصل بين هذه المجموعات، ويخبر هذا الخيار مفكك الترميز بتذكر المحارف غير المرمزة، وأن يفك ترميزها عندما تصل المجموعة الجديدة من البيانات. فمثلًا: let uint8Array = new Uint8Array([72, 101, 108, 108, 111]); alert( new TextDecoder().decode(uint8Array) ); // Hello let uint8Array = new Uint8Array([228, 189, 160, 229, 165, 189]); alert( new TextDecoder().decode(uint8Array) ); // 你好 يمكن فك ترميز جزء من المخزن المؤقت بإنشاء مصفوفة ثانوية subarray بالطريقة التالية: let uint8Array = new Uint8Array([0, 72, 101, 108, 108, 111, 0]); // النص في الوسط // أنشئ تمثيلًا جديدًا دون نسخ أي شيء let binaryString = uint8Array.subarray(1, -1); alert( new TextDecoder().decode(binaryString) ); // Hello مرمز النصوص TextEncoder مهمته معاكسة لمهمة مفكك الترميز، إذ يحوّل المُرمِّز TextEncoder النص إلى بايتات. ويُستخدم كالتالي: let encoder = new TextEncoder(); كما يدعم طريقة الترميز "utf-8" فقط. وله تابعان هما: (encode(str: ويعيد كائنًا من النوع Uint8Array انطلاقًا من نص. (encodeInto(str, destination: يُرمز str إلى destination والتي ينبغي أن تكون من النوع Uint8Array. let encoder = new TextEncoder(); let uint8Array = encoder.encode("Hello"); alert(uint8Array); // 72,101,108,108,111 الكائنان File وFileReader يرث الكائن File الكائن Blob ويُوسَّع بإمكانيات تتعلق بنظام الملفات، وتوجد طريقتان لإنشائه: باستخدام الدالة البانية بشكل مشابه للكائن Blob: new File(fileParts, fileName, [options]) حيث: fileparts: يمثل مصفوفةً قد تكون قيمها نصية أو blob أو BufferSource. fileName: اسم الملف. options: وتضم الكائن الاختياري التالي: lastModified: تاريخ آخر تعديل (تاريخ من النوع الصحيح integer). الحصول على ملف باستخدام <"input type="file> أو أسلوب الجر والإفلات، أو غيرها من الواجهات التي يؤمنها المتصفح، حيث يأخذ الملف في هذه الحالات المعلومات السابقة التي استخدمَت بمثابة وسطاء من نظام التشغيل. بما أن الكائن File يرث الكائن Blob، فله خصائصه نفسها، بالإضافة إلى الآتي: name: اسم الملف. lastModified: تاريخ آخر تعديل. إليك مثالًا يصف الحصول على الكائن File باستخدام <"input type="file>: <input type="file" onchange="showFile(this)"> <script> function showFile(input) { let file = input.files[0]; alert(`File name: ${file.name}`); // e.g my.png alert(`Last modified: ${file.lastModified}`); // e.g 1552830408824 } </script> See the Pen JS-P3-02-File-and-FileReader-ex1 by Hsoub (@Hsoub) on CodePen. الكائن FileReader يمثل FileReader كائنًا يخدم غرضًا وحيدًا هو قراءة البيانات من الكائن Blob (وبالتالي من الكائن File أيضًا)، وينقل البيانات مستخدمًا الأحداث، لأن القراءة من القرص قد تستغرق وقتًا، وصيغة الدالة البانية له هي بالشكل: let reader = new FileReader(); // لا وسطاء لهذا الكائن عدة توابع، أهمها: (readAsArrayBuffer(blob: يقرأ البيانات بالصيغة الثنائية على شكل ArrayBuffer. ([readAsText(blob, [encoding: يقرأ البيانات مثل نص مستعمل للتشفير المحدد، وهو utf-8 افتراضيًا. (readAsDataURL(blob: يقرأ البيانات الثنائية ويحولها إلى عنوان بيانات data url مشفر بطريقة base64. ()abort: يلغي العملية. نحدد التابع الذي سيستعمَل في القراءة وفقًا لصيغة البيانات التي نفضلها، وكيف سنستخدمها. readAsArrayBuffer: يُستخدم لقراءة الملفات الثنائية وتنفيذ عمليات ثنائية منخفضة المستوى، بينما نستدعي الكائن File مباشرةً دون قراءة عند تنفيذ عمليات عالية المستوى (ليست ثنائيةً) مثل اقتطاع شرائح من البيانات slicing. readAsText: يُستخدم عند قراءة ملفات نصية والحصول على قيم نصية. readAsDataURL: يُستخدم عندما نريد استخدام البيانات المقروءة في الخاصية src للمعرف img وغيره من المعرّفات، كما توجد طريقة أخرى لقراءة الملف تعرفنا عليها في مقال "كائن البيانات الثنائية Blob"، وهي استخدام التابع (URL.createObjectURL(file يمكن الاستفادة من عدة أحداث تقع أثناء عملية القراءة: loadstart: يقع عند بداية التحميل. progress: يقع خلال القراءة. load: يقع عند انتهاء القراءة دون أخطاء. abort: يقع عند استدعاء التابع ()abort. error: يقع عند حدوث خطأ. loadend: يقع عند انتهاء القراءة بنجاح أو بفشل. يمكن الوصول إلى النتيجة عند انتهاء القراءة بالشكل التالي: reader.result: للحصول على النتيجة عند نجاح العملية. reader.error: للحصول على الخطأ عند إخفاق القراءة. وأكثر الأحداث استخدامًا هما الحدثان load وerror. إليك مثالًا عن قراءة ملف: <input type="file" onchange="readFile(this)"> <script> function readFile(input) { let file = input.files[0]; let reader = new FileReader(); reader.readAsText(file); reader.onload = function() { console.log(reader.result); }; reader.onerror = function() { console.log(reader.error); }; } </script> See the Pen JS-P3-02-File-and-FileReader-ex2 by Hsoub (@Hsoub) on CodePen. خلاصة تعرفنا في هذا المقال على كيفية ترميز النصوص وفك ترميزها بجافاسكريبت، مركزين على مرمز النصوص TextEncoder، بعدها انتقلنا للحديث عن كيفية التعامل مع كائنات الملفات، حيث توصلنا إلى الآتي: يرث الكائن File والكائن Blob. للكائن File الخاصيتين name وlastModified بالإضافة إلى خصائص الكائن Blob وتوابعه، مع إمكانية القراءة من نظام الملفات المرتبط بنظام التشغيل. يمكن الحصول على الكائنات File عن طريق مُدخلات المستخدم مثل <input> أو أحداث الجر والإفلات مثل ondragend. يمكن لكائن FileReader القراءة من ملف أو Blob بإحدى التنسيقات التالية: نص باستخدام التابع readAsText. ArrayBuffer باستخدام التابع readAsArrayBuffer. عنوان بيانات بتشفير base64 باستخدام التابع readAsDataURL. لا نحتاج في بعض الحالات إلى قراءة محتويات ملف، لذا وكما فعلنا مع الكائن blob سننشئ عنوانًا قصيرًا باستخدام الأمر (URL.createObjectURL(file ونسنده إلى المعرف <a> أو <img>، وبهذا يمكن تنزيل الملف أو عرضه كصورة أو كجزء من لوحة رسم canvas. سيكون الأمر بسيطًا إذا كنا سنرسل ملفًا عبر الشبكة، إذ يقبل الكائنان XMLHttpRequest و fetch الكائن File مباشرةً. ترجمة -وبتصرف- للفصلين text decoder and text encoder وFile and FileReader من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: مصفوفة المخزن المؤقت ArrayBuffer والمصفوفات الثنائية binary arrays هياكل البيانات: الكائنات والمصفوفات في جافاسكريبت تطويع البيانات في جافاسكربت
-
- كائنات الملفات
- ترميز
-
(و 1 أكثر)
موسوم في:
-
نتعامل في تطوير الويب مع البيانات بالصيغة الثنائية لما نتعامل مع الملفات، سواءً عند إنشائها أو رفعها أو تنزيلها، كما يمكن استخدامها في عمليات معالجة الصور، ويمكن إنجاز كل ما سبق في JavaScript التي تتميز العمليات الثنائية فيها بأنها عالية الأداء على الرغم من وجود بعض الالتباسات، نظرًا لوجود العديد من الأصناف، منها ArrayBuffer وUint8Array وDataView وBlob وFile، وغيرها. لا توجد طريقة معيارية للتعامل مع البيانات الثنائية في JavaScript موازنةً بغيرها من اللغات، لكن عندما نصنف الأمور فسيبدو كل شيء بسيطًا نوعًا ما، إذ يمثل الكائن الثنائي الأساسي ArrayBuffer مثلًا مرجعًا إلى مساحة ذاكرة متجاورة وذات حجم محدد، ويمكن إنشاؤه كالتالي: let buffer = new ArrayBuffer(16); // إنشاء مخزن مؤقت حجمه 16 alert(buffer.byteLength); // 16 حيث تحجز الشيفرة السابقة حجمًا من الذاكرة مقداره 16 بايت، ويُملأ بالأصفار. فالكائن ArrayBuffer هو مساحة من الذاكرة التي لا يمكن معرفة ما يُخزّن فيها، بل هي سلسلة خام من البايتات، وللتعامل مع ArrayBuffer، سنحتاج إلى كائن عرض لا يخزن أي بيانات بمفرده، بل تنحصر مهمته في تفسير البايتات المخزنة في ArrayBuffer، ومن بين هذه الكائنات الآتي: "Uint8Array": يعامِل كل بايت من ArrayBuffer مثل عدد مستقل يأخذ قيمةً بين 0 و255، وتُمثِّل عددًا صحيحًا بلا إشارة مؤلفًا من 8-بت. "Uint16Array": يعامِل كل بايتين مثل عدد صحيح يأخذ قيمةً بين 0 و65535، وتمثِّل عددًا صحيحًا بلا إشارة مؤلفًا من 16-بت. "Uint32Array": يعامِل كل 4 بايتات مثل عدد صحيح يأخذ قيمةً بين 0 و4294967295، وتمثل عددًا صحيحًا بلا إشارة مؤلفًا من 16-بت. "Float64Array": يعامل كل 8 بايتات مثل عدد عشري ذي فاصلة عائمة floating-point، ويأخذ قيمةً بين 5.0x 10^-324 و1.8x 10^308. وبالتالي سيفسَّر الكائن المؤلف من 16 بايت -وفق الكائنات السابقة- إلى 16 عدد صغير، أو 8 أرقام أكبر (بايتان لكل عدد)، أو 4 أعداد أضخم (4 بايتات لكل عدد)، أو عددين بالفاصلة العائمة عاليي الدقة (8 بايتات لكل منهما). فالكائن ArrayBuffer هو الكائن البنيوي وأساس البيانات الخام. ولا بدّ من استخدام كائن عرض عند محاولة الكتابة ضمنه أو المرور على قيمه وكذلك لأي عملية بالأساس، مثلًا: let buffer = new ArrayBuffer(16); // إنشاء مخزن مؤقت من 16 بايت let view = new Uint32Array(buffer); // تعامل مع المخزن كعدد صحيح من 32-بت alert(Uint32Array.BYTES_PER_ELEMENT); // أربعة بايت لكل عدد alert(view.length); // 4 alert(view.byteLength); // 16 // كتابة قيمة view[0] = 123456; // المرور على قيمه for(let num of view) { alert(num); // 123456, then 0, 0, 0 (أربعة قيم ككل) } الكائن TypedArray إنّ المصطلح الرئيسي لكائنات العرض السابقة هو مصفوفات النوع TypedArray، والتي تشترك جميعها بمجموعة التوابع والخصائص، مع ملاحظة عدم وجود دالة بانية اسمها TypedArray، فهو مجرد مصطلح يمثِّل كائنات العرض السابقة التي تطبق على ArrayBuffer، وهي Int8Array وUint8Array، وغيرها من الكائنات، وسنتابع بقية القائمة لاحقًا. عندما نرى أمرًا وفق الصيغة new TypedArray، فقد يعني ذلك أيًا من new Int8Array أو new Uint8Array وغيرها، إذ تسلك مصفوفات النوع سلوك بقية المصفوفات، فلها فهارس indexes ويمكن المرور على عناصرها، كما يختلف سلوكها باختلاف نوع الوسائط التي تقبلها، وهناك 5 حالات ممكنة للوسائط: new TypedArray(buffer, [byteOffset], [length]); new TypedArray(object); new TypedArray(typedArray); new TypedArray(length); new TypedArray(); إذا أضيف وسيط من النوع ArrayBuffer فسيتشكل كائن عرض موافق له، وقد استخدمنا هذه الصيغة سابقًا. يمكن أن نختار إزاحةً byteOffset لنبدأ منها (ولها القيمة 0 افتراضيًا)، وأن نحدد حجمًا محددًا length من المخزن (وسيكون حجم المخزن الكلي هو القيمة الافتراضية)، وبهذا سنغطي جزءًا من المخزن المؤقت buffer فقط. عندما نضع وسيطًا على شكل مصفوفة Array أو أي كائن شبيه بها، فستُنشِئ مصفوفةَ نوع لها نفس الحجم وتنسخ المحتويات إليها، ويمكن استخدام ذلك لملء المصفوفة مسبقًا بالبيانات: let arr = new Uint8Array([0, 1, 2, 3]); alert( arr.length ); // 4, ينشئ مصفوفة ثنائية لها الحجم نفسه alert( arr[1] ); // 1,لها القيم نفسها (unsigned 8-bit integers) يملؤها بـ 4 بايتات من النوع لو استخدمنا مصفوفة نوع TypedArray من نوع مختلف، فستُنشَأ مصفوفة نوع لها نفس الحجم وتنسخ المحتويات إليها، وستحوَّل القيم إلى النوع الجديد خلال العملية عند الحاجة لذلك، مثلًا: let arr16 = new Uint16Array([1, 1000]); let arr8 = new Uint8Array(arr16); alert( arr8[0] ); // 1 alert( arr8[1] ); // 232, tried to copy 1000, but can't fit 1000 into 8 bits (explanations below) يُنشئ الوسيط العددي length مصفوفة نوع تحتوي عدد عناصر مساويًا لقيمته، أما طولها بالبايت فسيكون مساويًا للقيمة length مضروبةً بعدد البايتات اللازم لتمثيل أحد عناصرها TypedArray.BYTES_PER_ELEMENT، مثلًا: let arr = new Uint16Array(4); // integer إنشاء مصفوفة نوع تحوي 4 أعداد من النوع alert( Uint16Array.BYTES_PER_ELEMENT ); // بايتان لكل عدد alert( arr.byteLength ); // حجمها 8 بايت عندما لا نستخدم أي وسائط، فستتشكل مصفوفة نوع طولها 0. يمكن إنشاء مصفوفة نوع مباشرةً دون الإشارة إلى الكائن ArrayBuffer، لكن كائن العرض لا يتشكل دون وجود الكائن الأساسي ArrayBuffer، وبالتالي سيتشكل كائن التخزين المؤقت أوتوماتيكيًا في الحالات السابقة كلها عدا الحالة الأولى (عندما ننشئه نحن). سنستخدم الخصائص التالية للوصول إلى الكائن ArrayBuffer: arr.buffer: يعيد مرجعًا إلى الكائن ArrayBuffer. arr.byteLength: يعيد طول الكائن ArrayBuffer. وبالتالي يمكننا الانتقال من كائن عرض إلى آخر، مثلًا: let arr8 = new Uint8Array([0, 1, 2, 3]); // كائن عرض آخر للمعطيات نفسها let arr16 = new Uint16Array(arr8.buffer); إليك قائمةً بمصفوفات النوع: "‘Uint32Array" و"Uint16Array" و"Uint8Array": تستخدم للأعداد الصحيحة 8 و16 و32 بت. "Uint8ClampedArray": لتمثيل الأعداد الصحيحة ذات 8-بت، وتبقي القيمة 0 أو 255 إن كانت خارج المجال. "‘Int32Array" و"Int16Array" و"Int8Array": لتمثيل الأعداد الصحيحة السالبة أو الموجبة. "Float32Array" و"Float64Array": لتمثيل الأعداد العشرية ذات 32 و64 بت والتي تحمل إشارةً بطريقة الفاصلة العائمة. سلوك المصفوفات عند خروج القيم عن حدودها عندما نحاول كتابة قيمة خارج حدود مصفوفة النوع فلن يظهر لنا خطأ، لكن ستُحذف البتات الزائدة. لنحاول مثلًا وضع القيمة 256 ضمن Uint8Array، فإذا حولنا 256 إلى الصيغة الثنائية، فستكون بالشكل "100000000" (9 بتات)، لكن النوع الذي استخدمناه يحوي 8 بتات فقط، أي قيمًا بين 0 و255، وفي حال أدخلنا رقمًا أكبر من النطاق السابق فستُخزن الخانات اليمينية الثمان (الأقل قيمة) فقط ويُحذف الباقي، وبالتالي سنحصل على القيمة 0 في هذا المثال. لنأخذ العدد 257 مثالًا آخر لذلك، أينما سنحصل ثنائيًا على تسع بتات "100000001"، ستُخزّن منها الخانات اليمينية الثمان الأولى، وسنحصل بالنتيجة على القيمة 1. let uint8array = new Uint8Array(16); let num = 256; alert(num.toString(2)); // 100000000 (التمثيل الثنائي) uint8array[0] = 256; uint8array[1] = 257; alert(uint8array[0]); // 0 alert(uint8array[1]); // 1 للنوع Uint8ClampedArray ميزة خاصة، فهو يعطي القيمة 255 لأي عدد أكبر من 255، والقيمة 0 لأي عدد سالب، وهذا مفيد في معالجة الصور. توابع الصنف TypedArray لهذا الصنف نفس توابع المصفوفات مع بعض الاستثناءات الواضحة، حيث يمكننا مسح عناصره، وتنفيذ التوابع map وslice وfind وreduce؛ إلا أنه لا يمكن تنفيذ عدة أشياء، منها الآتي: لا يمكن استخدام التابع splice: لا يمكن "حذف" قيمة ما، لأنّ مصفوفات النوع هي كائنات تعرض مساحات ثابتةً متجاورةً من الذاكرة مؤقتًا، وكل ما يمكننا فعله هو إسناد القيمة 0 لها. لا يمكن استخدام التابع concat. كما يوجد تابعان إضافيان: ([arr.set(fromArr, [offset: ينقل عناصر المصفوفة fromArr إلى arr ابتداءً من الموقع offset، والذي يأخذ القيمة 0 افتراضيًا. ([arr.subarray([begin, end: يُنشئ كائن عرض جديد من نفس النوع بين القيمتين begin وend ضمنًا، وهذا يشبه التابع slice المدعوم أيضًا، لكنه لا ينسخ أي قيم، بل ينشئ كائن عرض جديد للتعامل مع جزء محدد من البيانات. تتيح لنا التوابع السابقة نسخ مصفوفات النوع ودمجها وإنشاء مصفوفات جديدة من مصفوفات موجودة مسبقًا وغيرها. الكائن DataView يمثل الكائن DataView كائن عرض عالي المرونة بلا نوع untyped للكائن ArrayBuffer، حيث يسمح بالوصول إلى البيانات ابتداءً من أي موضع وبأي تنسيق. تحدِّد الدالة البانية تنسيق البيانات في مصفوفات النوع، ويُفترض أن تكون الدالة منتظمةً ككل، وأن نحصل على العنصر الذي ترتيبه "i" باستخدام التعليمة [arr[i. يمكن الوصول إلى البيانات عند استخدام DataView من خلال توابع مثل (getUint8(i. أو (getUint16(i.، ونختار التنسيق عند استدعاء التابع وليس عند إنشائه. نستخدم DataView كالتالي: new DataView(buffer, [byteOffset], [byteLength]) buffer: ويمثل الكائن ArrayBuffer الأساسي، ولا تنشئ DataView مخزنًا مؤقتًا خاصًا بها على عكس مصفوفات النوع، وإنما ينبغي أن يكون موجودًا مسبقًا. byteOffset: ويمثل بايت البداية لكائن العرض (القيمة الافتراضية 0). byteLength: عدد بايتات كائن العرض (إلى نهاية المخزن افتراضيًا). في المثال التالي نستخرج أعدادًا بتنسيقات مختلفة من المخزن ذاته: // مصفوفة ثنائية مكونة من 4 بايتات، وستكون أكبر قيمة 255 let buffer = new Uint8Array([255, 255, 255, 255]).buffer; let dataView = new DataView(buffer); // الحصول على رقم 8 بت عند الإزاحة 0 alert( dataView.getUint8(0) ); // 255 // الحصول على رقم 16 بت عند الإزاحة 0 alert( dataView.getUint16(0) ); // 65535 (biggest 16-bit unsigned int) // الحصول على رقم 32 بت عند الإزاحة 0 alert( dataView.getUint32(0) ); // 4294967295 (biggest 32-bit unsigned int) dataView.setUint32(0, 0); // إعطاء القيمة 0 لرقم من 4 بايت، وبالتالي تصفير المخزن تَظهر قدرة DataView عند تخزين أرقام ذات تنسيق مختلف في المخزن المؤقت نفسه، فلو خزّنّا مثلًا سلسلةً من البيانات مؤلفةً من الأزواج (عدد صحيح 16 بت، عدد عشري عائم 32 بت)، فسيسمح لنا DataView بالوصول إليها بكل سهولة. الخلاصة يمثل الكائن الأساسي ArrayBuffer مرجعًا إلى حجم ثابت ومتجاور في الذاكرة. نحتاج إلى كائن عرض لتنفيذ أي عملية على ArrayBuffer، ومن هذه الكائنات: مصفوفة النوع، والتي يمكن أن تكون: Uint8Array وUint16Array وUint32Array: وهي أعداد بلا إشارة من 8 أو 16 أو 32 بت. Uint8ClampedArray: أعداد صحيحة من 8 بت، مع اجتزاء البتات التي تزيد عن 8. Int8Array وInt16Array وInt32Array: أعداد صحيحة ذات إشارة. Float32Array وFloat64Array: أعداد بفاصلة عائمة من 32 أو 64 بت، وذات إشارة. الكائن DataView: ويستخدم التوابع لتحديد تنسيق الأعداد في مخزن مؤقت للبيانات، مثل (getUint8(offset. في معظم الأحيان يمكن العمل مباشرةً على مصفوفات النوع محتفظين بالكائن ArrayBuffer خلف الستار، ويمكن الوصول إليه باستخدام الأمر buffer.، كما يمكن إنشاء كائن عرض جديد عند الحاجة. يوجد مصطلحان إضافيان يستخدَمان في وصف التوابع التي تتعامل مع البيانات الثنائية: ArrayBufferView: وهو مصطلح يغطي كل أنواع كائنات العرض. BufferSource: مصطلح يغطي كلًا من ArrayBuffer أو ArrayBufferView، وسنرى هذه المصطلحات في الفصول القادمة، BufferSource من المصطلحات الشائعة، ويعني "أي نوع من البيانات الثنائية" سواءً كانت الكائن ArrayBuffer أو أي كائن عرض له. مهمات للإنجاز ضم مصفوفات النوع Concatenate typed arrays لنفترض وجود مصفوفة من النوع Uint8Array، يمكن استخدام الدالة (concat(arrays لضمّ عدة مصفوفات في مصفوفة واحدة. الحل إليك الحل المتمثل بالشيفرة التالية: function concat(arrays) { // مجموع أطوال كل مصفوفة let totalLength = arrays.reduce((acc, value) => acc + value.length, 0); if (!arrays.length) return null; let result = new Uint8Array(totalLength); // انسخ محتوى كل مصفوفة إلى الناتج result // يُنسخ محتوى المصفوفة اللاحقة على الجانب الأيمن للمصفوفة السابقة let length = 0; for(let array of arrays) { result.set(array, length); length += array.length; } return result; } افتح الحل في تجربة حية ترجمة -وبتصرف- للفصل Array buffer, Binary data من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: هجمات الاختطاف بالنقر Clickjacking في جافاسكريبت هياكل البيانات: الكائنات والمصفوفات في جافاسكريبت معالجة المصفوفات Arrays في جافا المصفوفات ثنائية البعد Two-dimensional Arrays في جافا البحث والترتيب في المصفوفات Array في جافا