Всем доброго времени суток!
Данная статья начинает цикл публикаций, посвященных basis.js – фреймворку для создания полноценных Single Page Application.


Про фреймворки


Ядро современных фронтенд–фреймворков практически не ориентировано на работу с данными так как не имеет структур и алгоритмов для обработки коллекций.
Вместо этого, разработчику предоставляется возможность самостоятельно работать с данными и итерировать коллекции прямо в шаблоне, либо оперировать DOM–элементами прямо из кода контроллера/компонента, опять же, самостоятельно связывая данные с их визуальным представлением.
Минусы таких подходов должны быть очевидны.
При манипуляции с DOM–элементами из контроллера/компонента:
— приходится самостоятельно реализовывать структуры данных и алгоритмы обработки наборов данных
— увеличивается сложность и поддерживаемость кода, так как приходится вручную работать с DOM–элементами и налаживать связи между данными и DOM–элементами
— чаще всего невозможно использовать шаблонизаторы

При использовании шаблонизатора в работе с набором данных:
— ухудшается читаемость шаблона, так как логика частично переносится в шаблон
— эффективно ли в шаблонизаторе реализован механизм обновления частей представлений и реализован ли?
— всё еще отсутствуют механизмы обработки наборов данных

Раньше всё это практически не имело значения, так как за обработку данных и их визуальное представление отвечал backend.
Всё, что нужно было сделать – загрузить страницу по нужному адресу и представление уже сформировано.
Минус такого подхода – отсутствие интерактивности и динамичности в плане обновления данных.
Современное SPA должно быть автономным в плане функционала и обращаться на сервер только тогда, когда необходимо синхронизировать данные (сохранить или получить новые).
Соответственно, всю работу по обработке и визуализации этих данных должен брать на себя фронтенд.
С приходом AJAX и WebSocket, стало гораздо проще следить за обновлением данных и обеспечить интерактивность приложения.
Но AJAX и WebSocket про работу с сетью и они не решают базовой задачи – работы с данными.

Допустим, вы делаете одностраничное приложение – клиент для ВКонтакте.
Вот некоторые из требований к приложению: загрузка, обновление, поиск, группировка, сортировка друзей из социальной сети.
Затем добавляется раздел «музыка». Здесь уже требуется работать с плейлистами (как своими, так и друзей). Тот же поиск.
Добавляется раздел «сообщения». Здесь есть работа с «комнатами» и сообщениями.
Думаю смысл понятен…
Так вот: музыка, друзья, сообщения и так далее – это всё данные, которые надо сортировать, группировать, искать и, наконец, визуализировать. При этом визуальное представление должно своевременно обновляться в режиме реального времени.

Таким образом, в арсенале разработчика должны быть мощные и удобные инструменты для работы, теоретически, с любым количеством данных.

Про шаблонизаторы


Дабы не разводить очередной холивар, я не буду приводить в пример конкретные популярный фреймворки. Вместо этого, представим, что есть некий абстрактный фреймворк с гибким и удобным шаблонизатором.
Как бы мы решали задачи описанного выше SPA?
Очень просто – загружаем данные с сервера соц.сети и скармливаем их шаблонизатору фреймворка.
Отлично, задачу вывода мы, казалось бы, решили.
Но вот мы понимаем, что в уже отрисованную коллекцию добавились новые элементы.
Что делать?
Опять же, всё очень просто – просим шаблонизатор перерисовать представление, но уже с новыми данными.
И всё вроде бы хорошо, пока данных не так много.
А теперь представим, что у нас есть коллекция из 100 объектов, которую мы получили от сервера.
Мы отдали эти данные шаблонизатору, он их послушно отрисовал, но через некоторые время, сервер сообщает нам, что в коллекцию добавился еще один элемент.
Что мы делаем? Снова отдаем данные шаблонизатору и он, внимание, перерисовывает все ранее отрисованные данные.
Не очень–то эффективно, не правда ли? Особенно если элементов в коллекции будет больше.
Отмечу, что здесь я говорю про строковые шаблонизаторы, которые на вход получают шаблон и данные, а на выходе выдают строку с HTML–кодом, которую и нужно вставить в DOM.
Но строковые шаблонизаторы они на то и строковые, что понятия не имеют об HTML и DOM. Им всё равно какие данные в какой шаблон вставлять и что потом разработчик будет с этими данными делать.

