Автор материала, перевод которого мы публикуем сегодня, говорит, что ему очень и очень часто приходилось видеть, как веб-разработчики бездумно пользуются современными фреймворками вроде React, Angular или Vue.js. Эти фреймворки предлагают много интересного, но, как правило, программисты, применяя их, не учитывают главнейшей причины их существования. Обычно на вопрос: «Почему вы используете фреймворк X», можно услышать следующие ответы, среди которых, однако, нет самого главного:
- Этот фреймворк основан на компонентах.
- У него имеется мощное сообщество.
- Для него разработано множество сторонних библиотек, которые помогают решать различные задачи.
- Существуют полезные дополнительные компоненты для этого фреймворка.
- Имеются расширения для браузеров, которые помогают отлаживать приложения, созданные с помощью данного фреймворка.
- Этот фреймворк хорошо подходит для создания одностраничных приложений.
На самом же деле самая главная, фундаментальная причина использования фреймворков заключается в том, что они помогают решать задачу синхронизации пользовательского интерфейса и внутреннего состояния приложения. Это — чрезвычайно сложная и важная задача, и именно о ней мы сегодня и поговорим.
Почему синхронизация интерфейса и состояния — это важно
Представим, что вы создаёте веб-приложение, пользуясь которым пользователь может разослать множеству людей некие материалы, вроде приглашений, введя на особой странице адреса их электронной почты. Вводя адрес в поле, пользователь нажимает клавишу Enter и приложение сохраняет введённый адрес и выводит его на странице. UX/UI-дизайнер принял следующее решение: если в состоянии приложения ничего нет — мы показываем на странице поле ввода со вспомогательным текстом, в других случаях выводится такое же поле ввода и уже введённые адреса электронной почты, правее каждого из которых расположена кнопка или ссылка для удаления адреса.
Два состояния формы — пустая форма, и форма с адресами
В качестве хранилища состояния такой формы может использоваться нечто вроде массива объектов, каждый из которых содержит адрес электронной почты и некий уникальный идентификатор. Добавление в приложение нового адреса путём ввода его в поле и нажатия на клавишу Enter приводит к помещению этого адреса в массив и к обновлению пользовательского интерфейса. При щелчке по ссылке
delete
адрес удаляется из массива, и, опять же, обновляется интерфейс приложения. Несложно заметить, что каждый раз, когда меняется состояние приложения, нужно обновить пользовательский интерфейс.Почему именно этот момент особенно важен? Для того чтобы в этом разобраться, попробуем реализовать подобную функциональность на чистом JavaScript и с использованием какого-нибудь фреймворка.
Реализация сложного интерфейса на чистом JavaScript
Вот реализация рассмотренного выше интерфейса на чистом JS, подготовленная с помощью CodePen.
Глядя на код этой реализации, можно оценить объём работы, необходимый для того, чтобы создать подобный интерфейс и внутренние механизмы приложения без применения каких-либо вспомогательных средств (кстати, использование классических библиотек, вроде jQuery, приведёт к примерно такому же процессу разработки и результату).
В этом примере HTML формирует статическую структуру страницы, а динамические элементы создаются средствами JavaScript (с помощью
document.createElement
) Это приводит нас к первой проблеме: JS-код, который описывает пользовательский интерфейс, не отличается хорошей читаемостью, и, используя и его, и HTML, мы разбиваем определение элементов интерфейса на две части. Мы могли бы в подобной ситуации использовать innerHTML
, то, что получилось бы, отличалось бы большей понятностью, но такой подход менее эффективен и может приводить к проблемам из области межсайтового скриптинга.Тут можно было бы воспользоваться движком для работы с шаблонами, но если нужно воссоздать большое поддерево DOM, это приводит к ещё двум проблемам. Во-первых, это не особенно эффективно, во-вторых — при таком подходе обычно приходится повторно подключать обработчики событий.
Однако среди вышеописанных проблем нет главной, которая заключается в том, что пользовательский интерфейс нужно обновлять при каждом изменении состояния приложения. При каждом обновлении состояния требуются серьёзные усилия, выраженные соответствующим кодом, направленные на обновление пользовательского интерфейса. Например, в нашем случае, при добавлении адреса электронной почты, нужно 2 строки кода для того, чтобы обновить состояние, и больше десятка строк — для того, чтобы обновить интерфейс. При этом надо учитывать, что интерфейс в нашем примере максимально упрощён.
Код, необходимый для обновления состояния и пользовательского интерфейса приложения
Такой код не только сложно писать и поддерживать. Важнее то, что он весьма ненадёжен. Его очень легко «поломать». Представьте себе, что нам надо реализовать возможность синхронизации списка адресов с сервером. Для этого понадобится сравнивать данные, которые имеются в приложении, с тем, что получено с сервера. Тут придётся сравнить все идентификаторы объектов, использующихся для хранения адресов, и сами адреса, так как в приложении вполне могут быть копии записей с теми же идентификаторами, что и у записей на сервере, но с другими адресами.
Для этого понадобится создать большой объём узкоспециализированного кода для организации эффективного изменения DOM. При этом, если будет допущена малейшая ошибка, синхронизация интерфейса с данными нарушится. Это может выражаться в отсутствии неких данных на странице, в показе неправильной информации, в появлении элементов управления, которые не реагируют на воздействия пользователя, или, что ещё хуже, вызывают выполнение неправильных действий (например — кнопка для удаления элемента воздействует не на тот элемент, которому она соответствует в интерфейсе).
В результате поддержка синхронизации интерфейса и данных подразумевает написание большого объёма однообразного и ненадёжного кода. Что же делать? Обратимся к декларативному описанию интерфейсов.
Декларативное описание интерфейсов как решение проблемы
Решением проблемы является не мощное сообщество некоего фреймворка, не дополнительные инструменты, не экосистема и не сторонние библиотеки. Основная возможность фреймворков, которая позволяет решить вышеописанную проблему, заключается в том, что они позволяют реализовывать интерфейсы, которые гарантированно будут синхронизированы со внутренним состоянием приложения.
На самом деле, выше описан идеальный вариант решения проблемы, на самом же деле, это так в том случае, если разработчику не приходится ограничивать себя некими правилами, которые может иметь конкретный фреймворк (например — правилом, касающимся иммутабельности состояния приложения).
Используя фреймворк, мы определяем интерфейс за один заход, не имея необходимости писать код для работы с интерфейсом при выполнении каждого действия. Это всегда даёт один и тот же видимый результат, основанный на конкретном состоянии: фреймворк автоматически обновляет интерфейс после изменения состояния.
О стратегиях синхронизации интерфейса и состояния
Существуют две основных стратегии синхронизации интерфейса и состояния.
- Повторный рендеринг всего компонента. Так это делается в React. Когда состояние компонента меняется, фреймворк рендерит DOM в памяти и сравнивает его с тем, что уже имеется на странице. Работа с DOM — операция ресурсоёмкая, поэтому фреймворк генерирует то, что называется Virtual DOM, а то, что получается, сравнивает с предыдущей версией Virtual DOM. Затем фреймворк находит различия и вносит изменения в DOM страницы. Этот процесс называют согласованием (reconciliation).
- Отслеживание изменений с использованием наблюдателей. Так работают Angular и Vue.js. Переменные состояния являются наблюдаемыми объектами, и когда они меняются, обновляются лишь те DOM-элементы, на которые влияют эти изменения, что приводит к обновлению интерфейса.
О веб-компонентах
React, Angular и Vue.js часто сравнивают с веб-компонентами. Это демонстрирует тот факт, что многие не понимают главной возможности, которую предоставляют разработчику эти фреймворки. Это, как мы уже выяснили — синхронизация интерфейса и внутреннего состояния приложения. Веб-компоненты таких возможностей не дают. Они дают разработчику тег <template>, но не механизм согласования интерфейса и состояния. Если программист пользуется веб-компонентами и поддерживает синхронизацию интерфейса и состояния приложения, ему придётся решать эту задачу самостоятельно, или воспользоваться чем-то вроде Stencil.js (эта библиотека, как и React, использует Virtual DOM).
Отметим, что сила подобного решения — не в использовании веб-компонентов, а в возможности поддерживать синхронизацию интерфейса и состояния. Веб-компоненты не содержат встроенных средств для такой синхронизации. Тут придётся либо пользоваться сторонней библиотекой, либо делать всё вручную. На самом деле, невозможно создать сложный, эффективный и простой в плане поддержки интерфейс на чистом JavaScript. Именно это и является основной причиной, по которой разработчикам необходимы современные фреймворки.
Эксперимент: собственный фреймворк
Поэкспериментируем, перепишем наш пример, который основан на чистом JS, пользуясь реализацией Virtual DOM, при этом не будем прибегать к применению фреймворков. То, что у нас получится, вполне можно назвать нашим собственным фреймворком. Вот его основа, базовый класс, на котором будут основаны компоненты.
Ядро современного фреймворка собственной разработки
Вот — переписанный компонент AddressList (тут, для поддержки JSX, применяется babel).
Интерфейс приложения, реализованный средствами фреймворка собственной разработки
Теперь пользовательский интерфейс определён в декларативном стиле, при этом мы не пользовались существующими фреймворками. Мы можем реализовать новую логику, которая меняет состояние так, как нам надо, при этом не нужно писать дополнительный код для того, чтобы поддерживать синхронизацию интерфейса и состояния приложения. Проблема решена!
Теперь, за исключением обработчиков событий, всё это очень похоже на React-приложение. Тут имеются, например, методы
render()
, componentDidMount()
, setState()
. После того, как решена базовая проблема синхронизации интерфейса и внутреннего состояния приложения, другие возможности интегрируются в полученную систему совершенно естественным образом.Полный код этого проекта можно найти в этом GitHub-репозитории.
Итоги
Подводя итоги, мы можем сделать следующие выводы:
- Очевидно то, что главная проблема, которую решают современные JS-фреймворки, заключается в поддержке синхронизации интерфейса и состояния приложения.
- Невозможно создать сложный, эффективный и удобный в обслуживании интерфейс на чистом JavaScript.
- Веб-компоненты не дают решения проблемы синхронизации интерфейса и состояния.
- Несложно создать собственный фреймворк, используя существующую библиотеку, реализующую Virtual DOM, но это подходит лишь для того, чтобы разобраться с технологиями, на практике так делать не рекомендуется.
Уважаемые читатели! Как по-вашему, действительно ли синхронизация интерфейса и состояния приложения является главной проблемой, которую решают современные фронтенд-фреймворки?