Генерация или шаблонизация

Генерация

Для рендеринга модели в представление проще всего использовать генерацию средствами самого JS или сторонних библиотек. Рассмотрим пример:

ClientWidget.prototype.render = function(){
  var div = document.createElement('div');
  div.appendChild(document.createTextNode(this.model.name));

  return div;
};

Этот простой рендер создает элемент тега div, вставляет в него имя клиента из данных модели и возвращает результат, который может быть добавлен на страницу. Конечно, вместо нативных методов JS, для генерации представления можно использовать сторонние библиотеки, такие как JQuery.

Данное решение имеет один важный недостаток - логика генерации представления достаточно сложна, чтобы в ней запутаться. Эта проблема сродни “лапше из JQuery кода”, чем сложнее виджет, тем запутаннее генератор.

Шаблонизация

Решением указанной проблемы служит альтернативное решение, использующее шаблонизацию. В этом случае генерация заменяется на вызов шаблонизатора с передачей ему модели, а тот, в свою очередь, заполняет шаблон данными и возвращает результат:

function ClientWidget(model){
  this.model = model;

  // Шаблон, созданный с помощью Underscore
  this.template = _.template('<div><%= name %></div>');
};

ClientWidget.prototype.render = function(){
  return this.template(this.model);
};

Решение заметно “похудело”, за счет вынесения представления в шаблон. Шаблонизатор (в данном случае Underscore) просто заменяет специальные метки в строке одноименными данными из модели и возвращает готовую DOM Node.

Данное решение как и предыдущее имеет серьезный недостаток - не смотря на то, что шаблон (<div><%= name %></div>) можно вынести в отдельный HTML файл и запрашивать асинхронно, для систем, использующих генерацию страниц на стороне сервера потребуется поддерживать два, обычно разных, шаблонизатора. Другими словами вам будет необходимо реализовать два шаблона виджета, один на стороне сервера (к примеру на Smarty), а другой на стороне клиента (к примеру на Underscore), полностью повторив их, но с заменой маркеров.

Комбинирование решений

Довольно часто не обойтись одним из решений и приходится их комбинировать. К примеру, для реализации представления списка продуктов в конкретном заказе потребуется следующий код:

function ProductWidget(model){
  this.model = model;

  this.template = _.template('<div><%= name %></div>');
}

ProductWidget.prototype.render = function(){
  return this.template(this.model);
};

function OrderWidget(rows){
  this.rows = rows || [];
}

OrderWidget.prototype.render = function(){
  var ul = $('<ul>');
  for(var i in this.rows){
    var productWidget = new ProductWidget(this.rows[i]);
    ul.append(productWidget.render());
  }

  return ul;
};

В данном решении используется как шаблонизатор, для представления каждого конкретного продукта, так и генератор, для представления списка всех продуктов. Конечно, современные шаблонизаторы, такие как React, позволяют встраивать циклический обход модели прямо в шаблон, что позволит исключить генерацию на уровне виджета, но это решение требует вынести часть логики в шаблон, что многим (в том числе и мне) не нравится.

Обработка результата рендеринга

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

Во всех предыдущих примерах мы использовали первое решение, но часто можно встретить их комбинацию, а выбор того или иного решения зависит от конфигурации виджета. Рассмотрим пример реализации:

function ClientWidget(model, el){
  this.model = model;
  this.el = el;

  // Шаблоны и другие свойства виджета
}

ClientWidget.prototype.render = function(){
  var view = this.template(this.model);

  if(this.el){
    $(this.el).empty().append(view);
  }

  return view;
};

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

Реакция на изменения модели

Об изменении состояния модели мы поговорим в следующей статье, но сейчас нам необходимо затронуть вопрос синхронизации представления с моделью при ее изменении. Рассмотрим очередной пример:

function ClientWidget(model, el){
  this.model = model;
  this.el = el;

  this.model.on('change', this.render);
  // Шаблоны и другие свойства виджета
}

Для этого решения, объект модели должен выбрасывать событие change при любом изменении своего состояния. Виджет использует это событие для вызова метода render, который выполнит повторный рендеринг представления и обновит страницу.

Такое решение может показаться довольно ресурсоемким, так как потребует повторного рендеринга представления даже при самых незначительных изменениях модели. Некоторые системы, такие как React, решают эту проблему с помощью использования Виртуального DOM дерева, другие (на пример Angular), используют Двустороннее связывание, но целью везде одна - ограничить рендеринг только теми частями представления, которые реально затронуло изменение модели.