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

HTTP والاستمارات في جافاسكربت


أسامة دمراني
اقتباس

يجب ألا تكون الاتصالات ذات طبيعة حالية، إذ يجب أن يحتوي كل طلب من العميل إلى الخادم على جميع المعلومات الضرورية لفهم الطلب، ولا يحتاج إلى أيّ سياق مخزَّن على الخادم، بل يجب ألا يتمكن من استغلاله.

ـــ روي فيلدينج Roy Fielding، كتاب الأنماط المعمارية وتصميم معماريات البرمجيات المبنية على الشبكات Architectural Styles and the Design of Network-based Software Architectures.

chapter_picture_18.jpg

يُعَدّ بروتوكول نقل النصوص الفائقة Hypertext Transfer Protocol الذي ذكرناه في مقال علاقة جافاسكريبت بتطور الإنترنت والمتصفحات آليةً تُطلب البيانات وتوفَّر من خلالها على الشبكة العالمية، كما سننظر فيه بالتفصيل ونشرح الطريقة التي تستخدِمه بها جافاسكربت المتصفحات.

البروتوكول

إذا كتبت eloquentjavascript.net/18_http.html في شريط العنوان لمتصفحك، فسيبحث المتصفح أولًا عن عنوان الخادم المرتبط بـ eloquentjavascript.net ويحاول فتح اتصال TCP معه على المنفَذ 80 الذي هو المنفَذ الافتراضي لحركة مرور HTTP، فإذا كان الخادم موجودًا ويقبل الاتصال فقد يرسل المتصفح شيئًا مثل هذا:

GET /18_http.html HTTP/1.1
Host: eloquentjavascript.net
User-Agent: Your browser's name

ثم يستجيب الخادم من خلال نفس قناة الاتصال:

HTTP/1.1 200 OK
Content-Length: 65585
Content-Type: text/html
Last-Modified: Mon, 08 Jan 2018 10:29:45 GMT

<!doctype html>
... the rest of the document

تكون أول كلمة هي التابع الخاص بالطلب، إذ تعني GET أننا نريد الحصول على مصدر بعينه، كما هناك توابع أخرى مثل DELETE لحذف المصدر وPUT لإنشائه أو استبداله وPOST لإرسال معلومات إليه. لاحظ أنّ الخادم ليس عليه تنفيذ جميع الطلبات التي تأتيه، فإذا ذهبتَ إلى موقع ما وطلبت حذف صفحته الرئيسية فسيرفض الخادم؛ أما الجزء الذي يلي اسم التابع، فيكون مسار المورد الذي يُطبق الطلب عليه، حيث يكون ملفًا على الخادم في أبسط حالاته، لكن البروتوكول لا يشترط كونه ملفًا فقط، بل قد يكون أي شيء يمكن نقله كما لو كان ملفًا، كما تولِّد العديد من الخوادم الاستجابات التي تنتجها لحظيًا، فإذا فتحت https://github.com/marijnh مثلًا، فسيبحث الخادم في قاعدة بياناته عن مستخدِم باسم marijnh، فإذا وجده فسيولِّد صفحة مستخدِم له.

يذكر أول سطر في الطلب بعد مسار المورد الـ HTTP/1.1 للإشارة إلى نسخة بروتوكول HTTP الذي يستخدِمه، كما تستخدِم مواقع كثيرة النسخة الثانية من HTTP عمليًا، إذ تدعم المفاهيم نفسها التي تدعمها النسخة الأولى 1.1، لكنها أعقد منها لتكون أسرع، كما ستبدِّل المتصفحات إلى البروتوكول المناسب تلقائيًا أثناء التحدث مع الخادم المعطى، وسيكون خرج الطلب هو نفسه بغض النظر عن النسخة المستخدَمة، لكننا سنركز على النسخة 1.1 بما أنها أبسط وأسهل في التعديل عليها.

ستبدأ استجابة الخادم بالنسخة أيضًا تليها بحالة الاستجابة مثل شيفرة حالة من ثلاثة أرقام أولًا، ثم مثل سلسلة نصية مقروءة من قِبَل المستخدِم.

HTTP/1.1 200 OK

تبدأ رموز الحالة بـ 2 لتوضح نجاح الطلب؛ أما الطلبات التي تبدأ بـ 4 فتعني أنّ ثمة شيء خطأ في الطلب، ولعل أشهر رمز حالة HTTP هنا هي 404، والتي تعني أن المصدر غير موجود أو لا يمكن العثور عليه؛ أما الرموز التي تبدأ بالرقم 5، فتعني حدوث خطأ على الخادم ولا تتعلق المشكلة بالطلب نفسه، وقد يُتبع أول سطر من الطلب أو الاستجابة بعدد من الترويسات، وهي أسطر في صورة name: value توضِّح معلومات إضافية عن الطلب أو الاستجابة، وهي جزء من المثال الذي يوضح الاستجابة:

Content-Length: 65585
Content-Type: text/html
Last-Modified: Thu, 04 Jan 2018 14:05:30 GMT

يخبرنا هذا بحجم مستند الاستجابة ونوعه، وهو مستند HTML في هذه الحالة حجمه 65585 بايت، كما يخبرنا متى كانت آخر مرة عُدِّل فيها.

يملك كل من العميل والخادم في أغلب الترويسات حرية إدراجها في الطلب أو الاستجابة، لكن بعض الترويسات يكون إدراجها إلزاميًا مثل ترويسة HOST التي تحدد اسم المضيف hostname، إذ يجب إدراجها في الطلب لأن الخادم قد يخدِّم عدة أسماء مضيفين على عنوان IP واحد، فبدون الترويسة لن يعرف أيّ واحد فيها يقصده العميل الذي يحاول التواصل معه، وقد تدرِج الطلبات أو الاستجابات سطرًا فارغًا بعد اسم المضيف متبوعًا بمتن body يحتوي على البيانات المرسلة، كما لا ترسل طلبات GET وDELETE أيّ بيانات، على عكس طلبات PUT وPOST، وبالمثل فقد لا تحتاج بعض أنواع الاستجابات إلى متن مثل استجابات الخطأ error responses.

المتصفحات وHTTP

رأينا في المثال السابق أنّ المتصفح سينشئ الطلب حين نكتب الرابط URL في شريط عنوانه، فإذا أشارت صفحة HTML الناتجة إلى ملفات أخرى مثل صور أو ملفات جافاسكربت، فسيجلب المتصفح هذه الملفات أيضًا، ومن المعتاد للمواقع متوسطة التعقيد إدراج من 10 إلى 200 مصدر مع الصفحة، كما سترسل المتصفحات عدة طلبات GET في الوقت نفسه بدلًا من انتظار الاستجابة الواحدة، ثم إرسال طلب آخر من أجل تحميل الصفحة بسرعة، وقد تحتوي صفحات HTML على استمارات forms تسمح للمستخدِم بملء بيانات وإرسالها إلى الخادم، وفيما يلي مثال عن استمارة:

<form method="GET" action="example/message.html">
  <p>Name: <input type="text" name="name"></p>
  <p>Message:<br><textarea name="message"></textarea></p>
  <p><button type="submit">Send</button></p>
</form>

تصف الشيفرة السابقة استمارةً لها حقلين أحدهما صغير يطلب الاسم والآخر أكبر لكتابة رسالة فيه، وتُرسَل الاستمارة عند الضغط على زر إرسال Send، أي يحزَّم محتوى حقلها في طلب HTTP وينتقل المتصفح إلى نتيجة ذلك الطلب.

تضاف المعلومات التي في الاستمارة إلى نهاية رابط action على أساس سلسلة استعلام نصية إذا كانت سمة العنصر method الخاص بالاستمارة <form> هي GET -أو إذا أُهملت-، وقد ينشئ المتصفح طلبًا إلى هذا الرابط:

GET /example/message.html?name=Jean&message=Yes%3F HTTP/1.1

تحدِّد علامة الاستفهام نهاية جزء المسار من الرابط وبداية الاستعلام، كما تُتبع بأزواج من الأسماء والقيم تتوافق مع سمة name في عناصر حقول الاستمارة ومحتوى تلك العناصر على الترتيب، ويُستخدَم محرف الإضافة ampersand أي & لفصل تلك الأزواج، في حين تكون الرسالة الفعلية المرمَّزة في الرابط هي "Yes?‎"، لكن ستُستبدَل شيفرة غريبة بعلامة الاستفهام، كما يجب تهريب بعض المحارف في سلاسل الاستعلامات النصية، فعلامة الاستفهام الممثلة بـ ‎%3F هي أحد تلك المحارف، وسنجد أن هناك شبه قاعدة غير مكتوبة تقول أنّ كل صيغة تحتاج إلى طريقتها الخاصة في تهريب المحارف، وهذه التي بين أيدينا تُسمى ترميز الروابط التشعبية URL encoding، حيث تستخدِم علامة النسبة المئوية ويليها رقمين ست-عشريين يرمِّزان شيفرة المحرف، وفي حالتنا تكون 3F -التي هي 63 في النظام العشري- شيفرة محرف علامة الاستفهام، وتوفِّر جافاسكربت الدالتين encodeURIComponent وdecodeURIComponent من أجل ترميز تلك الصيغة وفك ترميزها أيضًا.

console.log(encodeURIComponent("Yes?"));
// → Yes%3F
console.log(decodeURIComponent("Yes%3F"));
// → Yes?

إذا غيرنا السمة method لاستمارة HTML في المثال الذي رأيناه إلى POST، فسيستخدِم طلب HTTP الذي أنشئ لإرسال الاستمارة التابع POST ويضع سلسلة الاستعلام النصية في متن الطلب بدلًا من إضافتها إلى الرابط.

POST /example/message.html HTTP/1.1
Content-length: 24
Content-type: application/x-www-form-urlencoded

name=Jean&message=Yes%3F

يجب استخدام طلبات GET للطلبات التي تطلب معلومات فقط وليس لها تأثيرات جانبية؛ أما الطلبات التي تغيِّر شيئًا في الخادم مثل إنشاء حساب جديد أو نشر رسالة، فيجب التعبير عنها بتوابع أخرى مثل POST، كما تدرك برامج العميل مثل المتصفحات أنها يجب ألا تنشئ طلبات POST عشوائيًا وإنما تنشئ طلبات GET أولًا ضمنيًا لجلب المصدر التي تظن أنّ المستخدِم سيحتاجه قريبًا، كما سنعود إلى كيفية التفاعل مع الاستمارات من جافاسكربت لاحقًا في هذا المقال.

واجهة Fetch

تُسمى الواجهة التي تستطيع جافاسكربت الخاصة بالمتصفح إنشاء طلبات HTTP من خلالها باسم fetch، وبما أنها جديدة نسبيًا فستستخدم الوعود promises وهو الأمر النادر بالنسبة لواجهات المتصفحات.

fetch("example/data.txt").then(response => {
  console.log(response.status);
  // → 200
  console.log(response.headers.get("Content-Type"));
  // → text/plain
});

يعيد استدعاء fetch وعدًا يُحل إلى كائن Response حاملًا معلومات عن استجابة الخادم مثل شيفرة حالته وترويساته، وتغلَّف الترويسات في كائن شبيه بالخارطة ‎Map-like الذي يهمل حالة الأحرف في مفاتيحه -أي أسماء الترويسات- لأنه لا يفترض أن تكون أسماء الترويسات حساسةً لحالة الأحرف. هذا يعني أن كلا من الآتي:

  •  ‎headers.get("Content-Type")‎0.
  • headers.get("content-TYPE")‎.

سيُعيدان القيمة نفسها.

لاحظ أن الوعد الذي تعيده fetch يُحل بنجاح حتى لو استجاب الخادم برمز خطأ، وقد يُرفض إذا كان ثمة خطأ في الشبكة أو لم يوجد الخادم الذي أرسل إليه الطلب، كما يجب أن يكون أول وسيط لواجهة fetch هو الرابط الذي يراد طلبه، فإذا لم يبدأ الرابط باسم البروتوكول - http‎:‎ مثلًا- فسيعامَل نسبيًا، أي يفسَّر وفق المستند الحالي، وإذا بدأ بشرطة مائلة / فسيستبدل المسار الحالي الذي يكون جزء المسار الذي يلي اسم الخادم، أما إذا لم يبدأ بالشرطة المائلة، فسيوضع جزء المسار الحالي إلى آخر محرف شرطة مائلة -مع الشرطة نفسها- أمام الرابط النسبي.

يُستخدَم التابع text للحصول على المحتوى الفعلي للاستجابة، ويعيد هذا التابع وعدًا لأن الوعد الأولي يُحَل عند استقبال ترويسات الاستجابة، ولأنّ قراءة متن الاستجابة قد تستغرق وقتًا أطول.

fetch("example/data.txt")
  .then(resp => resp.text())
  .then(text => console.log(text));
// → This is the content of data.txt

يعيد التابع json -وهو تابع شبيه بالسابق- وعدًا يُحل إلى القيمة التي تحصل عليها حين تحلل المتن مثل JSON أو يُرفض إذا لم يكن JSON صالحًا، كما تستخدِم واجهة fetch التابع GET افتراضيًا لإنشاء طلبها ولا تدرِج متن الطلب، كما يمكنك إعدادها لغير ذلك بتمرير كائن له خيارات إضافية على أساس وسيط ثاني، فهذا الطلب مثلًا يحاول حذف example/data.txt:

fetch("example/data.txt", {method: "DELETE"}).then(resp => {
  console.log(resp.status);
  // → 405
});

يعني رمز الحالة 405 أنّ "الطلب غير مسموح به" وهو أسلوب خادم HTTP ليقول "لا أستطيع فعل هذا"، كما يمكن إضافة الخيار body لإضافة متن الطلب، كما يُستخدَم الخيار headers لضبط الترويسات، فهذا الطلب مثلًا يضمِّن الترويسة Range التي تخبر الخادم بإعادة جزء من الاستجابة فقط.

fetch("example/data.txt", {headers: {Range: "bytes=8-19"}})
  .then(resp => resp.text())
  .then(console.log);
// → المحتوى

سيضيف المتصفح بعض ترويسات الطلب تلقائيًا مثل Host وتلك المطلوبة كي يعرف الخادم حجم المتن، لكن ستكون إضافة ترويساتك الخاصة مفيدةً إذا أردنا إضافة أشياء مثل معلومات التوثيق، أو لنخبر الخادم بصيغة الملف التي نريد استقبالها.

صندوق اختبارات HTTP

لا شك أنّ إنشاء طلبات HTTP في سكربتات صفحة الويب سيرفع علامات استفهام حول الأمان، فالشخص الذي يتحكم بالسكربت قد لا تكون لديه دوافع الشخص نفسها التي يشغلها على حاسوبه، فإذا زرنا الموقع themafia.org مثلًا، فلا نريد لسكربتاته أن تكون قادرةً على إنشاء طلب إلى mybank.com باستخدام معلومات التعريف من متصفحنا مع تعليمات بتحويل جميع أموالنا إلى حساب عشوائي، لهذا تحمينا المتصفحات من خلال عدم السماح للسكربتات بإنشاء طلبات HTTP إلى نطاقات أخرى (أسماء نطاقات مثل themafia.org وmybank.com)، وتُعَدّ هذه مشكلةً مؤرقةً عند بناء أنظمة تريد الوصول إلى عدة نطاقات من أجل أسباب مشروعة ومنطقية، ولحسن الحظ تستطيع الخوادم إدراج ترويسة لهذا الغرض في استجابتها لإخبار المتصفح صراحةً أن هذا الطلب يمكن أن يأتي من نطاق آخر:

Access-Control-Allow-Origin: *

تقدير HTTP

هناك عدة طرق مختلفة لنمذجة التواصل بين برامج جافاسكربت العاملة في المتصفح -جانب العميل- والبرنامج الذي على الخادم -أي جانب الخادم-، وإحدى أكثر تلك الطرق استخدامًا هي استدعاءات الإجراءات البعيدة remote procedure calls، إذ يتبع التواصل في هذا النموذج أنماط استدعاءات الدوال العادية عدا أنّ الدالة تعمل فعليًا على حاسوب آخر، حيث يتطلب استدعاؤها إنشاء طلب إلى الخادم الذي يتضمن اسم الدالة والوسائط، كما تحتوي استجابة ذلك الطلب على القيمة المعادة.

عند التفكير في شأن استدعاءات الإجراء البعيد، لا يكون HTTP أكثر من أنه وسيلة تواصل، وستكتب على الأرجح طبقةً مجردةً تخفيه كليًا، كما يوجد هناك منظور آخر نبني فيه التواصل حول مفهوم الموارد وتوابع HTTP، فبدلًا من addUser المستدعى استدعاءًا بعيدًا، فإننا سنستخدم طلب PUT إلى ‎/users/larry، وبدلًا من ترميز خصائص ذلك المستخدِم في وسائط دالة، فإنك تعرِّف صيغة مستند JSON من أجل تمثيل المستخدِم أو تستخدم صيغةً موجودةً من قبل لذلك.

كما يُجلَب المورد بإنشاء طلب GET إلى رابط المورد -‎/users/larry مثلًا- والذي يُعيد المستند الممثل للمورد، إذ يسهل هذا المنظور استخدام بعض المزايا التي يوفرها HTTP مثل دعم تخزين الموارد -أي إنشاء نسخة مؤقتة عند العميل من أجل تسريع الوصول-، كما توفر المفاهيم المستخدَمة في HTTP مجموعة مبادئ مفيدة في تصميم واجهة الخوادم الخاصة بك بما أنها جيدة التصميم.

الأمان وHTTP

تمر البيانات المتنقلة عبر الانترنت في طرق محفوفة بالمخاطر، إذ تمر خلال أي طريق نتواجد فيه سواء كان نقطة اتصال في مقهى أو شبكات تتحكم بها شركات أو دول بغية الحصول على وجهتها، وقد تُعتَرض وتفتَّش في أي نقطة في طريقها وقد تُعدَّل أيضًا، وهنا لا يكفي بروتوكول HTTP العادي بما أنّ بعض البيانات سرية مثل كلمات مرور بريدك أو يجب عليها الوصول إلى وجهتها دون تعديل مثل رقم الحساب الذي تحوِّل المال إليه من خلال موقع البنك الذي تتعامل معه.

نستخدِم هنا بروتوكولًا أحدث هو HTTPS الذي نجده في الروابط التي تبدأ بـ https://‎، إذ يغلِّف حركة مرور HTTP بطريقة تصعب قراءتها والتعديل عليها، ويؤكد الطرف العميل قبل إرسال البيانات أنّ الخادم الذي يطلبها هو نفسه وليس منتحلًا له من خلال التأكد من شهادة مشفرة مصدَرة من جهة توثيق يعتمدها المتصفح، ثم تشفَّر جميع البيانات بطريقة تمنع استراق النظر إليها أو التعديل عليها، وعليه يمنع HTTPS أيّ جهة خارجية من انتحال الموقع الذي تريد التواصل معه ومن اختلاس النظر أو التجسس على تواصلكما، لكنه ليس مثاليًا بالطبع فقد وقعت عدة حوادث فشل فيها HTTPS بسبب شهادات مزورة أو مسروقة وبسبب برامج مخترَقة أو معطوبة، لكنه أكثر أمانًا من HTTP العادي.

حقول الاستمارات

صُمِّمت الاستمارات ابتداءً للويب قبل مجيء جافاسكربت من أجل السماح لمواقع الويب إرسال البيانات التي يدخلها المستخدِم في هيئة طلب HTTP، حيث يفترِض هذا التصميم أنّ التفاعل مع الخادم سيحدث دائمًا من خلال الانتقال إلى صفحة جديدة، غير أنّ عناصرها جزء من نموذج كائن مستند DOM مثل بقية الصفحة، كما تدعم عناصر DOM التي تمثِّل حقول الاستمارة عددًا من الخصائص والأحداث التي ليست موجودةً في العناصر الأخرى، حيث تمكننا من فحص حقول الإدخال تلك والتحكم فيها ببرامج جافاسكربت وأمور أخرى مثل إضافة وظيفة جديدة إلى استمارة أو استخدام استمارات وحقول على أساس وحدات بناء في تطبيق جافاسكربت.

تتكون استمارة الويب من عدد من حقول الإدخال تُجمع في وسم <form>، وتسمح HTML بعدة تنسيقات من الحقول بدايةً من أزرار الاختيار checkboxes إلى القوائم المنسدلة وحقول إدخال النصوص، ولن نناقش كل أنواع الحقول في هذا الكتاب لكن سنبدأ بنظرة عامة عليها.

تستخدِم أكثر أنواع الحقول وسم <input> وتُستخدَم السمة type الخاصة بهذا الوسم من أجل اختيار تنسيق الحقل، وفيما يلي أكثر أنواع <input> المستخدَمة:

أجل تمثيل المستخدِم أو تستخدم صيغةً موجودةً من قبل لذل

text حقل نصي ذو سطر واحد
password حقل نصي مثل text لكن يخفي النص الذي يُكتَب فيه
checkbox مفتاح تشغيل/إغلاق
radio جزء من حقل اختيار من متعدد
file يسمح للمستخدِم اختيار ملف من حاسوبه

يمكن وضع حقول الاستمارات في أي مكان في الصفحة ولا يشترط ظهورها في وسوم <form> وحدها، كما لا يمكن إرسال تلك الحقول التي تكون مستقلة بذاتها وخارج استمارة، فالاستمارات وحدها هي التي ترسَل، لكن على أيّ حال لا نحتاج إلى إرسال محتوى حقولنا بالطريقة التقليدية عند استخدام جافاسكربت في الاستجابة للمدخلات.

<p><input type="text" value="abc"> (text)</p>
<p><input type="password" value="abc"> (password)</p>
<p><input type="checkbox" checked> (checkbox)</p>
<p><input type="radio" value="A" name="choice">
   <input type="radio" value="B" name="choice" checked>
   <input type="radio" value="C" name="choice"> (radio)</p>
<p><input type="file"> (file)</p>

تختلف واجهة جافاسكربت لمثل تلك العناصر باختلاف نوع العنصر.

تمتلك الحقول النصية متعددة الأسطر وسمًا خاصًا بها هو <textarea>، وذلك بسبب غرابة استخدام سمة لتحديد قيمة ابتدائية لسطر متعدد، كما يجب إغلاق هذا الوسم بأسلوب الإغلاق المعتاد في HTML بإضافة ‎</textarea>‎، حيث يُستخدَم النص الموجود بين هذين الوسمين على أساس نص ابتدائي بدلًا من سمة value.

<textarea>
one
two
three
</textarea>

أخيرًا، يُستخدَم الوسم <select> لإنشاء حقل يسمح للمستخدِم بالاختيار من عدد من الخيارات المعرَّفة مسبقًا، ويُطلَق الحدث "change" كلما تغيرت قيمة حقل من حقول الاستمارة.

<select>
  <option>Pancakes</option>
  <option>Pudding</option>
  <option>Ice cream</option>
</select>

التركيز Focus

تستطيع حقول الاستمارات الحصول على نشاط لوحة المفاتيح عند النقر أو تفعيلها بأيّ شكل آخر على عكس أغلب عناصر مستندات HTML، بحيث تصبح هي العنصر المفعَّل الحالي ومستقبل إدخال لوحة المفاتيح، وعلى ذلك نستطيع الكتابة في الحقل النصي حين يكون مركَّزًا فقط؛ أما الحقول الأخرى فتختلف في استجابتها لأحداث لوحة المفاتيح، إذ تحاول قائمة <select> مثلًا الانتقال إلى الخيار الذي يحتوي النص الذي كتبه المستخدِم وتستجيب لمفاتيح الأسهم عبر تحريك اختيارها لأعلى وأسفل.

يمكننا التحكم في التركيز باستخدام جافاسكربت من خلال التابعَين focus وblur، حيث ينقل focus التركيز إلى عنصر DOM الذي استدعي عليه؛ أما الثاني فسيزيل التركيز منه، وتتوافق القيمة التي في document.activeElement مع العنصر المركَّز حاليًا.

<input type="text">
<script>
  document.querySelector("input").focus();
  console.log(document.activeElement.tagName);
  // → INPUT
  document.querySelector("input").blur();
  console.log(document.activeElement.tagName);
  // → BODY
</script>

يُتوقَّع من المستخدِم في بعض الصفحات أن يرغب في التفاعل مع أحد حقول الاستمارة فورًا، ويمكن استخدام جافاسكربت لتركيز ذلك الحقل عند تحميل المستند، لكن توفر HTML السمة autofocus أيضًاـ، والتي تعطينا التأثير نفسه وتخبر المتصفح بما نحاول فعله، وهذا يعطي المتصفح خيار تعطيل السلوك إذا كان غير مناسب كما في حالة محاولة المستخدِم تركيز حقل آخر أو عنصر آخر، وقد تعارفت المتصفحات على تمكين المستخدم من نقل التركيز خلال المستند بمجرد ضغط زر جدول أو tab على لوحة المفاتيح، ونستطيع هنا التحكم في الترتيب الذي تستقبل به العناصر ذلك التركيز باستخدام السمة tabindex، وسيجعل المثال التالي التركيز يقفز من المدخلات النصية إلى زر OK بدلًا من المرور على رابط المساعدة أولًا:

<input type="text" tabindex=1> <a href=".">(help)</a>
<button onclick="console.log('ok')" tabindex=2>OK</button>

السلوك الافتراضي لأغلب عناصر HTML أنها لا يمكن تركيزها، غير أنك تستطيع إضافة سمة tabindex إلى أي عنصر لجعله قابلًا للتركيز؛ أما إذا جعلنا قيمتها ‎-1 فسيتم تخطي العنصر حتى لو كان قابلًا للتركيز.

الحقول المعطلة

يمكن تعطيل أيّ حقل من حقول الاستمارات من خلال السمة disabled الخاصة بها، وهي سمة يمكن تحديدها دون قيمة، إذ يعطِّل وجودها الحقل مباشرةً، ولا يمكن تركيز الحقول المعطَّلة أو تغييرها، كما ستظهرها المتصفحات بلون رمادي وباهت.

<button>أنا مركَّز الآن</button>
<button disabled>خرجت!‏</button>

إذا عالج البرنامج إجراءً سببه زر أو تحكم آخر قد يحتاج إلى تواصل مع الخادم وسيستغرق وقتًا، فمن الأفضل تعطيل التحكم حتى انتهاء ذلك الإجراء، وهكذا لن يتكرر الإجراء إذا نفذ صبر المستخدِم ونقر عليه مرةً أخرى.

الاستمارات على أساس عنصر كامل

إذا احتوى الحقل على العنصر <form> فسيحتوي عنصر DOM الخاص به على الخاصية form التي تربطه إلى عنصر DOM الخاص بالاستمارة، ويحتوي العنصر <form> بدوره على خاصية تسمى elements تحوي تجميعةً شبيهةً بالمصفوفة من الحقول التي بداخلها. كذلك تحدِّد السمة name الموجودة في حقل الاستمارة الطريقة التي تعرَّف بها قيمتها عند إرسال الاستمارة، كما يمكن استخدامها على أساس اسم خاصية عند الوصول إلى الخاصية elements الخاصة بالاستمارة والتي تتصرف على أساس كائن شبيه بالمصفوفة -يمكن الوصول إليه بعدد-، وخريطة map -يمكن الوصول إليها باسم-.

<form action="example/submit.html">
  Name: <input type="text" name="name"><br>
  Password: <input type="password" name="password"><br>
  <button type="submit">Log in</button>
</form>
<script>
  let form = document.querySelector("form");
  console.log(form.elements[1].type);
  // → password
  console.log(form.elements.password.type);
  // → password
  console.log(form.elements.name.form == form);
  // → true
</script>

يرسل الزر الذي فيه سمة type الخاصة بـ submit الاستمارة عند الضغط عليه، كما سنحصل على التأثير نفسه عند الضغط على زر الإدخال Enter وإذا كان حقل الاستمارة مركَّزًا، ويعني إرسال الاستمارة غالبًا أنّ المتصفح ينتقل إلى الصفحة التي تحددها سمة action الخاصة بالاستمارة مستخدِمًا أحد الطلبَين GET أو POST، لكن يُطلَق الحدث "submit" قبل حدوث ذلك، وتستطيع معالجة هذا الحدث بجافاسكربت، ونمنع ذلك السلوك الافتراضي من خلال استدعاء preventDefault على كائن الحدث.

<form action="example/submit.html">
  Value: <input type="text" name="value">
  <button type="submit">Save</button>
</form>
<script>
  let form = document.querySelector("form");
  form.addEventListener("submit", event => {
    console.log("Saving value", form.elements.value.value);
    event.preventDefault();
  });
</script>

يملك اعتراض أحداث "submit" في جافاسكربت فوائدً عديدةً، حيث نستطيع كتابة شيفرة للتحقق من أنّ القيم التي أدخلها المستخدِم منطقية، ونخرِج رسائل خطأ له إذا وجدنا أخطاءً في تلك القيم بدلًا من إرسال الاستمارة، أو نستطيع تعطيل الطريقة المعتادة في إرسال الاستمارة بالكامل كما في المثال، ونجعل البرنامج يعالِج المدخلات باستخدام fetch لإرسالها إلى خادم دون إعادة تحميل الصفحة.

الحقول النصية

تشترك الحقول التي أنشئت بواسطة الوسوم <textarea> أو <input> مع النوع text وpassword واجهةً مشتركةً، كما تحتوي عناصر DOM الخاصة بها على الخاصية value التي تحمل محتواها الحالي على أساس قيمة نصية، وإذا عيّنا تلك الخاصية إلى سلسلة نصية أخرى فسيتغيَّر محتوى الحقل.

تعطينا كذلك الخصائص selectionStart وselectionEnd الخاصة بالحقول النصية معلومات عن المؤشر والجزء المحدَّد في النص، فإذا لم يكن ثمة نص محدَّد، فستحمل هاتان الخاصيتان العدد نفسه والذي يشير إلى موضع المؤشر، حيث يشير 0 مثلًا إلى بداية النص، ويشير 10 إلى أنّ المؤشر بعد المحرف العاشر؛ أما إذا حدِّد جزء من الحقل، فستختلف هاتين الخاصيتين لتعطينا بداية النص المحدَّد ونهايته، كما يمكن الكتابة فيهما مثل value تمامًا.

لنفترض أنك تكتب مقالةً عن الفرعون "خع سخموي Khasekhemwy" لكن لا نستطيع تهجئة اسمه، لذا تساعدنا هنا الشيفرة التالية التي توصل وسم <textarea> بمعالج حدث يُدخل النص Khasekhemwy لك إذا ضغطت على مفتاح F2.

<textarea></textarea>
<script>
  let textarea = document.querySelector("textarea");
  textarea.addEventListener("keydown", event => {
    // The key code for F2 happens to be 113
    if (event.keyCode == 113) {
      replaceSelection(textarea, "Khasekhemwy");
      event.preventDefault();
    }
  });
  function replaceSelection(field, word) {
    let from = field.selectionStart, to = field.selectionEnd;
    field.value = field.value.slice(0, from) + word +
                  field.value.slice(to);
    // ضع المؤشر بعد الكلمة
    field.selectionStart = from + word.length;
    field.selectionEnd = from + word.length;
  }
</script>

تستبدِل الدالة replaceSelection الكلمة الصعبة المعطاة والتي نريدها بالجزء المحدَّد حاليًا من محتوى الحقل النصي، ثم تنقل المؤشر بعد تلك الكلمة كي يستطيع المستخدِم متابعة الكتابة، ولا يُطلَق الحدث "change" للحقل النصي في كل مرة يُكتب شيء ما، بل عندما يزال التركز عن أحد الحقول بعد تغيُّر محتواه فقط، ويجب علينا تسجيل معالج للحدث "input" من أجل الاستجابة الفورية للتغيرات الحادثة في الحقل النصي، حيث يُطلَق في كل مرة يكتب المستخدِم فيها محرفًا أو يحذف نصًا أو يعدِّل في محتوى الحقل، ويظهر المثال التالي حقلًا نصيًا وعدّادًا يعرض الطول الحالي للنص في الحقل:

<input type="text"> length: <span id="length">0</span>
<script>
  let text = document.querySelector("input");
  let output = document.querySelector("#length");
  text.addEventListener("input", () => {
    output.textContent = text.value.length;
  });
</script>

أزرار الاختيار وأزرار الانتقاء

يُعَدّ حقل زر الانتقاء checkbox مخيّر فهو يخير بين اختياره من المجموعة أو لا ويمكن استخراج قيمته أو تغييرها من خلال الخاصية checked الخاصة به والتي تحمل قيمةً بوليانيةً.

<label>
  <input type="checkbox" id="purple"> Make this page purple
</label>
<script>
  let checkbox = document.querySelector("#purple");
  checkbox.addEventListener("change", () => {
    document.body.style.background =
      checkbox.checked ? "mediumpurple" : "";
  });
</script>

يربط وسم العنوان <label> جزءًا من المستند بحقل إدخال، فإذا نقرنا في أيّ مكان على العنوان فسنفعِّل الحقل ونغير قيمته إذا كان زر اختيار أو زر انتقاء.

يشبه زر انتقاء أو زر الانتقاء زر الاختيار لكنه يرتبط ضمنيًا بأزرار انتقاء أخرى لها سمة name نفسها كي يُنتقى واحد منها فقط، وقد سميت بهذا الاسم لأنها تشبه أزرار اختيار محطات الراديو في أجهزة المذياع في شكلها ووظيفتها، حيث إذا ضغطنا على أحد تلك الأزرار فإن بقية الأزرار تقفز إلى الخارج ولا تعمل سوى المحطة صاحبة الزر المنضغط.

Color:
<label>
  <input type="radio" name="color" value="orange"> Orange
</label>
<label>
  <input type="radio" name="color" value="lightgreen"> Green
</label>
<label>
  <input type="radio" name="color" value="lightblue"> Blue
</label>
<script>
  let buttons = document.querySelectorAll("[name=color]");
  for (let button of Array.from(buttons)) {
    button.addEventListener("change", () => {
      document.body.style.background = button.value;
    });
  }
</script>

تُستخدَم الأقواس المربعة في استعلام CSS المعطى إلى querySelectorAll لمطابقة السمات، وهي تختار العناصر التي تكون سمة name الخاصة بها هي "color".

حقول التحديد

