От JQuery до Backbone

imageВ данной статье будет показано как можно реорганизовывать код написанный в «простом» JQuery стиле в код на Backbone, с использованием представлений, моделей, коллекций и событий. Реорганизация будет постепенной, так чтобы этот процесс дал четкое понимание основных абстракций в Backbone. Статья рассчитана на тех кто использует JQuery и хотел бы познакомится со схемой MVC для клиентского кода.

Данная статья является переводом материала с github.


Начнем с кода приложения, которое собственно и будем реорганизовывать.
$(document).ready(function() {
    $('#new-status form').submit(function(e) {
        e.preventDefault();

        $.ajax({
            url: '/status',
            type: 'POST',
            dataType: 'json',
            data: { text: $('#new-status').find('textarea').val() },
            success: function(data) {
                $('#statuses').append('<li>' + data.text + '</li>');
                $('#new-status').find('textarea').val('');
            }
        });
    });
});

<body>
    <div id="new-status">
      <h2>New monolog</h2>
      <form action="">
        <textarea></textarea><br>
        <input type="submit" value="Post">
      </form>
    </div>

    <div id="statuses">
      <h2>Monologs</h2>
      <ul></ul>
    </div>
</body>

Здесь можно посмотреть на код в действии. Приложение позволяет ввести текст, при нажатии на «Post» этот текст отправляется на сервер и отображается ниже в истории.
Приложение ждет загрузки страницы, добавляет ожидание сабмита формы, в котором и находится вся логика. Но, в чем проблема? Этот код делает много вещей одновременно. Он слушает события страницы, события пользователя, сетевые события, обрабатывает вводимые пользователем данные, анализирует ответ, и манипулирует с DOM. И это все в 16 строк кода. Далее мы реорганизуем этот код так, чтобы он отвечал принципу единой ответственности, чтобы его было легко тестировать, обслуживать, повторно использовать и расширять.
Вот три убеждения, которые мы хотим достичь:
  • Мы хотим вынести как можно больше кода из $(document).ready. В своем нынешнем состоянии код практически невозможно тестировать.
  • Мы хотим придерживаться единого принципа ответственности, и сделать код более используемым и проверяемым.
  • Мы хотим разорвать связь между DOM и Ajax.

Разделение DOM и Ajax


Манипуляций с DOM необходимо разделить от работы с Ajax, и первый шаг это создание функции addStatus:

Изменение №1

+function addStatus(options) {
+    $.ajax({
+        url: '/status',
+        type: 'POST',
+        dataType: 'json',
+        data: { text: $('#new-status textarea').val() },
+        success: function(data) {
+            $('#statuses ul').append('<li>' + data.text + '</li>');
+            $('#new-status textarea').val('');
+        }
+    });
+}
+
 $(document).ready(function() {
     $('#new-status form').submit(function(e) {
         e.preventDefault();

-        $.ajax({
-            url: '/status',
-            type: 'POST',
-            dataType: 'json',
-            data: { text: $('#new-status textarea').val() },
-            success: function(data) {
-                $('#statuses ul').append('<li>' + data.text + '</li>');
-                $('#new-status textarea').val('');
-            }
-        });
+        addStatus();
     });
 });


Здесь и далее знаком "+" отмечены добавленные строки, а знаком "-" удаленные.

Конечно, и в data и в success мы по прежнему работаем с DOM. Мы должны разорвать эту связь, передавая их в качестве аргументов:

Изменение №2
function addStatus(options) {
     $.ajax({
         url: '/status',
         type: 'POST',
         dataType: 'json',
-        data: { text: $('#new-status textarea').val() },
-        success: function(data) {
-            $('#statuses ul').append('<li>' + data.text + '</li>');
-            $('#new-status textarea').val('');
-        }
+        data: { text: options.text },
+        success: options.success
     });
 }

 $(document).ready(function() {
     $('#new-status form').submit(function(e) {
         e.preventDefault();

-        addStatus();
+        addStatus({
+            text: $('#new-status textarea').val(),
+            success: function(data) {
+                $('#statuses ul').append('<li>' + data.text + '</li>');
+                $('#new-status textarea').val('');
+            }
+        });
     });
 });


Далее необходимо обернуть эти статусы в объект, так чтобы можно было написать statuses.add вместо addStatus.
Для этого используем паттерн конструктор с прототипом, и создадим «класс» Statuses:

