Zagnieżdżone modele w Backbone.js, jak podejść


117

Otrzymałem następujący kod JSON z serwera. W tym celu chcę utworzyć model z modelem zagnieżdżonym. Nie jestem pewien, w jaki sposób można to osiągnąć.

//json
[{
    name : "example",
    layout : {
        x : 100,
        y : 100,
    }
}]

Chcę, aby zostały one przekonwertowane na dwa zagnieżdżone modele szkieletowe o następującej strukturze:

// structure
Image
    Layout
...

Dlatego definiuję model układu w następujący sposób:

var Layout = Backbone.Model.extend({});

Ale której z dwóch (jeśli w ogóle) poniższych technik powinienem użyć do zdefiniowania modelu obrazu? A czy B poniżej?

ZA

var Image = Backbone.Model.extend({
    initialize: function() {
        this.set({ 'layout' : new Layout(this.get('layout')) })
    }
});

lub B.

var Image = Backbone.Model.extend({
    initialize: function() {
        this.layout = new Layout( this.get('layout') );
    }
});

Odpowiedzi:


98

Ten sam problem mam podczas pisania aplikacji Backbone. Konieczność radzenia sobie z modelami osadzonymi / zagnieżdżonymi. Zrobiłem kilka poprawek, które uważałem za całkiem eleganckie rozwiązanie.

Tak, możesz zmodyfikować metodę parsowania, aby zmienić atrybuty w obiekcie, ale wszystko to jest w rzeczywistości kodem IMO nie do utrzymania i wydaje się bardziej hackem niż rozwiązaniem.

Oto, co proponuję na przykład:

Najpierw zdefiniuj swój model układu w ten sposób.

var layoutModel = Backbone.Model.extend({});

Oto Twój model obrazu:

var imageModel = Backbone.Model.extend({

    model: {
        layout: layoutModel,
    },

    parse: function(response){
        for(var key in this.model)
        {
            var embeddedClass = this.model[key];
            var embeddedData = response[key];
            response[key] = new embeddedClass(embeddedData, {parse:true});
        }
        return response;
    }
});

Zauważ, że nie majstrowałem przy samym modelu, a jedynie przekazałem żądany obiekt z metody analizy.

Powinno to zapewnić strukturę modelu zagnieżdżonego podczas czytania z serwera. Teraz można zauważyć, że zapisywanie lub ustawianie w rzeczywistości nie jest tutaj obsługiwane, ponieważ uważam, że sensowne jest ustawienie zagnieżdżonego modelu jawnie przy użyciu właściwego modelu.

Tak jak to:

image.set({layout : new Layout({x: 100, y: 100})})

Zwróć również uwagę, że faktycznie wywołujesz metodę analizy w swoim modelu zagnieżdżonym, wywołując:

new embeddedClass(embeddedData, {parse:true});

Możesz zdefiniować dowolną liczbę modeli zagnieżdżonych w modelpolu.

Oczywiście, jeśli chcesz posunąć się do zapisania zagnieżdżonego modelu we własnej tabeli. To nie wystarczy. Jednak w przypadku czytania i zapisywania obiektu jako całości takie rozwiązanie powinno wystarczyć.


4
To jest miłe… powinna być akceptowaną odpowiedzią, ponieważ jest znacznie czystsza niż inne podejścia. Jedyne sugestie, jakie mam, to zapisanie pierwszej litery twoich zajęć z dużej litery, które rozszerzają Backbone.Model dla czytelności .. tj. ImageModel i LayoutModel
— Stephen Handley

1
@StephenHandley Dzięki za komentarz i twoją sugestię. Dla informacji, faktycznie używam tego w kontekście requireJS. Tak więc, aby odpowiedzieć na kwestię wielkich liter, zmienna „imageModel” jest w rzeczywistości zwracana do requireJS. A odniesienie do modelu byłoby zawarte w następującej konstrukcji: define(['modelFile'], function(MyModel){... do something with MyModel}) Ale masz rację. Mam w zwyczaju odwoływać się do modelu według zaproponowanej przez Ciebie konwencji.
— rycfung

