الفصل 18
HTTP والنماذج
ما كان يصعب على الناس فهمه غالباً في التصميم هو أنه لم يكن هناك شيء آخر يتجاوز URLs وHTTP وHTML. لم يكن هناك حاسوب مركزي «يتحكم» في الويب، ولا شبكة واحدة تعمل عليها هذه البروتوكولات، ولا حتى منظمة في أي مكان «تدير» الويب. لم يكن الويب «شيئاً» مادياً موجوداً في «مكان» معيّن. كان «فضاءً» يمكن أن توجد فيه المعلومات.
— تيم برنرز-لي

بروتوكول نقل النص التشعبي (HyperText Transfer Protocol)، الذي قدمناه في الفصل 13، هو الآلية التي تُطلب بها البيانات وتُقدَّم على الشبكة العنكبوتية العالمية. يصف هذا الفصل البروتوكول بمزيد من التفصيل ويشرح الطريقة التي تصل بها JavaScript في المتصفح إليه.
البروتوكول
إذا كتبت 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: 87320
Content-Type: text/html
Last-Modified: Fri, 13 Oct 2023 10:05:41 GMT
<!doctype html>
... the rest of the document
يأخذ المتصفح الجزء الذي يأتي بعد السطر الفارغ من الاستجابة، أي جسمها (body) — ولا يُخلط بينه وبين وسم HTML <body> — ويعرضه كمستند HTML.
تُسمى المعلومات التي يرسلها العميل الطلب. ويبدأ بهذا السطر:
GET /18_http.html HTTP/1.1
الكلمة الأولى هي طريقة الطلب. وتعني GET أننا نريد الحصول على المورد المحدد. ومن الطرائق الشائعة الأخرى DELETE لحذف مورد، وPUT لإنشائه أو استبداله، وPOST لإرسال معلومات إليه. لاحظ أن الخادم غير ملزم بتنفيذ كل طلب يصله. فإذا ذهبت إلى موقع عشوائي وطلبت منه DELETE صفحته الرئيسية، فسيرفض على الأرجح.
الجزء الذي يلي اسم الطريقة هو مسار المورد الذي يخصه الطلب. في أبسط الحالات، يكون المورد مجرد ملف على الخادم، لكن البروتوكول لا يشترط ذلك. فقد يكون المورد أي شيء يمكن نقله كما لو كان ملفاً. وكثير من الخوادم تولّد الاستجابات التي تنتجها في حينه. على سبيل المثال، إذا فتحت https://github.com/marijnh، يبحث الخادم في قاعدة بياناته عن مستخدم اسمه "marijnh"، وإن وجده، يولّد صفحة تعريف بذلك المستخدم.
بعد مسار المورد، يذكر السطر الأول من الطلب HTTP/1.1 للإشارة إلى إصدار بروتوكول HTTP المستخدم.
عملياً، تستخدم مواقع كثيرة الإصدار 2 من HTTP، وهو يدعم المفاهيم نفسها التي يدعمها الإصدار 1.1 لكنه أكثر تعقيداً بكثير كي يكون أسرع. وستتحول المتصفحات تلقائياً إلى إصدار البروتوكول المناسب عند التحدث إلى خادم معيّن، ونتيجة الطلب واحدة بصرف النظر عن الإصدار المستخدم. ولأن الإصدار 1.1 أبسط وأسهل للتجربة، سنستخدمه لشرح البروتوكول.
ستبدأ استجابة الخادم بإصدار أيضاً، يليه حالة الاستجابة، أولاً كرمز حالة من ثلاثة أرقام ثم كنص مقروء للبشر.
HTTP/1.1 200 OK
رموز الحالة التي تبدأ بالرقم 2 تدل على نجاح الطلب. أما الرموز التي تبدأ بالرقم 4 فتعني أن ثمة خطأ في الطلب. وأشهر رمز حالة HTTP هو على الأرجح 404، ويعني أنه تعذّر العثور على المورد. والرموز التي تبدأ بالرقم 5 تعني أن خطأ حدث على الخادم وأن الطلب ليس مسؤولاً عن ذلك.
قد يتبع السطر الأول من الطلب أو الاستجابة أي عدد من الترويسات (headers). وهي أسطر بالصيغة name: value تحدد معلومات إضافية عن الطلب أو الاستجابة. كانت هذه الترويسات جزءاً من الاستجابة في المثال:
Content-Length: 87320
Content-Type: text/html
Last-Modified: Fri, 13 Oct 2023 10:05:41 GMT
يخبرنا هذا بحجم مستند الاستجابة ونوعه. في هذه الحالة، إنه مستند HTML حجمه 87,320 بايت. ويخبرنا أيضاً بآخر مرة عُدّل فيها ذلك المستند.
للعميل والخادم الحرية في تقرير الترويسات التي يضمّنانها في طلباتهما أو استجاباتهما. لكن بعضها ضروري لكي تعمل الأمور. مثلاً، من دون ترويسة Content-Type في الاستجابة، لن يعرف المتصفح كيف يعرض المستند.
بعد الترويسات، قد يضم الطلب والاستجابة معاً سطراً فارغاً يليه جسم يحتوي المستند الفعلي المرسل. ولا ترسل طلبات GET وDELETE أي بيانات معها، بينما تفعل طلبات PUT وPOST. وبعض أنواع الاستجابات، كاستجابات الخطأ، لا تتطلب جسمًا أيضاً.
المتصفحات وHTTP
كما رأينا، يُجري المتصفح طلباً عندما ندخل URL في شريط عنوانه. وعندما تشير صفحة HTML الناتجة إلى ملفات أخرى، كالصور وملفات JavaScript، فسيسترجعها أيضاً.
يمكن لموقع معقد بدرجة متوسطة أن يضم بسهولة ما بين 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، يُرسَل النموذج (submitted)، أي يُحزَم محتوى حقوله في طلب HTTP وينتقل المتصفح إلى نتيجة ذلك الطلب.
عندما تكون خاصية method في عنصر <form> هي GET (أو غائبة)، تُضاف معلومات النموذج إلى نهاية URL الخاص بـaction على هيئة سلسلة استعلام (query string). وقد يُجري المتصفح طلباً إلى هذا URL:
GET /example/message.html?name=Jean&message=Yes%3F HTTP/1.1
تشير علامة الاستفهام إلى نهاية جزء المسار في URL وبداية الاستعلام. ويتبعها أزواج من الأسماء والقيم، تقابل خاصية name في عناصر حقول النموذج ومحتوى تلك العناصر على الترتيب. وتُستخدم علامة العطف (&) للفصل بين الأزواج.
الرسالة الفعلية المرمّزة في URL هي "Yes?" لكن علامة الاستفهام استُبدلت برمز غريب. فبعض المحارف في سلاسل الاستعلام يجب تخطّيها (escaped). وعلامة الاستفهام، الممثلة بـ%3F، من تلك المحارف. ويبدو أن هناك قاعدة غير مكتوبة تقول إن كل صيغة تحتاج طريقتها الخاصة في تخطّي المحارف. وهذه الصيغة، المسماة ترميز URL (URL encoding)، تستخدم علامة النسبة المئوية متبوعة برقمين ست عشريين (أساس 16) يرمّزان رمز المحرف. في هذه الحالة، 3F، وهو 63 في الترميز العشري، هو رمز محرف علامة الاستفهام. وتوفر JavaScript الدالتين encodeURIComponent وdecodeURIComponent لترميز هذه الصيغة وفك ترميزها.
console.log(encodeURIComponent("Yes?"));
// → Yes%3F
console.log(decodeURIComponent("Yes%3F"));
// → Yes?
إذا غيّرنا خاصية method في نموذج HTML الذي رأيناه سابقاً إلى POST، فسيستخدم طلب HTTP الذي يُرسَل به النموذج الطريقة POST ويضع سلسلة الاستعلام في جسم الطلب بدلاً من إضافتها إلى URL.
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 ضمنياً — للجلب المسبق لمورد يعتقد أن المستخدم سيحتاجه قريباً، مثلاً.
سنعود إلى النماذج وكيفية التفاعل معها من JavaScript لاحقاً في هذا الفصل.
الجلب (Fetch)
تسمى الواجهة التي تستطيع بها JavaScript في المتصفح إجراء طلبات HTTP بـfetch.
fetch("example/data.txt").then(response => {
console.log(response.status);
// → 200
console.log(response.headers.get("Content-Type"));
// → text/plain
});
يعيد استدعاء fetch وعداً يُحلّ إلى كائن Response يحمل معلومات عن استجابة الخادم، كرمز حالتها وترويساتها. والترويسات ملفوفة في كائن شبيه بـMap يتعامل مع مفاتيحه (أسماء الترويسات) بلا حساسية لحالة الأحرف، لأن أسماء الترويسات لا يُفترض أن تكون حساسة لحالة الأحرف. وهذا يعني أن headers.get("Content-Type") وheaders.get("content-TYPE") سيعيدان القيمة نفسها.
لاحظ أن الوعد الذي يعيده fetch يُحلّ بنجاح حتى لو استجاب الخادم برمز خطأ. ويمكن أيضاً رفضه إذا حدث خطأ في الشبكة أو إذا تعذّر العثور على الخادم الموجّه إليه الطلب.
المعطى الأول لـfetch هو URL المطلوب. وعندما لا يبدأ ذلك URL باسم بروتوكول (مثل http:)، يُعامل باعتباره نسبياً، أي يُفسَّر نسبةً إلى المستند الحالي. وعندما يبدأ بشرطة مائلة (/)، فإنه يستبدل المسار الحالي، وهو الجزء الذي يلي اسم الخادم. وعندما لا يبدأ بها، يُوضع الجزء من المسار الحالي حتى آخر شرطة مائلة فيه (شاملةً إياها) أمام URL النسبي.
للوصول إلى المحتوى الفعلي للاستجابة، يمكنك استخدام طريقتها 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 في قول "أخشى أنني لا أستطيع فعل ذلك".
لإضافة جسم طلب إلى طلب PUT أو POST، يمكنك تضمين خيار body. ولتعيين الترويسات، هناك خيار headers. مثلاً، يضم هذا الطلب ترويسة Range التي تطلب من الخادم إعادة جزء من المستند فقط.
fetch("example/data.txt", {headers: {Range: "bytes=8-19"}})
.then(resp => resp.text())
.then(console.log);
// → the content
سيضيف المتصفح تلقائياً بعض ترويسات الطلب، مثل "Host" وتلك التي يحتاجها الخادم لمعرفة حجم الجسم. لكن إضافة ترويساتك الخاصة مفيدة غالباً لتضمين أشياء مثل معلومات المصادقة أو إخبار الخادم بصيغة الملف التي تودّ استلامها.
عزل HTTP في صندوق الرمل
إجراء طلبات HTTP في سكربتات صفحات الويب يثير من جديد مخاوف تتعلق بالأمان. فالشخص الذي يتحكم في السكربت قد لا تكون له المصالح نفسها التي لمن يعمل على حاسوبه. وبشكل أكثر تحديداً، إذا زرت themafia.org، لا أريد أن تتمكن سكربتاته من إجراء طلب إلى mybank.com، باستخدام معلومات تعريفية من متصفحي، مع تعليمات بتحويل كل أموالي بعيداً.
لهذا السبب، يحمينا المتصفح بمنع السكربتات من إجراء طلبات HTTP إلى نطاقات أخرى (أسماء مثل themafia.org وmybank.com).
قد تكون هذه مشكلة مزعجة عند بناء أنظمة تريد الوصول إلى عدة نطاقات لأسباب مشروعة. لحسن الحظ، يمكن للخوادم تضمين ترويسة كهذه في استجابتها للإشارة صراحةً إلى المتصفح بأنه لا مانع من أن يأتي الطلب من نطاق آخر:
Access-Control-Allow-Origin: *
تقدير HTTP
عند بناء نظام يتطلب تواصلاً بين برنامج JavaScript يعمل في المتصفح (على جانب العميل) وبرنامج على خادم (على جانب الخادم)، توجد طرق مختلفة عدة لنمذجة هذا التواصل.
من النماذج الشائعة استدعاءات الإجراءات عن بعد (remote procedure calls). في هذا النموذج، يتبع التواصل أنماط استدعاءات الدوال العادية، إلا أن الدالة تعمل فعلياً على جهاز آخر. ويتضمن استدعاؤها إجراء طلب إلى الخادم يشمل اسم الدالة ومعطياتها. وتحتوي الاستجابة لذلك الطلب على القيمة المُرجَعة.
عند التفكير بمصطلحات استدعاءات الإجراءات عن بعد، يكون HTTP مجرد وسيلة للتواصل، وستكتب على الأرجح طبقة تجريد تخفيه كلياً.
وثمة نهج آخر هو بناء تواصلك حول مفهوم الموارد وطرائق HTTP. فبدلاً من إجراء عن بعد يُسمى addUser، تستخدم طلب PUT إلى /users/larry. وبدلاً من ترميز خصائص ذلك المستخدم في معطيات دالة، تعرّف صيغة مستند JSON (أو تستخدم صيغة موجودة) تمثل مستخدماً. ويكون جسم طلب PUT لإنشاء مورد جديد مستنداً كهذا. ويُجلب المورد بإجراء طلب GET إلى URL المورد (مثلاً /users/larry)، وهو يعيد بدوره المستند الذي يمثل المورد.
يسهّل هذا النهج الثاني استخدام بعض الميزات التي يوفرها HTTP، مثل دعم تخزين الموارد مؤقتاً (الاحتفاظ بنسخة من المورد على العميل للوصول السريع). والمفاهيم المستخدمة في HTTP، وهي مصممة تصميماً جيداً، يمكن أن توفر مجموعة مفيدة من المبادئ لتصميم واجهة خادمك حولها.
الأمان وHTTPS
تميل البيانات المسافرة عبر الإنترنت إلى سلوك طريق طويل خطير. فللوصول إلى وجهتها، عليها أن تتنقل عبر أي شيء من نقاط اتصال Wi-Fi في المقاهي إلى شبكات تسيطر عليها شركات ودول شتى. وفي أي نقطة على طول طريقها، قد تُفحَص أو حتى تُعدَّل.
إذا كان من المهم أن يبقى شيء ما سراً، ككلمة مرور حساب بريدك الإلكتروني، أو أن يصل إلى وجهته دون تعديل، كرقم الحساب الذي تحوّل إليه المال عبر موقع بنكك، فإن HTTP العادي ليس كافياً.
بروتوكول HTTP الآمن، المستخدم في عناوين URL التي تبدأ بـhttps://، يلفّ حركة HTTP بطريقة تجعل قراءتها والتلاعب بها أصعب. وقبل تبادل البيانات، يتحقق العميل من أن الخادم هو من يدّعي أنه هو بأن يطلب منه إثبات حيازته شهادة تشفير (cryptographic certificate) صادرة عن جهة إصدار شهادات يعرفها المتصفح. بعد ذلك، تُشفَّر كل البيانات المارة عبر الاتصال بطريقة ينبغي أن تمنع التنصت والتلاعب.
وهكذا، عندما يعمل على نحو صحيح، يمنع HTTPS الآخرين من انتحال هوية الموقع الذي تحاول التحدث إليه ومن التجسس على تواصلك. وهو ليس مثالياً، وقد وقعت حوادث شتى فشل فيها HTTPS بسبب شهادات مزوّرة أو مسروقة وبرمجيات معطوبة، لكنه أكثر أماناً بكثير من HTTP العادي.
حقول النموذج
صُممت النماذج أصلاً للويب قبل JavaScript لتتيح للمواقع إرسال المعلومات التي يقدّمها المستخدم في طلب HTTP. ويفترض هذا التصميم أن التفاعل مع الخادم يحدث دائماً بالانتقال إلى صفحة جديدة.
غير أن عناصر النموذج جزء من DOM، مثل بقية الصفحة، وعناصر DOM التي تمثل حقول النموذج تدعم عدداً من الخصائص والأحداث غير الموجودة في عناصر أخرى. وهذه تجعل من الممكن فحص حقول الإدخال هذه والتحكم فيها ببرامج JavaScript، وفعل أشياء مثل إضافة وظائف جديدة إلى نموذج أو استخدام النماذج والحقول لبناتٍ أساسية في تطبيق JavaScript.
يتكوّن نموذج الويب من أي عدد من حقول الإدخال مجمّعة في وسم <form>. وتتيح HTML أنماطاً مختلفة عدة من الحقول، تتراوح بين مربعات اختيار بسيطة تعمل بالتشغيل/الإيقاف وقوائم منسدلة وحقول لإدخال النصوص. لن يحاول هذا الكتاب مناقشة جميع أنواع الحقول على نحو شامل، لكننا سنبدأ بلمحة عامة موجزة.
تستخدم أنواع كثيرة من الحقول وسم <input>. وتُستخدم خاصية type في هذا الوسم لاختيار نمط الحقل. وهذه بعض أنواع <input> الشائعة الاستخدام:
text |
حقل نص من سطر واحد |
|---|---|
password |
مثل text لكنه يخفي النص المكتوب |
checkbox |
مفتاح تشغيل/إيقاف |
color |
لون |
date |
تاريخ تقويمي |
radio |
(جزء من) حقل اختيار من متعدد |
file |
يتيح للمستخدم اختيار ملف من حاسوبه |
لا يلزم بالضرورة أن تظهر حقول النموذج داخل وسم <form>. يمكنك وضعها في أي مكان في الصفحة. ومثل هذه الحقول الخالية من نموذج لا يمكن إرسالها (فالنموذج ككل وحده يمكن إرساله)، لكننا عندما نستجيب للإدخال بـJavaScript، لا نريد غالباً إرسال حقولنا بالطريقة العادية على أي حال.
<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="color" value="orange"> (color)</p>
<p><input type="date" value="2023-10-13"> (date)</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>
تختلف واجهة JavaScript لهذه العناصر باختلاف نوع العنصر.
لحقول النص متعددة الأسطر وسمها الخاص، <textarea>، لأن استخدام خاصية لتحديد قيمة ابتدائية متعددة الأسطر سيكون مربكاً على الأرجح. ويتطلب وسم <textarea> وسم إغلاق مطابقاً </textarea> ويستخدم النص بينهما، بدلاً من خاصية value، كنص ابتدائي.
<textarea>
one
two
three
</textarea>
وأخيراً، يُستخدم وسم <select> لإنشاء حقل يتيح للمستخدم الاختيار من عدد من الخيارات المعرّفة مسبقاً.
<select>
<option>Pancakes</option>
<option>Pudding</option>
<option>Ice cream</option>
</select>
كلما تغيّرت قيمة حقل نموذج، يُطلق حدث "change".
التركيز
خلافاً لمعظم العناصر في مستندات HTML، يمكن لحقول النموذج أن تحصل على تركيز لوحة المفاتيح (keyboard focus). فعند النقر عليها، أو الانتقال إليها بمفتاح tab، أو تنشيطها بطريقة أخرى، تصبح العنصر النشط حالياً والمتلقي لإدخال لوحة المفاتيح.
وبالتالي، لا يمكنك الكتابة في حقل نص إلا عندما يكون مركّزاً. وتستجيب الحقول الأخرى لأحداث لوحة المفاتيح على نحو مختلف. مثلاً، تحاول قائمة <select> الانتقال إلى الخيار الذي يحتوي النص الذي كتبه المستخدم، وتستجيب لمفاتيح الأسهم بتحريك اختيارها صعوداً ونزولاً.
يمكننا التحكم في التركيز من JavaScript بالطريقتين focus وblur. تنقل الأولى التركيز إلى عنصر 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>
في بعض الصفحات، يُتوقع أن يريد المستخدم التفاعل مع حقل نموذج فوراً. ويمكن استخدام JavaScript لتركيز هذا الحقل عند تحميل المستند، لكن HTML توفر أيضاً خاصية autofocus التي تحقق التأثير نفسه وتُعلِم المتصفح بما نحاول تحقيقه. وهذا يمنح المتصفح خيار تعطيل السلوك عندما لا يكون مناسباً، كما حين يكون المستخدم قد وضع التركيز على شيء آخر.
تتيح المتصفحات للمستخدم تحريك التركيز عبر المستند بالضغط على tab للانتقال إلى العنصر التالي القابل للتركيز، وshift-tab للرجوع إلى العنصر السابق. وافتراضياً، تُزار العناصر بالترتيب الذي تظهر به في المستند. ويمكن استخدام خاصية tabindex لتغيير هذا الترتيب. وسيتيح المستند المثال التالي للتركيز القفز من حقل النص إلى زر OK، بدلاً من المرور أولاً برابط المساعدة:
<input type="text" tabindex=1> <a href=".">(help)</a>
<button onclick="console.log('ok')" tabindex=2>OK</button>
افتراضياً، لا يمكن تركيز معظم أنواع عناصر HTML. ويمكنك إضافة خاصية tabindex إلى أي عنصر لجعله قابلاً للتركيز. وقيمة tabindex تساوي 0 تجعل العنصر قابلاً للتركيز من دون التأثير في ترتيب التركيز.
الحقول المعطّلة
يمكن تعطيل جميع حقول النموذج عبر خاصية disabled. وهي خاصية يمكن تحديدها بلا قيمة — فمجرد وجودها يعطّل العنصر.
<button>I'm all right</button>
<button disabled>I'm out</button>
لا يمكن تركيز الحقول المعطّلة أو تغييرها، وتجعلها المتصفحات تبدو رمادية وباهتة.
عندما يكون برنامج في طور معالجة إجراء ناتج عن زر ما أو عنصر تحكم آخر قد يتطلب تواصلاً مع الخادم وبالتالي يستغرق وقتاً، فقد تكون فكرة جيدة تعطيل عنصر التحكم حتى ينتهي الإجراء. وبهذه الطريقة، عندما ينفد صبر المستخدم وينقر عليه مرة أخرى، لن يكرر إجراؤه عن غير قصد.
النموذج ككل
عندما يكون حقل ما محتوى في عنصر <form>، سيكون لعنصر DOM الخاص به خاصية form تربطه رجوعاً بعنصر DOM الخاص بالنموذج. وعنصر <form> بدوره له خاصية تُسمى elements تحتوي مجموعة شبيهة بمصفوفة من الحقول داخله.
تحدد خاصية name في حقل النموذج الطريقة التي ستُعرَّف بها قيمته عند إرسال النموذج. ويمكن استخدامها أيضاً كاسم خاصية عند الوصول إلى خاصية elements في النموذج، وهي تعمل كائنًا شبيهًا بمصفوفة (يمكن الوصول إليه بالرقم) وخريطة (يمكن الوصول إليها بالاسم) في آن واحد.
<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". ويمكنك معالجة هذا الحدث بـJavaScript ومنع هذا السلوك الافتراضي باستدعاء preventDefault على كائن الحدث.
<form>
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" في JavaScript استخدامات شتى. يمكننا كتابة شيفرة للتحقق من أن القيم التي أدخلها المستخدم معقولة وإظهار رسالة خطأ فوراً بدلاً من إرسال النموذج. أو يمكننا تعطيل الطريقة العادية لإرسال النموذج كلياً، كما في المثال، وجعل برنامجنا يتولى الإدخال، ربما باستخدام fetch لإرساله إلى خادم من دون إعادة تحميل الصفحة.
حقول النص
تتشارك الحقول المنشأة بوسم <textarea>، أو بوسم <input> من النوع text أو password، واجهة مشتركة. فلعناصر DOM الخاصة بها خاصية value تحمل محتواها الحالي كقيمة نصية. وتعيين هذه الخاصية إلى نص آخر يغيّر محتوى الحقل.
تعطينا خاصيتا selectionStart وselectionEnd في حقول النص معلومات عن المؤشر والتحديد في النص. وعندما لا يكون هناك تحديد، تحمل الخاصيتان الرقم نفسه، مشيرتين إلى موضع المؤشر. مثلاً، 0 يشير إلى بداية النص، و10 يشير إلى أن المؤشر بعد المحرف العاشر. وعندما يكون جزء من الحقل محدداً، تختلف الخاصيتان، فتعطياننا بداية النص المحدد ونهايته. ومثل value، يمكن أيضاً الكتابة في هاتين الخاصيتين.
تخيل أنك تكتب مقالاً عن خع سخموي، آخر فراعنة الأسرة الثانية، لكنك تواجه بعض الصعوبة في كتابة اسمه. تربط الشيفرة التالية وسم <textarea> بمعالج حدث يُدرج لك النص "Khasekhemwy" عند الضغط على F2.
<textarea></textarea>
<script>
let textarea = document.querySelector("textarea");
textarea.addEventListener("keydown", event => {
if (event.key == "F2") {
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>
مربعات الاختيار وأزرار الراديو
حقل مربع الاختيار مفتاح تبديل ثنائي. ويمكن استخراج قيمته أو تغييرها عبر خاصيته 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
حقول select مشابهة مفهومياً لأزرار الراديو — فهي أيضاً تتيح للمستخدم الاختيار من مجموعة خيارات. لكن حيث يضع زر الراديو تخطيط الخيارات تحت سيطرتنا، يتحدد مظهر وسم <select> بالمتصفح.
لحقول select أيضاً صيغة أقرب إلى قائمة من مربعات الاختيار لا أزرار الراديو. فعند إعطاء وسم <select> خاصية multiple، سيتيح للمستخدم اختيار أي عدد من الخيارات، لا خياراً واحداً فقط. وبينما يُرسم حقل select العادي كعنصر تحكم منسدل لا يُظهر الخيارات غير النشطة إلا عند فتحه، يُظهر الحقل المفعّل فيه multiple عدة خيارات في الوقت نفسه، مما يتيح للمستخدم تفعيلها أو تعطيلها فرادى.
لكل وسم <option> قيمة. ويمكن تعريف هذه القيمة بخاصية value. وعندما لا تُعطى، يُحتسب النص داخل الخيار قيمته. وتعكس خاصية value في عنصر <select> الخيار المحدد حالياً. لكن في حقل multiple، لا تعني هذه الخاصية الكثير، لأنها ستعطي قيمة واحد فقط من الخيارات المحددة حالياً.
يمكن الوصول إلى وسوم <option> الخاصة بحقل <select> كائنًا شبيهًا بمصفوفة عبر خاصية options في الحقل. ولكل خيار خاصية تُسمى selected تشير إلى ما إذا كان ذلك الخيار محدداً حالياً. ويمكن أيضاً الكتابة في هذه الخاصية لتحديد خيار أو إلغاء تحديده.
يستخرج هذا المثال القيم المحددة من حقل select من نوع multiple ويستخدمها لتكوين عدد ثنائي من بتات فردية. اضغط باستمرار على ctrl (أو 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>
حقول الملفات
صُممت حقول الملفات أصلاً كطريقة لرفع ملفات من جهاز المستخدم عبر نموذج. وفي المتصفحات الحديثة، توفر أيضاً طريقة لقراءة هذه الملفات من برامج JavaScript. ويعمل الحقل كنوع من حارس البوابة. فلا يستطيع السكربت مجرد البدء بقراءة ملفات خاصة من حاسوب المستخدم، لكن إذا اختار المستخدم ملفاً في حقل كهذا، يفسّر المتصفح ذلك الإجراء بأنه إذن للسكربت بقراءة الملف.
يبدو حقل الملف عادةً كزر مكتوب عليه شيء مثل "choose file" أو "browse"، وبجانبه معلومات عن الملف المختار.
<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 (النوع الوسيطي للملف، مثل 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 في القارئ. صُممت هذه الواجهة قبل أن تصبح الوعود جزءاً من اللغة. ويمكنك لفّها في وعد كهذا:
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 البسيطة مع قليل من JavaScript أن تكون صيغة رائعة لـ«التطبيقات المصغرة» — برامج مساعدة صغيرة تؤتمت مهام أساسية. وبربط بعض حقول النموذج بمعالجات أحداث، يمكنك فعل أي شيء من التحويل بين السنتيمترات والبوصات إلى حساب كلمات المرور من كلمة مرور رئيسية واسم موقع.
عندما يحتاج تطبيق كهذا إلى تذكّر شيء بين الجلسات، لا يمكنك استخدام ارتباطات JavaScript — فهي تُطرح بعيداً كل مرة تُغلق فيها الصفحة. ويمكنك إعداد خادم ووصله بالإنترنت وجعل تطبيقك يخزّن شيئاً هناك (سنرى كيف نفعل ذلك في الفصل 20). لكن ذلك عمل إضافي وتعقيد كبيران. وأحياناً يكفي مجرد إبقاء البيانات في المتصفح.
يمكن استخدام كائن 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", () => {
let {selected} = state;
setState({
notes: {...state.notes, [selected]: note.value},
selected
});
});
document.querySelector("button")
.addEventListener("click", () => {
let name = prompt("Note name");
if (name) setState({
notes: {...state.notes, [name]: ""},
selected: name
});
});
</script>
تحصل الشيفرة على حالتها الابتدائية من القيمة "Notes" المخزنة في localStorage أو، إن كانت غائبة، تنشئ حالة مثال لا تحتوي سوى قائمة تسوق. وقراءة حقل غير موجود من localStorage ستُنتج null. وتمرير null إلى JSON.parse سيجعله يحلل النص "null" ويعيد null. وبالتالي يمكن استخدام المعامل ?? لتوفير قيمة افتراضية في حالة كهذه.
تتأكد طريقة setState من أن DOM يعرض حالة معطاة وتخزّن الحالة الجديدة في localStorage. وتستدعي معالجات الأحداث هذه الدالة للانتقال إلى حالة جديدة.
تُستخدم صيغة ... في المثال لإنشاء كائن جديد نسخة من state.notes القديم، لكن مع خاصية واحدة مضافة أو مستبدلة. وهي تستخدم صيغة النشر (spread syntax) لتضيف أولاً الخصائص من الكائن القديم ثم تعيّن خاصية جديدة. وتُستخدم صيغة القوسين المعقوفين في الكائن الحرفي لإنشاء خاصية يستند اسمها إلى قيمة ديناميكية ما.
يوجد كائن آخر مشابه لـlocalStorage يُسمى sessionStorage. والفرق بينهما أن محتوى sessionStorage يُنسى في نهاية كل جلسة، وهو ما يعني في معظم المتصفحات كل مرة يُغلق فيها المتصفح.
الملخص
ناقشنا في هذا الفصل كيفية عمل بروتوكول HTTP. يرسل العميل طلباً يحتوي طريقة (عادةً GET) ومساراً يحدد مورداً. ثم يقرر الخادم ما يفعل بالطلب ويستجيب برمز حالة وجسم استجابة. وقد يحتوي الطلب والاستجابة معاً ترويسات توفر معلومات إضافية.
تسمى الواجهة التي تستطيع بها JavaScript في المتصفح إجراء طلبات HTTP بـfetch. ويبدو إجراء طلب كهذا:
fetch("/18_http.html").then(r => r.text()).then(text => {
console.log(`The page starts with ${text.slice(0, 15)}`);
});
تُجري المتصفحات طلبات GET لجلب الموارد اللازمة لعرض صفحة ويب. وقد تحتوي الصفحة أيضاً نماذج تتيح إرسال المعلومات التي يدخلها المستخدم كطلب لصفحة جديدة عند إرسال النموذج.
تستطيع HTML تمثيل أنواع شتى من حقول النموذج، كحقول النص ومربعات الاختيار وحقول الاختيار من متعدد ومنتقيات الملفات. ويمكن فحص هذه الحقول والتلاعب بها بـJavaScript. وهي تُطلق حدث "change" عند تغيّرها، وتُطلق حدث "input" عند كتابة نص، وتتلقى أحداث لوحة المفاتيح عندما يكون لها تركيز لوحة المفاتيح. وتُستخدم خصائص مثل value (لحقوق النص وselect) أو checked (لمربعات الاختيار وأزرار الراديو) لقراءة محتوى الحقل أو تعيينه.
عند إرسال نموذج، يُطلق عليه حدث "submit". ويمكن لمعالج JavaScript استدعاء preventDefault على ذلك الحدث لتعطيل سلوك المتصفح الافتراضي. وقد تظهر عناصر حقول النموذج أيضاً خارج وسم form.
عندما يختار المستخدم ملفاً من نظام ملفاته المحلي في حقل منتقي ملفات، يمكن استخدام واجهة FileReader للوصول إلى محتوى هذا الملف من برنامج JavaScript.
يمكن استخدام كائنَي localStorage وsessionStorage لحفظ المعلومات بطريقة تنجو من إعادة تحميل الصفحات. ويحفظ الكائن الأول البيانات إلى الأبد (أو حتى يقرر المستخدم مسحها)، ويحفظها الثاني حتى يُغلق المتصفح.
التمارين
التفاوض على المحتوى
من الأشياء التي يستطيع HTTP فعلها ما يُسمى التفاوض على المحتوى (content negotiation). وتُستخدم ترويسة الطلب Accept لإخبار الخادم بنوع المستند الذي يودّ العميل الحصول عليه. وتتجاهل خوادم كثيرة هذه الترويسة، لكن عندما يعرف الخادم طرقاً شتى لترميز مورد ما، يمكنه النظر في هذه الترويسة وإرسال ما يفضّله العميل.
عُدّ URL https://eloquentjavascript.net/author ليستجيب بنص عادي أو HTML أو JSON، حسب ما يطلبه العميل. وتُعرَّف هذه الصيغ بـالأنواع الوسيطية (media types) المعيارية text/plain وtext/html وapplication/json.
أرسل طلبات لجلب الصيغ الثلاث كلها لهذا المورد. استخدم خاصية headers في كائن الخيارات الممرّر إلى fetch لتعيين الترويسة المسماة Accept إلى النوع الوسيطي المطلوب.
وأخيراً، جرّب طلب النوع الوسيطي application/rainbows+unicorns وانظر رمز الحالة الذي ينتج عن ذلك.
// اكتب شيفرتك هنا.
إظهار التلميح
ابنِ شيفرتك على أمثلة fetch السابقة في هذا الفصل.
طلب نوع وسيطي وهمي سيعيد استجابة برمز 406، "Not acceptable" (غير مقبول)، وهو الرمز الذي ينبغي أن يعيده الخادم عندما لا يستطيع تلبية ترويسة Accept.
ورشة عمل JavaScript
ابنِ واجهة تتيح للمستخدمين كتابة قطع من شيفرة JavaScript وتشغيلها.
ضع زراً بجانب حقل <textarea> يستخدم، عند ضغطه، بانية Function التي رأيناها في الفصل 10 للفّ النص في دالة واستدعائها. حوّل القيمة المُرجَعة من الدالة، أو أي خطأ ترميه، إلى نص واعرضه أسفل حقل النص.
<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 وألحقها بالعنصر. تذكّر إضافة محرف سطر جديد في النهاية كي لا يظهر كل الإخراج في سطر واحد.
لعبة الحياة لكونواي
لعبة الحياة لكونواي محاكاة بسيطة تنشئ «حياة» اصطناعية على شبكة، كل خلية فيها إما حية وإما لا. وفي كل جيل (دور)، تُطبَّق القواعد التالية:
- تموت أي خلية حية لها أقل من جارين حيين أو أكثر من ثلاثة.
- تبقى أي خلية حية لها جاران أو ثلاثة جيران أحياء على قيد الحياة إلى الجيل التالي.
- تصبح أي خلية ميتة لها ثلاثة جيران أحياء بالضبط خلية حية.
يُعرَّف الجار بأنه أي خلية مجاورة، بما في ذلك المجاورة قطرياً.
لاحظ أن هذه القواعد تُطبَّق على الشبكة كلها دفعة واحدة، لا مربعاً واحداً في كل مرة. وهذا يعني أن عدّ الجيران يستند إلى الوضع في بداية الجيل، ولا ينبغي أن تؤثر التغيرات التي تطرأ على الخلايا المجاورة خلال هذا الجيل في الحالة الجديدة لخلية معينة.
نفّذ هذه اللعبة بأي بنية بيانات تجدها مناسبة. استخدم Math.random لملء الشبكة بنمط عشوائي ابتدائياً. اعرضها كشبكة من حقول مربعات الاختيار، مع زر بجانبها للانتقال إلى الجيل التالي. وعندما يحدد المستخدم مربعات الاختيار أو يلغي تحديدها، ينبغي تضمين تغييراته عند حساب الجيل التالي.
<div id="grid"></div>
<button id="next">Next generation</button>
<script>
// اكتب شيفرتك هنا.
</script>
إظهار التلميح
لحل مشكلة حدوث التغييرات في الوقت نفسه مفهومياً، حاول أن ترى حساب الجيل كدالة نقية تأخذ شبكة واحدة وتنتج شبكة جديدة تمثل الدور التالي.
يمكن تمثيل المصفوفة بمصفوفة واحدة من width × height عنصراً، تخزّن القيم صفاً صفاً، فمثلاً العنصر الثالث في الصف الخامس يُخزَّن (باستخدام فهرسة تبدأ من الصفر) في الموضع 4 × width + 2. ويمكنك عدّ الجيران الأحياء بحلقتين متداخلتين، تمرّان على الإحداثيات المجاورة في البعدين. واحرص على ألا تحسب خلايا خارج الشبكة وأن تتجاهل الخلية في المركز التي نحسب جيرانها.
يمكن ضمان سريان تغييرات مربعات الاختيار على الجيل التالي بطريقتين. فقد يلاحظ معالج حدث هذه التغييرات ويحدّث الشبكة الحالية لتعكسها، أو يمكنك توليد شبكة جديدة من القيم في مربعات الاختيار قبل حساب الدور التالي.
إن اخترت المضي بمعالجات الأحداث، فقد تريد إرفاق خصائص تحدد الموضع الذي يقابله كل مربع اختيار بحيث يسهل معرفة الخلية الواجب تغييرها.
لرسم شبكة مربعات الاختيار، يمكنك إما استخدام عنصر <table> (انظر الفصل 14) أو ببساطة وضعها كلها في العنصر نفسه ووضع عناصر <br> (فاصل أسطر) بين الصفوف.