[Перевод] О главнейшей причине существования современных JS-фреймворков

g6hmvuw4moewuvo9qckgafeszz4.png

Автор материала, перевод которого мы публикуем сегодня, говорит, что ему очень и очень часто приходилось видеть, как веб-разработчики бездумно пользуются современными фреймворками вроде React, Angular или Vue.js. Эти фреймворки предлагают много интересного, но, как правило, программисты, применяя их, не учитывают главнейшей причины их существования. Обычно на вопрос: «Почему вы используете фреймворк X», можно услышать следующие ответы, среди которых, однако, нет самого главного:

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


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

Почему синхронизация интерфейса и состояния — это важно


Представим, что вы создаёте веб-приложение, пользуясь которым пользователь может разослать множеству людей некие материалы, вроде приглашений, введя на особой странице адреса их электронной почты. Вводя адрес в поле, пользователь нажимает клавишу Enter и приложение сохраняет введённый адрес и выводит его на странице. UX/UI-дизайнер принял следующее решение: если в состоянии приложения ничего нет — мы показываем на странице поле ввода со вспомогательным текстом, в других случаях выводится такое же поле ввода и уже введённые адреса электронной почты, правее каждого из которых расположена кнопка или ссылка для удаления адреса.

a5c7044045557d87e1652f769f6a98e5.png


Два состояния формы — пустая форма, и форма с адресами

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

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

Реализация сложного интерфейса на чистом JavaScript


Вот реализация рассмотренного выше интерфейса на чистом JS, подготовленная с помощью CodePen.

Глядя на код этой реализации, можно оценить объём работы, необходимый для того, чтобы создать подобный интерфейс и внутренние механизмы приложения без применения каких-либо вспомогательных средств (кстати, использование классических библиотек, вроде jQuery, приведёт к примерно такому же процессу разработки и результату).

В этом примере HTML формирует статическую структуру страницы, а динамические элементы создаются средствами JavaScript (с помощью document.createElement) Это приводит нас к первой проблеме: JS-код, который описывает пользовательский интерфейс, не отличается хорошей читаемостью, и, используя и его, и HTML, мы разбиваем определение элементов интерфейса на две части. Мы могли бы в подобной ситуации использовать innerHTML, то, что получилось бы, отличалось бы большей понятностью, но такой подход менее эффективен и может приводить к проблемам из области межсайтового скриптинга.

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

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

528b16c0973c1174149f1520077f79d7.png


Код, необходимый для обновления состояния и пользовательского интерфейса приложения

Такой код не только сложно писать и поддерживать. Важнее то, что он весьма ненадёжен. Его очень легко «поломать». Представьте себе, что нам надо реализовать возможность синхронизации списка адресов с сервером. Для этого понадобится сравнивать данные, которые имеются в приложении, с тем, что получено с сервера. Тут придётся сравнить все идентификаторы объектов, использующихся для хранения адресов, и сами адреса, так как в приложении вполне могут быть копии записей с теми же идентификаторами, что и у записей на сервере, но с другими адресами.

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

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

Декларативное описание интерфейсов как решение проблемы


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

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

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

О стратегиях синхронизации интерфейса и состояния


Существуют две основных стратегии синхронизации интерфейса и состояния.

  • Повторный рендеринг всего компонента. Так это делается в React. Когда состояние компонента меняется, фреймворк рендерит DOM в памяти и сравнивает его с тем, что уже имеется на странице. Работа с DOM — операция ресурсоёмкая, поэтому фреймворк генерирует то, что называется Virtual DOM, а то, что получается, сравнивает с предыдущей версией Virtual DOM. Затем фреймворк находит различия и вносит изменения в DOM страницы. Этот процесс называют согласованием (reconciliation).
  • Отслеживание изменений с использованием наблюдателей. Так работают Angular и Vue.js. Переменные состояния являются наблюдаемыми объектами, и когда они меняются, обновляются лишь те DOM-элементы, на которые влияют эти изменения, что приводит к обновлению интерфейса.


О веб-компонентах


React, Angular и Vue.js часто сравнивают с веб-компонентами. Это демонстрирует тот факт, что многие не понимают главной возможности, которую предоставляют разработчику эти фреймворки. Это, как мы уже выяснили — синхронизация интерфейса и внутреннего состояния приложения. Веб-компоненты таких возможностей не дают. Они дают разработчику тег