VA
Comment lire ce livre ? Qui sommes nous ?
I. PAGES WEB
II. SERVEUR WEB
III. BASES DE DONNEES
IV. INTERACTION FRONT-BACK
Annexe I. ENSEIGNEMENT NSI
Annexe II. LES FRAMEWORKS

Génération des pages dans le front - le single page

Cette annexe présente l'approche dite single page dont l'objectif est de ne charger qu'une seule et unique page web durant l'intégralité du parcours de l'utilisateur. Le navigateur modifie dynamiquement cette page en fonction des interactions faites par l'utilisateur. Il interagit avec le serveur web pour récupérer les données dont il a besoin.

L'approche single page se retrouve dans de nombreux frameworks très utilisés tels que VueJS ou ReactJS. Elle transforme l'architecture des applications web en demandant au serveur web de ne renvoyer que des données et non plus des pages web.

Nous décrivons ici le besoin auquel répond cette approche, présentons ses principes de base et proposons une implémentation simpliste qui permet de bien comprendre ses avantages et son mode de fonctionnement.

Les inconvénients de la génération des pages web par le serveur web

Dans tous les chapitres de ce livre nous avons proposé une architecture dans laquelle le serveur web fournit les pages web et toutes les ressources dont elles sont composées. Le navigateur web se contente quant à lui d'effectuer l'affichage (le painting) et peut parfois modifier légèrement la page en fonction des interactions de l'utilisateur. On appelle cette approche multi pages car l'utilisateur visite plusieurs pages web en fonction des interactions qu'il réalise. En effet, dès qu'une requête est envoyée par le navigateur, le serveur retoune une nouvelle page web permettant à l'utilisateur de continuer son parcours.

L'approche multi pages a deux inconvénients principaux. Le premier vient du fait que le chargement d'une nouvelle page par le navigateur implique un rafraichissement intégral du DOM et la création d'un nouvel environnement Javascript vierge (voir chapitre 9). De fait, le navigateur ne peut pas garder de l'information entre les pages qu'il visite à moins d'utiliser des mécanismes tels que les cookies (voir chapitre 10). Le deuxième inconvénient est que le serveur web doit absolument renvoyer des pages web dans toutes ses réponses. Le code du serveur contient donc nécessairement une partie dévolue à la génération du code HTML. Dans l'annexe sur le templating nous avons d'ailleurs présenté l'approche par templating qui permet de séparer cette génération HTML dans des templates.

Prenons l'exemple de notre mur d'images et attachons-nous à la page qui affiche une image (voir Figure 14.1). Dans cette page, un clic sur les images miniatures du bas permet de changer d'image et de passer à l'image précédente ou suivante. Avec l'approche multi pages un tel clic envoie une requête au serveur et lui demande de regénérer complètement une nouvelle page qui est pourtant quasiment identique à la page affichée.

Alt Text

Figure 14.1 : La page qui affiche une image. Les miniatures en bas à droite et à gauche pointent sur l'image précédente et suivante.

Le Code 14.1 présente la partie du code du serveur web relative à la génération de la page qui affiche une image. Ce code est relativement complexe car il fait quatre requêtes SQL pour obtenir les informations sur les images à afficher ainsi que sur les commentaires à intégrer. La génération du HTML est quant à elle relativement simple même si elle gagnerait à être mise dans un template pour faciliter la lecture du code.

else if (req.url.startsWith('/page-image')) {
  let imageId = parseInt(req.url.split('/')[2]);
  const sqlQueryImage = `SELECT * FROM images WHERE id = ${imageId};`; 
  const sqlResultImage = await client.query(sqlQueryImage); 
  const image = {
      id: sqlResultImage.rows[0].id,
      fichier: sqlResultImage.rows[0].fichier,
      nom: sqlResultImage.rows[0].nom,
      href: `/page-image/${sqlResultImage.rows[0].id}`,
      big: `/public/images/${sqlResultImage.rows[0].fichier}`,
      small: `/public/images/${sqlResultImage.rows[0].fichier.split('.jpg')[0]}_small.jpg`
  };
  const sqlQueryPrevImage = `SELECT * FROM images WHERE id < ${imageId} ORDER BY id DESC LIMIT 1;`; 
  const sqlResultPrevImage = await client.query(sqlQueryPrevImage); 
  let imagePrev;
  if (sqlResultPrevImage.rows.length === 1) {
    imagePrev = {
      id: sqlResultPrevImage.rows[0].id,
      fichier: sqlResultPrevImage.rows[0].fichier,
      nom: sqlResultPrevImage.rows[0].nom,
      href: `/page-image/${sqlResultPrevImage.rows[0].id}`,
      big: `/public/images/${sqlResultPrevImage.rows[0].fichier}`,
      small: `/public/images/${sqlResultPrevImage.rows[0].fichier.split('.jpg')[0]}_small.jpg`
    };
  };
  let prevHtml = imagePrev ? `<a href="${imagePrev.href}"><img src="${imagePrev.small}"></a>` : '';

  const sqlQueryNextImage = `SELECT * FROM images WHERE id > ${imageId} ORDER BY id ASC LIMIT 1;`; 
  const sqlResultNextImage = await client.query(sqlQueryNextImage); 
  let imageNext;
  if (sqlResultNextImage.rows.length === 1) {
    imageNext = {
      id: sqlResultNextImage.rows[0].id,
      fichier: sqlResultNextImage.rows[0].fichier,
      nom: sqlResultNextImage.rows[0].nom,
      href: `/page-image/${sqlResultNextImage.rows[0].id}`,
      big: `/public/images/${sqlResultNextImage.rows[0].fichier}`,
      small: `/public/images/${sqlResultNextImage.rows[0].fichier.split('.jpg')[0]}_small.jpg`
    };
  };
  let nextHtml = imageNext ? `<a href="${imageNext.href}"><img src="${imageNext.small}"></a>` : '';

  const sqlQueryComments = `SELECT * FROM commentaires WHERE id_image = ${imageId};`;	
  const sqlResultComments = await client.query(sqlQueryComments); 
  let imageComments = sqlResultComments.rows.map(comment => comment.texte);
  let commentsHtml = imageComments.map(comment => `<div> -- ${comment} -- </div>`).join('');
  html = `
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Image5</title>
        <link rel="stylesheet" href="/public/style.css">
        <script src="/public/page-image.js" defer></script>
    </head>
    <body>
        <a href="/images">Mur</a>
        <div class="center">
            <img src="${image.big}" width="500">
            <p>${image.nom}</p>
            ${commentsHtml.length!=0?`<h4>Commentaires</h4>`:''}
            ${commentsHtml}
            <h4>Ajouter un nouveau commentaire</h4>
            <form action="/image-description" method="POST">
              <input type="hidden" name="numero" id="numero" value="${imageId}">
              <label for="commentaire">Commentaire : </label>
              <input type="text" name="commentaire" id="commentaire">
              <input type="submit" value="Envoyer">
            </form>
        </div>
        <div>
            <span class="left">${prevHtml}</span>
            <span class="right">${nextHtml}</span>
        </div>
    </body>
    </html>
  `;
  res.end(html);
}

Code 14.1 : La partie du code sur serveur qui génère la page d'une image.

Cet exemple de la page qui affiche une seule image illustre les limites de l'approche multi pages. En effet, un clic sur une miniature ne change que très faiblement la page affichée (seules les URLs des trois images changent ainsi que la liste des commentaires). Pour autant, l'intégralité de la page sera regénérée par le serveur web. Il serait bien plus efficace que le navigateur demande au serveur de lui founir uniquement les nouvelles informations relatives aux images et aux commentaires à afficher et qu'il fasse lui-même la modification de la page HTML.

Les principes de base : échanges de données et modification du HTML

L'objectif de l'approche single page est de permettre au navigateur d'effectuer lui-même les changements sur une page web et ce en récupérant les informations auprès du serveur web. Cela nécessite de mettre en place deux principes de base : les échanges de données et la modification du HTML.

Le premier principe est de faire en sorte que le serveur web fournisse des données au navigateur web et non plus des pages web. Les données fournies par le serveur web sont exploitées par le navigateur web pour réaliser l'affichage des pages web. Pour mettre en oeuvre ce principe, il est conseillé de faire des routes simples dans le serveur et que chaque route retourne les données sous format JSON.

Dans l'exemple de la page qui affiche une seule image le serveur web doit être en mesure de fournir les données relatives aux images et les données relatives aux commentaires. Nous proposons donc deux routes, une pour les images et une pour les commentaires. Le Code 14.2 présente le code du serveur web relatif à ces routes. On voit que le code reprend la même logique que le Code 14.1. La séparation en deux routes et le fait qu'il n'y ait plus du tout de génération de HTML aident grandement la lisibilité.

else if (req.url.startsWith('/page-image')) {
  let imageId = parseInt(req.url.split('/')[2]);
  const sqlQueryImage = `SELECT * FROM images WHERE id = ${imageId};`; 
  const sqlResultImage = await client.query(sqlQueryImage); 
  const image = {
      id: sqlResultImage.rows[0].id,
      fichier: sqlResultImage.rows[0].fichier,
      nom: sqlResultImage.rows[0].nom,
      href: `/page-image/${sqlResultImage.rows[0].id}`,
      big: `/public/images/${sqlResultImage.rows[0].fichier}`,
      small: `/public/images/${sqlResultImage.rows[0].fichier.split('.jpg')[0]}_small.jpg`
  };
  const sqlQueryPrevImage = `SELECT * FROM images WHERE id < ${imageId} ORDER BY id DESC LIMIT 1;`; 
  const sqlResultPrevImage = await client.query(sqlQueryPrevImage); 
  let imagePrev;
  if (sqlResultPrevImage.rows.length === 1) {
    imagePrev = {
      id: sqlResultPrevImage.rows[0].id,
      fichier: sqlResultPrevImage.rows[0].fichier,
      nom: sqlResultPrevImage.rows[0].nom,
      href: `/page-image/${sqlResultPrevImage.rows[0].id}`,
      big: `/public/images/${sqlResultPrevImage.rows[0].fichier}`,
      small: `/public/images/${sqlResultPrevImage.rows[0].fichier.split('.jpg')[0]}_small.jpg`
    };
  };

  const sqlQueryNextImage = `SELECT * FROM images WHERE id > ${imageId} ORDER BY id ASC LIMIT 1;`; 
  const sqlResultNextImage = await client.query(sqlQueryNextImage); 
  let imageNext;
  if (sqlResultNextImage.rows.length === 1) {
    imageNext = {
      id: sqlResultNextImage.rows[0].id,
      fichier: sqlResultNextImage.rows[0].fichier,
      nom: sqlResultNextImage.rows[0].nom,
      href: `/page-image/${sqlResultNextImage.rows[0].id}`,
      big: `/public/images/${sqlResultNextImage.rows[0].fichier}`,
      small: `/public/images/${sqlResultNextImage.rows[0].fichier.split('.jpg')[0]}_small.jpg`
    };
  };
  res.end(JSON.stringify({image, imagePrev, imageNext}));
} else if (req.url.startsWith('/comments')) {
  let imageId = parseInt(req.url.split('/')[2]);
  const sqlQueryComments = `SELECT * FROM commentaires WHERE id_image = ${imageId};`;	
  const sqlResultComments = await client.query(sqlQueryComments); 
  let imageComments = sqlResultComments.rows.map(comment => comment.texte);
  res.end(JSON.stringify({comments: imageComments}));
} 

Code 14.2 : La partie du code sur serveur qui fournit les données en JSON.

Le deuxième principe de l'approche single page est que la génération du HTML se fasse intégralement du côté du navigateur. Cela nécessite une exploitation assez importante de Javascript. En outre, il faut désactiver tous les traitements par défaut du navigateur qui entrainent le chargement d'une nouvelle page web (clic sur les hyperliens, soumissions des formulaires, etc.). Il faut de plus proposer un système permettant de récupérer les données du serveur web et effectuer des changements dans la page web.

