Еще один шаг к лучему GUI
Здесь я просто покажу кусочки решения, которые позволяют ответить на основные вопросы потребителей нашего GUI решения.
в прошлой версии системы я синхронизировал DOM клиента и сервера и это позволяло писать весь код на сторон сервера.
Проблема была в том, что это оказалось не в тренде, появилось много Browser библиотек, новые стандарты, разгон JS, новые возможности в браузере, все переориентировались…
И я решил синхронизировать только View модель, построив нормальное решение для работы с ней в браузере, ниже немного о предпринятых шагах.
Итак, ответы на чаяния client-side разработчика:
1) Простота. Хочу понимать, как это работает без многоуровневых диаграмм. Фреймворк и принцип его работы должны быть просты и понятны.
четыре составляющих нашего решения, которые слабо связаны, и при желании могут быть заменены на аналоги:
- AngularLight — минималистическая библиотека 2way биндинга — связующее звено для моделей и DOM (Dirty)
- почему я выбрал именно AngularLight — тема для отдельной дискуссии
- aplib — собственная разработка для удобной работы с моделями
- концепция описания компонентов — набор рекомендаций, который позволит просто делать компоненты, которые можно легко использовать в самых разных подходах к web gui.
- роутинг. (про него тут не буду рассказывать)
первые две части наиболее важны. AngularLight мы используем немного доработанный, и главное с другой документацией, т.к. у него самого документация очень плохая.
дальше можете переходить к следующему разделу, если не интересна более техническая информация:
мини туториал, чтобы понять принцип работы
— откройте консоль в браузере в котором загружен alight.
— Наберите
scope = alight.Scope(); scope.$watch('title', function(value) { console.log('title =', value) }) ; scope.title = 'new' scope.$scan() ; // print scope.title = 'linux' ; scope.$scan() ; // print scope.title = 'linux'; scope.$scan() // do nothing
не сложно ведь, да? тут мы сделали контекст, начали следить за полем ‘title’, и когда сделали $scan(), то система обнаружила, что значения поменялись и вызвала наш код.
Это основа. вокруг нее крутится способ делать директивы
— наберите
s.r = 'blabla' s.$evalText('trtrtr {{v}} вот так вот {{r+v}}')
— посмотрите на API он очень прост http://angularlight.org/doc/api.html так работают текстовые директивы, в {{}} скобках могут быть выражения, или вызовы методов, или вообще разные функции (и текстовые директивы)
вот как работают директивы: официальная документация http://angularlight.org/doc/directives/
но лучше привести свой пример (НЕ ТИПОВОЙ, т.к. SVG, а не HTML)
Мне нужно было под слоем html разместить svg линии, и каждую линию я рисовал своей директивой, вот код директивы
alight.directives.sup.path=function(e,name,scope){ var node=scope[name]; scope.$watch(name+'._edgeCoord()._edgeCoord', function(c){ if(c[0]){ var v='M'+c[0]+','+c[1]; v+='C'+c[2]+','+c[3]+','; v+=c[4]+','+c[5]+','; v+=c[6]+','+c[7] ; $(e).attr('d', v); $(e).attr('visibility', 'visible'); $('svg').attr('width' ,(scope.nodes.reduce(function(prev, node){ return Math.max(prev, node.left)}, -1) +20)*dpi.get(false) ); $('svg').attr('height',(scope.nodes.reduce(function(prev, node){ return Math.max(prev, node.top)}, -1) +20)*dpi.get(false) ); }else{ $(e).attr('d', 'M0,0'); $(e).attr('visibility', 'hidden') } }, {readOnly : true}); }
Использование: тут сразу много всего можно обсудить,а в коде я использовал эту директиву так:
<path fill="none" stroke="rgba(0,0,0, 0.7)" stroke-width="2" stroke-linecap="round" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); stroke-linecap: round;" d="M0,0M1,1" al-repeat="nodeid in helper.nodesIDs()" sup-path="nodeid" ></path>
Например, опытные кодеры заметят связи с конкретным контекстом помимо прав самой директивы ( nodeid in helper.nodeIDs() ) а также доступ к другим элементам не только к внутренним для e, например, ( или $(‘svg’) ). для моего случая это допустимо, и позволяет быстрее решить задачу, быстрее получить прототип… просто AL в основном для HTML, но я показал, что даже с SVG использовать его удобно.
Cуть такая, есть директивы а) текстовые — они позволяют вставить в разметку текст или в атрибуты значения из модели, например так:
а есть директивы б) “для элемента”, которые могут сами что -то делать с элементом, как sup-path из примера выше — более гибкий вариант.
я рекомендую придерживаться следующих правил, чтобы избежать проблем: (это касается не только AL (Angular Light), но и других подобных библиотек).
- следи за зависимостями в модели — модель должна исключать гонку изменений по цепочке подписок
- не подписывайся на изменения какой-то части модели, если в обработке эта часть модели будет изменяться (ajax на изменение)
- если по событию DOM меняется состояние модели и по этому же событию хочется обработать изменение — не делай так, лучше подпишись на обработку события, а обработку произведи в каллбеке для setTimeout( callback, 0); — так будет гарантировано, что обработка вызовется после изменения всех состояний, и состояние которое она прочитает будет корректным (порядок срабатывания каллбеков при обработке событий не гарантируется браузерами)
- подписывайся на изменения части модели только если знаешь, что в череде обработок и изменений не будет зацикливания — если прослеживается dataflow без рекурсии. если прослеживается — все обработки лучше подписывать на dom события, не на части модели.
- не используй параллельно с AL «jQuery» для «лазания по DOM» — нарушаешь свободу дизайнера менять интерфейс независимо от разработчика. (это можно в деклатаривных директивах, но не в БЛ коде)
- не вызывай $scan до тех пор пока это не станет необходимо — вызывай его как можно реже.
- в больших моделях конструируй свои контексты и привязывай их (например для компонентов), чтобы добавить зернистость, увеличить производительность и не засорять родительские контексты, благо , что в AL это в удовольствие — не то, что в других фреймворках.
- Используй , по возможности, дочерние контексты вместо корневых
- пиши свои директивы и библиотеки директив. в AL это очень просто, проще нет нигде. производительность и наглядность увеличивается в разы.
конечно такой подходсокрашает сложность итогового решения, очень модульный итд.
2) Высокая скорость разработки. Не хочу городить тонны кода, чтобы выполнить простецкую операцию.
- ты не должен сам делать операции вообще. смысл декларативного подхода в том, что ты говоришь: “это должно быть. Работаешь только со своей моделью, остальное делает система сама”.
- скорость разработки. Основная трудоёмкость связана не самим фреймворком, много труда уходит на работу со своими моделями.
- Прочие фреймворки (и AL тоже) уделяют внимание именно отображению модели в DOM, но не самой модели и работе с ней. Для того чтобы упростить с ней работу нужно иное средство.
Тут я предлагаю воспользоваться специально разработанной для этого библиотекой “aplib”, предназначенной для упрощения и сокращения объёма кода, работающего с вашей моделью (ViewModel) .
TODO ссылка на aplib, ссылка на видео с ее использованием и принципом работы.
Итак, модульная библиотека для работы с моделями я назвал ее aplib.js.
Эта штука сильно упрощает работу с dirty check моделями, может быть использована для AngularLight а также для Angular и вообще где угодно в коде.
важно: на данный момент поддерживаются браузеры где есть JS getter \ setter, т.е. ЛЮБОЙ, но IE>=11. При моем желании я могу сделать версию под любой (старый) браузер, но лично я смысла пока в этом не вижу совсем.
проблемы с которыми борется данная библиотека хорошо/подробно писаны тут: Gui_проблемы.
ситуация:
- есть модель на JS, которая пришла в браузер, например с сервера (AJAX, либо встроена в код страницы) не важно как
- в этой модели, например, иерархия/список иерархии/списков итд объектов — по смыслу это объекты определенного класса с каким то способом идентификации (идентификаторами). или контейнеры для них
- возможно, там уже один и тот же объект фигурирует несколько раз в разных местах, но это JSON и после десериализации это для JS VM разные объекты (разные копии объекта заданного класса с одинаковым ид)
- объект каждого класса может содержать или нет свойственный ему набор полей, для каждого поля нам известно его значение по умолчанию
- в процессе работы JS эти объекты будут меняться и связываться друг с другом иначе чем были, буду появляться мапы одних объектов/ид на другие объекты/ид, какие-то списки, появляться новые объекты
типовые задачи:
- если у объекта некоего класса по нашему разумению не хватает свойственных классу полей — они должны появится, чтобы у нашего кода было меньше шансов вылетать, мы не ддолжны за этим следить
- у каждого класса объектов могут быть методы (для целей GUI) — они должны появится
- у каждого класса объектов могут быть методы для работ с серверной частью (Бизнес логика) — она должна иметь возможность тоже там появится
- много копий одного и тогоже логического объекта должны меняться синхронно, не важно какую копию я меняю.
- простой доступ ко всем копиям по ид. PUSH с сервера — нужно для объекта с ид=3, класса=мой_объект изменить поле=название на новое значение. как быстро его найти и сделать чтобы это касалось всех таких объектов?
- Аспекты, расширение базовых операций. если любые действия с объектом должны логироваться или специально обрабатываться, например, синхронизироваться с сервером — это должно происходить само
код в студию: описание наших классов (классы Бизнес-логики)
для решения задач требуется воспользоваться aplib.js и всего делов 😉
короче, чтобы понять, загрузите в браузер код и сделайте в консоли:
Описание «ваших классов»
ap.jsdesc['server'] = { // server - Имя нашего класса fields:{ // набор полей со значениями по умолчанию name: 'имя', address: 'http://127.0.0.1:8888', connected: false, s_ostype:'Linux', s_db_type: 'mysql', r_db_state: '', r_nginx_state: '', r_jboss_state: '', checked: false, }, methods:{ getid:{ // один из методов, который появится во всех экземплярах этого класса code : function(){ return { getid: ap.walk.jslabels.objid.id(this) } // this - это сам экземпляр класса, возвращаем словарь } } } };
так вы описали некий View класс вашей логики, у него есть поля, значения в них по умолчанию, и метод, у которого есть код.
допустим, вам из ajax пришла структура данных, в которой объекты типа server помечены метками objid, тогда опишем эти метки.
ap.walk.jslabels = { objid :{ classname :function(o){ var a = $(o).attr('objid'); return a.split('_')[0]; }, id : function(o){ var a = $(o).attr('objid'); return a.split('_')[1]; } } }
тут есть две функции, одна возвращает имя класса, другая ид объекта. “o” — объект.
при таком описании, если objid=”server_5” получится, что система распознает объект как экземпляр сервера с ид=5.
давайте приведем пример как это работает. например из ajax к вам пришла структура
servers=[ {name:'s1', objid:"server_1"}, {name:'s2', objid:'server_2'}, { someinfo:1, S: { objid:'server_2', address:'http://192.168.11.1' } }, {a:1}, {b:2}, {c:3} ]; servers
посмотрите внимательно, что из себя представляют элементы этого списка. это обычные объекты, помеченные нашим специальным описанным полем objid , но помечены не все.
причем, объект с ИД=2 есть в двух экземплярах, в одном случае у него задано имя, в другом только адрес, но помечены эти структуры как один и тот же объект.
а теперь сделаем с ними:
ap.walk.walkJS(servers); servers
теперь у нас есть не просто какие-то поля, теперь у нас есть все описанные для объекта этого класса поля, у нас есть методы итд. прямо по месту, объекты остались теми , же ссылки не поехали.теперь посмотрите, на servers, обратите внимание на содержимое объектов.
лог моей консоли:
servers[0].name "s1" servers[0].address "http://127.0.0.1:8888" servers[1].getid().getid "2" servers[1].name "s2" servers[1].address "http://192.168.11.1"
чтобы поменять все копии одного и тоже объекта не нужно их все искать, достаточно поменять любую копию, и даже ее искать в модели не обязательно! достаточно:
ap.walk.getObject('server', 2).name='s2_2';
эта маленькая штучка делает код работы с моделями очень чистым и простым — больше не надо лазать по всей модели чтобы поменять какой-то объект, и помнить где он может быть. ООООЧЕНЬ ускоряет разработку. т.к. вы просто работаете с данными и не делаете лишних действий совсем, это удобно делать прямо на JS.
Лирика:
кроме того, модификации происходят более глубокие, например, появляется возможность избежать кодирования на JS, т.к. происходит синхронизация View модели , аналогичная синхронизации DOM дерева из прошлой итерации системы (ссылка на верху статьи). зачем это надо ? да ведь манипуляция с View происходит из View контроллера, который часть контроллера вообще. а он часть Бизнес-логики. а ее лучше располагать на сервере.
View Model шире, чем Model. так как в ней содержится помимо бизнес свойств еще и обработка отображения, всякие флаги и компоненты для работы с пользователем.
логика для View может быть в браузере, остальная логика может быть на сервере или в браузере…
возможно это долгий спор, но я вам скажу факт: когда я делаю прототипы приложений я пишу логику на сервере и не пишу ее в браузере, данный механизм автоматической связи моделей клиента и сервера ускоряет мою работу СУЩЕСТВЕННО.
а так как клиентский интерфейс состоит из работы дизайнера, декларативных компонентов и декларативного описания связей с моделью вообще без прикладного JS кода, то прототип удается сделать буквально за 1 день.
3) Гибкость (чтобы не приходилось городить костыли, если реальная задача чуть уходит от той, что показана в презенташке на офф. сайте).
вам повезло, APLIB — РАСШИРЯЕМАЯ.
можно добавить в мета-описание модели свои директивы, и гибкость очень сильно увеличивается.
например,
- надо сделать обработку доступа к полям — пожалуйста,
- надо синхронизировать объекты с сервером — пожалуйста,
- надо делать проверки — тоже не проблема.
- пишешь это все в одном месте,
- и работаешь с данными как обычно,
- все, что надо произойдет само.
TODO ссылка на пример, в котором к работе с моделями
- добавляется логирование записей в заданные поля экземпляров какого-то класса,
- оборачивается метод
- добавляется свой раздел в описание модели “sync”
============ ОСТАЛЬНОЕ УБРАТЬ — СЛИШКОМ МНОГО TODO ========
4) Удобная связь с данными. Чтобы было удобно делать запросы и удобно использовать/отрисовывать полученные данные.
AL как раз и делает эту связь . Я считаю — она простая и удобная, и главное контролируемая.
> удобно делать запросы…
Тут многое зависит от архитектуры вашего приложения, но сделать это удобным вообще-то дело трех часов.
> удобно отрисовывать данные…
Имеете ввиду canvas? — Да, бывает — в декларативном мире императивные операции иногда тоже нужны.
Например, есть модель, часть ее отрисовывается на DOM за счет биндингов, а часть ее отрисовывается на CANVAS какими-то функциями… Тут есть применение для $watch. TODO пример
короче это тоже удобно делать.
Я считаю, что подобный код лучше располагать в вашей директиве и метить ей canvas.
5) Модульность. Без нее нынче никуда.
Это отдельная проблема решается отдельными средствами.
Запуск bootstrap в AL можно сделать множеством способов, в том числе и из подгружаемых модулей.
Фреймворк не обязан навязывать решение этой мелкой частной проблемы.
аналогично отвечаю и про роутинг, это нельзя мешать в одну кучу — это перегужает повседневную работу, быстрее приходит утомление.
другое дело компоненты, я бы не сказал, что компоненты и модульность это одно и то же.
третья составляющая решения — компоненты. Например, Polymer — барахло, но вектор у них верный.
Есть возможность описать компонент так, что его легко будет использовать из :
- простой JS или Jquery код, на подобии jquery-ui рекомендаций
- Mustache like templates, Текстовые шаблонизаторы
- DOM based шаблонизаторы
- dirty биндинги
- observable биндинги
- крепить внешнюю модель в качестве контроллера компонентов
- прочие пока не известные методы.
все эти способы можно перемешивать. Ещё ни один фреймворк не предоставил нам компонентов столь свободных в стиле использования.
Это сильно расширяет $ рынок для написанного компонента.
Но здесь не то место чтобы описывать идеологию компонентов (а без идеологии в случае с ними — никуда). Это предмет отдельной статьи.
6) Удобство отладки. В том фремворке, с которым приходится работать сейчас, очень часто возникает ситуация, когда все падает и выдается сообщение типа «что-то пошло не так». Без стектрейса, без указания, где хоть примерно упало. Просто упало и все.
Понимаю, из-за этого невозможно пользоваться, например, handlebars, который выдает при компиляции шаблона описание ошибки, которое ссылается не туда и вообще бесполезно…
лично я, разрабатываю по чуть чуть, сделал — посмотрел.
в своем фреймворке я знаю как отладить и сложную ситуацию, даже очень сложную — это не очень трудно, но нужно еще лучше над этим поработать.
TODO как отлаживать сложные ошибки
Что надежно уменьшает число ошибок ?
TDD — Test driven development, орг. способы менее надежны.
В GUI эту методику трудно применить обычно, я считаю в этом направлении нужно работать. по TDD в GUI пока нормальных предложений не имею.
7) Естественно, сайт на выходе не должен тормозить.
общая производительность сильно зависит от производительности биндинга, и количества выкрутасов, для упрощения работы с моделью и увеличения наглядности кода.
AL один из самых быстрых биндингов (и быстрее кода на jquery), по балансу цена/качество он лучше всех рассморенных (одна из главных причин для его выбора)
Ссылка, убедитесь в производительности сами
8) Сгененированного кода не должно быть очень много. Иначе исходный код страницы станет нечитаем. Да и вообще, хотелось бы как можно меньше генерируемого кода.
сгенерированного кода вообще нет.
есть только наглядный не замусоренный исходный код
В html — верстка, компоненты, красОты. JS кода мало — там View модели и роутинг. Может быть часть БЛ в JS, может не быть. всякие лаяуты лучше оформить в виде декларативных компонентов… короче КПД кода близок к 80%.
9) Должна быть возможность масштабировать полученное приложение.
масштабировать gui? в каком смысле?
если имеется ввиду делать приложения из готовых приложений и делать их композиции… тогда:
это все архитектурой определяется, а именно:
- как связаны между собой контексты, сколько их
- как связаны контексты и описания классов
- могут ли одни и те же бизнес-классы работать в разных приложениях в рамках одной страницы, от этого зависит способ обработки событий на связанных с ними элементах, при вызовах методах на моделях… в общем это зависит от того насколько вы упрощаете вашу ситуацию при проектировании классов приложений.
- одинаковы ли серверные части приложений в gui составляющих композицию
- и т. д.
Это зависит от разработчиков приложений! ну не может фреймворк этого однозначно задать.
Могу сказать одно: предлагаемый подход не противоречит возможностям масштабирования.
Надо не только понимать как работает та или иная часть, но и следовать советам по использованию и следовать концепциям заложенным в систему, и все будет успешно.
До новых встреч!
за кадром осталось куча материалов (руды, отвалов, но и самородков).
особенно на мой взгляд интересны
- варианты которые я пробовал, и к каким выводам приходил.
- сравнения фреймворков.
- истории реальных применений
- или как я нечаянно написал свой Ember.js а потом узнал, что он уже есть (блин как две капли воды получилось) и тут интересен именно путь, видимо разработчики Ember в мыслях шли тем же путем. и в итоге сейчас я досконально могу рассказать минусы подобных решений.
- размышления о возможности совмещения фреймворков и способа описания компонентов
- критика существующих решений. тут накопилась гора из критики 23-х фреймворков, которые я анализировал когда делал прошлую версию системы, и в этот раз добавилось еще с десяток, но это уже по большей части клиентские JS штуки.
- сравнение концептов и архитектур современных библиотек биндинга, особенно глубоко я сравнивал Angular и Knockout. ну прямо диаметрально противоположные… хотя и разные весовые категории, но концепты очень выпячены у них. а также базирующиеся на пересоздании узлов DOM, и с шаблонными подходами, типа dombars… смотрел и на экзотику типа transparency.
- еще интересно : на каждом из этих фреймворках я написал одно и то же не самое простое приложение, и приходилось каждый раз придумывать свой костыль, хоть один но надо было. Тут интересно сравнить эти костыли и решить для себя, что наиболее приемлемо.
Александр Петров
31.01.2015 в 13:06> мне нужно было под слоем html разместить svg линии, и каждую линию я рисовал своей директивой, вот код директивы
было бы хорошо показать результат (скриншот, например). По коду не очень понятно, что происходит.
>тут сразу много всего можно обсудить,а в коде я использовал эту директиву так:
< пусто?>
>проблемы с которыми борется данная библиотека хорошо/подробно писаны тут: Gui_проблемы.
эта ссылка (Gui_проблемы), наверное, доступна только участникам проекта в Redmine
Александр Петров
31.01.2015 в 13:07И я бы сократил заметку до 3х первых пунктов — они хорошие. конструктивные. остальное — уже просто разговор с невидимым читателем, кода нет, много todo, много просто мыслей «обовсем»
Михаил
02.02.2015 в 14:45сократить заметку можно, но там же есть ответы…, может ответы сократить сделать меньше слов?
ладно, думаю надо ее менять.