¿Cómo manejar la inicialización y representación de subvistas en Backbone.js?

199

Tengo tres formas diferentes de inicializar y representar una vista y sus subvistas, y cada una de ellas tiene diferentes problemas. Tengo curiosidad por saber si hay una mejor manera de resolver todos los problemas:


Escenario uno:

Inicialice los hijos en la función de inicialización de los padres. De esta manera, no todo se atasca en el renderizado, por lo que hay menos bloqueo en el renderizado.

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

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

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

Los problemas:

  • El mayor problema es que llamar al renderizado en el padre por segunda vez eliminará todos los enlaces de eventos del niño. (Esto se debe a cómo funciona jQuery $.html()). Esto podría mitigarse llamando en su this.child.delegateEvents().render().appendTo(this.$el);lugar, pero luego, en el primer caso, y el más frecuente, estás haciendo más trabajo innecesariamente.

  • Al agregar los elementos secundarios, obliga a la función de representación a conocer la estructura DOM de los padres para obtener el orden que desea. Lo que significa que cambiar una plantilla puede requerir actualizar la función de representación de una vista.


Escenario dos:

Inicialice los elementos secundarios en la imagen fija de los padres initialize(), pero en lugar de agregarlos, use setElement().delegateEvents()para establecer el elemento secundario en un elemento en la plantilla de los padres.

initialize : function () {

    //parent init stuff

    this.child = new Child();
},

render : function () {

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

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

Problemas:

  • Esto hace lo delegateEvents()necesario ahora, lo cual es un poco negativo, ya que solo es necesario en llamadas posteriores en el primer escenario.

Escenario tres:

Inicialice los hijos en el render()método de los padres en su lugar.

initialize : function () {

    //parent init stuff
},

render : function () {

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

    this.child = new Child();

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

Problemas:

  • Esto significa que la función de renderizado ahora también debe estar vinculada con toda la lógica de inicialización.

  • Si edito el estado de una de las vistas secundarias, y luego llamo render en el padre, se creará un hijo completamente nuevo y se perderá todo su estado actual. Lo que también parece que podría ser peligroso para las pérdidas de memoria.


Realmente curioso por ver a tus muchachos en esto. ¿Qué escenario usarías? ¿O hay un cuarto mágico que resuelve todos estos problemas?

¿Alguna vez ha realizado un seguimiento de un estado representado para una Vista? ¿Decir una renderedBeforebandera? Parece realmente extraño.

Ian Storm Taylor
fuente
1
Por lo general, no escribo referencias a las vistas secundarias en las vistas primarias porque la mayor parte de la comunicación ocurre a través de modelos / colecciones y eventos provocados por cambios en estos. Aunque el tercer caso es el más cercano a lo que uso la mayor parte del tiempo. Caso 1 donde tiene sentido. Además, la mayoría de las veces no debería volver a mostrar toda la vista, sino la parte que ha cambiado
Tom Tu
Claro, definitivamente renderizo solo las partes cambiadas cuando es posible, pero aun así creo que la función de render debería estar disponible y no ser destructiva de todos modos. ¿Cómo manejas no tener una referencia a los hijos en el padre?
Ian Storm Taylor
Por lo general, escucho eventos en los modelos asociados a las vistas secundarias; si quiero hacer algo más personalizado, estoy vinculando eventos a subvistas. Si este tipo de 'comunicación' se requiere entonces yo estoy por lo general la creación método de ayuda para la creación de subvistas que se une a los eventos, etc.
Tom Tu
1
Para una discusión relacionada de nivel superior, consulte: stackoverflow.com/questions/10077185/… .
Ben Roberts
2
En el escenario dos, ¿por qué es necesario llamar delegateEvents()después del setElement()? Según los documentos: "... y mover los eventos delegados de la vista del elemento antiguo al nuevo", el setElementmétodo en sí mismo debe manejar la nueva delegación de eventos.
rdamborsky

Respuestas:

260

Esta es una gran pregunta. La columna vertebral es excelente debido a la falta de suposiciones que hace, pero significa que debe (decidir cómo) implementar cosas como esta usted mismo. Después de revisar mis propias cosas, descubro que (más o menos) uso una combinación de escenario 1 y escenario 2. No creo que exista un cuarto escenario mágico porque, simplemente, todo lo que haces en los escenarios 1 y 2 debe ser hecho.

Creo que sería más fácil explicar cómo me gusta manejarlo con un ejemplo. Digamos que tengo esta página simple dividida en las vistas especificadas:

Desglose de página

Digamos que el HTML es, después de ser renderizado, algo como esto:

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

Esperemos que sea bastante obvio cómo el HTML coincide con el diagrama.

El ParentViewsostiene 2 vistas secundarias, InfoViewy PhoneListViewasí como algunos divs adicionales, uno de los cuales, #name, se debe establecer en algún momento. PhoneListViewcontiene vistas secundarias propias, una serie de PhoneViewentradas.

Entonces a su pregunta real. Manejo la inicialización y el renderizado de manera diferente según el tipo de vista. Divido mis puntos de vista en dos tipos, Parentpuntos de vista y Childpuntos de vista.

La diferencia entre ellos es simple, las Parentvistas tienen vistas secundarias mientras que las Childvistas no. Entonces, en mi ejemplo, ParentViewy PhoneListViewson Parentvistas, while InfoViewy las PhoneViewentradas son Childvistas.

Como mencioné antes, la mayor diferencia entre estas dos categorías es cuando se les permite renderizar. En un mundo perfecto, quiero que las Parentvistas solo se muestren una vez. Depende de sus vistas secundarias manejar cualquier representación cuando los modelos cambien. Childvistas, por otro lado, permito volver a renderizar en cualquier momento que lo necesiten, ya que no tienen ninguna otra vista que se base en ellas.

En un poco más de detalle, para las Parentvistas me gusta que mis initializefunciones hagan algunas cosas:

  1. Inicializar mi propia vista
  2. Render mi propio punto de vista
  3. Cree e inicialice las vistas secundarias.
  4. Asigne a cada vista secundaria un elemento dentro de mi vista (por ejemplo, InfoViewse le asignaría #info).

El paso 1 se explica por sí mismo.

El paso 2, la representación, se realiza para que los elementos en los que se basan las vistas secundarias ya existan antes de que intente asignarlos. Al hacer esto, sé que todos los niños eventsestarán configurados correctamente, y puedo volver a renderizar sus bloques tantas veces como quiera sin preocuparme de tener que volver a delegar nada. En realidad, no veo renderningún punto de vista infantil aquí, les permito que lo hagan dentro de los suyos initialization.

Los pasos 3 y 4 se manejan realmente al mismo tiempo que paso elal crear la vista secundaria. Me gusta pasar un elemento aquí, ya que siento que el padre debe determinar dónde, según su propia opinión, el niño puede poner su contenido.

Para la representación, trato de mantenerlo bastante simple para las Parentvistas. Quiero que la renderfunción no haga nada más que representar la vista principal. Sin delegación de eventos, sin representación de vistas secundarias, nada. Solo un simple render.

Sin embargo, a veces esto no siempre funciona. Por ejemplo, en mi ejemplo anterior, el #nameelemento deberá actualizarse cada vez que cambie el nombre dentro del modelo. Sin embargo, este bloque es parte de la ParentViewplantilla y no es manejado por una Childvista dedicada , así que evito eso. Crearé algún tipo de subRenderfunción que solo reemplace el contenido del #nameelemento, y no tenga que desechar todo el #parentelemento. Esto puede parecer un truco, pero realmente he descubierto que funciona mejor que tener que preocuparme por volver a representar todo el DOM y volver a conectar elementos y demás. Si realmente quisiera hacerlo limpio, crearía una nueva Childvista (similar a la InfoView) que manejaría el #namebloque.

Ahora, para las Childvistas, initializationes bastante similar a las Parentvistas, solo que sin la creación de más Childvistas. Entonces:

  1. Inicializar mi vista
  2. La configuración se vincula a la escucha de cualquier cambio en el modelo que me interesa
  3. Render mi punto de vista

ChildLa visualización de renderizado también es muy simple, solo renderiza y configura el contenido de mi el. Una vez más, no te metas con la delegación ni nada de eso.

Aquí hay un código de ejemplo de cómo se ParentViewpuede ver mi :

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);
    }
});

Puedes ver mi implementación de subRenderaquí. Al tener cambios vinculados en subRenderlugar de render, no tengo que preocuparme por volar y reconstruir todo el bloque.

Aquí hay un código de ejemplo para el InfoViewbloque:

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());
    }
});

Los enlaces son la parte importante aquí. Al vincularme a mi modelo, nunca tengo que preocuparme de llamarme manualmente render. Si el modelo cambia, este bloque se volverá a representar sin afectar ninguna otra vista.

El PhoneListViewserá similar a la ParentView, usted sólo necesita un poco más de la lógica, tanto en sus initializationy renderfunciones a las colecciones de la manija. La forma en que maneja la colección depende de usted, pero al menos deberá escuchar los eventos de la colección y decidir cómo desea renderizar (agregar / eliminar o simplemente volver a renderizar todo el bloque). Personalmente, me gusta agregar nuevas vistas y eliminar las viejas, no volver a renderizar toda la vista.

El PhoneViewserá casi idéntico al InfoViewsólo escuchar a los cambios en el modelo que se preocupa.

Espero que esto haya ayudado un poco, avíseme si algo es confuso o no está lo suficientemente detallado.

Kevin Peel
fuente
8
Realmente explicación completa gracias. Sin embargo, he escuchado y tiendo a estar de acuerdo en que llamar renderdentro del initializemétodo es una mala práctica, ya que evita que seas más eficiente en los casos en que no quieres renderizar de inmediato. ¿Qué piensas sobre esto?
Ian Storm Taylor
Por supuesto. Hmm ... Solo trato de tener vistas inicializadas que deberían mostrarse ahora (o podrían mostrarse en el futuro cercano, por ejemplo, un formulario de edición que está oculto ahora pero que se muestra al hacer clic en un enlace de edición), por lo que deberían mostrarse de inmediato. Si digo, tengo un caso en el que una vista tarda un tiempo en renderizarse, personalmente no creo la vista en sí hasta que deba mostrarse, por lo que no se realizará la inicialización ni la representación hasta que sea necesario. Si tiene un ejemplo, puedo tratar de entrar en más detalles sobre cómo lo manejo ...
Kevin Peel
55
Es una gran limitación no poder volver a representar un ParentView. Tengo una situación en la que básicamente tengo un control de pestaña, donde cada pestaña muestra un ParentView diferente. Me gustaría volver a renderizar el ParentView existente cuando se hace clic en una pestaña por segunda vez, pero al hacerlo se pierden eventos en ChildViews (creo hijos en init y los renderizo en render). Supongo que o bien 1) esconder / mostrar en lugar de renderizar, 2) delegar eventos en el renderizado de ChildViews, o 3) crear hijos en render, o 4) no preocuparme por el rendimiento. antes de que sea un problema y vuelva a crear ParentView cada vez. Hmmmm ....
Paul Hoenecke
Debo decir que es una limitación en mi caso. Esta solución funcionará bien si no intentas volver a representar al padre :) No tengo una buena respuesta pero tengo la misma pregunta que OP. Todavía espero encontrar la forma adecuada de 'Backbone'. En este punto, estoy pensando en ocultar / mostrar y no volver a renderizar es el camino a seguir para mí.
Paul Hoenecke
1
¿Cómo se pasa la colección al niño PhoneListView?
Tri Nguyen
6

Para mí, no parece la peor idea del mundo para diferenciar entre la configuración inicial y las configuraciones posteriores de sus puntos de vista a través de algún tipo de bandera. Para hacer esto limpio y fácil, la bandera debe agregarse a su propia Vista, que debe extender la Vista Backbone (Base).

Al igual que Derick, no estoy completamente seguro de si esto responde directamente a su pregunta, pero creo que al menos vale la pena mencionarlo en este contexto.

Ver también: Uso de un Eventbus en Backbone

Matón
fuente
3

Kevin Peel da una gran respuesta: aquí está mi versión tl; dr:

initialize : function () {

    //parent init stuff

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

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

Estoy tratando de evitar el acoplamiento entre puntos de vista como estos. Hay dos formas en que suelo hacerlo:

Usa un enrutador

Básicamente, permite que la función del enrutador inicialice la vista principal y secundaria. Por lo tanto, la vista no se conoce entre sí, pero el enrutador lo maneja todo.

Pasando el mismo el a ambas vistas

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

Ambos tienen conocimiento del mismo DOM, y puede ordenarlos de la forma que desee.

Sơn Trần-Nguyễn
fuente
2

Lo que hago es dar a cada niño una identidad (que Backbone ya lo ha hecho por usted: cid)

Cuando Container realiza el renderizado, el uso de 'cid' y 'tagName' genera un marcador de posición para cada elemento secundario, por lo que en la plantilla, los elementos secundarios no tienen idea de dónde lo colocará el contenedor.

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

de lo que puedes usar

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();

no se necesita un marcador de posición específico, y Contenedor solo genera el marcador de posición en lugar de la estructura DOM del niño. Cotainer y Children siguen generando elementos DOM propios y solo una vez.

Villa
fuente
1

Aquí hay una combinación ligera para crear y renderizar subvistas, que creo que aborda todos los problemas en este hilo:

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

El enfoque adoptado por este complemento es crear y representar subvistas después de la primera vez que se representa la vista principal. Luego, en representaciones posteriores de la vista principal, $ .detach los elementos de la subvista, vuelva a representar la matriz, luego inserte los elementos de la subvista en los lugares apropiados y vuelva a representarlos. De esta manera, los objetos de subvistas se reutilizan en representaciones posteriores y no es necesario volver a delegar eventos.

Tenga en cuenta que el caso de una vista de colección (donde cada modelo de la colección se representa con una subvista) es bastante diferente y creo que merece su propia discusión / solución. La mejor solución general que conozco para ese caso es el CollectionView en Marionette .

EDITAR: para el caso de la vista de colección, también puede consultar esta implementación más centrada en la interfaz de usuario , si necesita seleccionar modelos basados ​​en clics y / o arrastrar y soltar para reordenar.

Valiente Dave
fuente