@BobS Przepraszamy, była literówka. Powinna być odpowiedź. Naprawiłem to, dziękuję za wskazanie.
— rycfung

2
Miły! Polecam dodanie tego do Backbone.Model.prototype.parsefunkcji. Następnie wszystkie modele muszą zrobić, aby zdefiniować typy obiektów podmodelu (w atrybucie „model”).
— jasop

1
Chłodny! Skończyło się na tym, że zrobiłem coś podobnego (szczególnie i niestety po tym, jak znalazłem tę odpowiedź) i napisałem to tutaj: blog.untrod.com/2013/08/declarative-approach-to-nesting.html Duża różnica polega na tym, że w przypadku modeli głęboko zagnieżdżonych Deklaruję od razu całe mapowanie w modelu głównym / nadrzędnym, a kod bierze je stamtąd i przechodzi przez cały model, uwadniając odpowiednie obiekty w kolekcje i modele Backbone. Ale naprawdę bardzo podobne podejście.
— Chris Clark

16

Publikuję ten kod jako przykład sugestii Petera Lyona, aby przedefiniować parsowanie. Miałem to samo pytanie i to zadziałało (z backendem Railsów). Ten kod jest napisany w Coffeescript. Wyraźnie przedstawiłem kilka rzeczy osobom, które go nie znają.

class AppName.Collections.PostsCollection extends Backbone.Collection
  model: AppName.Models.Post

  url: '/posts'

  ...

  # parse: redefined to allow for nested models
  parse: (response) ->  # function definition
     # convert each comment attribute into a CommentsCollection
    if _.isArray response
      _.each response, (obj) ->
        obj.comments = new AppName.Collections.CommentsCollection obj.comments
    else
      response.comments = new AppName.Collections.CommentsCollection response.comments

    return response

lub w JS

parse: function(response) {
  if (_.isArray(response)) {
    return _.each(response, function(obj) {
      return obj.comments = new AppName.Collections.CommentsCollection(obj.comments);
    });
  } else {
    response.comments = new AppName.Collections.CommentsCollection(response.comments);
  }
  return response;
};

Rekwizyty dla przykładowego kodu i sugerujące przesłanianie analizy. Dzięki!
— Edward Anderson

11
byłoby miło otrzymać odpowiedź w prawdziwym JS
— Jason,

6
cieszę się, że mam wersję coffeescript, dzięki. Dla innych, wypróbuj js2coffee.org
— ABCD.ca

16
Jeśli pytanie jest prawdziwym JS, odpowiedź również powinna być.
— Manuel Hernandez


11

Nie jestem pewien, czy sam Backbone ma zalecany sposób, aby to zrobić. Czy obiekt układu ma własny identyfikator i rekord w wewnętrznej bazie danych? Jeśli tak, możesz zrobić z niego swój własny Model, tak jak masz. Jeśli nie, możesz po prostu pozostawić go jako zagnieżdżony dokument, po prostu upewnij się, że konwertujesz go na format JSON iz niego poprawnie w metodach savei parse. Jeśli ostatecznie podejmiesz takie podejście, myślę, że Twój przykład A jest bardziej spójny z kręgosłupem, ponieważ setzostanie poprawnie zaktualizowany attributes, ale znowu nie jestem pewien, co Backbone robi domyślnie z modelami zagnieżdżonymi. Prawdopodobnie będziesz potrzebować niestandardowego kodu, aby to obsłużyć.


Ach! Przepraszamy, brakowało newoperatora. Edytowałem go, aby naprawić ten błąd.
— Ross

Och, więc źle zinterpretowałem twoje pytanie. Zaktualizuję odpowiedź.
— Peter Lyons,

8

Wybrałbym opcję B, jeśli chcesz, aby wszystko było proste.

Inną dobrą opcją byłoby użycie Backbone-Relational . Po prostu zdefiniowałbyś coś takiego:

var Image = Backbone.Model.extend({
    relations: [
        {
            type: Backbone.HasOne,
            key: 'layout',
            relatedModel: 'Layout'
        }
    ]
});

+1 Backbone-Releational wydaje się dość ugruntowany: własna strona internetowa, 1,6 tys. Gwiazdek, ponad 200 widelców.
— Ross


5

Wersja CoffeeScript pięknej odpowiedzi rycfung :

class ImageModel extends Backbone.Model
  model: {
      layout: LayoutModel
  }

  parse: (response) =>
    for propName,propModel of @model
      response[propName] = new propModel( response[propName], {parse:true, parentModel:this} )

    return response

Czy to nie słodkie? ;)


11
Nie biorę cukru w ​​moim JavaScript
— Ross

2

Miałem ten sam problem i eksperymentowałem z kodem w odpowiedzi rycfung , co jest świetną sugestią.
Jeśli jednak nie chcesz setbezpośrednio zagnieżdżać modeli lub nie chcesz ciągle przekazywać {parse: true}ich options, innym podejściem byłoby przedefiniowanie setsiebie.

W Backbone 1.0.0 , setnazywa się constructor, unset, clear, fetchi save.

Rozważ następujący super model dla wszystkich modeli, które muszą zagnieżdżać modele i / lub kolekcje.

/** Compound supermodel */
var CompoundModel = Backbone.Model.extend({
    /** Override with: key = attribute, value = Model / Collection */
    model: {},

    /** Override default setter, to create nested models. */
    set: function(key, val, options) {
        var attrs, prev;
        if (key == null) { return this; }

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

        // Run validation.
        if (options) { options.validate = true; }
        else { options = { validate: true }; }

        // For each `set` attribute, apply the respective nested model.
        if (!options.unset) {
            for (key in attrs) {
                if (key in this.model) {
                    if (!(attrs[key] instanceof this.model[key])) {
                        attrs[key] = new this.model[key](attrs[key]);
                    }
                }
            }
        }

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

        if (!(attrs = this.changedAttributes())) { return this; }

        // Bind new nested models and unbind previous nested models.
        for (key in attrs) {
            if (key in this.model) {
                if (prev = this.previous(key)) {
                    this._unsetModel(key, prev);
                }
                if (!options.unset) {
                    this._setModel(key, attrs[key]);
                }
            }
        }
        return this;
    },

    /** Callback for `set` nested models.
     *  Receives:
     *      (String) key: the key on which the model is `set`.
     *      (Object) model: the `set` nested model.
     */
    _setModel: function (key, model) {},

    /** Callback for `unset` nested models.
     *  Receives:
     *      (String) key: the key on which the model is `unset`.
     *      (Object) model: the `unset` nested model.
     */
    _unsetModel: function (key, model) {}
});

Zauważ, że model, _setModeli _unsetModelsą pozostawione puste celowo. Na tym poziomie abstrakcji prawdopodobnie nie możesz zdefiniować żadnych rozsądnych działań dla wywołań zwrotnych. Możesz jednak chcieć zastąpić je w podmodelach, które rozszerzają CompoundModel.
Te wywołania zwrotne są przydatne, na przykład, do wiązania detektorów i propagowania changezdarzeń.


Przykład:

var Layout = Backbone.Model.extend({});

var Image = CompoundModel.extend({
    defaults: function () {
        return {
            name: "example",
            layout: { x: 0, y: 0 }
        };
    },

    /** We need to override this, to define the nested model. */
    model: { layout: Layout },

    initialize: function () {
        _.bindAll(this, "_propagateChange");
    },

    /** Callback to propagate "change" events. */
    _propagateChange: function () {
        this.trigger("change:layout", this, this.get("layout"), null);
        this.trigger("change", this, null);
    },

    /** We override this callback to bind the listener.
     *  This is called when a Layout is set.
     */
    _setModel: function (key, model) {
        if (key !== "layout") { return false; }
        this.listenTo(model, "change", this._propagateChange);
    },

    /** We override this callback to unbind the listener.
     *  This is called when a Layout is unset, or overwritten.
     */
    _unsetModel: function (key, model) {
        if (key !== "layout") { return false; }
        this.stopListening();
    }
});

Dzięki temu masz automatyczne tworzenie zagnieżdżonych modeli i propagację zdarzeń. Przykładowe użycie jest również dostarczane i testowane:

function logStringified (obj) {
    console.log(JSON.stringify(obj));
}

// Create an image with the default attributes.
// Note that a Layout model is created too,
// since we have a default value for "layout".
var img = new Image();
logStringified(img);

// Log the image everytime a "change" is fired.
img.on("change", logStringified);

// Creates the nested model with the given attributes.
img.set("layout", { x: 100, y: 100 });

// Writing on the layout propagates "change" to the image.
// This makes the image also fire a "change", because of `_propagateChange`.
img.get("layout").set("x", 50);

// You may also set model instances yourself.
img.set("layout", new Layout({ x: 100, y: 100 }));

Wynik:

{"name":"example","layout":{"x":0,"y":0}}
{"name":"example","layout":{"x":100,"y":100}}
{"name":"example","layout":{"x":50,"y":100}}
{"name":"example","layout":{"x":100,"y":100}}

2

Zdaję sobie sprawę, że spóźniłem się na tę imprezę, ale niedawno wydaliśmy wtyczkę, aby poradzić sobie dokładnie z tym scenariuszem. Nazywa się to backbone-nestify .

Więc twój zagnieżdżony model pozostaje niezmieniony:

var Layout = Backbone.Model.extend({...});

Następnie użyj wtyczki podczas definiowania modelu zawierającego (za pomocą Underscore.extend ):

var spec = {
    layout: Layout
};
var Image = Backbone.Model.extend(_.extend({
    // ...
}, nestify(spec));

Następnie, zakładając, że masz model, mktóry jest instancją Imagei ustawiłeś JSON z pytania m, możesz wykonać:

m.get("layout");    //returns the nested instance of Layout
m.get("layout|x");  //returns 100
m.set("layout|x", 50);
m.get("layout|x");  //returns 50

2

Używaj form szkieletowych

Obsługuje zagnieżdżone formularze, modele i toJSON. WSZYSTKO ZAGNIEŻDŻONE

var Address = Backbone.Model.extend({
    schema: {
    street:  'Text'
    },

    defaults: {
    street: "Arteaga"
    }

});


var User = Backbone.Model.extend({
    schema: {
    title:      { type: 'Select', options: ['Mr', 'Mrs', 'Ms'] },
    name:       'Text',
    email:      { validators: ['required', 'email'] },
    birthday:   'Date',
    password:   'Password',
    address:    { type: 'NestedModel', model: Address },
    notes:      { type: 'List', itemType: 'Text' }
    },

    constructor: function(){
    Backbone.Model.apply(this, arguments);
    },

    defaults: {
    email: "x@x.com"
    }
});

var user = new User();

user.set({address: {street: "my other street"}});

console.log(user.toJSON()["address"]["street"])
//=> my other street

var form = new Backbone.Form({
    model: user
}).render();

$('body').append(form.el);

1

Jeśli nie chcesz, aby dodać kolejną ramy, można rozważyć utworzenie klasy bazowej z przesłonięte seti toJSONi używać go tak:

// 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

Będziesz potrzebować BaseModeltej odpowiedzi (dostępnej, jeśli chcesz, jako streszczenie ).


1

My też mamy ten problem, a pracownik zespołu zaimplementował wtyczkę o nazwie backbone-nested-attributes.

Użycie jest bardzo proste. Przykład:

var Tree = Backbone.Model.extend({
  relations: [
    {
      key: 'fruits',
      relatedModel: function () { return Fruit }
    }
  ]
})

var Fruit = Backbone.Model.extend({
})

Dzięki temu model Tree może uzyskać dostęp do owoców:

tree.get('fruits')

Więcej informacji można znaleźć tutaj:

https://github.com/dtmtec/backbone-nested-attributes

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.