Blog de Antonio Manuel Muñiz

Desarrollo, Ingeniería y Calidad del Software

Archivos por Etiqueta: extjs

Carga lo que necesitas y cuando lo necesitas con Ext.Loader

Cuando se usa ExtJS en aplicaciones relativamente grandes se tiende a tener una gran cantidad de ficheros javascript, la descarga de todos esos ficheros puede suponer un lastre muy importante durante la inicialización de la aplicación. Hasta ahora en klicap hemos usado la solución tradicional, que durante mucho tiempo ha sido el uso de compresión y ofuscado (minify), pero ExtJS 4 ofrece otra posibilidad, el uso de un cargador (Ext.Loader) que descarga los ficheros javascript necesarios en el momento necesario.

Para usar Ext.Loader es necesario seguir algunas normas y convenciones, que por otro lado vienen bien para que el proyecto esté ordenado. El primer paso es incluir sólamente el core de ExtJS, lo cual supone el primer beneficio, en lugar de 1,1MB (ext-all.js) sólo hay que descargar 175KB (ext.js):

<head>
    <script type="text/javascript" src="ext/ext-debug.js"></script>
</head>

 

Ext.Loader.setPath({
    'MyApp': 'ui/app'
});

Esta configuración indica al loader que las clases con namespace MyApp debe buscarlas en ui/app. Por ejemplo, la clase MyApp.view.User debe estar contenida en el fichero ui/app/view/User.js.

El uso de Ext.Loader implica la separación de clases en ficheros independientes, práctica muy común en otros lenguajes de programación, pero no obligatoria en Javascript.
El resultado (y el objetivo) de Ext.Loader es que el fichero User.js sea descargado y evaluado sólo cuando sea necesario, es decir, cuando se llame a Ext.create(‘MyApp.view.User’).

En este sentido hay dos opciones, el uso de requires o no en la definición de las clases Javascript:

Ext.define('MyApp.view.User', {
    extend: 'Ext.panel.Panel',
    requires: [
        'MyApp.view.UsersGrid'
    ],
    initComponent: function() {
        var grid = Ext.create('MyApp.view.UsersGrid');
        ...
        this.callParent(arguments);
    }
});

En este caso, la carga de MyApp.view.UsersGrid (fichero ui/app/view/UsersGrid.js) se realizará en el momento de la definición de la clase MyApp.view.User, por tanto la descarga de UsersGrid.js se producirá siempre que se cargue User.js. Si se elimina el uso de requires, la carga de UsersGrid.js se realizará en el momento en que se llame a initComponent en User, es decir, cuando se llame a Ext.create(‘MyApp.view.User’), si esta llamada no se produce entonces nunca se descargará UsersGrid.js.

ComboBox en Sencha ExtJS 4. Carga asíncrona

La clase  Ext.form.field.ComboBox en ExtJS es realmente potente, permitiendo desde paginación hasta la posibilidad de definir mediante Ext.XTemplate el contenido de cada línea del combo. Pero su comportamiento asíncrono puede convertirse en un problema cuando la conexión con el lado del servidor es lenta. En klicap usamos ExtJS desde hace casi dos años, y nos hemos encontrado con este comportamiento recurrentemente, ya era hora de solucionarlo :-)

Para situarnos. Todo componente de ExtJS que actúa como contenedor de datos tiene asociado un Store (Ext.data.Store) el cual define una serie de parámetros que afectan a la relación entre el contenedor y los datos:

  • Estructura de datos. Haciendo uso de Ext.data.Model (se establece una relación entre la fuente de datos y los objetos javascript que los mapean).
  • Características de la fuente. La fuente de datos puede encontrarse en memoria (un array que contiene los datos) o puede ser remota (lo cual implica el uso de AJAX). En este último caso se ha de definir el formato de los datos recibidos (JSON o XML).
  • Comportamiento. Se indica si el Store se cargará automáticamente al crear el objeto, o requiere una llamada a load().

Un ejemplo de definición de un combo (de timezone) con fuente de datos remota, JSON y carga automática (el combo se encuentra dentro de un formulario y este asu vez dentro de una ventana):

Ext.define('User', {
    extend: 'Ext.window.Window',
    initComponent: function() {
        this.items = [{
            xtype: 'form',
            border: false,
            fieldDefaults: {
                labelWidth: 100
            },
            items: [{
                xtype: 'fieldset',
                anchor: '100%',
                title: 'User data',
                items: [{
                    layout: 'anchor',
                    items: [{
                        xtype: 'combo',
                        fieldLabel: 'Timezone',
                        name: 'timezone',
                        width: 300,
                        queryMode: 'local',
                        triggerAction: 'all',
                        valueField: 'id',
                        displayField: 'timezone'
                        store: Ext.create('Ext.data.Store', {
                            model: 'Timezone',
                            proxy: {
                                type: 'ajax',
                                url: 'api/timezone',
                                reader: {
                                    type: 'json',
                                    root: 'timezones'
                                }
                            },
                            autoLoad: true
                        })
                    }]
                }]
            }]
        }];
        this.callParent(arguments);
    },
    modal: true,
    width: 430,
    height: 350,
    layout: 'fit',
    title: 'User'
});

Durante el proceso de carga de datos en el formulario ExtJS hace el mapping de los valores que vienen en el JSON en los campos del formulario (usando la propiedad name del campo). Por tanto la carga del formulario sería tan simple como:

var userForm = Ext.create('User');
userForm.getComponent(0).getForm().load({
    url: 'api/user/12',
    method: 'GET',
    success: function (form, action) {
        userForm.show();
    }
});

En este fragmento de código se realizarán dos peticiones HTTP GET al servidor: una para la carga de combo (GET ‘/api/timezone’ debido a la propiedad autoload: true del combo) y otra para la carga de los datos del usuario (GET ‘/api/user/12’).

Como he comentado antes ExtJS realiza llamadas internas a Ext.form.field.Base.setValue() para setear el valor de cada campo del formulario (como parte de la llamada a load). Pero ¿qué sucede cuando se intenta setear el valor del combo antes de que el listado de valores haya sido cargado?, es decir, la llamada a ‘/api/user/12’ termina antes que ‘/api/timezone’. El resultado es que el valor no se fija y el combo se queda vacío, y eso es un #FAIL.

Para evitarlo es necesario esperar a que la carga del listado de valores de combo haya terminado y sólo despues hacer el setValue(). La siguiente extensión se encarga precisamente de esto de una forma bastante simple:

Ext.define('AsyncSafeComboBox', {
    extend: 'Ext.form.field.ComboBox',
    alias: 'widget.safecombo',
    setValue: function(value, doSelect) {
        if(this.store.loading){
            this.store.on('load', Ext.bind(this.setValue, this, arguments));
            return;
        }
        this.callParent(arguments);
    }
});

La clave está en la propiedad ‘loading’ de Ext.data.Store que indica si el store asociado al combo está aún cargando sus datos. Si la llamada a setValue se realiza durante la carga del store, entonces se retrasa hasta que el store indique que ya ha acabado (evento ‘load’).

Sólo habría que cambiar el xtype de nuestro combo a ‘safecombo’ y siempre se realizará la carga de forma correcta.

Migración de ExtJS 3 a ExtJS 4

En klicap estamos llevando a cabo la migración de uno de nuestros productos (Opina 2, aún en desarrollo) a ExtJS 4. He creido oportuno compartir aquí algunas notas sobre la migración.

Uso de Ext.define y Ext.create

En ExtJS 4 las clases se definen con Ext.define y se crean instancias con Ext.create, lo cual sustituye a Ext.extend y new en ExtJS 3

ExtJS 3:

Ext.namespace('Opina.view');
Opina.view.TextQuestion = Ext.extend(Ext.grid.GridPanel, {
    initComponent: function() {
        ...
        Opina.view.TextQuestion.superclass.initComponent.call(this);
    }
});
Ext.reg('textquestion', Opina.view.TextQuestion);

ExtJS 4:

Ext.define('Opina.view.TextQuestion', {
    extend: 'Ext.grid.Panel',
    alias: 'widget.textquestion',
    initComponent : function() {
        ....
        this.callParent(arguments);
    }
});

Este fragmento de código está lleno de detalles:

  • Uso de Ext.define y el parámetro extend
  • Nueva nomenclatura, Ext.grid.GridPanel pasa a ser Ext.grid.Panel (regla extensible a todo el framework)
  • Nueva función callParent presente en todos los componentes
  • Definición automática de xtype usando alias

2. Uso de Ext.data.Model y Ext.data.Store
En ExtJS 4 se ha separado por completo el concepto de almacén de datos (Store) y definición de estructura de datos (Model). Lo que era un Store con una serie de campos (fields) ahora es un Store asociado a un modelo:

Ext.define('Opina.model.TextQuestion', {
    extend: 'Ext.data.Model',
    fields: [
       { name: 'elementSetId' },
       { name: 'elementType' },
       { name: 'freeTextLabel' },
       { name: 'helpMessage' },
       { name: 'id' },
       { name: 'position' },
       { name: 'questionType' },
       { name: 'surveyId' },
       { name: 'template' },
       { name: 'wording' },
       { name: 'multiplelines', mapping: 'data.multipleLines' },
       { name: 'validations' }
    ]
});
var store = Ext.create('Ext.data.Store', {
    model: 'Opina.model.TextQuestion',
    proxy: {
        type: 'ajax',
        reader: {
            type: 'json',
            root: 'question'
        }
    }
});

3. Mejora de formularios

El layout “form” ha desaparecido. En ExtJS 3 si querías un campo de texto con un label (muy común) la única opción era que el campo estuviera dentro de un panel con layout “form”. En ExtJS 4 cualquier layout puede acojer un campo con label, lo cual flexibiliza mucho la definición de formularios y evita el uso de paneles sin sentido que sólo contienen un campo.

4. Métodos up y down

ExtJS 4 proporciona dos métodos que facilitan la recuperación de elementos en un contenedor, sus hijos y ancestros. En ExtJS 3 se suele usar “find”, pero down es más versátil al permitir el uso de expresiones de búsqueda complejas, por ejemplo “#” para buscar por “itemId”.

var store = myWindow.down('#options-grid').getStore()

El funcionamiento de “up” es igual pero aplica a ancestros. Realmente útil.

5. Mejoras en Grids

Los grids han sufrido cambios importantes, sobre todo en lo relacionado a eventos. En ExtJS 4 los grids son un DataView más, por tanto cada fila es un item y se trata como tal. No existen los eventos del tipo “rowdblclick” sino “itemdblclick”.

6. Organización

Esto no está directamente relacionado con la migración, pero el nuevo enfoque MVC de ExtJS hace que de forma natural tu proyecto esté mejor organizado. Nosotros hemos organizado el proyecto así:

7. Firefox + Firebug son tus amigos

Durante el proceso de migración ha sido fundamental el uso del binomio Firefox + Firebug. Sin estas herramientas la migración es un infierno.
Sencha proporciona un adaptador especial para la migración, que relaiza una traslación de ExtJS 3 a ExtJS 4 de ciertas propiedades, nombre de clases, paquetes y métodos. De esta forma podemos ir realizando la migración gradualmente y solucionando las incompatibilidades que son logeadas en la consola de Firebug.

8. Otros detalles
Dejo en el tintero muchos detalles de menor importancia, como cambios de nombre en métodos (que no son detectados por el pack de migración) como “reload()” en Ext.grid.Panel, que deja de existir estando disponible sólo “load”, o el objeto resultado de un submit cuya respuesta viene en “action.result.data” y no en “action.reader.jsonData”.

9. Sigo echándolo en falta
Sigo echando el falta un mecanismo que controle la carga asíncrona de un ComboBox, que tantos problemas da cuando la conexión es lenta (se hace el setValue antes de que los valores del combo hayan sido cargados). Paro solventar esto en @klicap hemos desarrollado una pequeña extensión, pero eso lo contaré en otro post ;-)

Autorización del lado del cliente con ExtJS

Llevo algún tiempo usando ExtJS como API Javascript para desarrollar interfaces gráficas de aplicaciones web. Uno de los aspectos que más “ensucian” el diseño (a nivel de código fuente) es el control de la autorización, es decir, cómo cambia la UI en función de los permisos del usuario: qué puede/no puede ver y qué puede/no puede hacer. Evidentemente este sólo es un primer nivel de control que no está orientado a la seguridad, sino a la usabilidad, es el servidor el que realmente permite (o no) llevar a cabo una acción a un usuario autenticado.

En este post voy a contar como implementamos en klicap este tipo de autorización del lado del cliente.

Es lógico que la base de la aplicación sea la misma para todos los tipos de usuarios (con todo tipo de roles), y que lo único que cambie sean pequeños detalles, como las barras de botones (Ext.ToolBar, Ext.Window.bbar, etc) o los formularios (Ext.form.FormPanel), además de secciones completas con acceso restringido.

En klicap hemos desarrollado una pequeña utilidad a la que llamamos “micro framework de autorización”. Para ilustrar el ejemplo supongamos que tenemos una ventana (Ext.Window) con botones de Guardar y Cancelar que contiene un Grid (Ext.grid.GridPanel) con tres botones: añadir, eliminar y refrescar. La lógica de autorización del ejemplo sería:

  • role1: puede ver y usar todos los botones
  • role2: sólo puede ver y usar el botón refrescar

El código fuente sería algo así (se omiten partes el código para simplificar):

// Valores de los roles del usuario actual, recuperado
// de alguna forma desde el servidor
var role1 = getRole1();
var role2 = getRole2();

MyGrid = Ext.extend(Ext.grid.GridPanel, {
 initComponent: function() {
  if(role1) {
   this.tbar = [
    {text: 'Añadir'}, '-',
    {text: 'Eliminar'}, '->',
    {text: 'Refrescar'}
   ];
  } else if(role2) {
   this.tbar = [
    {text: 'Refrescar'}
   ];
  }
  MyGrid.superclass.initComponent.call(this);
 }
});
MyWindow = Ext.extends(Ext.Window, {
 initComponent: function() {
  if(role1) {
   this.bbar= [
    {text: 'Guardar'},
    {text: 'Cancelar'}
   ];
  }
  MyWindow.superclass.initComponent.call(this);
 }
});

var window = new MyWindow();
window.add(new MyGrid());
window.show();

El número de sentencias “if else if” crecerá (como mínimo) de forma lineal con el número de roles en un caso de autorización común. Por tanto el objetivo es eliminar todo ese código y centralizarlo de forma genérica. Usando el “micro framework” el código sería el siguiente:

var authz = new klicap.commons.extjs.auth.Authorization(
 new klicap.commons.extjs.auth.Roles({
  role1: getRole1(),
  role2: getRole2()
 })
);
MyGrid = Ext.extend(Ext.grid.GridPanel, {
 initComponent: function() {
  var fullTbar = [
   {text: 'Añadir', require: {role1: true} }, '-',
   {text: 'Eliminar', require: {role1: true} }, '->',
   {text: 'Refrescar', require: {role2: true} }
  ];
  this.tbar = authz.getAuthorizedElements(fullTbar);
  MyGrid.superclass.initComponent.call(this);
 }
});
MyWindow = Ext.extends(Ext.Window, {
 initComponent: function() {
  fullBbar= [
   {text: 'Guardar', require: {role1: true} },
   {text: 'Cancelar', require: {role1: true} }
  ];
  this.bbar = authz.getAuthorizedElements(fullBbar);
  MyWindow.superclass.initComponent.call(this);
 }
});

var window = new MyWindow();
window.add(new MyGrid());
window.show();

El efecto será el mismo pero en el código fuente de la UI no hay una sola sentencia condicional para el control de la autorización. Simplemente se debe especificar qué roles requiere cada elemento para ser mostrado (mediante el atributo ‘require’).

Actualmente existe otra funcionalidad del micro framework para autorización en formularios (se habilita o deshabilita la edición de ciertos campos de un formulario en función de los ‘require’ de éste). Este pequeña utilidad irá evolucionando acorde a las necesidades de klicap en este sentido.

La utilidad está contenida en un único fichero javascript. Si quieres probarlo puedes descargarlo, desde klicap lo hemos publicado con licencia GNU GPL v3.