Это копия поста, который я написал, работая в Rollout.io — это действительно хороший израильский стартап, который предлагает отличные решения для управления флажками функций, используя самые передовые отраслевые практики. Оригинал статьи вы можете найти по этой ссылке, хотя она полностью такая же.

Вложенные модели BackboneJS

Мои отношения с Backbone.js сложные. Мы провели вместе дюжину часов, полных занимательных и глубоких разговоров (до поздней ночи). Я был действительно впечатлен, взволнован его простым и нежным восприятием мира, его мощными способностями и преимуществами для того, кто решил его придерживаться. Я видел красочное будущее, будущее, в котором мы вместе, рука об руку, создаем красивые, простые и многоразовые компоненты пользовательского интерфейса, выращиваем их в мире и счастье. В моих снах компоненты уважают друг друга и сотрудничают, обращая внимание на события других компонентов, внимательно слушая и реагируя уместно и уважительно. Это был сон.

Через какое-то время я стал обращать внимание, что не все так идеально. Основа требовательна, жадна до деталей. Это требовало всего моего внимания, пока мы вместе рендерили представления, нам было весело предотвращать утечки памяти, но это также было сложно, так как мы оба обвиняли друг друга в их создании. Однажды мне сказали, что я не очень понимаю это. Постепенно мое волнение начало снижаться, я обнаружил, что начинаю смотреть на другие интерфейсные фреймворки, они были сексуальнее, моложе, привлекательнее.

Однако после многих часов и строк кода, которые мы провели вместе, было тяжело расстаться просто так. У меня также были обязательства — я пообещал заботиться о взглядах и моделях, которые мы создали вместе, уважать все усилия и инвестиции, которые мы сделали до сих пор. Мы договорились внести некоторые изменения, использовали Marionette, чтобы сделать Backbone более привлекательным, добавили несколько плагинов для красоты и производительности.

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

Вложенные модели

Здесь, в Rollout.io, у нас была отчаянная потребность во вложенной модели — структуры данных, с которыми мы работаем, имеют сильную «вложенную» ориентацию. Причин тому много, вот некоторые из них, которые я считаю разумными:

  • Концепция хранения документов в MongoDB подразумевает использование аналогичной концепции на стороне внешнего интерфейса — т. е. вы получаете один документ, который может иметь вложенные вложенные документы.
  • Для получения всех ваших данных с вложенной структурой, похожей на документ, требуется один сетевой запрос, для извлечения вложенных документов из API потребуется несколько сетевых запросов, вы также должны настроить и внедрить конечные точки API для каждого такого запроса.
  • Разбиение документа на вложенные документы (и сборка их обратно) — утомительный и трудоемкий процесс.
  • Концептуальная простота вложенной модели очевидна — хотя технически реализовать и использовать вложенную модель может быть сложнее, может быть проще понять структуру уровня данных приложения при работе с интуитивно понятными терминами, которые должным образом отражены в виде вложенных моделей.

Существует довольно много дискуссий о концепции вложенной или глубокой модели, включая комментарий Джереми Ашкенаса из BackboneJS на GitHub и в разделе официального FAQ по BackboneJS.

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

Чтобы продемонстрировать проблему, давайте рассмотрим небольшой пример, который подчеркивает проблемы, которые я упомянул.

Почувствуй боль

Представьте, что у вас есть схема человека в хранилище MongoDB. С каждым человеком может быть связано 0 или более телефонных номеров. Каждый номер телефона имеет метку, например. домашний, рабочий, мобильный и реальный номер. Документ человека будет представлен как:

var data = {
   name: 'Bob',
   lastName: 'Flop',
   email: '[email protected]',
   phones: [
       {
           label: 'Home',
           number: 101
       },
       {
           label: 'Work',
           number: 102
       }
   ]
};

Представление документа в виде модели Backbone будет иметь следующие ключи своих атрибутов:

var personModel = new Backbone.Model(data);
_.keys(personModel.attributes)
> ["name", "lastname", "phones"]

Вы не можете получить доступ к деталям объекта телефона напрямую, используя `Backbone.Model.get`:

personModel.get('phones[0].label')
> undefined

Чтобы получить доступ к вложенным документам phone, необходимо выполнить итерацию по массиву телефонов:

var phones = personModel.get('phones');
phones.map(function(phone) {
  // do stuff with phone
});

Экземпляр объекта phone не является моделью Backbone — вы не можете применять общие шаблоны Backbone (такие как `get`, `set`, `validation`, нет распространения событий). Чтобы использовать объект, нам нужно «обновить» его, создав новую общую модель и обернув в нее объект телефона:

var phones = personModel.get('phones');
var phoneModels = phones.map(function(phone) {
var phoneModel = new Backbone.Model(phone);
  return phoneModel;
});
console.log(phoneModels);
> [Backbone.Model, Backbone.Model]

Однако вновь созданные модели телефонов никоим образом не связаны с исходной моделью человека. Что означает, что:

  • Нет распространения событий по модели, изменение атрибута модели телефона не вызывает событие изменения для человека
personModel.on('change', function (e) {
  console.log('person has changed');
});
phoneModels[0].set('number',999);
// no change in person
  • Проверка модели человека не выполняется при изменении модели телефона, т. е. вам необходимо реализовать различную логику проверки для телефона и модели человека, чтобы процесс проверки происходил, как только изменяется какое-либо свойство модели телефона.
  • В конце концов нам нужно будет отправить измененную модель человека на серверную часть, что потребует обработки всех изменений в подмоделях телефонов и отправки единой модели на наши серверные серверы.

Блеск вложенной модели

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

Это то, что позволяет нам сделать вложенная модель. Основное преимущество заключается в том, что все вложенные атрибуты доступны напрямую с использованием собственного синтаксиса JS.

Доступны различные реализации вложенных моделей, вот некоторые из них:

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

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

  • Получите доступ к вложенным данным без усилий, используя синтаксис JS:
var nestedPersonModel = new Backbone.NestedModel(data)
nestedPersonModel.get('phones[0].label')
> "Work"
  • События обрабатываются правильно:
nestedPersonModel.on('change:phones',
  function () {
    console.log ("Person has changed");
});
nestedPersonModel.set('phones[0].label', 'Office');
> Person has changed
  • Проверка работает, как и ожидалось:
// define validation - disallow duplicated phone labels
var NestedPersonClass = Backbone.NestedModel.extend({
  validate: function (attrs, options) {
    var uniqueLabels = _.uniq(_.pluck(attrs.phones, 'label'));
    if (uniqueLabels.length !== attrs.phones.length) {
      return 'Duplicated phone labels are not allowed';
    }  
  }
});
var nestedPerson = new NestedPersonClass(data);
nestedPerson.set('phones[0].label', 'Work', {validate: true} )
> false

console.log(nestedPerson.validationError);
> "Duplicated phone labels are not allowed"

Как мы видели, большинство проблем, с которыми мы сталкивались при использовании оригинальных плоских моделей, решены. Теперь мы можем работать с единой моделью, которая имеет идентичное представление в хранилище и во внешнем интерфейсе.

Возиться с вложенными моделями

Несмотря на все преимущества вложенных моделей, они все же не совершенны. Проблема в том, что, используя вложенные структуры данных вместе с Backbone (и с расширениями Marionette, такими как CollectionView, Layout и т. д.), мы нарушаем рабочий процесс и простоту представлений Backbone.

Представления (и представления Коллекция/Макет) предназначены для работы с простыми плоскими моделями. Наличие единой, но сложной вложенной модели и передача ее представлениям требует указания того, какая часть «большой» модели доступна для конкретного представления.

Мы использовали эту технику, передавая вложенную модель между макетом и его подпредставлениями, после достижения определенного уровня сложности мы начали путаться с определением путей для ItemViews, которые заставляют всю вложенную модель правильно определять данные. чанк, к которому представление должно иметь доступ:

Сложность обслуживания правильной модели для ItemViews находится в сфере действия Layout View, и часто не так просто изменить текущий подэлемент (технически, изменение индекса `i` или `j`). Реализация ItemViews и Layout не была интуитивно понятной и сложной для понимания, поскольку каждое изменение доступа во вложенной модели требовало пересчета путей (значений индекса) и повторного рендеринга зависимой части связанных представлений.

Более того, большую боль, которую мы испытали, было отсутствие возможности создавать нативные коллекции, которые могли бы правильно использоваться CollectionViews. В качестве обходного пути мы начали создавать «фиктивные» коллекции, которые «привязаны» к исходной вложенной модели и служат прокси для удобного использования в представлениях на основе коллекций.

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

Упрощенная версия коллекции proxy выглядит так:

var SubCollection = Backbone.Collection.extend({
  // sync data to the original nested model when needed
  flushData: function(model, path) {
    var arrayPlaceholder = [];
    this.forEach(function(model) {
      return arrayPlaceholder.push(model.toJSON());
    });
    model.set(path, arrayPlaceholder);
    return model.save();
  };

  // bind the collection to the specific path of a nested model
  bindToModel: function(model, path) {
    var dataArray, item, i, len;
    dataArray = model.get(path);
    this.on('change', function() {
      this.flushData(model, path);
    });

    this.on('remove', function() {
      this.flushData(model, path);
    });

    for (i = 0, len = dataArray.length; i < len; i++) {
      item = dataArray[i];
      // create sub-models from the array elements of the nested model
      this.add(item);
    }
  };
});

Подмодели утопия

Итак, каким был бы идеальный подход для работы со сложными моделями данных? Чтобы свободно использовать все преимущества экосистемы Backbone-Marionette, предназначенной для работы с простыми и плоскими моделями, не разбивая вручную структуру данных на более мелкие фрагменты, не связанные между собой, было бы идеально:

  1. Иметь возможность создавать новую подмодель из любого пути «большой» вложенной модели. Вновь созданная подмодель должна вести себя как отдельная модель Backbone, но она также должна быть тесно связана со своим «родителем» с точки зрения целостности данных, распространения событий, проверки и т. д.
  2. Уметь создавать коллекцию подмоделей из массивоподобных элементов вложенной модели, при этом все привязки, перечисленные выше, работают правильно.

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

Наличие такой мощной функции значительно расширило бы возможности разработчиков по работе со сложными структурами данных, созданию подмоделей и подколлекций по запросу, а также их естественному использованию с представлениями и макетами Backbone/Marionette.