Pull to refresh

Comments 33

Да я раньше туда отправлял реализацию, но тогда это был лишь шаблонизатор без разных «наворотов» и они приняли только как альтернативу. Сейчас я обновил приложение и может попробую ещё раз послать «request». Приложение здесь. Или более длинный путь:
  • установить nodejs / git / inlcudejs / libjs
  • > cd tempFolder && ijs template todoapp
Отличная идея, но реализация, по-моему, неудачная, как и у всех других подобных проектах, которые в последнее время появляются в огромном количестве.
Что лично мне не понравилось:
1.
#logout x-pipe-signal='click: user.logout' > button > 'Sign out'
или
'~[bind: date.getDate() ]'
Вы тут предполагаете, что свойства «user»/«date» в некой js-модели будут всегда называться именно так. Т.е. при общей декларативности фреймворка вы привязываетесь к js-объектам.

2. Примеси/наследование компонентов непонятно как должно работать (должно ли вообще).
Т.е. в вашем примере я не смог «подмешать» компонент :customTag в конец компонента :datePicker, он постоянно оказывался вверху скопа. Возможно у вас есть какой-то другой синтаксис для этого?
td > :datePicker {
      р4 > 'test'
            
      :dualbind value='date' getter='getDate' setter='setDate';
            
      :customTag > button x-signal='click: clicked' > 'Click';
}


3. Опять же, несмотря на декларативность фреймворка, вы привязываетесь в DOM дереву. Лично меня это всегда отпугивает от подобных декларативных фреймворков, потому, что, при желании, view backend можно писать и на WebGL/canvas.

4. Синтаксис избыточен. Т.е. вместо
td > div > '~[bind:name]';
можно было бы сделать
td > $name
div -тег по-умолчанию и его можно опустить. Префикс $ — указание на то, что данный «узел» является свойством модели.
Дельный комментарий, спасибо — теперь по-пунктам:
  1. Сигналы — это не вызовы методов, а как бы именованные события, которые по дереву контроллеров или трубкам гуляют. А привязываться к модели из шаблонов вы как предлагаете по другому? Эту задачи в любом случае нужно кому-то делегировать. Если мы из конроллеров будем вставлять в дом данные — тогда контроллеры слишком толстые получатся. Здесь же шаблоны отвечают за свои задачи — вывести данные пользователю, а в случае сигналов — предают динамичности посылая «события». Мне кажется такое распределение вполне уместно. Или есть у вас другие идеи?
  2. :datePicker, это всего лишь обёртка над jQuery виджетом. И её реализация не подразумевает, что вы дополнительно в календарь свою разметку вставлять будете. Поэтому он просто это игнорирует. datePicker
  3. В отличии от других реализаций, разметка превращается в AST дерево. И реализовав свой билдер можно будет и на canvas рисовать
    rect dimension='0:0:100:100' color='red' {
        line from='0:0' to='5:5' size=5;
    }
    

    Ну смысл вы уловили.
  4. Вы убрали div — а если нам надо, что бы данные выводились в span? Сейчас data биндинги это лишь надстройка над MaskJS (не путать ~[name] и ~[bind: name]]), поэтому и синтаксиса специально для него нету. И из вашего примера, так будет через чур свободно, или?
    td > p > 'My name is $name'
    // vs
    td > p > 'My name is ~[name]'
    

    Хотя да, смотрится не плохо, но не зря все шаблонные движки выбирает немножко посложнее синтаксис для интерполяции.

2. Можно ввести ключевое слово для вывода контента по-умолчанию. Например __yield__:
td > :datePicker {
р4 > 'test'

:dualbind value='date' getter='getDate' setter='setDate';

__yield__

:customTag > button x-signal='click: clicked' > 'Click';
}

__________

Или есть у вас другие идеи?

Как я себе это представляю:
1. Каждый компонент содержит набор свойств и модель по-умолчанию. Доступ к свойствам осуществляется через прослойку (backend). Для HTML, свойства — это DOM узлы. У каждого свойства есть шаблон по-умолчанию, это может быть просто вставка в innerHTML или аттрибут (src для тега img) или что-то сложнее.

