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

How to render and append sub-views in Backbone.js

本文关键字:视图 Backbone js      更新时间:2023-09-26

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

下面是我想到的几个:

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中执行所有子视图渲染逻辑
  • 使用setElement还是append/appendTo

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

解决方案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. 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,原因有几个:

  1. 我的许多视图都依赖于它们的render()方法中已经存在于DOM中
  2. 当外部视图被重新渲染时,视图不必重新初始化,重新初始化可能会导致内存泄漏,也会导致现有绑定出现异常问题

请记住,如果每次调用render()时都要初始化new View(),那么该初始化无论如何都会调用delegateEvents()。所以这不一定是一个"骗局",正如你所表达的那样。

这是Backbone的一个长期问题,根据我的经验,这个问题并没有真正令人满意的答案。我和你一样感到沮丧,尤其是因为尽管这个用例很常见,但几乎没有什么指导。也就是说,我通常会使用类似于你的第二个例子的东西。

首先,我会立即驳回任何要求您重新委派活动的要求。Backbone的事件驱动视图模型是其最关键的组件之一,如果仅仅因为应用程序不平凡而失去该功能,任何程序员都会感到不愉快。所以,从第一个开始。

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

回到你的第二个例子,它可能是三害相权取其轻。以下是从带骨干的食谱中提取的示例代码,可以在我的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,它似乎有一种更健康的添加子视图的方法。然而,就连他们最近也就类似的问题进行了辩论。

令人惊讶的是,还没有提到这一点,但我会认真考虑使用Marionette。

它为Backbone应用程序提供了更多的结构,包括特定的视图类型(ListViewItemViewRegionLayout),添加了适当的Controller等等。

以下是Github上的项目,以及Addy Osmani在《骨干基础》一书中为您提供的一份很棒的指南。

我认为,我有一个非常全面的解决方案来解决这个问题。它允许集合中的模型进行更改,并且只重新渲染其视图(而不是整个集合)。它还通过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
});

查看这个用于创建和渲染子视图的混合:

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

这是一个最低限度的解决方案,解决了本线程中讨论的许多问题,包括渲染顺序、不必重新委托事件等。请注意,集合视图的情况(集合中的每个模型都用一个子视图表示)是不同的主题。据我所知,最好的通用解决方案是Marionette的CollectionView。

我真的不喜欢上面的任何解决方案。我更喜欢这种配置,而不是每个视图都必须手动完成渲染方法中的工作。

  • 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)串接在一起,然后将其传递给主模板,主模板如下所示: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

最后像这样注入:

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

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

我喜欢使用以下方法,确保正确删除子视图。这是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);            
    }
});