You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Web-Dev-For-Beginners/translations/ru/7-bank-project/4-state-management/README.md

46 KiB

Создание банковского приложения, часть 4: Концепции управления состоянием

Викторина перед лекцией

Викторина перед лекцией

Введение

Управление состоянием похоже на навигационную систему космического аппарата Voyager когда все работает гладко, вы едва замечаете ее существование. Но когда что-то идет не так, это становится разницей между достижением межзвездного пространства и блужданием в космической пустоте. В веб-разработке состояние представляет собой все, что ваше приложение должно запомнить: статус входа пользователя, данные формы, историю навигации и временные состояния интерфейса.

По мере того как ваше банковское приложение эволюционировало от простого формы входа к более сложному приложению, вы, вероятно, столкнулись с некоторыми распространенными проблемами. Обновите страницу, и пользователи неожиданно выйдут из системы. Закройте браузер, и весь прогресс исчезнет. Попробуйте устранить проблему, и вам придется искать среди множества функций, которые по-разному изменяют одни и те же данные.

Это не признаки плохого кодирования это естественные трудности, которые возникают, когда приложения достигают определенного уровня сложности. Каждый разработчик сталкивается с этими проблемами, когда его приложения переходят от "доказательства концепции" к "готовности к производству".

В этом уроке мы внедрим централизованную систему управления состоянием, которая преобразует ваше банковское приложение в надежное профессиональное приложение. Вы научитесь управлять потоками данных предсказуемо, сохранять пользовательские сессии и создавать плавный пользовательский опыт, который требуется современным веб-приложениям.

Предварительные требования

Прежде чем углубляться в концепции управления состоянием, вам нужно правильно настроить среду разработки и подготовить основу для вашего банковского приложения. Этот урок напрямую опирается на концепции и код из предыдущих частей этой серии.

Убедитесь, что у вас есть следующие компоненты, прежде чем продолжить:

Необходимая настройка:

  • Завершите урок по извлечению данных ваше приложение должно успешно загружать и отображать данные аккаунта
  • Установите Node.js на вашу систему для запуска API на сервере
  • Запустите сервер API локально для обработки операций с данными аккаунта

Проверка вашей среды:

Убедитесь, что ваш сервер API работает корректно, выполнив эту команду в терминале:

curl http://localhost:5000/api
# -> should return "Bank API v1.0.0" as a result

Что делает эта команда:

  • Отправляет GET-запрос на ваш локальный сервер API
  • Тестирует соединение и проверяет, отвечает ли сервер
  • Возвращает информацию о версии API, если все работает корректно

Диагностика текущих проблем со состоянием

Как Шерлок Холмс, исследующий место преступления, мы должны понять, что именно происходит в нашей текущей реализации, прежде чем мы сможем решить загадку исчезающих пользовательских сессий.

Давайте проведем простой эксперимент, который выявит основные проблемы управления состоянием:

🧪 Попробуйте провести этот диагностический тест:

  1. Войдите в свое банковское приложение и перейдите на панель управления
  2. Обновите страницу браузера
  3. Посмотрите, что происходит с вашим статусом входа

Если вас перенаправляют обратно на экран входа, вы обнаружили классическую проблему сохранения состояния. Такое поведение происходит потому, что наша текущая реализация сохраняет данные пользователя в переменных JavaScript, которые сбрасываются при каждом обновлении страницы.

Проблемы текущей реализации:

Простая переменная account из нашего предыдущего урока создает три значительные проблемы, которые влияют как на пользовательский опыт, так и на поддерживаемость кода:

Проблема Техническая причина Влияние на пользователя
Потеря сессии Обновление страницы очищает переменные JavaScript Пользователи должны часто повторно проходить аутентификацию
Разрозненные обновления Несколько функций напрямую изменяют состояние Отладка становится все сложнее
Неполное очищение Выход из системы не очищает все ссылки на состояние Возможные проблемы с безопасностью и конфиденциальностью

Архитектурная задача:

Как и в случае с конструкцией Титаника, которая казалась надежной, пока несколько отсеков не затопило одновременно, исправление этих проблем по отдельности не решит основную архитектурную проблему. Нам нужно комплексное решение для управления состоянием.

