一次保存多个主干模型

Save several Backbone models at once

本文关键字:模型 保存 一次      更新时间:2023-09-26

我有一个包含大量模型的骨干集合。

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

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

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

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

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

我最终用几种方法来增强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(( 方法,因此使用 backbone 的 hasChanged(( 来标记等待保存的模型是有意义的。当然,如果您出于其他目的静默地更改模型,这将有问题 - collection.saveChanged(( 也会保存这些模型,因此值得考虑设置替代标志。

无论如何,如果我们这样做,在保存时,我们需要确保 Backbone 现在认为模型没有更改(不触发其更改事件(,因此我们需要手动操作模型,就好像它没有被更改一样。 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();

再。安息..对集合的端点执行 PUT 以更改其"某些"记录是不完全正确的。 从技术上讲,PUT应该替换整个集合,尽管在我的应用程序真正需要替换整个集合之前,我很乐意采取务实的方法。

您可以定义一个新资源来完成这种行为,您可以将其称为MyModelBatch

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

此外,您还需要在 Backbone 客户端中实现一个Model,其中包含一个属性,即模型数组和一个不使用id的特殊url

关于重新渲染

的事情,我建议您尝试每个模型都有一个视图,这样渲染的渲染量将与模型更改的渲染量一样多,但它们将是细节重新渲染而不会重复。

这就是我想出的。

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);
    }
});