Jak renderować i dołączać widoki podrzędne w Backbone.js

133

Mam konfigurację zagnieżdżonego widoku, która może nieco zagłębić się w mojej aplikacji. Jest kilka sposobów na inicjalizację, renderowanie i dołączanie widoków podrzędnych, ale zastanawiam się, jaka jest powszechna praktyka.

Oto kilka, o których myślałem:

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

Zalety: nie musisz martwić się o utrzymanie właściwej kolejności DOM podczas dołączania. Widoki są inicjalizowane wcześnie, więc funkcja renderująca nie ma tak wiele do zrobienia na raz.

Wady: jesteś zmuszony do ponownego delegowania wydarzeń (), co może być kosztowne? Funkcja renderowania widoku nadrzędnego jest zaśmiecona całym renderowaniem widoku podrzędnego, które musi nastąpić? Nie masz możliwości ustawienia tagNameelementów, więc szablon musi utrzymywać poprawne nazwy tagów.

Inny sposób:

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

Zalety: nie musisz ponownie delegować wydarzeń. Nie potrzebujesz szablonu, który zawiera tylko puste symbole zastępcze, a Twoje tagName są ponownie definiowane przez widok.

Wady: Musisz teraz upewnić się, że dodałeś rzeczy we właściwej kolejności. Renderowanie widoku nadrzędnego jest nadal zaśmiecone przez renderowanie widoku podrzędnego.

W przypadku onRenderwydarzenia:

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

Zalety: logika widoku podrzędnego jest teraz oddzielona od metody widoku render().

W przypadku onRenderwydarzenia:

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

W pewnym sensie wymieszałem i dopasowałem kilka różnych praktyk we wszystkich tych przykładach (przepraszam za to), ale jakie są te, które chciałbyś zachować lub dodać? a czego byś nie zrobił?

Podsumowanie praktyk:

  • Utworzyć instancję podglądów podrzędnych w initializelub w render?
  • Wykonać całą logikę renderowania widoku podrzędnego w renderlub w onRender?
  • Użyj setElementlub append/appendTo?
Ian Storm Taylor
źródło
Byłbym ostrożny z nowym bez usuwania, masz tam wyciek pamięci.
vimdude
1
Nie martw się, mam closemetodę i narzędzie, onClosektóre czyści dzieci, ale jestem po prostu ciekawy, jak je utworzyć i renderować.
Ian Storm Taylor,
3
@abdelsaid: W JavaScript, GC obsługuje zwalnianie pamięci. deletew JS to nie to samo, co deletez C ++. Jeśli o mnie chodzi, jest to bardzo słabo nazwane słowo kluczowe.
Mike Bailey
@MikeBantegui ma to, ale jest to to samo, co w javie, z wyjątkiem tego, że w JS, aby zwolnić pamięć, wystarczy przypisać null. Aby wyjaśnić, o co mi chodzi, spróbuj tego utworzyć pętlę z nowym obiektem w środku i monitorować pamięć. Oczywiście GC się do tego dostanie, ale stracisz pamięć, zanim to zrobi. W tym przypadku Render, który może być wywoływany wiele razy.
vimdude
3
Jestem początkującym programistą Backbone. Czy ktoś może wyjaśnić, dlaczego przykład 1 zmusza nas do ponownego delegowania wydarzeń? (A może powinienem zadać to w swoim własnym pytaniu?) Dzięki.
pilau

Odpowiedzi:

58

Ogólnie widziałem / stosowałem kilka różnych rozwiązań:

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

Jest to podobne do twojego pierwszego przykładu, z kilkoma zmianami:

  1. Kolejność, w jakiej dodajesz elementy podrzędne, ma znaczenie
  2. Widok zewnętrzny nie zawiera elementów html, które można ustawić w widokach wewnętrznych (co oznacza, że ​​nadal można określić tagName w widoku wewnętrznym)
  3. render()nazywa się PO umieszczeniu elementu widoku wewnętrznego w DOM, co jest pomocne, jeśli render()metoda Twojego widoku wewnętrznego polega na umieszczeniu / zmianie rozmiaru na stronie w oparciu o pozycję / rozmiar innych elementów (co jest częstym przypadkiem użycia, z mojego doświadczenia)

Rozwiązanie 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);
    }
});

Rozwiązanie 2 może wyglądać na czystsze, ale z mojego doświadczenia wynika, że ​​spowodowało to dziwne rzeczy i miało negatywny wpływ na wydajność.

Generalnie używam rozwiązania 1 z kilku powodów:

  1. Wiele moich poglądów polega na tym, że w swojej render()metodzie już jestem w DOM
  2. Gdy widok zewnętrzny jest ponownie renderowany, widoki nie muszą być ponownie inicjowane, co może powodować wycieki pamięci, a także powodować dziwaczne problemy z istniejącymi powiązaniami

