Здравствуйте, меня зовут Дмитрий Карловский, и я... архитектор множества широко известных в узких кругах фреймворков. Меня никогда не устраивала необходимость из раза в раз решать одни и те же проблемы, поэтому я всегда стараюсь решать их в корне. Но прежде, чем их решить, нужно их обнаружить и осознать, что довольно сложно находясь в плену привычек, паттернов, стереотипов и "готовых" решений. Каждый раз сталкиваясь с проблемами в реализации задачи, я задумываюсь "что, блин, не так с этим инструментом?" и, конечно же, иду пилить свой инструмент: функцию, модуль, библиотеку, фреймворк, язык программирования, архитектуру ЭВМ... стоп, до последнего я ещё не докатился.
Речь сегодня пойдёт о JS-фреймворках. Нет, я не буду рассказывать про очередное готовое решение, не в том цель поста. Я лишь хочу посеять в ваших головах несколько простых идей, которые вы не встретите в документации ни к одному популярному фреймворку. А в конце мы постараемся сформировать видение идеальной архитектуры построения пользовательского интерфейса.
Длинный цикл отладки
Типичный цикл отладки выглядит так:
Редактирование кода.
Запуск приложения.
Проверка и обнаружение проблем.
Исследование их причин.
И цикл этот повторяется для любой опечатки. Чем быстрее разработчик поймёт где и почему ошибся, тем быстрее он всё реализует. Поэтому обратную связь программист должен получать как можно быстрее, прямо в процессе написания кода. Тут помогают средства среды разработки, которые в реальном времени анализируют набранный код и проверяют, будет ли это всё работать. Как следствие, очень важно, чтобы среда разработки могла получить из кода как можно больше как можно более конкретной информации. Чтобы этого добиться, используемый язык должен быть статически типизирован настолько на сколько это возможно. JavaScript же типизирован лишь динамически, от чего IDE пытается угадать типы по косвенным признакам (типичные паттерны, JSDoc-и), но, как показывает практика, даже у наиболее продвинутых сред разработки это получается плохо.
Однако, существует минималистичное расширение JavaScript, добавляющее в него опциональную статическую типизацию - TypeScript. Его отлично понимает даже, какой-нибудь простой текстовый редактор типа GitHub Atom.
На текущий момент 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()
}
} )
Что не так с этим кодом:
Один и тот же модуль (который jQuery) в разных файлах имеет разные локальные имена. То есть программисту нужно постоянно держать в голове как jQuery называется в каждом модуле. Зачем нам возможность по разному именовать одну и ту же сущность? Чтобы всех запутать?
У нас нет простого доступа к переменной count. Мы не можете открыть консоль и просто набрать app.count, чтобы узнать какое там сейчас значение. Для этого необходимо изрядно пожонглировать отладчиком.
Каждый раз используя какую-либо сущность, нужно проконтролировать, чтобы она была "импортирована", а переставая её использовать надо удалить и эти "импорты". Существование специального инструмента для автоматической синхронизации списка импортов с кодом, подчёркивает их бессмысленность - это типовой, легко автоматизируемый инфраструктурный код, до которого программисту, вообще говоря, нет никакого дела.
Много лишнего кода, который зачастую просто генерируется по шаблону, так как писать руками одно и то же, никто не любит.
Как решить эти проблемы, не создавая новых? А очень просто, воспользуемся форматом 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 в консоль, чтобы посмотреть текущее состояние. Вот к чему стоит стремиться, а не к изоляции всего и вся.
Перила помогают не упасть в пропасть, но не стоит ими себя окружать.
А вот что действительно необходимо - это пространства имён, и структура директорий как нельзя лучше позволяет гарантировать отсутствие конфликтов в простанствах имён.
Раздувание кода
Если раньше были споры стоит ли грузить jQuery на сотню килобайт, то сейчас уже никого не смущает подключение мегабайтного фреймворка. Некоторые даже гордятся тем, что написали миллионы строчек кода для реализации не слишком сложного приложения. И если бы проблема была только в скорости загрузки этих слонов, да скорости их инициализации. Но есть и куда более существенные вытекающие из этого проблемы. Чем больше кода, тем больше в нём ошибок. Более того, чем больше кода, тем больше процент этих ошибок. Так что если вы видите огромный зрелый фреймворк, то можете быть уверенными, что на отладку таких объёмов кода была потрачена уйма человекочасов. И ещё уйму предстоит потратить, как на существующие ещё не замеченные баги, так и на баги, привносимые новыми фичами и рефакторингами. И пусть вас не вводит в заблуждение "100% покрытия тестами" или "команда высококлассных специалистов с десятилетним опытом" - багов точно нет лишь в пустом файле. Сложность поддержки кода растёт нелинейно по мере его разрастания. Развитие фреймворка/библиотеки/приложения замедляется, пока совсем не вырождается в бесконечное латание дыр, без существенных улучшений, но требующее постоянное увеличение штата. Так что, если кто-то предложит вам написать дополнительный код по превращению ВерблюжьихИдентификаторовДиректив в идентификаторы-директив-с-дефисами, только потому, что в JS традиционно используется одна нотация, а в CSS - другая, с ней не совместимая, то плюньте ему в лицо и воспользуйтесь универсальной_нотацией_идентификаторов, не требующей ни дополнительного кода, ни среды разработки со сложными эвристическими алгоритмами поиска соответствий.
Большой объём кода далеко не всегда означает большое число возможностей. Зачастую это следствие использования слишком многословных инструментов, переусложнённой логики и банальной копипасты (в том числе и кодогенерации). Не стоит впадать в крайности, давая всем переменным однобуквенные имена, но стоит насторожиться, видя десятки тысяч строк рисующие простую формочку на экране. Вы можете не обращать на это внимание, полагая, что вам не потребуется в нём разбираться, ограничившись лишь чтением документации; полагая, что публичного API вам хватит для любых хотелок; полагая, что багов либо нет, либо они будут быстро исправляться мейнтейнерами. Но практика показывает, что рано или поздно вам придётся лезть в эту груду кода, если не вносить изменения, то как минимум исследовать его работу. А чем больше кода, тем сложнее в нём разобраться. Но ещё сложнее разобраться в коде, изобилующем множеством абстракций. Небольшое число, грамотных абстракций позволяет значительно упростить код, но мы получаем резко противоположный эффект, когда лепим их без разбора. Фабрики, прокси, адаптеры, реестры, сервисы, провайдеры, директивы, декораторы, примеси, типажи, компоненты, контейнеры, модули, приложения, стратегии, команды, роутеры, генераторы, итераторы, монады, контроллеры, модели, отображения, модели отображения, презентаторы, шаблоны, билдеры, виртуальный дом, грязные проверки, биндинги, события, стримы.. Во всём этом многообразии абстракций теряются даже опытные разработчики. А попробуйте объяснить новичку, как на таком фреймворке сделать простой компонент так, чтобы не плакать потом кровавыми слезами, глядя на то, что у него получилось.
Чем проще - тем лучше.
Сложность разработки
По мере роста приложения всё сложнее вносить в него изменения не разломав. Требуется учитывать всё большее число состояний одновременно, разбросанных по разным частям приложения. Множество неявных внутренних соглашений неизбежно начинают противоречить друг другу. Поэтому важно, чтобы за одно состояние отвечало лишь одно место в коде. Если вы меняете значение переменной из нескольких разных обработчиков событий, то рано или поздно вы получите неверное его значение, что будет выглядеть как глюк, на поиск причины которого придётся потратить не мало времени. Напротив, если вы будете выражать любое состояние как функцию от других состояний, то, даже если ваша переменная и примет неверное значение, вы всегда знаете где искать ошибку - в функции вычисления или получаемых ею данных.
Продолжая развивать тему, заметим, что такая функция не просто позволяет вычислить одно состояние на основе других, эта функция - задекларированная в коде зависимость между состояниями, инвариант, который должен быть поддержан в актуальном состоянии всё время жизни приложения. То есть, если изменяется состояние зависимости, то и все зависимые состояния должны быть обновлены автоматически, не полагаясь на внимательность и трудолюбивость разработчика.
Реактивная архитектура значительно упрощает поддержку.
Зачастую при разработке фреймворков основное внимание уделяется таким вещам как "простота", "гибкость" и "скорость", но практически игнорируется такое немаловажное качество как "исследуемость". Предполагается, что взявшийся работать с ним разработчик уже прочитал всю документацию, понял её правильно и вообще очень одарённый человек. Но правда жизни заключается в том, что документация зачастую не полна, плохо структурирована, на не родном языке и требует очень много времени для полного изучения, которого всегда не хватает, да и разработчик зачастую не имеет за плечами десяти лет опыта и досконального знания паттернов. Поэтому лучшая документация - это примеры. Лучшие примеры - это тесты. А лучший способ понять как оно работает - разобрать. Так что важно не изолировать внутреннее состояние, а предоставлять простой и удобный доступ к нему. Через консоль, через отладчик, через логи. Необходимо именовать одни и те же сущности одинаково в разных местах: имена переменных в разных модулях, имена модулей в разных контекстах, имена классов в скриптах, стилях и вёрстке и тд. А также необходимо добавлять информацию о не очевидных связях между сущностями. Например, посмотрите на этот код:
Теперь стало понятно куда копать, и кто виноват в том, что кнопка удаления страницы находится не на той панели. Другой яркий пример связан с парсингом. Когда вы применяете 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
Хлебные крошки не помешают как в пользовательском интерфейсе, так и в программном коде.
Компонентная декомпозиция
Самым важным аспектом любого фреймворка является реализация единого протокола взаимодействия. Собственно, в случае микромодульного фреймворка, единственное, чем занимается ядро - это организация взаимодействия модулей, как стандартных, так и пользовательских. В случае ui фреймворка, основной его задачей является организация взаимодействия компонент - этаких мини приложений, которые и сами по себе могут работать, но поддерживают и некоторый API для взаимодействия с другими компонентами, позволяя собирать из них как из кубиков лего более сложные компоненты, вплоть до полноценных приложений. Давайте рассмотрим, что нам предлагают современные фреймворки...
Для объявления компоненты нам потребовалось написать немного скриптов, немного шаблонов и приложить к ним некоторый конфиг, описывающий API. Получилось весьма многословно и запутанно. Необходимо держать в голове, что ng-transclude содержит не просто текст, а имя параметра, значение которого будет вставлено внутрь элемента. Нужно внимательно поддерживать маппинг внешних имён параметров на внутренние. Нужно всем элементам расставить классы, чтобы их можно было стилизовать. А теперь попробуем воспользоваться этой компонентой:
Как видим, для каждого параметра нам пришлось добавить дополнительные тэги. Поскольку все эти тэги находятся в одном пространстве имён, то нам пришлось к каждому параметру добавить префикс с именем компонента (эти тэги как есть будут зачем-то вставлены в результирующее дерево). Да, в простейших случаях вы можете воспользоваться атрибутами, но если есть вероятность, что потребуется вставлять вложенную компоненту, а не просто строку текста, то приходится использовать такие вот громоздкие конструкции.
А теперь типичная ситуация: есть готовая компонента и нам надо её слегка кастомизировать. Например, добавить подвал. Беда многих подобных фреймворков в том, что реализованные на них компоненты слишком жёсткие и изолированные. Вы не можете просто отнаследоваться от компонента и слегка поменять его поведение. Вместо этого, вам необходимо либо вносить изменения в сам исходный компонент, усложняя его и заставляя уметь слишком многое:
Уже гораздо лучше, хотя и осталась по прежнему неотделимая завязка на JS. Почему это плохо? Потому, что не везде JS является оптимальным языком программирования. В вебе у вас нет выбора. Но под iOS лучше бы подошёл Swift или ObjectiveC, под Android - Java, а под десктопы выбор языков вообще огромен, но на JS свет клином не сошёлся. По прежнему мы имеем проблему жёсткости и изолированности компоненты, так что с кастомизацией всё почти так же плохо, как и в AngularJS. "Почти", потому, что мы можем расчленить наш шаблон на мелкие кусочки, что позволит в дальнейшем их переопределять:
Мы получили неплохую гибкость, но потеряли наглядность иерархии элементов. А использование XML синтаксиса в этом случае становится излишним. Особенно экстравагантно выглядело бы использование компоненты без расчленения на функции:
В целом, описание компоненты похоже на хак, который берёт чужое дерево и видоизменяет его до неузнаваемости. В хитросплетениях реального и теневого дерева довольно сложно разбираться.
Наследование пока не поддерживается, так что используем силу копипасты:
Отдельно стоит отметить проблему стилизации. Каждая компонента тянет с собой свои стили, которые по умолчанию изолированы от всех остальных компонент. С одной стороны, это позволяет использовать короткие имена классов в рамках одной компоненты, с другой же, добавляет проблем при необходимости кастомизировать визуализацию компоненты в каком-то конкретном контексте использования.
Сферический идеальный фреймворк
Давайте попробуем сформурировать, что необходимо для описания компонента. Как бы ни хотелось при построении интерфейса реиспользовать готовые компоненты как есть, их всегда требуется дотачивать под себя. То есть, описание компонента должно поддерживать наследование. Чтобы из компонент можно было собирать произвольные интерфейсы, требуется, чтобы их можно было вкладывать друг в друга произвольным образом. То есть, нам нужен полиморфизм. Чтобы одни и те же компоненты можно было использовать из разных языков, их описание должно быть декларативным, при этом синтаксис должен быть максимально простым и наглядным. Ну и статическую типизацию никто не отменял.
Как показано выше, XML совсем не удовлетворяет этим требованиям. Вообще, все попытки сделать из XML шаблонизатор выглядят как помесь ужа с ежом. То XML встраивается в JS, то JS встраивается в XML, а то и изобретается свой примитивный язык программирования с XML-синтаксисом.
Давайте воспользуемся tree-синтаксисом для объявления простой компоненты:
$my_panel $mol_view
Тут мы просто говорим, что компонент $my_panel - наследник от $mol_view. Нас не волнует как и на каком языке реализован $mol_view, но мы утверждаем, что $mol_panel должен быть реализован точно так же. Например, приведённое выше описание, может развернуться в следующее DOM-дерево:
Как можно заметить, для элемента были автоматически сгенерированы некоторые атрибуты. Прежде всего это id - глобальный идентификатор компонента. Он не зря имеет такой странный вид, ведь его можно скопировать и вставить в консоль, получив тем самым прямой доступ к инстансу компонента, за этот элемент ответственного. Это очень сильно упрощает отладку и исследование чужого кода. Далее идут атрибуты, предназначенные прежде всего для стилизации. Вы можете определить стилизацию для базового компонента, а потом перегрузить её для наследника:
Как видите, нам не потребовался даже css препроцессор, чтобы реализовать наследование в CSS. То есть, для декларации наследования у нас есть только одно место в коде - это описание компоненты. И не нужно отдельно наследоваться в JS, отдельно в CSS, отдельно в HTML. По этому описанию также может быть сгенерирован и TypeScript-класс для этого компонента:
Компонент $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 атрибутов:
В итоге мы сохранили иерархию, не потеряв в гибкости. Создадим вариант панельки с подвалом:
$my_panel_ext $my_panel
sub /
<= Head -
<= Body -
<= Foot $mol_view
sub <= foot /
Но что если нам нужна динамика? Например, показывать подвал в зависимости от какого-либо условия. Нет ничего лучше для описания логики, чем язык программирования. Поэтому воспользуемся TypeScript, чтобы добавить логику к декларативному описанию компоненты:
Важно отметить, что компонент самодостаточнен и без логики. Это позволяет верстальщику вывести один и тот же компонент в разных состояниях, не залезая в программный код, что очень сильно повышает его продуктивность:
$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 реализованный через специальный интерфейс для быстрого написания крутых бенчмарков.
Каждое приложение прогоняется через 2 теста:
Создание 100 задач через поле создания задач. Каждая следующая задача создаётся через setTimeout, что не позволяет фреймворку срезать путь и схлопнуть 100 операций рендеринга в одну. Визуально это выглядит как постепенное разрастание списка задач от 0 до 100. Этот шаг показывает насколько дорого фреймворку обходится формирование интерфейса.
Удаление задач по одной, нажатиями на кнопки удаления задачи. Тут опять используется setTimeout для предотвращения схлопывания 100 операций рендеринга в одну. Этот шаг показывает эффективность фреймворка по уничтожению интерфейса. Визуально это выглядит как постепенное исчезновение задач сверху списка. Такое удаление задач гарантирует, что все строки рано или поздно будут отрендерены, даже если фреймворк рендерит лишь компоненты, попадающие в видимую область.
Также стоит отметить, что каждое приложение прогоняется ровно один раз в отдельном фрейме, что не позволяет ему кешировать какие-либо объекты.
А теперь уберём из тестирования совсем аутсайдеров и увеличим число задач вдвое:
Ожидаемым является увеличение времени работы не более чем в 2 раза. Однако лишь немногим решениям удалось добиться такой пологой динамики. Как правило замедление составило порядка 2.5, а в некоторых случаях даже превысило 3.
Неправильный выбор фреймворка может замедлить ваше приложение более чем в 10 раз.
Призыв к действию
Хватит это терпеть! Если вы, как и я, устали от этих всё более усложняющихся инструментов, которые не упрощают разработку, а меняют одни сложности на другие, то присоединяйтесь к разработке простого и эффективного сферического фреймворка в безвоздушном пространстве.
Основные характеристики
Минимум абстракций и соглашений.
Статическая типизация.
Реактивная архитектура.
Высокая эффективность.
Компактный размер.
Простота компоновки.
Высокая кастомизируемость компонент.
Высокая исследуемость и отлаживаемость.
Разработка множества библиотек/приложений в одной кодовой базе.
https://page.hyoo.ru/#!=kj3iky_eignng
Здравствуйте, меня зовут Дмитрий Карловский, и я... архитектор множества широко известных в узких кругах фреймворков. Меня никогда не устраивала необходимость из раза в раз решать одни и те же проблемы, поэтому я всегда стараюсь решать их в корне. Но прежде, чем их решить, нужно их обнаружить и осознать, что довольно сложно находясь в плену привычек, паттернов, стереотипов и "готовых" решений. Каждый раз сталкиваясь с проблемами в реализации задачи, я задумываюсь "что, блин, не так с этим инструментом?" и, конечно же, иду пилить свой инструмент: функцию, модуль, библиотеку, фреймворк, язык программирования, архитектуру ЭВМ... стоп, до последнего я ещё не докатился.
Речь сегодня пойдёт о JS-фреймворках. Нет, я не буду рассказывать про очередное готовое решение, не в том цель поста. Я лишь хочу посеять в ваших головах несколько простых идей, которые вы не встретите в документации ни к одному популярному фреймворку. А в конце мы постараемся сформировать видение идеальной архитектуры построения пользовательского интерфейса.
Длинный цикл отладки
Типичный цикл отладки выглядит так:
И цикл этот повторяется для любой опечатки. Чем быстрее разработчик поймёт где и почему ошибся, тем быстрее он всё реализует. Поэтому обратную связь программист должен получать как можно быстрее, прямо в процессе написания кода. Тут помогают средства среды разработки, которые в реальном времени анализируют набранный код и проверяют, будет ли это всё работать. Как следствие, очень важно, чтобы среда разработки могла получить из кода как можно больше как можно более конкретной информации. Чтобы этого добиться, используемый язык должен быть статически типизирован настолько на сколько это возможно. JavaScript же типизирован лишь динамически, от чего IDE пытается угадать типы по косвенным признакам (типичные паттерны, JSDoc-и), но, как показывает практика, даже у наиболее продвинутых сред разработки это получается плохо.
Однако, существует минималистичное расширение JavaScript, добавляющее в него опциональную статическую типизацию - TypeScript. Его отлично понимает даже, какой-нибудь простой текстовый редактор типа GitHub Atom.
На текущий момент 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:
Что не так с этим кодом:
app.count
, чтобы узнать какое там сейчас значение. Для этого необходимо изрядно пожонглировать отладчиком.Как решить эти проблемы, не создавая новых? А очень просто, воспользуемся форматом jam.js:
Кода получилось существенно меньше и весь он по делу, при этом для работы с ним нам уже не обязательно нужна мощная среда разработки. Нам не приходится ломать голову в каком модуле как названа jQuery, ведь называется она везде одинаково - в соответствии с путём до неё в файловой системе. При этом мы всегда можем скопировать
$my_app.count
в консоль, чтобы посмотреть текущее состояние. Вот к чему стоит стремиться, а не к изоляции всего и вся.Перила помогают не упасть в пропасть, но не стоит ими себя окружать.
А вот что действительно необходимо - это пространства имён, и структура директорий как нельзя лучше позволяет гарантировать отсутствие конфликтов в простанствах имён.
Раздувание кода
Если раньше были споры стоит ли грузить jQuery на сотню килобайт, то сейчас уже никого не смущает подключение мегабайтного фреймворка. Некоторые даже гордятся тем, что написали миллионы строчек кода для реализации не слишком сложного приложения. И если бы проблема была только в скорости загрузки этих слонов, да скорости их инициализации. Но есть и куда более существенные вытекающие из этого проблемы. Чем больше кода, тем больше в нём ошибок. Более того, чем больше кода, тем больше процент этих ошибок. Так что если вы видите огромный зрелый фреймворк, то можете быть уверенными, что на отладку таких объёмов кода была потрачена уйма человекочасов. И ещё уйму предстоит потратить, как на существующие ещё не замеченные баги, так и на баги, привносимые новыми фичами и рефакторингами. И пусть вас не вводит в заблуждение "100% покрытия тестами" или "команда высококлассных специалистов с десятилетним опытом" - багов точно нет лишь в пустом файле. Сложность поддержки кода растёт нелинейно по мере его разрастания. Развитие фреймворка/библиотеки/приложения замедляется, пока совсем не вырождается в бесконечное латание дыр, без существенных улучшений, но требующее постоянное увеличение штата. Так что, если кто-то предложит вам написать дополнительный код по превращению ВерблюжьихИдентификаторовДиректив в идентификаторы-директив-с-дефисами, только потому, что в JS традиционно используется одна нотация, а в CSS - другая, с ней не совместимая, то плюньте ему в лицо и воспользуйтесь универсальной_нотацией_идентификаторов, не требующей ни дополнительного кода, ни среды разработки со сложными эвристическими алгоритмами поиска соответствий.
Большой объём кода далеко не всегда означает большое число возможностей. Зачастую это следствие использования слишком многословных инструментов, переусложнённой логики и банальной копипасты (в том числе и кодогенерации). Не стоит впадать в крайности, давая всем переменным однобуквенные имена, но стоит насторожиться, видя десятки тысяч строк рисующие простую формочку на экране. Вы можете не обращать на это внимание, полагая, что вам не потребуется в нём разбираться, ограничившись лишь чтением документации; полагая, что публичного API вам хватит для любых хотелок; полагая, что багов либо нет, либо они будут быстро исправляться мейнтейнерами. Но практика показывает, что рано или поздно вам придётся лезть в эту груду кода, если не вносить изменения, то как минимум исследовать его работу. А чем больше кода, тем сложнее в нём разобраться. Но ещё сложнее разобраться в коде, изобилующем множеством абстракций. Небольшое число, грамотных абстракций позволяет значительно упростить код, но мы получаем резко противоположный эффект, когда лепим их без разбора. Фабрики, прокси, адаптеры, реестры, сервисы, провайдеры, директивы, декораторы, примеси, типажи, компоненты, контейнеры, модули, приложения, стратегии, команды, роутеры, генераторы, итераторы, монады, контроллеры, модели, отображения, модели отображения, презентаторы, шаблоны, билдеры, виртуальный дом, грязные проверки, биндинги, события, стримы.. Во всём этом многообразии абстракций теряются даже опытные разработчики. А попробуйте объяснить новичку, как на таком фреймворке сделать простой компонент так, чтобы не плакать потом кровавыми слезами, глядя на то, что у него получилось.
Чем проще - тем лучше.
Сложность разработки
По мере роста приложения всё сложнее вносить в него изменения не разломав. Требуется учитывать всё большее число состояний одновременно, разбросанных по разным частям приложения. Множество неявных внутренних соглашений неизбежно начинают противоречить друг другу. Поэтому важно, чтобы за одно состояние отвечало лишь одно место в коде. Если вы меняете значение переменной из нескольких разных обработчиков событий, то рано или поздно вы получите неверное его значение, что будет выглядеть как глюк, на поиск причины которого придётся потратить не мало времени. Напротив, если вы будете выражать любое состояние как функцию от других состояний, то, даже если ваша переменная и примет неверное значение, вы всегда знаете где искать ошибку - в функции вычисления или получаемых ею данных.
Продолжая развивать тему, заметим, что такая функция не просто позволяет вычислить одно состояние на основе других, эта функция - задекларированная в коде зависимость между состояниями, инвариант, который должен быть поддержан в актуальном состоянии всё время жизни приложения. То есть, если изменяется состояние зависимости, то и все зависимые состояния должны быть обновлены автоматически, не полагаясь на внимательность и трудолюбивость разработчика.
Реактивная архитектура значительно упрощает поддержку.
Зачастую при разработке фреймворков основное внимание уделяется таким вещам как "простота", "гибкость" и "скорость", но практически игнорируется такое немаловажное качество как "исследуемость". Предполагается, что взявшийся работать с ним разработчик уже прочитал всю документацию, понял её правильно и вообще очень одарённый человек. Но правда жизни заключается в том, что документация зачастую не полна, плохо структурирована, на не родном языке и требует очень много времени для полного изучения, которого всегда не хватает, да и разработчик зачастую не имеет за плечами десяти лет опыта и досконального знания паттернов. Поэтому лучшая документация - это примеры. Лучшие примеры - это тесты. А лучший способ понять как оно работает - разобрать. Так что важно не изолировать внутреннее состояние, а предоставлять простой и удобный доступ к нему. Через консоль, через отладчик, через логи. Необходимо именовать одни и те же сущности одинаково в разных местах: имена переменных в разных модулях, имена модулей в разных контекстах, имена классов в скриптах, стилях и вёрстке и тд. А также необходимо добавлять информацию о не очевидных связях между сущностями. Например, посмотрите на этот код:
Как тут можно догадаться, что оба этих элемента были добавлены в дерево компонентом my-page? Добавим недостающую информацию:
Теперь стало понятно куда копать, и кто виноват в том, что кнопка удаления страницы находится не на той панели. Другой яркий пример связан с парсингом. Когда вы применяете JSON.parse, то теряете информацию о расположении данных в исходном файле. Поэтому, когда при последующей валидации вы обнаруживаете ошибку, то не можете сообщить пользователю "На такой-то строке обнаружен не валидный e-mail", а вынуждены изобретать костыли вида "Невалидный e-mail по пути
departaments[2].users[14].mails[0]
". Напротив, при использовании формата tree вы всегда можете получить из узла информацию о месте его объявления:Хлебные крошки не помешают как в пользовательском интерфейсе, так и в программном коде.
Компонентная декомпозиция
Самым важным аспектом любого фреймворка является реализация единого протокола взаимодействия. Собственно, в случае микромодульного фреймворка, единственное, чем занимается ядро - это организация взаимодействия модулей, как стандартных, так и пользовательских. В случае ui фреймворка, основной его задачей является организация взаимодействия компонент - этаких мини приложений, которые и сами по себе могут работать, но поддерживают и некоторый API для взаимодействия с другими компонентами, позволяя собирать из них как из кубиков лего более сложные компоненты, вплоть до полноценных приложений. Давайте рассмотрим, что нам предлагают современные фреймворки...
AngularJS
Объявление компоненты:
Для объявления компоненты нам потребовалось написать немного скриптов, немного шаблонов и приложить к ним некоторый конфиг, описывающий API. Получилось весьма многословно и запутанно. Необходимо держать в голове, что ng-transclude содержит не просто текст, а имя параметра, значение которого будет вставлено внутрь элемента. Нужно внимательно поддерживать маппинг внешних имён параметров на внутренние. Нужно всем элементам расставить классы, чтобы их можно было стилизовать. А теперь попробуем воспользоваться этой компонентой:
Как видим, для каждого параметра нам пришлось добавить дополнительные тэги. Поскольку все эти тэги находятся в одном пространстве имён, то нам пришлось к каждому параметру добавить префикс с именем компонента (эти тэги как есть будут зачем-то вставлены в результирующее дерево). Да, в простейших случаях вы можете воспользоваться атрибутами, но если есть вероятность, что потребуется вставлять вложенную компоненту, а не просто строку текста, то приходится использовать такие вот громоздкие конструкции.
А теперь типичная ситуация: есть готовая компонента и нам надо её слегка кастомизировать. Например, добавить подвал. Беда многих подобных фреймворков в том, что реализованные на них компоненты слишком жёсткие и изолированные. Вы не можете просто отнаследоваться от компонента и слегка поменять его поведение. Вместо этого, вам необходимо либо вносить изменения в сам исходный компонент, усложняя его и заставляя уметь слишком многое:
Либо копипастить, создавая новый компонент очень похожий на старый, но слегка другой:
ReactJS
Объявление компоненты:
Уже гораздо лучше, хотя и осталась по прежнему неотделимая завязка на JS. Почему это плохо? Потому, что не везде JS является оптимальным языком программирования. В вебе у вас нет выбора. Но под iOS лучше бы подошёл Swift или ObjectiveC, под Android - Java, а под десктопы выбор языков вообще огромен, но на JS свет клином не сошёлся. По прежнему мы имеем проблему жёсткости и изолированности компоненты, так что с кастомизацией всё почти так же плохо, как и в AngularJS. "Почти", потому, что мы можем расчленить наш шаблон на мелкие кусочки, что позволит в дальнейшем их переопределять:
Мы получили неплохую гибкость, но потеряли наглядность иерархии элементов. А использование XML синтаксиса в этом случае становится излишним. Особенно экстравагантно выглядело бы использование компоненты без расчленения на функции:
Polymer
Объявление компоненты:
Бросается в глаза получение параметров через селекторы, что делает использование компоненты весьма не наглядным:
В целом, описание компоненты похоже на хак, который берёт чужое дерево и видоизменяет его до неузнаваемости. В хитросплетениях реального и теневого дерева довольно сложно разбираться.
Наследование пока не поддерживается, так что используем силу копипасты:
Отдельно стоит отметить проблему стилизации. Каждая компонента тянет с собой свои стили, которые по умолчанию изолированы от всех остальных компонент. С одной стороны, это позволяет использовать короткие имена классов в рамках одной компоненты, с другой же, добавляет проблем при необходимости кастомизировать визуализацию компоненты в каком-то конкретном контексте использования.
Сферический идеальный фреймворк
Давайте попробуем сформурировать, что необходимо для описания компонента. Как бы ни хотелось при построении интерфейса реиспользовать готовые компоненты как есть, их всегда требуется дотачивать под себя. То есть, описание компонента должно поддерживать наследование. Чтобы из компонент можно было собирать произвольные интерфейсы, требуется, чтобы их можно было вкладывать друг в друга произвольным образом. То есть, нам нужен полиморфизм. Чтобы одни и те же компоненты можно было использовать из разных языков, их описание должно быть декларативным, при этом синтаксис должен быть максимально простым и наглядным. Ну и статическую типизацию никто не отменял.
Как показано выше, XML совсем не удовлетворяет этим требованиям. Вообще, все попытки сделать из XML шаблонизатор выглядят как помесь ужа с ежом. То XML встраивается в JS, то JS встраивается в XML, а то и изобретается свой примитивный язык программирования с XML-синтаксисом.
Давайте воспользуемся tree-синтаксисом для объявления простой компоненты:
Тут мы просто говорим, что компонент $my_panel - наследник от $mol_view. Нас не волнует как и на каком языке реализован $mol_view, но мы утверждаем, что $mol_panel должен быть реализован точно так же. Например, приведённое выше описание, может развернуться в следующее DOM-дерево:
Как можно заметить, для элемента были автоматически сгенерированы некоторые атрибуты. Прежде всего это id - глобальный идентификатор компонента. Он не зря имеет такой странный вид, ведь его можно скопировать и вставить в консоль, получив тем самым прямой доступ к инстансу компонента, за этот элемент ответственного. Это очень сильно упрощает отладку и исследование чужого кода. Далее идут атрибуты, предназначенные прежде всего для стилизации. Вы можете определить стилизацию для базового компонента, а потом перегрузить её для наследника:
Как видите, нам не потребовался даже css препроцессор, чтобы реализовать наследование в CSS. То есть, для декларации наследования у нас есть только одно место в коде - это описание компоненты. И не нужно отдельно наследоваться в JS, отдельно в CSS, отдельно в HTML. По этому описанию также может быть сгенерирован и TypeScript-класс для этого компонента:
Компонент $mol_view может предоставлять стандартное свойство sub для определения списка дочерних узлов. Поэтому давайте добавим возможность объявления и перегрузки свойств:
Тут мы объявили свойства head и body, представляющие собой содержимое шапки и тела панели. После имени мы указали значение по умолчанию. Чтобы компонента была самодостаточной, значения по умолчанию должны быть всегда. Далее, мы определили свойства Head и Body, значения которых по умолчанию являются инстансами $mol_view с перегруженным свойством sub - оно будет замещено соответствующим свойством из определяемого компонента. Соответственно, при использовании панели мы можем переопределить любое из этих свойств, достигнув тем самым очень высокой гибкости:
Однако, как и в случае с ReactJS, мы потеряли структуру. Но заметим, что после имени свойства при биндинге мы можем тут же указать и значение по умолчанию для него:
В результате у нас получилось довольно компактное описание компоненты. При этом каждый вложенный элемент получил уникальное имя, которое можно использовать для генерации bem-like атрибутов:
В итоге мы сохранили иерархию, не потеряв в гибкости. Создадим вариант панельки с подвалом:
Но что если нам нужна динамика? Например, показывать подвал в зависимости от какого-либо условия. Нет ничего лучше для описания логики, чем язык программирования. Поэтому воспользуемся TypeScript, чтобы добавить логику к декларативному описанию компоненты:
Важно отметить, что компонент самодостаточнен и без логики. Это позволяет верстальщику вывести один и тот же компонент в разных состояниях, не залезая в программный код, что очень сильно повышает его продуктивность:
А так как это описание в конечном счёте будет транслировано в typescript класс, то мы автоматом получим и статическую типизацию. И хотя IDE без специальных плагинов всё же не сможет вывести контекстные подсказки во view.tree коде, компилятор, тем не менее, проверит, что мы не используем необъявленные свойства, передаём им правильные типы значений и тп.
Производительность
В статьях про каждый второй фреймворк пишут, что он разрабатывается с прицелом на производительность, и что тот чуть ли не самый быстрый из всех. Иногда даже приводят в доказательство какие-нибудь синтетические тесты. Как правило код в таких тестах весьма далёк от того, что можно было бы встретить в реальном приложении. В погоне за попугаями теряется простота, гибкость, компонуемость, настраиваемость. Не редко можно встретить чуть ли не полный отказ от возможностей фреймворка ради скорости. Результат такого надругательства над кодом - производительность, которую можно достигнуть приложив знания, умения и время. Но куда важнее не максимальная скорость, которую можно достичь, а минимальная, которую вы получите написав идиоматичный для данного фреймворка код. Чем выше минимальная скорость, тем реже вам придётся браться за напильник, тем менее квалифицированные (и как следствие - менее дорогие) требуются разработчики, тем быстрее идёт разработка. Поэтому фреймворк, на котором сложно сделать медленно, предпочтительнее фреймворка, на котором можно сделать быстро.
Типичный jQuery рендеринг выглядит так: берём шаблон, генерируем новый HTML подставляя в него данные, вставляем в дерево, навешиваем события. Проблема этого подхода в том, что даже малейшее изменение данных требует повторного рендеринга всего шаблона. Это не заметно на малых объёмах данных, но рано или поздно данных становится много и всё начинает тормозить. А что более существенно, при ререндеринге сбрасываются скроллинги, введённые в поля значения и тому подобные вещи. Представьте, что вы пришли побриться, а цирюльник убивает вас, берёт ДНК и выращивает вашего клона без бороды. Решается это обычно вручную - находим нужные элементы и патчим их, что приводит к большому объёму очень хрупкого кода.
Всё, что делает ReactJS - это ускоряет рендеринг всего шаблона за счёт того, что рендерит его не напрямую в DOM дерево, а в промежуточное дерево JS объектов, которые уже сравниваются с реальным деревом и при наличии расхождения точечно меняет DOM. Уже лучше, конечно, но всё равно куча лишней работы: цирюльник берёт вашу ДНК, выращивает клона без бороды, раздевает вас обоих до гола и, педантично сравнивая волосяной покров, с хирургической точностью делает вас похожим на вашего клона, после чего клона отправляют в биореактор. Похожим образом поступает AngularJS с той лишь разницей, что сравнивает он не ваш клон и вас самих, а фотографии какого-то мужика вчера и сегодня, и если обнаруживает, что у того пропала борода, то сбривает её и вам. А что творится в Polymer после 100500 транспиляций и представить сложно.
Но нет ничего эффективней, чем просто показать цирюльнику фотографию чистого подбородка, удобно расположиться в кресле и быть уверенным, что всё, что тот сделает - это просто сбреет вам бороду.
Но вернёмся к бенчмаркам. Производительность всё же надо контролировать. Причём важна производительность не отдельных подсистем в рафинированных условиях, а общая производительность итогового приложения. Поэтому мы воспользуемся для тестирования широко известной коллекцией одного и того же приложения на разных фреймворках - ToDoMVC. Так как основная задача этого проекта - демонстрация идиоматичного кода на разных фреймворках, то он отлично подходит для интегральной оценки их производительности. По сети разбросано множество версий бенчмарка с ToDoMVC приложениями, поэтому пришлось попотеть, собирая из разных форков один ToDoMVC Benchmark реализованный через специальный интерфейс для быстрого написания крутых бенчмарков.
Каждое приложение прогоняется через 2 теста:
Также стоит отметить, что каждое приложение прогоняется ровно один раз в отдельном фрейме, что не позволяет ему кешировать какие-либо объекты.
А теперь уберём из тестирования совсем аутсайдеров и увеличим число задач вдвое:
Ожидаемым является увеличение времени работы не более чем в 2 раза. Однако лишь немногим решениям удалось добиться такой пологой динамики. Как правило замедление составило порядка 2.5, а в некоторых случаях даже превысило 3.
Неправильный выбор фреймворка может замедлить ваше приложение более чем в 10 раз.
Призыв к действию
Хватит это терпеть! Если вы, как и я, устали от этих всё более усложняющихся инструментов, которые не упрощают разработку, а меняют одни сложности на другие, то присоединяйтесь к разработке простого и эффективного сферического фреймворка в безвоздушном пространстве.
Основные характеристики
Документация
Живые примеры