Le Code 14.3 présente le code de la page qui affiche l'image selon l'approche single page. Ce code intègre une partie Javascript très complexe. Celle-ci définit la fonction updateImage qui récupère les données fournies par le serveur (en exploitant la méthode fetch) et qui met à jour la page web en fonction des données récupérées. On peut voir que deux requêtes sont émises pour récupérer les informations sur les images et sur les commentaires. On voit aussi l'ajout d'une gestion d'événements pour récupérer les clics réalisés sur les miniatures et modifier la page sans la rafraichir complètement. Il faut noter que le code de cette page est incomplet car il n'y a pas de gestion de la soumission du formulaire.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Image</title>
    <link rel="stylesheet" href="/public/style.css">
</head>
<body>
    <a href="/images">Mur</a>
    <div class="center">
        <img id="image" width="500">
        <p id="nom"></p>
        <div id="commentaires">
        </div>

        <h4>Ajouter un nouveau commentaire</h4>
        <form action="/image-description" method="POST">
          <input type="hidden" name="numero" id="numero">
          <label for="commentaire">Commentaire : </label>
          <input type="text" name="commentaire" id="commentaire">
          <input type="submit" value="Envoyer">
        </form>
    </div>
    <div>
        <span id="imagePrev" class="left"></span>
        <span id="imageNext" class="right"></span>
    </div>
</body>

<script>
let imageId = 1;
updateImage(1);

function updateImage(newImageId) {
  imageId = newImageId;
  fetch('/page-image/'+imageId)
  .then(rawData => {
    return rawData.json();
  })
  .then(jsonData => {
    document.getElementById('image').src = jsonData.image.big;
    document.getElementById('nom').innerHTML = jsonData.image.nom;
    document.getElementById('numero').value = jsonData.image.id;
    if (json.imagePrev) {
      let img = document.createElement('img');
      img.src = json.imagePrev.small;
      document.getElementById('imagePrev').appendChild(img);
      img.addEventListener('clic', (e) => {
        updateImage(json.imagePrev.id);
      })
    }
    if (json.imageNext) {
      let img = document.createElement('img');
      img.src = json.imageNext.small;
      document.getElementById('imageNext').appendChild(img);
      img.addEventListener('clic', (e) => {
        updateImage(json.imageNext.id);
      })
    }
  })

  fetch('/comments/'+imageId)
  .then(rawData => {
    return rawData.json();
  })
  .then(jsonData => {
    if (jsonData.comments.length != 0) {
      let html = '<h4>Commentaires</h4>';
      html += jsonData.comments.map(comment => '<div> -- ' + comment + ' -- </div>').join('');
      document.getElementById('commentaires').innerHTML = html;
    }
  })
}
</script>
</html>

Code 14.3 : La page qui affiche une image selon l'approche single page.

L'exemple que nous venons de présenter illustre les deux principes de l'approche single page. Il permet de bien mesurer la différence avec l'approche classique (multi pages). Cela permet aussi de voir la complexité du code Javascript qui s'exécute sur le navigateur. C'est d'ailleurs dans l'objectif de simplifier ce code que plusieurs frameworks ont été proposés.

Fonctionnement d'un framework supportant l'approche single page

Les frameworks single page visent à simplifier le code Javascript du navigateur web. Ils doivent supporter : (1) la génération et la modification des pages HTML dans le navigateur, (2) l'accès aux données qui sont fournies par le serveur web, et (3) la gestion des interactions de l'utilisateur.

Nous proposons ici un framewok minimaliste qui s'inspire du framework VueJS. L'objectif est de comprendre le rôle joué par les frameworks single page et la valeur ajoutée qu'ils apportent. Notre framework est entièrement développé en Javascript dans un fichier nommé mvue.js (voir Code 14.8). Il permet la définition d'une application web qui contient des composants web. L'application web et ses composants sont affichés dans une unique page web et changent en fonction des interactions de l'utilisateur et des données transmises par le serveur web.

Dans notre framework l'application web et les composants web doivent être développés en Javascript dans des fichiers séparés. Pour notre exemple du mur d'images nous avons développé une application web (app.js) qui contient deux composants : le mur (images.js) et l'image (image.js).

L'exploitation de notre framework nécessite de construire une unique page HTML qui inclut notre framework, l'application web et tous les composants qu'elle contient. Cette page HTML est la seule page HTML qui est fournie par le serveur web. Le code 14.4 présente la seule page HTML de notre application du mur d'images réalisée avec notre framework. Cette page HTML référence notre framework (mvue.js), l'application web (app.js), et les deux composants (images.js et image.js). Notons que cette page définit une balise <div> dont l'id est app. Cette balise contiendra le code HTML généré par notre framework.

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mon Mur d'images</title>
    <script src="/public/mvue.js"></script>
    <script src="/public/webcomp/app.js" defer></script>
    <script src="/public/webcomp/images.js" defer></script>
    <script src="/public/webcomp/image.js" defer></script>
    <link rel="stylesheet" href="/public/style.css">
</head>
<body>
    <div class="center" id="app">
    </div>
</body>
</html>

Code 14.4 : L'unique page HTML de notre application du mur d'image avec notre framework mvue.js.

Notre framework propose une seule fonction principale (nommée createApp) qui permet de créer une application. Cette fonction prend en entrée un template qui sera exécuté pour afficher l'application (voir Code 14.8). Elle retourne un objet qui représente l'application web. Dans notre exemple, c'est l'application app.js qui appelle cette fonction. Nous suivons la même convention que VueJs et stockons l'objet qui représente l'application web dans une variable globale appelée app. Ainsi fait tous les scripts Javascript ont accès à cette variable globale et peuvent ainsi manipuler l'application web pour, entre autres, y ajouter des composants.

L'objet app propose trois méthodes : mount, component et route. La méthode mount doit être appelée pour intégrer l'application dans un des éléments de la page HTML. La méthode component permet d'ajouter un composant web dans l'application. Elle prend en entrée le nom du composant et une fonction qui permet de construire le composant. Les composants images.js et image.js appellent cette méthode. Enfin, la méthode route permet de lier un composant web à une route ce qui permet de gérer la navigation entre les composants. Là encore, les deux composants web de notre exemple utilisent cette méthode.

