Зарегистрируйтесь, чтобы продолжить обучение

MVC JS: Архитектура фронтенда

Как говорилось ранее, наша схема работы с состоянием имеет один существенный недостаток — за вызов отрисовки отвечают обработчики. Ниже приведен пример, демонстрирующий вызов render().

input.addEventListener('change', () => {
  const { registrationProcess } = state;
  if (input.value === '') {
    registrationProcess.validationState = 'valid';
    registrationProcess.errors = [];
  } else if (!input.value.match(/^\d+$/)) {
    registrationProcess.validationState = 'invalid';
    registrationProcess.errors = ['Bad format'];
  } else {
    registrationProcess.validationState = 'valid';
    registrationProcess.errors = [];
  }

  render(state);
});

Какие проблемы могут возникнуть при таком подходе?

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

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

Существует другой способ выполнить эту задачу. Он основан на такой концепции (обычно говорят «шаблон проектирования»), как Наблюдатель (Observer). Его идея очень проста: одна часть системы наблюдает за изменением другой части системы. Если наблюдаемый изменился, то наблюдатель может сделать что-то полезное.

В JS подобный механизм можно реализовать через Proxy, но это довольно сложно. Более простым решением будет использование готовой библиотеки on-change.

import onChange from 'on-change';

const app = () => {
  const state = {
    ui: {
      value: 'hello',
    },
  };

  const watchedState = onChange(state, (path, value, previousValue) => {
    alert('value changed!');
    console.log(path);
    // => 'ui.value'
    console.log(value);
    // => 'other value'
    console.log(previousValue);
    // => 'hello'
  });

  // После изменения атрибута возникнет алерт
  const el = document.querySelector('<selector>');
  el.addEventListener('change', () => {
    watchedState.ui.value = 'other value';
  });
}

// Где-то в другом файле (обычно в index.js)
app();

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

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

Теперь обработчики ничего не знают про рендеринг и отвечают только за взаимодействие с состоянием. В свою очередь рендеринг следит за состоянием и меняет DOM только там, где нужно и так, как нужно. Этот способ организации приложения считается уже классическим и носит имя MVC (Model View Controller). Каждое слово обозначает слой приложения со своей зоной ответственности. Model — состояние приложения и бизнес-логика, View — слой, отвечающий за взаимодействие с DOM, Controller — обработчики.

Обратите внимание на то, что Model, Controller или View — это ни файлы, ни классы, ни что-либо еще конкретное. Это логические слои, которые выполняют свою задачу и определенным образом взаимодействуют друг с другом.

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

MVC

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

// Предположим, что на странице есть одна форма
// с полем для ввода задачи и кнопкой для ее добавления

const form = document.querySelector('form');
const input = document.querySelector('form input');
form.addEventListener('submit', () => {
  watchedState.registrationProcess.state = 'processing';
  // Что-то делаем с данными, например, добавляем в состояние
  input.value = ''; // Очистка поля ввода напрямую! Нарушение MVC!
});

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

const watchedState = onChange(state, (path) => {
  if (path === 'registrationProcess.state') {
    // Обновляется состояние! Нарушение MVC!
    watchedState.registrationProcess.alert = 'Sending data...';
  }
});

И, конечно, представление не может притворяться контроллером и выполнять, например, HTTP-запросы:

const watchedState = onChange(state, (path, value) => {
  if (path === 'registrationProcess.state') {
    // Делаем HTTP-запрос! Нарушение MVC!
    if (value === 'sending') {
      axios.post(endpoint, watchedState.registrationProcess.data);
    }
  }
});

Итого: контроллер что-то делает с данными, на изменение данных срабатывает слой представления и изменяет DOM.


Дополнительные материалы

  1. Backbone MVC
  2. Что такое MVC: рассказываем простыми словами
  3. Как работать с библиотекой on-change

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка фронтенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 26 декабря
профессия
от 39 525 ₸ в месяц
Разработка фронтенд- и бэкенд-компонентов для веб-приложений
16 месяцев
с нуля
Старт 26 декабря

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»