فصل 18HTTP و فرم‌ها

ارتباط باید مستقل از وضعیت باشد [...] این‌گونه که هر درخواست از سمت سرویس‌گیرنده به سرویس‌دهنده باید شامل تمامی اطلاعات مورد نیاز برای درک درخواست باشد، و نباید بتوان از زمینه‌ای در ارتباط که روی سرویس‌دهنده از پیش ذخیره شده است، استفاده نمود.

روی فیلدینگ, سبک‌های معماری و طراحی معمار‌ی‌های نرم‌افزار مبتنی بر شبکه
Picture of a web form on a medieval scroll

پروتوکل انتقال ابرمتن (http) که پیش از این در فصل 13 آمد، مکانیزمی است که از طریق آن داده‌ها در شبکه‌ی گسترده‌ی وب، درخواست و فراهم می شوند. این فصل به توصیف این پروتوکل با جزئیات بیشتری می پردازد و چگونگی دسترسی جاوااسکریپت در مرورگر را به آن شرح می‌دهد.

پروتوکل

اگر آدرس eloquentjavascript.net/18_http.html را در نوار آدرس مرورگرتان تایپ کنید، مرورگر ابتدا به دنبال آدرس سرویس‌دهنده‌ی (server) مرتبط با دامنه‌ی 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

مرورگر بخشی از پاسخ را که بعد از خط خالی می آید را می‌گیرد، بخش بدنه یا body (نباید با برچسب <body> در HTML اشتباه گرفته شود)، و آن را به عنوان یک سند HTML نشان می‌دهد.

اطلاعاتی که توسط کاربر ارسال می شود را درخواست (request) می نامند. یک درخواست با این خط شروع می شود:

GET /18_http.html HTTP/1.1

اولین کلمه معرف روش یا متد (method) درخواست است. GET به این معناست که ما قصد گرفتن منبع مشخص شده را داریم. دیگر متدهای رایج شامل DELETE برای حذف یک منبع، PUT برای جایگزین کردن آن، و POST برای ارسال اطلاعات به آن می شوند. توجه داشته باشید که سرویس دهنده مجبور نیست هر درخواستی را که دریافت می کند اجرا کند. اگر به یک وب سایت تصادفی درخواستی از نوع DELETE برای حذف صفحه‌ی اصلی آن ارسال نمایید، احتمالا رد خواهد شد.

قسمت بعد از نام متد، معرف مسیر منبعی است که درخواست برای آن ارسال می شود. در ساده ترین شکل، یک منبع، فایلی روی سرویس‌دهنده می‌باشد اما اجباری برای آن در پروتوکل وجود ندارد. یک منبع می تواند هرچیزی که بتوان آن را انتقال داد باشد همان طور که یک فایل اینگونه می‌باشد. خیلی از سرویس‌دهنده‌ها پاسخ‌های تولیدی را به صورت پویا و درحین اجرا ایجاد می کنند. به عنوان مثال، اگر آدرس https://github.com/marijnh را باز کنید، سرویس دهنده در بانک اطلاعاتیش به دنبال کاربری با نام “marijnh” می گردد و اگر پیدا کرد، یک صفحه‌ی پروفایل برای این کاربر ایجاد و برمی گرداند.

بعد از مسیر منبع، اولین خط از درخواست شامل 1.1/HTTP می‌باشد که بیانگر نسخه‌ی پروتوکل HTTP در حال استفاده می‌باشد.

در عمل، خیلی از سایت‌ها از HTTP نسخه 2 استفاده می کنند که از همان مفاهیم نسخه 1.1 پشتبانی می کند اما بسیار پیش‌رفته‌تر است که در نتیجه سریع تر عمل می کند. مرورگرها در هنگام ارتباط با سرویس‌دهنده‌ی داده شده، به صورت خودکار به نسخه‌ی پروتوکل مناسب رجوع می کنند و نتیجه‌ی درخواست فارغ از نسخه‌ی پروتوکل یکسان است. به دلیل اینکه نسخه‌ی 1.1 سرراست تر و آسان تر است، ما به همان می پردازیم.

پاسخ سرویس‌دهنده با یک شماره‌ی نسخه شروع می شود که بعد از آن وضعیت پاسخ درج می شود. ابتدا یک عدد سه رقمی وضعیت و سپس یک رشته‌ی متنی قابل خواندن برای انسان برگردانده می‌شود.

HTTP/1.1 200 OK

کدهای وضعیتی که با عدد 2 شروع می شوند بیانگر موفقیت درخواست ارسال شده می باشند. کدهایی که با 4 شروع می شوند به این معنا هستند که در درخواست مشکلی وجود داشته است. 404 احتمالا معروف ترین کد وضعیت HTTP می‌باشد – به معنای این که منبع درخواستی پیدا نشد. کدهایی که با 5 شروع می شوند به معنای وجود خطا در سرویس دهنده می باشند و اشکال در درخواست نیست.

اولین خط درخواست یا پاسخ ممکن است شامل تعدادی header یا سرپیام باشد. این خطوط به شکل name: value هستند و اطلاعات بیشتری درباره‌ی درخواست یا پاسخ فراهم می نمایند. سرپیام‌های زیر بخشی از پاسخ مثال بودند:

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

این پیام اندازه و نوع سند پاسخ را مشخص می کند. در این مثال، این پاسخ، سندی HTML به اندازه‌ی 65585 بایت می‌باشد. همچنین زمان آخرین تغییر ایجاد شده در سند نیز آمده است.

استفاده از بیشتر این سرپیام‌ها برای سرویس‌دهنده و سرویس‌گیرنده اختیاری است. اما تعداد کمی از آن ها اجباری می باشند. به عنوان مثال، سرپیام Host که بیانگر نام میزبان (hostname) است باید در درخواست وجود داشته باشد چرا که یک سرویس دهنده ممکن است به چندین نام میزبان رو یک آدرس IP سرویس‌دهی کند، که در صورت مشخص نبودن آن سرپیام، سرویس‌دهنده نمی تواند تشخیص دهد که درخواست سرویس‌گیرنده مربوط به کدام نام میزبان می‌باشد.

بعد از قسمت سرپیام‌ها، در هر دوی درخواست و پاسخ، یک خط خالی قرار می گیرد که بعد از آن بدنه خواهد آمد که حاوی داده‌هایی است که ارسال می شوند. در درخواست‌های GET و DELETE هیچ دادهای ارسال نمی شود اما در PUT و POST این گونه نیست. به طور مشابه، بعضی از انواع پاسخ، مثل پاسخ‌های خطا، دارای قسمت بدنه نمی باشند.

مرورگرها و HTTP

همانطور که در مثال دیدیم، یک مرورگر وقتی که ما یک URL را در نوار آدرسش وارد می کنیم، یک درخواست می سازد. زمانی که صفحه‌ی HTML دریافتی، به فایل‌های دیگری ارجاع دارد مثل تصاویر و فایل‌های جاوااسکریپت ، آن فایل‌ها نیز بازیابی می شوند.

یک وب‌سایت نسبتا بزرگ به آسانی می تواند دارای چیزی بین 10 تا 200 منبع باشد. برای اینکه بتوان این منابع را به سرعت بارگیری کرد، مرورگر چندین درخواست GET را همزمان ارسال می کند به جای اینکه برای تک تک پاسخ‌ها منتظر بماند.

صفحات HTML ممکن است حاوی فرم‌ها باشند که این امکان را به کاربر می دهند تا اطلاعاتی را در آن ها نوشته و به سرویس‌دهنده ارسال کنند. اینجا مثالی از یک فرم را مشاهده می کنید:

<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 کلیک می کنید، فرم ثبت می شود (submit) به این معنا که محتوای فیلدهای آن به صورت یک درخواست 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” است اما علامت سوال با یک کد رمزی جایگزین شده است. بعضی از کاراکترهای موجود در رشته‌های پرس و جو باید گریز داده شوند. علامت سوال که به صورت %3F در آمده است یکی از آن ها است. به نظر یک قانون نانوشته وجود دارد که هر فرمتی روش گریزدهی کاراکتر مخصوص به خود را لازم دارد. در این مورد، که کدگذاری URL نامیده می شود (URL Encoding) از یک علامت درصد و دو رقم هگزادسیمال (مبنای 16) برای کدگذاری کد کاراکتر استفاده می شود. در این مورد، 3F که در سیستم دهدهی معادل 63 می‌باشد، کد کاراکتر علامت سوال است. جاوااسکریپت دو تابع 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 ارسال شوند. یک نرم افزار سمت کاربر (client-side) مثل مرورگر می داند که نباید به صورت کورکورانه درخواست‌های POST ارسال می کند اما به صورت ضمنی درخواست‌های GET ایجاد کند – به عنوان مثال برای پیش‌واکشی یک منبع که کاربر به زودی به آن نیاز خواهد داشت.

به فرم‌ها و نحوه‌ی تعامل با آن ها به وسیله‌ی جاوااسکریپت در همین فصل باز خواهیم گشت.

Fetch

رابطی‌ را که از طریق آن، جاوااسکریپت مرورگر می تواند درخواست‌های HTTP بسازد، fetch می نامند. به دلیل اینکه این رابط نسبتا جدید است، به خوبی از promise ها استفاده می کند ( که در رابط های مرورگر چیز نادری است).

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

فراخوانی fetch یک promise را برمی گرداند که به یک شیء Response منجر می شود که حاوی اطلاعاتی درباره‌ی پاسخ سرویس‌دهنده است، مثل کد وضعیت آن و سرپیام‌هایش. سرپیام‌ها درون یک شیء شبیه Map قرار می گیرند که نسبت به بزرگی و کوچکی حروف کلیدهایش (نام سرپیام‌ها) حساس نیست چرا که نام سرپیام ها لزومی ندارد به حروف حساس باشد. به این معنا که headers.get("Content-Type") و headers.get("content-TYPE") مقدار مشابهی را برمی گردانند.

توجه داشته باشید که promise برگشتی از fetch به صورت موفقیت‌آمیز حل‌وفصل می‌شود حتی اگر سرویس دهنده با کد خطا پاسخ داده باشد. همچنین ممکن است لغو شود، اگر خطایی در شبکه رخ بدهد یا سرویس‌دهنده‌ای که درخواست به آن ارسال می شود پیدا نشود.

اولین آرگومانی که fetch دریافت می کند URLای است که مورد درخواست است. زمانی که این URL با نام یک پروتوکل شروع نمی شود (مثل http:)، به صورت نسبی در نظر گرفته می شود، به این معنا که آدرس نسبت به موقعیت سند فعلی تفسیر می شود. اگر این URL با یک کاراکتر اسلش (/) شروع شود، جایگزین مسیر فعلی خواهد شد منظور بخشی است که پس از نام سرویس‌دهنده در URL می‌آید. در صورت نبود اسلش در ابتدا، مسیر فعلی تا آخرین کاراکتر اسلش، پیش از URL نسبی داده شده قرار خواهد گرفت.

برای دریافت محتوای اصلی یک پاسخ، می توانید از متد text آن استفاده کنید. به دلیل اینکه promise اولیه، به محض اینکه سرپیام‌های پاسخ دریافت شوند، حل و فصل می شود، و خواندن بدنه‌ی پاسخ ممکن است زمان بیشتری بطلبد، دوباره به شکل یک promise برگردانده می شود.

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

متد مشابهی به نام json وجود دارد که promiseای برمی گرداند که زمانی موفق به تولید مقدار می شود که بدنه را بتوان به عنوان JSON تفسیر کرد، و اگر محتوای بدنه یک JSON معتبر نباشد، promise لغو می شود.

به صورت پیش‌فرض، 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);
// → the content

مرورگر به صورت خودکار چند سرپیام به درخواست اضافه می کند مثل “Host” و آن دسته از سرپیام‌ها که برای سرویس‌دهنده لازم هستند تا بتواند اندازه‌ی بدنه را بداند. اما اضافه کردن سرپیام‌های خودتان معمولا برای افزودن چیزهایی مثل اطلاعات هویت‌سنجی یا اعلام نوع فرمت فایل درخواستی به سرویس‌دهنده، استفاده می شود.

سازوکار حفاظتی - HTTP sandboxing

ساختن درخواست‌های HTTP درون اسکریپت‌های صفحه‌ی وب، نگرانی‌هایی در مورد امنیت به وجود می‌آورد. فردی که اسکریپت را کنترل می کند ممکن است نیتی مشابه کسی که اسکریپت در کامپیوترش اجرا می شود، نداشته باشد. به صورت واضح‌تر، اگر من از سایت themafia.org بازدید کنم دوست ندارم که اسکریپت‌های این سایت بتوانند درخواستی به سایت بانک من (mybank.com) ارسال کنند و از اطلاعات شناسایی من در مرورگرم استفاده کنند و دستوراتی برای انتقال پول‌های من به حساب‌های ناشناس اجرا کنند.

به همین دلیل، مرورگرها با غیرفعال کردن اجازه‌ی ارسال درخواست HTTP از درون اسکریپت‌ها به دیگر دامنه‌ها( نام هایی مثل themefia.org یا mybank.com) از ما محافظت می کنند.

ممکن است این محدودیت در زمان ساختن سیستم‌هایی که لازم است برای اهداف قانونی به چند دامنه دسترسی داشته باشد آزار دهنده باشد. خوشبختانه، سرویس‌دهنده‌ها می توانند یک سرپیام شبیه مثال زیر را در پاسخ خودشان قرار دهند تا صراحتا به مرورگر اعلام کنند که مشکلی نیست که درخواست از دامنه‌ی دیگری بیاید.

Access-Control-Allow-Origin: *

بهره‌مندی از HTTP

در زمان ساخت سیستمی که لازم است در آن بین یک برنامه‌ی جاوااسکریپت که در مرورگر اجرا می شود (سمت کاربر) و برنامه‌ای که روی سرویس‌دهنده است (سمت سرور) ارتباط برقرار شود، می توان از روش‌های متفاوتی برای مدلسازی این ارتباط استفاده کرد.

یکی از مدل‌های رایج، استفاده از فراخوانی‌های رویه از راه دور (remote procedure calls) است. دراین مدل، ارتباط از الگوی فراخوانی نرمال توابع پیروی می کند با این استثناء که تابع در واقع روی کامپیوتر دیگر اجرا می شود. فراخوانی آن شامل ساخت یک درخواست به سرویس‌دهنده و مشخص کردن نام تابع و آرگومان‌های آن می‌باشد. پاسخ به آن درخواست حاوی مقدار بازگشتی است.

در مدل فراخوانی از راه دور، HTTP فقط نقش یک وسیله ارتباطی را بازی می کند و شما به احتمال زیاد آن را با یک لایه‌ی تجرید مخفی می کنید.

روشی دیگر ساختن سیستم ارتباط برپایه مفهوم منابع و متد‌های HTTP است. به جای اینکه یک رویه‌ی از راه دور به نام addUser فراخوانی شود، از درخواست PUT به /users/larry استفاده می کنید. به جای قرار دادن مشخصات آن کاربر در آرگومان‌های تابع، یک فرمت سند JSON تعریف می کنید ( یا از فرمتی موجود استفاده می کنید) که نمایانگر یک کاربر باشد. بدنه‌ی درخواست PUT برای ایجاد یک منبع جدید، سندی به این شکل خواهد بود. یک منبع را می توان با ساختن یک درخواست GET به URL منبع (به عنوان مثال /users/larry) واکشی کرد که دوباره سندی که نمایانگر منبع مورد نظر است را بر‌می‌گرداند.

استفاده از روش دوم باعث می شود بتوان از بعضی امکاناتی که HTTP فراهم می کند آسان تر استفاده کرد مثل پشتیبانی از کش کردن منابع (نگه داری یک کپی روی سیستم کاربر برای دسترسی سریع). مفاهیم استفاده شده در HTTP که خوبی طراحی شده اند، می توانند مجموعه‌ی مفیدی از قواعد را برای طراحی رابط سرویس‌دهنده فراهم نمایند.

امنیت و HTTPS

سفر داده‌ها در اینترنت معمولا در مسیری بلند و پرمخاطره رخ می دهد. برای اینکه داده‌ها به مقصدشان برسند، باید از چیزهای مختلفی مثل Wifi یک کافی شاپ تا شبکه‌هایی که توسط شرکت‌های متنوع و دولت‌ها کنترل می شوند عبور کنند. در هر نقطه‌ای در طول مسیرش می توانند مورد بازرسی یا حتی دستکاری قرار بگیرند.

بعضی اطلاعات باید مخفی بمانند مانند رمز عبور حساب ایمیلتان، یا اطلاعاتی هستند که باید هنگامی که به مقصد می‌رسند دستکاری نشده باشند مانند شماره‌ی حسابی که در سایت بانکتان به آن پول واریز می کنید. HTTP ساده زیاد مناسب این این موارد نیست.

پروتوکل امن HTTP که URLها در آن با https:// شروع می شوند، ترافیک HTTP را به شکلی محافظت می کند که ساختن و دستکاری آن خیلی سخت بشود. قبل از تبادل داده‌ها ، سرویس‌گیرنده تحقیق می کند که سرویس‌دهنده همانی باشد که ادعا می کند و این کار با درخواست از سرور برای فراهم ساختن گواهینامه‌ی رمزی که توسط یک مرجع معتبر صدور گواهینامه صادر شده است و مورد شناسایی مرورگر می‌باشد، انجام می شود. بعد تمامی داده‌هایی که از طریق این ارتباط جابجا می شوند به رمز در می آیند به شکلی که از استراق سمع و مداخله ایمن بمانند.

بنابراین، وقتی این پروتوکل به درستی کار می کند، HTTPS از جعل وب سایت مورد ارتباط توسط شخص ثالث و جاسوسی در ارتباطات شما جلوگیری می‌کند. این پروتوکل کامل و بی نقص نیست، و موارد متنوعی وجود داشته که HTTPS موفقیت آمیز نبوده‌است؛ به دلیل لو رفتن گواهینامه یا دزدیده شدن آن یا نرم‌افزار معیوب، اما به هر حال این پروتوکل بسیار از HTTP ساده امن تر می‌باشد.

فیلدهای فرم

فرم ها اساسا برای وب قبل از جاوااسکریپت طراحی شده بوند، تا به وب‌سایت‌ها امکان ارسال اطلاعات ثبت شده توسط کاربر را به صورت یک درخواست HTTP بدهند. این طراحی فرض را بر این می گذارد که تعامل با سرویس‌دهنده همیشه با انتقال به یک صفحه‌ی دیگر رخ می دهد.

اما عناصر فرم هم شبیه به دیگر قسمت‌های صفحه، بخشی از DOM محسوب می شوند و عناصر DOM که فیلدهای فرم را نشان می دهند دارای تعدادی خاصیت و رخداد هستند که در دیگر عناصر موجود نیست. این باعث می شود که بتوان با برنامه‌های جاوااسکریپت این گونه فیلدها را کنترل و رسیدگی کرد و بتوان کارهایی مثل اضافه کردن کارکرد‌های جدید به یک فرم یا استفاده از فرم‌ها و فیلدها به عنوان بلاک‌های سازنده در اپلیکیشن‌های جاوااسکریپت استفاده کرد.

یک فرم وب شامل هر تعداد از فیلدهای ورودی که توسط برچسب <form> محصور می شوند است. HTML امکان داشتن سبک‌های متفاوت و متعددی از فیلدها را فراهم می سازد، از چک‌باکس‌های on/off تا منوهای بازشدنی و فیلدهایی برای ورودی متنی. این کتاب به صورت جامع در مورد همه‌ی انواع فیلدها صحبت نمی کند، اما به صورت کلی نگاهی به آن ها می اندازیم.

خیلی از انواع فیلدها از برچسب <input> استفاده می کنند. خصوصیت type این برچسب برای انتخاب سبک فیلد استفاده می شود. اینجا بعضی از انواع <input> رایج آورده شده است:

textیک فیلد یک خطی برای دریافت متن
passwordفیلدی شبیه به text با این تفاوت که متن تایپ‌شده قابل شناسایی نیست
checkboxفیلدی دارای دو حالت فعال و غیرفعال
radioبخشی از یک فیلد چند گزینه‌ای
fileاین فیلد به کاربر اجازه‌ی انتخاب یک فایل از کامپیوترش را می دهد

فیلدهای فرم نیازی نیست حتما درون یک برچسب <form> قرار گیرند. می توانید آن ها در هر جای صفحه استفاده کنید. استفاده بدون برچسب 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>، بیشتر به دلیل اینکه قرار دادن یک مقدار چند خطی در قسمت خصوصیت‌ها (attribute) مناسب نیست. برچسب <textarea> نیاز به برچسب پایانی </textarea> دارد و از متن بین این دو برچسب به جای خاصیت value استفاده می کند.

<textarea>
one
two
three
</textarea>

و در نهایت، برچسب <select> برای ایجاد فیلدی که امکان انتخاب از بین تعدادی گزینه‌ی از پیش تعریف شده را به کاربر می دهد استفاده می شود.

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

هر زمان که مقدار یک فیلد فرم تغییر کند، یک رخداد "change" ایجاد می شود.

Focus

برخلاف بیشتر عناصر موجود در اسناد HTML، فیلدهای فرم را می توان با صفحه‌ی کلید فعال (focus) کرد. زمانی که با کلیک موس یا روشی دیگر این عناصر فعال می شوند در دسترس ورودی صفحه‌ی کلید قرار می گیرند.

بنابراین زمانی می توانید درون یک فیلد متنی چیزی تایپ کنید که در مرورگر فعال (مورد تمرکز) باشد. دیگر فیلدها به رخدادهای صفحه‌کلید به شکلی متفاوت پاسخ می دهند. به عنوان مثال در <select> با تایپ متن توسط کاربر این فیلد تلاش می کند تا گزینه‌ای که محتوای تایپ شده را دارد انتخاب شود و با کلیدهای جهت دار می توان گزینه‌ی انتخابی را تغییر داد.

