"Whilst native solutions to these problems will be arriving in ES Harmony, the good news is that writing modular JavaScript has never been easier and you can start doing it today." - Addy Osmani, créateur de Yeoman
Les bienfaits d’une application modulaire ne sont plus à présenter. Malheureusement, en attendant la version ECMAScript 6, baptisée Harmony, JavaScript n’offre pas au développeur un moyen simple d’écrire de tels module. Mais c’est sans compter sur AMD.
AMD (Asynchronous Module Definition) définit un format pour rendre notre JavaScript modulaire dans le navigateur dès à présent. Plusieurs script loaders implémentent ce standard. Le plus célèbre est RequireJS.
Grâce à RequireJS, nous allons définir des modules, parfaitement isolés, qui ne polluent pas l’espace de nommage global et qui définissent explicitement leur(s) dépendance(s). RequireJS est une extension du pattern Module très répandu en JavaScript.
RequireJS est publié sous licence new BSD et MIT. Le code présenté dans cet article a été simplifié pour des raisons évidentes et n’a pas pour vocation à être utilisé en dehors de ce contexte d’apprentissage. Cet article est basé sur la dernière version de RequireJS (2.1.16) au moment de cet article.
Getting Started with Modules
AMD définit deux méthodes clés :
define
, qui pour comme son nom l’indique, définit un nouveau module- et
require
, plus adaptée pour gérer le chargement des dépendances.
Commençons par la méthode define
:
Le premier paramètre permet de nommer le module. Bien souvent, les modules seront anonymes (bonne pratique qui facilite leur réorganisation). Vient ensuite les dépendances à charger, qui seront passées en argument du dernier paramètre : la fonction instanciant véritablement le module.
Voici un exemple :
require
est plus souvent utilisée pour définir le point d’entrée (le premier fichier chargé par
RequireJS) ou au sein même de la définition d’un module comme suit :
Nous n’irons pas plus loin dans la présentation de RequireJS/AMD. Pour découvrir plus en détail la librairie, la documentation de l’API est fortement conseillée.
Un premier exemple
Au démarrage, RequireJS inspecte l’attribut spécial data-main
pour savoir le premier module de
l’application à charger. Il s’agit du fichier main.js
présent dans le dossier scripts
:
Ce fichier contient en réalité un simple appel à la fonction require
pour demander le chargement
d’un autre module (helper/util
) qui définit une fonction toFunnyCase
. Le reste du code
sert davantage de démonstration, parcourant chaque titre de la page HTML pour modifier le texte à l’aide de cette
mystérieuse fonction.
Il ne nous reste plus qu’à regarder ce dernier fichier scripts/helper/util.js
:
Il s’agit d’un module anonyme, sans aucune dépendance, définissant une unique fonction qui produit le résultat suivant, une fois la page affichée :
L’objectif est maintenant de supprimer la dépendance sur require.js, et la remplacer par une nouvelle implémentation que l’on va écrire pas à pas. Il n’est pas question d’avoir une implémentation aussi complète. L’idée est d’avoir une implémentation suffisante pour que notre exemple continue de fonctionner.
RequireJS, Under the hood
Avant de se lancer tête baissée dans le code, intéressons-nous aux requêtes HTTP émises par RequireJS sur notre exemple.
-
Le code de
require.js
inspecte l’attributdata-main
pour connaître le premier fichier à récupérer, en l’occurence,main.js
. Un premier appel Ajax est donc émis. -
main.js
appelle la fonctionrequire
. Une dépendance est nécessaire. La méthoderequire
déclenche alors un deuxième appel Ajax pour récupérerutil.js
. -
util.js
ne nécessite lui aucune dépendance. Le callback d’instanciation du module est alors exécuté. RequireJS mémorise le résultat pour l’étape suivante. -
Nous revenons alors au fichier
main.js
.Toutes les dépendances ont été chargées, le callback d’instanciation s’exécute enfin.
C’est parti !
Commençons par modifier notre fichier de démonstration :
Pas de grande surprise. On a remplacé la librairie RequireJS par un nouveau fichier require.lite.js
que l’on va compléter tout au long de cet article.
Notons également la présence de jQuery, pas indispensable mais qui va nous éviter de recoder certaines méthodes utilitaires courantes.
Voici le squelette de require.lite.js
:
Commençons par inspecter l’attribut data-main
:
Après avoir extrait la valeur de l’attribut, on sépare le dossier racine (baseUrl
) du nom du fichier
à charger (mainScript
). Ce dossier racine servira par la suite de préfixe à chaque récupération d’un
nouveau script. Le code termine par appeler la méthode require
. Il est donc temps de rentrer
véritablement au coeur de RequireJS et de ses modules.
Le Module
RequireJS repose fortement sur l’objet Module
dont voici son constructeur :
- 7
-
depExports
va contenir les arguments qui seront passés au callback d’instanciation du module. A chaque chargement d’une dépendance, on mémorise le résultat dans ce tableau. - 8
- Le callback d’instanciation ne doit s’exécuter qu’une fois toutes les dépendances chargées. Grâce à ce compteur, on mémorise le nombre de dépendances restantes. Nous verrons bientôt comment ce compteur est utilisé.
- 12
-
Il est important d’assigner un id même pour les fichiers contenant un simple
require
. Si le fichier est référencé plusieurs fois, seul le premier chargement sera considéré. - 16
- Implémentation du pattern Observer. Les autres modules peuvent surveiller notre avancement. En pratique, on l’utilisera uniquement pour savoir quand un module a été défini. (RequireJS génère bien plus d’événements en interne qui ne sont pas utiles pour notre cas d’exemple). Voici les deux méthodes utilitaires nécessaires pour cette gestion événementielle.
Notre Module
est créé mais il ne se passe toujours rien. Ce n’est que lors de l’appel à la méthode
init
que la magie commence à opérer, et plus particulièrement lors de l’appel à la méthode enable
.
Déroulons pas à pas le fonctionnement de cette dernière méthode enable
.
- 29
-
Comme pour l’
init
, on mémorise le fait que l’activation du module a déjà été appelée. Cela nous évitera d’initialiser plusieurs fois un même module. - 33
-
On arrive alors à la gestion transitive des dépendances. Pour que notre module puisse s’activer, il faut au
préable que ses dépendances soit également activées. On itère donc sur chacune d’entre elles. Si la dépendance
est nouvelle (c’est le but de la variable
registry
), on instancie sonModule
et dans tous les cas, on tente son activation. (méthode récursive). - 40
-
La propriété
depCount
est incrémentée pour indiquer que nous sommes dans l’attente de ce module. - 42
- On en profite également pour s’enregistrer auprès de ce module pour décrémenter cette variable une fois le module défini.
- 52, 56
-
Dissimulés au sein de la méthode se trouvent deux appels à la méthode
check
: une à la fin de notre activation, et une à chaque définition d’une dépendance. Que fait donc cette méthodecheck
:
Cette méthode check
tente de finaliser le module (c’est-à-dire appeler le callback). On commence
donc par tester que le module est déjà initialisé, dans quel cas il est inutile d’aller plus loin, on demande
juste son chargement (= appel Ajax). Sinon, on va tenter notre chance à l’aide de la méthode define
.
define
vérifie que toutes les dépendances sont chargées (à l’aide de la propriété
depCount
qui fait sa dernière apparition). Si les conditions sont réunies, depExports
contient les arguments que l’on communique au callback d’instanciation. Terminé ! On publie un événement pour
annoncer la bonne nouvelle aux autres modules, qui rappelons-nous, écoute attentivement cet événement pour à leur
tour, tenter d’appeler la méthode check
pour finaliser eux aussi leur définition.
Comment charger un script dynamiquement en JavaScript ?
Plusieurs solutions sont possibles mais la plus répandue consiste à créer une nouvelle balise
script
et à l’ajouter dans le DOM (dans le head
par exemple). C’est exactement ce que
fait RequireJS :
require
L’heure est venue de retourner aux deux méthodes définies par AMD. Avec l’objet Module
, la
définition de la méthode require
devient triviale.
Il suffit de créer un nouveau module que l’on initialise immédiatement sans oublier de forcer son activation. Cela provoque le chargement des dépendances de manière transitive.
define
La méthode define
n’est pas aussi simple, mais rien d’insurmontable.
Prenons l’exemple de notre fichier main.js
.
Lors de l’exécution du script, nous venons de voir que la méthode require va déclencher le chargement des dépendances (méthode enable). Le script util.js s’exécute alors :
Nous arrivons dans la méthode define
, sans pour autant connaître le nom du module en question.
Comment pouvons nous donc finir son activation ? A quel nom associé le résultat de l’exécution de la factory ?
La solution retenue par RequireJS est de mémoriser les paramètres d’appel dans la méthode define
(dans une file, étant donné que plusieurs modules peuvent être chargés en même temps) :
La principale difficulté est de supporter les paramètres optionnels.
- 5
- Pas de string en premier, on sait qu’il s’agit d’un module anonyme, on décale les variables en conséquence.
- 13
- Pas de tableau, on sait que le module n’a pas de dépendance. On décale à nouveau les variables.
- 18
- Les trois variables contiennent désormais les bonnes valeurs.
On modifie ensuite la méthode de chargement de script pour ajouter un callback. Cette fonction s’exécutera juste
après que la méthode define
ait terminé, le moment idéal pour venir rechercher les informations
sauvegardées et terminer l’instanciation du module.
- 16
- On enregistre le callback sur l’événement
load
. - 39
- On parcourt les valeurs sauvegardées jusqu’à trouver le module.
- 51
- On termine l’initialisation du module.
Terminé !
Bravo, notre tour de RequireJS est désormais achevé. Moins de 300 lignes auront été nécessaires pour refaire fonctionner de nouveau notre exemple. Le source complet est disponible ici.
A retenir
-
Il est possible de charger une dépendance avec la balise
script
, en utilisanthead.appendChild()
. - RequireJS est un bon exemple de “Programming into a language” comparé à “Programming in a language” (Code Complete, Steve McConnell) : “Programmers who program “in” a language limit their thoughts to constructs that the language directly supports. … Programmers who program “into” a language first decide what thoughts they want to express, and then determine how to express those thoughts using the tools provided by their specific language.”
A vous de forker
- Toutes les librairies ne sont pas définies en tant que module AMD. Beaucoup continuent de reposer sur un objet global mais tout n’est pas perdu. RequireJS supporte la notion de shim afin de configurer explicitement les dépendances de ces librairies.
- Les modules RequireJS ne polluent pas l’espace de nommage global avec comme avantage de pouvoir charger plusieurs versions d’une même librairie. Indice : RequireJS supporte plusieurs contextes.
- RequireJS propose également un optimiseur, dont la tâche est de regrouper plusieurs modules, de les minifier, etc, afin de réduire le nombre d’appels Ajax. Comment fonctionne-t-il ?
- RequireJS supporte le format CommonJS en proposant notamment un wrapper simplifié qui permet d’obtenir cette syntaxe :
Comment l’injection de dépendances fonctionne-elle sans le tableau des dépendances ? Indice :
Function.prototype.toString()