Come eseguire il rendering e aggiungere visualizzazioni secondarie in Backbone.js


133

Ho una configurazione di visualizzazione nidificata che può diventare un po 'profonda nella mia applicazione. Ci sono molti modi in cui potrei pensare di inizializzare, rendere e aggiungere le sotto-viste, ma mi chiedo quale sia la pratica comune.

Ecco un paio a cui ho pensato:

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

Pro: Non devi preoccuparti di mantenere il giusto ordine DOM con l'aggiunta. Le viste vengono inizializzate all'inizio, quindi non c'è molto da fare tutto in una volta nella funzione di rendering.

Contro: sei costretto a ri-delegareEvents (), che potrebbe essere costoso? La funzione di rendering della vista padre è ingombra di tutto il rendering della sottoview che deve avvenire? Non hai la possibilità di impostare gli tagNameelementi, quindi il modello deve mantenere i tagNames corretti.

Un altro modo:

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

Pro: non è necessario delegare nuovamente gli eventi. Non è necessario un modello che contenga solo segnaposto vuoti e i tagName tornino ad essere definiti dalla vista.

Contro: ora devi assicurarti di aggiungere le cose nel giusto ordine. Il rendering della vista padre è ancora ingombro dal rendering della vista secondaria.

Con un onRenderevento:

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

Pro: la logica della vista secondaria ora è separata dal render()metodo della vista .

Con un onRenderevento:

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

Ho un po 'mescolato e abbinato un sacco di pratiche diverse in tutti questi esempi (quindi mi dispiace per quello) ma quali sono quelli che vorresti mantenere o aggiungere? e cosa non faresti?

Riepilogo delle pratiche:

  • Creare istanze secondarie in initializeo in render?
  • Eseguire tutta la logica di rendering della vista secondaria in rendero in onRender?
  • Usa setElemento append/appendTo?

Starei attento al nuovo senza cancellare, hai una perdita di memoria.
vimdude,

1
Non preoccuparti, ho un closemetodo e un metodo per onCloseripulire i bambini, ma sono solo curioso di sapere come creare un'istanza e renderli in primo luogo.
Ian Storm Taylor,

3
@abdelsaid: in JavaScript, il GC gestisce la deallocazione della memoria. deletein JS non è lo stesso deletedi C ++. È una parola chiave con un nome molto scarso se me lo chiedi.
Mike Bailey,

@MikeBantegui ha capito, ma è lo stesso di java tranne che in JS per liberare memoria devi solo assegnare null. Per chiarire cosa intendo, prova a creare un ciclo con un nuovo oggetto all'interno e monitorare la memoria. Naturalmente GC ci riuscirà, ma perderai memoria prima che arrivi. In questo caso Render che potrebbe essere chiamato più volte.
vimdude,

3
Sono uno sviluppatore Backbone alle prime armi. Qualcuno può spiegare perché l'esempio 1 ci obbliga a delegare nuovamente gli eventi? (O dovrei fare questo nella sua stessa domanda?) Grazie.
pilau

Risposte:


58

In genere ho visto / usato un paio di soluzioni diverse:

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

Questo è simile al tuo primo esempio, con alcune modifiche:

  1. L'ordine in cui si aggiungono gli elementi secondari è importante
  2. La vista esterna non contiene gli elementi html da impostare nelle viste interne (il che significa che puoi ancora specificare tagName nella vista interna)
  3. render()si chiama DOPO che l'elemento della vista interna è stato inserito nel DOM, il che è utile se il render()metodo della vista interna sta posizionando / dimensionando se stesso sulla pagina in base alla posizione / dimensione di altri elementi (che è un caso d'uso comune, nella mia esperienza)

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

La soluzione 2 può sembrare più pulita, ma ha causato alcune cose strane nella mia esperienza e ha influito negativamente sulle prestazioni.

In genere utilizzo la Soluzione 1, per un paio di motivi:

  1. Molte delle mie opinioni si basano sul fatto di essere già nel DOM nel loro render()metodo
  2. Quando la vista esterna viene ridistribuita, non è necessario reinizializzare le viste, la cui reinizializzazione può causare perdite di memoria e anche problemi bizzarri con i collegamenti esistenti

Tieni presente che se stai inizializzando new View()ogni volta che render()viene chiamato, l'inizializzazione chiamerà delegateEvents()comunque. Quindi non dovrebbe essere necessariamente una "truffa", come hai espresso.


1
Nessuna di queste soluzioni funziona con l'albero della vista secondaria che chiama View.remove, che può essere vitale per eseguire la pulizia personalizzata nella vista, che altrimenti impedirebbe la raccolta dei rifiuti
Dominic,

31

Questo è un problema perenne con Backbone e, nella mia esperienza, non c'è davvero una risposta soddisfacente a questa domanda. Condivido la tua frustrazione, soprattutto perché c'è così poca guida nonostante quanto sia comune questo caso d'uso. Detto questo, di solito vado con qualcosa di simile al tuo secondo esempio.

Prima di tutto, vorrei scartare di mano tutto ciò che richiede di delegare nuovamente gli eventi. Il modello di visualizzazione basato sugli eventi di Backbone è uno dei suoi componenti più cruciali e perdere tale funzionalità semplicemente perché l'applicazione non è banale lascerebbe un cattivo gusto nella bocca di qualsiasi programmatore. Quindi gratta e vinci numero uno.

Per quanto riguarda il tuo terzo esempio, penso che sia solo una conclusione della pratica di rendering convenzionale e non aggiunge molto significato. Forse se stai attivando un evento reale (cioè, non un " onRender" evento inventato ), varrebbe la pena limitarsi a legare quegli eventi a renderse stesso. Se ti renderaccorgi che diventa ingombrante e complesso, hai troppe poche visualizzazioni.

Torna al tuo secondo esempio, che è probabilmente il minore dei tre mali. Ecco un esempio di codice estratto da Recipes With Backbone , che si trova a pagina 42 della mia edizione 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);
}

Questa è solo una configurazione leggermente più sofisticata rispetto al tuo secondo esempio: specificano un insieme di funzioni addAlle addOne, che fanno il lavoro sporco. Penso che questo approccio sia praticabile (e certamente lo uso); ma lascia ancora un bizzarro retrogusto. (Perdonate tutte queste metafore linguistiche.)

Al punto di aggiungere nel giusto ordine: se stai rigorosamente aggiungendo, questo è un limite. Ma assicurati di considerare tutti i possibili schemi di templating. Forse in realtà ti piacerebbe un elemento segnaposto (ad esempio un vuoto divo ul) che puoi quindi replaceWithun nuovo elemento (DOM) che contiene le subview appropriate. L'aggiunta non è l'unica soluzione e puoi sicuramente aggirare il problema degli ordini se ti preoccupi così tanto, ma immaginerei che tu abbia un problema di progettazione se ti sta inciampando. Ricorda, le sottoview possono avere subview e dovrebbero, se è appropriato. In questo modo, hai una struttura piuttosto simile ad un albero, che è abbastanza bella: ogni sottoview aggiunge tutte le sue sottoview, in ordine, prima che la vista principale ne aggiunga un'altra e così via.

Sfortunatamente, la soluzione n. 2 è probabilmente la migliore che si possa sperare di usare Backbone out-of-the-box. Se sei interessato a controllare le librerie di terze parti, una delle quali ho esaminato (ma in realtà non ho ancora avuto tempo di giocare) è Backbone.LayoutManager , che sembra avere un metodo più salutare per aggiungere subview. Tuttavia, anche loro hanno avuto dibattiti recenti su questioni simili a queste.


4
La penultima riga - model.bind('remove', view.remove);non dovresti farlo nella funzione di inizializzazione dell'appuntamento per tenerli separati?
atp

2
Che dire di quando una vista non può essere ri-istanziata ogni volta che viene eseguito il rendering del genitore perché mantiene uno stato?
mor

Ferma tutta questa follia e usa semplicemente il plug-in Backbone.subviews !
Brave Dave,

6

Sorpreso, questo non è ancora stato menzionato, ma prenderei seriamente in considerazione l'uso della marionetta .

Si impone un po 'di più la struttura per applicazioni backbone, compresi i tipi vista specifica ( ListView, ItemView, Regione Layout), l'aggiunta di adeguate Controllers e molto altro ancora.

