Pull to refresh

От JQuery до Backbone

Reading time 23 min
Views 28K
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 для удобства скрыл исходники изменений в спойлеры
Tags:
Hubs:
+70
Comments 43
Comments Comments 43

Articles