Практический пример использования Backbone

В данной заметке речь пойдет об использовании Backbone, а в частности, примеры кода работающего с сервером. Это должен быть некий промежуточный пункт в блужданиях Ищущего, так как иначе начать им пользоваться очень сложно, а вероятность отказаться от идеи перехода стремится к единице.

Зачем вообще нужна данная публикация? Тут уже были статьи по теме, но они затрагивают лишь очень поверхностно то, о чем и так написано в документации, хотя следовало бы показать пути недокументированные. Именно так: backbone это не комплексная standalone библиотека вроде Jquery, которую и не тронешь, не сломав. Данная библиотека предназначена лишь для построения приблизительной структуры; мы же можем лепить из данного материала то, что нам нужно. Еще раз: тут нет смысла искать готовые паттерны, буква в букву примеры не нужно перепечатывать, все равно не поможет. Нужно научиться пользоваться инструментом, после чего уже бросаться в бой.

Итак, вы перешагнули рубеж хабраката. Я не ставлю своей целью объяснение базовых принципов библиотеки, статей было уже предостаточно:
habrahabr.ru/blogs/javascript/129928
habrahabr.ru/blogs/javascript/118782
habrahabr.ru/blogs/webdev/123130

Замечу, что все три описывают стандартное использование Backbone, но очень редко действительно нужно, например, использовать роутер. Или же нужно банально связаться с сервером — а как это сделать? Все отсылают к Backbone.sync, а примеров почему-то никто не предоставляет. Считайте предыдущее предложение одной из основных причин написания данной заметки. Если вы с ним не согласны — дальше можно не читать.

Начнем. Писать мы будем фронтенд редактирования заметок. Сфокусируемся на сриптах, нагло игнорируя как эстетические пожелания пользователей, так и право выбора: тестировать я буду только в хроме. Ссылки на пример и код в архиве приведу в конце.

Что нам нужно, чтобы начать? Создаем пустую xhtml страничку со стандартной структурой, подключаем jquery, underscore, backbone (именно в таком порядке). В конце добавляем ссылку на наш скрипт. Со стороны сервера создаем php файл, который будет отвечать за чтение/запись данных (лежит в архиве, app.php, код приводить не буду, скрипт просто обрабатывает запросы вида ?method=).

Когда приготовления закончены, начнем писать скрипт на js. Создадим контейнер для хранения моделей и лога:
app = {
	debug: true,
	log: function(obj) {
		if(this.debug) {
			console.log(obj);
		}
	},
	models: {}
}

Функция Backbone.sync спроектирована так, что ее очень легко переобъявить, ничего не испортив. Кроме того, у кажой модели может быть свой метод синхронизации (угадайте, как он должен называться?). Мы будем писать глобальную функцию для всего нашего срипта. Цели, преследуемые нами:
  • Заставить Backbone работать с нашим бекэндом
  • При получении данных фронтеном аукать в лог для проверок
  • Проверять данные с сервера на флаг ошибки (is_error — устанафливается нашим скриптом)
  • Упростить добавление/сохранение (сливаем методы в один)
  • Производить проверку входных данных
  • Прерывать старый запрос при новом (только для пары модель/метод)

Что получилось у меня (ваша реализация может отличаться):
Backbone.sync = function(method, model, options) {
  // Сливаем методы дубовым способом
  var method = (method=='update'||method=='create')?'save':method;
  // Прерываем старый запрос
  if(model.loading && model.loading.method == method) {
    model.loading.xhr.abort();
  }
  app.log('Запрос на "'+model.url(method)+'"');
  var xhr = $.ajax({
    type: 'POST',
    url: model.url(method),
    data: (model instanceof Backbone.Model)?model.toJSON():{},
    success: function(ret) {
      // Проверка наличия ошибки
      if(ret.is_error) {
        app.log('Ошибка запроса');
      } else {
        app.log('Запрос завершен');
        (function(data){
          app.log('Backbone.sync получил данные:', data);
          if(data.res) {
            // Ответ - строка, вместо записей
            model.trigger('load:res', data.res);
          } else {
            // Ответ - записи, либо данные
            options.success(data.rows?data.rows:data);
          }
          model.trigger((method=='save')?'save':'load', data);
        })(ret.data);
      }
    },
    error: function (req_obj, msg, error) {
      app.log('Ошибка запроса');
    },
    dataType: 'json'
  });
  // Сохраняем ссылку на запрос в модель
  model.loading = {
    method: method,
    xhr: xhr
  }
};

