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

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

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

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


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

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

И цикл этот повторяется для любой опечатки. Чем быстрее разработчик поймёт где и почему ошибся, тем быстрее он всё реализует. Поэтому обратную связь программист должен получать как можно быстрее, прямо в процессе написания кода. Тут помогают средства среды разработки, которые в реальном времени анализируют набранный код и проверяют, будет ли это всё работать. Как следствие, очень важно, чтобы среда разработки могла получить из кода как можно больше как можно более конкретной информации. Чтобы этого добиться, используемый язык должен быть статически типизирован настолько на сколько это возможно. 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 на AngularJS в общей сложности весит 1.1МБ (60КБ в ужатопережатом виде), а на $mol — 100КБ (8КБ в ужатопережатом виде). Сравните масштабы. Надо ли рассказывать какое приложение быстрее откроется на мобилке через 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_header : $mol_view

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

<div id="$my_header.app()" my_header mol_view></div>

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

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

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

module $mol {
    export class $my_header extends $mol_view {
    }
}

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

$my_panel : $mol_view
    head : null
    body =No data
    header : $mol_view
        childs < head
    bodier : $mol_view
        childs < body
    childs
        < header
        < bodier

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

$my_app : $my_panel
    head : =My tasks
    body : $my_task_list
        assignee : =me
        status : =todo

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

$my_panel : $mol_view childs
    < header : $mol_view childs < head : null
    < bodier : $mol_view childs < body =No data

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

<div id="$my_panel.app()" my_panel mol_view>
    <div id="$my_panel.app().header()" mol_view my_panel_header></div>
    <div id="$my_panel.app().bodier()" mol_view my_panel_bodier></div>
</div>

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

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

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

@ $mol_replace // перегружаем исходный класс
class $my_panel extends $mol.$my_panel {

    footerShowing() { return this.persist<boolean>('footerShowing') }

    childs(){ return this.prop( () => [
        this.header().get() ,
        this.bodier().get() ,
        this.footerShowing.get() ? this.footer().get() : null ,
    ] ) }

}

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

$my_panel_demo : $mol_view childs
    < simple : $mol_panel
        footerShowing : false
        body =I am simple panel
    < footered : $mol_panel
        footerShowing : true
        body =I am panel with footer

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

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


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

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

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

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

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

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

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

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

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

Особенности этой версии бенчмарка:

  1. Защита от схлопывания рендеринга на первом и третьем шаге, но провоцирование на втором.
  2. Вывод гистограммы в конце замеров. Подробности по времени выполнении каждого шага можно найти в консоли.
  3. Автоматический подхват всех добавленных в ToDoMVC приложений. Те, которые с бенчмарком работают не корректно (около половины из всех), занесены в чёрный список, который можно найти в коде страницы. Патчи к этим решениям или к самому бенчмарку для совместимости с ними — приветствуются.
  4. Чекбоксами можно включать и выключать отдельные приложения. Состояние чекбоксов запоминается.

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

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

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

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

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


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

Что уже есть


Что ещё предстоит сделать

  • Переписать сборщик. Сейчас он написан на коленке и переусложнён.
  • Покрыть код тестами. Куда уж без них.
  • Заредизайнить существующие компоненты и продумать какие ещё необходимы.
  • Написать документацию.
  • Создать симпатичную домашнюю страничку.
  • Реализовать адаптеры для полупрозрачной интеграции в другие фреймворки.

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

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

Проголосовало 563 человека. Воздержалось 368 человек.

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

Комментарии (103)


  1. Rastishka
    23.02.2016 11:25
    -3

    vue.js


  1. dshster
    23.02.2016 12:07

    Пока на Angular 1.*, а там видно будет, насчет TypeScript — не сложится ли ситуация как с CoffeScript? И jQuery не фреймворк вовсе.


    1. vintage
      23.02.2016 12:46
      +5

      В крупных проектах есть острая потребность в статическом выведении типов. В мелких, она не такая острая, но тоже приносит не мало пользы.
      Микрософт очень грамотно спозиционировала TypeScript — это современные спецификации ECMAScript + типы. И ничего лишнего. В отличие от CoffeScript или Dart, меняющих язык до неузнаваемости.


    1. serf
      29.02.2016 11:07
      -1

      Не сложится потому что в ECMAScript даже следующих версиях не предвидится типизации (до них все не дойдет что есть вещи поважнее чем их "сахар"), а TypeScript это и есть ECMAScript + опциональная типизация. Можно считать это расширением ECMAScript.


  1. michael_vostrikov
    23.02.2016 12:08
    +7

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

    Напрашивается картинка про 14 конкурирующих стандартов


    1. vintage
      23.02.2016 12:53

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


      1. denis_g
        23.02.2016 16:20

        Да, создатели картинки про стандарты не учли адаптеры, надо создать еще одну картинку, на которой есть адаптеры :)


        1. vintage
          23.02.2016 16:26
          -1

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


          1. lorc
            23.02.2016 17:53
            +1

            У таких железных штук есть простые метрики, по которым их можно сравнивать. Другими словами, специалист всегда точно скажет когда нужно использовать SPI, а когда — PCI Express. Я так понимаю, что с UI фреймворками ситуация несколько иная, иначе этого поста вообще не было бы.


            1. vintage
              23.02.2016 19:07
              -2

              У фреймворков тоже есть простые метрики — бенчмарки. Но и у портов есть дискуссионные характеристики — распространённость, перспективность, надёжность.


              1. lorc
                23.02.2016 20:01
                +4

                Бенчмарки показывают только производительность. При этом, надо сильно постараться, что бы тормозил именно UI фреймворк, а не сосбтвенно бизнесс-логика. По вашему же бенчмарку видно, что большая часть фреймворков справилась с задачей мене чем за 7 секунд, учитывая, что было совершенно порядка 400 действий, можно посчитать что на одно действие тратилось не более 7/400 = 0.0175 секунды. Это чуть больше времени отображения одного кадра при 60 FPS (0.0166 c). По исследованиям Роберта Миллера, время реакции интерфейса меньше чем 0.1с считается мгновенным. Поэтому, даже если все эти фреймворки будут в 5 раз медленее — катастрофы не произойдет.

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

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

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


                1. vintage
                  23.02.2016 20:53
                  +2

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

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

                  Я говорил вот об этих портах.


  1. lair
    23.02.2016 13:33
    +2

    При этом каждый вложенный элемент получил уникальное имя

    … где же я это видел? А, конечно. asp.net Web Forms. Гарантированно уникальный идентификатор для каждого контрола.


    1. michael_vostrikov
      23.02.2016 14:00

      Да так-то такой подход много где используется. Я использую фреймворк Yii, там есть нечто похожее. Генерация простая, вида "'w' .+ $counter++" ('w' от слова widget). А в web forms это как делается?


      1. lair
        23.02.2016 14:07
        +3

        Да почти так же, магические константы и счетчики на каждом уровне. Другое дело, что это одна из тех вещей, за которые вебформзы ненавидели.


        1. vintage
          23.02.2016 14:10

          Почему ненавидели?


          1. lair
            23.02.2016 18:56

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


            1. vintage
              23.02.2016 19:19

              В предлагаемом мной решении, идентификаторы обоснованно длинные. Помимо идентификационной функции они несут также и отладочную. Ну и в целом человекопонятны. Например, по идентификатору $mol_app_todo.app().taskRow(4).dropper() сразу понятно, что это кнопка удаления задачи в четвёртой по счёту строке в приложении mol_app_todo.


              1. lair
                23.02.2016 19:51

                В вебформзах тоже было понятно, что где. К сожалению, недостатков это не перевешивало.


                1. vintage
                  23.02.2016 19:56

                  Из упомянутых вами недостатков, я вижу тут только один — длинна. Зато достоинств — масса.


                  1. lair
                    23.02.2016 19:58

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


                    1. vintage
                      23.02.2016 20:56

                      Какой, например, цирк?


                      1. lair
                        23.02.2016 21:11

                        А как мне, находясь снаружи компонента — получить идентификатор какого-нибудь поля внутри компонента?


                        1. vintage
                          23.02.2016 21:14

                          А зачем?


                          1. lair
                            23.02.2016 21:15

                            Например, если я хочу сделать с ним что-нибудь инструментами, отличными от вашего фреймворка?


                            1. vintage
                              23.02.2016 21:31

                              Какими, например?

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

                              var cellId = row.cell('title').objectPath
                              var cellNode = row.cell('title').node().get()


                              1. lair
                                24.02.2016 00:54

                                Какими, например?

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

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

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


                                1. sferrka
                                  24.02.2016 04:40

                                  1. lair
                                    24.02.2016 12:16

                                    Все равно же генерить надо — так почему бы не генерить нормальные свойства?


                                1. vintage
                                  24.02.2016 09:24

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

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

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

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

                                  Ну, или использовать энумерацию, как предложила Сферрка.


                                  1. lair
                                    24.02.2016 12:17

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

                                    Вооо, вот мы и вернулись к стартовому "Все зависит от того, кто работает с объектами по этим идентификаторам. Если только сами компоненты, то пофиг."


                                    1. vintage
                                      24.02.2016 12:39

                                      Я не понимаю, что вы пытаетесь доказать. Впрочем, как обычно.


                                      1. bromzh
                                        24.02.2016 13:10
                                        +2

                                        Ну вот есть JSF. Он для компонентов генерирует свои id. Причём, значение id при работе с компонентами в рамках самого фреймворка и id, которые получаются после рендера — разные.

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

                                        Как видно, эти проблемы не только в вебформах, а в подходе в целом.


  1. nazarpc
    23.02.2016 14:32

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


    1. vintage
      23.02.2016 15:02
      -1

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


      1. nazarpc
        23.02.2016 15:10
        +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 их не внедряет пока не появятся в стабильных версиях популярных браузеров.


        1. vintage
          23.02.2016 15:30

          Спасибо, поправил статью.


  1. i360u
    23.02.2016 16:33

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


    1. vintage
      23.02.2016 17:11

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


      1. i360u
        23.02.2016 18:25

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


        1. vintage
          23.02.2016 19:28

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


      1. lair
        23.02.2016 18:57
        +1

        Или вы считаете отсутствие наследования

        А вам точно нужно наследование в UI-фреймворке?


        1. vintage
          23.02.2016 19:32

          Мне нужна кастомизация готовых компонент. Какие-то блоки удалить, какие-то добавить, какие-то перенести в другое место, какие-то по другому раскрасить. Знаете способы реализации этого без наследования?


          1. lair
            23.02.2016 19:53
            +1

            Конечно, знаю. Точки расширения, шаблоны, стратегии — первое, что в голову пришло. Наследование для кастомизации, на самом деле, далеко не идеально подходит — слишком неочевиден баланс между is-a и has-a.


            1. vintage
              23.02.2016 20:14

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

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


              1. lair
                23.02.2016 20:39
                +1

                Вы не в теме проблематики.

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

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

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


                1. vintage
                  23.02.2016 20:57
                  -1

                  Предлагаемое мной решение тем и примечательно, что автоматически предоставляет точки расширения для практически любого аспекта поведения.


                  1. lair
                    23.02.2016 21:14

                    Прекрасно, если у вас есть точки расширения, зачем вам наследование?

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


                    1. vintage
                      23.02.2016 21:23
                      -1

                      Затем, чтобы создать свой компонент на 90% похожий на оригинал. Да, на разных страницах нужны разные календарики.

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


                      1. lair
                        24.02.2016 01:18

                        Затем, чтобы создать свой компонент на 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>"
                        

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


                        1. vintage
                          24.02.2016 09:48

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

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


                          1. mird
                            24.02.2016 10:44
                            +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);
                                       }
                                 }     


                            1. vintage
                              24.02.2016 11:49
                              -2

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


                              1. lair
                                24.02.2016 11:59

                                то выглядит как наследование и ведёт себя как наследование

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

                                Собственно, вот и различие is-a и has-a.


                                1. vintage
                                  24.02.2016 12:27

                                  Вы запутались в паттренах :-) Это Адаптер я не могу использовать, так как меняется интерфейс, а Декоратор реализует тот же самый интерфейс. В том его и суть.


                                  1. lair
                                    24.02.2016 12:30

                                    В примере выше нет "того же самого интерфейса", так что лично я ни в чем не запутался.


                          1. lair
                            24.02.2016 12:24

                            Именно так и работает наследование.

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

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

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

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

                            GenerateTable
                              GenerateHeader
                              for each row
                                GenerateRow
                              GenerateFooter
                            

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

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


                            1. vintage
                              24.02.2016 12:38

                              Допустим у нас такое описание:

                              $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 )
                              } ) }

                              Её тоже можно переопределить.


                              1. lair
                                24.02.2016 12:46

                                … и когда я переопределяю rows, я должен повторить большую часть этого замечательного кода ради измения, скажем в var i = 0 ; i < 50 ; ++i?


                                1. vintage
                                  24.02.2016 12:52

                                  Если число строк захардкожено прямо в коде, то, очевидно, да. А вы ожидали чуда?


                                  1. lair
                                    24.02.2016 13:01
                                    +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> хотите сделать легко и просто, то наследование вам не поможет (точнее, его недостаточно).


                                    1. vintage
                                      24.02.2016 13:22

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

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

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

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


                                      1. lair
                                        24.02.2016 13:31

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

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

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

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

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

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

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

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


                                        1. vintage
                                          24.02.2016 13:52

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

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

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

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


                                          1. lair
                                            24.02.2016 14:01

                                            Во view.tree создавать подкомпоненты можно исключительно через именованные фабрики.

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

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

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

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

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

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

                                            Называете гридом не то же самое, что другие люди.


                                            1. vintage
                                              24.02.2016 14:12

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

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

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


                                              1. lair
                                                24.02.2016 14:48

                                                Но во view.ts вы, конечно, можете сотворить любое непотребство, если сильно заморочиться.

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

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

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

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

                                                Так вы не путайте "абстракцию табличного вывода" и грид. Это два разных уровня.


            1. Rothmansua
              24.02.2016 00:44

              +1


  1. lega
    24.02.2016 11:03
    +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):


    1. RubaXa
      24.02.2016 11:42
      +6

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

      vintage, весь смысл проекта TodoMVC показать идентичный результат, через призму разных подходов. Вы не просто «упростили», как написал lega, вы даже не используете шаблоны и CSS предоставляемый TodoMVC. Ай-ай.


    1. vintage
      24.02.2016 12:19
      -2

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


      1. RubaXa
        24.02.2016 12:40
        +1

        Неважно как оно сверстано, я например не верстаю, а работаю с готовым html/css, который уже вылизан под все браузеры и прошел все цепочки принимающий, так что переверстывать уже некогда, да и бессмысленно, мой инструмент должен легко справиться с чем угодно. Вы же пошли по пути перепила под ваш инструмент, что наводит на определенные мысли.

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


        1. vintage
          24.02.2016 12:58
          -1

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

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


          1. RubaXa
            24.02.2016 13:08
            +1

            Я пошёл по пути более грамотного построения процесса:

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

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

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

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

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


            1. vintage
              24.02.2016 13:36

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


              1. RubaXa
                24.02.2016 13:50
                +1

                Я правильно понял, что если сейчас захочу попробовать $mol, мне придется с нуля переделать свои уже компоненты, потому что они «неправильные» и я просто не смогу загнать свою уже существующую вёрстку в него?


                1. vintage
                  24.02.2016 14:00
                  -3

                  А вы думали можно ничего не переписывая сменить парадигму программирования?


                  1. RubaXa
                    24.02.2016 14:40
                    +1

                    Да, именно так я и думаю.

                    При переходе на Angular, React, Meteor, Aurelia и так далее, я смогу использовать уже готовую верстку. Даже в Elm, можно загнать верстку или просто воспользоваться каким-нибудь elm-html.


                    1. vintage
                      24.02.2016 15:22

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


      1. lega
        24.02.2016 15:06
        +2

        Если тот же функционал можно реализовать меньшим числом элементов — это разве не плюс?
        Это плюс, но для большинства фреймворков можно оптимизировать HTML, что даст лучшие показатели. В итоге получается соревнование кто лучше HTML подпилит, а не скорость фреймворка (хотя они и составят основную часть). Чтобы было честно нужно для всех оптимизировать HTML, а это накладно. Поэтому, я считаю, как минимум нужно выкладывать «каноническую» версию, ну и для интереса можно оптимизированную прикладывать.

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

        жду пул-реквест. :-)
        Да, я ещё оптимизированную версию запилю, интересно :)


        1. vintage
          24.02.2016 16:29

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

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


  1. snater
    24.02.2016 11:56

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

    Я правильно понимаю, что достигается это подпиской на конкретные поля данных и перерендером/патчингом частей шаблона, которые на эти данные завязаны?


    1. vintage
      24.02.2016 12:22

      Именно так.


  1. lega
    24.02.2016 16:22

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

    Сферический идеальный фреймворк
    Я правильно понял что все манипуляции вокруг tree формата (в основном) для того что-бы можно было наследовать шаблоны (и CSS)?, а JS/TS и так наследовать можно.


    1. vintage
      24.02.2016 16:34

      Т.е. вы на каждый элемент создаете глобальную переменную?, это только в debug режиме?
      Любой объект доступен по абсолютному пути, который и используется в качестве идентификатора. Это фича такая. Там ещё ссылки на объекты контролируются — когда на него не остаётся ссылок — он уничтожается. Вообще говоря, можно было бы сильно ускорить работу, если бы браузер предоставлял апи по отключению GC.

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


      1. lega
        24.02.2016 17:48
        +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, кажется вы изобретаете то же самое, только синтаксис другой.


        1. vintage
          24.02.2016 18:13

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

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

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

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

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

          О! Я вспомнил, раньше модно было использовать XSLT, кажется вы изобретаете то же самое, только синтаксис другой.
          ответить
          Polymer куда ближе к XSLT, он делает выборки из деревьев по селекторам.


  1. vintage
    24.02.2016 18:13

    ...


  1. lega
    25.02.2016 13:27
    +1

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


    1. vintage
      25.02.2016 14:00

      Думаю дело в том, что вы не сделали форк через github. Чуть позже перенесу патч и добавлю редизайн своей версии. Кстати, вы перевели на новый дизайн или всё ещё используется старый?


      1. lega
        25.02.2016 15:42
        +1

        Перезалил комит, сделал пул-реквест.


  1. lega
    25.02.2016 15:32
    -1

    Да, наверно это "мульки" гибхаба, чисто с гитом все прошло бы гладко. Дизайн старый.

    Ответ для https://habrahabr.ru/post/276747/#comment_8784117


    1. vintage
      26.02.2016 20:25

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


      1. vintage
        26.02.2016 20:46

        Текущие результаты на 100 задач:


      1. lega
        26.02.2016 21:24

        Чуть чуть не дождались вы, вот исправленная версия: https://github.com/nin-jin/todomvc/pull/2
        А что значит "не комплитит задачи"? Вручную все работает (да и на тесте тоже).


        1. lega
          26.02.2016 21:42

          А что значит «не комплитит задачи»
          Не правильно прочитал, хз, но сейчас все нормально работает.


          1. vintage
            27.02.2016 18:38
            +1

            Обновил статью новыми результатами бенчмарков. Теперь самый быстрый — AL.


  1. vintage
    28.02.2016 18:56
    +1

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


  1. wrmax
    29.02.2016 22:56
    -1

    Ещё один программист собрался написать ещё один новый фреймворк

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

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


    1. vintage
      01.03.2016 00:11
      +3

      Это всё, что вы почерпнули из статьи?


  1. Scf
    01.03.2016 21:45

    Как обычно, очень интересная статья, если не смотреть код :-) Оставляет двойственное впечатление — кладезь интересных мыслей и какие-то странно-закрученные реализации.

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


    1. vintage
      01.03.2016 22:52

      Не поделитесь своей реализацией в 160 строк? Я бы с радостью выкинул свои 410 :-)

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


      1. Scf
        02.03.2016 17:00

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

        А вот html… пытаться от него изолироваться, имхо, бесполезное дело. Но я до сих пор не уверен, как делать правильно. Возможно, использование компонента не обязано быть единственным тегом? Как в старые добрые времена jquery — мы пишем разметку, а потом она "оживает". типа: <div class=tabs activeTab={activeTab}><div class=tab></div><div class=tab></div></div>. Не знаю.