Pamiętaj, że jeśli inicjalizujesz a przy new View()każdym render()wywołaniu, inicjalizacja i delegateEvents()tak zadzwoni . Więc to niekoniecznie musi być „oszustwem”, jak to wyraziłeś.

Lukas
źródło
1
Żadne z tych rozwiązań nie działa w drzewie widoku podrzędnego wywołującego View.remove, co może być kluczowe przy wykonywaniu niestandardowego czyszczenia w widoku, co w przeciwnym razie uniemożliwiłoby zbieranie śmieci
Dominic
31

Jest to odwieczny problem z Backbone iz mojego doświadczenia wynika, że ​​nie ma satysfakcjonującej odpowiedzi na to pytanie. Podzielam twoją frustrację, zwłaszcza, że ​​pomimo tego, jak powszechny jest ten przypadek użycia, jest tak mało wskazówek. To powiedziawszy, zwykle idę z czymś podobnym do twojego drugiego przykładu.

Przede wszystkim odrzuciłbym z ręki wszystko, co wymaga ponownego delegowania wydarzeń. Model widoku sterowanego zdarzeniami w Backbone jest jednym z jego najważniejszych elementów, a utrata tej funkcjonalności tylko dlatego, że aplikacja jest nietrywialna, pozostawiłaby zły smak w ustach każdego programisty. Więc zera numer jeden.

Jeśli chodzi o twój trzeci przykład, myślę, że to tylko koniec z konwencjonalną praktyką renderowania i nie dodaje zbyt wiele znaczenia. Być może, jeśli wykonujesz rzeczywiste wyzwalanie zdarzenia (tj. Nie jest to " onRender" wymyślone zdarzenie), warto byłoby po prostu powiązać te zdarzenia ze rendersobą. Jeśli znajdzieszrender się nieporęczny i złożony, masz zbyt mało podglądów podrzędnych.

Wróćmy do drugiego przykładu, który jest prawdopodobnie mniejszym z trzech rodzajów zła. Oto przykładowy kod pobrany z Recipes With Backbone , znajdujący się na stronie 42 mojego wydania 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);
}

Jest to tylko nieco bardziej wyrafinowana konfiguracja niż twój drugi przykład: określają zestaw funkcji addAlli addOnewykonują brudną robotę. Myślę, że to podejście jest wykonalne (iz pewnością go stosuję); ale nadal pozostawia dziwny posmak. (Wybacz wszystkie te metafory językowe).

Do twojego punktu dołączania we właściwej kolejności: jeśli dołączasz ściśle, to pewne, to jest ograniczenie. Ale upewnij się, że rozważasz wszystkie możliwe schematy szablonów. Być może faktycznie chciałbyś mieć element zastępczy (np. Pusty divlub ul), który możesz następnie replaceWithdodać do nowego elementu (DOM), który zawiera odpowiednie podglądy. Dołączanie nie jest jedynym rozwiązaniem iz pewnością możesz obejść problem z zamówieniem, jeśli tak bardzo ci na tym zależy, ale wyobrażam sobie, że masz problem z projektem, jeśli cię to potyka. Pamiętaj, podglądy mogą mieć podglądy i powinny, jeśli jest to stosowne. W ten sposób masz raczej drzewiastą strukturę, co jest całkiem przyjemne: każdy podwidok dodaje wszystkie swoje podglądy w kolejności, zanim widok nadrzędny doda kolejny i tak dalej.

Niestety, rozwiązanie nr 2 jest prawdopodobnie najlepszym, na jakie możesz liczyć w przypadku korzystania z gotowego do użycia Backbone. Jeśli jesteś zainteresowany sprawdzeniem bibliotek innych firm, jedną z nich zajrzałem (ale nie miałem jeszcze czasu na zabawę) jest Backbone.LayoutManager , który wydaje się mieć zdrowszą metodę dodawania podglądów podrzędnych. Jednak nawet oni mieli ostatnio debaty na podobne tematy.

Josh Leitzel
źródło
4
Przedostatni wiersz - model.bind('remove', view.remove);- czy nie powinieneś tego po prostu zrobić w funkcji inicjalizacji spotkania, aby je oddzielić?
ATP
2
A co w sytuacji, gdy nie można ponownie utworzyć wystąpienia widoku za każdym razem, gdy jego rodzic renderuje, ponieważ zachowuje stan?
jutro
Skończ z tym szaleństwem i po prostu użyj wtyczki Backbone.subviews !
Brave Dave
6

Zaskoczony, że nie zostało to jeszcze wspomniane, ale poważnie rozważałbym użycie Marionette .

To wymusza nieco więcej strukturę aplikacji kręgosłup, w tym szczególne widzenia typów ( ListView, ItemView, Regioni Layout), dodając odpowiednie ControllerS i wiele więcej.

Oto projekt na Github i świetny przewodnik autorstwa Addy Osmani w książce Backbone Fundamentals, na początek.

Dana Woodman
źródło
3
To nie odpowiada na pytanie.
Ceasar Bautista
2
@CeasarBautista Nie wchodzę w to, jak użyć Marionette, aby to osiągnąć, ale Marionette rzeczywiście rozwiązuje powyższy problem
Dana Woodman
4

Mam, jak sądzę, dość kompleksowe rozwiązanie tego problemu. Umożliwia zmianę modelu w kolekcji i ponowne renderowanie tylko jego widoku (zamiast całej kolekcji). Obsługuje również usuwanie widoków zombie za pomocą metod 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");
        });
    }
});

Stosowanie:

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

var PartListView = ViewCollection.extend({
    el: $("table#parts"),
    subViewClass: PartView
});
sarink
źródło
2

Sprawdź ten zestaw do tworzenia i renderowania podglądów podrzędnych:

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

Jest to minimalistyczne rozwiązanie, które rozwiązuje wiele kwestii omawianych w tym wątku, w tym kolejność renderowania, brak konieczności ponownego delegowania zdarzeń itp. Zwróć uwagę, że przypadek widoku kolekcji (gdzie każdy model w kolekcji jest reprezentowany przez jeden subview) to inny temat. Najlepszym znanym mi rozwiązaniem ogólnym w tym przypadku jest CollectionView w Marionette .

Odważny Dave
źródło
0

Nie podoba mi się żadne z powyższych rozwiązań. Wolę tę konfigurację niż każdy widok, który musi ręcznie wykonywać pracę w metodzie renderowania.

  • views może być funkcją lub obiektem zwracającym obiekt definicji widoku
  • Gdy .removewywoływana jest funkcja rodzica, .removenależy wywołać zagnieżdżone elementy podrzędne od najniższego rzędu w górę (aż do widoków podrzędnych)
  • Domyślnie widok nadrzędny przekazuje swój własny model i kolekcję, ale opcje można dodawać i zastępować.

Oto przykład:

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
źródło
0

Backbone został celowo zbudowany tak, aby nie było „powszechnej” praktyki w tej i wielu innych kwestiach. Ma to być jak najbardziej nieopinowane. Teoretycznie nie musisz nawet używać szablonów z Backbone. Możesz użyć javascript / jquery w renderfunkcji widoku, aby ręcznie zmienić wszystkie dane w widoku. Aby uczynić to bardziej ekstremalnym, nie potrzebujesz nawet jednej konkretnej renderfunkcji. Możesz mieć funkcję o nazwie, renderFirstNamektóra aktualizuje imię w domenie i renderLastNameaktualizuje nazwisko w dom. Gdybyś zastosował takie podejście, byłoby znacznie lepsze pod względem wydajności i nigdy nie musiałbyś ponownie ręcznie delegować wydarzeń. Kod miałby również sens dla kogoś, kto go czytał (chociaż byłby to dłuższy / bardziej niechlujny kod).

Jednak zwykle nie ma żadnych wad w używaniu szablonów i po prostu niszczeniu i przebudowywaniu całego widoku i jego podglądów podrzędnych przy każdym wywołaniu renderowania, ponieważ pytającemu nawet nie przyszło do głowy, aby zrobić coś innego. Więc to właśnie robi większość ludzi praktycznie w każdej sytuacji, z którą się spotykają. I dlatego uparte frameworki sprawiają, że jest to zachowanie domyślne.

Nick Manning
źródło
0

Możesz także wstawić wyrenderowane podwidoki jako zmienne do głównego szablonu jako zmienne.

najpierw wyrenderuj podglądy i przekonwertuj je na HTML w następujący sposób:

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

(w ten sposób można również dynamicznie łączyć widoki, tak jak subview1 + subview2w pętlach), a następnie przekazać je do szablonu głównego, który wygląda tak: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

i na koniec wstrzyknij to w ten sposób:

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

Odnośnie zdarzeń w widokach podrzędnych: Najprawdopodobniej będą one musiały być połączone w nadrzędnym (masterView) z tym podejściem, a nie w widokach podrzędnych.

B Piltz
źródło
0

Lubię stosować następujące podejście, które również zapewnia prawidłowe usuwanie widoków podrzędnych. Oto przykład z książki 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
źródło
0

Nie ma potrzeby ponownego delegowania wydarzeń, ponieważ jest to kosztowne. Zobacz poniżej:

    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
źródło