Манипулирование домом — задача простая только в самых примитивных ситуациях. Как только понадобится реализовать полноценное, пусть и небольшое, приложение, код моментально превращается в тыкву. Десятка обработчиков достаточно для того чтобы потеряться. С каждым новым событием сложность кода растет еще быстрее, а, ведь, в реальных приложениях событий сотни. Почему так происходит?

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

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

Из браузера в базу данных через приложение

По сути, в типичных веб проектах, приложение занимается двумя вещами, либо обновляет данные в базе, либо извлекает эти данные и на основе них формирует HTML. Необходимость базы данных довольно очевидна и понятна для всех, но тоже самое не очевидно во фронтенде, DOM позволяет хранить состояние внутри себя и более того, провоцирует так делать. На протяжении курса мы встречались с этой ситуацией не раз, когда нужно выполнить некоторое действие, а оно зависит от того что сейчас на экране. Для анализа приходилось лезть в DOM и смотреть что же там происходит. Первый шаг в построении правильной архитектуры, состоит в выделении состояния из дома, в некое подобие базы данных, основанной на обычном объекте js. При такой организации кода, вырисовывается следующая схема работы:

  • Возникает событие
  • Меняется состояние
  • Обновляется DOM

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

See the Pen js_dom_state_simple by Hexlet (@hexlet) on CodePen.

Главная особенность кода выше, в том как идет работа с состоянием. Здесь нет никаких обращений к дому для извлечения текущего значения, оно хранится в переменной и доступно для всех обработчиков. Обратите внимание на структуру кода. Состояние и навешивание колбеков находятся внутри функции. Это важно, как минимум, по двум причинам. Во-первых, такой подход позволяет использовать данный файл как модуль, импортировать из него другие функции (если они там есть) и, например, тестировать их. Если код вызывается на уровне модуля, то импорт привел бы к его немедленному исполнению. Во-вторых, состояние локально (оно определено внутри функции), а значит мы можем запустить несколько версий приложения, это особенно актуально при написании тестов. Напротив, определение состояния на уровне модуля, сделало бы его "синглотоном" (что-то существующее в единственном экземпляре), другими словами, состояние было бы всегда одним и общим для всего кода, который его использует. Не было бы возможности начать с чистого листа.

Перед тем как смотреть более сложный пример, в котором состояние представлено объектом, давайте разберемся с тем, что включает в себя понятие состояние. Если коротко, то состояние это данные нашего приложения в любой момент времени, например, открытые вкладки в редакторе или браузере. Их количество и содержимое меняются в зависимости от того, какие кнопки мы нажимаем и что пытаемся загрузить. В общем случае, любое визуальное изменение в приложении или на странице, это всегда изменение состояния и никак иначе. Невозможна ситуация при которой на странице сайта меняется какая-то деталь, но состояние при этом остается тем же. Изменение представления возможно только на основе изменения состояния. Вы можете возразить, что анимация через CSS не меняет ничего в нашем приложении и будете правы лишь на половину. Да, анимация в css не связана с нашим приложением, но внутри браузера это состояние есть и оно меняется.

Отличным примером неочевидного, для начинающего фронтенд специалиста, состояния, служит состояние формы. Представьте себе поле для ввода телефона, которое отслеживает ошибки при вводе и сразу их показывает. Если ошибок нет, то оно позволяет выполнить сабмит формы, иначе кнопка заблокирована. Что в данном случае является состоянием? Однозначно состояние валидности данных формы: "валидно" и "не валидно". На основе этого состояния определяется обводить красной рамкой поле для ввода или нет. Ну, и, конечно, состоянием является заблокированность кнопки.

Как видно из примера, состояние описывается обычным js объектом, который создается при старте приложения:

const state = {
  registrationProcess: {
    valid: true,
    submitDisabled: true,
  }
};

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

const state = {
  centralBlock: {
    valid: true,
    submitDisabled: true,
  },
  sideBar: {
    formValue: 'value'
  },
};

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

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

input.addEventListener('keyup', () => {
  if (input.value === '') {
    state.registrationProcess.valid = true;
    state.registrationProcess.submitDisabled = true;
  } else if (!input.value.match(/^\d+$/)) {
    state.registrationProcess.valid = false;
    state.registrationProcess.submitDisabled = true;
  } else {
    state.registrationProcess.valid = true;
    state.registrationProcess.submitDisabled = false;
  }

  render(state);
});

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

Теперь наше приложение разделено на три независимых части: состояние, обработчики и рендеринг. Эта модель работы на тривиальных приложениях (в пару тройку обработчиков) смотрится избыточной, но если обработчиков станет хотя бы 10, то вы увидите, что с приложением достаточно удобно работать. Виден поток данных, всегда можно отследить, что изменилось и как одни части приложения зависят от других. К тому же сокращается дублирование. Например, изменение состояния может идти из разных частей приложения, но логика отрисовки при этом остается неизменной. В такой ситуации достаточно описать новый способ изменения уже существующего состояния, а рендеринг сделает все остальное.

const render = (state) => {
  const input = document.querySelector('.phone');
  const submit = document.querySelector('.submit');
  submit.disabled = state.registrationProcess.submitDisabled;
  if (state.registrationProcess.valid) {
    input.style.border = null;
  } else {
    input.style.border = "thick solid red";
  }
}

Кроме наличия разделения на три части, не менее важно то, как они друг с другом взаимодействуют, более того, это основа модульности:

  • Состояние не знает ничего про остальные части системы, оно ядро.
  • Рендеринг ничего не знает про существование обработчиков, но пользуется состоянием для отрисовки
  • Обработчики знают про состояние, так как обновляют его и инициируют рендеринг

Этот способ разделения по прежнему обладает одним важным недостатком, который мы устраним в следующем уроке, когда поговорим про MVC.

Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →