【问题标题】:Backbone.js - Best Practice for Implementing "Instant" SearchBackbone.js - 实现“即时”搜索的最佳实践
【发布时间】:2013-08-10 00:00:41
【问题描述】:

我希望在我的 Backbone 应用程序中的几个地方对集合进行即时搜索,但我很难想出实现它的最佳方法。

这是一个快速实现。 http://jsfiddle.net/7YgeE/ 请记住,我的收藏可能包含超过 200 个模型。

var CollectionView = Backbone.View.extend({

  template: $('#template').html(),

  initialize: function() {

    this.collection = new Backbone.Collection([
      { first: 'John', last: 'Doe' },
      { first: 'Mary', last: 'Jane' },
      { first: 'Billy', last: 'Bob' },
      { first: 'Dexter', last: 'Morgan' },
      { first: 'Walter', last: 'White' },
      { first: 'Billy', last: 'Bobby' }
    ]);
    this.collection.on('add', this.addOne, this);

    this.render();
  },

  events: {
    'keyup .search': 'search',
  },

  // Returns array subset of models that match search.
  search: function(e) {

    var search = this.$('.search').val().toLowerCase();

    this.$('tbody').empty(); // is this creating ghost views?

    _.each(this.collection.filter(function(model) {
      return _.some(
        model.values(), 
        function(value) {
          return ~value.toLowerCase().indexOf(search);
        });
    }), $.proxy(this.addOne, this));
  },

  addOne: function(model) {

    var view = new RowView({ model: model });
    this.$('tbody').append(view.render().el);
  },

  render: function() {

    $('#insert').replaceWith(this.$el.html(this.template));
      this.collection.each(this.addOne, this);
  }
});

每个模型都有一个小视图...

var RowView = Backbone.View.extend({

  tagName: 'tr',

  events: {
    'click': 'click'
  },

  click: function () {
    // Set element to active 
    this.$el.addClass('selected').siblings().removeClass('selected');

    // Some detail view will listen for this.
    App.trigger('model:view', this.model);
  },

  render: function() {

    this.$el.html('<td>' + this.model.get('first') + '</td><td>' + this.model.get('last') + '</td>');
      return this;
  }
});

new CollectionView;

问题 1

在每次按键时,我都会过滤集合,清空tbody,然后渲染结果,从而为每个模型创建一个新视图。我刚刚创建了幽灵视图,是吗?最好适当地销毁每个视图吗?或者我应该尝试管理我的RowViews... 每个只创建一次,然后循环通过它们只呈现结果?我的CollectionView 中的数组可能吗?清空tbody 后,RowViews 是否还有它们的el,或者现在是否为 null 并需要重新渲染?

问题 2,模型选择

您会注意到我在RowView 中触发了一个自定义事件。我想在某处有一个详细视图来处理该事件并显示我的整个模型。当我搜索我的列表时,如果我选择的模型保留在搜索结果中,我想保留该状态并让它保留在我的详细视图中。一旦它不再出现在我的结果中,我将清空详细视图。所以我当然需要管理一系列视图,对吧?我考虑过一个双重链接的结构,其中每个视图都指向它的模型,每个模型都指向它的视图......但是如果我将来要在我的模型上实现一个单例工厂,我不能将它强加于模型。 :/

那么管理这些视图的最佳方法是什么?

【问题讨论】:

    标签: javascript backbone.js


    【解决方案1】:

    在玩你的问题时,我有点忘乎所以。

    首先,我将创建一个专用集合来保存过滤后的模型和一个“状态模型”来处理搜索。例如,

    var Filter = Backbone.Model.extend({
        defaults: {
            what: '', // the textual search
            where: 'all' // I added a scope to the search
        },
        initialize: function(opts) {
            // the source collection
            this.collection = opts.collection; 
            // the filtered models
            this.filtered = new Backbone.Collection(opts.collection.models); 
            //listening to changes on the filter
            this.on('change:what change:where', this.filter); 
        },
    
        //recalculate the state of the filtered list
        filter: function() {
            var what = this.get('what').trim(),
                where = this.get('where'),
                lookin = (where==='all') ? ['first', 'last'] : where,
                models;
    
            if (what==='') {
                models = this.collection.models;            
            } else {
                models = this.collection.filter(function(model) {
                    return _.some(_.values(model.pick(lookin)), function(value) {
                        return ~value.toLowerCase().indexOf(what);
                    });
                });
            }
    
            // let's reset the filtered collection with the appropriate models
            this.filtered.reset(models); 
        }
    });
    

    将被实例化为

    var people = new Backbone.Collection([
        {first: 'John', last: 'Doe'},
        {first: 'Mary', last: 'Jane'},
        {first: 'Billy', last: 'Bob'},
        {first: 'Dexter', last: 'Morgan'},
        {first: 'Walter', last: 'White'},
        {first: 'Billy', last: 'Bobby'}
    ]);
    var flt = new Filter({collection: people});
    

    然后我将为列表和输入字段创建单独的视图:更易于维护和移动

    var BaseView = Backbone.View.extend({
        render:function() {
            var html, $oldel = this.$el, $newel;
    
            html = this.html();
            $newel=$(html);
    
            this.setElement($newel);
            $oldel.replaceWith($newel);
    
            return this;
        }
    });
    var CollectionView = BaseView.extend({
        initialize: function(opts) {
            // I like to pass the templates in the options
            this.template = opts.template;
            // listen to the filtered collection and rerender
            this.listenTo(this.collection, 'reset', this.render);
        },
        html: function() {
            return this.template({
                models: this.collection.toJSON()
            });
        }
    });
    var FormView = Backbone.View.extend({
        events: {
            // throttled to limit the updates
            'keyup input[name="what"]': _.throttle(function(e) {
                 this.model.set('what', e.currentTarget.value);
            }, 200),
    
            'click input[name="where"]': function(e) {
                this.model.set('where', e.currentTarget.value);
            }
        }
    });
    

    BaseView 允许就地更改 DOM,详见Backbone, not "this.el" wrapping

    实例看起来像

    var inputView = new FormView({
        el: 'form',
        model: flt
    });
    var listView = new CollectionView({
        template: _.template($('#template-list').html()),
        collection: flt.filtered
    });
    $('#content').append(listView.render().el);
    

    以及现阶段搜索的演示http://jsfiddle.net/XxRD7/2/

    最后,我会修改 CollectionView 以在我的渲染函数中移植行视图,类似于

    var ItemView = BaseView.extend({
        events: {
            'click': function() {
                console.log(this.model.get('first'));
            }
        }
    });
    
    var CollectionView = BaseView.extend({
        initialize: function(opts) {
            this.template = opts.template;
            this.listenTo(this.collection, 'reset', this.render);
        },
        html: function() {
            var models = this.collection.map(function (model) {
                return _.extend(model.toJSON(), {
                    cid: model.cid
                });
            });
            return this.template({models: models});
        },
        render: function() {
            BaseView.prototype.render.call(this);
    
            var coll = this.collection;
            this.$('[data-cid]').each(function(ix, el) {
                new ItemView({
                    el: el,
                    model: coll.get($(el).data('cid'))
                });
            });
    
            return this;
        }
    });
    

    另一个小提琴http://jsfiddle.net/XxRD7/3/

    【讨论】:

    • 谢谢,这非常有帮助。我真的很喜欢你用过滤器做的事情。在我早期的尝试中,我也有范围,但它是硬编码的,我不知何故错过了文档中的 pick 函数。另外,从来不知道throttle函数,也很有帮助。
    • 我仍然在思考你渲染事物的方式,我对setElement 的使用并不满意。在我看来,在每次渲染时重新绑定事件是不优雅的。我从未见过这种嫁接技术,您在 CollectionView 中渲染列表项并在 ItemViews 上嫁接......我不习惯 ItemView 不负责渲染本身,一方面看起来像是分离不应该发生的问题,但另一方面却令人惊讶地直截了当,因为让模板迭代我们的集合总是更容易。
    • @savinger setElement 主要是出于美观的原因和模板的“自包含”,例如,如果您必须重新渲染行,这种技术会更有用。这个答案可能会帮助你理解我的观点stackoverflow.com/questions/12004534/…
    • @savinger 对于嫁接技术,重新渲染比子视图渲染+附加要快得多,并且它允许服务器端渲染而无需在初始加载时渲染客户端。如果这有意义的话。
    【解决方案2】:

    与您的 CollectionView 关联的 Collection 必须与您正在呈现的内容一致,否则您会遇到问题。您不必手动清空 tbody。您应该更新集合,并在 CollectionView 中侦听集合发出的事件并使用它来更新视图。在您的搜索方法中,您应该只更新您的 Collection 而不是您的 CollectionView。这是您可以在 CollectionView 初始化方法中实现它的一种方式:

    
    initialize: function() {
      //...
    
      this.listenTo(this.collection, "reset", this.render);
      this.listenTo(this.collection, "add", this.addOne);
    }
    

    在您的搜索方法中,您只需重置您的集合,视图将自动呈现:

    
    search: function() {
      this.collection.reset(filteredModels);
    }
    

    其中filteredModels 是与搜索查询匹配的模型数组。请注意,一旦您使用过滤模型重置您的集合,您将无法访问在搜索之前最初存在的其他模型。无论搜索如何,您都应该引用包含所有模型的主集合。此“主集合”本身与您的视图无关,但您可以在此主集合上使用过滤器并使用过滤后的模型更新视图的集合。

    至于您的第二个问题,您不应该参考模型中的视图。模型应该完全独立于视图——只有视图应该引用模型。

    您的 addOne 方法可以这样重构以获得更好的性能(总是使用 $el 附加子视图):

    
    var view = new RowView({ model: model });
    this.$el.find('tbody').append(view.render().el);
    

    【讨论】:

    • 感谢您的回复。第一个问题...this.listenTo(this.collection, "reset", this.render)this.collection.on("reset", this.render, this) 有什么区别?
    • 第二个问题。我喜欢你所说的关于主集合和 CollectionView 集合......但你没有解决子视图。可以用每个addOne 创建新的RowViews 吗?
    • @savinger 他们基本上完成了同样的事情——他们监听事件。但是,this.listenTo 将监听与视图相关联,而this.collection.on 将监听与集合相关联。这似乎没有太大区别,但请记住,如果您使用 this.collection.on,即使您删除了可能导致内存泄漏并大大降低应用程序速度的视图,该集合仍将继续侦听。另一方面,如果你使用this.listenTo,在你移除视图后它不会监听事件。
    • 不,不会有内存泄漏。但是,您可能需要考虑使用set 而不是reset。虽然 reset 会清除现有模型并渲染新模型,但 set 将执行“智能合并”,“更新”视图并仅删除需要删除的模型 - 这可能更有效。
    • 附带说明,如果您知道要多次调用this.$('something'),那么缓存该选择器会更好吗?喜欢var $foo = this.$('foo'); 看到这个:jsperf.com/find-vs-cached-dom
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-05
    • 2015-09-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-15
    相关资源
    最近更新 更多