24 KiB
ساخت یک اپلیکیشن بانکی بخش ۴: مفاهیم مدیریت وضعیت
آزمون پیش از درس
مقدمه
با رشد یک اپلیکیشن وب، پیگیری جریانهای داده به یک چالش تبدیل میشود. کدام کد دادهها را دریافت میکند، کدام صفحه از آن استفاده میکند، کجا و چه زمانی باید بهروزرسانی شود... بهراحتی میتوان به کدی شلوغ و دشوار برای نگهداری رسید. این موضوع بهویژه زمانی صادق است که نیاز به اشتراکگذاری دادهها بین صفحات مختلف اپلیکیشن خود دارید، مثلاً دادههای کاربر. مفهوم مدیریت وضعیت همیشه در انواع برنامهها وجود داشته است، اما با افزایش پیچیدگی اپلیکیشنهای وب، اکنون به یک نکته کلیدی در طول توسعه تبدیل شده است.
در این بخش پایانی، اپلیکیشنی که ساختهایم را بررسی میکنیم تا نحوه مدیریت وضعیت را بازنگری کنیم، بهگونهای که از تازهسازی مرورگر در هر نقطه پشتیبانی کند و دادهها را در طول جلسات کاربر حفظ کند.
پیشنیاز
برای این درس، باید بخش دریافت دادهها از اپلیکیشن وب را تکمیل کرده باشید. همچنین باید Node.js را نصب کرده و سرور API را بهصورت محلی اجرا کنید تا بتوانید دادههای حساب را مدیریت کنید.
میتوانید با اجرای این دستور در یک ترمینال، بررسی کنید که سرور بهدرستی اجرا میشود:
curl http://localhost:5000/api
# -> should return "Bank API v1.0.0" as a result
بازنگری مدیریت وضعیت
در درس قبلی، یک مفهوم ابتدایی از وضعیت را در اپلیکیشن خود با متغیر سراسری account
معرفی کردیم که دادههای بانکی کاربر واردشده را نگه میدارد. با این حال، پیادهسازی فعلی ما دارای برخی نقصها است. صفحه را در داشبورد تازهسازی کنید. چه اتفاقی میافتد؟
سه مشکل در کد فعلی وجود دارد:
- وضعیت حفظ نمیشود، زیرا تازهسازی مرورگر شما را به صفحه ورود بازمیگرداند.
- چندین تابع وضعیت را تغییر میدهند. با رشد اپلیکیشن، پیگیری این تغییرات دشوار میشود و بهراحتی میتوان بهروزرسانی یکی از آنها را فراموش کرد.
- وضعیت پاک نمیشود، بنابراین وقتی روی خروج کلیک میکنید، دادههای حساب همچنان وجود دارند، حتی اگر در صفحه ورود باشید.
میتوانیم کد خود را بهگونهای بهروزرسانی کنیم که این مشکلات را یکییکی حل کنیم، اما این کار باعث تکرار کد بیشتر و پیچیدهتر شدن اپلیکیشن میشود. یا میتوانیم چند دقیقه مکث کنیم و استراتژی خود را بازنگری کنیم.
واقعاً چه مشکلاتی را میخواهیم اینجا حل کنیم؟
مدیریت وضعیت تماماً درباره یافتن یک رویکرد مناسب برای حل این دو مشکل خاص است:
- چگونه میتوان جریانهای داده در یک اپلیکیشن را قابلفهم نگه داشت؟
- چگونه میتوان دادههای وضعیت را همیشه با رابط کاربری هماهنگ نگه داشت (و بالعکس)؟
وقتی این موارد را حل کردید، هر مشکل دیگری که ممکن است داشته باشید یا قبلاً حل شده است یا حل آن آسانتر شده است. روشهای زیادی برای حل این مشکلات وجود دارد، اما ما با یک راهحل رایج پیش میرویم که شامل متمرکز کردن دادهها و روشهای تغییر آنها است. جریانهای داده به این صورت خواهند بود:
در اینجا بخشی که دادهها بهطور خودکار نمای را بهروزرسانی میکنند پوشش داده نمیشود، زیرا این موضوع به مفاهیم پیشرفتهتر برنامهنویسی واکنشی مرتبط است. اگر به دنبال یک مطالعه عمیق هستید، این موضوع میتواند یک موضوع پیگیری خوب باشد.
✅ کتابخانههای زیادی با رویکردهای مختلف برای مدیریت وضعیت وجود دارند، Redux یکی از گزینههای محبوب است. به مفاهیم و الگوهای استفادهشده نگاهی بیندازید، زیرا اغلب راه خوبی برای یادگیری مشکلات احتمالی است که ممکن است در اپلیکیشنهای وب بزرگ با آنها مواجه شوید و نحوه حل آنها.
وظیفه
با کمی بازسازی کد شروع میکنیم. اعلان account
را جایگزین کنید:
let account = null;
با:
let state = {
account: null
};
ایده این است که تمام دادههای اپلیکیشن خود را در یک شیء وضعیت واحد متمرکز کنیم. در حال حاضر فقط account
را در وضعیت داریم، بنابراین تغییر زیادی ایجاد نمیکند، اما مسیری برای تکامل ایجاد میکند.
همچنین باید توابعی که از آن استفاده میکنند را بهروزرسانی کنیم. در توابع register()
و login()
، account = ...
را با state.account = ...
جایگزین کنید.
در ابتدای تابع updateDashboard()
، این خط را اضافه کنید:
const account = state.account;
این بازسازی بهتنهایی بهبود زیادی ایجاد نکرد، اما ایده این بود که پایهای برای تغییرات بعدی ایجاد کنیم.
پیگیری تغییرات دادهها
اکنون که شیء state
را برای ذخیره دادههای خود ایجاد کردهایم، گام بعدی متمرکز کردن بهروزرسانیها است. هدف این است که پیگیری هرگونه تغییر و زمان وقوع آنها آسانتر شود.
برای جلوگیری از ایجاد تغییرات در شیء state
، همچنین یک تمرین خوب است که آن را غیرقابلتغییر در نظر بگیریم، به این معنی که اصلاً نمیتوان آن را تغییر داد. این همچنین به این معنی است که اگر میخواهید چیزی را در آن تغییر دهید، باید یک شیء وضعیت جدید ایجاد کنید. با انجام این کار، از اثرات جانبی ناخواسته side effects محافظت میکنید و امکان ایجاد ویژگیهای جدید در اپلیکیشن خود مانند پیادهسازی undo/redo را فراهم میکنید، در حالی که اشکالزدایی را نیز آسانتر میکنید. برای مثال، میتوانید هر تغییری که در وضعیت ایجاد میشود را ثبت کنید و تاریخچه تغییرات را نگه دارید تا منبع یک باگ را درک کنید.
در جاوااسکریپت، میتوانید از Object.freeze()
برای ایجاد نسخهای غیرقابلتغییر از یک شیء استفاده کنید. اگر سعی کنید تغییراتی در یک شیء غیرقابلتغییر ایجاد کنید، یک استثنا ایجاد میشود.
✅ آیا تفاوت بین یک شیء غیرقابلتغییر سطحی و عمیق را میدانید؟ میتوانید درباره آن اینجا بخوانید.
وظیفه
یک تابع جدید به نام updateState()
ایجاد کنید:
function updateState(property, newData) {
state = Object.freeze({
...state,
[property]: newData
});
}
در این تابع، یک شیء وضعیت جدید ایجاد میکنیم و دادهها را از وضعیت قبلی با استفاده از عملگر گسترش (...
) کپی میکنیم. سپس یک ویژگی خاص از شیء وضعیت را با دادههای جدید با استفاده از نشانهگذاری براکت [property]
برای تخصیص بازنویسی میکنیم. در نهایت، شیء را قفل میکنیم تا از تغییرات جلوگیری کنیم با استفاده از Object.freeze()
. در حال حاضر فقط ویژگی account
را در وضعیت ذخیره کردهایم، اما با این رویکرد میتوانید به تعداد مورد نیاز ویژگی به وضعیت اضافه کنید.
همچنین مقداردهی اولیه state
را بهروزرسانی میکنیم تا مطمئن شویم وضعیت اولیه نیز قفل شده است:
let state = Object.freeze({
account: null
});
سپس تابع register
را بهروزرسانی کنید و تخصیص state.account = result;
را با این جایگزین کنید:
updateState('account', result);
همین کار را با تابع login
انجام دهید و تخصیص state.account = data;
را با این جایگزین کنید:
updateState('account', data);
اکنون از این فرصت استفاده میکنیم تا مشکل پاک نشدن دادههای حساب هنگام کلیک کاربر روی خروج را برطرف کنیم.
یک تابع جدید به نام logout()
ایجاد کنید:
function logout() {
updateState('account', null);
navigate('/login');
}
در updateDashboard()
، تغییر مسیر return navigate('/login');
را با return logout();
جایگزین کنید.
یک حساب جدید ثبت کنید، خارج شوید و دوباره وارد شوید تا بررسی کنید که همه چیز همچنان بهدرستی کار میکند.
نکته: میتوانید با افزودن
console.log(state)
در انتهایupdateState()
و باز کردن کنسول در ابزارهای توسعه مرورگر خود، به تمام تغییرات وضعیت نگاهی بیندازید.
حفظ وضعیت
بیشتر اپلیکیشنهای وب برای عملکرد صحیح نیاز به حفظ دادهها دارند. تمام دادههای حیاتی معمولاً در یک پایگاه داده ذخیره میشوند و از طریق یک سرور API دسترسی پیدا میکنند، مانند دادههای حساب کاربر در مورد ما. اما گاهی اوقات، حفظ برخی دادهها در اپلیکیشن کلاینت که در مرورگر شما اجرا میشود نیز جالب است، برای تجربه کاربری بهتر یا بهبود عملکرد بارگذاری.
هنگامی که میخواهید دادهها را در مرورگر خود حفظ کنید، چند سؤال مهم وجود دارد که باید از خود بپرسید:
- آیا دادهها حساس هستند؟ باید از ذخیره هرگونه داده حساس در کلاینت، مانند رمز عبور کاربر، خودداری کنید.
- برای چه مدت به این دادهها نیاز دارید؟ آیا قصد دارید فقط برای جلسه فعلی به این دادهها دسترسی داشته باشید یا میخواهید برای همیشه ذخیره شوند؟
روشهای متعددی برای ذخیره اطلاعات در یک اپلیکیشن وب وجود دارد، بسته به آنچه میخواهید به دست آورید. برای مثال، میتوانید از URLها برای ذخیره یک جستجو استفاده کنید و آن را بین کاربران به اشتراک بگذارید. همچنین میتوانید از کوکیهای HTTP استفاده کنید اگر دادهها نیاز به اشتراک با سرور دارند، مانند اطلاعات احراز هویت.
گزینه دیگر استفاده از یکی از بسیاری از APIهای مرورگر برای ذخیره دادهها است. دو مورد از آنها بهویژه جالب هستند:
localStorage
: یک فروشگاه کلید/مقدار که امکان حفظ دادههای خاص وبسایت فعلی را در جلسات مختلف فراهم میکند. دادههای ذخیرهشده در آن هرگز منقضی نمیشوند.sessionStorage
: این یکی همانندlocalStorage
کار میکند، با این تفاوت که دادههای ذخیرهشده در آن هنگام پایان جلسه (بسته شدن مرورگر) پاک میشوند.
توجه داشته باشید که هر دوی این APIها فقط اجازه ذخیره رشتهها را میدهند. اگر میخواهید اشیاء پیچیده را ذخیره کنید، باید آن را به فرمت JSON با استفاده از JSON.stringify()
سریالسازی کنید.
✅ اگر میخواهید یک اپلیکیشن وب ایجاد کنید که با سرور کار نمیکند، همچنین میتوانید با استفاده از API IndexedDB
یک پایگاه داده در کلاینت ایجاد کنید. این یکی برای موارد استفاده پیشرفته یا اگر نیاز به ذخیره مقدار زیادی داده دارید، رزرو شده است، زیرا استفاده از آن پیچیدهتر است.
وظیفه
ما میخواهیم کاربران ما تا زمانی که بهصراحت روی دکمه خروج کلیک نکنند، وارد سیستم بمانند، بنابراین از localStorage
برای ذخیره دادههای حساب استفاده خواهیم کرد. ابتدا، یک کلید تعریف کنید که برای ذخیره دادههای خود از آن استفاده خواهیم کرد:
const storageKey = 'savedAccount';
سپس این خط را در انتهای تابع updateState()
اضافه کنید:
localStorage.setItem(storageKey, JSON.stringify(state.account));
با این کار، دادههای حساب کاربر حفظ میشوند و همیشه بهروز خواهند بود، زیرا قبلاً تمام بهروزرسانیهای وضعیت خود را متمرکز کردهایم. اینجاست که شروع به بهرهبرداری از تمام بازسازیهای قبلی خود میکنیم 🙂.
از آنجا که دادهها ذخیره میشوند، همچنین باید هنگام بارگذاری اپلیکیشن آنها را بازیابی کنیم. از آنجا که شروع به داشتن کد مقداردهی اولیه بیشتری میکنیم، ممکن است ایده خوبی باشد که یک تابع جدید init
ایجاد کنیم، که شامل کد قبلی ما در انتهای app.js
نیز باشد:
function init() {
const savedAccount = localStorage.getItem(storageKey);
if (savedAccount) {
updateState('account', JSON.parse(savedAccount));
}
// Our previous initialization code
window.onpopstate = () => updateRoute();
updateRoute();
}
init();
در اینجا دادههای ذخیرهشده را بازیابی میکنیم، و اگر دادهای وجود داشته باشد، وضعیت را مطابق آن بهروزرسانی میکنیم. مهم است که این کار را قبل از بهروزرسانی مسیر انجام دهیم، زیرا ممکن است کدی وجود داشته باشد که در طول بهروزرسانی صفحه به وضعیت متکی باشد.
همچنین میتوانیم صفحه داشبورد را بهعنوان صفحه پیشفرض اپلیکیشن خود قرار دهیم، زیرا اکنون دادههای حساب را حفظ میکنیم. اگر دادهای یافت نشود، داشبورد به هر حال مسئول تغییر مسیر به صفحه ورود است. در updateRoute()
، بازگشت پیشفرض return navigate('/login');
را با return navigate('/dashboard');
جایگزین کنید.
اکنون وارد اپلیکیشن شوید و سعی کنید صفحه را تازهسازی کنید. باید در داشبورد بمانید. با این بهروزرسانی، تمام مشکلات اولیه خود را برطرف کردهایم...
تازهسازی دادهها
...اما ممکن است یک مشکل جدید ایجاد کرده باشیم. اوه!
به داشبورد بروید و از حساب test
استفاده کنید، سپس این دستور را در یک ترمینال اجرا کنید تا یک تراکنش جدید ایجاد کنید:
curl --request POST \
--header "Content-Type: application/json" \
--data "{ \"date\": \"2020-07-24\", \"object\": \"Bought book\", \"amount\": -20 }" \
http://localhost:5000/api/accounts/test/transactions
اکنون سعی کنید صفحه داشبورد را در مرورگر تازهسازی کنید. چه اتفاقی میافتد؟ آیا تراکنش جدید را میبینید؟
وضعیت به لطف localStorage
بهطور نامحدود حفظ میشود، اما این همچنین به این معنی است که تا زمانی که از اپلیکیشن خارج نشوید و دوباره وارد نشوید، هرگز بهروزرسانی نمیشود!
یک استراتژی ممکن برای رفع این مشکل این است که هر بار که داشبورد بارگذاری میشود، دادههای حساب را دوباره بارگذاری کنیم تا از دادههای قدیمی جلوگیری کنیم.
وظیفه
یک تابع جدید به نام updateAccountData
ایجاد کنید:
async function updateAccountData() {
const account = state.account;
if (!account) {
return logout();
}
const data = await getAccount(account.user);
if (data.error) {
return logout();
}
updateState('account', data);
}
این روش بررسی میکند که آیا در حال حاضر وارد سیستم شدهایم، سپس دادههای حساب را از سرور دوباره بارگذاری میکند.
یک تابع دیگر به نام refresh
ایجاد کنید:
async function refresh() {
await updateAccountData();
updateDashboard();
}
این یکی دادههای حساب را بهروزرسانی میکند، سپس HTML صفحه داشبورد را بهروزرسانی میکند. این همان چیزی است که باید هنگام بارگذاری مسیر داشبورد فراخوانی کنیم. تعریف مسیر را با این بهروزرسانی کنید:
const routes = {
'/login': { templateId: 'login' },
'/dashboard': { templateId: 'dashboard', init: refresh }
};
اکنون سعی کنید داشبورد را تازهسازی کنید، باید دادههای حساب بهروزرسانیشده را نمایش دهد.
🚀 چالش
اکنون که هر بار که داشبورد بارگذاری میشود دادههای حساب را دوباره بارگذاری میکنیم، آیا فکر میکنید هنوز نیاز داریم تمام دادههای حساب را حفظ کنیم؟
سعی کنید با هم کار کنید تا آنچه را که از localStorage
ذخیره و بارگذاری میشود تغییر دهید تا فقط شامل آنچه که برای عملکرد اپلیکیشن کاملاً ضروری است باشد.
آزمون پس از درس
تکلیف
پیادهسازی "افزودن تراکنش" در دیالوگ
در اینجا یک نمونه نتیجه پس از تکمیل وظیفه آورده شده است:
سلب مسئولیت:
این سند با استفاده از سرویس ترجمه هوش مصنوعی Co-op Translator ترجمه شده است. در حالی که ما برای دقت تلاش میکنیم، لطفاً توجه داشته باشید که ترجمههای خودکار ممکن است شامل خطاها یا نادرستیهایی باشند. سند اصلی به زبان اصلی آن باید به عنوان منبع معتبر در نظر گرفته شود. برای اطلاعات حساس، ترجمه حرفهای انسانی توصیه میشود. ما هیچ مسئولیتی در قبال سوءتفاهمها یا تفسیرهای نادرست ناشی از استفاده از این ترجمه نداریم.