Как обрабатывать инициализацию и рендеринг подпунктов в Backbone.js?

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


Сценарий первый:

Инициализировать дочерние элементы в родительской функции инициализации. Таким образом, не все застревает в рендере, так что на рендеринг меньше блокируется.

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

    this.$el.html(this.template());

    this.child.render().appendTo(this.$('.container-placeholder');
}

Проблемы:

  • Самая большая проблема заключается в том, что вызов рендеринга родителя второй раз удалит все привязки событий childs. (Это связано с тем, как работает jQuery $.html().) Это можно было бы смягчить, вызвав вместо него this.child.delegateEvents().render().appendTo(this.$el);, но затем первый и наиболее часто делающий ненужную работу.

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


Сценарий второй:

Инициализируйте дочерние элементы родительского initialize(), но вместо добавления используйте setElement().delegateEvents(), чтобы установить дочерний элемент в шаблон родителей.

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

    this.$el.html(this.template());

    this.child.setElement(this.$('.placeholder-element')).delegateEvents().render();
}

Проблемы:

  • Это делает теперь delegateEvents() необходимым, что является небольшим отрицательным по отношению к нему только при последующих вызовах в первом сценарии.

Сценарий три:

Инициализируйте дочерние элементы в родительском методе render().

initialize : function () {

    //parent init stuff
},

render : function () {

    this.$el.html(this.template());

    this.child = new Child();

    this.child.appendTo($.('.container-placeholder').render();
}

Проблемы:

  • Это означает, что теперь функция рендеринга также должна быть привязана ко всей логике инициализации.

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


Действительно любопытно, чтобы ваши парни взяли на себя это. Какой сценарий вы бы использовали? или есть четвертый магический, который решает все эти проблемы?

Вы когда-нибудь отслеживали состояние рендеринга для представления? Скажите renderedBefore флаг? Кажется действительно сумасшедшим.

Ответ 1

Это отличный вопрос. Магистраль велика из-за отсутствия предположений, которые она делает, но это означает, что вы должны (решать, как) реализовать такие вещи, как это самостоятельно. Изучив мои собственные вещи, я обнаружил, что я (вроде) использую сочетание сценария 1 и сценария 2. Я не думаю, что существует 4-й магический сценарий, потому что, достаточно просто, все, что вы делаете в сценариях 1 и 2, должно быть сделал.

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

Page Breakdown

Скажем, что HTML после визуализации выглядит примерно так:

<div id="parent">
    <div id="name">Person: Kevin Peel</div>
    <div id="info">
        First name: <span class="first_name">Kevin</span><br />
        Last name: <span class="last_name">Peel</span><br />
    </div>
    <div>Phone Numbers:</div>
    <div id="phone_numbers">
        <div>#1: 123-456-7890</div>
        <div>#2: 456-789-0123</div>
    </div>
</div>

Надеюсь, это довольно очевидно, как HTML соответствует диаграмме.

ParentView содержит 2 дочерних представления, InfoView и PhoneListView, а также несколько дополнительных div, один из которых, #name, должен быть установлен в какой-то момент. PhoneListView содержит собственные дочерние представления, массив записей PhoneView.

Так на ваш реальный вопрос. Я обрабатываю инициализацию и визуализацию по-разному в зависимости от типа представления. Я разбиваю свои взгляды на два типа: Parent виды и Child виды.

Разница между ними проста, Parent представления содержат дочерние представления, а представления Child - нет. Таким образом, в моем примере ParentView и PhoneListView являются Parent представлениями, а InfoView и PhoneView являются Child представлениями.

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

В более детальной форме для представлений Parent мне нравятся мои функции initialize, чтобы сделать несколько вещей:

  • Инициализировать мой собственный вид
  • Измените мой собственный вид
  • Создайте и инициализируйте любые дочерние представления.
  • Назначить каждому дочернему элементу представление элемента в моем представлении (например, InfoView будет назначено #info).

Шаг 1 довольно понятен.

Шаг 2, рендеринг, делается так, чтобы все элементы, на которые полагается дочерние элементы, уже существуют, прежде чем я попытаюсь их назначить. Делая это, я знаю, что все дочерние events будут правильно установлены, и я могу повторно отображать их блоки столько раз, сколько хочу, не беспокоясь о необходимости делегировать что-либо. На самом деле, я не render всех дочерних представлений, я разрешаю им делать это в своем собственном initialization.

Шаги 3 и 4 фактически обрабатываются одновременно с передачей el во время создания дочернего представления. Мне нравится передавать элемент здесь, поскольку я считаю, что родитель должен определить, где в его собственном представлении дочерний объект может помещать его содержимое.

Для рендеринга я стараюсь, чтобы это было довольно просто для представлений Parent. Я хочу, чтобы функция render выполняла не что иное, как визуализацию родительского представления. Нет делегирования событий, без рендеринга дочерних просмотров, ничего. Просто простой рендер.

Иногда это не всегда работает. Например, в приведенном выше примере элемент #name необходимо обновить при каждом изменении имени в модели. Тем не менее, этот блок является частью шаблона ParentView и не обрабатывается специальным представлением Child, поэтому я обхожу это. Я создам некоторую функцию subRender, которая заменяет только содержимое элемента #name и не должна удалять весь элемент #parent. Это может показаться взломом, но я действительно нашел, что он работает лучше, чем беспокоиться о повторном рендеринге всей DOM и повторной привязке элементов и т.д. Если бы я действительно хотел очистить его, я бы создал новый Child вид (похожий на InfoView), который обрабатывал блок #name.

Теперь для представлений Child initialization очень похож на представления Parent, без создания каких-либо дальнейших представлений Child. Итак:

  • Инициализировать мой просмотр
  • Настройка привязывает прослушивание любых изменений к модели, которая меня волнует.
  • Измените мой взгляд

Child просмотр рендеринга также очень прост, просто визуализируйте и установите содержимое моего el. Опять же, не возиться с делегацией или что-то в этом роде.

Вот пример кода, на который может выглядеть мой ParentView:

var ParentView = Backbone.View.extend({
    el: "#parent",
    initialize: function() {
        // Step 1, (init) I want to know anytime the name changes
        this.model.bind("change:first_name", this.subRender, this);
        this.model.bind("change:last_name", this.subRender, this);

        // Step 2, render my own view
        this.render();

        // Step 3/4, create the children and assign elements
        this.infoView = new InfoView({el: "#info", model: this.model});
        this.phoneListView = new PhoneListView({el: "#phone_numbers", model: this.model});
    },
    render: function() {
        // Render my template
        this.$el.html(this.template());

        // Render the name
        this.subRender();
    },
    subRender: function() {
        // Set our name block and only our name block
        $("#name").html("Person: " + this.model.first_name + " " + this.model.last_name);
    }
});

Здесь вы можете увидеть мою реализацию subRender. Имея изменения, привязанные к subRender вместо render, мне не нужно беспокоиться о взломе и перестройке всего блока.

Вот пример кода для блока InfoView:

var InfoView = Backbone.View.extend({
    initialize: function() {
        // I want to re-render on changes
        this.model.bind("change", this.render, this);

        // Render
        this.render();
    },
    render: function() {
        // Just render my template
        this.$el.html(this.template());
    }
});

Связи - важная часть здесь. Связываясь с моей моделью, мне никогда не придется беспокоиться о том, чтобы вручную вызвать render сам. Если модель изменится, этот блок будет повторно отображать себя, не затрагивая никаких других представлений.

PhoneListView будет похож на ParentView, вам понадобится немного больше логики для ваших функций initialization и render для обработки коллекций. Как вы справляетесь с коллекцией, это зависит от вас, но вам, по крайней мере, нужно будет слушать события коллекции и решать, как вы хотите отображать (добавлять/удалять или просто повторно отображать весь блок). Мне лично нравится добавлять новые представления и удалять старые, а не повторно отображать весь вид.

PhoneView будет почти идентичен InfoView, только слушая изменения модели, о которых он заботится.

Надеюсь, это немного помогло, пожалуйста, дайте мне знать, если что-то вводит в заблуждение или недостаточно подробно описано.

Ответ 2

Я не уверен, что это прямо ответит на ваш вопрос, но я думаю, что это актуально:

http://lostechies.com/derickbailey/2011/10/11/backbone-js-getting-the-model-for-a-clicked-element/

Контекст, в котором я создал эту статью, конечно, отличается, но я думаю, что два решения, которые я предлагаю, наряду с плюсами и минусами каждого, должны заставить вас двигаться в правильном направлении.

Ответ 3

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

То же, что и Derick. Я не совсем уверен, что это прямо ответит на ваш вопрос, но я думаю, что в этом контексте, по крайней мере, стоит упомянуть.

Также см.: Использование Eventbus в базовой линии

Ответ 4

Кевин Пил дает отличный ответ - здесь моя версия tl; dr:

initialize : function () {

    //parent init stuff

    this.render(); //ANSWER: RENDER THE PARENT BEFORE INITIALIZING THE CHILD!!

    this.child = new Child();
},

Ответ 5

Я пытаюсь избежать связи между такими видами. Есть два способа, которые я обычно делаю:

Использовать маршрутизатор

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

Передача одного и того же эль в оба вида

this.parent = new Parent({el: $('.container-placeholder')});
this.child = new Child({el: $('.container-placeholder')});

Оба имеют знания того же DOM, и вы можете заказать их в любом случае.

Ответ 6

То, что я делаю, дает каждому ребенку идентификатор (который Backbone уже сделал для вас: cid)

Когда Container выполняет рендеринг, используя 'cid' и 'tagName', генерирует местозаполнитель для каждого дочернего элемента, поэтому в шаблоне дети не имеют представления о том, куда он будет помещен контейнером.

<tagName id='cid'></tagName>

чем вы можете использовать

Container.render()
Child.render();
this.$('#'+cid).replaceWith(child.$el);
// the rapalceWith in jquery will detach the element 
// from the dom first, so we need re-delegateEvents here
child.delegateEvents();

не требуется указанный заполнитель, а в контейнере создается только местозаполнитель, а не структура DOM детей. Cotainer и Children по-прежнему создают собственные элементы DOM и только один раз.

Ответ 7

Вот легкий микс для создания и рендеринга subviews, который, я думаю, касается всех проблем в этом потоке:

https://github.com/rotundasoftware/backbone.subviews

Подход, используемый этим подключаемым модулем, - это создание и рендеринг подвью при первом просмотре родительского представления. Затем, при последующем отображении родительского представления, $.detach элементы subview, повторно визуализируйте родителя, затем вставьте элементы subview в соответствующие места и повторно выполните их. Таким образом, объекты subviews повторно используются на последующих визуализаторах, и нет необходимости повторно делегировать события.

Обратите внимание, что случай представления коллекции (где каждая модель в коллекции представлен одним подзаголовком) совершенно отличается и заслуживает собственного обсуждения/решения, я думаю. Лучшее общее решение, о котором я знаю, это CollectionView в Marionette.

EDIT: для случая просмотра коллекции вы также можете проверить эту более ориентированную на пользовательский интерфейс реализацию, если вам нужен выбор моделей на основе на кликах и/или перетаскивании для переупорядочения.