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

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 : &nbsp;
<p>Vive les mariés&nbsp;!</p>
  • < : &lt;
  • > : &gt;
  • & : &amp;
  • " : &quot;
  • ' : &apos;
  • ¢ : &cent;
  • £ : &pound;
  • ¥ : &yen;
  • : &euro;
  • © : &copy;
  • ® : &reg;

Caractères Unicode par codepoint

Exemple de caractère chinois avec codepoint U+54F4 :

<p>&#x54F4;</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 1in
  • pt : points, pour les supports imprimés ; 1pt = 1/72e de 1in

Relatives

  • em, taille de fonte de l’élément parent
  • rem (root em), taille de fonte de l’élément racine du document
  • %, relative à l’élément parent, sa taille de fonte ou ses dimensions
  • vw, 1% de la largeur du viewport
  • vh, 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és
  • sans-serif : pas de détails aux extrémités
  • monospace : tous les caractères occupent la même largeur, idéal pour afficher du code
  • cursive et fantasy, peu utilisées
Les serifs sont les petits détails situés aux extrémités des caractères.

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 disponible
  • contains: 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 ou landscape) 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 ?

https://node.green/

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) :

  1. 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
  2. La fonction constructrice Type est appelée avec les arguments fournis, this étant lié au nouvel objet créé.
  3. 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 de obj (sans ordre garanti)
  • Object.values(obj) : renvoie la liste des valeurs des propriétés propres de obj (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 avec n emplacements vides, c’est à dire avec la propriété length à n
  • propriété length : quand un élément est ajouté au tableau à la position i, length prend la valeur i+1 si i >= length
  • méthode includes(elt) : renvoie un booleéen indiquant si elt 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 par sep
  • méthode slice(start, end) : renvoie un objet tableau, contenant une copie superficielle de la portion du tableau original entre l’indice start (inclus) et l’indice end (exclu). Le tableau original n’est pas modifié
  • méthode fill(val) : remplit tous les éléments du tableau avec la valeur val
  • 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 comparaison func
  • méthode splice(i, n) : supprime sur place n éléments dans le tableau à partir de la position i
  • méthode concat(a1[, a2,...]) : renvoie un nouveau tableau constitué de ce tableau concaténé avec a1, 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 : ‘not expression
  • expression1 && expression2 : expression1 and expression2
  • expression1 || expression2 : expression1 or expression2

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 courant
  • module : référence au module courant
    • module.exports : définit ce qu’un module exporte, accessible par require()

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éussi
  • Promise.resolve(valeur) : renvoie une promesse qui réussi en renvoyant valeur
  • Promise.reject(err) : renvoie une promesse qui échoue avec l’erreur err

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

https://2020.stateofjs.com

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 simplement document, contient le DOM de cet onglet
  • window.innerHeight, window.innerWidth : dimensions de la zone d’affichage
  • window.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 document
  • document.body : élément <body> du document
  • document.URL : url du document
  • document.cookies : cookies du document
  • document.domain : domain name du document
  • document.lastModified : date de dernière modification du document

  • document.title : titre du document
  • document.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énements typeEvent ; à chaque occurence d’un événement de ce type, la fonction callbacFunc 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

  1. Le client (navigateur) fait une requête d’accès à une ressource auprès d’un serveur Web selon le protocole HTTP
  2. Le serveur vérifie la demande, les autorisations et effectue la tache demandée. Il envoie en retour une réponse au client.
  3. 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ête

    Content-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éponse

    Content-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; 
--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

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 –> ‘$2b1010Mc07H0wBqBL9a6g32ecig.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

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 et color 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 par counter.value et color.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 :

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és props 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ètres param1, …, il faut appeler context.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 de props (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;
    }
}

Technologies émergeantes

PWA

Webassembly

Web components