Cette annexe présente le templating qui est une technique très utilisée pour générer dynamiquement des pages web. Le templating se retrouve dans de nombreux langages de programmation que cela soit nativement pour PHP ou sous forme de framework pour les autres languages (EJS pour Node, JSP pour Java, ASP pour .Net et C#, etc.).
Nous décrivons ici le besoin auquel répond le templating, présentons ses principes de base et proposons une petite implémentation de cette approche permettant de bien comprendre ses avantages et son mode de fonctionnement.
La génération dynamique de pages web consiste à construire et renvoyer une chaîne de caractères qui doit respecter le format HTML. Lors de la construction de cette chaîne de caractères il faut intégrer des éléments qui sont calculés à partir d'informations stockées dans une base de données ou fournies dans la requête reçue par le serveur web.
Dans le Chapitre 4 nous avons montré la génération dynamique du mur d'images de notre example fil rouge.
Le Code 13.1 présente cette génération.
La construction de la chaîne de caractères HTML démarre à la ligne 19.
On voit que la variable html est déclarée et qu'elle contient les caractères HTML.
L'ajout des informations dynamiques se fait dans les lignes 23 à 25 avec la boucle for qui va créer une balise <img>
par image contenue dans le répertoire public.
Enfin, la génération se termine avec la fermeture des balises <body>
et <html>
.
Une fois que la chaîne de caractères est construite il ne reste plus qu'à la renvoyer vers le navigateur.
const fs = require("fs");
const http = require("http");
const host = 'localhost';
const port = 8080;
const server = http.createServer();
server.on("request", (req, res) => {
if (req.url.startsWith('/public/')) {
try {
const fichier = fs.readFileSync('.'+req.url);
res.end(fichier);
} catch (err) {
console.log(err);
res.end("erreur ressource");
}
} else if (req.url === '/mur-image') {
let files = fs.readdirSync('./public');
let sFiles = files.filter(f => f.endsWith('_small.jpg'));
let html = '<!DOCTYPE html><html lang="fr">';
html += '<head><title>Mur d\'images</title></head>';
html += '<body> <h1>Mur</h1>';
for (let f of sFiles) {
html += '<img src="/public/' + f + '">';
}
html += '</body></html>';
res.end(html);
} else {
res.end("erreur URL");
}
});
server.listen(port, host, () => {
console.log(`Server running at http://${host}:${port}/`);
});
Code 13.1 : Serveur web qui génère dynamiquement un mur d'images.
La partie génération HTML du Code 13.1 n'est pas très lisible. Déjà, le code HTML est intégré sous forme d'une simple chaîne de caractères ce qui fait que l'éditeur de code ne sait pas que c'est du HTML et ne peut donc pas proposer une coloration syntaxique qui aide à la lisibilité. De plus, dans une chaîne de caractères il faut protéger les caractères spéciaux ; ce qui est le cas pour le texte contenu dans la ligne 21 où l'on voit qu'il faut protéger l'apostrophe. La protection des caractères perturbe la lecture du HTML. Enfin la boucle for rend la lecture plus difficile car il faut comprendre que la ligne 24 va ajouter une balise par image et que le nom de l'image (variable f) est utilisé dans la propriété src de la balise de l'image.
Cet exemple de code illustre les inconvénients de l'approche par concaténation de chaînes de caractères. Le fait que le code HTML apparaisse comme une simple chaîne de caractères et qu'il soit noyé dans du code Javascript rend la lecture difficile. L'idéal serait de partir d'une page HTML puis de préciser où doivent être intégrées les informations dynamiques : c'est l'idée de base du templating.
Le principe de base du templating est de coder le HTML tel qu'on voudrait qu'il soit généré et de préciser comment intégrer les informations dynamiques.
A titre d'exemple le Code 13.2 présente un template qui illustre ce que pourrait être le mur d'images avec une approche de templating.
On voit que HTML est le langage de base du template ce qui permet une lecture plus aisée et aide à mieux comprendre le résultat qui sera généré.
On voit clairement que certaines parties du template sont des directives qui permettent d'intégrer les informations dynamiques.
Nous avons mis ici ces directives entre les balises <% %>
ce qui permet de rapidement les identifier (cette notation est celle du framework EJS).
<!DOCTYPE html>
<html lang="fr">
<head>
<title>Mur d'images</title>
</head>
<body>
<h1>Mur</h1>
<% for (let f of sFiles) { %>
<img src="/public/<%=f%>">
<% } %>
</body>
</html>
Code 13.2 : Le template du mur d'images.
Un template de page HTML, tel que celui présenté dans le Code 13.2, doit être exécuté pour générer le document HTML correspondant. L'exécution d'un template doit être faite par le serveur web avant l'envoi du document HTML.
Tous les frameworks qui supportent l'approche par templating proposent un mécanisme d'exécution des templates. A titre d'exemple, le Code 13.3 illustre l'exploitation du framework EJS avec l'appel de la méthode render pour exécuter le template du mur de template (variable mur). Plus précisément, la ligne 1 importe le framework. La ligne 4 appelle la méthode render qui exécute le template mur en fournissant sFiles comme paramètre. La méthode render renvoie la chaîne de caractères qui correspond au HTML généré. La ligne 5 retourne enfin le HTML vers le navigateur web.
let ejs = require('ejs');
let files = fs.readdirSync('./public');
let sFiles = files.filter(f => f.endsWith('_small.jpg'));
const html = ejs.render('mur',{sFiles});
res.end(html);
Code 13.3 : Serveur web qui exécute dynamiquement un mur d'images.
Les lignes du Code 13.3 remplacent les lignes 17 à 27 du Code 13.2. On voit que le code HTML est dans le template et plus dans le serveur web. Cette séparation entre le template et le serveur facilite la lecture du code. Un autre intérêt, que nous ne montrerons pas dans nos exemples, est qu'il est même possible de composer les templates et de les assembler pour construire des pages web complexes. Toutes ces possibilités offertes par les templates facilitent grandement la génération de pages web dynamiques.
Un framework qui supporte l'approche par templating doit : (1) proposer une approche pour intégrer les directives dans le code HTML et, (2) fournir un moyen pour exécuter les templates et générer le HTML. Nous proposons ici de coder un tel framework dans l'objectif de mieux comprendre le fonctionnement interne de l'approche templating.
Le framework que nous proposons s'inspire de EJS.
L'intégration des directives se fait avec deux balises : <% %>
et <%= %>
.
La balise <% %>
intègre du Javascript qui sera exécuté lors de l'exécution du template.
La balise <%= %>
intègre du Javascript qui sera lui aussi exécuté et dont la valeur de retour sera concaténée au HTML généré.
Le Code 13.2 est donc compatible avec notre framework.
L'exécution d'un template par notre framework se fait en trois étapes.
La première étape consiste à lire le template et à isoler les parties HTML des parties contenant les deux types de directive (<% %>
ou <%= %>
).
La deuxième étape consiste à construire une fonction Javascript dont le code permet de générer le HTML correspondant au template.
La troisième étape consiste à exécuter la fonction de génération pour obtenir le HTML.
Pour la première étape nous proposons la fonction parse qui lit les caractères du template les uns après les autres et qui détecte les parties HTML, JS (balise <% %>
) et JS_VAL (balise <%= %>
) du template.
La fonction parse retourne un tableau qui contient les parties du template avec leur type (HTML, JS ou JS_VAL).
Le Code 13.4 présente la fonction parse.
La boucle for est la boucle principale. Elle itère sur tous les caractères du template, détecte les balises, construit les parties du template et les ajoute dans le tableau parts.
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;
}
Code 13.4 : La fonction parse qui identifie les différentes parties d'un template.
En exécutant la fonction parse sur l'exemple du Code 13.2 on obtient alors un tableau avec sept parties (voir Code 13.5).
[{
"type": "HTML",
"value": "<!DOCTYPE html><html lang='fr'><head><title>Mur d'images</title></head><body><h1>Mur</h1>"
}, {
"type": "JS",
"value" : "for (let f of sFiles) {"
}, {
"type": "HTML",
"value": "<img src='/public/"
}, {
"type": "JS_VAL",
"value": "f"
}, {
"type": "HTML",
"value": "'>"
}, {
"type": "JS",
"value": "}"
}, {
"type": "HTML",
"value": "</body></html>"
}]
Code 13.5 : Le résultat obtenu en "parsant" le template exemple.
La deuxième étape de notre framework consiste à créer une fonction de génération. Cette fonction est créée à partir d'un template et à partir d'un ensemble de paramètres. Une fois exécutée elle génère le HTML correspondant au template avec les paramètres donnés. Le Code 13.6 présente la fonction créée à partir du template du Code 13.2 avec comme paramètre un tableau avec deux images (image1 et image2). L'exécution de cette fonction de génération retourne le code HTML du mur d'images.
function () {
let sFiles = ["image1","image2"];
let html = `<!DOCTYPE html>
<html lang="fr">
<head>
<title>Mur d'images</title>
</head>
<body>
<h1>Mur</h1>
`;
for (let f of sFiles) {
html += `<img src="/public/${f}">`;
}
html += `</body></html>`;
return html
}
Code 13.6 : La fonction créée à partir du template du Code 13.2 et avec image1 et image2 comme paramètre. Une fois exécutée cette fonction retourne le document HTML du mur d'images.
Pour réaliser cette deuxième étape nous avons fait le choix de développer une fonction qui prend en entrée les parties du template ainsi que les paramètres nécessaires à l'exécution du template, et qui fournit en sortie la fonction de génération (voir Code 13.7). Cette approche est similaire à ce que fait le framework EJS. L'idée principale est d'itérer sur les parties du template et de générer le code source correspondant. La génération du code source dépend du type de la partie du template visitée (HTML, JS et JS_VAL) ainsi que du contexte courant de la génération de code. Dès lors que le code source est généré on peut construire un objet Function qui correspond à la fonction de génération (dernière ligne du Code 13.7 )
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);
}
Code 13.7 : La fonction qui construit la fonction de génération de HTML à partir d'un template et de paramètres.
La troisième et dernière étape de notre framework consiste à exécuter la fonction de génération afin d'obtenir le code HTML. Le Code 13.8 commence par la lecture du template dans le fichier puis montre ces trois étapes. On voit la première étape qui consiste à découper le template en parties, la deuxième étape qui consiste à créer la fonction de génération et la troisième et dernière étape qui consiste à exécuter la fonction de génération.
function render(file, parameters) {
const template = fs.readFileSync(file, { encoding: 'utf8' });
const templateParts = parse(template);
const generate = createGenerateFunction(templateParts, parameters)
return generate();
}
Code 13.8 : La fonction render.
Le framework que nous proposons ici reprend la conception du framework EJS. Il permet de bien comprendre que le templating permet de traduire un template en une fonction qui va générer le document HTML. Du point de vue du développeur l'utilisation d'un tel framework permet de développer des templates qui ressemblent fortement à des pages HTML puis à exploiter le framework pour les exécuter et ainsi obtenir les documents HTML correspondants.
Cette annexe présente la technique du templating qui permet de générer dynamiquement des pages web. Il faut retenir les trois points suivants: