- PVSM.RU - https://www.pvsm.ru -
Разрабатывая MaskJS вот уже больше полугода, он превратился из DOM шаблонизатора в очень мощный, но при этом производительный веб фреймворк. В статье познакомлю вас с возможно интересными подходами к разработки. Уверен, будет интересно почитать о использовании сигналов и слотов вместо DOM событий. И как компоненты делают нашу жизнь проще. Маска легко интегрируется в уже готовый проект, и даже может быть использована вместе с любым другим фреймворком. Основным же отличием наверное является render flow
, где в процессе поэтапно создается Document Fragment / контроллеры / «биндинги». Собственно всю гибкость даже сложно передать, но я попробую, и приглашаю под кат.
MaskJS@GitHub [1]
Небольшой todo пример [2] для разогрева. mask-fiddle лучше всего работает на webkit-е
На случай, если кто-то не знает, в шаблонах используется синтаксис схожий с css/less/sass. За последнее время были исправлены различные баги, поэтому работа движка должна быть теперь стабильной.
p {
div#info.dark > 'Single Child'
button data-user='123' style='cursor:pointer;' > span > 'Submit'
input type=hidden value=x;
}
Как видно, мы убрали "<>" из тэгов и убрали закрывающие тэги, а вместо этого блоки выделяются привычными "{}" скобками. (Для простоты блок с одним ребёнком похож не селектор с ">" переходом, а вовсе без детей — закрывается точкой с запятой). Текст же помещается в литералы, как в javascript. Вот таким не хитрым преобразованием, мы легко сконцентрировали разметку на структуре — что собственно более востребовано в архитектуре приложения, чем избыточность html, который нацелен на разметку текста. Многих приверженцев html это удивляет, но давайте посмотрим правде в глаза — мы, и наверное также как и вы, разрабатываем приложения с множественной локализацией. Текста в представлениях у нас нету — только ключи к json с локализацией, зачем тогда спрашивается синтаксис гипертекстовой разметки?
А в отличии от шаблонов основанных на отступах, маска легко минифицируется и занимает минимум места.
Это является основным приоритетом в разработке MaskJS — что бы предлагать максимальную производительность на мобильных устройствах. Удалось добиться скорости более-менее сопоставимой с html, a в случае с webkit движком — даже увеличить, особенно это относится к мобильным платформам. И не потому что html parsing в webkit-ах медленный, а потому, что str.charCodeAt и document.createElement очень быстрые ). И главное, накладные расходы на контроллеры / интерполяцию / dom события / data bindings в архитектуре MaskJS минимальны. В результате, нам больше не нужно компилировать шаблоны, а это уже большой плюс к наслаждению от разработки. Если интересно, несколько ссылок на jsperf.com найдете в readme на гитхабе [3].
MaskJS — довольно расширяемая система. Мы можем определять контроллеры к любым тегам и создавать новые, собственно вся иерархия контроллеров(HMVC) строится на этой фиче. Можем определять обработчики к любым атрибутам. Также мы можем определить утилиты, которые, при интерполяции модели в шаблоне, будут трансформировать или переопределять данные. И напомню, что маска на клиенте рендерится непосредственно в DocumentFragment, поэтому мы всегда работаем с DOM элементами.
Все контроллеры создаются через подобие IoC контейнеров, а если вы «в теме», то сами понимаете, как легко будет их переопределять или имитировать («мокать»).
У вас есть jQuery widget (или аналог) и вы устали каждый раз его инициализировать после вставки в дом?.. Например, пришел ответ от серверa, используя любой другой шаблонизатор — вы создали представление, вставили в DOM, а потом ещё прошлись по нужным элементам и проинициализировали widget-ы. С MaskJS вы создаете Тэг-Обёртку над вашим виджетом, и маска сделает всё за вас:
mask.registerHandler(':timer', Compo({
// пример шаблона для виджета
template: '.cotainer > .someInnerPanel',
slots: {
domInsert: function(){
// этот слот будет вызван после вставки в "живой дом",
// на случай, если нужно производить дом-зависимые расчёты
}
}
onRenderEnd: function(){
this.$.mySuperTimer({ timespan: this.attr.timespan << 0 });
/**
Важный момент - значения атрибутов это строки, если только значение не было
интерполировано с модели / контроллера, тогда значение может быть как любым значением,
так и любым экземпляром класса.
В данном примере использую левый сдвиг для преобразования строк в целое (int) число, а undefined в ноль.
*/
}
});
// = шаблон
// ...
:timer timespan="5000";
Теперь, можно хоть сколько угодно этот тэг использовать в шаблоне, а вот инициализировать больше не нужно. Небольшой пример создание таймеров:
$.getJSON(url).done(function(collection){
jmask("ul > % each=timers > li > :timer timespan='~[timespan]'; ", collection).appendTo(document.body);
});
Существует множество разных архитектурных решений, но у всех есть общая цель — уменьшить связи и зависимости. В MaskJS основной акцент делается на V(View) из MVC, и мы пытаемся абстрагироваться от Модели. Маске не важно, как выглядит ваш Business Layer и откуда он «берётся». А это значит, что все классы, данные и любая бизнес логика независима от представления и контроллеров — и не только архитектурно, но и от MaskJS библиотеки в целом. Модель может быть как Data Centric (прим. — json service response), так и комплексным Domain Model. Но в любом случае она отделена и тем самым проста для разработки и тестировании.
Далее приведу маленькие примеры разных сценариев MVC, кое что будет утрировано — так что не судите строго, делаю это только в целях лучшей наглядности.
mask.render(" div > 'A' ");
var model = { letter: 'A' }
):
mask.render(" div > '~[letter]' ", model)
— получаем (Data)Model / View
mask.render(" div > '~[bind: letter]' ", model);
— вот уже и Model / View / ViewModel
mask.registerHandler(':myModelAdapter', {
renderStart: function(model){
_extendModelFromLocalStorage(model);
}
});
mask.render(" :myModelAdapter > div > '~[letter]' ", model);
mask.registerHandler(':myPresenter', Compo({
onRenderStart: function(model){
this.letter = _handle(model);
this.model = this;
}
});
mask.render(" :myPresenter > div > '~[letter]' ", model);
mask.registerHandler(':letterChanger', Compo({
events: {
'click: div' : function(event){
this.model.letter = 'B';
// если не испольовать биндинги - должны обновить представление сами
// this.$.text(‘B’); /* this.$ = jQuery/Zepto wrapper */
}
}
});
mask.render(" :letterChanger > div > '~[bind: letter]' ");
mask.render(':myAdapter > :letterChanger > div > “~[bind: letter]" ');
mask.registerHandler(':letter', Compo({
template: ":letterChanger > div > '~[bind: letter]' "
});
mask.render(" :myAdapter > :letter; " , model);
А вы знали — что для полной мощи инкапсуляции не плохо пользоваться различными загрузчиками, тем самым выносить контроллеры, их представления и стили в отдельные файлы. Простой пример композиции компонент:
header {
#logo;
:menu;
:userInfo;
}
:viewManager {
:userView;
:aboutView;
}
:pageActivity;
:notifier;
:footer;
Названия компонент начинается двоеточием только для лучшей семантики представлений.
Но главное в этих всех MV* — не их названия, тем более здесь все притянуто за уши (надеюсь никого не обидел?). А сама суть, то как мы создаем контроллеры разного назначения. И как видите, зависимости мы указываем непосредственно из представления — этим самым разгружая сами контроллеры и оставляем их заниматься только своими непосредственными задачами.
«Парсер» трансформирует View в дерево «нод». По нему потом проходится «билдер» и интерполирует модель — создает HTMLElement-ы и Контроллеры (произвольных тегов). В стандартную сборку MaskJS входит ещё одна хорошая библиотека — jmask@github [4]. Она помогает работать с maskDOM деревом, в ней используется синтаксис jQuery и её удобно использовать везде, где нужно динамически создавать maskdom дерево или изменять его, например в onRenderStart компоненты:
//..
onRenderStart: function() {
jmask(this).tag('div').addClass('pixel').wrappAll('.container data-id=dialog');
// eq. == jmask(this).wrappAll('.pixel > .container data-id=dialog');
}
Если где-то вы используете jQuery для создания DOM, то маска справится с этим точно также, и причем в разы быстрее [5], маленький пример
$('<div><span></span></div>').addClass('container').data('foo','bar').children('span').text('2013').appendTo('body')
// то же с jmask
jmask('div > span').addClass('container').data('foo','bar').children('span').text('2013').appendTo(document.body)
jmask('.container foo=bar > span > "~[text]"').appendTo(document.body, { year: 2013 })
Билдер также создаёт дерево из компонент, поэтому можно через селекторы находить другие контроллеры
mask.registerHandler(':page', Compo({
// ...
foo: function(){
// ...
// функция find вернёт первый найденный компонент
this.find(':scroller').scroller.refresh();
this.closest(':item[data-id=5]]').bar();
// через jmask можно находить все контроллеры
jmask(this).find(':listItem').each(function(x) { x.bar() })
}
});
Компонент может иметь хэш объект с перечнем всех событий которые хочет обработать —
var _myCompo = Compo({
constructor: function(){ this.name = 'C'; },
events: {
'touchstart: .pane': function(event){ this instanceof _myCompo // -> true },
//...
}
});
Но таким образом, мы привязываемся к разметке(css классам) — а это значит — привязка к реализации представления, что усложняет нам замену View. И это не есть хорошо. Во многих фреймворках можно вызывать методы контроллера непосредственно из представления, но это тоже не дело, хотя MaskJS поддерживает expressions в шаблонах — div > '~[: controllerMethod("test") ]'
(замечу, что в маске реализован свой expression parser и evaluator без with/new Function /eval). Намного лучше, когда представление посылает сигналы вверх по дереву контроллеров, начиная с «владельца» элемента — а там уже, кто хочет, тот реализовывает логику.
mask.registerHandler(':myCompo', Compo({
constructor: function(){ this.name = 'C'; },
slots : {
greet: function(sender){
// sender в контексте dom событий это сам event object, иначе компонент пославший сигнал
alert(this.name); // "C"
// return false - на случай, если нужно остановить передачу сигнала дальше по дереву компонент вверх.
},
//...
}
}));
mask.render(" :myCompo > .panel x-signal='click: greet'; ");
Заметьте декларативное объявление слотов в объекте slots
— этим самым, мы чётко разделяем логику контроллера, а маска сама вызовет эти обработчики, когда соответствующие сигналы будут запущены.
Дополнительно, мы сможем в любой момент слот или сигнал деактивировать, и при этом, все элементы, которые посылают этот сигнал в данной «области видимости» контроллеров, получат статус :disabled.
Обычные сигналы гуляют только вверх или вниз по дереву компонент, и что бы связать два компонента, которые лежат вне иерархии, нужно использовать трубки.
mask.registerHandler(':userInfo', Compo({
pipes: {
// pipe name
user: {
logout: function(){
this.model.authenticated = false;
}
}
}
}));
// = template.mask =
menu {
#logout x-pipe-signal='click: user.logout' > button > 'Sign out'
// ....
}
section #content {
// возьмём объект "user" из модели и передадим нижнему шаблону
% use="user" > :userInfo type='brief' ;
// ..
}
section #footer {
// bind to "user.authenticated" prop
%% if="user.authenticated" > % each="user.keys" > "~[.]"
}
В этом примере попытался немножко усложнить представления.
Все сигналы также можно посылать из самих контроллеров:
this.emitIn('name', args...); // детям
this.emitOut('name', args...); // родителям
Compo.pipe('user').emit('logout'); // начинаем с последнего контроллера присоединившегося к "трубе".
mask-binding@github [7]
Как же веб фреймворк без «привязок»? Здесь всё в лучших традициях жанра: One- / Two-way Bindings, Custom Binding Providers, Array Mutators, Validators.
Пример можно посмотреть на mask-try | bindings [8]. Биндинги по своей природе очень производительны, так как в render time они только сохраняют ссылки на дом элементы, и привязываются к моделе через defineProperty/ __defineSetter__
. И да — вы правильно заметили, старые браузеры не поддерживаются — но переопределив стандартный провайдер, можно добиться привязки к функциям вида setX/getX
, или другим шаблонам, как .get("x")/.set("x")
. Собственно, если нужно, можно ограничения обойти.
Интересные моменты:
div style='height: ~[bind: item.age * index + 10]px'
В зависимости как будет меняться возраст или индекс — такой будет высота панели div
dom events / (jquery) custom events
/ сигналах. :validate
. Перед тем, как присвоить значение модели, провайдер вначале проверит его, а в случае ошибки сообщит об этом пользователю и предложит вернутся к последнему верному значению. Так наша модель остаётся целостной.
input #device-type type=value > :dualbind value="age" {
:validate match="^[a-z]{2}-[d]{4}$" message=" ... pattern: xx-1234"
}
%
, который реализует логику if/else/each/repeat/use
и прочее. Так вот, в модуле bindings реализован также one-way binding для этих вещей:
%% if="state == true" {
%% each=userList {
// ... template
Важно не только писать большие и производительные приложения, но и получать от этого максимум удовольствия. Разделив разработку на компоненты, начиная от самых маленьких(:customCheckBox
) до самых больших(:inbox
), мы всегда концентрируемся на необходимом. Что бы отловить баги, в системном контроллере есть атрибуты debugger
и log
:
.user {
% debugger;
.user-status > '~[bind:info.status]'
%% log="info.status";
}
debugger
— во время render flow мы остановимся и можем посмотреть стэк компонент, текущую модель и html-элемент.
log
— выводим в консоль данные. Можем доступиться как к модели, так и контроллеру.
… для IncludeJS [9]
«Горячим» обновлением ресурсов без перезагрузки страницы сейчас никого не удивишь — да и реализация довольно тривиальная, но со скриптами не всё так просто. Здесь должны учитываться замыкания, dom события и прочее. Мы используем IncludeJS
библиотеку для загрузки всех модулей, и каждый скрипт файл может экспортировать метод reload. Также в состав IncludeJS.Builder
-а входит сервер, который следит за изменения запрашиваемых файлов и через socket.io
оповещает IncludeJS
. Собственно сценарий довольно простой. А вот MaskJS в свою очередь, в reload плагине переопределяет mask.registerHandler
— в нем он записывает все компоненты которые регистрируются и соотносит их к пути текущего скрипта. Также плагин подписывается на событие создания контроллера, и сохраняет текущую модель и ссылку на контроллер. Таким образом, когда через socket.io мы получим оповещение о изменения файла, у нас есть список названий компонент, которые этот файл создаёт, а также список экземпляров(instances)
. И далее дело техники — вызвать remove/dispose
каждого контроллера, и проинициализировать на их места обновлённые компоненты. Используя сигналы и слоты, родителям не нужно подписываться на dom события обновлённых компонент — сигнал и так дойдёт. Если компонент загружает mask markup отдельно и мы что-то в нем изменили, тогда IncludeJS
будет расценивать это, как изменение в самом customCompo.js
файле. Тема IncludeJS довольно ёмкая и об всех его возможностях как-нибудь в другой раз. Но то, что архитектура MaskJS позволяет заменять компоненты на лету, сильно упрощает разработку, особенно, если компонент спрятан за N кликами(прим. где-то в диалоге).
На данный момент MaskJS также работает в node.js. Принципы работы те же, только после создания mask dom, создается html dom, которое в свою очередь превращается в string buffer. А на клиенте все компоненты будут проинициализированы и для завершения получат нужные DOM элементы в onRenderEnd методе.
Маршрутизация завязана не на контроллерах, как во многих фрэймворках, а на представлениях. Помните? Представление само инициализирует нужные контроллеры. Тут же можно использовать Master Pages техники и прочее.
Работа в этой области ещё не закончена, нужно будет ещё разобраться с некоторыми нюансами. Но целью является то, что бы компоненты/контроллеры / виджеты работали как на клиенте (в первую очередь), но и с возможностью рендеринга на backend-е с завершением на клиенте.
Хочется ещё создать обертку из компонент над каким нибудь css фреймворком. Маска упростит разметку в разы — спрячем css классы и div wrappers
). И упростим создание меню/календарей/диалогов и т.д.
FIN
Это пожалуй все основные моменты. Многое ещё не рассказал, но надеюсь было понятно хотя бы то, о чём шла речь, ведь с меня рассказчик никакой, а материала много, поэтому сложно сконцентрироваться.
В комментариях, пожалуйста, не пишите — «посмотри на ИКС фреймворк!» — мы следим за мейнстримом, а вот более глубоким комментариям, буду очень рад.
Хотел бы ещё поблагодарить хабраюзара rma4ok [10] за многие дельные советы. Многое попытался учесть. Также буду рад любым другим советам или пожеланиям. Если вы знаете интересные техники из других языков / фреймворков — пожалуйста, поделитесь знаниями ). А также можете присоединиться к разработке.
Удачи.
Автор: tenbits
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/34443
Ссылки в тексте:
[1] MaskJS@GitHub : http://github.com/tenbits/MaskJS
[2] пример: http://libjs.it/mask-try/#preset:TODO
[3] гитхабе: https://github.com/tenbits/MaskJS
[4] jmask@github: https://github.com/tenbits/mask-j
[5] в разы быстрее: http://jsperf.com/dom-builder-mask-vs-jquery/4
[6] mask-compo@github: https://github.com/tenbits/mask-compo
[7] mask-binding@github: https://github.com/tenbits/mask-binding
[8] mask-try | bindings: http://libjs.it/mask-try
[9] IncludeJS: http://github.com/tenbits/IncludeJS
[10] rma4ok: http://habrahabr.ru/users/rma4ok/
[11] Источник: http://habrahabr.ru/post/179359/
Нажмите здесь для печати.