wake-up-neo.com

Rendern und Anhängen von Unteransichten in Backbone.js

Ich habe ein Nested-View-Setup, das sich in meiner Anwendung etwas vertiefen kann. Es gibt eine Reihe von Möglichkeiten, wie ich die Unteransichten initialisieren, rendern und anhängen könnte, aber ich frage mich, was allgemein üblich 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();
}

Profis: Sie müssen sich keine Sorgen machen, die richtige DOM-Reihenfolge beim Anhängen beizubehalten. Die Ansichten werden zu einem frühen Zeitpunkt initialisiert, sodass in der Renderfunktion nicht so viel auf einmal erledigt werden muss.

Nachteile: Sie sind gezwungen, Ereignisse () neu zu delegieren, was kostspielig sein kann? Die Render-Funktion der übergeordneten Ansicht ist mit dem gesamten Subview-Rendering überladen, das ausgeführt werden muss. Sie können das tagName der Elemente nicht festlegen, daher muss die Vorlage die richtigen Tag-Namen 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);
}

Profis: 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 Rendering der übergeordneten Ansicht wird weiterhin durch das Rendering der Unteransicht überladen.

Mit einem onRender -Ereignis:

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

Pros: Die Unteransichtslogik ist jetzt von der render() -Methode der Ansicht getrennt.

Mit einem onRender -Ereignis:

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 von unterschiedlichen Vorgehensweisen kombiniert (es tut mir leid), aber welche würden Sie beibehalten oder hinzufügen? und was würdest du nicht tun

Zusammenfassung der Praktiken:

  • Teilansichten in initialize oder in render instanziieren?
  • Alle Unteransichts-Rendering-Logik in render oder in onRender ausführen?
  • Verwenden Sie setElement oder append/appendTo?
133

Ich habe im Allgemeinen ein paar 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 ein paar Änderungen:

  1. Die Reihenfolge, in der Sie die Unterelemente anhängen, ist von Bedeutung
  2. Die äußere Ansicht enthält nicht die HTML-Elemente, die für die innere Ansicht (en) festgelegt werden sollen (dh, Sie können in der inneren Ansicht weiterhin tagName angeben).
  3. render() wird aufgerufen, nachdem das Element der inneren Ansicht im DOM platziert wurde. Dies ist hilfreich, wenn die render() -Methode der inneren Ansicht sich selbst auf der Seite basierend auf der Position anderer Elemente platziert/ihre Größe ändert/size (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 jedoch 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 die Ansichten nicht neu initialisiert werden. Diese Neuinitialisierung kann zu Speicherverlusten und auch zu verrückten Problemen mit vorhandenen Bindungen führen

Beachten Sie, dass die Initialisierung beim Initialisieren einer new View() bei jedem Aufruf von render() ohnehin delegateEvents() aufruft. Das sollte also nicht unbedingt ein "Betrug" sein, wie Sie ausgedrückt haben.

58
Lukas

Dies ist ein mehrjähriges 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 Anleitungen gibt. Das heißt, ich gehe normalerweise mit etwas, das Ihrem zweiten Beispiel ähnlich ist.

Zuallererst würde ich alles, was Sie zur erneuten Delegierung von Ereignissen benötigen, aus der Hand weisen. Das ereignisgesteuerte Ansichtsmodell von Backbone ist eine der wichtigsten Komponenten. Wenn Sie diese Funktionalität verlieren, nur weil Ihre Anwendung nicht trivial ist, hinterlässt dies einen schlechten Geschmack im Mund eines jeden Programmierers. Also Kratz Nummer eins.

In Bezug auf Ihr drittes Beispiel denke ich, dass es nur ein Ende der herkömmlichen Renderpraxis darstellt und nicht viel Bedeutung hinzufügt. Wenn Sie eine tatsächliche Ereignisauslösung durchführen (d. H. Kein erfundenes "onRender" -Ereignis), ist es möglicherweise sinnvoll, diese Ereignisse nur an render selbst zu binden. Wenn Sie feststellen, dass render unhandlich und komplex wird, haben Sie zu wenige Unteransichten.

Zurück zu Ihrem zweiten Beispiel, das wahrscheinlich das geringere der drei Übel ist. Hier ist ein Beispielcode aus Rezepte mit Backbone , gefunden auf Seite 42 von meinem PDF = Ausgabe:

...
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 eine etwas komplexere Konfiguration als in Ihrem zweiten Beispiel: Sie spezifizieren eine Reihe von Funktionen, addAll und addOne, die die Dirty-Arbeit erledigen. Ich denke, dieser Ansatz ist praktikabel (und ich verwende ihn auf jeden Fall). aber es hinterlässt immer noch einen bizarren Nachgeschmack. (Verzeihen Sie all diese Zungenmetaphern.)

Was das Anhängen in der richtigen Reihenfolge angeht: Wenn Sie streng anhängen, ist das 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 div oder ul), das Sie dann replaceWith ein neues (DOM) Element, 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 dafür interessieren, aber ich kann mir vorstellen, dass Sie ein Designproblem haben, wenn es Sie auslöst. Denken Sie daran, dass Unteransichten Unteransichten haben können und sollten, wenn es angebracht ist. Auf diese Weise haben Sie eine ziemlich baumartige Struktur, die sehr schön ist: Jede Unteransicht fügt alle Unteransichten der Reihe nach hinzu, bevor die übergeordnete Ansicht eine andere hinzufügt, und so weiter.

Leider ist Lösung Nr. 2 wahrscheinlich die beste Lösung, die Sie für die Verwendung von Backbone aus der Box hoffen können. Wenn Sie sich für das Auschecken von Bibliotheken von Drittanbietern interessieren, ist Backbone.LayoutManager anscheinend eine gesündere Methode zum Hinzufügen von Unteransichten. Sogar sie hatten jüngste Debatten zu ähnlichen Themen wie diese.

31
Josh Leitzel

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

Die Backbone-Apps werden etwas strukturierter, einschließlich bestimmter Ansichtstypen (ListView, ItemView, Region und Layout), wobei die 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.

5
Dana Woodman

Ich habe, wie ich glaube, eine recht umfassende Lösung für dieses Problem. Dadurch kann sich ein Modell innerhalb einer Sammlung ändern und nur die Ansicht wird neu gerendert (und nicht die gesamte Sammlung). Es behandelt auch das Entfernen von Zombie-Ansichten durch 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
});
4
sarink

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 diskutierten Probleme behebt, einschließlich der Renderreihenfolge, ohne dass Ereignisse neu delegiert werden müssen usw. Beachten Sie, dass der Fall einer Sammlungsansicht (wobei jedes Modell in der Sammlung mit einem Modell dargestellt wird) Unteransicht) ist ein anderes Thema. Die beste allgemeine Lösung, die mir für diesen Fall bekannt ist, ist CollectionView in Marionette .

2
Brave Dave

Ich benutze gerne den folgenden Ansatz, der auch sicherstellt, 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() {
    } });
0
FlintOff

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

  • views kann eine Funktion oder ein Objekt sein, das ein Objekt mit Sichtdefinitionen zurückgibt
  • Wenn der .remove Eines Elternteils aufgerufen wird, sollte der .remove Der verschachtelten Kinder von der niedrigsten bis zur niedrigsten Reihenfolge aufgerufen werden (bis hin zu Unter-Unter-Unter-Ansichten).
  • Standardmäßig übergibt die übergeordnete Ansicht ihr eigenes Modell und ihre eigene Sammlung, aber Optionen können 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'
            };
        }
    }
}
0
Dominic

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);            
    }
});
0
soham joshi

Sie können die gerenderten Unteransichten auch 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 wie subview1 + subview2 verketten, wenn sie in Schleifen verwendet werden) und sie dann an die Master-Vorlage übergeben, die so aussieht: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

und spritze es endlich so ein:

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

Zu den Ereignissen in den Unteransichten: Sie müssen höchstwahrscheinlich in der übergeordneten Ansicht (MasterView) mit dieser Methode verbunden werden, die nicht in den Unteransichten enthalten ist.

0
B Piltz

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 mit Backbone nicht einmal Vorlagen verwenden. Sie können Javascript/jquery in der Funktion render einer Ansicht verwenden, um alle Daten in der Ansicht manuell zu ändern. Um es extremer zu machen, benötigen Sie nicht einmal eine bestimmte render -Funktion. Sie könnten eine Funktion namens renderFirstName haben, die den Vornamen im Dom und renderLastName den Nachnamen im Dom aktualisiert. Wenn Sie diesen Ansatz wählen, ist die Leistung erheblich besser und Sie müssen Ereignisse nie wieder manuell delegieren. Der Code würde auch für jemanden, der ihn liest, Sinn machen (obwohl er länger/unordentlicher wäre).

Normalerweise ist es jedoch kein Nachteil, Vorlagen zu verwenden und die gesamte Ansicht und ihre Teilansichten bei jedem Renderaufruf zu zerstören und neu zu erstellen, da der Fragesteller nicht einmal auf die Idee gekommen ist, etwas anderes zu tun. Das ist es also, was die meisten Menschen für so ziemlich jede Situation tun, in der sie auftauchen. Aus diesem Grund machen meinungsbasierte Frameworks dies einfach zum Standardverhalten.

0
Nick Manning