web-dev-qa-db-fra.com

Comment gérer l'initialisation et le rendu des sous-vues dans Backbone.js?

J'ai trois façons différentes d'initialiser et de restituer une vue et ses sous-vues, et chacune d'entre elles pose des problèmes différents. Je suis curieux de savoir s'il existe un meilleur moyen de résoudre tous les problèmes:


Premier scénario:

Initialisez les enfants dans la fonction d'initialisation du parent. De cette façon, tout ne reste pas bloqué dans le rendu, de sorte qu'il y ait moins de blocage sur le rendu.

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

    this.$el.html(this.template());

    this.child.render().appendTo(this.$('.container-placeholder');
}

Les problèmes:

  • Le plus gros problème est que l'appel du rendu sur le parent pour une deuxième fois supprimera toutes les liaisons d'événement childs. (Ceci est dû au fonctionnement de $.html() de jQuery.) Cela pourrait être atténué en appelant this.child.delegateEvents().render().appendTo(this.$el); à la place, mais ensuite, le premier et le cas le plus souvent, vous faites plus de travail inutilement .

  • En ajoutant les enfants, vous forcez la fonction de rendu à connaître la structure DOM des parents afin d'obtenir l'ordre que vous souhaitez. Cela signifie que la modification d'un modèle peut nécessiter la mise à jour de la fonction de rendu d'une vue.


Deuxième scénario:

Initialisez les enfants dans la initialize() du parent, mais au lieu de l'ajouter, utilisez setElement().delegateEvents() pour définir l'enfant sur un élément du modèle de parent.

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

    this.$el.html(this.template());

    this.child.setElement(this.$('.placeholder-element')).delegateEvents().render();
}

Problèmes:

  • Cela rend la delegateEvents() nécessaire maintenant, ce qui est un léger inconvénient car elle n'est nécessaire que lors d'appels suivants dans le premier scénario.

Troisième scénario:

Initialisez les enfants dans la méthode render() du parent à la place.

initialize : function () {

    //parent init stuff
},

render : function () {

    this.$el.html(this.template());

    this.child = new Child();

    this.child.appendTo($.('.container-placeholder').render();
}

Problèmes:

  • Cela signifie que la fonction de rendu doit maintenant être liée à toute la logique d'initialisation.

  • Si j'édite l'état de l'une des vues enfants et que j'appelle ensuite render sur le parent, un tout nouvel enfant sera créé et tout son état actuel sera perdu. Ce qui semble également être risqué pour les fuites de mémoire.


Vraiment curieux de connaître le point de vue de vos gars. Quel scénario utiliseriez-vous? ou y en a-t-il un quatrième qui résoudra tous ces problèmes?

Avez-vous déjà gardé la trace d'un état rendu pour une vue? Dites un drapeau renderedBefore? Semble vraiment janky.

198
Ian Storm Taylor

C'est une excellente question. Backbone est formidable en raison de l’absence d’hypothèses, mais cela signifie que vous devez (décider comment) mettre en œuvre de telles choses vous-même. Après avoir parcouru mes propres dossiers, je constate que j'utilise (en quelque sorte) un mélange de scénario 1 et de scénario 2. Je ne pense pas qu'un quatrième scénario magique existe, car, tout simplement, tout ce que vous faites dans les scénarios 1 et 2 doit être terminé.

Je pense qu'il serait plus facile d'expliquer comment j'aime gérer cela avec un exemple. Disons que cette page simple est divisée en vues spécifiées:

Page Breakdown

Supposons que le code HTML, après avoir été rendu, ressemble à ceci:

<div id="parent">
    <div id="name">Person: Kevin Peel</div>
    <div id="info">
        First name: <span class="first_name">Kevin</span><br />
        Last name: <span class="last_name">Peel</span><br />
    </div>
    <div>Phone Numbers:</div>
    <div id="phone_numbers">
        <div>#1: 123-456-7890</div>
        <div>#2: 456-789-0123</div>
    </div>
</div>

J'espère que la façon dont le code HTML correspond au diagramme est assez évidente.

Le ParentView contient 2 vues enfants, InfoView et PhoneListView, ainsi que quelques divs supplémentaires, dont l'un, #name, doit être réglé à un moment donné. PhoneListView contient ses propres vues enfants, un tableau de PhoneView entrées.

Passons à votre question actuelle. Je gère l'initialisation et le rendu différemment en fonction du type de vue. Je divise mes points de vue en deux types, Parent vues et Child vues.

La différence entre eux est simple: les vues Parent conservent les vues enfants alors que les vues Child ne le sont pas. Ainsi, dans mon exemple, ParentView et PhoneListView sont Parent vues, tandis que InfoView et les entrées PhoneView sont Child vues.

Comme je l'ai déjà mentionné, la plus grande différence entre ces deux catégories réside dans le moment où elles sont autorisées à effectuer le rendu. Dans un monde parfait, je veux que Parent les vues ne soient rendues qu'une seule fois. Il appartient à leurs vues enfant de gérer tout rendu lors du changement de modèle. Child vues, par contre, j’autorise à effectuer un nouveau rendu à chaque fois qu’ils en ont besoin, car ils ne disposent d’aucune autre vue.

Plus en détail, pour les vues Parent, j'aime que mes fonctions initialize fassent plusieurs choses:

  1. Initialiser ma propre vue
  2. Rendre mon propre point de vue
  3. Créez et initialisez toutes les vues enfants.
  4. Attribuez à chaque vue enfant un élément dans ma vue (par exemple, le InfoView se verrait attribuer #info).

L'étape 1 est assez explicite.

L'étape 2, le rendu, est effectuée de sorte que tous les éléments sur lesquels reposent les vues enfants existent déjà avant que je ne les affecte. En faisant cela, je sais que tous les enfants events seront correctement configurés et que je pourrai restituer leurs blocs autant de fois que je le voudrai sans me soucier de devoir déléguer à nouveau. En fait, je n'utilise render aucun enfant vu ici, je les autorise à le faire dans leur propre initialization.

Les étapes 3 et 4 sont en réalité traitées en même temps que je passe el lors de la création de la vue enfant. J'aime passer un élément ici car j'estime que le parent devrait déterminer où, de son point de vue, l'enfant est autorisé à mettre son contenu.

Pour le rendu, j'essaie de garder les choses simples pour les vues Parent. Je veux que la fonction render ne fasse que rendre la vue parente. Pas de délégation d'événement, pas de rendu de vues enfants, rien. Juste un rendu simple.

Parfois, cela ne fonctionne pas toujours bien. Par exemple, dans mon exemple ci-dessus, le #name L'élément devra être mis à jour chaque fois que le nom du modèle sera modifié. Cependant, ce bloc fait partie du modèle ParentView et n'est pas géré par une vue dédiée Child. Je contourne donc cela. Je vais créer une sorte de fonction subRender qui seulement remplace le contenu du #name élément, et ne pas avoir à jeter tout le #parent élément. Cela peut sembler être un bidouillage, mais j’ai vraiment trouvé que cela fonctionnait mieux que de devoir redéfinir le rendu de l’ensemble du DOM et réattacher des éléments, etc. Si je voulais vraiment le rendre propre, je créerais une nouvelle vue Child (semblable au InfoView) qui gèrerait le #name bloquer.

Maintenant, pour les vues Child, la vue initialization est assez similaire à la vue Parent, sans la création d'aucune autre vue Child. Alors:

  1. Initialiser ma vue
  2. La configuration oblige à écouter les modifications apportées au modèle qui m’intéresse
  3. Rendre mon avis

Le rendu de la vue Child est également très simple, il suffit de restituer et de définir le contenu de mon el. Encore une fois, pas de problème avec la délégation ou quelque chose comme ça.

Voici un exemple de code de ce à quoi mon ParentView pourrait ressembler:

var ParentView = Backbone.View.extend({
    el: "#parent",
    initialize: function() {
        // Step 1, (init) I want to know anytime the name changes
        this.model.bind("change:first_name", this.subRender, this);
        this.model.bind("change:last_name", this.subRender, this);

        // Step 2, render my own view
        this.render();

        // Step 3/4, create the children and assign elements
        this.infoView = new InfoView({el: "#info", model: this.model});
        this.phoneListView = new PhoneListView({el: "#phone_numbers", model: this.model});
    },
    render: function() {
        // Render my template
        this.$el.html(this.template());

        // Render the name
        this.subRender();
    },
    subRender: function() {
        // Set our name block and only our name block
        $("#name").html("Person: " + this.model.first_name + " " + this.model.last_name);
    }
});

Vous pouvez voir mon implémentation de subRender ici. En ayant des modifications liées à subRender au lieu de render, je n'ai pas à craindre de faire sauter et de reconstruire tout le bloc.

Voici un exemple de code pour le bloc InfoView:

var InfoView = Backbone.View.extend({
    initialize: function() {
        // I want to re-render on changes
        this.model.bind("change", this.render, this);

        // Render
        this.render();
    },
    render: function() {
        // Just render my template
        this.$el.html(this.template());
    }
});

Les liens sont la partie importante ici. En me liant à mon modèle, je n'ai jamais à m'inquiéter d'appeler manuellement moi-même render. Si le modèle change, ce bloc se re-rendra sans affecter les autres vues.

Le PhoneListView sera similaire au ParentView, vous aurez juste besoin d'un peu plus de logique dans vos deux fonctions initialization et render pour gérer les collections. La façon dont vous gérez la collection dépend vraiment de vous, mais vous devez au moins écouter les événements de la collection et décider du mode de rendu (ajouter/supprimer ou simplement restituer le bloc en entier). Personnellement, j'aime bien ajouter de nouvelles vues et supprimer les anciennes, sans refaire le rendu de la vue.

Le PhoneView sera presque identique au InfoView, n'écoutant que les modifications de modèle dont il se soucie.

J'espère que cela a aidé un peu, s'il vous plaît laissez-moi savoir si quelque chose est déroutant ou pas assez détaillé.

258
Kevin Peel

Pour moi, il ne semble pas que la pire idée au monde soit de faire la différence entre la configuration initiale et la configuration suivante de vos vues via une sorte de drapeau. Pour que cela soit facile et net, le drapeau doit être ajouté à votre propre vue, ce qui devrait étendre la vue Backbone (Base).

Comme Derick, je ne suis pas tout à fait sûr que cela réponde directement à votre question, mais je pense que cela mérite au moins d'être mentionné dans ce contexte.

Voir aussi: Utilisation d'un Eventbus dans le Backbone

6
Bruiser

Je ne sais pas si cela répond directement à votre question, mais je pense que c'est pertinent:

http://lostechies.com/derickbailey/2011/10/11/backbone-js-getting-the-model-for-a-clicked-element/

Le contexte dans lequel j’ai mis en place cet article est bien sûr différent, mais je pense que les deux solutions que j’offre, ainsi que les avantages et les inconvénients de chacune d’elles, devraient vous faire avancer dans la bonne direction.

6
Derick Bailey

Kevin Peel donne une excellente réponse - voici ma version de tl; dr:

initialize : function () {

    //parent init stuff

    this.render(); //ANSWER: RENDER THE PARENT BEFORE INITIALIZING THE CHILD!!

    this.child = new Child();
},
3
Nate Barr

Ce que je fais est de donner à chaque enfant une identité (ce que Backbone a déjà fait pour vous: cid)

Lorsque Container effectue le rendu, l'utilisation de 'cid' et de 'tagName' génère un espace réservé pour chaque enfant. Ainsi, dans le modèle, les enfants n'ont aucune idée de l'endroit où ils seront placés par le conteneur.

<tagName id='cid'></tagName>

que vous pouvez utiliser

Container.render()
Child.render();
this.$('#'+cid).replaceWith(child.$el);
// the rapalceWith in jquery will detach the element 
// from the dom first, so we need re-delegateEvents here
child.delegateEvents();

aucun espace réservé spécifié n'est requis et Container ne génère que l'espace réservé plutôt que la structure DOM des enfants. Cotainer et Children génèrent toujours leurs propres éléments DOM et une seule fois.

2
Villa

J'essaie d'éviter le couplage entre des vues comme celles-ci. Je fais habituellement de deux façons:

Utilisez un routeur

En gros, vous laissez la fonction de votre routeur initialiser la vue parent et enfant. Donc, la vue ne se connaît pas, mais le routeur gère tout.

Passer le même el aux deux vues

this.parent = new Parent({el: $('.container-placeholder')});
this.child = new Child({el: $('.container-placeholder')});

Tous deux connaissent le même DOM et vous pouvez les commander quand vous le souhaitez.

2

Voici un mix léger pour la création et le rendu des sous-vues, qui, je pense, résout tous les problèmes de ce fil de discussion:

https://github.com/rotundasoftware/backbone.subviews

L'approche prise par ce plug est de créer et de rendre les sous-vues après le premier affichage de la vue parent. Ensuite, lors des rendus de la vue parent, $ .detachut les éléments de la sous-vue, restituez le rendu du parent, puis insérez les éléments de la sous-vue aux emplacements appropriés et restituez-les. De cette manière, les objets de sous-vues sont réutilisés lors de rendus ultérieurs, sans qu'il soit nécessaire de déléguer à nouveau des événements.

Notez que le cas d'une vue de collection (où chaque modèle de la collection est représenté avec une seule sous-vue) est très différent et mérite sa propre discussion/solution, je pense. La meilleure solution générale que je connaisse pour ce cas est le CollectionView in Marionette .

EDIT: Pour le cas de la vue de collection, vous pouvez également vouloir utiliser cette implémentation davantage axée sur l'interface utilisateur , si vous avez besoin de sélectionner des modèles basés sur des clics et/ou des glisser-déposer pour les réorganiser.

1
Brave Dave