تسمح حقول التحديد select fields للمستخدِم الاختيار من بين مجموعة خيارات كما في حالة أزرار الانتقاء، لكن مظهر الوسم <select> يختلف بحسب المتصفح على عكس أزرار الانتقاء التي نتحكم في مظهرها، كما تحتوي هذه الحقول على متغير يشبه قائمة أزرار الاختيار أكثر من أزرار الانتقاء، فإذا أعطينا وسم <select> السمة multiple، فسيسمح للمستخدِم اختيار أيّ عدد من الخيارات التي يريدها بدلًا من خيار واحد، وسيكون مظهر هذا مختلفًا باختلاف المتصفح، لكنه سيختلف عن حقل التحديد العادي الذي يكون تحكمًا منسدلًا drop-down control لا يعرض الخيارات إلا عند فتحه.

يحتوي كل وسم <option> على قيمة يمكن تعريفها باستخدام السمة value، وإذا لم تعطى تلك القيمة، فسيُعَدّ النص الذي بداخل الخيار هو قيمته، كما تعكس خاصية value الخاصة بالعنصر <select> الخيار المحدَّد حاليًا، لكن لن تكون هذه الخاصية ذات شأن في حالة الحقل multiple بما أنها ستعطي قيمة خيار واحد فقط من الخيارات المحدَّدة الحالية، ويمكن الوصول إلى وسوم <option> الخاصة بحقل <select> على أساس كائن شبيه بالمصفوفة من خلال خاصية الحقل options، كما يحتوي كل خيار على خاصية تسمى selected توضِّح هل الخيار محدَّد حاليًا أم لا، ويمكن كتابة الخاصية لتحديد خيار ما أو إلغاء تحديده.

يستخرِج المثال التالي القيم المحدَّدة من حقل التحديد multiple ويستخدِمها لتركيب عدد ثنائي من بِتّات منفصلة، لتحديد عدة خيارات اضغط باستمرار على زر control -أو command على ماك Mac-.

<select multiple>
  <option value="1">0001</option>
  <option value="2">0010</option>
  <option value="4">0100</option>
  <option value="8">1000</option>
</select> = <span id="output">0</span>
<script>
  let select = document.querySelector("select");
  let output = document.querySelector("#output");
  select.addEventListener("change", () => {
    let number = 0;
    for (let option of Array.from(select.options)) {
      if (option.selected) {
        number += Number(option.value);
      }
    }
    output.textContent = number;
  });
</script>

حقول الملفات

صُمِّمت حقول الملفات ابتداءً على أساس طريقة لرفع الملفات من حاسوب المستخدِم من خلال استمارة؛ أما في المتصفحات الحديثة، فهي توفر طريقةً لقراءة تلك الملفات، ولكن من خلال برامج جافاسكربت، إذ يتصرف الحقل على أساس حارس لبوابة، بحيث لا تستطيع السكربت البدء بقراءة ملفات خاصة بالمستخدِم من حاسوبه، لكن إذا اختار المستخدِم ملفًا في حقل كهذا، فسيفسِّر المتصفح ذلك الإجراء على أنه سماح للسكربت بقراءة الملف، ويبدو حقل الملف على أساس زر عليه عنوان مثل "اختر الملف" أو "تصفح الملف" مع معلومات عن الملف المختار تظهر إلى جانبه.

<input type="file">
<script>
  let input = document.querySelector("input");
  input.addEventListener("change", () => {
    if (input.files.length > 0) {
      let file = input.files[0];
      console.log("You chose", file.name);
      if (file.type) console.log("It has type", file.type);
    }
  });
</script>

تحتوي الخاصية files لعنصر حقل الملف على الملفات المختارة في الحقل، وهي كائن شبيه بالمصفوفة وليست مصفوفةً حقيقيةً، كما تكون فارغةً في البداية، والسبب في عدم وجود خاصية مستقلة باسم file، هو دعم الحقول لسمة multiple التي تجعل من الممكن تحديد عدة ملفات في الوقت نفسه، كما تحتوي الكائنات في كائن files على خاصيات مثل name لاسم الملف وsize لحجمه مقدَّرًا بالبايت -الذي هو وحدة قياس تخزينية تتكون من 8 بِتّات-، كما تحتوي على الخاصية type التي تمثِّل نوع وسائط media الملف التي قد تكون نصًا عاديًا text/plain أو صورةً image/jpeg، لكن ليس لتلك الكائنات خاصيةً يكون فيها محتوى الملف، وبما أنّ قراءة الملف من القرص ستستغرق وقتًا، فيجب أن تكون الواجهة غير تزامنية لتجنب تجميد أو تعليق المستند أثناء قراءته.

<input type="file" multiple>
<script>
  let input = document.querySelector("input");
  input.addEventListener("change", () => {
    for (let file of Array.from(input.files)) {
      let reader = new FileReader();
      reader.addEventListener("load", () => {
        console.log("File", file.name, "starts with",
                    reader.result.slice(0, 20));
      });
      reader.readAsText(file);
    }
  });
</script>

تتم عملية قراءة الملف من خلال إنشاء كائن FileReader الذي يسجِّل معالج الحدث "load" له، ويستدعي التابع readAsText الخاص به ليعطيه الملف الذي نريد قراءته، كما ستحتوي الخاصية result الخاصة بالقارئ على محتويات الملف بمجرد انتهاء التحميل، ويطلق الكائن FileReader أيضًا حدث خطأ "error" عند فشل قراءة الملف لأيّ سبب، إذ سيؤول كائن الخطأ نفسه في خاصية error الخاصة بالقارئ، ورغم تصميم تلك الواجهة قبل أن تصبح الوعود promises جزءًا من جافاسكربت، إلا أنك تستطيع تغليفها بوعد كما يلي:

function readFileText(file) {
  return new Promise((resolve, reject) => {
    let reader = new FileReader();
    reader.addEventListener(
      "load", () => resolve(reader.result));
    reader.addEventListener(
      "error", () => reject(reader.error));
    reader.readAsText(file);
  });
}

تخزين البيانات في جانب العميل

ستكون صفحات HTML البسيطة التي فيها قليل من جافاسكربت صيغةً رائعةً من أجل التطبيقات المصغَّرة، وهي برامج مساعِدة صغيرة تؤتمت مهامًا أساسية عبر توصيل بعض حقول الاستمارات بمعالِجات الأحداث، حيث يمكنك فعل أيّ شيء بدءًا من التحويل بين وحدات القياس المختلفة إلى حساب كلمات المرور من كلمة مرور رئيسية واسم موقع.

لكن لا نستطيع استخدام رابطات جافاسكربت إذا احتاج مثل ذلك التطبيق إلى تذكر أمر بين جلساته sessions، ذلك أنّ هذه الرابطات تُحذَف عند كل إغلاق للصفحة، غير أنه يمكن إعداد خادم وتوصيله بالانترنت وجعل التطبيق يخزن هناك، وسنرى كيفية فعل ذلك في مقال لاحق، لكن المقام يقصر هنا عن شرحه لتعقيده، وعلى أيّ حال، من المفيد أحيانًا إبقاء البيانات في المتصفح، كما يمكن استخدام الكائن localStorage لتخزين البيانات بطريقة تبقيها موجودةً حتى مع إعادة تحميل الصفحات، حيث يسمح لك ذلك الكائن بتصنيف قيم السلاسل النصية تحت أسماء.