می توانیم فعال بودن یک فیلد را با جاوااسکریپت به وسیله‌ی متدهای focus و blur کنترل کنیم. اولین متد عنصری که روی آن فراخوانی شده را فعال می کند و دومی از حالت فعال آن را خارج می کند. مقداری که در 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، به ترتیب این فعال‌سازی اثر بگذاریم . در مثال پیش رو، در سند مشخص می شود که حالت فعال فیلد بعد از فیلد متنی به جای لینک help، به دکمه‌ی OK منتقل شود.

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

به صورت پیش‌فرض، بیشتر انواع عناصر HTML نمی توانند فعال شوند (focus). اما می توانید از tabindex در همه‌ی عناصر استفاده کنید که باعث می شود بتوان به آن ها قابلیت فعال بودن داد. قرار دادن -1 برای tabindex باعث می شود که از آن عنصر صرف نظر شود حتی اگر به صورت نرمال امکان فعال بودن داشته باشد.

فیلد‌های خاموش (disabled)

می توان تمامی فیلدهای فرم را به وسیله‌ی خاصیت disabled خاموش کرد. این خاصیت را می توان بدون مشخص کردن مقدار به کار برد- همین که وجود داشته باشد باعث خاموش شدن فیلد می شود.

<button>I'm all right</button>
<button disabled>I'm out</button>

فیلدهای خاموش را نمی توان تغییر داد یا مورد تمرکز (focus) قرار داد. همچنین مرورگرها ظاهر آن ها خاکستری و کم رنگ می کنند.

زمانی که یک برنامه در حال رسیدگی به کاری است که از فشردن یک دکمه یا یک فیلد کنترلی دیگر ایجاد شده، و این‌کار نیازمند ارتباط با سرویس‌دهنده است که در نتیجه نیاز به کمی زمان دارد، بهتر است تا پایان انجام آن کار، فیلد مورد نظر را خاموش کنیم. با این کار ، اگر کاربر صبر نکند و دوباره روی دکمه کلیک کند، به صورت ناخواسته آن کار تکرار نمی شود.

فرم به عنوان یک کل

زمانی که یک فیلد درون عنصر <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 به سرویس‌دهنده ارسال کنیم.

فیلدهای متنی

فیلدهایی که به وسیله‌ی برچسب‌های <input> ایجاد می شوند و نوع text یا password را دارند به همراه برچسب‌های <textarea>، از رابط یکسانی استفاده می کنند. عناصر متناظرشان در DOM دارای خاصیتی به نام value می‌باشد که محتوای کنونی آن ها را به شکل رشته‌ای نگهداری می کند. تغییر این خاصیت و استفاده از رشته‌ای دیگر برای آن محتوای فیلد را تغییر می دهد.

دو خاصیت selectionStart و selectionEnd مربوط به فیلدهای متنی اطلاعاتی در رابطه با مکان‌نما و ناحیه‌ی انتخاب شده در متن فراهم می سازند. زمانی که چیزی انتخاب نشده است، این دو خاصیت مقدار عددی یکسانی را نگهداری می کنند که موقعیت مکان نما می باشد. به عنوان مثال، 0 به معنای شروع متن و 10 بیانگر این است که مکان‌نما بعد از کاراکتر دهم همین کاراکتر قرار دارد. زمانی که بخشی از فیلد انتخاب شده باشد، این دو خاصیت مقدار متفاوتی خواهند داشت و بیانگر شروع و پایان متن انتخاب شده می باشند. مثل value این خاصیت‌ها را نیز می توان تغییر داد.

فرض کنید که در حال نوشتن مقاله‌ای در مورد Khasekhemwy (پادشاهی در مصر باستان) می باشید و در نوشتن نام او دچار مشکل هستید. در کد پیش رو یک برچسب <textarea> و یک گرداننده‌ی رخداد با هم ترکیب می شوند و با فشردن کلید F2 کلمه‌ی “Khasekhemwy” برای شما در فیلد وارد می شود.

<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);
    // Put the cursor after the word
    field.selectionStart = from + word.length;
    field.selectionEnd = from + word.length;
  }
</script>

تابع replaceSelection بخش انتخاب شده در فیلد متنی را با کلمه‌ی داده شده جایگزین می نماید و بعد مکان‌نما را به بعد از آن کلمه منتقل می کند تا کاربر بتواند به تایپ خود ادامه دهد.

رخداد "change" برای یک فیلد متنی با هر بار نوشتن چیزی ایجاد نمی شود. بلکه زمانی ایجاد می شود که فیلد مذکور بعد از اینکه محتوایش تغییر کرد از حالت focus خارج می شود. برای اینکه بتوان به سرعت به تغییرات در یک فیلد متنی پاسخ داد، باید یک گرداننده برای رخداد "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> یک بخش از سند را به یک فیلد ورودی مرتبط می‌سازد. با کلیک روی 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 ارسال می شوند برای انتخاب و نشانه‌گرفتن خصوصیت‌های عناصر (attributes)، استفاده می شوند. این پرس و جو عناصری را نتخاب می کند که مقدار خاصیت name شان برابر با “color” باشد.

فیلدهای select (انتخاب گزینه)

فیلدهای select به طور مفهومی مشابه دکمه‌های رادیویی هستند- این فیلدها هم به کاربر، امکان انتخاب از یک مجموعه گزینه‌ها را می دهند. در حالیکه در یک دکمه‌ی رادیویی چیدمان گزینه‌ها تحت کنترل ما می‌باشد، ظاهر یک برچسب <select> توسط مرورگر تعیین می شود.

فیلدهای select همچنین در یک حالت خاص، بیشتر شبیه لیستی از checkboxها می‌باشد تا دکمه‌های رادیویی. اگر خاصیت multiple را به یک برچسب <select> اضافه کنیم، کاربر این امکان را خواهد داشت که به جای یک گزینه بتواند چندین گزینه انتخاب نماید. این شکل از فیلد در بیشتر مرورگرها به صورت متفاوتی نسبت به حالت نرمال یک گزینه‌ای نشان داده می شود. در حالت عادی، این فیلد به صورت یک منوی کشویی (بازشونده) نمایش داده می شود که گزینه‌هایش زمانی دیده می‌شوند که آن را باز نمایید.

