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

от автора

Привет! Меня зовут Валерий Зайцев, я клиентсайд-разработчик проекта Таргет 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 подход, нам необходимо:

  1. забрать объект из модели;
  2. создать копию этого объекта;
  3. внести необходимые изменения;
  4. положить обратно.

Согласитесь, это крайне неудобно. А если учесть тот факт, что объекты могут быть огромными, то это еще и очень затратно. Куда проще изменить именно тот атрибут, который нужно:

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, а не что-то другое?

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

Три составляющие Ribs.js (вложенные атрибуты, вычисляемые поля и биндинги) могут работать независимо друг от друга. Но вся мощь раскрывается, когда вы используете их вместе (последний пример это наглядно иллюстрирует).

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

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

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

ссылка на оригинал статьи http://habrahabr.ru/company/mailru/blog/228135/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *