Привет, Хабр! Я до сих пор помню времена, когда доминирующим паттерном было принудительное ООП.
Сейчас это явно не так, и все современные языки предлагают гораздо больше парадигм.
Однако в сфере веб-разработки полностью (и на мой взгляд неоправданно) доминирует реактивность, которая в свое время эффективно решила проблему несовершенства DOM API, попутно создав несколько архитектурных проблем, таких как централизованное хранение данных (что фактически нарушает принципы SOLID), либо слишком усложненный механизм взаимодействия компонентов.
В контексте современных WEB-стандартов реактивность требует хотя бы некоторого переосмысления.
Например, реактивная парадигма отлично выглядит, если наше состояние централизовано (неслучайно самый популярный стек — это реагирование/редукс), а если оно распределено по дереву компонентов (что архитектурно более правильно), то мы часто нужно меньше реактивности и больше четкой императивности.
Я пишу свои проекты на ванильных веб-компонентах, в императивном стиле ООП, с минимальным количеством библиотечного кода, и мне очень редко не хватает реактивности.
Если бы чистая реактивность охватывала все потребности разработчика, не было бы необходимости создавать императивные лазейки в каждом фреймворке, которые позволяют модифицировать компонент вместо его повторного создания (ссылки, неуправляемые формы, $parent и т. д.).
А когда стоит задача получить максимально отзывчивое приложение, то волей-неволей приходится продумывать (и вручную контролировать) момент и способ обновления DOM, как это собственно и делается в большинстве хороших PWA (например Twitter) и не является сделано в менее хороших PWA (например ВК).
Таким образом, большие списки выгоднее формировать с помощью метода InsertAdjacentHTML(), который вполне способен работать с текстовыми параметризуемыми веб-компонентами, но вряд ли применим к управляемым компонентам, и таких примеров достаточно.
Какие проблемы решает реактивность, и как их можно решить по-другому:
- Состояние веб-приложения состоит из переменных JS, а DOM — это просто отображение, которое необходимо «умно» воссоздать.
Идея хорошая, но почему собственно данные должны храниться в JS-объектах, а не напрямую в DOM-узлах? Когда мы говорим «данные веб-приложения», мы имеем в виду не базу данных или бизнес-логику, а исключительно пользовательский интерфейс, где все данные уже так или иначе принадлежат слою представления.
Так почему же мы изначально не можем организовать дерево DOM таким образом, чтобы оно отражало структуру предметной области? Здесь пригодится компонентный подход — веб-компоненты могут гибко инкапсулировать собственное состояние (частные члены классов JS), иметь документированный общедоступный API в виде геттеров/сеттеров, генерировать и перехватывать пользовательские события DOM внутри своей иерархии, обращаться с помощью querySelector(), регистрируйтесь глобально в окне или в пользовательской «шине событий» — и все это стандартными средствами, без сторонних концепций, привнесенных различными фреймворками.
Во-первых статья Я пытался это сказать, попробую еще раз.
- Данные меняются в одном месте и автоматически отображаются везде.
Это сильное преимущество, но в целом хорошо спроектированное приложение строится не из стандартных элементов HTML, а из пользовательских компонентов, которые также (теоретически) могут управляться разными фреймворками и, следовательно, могут иметь несовместимые API состояния.
Так зачем мне в этом случае React или Vue? Мне нужна реактивность компонентов, а не реактивность HTML. С компонентами я могу использовать любую библиотеку реактивности или обойтись без нее, в зависимости от масштаба приложения.
К тому же такая реактивность не часто бывает востребована.
Я помню сайты с «дизайном 90-х», где действительно одни и те же данные могли отображаться на странице несколько раз в разных представлениях.
Однако сейчас дизайн тяготеет к минимализму, а в мобильных PWA тем более — из-за небольшой диагонали сложные формы приходится разбивать на несколько последовательных экранов, и у нас всегда есть событие смены экрана, в котором мы можем обновить нужную часть ДОМ.
То есть вместо push-реактивности нам достаточно иметь набор геттеров для данных, независимо от того, где они на самом деле хранятся.
- Функциональный (декларативный) код легче тестировать и поддерживать.
С этим невозможно спорить, но к сожалению функциональность + виртуальный DOM — вещи не бесплатные, они существенно нагружают и процессор, и сборщик мусора.
В противном случае РСБ не была бы изобретена.
УПД Выпуск сборки демонстрационные приложения Ionic-React — это 2,3 МБ минифицированного JS, тогда как ванильное приложение, обладая в несколько раз большей функциональностью, весит в 85 раз (!) меньше.
Потому что:
- Вы можете прикрепить любые данные к элементу DOM с помощью пользовательских свойств DOM; коллекции таких элементов можно трансформировать в стиле filter/map/reduce и даже передавать в качестве параметра другим компонентам.
- Функция querySelector() — отличный API для обращения к компонентам (которого нет даже в JS), и нет смысла изобретать собственный велосипед внутри искусственно созданного «единого источника истины».
- Система событий DOM — отличный механизм взаимодействия компонентов внутри иерархии (и, что немаловажно, без цепочек) и позволяет связывать компоненты, управляемые разными реактивными библиотеками.
- Новый синтаксис частных свойств (#) и методы получения/установки JS позволяют гибко инкапсулировать состояние (в отличие от свойств).
Этот мобильное PWA для условного риэлтора - сотрудник подошел к точке, сфотографировал, записал видео, добавил описание с помощью клавиатуры или голоса, расставил теги и сохранил карту вместе с геокоординатами в локальную IndexedDB. При появлении соединения происходит фоновая синхронизация с сервером.
Я попытаюсь продемонстрировать вышесказанное на примере следующих компонентов:
- Форма списка .
Список создается один раз, а затем перехватывает соответствующие события DOM и на основе их данных изменяется сам.
Например, так обновляется и удаляется карточка объекта (ev.val содержит обновленный объект, а созданное свойство является ключом объекта в базе данных и одновременно идентификатором узла).
Первая строка изменяет базу данных, вторая — DOM компонента:
this.addEventListener('save-item', async (ev) => { await this.saveExistObj(ev.val) this.querySelector('#' + ev.val.created).
replaceWith( document.createElement('obj-list-item').
build(ev.val) ) }) this.addEventListener('delete-item', async (ev) => { await this.deleteObj(ev.val) this.querySelector('#' + ev.val).
remove() })
Да, ванильные веб-компоненты имеют досадный недостаток: они не могут использовать конструктор с параметрами, поэтому вам придется использовать фабричный метод build(), но с этим можно жить.
- Форма редактирования объекта .
Он вызывается кликом по элементу списка и открывается «модально», то есть в абсолютно позиционированном DIVе, полностью закрывающем список.
Это удобно, потому что при закрытии формы текущая позиция прокрутки списка сохраняется с точностью до пикселя, а если добавить CSS-анимацию, то будет совсем красиво.
Важно, что с точки зрения DOM-дерева форма редактирования является потомком элемента списка, а значит, события формы можно перехватывать как на уровне элемента списка, так и на уровне самого списка ( как в нашем случае).
Когда пользователь нажимает кнопку «Сохранить», генерируется обновленный объект и генерируется всплывающее событие, которое перехватывается приведенным выше кодом.
Таким образом, форма редактирования не привязана к форме списка; он не знает, что именно редактирует. Вот фрагменты формы редактирования, включая описание кнопки Сохранить:
import * as WcMixin from '/WcApp/WcMixin.js' const me = 'obj-edit' customElements.define(me, class extends HTMLElement { obj = null props = null location = null connectedCallback() { WcMixin.addAdjacentHTML(this, ` .
<div w-id='descDiv/desc' contenteditable='true'></div> .
<media-container w-id='mediaContainer/medias' add='true' del='true'/> `) .
this.appBar = [ .
['save', () => { if (!this.desc) { APP.setMessage('EMPTY DESCRIPTION!', 3000) this.descDiv.focus() } else { const obj = { created: this.obj.created, modified: APP.now(), location: this.location, desc: this.desc, props: this.props, medias: this.medias } this.bubbleEvent('save-item', obj) history.go(-1) APP.setMessage('SAVED !', 3000) } }] ] } .
})
Добавляем HTML с помощью крошечной библиотеки WcMixin , это единственный библиотечный код в проекте, и все, что он делает, — это для каждого элемента HTML, отмеченного атрибутом w-id, создает метод получения/установки для его «значения» (тип значения зависит от типа элемент).Таким образом, this.deskDiv — это ссылка на элемент div, а this.desc — «значение» элемента div (в данном случае InnerHTML).
То есть мы можем получить доступ к значениям элементов HTML-формы (input, select, radio и т.д.) как к обычным переменным текущего класса.
Это также работает для веб-компонентов, вам просто нужно добавить к компоненту геттер val (см.
ниже).
Таким образом, this.medias возвращает массив медиа-объектов (фото, видео и аудио) из элемента медиа-контейнера.
- Компонент медиа-контейнер содержит коллекцию мультимедиа (фото, видео, аудио) в виде коллекции дочерних узлов img. Свойство src элемента img хранит только изображение предварительного просмотра (в формате data-url для ускорения рендеринга), а полный медиа-объект, содержащий тип, предварительный просмотр и исходный большой двоичный объект, хранится в пользовательском свойстве _source того же элемента img. .
В результате код геттера, возвращающего медиа-массив, выглядит как преобразование списка узлов в массив.
И зачем нам здесь какое-то глобальное «государство»?
get val() { return Array.from(this.querySelectorAll('img')).
map(el => el._source) }
А вот так выглядит добавление нового медиа-элемента в контейнер (нажатие на превью открывает «модальную» форму медиаплеера):add(media) { const med = document.createElement('img') med._source = media med.src = media.preview this.addBut.before(med) med.onclick = (ev) => { ev.stopPropagation() APP.routeModal( 'media-player', document.createElement('media-player').
build(media) ) } }
- Форма для добавления новых медиа .
Также содержит элемент медиа-контейнера, который в момент съемки добавляет фотообъект с помощью метода add() выше:
this.imgBut.onclick = async () => { const blob = await this.imgCapturer.takePhoto(this.imgParams) this.mediaContainer.add( { created: APP.now(), tagName: 'img', preview: this._takePreview('IMG'), origin: await this._takeOrigin(blob) } ) }
- Компонент приложение-приложение — это платформа приложения, которая обеспечивает навигацию по страницам (линейную в стиле мастера и модальную в стиле стека), правильную обработку кнопки возврата браузера (навигация на основе хеша), панель приложения с контекстно-зависимыми кнопками и небольшой дополнительный сервис.
Эти компоненты не имеют прямого отношения к теме статьи, их спроектировал я.
отдельный проект и использовать его как свою собственную мини-платформу.
Краткое содержание
Собственно, это все, что я хотел сказать.На примере готового мобильного приложения я попытался показать, что реактивный фреймворк совершенно необязателен, а архитектурно приемлемый код можно получить, просто используя современные «ванильные» веб-стандарты.
Я уважаю React, его концепции теоретически интересны и практически полезны, но их слишком много, в результате чего два React-приложения могут быть совершенно разными по своей структуре до неузнаваемости.
С другой стороны, веб-компоненты настолько просты, что вам не нужно знать ничего, кроме шаблонов ООП.
Спасибо за внимание.
Теги: #Разработка веб-сайтов #JavaScript #pwa #vue.js #react.js #Веб-компоненты #реактивность #Valilla JS
-
Интернет-Технологии: Формирование Сети
19 Oct, 24 -
Фрагменты Для Twitter Bootstrap
19 Oct, 24 -
Пять Тупиковых Диалогов Лидера И Менеджера
19 Oct, 24