如何在Backbone.js中渲染和追加子视图

时间:2012-02-14 04:00:32

标签: javascript model-view-controller backbone.js

我有一个嵌套视图设置,可以在我的应用程序中获得一些深度。有很多方法我可以想到初始化,渲染和附加子视图,但我想知道常见的做法是什么。

以下是我想过的一对夫妇:

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

优点:您不必担心通过追加维护正确的DOM订单。视图在早期就被初始化了,因此在渲染函数中没有一次完成所有操作。

缺点:您被迫重新委托事件(),这可能代价高昂?父视图的渲染函数与所有需要发生的子视图渲染混杂在一起?您无法设置元素的tagName,因此模板需要维护正确的tagNames。

另一种方式:

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

优点:您无需重新委派活动。您不需要只包含空占位符的模板,并且您的tagName将返回到视图定义。

缺点:您现在必须确保以正确的顺序附加内容。子视图渲染仍然使父视图的渲染变得混乱。

onRender事件:

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

优点:子视图逻辑现在与视图的render()方法分开。

onRender事件:

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

我在所有这些示例中混合并匹配了许多不同的做法(很抱歉)但是你要保留或添加的是什么?你会做什么?

实践摘要:

  • initializerender
  • 中实例化子视图
  • renderonRender
  • 中执行所有子视图呈现逻辑
  • 使用setElementappend/appendTo

10 个答案:

答案 0 :(得分:58)

我一般看到/使用过几种不同的解决方案:

解决方案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();
    }
});

这与您的第一个示例相似,但有一些更改:

  1. 附加子元素的顺序很重要
  2. 外部视图不包含要在内部视图上设置的html元素(意味着您仍然可以在内部视图中指定tagName)
  3. 在内部视图的元素放入DOM之后调用
  4. render(),如果内部视图的render()方法基于其他元素的位置放置/调整页面大小,这将非常有用大小(根据我的经验,这是一个常见的用例)
  5. 解决方案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);
        }
    });
    

    解决方案2可能看起来更干净,但它在我的经验中引起了一些奇怪的事情并且对性能产生了负面影响。

    我通常使用解决方案1,原因有两个:

    1. 我的很多观点都依赖于他们render()方法
    2. 中的DOM
    3. 重新渲染外部视图时,不必重新初始化视图,重新初始化可能导致内存泄漏,并且还会导致现有绑定出现异常问题
    4. 请记住,如果您每次调用new View()时初始化render(),那么初始化将会调用delegateEvents()。所以这不一定是你所表达的“骗局”。

答案 1 :(得分:31)

Backbone是一个长期存在的问题,根据我的经验,这个问题并没有真正令人满意的答案。我与你分享你的挫折感,特别是因为尽管这个用例有多么普遍,但是引用的却很少。也就是说,我通常会选择类似于你的第二个例子。

首先,我会解雇任何需要你重新委派事件的事情。 Backbone的事件驱动视图模型是其最关键的组件之一,并且仅仅因为您的应用程序非平凡而失去该功能会在任何程序员的口中留下不好的味道。所以划伤第一。

关于你的第三个例子,我认为这只是传统渲染实践的最终结果,并没有增加太多意义。也许如果你正在进行实际的事件触发(即,不是一个人为的“onRender”事件),那么将这些事件绑定到render本身是值得的。如果您发现render变得笨拙且复杂,则您的子视图太少。

回到你的第二个例子,这可能是三个邪恶中较小的一个。以下是从Recipes With Backbone解除的示例代码,可在我的PDF版本的第42页找到:

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

这只是比第二个例子稍微复杂一点的设置:它们指定了一组功能,addAlladdOne,它们可以完成繁琐的工作。我认为这种方法是可行的(我当然也使用它);但它仍留下一种奇怪的回味。 (原谅所有这些舌头的比喻。)

关于以正确顺序附加的要点:如果你严格追加,当然,这是一个限制。但请确保您考虑所有可能的模板方案。也许您实际上喜欢占位符元素(例如,空divul),然后您可以replaceWith一个包含相应子视图的新(DOM)元素。追加不是唯一的解决方案,如果你关心那么多,你当然可以解决订购问题,但我想你如果它绊倒你就会遇到设计问题。请记住,子视图可以包含子视图,如果合适,它们应该是。这样,你就有了一个非常类似于树的结构,这非常好:每个子视图按顺序添加所有子视图,然后父视图添加另一个,依此类推。

不幸的是,解决方案#2可能是您希望使用开箱即用的Backbone的最佳选择。如果您有兴趣查看第三方库,我已经查看过(但实际上还没有时间玩过)的是Backbone.LayoutManager,这似乎有一种更健康的方法来添加子视图。但是,即使他们在与这些问题类似的问题上也有recent debates

答案 2 :(得分:5)

对此尚未提及感到惊讶,但我会认真考虑使用Marionette

它为Backbone应用程序强制执行更多结构,包括特定视图类型(ListViewItemViewRegionLayout),添加适当的Controller s等等。

以下是the project on Github和一个很棒的guide by Addy Osmani in the book Backbone Fundamentals来帮助您入门。

答案 3 :(得分:4)

我相信这是一个非常全面的解决方案。它允许集合中的模型更改,并且仅重新呈现其视图(而不是整个集合)。它还通过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");
        });
    }
});

用法:

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

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

答案 4 :(得分:2)

查看此mixin以创建和呈现子视图:

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

这是一个极简主义的解决方案,解决了此线程中讨论的许多问题,包括呈现顺序,不必重新委托事件等。请注意集合视图的情况(集合中的每个模型都是用一个子视图表示)是一个不同的主题。我知道的最佳通用解决方案是CollectionView in Marionette

答案 5 :(得分:0)

我不喜欢上述任何解决方案。我更喜欢在每个视图上进行此配置,必须在render方法中手动执行。

  • views可以是返回视图定义对象的函数或对象
  • 调用父级.remove时,应调用最低级别的嵌套子级.remove(从子子视图一直到位)
  • 默认情况下,父视图会传递它自己的模型和集合,但可以添加和覆盖选项。

以下是一个例子:

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

答案 6 :(得分:0)

Backbone是故意建造的,因此没有"普通"关于这个和许多其他问题的实践。它意味着尽可能不受任何影响。从理论上讲,您甚至不必使用Backbone的模板。您可以在视图的render函数中使用javascript / jquery来手动更改视图中的所有数据。为了使其更加极端,您甚至不需要一个特定的render功能。你可以使用一个名为renderFirstName的函数来更新dom中的第一个名称,renderLastName更新dom中的姓氏。如果采用这种方法,那么在性能方面会更好,而且您不必再次手动委派事件。这段代码对于阅读它的人来说也是完全有意义的(尽管它会更长/更乱码)。

然而,通常使用模板并简单地破坏和重建整个视图及其在每个渲染调用中的子视图都没有缺点,因为提问者甚至没有做任何事情。除此以外。这就是大多数人为他们遇到的每种情况所做的事情。这就是为什么自以为是的框架只会使其成为默认行为。

答案 7 :(得分:0)

您还可以将渲染的子视图作为变量注入主模板中作为变量。

首先渲染子视图并将它们转换为html,如下所示:

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

(这样你也可以在循环中使用动态字符串连接像subview1 + subview2这样的视图),然后将它传递给主模板,如下所示: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

并最终注入它:

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

关于子视图中的事件:他们很可能必须使用此方法在父视图(masterView)中进行连接,而不是在子视图中。

答案 8 :(得分:0)

我喜欢使用以下方法,这也确保正确删除子视图。以下是Addy Osmani的book示例。

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

答案 9 :(得分:0)

没有必要重新委托事件,因为它很昂贵。见下文:

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