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

25 KiB

Создание банковского приложения. Часть 4: Понятие управления состоянием

Тест перед лекцией

Тест перед лекцией

Введение

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

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

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

Для этого урока вам нужно завершить часть веб-приложения, посвященную извлечению данных. Также необходимо установить Node.js и запустить серверный API локально, чтобы управлять данными учетной записи.

Вы можете проверить, правильно ли работает сервер, выполнив эту команду в терминале:

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

Пересмотр управления состоянием

В предыдущем уроке мы ввели базовое понятие состояния в нашем приложении с помощью глобальной переменной account, которая содержит банковские данные текущего авторизованного пользователя. Однако в текущей реализации есть недостатки. Попробуйте обновить страницу, находясь на панели управления. Что происходит?

В текущем коде есть три проблемы:

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

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

Какие проблемы мы действительно пытаемся решить?

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

  • Как сделать потоки данных в приложении понятными?
  • Как обеспечить синхронизацию данных состояния с пользовательским интерфейсом (и наоборот)?

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

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

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

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

Задание

Начнем с небольшого рефакторинга. Замените объявление account:

let account = null;

На:

let state = {
  account: null
};

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

Также нужно обновить функции, которые его используют. В функциях register() и login() замените account = ... на state.account = ...;

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

const account = state.account;

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

Отслеживание изменений данных

Теперь, когда мы создали объект state для хранения данных, следующим шагом будет централизация обновлений. Цель — упростить отслеживание любых изменений и времени их возникновения.

Чтобы избежать изменений в объекте state, также полезно рассматривать его как неизменяемый, то есть такой, который нельзя изменить. Это также означает, что для изменения чего-либо в нем нужно создать новый объект состояния. Такой подход защищает от нежелательных побочных эффектов и открывает возможности для новых функций в приложении, таких как реализация отмены/повтора действий, а также упрощает отладку. Например, вы могли бы записывать каждое изменение состояния и сохранять историю изменений, чтобы понять источник ошибки.

В JavaScript можно использовать 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 cookies, если данные нужно передавать на сервер, например, для аутентификации.

Еще один вариант — использовать один из многих 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();

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

Мы также можем сделать страницу Dashboard страницей по умолчанию для нашего приложения, так как теперь мы сохраняем данные учетной записи. Если данные не найдены, панель управления сама перенаправляет на страницу Login. В 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. Несмотря на наши усилия обеспечить точность перевода, автоматические переводы могут содержать ошибки или неточности. Оригинальный документ на его родном языке следует считать авторитетным источником. Для получения критически важной информации рекомендуется профессиональный перевод человеком. Мы не несем ответственности за любые недоразумения или неправильные интерпретации, возникшие в результате использования данного перевода.