Ribs.js — вложенные атрибуты, вычисляемые поля и биндинги для Backbone.js

0ebd63de0251d5479959ee0dd275cfad.jpgПривет! Меня зовут Валерий Зайцев, я клиентсайд-разработчик проекта Таргет Mail.ru. В нашем проекте мы используем небезызвестную библиотеку Backbone.js, и, конечно же, нам стало чего-то не хватать. Поразмыслив над возможными решениями наших проблем, я решил написать свое дополнение к Backbone.js, как говорится с блэкджеком и… О нем я и хочу рассказать в этой статье.

Ribs.js — библиотека, расширяющая возможности Backbone. И прелесть в том, что именно расширяет, а не изменяет. Вы можете использовать ваш любимый Backbone, как и прежде, но по необходимости задействовать новые возможности:

вложенные атрибуты: работа с атрибутами модели любой вложенности; вычисляемые атрибуты: добавление в модель атрибутов, которые автоматически пересчитываются при изменении зависимостей (других атрибутов модели); биндинги: динамическая связь между атрибутами модели и DOM-элементами. Рассмотрим эти возможности подробнее.Начнем с самого простого и очевидного. Если вы много пишете на Backbone, то наверняка сталкивались с проблемой, когда нужно внести изменения в модель, атрибуты которой далеко не плоские. var Simpsons = Backbone.Ribs.Model.extend ({ defaults: { homer: { age: 40, weight: 90, job: 'Safety Inspector' }, bart: { age: 10, weight: 30, job: '4th grade student' } } });

var family = new Simpsons (); Предположим, что Гомер плотно пообедал и набрал пару килограммов: Backbone:

var homer = _.clone (family.get ('homer'));

homer.weight = 92; family.set ('homer', homer); Для того, чтобы не нарушать get/set подход, нам необходимо: забрать объект из модели; создать копию этого объекта; внести необходимые изменения; положить обратно. Согласитесь, это крайне неудобно. А если учесть тот факт, что объекты могут быть огромными, то это еще и очень затратно. Куда проще изменить именно тот атрибут, который нужно: Backbone + Ribs:

family.set ('homer.weight', 92); В результате этого set-a будет сгенерировано событие 'change: homer.weight'. Не исключена ситуация, когда вам нужно, чтобы события были сгенерированы по всей цепочке вложенности. Для этого методу set необходимо передать {propagation: true}. family.set ('homer.weight', 92, {propagation: true}); В этом случае будут сгенерированы события 'change: homer.weight' и 'change: homer'. Сразу оговорюсь, что я привык называть их вычисляемыми полями, поэтому прошу меня извинить за двойную терминологию. Итак, приступим. Очень часто возникает ситуация, когда атрибуты модели нужно преобразовать в определенную форму (назовем ее «результат»), а потом этот результат использовать, да еще и не в одном месте. И хорошо бы, чтобы при изменении атрибутов, результат обновлялся, и всё, что на него завязано, тоже бы обновлялось. В результате получается достаточно громоздкая вереница из дополнительных методов и подписок, которую в будущем будет достаточно проблематично поддерживать.К примеру, Профессор Фринк задумал некое безумное исследование, в котором ему очень важно контролировать общий вес Гомера и Барта. Давайте сравним реализации на чистом Backbone и на Backbone + Ribs.

Backbone:

var Simpsons = Backbone.Model.extend ({ defaults: { homer: { age: 40, weight: 90, job: 'Safety Inspector' }, bart: { age: 10, weight: 30, job: '4th grade student' } } });

var family = new Simpsons (), doSmth = function (model, value) { console.log (value); };

family.on ('change: bart', function (model, bart) { var prev = family.previous ('bart').weight;

if (bart.weight!== prev) { doSmth (family, bart.weight + family.get ('homer').weight); } });

family.on ('change: homer', function (model, homer) { var prev = family.previous ('homer').weight;

if (homer.weight!== prev) { doSmth (family, homer.weight + family.get ('bart').weight); } });

var bart = _.clone (family.get ('bart'));

bart.weight = 32; family.set ('bart', bart);//В консоль будет выведено: 122

var homer = _.clone (family.get ('homer'));

homer.weight = 91; family.set ('homer', homer);//В консоль будет выведено: 123 Можно было написать немного по-другому, но это не сильно спасет ситуацию. Разберем, что мы здесь понаписали. Определили функцию, которая будет что-то делать с искомым суммарным весом. Подписались на обработку 'change: homer' и 'change: bart'. В обработчиках проверяем, изменилось ли значение веса, и в этом случае вызываем нашу рабочую функцию. Согласитесь, достаточно много писанины для достаточно простой и распространенной ситуации. Теперь то же самое, но короче, нагляднее и проще.Backbone + Ribs:

var Simpsons = Backbone.Ribs.Model.extend ({ defaults: { homer: { age: 40, weight: 90, job: 'Safety Inspector' }, bart: { age: 10, weight: 30, job: '4th grade student' } },

computeds: { totalWeight: { deps: ['homer.weight', 'bart.weight'], get: function (h, b) { return h + b; } } } });

var family = new Simpsons (), doSmth = function (model, value) { console.log (value); };

family.on ('change: totalWeight', doSmth);

family.set ('bart.weight', 32); //В консоль будет выведено: 122 family.set ('homer.weight', 91); //В консоль будет выведено: 123 Что же здесь происходит?! Мы добавили вычисляемое поле, которое зависит от двух атрибутов. При изменении какого-либо из атрибутов, вычисляемое поле пересчитается автоматически. Вычисляемый атрибут можно воспринимать, как обычный атрибут.Вы можете прочитать его значение:

family.get ('totalWeight'); // 120 Можете подписаться на его изменение: family.on ('change: totalWeight', function () {}); В случае необходимости, можно описать метод set для вычисляемого поля, и сетить его без зазрения совести. Стоит отметить, что вычисляемые поля можно использовать в зависимостях других вычисляемых полей. Также, вычисляемые поля очень удобны в биндингах! Биндинг — это связь между моделью и DOM-элементом. Проще тут и не скажешь. Веб-разработчику изо дня в день приходится выводить всякие данные в интерфейс. Следить за их изменениями. Обновлять. Снова выводить… А тут уже и рабочий день закончился. Вернемся к нашим желтым друзьям. Допустим, захотелось нам выводить суммарный вес в какой-нибудь span.Backbone:

var Simpsons = Backbone.Model.extend ({ defaults: { homer: { age: 40, weight: 90, job: 'Safety Inspector' }, bart: { age: 10, weight: 30, job: '4th grade student' } } });

var Table = Backbone.View.extend ({ initialize: function (family) { this.family = family;

family.on ('change: bart', function (model, bart) { var prev = this.family.previous ('bart').weight;

if (bart.weight!== prev) { this.onchangeTotalWeight (bart.weight + family.get ('homer').weight); } }, this);

family.on ('change: homer', function (model, homer) { var prev = family.previous ('homer').weight;

if (homer.weight!== prev) { this.onchangeTotalWeight (homer.weight + family.get ('bart').weight); } }, this); },

onchangeTotalWeight: function (totalWeight) { this.$('span').text (totalWeight); } });

var family = new Simpsons (), table = new Table (family); Backbone + Ribs:

var Simpsons = Backbone.Ribs.Model.extend ({ defaults: { homer: { age: 40, weight: 90, job: 'Safety Inspector' }, bart: { age: 10, weight: 30, job: '4th grade student' } },

computeds: { totalWeight: { deps: ['homer.weight', 'bart.weight'], get: function (h, b) { return h + b; } } } });

var Table = Backbone.Ribs.View.extend ({ bindings: { 'span': {text: 'family.totalWeight'} },

initialize: function (family) { this.family = family; } });

var family = new Simpsons (), table = new Table (family); Теперь, при любых изменениях веса Гомера или Барта, span будет обновлен. Помимо текста, вы можете создавать и другие связи между параметрами DOM-элементов и атрибутами моделей: двусторонняя связь с input-ами различных типов (text, checkbox, radio); css-атрибута; css-классы; модификаторы; и другое. Помимо обычных биндингов в Ribs.js можно создать биндинг коллекции. Описание этого механизма заслуживает отдельной статьи, поэтому в рамках данной статьи расскажу в двух словах. Биндинг коллекции связывает коллекцию моделей, Backbone.View и некий DOM-элемент. Для каждой модели из коллекции создается свой экземпляр View и кладется в DOM-элемент. Причем при любых изменениях коллекции (добавление/удаление моделей, сортировка) интерфейс обновляется без вашего вмешательства. Тем самым вы получаете динамическое представление для всей коллекции. Область применения очевидна — разнообразные списки и структуры с однотипными данными. На просторах интернета имеется ряд библиотек, которые добавляют возможность работать со вложенными атрибутами. Есть библиотеки, которые реализуют биндинги. Но это разные библиотеки, и заставить их работать вместе — задача очень непростая, а скорее всего нереализуемая.Три составляющие Ribs.js (вложенные атрибуты, вычисляемые поля и биндинги) могут работать независимо друг от друга. Но вся мощь раскрывается, когда вы используете их вместе (последний пример это наглядно иллюстрирует).

Ближайший известный мне конкурент — Epoxy.js. Это библиотека со схожими возможностями, но:

она не умеет работать с вложенными атрибутами, а это, как мы уже убедились, очень полезная вещь; одну коллекцию можно использовать только в одном биндинге (в Ribs.js вы можете на базе одной коллекции создавать сколько угодно разнообразных представлений); в тесте с биндингом коллекции из 10000 моделей Epoxy.js уступает Ribs.js почти в 2 раза. Исходники теста лежат здесь; есть еще ряд придирок к реализации и удобству использования. В сложных задачах из-за этого приходится выдумывать обходные пути и вставлять костыли. Используя Ribs.js, вы можете сосредоточиться на бизнес-логике, не отвлекаясь на реализацию простейших механизмов. Код становится нагляднее и компактнее, а это самым положительным образом сказывается как на самой разработке, так и на последующей поддержке. К тому же, работа над Ribs.js будет продолжена. Многие идеи, реализованные в Ribs.js родились в ходе работы над реальными боевыми задачами. Эти идеи будут появляться дальше, и лучшие из них будут попадать в следующие версии библиотеки.

© Habrahabr.ru