- PVSM.RU - https://www.pvsm.ru -
Уже какое то время использую/разрабатываю библиотеку MaskJS [1]. Вначале использовал её только как движок для шаблонов, а со временем, она полностью заменила HTML. В статье расскажу какими преимуществами обладает компонентный подход в разработке приложений и данная реализация в частности. Если выделить по-пунктам, то мы получим приблизительно такой список:
Более подробно о самой библиотеке и примеры можете посмотреть здесь — libjs/Mask [2], a исходники тут — github/Mask [3]
Сразу о ней, так как всегда первым вопросом, после длительного описания всех преимуществ, я слышу — «Ну хорошо, это у тебя реализовано через свой DOM Builder и все это вкусно, но наверное производительность страдает?!». Поэтому поспешу вас уверить, что данная библиотека даже быстрее, чем то, что вы сейчас используете. Извините за попсовое заявление, возможно я и ошибаюсь, но давайте углубимся.
Разберем тест, который приводил уже ранее, jsperf [4].
Замечу, что для меня, как для своих проектов, так и для работы важны Webkits JavaScriptCore и V8. Как видно из тестов — компиляция и рендеринг опережает другие решения во многих браузерах.
Mustache здесь немного в преимуществе, так как он кэширует скомпилированный шаблон.
Из шаблона мы строим JSON дерево вида {tagName:'div',attr:{...},nodes:[...]}
. Моей первой идеей было подготавливать шаблоны для клиента — для дальнейшего var template = JSON.parse(serializedJsonDom)
, но когда я провел тесты, oказалось, что mask.compile в целевых движках быстрее JSON.parse — jsperf [5], а в других браузерах не сильно уступает последнему. Так что прекомпиляция отпадает уже из-за ненадобности.
Получив JSON дерево, мы строим DocumentFragment, который потом вставляем в «живой» DOM. Из теста [6] видно, что и здесь производительность на уровне. Я оставил чистый .appendChild(documentFragment)
, чтобы показать, что вся соль в создании DocumentFragment. И что опять интересно, .innerHTML здесь тоже уступает в производительности (Chrome).
Важным моментом также является, что мы изначально рендерим все в один заход. Разберем пример с jQuery виджетами. Как обычно все происходит:
<div id='myWidget'></div>
$('#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 (Исходники [7], Документация [8])
Учитывая, что у нас древовидный 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 [9]
Их также можно назвать декораторами. Здесь мы изменяем уже родителей, и не шаблон, так как он уже прорендерин, а непосредственно 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 [10]
Извините за названия параграфа — название «уж больно сильно» нравится. Эх звучит то как… Не так мне сам паттерн нравится, как его название — чувствуется мощь. Простите за лирику, не удержался. Возвращаясь к нашей теме, можно сказать, что произвольные теги открывают нам горизонты новых или хорошо забытых старых паттернов.
В планах реализовать, например, фабрику редакторов свойств:
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. В странице имеем тег скрипт с типом «mask/template» содержимое которого рендерим в DOM. Многие готовые библиотеки для удобности стараюсь оборачивать в компонент, как на пример — TimePicker [11]
Прошу прощение, если кого-то обидел или где-то ошибся. Хотя в Chrome есть отличная проверка на орфографию, так с пунктуацией он не помогает, а наверное «ой как надо».
Удачного дня!
Автор: tenbits
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/19167
Ссылки в тексте:
[1] MaskJS: http://habrahabr.ru/post/149509/
[2] libjs/Mask: http://libjs.it/#/mask
[3] github/Mask: http://github.com/tenbits/MaskJS
[4] jsperf: http://jsperf.com/javascript-template-engine-compare/2
[5] jsperf: http://jsperf.com/maskjs-vs-json
[6] теста: http://jsperf.com/maskjs-vs-innerhtml-vs-documentfragment
[7] Исходники: http://github.com/tenbits/CompoJS
[8] Документация: http://libjs.it/#/compo
[9] layout.js: https://github.com/tenbits/Compos/blob/master/layout/lib/layout.js
[10] github: https://github.com/tenbits/Compos/blob/master/binding/lib/binding.js#L20
[11] TimePicker: https://github.com/tenbits/Compos/blob/master/timePicker/lib/timePicker.js
[12] Источник: http://habrahabr.ru/post/157383/
Нажмите здесь для печати.