البحث في الموقع
المحتوى عن 'fetch'.
-
تتيح الدالة fetch تتبع عملية التنزيل download. لاحظ أنه لا توجد حاليًا طريقة تسمح للدالة fetch بتتبع عملية الرفع upload، نستخدم لهذه الغاية الكائن XMLHttpRequest الذي سنغطيه لاحقًا. تُستخدم الخاصية response.body لتتبع تقدم التنزيل، وتمثل هذه الخاصية كائن ReadableStream، وهو كائن خاص يزودنا عند وصوله بجسم الطلب كتلةً بكتلة chunk-by-chunk، ستجد وصفًا لمجاري التدفق القابلة للقراءة Readable streams في توصيف الواجهة Streams API، وتمنح الخاصية response.body تحكمًا كاملًا بعملية القراءة على خلاف التابعين ()response.text و()response.json وغيرهما. كما تمنح إمكانية تقدير الوقت المستغرق في أية لحظة. إليك مثالُا عن شيفرة تقرأ الاستجابة من response.body: // والطرق الأخرى response.json() بدلا من const reader = response.body.getReader(); // حلقة لا نهائية حتي يكتمل التنزيل while(true) { // عند آخر جزء true القيمة done ستحمل //لبايتات كل جزء Unit8Array هو value const {done, value} = await reader.read(); if (done) { break; } console.log(`Received ${value.length} bytes`) } ستكون نتيجة الاستدعاء ()await reader.read كائنًا له الخاصيتان التاليتان: done : تأخذ القيمة true عندم اكتمال عملية القراءة، وإلا فستكون قيمتها false. value : مصفوفة من النوع Uint8Array. //واحصل على قارئ للبيانات Fetch الخطوة1: إبدأ تنفيذ الدالة let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits?per_page=100'); const reader = response.body.getReader(); // الخطوة2: احصل على الحجم الكلي const contentLength = +response.headers.get('Content-Length'); // الخطوة3 : إقرأ البيانات let receivedLength = 0; // حجم البايتات المستقبلة حتى اللحظة let chunks = []; // مصفوفة الأجزاء المستلمة التي تمثل جسم الاستجابة while(true) { const {done, value} = await reader.read(); if (done) { break; } chunks.push(value); receivedLength += value.length; console.log(`Received ${receivedLength} of ${contentLength}`) } // الخطوة 4: ضم الأجزاء في مصفوفة واحدة let chunksAll = new Uint8Array(receivedLength); // (4.1) let position = 0; for(let chunk of chunks) { chunksAll.set(chunk, position); // (4.2) position += chunk.length; } // الخطوة5: الترميز في سلسلة نصية let result = new TextDecoder("utf-8").decode(chunksAll); // النهاية let commits = JSON.parse(result); alert(commits[0].author.login); المخرجات: "Received 258566 of 0" "Received 444982 of 0" لنشرح الشيفرة السابقة: لقد نفّذنا الدالة ftech، لكننا استخلصنا مجرى التدفق ()reader response.body.getReader بدلًا من استدعاء التابع ()response.json، ولايمكن استخدام الطريقتين معًا لقراءة الاستجابة، استخدم إحداهما للحصول على النتيجة. يمكننا قبل الشروع في قراءة الاستجابة تحديد الحجم الكلي لها عن طريق الترويسة Content-Length، وقد لا تكون الترويسة موجودةً في الطلبات ذات الأصل المختلط Cross-origin، لكن لن يُعدَّها الخادم عمليًا، وستبقى في مكانها. نستدعي التابع ()await reader.read حتى ينهي عمله، ونُجمِّع أجزاء الاستجابة في المصفوفة chunks، وهذا الأمر ضروري لأن الاستجابة ستختفي ولن نتمكن من إعادة قراءتها باستخدام ()response.json ولا بأي طريقة أخرى، وستحصل على خطأ إذا حاولت ذلك. سنحصل في النهاية على chunks وهي مصفوفة من الأجزاء لها النوع Uint8Array، وعلينا تجميعها ضمن نتيجة واحدة، ولسوء الحظ لا يوجد تابع لضمها، لهذا علينا كتابة الشيفرة التي ستنجز العملية: إنشاء المصفوفة (chunksAll = new Uint8Array(receivedLength، وهي مصفوفة من النوع Uint8Array لها حجم جميع الأجزاء. استخدام التابع (set(chunk, position. لنسخ كل جزء بدوره إليها. سنحصل على النتيجة ضمن المصفوفة chunksAll، وهي مصفوفة من البايتات وليست نصًا، ولتحويلها إلى نص لا بد من تفسير هذه البيانات عن طريق الكائن TextDecoder، ثم استخدام JSON.parse إن استدعت الحاجة. لكن ماذا لو احتجنا إلى محتوىً ثنائي بدل النص؟ سيكون الأمر أبسط، علينا فقط استبدال الخطوتين 4 و5 بسطر وحيد يُنشئ كائن بيانات ثنائية Blob يضم كل الأجزاء. let blob = new Blob(chunks); وهكذا سنحصل على النتيجة بالصيغة التي نريد مع إمكانية تتبع تقدم العملية، مرةً أخرى يجب الانتباه إلى أن العملية غير صالحة لتتبع تقدم عملية الرفع، أي لا يمكن استخدام Fetch، بل فقط للتنزيل. ولا بد من التحقق من حجم البيانات الواصلة receivedLength في كل لحظة ضمن الحلقة وإنهائها بمجرد وصولها إلى حد معين، إذا لم يكن حجم البيانات التي سنستقبلها معروفًا، وبالتالي لن تستهلك المصفوفة chunks الذاكرة. الكائن AbortController: مقاطعة العمليات غير المتزامنة تعيد fetch كما نعرف وعدًا promise، ولكننا نعلم أنّ JavaScript لا تقبل إلغاء الوعود عمومًا، فكيف سنلغي عملية fetch أثناء تنفيذها؟ هنالك كائن خاص مدمج لهذا الغرض هو AbortController، يمكن استخدامه لإلغاء fetch وغيرها من المهام غير المتزامنة، ويطبّق مباشرةً. لننشئ متحكمًا بالشكل التالي: let controller = new AbortController(); والمتحكم هو كائن شديد البساطة، له: تابع وحيد هو ()abort. وخاصية واحدة هي signal تسمح بإعداد مستمع حدث event listener له. أما عند استدعاء التابع ()abort فسيحدث الآتي: تحرّض الخاصية controller.signal وقوع الحدث abort. تأخذ الخاصية controller.signal.aborted القيمة true. تكون للعملية في العادة مرحلتان: المرحلة التي تُنفِّذ عمليةً قابلة للإلغاء، وتهيئ مستمع حدث للخاصية controller.signal. المرحلة التي تلغي: وتُنفَّذ باستدعاء التابع ()controller.abort عندما يتطلب الأمر. إليك مثالًا كاملًا دون Fetch: let controller = new AbortController(); let signal = controller.signal; // القيام بعملية قابلة للإلغاء // "signal" الحصول على الكائن // controller.abort() ضبط إطلاق المستمع عند استدعاء signal.addEventListener('abort', () => alert("abort!")); // القيام بالإلغاء controller.abort(); // abort! // true القيمة signal.aborted إطلاق الحدث ويصبح لــ alert(signal.aborted); // true لاحظ أنّ الكائن AbortController هو مجرد أداة لتمرير الحدث abort عند استدعاء التابع ()abort، ومن الواضح أنه بالإمكان تنفيذ مستمع حدث كهذا باستخدام شيفرتنا الخاصة دون الحاجة إلى AbortController، لكن أهميته ستظهر عندما نعلم أن fetch تعرف تمامًا كيفية التعامل معه، فهو متكامل معها. مقاطعة العملية Fetch لإلغاء العملية fetch علينا تمرير قيمة الخاصية signal العائدة للكائن AbortController مثل خيار لها: let controller = new AbortController(); fetch(url, { signal: controller.signal }); تعلم fetch تمامًا كيفية التعامل مع AbortController، وستستمع إلى الحدث abort الذي تحرّض الخاصية signal وقوعه. لإلغاء العملية سنستدعي التابع: controller.abort(); وهكذا تلغى العملية، حيث تحصل fetch على الحدث abort من الخاصية signal وتلغي الطلب، عند إلغاء fetch سيُرفض الوعد الذي تعيده وسيُرمى الخطأ AbortError، وينبغي التعامل معه من خلال حلقة try..catch مثلًا. إليك مثالًا كاملًا مع استخدام fetch، حيث تلغى العملية بعد ثانية واحدة: // الإلغاء خلال ثانية واحدة let controller = new AbortController(); setTimeout(() => controller.abort(), 1000); try { let response = await fetch('/article/fetch-abort/demo/hang', { signal: controller.signal }); } catch(err) { if (err.name == 'AbortError') { // handle abort() alert("Aborted!"); } else { throw err; } } كائن قابل للتوسع يسمح الكائنAbortController بإلغاء عدة عمليات معًا، إليك الشيفرة التمثيلية التالية التي تحضر عدة موارد على التوازي، ثم تستخدم كائن متحكم وحيدًا لإلغائها جميعًا: let urls = [...]; //قائمة بالموارد التي ينبغي إحضارها let controller = new AbortController(); // fetch مصفوفة من الوعود التي ستعيدها عمليات let fetchJobs = urls.map(url => fetch(url, { signal: controller.signal })); let results = await Promise.all(fetchJobs); // بمجرد استدعاء حدث الإلغاء ستلغى جميع عمليات الإحضار وسنتمكن أيضًا من إلغاء أي عمليات أخرى غير متزامنة مع عمليات fetch باستخدام كائن AbortController وحيد، بمجرد الاستماع إلى الحدث abort: let urls = [...]; let controller = new AbortController(); let ourJob = new Promise((resolve, reject) => { // المهمة المطلوب إلغاءها ... controller.signal.addEventListener('abort', reject); }); let fetchJobs = urls.map(url => fetch(url, { // عمليات الإحضار signal: controller.signal })); // انتظار إنجاز جميع العمليات let results = await Promise.all([...fetchJobs, ourJob]); // بمجرد استدعاء حدث الإلغاء ستلغى جميع عمليات الإحضار // بالإضافة إلى بقية المهام خلاصة بهذا نكون قد تعرفنا على كيفية تتبع عملية التنزيل باستخدام Fetch، وذلك بالاعتماد على عدة خاصيات، كما تعرفنا على كيفية مقاطعة العملية Fetch، وذلك بالاعتماد على الكائنات الآتية: AbortController: هو كائن بسيط يولّد الحدث abort على الخاصية signal عند استدعاء التابع ()abort، الذي يعطي الخاصية signal.aborted القيمة "true" أيضًا. تتكامل fetch مع هذا الكائن، حيث تُمرر الخاصية signal كخيار لتستمع إليه، وبالتالي يصبح إلغاؤها ممكنًا. يمكن استخدام AbortController في شيفرتنا، حيث يستمع التابع ()abort إلى الحدث abort بعملية بسيطة تطبق في أي مكان، كما يمكن استخدامها دون استخدام fetch. ترجمة -وبتصرف- للفصلين Fetch: Download Progress وFetch: Abort من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا ترميز النصوص والتعامل مع كائنات الملفات في جافاسكريبت
-
من السهل رفع ملف باستخدام 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. اقرأ أيضًا هياكل البيانات: الكائنات والمصفوفات في جافاسكريبت الحياة السرية للكائنات في جافاسكريبت ترميز النصوص والتعامل مع كائنات الملفات في جافاسكريبت
-
لقد أخذنا فكرةً لا بأس بها عن fetch في المقالات السابقة من هذه السلسلة (بدءًا من مقال إرسال البيانات واستلامها عبر الشبكة وحتى مقال استخدام Fetch مع الطلبات ذات الأصل المختلط Cross-Origin)، والآن لنلق نظرةً على بقية مكوّنات الواجهة البرمجية لنغطي كل إمكاناتها. إليك قائمةً كاملةً بكل خيارت fetch الممكنة مع قيمها الافتراضية (وضعنا البدائل في تعليقات): let promise = fetch(url, { method: "GET", // POST, PUT, DELETE, etc. headers: { // the content type header value is usually auto-set // depending on the request body "Content-Type": "text/plain;charset=UTF-8" }, body: undefined // string, FormData, Blob, BufferSource, or URLSearchParams referrer: "about:client", // or "" to send no Referer header, // or an url from the current origin referrerPolicy: "no-referrer-when-downgrade", // no-referrer, origin, same-origin... mode: "cors", // same-origin, no-cors credentials: "same-origin", // omit, include cache: "default", // no-store, reload, no-cache, force-cache, or only-if-cached redirect: "follow", // manual, error integrity: "", // a hash, like "sha256-abcdef1234567890" keepalive: false, // true signal: undefined, // AbortController to abort request window: window // null }); لقد غطينا المفاهيم method وheaders وbody في مقال استخدام Fetch، كما غطينا signal في مقال إيقاف تنفيذ Fetch، وسنتعرف الآن على بقية الإمكانات. خيارا المحيل referrer وسياسة المحيل referrerPolicy يتحكم هذان الخياران بكيفية ضبط fetch للترويسة Referrer، وهي إحدى ترويسات HTTP، وتُضبط تلقائيًا لتحتوي على عنوان الصفحة التي ولّدت الطلب، ولا تعَد هامةً في معظم الأحيان، وقد يكون من المنطقي أحيانًا إزالتها أو تقصيرها لأسباب تتعلق بالأمان. يسمح الخيار referrer بتسمية أي مُحيل، وهو الصفحة أو الرابط الذي أحالك إلى الصفحة الحالية التي تعمل عليها، أو إزالته على أن يشترك بالأصل مع الصفحة الحالية، وإذا لم ترغب بإرسال أي محيل فأسند إليه نصًا فارغًا: fetch('/page', { referrer: "" // لا توجد توريسة محيل }); ولوضع عنوان مورد آخر من الأصل ذاته: fetch('/page', { // https://javascript.info بفرض أننا في // نستطيع ضبط أي ترويسة محيل، لكن ضمن الأصل الحالي referrer: "https://javascript.info/anotherpage" }); يضبط الخيار referrerPolicy بعض القواعد العامة للمُحيل Referer. تنقسم الطلبات إلى ثلاث مجموعات، هي الآتية: الطلب إلى مورد من الأصل ذاته. الطلب إلى مورد من أصل مختلف. الطلب من بروتوكول HTTPS إلى بروتوكول HTTP: أي من بروتوكول النقل الآمن إلى غير الآمن. يدل الخيار referrerPolicy المتصفح على القواعد الخاصة باستخدام المُحيل في كل مجموعة من الطلبات، ولا يسمح بضبط القيمة الدقيقة للمحيل. ستجد جميع القيم الممكنة في توصيف سياسة المحيل: no-referrer-when-downgrade: قيمتها الافتراضية "full"، حيث تُرسل قيمة المحُيل دومًا عدا الحالة التي يُرسَل فيها الطلب من HTTPS إلى HTTP (إلى بروتوكول أقل أمانًا). no-referrer: لا يُرسَل المُحيل. origin: يُرسل الأصل فقط ضمن المُحيل وليس عنوان الصفحة المُحيلة الكامل، أي يُرسل العنوان على الشكل http://site.com وليس على الشكل http://site.com/path. origin-when-cross-origin: يُرسل العنوان الكامل للمحيل إلى المواقع ذات الأصل المشترك، بينما يُرسَل الأصل فقط إلى المواقع ذات الأصل المختلط. same-origin: يُرسل المُحيل كاملًا إلى المواقع التي تنتمي إلى نفس الأصل، ولايرُسل أبدًا إلى المواقع ذات الأصول المختلطة. strict-origin: يُرسل الأصل فقط وليس المُحيل كاملًا في الطلبات من HTTPS إلى HTTP. strict-origin-when-cross-origin: يُرسل المُحيل كاملًا إلى المواقع التي تنتمي إلى نفس الأصل، ويُرسل الأصل فقط إلى المواقع ذات الأصول المختلطة، عدا الحالة التي يُرسَل فيها الطلب من HTTPS إلى HTTP، فلا يُرسل شيء. unsafe-url: يُرسل عنوان المُحيل كاملًا، حتى في الحالة التي يُرسَل فيها الطلب من HTTPS إلى HTTP. يوضح الجدول التالي جميع الخيارات: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } القيمة إلى نفس الأصل إلى أصل مختلف HTTPS→HTTP "no-referrer" - - - no-referrer-when-downgrade أو "" وهي القيمة الافتراضية كاملًا كاملًا - "origin" الأصل الأصل الأصل "origin-when-cross-origin" كاملًا الأصل الأصل "same-origin" كاملًا - - "strict-origin" الأصل الأصل - "strict-origin-when-cross-origin" كاملًا الأصل - "unsafe-url" كاملًا كاملًا كاملًا لنفترض وجود صفحة بصلاحيات مدير، ولا ينبغي كشف عنوانها خارج نطاق الموقع، لذا فعند إرسال fetch، فسترسَل الترويسة Referer افتراضيًا مع عنوان صفحتنا كاملًا، عدا الحالة التي يُرسَل فيها الطلب من HTTPS إلى HTTP. حيث لا توجد أي ترويسة Referer، فإذا كان العنوان هو Referer: https://javascript.info/admin/secret/paths مثلًا، وأردنا إرسال الأصل فقط وليس العنوان الكامل، فيمكن أن نرسل الخيار التالي: fetch('https://another.com/page', { // ... referrerPolicy: "origin-when-cross-origin" // Referer: https://javascript.info }); يمكن وضع الخيار السابق لكل استدعاءات fetch، كما يمكن أيضًا دمجه في مكتبة JavaScript التي نستخدمها في مشروعنا، والتي تنفّذ كل الطلبات التي تستخدم fetch، ويقتصر الفرق الوحيد بينه وبين الخيار الافتراضي في أنه يرسِل الجزء الأصلي من عنوان الموقع المُحيل، مثلًا: https://javascript.info ولا يُرسل المسار الكلي، وسنحصل على العنوان الكامل في الطلبات المُرسَلة إلى مواقع من نفس الأصل، فربما تكون مفيدةً لأغراض التنقيح. الخيار mode ويمثل هذا الخيار الحارس الذي يمنع الطلبات ذات الأصل المختلط التي تحدث فجأةً. cors: وهي القيمة الافتراضية، وتسمح بالطلبات ذات الأصل المختلط كما ورد في فصل استخدام Fetch في الطلبات ذات الأصل المختلط. same-origin: يمنع استخدام الطلبات ذات الأصل المختلط. no-cors: يسمح فقط لطلبات الأصل المختلط الآمنة. قد تظهر أهمية هذا الخيار عندما يأتي العنوان القادم مع fetch من طرف ثالث، ونريد آليةً للحد من الإمكانات المسموحة للأصول المختلطة. الخيار credentials ويحدد ما إذا كان على fetch إرسال ملفات تعريف الارتباط cookies، وترويسات استيثاق HTTP مع الطلب. same-origin: وهي القيمة الافتراضية، لا تُرسل الثبوتيات مع الطلبات ذات الأصول المختلطة. include: تُرسل الثبوتيات دومًا، ونحتاج إلى الترويسة Access-Control-Allow-Credentials من الخادم ذي الأصل المختلط لتتمكن جافا سكربت من الوصول إلى الاستجابة، وقد شرحنا ذلك في فصل استخدام Fetch في الطلبات ذات الأصل المختلط. omit: لا تُرسل الثبوتيات أبدًا، حتى للطلبات من الأصل نفسه. الخيار cache تستخدم طلبات fetch افتراضيًا ذاكرة HTTP المؤقتة المعيارية HTTP-cache، فهي تحترم الترويستين Expires وCache-Control، وترسل الترويسة If-Modified-Since تمامًا كما تفعله طلبات HTTP النظامية. يسمح الخيار cache بتجاهل "HTTP-cache" أو يضبط استخدامه: default: تستخدم fetch ترويسات وقواعد "HTTP-cache" المعيارية. no-store: يتجاهل الطلب قواعد "HTTP-cache" كليًا، وتصبح هذه القيمة افتراضيةً عند إرسال إحدى الترويسات التالية: If-Modified-Since أو If-None-Match أو If-Unmodified-Since أو If-Match أو If-Range. reload: لا يأخذ النتيجة من "HTTP-cache" -إن وجدت-، بل ينشر محتويات الذاكرة المؤقتة مع الاستجابة، إذا سمحت ترويسات الاستجابة بذلك. no-cache: يُنشئ طلبًا شرطيًا عند وجود استجابة مخزنة في الذاكرة المؤقتة، وطلبًا عاديًا في غير تلك الحالة، وينشر "HTTP-cache" مع الاستجابة. force-cache: يستخدم الاستجابة الموجودة في "HTTP-cache" حتى لو كانت قديمة، وسينشئ طلب HTTP نظاميًا إذا لم تحتوي على استجابة، كما سيسلك الطلب السلوك الطبيعي. only-if-cached: يستخدم الاستجابة الموجودة في "HTTP-cache" حتى لو كانت قديمةً، وسيرمي خطأً إذا لم تحتوي على استجابة، وتعمل فقط مع القيمة same-origin للخيار mode. الخيار redirect تخضع fetch بكل شفافية لإعادة التوجيه "HTTP-redirect" مثل الحالتين 301 (النقل النهائي لمحتوى) و302 (موجود ولكن يفضل الانتقال إلى العنوان الجديد). follow: وهي القيمة الافتراضية، ويخضع الطلب عندها لحالات إعادة التوجيه. error: يرمي خطأً عند محاولة إعادة توجيه الطلب. manual: يسمح بالتعامل مع إعادة توجيه الطلب يدويًا، وسنحصل عندها على كائن استجابة خاص من النوع "response.type="opaqueredirect، وتكون قيمة خاصية الحالة response.status صفرًا، وكذلك قيم أغلب خصائصه. الخيار integrity يسمح هذا الخيار بالتحقق من مطابقة الاستجابة للقيم الاختبارية Checksum المحددة مسبقًا، كما هو محدد في التوصيفات. وتُدعم دوال "hash" التالية: SHA-256 وSHA-384 وSHA-512، كما قد تتطلب بعض الاعتماديات وفقًا للمتصفح، فإذا كنا بصدد تنزيل ملف مثلًا، ونعلم أنّ القيمة الاختبارية له وفق SHA-256 هي "abcdef"، والتي ستكون أطول في الواقع، فيمكننا وضعها قيمةً للخيار integrity بالشكل التالي: fetch('http://site.com/file', { integrity: 'sha256-abcdef' }); ستحسب fetch قيمة SHA-256 بنفسها وتوازنها مع القيمة التي وضعناها، وسترمي خطأً عند عدم تطابق القيمتين. الخيار keepalive يسمح هذا الخيار ببقاء الطلب فعالًا خارج الصفحة التي أنشأتها. لنفترض مثلًا أننا نجمع إحصائيات عن سلوك المستخدم الحالي لصفحتنا (عدد نقرات الفأرة وأجزاء الصفحة التي زارها) لتحليل تجربة المستخدم وتطويرها، ونرغب بحفظ البيانات على الخادم عندما يغادر هذا المستخدم الصفحة، حيث يمكن أن ننفذ ذلك باستخدام الحدث window.onunload بالشكل التالي: window.onunload = function() { fetch('/analytics', { method: 'POST', body: "statistics", keepalive: true }); }; لكن ستُغلق كل طلبات الشبكة المتعلقة بالمستند عند إزالته، وهنا تظهر أهمية الخيار keepalive الذي يخبر المتصفح بإبقاء الطلبات حيةً في الخلفية حتى بعد أن يغادر الزائر الصفحة، لأن هذا الخيار أساسي لاستمرار الطلب ونجاحه. لكن بالطبع هناك بعض التقييدات في استخدامه، والمتمثلة في الآتي: لا يمكن إرسال أحجام بالميجابايت: لأن الحد الأعلى لحجم جسم الطلب مع خيار keepalive هو 64 كيلوبابت. إذا أردنا جمع إحصائيات كثيرةً عن الزائر، فلا بدّ من إرسالها بانتظام ضمن حزم متتالية، لكي لا تبقى الكثير من المعلومات التي لم ترسل بعد عند تنفيذ الطلب الأخير مع الحدث onunload. تطبق هذه التقييدات على كل الطلبات التي تحمل الخيار keepalive معًا، أي يمكن تنفيذ عدة طلبات من هذا النوع في الوقت نفسه، لكن يجب ألا يتجاوز مجموع أحجام أجسام هذه الطلبات حد 64 كيلوبايت. لا يمكن التعامل مع استجابة الخادم عند إزالة المستند، لذا سينجح استخدام fetch في مثالنا بوجود keepalive، لكن بالطبع لن تُنفَّذ الدوال اللاحقة. لن تظهر المشاكل في أغلب الأحيان عند إرسال بيانات مثل الإحصائيات، لأنّ الخادم سيقبل هذه البيانات وسيعيد غالبًا استجابةً فارغةً لطلبات مثل هذه. ترجمة -وبتصرف- للفصل Fetch: API من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: استخدام Fetch مع الطلبات ذات الأصل المختلط في جافاسكريبت
-
من المحتمل أن يخفق الطلب fetch المرسَل إلى موقع ويب آخر، مثلًا: ستخفق محاولة الحصول على http://example.com: try { await fetch('http://example.com'); } catch(err) { alert(err); // Failed to fetch } لا بدّ من الإشارة أولًا إلى المفاهيم البنيوية لموضوعنا: الأصل origin: وهو الثلاثية (نطاق ومنفذ وبروتوكول). الطلبات ذات الأصل المختلط cross-origin requests: وهي الطلبات المرسَلة إلى نطاق (أو نطاق فرعي) آخر أو عبر منفذ آخر أو باستخدام بروتوكول آخر، وتتطلب ترويسات خاصةً من الجانب البعيد. تدعى هذه السياسة "CROS" وهو اختصار للعبارة "Cross-Origin Resource Sharing"، وتعني مشاركة الموارد ذات الأصول المختلطة. لماذا نحتاج إلى CROS؟ لمحة تاريخية موجزة وُجِدت هذه السياسة لحماية الإنترنت من المخترقين، فلسنوات عديدة لم يكن مسموحًا لسكربت من موقع ما أن يصل إلى محتوى موقع آخر، حيث لا يمكن لسكربت مشبوه من الموقع hacker.com مثلًا الوصول إلى صندوق البريد الإلكتروني لمستخدم على الموقع gmail.com، مما أشعَر مستخدمي الإنترنت في ذلك الوقت بالأمان لعدم امتلاك جافاسكريبت JavaScript، أي توابع خاصة لتنفيذ طلبات عبر الشبكة، فقد كانت عبارةً عن لغة للتسلية وتزيين صفحات الويب، إلا أن مطوري الويب احتاجوا سريعًا إلى قوة أكبر للتحكم بالصفحات، فاخترعوا أساليب متنوعةً للالتفاف على محدودية اللغة وإرسال الطلبات إلى مواقع أخرى. استخدام النماذج لقد كانت إحدى طرق التواصل مع خادم آخر هي إرسال نموذج <form> إليه، وذلك باستخدام الإطارات <iframe> لإبقاء الزوار ضمن الصفحة نفسها: <!-- form target --> <iframe name="iframe"></iframe> <!-- a form could be dynamically generated and submited by JavaScript --> <form target="iframe" method="POST" action="http://another.com/…"> ... </form> وهكذا تمكن الناس من إرسال طلبات GET/POST إلى مواقع أخرى دون وجود توابع لتنفيذ ذلك، لأنّ النماذج قادرة على إرسال البيانات إلى أي مكان، لكن لم يكن بالإمكان الحصول على الاستجابة لأنّ الوصول إلى محتويات الإطار <iframe> غير مسموح، ولنكون دقيقين؛ وُجِدت بعض الحيل للالتفاف على ذلك أيضًا، لكنها تطلبت سكربتًا خاصًا يوضع ضمن الإطار والصفحة، أي صار التواصل بينهما ممكنًا من الناحية التقنية. استخدام السكربتات اعتمدت إحدى الحيل المستخدَمة أيضًا على المعرّف <script>، إذ يمكن أن تكون قيمة الخاصية src لسكربت هي اسم أي نطاق أو موقع مثل: <script src="http://another.com/…"> وبالتالي يمكن تنفيذ سكربت أيًا كان مصدره، فإذا أراد موقع ما مثل another.com إتاحة أمكانية الوصول لبياناته، فسيستخدم البروتوكول "JSON with padding" واختصاره JSNOP، وإليك آلية عمله: لنفترض أننا نريد الوصول إلى بيانات الطقس على الموقع http://another.com إنطلاقًا من موقعنا: نعرّف في البداية دالةً عامةً لاستقبال البيانات ولتكن gotWeather. // صرح عن الدالة التي ستعالج البيانات المطلوبة function gotWeather({ temperature, humidity }) { alert(`temperature: ${temperature}, humidity: ${humidity}`); } ننشئ <script>، وتكون قيمة الخاصية src فيه هي src="http://another.com/weather.json?callback=gotWeather" وللمستخدمين اسم الدالة العامة كقيمة للمعامل callback الخاص بالعنوان URL. let script = document.createElement('script'); script.src = `http://another.com/weather.json?callback=gotWeather`; document.body.append(script); يوّلد الخادم البعيد another.com ديناميكيًا سكربتًا يستدعي الدالة ()gotWeatherبالبيانات التي يريدنا أن نحصل عليها. // ستبدو الاستجابة التي نتوقعها من الخادم كالتالي gotWeather({ temperature: 25, humidity: 78 }); عندما يُحمَّل السكربت الذي يولده الخادم ويُنفَّذ، ستُنفَّذ الدالة gotWeather ونحصل على البيانات. سيعمل الأسلوب السابق ولن يشكل خرقًا لأمن الموقع البعيد بسبب اتفاق كلا الطرفين على تبادل المعلومات بهذا الشكل، ولهذا لن تُعَدَّ العملية عندها اختراقًا، ولا زالت بعض الخدمات تتبع نفس الأسلوب في الوصول إلى البيانات البعيدة وتعمل حتى على المتصفحات القديمة جدًا. ظهرت بعد فترة من الزمن ضمن لغة JavaScript توابع الطلبات عبر الشبكة والتي ينفذها المتصفح، وقد رفضت الطلبات ذات الأصول المختلطة في البداية، إلا أنه سُمح باستخدامها نتيجة نقاشات طويلة، بشرط الحصول على سماحيات صريحة من الخادم لتنفيذ أي متطلبات، ويُعبَّر عنها من خلال ترويسات خاصة. الطلبات الآمنة هنالك نوعان من الطلبات ذات الأصل المختلط: الطلبات الآمنة safe requests. بقية الأنواع. من السهل إنشاء الطلبات الآمنة لذلك سنبدأ بها، إذ يُعَد الطلب آمنًا إذا حقق الشرطين التاليين: يستخدم نوعًا آمنًا مثل GET أو POST أو HEAD. يستخدم ترويسات آمنةً. ويسمح بالترويسات المخصصة التالية فقط: Accept. Accept-Language. Content-Language. Content-Type: بحيث تحمل إحدى القيم التالية application/x-www-form-urlencoded أو multipart/form-data أو text/plain. وتُعَد بقية الطلبات "غير آمنة"، حيث لا تطابق الطلبات باستخدام PUT أو باستخدام الترويسة API-Key معايير الأمان السابقة مثلًا. ويكمن الفرق الجوهري في إمكانية تنفيذ الطلبات الآمنة باستخدام معرِّف النموذج <form> أو معرّف السكربت <script> دون الحاجة لأي توابع خاصة، وبالتالي ستكون أقدم الخوادم قادرةً على استقبالها. لا يمكن في المقابل استخدام الطلبات التي لا تمتلك ترويسات معياريةً مثل DELETE بهذه الطريقة، ولفترة طويلة لم تكن JavaScript قادرةً على استخدام هذا النوع من الطلبات، وهكذا سيفترض الخادم القديم أن هذه الطلبات قادمة من مصدر مخوّل بذلك، لأنه يتوقع أنّ صفحة الويب غير قادرة على إرسال هذه الطلبات. عندما نرسل طلبًا غير آمن، فسيرسل المتصفح طلبًا تمهيديَا preflight، سيسأل الخادم فيه إن كان سيوافق على طلبات ذات أصول مختلطةً أم لا، فإن لم يؤكد الخادم ذلك صراحةً من خلال الترويسات، فلن يُرسَل الطلب غير الآمن. سياسة CROS للطلبات غير الآمنة سيضيف المتصفح دائمًا الترويسة Origin إلى الطلب من الأصول المختلطة، فإذا طلبنا المورد https://anywhere.com/request من الموقع https://javascript.info/page مثلًا؛ فستبدو الترويسات بالشكل التالي: GET /request Host: anywhere.com Origin: https://javascript.info ... تحتوي الترويسة origin كما نرى الأصل (نطاق وبروتوكول ومنفذ) كما هو لكن دون مسار، ويمكن للخادم أن يتحقق من الترويسة Origin، فإن وافق على قبول هذا الطلب، فسيضيف ترويسةً خاصةً هي Access-Control-Allow-Origin إلى الاستجابة، وينبغي أن تحتوي الترويسة على الأصل المقبول (https://javascript.info في حالتنا) أو رمز النجمة (*)، عندها سيكون الطلب ناجحًا وإلا فسيُعَد خاطئًا. يلعب المتصفح دور الوسيط الموثوق حيث: يضمن إرسال الأصل الصحيح في الطلب ذي الأصول المختلطة. يتحقق من وجود السماحية Access-Control-Allow-Origin في الاستجابة، فإذا وُجدَت فسيسمح لشيفرة JavaScript بالوصول إلى الاستجابة وإلا ستخفق العملية وسيحدث خطأ. 200 OK Content-Type:text/html; charset=UTF-8 Access-Control-Allow-Origin: https://javascript.info ترويسات الاستجابة افتراضيًا قد لا تتمكن JavaScript من الوصول إلا إلى الترويسات الآمنة للاستجابة عند إرسال طلبات ذات أصل مختلط، وهذه الترويسات هي: Cache-Control. Content-Language. Content-Type. Expires. Last-Modified. Pragma. ويسبب الدخول إلى أي ترويسات أخرى خطأً. لمنح إمكانية الوصول إلى ترويسة الاستجابة، ينبغي أن يُرسل الخادم الترويسة Access-Control-Expose-Headers، والتي تتضمن قائمةً بأسماء الترويسات غير الآمنة التي يُفترض جعلها قابلةً للوصول، وتفصل بينها فاصلة، كالمثال التالي: 200 OK Content-Type:text/html; charset=UTF-8 Content-Length: 12345 API-Key: 2c9de507f2c54aa1 Access-Control-Allow-Origin: https://javascript.info Access-Control-Expose-Headers: Content-Length,API-Key وبوجود ترويسة مثل Access-Control-Expose-Headers سيُسمح للسكربت بقراءة ترويستي الاستجابة Content-Length وAPI-Key. الطلبات غير الآمنة يمكن استخدام جميع طلبات HTTP مثل PATCH وDELETE وغيرها، وليس فقط GET/POST، ولم يتخيل أحد في السابق إمكانية تنفيذ صفحات الويب لهذه الطلبات، لذلك قد تجد بعض خدمات الويب التي تعامل الطلبات غير المعيارية مثل إشارة "بأنها طلبات مصدرها ليس المتصفح"، ويمكنها أن تأخذ هذا الأمر في الحسبان عندما تتحقق من حقوق الوصول، ولتفادي سوء الفهم، لن ينفِّذ المتصفح أي طلبات غير آمنة كانت قابلة للتنفيذ مباشرةً فيما مضى، وسيرسل طلبًا تمهيديًا preflight إلى الخادم لطلب الإذن، ويستخدم الطلب التمهيدي التابع OPTIONS دون جسم للطلب، وبوجود ترويستين: الترويسة Access-Control-Request-Method: وتؤمن تابعًا للطلب غير الآمن. الترويسة Access-Control-Request-Headers: وتؤمن قائمةً بترويسات غير آمنة تفصل بينها فاصلة. إذا وافق الخادم على تنفيذ الطلبات، فسيرسل استجابةً بجسم فارغ ورمز الحالة 200، بالإضافة إلى الترويسات التالية: Access-Control-Allow-Origin: ويجب أن تحمل القيمة (*) أو أصل الموقع الذي أرسل الطلب، مثل "https://javascript.info"، ليُسمح له بالوصول. Access-Control-Allow-Methods: ويجب أن تحتوي التوابع المسموحة. Access-Control-Allow-Headers: ويحب أن تضم قائمةً بالترويسات المسموحة. Access-Control-Max-Age: وهي ترويسة إضافية يمكنها تحديد الفترة الزمنية (ثوانٍ) للاحتفاظ بالإذن، لذا لن يكون على المتصفح إرسال طلبات تمهيدية للطلبات اللاحقة التي تحقق السماحيات الممنوحة سابقًا. لنلق نظرةً على آلية العمل خطوةً خطوةً، بمثال عن طلب PATCH ذي أصول مختلطة (والذي يُستخدَم غالبًا لتحديث البيانات): let response = await fetch('https://site.com/service.json', { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'API-Key': 'secret' } }); لدينا ثلاثة أسباب لعدّ هذا الطلب غير آمن (ويكفي أحدها بالطبع): الطلب هو PATCH. لاتحمل الترويسة Content-Type إحدى القيم: application/x-www-form-urlencoded أو multipart/form-data أو text/plain. وجود الترويسة API-Key غير الآمنة. الخطوة 1: الطلب التمهيدي preflight يرسل المتصفح بنفسه -قبل إرسال طلب غير آمنٍ كهذا- طلبًا تمهيديًا له الشكل التالي: OPTIONS /service.json Host: site.com Origin: https://javascript.info Access-Control-Request-Method: PATCH Access-Control-Request-Headers: Content-Type,API-Key OPTIONS: تابع الطلب التمهيدي. /service.json: المسار، ويطابق مسار الطلب الرئيسي تمامًا. الترويسات الخاصة بالطلب ذي الأصل المختلط: Origin: أصل مُرسل الطلب. Access-Control-Request-Method: نوع الطلب Access-Control-Request-Headers:قائمة بترويسات غير آمنة تفصل بينها فاصلة. الخطوة 2: الاستجابة للطلب التمهيدي ينبغي أن يستجيب الخادم برمز الحالة 200 والترويسات التالية: Access-Control-Allow-Origin: https://javascript.info Access-Control-Allow-Methods: PATCH Access-Control-Allow-Headers: Content-Type,API-Key. تسمح هذه الاستجابة بالتواصل المستقبلي مع الخادم وإلا فسيقع خطأ. إذا كنت سترسل طلبات أو ترويسات من أنواع أخرى مستقبلًا، فمن المنطقي أن تطلب الإذن مسبقًا، وذلك بإضافتهم إلى القائمة أثناء الطلب التمهيدي، يوضح المثال التالي استجابةً يُسمح فيها باستخدام PUT وDELETE بالإضافة إلى ترويسات أخرى: 200 OK Access-Control-Allow-Origin: https://javascript.info Access-Control-Allow-Methods: PUT,PATCH,DELETE Access-Control-Allow-Headers: API-Key,Content-Type,If-Modified-Since,Cache-Control Access-Control-Max-Age: 86400 سيرى المتصفح الآن الطلب PATCH في قائمة الطلبات المسموحة Access-Control-Allow-Methods، كما سيرى الترويسة Content-Type,API-Key في قائمة الترويسات المسموحة، لذا لن يتردد بإرسال الطلب الرئيسي المبني عليهما. إذا رأى المتصفح الترويسة Access-Control-Max-Age وقد أسندت إليها قيمة بالثواني، فسيحتفظ بالسماحيات التي مُنحت للطلب التمهيدي خلال هذه المدة الزمنية، أي سيُحتفظ بالسماحيات في مثالنا السابق فترة 86400 ثانية (أي يوم كامل)، ولن تحتاج الطلبات اللاحقة إلى نفس الخادم طلبات تمهيديةً أخرى، بفرض أنها تتلاءم مع السماحيات الممنوحة، وستُرسل مباشرةً. الخطوة 3: الطلب الفعلي يرسِل المتصفح الطلب الفعلي عندما ينجح الطلب التمهيدي، وتُنفَّذ العملية يطريقة مماثلة لإرسال طلب آمن. سيتضمن الطلب الرئيسي الترويسة Origin (لأنه طلب ذو أصل مختلط): PATCH /service.json Host: site.com Content-Type: application/json API-Key: secret Origin: https://javascript.info الخطوة 4: الاستجابة الفعلية لا بدّ للخادم من إضافة الترويسة Access-Control-Allow-Origin إلى الاستجابة الرئيسية، ولن يُعفيه الطلب التمهيدي الناجح من هذه المهمة: Access-Control-Allow-Origin: https://javascript.info تستطيع بعد ذلك قراءة استجابة الخادم الفعلية. يمكن لشيفرة JavaScript الآن قراءة الاستجابة على الطلب الفعلي. الثبوتيات Credentials لا تحضر الطلبات ذات الأصل المختلط التي تنتج عن شيفرة JavaScript أية ثبوتيات (ملفات تعريف الارتباط cookies أو استيثاق HTTP)، وهذا ليس أمرًا شائعًا في طلبات HTTP، فعند إرسال طلب HTTP إلى الموقع http://site.com مثلَا، فسيحمل الطلب جميع ملفات تعريف الارتباط الموجودة في نطاق المُرسِل، لكن الطلبات ذات الأصل المختلط الناتجة عن JavaScript تُمثّل استثناءً، حيث لن يُرسل الأمر (fetch(http://another.com أي ملفات تعريف ارتباط حتى تلك التي تنتمي إلى النطاق another.com. لكن لماذا؟ لأنّ الطلبات التي تُزوَّد بثبوتيات أقوى بكثير، إذ يمكن لشيفرة JavaScript -إن سُمح لها- أن تعمل بكامل إمكانياتها بالنيابة عن المستخدم، وأن تصل إلى معلومات حساسة بالاستفادة من هذه الثبوتيات. لكن هل يثق الخادم بسكربت ما إلى هذا الحد؟ إن كان الأمر كذلك، فلا بدّ من السماح صراحةً بالطلبات التي تحمل ثبوتيات من خلال ترويسة إضافية، حيث سنحتاج إلى إضافة الخيار credentials: "include" عند إرسال الثبوتيات مع fetchبالشكل التالي: fetch('http://another.com', { credentials: "include" }); يمكن الآن إرسال ملفات تعريف الارتباط التي تنتمي إلى another.com عبر الطلب fetch إلى الموقع الهدف، وينبغي على الخادم إذا وافق على قبول الثبوتيات، إضافة الترويسة Access-Control-Allow-Credentials: true إلى استجابته بالإضافة إلى الترويسة Access-Control-Allow-Origin، مثلًا: 200 OK Access-Control-Allow-Origin: https://javascript.info Access-Control-Allow-Credentials: true لاحظ أنه يُمنع استخدام النجمة (*) كقيمة للترويسة Access-Control-Allow-Origin في الطلبات التي تحمل ثبوتيات، إذ لا بدّ -كما نرى في المثال السابق- من تحديد الأصل بدقة، وهذا معيار إضافي للتأكد من أنّ الخادم يعرف تمامًا الجهة التي يثق بها لتنفيذ طلبات مثل هذه. خلاصة هنالك نوعان من الطلبات ذات الأصول المختلطة من وجهة نظر المتصفح: آمنة وغير آمنة. لا بدّ للطلبات الآمنة من تحقيق الشرطين التاليين: أن تستخدم نوعًا آمنًا مثل: GET أو POST أو HEAD. أن تستخدم الترويسات الآمنة التالية: Accept Accept-Language Content-Language Content-Type: وتحمل إحدى القيم application/x-www-form-urlencoded أو multipart/form-data أو text/plain. الفرق الجوهري هو أن الطلبات الآمنة ومنذ وقت طويل، تُنفَّذ باستخدام معرِّف النموذج <form> أو معرِّف السكربت <script>، بينما مُنعت المتصفحات من تنفيذ الطلبات غير الآمنة لفترة طويلة. يظهر هذا الفرق عمليًا في إمكانية إرسال الطلبات الآمنة مع الترويسة Origin مباشرةً، بينما يحتاج المتصفح إلى إرسال طلب تمهيدي preflight عند إرسال الطلبات غير الآمنة، يطلب فيها إذنًا من الخادم. للطلبات الآمنة: يرسل المتصفح الترويسة مع الأصل Origin. بالنسبة للطلبات التي لا تحمل ثبوتيات (لا ترسَل الثبوتيات بشكل افتراضي) لا بدّ أن يضبط الخادم ما يلي: Access-Control-Allow-Origin: على القيمة (*) أو نفس قيمة الأصل Origin. بالنسبة للطلبات التي تحمل ثبوتيات لا بدّ أن يضبط الخادم: Access-Control-Allow-Origin: على نفس قيمة الأصل Origin. Access-Control-Allow-Credentials: على القيمة "true". ولمنح JavaScript الوصول إلى ترويسات الاستجابة عدا Cache-Control و Content-Language وContent-Type وExpires وLast-Modified وPragma، فلا بدّ أن يضع الخادم الترويسة التي يُسمح بالوصول إليها ضمن الترويسة Access-Control-Expose-Headers. يُرسل المتصفح طلبًا تمهيديًا قبل الطلب الفعلي عند إرسال طلبات غير آمنة: يرسل المتصفح الطلب OPTIONS إلى نفس العنوان الذي سيرسِل إليه الطلب الفعلي مزوّدًا بالترويسات التالية: Access-Control-Request-Method: ويحمل نوع الطلب. Access-Control-Request-Headers: ويحمل قائمةً بترويسات غير آمنة يُطلب الإذن باستخدامها. يستجيب الخادم برمز الحالة 200، وبالترويسات التالية: Access-Control-Allow-Methods: تضم قائمةً بأنواع الطلبات المسموحة. Access-Control-Allow-Headers: تضم قائمةً بالترويسات المسموحة. Access-Control-Max-Age: وتحتوي على قيمة تمثل الفترة الزمنية (مقدرةً بالثواني) التي يُحتفظ فيها بالسماحيات. يُرسَل الطلب الفعلي بعد ذلك، وتُطبق خطوات إرسال الطلب الآمن. ترجمة -وبتصرف- للفصل Fetch: Cross-origin Requests من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: تتبع تقدم عملية التنزيل باستخدام Fetch وإلغاء العملية Fetch
-
إحدى أسوأ الأسرار التي تمَّ الاحتفاظ بها حول 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 أكثر)
-
الواجهة البرمجية Promise رائعة ويمكنك جعلها مذهلة باستخدام async و await. إنّ الشيفرة المتزامنة سهلة التتبع والتنقيح إلا أنّ الشيفرة غير المتزامنة أفضل بشكل عام من حيث الأداء والمرونة، فلماذا "توقف العرض" بينما بإمكانك استقبال الكثير من الطلبات في وقت واحد ثمّ معالجة كلٍّ منها عندما يصبح جاهزًا؟ مع العديد من الواجهات البرمجية API الجديدة التي تم تحقيقها مع مبدأ الوعد، إذ أصبحت الوعود جزءًا كبيرًا في عالم الجافاسكربت. لنلقِ نظرة على كيفية استخدام الواجهة البرمجية للوعود. الوعود قيد التطبيق الواجهة البرمجية XMLHttpRequest غير متزامنة ولكنها لا تستخدم الواجهة البرمجية Promises. هناك عدة واجهات برمجية أصلية تستخدم الوعود الآن، مثل: Battery API. fetch API (بديل XHR) الواجهة البرمجية ServiceWorker ستصبح الوعود أكثر شيوعًا، لذا من المهم أن يعتاد عليها جميع مطوري الواجهات الأمامية، وتجدر الإشارة إلى أنّ Node.js هي منصة أخرى للوعود. (يبدو هذا واضحًا، كما الواجهة البرمجية Promise ميزة أساسية في اللغة). من المحتمل أن يكون اختبار الوعود أسهل مما تعتقد لأنه يمكنك استخدام setTimeout كـ "مهمة" لك غير متزامنة. استخدام Promise الأساسي يجب أن يستخدم الباني new Promise() فقط للمهام الغير متزامنة الموروثة، مثل استخدام setTimeout أو XMLHttpRequest. يتم إنشاء وعدًا جديدًا باستخدام الكلمة المفتاحية new ويتم تمرير توابع resolve و reject لردّ النداء المزوَّد: var p = new Promise(function(resolve, reject) { // ...القيام بمهمة غير متزامنة وثم if(/* شرط جيد */) { resolve('Success!'); } else { reject('Failure!'); } }); p.then(function(result) { /* القيام بفعل ما مع النتيجة */ }).catch(function() { /* خطأ :( */ }).finally(function() { /* تنفّذ بغض النظر عن النجاح أو الفشل */ }); يعود الأمر للمطور فيما إذا كان يريد استدعاء resolve أو reject يدويًا ضمن جسم رد النداء اعتمادًا على نتيجة المهمة المعطاة. مثال واقعي لتحويل XMLHttpRequest إلى مهمة تعتمد على الوعد: // من وعود جاك أرشيبالد والعودة // http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promisifying-xmlhttprequest function get(url) { //إعادة وعد جديد. return new Promise(function(resolve, reject) { // XHR القيام بالعمل الاعتيادي لـ var req = new XMLHttpRequest(); req.open('GET', url); req.onload = function() { // هذا يتم استدعاؤه حتى في حالة 404 // لذا اختبر الحالة if (req.status == 200) { // قم بإنهاء الوعد مع نص الرد resolve(req.response); } else { // وإلا ارفض مع نص الحالة // والذي نأمل أن يكون خطأ ذو معنى reject(Error(req.statusText)); } }; // معالجة أخطاء الشبكة req.onerror = function() { reject(Error("Network Error")); }; // القيام بالطلب req.send(); }); } // استخدمه! get('story.json').then(function(response) { console.log("Success!", response); }, function(error) { console.error("Failed!", error); }); إذا كان من الممكن اتخاذ إجراء غير متزامن قد لا تحتاج إلى إكمال المهام غير المتزامنة ضمن الوعد، لكن سيكون الأفضل هو أن تكون القيمة المعادة وعدًا لذا يمكنك أن تعدّ كم عدد الوعود التي حصلت عليها من تابع معطى. في تلك الحالة يمكنك ببساطة استدعاء Promise.resolve() أو Promise.reject() بدون استخدام الكلمة المفتاحية new. مثلًا: var userCache = {}; function getUserDetail(username) { // في كلتا الحالتين، تم إضافته إلى الذاكرة أو لا، سيتم إعادة وعد if (userCache[username]) { // new إعادة وعد بدون الكلمة المفتاحية return Promise.resolve(userCache[username]); } // لتحصل على المعلومات fetch استخدم الواجهة البرمجية // وعدًا fetch تعيد return fetch('users/' + username + '.json') .then(function(result) { userCache[username] = result; return result; }) .catch(function() { throw new Error('Could not find user: ' + username); }); } بما أن القيمة المعادة هي وعد لذا يمكنك استخدام التوابع then و catch عليها. then كل كائنات الوعد تملك التابع then الذي يسمح لك بالتفاعل مع الوعد. رد النداء الأول لتابع then يستدعي النتيجة المعطاة له عن طريق استدعاء ()resolve: new Promise(function(resolve, reject) { // setTimeout حدث زائف غير متزامن باستخدام setTimeout(function() { resolve(10); }, 3000); }) .then(function(result) { console.log(result); }); // console من الطرفية // 10 رد النداء لـ then يتم تشغيله عندما ينتهي الوعد. يمكنك أيضًا أن تسلسل ردود النداء للتابع then: new Promise(function(resolve, reject) { // setTimeout حدث زائف غير متزامن باستخدام setTimeout(function() { resolve(10); }, 3000); }) .then(function(num) { console.log('first then: ', num); return num * 2; }) .then(function(num) { console.log('second then: ', num); return num * 2; }) .then(function(num) { console.log('last then: ', num);}); // من الـ console // first then: 10 // second then: 20 // last then: 40 كل then تستقبل القيمة المعادة من استدعاء then السابق. إذا أُنهي الوعد قبل أن يتم استدعاء التابع then مجددًا، يتم إيقاف رد النداء مباشرةً. إذا تم رفض الوعد ثم استدعاء التابع then بعد الرفض، لا يتم استدعاء رد النداء أبدًا. catch يتم استدعاء رد النداء catch عندما يُرفض الوعد: new Promise(function(resolve, reject) { // setTimeout حدث زائف غير متزامن باستخدام setTimeout(function() { reject('Done!'); }, 3000); }) .then(function(e) { console.log('done', e); }) .catch(function(e) { console.log('catch: ', e); }); // console من الطرفية // 'catch: Done!' ما تمرره لتابع reject يعود لك ولكن النمط المعتاد هو إرسال Error لـ catch: reject(Error('Data could not be found')); finally رد النداء المعرّف حديثًا finally يتم استدعاؤه بغض النظر عن النجاح أو الفشل: (new Promise((resolve, reject) => { reject("Nope"); })) .then(() => { console.log("success") }) .catch(() => { console.log("fail") }) .finally(res => { console.log("finally") }); // >> fail // >> finally Promise.all فكر بمحمّلات الجافاسكربت: يوجد أوقات يتم فيها تشغيل عدة تفاعلات غير متزامنة وتريد الاستجابة عندما تكتمل جميعها، هنا يأتي دور التابع Promise.all، هذا التابع يأخذ مصفوفة من الوعود ويعطيك رد نداء واحد فقط عند إنهاء جميع الوعود. Promise.all([promise1, promise2]).then(function(results) { // كلا الوعدين تم إنهاؤهما }) .catch(function(error) { // تم رفض وعد واحد أو أكثر }); الطريقة المثالية للتفكير بـ Promise.all هي إطلاق عدة طلبات AJAX (باستخدام fetch) في نفس الوقت. var request1 = fetch('/users.json'); var request2 = fetch('/articles.json'); Promise.all([request1, request2]).then(function(results) { // كلا الوعدين تم تنفيذهما }); يمكنك أن تدمج عدة واجهات برمجية مثل fetch والواجهة البرمجية Battery بما أنّها تعيد وعودًا. Promise.all([fetch('/users.json'), navigator.getBattery()]).then(function(results) { // كلا الوعدين تم تنفيذهما }); التعامل مع الرفض صعب بالطبع. إذا تم رفض أيّ وعد سيتم إطلاق catch للرفض الأول: var req1 = new Promise(function(resolve, reject) { // setTimeout حدث زائف غير متزامن باستخدام setTimeout(function() { resolve('First!'); }, 4000); }); var req2 = new Promise(function(resolve, reject) { // setTimeout حدث زائف غير متزامن باستخدام setTimeout(function() { reject('Second!'); }, 3000); }); Promise.all([req1, req2]).then(function(results) { console.log('Then: ', results); }).catch(function(err) { console.log('Catch: ', err); }); // console من الـ // Catch: Second! ستكون Promise.all مفيدة أكثر للواجهات البرمجية التي تتجه لاستخدام الوعود. Promise.race تعدّ الدالة Promise.race مفيدة فبدلًا من أن يتم الانتظار حتى إنهاء جميع الوعود أو رفضها تقوم بتشغيل أيّ وعد في المصفوفة تم إنهاؤه أو رفضه. var req1 = new Promise(function(resolve, reject) { // setTimeout حدث زائف غير متزامن باستخدام setTimeout(function() { resolve('First!'); }, 8000); }); var req2 = new Promise(function(resolve, reject) { // setTimeout حدث زائف غير متزامن باستخدام setTimeout(function() { resolve('Second!'); }, 3000); }); Promise.race([req1, req2]).then(function(one) { console.log('Then: ', one); }).catch(function(one, two) { console.log('Catch: ', one); }); // console من الـ // Then: Second! يمكن أن تشغّل حالة الاستخدام طلبًا إلى مصدر أوليّ ومصدر ثانوي (في حال عدم توفر الأوليّ والثانوي). اعتد على الوعود كانت الوعود موضوعًا مثيرًا للاهتمام خلال السنوات القليلة الماضية (أو خلال السنوات العشرة الأخيرة إذا كنت مستخدم مجموعة أدوات Dojo) وانتقلت من كونها نمط في إطار عمل جافاسكربت إلى كونها من أساس اللغة. وربما من الأفضل افتراض أنك ستشاهد معظم الواجهات البرمجية الجديدة في جافاسكربت يتم تنفيذها بنمط يعتمد على الوعد، وهذا أمر ممتاز. إنّ المطورين قادرون على تجنب جحيم رد النداء وتمرير التفاعلات غير المتزامنة مثل أيّ متغير آخر. تستغرق الوعود بعض الوقت لتكون الأدوات الأصلية وقد حان الوقت لتعلّمها. يمكنك في أي وقت الرجوع إلى توثيق الكائن Promise في موسوعة حسوب كما ننصحك أيضًا بقراءة صفحة «استخدام الوعود» بعد هذه المقالة مباشرةً لوضع ما تعلمته موضع التطبيق. ترجمة -وبتصرف- للمقال JavaScript Promise API لصاحبه David Walsh
-
يجب أن تعرف كيف تدير المستودعات البعيدة لتكون قادرا على التعاون في مشروع يستخدم Git. المستودعات البعيدة هي نسخ من المشروع مضافة على خادوم غير جهازك المحلي. يمكن أن يكون لديك أكثر من مستودع بعيد، وهو إما أن يكون للقراءة فقط Read-only أو للقراءة والكتابة. يستدعي التعاون مع الآخرين إدارةَ المستودعات ودفع Push البيانات إليها أو جلبها منها Pull لمشاركة مساهماتك مع بقية الفريق. تتضمن إدارة المستودعات البعيدة معرفة كيفية إضافتها، حذفها عندما تصبح غير صالحة، إدارة الفروع Branches البعيدة ومتابعتها Tracking إضافةً لأمور أخرى. يعرِض هذا المقال لمهارات أساسية لإدارة المستودعات البعيدة. عرض المستودعات البعيدة يعرِض الأمر git remote الخواديم البعيدة المضبوطة لديك. ينتج عن تنفيذ الأمر إظهار لائحة بأسماء مختصرة لكل خادوم بعيد ضبطته. إن كنت نسخت مستودعا فسترى على الأقل الاسم المختصر origin، وهو الاسم المختصر الافتراضي الذي يعطيه Git للخادوم الذي نسخت منه المستودع. يستخدَم الاسم المختصر مرجعا للدلالة على المستودع بدلا من كتابة مساره كاملا. في المثال التالي ننسخ المستودع ticgit ثم نلج إلى مجلد المستودع وننفذ أمر git remote: git clone https://github.com/schacon/ticgit Cloning into 'ticgit'... remote: Reusing existing pack: 1857, done. remote: Total 1857 (delta 0), reused 0 (delta 0) Receiving objects: 100% (1857/1857), 374.35 KiB | 268.00 KiB/s, done. Resolving deltas: 100% (772/772), done. Checking connectivity... done. cd ticgit git remote origin لاحظ الاسم المختصر origin. يمكن أيضا استخدام الخيار v- الذي يُظهر مسارات URL التي خزنها Git للاستخدام عند القراءة من المستودع أو الكتابة فيه: git remote -v origin https://github.com/schacon/ticgit (fetch) origin https://github.com/schacon/ticgit (push) إن كان لديك أكثر من مستودع بعيد فسيسرُدها الأمر جميعا. على سبيل المثال، إن كان لمستودع واحد خواديم بعيدة متعدّدة للعمل مع متعاونين مختلفين فستكون نتيجة تنفيذ الأمر كالتالي: cd grit git remote -v bakkdoor https://github.com/bakkdoor/grit (fetch) bakkdoor https://github.com/bakkdoor/grit (push) cho45 https://github.com/cho45/grit (fetch) cho45 https://github.com/cho45/grit (push) defunkt https://github.com/defunkt/grit (fetch) defunkt https://github.com/defunkt/grit (push) koke git://github.com/koke/grit.git (fetch) koke git://github.com/koke/grit.git (push) origin git@github.com:mojombo/grit.git (fetch) origin git@github.com:mojombo/grit.git (push) يعني هذا أن بإمكاننا جلب مساهمات أي واحد من هؤلاء المتعاونين بسهولة. إضافة مستودعات بعيدة ذكرنا في الفقرات السابقة كيفية إضافة مستودعات بعيدة باختصار؛ في الفقرات التالية سنفصِّل في الكيفية. استخدم الأمر التالي لإضافة مستودع جديد باسم مختصر يمكنك جعله مرجعا للمستودع: git remote add [shortname] [url] مثلا: git remote origin git remote add pb https://github.com/paulboone/ticgit git remote -v origin https://github.com/schacon/ticgit (fetch) origin https://github.com/schacon/ticgit (push) pb https://github.com/paulboone/ticgit (fetch) pb https://github.com/paulboone/ticgit (push) يمكنك الآن استخدام الاسم pb بدلا من العنوان الكامل في سطر الأوامر. استخدم الأمر التالي لإحضار جميع البيانات الموجودة في المستودع البعيد الذي أضفته أعلاه والتي لا توجد لديك محليًّا: git fetch pb remote: Counting objects: 43, done. remote: Compressing objects: 100% (36/36), done. remote: Total 43 (delta 10), reused 31 (delta 5) Unpacking objects: 100% (43/43), done. From https://github.com/paulboone/ticgit * [new branch] master -> pb/master * [new branch] ticgit -> pb/ticgit يمكن الآن الوصول إلى الفرع الرئيس من المستودع عبر pb/master . جلب مستودعات بعيدة ودفع البيانات إليها يمكن جلب بيانات مستودع بعيد بتنفيذ الأمر: git fetch [remote-name] يذهب Git بعد تنفيذ الأمر أعلاه إلى المستودع البعيد وينزل جميع بياناته التي لا توجد لديك حتى الآن. تحصُل بعد تنفيذ الأمر على مراجع (أسماء مختصرة) لجميع الفروع يمكن بعد ذلك دمجها أو فحصها في أي وقت. يضيف أمر النسخ git clone الاسم المختصر origin للمستودع البعيد تلقائيا. يجلب أمر git fetch origin أي بيانات جديدة دُفِعت إلى الخادوم بعد نسخ المستودع (أو بعد آخر جلب منه). من المهم ملاحظة أن git fetch تضيف البيانات إلى المستودع المحلي، ولا تدمجها تلقائيا مع أي من أعمالك؛ كما أنها لا تعدل على ما تعمل عليه. يعني هذا أن عليك دمجها يدويا عندما تكون جاهزا. إن كان لديك فرع معدّ لتتبع مستودع بعيد فيمكنك استخدام git pull لجلب البيانات من المستودع البعيد ودمجها مع الفرع الحالي. يُعِد أمر git clone تلقائيا الفرع الرئيس المحلي لتتبع الفرع الرئيس على الخادوم البعيد الذي نُسخ المستودع منه. يجلب أمر git pull البيانات من الخادوم الذي نُسخ أصلا منه المستودع ويحاول تلقائيا دمجها إلى الشفرة البرمجية التي تعمل عليها حاليا. دفع البيانات إلى المستودع البعيد يجب دفع المشروع إلى الخادوم عندما يكون جاهزا لتشاركه مع الآخرين، بتنفيذ الأمر التالي: git push [remote-name] [branch-name] حيث [remote-name] يمثل الفرع على الخادوم البعيد و[branch-name] على الخادوم المحلي، مع التذكير أن نسخ المستودع يضبط الاسمين تلقائيا كما أشرنا أعلاه. عندما تريد دفع الفرع الرئيس إلى الخادوم الأصلي فيمكنك تنفيذ الأمر التالي لدفع الإيداعات التي أنجزتها إلى الخادوم: git push origin master يعمل الأمر السابق فقط إن كنت نسخت المستودع من خادوم لديك صلاحيات الكتابة عليه، مع شرط ألا يكون أي شخص آخر دفع بيانات جديدة للمستودع البعيد بعد آخر عملية جلب قمت بها. إذا نسخت المستودع أنت وشخص آخر في نفس الوقت ثم أضاف هو إيداعات جديدة بدفعها إلى المستودع ثم أتيت لدفع بياناتك فإن المستودع لن يقبلها. يجب عليك في هذه الحالة جلب إيداعات الشخص الآخر أولا إلى جهازك المحلي ثم تضمينها لديك في المشروع ثم دفعه من جديد. فحص مستودع بعيد إن أردت الحصول على معلومات أكثر تفصيلا عن مستودع بعيد فالأمر التالي يؤدي هذه المهمة: git remote show [remote-name] إن نفذت الأمر مع اسم مختصر مثل origin فستحصل على نتيجة شبيهة بالتالي: git remote show origin * remote origin Fetch URL: https://github.com/schacon/ticgit Push URL: https://github.com/schacon/ticgit HEAD branch: master Remote branches: master tracked dev-branch tracked Local branch configured for 'git pull': master merges with remote master Local ref configured for 'git push': master pushes to master (up to date) يظهر في نتيجة الأمر مسار المستودع البعيد إضافة إلى معلومات خاصة بفرع التتبع. يخبرك الأمر أيضا أنك إن نفذت الأمر git pull على الفرع الرئيس master فسيدمجه تلقائيا في الفرع الرئيس في المستودع البعيد بعد أن يجلب جميع المراجع البعيدة؛ كما أنه يسرد قائمة بجميع المراجع البعيدة التي جلبها. إن كنت تستخدم Git كثيرا فستظهر معلومات أكثر تفصيلا من المثال غير المعقد أعلاه: git remote show origin * remote origin URL: https://github.com/my-org/complex-project Fetch URL: https://github.com/my-org/complex-project Push URL: https://github.com/my-org/complex-project HEAD branch: master Remote branches: master tracked dev-branch tracked markdown-strip tracked issue-43 new (next fetch will store in remotes/origin) issue-45 new (next fetch will store in remotes/origin) refs/remotes/origin/issue-11 stale (use 'git remote prune' to remove) Local branches configured for 'git pull': dev-branch merges with remote dev-branch master merges with remote master Local refs configured for 'git push': dev-branch pushes to dev-branch (up to date) markdown-strip pushes to markdown-strip (up to date) master pushes to master (up to date) يعرض الأمر الفروع التي ستُدفَع إليها البيانات تلقائيا عند تنفيذ الأمر git push على فروع معيَّنة. كما يُظهر أيضا الفروع الموجودة على الخادوم التي لا توجد لديك حتى الآن، الفروع التي حذفت من الخادوم ولكنها لا زالت لديك محليًّا والفروع المختلفة التي دُمجت تلقائيا عند تنفيذ الأمر git pull. حذف المستودعات البعيدة وإعادة تسميتها يتيح الأمر git remote rename إمكانية تغيير الاسم المختصر الخاص بالمستودع البعيد. إن أردت مثلا تغيير pb إلى paul فيجب تنفيذ الأمر على النحو التالي: git remote rename pb paul git remote origin paul ينتج عن الأمر أيضا التعديل على أسماء الفروع أيضا، مثلا pb/master تصبح paul/master عند تعديل الاسم المختصر من pb إلى paul. استخدم الأمر git remote rm لحذف مستودع بعيد : git remote rm paul git remote origin ترجمة -وبتصرف- للفصل Git Basics - Working with Remotes من كتاب Pro Git لصاحبه Scott Chacon.