Преимущества произвольных тегов, или как я отказался от HTML разметки

в 10:08, , рубрики: html, javascript, Веб-разработка

Преимущества произвольных тегов, или как я отказался от HTML разметкиУже какое то время использую/разрабатываю библиотеку MaskJS. Вначале использовал её только как движок для шаблонов, а со временем, она полностью заменила HTML. В статье расскажу какими преимуществами обладает компонентный подход в разработке приложений и данная реализация в частности. Если выделить по-пунктам, то мы получим приблизительно такой список:

  • Скорость
  • Обработчики тегов
  • Пре- и Пост-процессоры
  • IoC
  • Изоляция/Декомпозиция
  • Разметка — Модель — Код — Стили

Более подробно о самой библиотеке и примеры можете посмотреть здесь — libjs/Mask, a исходники тут — github/Mask

§ Скорость

  1. Производительность

    Сразу о ней, так как всегда первым вопросом, после длительного описания всех преимуществ, я слышу — «Ну хорошо, это у тебя реализовано через свой 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.

  2. Скорость разработки

    Более подробно это мы рассмотрим в следующих параграфах, собственно ради этого и затевалась эта статья. Здесь же просто отмечу, что произвольные теги существенно повышают скорость и удобство разработки. Рассмотрим данный макет:

    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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js