|
3 weeks ago | |
---|---|---|
.. | ||
README.md | 3 weeks ago | |
assignment.md | 4 weeks ago |
README.md
ساخت یک اپلیکیشن بانکی بخش ۴: مفاهیم مدیریت وضعیت
آزمون پیش از درس
مقدمه
با رشد یک اپلیکیشن وب، پیگیری جریانهای داده دشوار میشود. کدام کد دادهها را دریافت میکند، کدام صفحه آن را مصرف میکند، کجا و چه زمانی باید بهروزرسانی شود... بهراحتی میتوان به کدی شلوغ و سخت برای نگهداری رسید. این موضوع بهویژه زمانی صادق است که نیاز به اشتراکگذاری دادهها بین صفحات مختلف اپلیکیشن خود داشته باشید، مثلاً دادههای کاربر. مفهوم مدیریت وضعیت همیشه در انواع برنامهها وجود داشته است، اما با افزایش پیچیدگی اپلیکیشنهای وب، اکنون به یک نکته کلیدی در طول توسعه تبدیل شده است.
در این بخش نهایی، اپلیکیشنی که ساختهایم را بررسی میکنیم تا نحوه مدیریت وضعیت را بازنگری کنیم، بهطوری که از تازهسازی مرورگر در هر نقطه پشتیبانی کند و دادهها را در طول جلسات کاربر حفظ کند.
پیشنیاز
برای این درس، باید بخش دریافت دادهها از اپلیکیشن وب را تکمیل کرده باشید. همچنین باید 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
، همچنین بهتر است آن را غیرقابل تغییر در نظر بگیریم، به این معنی که اصلاً نمیتوان آن را تغییر داد. این همچنین به این معنی است که اگر بخواهید چیزی را در آن تغییر دهید، باید یک شیء وضعیت جدید ایجاد کنید. با انجام این کار، از عوارض جانبی ناخواسته احتمالی محافظت میکنید و امکان ایجاد ویژگیهای جدید در اپلیکیشن خود مانند پیادهسازی 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 ترجمه شده است. در حالی که ما تلاش میکنیم دقت را حفظ کنیم، لطفاً توجه داشته باشید که ترجمههای خودکار ممکن است شامل خطاها یا نادرستیها باشند. سند اصلی به زبان اصلی آن باید به عنوان منبع معتبر در نظر گرفته شود. برای اطلاعات حساس، توصیه میشود از ترجمه انسانی حرفهای استفاده کنید. ما مسئولیتی در قبال سوء تفاهمها یا تفسیرهای نادرست ناشی از استفاده از این ترجمه نداریم.