Pour la génération et la modification du code HTML de l'application et de ses composants, nous avons fait le choix d'intégrer dans notre framework une approche de templating. Celle-ci est réalisée dans le navigateur web (et non pas par le serveur tel que cela est présenté dans l'annexe sur le templating). Ce choix est aussi appliqué par le framework VueJS. Notre framework contient donc deux fonctions (parse et createGenerateFunction) qui implémentent cette approche de templating (voir Code 14.8). Ces fonctions sont appelées par la fonction renderComponent qui réalise l'affichage de l'application et des composants web. Notons que les templates sont directement intégré dans l'application web et les composants qui définissent chacun leur propre template. Pour l'application web, le template est passé en paramètre de la fonction createApp. Pour les composants web, le template doit être défini dans la propriété template de l'objet qui est retourné par la fonction de création des composants.

Pour la gestion des données, chaque composant définit sa méthode d'accès aux données dans la propriété data de l'objet qui est retourné par la fonction de création du composant. Par exemple, pour le composant qui correspond au mur d'images, sa fonction d'accès aux données fait appel au serveur web pour récupérer toutes les informations relatives aux images (voir Code 14.5). Notons que la méthode d'accès aux données est exécutée par notre framework pendant l'appel à la fonction d'affichage renderComponent.

Pour la gestion des interactions, chaque composant définit les traitements qu'il réalise en fonction des événements des utilisateurs. Cela se fait dans la propriété events de l'objet retourné par la fonction de création du composant. Dans notre exemple, le composant qui gère l'affichage d'une image réalise un traitement lorsque le formulaire d'ajout de commentaire est soumis (voir Code 14.6).

Enfin, notre framework propose un routage entre les composants web en exploitant la partie ancre d'une URL et en ajoutant un listener sur l'événement hashchange. La méthode route permet alors d'enregistrer un composant à une ancre pour que le framework l'affiche. Par exemple, l'instruction app.route('/images','images'); ajoute le composant images à la route /images. Ainsi, en considérant que l'application est exécuté sur la machine locale, un accès à l'URL https://localhost/#/images route vers le composant du mur d'images.

let appTemplate = 
    `<img src="/public/logo.png" alt="logo">
    <p>
        Vous trouverez ici toutes les images que j'aime.
    </p>
    <div>
        <a href="#/page-image/1"><img src="/public/images/image1_small.jpg"></a>
        <a href="#/page-image/2"><img src="/public/images/image2_small.jpg"></a>
        <a href="#/page-image/3"><img src="/public/images/image3_small.jpg"></a>
    </div>
    <a href="#/images">Toutes les Images</a>`;

let app = createApp(appTemplate);

app.mount('#app');

console.log('app is ok');

Code 14.5 : Le composant app.js qui représente l'application web.

app.component('images',(props) => {
    return {
        template: `<a href="#/index">Index</a>
                    <div class="center">
                        <h1>Mur d'images</h1>
                    </div>
                    <div id="mur">
                        <% for (let image of images) { %>
                            <a href="#/image?imageId=<%=image.split('image')[1]%>"><img src="/public/images/<%=image%>_small.jpg"></a>
                        <% } %>
                    </div>`,
        data: () => {
            return fetch('/images')
            .then((res) => {
                if (res) {
                    return res.json();
                } else {
                    Promise.reject();
                }
            })
        }
    }
})

app.route('/images','images');

console.log('images is ok');

Code 14.6 : Le composant images.js qui représente le mur d'images.

app.component('image',(props) => {
    return {
        template: `<a href="#/images">Mur</a>
        <div class="center">
            <img src="/public/images/image<%=imageId%>.jpg" width="500">
            <p>Magnifique Image</p>
            <% if (comments.length > 0) { %>
                <h4>Commentaires</h4>
            <% } %>
            <%= comments.map(comment => '<div> -- ' + comment + ' -- </div>').join('') %>
            <h4>Ajouter un nouveau commentaire</h4>
            <form action="/image-description" method="POST">
            <input type="hidden" name="numero" id="numero" value="<%=imageId%>">
            <label for="commentaire">Commentaire : </label>
            <input type="text" name="commentaire" id="commentaire">
            <input type="submit" value="Envoyer">
        </form>
        </div>
        <div>
            <span class="left">
                <% if (parseInt(prevId)>0) { %>
                    <a href="#/image?imageId=<%=prevId%>"><img src="/public/images/image<%=prevId%>_small.jpg"></a>
                <% } %>
            </span>
            <span class="right">
                <% if (parseInt(nextId)<=images.length) { %>
                    <a href="#/image?imageId=<%=nextId%>"><img src="/public/images/image<%=nextId%>_small.jpg"></a>
                <% } %>
            </span>
        </div>`,
        data: () => {
            return Promise.all([fetch('/images'),fetch('/comments/'+props.imageId)])
            .then(([imagesRes,commentsRes]) => {
                if (imagesRes && commentsRes) {
                    return Promise.all([imagesRes.json(), commentsRes.json()]);
                } else {
                    Promise.reject();
                }
            })
            .then(([imagesJSON, commentsJSON]) => {
                return {
                    imageId : parseInt(props.imageId),
                    images : imagesJSON.images,
                    comments : commentsJSON.comments,
                    prevId : parseInt(props.imageId)-1,
                    nextId : parseInt(props.imageId)+1
                }
            })
        },
        events: [
            {
                kind: 'submit',
                target: 'FORM',
                listener: (e) => {
                    e.preventDefault();
                    let commentaire = document.getElementById('commentaire').value;
                    let params = {
                        "imageNumber": props.imageId,
                        "description": commentaire
                    }
                    let formBody = [];
                    for (var property in params) {
                        let encodedKey = encodeURIComponent(property);
                        let encodedValue = encodeURIComponent(params[property]);
                        formBody.push(encodedKey + "=" + encodedValue);
                    }
                    formBody = formBody.join("&");
                    
                    fetch('/image-description', {method:'POST', body: formBody})
                    .then((res)=> {
                        alert('Le commentaire a été pris en compte');
                    })
                }
            }
        ]
    }
})

app.route('/image','image');

console.log('image is ok');

Code 14.7 : Le composant image.js qui représente une seule image.

function createApp(template) {
    const HTML = "HTML";
    const JS = "JS";
    const JS_VAL = "JS_VAL";
    let _template = template;
    let _componentFactories = [];
    let _routes = [];
    let _root;

    return {
        mount : function(selector) {
            _root = document.querySelector(selector);
            renderComponent({template:_template, data: ()=> Promise.resolve({})}, _root);
        },
        component : function(name, componentFactory) {
            _componentFactories[name] = componentFactory;
        },
        route(route, componentName) {
            _routes[route] = componentName;
        }
    }

    function renderComponent(component, root) {
        return component.data()
        .then((data) => {
            console.log('data in render');
            console.log(JSON.stringify(data));
            let parts = parse(component.template);
            let generateFunction = createGenerateFunction(parts, data);
            root.innerHTML = generateFunction();
        })
    }

    function parse(template) {
        let parts = [];
        let current = {type: HTML, value: ''};
        for (let i=0 ; i < template.length ; i++) {
            if (current.type === HTML) {
                if (i < template.length-1 && template[i] === '<' && template[i+1] === '%') {
                    parts.push(current);
                    if (i < template.length-2 && template[i+2] === '=') {
                        current = {type: JS_VAL, value: ''};
                        i += 2;
                    } else {
                        current = {type: JS, value: ''};
                        i++;
                    }
                } else {
                    current.value += template[i];
                }   
            } else {
                if (i < template.length-1 && template[i] === '%' && template[i+1] === '>') {
                    parts.push(current);
                    current = {type: HTML, value: ''};
                    i++;
                } else {
                    current.value += template[i];
                }   
            }
        }
        if (current.value !== '') {
            parts.push(current);
        }
        return parts;
    }

    function createGenerateFunction(templateParts, parameters) {
        let dataAsVariable = Object.getOwnPropertyNames(parameters).map(p => `let ${p} = ${JSON.stringify(parameters[p])};\n`).join('');
        let rendering = {type:HTML, value:dataAsVariable+'let html = `'};
        for (let i = 0; i < templateParts.length; i++) {
            if (rendering.type === HTML) {
                if (templateParts[i].type === HTML) {
                    rendering.value += templateParts[i].value;
                } else if (templateParts[i].type === JS) {
                    rendering.value += '`;\n';
                    rendering.value += templateParts[i].value; 
                    rendering.type = JS;
                } else {
                    rendering.value += '${'+templateParts[i].value+'}';
                }
            } else if (rendering.type === JS) {
                if (templateParts[i].type === HTML) {
                    rendering.value += 'html += `' + templateParts[i].value;
                    rendering.type = HTML
                } else if (templateParts[i].type === JS) {
                    rendering.value += templateParts[i].value; 
                } else {
                    rendering.value += 'html += `${'+templateParts[i].value+'}'
                }
            }  
        }
        if (rendering.type === HTML) {
            rendering.value += '`;';
        }
        rendering.value += 'return html';
        return new Function(rendering.value);
    }

    function addListeners(component) {
        if (component.events && component.events.length > 0) {
            component.events.forEach((e) => {
                console.log(JSON.stringify(e));
                document.querySelector(e.target).addEventListener(e.kind, e.listener);
            })
        }
    } 

    window.addEventListener('hashchange', (e) => {
        let route = window.location.hash.substring(1).split('?')[0];
        console.log(route);
        if (_routes[route]) {
            console.log(_routes[route]);
            let componentFactory = _componentFactories[_routes[route]];
            if (componentFactory) {
                console.log('there is a factory');
                let params = {};
                if (window.location.hash.substring(1).split('?')[1]) {
                    window.location.hash.substring(1).split('?')[1].split('&').forEach((nameValue) => {
                        let name = nameValue.split('=')[0];
                        let value = nameValue.split('=')[1];
                        params[name] = value;
                    });
                }
                e.preventDefault();
                let component = componentFactory(params);
                renderComponent(component, _root).then(()=> {addListeners(component)});
            }
        }
    });
}

Code 14.8 : Le code de notre framework mvue.js.

Ce qu'il faut retenir

Cette annexe présente l'approche single page. Il faut retenir les trois points suivants:

  • Le principe d'une approche single page est de ne charger qu'une seule page HTML et de la modifier dynamiquement en fonction des interactions de l'utilisateur.
  • Dans une approche single page le serveur web ne fournit plus de HTML mais uniquement des données (au format JSON).
  • Les frameworks qui supportent le single page gère l'affichage et la modification du HTML, l'accès aux données ainsi que les interactions de l'utilisateur.