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

إرسال البيانات واستلامها عبر الشبكة في جافاسكربت


ابراهيم الخضور

يمكن للغة جافاسكربت 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
  • لاحظ هاتين الميزتين:
  1. يزيل التابع set الحقول التي لها نفس الاسم، بينما لا يفعل ذلك التابع append، وهذا هو الاختلاف الوحيد بينهما.
  2. لا بدّ من استخدام صيغة تضم ثلاثة وسطاء لإرسال الملف، آخرها اسم الملف والذي يؤخذ عادةً من منظومة ملفات المستخدم من خلال العنصر <"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 يوضح ذلك.

تفاصيل مهمة:

  1. ينبغي أن يكون هناك طلب إحضار واحد لكل مستخدم.
  2. لا ينبغي أن ينتظر أي طلب انتهاء طلب آخر، لكي تصل البيانات بالسرعة الممكنة.
  3. في حال إخفاق أي طلب، أو عدم وجود مستخدم بالاسم المُعطى، فينبغي أن تعيد الدالة القيمة "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


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

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

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



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

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

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

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

  Only 75 emoji are allowed.

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

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

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


×
×
  • أضف...