$mol: reactive micromodular ui-framework

Сколько нужно времени, чтобы просто вывести на экран большой список, используя современные фреймворки?


Список на 2000 строк ReactJS AngularJS Raw HTML SAPUI5 $mol
Появление списка 170 ms 420 ms 260 ms 1200 ms 50 ms
Обновление всех его данных 75 ms 75 ms 260 ms 1200 ms 10 ms

Напишем нехитрое приложение — личный список задач. Какие у него будут характеристики?


ToDoMVC ReactJS AngularJS PolymerJS VanillaJS $mol
Размер (html + js + css + templates) * gzip 322 KB 326 KB 56 KB 20 KB 23 KB
Время загрузки 1.4 s 1.5 s 1.0 s 1.7 s 0.7 s
Время создания и удаления 100 задач 1.3 s 1.7 s 1.4 s 1.6 s 0.5s

Небольшая головоломка: перед вами синхронный код, загружающий и обрабатывающий содержимое 4 файлов, но с сервера они грузятся параллельно. Как такое может быть?


Синхронная параллельная загрузка ресурсов


А теперь прошу за мной в кроличью нору, настало время удивительных историй…



Клуб именованных велосипедистов

Здравствуйте, меня зовут Дмитрий Карловский и я… руководитель группы веб-разработки компании SAPRUN. Наша компания занимается преимущественно внедрением и поддержкой продуктов SAP в ведущих компаниях России и ближнего зарубежья. Сам SAP — огромная сложная система, состоящая из множества компонент.


Один из таких компонент — веб фреймворк SAPUI5, предназначенный для создания одностраничных приложений. Это — типичный представитель коробочных фреймворков, то есть таких, которые предоставляют вам не только архитектуру, но и богатую библиотеку виджетов. И как любой коробочный фреймворк, данный подвержен страшной болезни современности — ожирению.


Старый, толстый, мрачный гусь


Проявляется ожирение во всём: огромные объёмы кода из изысканной немецкой пасты; неповоротливые виджеты, еле-еле двигающие списки на 100 элементов; развесистые деревья классов, в дебрях которых заблудится даже лесной эльф. Всё это приводит к достаточно длительной разработке, а время — деньги.


В результате, довольно сложно выигрывать тендеры на разработку веб приложений, если указываешь реальные временные оценки. А если и выигрываешь, то результат, скажем так, не впечатляет: приложение получается либо слишком простым, либо слишком тормозным. Особенно всё печально на смартфонах, где каждый киловатт на счету.


Нам требовался более эффективный инструмент, позволяющий малой кровью создавать конкурентоспособные масштабные кроссплатформенные приложения, поэтому мы решились на страшное — переизобрести колесо — собственный веб фреймворк с говорящим названием $mol. Разработанный с нуля, он вобрал в себя множество свежих идей, о которых и пойдёт дальнейшее повествование.


Реактивное программирование


Изобретённое 50 лет назад, оно только недавно добралось до мира пользовательских интерфейсов в вебе. Причём добралось в достаточно куцем «push» виде: вы описываете некоторую последовательность действий, на вход подаёте некоторые данные, и эти действия, последовательно применяются к каждому элементу данных. Однако, такой подход приводит ко сложностям при реализации ленивых и динамически меняющихся вычислений.


$mol же построен на «pull» архитектуре, где инициатором любых действий выступает потребитель результата этих действий, а не источник данных. Это позволяет рендерить лишь те части приложения, что попадают в видимую область; создавать лишь те объекты, что требуются для рендеринга в текущий момент; запрашивать с сервера лишь те данные, что требуются для созданных объектов.


$mol насквозь пропитан «ленивыми вычислениями» и автоматическим освобождением ресурсов. Вы можете всего одной строчкой закешировать результат выполнения функции и не беспокоиться об инвалидации и очистке этого кеша — модуль $mol_atom сам отследит все зависимости и выполнит всю рутинную работу.


const source = new $mol_atom( ( next? : number )=> next || Math.ceil( Math.random() * 1000 ) )

const middle = new $mol_atom( ()=> source.get() + 1 )

const target = new $mol_atom( ()=> middle.get() + 1 )

console.assert( target.get() === source.get() + 2 , 'Target must be calculated from source!' )
console.assert( target.get() === target.get() , 'Value must be cached!' )

source.push( 10 )

console.assert( target.get() === 12 , 'Target value must be changed after source change!' )

Тут в момент изменения source происходит инвалидация значения middle и target, так что при запросе значения target происходит вычисление его актуального значения, как бы далеко друг от друга source и target в программе ни находились.


Синхронное программирование


Нет ничего проще, чем синхронное программирование. Код получается коротким, понятным и вы можете свободно использовать все возможности языка по управлению потоком исполнения (if, for, while, switch, case, break, continue, throw, try, catch, finally).


К сожалению, JS — однопоточный язык, поэтому, для обеспечения конкурентного исполнения множества задач, код приходится писать асинхронный, что порождает множество проблем: начиная лапшой из мелких функций и заканчивая ненадёжной обработкой исключительных ситуаций. node-fibers позволяет писать синхронный код не блокируя системный поток, но работает только в NodeJS. async/await/generators позволяют создавать асинхронные функции, которые могут вызывать друг друга синхронно, но из-за несовместимости с обычными синхронными функциями, приходится чуть ли не все функции делать асинхронными. Кроме того, для них требуется специальная поддержка со стороны браузера или транспиляция в адскую машину состояний.


Модель реактивности же, используемая в $mol, позволяет элегантно абстрагировать код от асинхронности. Посмотрите, например, на исходный код Куайна из начала статьи:


content() {

    const paths = [
        '/mol/app/quine/quine.view.tree' ,
        '/mol/app/quine/quine.view.ts' ,
        '/mol/app/quine/quine.view.css' ,
        '/mol/app/quine/index.html' ,
    ]

    const sources = paths.map( path => {
        return $mol_http_resource.item( path ).text()
    } )

    const content = sources.map( ( source , index )=> {
        const header = `# ${ paths[ index ] }\n`
        const code = '```\n' + source.replace( /\n+$/ , '' ) + '\n```\n'
        return `${ header }\n${ code }`
    } ).join( '\n' )

    return content
}

Тут вы видите вполне себе синхронную генерацию содержимого страницы. Однако, системный поток не блокируется, а загрузка всех 4 файлов происходит параллельно. При этом, пока идёт загрузка данных, вместо них выводится индикатор загрузки. Формируется он автоматически, избавляя разработчика и от этой головной боли тоже.


Компонентное программирование


Разбиение приложения на компоненты позволяет разделять одну большую задачу, на задачи поменьше. Компоненты могут быть реализованы разными людьми одновременно, после чего собраны вместе. Поэтому важно, чтобы компоненты с одной стороны были самодостаточны, а с другой — очень гибко настраиваемы.


Конструктор LEGO содержит множество самых разнообразных деталей, но любые из них стыкуются вместе благодаря стандартизированному соединительному интерфейсу. В $mol в роли такого интерфейса выступают свойства. Когда родительский компонент создаёт дочерний, он переопределяет у того ряд свойств, настраивая его поведение под свои требования. А благодаря реактивности, риск что-либо непреднамеренно сломать в дочернем компоненте — минимален.


Вы можете легко и просто заменить какое-либо свойство моком, чтобы протестировать логику работы компонента; задать всем свойствам константные значения для проверки вёрстки; указать дочернему компоненту использовать какое-либо свойство родительского, и связать через него несколько дочерних компонент вместе.


Родительский компонент имеет полный контроль над дочерними, что позволяет делать компоненты очень простыми, без необходимости реализации всех возможных сценариев с огромными конфигами для их настройки.


В примере с Куайном используется компонент $mol_pager, рисующий типичную страничку с заголовком в шапке, скроллящимся телом и подвалом:


$mol_pager $mol_viewer
    childs /
        < header $mol_viewer
            childs < head /
                < titler $mol_viewer
                    childs /
                        < title
        < bodier $mol_scroller
            childs < body /
        < footer $mol_viewer
            childs < foot /

Но на базе него вы можете создать новый компонент, который в шапку добавляет иконку, тело заменяет компонентом из родителя, а подвал вообще удаляет:


$mol_app_quine $mol_pager
    head /
        < logo $mol_icon_refresh
        < titler
    body /
        < texter $mol_texter
            text < content \
    footer null

С голой грудью на амбразуру

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


Допустим, перед вами вот это не хитрое приложение:


Типичное бизнес-приложение


Тут слева вы видите список заявок на закупки, а справа — подробности по выбранной заявке: кому, что, когда и на какую сумму. И всё бы хорошо, да вот только дата поставки выводится в формате ISO8601 «YYYY-MM-DD», а не в привычном для целевой аудитории «MM/DD/YYYY». Кто мы такие, чтобы навязывать заказчику международные стандарты? Нет, так дело не пойдёт и нужно срочно исправить, но с чего начать, куда копать?


Единственная зацепка — DOM элемент, куда выводится дата. Возможно DOM инспектор сможет помочь найти какие-либо зацепки, которые позволят вам выйти на исполнителя:


Типичное бизнес-приложение кишками наружу


Что за больной психопат мог придумать столь длинные идентификаторы элементам? И почему они такие странные? Словно бы являются JS кодом… А что если…


Содержимое объекта, который вытягивается за кишки


Скопировав идентификатор в консоль вы с удивлением обнаруживаете, что данный код не просто рабочий, но и возвращает какой-то объект, подозрительно напоминающий визуальный компонент: он является экземпляром класса $mol_viewer и хранит в себе ссылку на DOM элемент с которого вы и начали своё расследование.


Тут вы подмечаете, странную закономерность: все поля именуются либо нормально, но хранят в себе функции, либо хранят не функции, но именуются со скобками в конце. Похоже, это ружьё тут тоже висит не просто так — вы пробуете вызвать у объекта метод objectOwner() и получаете ожидаемый результат — ссылку на компонент выше по иерархии:


Содержимое владельца находится на расстоянии вытянутой руки


Уже лучше ориентируясь в происходящем, вы возвращаетесь к жертве неправильной локализации и окидываете взглядом её немногочисленные поля, ни одно из которых не содержит заветной даты. Ваше внимание привлекает поле с говорящим названием childs, которое содержит некую функцию возвращаемым значением которой может быть именно то, что вы ищите.


Метод возвращает дату поставки


Ага! У вас есть подозреваемый. Щёлкнув правой кнопкой по функции вы в два счёта находите место её определения:


Исходный и сгенерированный код создания компонента


Перед вами код создания вложенного компонента, явно сгенерированный роботом. На это указывает странный путь к файлу и короткий комментарий, судя по всему, послуживший исходником для генератора. А найденная вами функция childs — не более чем посредник, передающий управление функции content компонента-владельца. Продолжая движение по цепочке улик, вы поднимаетесь всё выше, распутывая клубок интриг в высших эшелонах власти, пока, наконец, не выходите на истинного преступника под именем $mol_app_supplies.root(0).detailer().position(0).supplyDate():


Настоящий исполнитель этого зверского преступления


Дело за малым — направиться по указанному адресу, внести необходимые изменения и проверить их. Но с чего начать, куда копать?


Вы выкачиваете репозиторий и обнаруживаете в корне проекта package.json. Логично предположить, что это NodeJS проект, а значит нужно установить зависимости:


> npm install --depth 0