Про умные шаблонизаторы


В противовес обычным, строковым, шаблонизаторам есть более умные.
Их преимущество в том, что они–то уже «в курсе», что работают с HTML и на выходе отдают не строку с HTML–кодом, а DOM–дерево с подставленными в него данными. При этом, каждый элемент данных, который поступил на вход такого шаблонизатора, становится связанным с соответствующим ему DOM–узлом. Таким образом, при изменении какого–либо элемента коллекции, происходит обновление только того узла, с которым этот элемент связан. Возвращаемся к примеру со списком из 100 элементов. В случае с умным шаблонизатором, он сам определит – содержимое каких элементов коллекции было изменено и обновит только соответствующие узлы, не перерисовывая при этом всё представление.
Такой подход, несомненно, более эффективен, особенно при больших наборах данных.
Но, опять же, даже он не решает главной проблемы – работы с данными.
Да, в подобные шаблонизаторы встроена возможность использовать pipe–фильтры, которые позволяют модифицировать данные перед выводом, но, во–первых: такой подход ухудшает читаемость шаблона; во–вторых: это возможность шаблонизатора, а не фреймворка, который использует шаблонизатор и в более сложных ситуациях не спасет от нагромождения кода, как со стороны шаблона, так и со стороны контроллера/компонента.
Как следствие, возникает фундаментальная проблема, которая уже не раз здесь упоминалась – обработка данных.

Про basis.js


Basis.js – это фреймворк, ядро которого строилось с расчетом на работу с данными.
Отличительной особенностью фреймворка является сама модель построения приложения.
Законченное приложение на basis.js представляет собой иерархию компонентов, между которыми циркулирует поток данных.
При этом, образ потока данных лучше всего отражает суть происходящего, ведь создание приложения на basis.js – это налаживание связей между частями приложения.
Грамотно построенное приложение избавляет от необходимости даже банального перебора DOM–элементов.
За всё отвечает сам фреймворк и его гибкие инструменты.
При этом, сам фреймворк построен на абстракциях, что позволяет в полной мере использовать преимущества полиморфизма и, в большинстве случаев, абстрагироваться от конкретной реализации.

Основы работы с basis.js вы можете почерпнуть в этой статье, написанной автором фреймворка.
Мне выпала возможность продолжить цикл статей.

От теории – к практике


Одной из главных составляющих любого SPA является реакция приложения на действия пользователя, другими словами – своевременное обновление данных.
Если мы говорим про данные, то в basis.js есть множество абстракций для описания данных.
Наиболее простой из них является класс Token.
Token позволяет описать скалярное значение, на изменение которого можно подписаться:
let token = new basis.Token(); // создаем Token
let fn = (value) => console.log('значение изменено на:', value); // функция–обработчик

token.attach(fn); // подписываемся на изменение значения

token.set('привет'); // устанавливаем новое значение
                     // console> значение изменено на: привет

token.set('habrahabr'); // console> значение изменено на: habrahabr
token.set('habrahabr'); // ничего не выведет в консоль, т.к. значение не отличается от уже установленного

token.detach(fn); // отписываемся от изменений значения

token.set('basis.js'); // новое значение будет установлено, но у токена уже нет подписчиков

Метод Token#attach – добавляет подписчика на изменения значения токена.
Метод Token#detach – удаляет ранее добавленного подписчика.

Более того, один токен может зависеть от другого:
let token = new basis.Token();
let sqr = token.as((value) => value * value); // создаем еще один токен, зависимый от token
let fn = (value) => console.log('значение изменено на:', value);

token.attach(fn);

token.set(4); // console> значение изменено на: 4
console.log(token.get()); // console> 4
console.log(sqr.get()); // console> 16

token.set(8); // console> значение изменено на: 8
console.log(token.get()); // console> 8
console.log(sqr.get()); // console> 64

token.detach(fn);

token.set(10);
console.log(token.get()); // console> 10
console.log(sqr.get()); // console> 100

