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

199

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


Scénario un:

Initialisez les enfants dans la fonction d'initialisation du parent. De cette façon, tout ne reste pas bloqué dans le rendu afin 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 de rendu sur le parent pour la deuxième fois supprimera toutes les liaisons d'événements enfant. (C'est à cause du fonctionnement de jQuery $.html().) Cela pourrait être atténué en appelant à la this.child.delegateEvents().render().appendTo(this.$el);place, mais le premier, et le plus souvent, vous faites plus de travail inutilement.

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


Scénario deux:

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

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 le delegateEvents()nécessaire maintenant, ce qui est légèrement négatif par rapport au fait qu'il n'est nécessaire que lors des appels suivants dans le premier scénario.

Troisième scénario:

Initialisez render()plutôt les enfants dans la méthode du parent .

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 désormais également être liée à toute la logique d'initialisation.

  • Si je modifie l'état de l'une des vues enfant, puis que j'appelle 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 savoir ce que vos gars en pensent. Quel scénario utiliseriez-vous? ou y en a-t-il un quatrième magique qui résout tous ces problèmes?

Avez-vous déjà suivi l'état d'un rendu pour une vue? Dites un renderedBeforedrapeau? Semble vraiment dégingandé.

Ian Storm Taylor
la source
1
Je ne suis généralement pas en train de sotring les références aux vues enfant sur les vues parent car la plupart de la communication se fait par le biais de modèles / collections et d'événements déclenchés par des modifications de ceux-ci. Bien que le 3ème cas soit le plus proche de ce que j'utilise la plupart du temps. Cas 1 où cela a du sens. De plus, la plupart du temps, vous ne devez pas restituer l'ensemble de la vue, mais plutôt la partie qui a changé
Tom Tu
Bien sûr, je ne rends définitivement que les parties modifiées lorsque cela est possible, mais même dans ce cas, je pense que la fonction de rendu devrait être disponible et non destructive de toute façon. Comment gérez-vous le fait de ne pas avoir de référence aux enfants dans le parent?
Ian Storm Taylor
Habituellement, j'écoute les événements sur les modèles associés aux vues enfants - si je veux faire quelque chose de plus personnalisé, je lie les événements aux sous-vues. Si ce type de «communication» est nécessaire, je crée généralement une méthode d'aide pour créer des sous-vues qui lie les événements, etc.
Tom Tu
1
Pour une discussion connexe de niveau supérieur, voir: stackoverflow.com/questions/10077185/… .
Ben Roberts
2
Dans le scénario deux, pourquoi est-il nécessaire d'appeler delegateEvents()après le setElement()? Selon les documents: "... et déplacer les événements délégués de la vue de l'ancien élément vers le nouveau", la setElementméthode elle-même doit gérer la redélégation des événements.
rdamborsky

Réponses:

260

c'est une excellente question. L'épine dorsale est excellente en raison du manque d'hypothèses qu'elle fait, mais cela signifie que vous devez (décider comment) mettre en œuvre des choses comme ça vous-même. Après avoir regardé à travers mes propres trucs, je trouve 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 parce que, 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 le gérer avec un exemple. Supposons que cette simple page soit divisée en vues spécifiées:

Répartition des pages

Disons que le HTML est, après avoir été rendu, quelque chose comme 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.

Les ParentViewdétient 2 vues de l' enfant, InfoViewet PhoneListViewainsi que quelques divs supplémentaires, dont l' une #name, doit être fixé à un moment donné. PhoneListViewcontient ses propres vues enfants, un tableau d' PhoneViewentrées.

Passons donc à votre question réelle. Je gère l'initialisation et le rendu différemment en fonction du type de vue. Je divise mes vues en deux types, Parentvues et Childvues.

La différence entre les deux est simple, les Parentvues contiennent des vues enfants, Childcontrairement aux vues. Donc, dans mon exemple, ParentViewet PhoneListViewsont des Parentvues, tandis que InfoViewet les PhoneViewentrées sont des Childvues.

Comme je l'ai mentionné précédemment, la plus grande différence entre ces deux catégories est le moment où elles sont autorisées à effectuer le rendu. Dans un monde parfait, je veux que les Parentvues ne soient rendues qu'une seule fois. Il appartient à leur vue enfant de gérer tout nouveau rendu lorsque le ou les modèles changent. Childvues, d'autre part, je permets de restituer à tout moment dont ils ont besoin car ils n'ont pas d'autres vues en s'appuyant sur eux.

Dans un peu plus de détails, pour les Parentvues, j'aime mes initializefonctions pour faire quelques choses:

  1. Initialiser ma propre vue
  2. Rendre ma propre opinion
  3. Créez et initialisez toutes les vues enfant.
  4. Attribuez à chaque vue enfant un élément dans ma vue (par exemple, la InfoViewserait attribuée #info).

L'étape 1 est assez explicite.

L'étape 2, le rendu, est effectuée de sorte que tous les éléments sur lesquels les vues enfants s'appuient existent déjà avant que j'essaie de les affecter. Ce faisant, je sais que tous les enfants eventsseront correctement définis et je peux restituer leurs blocs autant de fois que je le souhaite sans me soucier de devoir déléguer quoi que ce soit. Je n'ai pas vraiment renderd'opinion d'enfant ici, je leur permets de le faire dans le leur initialization.

Les étapes 3 et 4 sont en fait gérées en même temps que je passe ellors de la création de la vue enfant. J'aime passer un élément ici car je pense que le parent devrait déterminer où à son avis l'enfant est autorisé à mettre son contenu.

Pour le rendu, j'essaie de le garder assez simple pour les Parentvues. Je veux que la renderfonction ne fasse rien de plus que rendre la vue parent. Aucune délégation d'événement, aucun rendu de vues enfants, rien. Juste un simple rendu.

Parfois, cela ne fonctionne pas toujours. Par exemple, dans mon exemple ci-dessus, l' #nameélément devra être mis à jour chaque fois que le nom dans le modèle change. Cependant, ce bloc fait partie du ParentViewmodèle et n'est pas géré par une Childvue dédiée , donc je contourne cela. Je vais créer une sorte de subRenderfonction qui ne remplace que le contenu de l' #nameélément, et je n'aurai pas à jeter la totalité de l' #parentélément. Cela peut sembler être un hack, mais j'ai vraiment trouvé que cela fonctionnait mieux que d'avoir à se soucier de restituer l'ensemble du DOM et de rattacher les éléments et autres. Si je voulais vraiment le nettoyer, je créerais une nouvelle Childvue (similaire à la InfoView) qui gérerait le #namebloc.

Maintenant, pour les Childvues, le initializationest assez similaire aux Parentvues, juste sans la création d'autres Childvues. Alors:

  1. Initialiser ma vue
  2. Le programme d'installation lie l'écoute de tout changement au modèle qui m'importe
  3. Rendre ma vue

Childle rendu de vue est également très simple, il suffit de rendre 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 ParentViewpeut 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 ma mise en œuvre subRenderici. En ayant des modifications liées à subRenderau lieu de render, je n'ai pas à me soucier de dynamiter et de reconstruire le bloc entier.

Voici un exemple de code pour le InfoViewbloc:

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 à me soucier de renderm'appeler manuellement . Si le modèle change, ce bloc se restitue sans affecter les autres vues.

Le PhoneListViewsera similaire au ParentView, vous aurez juste besoin d'un peu plus de logique dans vos fonctions initializationet renderpour gérer les collections. La façon dont vous gérez la collection dépend vraiment de vous, mais vous devrez au moins écouter les événements de la collection et décider de la façon dont vous souhaitez effectuer le rendu (ajouter / supprimer ou simplement restituer le bloc entier). Personnellement, j'aime ajouter de nouvelles vues et supprimer les anciennes, pas restituer la vue entière.

Le PhoneViewsera presque identique au InfoView, n'écoutant que les changements de modèle qui lui tiennent à cœur.

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

Kevin Peel
la source
8
Explication vraiment approfondie merci. Question cependant, j'ai entendu et j'ai tendance à convenir que l'appel renderà l'intérieur de la initializeméthode est une mauvaise pratique, car cela vous empêche d'être plus performant dans les cas où vous ne voulez pas rendre immédiatement. Que pensez-vous de ceci?
Ian Storm Taylor
Sûr. Hmm ... J'essaie seulement d'initialiser les vues qui devraient être affichées maintenant (ou pourraient être affichées dans un proche avenir, par exemple un formulaire d'édition qui est caché maintenant mais affiché en cliquant sur un lien d'édition), donc elles doivent être rendues immédiatement. Si, par exemple, j'ai un cas où une vue prend un certain temps pour être rendue, je ne crée pas la vue elle-même tant qu'elle n'a pas besoin d'être affichée, donc aucune initialisation et rendu n'auront lieu tant que cela ne sera pas nécessaire. Si vous avez un exemple, je peux essayer d'entrer plus en détail sur la façon dont je le gère ...
Kevin Peel
5
C'est une grosse limitation de ne pas pouvoir restituer un ParentView. J'ai une situation où j'ai essentiellement un contrôle d'onglet, où chaque onglet montre un ParentView différent. Je voudrais simplement restituer le ParentView existant lorsqu'un onglet est cliqué une deuxième fois, mais cela provoque la perte d'événements dans les ChildViews (je crée des enfants dans init et les rend dans le rendu). Je suppose que je 1) cache / montre au lieu de rendre, 2) delegateEvents dans le rendu des ChildViews, ou 3) crée des enfants dans le rendu, ou 4) ne vous inquiétez pas de la performance. avant que ce soit un problème et recréez le ParentView à chaque fois. Hmmmm ....
Paul Hoenecke
Je dois dire que c'est une limitation dans mon cas. Cette solution fonctionnera bien si vous n'essayez pas de restituer le parent :) Je n'ai pas une bonne réponse mais j'ai la même question que OP. Espérant toujours trouver la bonne manière de «Backbone». À ce stade, je pense que cacher / montrer et ne pas restituer est la voie à suivre pour moi.
Paul Hoenecke
1
comment passez-vous la collection à l'enfant PhoneListView?
Tri Nguyen
6

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 je propose, ainsi que les avantages et les inconvénients de chacune, devraient vous faire avancer dans la bonne direction.