Type 'npm start' to start dev server or 'npm start {relative/path}' to build some package.
.
+-- body-parser@1.15.2
+-- compression@1.6.2
+-- concat-with-sourcemaps@1.0.4
+-- express@4.14.0
+-- mam@1.0.0
+-- portastic@1.0.1
+-- postcss@5.2.4
+-- postcss-cssnext@2.8.0
+-- source-map-support@0.4.3
`-- typescript@2.0.3

Зависимостей не очень много, так что ставятся они все в течении минуты. Вы подмечаете, что в проекте активно используется транспиляция: скрипты пишутся на typescript, стили обрабатываются postcss, а для отладчика генерируются source-maps.


Cудя по подсказке, для запуска локального сервера, нужно выполнить очевидную команду:


> npm start

22:23 Builded mol/build/-/node.deps.json
22:23 Builded mol/build/-/node.js
22:23 Builded mol/build/-/node.test.js

$mol_build.root(".").server() started at http://127.0.0.1:8080/

Дальнейшие шаги не менее очевидны — открытие указанного адреса приводит вас к списку пакетов вида:


Список файлов в корне проекта


Вы идёте вглубь по пространствам имён, пока не попадаете на нужное приложение. Подозревая, что в рамках одного проекта может существовать множество приложений, вы проверяете другие пути и убеждаетесь, что это действительно так.


При этом вы подмечаете, что первое открытие приложения происходит несколько секунд, а повторные заходы уже отрабатывают мгновенно. Что за тормоза на пустом месте? Открытие консоли проясняет ситуацию:


$mol_build.root(".").server() started at http://127.0.0.1:8080/

mol> git fetch & git log --oneline HEAD..origin/master
> git fetch & git log --oneline HEAD..origin/master
jin> git fetch & git log --oneline HEAD..origin/master

23:00:23 Builded mol/app/supplies/-/web.css
23:00:27 Builded mol/app/supplies/-/web.js
23:00:27 Builded mol/app/supplies/-/web.locale=en.json
23:00:41 Builded mol/app/todomvc/-/web.css
23:00:45 Builded mol/app/todomvc/-/web.js
23:00:45 Builded mol/app/todomvc/-/web.locale=en.json

Ага, при первом заходе в приложение происходит сборка пакетов для него. Все скрипты в один файл, все стили — в другой, тексты — в третий. Вы перезапускаете сервер, открываете браузер и проверяете эту теорию:


Первая загрузка 4 секунды, вторая - пол секунды


Так и есть — грузятся всего 4 файла, причём, подозрительно малого объёма в сравнении с другими популярными фреймворками: все скрипты умещаются в 30 килобайт с учётом сжатия. Чёрная магия, не иначе. В 30 килобайт даже отдельно взятая jQuery не помещается, а ведь эта библиотека — основа большинства фреймворков. Вы смотрите в сгенерированный пакет web.js и офигеваете ещё сильнее, ведь код даже не минифицирован! Совсем ничего святого!


Что ж, хватит развлекаться, пора провести исправительные работы. Вы открываете positioner.view.ts и видите там следующую картину:


namespace $.$mol {
    export class $mol_app_supplies_positioner extends $.$mol_app_supplies_positioner {

        position() {
            return null as $mol_app_supplies_domain_supply_position
        }

        productName() {
            return this.position().name()
        }

        price() {
            return this.position().price()
        }

        quantity() {
            return this.position().quantity().toString()
        }

        cost() {
            return this.position().cost()
        }

        supplyDate() {
            return this.position().supplyMoment().toString( 'YYYY-MM-DD' )
        }

        divisionName() {
            return this.position().division().name()
        }

        storeName() {
            return this.position().store().name()
        }

    }
}

Как-то бедновато. Где лапша? Где фрикадельки? Всё, что делают эти 8 методов — это преобразуют хитросплетения данных доменной модели в свойства модели интерфейсной. Чтобы понять как данные выводятся, вы идёте по единственному видимому отсюда пути — зажимаете CTRL и щёлкаете по базовому классу, что приводит вас к тому самому генерированному коду, расположенному во '-/view.tree/positioner.view.tree.ts':


/// $mol_app_supplies_positioner $mol_carder
namespace $ { export class $mol_app_supplies_positioner extends $mol_carder {

    /// heightMinimal 68
    heightMinimal() {
        return 68
    }

    /// productLabel @ \Product
    productLabel() {
        return this.text( "productLabel" )
    }

    /// productName \
    productName() {
        return ""
    }

    /// productItem $mol_labeler 
    ///     title < productLabel 
    ///     content < productName
    @ $mol_mem()
    productItem( next? : any , prev? : any ) {
        return ( next !== void 0 ) ? next : new $mol_labeler().setup( __ => { 
            __.title = () => this.productLabel()
            __.content = () => this.productName()
        } )
    }
// ...

И тут куча мелких функций. Ни тебе обработчиков событий, ни создания DOM-элементов. Видя рядом расположенные куски исходного файла и сгенерированный код, вы быстро начинаете понимать этот птичий синтаксис:


  • При объявлении компонента сначала указывается имя его класса, а потом имя базового класса.
  • Внутри объявления комбинация имени и значения создают функцию, которая возвращает это значение.
  • В качестве значения можно указать число, либо строку, если предварить её «обратной косой чертой». Эта черта ассоциируется у вас с экранированием данных. По всей видимости всё, что идёт после неё не будет разбираться генератором, а будет вставлено как строка.
  • Если поставить «собачку», то текст пропадёт из кода, а вместо него будет вставлено получение его по ключу. По всей видимости именно на этом и основана генерация файла с текстами, которую вы подметили, когда игрались со сборкой проекта.
  • В качестве значения можно указать имя другого компонента и тогда функция будет возвращать соответствующий экземпляр. При этом можно перегрузить свойства вложенного компонента, своими свойствами. Угловая скобка, очевидно, показывает направление движения данных.

Вроде бы всё просто, но не понятно только зачем было вводить какой-то свой формат, если то же самое в typescript занимает не сильно больше места. Вы открываете исходный positioner.view.tree в надежде увидеть там что-то ещё.


$mol_app_supplies_positioner $mol_carder
    heightMinimal 68

    content < grouper $mol_viewer childs /

        < mainGroup $mol_rower childs /

            < productItem $mol_labeler
                title < productLabel @ \Product
                content < productName \

            < costItem $mol_labeler
                title < costlabel @ \Cost
                content < coster $mol_coster
                    value < cost $mol_unit_money
                        valueOf 0
- ...

И действительно — существенное отличие в том, что во view.tree иерархия вложенных компонент представлена наглядно, что позволяет быстро в них ориентироваться, но генерируемый класс получается вполне себе плоским, предоставляя доступ к любому вложенному компоненту за один вызов метода.


Воодушевлённый тем, как легко вы распутываете клубок внутренней архитектуры, вы берётесь за правки. Можно было бы просто поменять формат вывода в функции supplyDate и на этом закрыть дело:


        supplyDate() {
            return this.position().supplyMoment().toString( 'MM/DD/YYYY' )
        }

Но это бы лишь отсрочило решение настоящей проблемы — формат не зависит от установленной локали. А ведь чуть раньше вы выяснили, что локализация текстов уже вполне себе поддерживается. Вы возвращаетесь к positioner.view.tree.ts:


    /// productLabel @ \Product
    productLabel() {
        return this.text( "productLabel" )
    }

Погрузившись в text() вы доходите до места, где задаётся язык:


    export class $mol_locale extends $mol_object {

        @ $mol_mem()
        static lang( next? : string ) {
            return $mol_state_local.value( 'locale' , next ) || 'en'
        }

Ага, чтобы получить текущий язык, нужно выполнить:


$mol_locale.lang()

Вы выполняете этот код в консоли и убеждаетесь, что он действительно работает.


Осталось создать функцию, которая бы по идентификатору языка возвращала формат представления даты. Но где её разместить? Нужно создать отдельный модуль.


По аналогии с другими модулями вы создаёте новый по адресу mol/dateFormat/dateFormat.ts со следующего вида содержимым:


namespace $ {

    export const $mol_dateFormat_formats : { [ key : string ] : string } = {
        'en' : 'MM/DD/YYYY' ,
        'ru' : 'DD.MM.YYYY' ,
    }

    export function $mol_dateFormat() {
        return $mol_dateFormat_formats[ $mol_locale.lang() ] || 'YYYY-MM-DD'
    }

}

Только одно не понятно — ни в одном файле нет ни import, ни require. Как же система узнает, что этот файл нужно включить в пакет приложения? Не попадают же в пакет вообще все файлы? Чтобы проверить эту гипотезу вы перезагружаете приложение и пытаетесь вызвать свежесозданную функцию из консоли:


$.$mol_dateFormat() // Uncaught TypeError: $.$mol_dateFormat is not a function

Ну не может же оно само понимать какой модуль нужен, а какой — нет? Или может? Вы добавляете использование функции в приложение:


        supplyDate() {
            return this.position().supplyMoment().toString( $mol_dateFormat() )
        }

Перезагрузив страницу, вы с удивлением обнаруживаете, что приложение не только не упало, но и вывело дату в локализованном формате:


Вывод даты в американском формате


Вы переименовываете файл в dateFormat2.ts — всё работает. Переименовываете директорию в dateFormat2 — снова ошибка. Переименовываете функцию в $mol_dateFormat2 — снова работает. Всё становится ясно — при обращении ко глобальной функции/классу/переменной с таким странным именованием происходит поиск пути, соответствующего частям имени. И если находится такая директория — подключаются скрипты из неё.


Отменив последние переименовывания, вы, с чувством полного удовлетворения коммитите изменения и идёте в столовую праздновать столь быстрое завершение задачи по коду, который вы увидели в первый раз в жизни, даже не читая никакой документации.


Безудержное чаепитие


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


  • Все объекты доступны по ссылкам из глобальной области видимости, а не спрятаны в замыканиях. Это позволяет разработчику легко и просто исследовать внутреннее состояние приложения.


  • Для долгоживущих объектов автоматически генерируются уникальные человекопонятные идентификаторы, которые одновременно являются и «javascript-путями» до них из глобальной области видимости, что гарантирует их уникальность.


  • Изменения всех состояний логируются, с указанием идентификаторов объектов, что позволяет в точности понять, где что произошло. Например, если вы включите вывод логов всех сообщений, в идентификаторах которых есть подстрока «task», то, при завершении задачи в ToDoMVC, вы увидите следующие сообщения:

> $mol_log.filter('task')
< "task"
12:27:36 $mol_state_local.value("task=1476005250333") push Object {completed: true, title: "Hello!"} Object {completed: false, title: "Hello!"}
12:27:36 $mol_app_todomvc.root(0).taskCompleted(0) obsolete
12:27:36 $mol_app_todomvc.root(0).taskTitle(0) obsolete
12:27:36 $mol_app_todomvc.root(0).taskCompleted(0) push true false
12:27:36 $mol_app_todomvc.root(0).taskRow(0).completer().DOMTree() obsolete
12:27:36 $mol_app_todomvc.root(0).taskRow(0).DOMTree() obsolete

  • Пространства имён в рантайме однозначно соответствуют структуре директорий в репозитории. Это гарантирует отсутствие конфликтов и даёт чёткое понимание как человеку, так и машине, где искать исходные файлы.


  • Весь код псевдосинхронен и разбит на небольшие функции, что упрощает его понимание. В следующем примере происходит неблокирующий запрос файла с текстами на нужном языке. Обратите внимание на полезный стектрейс, который не доступен при использовании «обещаний», «стримов» и тому подобных абстракций.

Псевдосинхронный код с полезным стектрейсом


Прыжок без парашюта

Представьте, что перед вами внезапно вырисовалась задача разработать кроссплатформенное приложение, которое бы одинаково хорошо чувствовало себя как на мощных десктопах с огромными экранами, так и на миниатюрных смартфонах, где пол экрана то и дело закрывает клавиатура. Ах да, и сделать это надо было ещё вчера, а у вас ноутбук сломался и вам выдали девственно чистую замену.


Итак, вы работаете в компании ACME (а если не работаете, то основываете свою) и вам нужно реализовать веб-приложение для гиковского социального блога HabHub. Для начала, вам нужно просто загружать с гитхаба статьи и показывать их единой лентой.


Первым делом вы устанавливаете необходимое программное обеспечение: Git, WebStorm, NodeJS и NPM.


Далее вы выкачиваете репозиторий со стартовым проектом MAM:


git clone https://github.com/eigenmethod/mam.git ./mam && cd mam

Содержит он лишь общие для всех пакетов конфиги:


  • .idea — настройки для WebStorm: форматирование кода, статические проверки, запуск локального сервера.
  • .editorconfig — настройки для других редакторов.
  • .gitignore — указывает какие файлы git должен игнорировать.
  • .pms.tree — указывает какой пакет из какого репозитория выкачивать. Пакеты выкачиваются сборщиком автоматически по необходимости.
  • package.json — настройки для NPM.
  • tsconfig.json — настройки для TypeScript компилятора.

Открыв проект в WebStorm, вы запускаете локальный сервер, кнопкой «Start» на панели инструментов, либо, если вы предпочитаете другой редактор, выполнив в консоли:


npm start

Далее вы создаёте для приложения директорию acme/habhub и кладёте в неё index.html, который будет служить точкой входа в ваше приложение:













Содержимое этого файла весьма типовое, разве что в атрибуте mol_viewer_root вы указываете класс компонента, который будет использован в качестве приложения. Да, компоненты на базе $mol_viewer настолько самодостаточные, что любой из них может быть отрендерен изолированно от остальных, как отдельное приложение.


Чтобы создать упомянутый компонент, вы создаёте файл ./acme/habhub/habhub.view.tree:


$acme_habhub $mol_viewer

После чего открываете http://localhost:8080/acme/habhub/ и убеждаетесь, что загружается чистая страница, а в консоли нет ни одной ошибки — это значит, что все необходимые файлы успешно сгенерировались и загрузились, а тесты не выявили проблем.


В Багдаде всё чисто


Язык описания компонент


view.tree — мощный и лаконичный декларативный язык описания компонент, позволяющий собирать одни компоненты из других, как из кубиков LEGO. Выучив этот не хитрый язык, любой верстальщик может создавать гибкие переиспользуемые компоненты, которые легко интегрируются в другие, без традиционного «натягивания вёрстки на логику». Вся логика пишется в отдельном файле view.ts и как правило не требует изменений во view.tree, что позволяет программисту и верстальщику работать над одними и теми же компонентами, не мешая друг другу. Это достигается за счёт намеренного ограничения: вы не можете просто взять и вставить div в нужном месте. view.tree требует, чтобы вы использовали компоненты и (самое главное!) каждому из них давали уникальные имена. Фактически $mol_viewer просто создаст div при рендеринге в DOM, но в перспективе рендеринг может быть в графический холст, нативные компоненты или даже в excel файл.


Типичный сценарий создания компонента верстальщиком выглядит так (на примере компонента показывающего ненавязчивый лейбл над блоком):


Сперва он пишет демо-компоненты, которые являются примерами использования реализуемого компонента:


- Label over simple text
$mol_labeler_demo_text $mol_labeler
    title @ \Provider
    content @ \ACME Provider Inc.

- Label over string form field
$mol_labeler_demo_stringer $mol_labeler
    title @ \User name
    content $mol_stringer
        hint < hint @ \Jack Sparrow
        value > userName \

Потом, собственно реализует его:


$mol_labeler $mol_viewer
    childs /
        < titler $mol_viewer
            childs /
                < title
        < contenter $mol_viewer
            childs /
                < content null

А потом открывает страницу, где выводятся все демо компоненты и добавляет стили, глядя на все варианты использования компонента одновременно:


[mol_labeler_titler] {
    color: var(--mol_skin_passive_text);
    font-size: .75rem;
}

Многих смущает необычный синтаксис. То же самое можно было бы написать используя более привычный синтаксис XML:





    User name

    

        
            Jack Sparrow
        

        
            
        

    


Но он весьма громоздкий; во главе угла у него типы компонент, а не их имена в контексте родительского компонента; некоторые символы в строках требуют замены на xml-entities; велик соблазн просто скопипастить кусок вёрстки, без компонентной декомпозиции. Всё это приводит к осложнению работы с кодом и его поддержки, и поэтому в $mol используется именно синтаксис Tree, оптимально подходящий для задачи.


Небольшая шпаргалка по view.tree:


Объявление/использование компонента состоит из 3 частей:


  1. Имя компонента/свойства
  2. Имя базового компонента
  3. Список (пере)определяемых свойств

$ — префикс имён компонент. Данный префикс используется везде, кроме css, где он не допустим.


\ — с этого символа начинаются сырые данные. Содержать они могут любые символы (кроме символа конца строки), без какого-либо экранирования. Чтобы встравить несколько строк, нужно добавить символ \ перед каждой.


@ — вставленный между именем свойства и сырыми данными, он указывает вынести текст в файл с локализованными строками.


/ — объявляет список. Вставляйте элементы списка на отдельных строках с дополнительным отступом.


* — объявляет словарь. Сопоставляет текстовым ключам произвольные значения. Ключ не может содержать пробельные символы.


< — односторонне связывание (не путать с одноразовым). Указывает, что свойство слева (принадлежащее компоненту слева) должно брать значение из свойства справа (принадлежащее определяемому компоненту).


> — двустороннее связывание (не путать с обработчиками событий). Указывает, что в качестве свойства слева, должно быть взято свойство справа.


# — произвольный ключ. Указывает, что первым параметром свойство принимает некоторый ключ


Числа, логические значения и null выводятся как есть, без каких-либо префиксов.


Складываем кирпичики


Разобравшись в языке view.tree вы продолжаете пилить социальный блог. Прежде всего вы решаете, что у вас будет типичная раскладка страницы в виде шапки и скроллящейся области. Для этого вы используете готовый компонент $mol_pager:


$acme_habhub $mol_pager
    title \HabHub
    body /
        \Hello HabHub!
    footer null

Шапка и контент


Отлично! В теле страницы должны быть статьи. Статьи на GitHub пишутся в формате markdown, поэтому вы добавляете пару примеров статей, используя компонент для визуализации markdown — $mol_texter:


$acme_habhub $mol_pager
    title \HabHub
    body < gisters /
        < gister1 $acme_habhub_gister
            text \
                \# Hello markdown!
                \
                \*This* **is** some content.
        < gister2 $acme_habhub_gister
            text \
                \# Some List
                \
                \* Hello from one!
                \* Hello from two!
                \* Hello from three!
    footer null

$acme_habhub_gister $mol_texter

[acme_habhub_gister] {
    margin: 1rem;
}

Несколько демо карточек в теле


Супер! Теперь вы убираете жёсткий код и оставляете лишь формулу создания карточки статьи по её номеру:


$acme_habhub $mol_pager
    title \HabHub
    body < gisters /
    gister# $mol_texter
        text < gistContent# \
    footer null

Пришло время загрузить данные. Вы создаёте файл habhub.view.ts и пишете несколько мантр, которые позволят вам переопределить поведение уже созданного компонента:


namespace $.$mol {

    export class $acme_habhub extends $.$acme_habhub {

    }

}

Прежде всего вы описываете формат в котором от сервера приходят статьи:


interface Gist {
    id : number
    title : string
    body : string
}

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


uriSource() {
    return 'https://api.github.com/search/issues?q=label:HabHub+is:open&sort=reactions'
}

А теперь вы задаёте свойство, которое будет возвращать собственно данные, делая запрос к серверу через модуль $mol_http_resource_json, предназначенный для работы с json-rest ресурсами:


gists() {
    return $mol_http_resource_json.item<{ items : Gist[] }>( this.uriSource() ).json().items
}

Далее вы формируете карточки для показа статей по числу этих статей через свойство gister#, которое вы объявили ещё во view.tree:


gisters() {
    return this.gists().map( ( gist , index ) => this.gister( index ) )
}

gister# обращаясь к gistContent# передаёт ему тот же ключ, что передан и ему, так что осталось лишь задать, как по номеру статьи сформировать её содержимое:


gistContent( index : number ) {
    const gist = this.gists()[ index ]
    return `#${ gist.title }\n${ gist.body }`
}

