|
|
<!--
|
|
|
CO_OP_TRANSLATOR_METADATA:
|
|
|
{
|
|
|
"original_hash": "4fa20c513e367e9cdd401bf49ae16e33",
|
|
|
"translation_date": "2025-08-27T21:05:39+00:00",
|
|
|
"source_file": "7-bank-project/4-state-management/README.md",
|
|
|
"language_code": "he"
|
|
|
}
|
|
|
-->
|
|
|
# בניית אפליקציית בנקאות חלק 4: מושגים בניהול מצב
|
|
|
|
|
|
## שאלון לפני השיעור
|
|
|
|
|
|
[שאלון לפני השיעור](https://ff-quizzes.netlify.app/web/quiz/47)
|
|
|
|
|
|
### הקדמה
|
|
|
|
|
|
ככל שאפליקציית ווב גדלה, קשה יותר לעקוב אחר כל זרימות הנתונים. איזה קוד מקבל את הנתונים, איזה עמוד צורך אותם, איפה ומתי צריך לעדכן אותם... קל להגיע לקוד מבולגן שקשה לתחזק. זה נכון במיוחד כשצריך לשתף נתונים בין עמודים שונים באפליקציה, כמו נתוני משתמש. מושג *ניהול מצב* תמיד היה קיים בכל סוגי התוכניות, אבל ככל שאפליקציות ווב ממשיכות לגדול במורכבות, זה הפך לנקודה מרכזית שיש לחשוב עליה במהלך הפיתוח.
|
|
|
|
|
|
בחלק האחרון הזה, נבחן את האפליקציה שבנינו כדי לחשוב מחדש על איך המצב מנוהל, כך שנוכל לתמוך ברענון הדפדפן בכל נקודה ולשמר נתונים בין סשנים של משתמשים.
|
|
|
|
|
|
### דרישות מקדימות
|
|
|
|
|
|
עליכם להשלים את חלק [שאיבת הנתונים](../3-data/README.md) של אפליקציית הווב עבור שיעור זה. כמו כן, עליכם להתקין את [Node.js](https://nodejs.org) ולהריץ את [שרת ה-API](../api/README.md) באופן מקומי כדי שתוכלו לנהל נתוני חשבון.
|
|
|
|
|
|
ניתן לבדוק שהשרת פועל כראוי על ידי ביצוע הפקודה הזו בטרמינל:
|
|
|
|
|
|
```sh
|
|
|
curl http://localhost:5000/api
|
|
|
# -> should return "Bank API v1.0.0" as a result
|
|
|
```
|
|
|
|
|
|
---
|
|
|
|
|
|
## חשיבה מחדש על ניהול מצב
|
|
|
|
|
|
בשיעור [הקודם](../3-data/README.md), הצגנו מושג בסיסי של מצב באפליקציה שלנו עם המשתנה הגלובלי `account` שמכיל את נתוני הבנק של המשתמש המחובר כרגע. עם זאת, היישום הנוכחי שלנו מכיל כמה פגמים. נסו לרענן את העמוד כשאתם נמצאים בלוח הבקרה. מה קורה?
|
|
|
|
|
|
ישנן 3 בעיות בקוד הנוכחי:
|
|
|
|
|
|
- המצב אינו נשמר, רענון הדפדפן מחזיר אתכם לעמוד ההתחברות.
|
|
|
- ישנן פונקציות רבות שמעדכנות את המצב. ככל שהאפליקציה גדלה, זה יכול להקשות על מעקב אחר השינויים וקל לשכוח לעדכן אחת מהן.
|
|
|
- המצב אינו מנוקה, כך שכאשר לוחצים על *התנתקות*, נתוני החשבון עדיין שם למרות שאתם בעמוד ההתחברות.
|
|
|
|
|
|
יכולנו לעדכן את הקוד שלנו כדי להתמודד עם הבעיות הללו אחת אחת, אבל זה היה יוצר כפילות קוד רבה והופך את האפליקציה למורכבת יותר וקשה יותר לתחזוקה. או שיכולנו לעצור לכמה דקות ולחשוב מחדש על האסטרטגיה שלנו.
|
|
|
|
|
|
> אילו בעיות אנחנו באמת מנסים לפתור כאן?
|
|
|
|
|
|
[ניהול מצב](https://en.wikipedia.org/wiki/State_management) עוסק במציאת גישה טובה לפתרון שתי הבעיות הספציפיות הללו:
|
|
|
|
|
|
- איך לשמור על זרימות הנתונים באפליקציה מובנות?
|
|
|
- איך לשמור על נתוני המצב תמיד מסונכרנים עם ממשק המשתמש (ולהיפך)?
|
|
|
|
|
|
ברגע שטיפלתם בזה, כל בעיה אחרת שעשויה להיות לכם עשויה להיפתר כבר או להפוך לקלה יותר לפתרון. ישנן גישות רבות לפתרון בעיות אלו, אבל נבחר פתרון נפוץ שמורכב מ**מרכזיות הנתונים והדרכים לשנות אותם**. זרימות הנתונים ייראו כך:
|
|
|
|
|
|

|
|
|
|
|
|
> לא נעסוק כאן בחלק שבו הנתונים מעדכנים אוטומטית את התצוגה, מכיוון שזה קשור למושגים מתקדמים יותר של [תכנות תגובתי](https://en.wikipedia.org/wiki/Reactive_programming). זה נושא מעמיק טוב אם אתם מעוניינים.
|
|
|
|
|
|
✅ יש הרבה ספריות שם בחוץ עם גישות שונות לניהול מצב, [Redux](https://redux.js.org) היא אפשרות פופולרית. כדאי להסתכל על המושגים והתבניות שבהם משתמשים, מכיוון שזה לעיתים קרובות דרך טובה ללמוד אילו בעיות פוטנציאליות אתם עשויים להתמודד איתן באפליקציות ווב גדולות ואיך ניתן לפתור אותן.
|
|
|
|
|
|
### משימה
|
|
|
|
|
|
נתחיל עם קצת ריפקטורינג. החליפו את ההצהרה `account`:
|
|
|
|
|
|
```js
|
|
|
let account = null;
|
|
|
```
|
|
|
|
|
|
ב:
|
|
|
|
|
|
```js
|
|
|
let state = {
|
|
|
account: null
|
|
|
};
|
|
|
```
|
|
|
|
|
|
הרעיון הוא *לרכז* את כל נתוני האפליקציה שלנו באובייקט מצב יחיד. כרגע יש לנו רק `account` במצב, כך שזה לא משנה הרבה, אבל זה יוצר דרך להתפתחויות עתידיות.
|
|
|
|
|
|
עלינו גם לעדכן את הפונקציות שמשתמשות בו. בפונקציות `register()` ו-`login()`, החליפו `account = ...` ב-`state.account = ...`;
|
|
|
|
|
|
בראש הפונקציה `updateDashboard()`, הוסיפו את השורה הזו:
|
|
|
|
|
|
```js
|
|
|
const account = state.account;
|
|
|
```
|
|
|
|
|
|
הריפקטורינג הזה בפני עצמו לא הביא שיפורים רבים, אבל הרעיון היה להניח את היסודות לשינויים הבאים.
|
|
|
|
|
|
## מעקב אחר שינויים בנתונים
|
|
|
|
|
|
עכשיו כשיש לנו את אובייקט ה-`state` לאחסון הנתונים שלנו, השלב הבא הוא לרכז את העדכונים. המטרה היא להקל על מעקב אחר כל שינוי ומתי הוא מתרחש.
|
|
|
|
|
|
כדי להימנע משינויים באובייקט ה-`state`, זה גם נוהג טוב לשקול אותו [*בלתי ניתן לשינוי*](https://en.wikipedia.org/wiki/Immutable_object), כלומר שהוא לא יכול להיות שונה כלל. זה גם אומר שעליכם ליצור אובייקט מצב חדש אם אתם רוצים לשנות משהו בו. על ידי כך, אתם בונים הגנה מפני [תופעות לוואי](https://en.wikipedia.org/wiki/Side_effect_(computer_science)) לא רצויות, ופותחים אפשרויות לתכונות חדשות באפליקציה שלכם כמו יישום undo/redo, תוך כדי הקלה על איתור באגים. לדוגמה, תוכלו לרשום כל שינוי שנעשה במצב ולשמור היסטוריה של השינויים כדי להבין את מקור הבאג.
|
|
|
|
|
|
ב-JavaScript, ניתן להשתמש ב-[`Object.freeze()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) כדי ליצור גרסה בלתי ניתנת לשינוי של אובייקט. אם תנסו לבצע שינויים באובייקט בלתי ניתן לשינוי, תתעורר חריגה.
|
|
|
|
|
|
✅ האם אתם יודעים את ההבדל בין אובייקט בלתי ניתן לשינוי *שטחי* לבין *עמוק*? תוכלו לקרוא על כך [כאן](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze#What_is_shallow_freeze).
|
|
|
|
|
|
### משימה
|
|
|
|
|
|
בואו ניצור פונקציה חדשה בשם `updateState()`:
|
|
|
|
|
|
```js
|
|
|
function updateState(property, newData) {
|
|
|
state = Object.freeze({
|
|
|
...state,
|
|
|
[property]: newData
|
|
|
});
|
|
|
}
|
|
|
```
|
|
|
|
|
|
בפונקציה זו, אנחנו יוצרים אובייקט מצב חדש ומעתיקים נתונים מהמצב הקודם באמצעות [*אופרטור הפיזור (`...`)*](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals). לאחר מכן אנחנו מחליפים תכונה מסוימת של אובייקט המצב עם הנתונים החדשים באמצעות [הסימון הסוגריים](https://developer.mozilla.org/docs/Web/JavaScript/Guide/Working_with_Objects#Objects_and_properties) `[property]` להקצאה. לבסוף, אנחנו נועלים את האובייקט כדי למנוע שינויים באמצעות `Object.freeze()`. כרגע יש לנו רק את התכונה `account` שמאוחסנת במצב, אבל עם הגישה הזו תוכלו להוסיף כמה תכונות שתרצו במצב.
|
|
|
|
|
|
נעדכן גם את אתחול ה-`state` כדי לוודא שהמצב ההתחלתי נעול גם הוא:
|
|
|
|
|
|
```js
|
|
|
let state = Object.freeze({
|
|
|
account: null
|
|
|
});
|
|
|
```
|
|
|
|
|
|
לאחר מכן, עדכנו את הפונקציה `register` על ידי החלפת ההקצאה `state.account = result;` ב:
|
|
|
|
|
|
```js
|
|
|
updateState('account', result);
|
|
|
```
|
|
|
|
|
|
עשו את אותו הדבר עם הפונקציה `login`, החליפו `state.account = data;` ב:
|
|
|
|
|
|
```js
|
|
|
updateState('account', data);
|
|
|
```
|
|
|
|
|
|
עכשיו ניקח את ההזדמנות לתקן את הבעיה של נתוני החשבון שלא מתנקים כאשר המשתמש לוחץ על *התנתקות*.
|
|
|
|
|
|
צרו פונקציה חדשה בשם `logout()`:
|
|
|
|
|
|
```js
|
|
|
function logout() {
|
|
|
updateState('account', null);
|
|
|
navigate('/login');
|
|
|
}
|
|
|
```
|
|
|
|
|
|
ב-`updateDashboard()`, החליפו את ההפניה `return navigate('/login');` ב-`return logout()`;
|
|
|
|
|
|
נסו לרשום חשבון חדש, להתנתק ולהתחבר שוב כדי לבדוק שהכל עדיין עובד כראוי.
|
|
|
|
|
|
> טיפ: תוכלו להסתכל על כל השינויים במצב על ידי הוספת `console.log(state)` בתחתית `updateState()` ופתיחת הקונסול בכלי הפיתוח של הדפדפן שלכם.
|
|
|
|
|
|
## שמירת המצב
|
|
|
|
|
|
רוב אפליקציות הווב צריכות לשמור נתונים כדי לעבוד כראוי. כל הנתונים הקריטיים בדרך כלל נשמרים בבסיס נתונים ונגישים דרך שרת API, כמו נתוני חשבון המשתמש במקרה שלנו. אבל לפעמים, זה גם מעניין לשמור נתונים באפליקציה בצד הלקוח שרצה בדפדפן שלכם, לשיפור חוויית המשתמש או לשיפור ביצועי הטעינה.
|
|
|
|
|
|
כשאתם רוצים לשמור נתונים בדפדפן שלכם, יש כמה שאלות חשובות שכדאי לשאול את עצמכם:
|
|
|
|
|
|
- *האם הנתונים רגישים?* כדאי להימנע מאחסון נתונים רגישים בצד הלקוח, כמו סיסמאות משתמש.
|
|
|
- *לכמה זמן אתם צריכים לשמור את הנתונים האלה?* האם אתם מתכננים לגשת לנתונים רק עבור הסשן הנוכחי או שאתם רוצים שהם יישמרו לנצח?
|
|
|
|
|
|
ישנן דרכים רבות לאחסן מידע בתוך אפליקציית ווב, תלוי במה שאתם רוצים להשיג. לדוגמה, תוכלו להשתמש ב-URLs כדי לשמור שאילתת חיפוש, ולהפוך אותה לשיתופית בין משתמשים. תוכלו גם להשתמש ב-[עוגיות HTTP](https://developer.mozilla.org/docs/Web/HTTP/Cookies) אם הנתונים צריכים להיות משותפים עם השרת, כמו מידע [אימות](https://en.wikipedia.org/wiki/Authentication).
|
|
|
|
|
|
אפשרות נוספת היא להשתמש באחת מה-APIs הרבות של הדפדפן לאחסון נתונים. שתיים מהן מעניינות במיוחד:
|
|
|
|
|
|
- [`localStorage`](https://developer.mozilla.org/docs/Web/API/Window/localStorage): [מאגר מפתח/ערך](https://en.wikipedia.org/wiki/Key%E2%80%93value_database) שמאפשר לשמור נתונים ספציפיים לאתר הנוכחי בין סשנים שונים. הנתונים שנשמרים בו לעולם לא פג תוקפם.
|
|
|
- [`sessionStorage`](https://developer.mozilla.org/docs/Web/API/Window/sessionStorage): זה עובד באותו אופן כמו `localStorage` מלבד שהנתונים שנשמרים בו נמחקים כאשר הסשן מסתיים (כאשר הדפדפן נסגר).
|
|
|
|
|
|
שימו לב ששתי ה-APIs הללו מאפשרות רק לשמור [מחרוזות](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String). אם אתם רוצים לשמור אובייקטים מורכבים, תצטרכו לסדר אותם בפורמט [JSON](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/JSON) באמצעות [`JSON.stringify()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify).
|
|
|
|
|
|
✅ אם אתם רוצים ליצור אפליקציית ווב שלא עובדת עם שרת, זה גם אפשרי ליצור בסיס נתונים בצד הלקוח באמצעות [API של `IndexedDB`](https://developer.mozilla.org/docs/Web/API/IndexedDB_API). זה שמור למקרים מתקדמים או אם אתם צריכים לשמור כמות משמעותית של נתונים, מכיוון שזה מורכב יותר לשימוש.
|
|
|
|
|
|
### משימה
|
|
|
|
|
|
אנחנו רוצים שהמשתמשים שלנו יישארו מחוברים עד שהם לוחצים באופן מפורש על כפתור *התנתקות*, אז נשתמש ב-`localStorage` כדי לשמור את נתוני החשבון. קודם כל, נגדיר מפתח שנשתמש בו כדי לשמור את הנתונים שלנו.
|
|
|
|
|
|
```js
|
|
|
const storageKey = 'savedAccount';
|
|
|
```
|
|
|
|
|
|
לאחר מכן, הוסיפו את השורה הזו בסוף הפונקציה `updateState()`:
|
|
|
|
|
|
```js
|
|
|
localStorage.setItem(storageKey, JSON.stringify(state.account));
|
|
|
```
|
|
|
|
|
|
עם זה, נתוני חשבון המשתמש יישמרו ותמיד יהיו מעודכנים כפי שמרכזנו קודם את כל עדכוני המצב שלנו. כאן אנחנו מתחילים ליהנות מכל הריפקטורינג הקודם שלנו 🙂.
|
|
|
|
|
|
מכיוון שהנתונים נשמרים, עלינו גם לדאוג לשחזר אותם כאשר האפליקציה נטענת. מכיוון שאנחנו מתחילים לקבל יותר קוד אתחול, זה עשוי להיות רעיון טוב ליצור פונקציה חדשה בשם `init`, שכוללת גם את הקוד הקודם שלנו בתחתית `app.js`:
|
|
|
|
|
|
```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`, ואז הריצו את הפקודה הזו בטרמינל כדי ליצור עסקה חדשה:
|
|
|
|
|
|
```sh
|
|
|
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`:
|
|
|
|
|
|
```js
|
|
|
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`:
|
|
|
|
|
|
```js
|
|
|
async function refresh() {
|
|
|
await updateAccountData();
|
|
|
updateDashboard();
|
|
|
}
|
|
|
```
|
|
|
|
|
|
זו מעדכנת את נתוני החשבון, ואז דואגת לעדכן את ה-HTML של עמוד לוח הבקרה. זה מה שאנחנו צריכים לקרוא כאשר מסלול לוח הבקרה נטען. עדכנו את הגדרת המסלול עם:
|
|
|
|
|
|
```js
|
|
|
const routes = {
|
|
|
'/login': { templateId: 'login' },
|
|
|
'/dashboard': { templateId: 'dashboard', init: refresh }
|
|
|
};
|
|
|
```
|
|
|
|
|
|
נסו לרענן את לוח הבקרה עכשיו, הוא אמור להציג את נתוני החשבון המעודכנים.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 🚀 אתגר
|
|
|
|
|
|
עכשיו כשאנחנו טוענים מחדש את נתוני החשבון בכל פעם שלוח הבקרה נטען, האם אתם חושבים שאנחנו עדיין צריכים לשמור *את כל נתוני החשבון*?
|
|
|
|
|
|
נסו לעבוד יחד כדי לשנות מה נשמר ומה נטען מ-`localStorage` כך שיכלול רק את מה שדרוש באופן מוחלט כדי שהאפליקציה תעבוד.
|
|
|
|
|
|
## שאלון אחרי השיעור
|
|
|
[שאלון לאחר ההרצאה](https://ff-quizzes.netlify.app/web/quiz/48)
|
|
|
|
|
|
## משימה
|
|
|
|
|
|
[מימוש דיאלוג "הוספת עסקה"](assignment.md)
|
|
|
|
|
|
להלן דוגמה לתוצאה לאחר השלמת המשימה:
|
|
|
|
|
|

|
|
|
|
|
|
---
|
|
|
|
|
|
**כתב ויתור**:
|
|
|
מסמך זה תורגם באמצעות שירות תרגום מבוסס בינה מלאכותית [Co-op Translator](https://github.com/Azure/co-op-translator). למרות שאנו שואפים לדיוק, יש לקחת בחשבון שתרגומים אוטומטיים עשויים להכיל שגיאות או אי דיוקים. המסמך המקורי בשפתו המקורית צריך להיחשב כמקור סמכותי. עבור מידע קריטי, מומלץ להשתמש בתרגום מקצועי על ידי אדם. איננו נושאים באחריות לאי הבנות או לפרשנויות שגויות הנובעות משימוש בתרגום זה. |