Вопросы к UI. Шаблон компонента. Основная часть

Ну что, продолжаем критиковать существующие подходы создания пользовательских интерфейсов, стоить теории — как привести все это дело в порядок, и ныться о том, как мы до такого докатились.
Данная статья является основной частью ранее опубликованную работы, посвященной синтаксису и способам определения шаблонов компонентов.
https://habr.com/ru/articles/864816/
Не будем сразу обращаться к содержанию предыдущей статьи, начнем с базы.
База. Понятия
На случай если кто-то не особо понимает, о чем речь, освежим некоторые понятия
Верстка (визуальная часть) = разметка (HTML) + стили (CSS)
Шаблон компонента = Верстка + Часть логики над ней (JS).
Почему часть — принцип разделения логики управления данными и представления.
По сути, все что остается в шаблоне это привязка данных, и спец. конструкции
(вставки, if, for).
Цель шаблона — сделать интерфейс динамическим, интерактивным и модульным.
Компонент — структура, описывающая элемент интерфейса целиком.
Вставки, они же вставки данных — любой блок кода в верстке.
Распространенные обозначения: {{ }} или { }
Используются для задания пропсов, атрибутов, а также в качестве слотов.
Слот — произвольно место в верстке для вставки контента (данные, компоненты, if, for)
Структура шаблона
Композиция компонентов и тегов
Стили
Реактивные атрибуты тегов
Реактивные вставки
Условный рендеринг (if)
Рендеринг списков (for)
Реактивные пропс (?)
Некоторые «киллер фичи» — какие-то на первый взгляд неочевидные вещи, например передача ссылки на тег в переменную вот таким образом ref={formRef}
Способы формирования шаблона
Фундаментально можно выделить два способа:
UI-фреймворки, в большинстве своем формируют шаблон за счет синтаксического сахара,
а в дополнение предлагают некую «возможность» писать все без него, но как правило, она оставляет желать лучшего.
JSX
Text
;
Нативный синтаксис
React.createElement(Card, {
className: "title"
}, React.createElement("p", null, "Text"));
Ниже указаны некоторые особенности каждого из способов
Шаблоны на синтаксическом сахаре
Меньший контроль
Стандартизированный подход, абстракции => простота, читаемость, быстрая разработка
Подобные шаблоны более строгие и требовательные, дают меньше свободы => меньше шансов сделать ошибку
Лучшая интеграция с инструментами разработки — автокомплит, подсветка синтаксиса, и прочие спец. возможности IDE.
Набор встроенных функциональных фич — реактивность, привязка данных, слоты.
Зависимость от других инструментов — сборщиков, библиотек, плагинов, поскольку синтаксис требует транспиляции. Все это усложняет проект, процесс его разработки
За счет транспиляции и строгости — проще реализовать ssr или другой бэкенд-рендеринг
Магия — требует обучения, может привести к недопониманию происходящего, к путанице
Отсутствие нативности — решение не будет работать напрямую в браузере, или же потребует для этого дополнительных усилий, принудит пойти на неприятный компромисс
Вероятно меньшая производительность ввиду транспиляции, различных прослоек (?)
Высокая скорость разработки
Нативный шаблон
Прозрачность, полный контроль процесса
За счет меньшей строгости — больший шанс допустить ошибку, особенно новичку
Нативность/универсальность — стандартный синтаксис js, будет работать в любом окружении, поддерживаться любыми IDE.
Сложность и громоздкость — вероятно будет чуть больше кода и труднее читать (?)
Возможно более сложная реализация функциональных фич — реактивности, привязки данных, слотов (?)
Независимость от инструментов преобразования кода
Более трудная реализация ssr
Упрощенное обучение за счет нативности, прямой и очевидной реализации (?)
Вероятно более высокая производительность, за счет нативности (?)
Вероятно чуть меньшая скорость разработки (?)
Изначально хотел отразить это табличкой, но пункты не особо симметричны и не столь однозначны — большую часть из них так и хочется вынести за скобки.
По сути, это так — чисто наброски, чтобы примерно представлять на что следует обращать внимание.
Все решает исключительно конкретная реализация.
Ближе к делу
Чтобы продумать какой-то синтаксис, нужно четко понимать, чего мы от него хотим.
Для начала — давайте просто взглянем на жизненный путь кусочка jsx-кода (react)
1 — Написали так
Text
2 — После транспиляции получилось такое
React.createElement("div", { className: "title" }, "Text");
3 — После запуска кода получаем объект (часть virtual dom)
{ type: "div", props: { className: "title", children: "Text" } };
4 — После отрисовки мы получаем что? правильно — результат на лицо
Text
Вопросы?)
Разумеется, это упрощенный пример, без js-конструкций, но чисто в плане верстки все обстоит как-то так
Если кто-то не выкупает как формируются элементы DOM — вот один из способов.
const div = document.createElement("div");
div.className = "title";
const content = document.createTextNode("Text");
div.appendChild(content);
Другие способы: innerHTML и template + cloneNode, но нам пока они не интересны.
Итак, в этапах 2 и 3 можно разглядеть альтернативные способы задания шаблона
2 — посредством по сути создания тегов на месте — разметка будет формироваться сразу
Кстати — во vue и preact этот этап выглядит также
h("div", { className: "title" }, "Text"),
3 — посредством структуры, которую в дальнейшем придется обойти, чтобы превратить все в конечную верстку
<Способ 1 - через объект>
Про 3 мало что можно сказать. Синтаксис уже, по сути, перед вами. Улучшать тут нечего.
Необходимость дополнительного обхода циклом не кажется привлекательным решением.
К тому же при подобной структуре очень быстро нарастает вложенность дочерних элементов — шаблон слишком быстро растет вправо. Возможно, еще вернемся к этому, на первый взгляд идея кажется сомнительной.
а вот 2 — его можно попробовать доработать
Что вообще случилось в примере — мы взяли js, и решили с его помощью создать тег div, задать атрибут className: 'title', и добавить содержимое — 'Text'
Это похоже на то, к чему мы пришли в предыдущей статье
«Ну что, у вас есть: tagName, props и children. Осталось только объединить их.»
Недолго думая, упрощаем до сути
div({ className: "title", children: "Text" })
Для удобства делаем параметры именованными, поэтому используем объект
Разумеется, подобный подход уже существует, и зовется фабрикой элементов, а это лишь одна из его реализаций.
Фабрика элементов — это паттерн, который упрощает создание DOM-элементов путем создания функций или оберток вокруг document.createElement.
Продумывание синтаксиса
Ну что, пробуем реализовать все фичи шаблона для нашего примера.
В процессе будем стараться как-то улучшать и упрощать синтаксис
Некоторый план возможностей шаблона, для реализации
Тег и атрибуты
Композиция тегов
Компонент и пропс
Специальные конструкции (реактивные атрибуты и пропс, вставки, if, else)
Тег и атрибуты мы уже задали. Пробуем реализовать композицию.
За основу возьмем вот такой пример
Text
Text
С учетом того, что дочерних элементов может быть несколько, более подходящая структура для этого — массив
<Способ 2 - множественные функции. Вариант с children>
div({
className: "title",
children: [
div({
className: "title",
children: [
div({ className: "title", children: [ "Text" ] }),
div({ className: "title", children: [ "Text" ] })
]
})
]
})
Да, на что-то приятное это не тянет, да и от способа через json мы недалеко ушли
Что тут не так. Вложенность точно не задалась)
каждый раз приходится писать свойство children
приходится расписывать свойства построчно, ввиду чего шаблон быстро растет вправо
А что, если отделить children от прочих пропс? Но куда?!
Давайте попробуем сперва сделать второй параметр для div, который и будет принимать children
<Способ 2 - множественные функции. Вариант с [ ]>
div({ className: "title" }, [
div({ className: "title" }, [
div({ className: "title" }, ["Text"]),
div({ className: "title" }, ["Text"])
])
])
Вроде получше, но немного какой-то майнкрафт, не?
Конечно, массив можно сделать опциональным на случай одного child, или его отсутствия, но он в целом как будто избыточен — стоит хотя бы попробовать уйти от него, убираем.
<Способ 2 - множественные функции. Вариант без [ ]>
div(
{ className: "title" },
div(
{ className: "title" },
div({ className: "title" }, "Text"),
div({ className: "title" }, "Text")
)
])
Вроде тоже неплохо. Но основная проблема здесь — форматирование — непонятно как лучше делать переносы, хуже прослеживаются уровни скобок, к тому же — без особых настроек IDE, мы вероятно будем получать что-то такое при автоформатировании.
div({ className: "title" }, div({ className: "title" }, div({ className: "title" }, "Text")))
или такое
div({
className: "title"
},
div({
className: "title"
},
div({
className: "title"
}, "Text"),
div({
className: "title"
}, "Text")
)
Впрочем, это решаемо. Помимо этого — первый параметр обязательный.
а div у нас может не иметь атрибутов, но при этом иметь детей
Поэтому придется каждый раз вписывать нижнее подчеркивание _ или пустой объект { },
Об это нужно будет не забывать в процессе разработки, дабы не натворить делов.
Пробуем дальше: для children делаем второй вызов, получаем следующее
<Способ 2 - множественные функции. Вариант с замыканием>
div({ className: "title" })(
div({ className: "title" })(
div({ className: "title" })("Text"),
div({ className: "title" })("Text")
)
)
Выглядит вроде норм, особенно если сверить с исходным html куском
Но что случилось? Да, появилось замыкание, что плохо отразиться на производительности, особенно на большом количестве элементов.
Хотя сам по себе способ максимально привлекательный за счет хорошего форматирования, и гибкости, получаемой за счет каррирования.
Что тут еще можно улучшить?
Второй вызов делаем необязательным — на случай если тег одиночный, или не имеет детей. Помимо этого, можно сделать свойство child опциональным в первом параметре. В таком случае если оно задается одним простым примитивом, что бывает часто, — не придется делать для него отдельный вызов, некрасивый перенос скобок на новую строку.
и вместо
div({ className: "title" })(
"Text Text Text Text Text Text Text Text Text"
)
у нас будет
div({
className: "title",
child: "Text Text Text Text Text Text Text Text Text"
})
Для вставки экземпляра компонента в шаблон ничего не меняется.
div({ className: "title" }) => Card({ className: "title" })
Других хороших вариантов композиции тегов, кроме выведенных выше не вижу.
Двигаемся дальше.
Теперь по поводу спец. конструкций (реактивные атрибуты и пропс, вставки, if, else).
В нашем случае долго это обсуждать не придется, поскольку все нативно.
Для того чтобы отрендерить список элементов достаточно обойти массив, добавляя элементы в fragment или другой тег. А после вставить его в верстку.
div({ className: "list" })(
list(items, (item) => (
div({ className: "item", child: item })
));
)
Сработает ли это — да, но интерфейс будет статичным.
Все волшебство реализуется за счет реактивности, а это тема для другой статьи.
В целом первоначально по нашему плану мы прошлись, давайте делать какие-то выводы.
Зачастую компоненты в коде описывается в виде класса или функции.
Тоже самое касается и тегов, если смотреть не на сахар, а на конечное представление.
Получается — если бы мы писали нативно и делали все сразу как нужно — синтаксис был бы примерно таким, как в примерах выше.
Есть компонент, есть его пропс, есть дочерние элементы.
Все, желаемая композиция получена.
Поскольку все нативно — прямо в шаблоне мы, в целом, можем писать практически любой код, без использования повсеместных { } для вставок, как это делается в jsx.
Также это дает некоторую гибкость — можно легко дробить шаблон на части, выносить его пропс в отдельную константу для удобства. И тд и тд.
Причем, используя подобный подход вам необязательно пихать логику в шаблон — вы можете описать ее где-то рядом — в остальной части компонента.
Более того вы можете формировать шаблон компонента кусочками, в императивном стиле, обрабатывать каждый из тегов по отдельности, построчно.
В некоторых случаях это может дать читаемость для конечного (корневого) представления компонента — по итогу оно будет сведено к композиции из семантических названий составляющих ее элементов.
Примерно вот так
Page(
Header(
Logo,
Menu
),
Content(
ListOfItems
),
Footer,
)
А все прочее, включая атрибуты, пропсы, будет указано где-то выше, в императивном стиле.
Ну и присвоено разумеется в какие-то переменные/константы, а потом уже указано в конечном представлении.
const onHeaderClick = () => {};
const cn = cx('header', isDarkTheme ? 'dark' : 'light');
const Header = div({ className: cn, onClick: onHeaderClcik });
Подход, по сути, универсален — можно писать императивно, а можно декларативно.
И вы, как разработчик можете контролировать это. И скажем в повседневке описывать все декларативно, а сложные кейсы прописывать отдельно, императивно. И уже после монтировать их в конечное представление.
А что насчет множества html-тегов, как их сделать доступными всюду?
Мы еще вернемся к этому, но пока, дабы вас успокоить, навскидку предложу след. варианты
globalThis
объединить в один объект, и так и использовать ui.div (фи)
каждый раз делать деструктуризацию { div } = ui; (фи)
добавлять строку импорта со всеми тегами к скрипту при загрузке
with
Так в чем тут замысел?
Главной выжимкой из примеров ui на мобилках из предыдущей статьи является отсутствие там дополнительных языков для разработки. Абсолютно все от и до происходит за счет основного языка программирования.
То есть — пользовательский интерфейс полностью формируется за счет единой технологии.
Нет необходимости скрещивать ниф-нифа наф-нафа и нуф-нуфа.
Разметка, стили, логика — все едино, и описывается одним яп.
«Способ 2» имеет практически полное сходство с теми примерами, но за счет гибкости js, как мы видим, можно достичь еще большей привлекательности и гибкости синтаксиса.
Для меня этот способ остается самым приоритетным за счет того, что это небольшой шаг в сторону, от сложившийся ситуации в вебе.
Нет, дело ведь не в том, чтобы взять и отказаться от html и css, а в том чтобы грамотно спрятать их за ширмой, объединить за счет яп, и попробовать привести все в какой-то порядок. Напомню, в браузере уже существует базовый механизм, который позволяет это сделать — DOM API. Как видите, если немного поработать с ним напильником, можно получить что-то стоящее.
Важно понимать, что одна из базовых задач, которую за нас выполняют существующие фреймворки — склейка всего этого безобразия в какое-то единое целое. И происходит это как раз таки за счет различных jsx-подобных синтаксисов, за счет структуры компонента.
Как по мне, нетрудно предположить, что со временем весь ui будет описываться схожим или даже единым образом для всех платформ.
Согласны вы с этим всем или нет — мы придем к этому, 100.
Будь у нас сейчас подобный стандарт, на рынке было бы куда меньше когнитивного диссонанса от всего это оркестра безумных технологий.
Да, дело не только в шаблоне, это лишь кирпичик, необходимый для реализации данной концепции.
Касательно множества возникающих вопросов по данному способу, например: как на все это дело ляжет реактивность, какой она будет, как все это дело впишется в компонент, что со стилями, и как лучше прописывать оставшуюся часть логики — все это темы отдельных статей.
Если, конечно вам, вообще все это нужно)
Чтош, идем дальше. Разумеется, есть еще как минимум пара способов нативно задать шаблон, которые стоит хотя бы упомянуть.
<Способ 3 - Строковое представление. Шаблонные строки (template literals)>
const template = `
${title}
${description}
`;
Пожалуй, наипростейший способ создания шаблона. Но сделать из этого что-то достойное, по сути — анрил, кроме вставок тут больше никаких возможностей то и нет. А преобразование в конечный набор узлов реализуется через тот же innerHTML
<Способ 3 - Строковое представление. Тегированные функции (tag functions)>
html`
<${Header} name="ToDo's (${page})" />
${todos.map(todo => html`
- ${todo}
`)}
<${Footer}>footer content here/>
`;
Значительно более перспективный его собрат. Здесь за ширмой возможно вытворять всяческие манипуляции с прилетающими в функцию вставками и их значениями.
Подход используется во многих фреймворках, в данном случае это preact.
Как видите, идея синтаксиса тут примерно такая же, что и в решениях с использованием синтаксического сахара.
Оба способа позволяют реализовать шаблон нативно, и делают это достаточно производительно. Но данные подходы по-прежнему используют html в привычном виде.
Даже без учета этого — не знаю как вы, а я терпеть не могу шаблонизаторы. Как по мне это очередной вид извращений, который до сих пор реализуется во многих яп.
А про каскад шаблонных строк из кода выше я вообще молчу.
В общем просто оставляю примеры здесь, и пора закругляться.
Не хочу выносить анализ jsx в отдельную статью, давайте добьем на месте.
Изъяны синтаксиса jsx. Что же с ним не так?
Использование html в качестве основы шаблонов
использование угловых скобок
необходимость прописывать имя парного тега дважды, что избыточно
не самый приятный способ задания атрибутов, в html это делается в виде строк.
Вставки
для их объявления необходимо каждый раз указывать блок кода { }.
в целом это выглядит более-менее приемлемо, когда вставка где-то между тегов. Но при передаче пропс, или задании атрибутов, необходимость постоянного написания знака «ровно» и фигурных скобок (={ }) кажется избыточной, начинает бесить
также при совпадении key и value нельзя указать только key. Что кажется полным бредом, особенно на большом количестве ключей
комментарии также задаются с помощью обертки в блок, что неудобно
поддерживаются только js-выражения, произвольный код сюда нельзя писать, это может сбивать с толку
условный рендеринг прямо в шаблоне выглядит крайне непривлекательно, из-за него невероятно быстро растет вложенность. исключением мб лишь простейшее однострочное условие с помощью &&. Например, {yes && }
{content}
//это
//вместо этого
{/*Комментарий*/}
{yes ? (
Yes
) : (
No
)}
Работа с ContextAPI Вложенность провайдеров, с которой думаю все знакомы.
Вроде решаемо, но не стоит говорить что этого нет.
Это только то, что вспомнилось, а по факту — подобных бесячих моментов намного больше.
Итак, задача была в том, чтобы найти качественный подход задания шаблона компонента.
И по возможности сделать это нативным способом.
Как я говорил ранее — шаблон, реализованный через синтаксический сахар, может быть абсолютно любым — решаете только вы, ваша воображалка. Скажу как есть — я просто не знаю как можно было написать его иначе. После того как я открыл для себя подобный синтаксис, мне стало казаться что шаблон как будто бы вообще всегда задавался именно таким образом. Словно это норма, данность, стандарт. Короче — будь у меня задача реализовать шаблон через сахар — я бы сделал его точно таким.
Впрочем, судить вам.
Не знаю, что еще тут добавить)
Вроде все основное сказал по данной теме.
Надеюсь, вы уже понимаете, что это не последняя статья, это самое начало.
Мне крайне важна обратная связь, вопросы, обсуждения, поддержка и хейт с вашей стороны.
Пожалуйста, напишите что-нибудь от себя снизу, или в личку — будь то послевкусие, замечание, эмоция или мысль. Спасибо!
Оставлю небольшую голосовалку, мб кому-то будет интересно поучаствовать.
Пока преимущественно судим синтаксис и подход.
Лично мне понравились варианты с массивом и с замыканием.
Важно помнить, что все есть компромисс.