Token#as – создает новый токен и автоматически подписывает его на изменения значения оригинального токена.
Изменяя значение оригинального токена, оно передается функции, указанной в as и в порожденный токен записывается ее результат.
Таким образом, можно создать цепочку токенов, значение каждого из которых будут зависеть от значения предыдущего токена:
let token = new basis.Token();
let sqr = token.as((value) => value * value);
let twoSqr = sqr.as((value) => value * 2);
let fn = (value) => console.log('значение изменено на:', value);

token.attach(fn);

token.set(4); // console> значение изменено на: 4
console.log(token.get()); // console> 4
console.log(sqr.get()); // console> 16
console.log(twoSqr.get()); // console> 32

token.detach(fn);

token.set(10);
console.log(token.get()); // console> 10
console.log(sqr.get()); // console> 100
console.log(twoSqr.get()); // console> 200

Токен может быть разрушен вызовом метода Token#destroy.
Разрушенный токен становится абсолютно бесполезен, потому как перестает уведомлять подписчиков об обновлении своего значения, да и добавить новых подписчиков к нему уже нельзя.

Вот такой простой и удобный механизм обновления данных заложен в basis.js.

Давайте посмотрим как используются токены в компонентах basis.js:
let Node = require('basis.ui').Node;
let nameToken = new basis.Token('');

new Node({
    container: document.body, // где разместить элемент
    template: resource('./template.tmpl'), // шаблон
    binding: {
        name: nameToken
    },
    action: { // обработчики событий
        input: (e) => nameToken.set(e.sender.value)
    }
});

А вот и шаблон:
<div>
    <input type="text" event-input="input">
    <div>Привет {name}</div>
</div>

Теперь, при вводе данных в текстовое поле, вместо {name} будет подставляться актуальное значение.
Другими словами: свойство Node#binding представляет собой объект, свойствами которого могут быть токены.
Node подписывается на изменения значения таких токенов и своевременно обновляет представление, при чем только те его части, которые реально изменились.

Конечно же не могу обойти вниманием пример с Token#as:
let Node = require('basis.ui').Node;
let nameToken = new basis.Token('');

new Node({
    container: document.body,
    template: resource('./template.tmpl'),
    binding: {
        name: nameToken.as(value => value.toUpperCase())
    },
    action: {
        input: (e) => nameToken.set(e.sender.value)
    }
});

Уже догадались что будет выведено?

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

Да, но позже вы увидите как элегантно basis.js справляется с гораздо более сложными задачами.

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

Если вы, как и я, любите ES6 и хотите использовать его вместе с basis.js, тогда вам понадобится вот этот плагин.

Спасибо за внимание!

Огромная благодарность lahmatiy за бесценные советы ;)

Несколько полезных ссылок:


UPD:
Вторая часть
Запустили gitter-чатик по basis.js. Добавляйтесь, задавайте вопросы.
Поделиться с друзьями
-->

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


  1. aush
    26.06.2016 16:16
    -1

    Rx?


    1. vintage
      26.06.2016 17:46
      +1

      Краткий список методов, которые придётся вызубрить, чтобы хоть что-то сделать с таким подходом: aggregate, all, amb, and, any, asObservable, average, buffer, bufferWithCount, bufferWithTime, bufferWithTimeOrCount, catch, combineLatest, concat, concatAll, concatMap, connect, controlled, count, debounce, defaultIfEmpty, delay, delaySubscription, dematerialize, distinct, distinctUntilChanged, do, doOnNext, doOnError, doOnCompleted, doWhile, elementAt, every, expand, extend, filter, finally | ensure, find, findIndex, first, flatMap, flatMapFirst, flatMapLatest, flatMapObserver, flatMapWithMaxConcurrent, forkJoin, groupBy, groupByUntil, groupJoin, ignoreElements, includes, isEmpty, join, last, lastIndexOf, let, manySelect, map, max, maxBy, merge, mergeAll, min, minBy, multicast, observeOn, onErrorResumeNext, pairwise, partition, pausable, pausableBuffered, pluck, publish, publishLast, publishValue, share, shareReplay, shareValue, refCount, reduce, repeat, replay, retry, retryWhen, sample, scan, select, selectConcat, selectMany, selectManyObserver, sequenceEqual, single, singleInstance, skip, skipLast, skipLastWithTime, skipUntil, skipUntilWithTime, skipWhile, slice, some, startWith, subscribe | forEach, subscribeOn, sum, switch | switchLatest, switchFirst, take, takeLast, takeLastBuffer, takeLastBufferWithTime, takeLastWithTime, takeUntil, takeUntilWithTime, takeWhile, tap, tapOnNext, tapOnError, tapOnCompleted, throttle, timeInterval, timeout, timestamp, toArray, where, window, windowWithCount, windowWithTime, windowWithTimeOrCount, withLatestFrom, zip, zipIterable


      1. aush
        26.06.2016 18:07

        Краткий список методов, которые придётся вызубрить, чтобы хоть что-то сделать с таким подходом


        wat
        Вы с ума сошли? Чтобы пользоваться достаточно map, onValue, offValue — хватает для абсолютного большинства задач.


        1. vintage
          26.06.2016 18:26
          +8

          Вы правы, остальные просто так придумали.


      1. justboris
        27.06.2016 12:05
        +2

        Ну необязтельно же сразу все.
        В Lodash, например, тоже больше 300 методов, но можно же начать с пары нужных и изучать остальные по мере необходмости


  1. TheRabbitFlash
    27.06.2016 01:39
    -7

    Я скоро от слова фреймворк в контексте js буду блевать. Это как устоялось продавец менеджер — так и разного рода поделки называют фреймворками. Бросайте это дело.

    Код на 99% получается меньше, если этого всего мусора не использовать.


    1. RubaXa
      27.06.2016 07:30
      +3

      Вообще-то basis уже существовал когда это не было мейнстримом, никаких ангуляров и реактов тоже не было.


    1. DewDif
      27.06.2016 11:04
      -2

      Уважаемый TheRabbitFlash! Не использовать фреймворки в разработке серьезных проектов — все равно что с нуля изобретать теорему Пифагора. Согласитесь, ведь намного лучше за раз выучить ее в школе, а потом использовать все жизнь, чем придумывать и доказывать ее несколько лет.


      1. TheRabbitFlash
        27.06.2016 11:40
        -2

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


        1. oxidmod
          27.06.2016 11:44
          +5

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


          1. taujavarob
            28.06.2016 09:31

            oxidmod > не обновляйте без надобности и все будет хорошо

            А если есть надобность — то всё плохо?


            1. oxidmod
              29.06.2016 08:51
              +1

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


              1. taujavarob
                30.06.2016 13:47

                oxidmod > приведите реальный кейс такой фичи в сторонней библиотеке, без которой ваше приложение не может жить. вот не было фичи и все ок было. а если так, то приложение поживет без этой фичи в сторонней библиотеке еще некоторое время пока фича не выйдет в стабильный релиз.

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

                Да, в новой версии пофиксили баги — но это не спасает от того, что ты остаёшься на legasy коде.

                Кончается всё тем — что рано или поздно всё переписывается на ново. Или бросается.


                1. oxidmod
                  30.06.2016 13:52
                  +1

                  что это у вас за библиотеки такие? обычно библиотека имеет некий публичный интерфейс, который не ломается от версии к версии. Если же обратная совместимость и ломается, то в отдельной ветке. И не которое время поддерживаются обе. фичи в новой, секьюрити и критикал баги в обе. Если вам не нужны фичи — вы можете использовать старую. Если таки нужны фичи и сломана обратная совместимость — ну чтож, бывает. Самописные велосипеды заставляют переписывать гораздо чаще.
                  зыю вы всегда можете сделать адаптер к библиотеке со стабильным публичным интерфейсом для вашего приложение. при смене библиотеки на другую/самописную или поломаной обратной совместимости вы всегда можете дописать новый адаптер под ту библиотеку, которой решили заменить проблемную. Адаптер — по сути мостик между интерфейсом для вашего приложения и интерфейсом библиотеки. Никаких сложностей, простой мапинг методов


                  1. taujavarob
                    01.07.2016 16:31

                    oxidmod > Адаптер — по сути мостик между интерфейсом для вашего приложения и интерфейсом библиотеки. Никаких сложностей, простой мапинг методов

                    Пример — никаким адаптером вы Angular 1 не приведёте к Angular 2.

                    Даже если вы используете jQuery 1.4 — то переход к jQuery 2,0 уже проблематичен и повлечёт много времени на поиск и исправления вашего кода.

                    И т.д.


                    1. oxidmod
                      01.07.2016 17:30

                      потому что вы глобально обмазали свое приложение методами jQuery. Вы изначально не планировали архитектуру под возможность смены jQuery на чтото другое (или ветку 2,0)
                      Но если бы планировали и написали адаптер, то у вас было бы чтото такое:
                      https://jsfiddle.net/8xL5046k/
                      при таком подходе при смене на другую библиотеку вам достаточно было бы написать адаптер. остальное приложение не изменилось бы

                      ps. сори, не силен в js, наверняка реальный адаптер был бы намного сложней.


                      1. RigelNM
                        10.11.2016 16:00
                        +1

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

                        В моей логике есть парадокс?


                        1. oxidmod
                          02.07.2016 10:02
                          +1

                          а сколько раз вы меняли в своем приложении jquey1 на jquery2? или переходили с бекбона на ангуляр или еще чтото подобное?


                          1. taujavarob
                            04.07.2016 15:45

                            oxidmod > а сколько раз вы меняли в своем приложении jquey1 на jquery2? или переходили с бекбона на ангуляр или еще чтото подобное?

                            Мы этим не заморачиваемся. — Если нужно и оно меняется безболезненно(более-менее), то меняем.
                            Иначе думаем как не меняя обойти.
                            Так и движемся.


                        1. RubaXa
                          02.07.2016 13:21
                          +1

                          И главное, зачем? Какую проблему может решить переход на jQuery2 и тем более Angular2? Ради хайпа? Пришел реакт и что? Это просто View, со своими плюсами и минусами. Выбор инструмента и тем более переход с одного на другой должен быть обоснован, а не просто «это модно».


                          Лет 9 назад переходил с Prototype на jQuery, примерно 2-3 дня ушло на написание фасада, но вот вот обновление jQuery с 1.5 до 1.8 было болью, но боль только в том, что слишком много мегабайтов кода и jQuery тут совсем не причем, намного больше боли бы со своим кодом.


                          1. taujavarob
                            04.07.2016 15:46

                            RubaXa > И главное, зачем? Какую проблему может решить переход на jQuery2 и тем более Angular2? Ради хайпа?

                            Хайп важен. Иначе твои вопросы типа как сделать что-то при jQuery 1,4 будут просто не поняты всеми, ибо все уже давно поменяли jQuery на React.


                            1. RubaXa
                              04.07.2016 15:48
                              +1

                              0_o ясно, понятно, ну удачи.


  1. dempfi
    27.06.2016 08:33

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


    1. smelukov
      27.06.2016 08:34

      Больше конкретики пожалуйста


    1. Naoru
      28.06.2016 16:35
      +2

      Привет, Айк!

      За эти пару лет базис сильно изменился. Ящитаю, самая мощная фича с тех времен — Value.query(), это конструктор геттеров по относительному пути, который автомагически подписывается на все нужные события, которые обновляют вычисляемое значение.

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

      А к вопросу об островке — корень проблемы был, мягко говоря, не в базисе. Стройная и логичная система костылей и подпорок, причиняющая непоправимые психические травмы всем, кто ее касался — лишь последствие, хоть это и не было очевидно. *здесь умное лицо*


  1. dolphin4ik
    27.06.2016 09:06

    Честно говоря, очень сложно. Многое действительно надо запоминать. Не сочтите за рекламу но есть более простой и более удобный Matreshka.js от finom


  1. justboris
    27.06.2016 12:07

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


    1. smelukov
      27.06.2016 12:11

      Спасибо за статью.

      Пожалуйста!
      как мне сделать token, который будет зависеть сразу от двух и более входных значений?

      Для этого в basis.js есть такая вещь как Expression, о которой мы поговорим позднее.