Open nin-jin opened 3 years ago
Про комменты в jsx надо будет как-то проверить. В каких-то случаях работает двойной слеш, в каких-то фигурные скобки нужны, еще не разобрался) Но добавляются убираются комменты в большенстве случаев без проблем, выделение текста + хоткей
Периодически встречается проблема, когда в jsx куча тегов. Случайно удаляешь/забываешь закрывающий тег, или слеш в нем или что-нибудь подобное. Сложно найти проблемное место
Периодически встречается проблема, когда в jsx куча тегов. Случайно удаляешь/забываешь закрывающий тег, или слеш в нем или что-нибудь подобное. Сложно найти проблемное место
А есть какой-то пример кода?
Периодически встречается проблема, когда в jsx куча тегов. Случайно удаляешь/забываешь закрывающий тег, или слеш в нем или что-нибудь подобное. Сложно найти проблемное место
А есть какой-то пример кода?
Шикарный пример. Хотя, найти место ошибки сложно во многом из-за форматирования. Если добавить пустых строк между блоками и использовать 4 пробела вместо 2, то будет более заметно.
Шикарный пример. Хотя, найти место ошибки сложно во многом из-за форматирования. Если добавить пустых строк между блоками и использовать 4 пробела вместо 2, то будет более заметно.
ага
https://page.hyoo.ru/#!=xl437w_w1mpfo
Здравствуйте, меня зовут Дмитрий Карловский и я.. люблю рвать шаблоны. А во фронтенде как раз крайне много заблуждений вокруг шаблонизации. Так что давайте порвём их на лоскуты снизу вверх и справа налево.
Далее мы разберём что такое шаблоны. Их ключевые достоинства и фатальные недостатки. Зачем они нужны и почему не нужны. Сформируем представление о правильном решении и проедемся катком по популярным. Так что полная гамма чувств нам обеспечена.
Прошу к столу..
А что такое шаблон?
Казалось бы, тут всё очевидно: это способ генерации кода на целевом языке. Однако, всё не так просто. Чтобы понять ключевое свойство шаблона, давайте рассмотрим пару примеров..
Это шаблон:
А это уже нет:
Оба кода делают одно и то же, но почему одно является шаблоном, а другое - нет? Всё дело в том, какой язык является первичным в коде, а какой является опциональной добавкой к нему. Шаблон предполагает написание кода сразу на целевом языке, вкрапляя в него специальные управляющие конструкции, которые тем не менее синтаксически согласованы с целевым языком.
Яркий пример синтаксически согласованных управляющих конструкций можно наблюдать в XSLT:
А вот такой код, не смотря на использование шаблонов в 1 и 3 строке, в целом шаблоном всё же не является, так как чтобы понять, каков будет результат, нужно мысленно корректно исполнить JSX-код:
Грубо говоря, если взять парсер целевого языка, то он справится и с парсингом шаблона, просто проигнорировав управляющие шаблонные конструкции, никак их не обрабатывая. И, соответственно, человек тоже может буквально видеть получающийся из шаблона результат, а не собирать его в уме.
Как видно в последнем примере, код на JSX может быть шаблоном, а может им и не быть. И как правило шаблоном он всё же не является, не смотря на синтаксическое подражание HTML.
А необходим ли HTML?
Во фронтенде целевым языком для шаблонов как правило является HTML. А HTML является не более, чем сериализованным представлением DOM дерева. И в прошлом именно HTML был языком коммуникации между клиентом и сервером. Поэтому серверу нужно было генерировать именно его.
Однако, в современном вебе клиент и сервер больше не обмениваются HTML, предпочитая JSON, ProtoBuf и другие более эффективные форматы. Более того, теперь клиент уже сам формирует DOM напрямую, через JS-API, минуя HTML представление. А это значит, что в качестве целевого языка описания DOM может быть использован не только HTML, но и иные форматы сериализации DOM.
Например, HAML:
Или xml.tree:
Или даже JSON. Без примера, ибо слишком уж он развесистый получается.
В этом свете использование HTML-шаблонизации является скорее данью традиции, чем реальной необходимостью:
А достаточно ли HTML?
Мощности HTML хватает лишь для описания DOM. Но современная разработка предполагает компонентную декомпозицию. А где декомпозиция - там и композиция. То есть нам необходим инструмент для создания экземпляров компонент, их настройки и соединения друг с другом реактивными связями разных направлений.
Тут не то что HTML, а даже DOM уже катастрофически не хватает, что неизбежно порождает чудовищ. Например, вам нужно вставить несколько компонент и провязать их состояния друг с другом.
Возьмём ангуляровский "шаблон":
Весьма похоже на HTML, но только это не HTML, чтобы там ни говорили Angular-евангелисты. DOM (и как следствие HTML) поддерживают лишь задание строк в качестве атрибутов. А для компонент нужны не только строки, но и другие типы данных: числа, объекты и даже другие компоненты. И их надо не только хардкодить в шаблоне, но и брать из свойств, класть в свойства, а то и вообще обеспечивать двустороннее связывание.
И тут начинаются кастомные расширения HTML. Каждый атрибут в примере выше имеет свою семантику, но синтаксически выглядят они все одинаково:
#input
- это локальный идентификатор, для доступа через TS.class="editable"
- это имя класса для привязки стилей через CSS.side="left"
- это имя слота, куда этот элемент будет помещён.[(checked)]="editable"
- это двустороннее связывание свойств вложенного и внешнего компонентов.[enabled]="editable"
- это уже одностороннее.text="{{text}}"
- а это тоже самое.placeholer="Markdown content.."
- это какой-то захардкоженный текст.i18n-placeholder="Showed when input is empty"
- а это, внезапно, указание, что атрибутplaceholder
подлежит переводу, и пояснение переводчику.*ngIf="text"
- это же вообще к компоненту не относится, а регулирует будет ли компонент рендериться в родителе.Все 4 компонента лежат вперемешку, не смотря на то, что часть из них относится к левому слоту, а часть к правому. То есть это мало того, что не HTML, так это ещё и вовсе не шаблон. Это - язык для компоновки компонент, мимикрирующий под HTML. Из-за этой мимикрии он преисполнен горой сомнительных решений, осложняющих изучение, разработку, чтение и поддержку как самого прикладного кода, так и инструментария, превращающего эти "шаблоны" во что-то, что может исполнить браузер, чтобы показать интерфейс.
Но чем же HTML хорош?
Ключевое достоинство HTML - его декларативность. Вопреки расхожему мнению, декларативные языки описывают на самом деле не "результат", а некоторую семантическую структуру. И эта структура может быть использована для программного анализа с получением множества разных "результатов" в зависимости от потребностей.
Мы можем взять HTML и нарисовать на экране красивый плоский интерфейс. Можем в VR показать объёмный интерфейс, который можно потрогать. Можем реализовать голосовой интерфейс для не зрячих. Можем распечатать в виде книги. Можем собрать все заголовки для формирования оглавления и все термины для тезауруса. Можем собрать все ссылки и уведомить сайты, куда они ведут, о том, откуда на них ссылаются. Можем отправить уведомление всем упомянутым пользователям. И много чего ещё.
Но всё это многообразие возможностей крайне затруднено, а то и попросту не возможно при написании императивного кода, который описывает конкретные действия по получению одного конкретного результата. Что бы там ни говорили адепты функционального программирования, но оно ни в коем разе не является декларативным. ФП - это просто императивное программирование на иммутабельных структурах, описывающее конкретные действия, по трансформации заданных входных параметров в заданные выходные. Всё, что можно сделать с чистой функцией - это её исполнить. А всё, что может дать нам анализ её кода - это "ну, там вызываются какие-то функции, вот их сигнатуры".
Простой пример императивного функционального кода:
А вот пример настоящего декларативного кода в модели RDF:
Это логические триплеты. Благодаря нормализованному представлению их очень просто парсить и анализировать.
Но вернёмся к нашим шаблонам. Возьмём популярный сейчас JSX, который мимикрирует не только под HTML, но и под JS, и даже под ФП, при этом ничем из упомянутого не являясь:
Закроем пока глаза на объёмность и сложность кода. Давайте подумаем, что мы можем получить из него без исполнения..
Нет. Даже если мы распарсим весь код в AST, найдём все вызовы функции
l10n
и понадеемся, что передан ей всегда будет лишь строковый литерал, а не какое-нибудь выражение, нам всё равно неоткуда взять персистентную информацию для формирования ключей, чтобы они не менялись при каждом изменении вёрстки.Нет. И не верьте адептам Реакта, утверждающим, что двустороннего связывания там нет, и что оно вообще не нужно. Оно и нужно, и есть, хоть и реализуется через костыли с парными пропсами вида
checked={ editable } onToggle={ setEditable }
.Нет. Разве что очень сильно заморочиться и реализовать data-flow анализ. И то он будет справляться далеко не со всем многообразием возможного кода.
Нет. Так как имена классов собираются из строк в прикладном коде.
Продолжать можно долго, но суть уже должна быть ясна: императивные языки содержат слишком мало информации о высокоуровневых абстракциях и слишком много низкоуровневого шума. Это капитально осложняет программный анализ.
А возможна ли декларативность?
Чем меньше язык позволяет вольностей, тем проще его программно анализировать. Но обратная сторона этого - снижение гибкости. Чтобы достичь максимальной гибкости, нужен язык общего назначения. Во фронтенде таким языком обычно является JS и другие языки, в него компилирующиеся. Поэтому очень велик соблазн либо собирать компоненты сразу в нём, либо засовывать в "шаблоны" вкрапления на JS. Разумеется ни о какой декларативности в этом случае уже говорить не приходится.
Чтобы добиться гибкости, но не потерять декларативность, нужно разбивать код компонента на 2 части:
Именно поэтому надо отделять "шаблоны" от "скриптов", а не потому, что одно как бы бизнес-логика, а другое - её отображение.
Для примера возьмём язык view.tree, используемый в $mol:
Он мало того, что в несколько раз меньше эквивалентного JSX кода, так из него ещё и легко вычленять локализацию, собирать статистику использования, проверять переопределения стилей и много чего ещё. Можно даже построить конфигуратор позволяющий взять произвольное существующее приложение и, не написав ни строчки кода, собрать из него что-то новое. Ну а когда декларативных возможностей не хватает - всегда можно написать комплексную логику в отдельном скрипте, используя всю мощь языка общего назначения:
К сожалению, с подачи Фейсбука вместо чего-то такого мы имеем сейчас повсеместный императивный JSX и кучу костыльных проектов, пытающихся его программно анализировать. А в тех фреймворках, где есть отделение скриптов от шаблонов, вместо шаблонов мы видим императивный недо-DSL мимикрирующий под HTML, что приверженцы Реакта справедливо считают бессмысленным.
Что опять за наезды на JSX?
Раз уж мы уже наехали, то не будем останавливаться и проедемся до конца, по всем недостаткам дизайна JSX помимо недекларативности...
Push семантика
Вложенное поддерево вычисляется безусловно, даже если оно завёрнуто в компонент, который показывает своё содержимое лишь иногда. Решаться это могло бы через передачу замыкания вместо VDOM:
Но, как всегда, есть "но":
В результате реальный код становится куда более страшным:
Сравнение push и pull семантики - это отдельная большая тема. Поэтому вкратце обрисую преимущества pull: она позволяет просто и эффективно реализовать ленивые вычисления, рендеринг, загрузку и вообще экономить ресурсы. У push семантики же с этим всем серьёзные проблемы.
Неэффективность
JSX компилируется в крайне не удачный JS код, который из-за своей мегаморфности крайне сложно поддаётся оптимизации JIT-компилятором:
Сверху - то, каким он мог бы быть быстрым при мономорфности. А снизу - суровая реальность в FireFox.
Слабые возможности связывания
JSX заточен под проталкивание значений. Но любые другие связывания - это боль. Хочешь передать замыкание - изволь завернуть его в useCallback и описать отдельным массивом всё, от чего оно зависит (и счастливой отладки, если что-то забудешь):
Самое забавное, что useCallback тут должен спасать от лишних рендеров, но так как замыкание зависит от
info
, то его приходится указывать в зависимостях, что приводит к обновлению замыкания при каждом изменении данных, даже еслиinfo.name
фактически не поменялся. А следовательно рендерInput
будет происходить при каждом изменении любого поляinfo
.Про двустороннее связывание я ранее уже упомянул, что оно делается через костыли с прокидыванием пары свойств. Это мало того, что многословно, так ещё и легко разъезжается, приводя ко крепким обнимашкам с дебаггером.
Неконсистентность
Из-за подражания HTML константные строки прокидываются одним синтаксисом, а все остальные типы и неконстантные строки — другим:
Дочерние компоненты могут быть переданы двумя совсем разными способами:
А уж сколько есть вариантов условного рендеринга - один хуже другого.
Всё это - следствие попытки усидеть сразу на двух стульях: HTML и JS.
Костыли для комментариев
Набирать и читать их просто неудобно:
Волшебные атрибуты
JSX никак не форсирует простановку уникальных идентификаторов вложенным компонентам. А потребность получать ссылку на конкретный DOM элемент есть. Поэтому в вёрстке появляются волшебные атрибуты:
По той же причине, функция рендеринга не может отличить перемещённый элемент от нового, что приводит к лишнему рендерингу. Чтобы этого избежать вводится ещё один волшебный атрибут:
Правда, при переносе в другого родителя, не спасает и он.
И беда даже не в том, что эти атрибуты вообще существуют, а в том, что синтаксически они неотличимы от любых других.
Много мусора в вёрстке
Мало нам закрывающих тегов из HTML. Давайте добавим ещё и лесенку из контекстов:
Отсутствие ограничений
Отсутствие синтаксических ограничений мало того, что затрудняет программный анализ, так ещё и неизбежно приводит к ухудшению качества кода. Даже если код пишет не говнокодер, порой бывает, что некогда или лень делать правильно, а срезать угол ничего не мешает, и выглядит это вроде бы проще. Но разбитое окно тут, покосившееся там, и вот у нас уже не чётко структурированный код, а лютая тормозящая лапша, где всё связано со всем, лежит вперемешку, но концы с концами фиг сведёшь:
В цикле хреначим немемоизированное замыкание, код которого тут же по среди "вёрстки" и всё, что он делает, - меняет формат события. Не, ну а что, подумаешь ререндер всех элементов на каждый чих. Процессоры сейчас быстрые, данных мало, переход к определению работает везде..
А view.tree прям такой идеальный?
Нет, конечно, педаль в пол, давим и его..
Слабая интеграция с IDE
Microsoft добавила поддержку JSX прямо в компилятор TypeScript, что дало не только хороший тайпчек, но и интеграцию в тайпскриптовый Language Server. А это значит отличную интеграцию не только с их же VSCode, но и с другими IDE.
К сожалению, Microsoft не озаботилась простотой интеграции сторонних языков с TS.
view.tree
, конечно, компилируется в TS, что даёт тайпчек при сборке, но IDE этого всего не видит. Соответственно, не работают подсказки, рефакторинги и тп. Хорошо хоть подсветка синтаксиса есть.Неявная типизация
Во имя простоты и наглядности в языке почти нет возможности задать тип явно. Типы выводятся из значений по умолчанию, что не всегда даёт ожидаемый результат.
Например, значение
null
имеет типany
:Как и аргументы методов:
Но это всё компромиссы конкретного языка, а не декларативного подхода в целом. Ведь можно же было сделать, например, так:
Что тут ещё сказать?
Фух, покатались на славу. Пришло время остановиться и перевести дух, поразмыслить над смыслом бытия, и двинуться дальше..
В выступлении "Tree - единый AST чтобы править всеми" можно познакомиться с форматом
tree
. В выступлении "Свой язык с поддержкой sourcemaps за полчаса" с его пайплайном. А в выступлении "$mol - лучшее средство от геморроя" можно найти краткое введение конкретно в языкview.tree
.Заглядывайте в чат "Разработка языков программирования" всё это обсудить. Или даже в чат "$mol: Разработка" если заинтересовал фреймворк $mol.
Ну и, конечно, предлагайте в комментариях свои идеи как упростить жизнь разработчика интерфейсов, не водружая инвалидные коляски поверх многоэтажных костылей.