Ну что, продолжаем критиковать существующие подходы создания пользовательских интерфейсов, стоить теории — как привести все это дело в порядок, и ныться о том, как мы до такого докатились.
Данная статья является основной частью ранее опубликованную работы, посвященной синтаксису и способам определения шаблонов компонентов.
https://habr.com/ru/articles/864816/
Не будем сразу обращаться к содержанию предыдущей статьи, начнем с базы.
База. Понятия
На случай если кто-то не особо понимает, о чем речь, освежим некоторые понятия
Верстка (визуальная часть) = разметка (HTML) + стили (CSS)
Шаблон компонента = Верстка + Часть логики над ней (JS).
Почему часть — принцип разделения логики управления данными и представления.
По сути, все что остается в шаблоне это привязка данных, и спец. конструкции
(вставки, if, for).
Цель шаблона — сделать интерфейс динамическим, интерактивным и модульным.
Компонент — структура, описывающая элемент интерфейса целиком.
Вставки, они же вставки данных — любой блок кода в верстке.
Распространенные обозначения: {{ }} или { }
Используются для задания пропсов, атрибутов, а также в качестве слотов.
Слот — произвольно место в верстке для вставки контента (данные, компоненты, if, for)
Структура шаблона
-
Композиция компонентов и тегов
-
Стили
-
Реактивные атрибуты тегов
-
Реактивные вставки
-
Условный рендеринг (if)
-
Рендеринг списков (for)
-
Реактивные пропс (?)
-
Некоторые «киллер фичи» — какие-то на первый взгляд неочевидные вещи, например передача ссылки на тег в переменную вот таким образом ref={formRef}
Способы формирования шаблона
Фундаментально можно выделить два способа:
-
С использованием синтаксического сахара (лучше для ssr)
-
Нативно (лучше для рантайма)
UI-фреймворки, в большинстве своем формируют шаблон за счет синтаксического сахара,
а в дополнение предлагают некую «возможность» писать все без него, но как правило, она оставляет желать лучшего.
JSX
<Card className="title"> <p>Text</p> </Card>;
Нативный синтаксис
React.createElement(Card, { className: "title" }, React.createElement("p", null, "Text"));
Ниже указаны некоторые особенности каждого из способов
Шаблоны на синтаксическом сахаре
-
Меньший контроль
-
Стандартизированный подход, абстракции => простота, читаемость, быстрая разработка
-
Подобные шаблоны более строгие и требовательные, дают меньше свободы => меньше шансов сделать ошибку
-
Лучшая интеграция с инструментами разработки — автокомплит, подсветка синтаксиса, и прочие спец. возможности IDE.
-
Набор встроенных функциональных фич — реактивность, привязка данных, слоты.
-
Зависимость от других инструментов — сборщиков, библиотек, плагинов, поскольку синтаксис требует транспиляции. Все это усложняет проект, процесс его разработки
-
За счет транспиляции и строгости — проще реализовать ssr или другой бэкенд-рендеринг
-
Магия — требует обучения, может привести к недопониманию происходящего, к путанице
-
Отсутствие нативности — решение не будет работать напрямую в браузере, или же потребует для этого дополнительных усилий, принудит пойти на неприятный компромисс
-
Вероятно меньшая производительность ввиду транспиляции, различных прослоек (?)
-
Высокая скорость разработки
Нативный шаблон
-
Прозрачность, полный контроль процесса
-
За счет меньшей строгости — больший шанс допустить ошибку, особенно новичку
-
Нативность/универсальность — стандартный синтаксис js, будет работать в любом окружении, поддерживаться любыми IDE.
-
Сложность и громоздкость — вероятно будет чуть больше кода и труднее читать (?)
-
Возможно более сложная реализация функциональных фич — реактивности, привязки данных, слотов (?)
-
Независимость от инструментов преобразования кода
-
Более трудная реализация ssr
-
Упрощенное обучение за счет нативности, прямой и очевидной реализации(?)
-
Вероятно более высокая производительность, за счет нативности (?)
-
Вероятно чуть меньшая скорость разработки (?)
Изначально хотел отразить это табличкой, но пункты не особо симметричны и не столь однозначны — большую часть из них так и хочется вынести за скобки.
По сути, это так — чисто наброски, чтобы примерно представлять на что следует обращать внимание.
Все решает исключительно конкретная реализация.
Ближе к делу
Чтобы продумать какой-то синтаксис, нужно четко понимать, чего мы от него хотим.
Для начала — давайте просто взглянем на жизненный путь кусочка jsx-кода (react)
1 — Написали так
<div className="title">Text</div>
2 — После транспиляции получилось такое
React.createElement("div", { className: "title" }, "Text");
3 — После запуска кода получаем объект (часть virtual dom)
{ type: "div", props: { className: "title", children: "Text" } };
4 — После отрисовки мы получаем что? правильно — результат на лицо
<div className="title">Text</div>
Вопросы?)
Разумеется, это упрощенный пример, без 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)
Тег и атрибуты мы уже задали. Пробуем реализовать композицию.
За основу возьмем вот такой пример
<div className="wrapper"> <div className="header"> <div className="left">Left</div> <div className="right">Right</div> </div> </div>
С учетом того, что дочерних элементов может быть несколько, более подходящая структура для этого — массив
<Способ 2 — множественные функции. Вариант с children>
div({ className: "wrapper", children: [ div({ className: "header", children: [ div({ className: "left", children: [ "Left" ] }), div({ className: "right", children: [ "Right" ] }) ] }) ] })
Да, на что-то приятное это не тянет, да и от способа через json мы недалеко ушли
Что тут не так. Вложенность точно не задалась)
-
каждый раз приходится писать свойство children
-
приходится расписывать свойства построчно, ввиду чего шаблон быстро растет вправо
А что, если отделить children от прочих пропс? Но куда?!
Давайте попробуем сперва сделать второй параметр для div, который и будет принимать children
<Способ 2 — множественные функции. Вариант с [ ]>
div({ className: "wrapper" }, [ div({ className: "header" }, [ div({ className: "left" }, ["Left"]), div({ className: "right" }, ["Right"]) ]) ])
Вроде получше, но немного какой-то майнкрафт, не?
Конечно, массив можно сделать опциональным на случай одного child, или его отсутствия, но он в целом как будто избыточен — стоит хотя бы попробовать уйти от него, убираем.
<Способ 2 — множественные функции. Вариант без [ ]>
div( { className: "wrapper" }, div( { className: "header" }, div({ className: "left" }, "Left"), div({ className: "right" }, "Right") ) )
Вроде тоже неплохо. Но основная проблема здесь — форматирование — непонятно как лучше делать переносы, хуже прослеживаются уровни скобок, к тому же — без особых настроек IDE, мы вероятно будем получать что-то такое при автоформатировании.
div({ className: "wrapper" }, div({ className: "header" }, div({ className: "left" }, "Left"), div({ className: "right" }, "Right")))
или такое
div({ className: "wrapper" }, div({ className: "header" }, div({ className: "left" }, "Left"), div({ className: "right" }, "Right") ) )
Впрочем, это решаемо. Помимо этого — первый параметр обязательный.
а div у нас может не иметь атрибутов, но при этом иметь детей
Поэтому придется каждый раз вписывать нижнее подчеркивание _ или пустой объект { },
Об это нужно будет не забывать в процессе разработки, дабы не натворить делов.
Пробуем дальше: для children делаем второй вызов, получаем следующее
<Способ 2 — множественные функции. Вариант с замыканием>
div({ className: "wrapper" })( div({ className: "header" })( div({ className: "left" })("Left"), div({ className: "right" })("Right") ) )
Выглядит вроде норм, особенно если сверить с исходным html куском
Но что случилось? Да, появилось замыкание, что плохо отразиться на производительности, особенно на большом количестве элементов.
Хотя сам по себе способ максимально привлекательный за счет хорошего форматирования, и гибкости, получаемой за счет каррирования.
Что тут еще можно улучшить?
Второй вызов делаем необязательным — на случай если тег одиночный, или не имеет детей. Помимо этого, можно сделать свойство child опциональным в первом параметре. В таком случае если оно задается одним простым примитивом, что бывает часто, — не придется делать для него отдельный вызов, некрасивый перенос скобок на новую строку.
и вместо
div({ className: "wrapper" })( "Text Text Text Text Text Text Text Text Text" )
у нас будет
div({ className: "wrapper", child: "Text Text Text Text Text Text Text Text Text" })
Для вставки экземпляра компонента в шаблон ничего не меняется.
div({ className: "red" }) => Card({ className: "red" })
Других хороших вариантов композиции тегов, кроме выведенных выше не вижу.
Двигаемся дальше.
Теперь по поводу спец. конструкций (реактивные атрибуты и пропс, вставки, 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
Касательно множества возникающих вопросов по данному способу, например: как на все это дело ляжет реактивность, какой она будет, как все это дело впишется в компонент, что со стилями, и как лучше прописывать оставшуюся часть логики — все это темы отдельных статей.
Если, конечно вам, вообще все это нужно)
Чтош, идем дальше. Разумеется, есть еще как минимум пара способов нативно задать шаблон, которые стоит хотя бы упомянуть.
<Способ 3 — Строковое представление. Шаблонные строки (template literals)>
const template = ` <div class="card"> <h1>${title}</h1> <p>${description}</p> </div> `;
Пожалуй, наипростейший способ создания шаблона. Но сделать из этого что-то достойное, по сути — анрил, кроме вставок тут больше никаких возможностей то и нет. А преобразование в конечный набор узлов реализуется через тот же innerHTML
<Способ 3 — Строковое представление. Тегированные функции (tag functions)>
html` <div class="app"> <${Header} name="ToDo's (${page})" /> <ul> ${todos.map(todo => html` <li key=${todo}>${todo}</li> `)} </ul> <button onClick=${() => this.addTodo()}>Add Todo</button> <${Footer}>footer content here<//> </div> `;
Значительно более перспективный его собрат. Здесь за ширмой возможно вытворять всяческие манипуляции с прилетающими в функцию вставками и их значениями.
Подход используется во многих фреймворках, в данном случае это preact.
Как видите, идея синтаксиса тут примерно такая же, что и в решениях с использованием синтаксического сахара.
Оба способа позволяют реализовать шаблон нативно, и делают это достаточно производительно. Но данные подходы по-прежнему используют html в привычном виде.
Даже без учета этого — не знаю как вы, а я терпеть не могу шаблонизаторы. Как по мне это очередной вид извращений, который до сих пор реализуется во многих яп.
А про каскад шаблонных строк из кода выше я вообще молчу.
В общем просто оставляю примеры здесь, и пора закругляться.
Не хочу выносить анализ jsx в отдельную статью, давайте добьем на месте.
Изъяны синтаксиса jsx. Что же с ним не так?
-
Использование html в качестве основы шаблонов
-
использование угловых скобок
-
необходимость прописывать имя парного тега дважды, что избыточно
-
не самый приятный способ задания атрибутов, в html это делается в виде строк.
-
Вставки
-
для их объявления необходимо каждый раз указывать блок кода { }.
-
в целом это выглядит более-менее приемлемо, когда вставка где-то между тегов. Но при передаче пропс, или задании атрибутов, необходимость постоянного написания знака «ровно» и фигурных скобок (={ }) кажется избыточной, начинает бесить
-
также при совпадении key и value нельзя указать только key. Что кажется полным бредом, особенно на большом количестве ключей
-
комментарии также задаются с помощью обертки в блок, что неудобно
-
поддерживаются только js-выражения, произвольный код сюда нельзя писать, это может сбивать с толку
-
условный рендеринг прямо в шаблоне выглядит крайне непривлекательно, из-за него невероятно быстро растет вложенность. исключением мб лишь простейшее однострочное условие с помощью &&. Например, {yes && }
<div name={myName} age={myAge}>{content}</div> //это <Card a={a} b={b} c={c} d={d} e={e} /> //вместо этого <Card a b c d e /> {/*Комментарий*/} <div> {yes ? ( <div className="yes"> Yes </div> ) : ( <div className="No"> No </div> )} </div>
-
Работа с ContextAPI Вложенность провайдеров, с которой думаю все знакомы.
Вроде решаемо, но не стоит говорить что этого нет.
<ThemeContext.Provider value={theme}> <SettingsContext.Provider value={settings}> <UserDataContext.Provider value={userData}> <App /> </UserDataContext.Provider> </SettingsContext.Provider> </ThemeContext.Provider>
Это только то, что вспомнилось, а по факту — подобных бесячих моментов намного больше.
Итак, задача была в том, чтобы найти качественный подход задания шаблона компонента.
И по возможности сделать это нативным способом.
Как я говорил ранее — шаблон, реализованный через синтаксический сахар, может быть абсолютно любым — решаете только вы, ваша воображалка. Скажу как есть — я просто не знаю как можно было написать его иначе. После того как я открыл для себя подобный синтаксис, мне стало казаться что шаблон как будто бы вообще всегда задавался именно таким образом. Словно это норма, данность, стандарт. Короче — будь у меня задача реализовать шаблон через сахар — я бы сделал его точно таким.
Впрочем, судить вам.
Не знаю, что еще тут добавить)
Вроде все основное сказал по данной теме.
Надеюсь, вы уже понимаете, что это не последняя статья, это самое начало.
Мне крайне важна обратная связь, вопросы, обсуждения, поддержка и хейт с вашей стороны.
Пожалуйста, напишите что-нибудь от себя снизу, или в личку — будь то послевкусие, замечание, эмоция или мысль. Спасибо!
Оставлю небольшую голосовалку, мб кому-то будет интересно поучаствовать.
Пока преимущественно судим синтаксис и подход.
Лично мне понравились варианты с массивом и с замыканием.
Важно помнить, что все есть компромисс.
ссылка на оригинал статьи https://habr.com/ru/articles/866638/
Добавить комментарий