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.