قد لا تحتاج Axios
Fetch API إلى الإنقاذ!
قد لا تحتاج إلى Axios
هذا ليس هجومًا على Axios.
بل هو دعوة لواجهة fetch التي أصبحت قادرة جدًا. 🦄
نظرة عامة
هذه المقالة هي مجموعة من مقتطفات كود fetch “المفقودة” وحالات الاستخدام الشائعة التي تمنيت لو كان العثور عليها أسهل.
هل حالة الاستخدام الخاصة بك غير مدرجة؟ أخبرني ✉️
مقارنة الميزات
| fetch | axios | request | |
|---|---|---|---|
| اعتراض الطلب والاستجابة | ✅ | ✅ | ✅ |
| تحويل بيانات الطلب والاستجابة | ✅ | ✅ | ✅ |
| إلغاء الطلبات | ✅ | ✅ | ❌ |
| تحويلات تلقائية لبيانات JSON | مساعدات يدوية | ✅ | ✅ |
| دعم من جانب العميل للحماية من XSRF | ✅ | ✅ | ✅ |
| التقدم | ✅ | ✅ | ✅ |
| البث … |
| fetch | axios | request | |
|---|---|---|---|
| اعتراض الطلب والاستجابة | ✅ | ✅ | ✅ |
| تحويل بيانات الطلب والاستجابة | ✅ | ✅ | ✅ |
| إلغاء الطلبات | ✅ | ✅ | ❌ |
| تحويلات تلقائية لبيانات JSON | مساعدات يدوية | ✅ | ✅ |
| دعم من جانب العميل للحماية من XSRF | ✅ | ✅ | ✅ |
| التقدم | ✅ | ✅ | ✅ |
| البث | ✅ | ✅ | ✅ |
| إعادة التوجيه | ✅ | ✅ | ✅ |
عند بدء كتابة هذه المقالة (أواخر 2018، تم تحديثها في 2024)، افترضت أنني سأنتهي بجدول من مربعات اختيار مختلطة. بالتأكيد هناك حالات استخدام خاصة تبرر استخدام axios وrequest وr2 وsuperagent وgot وغيرها.
حسنًا، كما اتضح، لقد بالغت في تقدير الحاجة إلى مكتبات HTTP تابعة لجهات خارجية.
على الرغم من استخدامي لـ fetch لعدة سنوات (بما في ذلك مهام غير تافهة: رفع الملفات ودعم الأخطاء/إعادة المحاولة)، إلا أنه لا يزال لدي مفاهيم خاطئة حول قدرات fetch وحدودها.
لا يقوم fetch الأصلي تلقائيًا بتحليل استجابات JSON أو تحويل أجسام طلبات JSON إلى سلاسل نصية. أنت تستدعي response.json() في طريق العودة و JSON.stringify() في طريق الإرسال. لا يزال Axios يتفوق في هذه النقطة من بيئة العمل؛ الحجة لصالح fetch هي أن مساعدًا صغيرًا غالبًا ما يسد الفجوة.
حسنًا، دعنا نلقي نظرة على ما يمكن أن يفعله fetch…
وصفات Fetch
الحصول على JSON من عنوان URL
fetch('https://api.github.com/orgs/nodejs') .then(response => response.json()) .then(data => { console.log(data) // result from `response.json()` above }) .catch(error => console.error(error))الرؤوس المخصصة
fetch('https://api.github.com/orgs/nodejs', { headers: new Headers({ 'User-agent': 'Mozilla/4.0 Custom User Agent' })}).then(response => response.json()).then(data => { console.log(data)}).catch(error => console.error(error))معالجة أخطاء HTTP
const isOk = response => response.ok ? response.json() : Promise.reject(new Error('Failed to load data from server'))
fetch('https://api.github.com/orgs/nodejs') .then(isOk) // <= Use `isOk` function here .then(data => { console.log(data) // Prints result from `response.json()` }) .catch(error => console.error(error))مثال على CORS
يتم التحقق من CORS بشكل أساسي على الخادم – لذا تأكد من صحة إعداداتك على جانب الخادم.
يحدد خيار credentials ما إذا كانت ملفات تعريف الارتباط الخاصة بك تُضمَّن تلقائيًا.
fetch('https://api.github.com/orgs/nodejs', { credentials: 'include', // Useful for including session ID (and, IIRC, authorization headers)}).then(response => response.json()).then(data => { console.log(data) // Prints result from `response.json()`}).catch(error => console.error(error))إرسال JSON
postRequest('http://example.com/api/v1/users', {user: 'Dan'}) .then(data => console.log(data)) // Result from the `response.json()` call
function postRequest(url, data) { return fetch(url, { credentials: 'same-origin', // 'include', default: 'omit' method: 'POST', // 'GET', 'PUT', 'DELETE', etc. body: JSON.stringify(data), // Use correct payload (matching 'Content-Type') headers: { 'Content-Type': 'application/json' }, }) .then(response => response.json()) .catch(error => console.error(error))}إرسال نموذج HTML <form>
postForm('http://example.com/api/v1/users', 'form#userEdit') .then(data => console.log(data))
function postForm(url, formSelector) { const formData = new FormData(document.querySelector(formSelector))
return fetch(url, { method: 'POST', // 'GET', 'PUT', 'DELETE', etc. body: formData // a FormData will automatically set the 'Content-Type' }) .then(response => response.json()) .catch(error => console.error(error))}البيانات المشفرة للنموذج
لإرسال البيانات بنوع المحتوى application/x-www-form-urlencoded سنستخدم URLSearchParams لتشفير البيانات كسلسلة استعلام. على سبيل المثال، new URLSearchParams({a: 1, b: 2}) ينتج a=1&b=2.
postFormData('http://example.com/api/v1/users', {user: 'Mary'}) .then(data => console.log(data))
function postFormData(url, data) { return fetch(url, { method: 'POST', // 'GET', 'PUT', 'DELETE', etc. body: new URLSearchParams(data), headers: new Headers({ 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8' }) }) .then(response => response.json()) .catch(error => console.error(error))}رفع ملف
postFile('http://example.com/api/v1/users', 'input[type="file"].avatar') .then(data => console.log(data))
function postFile(url, fileSelector) { const formData = new FormData() const fileField = document.querySelector(fileSelector)
formData.append('username', 'abc123') formData.append('avatar', fileField.files[0])
return fetch(url, { method: 'POST', // 'GET', 'PUT', 'DELETE', etc. body: formData // Coordinate the body type with 'Content-Type' }) .then(response => response.json()) .catch(error => console.error(error))}رفع ملفات متعددة
قم بإعداد عنصر رفع ملفات مع الخاصية multiple:
<input type='file' multiple class='files' name='files' />ثم استخدمه مع شيء مثل:
postFile('http://example.com/api/v1/users', 'input[type="file"].files') .then(data => console.log(data))
function postFile(url, fileSelector) { const formData = new FormData() const fileFields = document.querySelectorAll(fileSelector)
// Add all files to formData Array.prototype.forEach.call(fileFields.files, f => formData.append('files', f)) // Alternatively for PHPeeps, use `files[]` for the name to support arrays // Array.prototype.forEach.call(fileFields.files, f => formData.append('files[]', f))
return fetch(url, { method: 'POST', // 'GET', 'PUT', 'DELETE', etc. body: formData // Coordinate the body type with 'Content-Type' }) .then(response => response.json()) .catch(error => console.error(error))}المهلات الزمنية
إليك مهلة زمنية عامة للـ Promise باستخدام نمط ‘التطبيق الجزئي’. ستعمل مع أي واجهة Promise. لا تقم بعمل كثير في سلسلة الـ Promise المقدمة، فستستمر في العمل - وأي إخفاقات قد تؤدي إلى تسربات ذاكرة طويلة الأمد.
function promiseTimeout(msec) { return promise => { const timeout = new Promise((yea, nah) => setTimeout(() => nah(new Error('Timeout expired')), msec)) return Promise.race([promise, timeout]) }}
promiseTimeout(5000)(fetch('https://api.github.com/orgs/nodejs')) .then(response => response.json()) .then(data => { console.log(data) // Prints result from `response.json()` in getRequest }) .catch(error => console.error(error)) // Catches any timeout (or other failure)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Alternative example:fetchTimeout(5000, 'https://api.github.com/orgs/nodejs') .then(console.log)// Alternative implementation:function fetchTimeout(msec, ...args) { return raceTimeout(fetch(...args))
function raceTimeout(promise) { const timeout = new Promise((yea, nah) => setTimeout(() => nah(new Error('Timeout expired')), msec)) return Promise.race([promise, timeout]) }}ومثال أكثر تعقيدًا، يتضمن علامة تتبع __timeout لتتمكن من اعتراض أي عمل مكلف.
function promiseTimeout(msec) { return (promise) => { let isDone = false promise.then(() => isDone = true) const timeout = new Promise((yea, nah) => setTimeout(() => { if (!isDone) { promise.__timeout = true nah(new Error('Timeout expired')) } }, msec)) return Promise.race([promise, timeout]) }}
promiseTimeout(5000)(fetch('https://api.github.com/orgs/nodejs')).then(response => response.json()).then(data => { console.log(data) // Prints result from `response.json()` in getRequest}).catch(error => console.error(error))مساعد تقدم التحميل
تقدم الرفع حاليًا به بعض الأخطاء خارج متصفح Chrome.
معالج التقدم يتجنب الأسلوب الموضح أدناه تغليف استدعاء fetch في إغلاق. 👍
progressHelper له الواجهة التالية (المصدر متاح أدناه)
const progressHelper = require('./progressHelper.js')
const handler = ({loaded, total}) => { console.log(`Downloaded ${loaded} of ${total}`)}// handler args: ({ loaded = Kb, total = 0-100% })const streamProcessor = progressHelper(handler)// => streamProcessor is a function for use with the response _stream_لنلقِ نظرة على مثال استخدام:
// The progressHelper could be inline w/ .then() below...const streamProcessor = progressHelper(console.log)
fetch('https://fetch-progress.anthum.com/20kbps/images/sunrise-progressive.jpg') .then(streamProcessor) // note: NO parentheses because `.then` needs to get a function .then(response => response.blob()) .then(blobData => { // ... set as base64 on an <img src="base64..."> })قد تبدو أداة تنزيل صور قابلة لإعادة الاستخدام مثل getBlob():
const getBlob = url => fetch(url) .then(progressHelper(console.log)) // progressHelper used inside the .then() .then(response => response.blob())بالمناسبة، Blob هو اختصار لـ “كائن ثنائي كبير” (Binary Large Object).
من المهم اختيار نمط واحد من النمطين أدناه (وظيفيًا متكافئان):
// OPTION #1: no temp streamProcessor varfetch(...) .then(progressHelper(console.log))
// ⚠️ OR️ ️⚠️
// OPTION #2: define a `streamProcessor` to hold our console loggerconst streamProcessor = progressHelper(console.log)fetch(...) .then(streamProcessor)تفضّلي هي الخيار رقم 1. لكن قد يفرض عليك تصميم النطاق استخدام الخيار رقم 2.
وأخيرًا، إليك الجزء الأخير من هذه الوصفة، وهو progressHelper:
المصدر: المساعد التقدمي
function progressHelper(onProgress) { return (response) => { if (!response.body) return response
let loaded = 0 const contentLength = response.headers.get('content-length') const total = !contentLength ? -1 : parseInt(contentLength, 10)
return new Response( new ReadableStream({ start(controller) { const reader = response.body.getReader() return read()
function read() { return reader.read() .then(({ done, value }) => { if (done) return void controller.close() loaded += value.byteLength onProgress({ loaded, total }) controller.enqueue(value) return read() }) .catch(error => { console.error(error) controller.error(error) }) } } }) ) }}شكر خاص: شكر خاص لـ Anthum Chris و إثبات المفهوم الرائع للتقدم + Fetch الموضّح هنا
مساعد إعادة المحاولة التكراري
/** * A **Smarter** retry wrapper with currying! */function retryCurry(fn, retriesLeft = 5) { const retryFn = (...args) => fn(...args) .catch(err => retriesLeft > 0 ? retryFn(fn, retriesLeft - 1) : Promise.reject(err) }) return retryFn}
const getJson = (url) => fetch(url) .then(response => response.json())
// Usageconst retryGetJson = retryCurry(getJson, 3);
// Now you can pass any arguments through to your function!retryGetJson('https://api.github.com/orgs/elite-libs') .then(console.log) .catch(console.error)/** Basic retry wrapper for Promises */function retryPromise(fn, retriesLeft = 5) { return fn() .catch(err => retriesLeft > 0 ? retryPromise(fn, retriesLeft - 1) : Promise.reject(err) })}
const getJson = (url) => fetch(url) .then(response => response.json())
// Usageretry(() => getJson('https://api.github.com/orgs/elite-libs')) .then(console.log) .catch(console.error)التعامل مع إعادة التوجيه HTTP
const checkForRedirect = (response) => { // Check for temporary redirect (307), or permanent (308) if (response.status === 307 || response.status === 308) { const location = response.headers.get('location') if (!location) { return Promise.reject(new Error('Invalid HTTP Redirect! No Location header.')); } // You can change the behavior here to any custom logic: // e.g. open a "confirm" modal, log the redirect url, etc. return fetch(location) // Bonus: this will handle recursive redirects ✨ .then(checkForRedirect) } return response};
fetch('https://api.github.com/orgs/elite-libs') // Next line will handle redirects .then(checkForRedirect) .then(response => response.json()) .then(console.log) .catch(console.error)إلغاء طلب fetch
const httpWithTimeout = (timeout = 5000, url) => { const controller = new AbortController(); // Set an Nsec cancellation timeout const timer = setTimeout(() => controller.abort(), timeout);
return fetch(url, { signal: controller.signal }) .then(response => { clearTimeout(timer); // not required but closes open ref return response.text(); }).then(text => { console.log(text); });}التوافق
اعتبارًا من عام 2022، أصبحت واجهة fetch مدعومة على نطاق واسع في جميع المتصفحات الحديثة وفي الإصدارات الأحدث من NodeJS v18+.
إذا كان عليك دعم IE، يمكنك استخدام polyfill لـ fetch عبر حزمة github/fetch (التي تحافظ عليها فريق رائع في GitHub). من الممكن العودة إلى IE8 - قد تختلف النتائج حسب استخدامك.
يمكن لإصدارات NodeJS الأقدم الاستفادة من واجهة fetch عبر حزمة node-fetch:
npm install node-fetchبعد polyfill+node-fetch: متوافق بنسبة 99.99% ✅
يرجى التغريد لي إذا كانت لديك حالات استخدام أخرى ترغب في رؤيتها. ❤️