Идеальный UI фреймворк

Здравствуйте, меня зовут Дмитрий Карловский, и я… архитектор множества широко известных в узких кругах фреймворков. Меня никогда не устраивала необходимость из раза в раз решать одни и те же проблемы, поэтому я всегда стараюсь решать их в корне. Но прежде, чем их решить, нужно их обнаружить и осознать, что довольно сложно находясь в плену привычек, паттернов, стереотипов и "готовых" решений. Каждый раз сталкиваясь с проблемами в реализации задачи, я задумываюсь "что, блин, не так с этим инструментом?" и, конечно же, иду пилить свой инструмент: функцию, модуль, библиотеку, фреймворк, язык программирования, архитектуру ЭВМ… стоп, до последнего я ещё не докатился.


Речь сегодня пойдёт о JS-фреймворках. Нет, я не буду рассказывать про очередное готовое решение, не в том цель поста. Я лишь хочу посеять в ваших головах несколько простых идей, которые вы не встретите в документации ни к одному популярному фреймворку. А в конце мы постараемся сформировать видение идеальной архитектуры построения пользовательского интерфейса.


Взгляд под другим углом


Длинный цикл отладки


Типичный цикл отладки выглядит так:


  • Редактирование кода.
  • Запуск приложения.
  • Проверка и обнаружение проблем.
  • Исследование их причин.

И цикл этот повторяется для любой опечатки. Чем быстрее разработчик поймёт где и почему ошибся, тем быстрее он всё реализует. Поэтому обратную связь программист должен получать как можно быстрее, прямо в процессе написания кода. Тут помогают средства среды разработки, которые в реальном времени анализируют набранный код и проверяют, будет ли это всё работать. Как следствие, очень важно, чтобы среда разработки могла получить из кода как можно больше как можно более конкретной информации. Чтобы этого добиться, используемый язык должен быть статически типизирован настолько на сколько это возможно. JavaScript же типизирован лишь динамически, от чего IDE пытается угадать типы по косвенным признакам (типичные паттерны, JSDoc-и), но, как показывает практика, даже у наиболее продвинутых сред разработки это получается плохо.


WebStorm не понимает JavaScript


Однако, существует минималистичное расширение JavaScript, добавляющее в него опциональную статическую типизацию — TypeScript. Его отлично понимает даже, какой-нибудь простой текстовый редактор типа GitHub Atom.


Atom понимает TypeScript


На текущий момент TypeScript является наиболее оптимальным языком для разработки веб приложений. Разработчики AngularJS это уже поняли. Не опоздайте на поезд!


Вторым эшелоном в деле ускорения отладочного цикла идут автоматизированные тесты, позволяющие быстро проверить работоспособность и обнаружить место неисправности. К сожалению, многие фреймворки для тестирования сконцентрированы лишь на первой фазе (проверка), но о второй (локализация неисправности) частенько даже и не задумываются, хотя она не менее важна. Например, популярный тестовый фреймворк QUnit оборачивает все тесты в try-catch, чтобы не останавливаться на первом же упавшем тесте, а в конце нарисовать красивый отчёт, который довольно бесполезен в деле поиска причин неисправности. Всё, что он может выдать — название теста и некликабельный стектрейс. Но есть костыль — вы можете добавить в ссылку параметр ?notrycatch, который не всегда работает, и тогда тесты по идее должны свалиться на первой же ошибке, после чего в зависимости от режима отладки вы получите либо остановку отладчика в месте возникновения исключения, либо кликабельный стектрейс одного упавшего теста в консоли. Но идеальным было бы решение без костылей: в режиме останова на исключениях — останавливаться на каждом (а не только на первом), а в режиме логирования — логировать все падения в консоль с кликабельным стектрейсом. Это не так сложно, как может показаться — достаточно запускать тесты в отдельных обработчиках событий, и ни в коем случае не заворачивать их в try-catch.


В свете вышесказанного стоит подчеркнуть, что try-catch лучше не использовать не только в тестовом фреймворке, но и в любом другом, ведь перехватывая исключение вы теряете возможность остановиться отладчиком в месте его возникновения. Единственное разумное применение try-catch в JavaScript — это игнорирование ожидаемых исключений, как например, делает jQuery при старте, проверяя поддержку браузером некоторых фич. И именно поэтому такой костыль, как опция отладчика "останавливаться не только на не перехваченных исключениях" плохо помогает, так как даёт слишком много ложных срабатываний, которые приходится проматывать.


Последним гвоздём в гроб try-catch можно забить тот факт, что как минимум V8 не оптимизирует функции, содержащие эту конструкцию.


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


Именно из соображений удобства отладки в последней реализации атомов нет ни одного try-catch, зато есть обработка события error, по которому происходит возобновление синхронизации атомов с учётом упавших.


Где что лежит?


Детективные расследования


Как быстро перейти к объявлению сущности? Как быстро найти все места использования сущности? Как найти объявление сущности по имени? Эти детективные расследования снижают продуктивность разработчика и отвлекают от решаемой задачи. Статическая типизация, как было замечено выше, позволяет среде разработки понимать семантику кода: где какой тип объявлен, какой тип возвращает функция, какие типы я могу использовать в текущем контексте. Но не менее важно, располагать и именовать файлы по простым и универсальным правилам, потому как работа с файлами происходит не только и не столько средствами среды разработки. Например, группировать файлы имеет смысл не по типу, а по функциональности. Модули с родственными функциями — в более крупные модули. А коллекции разнообразных модулей одного автора — в пакеты. Именно эта логика заложена в архитектуре PMS (Package/Module*/Source). В ней, иерархия директорий в точности повторяет иерархию пространств имён в коде. Благодаря этому по имени сущности всегда можно понять где она должна лежать. Это свойство используется pms-сборщиком для построения дерева зависимостей модулей: он анализирует исходники на предмет использования сторонних модулей, потом сериализует полученный граф так, чтобы к моменту исполнения зависимого модуля, все его зависимости тоже были исполнены. И для этого не требудется каких-то специальных объявлений в коде. Никаких AMD, LMD, CommonJS. Никаких import, require, include. Вы просто пишете код, как если бы необходимые модули уже были объявлены в том же файле, что и ваш, а обо всём остальном позаботится сборщик. Это прямой аналог автозагрузки классов из PHP, но работает с любыми модулями, содержащими исходники на самых разнообразных языках.


Благодаря тому, что зависимости между модулями отслеживаются автоматически, становится очень просто добавлять новые модули и переносить существующие. Для создания модуля достаточно создать директорию. Для добавления в него кода на любом языке достаточно просто создать файл. Но самое главное — в релиз уходят только те модули, которые реально используются, а не все подряд. Это позволяет строить код фреймворка и библиотек не из нескольких крупных модулей, 90% функций которых не используется, а из множества микроскопических модулей, не приводя ко километровым портянкам инклудов. Для сравнения: JavaScript код ToDoMVC на Angular2 в общей сложности весит 1.6МБ (320КБ в сжатом виде), а на $mol — 140КБ (22КБ в ужатопережатом виде). Сравните масштабы. Надо ли рассказывать какое приложение быстрее откроется на мобилке через EDGE? И не говорите, что сейчас 4G везде — в Москве даже EDGE во многих местах ловится с перебоями. А что нам предлагает современная индустрия? Подключать библиотеки целиком, а потом хитрой магией вырезать всё лишнее? Пожалейте мой CPU!


Микромодульная архитектура позволяет создавать компактные и быстрые приложения.


У многих сейчас наверняка уже поднялись руки выразить килобайты праведного гнева и возмущения моим непрофессионализмом в комментариях. Но позвольте отмотать время на несколько лет назад и напомнить об одной похожей ситуации. Ещё не так давно в тренде были XML технологии, все активно писали на XHTML, трепетали перед перед XSLT, а данными обменивались исключительно через XML. Если ты не ставил "/" в конце бестелесных тэгов или, упаси боже, не проходил html-валидацию, то на тебя смотрели как на профнепригодного. Но как-то больно много сложностей было с этим стеком технологий. Приходилось читать километровые спецификации, мириться с жёсткими ограничениями, вставлять кучу костылей, а светлое будущее всё не наступало — браузеры так и не довели поддержку XHTML до ума. Недовольство разработчиков росло, энтузиазм угасал, пока внезапно не пришло понимание, что с убогим JSON (по сравнению с мощным XML) работать проще и, главное, быстрее; что строгость и избыточность XML не даёт особого профита; что то, во что верили тысячи разработчиков, оказалось не самой лучшей идеей. Похожая ситуация была и с вендорными префиксами в CSS: сначала их копипастили руками, потом появились специальные утилиты, которые копипастят их автоматически, а теперь от них методично избавляются, так как они не решают никаких проблем, внося лишь излишнюю сложность. Но вернёмся к "модулям". Давайте напишем пару простых модулей на, например, RequireJS:


// my/worker.js
define( function( require ) {
    var $ = require( 'jQuery' )
    return function () {
        $('#log').text( 'Hello from worker' )
        return 1
    }
} )

// my/app.js
define( function( require ) {
    var jQuery = require( 'jQuery' )
    var worker = require( 'my/worker' )
    var count = 0
    return function () {
        $('#log').text( 'Hello from app' )
        count += worker()
        count += worker()
    }
} )

Что не так с этим кодом:


  1. Один и тот же модуль (который jQuery) в разных файлах имеет разные локальные имена. То есть программисту нужно постоянно держать в голове как jQuery называется в каждом модуле. Зачем нам возможность по разному именовать одну и ту же сущность? Чтобы всех запутать?
  2. У нас нет простого доступа к переменной count. Мы не можете открыть консоль и просто набрать app.count, чтобы узнать какое там сейчас значение. Для этого необходимо изрядно пожонглировать отладчиком.
  3. Каждый раз используя какую-либо сущность, нужно проконтролировать, чтобы она была "импортирована", а переставая её использовать надо удалить и эти "импорты". Существование специального инструмента для автоматической синхронизации списка импортов с кодом, подчёркивает их бессмысленность — это типовой, легко автоматизируемый инфраструктурный код, до которого программисту, вообще говоря, нет никакого дела.
  4. Много лишнего кода, который зачастую просто генерируется по шаблону, так как писать руками одно и то же, никто не любит.

Как решить эти проблемы, не создавая новых? А очень просто, воспользуемся форматом jam.js:


// jq/jq.js

// my/worker.jam.js
var $my_worker = function () {
    $jq('#log').text( 'Hello from worker' )
    return 1
}

// my/app.jam.js
var $my_app = function () {
    $jq('#log').text( 'Hello from app' )
    $my_app.count += $my_worker()
    $my_app.count += $my_worker()
}
$my_app.count = 0

Кода получилось существенно меньше и весь он по делу, при этом для работы с ним нам уже не обязательно нужна мощная среда разработки. Нам не приходится ломать голову в каком модуле как названа jQuery, ведь называется она везде одинаково — в соответствии с путём до неё в файловой системе. При этом мы всегда можем скопировать $my_app.count в консоль, чтобы посмотреть текущее состояние. Вот к чему стоит стремиться, а не к изоляции всего и вся.


Перила помогают не упасть в пропасть, но не стоит ими себя окружать.


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


Раздувание кода


Bloat boat


Если раньше были споры стоит ли грузить jQuery на сотню килобайт, то сейчас уже никого не смущает подключение мегабайтного фреймворка. Некоторые даже гордятся тем, что написали миллионы строчек кода для реализации не слишком сложного приложения. И если бы проблема была только в скорости загрузки этих слонов, да скорости их инициализации. Но есть и куда более существенные вытекающие из этого проблемы. Чем больше кода, тем больше в нём ошибок. Более того, чем больше кода, тем больше процент этих ошибок. Так что если вы видите огромный зрелый фреймворк, то можете быть уверенными, что на отладку таких объёмов кода была потрачена уйма человекочасов. И ещё уйму предстоит потратить, как на существующие ещё не замеченные баги, так и на баги, привносимые новыми фичами и рефакторингами. И пусть вас не вводит в заблуждение "100% покрытия тестами" или "команда высококлассных специалистов с десятилетним опытом" — багов точно нет лишь в пустом файле. Сложность поддержки кода растёт нелинейно по мере его разрастания. Развитие фреймворка/библиотеки/приложения замедляется, пока совсем не вырождается в бесконечное латание дыр, без существенных улучшений, но требующее постоянное увеличение штата. Так что, если кто-то предложит вам написать дополнительный код по превращению ВерблюжьихИдентификаторовДиректив в идентификаторы-директив-с-дефисами, только потому, что в JS традиционно используется одна нотация, а в CSS — другая, с ней не совместимая, то плюньте ему в лицо и воспользуйтесь универсальной_нотацией_идентификаторов, не требующей ни дополнительного кода, ни среды разработки со сложными эвристическими алгоритмами поиска соответствий.


Большой объём кода далеко не всегда означает большое число возможностей. Зачастую это следствие использования слишком многословных инструментов, переусложнённой логики и банальной копипасты (в том числе и кодогенерации). Не стоит впадать в крайности, давая всем переменным однобуквенные имена, но стоит насторожиться, видя десятки тысяч строк рисующие простую формочку на экране. Вы можете не обращать на это внимание, полагая, что вам не потребуется в нём разбираться, ограничившись лишь чтением документации; полагая, что публичного API вам хватит для любых хотелок; полагая, что багов либо нет, либо они будут быстро исправляться мейнтейнерами. Но практика показывает, что рано или поздно вам придётся лезть в эту груду кода, если не вносить изменения, то как минимум исследовать его работу. А чем больше кода, тем сложнее в нём разобраться. Но ещё сложнее разобраться в коде, изобилующем множеством абстракций. Небольшое число, грамотных абстракций позволяет значительно упростить код, но мы получаем резко противоположный эффект, когда лепим их без разбора. Фабрики, прокси, адаптеры, реестры, сервисы, провайдеры, директивы, декораторы, примеси, типажи, компоненты, контейнеры, модули, приложения, стратегии, команды, роутеры, генераторы, итераторы, монады, контроллеры, модели, отображения, модели отображения, презентаторы, шаблоны, билдеры, виртуальный дом, грязные проверки, биндинги, события, стримы… Во всём этом многообразии абстракций теряются даже опытные разработчики. А попробуйте объяснить новичку, как на таком фреймворке сделать простой компонент так, чтобы не плакать потом кровавыми слезами, глядя на то, что у него получилось.


Чем проще — тем лучше.


Сложность разработки


Сизифов труд


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


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


Реактивная архитектура значительно упрощает поддержку.


Зачастую при разработке фреймворков основное внимание уделяется таким вещам как "простота", "гибкость" и "скорость", но практически игнорируется такое немаловажное качество как "исследуемость". Предполагается, что взявшийся работать с ним разработчик уже прочитал всю документацию, понял её правильно и вообще очень одарённый человек. Но правда жизни заключается в том, что документация зачастую не полна, плохо структурирована, на не родном языке и требует очень много времени для полного изучения, которого всегда не хватает, да и разработчик зачастую не имеет за плечами десяти лет опыта и досконального знания паттернов. Поэтому лучшая документация — это примеры. Лучшие примеры — это тесты. А лучший способ понять как оно работает — разобрать. Так что важно не изолировать внутреннее состояние, а предоставлять простой и удобный доступ к нему. Через консоль, через отладчик, через логи. Необходимо именовать одни и те же сущности одинаково в разных местах: имена переменных в разных модулях, имена модулей в разных контекстах, имена классов в скриптах, стилях и вёрстке и тд. А также необходимо добавлять информацию о не очевидных связях между сущностями. Например, посмотрите на этот код:


<div class="my-panel">
    <button class="my-button_danger"></button>
</div>

Как тут можно догадаться, что оба этих элемента были добавлены в дерево компонентом my-page? Добавим недостающую информацию:


<div class="my-page_content my-panel">
    <button class="my-page_remove my-button_danger"></button>
</div>

Теперь стало понятно куда копать, и кто виноват в том, что кнопка удаления страницы находится не на той панели. Другой яркий пример связан с парсингом. Когда вы применяете JSON.parse, то теряете информацию о расположении данных в исходном файле. Поэтому, когда при последующей валидации вы обнаруживаете ошибку, то не можете сообщить пользователю "На такой-то строке обнаружен не валидный e-mail", а вынуждены изобретать костыли вида "Невалидный e-mail по пути departaments[2].users[14].mails[0]". Напротив, при использовании формата tree вы всегда можете получить из узла информацию о месте его объявления:


core.exception.RangeError@./jin/tree.d(271): Range violation
./examples/test.jack.tree#87:20 cut-tail
./examples/test.jack.tree#87:11 cut-head
./examples/test.jack.tree#88:7 body
./examples/test.jack.tree#85:6 jack
./examples/test.jack.tree#83:0 test
./examples/test.jack.tree#1:1

Хлебные крошки не помешают как в пользовательском интерфейсе, так и в программном коде.


Компонентная декомпозиция


LEGO


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


AngularJS


AngularJS


Объявление компоненты:


angular.module( 'my' ).component( 'panel' , {
    transclude : {
        myPanelHead : '?head',
        myPanelBody : 'body'
    },
    template: `
        <div class="my-panel">
            <div class="my-panel-header" ng-transclude="head"></div>
            <div class="my-panel-bodier" ng-transclude="body">No data</div>
        </div>
    `
} )

Для объявления компоненты нам потребовалось написать немного скриптов, немного шаблонов и приложить к ним некоторый конфиг, описывающий API. Получилось весьма многословно и запутанно. Необходимо держать в голове, что ng-transclude содержит не просто текст, а имя параметра, значение которого будет вставлено внутрь элемента. Нужно внимательно поддерживать маппинг внешних имён параметров на внутренние. Нужно всем элементам расставить классы, чтобы их можно было стилизовать. А теперь попробуем воспользоваться этой компонентой:


<body ng-app="my">
    <my-panel>
        <my-panel-head>My tasks</my-panel-header>
        <my-panel-body>
            <my-task-list
                assignee="me"
                status="todo"
            />
        </my-panel-body>
    </my-panel>
</body>

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


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


angular.module( 'app' ).component( 'myPanelExt' , {
    scope : {
        myPanelShowFooter : '='
    },
    transclude : {
        myPanelHead : '?head',
        myPanelBody : 'body',
        myPanelFoot : '?foot'
    },
    template: `
        <div class="my-panel" my-panel-show-footer="true">
            <div class="my-panel-header" ng-transclude="head"></div>
            <div class="my-panel-bodier" ng-transclude="body">No data</div>
            <div class="my-panel-header" ng-transclude="foot" ng-if="myPanelShowFooter"></div>
        </div>
    `
} )

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


angular.module( 'app' ).component( 'myPanelExt' , {
    transclude : {
        myPanelHead : '?head',
        myPanelBody : 'body',
        myPanelFoot : '?foot'
    },
    template: `
        <div class="my-panel">
            <div class="my-panel-header" ng-transclude="head"></div>
            <div class="my-panel-bodier" ng-transclude="body">No data</div>
            <div class="my-panel-header" ng-transclude="foot"></div>
        </div>
    `
} )

ReactJS


ReactJS


Объявление компоненты:


class MyPanel extends React.Component {
    render() { return (
        <div class="my-panel">
            <div class="my-panel-header">{this.props.head}</div>
            <div class="my-panel-bodier">{this.props.body}</div>
        </div>
    ) }
}

Уже гораздо лучше, хотя и осталась по прежнему неотделимая завязка на JS. Почему это плохо? Потому, что не везде JS является оптимальным языком программирования. В вебе у вас нет выбора. Но под iOS лучше бы подошёл Swift или ObjectiveC, под Android — Java, а под десктопы выбор языков вообще огромен, но на JS свет клином не сошёлся. По прежнему мы имеем проблему жёсткости и изолированности компоненты, так что с кастомизацией всё почти так же плохо, как и в AngularJS. "Почти", потому, что мы можем расчленить наш шаблон на мелкие кусочки, что позволит в дальнейшем их переопределять:


