Pregunta BackboneJS problemas de representación


Durante los últimos seis meses he estado trabajando con Backbone. Los primeros dos meses fueron por ahí, aprendiendo y averiguando cómo quiero estructurar mi código al respecto. Los siguientes 4 meses estaban consumiendo una aplicación de ajuste de producción. No me malinterpreten, Backbone me ha salvado del lío de miles de líneas del código del cliente que era el estándar anterior, pero me permitió hacer cosas más grandiosas en menos tiempo, abriendo una nueva pila de problemas. Para todas las preguntas que planteo aquí hay soluciones simples que se sienten como hacks o simplemente se sienten incorrecto. Prometo una recompensa de 300 puntos por una solución increíble. Aquí va:

  1. Cargando - Para nuestro caso de uso (un panel de administración), la sincronización pesimista es mala. Para algunas cosas, necesito validar cosas en el servidor antes de aceptarlas. Comenzamos antes de que el evento 'sync' se fusionara en Backbone,

y usamos este pequeño código para imitar el evento de carga:

window.old_sync = Backbone.sync

# Add a loading event to backbone.sync
Backbone.sync = (method, model, options) ->
  old_sync(method, model, options)
  model.trigger("loading")

Estupendo. Funciona como se esperaba, pero no se siente correcto. Vinculamos este evento a todas las vistas relevantes y mostramos un ícono de carga hasta que recibamos un evento de éxito o error de ese modelo. ¿Hay una manera mejor y más sana de hacer esto?

Ahora para los duros:

  1. Demasiadas cosas se rinden demasiado - Digamos que nuestra aplicación tiene pestañas. Cada pestaña controla una colección. En el lado izquierdo obtienes la colección. Hace clic en un modelo para comenzar a editarlo en el centro. Cambia su nombre y presiona la pestaña para ir al siguiente elemento del formulario. Ahora, su aplicación es un "algo en tiempo real algo" que nota la diferencia, ejecuta validaciones y sincroniza automáticamente el cambio al servidor, ¡sin necesidad de guardar el botón! Genial, pero el H2 al comienzo del formulario tiene el mismo nombre que en la entrada, necesita actualizarlo. Ah, y necesita actualizar el nombre en la lista al costado. OH, ¡y la lista se ordena por nombres!

Aquí hay otro ejemplo: desea crear un nuevo elemento en la colección. Presiona el botón "nuevo" y comienza a completar el formulario. ¿Inmediatamente agregas el artículo a la colección? Pero, ¿qué sucede si decidiste descartarlo? O si guarda la colección completa en otra pestaña? Y, hay una carga de archivo: debe guardar y sincronizar el modelo antes de poder comenzar a cargar el archivo (para que pueda adjuntar el archivo al modelo). Entonces todo comienza a renderizarse en temblores: guarda el modelo y la lista y el formulario vuelve a mostrarse: está sincronizado ahora, por lo que obtiene un nuevo botón de eliminar y se muestra en la lista, pero ahora la carga del archivo terminó de cargarse, por lo que todo comienza a renderizar nuevamente

Agregue subvistas a la mezcla y todo comienza a parecerse a una película de Fellini.

  1. Son subvistas todo el camino - Aquí hay un buen artículo sobre esto. No pude, por amor a todo lo que es sagrado, encontrar una forma correcta de adjuntar los plugins jQuery o los eventos DOM a cualquier vista que tenga subvistas. El infierno sigue rápidamente. La información sobre herramientas detecta un render que tarda mucho tiempo y comienza a flipar, las subvistas se vuelven zombis o no responden. Este es el principal punto de dolor, ya que aquí están los errores reales, pero todavía no tengo una solución completa.

  2. Parpadeo - La representación es rápida. De hecho, es tan rápido que mi pantalla parece que tuvo un ataque. A veces son las imágenes las que tienen que volver a cargarse (¡con otra llamada al servidor!), Por lo que el html se minimiza y luego se maximiza de nuevo abruptamente; un ancho css + altura para ese elemento lo arreglará. a veces podemos resolver esto con un fadeIn y un fadeOut, que son un dolor de cabeza para escribir, ya que a veces estamos reutilizando una vista y, a veces, creándola de nuevo.

TL; DR - Tengo problemas con vistas y subvistas en Backbone - Se renderiza demasiadas veces, parpadea cuando se renderiza, las subvistas separan mis eventos DOM y se comen mis cerebros.

¡Gracias!

Más detalles: BackboneJS con Ruby on Rails Gem. Plantillas que usan plantillas UnderscoreJS.


32
2017-08-17 11:01


origen


Respuestas:


Representación parcial de vistas

Para minimizar la representación completa de su jerarquía DOM, puede configurar nodos especiales en su DOM que reflejarán las actualizaciones en una propiedad determinada.

Usemos esta plantilla de subrayado simple, una lista de nombres:

<ul>
  <% _(children).each(function(model) { %>
    <li>
        <span class='model-<%= model.cid %>-name'><%= model.name %></span> :
        <span class='model-<%= model.cid %>-name'><%= model.name %></span>
    </li>
  <% }); %>
</ul>

Observe la clase model-<%= model.cid %>-name, este será nuestro punto de inyección.

Luego podemos definir una vista base (o modificar Backbone.View) para llenar estos nodos con los valores apropiados cuando se actualicen:

var V = Backbone.View.extend({
    initialize: function () {
        // bind all changes to the models in the collection
        this.collection.on('change', this.autoupdate, this);
    },

    // grab the changes and fill any zone set to receive the values
    autoupdate: function (model) {
        var _this = this,
            changes = model.changedAttributes(),
            attrs = _.keys(changes);

        _.each(attrs, function (attr) {
            _this.$('.model-' + model.cid + '-' + attr).html(model.get(attr));
        });
    },

    // render the complete template
    // should only happen when there really is a dramatic change to the view
    render: function () {
        var data, html;

        // build the data to render the template
        // this.collection.toJSON() with the cid added, in fact
        data = this.collection.map(function (model) {
            return _.extend(model.toJSON(), {cid: model.cid});
        });

        html = template({children: data});
        this.$el.html(html);

        return this;
    }
});

El código podría variar un poco para acomodar un modelo en lugar de una colección. Un violín con el que jugar http://jsfiddle.net/nikoshr/cfcDX/

Limitando las manipulaciones DOM

Delegar el renderizado a las subvistas puede ser costoso, sus fragmentos de HTML deben insertarse en el DOM del padre. Echa un vistazo a esto Prueba jsperf comparando diferentes métodos de renderizado

La esencia de esto es que generar la estructura HTML completa y luego aplicar vistas es mucho más rápido que crear vistas y subvistas y luego poner en cascada el renderizado. Por ejemplo,

<script id="tpl-table" type="text/template">
    <table>
        <thead>
            <tr>
                <th>Row</th>
                <th>Name</th>
            </tr>
        </thead>
        <tbody>
        <% _(children).each(function(model) { %>
            <tr id='<%= model.cid %>'>
                <td><%= model.row %></td>
                <td><%= model.name %></td>
            </tr>
        <% }); %>
        </tbody>
     </table>
</script>
var ItemView = Backbone.View.extend({
});

var ListView = Backbone.View.extend({
    render: function () {
        var data, html, $table, template = this.options.template;

        data = this.collection.map(function (model) {
            return _.extend(model.toJSON(), {
                cid: model.cid
            });
        });

        html = this.options.template({
            children: data
        });

        $table = $(html);

        this.collection.each(function (model) {
            var subview = new ItemView({
                el: $table.find("#" + model.cid),
                model: model
            });
        });

        this.$el.empty();
        this.$el.append($table);

        return this;
    }
});


var view = new ListView({
    template: _.template($('#tpl-table').html()),
    collection: new Backbone.Collection(data)
});

http://jsfiddle.net/nikoshr/UeefE/

Tenga en cuenta que jsperf muestra que la plantilla se puede dividir en subplanos sin demasiada penalización, lo que le permitiría proporcionar una representación parcial de las filas.

En una nota relacionada, no trabaje en nodos conectados al DOM, esto causará reflujos innecesarios. Cree un DOM nuevo o separe el nodo antes de manipularlo.

Aplastando zombies

Derick Bailey escribió un excelente artículo sobre el tema de erradicar las vistas de zombies

Básicamente, debe recordar que cuando descarta una vista, debe desvincular a todos los oyentes y realizar cualquier limpieza adicional, como destruir las instancias del complemento jQuery. Lo que uso es una combinación de métodos similares a los que Derick usa en Backbone.Marionette:

var BaseView = Backbone.View.extend({

    initialize: function () {
        // list of subviews
        this.views = [];
    },

    // handle the subviews
    // override to destroy jQuery plugin instances
    unstage: function () {
        if (!this.views) {
            return;
        }

        var i, l = this.views.length;

        for (i = 0; i < l; i = i + 1) {
            this.views[i].destroy();
        }
        this.views = [];
    },

    // override to setup jQuery plugin instances
    stage: function () {
    },

    // destroy the view
    destroy: function () {
        this.unstage();
        this.remove();
        this.off();

        if (this.collection) {
            this.collection.off(null, null, this);
        }
        if (this.model) {
            this.model.off(null, null, this);
        }
    }
});

La actualización de mi ejemplo anterior para dar a las filas un comportamiento arrastrable se vería así:

var ItemView = BaseView.extend({
    stage: function () {
        this.$el.draggable({
            revert: "invalid",
            helper: "clone"
        });
    },

    unstage: function () {
        this.$el.draggable('destroy');
        BaseView.prototype.unstage.call(this);
    }
});

var ListView = BaseView.extend({

    render: function () {
       //same as before

        this.unstage();
        this.collection.each(function (model) {
            var subview = new ItemView({
                el: $table.find("#" + model.cid),
                model: model
            });
            subview.stage();
            this.views.push(subview);
        }, this);
        this.stage();

        this.$el.empty();
        this.$el.append($table);

        return this;
    }
});

http://jsfiddle.net/nikoshr/yL7g6/

La destrucción de la vista raíz recorrerá la jerarquía de vistas y realizará las limpiezas necesarias.

NB: lo siento por el código JS, no estoy lo suficientemente familiarizado con Coffeescript para proporcionar fragmentos precisos.


16
2017-08-17 12:49



Ok, en orden ... :)

  1. Cargando...

En caso de que quiera validar los datos que se almacenan en el servidor, buenas prácticas hágalo en el lado del servidor. Si la validación en el servidor no tiene éxito, el servidor debe enviar un código HTTP no 200, por lo tanto, guarde el método de Backbone.Model provocará un error.

Otro lado, para la columna vertebral de datos de validación no se ha implementado validar método. Creo que esa es la elección correcta para implementarlo y usarlo. Pero tenga en cuenta que validar se invoca antes de establecer y guardar, y si validate devuelve un error, establecer y guardar no continuará y los atributos del modelo no se modificarán. Las validaciones fallidas activan un evento de "error".

Otra forma, cuando llamamos al conjunto silencioso (con {silencioso: verdadero} param), debemos llamar al método isValid manualmente para validar los datos.

  1. Demasiadas cosas se rinden demasiado ...

Tienes que separar tus Vistas bajo su lógica. Las buenas prácticas para la recolección son vistas separadas para cada modelo. En este caso, podría representar cada elemento de forma independiente. Y aún más: al inicializar la vista de contenedor para la recopilación, puede vincular cualquier evento de cada modelo de la colección a la vista adecuada, y se procesarán automáticamente.

Genial, pero el H2 al inicio del formulario tiene el mismo nombre que en el   entrada - necesita actualizarlo. Ah, y necesitas actualizar el nombre en   la lista al lado.

podrías usar JQuery en método para implementar la devolución de llamada que envía el valor para mostrar. Ejemplo:

//Container view
init: function() {
    this.collection = new Backbone.Collection({
        url: 'http://mybestpage.com/collection'
    });
    this.collection.bind('change', this.render, this);
    this.collection.fetch();
},
render: function() {

    _.each(this.collection.models, function(model) {
         var newView = new myItemView({
              model: model,
              name: 'view' + model.id
         });
         this.$('#my-collection').append(newView.render().$el);
         view.on('viewEdit', this.displayValue);
    }, this);
},
...
displayValue: function(value) {
    //method 1
    this.displayView.setText(value); //we can create little inner view before, 
                                     //for text displaying. Сonvenient at times.
    this.displayView.render();
    //method 2
    $(this.el).find('#display').html(value);
}

//View from collection
myItemView = Backbone.View.extend({
events: {
    'click #edit': 'edit'
},
init: function(options) {
    this.name = options.name;
},
...
edit: function() {
    this.trigger('viewEdit', this.name, this);
}

OH, ¡y la lista se ordena por nombres!

Puedes usar ordenar método para colecciones troncales. Pero (!) Calling sort desencadena el evento "reset" de la colección. Pase {silent: true} para evitar esto. Cómo

Aquí hay otro ejemplo: desea crear un nuevo elemento en   colección...

Cuando presionamos un botón "Nuevo" necesitamos crear un nuevo modelo, pero solo cuando el método .save () desencadenará el éxito, debemos llevar este modelo a la colección. En otro caso, deberíamos mostrar un mensaje de error. Por supuesto, no tenemos motivos para agregar un nuevo modelo a nuestra colección hasta que haya sido validado y guardado en el servidor.

  1. Son subvistas hasta el fondo ... las subvistas se vuelven zombis o no responden.

cuando usted (o cualquier modelo) llama al método de renderizado, todos los elementos dentro de él se recrearán. Entonces, en caso de que tenga subvistas, debe llamar subView.delegateEvents(subView.events); para todas las subvistas; Probablemente este método es un pequeño truco, pero funciona.

  1. Parpadeo..

Usar miniaturas para imágenes grandes y medianas minimizará el parpadeo en muchos casos. De otra manera, podría separar la representación de la vista de imágenes y otro contenido.

Ejemplo:

var smartView = Backbone.View.extend({
  initialize: function(){
    this.model.on( "imageUpdate", this.imageUpdate, this );
    this.model.on( "contentUpdate", this.contentUpdate, this );
  },

  render: function(){
    this.$el.html(this.template(this.model.toJSON()));
  },

  imageUpdate: function(){
    this.$el.find('#image').attr('src', this.model.get('imageUrl'));
  },
  contentUpdate: function(){
    this.$el.find('#content').html(this.model.get('content'));
  }
})

Espero que esto ayude a cualquiera. Perdón por los errores de gramática, si hay alguno :)


8
2017-08-21 19:19



Cargando...

Soy un gran fanático de la carga ansiosa. Todas las llamadas a mi servidor son respuestas JSON, por lo que no es un gran problema hacerlas más a menudo que no. Por lo general, actualizo una colección cada vez que la necesita una vista.

Mi forma favorita de carga ansiosa es mediante el uso de Backbone-relacional. Si organizo mi aplicación de forma jerárquica Considera esto:

Organization model
|--> Event model
|--> News model
   |--> Comment model

Entonces, cuando un usuario está viendo un organization Puedo desear cargar esa organización events y news. Y cuando un usuario está viendo una news artículo, estoy ansioso de cargar ese artículo comments.

Backbone-relacional proporciona una excelente interfaz para consultar registros relacionados desde el servidor.

Demasiadas cosas se rinden demasiado ...

Backbone-relacional ayuda aquí también! Backbone-relacional proporciona una tienda de discos global que resulta ser muy útil. De esta manera, puede pasar identificaciones y recuperar el mismo modelo en otro lugar. Si lo actualiza en un lugar, está disponible en otro.

a_model_instance = Model.findOrCreate({id: 1})

Otra herramienta aquí es Backbone.ModelBinder. Backbone.ModelBinder le permite crear sus plantillas y olvidarse de adjuntarlas para ver los cambios. Entonces, en tu ejemplo de recopilar información y mostrarla en el encabezado, solo cuenta Backbone.ModelBinder para ver AMBOS de esos elementos, y en la entrada change, su modelo será actualizado y en modelo change Tu vista se actualizará, por lo que ahora se actualizará el encabezado.

Son subvistas todo el camino ... las subvistas se vuelven zombis o no responden ...

de verdad me gusta Backbone.Marionette. Se ocupa de la limpieza por usted y agrega un onShow devolución de llamada que puede ser útil cuando se eliminan temporalmente vistas del DOM.

Esto también ayuda a facilitar la conexión de complementos jQuery. El método onShow se invoca después de que la vista se represente y se agregue al DOM para que el código del complemento jQuery pueda funcionar correctamente.

También proporciona algunas plantillas de visualización geniales como CollectionView que hace un gran trabajo al administrar una colección y sus subvistas.

Parpadeo

Lamentablemente, no tengo mucha experiencia con esto, pero podrías probar precargar las imágenes también. Prestarlos en una vista oculta y luego traerlos hacia adelante.


2
2017-08-24 15:51