Backbone.js - 实现“即时”搜索的最佳实践

13

我的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 ,并呈现结果,从而为每个模型创建一个新视图。我刚刚创建了幽灵视图,是吗?最好销毁每个视图吗?或者我应该尝试管理我的 RowView,只创建每个视图一次,并通过循环渲染结果来实现?在清空 tbody 后,RowViews 是否仍然具有其 el,还是现在为空并需要重新呈现?

问题 2,模型选择

您会注意到我在我的RowView中触发了自定义事件。我想在某个地方拥有一个详细视图来处理该事件并显示我模型的全部内容。当我搜索我的列表时,如果我的选择模型仍然在搜索结果中,我希望保持该状态并让它保留在我的详细视图中。一旦它不再在我的结果中,我就会清空详细视图。所以我肯定需要管理一个视图数组,对吗?我考虑过双向链接结构,其中每个视图指向它的模型,每个模型指向它的视图...但如果我将来要在我的模型上实现一个单例工厂,我就无法将其强加给模型。:/

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

2个回答

20

在玩弄你的问题时,我有点过度热衷。

首先,我会创建一个专门的集合来存储筛选模型,以及一个“状态模型”来处理搜索。例如:

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 函数,这也非常有用。 - savinger
我仍在努力理解您呈现事物的方式,并且对于使用setElement并不完全认同。在每次呈现时重新绑定事件似乎不太优雅。我从未见过这种嫁接技术,其中您在CollectionView中呈现列表项并嫁接ItemViews...我不习惯ItemView不负责自己的呈现,一方面似乎是不应该发生的关注点分离,但另一方面则出奇地直截了当,因为让模板迭代我们的集合总是更容易的。 - savinger
@savinger setElement 主要用于美观和模板的“自包含性”,如果您需要重新渲染行,则此技术将更有用。这个答案可能会帮助您理解我的观点:https://dev59.com/Ymct5IYBdhLWcg3wjuEj#12006179 - nikoshr
@savinger 关于嫁接技术,重新渲染速度比子视图渲染和附加要快得多,而且允许在不需要在初始加载时渲染客户端的情况下进行服务器端渲染。 如果这说得通的话。 - nikoshr

4

与您正在呈现的内容一致的CollectionView的集合必须是一致的,否则您将遇到问题。 您不应该手动清空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);

第二个问题。我喜欢你关于主集合和CollectionView集合的说法...但你没有涉及子视图。每次使用addOne创建新的RowView是否可以? - savinger
2
@savinger 它们本质上实现了相同的功能 - 它们监听事件。然而,this.listenTo将监听与视图关联,而this.collection.on将监听与集合关联。这似乎没有太大的区别,但请记住,如果您使用this.collection.on,即使您删除了视图,集合仍将继续监听,这可能会导致内存泄漏并严重减慢应用程序的速度。另一方面,如果您使用this.listenTo,它将在您删除视图后停止监听事件。 - hesson
好的。所以在我们的搜索功能中触发了重置,所有的行视图都被删除并重新创建。有任何内存泄漏吗?低效率问题? - savinger
1
不会出现内存泄漏。但是您可能希望考虑使用 set 而不是 reset。虽然 reset 清除现有的模型并呈现新模型,但 set 将执行“智能合并”,更新视图并仅删除需要删除的模型——这可能更有效率。 - hesson
1
顺便提一下,如果您知道将要多次调用 this.$('something'),那么最好缓存该选择器,例如 var $foo = this.$('foo'); 参见:http://jsperf.com/find-vs-cached-dom - j03w
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接