Как стать автором
Обновить

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

> без привлечения циклов и условных операторов
Чем вам не угодли циклы и условные операторы? :)

Вообще, решение на голом JS + JQuery (DOM + ajax) было бы куда понятней и проще (по моему).
А решение на React/Angular было бы совсем крохотным.

Так в чем преимущество basis.js в этом случае?
Чем вам не угодли циклы и условные операторы? :)

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

Так в чем преимущество basis.js в этом случае?

Как я и говорил в прошлой статье, для простых задач, basis.js может показаться избыточным. Потерпите, скоро рассматриваемые задачи усложнятся ;) мы подбираемся к ним постепенно…
Вы уже вторую статью ухудшаете репутацию этой бибилиотеки (или фреймворка) подобными словами :)

Если при решении простых задач он избыточен, то он будет неудобен для решения сложных задач.
Ведь главное преимущество тех же Angular/React в том, что они дают возможность разбить сложную задачу на несколько мелких и решить их просто. В вашем случае так не получится — вы сами сказали, что мелкие задачи базисом не решают.
вы сами сказали, что мелкие задачи базисом не решают.

Это где я такое сказал?
> для простых задач, basis.js может показаться избыточным
Разве не это имелось ввиду?
Абсолютно нет
Ок, ошибся.

Давайте на чистоту, решения на 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.

Вообще этот пример практически полностью повторяет структуру компонента\модели из Ember.js, в том числе контролем за состоянием, попробуйте, возможно вам понравится.
Для сравнения, за ~10 мин сделал аналог на Angular Light, получилось вроде компактнее. Т.к. на jsfiddle с сервером не очень, сделал загрузку с github.

Представьте, что список юзеров нужен в других местах и там так же надо знать в каком он состоянии, а еще следить за его изменениями. Если с флагом loading я ещё могу представить, как можно выкрутится, то во как следить за изменениями вне контроллера, а это значит не имея scope.$watch, нет.

Существует масса способов «синхронизации», Angular Light не «заставляет» использовать какой-то конкретный, он отдает это на откуп программисту.
Например сервис как общее хранилище, или pubsub/FRP, тот же $watch можно свободно использовать и вне контроллера (опять же это решает разработчик), ChangeDetector — это самостоятельный объект который просто отслеживает изменения, и его можно использовать где угодно.
Я чаще всего использую сервис + pubsub, т.к. это просто и удобно.

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

В статье, видимо для упрощения, все указано в одном модуле. На практике модели данных, наборы и логика их синхронизации описывается отдельно от view, и ничего не знают о view. Так, что любое view может в любой момент «сказать», что ему нужны определенные модель/коллекция/что-то еще. При этом данные могут быть еще не загружены, или загружаться, или быть уже загруженными. Представьте сколько сценариев вам нужно предусмотреть и реализовать, особенно если хотите чтобы не делалось лишних запросов к серверу и все данные были согласованы/актуальны.
В basis.js с этим несколько проще. view привязывая данные «сообщают» им что есть активный потребитель, и они при необходимости синхронизируются. Можно всегда получить текущее актуальное состояние данных и поменять интерфейс соотвественно. По большей части все разделено и не требует изобретения велосипедов (по крайней мере для частых кейсов точно)
Backbone?

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

Вы про то, что если загрузка не пройдет? Это просто пример, не готовое приложение, а главное удовлетворяет условиям (возможностям из списка в статье). Большее число пунктов связано с UI, поэтому я и сделал пример для сравнения UI.
Да, 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, если эту синхронизацию можно вызвать напрямую. Видимо эти враперы позволяют состыковать с остальной частью басиса.

Забацал на своём велосипеде: онлайн, код.


Кода получилось больше, но и функционала больше:


  1. Добавлен поиск пользователей по мере набора.
  2. Добавлена кнопка "перезагрузить".
  3. Добавлен вывод ошибок (в том числе и индикатора загрузки).
  4. Поддерживается задержка перед запросом, чтобы не слать 10 запросов в секунду.
  5. Сломанный контрол не ломает соседей.
  6. Поддерживается индикация сломанных (и ожидающих загрузки данных в том числе) контролов.
  7. Поддерживается ленивый рендеринг по мере прокрутки.
  8. Кнопка сохранения активируется только если есть несохранённые изменения.
  9. Пока пользователь не ввёл поисковой запрос — показывается только поле поиска.
  10. Модель отделена от вьюшки.
  11. Поисковой запрос сохраняется в урле.

Попутно порефакторил у себя рендеринг. Бенчмарки: онлайн, код. Интересная особенность: если отрендерить 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 наблюдается независимо от остальных фреймворков.

Немного странно видеть в 2016 году библиотеку, в которой компоненты по умолчанию стилизованы под Windows XP.
Все таки больше под Windows Vista. И странного в этом мало, т.к. фреймворк развивается где-то с 2006го. В каждом проекте свой дизайн и стандартный вид компонент не используется, вот и не дойдут руки его переделать.
И чем это кроме шаблонизатора отличается от Backbone.Ribs?

Выше lahmatiy написал же:


фреймворк развивается где-то с 2006го

Backbone — появился в конце 2010, Backbone.Ribs — 2014


Да и похожего у них очень мало, только факт наличия обертки на данными и коллекциями, да и то, это если очень сильно упростить. Basis наверно лучше сравнивать с KO, как мне кажется, если уж сравнивать, если я не прав, путь Роман меня поправит.

Не KO тут ни при чём, в KO автоматический трекинг зависимостей.

А разве нет? Так же создаем над всем обертки, они между собой сами связываются/подписываются, в КО вроде тоже никакой магии нет, так же всё observable, м?

Так или иначе похожесть в каких-то моментах можно найти между многими фреймворками/библиотеками. Но куда важней не конкретная функциональность, а то как это все между собой стыкуется и что в итоге получается.
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 в ином случае.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории