Salut les codeurs,
Je voulais partager avec vous une technique assez peu connue pour injecter dynamiquement des scripts et gérer des dépendances. Je ne me souviens pas qu'on en ait parlé sur dvpz.com, ou si c'est le cas, je suis passé à côté. Cette technique est utilisée notamment par AngularJS, vous savez, le framework JS le plus hype de toute l'histoire de 2014 :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 function MonController($scope, $http, monmoduleA, monmoduleB) { // ... }
Quand j'ai commencé à utiliser AngularJS, ce mode de déclaration des dépendances a été un grand mystère pour moi, et reste toujours assez contre-intuitif même avec le temps. Les fonctions contrôleurs prennent un nombre quelconque d'arguments, dans n'importe quel ordre, et seul le nom des variables locales déterminent quelles dépendances sont chargées. Le nom d'une variable locale ? Mes réflexes de vieux codeur JS me disent que ça n'a aucune importance, car c'est seulement utile dans le corps de la fonction.
Eh bien non, que nenni, ce n'est plus vrai quand on bosse avec la propriété de réflexion des fonctions en JavaScript. Réflexion, ça ne veut pas dire qu'elles se creusent les neurones pour faire le boulot à notre place. C'est ma traduction de "reflective" en anglais, que j'aurais aussi pu traduire par "réfléchissement" ou encore "introspection". Ça signifie simplement qu'une fonction, lors de son exécution, a accès à son propre code la définissant :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 (function HAL(){ console.log("Bonjour, je suis "+arguments.callee.toString()); })();
C'est une facette assez méconnue du langage et pourtant fichtrement intéressante. Car si une fonction a accès a son propre code, elle peut s'auto-analyser, voire pourquoi pas s'auto-réécrireet détruire le mondeet injecter du code selon le nom des variables locales qui y sont déclarées.
Finalement, le mécanisme tel qu'implémenté par Angular se révèle assez simple. Voilà comment récupérer dans une array la liste des noms de variables locales d'une fonction en argument :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7 function getVarLocales(scope){ var deps = scope.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1].replace(/ /g, '').split(','); console.log(deps); } getVarLocales(function(toto,tata,titi){ }); // affiche ["toto","tata","titi"]
A partir de là, c'est un jeu d'enfant de brancher ça à votre script loader favori (vous n'en avez pas ? je balance des noms en vrac: LABJS-browserify-require-headJS-Lazyload-jQuery.getScript-webpack-taFonctionPersoToiMemeTuSais)
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14 function include(scope){ var deps = scope.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1].replace(/ /g, '').split(','); console.log(deps); tonScriptLoader(deps, function(resolvedDeps){ scope.apply(this, resolvedDeps); }); } include(function(myLib, myModule){ console.log("myLib et myModule chargés et prêts", myLib, myModule); });
Un inconvénient est que les noms de fichiers doivent être des noms de variable valides, donc pas d'espaces, de points ou de chiffres en premier caractère. Chose que l'on peut arranger facilement en faisant évoluer la syntaxe comme ceci par exemple:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 function include(scope, paths){ var deps = scope.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1].replace(/ /g, '').split(','); console.log(deps); tonScriptLoader(deps, paths, function(resolvedDeps){ scope.apply(this, resolvedDeps); }); } include(function(myLib, myModule){ console.log("myLib et myModule chargés et prêts", myLib, myModule); }, { myLib: "../lib/maLib.min.js", myModule : "42 is the answer.js" });
Voilà, pas besoin de bosser chez Google pour obtenir un système d'injection de dépendances supra-simple à utiliser. Ah, mais il y a quand même deux gros bémols :
1) les minifieurs, ces outils qui compressent le code en renommant à la sauvage tout ce qui traîne, ne font qu'une bouchée de vos beaux noms de variables locales. Et vous allez vous retrouver à charger des modules a.js b.js c.js... C'est pour ça qu'Angular a dû amener une autre syntaxe à base de String littérales, mais alors on revient à la case départ et la DI réflechie n'a plus beaucoup d'intérêt...
2) ça fait toujours autant de fichiers à charger côté client. Et en attendant HTTP2, il est toujours conseillé de concaténer ses scripts en gros paquets pour économiser sur la latence réseau. Autrement dit, les préprocesseurs tels que browserify ou requireJS optimizer restent à ce jour le top en matière de perf.
Bref, pas une révolution mais toujours sympa à connaître et utile pour de l'injection de dépendances vite fait bien fait sur des petits projets![]()
Partager