rnb-js, objets natifs, espace de noms et modules

Deuxième (et dernier ?) rapport d'étape de la reconstruction de la librairie javascript rnb-js. Au menu : les composants en tant que modules et la surcharge du prototype des objets natifs.

J'avais déjà travaillé sur la définition des composants comme module précédemment mais la solution restait imparfaite :

rnb.define('rnb.foo', ['rnb.bar', 'rnb.baz', 'rnb.qux'], function () {
    rnb.foo = rnb.create({
        constructor: function () {
            rnb.baz.call(this);
        },
        createBar: function () {
            this.bar = new rnb.bar();
            this.quxMethod();
        }
    }, rnb.baz, rnb.qux);
});
Création d'objets dans un objet global (rnb).

On était toujours obligé de maintenir un objet global (rnb) et, pour organiser un peu le tout, de définir d'éventuels sous-objets servant d'espace de nom. L'idéal aurait été de pouvoir faire quelque chose comme suit :

rnb.define(['bar', 'baz', 'qux'], function (bar, baz, qux) {
    // create object
    var foo = rnb.create({
        constructor: function () {
            baz.call(this);
        },
        createBar: function () {
            this.bar = new bar();
            this.quxMethod();
        }
    }, baz, qux);
    
    // exports
    return foo;
});
création d'un objet qui est retourné comme résultat du define.

Je trouve la solution plus élégante et plus portable. Plus élégante car elle nous évite de créer des objets imbriqués les uns dans les autres pour simuler un équivalent des espaces de noms, connus dans d'autres langages comme le java par exemple mais qui n'existe pas (encore ?) en javascript (noter que beaucoup de libraires le font). Plus portable car l'objet retourné « existe par lui-même », sans présupposer l'existence d'un objet dans lequel il serait / devrait être défini.

L'option retenue est donc de garder l'objet global rnb pour stocker tout les utilitaires de programmation (accès au DOM, manipulation de chaînes de caractères, de fonctions, de tableaux, etc), mais de définir dorénavant les composants graphiques comme des modules à importer dans un contexte d'exécution.

Mon chargeur de script (rnb.require et rnb.ready) ne pouvait pas à l'origine gérer ce type de déclaration. Avant de me lancer dans une restructuration profonde d'un code qui sert de fondation à la librairie, j'ai essayé de me brancher sur un code qui fonctionne déjà de cette manière, à savoir require.js(1). A ma grande surprise, son intégration à été dés plus simple et rapide : copie du code dans le fichier principal, redirection et changement de la signature des définitions et le tour est joué.

Je me suis donc attelé à la tâche : modifier mon propre chargeur et, finalement, je me retrouve avec une solution beaucoup plus simple que la précédente, qui ne fait plus du tout appel à des timers (il faudra tout de même que j'en place un si les fichiers se chargent mal). Telle que fonctionne la librairie maintenant, je peux au choix utiliser les fonctionnalités internes ou me brancher sur require.js en redirigeant mes méthodes.

D'autres évolutions / corrections ont accompagné ces modifications ; l'une d'elle a consisté à diviser le fichier prinicpal en autant de fichiers que d'objets qui y étaient définis, soit 42(2). Faut dire que, commentaires compris, il faisait près de 10 000 lignes ! Difficile au bout d'un moment de s'y retrouver rapidement. Le processus de build se voit donc doté d'une étape supplémentaire où tous ces fichiers sont concaténés dans un ordre précis. J'avais peur que le debug s'en trouve allourdit, mais ce n'est finalement pas le cas.

Enfin, un dernier questionnement a concerné le code qui corrige les lacunes de certains navigateurs dans la manipulation des types javascript natifs. Des librairies comme Prototype ont décidé d'augmenter directement les objets natifs quand la fonctionnalité n'existe pas en surchargeant le prototype. J'ai hésité à faire de même… pour finalement garder le système du moment, même s'il est un peu lourd : des collections d'utilitaires dédiés aux chaînes de caractères (rnb.str), aux objets (rnb.obj), aux tableaux (rnb.arr) et aux fonctions (rnb.fn). Pourquoi ce status Quo ? Parce que je rechigne toujours à modifier le prototype des objets natifs et puis parce que ces collections possèdent aussi des méthodes qui n'ont pas d'équivalents nativement.

rnb.str.trim = function (s) {
    rnb.str.trim = String.prototype.trim ? 
        function (v) { 
            return v.trim(); 
        } :
        function (v) { 
            return v.replace(/^\s\s*/,'').replace(/\s\s*$/,''); 
        };
    return rnb.str.trim(s);
};
Exemple d'implémentation de la méthode trim.

BURKE, James. AMD. amdjs-api,

MICHAUX, Peter. JavaScript Namespacing. peter.michaux.ca,