Я немного слукавил. Если вызвать у модели методы так: read list read, то последний read не оборвет первый, но статья не об этом, так что кладем огромный болт.

Код модели записи:
app.models.note = (Backbone.Model.extend({
  defaults: {
    id: 0,
    text: ''
  },
    
  url: function(method){
    return './app.php?method='+method;
  }
}));

app.models.Note = (Backbone.View.extend({
  tagName: 'li',
  className: 'entity',
    
  render: function(){
    var data = this.model.toJSON();
    var that = this;
    $(this.el).html('').data('rowId', data.id);
    $(this.el).append($('<input type="text" />').val(data.text));
    $(this.el).append($('<button>Сохранить</button>').click(function(){
      app.models.page.trigger('post:save', {
        'id': $(this).closest('li').data('rowId'),
        'text': $(this).closest('li').find('input').val()
      });
    }));
    $(this.el).append($('<button>Удалить</button>').click(function(){
      if(!confirm('Вы уверены, что хотите удалить эту запись?')) return;
      app.models.notes.get($(this).closest('li').data('rowId')).destroy();
    }));
    return this;
  }
}));

Список записей:
app.models.notes = new (Backbone.Collection.extend({
  model: app.models.note,
    
  initialize: function(){
    this.bind('destroy', function(){
      this.reload();
    }, this);
  },
    
  reload: function(){
    var that = this;
    var options = ({
      error:function(){
        app.log('Ошибка обновления записей!');
        that.trigger('change');
      },
      success: function(){
        app.log('Записи обновлены');
        that.trigger('change');
      }
    });
    app.log('Обновление записей...');
    this.fetch(options);
  },
    
  url: function(method){
    return './app.php?method=list';
  }
}));

И последнее, модель страницы:
app.models.page = new (Backbone.View.extend({
  el: null,
  el_list: null,
  notes: null,
    
  initialize: function(){
    this.bind('page:load', this.pageLoad, this);
    this.bind('list:reload', this.listReload, this);
    this.bind('post:save', this.postSave, this);
    this.notes = app.models.notes;
    this.notes.bind('change', this.onListChange, this);
    this.notes.bind('load:res', this.onListChange, this);
    return this;
  },
    
  pageLoad: function(data) {
    var that = this;
    this.el = $('.layout');
    this.el_list = this.el.find('.items-list');
      
    // Кнопка обновления
    this.el.find('.title .refresh').bind('click', function(){
      that.trigger('list:reload')
    });
      
    // Кнопка добавления
    this.el.find('.items-add-submit').bind('click', function(){
      that.trigger('post:save', {
        id: false,
        text: $('.items-add-text').val()
      });
    });
    this.trigger('list:reload');
  },
    
  render: function(ret){
    $(this.el_list).html('');
    if(!ret) {
      app.log('Вывод записей. Количество: '+this.notes.length);
      _(this.notes.models).each(function(item){
        this.appendItem(item);
      }, this);
    } else {
      app.log('Вывод записей. Результат: "'+ret+'"');
      $(this.el_list).html('').append($('<li class="res"></li>').text(ret));
    }
    return this;
  },
    
  appendItem: function(item) {
    var view = new app.models.Note({
      model: item
    });
    $(this.el_list).append(view.render().el);
  },
    
  onListChange: function(ret){
    this.render(ret);
  },
    
  postSave: function(obj){
    var model = new app.models.note();
    if(obj.id) {
      model.set({
        id:obj.id
      });
    }
    model.set({
      text:obj.text
    });
    model.save();
    this.trigger('list:reload');
  },
    
  listReload: function(){
    this.notes.reload();
  }
}));

Что-то забыли… Ах да, запускаем рендеринг:
$(document).ready(function(){
	app.models.page.trigger('page:load');
});


Как видите, все просто. Я намеренно приводил код кусками, вместо разжевывания каждой функции, т.к. статья ориентированна на человека хоть немного знакомого с js/backbone. Если это не про вас — выше я давал ссылки, там подробно все расписано. Если возникнут сложности в понимании или нужны ополнительные пояснения к коду — пишите.
Код в действии: yurov.me/art
Весь код в архиве: yurov.me/art/art.tar.gz

