۱۴. Fetch قابل اعتماد؛ اعتبار پاسخ، لغو و مهار نتیجهٔ قدیمی
در رابط جستوجو، درست بودن آخرین پاسخ دریافتشده کافی نیست؛ باید متعلق به آخرین درخواست معتبر کاربر باشد. همچنین پاسخ HTTP موفق الزاماً ساختار دادهٔ درست ندارد. این درس دریافت کاتالوگ پروژهٔ فروشگاه را بهانه میکند تا سه مرز را جدا ببینیم: شبکه، قرارداد HTTP و قرارداد داده.
مثال اول؛ پاسخ را در چند مرحله بررسی کنید
از صفحهٔ فروشگاه آزمایشگاه، Console مرورگر را باز کنید و این کد را اجرا کنید. مسیر catalog.json در سرور همراه وجود دارد. اگر نمونه را در سایتی دیگر اجرا کنید، این مسیر لزوماً وجود ندارد؛ origin جزئی از معنای URL نسبی است.
const { validateCatalog } = await import('/shop/model.mjs');
async function readCatalog(signal) {
const response = await fetch('/shop/catalog.json', {
signal, cache: 'no-store'
});
if (!response.ok) throw new Error('HTTP ' + response.status);
const data = await response.json();
return validateCatalog(data);
}
const catalog = await readCatalog();
console.log(catalog.length, catalog[0]?.name);خروجی تعداد و نام اولین محصول فایل همراه است. fetch روی پاسخ 404 الزاماً Promise ردشده تولید نمیکند؛ response.ok را بررسی میکنیم. response.json هم ممکن است روی صفحهٔ HTML یا JSON خراب شکست بخورد. در پایان، validateCatalog قیمت صحیح، موجودی، شناسه و تکراری نبودن محصولات را بررسی میکند. آرایه بودن پاسخ بهتنهایی کافی نیست.
cache:'no-store' در این تمرین برای مشاهدهٔ دوبارهٔ فایل تغییرکرده است؛ آن را نسخهٔ همیشگی همهٔ برنامهها ندانید. سیاست کش باید با تازگی داده و هزینهٔ شبکه سازگار باشد. موجودی فروشگاه واقعی نیز با خواندن فایل مرورگر قطعی نمیشود؛ هنگام عملیات حساس، سرور دوباره آن را کنترل میکند.
مثال دوم؛ هم لغو، هم شمارهٔ درخواست
این تابع را پس از مثال اول در Console همان صفحه تعریف کنید. پارامترهای onData و onStatus اتصال به رابطاند. شمارنده باعث میشود نتیجهٔ یک درخواست قدیمی روی درخواست تازه اعمال نشود؛ کنترلر برای توقف کار قبلی به محیط سیگنال میدهد.
function createCatalogLoader(onData, onStatus) {
let revision = 0;
let controller;
return async function load() {
controller?.abort();
controller = new AbortController();
const signal = controller.signal;
const current = ++revision;
onStatus('loading');
try {
const rows = await readCatalog(signal);
if (signal.aborted || current !== revision) return;
onData(rows);
onStatus('ready');
} catch (error) {
if (signal.aborted || current !== revision) return;
onStatus('error: ' + error.message);
} finally {
if (current === revision) onStatus('finished');
}
};
}
const loadDemo = createCatalogLoader(
rows => console.log('محصول:', rows.length),
state => console.log('وضعیت:', state)
);
void loadDemo();
void loadDemo();در این مثال آموزشی finished فقط پایان انتظار است و جای ready یا error را در مدل وضعیت نمیگیرد. در رابط واقعی بهتر است busy را جدا از نتیجه نگه دارید؛ پیام خطا نباید با «تمام شد» پاک شود. پروژهٔ آماده از revision و AbortController استفاده میکند و در شکست، دکمهٔ تلاش دوباره را نشان میدهد.
لغو یک درخواست خواندنی با برگشت دادن عملیات نوشتنی فرق دارد. اگر POST ثبت سفارش را لغو کنید، ممکن است سرور قبلاً سفارش را ساخته باشد. همچنین اگر پس از fetch پردازش دیگری دارید که به signal توجه نمیکند، کنترلر بهتنهایی جلوی اعمال نتیجهٔ آن را نمیگیرد. بررسی revision قبل از تغییر رابط همچنان مفید است.
تلاش دوباره را آگاهانه طراحی کنید
برای خواندن کاتالوگ، دکمهٔ retry معمولاً قابل فهمتر از تکرار بیپایان است. اگر تکرار خودکار میسازید، تعداد محدود، فاصلهٔ زمانی و امکان لغو داشته باشد. همهٔ 4xxها با تکرار درست نمیشوند؛ ورودی نامعتبر یا نبود مجوز نیاز به اقدام دیگری دارد. برای عملیات نوشتنی، پیش از تکرار باید قرارداد جلوگیری از اثر تکراری را داشته باشید. «شبکه قطع شد» لزوماً «عمل انجام نشد» نیست.
تمرین مستقل و پاسخ تشریحی
سه شکست جدا بازتولید کنید: مسیر ناموجود، JSON خراب و دادهٔ JSON با قیمت رشتهای. سپس دو Promise با تأخیر متفاوت بسازید تا درخواست قدیمی بعد از جدید تمام شود. پاسخ راهنما: خطای HTTP قبل از json، خطای parse هنگام json و خطای schema در validator رخ میدهد. در آزمایش مسابقه، فقط نتیجهای که revision آن با مقدار فعلی برابر است اعمال شود؛ به سرعت واقعی شبکه وابسته نشوید.
معیار تحویل: دادهٔ قبلی در شکست به موفقیت جعلی تبدیل نشود؛ retry کار کند؛ نتیجهٔ قدیمی روی دادهٔ تازه ننشیند؛ busy درخواست جدید با finally قدیمی خاموش نشود. گزارش خطای کاربر باید کوتاه و قابل اقدام باشد و جزئیات فنی فقط برای عیبیابی مناسب نگه داشته شود.
منابع انگلیسی برای مطالعهٔ تکمیلی
https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
https://developer.mozilla.org/en-US/docs/Web/API/AbortController
https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status
تألیف و مثالهای مستقل: تحریریه درسآوند.