# Създаване на банково приложение, част 4: Концепции за управление на състоянието ## Предварителен тест [Предварителен тест](https://ff-quizzes.netlify.app/web/quiz/47) ## Въведение Управлението на състоянието е като навигационната система на космическия кораб Voyager – когато всичко работи гладко, едва забелязвате, че съществува. Но когато нещо се обърка, то става разликата между достигането на междузвездното пространство и загубата в космическата пустота. В уеб разработката състоянието представлява всичко, което вашето приложение трябва да запомни: статус на вход на потребителя, данни от формуляри, история на навигацията и временни състояния на интерфейса. Докато вашето банково приложение се е развивало от прост формуляр за вход в по-сложно приложение, вероятно сте се сблъскали с някои общи предизвикателства. Обновете страницата и потребителите се излизат неочаквано. Затворете браузъра и целият напредък изчезва. Дебъгнете проблем и се оказвате, че търсите през множество функции, които модифицират едни и същи данни по различни начини. Това не са признаци на лошо програмиране – това са естествени трудности, които се появяват, когато приложенията достигнат определен праг на сложност. Всеки разработчик се сблъсква с тези предизвикателства, когато приложенията преминават от "доказателство за концепция" към "готово за производство". В този урок ще внедрим централизирана система за управление на състоянието, която ще трансформира вашето банково приложение в надеждно, професионално приложение. Ще научите как да управлявате потоците от данни предсказуемо, да запазвате потребителските сесии по подходящ начин и да създавате гладко потребителско изживяване, което изискват съвременните уеб приложения. ## Предпоставки Преди да се потопите в концепциите за управление на състоянието, трябва да имате правилно настроена среда за разработка и основата на вашето банково приложение. Този урок се основава директно на концепциите и кода от предишните части на тази серия. Уверете се, че разполагате със следните компоненти, преди да продължите: **Необходима настройка:** - Завършете урока за [извличане на данни](../3-data/README.md) - вашето приложение трябва успешно да зарежда и показва данни за акаунта - Инсталирайте [Node.js](https://nodejs.org) на вашата система за стартиране на бекенд API - Стартирайте [сървърния API](../api/README.md) локално за обработка на операции с данни за акаунта **Тестване на вашата среда:** Проверете дали вашият API сървър работи правилно, като изпълните тази команда в терминала: ```sh curl http://localhost:5000/api # -> should return "Bank API v1.0.0" as a result ``` **Какво прави тази команда:** - **Изпраща** GET заявка към вашия локален API сървър - **Тества** връзката и проверява дали сървърът отговаря - **Връща** информация за версията на API, ако всичко работи правилно --- ## Диагностика на текущите проблеми със състоянието Като Шерлок Холмс, който изследва местопрестъпление, трябва да разберем точно какво се случва в текущата ни имплементация, преди да можем да решим мистерията на изчезващите потребителски сесии. Нека проведем прост експеримент, който разкрива основните предизвикателства при управлението на състоянието: **🧪 Опитайте този диагностичен тест:** 1. Влезте в банковото си приложение и отидете на таблото за управление 2. Обновете страницата на браузъра 3. Наблюдавайте какво се случва със статуса на вашия вход Ако сте пренасочени обратно към екрана за вход, сте открили класическия проблем с запазването на състоянието. Това поведение се случва, защото текущата ни имплементация съхранява потребителските данни в JavaScript променливи, които се нулират при всяко презареждане на страницата. **Проблеми с текущата имплементация:** Простата променлива `account` от нашия [предишен урок](../3-data/README.md) създава три значителни проблема, които засягат както потребителското изживяване, така и поддръжката на кода: | Проблем | Техническа причина | Въздействие върху потребителя | |---------|--------------------|------------------------------| | **Загуба на сесия** | Обновяването на страницата изчиства JavaScript променливите | Потребителите трябва често да се автентифицират | | **Разпръснати актуализации** | Множество функции модифицират състоянието директно | Дебъгването става все по-трудно | | **Непълно изчистване** | Излизането не изчиства всички препратки към състоянието | Потенциални проблеми със сигурността и поверителността | **Архитектурното предизвикателство:** Както дизайна на Титаник с отделни отделения, който изглеждаше здрав, докато няколко отделения не се наводниха едновременно, решаването на тези проблеми поотделно няма да адресира основния архитектурен проблем. Нуждаем се от цялостно решение за управление на състоянието. > 💡 **Какво всъщност се опитваме да постигнем тук?** [Управлението на състоянието](https://en.wikipedia.org/wiki/State_management) всъщност се свежда до решаването на два основни въпроса: 1. **Къде са моите данни?**: Проследяване на информацията, която имаме, и откъде идва 2. **Всички ли са на една и съща страница?**: Уверяване, че това, което потребителите виждат, съответства на това, което всъщност се случва **Нашият план за действие:** Вместо да се въртим в кръг, ще създадем **централизирана система за управление на състоянието**. Представете си, че имате един наистина организиран човек, който отговаря за всички важни неща: ![Схема, показваща потоците от данни между HTML, действията на потребителя и състоянието](../../../../translated_images/data-flow.fa2354e0908fecc89b488010dedf4871418a992edffa17e73441d257add18da4.bg.png) **Разбиране на този поток от данни:** - **Централизира** цялото състояние на приложението на едно място - **Насочва** всички промени в състоянието чрез контролирани функции - **Осигурява** синхронизация на потребителския интерфейс с текущото състояние - **Предоставя** ясен, предсказуем модел за управление на данни > 💡 **Професионален съвет**: Този урок се фокусира върху основни концепции. За сложни приложения библиотеки като [Redux](https://redux.js.org) предоставят по-напреднали функции за управление на състоянието. Разбирането на тези основни принципи ще ви помогне да овладеете всяка библиотека за управление на състоянието. > ⚠️ **Напреднала тема**: Няма да разглеждаме автоматични актуализации на потребителския интерфейс, предизвикани от промени в състоянието, тъй като това включва концепции за [Реактивно програмиране](https://en.wikipedia.org/wiki/Reactive_programming). Помислете за това като отлична следваща стъпка във вашето обучение! ### Задача: Централизиране на структурата на състоянието Нека започнем трансформирането на нашето разпръснато управление на състоянието в централизирана система. Тази първа стъпка установява основата за всички подобрения, които следват. **Стъпка 1: Създаване на централен обект за състояние** Заменете простото деклариране на `account`: ```js let account = null; ``` Със структурирания обект за състояние: ```js let state = { account: null }; ``` **Защо тази промяна е важна:** - **Централизира** всички данни на приложението на едно място - **Подготвя** структурата за добавяне на повече свойства на състоянието по-късно - **Създава** ясна граница между състоянието и другите променливи - **Установява** модел, който се мащабира с растежа на приложението **Стъпка 2: Актуализиране на моделите за достъп до състоянието** Актуализирайте вашите функции, за да използват новата структура на състоянието: **Във функциите `register()` и `login()`**, заменете: ```js account = ... ``` С: ```js state.account = ... ``` **Във функцията `updateDashboard()`**, добавете този ред в началото: ```js const account = state.account; ``` **Какво постигат тези актуализации:** - **Запазват** съществуващата функционалност, като подобряват структурата - **Подготвят** вашия код за по-сложно управление на състоянието - **Създават** последователни модели за достъп до данни за състоянието - **Установяват** основата за централизирани актуализации на състоянието > 💡 **Забележка**: Това рефакториране не решава веднага нашите проблеми, но създава основата за мощните подобрения, които предстоят! ## Внедряване на контролирани актуализации на състоянието С централизирано състояние, следващата стъпка включва установяване на контролирани механизми за модификации на данни. Този подход осигурява предсказуеми промени в състоянието и по-лесно дебъгване. Основният принцип прилича на контрол на въздушния трафик: вместо да позволяваме на множество функции да модифицират състоянието независимо, ще насочим всички промени чрез една контролирана функция. Този модел предоставя ясен контрол върху това кога и как се променят данните. **Управление на неизменяемо състояние:** Ще третираме нашия обект `state` като [*неизменяем*](https://en.wikipedia.org/wiki/Immutable_object), което означава, че никога няма да го модифицираме директно. Вместо това, всяка промяна създава нов обект за състояние с актуализираните данни. Въпреки че този подход може първоначално да изглежда неефективен в сравнение с директните модификации, той предоставя значителни предимства за дебъгване, тестване и поддържане на предсказуемостта на приложението. **Предимства на управлението на неизменяемо състояние:** | Предимство | Описание | Въздействие | |------------|----------|-------------| | **Предсказуемост** | Промените се случват само чрез контролирани функции | По-лесно дебъгване и тестване | | **Проследяване на историята** | Всяка промяна в състоянието създава нов обект | Позволява функции за отмяна/възстановяване | | **Предотвратяване на странични ефекти** | Няма случайни модификации | Предотвратява мистериозни грешки | | **Оптимизация на производителността** | Лесно откриване на промени в състоянието | Позволява ефективни актуализации на потребителския интерфейс | **Неизменяемост в JavaScript с `Object.freeze()`:** JavaScript предоставя [`Object.freeze()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze), за да предотврати модификации на обекти: ```js const immutableState = Object.freeze({ account: userData }); // Any attempt to modify immutableState will throw an error ``` **Какво се случва тук:** - **Предотвратява** директни присвоявания или изтривания на свойства - **Генерира** изключения при опити за модификация - **Осигурява** промени в състоянието само чрез контролирани функции - **Създава** ясен договор за начина, по който може да се актуализира състоянието > 💡 **Дълбоко разбиране**: Научете за разликата между *повърхностно* и *дълбоко* неизменяеми обекти в [документацията на MDN](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*. Създайте нова функция `logout()`: ```js function logout() { updateState('account', null); navigate('/login'); } ``` В `updateDashboard()` заменете пренасочването `return navigate('/login');` с `return logout();`. Опитайте да регистрирате нов акаунт, да излезете и да влезете отново, за да проверите дали всичко все още работи правилно. > Съвет: можете да разгледате всички промени в състоянието, като добавите `console.log(state)` в края на `updateState()` и отворите конзолата в инструментите за разработка на браузъра. ## Внедряване на запазване на данни Проблемът със загубата на сесия, който идентифицирахме по-рано, изисква решение за запазване, което да поддържа състоянието на потребителя през сесиите на браузъра. Това трансформира нашето приложение от временно изживяване в надежден, професионален инструмент. Помислете как атомните часовници поддържат прецизно време дори при прекъсвания на захранването, като съхраняват критично състояние в неволатилна памет. По подобен начин уеб приложенията се нуждаят от механизми за запазване на данни, за да съхраняват основни потребителски данни през сесиите на браузъра и обновяванията на страницата. **Стратегически въпроси за запазване на данни:** Преди да внедрите запазване, обмислете тези критични фактори: | Въпрос | Контекст на банковото приложение | Въздействие на решението | |--------|----------------------------------|--------------------------| | **Данните чувствителни ли са?** | Баланс на акаунта, история на транзакциите | Изберете сигурни методи за съхранение | | **Колко дълго трябва да се запазят?** | Статус на вход срещу временни UI предпочитания | Изберете подходяща продължителност на съхранение | | **Сървърът > 💡 **Разширена опция**: За сложни офлайн приложения с големи набори от данни, обмислете използването на [`IndexedDB` API](https://developer.mozilla.org/docs/Web/API/IndexedDB_API). Той предоставя пълна база данни на клиентската страна, но изисква по-сложно внедряване. ### Задача: Имплементиране на постоянство с localStorage Нека имплементираме постоянство на съхранението, за да останат потребителите вписани, докато не се отпишат изрично. Ще използваме `localStorage`, за да съхраняваме данни за акаунта между сесиите на браузъра. **Стъпка 1: Дефиниране на конфигурация за съхранение** ```js const storageKey = 'savedAccount'; ``` **Какво предоставя тази константа:** - **Създава** последователен идентификатор за съхранените данни - **Предотвратява** грешки при препратки към ключове за съхранение - **Улеснява** промяната на ключа за съхранение, ако е необходимо - **Следва** най-добрите практики за поддържане на кода **Стъпка 2: Добавяне на автоматично постоянство** Добавете този ред в края на функцията `updateState()`: ```js localStorage.setItem(storageKey, JSON.stringify(state.account)); ``` **Разбивка на случващото се тук:** - **Конвертира** обекта на акаунта в JSON низ за съхранение - **Запазва** данните, използвайки нашия последователен ключ за съхранение - **Изпълнява се** автоматично при всяка промяна на състоянието - **Гарантира**, че съхранените данни винаги са синхронизирани с текущото състояние > 💡 **Полза за архитектурата**: Тъй като централизирахме всички актуализации на състоянието чрез `updateState()`, добавянето на постоянство изискваше само един ред код. Това демонстрира силата на добрите архитектурни решения! **Стъпка 3: Възстановяване на състоянието при зареждане на приложението** Създайте функция за инициализация, която да възстановява запазените данни: ```js function init() { const savedAccount = localStorage.getItem(storageKey); if (savedAccount) { updateState('account', JSON.parse(savedAccount)); } // Our previous initialization code window.onpopstate = () => updateRoute(); updateRoute(); } init(); ``` **Разбиране на процеса на инициализация:** - **Извлича** всички предварително запазени данни за акаунта от localStorage - **Парсира** JSON низа обратно в JavaScript обект - **Актуализира** състоянието, използвайки нашата контролирана функция за актуализация - **Възстановява** сесията на потребителя автоматично при зареждане на страницата - **Изпълнява се** преди актуализациите на маршрута, за да гарантира, че състоянието е налично **Стъпка 4: Оптимизиране на основния маршрут** Актуализирайте основния маршрут, за да се възползвате от постоянството: В `updateRoute()` заменете: ```js // Replace: return navigate('/login'); return navigate('/dashboard'); ``` **Защо тази промяна има смисъл:** - **Използва** ефективно нашата нова система за постоянство - **Позволява** на таблото да обработва проверките за автентикация - **Пренасочва** автоматично към вход, ако няма запазена сесия - **Създава** по-плавно потребителско изживяване **Тестване на вашата имплементация:** 1. Впишете се в приложението за банкиране 2. Обновете страницата на браузъра 3. Уверете се, че оставате вписани и на таблото 4. Затворете и отворете отново браузъра 5. Върнете се към приложението и потвърдете, че все още сте вписани 🎉 **Постижение отключено**: Успешно имплементирахте управление на постоянно състояние! Вашето приложение вече се държи като професионално уеб приложение. ## Балансиране на постоянството със свежестта на данните Нашата система за постоянство успешно поддържа потребителските сесии, но въвежда ново предизвикателство: остарели данни. Когато множество потребители или приложения модифицират едни и същи данни на сървъра, локално кешираната информация става неактуална. Тази ситуация наподобява навигаторите викинги, които разчитали както на съхранени звездни карти, така и на текущи наблюдения на небесните тела. Картите предоставяли последователност, но навигаторите се нуждаели от свежи наблюдения, за да се съобразят с променящите се условия. По подобен начин нашето приложение се нуждае както от постоянно потребителско състояние, така и от актуални данни от сървъра. **🧪 Откриване на проблема със свежестта на данните:** 1. Впишете се в таблото, използвайки акаунта `test` 2. Изпълнете тази команда в терминала, за да симулирате транзакция от друг източник: ```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 ``` 3. Обновете страницата на таблото в браузъра 4. Наблюдавайте дали виждате новата транзакция **Какво демонстрира този тест:** - **Показва** как localStorage може да стане "остарял" (неактуален) - **Симулира** реални сценарии, при които данните се променят извън вашето приложение - **Разкрива** напрежението между постоянството и свежестта на данните **Предизвикателството със старите данни:** | Проблем | Причина | Влияние върху потребителя | |---------|---------|---------------------------| | **Остарели данни** | localStorage никога не изтича автоматично | Потребителите виждат неактуална информация | | **Промени на сървъра** | Други приложения/потребители модифицират същите данни | Несъответстващи изгледи между платформите | | **Кеш срещу реалност** | Локалният кеш не съответства на състоянието на сървъра | Лошо потребителско изживяване и объркване | **Стратегия за решение:** Ще имплементираме модел "обновяване при зареждане", който балансира предимствата на постоянството с нуждата от свежи данни. Този подход поддържа плавното потребителско изживяване, като същевременно гарантира точност на данните. ### Задача: Имплементиране на система за обновяване на данни Ще създадем система, която автоматично извлича свежи данни от сървъра, като същевременно запазва предимствата на нашето управление на постоянното състояние. **Стъпка 1: Създаване на обновител на данни за акаунта** ```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); } ``` **Разбиране на логиката на тази функция:** - **Проверява** дали потребителят е вписан (state.account съществува) - **Пренасочва** към изход, ако не е намерена валидна сесия - **Извлича** свежи данни за акаунта от сървъра, използвайки съществуващата функция `getAccount()` - **Обработва** грешки от сървъра, като излиза от невалидни сесии - **Актуализира** състоянието със свежи данни, използвайки нашата контролирана система за актуализация - **Задейства** автоматичното постоянство на localStorage чрез функцията `updateState()` **Стъпка 2: Създаване на обработващ за обновяване на таблото** ```js async function refresh() { await updateAccountData(); updateDashboard(); } ``` **Какво постига тази функция за обновяване:** - **Координира** процеса на обновяване на данните и актуализацията на потребителския интерфейс - **Изчаква** зареждането на свежи данни преди актуализацията на дисплея - **Гарантира**, че таблото показва най-актуалната информация - **Поддържа** чисто разделение между управлението на данни и актуализациите на потребителския интерфейс **Стъпка 3: Интеграция с системата за маршрути** Актуализирайте конфигурацията на маршрута, за да задействате автоматично обновяване: ```js const routes = { '/login': { templateId: 'login' }, '/dashboard': { templateId: 'dashboard', init: refresh } }; ``` **Как работи тази интеграция:** - **Изпълнява** функцията за обновяване всеки път, когато се зарежда маршрутът на таблото - **Гарантира**, че свежите данни винаги се показват, когато потребителите навигират към таблото - **Поддържа** съществуващата структура на маршрута, като добавя свежест на данните - **Осигурява** последователен модел за инициализация, специфична за маршрута **Тестване на вашата система за обновяване на данни:** 1. Впишете се в приложението за банкиране 2. Изпълнете командата curl от по-рано, за да създадете нова транзакция 3. Обновете страницата на таблото или навигирайте извън и обратно 4. Уверете се, че новата транзакция се появява веднага 🎉 **Постигнато перфектно равновесие**: Вашето приложение вече комбинира плавното изживяване на постоянното състояние с точността на свежите данни от сървъра! ## Предизвикателство на GitHub Copilot Agent 🚀 Използвайте режим Agent, за да завършите следното предизвикателство: **Описание:** Имплементирайте цялостна система за управление на състоянието с функционалност за отменяне/възстановяване за приложението за банкиране. Това предизвикателство ще ви помогне да практикувате напреднали концепции за управление на състоянието, включително проследяване на историята на състоянието, неизменни актуализации и синхронизация на потребителския интерфейс. **Задача:** Създайте подобрена система за управление на състоянието, която включва: 1) Масив за история на състоянието, който проследява всички предишни състояния, 2) Функции за отменяне и възстановяване, които могат да връщат предишни състояния, 3) Бутони в потребителския интерфейс за операции отменяне/възстановяване на таблото, 4) Максимален лимит на историята от 10 състояния, за да се предотвратят проблеми с паметта, и 5) Правилно изчистване на историята, когато потребителят се отпише. Уверете се, че функционалността за отменяне/възстановяване работи с промените в баланса на акаунта и се запазва между обновяванията на браузъра. Научете повече за [режим Agent](https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode) тук. ## 🚀 Предизвикателство: Оптимизация на съхранението Вашата имплементация вече ефективно обработва потребителските сесии, обновяването на данни и управлението на състоянието. Въпреки това, обмислете дали текущият ни подход оптимално балансира ефективността на съхранението с функционалността. Както шахматните майстори разграничават основните фигури от тези, които могат да бъдат пожертвани, ефективното управление на състоянието изисква идентифициране на данните, които трябва да се запазят, спрямо тези, които винаги трябва да се извличат свежи от сървъра. **Анализ на оптимизацията:** Оценете текущата си имплементация на localStorage и обмислете тези стратегически въпроси: - Каква е минималната информация, необходима за поддържане на автентикацията на потребителя? - Кои данни се променят достатъчно често, че локалното кеширане предоставя малка полза? - Как оптимизацията на съхранението може да подобри производителността, без да влошава потребителското изживяване? **Стратегия за имплементация:** - **Идентифицирайте** основните данни, които трябва да се запазят (вероятно само идентификацията на потребителя) - **Модифицирайте** вашата имплементация на localStorage, за да съхранява само критични данни за сесията - **Гарантирайте**, че свежи данни винаги се зареждат от сървъра при посещения на таблото - **Тествайте**, че вашият оптимизиран подход поддържа същото потребителско изживяване **Разширено разглеждане:** - **Сравнете** компромисите между съхранението на пълни данни за акаунта и само на токени за автентикация - **Документирайте** вашите решения и аргументи за бъдещите членове на екипа Това предизвикателство ще ви помогне да мислите като професионален разработчик, който обмисля както потребителското изживяване, така и ефективността на приложението. Отделете време да експериментирате с различни подходи! ## Тест след лекцията [Тест след лекцията](https://ff-quizzes.netlify.app/web/quiz/48) ## Задание [Имплементирайте диалог за "Добавяне на транзакция"](assignment.md) Ето примерен резултат след завършване на заданието: ![Снимка на екран, показваща примерен диалог за "Добавяне на транзакция"](../../../../translated_images/dialog.93bba104afeb79f12f65ebf8f521c5d64e179c40b791c49c242cf15f7e7fab15.bg.png) --- **Отказ от отговорност**: Този документ е преведен с помощта на AI услуга за превод [Co-op Translator](https://github.com/Azure/co-op-translator). Въпреки че се стремим към точност, моля, имайте предвид, че автоматизираните преводи може да съдържат грешки или неточности. Оригиналният документ на неговия роден език трябва да се счита за авторитетен източник. За критична информация се препоръчва професионален човешки превод. Ние не носим отговорност за каквито и да било недоразумения или погрешни интерпретации, произтичащи от използването на този превод.