19 KiB
בניית אפליקציית בנקאות חלק 4: מושגים בניהול מצב
שאלון לפני השיעור
הקדמה
ככל שאפליקציית ווב גדלה, קשה יותר לעקוב אחר כל זרימות הנתונים. איזה קוד מקבל את הנתונים, איזה עמוד צורך אותם, איפה ומתי צריך לעדכן אותם... קל להגיע לקוד מבולגן שקשה לתחזק. זה נכון במיוחד כשצריך לשתף נתונים בין עמודים שונים באפליקציה, למשל נתוני משתמש. מושג ניהול מצב תמיד היה קיים בכל סוגי התוכניות, אבל ככל שאפליקציות ווב ממשיכות לגדול במורכבות, זה הפך לנקודה מרכזית שיש לחשוב עליה במהלך הפיתוח.
בחלק האחרון הזה, נבחן את האפליקציה שבנינו כדי לחשוב מחדש על איך המצב מנוהל, כך שתתמוך ברענון הדפדפן בכל נקודה ותשמור נתונים בין סשנים של משתמשים.
דרישות מקדימות
עליך להשלים את חלק שאיבת הנתונים של אפליקציית הווב עבור שיעור זה. כמו כן, עליך להתקין Node.js ולהריץ את שרת ה-API באופן מקומי כדי שתוכל לנהל נתוני חשבון.
ניתן לבדוק שהשרת פועל כראוי על ידי ביצוע הפקודה הזו בטרמינל:
curl http://localhost:5000/api
# -> should return "Bank API v1.0.0" as a result
חשיבה מחדש על ניהול מצב
בשיעור הקודם, הצגנו מושג בסיסי של מצב באפליקציה שלנו עם המשתנה הגלובלי account
שמכיל את נתוני הבנק של המשתמש המחובר כרגע. עם זאת, היישום הנוכחי שלנו מכיל כמה פגמים. נסה לרענן את העמוד כשאתה נמצא בלוח הבקרה. מה קורה?
ישנן 3 בעיות בקוד הנוכחי:
- המצב אינו נשמר, רענון הדפדפן מחזיר אותך לעמוד ההתחברות.
- ישנן פונקציות רבות שמעדכנות את המצב. ככל שהאפליקציה גדלה, זה יכול להקשות על מעקב אחר השינויים וקל לשכוח לעדכן אחת מהן.
- המצב אינו מנוקה, כך שכאשר אתה לוחץ על התנתקות, נתוני החשבון עדיין שם למרות שאתה נמצא בעמוד ההתחברות.
יכולנו לעדכן את הקוד שלנו כדי להתמודד עם הבעיות הללו אחת אחת, אבל זה היה יוצר כפילות קוד רבה ומסבך את האפליקציה יותר. או שאנחנו יכולים לעצור לכמה דקות ולחשוב מחדש על האסטרטגיה שלנו.
אילו בעיות אנחנו באמת מנסים לפתור כאן?
ניהול מצב עוסק במציאת גישה טובה לפתרון שתי הבעיות הספציפיות הללו:
- איך לשמור על זרימות הנתונים באפליקציה מובנות?
- איך לשמור על נתוני המצב תמיד מסונכרנים עם ממשק המשתמש (ולהפך)?
ברגע שתטפל בזה, כל בעיה אחרת שעשויה להיות לך עשויה כבר להיפתר או להפוך לקלה יותר לפתרון. ישנן גישות רבות לפתרון בעיות אלו, אבל נבחר פתרון נפוץ שמורכב ממרכזיות הנתונים והדרכים לשנות אותם. זרימות הנתונים ייראו כך:
לא נעסוק כאן בחלק שבו הנתונים מעדכנים את התצוגה באופן אוטומטי, מכיוון שזה קשור למושגים מתקדמים יותר של תכנות תגובתי. זה נושא מעמיק טוב אם אתה מעוניין.
✅ יש הרבה ספריות עם גישות שונות לניהול מצב, Redux היא אפשרות פופולרית. כדאי להסתכל על המושגים והתבניות שבהם משתמשים, שכן זה לעיתים קרובות דרך טובה ללמוד על בעיות פוטנציאליות שאתה עשוי להתמודד איתן באפליקציות ווב גדולות וכיצד ניתן לפתור אותן.
משימה
נתחיל עם קצת ריפקטורינג. החלף את ההצהרה של account
:
let account = null;
ב:
let state = {
account: null
};
הרעיון הוא לרכז את כל נתוני האפליקציה שלנו באובייקט מצב יחיד. כרגע יש לנו רק account
במצב, כך שזה לא משנה הרבה, אבל זה יוצר דרך להתפתחויות עתידיות.
עלינו גם לעדכן את הפונקציות שמשתמשות בו. בפונקציות register()
ו-login()
, החלף account = ...
ב-state.account = ...
;
בראש הפונקציה updateDashboard()
, הוסף את השורה הזו:
const account = state.account;
הריפקטורינג הזה בפני עצמו לא הביא שיפורים רבים, אבל הרעיון היה להניח את היסודות לשינויים הבאים.
מעקב אחר שינויים בנתונים
עכשיו כששמנו במקום את אובייקט state
לאחסון הנתונים שלנו, השלב הבא הוא לרכז את העדכונים. המטרה היא להקל על מעקב אחר כל שינוי ומתי הוא מתרחש.
כדי להימנע משינויים באובייקט state
, זה גם נוהג טוב לשקול אותו בלתי ניתן לשינוי, כלומר שהוא לא יכול להיות שונה כלל. זה גם אומר שאתה צריך ליצור אובייקט מצב חדש אם אתה רוצה לשנות משהו בו. על ידי כך, אתה בונה הגנה מפני תופעות לוואי לא רצויות, ופותח אפשרויות לתכונות חדשות באפליקציה שלך כמו יישום undo/redo, תוך גם הקלה על איתור באגים. למשל, תוכל לרשום כל שינוי שנעשה במצב ולשמור היסטוריה של השינויים כדי להבין את מקור הבאג.
ב-JavaScript, ניתן להשתמש ב-Object.freeze()
כדי ליצור גרסה בלתי ניתנת לשינוי של אובייקט. אם תנסה לבצע שינויים באובייקט בלתי ניתן לשינוי, תתעורר חריגה.
✅ האם אתה יודע את ההבדל בין אובייקט בלתי ניתן לשינוי שטחי לבין עמוק? תוכל לקרוא על כך כאן.
משימה
בואו ניצור פונקציה חדשה בשם updateState()
:
function updateState(property, newData) {
state = Object.freeze({
...state,
[property]: newData
});
}
בפונקציה זו, אנחנו יוצרים אובייקט מצב חדש ומעתיקים נתונים מהמצב הקודם באמצעות אופרטור הפיזור (...
). לאחר מכן אנחנו מחליפים תכונה מסוימת של אובייקט המצב עם הנתונים החדשים באמצעות הסימון המרובע [property]
להקצאה. לבסוף, אנחנו נועלים את האובייקט כדי למנוע שינויים באמצעות Object.freeze()
. כרגע יש לנו רק את התכונה account
במצב, אבל עם הגישה הזו ניתן להוסיף כמה תכונות שצריך במצב.
נעדכן גם את אתחול המצב כדי לוודא שהמצב ההתחלתי נעול גם הוא:
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, כמו נתוני החשבון של המשתמש במקרה שלנו. אבל לפעמים, זה גם מעניין לשמור נתונים באפליקציה בצד הלקוח שרצה בדפדפן, לשיפור חוויית המשתמש או לשיפור ביצועי הטעינה.
כשאתה רוצה לשמור נתונים בדפדפן שלך, יש כמה שאלות חשובות שכדאי לשאול את עצמך:
- האם הנתונים רגישים? כדאי להימנע משמירת נתונים רגישים בצד הלקוח, כמו סיסמאות משתמש.
- לכמה זמן אתה צריך לשמור את הנתונים האלה? האם אתה מתכנן לגשת לנתונים רק עבור הסשן הנוכחי או שאתה רוצה שהם יישמרו לנצח?
ישנן דרכים רבות לאחסן מידע בתוך אפליקציית ווב, תלוי במה שאתה רוצה להשיג. למשל, ניתן להשתמש ב-URLs כדי לשמור שאילתת חיפוש, ולהפוך אותה לשיתופית בין משתמשים. ניתן גם להשתמש ב-עוגיות HTTP אם הנתונים צריכים להיות משותפים עם השרת, כמו מידע אימות.
אפשרות נוספת היא להשתמש באחת מה-APIs הרבות של הדפדפן לאחסון נתונים. שתיים מהן מעניינות במיוחד:
localStorage
: חנות מפתח/ערך שמאפשרת לשמור נתונים ספציפיים לאתר הנוכחי בין סשנים שונים. הנתונים שנשמרים בה לעולם לא פגים.sessionStorage
: זו עובדת באותו אופן כמוlocalStorage
מלבד שהנתונים שנשמרים בה נמחקים כאשר הסשן מסתיים (כאשר הדפדפן נסגר).
שימו לב ששתי ה-APIs הללו מאפשרות לשמור רק מחרוזות. אם תרצה לשמור אובייקטים מורכבים, תצטרך לסדר אותם לפורמט 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. למרות שאנו שואפים לדיוק, יש לקחת בחשבון שתרגומים אוטומטיים עשויים להכיל שגיאות או אי-דיוקים. המסמך המקורי בשפתו המקורית נחשב למקור הסמכותי. למידע קריטי, מומלץ להשתמש בתרגום מקצועי על ידי בני אדם. איננו נושאים באחריות לכל אי-הבנה או פרשנות שגויה הנובעת משימוש בתרגום זה.