Комментарии 55
Чем вам не угодли циклы и условные операторы? :)
Вообще, решение на голом JS + JQuery (DOM + ajax) было бы куда понятней и проще (по моему).
А решение на React/Angular было бы совсем крохотным.
Так в чем преимущество basis.js в этом случае?
Чем вам не угодли циклы и условные операторы? :)
Ничем. Я не призываю от них отказываться, т.к. это невозможно. Просто их отсутствие в клиентском коде дает более линейный код.
Так в чем преимущество basis.js в этом случае?
Как я и говорил в прошлой статье, для простых задач, basis.js может показаться избыточным. Потерпите, скоро рассматриваемые задачи усложнятся ;) мы подбираемся к ним постепенно…
Если при решении простых задач он избыточен, то он будет неудобен для решения сложных задач.
Ведь главное преимущество тех же Angular/React в том, что они дают возможность разбить сложную задачу на несколько мелких и решить их просто. В вашем случае так не получится — вы сами сказали, что мелкие задачи базисом не решают.
Давайте на чистоту, решения на Angular и тем более React будут выглядеть массивней.
- В случае с React, вам придется организовать всю работу с данными самому, начиная от XHR;
- С Angular часть логики будут размазана между контроллером и шаблоном, придется изучить как работает $http или ng-resource.
В обоих случаях нужно придумать какие-то флаги для состояний при работе с данными, чтобы избежать описанной проблемы с empty
.
Открой репозиторий этого пример, там 59 строк кода, из них 8 подключение и если знать как работает Value.query
, то код легко читается.
И главное,
было бы куда понятней и проще
Это если вы знаете, что такое React/Angular, иначе ничего не понятно, какие-то props/state, html в JS (?! который не поддерживает всех свойств, а вместо class -> className) или магический DI в Angular (в купе с неймингом factory/service) и $apply в придачу.
Не зная документации, всё страшно.
P.S. Если что, basis не использую, но статьи мне нравятся, а именно организация работы с потоками данных, с нетерпением жду продолжения.
Но чисто из интереса, какие свойства не поддерживает JSX? Как-то я никогда не сталкивался с этой проблемой, вдруг когда пригодится.
И да, я не говорил, что статья плохая. Хотя стиль изложения довольно сложный.
Не поддерживается любой кастомный атрибут, только data-*, aria-* и белый список.
С событиями тоже самое, только белый список.
Притом еще не очень понятна логика, почему список именно такой, думал они убрали всё что устарело по мнению html5, но это не так.
Ну ладно, может какие-то частные случаи и правда бывают.
Ну, это до первой интеграции со сторонней либой, особенно если необходимо слушать костюмные события, придется использовать ref
'ы и руками подписывать/отписываться от событий на mount/unmout.
Но вообще, если используете кастомные события, то ручное отписывание-подписывание это вполне нормально, как мне кажется.
Ну, не имя возможности написать onCustomEvent={...}, спасибо и на этом. Но если требуется список нод, то решение через ref
выглядело страшно, но хорошие новости есть, теперь вместо имени, можно и нужно использовать callback.
Представьте, что список юзеров нужен в других местах и там так же надо знать в каком он состоянии, а еще следить за его изменениями. Если с флагом loading
я ещё могу представить, как можно выкрутится, то во как следить за изменениями вне контроллера, а это значит не имея scope.$watch
, нет.
Например сервис как общее хранилище, или pubsub/FRP, тот же $watch можно свободно использовать и вне контроллера (опять же это решает разработчик), ChangeDetector — это самостоятельный объект который просто отслеживает изменения, и его можно использовать где угодно.
Я чаще всего использую сервис + pubsub, т.к. это просто и удобно.
Всё верно, но это нужно знать, но мы же в рамках basis говорим, он из коробки говорит как сделать и потом не бояться масштабирования. Я всё это к тому, что эквивалентный код, этому примеру не такой простой, вот и всё.
В basis.js с этим несколько проще. view привязывая данные «сообщают» им что есть активный потребитель, и они при необходимости синхронизируются. Можно всегда получить текущее актуальное состояние данных и поменять интерфейс соотвественно. По большей части все разделено и не требует изобретения велосипедов (по крайней мере для частых кейсов точно)
Как минимум не хватает обработки ошибок. Ваш пример может упасть в непредсказуемый момент и пользователь ничего не поймёт.
Да, Angular Light не предлагает инструментов для синхронизации с сервером, тут «свободный полет», и у меня на проектах не возникает с этим проблем.
Я про то, что всё компактно лишь в примерах, а как начинаешь все кейсы учитывать так код и обрастает копипастой.
У меня в разных проектах часто разные нюансы, в итоге какая-то универсальная тулза не очень подходит, например в одном проекте используются веб сокеты для данных, в другом — интернет явление временное и модель должна синхронизировать отложено с учетом того что девайс может вообще уснуть и проснутся в любой момент, или приложение может быть заморожено.
В basis.js есть возможность это законфигурировать? Думаю тоже обрастет доп. кодом, что сильно понизит ценность готовых моделей и синхронизации.
Это похоже на «универсальный ЯП» который не существует.
Не знаю насчёт базиса, у меня свой велосипед :-) фрп, впринципе, позволяет неплохо абстрагироваться от протокола взаимодействия с сервером. Будь он синхронным или асинхронным, затягивающим или толкающим, реального времени или сессионным.
Но в ангуляре не такой совсем. По умолчанию, ангуляр просто остановит рендеринг. Чтобы он этого не делал, нужно заворачивать всё в трайкатчи или промисы и вручную прокидывать ошибки. В лайте вроде так же. Или я не прав?
Ошибка сети, ошибка парсинга ответа, ошибка доступа к полям нулла.
Ошибка сети, ошибка парсинга ответа
всё в трайкатчи или промисы и вручную прокидывать ошибки.
Все сводится к тому что вы получаете событие об ошибки либо кетч, и меняете где-то статус/посылаете событие. В итоте нет разницы ваш фрп это или алайт.
Тот же парсинг json у вас будет где-то завернут в трайкетч. Далее измененный статус уже рендерится. С фрп то же самое, кетч делает пуш, оно идет по звеньям, меняется интересующий статус и вызывается событие на рендеринг.
ошибка доступа к полям нулла
Если речь про рендеринг, Ангуляр 1 просто выведет пустую строку. В Ангуляр 2 и алайт это опционально, сейчас там elvis оператор.
Вопрос лишь в том, делается ли это автоматически, или руками прописывается везде.
А при чём тут экпрешены в шаблонах? :-)
А при чём тут экпрешены в шаблонах? :-)В ангуляре 1 выражение {{obj.name}} выдает пустую строку если obj == null, в ангуляре 2 оно будет выдавать ошибку, и если в вашем приложении так задумано, что obj может быть null, то нужно писать так: {{obj?.name}}. Т.е. сейчас ошибка не прячется и её сразу видно.
Я думал вы это имеете ввиду, Ангуляр — это же про биндинг в первую очередь.
Не, я говорил про различные ошибки в коде контроллера. А в шаблоне будет что-то типа {{view.getUserListOnDemand()}}
.
Всё же GUI приложение не должно молча падать, а должно показывать, что такой-то функционал дал сбой.
должно показывать, что такой-то функционал дал сбойAngular Light для данного примера выдает в консоль:
1) текст ошибки
2) правильный трейс (можно кликнуть и перейти в файл на строку с ошибкой)
3) элемент на котором случилась проблема (можно кликнуть и перейти в DOM на проблемный элемент)
4) «привязанный» скоуп с данными
и ещё доп. информацию в зависимости от того места где произошла ошибка. Это работает для Chrome (для FF тоже должно).
Это замечательно, но пользователь-то видит:
{{view.getUserListOnDemand()}}
Кстати, у вас там ошибка выводится 2 раза с разными стектрейсами.
2 раза с разными стектрейсами
Потому что 2 раза вызывается, главное что трейсы правильные (кстати ключевая часть трейса одна и та же, я даже не обратил внимания)
но пользователь-то видит:
Это должен видеть разработчик при тестировании (либо авто-тестер).
А вы предлагаете прятать ошибку?
В basis.js при работе с данными абстрагируются от способа получения этих данных. По сути вызывается метод, который что-то делает и меняет состояние данных в соответствии с происходящим. На уровне данных (модели, коллекции) мы всегда работаем с механизмом состояний, чтобы понять что с ними происходит. Получение же данных может быть организовано как угодно хоть через XHR, хоть через сокеты, хоть из IndexDB/localStorage/etc или вообще генерацией в web worker'ах. Эта реализация выносится отдельно и может меняться, без необходимости менять описание/логику данных и представления.
По сути вызывается метод, который что-то делает и меняет состояние данных
Эта реализация выносится отдельно и может меняться
Если в данном случае синхронизация все равно делается вручную, тогда непонятно зачем для этого basis.js, если эту синхронизацию можно вызвать напрямую. Видимо эти враперы позволяют состыковать с остальной частью басиса.
Забацал на своём велосипеде: онлайн, код.
Кода получилось больше, но и функционала больше:
- Добавлен поиск пользователей по мере набора.
- Добавлена кнопка "перезагрузить".
- Добавлен вывод ошибок (в том числе и индикатора загрузки).
- Поддерживается задержка перед запросом, чтобы не слать 10 запросов в секунду.
- Сломанный контрол не ломает соседей.
- Поддерживается индикация сломанных (и ожидающих загрузки данных в том числе) контролов.
- Поддерживается ленивый рендеринг по мере прокрутки.
- Кнопка сохранения активируется только если есть несохранённые изменения.
- Пока пользователь не ввёл поисковой запрос — показывается только поле поиска.
- Модель отделена от вьюшки.
- Поисковой запрос сохраняется в урле.
Попутно порефакторил у себя рендеринг. Бенчмарки: онлайн, код. Интересная особенность: если отрендерить Raw и кликнуть по какой-нибудь строке, то дальнейшие ререндеры будут стабильно в 4 раза медленней. Почему так?
Бенчмарки:Сделал pull-request для Angular Light, у меня тест выдает такие результаты (заполнение и обновление):
React: 459ms - 166ms
Angular.js: 571ms - 96ms
Angular Light (alight): 236ms - 79ms
Knockout.js: 2012ms - 2008ms
Raw: 654ms - 625ms
$mol: 549ms - 114ms
Виртуальный «бесконечный» список можно сделать примерно так, при этом используется всего несколько строк DOM.
Интересная особенность: если отрендерить Raw и кликнуть...По хорошему нужно каждый фреймворк в отдельном окне и с одинаковым контентом тестировать, т.к. они друг на друга влияют, при каждом следующем тесте объем DOM возрастает и т.п. Я наблюдал в одном тесте, что простая замена одного символа в тексте на другой меняла +- 20% производительности.
Первичный рендеринг впечатляет.
Всё же скроллится он как-то не так — место перемещения строк, происходит перемещение данных между прибитыми гвоздями строками.
Разумеется я перезагружал страницу и тестировал каждый фреймворк отдельно. Странное поведение Raw наблюдается независимо от остальных фреймворков.
Выше lahmatiy написал же:
фреймворк развивается где-то с 2006го
Backbone — появился в конце 2010, Backbone.Ribs — 2014
Да и похожего у них очень мало, только факт наличия обертки на данными и коллекциями, да и то, это если очень сильно упростить. Basis наверно лучше сравнивать с KO, как мне кажется, если уж сравнивать, если я не прав, путь Роман меня поправит.
Не KO тут ни при чём, в KO автоматический трекинг зависимостей.
Так или иначе похожесть в каких-то моментах можно найти между многими фреймворками/библиотеками. Но куда важней не конкретная функциональность, а то как это все между собой стыкуется и что в итоге получается.
Token/Value/Expresion в basis.js близки к Observable/computed в KO — это так. В Ember тоже есть аналоги, как, наверняка, и в других фреймворках. FRP совсем не уникальный паттерн, как и ООП например. То что это реализовано совсем по разному не имеет того значения, как возможности, которые дает конкретное решение. В случае basis.js, например, реактивные значения могут привязываться ко многим ключевым свойствам объектов и если они содержат подходящее значение, то его подстановка происходит автоматически. Думаю это еще будет раскрыто в будущих статьях.
Что же касается Backbone и подобных фреймворков, то они не стремятся к разделению на логику и представление. Вот пример из readme Backbone.ribs
var BindingView = Backbone.Ribs.View.extend({
bindings: {
'el': {
toggle: 'model.isVisible'
},
'.bind-text': {
text: 'model.title'
}
},
el: '<div class="bind">' +
'<span class="bind-text"></span>' +
'</div>',
initialize: function () {
this.model = new Backbone.Ribs.Model({
isVisible: true,
title: 'Ribs'
});
this.$el.appendTo('body');
}
});
Что здесь плохо (ключевое):
- view знает о разметке все, более того определяет как ее менять
- сильная завязка на структуру разметки (для селекторов) и нужно хитро размечать — например, для вставки текстового значения нужен контейнер
- разметка (шаблон) сильно завязывается на особенность реализации view (изменение разметки может сломать view)
- модель назначается один раз и поменять ее без пересоздания view нельзя (или крайне не просто, поправьте меня если не прав)
Давайте взглянем на тот же пример на basis.js
var BindingView = Node.subclass({
binding: {
'isVisible': 'data:',
'title': 'data:'
},
template: resource('./rel/path/to/template.tmpl'),
container: document.body
});
В basis.js практически все можно изменить динамически и он максимально нацеливает на разделение логики и представления.
В чем разница:
- разметка (шаблон) вынесены из кода, и view совершенно все равно что там (да, совсем — хоть пустая строка) — это так же позволяет реализовывать из коробки продвинутые инструменты вроде live update (обновление разметки компонент без перезагрузки страницы), изоляцию стилей, линтинг и т.д. (немного про инструменты я рассказывал год назад https://www.youtube.com/watch?v=IUtbbN9aevU&list=PLf0s9ihTnfHyGOoQ_7Urte2lBRrJzqRqa)
- в биндингах описываются какие значения мы хотим чтобы были доступны в шаблоне и как они вычисляются (тут пример простой, и используется сокращенная запись — брать значения как есть из поля
data
) — на усмотрение шаблона использовать эти значения или нет (если что-то не используется, то это и не вычисляется) - компонент абстрагируется от источника данных, он может сам хранить данные или брать из привязанной модели (через механизм делегирования), способ получения данных может быть в сторонке и сколь угодно сложным
// создаем view с собственными данными
var view = BindingView({
data: { title: 'own data' }
});
// view.data.title === 'own data'
// обновляем
view.update({ title: 'updated title' });
// view.data.title === 'updated title'
// а теперь привяжем модель
var foo = new basis.data.Object({
data: { title: 'model data (foo)' }
});
view.setDelegate(foo);
// view.data.title === 'model data (foo)'
// меняем данные у view, но также меняются данные у привязанной в текущей момент модели
view.update({ title: 'updated (foo)' });
// view.data.title === 'updated (foo)'
// foo.data.title === 'updated (foo)'
// привяжем другую модель
var bar = new DataObject({
data: { title: 'model data (bar)' }
});
view.setDelegate(bar);
// view.data.title === 'model data (bar)'
// давайте нужную модель будет хранить реактивное значение
var selectedModel = new basis.Token(foo); // сохраним в начале туда модель foo
view.setDelegate(selectedModel);
// view.data.title === 'updated (foo)'
// а теперь поменяем значение реактивного значения на модель bar
selectedModel.set(bar);
// view.data.title === 'updated (bar)'
// а теперь хотим чтобы view назначалась вторая по величине (amount) модель в наборе
var models = new basis.data.Dataset({ // коллекция моделей
items: [
new basis.data.Object({ data: { amount: 1, title: 'one' } }),
new basis.data.Object({ data: { amount: 123, title: 'two' } }),
new basis.data.Object({ data: { amount: 42, title: 'three' } }),
new basis.data.Object({ data: { amount: 4, title: 'four' } }),
new basis.data.Object({ data: { amount: 13, title: 'five' } })
]
});
var sliceByAmount = new basis.data.dataset.Slice({ // автоматическая коллекция: упорядочивает по rule модели и возвращает заданное "окно" (срез) моделей
source: models, // делаем срез из коллекции models
rule: 'data.amount',
offset: 1, // начало среза начинается со второго элемента
limit: 2, // размер среза (для примера)
orderDesc: true // порядок по убыванию
});
// в sliceByAmount модели будут располагаться так (значение amount, `[` и `]` обозначают границы среза)
// 123 [ 42 13 ] 4 1
view.setDelegate(sliceByAmount.left(0)); // назначаем первую (нулевую) модель на левой границе среза
// view.data.title === 'three'
// давайте делать срез только по нечетным моделям: для этого в качестве источника среза назначаем коллекцию-фильтр
sliceByAmount.setSource(new basis.data.dataset.Filter({
source: models,
rule: function(model) {
return model.data.amount % 2 === 1;
}
}));
// теперь в sliceByAmount только нечетные модели:
// 123 [ 13 1 ]
// а во view актуальное значение:
// view.data.title === 'five'
// добавим моделей в models?
models.add([
new basis.data.Object({ data: { amount: 77, title: 'six' } }),
new basis.data.Object({ data: { amount: 100, title: 'seven' } }),
new basis.data.Object({ data: { amount: 111, title: 'eight' } })
]);
// фильтр отфильтрует ненужное и в sliceByAmount будут только нечетные модели в нужном порядке:
// 123 [ 111 77 ] 13 1
// во view по прежнему актуальное значение
// view.data.title === 'eight'
// хотим чтобы бралось третье по величине значение? один из способов поменять смещение в срезе:
sliceByAmount.setOffset(2);
// view.data.title === 'six'
// и т.д. и т.п.
Сорри за полотно кода. Вероятно не зная фреймворка в этом может сложно разобраться, но это быстро проходит.
Ключевая же идея здесь в том, что в примере несколько раз измен способ получения данных для view. При этом реализация самого view осталась нетронутой. И вся работа с данными может быть вынесена отдельно и меняться в дальнейшем. О синхронизации данных заботиться фреймворк. А не так давно мы научились визуализировать потоки данных (вот тут несколько примеров как это выглядит, и Настя Горячева рассказывала об этом на недавних конференциях).
Ровно так же мы можем свободно менять разметку (шаблон) компонента, совсем не думая о его реализации. И внутри все сделано достаточно оптимально, и в большинстве случаев можно не беспокоиться о производительности.
Как я писал в начале, в разных фреймворках и библиотеках есть те или иные похожие решения. Вопрос в том, насколько хорошо они позволяют декомпозировать, абстрагироваться и решать действительно сложные задачи. Basis.js не панацея и никто не утверждает что он идеален. Но есть большое количество задач, с которыми он успешно справляется. Так как разрабатывается достаточно давно и нацелен на большие SPA, которые создаются и поддерживаются годами.
Но имхо здесь нужно не столько сравнивать, сколько присмотреться к тому или иному решению. Я всегда призываю смотреть именно на идеи, больше чем на конкретные реализации.
Возможно, вы уже начали представлять себе, как для решения данной задачи пишете циклы, условные операторы и добавляете обработчики событий.
В Ember.js для этого используют библиотеку ember-concurrency
Код получается примерно такой:
export default Controller.extend({
askQuestion: task(function * () {
yield timeout(1000);
this.set('result', Math.random());
}).drop(),
result: null,
});
<button class={{if askQuestion.isIdle "button-primary"}}
onclick={{perform askQuestion}}>
{{#if askQuestion.isIdle}}
Ask
{{else}}
Thinking...
{{loading-spinner}}
{{/if}}
</button>
Допускаю вашу критику по поводу использования if и в ответ на нее предполагаю, что в вашем примере этот же if спрятан в state machine отвечающей за состояния
В показанном примере, мы создаем биндинг loading который должен говорить о том, идет ли сейчас процесс синхронизации или нет. Его значение будет зависеть от состояния набора данных — true, если набор находится в состоянии PROCESSING и false в ином случае.
Делаем крутые Single Page Application на basis.js — часть 2