В результате у вас получается следующего вида презентатор:


namespace $.$mol {

    interface Gist {
        id : number
        title : string
        body : string
    }

    export class $acme_habhub extends $.$acme_habhub {

        uriSource(){
            return 'https://api.github.com/search/issues?q=label:HabHub+is:open&sort=reactions'
        }

        gists() {
            return $mol_http_resource_json.item<{ items : Gist[] }>( this.uriSource() ).json().items
        }

        gisters() {
            return this.gists().map( ( gist , index ) => this.gister( index ) )
        }

        gistContent( index : number ) {
            const gist = this.gists()[ index ]
            return `#${ gist.title }\n${ gist.body }`
        }

    }

}

Код в целом тривиальный и в тестировании не нуждается: uriSource возвращает константу, правильность обращения gists к стороннему модулю проверит typescript компилятор, gisters тривиален и опять же проверяется компилятором, и только gistContent содержит нетривиальное формирование строки, поэтому вы пишете на него тест в habhub.test.ts:


namespace $.$mol {
    $mol_test({

        'gist content is title + body'() {

            const app = new $acme_habhub

            app.gists = ()=> [
                {
                    id : 1 ,
                    title : 'hello' ,
                    body : 'world' ,
                }
            ]

            $mol_assert_equal( app.gistContent( 0 ) , '# hello\nworld' )

        }

    })
}

Перезагрузив страницу, вы обнаруживаете в консоли:


Неправильно формируется текст


Ага, пробел потерялся. Исправив код, вы перезагружаете страницу и видите сначала индикатор загрузки, а потом собственно статьи.


Ожидание статей с GitHub


Статьи с GitHub


Блеск! Проверив, своё маленькое приложение на корректность, вы с чувством полного удовлетворения коммитите изменения и идёте в столовую праздновать столь быстрое завершение задачи, предполагающее неблокирующие запросы, визуализацию markdown и л

© Habrahabr.ru