class MyPanel extends React.Component {
    header() { return <div class="my-panel-header">{this.props.head}</div> }
    bodier() { return <div class="my-panel-bodier">{this.props.body}</div> }
    childs() { return [ this.header() , this.bodier() ] }
    render() { return <div class="my-panel">{this.childs()}</div>
}

class MyPanelExt extends MyPanel {
    footer() { return <div class="my-panel-footer">{this.props.foot}</div> }
    childs() { return [ this.header() , this.bodier() , this.footer() ] }
}

Мы получили неплохую гибкость, но потеряли наглядность иерархии элементов. А использование XML синтаксиса в этом случае становится излишним. Особенно экстравагантно выглядело бы использование компоненты без расчленения на функции:


class MyApp extends MyPanel {
    render() { return (
        <MyPanel
            head="My Tasks"
            body={
                <MyTaskList
                    assignee="me"
                    status="todo"
                />
            }
        />
    ) }
}

Polymer


PolymerJS


Объявление компоненты:


<dom-module id="my-panel">
    <template>
        <div class="header">
            <content select="[my-panel-head]" />
        </div>
        <div class="bodier">
            <content />
        </div>
    </template>
    <script>
        Polymer({
            is: 'my-panel'
        })
    </script>
</dom-module>

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


<link rel="import" href="../../my/panel/my-panel.html">
<dom-module id="my-app">
    <template>
        <my-panel>
            <div my-panel-head>My tasks</div>
            <my-task-list
                assignee="me"
                status="todo"
            />
        </my-panel>
    </template>
    <script>
        Polymer({
            is: 'my-app'
        })
    </script>
</dom-module>

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


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


<dom-module id="my-panel-ext">
    <template>
        <div class="header">
            <content select="[my-panel-head]" />
        </div>
        <div class="bodier">
            <content />
        </div>
        <div class="footer">
            <content select="[my-panel-foot]" />
        </div>
    </template>
    <script>
        Polymer({
            is: 'my-panel-ext'
        })
    </script>
</dom-module>

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


Сферический идеальный фреймворк


$mol


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


Как показано выше, XML совсем не удовлетворяет этим требованиям. Вообще, все попытки сделать из XML шаблонизатор выглядят как помесь ужа с ежом. То XML встраивается в JS, то JS встраивается в XML, а то и изобретается свой примитивный язык программирования с XML-синтаксисом.


Давайте воспользуемся tree-синтаксисом для объявления простой компоненты:


$my_panel $mol_view

Тут мы просто говорим, что компонент $my_panel — наследник от $mol_view. Нас не волнует как и на каком языке реализован $mol_view, но мы утверждаем, что $mol_panel должен быть реализован точно так же. Например, приведённое выше описание, может развернуться в следующее DOM-дерево:


<div id="$my_panel.Root(0)" my_panel mol_view></div>

Как можно заметить, для элемента были автоматически сгенерированы некоторые атрибуты. Прежде всего это id — глобальный идентификатор компонента. Он не зря имеет такой странный вид, ведь его можно скопировать и вставить в консоль, получив тем самым прямой доступ к инстансу компонента, за этот элемент ответственного. Это очень сильно упрощает отладку и исследование чужого кода. Далее идут атрибуты, предназначенные прежде всего для стилизации. Вы можете определить стилизацию для базового компонента, а потом перегрузить её для наследника:


[mol_view] {
    animation : mol_view_showing 1s ease;
}

[my_panel] {
    animation : none;
}

Как видите, нам не потребовался даже css препроцессор, чтобы реализовать наследование в CSS. То есть, для декларации наследования у нас есть только одно место в коде — это описание компоненты. И не нужно отдельно наследоваться в JS, отдельно в CSS, отдельно в HTML. По этому описанию также может быть сгенерирован и TypeScript-класс для этого компонента:


module $.$$ {
    export class $my_panel extends $.$mol_view {
    }
}

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


$my_panel $mol_view
    head /
    body /
        \No data
    Head $mol_view
        sub <= head /
    Body $mol_view
        sub <= body /
    sub
        <= Head -
        <= Body -

Тут мы объявили свойства head и body, представляющие собой содержимое шапки и тела панели. После имени мы указали значение по умолчанию. Чтобы компонента была самодостаточной, значения по умолчанию должны быть всегда. Далее, мы определили свойства Head и Body, значения которых по умолчанию являются инстансами $mol_view с перегруженным свойством sub — оно будет замещено соответствующим свойством из определяемого компонента. Соответственно, при использовании панели мы можем переопределить любое из этих свойств, достигнув тем самым очень высокой гибкости:


$my_app $my_panel
    head /
        \My tasks
    body /
        <= Tasks $my_task_list
            assignee \me
            status \todo

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


$my_panel $mol_view
    sub /
        <= Head $mol_view
            sub <= head /
        <= Body $mol_view
            sub <= body /
                \No data

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


<div id="$my_panel.Root(0)" my_panel mol_view>
    <div id="$my_panel.Root(0).Head()" mol_view my_panel_head></div>
    <div id="$my_panel.Root(0).Body()" mol_view my_panel_body></div>
</div>

В итоге мы сохранили иерархию, не потеряв в гибкости. Создадим вариант панельки с подвалом:


$my_panel_ext $my_panel
    sub /
        <= Head -
        <= Body -
        <= Foot $mol_view
            sub <= foot /

Но что если нам нужна динамика? Например, показывать подвал в зависимости от какого-либо условия. Нет ничего лучше для описания логики, чем язык программирования. Поэтому воспользуемся TypeScript, чтобы добавить логику к декларативному описанию компоненты:


namespace $.$$ {
    export class $my_panel extends $.$my_panel {

        @ $mol_mem
        foot_showed( next = false ) { return next }

        sub(){
            return [
                this.Head() ,
                this.Body() ,
                ... this.foot_chowed() ? [ this.Foot() ] : [] ,
            ]
        }

    }
}

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


$my_panel_demo $mol_demo_large
    sub /
        <= Simple $mol_panel
            foot_showed false
            head /
                \I am simple panel
        <= Footered $mol_panel
            foot_showed true
            head /
                \I am panel with footer

А так как это описание в конечном счёте будет транслировано в typescript класс, то мы автоматом получим и статическую типизацию. И хотя IDE без специальных плагинов всё же не сможет вывести контекстные подсказки во view.tree коде, компилятор, тем не менее, проверит, что мы не используем необъявленные свойства, передаём им правильные типы значений и тп.


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


В статьях про каждый второй фреймворк пишут, что он разрабатывается с прицелом на производительность, и что тот чуть ли не самый быстрый из всех. Иногда даже приводят в доказательство какие-нибудь синтетические тесты. Как правило код в таких тестах весьма далёк от того, что можно было бы встретить в реальном приложении. В погоне за попугаями теряется простота, гибкость, компонуемость, настраиваемость. Не редко можно встретить чуть ли не полный отказ от возможностей фреймворка ради скорости. Результат такого надругательства над кодом — производительность, которую можно достигнуть приложив знания, умения и время. Но куда важнее не максимальная скорость, которую можно достичь, а минимальная, которую вы получите написав идиоматичный для данного фреймворка код. Чем выше минимальная скорость, тем реже вам придётся браться за напильник, тем менее квалифицированные (и как следствие — менее дорогие) требуются разработчики, тем быстрее идёт разработка. Поэтому фреймворк, на котором сложно сделать медленно, предпочтительнее фреймворка, на котором можно сделать быстро.


Типичный jQuery рендеринг выглядит так: берём шаблон, генерируем новый HTML подставляя в него данные, вставляем в дерево, навешиваем события. Проблема этого подхода в том, что даже малейшее изменение данных требует повторного рендеринга всего шаблона. Это не заметно на малых объёмах данных, но рано или поздно данных становится много и всё начинает тормозить. А что более существенно, при ререндеринге сбрасываются скроллинги, введённые в поля значения и тому подобные вещи. Представьте, что вы пришли побриться, а цирюльник убивает вас, берёт ДНК и выращивает вашего клона без бороды. Решается это обычно вручную — находим нужные элементы и патчим их, что приводит к большому объёму очень хрупкого кода.


Всё, что делает ReactJS — это ускоряет рендеринг всего шаблона за счёт того, что рендерит его не напрямую в DOM дерево, а в промежуточное дерево JS объектов, которые уже сравниваются с реальным деревом и при наличии расхождения точечно меняет DOM. Уже лучше, конечно, но всё равно куча лишней работы: цирюльник берёт вашу ДНК, выращивает клона без бороды, раздевает вас обоих до гола и, педантично сравнивая волосяной покров, с хирургической точностью делает вас похожим на вашего клона, после чего клона отправляют в биореактор. Похожим образом поступает AngularJS с той лишь разницей, что сравнивает он не ваш клон и вас самих, а фотографии какого-то мужика вчера и сегодня, и если обнаруживает, что у того пропала борода, то сбривает её и вам. А что творится в Polymer после 100500 транспиляций и представить сложно.


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


Но вернёмся к бенчмаркам. Производительность всё же надо контролировать. Причём важна производительность не отдельных подсистем в рафинированных условиях, а общая производительность итогового приложения. Поэтому мы воспользуемся для тестирования широко известной коллекцией одного и того же приложения на разных фреймворках — ToDoMVC. Так как основная задача этого проекта — демонстрация идиоматичного кода на разных фреймворках, то он отлично подходит для интегральной оценки их производительности. По сети разбросано множество версий бенчмарка с ToDoMVC приложениями, поэтому пришлось попотеть, собирая из разных форков один ToDoMVC Benchmark реализованный через специальный интерфейс для быстрого написания крутых бенчмарков.


Производительность быстрых web-ui-фреймворков


Производительность медленных web-ui-фреймворков


Каждое приложение прогоняется через 2 теста:


  1. Создание 100 задач через поле создания задач. Каждая следующая задача создаётся через setTimeout, что не позволяет фреймворку срезать путь и схлопнуть 100 операций рендеринга в одну. Визуально это выглядит как постепенное разрастание списка задач от 0 до 100. Этот шаг показывает насколько дорого фреймворку обходится формирование интерфейса.
  2. Удаление задач по одной, нажатиями на кнопки удаления задачи. Тут опять используется setTimeout для предотвращения схлопывания 100 операций рендеринга в одну. Этот шаг показывает эффективность фреймворка по уничтожению интерфейса. Визуально это выглядит как постепенное исчезновение задач сверху списка. Такое удаление задач гарантирует, что все строки рано или поздно будут отрендерены, даже если фреймворк рендерит лишь компоненты, попадающие в видимую область.

Также стоит отметить, что каждое приложение прогоняется ровно один раз в отдельном фрейме, что не позволяет ему кешировать какие-либо объекты.


А теперь уберём из тестирования совсем аутсайдеров и увеличим число задач вдвое:


Производительность быстрых web-ui-фреймворков на большем числе задач


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


Неправильный выбор фреймворка может замедлить ваше приложение более чем в 10 раз.


Призыв к действию


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


Основные характеристики


  • Минимум абстракций и соглашений.
  • Статическая типизация.
  • Реактивная архитектура.
  • Высокая эффективность.
  • Компактный размер.
  • Простота компоновки.
  • Высокая кастомизируемость компонент.
  • Высокая исследуемость и отлаживаемость.
  • Разработка множества библиотек/приложений в одной кодовой базе.

Документация


Живые примеры

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Ну что, за каким кактусом будущее?

Поделиться публикацией

Комментарии 122

    –3
    vue.js
      0
      Пока на Angular 1.*, а там видно будет, насчет TypeScript — не сложится ли ситуация как с CoffeScript? И jQuery не фреймворк вовсе.
        +5
        В крупных проектах есть острая потребность в статическом выведении типов. В мелких, она не такая острая, но тоже приносит не мало пользы.
        Микрософт очень грамотно спозиционировала TypeScript — это современные спецификации ECMAScript + типы. И ничего лишнего. В отличие от CoffeScript или Dart, меняющих язык до неузнаваемости.
          0

          Внезапно, flow!

            +2

            Он от тайпскрипта отличается очень незначительно. Разве что тайпскрипт — это ещё и транспайлер, а не только тайпчекер, что является преимуществом, так как:


            1. Набор поддерживаемых фичей должен быть одинаковым в транспиляторе и тайпчекере.
            2. Не надо по нескольку раз парсить JS, что ускоряет транспиляцию и уменьшает число точек потенциального сбоя.
            3. Есть возможность билдить инкрементально, что ещё сильнее ускоряет билд при разработке.
            4. Не нужно настраивать и дружить между собой несколько инструментов.
              0
              Flow создает так называемый Flowgraph, который представляет собой граф всего приложения — он запоминает, какие модули связаны друг с другом. TypeScript не предлагает ничего подобного, потому что это слишком большая задача, Flow заходит дальше. Flow определяет, отрезаны ли большие части приложения от остальной части кода, и предлагает удалить их! Джефф Моррисон много говорил об этом на ReactEurope.

              @mxstbr

                0
                TypeScript не предлагает ничего подобного, потому что это слишком большая задача
                Любой статически типизированный язык строит граф всего приложения в процессе компиляции. Иначе он просто бы не смог проверять типы.

                Flow определяет, отрезаны ли большие части приложения от остальной части кода, и предлагает удалить их!

                Думаю и к тайпскрипту это вскоре прикрутят. А вообще, лучше бы они объединились, и вместе пилили JS++, а не конкурировали, вынуждая разработчиков выбирать между двумя похожими технологиями.

          –1
          Не сложится потому что в ECMAScript даже следующих версиях не предвидится типизации (до них все не дойдет что есть вещи поважнее чем их "сахар"), а TypeScript это и есть ECMAScript + опциональная типизация. Можно считать это расширением ECMAScript.
          +7
          Если вы, как и я, устали от этих всё более усложняющихся инструментов… то присоединяйтесь к разработке простого и эффективного сферического фреймворка

          Напрашивается картинка про 14 конкурирующих стандартов
            0
            Кстати, да, надо ещё реализовать адаптеры к остальным фреймворкам, чтобы можно было легко интегрировать в них сферические компоненты.
              0
              Да, создатели картинки про стандарты не учли адаптеры, надо создать еще одну картинку, на которой есть адаптеры :)
                –1
                Если бы каждый год не изобретали новый стандарт передачи данных, мы бы до сих пор подключали периферию через COM порты и по пол часа выводили одну фоточку на печать.
                  +1
                  У таких железных штук есть простые метрики, по которым их можно сравнивать. Другими словами, специалист всегда точно скажет когда нужно использовать SPI, а когда — PCI Express. Я так понимаю, что с UI фреймворками ситуация несколько иная, иначе этого поста вообще не было бы.
                    –2
                    У фреймворков тоже есть простые метрики — бенчмарки. Но и у портов есть дискуссионные характеристики — распространённость, перспективность, надёжность.
                      +4
                      Бенчмарки показывают только производительность. При этом, надо сильно постараться, что бы тормозил именно UI фреймворк, а не сосбтвенно бизнесс-логика. По вашему же бенчмарку видно, что большая часть фреймворков справилась с задачей мене чем за 7 секунд, учитывая, что было совершенно порядка 400 действий, можно посчитать что на одно действие тратилось не более 7/400 = 0.0175 секунды. Это чуть больше времени отображения одного кадра при 60 FPS (0.0166 c). По исследованиям Роберта Миллера, время реакции интерфейса меньше чем 0.1с считается мгновенным. Поэтому, даже если все эти фреймворки будут в 5 раз медленее — катастрофы не произойдет.

                      Получается, что бенчмарк — это хорошо, но он не показывает особого преимущества Angular Light над React.

                      В аппаратных интерфейсах (то что вы назвали "портами") вы кажется несколько некомпетенты. Каждый интерфейс выполняет свою задачу и к примеру нет дискуссий о надежности или перспективности. Тот же UART (который вы обозвали COM-портом), спокойно трудится в вашем смартфоне, соединяя чип Bluetooth с центральным процессором.
                      Что действительно стоит сравнивать — так это максимальную скорость, количество сигнальныйх линий, требования к линиям, ограничения на длину, устойчивость к помехам, требования к наличию PHY и т.д. Всё это можно выразить в числах и сравнить.

                      Новые интерфейсы изобретают просто потому что новые технологии позволяют сигналы быстрее и точнее.
                        +2
                        Обычно действия несколько сложнее, чем отрендерить 4 дива. А если вам нужно выполнять действие во время скролла страницы (ленивая подгрузка данных, например), то все действия должны укладываться в весьма жёсткие рамки, иначе будут заметны рывки. Кроме того вы не учитываете, что кроме ноутбуков за несколько килобаксов тем же интерфейсом пользуются и со смартфонов баксов за 100. Так что любые разговоры про абсолютные значения в достаточно простом тесте не имеют никакого смысла. Смысл имеет только один факт: там, где один фреймворк уже тормозит, другой ещё не тормозит.

                        Ангуляр, например, без конца дёргает функцию фильтрации (которая — бизнес-логика, ага) только для того, чтобы узнать не изменился ли список выводимых строк, потому как он не знает от чего он зависит, а от чего — нет. Реакт будет на каждый чих пытаться перерендерить весь виртуальный дом, если мы не понапишем костылей в виде shouldComponentUpdate. В приветмирах всё это отрабатывает быстро, но когда это всё начинает тормозить — вам уже некуда деваться. И повляются статьи по оптимизациям в духе "не используете $timeout", "делайте одноразовые биндинги", "добавляйте debounce" и тп.

                        Я говорил вот об этих портах.
            +2
            При этом каждый вложенный элемент получил уникальное имя

            … где же я это видел? А, конечно. asp.net Web Forms. Гарантированно уникальный идентификатор для каждого контрола.
              0
              Да так-то такой подход много где используется. Я использую фреймворк Yii, там есть нечто похожее. Генерация простая, вида "'w' .+ $counter++" ('w' от слова widget). А в web forms это как делается?
                +3
                Да почти так же, магические константы и счетчики на каждом уровне. Другое дело, что это одна из тех вещей, за которые вебформзы ненавидели.
                  0
                  Почему ненавидели?
                    0
                    Во-первых, получившиеся идентификаторы были гигантские и неудобоваримые.
                    А во-вторых, получались они на сервере и слабопредсказуемо, поэтому работать с ними из клиентского кода было адски неприятно.
                      0
                      В предлагаемом мной решении, идентификаторы обоснованно длинные. Помимо идентификационной функции они несут также и отладочную. Ну и в целом человекопонятны. Например, по идентификатору $mol_app_todo.app().taskRow(4).dropper() сразу понятно, что это кнопка удаления задачи в четвёртой по счёту строке в приложении mol_app_todo.
                        0
                        В вебформзах тоже было понятно, что где. К сожалению, недостатков это не перевешивало.
                          0
                          Из упомянутых вами недостатков, я вижу тут только один — длинна. Зато достоинств — масса.
                            0
                            Все зависит от того, кто работает с объектами по этим идентификаторам. Если только сами компоненты, то пофиг. А вот если идентификаторы используются снаружи — вот тогда начинается цирк.
                              0
                              Какой, например, цирк?
                                0
                                А как мне, находясь снаружи компонента — получить идентификатор какого-нибудь поля внутри компонента?
                                  0
                                  А зачем?
                                    0
                                    Например, если я хочу сделать с ним что-нибудь инструментами, отличными от вашего фреймворка?
                                      0
                                      Какими, например?

                                      Ну, допустим, так:

                                      var cellId = row.cell('title').objectPath
                                      var cellNode = row.cell('title').node().get()
                                        0
                                        Какими, например?

                                        Ванильным JS, например.

                                        var cellId = row.cell('title').objectPath

                                        Неплохо, но печально. Печально по двум причинам: во-первых, мне надо как-то догадаться, что objectPath — это идентификатор, назначенный HTML-элементу, и, во-вторых, на строковом идентификаторе все прелести статической проверки умерли.
                                            0
                                            Все равно же генерить надо — так почему бы не генерить нормальные свойства?
                                            0
                                            Так что вы хотите сделать на ванильном JS? В норме, фреймворк должен абстрагировать от реальных узлов, потому как этих реальных узлов может и не быть в случае нативных компонент.

                                            Не так уж это и сложно, догадаться:

                                            'title' — это ключ, для создания множества типовых объектов. Ничто не мешает сделать по отдельной фабрике для каждой ячейки:

                                            var cellId = row.cellTitle().objectPath
                                            var cellNode = row.cellTitle().node().get()

                                            Ну, или использовать энумерацию, как предложила Сферрка.
                                              0
                                              В норме, фреймворк должен абстрагировать от реальных узлов, потому как этих реальных узлов может и не быть в случае нативных компонент.

                                              Вооо, вот мы и вернулись к стартовому "Все зависит от того, кто работает с объектами по этим идентификаторам. Если только сами компоненты, то пофиг."
                                                0
                                                Я не понимаю, что вы пытаетесь доказать. Впрочем, как обычно.
                                                  +2
                                                  Ну вот есть JSF. Он для компонентов генерирует свои id. Причём, значение id при работе с компонентами в рамках самого фреймворка и id, которые получаются после рендера — разные.

                                                  Пока всё укладывается в рамки JSF, всё хорошо. Можно просто получить id компонента.
                                                  Но если вдруг так случалось, что нужно было получить правильный id уже отрендеренного компонента, начинались костыли.

                                                  Как видно, эти проблемы не только в вебформах, а в подходе в целом.
                0
                Странно что в 2016 году вы приводите синтаксис Polymer 0.5 при том, что версия 1.0 доступна уже давно, а вчера вышла 1.3.
                В 1.x всё ещё нет наследования, только композиция. А [select] это не хак, это фактический стандарт веб-компонентов (в v1 версии спецификации веб-компонентов заменено на именованные слоты, что немного отличается, но в целом более практично).
                  –1
                  Не подскажете, как эти примеры будут выглядеть на последней версии полимера?
                  А в какой версии оно есть?
                  Это хак в том смысле, что берётся одно дерево и оно видоизменяется. При этом происходит жёсткая завязка на конкретную структуру этого дерева.
                    +1
                    На самом деле ваш пример не сильно отличается, там такие вещи как data-binding и подобные сделали более простыми и явными:

                    <dom-module id="my-panel">
                        <template>
                            <div class="header">
                                <content select="[my-panel-head]" />
                            </div>
                            <div class="bodier">
                                <content />
                            </div>
                        </template>
                        <script>Polymer({is: 'my-panel'})</script>
                    </dom-module>

                    Нет больше полностью декларативного объявления компонентов, как минимум то, что я написал выше нужно сделать, потом оно найдет модуль с id соответствующим is и таким образом инициализирует компонент.
                    Также в 1.x получше дела с производительностью, много интересных фич в плане стилей — CSS variables & CSS mixins везде.

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

                    Наследования пока нет вообще (если не считать прототипы в локальных ветках отдельных разработчиков), обещают давно, не уверен на сколько оно сейчас готово. Слоты есть в спецификации, Polymer их не внедряет пока не появятся в стабильных версиях популярных браузеров.
                      0
                      Спасибо, поправил статью.
                  0
                  Критика в адрес Polymer выглядит неубедительно, очень советую автору копнуть его глубже.
                    0
                    С полимером всё оказалось куда хуже, чем я думал изначально. Или вы считаете отсутствие наследования, избыточность кода и теряющиеся в нём имена параметров не убедительными аргументами?
                      0
                      Наследование есть, с именами параметров все ок, со стилями там тоже все ок (можно использовать инкапсуляцию а можно общие стили) избыточность присуща любому фреймворку, ну и вообще по всем пунктам все неверно. А главное, что по сути вообще не важно что там внутри Полимера, потому как это только надстройка над веб-компонентами и вам ничего не мешает реализовать любые "финты" и хитромудрые архитектуры имея очень гибкий инструмент для декомпозиции. Прямо сейчас я делаю сложнейшее (в плане интерфейса) приложение на Polymer, и даже не знаю что бы я без него делал. До этого я делал приложения на React и Angular, поэтому мне есть с чем сравнивать.
                        0
                        Покажите же как реализуется наследование, не будьте голословными. Ну и было бы интересно узнать что это за сложнейшее приложение такое.
                        +1
                        Или вы считаете отсутствие наследования

                        А вам точно нужно наследование в UI-фреймворке?
                          0
                          Мне нужна кастомизация готовых компонент. Какие-то блоки удалить, какие-то добавить, какие-то перенести в другое место, какие-то по другому раскрасить. Знаете способы реализации этого без наследования?
                            +1
                            Конечно, знаю. Точки расширения, шаблоны, стратегии — первое, что в голову пришло. Наследование для кастомизации, на самом деле, далеко не идеально подходит — слишком неочевиден баланс между is-a и has-a.
                              0
                              Вы не в теме проблематики. Типичная ситуация — нужно прикрутить календарик. Берём готовое оупенсорсное решение и используем. Всё хорошо, но вот потребовалось нам, например, скрыть селектор года (ибо год в нашем контексте всегда один и тот же), а вместо него поставить кнопку "на следующей неделе", которая автоматически устанавливает дату на ближайший рабочий день после текущей недели. Варианты действий:

                              1. Форкнуть и героическими усилиями за неделю добавить недостающие точки расширения, шаблоны, стратегии, параметры или что там ещё может прийти в голову. Потом ещё долго огребать от несовместимостей с основной веткой.
                              2. Оставить фичереквест мейнтейнерам и надеяться, что до дедлайна они соизволят необходимый функционал реализовать.
                              3. Сказать заказчику, что это не возможно с готовыми решениями и требуется пара недель на разработку своего велосипеда.
                              4. Написать мартышкин патч, который после рендеринга пробегается по DOM-у и приводит его к нужному виду. Обычно так и делают.
                              5. Отнаследоваться и перегрузить необходимые аспекты поведения. Такое мало где поддерживается.
                                +1
                                Вы не в теме проблематики.

                                Вам, конечно, виднее.

                                Отнаследоваться и перегрузить необходимые аспекты поведения.

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

                                    Другое дело, что у вас, конечно, есть точки расширения, но уверены ли вы, что любое вписанное в них поведение не сломает компонент? Например, была у вас табличка, вы хотите переопределить общий генератор таблички, но оставить генераторы строчек — как (конечно же, не глядя в исходный код) это сделать?
                                      –1
                                      Затем, чтобы создать свой компонент на 90% похожий на оригинал. Да, на разных страницах нужны разные календарики.

                                      Перегрузить генератор таблички (rows) и не трогать генератор строчек (row). Все компоненты создаются через фабрики во владеющих их компонентах.
                                        0
                                        Затем, чтобы создать свой компонент на 90% похожий на оригинал.

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

                                        Перегрузить генератор таблички (rows) и не трогать генератор строчек (row).

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

                                        Если очень грубо, то:

                                        type Table =
                                          //...
                                          member this.Render context =
                                            html {
                                              yield! RenderHeader
                                              yield! Rows |> Seq.concat (fun r -> r.Render(context))
                                              yield! RenderFooter
                                            }
                                        

                                        Нас интересует фрагмент Rows |> Seq.concat (fun r -> r.Render(context)). Если мне нужна фильтрация, то я пишу this.Rows |> Seq.filter ... |> Seq.concat (fun r -> r.Render(context))), а если переопределение отображения — то:

                                          for (i, r) in Rows do
                                            yield "<tr>"
                                            yield! ["<td>"; i; "</td>"]
                                            yield! r.Cells |> Seq.concat (fun c -> c.Render(context))
                                            yield "</tr>"
                                        

                                        А вот теперь, собственно, вопрос: как не получить при этом код, который ломается от любого случайного чиха?
                                          0
                                          Берете свой компонент, включаете внутрь него нужный компонент, кастомизируете как надо, нужные операции делегируете.
                                          Именно так и работает наследование.

                                          Не понял, где у вас там что ломается. Но код, конечно, жуткий.
                                            +2
                                            То что lair описал — это не наследование, а декоратор:

                                                 public class BaseClass
                                                 {
                                                       public void DoSmthng(obj param)
                                                       {
                                                             .....
                                                       }
                                                 }
                                            
                                                 public class Wrapper
                                                 {
                                                       private BaseClass _innerObj = new BaseClass();           
                                            
                                                       public void DoSmthng(obj param)
                                                       {
                                                             ...
                                                             _innerObj.DoSmthng(param);
                                                       }
                                                 }     
                                              –2
                                              Это реализация наследования через декоратор + не упомянутые точки расширения. Что выглядит как наследование и ведёт себя как наследование — это и называют наследованием. Давайте оставим терминологические споры и вернёмся в практическое русло. Я продемонстрировал простое решение, позволяющее легко и просто кастомизировать готовый компонент, не привлекая 100500 паттернов и не превращая код в груду копипасты вида public void DoSmthng(obj param){ _innerObj.DoSmthng(param); } ради непонятно чего. Есть идеи как сделать проще и гибче? Буду рад их услышать.
                                                0
                                                то выглядит как наследование и ведёт себя как наследование

                                                Оно не ведет себя как наследование: вы не можете (в строго типизированных языках) использовать Wrapper вместо BaseClass.

                                                Собственно, вот и различие is-a и has-a.
                                                  0
                                                  Вы запутались в паттренах :-) Это Адаптер я не могу использовать, так как меняется интерфейс, а Декоратор реализует тот же самый интерфейс. В том его и суть.
                                                    0
                                                    В примере выше нет "того же самого интерфейса", так что лично я ни в чем не запутался.
                                              0
                                              Именно так и работает наследование.

                                              Нет, так работает композиция. Вы правда не знаете разницы, или просто шутите так?

                                              Не понял, где у вас там что ломается.

                                              Ну давайте еще раз, на пальцах.

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

                                              GenerateTable
                                                GenerateHeader
                                                for each row
                                                  GenerateRow
                                                GenerateFooter
                                              

                                              В каких-то случаях надо вмешаться то, какие строчки генерятся (т.е., поменять логику for each), а в каких-то — в то, как они генерятся (т.е., заменить вызов GenerateRow на что-нибудь другое).

                                              Так вот, в типичном "компоненте" GenerateTable и GenerateRow связаны между собой настолько сильно, что вмешательство в один требует постоянной сверки с другим (это, кстати, не проблема наследования, это проблема кастомизации вообще). Как вы решаете эту проблему?
                                                0
                                                Допустим у нас такое описание:

                                                $my_stats : $mol_tabler childs
                                                    < header : $mol_view childs < foot : null
                                                    < rows : null
                                                    < footer : $mol_view childs < foot : null

                                                Тут у нас объявлены следующие свойства: childs, header, head, footer, foot, rows. Можете перегружать любое из них.

                                                rows реализуется как-то так:

                                                rows() { this.prop( () => {
                                                    var next = []
                                                    for( var i = 0 ; i < 100 ; ++i ) next.push( this.row( i ).get() )
                                                    return rows
                                                } ) }

                                                row — просто фабрика, например:

                                                row( id : number ) { return (new $mol_view).setup( _ => {
                                                    _.child = () => this.prop( id )
                                                } ) }

                                                Её тоже можно переопределить.
                                                  0
                                                  … и когда я переопределяю rows, я должен повторить большую часть этого замечательного кода ради измения, скажем в var i = 0 ; i < 50 ; ++i?
                                                    0
                                                    Если число строк захардкожено прямо в коде, то, очевидно, да. А вы ожидали чуда?
                                                      +2
                                                      Ну да, вы же обещали чудо — "решение, позволяющее легко и просто кастомизировать готовый компонент". А я вам и показываю, что если компонент спроектирован без точки расширения в нужном вам месте, у вас не выйдет легко и просто.

                                                      Что же касается необходимости наследования: если header, rows и footer будут не перегружаемыми методами, а свойствами, содержащими функции, то каждый из нх все равно останется точкой расширения:

                                                      new table
                                                      {
                                                        header = () => {/* custom header generation */},
                                                        rows = () => {/* custom header generation */},
                                                        footer = () => {/* custom header generation */}
                                                      }

                                                      Ну да, а еще бывает вот так:```cs
                                                      Html.Table()
                                                      .Header(() => {//})
                                                      .Footer(() => {//})
                                                      .RowTemplate(item => {//})

                                                      И тоже никакого наследования.
                                                      
                                                      Но самое интересное все равно начинается в тот момент, когда вы поменяли поведение контрола (того же календаря), а теперь хотите, чтобы оно поменялось по всей системе (например, чтобы рабочие дни у вас помечались с понедельника по субботу), включая те места, где этот контрол создаете не вы, а другие контролы (например, в заголовке колонки грида есть кнопка "Фильтр", по которой для колонок с типом "дата" выпадает календарь). И вот если вы <i>это</i> хотите сделать легко и просто, то наследование вам не поможет (точнее, его недостаточно).
                                                        0
                                                        Речь шла не о всех возможных видах кастомизаций, а о кастомизации порядка и состава вложенных компонент.

                                                        В JS/TS методы — это поля содержащие функции. То, что вы реализовали — это тоже форма наследования, мартышкин патч. Она менее эффективна, чем через классы.

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

                                                        Я вам скажу страшную вещь, но грид не должен ничего знать про типы колонок — всё это инъектируется извне.
                                                          0
                                                          а о кастомизации порядка и состава вложенных компонент.

                                                          А там будут те же проблемы. Не вынесли конкретную "компоненту" в свойство — здравствую, копипаста.

                                                          То, что вы реализовали — это тоже форма наследования, мартышкин патч.

                                                          Нет, это не наследование — объект остается того же типа. Это как раз чистая кастомизация.

                                                          Так что я вполне в состоянии перегрузить фабрику создания календариков, чтобы она создавала инстанс другого класса.

                                                          В каждом компоненте будете ее перегружать (и молиться, чтобы каждый компонент ее выставлял)? Или будете надеяться, что все компоненты смотрят на общую фабрику (и используют один интерфейс)?

                                                          Я вам скажу страшную вещь, но грид не должен ничего знать про типы колонок — всё это инъектируется извне.

                                                          Грид как минимум знает про типы колонок, которые он умеет отображать. Метаданные конкретного набора данных он действительно получает снаружи.
                                                            0
                                                            Не вынесли конкретную «компоненту» в свойство —
                                                            — свалится при компиляции :-) Во view.tree создавать подкомпоненты можно исключительно через именованные фабрики.

                                                            Нет, это не наследование — объект остается того же типа. Это как раз чистая кастомизация.
                                                            В JS/TS все объекты «одного типа». Различия лишь в составе и значениях полей.

                                                            В каждом компоненте будете ее перегружать (и молиться, чтобы каждый компонент ее выставлял)? Или будете надеяться, что все компоненты смотрят на общую фабрику (и используют один интерфейс)?
                                                            Можно во все компонентны инъектировать и общую фабрику. Если уж надо совсем-совсем гарантированно везде, то проще, просто заменить исходный класс своим.

                                                            Грид как минимум знает про типы колонок, которые он умеет отображать.
                                                            Мой грид не занимается отображением колонок. Что я делаю не так? :-)
                                                              0
                                                              Во view.tree создавать подкомпоненты можно исключительно через именованные фабрики.

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

                                                              В JS/TS все объекты «одного типа».

                                                              Даже в JS это не так, а уж в TS — тем более. Ну и да, о какой тогда строгой типизации вы пишете в своем посте?

                                                              Можно во все компонентны инъектировать и общую фабрику.

                                                              Можно. Для этого вам придется налагать на все компоненты очень жесткие требования (которые при этом сложно статически валидировать).

                                                              Мой грид не занимается отображением колонок. Что я делаю не так

                                                              Называете гридом не то же самое, что другие люди.
                                                                0
                                                                Так я просто не стану считать то, что вы хотите кастомизировать, компонентой — и, как следствие, не буду ничего никуда выносить.
                                                                Во view.tree вы не сможете этого сделать. Но во view.ts вы, конечно, можете сотворить любое непотребство, если сильно заморочиться.

                                                                Даже в JS это не так, а уж в TS — тем более. Ну и да, о какой тогда строгой типизации вы пишете в своем посте?
                                                                Вы плохо знаете JS :-) В TS — структурная типизация.

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

                                                                  Ну вот видите.

                                                                  В TS — структурная типизация.

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

                                                                  Или наоборот, не превращаю простую абстракцию табличного вывода данных в кухонный комбайн

                                                                  Так вы не путайте "абстракцию табличного вывода" и грид. Это два разных уровня.
                                  0
                                  +1
                          +5
                          Вы не честно "выиграли" бенчмарк, у меня есть несколько притензий к вашему приложению "mol"

                          1) Урезанный функционал, например нет переключения активных/удаленных задач
                          2) Меньше DOM/HTML, упрощенный HTML дает до +30% скорости (400 DOM элементов вместо 800 на список).
                          3) Упрощенные стили (CSS), это дает до +15% скорости рендеринга
                          4) Хитрый способ сохранения в localStorage + JSON.stringify, дает до +10% скорости, мы тут не скорость конвертирования JSON меряем, сохранение должно быть везде одинаково, либо убрано вообще.

                          Так что если вы приведете свое приложение в соответствие с остальными, то результат может заметно просесть.

                          Кстати ваше приложение как то не так работает в FireFox (как минимум у меня), не видно тасков в todo листе в момент тестирования. Судя по результатам вы в нем и тестировали.

                          А в Chrome у меня такие результаты после 3-х запусков (с последние версией Angular Light):

                            +6
                            Оло-ло, а я поверил автору на слово и смотрел только исходники (и судя по всему, которые не понял), а наделе там просто какое-то своё «todomvc» (http://nin-jin.github.io/todomvc/examples/mol/)

                            vintage, весь смысл проекта TodoMVC показать идентичный результат, через призму разных подходов. Вы не просто «упростили», как написал lega, вы даже не используете шаблоны и CSS предоставляемый TodoMVC. Ай-ай.
                              –2
                              1. Не дошли руки добавить, да, однако фильтрация через изменение адреса поддерживается. Это не сильно влияет на результат.
                              2. Если тот же функционал можно реализовать меньшим числом элементов — это разве не плюс? Да и HTML не столь уж и проще. В решении на $mol используется 4 элемента на строку. В решении на AL — 5. Возможно вас смутило, что в решении на $mol рендерятся лишь те строки, что попадают в видимую область. Ну так это тоже плюс, а не минус.
                              3. К сожалению, изначально стили там написаны через одно место, так что требуется их адаптация. Кроме того, там есть ещё и 2 версии дизайна. 15% к скорости отрисовки — возможно, но сомнительно. К общему времени выполнения — совсем крохи.
                              4. Мы меряем общую скорость работы идиоматичного кода. На одних фреймворках удобнее хранить все данные одним огромным JSON-ом, на других — отдельными парами ключ-значение. В этом нет ничего хитрого или нечестного.
                              5. Это прототип, а не готовое решение, в ИЕ и ФФ я ещё даже не запускал. Бенчмарки я гонял в Хроме. Там погрешность порядка 100мс, так что выигрывает то одно решение, то другое. Если последняя версия AL стабильно выигрывает, то жду пул-реквест. :-)
                                +1
                                Неважно как оно сверстано, я например не верстаю, а работаю с готовым html/css, который уже вылизан под все браузеры и прошел все цепочки принимающий, так что переверстывать уже некогда, да и бессмысленно, мой инструмент должен легко справиться с чем угодно. Вы же пошли по пути перепила под ваш инструмент, что наводит на определенные мысли.

                                Да, TodoMVC не образец красивой верстки, но тогда нужно было бы сделать два вариант, первый как есть, а второй свой, но который выглядит точно также как и первый, только «правильно». А дальше хоть статью пиши, как можно упростить и выиграть в скорости просто оптимизировав верстку.
                                  –1
                                  Я пошёл по пути более грамотного построения процесса: сначала пишется описание компоненты, потом верстальщик дизайнит её, глядя на неё во всех возможных состояниях, а тем временем программист добавляет логику. А не так, что верстальщик каждый раз присылает новый хтмл, а программист его без конца "натягивает".

                                  Визуализацию я подтяну, не волнуйтесь, внешне будет не отличимо.
                                    +1
                                    Я пошёл по пути более грамотного построения процесса:

                                    Это конечно правильно и хорошо, по возможности всегда так делаю, но прекрасно понимаю, что моё «правильно» и «правильно» другого разработчика, могут сильно различаться при работе с одним и тем же инструментом.

                                    А не так, что верстальщик каждый раз присылает новый хтмл

                                    Да, жизнь она такая и если для вас проблема использовать уже готовую верстку как есть, то возможно вам нужен другой инструмент.

                                    Визуализацию я подтяну, не волнуйтесь, внешне будет не отличимо.

                                    Вот с этого и нужно было начинать, только помимо визуализации, нужно и функционал привести в идентичное состояние.
                                      0
                                      Спасибо, что открыли мне глаза на то, как устроена жизнь :-) Тем не менее своё решения я позиционирую как то, на котором сложно сделать плохо (тормозно, нерасширяемо и тп), но легко сделать хорошо (эффективное обновление состояний — по умолчанию, точки расширения компонент — придётся, иначе не заработает, бем-аннотации элементов — автоматически).
                                        +1
                                        Я правильно понял, что если сейчас захочу попробовать $mol, мне придется с нуля переделать свои уже компоненты, потому что они «неправильные» и я просто не смогу загнать свою уже существующую вёрстку в него?
                                          –3
                                          А вы думали можно ничего не переписывая сменить парадигму программирования?
                                            +1
                                            Да, именно так я и думаю.

                                            При переходе на Angular, React, Meteor, Aurelia и так далее, я смогу использовать уже готовую верстку. Даже в Elm, можно загнать верстку или просто воспользоваться каким-нибудь elm-html.
                                              0
                                              Тут вы тоже можете забить на любезно предоставленные вам фреймворком поддерживающие наследование бэм-атрибуты и вручную хардкодить определённые классы при каждом наследовании/использовании. Только зачем? Понимаете ли, тут подход к разработке с другой стороны. Не оживление статического HTML, а собирание интерфейса из конструктора. И демонстрационные приложения показывают, как легко и просто собрать интерфейс приложения из готовых компонент.
                                  +2
                                  Если тот же функционал можно реализовать меньшим числом элементов — это разве не плюс?
                                  Это плюс, но для большинства фреймворков можно оптимизировать HTML, что даст лучшие показатели. В итоге получается соревнование кто лучше HTML подпилит, а не скорость фреймворка (хотя они и составят основную часть). Чтобы было честно нужно для всех оптимизировать HTML, а это накладно. Поэтому, я считаю, как минимум нужно выкладывать «каноническую» версию, ну и для интереса можно оптимизированную прикладывать.

                                  15% к скорости отрисовки — возможно, но сомнительно. К общему времени выполнения — совсем крохи.
                                  Общее время сокращается, например крайний случай — если отключить стили совсем то общее время выполнения снижается на ~30% (для AL).

                                  жду пул-реквест. :-)
                                  Да, я ещё оптимизированную версию запилю, интересно :)
                                    0
                                    И всё же DOM в принципе не может быть идентичным. У каждого фреймворка свои особенности по работе с ним и их надо учитывать. Коробочные фреймворки типа ExtJS или OpenUI5 не ориентированы на прямую работу с хтмл. Они предоставляют компоненты (кнопки, чекбоксы, инпуты), которые и надо использовать. Не использовать эти компоненты — это всё равно, что не использовать фреймворк. А тестировать надо всё же идиоматичный код, а не заточенный под тесты.

                                    У меня время вообще в 2 раза уменьшается, но тут дело не в стилях как таковых, а в размере области рендеринга (в маленьком окне отключение стилей почти не влияет).
                                0
                                Но нет ничего эффективней, чем просто показать цирюльнику фотографию чистого подбородка, удобно расположиться в кресле и быть уверенным, что всё, что тот сделает — это просто сбреет вам бороду.

                                Я правильно понимаю, что достигается это подпиской на конкретные поля данных и перерендером/патчингом частей шаблона, которые на эти данные завязаны?
                                  0
                                  Именно так.
                                  0
                                  Он не зря имеет такой странный вид, ведь его можно скопировать и вставить в консоль, получив тем самым прямой доступ к инстансу компоненты, за этот элемент ответственной
                                  Т.е. вы на каждый элемент создаете глобальную переменную?, это только в debug режиме?

                                  Сферический идеальный фреймворк
                                  Я правильно понял что все манипуляции вокруг tree формата (в основном) для того что-бы можно было наследовать шаблоны (и CSS)?, а JS/TS и так наследовать можно.
                                    0
                                    Т.е. вы на каждый элемент создаете глобальную переменную?, это только в debug режиме?
                                    Любой объект доступен по абсолютному пути, который и используется в качестве идентификатора. Это фича такая. Там ещё ссылки на объекты контролируются — когда на него не остаётся ссылок — он уничтожается. Вообще говоря, можно было бы сильно ускорить работу, если бы браузер предоставлял апи по отключению GC.

                                    Я правильно понял что все манипуляции вокруг tree формата (в основном) для того что-бы можно было наследовать шаблоны (и CSS)?, а JS/TS и так наследовать можно.
                                    Наследование, биндинги, минимизация визуального шума, возможность объявлять и использовать компоненты изучив лишь один простой язык (он проще чем html, но даёт больше возможностей, и гораздо проще чем js).
                                      +1
                                      биндинги

                                      Хотел по биндингам задать вопрос, но сам нашел, получается в вашем случае когда нужно сделать текстовый биндинг (пример из Ангуляра)

                                      Clear completed {{getCount()}}

                                      Вам нужно "переместить" этот кусок в js и "назначить" дополнительное имя для связывания, таким образом шаблон получается разорван на 2 части, часть в tree другая часть в ts, хотя это по видимому ViewModel, но проще это этого не стало, да и верстальщику от этого поплохеет — что-бы поправить текст на месте, нужно лести в дебри TS.
                                      Кстати это не гибко т.к. (по видимому) нельзя даже параметр в эту ф-ию передать (например: {{getCount('active')}}, {{getCount('removed')}} )

                                      минимизация визуального шума

                                      С учетом того что к view еще нужно портянку в TS писать, шума тут явно больше (чем в том же ангуляре).

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

                                      Думаю проще было-бы сделать какой-нибудь трансформер html, например это

                                      $my_panelExt : $my_panel childs
                                          < header
                                          < bodier
                                          < footer : $mol_block childs < foot : null

                                      Можно сделать как-то так:

                                      myPanelExt = htmlTransform(myPanel, {
                                        header: true,
                                        bodier: true, 
                                        footer: foot()
                                      })

                                      О! Я вспомнил, раньше модно было использовать XSLT, кажется вы изобретаете то же самое, только синтаксис другой.
                                        0
                                        Вам нужно «переместить» этот кусок в js и «назначить» дополнительное имя для связывания, таким образом шаблон получается разорван на 2 части, часть в tree другая часть в ts, хотя это по видимому ViewModel, но проще это этого не стало, да и верстальщику от этого поплохеет — что-бы поправить текст на месте, нужно лести в дебри TS.
                                        Текстами и не верстальщик должен заниматься. В общем случае тексты нужно будет переключать в зависимости от языка, не трогая шаблоны. Я ещё не продумывал этот аспект, но он точно не должен быть в шаблонах.

                                        Кстати это не гибко т.к. (по видимому) нельзя даже параметр в эту ф-ию передать (например: {{getCount('active')}}, {{getCount('removed')}} )
                                        Идея в том, что шаблон не знает ничего про програмный код. Всё, что делает шаблон — это предоставляет слоты. А уже программист засовывает в эти слоты всё, что нужно.

                                        С учетом того что к view еще нужно портянку в TS писать, шума тут явно больше (чем в том же ангуляре).
                                        Конкретно во view.tree его меньше, во view.ts ещё меньше сделать не получилось без ущерба для статической типизации.

                                        Вообщем мне кажется вы все усложнили в погоне за возможностью наследования, которая не часто и нужна (хотя может и нужна но не такой ценой).
                                        Кастомизация нужна, прям очень. view.tree поддерживает наследование и агрегацию с мартышкиными патчами.

                                        Думаю проще было-бы сделать какой-нибудь трансформер html
                                        И статическая типизация идёт лесом. И верстальщик будет счастлив на JS шаблоны делать.

                                        О! Я вспомнил, раньше модно было использовать XSLT, кажется вы изобретаете то же самое, только синтаксис другой.
                                        ответить
                                        Polymer куда ближе к XSLT, он делает выборки из деревьев по селекторам.
                                    0
                                    ...
                                      +1
                                      vintage Выложил оптимизированную версию (хотя она не шибко быстрее), github не позволяет сделать реквест именно на ваш репозиторий (опция в репе? либо из-за того что я clone сделал с вашего, а push в свой), поэтому коммит тут.
                                      На данный момент результат такой (если сравнить с предыдущим скрином, видно что цифры немного ходят туда сюда).

                                        0
                                        Думаю дело в том, что вы не сделали форк через github. Чуть позже перенесу патч и добавлю редизайн своей версии. Кстати, вы перевели на новый дизайн или всё ещё используется старый?
                                          +1
                                          Перезалил комит, сделал пул-реквест.
                                        –1
                                        Да, наверно это "мульки" гибхаба, чисто с гитом все прошло бы гладко. Дизайн старый.

                                        Ответ для https://habrahabr.ru/post/276747/#comment_8784117
                                          0
                                          Смёржил, убрав читы со схлопыванием сохранения. Оптимизированную версию вынес отдельно, но её пришлось пока дисквалифицировать, так как она не комплитит задачи. Ну а своё решение привёл к новому дизайну.
                                            0
                                            Текущие результаты на 100 задач:

                                              0
                                              Чуть чуть не дождались вы, вот исправленная версия: https://github.com/nin-jin/todomvc/pull/2
                                              А что значит "не комплитит задачи"? Вручную все работает (да и на тесте тоже).
                                                0
                                                А что значит «не комплитит задачи»
                                                Не правильно прочитал, хз, но сейчас все нормально работает.
                                                  +1
                                                  Обновил статью новыми результатами бенчмарков. Теперь самый быстрый — AL.
                                            +1
                                            Кому интересно, я тут на коленке запилил осображение графа зависимостей для компонент. Например, для ToDoMVC он сейчас выглядит так.
                                              –1
                                              Ещё один программист собрался написать ещё один новый фреймворк

                                              У меня вот на работе коллега написал ещё один JS-грид, он конечно по сути ничем не отличается от десятков других, но зато свой родной.

                                              Мне даже нравится поддерживать такие начинания, особенно когда ты знаешь чем всё закончится.
                                                +3
                                                Это всё, что вы почерпнули из статьи?
                                                0
                                                Как обычно, очень интересная статья, если не смотреть код :-) Оставляет двойственное впечатление — кладезь интересных мыслей и какие-то странно-закрученные реализации.

                                                Те же атомы — я написал свою реализацию в 160 строк и доволен как слон. Что касается фреймворков… нужна простота. минимальное количество кода, чтобы проще было скопипастить к себе исходники, чем читать документацию. Максимальная приближенность к голому хтмл и яваскрипту.
                                                  0
                                                  Не поделитесь своей реализацией в 160 строк? Я бы с радостью выкинул свои 410 :-)

                                                  Мой тезис в том, что html + javascript плохо подходят для написания ui-компонент, в отличие от view.tree + typescript. html позволяет лишь передать элементу в качестве параметров лишь набор текстовых пар "ключ-зачение" и не более одного анонимного html-поддерева. А требуется-то и несколько именованных поддеревьев передавать, от чего возникают костыли со вложенными элементами, выносом параметров в отдельные функции и выборками по селектору. js код сложно исследовать — только в отладчике можно узнать наверняка какие поля есть у того или иного объекта.
                                                    0
                                                    Тайпскрипт — да. Я обожаю его за то, что тайпскрипт — это яваскрипт с метаданными. Прозрачная кодогенерация и полная совместимость с яваскриптом не оставляет ни одной причины НЕ использовать его.

                                                    А вот html… пытаться от него изолироваться, имхо, бесполезное дело. Но я до сих пор не уверен, как делать правильно. Возможно, использование компонента не обязано быть единственным тегом? Как в старые добрые времена jquery — мы пишем разметку, а потом она "оживает". типа: <div class=tabs activeTab={activeTab}><div class=tab></div><div class=tab></div></div>. Не знаю.
                                                      0
                                                      Есть несколько мест, где тоже нужны компоненты, но они никак не связаны c DOM — рендеринг в canvas и нативные компоненты. Кроме того, есть такие штуки как всплывающие окна, которые рендериться должны не внутрь компонента, а вообще где-то в стороне, но при этом принадлежать этой компоненте (она контролирует время жизни всплывающего окна). Вы вот привели типичный костыль, когда часть параметров передаются в атрибутах, а часть во вложенных элементах. Предлагаемое мной решение избавляет от этой дилемы — все параметры передаются единообразно:

                                                      $my_app : $my_tab_list
                                                          activeTab < activeTab < first
                                                          child < tabs
                                                               < first : $my_tab
                                                               < second : $my_tab
                                                  +1
                                                  Добавил пример (на коленке собрал) нормального vdom движка, PR отправил.

                                                    0
                                                    Вынудили вы меня заняться оптимизацией :-)

                                                    100 задач:

                                                    image
                                                    400 задач:

                                                    image
                                                      0
                                                      Глупостью вы занимаетесь, тратите время, оптимизируете что-то, в то время когда я просто взял готовый инструмент, который легко можно заменить на другой. Я мог, например использовать Inferno, который быстрей citojs. Но главное, ещё все эти инструменты имеют тесты, поддержку и много где используются, а citojs так прекрасно в IE8 работает.

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

                                                      По поводу «табличного процессора». Под такую задачу, с вероятностью 100% это будет canvas, а ячейки пересчитывать в едином цикле. Хотя возможно я бы попробовал какие-то элементы RP, взял бы а какую-нибудь уже готовую библиотеку, вроде reactor.js, хотя думаю единый цикл будет эффективнее.

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

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

                                                      гибкая реализация с атомами, с меньшей алгоритмической сложностью.

                                                      По моему этом само обман, серьезно, неявные связи атомов, это бич всего RP, которая в итоге приводит к необоснованному росту обсерверов.

                                                      Опять же, если RP это прямо-таки «серебренная пуля», почему нет крупных игроков? Да, есть Метеор, но там всё совсем не так гладко, а Blaze выпиливают в угоду React. В итоге имеем только Rx, да Beacon.

                                                      P.S. Но, я всё равно желаю вам удачи, это хорошая тема для исследования, но я бы всё попытался отточить сами атомы и скомбинировать их уже с каким-то готовым решением, а не пилить абсолютно всё своё, это путь в никуда.
                                                        0
                                                        $mol ещё на стадии прототипа. А по вашей логике авторы inferno и citojs тоже глупостями занимаются, ибо вместо того, чтобы просто взять готовый и протестированный react, пилят что-то своё, оптимизируют. Что ж вы, кстати, не воспользовались inferno?

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

                                                        Зачем тут canvas? И каким таким образом canvas спасёт вас от необходимости перевычислять значения всех ячеек на каждый чих? Может и от stack overflow он как-то поможет при длинных зависимостях между ячейками? Ну а касательно reactor.js — вы фактически признали ущербность подхода "перевычисляем всё на каждый чих", ибо reactor.js — это крайне урезанная версия атомов.

                                                        Вы видите основания для тормозов и утечек по мере роста приложения в случае $mol_view? Я бы с радостью про них послушал. Аргумент "сперва добейся" пропустим.

                                                        Опять же, можете обосновать откуда может взяться необоснованный рост обсерверов? Может я не вижу каких-то очевидных вещей? Может не всегда A зависит от B и C при A=B+C?

                                                        А может беда не в RP, а в том, что метеор имеет кривую архитектуру?

                                                        Комбинировать атомы легко с чем угодно. Для интероперабельности атомы поддерживают интерфейсы обсерверов (on) и промисов (then,catch). На прошлом проекте я, например, комбинировал их с angular. Правда сам ангуляр был как пятая нога — зачем делать грязную проверку всего скоупа, если и так известно какие свойства поменялись?
                                                          0
                                                          А по вашей логике авторы inferno и citojs тоже глупостями занимаются, ибо вместо того, чтобы просто взять готовый и протестированный react

                                                          Нет, citojs не замена, а дополнение, его можно использовать в качестве альтернативы React.createElement.

                                                          Что ж вы, кстати, не воспользовались inferno?

                                                          Нет поддержки IE8.

                                                          Ну а касательно reactor.js — вы фактически признали ущербность подхода "перевычисляем всё на каждый чих", ибо reactor.js — это крайне урезанная версия атомов.

                                                          Как это признал, я же написал, что буду использовать cavnas и единый цикл, как раз перерисовка всего и вся на каждый чих. А cavnas, потому что DOM под такую задачу не годиться, слишком медленный и не удобный. Сейчас даже проверил, Spreadsheets гугловый наконец-то переделали на canvas, вроде меньше тормозить стал. Осталось Docs ещё переделать, уверен что они либо это уже делают, либо сделают скоро.

                                                          Вы видите основания для тормозов и утечек по мере роста приложения в случае $mol_view?

                                                          Мне и не нужно это видеть, многое говорит банальное отсутствие покрытия тестами и реальных приложений. И в целом JS не любит обилие функций, поэтому kvo плетется позади банального «перерисуем всё».

                                                          Аргумент "сперва добейся" пропустим.

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

                                                          Опять же, можете обосновать откуда может взяться необоснованный рост обсерверов?

                                                          Мне сейчас тяжело сформировать пример чтобы показать что я имею ввиду. Главное я не имел ввиду, что атомы кто-то криво работают, просто если я правильно понимаю, при их использовании они образую две связи «от кого завися» и «кто от них зависит», вот тут и «проблема» (в кавычках).

                                                          А может беда не в RP, а в том, что метеор имеет кривую архитектуру?

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

                                                            Как вы ловко перескакиваете с обсуждения архитектуры на конкретные решения и обратно, а также сравниваете полноценный фреймворк с библиотекой для рисования. Судя по тестам, что я привёл выше, недоделанный и толком даже не оптимизировавшийся $mol_view уделал citojs на 400 задачах. Это называется "kvo далеко позади"? Есть такой показатель как "алгоритмическая сложность". И у vdom сложность обновления зашкаливает O(DataSize*AppComplexity*DOMSize). В случае атомов она куда меньше O(SubAppComplexity).

                                                            Реализация на citojs: 6.2кб без учёта транспиляции в js и собственно citojs.
                                                            Реализация на $mol_view: 5кб без учёта транспиляции и не относящихся к $mol_app_todo модулей.

                                                            Общий объём скомпилированного кода со всеми библиотеками оценить сложно, ибо вы и использовали babel, который секунду только компилирует приложение при загрузке. В любом случае, ваша реализация представляет из себя один единственный шаблон на всё приложение и гигантский свич на все действия пользователя, что, мягко выражаясь, не особо поддерживаемо, ибо порождает горы копипасты. Для сравнения: шаблон у вас весит 3кб, а у меня 1кб.

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

                                                            Если тянуть с собой мегабайт зависимостей — такого слона продать не менее сложно.
                                                              +2
                                                              Эх, короче ладно, я вас услышал, точнее понял, вы на своей волне и полностью погружены в свою тему, но вот я, да и судя по всему остальные, так и не могу проникнуться вашими идеями. На этом всё, сливаюсь, могу только удачи вам пожелать.
                                                    0
                                                    Спасибо за статью.
                                                    У меня вопрос, как замеры делаете? А то мне кажется мое f12 в Chrome кажется совсем колхозным решением.
                                                      0

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

                                                      0
                                                      Прочитал статью и первый вопрос который возник — идеальный фреймворк для чего?
                                                      Вы описали проблемы и возможные их решения. Но кто-то не столкнется и с десятой частью этих проблем, а для кого-то наоборот — здесь нет и десятой части их проблем.

                                                      Помните раньше было выражение — сферический конь в вакууме, вспомнилось :)

                                                      Про todomvc — какой оценивать реальную производительность фреймворка по этому показателю?

                                                      Думаете кто то реально будет использовать фреймворк на 1мб для реализации подобных задач?

                                                      Почти любой разраб реализует эту задачу на vanila.js более качественно чем с фреймворком.

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

                                                      Если отрисовка и фпс — самый критичный фактор — вряд ли кто то вообще будет использовать фреймворк, т.к. нативная реализация будет заведомо более оптимизируемой и контролируемой.
                                                        0
                                                        Про todomvc — какой оценивать реальную производительность фреймворка по этому показателю?

                                                        К сожалению, нет никакой "реальной производительности фреймворка". Есть только отзывчивость конкретных написанных с его помощью приложений в конкретных сценариях использования. Десятикратная разница в отзывчивости между реализациями на разных фреймворках в большей степени следствие архитектурных особенностей этих фреймворков.


                                                        Думаете кто то реально будет использовать фреймворк на 1мб для реализации подобных задач?

                                                        Это уже происходит, ибо с фреймворком разработка идёт быстрее.


                                                        Почти любой разраб реализует эту задачу на vanila.js более качественно чем с фреймворком.

                                                        Почему же тогда VanillaJS реализация существенно проигрывает половине фреймворков? Ассемблер Ванильный яваскрипт позволяет талантливому разработчику написать быстрое приложение. Си++ Фреймворк позволяет посредственному разработчику написать приложение не хуже за вдвое меньшее время.


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

                                                        Только если вы сначала продумываете архитектуру, а потом подбираете/пишите для неё фреймворк. Но зачастую сначала выбирают фреймворк, а потом он диктует архитектуру приложения.


                                                        Если отрисовка и фпс — самый критичный фактор — вряд ли кто то вообще будет использовать фреймворк, т.к. нативная реализация будет заведомо более оптимизируемой и контролируемой.

                                                        Почему же игры (где за низкий фпс вас закидают геймпадами) поголовно делаются на фреймворках (так называемых "движках")?

                                                          0
                                                          >К сожалению, нет никакой «реальной производительности фреймворка».


                                                          вот именно, вам говорят о том же, к чему ваши ссылки на тудумвц бенчмарки?
                                                            0
                                                            Почему же тогда VanillaJS реализация существенно проигрывает половине фреймворков?


                                                            вы про реализацию чего говорите?
                                                              0
                                                              И на главный вопрос не ответили,

                                                              Прочитал статью и первый вопрос который возник — идеальный фреймворк для чего?

                                                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                            Самое читаемое