localStorage.setItem("username", "marijn");
console.log(localStorage.getItem("username"));
// → marijn
localStorage.removeItem("username");

تبقى القيمة الموجودة في localStorage إلى أن يُكتَب فوقها، كما تُحذَف باستخدام removeItem أو بمحو المستخدِم لبياناته المحلية، وتحصل المواقع التي هي من نطاقات مختلفة على أقسام تخزين مختلفة، ويعني هذا نظريًا عدم إمكانية قراءة وتعديل البيانات المخزَّنة في localStorage من قِبل موقع ما إلا بسكربتات من نفس الموقع، كما تضع المتصفحات حدًا لحجم البيانات التي يمكن للمتصفح أن يخزِّنها في localStorage، وذلك لمنع تلك الخاصية من ملء أقراص المستخدِمين الصلبة ببيانات عديمة الفائدة واستخدام مساحات كبيرة بها، وتنفِّذ الشيفرة التالية تطبيقًا لكتابة الملاحظات، حيث يحتفظ بمجموعة من الملاحظات المسماة ويسمح للمستخدِم بتعديل الملاحظات وإنشاء الجديدا منها.

Notes: <select></select> <button>Add</button><br>
<textarea style="width: 100%"></textarea>

<script>
  let list = document.querySelector("select");
  let note = document.querySelector("textarea");

  let state;
  function setState(newState) {
    list.textContent = "";
    for (let name of Object.keys(newState.notes)) {
      let option = document.createElement("option");
      option.textContent = name;
      if (newState.selected == name) option.selected = true;
      list.appendChild(option);
    }
    note.value = newState.notes[newState.selected];

    localStorage.setItem("Notes", JSON.stringify(newState));
    state = newState;
  }
  setState(JSON.parse(localStorage.getItem("Notes")) || {
    notes: {"shopping list": "Carrots\nRaisins"},
    selected: "shopping list"
  });

  list.addEventListener("change", () => {
    setState({notes: state.notes, selected: list.value});
  });
  note.addEventListener("change", () => {
    setState({
      notes: Object.assign({}, state.notes,
                           {[state.selected]: note.value}),
      selected: state.selected
    });
  });
  document.querySelector("button")
    .addEventListener("click", () => {
      let name = prompt("Note name");
      if (name) setState({
        notes: Object.assign({}, state.notes, {[name]: ""}),
        selected: name
      });
    });
</script>

تحصل هذه السكربت على حالتها من القيمة "Notes" المخزَّنة في localStorage، وإذا لم تكن موجودة، فستنشئ حالة مثال وليس فيها إلا قائمة تسوق، ونحصل على القيمة nullعند محاولة قراءة حقل غير موجود من localStorage، كما أنّ تمرير null إلى JSON.parse سيجعله يحلل السلسلة النصية "null" ويُعيد null، وعلى ذلك يمكن استخدام العامِل || لتوفير قيمة افتراضية في مثل هذه المواقف؛ أما التابع setState فيتأكد أنّ DOM يُظهِر حالةً معطاةً ويخزِّن الحالة الجديدة في localStorage، كما تستدعي معالِجات الأحداث هذه الدالة للانتقال إلى حالة جديدة.

كان استخدام Object.assign في المثال السابق من أجل إنشاء كائن جديد يكون نسخة من state.notes القديم، لكن مع خاصية واحدة مضافة أو مكتوب فوقها، وتأخذ Object.assign وسيطها الأول وتضيف جميع الخاصيات من أيّ وسائط آخرين إليه، فإذا أعطيناها كائنًا فارغًا فسنجعلها تملأ كائنًا جديدًا، وتُستخدَم الأقواس المربعة في الوسيط الثالث لإنشاء خاصية اسمها يكون مبنيًا على قيمة ديناميكية، كما لدينا كائن آخر يشبه localStorage اسمه sessionStorage، والاختلاف بين الاثنين هو أنّ محتوى الأخير يُنسى بنهاية كل جلسة، حيث يحدث عند إغلاق المتصفح وذلك بالنسبة لأغلب المتصفحات.

خاتمة

ناقشنا في هذا المقال كيفية عمل بروتوكول HTTP، وقلنا أنّ العميل يرسل الطلب الذي يحتوي على تابع يكون GET في الغالب ومسار يحدِّد المصدر، ثم يقرر الخادم بعدها ماذا يفعل بذلك الطلب ويستجيب بشيفرة حالة ومتن استجابة، وقد يحتوي كل من الطلب والاستجابة على ترويسات توفر لنا معلومات إضافية، كما تُسمى الواجهة التي تستطيع جافاسكربت التي في المتصفح إنشاء طلبات HTTP منها باسم fetch، إذ يبدو إنشاء الطلب كما يلي:

fetch("/18_http.html").then(r => r.text()).then(text => {
  console.log(`The page starts with ${text.slice(0, 15)}`);
});

كما عرفنا من قبل، تنشئ المتصفحات طلبات GET لجلب الموارد المطلوبة لعرض صفحة ويب، وقد تحتوي الصفحة على استمارات تسمح للمستخدِم بإدخال المعلومات التي سترسلها بعدها في صورة طلب إلى الصفحة الجديدة عند إرسال الاستمارة، كما تستطيع HTML تمثيل عدة أنواع من حقول الاستمارات مثل الحقول النصية وأزرار الاختيار وحقول الاختيار من متعدد ومختارات الملفات file pickers، إذ يمكن فحص مثل تلك الحقول وتعديلها باستخدام جافاسكربت، وهي تطلق الحدث "change" عند تعديلها والحدث "input" عند كتابة نص فيها، كما تستقبل أحداث لوحة المفاتيح عند انتقال نشاط لوحة المفاتيح إليها.

عرفنا أيضًا أنّ الخاصيات مثل value -المستخدَمة في النصوص وحقول التحديد- أو checked -المستخدَمة في أزرار الاختيار وأزرار الانتقاء-، تُستخدَم من أجل قراءة أو تعيين محتوى الحقل، كما رأينا أن الحدث "submit" يُطلَق عند إرسال الاستمارة عليها، ويستطيع معالِج جافاسكربت استدعاء preventDefault على ذلك الحدث من أجل تعطيل السلوك الافتراضي للمتصفح، كما قد توجد عناصر حقول الاستمارات خارج وسم الاستمارة نفسه.

إذا اختار المستخدم ملفًا من نظام ملفاته المحلي في حقل مختار الملفات، فيمكن استخدام الواجهة FileReader من أجل الوصول إلى محتويات ذلك الملف من برنامج جافاسكربت، كما يُستخدم الكائنان localStorage وsessionStorage لحفظ المعلومات حتى مع إعادة التحميل، إذ يحتفظ الكائن الأول بالبيانات احتفاظًا دائمًا أو إلى أن يقرر المستخدِم محوها؛ أما الثاني فيحفظها إلى حين إغلاق المتصفح.

تدريبات

التفاوض على المحتوى

أحد الأمور التي يفعلها بروتوكول HTTP هو التفاوض على المحتوى content negotiation، حيث تُستخدَم ترويسة الطلب Accept لإخبار الخادم بنوع المستند الذي يريده العميل، وتتجاهل العديد من الخوادم هذه الترويسة، لكن إذا عرف الخادم عدة طرق لترميز أحد الموارد، فسينظر حينها في هذه الترويسة ويرسل نوع الملف الذي يريده العميل.

لقد هُيء الرابط https://eloquentjavascript.net/author ليستجيب للنصوص المجردة أو HTML أو JSON وفقًا لما يطلبه العميل، وتعرَّف تلك الصيغ بأنواع وسائط قياسية هي text/plain وtext/html وapplication/json.

أرسِل طلبات لجلب هذه الصيغ الثلاث من ذلك الرابط، واستخدم الخاصية headers في كائن الخيارات الممرَّر إلى fetch لضبط الترويسة Accept إلى نوع الوسائط media المفضل، ثم حاول طلب نوع الوسائط application/rainbows+unicorns، وانظر إلى شيفرة الحالة التي تنتجها.

تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen.

// شيفرتك هنا.

إرشادات الحل

  • ابن شيفرتك على أمثلة fetch الموجودة في المقال أعلاه.
  • إذا طلبت أنواع وسائط كاذبة فستحصل على استجابة بالرمز 406، بعنى "غير مقبول" أو Not acceptable، وهو الرمز الذي يجب أن يعيده الخادم إذا لم يستطع تحقيق الترويسة Accept.

طاولة عمل جافاسكربت

ابن واجهةً تسمح للناس بكتابة شيفرات جافاسكربت ويشغلونها، وضَع زرًا بجانب حقل <textarea>، بحيث إذا ضُغط عليه يمرِّر الباني Function الذي رأيناه في مقال الوحدات Modules في جافاسكريبت لتغليف النص في دالة واستدعائها، ثم حوِّل القيمة التي تعيدها الدالة أو أيّ خطأ ترفعه إلى سلسلة نصية واعرضها تحت الحقل النصي.

تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen.

<textarea id="code">return "hi";</textarea>
<button id="button">Run</button>
<pre id="output"></pre>

<script>
  // شيفرتك هنا.
</script>

إرشادات الحل

استخدم document.querySelector أو document.getElementById من أجل الوصول إلى العناصر المعرَّفة في HTML لديك، وبالنسبة للحدَثين "click" و"mousedown"، فسيستطيع معالِج الحدث الحصول على الخاصية value للحقل النصي واستدعاء Function عليها، وتأكد من تغليف كل من الاستدعاء إلى Function والاستدعاء إلى نتيجته في كتلة try كي تستطيع التقاط الاستثناءات التي ترفعها، كما أننا لا نعرف في حالتنا هذه نوع الاستثناء الذي لدينا، لذا يجب التقاط كل شيء.

يمكن استخدام الخاصية textContent الخاصة بعنصر الخرج لملئها برسالة نصية؛ أما في حالة الرغبة في الحفاظ على المحتوى القديم، فأنشئ عقدةً نصيةً جديدةً باستخدام document.createTextNode وألحقها بالعنصر، وتذكّر إضافة محرف سطر جديد إلى النهاية كي لا يظهر كل الخرج على سطر واحد.

لعبة حياة كونويل

تُعَدّ لعبة حياة كونويل Conway game of life محاكاةً بسيطةً تنشئ حياةً صناعيةً على شبكة، بحيث تكون كل خلية في تلك الشبكة إما حيةً أو ميتةً، وتطبق القواعد التالية في كل جيل -منعطف-:

  • تموت أيّ خلية حية لها أكثر من ثلاثة جيران أحياء أو أقل من اثنين.
  • تبقى أيّ خلية حية على قيد الحياة حتى الجيل التالي إذا كان لها جاران أحياء أو ثلاثة.
  • تعود أيّ خلية ميتة إلى الحياة إذا كان لها ثلاثة جيران أحياء حصرًا.

يُعرَّف الجار على أنه أيّ خلية مجاورة بما في ذلك الخلايا المجاورة له قطريًا. لاحظ أنّ تلك القواعد تطبَّق على كامل الشبكة مرةً واحدةً وليس على مربع واحد في المرة، وهذا يعني أنّ عدد الجيران مبني على الموقف الابتدائي، ولا تؤثر التغيرات الحادثة في الخلايا المجاورة في هذا الجيل على الحالة الجديدة للخلية.

نفِّذ هذه اللعبة باستخدام أيّ هيكل بيانات تجده مناسبًا، واستخدم Math.random لتوليد عشوائي لأماكن الخلايا في الشبكة في أول مرة، واعرضها على هيئة شبكة من حقول أزرار الاختيار مع زر بجانبها للانتقال إلى الجيل التالي، كما يجب عند حساب الجيل التالي إدراج التغييرات الحادثة عند اختيار المستخدم لزر الاختيار أو إلغاء الاختيار له.

تستطيع تعديل شيفرة التدريب لكتابة الحل وتشغيلها في طرفية المتصفح إن كنت تقرأ من متصفح، أو بنسخها إلى codepen.

<div id="grid"></div>
<button id="next">Next generation</button>

<script>
  // شيفرتك هنا.
</script>

إرشادات الحل

  • حاول النظر إلى حساب الجيل على أنه دالة محضة تأخذ شبكةً واحدةً وتنتج شبكةً جديدةً تمثِّل الدورة التالية.
  • يمكن تمثيل المصفوفة matrix بالطريقة المبينة في مقال الحياة السرية للكائنات في جافاسكريبت، حيث تَعد الجيران الأحياء بحلقتين تكراريتين متشعبتَين تكرران على إحداثيات متجاورة في كلا البعدين.
  • لا تَعد الخلايا التي تكون خارج الحقل وتجاهل الخلية التي تكون في المركز عند عَد خلايا مجاورة لها.
  • يمكن ضمان حدوث التغييرات على أزرار الاختيار في الجيل التالي بطريقتين؛ إما أن يلاحظ معالج حدث تلك التغييرات ويحدِّث الشبكة الحالية لتوافق ذلك، أو نولد شبكةً جديدةً من القيم التي في أزرار الاختيار قبل حساب الدورة التالية.
  • إذا اخترت أسلوب معالج الحدث فربما يجب عليك إلحاق سمات تعرِّف الموضع الموافق لكل زر اختيار كي يسهل معرفة الخلية التي يجب تغييرها؛ أما لرسم شبكة من أزرار الاختيار، فيمكن استخدام العنصر <table> أو وضعها جميعًا في العنصر نفسه ووضع عناصر <br> -أي فواصل الأسطر- بين الصفوف.

ترجمة -بتصرف- للفصل الثامن عشر من كتاب Elequent Javascript لصاحبه Marijn Haverbeke.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...