Как отрендерить и добавить подвиды в Backbone.js

133

У меня есть настройка вложенного просмотра, которая может немного углубиться в мое приложение. Я мог бы подумать о множестве способов инициализации, рендеринга и добавления подвидов, но мне интересно, какова общая практика.

Вот пара, о которой я подумал:

initialize : function () {

    this.subView1 = new Subview({options});
    this.subView2 = new Subview({options});
},

render : function () {

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

    this.subView1.setElement('.some-el').render();
    this.subView2.setElement('.some-el').render();
}

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

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

По-другому:

initialize : function () {

},

render : function () {

    this.$el.empty();

    this.subView1 = new Subview({options});
    this.subView2 = new Subview({options});

    this.$el.append(this.subView1.render().el, this.subView2.render().el);
}

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

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

С onRenderсобытием:

initialize : function () {
    this.on('render', this.onRender);
    this.subView1 = new Subview({options});
    this.subView2 = new Subview({options});
},

render : function () {

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

    //other stuff

    return this.trigger('render');
},

onRender : function () {

    this.subView1.setElement('.some-el').render();
    this.subView2.setElement('.some-el').render();
}

Плюсы: логика подпредставления теперь отделена от render()метода представления.

С onRenderсобытием:

initialize : function () {
    this.on('render', this.onRender);
},

render : function () {

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

    //other stuff

    return this.trigger('render');
},

onRender : function () {
    this.subView1 = new Subview();
    this.subView2 = new Subview();
    this.subView1.setElement('.some-el').render();
    this.subView2.setElement('.some-el').render();
}

Я как бы смешал и сопоставил кучу разных практик во всех этих примерах (извините за это), но какие из них вы бы сохранили или добавили? а что бы ты не делал?

Краткое изложение практики:

  • Создавать подпункты в initializeили в render?
  • Выполнить всю логику рендеринга подвидов в renderили в onRender?
  • Использовать setElementили append/appendTo?
Йен Сторм Тейлор
источник
Я был бы осторожен с новым без удаления, у вас там утечка памяти.
vimdude
1
Не волнуйтесь, у меня есть closeметод и объект, onCloseкоторый очищает детей, но мне просто любопытно, как создать их экземпляры и отрендерить их в первую очередь.
Ян Сторм Тейлор,
3
@abdelsaid: в JavaScript GC обрабатывает освобождение памяти. deleteв JS отличается deleteот C ++. Если вы спросите меня, это ключевое слово с очень неудачным названием.
Майк Бейли
@MikeBantegui получил его, но он такой же, как в java, за исключением того, что в JS для освобождения памяти вам просто нужно присвоить null. Чтобы прояснить, что я имею в виду, попробуйте создать цикл с новым объектом внутри и контролировать память. Конечно, GC доберется до него, но вы потеряете память, прежде чем он доберется до него. В этом случае Render может вызываться много раз.
vimdude 01
3
Я начинающий разработчик Backbone. Кто-нибудь может объяснить, почему пример 1 заставляет нас повторно делегировать события? (Или я должен задать это в своем собственном вопросе?) Спасибо.
плов

Ответы:

58

Я обычно видел / использовал несколько разных решений:

Решение 1

var OuterView = Backbone.View.extend({
    initialize: function() {
        this.inner = new InnerView();
    },

    render: function() {
        this.$el.html(template); // or this.$el.empty() if you have no template
        this.$el.append(this.inner.$el);
        this.inner.render();
    }
});

var InnerView = Backbone.View.extend({
    render: function() {
        this.$el.html(template);
        this.delegateEvents();
    }
});

Это похоже на ваш первый пример, с некоторыми изменениями:

  1. Порядок, в котором вы добавляете подэлементы, имеет значение
  2. Внешний вид не содержит элементов html, которые должны быть установлены во внутреннем представлении (ах) (это означает, что вы все равно можете указать tagName во внутреннем представлении)
  3. render()называется ПОСЛЕ того, как элемент внутреннего представления был помещен в DOM, что полезно, если render()метод вашего внутреннего представления размещает / изменяет размеры на странице в зависимости от положения / размера других элементов (что, на мой взгляд, является распространенным случаем использования)

Решение 2

var OuterView = Backbone.View.extend({
    initialize: function() {
        this.render();
    },

    render: function() {
        this.$el.html(template); // or this.$el.empty() if you have no template
        this.inner = new InnerView();
        this.$el.append(this.inner.$el);
    }
});

var InnerView = Backbone.View.extend({
    initialize: function() {
        this.render();
    },

    render: function() {
        this.$el.html(template);
    }
});

Решение 2 может выглядеть чище, но оно вызвало некоторые странные вещи в моем опыте и отрицательно сказалось на производительности.

Я обычно использую Решение 1 по нескольким причинам:

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

Имейте в виду, что если вы инициализируете new View()каждый раз, когда render()вызывается, эта инициализация все равно будет вызываться delegateEvents(). Так что это не обязательно должно быть «мошенником», как вы выразились.

Lukas
источник
1
Ни одно из этих решений не обрабатывает дерево вложенного представления, вызывающее View.remove, что может иметь жизненно важное значение для выполнения пользовательской очистки в представлении, что в противном случае предотвратило бы сборку мусора
Доминик
31

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

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

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

Вернемся к вашему второму примеру, который, вероятно, является меньшим из трех зол. Вот пример кода, взятого из Recipes With Backbone , найденного на странице 42 моего PDF-издания:

...
render: function() {
    $(this.el).html(this.template());
    this.addAll();
    return this;
},
  addAll: function() {
    this.collection.each(this.addOne);
},
  addOne: function(model) {
    view = new Views.Appointment({model: model});
    view.render();
    $(this.el).append(view.el);
    model.bind('remove', view.remove);
}

Это лишь немного более сложная настройка, чем ваш второй пример: они определяют набор функций addAllи addOne, которые выполняют грязную работу. Я думаю, что этот подход работоспособен (и я, конечно, использую его); но это все еще оставляет странное послевкусие. (Простите за все эти языковые метафоры.)

К вашему мнению, добавление в правильном порядке: если вы строго добавляете, конечно, это ограничение. Но обязательно учтите все возможные схемы шаблонов. Возможно, вам действительно нужен элемент-заполнитель (например, пустой divили ul), а затем вы можете replaceWithсоздать новый (DOM) элемент, содержащий соответствующие подвиды. Добавление не является единственным решением, и вы, конечно, можете обойти проблему заказа, если вы так сильно заботитесь о ней, но я думаю, у вас возникнут проблемы с дизайном, если вас это не устраивает. Помните, у подпредставлений могут быть подпредставления, и они должны, если это уместно. Таким образом, у вас есть довольно древовидная структура, что довольно приятно: каждое подпредставление добавляет все свои подпредставления по порядку, прежде чем родительское представление добавит другое, и так далее.

К сожалению, решение №2, вероятно, лучшее, на что вы можете надеяться, используя готовую Backbone. Если вы заинтересованы в проверке сторонних библиотек, то одна из них, которую я изучил (но на самом деле у нее еще не было времени поиграть) - это Backbone.LayoutManager , который, кажется, имеет более полезный метод добавления подпредставлений. Однако даже у них недавно были дебаты по аналогичным вопросам.

Джош Лейцель
источник
4
Предпоследняя строка - model.bind('remove', view.remove);разве вы не должны просто сделать это в функции инициализации встречи, чтобы разделить их?
atp
2
Как насчет того, когда представление не может быть повторно создано каждый раз, когда его родительский рендеринг, потому что оно сохраняет состояние?
Мор
Остановите все это безумие и просто используйте плагин Backbone.subviews !
Brave Dave
6

Удивлен, это еще не было упомянуто, но я серьезно рассмотрю возможность использования марионетки .

Он навязывает немного больше структуры для Backbone приложений, включая типы зрения конкретных ( ListView, ItemView, Regionи Layout), добавляя собственные ControllerS и многое другое.

Вот проект на Github и отличное руководство Адди Османи в книге Backbone Fundamentals, которое поможет вам начать работу.

Дана Вудман
источник
3
Это не отвечает на вопрос.
Цезарь Баутиста
2
@CeasarBautista Я не вникаю в то, как использовать Marionette для достижения этой цели, но Marionette действительно решает вышеуказанную проблему
Дана Вудман
4

У меня есть, как мне кажется, довольно комплексное решение этой проблемы. Это позволяет модели в коллекции изменяться, и визуализируется только ее представление (а не вся коллекция). Он также обрабатывает удаление представлений зомби с помощью методов close ().

var SubView = Backbone.View.extend({
    // tagName: must be implemented
    // className: must be implemented
    // template: must be implemented

    initialize: function() {
        this.model.on("change", this.render, this);
        this.model.on("close", this.close, this);
    },

    render: function(options) {
        console.log("rendering subview for",this.model.get("name"));
        var defaultOptions = {};
        options = typeof options === "object" ? $.extend(true, defaultOptions, options) : defaultOptions;
        this.$el.html(this.template({model: this.model.toJSON(), options: options})).fadeIn("fast");
        return this;
    },

    close: function() {
        console.log("closing subview for",this.model.get("name"));
        this.model.off("change", this.render, this);
        this.model.off("close", this.close, this);
        this.remove();
    }
});
var ViewCollection = Backbone.View.extend({
    // el: must be implemented
    // subViewClass: must be implemented

    initialize: function() {
        var self = this;
        self.collection.on("add", self.addSubView, self);
        self.collection.on("remove", self.removeSubView, self);
        self.collection.on("reset", self.reset, self);
        self.collection.on("closeAll", self.closeAll, self);
        self.collection.reset = function(models, options) {
            self.closeAll();
            Backbone.Collection.prototype.reset.call(this, models, options);
        };
        self.reset();
    },

    reset: function() {
        this.$el.empty();
        this.render();
    },

    render: function() {
        console.log("rendering viewcollection for",this.collection.models);
        var self = this;
        self.collection.each(function(model) {
            self.addSubView(model);
        });
        return self;
    },

    addSubView: function(model) {
        var sv = new this.subViewClass({model: model});
        this.$el.append(sv.render().el);
    },

    removeSubView: function(model) {
        model.trigger("close");
    },

    closeAll: function() {
        this.collection.each(function(model) {
            model.trigger("close");
        });
    }
});

Использование:

var PartView = SubView.extend({
    tagName: "tr",
    className: "part",
    template: _.template($("#part-row-template").html())
});

var PartListView = ViewCollection.extend({
    el: $("table#parts"),
    subViewClass: PartView
});
sarink
источник
2

Проверьте этот миксин для создания и рендеринга подпредставлений:

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

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

Храбрый Дейв
источник
0

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

  • views может быть функцией или объектом, возвращающим объект определений представления
  • Когда .removeвызывается родительский объект, .removeдолжны вызываться вложенные дочерние элементы от самого низкого порядка (вплоть до представлений sub-sub-sub)
  • По умолчанию родительский вид передает свою собственную модель и коллекцию, но параметры можно добавлять и переопределять.

Вот пример:

views: {
    '.js-toolbar-left': CancelBtnView, // shorthand
    '.js-toolbar-right': {
        view: DoneBtnView,
        append: true
    },
    '.js-notification': {
        view: Notification.View,
        options: function() { // Options passed when instantiating
            return {
                message: this.state.get('notificationMessage'),
                state: 'information'
            };
        }
    }
}
Dominic
источник
0

Магистраль была специально построена таким образом, чтобы не было «общей» практики в отношении этого и многих других вопросов. Предполагается, что он будет как можно более непрочным. Теоретически вам даже не нужно использовать шаблоны с Backbone. Вы можете использовать javascript / jquery в renderфункции представления, чтобы вручную изменить все данные в представлении. Чтобы сделать его более экстремальным, вам даже не нужна одна конкретная renderфункция. Вы можете вызвать функцию, renderFirstNameкоторая обновляет имя в dom и renderLastNameобновляет фамилию в dom. Если вы воспользуетесь этим подходом, это будет намного лучше с точки зрения производительности, и вам больше никогда не придется вручную делегировать события. Код также будет иметь смысл для того, кто его читает (хотя это будет более длинный / беспорядочный код).

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

Ник Мэннинг
источник
0

Вы также можете вставить визуализированные подвиды как переменные в основной шаблон как переменные.

сначала визуализируйте подвиды и преобразуйте их в HTML следующим образом:

var subview1 = $(subview1.render.el).html(); var subview2 = $(subview2.render.el).html();

(таким образом, вы также можете динамически объединять строки, как subview1 + subview2при использовании в циклах), а затем передавать его в главный шаблон, который выглядит следующим образом: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

и, наконец, введите его так:

this.$el.html(_.template(MasterTemplate, { sub1: subview1, sub2: subview2 } ));

Относительно событий в подпредставлениях. Скорее всего, они должны быть связаны в родительском (masterView) при таком подходе, а не в подпредставлениях.

Б Пильтц
источник
0

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

Backbone.View.prototype.close = function() {
    if (this.onClose) {
        this.onClose();
    }
    this.remove(); };

NewView = Backbone.View.extend({
    initialize: function() {
       this.childViews = [];
    },
    renderChildren: function(item) {
        var itemView = new NewChildView({ model: item });
        $(this.el).prepend(itemView.render());
        this.childViews.push(itemView);
    },
    onClose: function() {
      _(this.childViews).each(function(view) {
        view.close();
      });
    } });

NewChildView = Backbone.View.extend({
    tagName: 'li',
    render: function() {
    } });
Флинтофф
источник
0

Нет необходимости повторно делегировать события, так как это дорого. Увидеть ниже:

    var OuterView = Backbone.View.extend({
    initialize: function() {
        this.inner = new InnerView();
    },

    render: function() {
        // first detach subviews            
        this.inner.$el.detach(); 

        // now can set html without affecting subview element's events
        this.$el.html(template);

        // now render and attach subview OR can even replace placeholder 
        // elements in template with the rendered subview element
        this.$el.append(this.inner.render().el);

    }
});

var InnerView = Backbone.View.extend({
    render: function() {
        this.$el.html(template);            
    }
});
Сохам Джоши
источник