يمكن للغة جافاسكربت JavaScript إرسال طلبات شبكة إلى الخادم وتحميل معلومات جديدة عندما يتطلب الأمر ذلك، إذ يمكننا على سبيل المثال استخدام طلبات الشبكة في الحالات التالية:
- إرسال طلب.
- بتحميل معلومات مستخدم.
- الحصول على آخر التحديثات من الخادم.
ويجري كل ذلك دون إعادة تحميل الصفحة.
تنضوي طلبات الشبكة التي تنفذها لغة JavaScript تحت المظلة AJAX، وهي اختصار للعبارة Asynchronous JavaScript And XML، ورغم ذلك لا نحتاج إلى استخدام XML، فقد وضعت العبارة السابقة منذ فترة طويلة لذلك وجدت هذه الكلمة ضمنها، وقد تكون سمعت بهذه العبارة الآن أيضًا.
هنالك طرق عديدة لإرسال طلبات عبر الشبكة والحصول على معلومات من الخادم، وسنبدأ بالطريقة الأحدث ()fetch
، علمًا أنه لا تدعم المتصفحات القديمة هذه الدالة (ويمكن الاستعاضة عنها بشيفرة بديلة)، لكنها مدعومة جيدًا في المتصفحات الحديثة، وإليك صيغتها:
let promise = fetch(url, [options])
حيث:
-
url
: عنوان المورد الذي ستصل إليه الدالة. -
options
: المعاملات الاختيارية من توابع وترويسات وغيرها.
تتحول الدالة إلى طلب GET بسيط لتنزيل محتوى العنوان url
إن لم تكن هناك معاملات اختيارية options
، ويبدأ المتصفح الطلب مباشرةً ويعيد وعدًا promise ستستخدمه الشيفرة التي تستدعي الطلب للحصول على النتيجة، وتكون الاستجابة عادةً عمليةً بمرحلتين:
الأولى: يُحلَّل الوعد الذي تعيده fetch
عبر كائن من الصنف Respo-nse حالما يستجيب الخادم بالترويسات المناسبة، ويمكن التحقق من نجاح الطلب أو عدم نجاحه، والتحقق أيضًا من الترويسات، لكن لن يصل جسم الطلب في هذه المرحلة، ويُرفَض الوعد إن لم تكن fetch
قادرةً على إنجاز طلب HTTP لمشاكل في الشبكة مثلًا، أو لعدم وجود موقع على العنوان المُعطى، ولن تسبب حالات HTTP غير العادية مثل 404 أو 500 أخطاءً.
يمكن معرفة حالة طلب من خصائص الاستجابة:
-
status
: رمز الحالة status code لطلب HTTP مثل الرمز 200. -
ok
: قيمة منطقية "true" عندما يكون رمز الحالة بين 200 و299.
إليك المثال التالي:
let response = await fetch(url); if (response.ok) { // إن كان رمز الحالة بين 200-299 // الحصول على جسم الطلب let json = await response.json(); } else { alert("HTTP-Error: " + response.status); }
الثانية: استخدام استدعاء إضافي للحصول على جسم الطلب، ويؤمن الكائن Response
عدة توابع مبنية على الوعد للوصول إلى جسم الطلب وبتنسيقات مختلفة:
-
()response.text
: لقراءة الاستجابة وإعادة نص. -
()response.json
: يفسِّر النص وفق تنسيق JSON. -
()response.formData
: يعيد الاستجابة على شكل كائنFormData
سنشرحه في الفقرة التالية. -
()response.blob
: يعيد الاستجابة على شكل كائن البيانات الثنائية Blob. -
()response.arrayBuffer
: يعيد الاستجابة على شكل كائن ArrayBuffer وهو تمثيل منخفض المستوى للبيانات الثنائية. -
الكائن
response.body
وهو كائن من الصنف ReadableStream يسمح بقراءة جسم الطلب كتلةً كتلةً، وسنعرض مثالًا عن ذلك لاحقًا.
لنحاول على سبيل المثال الحصول على كائن JSON من آخر نسخة معتمدة لموقع الدورة التعليمية هذه على GitHub:
let url = 'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits'; let response = await fetch(url); let commits = await response.json(); // Json قراءة الاستجابة على شكل شيفرة alert(commits[0].author.login);
كما يمكن فعل ذلك من خلال الوعود الصرفة دون استخدام await
:
fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits') .then(response => response.json()) .then(commits => alert(commits[0].author.login));
استخدم ()await response.text
للحصول على نص الطلب بدلًا من ()json.
:
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits'); let text = await response.text(); // قراءة جسم الاستجابة على شكل نص alert(text.slice(0, 80) + '...');
لنستعرض مثالًا عن قراءة بيانات بالصيغة الثنائية، ونحضر صورةً ما ونظهرها:
let response = await fetch('https://javascript.info/article/fetch/logo-fetch.svg'); let blob = await response.blob(); // Blob تنزيل على شكل // <img> إنشاء عنصر let img = document.createElement('img'); img.style = 'position:fixed;top:10px;left:10px;width:100px'; document.body.append(img); // إظهاره img.src = URL.createObjectURL(blob); setTimeout(() => { // إخفاءه بعد ثلاث ثوان img.remove(); URL.revokeObjectURL(img.src); }, 3000);
اقتباسأمر هام: يمكن اختيار طريقة واحدة لقراءة جسم الطلب، فلو تلقينا الاستجابة باستخدام التابع
()response.text
؛ فلن يعمل بعدها التابع()response.json
لأنّ معالجة جسم الطلب قد أجريت وانتهت.
let text = await response.text(); // انتهاء معالجة جسم الطلب let parsed = await response.json(); // سيخفق، فقد جرت المعالجة وانتهت
ترويسات الاستجابة
يمكن الحصول على ترويسات الاستجابة على شكل كائن ترويسات شبيه بالترابط Map من خلال الأمر response.headers
، ولا يُعَد الكائن ترابطًا تمامًا، لكنه يمتلك توابع مماثلةً للحصول على ترويسات من خلال اسمها أو بالمرور عليها:
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits'); // الحصول على ترويسة واحدة alert(response.headers.get('Content-Type')); // application/json; charset=utf-8 // المرور على الترويسات كلها for (let [key, value] of response.headers) { alert(`${key} = ${value}`); }
ترويسات الطلب
يمكن استخدام خيار الترويسات headers
لإعداد ترويسة الطلب في الدالة featch
، إذ تمتلك كائنًا يضم الترويسات المُرسَلة كالتالي:
let response = fetch(protectedUrl, { headers: { Authentication: 'secret' } });
لكن هناك قائمة من ترويسات HTTP المنوعة التي لا يمكن ضبطها:
-
Accept-Charset
وAccept-Encoding
-
Access-Control-Request-Headers
-
Access-Control-Request-Method
-
Connection
-
Content-Length
-
Cookie
وCookie2
-
Date
-
DNT
-
Expect
-
Host
-
Keep-Alive
-
Origin
-
Referer
-
TE
-
Trailer
-
Transfer-Encoding
-
Upgrade
-
Via
-
Proxy-*
-
Sec-*
تضمن هذه الترويسات ملاءمة طلبات HTTP وأمانها، لذلك يتحكم فيها المتصفح حصرًا.
طلبات الكتابة POST
لإرسال طلب POST أو طلب من أي نوع لا بدّ من استخدام خيارات fetch
:
-
method
: نوع طلب HTTP مثل HTTP-POST. -
body
: ويمثل جسم الطلب وقد يكون: - نصًا: بتنسيق JSON مثلًا.
-
كائن
FormData
لإرسال بيانات على شكلform/multipart
. -
كائن
Blob
أوBufferSource
لإرسال بيانات ثنائية. -
URLSearchParams لإرسال البيانات بتشفير
x-www-form-urlencoded
، وهو نادر الاستخدام.
يُستخدم تنسيق JSON غالبًا، حيث تُرسل الشيفرة التالية الكائن user
وفق تنسيق JSON مثلًا:
let user = { name: 'John', surname: 'Smith' }; let response = await fetch('https://javascript.info/article/fetch/post/user', { method: 'POST', headers: { 'Content-Type': 'application/json;charset=utf-8' }, body: JSON.stringify(user) }); let result = await response.json(); alert(result.message);
لاحظ ضبط الترويسة Content-Type
افتراضيًا على القيمتين text/plain;charset=UTF-8
إذا كان جسم الطلب على شكل نص، لكن طالما أننا سنرسل البيانات بصيغة JSON، فسنستخدم الخيار headers
لإرسال الترويسة application/json
بدلًا عن text/plain
كونها تمثل المحتوى الصحيح للبيانات.
إرسال صورة
يمكن إرسال بيانات ثنائية عبر الدالة fetch
باستخدام الكائنات Blob
أو BufferSource
، سنجد في المثال التالي معرّف لوحة رسم <canvas>
التي يمكننا الرسم ضمنها بتحريك الفأرة، ومن ثم إرسال الصورة الناتجة إلى الخادم عند النقر على الزر "submit":
<body style="margin:0"> <canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas> <input type="button" value="Submit" onclick="submit()"> <script> canvasElem.onmousemove = function(e) { let ctx = canvasElem.getContext('2d'); ctx.lineTo(e.clientX, e.clientY); ctx.stroke(); }; async function submit() { let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png')); let response = await fetch('/article/fetch/post/image', { method: 'POST', body: blob }); // يستجيب الخادم بتأكيد وصول البيانات وبحجم الصورة let result = await response.json(); alert(result.message); } </script> </body>
ستظهر النتيجة كالتالي:
لاحظ أننا لم نضبط هنا قيمة الترويسة Content-Type
يدويًا، لأنّ الكائن Blob
له نوع مضمَّن (هو image/png
في حالتنا، كما ولّده التابع toBlob
)، وسيمثّل هذا النوع قيمة الترويسة Content-Type
في كائنات Blob
.
يمكن كتابة الدالة ()submit
دون استخدام الصيغة async/await
كالتالي:
function submit() { canvasElem.toBlob(function(blob) { fetch('https://javascript.info/article/fetch/post/image', { method: 'POST', body: blob }) .then(response => response.json()) .then(result => alert(JSON.stringify(result, null, 2))) }, 'image/png'); }
استخدام الكائن FormData لإرسال النماذج
يمكننا الاستفادة من الكائن FormData لإرسال نماذج HTML مع ملفات أو بدونها، بالإضافة إلى حقول إضافية. وكما قد تخمِّن؛ يمثل هذا الكائن بيانات نماذج HTML، وإليك صيغة الدالة البانية له:
let formData = new FormData([form]);
سيتحكم الكائن FormData تلقائيًا بحقول العنصر form
إذا استُخدم في مستند HTML، وما يميز الكائن FormData
هو أنّ توابع إرسال الطلبات واستقبالها عبر الشبكة مثل Fetch
ستقبله مثل جسم للطلب، إذ يُشفَّر ويُرسَل بترويسة قيمتها Content-Type: multipart/form-data
، وتبدو العملية بالنسبة إلى الخادم مثل إرسال عادي لنموذج.
إرسال نموذج بسيط
لنرسل أولًا نموذجًا بسيطًا، وسيظهر في مثالنا هذا في نموذج من سطر واحد:
<form id="formElem"> <input type="text" name="name" value="John"> <input type="text" name="surname" value="Smith"> <input type="submit"> </form> <script> formElem.onsubmit = async (e) => { e.preventDefault(); let response = await fetch('https://javascript.info/article/formdata/post/user', { method: 'POST', body: new FormData(formElem) }); let result = await response.json(); alert(result.message); }; </script>
وستكون النتيجة كالتالي:
لا توجد شيفرة خاصة بالخادم في هذا المثال، لأنها خارج نطاق هذه الدورة التعليمية، حيث سيقبل الخادم الطلب HTTP-POST ويستجيب بالرسالة "User saved" أي "خُزّن المستخدم".
توابع الكائن
نستخدم عددًا من التوابع لتعديل الحقول في الكائن FormData
:
-
(formData.append(name, value
: يُضيف حقلًا بالاسمname
قيمته هيvalue
. -
(formData.append(name, blob, fileName
: يضيف حقلًا كما لو أنه العنصر<"input type="file>
، حيث يحدد الوسيط الثالث للتابعfileName
اسم الملف -وليس اسم الحقل- كما لو أنه اسم لملف في منظومة ملفات الجهاز. -
(formData.delete(name
: يزيل حقلًا محددًا بالاسمname
. -
(formData.get(name
: يعطي قيمة الحقل المحدد بالاسمname
. -
(formData.has(name
: إذا وجد حقل بالاسمname
فسيعيد القيمةtrue
وإلاfalse
.
يمكن أن يحوي النموذج العديد من الحقول التي لها نفس الاسم، لذلك سينتج عن الاستدعاءات المختلفة لضم append
الحقول حقولًا لها نفس الاسم. وسنجد التابع set
الذي له صيغة append
نفسها، لكنه يزيل جميع الحقول التي لها اسم محدد name
، ثم يضيف الحقل الجديد، وبالتالي سنضمن وجود حقل وحيد بالاسم name
، تشبه باقي التفاصيل التابع append
.
-
(formData.set(name, value
-
(formData.set(name, blob, fileName
.
يمكن أيضًا إجراء تعداد على عناصر الكائن FormData
باستخدام الحلقة for..of
:
let formData = new FormData(); formData.append('key1', 'value1'); formData.append('key2', 'value2'); // قائمة من الأزواج مفتاح/قيمة for(let [name, value] of formData) { alert(`${name} = ${value}`); // key1 = value1, then key2 = value2 }
إرسال نموذج مع ملف
يُرسَل النموذج دائمًا بحيث تكون ترويسة المحتوى مثل التالي Content-Type: multipart/form-data
، وتسمح هذه الطريقة في الترميز بإرسال الملفات، أي ستُرسَل الملفات التي يحددها العنصر <"input type="file>
أيضًا بشكل مشابه للإرسال الاعتيادي للنماذج، إليك مثالًا عن ذلك:
<form id="formElem"> <input type="text" name="firstName" value="John"> Picture: <input type="file" name="picture" accept="image/*"> <input type="submit"> </form> <script> formElem.onsubmit = async (e) => { e.preventDefault(); let response = await fetch('https://javascript.info/article/formdata/post/user-avatar', { method: 'POST', body: new FormData(formElem) }); let result = await response.json(); alert(result.message); }; </script>
ستظهر النتيجة كالتالي:
إرسال ملف يحتوي على كائن بيانات ثنائية
يمكن أن نرسل بيانات ثنائيةً مولّدةً تلقائيًا، مثل الصور على شكل كائن بيانات Blob
، وبالتالي يمكن تمريره مباشرةً مثل المعامل body
للدالة Fetch كما رأينا في الفقرة السابقة، ومن الأنسب عمليًا إرسال صورة لتكون جزءًا من نموذج له حقول وبيانات وصفية Metadata وليس بشكل منفصل، إذ يُلائم الخوادم عادةً استقبال نماذج مشفرة مكونة من أجزاء متعددة أكثر من بيانات ثنائية خام.
يُرسل المثال التالي صورةً مرسومةً ضمن العنصر <canvas>
، بالإضافة إلى بعض الحقول على شكل نموذج باستخدام الكائن FormData
:
<body style="margin:0"> <canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas> <input type="button" value="Submit" onclick="submit()"> <script> canvasElem.onmousemove = function(e) { let ctx = canvasElem.getContext('2d'); ctx.lineTo(e.clientX, e.clientY); ctx.stroke(); }; async function submit() { let imageBlob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png')); let formData = new FormData(); formData.append("firstName", "John"); formData.append("image", imageBlob, "image.png"); let response = await fetch('https://javascript.info/article/formdata/post/image-form', { method: 'POST', body: formData }); let result = await response.json(); alert(result.message); } </script> </body>
وتظهر النتيجة كالتالي:
لاحظ كيف يُضاف الكائن Blob
الذي يمثل الصورة:
formData.append("image", imageBlob, "image.png");
وهذا الأسلوب مشابه لاستخدام العنصر ضمن نموذج، حيث يرسل الزائر الملف الذي يحمل الاسم "image.png" (الوسيط الثالث) والذي يحمل البيانات التي يحددها imageBlob
(الوسيط الثاني) انطلاقًا من منظومة الملفات، ويقرأ الخادم بيانات النموذج وكذلك الملف كما لو أنها عملية إرسال نموذج اعتيادية.
خلاصة
-
يتكون طلب إحضار بيانات تقليدي من استدعاءين باستخدام الصيغة
await
:
let response = await fetch(url, options); // يُنفَّذ مع ترويسة الاستجابة headers let result = await response.json(); // JSON قراءة جسم الطلب بتنسيق
أو دون الصيغة await
:
fetch(url, options) .then(response => response.json()) .then(result => /* process result */)
وتتمثل خصائص الاستجابة في الآتي:
-
response.status
: رمز حالة HTTP للاستجابة. -
response.ok
: يأخذ القيمة "true" إذا كانت قيمة رمز الحالة بين 200-299. -
response.headers
: تعيد كائنًا شبيهًا بالترابط Map يضم ترويسات HTTP.
توابع الحصول على جسم الاستجابة:
-
()response.text
: لقراءة الاستجابة وإعادة نص. -
()response.json
: يفسر النص وفق تنسيق JSON. -
()response.formData
: يعيد الاستجابة على شكل كائنFormData
. -
()response.blob
: تعيد الاستجابة على شكل كائن بيانات ثنائية Blob. -
()response.arrayBuffer
: يعيد الاستجابة على شكل كائن ArrayBuffer وهو تمثيل منخفض المستوى للبيانات الثنائية.
خيارات Fetch
التي تعرفنا عليها حتى الآن:
-
method
: نوع طلب HTTP. -
headers
: كائن يضم ترويسات الطلب، ويجب الانتباه إلى الترويسات التي يُمنع استخدامها. -
body
: البيانات التي ستُرسل (جسم الطلب) على شكلstring
أوFormData
أوBufferSource
أوBlob
أوUrlSearchParams
.
وسنتعرف على خيارات أخرى في الفصل التالي.
-
تُستخدم الكائنات FormData للتحكم بنماذج وإرسالها باستخدام
fetch
أو أي دوال لإرسال الطلبات عبر الشبكة، ويمكن إنشاؤها بالأمر(new FormData(form
انطلاقًا من نموذج HTML موجود، أو إنشاؤها دون نموذج ثم نضيف إليه الحقول باستخدام التوابع: -
(formData.append(name, value
-
(formData.append(name, blob, fileName
-
(formData.set(name, value
-
(formData.set(name, blob, fileName
- لاحظ هاتين الميزتين:
-
يزيل التابع
set
الحقول التي لها نفس الاسم، بينما لا يفعل ذلك التابعappend
، وهذا هو الاختلاف الوحيد بينهما. -
لا بدّ من استخدام صيغة تضم ثلاثة وسطاء لإرسال الملف، آخرها اسم الملف والذي يؤخذ عادةً من منظومة ملفات المستخدم من خلال العنصر
<"input type="file>
.
- من التوابع الأخرى:
-
(formData.delete(name
-
(formData.get(name
-
(formData.has(name
تمارين
إحضار بيانات مستخدمين من GitHub
أنشئ دالةً غير متزامنة "async" باسم (getUsers(names
للحصول على مصفوفة من سجلات الدخول Logins على GitHub، وتحضر المستخدمين أيضًا، ثم تعيد مصفوفةً بأسماء المستخدمين على GitHub.
سيكون العنوان الذي يحوي معلومات مستخدم معين له اسم مستخدم محدد USERNAME
هو:
api.github.com/users/USERNAME
.
ستجد مثالًا تجريبيًا وضعناه في نمط الحماية sandbox يوضح ذلك.
تفاصيل مهمة:
- ينبغي أن يكون هناك طلب إحضار واحد لكل مستخدم.
- لا ينبغي أن ينتظر أي طلب انتهاء طلب آخر، لكي تصل البيانات بالسرعة الممكنة.
- في حال إخفاق أي طلب، أو عدم وجود مستخدم بالاسم المُعطى، فينبغي أن تعيد الدالة القيمة "null" في المصفوفة الناتجة.
الحل
إليك حل التمرين:
- نفِّذ التعليمة التالية لإحضار مستخدم:
fetch('https://api.github.com/users/USERNAME')
-
استدع التابع
()json.
لقراءة الكائن JS، إن كان رمز الحالة المرافق لاستجابة الخادم هو200
. -
في الحالة التي تخفق فيها تعليمة الإحضار
fetch
أو لم يكن رمز الحالة200
، أعد القيمةnull
في المصفوفة الناتجة.
إليك الشيفرة:
async function getUsers(names) { let jobs = []; for(let name of names) { let job = fetch(`https://api.github.com/users/${name}`).then( successResponse => { if (successResponse.status != 200) { return null; } else { return successResponse.json(); } }, failResponse => { return null; } ); jobs.push(job); } let results = await Promise.all(jobs); return results; }
ملاحظة: يرتبط استدعاء التابع then.
بمباشرة بالدالة fetch
، وبالتالي لا تنتظر عمليات إحضار أخرى لتنتهي عندما تصلك الاستجابة على أحدها بل إبدأ بقراءة الاستجابة مستخدمًا ()json.
.
إن استخدمت الشيفرة التالية :
await Promise.all(names.map(name => fetch(...)))
ثم استدعيت ()json.
لقراءة النتائج، فقد يكون عليك الانتظار لتنتهي جميع عمليات الإحضار. ستضمن قراءة نتيجة كل عملية إحضار بمفردها إن استخدمت مباشرة ()json.
مع fetch
.
فما عرضناه كان مثالًا عن فائدة واجهة الوعود البرمجية منخفضة المستوى low-level Promise API حتى لو استخدمنا async/await
.
إليك الحل في بيئة تجريبية مع الاختبارات
ترجمة -وبتصرف- للفصلين popups and window methods و FormData من سلسلة The Modern JavaScript Tutorial
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.