البحث في الموقع
المحتوى عن 'xmlhttprequest'.
-
هو كائن مدمَج في المتصفح، يسمح بإرسال طلبات HTTP في جافاسكريبت JavaScript، ويمكنه العمل مع أي نوع من البيانات، وليس فقط بيانات "XML" على الرغم من وجود الكلمة "XML" في تسميته. يساعدنا هذا الكائن في عمليات رفع وتنزيل الملفات وتتبع تقدمها وغير ذلك الكثير، ولا بدّ من الإشارة إلى وجود أسلوب أكثر حداثةً منه وهو استخدام fetch التي ألغت استخدامه بشكل أو بآخر. يُستخدم XMLHttpRequest في تقنيات تطوير الويب العصرية لثلاثة أسباب، هي الآتية: أسباب تاريخية: عند الحاجة إلى دعم سكربت موجود يستخدم XMLHttpRequest. الحاجة إلى دعم المتصفحات القديمة دون استخدام شيفرة "polyfill". إنجاز شيء لا يمكن تنفيذه باستخدام fetch مثل تتبع تقدم رفع ملف. إن كنت تحتاج ما ذكرناه تابع قراءة المقال وإلا فتوجه إلى فصل استخدام Fetch. الأساسيات يعمل الكائن XMLHttpRequest وفق النمطين المتزامن وغير المتزامن، سنتحدث بدايةً عن الوضع غير المتزامن، لأنه يستخدم في أغلب الحالات. يُنجز طلب HTTP وفق 4 خطوات: الخطوة الأولى، إنشاء الكائن XMLHttpRequest: let xhr = new XMLHttpRequest(); ليس للدالة البانية أي وسطاء. الخطوة الثانية، تهيئة الكائن، وتكون بعد إنشائه عادةً: xhr.open(method, URL, [async, user, password]) يؤمن التابع open المعاملات الرئيسية للطلب، وهي: method: تحدد نوع طلب HTTP، عادةً يكون GET أو POST. URL: عنوان الموقع الذي نرسل الطلب إليه، ويمكن استخدام الكائن URL. async: تكون العملية متزامنةً إذا أسندت لها القيمة false صراحةً، وسنغطي ذلك لاحقًا. user وpassword: اسم المستخدم وكلمة المرور للاستيثاق، عند الحاجة. لا يفتح التابع open الاتصال على الرغم من اسمه، بل يهيئ الطلب فقط، ولا يبدأ الاتصال عبر الشبكة إلا باستدعاء send. الخطوة الثالثة، إرسال الطلب: xhr.send([body]) يفتح هذا التابع الاتصال ويرسل الطلب إلى الخادم، حيث يحتوي المعامل body على جسم الطلب.ولا تمتلك الطلبات مثل GET جسماً، وتستخدم طلبات أخرى مثل POST الجسم لإرسال البيانات إلى الخادم، وسنرى أمثلةً عن ذلك لاحقًا. الخطوة الرابعة، الاستماع إلى أحداث الكائن xhr لتلقي الاستجابة، وإليك أكثر الأحداث استخدامًا: load: عندما يكتمل الطلب ونحصل على الاستجابة كاملةً، حتى لو أعاد الخادم رمز الحالة 500 أو 400 مثلًا. error: عندما يتعذر تنفيذ الطلب، كأن تكون الشبكة معطلةً أو العنوان خاطئًا. progress: ويقع دوريًا أثناء تنزيل الاستجابة، ويوضح الكمية التي نُزِّلت. xhr.onload = function() { alert(`Loaded: ${xhr.status} ${xhr.response}`); }; xhr.onerror = function() { // عندما لا تحدث الاستجابة إطلاقًا alert(`Network Error`); }; xhr.onprogress = function(event) { // يقع دوريًا // event.loaded - الحجم الذي نزل بالبايت // event.lengthComputable = true إن أرسل الخادم ترويسة طول المحتوى // event.total - العدد الكلي للبايتات alert(`Received ${event.loaded} of ${event.total}`); }; إليك مثالًا كاملًا، حيث تحمّل الشيفرة الموالية العنوان الآتي article/xmlhttprequest/example/load/ من الخادم، وتطبع تقدم العملية: // 1. إنشاء الكائن let xhr = new XMLHttpRequest(); // 2. URL /article/.../load تهيئته مع العنوان xhr.open('GET', '/article/xmlhttprequest/example/load'); // 3. إرسال الطلب عبر الشبكة xhr.send(); // 4. سيستدعى هذا الجزء عند تلقي الاستجابة xhr.onload = function() { if (xhr.status != 200) { // analyze HTTP status of the response alert(`Error ${xhr.status}: ${xhr.statusText}`); // e.g. 404: Not Found } else { // show the result alert(`Done, got ${xhr.response.length} bytes`); // response is the server response } }; xhr.onprogress = function(event) { if (event.lengthComputable) { alert(`Received ${event.loaded} of ${event.total} bytes`); } else { alert(`Received ${event.loaded} bytes`); // no Content-Length } }; xhr.onerror = function() { alert("Request failed"); }; نستقبل نتيجة الطلب باستخدام خصائص الكائن xhr التالية حالما يستجيب الخادم: status: رمز حالة HTTP، عدد، مثل 200 و404 و403 وهكذا، كما يمكن أن يكون 0 عند حدوث خطأ لا علاقة له بالنقل وفق بروتوكول HTTP. statusText: رسالة حالة HTTP، نص، مثل OK لرمز الحالة 200 وNot Found لرمز الحالة 404، وForbidden لرمز الحالة 403. response: قد تستخدم السكربتات القديمة responseText، وتمثل جسم الاستجابة التي يرسلها الخادم. يمكن أن نحدد أيضًا زمن انتهاء timeout مستخدمين الخاصية الموافقة: xhr.timeout = 10000; // زمن الانتهاء بالميلي ثانية إن لم ينجح الطلب خلال الفترة الزمنية المحددة فسيُلغى وسيقع الحدث timeout. إضافة معاملات بحث إلى العنوان URL let url = new URL('https://google.com/search'); url.searchParams.set('q', 'test me!'); // 'q' ترميز المعامل xhr.open('GET', url); // https://google.com/search?q=test+me%21 نوع الاستجابة من الممكن استخدام الخاصية ResponseType لضبط تنسيق الاستجابة: "": القيمة الافتراضية، الحصول على الاستجابة كنص. "text": الحصول على الاستجابة كنص. "arraybuffer": الحصول على الاستجابة على شكل كائن ArrayBuffer، للحصول على بيانات ثنائية. "blob": الحصول على الاستجابة على شكل كائن Blob، للحصول على بيانات ثنائية. "document": الحصول على مستند XML، باستخدام اللغة XPath أو غيرها. "json": الحصول على الاستجابة بصيغة JSON، تفسر الاستجابة تلقائيًا. لنستقبل الاستجابة بصيغة JSON مثلًا: let xhr = new XMLHttpRequest(); xhr.open('GET', '/article/xmlhttprequest/example/json'); xhr.responseType = 'json'; xhr.send(); // {"message": "Hello, world!"} الاستجابة هي xhr.onload = function() { let responseObj = xhr.response; alert(responseObj.message); // Hello, world! }; حالات الجاهزية Ready States يمر الكائن بعدة حالات عند تقدم تنفيذ الطلب، ويمكن الوصول إلى الحالة من خلال xhr.readyState، وإليك الحالات جميعها كما وردت في التوصيفات: UNSENT = 0; // الحالة الأساسية OPENED = 1; //open استدعاء التابع HEADERS_RECEIVED = 2; // استقبال ترويسة الاستجابة LOADING = 3; // تلقي جسم الاستجابة DONE = 4; // اكتمال الطلب ينتقل الكائن XMLHttpRequest بين الحالات السابقة بالترتيب 0 ثم 1 ثم 2 ثم 3 ثم 4، وتتكرر الحالة 3 في كل مرة تُستقبل فيها حزمة بيانات عبر الشبكة، ويمكننا تتبع هذه الحالات باستخدام الحدث readystatechange. xhr.onreadystatechange = function() { if (xhr.readyState == 3) { // تحميل } if (xhr.readyState == 4) { // انتهاء الطلب } }; قد تجد مستمعات للأحداث readystatechange حتى في الشيفرات القديمة، وذلك لأنّ أحداثًا مثل load وغيرها، لم تكن موجودةً في فترة ما، لكن معالجات الأحداث load/error/progress قد ألغت استخدامها. طلب الإلغاء Aborting request يمكن إلغاء الطلب في أي لحظة، وذلك باستدعاء التابع ()xhr.abort : xhr.abort(); // إلغاء الطلب يحرّض استدعاء التابع الحدث abort وتأخذ الخاصية xhr.status القيمة 0. الطلبات المتزامنة عند إسناد القيمة false إلى المعامل الثالث async للتابع open، فسيُنفَّذ الطلب بتزامن Synchronously. وبعبارة أخرى، ستتوقف JavaScript مؤقتًا عن التنفيذ عند إرسال الطلب ()send ثم تتابع عند تلقي الاستجابة، وهذا يشابه تعليمتي alert وprompt. إليك المثال السابق وقد أعيدت كتابته مع وجود المعامل الثالث للتابع open: let xhr = new XMLHttpRequest(); xhr.open('GET', '/article/xmlhttprequest/hello.txt', false); try { xhr.send(); if (xhr.status != 200) { alert(`Error ${xhr.status}: ${xhr.statusText}`); } else { alert(xhr.response); } } catch(err) { // onerror بدلًا من alert("Request failed"); } قد يبدو الأمر جيدًا لكنها طريقة يندر استخدامها، لأنها تعيق تنفيذ شيفرة JavaScript حتى يكتمل التحميل، وقد يصبح من المستحيل تمرير الصفحة لعرض بقية المحتويات أثناء تنفيذها في بعض المتصفحات، وقد يقترح المتصفح أيضًا إغلاق الصفحة العالقة إذا تأخر تنفيذ الطلب المتزامن كثيرًا. ولا تتاح العديد من الإمكانيات المتقدمة للكائن XMLHttpRequest، مثل الطلب من نطاق آخر أو تحديد زمن الانتهاء من الطلب، عند إرسال الطلبات بتزامن، ولا مؤشرات أيضًا على تقدم العملية، لذلك لا تُنفّذ الطلبات المتزامنة إلا نادرًا. ترويسات HTTP يتيح الكائن XMLHttpRequest إرسال ترويسات مخصصة وقراءة ترويسات الاستجابة، وهنالك ثلاثة توابع للتعامل مع الترويسات: (setRequestHeader(name, value: يعطي اسمًا name وقيمةً value لترويسة الطلب، وإليك مثالًا عن ذلك: xhr.setRequestHeader('Content-Type', 'application/json') فيما يلي مثال لتوضيح ذلك: xhr.setRequestHeader('X-Auth', '123'); xhr.setRequestHeader('X-Auth', '456'); // ستكون الترويسة: // X-Auth: 123, 456 (getResponseHeader(name: تعيد ترويسة الاستجابة ذات الاسم المحدد name، عدا Set-Cookie وSet-Cookie2، إليك مثالًا: xhr.getResponseHeader('Content-Type') ()getAllResponseHeaders: تعيد كل ترويسات الاستجابة، عدا Set-Cookie وSet-Cookie2، وتُعاد القيم ضمن سطر مفرد، حيث سيكون الفاصل هو "\r\n" (لا يتعلق بنظام تشغيل محدد) بين كل ترويستين، وبالتالي يمكن فصلها إلى ترويسات مفردة، وإليك مثالًا: Cache-Control: max-age=31536000 Content-Length: 4260 Content-Type: image/png Date: Sat, 08 Sep 2012 16:53:16 GMT تفصل بين اسم الترويسة وقيمتها النقطتان المتعامدتان، يليها فراغ " :"، وهذا أمر ثابت في التوصيفات، فلو أردنا مثلًا الحصول على كائن له اسم وقيمةP فلا بد من تنفيذ الأمر بالشكل التالي، مفترضين أنه عند وجود ترويستين بنفس الاسم، وعندها ستحل الأخيرة منهما مكان الأولى: let headers = xhr .getAllResponseHeaders() .split('\r\n') .reduce((result, current) => { let [name, value] = current.split(': '); result[name] = value; return result; }, {}); // headers['Content-Type'] = 'image/png' الطلب POST والكائن FormData يمكن استخدام الكائن FormData لإرسال طلب HTTP-POST: إليك الشيفرة اللازمة: let formData = new FormData([form]); //<form> إنشاء كائن يُملأ اختيارًا من formData.append(name, value); // ربط الحقل تنشئ الشيفرة السابقة الكائن FormData وتملؤه بقيم من نموذج form اختياريًا، وتضيف append حقولًا أخرى إن اقتضى الأمر، ومن ثم: نستخدم الأمر (...,'xhr.open('POST: لنؤسس الطلب POST. نستخدم الأمر (xhr.send(formData: لإرسال النموذج إلى الخادم. إليك المثال التالي: <form name="person"> <input name="name" value="John"> <input name="surname" value="Smith"> </form> <script> // form ملؤها من let formData = new FormData(document.forms.person); // إضافة حقل جديد formData.append("middle", "Lee"); // إرساله let xhr = new XMLHttpRequest(); xhr.open("POST", "/article/xmlhttprequest/post/user"); xhr.send(formData); xhr.onload = () => alert(xhr.response); </script> يُرسَل النموذج بترميز multipart/form-data، فإذا أردنا استخدام JSON فلا بدّ من تنفيذ الأمر JSON.stringify ثم إرساله في هيئة نص، إلى جانب ضبط الترويسة Content-Type على القيمة application/json، وتفكِّك العديد من إطارات العمل مع الخادم محتوى JSON تلقائيًا بهذه الطريقة. let xhr = new XMLHttpRequest(); let json = JSON.stringify({ name: "John", surname: "Smith" }); xhr.open("POST", '/submit') xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8'); xhr.send(json); ويمكن للتابع أن يُرسل أي جسم للطلب بما في ذلك كائنات Blob وBufferSource. تقدم عمليات رفع البيانات يقع الحدث progress في مرحلة التنزيل فقط، فلو نشرنا شيئًا ما باستخدام الطلب POST، فسيرفع الكائن XMLHttpRequest البيانات -جسم الطلب- أولًا ومن ثم ينزل الاستجابة. وسيكون تتبع تقدم عملية رفع البيانات خاصةً إن كانت ضخمة؛ أمرًا هامًا، لكن لن نستفيد من الحدث xhr.onprogress في حالتنا، يوجد كائن آخر لا يمتلك توابعًا، وهو مخصص حصرًا لتتبع أحداث رفع البيانات وهو xhr.upload، الذي يولِّد أحداثًا كما يفعلxhr، لكنها تقع فقط عند رفع البيانات: loadstart: يقع عندما يبدأ رفع البيانات. progress: يقع دوريًا مع تقدم الرفع. abort: يقع عند إلغاء الرفع. error: يقع عند وقوع خطأ لا يتعلق بالبروتوكول HTTP. load: يقع عند نجاح عملية الرفع. timeout: يقع عند انتهاء الوقت المخصص لرفع البيانات، إذا ضُبطت الخاصية timeout. loadend: يقع عند انتهاء الرفع بنجاح أو بإخفاق. إليك أمثلةً عن معالجات هذه الأحداث: xhr.upload.onprogress = function(event) { alert(`Uploaded ${event.loaded} of ${event.total} bytes`); }; xhr.upload.onload = function() { alert(`Upload finished successfully.`); }; xhr.upload.onerror = function() { alert(`Error during the upload: ${xhr.status}`); }; إليك أيضًا مثالًا واقعيًا عن رفع ملف مع مؤشرات على تقدم العملية: <input type="file" onchange="upload(this.files[0])"> <script> function upload(file) { let xhr = new XMLHttpRequest(); // تعقب تقدم عملية الرفع xhr.upload.onprogress = function(event) { console.log(`Uploaded ${event.loaded} of ${event.total}`); }; // تعقب الانتهاء، بنجاح أو إخفاق xhr.onloadend = function() { if (xhr.status == 200) { console.log("success"); } else { console.log("error " + this.status); } }; xhr.open("POST", "/article/xmlhttprequest/post/upload"); xhr.send(file); } </script> الطلبات ذات الأصول المختلطة يمكن للكائن XMLHttpRequest تنفيذ طلبات الأصل المختلط مستخدمًا سياسة CORS، تمامًا كما يفعل fetch، ولن يُرسل ملفات تعريف الارتباط cookies أو معلومات استيثاق إلى مواقع ذات أصل مختلف افتراضيًا. ولتمكين ذلك لا بدّ من ضبط الخاصية xhr.withCredentials على القيمة true. let xhr = new XMLHttpRequest(); xhr.withCredentials = true; xhr.open('POST', 'http://anywhere.com/request'); ... اطلع على فصل "استخدام Fetch مع الطلبات ذات الأصل المختلط" لمعلومات أكثر. خلاصة تمثل الشيفرة التالية، الشيفرة النموذجية لطلب GET باستخدام XMLHttpRequest let xhr = new XMLHttpRequest(); xhr.open('GET', '/my/url'); xhr.send(); xhr.onload = function() { if (xhr.status != 200) { // HTTP error? // معالجة الخطأ alert( 'Error: ' + xhr.status); return; } // xhr.response الحصول على الاستجابة من }; xhr.onprogress = function(event) { // إعطاء تقرير عن التقدم alert(`Loaded ${event.loaded} of ${event.total}`); }; xhr.onerror = function() { // handle non-HTTP error (e.g. network down) }; هنالك أحداث أكثر في التوصيفات الحديثة، والتي وُضِعت في قائمة مرتبة وفق دورة حياة كل حدث: loadstart: عندما يبدأ الطلب. progress: عند وصول حزمة بيانات، وتكون الاستجابة الكاملة في هذه اللحظة ضمن الاستجابة response. ()abort: عند إلغاء الطلب باستدعاء التابع. error: عند وقوع خطأ في الاتصال، مثل اسم نطاق خاطئ ولم يحدث نتيجة خطأ HTTP مثل 404. load: عند انتهاء الطلب بنجاح. timeout: عند إلغاء الطلب نتيجة تجاوز الوقت المخصص، ويحدث عندما تُضبَط هذه الخاصية. loadend: ويقع بعد الأحداث load أو error أو timeout أو abort، وهذه الأحداث متنافية فيما بينها، أي لا يمكن وقوع سوى حدث واحد منها. إن أكثر الأحداث استخدامًا هما حدث إكمال التحميل load وحدث إخفاق التحميل error، كما يمكن استعمال معالج الحدث loadend والتحقق من خصائص الكائن xhr للتأكد من طبيعة الحدث الذي وقع. لقد تعرفنا في هذا المقال أيضًا على الحدث readystatechange، والذي ظهر منذ زمن بعيد قبل استقرار التوصيفات، ولا حاجة حاليًا لاستخدامه، حيث يُستبدل بالأحداث الأكثر عصريةً. لكن مع ذلك قد نصادفه في بعض السكربتات القديمة. إذا أردنا تعقب تقدم رفع البيانات، فلا بدّ من الاستماع إلى نفس أحداث التنزيل لكن باستخدام الكائن xhr.upload. ترجمة -وبتصرف- للفصل XMLHttpRequest من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: كائنات URL في جافاسكريبت ترميز النصوص والتعامل مع كائنات الملفات في جافاسكريبت فحص الأصناف عبر instanceof في جافاسكربت هياكل البيانات: الكائنات والمصفوفات في جافاسكريبت الحياة السرية للكائنات في جافاسكريبت
-
من السهل رفع ملف باستخدام fetch، لكن كيف سنستأنف عملية الرفع بعد فقدان الاتصال؟ لا توجد خيارات مدمجة لتنفيذ هذا الأمر، لكن لدينا كل الأجزاء التي تمكننا من تنفيذ ذلك. ينبغي أن تأتي عمليات الرفع القابلة للاستئناف مع مؤشرات على تقدم العملية، خاصةً عندما يكون الملف ضخمًا -إن أردنا استئناف الرفع-، وطالما لن تسمح fetch بتعقب عملية الرفع؛ فسنحتاج إلى الكائن XMLHttpRequest. حدث تتبع غير مفيد كفاية لا بدّ من معرفة الكمية التي جرى رفعها قبل انقطاع الاتصال لاستئناف رفع الملف، حيث يمتلك الكائن xhr الحدث xhr.upload.onprogress لتتبع تقدم عملية الرفع، لكنه لن يفيدنا في استئناف الرفع -لسوء الحظ-، لأنه سيقع عند انتهاء إرسال البيانات، لكن لا يمكن للمتصفح تحديد هل استقبلها الخادم فعلًا أم لا، فمن الممكن أن تكون هذه البيانات قد خُزّنت مؤقتًا من قبل شبكة محلية وكيلة، وربما تكون العملية انتهت على الخادم البعيد قبل أن يتمكن من معالجة البيانات، أو أنها ببساطة قد فُقدت في مرحلة ما ولم تصل إلى المستقبل النهائي، لذلك لن تتعدى فائدة هذا الحدث إظهار شريط تقدم جميل على الصفحة. إذًا لا بدّ من معرفة عدد البايتات التي استقبلها الخادم بدقة حتى نتمكن من استئناف الرفع، والخادم فقط هو من يمتلك هذه المعلومة، لذا فلا بدّ من إرسال طلب إضافي. الخوارزمية ننشئ في البداية معرّفًا مميزًا id للملف الذي سنرفعه، بالشكل التالي: let fileId = file.name + '-' + file.size + '-' + file.lastModified; سنحتاج إليه لاستئناف الرفع، إذ لا بدّ من إبلاغ الخادم عن الملف الذي سنستأنف رفعه، حيث إذا تغير اسم أو حجم أو تاريخ آخر تعديل للبيانات، فسيتغيّر معرّف الملف fileId. ثم نرسل إلى الخادم طلبًا نسأله عن حجم البيانات التي وصلت إليه (مقدرًا بالبايت)، بالشكل التالي: let response = await fetch('status', { headers: { 'X-File-Id': fileId } }); // تلقى الخادم هذا العدد من البايتات let startByte = +await response.text(); هذا يفترض قدرة الخادم على تتبع تقدم عملية رفع الملف باستخدام الترويسة X-File-Id، وينبغي إنجاز ذلك في الواجهة الخلفية (من جانب الخادم)، فإذا لم يكن الملف موجودًا بعد على الخادم، فيستجيب الخادم بالرمز 0. يمكننا استخدام التابع slice العائد لكائن البيانات الثنائية Blob لإرسال الملف ابتداءً من البايت الذي سنتابع بعده startByte، بالشكل التالي: xhr.open("POST", "upload", true); // معرّف الملف ليعلم الخادم اسم الملف الذي يُرفع xhr.setRequestHeader('X-File-Id', fileId); // البايت الذي سنستأنف منه الرفع xhr.setRequestHeader('X-Start-Byte', startByte); xhr.upload.onprogress = (e) => { console.log(`Uploaded ${startByte + e.loaded} of ${startByte + e.total}`); }; //أو أي مصدر input.files[0] قد يكون مصدر الملف من xhr.send(file.slice(startByte)); وهكذا سنرسل إلى الخادم معرّف الملف عبر الترويسة X-File-Id ليعرف الملف الذي نرفعه، كما سنرسل بايت البداية عبر الترويسة X-Start-Byte ليعرف الخادم بأننا لا نرفع الملف من البداية بل نستأنف عملية رفع سابقة، ولا بدّ أن يتحقق الخادم من سجلاته. فإذا جرت عملية رفع سابقة لهذا الملف وكان حجمه الحالي مساويًا تمامًا لقيمة X-Start-Byte؛ فسيُلحِق البيانات التي يستقبلها تاليًا به. إليك تطبيقًا نموذجيًا يضم شيفرتي الخادم والعميل وقد كتبت باستخدام Node.js، ويمكنك تنزيله وتشغيله محليًا لترى آلية عمله. شيفرة الخادم "server.js": let http = require('http'); let static = require('node-static'); let fileServer = new static.Server('.'); let path = require('path'); let fs = require('fs'); let debug = require('debug')('example:resume-upload'); let uploads = Object.create(null); function onUpload(req, res) { let fileId = req.headers['x-file-id']; let startByte = +req.headers['x-start-byte']; if (!fileId) { res.writeHead(400, "No file id"); res.end(); } // we'll files "nowhere" let filePath = '/dev/null'; // يمكن استخدام مسار حقيقي عوضًا عنه، مثل: // let filePath = path.join('/tmp', fileId); debug("onUpload fileId: ", fileId); // تهيئة عملية رفع جديدة if (!uploads[fileId]) uploads[fileId] = {}; let upload = uploads[fileId]; debug("bytesReceived:" + upload.bytesReceived + " startByte:" + startByte) let fileStream; // صفرًا أو غير مهيئ فسننشئ ملفًا جديدًا، وإلا فسيختبر الحجم ويضمه إلى الملف الموجود startByte إذا كان if (!startByte) { upload.bytesReceived = 0; fileStream = fs.createWriteStream(filePath, { flags: 'w' }); debug("New file created: " + filePath); } else { // we can check on-disk file size as well to be sure if (upload.bytesReceived != startByte) { res.writeHead(400, "Wrong start byte"); res.end(upload.bytesReceived); return; } // ضمه إلى الملف الموجود fileStream = fs.createWriteStream(filePath, { flags: 'a' }); debug("File reopened: " + filePath); } req.on('data', function(data) { debug("bytes received", upload.bytesReceived); upload.bytesReceived += data.length; }); // إرسال جسم الطلب إلى الملف req.pipe(fileStream); // عندما ينتهي الطلب وتكتَب المعلومات كلها fileStream.on('close', function() { if (upload.bytesReceived == req.headers['x-file-size']) { debug("Upload finished"); delete uploads[fileId]; // يمكننا فعل شيء آخر بالملف المرفوع res.end("Success " + upload.bytesReceived); } else { // فقد الاتصال، نترك الملف غير المكتمل debug("File unfinished, stopped at " + upload.bytesReceived); res.end(); } }); // I/O أنهِ الطلب في حال خطأ fileStream.on('error', function(err) { debug("fileStream error"); res.writeHead(500, "File error"); res.end(); }); } function onStatus(req, res) { let fileId = req.headers['x-file-id']; let upload = uploads[fileId]; debug("onStatus fileId:", fileId, " upload:", upload); if (!upload) { res.end("0") } else { res.end(String(upload.bytesReceived)); } } function accept(req, res) { if (req.url == '/status') { onStatus(req, res); } else if (req.url == '/upload' && req.method == 'POST') { onUpload(req, res); } else { fileServer.serve(req, res); } } // ----------------------------------- if (!module.parent) { http.createServer(accept).listen(8080); console.log('Server listening at port 8080'); } else { exports.accept = accept; } شيفرة العميل "uploader.js": class Uploader { constructor({file, onProgress}) { this.file = file; this.onProgress = onProgress; // أنشئ معرّف الملف // يمكننا إضافة معرّف لجلسة المستخدم (عند وجوده ) لجعله فريدًا أكثر this.fileId = file.name + '-' + file.size + '-' + file.lastModified; } async getUploadedBytes() { let response = await fetch('status', { headers: { 'X-File-Id': this.fileId } }); if (response.status != 200) { throw new Error("Can't get uploaded bytes: " + response.statusText); } let text = await response.text(); return +text; } async upload() { this.startByte = await this.getUploadedBytes(); let xhr = this.xhr = new XMLHttpRequest(); xhr.open("POST", "upload", true); // send file id, so that the server knows which file to resume xhr.setRequestHeader('X-File-Id', this.fileId); // send the byte we're resuming from, so the server knows we're resuming xhr.setRequestHeader('X-Start-Byte', this.startByte); xhr.upload.onprogress = (e) => { this.onProgress(this.startByte + e.loaded, this.startByte + e.total); }; console.log("send the file, starting from", this.startByte); xhr.send(this.file.slice(this.startByte)); // return // true if upload was successful, // false if aborted // throw in case of an error return await new Promise((resolve, reject) => { xhr.onload = xhr.onerror = () => { console.log("upload end status:" + xhr.status + " text:" + xhr.statusText); if (xhr.status == 200) { resolve(true); } else { reject(new Error("Upload failed: " + xhr.statusText)); } }; // xhr.abort() فقط عندما يُستدعى onabort يقع xhr.onabort = () => resolve(false); }); } stop() { if (this.xhr) { this.xhr.abort(); } } } الملف "index.js": <!DOCTYPE HTML> <script src="uploader.js"></script> <form name="upload" method="POST" enctype="multipart/form-data" action="/upload"> <input type="file" name="myfile"> <input type="submit" name="submit" value="Upload (Resumes automatically)"> </form> <button onclick="uploader.stop()">Stop upload</button> <div id="log">Progress indication</div> <script> function log(html) { document.getElementById('log').innerHTML = html; console.log(html); } function onProgress(loaded, total) { log("progress " + loaded + ' / ' + total); } let uploader; document.forms.upload.onsubmit = async function(e) { e.preventDefault(); let file = this.elements.myfile.files[0]; if (!file) return; uploader = new Uploader({file, onProgress}); try { let uploaded = await uploader.upload(); if (uploaded) { log('success'); } else { log('stopped'); } } catch(err) { console.error(err); log('error'); } }; </script> وستكون النتيجة بالشكل التالي: تُعَد الأساليب الجديدة لإرسال الطلبات عبر الشبكة أقرب في إمكانياتها إلى إدارة الملفات كما رأينا، مثل التحكم بالترويسات ومؤشرات تقدم العمليات وإرسال الملفات وغير ذلك، وهذا ما مكّننا من تنفيذ شيفرات لاستئناف رفع الملفات وغيرها الكثير. ترجمة -وبتصرف- للفصل Resumable file upload من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا هياكل البيانات: الكائنات والمصفوفات في جافاسكريبت الحياة السرية للكائنات في جافاسكريبت ترميز النصوص والتعامل مع كائنات الملفات في جافاسكريبت
-
إحدى أسوأ الأسرار التي تمَّ الاحتفاظ بها حول AJAX على الويب هو أنّ الواجهة البرمجية (API) الأساسية لها، XMLHttpRequest، لم توجد للغرض الذي نستخدمه الآن. لقد قمنا بعمل جيد في إنشاء واجهة برمجية جيّدة باستخدام الكائن XHR ولكننا نعرف أنه يمكننا القيام بعمل أفضل. نحن نبذل الجهود لتحقيق الأفضل الذي هو الواجهة البرمجية fetch. لنأخذ فكرة عامة عن التابع window.fetch الجديد، المتوفر الآن في Firefox و Chrome Canary. الكائن XMLHttpRequest إنَّ كائن XHR معقد قليلًا برأيي ولا أريد أن أبدأ بشرح لماذا "XML" تُكتب بأحرف كبيرة بينما "Http" تُكتب بأسلوب سنام الجمل. على أيّة حال، لنلاحظ كيف نستخدم XHR. // فوضى XHR فقط الحصول على if (window.XMLHttpRequest) { // موزيلا، سفاري... request = new XMLHttpRequest(); } else if (window.ActiveXObject) { // إنترنت إكسبلورر try { request = new ActiveXObject('Msxml2.XMLHTTP'); } catch (e) { try { request = new ActiveXObject('Microsoft.XMLHTTP'); } catch (e) {} } } // فتح، إرسال request.open('GET', 'https://davidwalsh.name/ajax-endpoint', true); request.send(null); بالطبع إنَّ أطر عمل جافاسكربت الخاصة بنا تجعل XHR أكثر متعة للعمل، ولكن ما نراه في الأعلى هو مثال بسيط على فوضى XHR. استخدام fetch الأساسي تم توفير دالة fetch في نطاق window العام، الوسيط الأول له هو الرابط URL: // الرابط (إلزامي)، الخيارات (اختيارية) fetch('https://davidwalsh.name/some/url', { method: 'get' }).then(function(response) { }).catch(function(err) { // خطأ :( }); وهذا يشبه إلى حد بعيد الواجهة البرمجية Battery المحدّثة، إذ تستخدم الواجهة البرمجية fetch الوعود في جافاسكربت لمعالجة النتائج/ردود النداء: // معالجة رد بسيط fetch('https://davidwalsh.name/some/url').then(function(response) { }).catch(function(err) { // خطأ :( }); // "تسلسل لمعالجة أكثر "تطورًا fetch('https://davidwalsh.name/some/url').then(function(response) { return //... }).then(function(returnedValue) { // ... }).catch(function(err) { // خطأ :( }); إذا لم تكن معتادًا على استخدام then، يجب أن تعتاد عليه لأنها ستنتشر قريبًا في كلّ مكان. ترويسات الطلب القدرة على ضبط ترويسات الطلب هي أمر مهم في مرونة الطلب. يمكنك العمل مع ترويسات الطلب بتنفيذ new Headers(): // إنشاء كائن ترويسة فارغ var headers = new Headers(); // إضافة بعض الترويسات headers.append('Content-Type', 'text/plain'); headers.append('X-My-Custom-Header', 'CustomValue'); // للترويسة set و get و check قيم headers.has('Content-Type'); // true headers.get('Content-Type'); // "text/plain" headers.set('Content-Type', 'application/json'); // حذف ترويسة headers.delete('X-My-Custom-Header'); // إضافة قيم ابتدائية var headers = new Headers({ 'Content-Type': 'text/plain', 'X-My-Custom-Header': 'CustomValue' }); يمكنك استخدام التوابع append و has و get و set و delete لتعديل ترويسات الطلب. لاستخدام ترويسات الطلب أنشئ الكائن Request: var request = new Request('https://davidwalsh.name/some-url', { headers: new Headers({ 'Content-Type': 'text/plain' }) }); fetch(request).then(function() { /* معالجة الرد*/ }); لنطّلع على عمل الكائنين Response و Request. الكائن Request يمثّل الكائن Request جزء الطلب عند استدعاء التابع fetch، يمكنك إنشاء طلبات مخصصة ومتطورة بتمرير الكائن Request للتابع fetch: method (الطريقة): يمكن أن تكون GET أو POST أو PUT أو DELETE أو HEAD url (الرابط): رابط الطلب headers (الترويسة): ترتبط مع الكائن Headers referrer (المرجع): مرجع الطلب mode (النمط): يكون cors أو no-cors أو same-origin credentials (بيانات الاعتماد): هل تعمل ملفات تعريف الارتباط (cookies) مع الطلب؟ وتأخذ إحدى القيمتين omit، أو same-origin. redirect (إعادة التوجيه): يمكن أن يأخذ القيمة follow أو error أو manual. integrity (التكامل): قيمة تكامل المصدر الفرعي. cache (التخزين المؤقت): وضع التخزين المؤقت ويمكن أن يأخذ إحدى القيم default أو reload أو no-cache عينة عن استخدام الكائن Request: var request = new Request('https://davidwalsh.name/users.json', { method: 'POST', mode: 'cors', redirect: 'follow', headers: new Headers({ 'Content-Type': 'text/plain' }) }); // الآن استخدمه fetch(request).then(function() { /* معالجة الرد */ }); الوسيط الأول URL هو فقط الوسيط الإلزامي، وكل خاصية تصبح للقراءة فقط حالما يتم إنشاء نسخة من الكائن Request، ومن المهم أن نلاحظ أن الكائن Request يملك التابع clone المهم عند استخدام fetch ضمن الواجهة البرمجية Service Worker -- يعد الكائن Request مجرًى ولهذا يجب أن يتم نسخه عند تمريره إلى استدعاء آخر للتابع fetch. بصمة التابع fetch ولكن نفس استخدام الكائن Request لذا يمكنك القيام بما يلي: fetch('https://davidwalsh.name/users.json', { method: 'POST', mode: 'cors', redirect: 'follow', headers: new Headers({ 'Content-Type': 'text/plain' }) }).then(function() { /* معالجة الرد */ }); من المحتمل أن تستخدم نسخًا من الكائن Request فقط ضمن Service Workers بما أن الكائن Request والتابع fetch يؤديان نفس الوظيفة. الكائن Response التابع then الذي يخص fetch مزود بالكائن Response ولكن يمكنك أيضًا إنشاء كائنات Response بنفسك -- حالة أخرى قد تواجهها عند استخدام service workers. يمكنك ضبط ما يلي عند استخدام الكائن Response: type (النوع): يمكن أن يكون basic أو cors url (الرابط) useFinalURL (استخدام الرابط النهائي): قيمة منطقية للرابط url إذا كان رابطًا نهائيًا أم لا status (الحالة): رمز الحالة (مثلًا 200، 400) ok: قيمة منطقية للاستجابة الناجحة (الحالة في المجال بين 200- 299) statusText (نص الحالة): نص يعبّر عن الحالة وفقًا للرمز (مثلًا: OK) headers (الترويسة): كائن Headers المرتبط بالاستجابة // service worker أنشئ ردك لاختبار // جديد (الجسم، الخيارات) Response كائن var response = new Response('.....', { ok: false, status: 404, url: '/' }); // Response يجلب مجدَّدًا نسخة من الكائن fetch الذي يخص then التابع fetch('https://davidwalsh.name/') .then(function(responseObj) { console.log('status: ', responseObj.status); }); يوفر الكائن Response أيضًا التوابع التالية: clone(): تُنشئ نسخة من الكائن Response ()error: تعيد كائن Response جديد مرتبط مع خطأ في الشبكة ()redirect: تنشئ استجابة جديدة مع رابط URL مختلف ()arrayBuffer: تعيد وعدًا يُقبل (resolve) مع ArrayBuffer ()blob: تعيد وعدًا يُقبل (resolve) مع Blob ()formData: تعيد وعدًا يُقبل (resolve) مع كائن FormData ()json: تعيد وعدًا يُقبل (resolve) مع كائن JSON ()text: تعيد وعدًا يُقبل (resolve) مع القيمة النصية USVString التعامل مع JSON بفرض أنّك أنشأت طلب لـJSON -- معلومات نتيجة ردود النداء لديها التابع json لتحويل البيانات الخام إلى كائن جافاسكربت: fetch('https://davidwalsh.name/demo/arsenal.json').then(function(response) { // JSON التحويل إلى return response.json(); }).then(function(j) { // هو كائن جافاسكربت j console.log(j); }); بالطبع هذا أسهل من (JSON.parse(jsonString لكن طريقة json تعد اختصارًا سهلًا أيضًا. التعامل مع استجابات Text/HTML الأساسية ليست دائما JSON هي صيغة رد الطلب المرغوبة لذا إليك كيف نجعل الاستجابة على شكل نص أو HTML: fetch('/next/page') .then(function(response) { return response.text(); }).then(function(text) { // <!DOCTYPE .... console.log(text); }); بإمكانك الحصول على استجابة نصية عبر تسلسل طريقة then للوعد مع طريقة text(). التعامل مع استجابات بشكل بيانات ثنائية إذا كنت ترغب مثلًا بتحميل صورة باستخدام التابع fetch فإن هذا سيكون مختلفًا. fetch('https://davidwalsh.name/flowers.jpg') .then(function(response) { return response.blob(); }) .then(function(imageBlob) { document.querySelector('img').src = URL.createObjectURL(imageBlob); }); يأخذ التابع blob() تدفق الاستجابة وتقرأه حتى يكتمل. الحصول على بيانات النموذج استخدام آخر شائع للـAJAX وهو إرسال بيانات نموذج، وإليك كيف يمكن أن نستخدم التابع fetch للحصول على بيانات نموذج مرسلة بالطريقة POST: fetch('https://davidwalsh.name/submit', { method: 'post', body: new FormData(document.getElementById('comment-form')) }); وإذا أردت إرسال البيانات بصيغة JSON إلى الخادم: fetch('https://davidwalsh.name/submit-json', { method: 'post', body: JSON.stringify({ email: document.getElementById('email').value, answer: document.getElementById('answer').value }) }); سهل جدًا ومريح للعينين أيضًا. قصة لم تُكتب رغم أن fetch هي واجهة برمجية سهلة الاستخدام، إلا أنّ الواجهة البرمجية الحالية -تاريخ ترجمة المقال- لا تسمح بإلغاء الطلب مما يجعلها غير مستخدمة لكثير من المطورين. إن الواجهة البرمجية fetch الجديدة تبدو أفضل وأسهل استخدامًا من XHR. وبعد كل ذلك تمّ إنشاؤها حتى نتمكّن من استخدام AJAX بالطريقة الصحيحة. تملك fetch فوائد الخبرات السابقة، ولا أطيق الانتظار حتى يتم استخدامها على نطاق واسع. ترجمة -وبتصرف- للمقال fetch API لصاحبه David Walsh
-
- fetch
- xmlhttprequest
- (و 4 أكثر)