如何在 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 顺序。视图是在早期初始化的,所以在呈现函数中不需要同时做很多事情。

缺点: 您被迫重新委托 Events () ,这可能代价高昂?父视图的渲染函数与所有需要发生的子视图渲染混杂在一起?您无法设置元素的 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中执行所有子视图渲染逻辑?
  • 使用 setElement还是 append/appendTo
73228 次浏览

这是 Backbone 长期存在的问题,根据我的经验,这个问题没有一个令人满意的答案。我与您一样感到沮丧,特别是因为尽管这个用例非常普遍,但却很少有指导。也就是说,我通常会用类似于你的第二个例子的东西。

首先,我会立刻驳回任何要求你重新委派任务的要求。Backbone 的事件驱动视图模型是其最关键的组件之一,如果仅仅因为应用程序不平凡而失去这一功能,那么任何程序员都会感到不快。所以第一条。

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

回到你的第二个例子,这可能是三个邪恶中较小的一个。下面是从 < em > 有骨干的食谱 中提取的示例代码,可以在我的 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 所能期望的最佳方案。如果你有兴趣查看第三方库,我已经研究过(但还没有实际上有任何时间玩)的一个是 主干,布局管理器,它似乎有一个更健康的方法添加子视图。然而,即使他们有 最近的辩论类似的问题,这些。

我通常看到/使用两种不同的解决方案:

解决方案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 之后,就会调用 render(),如果内部视图的 render()方法根据其他元素的位置/大小在页面上放置/调整自己的大小(在我的经验中,这是一个常见的用例) ,那么 render()会很有帮助

解决方案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()方法中已经存在于 DOM 中
  2. 当重新呈现外部视图时,视图不必重新初始化,重新初始化可能导致内存泄漏,并且还会导致现有绑定的奇怪问题

请记住,如果每次调用 render()时都要初始化 new View(),那么该初始化将无论如何都要调用 delegateEvents()。所以这不一定是你所说的“骗局”。

查看下面这个用于创建和呈现子视图的混合文件:

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

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

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

很惊讶还没有提到这一点,但是我会认真考虑使用 牵线木偶

它对 Backbone 应用程序加强了一些结构,包括特定的视图类型(ListViewItemViewRegionLayout) ,添加了适当的 Controller等等。

这里是 Github 上的项目和一个伟大的 指南由艾迪奥斯马尼在书骨干基础让你开始。

我不喜欢以上任何一种解决方案。我更喜欢这种配置,而不是每个视图都必须手动处理呈现方法中的工作。

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

Backbone 是有意构建的,以便在这个问题和许多其他问题上没有“共同的”实践。它应该尽可能地保持不固执。理论上讲,你甚至不需要在 Backbone 中使用模板。可以在视图的 render函数中使用 javascript/jquery 手动更改视图中的所有数据。为了使它更极端,您甚至不需要一个特定的 render函数。你可以拥有一个名为 renderFirstName的函数,它可以更新 dom 中的名字,而 renderLastName可以更新 dom 中的姓氏。如果您采用这种方法,那么在性能方面会好得多,而且您将不必再次手动委托事件。代码对于阅读它的人来说也是完全有意义的(尽管它会更长/更混乱)。

然而,通常使用模板和简单地破坏和重建整个视图以及每个呈现调用上的子视图是没有缺点的,因为提问者甚至没有想到要做其他的事情。大多数人遇到任何情况都会这么做。这就是为什么固执己见的框架只是将其作为默认行为。

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

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

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

(这样,当在循环中使用 subview1 + subview2时,您也可以动态地串接视图) ,然后将其传递给主模板,如下所示: 一些头条新闻。 <% = sub1% > <% = sub2% > ... 一些脚注的东西..。

最后像这样注入:

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

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

我喜欢使用下面的方法来确保正确地删除子视图。下面是 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() {
} });

没有必要重新委派活动,因为代价高昂。见下文:

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