- PVSM.RU - https://www.pvsm.ru -
Привет! Меня зовут Валерий Зайцев, я клиентсайд-разработчик проекта Таргет Mail.ru [2]. В нашем проекте мы используем небезызвестную библиотеку Backbone.js, и, конечно же, нам стало чего-то не хватать. Поразмыслив над возможными решениями наших проблем, я решил написать свое дополнение к Backbone.js, как говорится с блэкджеком и… О нем я и хочу рассказать в этой статье.
Ribs.js [3] — библиотека, расширяющая возможности Backbone. И прелесть в том, что именно расширяет, а не изменяет. Вы можете использовать ваш любимый Backbone, как и прежде, но по необходимости задействовать новые возможности:
Рассмотрим эти возможности подробнее.
Начнем с самого простого и очевидного. Если вы много пишете на 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-элементов и атрибутами моделей:
Помимо обычных биндингов в Ribs.js можно создать биндинг коллекции. Описание этого механизма заслуживает отдельной статьи, поэтому в рамках данной статьи расскажу в двух словах. Биндинг коллекции связывает коллекцию моделей, Backbone.View и некий DOM-элемент. Для каждой модели из коллекции создается свой экземпляр View и кладется в DOM-элемент. Причем при любых изменениях коллекции (добавление/удаление моделей, сортировка) интерфейс обновляется без вашего вмешательства. Тем самым вы получаете динамическое представление для всей коллекции. Область применения очевидна — разнообразные списки и структуры с однотипными данными.
На просторах интернета имеется ряд библиотек, которые добавляют возможность работать со вложенными атрибутами. Есть библиотеки, которые реализуют биндинги. Но это разные библиотеки, и заставить их работать вместе — задача очень непростая, а скорее всего нереализуемая.
Три составляющие Ribs.js (вложенные атрибуты, вычисляемые поля и биндинги) могут работать независимо друг от друга. Но вся мощь раскрывается, когда вы используете их вместе (последний пример это наглядно иллюстрирует).
Ближайший известный мне конкурент — Epoxy.js. Это библиотека со схожими возможностями, но:
Используя Ribs.js, вы можете сосредоточиться на бизнес-логике, не отвлекаясь на реализацию простейших механизмов. Код становится нагляднее и компактнее, а это самым положительным образом сказывается как на самой разработке, так и на последующей поддержке. К тому же, работа над Ribs.js будет продолжена. Многие идеи, реализованные в Ribs.js родились в ходе работы над реальными боевыми задачами. Эти идеи будут появляться дальше, и лучшие из них будут попадать в следующие версии библиотеки.
Автор: ZaValera
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/63826
Ссылки в тексте:
[1] Image: http://habrahabr.ru/company/mailru/blog/228135/
[2] Таргет Mail.ru: https://target.mail.ru/
[3] Ribs.js: https://github.com/ZaValera/backbone.ribs
[4] здесь: https://github.com/ZaValera/backbone.ribs/tree/master/epoxyVSribs
[5] Источник: http://habrahabr.ru/post/228135/
Нажмите здесь для печати.