如何在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命令。 视图初始化初始化,因此在渲染function中一次完成的任务并不多。

缺点:你不得不重新委托事件(),这可能是昂贵的? 父视图的渲染函数与所有需要发生的子视图渲染混淆? 您无法设置元素的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); } 

优点:您不必重新委派事件。 您不需要只包含空白占位符的模板,而您的标记名又回到由视图定义的位置。

缺点:你现在必须确保以正确的顺序追加东西。 父视图的渲染仍然是由子视图渲染混乱。

有了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实例化子视图?
  • render或在onRender执行所有的子视图渲染逻辑?
  • 使用setElementappend/appendTo

我通常看到/使用了几种不同的解决scheme:

解决scheme1

 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. render()被调用后,内部视图的元素已经被放置到DOM,这是有用的,如果你的内部视图的render()方法放置/根据其他元素的位置/大小本身的大小(这是一个常见的用途根据我的经验)

解决scheme2

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

解决scheme2可能看起来更干净,但在我的经验中造成了一些奇怪的事情,并对性能产生了负面影响。

我通常使用解决scheme1,原因如下:

  1. 我的很多观点都依赖于render()方法中的DOM
  2. 当重新渲染外部视图时,视图不必重新初始化,重新初始化可能导致内存泄漏,并且还会导致现有绑定的怪异问题

请记住,如果每次调用render()时初始化一个new View() ,那么初始化将调用delegateEvents() 。 所以这不应该像你所expression的那样是一个“骗局”。

这是Backbone的一个长期问题,根据我的经验,这个问题并不是一个令人满意的答案。 我分享你的挫败感,尤其是因为尽pipe这个用例有多普遍,所以还是有很less的指导。 也就是说,我通常会和第二个例子类似。

首先,我会忽略任何需要你重新委托事件的事情。 Backbone的事件驱动的视图模型是其中最重要的组件之一,并且仅仅因为你的应用程序不重要就会失去这种function,会给程序员留下不好的口味。 所以从头开始吧。

关于你的第三个例子,我认为这只是围绕着传统渲染练习的一个结束,并没有增加太多的意义。 也许如果你正在做实际的事件触发(也就是说,不是一个人为的“ onRender ”事件),那么绑定这些事件来render它本身就是值得的。 如果你发现render变得笨重和复杂,你的子视图太less了。

回到你的第二个例子,这可能是三个邪恶中较小的一个。 以下是从我的PDF版本的第42页上find的带骨干食谱的示例代码:

 ... 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来完成肮脏的工作。 我认为这种方法是可行的(我当然也使用它)。 但它仍然留下一个奇怪的回味。 (请原谅这些舌头的隐喻。)

要按照正确的顺序追加:如果你严格追加,那当然是一个限制。 但是请确保您考虑所有可能的模板scheme。 也许你会喜欢一个占位符元素(例如,一个空的divul ),然后你可以replaceWith一个新的(DOM)元素来保存适当的子视图。 追加并不是唯一的解决scheme,如果你关心这个问题,你当然可以解决订购问题,但是如果它让你失望,我会想象你有一个devise问题。 记住,子视图可以有子视图,如果适合的话也应该有子视图。 这样,你就有了一个相当像树的结构,这是相当不错的:每个子视图按顺序添加它的所有子视图,在父视图添加另一个子视图之前,依此类推。

不幸的是,解决scheme#2可能是您使用开箱即用Backbone的最佳select。 如果你有兴趣查看第三方库,那么我已经看过(但实际上还没有任何时间去玩)的Backbone.LayoutManager ,它似乎有一个更健康的添加子视图的方法。 然而,即使他们最近也在类似的问题上进行过辩论 。

惊讶这还没有提到,但我认真考虑使用木偶 。

它强化了Backbone应用程序的一些结构,包括特定的视图types( ListViewItemViewRegionLayout ),添加适当的Controller以及更多。

这里是Github上的项目 ,Addy Osmani在Backbone Fundamentals这本书中的一个很好的指导 。

我有,我相信是一个相当全面的解决这个问题。 它允许集合中的模型发生更改,并且只重新呈现其视图(而不是整个集合)。 它还通过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 }); 

看看这个混合创build和渲染子视图:

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

这是一个极简主义的解决scheme,解决了这个线程中讨论的很多问题,包括渲染顺序,不必重新委派事件等。注意,集合视图的情况下(集合中的每个模型都用一个子视图)是一个不同的话题。 我知道的最好的一般解决scheme是木偶中的CollectionView 。

我真的不喜欢上述任何解决scheme。 我更喜欢这种configuration,每个视图必须手动在渲染方法中工作。

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

骨干是有意build造的,所以在这个和其他许多问题上没有“共同”的做法。 它意味着尽可能不被select。 理论上,你甚至不必使用Backbone的模板。 您可以在视图的renderfunction中使用javascript / jquery手动更改视图中的所有数据。 为了使它更加极端,你甚至不需要一个特定的renderfunction。 你可以有一个名为renderFirstName的函数来更新dom中的名字, renderLastName更新dom中的姓。 如果采用这种方法,性能方面会更好,而且您不必再次手动委派事件。 代码也可以让读者阅读(尽pipe代码会更长/更乱)。

但是,通常使用模板并且简单地销毁和重build整个视图并且在每个渲染调用上都是子视图是没有任何缺点的,因为否则询问者甚至不会做任何事情。 所以这就是大多数人所遇到的几乎所有情况。 这就是为什么自以为是的框架只是使这个默认行为。

您也可以将渲染的子视图作为variables注入主模板中作为variables。

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

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

(这样你也可以在循环中使用dynamicstring连接像subview1 + subview2的意见),然后将其传递给主模板,看起来像这样: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

并像这样最后注入它:

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

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