Делаем крутые Single Page Application на basis.js. Часть 1, вступительно-теоретическая

Всем доброго времени суток!
Данная статья начинает цикл публикаций, посвященных 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)
    }
});


А вот и шаблон:

Привет {name}


Теперь, при вводе данных в текстовое поле, вместо {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, который поможет нам в понимании дальнейших тем.

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

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

© Habrahabr.ru