我有一个嵌套视图设置,可以深入到我的应用程序中。我可以想到很多方法来初始化、渲染和附加子视图,但我想知道常见的做法是什么。
这是我想到的一对:
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();
}
我在所有这些示例中混合并匹配了一堆不同的做法(对此感到抱歉),但您会保留或添加哪些做法?你不会做什么?
实践总结:
在初始化或渲染中实例化子视图?
在 render 或 onRender 中执行所有子视图渲染逻辑?
使用 setElement 或 append/appendTo?
close
方法和一个 onClose
来清理孩子,但我只是好奇如何首先实例化和渲染它们。
delete
与 C++ 中的 delete
不同。如果你问我,这是一个非常糟糕的关键字。
我通常看到/使用过几种不同的解决方案:
解决方案 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();
}
});
这与您的第一个示例类似,但有一些更改:
附加子元素的顺序很重要外部视图不包含要在内部视图上设置的 html 元素(这意味着您仍然可以在内部视图中指定 tagName)render() 在内部视图之后调用元素已被放置到 DOM 中,如果您的内部视图的 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,原因有两个:
我的很多视图都依赖于已经在它们的 render() 方法中的 DOM 当重新渲染外部视图时,视图不必重新初始化,重新初始化会导致内存泄漏并导致异常现有绑定的问题
请记住,如果您在每次调用 render()
时都初始化 new View()
,那么无论如何初始化都会调用 delegateEvents()
。因此,正如您所说,这不一定是“骗局”。
这是 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);
}
这只是比您的第二个示例稍微复杂一点的设置:它们指定了一组函数 addAll
和 addOne
,它们完成了肮脏的工作。我认为这种方法是可行的(我当然会使用它);但它仍然留下了奇异的回味。 (请原谅所有这些舌头隐喻。)
关于以正确顺序附加的观点:如果您严格附加,当然,这是一个限制。但请确保您考虑了所有可能的模板方案。也许您实际上想要一个占位符元素(例如,一个空的 div
或 ul
),然后您可以replaceWith
一个新的 (DOM) 元素来保存适当的子视图。附加不是唯一的解决方案,如果你非常关心它,你当然可以解决排序问题,但我想如果它让你绊倒,你会遇到设计问题。请记住,子视图可以有子视图,如果合适的话,它们应该有。这样,您就有了一个相当树状的结构,这非常好:每个子视图按顺序添加其所有子视图,然后父视图添加另一个,依此类推。
不幸的是,解决方案 #2 可能是您希望使用开箱即用的 Backbone 的最佳方案。如果您有兴趣查看第三方库,我研究过的(但实际上还没有时间玩)是 Backbone.LayoutManager,它似乎有一种更健康的添加子视图的方法。但是,即使他们在与这些类似的问题上也有过recent debates。
model.bind('remove', view.remove);
——你不应该在 Appointment 的初始化函数中这样做以保持它们分开吗?
很惊讶这还没有被提及,但我会认真考虑使用 Marionette。
它对 Backbone 应用程序实施了更多的结构,包括特定的视图类型(ListView
、ItemView
、Region
和 Layout
),添加了适当的 Controller
等等。
这是 the project on Github 和一个很好的 guide by Addy Osmani in the book 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
});
查看这个 mixin 以创建和渲染子视图:
https://github.com/rotundasoftware/backbone.subviews
这是一个极简的解决方案,解决了这个线程中讨论的许多问题,包括渲染顺序、不必重新委托事件等。请注意集合视图的情况(集合中的每个模型都用一个表示子视图)是一个不同的主题。对于这种情况,我知道的最佳通用解决方案是 CollectionView in Marionette。
我真的不喜欢上述任何解决方案。我更喜欢这种配置,而不是必须在渲染方法中手动完成工作的每个视图。
视图可以是返回视图定义对象的函数或对象
当调用父级的 .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
之类的视图),然后将其传递给主模板,如下所示: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...
最后像这样注入它:
this.$el.html(_.template(MasterTemplate, { sub1: subview1, sub2: subview2 } ));
关于子视图中的事件:它们很可能必须在父视图(masterView)中连接,这种方法不在子视图中。
我喜欢使用以下方法,它也确保正确删除子视图。这是 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() {
} });
无需重新委托事件,因为它的成本很高。见下文:
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);
}
});