среда, 13 июня 2012 г.

Практический пример использования 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($('').val(data.text));
    $(this.el).append($('').click(function(){
      app.models.page.trigger('post:save', {
        'id': $(this).closest('li').data('rowId'),
        'text': $(this).closest('li').find('input').val()
      });
    }));
    $(this.el).append($('').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($('
  • '
    ).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/

    Источник

    Комментариев нет: