Cách kết xuất và nối các khung nhìn phụ trong Backbone.js


133

Tôi có một thiết lập View-lồng nhau có thể nhận được phần nào sâu trong ứng dụng của tôi. Có rất nhiều cách tôi có thể nghĩ đến khi khởi tạo, kết xuất và nối thêm các khung nhìn phụ, nhưng tôi tự hỏi không biết thực tiễn phổ biến là gì.

Đây là một cặp vợ chồng tôi đã nghĩ đến:

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

Ưu điểm: Bạn không phải lo lắng về việc duy trì đúng thứ tự DOM khi nối thêm. Các khung nhìn được khởi tạo sớm, vì vậy không có nhiều việc phải làm cùng một lúc trong chức năng kết xuất.

Nhược điểm: Bạn buộc phải ủy quyền lạiEvents (), có thể tốn kém? Chức năng kết xuất của khung nhìn cha bị lộn xộn với tất cả kết xuất của khung nhìn phụ cần phải xảy ra? Bạn không có khả năng thiết lập tagNamecác yếu tố, vì vậy mẫu cần duy trì tên thẻ chính xác.

Cách khác:

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

Ưu điểm: Bạn không phải ủy quyền lại các sự kiện. Bạn không cần một mẫu chỉ chứa các phần giữ chỗ trống và tagName của bạn quay lại được xác định bởi chế độ xem.

Nhược điểm: Bây giờ bạn phải đảm bảo nối các thứ theo đúng thứ tự. Kết xuất của khung nhìn cha vẫn bị lộn xộn bởi kết xuất khung nhìn phụ.

Với một onRendersự kiện:

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

Ưu điểm: Logic của khung nhìn phụ hiện được tách ra khỏi render()phương thức của khung nhìn .

Với một onRendersự kiện:

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

Tôi đã kết hợp và kết hợp một loạt các thực tiễn khác nhau trong tất cả các ví dụ này (rất xin lỗi về điều đó) nhưng đâu là những cách bạn sẽ giữ hoặc thêm? và bạn sẽ không làm gì?

Tóm tắt thực hành:

  • Khởi tạo các cuộc phỏng vấn trong initializehoặc trong render?
  • Thực hiện tất cả logic kết xuất khung nhìn phụ trong renderhoặc trong onRender?
  • Sử dụng setElementhay append/appendTo?

Tôi sẽ cẩn thận về cái mới mà không xóa, bạn bị rò rỉ bộ nhớ trong đó.
vimdude

1
Đừng lo lắng, tôi có một closephương pháp và một cách onCloselàm sạch trẻ em, nhưng tôi chỉ tò mò về cách khởi tạo và kết xuất chúng ngay từ đầu.
Ian Storm Taylor

3
@abdelsaid: Trong JavaScript, GC xử lý việc phân bổ bộ nhớ. deletetrong JS không giống như deletetừ C ++. Đó là một từ khóa được đặt tên rất kém nếu bạn hỏi tôi.
Mike Bailey

@MikeBantegui có nó nhưng nó giống như trong java ngoại trừ trong JS để giải phóng bộ nhớ, bạn chỉ cần gán null. Để làm rõ ý tôi là gì, hãy thử điều này tạo một vòng lặp với một đối tượng mới bên trong và theo dõi bộ nhớ. Tất nhiên GC sẽ nhận được nó nhưng bạn sẽ mất bộ nhớ trước khi nó đến được nó. Trong trường hợp này Render có thể được gọi nhiều lần.
vimdude

3
Tôi là một nhà phát triển xương sống mới làm quen. Bất cứ ai có thể giải thích tại sao ví dụ 1 buộc chúng ta phải ủy quyền lại các sự kiện? (Hoặc tôi nên hỏi điều này trong câu hỏi của chính nó?) Cảm ơn.
pilau

Câu trả lời:


58

Tôi thường thấy / sử dụng một vài giải pháp khác nhau:

Giải pháp 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();
    }
});

Điều này tương tự với ví dụ đầu tiên của bạn, với một vài thay đổi:

  1. Thứ tự mà bạn nối các yếu tố phụ quan trọng
  2. Chế độ xem bên ngoài không chứa các thành phần html được đặt trên (các) chế độ xem bên trong (có nghĩa là bạn vẫn có thể chỉ định tagName trong chế độ xem bên trong)
  3. render()được gọi là SAU phần tử của chế độ xem bên trong đã được đặt vào DOM, rất hữu ích nếu render()phương thức của chế độ xem bên trong của bạn đang đặt / định cỡ trên trang dựa trên vị trí / kích thước của các phần tử khác (theo trường hợp sử dụng phổ biến, theo kinh nghiệm của tôi)

Giải pháp 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);
    }
});

Giải pháp 2 có thể trông sạch hơn, nhưng nó đã gây ra một số điều kỳ lạ trong trải nghiệm của tôi và ảnh hưởng tiêu cực đến hiệu suất.

Tôi thường sử dụng Giải pháp 1, vì một vài lý do:

  1. Rất nhiều quan điểm của tôi dựa vào việc đã có trong DOM trong render()phương thức của họ
  2. Khi chế độ xem bên ngoài được hiển thị lại, các chế độ xem không phải được khởi tạo lại, việc khởi tạo lại có thể gây rò rỉ bộ nhớ và cũng gây ra các vấn đề kỳ quặc với các ràng buộc hiện có

Hãy nhớ rằng nếu bạn đang khởi tạo new View()mỗi lần render()được gọi, thì việc khởi tạo đó sẽ được gọi bằng delegateEvents()mọi cách. Vì vậy, đó không nhất thiết phải là một "con", như bạn đã bày tỏ.


1
Cả hai giải pháp này đều hoạt động trên cây xem phụ, gọi View.remove, có thể rất quan trọng trong việc dọn dẹp tùy chỉnh trong chế độ xem, điều này sẽ ngăn chặn việc thu gom rác
Dominic

31

Đây là một vấn đề lâu năm với Backbone và, theo kinh nghiệm của tôi, thực sự không có câu trả lời thỏa mãn cho câu hỏi này. Tôi chia sẻ sự thất vọng của bạn, đặc biệt là vì có rất ít hướng dẫn mặc dù trường hợp sử dụng này phổ biến như thế nào. Điều đó nói rằng, tôi thường đi với một cái gì đó giống với ví dụ thứ hai của bạn.

Trước hết, tôi sẽ loại bỏ mọi thứ đòi hỏi bạn phải ủy quyền lại các sự kiện. Mô hình khung nhìn hướng sự kiện là một trong những thành phần quan trọng nhất của nó và để mất chức năng đó đơn giản vì ứng dụng của bạn không tầm thường sẽ để lại mùi vị khó chịu trong miệng của bất kỳ lập trình viên nào. Vì vậy, cào số một.

Về ví dụ thứ ba của bạn, tôi nghĩ rằng nó chỉ là một kết thúc xung quanh thực hành kết xuất thông thường và không thêm nhiều ý nghĩa. Có lẽ nếu bạn đang thực hiện kích hoạt sự kiện thực tế (nghĩa là không phải là một onRendersự kiện " " giả định ), thì sẽ đáng để ràng buộc những sự kiện đó với renderchính nó. Nếu bạn tìm thấyrender trở nên khó sử dụng và phức tạp, bạn có quá ít cuộc phỏng vấn.

Quay lại ví dụ thứ hai của bạn, có lẽ là ít tệ hơn trong ba tệ nạn. Dưới đây là mã ví dụ được lấy từ Recipes With Backbone , được tìm thấy trên trang 42 của phiên bản PDF của tôi:

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

Đây chỉ là một thiết lập phức tạp hơn một chút so với ví dụ thứ hai của bạn: họ chỉ định một tập hợp các hàm addAlladdOne thực hiện công việc bẩn thỉu. Tôi nghĩ rằng phương pháp này là khả thi (và tôi chắc chắn sử dụng nó); nhưng nó vẫn để lại dư vị kỳ quái. (Xin tha thứ tất cả những ẩn dụ lưỡi này.)

Theo quan điểm của bạn về việc nối thêm đúng thứ tự: nếu bạn thực sự bổ sung, chắc chắn, đó là một hạn chế. Nhưng hãy chắc chắn rằng bạn xem xét tất cả các chương trình tạo khuôn có thể. Có lẽ bạn thực sự thích một yếu tố giữ chỗ (ví dụ: một sản phẩm trống divhoặc ul) mà sau đó bạn có thể là replaceWithmột yếu tố (DOM) mới chứa các cuộc phỏng vấn thích hợp. Áp dụng không phải là giải pháp duy nhất và bạn chắc chắn có thể giải quyết vấn đề đặt hàng nếu bạn quan tâm đến vấn đề đó, nhưng tôi sẽ tưởng tượng bạn có vấn đề về thiết kế nếu nó làm bạn vấp ngã. Hãy nhớ rằng, các cuộc phỏng vấn có thể có các cuộc phỏng vấn, và họ nên nếu nó phù hợp. Bằng cách đó, bạn có một cấu trúc khá giống cây, khá đẹp: mỗi khung nhìn phụ thêm tất cả các khung nhìn của nó, theo thứ tự, trước khi khung nhìn cha thêm một khung hình khác, v.v.

Thật không may, giải pháp số 2 có lẽ là giải pháp tốt nhất mà bạn có thể hy vọng để sử dụng Backbone ngoài luồng. Nếu bạn quan tâm đến việc kiểm tra các thư viện của bên thứ ba, một thư viện mà tôi đã xem xét (nhưng thực sự chưa có thời gian để chơi) là Backbone.LayoutManager , dường như có một phương pháp bổ sung các cuộc phỏng vấn lành mạnh hơn. Tuy nhiên, thậm chí họ đã có những cuộc tranh luận gần đây về các vấn đề tương tự như vậy.


4
Dòng áp chót - model.bind('remove', view.remove);- bạn không nên làm điều đó trong chức năng khởi tạo của Cuộc hẹn để tách chúng ra sao?
atp

2
Thế còn khi một khung nhìn không thể được khởi tạo lại mỗi lần nó hiển thị bởi vì nó giữ trạng thái thì sao?
mor

Dừng tất cả sự điên rồ này và chỉ cần sử dụng plugin Backbone.subview !
Dũng cảm Dave

6

Ngạc nhiên là điều này chưa được đề cập, nhưng tôi nghiêm túc xem xét sử dụng Marionette .

Nó thực thi cấu trúc nhiều hơn một chút để ứng dụng Backbone, trong đó có cái nhìn cụ thể loại ( ListView, ItemView, RegionLayout), thêm đúng Controllers và nhiều hơn nữa.

Đây là dự án trên Github và một hướng dẫn tuyệt vời của Addy Osmani trong cuốn sách Backbone Fund basicals để giúp bạn bắt đầu.


3
Điều này không trả lời câu hỏi.
Ceasar Bautista

2
@CeasarBautista Tôi không đi vào cách sử dụng Marionette để thực hiện điều này nhưng Marionette thực sự giải quyết được vấn đề trên
Dana Woodman

4

Tôi có, những gì tôi tin là, một giải pháp khá toàn diện cho vấn đề này. Nó cho phép một mô hình trong bộ sưu tập thay đổi và chỉ hiển thị lại khung nhìn của nó (chứ không phải toàn bộ bộ sưu tập). Nó cũng xử lý loại bỏ các khung nhìn zombie thông qua các phương thức 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");
        });
    }
});

Sử dụng:

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

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

2

Kiểm tra mixin này để tạo và hiển thị các cuộc phỏng vấn:

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

Đó là một giải pháp tối giản giải quyết rất nhiều vấn đề được thảo luận trong luồng này, bao gồm thứ tự kết xuất, không phải ủy quyền lại các sự kiện, v.v ... Lưu ý rằng trường hợp của chế độ xem bộ sưu tập (trong đó mỗi mô hình trong bộ sưu tập được thể hiện bằng một subview) là một chủ đề khác nhau. Giải pháp chung tốt nhất mà tôi biết đối với trường hợp đó là CollectionView trong Marionette .


0

Tôi không thực sự thích bất kỳ giải pháp nào ở trên. Tôi thích cấu hình này hơn mỗi chế độ xem phải thực hiện thủ công trong phương thức kết xuất.

  • views có thể là một hàm hoặc đối tượng trả về một đối tượng của các định nghĩa khung nhìn
  • Khi cha mẹ .removeđược gọi,.remove đứa trẻ lồng nhau từ thứ tự thấp nhất trở lên (tất cả các cách từ chế độ xem phụ phụ)
  • Theo mặc định, chế độ xem chính vượt qua mô hình và bộ sưu tập của riêng nó, nhưng các tùy chọn có thể được thêm và ghi đè.

Đây là một ví dụ:

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

Xương sống được xây dựng có chủ ý để không có thông lệ "chung" liên quan đến vấn đề này và nhiều vấn đề khác. Nó có nghĩa là càng không bị ảnh hưởng càng tốt. Về mặt lý thuyết, bạn thậm chí không phải sử dụng các mẫu với Backbone. Bạn có thể sử dụng javascript / jquery trong renderchức năng của chế độ xem để thay đổi thủ công tất cả dữ liệu trong chế độ xem. Để làm cho nó cực hơn, bạn thậm chí không cần một renderchức năng cụ thể . Bạn có thể có một hàm được gọi là renderFirstNamecập nhật tên đầu tiên trong dom và renderLastNamecập nhật tên cuối cùng trong dom. Nếu bạn thực hiện phương pháp này, nó sẽ tốt hơn về mặt hiệu suất và bạn sẽ không bao giờ phải ủy thác các sự kiện một cách thủ công nữa. Mã cũng sẽ hoàn toàn có ý nghĩa với ai đó đọc nó (mặc dù nó sẽ là mã dài hơn / lộn xộn hơn).

Tuy nhiên, thường thì không có nhược điểm nào trong việc sử dụng các mẫu và chỉ đơn giản là phá hủy và xây dựng lại toàn bộ chế độ xem và đó là các cuộc phỏng vấn trên mỗi cuộc gọi kết xuất, vì nó thậm chí không xảy ra với người hỏi để làm gì khác. Vì vậy, đó là những gì hầu hết mọi người làm cho hầu hết mọi tình huống họ gặp phải. Và đó là lý do tại sao các khung công tác chỉ làm cho điều này trở thành hành vi mặc định.


0

Bạn cũng có thể đưa các phỏng vấn được kết xuất dưới dạng các biến vào mẫu chính dưới dạng các biến.

đầu tiên kết xuất các bài phỏng vấn và chuyển đổi chúng thành html như thế này:

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

(theo cách đó, bạn cũng có thể tự động xâu chuỗi các khung nhìn như subview1 + subview2khi được sử dụng trong các vòng lặp) và sau đó chuyển nó vào khuôn mẫu chính trông như thế này: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

và tiêm nó cuối cùng như thế này:

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

Về các Sự kiện trong các cuộc phỏng vấn: Rất có thể chúng sẽ phải được kết nối trong cha mẹ (masterView) với cách tiếp cận này không nằm trong các cuộc phỏng vấn.


0

Tôi thích sử dụng cách tiếp cận sau đây cũng đảm bảo loại bỏ các khung nhìn con đúng cách. Dưới đây là một ví dụ từ cuốn sách của 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

Không cần phải ủy quyền lại các sự kiện vì nó rất tốn kém. Xem bên dưới:

    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);            
    }
});
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.