Cara membuat dan menambahkan sub-tampilan di Backbone.js

133

Saya memiliki pengaturan nested-View yang bisa agak dalam di aplikasi saya. Ada banyak cara yang bisa saya pikirkan untuk menginisialisasi, merender, dan menambahkan sub-view, tapi saya bertanya-tanya apa praktik umum itu.

Berikut adalah beberapa yang pernah saya pikirkan:

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: Anda tidak perlu khawatir tentang mempertahankan pesanan DOM yang tepat dengan menambahkan. Tampilan diinisialisasi sejak awal, jadi tidak banyak yang bisa dilakukan sekaligus dalam fungsi render.

Kekurangan: Anda dipaksa untuk mendelegasikan kembali Acara (), yang mungkin mahal? Fungsi render tampilan orangtua berantakan dengan semua rendering subview yang perlu terjadi? Anda tidak memiliki kemampuan untuk mengatur tagNameelemen-elemen, sehingga templat perlu mempertahankan tagNames yang benar.

Cara lain:

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: Anda tidak perlu mendelegasikan kembali acara. Anda tidak perlu templat yang hanya berisi tempat penampung kosong dan tagName Anda sudah kembali ditentukan oleh tampilan.

Cons: Anda sekarang harus memastikan untuk menambahkan hal-hal dalam urutan yang benar. Render tampilan orangtua masih berantakan oleh rendering subview.

Dengan sebuah onRenderacara:

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: Logika subview sekarang dipisahkan dari metode tampilan render().

Dengan sebuah onRenderacara:

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

Saya telah mencampur dan mencocokkan sekelompok praktik berbeda di semua contoh ini (sangat menyesal tentang hal itu), tetapi apa saja yang akan Anda pertahankan atau tambahkan? dan apa yang tidak akan kamu lakukan?

Ringkasan praktik:

  • Instantiate subview di initializeatau di render?
  • Melakukan semua logika render tampilan sub-view di renderatau di onRender?
  • Gunakan setElementatau append/appendTo?
Ian Storm Taylor
sumber
Saya akan berhati-hati tentang yang baru tanpa menghapus, ada kebocoran memori di sana.
vimdude
1
Jangan khawatir, saya punya closemetode dan metode onCloseyang membersihkan anak-anak, tetapi saya hanya ingin tahu tentang cara membuat instance dan membuat mereka di tempat pertama.
Ian Storm Taylor
3
@abdelsaid: Dalam JavaScript, GC menangani deallokasi memori. deletedi JS tidak sama dengan deletedari C ++. Kata kunci dengan nama sangat buruk jika Anda bertanya kepada saya.
Mike Bailey
@ MikeBantegui mendapatkannya tetapi sama seperti di java kecuali bahwa di JS untuk membebaskan memori Anda hanya perlu menetapkan null. Untuk memperjelas apa yang saya maksud, coba ini buat lingkaran dengan objek baru di dalam dan monitor memori. Tentu saja GC akan sampai ke sana tetapi Anda akan kehilangan ingatan sebelum sampai ke sana. Dalam hal ini Render yang mungkin dipanggil berkali-kali.
vimdude
3
Saya seorang pengembang Backbone pemula. Adakah yang bisa menjelaskan mengapa contoh 1 memaksa kita untuk mendelegasikan kembali acara? (Atau haruskah saya menanyakan hal ini dalam pertanyaannya sendiri?) Terima kasih.
pilau

Jawaban:

58

Saya biasanya melihat / menggunakan beberapa solusi berbeda:

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

Ini mirip dengan contoh pertama Anda, dengan beberapa perubahan:

  1. Urutan tempat Anda menambahkan sub elemen penting
  2. Tampilan luar tidak mengandung elemen html yang akan ditetapkan pada tampilan dalam (artinya Anda masih dapat menentukan tagName di tampilan dalam)
  3. render()disebut SETELAH elemen tampilan dalam telah ditempatkan ke dalam DOM, yang membantu jika render()metode tampilan dalam Anda menempatkan / mengukur sendiri pada halaman berdasarkan posisi / ukuran elemen lain (yang merupakan kasus penggunaan umum, menurut pengalaman saya)

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

Solusi 2 mungkin terlihat lebih bersih, tetapi telah menyebabkan beberapa hal aneh dalam pengalaman saya dan telah mempengaruhi kinerja secara negatif.

Saya biasanya menggunakan Solusi 1, karena beberapa alasan:

  1. Banyak pandangan saya mengandalkan sudah berada di DOM dalam render()metode mereka
  2. Ketika tampilan luar dirender kembali, tampilan tidak harus diinisialisasi ulang, inisialisasi ulang yang dapat menyebabkan kebocoran memori dan juga menyebabkan masalah aneh dengan binding yang ada

Perlu diingat bahwa jika Anda menginisialisasi new View()setiap kali render()dipanggil, inisialisasi itu akan delegateEvents()tetap memanggil . Jadi itu tidak harus menjadi "penipu", seperti yang telah Anda ungkapkan.

Lukas
sumber
1
Tidak satu pun dari solusi ini yang bekerja di pohon tampilan sub memanggil View.remove, yang mungkin penting dalam melakukan pembersihan kustom dalam tampilan, yang jika tidak akan mencegah pengumpulan sampah
Dominic
31

Ini adalah masalah abadi dengan Backbone dan, dalam pengalaman saya, tidak ada jawaban yang memuaskan untuk pertanyaan ini. Saya berbagi rasa frustrasi Anda, terutama karena ada sedikit panduan meskipun betapa umum kasus penggunaan ini. Yang mengatakan, saya biasanya pergi dengan sesuatu yang mirip dengan contoh kedua Anda.

Pertama-tama, saya akan mengabaikan apa pun yang mengharuskan Anda untuk mendelegasikan kembali acara. Model tampilan yang digerakkan oleh acara Backbone adalah salah satu komponen yang paling penting, dan kehilangan fungsionalitas itu hanya karena aplikasi Anda non-sepele akan meninggalkan rasa tidak enak di mulut programmer mana pun. Jadi goreskan nomor satu.

Mengenai contoh ketiga Anda, saya pikir ini hanyalah akhir-run sekitar praktik rendering konvensional dan tidak menambah banyak makna. Mungkin jika Anda melakukan pemicu peristiwa aktual (yaitu, bukan " onRender" acara yang dibuat-buat ), akan lebih baik jika hanya mengikat acara renderitu sendiri. Jika Anda merasa rendermenjadi sulit dan rumit, Anda memiliki terlalu sedikit subview.

Kembali ke contoh kedua Anda, yang mungkin lebih kecil dari tiga kejahatan. Berikut adalah contoh kode yang diambil dari Recipes With Backbone , ditemukan pada halaman 42 dari edisi PDF saya:

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

Ini hanya pengaturan yang sedikit lebih canggih daripada contoh kedua Anda: mereka menentukan satu set fungsi, addAlldan addOne, yang melakukan pekerjaan kotor. Saya pikir pendekatan ini bisa diterapkan (dan tentu saja saya menggunakannya); tapi itu masih meninggalkan aftertaste yang aneh. (Maafkan semua metafora lidah ini.)

Sampai pada titik Anda menambahkan dalam urutan yang benar: jika Anda benar-benar menambahkan, tentu saja itu adalah batasan. Tetapi pastikan Anda mempertimbangkan semua skema templating yang mungkin. Mungkin Anda sebenarnya ingin elemen placeholder (mis., Kosong divatau ul) yang kemudian Anda dapat replaceWithelemen (DOM) baru yang memegang subview yang sesuai. Menambahkan bukan satu-satunya solusi, dan Anda pasti bisa mengatasi masalah pemesanan jika Anda sangat peduli, tetapi saya akan membayangkan Anda memiliki masalah desain jika itu membuat Anda tersandung. Ingat, subview dapat memiliki subview, dan harus jika pantas. Dengan begitu, Anda memiliki struktur mirip pohon, yang cukup bagus: setiap subview menambahkan semua subview, secara berurutan, sebelum tampilan induk menambahkan yang lain, dan seterusnya.

Sayangnya, solusi # 2 mungkin adalah yang terbaik yang dapat Anda harapkan untuk menggunakan Backbone out-of-the-box. Jika Anda tertarik untuk memeriksa perpustakaan pihak ketiga, salah satu yang telah saya lihat (tetapi belum benar-benar memiliki waktu untuk bermain) adalah Backbone.LayoutManager , yang tampaknya memiliki metode yang lebih sehat untuk menambahkan subview. Namun, bahkan mereka telah memiliki perdebatan baru - baru ini tentang masalah yang serupa dengan ini.

Josh Leitzel
sumber
4
Garis kedua dari belakang - model.bind('remove', view.remove);- bukankah seharusnya Anda hanya melakukan itu dalam fungsi inisialisasi Pengangkatan untuk menjaga mereka terpisah?
atp
2
Bagaimana ketika suatu tampilan tidak dapat di-instantiasi kembali setiap kali itu dibuat oleh orang tua karena itu mempertahankan keadaan?
mor
Hentikan semua kegilaan ini dan gunakan saja plugin Backbone.subviews !
Berani Dave
6

Terkejut ini belum disebutkan, tapi saya serius mempertimbangkan untuk menggunakan Marionette .

Ini memaksa struktur sedikit lebih untuk aplikasi Backbone, termasuk tampilan tertentu jenis ( ListView, ItemView, Regiondan Layout), menambahkan tepat Controllerdan banyak lagi.

Ini adalah proyek tentang Github dan panduan hebat dari Addy Osmani dalam buku Backbone Fundamentals untuk membantu Anda memulai.

Dana Woodman
sumber
3
Ini tidak menjawab pertanyaan.
Ceasar Bautista
2
@CeasarBautista Saya tidak membahas cara menggunakan Marionette untuk menyelesaikan ini, tetapi Marionette memang memecahkan masalah di atas
Dana Woodman
4

Saya memiliki, apa yang saya yakini, solusi yang cukup komprehensif untuk masalah ini. Ini memungkinkan model dalam koleksi untuk berubah, dan hanya tampilan yang dirender ulang (bukan seluruh koleksi). Ini juga menangani penghapusan tampilan zombie melalui metode 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");
        });
    }
});

Pemakaian:

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

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

Lihat mixin ini untuk membuat dan merender subview:

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

Ini adalah solusi minimalis yang mengatasi banyak masalah yang dibahas dalam utas ini, termasuk rendering order, tidak harus mendelegasikan kembali acara, dll. Perhatikan bahwa kasus tampilan koleksi (di mana setiap model dalam koleksi diwakili dengan satu Subview) adalah topik yang berbeda. Solusi umum terbaik yang saya ketahui tentang hal ini adalah CollectionView di Marionette .

Berani Dave
sumber
0

Saya tidak terlalu menyukai solusi di atas. Saya lebih suka untuk konfigurasi ini daripada setiap tampilan harus secara manual melakukan pekerjaan dalam metode render.

  • views bisa berupa fungsi atau objek yang mengembalikan objek definisi tampilan
  • Ketika orang tua .removedipanggil, the.remove anak-anak yang bersarang dari urutan paling bawah harus dipanggil (sepanjang jalan dari tampilan sub-sub-sub)
  • Secara default, tampilan induk melewati model dan koleksinya sendiri, tetapi opsi dapat ditambahkan dan diganti.

Ini sebuah contoh:

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

Backbone sengaja dibangun sehingga tidak ada praktik "umum" dalam hal ini dan banyak masalah lainnya. Ini dimaksudkan untuk tidak dibuka sebanyak mungkin. Secara teoritis, Anda bahkan tidak perlu menggunakan templat dengan Backbone. Anda bisa menggunakan javascript / jquery dalam renderfungsi tampilan untuk secara manual mengubah semua data dalam tampilan. Untuk membuatnya lebih ekstrem, Anda bahkan tidak perlu satu renderfungsi khusus . Anda bisa memiliki fungsi yang disebut renderFirstNameyang memperbarui nama depan di dom danrenderLastName yang memperbarui nama belakang di dom. Jika Anda mengambil pendekatan ini, akan jauh lebih baik dalam hal kinerja dan Anda tidak akan pernah harus mendelegasikan acara secara manual lagi. Kode tersebut juga akan masuk akal bagi seseorang yang membacanya (meskipun akan lebih panjang / berantakan).

Namun, biasanya tidak ada kerugian untuk menggunakan template dan hanya menghancurkan dan membangun kembali seluruh tampilan dan subview pada setiap panggilan render, karena bahkan tidak terpikir oleh penanya untuk melakukan apa pun sebaliknya. Jadi itulah yang dilakukan kebanyakan orang untuk hampir setiap situasi yang mereka temui. Dan itulah mengapa kerangka kerja yang dikemukakan hanya menjadikan ini perilaku default.

Nick Manning
sumber
0

Anda juga bisa menyuntikkan subview yang diberikan sebagai variabel ke template utama sebagai variabel.

pertama-tama render subview dan konversikan ke html seperti ini:

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

(dengan begitu Anda juga bisa secara dinamis merangkai tampilan seperti subview1 + subview2ketika digunakan dalam loop) dan kemudian meneruskannya ke template induk yang terlihat seperti ini: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

dan menyuntikkannya akhirnya seperti ini:

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

Mengenai Acara dalam subview: Mereka kemungkinan besar harus terhubung di induk (masterView) dengan pendekatan ini tidak dalam subview.

B Piltz
sumber
0

Saya suka menggunakan pendekatan berikut yang juga memastikan untuk menghapus pandangan anak dengan benar. Ini adalah contoh dari buku karya 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
sumber
0

Tidak perlu mendelegasikan kembali acara karena mahal. Lihat di bawah:

    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);            
    }
});
sooshi joshi
sumber