简体   繁体   English

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

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

I have a nested-View setup which can get somewhat deep in my application. 我有一个嵌套的视图设置,可以在我的应用程序中得到一些深度。 There are a bunch of ways I could think of initializing, rendering and appending the sub-views, but I'm wondering what common practice is. 有很多方法我可以想到初始化,渲染和追加子视图,但我想知道常见的做法是什么。

Here are a couple I've thought of: 这是我想到的一对夫妇:

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

Pros: You don't have to worry about maintaining the right DOM order with appending. 优点:您不必担心通过追加维护正确的DOM顺序。 The views are initialized early on, so there isn't as much to do all at once in the render function. 视图在早期初始化,因此在渲染函数中不会同时执行所有操作。

Cons: You are forced to re-delegateEvents(), which might be costly? 缺点:你被迫重新委托事件(),这可能是昂贵的? The parent view's render function is cluttered with all of the subview rendering that needs to happen? 父视图的渲染函数与所有需要发生的子视图渲染混杂在一起? You don't have the ability to set the tagName of the elements, so the template needs to maintain the correct tagNames. 您无法设置元素的tagName ,因此模板需要维护正确的tagNames。

Another way: 其他方式:

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

Pros: You don't have to re-delegate events. 优点:您无需重新委派活动。 You don't need a template that just contains empty placeholders and your tagName's are back to being defined by the view. 您不需要仅包含空占位符的模板,并且您的tagName将返回由视图定义。

Cons: You now have to make sure to append things in the right order. 缺点:您现在必须确保以正确的顺序附加内容。 The parent view's render is still cluttered by the subview rendering. 子视图渲染仍然使父视图的渲染变得混乱。

With an onRender event: 使用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();
}

Pros: The subview logic is now separated from the view's render() method. 优点:子视图逻辑现在与视图的render()方法分开。

With an onRender event: 使用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();
}

I've kind of mix and matched a bunch of different practices across all of these examples (so sorry about that) but what are the ones that you would keep or add? 在所有这些示例中,我有点混合并匹配了许多不同的实践(很抱歉)但是你要保留或添加的是什么? and what would you not do? 你不会做什么?

Summary of practices: 实践摘要:

  • Instantiate subviews in initialize or in render ? initializerender实例化子视图?
  • Perform all sub-view rendering logic in render or in onRender ? renderonRender执行所有子视图渲染逻辑?
  • Use setElement or append/appendTo ? 使用setElementappend/appendTo

I have generally seen/used a couple of different solutions: 我一般看到/使用了几种不同的解决方案:

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

This is similar to your first example, with a few changes: 这与您的第一个示例类似,只有一些更改:

  1. The order in which you append the sub elements matters 附加子元素的顺序很重要
  2. The outer view does not contain the html elements to be set on the inner view(s) (meaning you can still specify tagName in the inner view) 外部视图不包含要在内部视图上设置的html元素(这意味着您仍然可以在内部视图中指定tagName)
  3. render() is called AFTER the inner view's element has been placed into the DOM, which is helpful if your inner view's render() method is placing/sizing itself on the page based on other elements' position/size (which is a common use case, in my experience) 在内部视图的元素放入DOM之后调用render() ,如果内部视图的render()方法基于其他元素的位置/大小(这是常见的用途)在页面上放置/调整大小,这将非常有用案例,根据我的经验)

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

Solution 2 may look cleaner, but it has caused some strange things in my experience and has affected performance negatively. 解决方案2可能看起来更干净,但它在我的经验中引起了一些奇怪的事情并且对性能产生了负面影响。

I generally use Solution 1, for a couple of reasons: 我通常使用解决方案1,原因有两个:

  1. A lot of my views rely on already being in the DOM in their render() method 我的很多观点都依赖于他们在render()方法中已经存在于DOM中
  2. When the outer view is re-rendered, views don't have to be re-initialized, which re-initialization can cause memory leaks and also cause freaky issues with existing bindings 重新呈现外部视图时,不必重新初始化视图,重新初始化可能导致内存泄漏,并且还会导致现有绑定出现怪异问题

Keep in mind that if you are initializing a new View() every time render() is called, that initialization is going to call delegateEvents() anyway. 请记住,如果每次调用render()时初始化一个new View() ,那么初始化将调用delegateEvents() So that shouldn't necessarily be a "con", as you've expressed. 所以这不一定是你所表达的“骗局”。

This is a perennial problem with Backbone and, in my experience, there's not really a satisfying answer to this question. 这是Backbone长期存在的问题,根据我的经验,这个问题并没有真正令人满意的答案。 I share your frustration, especially since there is so little guidance despite how common this use case is. 我与你分享你的挫折感,特别是因为尽管这个用例有多么普遍,但是引用的却很少。 That said, I usually go with something akin to your second example. 也就是说,我通常会选择类似于你的第二个例子。

First of all, I would dismiss out of hand anything that requires you to re-delegate events. 首先,我会解雇任何需要你重新委托事件的事情。 Backbone's event-driven view model is one of its most crucial components, and to lose that functionality simply because your application is non-trivial would leave a bad taste in any programmer's mouth. Backbone的事件驱动视图模型是其最关键的组件之一,并且仅仅因为您的应用程序非平凡而失去该功能会在任何程序员的口中留下不好的味道。 So scratch number one. 所以划伤第一。

Regarding your third example, I think it's just an end-run around the conventional rendering practice and doesn't add much meaning. 关于你的第三个例子,我认为这只是传统渲染实践的最终目的,并没有增加太多意义。 Perhaps if you're doing actual event triggering (ie, not a contrived " onRender " event), it would be worth just binding those events to render itself. 也许如果你正在进行实际的事件触发(即,不是一个人为的“ onRender ”事件),那么将这些事件绑定到render本身是值得的。 If you find render becoming unwieldy and complex, you have too few subviews. 如果您发现render变得笨拙且复杂,则您的子视图太少。

Back to your second example, which is probably the lesser of the three evils. 回到你的第二个例子,这可能是三个邪恶中较小的一个。 Here is example code lifted from Recipes With Backbone , found on page 42 of my PDF edition: 以下是我的PDF版本第42页上的Recipes With Backbone提取的示例代码:

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

This is only a slightly more sophisticated setup than your second example: they specifiy a set of functions, addAll and addOne , that do the dirty work. 这只是比第二个例子稍微复杂的设置:它们指定了一组功能, addAlladdOne ,它们可以完成脏工作。 I think this approach is workable (and I certainly use it); 我认为这种方法是可行的(我当然也使用它); but it still leaves a bizarre aftertaste. 但它仍留下一种奇怪的回味。 (Pardon all these tongue metaphors.) (原谅所有这些舌头的比喻。)

To your point on appending in the right order: if you're strictly appending, sure, that's a limitation. 按照正确的顺序附加要点:如果你严格追加,那肯定是限制。 But make sure you consider all possible templating schemes. 但请确保您考虑所有可能的模板方案。 Perhaps you'd actually like a placeholder element (eg, an empty div or ul ) that you can then replaceWith a new (DOM) element that holds the appropriate subviews. 也许您实际上喜欢占位符元素(例如,空divul ),然后您可以使用包含相应子视图的新(DOM)元素replaceWith它。 Appending isn't the only solution, and you can certainly get around the ordering problem if you care about it that much, but I would imagine you have a design issue if it is tripping you up. 追加不是唯一的解决方案,如果你关心那么多,你当然可以解决订购问题,但我想你如果它绊倒你就会遇到设计问题。 Remember, subviews can have subviews, and they should if it's appropriate. 请记住,子视图可以包含子视图,如果合适,它们应该是。 That way, you have a rather tree-like structure, which is quite nice: each subview adds all its subviews, in order, before the parent view adds another, and so on. 这样,你有一个相当树状的结构,这是非常好的:每个子视图按顺序添加所有子视图,然后父视图添加另一个,依此类推。

Unfortunately, solution #2 is probably the best you can hope for using out-of-the-box Backbone. 不幸的是,解决方案#2可能是您希望使用开箱即用的Backbone的最佳选择。 If you're interested in checking out third-party libraries, one that I have looked into (but haven't actually had any time to play with yet) is Backbone.LayoutManager , which seems to have a healthier method of adding subviews. 如果你有兴趣检查第三方库,我已经查看过(但实际上还没有时间玩),是Backbone.LayoutManager ,它似乎有一种更健康的方法来添加子视图。 However, even they have had recent debates on similar issues to these. 然而,即使他们最近就类似问题进行了辩论

Surprised this hasn't been mentioned yet, but I'd seriously consider using Marionette . 感到惊讶的是还没有提到,但我会认真考虑使用木偶

It enforces a bit more structure to Backbone apps, including specific view types ( ListView , ItemView , Region and Layout ), adding proper Controller s and a lot more. 它为Backbone应用程序强制执行更多结构,包括特定的视图类型( ListViewItemViewRegionLayout ),添加适当的Controller等等。

Here is the project on Github and a great guide by Addy Osmani in the book Backbone Fundamentals to get you started. 这是Github上的项目,以及Addy Osmani在Backbone Fundamentals一书中的精彩指南,以帮助您入门。

I have, what I believe to be, a quite comprehensive solution to this problem. 我相信,这是一个非常全面的解决方案。 It allows a model within a collection to change, and have only its view re-rendered (rather than the entire collection). 它允许集合中的模型更改,并且仅重新呈现其视图(而不是整个集合)。 It also handles removal of zombie views through the close() methods. 它还通过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");
        });
    }
});

Usage: 用法:

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

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

Check out this mixin for creating and rendering subviews: 查看此mixin以创建和呈现子视图:

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

It is a minimalist solution that addresses a lot of the issues discussed in this thread, including rendering order, not having to re-delegate events, etc. Note that the case of a collection view (where each model in the collection is represented with one subview) is a different topic. 它是一个极简主义的解决方案,解决了该线程中讨论的许多问题,包括呈现顺序,不必重新委托事件等。请注意集合视图的情况(集合中的每个模型都用一个表示) subview)是一个不同的主题。 Best general solution I am aware of to that case is the CollectionView in Marionette . 我知道的最佳通用解决方案是Marionette中CollectionView

I don't really like any of the above solutions. 我真的不喜欢上述任何解决方案。 I prefer for this configuration over each view having to manually do work in the render method. 我更喜欢在每个视图上进行此配置,必须在render方法中手动完成工作。

  • views can be a function or object returning an object of view definitions views可以是返回视图定义对象的函数或对象
  • When a parent's .remove is called, the .remove of nested children from the lowest order up should be called (all the way from sub-sub-sub views) 当调用父级的.remove ,应调用从最低级别向上的嵌套子级的.remove (从子子视图一直到)
  • By default the parent view passes it's own model and collection, but options can be added and overridden. 默认情况下,父视图会传递它自己的模型和集合,但可以添加和覆盖选项。

Here's an example: 这是一个例子:

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 was intentionally built so that there was no "common" practice in regards to this and many other issues. 有意构建了Backbone,因此在这个和许多其他问题上没有“共同”的做法。 It is meant to be as unopinionated as possible. 它意味着尽可能不受任何影响。 Theoretically, you don't even have to use templates with Backbone. 从理论上讲,您甚至不必使用Backbone模板。 You could use javascript/jquery in the render function of a view to manually change all of the data in the view. 您可以在视图的render功能中使用javascript / jquery来手动更改视图中的所有数据。 To make it more extreme, you don't even need one specific render function. 为了使它更加极端,您甚至不需要一个特定的render功能。 You could have a function called renderFirstName which updates the first name in the dom and renderLastName which updates the last name in the dom. 您可以使用一个名为renderFirstName的函数来更新dom中的第一个名称,并使用renderLastName来更新dom中的姓氏。 If you took this approach, it would be way better in terms of performance and you'd never have to manually delegate events again. 如果你采用这种方法,那么在性能方面会更好,你永远不必再次手动委派事件。 The code would also make total sense to someone reading it (although it would be longer/messier code). 代码对于阅读它的人来说也是完全有意义的(虽然它会更长/更乱码)。

However, usually there is no downside to using templates and simply destroying and rebuilding the entire view and it's subviews on each and every render call, as it didn't even occur to the questioner to do anything otherwise. 但是,通常使用模板并简单地破坏和重建整个视图以及每次渲染调用的子视图都没有任何缺点,因为提问者甚至不会做任何事情。 So that's what most people do for pretty much every situation they come across. 这就是大多数人为他们遇到的每种情况所做的事情。 And that's why opinionated frameworks just make this the default behavior. 这就是为什么固定框架只会使这成为默认行为。

You could also inject the rendered subviews as variables into the main template as variables. 您还可以将渲染的子视图作为变量注入主模板中作为变量。

first render the subviews and convert them to html like this: 首先渲染子视图并将它们转换为html,如下所示:

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

(that way you could also dynamically string concatenate the views like subview1 + subview2 when used in loops) and then pass it to the master template which looks like this: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ... (这样你也可以在循环中使用动态字符串连接像subview1 + subview2这样的视图)然后将它传递给主模板,如下所示: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

and inject it finally like this: 并最终注入它:

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

Regarding the Events within the subviews: They will be most likely have to be connected in the parent (masterView) with this approach not within the subviews. 关于子视图中的事件:他们很可能必须在父视图(masterView)中使用此方法连接,而不是在子视图中。

I like to use the following approach which also make sure to remove the child views properly. 我喜欢使用以下方法,这也确保正确删除子视图。 Here is an example from the book by Addy Osmani. 以下是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() {
    } });

There is no need to re-delegate events as it is costly. 由于成本高昂,因此无需重新委托事件。 See below: 见下文:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM