Уже какое то время использую/разрабатываю библиотеку MaskJS. Вначале использовал её только как движок для шаблонов, а со временем, она полностью заменила HTML. В статье расскажу какими преимуществами обладает компонентный подход в разработке приложений и данная реализация в частности. Если выделить по-пунктам, то мы получим приблизительно такой список:
- Скорость
- Обработчики тегов
- Пре- и Пост-процессоры
- IoC
- Изоляция/Декомпозиция
- Разметка — Модель — Код — Стили
Более подробно о самой библиотеке и примеры можете посмотреть здесь — libjs/Mask, a исходники тут — github/Mask
§ Скорость
-
Производительность
Сразу о ней, так как всегда первым вопросом, после длительного описания всех преимуществ, я слышу — «Ну хорошо, это у тебя реализовано через свой DOM Builder и все это вкусно, но наверное производительность страдает?!». Поэтому поспешу вас уверить, что данная библиотека даже быстрее, чем то, что вы сейчас используете. Извините за попсовое заявление, возможно я и ошибаюсь, но давайте углубимся.
Разберем тест, который приводил уже ранее, jsperf.
Замечу, что для меня, как для своих проектов, так и для работы важны Webkits JavaScriptCore и V8. Как видно из тестов — компиляция и рендеринг опережает другие решения во многих браузерах.
Mustache здесь немного в преимуществе, так как он кэширует скомпилированный шаблон.Компиляция/Парсинг шаблона. Первый выигрыш
Из шаблона мы строим JSON дерево вида
{tagName:'div',attr:{...},nodes:[...]}
. Моей первой идеей было подготавливать шаблоны для клиента — для дальнейшегоvar template = JSON.parse(serializedJsonDom)
, но когда я провел тесты, oказалось, что mask.compile в целевых движках быстрее JSON.parse — jsperf, а в других браузерах не сильно уступает последнему. Так что прекомпиляция отпадает уже из-за ненадобности.Построение DOM. Второй выигрыш
Получив JSON дерево, мы строим DocumentFragment, который потом вставляем в «живой» DOM. Из теста видно, что и здесь производительность на уровне. Я оставил чистый
.appendChild(documentFragment)
, чтобы показать, что вся соль в создании DocumentFragment. И что опять интересно, .innerHTML здесь тоже уступает в производительности (Chrome).Важным моментом также является, что мы изначально рендерим все в один заход. Разберем пример с jQuery виджетами. Как обычно все происходит:
- в макете указываем контейнер
<div id='myWidget'></div>
- в javascript-e
$('#myWidget').myWidgetInit(config);
Огромным недостатком данного подхода является то, что мы создаем виджет в уже готовый DOM элемент. А как известно менять «живой» DOM вещь относительно дорогая. И при том мы должны признать, что часто у нас не один виджет в приложении.
Вот собственно и получаем в итоге очень быстрый DOM Builder.
- в макете указываем контейнер
-
Скорость разработки
Более подробно это мы рассмотрим в следующих параграфах, собственно ради этого и затевалась эта статья. Здесь же просто отмечу, что произвольные теги существенно повышают скорость и удобство разработки. Рассмотрим данный макет:
header > menuBar { li target='item1' > 'Item1 Title' li target='item2' > 'Item2 Title' } viewsManager { view#item1 > carousel { img src='img1.png'; img src='img2.png'; } view#item2 > scroller { 'About Content' } }
Если у нас компоненты
menuBar, viewsManager, scroller, carousel и slider
уже готовы, то нам даже ничего в javascript-e дополнительно писать не надо, что бы меню и вьюшки переключались, что бы работал скролл, что бы картинки крутились. Разве не прелесть? И разве не для этого существует макет? Конечно многие виджеты умеют также самo-инициализироваться, но в основном это реализовано через Dom Ready Event и поиск/замену нужных тегов — все это дополнительный «overhead». Здесь же все проходит через этот самый DOM Builder. Вы можете вставить такой макет в любое время и он будет работать.
§ Обработчики тэгов
Это и есть наши компоненты. Builder встретив тег обработчика, проинициализирует объект класса, и передаст контекст рендеринга в этот обработчик — никакой магии. Если же никто под тегом не зарегистрирован — создаст элемент сам. Для более полной и удобной работы с компонентами имеется также небольшой «абстрактный и не только» класс Compo (Исходники, Документация)
§ Препроцессоры
Учитывая, что у нас древовидный render flow, мы получаем мощный инструмент воздействие на рендеринг и макет в целом. Препроцессорами я называю компоненты которые изменяют нижний макет. Таким образом мы как бы внедряемся нашим компонентом в макет — правим по усмотрению нижний шаблон или подменяем входные данные модели и продолжаем рендерить. Это очень удобный паттерн для построение разных макетов (layouts). Вот пример с MasterPages (asp.net привет):
layout:master#twoColumnLayout {
div.wrap {
div.layoutLeft > placeholder#left;
div.layoutLeft > placeholder#right;
}
}
layout:view master='twoColumnLayout' {
left { /** content */ }
right { /** content */ }
}
Компонент #twoColumnLayout должен конечно дополнительно подгрузить свои стили, но об этом в другой раз.
Это очень простой пример, но здесь видно, как элементарно мы можем подставлять разные шаблоны нашему представлению. Пример реализации — layout.js
§ Постпроцессоры
Их также можно назвать декораторами. Здесь мы изменяем уже родителей, и не шаблон, так как он уже прорендерин, а непосредственно HTMLElement
(мы помним, что работаем с DocumentFragment, так что все изменения безболезненны). Такой подход также является мощным помощником при проектировании. На примере биндингов:
div {
bind value='name';
bind value='status' attr='class';
}
Здесь постпроцессор свяжет модель с элементом div
.
var person = { name: "Alex", status: "happy" }
container.appendChild(mask.renderDom(template, person));
setTimeout(function(){ person.status = 'busy' ; person.name="Anonym" }, 1000);
Код думаю понятный. А реализацию можно посмотреть здесь — github
§ Inversion of Control (Dependency Injection)
Извините за названия параграфа — название «уж больно сильно» нравится. Эх звучит то как… Не так мне сам паттерн нравится, как его название — чувствуется мощь. Простите за лирику, не удержался. Возвращаясь к нашей теме, можно сказать, что произвольные теги открывают нам горизонты новых или хорошо забытых старых паттернов.
В планах реализовать, например, фабрику редакторов свойств:
form {
propertyEditor value='name';
propertyEditor value='birthday';
propertyEditor value='age';
}
А реализация будет выглядеть примерно так:
mask.registerHandler('propertyEditor', Class({
render: function(values, container, cntx){
var value = Object.getProperty(values, this.attr.value),
template;
switch(Object.typeOf(value)){
case 'string':
template = Object.format('input type="text" value="#{%1}" > bind value="#{%1}" prop="value";',this.attr.value);
break;
case 'datetime':
template = Object.format('datePicker date="%1";', value.toString());
break;
/** и так далее */
}
mask.renderDom(template, values, container, cntx);
});
}}
Важным моментом здесь является то, что мы можем внедрятся в шаблон переопределив любой из тегов, даже тот же «DIV» — это открывает нам неограниченные горизонты для манипуляции с представлением, тестирования и прочего, что на ум придет.
§ Изоляция
Произвольные теги, или вернее их обработчики, поддаются хорошей изоляции. Как это и должно быть при блочной композиции, любой блок может быть выделен в отдельный проект (декомпозиция), разработан, протестирован и подключен обратно. Например, если из примера выше у нас нет компоненты «карусель» — мы закрываем основной проект, разрабатываем компонент который вращает своих детей (картинки), возвращаемся в основной проект и подключаем его. Наш загрузчик должен уметь подгружать все нужные компоненту ресурсы, такие как вспомогательные картинки и стили. О загрузчике я уже рассказывал, и потом хочу еще пару слов сказать, так как появилось несколько интересных идей. Но это в следующей статье.
Изоляция важна не только в процессе разработки, но и в архитектуре приложения. Это достигается через событийную модель.
§ Разметка — Модель — Код — Стили
Я очень люблю структуру вэб программирования (разметка — код — стили), поэтому старался придерживаться ее в MaskJS — максимально убрать логику из разметки сконцентрировавшись на макете и данных. Посмотрите на другие шаблонизаторы — циклы, условия, выражения и прочее. Мне кажется, намного приятнее иметь такoe (кода нет, есть только макет):
ul > list value='users' > li > '#{name}'
Тег(компонент) list;
возьмет массив "users"
из модели переданную в шаблон, и продублирует свой шаблон li > "#{name}"
N(users.length) раз. Все остальное можно также через компоненты реализовать, или воспользоватся уже готовым решением. Таким образом макет — макетом, код — кодом, а стили — стилями. Все это должно переплетаться по-минимуму. Конечно — это все субъективно, и возможно я в корне не прав, а вы знаете все намного лучше.
§ Уход от HTML
В силу всего выше сказанного, я полностью отказался от написания обычного HTML. В странице имеем тег скрипт с типом «mask/template» содержимое которого рендерим в DOM. Многие готовые библиотеки для удобности стараюсь оборачивать в компонент, как на пример — TimePicker
Прошу прощение, если кого-то обидел или где-то ошибся. Хотя в Chrome есть отличная проверка на орфографию, так с пунктуацией он не помогает, а наверное «ой как надо».
Удачного дня!
Автор: tenbits