10

我有一个带有大量模型的 Backbone 集合。

每当在模型上设置特定属性并保存它时,就会触发大量计算并重新呈现 UI。

但是,我希望能够一次在多个模型上设置属性,并且只有在它们全部设置后才进行保存和重新渲染。当然我不想为一个操作发出多个http请求,也绝对不想重新渲染界面十次。

我希望在 Backbone.Collection 上找到一个保存方法,它可以计算出哪些模型 hasChanged(),将它们作为 json 组合在一起并发送到后端。然后可以由集合上的事件触发重新呈现。没有这样的运气。

这似乎是一个很常见的要求,所以我想知道为什么 Backbone 没有实现。这是否违背了 RESTful 架构,将多个内容保存到单个端点?如果是这样,那又如何?发出 1000 个请求来持久化 1000 个小项目是不切实际的。

那么,使用我自己的 save 方法来增加 Backbone.Collection 的唯一解决方案是迭代其所有模型并为所有已更改的模型构建 json 并将其发送到后端?还是有人有更简洁的解决方案(或者我只是错过了什么!)?

4

3 回答 3

4

我最终用几种方法来增加 Backbone.Collection 来处理这个问题。

saveChangeMethod 创建一个要传递给 Backbone.sync 的虚拟模型。模型需要的所有主干同步方法是它的 url 属性和 toJSON 方法,所以我们可以轻松地敲掉它。

在内部,模型的 toJSON 方法仅返回其属性的副本(发送到服务器),因此我们可以愉快地使用仅返回模型数组的 toJSON 方法。Backbone.sync 对此进行了字符串化,它只为我们提供了属性数据。

成功时, saveChanged 会触发要处理一次的集合上的事件。已经添加了一些代码,使其针对任何批次模型中已更改的每个属性触发一次特定事件。

Backbone.Collection.prototype.saveChanged = function () {
    var me = this,
        changed = me.getChanged(),
        dummy = {
            url: this.url,
            toJSON: function () {
                return changed.models;
            }
        },
        options = {
            success: function (model, resp, xhr) {
                for (var i = 0; i < changed.models.length; i++) {
                    changed.models[i].chnageSilently();
                }
                for (var attr in changed.attributes) {
                    me.trigger("batchchange:" + attr);
                }
                me.trigger("batchsync", changed);
            }
        };
    return Backbone.sync("update", dummy, options);
}

然后,我们只需要集合上的 getChanged() 方法。这将返回一个具有 2 个属性的对象、一个已更改模型的数组和一个标记哪些属性已更改的对象:

Backbone.Collection.prototype.getChanged = function () {
    var models = [],
        changedAttributes = {};
    for (var i = 0; i < this.models.length; i++) {
        if (this.models[i].hasChanged()) {
            _.extend(changedAttributes, this.models[i].changedAttributes());
            models.push(this.models[i]);
        }
    }
    return models.length ? {models: models, attributes: changedAttributes} : null;
}

尽管这是对主干“更改模型”范式的预期用途的轻微滥用,但批处理的全部意义在于,当模型更改时,我们不希望发生任何事情(即触发任何事件)。

因此,我们必须将 {silent: true} 传递给模型的 set() 方法,因此使用主干的 hasChanged() 来标记等待保存的模型是有意义的。当然,如果您出于其他目的默默地更改模型,这将是有问题的 - collection.saveChanged() 也会保存这些,因此值得考虑设置一个替代标志。

无论如何,如果我们这样做,在保存时,我们需要确保主干现在认为模型没有改变(没有触发它们的更改事件),所以我们需要手动操作模型,就好像它没有改变一样被改变了。saveChanged() 方法迭代我们更改的模型并在模型上调用这个 changeSilently() 方法,这基本上只是 Backbone 的 model.change() 方法,没有触发器:

Backbone.Model.prototype.changeSilently = function () {
    var options = {},
    changing = this._changing;
    this._changing = true;
    for (var attr in this._silent) this._pending[attr] = true;
    this._silent = {};
    if (changing) return this;

    while (!_.isEmpty(this._pending)) {
        this._pending = {};
        for (var attr in this.changed) {
        if (this._pending[attr] || this._silent[attr]) continue;
        delete this.changed[attr];
        }
        this._previousAttributes = _.clone(this.attributes);
    }
    this._changing = false;
    return this;
}

用法:

model1.set({key: value}, {silent: true});
model2.set({key: value}, {silent: true});
model3.set({key: value}, {silent: true});
collection.saveChanged();

关于。RESTful .. 对集合的端点执行 PUT 以更改其“某些”记录是不太正确的。从技术上讲,一个 PUT 应该替换整个集合,但在我的应用程序真正需要替换整个集合之前,我很乐意采用务实的方法。

于 2012-09-06T18:09:34.400 回答
1

你可以定义一个新的资源来完成这种行为,你可以调用它MyModelBatch

您需要在服务器端实现一个新资源,该资源能够消化Array模型并执行正确的操作CREATEUPDATEDESTROY

此外,您需要Model在 Backbone 客户端中实现一个属性,即模型数组和一个url不使用id.

关于重新渲染的事情,我建议您尝试为每个模型创建一个视图,以便在模型更改时会有尽可能多的渲染,但它们将是细节重新渲染而不会重复。

于 2012-09-05T19:20:58.643 回答
0

这就是我想出的。

Backbone.Collection.extend({
    saveAll: function(models, key, val, options) {

        var attrs, xhr, wait, that = this;

        var transport = {
            url: this.url,
            models: [],
            toJSON: function () {
                return { models: this.models };
            },
            trigger: function(){
                return that.trigger.apply(that, arguments);
            }
        };

        if(models == null){
            models = this.models;
        }

        // Handle both `"key", value` and `{key: value}` -style arguments.
        if (key == null || typeof key === 'object') {
            attrs = key;
            options = val;
        } else {
            (attrs = {})[key] = val;
        }

        options = _.extend({validate: true}, options);
        wait = options.wait;

        // After a successful server-side save, the client is (optionally)
        // updated with the server-side state.
        if (options.parse === void 0) options.parse = true;

        var triggers = [];

        _.each(models, function(model){

            var attributes = model.attributes;

            // If we're not waiting and attributes exist, save acts as
            // `set(attr).save(null, opts)` with validation. Otherwise, check if
            // the model will be valid when the attributes, if any, are set.
            if (attrs && !wait) {
                if (!model.set(attrs, options)) return false;
            } else {
                if (!model._validate(attrs, options)) return false;
            }

            // Set temporary attributes if `{wait: true}`.
            if (attrs && wait) {
                model.attributes = _.extend({}, attributes, attrs);
            }

            transport.models.push(model.toJSON());

            triggers.push(function(resp){
                if(resp.errors){
                    model.trigger('error', model, resp, options);
                } else {
                    // Ensure attributes are restored during synchronous saves.
                    model.attributes = attributes;
                    var serverAttrs = options.parse ? model.parse(resp, options) : resp;
                    if (wait) serverAttrs = _.extend(attrs || {}, serverAttrs);
                    if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) {
                        return false;
                    }
                    model.trigger('sync', model, resp, options);
                }
            });

            // Restore attributes.
            if (attrs && wait) model.attributes = attributes;
        });

        var success = options.success;
        options.success = function(resp) {
            _.each(triggers, function(trigger, i){
                trigger.call(options.context, resp[i]);
            });
            if (success) success.call(options.context, models, resp, options);
        };
        return this.sync('create', transport, options);
    }
});
于 2018-04-11T10:54:54.953 回答