web-dev-qa-db-ja.com

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順序を維持することを心配する必要はありません。ビューは早い段階で初期化されるため、レンダリング関数で一度にすべてを行うことはあまりありません。

短所:イベントの再デリゲートを余儀なくされますが、これはコストがかかる可能性がありますか?親ビューのレンダリング機能は、発生する必要があるすべてのサブビューレンダリングで散らばっていますか?要素のtagNameを設定する機能がないため、テンプレートは正しいtagNameを維持する必要があります。

別の方法:

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

私はある種のミックスを行い、これらのすべての例で多くの異なるプラクティスを一致させました(それについてすみません)が、あなたが保持または追加するものは何ですか?そして、あなたは何をしませんか?

プラクティスの要約:

  • サブビューをinitializeまたはrenderでインスタンス化しますか?
  • すべてのサブビューレンダリングロジックをrenderまたはonRenderで実行しますか?
  • setElementまたはappend/appendToを使用しますか?
133

私は通常、いくつかの異なるソリューションを見てきました/使用しました:

ソリューション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. 外側のビューが再レンダリングされる場合、ビューを再初期化する必要はありません。再初期化すると、メモリリークが発生し、既存のバインディングで異常な問題が発生する可能性があります

new View()が呼び出されるたびにrender()を初期化する場合、その初期化はとにかくdelegateEvents()を呼び出すことに注意してください。したがって、あなたが表明したように、それは必ずしも「詐欺」ではないはずです。

58
Lukas

これはBackboneの永続的な問題であり、私の経験では、この質問に対する満足のいく答えはありません。特に、このユースケースがどれほど一般的であるにも関わらず、ガイダンスがほとんどないため、私はあなたのフラストレーションを共有します。とはいえ、私は通常、2番目の例に似たものを使用します。

まず第一に、イベントの再委任を必要とするものはすべて手に負えないものにします。 Backboneのイベント駆動型ビューモデルは最も重要なコンポーネントの1つであり、アプリケーションが自明ではないという理由だけでその機能を失うと、プログラマーの口に悪い味が残ります。だから一番スクラッチ。

3番目の例については、これは従来のレンダリング手法の最終段階に過ぎず、あまり意味を持たないと思います。おそらく、実際のイベントトリガーを実行している場合(つまり、 "onRender"イベントではない)、それらのイベントをrender自体にバインドするだけの価値があるでしょう。 renderが扱いにくく複雑になっている場合は、サブビューが少なすぎます。

2番目の例に戻ります。これは、おそらく3つの悪のうちの小さい方です。これは、 Recipes With Backbone から抜粋したコード例です。私のPDFエディション:

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

これは、2番目の例よりもわずかに洗練されたセットアップです。これらは、ダーティーな作業を行う関数addAllおよびaddOneのセットを指定します。このアプローチは実行可能であると思います(そして私は確かにそれを使用します)。しかし、それでも奇妙な後味が残ります。 (これらすべての舌の隠phorをご容赦ください。)

正しい順序で追加するという点に注意してください。厳密に追加しているのであれば、それは制限です。ただし、可能なすべてのテンプレートスキームを検討してください。おそらく、実際にプレースホルダー要素(たとえば、空のdivまたはul)が必要な場合は、適切なサブビューを保持する新しい(DOM)要素を replaceWith できます。追記だけが解決策ではありません。注文の問題をあまり気にせずに確実に回避できますが、つまずく場合は設計上の問題があると思います。サブビューにはサブビューを含めることができ、適切な場合はサブビューを使用する必要があることに注意してください。このようにして、かなりツリー状の構造ができます。これは非常に優れています。各サブビューは、親ビューが別のサブビューを追加する前に、すべてのサブビューを順番に追加します。

残念ながら、解決策2はおそらく、すぐに使用可能なBackboneを使用するのに最適です。サードパーティのライブラリをチェックアウトすることに興味がある場合、私が調べたもの(実際にはまだ遊んでいる時間はありません)は Backbone.LayoutManager 、サブビューを追加するより健全な方法があるようです。しかし、彼らも、これらと同様の問題について 最近の議論 を経験しています。

31
Josh Leitzel

これはまだ言及されていないことに驚いていますが、 Marionette の使用を真剣に検討します。

特定のビュータイプ(ListViewItemViewRegion、およびLayout)を含むBackboneアプリにもう少し構造を強制し、適切なControllersと多くを追加します。もっと。

Githubのプロジェクト および素晴らしい 本Backbone FundamentalsのAddy Osmaniによるガイド から始めましょう。

5
Dana Woodman

私が信じているのは、この問題に対する非常に包括的な解決策です。これにより、コレクション内のモデルを変更し、(コレクション全体ではなく)ビューのみを再レンダリングできます。また、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
});
4
sarink

サブビューの作成とレンダリングについては、このミックスインをご覧ください。

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

これは、レンダリング順序、イベントを再委任する必要がないなど、このスレッドで説明されている多くの問題に対処する最小限のソリューションです。コレクションビューの場合(コレクション内の各モデルは1つで表されます)サブビュー)は別のトピックです。私がその場合に知っている最も一般的な解決策は、 MarionetteのCollectionView です。

2
Brave Dave

私はまた、子ビューを適切に削除することを確認する次のアプローチを使用するのが好きです。以下は、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() {
    } });
0
FlintOff

上記のソリューションはどれも好きではありません。各ビューよりもこの構成の方が、renderメソッドで手動で作業する必要があります。

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

費用がかかるため、イベントを再委任する必要はありません。下記参照:

    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);            
    }
});
0
soham joshi

レンダリングされたサブビューを変数としてメインテンプレートに挿入することもできます。

最初にサブビューをレンダリングし、次のように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)で接続する必要があります。

0
B Piltz

バックボーンは、この問題や他の多くの問題に関して「一般的な」慣行がないように意図的に構築されました。可能な限り非ピニオン化されることを意図しています。理論的には、Backboneでテンプレートを使用する必要さえありません。ビューのrender関数でjavascript/jqueryを使用して、ビュー内のすべてのデータを手動で変更できます。さらに極端にするには、特定のrender関数さえ必要ありません。 domの名を更新するrenderFirstNameとdomの姓を更新するrenderLastNameという関数を使用できます。このアプローチを採用した場合、パフォーマンスの点ではるかに優れており、手動でイベントを再度委任する必要はありません。コードは、それを読んでいる人にとっても完全に理にかなっています(ただし、より長い/より厄介なコードになります)。

ただし、通常はテンプレートを使用し、単にビュー全体を破棄して再構築することにはマイナス面はありません。それは、レンダリングコールのたびにサブビューになります。だから、ほとんどの人が遭遇するほとんどすべての状況でそれをします。そして、それが、独断的なフレームワークがこれをデフォルトの動作にする理由です。

0
Nick Manning