package = modules + dépendances + tests
(*) dans l'ecosystème JavaScript en général
moment = manipulation de dates, underscore/lodash = manipulation des tableaux/collections, mongodb-native = driver MongoDB, js-yaml = parser YAML...
Dans une société, il est courant de réaliser des librairies pour des briques logicielles internes (connexion au DAS...), ou pour mutualiser un savoir-faire métier
Même si votre application n'est pas destinée à être réutilisée, la packager est une nécessité
NPM apporte des conventions (package.json) et des outils pour la distribution (registry central + client de packaging)
lib/ # votre code (aussi libs)
node_modules/ # les dépendances gérées par NPM
test/ # le code de vos tests
.npmignore # les exclusions du package
README.md
package-lock.json # à partir de NPM 5 (node 8)
package.json
app/ # code serveur uniquement
tests/
assets/ # Les assets coté serveur
config/ # les fichiers de configuration (aussi conf)
node_modules/ # les dépendances serveur gérées par NPM
public/ # code client
dist/ # le client minifié
test/
vendor/ # dépendances client, gérées par bower/webpack... (aussi libs)
package-lock.json # à partir de NPM 5 (node 8)
package.json
2 points communs: package.json
et node_modules
sont toujours à la racine
Il est courant de mettre le code du client et du serveur dans le même package : en NodeJS, le serveur static et le même que le serveur d'API/pages web dynamiques
NPM gère les dépendances coté serveur majoritairement, mais diversifie de plus en plus son écosystème, malgré tout d'autres outils permettent de gérer les dépendances coté client (bower, browserify, webpack et d'autres encore...)
Le versionning, avec git par exemple, sera aussi à la racine
Attention ! En l'absence d'un .npmignore
, NPM utilisera le .gitignore
éventuel
Ce ne sont que des exemples : il n'y a pas de dogme, et on trouve ne nombreuses variantes, souvient liée aux outils de build ou au serveur
package.json
{
"name": "nom-du-projet",
"version": "0.3.1",
"description": "Elle sera publiée",
"author": "You ",
"license": "MIT",
"repository": "https://github.com/you/nom-du-projet.git",
"dependencies": {
"lodash": "~2.4.1"
},
"main": "./lib/entry_point.js",
"bin": {
"start-project": "./bin/start"
},
"scripts": {
"test": "mocha test"
},
"engines": {"node": ">=0.10.3"}
}
name
et version
sont les deux seuls champs obligatoires les plus importants de votre descripteur
name
doit être en minuscule, sans lettres exotiques, raisonnablement court et descriptif. Il sera dans des require()
et fera partie d'une url !
version
doit respecter le format semver
description
, keywords
permettront aux développeurs qui cherchent des librairies sur le registry NPM de trouver la vôtre
author
, constributors
, license
relètent l'état de la communauté autour de votre projet
repository
, homepage
, bugs
sont des url affichées sur le site du registry NPM pour simplifier l'accès à votre code
dependencies
, devDependencies
, peerDependencies
, bundledDependencies
et optionalDependencies
décrivent les dépendances de votre projet. Nous y reviendrons juste après
Dans le cas d'une librairie, main
indique quel est le module qui sera chargé lors d'un require('nom-du-projet');
bin
déclare les éventuel exécutables, indispensable pour un projet de type CLI
scripts
permet de customiser les phases de packaging et d'ajouter ses propres phases
engines
, engineStrict
, cpu
, os
permettent de déclarer des restrictions de portabilité
package-lock.json
npm install
de manière sûrenpm install
ça évite les cas où un projet ne fonctionne plus car une librairie a introduit (par erreur) un breaking change
^x.y.z
: le premier chiffre différent de 0 est fixé (les autres chiffres au minimum)
^1.2.3
équivaut à >= 1.2.3 <2.0.0
^0.1.2
équivaut à >= 0.1.2 <0.2.0
^0.0.1
équivaut à 0.0.1
~x.y.z
: la version x.y la plus haute (z au minimum)
~1.2.3
équivaut à >= 1.2.3 <1.3.0
~0.1.2
équivaut à >= 0.1.2 <0.2.0
Il existe des des opérateurs <
, <=
, >
, >=
, NPM prendra la version la plus haute disponible
Il y a aussi des jokers *
, x
Enfin l'opérateur chapeau ~x.y.z
: la version x.y la plus haute (z au minimum)
Un article détaillé sur le comportement de ~
et ^
x.y.z
: exactement la versiongit://github.com/user/project.git#commit
http://bitbucket.org/user/repo?format=tar.gz
npm install ma-lib
qui mettra à jour les fichiers package.json
et package-lock.json
Cette vidéo explique bien le fonctionnement
Contrairement à l'écosystème Java, il n'y a qu'un seul dépôt central pour les packages NodeJS. Il existe des mirroirs qui sont en train de disparaitres avec la fiabilisation du dépôt central, ne les utilisez pas.
Il est possible de créer des dépôts privés (NPM Entreprise), ou de payer un espace privé sur npmjs.com (a venir)
Attention ! vous ne devez (pouvez) pas republiez une version déjà existante !
D'autres clients (bower...) utilisent aussi ce dépôt
npm install
: récupération & compilation des dépendances, création des exécutablesnpm test
: lance l'exécution des tests présentsnpm start/restart/stop
: lance et arrête le projetnpm publish
: publie le projet sur le registry centralLa récupération d'une dépendance revient à télécharger le code, et réaliser un npm install
dans le dossier de manière transitive
Les commandes npm test/start/restart/stop
exécutent les scripts preX, X et postX lorsqu'ils existent
La commande npm run-script X
exécute les scripts preX, X et postX de la même manière
Certaines phases (postinstall, prepublish...) peuvent être customisées avec les scripts
du descripteur
Il n'y a pas cycle de vie ni de phases ordonnancées
Les scripts sont dépendants de la plateforme
Il peut y avoir une compilation de code C/C++ lors de l'installation d'une dépendance
A la fin de l'installation du package si celui-ci contient des executables (package.json/bin
), ils sont enregistrés en global au niveau système si npm est lancé avec l'option -g
NPM n'est pas un build-system : il ne permet pas de déclarer et d'ordonnancer des tâches de mettre en oeuvre des configuration par environnement, et d'être un outil de compilation/packaging portable. Il propose des mécanismes minimalistes pour le packaging, dans l'unique optique de la publication
const assert = require('assert');
// describe est une fonction qui définit un "groupe" de tests
describe('Array#indexOf', () => {
// it est une fonction qui définit un test (pas d'inclusion)
it('should return -1 when the value is not present', () => {
// contenu du test : si le test lance une exception, il échoue
assert.equal(-1, [1,2,3].indexOf(5));
assert.equal(-1, [1,2,3].indexOf(0));
})
it('should return index when the value is present', () => {
assert.equal(2, [1,2,3].indexOf(3));
assert.equal(0, [1,2,3].indexOf(1));
// s'il arrive à la fin, il réussit
})
})
mocha test
describe()
et it()
sont des fonctions globales, définies par mocha. Il en existe d'autre que nous verrons plus loins
Mocha propose l'interface BDD (par défaut), celle de l'exemple. Il propose aussi l'interface TDD, où describe()
= suite()
et it()
= test()
Nous verrons plus loin en quoi l'interface TDD est moins intéressante
Mocha fonctionne en 2 phases : la détection des tests :
describe
pour avoir la liste des it()
it()
un à unassert
est un module NodeJS que nous n'utiliseront pas à cause de ses faibles capacités de reporting
Mocha est totallement décoréllé de NodeJS, et s'utilise très bien dans un navigateur
Il existe différents reporters, essayer le reporter nyan !
// notez l'inclusion de chai
const expect = require('chai').expect;
describe('Array#indexOf', () => {
it('should return -1 when the value is not present', (done) => {
const array = [1, 2, 3];
// permet de faire la même chose...
expect(array.indexOf(5)).to.equal(-1);
//...et des assertions bien plus expressives !
expect(array).to.be.an.instanceof(Array).and.to.have.a.lengthOf(3);
expect(array).to.include(2);
expect(array).not.to.contain(4);
done();
});
});
La combinaison Mocha mode BDD + Chai mode expect donne suffisamment de lisibilité au code pour qu'il reflète des spécifications fonctionnelles ! Vous n'aurez plus besoin de documenter vos assertions et vos cas de tests, car ils doivent être suffisamment parlants
Tout comme Mocha, Chai est totalement décorrélé de NodeJS, et s'utilise très bien dans un navigateur, il a malgré tout une API inconsistante
Le style d'assertion should est mal supporté sur IE car il instrumente les objets testés
Le style d'assertion assert est vraiment trop has-been :)
Une assertion Chai est une chaine de mots dont certains sont sans effet (to, be, been, is, that, and, has, have, with, at, of, same), et d'autres déclenchent une validation (instanceof(), lengthOf(), include()...), qui si elle échoue lèvera une exception
Le message d'exception reprend les mots précédents pour être suffisamment parlant : expected [ 1, 2, 3 ] to have a length of 4 but got 3
expect(x)/*not.*/.to./*deep.*/equals(y)
expect(x).to.be.null
expect(x).to.exist
expect(x).to.be.empty
expect(x).to.have.length(y)
expect(x).to.have./*nested.*/property('model.name').that.equals(z)
expect(x).to.contain/*.keys*/(y)
expect(x).to.be.at.least(y).and.at.most(z)
expect(1).to.satisfy((num) => {return num > 0;})
expect(() => {}).to.throw(/message/)
Liste complète disponible sur la documentation officielle
expect(x).to.be.equal(y)
Quelle que soit l'assertion suivante, not aura pour effet d'attendre son contraire
expect(x).to.be.true
Si le chemin vers la propriété contient des '.' ou des '[]', il faut utiliser deep
expect(x).to.have.property(name, value)
permet de tester l'existance et la valeur. On évitera cette forme, car si value est undefined
, impossible d'être certains que la propriété existe bel et biencontain et include sont strictement synonymes
Les tests sur les exceptions portent sur l'exécution d'une fonction !
describe()
: describe('Array', () => {
let array = []
beforeEach((done) => { // before(), after(), afterEach()
array = [1, 2, 3];
done();
});
it('should splice() remove elements', {skip: true}, (done) => {
expect(array.splice(1, 1)).to.deep.equals([1, 3]);
done();
})
})
it()
only
positionne skip
sur les autres it
du describe()
Ces "hooks" sont liés au describe()
englobant. Ils révèlent le principal intérêt des describe()
: grouper les pré-requis et le nettoyage de plusieurs tests
Il peut y avoir plusieurs fois le même "hook" dans un même describe()
: ils seront exécutés dans l'ordre de déclaration
Attention : ces "hooks" sont exécutés dans la 2ème phase, en même temps que les it()
!
npm init
pour générer un descripteur package.jsonnpm
install --save-dev mocha chai
Il est plus pratique d'installer mocha de manière globale avec npm install -g mocha
dans le contexte de cet exercice, sinon on le mettra en devDependencies du projet
const request = require('request');
request('http://www.google.com', (err, response, body) => {
if (!err && response.statusCode === 200) {
console.log(body);
}
})
Ce module est l'un des plus anciens (mai 2010) et plus utilisé de la communauté NodeJS, car il palie à la faible utilisabilité du client Http par défaut.
Le body est soit une chaîne de caractère, soit du JSON, soit un buffer (binaire) en fonction des différentes options passée lors de la requête.
request({
url: 'http://www.google.com/search',
method: 'GET',
qs: {
q: 'request'
},
proxy: 'http://proxy-internet.localnet:3128'
}, (err, response, body) => {
// ...
})
request.get()
, post()
etc...Les options possibles dépendent du verbe utilisé : l'option body
n'a pas de sens pour un GET.
L'option json
permet d'ajouter les en-têtes content: application/json et accept: application/json, réalise la sérialisation du corps éventuel de la requête et la désérialisation du corps de la réponse.
Pour indiquer que la requête ne DOIT PAS utiliser de proxy, malgré la présence de variable d'environnement, il faut spécifier null
ou false
dans l'option proxy
.
D'une manière générale, il est plus prudent de toujours spécifier l'option proxy, et de mettre vous même la valeur dans un fichier de configuration.
request
/request.get
renvoient un ReadableStream: request('http://google.com/doodle.png').pipe(fs.createWriteStream('doodle.png'));
request.post
/request.put
, un WritableSteam: fs.createReadStream('file.json').pipe(request.put('http://mysite.com/obj.json'));
request.post('http://service.com/upload').form({key:'value'});
request.post({url:'http://service.com/upload',
formData: {
field1: 'my_value',
field3: fs.createReadStream('unicycle.jpg')
}});
Dans ces exemples, on ne gère pas les cas d'erreurs.
Les stream en lecture ont un évènement spécial response lorsque les en-têtes de la réponse sont disponibles. L'évènement error doit être écouté pour gérer les erreurs de réception.
Lorsqu'on envoi un formulaire, le bon content-type est positionné.
L'objet FormData qui modélise le formulaire est accessible en retour de la méthode request.post(...).form();
. Rappelez vous que l'envoi est asynchrone : le formulaire peut être modifié immédiatement après l'appel à post()
.
On peut aussi envoyer un formulaire multipart/related avec l'option multipart
de request.post()
.
Le router permet d'associer un traitement spécifique (une fonction) à une url et un verbe bien particulier.
La requête d'entrée est parsée (JSON) et éventuellement validée avec un middleware.
La constitution des réponses (en-têtes, cache HTTP...) est facilitée, et des middlewares existent pour le contenu statique (fichier, dossiers).
Le mécanisme de templating permet d'utiliser des templates handlebars (par exemple) pour constituer la réponse avec des placeholders.
Une librairie volontairement simple et réduite en terme de fonctionnalité, mais les middlewares permettent d'ajouter des fonctionnalités (gzip, authent...)
La structure en middlewares rend son utilisation simple à comprendre et facile à faire évoluer
const express = require('express');
const app = express();
app.get('/:name', (req, res) => {
res.send('Hello ' + req.params.name);
});
app.listen(3000, () => {
console.log('Example app listening on port 3000!');
});
req
étend http.IncomingMessageres
étend http.ServerResponseLes paramètres de chemins peuvent être facultatif (suffixé par ?
).
req
encapsule l'objet IncomingMessage
de Node, et propose ses propres propriétés comme les paramètres de requêtes extraits (query
), le corps de la requête parsé (body
)
resp
est un objet réponse permettant de modifier les entêtes de réponse (header()
, type()
, le code HTTP (status()
), les cookies (cookie()
), les redirections...
(req, res, next) => {
next();
};
next() n'est pas obligatoire
manipuler les variables req et res
Appeler res.end() ou res.send() pour mettre fin au traitement
Appeler next() pour passer au middleware suivant, ou next(err) pour passer au middleware d'erreur
(err,req, res, next) => {
res.status(500).send('an error occured');
};
app.use((req, res, next) => {
console.log('start request %s %s', req.method, req.originalUrl);
next();
});
app.get('/hello', (req, res, next) => {
res.send('Hello world!')
});
Le path est optionnel pour use(), plusieurs middlewares peuvent être passés en paramètre
Pour créez rapidement son fichier package.json : > npm init
.
Ajouter rapidement des modules externes : > npm install --save(-dev) moduleA moduleB
.
Le fichier source doit exporter le serveur sans le démarrer : c'est le test qui appelera start()
et stop()
.
Pour invoquer une url du serveur depuis les tests, utilisez la librairie request.
Pensez a tester les codes de retour HTTP, et les entêtes importants. La réponse parsée est disponible depuis le test avec res.body
.
Mettre morgan en premier pour bien logguer toutes les requêtes
fs.readdir(source, (err, files) => {
if (err) {
console.log('Error finding files: ' + err)
} else {
files.forEach((filename, fileIndex) => {
console.log(filename)
gm(source + filename).size((err, values) => {
if (err) {
console.log('Error identifying file size: ' + err)
} else {
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach((width, widthIndex) => {
height = Math.round(width / aspect)
console.log('resizing '+filename+'to '+height+'x'+height)
this.resize(width, height).write(destination+'w'+width+'_'+filename,
(err) => {
if (err) console.log('Error writing file: ' + err)
})
}.bind(this))
}
})
})
}
})
async.map(['file1','file2','file3'], (file, next) => {
// appliqué de manière asynchrone sur tous les éléments du tableau
// la signature de next est next(err, result)
fs.readFile(file, next);
}, (err, results) => {
// err est la première erreur renvoyée
// results est le tableau (ordonné) des résultats intermédiaires
});
mapLimit(arr, n, ...)
lance les N première tâches et attend d'en avoir terminé avant d'en relancer d'autres
mapSeries()
lance les tâches les unes à la suite des autres, dans l'ordre. C'est équivalent à mapLimit(arr, 1, ...)
Les autres fonctions :
each()
, eachLimit()
, eachSeries()
qui invoque une fonction asynchrone sur chaque élémentfilter()
, filterSeries()
qui ne conserve que les élements passant le test asynchronereject()
, rejectSeries()
qui conserve les élements ne passant pas le test asynchronereduce()
, reduceRight()
identique à Array.reduce() en mode asynchronesortBy()
qui extrait de manière asynchrone une valeur servant pour le tri qui intervient dans un 2ème tempsdetect()
, detectSeries()
renvoie le premier élément passant le test asynchrone (attention, non ordonné !)some()
renvoie true si au moins un élément passe le test asynchrone (attention, non ordonné !)every()
renvoie true si tous les éléments passe le test asynchroneconcat()
, concatSeries()
renvoie la concaténation des résultats de la fonction asynchrone appliquée à chaque élémentasync.parallel([
(done) => {
// la signature de done est done(err, result)
fs.readFile('in.txt', done);
}, (done) => {
fs.writeFile('out.txt', 'finished !', done);
}
], (err, results) => {
// err est la première erreur renvoyée
// results est le tableau (ordonné) des resultats intermédiaires
});
il est aussi possible de spécifier un objet et pas un tableau :
async.parallel({
one: (done) => {
// la signature de done est done(err, result)
fs.readFile('in.txt', done);
}, two: (done) => {
fs.writeFile('out.txt', 'finished !', done);
}
], (err, results) => {
// err est la première erreur renvoyée
// results reprends les clé de l'objet initial: {one: '', two: ''}
});
Attention néanmoins, il n'y a pas d'ordre garanti...
whilst()
, doWhilst()
: executer une function asynchrone tant que le test synchrone est vraiuntil()
, doUntil()
: executer une function asynchrone jusqu'a ce que le test synchrone soit vraiseq()
, compose()
, waterfall()
: passe les résultats de l'étape N à l'étape N+1queue()
, cargo()
: pool d'exécution asynchronequeue()
, priorityQueue()
permet d'exécuter des tâches en parallèle jusqu'à une limite de concurrence donnée
Ces pools d'exécution tournent en tâche de fond jusqu'à ce qu'on les stoppe
Encore plein d'autres patterns proposés plus ou moins complexes
getDirStat
en utilisant async
new Promise((resolve, reject) => { ... });
resolve()
doit être appelé en cas de succès (accepte en paramètre le résultat du traitement)reject()
doit être appelé en cas d'erreur (accepte en paramètre l'erreur)then()
définit une fonction appelée lors que la promesse est résoluecatch()
définit une fonction appelée lors que la promesse est rejetéeconst request = require('request');
const getHello = () => {
return new Promise((resolve, reject) => {
request('http://google.com/', (err, res) {
if(err) {
reject(err);
return;
}
resolve(res);
});
});
getHello().then((res) => {
console.log(res.statusCode);
})
.catch((err) => {
console.log(err);
})
doSomething().then(function(result) {
// doSomethingElse retourne une promesse
return doSomethingElse(result);
})
.then(function(newResult) {
// doThirdThing retourne une promesse
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);
then()
retourne une promesseasync.series()
Promise.all
async.parallel()
util.promisify()
et util.callbackify()
pour convertir des fonctions "callback-style" en fonction "promise-style" et inversementutil.promisify()
nécessite que le callback soit le dernier paramètre de la fonction
async
est un nouveau mot-clé à placer devant une déclaration de fonctionconst doHello = async (name) => {
if(! name) {
throw 'name is missing';
}
return `Hello ${name}`
};
await
est un nouveau mot-clé à placer devant une promesse
throw
)await
ne peut être utilisé que dans une fonction async
const doHelloTwice = async () => {
const hello1 = await doHello('bob');
const hello2 = await doHello('alice');
return `${hello1}, ${hello2}`;
};
getDirContent
en utilisant async/awaitgetDirStat
en utilisant async/await