Лучшее решение для вложенных коллекций Backbone.js

17

Многие из моих моделей Backbone часто имеют дело с вложенными моделями и коллекциями, поэтому пока я использую комбинацию defaults , parse и toJSON вручную для достижения вложения:

ACME.Supplier = Backbone.Model.extend({
    defaults: function() {
        return {
            contacts: new ACME.Contacts(),
            tags: new ACME.Tags(),
            attachments: new ACME.Attachments()
        };
    },

    parse: function(res) {
        if (res.contacts) res.contacts = new ACME.Contacts(res.contacts);
        if (res.tags) res.tags = new ACME.Tags(res.tags);
        if (res.attachments) res.attachments = new ACME.Attachments(res.attachments);

        return res;
    }
});

ACME.Tag = Backbone.Model.extend({
    toJSON: function() {
        return _.pick(this.attributes, 'id', 'name', 'type');
    }
});

Я рассмотрел несколько плагинов, которые в основном работают так же, как и выше, но с гораздо меньшим контролем и большим количеством шаблонов, поэтому мне интересно, есть ли у кого-нибудь более элегантное решение этой распространенной проблемы Backbone.js.

Изменить. В итоге я выбрал следующий подход:

ACME.Supplier = Backbone.Model.extend({
    initialize: function(options) {
        this.tags = new ACME.Tags(options.tags);
    },

    parse: function(res) {
        res.tags && this.tags.reset(res.tags);

        return res;
    }
});

ACME.Tag = Backbone.Model.extend({
    toJSON: function() {
        return _.pick(this.attributes, 'id', 'name', 'type');
    }
});

Стоит отметить, что позже я обнаружил, что вам нужно будет передавать вложенные данные модели / коллекции из конструктора в конструктор вложенной модели через объект options .

    
задан Simen Brekken 02.05.2012 в 09:26
источник

4 ответа

8

Я не вижу никаких проблем с вашим подходом.

ИМХО метод Model.parse() , если для этого: должен быть перезаписан в случае необходимости специального анализа при разборе .

Единственное, что я хотел бы изменить, - это что-то вроде этого:

if (res.tags) res.tags = new ACME.Tags(res.tags);

Для этого:

if (res.tags) this.tags.reset(res.tags);

Поскольку у вас уже есть экземпляр коллекции ACME.Tags , я бы использовал его повторно.

Также мне не очень нравится реализация defaults , я привык делать эту инициализацию в Model.initialize() , но я думаю, что это дело вкуса.

    
ответ дан fguillen 02.05.2012 в 18:55
  • Я считаю, что этот подход - единственный способ пойти, не прибегая к плагину или тому подобному, спасибо. –  Simen Brekken 06.05.2012 в 09:14
  • спасибо, это было очень полезно –  Richard 01.09.2015 в 02:25
3

Я обнаружил, что при таком подходе функция toJSON Поставщика устареет, поэтому было бы неплохо выполнить повторную сборку, вернув ее состояние JSON и данные детей.

ACME.Supplier = Backbone.Model.extend({
    initialize: function(options) {
        this.tags = new ACME.Tags(options.tags);
    },

    parse: function(res) {
        res.tags && this.tags.reset(res.tags);

        return res;
    },

    toJSON: function({
        return _.extend(
            _.pick(this.attributes, 'id', 'attr1', 'attr2'), {
            tags: this.tags.toJSON(),
        });
    })

});

    
ответ дан iamtankist 29.10.2012 в 14:48
2

Мы не хотели добавлять другой фреймворк для достижения этой цели, поэтому мы абстрагировали его в классе базовой модели. Вот как вы можете объявить и использовать его ( доступно в виде гисти ):

// Declaration

window.app.viewer.Model.GallerySection = window.app.Model.BaseModel.extend({
  nestedTypes: {
    background: window.app.viewer.Model.Image,
    images: window.app.viewer.Collection.MediaCollection
  }
});

// Usage

var gallery = new window.app.viewer.Model.GallerySection({
    background: { url: 'http://example.com/example.jpg' },
    images: [
        { url: 'http://example.com/1.jpg' },
        { url: 'http://example.com/2.jpg' },
        { url: 'http://example.com/3.jpg' }
    ],
    title: 'Wow'
}); // (fetch will work equally well)

console.log(gallery.get('background')); // window.app.viewer.Model.Image
console.log(gallery.get('images')); // window.app.viewer.Collection.MediaCollection
console.log(gallery.get('title')); // plain string

Он одинаково хорошо работает с set и toJSON .
А вот BaseModel :