Derick Bailey
la source
6

Pour moi, cela ne semble pas être la pire idée au monde de faire la différence entre la configuration initiale et les configurations ultérieures de vos vues via une sorte de drapeau. Pour rendre cela propre et facile, le drapeau doit être ajouté à votre propre vue 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 Backbone

Cogneur
la source
3

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

initialize : function () {

    //parent init stuff

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

    this.child = new Child();
},
Nate Barr
la source
2

J'essaie d'éviter le couplage entre des vues comme celles-ci. Il y a deux façons que je fais habituellement:

Utilisez un routeur

Fondamentalement, vous laissez la fonction de votre routeur initialiser la vue parent et enfant. La vue ne se connaît donc 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')});

Les deux ont la connaissance du même DOM, et vous pouvez les commander comme vous le souhaitez.

Sơn Trần-Nguyễn
la source
2

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

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

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

que vous ne 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 nécessaire, et Container génère uniquement 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.

Villa
la source
1

Voici un mixin léger pour créer et rendre des sous-vues, qui, je pense, résout tous les problèmes de ce fil:

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

L'approche adoptée par ce plug-in est de créer et de rendre des sous-vues après le premier rendu de la vue parent. Ensuite, lors des rendus suivants de la vue parent, $ .détachez les éléments de la sous-vue, restituez le parent, puis insérez les éléments de la sous-vue aux endroits appropriés et restituez-les. De cette façon, les sous-vues sont réutilisées sur les rendus suivants, et il n'est pas nécessaire de déléguer à nouveau les événements.

Notez que le cas d'une vue de collection (où chaque modèle de la collection est représenté avec une sous-vue) est assez différent et mérite à mon avis sa propre discussion / solution. La meilleure solution générale que je connaisse dans ce cas est le CollectionView dans Marionette .

EDIT: pour le cas de la vue de la collection, vous pouvez également consulter cette implémentation plus axée sur l'interface utilisateur , si vous avez besoin d'une sélection de modèles en fonction des clics et / ou du glisser-déposer pour la réorganisation.

Brave Dave
la source