33

過去 6 か月間、私は Backbone と仕事をしてきました。最初の 2 か月は、自分のコードをどのように構築したいかを学び、理解しようとすることでした。次の 4 か月は、本番環境に適したアプリケーションの開発に費やされました。誤解しないでほしいのですが、Backbone のおかげで、以前は標準であった何千行ものクライアント サイド コードの混乱から解放されましたが、より壮大なことをより短い時間で実行できるようになり、まったく新しい問題のスタックが開かれました。ここで提起するすべての質問には、ハックのように感じたり、単に間違っていると感じたりする簡単な解決策があります。素晴らしいソリューションには 300 ポイントの報奨金をお約束します。ここに行きます:

  1. 読み込み中 - 私たちのユース ケース (管理パネル) では、悲観的な同期は良くありません。いくつかのことについては、受け入れる前にサーバー上で検証する必要があります。「同期」イベントがバックボーンにマージされる前に開始しました。

そして、読み込みイベントを模倣するために次の小さなコードを使用しました。

window.old_sync = Backbone.sync

# Add a loading event to backbone.sync
Backbone.sync = (method, model, options) ->
  old_sync(method, model, options)
  model.trigger("loading")

偉大な。期待どおりに動作しますが、正しくはありません。このイベントを関連するすべてのビューにバインドし、そのモデルから成功またはエラー イベントを受け取るまで読み込みアイコンを表示します。これを行うためのより良い、より健全な方法はありますか?

今、難しいもののために:

  1. あまりにも多くのものがレンダリングされすぎます- アプリケーションにタブがあるとしましょう。すべてのタブがコレクションを制御します。左側にコレクションがあります。モデルをクリックして、中央で編集を開始します。名前を変更してタブを押すと、次のフォーム項目に移動します。これで、アプリは「リアルタイムの何か」となり、違いに気づき、検証を実行し、変更をサーバーに自動的に同期します。保存ボタンは必要ありません! 素晴らしいですが、フォームの先頭にある H2 は入力と同じ名前です。更新する必要があります。ああ、リストの名前を横に更新する必要があります。ああ、リストは名前でソートされます!

別の例を次に示します。コレクションに新しいアイテムを作成します。「新規」ボタンを押すと、フォームへの入力が開始されます。アイテムをすぐにコレクションに追加しますか? しかし、それを捨てることにした場合はどうなりますか?または、コレクション全体を別のタブに保存した場合はどうなりますか? また、ファイルのアップロードがあります。ファイルのアップロードを開始する前に、モデルを保存して同期する必要があります (ファイルをモデルに添付できるようにするため)。したがって、すべてが微動でレンダリングを開始します。モデルとリストを保存すると、フォームが再びレンダリングされます-現在同期されているため、新しい削除ボタンが表示され、リストに表示されます-しかし、ファイルのアップロードがアップロードを終了したため、すべてレンダリングを再開します。

ミックスにサブビューを追加すると、すべてがフェリーニの映画のように見え始めます。

  1. それはずっとサブビューです-これについては良い記事があります。聖なるすべてのものへの愛のために、サブビューを持つビューに jQuery プラグインまたは DOM イベントをアタッチする正しい方法を見つけることができませんでした。地獄はすぐに続く。ツールチップは、レンダリングが長く続くのを聞き、おかしくなり始め、サブビューがゾンビのようになるか、応答しなくなります。ここには実際のバグが存在するため、これが主な問題点ですが、すべてを網羅する解決策はまだありません。

  2. ちらつき- レンダリングが高速です。実際、画面が発作を起こしたように見えるほど高速です。場合によっては、(別のサーバー呼び出しで!) 再度ロードする必要がある画像であるため、html が最小化されてから突然再び最大化されます。その要素の css width+height で修正されます。これは、fadeIn と fadeOut で解決できる場合もあります。これは、ビューを再利用したり、新しく作成したりする場合があるため、書くのが大変です。

TL;DR - バックボーンのビューとサブビューに問題があります - レンダリングの回数が多すぎます。レンダリング時にちらつき、サブビューが DOM イベントを切り離し、脳を食べます。

ありがとうございました!

詳細: Ruby on Rails Gem を使用した BackboneJS。UnderscoreJS テンプレートを使用したテンプレート。

4

3 に答える 3

17

ビューの部分レンダリング

DOM 階層の完全なレンダリングを最小限に抑えるために、特定のプロパティの更新を反映する特別なノードを DOM に設定できます。

名前のリストであるこの単純なアンダースコア テンプレートを使用してみましょう。

<ul>
  <% _(children).each(function(model) { %>
    <li>
        <span class='model-<%= model.cid %>-name'><%= model.name %></span> :
        <span class='model-<%= model.cid %>-name'><%= model.name %></span>
    </li>
  <% }); %>
</ul>

classmodel-<%= model.cid %>-nameに注意してください。これが注入ポイントになります。

次に、基本ビューを定義 (または Backbone.View を変更) して、更新時にこれらのノードに適切な値を入力します。

var V = Backbone.View.extend({
    initialize: function () {
        // bind all changes to the models in the collection
        this.collection.on('change', this.autoupdate, this);
    },

    // grab the changes and fill any zone set to receive the values
    autoupdate: function (model) {
        var _this = this,
            changes = model.changedAttributes(),
            attrs = _.keys(changes);

        _.each(attrs, function (attr) {
            _this.$('.model-' + model.cid + '-' + attr).html(model.get(attr));
        });
    },

    // render the complete template
    // should only happen when there really is a dramatic change to the view
    render: function () {
        var data, html;

        // build the data to render the template
        // this.collection.toJSON() with the cid added, in fact
        data = this.collection.map(function (model) {
            return _.extend(model.toJSON(), {cid: model.cid});
        });

        html = template({children: data});
        this.$el.html(html);

        return this;
    }
});

コレクションではなくモデルに対応するために、コードは少し異なります。http://jsfiddle.net/nikoshr/cfcDX/で遊ぶフィドル

DOM 操作の制限

サブビューへのレンダリングの委任はコストがかかる可能性があり、それらの HTML フラグメントを親の DOM に挿入する必要があります。さまざまなレンダリング方法を比較するこの jsperf テストを見てください。

その要点は、完全な HTML 構造を生成してからビューを適用する方が、ビューとサブビューを構築してからレンダリングをカスケードするよりもはるかに高速であるということです。例えば、

<script id="tpl-table" type="text/template">
    <table>
        <thead>
            <tr>
                <th>Row</th>
                <th>Name</th>
            </tr>
        </thead>
        <tbody>
        <% _(children).each(function(model) { %>
            <tr id='<%= model.cid %>'>
                <td><%= model.row %></td>
                <td><%= model.name %></td>
            </tr>
        <% }); %>
        </tbody>
     </table>
</script>
var ItemView = Backbone.View.extend({
});

var ListView = Backbone.View.extend({
    render: function () {
        var data, html, $table, template = this.options.template;

        data = this.collection.map(function (model) {
            return _.extend(model.toJSON(), {
                cid: model.cid
            });
        });

        html = this.options.template({
            children: data
        });

        $table = $(html);

        this.collection.each(function (model) {
            var subview = new ItemView({
                el: $table.find("#" + model.cid),
                model: model
            });
        });

        this.$el.empty();
        this.$el.append($table);

        return this;
    }
});


var view = new ListView({
    template: _.template($('#tpl-table').html()),
    collection: new Backbone.Collection(data)
});

http://jsfiddle.net/nikoshr/UeefE/

jsperf は、テンプレートをあまりペナルティなしでサブテンプレートに分割できることを示していることに注意してください。これにより、行の部分的なレンダリングを提供できます。

関連する注意として、DOM にアタッチされたノードでは動作しないでください。これにより、不要なリフローが発生します。ノードを操作する前に、新しい DOM を作成するか、ノードを切り離してください。

ゾンビをつぶす

Derick Bailey は、ゾンビ ビューの根絶に関する優れた記事を書きました。

基本的に、ビューを破棄するときは、すべてのリスナーのバインドを解除し、jQuery プラグイン インスタンスの破棄などの追加のクリーンアップを実行する必要があることを覚えておく必要があります。私が使用しているのは、Derick がBackbone.Marionetteで使用しているものと同様のメソッドの組み合わせです。

var BaseView = Backbone.View.extend({

    initialize: function () {
        // list of subviews
        this.views = [];
    },

    // handle the subviews
    // override to destroy jQuery plugin instances
    unstage: function () {
        if (!this.views) {
            return;
        }

        var i, l = this.views.length;

        for (i = 0; i < l; i = i + 1) {
            this.views[i].destroy();
        }
        this.views = [];
    },

    // override to setup jQuery plugin instances
    stage: function () {
    },

    // destroy the view
    destroy: function () {
        this.unstage();
        this.remove();
        this.off();

        if (this.collection) {
            this.collection.off(null, null, this);
        }
        if (this.model) {
            this.model.off(null, null, this);
        }
    }
});

前の例を更新して、行にドラッグ可能な動作を与えると、次のようになります。

var ItemView = BaseView.extend({
    stage: function () {
        this.$el.draggable({
            revert: "invalid",
            helper: "clone"
        });
    },

    unstage: function () {
        this.$el.draggable('destroy');
        BaseView.prototype.unstage.call(this);
    }
});

var ListView = BaseView.extend({

    render: function () {
       //same as before

        this.unstage();
        this.collection.each(function (model) {
            var subview = new ItemView({
                el: $table.find("#" + model.cid),
                model: model
            });
            subview.stage();
            this.views.push(subview);
        }, this);
        this.stage();

        this.$el.empty();
        this.$el.append($table);

        return this;
    }
});

http://jsfiddle.net/nikoshr/yL7g6/

ルート ビューを破棄すると、ビューの階層が走査され、必要なクリーンアップが実行されます。

注意: JS コードについては申し訳ありません。正確なスニペットを提供できるほど Coffeescript に精通していません。

于 2012-08-17T12:49:38.193 に答える
9

わかりました、順番に.. :)

  1. 読み込んでいます...

サーバーに保存されているデータを検証する場合は、サーバー側で行うことをお勧めします。サーバーでの検証が失敗した場合、サーバーは 200 HTTP コードを送信しないようにする必要があるため、Backbone.Model の保存メソッドはエラーをトリガーします。

反対側では、検証データ バックボーンには実装されていない検証メソッドがあります。それを実装して使用するのは正しい選択だと思います。ただし、validate は set と save の前に呼び出されることに注意してください。validate がエラーを返した場合、set と save は続行されず、モデルの属性は変更されません。検証に失敗すると、「エラー」イベントがトリガーされます。

別の方法として、({silent: true} パラメーターを使用して) サイレント セットを呼び出す場合、isValid メソッドを手動で呼び出してデータを検証する必要があります。

  1. あまりにも多くのものがあまりにも多くのことをレンダリングします..

ビューをロジックの下に分離する必要があります。コレクションのグッド プラクティスは、モデルごとに個別のビューです。この場合、各要素を個別にレンダリングできます。さらに、コレクションのコンテナー ビューを初期化するときに、コレクション内の各モデルのイベントを適切なビューにバインドすると、自動的にレンダリングされます。

素晴らしいですが、フォームの先頭にある H2 は入力と同じ名前です。更新する必要があります。ああ、リストの名前を横に更新する必要があります。

メソッドでJQuery使用して、表示する値を送信するコールバックを実装できます。例:

//Container view
init: function() {
    this.collection = new Backbone.Collection({
        url: 'http://mybestpage.com/collection'
    });
    this.collection.bind('change', this.render, this);
    this.collection.fetch();
},
render: function() {

    _.each(this.collection.models, function(model) {
         var newView = new myItemView({
              model: model,
              name: 'view' + model.id
         });
         this.$('#my-collection').append(newView.render().$el);
         view.on('viewEdit', this.displayValue);
    }, this);
},
...
displayValue: function(value) {
    //method 1
    this.displayView.setText(value); //we can create little inner view before, 
                                     //for text displaying. Сonvenient at times.
    this.displayView.render();
    //method 2
    $(this.el).find('#display').html(value);
}

//View from collection
myItemView = Backbone.View.extend({
events: {
    'click #edit': 'edit'
},
init: function(options) {
    this.name = options.name;
},
...
edit: function() {
    this.trigger('viewEdit', this.name, this);
}

ああ、リストは名前でソートされます!

バックボーン コレクションにはsortメソッドを使用できます。しかし (!) sort を呼び出すと、コレクションの「リセット」イベントがトリガーされます。これを回避するには、{silent: true} を渡します。方法

別の例を次に示します。コレクションに新しいアイテムを作成したい...

「新規」ボタンを押すと、新しいモデルを作成する必要がありますが、.save() メソッドが成功をトリガーする場合にのみ、このモデルをコレクションにプッシュする必要があります。別のケースでは、エラー メッセージを表示する必要があります。もちろん、新しいモデルを検証してサーバーに保存するまで、新しいモデルをコレクションに追加する理由はありません。

  1. それはずっとサブビューです...サブビューはゾンビのようになるか、応答しません。

あなた(または任意のモデル)が render メソッドを呼び出すと、その中のすべての要素が再作成されます。したがって、サブビューがある場合はsubView.delegateEvents(subView.events);、すべてのサブビューを呼び出す必要があります。おそらく、この方法はちょっとしたトリックですが、うまくいきます。

  1. ちらつき..

大きな画像と中程度の画像にサムネイルを使用すると、多くの場合、ちらつきが最小限に抑えられます。別の方法として、ビューのレンダリングを画像やその他のコンテンツに分離することもできます。

例:

var smartView = Backbone.View.extend({
  initialize: function(){
    this.model.on( "imageUpdate", this.imageUpdate, this );
    this.model.on( "contentUpdate", this.contentUpdate, this );
  },

  render: function(){
    this.$el.html(this.template(this.model.toJSON()));
  },

  imageUpdate: function(){
    this.$el.find('#image').attr('src', this.model.get('imageUrl'));
  },
  contentUpdate: function(){
    this.$el.find('#content').html(this.model.get('content'));
  }
})

これが誰にも役立つことを願っています。文法の間違いがあれば申し訳ありません:)

于 2012-08-21T19:19:01.047 に答える
2

読み込んでいます...

私は熱心な読み込みの大ファンです。私のサーバー呼び出しはすべて JSON 応答であるため、頻繁に行うことは大したことではありません。私は通常、ビューで必要になるたびにコレクションを更新します。

熱心な読み込みの私のお気に入りの方法は、Backbone-relationalを使用することです。アプリを階層的に編成するとします。このことを考慮:

Organization model
|--> Event model
|--> News model
   |--> Comment model

そのため、ユーザーが を表示しているときに、その組織のとorganizationを熱心にロードできます。そして、ユーザーが記事を表示しているときに、その記事の.eventsnewsnewscomments

Backbone-relationalは、サーバーから関連するレコードをクエリするための優れたインターフェイスを提供します。

あまりにも多くのものがあまりにも多くのことをレンダリングします...

ここでもバックボーン リレーショナルが役立ちます。Backbone-relationalは、非常に便利なグローバル レコード ストアを提供します。このようにして、ID を渡し、同じモデルを別の場所で取得できます。ある場所で更新すると、別の場所で利用できるようになります。

a_model_instance = Model.findOrCreate({id: 1})

ここでの別のツールはBackbone.ModelBinderです。Backbone.ModelBinderを使用すると、テンプレートを作成して、ビューの変更へのアタッチを忘れることができます。したがって、情報を収集してヘッダーに表示する例では、Backbone.ModelBinderにこれらの要素の両方を監視するように指示するだけで、 inputchangeでモデルが更新さchangeれ、表示するモデルで更新されるため、ヘッダーは次のようになります更新しました。

それはずっとサブビューです...サブビューはゾンビのようになるか、応答しません...

私はBackbone.Marionetteが本当に好きです。onShow多くのクリーンアップを処理し、DOM からビューを一時的に削除するときに役立つコールバックを追加します。

これは、jQuery プラグインのアタッチを容易にするのにも役立ちます。onShow メソッドは、jQuery プラグイン コードが適切に機能できるように、ビューがレンダリングされて DOM に追加された後に呼び出されます。

CollectionViewまた、コレクションとそのサブビューを管理するのに優れた機能を果たす、いくつかのクールなビュー テンプレートも提供します。

ちらつき

残念ながら、これについてはあまり経験がありませんが、画像をプリロードすることもできます。それらを隠しビューでレンダリングしてから、前面に出します。

于 2012-08-24T15:51:03.880 に答える