Пример использования Backbone: админка хранилища JS логов. Часть 3

Содержание

 

В этой, заключительной части статьи мы займемся реализацией представлений для созданных ранее моделей и коллекций.

Представления

Список URL

Для отображения списка URL мы будем использовать два вложенных представления: одно — для всего списка; другое — для каждого элемента списка, т.е. отдельного URL.
Начнем с представления элемента списка. Мы связываем его с новым DOM элементом ‘li’, содержимое которого задается в шаблоне.

Шаблон содержимого элемента списка URL:

<script type="text/template" id="url-template">
  <a href="#" class="item url-item"><%= title %></a>
  <span class="hovertools">
    <a href="<%= id %>" class="link" title="Go to this web app">
      <span class="glyphicon glyphicon-link"></span>
    </a>
    <a href="#remove" class="remove" title="Remove this web app">
      <span class="glyphicon glyphicon-remove"></span>
    </a>
  </span>
</script>

По наведению на элемент списка появляется мини-панель инструментов, содержащая ссылку на соответствующее веб-приложение и кнопку его удаления.
По нажатию на элемент списка (a.url-item) происходит выбор этого элемента, по нажатию на кнопку удаления (a.remove) — запрос на удаление соответствующего веб-приложения из списка.

Класс представления элемента списка URL:

var UrlView = Backbone.View.extend({
  tagName: 'li',
  template: _.template( $('#url-template').html() ),
  events: {
    'click a.url-item' : 'select',
    'click a.remove' : 'destroy'
  },
  initialize: function() {
    this.listenTo(this.model, 'change', this.render);
    this.listenTo(this.model, 'destroy', function() {
      // удаляем представление:
      this.remove();
    });
    this.listenTo(this.model, 'remove', function() {
      // освобождаем память - очищаем содержимое удаленной из коллекции модели:
      this.model.clear({silent:true});
      // удаляем представление:
      this.remove();
    });
  },
  render: function() {
    this.$el.html( this.template( this.model.toJSON() ) );
    if (this.model.get('selected'))
      this.$el.addClass('active');
    else
      this.$el.removeClass('active');
    return this;
  },
  select: function(e) {
    e.preventDefault();
    // устанавливаем значение выбора в модели urlSelection,
    // передаем атрибут id связанной модели:
    urlSelection.select(this.model.get('id'), true);
  },
  destroy: function(e) {
    e.preventDefault();
    // удаляем URL (веб-приложение) с сервера:
    this.model.destroy();
  }
});

В методе initialize мы регистрируем свои обработчики для событий destroy и remove связанной с представлением модели. Несмотря на похожие названия, эти события имеют разное назначение и не связаны друг с другом. Событие destroy возникает при удалении модели с сервера. В нашем случае это происходит, когда мы нажимаем кнопку удаления. При этом событие remove у модели не возникает (т.к. модели уже нет). Второе событие возникает, либо при явном вызове метода remove у коллекции, либо при неявном удалении модели из коллекции. В нашем случае это происходит неявно, после синхронизации всей коллекции с сервером, в том случае, если состав коллекции изменился. Здесь важно понимать, что событие remove коллекции не влечет за собой событие destroy модели, т.е. удаление модели из коллекции не означает автоматическое удаление самой модели. Поэтому нам следует самим об этом позаботиться. Вызывать метод destroy модели в этом случае нет смысла, т.к. модели уже нет на сервере. Достаточно просто освободить память, занимаемую моделью. Для этого мы используем метод clear.

Переходим к представлению списка URL. Мы связываем его с существующим DOM элементом, заданным посредством CSS-селектора в свойстве el.

Класс и объект представления списка URL:

var UrlListView = Backbone.View.extend({
  el: '#url-list',
  initialize: function() {
    this.listenTo(this.collection, 'add', this.addOne);
    this.listenTo(this.collection, 'all', this.render);
    // получаем данные коллекции с сервера:
    this.collection.fetch();
  },
  render: function() {
    if (this.collection.length)
      // если коллекция не пустая, показываем DOM элемент представления:
      this.$el.show();
    else
      // иначе скрываем его:
      this.$el.hide();
    return this;
  },
  // добавление модели в коллекцию
  addOne: function(item) {
    // item - добавляемая в коллекцию модель
    if (!('id' in item.attributes)) return;
    // создаем для данной модели экземпляр представления:
    var urlView = new UrlView({model:item});
    // выводим его DOM-содержимое и добавляем его DOM элемент
    // внутрь списка URL:
    this.$el.append(urlView.render().el);
  }
});
var urlListView = new UrlListView({collection: urlList});

В методе initialize мы заполняем коллекцию Список URL начальными данными, взятыми из модели конфигурации, используя для этого вызов метода fetch, а также регистрируем обработчики для событий add и all коллекции связанной с данным представлением.
Событие add происходит при добавлении новой модели в коллекцию. При возникновении этого события мы создаем представление для новой модели и вставляем соответствующий DOM элемент в конец списка URL.
Событие all мы используем для вызова метода render представления при каждом изменении сооветствующей коллекции. Событие all включает в себя все события, происходящие с коллекцией. На самом деле нам конечно не нужно слушать все события, но поскольку метод render почти ничего не делает (только изменяет видимость элемента-контейнера), то его лишними вызовами можно пренебречь.

Список лог-файлов

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

Представление элемента списка связываем с новым DOM элементом ‘li’, содержимое которого задается в шаблоне.

Шаблон содержимого элемента списка лог-файлов:

<script type="text/template" id="file-template">
  <a href="#" class="item file-item" title="IP: <%= ip %>">
    <%= time %>
  </a>
  <span class="hovertools">
    <a href="#remove" class="remove" title="Remove this log file">
      <span class="glyphicon glyphicon-remove"></span>
    </a>
  </span>
</script>

По наведению на элемент списка появляется мини-панель инструментов, содержащая кнопку удаления соответствующего лог-файла.
По нажатию на элемент списка (a.file-item) происходит выбор этого элемента, по нажатию на кнопку удаления (a.remove) — запрос на удаление соответствуюго лог-файла.

Класс представления элемента списка лог-файлов:

var FileView = Backbone.View.extend({
  tagName: 'li',
  template: _.template( $('#file-template').html() ),
  events: {
    'click a.file-item' : 'select',
    'click a.remove' : 'destroy'
  },
  initialize: function() {
    this.listenTo(this.model, 'change', this.render);
    this.listenTo(this.model, 'destroy', function() {
      // удаляем представление:
      this.remove();
    });
    this.listenTo(this.model, 'remove', function() {
      // освобождаем память - очищаем содержимое удаленной из коллекции модели:
      this.model.clear({silent:true});
      this.remove();
    });
  },
  render: function() {
    if (this.model.get('visible'))
      this.$el.html( this.template( this.model.toJSON() ) ).show();
    else
      this.$el.hide();
    if (this.model.get('selected'))
      this.$el.addClass('active');
    else
      this.$el.removeClass('active');
    return this;
  },
  select: function(e) {
    e.preventDefault();
    fileSelection.select(this.model.get('id'));
  },
  destroy: function(e) {
    e.preventDefault();
    this.model.destroy().always(function() {
      fileList.update();
    });
  }
});

Представление списка лог-файлов мы связываем с существующим DOM элементом, заданным CSS-селектором в свойстве el.

Класс и объект представления списка лог-файлов:

var FileListView = Backbone.View.extend({
  el: '#file-list',
  initialize: function() {
    this.listenTo(this.collection, 'add', this.addOne);
    this.listenTo(this.collection, 'reset', this.addAll);
    this.listenTo(this.collection, 'all', this.render);
    this.collection.reset(this.collection.parse(deftFilelist));
    this.collection.update();
  },
  render: function() {
    if (this.collection.length)
      this.$el.show();
    else
      this.$el.hide();
    return this;
  },
  // добавление модели в коллекцию
  addOne: function(item) {
    if (!('id' in item.attributes)) return;
    var fileView = new FileView({model:item});
    this.$el.append(fileView.render().el);
  },
  // добавление моделей в коллекцию
  addAll: function() {
    this.collection.each(this.addOne, this);
  }
});
var fileListView = new FileListView({collection: fileList});

В методе initialize мы инициализируем коллекцию Список лог-файлов начальными данными, взятыми из переменной deftFilelist, которая инициализируется во время загрузки страницы и передается скрипту из веб-страницы приложения. Кроме того, здесь мы регистрируем обработчики для событий add, reset и all коллекции связанной с данным представлением.

Содержимое выбранного лог-файла

Представление содержимого выбранного лог-файла связывается через свойство el с существующим DOM элементом #file-content, содержимое которого задается в шаблоне.

Шаблон Содержимого выбранного лог-файла:

<script type="text/template" id="content-template">
  <dl class="dl-horizontal">
    <dt>Log File:</dt>
    <dd><a href="<%= path %>" title="Download link"><%= id %></a></dd>
    <dt>Web App URL:</dt>
    <dd><a href="<%= url %>"><%= url %></a></dd>
    <dt>Client&#039;s IP:</dt>
    <dd><%= ip %></dd>
    <dt>UserAgent:</dt>
    <dd><%= useragent %></dd>
    <dt>Session started:</dt>
    <dd><%= time %></dd>
  </dl>
  <pre class="pre-scrollable"><%= content %></pre>
</script>

Класс и объект представления Содержимого выбранного лог-файла:

var FileContentView = Backbone.View.extend({
  el: '#file-content',
  template: _.template( $('#content-template').html() ),
  initialize: function() {
    this.listenTo(this.model, 'change', this.render);
  },
  render: function() {
    if (this.model.source &&
        this.model.get('visible') && this.model.get('content')) {
      // если есть выбранный лог-файл, который виден
      // в текущих фильтрах по URL и IP,
      // то вывести его содержимое и показать DOM элемент представления:
      this.$el.html( this.template( this.model.toJSON() ) ).show();
    }
    else
      // иначе скрыть DOM элемент представления:
      this.$el.hide();
    return this;
  }
});
var fileContentView = new FileContentView({model:fileContent});

Конфигурация логгера

Конфигурация логгера будет иметь представление в виде модального окна — компонента Bootstrap. Представление связывается через свойство el с существующим DOM элементом #config, содержимое которого задается в шаблоне.

HTML код модального окна:

<div id="config" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="configLabel" aria-hidden="true">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal">
          <span aria-hidden="true">&times;</span><span class="sr-only">Close</span>
        </button>
        <h4 class="modal-title" id="configLabel">Configuration</h4>
      </div>
      <div class="modal-body">
        <form id="configForm" role="form" onsubmit="return false"></form>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary save">Save changes</button>
      </div>
    </div>
  </div>
</div>

Шаблон Конфигурации логгера:

<script type="text/template" id="config-template">
  <div class="form-group">
    <label>Log buffer size, bytes</label>
    <input type="text" class="form-control" name="buff_size" value="<%= buff_size %>">
  </div>
  <div class="form-group">
    <label>Log flush interval, secs</label>
    <input type="text" class="form-control" name="interval" value="<%= interval %>">
  </div>
  <div class="form-group">
    <label>Background flush interval, secs</label>
    <input type="text" class="form-control" name="interval_bk" value="<%= interval_bk %>">
  </div>
  <div class="form-group">
    <label>Log session expiration time, hours</label>
    <input type="text" class="form-control" name="expire" value="<%= expire %>">
  </div>
  <div class="form-group">
    <label>Limit for requests per flush interval (0 - no limit)</label>
    <input type="text" class="form-control" name="requests_limit" value="<%= requests_limit %>">
  </div>
  <div class="form-group">
    <label>Include timeshift into each log record</label>
    <select class="form-control" name="log_timeshifts">
      <option value="0" <%= log_timeshifts? '' : 'selected' %>>No</option>
      <option value="1" <%= log_timeshifts? 'selected' : '' %>>Yes</option>
    </select>
  </div>
  <div class="form-group">
    <label>Substitute console object with logflush</label>
    <select class="form-control" name="subst_console">
      <option value="0" <%= subst_console? '' : 'selected' %>>No</option>
      <option value="1" <%= subst_console? 'selected' : '' %>>Yes</option>
    </select>
  </div>
  <div class="form-group">
    <label>Minify result JS script</label>
    <select class="form-control" name="minify">
      <option value="0" <%= minify? '' : 'selected' %>>No</option>
      <option value="1" <%= minify? 'selected' : '' %>>Yes</option>
    </select>
  </div>
</script>

Модальное окно появляется средствами Twitter Bootstrap, без участия Backbone; оно всплывает при нажатии на кнопку вызова конфигурации в верхней панели.
При нажатии на кнопку сохранения (button.save) в модальном окне происходит сохранение введенных/измененных данных конфигурации на сервер.

Класс представления Конфигурации логгера:

var CfgView = Backbone.View.extend({
  el: '#config',
  template: _.template( $('#config-template').html() ),
  events: {
    'click button.save' : 'saveForm'
  },
  initialize: function() {
    var self = this;
    this.$el.on('show.bs.modal', function () {
      self.render();
    });
  },
  render: function() {
    this.$('form').html( this.template( this.model.toJSON() ) );
    return this;
  },
  // сохранение данных конфигурации на сервер:
  saveForm: function(e) {
    e.preventDefault();
    var attrs = {};
    // преобразуем элементы ввода формы в массив имен и значений
    // и собираем из него объект атрибутов для модели конфигурации:
    _.each(this.$('form').serializeArray(), function(obj) {
      attrs[obj.name] = parseFloat(obj.value);
    });
    if (Object.keys(attrs).length)
      this.model.save(attrs, {wait: true});
    // скрываем модальное окно:
    this.$el.modal('hide');
  }
});
var cfgView = new CfgView({model:cfg});

Панель дополнительных элементов управления

Как упоминалось в первой части, панель дополнительных элементов управления содержит поле добавления/регистрации нового веб-приложения (URL) и кнопки обновления списка лог-файлов, установки/сброса фильтра лог-файлов по IP, и удаления всех лог-файлов видимых в текущем фильтре. Кроме того, там же находится кнопка вызова модального окна конфигурации логгера.

Представление панели будет связано с моделью Фильтра лог-файлов по IP, но оно будет также обрабатывать нажатия кнопок обновления и удаления. Свойство el представления будет связано с существующим DOM элементом #controls.

Класс представления панели дополнительных элементов управления:

var ControlsView = Backbone.View.extend({
  el: '#controls',
  events: {
    'keypress input' : 'create',
    'click button.filter' : 'toggle',
    'click button.refresh' : 'refresh',
    'click button.remove' : 'destroy'
  },
  initialize: function() {
    this.listenTo(this.model, 'change', this.render);
    this.btn = this.$('button.filter');
    this.$('input').val('');
  },
  render: function() {
    var v = this.model.get('value');
    if (v)
      // если значение фильтра не пустое,
      // присваиваем css-класс active кнопке фильтра
      // и выводим значение как текст внутри кнопки:
      this.btn.addClass('active').children('.value').html(': '+v);
    else
      // иначе удаляем css-класс active и очищаем текст внутри кнопки:
      this.btn.removeClass('active').children('.value').html('');
    return this;
  },
  // добавление/регистрация нового URL:
  create: function(e) {
    if (e.which != 13) return;
    var el = e.target;
    var v = $(el).val();
    if (!v) return;
    $(el).val('').blur();
    urlList.create(cfg.buildUrlAttrs(v));
  },
  // обновление списка лог-файлов:
  refresh: function(e) {
    e.preventDefault();
    $(e.target).blur();
    fileList.update();
  },
  // установка/сброс фильтра лог-файлов по IP:
  toggle: function(e) {
    e.preventDefault();
    $(e.target).blur();
    var v = this.model.get('value');
    if (v)
      this.model.reset();
    else if (fileContent.source)
      this.model.setVal(fileContent.source.get('ip'));
  },
  // удаление всех лог-файлов видимых в текущем фильтре:
  destroy: function(e) {
    e.preventDefault();
    $(e.target).blur();
    fileList.destroyVisible();
  }
});
var controlsView = new ControlsView({model:fileFilter});

 

На этом реализация файлового менеджера завершена. Теперь остается только собрать вместе весь написанный ранее код. Либо взять готовые исходники приложения отсюда.