Ecco il progetto su Github e un'ottima guida di Addy Osmani nel libro Backbone Fundamentals per iniziare.


3
Questo non risponde alla domanda.
Ceasar Bautista,

2
@CeasarBautista Non ho intenzione di usare Marionette per raggiungere questo obiettivo, ma Marionette risolve davvero il problema sopra descritto
Dana Woodman,

4

Ho, quello che credo di essere, una soluzione abbastanza completa a questo problema. Permette a un modello all'interno di una collezione di cambiare, e solo la sua vista viene ri-renderizzata (piuttosto che l'intera collezione). Gestisce anche la rimozione delle viste zombi attraverso i metodi 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");
        });
    }
});

Uso:

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

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

2

Dai un'occhiata a questo mixin per la creazione e il rendering di sottoview:

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

È una soluzione minimalista che affronta molti dei problemi discussi in questo thread, incluso l'ordine di rendering, non dover delegare nuovamente gli eventi, ecc. Si noti che il caso di una vista di raccolta (in cui ogni modello nella raccolta è rappresentato con uno sottoview) è un argomento diverso. La migliore soluzione generale di cui sono a conoscenza in questo caso è la CollectionView in Marionette .


0

Non mi piace davvero nessuna delle soluzioni di cui sopra. Preferisco per questa configurazione su ogni vista dover lavorare manualmente nel metodo di rendering.

  • views può essere una funzione o un oggetto che restituisce le definizioni di un oggetto di vista
  • Quando .removeviene chiamato un genitore , .removedovrebbe essere chiamato il numero di figli nidificati dall'ordine più basso in su (completamente dalle viste sub-sub-sub-sub)
  • Per impostazione predefinita, la vista padre passa il proprio modello e raccolta, ma le opzioni possono essere aggiunte e sovrascritte.

Ecco un esempio:

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

La spina dorsale è stata costruita intenzionalmente in modo tale che non esistessero pratiche "comuni" riguardo a questo e molti altri problemi. È pensato per essere il più nonopinionato possibile. Teoricamente, non è nemmeno necessario utilizzare i modelli con Backbone. È possibile utilizzare javascript / jquery nella renderfunzione di una vista per modificare manualmente tutti i dati nella vista. Per renderlo più estremo, non hai nemmeno bisogno di una renderfunzione specifica . Potresti avere una funzione chiamata renderFirstNameche aggiorna il nome nel dom e renderLastNameche aggiorna il cognome nel dom. Se avessi adottato questo approccio, sarebbe stato molto meglio in termini di prestazioni e non avresti mai più dovuto delegare manualmente gli eventi. Il codice avrebbe anche un senso totale per qualcuno che lo leggesse (anche se sarebbe un codice più lungo / più disordinato).

Tuttavia, di solito non vi è alcun aspetto negativo nell'utilizzare i modelli e semplicemente distruggere e ricostruire l'intera vista e le sue visualizzazioni secondarie su ogni chiamata di rendering, in quanto non è nemmeno venuto in mente all'interrogante di fare diversamente. Questo è ciò che la maggior parte delle persone fa praticamente per ogni situazione in cui si imbattono. Ed è per questo che i framework supponente rendono questo comportamento predefinito.


0

È anche possibile iniettare le visualizzazioni secondarie renderizzate come variabili nel modello principale come variabili.

per prima cosa esegui il rendering delle visualizzazioni secondarie e convertile in HTML in questo modo:

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

(in questo modo potresti anche concatenare dinamicamente le viste come subview1 + subview2quando usato nei loop) e quindi passarlo al modello principale che assomiglia a questo: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

e iniettarlo finalmente in questo modo:

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

Per quanto riguarda gli eventi all'interno delle visualizzazioni secondarie: molto probabilmente dovranno essere collegati nel genitore (masterView) con questo approccio non all'interno delle visualizzazioni secondarie.


0

Mi piace usare il seguente approccio che assicuri anche di rimuovere correttamente le viste figlio. Ecco un esempio del libro di 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

Non è necessario delegare nuovamente gli eventi in quanto è costoso. Vedi sotto:

    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);            
    }
});
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.