33

在过去的六个月里,我一直在与 Backbone 合作。前两个月是在搞乱,学习和弄清楚我想如何围绕它构建我的代码。在接下来的 4 个月里,我们正在敲定一个适合生产的应用程序。不要误会我的意思,Backbone 将我从之前标准的数千行客户端代码中解救出来,但它使我能够在更短的时间内完成更多宏伟的事情,从而引发了一系列全新的问题。对于我在这里提出的所有问题,都有一些简单的解决方案,感觉就像是 hacks 或者只是感觉不对。我承诺为一个很棒的解决方案提供 300 点赏金。开始:

  1. 加载- 对于我们的用例(管理面板),悲观同步是不好的。对于某些事情,我需要在接受它们之前在服务器上验证它们。我们在 'sync' 事件被合并到 Backbone 之前就开始了,

我们使用这个小代码来模拟加载事件:

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. 太多的东西使自己变得太多——假设我们的应用程序有标签。每个选项卡控制一个集合。在左侧,您将获得收藏。单击模型以开始在中心进行编辑。您更改其名称并按 Tab 键进入下一个表单项。现在,您的应用程序是一个“实时的东西”,它注意到差异,运行验证,并自动将更改同步到服务器,不需要保存按钮!很好,但是表单开头的 H2 与输入中的名称相同 - 您需要更新它。哦,您需要将列表中的名称更新到一边。哦,列表按名称排序!

这是另一个示例:您想在集合中创建一个新项目。您按下“新建”按钮并开始填写表格。您是否立即将项目添加到集合中?但是,如果您决定丢弃它会怎样?或者,如果您将整个集合保存在另一个选项卡上?而且,还有一个文件上传 - 您需要先保存并同步模型,然后才能开始上传文件(以便您可以将文件附加到模型)。所以一切都开始呈现震颤:你保存模型和列表,表单再次呈现自己 - 它现在已经同步,所以你得到一个新的删除按钮,它显示在列表中 - 但现在文件上传完成上传,所以一切再次开始渲染。

将子视图添加到组合中,一切都开始看起来像费里尼电影。

  1. 一直是子视图-这是一篇关于这些东西的好文章。出于对神圣事物的热爱,我无法找到将 jQuery 插件或 DOM 事件附加到任何具有子视图的视图的正确方法。地狱很快就来了。工具提示听到渲染的时间很长并开始发疯,子视图变得像僵尸一样或没有响应。这是主要的痛点,因为这里有实际的错误,但我仍然没有一个包罗万象的解决方案。

  2. 闪烁- 渲染速度很快。事实上,它是如此之快,以至于我的屏幕看起来像是癫痫发作了。有时它的图像必须再次加载(通过另一个服务器调用!),所以 html 最小化然后又突然最大化 - 该元素的 css 宽度+高度将解决这个问题。有时我们可以用fadeIn 和fadeOut 来解决这个问题——这写起来很麻烦,因为有时我们重用一个视图,有时又重新创建它。

TL;DR - 我在 Backbone 中的视图和子视图有问题 - 它渲染太多次,渲染时闪烁,子视图分离我的 DOM 事件并吃掉我的大脑。

谢谢!

更多细节:BackboneJS 与 Ruby on Rails Gem。使用 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>

注意 class model-<%= 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 会触发集合的“重置”事件。通过 {silent: true} 来避免这种情况。如何

这是另一个示例:您想在集合中创建一个新项目...

当我们按下“New”按钮时,我们需要创建一个新模型,但只有当 .save() 方法触发成功时,我们才应该将该模型推送到集合中。在另一种情况下,我们应该显示错误消息。当然,我们没有理由将新模型添加到我们的集合中,直到它经过验证并保存在服务器上。

  1. 它的子视图一直向下......子视图变得像僵尸一样或没有响应。

当您(或任何模型)调用渲染方法时,将重新创建其中的所有元素。因此,如果您有子视图,则应调用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我可以急切地加载该组织的eventsnews. 当用户查看news一篇文章时,我渴望加载该文章的comments.

Backbone-relational为从服务器查询相关记录提供了一个很好的接口。

太多的东西让自己变得太多……

骨干关系在这里也有帮助!Backbone-relational提供了一个被证明非常有用的全局记录存储。这样,您可以传递 ID 并在其他地方检索相同的模型。如果您在一个地方更新它,它在另一个地方可用。

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

这里的另一个工具是Backbone.ModelBinderBackbone.ModelBinder让您可以构建模板而忘记附加到视图更改。因此,在您收集信息并将其显示在标题中的示例中,只需告诉Backbone.ModelBinder监视这两个元素,并且在 input 上change,您的模型将被更新,并且change您查看的模型将被更新,所以现在标题将是更新。

一直是子视图……子视图变得像僵尸一样或没有响应……

我真的很喜欢Backbone.Marionette。它为您处理了大量的清理工作,并添加了一个onShow回调,在临时从 DOM 中删除视图时很有用。

这也有助于方便附加 jQuery 插件。onShow 方法在视图渲染并添加到 DOM 后调用,以便 jQuery 插件代码可以正常运行。

它还提供了一些很酷的视图模板,比如CollectionView在管理集合及其子视图方面做得很好。

闪烁

不幸的是,我对此没有太多经验,但您也可以尝试预加载图像。将它们呈现在隐藏视图中,然后将它们向前推进。

于 2012-08-24T15:51:03.880 回答