Utiliser jQuery avec Node.js sous Windows
Utiliser jQuery pour du développement côté serveur serait vraiment formidable ! C'est possible avec Node.js. Nous y sommes enfin, l'une des parties très intéressante qui peut justifier l'utilisation de Node.js à la place de vos développements PHP et Cie. C'est le moment de travailler de la même façon côté client et côté serveur sur vos processus de contrôle (vos Contrôleurs en MVC) et donc de développer vos sites avec un unique langage de programmation : JavaScript. Cependant, sous Windows, il y a une petite difficulté en plus liée aux modules développés en code natif que nous allons surmonter ! C'est parti pour manipuler le DOM côté Back-end !
À propos du module node-jquery
L'utilisation du module Node.js node-jquery permet de manipuler des objets jQuery dans du code JavaScript développé pour être interprété côté serveur par le moteur JavaScript V8 (l'interpréteur JavaScript socle de Node.js). node-jquery est lui-même le socle de base permettant cette utilisation de l'autre côté et comme son développeur le dit « n'est simplement que ça ».
Initialement développé par coolaj86 —le développeur qui a également développé le module node-walk utilisé dans mon précédent billet)— node-jquery va être dès à présent maintenu par la Team jQuery elle-même, ce qui assure un suivi et une utilisabilité solide de cette approche comme méthode de développement dans le futur.
Note : il est donc possible que dans un futur proche se soit un autre plugin que node-jquery qu'il faille installer pour utiliser jQuery côté serveur, car node-jquery n'est pas le seul module permettant d'utiliser jQuery côté serveur. Cependant, l'utilisation de jQuery en elle-même restera identique et équivalente à celle du côté client.
Ce que nous dit la documentation node-jquery...
Comme toujours, rendez-vous sur le registre des modules Node.js pour chercher node-jquery et ainsi atteindre la page de projet et de documentation.
On y apprend que :
- pour installer le module jQuery : on utilise npm install jquery (et pas npm install jQuery) et
- pour manipuler l'objet jQuery : on déclare var $ = require('jquery').create(); et manipule donc $.
Note : vous constaterez qu'il y a plusieurs projets de socle jQuery en place pour utiliser la librairie jQuery côté serveur si vous fouillez le registre NPM. Libre à-vous d'en choisir une autre. Il faut cependant prendre en compte le support du module dans le temps.
...mais ce qu'elle ne nous dit pas
Malheureusement, que vous exécutiez votre fichier avec la ligne require('jquery').create() avant ou après installation du module node-jquery, si vous êtes sous Windows, vous aurez toujours la même erreur : « Cannot find module 'jquery' ». Nous allons arranger cela plus loin (bon, en fait la doc mentionnait le problème sous Windows, mais j'aimais bien mes titres !).
jQuery ne marche pas avec Node.js sous Windows
Et oui ! Nous avons mis le doigt sur un problème que rencontre tous les débutants Node.js qui commence à jouer avec les modules NPM sous Windows. Effectivement, dans mon deuxième billet, j'ai passé sous silence les pré-requis qui nous auraient éviter ce souci pour entrer dans le vif du sujet. Maintenant, il est l'heure d'installer un environnement de développement Node.js plus complet pour résoudre ce problème.
Si vous ne possédez pas sur votre machine Python, Microsoft Visual Studio et quelques autres dépendances, en tapant npm install jquery dans votre console vous allez obtenir une erreur :
En fouillant dans le répertoire node_modules le dossier jquery est pourtant bien là ! Alors pourquoi ça ne marche pas ! La réponse se trouve dans les logs des erreurs disponibles dans votre console ou, comme indiqué, dans le fichier npm.debug.log :
Error: Can't find Python executable "python", you can set the PYTHON env variable.
et
contextify@0.1.5 install: `node-gyp rebuild`
error `cmd "/c" "node-gyp rebuild"` failed with 1
error Failed at the contextify@0.1.5 install script.
error This is most likely a problem with the contextify package,
error not with npm itself.
error Tell the author that this fails on your system:
error node-gyp rebuild
error You can get their info via:
error npm owner ls contextify
error There is likely additional logging output above.
Résolevons ce problème !
Python et Microsoft Visual Studio comme pré-requis Node.js sous Windows
En réalité si jQuery ne fonctionne pas, c'est parce que l'un des modules Node.js dont il est dépendant n'est pas un module écrits en JavaScript mais un module écrits en code natif, le module contextify, qui requiert donc d'être compilé sur la machine où il est utilisé pour fonctionner.
C'est le module node-gyp qui s'occupe de compiler les modules Node.js développés en language natif (autre qu'en JavaScript) et pour faire son travail il a besoin des logiciels Windows Python ainsi que Microsoft Visual Studio (ou Microsoft Visual C++).
Nous ne le verrons pas dans ce billet, mais sachez qu'il est également possible de se procurer les sources compilés de contextify pour votre machine et ne pas avoir à installer Python et Microsoft Visual Studio. Cependant, puisque le cas de figure se représentera assez souvent (avec d'autres modules natifs), je vous conseille de le faire.
Installation de Python 2.7.x pour Windows
Pour commencer nous allons installer le logiciel Python sur votre machine Windows. Bien que celui-ci existe en version 3.x.x il faut installer une version 2.7.x pour que cela fonctionne.
- Rendez-vous donc sur la page des « Releases » Windows et téléchargez la dernière version 2.7.x (dans mon cas c'est Python 2.7.4 du 6 avril 2013)
- Téléchargez le Windows X86-64 MSI Installer (2.7.4) et installez-le.
A ce stade, en ré-essayant la commande npm install jquery l'erreur précédente est passée et la nouvelle erreur est :
MSBUILD : error MSB3428: Impossible de charger le composant Visual C++ "VCBuild.exe" (MSBUILD : error MSB3428: Could not load the Visual C++ component "VCBuild.exe").
Installation de Microsoft Visual C++ pour Node.js
Pour vous procurer le « VCBuild.exe » dont il est question : installez le logiciel Microsoft Visual Studio 2010 (ou 2012). Vous pouvez vous contenter uniquement de la branche C++ du logiciel (Microsoft Visual C++) et vous contenter même de sa version Express (gratuite et moins lourde).
- Rendez-vous donc sur le site de Microsoft et téléchargez Microsoft Visual C++ 2010 Express par exemple.
- Ouvrez l'installeur « vc_web.exe » et suivez les « Next ». Vous n'êtes pas obligé de sélectionner les produits optionnels. Cliquez enfin sur « Install » et les fichiers sont téléchargés pour installation et installés.
En ré-essayant de nouveau la commande npm install jquery vous obtenez à présent l'erreur :
error MSB8007: The Plateform for project 'contextify.vcxproj' is invalid.
Microsoft Windows SDK for Windows 7 and .NET Framework 4
Pour corriger l'erreur précédente :
- Rendez-vous à la page de téléchargement de Microsoft Windows SDK for Windows 7 and .NET Framework 4, téléchargez « winsdk_web.exe » et installez-le.
En cas de problème :
- Désinstallez les Microsoft Visual C++ 2010 Redistributable x86 et x64 déjà installées (Menu Windows puis Panneau de congiguration puis Désinstaller un programmes). Ré-essayez.
- Vous pouvez en plus télécharger directement l'ISO du SDK (« GRMSDK_EN_DVD.iso » pour x86 et « GRMSDKX_EN_DVD.iso » pour x64), gravez ou dézippez l'ISO et exécutez « setup.exe ».
En ré-essayant de nouveau la commande npm install jquery vous obtenez à présent le message d'avertissement (en jaune) :
warning C4530: C++ exception handler used, but unwind semantics are not enabled.
Si c'est le cas c'est que tout s'est bien passé.
Visual C++ 2010 SP1 Redistributable Package
Pour (re)mettre les versions Microsoft Visual C++ 2010 Redistributable :
- Rendez-vous à la page de téléchargement de Microsoft Visual C++ 2010 SP1 Redistributable Package, téléchargez-le (en x86 ou x64 selon votre OS) et installez-le.
Microsoft Visual C++ 2010 Service Pack 1 Compiler Update for the Windows SDK 7.1
Pour vous assurer de posséder tout ce qu'il faut pour ne rencontrer aucun problème à l'avenir :
- Rendez-vous à la page de téléchargement de Microsoft Visual C++ 2010 Service Pack 1 Compiler Update for the Windows SDK 7.1, téléchargez « VC-Compiler-KB2519277.exe » et installez-le.
- Redémarrez votre ordinateur... (grrr)
Développer côté serveur avec jQuery
Maintenant que les pré-requis sont en place vous ne devriez plus avoir de problème pour compiler les modules Node.js en langage natif. À présent :
- si ce n'est pas déjà fait : installez le module jQuery (node-jquery) avec la commande npm install jquery depuis le répertoire de votre projet (pour moi C:\nodejs\website\),
- et installez jsdom depuis le même répertoire avec la commande npm install jsdom.
En reprenant le fichier website.js de mon billet précédent ; voici comment inclure jsdom et jQuery :
var
/*== Charger les modules Node.js ==*/
http = require("http"),
express = require("express"),
httpServer = express(),
jsdom = require("jsdom").jsdom, // Utilisation d'un créateur de DOM.
jquery = require('jquery'), // Utilisation de jQuery côté serveur.
/* ... */
Créer le DOM côté serveur avec jsdom
Reprenons l'exemple de la page d'accueil du site qui se présentait ainsi dans mon mon billet précédent :
/* ... */ /* Accueil */ httpServer.get(appConfig.path, function (request, response) { var header = { "Content-Type": "text/html", "Charset": "utf-8" }, template = '<!DOCTYPE html>' + '<html lang="fr">' + '<head>' + '<meta charset="utf-8" />' + '<title>Ma première page</title>' + '<base href="//' + appConfig.url + '" />' + '</head>' + '<body>' + "<p><a href='./connexion/' title='Page de connexion'>Page de connexion</a></p>" '</body>' + '</html>'; response.writeHead(200, header); response.write(template); response.end(); }); /* ... */
Effectuons plusieurs changements pour nous amuser avec jQuery.
- Tout d'abord, ligne 8, nous avions créé une variable template représentant le contenu de la page qui allait être fourni au client (le navigateur du visiteur réclamant la page).
- Nous allons dans un premier temps transformer cette variable de type String en un véritable document DOM côté serveur avec jsdom,
- et ensuite l'englober dans une window (fenêtre) émulée de 1024px par défaut.
- Pour finir nous allons restituer ce DOM sous forme de string pour l'envoyer au client comme précédemment à la ligne 21.
/* ... */ /* Accueil */ httpServer.get(appConfig.path, function (request, response) { var header = { "Content-Type": "text/html", "Charset": "utf-8" }, // L'ancienne variable "template" de type String est remplacée // par la variable "document" de type Object représentant le DOM de la page. document = jsdom("<!DOCTYPE html>" + "<html lang='fr'>" + "<head>" + "<meta charset='utf-8' />" + "<title>Ma première page</title>" + "<base href='//" + appConfig.url + "' />" + "</head>" + "<body>" + "<p><a href='./connexion/' title='Page de connexion'>Page de connexion</a></p>" "</body>" + "</html>"), // Un objet "window" est créé. // C'est l'émulation d'une fenêtre côté serveur qui a pour taille 1024px de large. window = document.createWindow(); // console.log(typeof document); // renvoi "object". // console.log(window.innerWidth); // renvoi "1024" response.writeHead(200, header); // On change la variable template par le rendu sous forme de string document (ou window.document). response.write(window.document.innerHTML); response.end(); }); /* ... */
Manipuler le DOM côté serveur avec jQuery
Nous allons dans un exemple tout simple ajouter une balise h1 à notre DOM après la création de celui-ci avec jQuery. Nous allons même effectuer un test pour démontrer comment le DOM peut être variable en fonction de conditions précises.
/* ... */ /* Accueil */ httpServer.get(appConfig.path, function (request, response) { var header = { "Content-Type": "text/html", "Charset": "utf-8" }, document = jsdom("<!DOCTYPE html>" + "<html lang='fr'>" + "<head>" + "<meta charset='utf-8' />" + "<title>Ma première page</title>" + "<base href='//" + appConfig.url + "' />" + "</head>" + "<body>" + "<p><a href='./connexion/' title='Page de connexion'>Page de connexion</a></p>" "</body>" + "</html>"), window = document.createWindow(), // Permettre à jQuery de manipuler le DOM... jQuery = jquery.create(window), // ...et au développeur de le manipuler via jQuery("selecteur")... $ = jQuery, // ...ou via $("selecteur"). // On crée un nœud "h1" contenant le texte "Ceci est le titre d'origine !". h1Title = $("<h1>").text("Ceci est le titre d'origine !"); // On l'ajoute au DOM serveur. $("body").prepend(h1Title); // Si la condition est réalisée... if (1 == 1) { // ...ce qui est le cas ; renvoi "true"... h1Title = $("h1").text("Ceci est le titre changé !"); // ... alors on cible le nœud h1 et on modifie le texte avec "Ceci est le titre changé !". } response.writeHead(200, header); response.write(window.document.innerHTML); response.end(); }); /* ... */
Exemple de rendu conditionnel après un POST avec jQuery
Tournons nous à présent vers la seconde page de mon fichier website.js précédent : la page de connexion. Ajoutons lui quelques styles et prévoyons son rendu en cas d'erreur de connexion ou de réussite de l'utilisateur.
/* ... */ /* Connexion */ function connectionRender(request, response) { var header = { "Content-Type": "text/html", "Charset": "utf-8" }, // LE CONTENU COMPLET DE "document" SE TROUVE PLUS BAS. document = jsdom("<!DOCTYPE html ><html>..."), window = document.createWindow(), jQuery = jquery.create(window), $ = jQuery; // Par défaut, on retire l'affichage après connexion puisqu'on vient d'arriver. // On modifiera cette partie de code plus loin. $("#logged").remove(); response.writeHead(200, header); response.write(window.document.innerHTML); response.end(); } httpServer.get(appConfig.path + "connexion", function (request, response) { connectionRender(request, response); }).post(appConfig.path + "connexion", function (request, response) { connectionRender(request, response); }); /* ... */
Contenu complet de document = jsdom("<!DOCTYPE html><html>...") :
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Page de connexion</title> "<base href=//" appConfig.url " />" <style text="text/css"> .error { display: none; } .error.show { display: block; } .error span { display: none; color: #ff0000; } .error span.show { display: block; } </style> </head> <body> <form id="login" action="./connexion/" method="POST"> <div class="field-login-email"> <label for="login-email" placeholser="Email">Email : </label> <input id="login-email" type="text" name="login[email]" /> <div class="error"> <span class="empty">Le champ email est vide.</span> <span class="invalide">Le champ email est invalide.</span> </div> </div> <div class="field-login-password"> <label for="login-password" placeholser="Mot de passe">Mot de passe : </label> <input id="login-password" type="password" name="login[password]" /> <div class="error"> <span class="empty">Le champ mot de passe est vide.</span> <span class="invalide">Le couple Email/Mot de passe est incorrecte !</span> </div> </div> <label><input type="submit" value="Ok" /></label> </form> <div id="logged">Vous êtes passé !</div> </body> </html>
Ce qui donne comme rendu :
Gérer les erreurs
Quand la page de connexion est demandée suite au clique sur le bouton « Ok » c'est une requête POST qui est envoyée et le résultat est traité par le code ligne 27 de notre exemple JavaScript plus haut. À cette ligne, la fonction connectionRender est appelée tout comme en GET à la différence prêt que des variables POST existent dans le cas de figure POST (ce qui n'est pas le cas en GET).
Nous allons donc tester l'existence et le contenu des variables POST du formulaire pour décider de la réponse adaptée à notre visiteur.
Tout d'abord, permettons à Express de récupérer les valeurs des champs de formulaire via l'attribut name. Pour cela il faut lui « demander » d'utiliser l'intergiciel bodyParser.
/* ... */ httpServer /* == Définir un environnement d'exécution : SET NODE_ENV=dev ==*/ .configure(function () { /* Commun à tous les environnements */ // Ajout du middleware "bodyParser" pour parser les attributs name // et les récupérer via request.body. httpServer.use(express.bodyParser()); }) /* ... */
Ensuite mettons en place le mécanisme de réception uniquement en POST.
/* ... */ /* Connexion */ function connectionRender(request, response) { var header = { "Content-Type": "text/html", "Charset": "utf-8" }, document = jsdom("<!DOCTYPE html ><html>..."), window = document.createWindow(), jQuery = jquery.create(window), $ = jQuery; // Si on passe par ici après une réclamation en POST, on entre. if (request.method == 'POST') { var // Par défaut, on estime que la validation du formulaire est bonne. isValid = true, // Normalement, cet info se récupère dans une base de donnée. C'est un des utilisateurs valide. passEmail = "tout@est.ok", // Et ça c'est le mot de passe associé à l'utilisateur. passPassword = "azerty", // Une regex pour vérifier la validité des emails. checkEmail = /^[-._a-z0-9]+@[-._a-z0-9]+\.[.a-z]{2,4}$/i; // Quoi qu'il arrive on réinjecte la valeur email (mais pas mot de passe). $("#login-email").val(request.body.login.email.trim()); // Le champ email est-il remplit ? if (request.body.login.email.trim() == "") { // S'il est vide, on le signale. $(".field-login-email .error").addClass("show"); $(".field-login-email .error .empty").addClass("show"); // Et on ne valide pas le formulaire. isValid = false; // Le champ email est-il valide ? } else if (!checkEmail.test(request.body.login.email.trim())) { // S'il est anormal, on le signale. $(".field-login-email .error").addClass("show"); $(".field-login-email .error .invalide").addClass("show"); // Et on ne valide pas le formulaire. isValid = false; } // Le champ mot de passe est-il remplit ? if (request.body.login.password.trim() == "") { // S'il est vide, on le signale. $(".field-login-password .error").addClass("show"); $(".field-login-password .error .empty").addClass("show"); // Et on ne valide pas le formulaire. isValid = false; } if (isValid) { // Y a-t-il une concordance email / mot de passe. if ( request.body.login.email.trim() == passEmail && request.body.login.password.trim() == passPassword ) { ///////////////// // C'est ici qu'on va mettre en place un mécanisme de remplissage de session. ///////////////// } else { // S'il y a une erreur, on le signale. $(".field-login-password .error").addClass("show"); $(".field-login-password .error .invalide").addClass("show"); } } } // Nous modifierons cette partie dans la partie suivante. $("#logged").remove(); response.writeHead(200, header); response.write(window.document.innerHTML); response.end(); } httpServer.get(appConfig.path + "connexion", function (request, response) { connectionRender(request, response); }).post(appConfig.path + "connexion", function (request, response) { connectionRender(request, response); }); /* ... */
Tout manquement au formulaire sera à présent signalé.
Gérer la réussite
Permettons également de conserver dans une variable de session des informations qui suivront l'utilisateur de page en page (un équivalent de $_SESSION en PHP par exemple) avec les intergiciels cookieParser et session de Express. Pour cela il va falloir allouer de la mémoire au cookie pour l'entregistrement avec express.session.MemoryStore.
On récupère l'objet MemoryStore
/* ... */ http = require("http"), express = require("express"), httpServer = express(), // Ajout de l'objet MemoryStore pour donner du volume au cookie de session. MemoryStore = express.session.MemoryStore, jsdom = require("jsdom").jsdom, jquery = require('jquery'), /* ... */
On permet les sessions avec cookieParser et session
/* ... */ httpServer /* == Définir un environnement d'exécution : SET NODE_ENV=dev ==*/ .configure(function () { /* Commun à tous les environnements */ httpServer.use(express.bodyParser()); /* Permettre de manipuler les sessions */ httpServer.use(express.cookieParser()); /* Paramétrage des sessions */ httpServer.use(express.session({ store: new MemoryStore(), // Allocation de place. secret: 'secret', // Secret connu du serveur rendant le cookie illisible pour le client. key: 'website.sid' // Nom du cookie. })); }) /* ... */
Ensuite terminons de mettre en place le mécanisme de réception.
/* ... */ if (isValid) { if ( request.body.login.email.trim() == passEmail && request.body.login.password.trim() == passPassword ) { // En passant ici on initialise l'état d'utilisateur identifié. request.session.user = request.session.user || {}; // Création ou conservation de l'objet arbitrairement nommé "user" s'il n'existe pas. // Passage à true de la propriété arbitrairement nommé "isLogged". request.session.user.isLogged = true; } else { $(".field-login-password .error").addClass("show"); $(".field-login-password .error .invalide").addClass("show"); } } } // Changement de la partie du rendu affiché en fonction d'un état connecté ou non connecté. // S'il y a un objet user avec la propriété isLogged à true. if (request.session.user && request.session.user.isLogged) { // On retire la partie hors-connexion. $("#login").remove(); } else { // On retire la partie connexion. $("#logged").remove(); } response.writeHead(200, header); response.write(window.document.innerHTML); response.end(); /* ... */
À présent en tapant respectivement dans les champs « Email » et « Mot de passe » : "tout@est.ok" et "azerty" la page s'affichera en "mode connecté", même après rechargement de la page. Seul la fermeture du navigateur détruira le lien entre le client et la session car le cookie est par défaut paramétré pour expirer à la fin de la sesion. On affichera donc alors la partie "non connecté".
Réflexion sur l'utilisation de jQuery côté serveur
Séparation de la vue et du contrôleur
C'est probablement le plus gros avantage d'utiliser jQuery côté serveur. Il permet ainsi d'avoir un template à plat dont on n'a pas à se soucier et un intégrateur ne connaissant rien en programmation (ça existe ?) pourrait créer des designs sans problèmes.
Ne pas faire du Responsive Web Design côté serveur
Même si l'on simule une fenêtre côté serveur, il ne sert à rien de gérer de la responsivité en essayant d'obtenir la taille de la fenêtre puisque celle qui nous intéresse est celle du client et non celle du serveur émulée. D'ailleurs les fonctions se rapportant à l'affichage ne fonctionnent pas toutes, car côté serveur, le navigateur n'est pas là pour vous répondre (ainsi show() et hide() ne marcheront pas par exemple).
Performance ?
La question. Il est sûr que passer par jQuery est plus lent à l'exécution que de simplement scinder le template en dix parties et de le remplir de conditions. Cependant, à la maintenance c'est un gain de temps complètement multiplié ! Entre payer des ressources qui développeront, débogueront et maintiendront en plus de temps et payer un serveur plus puissant : voyez où sont vos économies ! De plus, nous verrons plus loin que nous pourrons limiter jQuery à des cas précis et se servir d'un moteur de template (ce qui séparera « presque » la vue du contrôleur mais qui sera bien plus lisible que la méthode « v'l'a que je te fou tout au même endroit »).
Je n'ai pas assez de retour d'expérience sur la rapidité en production avec énormément d'utilisateur simultané. Si vous en avez, n'hésitez pas à la partager.
Source
Notre fichier commence à être bien remplis. Dans un prochain billet Node.js nous allons voir comment scinder notre code qui commence à être bien long dans plusieurs fichiers.
En attendant, vous pouvez télécharger le fichier résultat de cet article ici.
3 Commentaires
Choisir un Avatar depuis une url
Adresse : (64px x 64px)ou Changer la couleur de fond
et Choisir un Avatar dans la liste
Votre commentaire a été ajouter ci-dessous ! Si vous désirez le supprimer ultérieurement, servez vous du code suivant :
Les commentaires sont actuellement affiché du plus rescent au plus ancien. Vous pouvez inverser l'ordre en cliquant ci-dessous :
C’était facile pour moi d’assimiler ce tutoriel car j’ai déjà bénéficié des tutoriels en vidéo pour savoir comment utiliser JQuery sur http://www.alphorm.com/tutoriel/formation-en-ligne-jquery. Merci pour ce partage.
Je trouve ce commentaire pertinent ?
C’était facile pour moi d’assimiler ce tutoriel car j’ai déjà bénéficié des tutoriels en vidéo pour savoir comment utiliser JQuery sur http://www.alphorm.com/tutoriel/formation-en-ligne-jquery. Merci pour ce partage.
Je trouve ce commentaire pertinent ?
Merci pour cet article
Je trouve ce commentaire pertinent ?
Je trouve ce commentaire pertinent ?