💡 Чего мы на самом деле пытаемся достичь?

Управление состоянием на самом деле сводится к решению двух фундаментальных задач:

  1. Где мои данные?: Отслеживание того, какая информация у нас есть и откуда она поступает
  2. Все ли на одной волне?: Убедиться, что то, что видят пользователи, соответствует тому, что на самом деле происходит

Наш план действий:

Вместо того чтобы бегать по кругу, мы создадим централизованную систему управления состоянием. Представьте, что есть один очень организованный человек, который отвечает за все важные дела:

Схема, показывающая потоки данных между HTML, действиями пользователя и состоянием

Понимание этого потока данных:

  • Централизует все состояние приложения в одном месте
  • Направляет все изменения состояния через контролируемые функции
  • Обеспечивает, чтобы пользовательский интерфейс оставался синхронизированным с текущим состоянием
  • Создает четкий и предсказуемый шаблон для управления данными

💡 Профессиональный совет: Этот урок сосредоточен на фундаментальных концепциях. Для сложных приложений библиотеки, такие как Redux, предоставляют более продвинутые функции управления состоянием. Понимание этих основных принципов поможет вам освоить любую библиотеку управления состоянием.

⚠️ Продвинутая тема: Мы не будем рассматривать автоматические обновления пользовательского интерфейса, вызванные изменениями состояния, так как это связано с концепциями реактивного программирования. Считайте это отличным следующим шагом в вашем обучении!

Задача: Централизация структуры состояния

Давайте начнем преобразование нашего разрозненного управления состоянием в централизованную систему. Этот первый шаг создаст основу для всех последующих улучшений.

Шаг 1: Создайте центральный объект состояния

Замените простое объявление account:

let account = null;

На структурированный объект состояния:

let state = {
  account: null
};

Почему это изменение важно:

  • Централизует все данные приложения в одном месте
  • Подготавливает структуру для добавления новых свойств состояния в будущем
  • Создает четкую границу между состоянием и другими переменными
  • Устанавливает шаблон, который масштабируется по мере роста вашего приложения

Шаг 2: Обновите шаблоны доступа к состоянию

Обновите ваши функции для использования новой структуры состояния:

В функциях register() и login() замените:

account = ...

На:

state.account = ...

В функции updateDashboard() добавьте эту строку в начале:

const account = state.account;

Что дают эти обновления:

  • Сохраняют существующую функциональность, улучшая структуру
  • Подготавливают ваш код для более сложного управления состоянием
  • Создают последовательные шаблоны для доступа к данным состояния
  • Устанавливают основу для централизованных обновлений состояния

💡 Примечание: Этот рефакторинг не решает наши проблемы сразу, но создает необходимую основу для мощных улучшений, которые последуют!

Реализация контролируемых обновлений состояния

С централизованным состоянием следующим шагом будет создание контролируемых механизмов для изменения данных. Этот подход обеспечивает предсказуемые изменения состояния и упрощает отладку.

Основной принцип напоминает управление воздушным движением: вместо того, чтобы позволять нескольким функциям независимо изменять состояние, мы будем направлять все изменения через одну контролируемую функцию. Этот шаблон обеспечивает четкий контроль над тем, когда и как происходят изменения данных.

Управление неизменяемым состоянием:

Мы будем рассматривать наш объект state как неизменяемый, то есть никогда не будем изменять его напрямую. Вместо этого каждое изменение будет создавать новый объект состояния с обновленными данными.

Хотя этот подход может показаться менее эффективным по сравнению с прямыми изменениями, он предоставляет значительные преимущества для отладки, тестирования и поддержания предсказуемости приложения.

Преимущества управления неизменяемым состоянием:

Преимущество Описание Влияние
Предсказуемость Изменения происходят только через контролируемые функции Упрощает отладку и тестирование
Отслеживание истории Каждое изменение состояния создает новый объект Позволяет реализовать функции отмены/повтора
Предотвращение побочных эффектов Нет случайных изменений Предотвращает загадочные ошибки
Оптимизация производительности Легко определить, когда состояние действительно изменилось Обеспечивает эффективные обновления пользовательского интерфейса

Неизменяемость в JavaScript с помощью Object.freeze():

JavaScript предоставляет Object.freeze() для предотвращения изменений объекта:

const immutableState = Object.freeze({ account: userData });
// Any attempt to modify immutableState will throw an error

Разбор происходящего:

  • Предотвращает прямое присвоение или удаление свойств
  • Вызывает исключения при попытке изменения
  • Обеспечивает, что изменения состояния должны проходить через контролируемые функции
  • Создает четкий контракт о том, как можно обновлять состояние

💡 Углубленный анализ: Узнайте о различиях между поверхностной и глубокой неизменяемостью объектов в документации MDN. Понимание этого различия важно для сложных структур состояния.

Задача

Давайте создадим новую функцию 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() и открыв консоль в инструментах разработчика вашего браузера.

Реализация сохранения данных

Проблема потери сессии, которую мы выявили ранее, требует решения для сохранения данных, чтобы поддерживать состояние пользователя между сеансами браузера. Это преобразует наше приложение из временного опыта в надежный профессиональный инструмент.

Подумайте, как атомные часы сохраняют точное время даже при отключении питания, храня критически важное состояние в энергонезависимой памяти. Точно так же веб-приложения нуждаются в механизмах сохранения данных, чтобы сохранять важные данные пользователя между сеансами браузера и обновлениями страницы.

Стратегические вопросы для сохранения данных:

Прежде чем реализовывать сохранение, рассмотрите эти важные аспекты:

Вопрос Контекст банковского приложения Влияние на решение
Являются ли данные конфиденциальными? Баланс счета, история транзакций Выбор безопасных методов хранения
Как долго данные должны сохраняться? Состояние входа vs. временные настройки интерфейса Выбор подходящей продолжительности хранения
Нужны ли данные серверу? Токены аутентификации vs. настройки интерфейса Определение требований к обмену данными

Варианты хранения данных в браузере:

Современные браузеры предоставляют несколько механизмов хранения, каждый из которых предназначен для различных случаев использования:

Основные API для хранения:

  1. localStorage: Постоянное хранилище ключ/значение

    • Сохраняет данные между сеансами браузера на неопределенный срок
    • Сохраняется при перезапуске браузера и компьютера
    • Привязан к конкретному домену сайта
    • Идеально подходит для пользовательских настроек и состояний входа
  2. sessionStorage: Временное хранилище сеанса

    • Функционирует аналогично localStorage во время активных сеансов
    • Очищается автоматически при закрытии вкладки браузера
    • Идеально подходит для временных данных, которые не должны сохраняться
  3. HTTP Cookies: Хранилище, доступное серверу

    • Автоматически отправляется с каждым запросом к серверу
    • Идеально подходит для токенов аутентификации
    • Ограничено по размеру и может влиять на производительность

Требование к сериализации данных:

И localStorage, и sessionStorage хранят только строки:

// Convert objects to JSON strings for storage
const accountData = { user: 'john', balance: 150 };
localStorage.setItem('account', JSON.stringify(accountData));

// Parse JSON strings back to objects when retrieving
const savedAccount = JSON.parse(localStorage.getItem('account'));

Понимание сериализации:

  • Преобразует объекты JavaScript в строки JSON с помощью JSON.stringify()
  • Восстанавливает объекты из JSON с помощью JSON.parse()
  • Автоматически обрабатывает сложные вложенные объекты и массивы
  • Не работает с функциями, неопределенными значениями и циклическими ссылками

💡 Расширенный вариант: Для сложных офлайн-приложений с большими объемами данных рассмотрите использование IndexedDB API. Это полноценная клиентская база данных, но её реализация требует более сложного подхода.

Задача: Реализовать сохранение данных с помощью localStorage

Давайте реализуем систему сохранения данных, чтобы пользователи оставались авторизованными до тех пор, пока они явно не выйдут из аккаунта. Мы будем использовать localStorage для хранения данных аккаунта между сеансами браузера.

Шаг 1: Определите конфигурацию хранения

const storageKey = 'savedAccount';

Что обеспечивает эта константа:

  • Создает единый идентификатор для хранения данных
  • Предотвращает ошибки из-за опечаток в ключах хранения
  • Упрощает изменение ключа хранения при необходимости
  • Следует лучшим практикам для поддерживаемого кода

Шаг 2: Добавьте автоматическое сохранение

Добавьте эту строку в конец функции updateState():

localStorage.setItem(storageKey, JSON.stringify(state.account));

Разбор происходящего:

  • Преобразует объект аккаунта в строку JSON для хранения
  • Сохраняет данные с использованием единого ключа хранения
  • Выполняется автоматически при изменении состояния
  • Гарантирует, что сохраненные данные всегда синхронизированы с текущим состоянием

💡 Преимущество архитектуры: Благодаря тому, что все обновления состояния централизованы через updateState(), добавление сохранения потребовало всего одной строки кода. Это демонстрирует силу хороших архитектурных решений!

Шаг 3: Восстановите состояние при загрузке приложения

Создайте функцию инициализации для восстановления сохраненных данных:

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() замените:

// Replace: return navigate('/login');
return navigate('/dashboard');

Почему это изменение имеет смысл:

  • Эффективно использует нашу новую систему сохранения данных
  • Позволяет панели управления проверять авторизацию
  • Автоматически перенаправляет на страницу входа, если сохраненного сеанса нет
  • Создает более плавный пользовательский опыт

Тестирование реализации:

  1. Войдите в свой банковский аккаунт
  2. Обновите страницу браузера
  3. Убедитесь, что вы остались авторизованным и находитесь на панели управления
  4. Закройте и снова откройте браузер
  5. Перейдите обратно в приложение и убедитесь, что вы все еще авторизованы

🎉 Достижение разблокировано: Вы успешно реализовали управление состоянием с сохранением! Ваше приложение теперь работает как профессиональное веб-приложение.

Баланс между сохранением данных и их актуальностью

Наша система сохранения данных успешно поддерживает сеансы пользователей, но создает новую проблему: устаревание данных. Когда несколько пользователей или приложений изменяют одни и те же данные на сервере, локальная кэшированная информация становится устаревшей.

Эта ситуация напоминает навигаторов-викингов, которые полагались как на сохраненные звездные карты, так и на текущие наблюдения за небом. Карты обеспечивали стабильность, но навигаторы нуждались в свежих наблюдениях, чтобы учитывать изменяющиеся условия. Точно так же наше приложение нуждается как в сохранении состояния пользователя, так и в актуальных данных с сервера.

🧪 Исследование проблемы устаревания данных:

  1. Войдите в панель управления, используя аккаунт test
  2. Выполните эту команду в терминале, чтобы симулировать транзакцию из другого источника:
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
  1. Обновите страницу панели управления в браузере
  2. Проверьте, отображается ли новая транзакция

Что демонстрирует этот тест:

  • Показывает, как localStorage может стать "устаревшим" (неактуальным)
  • Симулирует реальные сценарии, где данные изменяются вне вашего приложения
  • Выявляет напряжение между сохранением данных и их актуальностью

Проблема устаревания данных:

Проблема Причина Влияние на пользователя
Устаревшие данные localStorage никогда не истекает автоматически Пользователи видят неактуальную информацию
Изменения на сервере Другие приложения/пользователи изменяют те же данные Несоответствие данных на разных платформах
Кэш vs. Реальность Локальный кэш не совпадает с состоянием сервера Плохой пользовательский опыт и путаница

Стратегия решения:

Мы реализуем паттерн "обновление при загрузке", который балансирует преимущества сохранения данных с необходимостью их актуальности. Этот подход сохраняет плавный пользовательский опыт, обеспечивая точность данных.

Задача: Реализовать систему обновления данных

Мы создадим систему, которая автоматически загружает актуальные данные с сервера, сохраняя преимущества нашей системы управления состоянием.

Шаг 1: Создайте функцию обновления данных аккаунта

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: Создайте обработчик обновления панели управления

async function refresh() {
  await updateAccountData();
  updateDashboard();
}

Что делает эта функция обновления:

  • Координирует процесс обновления данных и интерфейса
  • Ожидает, пока актуальные данные будут загружены, перед обновлением отображения
  • Гарантирует, что панель управления показывает самую актуальную информацию
  • Сохраняет четкое разделение между управлением данными и обновлением интерфейса

Шаг 3: Интеграция с системой маршрутов

Обновите конфигурацию маршрутов, чтобы автоматически запускать обновление:

const routes = {
  '/login': { templateId: 'login' },
  '/dashboard': { templateId: 'dashboard', init: refresh }
};

Как работает эта интеграция:

  • Запускает функцию обновления каждый раз при загрузке маршрута панели управления
  • Гарантирует, что актуальные данные всегда отображаются при переходе пользователей на панель управления
  • Сохраняет существующую структуру маршрутов, добавляя актуальность данных
  • Обеспечивает единообразный подход к инициализации маршрутов

Тестирование системы обновления данных:

  1. Войдите в свой банковский аккаунт
  2. Выполните команду curl, чтобы создать новую транзакцию
  3. Обновите страницу панели управления или перейдите на другую страницу и вернитесь
  4. Убедитесь, что новая транзакция отображается немедленно

🎉 Идеальный баланс достигнут: Ваше приложение теперь сочетает плавный опыт сохранения состояния с точностью актуальных данных сервера!

Вызов агента GitHub Copilot 🚀

Используйте режим Agent, чтобы выполнить следующий вызов:

Описание: Реализуйте комплексную систему управления состоянием с функцией отмены/повтора для банковского приложения. Этот вызов поможет вам освоить продвинутые концепции управления состоянием, включая отслеживание истории состояний, неизменяемые обновления и синхронизацию пользовательского интерфейса.

Задание: Создайте улучшенную систему управления состоянием, включающую: 1) Массив истории состояний, который отслеживает все предыдущие состояния, 2) Функции отмены и повтора для возврата к предыдущим состояниям, 3) Кнопки интерфейса для операций отмены/повтора на панели управления, 4) Максимальный лимит истории в 10 состояний для предотвращения проблем с памятью, и 5) Корректную очистку истории при выходе пользователя из аккаунта. Убедитесь, что функции отмены/повтора работают с изменениями баланса аккаунта и сохраняются между обновлениями браузера.

Узнайте больше о режиме Agent здесь.

🚀 Вызов: Оптимизация хранения

Ваша реализация теперь эффективно управляет сеансами пользователей, обновлением данных и состоянием. Однако стоит задуматься, оптимально ли наше текущее решение сочетает эффективность хранения с функциональностью.

Как мастера шахмат, которые различают важные фигуры и пешки, эффективное управление состоянием требует определения, какие данные должны сохраняться, а какие всегда должны загружаться с сервера.

Анализ оптимизации:

Оцените вашу текущую реализацию localStorage и рассмотрите следующие стратегические вопросы:

  • Какую минимальную информацию необходимо сохранить для поддержания авторизации пользователя?
  • Какие данные изменяются настолько часто, что локальное кэширование не приносит пользы?
  • Как оптимизация хранения может улучшить производительность без ухудшения пользовательского опыта?

Стратегия реализации:

  • Определите важные данные, которые необходимо сохранять (вероятно, только идентификацию пользователя)
  • Измените реализацию localStorage, чтобы сохранять только критически важные данные сеанса
  • Убедитесь, что актуальные данные всегда загружаются с сервера при посещении панели управления
  • Протестируйте, что оптимизированный подход сохраняет тот же пользовательский опыт

Продвинутое рассмотрение:

  • Сравните компромиссы между хранением полных данных аккаунта и только токенов аутентификации
  • Документируйте свои решения и аргументы для будущих членов команды

Этот вызов поможет вам мыслить как профессиональный разработчик, который учитывает как пользовательский опыт, так и эффективность приложения. Не торопитесь, чтобы попробовать разные подходы!

Итоговый тест

Итоговый тест

Задание

Реализовать диалог "Добавить транзакцию"

Вот пример результата после выполнения задания:

Скриншот, показывающий пример диалога "Добавить транзакцию"


Отказ от ответственности:
Этот документ был переведен с использованием сервиса автоматического перевода Co-op Translator. Хотя мы стремимся к точности, пожалуйста, учитывайте, что автоматические переводы могут содержать ошибки или неточности. Оригинальный документ на его родном языке следует считать авторитетным источником. Для получения критически важной информации рекомендуется профессиональный перевод человеком. Мы не несем ответственности за любые недоразумения или неправильные интерпретации, возникающие в результате использования данного перевода.