ExtJS

Cet article est une traduction de l'article de Don Griffin publié le 22 mai 2014.

Introduction

Ext JS 5 apporte plusieurs améliorations enthousiasmantes à utiliser pour architecturer votre application. Nous avons ajouté la prise en charge des ViewModels et des MVVM ainsi que celle des ViewControllers pour améliorer les applications MVC. Et le meilleur, c'est que ces apports ne sont pas mutuellement exclusifs si bien que vous pouvez introduire ces fonctionnalités de manière incrémentale voire les ignorer.

Récapitulatif sur les Controllers

Avec Ext JS 4, un contrôleur est une classe qui hérite de Ext.app.Controller. Ces contrôleurs utilisent des sélecteurs (selector) semblables aux sélecteurs CSS (appelés “Component Queries”) pour localiser les composants (component) et réagir à leurs événements. Ils font également usage de “refs” pour sélectionner et récupérer des instances de component.

Ces contrôleurs sont créés au lancement de l'application et restent présents pendant toute la durée de vie de celle-ci. Pendant cette durée de vie, les vues (view) intéressant un contrôleur vont apparaître et disparaître. On peut même avoir plusieurs instances d'une même vue gérées par un même contrôleur.

Défis

Pour les applications d'envergure, ces techniques peuvent présenter certains défis.

Dans de tels environnements, les vues et les contrôleurs peuvent être écrits par plusieurs équipes de développement puis intégrés dans l'application finale. S'assurer que les contrôleurs ne réagissent que pour les vues qu'ils sont supposés gérer peut être difficile. De plus, il est courant que les développeurs veuillent limiter le nombre de contrôleurs créés au lancement de l'application. Bien que créer des contrôleurs au moment où on en a besoin soit possible au prix d'un peu d'effort, ils n'est pas possible de les détruire si bien qu'ils subsistent même quand ils ne servent plus à rien.

ViewControllers

Bien que Ext JS 5 assure la compatibilité ascendante avec les contrôleurs actuels (i.e ceux de Ext JS 4), il offre un nouveau type de contrôleur conçu pour répondre à ces défis : Ext.app.ViewController. Un ViewController :

  • simplifie la connexion avec les vues en utilisant les entrées de configlisteners” et “reference”.
  • améliore le cycle de vie des vues pour gérer automatiquement leurs ViewController associés.
  • réduit la complexité en s'appuyant sur une relation un-un avec la vue prise en charge.
  • apporte l'encapsulation nécessaire pour fiabiliser l'imbrication des vues.
  • conserve la capacité de sélectionner des composants et de se mettre à l'écoute de leurs événements à tous les niveaux de profondeur de la vue associée.

Listeners

La config listeners n'est pas nouvelle mais elle a gagné plusieurs nouvelles capacités dans Ext JS 5. Une description plus complète des nouvelles fonctionnalités des listeners est disponible dans l'article -- “Declarative Listeners in Ext JS 5” (traduction à venir). Pour ce qui concerne les ViewControllers, nous pouvons nous limiter à examiner deux exemples. Le premier est une utilisation basique d'une config listeners concernant un sous-élément dans une vue (child item) :

Ext.define('MyApp.view.foo.Foo', {
    extend: 'Ext.panel.Panel',
    xtype: 'foo',
    controller: 'foo',

    items: [{
        xtype: 'textfield',
        fieldLabel: 'Bar',
        listeners: {
            change: 'onBarChange'  // scope non indiqué ici
        }
    }]
});
Ext.define('MyApp.view.foo.FooController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.foo',

    onBarChange: function (barTextField) {
        // appelé par l'événement 'change'
    }
});

L'utilisation précédente de listeners montre un gestionnaire d'événement nommé (« onBarChange ») qui n'indique pas la portée (scope). En interne, le système d'événements résout le scope par défaut pour le champ texte Bar via son ViewController d'appartenance.

Historiquement, la config listeners ne pouvait être utilisée que par le créateur d'un composant, donc comment une vue peut-elle se mettre à l'écoute de ses propres événements ou, éventuellement, à celle des événements émis par sa classe de base ? La solution passe par la déclaration explicite du scope :

Ext.define('MyApp.view.foo.Foo', {
    extend: 'Ext.panel.Panel',
    xtype: 'foo',
    controller: 'foo',

    listeners: {
        collapse: 'onCollapse',
        scope: 'controller'
    },

    items: [{
        ...
    }]
});

L'exemple précédent expose deux nouvelles fonctionnalités dans Ext JS 5: les named scopes et les listeners déclaratifs. Nous allons nous intéresser ici aux named scopes. Il existe deux valeurs possibles pour les named scopes : “this” et “controller”. Quand on écrit des applications MVC, on a presque toujours besoin d'utiliser “controller” qui a pour résultat évident d'observer le ViewController de la vue en question (et pas le ViewController de la vue qui a créé l'instance).

Puisqu'une vue est un type de Ext.Component, nous avons affecté à cette vue un “xtype” qui autorise d'autres vues à créer une instance de notre vue de la même façon qu'elle-même crée son champ texte. Pour voir comment tout ceci fonctionne ensemble, considérons une vue qui utilise celle-ci. Par exemple :

Ext.define('MyApp.view.bar.Bar', {
    extend: 'Ext.panel.Panel',
    xtype: 'bar',
    controller: 'bar',

    items: [{
        xtype: 'foo',
        listeners: {
            collapse: 'onCollapse'
        }
    }]
});

Dans ce cas, la vue Bar crée une instance de la vue Foo comme étant l'un de ses éléments. De plus, elle se met à l'écoute de l'événement collapse tout comme la vue Foo. Dans les versions précédentes d'Ext JS et de Sencha Touch, ces déclarations entraient en conflit. Avec Ext JS 5, cependant, ceci est résolu de la façon attendue. Les listeners déclarés par la vue Foo vont se déclencher dans le ViewController de Foo tandis que les listeners déclarés dans la vue Bar vont se déclencher dans le ViewController de Bar.

Références

L'une des difficultés les plus communes lorsqu'on écrit la logique d'un contrôleur est de se mettre en relation avec tous les composants nécessaires pour réaliser une action donnée. Prenons quelque chose de simple :

Ext.define('MyApp.view.foo.Foo', {
    extend: 'Ext.panel.Panel',
    xtype: 'foo',
    controller: 'foo',

    tbar: [{
        xtype: 'button',
        text: 'Add',
        handler: 'onAdd'
    }],

    items: [{
        xtype: 'grid',
        ...
    }]
});
Ext.define('MyApp.view.foo.FooController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.foo',

    onAdd: function () {
        // ... trouver le grid et lui ajouter un enregistrement ...
    }
});

Mais comment obtenir le composant grid ? Avec Ext JS 4, vous pouviez utiliser la configrefs” ou un autre moyen pour rechercher le composant. Toutes les techniques nécessitent que vous donniez à la grid une propriété reconnaissable afin qu'elle puisse être identifiée de manière unique. Les précédentes techniques utilisaient la configid” (et Ext.getCmp) ou la configitemId” (en utilisant “refs” ou une méthode de requête de composant). L'avantage de “id” est la rapidité avec laquelle on peut récupérer le composant correspondant, mais ces identifiants doivent être uniques à la fois dans toute l'application et le DOM, ce qui n'est pas toujours souhaitable. Utiliser “itemId” offre davantage de flexibilité mais rend nécessaire de réaliser une recherche pour trouver le composant souhaité.

Avec la nouvelle config reference dans Ext JS 5, nous ajoutons simplement la « reference » à la grid et nous utilisons “lookupReference” pour la récupérer :

Ext.define('MyApp.view.foo.Foo', {
    extend: 'Ext.panel.Panel',
    xtype: 'foo',
    controller: 'foo',

    tbar: [{
        xtype: 'button',
        text: 'Add',
        handler: 'onAdd'
    }],

    items: [{
        xtype: 'grid',
        reference: 'fooGrid'
        ...
    }]
});
Ext.define('MyApp.view.foo.FooController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.foo',

    onAdd: function () {
        var grid = this.lookupReference('fooGrid');
    }
});

Cela revient à affecter un itemIdfooGrid” et à exécuter : “this.down(‘#fooGrid’)”. Cependant, la différence sous le capot est tout à fait significative. D'abord, la config reference indique au composant de s'enregistrer lui-même auprès de sa vue (identifiée par la présence d'un ViewController dans ce cas précis). Ensuite, la méthode lookupReference se contente de consulter le cache pour voir si la référence doit être rafraîchie (par exemple du fait de l'ajout ou de la suppression éventuelle d'un container). Si tout va bien, la méthode renvoie simplement la référence stockée dans le cache. Ce qui donne, en pseudo-code ;

lookupReference: (reference) {
    var cache = this.references;
    if (!cache) {
        Ext.fixReferences(); // corrige toutes les références
        cache = this.references; // maintenant le cache est à jour
    }
    return cache[reference];
}

En d'autres mots, il n'y a pas de recherche et les liens endommagés par l'ajout ou la suppression d'éléments dans les containers sont corrigés en une fois, quand c'est nécessaire. Comme nous allons le voir après, cette approche possède d'autres avantages que la seule performance.

Encapsulation

L'utilisation de sélecteurs dans l'implémentation MVC d'Ext JS 4 était très flexible mais présentait dans le même temps certains risques. Le fait que ces sélecteurs « voyaient » tout à tous les niveaux de la hiérarchie des composants était à la fois très puissant mais propice aux erreurs. Par exemple, un contrôleur pouvait être opérationnel à 100 % quand il s'exécutait de manière isolée mais pouvait planter dès que d'autres vues étaient ajoutées parce que ses sélecteurs avaient des correspondances non désirées avec la nouvelle vue.

Ces problèmes peuvent être gérés en suivant certaines techniques, mais en utilisant des listeners et des references avec un ViewController, ces problèmes disparaissent purement et simplement. C'est parce que les config listeners et reference ne se connectent qu'avec leur ViewController d'appartenance. Les vues sont libres de choisir toute valeur de référence qui sera unique pour elle car ces noms ne seront pas exposés au créateur de la vue.

De la même façon, les listeners sont résolus pour leur ViewController d'appartenance et ne peuvent pas être accidentellement être distribués à des gestionnaires d'événements d'autres contrôleurs ayant des sélecteurs itinérants. Bien que les listeners soient souvent préférables aux sélecteurs, les deux mécanismes fonctionnent bien ensembles pour les cas où une approche par sélecteurs serait préférable.

Pour compléter ce modèle, les vues ont besoin d'activer des événements qui seront traités par leurs propres ViewController de vue. Il existe une méthode pour vous aider à ça dans ViewController : fireViewEvent. Par exemple :

Ext.define('MyApp.view.foo.FooController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.foo',

    onAdd: function () {
        var record = new MyApp.model.Thing();
        var grid = this.lookupReference('fooGrid');
        grid.store.add(record);

        this.fireViewEvent('addrecord', this, record);
    }
});

Ceci active la forme standard du listener pour le créateur de la vue :

Ext.define('MyApp.view.bar.Bar', {
    extend: 'Ext.panel.Panel',
    xtype: 'bar',
    controller: 'bar',

    items: [{
        xtype: 'foo',
        listeners: {
            collapse: 'onCollapse',
            addrecord: 'onAddRecord'
        }
    }]
});

Listeners et domaines d'événements

Avec Ext JS 4.2, le répartiteur d'événements (event dispatcher) avait été généralisé avec l'introduction des domaines d'événements (event domains). Ces domaines d'événements interceptaient les événements qui étaient générés et les répartissaient aux contrôleurs en fonction de la correspondance des sélecteurs. Le « composant » event domain possédait des sélecteurs de recherche de composants complets alors que les autres domaines avaient des sélecteurs plus limités.

Avec Ext JS 5, chaque ViewController crée une instance d'un nouveau type de domaine d'événements appelé le “viewevent domain. Ce domaine d'événements permet aux ViewControllers d'utiliser les méthodes standard “listen” et “control” tout en limitant leur portée implicitement à leur vue. Il ajoute également un sélecteur spécial correspondant à la vue elle-même :

Ext.define('MyApp.view.foo.FooController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.foo',

    control: {
        '#': {  // correspond à la vue elle-même
            collapse: 'onCollapse'
        },
        button: {
            click: 'onAnyButtonClick'
        }
    }
});

La différence majeure entre les listeners et les sélecteurs est visible ci-dessus. Le sélecteur « button » va correspondre avec tous les boutons de cette vue et de toutes les vues filles, quelle que soit la profondeur de parenté, donc même si le bouton appartient à une vue arrière-petite fille. En d'autres mots, les gestionnaires à base de sélecteurs ne respectent pas les frontières d'encapsulation. Ce comportement est conforme avec le comportement du Ext.app.Controller précédent, ce qui peut s'avérer une technique pratique pour certaines situations particulières.

Enfin, ces domaines d'événements respectent l'imbrication et font efficacement remonter un événement dans la hiérarchie de la vue. C'est-à-dire que lorsqu'un événement est activé, il est d'abord transmis à tout listener standard. Ensuite, il est transmis à son ViewController d'appartenance, puis aux ViewController parents (s'il y en a) en remontant la hiérarchie. Éventuellement, l'événement est transmis au composant standard domaine d'événements pour être pris en charge par les contrôleurs dérivés de Ext.app.Controller.

Cycle de vie

Une technique courante avec les grandes applications consiste à créer dynamiquement des contrôleurs la première fois qu'on en a besoin. Ceci peut contribuer à réduire le temps de chargement de l'application et améliore également les performances d'exécution en n'activant pas la totalité des contrôleurs pouvant exister. Dans les versions précédentes, une des limitations de cette technique était que ces contrôleurs restaient actifs dans l'application. Il n'était pas possible de les détruire et de libérer leurs ressources. De plus, cela ne changeait pas le fait qu'un contrôleur pouvait avoir n'importe quel nombre de vues associées (y compris aucune).

Le ViewController, cependant, est créé très tôt dans le cycle de vie du composant et est lié à cette vue pour sa durée de vie complète. Quand une vue est détruite, le ViewController est détruit en même temps. Cela signifie que le ViewController n'est désormais plus obligé de gérer les situations dans lesquelles il n'y a pas de vue ou il y en a plusieurs.

Cette relation un-un implique que le suivi des références est simplifié et n'est plus susceptible de provoquer de fuite pour les composants détruits. Un ViewController peut implémenter n'importe lesquelles de ces méthodes pour exécuter des tâches aux moments clefs de son cycle de vie :

  • beforeInit — Cette méthode peut être surchargée de façon à agir sur la vue avant que sa méthode initComponent ne soit appelée. Cette méthode est appelée immédiatement après que le contrôleur a été créé ce qui se produit lorsque initConfig est appelée par le constructeur de Component.
  • init — Appelée juste après que initComponent a été appelé pour la vue. C'est le moment typique pour réaliser l'initialisation du contrôleur dès lors que la vue est initialisée.
  • initViewModel — Appelée quand le ViewModel de la vue est créé (s'il y en a un de défini).
  • destroy — Nettoyage de toutes les ressources (assurez-vous d'appeler callParent).

Conclusion

Nous pensons que les ViewControllers vont significativement rationaliser vos applications MVC. Ils fonctionnent également très bien avec les ViewModels si bien que vous pouvez combiner ces approches et leurs avantages respectifs. Nous sommes impatients que cette version soit livrée (ExtJS 5, ce qui est fait désormais) et de voir à l’œuvre dans vos applications les améliorations qu'elle apporte.

Écrit par Don Griffin

Don Griffin est un membre de l'équipe centrale de Ext JS. Il a été un utilisateur de Ext JS pendant 2 ans avant de rejoindre Sencha et possède plus de 20 ans d'expérience comme ingénieur de développement sur une large gamme de plates-formes. Son expérience s'étend de la conception des front-ends et backends d'applications web, aux applications client lourd, aux protocoles réseau et aux pilotes de périphériques. La passion de Don est de construire des produits de classe mondiale que les gens aiment utiliser.

Articles liés