Rendern und Anhängen von Unteransichten in Backbone.js

133

Ich habe ein Nested-View-Setup, das in meiner Anwendung etwas tiefer gehen kann. Es gibt eine Reihe von Möglichkeiten, die Unteransichten zu initialisieren, zu rendern und anzuhängen, aber ich frage mich, was gängige Praxis ist.

Hier sind ein paar, an die ich gedacht habe:

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();
}

Vorteile: Sie müssen sich beim Anhängen nicht um die Einhaltung der richtigen DOM-Reihenfolge kümmern. Die Ansichten werden frühzeitig initialisiert, sodass in der Renderfunktion nicht so viel auf einmal zu tun ist.

Nachteile: Sie sind gezwungen, Events () neu zu delegieren, was kostspielig sein kann? Die Renderfunktion der übergeordneten Ansicht ist mit dem gesamten Rendering der Unteransicht überfüllt, das ausgeführt werden muss. Sie können tagNamedie Elemente nicht festlegen , daher muss die Vorlage die richtigen TagNames beibehalten.

Ein anderer Weg:

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);
}

Vorteile: Sie müssen Ereignisse nicht erneut delegieren. Sie benötigen keine Vorlage, die nur leere Platzhalter enthält, und Ihre Tag-Namen werden wieder von der Ansicht definiert.

Nachteile: Sie müssen jetzt sicherstellen, dass die Dinge in der richtigen Reihenfolge angehängt werden. Das Rendern der übergeordneten Ansicht wird weiterhin durch das Rendern der Unteransicht überladen.

Mit einer onRenderVeranstaltung:

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();
}

Vorteile : Die Subview-Logik ist jetzt von der View- render()Methode getrennt.

Mit einer onRenderVeranstaltung:

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();
}

Ich habe in all diesen Beispielen eine Reihe verschiedener Praktiken gemischt und aufeinander abgestimmt (tut mir leid), aber welche würden Sie behalten oder hinzufügen? und was würdest du nicht tun

Zusammenfassung der Praktiken:

  • Unteransichten in initializeoder in instanziieren render?
  • Führen Sie die gesamte Renderlogik für Unteransichten in renderoder in onRender?
  • Verwenden Sie setElementoder append/appendTo?
Ian Storm Taylor
quelle
Ich wäre vorsichtig mit dem neuen ohne Löschen, Sie haben dort Speicherleck.
Vimdude
1
Keine Sorge, ich habe eine closeMethode und eine onClose, die Kinder aufräumt, aber ich bin nur neugierig, wie ich sie überhaupt instanziieren und rendern kann.
Ian Storm Taylor
3
@abdelsaid: In JavaScript übernimmt der GC die Freigabe des Speichers. deletein JS ist nicht dasselbe wie in deleteC ++. Es ist ein sehr schlecht benanntes Schlüsselwort, wenn Sie mich fragen.
Mike Bailey
@MikeBantegui hat es bekommen, aber es ist das gleiche wie in Java, außer dass Sie in JS, um Speicher freizugeben, nur null zuweisen müssen. Um zu verdeutlichen, was ich meine, versuchen Sie, eine Schleife mit einem neuen Objekt darin zu erstellen und den Speicher zu überwachen. Natürlich wird GC dazu kommen, aber Sie werden Speicher verlieren, bevor es dazu kommt. In diesem Fall Rendern, das möglicherweise mehrmals aufgerufen wird.
Vimdude
3
Ich bin ein unerfahrener Backbone-Entwickler. Kann jemand bitte erklären, warum Beispiel 1 uns zwingt, Ereignisse neu zu delegieren? (Oder sollte ich das in einer eigenen Frage stellen?) Danke.
Pilau

Antworten:

58

Ich habe im Allgemeinen einige verschiedene Lösungen gesehen / verwendet:

Lösung 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();
    }
});

Dies ähnelt Ihrem ersten Beispiel mit einigen Änderungen:

  1. Die Reihenfolge, in der Sie die Unterelemente anhängen, ist wichtig
  2. Die äußere Ansicht enthält nicht die HTML-Elemente, die in den inneren Ansichten festgelegt werden sollen (dh, Sie können in der inneren Ansicht weiterhin tagName angeben).
  3. render()wird aufgerufen, nachdem das Element der inneren Ansicht in das DOM eingefügt wurde. Dies ist hilfreich, wenn die render()Methode Ihrer inneren Ansicht sich auf der Seite basierend auf der Position / Größe anderer Elemente platziert / dimensioniert (was meiner Erfahrung nach ein häufiger Anwendungsfall ist).

Lösung 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);
    }
});

Lösung 2 mag sauberer aussehen, hat aber meiner Erfahrung nach einige seltsame Dinge verursacht und die Leistung negativ beeinflusst.

Im Allgemeinen verwende ich Lösung 1 aus mehreren Gründen:

  1. Viele meiner Ansichten beruhen darauf, dass sie in ihrer render()Methode bereits im DOM sind
  2. Wenn die äußere Ansicht neu gerendert wird, müssen Ansichten nicht neu initialisiert werden. Diese Neuinitialisierung kann zu Speicherverlusten und auch zu ausgeflippten Problemen mit vorhandenen Bindungen führen

Beachten Sie , dass diese Initialisierung ohnehin aufgerufen wird, wenn Sie bei new View()jedem render()Aufruf eine Initialisierung durchführen delegateEvents(). Das sollte also nicht unbedingt ein "Betrug" sein, wie Sie ausgedrückt haben.

Lukas
quelle
1
Keine dieser Lösungen funktioniert im Unteransichtsbaum mit dem Aufruf von View.remove. Dies kann für die benutzerdefinierte Bereinigung in der Ansicht von entscheidender Bedeutung sein, da sonst die Speicherbereinigung verhindert wird
Dominic
31

Dies ist ein beständiges Problem mit Backbone, und meiner Erfahrung nach gibt es keine wirklich zufriedenstellende Antwort auf diese Frage. Ich teile Ihre Frustration, zumal es trotz der Häufigkeit dieses Anwendungsfalls so wenig Anleitung gibt. Das heißt, ich gehe normalerweise mit etwas, das Ihrem zweiten Beispiel ähnelt.

Zuallererst würde ich alles, was erfordert, dass Sie Ereignisse neu delegieren, sofort ablehnen. Das ereignisgesteuerte Ansichtsmodell von Backbone ist eine der wichtigsten Komponenten, und diese Funktionalität zu verlieren, nur weil Ihre Anwendung nicht trivial ist, würde jedem Programmierer einen schlechten Geschmack hinterlassen. Also Kratzer Nummer eins.

In Bezug auf Ihr drittes Beispiel denke ich, dass es nur ein Ende der herkömmlichen Rendering-Praxis ist und nicht viel Bedeutung hinzufügt. Wenn Sie eine tatsächliche Ereignisauslösung durchführen (dh kein erfundenes " onRender" Ereignis), lohnt es sich möglicherweise, diese Ereignisse nur an sich renderselbst zu binden . Wenn Sie renderunhandlich und komplex werden, haben Sie zu wenige Unteransichten.

Zurück zu Ihrem zweiten Beispiel, das wahrscheinlich das kleinere der drei Übel ist. Hier ist ein Beispielcode aus Recipes With Backbone , der auf Seite 42 meiner PDF-Ausgabe zu finden ist:

...
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);
}

Dies ist nur ein etwas komplexeres Setup als Ihr zweites Beispiel: Sie spezifizieren eine Reihe von Funktionen addAllund addOneerledigen die Drecksarbeit. Ich denke, dieser Ansatz ist praktikabel (und ich benutze ihn auf jeden Fall); aber es hinterlässt immer noch einen bizarren Nachgeschmack. (Verzeihen Sie all diese Zungenmetaphern.)

Zu Ihrem Punkt beim Anhängen in der richtigen Reihenfolge: Wenn Sie streng anhängen, ist dies sicher eine Einschränkung. Stellen Sie jedoch sicher, dass Sie alle möglichen Vorlagenschemata berücksichtigen. Vielleicht möchten Sie tatsächlich ein Platzhalterelement (z. B. ein leeres divoder ul), das Sie dann replaceWithals neues (DOM) Element verwenden können, das die entsprechenden Unteransichten enthält. Das Anhängen ist nicht die einzige Lösung, und Sie können das Bestellproblem sicherlich umgehen, wenn Sie sich so sehr darum kümmern, aber ich würde mir vorstellen, dass Sie ein Designproblem haben, wenn es Sie auslöst. Denken Sie daran, Unteransichten können Unteransichten haben, und sie sollten dies gegebenenfalls tun. Auf diese Weise haben Sie eine ziemlich baumartige Struktur, was sehr schön ist: Jede Unteransicht fügt alle ihre Unteransichten der Reihe nach hinzu, bevor die übergeordnete Ansicht eine weitere hinzufügt, und so weiter.

Leider ist Lösung 2 wahrscheinlich die beste, die Sie sich für die Verwendung von Out-of-the-Box-Backbone erhoffen können. Wenn Sie sich für das Auschecken von Bibliotheken von Drittanbietern interessieren, habe ich mir Backbone.LayoutManager angesehen (aber noch keine Zeit zum Spielen gehabt) , das eine gesündere Methode zum Hinzufügen von Unteransichten zu bieten scheint. Selbst sie hatten jedoch kürzlich Debatten zu ähnlichen Themen wie diese.

Josh Leitzel
quelle
4
Die vorletzte Zeile - model.bind('remove', view.remove);- sollten Sie das nicht einfach in der Initialisierungsfunktion des Termins tun, um sie getrennt zu halten?
atp
2
Was ist, wenn eine Ansicht nicht jedes Mal neu instanziiert werden kann, wenn das übergeordnete Element sie rendert, weil sie einen Status beibehält?
mor
Stoppen Sie all diese Verrücktheit und verwenden Sie einfach das Plugin Backbone.subviews !
Brave Dave
6

Überrascht, dass dies noch nicht erwähnt wurde, aber ich würde ernsthaft in Betracht ziehen, Marionette zu verwenden .

Es erzwingt ein bisschen mehr Struktur zu Backbone - Anwendungen, einschließlich spezifische Ansichtstypen ( ListView, ItemView, Regionund Layout), das Hinzufügen richtigen Controllers und vieles mehr.

Hier ist das Projekt auf Github und eine großartige Anleitung von Addy Osmani im Buch Backbone Fundamentals , um Ihnen den Einstieg zu erleichtern.

Dana Woodman
quelle
3
Dies beantwortet die Frage nicht.
Ceasar Bautista
2
@CeasarBautista Ich gehe nicht darauf ein, wie man Marionette benutzt, um dies zu erreichen, aber Marionette löst tatsächlich das obige Problem
Dana Woodman
4

Ich habe, wie ich glaube, eine ziemlich umfassende Lösung für dieses Problem. Dadurch kann sich ein Modell innerhalb einer Sammlung ändern und nur seine Ansicht wird neu gerendert (und nicht die gesamte Sammlung). Es behandelt auch das Entfernen von Zombie-Ansichten über die close () -Methoden.

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");
        });
    }
});

Verwendung:

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

var PartListView = ViewCollection.extend({
    el: $("table#parts"),
    subViewClass: PartView
});
Sarink
quelle
2

Schauen Sie sich dieses Mixin zum Erstellen und Rendern von Unteransichten an:

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

Es handelt sich um eine minimalistische Lösung, die viele der in diesem Thread behandelten Probleme behebt, einschließlich der Renderreihenfolge, der Tatsache, dass Ereignisse nicht erneut delegiert werden müssen usw. Beachten Sie den Fall einer Sammlungsansicht (bei der jedes Modell in der Sammlung mit einem dargestellt wird Unteransicht) ist ein anderes Thema. Die beste allgemeine Lösung, die mir für diesen Fall bekannt ist, ist die CollectionView in Marionette .

Tapferer Dave
quelle
0

Ich mag keine der oben genannten Lösungen wirklich. Ich bevorzuge für diese Konfiguration jede Ansicht, die manuell in der Rendermethode arbeiten muss.

  • views kann eine Funktion oder ein Objekt sein, das ein Objekt mit Ansichtsdefinitionen zurückgibt
  • Wenn ein Elternteil .removeaufgerufen wird, sollte das .removevon verschachtelten Kindern ab der niedrigsten Ordnung aufgerufen werden (vollständig aus Sub-Sub-Sub-Ansichten).
  • Standardmäßig übergibt die übergeordnete Ansicht ihr eigenes Modell und ihre eigene Sammlung. Optionen können jedoch hinzugefügt und überschrieben werden.

Hier ist ein Beispiel:

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
quelle
0

Das Backbone wurde absichtlich so gebaut, dass es in Bezug auf dieses und viele andere Probleme keine "übliche" Praxis gab. Es soll so unbefangen wie möglich sein. Theoretisch müssen Sie nicht einmal Vorlagen mit Backbone verwenden. Sie können Javascript / jquery in der renderFunktion einer Ansicht verwenden, um alle Daten in der Ansicht manuell zu ändern. Um es extremer zu machen, benötigen Sie nicht einmal eine bestimmte renderFunktion. Sie könnten eine Funktion aufrufen lassen, renderFirstNamedie den Vornamen im Dom und renderLastNameden Nachnamen im Dom aktualisiert. Wenn Sie diesen Ansatz wählen würden, wäre dies in Bezug auf die Leistung viel besser und Sie müssten Ereignisse nie wieder manuell delegieren. Der Code wäre auch für jemanden, der ihn liest, absolut sinnvoll (obwohl er länger / unordentlicher wäre).

Normalerweise ist es jedoch kein Nachteil, Vorlagen zu verwenden und einfach die gesamte Ansicht und ihre Unteransichten bei jedem Renderaufruf zu zerstören und neu zu erstellen, da es dem Fragesteller nicht einmal in den Sinn gekommen ist, etwas anderes zu tun. Das ist es, was die meisten Menschen für so ziemlich jede Situation tun, auf die sie stoßen. Und deshalb machen meinungsgebundene Frameworks dies einfach zum Standardverhalten.

Nick Manning
quelle
0

Sie können die gerenderten Unteransichten auch als Variablen als Variablen in die Hauptvorlage einfügen.

Rendern Sie zuerst die Unteransichten und konvertieren Sie sie wie folgt in HTML:

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

(Auf diese Weise können Sie die Ansichten auch dynamisch als Zeichenfolge verketten, wie subview1 + subview2sie in Schleifen verwendet werden) und sie dann an die Master-Vorlage übergeben, die folgendermaßen aussieht: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

und spritze es endlich so:

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

Zu den Ereignissen in den Unteransichten: Sie müssen höchstwahrscheinlich im übergeordneten Element (masterView) mit diesem Ansatz verbunden sein, nicht in den Unteransichten.

B Piltz
quelle
0

Ich verwende gerne den folgenden Ansatz, um sicherzustellen, dass die untergeordneten Ansichten ordnungsgemäß entfernt werden. Hier ist ein Beispiel aus dem Buch von Addy Osmani.

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() {
    } });
FlintOff
quelle
0

Es ist nicht erforderlich, Ereignisse erneut zu delegieren, da dies kostspielig ist. Siehe unten:

    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);            
    }
});
Soham Joshi
quelle