:orphan: ========================================= Programmation coté client en Javascript ========================================= .. role:: lat .. role:: en .. highlight:: javascript Motivation ========== Architecture Client-Serveur --------------------------- .. figure:: client-server.png :height: 8ex Source image http://commons.wikimedia.org/wiki/File:Client-server-model.svg * Jusqu'à maintenant, le gros du travail était fait par le serveur. * On souhaite pouvoir déporter une partie de la **logique applicative** coté client. Programmation coté client ------------------------- * Ceci suppose d'avoir un langage de programmation *généraliste* (≠ HTML/CSS) compris par tous les navigateurs. * Actuellement, ce langage est Javascript. .. note:: Javascript a beaucoup évolué au cours de son histoire. La version que nous présentons ici est ES6, une version relativement récente (2015) qui a apporté beaucoup de nouveautés au langage, mais également des incompatibilités avec les versions précédentes. Gardez cela en tête lorsque vous trouverez des exemples en ligne. Syntaxe ======= Inspiration ----------- * Comme son nom l'indique, la syntaxe de Javascript est (librement) inspirirée de celle de Java (ou du C). * La similitude s'arrête là : Javascript n'est pas basé sur Java. Condition --------- .. container:: comparison .. code-block:: javascript if (i < 10) { j = j+1; k += i; } else { j = 0; } .. code-block:: python if i < 10: j = j+1 k += i else: j = 0 Boucles ------- .. container:: comparison .. code-block:: javascript while (i < 10) { j = j*i; i += 1; } .. code-block:: python while i < 10: j = j*i i += 1 .. container:: comparison .. code-block:: javascript for(let i of [1,1,2,3,5,8]) { j = j*i; } .. code-block:: python for i in [1,1,2,3,5,8]: j = j*i .. container:: comparison .. code-block:: javascript for(let i=2; i<1000; i=i*i) { console.log(i); } .. code-block:: python i = 2 while i<1000: print(i) i = i*i Fonctions --------- .. container:: comparison .. code-block:: javascript function fact(n) { let f = 1; while (n>1) { f = f*n; n -= 1; } return f; } .. code-block:: python def fact(n): f = 1 while n>1: f = f*n n -= 1 return f Exceptions ---------- .. container:: comparison .. code-block:: javascript if (i < 0) { throw new Error( "negative value"); } .. code-block:: python if i < 0: raise Exception( "negative value") .. container:: comparison .. code-block:: javascript try { i = riskyFunction(); } catch (err) { i = -1; } .. code-block:: python try: i = riskyFunction() except Exception as err: i = -1 Tableaux -------- .. container:: comparison .. code-block:: javascript let a = [4,1,3,6,4]; let i = 1; while (i 32) { // erreur: undefined n'a pas de longueur // ... } .. _dom: L'arbre DOM =========== Présentation ------------ La structure d'un fichier HTML peut être vue comme un *arbre*. .. graphviz:: graph { node [ shape=box, style=rounded ] html -- head head -- title -- title_txt html -- body body -- h1 -- h1_txt body -- p p -- p_txt p -- a -- a_txt body -- img a [ label="a\nhref='./link'" ] img [ label="img\nsrc='./pic'" ] title_txt [ shape=box, style=filled, label="Le titre" ] h1_txt [ shape=box, style=filled, label="Le titre" ] p_txt [ shape=box, style=filled, label="Bonjour le " ] a_txt [ shape=box, style=filled, label="monde" ] } Terminologie ------------ * Cet arbre s'appelle l'arbre DOM (pour :en:`Document Object Model`). * Les nœuds correspondant aux balises sont appelés des **éléments**. * Les nœuds contenant le contenu textuels sont simplement appelés des « nœuds texte ». .. note:: Il existe d'autres types de nœuds (par exemple les nœuds commentaire), mais ils sont plus rarement utiles. L'objet ``document`` -------------------- En JS, la variable globale ``document`` contient un objet représentant le document HTML. Elle permet d'accéder aux éléments du document : * `document.getElementById `_, * `document.getElementsByTagName `_, * `document.getElementsByClassName `_, * `document.querySelector `_, * `document.querySelectorAll `_ .. note:: Il est plus efficace d'utiliser les méthodes ``getElement*`` que d'utiliser ``querySelector*`` avec les sélecteurs correspondants (``tagname`` ou ``#identifier``). .. _attributs_elements: Attributs et méthodes d'un élément ---------------------------------- ``textContent``: permet de consulter *et modifier* le contenu textuel de l'élément ``style``: permet de consulter et modifier l'attribut ``style`` de l'élément, sous forme d'un objet ayant un attribut pour chaque propriété CSS. (:lat:`e.g.` ``e.style.fontSize`` pour la propriété ``font-size``) .. note:: Pour faciliter l'utilisation en Javascript, la typographie des attributs de ``style`` n'est pas la même que celle des propriétés CSS correspondantes. Les tirets (``-``) sont remplacés par une mise en majuscule de la letter suivante (`CamelCase `_). .. nextslide:: ``classList``: permet de consulter et modifier l'attribut ``class`` de l'élément, grâce aux méthodes suivantes : * ``add(cls)``: ajoute la classe `cls` a l'élément. * ``remove(cls)``: retire la classe `cls` a l'élément. * ``contains(cls)``: indique si l'élément possède actuellement la classe ``cls``. * ``toggle(cls)``: inverse l'état de la classe `cls` (présente/absente) sur l'élément. .. note:: Comme en HTML+CSS, il est préférable de spécifier la mise en forme à l'aide de classes dans le CSS, et de modifier ces classes dans le code Javascript, plutôt que la spécifier directement dans le code Javascript à travers l'attribute ``style``. .. nextslide:: Les éléments possèdent de nombreux autres attributs; en particulier, chaque attribut HTML a une contrepartie en Javascript. On peut notamment citer : * ``href`` (pour les ````) * ``src`` (pour les ````) * ``value`` (pour les ````) * ``disabled`` (pour tous les éléments de formulaire) * ``checked`` (pour les cases à cocher) * :lat:`etc`... Expérimentez sur `cet exemple`__. __ http://champin.net/enseignement/intro-js/_static/exemples/element_manipulation.html Parcours du DOM --------------- Récupérer des nœuds depuis un élément ``e``\  : * `e.getElementsByTagName `_, * `e.getElementsByClassName `_, * `e.querySelector `_, * `e.querySelectorAll `_, * `e.childNodes `_, * `e.children `_, * `e.parentNode `_, Modification du DOM ------------------- Création d'un nœud : * `document.createElement `_, * `document.createTextNode `_, * `n.cloneNode `_ Une fois créé, le nœud est encore *hors* de l'arborescence du document (et donc, non affiché). Il est nécessaire de le rattacher à un nœud parent par l'une des méthodes suivante : * `n.insertBefore `_, * `n.replaceChild `_, * `n.removeChild `_, * `n.appendChild `_ Intégration JS dans HTML ======================== Avertissement ------------- Il existe de nombreuses méthodes. Celle proposée ici vise à être simple et évolutive, mais suppose un navigateur moderne. Intégration d'un script à une page ---------------------------------- On include dans le HTML (dans le ``head`` ou le ``body``) une balise ``script`` ayant la structure suivante : .. code-block:: html Le script sera exécuté après le chargement complet du code HTML. Programmation événementielle ---------------------------- * En programmation impérative classique, la fonction principale (``main``) décrit dans quel ordre les différentes fonctions du programme doivent s'exécuter. * En programmation événementielle, on « abonne » chaque fonction à un (ou plusieurs) **événement(s)**. * La fonction s'exécute lorsqu'un événement auquel elle est abonnée se produit. * Les événement sont (souvent) liés aux interactions de l'utilisateur avec l'application. Quelques événements utiles -------------------------- * `click `_ * `mouseover `_ * `keypressed `_ * `input `_ * `change `_ * `submit `_ Des listes plus exhaustives sont disponibles `ici `_ et `là `_. .. note:: Souvent, et pour des raisons historiques, les noms des événements sont préfixés par ``on``. Attention : le véritable nom de l'événement n'inclut *pas* ce préfixe. Mise en œuvre ------------- .. code:: js function incrementCounter() { let i = document.getElementsByTagName("input")[0]; i.value = Number(i.value) + 1; } let b = document.querySelector("button"); b.addEventListener('click', incrementCounter); http://jsbin.com/mexuna/1/edit?html,js,output .. note:: * La méthode ``addEventListener`` associe un *comportement* (fonction) à un événement émis par un élément. * ⚠ Attention : le deuxième paramètre de ``addEventListener`` est le **nom** d'une fonction, **sans** parenthèses (ce n'est pas un *appel* de la fonction). Autre exemple ------------- .. code:: js function incrementCounter() { let b = document.getElementsByTagName("body")[0]; let p = document.createElement("p"); p.textContent = "Vous avez cliqué"; p.class.add("message"); b.appendChild(p); } let b = document.querySelector("button"); b.addEventListener('click', incrementCounter); Fonction anonyme ================ Objets de premier niveau ------------------------ * En Javascript, les fonctions sont des objets de *premier niveau*, ce qui signifie qu'ils peuvent être manipulés au même titre que, par exemple, les entiers, les tableaux ou les objets. * On peut par exemple - affecter une fonction à une variable ; - la passer en paramètre d'une autre fonction (par exemple ``addEventListener``) ; - l'affecter comme attribut à un objet (ce qui est une manière de doter un objet de méthodes, comme en Python). Fonctions anonymes ------------------ * Javascript autorise la création de fonctions *anonymes* :: function (param1, param2) { /* instructions here */ } * Contrairement à une *déclaration* de fonction, une fonction anonyme peut apparaître *partout* ou une fonction est acceptée. * C'est utile dans les cas ou cette fonction n'a pas vocation à être réutilisée ailleurs, en particulier pour les abonnements à des événements. .. nextslide:: Exemple :: let b = document.querySelector("button"); b.addEventListener('click', function() { let i = document.getElementsByTagName("input")[0]; i.value = Number(i.value) + 1; }); Fonctions "flêches" ------------------- En ES6, il existe une syntaxe plus compacte pour les fonctions anonymes. Au lieu d'écrire :: function (x, y) { return x+y; } on peut utiliser la notation :: (x, y) => { return x+y; } Dans le cas où la fonction comporte une unique instruction ``return``, on peut même remplacer le corps de la fonction par l'expression à retourner :: (x, y) => x+y .. nextslide:: Dans le cas où la fonction comporte exactement un argument, on peut ommettre les parenthèses autour de l'argument :: x => { console.log(x); return x+1; } Autres exemples :: x => x+1 .. code:: () => { console.log("Fonction flêche sans argument,"); console.log("et comportant plusieurs instructions."); } .. nextslide:: Adaptation de l'exemple précédent :: let b = document.querySelector("button"); b.addEventListener('click', () => { let i = document.getElementsByTagName("input")[0]; i.value = Number(i.value) + 1; }); .. note:: Les fonctions "flêches" ne sont pas absolument équivalentes aux fonctions anonymes, mais la distinction concerne des notions non abordées dans ce chapitre. Pour en savoir plus : https://stackoverflow.com/a/34361380 Modularité grâce aux fonctions anonymes --------------------------------------- Les variables déclarées hors de toute fonction, **même préfixées** par ``let``, sont considérées par Javascript comme des variables globales. Afin de contourner ce problème, il existe en Javascript une convention : on "enferme" tout le code du script dans une fonction anonyme, que l'on appelle immédiatement :: (function() { // ... mon code Javascript ici ... })(); Ainsi, on évite de polluer l'environnement global. .. _exemple_setTimeout: Autre exemple d'utilisation de fonctions anonymes ------------------------------------------------- .. code-block:: javascript let b1 = document.querySelector('button#b1'); b1.addEventListener("click", () => { let previousContent = b1.textContent; b1.textContent = "(en attente)"; b1.disabled = true; setTimeout(() => { alert("message"); b1.textContent = previousContent; b1.disabled = false; }, 2000); }); `Voir et modifier cet exemple.`__ __ http://jsbin.com/vojojag/3/edit?html,js,output Exécution asynchrone ==================== Motivation ---------- Dans l'`exemple précédent`__, pourquoi utiliser la fonction ``setTimeout`` et non une fonction qui bloquerait l'exécution pendant 2s, comme on le ferait (par exemple) en Python, à l'image de l'exemple ci-dessous ? __ exemple_setTimeout_ .. code:: // ⚠ MAUVAIS EXEMPLE ⚠ let previousContent = b1.textContent; b1.textContent = "(en attente)"; b1.disabled = true; sleep(2000); // fonction imaginaire (n'existe pas en JS) alert("message"); b1.textContent = previousContent; b1.disabled = false; .. note:: La fonction ``sleep`` n'existe pas réellement en Javascript, et pour cause puisque ce n'est pas la bonne manière de faire. .. nextslide:: * Réponse : parce que le navigateur ne peut pas faire deux choses à la fois dans une page : gérer l'affichage du HTML et exécuter le code Javascript. * Pendant qu'une fonction Javascript s'exécute, la page est totalement "gelée" (saisie, défilement...). * Si la fonction garde la main pendant une durée trop longue, ce phénomène sera perceptible, et dégradera l'expérience utilisateur. Pour aller plus loin -------------------- http://latentflip.com/loupe/ Loupe vous permet de visualiser la manière dont les événements sont gérés en Javascript. La vidéo qui sert d'introduction est également une bonne introductions aux mécanismes mis en œuvre. Portée et fermeture =================== Portée (:en:`scope`) -------------------- * La **portée** d'une variable est la partie du code sur laquelle cette variable est définie. * Pour les variables globales, c'est l'ensemble des scripts exécutés par la page (y compris les scripts écrits par d'autres). * Pour une variable locale (``let``), c'est le bloc entre accolades dans lequel elle est déclarée. * Une variable locale d'une fonction ``f`` est donc accessible par les fonctions (anonymes ou non) définies *à l'intérieur* de ``f``. Fermeture (:en:`closure`) ------------------------- * Une fonction définie à l'intérieur d'une autre porte avec elle le *contexte* dans lequel elle a été créé, c'est à dire l'état des variables locales qui lui sont accessibles. * Une telle fonction assortie d'un contexte est appelée une **fermeture**. * Dans l'`exemple précédent`__, la variable ``b1`` est définie au chargement du script, mais « survit » à cette fonction, puisqu'elle est réutilisée au clic sur ce bouton, et encore deux secondes plus tard, dans le :en:`callback` de la fonction ``setTimeout``. * Visualisez l'exécution pas à pas d'un autre exemple de fermeture sur pythontutor__. __ exemple_setTimeout_ __ http://pythontutor.com/visualize.html#code=//%20patron%20de%20Crockford%0A%0Afunction%20make_point%28%29%20%7B%0A%20%20let%20x%20%3D%200%3B%0A%20%20let%20y%20%3D%200%3B%0A%20%20return%20%7B%0A%20%20%20%20get_x%3A%20%28%29%20%3D%3E%20%7B%0A%20%20%20%20%20%20return%20x%3B%0A%20%20%20%20%7D,%0A%20%20%20%20get_y%3A%20%28%29%20%3D%3E%20%7B%0A%20%20%20%20%20%20return%20y%3B%0A%20%20%20%20%7D,%0A%20%20%20%20translate%3A%20%28dx,%20dy%29%20%3D%3E%20%7B%0A%20%20%20%20%20%20x%20%2B%3D%20dx%3B%0A%20%20%20%20%20%20y%20%2B%3D%20dy%3B%0A%20%20%20%20%7D,%0A%20%20%7D%0A%7D%0A%0Alet%20p%20%3D%20make_point%28%29%3B%0Ap.translate%281,2%29%3B%0Aconsole.log%28p.get_x%28%29,%20p.get_y%28%29%29%3B&cumulative=false&curInstr=16&heapPrimitives=false&mode=display&origin=opt-frontend.js&py=js&rawInputLstJSON=%5B%5D&textReferences=false Gestion avancée d'événements ============================ Cheminement d'un événement -------------------------- .. figure:: event-flow.* Source : http://www.w3.org/TR/DOM-Level-3-Events/#event-flow .. nextslide:: * La plupart des événements se *propagent* dans l'arbre DOM. * Il est donc possible de s'abonner à tous les événements se produisant *à l'intérieur* d'un élément (et pas uniquement *sur* cet élément). * Par défaut, les :en:`listeners` dont déclenchés à la remontée de l'événement. .. note:: On peut également forcer un listener à se déclencher à la descente (:en:`capture` d'un événement) plutôt qu'à sa remontée (:en:`bubbling`). Pour cela on passera ``true`` en troisème paramètre de `addEventListener `_. .. _parametre-listener: Paramètre d'un :en:`listener` ----------------------------- * Le :en:`listener` reçoit en paramètre un objet `événement `_, contenant notamment les attributs suivants : + ``target``: l'élément le plus spécifique concerné par l'événement ; + ``type``: le type d'événement (``click``, ``mouseover``...) .. nextslide:: .. code-block:: javascript document.getElementsByTagName("body")[0] .addEventListener("click", (evt) => { let msg = document.getElementById("msg"); if (evt.target.tagName === "BUTTON") { msg.textContent = "Vous avez cliqué sur " + evt.target.textContent; } else { console.log(evt.target); msg.textContent = "Vous avez raté les boutons..."; } }); `Voir et modifier cet exemple`__ __ http://jsbin.com/zifogo/9/edit?html,js,output