window.app.Model.BaseModel = Backbone.Model.extend({
  constructor: function () {
    if (this.nestedTypes) {
      this.checkNestedTypes();
    }

    Backbone.Model.apply(this, arguments);
  },

  set: function (key, val, options) {
    var attrs;

    /* jshint -W116 */
    /* jshint -W030 */
    // Code below taken from Backbone 1.0 to allow different parameter styles
    if (key == null) return this;
    if (typeof key === 'object') {
      attrs = key;
      options = val;
    } else {
      (attrs = {})[key] = val;
    }
    options || (options = {});
    // Code above taken from Backbone 1.0 to allow different parameter styles
    /* jshint +W116 */
    /* jshint +W030 */

    // What we're trying to do here is to instantiate Backbone models and collections
    // with types defined in this.nestedTypes, and use them instead of plain objects in attrs.

    if (this.nestedTypes) {
      attrs = this.mapAttributes(attrs, this.deserializeAttribute);
    }

    return Backbone.Model.prototype.set.call(this, attrs, options);
  },

  toJSON: function () {
    var json = Backbone.Model.prototype.toJSON.apply(this, arguments);

    if (this.nestedTypes) {
      json = this.mapAttributes(json, this.serializeAttribute);
    }

    return json;
  },

  mapAttributes: function (attrs, transform) {
    transform = _.bind(transform, this);
    var result = {};

    _.each(attrs, function (val, key) {
      result[key] = transform(val, key);
    }, this);

    return result;
  },

  serializeAttribute: function (val, key) {
    var NestedType = this.nestedTypes[key];
    if (!NestedType) {
      return val;
    }

    if (_.isNull(val) || _.isUndefined(val)) {
      return val;
    }

    return val.toJSON();
  },

  deserializeAttribute: function (val, key) {
    var NestedType = this.nestedTypes[key];
    if (!NestedType) {
      return val;
    }

    var isCollection = this.isTypeASubtypeOf(NestedType, Backbone.Collection),
        child;

    if (val instanceof Backbone.Model || val instanceof Backbone.Collection) {
      child = val;
    } else if (!isCollection && (_.isNull(val) || _.isUndefined(val))) {
      child = null;
    } else {
      child = new NestedType(val);
    }

    var prevChild = this.get(key);

    // Return existing model if it is equal to child's attributes

    if (!isCollection && child && prevChild && _.isEqual(prevChild.attributes, child.attributes)) {
      return prevChild;
    }

    return child;
  },

  isTypeASubtypeOf: function (DerivedType, BaseType) {
    // Go up the tree, using Backbone's __super__.
    // This is not exactly encouraged by the docs, but I found no other way.

    if (_.isUndefined(DerivedType['__super__'])) {
      return false;
    }

    var ParentType = DerivedType['__super__'].constructor;
    if (ParentType === BaseType) {
      return true;
    }

    return this.isTypeASubtypeOf(ParentType, BaseType);
  },

  checkNestedTypes: function () {
    _.each(this.nestedTypes, function (val, key) {
      if (!_.isFunction(val)) {
        console.log('Not a function:', val);
        throw new Error('Invalid nestedTypes declaration for key ' + key + ': expected a function');
      }
    });
  },
}
    
ответ дан Dan Abramov 24.09.2013 в 20:45
0

Столкнувшись с той же проблемой, я делаю что-то подобное (приведенный ниже код является выводом компилятора TypeScript, поэтому он немного многословен):

  var Model = (function (_super) {
    __extends(Model, _super);
    function Model() {
        _super.apply(this, arguments);
    }
    Model.prototype.fieldToType = function () {
        return {};
    };

    Model.prototype.parse = function (response, options) {
        _.each(this.fieldToType(), function (type, field) {
            if (response[field]) {
                if (_.isArray(response[field])) {
                    response[field] = _.map(response[field], function (value) {
                        return new type(value, { parse: true });
                    });
                } else {
                    response[field] = new type(response[field], { parse: true });
                }
            }
        });
        return _super.prototype.parse.call(this, response, options);
    };
    Model.prototype.toJSON = function () {
        var j = _super.prototype.toJSON.call(this);
        _.each(this.fieldToType(), function (type, field) {
            if (j[field]) {
                if (_.isArray(j[field])) {
                    j[field] = _.map(j[field], function (value) {
                        return value.toJSON();
                    });
                } else {
                    j[field] = j[field].toJSON();
                }
            }
        });
        return j;
    };
    return Model;
})(Backbone.Model);

И тогда я могу просто переопределить метод fieldToType, чтобы определить типы моих полей:

PendingAssignmentOffer.prototype.fieldToType = function () {
    return {
        'creator': User,
        'task_templates': TaskTemplateModel,
        'users': User,
        'school_classes': SchoolClass
    };
};
    
ответ дан qbolec 12.02.2015 в 10:29