JSX: антипаттерн или нет?
Довольно часто приходится слышать, что React и особенно JSX-шаблоны — это плохо, и хороший разработчик так не делает. Однако нечасто объясняется, чем именно вредит смешивание верстки и шаблонов в одном файле. И с этим мы попробуем сегодня разобраться.
Подход «каждой технологии свой файл» использовался с начала существования веба, поэтому неудивительно, что слом этого шаблона вызывает отторжение некоторых разработчиков. Но перед тем, как заявлять «нет, мы так делать не будем никогда», будет полезно разобраться истории и понять, почему JSX пользоваться можно, а смешивать скрипты и html — нет.
История
С самого начала Веб состоял только из статических HTML-страниц. Вы открывали адрес, сервер возвращал контент страницы. Верстка, стили, скрипты — все было в общей куче.
Однако по мере усложнения сайтов появилась необходимость переиспользовать стили и скрипты на разных страницах. Со временем подход «CSS и JS отдельно от HTML» стал общей рекомендацией. Особенно при условии, когда HTML генерируется серверным движком, в отличие от CSS и JS файлов, которые меняются только при разработке и отдаются сервером как есть. Разделение контента на статический и динамический позволяет пользователю загружать меньше данных, за счет кеширования, а разработчику удобнее редактировать статические файлы, а не искать куски Javascript в шаблонах используемой CMS.
Со временем стало появляться все больше одностраничных веб-приложений, где основная часть логики сосредоточена на клиентской стороне. На любой URL сервер отдает один и тот же статичный HTML, а Javascript код в браузере пользователя определяет текущий адрес и делает запрос на сервер за нужными данными. При клике на ссылку, страница не перезагружается целиком, а лишь обновляется URL, а Javascript-код делает новый запрос для следующей страницы.
Появление веб-приложений в корне поменяло подход к разработке. Появились новые специальные фреймворки, предназначенные для создания веб-приложений с большим количеством кода и сложной логикой пользовательского интерфейса. Рендеринг данных теперь происходит на клиенте, в ресурсах страницы хранятся статические html-шаблоны, которые натягиваются на данные полученные с сервера.
Наиболее подходящим способом организации такой массы кода стал компонентный подход. Каждый более или менее независимый блок оформляется в отдельный компонент, со своими стилями, шаблоном и Javascript, а потом из этих блоков собирается страница целиком.
Этот подход реализуется большинством современных JS-фреймворков, но с одним отличием: Angular, Ember и Marionette поощряют создание отдельного файла с шаблоном, а React предлагает писать HTML внутри JS. И это становится красной тряпкой для некоторых разработчиков.
Шаблон и компонент
А в чем смысл создания отдельного файла с шаблоном? Часто приводится довод: разделить разные сущности, потому что так правильно. Хотя исторически причиной отделения HTML и JS было разделение статики и динамики, что уже неактуально для рендеринга на клиенте
Итак, насколько же компонент и его шаблон разные? Возьмем фрагмент кода
Линиями показаны взаимосвязи, в которых изменение изменения одного файла затронут и другой. Если бы это была связь между двумя JS-модулями, рефакторинг напрашивается сам собой. Но для view и шаблона так не делают, потому что это неправильно.
Некоторое время назад, в других условиях, смешивание HTML и JS было нежелательным, и до сих пор разработчики слепо следуют тому правилу. Но при рендеринге на клиенте ситуация не такая, кроме того, изначально рекомендовалось вынести JS из HTML, но не HTML из JS.
А насколько это вредит в реальному проекту? Попробуем провести эксперимент и объединить шаблон с JS:
import Marionette from 'backbone.marionette';
import {warningText} from '../constants';
export default Marionette.ItemView.extend({
template(templateData) {
const {title, img_url, count} = templateData;
const isSelected = this.model.isSelected();
const isOverLimit = this.model.get('count') > this.model.get('maxAvailable')
return `
${title}
${isOverLimit ? warningText : ''}
`;
},
events: {
'click btn-less': 'onLessClick',
'click btn-more': 'onMoreClick'
}
});
Благодаря появлению в EcmaScript6 template strings, создание встроенных шаблонов стало приятнее. Подсветка HTML внутри строки так же настраивается. А по сравнению с прошлым примером, кода стало меньше за счет удаления прослойки, которая готовила данные в шаблон.
Так стоит ли так делать в своих проектах с использованием не React? Скорее всего, нет, потому что они не заточены на такой стиль. Но я надеюсь, что теперь стало понятнее, что встраивание HTML в код компонента не так уж плохо и приносит пользу в некоторых случаях.
Комментарии (16)
28 сентября 2016 в 15:33
+1↑
↓
https://www.youtube.com/watch? v=mVVNJKv9esE&t=122028 сентября 2016 в 16:08
0↑
↓
Всё начинается с утверждения что шаблоны это данные, но это только если это строки.
Если же мы используем возможности веб-платформы, то строки превращаются в реальную DOM, которую не мы парсим вручную, а браузер самостоятельно:[[text]]
И это будет более производительно, чем манипуляции со строками.
28 сентября 2016 в 15:42 (комментарий был изменён)
0↑
↓
React компоненты — это и есть View + поведение. Как в PHP принято выносить в шаблоны View какое то поведение включающее в себя незамысловатое ветвление
if
,else
, так собственно и здесь. За одним исключением — добавляется какая то минимальная реакция на события. Важно понимать границы где заканчивается View и начинается бизнес-логика.28 сентября 2016 в 16:01 (комментарий был изменён)
0↑
↓
Импользование template string вместо
элемента значит, что вы игнорируете возможности платформы, которые могут помочь вам распарсить кусок DOM заблаговременно, тем самым увеличив производительность.
А по сравнению с прошлым примером, кода стало меньше за счет удаления прослойки, которая готовила данные в шаблон.
Разница в 8 строк. Но если в первом случае когнитивная нагрузка при работе с файлами небольшая, то с JSX (имхо) когнитивная нагрузка сильно возрастает. К тому же, сама разница в количестве строк по большей части обусловлена самим React. К примеру, в Polymer подобное:
{{#if isOverLimit}} {{warningText}} {{/if}}
я решил кастомным методом if:
[[if(isOverLimit, warningText)]]
При чем я его могу таким образом использовать и для аттрибутов, и для свойств DOM элементов, сравните:
Синтаксис сравним по читаемости к template string, такой же краткий, и при этом в HTML формате, позволяя использовать для обработки возможности платформы.
А ещё можно писать не HTML, а Pug/Jade, где читаемость становится ещё лучше (нужно знать синтаксис, но мне кажется, зная CSS вам не составит труда понять что тут происходит):
div(class$="cart-item [[if(isSelected, 'active', '']]") h3 [[title]] img(src="http://[[img_url]]") div button.btn-less - input(value="[[count]]") button.btn-more(disabled="[[isOverLimit]]") + | [[isOverLimit, warningText, '']]
Если в ход идут миксины, тогда разница становится ещё больше.
В итоге, мне кажется, JSX делает всё только хуже, не позволяя использовать сторонние инструменты вроде Pug/Jade и, по сути, больше решает проблемы синтаксиса самого React.
28 сентября 2016 в 16:16
0↑
↓
Добрый день!
С первым пунктом про template я соглашусь. Поэтому не стоит вставлять html в код фреймворка, который на это не заточен. Но React как раз приспособлен, и разбирает шаблоны во время сборки, превращая их в быстро работающий код.
Теперь давайте про конгитивную нагрузку.
В том-то и дело, что использование дополнительных конструкций, наподобие вашей:
[[if(isOverLimit, warningText)]]
создают даже большую нагрузку. Чтобы прочесть код компонента, нужно знать не только html и javascript, но и вспомогательные методы шаблонизатора. Поэтому при прочих равных я выберу решение, где от меня нужно знать только html и js, то есть React.
Аргумент про Jade, конечно весомый. Тем, кто им пользуется, будет трудно отказаться от него при миграции на React это да. Но как показывает мой опыт, Jade полезен, когда надо писать большие простыни html, а React используют в веб-приложениях, где весь html короткий, поэтому разницы особой не чувствуется.
28 сентября 2016 в 16:29
0↑
↓
где весь html короткий
Любой HTML слишком длинный, чтобы его писать.
Поэтому при прочих равных я выберу решение, где от меня нужно знать только html и js, то есть React.
В React тоже есть синтаксис шаблонизатора, который надо знать, пусть его не так много, как в каком-нибудь handlebars.
28 сентября 2016 в 16:33
0↑
↓
Само собой, JSX продиктован тем, как устроен React, и в контексте React он может быть предпочтительнее.
Опять таки согласен с коротким HTML, в этом случае может быть вполне резонно, но даже сниппет в статье как по мне уже больше оптимального для внедрения в JS размера.Jade сам по себе может сослужить достаточно мощным DSL, без ущерба читаемости.
Вот сравните jade (379 байт) с html (2176 байт) (желательно отформатировать, чтобы увидеть масштаб преобразований).
Там форма с 7 параметрами конфигурации и в Jade это легко понять. Не сложнее устроен и лежащий рядом JS файл, который использует общие с другими формами абстракции. Но HTML как раз получается достаточно объемным.Да, можно создать такие контролы в виде отдельных элементов, но это будут слишком большие накладные расходы для подобной задачи.
Как бы вы решили подобную задачу в React? Элемент загружает данные из API в форму, и при сохранении отправляет те же данные с изменениями обратно.
28 сентября 2016 в 16:03
0↑
↓
Да, это довольно удобно, когда верстка и код этой верстки рядом (но не бизнес-логика, конечно). Но JSX — это новый синтаксис. Нужна поддержка со стороны редакторов, IDE, линтеров, прочих инструментов. Нужно привыкать. Нужно запоминать, что и как делается, что возможно и что невозможно.
И то, что это именно синтаксис, накладывает серьезные ограничения. Поддержка Dart, CoffeeScript, ClosureScript, HAML, Pug/Jade? Даже если есть компилятор, не факт, что IDE/редактор не сойдет с ума.
В этом отношении подход Vue.js гораздо изящнее. *.vue — опциональный формат (и не такой опциональный, как JSX, потому что писать React.createElement ('div') никто в здравом уме не будет), и это просто html. Шаблоны, стили, код — все это пишется на любом языке и при необходимости (разрастание, верстальщик без специальных познаний) легко выносится в отдельные файлы.
При этом Vue.js умеет в Redux с вытекающими плюшками.Простите, увлекся:) По существу: большие шаблоны в коде зло, маленькие иногда можно:)
28 сентября 2016 в 16:14
–1↑
↓
Я возьму Ваш комментарий в копилку IDE-зависимостей, в холиварах vim vs IDE помогает)28 сентября 2016 в 16:28
0↑
↓
Я смотрел на Vue и Polymer, который использует такой же подход html-модулей. Но у меня как-то не зашло.
Когда у вас на первом месте html, то и импорты зависимостей производятся в html. Но javascript-функции с логикой надо импортировать в JS. В результате у меня сложилось ощущение дискомфорта от разнородных импортов.
У JS-first подхода есть свои преимущества. Например то, что стартовой точкой приложения в любом случае будет JS, поэтому логично с него начинать.
28 сентября 2016 в 16:32
0↑
↓
Странно, у меня во Vue как раз JS-first и входная точка именно JS. HTML я не импортирую. Vue отвечает только за вью, простите за каламбур, как и положено:) За Polymer не скажу.
28 сентября 2016 в 16:03 (комментарий был изменён)
–2↑
↓
JSX просто удобен, и плевать на идеологию. мне например удобно делать так:import {dom} from '../inc/dom'; import {WAElement} from '../core/'; export let login = function(dom: any){ return class Login extends WAElement { constructor(){ super(); } connectedCallback() { let holder = this; let form = ; form.addEventListener('submit', function(evt: any){ let formData = new FormData(form); holder.sendMessage('authorize', {formData}); evt.preventDefault(); }); this.appendChild(
{ form }); } }; }(dom);Просто и удобно. Просто удобно. Чьи религиозные чувства это задевает?
28 сентября 2016 в 16:04
0↑
↓
у меня картинка не загружается, не могу ничего сказать
28 сентября 2016 в 16:10
0↑
↓
обновил
28 сентября 2016 в 16:15
0↑
↓
this.appendChild(
{ form });
Вы же основную нямочку — удобный one-way data flow не используете28 сентября 2016 в 16:23
0↑
↓
Если что — это не React) Это объявление кастомного элемента. Я просто показал как удобно когда у тебя по ходу кода верстка в нужный момент становится переменной. Так то в реале весь смак в dom функции — как запилишь так и будет (и декларативный биндинг и state и события)