Pull to refresh

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

JavaScript *
Sandbox
В данной заметке речь пойдет об использовании 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
Tags:
Hubs:
Total votes 32: ↑27 and ↓5 +22
Views 44K
Comments Comments 37