2. К компоненту может быть привязана кастомная модель, которая переопределяет доступ к одному или нескольким свойствам, или переопределять шаблоны валидации всей модели или отдельных свойств.

3. У каждого компонента есть контроллер по-уломчанию, который может быть заменён или расширен кастомным конроллером.

4. Контроллеры компонента не имеют доступа к DOM API и не подписываются на DOM события.
Фреймворком должен быть определён набор сандартных событий в которые будут транслироваться DOM события, типа «active:start» (mousedown, touchstart), «active:end» (mouseup, touchend), «hover:start» (mouseenter), «hover:end» (mouseleave), «action» (click, submit) и т.д.
Обработчик события можно повесить только на свойство компонента.

5. Каждое свойство — это некий узел (Node для HTML backend) который может отлавливать события. При возникновении какого-либо события, класс с таким же именем устанавливаться/снимается на узел привязанный к свойству — Например, node.classList.add(«active») для «active:start» и node.classList.add(«remove») для «active:end».

6. К компоненту может быть привязан источник данных. Это может быть как и просто массив, так и класс имплиментирущий абстрактный интерфейс ModelDataView с методами получения/записи/фильтрации данных.

7. В модели описано, какие узлы array-like и какие одиночные, поэтому при применении данных на шаблон, array-like автоматически склонируются, и не нужно никаких each в коде шаблона

8. В шаблоне, на один узел может быть повешено более одного компонента.

Абстрактный шаблон компонента ImageGallery
используется псевдо-HTML
= section :ImageObject extend :MediaObject # '= ' - объявление компонента
    p ::name
    image ::contentURL
    body
        p ?::author # '?' - узел будет создан только если свойство присутствует в источнике данных
        data ?::datePublished

= section :ImageGallery extend :CollectionPage
    header
        hx ::name
        p ?::description
        image ?::image
    body #
        ::primaryImageOfPage[ :ImageObject ] # свойство суб-компонент типа ImageObject
    footer
        ::associatedMedia[ :ImageObject ] # превьюшки, суб-компоненты типа ImageObject



Контроллер
class ImageObject_controller extend MediaObject_controller {
    constructor(el) {
        super(el);
    
        this.model = _.getModel(el); // _ - глобальная переменная фреймворка
        this._el = _(el); // обёртка над Node элементом (в случае html backend)
    }

    openImage(imageObject = 0) {
        if ( Number.isInteger( imageObject ) ) {
            imageObject = this.model.associatedMedia[ imageObject ];
        }

        if ( !imageObject ) return;

        this.model.primaryImageOfPage = imageObject; //тут вызывается setter у модели
    }

    events: {
        "associatedMedia action": function(event) {
            let {target, metaKey} = event; // деструкруризация let target = event.target, type = event.type
            
            if ( !metaKey ) {
                this.openImage(target);
            }
            else { // Нажали Ctrl + click - открыти картинку в новом окне
                let model = _.getModel(el)
                    , url
                ;
                                
                if ( model && (url = model.url) ) {
                    _.globalRouter.openInNewWindow(url);
                }
            }
        }
    }
}



Модель
class ImageObject_model extend MediaObject_model {
    constructor(dataView) {
        super(dataView);
        
        this.dataView = dataView;
    }

    get associatedMedia() {
        return this.dataView.get("associatedMedia");
    }

    set associatedMedia(singleOrArray) {
        if ( !_.validate("associatedMedia", singleOrArray, this.validation ) {
            throw new ValidationError(_.validate.lastError);
        }
    
        if ( Array.isArray(singleOrArray) ) { // Если массив значению
            return this.dataView.replace("associatedMedia", singleOrArray); // заменяем всю коллекцию
        }
    
        // Если один объект
        return this.dataView.add("associatedMedia", singleOrArray); // то добавляем его вколлекцию
        
    }
    
    // ... остальные свойства
    
    validation: { // Валидация
        ":scope": function() { // ":scope" ссылка на "всю" модель
            // ...
            // валидация всей модели
        }
    
        "associatedMedia": function(obj) {
            return obj.contentURL && obj.name;
        }
    }
}



Применение в шаблоне
!!!
html
    head
        js 'script/something.js'

body.image-gallery :ImageGallery /images/get/92188?from=0&to=10 # /images/get/* ссылка на источник данных
    # Устанавливаем css класс и текст по-умолчанию
    .image-gallery__author ::author ? "Автор не указан"
    



Прошу прощения за длинный комментарий
Хорошие комментарии длинными не бывают, даже если занимают 10 скроллов)

  • С :datePicker-ом вы делаете, что то не то) Если вам надо до или после календаря вывести текст / кнопkи, так не обязательно помещать их в блок :datePicker —
       h4 > 'Title'
       :datePicker;
      button > 'X'
    

    • Шаблон — p ::name — у вас также шаблон зависит от модели, а если имеется ввиду, что это данные из контроллера — тогда контроллер, это чистой воды Presenter — который в свою очередь получает данные из модели. (см. статью)
      Наследование тоже самое что и MasterPages — layout
    • Контроллер — не понятно, как у вас определяются события. На узлы (DOM HTMLElements) всех компонент вешаются события клик (action)? А если надо не на весь элемент вешать обработчик, а только на кнопку 'OK' например, а если много кнопок?

      Наследование контроллеров в MaskJS устроено пока что, только как обычное наследование Javascript прототипов — а там уже мы можем, например, переопределить метод onRenderEnd подсунув там другой шаблон для компоненты.

    • Модель — точно также можно и в маске это сделать. Но мне нравится, что не обязательно!! создавать врапперы — маске подойдет любой объект.



На счёт array-like — исходить из того, что фреймворк и так догадается, не особо стоит, потому что порой мы передаём массив, но хотим, что бы не элементы выводились из него, а к примеру, только статистика по нему. И если у вас есть модель, где не только массив, а помимо и другие пропертя, поэтому нам всё равно нужно передавать его (массив) в шаблон / контроллеры — почему бы не через each?

Ну а так расширить MaskJS до вашего видения не составит труда — я попытался по максимуму упрастить всё — и по максимуму дать возможность расширять. И если у вас есть время, можете форкнуть и попробовать поиграться — а если будут вопросы, я вам помогу.
Чтобы мне долго не разбираться (идея навскидку неплохая, но времени сейчас нет) — пробовали ли сочетать с Backbone.js? Раз уж за мейнстримом следите.
Скажем, Backbone мне по идеологии нравится значительно больше аналогов, но по части работы с DOM он безнадёжно отстал от жизни (из основного: нет встроенной системы байндинга, нет компонент). Если использовать Backbone для роутинга/фетча а Mask для отрисовки, много ли придётся дорабатывать напильником? Скажем, обращение к атрибутам модели там только через параметризованные геттеры: model.get( «someAttr» ). Mask это сможет обрабатывать, включая отслеживание изменений?
И да, плюсую по предыдущему комментарию: синтаксис шаблонов действительно избыточен.
Да я видел, как люди использовали маску с бэкбоном, используя её как шаблонизатор. И постепенно создавали свои компоненты (обработчики тэгов). Биндинг в Маске реализoван привязками через defineProperty — согласитесь, иметь в конечном результате "model.someAttr" лучше, чем model.get("someAttr");. Если вы получаете данные по ajax запросу, вам их не надо расширять до бэкбоновской модели — а напрямую передавать во View.
В Backbone это именно модель, а у вас — данные или состояние.
Это плохой вариант, в Backbone на модели завязано очень многое, в том числе и фетчинг. То есть фетч там забирает данные сразу в модель, а вы мне говорите, не используйте бэкбоновские модели. Всю работу с серверной частью вы предлагаете вынести в голые Ajax-запросы? Во-первых, это шаг назад, во-вторых, от бэкбона тогда остаётся только роутер, то есть это никак не тянет на «использовали маску с бэкбоном». Да и использовать бэкбон только ради роутера смысла нет, роутер можно и отдельно найти.
В общем, советую пересмотреть концепцию: пока получается, что на серьёзных проектах использовать «голую» маску большого смысла нет, так как она не решает значительной части актуальных задач, а гибкости для сочетания с фреймворками, которые эти задачи решают, не хватает.
Да, Маска — model agnostic. И это к лучшему — модель может выглядеть как угодно — будь это данные из mongoose в node.js, или данные из localStorage, или обычный класс User на клиенте. Плюс для тестов это на порядок лучше.
Хотя маска и поддерживает такую запись — div > '~[:get("username")]', что бы достать данные из модели — но биндинги на данный момент работать не будут таким образом.
Так где же «модель может выглядеть как угодно», если вы мне ответом выше сказали «не используйте бэкбоновские модели», и здесь говорите, что байндинги с ними работать не будут?
Вопрос-то очень простой: если не использовать бэкбоновские модели, тогда зачем бэкбон? Если не использовать байндинги, тогда зачем маска (односторонних шаблонизаторов для бэкбона пруд пруди)?
Я поэтому и спросил, можно ли использовать вместе. Получается, фактически нельзя.
Понимаете, model.get('attrName') — это ужасный хак. Проперти должны оставаться ими, а не создаваться функции геттеры / сеттеры. Биндинг в маске это всего лишь плагин, который может привязаться к свойствам любой модели model.attrName. Но как уже сказал в статье, можно создать кастомный биндинг провайдер, который будет привязываться к этим функциям и слушать изменения. А такая запись div > '~[:get("username")]' — это не binding, а просто expression.

А ответ на вопрос довольно прост — backbone и не нужен. Нормальных Data Fetch / Routings библиотек тоже пруд пруди. Маску же можно использовать в контексте backbone как шаблонизатор для более плавной миграции от бэкбона к маске в целом. В результате получите более целостное и более быстрое приложение.
Ну позиция понятна, но мигрировать с бэкбона на маску я точно не собираюсь. Если бы хотел мигрировать, выбрал бы Angular, но я уже обозначил выше, что идеологически бэкбон мне намного ближе.
В качестве шаблонизатора с байндингом/компонентами поюзал бы, но нет так нет.
Angular ничем не лучше, а в производительности очень отстаёт, особенно в контексте мобильных девайсов, где у всех webkit. jsperf — одно и тоже приложение. А если вы сравните с бэкбоном, то тоже думаю удивитесь. Кстати, не хотели бы создать такое же маленько todo на бэкбоне?
А смысл в этих todo? Они ничего не показывают. Хороший фреймворк для написания todo != хороший фреймворк для написания сложных приложений.
Приложения — это во-первых, данные, во-вторых, бизнес-логика. И только в третьих — представление. Именно поэтому все UX-центричные фреймворки я недолюбливаю. В качестве дополнения к Backbone (где в центре внимания данные и логика) — да, хорошо. В качестве замены — нет, спасибо.
Это очень некорректный тест, которым вы вводите людей в заблуждение. Angular не шаблонизатор и замерять «холодный старт» совершенно не верно. Максимум можно сравнивать выполнение `$scope.$apply()`.
А можете более подробно сказать, почему тест не корректный — это две идентичные программы — после теста можете вставить тестируемые участки в консоль, и убедитесь, что все биндинги и прочее работает. И маска здесь выступает не только как шаблонизатор…

Поэтому, я исхожу из этого, что вполне корректные тесты — на выходе дают один и тот же результат…
Нет, сравнивать «запуск» Angular неправильно, он происходит один раз на domReady и всё, так что нужно сравнивать именно реакцию на изменения данных/модели, т.е. замерять скорость обработки биндинга. В теории, у вас должно быть быстрей, т.к. используете getter/setter, в то время как у Angular dirty check, вот примеры более менее грамотных тестов, хотя и не совсем:
angularjs-vs-knockoutjs
angular-vs-knockout-vs-ember
И ещё один вопрос, а вы группируете манипуляции с DOM, если одновременно меняются N количество моделей?
происходит один раз на domReady
— я же как раз и сравниваю этот «запуск» приложения — на сколько быстро angularjs сможет приложение зарендерить и создать биндинги — и не важно где происходит инициализация участка страницы — в domReady или после ответа сервера.
Вам разве не важна скорость отображения вашего приложения, а только насколько быстро «а» изменится на «б» в рантайме? Мне как раз первое важнее — это и тестирую.

Манипуляции — если это две разные модельные сущности в разных участках дома, то нет. Если же это списки то да, но лучше массивы менять через splice, чем 100 раз push — ну или просто сообщить маске, что нужно кешировать изменения — (lock model), а потом пакетом применить — (unlock model).
Окей, я вас понял, вы считаете, что старт ангулара за ~6ms может вызвать у пользователя дискомфорт? :]
На мобильном девайсе и со сложным UI и моделью будет далеко на 6мс… Как раз мобильной разработкой занимаемся — так там всё на счету. )
Окей, ~21ms :] И ведь это число будет почти константа.
О, как это мило, напоминает славные дни, когда я сутками сидел за собственным виью-ивент-фреймворком. Но к сожалению, пришли злые дяди и рассказали мне, что такое модели, зачем и главное как их надо использовать. Теперь я таким не страдаю.

Вообще очень советую вам ознакомиться близко с Backbone и его примерами (как я понял вы очень поверхностно его знаете). Только учтите, что если после прочтения вам хоть на секунду покажется, что он или ваш же фреймворк решает все проблемы, начните читать с начала. Потом изучите еще парочку. В какой-то момент озарение должно придти. Искренне желаю вам удачи, так как вы в любом случае молодец.
Спасибо! Но с событиями вы, наверное, делали что-то не так. В маске вы нигде на события(сигналы) не подписываетесь — всё довольно строго распределено — из любого контроллера или из любого дом элемента можно узнать, кто может / обрабатывает сигнал. В бэкбоне вы же наверняка подписываетесь на события? events: { «click .my-button»: «x»} — это называется «слушать разметку», а в этом есть большой недостаток — если есть иерархия контроллеров и каждый имеет свою разметку — тогда из предков, ни в коем случае нельзя «слушать разметку» детей. До этого, было как бы, два решения — первое, ребенок посылает «custom event», и второе, ребенок вызывает функции отца. Второе отпадает — из-за непозволительно большой связности. А «custom event» это уже лучше но лишние телодвижение — ребенок слушает свою разметку и посылает событие. Я же предлагаю выводить «событие»(сигнал) непосредственно в представление.

Модель:
В Backbone это именно модель
— вы, думаю, понимаете, что любые данные — это модель. Эти данные / состояния можно обернуть или скрыть в классах / структурах. И эти «обертки» собственно и называются domain model. Маска может работать и с тем, и с другим.

View из Backbone я выпиливаю за ненадобностью. Не слушать «разметку детей» задача довольно простая и никак с архитектурой может быть не связана. Различное понимание моделей я готов обсудить приватно. Вот моя почта tenphi@gmail.com
Не хочу разводить холивары, ведь тут могут быть дети.
Статья интересная, спасибо автору, вот только есть два вопроса, а вам не кажется что это выглядит сложно?
input #device-type type=value > :dualbind value="age" {
    :validate match="^[a-z]{2}-[\d]{4}$" message=" ... pattern: xx-1234"
}


— вот тут два элемента или один, а внутри блока, вот эти параметры, это глюк хабра или так оно и есть? :]
Извините, ещё раз — какие параметры? )

Можно записать и так, (с аттр. type опечатка вышла)
input #device-type type=text {
     :dualbind value="age" { 
          :validate match="^[a-z]{2}-[\d]{4}$" message=" ... pattern: xx-1234"; 
          // ....
     }
}


Согласен, что запись дуалбиндеров не очень лаконичная, но тэг нужен, так как через него можно указывать много других параметров BindingProvider-a.
Ну я не знаю, сходу так:
input #device-type type=text value="{{age}}"

`:validate match="^[a-z]{2}-[\d]{4}$" message="… pattern: xx-1234"; ` — эти параметры, многоточие смутило.
Ааа. Это просто сообщение — может быть любой текст. Ваш пример отличный ({{age}}). Просто, понимаете, если будет много других параметров — changeEvent, signal listener, геттеры и сеттеры и прочее, то инлайн запись не подойдёт, через тэг тогда нужно будет.
Но простые записи двухсвязных биндингов обязательно сделаю проще. Спасибо за замечание.
Я понимаю, удачи вам.
Sign up to leave a comment.

Articles