۱۷. مدیریت وضعیت رابط؛ بازیابی خطا بدون گمشدن داده
در دورهٔ مقدماتی آموختید با کلیک روی دکمه، متن صفحه را تغییر بدهید. اکنون مسئله این است که چند عملیات غیرهمزمان، یک رابط مشترک را تغییر میدهند. کاربر همزمان تایپ میکند، اتصال قطع میشود و پاسخ یک درخواست قدیمی دیرتر میرسد. هدف این درس طراحی وضعیتی است که بتوان رفتار آن را توضیح داد و آزمود، نه اضافهکردن چند متغیر بولی پراکنده. مثال اصلی ما کاتالوگ فروشگاه و پیشنویس پاسخ تیکت است.
یک وضعیت اصلی، چند دادهٔ همراه
اگر loading و failed و ready مستقل باشند، ترکیب «همزمان آماده و در حال خطا» بهسادگی ساخته میشود. در نمونهٔ مستقل زیر، status فقط یک مقدار دارد. دادهٔ قبلی هنگام شروع دریافت باقی میماند؛ این تصمیم محصول است و رابط باید قدیمیبودن آن را روشن کند. نام reducer به معنی تابعی است که وضعیت و رویداد را میگیرد و وضعیت بعدی را برمیگرداند؛ به کتابخانهای وابسته نیست.
function reduce(state, event) {
switch (event.type) {
case 'load':
return { ...state, status: 'loading', error: '' };
case 'success':
return { status: 'ready', items: event.items, error: '' };
case 'failure':
return { ...state, status: 'error', error: event.message };
default:
throw new Error('Unknown event');
}
}
let state = { status: 'idle', items: [], error: '' };
state = reduce(state, { type: 'load' });
state = reduce(state, { type: 'success', items: [] });
console.log(state.status, state.items.length); // ready 0خروجی خالی موفقیتآمیز با شکست دریافت فرق دارد. برای اولی «محصولی پیدا نشد» و برای دومی «دریافت انجام نشد؛ دوباره تلاش کنید» لازم است. در نسخهٔ همراه فروشگاه، ready مجازبودن عملیات سبد را کنترل میکند و revision جلوی اعمال پاسخ منقضی را میگیرد. reducer بالا تمرین استخراج همین تصمیمهاست و تابع آمادهای به نام reduce در فایل پروژه وجود ندارد.
نتیجهٔ قدیمی نباید رابط تازه را تغییر بدهد
در پروژهٔ پشتیبانی، epoch هنگام تعویض هویت افزایش مییابد. request نیز ترتیب بازکردن رشتهها را مشخص میکند. اگر آلیس یک تیکت را باز کند و پیش از رسیدن پاسخ، هویت به باب تغییر کند، نتیجهٔ قبلی نباید در صفحهٔ باب نمایش داده شود. این کنترل رابط جای بررسی مالکیت روی سرور را نمیگیرد؛ هر دو لازماند. نمونهٔ زیر بدون شبکه، مسئله را تکرارپذیر میکند و در Node.js یا کنسول مرورگر اجرا میشود.
let revision = 0;
let visible = '';
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
async function show(label, ms) {
const own = ++revision;
await delay(ms);
if (own !== revision) return;
visible = label;
}
await Promise.all([show('قدیمی', 30), show('جدید', 1)]);
console.log(visible); // جدیدشمارنده، ترتیب شروع را نگه میدارد؛ زمان پایان قابل اتکا نیست. در مرورگر واقعی بهتر است درخواست خواندنی قبلی را با AbortController نیز لغو کنید، اما شرط نسخه را نگه دارید. لغو ممکن است وقتی انجام شود که بخشی از کار تمام شده است. finally هم نباید بدون کنترل نسخه، دکمهٔ درخواست جدید را دوباره فعال کند. مالک وضعیت دریافت باید همان عملیاتی باشد که هنوز معتبر است.
پیشنویس به عملیات شبکه وابسته نیست
در projects/support/app.mjs، فرم پاسخ فقط بعد از دریافت موفق پاک میشود. در خطای 409، متن در input باقی میماند و کاربر نسخهٔ تازه را بازخوانی میکند. دکمههای صفحه با busy قفل میشوند تا ارسال دوبارهٔ فوری کمتر شود. این قفل تضمین سرور نیست و جلوی درخواست یک تب دیگر را نمیگیرد. همچنین تغییر هویت یا بازکردن تیکتی دیگر، اگر متن ذخیرهنشده وجود داشته باشد، از کاربر تصمیم میخواهد. حفظ متن بخشی از مدل تعامل است، نه تزئین رابط.
خطا را به پیام قابل اقدام تبدیل کنید: چه کاری انجام نشد، آیا متن حفظ شده است و قدم بعدی چیست؟ نمایش صرف عبارت Error برای دانشجو مفید نیست. در عین حال پیام «ارسال ناموفق بود» را وقتی نتیجه واقعاً نامعلوم است با اطمینان ننویسید؛ قطع ارتباط پس از ثبت سرور، وضع متفاوتی دارد که در درس بیستم بررسی میکنیم.
تمرین، پاسخ راهنما و معیار تحویل
تمرین مستقل: reducer را طوری گسترش دهید که رویداد reset وضعیت آغازین را برگرداند و failure بدون message معتبر را رد کند. سپس سناریوی «دریافت کند اول، دریافت سریع دوم، خطای اول» را بنویسید. پاسخ راهنما این است که فقط آخرین شناسه اجازهٔ ارسال رویداد به reducer داشته باشد؛ صرف تغییر reducer پاسخ قدیمی را متوقف نمیکند. برای پیشنویس نیز یک رشتهٔ جدا از دادهٔ دریافتی نگه دارید و آن را در شروع درخواست پاک نکنید.
تحویل قابل قبول باید سه شاهد داشته باشد: نتیجهٔ خالی با خطا اشتباه نشده، پاسخ منقضی آخرین نتیجه را عوض نکرده و متن کاربر پس از خطای کنترلشده باقی مانده است. از صفحهکلید هم دکمهٔ تلاش دوباره را فعال کنید. اگر فقط مسیر موفقیت را نشان دادهاید، این تمرین کامل نیست. در گزارش کوتاه بنویسید کدام تصمیمها روی رابطاند و کدام تضمینها باید از سرور بیایند.
منابع رسمی برای مطالعهٔ بیشتر
https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
https://developer.mozilla.org/en-US/docs/Web/API/AbortController
تألیف و مثالهای اختصاصی: تحریریه درسآوند.