Изменение №3
-function addStatus(options) {
+var Statuses = function() {
+};
+Statuses.prototype.add = function(options) {
     $.ajax({
         url: '/status',
         type: 'POST',
         dataType: 'json',
         data: { text: options.text },
         success: options.success
     });
-}
+};

 $(document).ready(function() {
+    var statuses = new Statuses();
+
     $('#new-status form').submit(function(e) {
         e.preventDefault();

-        addStatus({
+        statuses.add({
             text: $('#new-status textarea').val(),
             success: function(data) {
                 $('#statuses ul').append('<li>' + data.text + '</li>');
                 $('#new-status textarea').val('');
             }
         });
     });
 });


Создание представления


Наш обработчик формы теперь имеет одну зависимость от переменной statuses, и все что находится внутри обработчика работает только с DOM. Давайте переместим обработчик формы и все что внутри в отдельный «класс» NewStatusView:

Изменение №4
var Statuses = function() {
 };
 Statuses.prototype.add = function(options) {
     $.ajax({
         url: '/status',
         type: 'POST',
         dataType: 'json',
         data: { text: options.text },
         success: options.success
     });
 };

+var NewStatusView = function(options) {
+    var statuses = options.statuses;
+
+    $('#new-status form').submit(function(e) {
+        e.preventDefault();
+
+        statuses.add({
+            text: $('#new-status textarea').val(),
+            success: function(data) {
+                $('#statuses ul').append('<li>' + data.text + '</li>');
+                $('#new-status textarea').val('');
+            }
+        });
+    });
+};
+
 $(document).ready(function() {
     var statuses = new Statuses();

-    $('#new-status form').submit(function(e) {
-        e.preventDefault();
-
-        statuses.add({
-            text: $('#new-status textarea').val(),
-            success: function(data) {
-                $('#statuses ul').append('<li>' + data.text + '</li>');
-                $('#new-status textarea').val('');
-            }
-        });
-    });
+    new NewStatusView({ statuses: statuses });
 });


Теперь мы только инициализируем приложение когда DOM загружен, а все остальное вынесено за $(document).ready. Шаги, которые мы сделали до сих пор, выделили из кода два компонента, которые легче тестировать и имеют более четкие обязанности. Но все же тут есть над чем поработать. Давайте начнем с вынесения обработчика формы из представления в отдельный метод addStatus:

Изменение №5
 var Statuses = function() {
 };
 Statuses.prototype.add = function(options) {
     $.ajax({
         url: '/status',
         type: 'POST',
         dataType: 'json',
         data: { text: options.text },
         success: options.success
     });
 };

 var NewStatusView = function(options) {
-    var statuses = options.statuses;
+    this.statuses = options.statuses;

-    $('#new-status form').submit(function(e) {
-        e.preventDefault();
-        statuses.add({
-            text: $('#new-status textarea').val(),
-            success: function(data) {
-                $('#statuses ul').append('<li>' + data.text + '</li>');
-                $('#new-status textarea').val('');
-            }
-        });
-    });
+    $('#new-status form').submit(this.addStatus);
 };
+NewStatusView.prototype.addStatus = function(e) {
+    e.preventDefault();
+
+    this.statuses.add({
+        text: $('#new-status textarea').val(),
+        success: function(data) {
+            $('#statuses ul').append('<li>' + data.text + '</li>');
+            $('#new-status textarea').val('');
+        }
+    });
+};

 $(document).ready(function() {
     var statuses = new Statuses();
     new NewStatusView({ statuses: statuses });
 });


Но при запуске в Chrome мы увидим ошибку:
Uncaught TypeError: Cannot call method 'add' of undefined

Мы получаем эту ошибку, так как this имеет разные значения в конструкторе и методе addStatus. (Если вы не в полной мере понимаете почему это происходит, я рекомендую прочитать Understanding JavaScript Function Invocation and “this”). ​​Для решения этой проблемы мы можем использовать $.proxy, который создает функцию, в которой this имеет нужный нам контекст.

Изменение №6
var Statuses = function() {
 };
 Statuses.prototype.add = function(options) {
     $.ajax({
         url: '/status',
         type: 'POST',
         dataType: 'json',
         data: { text: options.text },
         success: options.success
     });
 };

 var NewStatusView = function(options) {
     this.statuses = options.statuses;

-    $('#new-status form').submit(this.addStatus);
+    var add = $.proxy(this.addStatus, this);
+    $('#new-status form').submit(add);
 };
 NewStatusView.prototype.addStatus = function(e) {
     e.preventDefault();

     this.statuses.add({
         text: $('#new-status textarea').val(),
         success: function(data) {
             $('#statuses ul').append('<li>' + data.text + '</li>');
             $('#new-status textarea').val('');
         }
     });
 };

 $(document).ready(function() {
     var statuses = new Statuses();
     new NewStatusView({ statuses: statuses });
 });


Давайте сделаем success отдельным методом, который будет работать с DOM, что сделает код более читаемым и гибким:

Изменение №7
var Statuses = function() {
 };
 Statuses.prototype.add = function(options) {
     $.ajax({
         url: '/status',
         type: 'POST',
         dataType: 'json',
         data: { text: options.text },
         success: options.success
     });
 };

 var NewStatusView = function(options) {
     this.statuses = options.statuses;

     var add = $.proxy(this.addStatus, this);
     $('#new-status form').submit(add);
 };
 NewStatusView.prototype.addStatus = function(e) {
     e.preventDefault();

+    var that = this;
+
     this.statuses.add({
         text: $('#new-status textarea').val(),
         success: function(data) {
-            $('#statuses ul').append('<li>' + data.text + '</li>');
-            $('#new-status textarea').val('');
+            that.appendStatus(data.text);
+            that.clearInput();
         }
     });
 };
+NewStatusView.prototype.appendStatus = function(text) {
+    $('#statuses ul').append('<li>' + text + '</li>');
+};
+NewStatusView.prototype.clearInput = function() {
+    $('#new-status textarea').val('');
+};

 $(document).ready(function() {
     var statuses = new Statuses();
     new NewStatusView({ statuses: statuses });
 });


Это намного проще для тестирования и поддержки проекта при его развитии. Мы также стали ближе к применению Backbone.

Добавление событий


Для следующего шага нам понадобится использовать наш первый модуль Backbone — события. События это просто способ сказать: «Привет, я хочу знать когда некоторое действие произойдет» и «Привет, вы знаете что действие которое вы ждали только что произошло?». Это та же самая идея что и события jQuery при работе с DOM, такие как ожидание клика или сабмита.
В документации Backbone поясняется про Backbone.Events так: «События это модуль, который может быть смешан с любым объектом, что дает объекту способностью связывать и вызывать пользовательские события». Документация также подсказывает нам, как мы можем использовать Underscore.js, чтобы создать диспетчер событий:
var events = _.clone(Backbone.Events);

С этой небольшой функциональностью мы можем позволить success оповещать вместо вызова функций. Мы также можем объявить в конструкторе, какие методы мы хотим оповестить, когда событие произойдет:

Изменение №8
+var events = _.clone(Backbone.Events);
+
 var Statuses = function() {
 };
 Statuses.prototype.add = function(options) {
     $.ajax({
         url: '/status',
         type: 'POST',
         dataType: 'json',
         data: { text: options.text },
         success: options.success
     });
 };

 var NewStatusView = function(options) {
     this.statuses = options.statuses;

+    events.on('status:add', this.appendStatus, this);
+    events.on('status:add', this.clearInput, this);
+
     var add = $.proxy(this.addStatus, this);
     $('#new-status form').submit(add);
 };
 NewStatusView.prototype.addStatus = function(e) {
     e.preventDefault();

-    var that = this;
-
     this.statuses.add({
         text: $('#new-status textarea').val(),
         success: function(data) {
-            that.appendStatus(data.text);
-            that.clearInput();
+            events.trigger('status:add', data.text);
         }
     });
 };
 NewStatusView.prototype.appendStatus = function(text) {
     $('#statuses ul').append('<li>' + text + '</li>');
 };
 NewStatusView.prototype.clearInput = function() {
     $('#new-status textarea').val('');
 };

 $(document).ready(function() {
     var statuses = new Statuses();
     new NewStatusView({ statuses: statuses });
 });


Теперь в конструкторе мы можем объявить что мы хотим вызвать, когда добавится новый статус, вместо того, чтобы addStatus вызывал нужную функцию. Единственная обязанность addStatus это обратная связь, а не манипуляции с DOM.

Изменение №9
var events = _.clone(Backbone.Events);

 var Statuses = function() {
 };
-Statuses.prototype.add = function(options) {
+Statuses.prototype.add = function(text) {
     $.ajax({
         url: '/status',
         type: 'POST',
         dataType: 'json',
-        data: { text: options.text },
-        success: options.success
+        data: { text: text },
+        success: function(data) {
+            events.trigger('status:add', data.text);
+        }
     });
 };

 var NewStatusView = function(options) {
     this.statuses = options.statuses;

     events.on('status:add', this.appendStatus, this);
     events.on('status:add', this.clearInput, this);

     var add = $.proxy(this.addStatus, this);
     $('#new-status form').submit(add);
 };
 NewStatusView.prototype.addStatus = function(e) {
     e.preventDefault();

-    this.statuses.add({
-        text: $('#new-status textarea').val(),
-        success: function(data) {
-            events.trigger('status:add', data.text);
-        }
-    });
+    this.statuses.add($('#new-status textarea').val());
 };
 NewStatusView.prototype.appendStatus = function(text) {
     $('#statuses ul').append('<li>' + text + '</li>');
 };
 NewStatusView.prototype.clearInput = function() {
     $('#new-status textarea').val('');
 };

 $(document).ready(function() {
     var statuses = new Statuses();
     new NewStatusView({ statuses: statuses });
 });


Обязанности представлений


Смотря на appendStatus и clearInput в NewStatusView, мы видим что эти методы работают с двумя различными DOM элементами, #statuses и #new-status соответственно. Это не соответствует принципу единой ответственности. Давайте вынесем из NewStatusView ответственность за работу с #statuses в отдельное представление StatusesView. Это разделение не потребует от нас много усилий, так как теперь мы используем диспетчер событий, а с жесткими обратными вызовами функций это было бы намного сложнее.

Изменение №10
var events = _.clone(Backbone.Events);

 var Statuses = function() {
 };
 Statuses.prototype.add = function(text) {
     $.ajax({
         url: '/status',
         type: 'POST',
         dataType: 'json',
         data: { text: text },
         success: function(data) {
             events.trigger('status:add', data.text);
         }
     });
 };

 var NewStatusView = function(options) {
     this.statuses = options.statuses;

-    events.on('status:add', this.appendStatus, this);
     events.on('status:add', this.clearInput, this);

     var add = $.proxy(this.addStatus, this);
     $('#new-status form').submit(add);
 };
 NewStatusView.prototype.addStatus = function(e) {
     e.preventDefault();

     this.statuses.add($('#new-status textarea').val());
 };
-NewStatusView.prototype.appendStatus = function(text) {
-    $('#statuses ul').append('<li>' + text + '</li>');
-};
 NewStatusView.prototype.clearInput = function() {
     $('#new-status textarea').val('');
 };

+var StatusesView = function() {
+    events.on('status:add', this.appendStatus, this);
+};
+StatusesView.prototype.appendStatus = function(text) {
+    $('#statuses ul').append('<li>' + text + '</li>');
+};
+
 $(document).ready(function() {
     var statuses = new Statuses();
     new NewStatusView({ statuses: statuses });
+    new StatusesView();
 });


Теперь, поскольку представления отвечают только за один HTML элемент, мы можем указать их в конструкторе:

Изменение №11
var events = _.clone(Backbone.Events);

 var Statuses = function() {
 };
 Statuses.prototype.add = function(text) {
     $.ajax({
         url: '/status',
         type: 'POST',
         dataType: 'json',
         data: { text: text },
         success: function(data) {
             events.trigger('status:add', data.text);
         }
     });
 };

 var NewStatusView = function(options) {
     this.statuses = options.statuses;
+    this.el = $('#new-status');

     events.on('status:add', this.clearInput, this);

     var add = $.proxy(this.addStatus, this);
-    $('#new-status form').submit(add);
+    this.el.find('form').submit(add);
 };
 NewStatusView.prototype.addStatus = function(e) {
     e.preventDefault();

-    this.statuses.add($('#new-status textarea').val());
+    this.statuses.add(this.el.find('textarea').val());
 };
 NewStatusView.prototype.clearInput = function() {
-    $('#new-status textarea').val('');
+    this.el.find('textarea').val('');
 };

 var StatusesView = function() {
+    this.el = $('#statuses');
+
     events.on('status:add', this.appendStatus, this);
 };
 StatusesView.prototype.appendStatus = function(text) {
-    $('#statuses ul').append('<li>' + text + '</li>');
+    this.el.find('ul').append('<li>' + text + '</li>');
 };

 $(document).ready(function() {
     var statuses = new Statuses();
     new NewStatusView({ statuses: statuses });
     new StatusesView();
 });


Наши представления, NewStatusView и StatusesView все еще трудно тестировать, потому что они зависят он наличия HTML элемента. Для того, чтобы это исправить, мы зададим эти элементы при создании представлений:

Изменение №12
var events = _.clone(Backbone.Events);

 var Statuses = function() {
 };
 Statuses.prototype.add = function(text) {
     $.ajax({
         url: '/status',
         type: 'POST',
         dataType: 'json',
         data: { text: text },
         success: function(data) {
             events.trigger('status:add', data.text);
         }
     });
 };

 var NewStatusView = function(options) {
     this.statuses = options.statuses;
-    this.el = $('#new-status');
+    this.el = options.el;

     events.on('status:add', this.clearInput, this);

     var add = $.proxy(this.addStatus, this);
     this.el.find('form').submit(add);
 };
 NewStatusView.prototype.addStatus = function(e) {
     e.preventDefault();

     this.statuses.add(this.el.find('textarea').val());
 };
 NewStatusView.prototype.clearInput = function() {
     this.el.find('textarea').val('');
 };

-var StatusesView = function() {
-    this.el = $('#statuses');
+var StatusesView = function(options) {
+    this.el = options.el;

     events.on('status:add', this.appendStatus, this);
 };
 StatusesView.prototype.appendStatus = function(text) {
     this.el.find('ul').append('<li>' + text + '</li>');
 };

 $(document).ready(function() {
     var statuses = new Statuses();
-    new NewStatusView({ statuses: statuses });
-    new StatusesView();
+    new NewStatusView({ el: $('#new-status'), statuses: statuses });
+    new StatusesView({ el: $('#statuses') });
 });


Теперь код легко протестировать. С этим изменением мы можем использовать следующий трюк с jQuery, чтобы протестировать представления. Вместо того, чтобы инициализировать представление при помощи например $('#new-status'), мы можем передать необходимую HTML обертку jQuery, например $('…'). jQuery создаст необходимые элементы на лету. Это обеспечивает невероятно быстрые тесты, так как манипуляции с DOM отсутствуют.

Нашим следующим шагом будет введение помощника, чтобы немного очистить наши представления. Вместо того, чтобы писать this.el.find мы можем создать простую функцию помощник, чтобы мы могли написать this.$. С этим небольшим изменением это выглядит так, словно мы говорим, «я хочу использовать JQuery для поиска чего-то локально в представлении, а не глобально во всем HTML». И это так легко добавить:

Изменение №13
 var events = _.clone(Backbone.Events);

 var Statuses = function() {
 };
 Statuses.prototype.add = function(text) {
     $.ajax({
         url: '/status',
         type: 'POST',
         dataType: 'json',
         data: { text: text },
         success: function(data) {
             events.trigger('status:add', data.text);
         }
     });
 };

 var NewStatusView = function(options) {
     this.statuses = options.statuses;
     this.el = options.el;

     events.on('status:add', this.clearInput, this);

     var add = $.proxy(this.addStatus, this);
-    this.el.find('form').submit(add);
+    this.$('form').submit(add);
 };
 NewStatusView.prototype.addStatus = function(e) {
     e.preventDefault();

-    this.statuses.add(this.el.find('textarea').val());
+    this.statuses.add(this.$('textarea').val());
 };
 NewStatusView.prototype.clearInput = function() {
-    this.el.find('textarea').val('');
+    this.$('textarea').val('');
 };
+NewStatusView.prototype.$ = function(selector) {
+    return this.el.find(selector);
+};

 var StatusesView = function(options) {
     this.el = options.el;

     events.on('status:add', this.appendStatus, this);
 };
 StatusesView.prototype.appendStatus = function(text) {
-    this.el.find('ul').append('<li>' + text + '</li>');
+    this.$('ul').append('<li>' + text + '</li>');
 };
+StatusesView.prototype.$ = function(selector) {
+    return this.el.find(selector);
+};

 $(document).ready(function() {
     var statuses = new Statuses();
     new NewStatusView({ el: $('#new-status'), statuses: statuses });
     new StatusesView({ el: $('#statuses') });
 });


Однако, добавление этой функции для каждого представления выглядит глупо. Это одна из причин, чтобы использовать Backbone представления — повторное использование функциональности в представлениях.

Начало работы с представлениями(views)


В текущем состоянии нашего кода, нужно написать всего лишь пару строк для добавления Backbone представлений:

Изменение №14
 var events = _.clone(Backbone.Events);

 var Statuses = function() {
 };
 Statuses.prototype.add = function(text) {
     $.ajax({
         url: '/status',
         type: 'POST',
         dataType: 'json',
         data: { text: text },
         success: function(data) {
             events.trigger('status:add', data.text);
         }
     });
 };

-var NewStatusView = function(options) {
-    this.statuses = options.statuses;
-    this.el = options.el;
-
-    events.on('status:add', this.clearInput, this);
-
-    var add = $.proxy(this.addStatus, this);
-    this.$('form').submit(add);
-};
+var NewStatusView = Backbone.View.extend({
+    initialize: function(options) {
+        this.statuses = options.statuses;
+        this.el = options.el;
+
+        events.on('status:add', this.clearInput, this);
+
+        var add = $.proxy(this.addStatus, this);
+        this.$('form').submit(add);
+    }
+});
 NewStatusView.prototype.addStatus = function(e) {
     e.preventDefault();

     this.statuses.add(this.$('textarea').val());
 };
 NewStatusView.prototype.clearInput = function() {
     this.$('textarea').val('');
 };
 NewStatusView.prototype.$ = function(selector) {
     return this.el.find(selector);
 };

 var StatusesView = function(options) {
     this.el = options.el;

     events.on('status:add', this.appendStatus, this);
 };
 StatusesView.prototype.appendStatus = function(text) {
     this.$('ul').append('<li>' + text + '</li>');
 };
 StatusesView.prototype.$ = function(selector) {
     return this.el.find(selector);
 };

 $(document).ready(function() {
     var statuses = new Statuses();
     new NewStatusView({ el: $('#new-status'), statuses: statuses });
     new StatusesView({ el: $('#statuses') });
 });


Как вы видите из кода, мы используем Backbone.View.extend для создания нового Backbone представления. В наследнике мы можем указать методы, такие как инициализация, которая является конструктором.
Теперь, когда мы начали использовать Backbone представления, давайте переведем и второе представление на Backbone:

Изменение №15
var events = _.clone(Backbone.Events);

 var Statuses = function() {
 };
 Statuses.prototype.add = function(text) {
     $.ajax({
         url: '/status',
         type: 'POST',
         dataType: 'json',
         data: { text: text },
         success: function(data) {
             events.trigger('status:add', data.text);
         }
     });
 };

 var NewStatusView = Backbone.View.extend({
     initialize: function(options) {
         this.statuses = options.statuses;
         this.el = options.el;

         events.on('status:add', this.clearInput, this);

         var add = $.proxy(this.addStatus, this);
         this.$('form').submit(add);
-    }
+    },
+
+    addStatus: function(e) {
+        e.preventDefault();
+
+        this.statuses.add(this.$('textarea').val());
+    },
+
+    clearInput: function() {
+        this.$('textarea').val('');
+    },
+
+    $: function(selector) {
+        return this.el.find(selector);
+    }
 });
-NewStatusView.prototype.addStatus = function(e) {
-    e.preventDefault();
-
-    this.statuses.add(this.$('textarea').val());
-};
-NewStatusView.prototype.clearInput = function() {
-    this.$('textarea').val('');
-};
-NewStatusView.prototype.$ = function(selector) {
-    return this.el.find(selector);
-};

-var StatusesView = function(options) {
-    this.el = options.el;
-
-    events.on('status:add', this.appendStatus, this);
-};
-StatusesView.prototype.appendStatus = function(text) {
-    this.$('ul').append('<li>' + text + '</li>');
-};
-StatusesView.prototype.$ = function(selector) {
-    return this.el.find(selector);
-};
+var StatusesView = Backbone.View.extend({
+    initialize: function(options) {
+        this.el = options.el;
+
+        events.on('status:add', this.appendStatus, this);
+    },
+
+    appendStatus: function(text) {
+        this.$('ul').append('<li>' + text + '</li>');
+    },
+
+    $: function(selector) {
+        return this.el.find(selector);
+    }
+});

 $(document).ready(function() {
     var statuses = new Statuses();
     new NewStatusView({ el: $('#new-status'), statuses: statuses });
     new StatusesView({ el: $('#statuses') });
 });


Теперь, так как мы используем только Backbone представления, мы можем удалить функцию помощник this.$, потому что она уже существует в Backbone. Так же мы больше не нуждаемся в сохранении this.el, так как Backbone делает это автоматически когда представление инициализируется HTML элементом.

Изменение №16
var events = _.clone(Backbone.Events);

 var Statuses = function() {
 };
 Statuses.prototype.add = function(text) {
     $.ajax({
         url: '/status',
         type: 'POST',
         dataType: 'json',
         data: { text: text },
         success: function(data) {
             events.trigger('status:add', data.text);
         }
     });
 };

 var NewStatusView = Backbone.View.extend({
     initialize: function(options) {
         this.statuses = options.statuses;
-        this.el = options.el;

         events.on('status:add', this.clearInput, this);

         var add = $.proxy(this.addStatus, this);
         this.$('form').submit(add);
     },

     addStatus: function(e) {
         e.preventDefault();

         this.statuses.add(this.$('textarea').val());
     },

     clearInput: function() {
         this.$('textarea').val('');
     },
-
-    $: function(selector) {
-        return this.el.find(selector);
-    }
 });

 var StatusesView = Backbone.View.extend({
     initialize: function(options) {
-        this.el = options.el;
-
         events.on('status:add', this.appendStatus, this);
     },

     appendStatus: function(text) {
         this.$('ul').append('<li>' + text + '</li>');
     },
-
-    $: function(selector) {
-        return this.el.find(selector);
-    }
 });

 $(document).ready(function() {
     var statuses = new Statuses();
     new NewStatusView({ el: $('#new-status'), statuses: statuses });
     new StatusesView({ el: $('#statuses') });
 });


Использование моделей(models)


Следующим шагом является внедрение моделей, которые отвечают за общение с сервером, то есть за Ajax запросы и ответы. Так как Backbone хорошо абстрагирует Ajax, нам больше не нужно указывать тип запроса, тип данных и данные. Теперь нам нужно только указать URL и вызвать сохранение модели. Метод сохранения принимает данные, которые мы хотим сохранить, в качестве первого параметра, и параметры, такие как callback, в качестве второго параметра.

Изменение №17
var events = _.clone(Backbone.Events);

+var Status = Backbone.Model.extend({
+    url: '/status'
+});
+
 var Statuses = function() {
 };
 Statuses.prototype.add = function(text) {
-    $.ajax({
-        url: '/status',
-        type: 'POST',
-        dataType: 'json',
-        data: { text: text },
-        success: function(data) {
-            events.trigger('status:add', data.text);
-        }
-    });
+    var status = new Status();
+    status.save({ text: text }, {
+        success: function(model, data) {
+            events.trigger('status:add', data.text);
+        }
+    });
 };

 var NewStatusView = Backbone.View.extend({
     initialize: function(options) {
         this.statuses = options.statuses;

         events.on('status:add', this.clearInput, this);

         var add = $.proxy(this.addStatus, this);
         this.$('form').submit(add);
     },

     addStatus: function(e) {
         e.preventDefault();

         this.statuses.add(this.$('textarea').val());
     },

     clearInput: function() {
         this.$('textarea').val('');
     }
 });

 var StatusesView = Backbone.View.extend({
     initialize: function(options) {
         events.on('status:add', this.appendStatus, this);
     },

     appendStatus: function(text) {
         this.$('ul').append('<li>' + text + '</li>');
     }
 });

 $(document).ready(function() {
     var statuses = new Statuses();
     new NewStatusView({ el: $('#new-status'), statuses: statuses });
     new StatusesView({ el: $('#statuses') });
 });


Обработка нескольких моделей


Теперь, когда мы ввели модели, нам нужна концепция списка моделей, таких как список статусов в нашем приложении. В Backbone эта концепция называется коллекция(collection).
Одной из замечательных вещей в коллекциях является то, что они имеют область событий. В основном, это просто означает, что мы можем привязать и вызывать события, непосредственно на коллекции, вместо использования наших переменных событий. Если же мы теперь начинаем генерировать события непосредственно на статусах, то нет необходимости в слове «status» в имени события, поэтому мы переименуем его из «status:add» в «add».

Изменение №18
-var events = _.clone(Backbone.Events);
-
 var Status = Backbone.Model.extend({
     url: '/status'
 });

-var Statuses = function() {
-};
-Statuses.prototype.add = function(text) {
-    var status = new Status();
-    status.save({ text: text }, {
-        success: function(model, data) {
-            events.trigger("status:add", data.text);
-        }
-    });
-};
+var Statuses = Backbone.Collection.extend({
+    add: function(text) {
+        var that = this;
+        var status = new Status();
+        status.save({ text: text }, {
+            success: function(model, data) {
+                that.trigger("add", data.text);
+            }
+        });
+    }
+});

 var NewStatusView = Backbone.View.extend({
     initialize: function(options) {
         this.statuses = options.statuses;

-        events.on("status:add", this.clearInput, this);
+        this.statuses.on("add", this.clearInput, this);

         var add = $.proxy(this.addStatus, this);
         this.$('form').submit(add);
     },

     addStatus: function(e) {
         e.preventDefault();

         this.statuses.add(this.$('textarea').val());
     },

     clearInput: function() {
         this.$('textarea').val('');
     }
 });

 var StatusesView = Backbone.View.extend({
     initialize: function(options) {
+        this.statuses = options.statuses;
+
-        events.on("status:add", this.appendStatus, this);
+        this.statuses.on("add", this.appendStatus, this);
     },

     appendStatus: function(text) {
         this.$('ul').append('<li>' + text + '</li>');
     }
 });

 $(document).ready(function() {
     var statuses = new Statuses();
     new NewStatusView({ el: $('#new-status'), statuses: statuses });
-    new StatusesView({ el: $('#statuses') });
+    new StatusesView({ el: $('#statuses'), statuses: statuses });
 });


Мы можем упростить и этот код, используя метод создания Backbone. Он создает новый экземпляр модели, добавляет его в коллекцию и сохраняет его на сервере. Поэтому мы должны указать, какой тип модели мы будем использовать для коллекции. Есть две вещи, которые мы должны изменить, чтобы использовать Backbone коллекции:
  • При создании нового статуса мы должны передать и имя атрибута, которое хотим сохранить, а не просто текст
  • При создании автоматически вызовется событие «add», но вместо передачи только текста, как было до сих пор, отправится вся модель. Мы можем получить текст из модели вызвав model.get(«text»)


Изменение №19
 var Status = Backbone.Model.extend({
     url: '/status'
 });

 var Statuses = Backbone.Collection.extend({
-    add: function(text) {
-        var that = this;
-        var status = new Status();
-        status.save({ text: text }, {
-            success: function(model, data) {
-                that.trigger("add", data.text);
-            }
-        });
-    }
+    model: Status
 });

 var NewStatusView = Backbone.View.extend({
     initialize: function(options) {
         this.statuses = options.statuses;

         this.statuses.on("add", this.clearInput, this);

         var add = $.proxy(this.addStatus, this);
         this.$('form').submit(add);
     },

     addStatus: function(e) {
         e.preventDefault();

-        this.statuses.add(this.$('textarea').val());
+        this.statuses.create({ text: this.$('textarea').val() });
     },

     clearInput: function() {
         this.$('textarea').val('');
     }
 });

 var StatusesView = Backbone.View.extend({
     initialize: function(options) {
         this.statuses = options.statuses;

         this.statuses.on("add", this.appendStatus, this);
     },

-    appendStatus: function(text) {
+    appendStatus: function(status) {
-        this.$('ul').append('<li>' + text + '</li>');
+        this.$('ul').append('<li>' + status.get("text") + '</li>');
     }
 });

 $(document).ready(function() {
     var statuses = new Statuses();
     new NewStatusView({ el: $('#new-status'), statuses: statuses });
     new StatusesView({ el: $('#statuses'), statuses: statuses });
 });


Как с el ранее, Backbone автоматически установит this.collection когда будет передана коллекция. Поэтому мы переименуем statuses в collection в наших представлениях:

Изменение №20
var Status = Backbone.Model.extend({
     url: '/status'
 });

 var Statuses = Backbone.Collection.extend({
     model: Status
 });

 var NewStatusView = Backbone.View.extend({
-    initialize: function(options) {
+    initialize: function() {
-        this.statuses = options.statuses;
-
-        this.statuses.on('add', this.clearInput, this);
+        this.collection.on('add', this.clearInput, this);

         var add = $.proxy(this.addStatus, this);
         this.$('form').submit(add);
     },

     addStatus: function(e) {
         e.preventDefault();

-        this.statuses.add({ text: this.$('textarea').val() });
+        this.collection.create({ text: this.$('textarea').val() });
     },

     clearInput: function() {
         this.$('textarea').val('');
     }
 });

 var StatusesView = Backbone.View.extend({
-    initialize: function(options) {
+    initialize: function() {
-        this.statuses = options.statuses;
-
-        this.statuses.on('add', this.appendStatus, this);
+        this.collection.on('add', this.appendStatus, this);
     },

     appendStatus: function(status) {
         this.$('ul').append('<li>' + status.get('text') + '</li>');
     }
 });

 $(document).ready(function() {
     var statuses = new Statuses();
-    new NewStatusView({ el: $('#new-status'), statuses: statuses });
+    new NewStatusView({ el: $('#new-status'), collection: statuses });
-    new StatusesView({ el: $('#statuses'), statuses: statuses });
+    new StatusesView({ el: $('#statuses'), collection: statuses });
 });


События в представлениях


Теперь, давайте наконец избавимся от $.proxy. Мы можем сделать это делегируя Backbone управление событиями. Это выглядит так {'event selector': 'callback'}:

Изменение №21
var Status = Backbone.Model.extend({
     url: '/status'
 });

 var Statuses = Backbone.Collection.extend({
     model: Status
 });

 var NewStatusView = Backbone.View.extend({
+    events: {
+        'submit form': 'addStatus'
+    },
+
     initialize: function() {
         this.collection.on('add', this.clearInput, this);
-
-        var add = $.proxy(this.addStatus, this);
-        this.$('form').submit(add);
     },

     addStatus: function(e) {
         e.preventDefault();

         this.collection.create({ text: this.$('textarea').val() });
     },

     clearInput: function() {
         this.$('textarea').val('');
     }
 });

 var StatusesView = Backbone.View.extend({
     initialize: function() {
         this.collection.on('add', this.appendStatus, this);
     },

     appendStatus: function(status) {
         this.$('ul').append('<li>' + status.get('text') + '</li>');
     }
 });

 $(document).ready(function() {
     var statuses = new Statuses();
     new NewStatusView({ el: $('#new-status'), collection: statuses });
     new StatusesView({ el: $('#statuses'), collection: statuses });
 });


Экранируй это


Нашим последним шагом будет предотвращение XSS атак. Вместо использования model.get('text') будем использовать встроенную функцию экранирования, выглядит она так model.escape('text'). Если вы используете Handlebars, Mustache или другие шаблонизаторы, вы можете получит защиту из коробки.

Изменение №22
 var Status = Backbone.Model.extend({
     url: '/status'
 });

 var Statuses = Backbone.Collection.extend({
     model: Status
 });

 var NewStatusView = Backbone.View.extend({
     events: {
         "submit form": "addStatus"
     },

     initialize: function(options) {
         this.collection.on("add", this.clearInput, this);
     },

     addStatus: function(e) {
         e.preventDefault();

         this.collection.create({ text: this.$('textarea').val() });
     },

     clearInput: function() {
         this.$('textarea').val('');
     }
 });

 var StatusesView = Backbone.View.extend({
     initialize: function(options) {
         this.collection.on("add", this.appendStatus, this);
     },

     appendStatus: function(status) {
-        this.$('ul').append('<li>' + status.get("text") + '</li>');
+        this.$('ul').append('<li>' + status.escape("text") + '</li>');
     }
 });

 $(document).ready(function() {
     var statuses = new Statuses();
     new NewStatusView({ el: $('#new-status'), collection: statuses });
     new StatusesView({ el: $('#statuses'), collection: statuses });
 });


Мы закончили!


Так выглядит финальная версия кода:
var Status = Backbone.Model.extend({
    url: '/status'
});

var Statuses = Backbone.Collection.extend({
    model: Status
});

var NewStatusView = Backbone.View.extend({
    events: {
        'submit form': 'addStatus'
    },

    initialize: function() {
        this.collection.on('add', this.clearInput, this);
    },

    addStatus: function(e) {
        e.preventDefault();

        this.collection.create({ text: this.$('textarea').val() });
    },

    clearInput: function() {
        this.$('textarea').val('');
    }
});

var StatusesView = Backbone.View.extend({
    initialize: function() {
        this.collection.on('add', this.appendStatus, this);
    },

    appendStatus: function(status) {
        this.$('ul').append('<li>' + status.escape('text') + '</li>');
    }
});

$(document).ready(function() {
    var statuses = new Statuses();
    new NewStatusView({ el: $('#new-status'), collection: statuses });
    new StatusesView({ el: $('#statuses'), collection: statuses });
});

Здесь можно посмотреть на приложение после реорганизации. Да, конечно с пользовательской точки зрения оно выглядит точно так же как и первая версия. И к тому же код разросся с 16 строк до более чем 40, так почему же я думаю что код стал лучше? Да потому что теперь мы работаем на более высоком уровне абстракции. Этот код является более легким в обслуживании, его легче повторно использовать и расширять, и проще тестировать.
Как мы видели, Backbone помог значительно улучшить структуру кода приложения, и по моему опыту конечный результат является менее сложным и имеет меньше кода, чем мой «ванильный JavaScript».

UPD для удобства скрыл исходники изменений в спойлеры
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 43

    +13
    Скролл умер.
    А если серьёзно, то код сложно читать с этими плюсами и минусами. Может можно как-то цветом обозначить? И да, спасибо за труд.
      0
      Похоже, подсветка кода на Хабре не умеет отображать синтаксис diff. Я к диффам привык, очень информативно, хотя статью пока еще не до конца осилил.
        +8
        Все же лучше выкладывать итоговый код, без минусов, а дифы прятать в спойлер. А то правда очень тяжело читается.
          –1
          Очень интересна тема, но из-за плюсов-минусов не смог осилить статью.
            +5
            Не знаю, мне нравится ваша идея с ±
              0
              Поддерживаю подход и использованием diff. Даже без подсветки это удобнее, чем просто код.
            +9
            Вы забыли добавить ссылку на автора.
              +1
              Добавил и перенес в хаб переводы.
              –2
              А теперь, внимание, главный вопрос: зачем?
                +3
                Автор как бы намекает
                Этот код является более легким в обслуживании, его легче повторно использовать и расширять, и проще тестировать.
                  +9
                  Вы действительно считаете, что вот это
                  Скрытый текст
                  var Status = Backbone.Model.extend({
                      url: '/status'
                  });
                  
                  var Statuses = Backbone.Collection.extend({
                      model: Status
                  });
                  
                  var NewStatusView = Backbone.View.extend({
                      events: {
                          'submit form': 'addStatus'
                      },
                  
                      initialize: function() {
                          this.collection.on('add', this.clearInput, this);
                      },
                  
                      addStatus: function(e) {
                          e.preventDefault();
                  
                          this.collection.create({ text: this.$('textarea').val() });
                      },
                  
                      clearInput: function() {
                          this.$('textarea').val('');
                      }
                  });
                  
                  var StatusesView = Backbone.View.extend({
                      initialize: function() {
                          this.collection.on('add', this.appendStatus, this);
                      },
                  
                      appendStatus: function(status) {
                          this.$('ul').append('<li>' + status.escape('text') + '</li>');
                      }
                  });
                  
                  $(document).ready(function() {
                      var statuses = new Statuses();
                      new NewStatusView({ el: $('#new-status'), collection: statuses });
                      new StatusesView({ el: $('#statuses'), collection: statuses });
                  });
                  



                  Легче поддерживать, чем это?
                  Скрытый текст
                  $(document).ready(function() {
                      $('#new-status form').submit(function(e) {
                          e.preventDefault();
                  
                          $.ajax({
                              url: '/status',
                              type: 'POST',
                              dataType: 'json',
                              data: { text: $('#new-status').find('textarea').val() },
                              success: function(data) {
                                  $('#statuses').append('<li>' + data.text + '</li>');
                                  $('#new-status').find('textarea').val('');
                              }
                          });
                      });
                  });
                  
                  



                  Я вовсе не хочу сказать, что второй пример — элегантный. Но если выбирать наименьшее из двух зол — второй пример намного лучше.
                    +4
                    Backbone код легко разносится на файлы где у каждого файла строгие соглашения по именованию и ответственности, а значит на проекте где от фронтенд-кода можно рехнутся будет гораздо проще найти нужный файл и внести правки или создать новую функциональность.

                    В jQuery же всегда одна и та же проблема — спагетти-код, который, как бы ты не старался будет удерживать рост фронтенда и душить проект.
                      +3
                      Вы давно пишете на бэкбоне или чисто теоретизируете? Доводилось ли писать что-то более-менее сложное?
                      Когда в проекте много разнообразных вьюх, моделей и шаблонов, количество файлов растёт с бешеной скоростью, что, как не трудно догадаться, уже не становится таким очевидным преимуществом.

                      В jQuery можно написать 3 строчки и они просто будут работать. Без создания новых файлов, вьюх, моделей, коллекций и шаблонов. К тому же, в случае изменения требований (что бывает почти всегда), не придётся переписывать всю систему или вставлять костыли на том же jQuery.
                        0
                        Нет, я не много пишу на бэкбоне к сожалению, но это и не чистая теория. Я вижу то что сейчас творится в проектах с jQuery без бэкбона и с бэкбоном. Второй вариант мне нравится больше, и в текущий момент, и в ближайшем будущем.

                        Я согласен что на jQuery можно написать код который будет просто работать, но черт возьми, в нем реально можно умереть при малейшем изменении верстки. С бэкбоном все проще в этом плане, он модульней и понятней, во всяком случае — мне. А требования у нас не меняются, тьфу-тьфу-тьфу, потому что мы не работаем на стороннего заказчика.
                          +3
                          Что вам мешает писать модульно без бекбона?
                          0
                          В данный момент я разрабатываю на Backbone.Marionette довольно сложный проект с очень толстым клиент-сайдом. Честно вам скажу, на JQuery я бы такое рехнулся писать, про поддержку вообще молчу.

                          Ну и Coffee Script выручает.
                            0
                            ...Backbone.Marionette…
                            Ну и Coffee Script выручает.

                            Искренне сочувствую вам.
                              +1
                              Не стоит. Мне очень нравится.
                            0
                            А потом ищи кто и где повесил обработчик. В случае бэкбона все будет на своих местах. Нашел вьюху, используемые модели и радуешься жизни.
                              +1
                              Это если одна вьюха — нашёл. А если их много, да они ещё и вложенные?
                              +3
                              Я сейчас работаю над довольно большим проектом в кучей JS кода (только одних вьюх порядка 50). Конечно не всё так радужно как в статье, но лучше иметь дело с backbone-style кодом чем с jquery-style лапшой в таких количествах.

                              То что в jquery пишется в 3 строки — на бекбоне пишется в 6. В статье автор специально усложняет проект, чтоб показать что такой код легче улучать.

                              И да backbone далеко не идеал, но сидеть и искать баг в 10 килобайтах jquery лапши это ТО еще удовольствие.

                              PS. По поводу кучи файлов. Я пришел к такому методу. Создаю один JS файл на какой-то логический модуль. Внутри файла анонимная функция в которой описаны все модели, коллекции и вью модуля. Наружу обычно вывожу только класс самой общей view, который подключаю на странице.
                                0
                                И да backbone далеко не идеал, но сидеть и искать баг в 10 килобайтах jquery лапши это ТО еще удовольствие

                                Уверяю вас, лапша не зависит от того, используется ли в проекте Backbone или нет.

                                Наружу обычно вывожу только класс самой общей view, который подключаю на странице.

                                Но в таком случае, вы не сможете использовать одну и ту же модель в разных модулях.
                                  0
                                  > Уверяю вас, лапша не зависит от того, используется ли в проекте Backbone или нет.
                                  Чисто теоретически я с Вами согласен. Но опыт моих и чужих проектов говорит о другом. И этому есть простое объяснение: для jquery нужен еще отдельный стайлгаид, иначе будет лапша в 100% случаях (так тупо быстрее). У бекбона уже есть структура, место биндинга к эвентам и прочее.
                                  Всё выше сказанное справедливо для так сказать не профессионалов JS. Т.е. серверных разработчиков которые заодно пишут и фронтенд. Чаще всего такие разработчики не профессионалы в JS, а значит с бекбоном им будет проще писать более понятный остальным код.

                                  > Но в таком случае, вы не сможете использовать одну и ту же модель в разных модулях.
                                  Если понадобится можно всегда вынести модель в отдельный файл, либо просто вывести наружу
                                0
                                Когда поведение клиента обусловлено довольно сложной логикой, то проще работать с моделями/коллбеками, чем превращать в код в лапшу, которая работает способом, который понятен лишь для его создателя. Мне даже в прототипе приложения доводилось писать js код на 1000 строк, у которого логика, скажем так, не тривиальная. При этом страница была обычной, с точки зрения пользователя.

                                Я не могу сказать, что backbone был бы идеальной альтернативной, но подход «просто работает» не подходит, особенно когда нужно сильно расширять функционал.
                                +1
                                jQuery код разносится на файлы с соглашениями по именованию и ответственности, если этого хотеть. Фабрика виджетов jQueryUI в помощь для создания модульного интерфейса. Если виджет сложный со связанными полями тогда mvc сама напрашивается и реализуется внутри виджета созданием объекта (модели) со своими set/get методами и событиями. Тут, может, поможет Backbone, но пока без него справляюсь)
                                  0
                                  Много раз слышал про jQueryUI, но не довелось потрогать, так что не могу поддержать спор :)
                                  Возможно вы здесь правы, но мне Backbone привычней, хоть и появился смысл взглянуть на jQueryUI
                                    +1
                                    И не трогайте. Слишком тяжелая. В этом плане те же бутстраповские виджеты более симпатичны.
                                0
                                Мне кажется здесь вопрос в подходе раз, и во-вторых, в количестве. Когда подобных запросов много, поддерживать backbone на мой взгляд проще. Он очень логичен. Все-таки весь конечный код не окажется в одном файле, а будет логично собираться и в случае проблем, будет достаточно легко определить, где проблема.

                                Хотя я соглашусь в вами, как бы я не любил backbone — для примера приведенного выше — второй пример намного легче и для понимания и для поддержания :) Но автор я думаю хотел донести до нас принцип.
                                  0
                                  Да, я согласен с вами насчет данного конкретного примера. Когда у вас небольшая задача и вы знаете, что вам не придется дорабатывать и развивать код, то да можно писать спагетти-код. Но во всех остальных случаях необходимо писать «правильный» код. (Под «правильным» кодом я подразумеваю не код на Backbone. Я подразумеваю код, который отвечает единому принципу ответственности, код который можно тестировать, и легко поддерживать.)
                                    0
                                    Друг мой, понимаете ли вы, во что превратится ваш «правильный» код, когда вьюх станет больше 50, а модели будут вложены друг в друга? Если вы покинете такой проект, то заберёте сакральные знания об устройстве ваших абстракций с собой, и уже никто не сможет поддерживать такой проект.
                                      0
                                      Тут не 50 штук вьюшек, но все же. Просто показать структуру. На мой взгляд она проще чем многое другое.
                                      NodeCellar
                                        0
                                        Я правильно понимаю, что вот это всё написано для отображения 5 статичных страниц?
                                          0
                                          Ничего сложного не увидел
                                        0
                                        А представляете, что это будет за простыня кода на классическом jQuery, если в организованом виде на Backbone вьюх больше 50?
                                        Todomvc Backbone Requirejs Example
                                        Todomvc jQuery Example
                                        Кода, как видно, больше, но читаемость, расширяемость и модульность в разы лучше, как по мне.
                                          0
                                          Больше кода не может быть читабельнее, если не впадать в крайности, конечно.
                                          Модульность? Полагаю, разговор не про разные названия объектов, в которых определены методы, а про возможность повторного использования кода в других проектах. Очень интересно посмотреть, где вы будете использовать TodoModel и TodoView?
                                          Расширяемость extend'ом-то?
                                      +1
                                      Допустим нужно доработать задачу, получить сохраненные статусы. В примере с Backbone, просто добавляем ещё одну строчку `statuses.fetch();`, а пример на jQuery нужно будет переписать, и если изначально писали не вы, так сначала разобраться.
                                  0
                                  Спасибо за перевод, кода действительно много, тяжело осилить в понедельник утром :) Но зато подойдет для любого уровня знаний.
                                    0
                                    Почему бы код не запрятать под спойлер? Тогда будет значительно меньше статья, а, следовательно, менее громоздкой.
                                      0
                                      Отличная идея. Обновил статью.
                                      +3
                                      Статья супер, наконец-то я понял зачем нужен backbone.
                                        0
                                        В свое время решал следующую задачу: реализовать виджет, который может строить древовидные фильтры (с поддержкой функций), для фильтрации grid'a с данными. Упрощенным примером могут служить «Расширенные сегменты» в Google Analytics.

                                        Рассматривал вариант использовать backbone. В результате отказался, поскольку backbone не умеют реализовывать древовидные структуры ни из моделей, ни из view. Реализовал на jQuery-спагетти, о чем уже не раз пожалел. Но речь не об этом.

                                        Может кто-то поделиться ссылочками на проекты, сделанные на backbone, но с нестандартной структурой данных (не коллекция моделей), а деревом к примеру? Или предложения, как решить описанную мной задачу с использованием backbone?
                                          0
                                          Автор! Вы — человек-патч! :]
                                            +1
                                            >Изменение №5
                                            >Но при запуске в Chrome мы увидим ошибку:
                                            >Uncaught TypeError: Cannot call method 'add' of undefined

                                            Почему то вспомнился анекдот, где программист копирующий код пошагово с книги получил ошибку компиляции, перелазил все форума сам решил проблему, и только потом прочитал следующую страничку книги где было написано «вы увидите что код не компилируется. Поэтому давайте сделаем ...»

                                            Only users with full accounts can post comments. Log in, please.