P.s. код на сервере дубовый, возможны глюки. Важно показать фронтенд. Если не будет работать — можете попробовать запустить у себя локально либо просто просмотреть архив

P.P.S. Товарищ oWeRQ привел код в порядок, позже обновлю статью (его код значительно чище):
owerq.dyndns.org/test/art

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 37

    +1
    а чем ваш пример отличается от примера из оф. документации ?
      +4
      Самым важным — связкой с сервером. Я ведь с этого начал заметку.
        +2
        Что то связку с сервером я не увидел толком. Да данные для асинхронных запросов я вижу, но зачем переписывать Backbone.Sync? Чтобы посылать всё через POST? у модели и коллекции есть четыре метода, по одному на каждый из запросов: Get Post и тд. Можно было юзать их и просто передавать функции успеха и эррора. Дальше, что вобще за идея такая появилась отдать success на выполнение и проверить там на error ещё раз? Это ашипко в логике бэкэнда явно...

        Было бы лучше более интересно посмотреть на танцы с бубном между бэкбоном и какими то самопальными рест сервисами хотябы, или респонзами из PHP. Лично я в своё время промониторил все статьи по бэкбону, но с вашей ничего полезного и нового не вытянул
      +1
      >статья ориентированна на человека хоть немного знакомого с js/backbone.
      А зачем она ему тогда?
        +9
        Объясню на своем примере.
        Почитал я о backbone. Решил что ООП и mvc на клиенте — это круто. Что дальше? Модели? Коллекции? Я полностью дважды перечитывал (ну не полностью, процентов 60) доки. А толку? Буквально, там так и написано:
        documentcloud.github.com/backbone/
        «Backbone.sync is the function that Backbone calls every time it attempts to read or save a model to the server… You can override it in order to use a different persistence strategy, such as WebSockets, XML transport, or Local Storage.»:
        «Backbone.sync — функция для связи с сервером. Вы можете ее переписать, чтобы хранить данные иначе.». И Че?
        После подробного изучения мне понадобилась куча времени, чтобы разобраться с тем, как модели и коллекции взаимосвязаны (именно внутренние связи библиотеки), как backbone парсит данные (что он делает с ответом? Просто перезаписывает данные модели? Обновляет ли он каждую?) и т.д. Из-за этих сложностей сходу его поднять не просто, но как разберешься — сразу все становится простым для понимания. Эта статья для новичков, которые на backbone не писали, но знакомы с основами.
        Об этом я, кстати, тоже писал вначале, чтобы не возникало подобных вопросов.
        +2
        А зачем вы вызов Backbone.[View|Model].extend оборачиваете в ();?
          0
          Я сталкивался со случаем, когда без внешних скобок код не работал. Тут есть описание проблемы:
          stackoverflow.com/questions/3796308/parenthesis-around-functions-in-javascript

          Да и логически это может быть удобнее. Помешать точно не сможет, лучше перестраховаться.
            0
            Одно дело — вокруг function (...) {… }, другое дело — вокруг обычного выражения. Зачем???
              0
              Я ведь не лабораторную писал. Сработала многолетняя привычка экранировать/закрывать/обрамлять все, что может потенциально что-то нарушить. Здесь действительно скобки лишние.

              В любом случае, ссылка выше может быть полезна тем, кто не знал об этом.
                0
                Они действительно нужны:
                habrahabr.ru/blogs/javascript/132728/#comment_4407956
                +1
                Перестраховаться? Карго-культ? Они там не нужны.
                  0
                  А вот ничего подобного. Только что убрал — перестало работать. Копаю.
                    0
                    Я даже fiddle создал. Вы говорили очень умно, так что я запутался. Они там очень критичны. Посмотрите пояснение тут:
                    jsfiddle.net/nick4fake/6pmhM/2/

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

                    Как видите то, что вы назвали «культом Карго» на самом деле помогло избежать фатальной ошибки. Я не говорю, что я прав, вставляя скобки лишний раз. Просто в данном примере они действительно нужны. Вы бы банально проверили, прежде чем утверждать, что «Они там не нужны»
                      +2
                      Ох блин, я понял. Да, надо внимательнее смотреть на код.

                      Но употреблять слово «перестраховаться» по отношению к коду (а не логике) — это неправильно.
                        0
                        Вспомнилось:
                        govnokod.ru/111
                          0
                          У меня на прошлой работе один чувак умудрялся писать код типа такого:

                          var q = parseInt(incoming, 10);
                          console.log(q);
                          q = parseInt(q, 10);


                          Чтоб уж точно отпарсилось!
                +2
                Гхм… Я чего-то не понимаю? View.extend? в models?

                app.models.page = new (Backbone.View.extend({

                  0
                  Вернее было назвать app.classes. Просто нужно все это где-то хранить, не захламляя namespace. Так уж вышло, что пока писал назвал models, даже внимания не обратил.
                  0
                  Код у вас во вьюхах ужасный. Зачем использовать data-аттрибуты для хранения данных, которые вы храните в модели? Почему не используете delegateEvents для обработки событий вьюхи?

                  Посмотрите хороший скринкаст (peepcode).
                    0
                    С delegateEvents верно, я ошибся. Тут это примером служить не может.

                    Зачем использовать data-аттрибуты для хранения данных, которые вы храните в модели?

                    Именно потому, что delegateEvents не использовал. Как мне привязать модель к dom? Выбрал самый банальный способ
                      +1
                      Например, в view Note вместо .data('rowId') можно использовать this.model.get('id'), а вместо .find('input').val() можно было при создании инпута запомнить его и лишний раз не бегать по дереву.

                      Вообще вся прелесть Backbone.View в том, что объект привязан в определенному элементу DOM, при этом четко знаешь, что есть внутри, и через делегирование можно поставить нужные обработчики.

                        0
                        Да, вы несомненно правы. Чуть позже поправлю статью.
                    0
                    >Или же нужно банально связаться с сервером — а как это сделать? Все отсылают к Backbone.sync, а примеров почему-то никто не предоставляет.

                    Позволю себе высказать предположение, что примеров никто не предоставляет потому, что реализация функции Backbone.sync представляет какую либо сложность только для джуниор программистов. Для более менее опытного программиста реализовать бэкбон.синк задача на 15 минут.
                      0
                      Во-первых, я выразился ясно:
                      Считайте предыдущее предложение одной из основных причин написания данной заметки. Если вы с ним не согласны — дальше можно не читать.

                      Эта заметка именно для начинающих.

                      Во-вторых, вы не правы. Нельзя только по себе судить. Мне например это не показалось таким банальным. Вставить запрос легко, но где уверенность в том, что все будет нормально подхвачено? Нужно как минимум не раз проверить с разными объектами + нужно связать все это в кучу. Даже те паттерны, которые кажутся кому-то очевидными обычно где-то описаны
                        0
                        Зачем было писать Backbone.sync с нуля, а не использовать стандарный, с REST интерфейсом или допилить стандартную функцию под свои нужды(напр., прерывае предыдущего запроса)? Я плохо знаком с backbone, но с sync немного повозился, когда хотел отсылать не весь объект, а только измененные свойства, и на первый взгляд ваше решение гораздо менее гибкое.
                      0
                      Сравниваю с примером из туториала: здесь продвинутый парсинг из-за того, что нет интерфейса REST, и сервер выдаёт не JSON. В принципе, на многих фреймворках уже есть такие интерфейсы, запускаются очень легко.

                      Было бы очень ценный материал, если бы вы сделали редактор в броузере, как на translated.by/
                        0
                        Но за старания плюс поставил.
                          0
                          Сервер выдает JSON, просто REST не всегда удобен. Мне например нужно попутно отсылать состояние (просто строка) + сообщение об ошибке (если нужно). Если вы подскажите, как это сделать со стандартной реализацией — буду очень благодарен
                            +2
                            >> Мне например нужно попутно отсылать состояние (просто строка) + сообщение об ошибке (если нужно).

                            Если единоразово — в методы save и fetch можно передавать хэш с настройками для jQuery.ajax:

                            model.save({}, {data: {state: 1}});
                            model.fetch({data: {state: 2}});


                            Можно переопределить метод sync для конкретной модели или коллекции:

                            var someModelData = {data: {state: 1}};
                             
                            var SomeModel = Backbone.Model.extend({
                                …
                                sync: function(method, model, options) {
                                    var myOptions = options;
                             
                                    if (method == 'read') {
                                        myOptions = _.extend(someModelData, myOptions);
                                    }
                             
                                    Backbone.sync.call(this, method, model, myOptions);
                                }
                            });


                            Ну и наконец, можно расширить Backbone.sync, а не переписать полностью:

                            var someData = {data: {state: 1}};
                            var oldSync = Backbone.sync;
                            Backbone.sync = function(method, model, options) {
                                var myOptions = options;
                             
                                if (method == 'read') {
                                    myOptions = _.extend(someData, myOptions);
                                }
                             
                                return oldSync(method, model, myOptions);
                            };
                              0
                              Вы не совсем поняли. Передать данные на сервер элементарно. Как получить состояние с сервера? Оба ваших примера для этого не подходят
                                0
                                Ок, каюсь :)
                                К чему тогда относиться ваше «как это сделать со стандартной реализацией»?
                                  0
                                  Вот я и имею ввиду, как передать дополнительные данные при синхронизации? Например код ошибки. Потому и написал свой велосипед
                                    0
                                    Вы все правильно поняли. «Стандартная реализация» дополнительных действий при синхронизации с сервером — переопределить / переписать Backbone.sync.
                                    Только сделали вы это немного варварски.

                                    Более правильно — добавить нужную логику, а не полностью переписывать все, что не устраивает, например:
                                    var SomeModel = Backbone.Model.extend({
                                        …
                                        sync: function(method, model, options) {
                                            var myOptions = options;
                                     
                                            if (method == 'create') {
                                                myOptions.success = function(resp, status, xhr) {
                                                    if (resp.has_error) {
                                                        console.log('Error', resp);
                                                        return;
                                                    }
                                     
                                                    return options.success(resp, status, xhr);
                                                };
                                            }
                                     
                                            Backbone.sync.call(this, method, model, myOptions);
                                        }
                                    });
                          +1
                          Нашел время внимательней посмотреть на скрипт, не могу понять зачем так часто используются события, даже если вызываются они только внутри класса, например, «list:reload» в app.models.page?

                          Довольно сильно путают навзвания, кроме упомянутого app.models:
                          app.models.note = (Backbone.Model.extend({...
                          app.models.Note = (Backbone.View.extend({...

                          После создания классов идет внезапное создание объектов:
                          app.models.notes = new (Backbone.Collection.extend({...
                          app.models.page = new (Backbone.View.extend({...

                          Вся работа с backbone после document.ready, хотя описать классы можно заранее, а после загрузки страницы создать объект page.
                            0
                            Классы-то можно создать заранее, это не слишком важно в данном случае. А названия — вы действительно считаете, что это в данном примере важно?

                            События использовать очень удобно, иногда гораздо удобнее чем просто методы. У меня есть два рабочих проекта, где нужно, допустим, обновлять список. При обновлении списка нужно также подгрузить дополнительные данные + обновить некоторые модели и т.д. (ну логика такая). Через триггеры это выходит прозрачно, просто вешаем обработчики.
                              0
                              > Классы-то можно создать заранее, это не слишком важно в данном случае. А названия — вы действительно считаете, что это в данном примере важно?
                              Важно, важно не путать читателей и самого себя, что у вас получилось отлично.

                              > Через триггеры это выходит прозрачно, просто вешаем обработчики.
                              Только такой код слабо читабелен, в вашем случае внешний апи(события) выходит тесно связан со внутренним устройством.

                              Заметил еще большой косяк, при добавлении записи/изменении перезагружается весь список, что крайне не глупо для примера.

                              З. Ы. shuffle списка на стороне сервера совершенно непонятно к чему и сбивает с толку.
                            +1
                            Довольно интересный момент, в доке особо не уточняется, но если у модели дефолтное значение id = 0, то проверка isNew() будет давать false(в js null != 0), а при сохранении всегда будет посылаться update/PUT запрос, чтобы для новых элементов был create/POST запрос, надо не задавать значение id в defaults.

                            Основываясь на этом, можно оставить id = 0 и убрать из синка костыль:
                            // Сливаем методы дубовым способом
                            var method = (method=='update'||method=='create')?'save':method;

                            т. к. всегда будет посылаться update, заодно убрать явный вызов с аргументом model.url(method)

                            Вместо хардкорного запиливания в sync отдачи вложенного объекта:
                            options.success(data.rows?data.rows:data);
                            стоит использовать метод parse(resp, xhr) доступный для моделей и коллекций.

                            После чего вспомним, что Backbone.sync возвращает xhr, который можно использовать для обрыва предыдущего соединения, тип запроса, метод и тип запроса можно подставлять через опции…

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое