Développement d'applications web et mobiles
Compétences visées
- usage intermédiaire de Node.js et Javascript
- compréhension du DOM et usage de son API
- usage intermédiaire de HTML
- usage intermédiaire du CSS
- usage de JSON, du Markdown
- compréhension des protocoles HTTP, REST, Websocket
- usage d’un query-builder (Knex)
- usage du serveur d’applications FeathersJS pour créer un back-end REST ou temps-réel
- usage intermédiaire de VueJS 3 pour créer un front-end
- déploiement sur un serveur virtuel privé (Nginx, PM2)
Environnement de Développement
Visual Studio Code
Visual Studio Code, dit ‘VSCode’, est l’environnement de développement intégré (IDE) de référence :
- utilisable sous Linux, MacOSX, Windows
- shells de commande intégrés
- excellent debugger, à utiliser absolument
- commandes GIT intégrées
- permet de travailler à plusieurs avec le plugin ‘LiveShare’
- écrit entièrement en Typescript / Electron !
Création / ouverture d’un projet avec VSCode
On n’utilisera que des projets contenus dans un répertoire unique (on n’utilisera pas les “espaces de travail”, multi-racines).
- il n’y a pas de commande VSCode pour créer un nouveau projet, il faut créer un répertoire avec une commande shell ou avec l’explorateur de fichiers.
- pour ouvrir un projet existant, il suffit de cliquer sur ‘File –> Ouvrir…’ ou taper ‘Ctrl/Cmd + o’ et sélectionner le répertoire
Configuration du terminal intégré de VSCode
Pour Linux ou MacOSX, il n’y a rien à configurer, c’est le shell par défaut qui est utilisé, généralement bash.
Pour Windows, le terminal PowerShell est directement utilisable. Il est préférable d’installer également un shell bash
à l’aide de Git Bash
:
- Installer Git Bash depuis : https://git-scm.com/download/win
- Ouvrir VSCode et appuyer sur Ctrl + ` pour ouvrir le terminal
- Ouvrir la palette de commande avec Ctrl + Shift + P
- Sélectionner le profil par défaut
- Sélectionner Git Bash depuis les options
- Cliquer sur l’icone + dans la fenêtre du terminal
Le nouveau terminal est alors un terminal Git Bash (le chargement prend quelques secondes)
Chrome et Firefox
Installer Chrome ET Firefox :
- Chrome a une API DOM plus complète
- Firefox permet de mieux inspecter les messages Websocket
- leurs outils de développement s’ouvrent avec
ctrl + F12
pour linux ou windows,F12
pour MacOSX
Javascript & NodeJS
Un moteur javascript est déjà présent dans le navigateur.
Pour utiliser javascript en dehors du navigateur, il faut installer NodeJS :
- pour un système Linux de type Debian (Ubuntu, etc.) :
sudo apt install nodejs
- pour MacOSX et Windows : utiliser l’installeur disponible ici : https://nodejs.org/en/download/
- vérifier la version avec la commande :
node --version
Git & Gitlab
Git est un programme qui permet de gérer une hiérarchie de fichiers situés dans un répertoire, ainsi que leurs différentes versions. Git est décentralisé et la plupart de ses opérations s’effectuent localement. En général on utilise aussi un serveur distant qui permet de sauvegarder un projet git, et de le partager avec d’autres développeurs. On utilisera la plateforme gitlab.com pour héberger nos projets git.
Installation
- MacOSX : git est pré-installé
- Linux/Debian :
sudo apt install git
- Windows : installer
Git Bash
: https://git-scm.com/download/win
Workflow minimal pour git
Création des raccourcis
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.st status
Création d’un projet
- Sur gitlab.com, créer un projet
my-project
- Sur la machine locale, créer un repository synchronisé sur ce projet :
$ git clone <url du projet>
$ cd my-project
Développement d’un nouveau feature
- Créer une branche pour ce feature :
git co -b feature-branch # créé la branche localement
git push -u origin feature-branch # (optionnel) pousse la branche sur le serveur, après l'avoir créé (-u)
- Tant que le travail sur ce feature n’est pas terminé :
...
<travail sur le projet>
...
$ git add file1 file2 ... # ajout des fichiers à commiter
$ git st # on vérifie que la liste (verte) des fichiers à commiter est ok
$ git commit -m "message" # prend une photo locale (un 'commit') du contenu du répertoire projet
$ git push # (optionnel) pousse le commit sur la branche distante
- quand le travail sur le feature est terminé et qu’on souhaite l’intégrer à la branche ‘main’ :
$ git co main # revient à la branche 'main'
$ git merge feature-branch # fusionne dans 'main' tout le travail fait dans 'feature-branch'
$ git push # (optionnel) pousse la branche 'main' sur le serveur
Activités à réaliser
1- VSCode
- installer VSCode
- installer l’extension ‘HTML preview’
- installer l’extension ‘Live Share’
- installer une extension pour VueJS
- fixer à 3 espaces l’indentation des fichiers de type .js, .html
2- Navigateur (Chrome ou Firefox) : installer l’extension des devtools pour VueJS 3
3- NodeJS : l’installer et vérifier que la version est >= 14
4- Git & Gitlab
- créer un compte sur gitlab.com
- ajouter sa clé personnelle SSH
- créer un projet ‘CV’ dans gitlab.com
- cloner le projet localement, et l’ouvrir avec VSCode
- ajouter dans le répertoire projet un fichier texte
cv.txt
- créer une version différente de ce fichier dans une branche
improvements
- pousser les deux branches ‘main’ et ‘improvements’ sur le serveur
Usage minimal du HTML
Pourquoi il est toujours utile de connaître HTML et le CSS :
- indispensable si on utilise pas de toolkit de composants graphiques
- indispensable si on utilise des frameworks CSS ‘utiliy-based’ comme Tailwindcss
- indispensable si on doit créer des web components
Exemple de document HTML :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="description" content="Descriptif de la page">
<title>Titre de la page</title>
</head>
<body>
<!-- je suis un commentaire multiligne;
je ne peux pas contenir de commentaires emboités-->
<h1>Première partie</h1>
<p>Paragraphe pour lequel
les sauts de lignes et les espaces multiples ne comptent
que comme un seul espace
</p>
<br/>
</body>
</html>
- L’intitulé
<!DOCTYPE html>
dénote une page HTML5 - La partie
<head>...</head>
est optionnelle - La partie qui sera affichée est décrite dans le bloc
<body>...</body>
- Les espaces et les sauts de ligne (en dehors des chaînes litérales) sont des séparateurs ; s’ils sont répétés ils ne comptent que pour un
- Les commentaires sont situés dans des blocs
<!-- ... -->
; ils ne peuvent pas être imbriqués
Éléments HTML
On appelle éléments HTML les blocs encadrés par des tags. Il est conseillé de fermer tous les tags, même si ce n’est pas toujours obligatoire en HTML.
Exemples :
<p style="text-size: 2em;">Paragraphe</p>
<br/>
Le contenu d’un élément HTML est la partie située entre le tag ouvrant et le tag fermant. Ce peut être un texte, ou d’autres éléments HTML.
Par exemple, le contenu de <p style="text-size: 2em;">Paragraphe</p>
est le texte "Paragraphe"
.
Un élément HTML sans contenu peut être fermé avec le tag fermant normal, ou de façon abrégée avec un /
en fin de tag ouvrant.
Exemples : <br/>
, <input v-bind='login' />
Les attributs d’un éléments HTML sont placés dans le tag ouvrant sous la forme attribut="valeur"
.
Exemples : <p style="text-size: 2em;">
, <meta charset="utf-8">
À noter :
- les noms de tags et d’attributs sont case-insensitive. Il est conseillé de les écrire en minuscules. Ils doivent être composés uniquement des caractères [a-z], [A-Z], [0-9], sans tirets, même si la plupart des navigateurs acceptent les tirets haut et bas.
- la valeur des attributs doit être mise entre quotes ou entre guillemets, au choix :
name="Abricot"
,name='Abricot'
,name="Blanc d'oeuf"
,name='Une "autorité"'
\
n’est pas un caractère d’échappement
On distingue les éléments HTML de type block qui s’affichent verticalement par défaut (ex : <p>
) et les éléments inline qui s’insèrent dans le flux horizontal de la ligne courante (ex: <a>
)
Principaux éléments HTML de type “block”
Les éléments HTML de type ‘block’ s’affichent verticalement, les uns sous les autres.
Paragraphes, Titres de sections
<p>
indique un nouveau paragraphe de texte; il ne peut contenir que du texte ou des éléments inline<h1>
,<h2>
, …,<h6>
indiquent des titres de paragraphes, de tailles décroissantes. Ils ne peuvent contenir que du texte ou des éléments inline
Séparations verticales
<div>
indique un nouveau bloc, une rupture verticale sans type particulier<br/>
indique une rupture verticale vide (line-break)<hr/>
indique une rupture verticale avec une ligne de séparation horizontale sur toute la largeur du bloc
Listes
Exemple de liste non-numérotée :
<ul>
<li>Rouge</li>
<li>Vert</li>
<li>Bleu</li>
</ul>
Exemple de liste numérotée :
<ol>
<li>Rouge</li>
<li>Vert</li>
<li>Bleu</li>
</ol>
Tables
Exemple de table à 3 colonnes avec une ligne d’en-tête et 2 lignes de données :
<table>
<tr>
<th>Prénom</th>
<th>Nom</th>
<th>Age</th>
</tr>
<tr>
<td>Jean</td>
<td>Bonneau</td>
<td>50</td>
</tr>
<tr>
<td>Paul</td>
<td>Hauchon</td>
<td>37</td>
</tr>
</table>
Images
Exemples :
<img src="https://placekitten.com/300/300" width="500" height="600" />
L’image est désignée de façon absolue par une url. Elle est affichée dans une taille précise, possiblement déformante.
<img src="images/img_123.jpg" alt="description pour malvoyant" height="50%" />
Le fichier est pris dans un répertoire images
relativement au répertoire de la page chargée. Elle est affichée en conservant les proportions, avec une hauteur égale à 50% de la hauteur de son parent.
Formulaires
Exemple de formulaire incluant les types d’input les plus communs :
<form action="/submit_identity" method="POST">
Prénom :<br/>
<input type="text" name="firstname" placeholder="Entrez votre prénom"><br/>
Nom de famille :<br/>
<input type="text" name="lastname" placeholder="Entrez votre nom"><br/>
Mot de passe :<br/>
<input type="password" name="passwd"><br/>
Genre :<br/>
<input type="radio" name="gender" value="male" checked>Masculin<br/>
<input type="radio" name="gender" value="female">Féminin<br/>
Couleur préférée :<br/>
<select name="color">
<option value="red">Rouge</option>
<option value="green">Vert</option>
<option value="blue">Bleu</option>
</select>
Participation :<br/>
<input type="checkbox" name="participation" value="yes">Je souhaite participer<br/>
Remarques :<br/>
<textarea name="remarks" rows="10" cols="30">Texte initial</textarea>
<input type="hidden" name="country" value="France">
<input type="submit" value="Envoyer">
</form>
Un clic sur le bouton ‘Envoyer’ provoquera un POST à l’url ‘/submit_identity’ avec les données contenues dans les champs saisis.
Principaux éléments HTML de type “inline”
Les éléments HTML de type ‘inline’ s’insèrent dans le flux horizontalement sans commencer sur une nouvelle ligne.
Liens hypertexte
<a>
est utilisé pour introduire des liens hypertexte; par exemple :
<a href="https://gitlab.enseeiht.fr">GitLab ENSEEIHT</a>
Si on souhaite ouvrir un nouvel onglet lors du clic, on ajoute l’attribut target="_blank"
:
<a href="https://gitlab.enseeiht.fr" target="_blank">GitLab ENSEEIHT</a>
N’importe quel élément html peut être utilisé comme lien, par exemple une image :
<a href="/home">
<img src="home.gif" alt="Accueil">
</a>
span
<span>
ne représente rien mais permet de grouper des éléments inline, par exemple pour les styler de façon particulière :
<span class="emphatic">texte à styler</span>
Mise en value du texte
Il est possible d’appliquer certaines marques de style sans utiliser de CSS : <b>
(bold), <i>
(italique), <strong>
(important), <em>
(emphase), <small>
(petit), <del>
(barré), <sub>
(en indice), <sup>
(en exposant), <mark>
(surligné)
Ancres (bookmarks)
une ancre peut être attachée à un élément avec l’attribut
id
, par exemple :<h2 id="CH1">Chapter 1- Variation Under Domestication</h2>
pour qu’un lien cible cette ancre, on utilise le caractère
#
dans l’url, par exemple :<a href="contents.html#CH1">Chapter 1</h2>
Caractères spéciaux
HTML Entities
- espace insécable :
<p>Vive les mariés !</p>
<
:<
>
:>
&
:&
"
:"
'
:'
¢
:¢
£
:£
¥
:¥
€
:€
©
:©
®
:®
Caractères Unicode par codepoint
Exemple de caractère chinois avec codepoint U+54F4 : 哴
<p>哴</p>
Emoji
Les emojis sont des caractères unicodes comme les autres, avec des représentations qui varient selon le dispositif d’affichage. Ils sont des plus en plus nombreux et variés, et peuvent être copiés/collés avec un éditeur de texte comme VSCode.
Par exemple : ❌, 🌐, ⇨, 🇱🇧
Vidéo
<video src="palladia.mp4" width="400" height="800" loop muted autoplay controls></video>
Avec un poster :
<video src="palladia.mp4" poster="palladia.jpg" controls></video>
Fallback content
On peut mettre dans le corps de <video>
:
- un élément de ‘fallback’ si la vidéo ne peut pas être jouée
- des alternatives de sources vidéos, qui sont essayées dans l’ordre d’ajout
<video controls>
<source src="palladia.mp4" type="video/mp4; codecs='avc1, mp4a'">
<source src="palladia.webm" type="video/webm; codecs='vp8.0, vorbis'">
<source src="palladia.mov" type="video/quicktime">
<p>Browser no likey HTML 5.</p>
</video>
Audio
Fonctionne essentiellement comme la vidéo :
<audio controls>
<source src="homer.wav" type="audio/wav">
<source src="homer.mp3" type="audio/mpeg">
<source src="myAudhomerio.ogg" type="audio/ogg">
<p>Votre navigateur ne prend pas en charge l'audio HTML5.</p>
</audio>
Activités à réaliser
- cloner le projet gitlab https://gitlab.com/buisson31/fs-html-css
- ajouter au projet un fichier
index.html
dont la structure sera celle de l’image qui suit - utiliser les images du répertoire
assets
; ne pas s’occuper des couleurs, ni des tailles, ni de la mise en page - commiter le travail et le pousser vers gitlab.com

SVG : Scalable Vector graphics
Le langage SVG permet de décrire des objets graphiques vectoriels ; c’est par exemple un des formats de sortie d’applications comme Illustrator ou Inkscape.
SVG ne fait pas partie au sens strict de la spécification HTML, mais il peut être inclus directement dans des documents HTML5.
Éléments SVG :
- si nécessaire ils peuvent être mis à l’échelle en gardant une définition parfaite
- ils servent souvent pour la représentation des symboles
- ils peuvent être stylés avec du CSS
- on peut leur attacher des écouteurs d’événements
En un mot, tout se passe comme si les balises <svg>
, <path>
, etc. faisaient partie des éléments HTML.
Exemple
<!DOCTYPE html>
<html lang="fr">
<body>
<h1>SVG experiments</h1>
<svg width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
</svg>
<svg width="17" height="16">
<path d="M8.500,0.000 L11.301,5.028 L16.999,6.112 L13.033,10.302 L13.753,16.000 L8.500,13.561 L3.247,16.000 L3.967,10.302 L0.001,6.112 L5.699,5.028 L8.500,0.000" />
</svg>
</body>
</html>
Usage minimal du CSS
Principe général
Le CSS est un ensemble de règles qui s’appliquent à un document HTML.
Par exemple pour que le texte du document soit bleu, on peut employer la règle suivante :
html {
color: blue;
}
- la règle s’applique à l’élément html de tag
<html>
- l’attribut de couleur de texte
color
est affecté àblue
, et cet attribut est hérité par tous les sous-éléments de<html>
Emplacement du CSS
Les règles CSS peuvent être placées à différents endroits :
directement sur l’élément html à styler, dans l’attribut
style
, par exemple :<h1 style="color: red;">Usage minimal du CSS</h1>
dans un fichier séparé, qu’on charge à l’aide de la directive html
link
:<!doctype html> <html> <head> <link rel="stylesheet" href="./styles.css" /> </head> <body> <h1>Usage minimal du CSS</h1> </body> </html>
styles.css
html {
color: blue;
}
à l’intérieur d’une section
<style>
qu’on peut placer dans la section<head>
du document html, ou dans le<body>
, ou à la fin :<!doctype html> <html> <body> <h1>Usage minimal du CSS</h1> </body> </html> <style> html { color: red; } </style>
Structure d’une règle CSS
Une règle CSS a la forme générale suivante :
sélecteur {
propriété: valeur[unité];
}
/* Commentaire
multi-lignes
*/
Exemple :
h1 {
color: green;
font-size: 48px;
border-bottom: 1px solid;
}
Propriétés et valeurs des règles CSS
Valeurs numériques
{
width: 300px;
height: 2em;
font-size: 200%;
line-height: 1.2;
}
Unités
Absolues
px
: pixels, pour les supports écran ; 1px = 1/96e de 1inpt
: points, pour les supports imprimés ; 1pt = 1/72e de 1in
Relatives
em
, taille de fonte de l’élément parentrem
(root em), taille de fonte de l’élément racine du document%
, relative à l’élément parent, sa taille de fonte ou ses dimensionsvw
, 1% de la largeur du viewportvh
, 1% de la hauteur du viewport
Chemin ou URL
{
background-image: url('chemin/local/vers/image');
}
{
background-image: url('https://exemple.com/lien/vers/image');
}
Série de valeurs
{
border: 1px solid red;
}
Équivaut à
{
border-width: 1px;
border-style: solid;
border-color: red;
}
Couleurs : texte, fond, bordure
{
color: #446655;
background-color: #F0F7F3;
border-color: red;
}
Notation des couleurs
- RGB en hexadécimal :
#CCCCCC
, forme simplidiée :#CCC
- RGB en décimal :
rgb(128, 0, 128)
- HSL (Hue, Saturation, Luminosity) :
hsl(300, 100%, 25%)
Les sélecteurs
Sélecteurs de balise
h1 { color: red; }
Sélecteurs de classe
<h2 class="title article">Les sélecteurs</h1>
.title { color: blue; } /* cible tous les éléments ayant pour classe `title` */
h1.title { color: blue; } /* cible les élément de type `h1` ayant pour classe `title` */
Sélecteurs d’id
<h1 class="title" id="main">Les CSS</h1>
#main { color: green; }
L’identifiant ne doit apparaître qu’une fois dans le document HTML (la page)
Hiérarchie des sélecteurs
Sélecteur de classe > Sélecteur de balise > Sélecteur d’id
Combinaisons de sélecteurs
Union de sélecteurs
h1, h2 {
... /* cible les <h1> et les <h2> */
}
p, .para, #chapo {
... /* cible les éléments <p> et les éléments de classe "para" et l'élément d'identifiant 'chapo' */
}
Sélecteur en cascade
.header h1 {
... /* cible les éléments <h1> qui se situent à un ou plusieurs niveaux sous un élément de classe 'header' */
}
Sélecteur suivant
h1 + p {
... /* cible les éléments <p> qui se situent au même niveau qu'un élément <h1>, juste après */
}
Sélecteur enfant
ul li {
... /* cible les éléments <li> sous un élément <ul>, directement ou non */
}
Sélecteur enfant direct
ul > li {
... /* cible les éléments <li> directement sous un élément <ul> */
}
Sélecteur d’attribut
input[disabled] /* sélectionne tous les <input> ayant un attribut boolean 'disabled' */
input[type="checkbox"] /* sélectionne tous les <input> ayant un attribut 'type' égal à 'checkbox' */
Pseudo-classes
:root
:root
est une pseudo-classe qui désigne la racine du document. À préférer à <html>
qui n’est pas toujours présent.
:hover
, :focus
, :active
:hover
, :focus
et :active
s’appliquent à la plupart des éléments html
.btn:hover {
text-decoration: none; /* s'applique au survol */
}
a:focus {
border: 1px solid red; /* s'applique quand l'élément a le focus */
}
.btn:active {
background-color red; /* s'applique quand l'élément est activé (clic souris, touche tabulation) */
}
:visited
a:visited {
color: #666; /* s'applique aux liens <a> déjà visités */
}
Variables CSS (custom properties)
Déclaration
:root {
--main-bg-color: #261194;
}
Utilisation
element {
background-color: var(--main-bg-color);
}
Activités à réaliser
Créer une nouvelle branche colors
et introduire les couleurs dans le texte, en utilisant des variables CSS.
Merger colors
dans main
quand le travail est terminé
Formater du texte
Polices de caractère, familles de polices
Une famille de police regroupe un ensemble de polices de caractères analogues, dans des variantes de style : niveau d’épaisseur, italic, oblique notamment.
On peut distinguer plusieurs groupes :
serif
: avec détails aux extrémitéssans-serif
: pas de détails aux extrémitésmonospace
: tous les caractères occupent la même largeur, idéal pour afficher du codecursive
etfantasy
, peu utilisées

Les ‘serifs’ sont les petits détails situés aux extrémités des caractères.
CSS
:root {
font-family: Arial;
font-size: 16px;
font-style: italic; /* normal | italic | oblique */
font-weight: bold; /* light | normal | medium | bold | 100-900 */
}
‘web-safe’ fonts
Ce sont les polices de caractères (fontes) généralement incluses dans tous les navigateurs, dont le rendu est identique.
- Arial, Verdana pour les sans-serif
- Georgie et Times New Roman pour les serif
- Courrier New pour monospace
Arial
est la fonte la plus utilisée sur l’ensemble des sites web (version Microsoft de ‘Helvetica’)
On peut préciser des fontes de repli en cas d’indisponibilité :
{
font-family: Roboto, Arial, sans-serif;
}
Google fonts
Google propose un espace de fontes open-source : https://fonts.google.com/
Roboto
est une de ces fontes, très utilisée
Casse et décoration du texte
N’écrivez pas en majuscules, tranformez en CSS !
{
text-transform: uppercase /* capitalize | lowercase | none */;
text-decoration: underline /* overline | line-through | none */;
}
Alignement et hauteur du texte
{
text-align: justify /* left | center | right */;
line-height: 1.25 /* 125% | 1.25em | 20px */;
}
Bonne pratique
Définir la taille de référence pour le <body>
, puis définir les autres tailles en em
ou rem
Activités à réaliser
Créer une nouvelle branche fonts
; trouver et introduire la famille de fonte la plus proche.
Merger fonts
dans main
quand le travail est terminé
Styles des blocs
Attributs de taille
width
height
min-width
max-width
Modèle de boîte

Modèle standard
{
box-sizing: border-content;
}
Largeur totale = width + padding + border left et right
Hauteur totale = height + padding + border top et bottom
Modèle alternatif
{
box-sizing: border-box;
}
Largeur / hauteur totale = largeur / hauteur définie (padding inclus)
Marges et espacement
{
margin: 10px;
padding: 10px;
}
Les marges sont sans effet sur les éléments inline
(strong
ou a
par exemple)
Par côté
{
margin-bottom: 10px;
padding-left: 10px;
}
Combiné
{
margin: <top> <right> <bottom> <left>;
margin: <haut et bas> <côtés>;
}
Exemple
{
margin: 20px 5px 10px 5px;
padding: 20px 0;
}
Bordures
{
border: 1px solid red;
border: <width> <style> <couleur>;
}
Un seul côté
{
border-bottom: 1px solid red;
}
Largeur, style et couleur
{
border-width: 1px;
border-style: solid;
border-color: red;
}
Annuler une bordure
{
border: none;
border-right: none;
}
Block arrondi
{
border-radius: 20px;
}

Définir un fond
{
background-color: #CCC;
background-image: url('lien/vers/image/');
background-repeat: repeat; /* no-repeat */
background-position: top left; /* center | <value px> | <%> */
background-size: 200px 100%; /* cover | contains */
}
cover
: l’image occupe, “couvre” tout l’espace disponiblecontains
: l’image est entièrement visible, “contenue” dans l’espace disponible
L’attribut position
position: absolu;
{
position: absolute;
top: 40px; left: 40px;
}
- l’élément sort du flux
- sa position est relative à l’élément parent le plus proche positionné en relatif, ou au document par défaut

position: relative;
- l’élément reste dans le flux
- sa position est relative au document ou à l’élément parent le plus proche positionné en relatif
{
position: relative;
top: 40px; left: 40px;
}

position: sticky;
- l’élément sort du flux
- il conserve la même position, même en cas de scrolling
{
position: sticky;
top: 20px;
}

L’attribut : display
- display: block; /* indique que l’élément s’insère dans le flux vertical)
- display: inline; /* indique que l’élément s’insère dans le flux horizontal)
- display: none; /* l’élément et tous ses descendants sont ignorés */
On peut manipuler la présence d’un élément en agissant sur la valeur de son attribut display
.
Flexbox
Voir : https://css-tricks.com/snippets/css/a-guide-to-flexbox/
CSS responsive
Media-queries
@media screen and (min-width: 768px) and (orientation: portrait) {
/* les styles pour cette configuration */
}
- Orientation (
portrait
oulandscape
) et Localisation - Device api (
screen
,print
,tv
)
En utilisant display: non;
on peut faire disparaitre des éléments pour certains type de devices ou de tailles/modes d’écran, par exemple sur mobile, ou lors de l’impression
Approche mobile first
Par exemple, pour qu’une sidebar ne soit visible que sur grands écrans :
sidebar {
display: none;
}
@media (min-width: 700px) {
sidebar {
display: block;
}
}
Activités à réaliser
Créer une nouvelle branche responsive
; ajouter les directives @media qui provoquent le changement d’affichage de l’image suivante à partir de 700px de largeur.
Merger responsive
dans main
quand le travail est terminé.

CSS utility classes : Tailwind CSS
Une approche récente consiste à utiliser des frameworks CSS tels que Tailwind CSS qui exposent autant de classes CSS prédéfinies que de couples attribut/valeur, de triplets taille d’écran/attribut/valeur, etc.
Par exemple pour les marges :
m-0 margin: 0px;
m-px margin: 1px;
m-0.5 margin: 0.125rem;
m-1 margin: 0.25rem;
m-1.5 margin: 0.375rem;
m-2 margin: 0.5rem;
m-2.5 margin: 0.625rem;
m-3 margin: 0.75rem;
...
Pour styler un élément, on lui ajoute un attribut class
avec une suite de utility classes
qui réalisent l’effet voulu.
Par exemple, pour ajouter une marge autour d’un élément <div>
:
<div class="m-2">
...
</div>
Pour afficher un texte en rouge, large sur mobile, très large à partir de la taille medium, avec troncature en cas de débordement du texte :
<p class="md:text-2xl text-lg text-center text-red-500 truncate">
...
</p>
Installation
En général on préférera utiliser tailwind incorporé à processus de build (ex: webpack) qui fera un tree-shake des styles inutilisés.
Pour une installation rapide, on peut utiliser un CDN, par exemple en ajoutant <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
dans le document html.
Plus d’informations : https://tailwindcss.com/
Activités à réaliser
Créer une nouvelle branche tailwind
; remplacer tout le styling par des classes Tailwind.
Merger tailwind
dans main
quand le travail est terminé.
Javascript et NodeJS
Javascript est un langage de programmation, utilisable dans tous les navigateurs web sous différentes versions.
NodeJS est un environnement d’exécution Javascript, généralement utilisé côté serveur, basé sur le moteur Javascript V8 de Google.
Installation de node et de npm
Installation sur tout le système
Debian
$ curl -sL https://deb.nodesource.com/setup_14.x -o nodesource_setup.sh
$ sudo bash nodesource_setup.sh
$ sudo apt-get install -y nodejs
$ which node
/usr/bin/node
$ node
> 1 + 1 // expression à évaluer
2 // affichage de la valeur
> Ctrl-d # fermeture du flux stdin -> exit
MacOsX
Utiliser l’installer graphique du site officiel
Installation locale par nvm
npm
est un package-manager qui simplifie la distribution, le partage et l’installation de NodeJS.
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash # téléchargement du script d'install + exécution
$ nvm install 11 # demande à nvm d'installer la version 11.x de node la plus récente
$ nvm use 11 # pas nécessaire après une installation
$ which node
/home/chris/.nvm/versions/node/v8.10.0/bin/node # la version 8.10.0 est installée localement pour le user 'chris'
Quel feature avec quelle version ?
Création d’un projet node
$ mkdir myproject ; cd myproject
$ npm init # création du fichier `package.json` qui définit les dépendances
... répondre aux questions ...
Installation de packages npm
Installation globale d’un package
$ npm install <package_name> -g
Installation locale (pour le projet courant) d’un package et sauvegarde de la dépendance dans ./package.json
. Les fichiers sont copiés dans le répertoire ./node_modules/<package_name>
$ npm install <package_name>
Installation de tous les packages référencés dans package.json
:
$ npm install
Console
$ node
> console.log("Hello!") // écriture sur la sortie standard stdout
Hello!
> console.log("Hello", 123) // écriture multiple sur la même ligne
Hello 123
> console.error("Error") // écriture sur la sortie erreur standard stderr
Error
Types de données primitifs (built-in)
Nombres, entiers ou réels
$ node
> 123
123
> .123
0.123
> typeof(123)
'number'
> parseInt("123") // conversion d'une chaîne en nombre entier
123
> parseInt("az")
NaN
Booléens : true
/ false
$ node
> true
true
> false
false
> typeof(true)
'boolean'
Chaînes de caractères
$ node
> "C'est l'été"
'C\'est l\'été'
> 'Il est "lent"'
'Il est "lent"'
> typeof("aze")
'string'
En utilisant des backquotes, les chaines peuvent contenir des retours à la ligne et des placeholders :
$ node
> let item = { name: "bananes", count: 2 }
undefined
> `Item: ${item.count} ${item.name}`
Item: 2 bananes
Les opérateurs <
et >
permettent de comparer les chaines :
$ node
> "aze" < "qsd"
true
> "aze" === "aze"
true
null & undefined
$ node
> z
ReferenceError: z is not defined
> let z ; z
undefined
> let z = null ; z
null
Objets
C’est la structure de données reine en JS. Un objet est un ensemble de paires clés/valeur.
Les clés sont toujours des chaines ; les valeurs peuvent être quelconques. Il n’y a pas d’ordre entre les paires clé/valeur.
$ node
> let dict = {a: 1, b: 2} ; dict
{ a: 1, b: 2 }
> dict['b'] // accès à la valeur associée à la propriété
2
> let key = 'a' ; dict[key]
1
> dict.a // syntaxe raccourcie
2
> dict.a = 11
11
> dict
{ a: 11, b: 2 }
> typeof(dict)
'object'
> Object.entries(dict)
[ [ 'a', 11 ], [ 'b', 2 ] ]
> dict[[1, 2, {x:1}]] = 234
234
> dict
{ a: 11, b: 2, '1,2,[object Object]': 234 }
Écriture raccourcie (ES6) :
$ node
> let a = 1, b = 2
undefined
> { a, b }
{ a: 1, b: 2 }
Héritage des objets par prototypes
Un objet est une instance du type de base Object
:
$ node
> x = new Object()
{}
> x.a = 1 ; x
{ a: 1 }
Javascript est un langage objet qui utilise un héritage par prototype: lorsque obj.prop
est évalué, la clé prop
est recherchée d’abord dans obj
, puis dans le type père obj.prototype
(s’il existe), puis à défaut dans obj.prototype.prototype, etc. jusqu’à Object
Lors d’un appel à new Type(args)
:
- un nouvel objet est créé par clonage de Type.prototype. Il possèdera donc déjà toutes les propriétés et méthodes de son prototype, l’équivalent d’une classe ‘père’ dont il hérite
- La fonction constructrice Type est appelée avec les arguments fournis,
this
étant lié au nouvel objet créé. - L’objet renvoyé par le constructeur devient le résultat de l’expression qui contient
new
.
On peut étendre un type d’objet existant avec des propriétés ou des méthodes supplémentaires, en les ajoutant à son prototype :
> Array.prototype.toString = function() {
> return `tableau, taille ${this.length}`
> }
> [1, 2].toString()
'tableau, taille 2'
Le type Object
possède des méthodes statiques très utiles, notamment :
Object.keys(obj)
: renvoie la liste des clés des propriétés propres deobj
(sans ordre garanti)Object.values(obj)
: renvoie la liste des valeurs des propriétés propres deobj
(sans ordre garanti)
L’expression booléenne x instanceof t
permet de déterminer si x
est du type t
, par exemple myarray instanceof Array
.
Tableaux
Un tableau est un object
dont les propriétés sont des nombres entiers (convertis en chaines, en base 10), et qui possèdent une propriété length
$ node
> let myarray = ["a", "b"] ; myarray
[ 'a', 'b' ]
> myarray[1]
'b'
> myarray.length // taille du tableau
2
> typeof(myarray)
'object'
> myarray instanceof Array
true
> Object.keys(myarray)
['0', '1']
On peut créer des tableaux creux :
$ node
> tab = []
[]
> tab[1000] = 1 ; tab
[ <1000 empty items>, 1 ]
> tab.length
1001
> Object.keys(tab)
[ '1000' ]
Un tableau est en fait un objet du type Array
, qui possède les constructeurs, propriétés et méthodes suivantes :
new Array(n)
: créé un tableau avecn
emplacements vides, c’est à dire avec la propriétélength
àn
- propriété
length
: quand un élément est ajouté au tableau à la positioni
,length
prend la valeuri+1
sii >= length
- méthode
includes(elt)
: renvoie un booleéen indiquant sielt
est inclus dans le tableau - méthode
indexOf()
: retourne le premier (plus petit) index d’un élément égal à la valeur passée en paramètre à l’intérieur du tableau, ou -1 si aucun n’a été trouvé - méthode
join(sep)
: concatène tous les éléments d’un tableau en une chaîne de caractères, les éléments étant séparés parsep
- méthode
slice(start, end)
: renvoie un objet tableau, contenant une copie superficielle de la portion du tableau original entre l’indicestart
(inclus) et l’indiceend
(exclu). Le tableau original n’est pas modifié - méthode
fill(val)
: remplit tous les éléments du tableau avec la valeurval
- méthode
pop()
: supprime le dernier élément d’un tableau et retourne cet élément - méthode
push(val[, val, ...])
: ajoute un ou plusieurs éléments à la fin d’un tableau et retourne la nouvelle longueur du tableau - méthode
shift()
: supprime le premier élément d’un tableau et retourne cet élément - méthode
sort(func)
: trie sur place le tableau à l’aide de la fonction de comparaisonfunc
- méthode
splice(i, n)
: supprime sur placen
éléments dans le tableau à partir de la positioni
- méthode
concat(a1[, a2,...])
: renvoie un nouveau tableau constitué de ce tableau concaténé aveca1
,a2
, etc. - méthodes
map
,reduce
,filter
,find
, ’forEach,
some,
every` qui permettent une programmation fonctionnelle (voir plus loin)
Fonctions
Une fonction Javascript est un objet héritant du type prédéfini Function
:
$ node
> parseInt
[Function: parseInt]
> parseInt instanceof Object
true
> parseInt instanceof Function
true
Les fonctions se manipulent comme les autres types d’objets : elles peuvent être affectées à des variables, passées en argument d’autres fonctions, etc.
$ node
> function square(x) { return x*x } // 'square' est une fonction définie globalement
undefined
> square(3)
9
> let cube = function(x) { return x*x*x } // 'cube' est une variable locale, ayant pour valeur une fonction
undefined
> cube
[Function: cube]
> typeof(cube)
'function'
> cube(3)
27
Écriture ‘fat arrow’ (depuis ES6) :
> let square = (x => x*x)
undefined
> typeof(square)
'function'
> square(2)
4
> let printSquare = (x => {
> console.log("Le carré de ", x, "est", x*x)
> })
> printSquare(2)
Le carré de 2 est 4
Dans le corps d’une arrow function, this
est le dictionnaire de son scope environnant lexical
Une fonction déclarée avec le mot-clé function
est remontée dans le code (‘hoisting’) : cela permet de l’utiliser avant qu’elle ait été déclarée dans le code. Ce n’est pas le cas des fonctions anonymes affectées à des variables, dont la portée suit les règles habituelles.
Lors d’un appel de fonction :
- si le nombre d’arguments est supérieur au nombre de paramètres, les arguments supplémentaires sont ignorés
- si le nombre d’arguments est inférieur au nombre de paramètres, les paramètres manquants prennent la valeur
undefined
, sauf si une valeur par défaut a été prévue. Exemple :
function f(a, b=2) {
console.log('a', a, 'b', b)
}
f(3, 4) // --> a 3 b 4
f(3) // --> a 3 b 2
Type String
La chaine de caractères est un type primitif en javascript. Lorsqu’une instruction accède à des propriétés (telle que .length
) et des méthodes (telles que .substring()
) sur une chaine, celle-ci est temporairement convertie en un object
de type String
qui possède ces méthodes et propriétés. Après usage cet objet est candidat au garbage-collecting.
length
: propriété, longueur de la chaine. Ne pas modifier.charAt()
: Renvoie le caractère (ou plus précisement, le point de code UTF-16) à la position spécifiée.concat()
: Combine le texte de deux chaînes et renvoie une nouvelle chaîne.includes()
: Défini si une chaîne de caractères est contenue dans une autre chaîne de caractères.endsWith()
: Défini si une chaîne de caractère se termine par une chaîne de caractères spécifique.indexOf()
: Renvoie la position, au sein de l’objet String appelant, de la première occurrence de la valeur spécifiée, ou -1 si celle-ci n’est pas trouvée.lastIndexOf()
: Renvoie la position, au sein de l’objet String appelant, de la dernière occurrence de la valeur spécifiée, ou -1 si celle-ci n’est pas trouvée.localeCompare()
: Renvoie un nombre indiquant si une chaîne de référence vient avant, après ou est en position identique à la chaîne donnée selon un ordre de tri.match()
: Utilisée pour faire correspondre une expression rationnelle avec une chaîne.matchAll()
: Renvoie un itérateur listant l’ensemble des correspondances d’une expression rationnelle avec la chaîne.padEnd()
: Complète la chaîne courante avec une autre chaîne de caractères, éventuellement répétée, afin d’obtenir une nouvelle chaîne de la longueur indiquée. La chaîne complémentaire est ajoutée à la fin.padStart()
: Complète la chaîne courante avec une autre chaîne de caractères, éventuellement répétée, afin d’obtenir une nouvelle chaîne de la longueur indiquée. La chaîne complémentaire est ajoutée au début.repeat()
: Renvoie une chaîne dont le contenu est la chaîne courante répétée un certain nombre de fois.replace()
: Utilisée pour rechercher une correspondance entre une expression rationnelle et une chaîne, et pour remplacer la sous-chaîne correspondante par une nouvelle chaîne.search()
: Exécute la recherche d’une correspondance entre une expression régulière et une chaîne spécifiée.slice()
: Extrait une section d’une chaîne et renvoie une nouvelle chaîne.split()
: Sépare un objet String en un tableau de chaînes en séparant la chaîne en plusieurs sous-chaînes.startsWith()
: Détermine si une chaîne commence avec les caractères d’une autre chaîne.substr()
: Renvoie les caractères d’une chaîne à partir de la position spécifiée et pour la longueur spécifiée.substring()
: Renvoie les caractères d’une chaîne entre deux positions dans celle-ci.toLowerCase()
: Renvoie la valeur de la chaîne appelante convertie en minuscules.toUpperCase()
: Renvoie la valeur de la chaîne appelante convertie en majuscules.trim()
: Retire les blancs en début et en fin de chaîne.trimStart()
: Retire les blancs situés au début de la chaîne.trimEnd()
: Retire les blancs situés à la fin de la chaîne.
Programmation objet basée sur des prototypes
Ce qui est présenté dans cette section est un usage ancien qui ne doit plus être utilisé dans de nouveaux projets ; on préferera la nouvelle syntaxe ES6 utilisant le mot-clé class
Exemple :
$ node
> function Rectangle(width, height) {
> this.width = width
> this.height = height
> this.area = function() {
> return this.width * this.height
> }
> }
> const rectangle = new Rectangle(100, 50)
undefined
> rectangle
Rectangle { width: 100, height: 50 }
> rectangle.width
100
> rectangle.area()
5000
> console.log(rectangle instanceof Rectangle)
true
Classes (depuis ES6)
Elles ont été introduites dans la syntaxe, mais restent du sucre syntaxique par dessus l’organisation objet basée sur des proptotypes.
Exemple:
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
get area() {
return this.calcArea();
}
calcArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
constructor(size) {
super(size, size)
}
}
let square = new Square(100)
console.log('area', square.area)
Les déclarations de classes ne sont pas remontées dans le code comme les déclaration de fonctions.
Déclaration et portée des variables
const
Une variable déclarée const
ne peut être modifiée ultérieurement :
$ node
> const x = 1
undefined
> x = 2
TypeError: Assignment to constant variable.
Si la valeur de la variable const
est un objet, son contenu peut malgré tout être modifié…
let
Une variable déclarée let
peut être modifiée ultérieurement :
> let y = 2
undefined
> y = 3
3
Une variable déclarée let
a une portée limitée au bloc {}
auquel elle appartient :
> let z = 4; { let z = 5; console.log('inner z', z) } console.log('outer z', z)
inner z 5
outer z 4
undefined
> { let inv = function(x) { return 1/x } ; console.log(inv(2)) } console.log(inv(3))
0.5
ReferenceError: inv is not defined
var
Ne jamais l’utiliser !
Variables globales
Les variables sans déclaration sont accessibles globalement ; éviter de les utiliser :
> g1 = 123
undefined
g1
123
Expressions booléennes
$ node
> 3 === 2 + 1 // test d'égalité des types et des valeurs
true
> 3 == "3" // test d'égalité faible
true
> 3 === "3"
false
> let x = {a:1,b:2}, y = {a:1,b:2}
undefined
> x === y // test d'égalité des *adresses* des objets : elles sont différentes
false // même si les contenus sont identiques
! expression
: ‘notexpression
’expression1 && expression2
:expression1
andexpression2
expression1 || expression2
:expression1
orexpression2
truthy & falsy
$ node
> [0, 1, null, "", [], {}].map(x => console.log(x, x ? "is truthy" : "is falsy"))
0 'is falsy'
1 'is truthy'
null 'is falsy'
is falsy
[] 'is truthy'
{} 'is truthy'
[ undefined, undefined, undefined, undefined, undefined, undefined ]
Itérations avec for
Itération avec for .. in
$ node
> let object = {a: 1, b: 2, c: 3}
> for (property in object) console.log(property, object[property])
a 1
b 2
c 3
undefined
Attention, un tableau est un object
, donc :
$ node
> for (let x in ['a', 'b', 'c']) console.log(x, typeof(x))
0, string
1, string
2, string
undefined
L’itération ne porte pas sur les cases vides :
$ node
> let tab = []
undefined
>
> tab[0] = 1
1
> tab[10] = 2
2
> for (let i in tab) { console.log(i, tab[i]) }
0 1
10 2
undefined
Itération avec for .. of
(depuis ES7)
$ node
> for (let element of [3, 4, 5]) console.log(element)
3
4
5
undefined
>
> tab = []; tab[2] = 123; for (let i of [3, 4, 5]) console.log(i)
undefined
undefined
123
undefined
try / catch / finally
function isValidJSON(txt){
try {
JSON.parse(txt);
return true;
} catch {
return false;
}
}
openFile()
try {
readFile()
} catch(error) {
console.err(error)
} finally {
closeFile()
}
Problématique des modules
Les modules permettent de diviser un programme en différentes parties. Un module exporte des fonctions, classes etc. et peut importer les fonctions, classes etc. d’autres modules, appelés dépendances. Il existe différents formats de modules, incompatibles les uns avec les autres, mais c’est le format le plus récent ESM (Ecma Script Module) qui est amené à les remplacer tous.
Modules CJS (CommonJS)
Il s’agit du format de module historique de NodeJS, utilisé notamment dans les packages npm.
On utilise la fonction require
pour importer un module.
Module externe npm
$ npm install express
$ node
> const express = require('express')
Module sous forme d’un fichier
$ cat math.js
module.exports = {
pi: 3.14159,
e: 2.71828,
square: function(x) { return x*x }
}
$ node
> const math = require('./math') // ou './math.js'
undefined
> math.e
2.71828
> math.square(5)
25
Module sous forme d’un répertoire
Le répertoire doit contenir un fichier index.js
$ cat math/index.js
module.exports = {
pi: 3.14159,
e: 2.71828,
square: function(x) { return x*x }
}
$ node
> require('./math')
{ pi: 3.14159, e: 2.71828, square: [Function: square] }
Variables visibles dans le scope d’un module
__dirname
: chemin d’accès absolu au répertoire du module courantmodule
: référence au module courantmodule.exports
: définit ce qu’un module exporte, accessible parrequire()
Modules ESM
C’est le format de modules utilisé dans les navigateurs récents ; il devrait à terme remplacer tous les autres. Il est déjà compatible avec NodeJS version 14. Les modules ESM sont généralement stockés dans des fichiers d’extension .mjs
.
On utilise le mot-clé import
pour importer un module.
exportations multiples
$ cat math.mjs
export const pi = 3.14159
export function square(x) { return x*x }
$ cat main.js
...
import { pi, square } from './math.mjs'
...
exportation ‘default’
$ cat math.mjs
export default {
pi: 3.14159,
square: (x => x*x)
}
$ cat main.js
...
import math from './math.mjs'
...
let PI = math.pi
...
Promesses
Une promesse est un object renvoyé par une opération asynchrone (ex : requête HTTP) et auquel sont attachés des callback, lors des événements :
- de complétion de l’opération, avec renvoi d’une valeur : callback
.then(rep => ...; return value)
- d’échec de l’opération : callback
.catch(err => ...)
- de fin de l’opération, qu’elle ait échoué ou pas : callback
.finally(() => ...)
.then()
, .catch()
et .finally()
renvoient la promesse elle-même, ce qui permet de les chainer afin d’écrire un code asynchrone de façon linéaire, ressemblant à un code synchrone.
Exemple :
$ npm install axios
...
$ node
> let axios = require('axios')
undefined
> axios.get("https://yesno.wtf/api")
... .then(rep => {
... console.log(rep.data)
... }
... .catch(err => {
... console.err(err.toString())
... }
... .finally(() => {
... console.log("The End")
... }
{ answer: 'no',
forced: false,
image: 'https://yesno.wtf/assets/no/25-55dc62642f92cf4110659b3c80e0d7ec.gif'
}
The End
De nombreuses librairies de promesses existent, mais depuis ES6 la classe Promise
est directement disponible et incorpore les fonctionnalités essentielles. Notamment :
Promise.all(<liste/iterable de promesses>)
: renvoie une promesse qui réussi lorsque toutes les promesses de la liste ont réussiPromise.resolve(valeur)
: renvoie une promesse qui réussi en renvoyantvaleur
Promise.reject(err)
: renvoie une promesse qui échoue avec l’erreurerr
async / await (depuis ES6)
Ces mot-clés sont du sucre syntaxique qui permet l’utilisation de promesses de façon simplifiée. Ils doivent être préférés à l’utilisation directe des promesses, sauf cas particuliers tels que l’usage de Promise.all()
.
Exemple :
async function getYesNo() {
try {
let rep = await axios.get("https://yesno.wtf/api")
console.log(rep.data)
} catch(err) {
console.err(err.toString())
} finally {
console.log("The End")
}
}
Programmation fonctionnelle avec map, forEach, reduce, filter, find

$ node
> [1, 2, 3].map(function(x) { return x*x })
[ 1, 4, 9 ]
$ node
> [1, 2, 3].forEach(function(x) { console.log(x*x) })
1
4
9
undefined
Les indices sont accessibles optionnellement :
process.argv.forEach((val, index) => {
console.log(`${index}: ${val}`)
})
$ node
> [1, 2, 3].reduce(function(accu, e) {
> return accu + e
>}, 0)
6
$ node
> [1, 2, -3, 4, -5, 6].find(function(e) {
> return e > 5
>})
6
$ node
> [1, 2, -3, 4, -5, 6].filter(function(e) {
> return e < 0
>})
[-3, -5]
Exemple : opérations ensemblistes
- intersection :
arr1.filter(x => arr2.includes(x))
- difference :
arr1.filter(x => !arr2.includes(x))
déstructuration / restructuration
listes
$ node
> let [a, b] = [1, 2]
undefined
> a
1
> b
2
> [a = 5, b = 7] = [1]
> a
1
> b
7
$ node
> let [ a, b, ...others ] = [1, 2, 3, 4, 5]
undefined
> others
[3, 4, 5]
> [ 11, 22, ...others]
[11, 22, 3, 4, 5]
dictionnaires
$ node
> let dict = { a: 1, b: 2, c: 3, d: 4 }
undefined
> let { b, a } = dict
undefined
> a
1
> b
2
> let { c, d, ...others } = dict
undefined
> others
{ a: 1, b: 2 }
> { x: 11, y: 22, ...others }
{ a: 1, b: 2, x: 11, y: 22 }
Ce mécanisme permet d’écrire des fonctions avec l’équivalent d’un passage de paramètres par mots-clés :
function func({a, b, c}) {
if (c === undefined) c = 3
...
}
func({b: 2, a: 1})
Activités à réaliser
Écrire une fonction qui inverse une chaine
Écrire une fonction qui indique qu’une chaine est un palindrome
Écrire une fonction qui renvoie un dictionnaire du nombre d’occurence de chaque lettre d’une chaine
- Écrire une fonction qui renvoie le caractère le plus fréquent dans une chaine (non vide)
Depuis ES7/ECMA2016
Set
$ node
> let ensemble = new Set([1, 2])
undefined
> ensemble.add(3); ensemble.add(2)
Set { 1, 2, 3 }
> ensemble.has(1)
true
> ensemble.has(4)
false
Map
TODO
Generateurs
$ node
> function *gen() { yield 1; yield 2; yield 3 }
undefined
> let g = gen()
undefined
> g
Object [Generator] {}
> g.next()
{ value: 1, done: false }
> g.next()
{ value: 2, done: false }
> g.next()
{ value: 3, done: false }
> g.next()
{ value: undefined, done: true }
$ node
> function *fibonacci() { yield 1; yield 1; let prev=1; prevprev=1; while (true) { let curr=prev+prevprev; yield curr; [prevprev, prev] = [prev, curr] } }
undefined
> let fib = fibonacci()
undefined
> fib.next()
{ value: 1, done: false }
> fib.next()
{ value: 1, done: false }
> fib.next()
{ value: 2, done: false }
> fib.next()
{ value: 3, done: false }
> fib.next()
{ value: 5, done: false }
> fib.next()
{ value: 8, done: false }
> fib.next()
{ value: 13, done: false }
Fermetures
Une fermeture est la paire formée d’une fonction et des références à son état environnant (l’environnement lexical).
En JavaScript, une fermeture est créée chaque fois qu’une fonction est créée.
function addTo(x) {
return function(y) {
return x + y
}
}
const add5 = addTo(5)
const add10 = addTo(10)
console.log(add5(2)) // 7
console.log(add10(2)) // 12
Les expressions régulières
TODO
Les événements
Le navigateur et NodeJS implémentent un mode de fonctionnement asynchrone basé sur les événements.
Dans le contexte du navigateur, les événements sont des interactions sur la page (clics et mouvementd de souris, clavier etc.), des déclenchements de timers, alors qu’en NodeJS les événements sont liés aux opérations asynchrones d’entrées/sorties (accès aux fichiers, réseau, etc.)
Javascript est un langage synchrone et mono-thread, mais son inclusion dans une ‘event-loop’ fournit l’impression d’une exécution asynchrone multi-thread :

Browser events
const event = new Event('build')
document.documentElement.addEventListener('build', (e) => {
console.log('a build event occurred!', e)
}, false)
document.documentElement.dispatchEvent(event)
Liste des événements navigateur : https://developer.mozilla.org/en-US/docs/Web/Events
NodeJS events
const EventEmitter = require('events')
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter()
myEmitter.on('event', (a, b) => {
console.log('an event occurred!', a, b)
})
myEmitter.emit('event', 11, 22)
Créer une commande shell avec NodeJS
Modifier package.json et ajouter l’entrée bin
; la propriété va devenir le nom de la commande :
{
"name": "jcbhello",
"version": "1.0.0",
"description": "jcb hello",
"bin": {
"hello": "./helloCmd.js"
}
}
helloCmd.js
#!/usr/bin/env node
console.log("Hello, world!")
Installation de la commande (dans un des chemins de $PATH) :
$ npm install -g
$ hello
Hello, world!
$ which hello
/Users/chris/.nvm/versions/node/v8.10.0/bin/jcbhello
Tendances dans l’écosystème JS
Le DOM et son API
Le Document Object Model est la représentation arborescente du contenu du document affiché dans un onglet, avec des noeuds de type Element
. Par exemple :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Le DOM et son API</title>
</head>
<body>
<div class="part1">
<p>Bla-bla</p>
</div>
<div class="part2">
<p>Bla-bla</p>
</div>
</body>
</html>
Le DOM associé à ce document HTML est le suivant :
Interface Window
Elle représente la fenêtre associée à un onglet.
window.document
, ou simplementdocument
, contient le DOM de cet ongletwindow.innerHeight
,window.innerWidth
: dimensions de la zone d’affichagewindow.open([<url>])
: créé un nouvel onglet ; optionnellement y charge<url>
Example :
const newWindow = window.open()
Interface Document
Elle représente le document affiché dans un onglet
Propriétés
document.documentElement
: élément racine du documentdocument.body
: élément<body>
du documentdocument.URL
: url du documentdocument.cookies
: cookies du documentdocument.domain
: domain name du documentdocument.lastModified
: date de dernière modification du documentdocument.title
: titre du documentdocument.innerHTML
: contenu du document
Example :
const newWindow = window.open()
newWindow.document.title = "Hello"
newWindow.document.documentElement.textContent = "Hello World!"
Remarque : Il est préférable de travailler à partir d’un template minimal, en particulier pour l’accès aux éléments tels que <body>
.
Méthodes
document.createElement(<tagName>)
: Ex:document.createElement("li")
document.getElementById(<id>)
: renvoie l’élément du document d’identifiant<id>
- `document.querySelector() : renvoie le premier élément correspondants au sélecteur
- `document.querySelectorAll() : renvoie la liste des éléments correspondants au sélecteur
Interface Element
C’est l’interface de base pour tous les objets d’un document
element.textContent
: contenu texte de l’élément. Ex:document.body.textContent = "Hello World!"
element.tagName
: tag de l’élément (ex:h1
)element.style
: dictionnaire des styles de l’élément. Ex:element.style.color = 'blue'
element.appendChild(<element>)
: ajoute un élément fils àelement.addEventListener(<eventType>, <callbackFunc>)
: ajoute à l’élément un écouteur des événementstypeEvent
; à chaque occurence d’un événement de ce type, la fonctioncallbacFunc
sera appelée.
Example :
index.html
<!DOCTYPE html>
<html>
<body>
<h1 id='btn'>ClickMe</h1>
</body>
</html>
<script>
const btn = document.getElementById('btn')
btn.addEventListener('click', (event) => {
console.log('click', event)
})
</script>
Autres APIs du DOM
https://developer.mozilla.org/en-US/docs/Web/API
Activité à réaliser : application JS/DOM
Écrire une application en HTML/CSS + Javascript ‘Vanilla’ qui affiche une barre de progression reflètant le niveau de charge de la batterie. La barre doit évoluer en temps réel. On utilisera la Battery API (ne fonctionne que sur Chrome).

Structure générale des applications web et mobiles
Avec le protocole HTTP
- Le client (navigateur) fait une requête d’accès à une ressource auprès d’un serveur Web selon le protocole HTTP
- Le serveur vérifie la demande, les autorisations et effectue la tache demandée. Il envoie en retour une réponse au client.
- Le client interprète la réponse reçue (ex: modification de l’affichage)
Il faut noter que le serveur ne peut pas envoyer d’information à un client spontanément, c’est à dire sans que celui-ci ne lui ait fait une demande.
Avec le protocole Websocket
Le client fait des demandes au serveur, mais le protocole websocket permet également à un serveur d’envoyer des informations de son propre chef à un ou plusieurs de ses clients connectés.
Les connexions entre client et serveur par un websocket sont persistantes, par opposition aux connexions http ordinaires.
Sites web statiques
Les premiers sites web étaient entièrement statiques. Il en existe encore beaucoup, notamment pour des documentations. Ici les ressources accédées sont toutes statiques : pages html, images, vidéos, feuilles de style (css). Aucun script n’est exécuté, le navigateur affiche les éléments statiques html, css etc. et permet la navigation entre les pages en cliquant sur les liens hypertexte.
L’arborescence des fichiers html définit la structure des urls.
Par exemple, l’accès à http://monsite.org/collections/hiver/manteaux.html
provoquera un accès en lecture à un fichier situé à l’emplacement relatif collections/hiver/manteaux.html
par rapport à la racine de l’emplacement des pages statiques, typiquement /var/www/monsite/html/
Applications web dynamiques
Deux éléments peuvent rendre un site web dynamique :
- du code javascript peut être ajouté aux pages html, permettant l’exécution de fonctions par le moteur javascript du navigateur.
- les requêtes du client vers le serveur peuvent être des commandes vers un serveur d’application, qui peut faire des accès à une base de données et/ou calculer dynamiquement une réponse au navigateur
Les protocole HTTP et Websocket
Protocole HTTP : requêtes et réponses

Une requête HTTP est un flux d’octets (un texte) envoyé par le client vers le serveur au travers du réseau. Elle est structurée en lignes.
Exemple d’une requête de type GET :
$ curl https://yesno.wtf/api
GET /api HTTP/1.1
Host: yesno.wtf
User-Agent: curl/7.64.1
Accept: */*
En retour, le serveur renvoie une réponse
à cette requête, avec une structure analogue.
Exemple de réponse à la requête précédente :
Status: 200 OK
Transfer-Encoding: chunked
X-Powered-By: Phusion Passenger 5.0.1
access-control-allow-origin: *
access-control-request-method: *
cache-control: max-age=0, private, must-revalidate
etag: "c869ad1693dfe675a79a187fa6c1020e"
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-request-id: a6d669c1-401d-4355-aa9a-9dfcd97859a3
x-runtime: 0.002400
x-xss-protection: 1; mode=block
{
"answer": "no",
"forced": false,
"image": "https://yesno.wtf/assets/no/32-b62f1f8058c1d7f06c528319faccfb38.gif"
}
Les requêtes sont souvent envoyées par les navigateurs web :
- obtention du contenu d’une page
- obtention du contenu d’un fichier de script (js) ou de style (css)
- post des données d’un formulaire
- appel d’un webservice, sur le serveur d’origine ou sur un autre serveur (cross-origin)
Elles peuvent aussi être envoyées par tout type de programme ou d’application.
Par exemple, la commande GNU curl
permet facilement d’envoyer des requêtes à un serveur et d’en afficher la réponse :
$ curl https://www.google.fr
<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="fr">...
Principaux headers
Headers de la requête
Content-Type
, type du média du body de la requêteContent-Type: application/json Content-Type: application/json; charset=UTF-8 Content-Type: text/html
Accept
indique quels sont les types de contenu, exprimés sous la forme de types MIME, que le client sera capable d’interpréter
Headers de la réponse
Content-Type
, type du média du body de la réponseContent-Type: application/json Content-Type: text/html
‘ETag’
Etag: "chaine de hashage unique"
Si un ETag est présent dans la réponse, le navigateur ira toujours requêter le serveur pour vérifier qu’il a la dernière version. Si le navigateur possède une copie de la ressource en cache et si l’ETag sur le serveur est le même que celui du cache, le body de la ressource ne sera pas requêté et c’est celui du cache qui sera utilisé. S’il est différent, le navigateur chargera son cache avec le nouveau body et mettra à jour l’ETag. Cette stratégie est nécessaire pour les applications SPA dont le point de départ est le fichier
index.html
, afin que le déploiement d’une nouvelle version soit immédiatement disponible par les utilisateurs sans qu’ils aient besoin de faire un reload ou un hard-reload.
Le header ETag
est prioritaire sur le header Cache-Control
Cache-Control
Une directive Cache-Control
indique au navigateur de ne même pas chercher à charger une version plus récente d’une ressource avant une certaine date.
Cache-Control: "no-cache, no-store, max-age=0, must-revalidate"
Cookies
Un cookie est une paire (clé, valeur) qui est stockée dans un espace dédié du navigateur, associé au nom de domaine appelé.
Un serveur peut mettre dans une response
HTTP un ordre de stockage d’un nouveau cookie dans le navigateur client :
Set-Cookie: token=JjlmIyAZuV6u6HY4R8Vl8byOhvkkLzZDTcgTD7Paehe3xLy4VI; mode=dev;
Lors de chaque requête sur le même nom de domaine, le navigateur inclut systématiquement tous les cookies dans la requête HTTP, dans une ligne (attention à l’espace après ;
) :
Cookie: key1=value1; key2=value2;
Exemple:
GET /spec.html HTTP/1.1
Host: www.example.org
Cookie: token=JjlmIyAZuV6u6HY4R8Vl8byOhvkkLzZDTcgTD7Paehe3xLy4VI; mode=dev;
curl & cookie
--cookie "key=value"
Fonctions et librairies de requêtes HTTP
NodeJS (back-end) & Javascript (front-end)
Plusieurs librairies existent, basées sur le package http
.
Exemple avec la librairie axios
:
let response = await axios({
url: USERS_ENDPOINT,
method: 'post',
headers: {
"Authorization": `Bearer ${sessionToken}`,
"Content-Type": "application/json",
},
data: userData,
})
let createdUser = response.data
...
Javascript (front-end)
Les requêtes http effectuées depuis un script par le navigateur sont appelées requêtes XHR (XML HTTP Request). On les effectue avec la fonction built-in fetch
.
Exemple :
let response = await fetch(USERS_ENDPOINT, {
method: 'POST',
headers: {
"Content-Type": "application/json",
},
body: userData,
})
let createdUser = await response.json()
Le protocole Websocket
C’est un protocole de communication bi-directionnel sur une connection TCP. Dans une communication par websocket il y a toujours un serveur et un client comme dans le protocole HTTP, mais le serveur peut à tout moment envoyer des données vers le client, sans qu’il s’agisse d’une réponse à une demande.
Les connexions entre un serveur et un client sont persistantes, contrairement aux connexions http. Un serveur peut être connecté à un grand nombre de clients
Un client connecté à un serveur peut lui envoyer des messages, et il est prévenu par des événements de l’arrivée de messages en provenance du serveur. Un serveur peut émettre des événements à un client particulier, ou globalement à tous les clients connectés (broadcast).

Spécification REST
Elle s’appuie sur la notion de ressource, une ressource étant associée à un endpoint.
Exemple : la ressource users
associée au end-point http://json-server.jcbuisson.dev/users
- chaque instance de ressource (par ex. chaque user) possède un identifiant, généralement autogénéré par la base de données
- les verbes HTTP GET, POST, PUT et DELETE correspondent à des opérations de type CRUD (CREATE, READ, UPDATE, and DELETE) sur les ressources
- cette spécification est applicable aussi bien au protocole HTTP qu’au protocole Websocket
GET
La méthode GET est utilisée pour obtenir du serveur la liste des toutes les ressources d’un certain type, ou une ressource particulière précisée par son identifiant
Exemples :
GET http://json-server.jcbuisson.dev/users
GET http://json-server.jcbuisson.dev/users/89
GET http://json-server.jcbuisson.dev/users?gender=male
Les deux requêtes font référence au end-point
http://json-server.jcbuisson.dev/users
; la première demande la liste de tous les utilisateurs, la seconde la liste de tous les utilisateurs hommes, la troisième l’utilisateur d’identifiant 89.
curl http://json-server.jcbuisson.dev/users
POST
La méthode POST est utilisée pour créer une nouvelle ressource sur le serveur
Exemple :
POST http://json-server.jcbuisson.dev/users
Cette requête créée une nouvelle resource pour le endpoint http://json-server.jcbuisson.dev/users
, c’est à dire un nouvel utilisateur. Les données qui concernent l’utilisateur doivent être placées dans la section DATA
de la requête.
curl -X POST -H 'Content-Type: application/json' -d '{"firstname": "John", "lastname": "Doe"}' http://json-server.jcbuisson.dev/users/1
PUT
La méthode PUT est utilisée pour mettre à jour une ressource spécifique sur le serveur
Exemple :
PUT http://json-server.jcbuisson.dev/users/89
L’ensemble des données de l’utilisateur 89 doit être placé dans la section DATA de la requête.
curl -X POST -H 'Content-Type: application/json' -d '{"firstname": "Jon", "lastname": "Doe"}' http://json-server.jcbuisson.dev/users
PATCH
La méthode PATCH est également utilisée pour mettre à jour une ressource spécifique sur le serveur
Exemple :
PATCH http://json-server.jcbuisson.dev/users/89
À la différence de PUT, seules les données à modifier doivent être placées dans la section DATA de la requête
curl -X PATCH -H 'Content-Type: application/json' -d '{"firstname": "Joe"}' http://json-server.jcbuisson.dev/users/1
DELETE
La méthode DELETE permet de supprimer une ressource spécifique Example:
DELETE http://json-server.jcbuisson.dev/users/1
curl -X DELETE http://json-server.jcbuisson.dev/users/1
OPTIONS
OPTIONS est utilisée pour obtenir les options de communication avec la cible : verbes HTTP possibles, méthodes d’authentification demandées, utilisation de la politique CORS (Cross Origin Resource Sharing)
Feathers
Feathers est une librairie légère, utilisable principalement côté serveur, mais avec également des modules client.
Feathers opère sur les aspects suivants :
- les opérations back-end, qui sont abstraites sous forme de services
- des connecteurs vers toutes les bases de données, sous forme de services REST prédéfinis
- l’authentification
- le transport entre client et serveur peut être http ou websocket, par simple configuration et sans modification du reste du code
- le temps-réel, grace à un mécanisme de type publish/subscribe (transport par websocket)
- une librairie client simplifie l’accès aux services par le front-end
Exemple d’application temps-réel : liste partagée
Clonez le projet : https://gitlab.com/buisson31/fs-feathers-items

backend/src/app.js
const feathers = require('@feathersjs/feathers')
const socketio = require('@feathersjs/socketio')
const memory = require('feathers-memory')
// Create a Feathers application
const app = feathers()
// Configure the Socket.io transport
app.configure(socketio())
// Create a channel that will handle the transportation of all realtime events
app.on('connection', connection => app.channel('everybody').join(connection))
// Publish all realtime events to the `everybody` channel
app.publish(() => app.channel('everybody'))
// Create an 'items' service which stores the items in memory (instead of a database)
app.use('items', memory())
// Start the server on port 3030
app.listen(3030)
console.log('listening on port 3030')
frontend/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Liste partagée</title>
</head>
<body>
<h2>Liste de courses partagée</h2>
<label>Item
<input type="text" />
</label>
<button type="submit">Ajouter</button>
<hr/>
</body>
</html>
<script type="module">
import io from 'socket.io-client'
import feathers from '@feathersjs/client'
// Create a websocket connecting to Feathers server
const socket = process.env.NODE_ENV === 'production' ? io() : io('localhost:3030')
const app = feathers()
app.configure(feathers.socketio(socket))
function addItemElement(item) {
const itemElement = document.createElement('li')
itemElement.innerText = item.text
document.body.appendChild(itemElement)
}
// get & display already existing items
app.service('items').find({}).then(itemsList => {
itemsList.forEach(item => addItemElement(item))
})
// Listen to new items being created
app.service('items').on('created', item => {
console.log('item created!', item)
addItemElement(item)
})
const input = document.querySelector('input')
const addButton = document.querySelector('button[type="submit"]')
addButton.addEventListener('click', ev => {
// Create a new item
app.service('items').create({ text: input.value })
input.value = ''
})
</script>
Activité à réaliser
Utiliser les devtools de Firefox pour visualiser le contenu des messages échangés au travers de la connexion websocket persistante
Services
Toutes les taches du back-end sont abstraites sous la forme de services.
Un service est un point d’accès (endpoint) REST, qu’on utilise un transport HTTP ou Websocket.
Implémentation
Un service est une simple classe possédant une ou plusieurs des méthodes suivantes :
class MyService {
async create(data, params)
async get(id, params)
async find(params)
async update(id, data, params)
async patch(id, data, params)
async remove(id, params)
setup(app, path)
}
Dans l’exemple, le service ‘items’ s’appuie sur l’adapter feathers-memory
qui permet un stockage de type REST en mémoire (non persistant).
Activité à réaliser
Ajouter (et gérer) un bouton ‘supprimer’ en face de chaque item dans l’interface web. On utilisera la méthode REST ‘remove’ du service ‘items’.

Services de bases de données
FeathersJS permet en quelques lignes de code de créer un service qui fournit un accès REST sur une table de base de données relationnelle, ou une base NoSQL. FeathersJS dispose d’adapters pour la plupart des SGBD (DBMS) existants.
Dans ce qui suit on va utiliser le query-builder Knex avec l’adapter feathers-knex
On utilisera aussi le système de configuration de feathers, qui prend les constantes dans un répertoire config
Installation de PostgreSQL sur Linux (debian)
apt install postgresql
sudo su postgres
psql # CLI de postgres, cheatsheet: https://gist.github.com/Kartones/dd3ff5ec5ea238d4c546
# \password postgres
-> "motdepasse" x 2
# createdb fs-feathers-items
# Ctrl-d
Ctrl-d
backend/config/default.json
{
"dbConfig": {
"client": "pg",
"connection": "postgres://postgres:motdepasse@localhost:5432/fs-feathers-items"
}
}
backend/src/knex.js
const knex = require('knex')
module.exports = function (app) {
const { client, connection } = app.get('dbConfig')
const db = knex({ client, connection })
app.set('knexClient', db)
}
backend/src/services/database/items/items.model.js
module.exports = async function(app, tableName) {
try {
const db = app.get('knexClient')
await db.schema.createTable(tableName, table => {
table.increments('id')
table.timestamp('created_at').defaultTo(db.fn.now()) // ex: 2020-11-23 06:32:48.524937+01
table.text('text').notNull()
})
console.log(`Created ${tableName} table`)
} catch(err) {
console.log(`Error creating ${tableName} table: ${err.toString()}`)
}
}
backend/src/services/database/items/items.service.js
const service = require('feathers-knex');
module.exports = function(app) {
const db = app.get('knexClient')
let name = 'items'
app.use(name, service({
Model: db,
name,
paginate: false,
}))
}
backend/src/scripts/create-tables.js
const feathers = require('@feathersjs/feathers')
const configuration = require('@feathersjs/configuration')
const knex = require('../knex')
const createItemsTable = require('../services/database/items/items.model')
async function main() {
const app = feathers()
app.configure(configuration())
app.configure(knex)
// database
await createItemsTable(app, "items")
process.exit(0)
}
main()
backend/src/app.js
...
const configuration = require('@feathersjs/configuration')
const knex = require('./knex')
const itemsService = require('./database/items/items.service.js')
...
app.configure(configuration())
app.configure(knex)
app.configure(itemsService)
...
Services ‘custom’
On peut aussi créer des services totalement personalisés, mais leur utilisation doit nécessairement passer par l’utilisation d’une ou plusieurs des 7 méthodes ‘create’, ‘get’, ‘find’, etc.
Exemple : ajout d’un service ‘custom’ d’envoi de mail
app.js
...
const emailService = require("./services/custom/email/emailService")
app.configure(emailService)
...
src/services/email/emailService.js
const nodemailer = require('nodemailer')
module.exports = function(app) {
class MailService {
create(data) {
const mailConfig = {
"host": "smtp.online.net",
"port": "587",
"secure": false,
"auth": {
"user": "fullstack@shdl.fr",
"pass": process.env.SMTP_PASSWORD,
},
"name": "shdl.fr"
}
const transporter = nodemailer.createTransport(mailConfig)
try {
return transporter.sendMail({
from: data.from,
to: data.to,
subject: data.subject,
text: data.text,
html: data.html,
})
} catch(err) {
console.log(err)
}
}
}
app.use("email", new MailService())
}
Client node : src/scripts/send-mail.js
// Usage:
// cd backend
// node src/scripts/send-email.js buisson@n7.fr Hello "Hello JC how are you doing?"
const io = require('socket.io-client')
const feathers = require('@feathersjs/feathers')
const socketio = require('@feathersjs/socketio-client')
const socket = io('http://localhost:3030')
const app = feathers()
app.configure(socketio(socket))
async function main() {
await app.service('email').create({
from: 'buisson@enseeiht.fr',
to: process.argv[2],
subject: process.argv[3],
text: process.argv[4],
})
process.exit(0)
}
main()
Activité à réaliser
Créer un client web d’envoi de mail, avec l’interface suivante :
Hooks
Ce sont des méthodes qui peuvent être appelées avant ou après l’exécution d’une des méthodes d’un service :
- vérifier que l’utilisateur est authentifié
- vérifier que l’utilisateur a des privilèges suffisants
- limiter les requêtes d’un utilisateur à ce qu’il est autorisé à accéder
- valider le ‘payload’ d’une requête par rapport à un schema
- adapter certains attributs du payload des requêtes (ajouter une date de modification, normaliser un numéro de téléphone)
- retirer certains champs d’une réponse (ex: mot de passe)
- déclencher des actions (ex: envoyer un email, créer une entrée d’historisation)
Exemple :
app.service('users').hooks({
before: {
patch: [async context => {
context.data.updatedAt = new Date();
return context;
}],
...
Lorsqu’un hook produit une erreur, sels les hooks d’erreur sont appelés. Les autres hooks et le service lui même (s’il n’a pas déjà été exécuté) ne sont pas appelés.
FeathersJS fournit de nombreux hooks prédéfinis qu’il suffit d’ajouter dans la bonne rubrique. Par exemple, des hooks d’authentification prédéfinis permettent de vérifier que l’utilisateur est authentifié (hook authenticate
), de hasher un mot de pass (hook hashPassword
), de supprimer un attribut/valeur d’un object résultat (hook protect
) :
app.js
...
app.configure('./services/users/users.hooks')
...
users.hooks.js
const { authenticate } = require('@feathersjs/authentication').hooks
const { hashPassword, protect } = require('@feathersjs/authentication-local').hooks
module.exports = {
before: {
all: [],
find: [authenticate('jwt')],
get: [authenticate('jwt')],
create: [hashPassword('password')],
update: [hashPassword('password'), authenticate('jwt')],
patch: [hashPassword('password'), authenticate('jwt')],
remove: [authenticate('jwt')],
},
after: {
all: [
protect('password'),
],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
error: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
}
}
Problématique de l’authentification
Gestion des mots de passe
- ils doivent être stockés de façon hashée dans la BD, jamais en clair
- la production du hash est non-inversible, mais permet de vérifier la concordance
- le hash doit être salé, sinon il peut être attaqué par des dictionnaires de valeurs hashées de mots de passe
Librairie bcrypt
$ npm install bcrypt
Production du hash
const bcrypt = require('bcrypt')
let saltRounds = 10
let hash = bcrypt.hashSync("eureka", saltRounds)
hash –> ‘$2bMc07H0wBqBL9a6g32ecig.LbUbk2uBeCYIlDrcZ0Goe9lSLXVeTj2’
Le hash d’un mot de passe est modifié à chaque appel (salage), mais cela n’affecte pas la vérification de la compatibilité d’un mot de passe candidat avec l’une quelconque de ses valeurs de hashage
Vérification d’un mot de passe candidat
bcrypt.compareSync("eureka", hash) // true
bcrypt.compareSync("eurehack", hash) // false
Les tokens JWT
Voir https://jwt.io
Un token JWT est composé de trois parties, séparées par un point :
- une en-tête qui indique notamment le type de signature
- un payload json, non crypté. Sa validité est garantie par la signature
- une signature
Après authentification d’un utilisateur, le serveur lui transmet un token (de session) que le client ajoutera à toutes ses requêtes. Typiquement, un client stocke ce token dans un cookie.
Le serveur n’accepte de traiter une requête que si elle contient un token dont la signature est valide.
Le payload du token contient typiquement l’id de l’utilisateur et une date d’expiration. Conrairement à d’autres types de tokens, le serveur n’a pas besoin de stocker les tokens JWT car ils contiennent dans leur payload les informations dont il a besoin.
Créer un token
let jwt = require('jsonwebtoken')
let token = jwt.sign({ foo: 'bar' }, "MYSECRET")
Lire le payload d’un token
let payload = jwt.decode(token)
Vérifier la signature d’un token
try {
let payload = jwt.verify(token, "MYSECRET")
} catch(err) {
console.log("signature invalide")
}
Authentification par cookies vs authentification par tokens
lors d’une authentification par cookies, un cookie d’authentification est envoyé par le backend vers le navigateur lors du login. Toute requête du frontend vers le backend est alors sécurisée, car elle contient automatiquement le cookie d’authentification.
si l’authentification est basée sur des tokens (ex: OAuth), un token d’accès est envoyé par le backend vers le frontend lors du login. Le frontend stocke ce token d’accès dans LocalStorage ou SessionStorage ou plus rarement dans un cookie, et toutes les requêtes vers le backend doivent comporter un header ‘Authorization: Bearer ’.
Authentification avec FeathersJS
FeathersJS contient des fonctions back et front d’authentification par token. Elles sont de deux sortes :
- l’authentification dite ‘locale’, avec identifiant/mot de passe
- l’authentification OAuth 2 avec tout type de serveur d’authentification (Google, Facebook, LinkedIn, etc.)
Authentification ‘locale’ avec FeathersJS
Authentification OAuth 2 avec FeathersJS
VueJS, version 3
Vue ne gère que la vue
les autres fonctions (routage, requêtes http, etc.) sont gérées dans des modules séparés, facilement pluggables
les templates sont généralement à base html, mais pas nécessairement (ex: nativescript-vue)
on peut décrire chaque vue avec l’Option API ou la nouvelle Composition API. On ne présentera ici que la Composition API plus moderne et qui permet de placer la logique de l’application à l’extérieur des vues.
Premier exemple
Cloner le projet : git@gitlab.com:buisson31/fs-vue3.git
counter.html
<div id="app">
<div :style="{ color }">
Compteur: {{ counter }}
</div>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script type="module">
const { createApp, ref } = Vue
const App = {
setup() {
const counter = ref(0)
const color = ref('green')
setInterval(() => {
counter.value++
color.value = counter.value % 2 ? 'red' : 'green'
}, 1000)
return {
counter,
color,
}
}
}
createApp(App).mount('#app')
</script>
l’exécution de
Vue.createApp(App).mount('#app')
rend dynamique le template<div id="app">...</div>
counter
etcolor
sont réactifs, c’est à dire qu’ils sont liés aux élements du DOM qui le référencent. Dans la partie<script>
, leur valeur est accessible parcounter.value
etcolor.value
. Lorsque cette valeur change, le DOM associé change aussi en temps-réel. La relation est mono-directionnelle, de Javascript vers le DOM.{{ <expr> }}
: contenu texte d’éléments:<attr>="<expr>"
: valeurs d’attributs
Les expressions peuvent être quelconques, multiples (séparées par des ;
) et référencer des variables réactives.
Activité à réaliser
Exécuter cette application dans un environnement de développement social tel que https://codepen.io
Deuxième exemple : double binding
input.html
<div id="app">
<h1>{{ name }}</h1>
<input type="text" v-model="name" />
<div>Hello {{ name }}</div>
<input type="text" v-model="name" />
</div>
<script src="https://unpkg.com/vue@next"></script>
<script type="module">
const { createApp, ref } = Vue
const App = {
setup() {
const name = ref('JC')
return {
name,
}
}
}
createApp(App).mount('#app')
</script>
L’attribut v-model="name"
indique que name
est le modèle de la vue que constitue l’input.
Le binding est cette fois dans les deux sens JS <–> DOM : la valeur initiale ‘JC’ dans la partie script est immédiatement visible dans le DOM ; l’entrée d’un nouveau texte est un changement dans le DOM qui est immédiatement propagé vers le script et met à jour la valeur name.value
, qui en retour met à jour les 4 éléments réactifs du template.
En réalité, <input v-model="name">
est du sucre syntaxique pour :
<input
:modelValue="name"
@update:modelValue="name = $event.target.value"
/>
Propriétés calculées (computed properties)
Une computed property
est un objet réactif dont la valeur est synchronisée sur la valeur d’autres objets réactifs par le biais d’une fonction.
Exemple :
const { createApp, ref, computed } = Vue
...
const normalizedName = computed(() => name.value.replace('-', ' '))
- elles apportent beaucoup de déclarativité
- les propriétés calculées sont mises en cache selon leurs dépendances ; leur évaluation est paresseuse
Boucles et conditionnelles
index.html
<html>
<div id="app">
<div v-if="items.length > 0">
<ul>
<li v-for="item in items">
{{ item.text }}
</li>
</ul>
</div>
<div v-else>
La liste est vide
</div>
</div>
</html>
Single-file components
On peut décrire tous les éléments d’un composant (template, script, style) dans un même fichier d’extension .vue
. Avec Webpack et son plugin vue-loader
ces parties sont extraites et traitées séparément. Cela permet :
- une édition simplifiée avec coloration syntaxique si on installe dans son IDE un plugin pour fichiers
.vue
- la transpilation de javascript ES6 ou ES7 avec Babel
- des modules javascript
CommonJS
- un styling CSS dont le scope peut être restreint au composant
- le hot-reload
l’utilisation de l’extension de navigateur ‘Vue devtools’
Chrome : https://chrome.google.com/webstore/detail/vuejs-devtools/ljjemllljcmogpfapbkkighbhhppjdbg)
La mise en place d’un tel système se fait simplement en utilisant vue-cli
npm install -g @vue/cli
vue create my-project
cd my-project
npm run serve
Exemple : liste partagée avec Vue3
Aller sur la branche vue3
du projet fs-feathers-items
src/App.vue
<template>
<label>Item
<input type="text" v-model="itemText" />
</label>
<button @click="addItem(itemText)">Ajouter</button>
<hr/>
<div>
<ul>
<li v-for="item in itemsList">
{{ item.text }} <span><button @click="deleteItem(item.id)">Supprimer</button></span>
</li>
</ul>
<div v-if="itemsList.length === 0">
Aucun item présent
</div>
</div>
</template>
<script>
import { ref } from "vue"
import { useItems } from "@/use/useItems"
export default {
setup() {
const { itemsList, addItem, deleteItem } = useItems()
const itemText = ref('')
return {
itemsList, addItem, deleteItem,
itemText,
}
}
}
</script>
src/feathers-client.js
import io from 'socket.io-client'
import feathers from '@feathersjs/client'
const app = feathers()
const socket = io()
app.configure(feathers.socketio(socket))
export default app
src/use/useItems/index.js
import { reactive, computed } from "vue"
import app from "@/feathers-client"
const itemsState = reactive({
itemsListReady: false,
items: {},
})
app.service('items').on('created', async item => {
console.log('ITEMS EVENT created', item)
itemsState.items[item.id] = item
})
app.service('items').on('patched', item => {
console.log('ITEMS EVENT patched', item)
itemsState.items[item.id] = item
})
app.service('items').on('removed', item => {
console.log('ITEMS EVENT removed', item)
delete itemsState.items[item.id]
})
const itemsList = computed(() => {
if (!itemsState.itemsListReady) {
app.service('items').find({}).then(list => {
list.forEach(item => { itemsState.items[item.id] = item })
itemsState.itemsListReady = true
})
return []
}
return Object.values(itemsState.items)
})
const addItem = (text) => {
app.service('items').create({ text })
}
const deleteItem = (id) => {
app.service('items').remove(id)
}
export function useItems() {
return {
itemsList, addItem, deleteItem,
}
}
Activité à réaliser
Réaliser un éditeur Markdown en utilisant le package npm marked
Cycle de vie d’un composant Vue

On peut créer des hooks
pour chacun de ces moments du cycle de vie, par exemple :
onMounted() {
...
}
onUpdated() {
...
}
Composants et sous-composants
Qu’est-ce qu’un composant ?
- un composant est une
Vue
paramétrable avec des propriétésprops
fournies par son composant parent - un composant peut émettre des événements vers son composant parent
Exemple
Aller sur la branche components
du projet fs-feathers-items
src/App.vue
<template>
<label>Item
<input type="text" v-model="itemText" />
</label>
<button @click="addItem(itemText)">Ajouter</button>
<hr/>
<div style="display: flex;">
<div class="list">
<div v-for="item in itemsList" @click="selectItem(item)"
:class="{'list-item': true, selected: selectedItem && item.id === selectedItem.id, changed: item.changed }">
{{ item.text }} <span><button @click="deleteItem(item.id)">Supprimer</button></span>
</div>
</div>
<div v-if="itemsList.length === 0">
Aucun item présent
</div>
<ItemDetails v-if="selectedItem" :item="selectedItem" @itemChanged="onItemChange"></ItemDetails>
</div>
</template>
<script>
import { ref } from "vue"
import { useItems } from "@/use/useItems"
import ItemDetails from "@/components/ItemDetails.vue"
export default {
components: {
ItemDetails,
},
setup() {
const { itemsList, addItem, deleteItem } = useItems()
const itemText = ref('')
const selectedItem = ref()
const selectItem = (item) => {
selectedItem.value = item
}
const onItemChange = (item) => {
console.log('changed', item)
item.changed = true
}
return {
itemsList, addItem, deleteItem,
itemText,
selectItem,
selectedItem,
onItemChange,
}
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
.list {
min-width: 400px;
}
.list-item {
border-bottom: solid 1px;
}
.list-item:hover {
background-color: lightgrey;
color: white;
}
.list-item.selected {
background-color: grey;
color: white;
}
.list-item.changed {
background-color: blue;
color: white;
}
</style>
src/components/ItemDetails.vue
<template>
<div style="margin: 20px;">
<input type="text" :value="item.text" @input="onInput" />
</div>
</template>
<script>
import { toRefs } from "vue"
export default {
emits: [
'itemChanged',
],
props: {
item : Object,
},
setup(props, context) {
const { item } = toRefs(props)
const onInput = (text) => {
context.emit('itemChanged', item.value)
}
return {
item,
onInput,
}
}
}
</script>
Modèle de communication entre composants
- pour faire court : props down, events up
- l’héritage des
props
ne va que du père vers les fils, il ne descend pas vers les petit-fils - un composant émet des événements que seul son père peut écouter (et pas son grand-père). Pour émettre un événement
'myevent'
avec les paramètresparam1
, …, il faut appelercontext.emit('myevent', param1, ...)
Le modèle de communication est donc simple et bien cloisonné, strictement entre un père et son fils. Les propriétés descendent du père vers le fils, alors que des événements remontent du fils vers le père.
Routage des URL front-end
- Routage traditionnel : l’utilisateur clique sur un lien, le navigateur envoie alors une requête au serveur avec la nouvelle url puis le navigateur remplace le contenu de la page par le contenu répondu par le serveur. La première requête vers un site est forcément traditionnelle puisqu’aucun javascript n’est encore chargé pour faire fonctionner le routage js/html5.
- Routage js/html5 : l’utilisateur clique sur un lien, le moteur javascript (qui observe le window.location) réagit au changement, calcule et met à jour le DOM pour présenter le nouveau contenu (et en récupérant éventuellement des données en provenance du serveur). Conséquence importante : la majeur partie du travail se fait coté client ce qui permet plus de réactivité mais il faut du js pour que ça fonctionne -> pb SEO. Autre conséquence, que se passe-t-il quand on bookmark une url et qu’on la recharge alors que le js n’est pas en train d’écouter ? -> obligation de gérer ce cas coté serveur.
Le routage front-end : push-state ou hash-based
La partie chemin d’accès d’une url (après le protocole, le nom de domaine et le port) a la forme suivante :
/<chemin>/.../<chemin>/#/<chemin>/.../<chemin>/
Dans tous les framework front-end modernes comme React, Angular ou Vue, le choix est proposé d’opérer le routage front-end, soit sur la partie avant le hash #
, soit sur la partie après le hash.
Quand on prend en compte seulement la partie avant le hash, on parle de mode historique HTML5, ou push-state ; par exemple : http://mon.domaine/register
Quand on prend en compte la partie après le hash, on parle de routage en mode hash ; par exemple : http://mon.domaine/client/#/register
Vue-router
Le routage des url par le client n’est pas assuré par VueJS lui-même, mais par un module tiers, généralement vue-router
.
Il est disponible sous forme d’un plugin pour le cli de vue : vue add router
<template>
<div id="app">
<router-link to="#/componenta">Composant A</router-link>
<router-link to="#/componentb">Composant B</router-link>
<router-view></router-view>
</div>
</template>
router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import ComponentA from '@/components/ComponentA'
import ComponentB from '@/components/ComponentB'
Vue.use(Router)
const router = new Router({
mode: 'hash',
base: '/',
routes: [
{
path: '/componenta',
component: ComponentA,
},
{
path: '/componentb',
component: ComponentB,
// component: () => import("@/components/ComponentA.vue"),
},
]
})
export default router
components/ComponentA.vue
<template>
<p>Hello composant A</p>
</template>
components/ComponentB.vue
<template>
<p>Hello composant B</p>
</template>
main.js
import router from './router'
...
new Vue({
...
router
}).$mount('#app')
Algorithmique
- à chaque fois qu’un nouvel url est poussé (
router.push
, lien<router-link>
), ou modifié (router.go
), la table de routage est consultée et la première règle qui s’applique est prise en compte. Toutes les autres sont ignorées. - le tag
<router-view />
de plus haut niveau est remplacé par le composant spécifié par la règle active. Selon la valeur deprops
(booléen, objet, fonction), les props du composant sont affectées (voir plus loin) - si la route contient des
children
, le même algorithme est appelé récursivement pour peupler les<router-view />
emboités
Le plugin Vue
dans le navigateur permet de suivre les associations entre routes et instances de <router-view />
Passage des props
aux composants d’une route
Mode booléen
Indiquer props: true
passe au composant référencé par la route, tout l’objet route.params
en tant que props
. Par exemple :
{
path: '/user/:id/posts/:postid',
component: Post,
props: true
}
Ici c’est l’objet { id: <id>, postid: <postid> }
qui est passé comme props
au composant Post
associé à la route
Mode objet
Quand props
dans la définition de la route est un objet, c’est lui qui sera passé tel-quel au composant
Mode fonction
Quand props
dans la définition de la route est une fonction, c’est la valeur de cette fonction qui sera passée au composant. Cette fonction prend comme argument l’objet route
. Par exemple :
props: (route) => ({ query: route.query.q })
The Full Navigation Resolution Flow
- Navigation triggered.
- Call beforeRouteLeave guards in deactivated components.
- Call global beforeEach guards.
- Call beforeRouteUpdate guards in reused components.
- Call beforeEnter in route configs.
- Resolve async route components.
- Call beforeRouteEnter in activated components.
- Call global beforeResolve guards.
- Navigation confirmed.
- Call global afterEach hooks.
- DOM updates triggered.
- Call callbacks passed to next in beforeRouteEnter guards with instantiated instances.
Bonne pratique : normaliser les données
Stockage de données sur le navigateur : WebStorage et IndexedDB
- l’API WebStorage (localStorage & sessionStorage) permet de stocker de petites quantités de données non structurées dans le navigateur. Le support physique de ce stockage est variable selon les navigateurs
- l’API IndexDB, plus complexe, permet de stocker de grandes quantités de données structurées. Le support physique de ce stockage dépend du navigateur (SQLite pour Firefox, LevelDB)
- ces données sont visibles dans les DevTools (‘Application’ sur Chrome, ‘Stockage’ sur Firefox)
API WebStorage
- stockage = dictionnaire clé-valeur
- les dictionnaires (localStorage ou sessionStorage) sont associés aux urls des onglets, plus précisément à leur domain-name (sans le path, les query params, le hash)
- dans la console d’un onglet, ces dictionnaires sont accessibles sous les noms ‘localStorage’ et ‘sessionStorage’
- les clés et les valeurs sont des chaînes. Pour manipuler des valeurs complexes, il faut utiliser JSON.stringify et JSON.parse
localStorage
- deux onglets dont les urls ont le même domain-name, partagent le même dictionnaire localStorage
- les données persistent même après la fermeture de l’onglet
sessionStorage
- les données ne sont accessible que dans l’onglet d’origine
- les données ont la durée de vie de l’onglet
Déploiement d’une application sur un serveur Nginx
Démonisation du programme serveur avec PM2
PM2 est un process manager pour applications NodeJS
$ sudo npm install -g pm2
Pour lancer le processus serveur et l’ajouter à la liste des processus gérés par PM2 :
$ pm2 start server.js
...
[PM2] Spawning PM2 daemon with pm2_home=/Users/chris/.pm2
[PM2] PM2 Successfully daemonized
[PM2] Starting /Users/chris/workspaces/FULLSTACK/node-server/server.js in fork_mode (1 instance)
[PM2] Done.
┌──────────┬────┬──────┬───────┬────────┬─────────┬────────┬─────┬──────────┬───────┬──────────┐
│ App name │ id │ mode │ pid │ status │ restart │ uptime │ cpu │ mem │ user │ watching │
├──────────┼────┼──────┼───────┼────────┼─────────┼────────┼─────┼──────────┼───────┼──────────┤
│ server │ 0 │ fork │ 21716 │ online │ 0 │ 0s │ 0% │ 9.2 MB │ chris │ disabled │
└──────────┴────┴──────┴───────┴────────┴─────────┴────────┴─────┴──────────┴───────┴──────────┘
Use `pm2 show <id|name>` to get more details about an app
Visionnage en continu des logs :
$ pm2 log <id>
Configuration d’un VirtualHost pour Nginx
server {
listen 80;
server_name myapp.fr www.myapp.fr;
location = /favicon.ico { access_log off; log_not_found off; }
# default location - for directives 'location /' and 'location /index.html'
root /home/chris/workspaces/myproject/dist;
# static assets (images, videos, etc.)
location /static {
add_header Access-Control-Allow-Origin *;
root /var/www/static;
}
location /index.html {
# because the path to /index.html never changes, ETag is used to leverage browser-side caching
etag on;
}
location / {
# js, css etc. static files other than /index.html contain fingerprints (hashes) in their filenames
# so ETag is not needed. Cache them for 5 years
etag off;
add_header Cache-Control "max-age=315360000, immutable";
# for history mode vue app, all non-static requests should be redirected to index.html
try_files $uri $uri/ /index.html;
}
# feathersjs websocket route
location /socket.io {
proxy_pass http://localhost:3030;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}