هر برچسب <option> دارای یک مقدار می‌باشد. این مقدار را می توان با خاصیت value تعریف کرد. اگر این خاصیت را مشخص نکنید، متن درون گزینه به عنوان مقدار در نظر گرفته می‌شود. خاصیت value متعلق به عنصر <select> نشان‌دهنده‌ی گزینه‌ی انتخاب شده می‌باشد. برای یک فیلد چند‌گزینه‌ای (multiple) این خاصیت معنای خاصی نخواهد داشت چرا که فقط به یک مقدار از مقادیر انتخاب شده اشاره می کند.

برچسب‌های <option> متعلق به فیلد <select> را می توان از طریق یک شیء آرایه‌گونه و به وسیله‌ی خاصیت options فیلد مورد دسترسی قرار داد. هر گزینه دارای یک خاصیت به نام selected می‌باشد که بیانگر این است که آیا انتخاب شده است یا خیر. این خاصیت را همچنین می توان برای انتخاب کردن یا از انتخاب خارج نمودن یک گزینه‌ نیز استفاده کرد.

این مثال مقادیر انتخاب شده را از یک فیلد انتخاب چند‌گزینه‌ای (multiple) استخراج می کند و از آن ها برای نوشتن یک عدد دودویی از بیت‌های جداگانه استفاده می کند. برای انتخاب چندین گزینه کلید control (یا command در مک) را نگه دارید.

<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>

فیلدهای انتخاب فایل

فیلدهای فایل در ابتدا به عنوان راهی برای بارگذاری فایل‌ها از کامپیوتر کاربر به وسیله‌ی یک فرم طراحی شدند. در مرورگرهای مدرن، همچنین این فیلدها راهی برای خواندن این فایل‌ها به وسیله‌ی برنامه‌های جاوااسکریپت فراهم می‌کنند. فیلد انتخاب فایل به عنوان نوعی دربان عمل می کند؛ اسکریپت نمی تواند فایل‌های خصوصی را از کامپیوتر کاربر بخواند، اما اگر کاربر فایلی را به وسیله‌ی این فیلد انتخاب کند، تفسیر مرورگر این خواهد بود که اسکریپت ممکن است فایل انتخاب شده را بخواند.

ظاهر یک فیلد فایل معمولا شبیه به یک دکمه که عنوانی شبیه به “choose a 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 نیست این است که خصوصیتی(attribute) در فیلد فایل وجود دارد به نام multiple که باعث می شود بتوان چندین فایل را به صورت همزمان انتخاب کرد.

اشیائی که در شیء files قرار دارند دارای خاصیت‌هایی مثل name (نام فایل)، size (اندازه فایل به بایت که معادل 8 بیت است) و type (نوع فایل مثل text/plain یا image/jpeg) می باشند.

چیزی که در آن وجود ندارد خاصیتی است که محتوای فایل را نگهداری کند. بدست آوردن محتوای فایل کمی پیچیده تر است. به دلیل اینکه خواندن فایل از روی دیسک سخت ممکن است طول بکشد، رابط آن باید به صورت ناهمگام (asynchronous) باشد تا موجب قفل شدن شدن صفحه نشود.

<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 شیء reader فایل، حاوی محتوای فایل خواهد بود.

اشیاء FileReader زمانی که به هر دلیل خواندن فایل با مشکل روبرو شود، یک رخداد “error” ایجاد می کنند . شیء خطا در نهایت در خاصیت error شیء reader قرار می گیرد. این رابط قبل از اضافه شدن promiseها به زبان جاوااسکریپت طراحی شده است .می توانید این کار را در قالب promise به صورت زیر انجام دهید.

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 با کمی جاوااسکریپت می توانند فرمت خوبی برای اپلیکیشن‌های کوچک باشند – برنامه‌های کمکی کوچکی که بعضی کارها را خودکار می کنند. با ترکیب چند فیلد فرم با گرداننده‌های رخداد، می توانید هر کاری از تبدیل سانتی متر به اینچ گرفته تا محاسبه‌ی رمز‌های عبور از روی یک رمزعبور مادر و نام یک وب‌سایت را انجام دهید.

زمانی که این گونه اپلیکیشن‌ها نیاز به ذخیره و به خاطرسپاری چیزی به صورت پایدار دارند، نمی توانید از متغیرهای جاوااسکریپت استفاده کنید- متغیرها با بسته شدن صفحه‌ی مرورگر از بین می روند. می توانید یک سرویس‌دهنده راه اندازی کنید، آن‌را به اینترنت متصل کنید و اطلاعات اپلیکیشن تان را در آنجا ذخیره کنید. در فصل 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", () => {
    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 ذخیره شده است می‌گیرد یا در صورت نبود آن، یک وضعیت نمونه که یک لیست خرید فرضی در آن قرار دارد را ایجاد می کند. خواندن یک فیلد که وجود خارجی ندارد از localStorage موجب می شود تا مقدار null برگردانده شود. ارسال null به JSON.parse موجب می شود که رشته‌ی "null" به وجود بیاید و درنتیجه خروجی null تولید شود. بنابراین، عملگر || را می توان برای در نظر گرفتن مقدار پیش‌فرض در شرایطی مثل این استفاده کرد.

متد setState موجب می شود اطمینان حاصل کنیم که DOM وضعیت مشخصی را نمایش می دهد و وضعیت جدید را در localStorage ذخیره می کند. گرداننده‌های رخداد این تابع را برای انتقال به یک وضعیت جدید فراخوانی می کنند.

استفاده از Object.assign در مثال بالا به این خاطر بود که یک کپی از شیء state.notes قدیمی ایجاد شود که حاوی خاصیتی باشد که اضافه یا بازنویسی شده است. Object.assign اولین آرگومانش را گرفته و تمامی خاصیت‌هایی که در آرگومان‌های بعدی می آید را به آن اضافه می کند. بنابراین، روش استفاده از براکت‌ها در آرگومان سوم برای ایجاد یک خاصیت که نام آن بر اساس یک مقدار پویا مشخص می شود استفاده شده است.

شیء دیگری وجود دارد شبیه به localStorage به نام sessionStorage. تفاوت بین این دو این است که محتوای sessionStorage با به پایان رسیدن هر جلسه (session) فراموش می شود که در بیشتر مرورگرها با بستن مرورگر این اتفاق می افتد.

خلاصه

در این فصل به نحوه‌ی عملکرد پروتوکل HTTP پرداختیم. یک کلاینت درخواستی را ارسال می کند که حاوی یک متد (معمولا GET) و یک مسیر که معرف یک منبع است می‌باشد. سرویس دهنده سپس تصمیم می گیرد که با این درخواست چه کند و با یک کد وضعیت و یک بدنه‌ی پاسخ، به درخواست جواب می دهد. هر دوی درخواست و پاسخ می توانند حاوی سرپیام‌ها (headers) باشند که اطلاعات بیشتری را فراهم می کنند.

رابطی که از طریق آن جاوااسکریپت مرورگر می تواند درخواست‌های HTTP را بسازد fetch نامیده می شود. ساخت یک درخواست به شکل زیر می ماند:

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

مرورگرها از درخواست‌های نوع GET برای بازیابی منابع مورد نیاز جهت نمایش یک صفحه‌ی وب استفاده می کنند. یک صفحه وب ممکن است حاوی فرم‌ها نیز باشد که این امکان را فراهم می کنند که با ثبت فرم، اطلاعات ورودی کاربر برای ارسال درخواست برای یک صفحه‌ی جدید ارسال شود.

HTML اشکال متنوعی از فیلدهای فرم را پشتیبانی می کند مانند فیلدهای متنی، چک‌باکس‌ها یا، فیلد‌های چند‌گزینه‌ای و فیلدهای انتخاب فایل.

این فیلدها را می تواند توسط جاوااسکریپت کنترل و تغییر داد. رخداد "change" با ایجاد تغییر در یک فیلد تولید می شود، رخداد "input" زمانی ایجاد می شود که متنی در فیلد تایپ شود و رخدادهای مربوط به صفحه‌کلید هنگامی که فیلد توسط صفحه‌کلید در دسترس و فعال است دریافت می شوند. خاصیت‌هایی مثل value (در فیلدهای text و select) یا checked (در چک‌باکس‌ها و دکمه‌های رادیویی) برای خواندن یا تنظیم محتوای فیلد استفاده می شوند.

زمانی که یک فرم ثبت می شود، یک رخداد "submit" روی آن ایجاد می شود. یک گرداننده‌ی جاوااسکریپت می تواند با فراخوانی preventDefault مانع از ثبت فرم شود. فیلدهای فرم را نیز می تواند بیرون از برچسب form استفاده کرد.

زمانی که کاربر فایلی را از سیستم محلی فایل خود به وسیله‌ی فیلد انتخاب فایل گزینش می کند، رابط FileReader را می تواند برای دسترسی به محتوای آن فایل از درون یک برنامه جاوااسکریپت استفاده کرد.

اشیاء localStorage و sessionStorage را می توان برای ذخیره اطلاعات به صورت مانا که علی رغم بارگیری مجدد صفحه باقی می‌مانند استفاده کرد. اولین شیء، داده‌ها را به صورت همیشگی ذخیره می کند( یا تا هنگامی که کاربر تصمیم به حذف آنها بگیرد) و دومی تا زمانی که مرورگر بسته نشده داده ها را نگه می دارد.

تمرین‌ها

مذاکره محتوا

یکی از کارهایی که HTTP می تواند انجام دهد اصطلاحا مذاکره‌ی محتوا – content negotiation خوانده می شود. از سرپیام درخواست Accept برای اعلام نوع سندی که سرویس‌گیرنده درخواست دارد به سرویس‌دهنده استفاده می شود.خیلی از سرویس‌دهنده‌ها این سرپیام را نادیده می گیرند، اما زمانی که یک سرویس‌دهنده از راه‌های متنوع کدگذاری یک منبع آگاه است، می تواند به این سرپیام نگاه کرده و چیزی که سرویس‌گیرنده ترجیح می دهد را برایش ارسال کند.

آدرس https://eloquentjavascript.net/author تنظیم شده است که بسته به درخواست سرویس‌گیرنده با متن عادی، HTML یا JSON پاسخ می‌دهد. این فرمت‌ها توسط انواع استاندارد شده‌ی رسانه، application/json و text/plain، text/html شناسایی می‌شود.

درخواست‌هایی برای بازیابی هر سه نوع فرمت گفته شده را به منبع ارسال کنید. از خاصیت headers در شیء مربوط به گزینه‌های که به fetch داده می شود برای تنظیم سرپیام Accept به نوع رسانه‌ی مورد نظر استفاده کنید.

درآخر، تلاش کنید که نوع رسانه‌ی application/rainbows+unicorns را درخواست کنید و ببینید چه اتفاقی خواهد افتاد.

// Your code here.

کد خود را بر اساس مثال‌های مربوط به fetch بنویسید که پیش‌تر در این فصل آمد.

درخواست برای یک نوع رسانه‌ی نامعتبر موجب می‌شود که پاسخی با کد 406 برگردانده شود، “Not acceptable”، که کدی است که سرویس‌دهنده باید در زمانی که قادر نیست به سرپیام Accept پاسخ معتبری بدهد، ارسال کند.

یک میز کار جاوااسکریپت

رابطی بسازید که به کاربران این امکان را بدهد که بتوانند کدهای جاوااسکریپت بنویسند و آن‌ها را اجرا کنند.

یک دکمه‌ی کنار فیلد <textarea> قرار دهید که در صورت فشرده شدن ، از سازنده‌ی Function که در فصل 10 دیدیم استفاده کند تا محتوای فیلد را درون یک تابع قرار داده و فراخوانی کند. مقدار بازگشتی از تابع را ، یا هر خطایی که به وجود می آید، به یک رشته تبدیل کنید و آن را پایین فیلد متنی نمایش دهید.

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

<script>
  // Your code here.
</script>

از document.querySelector یا document.getElementById برای دسترسی به عناصر موجود در صفحه‌ی HTMLتان استفاده کنید. یک گرداننده‌ی رخداد برای "click" یا "mousedown" روی دکمه می می تواند خاصیت value مربوط به فیلد متنی را گرفته و Function را روی آن فراخوانی کند.

اطمینان حاصل کنید که هم فراخوانی به Function و هم فراخوانی نتیجه‌ی آن را در یک بلاک try محصور کنید تا بتوانید استثناء‌هایی که تولید می شود را پوشش دهید. در این مورد، واقعا نمی‌دانیم که چه نوع استثنائی را باید انتظار داشته باشیم، پس همه را پوشش می دهیم.

خاصیت textContent مربوط به عنصر محل نمایش را می توان برای قرار دادن متن رشته‌ای در آن استفاده کرد. یا، اگر قصد دارید که محتوای قبلی آن را نگه‌دارید، یک گره‌ی متنی جدید با استفاده از document.createTextNode بسازید و آن را به عنصر مورد نظر الحاق کنید. به خاطر داشته باشید که به پایان آن یک کاراکتر خط جدید اضافه کنید تا از ظاهر شدن تمام خروجی در یک خط جلوگیری کنید.

بازی زندگی کانوی ( Conway’s Game of Life)

بازی زندگی کانوی یک تشبیه ساده است که یک “زندگی” مصنوعی را روی یک جدول ایجاد می کند. هر خانه جدول می تواند زنده یا مرده باشد. در هر نسل(دوره) قوانین زیر اعمال می شوند:

هر خانه‌ای که مجاور محسوب شود به عنوان همسایه در نظر گرفته می شود حتی خانه‌هایی که به صورت قطری مجاور هستند.

توجه داشته باشید که این قوانین به کل جدول (grid) اعمال می شوند نه به یک چهار ضلعی محدود. به این معنا که شمارش همسایه‌ها بر اساس وضعیتی خواهد بود که ابتدای هر نسل وجود دارد و تغییرات اعمالی به همسایه‌ها در طول این نسل نباید به وضعیت جدید یک سلول مشخص اثر بگذارد.

این بازی را به وسیله‌ی جاوااسکریپت و با استفاده هر ساختار داده‌ای که مناسب می دانید پیاده سازی کنید. از Math.random برای پر کردن جدول با الگوی تصادفی برای شروع استفاده کنید. از فیلدهای checkbox به همراه دکمه‌ای در کنارشان برای انتقال به نسل بعد در جدول استفاده کنید. زمانی که کاربر checkbox را فعال یا غیر فعال می کند تغییرات آن ها باید در هنگام محاسبه‌ی نسل بعد اعمال شوند.

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

<script>
  // Your code here.
</script>

برای حل مساله‌ی اتفاق افتادن تغییرات در یک زمان واحد به صورت مفهومی، سعی کنید که محاسبه‌ی یک نسل را به صورت یک تابع ناب (pure) ببینید، که یک grid را گرفته و یک grid جدید تولید می کند که نماینده‌ی دوره‌ی بعد می‌باشد.

نمایش ماتریکس را می‌توان از راهی که در فصل 6 نشان داده شد، میسر کرد. می‌توانید تعداد همسایه‌های زنده را به وسیله‌ی دو حلقه‌ی تودرتو بشمارید، با پیمایش مختصات مجاور در هر دو بعد. مواظب باشید که سلول‌های خارج از فیلد را نشمارید و سلول مرکز را هم به حساب نیاورید، سلولی که همسایه‌هایش را قصد داریم بشماریم.

از دو راه می توان اطمینان حاصل کرد که تغییر checkbox ها روی نسل بعد تاثیر می‌گذارد. یک گرداننده‌ی رخداد می تواند این تغییرات را متوجه شود و grid فعلی را برای بازتاب آن به‌روز کند، یا می توانید یک grid تازه از مقادیر موجود در checkbox ها قبل از محاسبه‌ی نسل بعد برای تولید یک grid تازه استفاده کنید.

اگر انتخاب شما استفاده از گرداننده‌های رخداد باشد، ممکن است بخواهید که خاصیت‌هایی برای مشخص نمودن موقعیت هر checkbox الحاق کنید تا بتوان به آسانی سلولی که باید تغییر کند را پیدا کنید.

برای رسم جدول checkbox ها، می‌توانید یا از یک <table> ( به Chapter 14 رجوع کنید ) استفاده کنید یا به سادگی همه‌ی آن ها را در یک عنصر قرار داده و از <br> بین ردیف‌ها استفاده کنید.