Backbone.js — это популярный общедоступный JavaScript “MV *” фреймворк, который получил огромное распространение с момента своего официального выхода немногим более, чем три года назад. Хотя Backbone.js и помогает создать подходящую структуру для приложений JavaScript, разработчику все равно приходится сталкиваться со многими обычными проблемами при выборе шаблонов и решений. В этой статье, мы проанализируем различные проектные шаблоны, которые вы можете использовать в своих Backbone.js приложениях. Также мы постараемся рассмотреть многие общие понятия, которые сбивают с толку разработчиков.
Приложения, также как и здания, лучше всего создавать на основе известных образцов. (Изображение: Мэтью Ратледж).Создайте подробные копии объектов.
JavaScript интерпретирует все переменные примитивного типа как переданные по значению. Значение переменной передается только тогда, когда на эту переменную ссылаются.
var helloWorld = “Hello World”;
var helloWorldCopy = helloWorld;
Например, результатом выполнения кода выше станет присвоение переменной helloWorldCopy
значения helloWorld
. Любая модификация helloWorldCopy
не изменила бы helloWorld
, так как это — копия. JavaScript рассматривает все переменные не примитивного типа как передачу по ссылке, подразумевая, что JavaScript передаст ссылку адреса памяти переменной только тогда, когда на эту переменную будут ссылаться.
var helloWorld = {
‘hello’: ‘world’}
var helloWorldCopy = helloWorld;
Например, код выше установит значение перемонной helloWorldCopy
, равное ссылке helloWorld
, и, как вы уже могли предположить, любые модификации helloWorldCopy
будут непосредственно относиться к helloWorld
. Если вы захотите получить копию helloWorld
, тогда необходимо будет создать копию объекта.
У вас,наверное, возник вопрос “Почему он объясняет весь этот материал передачи по ссылке и передачи по значению?” Backbone.js не копирует объекты, что означает если вы примените какие-либо модификации к объекту.get()
из модели, то изменения затронут непосредственно весь объект! Давайте посмотрим на пример, чтобы проиллюстрировать места возникновения возможных проблем. Предположим, что у вас есть модель Person
как и в следующем коде:
var Person = Backbone.Model.extend({
defaults: {
'name': 'John Doe',
'address': {
'street': '1st Street'
'city': 'Austin',
'state': 'TX'
'zipCode': 78701}
}
});
И, предположим, вы создали новый объект person
:
var person = new Person({
'name': 'Phillip W'
});
Теперь, давайте поэкспериментируем с некоторыми атрибутами нового объекта person
:
person.set('name', 'Phillip W.');
Код выше успешно управляет атрибутом name
объекта person
. Теперь давайте попытаемся управлять адресом объекта person
. Однако, прежде, чем сделать это, давайте выполним требуемую проверку достоверности для адреса.
var Person = Backbone.Model.extend({
validate: function(attributes) {
if(isNaN(attributes.address.zipCode)) return "Address ZIP code must be a number!";
},
defaults: {
'name': 'John Doe',
'address': {
'street': '1st Street'
'city': 'Austin',
'state': 'TX'
'zipCode': 78701}
}
});
Теперь, давайте попытаемся управлять адресом с некорректным ZIP кодом.
var address = person.get('address');
address.zipCode = 'Hello World';
// Raises an error since the ZIP code is invalid
person.set('address', address);
console.log(person.get('address'));
/* Prints an object with these properties.
{
'street': '1st Street'
'city': 'Austin',
'state': 'TX'
'zipCode': 'Hello World'}
*/
Как это может быть? В результате проверки подлинности появилось сообщение об ошибке! Тогда почему атрибуты изменились? Как уже упоминалось выше, Backbone.js не копирует атрибуты модели; он просто возвращает то, что вы просите. Вы наверное предположили, что если запросить объект, то на него можно получить ссылку, и любая манипуляция над объектом будет непосредственно управлять фактическим объектом в модели. Это предположение может быстро завести вас глубоко вниз, в очень темную кроличью нору, выход из которой может забрать часы отладки и диагностирования ошибок.
Создание подробной копии модели объектов может уберечь вас от падения в кроличью нору отладки. (Изображение: Дэвид Орбэн).
Эта проблема подстерегает не только разработчиков, которые плохо знакомы с Backbone.js, но и даже опытных разработчиками JavaScript. Данная проблема подробно обсуждалась в разделе GitHub проблемы Backbone.js. Джереми Ашкенас акцентирует внимание на том, что выполнение подробной копии является очень трудной задачей и решить эту проблему достаточно сложно. Создание копии может стать дорогостоящей операцией в случае использования очень больших, глубоких объектов.
К счастью, jQuery предоставляет возможность использования такой функции как глубокое копирование, $.extend
. Библиотека Underscore.js, зависящая от Backbone.js, предлагает функцию _.extend
, но я бы рекомендовал избегать её использования, потому что в этом случае не удастся выполнить глубокое копирование. Чтобы выполнить глубокую копию любого нужного мне объекта я использую функцию $.extend
, которая предполагает использование следующего синтаксиса. Не забудьте установить значение true
, чтобы эта команда выполнила глубокую копию объекта.
var address = $.extend(true, {}, person.address);
У нас теперь есть точная копия объекта address
, и мы можем изменять его содержимое, не опасаясь изменений фактической модели. Вы должны знать, что выполнение глубокого копирования оказывает незначительное влияние на быстродействие, но мне еще никогда не приходилось слышать о существенном влиянии этой проблемы. Однако, если вы будете выполнять глубокое копирование массивных объектов или одновременно тысячи объектов, то предварительно необходимо проанализировать показатели производительности. Эта памятка подводит нас непосредственно к следующему шаблону.
Создавайте фасадный шаблон объекта.
В реальном мире требования очень часто меняются, и поэтому JavaScript Object Notation, текстовый формат обмена данными, (или JSON) возвращается конечными точками, которые создаются вашими моделями и коллекциями. Если ваше представление тесно связано с базовой моделью данных, то это может стать действительно большой проблемой основного кода. Поэтому я создал извлечения и установки для всех объектов.
Плюсов этой модели очень много. Если произойдет любое из основополагающих изменений структуры данных, то не нужно будет выполнять многочисленные обновления слоёв представления; вы будете иметь одну точку доступа к данным, таким образом, уменьшается вероятность того, что вы забудете сделать глубокое копирование, а ваш код будет более легким в обслуживании и гораздо проще в отладке. Недостатком этого метода является то, что шаблон может вызвать увеличение объема моделей или коллекций.
Давайте посмотрим на пример, который наглядно иллюстририрует эту модель. Допустим, у нас есть модель Hotel
, которая содержит номера комнат в отеле и их состояние в настоящее время, а мы хотим иметь возможность получить номера комнат с размерами кровати.
var Hotel = Backbone.Model.extend({
defaults: {
"availableRooms": ["a"],
"rooms": {
"a": {
"size": 1200,
"bed": "queen"},
"b": {
"size": 900,
"bed": "twin"},
"c": {
"size": 1100,
"bed": "twin"}
},
getRooms: function() {
$.extend(true, {}, this.get("rooms"));
},
getRoomsByBed: function(bed) {
return _.where(this.getRooms(), function() {
{ "bed": bed }
});
}
}
});
А теперь давайте представим, что завтра вам нужно будет выпускать свой код, а вы узнаете, что разработчики конечного варианта забыли сказать, что данные структуры номеров изменились от объекта до массива. Ваш код теперь будет выглядеть следующим образом.
var Hotel = Backbone.Model.extend({
defaults: {
"availableRooms": ["a"],
"rooms": {
"a": {
"size": 1200,
"bed": "queen"},
"b": {
"size": 900,
"bed": "twin"},
"c": {
"size": 1100,
"bed": "twin"}
},
getRooms: function() {
$.extend(true, {}, this.get("rooms"));
},
getRoomsByBed: function(bed) {
return _.where(this.getRooms(), function() {
{ "bed": bed }
});
}
}
});
Мы обновили только одну функцию для преобразования структуры Hotel
в структуру, которая необходима была для приложения. Несмотря на это изменение, все наше приложение по-прежнему функционирует как и раньше. Если бы в этом случае мы не воспользовались геттером, то нам бы пришлось обновлять каждую точку доступа к rooms
. В идеале, необходимо обновить все функции для работы с новой структурой данных, но если у вас мало времени и срочно нужно представлять готовый продукт, эта возможность станет для вас спасением.
Подобную модель можно рассматривать и как фасадный шаблон проекта, поскольку он избавляет от сложного создания копии объектов, и как мостовой шаблон проекта, поскольку он может быть использован для преобразования данных в требуемую форму. Хорошее практическое правило говорит о необходимости использования методов получения и установки (геттеров и сеттеров) для любых объектов.
Сохраняйте данные, которые не сохраняются сервером.
Хотя Backbone.js предписывает, что модели и коллекции должны отображаться в конечных точках передачи репрезентативного состояния (или REST-FUL), иногда может возникать необходимость сохранения данных в модели или коллекции, которые не сохраняются на сервере. Подобные ситуации описаны многими статьями о Backbone.js, например в статье Пратика Даяла из SupportBee “Backbone.js советы” подробно описан этот шаблон. Давайте рассмотрим небольшой пример, чтобы было более понятно в каких ситуациях может пригодиться такой шаблон. Предположим, у вас есть список ul
.
<ul>
<li><a href="#" data-id="1">One</a></li>
<li><a href="#" data-id="2">Two</a></li>
. . .
<li><a href="#" data-id="n">n</a></li>
</ul>
Где n
равно 200. Когда пользователь нажимает на один из пунктов списка, этот пункт выделяется и происходит его визуализация для пользователей, как выбранного товара благодаря добавлению класса selected
. Данный подход будет иметь следующий вид:
var Model = Backbone.Model.extend({
defaults: {
items: [
{"name": "One",
"id": 1 },
{"name": "Two",
"id": 2 },
{"name": "Three",
"id": 3 }
]
}
});
var View = Backbone.View.extend({
template: _.template($('#list-template').html()),
events: {
"#items li a": "setSelectedItem"
},
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
},
setSelectedItem: function(event) {
var selectedItem = $(event.currentTarget);
// Set all of the items to not have the selected class
$('#items li a').removeClass('selected');
selectedItem.addClass('selected');
return false;}
});
<script id="list-template" type="template">
<ul id="items">
<% for(i = items.length - 1; i >= 0; i--) { %>
<li>
<a href="#" data-id="<%= item[i].id %>"><%= item[i].name %></a></li>
<% } %></ul>
</script>
Теперь, предположим, нам необходимо выяснить, какой именно элемент был выбран. Один из способов сделать это — выполнить просмотр всего списка. Но если список очень длинный, то это может стать довольно рутинной задачей. Учитывая этот факт, давайте также сохранять информацию о выбранном пункте из списка, когда пользователь нажимает на этот пункт.
var Model = Backbone.Model.extend({
defaults: {
selectedId: undefined,
items: [
{"name": "One",
"id": 1},
{ "name": "Two",
"id": 2},
{"name": "Three",
"id": 3}
]
}
});
var View = Backbone.View.extend({
initialize: function(options) {
// Re-render when the model changes
this.model.on('change:items', this.render, this);
},
template: _.template($('#list-template').html()),
events: {
"#items li a": "setSelectedItem"
},
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
},
setSelectedItem: function(event) {
var selectedItem = $(event.currentTarget);
// Set all of the items to not have the selected class
$('#items li a').removeClass('selected');
selectedItem.addClass('selected');
// Store a reference to what item was selected
this.model.set('selectedId', selectedItem..data('id'));
return false;}
});
Сейчас мы легко можем опросить нашу модель и определить, какой элемент был выбран, при этом мы не должны анализировать Объектную модель документа (DOM). Эта модель является чрезвычайно полезной для хранения дополнительных данных, которые вы можете отслеживать; также следует иметь в виду, что вы можете создавать модели и коллекции, которые не обязательно связаны с конечными точками.
Основной недостаток этого шаблона в том, что ваши модели или коллекции в действительности не будут соответствовать архитектуре RESTful. Это объясняется тем, что они не будут должным образом представлены в веб-ресурсе. Также нужно отметить, что этот шаблон может привести к разрастанию вашей модели, и к трудностям при сохранении основных параметров модели. Трудности возникают в том случае, если ваша конечная точка воспринимает только текстовый формат обмена данными JSON.
Нужно Визуализировать части Представлений.
Когда вы впервые начинаете разработку приложений Backbone.js, ваши представления, как правило, структурированы следующим образом :
var View = Backbone.View.extend({
initialize: function(options) {
this.model.on('change', this.render, this);
},
template: _.template($(‘#template’).html()),
render: function() {
this.$el.html(template(this.model.toJSON());
$(‘#a’, this.$el).html(this.model.get(‘a’));
$(‘#b’, this.$el).html(this.model.get(‘b’));}
});
В этом случае, любые изменения в модели вызовут полную повторную визуализацию представления. Я активно пользовался этой моделью до того периода, когда впервые начал разрабатывать проекты с использованием Backbone.js. Но, как только мой код начал разрастаться и увеличился в размерах, я быстро понял, что при использовании этого подхода возникают проблемы с обслуживанием и оптимизацией. Основная причина заключалась в том, что при любом изменении атрибута модели представление полностью подвергалось повторной визуализации.
Когда я впервые столкнулся с этой проблемой, я сразу же воспользовался быстрым поиском Google, чтобы посмотреть, какие решения нашли другие разработчики. Я натолкнулся на публикацию в блоге Иэна Сторма Тэйлора, “Избавьтесь от своих Backbone.js методов визуализации“, в которой он описывает возможность изменения отдельного атрибута в модели, а затем выполнение повторной визуализации только части представления, соответствующей измененному атрибуту. Тейлор также описывает процедуру возвращения ссылки на объект таким образом, чтобы отдельные функции визуализации могли с легкостью сочетаться друг с другом. Программный код, в приведенном выше примере, теперь становится гораздо более простым в обслуживании и более производительным. Это объясняется тем, что мы выполняем обновление только тех частей представления, которые соответствуют измененным атрибутам модели.
var View = Backbone.View.extend({
initialize: function(options) {
this.model.on('change:a', this.renderA, this);
this.model.on('change:b', this.renderB, this);
},
renderA: function() {
$(‘#a’, this.$el).html(this.model.get(‘a’));
return this;
},
renderB: function() {
$(‘#b’, this.$el).html(this.model.get(‘b’));
return this;
},
render: function() {
this
.renderA()
.renderB();}
});
Я должен упомянуть, что существуют некоторые плагины, например, такие как Backbone.StickIt и Backbone.ModelBinder, которые обеспечивают привязки ключевых значений между атрибутами модели и представлением элементов, которые могут спасти вас от объемного шаблонного кода. Таким образом, если вы работаете со сложной формой, то не забывайте о наличии подобных плагинов.
Держите Модели независимо от представлений.
Джереми Ашкенас указывает на одну важную GitHub проблему Backbone.js. Как оказалось Backbone.js не обеспечивает реального разделения между данными и уровнем представления, кроме того, что модели не создаются со ссылкой на своё представление. Если Backbone.js не обеспечивает такого разделения, то должны ли вы его создать? Я и многие другие разработчики Backbone.js, такие как Оз Кац и Даял, считаем, что ответ в подавляющем большинстве случаев да: модели и коллекции, на уровне данных, должны быть полностью независимы от представлений, которые связаны с ними, при этом сохраняя четкое разделение обязанностей. Если вы не будете следовать принципу разделения, то ваш основной код может быстро превратиться в программное спагетти. А спагетти из кода никто не любит.
Хранение вашей модели независимо от представлений поможет предотвратить спагетти из кода, ведь никто не любит спагетти из кода! (Изображение: Сайра Ханчана).
…
Если вы хотите прочитать полностью статью, посетите сайт наших спонсоров