Haxe pour le développement Web

Magazine
Marque
GNU/Linux Magazine
Numéro
185
Mois de parution
septembre 2015
Spécialité(s)


Résumé
Haxe est un langage de programmation multi-plateformes permettant aussi bien le développement d'applications mobiles que Web. Dans ce dernier cas, par rapport au PHP, il présente l'intérêt d'être compilé et de permettre à partir d'un seul langage de générer à la fois du PHP et du Javascript.

Body


Haxe [1] est un langage de programmation libre crée en 2005 par Nicolas Canasse et la société Motion Twin [2], une société éditrice de jeux vidéos implantée à Bordeaux. Il est multi-plateformes et indépendant de l'environnement d'exécution : il peut être compilé notamment en C++, C#, PHP, Javascript, Flash, Java et Python ; ainsi, le code source d'un jeu Flash est portable pour le Web (HTML5), Android, IOS, …

Haxe utilise une syntaxe qui sera familière aux développeurs C, Java, Javascript, … C'est un langage orienté objet, avec typage strict et inférence de type.

L'intérêt de Haxe pour le développement d'applications Web est d'utiliser le même langage côtés serveur et client, ce qui permet la réutilisation de code. De plus, le fait que ce soit un langage compilé facilite également la mise au point du programme (de nombreuses erreurs sont détectées dès de la compilation).

1. Installation des outils

Le développement d'une application Web en langage Haxe suit le processus suivant :

1) le développeur code son programme en langage Haxe (fichiers *.hx) et HTML (pour les gabarits) ;

2) le compilateur génère du PHP et / ou du Javascript ;

3) le code généré est déployé sur le serveur Web.

Pour installer Haxe manuellement (cette application requiert la version 3.x), télécharger les binaires (http://haxe.org/download), extraire les fichiers de l'archive puis les installer avec les commandes suivantes  :

# cd haxe-3.2.0/

# mkdir /usr/lib/haxe

# mv * /usr/lib/haxe/

# ln -sf /usr/lib/haxe/haxelib /usr/bin/

# ln -sf /usr/lib/haxe/haxe /usr/bin/

Pour l'hébergement de l'application Web, il suffit d'un serveur Web supportant le PHP (Apache par exemple) et les bases de données MySQL / MariaDB ou Sqlite3.

Pour éditer le code source, Geany supporte le langage Haxe par défaut. Pour utiliser un autre éditeur ou environnement de développement intégré (IDE), se référer à [3].

2. L'application de gestion de contacts

L'application sera développée en s'inspirant de l'architecture MVC (Modèle-Vue-Contrôleur) et du style d'architecture ReST (Representational State Transfer).

2.1 Organisation des fichiers

L'organisation des fichiers est à la discrétion du programmeur, mais dans le cadre de ce tutoriel, il est proposé de mettre en place l'arborescence suivante :

├── compile.hxml

├── source/

│   ├── Index.hx

│   ├── controlers/

│   │   └── CtrlContact.hx

│   └─── models/

│       └── Contact.hx

── tpl/

├── styles.css

├── contact-edit.html

    └── contacts-list.html

Le fichier compile.hxml contient les directives de compilation ; son contenu est le suivant :

-cp source

-main Index

-php export

-resource tpl/contact-edit.html@contact-edit

-resource tpl/contacts-list.html@contacts-list

La directive cp indique l'emplacement du code source, la directive main définit le nom de la classe contenant le point d'entrée du programme et la directive php le type et le nom du dossier contenant le code compilé.

Dans le cadre d'une application Web, la directive resource permet quant-à elle de lister les gabarits HTML utiles au programme ; d'autres types de ressources seraient également envisageables, comme des fichiers de langue par exemple (pour internationaliser l'application) ; les feuilles de style ne sont pas des ressources car le code du programme Haxe n'a pas besoin d'y faire référence. La syntaxe pour définir une ressource est : chemin_fichier@identifiant (voir [4] pour la documentation détaillée).

2.2 Le modèle

C'est le rôle du modèle de prendre en charge la persistance des données. Ici, elles seront stockées dans une base de données (MySQL / MariaDB ou SQLite3 au choix), en utilisant le mappage relationnel-objet via la bibliothèque SPOD (Simple Persistent Object Database) de Haxe (voir [5] pour la documentation détaillée).

Le mappage relationnel-objet

Le mappage relationnel-objet (ORM – Object Relationnal Mapping) est une technique de programmation qui concilie le paradigme de la programmation orientée objet avec les bases de données relationnelles (SGBD-R) en établissant des correspondances entre classes et propriétés avec tables et champs.

Le développeur se focalise sur les concepts de la programmation orientée objet, sans se soucier de la base de données sous-jacente, l'ORM gérant automatiquement les requêtes SQL (Structured Query Language).

Des métadonnées permettent de spécifier des informations comme la clé primaire de la table, les clés étrangères, etc.

Avec Haxe, la mise en œuvre de l'ORM consiste à :

- créer une classe qui décrit les caractéristiques d'un objet (ici un contact a un identifiant, un nom, un prénom et une adresse électronique) ;

- hériter de la classe Object (du paquetage sys.db) ;

- préciser des métadonnées pour les concepts relevant du SGBD (comme la clé primaire).

package models;

import sys.db.Object;

import sys.db.Types;

using StringTools;

@:id(id) //métadonnée définissant le champ clé primaire de la table

class Contact extends Object {

 public var id(default,null) : SId; //'Int' auto-incrémenté

 public var firstname(default,set) : String;

 public var lastname(default,set) : String;

 public var email(default,set) : String;

    

 public function new(firstname : String, lastname : String, ?email : String = "") {

  super();

  this.firstname = firstname;

  this.lastname = lastname;

  this.email = email;

 }

 function set_firstname(firstname : String) : String {

  if (null == id || ("" != firstname && null != firstname))

   this.firstname = firstname.htmlEscape();

  return this.firstname;

 }

 function set_lastname(lastname : String) : String {

  if (null == id || ("" != lastname && null != lastname))

   this.lastname = lastname.htmlEscape();

  return this.lastname;

 }

 function set_email(email : String) : String {

  var regexp = ~/^[a-z0-9._%-]+@[a-z0-9.-]+\.[a-z][a-z]+$/i;

  if ("" == email || regexp.match(email)) this.email = email;

  return this.email;

 }

}

La classe Object (dont hérite la classe Contact) définit les méthodes insert, update et delete (implémentant les requêtes SQL correspondantes) et la propriété à portée classe (statique) manager (de la classe sys.db.Manager) ; cette dernière dispose notamment des méthodes all et get permettant respectivement d'obtenir la liste des enregistrements de la table ou seulement un seul (à partir de sa clé primaire). C'est le contrôleur qui fait usage de ces méthodes.

Remarquez que le constructeur (fonction new) de la classe n'initialise pas l'identifiant (id) : ce dernier l'est automatiquement lors de l'insertion de l'enregistrement dans la base de données.

2.3 Routage et contrôleur

Le modèle à lui seul ne fait rien ; c'est le contrôleur qui pilote le programme en traitant les requêtes HTTP.

2.3.1 Point d'entrée du programme

Le fichier source/Index.hx contient le point d'entrée du programme (la fonction main), dont les actions consistent (généralement) à :

- initialiser SPOD (le mappage relationnel-objet) et se connecter à la base de données ;

- créer les tables si nécessaire (avec ici un jeu d'essai) ;

- déclencher le routage en fonction des paramètres de l'URL (voir [6] pour la documentation détaillée).

Note sur la déclaration des propriétés en Haxe

En Haxe, lorsque une propriété est publique, les paramètres entre parenthèses permettent d'affiner les droits d'accès et de définir des accesseurs en lecture et écriture :

- le premier restreint la visibilité en consultation et peut avoir pour valeur null (consultation interdite), default (consultation autorisée) ou get (la valeur est retournée par la fonction get_nom_propriété) ;

- le second restreint la visibilité en modification et peut avoir pour valeur nulldefault ou set.

Notez que public var nom_propriété(default,default) est identique à  public var nom_propriété.

Dans l'exemple, la propriété lastname peut être consultée telle qu'elle mais sa modification est contrôlée par une fonction qui force une valeur non nulle dès lors que le contact a été enregistré dans la base de données.

Voici donc le code du fichier source/Index.hx :

import sys.db.Manager;

import php.db.PDO;

import sys.db.TableCreate;

import php.Web;

import haxe.web.Dispatch;

import haxe.ds.StringMap; //tableau associatif

import models.Contact;

class Router {

 public function new() {} //il n'y a rien de particulier à faire ici

 function doDefault() {

  Web.redirect("?/contacts");

 }

 function doContacts(?id : Int = null) {

  new controlers.ContactCtrl(id);

 }

}

class Index { //cf paramètre '-main' du fichier 'compile.hx'

 public static function main() {

  Manager.initialize(); //initialisation de SPOD

  //connexion à la base de données; choisir entre SQLite / MySQL:

  Manager.cnx = PDO.open("sqlite:" + Web.getCwd() + "data/db.sqlite");

  //~ Manager.cnx = PDO.open("mysql:host=localhost;dbname=nom_bd", "login", "mdp");

  if (! TableCreate.exists(Contact.manager)) { //création des tables

   TableCreate.create(Contact.manager);

   var c : Contact;

   c = new Contact("Leto", "Atreides", "leto@atreides.dune");

   c.insert(); //insert into Contact ...

   c = new Contact("Liet", "Kynes");

   c.insert();

  }

  try {

   //déclenchement du routage; on passe en paramètre:

//- les paramètres de l'URL (ce qui suit le '?'), cf 'Web.getParamsString()',

//-et une instance de la classe chargée du routage, ici 'Router':

   Dispatch.run(Web.getParamsString(), new StringMap(), new Router());

  } catch (e : DispatchError) {

   Web.setReturnCode(400); //bad request

  }

               manager.cnx.close();

 }

}

La classe Router (qui pourrait avoir un autre nom) est ici responsable du routage ; le routage dépend du premier paramètre transmis à la méthode run de la classe Dispatch ; il s'agit en général (et en particulier si on s'inspire du style d'architecture ReST) des paramètres de l'URL (ce qui suit le ?) ;

- la requête http://www.monserveur.org/index.php (chaîne vide après le ?) déclencherait l'exécution de la fonction doDefault,

- avec la requête index.php?/contacts/5, c'est /contacts/5 qui est transmis au routeur, ce qui déclenche l'exécution de la méthode doContacts (attention à la casse) et le passage de 5 dans le paramètre (facultatif) id ; remarquez que les paramètres de l'URL ne sont pas au format x-www-form-urlencoded.

Une exception est levée si, par exemple, l'utilisateur passe en paramètre de l'URL :

- /contacts/duncan car duncan devrait être de type entier (cf profil de la méthode),

- /agenda/… car il n'y a pas de méthode doAgenda …

2.3.2 Gestion des requêtes concernant les contacts

C'est le constructeur de la classe CtrlContact (dont le nom a été choisi arbitrairement) qui prend en charge les requêtes concernant les contacts. Plusieurs types de requêtes sont possibles (cf CRUD, pour « Create, Retrieve, Update, Delete »), les plus courants étant l'affichage de la liste ou d'un élément ainsi que la création, la mise à jour et la suppression d'un élément. Il faut par conséquent un moyen de distinguer ces différentes requêtes ; la solution retenue ici (mais d'autres sont possibles) consiste à utiliser la valeur de l'identifiant, la présence ou non de données dans le corps de la requête HTTP et la méthode HTTP utilisée (POST pour les modifications et GET pour toutes les requêtes qui permettent d'obtenir des informations). Le schéma du routage proposé est le suivant :

Requête utilisateur

Méthode HTTP et URL

Corps requête HTTP

Afficher la liste des contacts

GET ?/contacts


Saisir un nouveau contact

GET ?/contacts/ 


Afficher (et saisir) un contact

GET ?/contacts/id_contact


Créer un nouveau contact

POST ?/contacts/0

firstname,lastname, email

Mettre à jour un contact

POST ?/contacts/id_contact

firstname,lastname, email

Supprimer un contact

POST ?/contacts/id_contact


La distinction entre mise-à-jour et suppression se fait donc selon l'absence ou la présence de données dans le corps de la requête.

Ce contrôleur traitera les requêtes de la façon suivante :

- dans le cas d'une consultation : récupération du ou des contact(s) à partir du modèle et transmission à la vue (ici un moteur de gabarits) ;

- dans le cas d'une modification : récupération des données de la requête, mise à jour des enregistrements de la table Contact (via le modèle) et redirection de l'utilisateur vers une page de consultation (ce qui pourrait à juste titre être considéré comme un gaspillage de ressources).

Le corps de la requête est une chaîne au format x-www-form-urlencoded de la forme  firsname=Guerney&lastname=Halleck&email= ; cette chaîne, comme en PHP, est analysée et convertie en tableau associatif (cf variable $_POST) par la méthode getParams de la classe Web. Pour accéder aux différents éléments de ce tableau associatif (un StringMap en Haxe), on utilise la méthode get avec le nom du champ en paramètre (exemple : data.get("firstname")).

Le code du fichier source/controlers/ContactCtrl.hx est par conséquent le suivant :

package controlers;

import haxe.Template;

import haxe.Resource;

import haxe.ds.StringMap; //tableaux associatifs

import php.Web;

import models.Contact;

class ContactCtrl {

 public function new(id : Int) {

  switch (Web.getMethod()) {

   case "GET":

    if (null == id) retrieveAll();

    else retrieveItem(id);

   case "POST":

    if (0 == id) create(Web.getParams());

    else { //'Web.getPostData' est le corps « brut » de la requête :

     if (0 == Web.getPostData().length) delete(id);

     else update(id, Web.getParams());

    }

  }

 }

 function create(data : StringMap<String>) {

  var c = new Contact(data.get("firstname"), data.get("lastname"), data.get("email"));

  if (0 == c.firstname.length || 0 == c.lastname.length) {

   Web.setReturnCode(400); //bad request

  } else {

   c.insert(); //insert into Contact …

   Web.redirect("?/contacts");

  }

 }

 function retrieveItem(id : Int) {

  var data : Contact;

  if (0 == id) data = new Contact("", "");

  else data = Contact.manager.get(id); //select * from Contact where id = …

  if (null == data) Web.setReturnCode(404); //not found

  else { //initialisation du moteur de gabarits avec le code HTML du fichier

   //'tpl/contact-edit.html' (cf ressource définie dans 'compile.hxml')

   var tpl = new Template(Resource.getString("contact-edit"));

   Sys.println(tpl.execute(data)); //fusion des données avec le HTML

  }

 }

 function retrieveAll() {

  var data : List<Contact> = Contact.manager.all(); //select * from Contact

  var tpl = new Template(Resource.getString("contacts-list"));

  //on nomme la liste des contacts pour pouvoir y faire référence dans le gabarit:

  Sys.println(tpl.execute({ theContacts : data }));

 }

 function update(id : Int, data : StringMap<String>) {

  var c : Contact = Contact.manager.get(id);

  if (null == c) Web.setReturnCode(404); //not found

  else {

   c.firstname = data.get("firstname"); //(ne fait rien si la chaîne est vide)

   c.lastname = data.get("lastname");

   c.email = data.get("email"); //(ne fait rien si non valide)

   c.update(); //update Contact … where id = …

   Web.redirect("?/contacts");

  }

 }

 function delete(id : Int) {

  var c : Contact = Contact.manager.get(id);

  if (null == c) Web.setReturnCode(404); //not found

  else {

   c.delete(); //delete from Contact where id = …

   Web.redirect("?/contacts");

  }

 }

}

2.4 Les gabarits (templates)

Il ne manque plus que la vue, constituée de gabarits HTML pour terminer l'application ; deux écrans sont prévus : le premier pour afficher la liste des contacts, le second pour afficher et éditer un contact. Cette partie est très simple et les codes seront présentés puis rapidement expliqués.

2.4.1 Liste des contacts

Contenu du fichier tpl/contacts-list.html :

<!DOCTYPE html>

<html lang="fr"><head>

 <title>Gestion des contacts</title><meta charset="UTF-8"/>

 <link rel="stylesheet" type="text/css" href="styles.css"/>

</head><body>

 <header><h1>Liste des contacts</h1></header>

 <section>

  <table><thead>

   <tr>

    <th>Prénom</th><th>Nom</th>

    <th><a class="button" href="?/contacts/0">Nouveau</a></th>

   </tr>

  </thead><tbody>

   ::foreach theContacts:: <!-- cf CtrlContact.retrieveAll -->

   <tr>

    <td>::firstname::</td><td>::lastname::</td>

    <td>

     <a class="button" href="?/contacts/::id::">Détail</a>

     <form method="POST" action="?/contacts/::id::"

onsubmit="return confirm('Supprimer ::firstname:: ::lastname::')">

      <input type="submit" value="Supprimer"/>

     </form>

    </td>

   </tr>

   ::end::

  </tbody></table>

 </section>

</body></html>

Les directives destinées au moteur de gabarits (voir [7] pour la documentation détaillée) sont ici :

- ::foreach nomListe:: code HTML ::end:: qui permet de dupliquer le code HTML pour chaque élément de la liste en paramètre ; à l'intérieur de cette boucle, l'élément traité est un objet de la classe Contact.

- ::nom_propriété:: remplacé par la valeur de la propriété de l'objet.

2.4.2 Affichage et édition d'un contact

Contenu du fichier tpl/contact-edit.html :

<!DOCTYPE html>

<html lang="fr"><head>

 <title>Gestion des contacts</title><meta charset="UTF-8"/>

 <link rel="stylesheet" type="text/css" href="styles.css"/>

</head><body>

 <header><h1>Édition d'un contact</h1></header>

 <section>

  ::if (null == id)::

  <form action="?/contacts/0" method="post">

  ::else::

  <form action="?/contacts/::id::" method="post">

  ::end::

   <fieldset>

    <label for="firstname">Prénom :</label>

    <input id="firstname" name="firstname" type="text"

required="required" value="::firstname::"/>

   </fieldset>

   <!--idem pour 'lastname'-->

   <fieldset>

    <label for="email">Mél :</label>

    <input id="email" name="email" type="email" value="::email::"/>

   </fieldset>

   <fieldset>

    <a class="button" href="?/contacts">Retour</a>

    <input type="submit" value="Enregistrer">

   </fieldset>

  </form>

 </section>

</body></html>

Il est possible d'effectuer des tests conditionnels à l'aide d'une instruction de la forme ::if (condition):: ... ::else:: ... ::end::.

2.4.3 Feuille de style

Cette feuille de style est minimaliste ; elle permet notamment d'afficher les liens sous la forme de boutons :

table, th, td { border: 1px solid; border-collapse: collapse; }

td form { display: inline; }

h1 { font-size: 14pt; text-decoration: underline; }

a.button { border-radius: 3px; border: 1px solid #aaaaaa; padding: 3px 10px;

 background: linear-gradient(#eeeeee, #dddddd); color: #222222;

 text-decoration: none; font-family: sans; font-size: 80%; font-weight: normal;

 display: inline-block; }

a.button:hover { background: linear-gradient(#ffffff, #eeeeee); }

fieldset { border: none; padding: 0.25em 0; }

label { display: inline-block; width: 100px; }

2.5 Compilation et déploiement

Tous les fichiers nécessaires à l'application étant terminés, il est temps de la compiler et de la tester :

$ haxe compile.hxml

$ mkdir export/data; chmod a+w export/data #uniquement si la base de données est SQLite

$ cp -f tpl/styles.css export/

L'étude du contenu du dossier export (à téléverser sur le serveur Web) permet de comprendre le travail réalisé par le compilateur :

- le fichier index.php fait référence à la méthode main de la classe Index, point d'entrée du programme ;

- le dossier lib contient le code de l'application et des classes Haxe utilisées (compilé en PHP) ;

- le dossier res contient les fichiers ressources.

Conclusion

Ce premier article sur le développement Web avec Haxe a permis de découvrir le fonctionnement de la bibliothèque standard pour le mappage relationnel-objet, le routage d'URL et le moteur de gabarits. Ce qui est appréciable, c'est la rigueur du langage (le typage strict notamment), et l'étape de compilation qui révèle notamment les erreurs de noms de variables (ou d'utilisation de variables non initialisées).

La prochaine étape serait de mettre en œuvre le principe de l'amélioration progressive pour exploiter AJAX (Asynchronous Javascript And XML) si le navigateur le permet, sans changer de langage de programmation.

Références

[1] Site du langage Haxe : http://haxe.org

[2] Site de la société Motion-Twin : http://motion-twin.com

[3] Editeurs et IDE pour le langage : http://haxe.org/documentation/introduction/editors-and-ides.html

[4] Documentation sur l'utilisation des ressources : http://haxe.org/manual/cr-resources.html

[5] Documentation sur les macros SPOD : http://old.haxe.org/manual/spod

[6] Documentation sur le répartiteur d'URL : http://old.haxe.org/manual/dispatch

[7] Documentation sur le moteur de gabarits : http://haxe.org/manual/std-template.html




Article rédigé par

Par le(s) même(s) auteur(s)

Live-System from scratch

Magazine
Marque
GNU/Linux Magazine
Numéro
202
Mois de parution
mars 2017
Spécialité(s)
Résumé
Un système vif (live-system) avec données persistantes permet de démarrer, avec une simple clé USB, n’importe quel ordinateur comme si c’était le sien. Cet article présente les étapes de la construction d’un tel système, à partir de zéro.

Introduction à Haxe-NodeJS

Magazine
Marque
GNU/Linux Magazine
Numéro
201
Mois de parution
février 2017
Spécialité(s)
Résumé
Haxe est un langage de programmation permettant – entre autres – le développement d'applications web compilées en PHP ou en JavaScript (pour le navigateur ou Node.js). Il présente l'intérêt d'avoir un typage strict, ce qui permet la détection de certaines erreurs dès la compilation.

Service Web avec Haxe

Magazine
Marque
GNU/Linux Magazine
Numéro
187
Mois de parution
novembre 2015
Spécialité(s)
Résumé
Un premier article a présenté la programmation avec le langage Haxe d'une application Web compilée en PHP. Cet article propose d'étudier comment Haxe permet de réutiliser une partie du code pour développer et tester un service Web.

Les derniers articles Premiums

Les derniers articles Premium

Quarkus : applications Java pour conteneurs

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Initié par Red Hat, il y a quelques années le projet Quarkus a pris son envol et en est désormais à sa troisième version majeure. Il propose un cadre d’exécution pour une application de Java radicalement différente, où son exécution ultra optimisée en fait un parfait candidat pour le déploiement sur des conteneurs tels que ceux de Docker ou Podman. Quarkus va même encore plus loin, en permettant de transformer l’application Java en un exécutable natif ! Voici une rapide introduction, par la pratique, à cet incroyable framework, qui nous offrira l’opportunité d’illustrer également sa facilité de prise en main.

De la scytale au bit quantique : l’avenir de la cryptographie

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Imaginez un monde où nos données seraient aussi insaisissables que le célèbre chat de Schrödinger : à la fois sécurisées et non sécurisées jusqu'à ce qu'un cryptographe quantique décide d’y jeter un œil. Cet article nous emmène dans les méandres de la cryptographie quantique, où la physique quantique n'est pas seulement une affaire de laboratoires, mais la clé d'un futur numérique très sécurisé. Entre principes quantiques mystérieux, défis techniques, et applications pratiques, nous allons découvrir comment cette technologie s'apprête à encoder nos données dans une dimension où même les meilleurs cryptographes n’y pourraient rien faire.

Les nouvelles menaces liées à l’intelligence artificielle

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Sommes-nous proches de la singularité technologique ? Peu probable. Même si l’intelligence artificielle a fait un bond ces dernières années (elle est étudiée depuis des dizaines d’années), nous sommes loin d’en perdre le contrôle. Et pourtant, une partie de l’utilisation de l’intelligence artificielle échappe aux analystes. Eh oui ! Comme tout système, elle est utilisée par des acteurs malveillants essayant d’en tirer profit pécuniairement. Cet article met en exergue quelques-unes des applications de l’intelligence artificielle par des acteurs malveillants et décrit succinctement comment parer à leurs attaques.

Les listes de lecture

9 article(s) - ajoutée le 01/07/2020
Vous désirez apprendre le langage Python, mais ne savez pas trop par où commencer ? Cette liste de lecture vous permettra de faire vos premiers pas en découvrant l'écosystème de Python et en écrivant de petits scripts.
11 article(s) - ajoutée le 01/07/2020
La base de tout programme effectuant une tâche un tant soit peu complexe est un algorithme, une méthode permettant de manipuler des données pour obtenir un résultat attendu. Dans cette liste, vous pourrez découvrir quelques spécimens d'algorithmes.
10 article(s) - ajoutée le 01/07/2020
À quoi bon se targuer de posséder des pétaoctets de données si l'on est incapable d'analyser ces dernières ? Cette liste vous aidera à "faire parler" vos données.
Voir les 64 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous