Codez en JavaScript de manière intelligente et modulaire

Certaines personnes semblent encore surpris que JavaScript soit considéré comme un langage de programmation respectable et adulte pour les applications sérieuses. En fait, le développement de JavaScript a bien mûri pendant des années, avec les meilleures pratiques de développement modulaire comme exemple.

Les avantages de l'écriture de code modulaire sont bien documentés: meilleure maintenabilité, évitement des fichiers monolithiques et découplage du code en unités qui peuvent être testées correctement. Pour ceux d'entre vous qui aimeraient être rattrapés rapidement, voici les pratiques courantes utilisées par les développeurs JavaScript modernes pour écrire du code modulaire.

Modèle de module

Commençons par un modèle de conception de base appelé modèle de module. Comme vous vous en doutez, cela nous permet d'écrire du code de manière modulaire, nous permettant de protéger le contexte d'exécution de modules donnés et d'exposer globalement uniquement ce que nous voulons exposer.

(function(){

//'private' variable

var orderId = 123;

// expose methods and variables by attaching them

// to the global object

window.orderModule = {

getOrderId: function(){

//brought to you by closures

return orderId;

}

};

})()

Une function expression anonyme  , agissant comme une usine dans ce cas, est écrite et appelée immédiatement. Pour plus de rapidité, vous pouvez transmettre explicitement des variables à l'  function appel, reliant efficacement ces variables à une portée locale. Vous verrez également cela parfois comme une manœuvre défensive dans les bibliothèques prenant en charge d'anciens navigateurs où certaines valeurs (telles que undefined) sont des propriétés inscriptibles.

(function(global, undefined){

// code here can access the global object quickly,

// and 'undefined' is sure to be 'undefined'

// NOTE: In modern browsers 'undefined' isn't writeable,

// but it's worth keeping it in mind

// when writing code for old browsers.

})(this)

Ceci est juste un modèle de conception. Avec cette technique, pour écrire du JavaScript modulaire, vous n'avez pas besoin d'inclure des bibliothèques supplémentaires. Le manque de dépendances est un gros plus (ou essentiel à la mission) dans certains contextes, en particulier si vous écrivez une bibliothèque. Vous verrez que la plupart des bibliothèques populaires utiliseront ce modèle pour encapsuler des fonctions et des variables internes, exposant uniquement ce qui est nécessaire.

Si vous rédigez une application, cependant, cette approche présente quelques inconvénients. Disons que vous créez un module qui définit quelques méthodes  window.orders. Si vous souhaitez utiliser ces méthodes dans d'autres parties de votre application, vous devez vous assurer que le module est inclus avant de les appeler. Ensuite, dans le code où vous appelez window.orders.getOrderId, vous écrivez le code et espérez que l'autre script s'est chargé.

Cela peut ne pas sembler être la fin du monde, mais cela peut rapidement devenir incontrôlable sur des projets compliqués - et gérer l'ordre d'inclusion des scripts devient une douleur. En outre, tous vos fichiers doivent être chargés de manière synchrone, ou vous inviterez des conditions de concurrence à casser votre code. Si seulement il y avait un moyen de déclarer explicitement les modules que vous vouliez utiliser pour un bit de code donné ...

AMD (définition de module asynchrone)

AMD est né de la nécessité de spécifier des dépendances explicites tout en évitant le chargement synchrone de tous les scripts. Il est facile à utiliser dans un navigateur mais n'est pas natif, vous devez donc inclure une bibliothèque qui effectue le chargement de script, comme RequireJS ou curl.js. Voici à quoi cela ressemble de définir un module avec AMD:

// libs/order-module.js

define(function(){

//'private' variable

var orderId = 123;

// expose methods and variables by returning them

return {

getOrderId: function(){

return orderId;

}

});

Cela ressemble à ce que nous avions affaire auparavant, sauf qu'au lieu d'appeler immédiatement notre fonction de fabrique directement, nous la passons en argument à  define. La vraie magie commence à se produire lorsque vous souhaitez utiliser le module plus tard:

define( [ 'libs/order-module' ], function(orderModule) {

orderModule.getOrderId(); //evaluates to 123

});

Le premier argument de  define est maintenant un tableau de dépendances, qui peut être arbitrairement long, et la fonction de fabrique liste les paramètres formels pour ces dépendances à y être attachées. Maintenant, certaines dépendances dont vous avez besoin peuvent avoir leurs propres dépendances, mais avec AMD, vous n'avez pas besoin de savoir que:

// src/utils.js

define( [ 'libs/underscore' ], function(_) {

return {

moduleId: 'foo',

_ : _

});

// src/myapp.js

define( [

'libs/jquery',

'libs/handlebars',

'src/utils'

], function($, Handlebars, Utils){

// Use each of the stated dependencies without

// worrying if they're there or not.

$('div').addClass('bar');

// Sub dependencies have also been taken care of

Utils._.keys(window);

});

C'est un excellent moyen de développer un JavaScript modulaire lorsqu'il s'agit de nombreuses pièces mobiles et dépendances. La responsabilité de la commande et de l'inclusion des scripts incombe désormais au chargeur de scripts, vous laissant libre de simplement indiquer ce dont vous avez besoin et de commencer à l'utiliser.

D'un autre côté, il y a quelques problèmes potentiels. Tout d'abord, vous avez une bibliothèque supplémentaire à inclure et à apprendre à utiliser. Je n'ai aucune expérience avec curl.js, mais RequireJS consiste à apprendre à configurer la configuration de votre projet. Cela prendra quelques heures pour se familiariser avec les paramètres, après quoi l'écriture de la configuration initiale ne devrait prendre que quelques minutes. En outre, les définitions de module peuvent s'allonger si elles conduisent à une pile de dépendances. Voici un exemple, tiré de l'explication de ce problème dans la documentation RequireJS:

// From RequireJS documentation:

// //requirejs.org/docs/whyamd.html#sugar

define([ "require", "jquery", "blade/object", "blade/fn", "rdapi",

"oauth", "blade/jig", "blade/url", "dispatch", "accounts",

"storage", "services", "widgets/AccountPanel", "widgets/TabButton",

"widgets/AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",

"jquery.textOverflow"],

function (require, $, object, fn, rdapi,

oauth, jig, url, dispatch, accounts,

storage, services, AccountPanel, TabButton,

AddAccount, less, osTheme) {

});

Aie! RequireJS fournit un peu de sucre syntaxique pour gérer cela, qui ressemble un peu à une autre API populaire pour le développement modulaire, CommonJS.

CJS (CommonJS)

Si vous avez déjà écrit du JavaScript côté serveur à l'aide de Node.js, vous avez utilisé des modules CommonJS. Chaque fichier que vous écrivez n'est pas emballé dans quoi que ce soit de fantaisie, mais a accès à une variable appelée  exports à laquelle vous pouvez affecter tout ce que vous voulez voir exposé par le module. Voici à quoi cela ressemble:

// a 'private' variable

var orderId = 123;

exports.getOrderId = function() {

return orderId;

};

Ensuite, lorsque vous souhaitez utiliser le module, vous le déclarez en ligne:

// orderModule gets the value of 'exports'

var orderModule = require('./order-module');

orderModule.getOrderId(); // evaluates to 123

Sur le plan syntaxique, cela m'a toujours semblé meilleur, principalement parce que cela n'implique pas l'indentation inutile présente dans les autres options dont nous avons discuté. D'un autre côté, il diffère grandement des autres, en ce sens qu'il est conçu pour le chargement synchrone des dépendances. Cela a plus de sens sur le serveur, mais ne le fera pas sur le front-end. Le chargement synchrone des dépendances signifie des temps de chargement de page plus longs, ce qui est inacceptable pour le Web. Bien que CJS soit de loin ma syntaxe de module préférée, je peux l'utiliser uniquement sur le serveur (et lors de l'écriture d'applications mobiles avec Titanium Studio d'Appcelerator).

Un pour les gouverner tous

Le projet actuel de la sixième édition d'ECMAScript (ES6), la spécification à partir de laquelle JavaScript est implémenté, ajoute un support natif pour les modules. La spécification est toujours à l'état de brouillon, mais cela vaut la peine de jeter un coup d'œil à ce à quoi pourrait ressembler l'avenir du développement modulaire. De nombreux nouveaux mots-clés sont utilisés dans la spécification ES6 (également appelée Harmony), dont plusieurs sont utilisés avec des modules:

// libs/order-module.js

var orderId = 123;

export var getOrderId = function(){

return orderId;

};

Vous pouvez l'appeler ultérieurement de plusieurs manières:

import { getOrderId } from "libs/order-module";

getOrderId();

Ci-dessus, vous choisissez les exportations d'un module que vous souhaitez lier à des variables locales. Vous pouvez également importer le module entier comme s'il s'agissait d'un objet (similaire à l'  exports objet dans les modules CJS):

import "libs/order-module" as orderModule;

orderModule.getOrderId();

Il y a beaucoup plus dans les modules ES6 (consultez le blog 2ality de Dr. Axel Rauschmayer pour plus), mais l'exemple devrait vous montrer quelques choses. Tout d'abord, nous avons une syntaxe similaire aux modules CJS, ce qui signifie qu'une indentation supplémentaire est introuvable, bien que ce ne soit pas toujours le cas, comme vous le trouverez dans le lien 2ality ci-dessus. Ce qui n'est pas évident en regardant l'exemple, en particulier parce qu'il ressemble tellement à CJS, c'est que les modules répertoriés dans l'instruction d'importation sont chargés de manière asynchrone.

Le résultat final est la syntaxe facile à lire de CJS mélangée à la nature asynchrone d'AMD. Malheureusement, il faudra un certain temps avant que ceux-ci soient entièrement pris en charge par tous les navigateurs couramment ciblés. Cela dit, "un certain temps" est de plus en plus court à mesure que les fournisseurs de navigateurs resserrent leurs cycles de publication.

Aujourd'hui, une combinaison de ces outils est la voie à suivre en matière de développement JavaScript modulaire. Tout dépend de ce que vous faites. Si vous écrivez une bibliothèque, utilisez le modèle de conception de module. Si vous créez des applications pour le navigateur, utilisez des modules AMD avec un chargeur de script. Si vous êtes sur le serveur, profitez des modules CJS. Finalement, bien sûr, ES6 sera pris en charge dans tous les domaines - à quel point vous pouvez faire les choses comme ES6 et supprimer le reste!

Questions ou réflexions? N'hésitez pas à laisser un message ci-dessous dans la section commentaires ou à me contacter sur Twitter, @ freethejazz.